[
  {
    "path": ".gitattributes",
    "content": "*.bat text eol=crlf\n*.cmd text eol=crlf\n*.bin binary\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# CODEOWNERS file for Delta Lake\n# This file defines code owners who must approve changes to specific files/directories.\n# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners\n\n# Build configuration files and directories\n/build/                         @tdas\n/build.sbt                      @tdas\n/project/                       @tdas\n/version.sbt                    @tdas\n\n# Spark V2 and Unified modules\n/spark/v2/                      @tdas @huan233usc @TimothyW553 @raveeram-db @murali-db\n/spark-unified/                 @tdas @huan233usc @TimothyW553 @raveeram-db @murali-db\n\n# All files in the root directory\n/*                              @tdas\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-issue.md",
    "content": "---\nname: Bug Issue\nabout: Use this template for reporting a bug\nlabels: 'bug'\ntitle: '[BUG]'\n\n---\n\n## Bug\n\n#### Which Delta project/connector is this regarding?\n<!--\nPlease add the component selected below to the beginning of the issue title\nFor example: [BUG][Spark] Title of my issue\n-->\n\n- [ ] Spark\n- [ ] Standalone\n- [ ] Flink\n- [ ] Kernel\n- [ ] Other (fill in here)\n\n### Describe the problem\n\n#### Steps to reproduce\n\n<!--\nPlease include copy-pastable code snippets if possible.\n1. _____\n2. _____\n3. _____\n-->\n\n#### Observed results\n\n<!-- What happened?  This could be a description, log output, etc. -->\n\n#### Expected results\n\n<!-- What did you expect to happen? -->\n\n#### Further details\n\n<!--\nInclude any additional details that may be useful for diagnosing the problem here. If including tracebacks, please include the full traceback. Large logs and files should be attached.\n-->\n\n### Environment information\n\n* Delta Lake version:\n* Spark version:\n* Scala version:\n\n### Willingness to contribute\n\nThe Delta Lake Community encourages bug fix contributions. Would you or another member of your organization be willing to contribute a fix for this bug to the Delta Lake code base?\n\n- [ ] Yes. I can contribute a fix for this bug independently.\n- [ ] Yes. I would be willing to contribute a fix for this bug with guidance from the Delta Lake community.\n- [ ] No. I cannot contribute a bug fix at this time.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature Request\nabout: Use this template for raising a feature request\nlabels: 'enhancement'\ntitle: '[Feature Request]'\n\n---\n\n## Feature request\n\n#### Which Delta project/connector is this regarding?\n<!--\nPlease add the component selected below to the beginning of the issue title\nFor example: [Feature Request][Spark] Title of my issue\n-->\n\n- [ ] Spark\n- [ ] Standalone\n- [ ] Flink\n- [ ] Kernel\n- [ ] Other (fill in here)\n\n### Overview\n\n<!-- Provide a high-level description of the feature request. -->\n\n### Motivation\n\n<!-- How will this feature be used? Why is it important? Which users will benefit from it? -->\n\n### Further details\n\n<!--\nUse this section to include any additional information about the feature. If you have a proposal for how to implement this feature, please include it here. For implementation guidelines, please read our contributor guidelines: https://github.com/delta-io/delta/blob/master/CONTRIBUTING.md\nIf there are any specific requirements for this feature that are not immediately obvious please outline them here.\n-->\n\n### Willingness to contribute\n\nThe Delta Lake Community encourages new feature contributions. Would you or another member of your organization be willing to contribute an implementation of this feature?\n\n- [ ] Yes. I can contribute this feature independently.\n- [ ] Yes. I would be willing to contribute this feature with guidance from the Delta Lake community.\n- [ ] No. I cannot contribute this feature at this time."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/protocol-rfc.md",
    "content": "---\nname: Protocol Change Request\nabout: Use this template to propose a new feature that impacts the Delta protocol specification\nlabels: 'protocol'\ntitle: '[PROTOCOL RFC]'\n\n---\n\n## Protocol Change Request\n\n### Description of the protocol change\n\n<!--\nPlease describe the motivation and high-level description of the protocol change you are proposing.\nFor a fairly large protocol change, it is recommended that you provide a design doc - (e.g., a google doc, preferably with the ability to comment in the doc). \nFor the next steps on how to proceed with the request, see the protocol RFC process in https://github.com/delta-io/delta/tree/master/protocol_rfcs\n--> \n\n\n### Willingness to contribute\n\nThe Delta Lake Community encourages protocol innovations. Would you or another member of your organization be willing to contribute this feature to the Delta Lake code base?\n\n- [ ] Yes. I can contribute.\n- [ ] Yes. I would be willing to contribute with guidance from the Delta Lake community.\n- [ ] No. I cannot contribute at this time.\n\n\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nThanks for sending a pull request!  Here are some tips for you:\n  1. If this is your first time, please read our contributor guidelines: https://github.com/delta-io/delta/blob/master/CONTRIBUTING.md\n  2. If the PR is unfinished, add '[WIP]' in your PR title, e.g., '[WIP] Your PR title ...'.\n  3. Be sure to keep the PR description updated to reflect all changes.\n  4. Please write your PR title to summarize what this PR proposes.\n  5. If possible, provide a concise example to reproduce the issue for a faster review.\n  6. If applicable, include the corresponding issue number in the PR title and link it in the body.\n-->\n\n#### Which Delta project/connector is this regarding?\n<!--\nPlease add the component selected below to the beginning of the pull request title\nFor example: [Spark] Title of my pull request\n-->\n\n- [ ] Spark\n- [ ] Standalone\n- [ ] Flink\n- [ ] Kernel\n- [ ] Other (fill in here)\n\n## Description\n\n<!--\n- Describe what this PR changes.\n- Describe why we need the change.\n \nIf this PR resolves an issue be sure to include \"Resolves #XXX\" to correctly link and close the issue upon merge.\n-->\n\n## How was this patch tested?\n\n<!--\nIf tests were added, say they were added here. Please make sure to test the changes thoroughly including negative and positive cases if possible.\nIf the changes were tested in any way other than unit tests, please clarify how you tested step by step (ideally copy and paste-able, so that other reviewers can test and check, and descendants can verify in the future).\nIf the changes were not tested, please explain why.\n-->\n\n## Does this PR introduce _any_ user-facing changes?\n\n<!--\nIf yes, please clarify the previous behavior and the change this PR proposes - provide the console output, description and/or an example to show the behavior difference if possible.\nIf possible, please also clarify if this is a user-facing change compared to the released Delta Lake versions or within the unreleased branches such as master.\nIf no, write 'No'.\n-->\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: \"Delta Build\"\non:\n  push:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\njobs:\n  test:\n    name: \"Build Test\"\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Install Java 17\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.9'\n\n      - name: Cache Scala, SBT\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2\n            ~/.cache/coursier\n          key: delta-sbt-cache-cross-spark\n\n      - name: Run cross-Spark build test\n        run: python project/tests/test_cross_spark_publish.py\n"
  },
  {
    "path": ".github/workflows/flink_test.yaml",
    "content": "name: \"Delta Flink\"\n\non: [push, pull_request]\n\n# Cancel previous runs when new commits are pushed\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\nenv:\n  # Point SBT to our cache directories for consistency\n  SBT_OPTS: \"-Dsbt.coursier.home-dir=/home/runner/.cache/coursier -Dsbt.ivy.home=/home/runner/.ivy2\"\n\njobs:\n  test:\n    name: \"DF\"\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Show runner specs\n        run: |\n          echo \"=== GitHub Runner Specs ===\"\n          echo \"CPU cores: $(nproc)\"\n          echo \"CPU info: $(lscpu | grep 'Model name' | cut -d':' -f2 | xargs)\"\n          echo \"Total RAM: $(free -h | grep '^Mem:' | awk '{print $2}')\"\n          echo \"Available RAM: $(free -h | grep '^Mem:' | awk '{print $7}')\"\n          echo \"Disk space: $(df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}')\"\n          echo \"Runner OS: ${{ runner.os }}\"\n          echo \"Runner arch: ${{ runner.arch }}\"\n      - name: Checkout code\n        uses: actions/checkout@v4\n      # Run unit tests with JDK 17. These unit tests depend on Spark, and Spark 4.0+ is JDK 17.\n      - name: install java\n        uses: actions/setup-java@v4\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Cache SBT and dependencies\n        id: cache-sbt\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2/cache\n            ~/.coursier/cache\n            ~/.cache/coursier\n          key: sbt-flink\n      - name: Check cache status\n        run: |\n          if [ \"${{ steps.cache-sbt.outputs.cache-hit }}\" == \"true\" ]; then\n            echo \"✅ Cache HIT - using cached dependencies\"\n          else\n            echo \"❌ Cache MISS - will download dependencies\"\n          fi\n      - name: Run unit tests\n        run: |\n          build/sbt flinkGroup/test\n"
  },
  {
    "path": ".github/workflows/iceberg_test.yaml",
    "content": "name: \"Delta Iceberg Latest\"\non:\n  push:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\njobs:\n  test:\n    name: \"DIL: Scala ${{ matrix.scala }}\"\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        # These Scala versions must match those in the build.sbt\n        scala: [2.13.16]\n    env:\n      SCALA_VERSION: ${{ matrix.scala }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Cache Scala, SBT\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2\n            ~/.cache/coursier\n          # Change the key if dependencies are changed. For each key, GitHub Actions will cache the\n          # the above directories when we use the key for the first time. After that, each run will\n          # just use the cache. The cache is immutable so we need to use a new key when trying to\n          # cache new stuff.\n          key: delta-sbt-cache-spark4.0-scala${{ matrix.scala }}\n      - name: Install Job dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git\n          sudo apt install libedit-dev\n          curl -LO https://github.com/bufbuild/buf/releases/download/v1.28.1/buf-Linux-x86_64.tar.gz\n          mkdir -p ~/buf\n          tar -xvzf buf-Linux-x86_64.tar.gz -C ~/buf --strip-components 1\n          rm buf-Linux-x86_64.tar.gz\n          sudo apt install python3-pip --fix-missing\n          sudo pip3 install pipenv==2024.4.1\n          curl https://pyenv.run | bash\n          export PATH=\"~/.pyenv/bin:$PATH\"\n          eval \"$(pyenv init -)\"\n          eval \"$(pyenv virtualenv-init -)\"\n          pyenv install 3.8.18\n          pyenv global system 3.8.18\n          pipenv --python 3.8.18 install\n      - name: Run Scala/Java and Python tests\n        # when changing TEST_PARALLELISM_COUNT make sure to also change it in spark_master_test.yaml\n        run: |\n          TEST_PARALLELISM_COUNT=4 pipenv run python run-tests.py --group iceberg --spark-version 4.0\n"
  },
  {
    "path": ".github/workflows/kernel_docs.yaml",
    "content": "# Simple workflow for deploying static content to GitHub Pages\nname: Deploy static content to Pages\n\non:\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  # Single deploy job since we're just deploying\n  deploy_docs:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"11\"\n      - name: Generate docs\n        run: |\n          build/sbt kernelGroup/unidoc\n          mkdir -p kernel/docs/snapshot/kernel-api/java\n          mkdir -p kernel/docs/snapshot/kernel-defaults/java\n          cp -r kernel/kernel-api/target/javaunidoc/. kernel/docs/snapshot/kernel-api/java/\n          cp -r kernel/kernel-defaults/target/javaunidoc/. kernel/docs/snapshot/kernel-defaults/java/\n      - name: Setup Pages\n        uses: actions/configure-pages@v3\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v1\n        with:\n          # Upload kernel docs\n          path: kernel/docs\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v2\n"
  },
  {
    "path": ".github/workflows/kernel_test.yaml",
    "content": "name: \"Delta Kernel\"\n\non:\n  push:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n\n# Cancel previous runs when new commits are pushed\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\nenv:\n  # Point SBT to our cache directories for consistency\n  SBT_OPTS: \"-Dsbt.coursier.home-dir=/home/runner/.cache/coursier -Dsbt.ivy.home=/home/runner/.ivy2\"\n\njobs:\n  test:\n    name: \"DK: Shard ${{ matrix.shard }}\"\n    runs-on: ubuntu-24.04\n    strategy:\n      fail-fast: false # Allow all shards to run even if one fails\n      matrix:\n        shard: [0, 1, 2, 3]\n    env:\n      SCALA_VERSION: 2.13.16\n      NUM_SHARDS: 4\n      DISABLE_UNIDOC: true # Another unidoc workflow will test unidoc.\n      TEST_PARALLELISM_COUNT: 4\n    steps:\n      - name: Show runner specs\n        run: |\n          echo \"=== GitHub Runner Specs ===\"\n          echo \"CPU cores: $(nproc)\"\n          echo \"CPU info: $(lscpu | grep 'Model name' | cut -d':' -f2 | xargs)\"\n          echo \"Total RAM: $(free -h | grep '^Mem:' | awk '{print $2}')\"\n          echo \"Available RAM: $(free -h | grep '^Mem:' | awk '{print $7}')\"\n          echo \"Disk space: $(df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}')\"\n          echo \"Runner OS: ${{ runner.os }}\"\n          echo \"Runner arch: ${{ runner.arch }}\"\n      - name: Checkout code\n        uses: actions/checkout@v4\n      # Run unit tests with JDK 17. These unit tests depend on Spark, and Spark 4.0+ is JDK 17.\n      - name: install java\n        uses: actions/setup-java@v4\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Cache SBT and dependencies\n        id: cache-sbt\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2/cache\n            ~/.coursier/cache\n            ~/.cache/coursier\n          key: sbt-kernel-${{ runner.os }}-scala${{ env.SCALA_VERSION }}\n      - name: Check cache status\n        run: |\n          if [ \"${{ steps.cache-sbt.outputs.cache-hit }}\" == \"true\" ]; then\n            echo \"✅ Cache HIT - using cached dependencies\"\n          else\n            echo \"❌ Cache MISS - will download dependencies\"\n          fi\n      - name: Run unit tests\n        run: |\n          python run-tests.py --group kernel --coverage --shard ${{ matrix.shard }}\n\n  integration-test:\n    name: \"DK: Integration\"\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v3\n      # Run integration tests with JDK 11, as they have no Spark dependency\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"11\"\n      - name: Run integration tests\n        run: |\n          cd kernel/examples && python run-kernel-examples.py --use-local\n"
  },
  {
    "path": ".github/workflows/kernel_unitycatalog_test.yaml",
    "content": "name: \"Kernel Unity Catalog\"\non:\n  push:\n    paths:\n      - 'build.sbt'\n      - 'version.sbt'\n      - 'kernel/**/*.scala'\n      - 'kernel/**/*.java'\n      - 'storage/**/*.scala'\n      - 'storage/**/*.java'\n      - '.github/workflows/kernel_unitycatalog_test.yaml'\n  pull_request:\n    paths:\n      - 'build.sbt'\n      - 'version.sbt'\n      - 'kernel/**/*.scala'\n      - 'kernel/**/*.java'\n      - 'storage/**/*.scala'\n      - 'storage/**/*.java'\n      - '.github/workflows/kernel_unitycatalog_test.yaml'\njobs:\n  test:\n    name: \"Kernel Unity Catalog Tests\"\n    runs-on: ubuntu-24.04\n    env:\n      SCALA_VERSION: 2.13.16\n    steps:\n      - uses: actions/checkout@v3\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Run Unity tests with coverage\n        run: |\n          ./build/sbt \"++ ${{ env.SCALA_VERSION }}\" clean coverage kernelUnityCatalog/test coverageAggregate coverageOff -v\n"
  },
  {
    "path": ".github/workflows/publish_docs.yaml",
    "content": "name: Publish Docs\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - docs/**\n  release:\n    types:\n      - published\n  workflow_dispatch:\n\njobs:\n  build_api_docs:\n    name: Build API docs (${{ matrix.version.name }})\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        version:\n          - name: latest\n            ref: v4.0.1\n            java: 17\n            out_dir: docs/apis/_site/api\n          - name: 3.3.2\n            ref: v3.3.2\n            java: 8\n            out_dir: docs/apis/_site/api\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          repository: delta-io/delta\n          ref: ${{ matrix.version.ref }}\n\n      - uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: ${{ matrix.version.java }}\n\n      - name: Setup python environment\n        uses: conda-incubator/setup-miniconda@v3\n        with:\n          activate-environment: delta_docs\n          environment-file: docs/environment.yml\n\n      - name: Fix generate_api_docs script\n        if: contains(matrix.version.ref, 'v4')\n        run: |\n          sed -i 's|scala-2\\.12|scala-2.13|g' docs/apis/generate_api_docs.py\n          sed -i '/standalone_javadoc_gen_dir,/d' docs/apis/generate_api_docs.py\n          sed -i '/flink_javadoc_gen_dir,/d' docs/apis/generate_api_docs.py\n\n      - name: Generate API docs\n        shell: bash -el {0}\n        run: python3 docs/generate_docs.py --api-docs\n        env:\n          _DELTA_LAKE_RELEASE_VERSION_: ${{ matrix.version.name }}\n\n      - name: Move doc contents up one level\n        run: |\n          find docs/apis/_site/api -type d \\( -name \"unidoc\" -o -name \"javaunidoc\" -o -name \"html\" \\) | while read dir; do\n            echo \"Processing $dir\"\n            parent=\"$(dirname \"$dir\")\"\n            # Move all files (including hidden ones) using find\n            find \"$dir\" -maxdepth 1 -type f -exec mv {} \"$parent\"/ \\;\n            # Move all subdirectories\n            find \"$dir\" -maxdepth 1 -type d ! -path \"$dir\" -exec mv {} \"$parent\"/ \\;\n            rmdir \"$dir\"\n          done\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ github.run_id }}-apidocs-${{ matrix.version.name }}\n          path: ${{ matrix.version.out_dir }}\n\n  build_site:\n    name: Build site\n    needs: build_api_docs\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: docs/.nvmrc\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        with:\n          package_json_file: docs/package.json\n\n      - name: Install Node.js dependencies\n        run: pnpm install\n        working-directory: docs\n\n      - name: Download API docs artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: ${{ github.run_id }}-apidocs-*\n          path: docs/public/api\n\n      - name: Rename API docs artifact folders\n        run: |\n          for d in docs/public/api/${{ github.run_id }}-apidocs-*; do\n            [ -d \"$d\" ] || continue\n            new_name=\"$(echo \"$d\" | sed \"s|docs/public/api/${{ github.run_id }}-apidocs-||\")\"\n            mv \"$d\" \"docs/public/api/$new_name\"\n          done\n\n      - name: Generate docs site\n        run: python3 docs/generate_docs.py\n\n      - name: Install Netlify CLI\n        run: pnpm i -g netlify-cli\n\n      - name: Publish site to Netlify\n        run: netlify deploy --dir=docs/dist --prod\n        env:\n          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n          NETLIFY_SITE_ID: ${{ vars.NETLIFY_SITE_ID }}\n"
  },
  {
    "path": ".github/workflows/spark_examples_test.yaml",
    "content": "name: \"Delta Spark Publishing and Examples\"\non:\n  push:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\njobs:\n  # Generate Spark versions matrix from CrossSparkVersions.scala\n  # This workflow tests against released versions only (no snapshots)\n  generate-matrix:\n    name: \"Generate Released Spark Versions Matrix\"\n    runs-on: ubuntu-24.04\n    outputs:\n      spark_versions: ${{ steps.generate.outputs.spark_versions }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Generate released Spark versions matrix\n        id: generate\n        run: |\n          # Get only released versions (exclude snapshots)\n          SPARK_VERSIONS=$(python3 project/scripts/get_spark_version_info.py --released-spark-versions)\n          echo \"spark_versions=$SPARK_VERSIONS\" >> $GITHUB_OUTPUT\n          echo \"Generated released Spark versions: $SPARK_VERSIONS\"\n\n  test:\n    name: \"DSP&E: Spark ${{ matrix.spark_version }}, Scala ${{ matrix.scala }}\"\n    runs-on: ubuntu-24.04\n    needs: generate-matrix\n    strategy:\n      matrix:\n        # Spark versions are dynamically generated - released versions only\n        spark_version: ${{ fromJson(needs.generate-matrix.outputs.spark_versions) }}\n        # These Scala versions must match those in the build.sbt\n        scala: [2.13.17]\n    env:\n      SCALA_VERSION: ${{ matrix.scala }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: Get Spark version details\n        id: spark-details\n        run: |\n          # Get JVM version, package suffix, iceberg support, and full version for this Spark version\n          JVM_VERSION=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" targetJvm | jq -r)\n          SPARK_PACKAGE_SUFFIX=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" packageSuffix | jq -r)\n          SUPPORT_ICEBERG=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" supportIceberg | jq -r)\n          SPARK_FULL_VERSION=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" fullVersion | jq -r)\n          echo \"jvm_version=$JVM_VERSION\" >> $GITHUB_OUTPUT\n          echo \"spark_package_suffix=$SPARK_PACKAGE_SUFFIX\" >> $GITHUB_OUTPUT\n          echo \"support_iceberg=$SUPPORT_ICEBERG\" >> $GITHUB_OUTPUT\n          echo \"spark_full_version=$SPARK_FULL_VERSION\" >> $GITHUB_OUTPUT\n          echo \"Using JVM $JVM_VERSION for Spark $SPARK_FULL_VERSION, package suffix: '$SPARK_PACKAGE_SUFFIX', support iceberg: '$SUPPORT_ICEBERG'\"\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: ${{ steps.spark-details.outputs.jvm_version }}\n      - name: Cache Scala, SBT\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2\n            ~/.cache/coursier\n          # Change the key if dependencies are changed. For each key, GitHub Actions will cache the\n          # the above directories when we use the key for the first time. After that, each run will\n          # just use the cache. The cache is immutable so we need to use a new key when trying to\n          # cache new stuff.\n          key: delta-sbt-cache-spark${{ matrix.spark_version }}-scala${{ matrix.scala }}\n      - name: Install Job dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git\n          sudo apt install libedit-dev\n      - name: Run Delta Spark Local Publishing and Examples Compilation\n        # examples/scala/build.sbt will compile against the local Delta release version (e.g. 3.2.0-SNAPSHOT).\n        # Thus, we need to publishM2 first so those jars are locally accessible.\n        # -DsparkVersion is for the Delta project's publishM2 (which Spark version to compile Delta against).\n        # SPARK_VERSION/SPARK_PACKAGE_SUFFIX/SUPPORT_ICEBERG are for examples/scala/build.sbt (dependency resolution).\n        env:\n          SPARK_PACKAGE_SUFFIX: ${{ steps.spark-details.outputs.spark_package_suffix }}\n          SUPPORT_ICEBERG: ${{ steps.spark-details.outputs.support_iceberg }}\n          SPARK_VERSION: ${{ steps.spark-details.outputs.spark_full_version }}\n        run: |\n          build/sbt clean\n          build/sbt -DsparkVersion=${{ steps.spark-details.outputs.spark_full_version }} publishM2\n          cd examples/scala && build/sbt \"++ $SCALA_VERSION compile\"\n      - name: Run UC Delta Integration Test\n        # Verifies that delta-spark resolved from Maven local includes all kernel module\n        # dependencies transitively by running a real UC-backed Delta workload.\n        env:\n          SPARK_PACKAGE_SUFFIX: ${{ steps.spark-details.outputs.spark_package_suffix }}\n          SPARK_VERSION: ${{ steps.spark-details.outputs.spark_full_version }}\n        run: |\n          cd examples/scala && build/sbt \"++ $SCALA_VERSION runMain example.UnityCatalogQuickstart\"\n"
  },
  {
    "path": ".github/workflows/spark_python_test.yaml",
    "content": "name: \"Delta Spark Python\"\non:\n  push:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\njobs:\n  # Generate Spark versions matrix from CrossSparkVersions.scala\n  # This workflow tests against released versions only (no snapshots)\n  generate-matrix:\n    name: \"Generate Released Spark Versions Matrix\"\n    runs-on: ubuntu-24.04\n    outputs:\n      spark_versions: ${{ steps.generate.outputs.spark_versions }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Generate released Spark versions matrix\n        id: generate\n        run: |\n          # Get only released versions (exclude snapshots)\n          SPARK_VERSIONS=$(python3 project/scripts/get_spark_version_info.py --released-spark-versions)\n          echo \"spark_versions=$SPARK_VERSIONS\" >> $GITHUB_OUTPUT\n          echo \"Generated released Spark versions: $SPARK_VERSIONS\"\n\n  test:\n    name: \"DSP (${{ matrix.spark_version }})\"\n    runs-on: ubuntu-24.04\n    needs: generate-matrix\n    strategy:\n      matrix:\n        # Spark versions are dynamically generated - released versions only\n        spark_version: ${{ fromJson(needs.generate-matrix.outputs.spark_versions) }}\n        # These Scala versions must match those in the build.sbt\n        scala: [2.13.16]\n    env:\n      SCALA_VERSION: ${{ matrix.scala }}\n      SPARK_VERSION: ${{ matrix.spark_version }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: Get Spark version details\n        id: spark-details\n        run: |\n          # Get JVM version and full Spark version for this matrix entry\n          JVM_VERSION=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" targetJvm | jq -r)\n          FULL_VERSION=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" fullVersion | jq -r)\n          echo \"jvm_version=$JVM_VERSION\" >> $GITHUB_OUTPUT\n          echo \"spark_full_version=$FULL_VERSION\" >> $GITHUB_OUTPUT\n          echo \"Using JVM $JVM_VERSION for Spark ${{ matrix.spark_version }} (full: $FULL_VERSION)\"\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: ${{ steps.spark-details.outputs.jvm_version }}\n      - name: Cache Scala, SBT\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2\n            ~/.cache/coursier\n          # Change the key if dependencies are changed. For each key, GitHub Actions will cache the\n          # the above directories when we use the key for the first time. After that, each run will\n          # just use the cache. The cache is immutable so we need to use a new key when trying to\n          # cache new stuff.\n          key: delta-sbt-cache-spark${{ matrix.spark_version }}-scala${{ matrix.scala }}\n      - name: Install Job dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git\n          sudo apt install libedit-dev\n          curl -LO https://github.com/bufbuild/buf/releases/download/v1.28.1/buf-Linux-x86_64.tar.gz\n          mkdir -p ~/buf\n          tar -xvzf buf-Linux-x86_64.tar.gz -C ~/buf --strip-components 1\n          rm buf-Linux-x86_64.tar.gz\n          sudo apt install python3-pip --fix-missing\n          sudo pip3 install pipenv==2024.4.1\n          curl https://pyenv.run | bash\n          export PATH=\"~/.pyenv/bin:$PATH\"\n          eval \"$(pyenv init -)\"\n          eval \"$(pyenv virtualenv-init -)\"\n          pyenv install 3.10\n          pyenv global system 3.10\n          pipenv --python 3.10 install\n          # Update the pip version to 24.0. By default `pyenv.run` installs the latest pip version\n          # available. From version 24.1, `pip` doesn't allow installing python packages\n          # with version string containing `-`. In Delta-Spark case, the pypi package generated has\n          # `-SNAPSHOT` in version (e.g. `3.3.0-SNAPSHOT`) as the version is picked up from\n          # the`version.sbt` file.\n          pipenv run pip install pip==24.0 setuptools==69.5.1 wheel==0.43.0\n          # Install pyspark matching the full spark version\n          pipenv run pip install pyspark==${{ steps.spark-details.outputs.spark_full_version }}\n          pipenv run pip install flake8==3.9.0\n          pipenv run pip install black==23.12.1\n          pipenv run pip install importlib_metadata==3.10.0\n          pipenv run pip install mypy==1.8.0\n          pipenv run pip install mypy-protobuf==3.3.0\n          pipenv run pip install cryptography==37.0.4\n          pipenv run pip install twine==4.0.1\n          pipenv run pip install wheel==0.33.4\n          pipenv run pip install setuptools==41.1.0\n          pipenv run pip install pydocstyle==3.0.0\n          pipenv run pip install pandas==2.2.0\n          pipenv run pip install pyarrow==15.0.0\n          pipenv run pip install pypandoc==1.3.3\n          pipenv run pip install numpy==1.22.4\n          pipenv run pip install googleapis-common-protos-stubs==2.2.0\n          pipenv run pip install grpc-stubs==1.24.11\n          # Version-specific dependencies for Spark Connect compatibility\n          if [[ \"${{ matrix.spark_version }}\" == \"4.0\" ]]; then\n            pipenv run pip install grpcio==1.67.0\n            pipenv run pip install grpcio-status==1.67.0\n            pipenv run pip install googleapis-common-protos==1.65.0\n            pipenv run pip install protobuf==5.29.1\n          else\n            # Spark 4.1+ requirements from https://github.com/apache/spark/blob/branch-4.1/dev/requirements.txt\n            pipenv run pip install grpcio==1.76.0\n            pipenv run pip install grpcio-status==1.76.0\n            pipenv run pip install googleapis-common-protos==1.71.0\n            pipenv run pip install protobuf==6.33.0\n            pipenv run pip install zstandard==0.25.0\n          fi\n      - name: Run Python tests\n        # when changing TEST_PARALLELISM_COUNT make sure to also change it in spark_test.yaml\n        run: |\n          TEST_PARALLELISM_COUNT=4 pipenv run python run-tests.py --group spark-python --spark-version ${{ matrix.spark_version }}\n"
  },
  {
    "path": ".github/workflows/spark_test.yaml",
    "content": "name: \"Delta Spark\"\non:\n  push:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - '**.txt'\njobs:\n  # Generate Spark versions matrix from CrossSparkVersions.scala\n  # This ensures the workflow always uses the versions defined in the build\n  generate-matrix:\n    name: \"Generate Spark Versions Matrix\"\n    runs-on: ubuntu-24.04\n    outputs:\n      spark_versions: ${{ steps.generate.outputs.spark_versions }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n      - name: Generate Spark versions matrix\n        id: generate\n        run: |\n          # The script automatically generates spark-versions.json from CrossSparkVersions.scala\n          SPARK_VERSIONS=$(python3 project/scripts/get_spark_version_info.py --all-spark-versions)\n          echo \"spark_versions=$SPARK_VERSIONS\" >> $GITHUB_OUTPUT\n          echo \"Generated Spark versions: $SPARK_VERSIONS\"\n\n  test:\n    name: \"DS: Spark ${{ matrix.spark_version }}, Scala ${{ matrix.scala }}, Shard ${{ matrix.shard }}\"\n    runs-on: ubuntu-24.04\n    needs: generate-matrix\n    strategy:\n      fail-fast: false\n      matrix:\n        # Spark versions are dynamically generated from CrossSparkVersions.scala\n        # DO NOT hardcode versions here - they are automatically loaded from the build configuration\n        spark_version: ${{ fromJson(needs.generate-matrix.outputs.spark_versions) }}\n        # These Scala versions must match those in the build.sbt\n        scala: [2.13.16]\n        # Important: This list of shards must be [0..NUM_SHARDS - 1]\n        shard: [0, 1, 2, 3, 4, 5, 6, 7]\n    env:\n      SCALA_VERSION: ${{ matrix.scala }}\n      SPARK_VERSION: ${{ matrix.spark_version }}\n      # Important: This must be the same as the length of shards in matrix\n      NUM_SHARDS: 8\n    steps:\n      - uses: actions/checkout@v3\n      - name: Get Spark version details\n        id: spark-details\n        run: |\n          # The script automatically generates spark-versions.json if needed\n          JVM_VERSION=$(python3 project/scripts/get_spark_version_info.py --get-field \"${{ matrix.spark_version }}\" targetJvm | jq -r)\n          echo \"jvm_version=$JVM_VERSION\" >> $GITHUB_OUTPUT\n          echo \"Using JVM version: $JVM_VERSION for Spark ${{ matrix.spark_version }}\"\n      - name: install java\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: ${{ steps.spark-details.outputs.jvm_version }}\n      - name: Cache Scala, SBT\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.sbt\n            ~/.ivy2\n            ~/.cache/coursier\n          # Change the key if dependencies are changed. For each key, GitHub Actions will cache the\n          # the above directories when we use the key for the first time. After that, each run will\n          # just use the cache. The cache is immutable so we need to use a new key when trying to\n          # cache new stuff.\n          key: delta-sbt-cache-spark${{ matrix.spark_version }}-scala${{ matrix.scala }}\n      - name: Install Job dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git\n          sudo apt install libedit-dev\n          curl -LO https://github.com/bufbuild/buf/releases/download/v1.28.1/buf-Linux-x86_64.tar.gz\n          mkdir -p ~/buf\n          tar -xvzf buf-Linux-x86_64.tar.gz -C ~/buf --strip-components 1\n          rm buf-Linux-x86_64.tar.gz\n          sudo apt install python3-pip --fix-missing\n          sudo pip3 install pipenv==2024.4.1\n          curl https://pyenv.run | bash\n          export PATH=\"~/.pyenv/bin:$PATH\"\n          eval \"$(pyenv init -)\"\n          eval \"$(pyenv virtualenv-init -)\"\n          pyenv install 3.9\n          pyenv global system 3.9\n          pipenv --python 3.9 install\n          # Update the pip version to 24.0. By default `pyenv.run` installs the latest pip version\n          # available. From version 24.1, `pip` doesn't allow installing python packages\n          # with version string containing `-`. In Delta-Spark case, the pypi package generated has\n          # `-SNAPSHOT` in version (e.g. `3.3.0-SNAPSHOT`) as the version is picked up from\n          # the`version.sbt` file.\n          pipenv run pip install pip==24.0 setuptools==69.5.1 wheel==0.43.0\n          pipenv run pip install flake8==3.9.0\n          pipenv run pip install black==23.12.1\n          pipenv run pip install importlib_metadata==3.10.0\n          pipenv run pip install mypy==1.8.0\n          pipenv run pip install mypy-protobuf==3.3.0\n          pipenv run pip install cryptography==37.0.4\n          pipenv run pip install twine==4.0.1\n          pipenv run pip install wheel==0.33.4\n          pipenv run pip install setuptools==41.1.0\n          pipenv run pip install pydocstyle==3.0.0\n          pipenv run pip install pandas==2.2.0\n          pipenv run pip install pyarrow==11.0.0\n          pipenv run pip install pypandoc==1.3.3\n          pipenv run pip install numpy==1.22.4\n          pipenv run pip install grpcio==1.67.0\n          pipenv run pip install grpcio-status==1.67.0\n          pipenv run pip install googleapis-common-protos==1.65.0\n          pipenv run pip install protobuf==5.29.1\n          pipenv run pip install googleapis-common-protos-stubs==2.2.0\n          pipenv run pip install grpc-stubs==1.24.11\n      - name: Scala structured logging style check\n        run: |\n          if [ -f ./dev/spark_structured_logging_style.py ]; then\n              python3 ./dev/spark_structured_logging_style.py\n          fi\n      - name: Run Scala/Java tests\n        # when changing TEST_PARALLELISM_COUNT make sure to also change it in spark_python_test.yaml\n        run: |\n          TEST_PARALLELISM_COUNT=4 pipenv run python run-tests.py --group spark --shard ${{ matrix.shard }} --spark-version ${{ matrix.spark_version }}\n      - name: Upload test reports\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: test-reports-spark${{ matrix.spark_version }}-shard${{ matrix.shard }}\n          path: \"**/target/test-reports/*.xml\"\n          retention-days: 7\n"
  },
  {
    "path": ".github/workflows/unidoc.yaml",
    "content": "  name: \"Unidoc\"\n  on: [push, pull_request]\n  jobs:\n    build:\n      name: \"U: Scala ${{ matrix.scala }}\"\n      runs-on: ubuntu-24.04\n      strategy:\n        matrix:\n          # These Scala versions must match those in the build.sbt\n          scala: [2.13.16]\n      steps:\n        - name: install java\n          uses: actions/setup-java@v3\n          with:\n            distribution: \"zulu\"\n            java-version: \"17\"\n        - uses: actions/checkout@v3\n        - name: generate unidoc\n          run: build/sbt \"++ ${{ matrix.scala }}\" unidoc\n"
  },
  {
    "path": ".gitignore",
    "content": "*#*#\n*.#*\n*.iml\n*.ipr\n*.iws\n*.pyc\n*.pyo\n*.swp\n*~\n.DS_Store\n.ammonite\n.bloop\n.bsp\n.cache\n.classpath\n.ensime\n.ensime_cache/\n.ensime_lucene\n.generated-mima*\n.idea/\n.idea_modules/\n.metals\n.project\n.pydevproject\n.scala_dependencies\n.settings\n/lib/\nR-unit-tests.log\nR/unit-tests.out\nR/cran-check.out\nR/pkg/vignettes/sparkr-vignettes.html\nR/pkg/tests/fulltests/Rplots.pdf\nbuild/*.jar\nbuild/apache-maven*\nbuild/scala*\nbuild/zinc*\ncache\ncheckpoint\nconf/*.cmd\nconf/*.conf\nconf/*.properties\nconf/*.sh\nconf/*.xml\nconf/java-opts\ndependency-reduced-pom.xml\nderby.log\ndev/create-release/*final\ndev/create-release/*txt\ndev/pr-deps/\ndist/\ndocs/_site\ndocs/api\nsql/docs\nsql/site\nlib_managed/\nlint-r-report.log\nlog/\nlogs/\nmetals.sbt\nout/\nproject/boot/\nproject/build/target/\nproject/plugins/lib_managed/\nproject/plugins/project/build.properties\nproject/plugins/src_managed/\nproject/plugins/target/\npython/lib/pyspark.zip\npython/deps\ndocs/python/_static/\ndocs/python/_templates/\ndocs/python/_build/\npython/test_coverage/coverage_data\npython/test_coverage/htmlcov\npython/pyspark/python\nreports/\nscalastyle-on-compile.generated.xml\nscalastyle-output.xml\nscalastyle.txt\nspark-*-bin-*.tgz\nspark-tests.log\nsrc_managed/\nstreaming-tests.log\ntarget/\nunit-tests.log\nwork/\ndocs/.jekyll-metadata\n\n# For Hive\nTempStatsStore/\nmetastore/\nmetastore_db/\nsql/hive-thriftserver/test_warehouses\nwarehouse/\nspark-warehouse/\n\n# For R session data\n.RData\n.RHistory\n.Rhistory\n*.Rproj\n*.Rproj.*\n\n.Rproj.user\n\n**/src/main/resources/js\n\n# For SBT\n.jvmopts\nsbt-launch-*.jar\n\n# For Python linting\npep8*.py\npycodestyle*.py\n\n# For IDE settings\n.vscode\n\n# For Terraform\n**/.terraform/*\n*.tfstate\n*.tfstate.*\ncrash.log\ncrash.*.log\n*.tfvars\n*.tfvars.json\noverride.tf\noverride.tf.json\n*_override.tf\n*_override.tf.json\n.terraformrc\n.terraform.rc\n\n# Local Netlify folder\n.netlify\n\n# Ignore kernel benchmark report\nkernel/kernel-benchmarks/benchmark_report.json\n\n# Unity Catalog test artifacts\nspark/unitycatalog/etc/\n.scala-build/\n\n"
  },
  {
    "path": ".sbtopts",
    "content": "-J-Xmx4G\n"
  },
  {
    "path": ".scalafmt.conf",
    "content": "# Copyright (2025) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nalign = none\nalign.openParenDefnSite = false\nalign.openParenCallSite = false\nalign.tokens = []\nimportSelectors = \"singleLine\"\noptIn.configStyleArguments = false\ncontinuationIndent {\n  callSite = 2\n  defnSite = 4\n}\ndanglingParentheses {\n  defnSite = false\n  callSite = false\n}\ndocstrings {\n  style = Asterisk\n  wrap = no\n}\nliterals.hexDigits = upper\nmaxColumn = 100\nnewlines {\n  beforeCurlyLambdaParams = false\n  source = keep\n}\nrewrite.rules = [Imports]\nrewrite.imports.sort = scalastyle\nrewrite.imports.groups = [\n  [\"java\\\\..*\"],\n  [\"scala\\\\..*\"],\n  [\"io\\\\.delta\\\\..*\"],\n  [\"org\\\\.apache\\\\.spark\\\\.sql\\\\.delta.*\"]\n]\nrunner.dialect = scala212\nversion = 3.8.6\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Delta Lake Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\nshipit\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\nshipit\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the Technical Steering Committee defined [here](https://github.com/delta-io/delta/blob/master/CONTRIBUTING.md#governance). All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n\n## Linux Foundation Code of Conduct\nYour use is additionally subject to the [Linux Foundation Code of Conduct](https://lfprojects.org/policies/code-of-conduct/)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "We happily welcome contributions to Delta Lake. We use [GitHub Issues](/../../issues/) to track community reported issues and [GitHub Pull Requests ](/../../pulls/) for accepting changes.\n\n# Governance\nDelta Lake is an independent open-source project and not controlled by any single company. To emphasize this we joined the [Delta Lake Project](https://community.linuxfoundation.org/delta-lake/) in 2019, which is a sub-project of the Linux Foundation Projects. Within the project, we make decisions based on [these rules](https://delta.io/pdfs/delta-charter.pdf).\n\nDelta Lake is supported by a wide set of developers from over 50 organizations across multiple repositories.  Since 2019, more than 190 developers have contributed to Delta Lake!  The Delta Lake community is growing by leaps and bounds with more than 6000 members in the [Delta Users slack](https://go.delta.io/slack)).\n\nFor more information, please refer to the [founding technical charter](https://delta.io/pdfs/delta-charter.pdf).\n\n# Communication\n- Before starting work on a major feature, please reach out to us via [GitHub](https://github.com/delta-io/delta/issues), [Slack](https://go.delta.io/slack), [email](https://groups.google.com/g/delta-users), etc. We will make sure no one else is already working on it and ask you to open a GitHub issue.\n- A \"major feature\" is defined as any change that is > 100 LOC altered (not including tests), or changes any user-facing behavior.\n- We will use the GitHub issue to discuss the feature and come to agreement.\n- This is to prevent your time being wasted, as well as ours.\n- The GitHub review process for major features is also important so that organizations with commit access can come to agreement on design.\n- If it is appropriate to write a design document, the document must be hosted either in the GitHub tracking issue, or linked to from the issue and hosted in a world-readable location. Examples of design documents include [sample 1](https://docs.google.com/document/d/16S7xoAmXpSax7W1OWYYHo5nZ71t5NvrQ-F79pZF6yb8), [sample 2](https://docs.google.com/document/d/1MJhmW_H7doGWY2oty-I78vciziPzBy_nzuuB-Wv5XQ8), and [sample 3](https://docs.google.com/document/d/19CU4eJuBXOwW7FC58uSqyCbcLTsgvQ5P1zoPOPgUSpI).\n- Specifically, if the goal is to add a new extension, please read the extension policy.\n- Small patches and bug fixes don't need prior communication. If you have identified a bug and have ways to solve it, please create an [issue](https://github.com/delta-io/delta/issues) or create a [pull request](https://github.com/delta-io/delta/pulls).\n- If you have an example code that explains a use case or a feature, create a pull request to post under [examples](https://github.com/delta-io/delta/tree/master/examples). \n\n\n# Coding style\nWe generally follow the [Apache Spark Scala Style Guide](https://spark.apache.org/contributing.html).\n\n# Sign your work\nThe sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from developercertificate.org):\n\n```\nDeveloper Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n1 Letterman Drive\nSuite D4700\nSan Francisco, CA, 94129\n\nEveryone is permitted to copy and distribute verbatim copies of this\nlicense document, but changing it is not allowed.\n\n\nDeveloper's Certificate of Origin 1.1\n\nBy making a contribution to this project, I certify that:\n\n(a) The contribution was created in whole or in part by me and I\n    have the right to submit it under the open source license\n    indicated in the file; or\n\n(b) The contribution is based upon previous work that, to the best\n    of my knowledge, is covered under an appropriate open source\n    license and I have the right under that license to submit that\n    work with modifications, whether created in whole or in part\n    by me, under the same open source license (unless I am\n    permitted to submit under a different license), as indicated\n    in the file; or\n\n(c) The contribution was provided directly to me by some other\n    person who certified (a), (b) or (c) and I have not modified\n    it.\n\n(d) I understand and agree that this project and the contribution\n    are public and that a record of the contribution (including all\n    personal information I submit with it, including my sign-off) is\n    maintained indefinitely and may be redistributed consistent with\n    this project or the open source license(s) involved.\n```\n\nThen you just add a line to every git commit message:\n\n```\nSigned-off-by: Jane Smith <jane.smith@email.com>\nUse your real name (sorry, no pseudonyms or anonymous contributions.)\n```\n\nIf you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`.\n"
  },
  {
    "path": "Dockerfile",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nFROM ubuntu:focal-20221019\n\nENV DEBIAN_FRONTEND noninteractive\nENV DEBCONF_NONINTERACTIVE_SEEN true\n\nRUN apt-get update\nRUN apt-get install -y software-properties-common\nRUN apt-get install -y curl\nRUN apt-get install -y wget\nRUN apt-get install -y openjdk-8-jdk\nRUN apt-get install -y python3.8\nRUN apt-get install -y python3-pip\nRUN apt-get install -y git\n\n# Upgrade pip. This is needed to use prebuilt wheels for packages cffi (dep of cryptography) and\n# cryptography. Otherwise, building wheels for these packages fails.\nRUN pip3 install --upgrade pip\n\n# Update the pip version to 24.0. By default `pyenv.run` installs the latest pip version\n# available. From version 24.1, `pip` doesn't allow installing python packages\n# with version string containing `-`. In Delta-Spark case, the pypi package generated has\n# `-SNAPSHOT` in version (e.g. `3.3.0-SNAPSHOT`) as the version is picked up from\n# the`version.sbt` file.\nRUN pip install pip==24.0 setuptools==69.5.1 wheel==0.43.0\n\nRUN pip3 install pyspark==3.5.3\n\nRUN pip3 install mypy==0.982\n\nRUN pip3 install pydocstyle==3.0.0\n\nRUN pip3 install pandas==1.0.5\n\nRUN pip3 install pyarrow==8.0.0\n\nRUN pip3 install numpy==1.20.3\n\nRUN pip3 install importlib_metadata==3.10.0\n\nRUN pip3 install cryptography==37.0.4\n\n# We must install cryptography before twine. Else, twine will pull a newer version of\n# cryptography that requires a newer version of Rust and may break tests.\nRUN pip3 install twine==4.0.1\n\nRUN pip3 install wheel==0.33.4\n\nRUN pip3 install setuptools==41.0.1\n\n# Do not add any non-deterministic changes (e.g., copy from files \n# from repo) in this Dockerfile, so that the  docker image \n# generated from this can be reused across builds.\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright (2021) The Delta Lake Project Authors.  All rights reserved.\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n\n\n-------------------------------------------------------------------------\nThis project includes code derived from the Apache Spark project.\nThe individual files containing this code carry the original Apache Spark\nlicense, which is reproduced here as well:\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "NOTICE.txt",
    "content": "Delta Lake\nCopyright (2021) The Delta Lake Project Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\nThis project includes software licensed by the Apache Software Foundation (Apache 2.0)\nfrom the Apache Spark project (www.github.com/apache/spark)\n\n----------------------------------------------------------\nApache Spark\nCopyright 2014 and onwards The Apache Software Foundation.\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n"
  },
  {
    "path": "PROTOCOL.md",
    "content": "<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n# Delta Transaction Log Protocol\n\n- [Overview](#overview)\n- [Delta Table Specification](#delta-table-specification)\n  - [File Types](#file-types)\n    - [Data Files](#data-files)\n    - [Deletion Vector Files](#deletion-vector-files)\n    - [Change Data Files](#change-data-files)\n    - [Delta Log Entries](#delta-log-entries)\n    - [Checkpoints](#checkpoints)\n      - [Sidecar Files](#sidecar-files)\n    - [Log Compaction Files](#log-compaction-files)\n    - [Last Checkpoint File](#last-checkpoint-file)\n    - [Version Checksum File](#version-checksum-file)\n      - [File Size Histogram Schema](#file-size-histogram-schema)\n  - [Actions](#actions)\n    - [Change Metadata](#change-metadata)\n      - [Format Specification](#format-specification)\n    - [Add File and Remove File](#add-file-and-remove-file)\n    - [Add CDC File](#add-cdc-file)\n      - [Writer Requirements for AddCDCFile](#writer-requirements-for-addcdcfile)\n      - [Reader Requirements for AddCDCFile](#reader-requirements-for-addcdcfile)\n    - [Transaction Identifiers](#transaction-identifiers)\n    - [Protocol Evolution](#protocol-evolution)\n    - [Commit Provenance Information](#commit-provenance-information)\n    - [Domain Metadata](#domain-metadata)\n      - [Reader Requirements for Domain Metadata](#reader-requirements-for-domain-metadata)\n      - [Writer Requirements for Domain Metadata](#writer-requirements-for-domain-metadata)\n    - [Sidecar File Information](#sidecar-file-information)\n      - [Checkpoint Metadata](#checkpoint-metadata)\n- [Action Reconciliation](#action-reconciliation)\n- [Table Features](#table-features)\n  - [Table Features for New and Existing Tables](#table-features-for-new-and-existing-tables)\n  - [Supported Features](#supported-features)\n  - [Active Features](#active-features)\n- [Column Mapping](#column-mapping)\n  - [Writer Requirements for Column Mapping](#writer-requirements-for-column-mapping)\n  - [Reader Requirements for Column Mapping](#reader-requirements-for-column-mapping)\n- [Deletion Vectors](#deletion-vectors)\n  - [Deletion Vector Descriptor Schema](#deletion-vector-descriptor-schema)\n    - [Derived Fields](#derived-fields)\n    - [JSON Example 1 — On Disk with Relative Path (with Random Prefix)](#json-example-1--on-disk-with-relative-path-with-random-prefix)\n    - [JSON Example 2 — On Disk with Absolute Path](#json-example-2--on-disk-with-absolute-path)\n    - [JSON Example 3 — Inline](#json-example-3--inline)\n  - [Reader Requirements for Deletion Vectors](#reader-requirements-for-deletion-vectors)\n  - [Writer Requirement for Deletion Vectors](#writer-requirement-for-deletion-vectors)\n- [Iceberg Compatibility V1](#iceberg-compatibility-v1)\n  - [Writer Requirements for IcebergCompatV1](#writer-requirements-for-icebergcompatv1)\n- [Iceberg Compatibility V2](#iceberg-compatibility-v2)\n  - [Writer Requirement for IcebergCompatV2](#iceberg-compatibility-v2)\n- [Timestamp without timezone (TimestampNtz)](#timestamp-without-timezone-timestampntz)\n- [V2 Checkpoint Table Feature](#v2-checkpoint-table-feature)\n- [Row Tracking](#row-tracking)\n  - [Row IDs](#row-ids)\n  - [Row Commit Versions](#row-commit-versions)\n  - [Reader Requirements for Row Tracking](#reader-requirements-for-row-tracking)\n  - [Writer Requirements for Row Tracking](#writer-requirements-for-row-tracking)\n- [VACUUM Protocol Check](#vacuum-protocol-check)\n  - [Writer Requirements for Vacuum Protocol Check](#writer-requirements-for-vacuum-protocol-check)\n  - [Reader Requirements for Vacuum Protocol Check](#reader-requirements-for-vacuum-protocol-check)\n- [Clustered Table](#clustered-table)\n  - [Writer Requirements for Clustered Table](#writer-requirements-for-clustered-table)\n- [Variant Data Type](#variant-data-type)\n  - [Variant data in Parquet](#variant-data-in-parquet)\n  - [Writer Requirements for Variant Type](#writer-requirements-for-variant-type)\n  - [Reader Requirements for Variant Data Type](#reader-requirements-for-variant-data-type)\n  - [Compatibility with other Delta Features](#compatibility-with-other-delta-features)\n- [Catalog-managed tables](#catalog-managed-tables)\n  - [Terminology: Commits](#terminology-commits)\n  - [Terminology: Delta Client](#terminology-delta-client)\n  - [Terminology: Catalogs](#terminology-catalogs)\n  - [Catalog Responsibilities](#catalog-responsibilities)\n  - [Reading Catalog-managed Tables](#reading-catalog-managed-tables)\n  - [Commit Protocol](#commit-protocol)\n  - [Getting Ratified Commits from the Catalog](#getting-ratified-commits-from-the-catalog)\n  - [Publishing Commits](#publishing-commits)\n  - [Maintenance Operations on Catalog-managed Tables](#maintenance-operations-on-catalog-managed-tables)\n  - [Creating and Dropping Catalog-managed Tables](#creating-and-dropping-catalog-managed-tables)\n  - [Catalog-managed Table Enablement](#catalog-managed-table-enablement)\n  - [Writer Requirements for Catalog-managed tables](#writer-requirements-for-catalog-managed-tables)\n  - [Reader Requirements for Catalog-managed tables](#reader-requirements-for-catalog-managed-tables)\n  - [Table Discovery](#table-discovery)\n  - [Sample Catalog Client API](#sample-catalog-client-api)\n- [Requirements for Writers](#requirements-for-writers)\n  - [Creation of New Log Entries](#creation-of-new-log-entries)\n  - [Consistency Between Table Metadata and Data Files](#consistency-between-table-metadata-and-data-files)\n  - [Delta Log Entries](#delta-log-entries-1)\n  - [Checkpoints](#checkpoints-1)\n    - [Checkpoint Specs](#checkpoint-specs)\n      - [V2 Spec](#v2-spec)\n      - [V1 Spec](#v1-spec)\n    - [Checkpoint Naming Scheme](#checkpoint-naming-scheme)\n      - [UUID-named checkpoint](#uuid-named-checkpoint)\n      - [Classic checkpoint](#classic-checkpoint)\n      - [Multi-part checkpoint](#multi-part-checkpoint)\n        - [Problems with multi-part checkpoints](#problems-with-multi-part-checkpoints)\n    - [Handling Backward compatibility while moving to UUID-named v2 Checkpoints](#handling-backward-compatibility-while-moving-to-uuid-named-v2-checkpoints)\n    - [Allowed combinations for `checkpoint spec` <-> `checkpoint file naming`](#allowed-combinations-for-checkpoint-spec---checkpoint-file-naming)\n    - [Metadata Cleanup](#metadata-cleanup)\n  - [Data Files](#data-files-1)\n  - [Append-only Tables](#append-only-tables)\n  - [Column Invariants](#column-invariants)\n  - [CHECK Constraints](#check-constraints)\n  - [Generated Columns](#generated-columns)\n  - [Default Columns](#default-columns)\n  - [Identity Columns](#identity-columns)\n  - [Writer Version Requirements](#writer-version-requirements)\n- [Requirements for Readers](#requirements-for-readers)\n  - [Reader Version Requirements](#reader-version-requirements)\n- [Appendix](#appendix)\n  - [Valid Feature Names in Table Features](#valid-feature-names-in-table-features)\n  - [Deletion Vector Format](#deletion-vector-format)\n    - [Deletion Vector File Storage Format](#deletion-vector-file-storage-format)\n  - [Per-file Statistics](#per-file-statistics)\n  - [Partition Value Serialization](#partition-value-serialization)\n  - [Schema Serialization Format](#schema-serialization-format)\n    - [Primitive Types](#primitive-types)\n    - [Struct Type](#struct-type)\n    - [Struct Field](#struct-field)\n    - [Array Type](#array-type)\n    - [Map Type](#map-type)\n    - [Variant Type](#variant-type)\n    - [Column Metadata](#column-metadata)\n    - [Example](#example)\n  - [Checkpoint Schema](#checkpoint-schema)\n  - [Last Checkpoint File Schema](#last-checkpoint-file-schema)\n    - [JSON checksum](#json-checksum)\n      - [How to URL encode keys and string values](#how-to-url-encode-keys-and-string-values)\n  - [Delta Data Type to Parquet Type Mappings](#delta-data-type-to-parquet-type-mappings)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n# Overview\nThis document is a specification for the Delta Transaction Protocol, which brings [ACID](https://en.wikipedia.org/wiki/ACID) properties to large collections of data, stored as files, in a distributed file system or object store. The protocol was designed with the following goals in mind:\n\n- **Serializable ACID Writes** - multiple writers can concurrently modify a Delta table while maintaining ACID semantics.\n- **Snapshot Isolation for Reads** - readers can read a consistent snapshot of a Delta table, even in the face of concurrent writes.\n- **Scalability to billions of partitions or files** - queries against a Delta table can be planned on a single machine or in parallel.\n- **Self describing** - all metadata for a Delta table is stored alongside the data. This design eliminates the need to maintain a separate metastore just to read the data and also allows static tables to be copied or moved using standard filesystem tools.\n- **Support for incremental processing** - readers can tail the Delta log to determine what data has been added in a given period of time, allowing for efficient streaming.\n\nDelta's transactions are implemented using multi-version concurrency control (MVCC).\nAs a table changes, Delta's MVCC algorithm keeps multiple copies of the data around rather than immediately replacing files that contain records that are being updated or removed.\n\nReaders of the table ensure that they only see one consistent _snapshot_ of a table at time by using the _transaction log_ to selectively choose which _data files_ to process.\n\nWriters modify the table in two phases:\nFirst, they optimistically write out new data files or updated copies of existing ones.\nThen, they _commit_, creating the latest _atomic version_ of the table by adding a new entry to the log.\nIn this log entry they record which data files to logically add and remove, along with changes to other metadata about the table.\n\nData files that are no longer present in the latest version of the table can be lazily deleted by the vacuum command after a user-specified retention period (default 7 days).\n\n# Delta Table Specification\nA table has a single serial history of atomic versions, which are named using contiguous, monotonically-increasing integers.\nThe state of a table at a given version is called a _snapshot_ and is defined by the following properties:\n - **Delta log protocol** consists of two **protocol versions**, and if applicable, corresponding **table features**, that are required to correctly read or write the table\n   - **Reader features** only exists when Reader Version is 3\n   - **Writer features** only exists when Writer Version is 7\n - **Metadata** of the table (e.g., the schema, a unique identifier, partition columns, and other configuration properties)\n - **Set of files** present in the table, along with metadata about those files\n - **Set of tombstones** for files that were recently deleted\n - **Set of applications-specific transactions** that have been successfully committed to the table\n\n## File Types\nA Delta table is stored within a directory and is composed of the following different types of files.\n\nHere is an example of a Delta table with four entries in the commit log, stored in the directory `mytable`.\n```\n/mytable/_delta_log/00000000000000000042.json\n/mytable/_delta_log/00000000000000000042.checkpoint.parquet\n/mytable/_delta_log/00000000000000000043.json\n/mytable/_delta_log/00000000000000000044.json\n/mytable/_delta_log/00000000000000000045.json\n/mytable/_delta_log/_last_checkpoint\n/mytable/_change_data/cdc-00000-924d9ac7-21a9-4121-b067-a0a6517aa8ed.c000.snappy.parquet\n/mytable/part-00000-3935a07c-416b-4344-ad97-2a38342ee2fc.c000.snappy.parquet\n/mytable/deletion_vector-0c6cbaaf-5e04-4c9d-8959-1088814f58ef.bin\n```\n\nThis example represents a table after [metadata cleanup](#metadata-cleanup) has removed older log entries. The checkpoint at version 42 contains the complete table state, while versions 43-45 are subsequent commits. Each file type is described in the sections below.\n\n### Data Files\nData files can be stored in the root directory of the table or in any non-hidden subdirectory (i.e., one whose name does not start with an `_`).\nBy default, the reference implementation stores data files in directories that are named based on the partition values for data in that file (i.e. `part1=value1/part2=value2/...`).\nThis directory format is only used to follow existing conventions and is not required by the protocol.\nActual partition values for a file must be read from the transaction log.\n\n### Deletion Vector Files\nDeletion Vector (DV) files are stored in the root directory of the table alongside the data files. A DV file contains one or more serialised DV, each describing the set of *invalidated* (or \"soft deleted\") rows for a particular data file it is associated with.\nFor data with partition values, DV files are *not* kept in the same directory hierarchy as data files, as each one can contain DVs for files from multiple partitions.\nDV files store DVs in a [binary format](#deletion-vector-format).\n\n### Change Data Files\nChange data files are stored in a directory at the root of the table named `_change_data`, and represent the changes for the table version they are in. For data with partition values, it is recommended that the change data files are stored within the `_change_data` directory in their respective partitions (i.e. `_change_data/part1=value1/...`). Writers can _optionally_ produce these change data files as a consequence of operations that change underlying data, like `UPDATE`, `DELETE`, and `MERGE` operations to a Delta Lake table. If an operation only adds new data or removes existing data without updating any existing rows, a writer can write only data files and commit them in `add` or `remove` actions without duplicating the data into change data files. When available, change data readers should use the change data files instead of computing changes from the underlying data files.\n\nIn addition to the data columns, change data files contain additional columns that identify the type of change event:\n\nField Name | Data Type | Description\n-|-|-\n_change_type|`String`| `insert`, `update_preimage` , `update_postimage`, `delete` __(1)__\n\n__(1)__ `preimage` is the value before the update, `postimage` is the value after the update.\n\n### Delta Log Entries\n\nDelta Log Entries, also known as Delta files, are JSON files stored in the `_delta_log`\ndirectory at the root of the table. Together with checkpoints, they make up the log of all changes\nthat have occurred to a table. Delta files are the unit of atomicity for a table, and are named\nusing the next available version number, zero-padded to 20 digits.\n\nFor example:\n\n```\n./_delta_log/00000000000000000000.json\n```\n\nDelta files use newline-delimited JSON format, where every action is stored as a single-line\nJSON document. A Delta file, corresponding to version `v`, contains an atomic set of\n[_actions_](#actions) that should be applied to the previous table state corresponding to version\n`v-1`, in order to construct the `v`th snapshot of the table. An action changes one aspect of the\ntable's state, for example, adding or removing a file.\n\n**Note:** If the [catalogManaged table feature](#catalog-managed-tables) is enabled on the table,\nrecently [ratified commits](#ratified-commit) may not yet be published to the `_delta_log` directory as normal Delta\nfiles - they may be stored directly by the catalog or reside in the `_delta_log/_staged_commits`\ndirectory. Delta clients must contact the table's managing catalog in order to find the information\nabout these [ratified, potentially-unpublished commits](#publishing-commits).\n\nThe `_delta_log/_staged_commits` directory is the staging area for [staged](#staged-commit)\ncommits. Delta files in this directory have a UUID embedded into them and follow the pattern\n`<version>.<uuid>.json`, where the version corresponds to the proposed commit version, zero-padded\nto 20 digits.\n\nFor example:\n\n```\n./_delta_log/_staged_commits/00000000000000000000.3a0d65cd-4056-49b8-937b-95f9e3ee90e5.json\n./_delta_log/_staged_commits/00000000000000000001.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\n./_delta_log/_staged_commits/00000000000000000001.016ae953-37a9-438e-8683-9a9a4a79a395.json\n./_delta_log/_staged_commits/00000000000000000002.3ae45b72-24e1-865a-a211-34987ae02f2a.json\n```\n\nNOTE: The (proposed) version number of a staged commit is authoritative - file\n`00000000000000000100.<uuid>.json` always corresponds to a commit attempt for version 100. Besides\nsimplifying implementations, it also acknowledges the fact that commit files cannot safely be reused\nfor multiple commit attempts. For example, resolving conflicts in a table with [row\ntracking](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#row-tracking) enabled requires\nrewriting all file actions to update their `baseRowId` field.\n\nThe [catalog](#terminology-catalogs) is the source of truth about which staged commit files in\nthe `_delta_log/_staged_commits` directory correspond to ratified versions, and Delta clients should\nnot attempt to directly interpret the contents of that directory. Refer to\n[catalog-managed tables](#catalog-managed-tables) for more details.\n\n### Checkpoints\nCheckpoints are also stored in the `_delta_log` directory, and can be created at any time, for any committed version of the table.\nFor performance reasons, readers should prefer to use the newest complete checkpoint possible.\nFor time travel, the checkpoint used must not be newer than the time travel version.\n\nA checkpoint contains the complete replay of all actions, up to and including the checkpointed table version, with invalid actions removed.\nInvalid actions are those that have been canceled out by subsequent ones (for example removing a file that has been added), using the [rules for reconciliation](#Action-Reconciliation).\nIn addition to above, checkpoint also contains the [_remove tombstones_](#add-file-and-remove-file) until they are expired.\nCheckpoints allow readers to short-cut the cost of reading the log up-to a given point in order to reconstruct a snapshot, and they also allow [Metadata cleanup](#metadata-cleanup) to delete expired JSON Delta log entries.\n\nReaders SHOULD NOT make any assumptions about the existence or frequency of checkpoints, with one exception:\n[Metadata cleanup](#metadata-cleanup) MUST provide a checkpoint for the oldest kept table version, to cover all deleted [Delta log entries](#delta-log-entries).\nThat said, writers are encouraged to checkpoint reasonably frequently, so that readers do not pay excessive log replay costs due to reading large numbers of delta files.\n\nThe checkpoint file name is based on the version of the table that the checkpoint contains.\n\nDelta supports three kinds of checkpoints:\n\n1. UUID-named Checkpoints: These follow [V2 spec](#v2-spec) which uses the following file name: `n.checkpoint.u.{json/parquet}`, where `u` is a UUID and `n` is the\nsnapshot version that this checkpoint represents. Here `n` must be zero padded to have length 20. The UUID-named V2 Checkpoint may be in json or parquet format, and references zero or more checkpoint sidecars\nin the `_delta_log/_sidecars` directory. A checkpoint sidecar is a uniquely-named parquet file: `{unique}.parquet` where `unique` is some unique\nstring such as a UUID.\n\nFor example:\n\n```\n00000000000000000010.checkpoint.80a083e8-7026-4e79-81be-64bd76c43a11.json\n_sidecars/3a0d65cd-4056-49b8-937b-95f9e3ee90e5.parquet\n_sidecars/016ae953-37a9-438e-8683-9a9a4a79a395.parquet\n_sidecars/7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.parquet\n```\n\n2. A [classic checkpoint](#classic-checkpoint) for version `n` of the table consists of a file named `n.checkpoint.parquet`. Here `n` must be zero padded to have length 20.\nThese could follow either [V1 spec](#v1-spec) or [V2 spec](#v2-spec).\nFor example:\n\n```\n00000000000000000010.checkpoint.parquet\n```\n\n\n3. A [multi-part checkpoint](#multi-part-checkpoint) for version `n` consists of `p` \"part\" files (`p > 1`), where\npart `o` of `p` is named `n.checkpoint.o.p.parquet`. Here `n` must be zero padded to have length 20, while `o` and `p` must be zero padded to have length 10. These are always [V1 checkpoints](#v1-spec).\nFor example:\n\n```\n00000000000000000010.checkpoint.0000000001.0000000003.parquet\n00000000000000000010.checkpoint.0000000002.0000000003.parquet\n00000000000000000010.checkpoint.0000000003.0000000003.parquet\n```\n\nA writer can choose to write checkpoints with following constraints:\n- Writers are always allowed create a [classic checkpoint](#classic-checkpoint) following [v1 spec](#v1-spec).\n- Writers are forbidden to create [multi-part checkpoints](#multi-part-checkpoint) if [v2 checkpoints](#v2-checkpoint-table-feature) are enabled.\n- Writers are allowed to create v2 spec checkpoints (either [classic](#classic-checkpoint) or [uuid-named](#uuid-named-checkpoint)) if [v2 checkpoint table feature](#v2-checkpoint-table-feature) is enabled.\n\nMulti-part checkpoints are [deprecated](#problems-with-multi-part-checkpoints), and writers should avoid creating them. Use uuid-named [V2 spec](#v2-spec) checkpoints instead of these.\n\nMultiple checkpoints could exist for the same table version, e.g. if two clients race to create checkpoints at the same time, but with different formats.\nIn such cases, a client can choose which checkpoint to use.\n\nBecause a multi-part checkpoint cannot be created atomically (e.g. vulnerable to slow and/or failed writes), readers must ignore multi-part checkpoints with missing parts.\n\nCheckpoints for a given version must only be created after the associated delta file has been successfully written.\n\n#### Sidecar Files\n\nA sidecar file contains file actions. These files are in parquet format and they must have unique names.\nThese are then [linked](#sidecar-file-information) to checkpoints. Refer to [V2 checkpoint spec](#v2-spec)\nfor more detail. The sidecar files can have only [add file and remove file](#Add-File-and-Remove-File) entries\nas of now. The add and remove file actions are stored as their individual columns in parquet as struct fields.\n\nThese files reside in the `_delta_log/_sidecars` directory.\n\n### Log Compaction Files\n\nLog compaction files reside in the `_delta_log` directory. A log compaction\nfile from a start version `x` to an end version `y` (`y` must be _greater_ than `x`)\nwill have the following name:\n`<x>.<y>.compacted.json`. This contains the aggregated\nactions for commit range `[x, y]`. Similar to commits, each row in the log\ncompaction file represents an [action](#actions).\nThe commit files for a given range are created by doing [Action Reconciliation](#action-reconciliation)\nof the corresponding commits.\nInstead of reading the individual commit files in range `[x, y]`, an implementation could choose to read\nthe log compaction file `<x>.<y>.compacted.json` to speed up the snapshot construction.\n\nExample:\nSuppose we have `00000000000000000004.json` as:\n```\n{\"commitInfo\":{...}}\n{\"add\":{\"path\":\"f2\",...}}\n{\"remove\":{\"path\":\"f1\",...}}\n```\n`00000000000000000005.json` as:\n```\n{\"commitInfo\":{...}}\n{\"add\":{\"path\":\"f3\",...}}\n{\"add\":{\"path\":\"f4\",...}}\n{\"txn\":{\"appId\":\"3ae45b72-24e1-865a-a211-34987ae02f2a\",\"version\":4389}}\n```\n`00000000000000000006.json` as:\n```\n{\"commitInfo\":{...}}\n{\"remove\":{\"path\":\"f3\",...}}\n{\"txn\":{\"appId\":\"3ae45b72-24e1-865a-a211-34987ae02f2a\",\"version\":4390}}\n```\n\nThen `00000000000000000004.00000000000000000006.compacted.json` will have the following content:\n```\n{\"add\":{\"path\":\"f2\",...}}\n{\"add\":{\"path\":\"f4\",...}}\n{\"remove\":{\"path\":\"f1\",...}}\n{\"remove\":{\"path\":\"f3\",...}}\n{\"txn\":{\"appId\":\"3ae45b72-24e1-865a-a211-34987ae02f2a\",\"version\":4390}}\n```\n\nWriters:\n- Can optionally produce log compactions for any given commit range\n\nReaders:\n- Can optionally consume log compactions, if available\n- The compaction replaces the corresponding commits during action reconciliation\n\n### Last Checkpoint File\nThe Delta transaction log will often contain many (e.g. 10,000+) files.\nListing such a large directory can be prohibitively expensive.\nThe last checkpoint file can help reduce the cost of constructing the latest snapshot of the table by providing a pointer to near the end of the log.\n\nRather than list the entire directory, readers can locate a recent checkpoint by looking at the `_delta_log/_last_checkpoint` file.\nDue to the zero-padded encoding of the files in the log, the version id of this recent checkpoint can be used on storage systems that support lexicographically-sorted, paginated directory listing to enumerate any delta files or newer checkpoints that comprise more recent versions of the table.\n\n### Version Checksum File\n\nThe Delta transaction log must remain an append-only log. To enable the detection of non-compliant modifications to Delta files, writers can optionally emit an auxiliary file with every commit, which contains important information about the state of the table as of that version. This file is referred to as the **Version Checksum** and can be used to validate the integrity of the table.\n\n### Version Checksum File Schema\n\nA Version Checksum file must have the following properties:\n- Be named `{version}.crc` where `version` is zero-padded to 20 digits (e.g., `00000000000000000001.crc`)\n- Be stored directly in the `_delta_log` directory alongside Delta log files\n- Contain exactly one JSON object with the following schema:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\ntxnId | String | A unique identifier for the transaction that produced this commit. | optional\ntableSizeBytes | Long | Total size of the table in bytes, calculated as the sum of the `size` field of all live `add` actions. | required\nnumFiles | Long | Number of live `add` actions in this table version after Action Reconciliation. | required\nnumMetadata | Long | Number of `metaData` actions. Must be 1. | required\nnumProtocol | Long | Number of `protocol` actions. Must be 1. | required\ninCommitTimestampOpt | Long | The in-commit timestamp of this version. Present if and only if [In-Commit Timestamps](#in-commit-timestamps) are enabled. | optional\nsetTransactions | Array[`txn`] | Live [Transaction Identifier](#transaction-identifiers) actions at this version. | optional\ndomainMetadata | Array[`domainMetadata`] | Live [Domain Metadata](#domain-metadata) actions at this version, excluding tombstones. | optional\nmetadata | Metadata | The table [metadata](#change-metadata) at this version. | required\nprotocol | Protocol | The table [protocol](#protocol-evolution) at this version. | required\nfileSizeHistogram | FileSizeHistogram | Size distribution information of files remaining after [Action Reconciliation](#action-reconciliation). See [FileSizeHistogram](#file-size-histogram-schema) for more details. | optional\nallFiles | Array[`add`] | All live [Add File](#add-file-and-remove-file) actions at this version. | optional\nnumDeletedRecordsOpt | Long | Number of records deleted through Deletion Vectors in this table version. | optional\nnumDeletionVectorsOpt | Long | Number of Deletion Vectors active in this table version. | optional\ndeletedRecordCountsHistogramOpt | DeletedRecordCountsHistogram | Distribution of deleted record counts across files. See [this](#deleted-record-counts-histogram-schema) section for more details. | optional\n\n##### File Size Histogram Schema\n\nThe `FileSizeHistogram` object represents a histogram tracking file counts and total bytes across different size ranges. It has the following schema:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\nsortedBinBoundaries | Array[Long] | A sorted array of bin boundaries where each element represents the start of a bin (inclusive) and the next element represents the end of the bin (exclusive). The first element must be 0. | required\nfileCounts | Array[Long] | Count of files in each bin. Length must match `sortedBinBoundaries`. | required\ntotalBytes | Array[Long] | Total bytes of files in each bin. Length must match `sortedBinBoundaries`. | required\n\nEach index `i` in these arrays corresponds to a size range from `sortedBinBoundaries[i]` (inclusive) up to but not including `sortedBinBoundaries[i+1]`. The last bin ends at positive infinity. For example, given boundaries `[0, 1024, 4096]`:\n- Bin 0 contains files of size [0, 1024) bytes\n- Bin 1 contains files of size [1024, 4096) bytes\n- Bin 2 contains files of size [4096, ∞) bytes\n\nThe arrays `fileCounts` and `totalBytes` store the number of files and their total size respectively that fall into each bin. This data structure enables efficient analysis of file size distributions in Delta tables.\n\n### Deleted Record Counts Histogram Schema\n\nThe `DeletedRecordCountsHistogram` object represents a histogram tracking the distribution of deleted record counts across files in the table. Each bin in the histogram represents a range of deletion counts and stores the number of files having that many deleted records.\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\ndeletedRecordCounts | Array[Long] | Array of size 10 where each element represents the count of files falling into a specific deletion count range. | required\n\nThe histogram bins correspond to the following ranges:\n- Bin 0: [0, 0] (files with no deletions)\n- Bin 1: [1, 9] (files with 1-9 deleted records)\n- Bin 2: [10, 99] (files with 10-99 deleted records)\n- Bin 3: [100, 999] (files with 100-999 deleted records) \n- Bin 4: [1000, 9999] (files with 1,000-9,999 deleted records)\n- Bin 5: [10000, 99999] (files with 10,000-99,999 deleted records)\n- Bin 6: [100000, 999999] (files with 100,000-999,999 deleted records)\n- Bin 7: [1000000, 9999999] (files with 1,000,000-9,999,999 deleted records)\n- Bin 8: [10000000, 2147483646] (files with 10,000,000 to 2147483646 (i.e. Int.MaxValue-1 in Java) deleted records)\n- Bin 9: [2147483647, ∞) (files with 2147483647 or more deleted records)\n\nThis histogram allows analyzing the distribution of deleted records across files in a Delta table, which can be useful for monitoring and optimizing deletion patterns.\n\n#### State Validation\n\nReaders can validate table state integrity at a particular version by:\n1. Reading the Version Checksum file for that version\n2. Independently computing the same metrics by performing [Action Reconciliation](#action-reconciliation) on the table state\n3. Comparing the computed values against those recorded in the Version Checksum\n\nIf any discrepancy is found between computed and recorded values, the table state at that version should be considered potentially corrupted.\n\n### Writer Requirements\n\n- Writers SHOULD produce a Version Checksum file for each commit\n- Writers MUST ensure all metrics in the Version Checksum accurately reflect table state after Action Reconciliation\n- Writers MUST write the Version Checksum file only after successfully writing the corresponding Delta log entry\n- Writers MUST NOT overwrite existing Version Checksum files\n\n### Reader Requirements\n\n- Readers MAY use Version Checksums to validate table state integrity\n- If performing validation, readers SHOULD verify all required fields match computed values\n- If validation fails, readers SHOULD surface the discrepancy to users via error messaging\n- Readers MUST continue functioning if Version Checksum files are missing\n\n\n## Actions\nActions modify the state of the table and they are stored both in delta files and in checkpoints.\nThis section lists the space of available actions as well as their schema.\n\n### Change Metadata\nThe `metaData` action changes the current metadata of the table.\nThe first version of a table must contain a `metaData` action.\nSubsequent` metaData` actions completely overwrite the current metadata of the table.\n\nThere can be at most one metadata action in a given version of the table.\n\nEvery metadata action **must** include required fields at a minimum.\n\nThe schema of the `metaData` action is as follows:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\nid|`GUID`|Unique identifier for this table | required\nname|`String`| User-provided identifier for this table | optional\ndescription|`String`| User-provided description for this table | optional\nformat|[Format Struct](#Format-Specification)| Specification of the encoding for the files stored in the table | required\nschemaString|[Schema Struct](#Schema-Serialization-Format)| Schema of the table | required\npartitionColumns|`Array[String]`| An array containing the names of columns by which the data should be partitioned | required\ncreatedTime|`Option[Long]`| The time when this metadata action is created, in milliseconds since the Unix epoch | optional\nconfiguration|`Map[String, String]`| A map containing configuration options for the metadata action | required\n\n#### Format Specification\nField Name | Data Type | Description\n-|-|-\nprovider|`String`|Name of the encoding for files in this table\noptions|`Map[String, String]`|A map containing configuration options for the format\n\nIn the reference implementation, the provider field is used to instantiate a Spark SQL [`FileFormat`](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileFormat.scala). As of Spark 2.4.3 there is built-in `FileFormat` support for `parquet`, `csv`, `orc`, `json`, and `text`.\n\nAs of Delta Lake 0.3.0, user-facing APIs only allow the creation of tables where `format = 'parquet'` and `options = {}`. Support for reading other formats is present both for legacy reasons and to enable possible support for other formats in the future (See [#87](https://github.com/delta-io/delta/issues/87)).\n\nThe following is an example `metaData` action:\n```json\n{\n  \"metaData\":{\n    \"id\":\"af23c9d7-fff1-4a5a-a2c8-55c59bd782aa\",\n    \"format\":{\"provider\":\"parquet\",\"options\":{}},\n    \"schemaString\":\"...\",\n    \"partitionColumns\":[],\n    \"configuration\":{\n      \"appendOnly\": \"true\"\n    }\n  }\n}\n```\n\n<!-- TODO: forward references configuration options -->\n\n### Add File and Remove File\nThe `add` and `remove` actions are used to modify the data in a table by adding or removing individual _logical files_ respectively.\n\nEvery _logical file_ of the table is represented by a path to a data file, combined with an optional Deletion Vector (DV) that indicates which rows of the data file are no longer in the table. Deletion Vectors are an optional feature, see their [reader requirements](#deletion-vectors) for details.\n\nWhen an `add` action is encountered for a logical file that is already present in the table, statistics and other information from the latest version should replace that from any previous version.\nThe primary key for the entry of a logical file in the set of files is a tuple of the data file's `path` and a unique id describing the DV. If no DV is part of this logical file, then its primary key is `(path, NULL)` instead.\n\nThe `remove` action includes a timestamp that indicates when the removal occurred.\nPhysical deletion of physical files can happen lazily after some user-specified expiration time threshold.\nThis delay allows concurrent readers to continue to execute against a stale snapshot of the data.\nA `remove` action should remain in the state of the table as a _tombstone_ until it has expired.\nA tombstone expires when *current time* (according to the node performing the cleanup) exceeds the expiration threshold added to the `remove` action timestamp.\n\nIn the following statements, `dvId` can refer to either the unique id of a specific Deletion Vector (`deletionVector.uniqueId`) or to `NULL`, indicating that no rows are invalidated. Since actions within a given Delta commit are not guaranteed to be applied in order, a **valid** version is restricted to contain at most one file action *of the same type* (i.e. `add`/`remove`) for any one combination of `path` and `dvId`. Moreover, for simplicity it is required that there is at most one file action of the same type for any `path` (regardless of `dvId`).\nThat means specifically that for any commit…\n\n - it is **legal** for the same `path` to occur in an `add` action and a `remove` action, but with two different `dvId`s.\n - it is **legal** for the same `path` to be added and/or removed and also occur in a `cdc` action.\n - it is **illegal** for the same `path` to occur twice with different `dvId`s within each set of `add` or `remove` actions.\n - it is **illegal** for a `path` to occur in an `add` action that already occurs with a different `dvId` in the list of `add` actions from the snapshot of the version immediately preceeding the commit, unless the commit also contains a remove for the later combination.\n - it is **legal** to commit an existing `path` and `dvId` combination again (this allows metadata updates).\n\nThe `dataChange` flag on either an `add` or a `remove` can be set to `false` to indicate that an action when combined with other actions in the same atomic version only rearranges existing data or adds new statistics.\nFor example, streaming queries that are tailing the transaction log can use this flag to skip actions that would not affect the final results.\n\nThe schema of the `add` action is as follows:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\npath| String | A relative path to a data file from the root of the table or an absolute path to a file that should be added to the table. The path is a URI as specified by [RFC 2396 URI Generic Syntax](https://www.ietf.org/rfc/rfc2396.txt), which needs to be decoded to get the data file path. | required\npartitionValues| Map[String, String] | A map from partition column to value for this logical file. See also [Partition Value Serialization](#Partition-Value-Serialization) | required\nsize| Long | The size of this data file in bytes | required\nmodificationTime | Long | The time this logical file was created, as milliseconds since the epoch | required\ndataChange | Boolean | When `false` the logical file must already be present in the table or the records in the added file must be contained in one or more `remove` actions in the same version | required\nstats | [Statistics Struct](#Per-file-Statistics) | Contains statistics (e.g., count, min/max values for columns) about the data in this logical file | optional\ntags | Map[String, String] | Map containing metadata about this logical file | optional\ndeletionVector | [DeletionVectorDescriptor Struct](#Deletion-Vectors) | Either null (or absent in JSON) when no DV is associated with this data file, or a struct (described below) that contains necessary information about the DV that is part of this logical file. | optional\nbaseRowId | Long  | Default generated Row ID of the first row in the file. The default generated Row IDs of the other rows in the file can be reconstructed by adding the physical index of the row within the file to the base Row ID. See also [Row IDs](#row-ids) | optional\ndefaultRowCommitVersion | Long | First commit version in which an `add` action with the same `path` was committed to the table. | optional\nclusteringProvider | String | The name of the clustering implementation. See also [Clustered Table](#clustered-table)| optional\n\nThe following is an example `add` action for a partitioned table:\n```json\n{\n  \"add\": {\n    \"path\": \"date=2017-12-10/part-000...c000.gz.parquet\",\n    \"partitionValues\": {\"date\": \"2017-12-10\"},\n    \"size\": 841454,\n    \"modificationTime\": 1512909768000,\n    \"dataChange\": true,\n    \"baseRowId\": 4071,\n    \"defaultRowCommitVersion\": 41,\n    \"stats\": \"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"val...\"\n  }\n}\n```\n\nThe following is an example `add` action for a clustered table:\n```json\n{\n  \"add\": {\n    \"path\": \"date=2017-12-10/part-000...c000.gz.parquet\",\n    \"partitionValues\": {},\n    \"size\": 841454,\n    \"modificationTime\": 1512909768000,\n    \"dataChange\": true,\n    \"baseRowId\": 4071,\n    \"defaultRowCommitVersion\": 41,\n    \"clusteringProvider\": \"liquid\",\n    \"stats\": \"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"val...\"\n  }\n}\n```\n\nThe schema of the `remove` action is as follows:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\npath| String | A relative path to a file from the root of the table or an absolute path to a file that should be removed from the table. The path is a URI as specified by [RFC 2396 URI Generic Syntax](https://www.ietf.org/rfc/rfc2396.txt), which needs to be decoded to get the data file path. | required\ndeletionTimestamp | Option[Long] | The time the deletion occurred, represented as milliseconds since the epoch | optional\ndataChange | Boolean | When `false` the records in the removed file must be contained in one or more `add` file actions in the same version | required\nextendedFileMetadata | Boolean | When `true` the fields `partitionValues`, `size`, and `tags` are present | optional\npartitionValues| Map[String, String] | A map from partition column to value for this file. See also [Partition Value Serialization](#Partition-Value-Serialization) | optional\nsize| Long | The size of this data file in bytes | optional\nstats | [Statistics Struct](#Per-file-Statistics) | Contains statistics (e.g., count, min/max values for columns) about the data in this logical file | optional\ntags | Map[String, String] | Map containing metadata about this file | optional\ndeletionVector | [DeletionVectorDescriptor Struct](#Deletion-Vectors) | Either null (or absent in JSON) when no DV is associated with this data file, or a struct (described below) that contains necessary information about the DV that is part of this logical file. | optional\nbaseRowId | Long | Default generated Row ID of the first row in the file. The default generated Row IDs of the other rows in the file can be reconstructed by adding the physical index of the row within the file to the base Row ID. See also [Row IDs](#row-ids) | optional\ndefaultRowCommitVersion | Long | First commit version in which an `add` action with the same `path` was committed to the table | optional\n\nThe following is an example `remove` action.\n```json\n{\n  \"remove\": {\n    \"path\": \"part-00001-9…..snappy.parquet\",\n    \"deletionTimestamp\": 1515488792485,\n    \"baseRowId\": 4071,\n    \"defaultRowCommitVersion\": 41,\n    \"dataChange\": true\n  }\n}\n```\n\n### Add CDC File\nThe `cdc` action is used to add a [file](#change-data-files) containing only the data that was changed as part of the transaction. The `cdc` action is allowed to add a [Data File](#data-files) that is also added by an `add` action, when it does not contain any copied rows and the `_change_type` column is filled for all rows.\n\nWhen change data readers encounter a `cdc` action in a particular Delta table version, they must read the changes made in that version exclusively using the `cdc` files. If a version has no `cdc` action, then the data in `add` and `remove` actions are read as inserted and deleted rows, respectively.\n\nThe schema of the `cdc` action is as follows:\n\nField Name | Data Type | Description\n-|-|-\npath| String | A relative path to a change data file from the root of the table or an absolute path to a change data file that should be added to the table. The path is a URI as specified by [RFC 2396 URI Generic Syntax](https://www.ietf.org/rfc/rfc2396.txt), which needs to be decoded to get the file path.\npartitionValues| Map[String, String] | A map from partition column to value for this file. See also [Partition Value Serialization](#Partition-Value-Serialization)\nsize| Long | The size of this file in bytes\ndataChange | Boolean | Should always be set to `false` for `cdc` actions because they _do not_ change the underlying data of the table\ntags | Map[String, String] | Map containing metadata about this file\n\nThe following is an example of `cdc` action.\n\n```json\n{\n  \"cdc\": {\n    \"path\": \"_change_data/cdc-00001-c…..snappy.parquet\",\n    \"partitionValues\": {},\n    \"size\": 1213,\n    \"dataChange\": false\n  }\n}\n```\n\n#### Writer Requirements for AddCDCFile\n\nFor [Writer Versions 4 up to 6](#Writer-Version-Requirements), all writers must respect the `delta.enableChangeDataFeed` configuration flag in the metadata of the table. When `delta.enableChangeDataFeed` is `true`, writers must produce the relevant `AddCDCFile`'s for any operation that changes data, as specified in [Change Data Files](#change-data-files).\n\nFor Writer Version 7, all writers must respect the `delta.enableChangeDataFeed` configuration flag in the metadata of the table only if the feature `changeDataFeed` exists in the table `protocol`'s `writerFeatures`.\n\n#### Reader Requirements for AddCDCFile\n\nWhen available, change data readers should use the `cdc` actions in a given table version instead of computing changes from the underlying data files referenced by the `add` and `remove` actions.\nSpecifically, to read the row-level changes made in a version, the following strategy should be used:\n1. If there are `cdc` actions in this version, then read only those to get the row-level changes, and skip the remaining `add` and `remove` actions in this version.\n2. Otherwise, if there are no `cdc` actions in this version, read and treat all the rows in the `add` and `remove` actions as inserted and deleted rows, respectively.\n3. Change data readers should return the following extra columns:\n\n    Field Name | Data Type | Description\n    -|-|-\n    _commit_version|`Long`| The table version containing the change. This can be derived from the name of the Delta log file that contains actions.\n    _commit_timestamp|`Timestamp`| The timestamp associated when the commit was created. Depending on whether [In-Commit Timestamps](#in-commit-timestamps) are enabled, this is derived from either the `inCommitTimestamp` field of the `commitInfo` action of the version's Delta log file, or from the Delta log file's modification time.\n\n##### Note for non-change data readers\n\nIn a table with Change Data Feed enabled, the data Parquet files referenced by `add` and `remove` actions are allowed to contain an extra column `_change_type`. This column is not present in the table's schema. When accessing these files, readers should disregard this column and only process columns defined within the table's schema.\n\n### Transaction Identifiers\nIncremental processing systems (e.g., streaming systems) that track progress using their own application-specific versions need to record what progress has been made, in order to avoid duplicating data in the face of failures and retries during a write.\nTransaction identifiers allow this information to be recorded atomically in the transaction log of a delta table along with the other actions that modify the contents of the table.\n\nTransaction identifiers are stored in the form of `appId` `version` pairs, where `appId` is a unique identifier for the process that is modifying the table and `version` is an indication of how much progress has been made by that application.\nThe atomic recording of this information along with modifications to the table enables these external system to make their writes into a Delta table _idempotent_.\n\nFor example, the [Delta Sink for Apache Spark's Structured Streaming](https://github.com/delta-io/delta/blob/master/core/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSink.scala) ensures exactly-once semantics when writing a stream into a table using the following process:\n 1. Record in a write-ahead-log the data that will be written, along with a monotonically increasing identifier for this batch.\n 2. Check the current version of the transaction with `appId = streamId` in the target table. If this value is greater than or equal to the batch being written, then this data has already been added to the table and processing can skip to the next batch.\n 3. Write the data optimistically into the table.\n 4. Attempt to commit the transaction containing both the addition of the data written out and an updated `appId` `version` pair.\n\nThe semantics of the application-specific `version` are left up to the external system.\nDelta only ensures that the latest `version` for a given `appId` is available in the table snapshot.\nThe Delta transaction protocol does not, for example, assume monotonicity of the `version` and it would be valid for the `version` to decrease, possibly representing a \"rollback\" of an earlier transaction.\n\nThe schema of the `txn` action is as follows:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\nappId | String | A unique identifier for the application performing the transaction | required\nversion | Long | An application-specific numeric identifier for this transaction | required\nlastUpdated | Option[Long] | The time when this transaction action is created, in milliseconds since the Unix epoch | optional\n\nThe following is an example `txn` action:\n```json\n{\n  \"txn\": {\n    \"appId\":\"3ba13872-2d47-4e17-86a0-21afd2a22395\",\n    \"version\":364475\n  }\n}\n```\n\n### Protocol Evolution\nThe `protocol` action is used to increase the version of the Delta protocol that is required to read or write a given table.\nProtocol versioning allows a newer client to exclude older readers and/or writers that are missing features required to correctly interpret the transaction log.\nThe _protocol version_ will be increased whenever non-forward-compatible changes are made to this specification.\nIn the case where a client is running an invalid protocol version, an error should be thrown instructing the user to upgrade to a newer protocol version of their Delta client library.\n\nSince breaking changes must be accompanied by an increase in the protocol version recorded in a table or by the addition of a table feature, clients can assume that unrecognized actions, fields, and/or metadata domains are never required in order to correctly interpret the transaction log. Clients must ignore such unrecognized fields, and should not produce an error when reading a table that contains unrecognized fields.\n\nReader Version 3 and Writer Version 7 add two lists of table features to the protocol action. The capability for readers and writers to operate on such a table is not only dependent on their supported protocol versions, but also on whether they support all features listed in `readerFeatures` and `writerFeatures`. See [Table Features](#table-features) section for more information.\n\nThe schema of the `protocol` action is as follows:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\nminReaderVersion | Int | The minimum version of the Delta read protocol that a client must implement in order to correctly *read* this table | required\nminWriterVersion | Int | The minimum version of the Delta write protocol that a client must implement in order to correctly *write* this table | required\nreaderFeatures | Array[String] | A collection of features that a client must implement in order to correctly read this table (exist only when `minReaderVersion` is set to `3`) | optional\nwriterFeatures | Array[String] | A collection of features that a client must implement in order to correctly write this table (exist only when `minWriterVersion` is set to `7`) | optional\n\nSome example Delta protocols:\n```json\n{\n  \"protocol\":{\n    \"minReaderVersion\":1,\n    \"minWriterVersion\":2\n  }\n}\n```\n\nA table that is using table features only for writers:\n```json\n{\n  \"protocol\":{\n    \"readerVersion\":2,\n    \"writerVersion\":7,\n    \"writerFeatures\":[\"columnMapping\",\"identityColumns\"]\n  }\n}\n```\nReader version 2 in the above example does not support listing reader features but supports Column Mapping. This example is equivalent to the next one, where Column Mapping is represented as a reader table feature.\n\nA table that is using table features for both readers and writers:\n```json\n{\n  \"protocol\": {\n    \"readerVersion\":3,\n    \"writerVersion\":7,\n    \"readerFeatures\":[\"columnMapping\"],\n    \"writerFeatures\":[\"columnMapping\",\"identityColumns\"]\n  }\n}\n```\n\n### Commit Provenance Information\nA delta file can optionally contain additional provenance information about what higher-level operation was being performed as well as who executed it.\n\nWhen the `catalogManaged` table feature is enabled, the `commitInfo` action must have a field\n`txnId` that stores a unique transaction identifier string.\n\nImplementations are free to store any valid JSON-formatted data via the `commitInfo` action.\n\nWhen [In-Commit Timestamps](#in-commit-timestamps) are enabled, writers are required to include a `commitInfo` action with every commit, which must include the `inCommitTimestamp` field. Also, the `commitInfo` action must be first action in the commit.\n\nAn example of storing provenance information related to an `INSERT` operation:\n```json\n{\n  \"commitInfo\":{\n    \"timestamp\":1515491537026,\n    \"userId\":\"100121\",\n    \"userName\":\"michael@databricks.com\",\n    \"operation\":\"INSERT\",\n    \"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\n    \"notebook\":{\n      \"notebookId\":\"4443029\",\n      \"notebookPath\":\"Users/michael@databricks.com/actions\"},\n      \"clusterId\":\"1027-202406-pooh991\"\n  }  \n}\n```\n\n### Domain Metadata\nThe domain metadata action contains a configuration (string) for a named metadata domain. Two overlapping transactions conflict if they both contain a domain metadata action for the same metadata domain.\n\nThere are two types of metadata domains:\n1. **User-controlled metadata domains** have names that start with anything other than the `delta.` prefix. Any Delta client implementation or user application can modify these metadata domains, and can allow users to modify them arbitrarily. Delta clients and user applications are encouraged to use a naming convention designed to avoid conflicts with other clients' or users' metadata domains (e.g. `com.databricks.*` or `org.apache.*`).\n2. **System-controlled metadata domains** have names that start with the `delta.` prefix. This prefix is reserved for metadata domains defined by the Delta spec, and Delta client implementations must not allow users to modify the metadata for system-controlled domains. A Delta client implementation should only update metadata for system-controlled domains that it knows about and understands. System-controlled metadata domains are used by various table features and each table feature may impose additional semantics on the metadata domains it uses.\n\nThe schema of the `domainMetadata` action is as follows:\n\nField Name | Data Type | Description\n-|-|-\ndomain | String | Identifier for this domain (system- or user-provided)\nconfiguration | String | String containing configuration for the metadata domain\nremoved | Boolean | When `true`, the action serves as a tombstone to logically delete a metadata domain. Writers should preserve an accurate pre-image of the configuration.\n\nTo support this feature:\n- The table must be on Writer Version 7.\n- A feature name `domainMetadata` must exist in the table's `writerFeatures`.\n\n#### Reader Requirements for Domain Metadata\n- Readers are not required to support domain metadata.\n- Readers who choose not to support domain metadata should ignore metadata domain actions as unrecognized (see [Protocol Evolution](#protocol-evolution)) and snapshots should not include any metadata domains.\n- Readers who choose to support domain metadata must apply [Action Reconciliation](#action-reconciliation) to all metadata domains and snapshots must include them -- even if the reader does not understand them.\n- Any system-controlled domain that imposes any requirements on readers is a [breaking change](#protocol-evolution), and must be part of a reader-writer table feature that specifies the desired behavior.\n\n#### Writer Requirements for Domain Metadata\n- Writers must preserve all domains even if they don't understand them.\n- Writers must not allow users to modify or delete system-controlled domains.\n- Writers must only modify or delete system-controlled domains they understand.\n- Any system-controlled domain that imposes additional requirements on the writer is a [breaking change](#protocol-evolution), and must be part of a writer table feature that specifies the desired behavior.\n\nThe following is an example `domainMetadata` action:\n```json\n{\n  \"domainMetadata\": {\n    \"domain\": \"delta.deltaTableFeatureX\",\n    \"configuration\": \"{\\\"key1\\\":\\\"value1\\\"}\",\n    \"removed\": false\n  }\n}\n```\n\n### Sidecar File Information\nThe `sidecar` action references a [sidecar file](#sidecar-files) which provides some of the checkpoint's file actions.\nThis action is only allowed in checkpoints following [V2 spec](#v2-spec).\nThe schema of `sidecar` action is as follows:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\npath | String | URI-encoded path to the sidecar file. Because sidecar files must always reside in the table's own _delta_log/_sidecars directory, implementations are encouraged to store only the file's name (without scheme or parent directories). | required\nsizeInBytes | Long | Size of the sidecar file. | required\nmodificationTime | Long | The time this logical file was created, as milliseconds since the epoch. | required\ntags|`Map[String, String]`|Map containing any additional metadata about the checkpoint sidecar file. | optional\n\nThe following is an example `sidecar` action:\n```json\n{\n  \"sidecar\":{\n    \"path\": \"016ae953-37a9-438e-8683-9a9a4a79a395.parquet\",\n    \"sizeInBytes\": 2304522,\n    \"modificationTime\": 1512909768000,\n    \"tags\": {}\n  }\n}\n```\n\n#### Checkpoint Metadata\nThis action is only allowed in checkpoints following [V2 spec](#v2-spec).\nIt describes the details about the checkpoint. It has the following schema:\n\nField Name | Data Type | Description | optional/required\n-|-|-|-\nversion|`Long`|The checkpoint version.| required\ntags|`Map[String, String]`|Map containing any additional metadata about the v2 spec checkpoint.| optional\n\nE.g.\n```json\n{\n  \"checkpointMetadata\":{\n    \"version\":1,\n    \"tags\":{}\n  }\n}\n```\n\n# Action Reconciliation\nA given snapshot of the table can be computed by replaying the events committed to the table in ascending order by commit version. A given snapshot of a Delta table consists of:\n\n - A single `protocol` action\n - A single `metaData` action\n - A collection of `txn` actions with unique `appId`s\n - A collection of `domainMetadata` actions with unique `domain`s.\n - A collection of `add` actions with unique path keys, corresponding to the newest (path, deletionVector.uniqueId) pair encountered for each path.\n - A collection of `remove` actions with unique `(path, deletionVector.uniqueId)` keys. The intersection of the primary keys in the `add` collection and `remove` collection must be empty. That means a logical file cannot exist in both the `remove` and `add` collections at the same time; however, the same *data file* can exist with *different* DVs in the `remove` collection, as logically they represent different content. The `remove` actions act as _tombstones_, and only exist for the benefit of the VACUUM command. Snapshot reads only return `add` actions on the read path.\n \nTo achieve the requirements above, related actions from different delta files need to be reconciled with each other:\n \n - The latest `protocol` action seen wins\n - The latest `metaData` action seen wins\n - For `txn` actions, the latest `version` seen for a given `appId` wins\n - For `domainMetadata`, the latest `domainMetadata` seen for a given `domain` wins. The actions with `removed=true` act as tombstones to suppress earlier versions. Snapshot reads do _not_ return removed `domainMetadata` actions.\n - For `commitInfo` actions, only the `commitInfo` from the commit at the snapshot version is included in the snapshot. [Checkpoints](#checkpoints) and [log compaction files](#log-compaction-files) do not preserve `commitInfo` actions, so this information must be read from the JSON commit file at the snapshot version.\n - Logical files in a table are identified by their `(path, deletionVector.uniqueId)` primary key. File actions (`add` or `remove`) reference logical files, and a log can contain any number of references to a single file.\n - To replay the log, scan all file actions and keep only the newest reference for each logical file.\n - `add` actions in the result identify logical files currently present in the table (for queries). `remove` actions in the result identify tombstones of logical files no longer present in the table (for VACUUM).\n - [v2 checkpoint spec](#v2-spec) actions are not allowed in normal commit files, and do not participate in log replay.\n\n# Table Features\nTable features must only exist on tables that have a supported protocol version. When the table's Reader Version is 3, `readerFeatures` must exist in the `protocol` action, and when the Writer Version is 7, `writerFeatures` must exist in the `protocol` action. `readerFeatures` and `writerFeatures` define the features that readers and writers must implement in order to read and write this table.\n\nReaders and writers must not ignore table features when they are present:\n - to read a table, readers must implement and respect all features listed in `readerFeatures`;\n - to write a table, writers must implement and respect all features listed in `writerFeatures`. Because writers have to read the table (or only the Delta log) before write, they must implement and respect all reader features as well.\n\n## Table Features for New and Existing Tables\nIt is possible to create a new table or upgrade an existing table to the protocol versions that supports the use of table features. A table must support either the use of writer features or both reader and writer features. It is illegal to support reader but not writer features.\n\nFor new tables, when a new table is created with a Reader Version up to 2 and Writer Version 7, its `protocol` action must only contain `writerFeatures`. When a new table is created with Reader Version 3 and Writer Version 7, its `protocol` action must contain both `readerFeatures` and `writerFeatures`. Creating a table with a Reader Version 3 and Writer Version less than 7 is not allowed.\n\nWhen upgrading an existing table to Reader Version 3 and/or Writer Version 7, the client should, on a best effort basis, determine which features supported by the original protocol version are used in any historical version of the table, and add only used features to reader and/or writer feature sets. The client must assume a feature has been used, unless it can prove that the feature is *definitely* not used in any historical version of the table that is reachable by time travel. \n\nFor example, given a table on Reader Version 1 and Writer Version 4, along with four versions:\n 1. Table property change: set `delta.enableChangeDataFeed` to `true`.\n 2. Data change: three rows updated.\n 3. Table property change: unset `delta.enableChangeDataFeed`.\n 4. Table protocol change: upgrade protocol to Reader Version 3 and Writer Version 7.\n\nTo produce Version 4, a writer could look at only Version 3 and discover that Change Data Feed has not been used. But in fact, this feature has been used and the table does contain some Change Data Files for Version 2. This means that, to determine all features that have ever been used by the table, a writer must either scan the whole history (which is very time-consuming) or assume the worst case: all features supported by protocol `(1, 4)` has been used.\n\n## Supported Features\nA feature is supported by a table when its name is in the `protocol` action’s `readerFeatures` and/or `writerFeatures`. Subsequent read and/or write operations on this table must respect the feature. Clients must not remove the feature from the `protocol` action.\n\nWriters are allowed to add support of a feature to the table by adding its name to `readerFeatures` or `writerFeatures`. Reader features should be listed in both `readerFeatures` and `writerFeatures` simultaneously, while writer features should be listed only in `writerFeatures`. It is not allowed to list a feature only in `readerFeatures` but not in `writerFeatures`.\n\nA feature being supported does not imply that it is active. For example, a table may have the [Append-only Tables](#append-only-tables) feature (feature name `appendOnly`) listed in `writerFeatures`, but it does not have a table property `delta.appendOnly` that is set to `true`. In such a case the table is not append-only, and writers are allowed to change, remove, and rearrange data. However, writers must know that the table property `delta.appendOnly` should be checked before writing the table.\n\n## Active Features\nA feature is active on a table when it is supported *and* its metadata requirements are satisfied. Each feature defines its own metadata requirements, as stated in the corresponding sections of this document. For example, the Append-only feature is active when the `appendOnly` feature name is present in a `protocol`'s `writerFeatures` *and* a table property `delta.appendOnly` set to `true`.\n\n# Column Mapping\nDelta can use column mapping to avoid any column naming restrictions, and to support the renaming and dropping of columns without having to rewrite all the data. There are two modes of column mapping, by `name` and by `id`. In both modes, every column - nested or leaf - is assigned a unique _physical_ name, and a unique 32-bit integer as an id. The physical name is stored as part of the column metadata with the key `delta.columnMapping.physicalName`. The column id is stored within the metadata with the key `delta.columnMapping.id`.\n\nThe column mapping is governed by the table property `delta.columnMapping.mode` being one of `none`, `id`, and `name`. The table property should only be honored if the table's protocol has reader and writer versions and/or table features that support the `columnMapping` table feature. For readers this is Reader Version 2, or Reader Version 3 with the `columnMapping` table feature listed as supported. For writers this is Writer Version 5 or 6, or Writer Version 7 with the `columnMapping` table feature supported.\n\nThe following is an example for the column definition of a table that leverages column mapping. See the [appendix](#schema-serialization-format) for a more complete schema definition.\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : {\n      \"type\" : \"array\",\n      \"elementType\" : {\n        \"type\" : \"struct\",\n        \"fields\" : [ {\n          \"name\" : \"d\",\n          \"type\" : \"integer\",\n          \"nullable\" : false,\n          \"metadata\" : { \n            \"delta.columnMapping.id\": 5,\n            \"delta.columnMapping.physicalName\": \"col-a7f4159c-53be-4cb0-b81a-f7e5240cfc49\"\n          }\n        } ]\n      },\n      \"containsNull\" : true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.columnMapping.id\": 4,\n      \"delta.columnMapping.physicalName\": \"col-5f422f40-de70-45b2-88ab-1d5c90e94db1\"\n    }\n  }\n```\n\n## Writer Requirements for Column Mapping\nIn order to support column mapping, writers must:\n - Write `protocol` and `metaData` actions when Column Mapping is turned on for the first time:\n   - If the table is on Writer Version 5 or 6: write a `metaData` action to add the `delta.columnMapping.mode` table property;\n   - If the table is on Writer Version 7:\n     - write a `protocol` action to add the feature `columnMapping` to both `readerFeatures` and `writerFeatures`, and\n     - write a `metaData` action to add the `delta.columnMapping.mode` table property.\n - Write data files by using the _physical name_ that is chosen for each column. The physical name of the column is static and can be different than the _display name_ of the column, which is changeable.\n - Write the 32 bit integer column identifier as part of the `field_id` field of the `SchemaElement` struct in the [Parquet Thrift specification](https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift).\n - Track partition values, column level statistics, and [clustering column](#clustered-table) names with the physical name of the column in the transaction log.\n - Assign a globally unique identifier as the physical name for each new column that is added to the schema. This is especially important for supporting cheap column deletions in `name` mode. In addition, column identifiers need to be assigned to each column. The maximum id that is assigned to a column is tracked as the table property `delta.columnMapping.maxColumnId`. This is an internal table property that cannot be configured by users. This value must increase monotonically as new columns are introduced and committed to the table alongside the introduction of the new columns to the schema.\n\n## Reader Requirements for Column Mapping\nIf the table is on Reader Version 2, or if the table is on Reader Version 3 and the feature `columnMapping` is present in `readerFeatures`, readers and writers must read the table property `delta.columnMapping.mode` and do one of the following.\n\nIn `none` mode, or if the table property is not present, readers must read the parquet files by using the display names (the `name` field of the column definition) of the columns in the schema.\n\nIn `id ` mode, readers must resolve columns by using the `field_id` in the parquet metadata for each file, as given by the column metadata property `delta.columnMapping.id` in the Delta schema. Partition values and column level statistics must be resolved by their *physical names* for each `add` entry in the transaction log. If a data file does not contain field ids, readers must refuse to read that file or return nulls for each column. For ids that cannot be found in a file, readers must return `null` values for those columns.\n\nIn `name` mode, readers must resolve columns in the data files by their physical names as given by the column metadata property `delta.columnMapping.physicalName` in the Delta schema. Partition values and column level statistics will also be resolved by their physical names. For columns that are not found in the files, `null`s need to be returned. Column ids are not used in this mode for resolution purposes.\n\n# Deletion Vectors\nTo support this feature:\n - To support Deletion Vectors, a table must have Reader Version 3 and Writer Version 7. A feature name `deletionVectors` must exist in the table's `readerFeatures` and `writerFeatures`.\n\nWhen supported:\n - A table may have a metadata property `delta.enableDeletionVectors` in the Delta schema set to `true`. Writers must only write new Deletion Vectors (DVs) when this property is set to `true`.\n - A table's `add` and `remove` actions can optionally include a DV that provides information about logically deleted rows, that are however still physically present in the underlying data file and must thus be skipped during processing. Readers must read the table considering the existence of DVs, even when the `delta.enableDeletionVectors` table property is not set.\n\nDVs can be stored and accessed in different ways, indicated by the `storageType` field. The Delta protocol currently supports inline or on-disk storage, where the latter can be accessed either by a relative path derived from a UUID or an absolute path.\n\n## Deletion Vector Descriptor Schema\n\nThe schema of the `DeletionVectorDescriptor` struct is as follows:\n\nField Name | Data Type | Description\n-|-|-\nstorageType | String | A single character to indicate how to access the DV. Legal options are: `['u', 'i', 'p']`.\npathOrInlineDv | String | Three format options are currently proposed:<ul><li>If `storageType = 'u'` then  `<random prefix - optional><base85 encoded uuid>`: The deletion vector is stored in a file with a path relative to the data directory of this Delta table, and the  file name can be reconstructed from the UUID. See Derived Fields for how to reconstruct the file name. The random prefix is recovered as the extra characters before the (20 characters fixed length) uuid.</li><li>If `storageType = 'i'` then `<base85 encoded bytes>`: The deletion vector is stored inline in the log. The format used is the `RoaringBitmapArray` format also used when the DV is stored on disk and described in [Deletion Vector Format](#Deletion-Vector-Format).</li><li>If `storageType = 'p'` then `<absolute path>`: The DV is stored in a file with an absolute path given by this path, which has the same format as the `path` field in the `add`/`remove` actions.</li></ul>\noffset | Option[Int] | Start of the data for this DV in number of bytes from the beginning of the file it is stored in. Always `None` (absent in JSON) when `storageType = 'i'`.\nsizeInBytes | Int | Size of the serialized DV in bytes (raw data size, i.e. before base85 encoding, if inline).\ncardinality | Long | Number of rows the given DV logically removes from the file.\n\nThe concrete Base85 variant used is [Z85](https://rfc.zeromq.org/spec/32/), because it is JSON-friendly.\n\n### Derived Fields\n\nSome fields that are necessary to use the DV are not stored explicitly but can be derived in code from the stored fields.\n\nField Name | Data Type | Description | Computed As\n-|-|-|-\nuniqueId | String | Uniquely identifies a DV for a given file. This is used for snapshot reconstruction to differentiate the same file with different DVs in successive versions. | If `offset` is `None` then `<storageType><pathOrInlineDv>`. <br> Otherwise `<storageType><pathOrInlineDv>@<offset>`.\nabsolutePath | String/URI/Path | The absolute path of the DV file. Can be calculated for relative path DVs by providing a parent directory path. | If `storageType='p'`, just use the already absolute path. If `storageType='u'`, the DV is stored at `<parent path>/<random prefix>/deletion_vector_<uuid in canonical textual representation>.bin`. This is not a legal field if `storageType='i'`, as an inline DV has no absolute path.\n\n### JSON Example 1 — On Disk with Relative Path (with Random Prefix)\n```json\n{\n  \"storageType\" : \"u\",\n  \"pathOrInlineDv\" : \"ab^-aqEH.-t@S}K{vb[*k^\",\n  \"offset\" : 4,\n  \"sizeInBytes\" : 40,\n  \"cardinality\" : 6\n}\n```\nAssuming that this DV is stored relative to an `s3://mytable/` directory, the absolute path to be resolved here would be: `s3://mytable/ab/deletion_vector_d2c639aa-8816-431a-aaf6-d3fe2512ff61.bin`.\n\n### JSON Example 2 — On Disk with Absolute Path\n```json\n{\n  \"storageType\" : \"p\",\n  \"pathOrInlineDv\" : \"s3://mytable/deletion_vector_d2c639aa-8816-431a-aaf6-d3fe2512ff61.bin\",\n  \"offset\" : 4,\n  \"sizeInBytes\" : 40,\n  \"cardinality\" : 6\n}\n```\n\n### JSON Example 3 — Inline\n```json\n{\n  \"storageType\" : \"i\",\n  \"pathOrInlineDv\" : \"wi5b=000010000siXQKl0rr91000f55c8Xg0@@D72lkbi5=-{L\",\n  \"sizeInBytes\" : 40,\n  \"cardinality\" : 6\n}\n```\nThe row indexes encoded in this DV are: 3, 4, 7, 11, 18, 29.\n\n## Reader Requirements for Deletion Vectors\nIf a snapshot contains logical files with records that are invalidated by a DV, then these records *must not* be returned in the output.\n\n## Writer Requirement for Deletion Vectors\nWhen adding a logical file with a deletion vector, then that logical file must have correct `numRecords` information for the data file in the `stats` field.\n\n# Catalog-managed tables\n\nWith this feature enabled, the [catalog](#terminology-catalogs) that manages the table becomes the\nsource of truth for whether a given commit attempt succeeded.\n\nThe table feature defines the parts of the [commit protocol](#commit-protocol) that directly impact\nthe Delta table (e.g. atomicity requirements, publishing, etc). The Delta client and catalog\ntogether are responsible for implementing the Delta-specific aspects of commit as defined by this\nspec, but are otherwise free to define their own APIs and protocols for communication with each\nother.\n\n**NOTE**: Filesystem-based access to catalog-managed tables is not supported. Delta clients are\nexpected to discover and access catalog-managed tables through the managing catalog, not by direct\nlisting in the filesystem. This feature is primarily designed to warn filesystem-based readers that\nmight attempt to access a catalog-managed table's storage location without going through the catalog\nfirst, and to block filesystem-based writers who could otherwise corrupt both the table and the\ncatalog by failing to commit through the catalog.\n\nBefore we can go into details of this protocol feature, we must first align our terminology.\n\n## Terminology: Commits\n\nA commit is a set of [actions](#actions) that transform a Delta table from version `v - 1` to `v`.\nIt contains the same kind of content as is stored in a [Delta file](#delta-log-entries).\n\nA commit may be stored in the file system as a Delta file - either _published_ or _staged_ - or\nstored _inline_ in the managing catalog, using whatever format the catalog prefers.\n\nThere are several types of commits:\n\n1. **Proposed commit**:  A commit that a Delta client has proposed for the next version of the\n   table. It could be _staged_ or _inline_. It will either become _ratified_ or be rejected.\n\n2. <a name=\"staged-commit\">**Staged commit**</a>: A commit that is written to disk at\n   `_delta_log/_staged_commits/<v>.<uuid>.json`. It has the same content and format as a published\n   Delta file.\n    - Here, the `uuid` is a random UUID that is generated for each commit and `v` is the version\n      which is proposed to be committed, zero-padded to 20 digits.\n    - The mere existence of a staged commit does not mean that the file has been ratified or even\n      proposed. It might correspond to a failed or in-progress commit attempt.\n    - The catalog is the source of truth around which staged commits are ratified.\n    - The catalog stores only the location, not the content, of a staged (and ratified) commit.\n\n3. <a name=\"inline-commit\">**Inline commit**</a>: A proposed commit that is not written to disk but\n   rather has its content sent to the catalog for the catalog to store directly.\n\n4. <a name=\"ratified-commit\">**Ratified commit**</a>: A proposed commit that a catalog has\n   determined has won the commit at the desired version of the table.\n    - The catalog must store ratified commits (that is, the staged commit's location or the inline\n      commit's content) until they are published to the `_delta_log` directory.\n    - A ratified commit may or may not yet be published.\n    - A ratified commit may or may not even be stored by the catalog at all - the catalog may\n      have just atomically published it to the filesystem directly, relying on PUT-if-absent\n      primitives to facilitate the ratification and publication all in one step.\n\n5. <a name=\"published-commit\">**Published commit**</a>: A ratified commit that has been copied into\n   the `_delta_log` as a normal Delta file, i.e. `_delta_log/<v>.json`.\n    - Here, the `v` is the version which is being committed, zero-padded to 20 digits.\n    - The existence of a `<v>.json` file proves that the corresponding version `v` is ratified,\n      regardless of whether the table is catalog-managed or filesystem-based. The catalog is allowed\n      to return information about published commits, but Delta clients can also use filesystem\n      listing operations to directly discover them.\n    - Published commits do not need to be stored by the catalog.\n\n## Terminology: Delta Client\n\nThis is the component that implements support for reading and writing Delta tables, and implements\nthe logic required by the `catalogManaged` table feature. Among other things, it\n- triggers the filesystem listing, if needed, to discover published commits\n- generates the commit content (the set of [actions](#actions))\n- works together with the query engine to trigger the commit process and invoke the client-side\n  catalog component with the commit content\n\nThe Delta client is also responsible for defining the client-side API that catalogs should target.\nThat is, there must be _some_ API that the [catalog client](#catalog-client) can use to communicate\nto the Delta client the subset of catalog-managed information that the Delta client cares about.\nThis protocol feature is concerned with what information Delta cares about, but leaves to Delta\nclients the design of the API they use to obtain that information from catalog clients.\n\n## Terminology: Catalogs\n\n1. **Catalog**: A catalog is an entity which manages a Delta table, including its creation, writes,\n   reads, and eventual deletion.\n    - It could be backed by a database, a filesystem, or any other persistence mechanism.\n    - Each catalog has its own spec around how catalog clients should interact with them, and how\n      they perform a commit.\n\n2. <a name=\"catalog-client\">**Catalog Client**</a>: The catalog always has a client-side component\n   which the Delta client interacts with directly. This client-side component has two primary\n   responsibilities:\n    - implement any client-side catalog-specific logic (such as staging or\n      [publishing](#publishing-commits) commits)\n    - communicate with the Catalog Server, if any\n\n3. **Catalog Server**: The catalog may also involve a server-side component which the client-side\n   component would be responsible to communicate with.\n    - This server is responsible for coordinating commits and potentially persisting table metadata\n      and enforcing authorization policies.\n    - Not all catalogs require a server; some may be entirely client-side, e.g. filesystem-backed\n      catalogs, or they may make use of a generic database server and implement all of the catalog's\n      business logic client-side.\n\n**NOTE**: This specification outlines the responsibilities and actions that catalogs must implement.\nThis spec does its best not to assume any specific catalog _implementation_, though it does call out\nlikely client-side and server-side responsibilities. Nonetheless, what a given catalog does\nclient-side or server-side is up to each catalog implementation to decide for itself.\n\n## Catalog Responsibilities\n\nWhen the `catalogManaged` table feature is enabled, a catalog performs commits to the table on behalf\nof the Delta client.\n\nAs stated above, the Delta spec does not mandate any particular client-server design or API for\ncatalogs that manage Delta tables. However, the catalog does need to provide certain capabilities\nfor reading and writing Delta tables:\n\n- Atomically commit a version `v` with a given set of `actions`. This is explained in detail in the\n  [commit protocol](#commit-protocol) section.\n- Retrieve information about recent ratified commits and the latest ratified version on the table.\n  This is explained in detail in the [Getting Ratified Commits from the Catalog](#getting-ratified-commits-from-the-catalog) section.\n- Though not required, it is encouraged that catalogs also return the latest table-level metadata,\n  such as the latest Protocol and Metadata actions, for the table. This can provide significant\n  performance advantages to conforming Delta clients, who may forgo log replay and instead trust\n  the information provided by the catalog during query planning.\n\n## Reading Catalog-managed Tables\n\nA catalog-managed table can have a mix of (a) published and (b) ratified but non-published commits.\nThe catalog is the source of truth for ratified commits. Also recall that ratified commits can be\n[staged commits](#staged-commit) that are persisted to the `_delta_log/_staged_commits` directory,\nor [inline commits](#inline-commit) whose content the catalog stores directly.\n\nFor example, suppose the `_delta_log` directory contains the following files:\n\n```\n00000000000000000000.json\n00000000000000000001.json\n00000000000000000002.checkpoint.parquet\n00000000000000000002.json\n00000000000000000003.00000000000000000005.compacted.json\n00000000000000000003.json\n00000000000000000004.json\n00000000000000000005.json\n00000000000000000006.json\n00000000000000000007.json\n_staged_commits/00000000000000000007.016ae953-37a9-438e-8683-9a9a4a79a395.json // ratified and published\n_staged_commits/00000000000000000008.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json // ratified\n_staged_commits/00000000000000000008.b91807ba-fe18-488c-a15e-c4807dbd2174.json // rejected\n_staged_commits/00000000000000000010.0f707846-cd18-4e01-b40e-84ee0ae987b0.json // not yet ratified\n_staged_commits/00000000000000000010.7a980438-cb67-4b89-82d2-86f73239b6d6.json // partial file\n```\n\nFurther, suppose the catalog stores the following ratified commits:\n```\n{\n  7  -> \"00000000000000000007.016ae953-37a9-438e-8683-9a9a4a79a395.json\",\n  8  -> \"00000000000000000008.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\",\n  9  -> <inline commit: content stored by the catalog directly>\n}\n```\n\nSome things to note are:\n- the catalog isn't aware that commit 7 was already published - perhaps the response from the\n  filesystem was dropped\n- commit 9 is an inline commit\n- neither of the two staged commits for version 10 have been ratified\n\nTo read such tables, Delta clients must first contact the catalog to get the ratified commits. This\ninforms the Delta client of commits [7, 9] as well as the latest ratified version, 9.\n\nIf this information is insufficient to construct a complete snapshot of the table, Delta clients\nmust LIST the `_delta_log` directory to get information about the published commits. For commits\nthat are both returned by the catalog and already published, Delta clients must treat the catalog's\nversion as authoritative and read the commit returned by the catalog. Additionally, Delta clients\nmust ignore any files with versions greater than the latest ratified commit version returned by the\ncatalog.\n\nCombining these two sets of files and commits enables Delta clients to generate a snapshot at the\nlatest version of the table.\n\n**NOTE**: This spec prescribes the _minimum_ required interactions between Delta clients and\ncatalogs for commits. Catalogs may very well expose APIs and work with Delta clients to be\ninformed of other non-commit [file types](#file-types), such as checkpoint, log\ncompaction, and version checksum files. This would allow catalogs to return additional\ninformation to Delta clients during query and scan planning, potentially allowing Delta\nclients to avoid LISTing the filesystem altogether.\n\n## Commit Protocol\n\nTo start, Delta Clients send the desired actions to be committed to the client-side component of the\ncatalog.\n\nThis component then has several options for proposing, ratifying, and publishing the commit,\ndetailed below.\n\n- Option 1: Write the actions (likely client-side) to a [staged commit file](#staged-commit) in the\n  `_delta_log/_staged_commits` directory and then ratify the staged commit (likely server-side) by\n  atomically recording (in persistent storage of some kind) that the file corresponds to version `v`.\n- Option 2: Treat this as an [inline commit](#inline-commit) (i.e. likely that the client-side\n  component sends the contents to the server-side component) and atomically record (in persistent\n  storage of some kind) the content of the commit as version `v` of the table.\n- Option 3: Catalog implementations that use PUT-if-absent (client- or server-side) can ratify and\n  publish all-in-one by atomically writing a [published commit file](#published-commit)\n  in the `_delta_log` directory. Note that this commit will be considered to have succeeded as soon\n  as the file becomes visible in the filesystem, regardless of when or whether the catalog is made\n  aware of the successful publish. The catalog does not need to store these files.\n\nA catalog must not ratify version `v` until it has ratified version `v - 1`, and it must ratify\nversion `v` at most once.\n\nThe catalog must store both flavors of ratified commits (staged or inline) and make them available\nto readers until they are [published](#publishing-commits).\n\nFor performance reasons, Delta clients are encouraged to establish an API contract where the catalog\nprovides the latest ratified commit information whenever a commit fails due to version conflict.\n\n## Getting Ratified Commits from the Catalog\n\nEven after a commit is ratified, it is not discoverable through filesystem operations until it is\n[published](#publishing-commits).\n\nThe catalog-client is responsible to implement an API (defined by the Delta client) that Delta clients can\nuse to retrieve the latest ratified commit version (authoritative), as well as the set of ratified\ncommits the catalog is still storing for the table. If some commits needed to complete the snapshot\nare not stored by the catalog, as they are already published, Delta clients can issue a filesystem\nLIST operation to retrieve them.\n\nDelta clients must establish an API contract where the catalog provides ratified commit information\nas part of the standard table resolution process performed at query planning time.\n\n## Publishing Commits\n\nPublishing is the process of copying the ratified commit with version `<v>` to\n`_delta_log/<v>.json`. The ratified commit may be a staged commit located in\n`_delta_log/_staged_commits/<v>.<uuid>.json`, or it may be an inline commit whose content the\ncatalog stores itself. Because the content of a ratified commit is immutable, it does not matter\nwhether the client-side, server-side, or both catalog components initiate publishing.\n\nImplementations are strongly encouraged to publish commits promptly. This reduces the number of\ncommits the catalog needs to store internally (and serve up to readers).\n\nCommits must be published _in order_. That is, version `v - 1` must be published _before_ version\n`v`.\n\n**NOTE**: Because commit publishing can happen at any time after the commit succeeds, the file\nmodification timestamp of the published file will not accurately reflect the original commit time.\nFor this reason, catalog-managed tables must use [in-commit-timestamps](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#in-commit-timestamps)\nto ensure stability of time travel reads. Refer to [Writer Requirements for Catalog-managed Tables](#writer-requirements-for-catalog-managed-tables)\nsection for more details.\n\n## Maintenance Operations on Catalog-managed Tables\n\n[Checkpoints](#checkpoints-1) and [Log Compaction Files](#log-compaction-files) can only be created\nfor versions that are already published in the `_delta_log`. In other words, in order to checkpoint\nversion `v` or produce a log compaction file for commit range `x <= v <= y`, `_delta_log/<v>.json`\nmust exist.\n\nNotably, the [Version Checksum File](#version-checksum-file) for version `v` _can_ be created in the\n`_delta_log` even if the commit for version `v` is not published.\n\nBy default, maintenance operations are prohibited unless the managing catalog explicitly permits\nthe client to run them. The only exceptions are checkpoints, log compaction, and version checksum,\nas they are essential for all basic table operations (e.g. reads and writes) to operate reliably.\nAll other maintenance operations such as the following are not allowed by default.\n- [Log and other metadata files clean up](#metadata-cleanup).\n- Data files cleanup, for example VACUUM.\n- Data layout changes, for example OPTIMIZE and REORG.\n\n## Creating and Dropping Catalog-managed Tables\n\nThe catalog and query engine ultimately dictate how to create and drop catalog-managed tables.\n\nAs one example, table creation often works in three phases:\n\n1. An initial catalog operation to obtain a unique storage location which serves as an unnamed\n   \"staging\" table\n2. A table operation that physically initializes a new `catalogManaged`-enabled table at the staging\n   location.\n3. A final catalog operation that registers the new table with its intended name.\n\nDelta clients would primarily be involved with the second step, but an implementation could choose\nto combine the second and third steps so that a single catalog call registers the table as part of\nthe table's first commit.\n\nAs another example, dropping a table can be as simple as removing its name from the catalog (a \"soft\ndelete\"), followed at some later point by a \"hard delete\" that physically purges the data. The Delta\nclient would not be involved at all in this process, because no commits are made to the table.\n\n## Catalog-managed Table Enablement\n\nThe `catalogManaged` table feature is supported and active when:\n- The table is on Reader Version 3 and Writer Version 7.\n- The table has a `protocol` action with `readerFeatures` and `writerFeatures` both containing the\n  feature `catalogManaged`.\n\n## Writer Requirements for Catalog-managed tables\n\nWhen supported and active:\n\n- Writers must discover and access the table using catalog calls, which happens _before_ the table's\n  protocol is known. See [Table Discovery](#table-discovery) for more details.\n- The [in-commit-timestamps](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#in-commit-timestamps)\n  table feature must be supported and active.\n- The `commitInfo` action must also contain a field `txnId` that stores a unique transaction\n  identifier string\n- Writers must follow the catalog's [commit protocol](#commit-protocol) and must not perform\n  ordinary filesystem-based commits against the table.\n- Writers must follow the catalog's [maintenance operation protocol](#maintenance-operations-on-catalog-managed-tables)\n\n## Reader Requirements for Catalog-managed tables\n\nWhen supported and active:\n\n- Readers must discover the table using catalog calls, which happens before the table's protocol\n  is known. See [Table Discovery](#table-discovery) for more details.\n- Readers must contact the catalog for information about unpublished ratified commits.\n- Readers must follow the rules described in the [Reading Catalog-managed Tables](#reading-catalog-managed-tables)\n  section above. Notably\n  - If the catalog said `v` is the latest version, clients must ignore any later versions that may\n    have been published\n  - When the catalog returns a ratified commit for version `v`, readers must use that\n    catalog-supplied commit and ignore any published Delta file for version `v` that might also be\n    present.\n\n## Table Discovery\n\nThe requirements above state that readers and writers must discover and access the table using\ncatalog calls, which occurs _before_ the table's protocol is known. This raises an important\nquestion: how can a client discover a `catalogManaged` Delta table without first knowing that it\n_is_, in fact, `catalogManaged` (according to the protocol)?\n\nTo solve this, first note that, in practice, catalog-integrated engines already ask the catalog to\nresolve a table name to its storage location during the name resolution step. This protocol\ntherefore encourages that the same name resolution step also indicate whether the table is\ncatalog-managed. Surfacing this at the very moment the catalog returns the path imposes no extra\nround-trips, yet it lets the client decide — early and unambiguously — whether to follow the\n`catalogManaged` read and write rules.\n\n## Sample Catalog Client API\n\nThe following is an example of a possible API which a Java-based Delta client might require catalog\nimplementations to target:\n\n```scala\n\ninterface CatalogManagedTable {\n    /**\n     * Commits the given set of `actions` to the given commit `version`.\n     *\n     * @param version The version we want to commit.\n     * @param actions Actions that need to be committed.\n     *\n     * @return CommitResponse which has details around the new committed delta file.\n     */\n    def commit(\n        version: Long,\n        actions: Iterator[String]): CommitResponse\n\n    /**\n     * Retrieves a (possibly empty) suffix of ratified commits in the range [startVersion,\n     * endVersion] for this table.\n     * \n     * Some of these ratified commits may already have been published. Some of them may be staged,\n     * in which case the staged commit file path is returned; others may be inline, in which case\n     * the inline commit content is returned.\n     * \n     * The returned commits are sorted in ascending version number and are contiguous.\n     *\n     * If neither start nor end version is specified, the catalog will return all available ratified\n     * commits (possibly empty, if all commits have been published).\n     *\n     * In all cases, the response also includes the table's latest ratified commit version.\n     *\n     * @return GetCommitsResponse which contains an ordered list of ratified commits\n     *         stored by the catalog, as well as table's latest commit version.\n     */\n    def getRatifiedCommits(\n        startVersion: Option[Long],\n        endVersion: Option[Long]): GetCommitsResponse\n}\n```\n\nNote that the above is only one example of a possible Catalog Client API. It is also _NOT_ a catalog\nAPI (no table discovery, ACL, create/drop, etc). The Delta protocol is agnostic to API details, and\nthe API surface Delta clients define should only cover the specific catalog capabilities that Delta\nclient needs to correctly read and write catalog-managed tables.\n\n# Iceberg Compatibility V1\n\nThis table feature (`icebergCompatV1`) ensures that Delta tables can be converted to Apache Iceberg™ format, though this table feature does not implement or specify that conversion.\n\nTo support this feature:\n- Since this table feature depends on Column Mapping, the table must be on Reader Version = 2, or it must be on Reader Version >= 3 and the feature `columnMapping` must exist in the `protocol`'s `readerFeatures`.\n- The table must be on Writer Version 7.\n- The feature `icebergCompatV1` must exist in the table `protocol`'s `writerFeatures`.\n\nThis table feature is enabled when the table property `delta.enableIcebergCompatV1` is set to `true`.\n\n## Writer Requirements for IcebergCompatV1\n\nWhen supported and active, writers must:\n- Require that Column Mapping be enabled and set to either `name` or `id` mode\n- Require that Deletion Vectors are not supported (and, consequently, not active, either). i.e., the `deletionVectors` table feature is not present in the table `protocol`.\n- Require that partition column values are materialized into any Parquet data file that is present in the table, placed *after* the data columns in the parquet schema\n- Require that all `AddFile`s committed to the table have the `numRecords` statistic populated in their `stats` field\n- Block adding `Map`/`Array`/`Void` types to the table schema (and, thus, block writing them, too)\n- Block replacing partitioned tables with a differently-named partition spec\n  - e.g. replacing a table partitioned by `part_a INT` with partition spec `part_b INT` must be blocked\n  - e.g. replacing a table partitioned by `part_a INT` with partition spec `part_a LONG` is allowed\n- When the [Type Widening](#type-widening) table feature is supported, require that all type changes applied on the table are supported by [Iceberg V2](https://iceberg.apache.org/spec/#schema-evolution), based on the [Type Change Metadata](#type-change-metadata) recorded in the table schema.\n\n# Iceberg Compatibility V2\n\nThis table feature (`icebergCompatV2`) ensures that Delta tables can be converted to Apache Iceberg™ format, though this table feature does not implement or specify that conversion.\n\nTo support this feature:\n- Since this table feature depends on Column Mapping, the table must be on Reader Version = 2, or it must be on Reader Version >= 3 and the feature `columnMapping` must exist in the `protocol`'s `readerFeatures`.\n- The table must be on Writer Version 7.\n- The feature `icebergCompatV2` must exist in the table protocol's `writerFeatures`.\n\nThis table feature is enabled when the table property `delta.enableIcebergCompatV2` is set to `true`. \n\n## Writer Requirements for IcebergCompatV2\n\nWhen this feature is supported and enabled, writers must:\n- Require that Column Mapping be enabled and set to either `name` or `id` mode\n- Require that the nested `element` field of ArrayTypes and the nested `key` and `value` fields of MapTypes be assigned 32 bit integer identifiers. These identifiers must be unique and different from those used in [Column Mapping](#column-mapping), and must be stored in the metadata of their nearest ancestor [StructField](#struct-field) of the Delta table schema. Identifiers belonging to the same `StructField` must be organized as a `Map[String, Long]` and stored in metadata with key `parquet.field.nested.ids`. The keys of the map are \"element\", \"key\", or \"value\", prefixed by the name of the nearest ancestor StructField, separated by dots. The values are the identifiers. The keys for fields in nested arrays or nested maps are prefixed by their parents' key, separated by dots. An [example](#example-of-storing-identifiers-for-nested-fields-in-arraytype-and-maptype) is provided below to demonstrate how the identifiers are stored. These identifiers must be also written to the `field_id` field of the `SchemaElement` struct in the [Parquet Thrift specification](https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift) when writing parquet files.\n- Require that IcebergCompatV1 is not active, which means either the `icebergCompatV1` table feature is not present in the table protocol or the table property `delta.enableIcebergCompatV1` is not set to `true`\n- Require that Deletion Vectors are not active, which means either the `deletionVectors` table feature is not present in the table protocol or the table property `delta.enableDeletionVectors` is not set to `true`\n- Require that partition column values be materialized when writing Parquet data files\n- Require that all new `AddFile`s committed to the table have the `numRecords` statistic populated in their `stats` field\n- Require writing timestamp columns as int64\n- Require that the table schema contains only data types in the following allow-list: [`byte`, `short`, `integer`, `long`, `float`, `double`, `decimal`, `string`, `binary`, `boolean`, `timestamp`, `timestampNTZ`, `date`, `array`, `map`, `struct`].\n- Block replacing partitioned tables with a differently-named partition spec\n  - e.g. replacing a table partitioned by `part_a INT` with partition spec `part_b INT` must be blocked\n  - e.g. replacing a table partitioned by `part_a INT` with partition spec `part_a LONG` is allowed\n- When the [Type Widening](#type-widening) table feature is supported, require that all type changes applied on the table are supported by [Iceberg V2](https://iceberg.apache.org/spec/#schema-evolution), based on the [Type Change Metadata](#type-change-metadata) recorded in the table schema.\n\n### Example of storing identifiers for nested fields in ArrayType and MapType\nThe following is an example of storing the identifiers for nested fields in `ArrayType` and `MapType`, of a table with the following schema,\n```\n|-- col1: array[array[int]] \n|-- col2: map[int, array[int]]    \n|-- col3: map[int, struct]\n                     |-- subcol1: array[int]\n```\nThe identifiers for the nested fields are stored in the metadata as follows: \n```json\n[\n  {\n    \"name\": \"col1\",\n    \"type\": {\n      \"type\": \"array\",\n      \"elementType\": {\n        \"type\": \"array\",\n        \"elementType\": \"int\"\n      }\n    },\n    \"metadata\": {\n      \"parquet.field.nested.ids\": {\n        \"col1.element\": 100,\n        \"col1.element.element\": 101\n      }\n    }\n  },\n  {\n    \"name\": \"col2\",\n    \"type\": {\n      \"type\": \"map\",\n      \"keyType\": \"int\",\n      \"valueType\": {\n        \"type\": \"array\",\n        \"elementType\": \"int\"\n      }\n    },\n    \"metadata\": {\n      \"parquet.field.nested.ids\": {\n        \"col2.key\": 102,\n        \"col2.value\": 103,\n        \"col2.value.element\": 104\n      }\n    }\n  },\n  {\n    \"name\": \"col3\",\n    \"type\": {\n      \"type\": \"map\",\n      \"keyType\": \"int\",\n      \"valueType\": {\n        \"type\": \"struct\",\n        \"fields\": [\n          {\n            \"name\": \"subcol1\",\n            \"type\": {\n              \"type\": \"array\",\n              \"elementType\": \"int\"\n            },\n            \"metadata\": {\n              \"parquet.field.nested.ids\": {\n                \"subcol1.element\": 107\n              }\n            }\n          }\n        ]\n      }\n    },\n    \"metadata\": {\n      \"parquet.field.nested.ids\": {\n        \"col3.key\": 105,\n        \"col3.value\": 106\n      }\n    }\n  }\n]\n```\n# Timestamp without timezone (TimestampNtz)\nThis feature introduces a new data type to support timestamps without timezone information. For example: `1970-01-01 00:00:00`, or `1970-01-01 00:00:00.123456`.\nThe serialization method is described in Sections [Partition Value Serialization](#partition-value-serialization) and [Schema Serialization Format](#schema-serialization-format).\n\nTo support this feature:\n- To have a column of TimestampNtz type in a table, the table must have Reader Version 3 and Writer Version 7. A feature name `timestampNtz` must exist in the table's `readerFeatures` and `writerFeatures`.\n\n\n# V2 Checkpoint Table Feature\nTo support this feature:\n- To add [V2 Checkpoints](#v2-spec) support to a table, the table must have Reader Version 3 and Writer Version 7. A feature name `v2Checkpoint` must exist in the table's `readerFeatures` and `writerFeatures`.\n\nWhen supported:\n- A table could use [uuid-named](#uuid-named-checkpoint) [V2 spec Checkpoints](#v2-spec) which must have [checkpoint metadata](#checkpoint-metadata) and may have [sidecar files](#sidecar-files) OR\n- A table could use [classic](#classic-checkpoint) checkpoints which can follow [V1](#v1-spec) or [V2](#v2-spec) spec.\n- A table must not use [multi-part checkpoints](#multi-part-checkpoint)\n\n# Row Tracking\n\nRow Tracking is a feature that allows the tracking of rows across multiple versions of a Delta table.\nIt enables this by exposing two metadata columns: Row IDs, which uniquely identify a row across multiple versions of a table,\nand Row Commit Versions, which make it possible to check whether two rows with the same ID in two different versions of the table represent the same version of the row.\n\nRow Tracking is defined to be **supported** or **enabled** on a table as follows:\n- When the feature `rowTracking` exists in the table `protocol`'s `writerFeatures`, then we say that Row Tracking is **supported**.\n  In this situation, writers must assign Row IDs and Commit Versions as long as `delta.rowTrackingSuspended` table property is absent or set to false. However, they cannot yet be relied upon to be present in the table.\n  When Row Tracking is supported but not yet enabled writers cannot preserve Row IDs and Commit Versions.\n- When additionally the table property `delta.enableRowTracking` is set to `true`, then we say that Row Tracking is **enabled**.\n  In this situation, Row IDs and Row Commit versions can be relied upon to be present in the table for all rows.\n  When Row Tracking is enabled writers are expected to preserve Row IDs and Commit Versions.\n- When the table property `delta.rowTrackingSuspended` is set to true, writers should suspend the assignment of Row IDs and Commit Versions.\n  Table property `delta.rowTrackingSuspended` should not be enabled together with table property `delta.enableRowTracking`.\n\nEnablement:\n- The table must be on Writer Version 7.\n- The feature `rowTracking` must exist in the table `protocol`'s `writerFeatures`. The feature `domainMetadata` is required in the table `protocol`'s `writerFeatures`.\n- The table property `delta.enableRowTracking` must be set to `true`.\n- The table property `delta.rowTrackingSuspended` should be absent or set to `false`.\n\n## Row IDs\n\nDelta provides Row IDs. Row IDs are integers that are used to uniquely identify rows within a table.\nEvery row has two Row IDs:\n\n- A **fresh** or unstable **Row ID**.\n  This ID uniquely identifies the row within one version of the table.\n  The fresh ID of a row may change every time the table is updated, even for rows that are not modified. E.g. when a row is copied unchanged during an update operation, it will get a new fresh ID. Fresh IDs can be used to identify rows within one version of the table, e.g. for identifying matching rows in self joins.\n- A **stable Row ID**.\n  This ID uniquely identifies the row across versions of the table and across updates.\n  When a row is inserted, it is assigned a new stable Row ID that is equal to the fresh Row ID.\n  When a row is updated or copied, the stable Row ID for this row is preserved.\n  When a row is restored (i.e. the table is restored to an earlier version), its stable Row ID is restored as well.\n\nThe fresh and stable Row IDs are not required to be equal.\n\nRow IDs are stored in two ways:\n\n- **Default generated Row IDs** use the `baseRowId` field stored in `add` and `remove` actions to generate fresh Row IDs.\n  The default generated Row IDs for data files are calculated by adding the `baseRowId` of the file in which a row is contained to the (physical) position (index) of the row within the file.\n  Default generated Row IDs require little storage overhead but are reassigned every time a row is updated or moved to a different file (for instance when a row is contained in a file that is compacted by OPTIMIZE).\n\n- **Materialized Row IDs** are stored in a column in the data files.\n  This column is hidden from readers and writers, i.e. it is not part of the `schemaString` in the table's `metaData`.\n  Instead, the name of this column can be found in the value for the `delta.rowTracking.materializedRowIdColumnName` key in the `configuration` of the table's `metaData` action.\n  This column may contain `null` values meaning that the corresponding row has no materialized Row ID. This column may be omitted if all its values are `null` in the file.\n  Materialized Row IDs provide a mechanism for writers to preserve stable Row IDs for rows that are updated or copied.\n\nThe fresh Row ID of a row is equal to the default generated Row ID. The stable Row ID of a row is equal to the materialized Row ID of the row when that column is present and the value is not NULL, otherwise it is equal to the default generated Row ID.\n\nWhen Row Tracking is enabled:\n- Default generated Row IDs must be assigned to all existing rows.\n  This means in particular that all files that are part of the table version that sets the table property `delta.enableRowTracking` to `true` must have `baseRowId` set.\n  A backfill operation may be required to commit `add` and `remove` actions with the `baseRowId` field set for all data files before the table property `delta.enableRowTracking` can be set to `true`.\n\n## Row Commit Versions\n\nRow Commit Versions provide versioning of rows.\n\n- **Fresh** or unstable **Row Commit Versions** can be used to identify the first commit version in which the `add` action containing the row was committed.\n  The fresh Commit Version of a row may change every time the table is updated, even for rows that are not modified. E.g. when a row is copied unchanged during an update operation, it will get a new fresh Commit Version.\n- **Stable Row Commit Versions** identify the last commit version in which the row (with the same ID) was either inserted or updated.\n  When a row is inserted or updated, it is assigned the commit version number of the log entry containing the `add` entry with the new row.\n  When a row is copied, the stable Row Commit Version for this row is preserved.\n  When a row is restored (i.e. the table is restored to an earlier version), its stable Row Commit Version is restored as well.\n\nThe fresh and stable Row Commit Versions are not required to be equal.\n\nCommit Versions are stored in two ways:\n\n- **Default generated Row Commit Versions** use the `defaultRowCommitVersion` field in `add` and `remove` actions.\n  Default generated Row Commit Versions require little storage overhead but are reassigned every time a row is updated or moved to a different file (for instance when a row is contained in a file that is compacted by OPTIMIZE).\n\n- **Materialized Row Commit Versions** are stored in a column in the data files.\n  This column is hidden from readers and writers, i.e. it is not part of the `schemaString` in the table's `metaData`.\n  Instead, the name of this column can be found in the value for the `delta.rowTracking.materializedRowCommitVersionColumnName` key in the `configuration` of the table's `metaData` action.\n  This column may contain `null` values meaning that the corresponding row has no materialized Row Commit Version. This column may be omitted if all its values are `null` in the file.\n  Materialized Row Commit Versions provide a mechanism for writers to preserve Row Commit Versions for rows that are copied.\n\nThe fresh Row Commit Version of a row is equal to the default generated Row Commit version.\nThe stable Row Commit Version of a row is equal to the materialized Row Commit Version of the row when that column is present and the value is not NULL, otherwise it is equal to the default generated Commit Version.\n\n## Reader Requirements for Row Tracking\n\nWhen Row Tracking is enabled (when the table property `delta.enableRowTracking` is set to `true`), then:\n- When Row IDs are requested, readers must reconstruct stable Row IDs as follows:\n  1. Readers must use the materialized Row ID if the column determined by `delta.rowTracking.materializedRowIdColumnName` is present in the data file and the column contains a non `null` value for a row.\n  2. Otherwise, readers must use the default generated Row ID of the `add` or `remove` action containing the row in all other cases.\n     I.e. readers must add the index of the row in the file to the `baseRowId` of the `add` or `remove` action for the file containing the row.\n- When Row Commit Versions are requested, readers must reconstruct them as follows:\n  1. Readers must use the materialized Row Commit Versions if the column determined by `delta.rowTracking.materializedRowCommitVersionColumnName` is present in the data file and the column contains a non `null` value for a row.\n  2. Otherwise, Readers must use the default generated Row Commit Versions of the `add` or `remove` action containing the row in all other cases.\n     I.e. readers must use the `defaultRowCommitVersion` of the `add` or `remove` action for the file containing the row.\n- Readers cannot read Row IDs and Row Commit Versions while reading change data files from `cdc` actions.\n\n## Writer Requirements for Row Tracking\n\nWhen Row Tracking is supported (when the `writerFeatures` field of a table's `protocol` action contains `rowTracking`) and Row Tracking is not suspended (when `delta.rowTrackingSuspended` table property is absent or set to false), then:\n- Writers must assign unique fresh Row IDs to all rows that they commit.\n  - Writers must set the `baseRowId` field in all `add` actions that they commit so that all default generated Row IDs are unique in the table version.\n    Writers must never commit duplicate Row IDs in the table in any version.\n  - Writers must set the `baseRowId` field in recommitted and checkpointed `add` actions and `remove` actions to the `baseRowId` value (if present) of the last committed `add` action with the same `path`.\n  - Writers must track the high water mark, i.e. the highest fresh row id assigned.\n    - The high water mark must be stored in a `domainMetadata` action with `delta.rowTracking` as the `domain`\n      and a `configuration` containing a single key-value pair with `rowIdHighWaterMark` as the key and the highest assigned fresh row id as the value.\n    - Writers must include a `domainMetadata` for `delta.rowTracking` whenever they assign new fresh Row IDs that are higher than `rowIdHighWaterMark` value of the current `domainMetadata` for `delta.rowTracking`.\n      The `rowIdHighWaterMark` value in the `configuration` of this `domainMetadata` action must always be equal to or greater than the highest fresh Row ID committed so far.\n      Writers can either commit this `domainMetadata` in the same commit, or they can reserve the fresh Row IDs in an earlier commit.\n    - Writers must set the `baseRowId` field to a value that is higher than the row id high water mark.\n- Writer must assign fresh Row Commit Versions to all rows that they commit.\n  - Writers must set the `defaultRowCommitVersion` field in new `add` actions to the version number of the log enty containing the `add` action.\n  - Writers must set the `defaultRowCommitVersion` field in recommitted and checkpointed `add` actions and `remove` actions to the `defaultRowCommitVersion` of the last committed `add` action with the same `path`.\n\nOn the other hand, when Row Tracking is supported but suspended (table property `delta.rowTrackingSuspended` is set to `true`), writers should not assign the `baseRowId` or the `defaultRowCommitVersion`.\n\nWriters can enable Row Tracking by setting `delta.enableRowTracking` to `true` in the `configuration` of the table's `metaData`.\nThis is only allowed if the following requirements are satisfied:\n- The feature `rowTracking` has been added to the `writerFeatures` field of a table's `protocol` action either in the same version of the table or in an earlier version of the table.\n- The column name for the materialized Row IDs and Row Commit Versions have been assigned and added to the `configuration` in the table's `metaData` action using the keys `delta.rowTracking.materializedRowIdColumnName` and `delta.rowTracking.materializedRowCommitVersionColumnName` respectively.\n  - The assigned column names must be unique. They must not be equal to the name of any other column in the table's schema.\n    The assigned column names must remain unique in all future versions of the table.\n    If [Column Mapping](#column-mapping) is enabled, then the assigned column name must be distinct from the physical column names of the table.\n- The `baseRowId` and `defaultRowCommitVersion` fields are set for all active `add` actions in the version of the table in which `delta.enableRowTracking` is set to `true`.\n- If the `baseRowId` and `defaultRowCommitVersion` fields are not set in some active `add` action in the table, then writers must first commit new `add` actions that set these fields to replace the `add` actions that do not have these fields set.\n  This can be done in the commit that sets `delta.enableRowTracking` to `true` or in an earlier commit.\n  The assigned `baseRowId` and `defaultRowCommitVersion` values must satisfy the same requirements as when assigning fresh Row IDs and fresh Row Commit Versions respectively.\nFurthermore, writers should also verify table property `delta.rowTrackingSuspended` is absent or set to false before enabling Row Tracking.\n\nWhen Row Tracking is enabled (when the table property `delta.enableRowTracking` is set to `true`), then:\n- Writers must assign stable Row IDs to all rows.\n  - Stable Row IDs must be unique within a version of the table and must not be equal to the fresh Row IDs of other rows in the same version of the table.\n  - Writers should preserve the stable Row IDs of rows that are updated or copied using materialized Row IDs.\n    - The preserved stable Row ID (i.e. a stable Row ID that is not equal to the fresh Row ID of the same physical row) should be equal to the stable Row ID of the same logical row before it was updated or copied.\n    - Materialized Row IDs must be written to the column determined by `delta.rowTracking.materializedRowIdColumnName` in the `configuration` of the table's `metaData` action.\n      The value in this column must be set to `NULL` for stable Row IDs that are not preserved.\n- Writers must assign stable Row Commit Versions to all rows.\n  - Writers should preserve the stable Row Commit Versions of rows that are copied (but not updated) using materialized Row Commit Versions.\n    - The preserved stable Row Commit Version (i.e. a stable Row Commit Version that is not equal to the fresh Row Commit Version of the same physical row) should be equal to the stable Commit Version of the same logical row before it was copied.\n    - Materialized Row Commit Versions must be written to the column determined by `delta.rowTracking.materializedRowCommitVersionColumnName` in the `configuration` of the table's `metaData` action.\n      The value in this column must be set to `NULL` for stable Row Commit Versions that are not preserved (i.e. that are equal to the fresh Row Commit Version).\n- Writers should set `delta.rowTracking.preserved` in the `tags` of the `commitInfo` action to `true` whenever all the stable Row IDs of rows that are updated or copied and all the stable Row Commit Versions of rows that are copied were preserved.\n  In particular, writers should set `delta.rowTracking.preserved` in the `tags` of the `commitInfo` action to `true` if no rows are updated or copied.\n  Writers should set that flag to false otherwise.\n\n# VACUUM Protocol Check\n\nThe `vacuumProtocolCheck` ReaderWriter feature ensures consistent application of reader and writer protocol checks during `VACUUM` operations, addressing potential protocol discrepancies and mitigating the risk of data corruption due to skipped writer checks.\n\nEnablement:\n- The table must be on Writer Version 7 and Reader Version 3.\n- The feature `vacuumProtocolCheck` must exist in the table `protocol`'s `writerFeatures` and `readerFeatures`.\n\n## Writer Requirements for Vacuum Protocol Check\n\nThis feature affects only the VACUUM operations; standard commits remain unaffected.\n\nBefore performing a VACUUM operation, writers must ensure that they check the table's write protocol. This is most easily implemented by adding an unconditional write protocol check for all tables, which removes the need to examine individual table properties.\n\nWriters that do not implement VACUUM do not need to change anything and can safely write to tables that enable the feature.\n\n## Reader Requirements for Vacuum Protocol Check\n\nFor tables with Vacuum Protocol Check enabled, readers don’t need to understand or change anything new; they just need to acknowledge the feature exists.\n\nMaking this feature a ReaderWriter feature (rather than solely a Writer feature) ensures that:\n- Older vacuum implementations, which only performed the Reader protocol check and lacked the Writer protocol check, will begin to fail if the table has `vacuumProtocolCheck` enabled. This change allows future writer features to have greater flexibility and safety in managing files within the table directory, eliminating the risk of older Vacuum implementations (that lack the Writer protocol check) accidentally deleting relevant files.\n\n# Clustered Table\n\nThe Clustered Table feature facilitates the physical clustering of rows that share similar values on a predefined set of clustering columns.\nThis enhances query performance when selective filters are applied to these clustering columns through data skipping.\nClustering columns can be specified during the initial creation of a table, or they can be added later, provided that the table doesn't have partition columns.\n\nA table is defined as a clustered table through the following criteria:\n- When the feature `clustering` exists in the table `protocol`'s `writerFeatures`, then we say that the table is a clustered table.\n  The feature `domainMetadata` is required in the table `protocol`'s `writerFeatures`.\n\nEnablement:\n- The table must be on Writer Version 7.\n- The feature `clustering` must exist in the table `protocol`'s `writerFeatures`, either during its creation or at a later stage, provided the table does not have partition columns.\n\n## Writer Requirements for Clustered Table\n\nWhen the Clustered Table is supported (when the `writerFeatures` field of a table's `protocol` action contains `clustering`), then:\n- Writers must track clustering column names in a `domainMetadata` action with `delta.clustering` as the `domain` and a `configuration` containing all clustering column names.\n  If [Column Mapping](#column-mapping) is enabled, the physical column names should be used.\n- Writers must write out [per-file statistics](#per-file-statistics) and per-column statistics for clustering columns in `add` action. \n  If a new column is included in the clustering columns list, it is required for all table files to have statistics for these added columns.\n- When a clustering implementation clusters files, writers must set the name of the clustering implementation in the `clusteringProvider` field when adding `add` actions for clustered files.\n  - By default, a clustering implementation must only recluster files that have the field `clusteringProvider` set to the name of the same clustering implementation, or to the names of other clustering implementations that are superseded by the current clustering implementation. In addition, a clustering implementation may cluster any files with an unset `clusteringProvider` field (i.e., unclustered files).\n  - Writer is not required to cluster a specific file at any specific moment.\n  - A clustering implementation is free to add additional information such as adding a new user-controlled metadata domain to keep track of its metadata.\n- Writers must not define clustered and partitioned table at the same time.\n\nThe following is an example for the `domainMetadata` action definition of a table that leverages column mapping.\n```json\n{\n  \"domainMetadata\": {\n    \"domain\": \"delta.clustering\",\n    \"configuration\": \"{\\\"clusteringColumns\\\":[\\\"col-daadafd7-7c20-4697-98f8-bff70199b1f9\\\", \\\"col-5abe0e80-cf57-47ac-9ffc-a861a3d1077e\\\"]}\",\n    \"removed\": false\n  }\n}\n```\nThe example above converts `configuration` field into JSON format, including escaping characters. Here's how it looks in plain JSON for better understanding.\n```json\n{\n  \"clusteringColumns\": [\n    \"col-daadafd7-7c20-4697-98f8-bff70199b1f9\",\n    \"col-5abe0e80-cf57-47ac-9ffc-a861a3d1077e\"\n  ]\n}\n```\n\n\n# Variant Data Type\n\nThis feature enables support for the `variant` data type, which stores semi-structured data.\nThe schema serialization method is described in [Schema Serialization Format](#schema-serialization-format).\n\nTo support this feature:\n- The table must be on Reader Version 3 and Writer Version 7\n- The feature `variantType` must exist in the table `protocol`'s `readerFeatures` and `writerFeatures`.\n\n## Example JSON-Encoded Delta Table Schema with Variant types\n\n```\n{\n  \"type\" : \"struct\",\n  \"fields\" : [ {\n    \"name\" : \"raw_data\",\n    \"type\" : \"variant\",\n    \"nullable\" : true,\n    \"metadata\" : { }\n  }, {\n    \"name\" : \"variant_array\",\n    \"type\" : {\n      \"type\" : \"array\",\n      \"elementType\" : {\n        \"type\" : \"variant\"\n      },\n      \"containsNull\" : false\n    },\n    \"nullable\" : false,\n    \"metadata\" : { }\n  } ]\n}\n```\n\n## Variant data in Parquet\n\nThe Variant data type is represented as two binary encoded values, according to the [Spark Variant binary encoding specification](https://github.com/apache/spark/blob/master/common/variant/README.md).\nThe two binary values are named `value` and `metadata`.\n\nWhen writing Variant data to parquet files, the Variant data is written as a single Parquet struct, with the following fields:\n\nStruct field name | Parquet primitive type | Description\n-|-|-\nvalue | binary | The binary-encoded Variant value, as described in [Variant binary encoding](https://github.com/apache/spark/blob/master/common/variant/README.md)\nmetadata | binary | The binary-encoded Variant metadata, as described in [Variant binary encoding](https://github.com/apache/spark/blob/master/common/variant/README.md)\n\nThe parquet struct must include the two struct fields `value` and `metadata`.\nSupported writers must write the two binary fields, and supported readers must read the two binary fields.\n\n[Variant shredding](https://github.com/apache/parquet-format/blob/master/VariantShredding.md) will be introduced in a separate `variantShredding` table feature. will be introduced later, as a separate `variantShredding` table feature.\n\n## Writer Requirements for Variant Data Type\n\nWhen Variant type is supported (`writerFeatures` field of a table's `protocol` action contains `variantType`), writers:\n- must write a column of type `variant` to parquet as a struct containing the fields `value` and `metadata` and storing values that conform to the [Variant binary encoding specification](https://github.com/apache/spark/blob/master/common/variant/README.md)\n- must not write a parquet struct field named `typed_value` to avoid confusion with the field required by [Variant shredding](https://github.com/apache/parquet-format/blob/master/VariantShredding.md) with the same name.\n\n## Reader Requirements for Variant Data Type\n\nWhen Variant type is supported (`readerFeatures` field of a table's `protocol` action contains `variantType`), readers:\n- must recognize and tolerate a `variant` data type in a Delta schema\n- must use the correct physical schema (struct-of-binary, with fields `value` and `metadata`) when reading a Variant data type from file\n- must make the column available to the engine:\n    - [Recommended] Expose and interpret the struct-of-binary as a single Variant field in accordance with the [Spark Variant binary encoding specification](https://github.com/apache/spark/blob/master/common/variant/README.md).\n    - [Alternate] Expose the raw physical struct-of-binary, e.g. if the engine does not support Variant.\n    - [Alternate] Convert the struct-of-binary to a string, and expose the string representation, e.g. if the engine does not support Variant.\n\n## Compatibility with other Delta Features\n\nFeature | Support for Variant Data Type\n-|-\nPartition Columns | **Supported:** A Variant column is allowed to be a non-partitioned column of a partitioned table. <br/> **Unsupported:** Variant is not a comparable data type, so it cannot be included in a partition column.\nClustered Tables | **Supported:** A Variant column is allowed to be a non-clustering column of a clustered table. <br/> **Unsupported:** Variant is not a comparable data type, so it cannot be included in a clustering column.\nDelta Column Statistics | **Supported:** A Variant column supports the `nullCount` statistic. <br/> **Unsupported:** Variant is not a comparable data type, so a Variant column does not support the `minValues` and `maxValues` statistics.\nGenerated Columns | **Supported:** A Variant column is allowed to be used as a source in a generated column expression, as long as the Variant type is not the result type of the generated column expression. <br/> **Unsupported:** The Variant data type is not allowed to be the result type of a generated column expression.\nDelta CHECK Constraints | **Supported:** A Variant column is allowed to be used for a CHECK constraint expression.\nDefault Column Values | **Supported:** A Variant column is allowed to have a default column value.\nChange Data Feed | **Supported:** A table using the Variant data type is allowed to enable the Delta Change Data Feed.\n\n# In-Commit Timestamps\n\nThe In-Commit Timestamps writer feature strongly associates a monotonically increasing timestamp with each commit by storing it in the commit's metadata.\n\nEnablement:\n- The table must be on Writer Version 7.\n- The feature `inCommitTimestamp` must exist in the table `protocol`'s `writerFeatures`.\n- The table property `delta.enableInCommitTimestamps` must be set to `true`.\n\n## Writer Requirements for In-Commit Timestamps\n\nWhen In-Commit Timestamps is enabled, then:\n1. Writers must write the `commitInfo` (see [Commit Provenance Information](#commit-provenance-information)) action in the commit.\n2. The `commitInfo` action must be the first action in the commit.\n3. The `commitInfo` action must include a field named `inCommitTimestamp`, of type `long` (see [Primitive Types](#primitive-types)), which represents the time (in milliseconds since the Unix epoch) when the commit is considered to have succeeded. It is the larger of two values:\n   - The time, in milliseconds since the Unix epoch, at which the writer attempted the commit\n   - One millisecond later than the previous commit's `inCommitTimestamp`\n4. If the table has commits from a period when this feature was not enabled, provenance information around when this feature was enabled must be tracked in table properties:\n   - The property `delta.inCommitTimestampEnablementVersion` must be used to track the version of the table when this feature was enabled.\n   - The property `delta.inCommitTimestampEnablementTimestamp` must be the same as the `inCommitTimestamp` of the commit when this feature was enabled.\n5. The `inCommitTimestamp` of the commit that enables this feature must be greater than the file modification time of the immediately preceding commit.\n\n## Recommendations for Readers of Tables with In-Commit Timestamps\n\nFor tables with In-Commit timestamps enabled, readers should use the `inCommitTimestamp` as the commit timestamp for operations like time travel and [`DESCRIBE HISTORY`](https://docs.delta.io/latest/delta-utility.html#retrieve-delta-table-history).\nIf a table has commits from a period before In-Commit timestamps were enabled, the table properties `delta.inCommitTimestampEnablementVersion` and `delta.inCommitTimestampEnablementTimestamp` would be set and can be used to identify commits that don't have `inCommitTimestamp`.\nTo correctly determine the commit timestamp for these tables, readers can use the following rules:\n1. For commits with version >= `delta.inCommitTimestampEnablementVersion`, readers should use the `inCommitTimestamp` field of the `commitInfo` action.\n2. For commits with version < `delta.inCommitTimestampEnablementVersion`, readers should use the file modification timestamp.\n\nFurthermore, when attempting timestamp-based time travel where table state must be fetched as of `timestamp X`, readers should use the following rules:\n1. If `timestamp X` >= `delta.inCommitTimestampEnablementTimestamp`, only table versions >= `delta.inCommitTimestampEnablementVersion` should be considered for the query.\n2. Otherwise, only table versions less than `delta.inCommitTimestampEnablementVersion` should be considered for the query.\n\n# Type Widening\n\nThe Type Widening feature enables changing the type of a column or field in an existing Delta table to a wider type.\n\nThe supported type changes are:\n- Integer widening:\n  - `Byte` -> `Short` -> `Int` -> `Long`\n- Floating-point widening:\n  - `Float` -> `Double`\n  - `Byte`, `Short` or `Int` -> `Double`\n- Date widening:\n  - `Date` -> `Timestamp without timezone`\n- Decimal widening - `p` and `s` denote the decimal precision and scale respectively.\n  - `Decimal(p, s)` -> `Decimal(p + k1, s + k2)` where `k1 >= k2 >= 0`.\n  - `Byte`, `Short` or `Int` -> `Decimal(10 + k1, k2)` where `k1 >= k2 >= 0`.\n  - `Long` -> `Decimal(20 + k1, k2)` where `k1 >= k2 >= 0`.\n\nTo support this feature:\n- The table must be on Reader version 3 and Writer Version 7.\n- The feature `typeWidening` must exist in the table `protocol`'s `readerFeatures` and `writerFeatures`, either during its creation or at a later stage.\n\nWhen supported:\n - A table may have a metadata property `delta.enableTypeWidening` in the Delta schema set to `true`. Writers must reject widening type changes when this property isn't set to `true`.\n - The `metadata` for a column or field in the table schema may contain the key `delta.typeChanges` storing a history of type changes for that column or field.\n\n### Type Change Metadata\n\nType changes applied to a table are recorded in the table schema and stored in the `metadata` of their nearest ancestor [StructField](#struct-field) using the key `delta.typeChanges`.\nThe value for the key `delta.typeChanges` must be a JSON list of objects, where each object contains the following fields:\nField Name | optional/required | Description\n-|-|-\n`fromType`| required | The type of the column or field before the type change.\n`toType`| required | The type of the column or field after the type change.\n`fieldPath`| optional | When updating the type of a map key/value or array element only: the path from the struct field holding the metadata to the map key/value or array element that was updated.\n\nThe `fieldPath` value is \"key\", \"value\" and \"element\"  when updating resp. the type of a map key, map value and array element.\nThe `fieldPath` value for nested maps and nested arrays are prefixed by their parents' path, separated by dots.\n\nThe following is an example for the definition of a column that went through two type changes:\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : \"long\",\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.typeChanges\": [\n        {\n          \"fromType\": \"short\",\n          \"toType\": \"integer\"\n        },\n        {\n          \"fromType\": \"integer\",\n          \"toType\": \"long\"\n        }\n      ]\n    }\n  }\n```\n\nThe following is an example for the definition of a column after changing the type of a map key:\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : {\n      \"type\": \"map\",\n      \"keyType\": \"double\",\n      \"valueType\": \"integer\",\n      \"valueContainsNull\": true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.typeChanges\": [\n        {\n          \"fromType\": \"float\",\n          \"toType\": \"double\",\n          \"fieldPath\": \"key\"\n        }\n      ]\n    }\n  }\n```\n\nThe following is an example for the definition of a column after changing the type of a map value nested in an array:\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : {\n      \"type\": \"array\",\n      \"elementType\": {\n        \"type\": \"map\",\n        \"keyType\": \"string\",\n        \"valueType\": \"decimal(10, 4)\",\n        \"valueContainsNull\": true\n      },\n      \"containsNull\": true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.typeChanges\": [\n        {\n          \"fromType\": \"decimal(6, 2)\",\n          \"toType\": \"decimal(10, 4)\",\n          \"fieldPath\": \"element.value\"\n        }\n      ]\n    }\n  }\n```\n\n## Writer Requirements for Type Widening\n\nWhen Type Widening is supported (when the `writerFeatures` field of a table's `protocol` action contains `typeWidening`), then:\n- Writers must reject applying any unsupported type change.\n- Writers must reject applying type changes not supported by [Iceberg V2](https://iceberg.apache.org/spec/#schema-evolution)\n  when either the [Iceberg Compatibility V1](#iceberg-compatibility-v1) or [Iceberg Compatibility V2](#iceberg-compatibility-v2) table feature is supported:\n  - `Byte`, `Short` or `Int` -> `Double`\n  - `Date`  -> `Timestamp without timezone`\n  - Decimal scale increase\n  - `Byte`, `Short`, `Int` or `Long` -> `Decimal`\n- Writers must record type change information in the `metadata` of the nearest ancestor [StructField](#struct-field). See [Type Change Metadata](#type-change-metadata).\n- Writers must preserve the `delta.typeChanges` field in the metadata fields in the schema when the table schema is updated.\n- Writers may remove the `delta.typeChanges` metadata in the table schema if all data files use the same field types as the table schema.\n\nWhen Type Widening is enabled (when the table property `delta.enableTypeWidening` is set to `true`), then:\n- Writers should allow updating the table schema to apply a supported type change to a column, struct field, map key/value or array element.\n\nWhen removing the Type Widening table feature from the table, in the version that removes `typeWidening` from the `writerFeatures` and `readerFeatures` fields of the table's `protocol` action:\n- Writers must ensure no `delta.typeChanges` metadata key is present in the table schema. This may require rewriting existing data files to ensure that all data files use the same field types as the table schema in order to fulfill the requirement to remove type widening metadata.\n- Writers must ensure that the table property `delta.enableTypeWidening` is not set.\n\n## Reader Requirements for Type Widening\nWhen Type Widening is supported (when the `readerFeatures` field of a table's `protocol` action contains `typeWidening`), then:\n- Readers must allow reading data files written before the table underwent any supported type change, and must convert such values to the current, wider type.\n- Readers must validate that they support all type changes in the `delta.typeChanges` field in the table schema for the table version they are reading and fail when finding any unsupported type change.\n\n# Requirements for Writers\nThis section documents additional requirements that writers must follow in order to preserve some of the higher level guarantees that Delta provides.\n\n## Creation of New Log Entries\n - Writers MUST never overwrite an existing log entry. When ever possible they should use atomic primitives of the underlying filesystem to ensure concurrent writers do not overwrite each other's entries.\n\n## Consistency Between Table Metadata and Data Files\n - Any column that exists in a data file present in the table MUST also be present in the metadata of the table.\n - Values for all partition columns present in the schema MUST be present for all files in the table.\n - Columns present in the schema of the table MAY be missing from data files. Readers SHOULD fill these missing columns in with `null`.\n\n## Delta Log Entries\n- A single log entry MUST NOT include more than one action that reconciles with each other.\n  - Add / Remove actions with the same `(path, DV)` tuple.\n  - More than one Metadata action\n  - More than one protocol action\n  - More than one SetTransaction with the same `appId`\n\n## Checkpoints\nEach row in the checkpoint corresponds to a single action. The checkpoint **must** contain all information regarding the following actions:\n * The [protocol version](#Protocol-Evolution)\n * The [metadata](#Change-Metadata) of the table\n * Files that have been [added](#Add-File-and-Remove-File) and not yet removed\n * Files that were recently [removed](#Add-File-and-Remove-File) and have not yet expired\n * [Transaction identifiers](#Transaction-Identifiers)\n * [Domain Metadata](#Domain-Metadata)\n * [Checkpoint Metadata](#checkpoint-metadata) - Requires [V2 checkpoints](#v2-spec)\n * [Sidecar File](#sidecar-files) - Requires [V2 checkpoints](#v2-spec)\n\nAll of these actions are stored as their individual columns in parquet as struct fields. Any missing column should be treated as null.\n\nCheckpoints must not preserve [commit provenance information](#commit-provenance-information) nor [change data](#add-cdc-file) actions.\n\nWithin the checkpoint, the `add` struct may or may not contain the following columns based on the configuration of the table:\n - partitionValues_parsed: In this struct, the column names correspond to the partition columns and the values are stored in their corresponding data type. This is a required field when the table is partitioned and the table property `delta.checkpoint.writeStatsAsStruct` is set to `true`. If the table is not partitioned, this column can be omitted. For example, for partition columns `year`, `month` and `event` with data types `int`, `int` and `string` respectively, the schema for this field will look like:\n\n ```\n|-- add: struct\n|    |-- partitionValues_parsed: struct\n|    |    |-- year: int\n|    |    |-- month: int\n|    |    |-- event: string\n ```\n\n - stats: Column level statistics can be stored as a JSON string in the checkpoint. This field needs to be written when statistics are available and the table property: `delta.checkpoint.writeStatsAsJson` is set to `true` (which is the default). When this property is set to `false`, this field should be omitted from the checkpoint.\n - stats_parsed: The stats can be stored in their [original format](#Per-file-Statistics). This field needs to be written when statistics are available and the table property: `delta.checkpoint.writeStatsAsStruct` is set to `true`. When this property is set to `false` (which is the default), this field should be omitted from the checkpoint.\n\nWithin the checkpoint, the `remove` struct does not contain the `stats` and `tags` fields because the `remove` actions stored in checkpoints act only as tombstones for VACUUM operations, and VACUUM tombstones do not require `stats` or `tags`. These fields are only stored in Delta JSON commit files.\n\nRefer to the [appendix](#checkpoint-schema) for an example on the schema of the checkpoint.\n\nDelta supports two checkpoint specs and three kind of checkpoint naming schemes.\n\n### Checkpoint Specs\nDelta supports following two checkpoint specs:\n\n#### V2 Spec\nThis checkpoint spec allows putting [add and remove file](#Add-File-and-Remove-File) in the\n[sidecar files](#sidecar-files). This spec can be used only when [v2 checkpoint table feature](#v2-checkpoint-table-feature) is enabled.\nCheckpoints following V2 spec have the following structure:\n- Each v2 spec checkpoint includes exactly one [Checkpoint Metadata](#checkpoint-metadata) action.\n- Remaining rows in the V2 spec checkpoint refer to the other actions mentioned [here](#checkpoints-1)\n- All the non-file actions i.e. all actions except [add and remove file](#Add-File-and-Remove-File)\nmust be part of the v2 spec checkpoint itself.\n- A writer could choose to include the [add and remove file](#Add-File-and-Remove-File) action in the\nV2 spec Checkpoint or they could write the [add and remove file](#Add-File-and-Remove-File) actions in\nseparate [sidecar files](#sidecar-files). These sidecar files will then be referenced in the V2 spec checkpoint.\nAll sidecar files reside in the `_delta_log/_sidecars` directory.\n- A V2 spec Checkpoint could reference zero or more [sidecar file actions](#sidecar-file-information).\n\nNote: A V2 spec Checkpoint can either have all the [add and remove file](#Add-File-and-Remove-File) actions\nembedded inside itself or all of them should be in [sidecar files](#sidecar-files). Having partial\nadd and remove file actions in V2 Checkpoint and partial entries in sidecar files is not allowed.\n\nAfter producing a V2 spec checkpoint, a writer can choose to embed some or all of the V2 spec checkpoint in\nthe `_last_checkpoint` file, so that readers don't have to read the V2 Checkpoint.\n\nE.g. showing the content of V2 spec checkpoint:\n```\n{\"checkpointMetadata\":{\"version\":364475,\"tags\":{}}}\n{\"metaData\":{...}}\n{\"protocol\":{...}}\n{\"txn\":{\"appId\":\"3ba13872-2d47-4e17-86a0-21afd2a22395\",\"version\":364475}}\n{\"txn\":{\"appId\":\"3ae45b72-24e1-865a-a211-34987ae02f2a\",\"version\":4389}}\n{\"sidecar\":{\"path\":\"3a0d65cd-4056-49b8-937b-95f9e3ee90e5.parquet\",\"sizeInBytes\":2341330,\"modificationTime\":1512909768000,\"tags\":{}}\n{\"sidecar\":{\"path\":\"016ae953-37a9-438e-8683-9a9a4a79a395.parquet\",\"sizeInBytes\":8468120,\"modificationTime\":1512909848000,\"tags\":{}}\n```\n\nAnother example of a v2 spec checkpoint without sidecars:\n```\n{\"checkpointMetadata\":{\"version\":364475,\"tags\":{}}}\n{\"metaData\":{...}}\n{\"protocol\":{...}}\n{\"txn\":{\"appId\":\"3ba13872-2d47-4e17-86a0-21afd2a22395\",\"version\":364475}}\n{\"add\":{\"path\":\"date=2017-12-10/part-000...c000.gz.parquet\",...}\n{\"add\":{\"path\":\"date=2017-12-09/part-000...c000.gz.parquet\",...}\n{\"remove\":{\"path\":\"date=2017-12-08/part-000...c000.gz.parquet\",...}\n```\n\n#### V1 Spec\n\nThe V1 Spec does not support [sidecar files](#sidecar-files) and [checkpoint metadata](#checkpoint-metadata).\nThese are flat checkpoints which contains all actions mentioned [here](#checkpoints-1).\n\n### Checkpoint Naming Scheme\nDelta supports three checkpoint naming schemes: UUID-named, classic, and multi-part.\n\n#### UUID-named checkpoint\nThis naming scheme represents a [V2 spec checkpoint](#v2-spec) with following file name: `n.checkpoint.u.{json/parquet}`,\nwhere `u` is a UUID and `n` is the snapshot version that this checkpoint represents.\nThe UUID-named checkpoints may be in JSON or parquet format. Since these are following [V2 spec](#v2-spec), they must\nhave a [checkpoint metadata](#checkpoint-metadata) action and may reference zero or more checkpoint [sidecar files](#sidecar-files).\n\nExample-1: Json UUID-named checkpoint with sidecars\n\n```\n00000000000000000010.checkpoint.80a083e8-7026-4e79-81be-64bd76c43a11.json\n_sidecars/016ae953-37a9-438e-8683-9a9a4a79a395.parquet\n_sidecars/3a0d65cd-4056-49b8-937b-95f9e3ee90e5.parquet\n_sidecars/7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.parquet\n```\n\nExample-2: Parquet UUID-named checkpoint with sidecars\n\n```\n00000000000000000020.checkpoint.80a083e8-7026-4e79-81be-64bd76c43a11.parquet\n_sidecars/016ae953-37a9-438e-8683-9a9a4a79a395.parquet\n_sidecars/3a0d65cd-4056-49b8-937b-95f9e3ee90e5.parquet\n```\n\nExample-3: Json UUID-named checkpoint without sidecars\n\n```\n00000000000000000112.checkpoint.80a083e8-7026-4e79-81be-64bd76c43a11.json\n```\n\n#### Classic checkpoint\n\nA classic checkpoint for version `n` uses the file name `n.checkpoint.parquet`. For example:\n\n```\n00000000000000000010.checkpoint.parquet\n```\n\nIf two checkpoint writers race to create the same classic checkpoint, the latest writer wins.\nHowever, this should not matter because both checkpoints should contain the same information and a\nreader could safely use either one.\n\nA classic checkpoint could:\n1. Either follow [V1 spec](#v1-spec) or\n2. Could follow [V2 spec](#v2-spec). This is possible only when\n[V2 Checkpoint table feature](#v2-checkpoint-table-feature) is enabled. In this case it must include\n[checkpoint metadata](#checkpoint-metadata) and may or may not have [sidecar files](#sidecar-file-information).\n\n#### Multi-part checkpoint\nMulti-part checkpoint uses parquet format.\nThis checkpoint type is [deprecated](#problems-with-multi-part-checkpoints) and writers should avoid using it.\n\nA multi-part checkpoint for version `n` consists of `p` \"part\" files (`p > 1`), where part `o` of `p` is named `n.checkpoint.o.p.parquet`. For example:\n\n```\n00000000000000000010.checkpoint.0000000001.0000000003.parquet\n00000000000000000010.checkpoint.0000000002.0000000003.parquet\n00000000000000000010.checkpoint.0000000003.0000000003.parquet\n```\n\nFor [safety reasons](#problems-with-multi-part-checkpoints), multi-part checkpoints MUST be clustered by\nspark-style hash partitioning. If the table supports [Deletion Vectors](#deletion-vectors), the partitioning\nkey is the logical file identifier `(path, dvId)`; otherwise the key is just `path` (not `(path, NULL)`). This\nensures deterministic content in each part file in case of multiple attempts to write the files -- even when\nolder and newer Delta clients race.\n\n##### Problems with multi-part checkpoints\n\nBecause they cannot be written atomically, multi-part checkpoints have several weaknesses:\n\n1. A writer cannot validate the content of the just-written checkpoint before readers could start using it.\n\n2. Two writers who race to produce the same checkpoint (same version, same number of parts) can overwrite each other, producing an arbitrary mix of checkpoint part files. If an overwrite changes the content of a file in any way, the resulting checkpoint may not produce an accurate snapshot.\n\n3. Not amenable to performance and scalability optimizations. For example, there is no way to store skipping stats for checkpoint parts, nor to reuse checkpoint part files across multiple checkpoints.\n\n4. Multi-part checkpoints also bloat the _delta_log dir and slow down LIST operations.\n\nThe [UUID-named](#uuid-named-checkpoint) checkpoint (which follows [V2 spec](#v2-spec)) solves all\nof these problems and should be preferred over multi-part checkpoints. For this reason, Multi-part\ncheckpoints are forbidden when [V2 Checkpoints table feature](#v2-checkpoint-table-feature) is enabled.\n\n### Handling Backward compatibility while moving to UUID-named v2 Checkpoints\n\nA UUID-named v2 Checkpoint should only be created by clients if the [v2 checkpoint table feature](#v2-checkpoint-table-feature) is enabled.\nWhen UUID-named v2 checkpoints are enabled, Writers should occasionally create a v2 [Classic Checkpoint](#classic-checkpoint)\nto maintain compatibility with older clients which do not support [v2 checkpoint table feature](#v2-checkpoint-table-feature) and\nso do not recognize UUID-named checkpoints. These classic checkpoints have the same content as the UUID-named v2 checkpoint, but older\nclients will recognize the classic file name, allowing them to extract [Protocol](#protocol-evolution) and fail gracefully with an\ninvalid protocol version error on v2-checkpoint-enabled tables. Writers should create classic checkpoints often enough to allow older\nclients to discover them and fail gracefully.\n\n### Allowed combinations for `checkpoint spec` <-> `checkpoint file naming`\n\nCheckpoint Spec | [UUID-named](#uuid-named-checkpoint) | [classic](#classic-checkpoint) | [multi-part](#multi-part-checkpoint)\n-|-|-|-\n[V1](#v1-spec) | Invalid | Valid | Valid\n[V2](#v2-spec) | Valid | Valid | Invalid\n\n### Metadata Cleanup\n\nThe _delta_log directory grows over time as more and more commits and checkpoints are accumulated.\nImplementations are recommended to delete expired commits and checkpoints in order to reduce the directory size.\nThe following steps could be used to do cleanup of the DeltaLog directory:\n1. Identify a threshold (in days) uptil which we want to preserve the deltaLog. Let's refer to\nmidnight UTC of that day as `cutOffTimestamp`. The newest commit not newer than the `cutOffTimestamp` is\nthe `cutoffCommit`, because a commit exactly at midnight is an acceptable cutoff. We want to retain everything including and after the `cutoffCommit`.\n2. Identify the newest checkpoint that is not newer than the `cutOffCommit`. A checkpoint at the `cutOffCommit` is ideal, but an older one will do. Let's call it `cutOffCheckpoint`.\nWe need to preserve the `cutOffCheckpoint` (both the checkpoint file and the JSON commit file at that version) and all published commits after it. The JSON commit file at the `cutOffCheckpoint` version must be preserved because checkpoints do not preserve [commit provenance information](#commit-provenance-information) (e.g., `commitInfo` actions), which may be required by table features such as [In-Commit Timestamps](#in-commit-timestamps). All published commits after `cutOffCheckpoint` must be preserved to enable time travel for commits between `cutOffCheckpoint` and the next available checkpoint.\n    - If no `cutOffCheckpoint` can be found, do not proceed with metadata cleanup as there is\n      nothing to cleanup.\n3. Delete all [delta log entries](#delta-log-entries), [checkpoint files](#checkpoints), and\n   [version checksum files](#version-checksum-file) before the `cutOffCheckpoint` checkpoint. Also delete all the [log compaction files](#log-compaction-files)\n   having startVersion <= `cutOffCheckpoint`'s version.\n    - Also delete all the [staged commit files](#staged-commit) having version <=\n      `cutOffCheckpoint`'s version from the `_delta_log/_staged_commits` directory.\n4. Now read all the available [checkpoints](#checkpoints-1) in the _delta_log directory and identify\nthe corresponding [sidecar files](#sidecar-files). These sidecar files need to be protected.\n5. List all the files in `_delta_log/_sidecars` directory, preserve files that are less than a day\nold (as of midnight UTC), to not break in-progress checkpoints. Also preserve the referenced sidecar files\nidentified in Step-4 above. Delete everything else.\n\n## Data Files\n - Data files MUST be uniquely named and MUST NOT be overwritten. The reference implementation uses a GUID in the name to ensure this property.\n\n## Append-only Tables\nTo support this feature:\n - The table must be on a Writer Version starting from 2 up to 7.\n - If the table is on Writer Version 7, the feature `appendOnly` must exist in the table `protocol`'s `writerFeatures`.\n\nWhen supported, and if the table has a property `delta.appendOnly` set to `true`:\n - New log entries MUST NOT change or remove data from the table.\n - New log entries may rearrange data (i.e. `add` and `remove` actions where `dataChange=false`).\n\nTo remove the append-only restriction, the table property `delta.appendOnly` must be set to `false`, or it must be removed.\n\n## Column Invariants\nTo support this feature\n - If the table is on a Writer Version starting from 2 up to 6, Column Invariants are always enabled.\n - If the table is on Writer Version 7, the feature `invariants` must exist in the table `protocol`'s `writerFeatures`.\n\nWhen supported:\n - The `metadata` for a column in the table schema MAY contain the key `delta.invariants`.\n - The value of `delta.invariants` SHOULD be parsed as a JSON string containing a boolean SQL expression at the key `expression.expression` (that is, `{\"expression\": {\"expression\": \"<SQL STRING>\"}}`).\n - Writers MUST abort any transaction that adds a row to the table, where an invariant evaluates to `false` or `null`.\n\nFor example, given the schema string (pretty printed for readability. The entire schema string in the log should be a single JSON line):\n\n```json\n{\n    \"type\": \"struct\",\n    \"fields\": [\n        {\n            \"name\": \"x\",\n            \"type\": \"integer\",\n            \"nullable\": true,\n            \"metadata\": {\n                \"delta.invariants\": \"{\\\"expression\\\": { \\\"expression\\\": \\\"x > 3\\\"} }\"\n            }\n        }\n    ]\n}\n```\n\nWriters should reject any transaction that contains data where the expression `x > 3` returns `false` or `null`.\n\n## CHECK Constraints\n\nTo support this feature:\n- If the table is on a Writer Version starting from 3 up to 6, CHECK Constraints are always supported.\n- If the table is on Writer Version 7, a feature name `checkConstraints` must exist in the table `protocol`'s `writerFeatures`.\n\nCHECK constraints are stored in the map of the `configuration` field in [Metadata](#change-metadata). Each CHECK constraint has a name and is stored as a key value pair. The key format is `delta.constraints.{name}`, and the value is a SQL expression string whose return type must be `Boolean`. Columns referred by the SQL expression must exist in the table schema.\n\nRows in a table must satisfy CHECK constraints. In other words, evaluating the SQL expressions of CHECK constraints must return `true` for each row in a table.\n\nFor example, a key value pair (`delta.constraints.birthDateCheck`, `birthDate > '1900-01-01'`) means there is a CHECK constraint called `birthDateCheck` in the table and the value of the `birthDate` column in each row must be greater than `1900-01-01`.\n\nHence, a writer must follow the rules below:\n- CHECK Constraints may not be added to a table unless the above \"to support this feature\" rules are satisfied. When adding a CHECK Constraint to a table for the first time, writers are allowed to submit a `protocol` change in the same commit to add support of this feature.\n- When adding a CHECK constraint to a table, a writer must validate the existing data in the table and ensure every row satisfies the new CHECK constraint before committing the change. Otherwise, the write operation must fail and the table must stay unchanged.\n- When writing to a table that contains CHECK constraints, every new row being written to the table must satisfy CHECK constraints in the table. Otherwise, the write operation must fail and the table must stay unchanged.\n\n## Generated Columns\n\nTo support this feature:\n - If the table is on a Writer Version starting from 4 up to 6, Generated Columns are always supported.\n - If the table is on Writer Version 7, a feature name `generatedColumns` must exist in the table `protocol`'s `writerFeatures`.\n\nWhen supported:\n - The `metadata` for a column in the table schema MAY contain the key `delta.generationExpression`.\n - The value of `delta.generationExpression` SHOULD be parsed as a SQL expression.\n - Writers MUST enforce that any data writing to the table satisfy the condition `(<value> <=> <generation expression>) IS TRUE`. `<=>` is the NULL-safe equal operator which performs an equality comparison like the `=` operator but returns `TRUE` rather than NULL if both operands are `NULL`\n\n## Default Columns\n\nDelta supports defining default expressions for columns on Delta tables. Delta will generate default values for columns when users do not explicitly provide values for them when writing to such tables, or when the user explicitly specifies the `DEFAULT` SQL keyword for any such column.\n\nSemantics for write and read operations:\n- Note that this metadata only applies for write operations, not read operations.\n- Table write operations (such as SQL INSERT, UPDATE, and MERGE commands) will use the default values. For example, this SQL command will use default values: `INSERT INTO t VALUES (42, DEFAULT);`\n- Table operations that add new columns (such as SQL ALTER TABLE ... ADD COLUMN commands) MUST not specify a default value for any column in the same command that the column is created. For example, this SQL command is not supported in Delta Lake: `ALTER TABLE t ADD COLUMN c INT DEFAULT 42;`\n- Note that it is acceptable to assign or update default values for columns that were already created in previous commands, however. For example, this SQL command is valid: `ALTER TABLE t ALTER COLUMN c SET DEFAULT 42;`\n\nEnablement:\n- The table must be on Writer Version 7, and a feature name `allowColumnDefaults` must exist in the table `protocol`'s `writerFeatures`.\n\nWhen enabled:\n- The `metadata` for the column in the table schema MAY contain the key `CURRENT_DEFAULT`.\n- The value of `CURRENT_DEFAULT` SHOULD be parsed as a SQL expression.\n- Writers MUST enforce that before writing any rows to the table, for each such requested row that lacks any explicit value (including NULL) for columns with default values, the writing system will assign the result of evaluating the default value expression for each such column as the value for that column in the row. By the same token, if the engine specified the explicit `DEFAULT` SQL keyword for any column, the expression result must be substituted in the same way.\n- All columns of `variant` type must default to null.\n\n## Identity Columns\n\nDelta supports defining Identity columns on Delta tables. Delta will generate unique values for Identity columns when users do not explicitly provide values for them when writing to such tables. To support Identity Columns:\n - The table must be on Writer Version 6, or\n - The table must be on Writer Version 7, and a feature name `identityColumns` must exist in the table `protocol`'s `writerFeatures`.\n\nWhen supported, the `metadata` for a column in the table schema MAY contain the following keys for Identity Column properties:\n- `delta.identity.start`: Starting value for the Identity column. This is a long type value. It should not be changed after table creation.\n- `delta.identity.step`: Increment to the next Identity value. This is a long type value. It cannot be set to 0. It should not be changed after table creation.\n- `delta.identity.highWaterMark`: The highest value generated for the Identity column. This is a long type value. When `delta.identity.step` is positive (negative), this should be the largest (smallest) value in the column.\n- `delta.identity.allowExplicitInsert`: True if this column allows explicitly inserted values. This is a boolean type value. It should not be changed after table creation.\n\nWhen `delta.identity.allowExplicitInsert` is true, writers should meet the following requirements:\n- Users should be allowed to provide their own values for Identity columns.\n\nWhen `delta.identity.allowExplicitInsert` is false, writers should meet the following requirements:\n- Users should not be allowed to provide their own values for Identity columns.\n- Delta should generate values that satisfy the following requirements\n  - The new value does not already exist in the column.\n  - The new value should satisfy `value = start + k * step` where k is a non-negative integer.\n  - The new value should be higher than `delta.identity.highWaterMark`. When `delta.identity.step` is positive (negative), the new value should be the greater (smaller) than `delta.identity.highWaterMark`.\n- Overflow when calculating generated Identity values should be detected and such writes should not be allowed.\n- `delta.identity.highWaterMark` should be updated to the new highest value when the write operation commits.\n\n## Writer Version Requirements\n\nThe requirements of the writers according to the protocol versions are summarized in the table below. Each row inherits the requirements from the preceding row.\n\n<br> | Requirements\n-|-\nWriter Version 2 | - Respect [Append-only Tables](#append-only-tables)<br>- Respect [Column Invariants](#column-invariants)\nWriter Version 3 | - Enforce `delta.checkpoint.writeStatsAsJson`<br>- Enforce `delta.checkpoint.writeStatsAsStruct`<br>- Respect [`CHECK` constraints](#check-constraints)\nWriter Version 4 | - Respect [Change Data Feed](#add-cdc-file)<br>- Respect [Generated Columns](#generated-columns)\nWriter Version 5 | Respect [Column Mapping](#column-mapping)\nWriter Version 6 | Respect [Identity Columns](#identity-columns)\nWriter Version 7 | Respect [Table Features](#table-features) for writers\n\n# Requirements for Readers\n\nThis section documents additional requirements that readers must respect in order to produce correct scans of a Delta table.\n\n## Reader Version Requirements\n\nThe requirements of the readers according to the protocol versions are summarized in the table below. Each row inherits the requirements from the preceding row.\n\n<br> | Requirements\n-|-\nReader Version 2 | Respect [Column Mapping](#column-mapping)\nReader Version 3 | Respect [Table Features](#table-features) for readers<br> - Writer Version must be 7\n\n# Appendix\n\n## Valid Feature Names in Table Features\n\nFeature | Name | Readers or Writers?\n-|-|-\n[Append-only Tables](#append-only-tables) | `appendOnly` | Writers only\n[Column Invariants](#column-invariants) | `invariants` | Writers only\n[`CHECK` constraints](#check-constraints) | `checkConstraints` | Writers only\n[Generated Columns](#generated-columns) | `generatedColumns` | Writers only\n[Default Columns](#default-columns) | `allowColumnDefaults` | Writers only\n[Change Data Feed](#add-cdc-file) | `changeDataFeed` | Writers only\n[Column Mapping](#column-mapping) | `columnMapping` | Readers and writers\n[Identity Columns](#identity-columns) | `identityColumns` | Writers only\n[Deletion Vectors](#deletion-vectors) | `deletionVectors` | Readers and writers\n[Row Tracking](#row-tracking) | `rowTracking` | Writers only\n[Timestamp without Timezone](#timestamp-without-timezone-timestampNtz) | `timestampNtz` | Readers and writers\n[Domain Metadata](#domain-metadata) | `domainMetadata` | Writers only\n[V2 Checkpoint](#v2-checkpoint-table-feature) | `v2Checkpoint` | Readers and writers\n[Catalog-managed Tables](#catalog-managed-tables) | `catalogManaged` | Readers and writers\n[Iceberg Compatibility V1](#iceberg-compatibility-v1) | `icebergCompatV1` | Writers only\n[Iceberg Compatibility V2](#iceberg-compatibility-v2) | `icebergCompatV2` | Writers only\n[Clustered Table](#clustered-table) | `clustering` | Writers only\n[VACUUM Protocol Check](#vacuum-protocol-check) | `vacuumProtocolCheck` | Readers and Writers\n[In-Commit Timestamps](#in-commit-timestamps) | `inCommitTimestamp` | Writers only\n\n## Deletion Vector Format\n\nDeletion Vectors are basically sets of row indexes, that is 64-bit integers that describe the position (index) of a row in a parquet file starting from zero. We store these sets in a compressed format. The fundamental building block for this is the open source [RoaringBitmap](https://roaringbitmap.org/) library. RoaringBitmap is a flexible format for storing 32-bit integers that automatically switches between three different encodings at the granularity of a 16-bit block (64K values):\n\n- Simple integer array, when the number of values in the block is small.\n- Bitmap-compressed, when the number of values in the block is large and scattered.\n- Run-length encoded, when the number of values in the block is large, but clustered.\n\nThe serialization format is [standardized](https://github.com/RoaringBitmap/RoaringFormatSpec), and both [Java](https://github.com/lemire/RoaringBitmap/) and [C/C++](https://github.com/RoaringBitmap/CRoaring) implementations are available (among others).\n\nThe above description only applies to 32-bit bitmaps, but Deletion Vectors use 64-bit integers. In order to extend coverage from 32 to 64 bits, RoaringBitmaps defines a \"portable\" serialization format in the [RoaringBitmaps Specification](https://github.com/RoaringBitmap/RoaringFormatSpec#extention-for-64-bit-implementations). This format essentially splits the space into an outer part with the most significant 32-bit \"keys\" indexing the least significant 32-bit RoaringBitmaps in ascending sequence. The spec calls these least signficant 32-bit RoaringBitmaps \"buckets\".\n\nBytes | Name | Description\n-|-|-\n0 – 7 | numBuckets | The number of distinct 32-bit buckets in this bitmap.\n`repeat for each bucket b` | | For each bucket in ascending order of keys.\n`<start of b>` – `<start of b> + 3` | key | The most significant 32-bit of all the values in this bucket.\n`<start of b> + 4` – `<end of b>` | bucketData | A serialized 32-bit RoaringBitmap with all the least signficant 32-bit entries in this bucket.\n\nThe 32-bit serialization format then consists of a header that describes all the (least signficant) 16-bit containers, their types (s. above), and their key (most significant 16-bits).\nThis is followed by the data for each individual container in a container-specific format.\n\nReference Implementations of the Roaring format:\n\n- [32-bit Java RoaringBitmap](https://github.com/RoaringBitmap/RoaringBitmap/blob/c7993318d7224cd3cc0244dcc99c8bbc5ddb0c87/RoaringBitmap/src/main/java/org/roaringbitmap/RoaringArray.java#L905-L949)\n- [64-bit Java RoaringNavigableBitmap](https://github.com/RoaringBitmap/RoaringBitmap/blob/c7993318d7224cd3cc0244dcc99c8bbc5ddb0c87/RoaringBitmap/src/main/java/org/roaringbitmap/longlong/Roaring64NavigableMap.java#L1253-L1260)\n\nDelta uses the format described above as a black box, but with two additions:\n\n1. We prepend a \"magic number\", which can be used to make sure we are reading the correct format and also retains the ability to evolve the format in the future.\n2. We require that every \"key\" (s. above) in the bitmap has a 0 as its most significant bit. This ensures that in Java, where values are read signed, we never read negative keys. \n\nThe concrete serialization format is as follows (all numerical values are written in little endian byte order):\n\nBytes | Name | Description\n-|-|-\n0 — 3 | magicNumber | 1681511377; Indicates that the following bytes are serialized in this exact format. Future alternative—but related—formats must have a different magic number, for example by incrementing this one.\n4 — end | bitmap | A serialized 64-bit bitmap in the portable standard format as defined in the [RoaringBitmaps Specification](https://github.com/RoaringBitmap/RoaringFormatSpec#extention-for-64-bit-implementations). This can be treated as a black box by any Delta implementation that has a native, standard-compliant RoaringBitmap library available to pass these bytes to.\n\n### Deletion Vector File Storage Format\n\nDeletion Vectors can be stored in files in cloud storage or inline in the Delta log.\nThe format for storing DVs in file storage is one (or more) DV, using the 64-bit RoaringBitmaps described in the previous section, per file, together with a checksum for each DV.\nThe concrete format is as follows, with all numerical values written in big endian byte order:\n\nBytes | Name | Description\n-|-|-\n0 | version | The format version of this file: `1` for the format described here.\n`repeat for each DV i` | | For each DV\n`<start of i>` — `<start of i> + 3` | dataSize | Size of this DV’s data (without the checksum)\n`<start of i> + 4` — `<start of i> + 4 + dataSize - 1` | bitmapData | One 64-bit RoaringBitmap serialised as described above.\n`<start of i> + 4 + dataSize` — `<start of i> + 4 + dataSize + 3` | checksum | CRC-32 checksum of `bitmapData`\n\n## Per-file Statistics\n`add` and `remove` actions can optionally contain statistics about the data in the file being added or removed from the table.\nThese statistics can be used for eliminating files based on query predicates or as inputs to query optimization.\n\nGlobal statistics record information about the entire file.\nThe following global statistic is currently supported:\n\nName | Description\n-|-\nnumRecords | The number of records in this data file.\ntightBounds | Whether per-column statistics are currently **tight** or **wide** (see below).\n\nFor any logical file where `deletionVector` is not `null`, the `numRecords` statistic *must* be present and accurate. That is, it must equal the number of records in the data file, not the valid records in the logical file.\nIn the presence of [Deletion Vectors](#Deletion-Vectors) the statistics may be somewhat outdated, i.e. not reflecting deleted rows yet. The flag `stats.tightBounds` indicates whether we have **tight bounds** (i.e. the min/maxValue exists[^1] in the valid state of the file) or **wide bounds** (i.e. the minValue is <= all valid values in the file, and the maxValue >= all valid values in the file). These upper/lower bounds are sufficient information for data skipping. Note, `stats.tightBounds` should be treated as `true` when it is not explicitly present in the statistics.\n\nPer-column statistics record information for each column in the file and they are encoded, mirroring the schema of the actual data.\nFor example, given the following data schema:\n```\n|-- a: struct\n|    |-- b: struct\n|    |    |-- c: long\n```\n\nStatistics could be stored with the following schema:\n```\n|-- stats: struct\n|    |-- numRecords: long\n|    |-- tightBounds: boolean\n|    |-- minValues: struct\n|    |    |-- a: struct\n|    |    |    |-- b: struct\n|    |    |    |    |-- c: long\n|    |-- maxValues: struct\n|    |    |-- a: struct\n|    |    |    |-- b: struct\n|    |    |    |    |-- c: long\n```\n\nThe following per-column statistics are currently supported:\n\nName | Description (`stats.tightBounds=true`) | Description (`stats.tightBounds=false`)\n-|-|-\nnullCount | The number of `null` values for this column | <p>If the `nullCount` for a column equals the physical number of records (`stats.numRecords`) then **all** valid rows for this column must have `null` values (the reverse is not necessarily true).</p><p>If the `nullCount` for a column equals 0 then **all** valid rows are non-`null` in this column (the reverse is not necessarily true).</p><p>If the `nullCount` for a column is any value other than these two special cases, the value carries no information and should be treated as if absent.</p>\nminValues | A value that is equal to the smallest valid value[^1] present in the file for this column. If all valid rows are null, this carries no information. | A value that is less than or equal to all valid values[^1] present in this file for this column. If all valid rows are null, this carries no information.\nmaxValues | A value that is equal to the largest valid value[^1] present in the file for this column. If all valid rows are null, this carries no information. | A value that is greater than or equal to all valid values[^1] present in this file for this column. If all valid rows are null, this carries no information.\n\n[^1]: String columns are cut off at a fixed prefix length. Timestamp columns are truncated down to milliseconds.\n\n## Partition Value Serialization\n\nPartition values are stored as strings, using the following formats. An empty string for any type translates to a `null` partition value.\n\nType | Serialization Format\n-|-\nstring | No translation required\nnumeric types | The string representation of the number\ndate | Encoded as `{year}-{month}-{day}`. For example, `1970-01-01`\ntimestamp | Encoded as `{year}-{month}-{day} {hour}:{minute}:{second}` or `{year}-{month}-{day} {hour}:{minute}:{second}.{microsecond}`. For example: `1970-01-01 00:00:00`, or `1970-01-01 00:00:00.123456`. Timestamps may also be encoded as an ISO8601 formatted timestamp adjusted to UTC timestamp such as `1970-01-01T00:00:00.123456Z`\ntimestamp without timezone | Encoded as `{year}-{month}-{day} {hour}:{minute}:{second}` or `{year}-{month}-{day} {hour}:{minute}:{second}.{microsecond}` For example: `1970-01-01 00:00:00`, or `1970-01-01 00:00:00.123456` To use this type, a table must support a feature `timestampNtz`. See section [Timestamp without timezone (TimestampNtz)](#timestamp-without-timezone-timestampNtz) for more information.\nboolean | Encoded as the string \"true\" or \"false\"\nbinary | Encoded as a string of escaped binary values. For example, `\"\\u0001\\u0002\\u0003\"`\n\nNote: A timestamp value in a partition value may be stored in one of the following ways:\n1. Without a timezone, where the timestamp should be interpreted using the time zone of the system which wrote to the table.\n2. Adjusted to UTC and stored in ISO8601 format.\n\nIt is highly recommended that modern writers adjust the timestamp to UTC and store the timestamp in ISO8601 format as outlined in 2.\n\n## Schema Serialization Format\n\nDelta uses a subset of Spark SQL's JSON Schema representation to record the schema of a table in the transaction log.\nAll column names must be unique regardless of casing.\nA reference implementation can be found in [the catalyst package of the Apache Spark repository](https://github.com/apache/spark/tree/master/sql/catalyst/src/main/scala/org/apache/spark/sql/types).\n\n### Primitive Types\n\nType Name | Description\n-|-\nstring| UTF-8 encoded string of characters\nlong| 8-byte signed integer. Range: -9223372036854775808 to 9223372036854775807\ninteger|4-byte signed integer. Range: -2147483648 to 2147483647\nshort| 2-byte signed integer numbers. Range: -32768 to 32767\nbyte| 1-byte signed integer number. Range: -128 to 127\nfloat| 4-byte single-precision floating-point numbers\ndouble| 8-byte double-precision floating-point numbers\ndecimal| signed decimal number with fixed precision (maximum number of digits) and scale (number of digits on right side of dot). The precision and scale can be up to 38.\nboolean| `true` or `false`\nbinary| A sequence of binary data.\ndate| A calendar date, represented as a year-month-day triple without a timezone.\ntimestamp| Microsecond precision timestamp elapsed since the Unix epoch, 1970-01-01 00:00:00 UTC. When this is stored in a parquet file, its `isAdjustedToUTC` must be set to `true`.\ntimestamp without time zone | Microsecond precision timestamp in a local timezone elapsed since the Unix epoch, 1970-01-01 00:00:00. It doesn't have the timezone information, and a value of this type can map to multiple physical time instants. It should always be displayed in the same way, regardless of the local time zone in effect. When this is stored in a parquet file, its `isAdjustedToUTC` must be set to `false`. To use this type, a table must support a feature `timestampNtz`. See section [Timestamp without timezone (TimestampNtz)](#timestamp-without-timezone-timestampNtz) for more information.\n\nSee Parquet [timestamp type](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#timestamp) for more details about timestamp and `isAdjustedToUTC`.\n\nNote: Existing tables may have `void` data type columns. Behavior is undefined for `void` data type columns but it is recommended to drop any `void` data type columns on reads (as is implemented by the Spark connector).\n\n### Struct Type\n\nA struct is used to represent both the top-level schema of the table as well as struct columns that contain nested columns. A struct is encoded as a JSON object with the following fields:\n\nField Name | Description\n-|-\ntype | Always the string \"struct\"\nfields | An array of fields\n\n### Struct Field\n\nA struct field represents a top-level or nested column.\n\nField Name | Description\n-|-\nname| Name of this (possibly nested) column\ntype| String containing the name of a primitive type, a struct definition, an array definition or a map definition\nnullable| Boolean denoting whether this field can be null\nmetadata| A JSON map containing information about this column. Keys prefixed with `Delta` are reserved for the implementation. See [Column Metadata](#column-metadata) for more information on column level metadata that clients must handle when writing to a table.\n\n### Array Type\n\nAn array stores a variable length collection of items of some type.\n\nField Name | Description\n-|-\ntype| Always the string \"array\"\nelementType| The type of element stored in this array, represented as a string containing the name of a primitive type, a struct definition, an array definition or a map definition\ncontainsNull| Boolean denoting whether this array can contain one or more null values\n\n### Map Type\n\nA map stores an arbitrary length collection of key-value pairs with a single `keyType` and a single `valueType`.\n\nField Name | Description\n-|-\ntype| Always the string \"map\".\nkeyType| The type of element used for the key of this map, represented as a string containing the name of a primitive type, a struct definition, an array definition or a map definition\nvalueType| The type of element used for the key of this map, represented as a string containing the name of a primitive type, a struct definition, an array definition or a map definition\n\n### Variant Type\n\nVariant data uses the Delta type name `variant` for Delta schema serialization.\n\nField Name | Description\n-|-\ntype | Always the string \"variant\"\n\n### Column Metadata\nA column metadata stores various information about the column.\nFor example, this MAY contain some keys like [`delta.columnMapping`](#column-mapping) or [`delta.generationExpression`](#generated-columns) or [`CURRENT_DEFAULT`](#default-columns).  \nField Name | Description\n-|-\ndelta.columnMapping.*| These keys are used to store information about the mapping between the logical column name to  the physical name. See [Column Mapping](#column-mapping) for details.\ndelta.identity.*| These keys are for defining identity columns. See [Identity Columns](#identity-columns) for details.\ndelta.invariants| JSON string contains SQL expression information. See [Column Invariants](#column-invariants) for details.\ndelta.generationExpression| SQL expression string. See [Generated Columns](#generated-columns) for details.\ndelta.typeChanges| JSON string containing information about previous type changes applied to this column. See [Type Change Metadata](#type-change-metadata) for details.\n\n### Example\n\nExample Table Schema:\n```\n|-- a: integer (nullable = false)\n|-- b: struct (nullable = true)\n|    |-- d: integer (nullable = false)\n|-- c: array (nullable = true)\n|    |-- element: integer (containsNull = false)\n|-- e: array (nullable = true)\n|    |-- element: struct (containsNull = true)\n|    |    |-- d: integer (nullable = false)\n|-- f: map (nullable = true)\n|    |-- key: string\n|    |-- value: string (valueContainsNull = true)\n```\n\nJSON Encoded Table Schema:\n```\n{\n  \"type\" : \"struct\",\n  \"fields\" : [ {\n    \"name\" : \"a\",\n    \"type\" : \"integer\",\n    \"nullable\" : false,\n    \"metadata\" : { }\n  }, {\n    \"name\" : \"b\",\n    \"type\" : {\n      \"type\" : \"struct\",\n      \"fields\" : [ {\n        \"name\" : \"d\",\n        \"type\" : \"integer\",\n        \"nullable\" : false,\n        \"metadata\" : { }\n      } ]\n    },\n    \"nullable\" : true,\n    \"metadata\" : { }\n  }, {\n    \"name\" : \"c\",\n    \"type\" : {\n      \"type\" : \"array\",\n      \"elementType\" : \"integer\",\n      \"containsNull\" : false\n    },\n    \"nullable\" : true,\n    \"metadata\" : { }\n  }, {\n    \"name\" : \"e\",\n    \"type\" : {\n      \"type\" : \"array\",\n      \"elementType\" : {\n        \"type\" : \"struct\",\n        \"fields\" : [ {\n          \"name\" : \"d\",\n          \"type\" : \"integer\",\n          \"nullable\" : false,\n          \"metadata\" : { }\n        } ]\n      },\n      \"containsNull\" : true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { }\n  }, {\n    \"name\" : \"f\",\n    \"type\" : {\n      \"type\" : \"map\",\n      \"keyType\" : \"string\",\n      \"valueType\" : \"string\",\n      \"valueContainsNull\" : true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { }\n  } ]\n}\n```\n\n## Checkpoint Schema\nThe following examples uses a table with two partition columns: \"date\" and \"region\" of types date and string, respectively, and three data columns: \"asset\", \"quantity\", and \"is_available\" with data types string, double, and boolean. The checkpoint schema will look as follows:\n\n```\n|-- metaData: struct\n|    |-- id: string\n|    |-- name: string\n|    |-- description: string\n|    |-- format: struct\n|    |    |-- provider: string\n|    |    |-- options: map<string,string>\n|    |-- schemaString: string\n|    |-- partitionColumns: array<string>\n|    |-- createdTime: long\n|    |-- configuration: map<string, string>\n|-- protocol: struct\n|    |-- minReaderVersion: int\n|    |-- minWriterVersion: int\n|    |-- readerFeatures: array[string]\n|    |-- writerFeatures: array[string]\n|-- txn: struct\n|    |-- appId: string\n|    |-- version: long\n|-- add: struct\n|    |-- path: string\n|    |-- partitionValues: map<string,string>\n|    |-- size: long\n|    |-- modificationTime: long\n|    |-- dataChange: boolean\n|    |-- stats: string\n|    |-- tags: map<string,string>\n|    |-- baseRowId: long\n|    |-- defaultRowCommitVersion: long\n|    |-- partitionValues_parsed: struct\n|    |    |-- date: date\n|    |    |-- region: string\n|    |-- stats_parsed: struct\n|    |    |-- numRecords: long\n|    |    |-- minValues: struct\n|    |    |    |-- asset: string\n|    |    |    |-- quantity: double\n|    |    |-- maxValues: struct\n|    |    |    |-- asset: string\n|    |    |    |-- quantity: double\n|    |    |-- nullCount: struct\n|    |    |    |-- asset: long\n|    |    |    |-- quantity: long\n|-- remove: struct\n|    |-- path: string\n|    |-- deletionTimestamp: long\n|    |-- dataChange: boolean\n|-- checkpointMetadata: struct\n|    |-- version: long\n|    |-- tags: map<string,string>\n|-- sidecar: struct\n|    |-- path: string\n|    |-- sizeInBytes: long\n|    |-- modificationTime: long\n|    |-- tags: map<string,string>\n```\n\nObserve that `readerFeatures` and `writerFeatures` fields should comply with:\n- If a table has Reader Version 3, then a writer must write checkpoints with a not-null `readerFeatures` in the schema.\n- If a table has Writer Version 7, then a writer must write checkpoints with a not-null `writerFeatures` in the schema.\n- If a table has neither of the above, then a writer chooses whether to write `readerFeatures` and/or `writerFeatures` into the checkpoint schema. But if it does, their values must be null.\n\nNote that `remove` actions in the checkpoint are tombstones used only by VACUUM, and do not contain the `stats` and `tags` fields.\n\nFor a table that uses column mapping, whether in `id` or `name` mode, the schema of the `add` column will look as follows.\n\nSchema definition:\n```\n{\n  \"type\" : \"struct\",\n  \"fields\" : [ {\n    \"name\" : \"asset\",\n    \"type\" : \"string\",\n    \"nullable\" : true,\n    \"metadata\" : {\n      \"delta.columnMapping.id\": 1,\n      \"delta.columnMapping.physicalName\": \"col-b96921f0-2329-4cb3-8d79-184b2bdab23b\"\n    }\n  }, {\n    \"name\" : \"quantity\",\n    \"type\" : \"double\",\n    \"nullable\" : true,\n    \"metadata\" : {\n      \"delta.columnMapping.id\": 2,\n      \"delta.columnMapping.physicalName\": \"col-04ee4877-ee53-4cb9-b1fb-1a4eb74b508c\"\n    }\n  }, {\n    \"name\" : \"date\",\n    \"type\" : \"date\",\n    \"nullable\" : true,\n    \"metadata\" : {\n      \"delta.columnMapping.id\": 3,\n      \"delta.columnMapping.physicalName\": \"col-798f4abc-c63f-444c-9a04-e2cf1ecba115\"\n    }\n  }, {\n    \"name\" : \"region\",\n    \"type\" : \"string\",\n    \"nullable\" : true,\n    \"metadata\" : {\n      \"delta.columnMapping.id\": 4,\n      \"delta.columnMapping.physicalName\": \"col-19034dc3-8e3d-4156-82fc-8e05533c088e\"\n    }\n  } ]\n}\n```\n\nCheckpoint schema (just the `add` column):\n```\n|-- add: struct\n|    |-- path: string\n|    |-- partitionValues: map<string,string>\n|    |-- size: long\n|    |-- modificationTime: long\n|    |-- dataChange: boolean\n|    |-- stats: string\n|    |-- tags: map<string,string>\n|    |-- baseRowId: long\n|    |-- defaultRowCommitVersion: long\n|    |-- partitionValues_parsed: struct\n|    |    |-- col-798f4abc-c63f-444c-9a04-e2cf1ecba115: date\n|    |    |-- col-19034dc3-8e3d-4156-82fc-8e05533c088e: string\n|    |-- stats_parsed: struct\n|    |    |-- numRecords: long\n|    |    |-- minValues: struct\n|    |    |    |-- col-b96921f0-2329-4cb3-8d79-184b2bdab23b: string\n|    |    |    |-- col-04ee4877-ee53-4cb9-b1fb-1a4eb74b508c: double\n|    |    |-- maxValues: struct\n|    |    |    |-- col-b96921f0-2329-4cb3-8d79-184b2bdab23b: string\n|    |    |    |-- col-04ee4877-ee53-4cb9-b1fb-1a4eb74b508c: double\n|    |    |-- nullCount: struct\n|    |    |    |-- col-b96921f0-2329-4cb3-8d79-184b2bdab23b: long\n|    |    |    |-- col-04ee4877-ee53-4cb9-b1fb-1a4eb74b508c: long\n```\n\n## Last Checkpoint File Schema\n\nThis last checkpoint file is encoded as JSON and contains the following information:\n\nField | Description\n-|-\nversion | The version of the table when the last checkpoint was made.\nsize | The number of actions that are stored in the checkpoint.\nparts | The number of fragments if the last checkpoint was written in multiple parts. This field is optional.\nsizeInBytes | The number of bytes of the checkpoint. This field is optional.\nnumOfAddFiles | The number of AddFile actions in the checkpoint. This field is optional.\ncheckpointSchema | The schema of the checkpoint file. This field is optional.\ntags | String-string map containing any additional metadata about the last checkpoint. This field is optional.\nchecksum | The checksum of the last checkpoint JSON. This field is optional.\n\nThe checksum field is an optional field which contains the MD5 checksum for fields of the last checkpoint json file.\nLast checkpoint file readers are encouraged to validate the checksum, if present, and writers are encouraged to write the checksum\nwhile overwriting the file. Refer to [this section](#json-checksum) for rules around calculating the checksum field\nfor the last checkpoint JSON.\n\n### JSON checksum\nTo generate the checksum for the last checkpoint JSON, firstly, the checksum JSON is canonicalized and converted to a string. Then\nthe 32 character MD5 digest is calculated on the resultant string to get the checksum. Rules for [JSON](https://datatracker.ietf.org/doc/html/rfc8259) canonicalization are:\n\n1. Literal values (`true`, `false`, and `null`) are their own canonical form\n2. Numeric values (e.g. `42` or `3.14`) are their own canonical form\n3. String values (e.g. `\"hello world\"`) are canonicalized by preserving the surrounding quotes and [URL-encoding](#how-to-url-encode-keys-and-string-values)\ntheir content, e.g. `\"hello%20world\"`\n4. Object values (e.g. `{\"a\": 10, \"b\": {\"y\": null, \"x\": \"https://delta.io\"} }` are canonicalized by:\n   * Canonicalize each scalar (leaf) value following the rule for its type (literal, numeric, string)\n   * Canonicalize each (string) name along the path to that value\n   * Connect path segments by `+`, e.g. `\"b\"+\"y\"`\n   * Connect path and value pairs by `=`, e.g. `\"b\"+\"y\"=null`\n   * Sort canonicalized path/value pairs using a byte-order sort on paths. The byte-order sort can be done by converting paths to byte array using UTF-8 charset\\\n    and then comparing them, e.g. `\"a\" < \"b\"+\"x\" < \"b\"+\"y\"`\n   * Separate ordered pairs by `,`, e.g. `\"a\"=10,\"b\"+\"x\"=\"https%3A%2F%2Fdelta.io\",\"b\"+\"y\"=null`\n\n5. Array values (e.g. `[null, \"hi ho\", 2.71]`) are canonicalized as if they were objects, except the \"name\" has numeric type instead of string type, and gives the (0-based)\nposition of the corresponding array element, e.g. `0=null,1=\"hi%20ho\",2=2.71`\n\n6. Top level `checksum` key is ignored in the canonicalization process. e.g.\n`{\"k1\": \"v1\", \"checksum\": \"<anything>\", \"k3\": 23}` is canonicalized to `\"k1\"=\"v1\",\"k3\"=23`\n\n7. Duplicate keys are not allowed in the last checkpoint JSON and such JSON is considered invalid.\n\nGiven the following test sample JSON, a correct implementation of JSON canonicalization should produce the corresponding canonicalized form and checksum value:\ne.g.\nJson: `{\"k0\":\"'v 0'\", \"checksum\": \"adsaskfljadfkjadfkj\", \"k1\":{\"k2\": 2, \"k3\": [\"v3\", [1, 2], {\"k4\": \"v4\", \"k5\": [\"v5\", \"v6\", \"v7\"]}]}}`\\\nCanonicalized form: `\"k0\"=\"%27v%200%27\",\"k1\"+\"k2\"=2,\"k1\"+\"k3\"+0=\"v3\",\"k1\"+\"k3\"+1+0=1,\"k1\"+\"k3\"+1+1=2,\"k1\"+\"k3\"+2+\"k4\"=\"v4\",\"k1\"+\"k3\"+2+\"k5\"+0=\"v5\",\"k1\"+\"k3\"+2+\"k5\"+1=\"v6\",\"k1\"+\"k3\"+2+\"k5\"+2=\"v7\"`\\\nChecksum is `6a92d155a59bf2eecbd4b4ec7fd1f875`\n\n#### How to URL encode keys and string values\nThe [URL Encoding](https://datatracker.ietf.org/doc/html/rfc3986) spec is a bit flexible to give a reliable encoding. e.g. the spec allows both\nuppercase and lowercase as part of percent-encoding. Thus, we require a stricter set of rules for encoding:\n\n1. The string to be encoded must be represented as octets according to the UTF-8 character encoding\n2. All octets except a-z / A-Z / 0-9 / \"-\" / \".\" / \"_\" / \"~\" are reserved\n3. Always [percent-encode](https://datatracker.ietf.org/doc/html/rfc3986#section-2) reserved octets\n4. Never percent-encode non-reserved octets\n5. A percent-encoded octet consists of three characters: `%` followed by its 2-digit hexadecimal value in uppercase letters, e.g. `>` encodes to `%3E`\n\n## Delta Data Type to Parquet Type Mappings\nBelow table captures how each Delta data type is stored physically in Parquet files. Parquet files are used for storing the table data or metadata ([checkpoints](#checkpoints)). Parquet has a limited number of [physical types](https://parquet.apache.org/docs/file-format/types/). Parquet [logical types](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md) are used to extend the types by specifying how the physical types should be interpreted.\n\nFor some of the Delta data types, there are multiple ways store the values physically in Parquet file. For example, `timestamp` can be stored either as `int96` or `int64`. The exact physical type depends on the engine that is writing the Parquet file and/or engine specific configuration options. For a Delta lake table reader, it is recommended that the Parquet file reader support at least the Parquet physical and logical types mentioned in the below table.\n\nDelta Type Name | Parquet Physical Type | Parquet Logical Type\n-|-|-\nboolean| `boolean` |\nbyte| `int32` | `INT(bitwidth = 8, signed = true)`\nshort| `int32` | `INT(bitwidth = 16, signed = true)`\nint| `int32` | `INT(bitwidth = 32, signed = true)`\nlong| `int64` | `INT(bitwidth = 64, signed = true)`\ndate| `int32` | `DATE`\ntimestamp| `int96` or `int64` | `TIMESTAMP(isAdjustedToUTC = true, units = microseconds)`\ntimestamp without time zone| `int96` or `int64` | `TIMESTAMP(isAdjustedToUTC = false, units = microseconds)`\nfloat| `float` |\ndouble| `double` |\ndecimal| `int32`, `int64` or `fixed_length_binary` | `DECIMAL(scale, precision)`\nstring| `binary` | `string (UTF-8)`\nbinary| `binary` |\narray| either as `2-level` or `3-level` representation. Refer to [Parquet documentation](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists) for further details | `LIST`\nmap| either as `2-level` or `3-level` representation. Refer to [Parquet documentation](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#maps) for further details | `MAP`\nstruct| `group` |\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"docs/src/assets/delta-lake-logo-light.svg\" width=\"200\" alt=\"Delta Lake Logo\"></img>\n\n[![Test](https://github.com/delta-io/delta/actions/workflows/test.yaml/badge.svg)](https://github.com/delta-io/delta/actions/workflows/test.yaml)\n[![License](https://img.shields.io/badge/license-Apache%202-brightgreen.svg)](https://github.com/delta-io/delta/blob/master/LICENSE.txt)\n[![PyPI](https://img.shields.io/pypi/v/delta-spark.svg)](https://pypi.org/project/delta-spark/)\n[![PyPI - Downloads](https://img.shields.io/pypi/dm/delta-spark)](https://pypistats.org/packages/delta-spark)\n\nDelta Lake is an open-source storage framework that enables building a [Lakehouse architecture](http://cidrdb.org/cidr2021/papers/cidr2021_paper17.pdf) with compute engines including Spark, PrestoDB, Flink, Trino, and Hive and APIs for Scala, Java, Rust, Ruby, and Python. \n* See the [Delta Lake Documentation](https://docs.delta.io) for details.\n* See the [Quick Start Guide](https://docs.delta.io/latest/quick-start.html) to get started with Scala, Java and Python.\n* Note, this repo is one of many Delta Lake repositories in the [delta.io](https://github.com/delta-io) organizations including\n[delta](https://github.com/delta-io/delta), \n[delta-rs](https://github.com/delta-io/delta-rs),\n[delta-sharing](https://github.com/delta-io/delta-sharing),\n[kafka-delta-ingest](https://github.com/delta-io/kafka-delta-ingest), and\n[website](https://github.com/delta-io/website).\n\nThe following are some of the more popular Delta Lake integrations, refer to [delta.io/integrations](https://delta.io/integrations/) for the complete list:\n\n* [Apache Spark™](https://docs.delta.io/): This connector allows Apache Spark™ to read from and write to Delta Lake.\n* [Apache Flink (Preview)](https://github.com/delta-io/delta/tree/master/connectors/flink): This connector allows Apache Flink to write to Delta Lake.\n* [PrestoDB](https://prestodb.io/docs/current/connector/deltalake.html): This connector allows PrestoDB to read from Delta Lake.\n* [Trino](https://trino.io/docs/current/connector/delta-lake.html): This connector allows Trino to read from and write to Delta Lake.\n* [Delta Standalone](https://docs.delta.io/latest/delta-standalone.html): This library allows Scala and Java-based projects (including Apache Flink, Apache Hive, Apache Beam, and PrestoDB) to read from and write to Delta Lake.\n* [Apache Hive](https://docs.delta.io/latest/hive-integration.html): This connector allows Apache Hive to read from Delta Lake.\n* [Delta Rust API](https://docs.rs/deltalake/latest/deltalake/): This library allows Rust (with Python and Ruby bindings) low level access to Delta tables and is intended to be used with data processing frameworks like datafusion, ballista, rust-dataframe, vega, etc.\n\n<br/>\n\n<details>\n<summary><strong><em>Table of Contents</em></strong></summary>\n\n* [Latest binaries](#latest-binaries)\n* [API Documentation](#api-documentation)\n* [Compatibility](#compatibility)\n  * [API Compatibility](#api-compatibility)\n  * [Data Storage Compatibility](#data-storage-compatibility)\n* [Roadmap](#roadmap)\n* [Building](#building)\n* [Transaction Protocol](#transaction-protocol)\n* [Requirements for Underlying Storage Systems](#requirements-for-underlying-storage-systems)\n* [Concurrency Control](#concurrency-control)\n* [Reporting issues](#reporting-issues)\n* [Contributing](#contributing)\n* [License](#license)\n* [Community](#community)\n</details>\n\n\n## Latest Binaries\n\nSee the [online documentation](https://docs.delta.io/latest/) for the latest release.\n\n## API Documentation\n\n* [Scala API docs](https://docs.delta.io/latest/delta-apidoc.html)\n* [Java API docs](https://docs.delta.io/latest/api/java/index.html)\n* [Python API docs](https://docs.delta.io/latest/api/python/index.html)\n\n## Compatibility\n[Delta Standalone](https://docs.delta.io/latest/delta-standalone.html) library is a single-node Java library that can be used to read from and write to Delta tables. Specifically, this library provides APIs to interact with a table’s metadata in the transaction log, implementing the Delta Transaction Log Protocol to achieve the transactional guarantees of the Delta Lake format.\n\n\n### API Compatibility\n\nThere are two types of APIs provided by the Delta Lake project. \n\n- Direct Java/Scala/Python APIs - The classes and methods documented in the [API docs](https://docs.delta.io/latest/delta-apidoc.html) are considered as stable public APIs. All other classes, interfaces, methods that may be directly accessible in code are considered internal, and they are subject to change across releases.\n- Spark-based APIs - You can read Delta tables through the `DataFrameReader`/`Writer` (i.e. `spark.read`, `df.write`, `spark.readStream` and `df.writeStream`). Options to these APIs will remain stable within a major release of Delta Lake (e.g., 1.x.x).\n- See the [online documentation](https://docs.delta.io/latest/releases.html) for the releases and their compatibility with Apache Spark versions.\n\n\n### Data Storage Compatibility\n\nDelta Lake guarantees backward compatibility for all Delta Lake tables (i.e., newer versions of Delta Lake will always be able to read tables written by older versions of Delta Lake). However, we reserve the right to break forward compatibility as new features are introduced to the transaction protocol (i.e., an older version of Delta Lake may not be able to read a table produced by a newer version).\n\nBreaking changes in the protocol are indicated by incrementing the minimum reader/writer version in the `Protocol` [action](https://github.com/delta-io/delta/blob/master/spark/src/test/scala/org/apache/spark/sql/delta/ActionSerializerSuite.scala).\n\n## Roadmap\n\n* For the high-level Delta Lake roadmap, see [Delta Lake 2022H1 roadmap](http://delta.io/roadmap).  \n* For the detailed timeline, see the [project roadmap](https://github.com/delta-io/delta/milestones). \n\n## Transaction Protocol\n\n[Delta Transaction Log Protocol](PROTOCOL.md) document provides a specification of the transaction protocol.\n\n## Requirements for Underlying Storage Systems\n\nDelta Lake ACID guarantees are predicated on the atomicity and durability guarantees of the storage system. Specifically, we require the storage system to provide the following.\n\n1. **Atomic visibility**: There must be a way for a file to be visible in its entirety or not visible at all.\n2. **Mutual exclusion**: Only one writer must be able to create (or rename) a file at the final destination.\n3. **Consistent listing**: Once a file has been written in a directory, all future listings for that directory must return that file.\n\nSee the [online documentation on Storage Configuration](https://docs.delta.io/latest/delta-storage.html) for details.\n\n## Concurrency Control\n\nDelta Lake ensures _serializability_ for concurrent reads and writes. Please see [Delta Lake Concurrency Control](https://docs.delta.io/concurrency-control/) for more details.\n\n## Reporting issues\n\nWe use [GitHub Issues](https://github.com/delta-io/delta/issues) to track community reported issues. You can also [contact](#community) the community for getting answers.\n\n## Contributing \n\nWe welcome contributions to Delta Lake. See our [CONTRIBUTING.md](https://github.com/delta-io/delta/blob/master/CONTRIBUTING.md) for more details.\n\nWe also adhere to the [Delta Lake Code of Conduct](https://github.com/delta-io/delta/blob/master/CODE_OF_CONDUCT.md).\n\n## Building\n\nDelta Lake is compiled using [SBT](https://www.scala-sbt.org/1.x/docs/Command-Line-Reference.html).\nEnsure that your Java version is at least 17 (you can verify with `java -version`).\n\nTo compile, run\n\n    build/sbt compile\n\nTo generate artifacts, run\n\n    build/sbt package\n\nTo execute tests, run\n\n    build/sbt test\n\nTo execute a single test suite, run\n\n    build/sbt spark/'testOnly org.apache.spark.sql.delta.optimize.OptimizeCompactionSQLSuite'\n\nTo execute a single test within and a single test suite, run\n\n    build/sbt spark/'testOnly *.OptimizeCompactionSQLSuite -- -z \"optimize command: on partitioned table - all partitions\"'\n\nRefer to [SBT docs](https://www.scala-sbt.org/1.x/docs/Command-Line-Reference.html) for more commands.\n\n## Running python tests locally\n\n### Setup Environment\n#### Install Conda (Skip if you already installed it)\nFollow [Conda Download](https://www.anaconda.com/download/) to install Anaconda.\n\n#### Create an environment from environment file\nFollow [Create Environment From Environment file](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-from-file) to create a Conda environment from `<repo-root>/python/environment.yml` and activate the newly created `delta_python_tests` environment.\n\n```\n# Note the `--file` argument should be a fully qualified path. Using `~` in file\n# path doesn't work. Example valid path: `/Users/macuser/delta/python/environment.yml`\n\nconda env create --name delta_python_tests --file=<absolute_path_to_delta_repo>/python/environment.yml`\n```\n\n#### JDK Setup\nBuild needs JDK 11. Make sure to setup `JAVA_HOME` that points to JDK 11.\n\n#### Running tests\n```\nconda activate delta_python_tests\npython3 <delta-root>/python/run-tests.py\n```\n\n## IntelliJ Setup\n\nIntelliJ is the recommended IDE to use when developing Delta Lake. To import Delta Lake as a new project:\n1. Clone Delta Lake into, for example, `~/delta`.\n2. In IntelliJ, select `File` > `New Project` > `Project from Existing Sources...` and select `~/delta`.\n3. Under `Import project from external model` select `sbt`. Click `Next`.\n4. Under `Project JDK` specify a valid Java `11` JDK and opt to use SBT shell for `project reload` and `builds`.\n5. Click `Finish`.\n6. In your terminal, run `build/sbt clean package`. Make sure you use Java `11`. The build will generate files \n   that are necessary for Intellij to index the repository.\n\n### Setup Verification\n\nAfter waiting for IntelliJ to index, verify your setup by running a test suite in IntelliJ.\n1. Search for and open `DeltaLogSuite`\n2. Next to the class declaration, right click on the two green arrows and select `Run 'DeltaLogSuite'`\n\n### Troubleshooting\n\nIf you see errors of the form\n\n```\nError:(46, 28) object DeltaSqlBaseParser is not a member of package io.delta.sql.parser\nimport io.delta.sql.parser.DeltaSqlBaseParser._\n...\nError:(91, 22) not found: type DeltaSqlBaseParser\n    val parser = new DeltaSqlBaseParser(tokenStream)\n```\n\nthen follow these steps:\n1. Ensure you are using Java `11`. You can set this using\n```\nexport JAVA_HOME=`/usr/libexec/java_home -v 11`\n```\n2. Compile using the SBT CLI: `build/sbt clean compile`.\n2. Go to `File` > `Project Structure...` > `Modules` > `delta-spark`.\n3. In the right panel under `Source Folders` remove any `target` folders, e.g. `target/scala-2.12/src_managed/main [generated]`\n4. Click `Apply` and then re-run your test.\n\n## License\nApache License 2.0, see [LICENSE](https://github.com/delta-io/delta/blob/master/LICENSE.txt).\n\n## Community\n\nThere are two mediums of communication within the Delta Lake community.\n\n* Public Slack Channel\n  - [Register here](https://go.delta.io/slack)\n  - [Login here](https://delta-users.slack.com/)\n* [Linkedin page](https://www.linkedin.com/company/deltalake)\n* [Youtube channel](https://www.youtube.com/c/deltalake)\n* Public [Mailing list](https://groups.google.com/forum/#!forum/delta-users)\n"
  },
  {
    "path": "benchmarks/README.md",
    "content": "# Benchmarks \n\n## Overview\nThis is a basic framework for writing benchmarks to measure Delta's performance. It is currently designed to run benchmark on Spark running in an EMR or a Dataproc cluster. However, it can be easily extended for other Spark-based benchmarks. To get started, first download/clone this repository in your local machine. Then you have to set up a cluster and run the benchmark scripts in this directory. See the next section for more details.\n\n## Running TPC-DS benchmark\n\nThis TPC-DS benchmark is constructed such that you have to run the following two steps. \n1. *Load data*: You have to create the TPC-DS database with all the Delta tables. To do that, the raw TPC-DS data has been provided as Apache Parquet files. In this step you will have to use your EMR or a Dataproc cluster to read the parquet files and rewrite them as Delta tables.\n2. *Query data*: Then, using the tables definitions in the Hive Metatore, you can run the 99 benchmark queries.   \n\nThe next section will provide the detailed steps of how to setup the necessary Hive Metastore and a cluster, how to test the setup with small-scale data, and then finally run the full scale benchmark.\n\n### Configure cluster with Amazon Web Services\n\n#### Prerequisites\n- An AWS account with necessary permissions to do the following:\n  - Manage RDS instances for creating an external Hive Metastore\n  - Manage EMR clusters for running the benchmark\n  - Read and write to an S3 bucket from the EMR cluster\n- A S3 bucket which will be used to generate the TPC-DS data.\n- A machine which has access to the AWS setup and where this repository has been downloaded or cloned.\n\nThere are two ways to create infrastructure required for benchmarks - using provided [Terraform template](infrastructure/aws/terraform/README.md) or manually (described below).\n\n#### Create external Hive Metastore using Amazon RDS\nCreate an external Hive Metastore in a MySQL database using Amazon RDS with the following specifications:\n- MySQL 8.x on a `db.m5.large`.\n- General purpose SSDs, and no Autoscaling storage.\n- Non-empty password for admin\n- Same region, VPC, subnet as those you will run the EMR cluster. See AWS docs for more guidance.\n  - *Note:* Region us-west-2 since that is what this benchmark has been most tested with.\n\nAfter the database is ready, note the JDBC connection details, the username and password. We will need them for the next step. Note that this step needs to be done just once. All EMR clusters can connect and reused this Hive Metastsore. \n  \n#### Create EMR cluster\nCreate an EMR cluster that connects to the external Hive Metastore.  Here are the specifications of the EMR cluster required for running benchmarks.\n- EMR with Spark and Hive (needed for writing to Hive Metastore). Choose the EMR version based on the Spark version compatible with the format. For example:\n  - For Delta 2.0 on Spark 3.2 - EMR 6.6.0\n  - For Delta 1.0 on Spark 3.1 - EMR 6.5.0\n- Master - i3.2xlarge\n- Workers - 16 x i3.2xlarge (or just 1 worker if you are just testing by running the 1GB benchmark).\n- Hive-site configuration to connect to the Hive Metastore. See [Using an external MySQL database or Amazon Aurora](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-hive-metastore-external.html) for more details.\n- Same region, VPC, subnet as those of the Hive Metastore.\n  - *Note:* Region us-west-2 since that is what this benchmark has been most tested with.\n- No autoscaling, and default EBS storage.\n\nOnce the EMR cluster is ready, note the following: \n- Hostname of the EMR cluster master node.\n- PEM file for SSH into the master node.\nThese will be needed to run the workloads in this framework. \n\n#### Prepare S3 bucket\nCreate a new S3 bucket (or use an existing one) which is in the same region as your EMR cluster.\n\n_________________\n\n### Configure cluster with Google Cloud Platform\n\n#### Prerequisites\n- A GCP account with necessary permissions to do the following:\n  - Manage Dataproc clusters for running the benchmark\n  - Manage Dataproc Metastore instances\n  - Read and write to a GCS bucket from the Dataproc cluster\n- A GCS bucket which will be used to generate the TPC-DS data.\n- A machine which has access to the GCP setup and where this repository has been downloaded or cloned.\n- SSH keys for a user which will be used to access the master node. The user's SSH key can be either [a project-wide key](https://cloud.google.com/compute/docs/connect/add-ssh-keys#add_ssh_keys_to_project_metadata) \n  or assigned to the [master node](https://cloud.google.com/compute/docs/connect/add-ssh-keys#after-vm-creation) only.\n- Ideally, all GCP components used in benchmark should be in the same location (Storage bucket, Dataproc Metastore service and Dataproc cluster).\n\nThere are two ways to create infrastructure required for benchmarks - using provided [Terraform template](infrastructure/gcp/terraform/README.md) or manually (described below).\n\n#### Prepare GCS bucket\nCreate a new GCS bucket (or use an existing one) which is in the same region as your Dataproc cluster.\n\n#### Create Dataproc Metastore\nYou can create [Dataproc metastore](https://cloud.google.com/dataproc-metastore/docs/create-service)\neither via Web Console or gcloud command.\n\nSample create command:\n```bash\ngcloud metastore services create dataproc-metastore-for-benchmarks \\\n    --location=<region> \\\n    --tier=enterprise\n```\n\n#### Create Dataproc cluster\nHere are the specifications of the Dataproc cluster required for running benchmarks.\n- Image version >= 2.0 having Apache Spark 3.1\n- Master - n2-highmem-8 (8 vCPU, 64 GB memory)\n- Workers - 16 x n2-highmem-8 (or just 2 workers if you are just testing by running the 1GB benchmark).\n- The cluster connects to the Dataproc Metastore.\n- Same region and subnet as those of the Dataproc Metastore and GCS bucket.\n- No autoscaling.\n\nSample create command:\n```bash\ngcloud dataproc clusters create delta-performance-benchmarks-cluster \\\n    --project <project-name> \\\n    --enable-component-gateway \\\n    --region <region> \\\n    --zone <zone> \\\n    --subnet default \\\n    --master-machine-type n2-highmem-8 \\\n    --master-boot-disk-type pd-ssd \\\n    --master-boot-disk-size 100 \\\n    --num-master-local-ssds 4 \\\n    --master-local-ssd-interface NVME \\\n    --num-workers 16 \\\n    --worker-machine-type n2-highmem-8 \\\n    --worker-boot-disk-type pd-ssd \\\n    --worker-boot-disk-size 100 \\\n    --num-worker-local-ssds 4 \\\n    --worker-local-ssd-interface NVME \\\n    --dataproc-metastore projects/<project-name>/locations/<region>/services/dataproc-metastore-for-benchmarks \\\n    --enable-component-gateway \\\n    --image-version 2.0-debian10\n```\n\n#### Input data\nThe benchmark is run using the raw TPC-DS data which has been provided as Apache Parquet files. There are two\npredefined datasets of different size, 1GB and 3TB, located in `s3://devrel-delta-datasets/tpcds-2.13/tpcds_sf1_parquet/`\nand `s3://devrel-delta-datasets/tpcds-2.13/tpcds_sf3000_parquet/`, respectively. Please keep in mind that\n`devrel-delta-datasets` bucket is configured as [Requester Pays](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ObjectsinRequesterPaysBuckets.html) bucket,\nso [access requests have to be configured properly](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ObjectsinRequesterPaysBuckets.html).\n\nUnfortunately, Hadoop in versions available in Dataproc does not support *Requester Pays* feature. It will be available\nas of Hadoop 3.3.4 ([HADOOP-14661](https://issues.apache.org/jira/browse/HADOOP-14661)).\n\nIn consequence, one need to copy the datasets to Google Storage manually before running benchmarks. The simplest\nsolution is to copy the data in two steps: first to a S3 bucket with *Requester Pays* disabled, then copy the data\nusing [Cloud Storage Transfer Service](https://cloud.google.com/storage-transfer/docs/how-to).\n\n_________________\n\n### Test the cluster setup\nNavigate to your local copy of this repository and this benchmark directory. Then run the following steps.\n\n#### Run simple test workload\nVerify that you have the following information\n  - <HOST_NAME>: Cluster master node host name\n  - <PEM_FILE>: Local path to your PEM file for SSH into the master node.\n  - <SSH_USER>: The username that will be used to SSH into the master node. The username is tied to the SSH key you\n    have imported into the cloud. It defaults to `hadoop`.\n  - <BENCHMARK_PATH>: Path where tables will be created. Make sure your credentials have read/write permission to that path.\n  - <CLOUD_PROVIDER>: Currently either `gcp` or `aws`. For each storage type, different Delta properties might be added.\n    \nThen run a simple table write-read test: Run the following in your shell.\n\n```sh\n./run-benchmark.py \\\n    --cluster-hostname <HOSTNAME> \\\n    -i <PEM_FILE> \\\n    --ssh-user <SSH_USER> \\\n    --benchmark-path <BENCHMARK_PATH> \\\n    --cloud-provider <CLOUD_PROVIDER> \\\n    --benchmark test\n```\n\nIf this works correctly, then you should see an output that look like this.\n     \n```text\n>>> Benchmark script generated and uploaded\n\n...\nThere is a screen on:\n12001..ip-172-31-21-247\t(Detached)\n\nFiles for this benchmark:\n20220126-191336-test-benchmarks.jar\n20220126-191336-test-cmd.sh\n20220126-191336-test-out.txt\n>>> Benchmark script started in a screen. Stdout piped into 20220126-191336-test-out.txt.Final report will be generated on completion in 20220126-191336-test-report.json.\n```\n\nThe test workload launched in a `screen` is going to run the following: \n- Spark jobs to run a simple SQL query\n- Create a Delta table in the given location \n- Read it back\n    \nTo see whether they worked correctly, SSH into the node and check the output of 20220126-191336-test-out.txt. Once the workload terminates, the last few lines should be something like the following:\n```text\nRESULT:\n{\n  \"benchmarkSpecs\" : {\n    \"benchmarkPath\" : ...,\n    \"benchmarkId\" : \"20220126-191336-test\"\n  },\n  \"queryResults\" : [ {\n    \"name\" : \"sql-test\",\n    \"durationMs\" : 11075\n  }, {\n    \"name\" : \"db-list-test\",\n    \"durationMs\" : 208\n  }, {\n    \"name\" : \"db-create-test\",\n    \"durationMs\" : 4070\n  }, {\n    \"name\" : \"db-use-test\",\n    \"durationMs\" : 41\n  }, {\n    \"name\" : \"table-drop-test\",\n    \"durationMs\" : 74\n  }, {\n    \"name\" : \"table-create-test\",\n    \"durationMs\" : 33812\n  }, {\n    \"name\" : \"table-query-test\",\n    \"durationMs\" : 4795\n  } ]\n}\nFILE UPLOAD: Uploaded /home/hadoop/20220126-191336-test-report.json to s3:// ...\nSUCCESS\n```\n    \nThe above metrics are also written to a json file and uploaded to the given path. Please verify that both the table and report are generated in that path. \n\n#### Run 1GB TPC-DS\nNow that you are familiar with how the framework runs the workload, you can try running the small scale TPC-DS benchmark.\n\n\n1. Load data as Delta tables:\n    ```bash\n    ./run-benchmark.py \\\n        --cluster-hostname <HOSTNAME> \\\n        -i <PEM_FILE> \\\n        --ssh-user <SSH_USER> \\\n        --benchmark-path <BENCHMARK_PATH> \\\n        --cloud-provider <CLOUD_PROVIDER> \\\n        --benchmark tpcds-1gb-delta-load\n    ```\n   If you run the benchmark in GCP you should provide `--source-path <SOURCE_PATH>` parameter, where `<SOURCE_PATH>` is the location of the raw parquet input data files (see *Input data* section).\n    ```bash\n    ./run-benchmark.py \\\n        --cluster-hostname <HOSTNAME> \\\n        -i <PEM_FILE> \\\n        --ssh-user <SSH_USER> \\\n        --benchmark-path <BENCHMARK_PATH> \\\n        --source-path <SOURCE_PATH> \\\n        --cloud-provider gcp \\\n        --benchmark tpcds-1gb-delta-load\n    ```\n\n3. Run queries on Delta tables:\n    ```bash\n    ./run-benchmark.py \\\n        --cluster-hostname <HOSTNAME> \\\n        -i <PEM_FILE> \\\n        --ssh-user <SSH_USER> \\\n        --benchmark-path <BENCHMARK_PATH> \\\n        --cloud-provider <CLOUD_PROVIDER> \\\n        --benchmark tpcds-1gb-delta\n    ```\n\n### Run 3TB TPC-DS\nFinally, you are all set up to run the full scale benchmark. Similar to the 1GB benchmark, run the following\n\n1. Load data as Delta tables:\n    ```bash\n    ./run-benchmark.py \\\n        --cluster-hostname <HOSTNAME> \\\n        -i <PEM_FILE> \\\n        --ssh-user <SSH_USER> \\\n        --benchmark-path <BENCHMARK_PATH> \\\n        --cloud-provider <CLOUD_PROVIDER> \\\n        --benchmark tpcds-3tb-delta-load\n    ```\n   If you run the benchmark in GCP you should provide `--source-path <SOURCE_PATH>` parameter, where `<SOURCE_PATH>` is the location of the raw parquet input data files (see *Input data* section).\n    ```bash\n    ./run-benchmark.py \\\n        --cluster-hostname <HOSTNAME> \\\n        -i <PEM_FILE> \\\n        --ssh-user <SSH_USER> \\\n        --benchmark-path <BENCHMARK_PATH> \\\n        --source-path <SOURCE_PATH> \\\n        --cloud-provider gcp \\\n        --benchmark tpcds-3tb-delta-load\n    ```\n\n2. Run queries on Delta tables:\n    ```bash\n    ./run-benchmark.py \\\n        --cluster-hostname <HOSTNAME> \\\n        -i <PEM_FILE> \\\n        --ssh-user <SSH_USER> \\\n        --benchmark-path <BENCHMARK_PATH> \\\n        --cloud-provider <CLOUD_PROVIDER> \\\n        --benchmark tpcds-3tb-delta\n    ```\n\nCompare the results using the generated JSON files.\n\n_________________\n\n## Internals of the framework\n\nStructure of this framework's code\n- `build.sbt`, `project/`, `src/` form the SBT project which contains the Scala code that define the benchmark workload.\n    - `Benchmark.scala` is the basic interface, and `TestBenchmark.scala` is a sample implementation.\n- `run-benchmark.py` contains the specification of the benchmarks defined by name (e.g. `tpcds-3tb-delta`). Each benchmark specification is defined by the following: \n    - Fully qualified name of the main Scala class to be started.\n    - Command line argument for the main function.\n    - Additional Maven artifact to load (example `io.delta:delta-core_2.12:1.0.0`).\n    - Spark configurations to use.\n- `scripts` has the core python scripts that are called by `run-benchmark.py`\n\nThe script `run-benchmark.py` does the following:\n- Compile the Scala code into a uber jar.\n- Upload it to the given hostname.\n- Using ssh to the hostname, it will launch a screen and start the main class with spark-submit.\n"
  },
  {
    "path": "benchmarks/build/sbt",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n\n# When creating new tests for Spark SQL Hive, the HADOOP_CLASSPATH must contain the hive jars so\n# that we can run Hive to generate the golden answer.  This is not required for normal development\n# or testing.\nif [ -n \"$HIVE_HOME\" ]; then\n    for i in \"$HIVE_HOME\"/lib/*\n    do HADOOP_CLASSPATH=\"$HADOOP_CLASSPATH:$i\"\n    done\n    export HADOOP_CLASSPATH\nfi\n\nrealpath () {\n(\n  TARGET_FILE=\"$1\"\n\n  cd \"$(dirname \"$TARGET_FILE\")\"\n  TARGET_FILE=\"$(basename \"$TARGET_FILE\")\"\n\n  COUNT=0\n  while [ -L \"$TARGET_FILE\" -a $COUNT -lt 100 ]\n  do\n      TARGET_FILE=\"$(readlink \"$TARGET_FILE\")\"\n      cd $(dirname \"$TARGET_FILE\")\n      TARGET_FILE=\"$(basename $TARGET_FILE)\"\n      COUNT=$(($COUNT + 1))\n  done\n\n  echo \"$(pwd -P)/\"$TARGET_FILE\"\"\n)\n}\n\nif [[ \"$JENKINS_URL\" != \"\" ]]; then\n  # Make Jenkins use Google Mirror first as Maven Central may ban us\n  SBT_REPOSITORIES_CONFIG=\"$(dirname \"$(realpath \"$0\")\")/sbt-config/repositories\"\n  export SBT_OPTS=\"-Dsbt.override.build.repos=true -Dsbt.repository.config=$SBT_REPOSITORIES_CONFIG\"\nfi\n\n. \"$(dirname \"$(realpath \"$0\")\")\"/sbt-launch-lib.bash\n\n\ndeclare -r noshare_opts=\"-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy\"\ndeclare -r sbt_opts_file=\".sbtopts\"\ndeclare -r etc_sbt_opts_file=\"/etc/sbt/sbtopts\"\n\nusage() {\n cat <<EOM\nUsage: $script_name [options]\n\n  -h | -help         print this message\n  -v | -verbose      this runner is chattier\n  -d | -debug        set sbt log level to debug\n  -no-colors         disable ANSI color codes\n  -sbt-create        start sbt even if current directory contains no sbt project\n  -sbt-dir   <path>  path to global settings/plugins directory (default: ~/.sbt)\n  -sbt-boot  <path>  path to shared boot directory (default: ~/.sbt/boot in 0.11 series)\n  -ivy       <path>  path to local Ivy repository (default: ~/.ivy2)\n  -mem    <integer>  set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem))\n  -no-share          use all local caches; no sharing\n  -no-global         uses global caches, but does not use global ~/.sbt directory.\n  -jvm-debug <port>  Turn on JVM debugging, open at the given port.\n  -batch             Disable interactive mode\n\n  # sbt version (default: from project/build.properties if present, else latest release)\n  -sbt-version  <version>   use the specified version of sbt\n  -sbt-jar      <path>      use the specified jar as the sbt launcher\n  -sbt-rc                   use an RC version of sbt\n  -sbt-snapshot             use a snapshot version of sbt\n\n  # java version (default: java from PATH, currently $(java -version 2>&1 | grep version))\n  -java-home <path>         alternate JAVA_HOME\n\n  # jvm options and output control\n  JAVA_OPTS          environment variable, if unset uses \"$java_opts\"\n  SBT_OPTS           environment variable, if unset uses \"$default_sbt_opts\"\n  .sbtopts           if this file exists in the current directory, it is\n                     prepended to the runner args\n  /etc/sbt/sbtopts   if this file exists, it is prepended to the runner args\n  -Dkey=val          pass -Dkey=val directly to the java runtime\n  -J-X               pass option -X directly to the java runtime\n                     (-J is stripped)\n  -S-X               add -X to sbt's scalacOptions (-S is stripped)\n  -PmavenProfiles    Enable a maven profile for the build.\n\nIn the case of duplicated or conflicting options, the order above\nshows precedence: JAVA_OPTS lowest, command line options highest.\nEOM\n}\n\nprocess_my_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n     -no-colors) addJava \"-Dsbt.log.noformat=true\" && shift ;;\n      -no-share) addJava \"$noshare_opts\" && shift ;;\n     -no-global) addJava \"-Dsbt.global.base=$(pwd)/project/.sbtboot\" && shift ;;\n      -sbt-boot) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.boot.directory=$2\" && shift 2 ;;\n       -sbt-dir) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.global.base=$2\" && shift 2 ;;\n     -debug-inc) addJava \"-Dxsbt.inc.debug=true\" && shift ;;\n         -batch) exec </dev/null && shift ;;\n\n    -sbt-create) sbt_create=true && shift ;;\n\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  # Now, ensure sbt version is used.\n  [[ \"${sbt_version}XXX\" != \"XXX\" ]] && addJava \"-Dsbt.version=$sbt_version\"\n}\n\nloadConfigFile() {\n  cat \"$1\" | sed '/^\\#/d'\n}\n\n# if sbtopts files exist, prepend their contents to $@ so it can be processed by this runner\n[[ -f \"$etc_sbt_opts_file\" ]] && set -- $(loadConfigFile \"$etc_sbt_opts_file\") \"$@\"\n[[ -f \"$sbt_opts_file\" ]] && set -- $(loadConfigFile \"$sbt_opts_file\") \"$@\"\n\nexit_status=127\nsaved_stty=\"\"\n\nrestoreSttySettings() {\n  stty $saved_stty\n  saved_stty=\"\"\n}\n\nonExit() {\n  if [[ \"$saved_stty\" != \"\" ]]; then\n    restoreSttySettings\n  fi\n  exit $exit_status\n}\n\nsaveSttySettings() {\n  saved_stty=$(stty -g 2>/dev/null)\n  if [[ ! $? ]]; then\n    saved_stty=\"\"\n  fi\n}\n\nsaveSttySettings\ntrap onExit INT\n\nrun \"$@\"\n\nexit_status=$?\nonExit\n"
  },
  {
    "path": "benchmarks/build/sbt-launch-lib.bash",
    "content": "#!/usr/bin/env bash\n#\n\n# A library to simplify using the SBT launcher from other packages.\n# Note: This should be used by tools like giter8/conscript etc.\n\n# TODO - Should we merge the main SBT script with this library?\n\nif test -z \"$HOME\"; then\n  declare -r script_dir=\"$(dirname \"$script_path\")\"\nelse\n  declare -r script_dir=\"$HOME/.sbt\"\nfi\n\ndeclare -a residual_args\ndeclare -a java_args\ndeclare -a scalac_args\ndeclare -a sbt_commands\ndeclare -a maven_profiles\n\nif test -x \"$JAVA_HOME/bin/java\"; then\n    echo -e \"Using $JAVA_HOME as default JAVA_HOME.\"\n    echo \"Note, this will be overridden by -java-home if it is set.\"\n    declare java_cmd=\"$JAVA_HOME/bin/java\"\nelse\n    declare java_cmd=java\nfi\n\nechoerr () {\n  echo 1>&2 \"$@\"\n}\nvlog () {\n  [[ $verbose || $debug ]] && echoerr \"$@\"\n}\ndlog () {\n  [[ $debug ]] && echoerr \"$@\"\n}\n\nacquire_sbt_jar () {\n  SBT_VERSION=`awk -F \"=\" '/sbt\\.version/ {print $2}' ./project/build.properties`\n  URL1=${DEFAULT_ARTIFACT_REPOSITORY:-https://repo1.maven.org/maven2/}org/scala-sbt/sbt-launch/${SBT_VERSION}/sbt-launch-${SBT_VERSION}.jar\n  JAR=build/sbt-launch-${SBT_VERSION}.jar\n\n  sbt_jar=$JAR\n\n  if [[ ! -f \"$sbt_jar\" ]]; then\n    # Download sbt launch jar if it hasn't been downloaded yet\n    if [ ! -f \"${JAR}\" ]; then\n    # Download\n    printf \"Attempting to fetch sbt\\n\"\n    JAR_DL=\"${JAR}.part\"\n    if [ $(command -v curl) ]; then\n      curl --fail --location --silent ${URL1} > \"${JAR_DL}\" &&\\\n        mv \"${JAR_DL}\" \"${JAR}\"\n    elif [ $(command -v wget) ]; then\n      wget --quiet ${URL1} -O \"${JAR_DL}\" &&\\\n        mv \"${JAR_DL}\" \"${JAR}\"\n    else\n      printf \"You do not have curl or wget installed, please install sbt manually from http://www.scala-sbt.org/\\n\"\n      exit -1\n    fi\n    fi\n    if [ ! -f \"${JAR}\" ]; then\n    # We failed to download\n    printf \"Our attempt to download sbt locally to ${JAR} failed. Please install sbt manually from http://www.scala-sbt.org/\\n\"\n    exit -1\n    fi\n    printf \"Launching sbt from ${JAR}\\n\"\n  fi\n}\n\nexecRunner () {\n  # print the arguments one to a line, quoting any containing spaces\n  [[ $verbose || $debug ]] && echo \"# Executing command line:\" && {\n    for arg; do\n      if printf \"%s\\n\" \"$arg\" | grep -q ' '; then\n        printf \"\\\"%s\\\"\\n\" \"$arg\"\n      else\n        printf \"%s\\n\" \"$arg\"\n      fi\n    done\n    echo \"\"\n  }\n\n  \"$@\"\n}\n\naddJava () {\n  dlog \"[addJava] arg = '$1'\"\n  java_args=( \"${java_args[@]}\" \"$1\" )\n}\n\nenableProfile () {\n  dlog \"[enableProfile] arg = '$1'\"\n  maven_profiles=( \"${maven_profiles[@]}\" \"$1\" )\n  export SBT_MAVEN_PROFILES=\"${maven_profiles[@]}\"\n}\n\naddSbt () {\n  dlog \"[addSbt] arg = '$1'\"\n  sbt_commands=( \"${sbt_commands[@]}\" \"$1\" )\n}\naddResidual () {\n  dlog \"[residual] arg = '$1'\"\n  residual_args=( \"${residual_args[@]}\" \"$1\" )\n}\naddDebugger () {\n  addJava \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1\"\n}\n\n# a ham-fisted attempt to move some memory settings in concert\n# so they need not be dicked around with individually.\nget_mem_opts () {\n  local mem=${1:-1000}\n  local perm=$(( $mem / 4 ))\n  (( $perm > 256 )) || perm=256\n  (( $perm < 4096 )) || perm=4096\n  local codecache=$(( $perm / 2 ))\n\n  echo \"-Xms${mem}m -Xmx${mem}m -XX:ReservedCodeCacheSize=${codecache}m\"\n}\n\nrequire_arg () {\n  local type=\"$1\"\n  local opt=\"$2\"\n  local arg=\"$3\"\n  if [[ -z \"$arg\" ]] || [[ \"${arg:0:1}\" == \"-\" ]]; then\n    echo \"$opt requires <$type> argument\" 1>&2\n    exit 1\n  fi\n}\n\nis_function_defined() {\n  declare -f \"$1\" > /dev/null\n}\n\nprocess_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n       -h|-help) usage; exit 1 ;;\n    -v|-verbose) verbose=1 && shift ;;\n      -d|-debug) debug=1 && shift ;;\n\n           -ivy) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.ivy.home=$2\" && shift 2 ;;\n           -mem) require_arg integer \"$1\" \"$2\" && sbt_mem=\"$2\" && shift 2 ;;\n     -jvm-debug) require_arg port \"$1\" \"$2\" && addDebugger $2 && shift 2 ;;\n         -batch) exec </dev/null && shift ;;\n\n       -sbt-jar) require_arg path \"$1\" \"$2\" && sbt_jar=\"$2\" && shift 2 ;;\n   -sbt-version) require_arg version \"$1\" \"$2\" && sbt_version=\"$2\" && shift 2 ;;\n     -java-home) require_arg path \"$1\" \"$2\" && java_cmd=\"$2/bin/java\" && export JAVA_HOME=$2 && shift 2 ;;\n\n            -D*) addJava \"$1\" && shift ;;\n            -J*) addJava \"${1:2}\" && shift ;;\n            -P*) enableProfile \"$1\" && shift ;;\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  is_function_defined process_my_args && {\n    myargs=(\"${residual_args[@]}\")\n    residual_args=()\n    process_my_args \"${myargs[@]}\"\n  }\n}\n\nrun() {\n  # no jar? download it.\n  [[ -f \"$sbt_jar\" ]] || acquire_sbt_jar \"$sbt_version\" || {\n    # still no jar? uh-oh.\n    echo \"Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar\"\n    exit 1\n  }\n\n  # process the combined args, then reset \"$@\" to the residuals\n  process_args \"$@\"\n  set -- \"${residual_args[@]}\"\n  argumentCount=$#\n\n  # run sbt\n  execRunner \"$java_cmd\" \\\n    ${SBT_OPTS:-$default_sbt_opts} \\\n    $(get_mem_opts $sbt_mem) \\\n    ${java_opts} \\\n    ${java_args[@]} \\\n    -jar \"$sbt_jar\" \\\n    \"${sbt_commands[@]}\" \\\n    \"${residual_args[@]}\"\n}\n"
  },
  {
    "path": "benchmarks/build.sbt",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nname := \"benchmarks\"\nscalaVersion := \"2.12.18\"\n\nlazy val root = (project in file(\".\"))\n  .settings(\n    name := \"benchmarks\",\n    libraryDependencies += \"org.apache.spark\" %% \"spark-sql\" % \"3.5.3\" % \"provided\",\n    libraryDependencies += \"com.github.scopt\" %% \"scopt\" % \"4.0.1\",\n    libraryDependencies += \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % \"2.13.1\",\n\n    assemblyMergeStrategy in assembly := {\n      case PathList(\"META-INF\", xs @ _*) => MergeStrategy.discard\n      case x => MergeStrategy.first\n    }\n  )\n  \n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"4.15.1\"\n  constraints = \"~> 4.15.1\"\n  hashes = [\n    \"h1:KPu3MdNXCScye05Sp4JlE9WwhS9k3yD9KRoRDHg5sDE=\",\n    \"zh:1d944144f8d613b8090c0c8391e4b205ca036086d70aceb4cdf664856fa8410c\",\n    \"zh:2a0ca16a6b12c0ac509f64512f80bd2ed6e7ea0ec369212efd4be3fa65e9773d\",\n    \"zh:3f9efdce4f1c320ffd061e8715e1d031deac1be0b959eaa60c25a274925653e4\",\n    \"zh:4cf82f3267b0c3e08be29b0345f711ab84ea1ea75f0e8ce81f5a2fe635ba67b4\",\n    \"zh:58474a0b7da438e1bcd53e87f10e28830836ff9b46cce5f09413c90952ae4f78\",\n    \"zh:6eb1be8afb0314b6b8424fe212b13beeb04f3f24692f0f3ee86c5153c7eb2e63\",\n    \"zh:8022da7d3b050d452ce6c679844e13729bdb4e1b3e75dcf68931af17a06b9277\",\n    \"zh:8e2683d00fff1df43440d6e7c04a2c1eb432c7d5dacff32fe8ce9045bc948fe6\",\n    \"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425\",\n    \"zh:b0c22d9a306e8ac2de57b5291a3d0a7a2c1713e33b7d076005662451afdc4d29\",\n    \"zh:ba6b7d7d91388b636145b133da6b4e32620cdc8046352e2dc8f3f0f81ff5d2e2\",\n    \"zh:d38a816eb60f4419d99303136a3bb61a0d2df3ca8a1dce2ced9b99bf23efa9f7\",\n  ]\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/README.md",
    "content": "# Create infrastructure with Terraform\n\n1. Install [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started).\n2. Create an IAM user which will be used to create benchmarks infrastructure. Ensure that your AWS CLI is configured.\n   You should either have valid credentials in shared credentials file (e.g. `~/.aws/credentials`)\n   ```\n   [default]\n   aws_access_key_id = anaccesskey\n   aws_secret_access_key = asecretkey\n   ```\n   or export keys as environment variables:\n   ```bash\n   export AWS_ACCESS_KEY_ID=\"anaccesskey\"\n   export AWS_SECRET_ACCESS_KEY=\"asecretkey\"\n   ```\n3. Add permissions for the IAM user. You can either assign `AdministratorAccess` AWS managed policy (discouraged)\n   or assign AWS managed policies in a more granular way:\n    * `IAMFullAccess`\n    * `AmazonVPCFullAccess`\n    * `AmazonEMRFullAccessPolicy_v2`\n    * `AmazonElasticMapReduceFullAccess`\n    * `AmazonRDSFullAccess`\n    * `AmazonS3FullAccess`\n    * a custom policy for EC2 key pairs management\n      ```json\n      {\n        \"Version\": \"2012-10-17\",\n        \"Statement\": [\n          {\n            \"Effect\": \"Allow\",\n            \"Action\": [\n              \"ec2:ImportKeyPair\",\n              \"ec2:CreateKeyPair\",\n              \"ec2:DeleteKeyPair\"\n            ],\n            \"Resource\": \"arn:aws:ec2:*:*:key-pair/benchmarks_key_pair\"\n          }\n        ]\n      }\n      ```\n\n4. Create Terraform variable file `benchmarks/infrastructure/aws/terraform/terraform.tfvars` and fill in variable values.\n   ```tf\n   region                 = \"<REGION>\"\n   availability_zone1     = \"<AVAILABILITY_ZONE1>\"\n   availability_zone2     = \"<AVAILABILITY_ZONE2>\"\n   benchmarks_bucket_name = \"<BUCKET_NAME>\"\n   source_bucket_name     = \"<SOURCE_BUCKET_NAME>\"\n   mysql_user             = \"<MYSQL_USER>\"\n   mysql_password         = \"<MYSQL_PASSWORD>\"\n   emr_public_key_path    = \"<EMR_PUBLIC_KEY_PATH>\"\n   user_ip_address        = \"<MY_IP>\"\n   emr_workers            = WORKERS_COUNT\n   tags                   = {\n     key1 = \"value1\"\n     key2 = \"value2\"\n   }\n   ```\n   Please check `variables.tf` to learn more about each parameter.\n\n5. Run:\n   ```bash\n   terraform init\n   terraform validate\n   terraform apply\n   ```\n   As a result, a new VPC, a S3 bucket, a MySQL instance (metastore) and a EMR cluster will be created.\n   The `apply` command returns `master_node_address` that will be used when running benchmarks.\n   ```\n   Apply complete! Resources: 16 added, 0 changed, 0 destroyed.\n   Outputs:\n   master_node_address = \"35.165.163.250\"\n   ```\n\n6. Once the benchmarks are finished, destroy the resources.\n   ```bash\n   terraform destroy\n   ```\n   If the S3 bucket contains any objects, it will not be destroyed automatically.\n   One need to do that manually to avoid any accidental data loss.\n   ```\n   Error: deleting S3 Bucket (my-bucket): BucketNotEmpty: The bucket you tried to delete is not empty \n   status code: 409, request id: Q11TYZ5E0B23QGQ2, host id: WdeFY88km5IBhy+bi2hqXzgjBxjrn1+OPtCstsWDjkwGNCyEhXYjq330DZq1jbfNXojBEejH6Wg=\n   ```\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/main.tf",
    "content": "module \"networking\" {\n  source = \"./modules/networking\"\n\n  availability_zone1 = var.availability_zone1\n  availability_zone2 = var.availability_zone2\n}\n\nmodule \"storage\" {\n  source = \"./modules/storage\"\n\n  benchmarks_bucket_name = var.benchmarks_bucket_name\n}\n\nmodule \"processing\" {\n  source = \"./modules/processing\"\n\n  vpc_id     = module.networking.vpc_id\n  subnet1_id = module.networking.subnet1_id\n  subnet2_id = module.networking.subnet2_id\n\n  availability_zone1     = var.availability_zone1\n  benchmarks_bucket_name = var.benchmarks_bucket_name\n  source_bucket_name     = var.source_bucket_name\n  mysql_user             = var.mysql_user\n  mysql_password         = var.mysql_password\n  emr_public_key_path    = var.emr_public_key_path\n  emr_workers            = var.emr_workers\n  user_ip_address        = var.user_ip_address\n\n  depends_on = [module.networking, module.storage]\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/networking/main.tf",
    "content": "resource \"aws_vpc\" \"this\" {\n  cidr_block = \"10.0.0.0/16\"\n}\n\nresource \"aws_subnet\" \"benchmarks_subnet1\" {\n  vpc_id            = aws_vpc.this.id\n  availability_zone = var.availability_zone1\n  cidr_block        = \"10.0.0.0/17\"\n}\n\n# There are two subnets needed to create an RDS subnet group. In fact this one is unused.\n# If DB subnet group is built using only one AZ, the following error is thrown:\n#     The DB subnet group doesn't meet Availability Zone (AZ) coverage requirement.\n#     Current AZ coverage: us-west-2a. Add subnets to cover at least 2 AZs.\nresource \"aws_subnet\" \"benchmarks_subnet2\" {\n  vpc_id            = aws_vpc.this.id\n  availability_zone = var.availability_zone2\n  cidr_block        = \"10.0.128.0/17\"\n}\n\nresource \"aws_internet_gateway\" \"this\" {\n  vpc_id = aws_vpc.this.id\n}\n\nresource \"aws_default_route_table\" \"public\" {\n  default_route_table_id = aws_vpc.this.default_route_table_id\n\n  route {\n    cidr_block = \"0.0.0.0/0\"\n    gateway_id = aws_internet_gateway.this.id\n  }\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/networking/outputs.tf",
    "content": "output \"vpc_id\" {\n  value = aws_vpc.this.id\n}\n\noutput \"subnet1_id\" {\n  value = aws_subnet.benchmarks_subnet1.id\n}\n\noutput \"subnet2_id\" {\n  value = aws_subnet.benchmarks_subnet2.id\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/networking/variables.tf",
    "content": "variable \"availability_zone1\" {\n  type = string\n}\n\nvariable \"availability_zone2\" {\n  type = string\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/processing/main.tf",
    "content": "resource \"aws_db_instance\" \"metastore_service\" {\n  engine                 = \"mysql\"\n  engine_version         = \"8.0.28\"\n  instance_class         = \"db.m5.large\"\n  db_name                = \"hive\"\n  username               = var.mysql_user\n  password               = var.mysql_password\n  availability_zone      = var.availability_zone1\n  skip_final_snapshot    = true\n  allocated_storage      = 50\n  db_subnet_group_name   = aws_db_subnet_group.metastore_service.name\n  vpc_security_group_ids = [aws_security_group.metastore_service.id]\n}\n\nresource \"aws_db_subnet_group\" \"metastore_service\" {\n  name       = \"benchmarks_subnet_group_for_metastore_service\"\n  subnet_ids = [var.subnet1_id, var.subnet2_id]\n}\n\n/* EC2 key used to SSH to EMR cluster nodes. */\nresource \"aws_key_pair\" \"benchmarks\" {\n  key_name   = \"benchmarks_key_pair\"\n  public_key = file(var.emr_public_key_path)\n}\n\nresource \"aws_emr_cluster\" \"benchmarks\" {\n  name                              = \"delta_performance_benchmarks_cluster\"\n  release_label                     = \"emr-6.5.0\"\n  applications                      = [\"Spark\", \"Hive\"]\n  termination_protection            = false\n  keep_job_flow_alive_when_no_steps = true\n  ec2_attributes {\n    instance_profile                  = aws_iam_instance_profile.benchmarks_emr_profile.arn\n    key_name                          = aws_key_pair.benchmarks.key_name\n    subnet_id                         = var.subnet1_id\n    emr_managed_master_security_group = aws_security_group.emr.id\n    emr_managed_slave_security_group  = aws_security_group.emr.id\n  }\n  master_instance_group {\n    instance_type = \"i3.2xlarge\"\n  }\n  core_instance_group {\n    instance_type  = \"i3.2xlarge\"\n    instance_count = var.emr_workers\n  }\n\n  configurations_json = <<EOF\n  [\n    {\n      \"Classification\": \"hive-site\",\n      \"Properties\": {\n        \"javax.jdo.option.ConnectionURL\": \"jdbc:mysql://${aws_db_instance.metastore_service.endpoint}/hive?createDatabaseIfNotExist=true\",\n        \"javax.jdo.option.ConnectionDriverName\": \"org.mariadb.jdbc.Driver\",\n        \"javax.jdo.option.ConnectionUserName\": \"${var.mysql_user}\",\n        \"javax.jdo.option.ConnectionPassword\": \"${var.mysql_password}\"\n      }\n    }\n  ]\nEOF\n  service_role        = aws_iam_role.benchmarks_iam_emr_service_role.arn\n  depends_on          = [aws_db_instance.metastore_service]\n}\n\nresource \"aws_security_group\" \"metastore_service\" {\n  name   = \"benchmarks_metastore_security_group\"\n  vpc_id = var.vpc_id\n  ingress {\n    description     = \"Allow inbound traffic only from EMR cluster nodes.\"\n    from_port       = 3306\n    to_port         = 3306\n    protocol        = \"TCP\"\n    security_groups = [aws_security_group.emr.id]\n  }\n  egress {\n    description = \"Allow all outbound traffic.\"\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\nresource \"aws_security_group\" \"emr\" {\n  name   = \"benchmarks_master_security_group\"\n  vpc_id = var.vpc_id\n  ingress {\n    description = \"Allow inbound traffic from given IP.\"\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"${var.user_ip_address}/32\"]\n  }\n  egress {\n    description      = \"Allow all outbound traffic.\"\n    from_port        = 0\n    to_port          = 0\n    protocol         = \"-1\"\n    cidr_blocks      = [\"0.0.0.0/0\"]\n    ipv6_cidr_blocks = [\"::/0\"]\n  }\n  # Amazon EMR will automatically add rules enabling traffic between all nodes.\n}\n\n# According to: https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-iam-roles-custom.html\n#   To customize permissions, we recommend that you create new roles and policies. Begin with the permissions in\n#   the managed policies for the default roles. Then, copy and paste the contents to new policy statements, modify\n#   the permissions as appropriate, and attach the modified permissions policies to the roles that you create.\nresource \"aws_iam_role\" \"benchmarks_iam_emr_service_role\" {\n  name               = \"benchmarks_iam_emr_service_role\"\n  assume_role_policy = <<EOF\n{\n  \"Version\": \"2008-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"\",\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"elasticmapreduce.amazonaws.com\"\n      },\n      \"Action\": \"sts:AssumeRole\"\n    }\n  ]\n}\nEOF\n}\n\nresource \"aws_iam_role_policy\" \"benchmarks_iam_emr_service_policy\" {\n  name   = \"benchmarks_iam_emr_service_policy\"\n  role   = aws_iam_role.benchmarks_iam_emr_service_role.id\n  policy = <<EOF\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": [\n                \"arn:aws:s3:::${var.benchmarks_bucket_name}\",\n                \"arn:aws:s3:::${var.source_bucket_name}\"\n            ],\n            \"Action\": [\n                \"s3:ListBucket\"\n            ]\n        },\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": \"arn:aws:s3:::${var.benchmarks_bucket_name}/*\",\n            \"Action\": [\n                \"s3:GetObject\",\n                \"s3:PutObject\"\n            ]\n        },\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": \"arn:aws:s3:::${var.source_bucket_name}/*\",\n            \"Action\": [\n                \"s3:GetObject\"\n            ]\n        },\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": \"*\",\n            \"Action\": [\n                \"ec2:AuthorizeSecurityGroupEgress\",\n                \"ec2:AuthorizeSecurityGroupIngress\",\n                \"ec2:CancelSpotInstanceRequests\",\n                \"ec2:CreateNetworkInterface\",\n                \"ec2:CreateSecurityGroup\",\n                \"ec2:CreateTags\",\n                \"ec2:DeleteNetworkInterface\",\n                \"ec2:DeleteSecurityGroup\",\n                \"ec2:DeleteTags\",\n                \"ec2:DescribeAvailabilityZones\",\n                \"ec2:DescribeAccountAttributes\",\n                \"ec2:DescribeDhcpOptions\",\n                \"ec2:DescribeInstanceStatus\",\n                \"ec2:DescribeInstances\",\n                \"ec2:DescribeKeyPairs\",\n                \"ec2:DescribeNetworkAcls\",\n                \"ec2:DescribeNetworkInterfaces\",\n                \"ec2:DescribePrefixLists\",\n                \"ec2:DescribeRouteTables\",\n                \"ec2:DescribeSecurityGroups\",\n                \"ec2:DescribeSpotInstanceRequests\",\n                \"ec2:DescribeSpotPriceHistory\",\n                \"ec2:DescribeSubnets\",\n                \"ec2:DescribeVpcAttribute\",\n                \"ec2:DescribeVpcEndpoints\",\n                \"ec2:DescribeVpcEndpointServices\",\n                \"ec2:DescribeVpcs\",\n                \"ec2:DetachNetworkInterface\",\n                \"ec2:ModifyImageAttribute\",\n                \"ec2:ModifyInstanceAttribute\",\n                \"ec2:RequestSpotInstances\",\n                \"ec2:RevokeSecurityGroupEgress\",\n                \"ec2:RunInstances\",\n                \"ec2:TerminateInstances\",\n                \"ec2:DeleteVolume\",\n                \"ec2:DescribeVolumeStatus\",\n                \"ec2:DescribeVolumes\",\n                \"ec2:DetachVolume\",\n                \"iam:GetRole\",\n                \"iam:GetRolePolicy\",\n                \"iam:ListInstanceProfiles\",\n                \"iam:ListRolePolicies\",\n                \"iam:PassRole\"\n            ]\n        }\n    ]\n}\nEOF\n}\n\nresource \"aws_iam_role\" \"benchmarks_iam_emr_profile_role\" {\n  name               = \"benchmarks_iam_emr_profile_role\"\n  assume_role_policy = <<EOF\n{\n  \"Version\": \"2008-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"\",\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"ec2.amazonaws.com\"\n      },\n      \"Action\": \"sts:AssumeRole\"\n    }\n  ]\n}\nEOF\n}\n\nresource \"aws_iam_instance_profile\" \"benchmarks_emr_profile\" {\n  name = \"benchmarks_emr_profile\"\n  role = aws_iam_role.benchmarks_iam_emr_profile_role.name\n}\n\nresource \"aws_iam_role_policy\" \"benchmarks_iam_emr_profile_policy\" {\n  name   = \"benchmarks_iam_emr_profile_policy\"\n  role   = aws_iam_role.benchmarks_iam_emr_profile_role.id\n  policy = <<EOF\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": [\n                \"arn:aws:s3:::${var.benchmarks_bucket_name}\",\n                \"arn:aws:s3:::${var.source_bucket_name}\"\n            ],\n            \"Action\": [\n                \"s3:ListBucket\"\n            ]\n        },\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": \"arn:aws:s3:::${var.benchmarks_bucket_name}/*\",\n            \"Action\": [\n                \"s3:GetObject\",\n                \"s3:PutObject\"\n            ]\n        },\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": \"arn:aws:s3:::${var.source_bucket_name}/*\",\n            \"Action\": [\n                \"s3:GetObject\"\n            ]\n        },\n        {\n            \"Effect\": \"Allow\",\n            \"Resource\": \"*\",\n            \"Action\": [\n                \"ec2:Describe*\",\n                \"elasticmapreduce:Describe*\",\n                \"elasticmapreduce:ListBootstrapActions\",\n                \"elasticmapreduce:ListClusters\",\n                \"elasticmapreduce:ListInstanceGroups\",\n                \"elasticmapreduce:ListInstances\",\n                \"elasticmapreduce:ListSteps\"\n            ]\n        }\n    ]\n}\nEOF\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/processing/outputs.tf",
    "content": "output \"master_node_address\" {\n  value = aws_emr_cluster.benchmarks.master_public_dns\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/processing/variables.tf",
    "content": "variable \"availability_zone1\" {\n  type = string\n}\n\nvariable \"vpc_id\" {\n  type = string\n}\n\nvariable \"subnet1_id\" {\n  type = string\n}\n\nvariable \"subnet2_id\" {\n  type = string\n}\n\nvariable \"benchmarks_bucket_name\" {\n  type = string\n}\n\nvariable \"source_bucket_name\" {\n  type = string\n}\n\nvariable \"mysql_user\" {\n  type = string\n}\n\nvariable \"mysql_password\" {\n  type = string\n}\n\nvariable \"emr_public_key_path\" {\n  type = string\n}\n\nvariable \"emr_workers\" {\n  type = number\n}\n\nvariable \"user_ip_address\" {\n  type = string\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/storage/main.tf",
    "content": "resource \"aws_s3_bucket\" \"benchmarks_data\" {\n  bucket = var.benchmarks_bucket_name\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/modules/storage/variables.tf",
    "content": "variable \"benchmarks_bucket_name\" {\n  type = string\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/outputs.tf",
    "content": "output \"master_node_address\" {\n  value = module.processing.master_node_address\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/providers.tf",
    "content": "provider \"aws\" {\n  region = var.region\n  default_tags {\n    tags = var.tags\n  }\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/variables.tf",
    "content": "variable \"region\" {\n  description = \"The default region to manage resources in.\"\n  type        = string\n  default     = \"us-west-2\"\n}\n\nvariable \"availability_zone1\" {\n  description = \"The default availability zone to manage resources in.\"\n  type        = string\n  default     = \"us-west-2a\"\n}\n\nvariable \"availability_zone2\" {\n  description = \"The secondary availability zone.\"\n  type        = string\n  default     = \"us-west-2b\"\n}\n\nvariable \"benchmarks_bucket_name\" {\n  description = \"The name of the AWS S3 bucket that will be used to store benchmark data.\"\n  type        = string\n}\n\nvariable \"source_bucket_name\" {\n  description = \"The S3 bucket name where the raw input data is stored.\"\n  type        = string\n  default     = \"devrel-delta-datasets\"\n}\n\nvariable \"mysql_user\" {\n  description = \"MySQL database user.\"\n  type        = string\n  default     = \"benchmark\"\n}\n\nvariable \"mysql_password\" {\n  description = \"MySQL database password.\"\n  type        = string\n  default     = \"benchmark\"\n}\n\nvariable \"emr_public_key_path\" {\n  description = \"The path to the public key in the typical format, specified in RFC4716. The key is necessary to SSH to EMR cluster nodes.\"\n  type        = string\n  default     = \"~/.ssh/id_rsa.pub\"\n}\n\nvariable \"emr_workers\" {\n  description = \"The number of worker nodes in EMR cluster.\"\n  type        = number\n  default     = 16\n}\n\nvariable \"user_ip_address\" {\n  description = \"The IP of the machine which is used to access master node.\"\n  type        = string\n}\n\nvariable \"tags\" {\n  description = \"Common tags assigned to each resource.\"\n  type        = map(string)\n  default     = {}\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/aws/terraform/versions.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 4.15.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/hashicorp/google\" {\n  version     = \"4.22.0\"\n  constraints = \"4.22.0\"\n  hashes = [\n    \"h1:1HjdT6HX8mPJjdr80lESKArCVkQzTTnvmgDSlZiuRBc=\",\n    \"zh:0093d36809054c0ce63fcac0d955511b4afd1acdcbcc7af009cc60650cfc4291\",\n    \"zh:343ceca3442a09cca0690f43c5578af39f432691077513cb39eddae270ad3a0d\",\n    \"zh:35ca4ba3c048d442bfa72c8df626c4f3eb9102a821b8e0ef4963cbecada78ae6\",\n    \"zh:573307f69250fa6f1b20a401c3d2c626553a07843ba36bb0be60bc64221ea765\",\n    \"zh:7a5af64663c23848500a4b83c7ac2bfbb460a5efd302e9cd2460a0be4122e2c4\",\n    \"zh:989c5a3103d705c1611b7985b8a077518530b707684a47fd9ea9dad390c329c8\",\n    \"zh:a583b2f762f28181dde1483fb590a666a69db0b0d82faaf8eceb5f5991fac803\",\n    \"zh:c230e2f3fb2aec5878800c946be4b571662d0cb78838e6ad32e99e1e75967639\",\n    \"zh:d1d6cf87698de22202ae7d1652c7388ce4142088105cb18b7ed5307da1ff6ef7\",\n    \"zh:db99274fa832bcb9d72f7a5e1138c78dd19851a1c3f2574ccdfef6b6f6af8006\",\n    \"zh:eb6047a6f7402c8e28db4529fcd43b2a94252bc0fc386eb3eff2a3461c0b2375\",\n    \"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/google-beta\" {\n  version     = \"4.22.0\"\n  constraints = \"4.22.0\"\n  hashes = [\n    \"h1:j0D6Hpugxq6UgFspoSz0yKf9CAwfDVDMtYZ8iRbrWGo=\",\n    \"zh:142449e2314d5d6a34d737e749d7d573cb4808f11f8b480c4f3a7a92fd43bdcf\",\n    \"zh:181b7f547173e7e3a7fb19005ca86e3b617af79bbfb693afce1f32efc482b2c5\",\n    \"zh:2f27fd47e554e1765a1b01dc47a79c8d8bb19ee0dfb5496f71eacbcea407930f\",\n    \"zh:51f42ee289b742de0b32048c97bee13189901c0edf2818d7aea7b745366ee54f\",\n    \"zh:5e40390e56fad14d6899cfd3bc20308bc2594f6eeb4d508078de49c2e407cb41\",\n    \"zh:7b7bfb6631577e06c0e3d8a7019f51b0064822044a4739644ec7ffe7e72b2e3b\",\n    \"zh:88dfa69b5cf1c6f75ab0df4d4141373cd1c9ad9b1d854c805515c13da2415c83\",\n    \"zh:95ea1b926114c014c7997ad2406a317ad237baed8f0f607e3254f516349fcbc8\",\n    \"zh:a8e17c0d134bb39f13fde8c38488bbb004fec4476be93f7b574780ce2c927296\",\n    \"zh:b24cec92170f006a795a7a42340846db409effd018e22096a907d07223866a73\",\n    \"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c\",\n    \"zh:fad38be843066e91456e810d0665fa0e78403d2e22bc8899c08ff076061cecbb\",\n  ]\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/README.md",
    "content": "# Create infrastructure with Terraform\n\n1. Install [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/gcp-get-started).\n\n2. Create and download [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-console).\n   Please note that the key file should be in JSON format.\n   The path to the file is required in the next step (`credentials_file`).\n\n3. Make sure the service account has at least the following permissions:\n    * `Dataproc Editor`,\n    * `Dataproc Metastore Editor`,\n    * `Dataproc Service Agent`,\n    * `Service Account User`,\n    * `Storage Object Admin`.\n\n4. Create Terraform variable file `benchmarks/infrastructure/gcp/terraform/terraform.tfvars` and fill in variable values.\n   ```tf\n   project          = \"<PROJECT_ID>\"\n   credentials_file = \"<CREDENTIALS_FILE>\"\n   public_key_path  = \"<PUBLIC_KEY_PATH>\"\n   region           = \"<REGION>\"\n   zone             = \"<ZONE>\"\n   bucket_name      = \"<BUCKET_NAME>\"\n   dataproc_workers = WORKERS_COUNT\n   labels           = {\n     key1 = \"value1\"\n     key2 = \"value2\"\n   }\n   ```\n   Please check `variables.tf` to learn more about each parameter.\n\n5. Run:\n   ```bash\n   terraform init\n   terraform validate\n   terraform apply\n   ```\n   As a result, a new Google Storage bucket, a Dataproc Metastore and a Dataproc cluster will be created.\n   The `apply` command returns `master_node_address` that will be used when running benchmarks.\n   ```\n   Apply complete! Resources: 4 added, 0 changed, 0 destroyed.\n   Outputs:\n   master_node_address = \"35.165.163.250\"\n   ```\n\n6. Once the benchmarks are finished, destroy the resources.\n   ```bash\n   terraform destroy\n   ```\n   If the Google Storage bucket contains any objects, it will not be destroyed automatically. One need to do that\n   manually to avoid any accidental data loss.\n   ```\n   Error: Error trying to delete bucket my_bucket containing objects without `force_destroy` set to true\n   ```\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/main.tf",
    "content": "module \"processing\" {\n  source = \"./modules/processing\"\n\n  project          = var.project\n  credentials_file = var.credentials_file\n  public_key_path  = var.public_key_path\n  region           = var.region\n  zone             = var.zone\n  dataproc_workers = var.dataproc_workers\n  labels           = var.labels\n}\n\nmodule \"storage\" {\n  source = \"./modules/storage\"\n\n  bucket_name = var.bucket_name\n  region      = var.region\n  labels      = var.labels\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/modules/processing/data.tf",
    "content": "data \"google_client_openid_userinfo\" \"me\" {\n}\n\ndata \"google_compute_instance\" \"benchmarks_master\" {\n  provider   = google-beta\n  depends_on = [google_dataproc_cluster.benchmarks]\n  name       = google_dataproc_cluster.benchmarks.cluster_config.0.master_config.0.instance_names.0\n  zone       = var.zone\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/modules/processing/main.tf",
    "content": "resource \"google_os_login_ssh_public_key\" \"key_to_login_to_master_node\" {\n  user = data.google_client_openid_userinfo.me.email\n  key  = file(var.public_key_path)\n}\n\nresource \"google_dataproc_metastore_service\" \"this\" {\n  provider   = google-beta\n  service_id = \"dataproc-metastore-for-benchmarks\"\n  location   = var.region\n  tier       = \"ENTERPRISE\"\n  hive_metastore_config {\n    version = \"3.1.2\"\n  }\n  labels = var.labels\n}\n\nresource \"google_dataproc_cluster\" \"benchmarks\" {\n  provider = google-beta\n  name     = \"delta-performance-benchmarks-cluster\"\n  region   = var.region\n\n  cluster_config {\n    master_config {\n      num_instances = 1\n      machine_type  = \"n2-highmem-8\"\n      disk_config {\n        boot_disk_type    = \"pd-ssd\"\n        boot_disk_size_gb = 100\n        num_local_ssds    = 2\n      }\n    }\n    worker_config {\n      num_instances = var.dataproc_workers\n      machine_type  = \"n2-highmem-8\"\n      disk_config {\n        boot_disk_type    = \"pd-ssd\"\n        boot_disk_size_gb = 100\n        num_local_ssds    = 4\n      }\n    }\n    software_config {\n      image_version = \"2.0-debian10\"\n    }\n    metastore_config {\n      dataproc_metastore_service = google_dataproc_metastore_service.this.id\n    }\n    gce_cluster_config {\n      zone = var.zone\n    }\n    endpoint_config {\n      enable_http_port_access = \"true\"\n    }\n  }\n  labels     = var.labels\n  depends_on = [\n    google_dataproc_metastore_service.this\n  ]\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/modules/processing/outputs.tf",
    "content": "output \"master_node_address\" {\n  value = data.google_compute_instance.benchmarks_master.network_interface.0.access_config.0.nat_ip\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/modules/processing/variables.tf",
    "content": "variable \"project\" {\n  type = string\n}\n\nvariable \"credentials_file\" {\n  type = string\n}\n\nvariable \"public_key_path\" {\n  type = string\n}\n\nvariable \"region\" {\n  type = string\n}\n\nvariable \"zone\" {\n  type = string\n}\n\nvariable \"dataproc_workers\" {\n  type = number\n}\n\nvariable \"labels\" {\n  type = map(string)\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/modules/storage/main.tf",
    "content": "resource \"google_storage_bucket\" \"benchmarks_data\" {\n  provider      = google\n  name          = var.bucket_name\n  location      = var.region\n  storage_class = \"STANDARD\"\n  labels        = var.labels\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/modules/storage/variables.tf",
    "content": "variable \"bucket_name\" {\n  type = string\n}\n\nvariable \"region\" {\n  type = string\n}\n\nvariable \"labels\" {\n  type = map(string)\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/outputs.tf",
    "content": "output \"master_node_address\" {\n  value = module.processing.master_node_address\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/providers.tf",
    "content": "provider \"google\" {\n  credentials = file(var.credentials_file)\n  project     = var.project\n  region      = var.region\n  zone        = var.zone\n}\n\nprovider \"google-beta\" {\n  credentials = file(var.credentials_file)\n  project     = var.project\n  region      = var.region\n  zone        = var.zone\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/variables.tf",
    "content": "variable \"project\" {\n  description = \"The ID of the GCP project.\"\n  type        = string\n}\n\nvariable \"credentials_file\" {\n  description = \"The path to a service account key file in JSON format.\"\n  type        = string\n}\n\nvariable \"public_key_path\" {\n  description = \"The path to the public key in the typical format, specified in RFC4716. The key is necessary to SSH to Dataproc cluster nodes.\"\n  type        = string\n  default     = \"~/.ssh/id_rsa.pub\"\n}\n\nvariable \"region\" {\n  description = \"The default region to manage resources in.\"\n  type        = string\n  default     = \"us-central1\"\n}\n\nvariable \"zone\" {\n  description = \"The default zone to manage resources in.\"\n  type        = string\n  default     = \"us-central1-a\"\n}\n\nvariable \"bucket_name\" {\n  description = \"The name of the Google Storage bucket that will be used to store benchmark data. Please note that the bucket name has to be globally unique.\"\n  type        = string\n}\n\nvariable \"dataproc_workers\" {\n  description = \"The number of worker nodes in Dataproc cluster.\"\n  type        = number\n  default     = 16\n}\n\nvariable \"labels\" {\n  description = \"Labels that will be assigned to each resource.\"\n  type        = map(string)\n  default     = {}\n}\n"
  },
  {
    "path": "benchmarks/infrastructure/gcp/terraform/versions.tf",
    "content": "terraform {\n  required_providers {\n    google = {\n      source  = \"hashicorp/google\"\n      version = \"4.22.0\"\n    }\n    google-beta = {\n      source  = \"hashicorp/google-beta\"\n      version = \"4.22.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/project/build.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\nsbt.version=1.9.9\n"
  },
  {
    "path": "benchmarks/project/plugins.sbt",
    "content": "addSbtPlugin(\"com.eed3si9n\" % \"sbt-assembly\" % \"0.15.0\")\n"
  },
  {
    "path": "benchmarks/run-benchmark.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport argparse\nfrom scripts.benchmarks import *\n\ndelta_version = \"2.3.0\"\n\n# Benchmark name to their specifications. See the imported benchmarks.py for details of benchmark.\n\nbenchmarks = {\n    \"test\":\n        DeltaBenchmarkSpec(\n            delta_version=delta_version,\n            benchmark_main_class=\"benchmark.TestBenchmark\",\n            main_class_args=[\"--test-param\", \"value\"],\n        ),\n\n    # TPC-DS data load\n    \"tpcds-1gb-delta-load\": DeltaTPCDSDataLoadSpec(delta_version=delta_version, scale_in_gb=1),\n    \"tpcds-3tb-delta-load\": DeltaTPCDSDataLoadSpec(delta_version=delta_version, scale_in_gb=3000),\n    \"tpcds-1gb-parquet-load\": ParquetTPCDSDataLoadSpec(scale_in_gb=1),\n    \"tpcds-3tb-parquet-load\": ParquetTPCDSDataLoadSpec(scale_in_gb=3000),\n\n    # TPC-DS benchmark\n    \"tpcds-1gb-delta\": DeltaTPCDSBenchmarkSpec(delta_version=delta_version, scale_in_gb=1),\n    \"tpcds-3tb-delta\": DeltaTPCDSBenchmarkSpec(delta_version=delta_version, scale_in_gb=3000),\n    \"tpcds-1gb-parquet\": ParquetTPCDSBenchmarkSpec(scale_in_gb=1),\n    \"tpcds-3tb-parquet\": ParquetTPCDSBenchmarkSpec(scale_in_gb=3000),\n\n    # Merge data load.\n    \"merge-1gb-delta-load\": DeltaMergeDataLoadSpec(delta_version=delta_version, scale_in_gb=1),\n    \"merge-3tb-delta-load\": DeltaMergeDataLoadSpec(delta_version=delta_version, scale_in_gb=3000),\n\n    # Merge benchmark.\n    \"merge-1gb-delta\": DeltaMergeBenchmarkSpec(delta_version=delta_version, scale_in_gb=1),\n    \"merge-3tb-delta\": DeltaMergeBenchmarkSpec(delta_version=delta_version, scale_in_gb=3000),\n\n}\n\ndelta_log_store_classes = {\n    \"aws\": \"spark.delta.logStore.class=org.apache.spark.sql.delta.storage.S3SingleDriverLogStore\",\n    \"gcp\": \"spark.delta.logStore.gs.impl=io.delta.storage.GCSLogStore\",\n}\n\nif __name__ == \"__main__\":\n    \"\"\"\n    Run benchmark on a cluster using ssh.\n\n    Example usage:\n\n    ./run-benchmark.py --cluster-hostname <hostname> -i <pem file> --ssh-user <ssh user> --cloud-provider <cloud provider> --benchmark test\n\n    \"\"\"\n\n\ndef parse_args():\n    # Parse cmd line arguments\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--benchmark\", \"-b\",\n        required=True,\n        help=\"Run the given benchmark. See this \" +\n             \"python file for the list of predefined benchmark names and definitions.\")\n    parser.add_argument(\n        \"--cluster-hostname\",\n        required=True,\n        help=\"Hostname or public IP of the cluster driver\")\n    parser.add_argument(\n        \"--ssh-id-file\", \"-i\",\n        required=True,\n        help=\"SSH identity file\")\n    parser.add_argument(\n        \"--spark-conf\",\n        action=\"append\",\n        help=\"Run benchmark with given spark conf. Use separate --spark-conf for multiple confs.\")\n    parser.add_argument(\n        \"--resume-benchmark\",\n        help=\"Resume waiting for the given running benchmark.\")\n    parser.add_argument(\n        \"--use-local-delta-dir\",\n        help=\"Local path to delta repository which will be used for running the benchmark \" +\n             \"instead of the version specified in the specification. Make sure that new delta\" +\n             \" version is compatible with version in the spec.\")\n    parser.add_argument(\n        \"--cloud-provider\",\n        choices=delta_log_store_classes.keys(),\n        help=\"Cloud where the benchmark will be executed.\")\n    parser.add_argument(\n        \"--ssh-user\",\n        default=\"hadoop\",\n        help=\"The user which is used to communicate with the master via SSH.\")\n\n    parsed_args, parsed_passthru_args = parser.parse_known_args()\n    return parsed_args, parsed_passthru_args\n\n\ndef run_single_benchmark(benchmark_name, benchmark_spec, other_args):\n    benchmark_spec.append_spark_confs(other_args.spark_conf)\n    benchmark_spec.append_spark_conf(delta_log_store_classes.get(other_args.cloud_provider))\n    benchmark_spec.append_main_class_args(passthru_args)\n    print(\"------\")\n    print(\"Benchmark spec to run:\\n\" + str(vars(benchmark_spec)))\n    print(\"------\")\n\n    benchmark = Benchmark(benchmark_name, benchmark_spec,\n                          use_spark_shell=True, local_delta_dir=other_args.use_local_delta_dir)\n    benchmark_dir = os.path.dirname(os.path.abspath(__file__))\n    with WorkingDirectory(benchmark_dir):\n        benchmark.run(other_args.cluster_hostname, other_args.ssh_id_file, other_args.ssh_user)\n\n\nif __name__ == \"__main__\":\n    \"\"\"\n    Run benchmark on a cluster using ssh.\n\n    Example usage:\n\n    ./run-benchmark.py --cluster-hostname <hostname> -i <pem file> --ssh-user <ssh user> --cloud-provider <cloud provider> --benchmark test\n\n    \"\"\"\n    args, passthru_args = parse_args()\n\n    if args.resume_benchmark is not None:\n        Benchmark.wait_for_completion(\n            args.cluster_hostname, args.ssh_id_file, args.resume_benchmark, args.ssh_user)\n        exit(0)\n\n    benchmark_names = args.benchmark.split(\",\")\n    for benchmark_name in benchmark_names:\n        # Create and run the benchmark\n        if benchmark_name in benchmarks:\n            run_single_benchmark(benchmark_name, benchmarks[benchmark_name], args)\n        else:\n            raise Exception(\"Could not find benchmark spec for '\" + benchmark_name + \"'.\" +\n                            \"Must provide one of the predefined benchmark names:\\n\" +\n                            \"\\n\".join(benchmarks.keys()) +\n                            \"\\nSee this python file for more details.\")\n"
  },
  {
    "path": "benchmarks/scripts/benchmarks.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom scripts.utils import *\nfrom datetime import datetime\nimport time\n\n\nclass BenchmarkSpec:\n    \"\"\"\n    Specifications of a benchmark.\n\n    :param format_name: Spark format name\n    :param maven_artifacts: Maven artifact name in x:y:z format\n    :param spark_confs: list of spark conf strings in key=value format\n    :param benchmark_main_class: Name of main Scala class from the JAR to run\n    :param main_class_args: command line args for the main class\n    \"\"\"\n    def __init__(\n            self, format_name, maven_artifacts, spark_confs,\n            benchmark_main_class, main_class_args, extra_spark_shell_args=None, **kwargs):\n        if main_class_args is None:\n            main_class_args = []\n        if extra_spark_shell_args is None:\n            extra_spark_shell_args = []\n        self.format_name = format_name\n        self.maven_artifacts = maven_artifacts\n        self.spark_confs = spark_confs\n        self.benchmark_main_class = benchmark_main_class\n        self.benchmark_main_class_args = main_class_args\n        self.extra_spark_shell_args = extra_spark_shell_args\n\n    def append_spark_conf(self, new_conf):\n        if isinstance(new_conf, str):\n            self.spark_confs.append(new_conf)\n\n    def append_spark_confs(self, new_confs):\n        if new_confs is not None and isinstance(new_confs, list):\n            self.spark_confs.extend(new_confs)\n\n    def append_main_class_args(self, new_args):\n        if new_args is not None and isinstance(new_args, list):\n            self.benchmark_main_class_args.extend(new_args)\n\n    def get_sparksubmit_cmd(self, benchmark_jar_path):\n        spark_conf_str = \"\"\n        for conf in self.spark_confs:\n            print(f\"conf={conf}\")\n            spark_conf_str += f\"\"\"--conf \"{conf}\" \"\"\"\n        main_class_args = ' '.join(self.benchmark_main_class_args)\n        spark_shell_args_str = ' '.join(self.extra_spark_shell_args)\n        spark_submit_cmd = (\n            f\"spark-submit {spark_shell_args_str} \" +\n            (f\"--packages {self.maven_artifacts} \" if self.maven_artifacts else \"\") +\n            f\"{spark_conf_str} --class {self.benchmark_main_class} \" +\n            f\"{benchmark_jar_path} {main_class_args}\"\n        )\n        print(spark_submit_cmd)\n        return spark_submit_cmd\n\n    def get_sparkshell_cmd(self, benchmark_jar_path, benchmark_init_file_path):\n        spark_conf_str = \"\"\n        for conf in self.spark_confs:\n            print(f\"conf={conf}\")\n            spark_conf_str += f\"\"\"--conf \"{conf}\" \"\"\"\n        spark_shell_args_str = ' '.join(self.extra_spark_shell_args)\n        spark_shell_cmd = (\n                f\"spark-shell {spark_shell_args_str} \" +\n                (f\"--packages {self.maven_artifacts} \" if self.maven_artifacts else \"\") +\n                f\"{spark_conf_str} --jars {benchmark_jar_path} -I {benchmark_init_file_path}\"\n        )\n        print(spark_shell_cmd)\n        return spark_shell_cmd\n\n\nclass TPCDSDataLoadSpec(BenchmarkSpec):\n    \"\"\"\n    Specifications of TPC-DS data load process.\n    Always mixin in this first before the base benchmark class.\n    \"\"\"\n    def __init__(self, scale_in_gb, exclude_nulls=True, **kwargs):\n        # forward all keyword args to next constructor\n        super().__init__(benchmark_main_class=\"benchmark.TPCDSDataLoad\", **kwargs)\n        self.benchmark_main_class_args.extend([\n            \"--format\", self.format_name,\n            \"--scale-in-gb\", str(scale_in_gb),\n            \"--exclude-nulls\", str(exclude_nulls),\n        ])\n        # To access the public TPCDS parquet files on S3\n        self.spark_confs.extend([\"spark.hadoop.fs.s3.useRequesterPaysHeader=true\"])\n\n\nclass TPCDSBenchmarkSpec(BenchmarkSpec):\n    \"\"\"\n    Specifications of TPC-DS benchmark.\n    \"\"\"\n    def __init__(self, scale_in_gb, **kwargs):\n        # forward all keyword args to next constructor\n        super().__init__(benchmark_main_class=\"benchmark.TPCDSBenchmark\", **kwargs)\n        # after init of super class, use the format to add main class args\n        self.benchmark_main_class_args.extend([\n            \"--format\", self.format_name,\n            \"--scale-in-gb\", str(scale_in_gb)\n        ])\n\n\nclass MergeDataLoadSpec(BenchmarkSpec):\n    \"\"\"\n    Specifications of Merge data load process.\n    Always mixin in this first before the base benchmark class.\n    \"\"\"\n    def __init__(self, scale_in_gb, exclude_nulls=True, **kwargs):\n        # forward all keyword args to next constructor\n        super().__init__(benchmark_main_class=\"benchmark.MergeDataLoad\", **kwargs)\n        self.benchmark_main_class_args.extend([\n            \"--scale-in-gb\", str(scale_in_gb),\n        ])\n        # To access the public TPCDS parquet files on S3\n        self.spark_confs.extend([\"spark.hadoop.fs.s3.useRequesterPaysHeader=true\"])\n\n\nclass MergeBenchmarkSpec(BenchmarkSpec):\n    \"\"\"\n    Specifications of Merge benchmark.\n    \"\"\"\n    def __init__(self, scale_in_gb, **kwargs):\n        # forward all keyword args to next constructor\n        super().__init__(benchmark_main_class=\"benchmark.MergeBenchmark\", **kwargs)\n        # after init of super class, use the format to add main class args\n        self.benchmark_main_class_args.extend([\n            \"--scale-in-gb\", str(scale_in_gb)\n        ])\n\n\n\n\n# ============== Delta benchmark specifications ==============\n\n\nclass DeltaBenchmarkSpec(BenchmarkSpec):\n    \"\"\"\n    Specification of a benchmark using the Delta format.\n    \"\"\"\n    def __init__(self, delta_version, benchmark_main_class, main_class_args=None, scala_version=\"2.12\", **kwargs):\n        delta_spark_confs = [\n            \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\",\n            \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n        ]\n        self.scala_version = scala_version\n\n        if \"spark_confs\" in kwargs and isinstance(kwargs[\"spark_confs\"], list):\n            kwargs[\"spark_confs\"].extend(delta_spark_confs)\n        else:\n            kwargs[\"spark_confs\"] = delta_spark_confs\n\n        super().__init__(\n            format_name=\"delta\",\n            maven_artifacts=self.delta_maven_artifacts(delta_version, self.scala_version),\n            benchmark_main_class=benchmark_main_class,\n            main_class_args=main_class_args,\n            **kwargs\n        )\n\n    def update_delta_version(self, new_delta_version):\n        self.maven_artifacts = \\\n            DeltaBenchmarkSpec.delta_maven_artifacts(new_delta_version, self.scala_version)\n\n    @staticmethod\n    def delta_maven_artifacts(delta_version, scala_version):\n        return f\"io.delta:delta-core_{scala_version}:{delta_version},io.delta:delta-contribs_{scala_version}:{delta_version},io.delta:delta-hive_{scala_version}:0.2.0\"\n\n\nclass DeltaTPCDSDataLoadSpec(TPCDSDataLoadSpec, DeltaBenchmarkSpec):\n    def __init__(self, delta_version, scale_in_gb=1):\n        super().__init__(delta_version=delta_version, scale_in_gb=scale_in_gb)\n\n\nclass DeltaTPCDSBenchmarkSpec(TPCDSBenchmarkSpec, DeltaBenchmarkSpec):\n    def __init__(self, delta_version, scale_in_gb=1):\n        super().__init__(delta_version=delta_version, scale_in_gb=scale_in_gb)\n\n\nclass DeltaMergeDataLoadSpec(MergeDataLoadSpec, DeltaBenchmarkSpec):\n    def __init__(self, delta_version, scale_in_gb=1):\n        super().__init__(delta_version=delta_version, scale_in_gb=scale_in_gb)\n\n\nclass DeltaMergeBenchmarkSpec(MergeBenchmarkSpec, DeltaBenchmarkSpec):\n    def __init__(self, delta_version, scale_in_gb=1):\n        super().__init__(delta_version=delta_version, scale_in_gb=scale_in_gb)\n\n\n\n# ============== Parquet benchmark specifications ==============\n\n\nclass ParquetBenchmarkSpec(BenchmarkSpec):\n    \"\"\"\n    Specification of a benchmark using the Parquet format.\n    \"\"\"\n    def __init__(self, benchmark_main_class, main_class_args=None, **kwargs):\n        super().__init__(\n            format_name=\"parquet\",\n            maven_artifacts=None,\n            spark_confs=[],\n            benchmark_main_class=benchmark_main_class,\n            main_class_args=main_class_args,\n            **kwargs\n        )\n\nclass ParquetTPCDSDataLoadSpec(TPCDSDataLoadSpec, ParquetBenchmarkSpec):\n    def __init__(self, scale_in_gb=1):\n        super().__init__(scale_in_gb=scale_in_gb)\n\n\nclass ParquetTPCDSBenchmarkSpec(TPCDSBenchmarkSpec, ParquetBenchmarkSpec):\n    def __init__(self, scale_in_gb=1):\n        super().__init__(scale_in_gb=scale_in_gb)\n\n\n# ============== General benchmark execution ==============\n\n\nclass Benchmark:\n    \"\"\"\n    Represents a benchmark that can be run on a remote Spark cluster\n    :param benchmark_name: A name to be used for uniquely identifying this benchmark.\n                           Added to file names generated by this benchmark.\n    :param benchmark_spec: Specification of the benchmark. See BenchmarkSpec.\n    \"\"\"\n    def __init__(self, benchmark_name, benchmark_spec, use_spark_shell, local_delta_dir=None):\n        now = datetime.now()\n        self.benchmark_id = now.strftime(\"%Y%m%d-%H%M%S\") + \"-\" + benchmark_name\n        self.benchmark_spec = benchmark_spec\n\n        # Add benchmark id as a spark conf so that it get transferred automatically to scala code\n        self.benchmark_spec.append_spark_confs([f\"spark.benchmarkId={self.benchmark_id}\"])\n        self.output_file = Benchmark.output_file(self.benchmark_id)\n        self.json_report_file = Benchmark.json_report_file(self.benchmark_id)\n        self.completed_file = Benchmark.completed_file(self.benchmark_id)\n        self.use_spark_shell = use_spark_shell\n        self.local_delta_dir = local_delta_dir\n\n    def run(self, cluster_hostname, ssh_id_file, ssh_user):\n        if self.local_delta_dir and isinstance(self.benchmark_spec, DeltaBenchmarkSpec):\n            # Upload new Delta jar to cluster and update spec to use the jar's version\n            delta_version_to_use = \\\n                self.upload_delta_jars_to_cluster_and_get_version(cluster_hostname, ssh_id_file, ssh_user)\n            self.benchmark_spec.update_delta_version(delta_version_to_use)\n\n        jar_path_in_cluster = self.upload_jar_to_cluster(cluster_hostname, ssh_id_file, ssh_user)\n        self.install_dependencies_via_ssh(cluster_hostname, ssh_id_file, ssh_user)\n        self.start_benchmark_via_ssh(cluster_hostname, ssh_id_file, jar_path_in_cluster, ssh_user)\n        Benchmark.wait_for_completion(cluster_hostname, ssh_id_file, self.benchmark_id, ssh_user)\n\n    def spark_submit_script_content(self, jar_path):\n        return f\"\"\"\n#!/bin/bash\njps | grep \"Spark\" | cut -f 1 -d ' ' |  xargs kill -9\nset -e\n{self.benchmark_spec.get_sparksubmit_cmd(jar_path)} 2>&1 | tee {self.output_file}\n\"\"\".strip()\n\n    def spark_shell_script_content(self, jar_path):\n        shell_init_file_name = f\"{self.benchmark_id}_shell_init.scala\"\n        benchmark_cmd_line_params_str = \\\n            ', '.join(f'\"{w}\"' for w in self.benchmark_spec.benchmark_main_class_args)\n        call_main_with_args = \\\n            f\"{self.benchmark_spec.benchmark_main_class}.main(Array[String]({benchmark_cmd_line_params_str}))\"\n        shell_init_file_content = \\\n            \"try { %s } catch { case t => println(t); println(\\\"FAILED\\\"); System.exit(1) } ; System.exit(0)\" % call_main_with_args\n        shell_cmd = self.benchmark_spec.get_sparkshell_cmd(jar_path, shell_init_file_name)\n        return f\"\"\"\n#!/bin/bash\njps | grep \"Spark\" | cut -f 1 -d ' ' |  xargs kill -9\necho '{shell_init_file_content}' > {shell_init_file_name}\n{shell_cmd} 2>&1 | tee {self.output_file}\ntouch {self.completed_file}\n\"\"\".strip()\n\n    def upload_jar_to_cluster(self, cluster_hostname, ssh_id_file, ssh_user, delta_version_to_use=None):\n        # Compile JAR\n        # Note: Deleting existing JARs instead of sbt clean is faster\n        if os.path.exists(\"target\"):\n            run_cmd(\"\"\"find target -name \"*.jar\" -type f -delete\"\"\", stream_output=True)\n        run_cmd(\"build/sbt assembly\", stream_output=True)\n        (_, out, _) = run_cmd(\"find target -name *.jar\")\n        print(\">>> Benchmark JAR compiled\\n\")\n\n        # Upload JAR\n        jar_local_path = out.decode(\"utf-8\").strip()\n        jar_remote_path = f\"{self.benchmark_id}-benchmarks.jar\"\n        scp_cmd = \\\n            f\"scp -C -i {ssh_id_file} {jar_local_path} {ssh_user}@{cluster_hostname}:{jar_remote_path}\"\n        print(scp_cmd)\n        run_cmd(scp_cmd, stream_output=True)\n        print(\">>> Benchmark JAR uploaded to cluster\\n\")\n        return f\"~/{jar_remote_path}\"\n\n    def install_dependencies_via_ssh(self, cluster_hostname, ssh_id_file, ssh_user):\n        script_file_name = f\"{self.benchmark_id}-install-deps.sh\"\n        script_file_text = \"\"\"\n#!/bin/bash\npackage='screen'\nif [ -x \"$(command -v yum)\" ]; then\n    if rpm -q $package; then\n        echo \"$package has already been installed\"\n    else\n        sudo yum -y install $package\n    fi\nelif [ -x \"$(command -v apt)\" ]; then\n    if dpkg -s $package; then\n        echo \"$package has already been installed\"\n    else\n        sudo apt install $package\n    fi\nelse\n    echo \"Failed to install packages: Package manager not found. You must manually install: $package\">&2; exit 1;\nfi\n\n\n        \"\"\".strip()\n        self.copy_script_via_ssh(cluster_hostname, ssh_id_file, ssh_user, script_file_name, script_file_text)\n        print(\">>> Install dependencies script generated and uploaded\\n\")\n\n        job_cmd = (\n                f\"ssh -i {ssh_id_file} {ssh_user}@{cluster_hostname} \" +\n                f\"bash {script_file_name}\"\n        )\n        print(job_cmd)\n        run_cmd(job_cmd, stream_output=True)\n        print(\">>> Dependencies have been installed\\n\")\n\n    def start_benchmark_via_ssh(self, cluster_hostname, ssh_id_file, jar_path, ssh_user):\n        # Generate and upload the script to run the benchmark\n        script_file_name = f\"{self.benchmark_id}-cmd.sh\"\n        if self.use_spark_shell:\n            script_file_text = self.spark_shell_script_content(jar_path)\n        else:\n            script_file_text = self.spark_submit_script_content(jar_path)\n\n        self.copy_script_via_ssh(cluster_hostname, ssh_id_file, ssh_user, script_file_name, script_file_text)\n        print(\">>> Benchmark script generated and uploaded\\n\")\n\n        # Start the script\n        job_cmd = (\n            f\"ssh -i {ssh_id_file} {ssh_user}@{cluster_hostname} \" +\n            f\"screen -d -m bash {script_file_name}\"\n        )\n        print(job_cmd)\n        run_cmd(job_cmd, stream_output=True)\n\n        # Print the screen where it is running\n        run_cmd(f\"ssh -i {ssh_id_file} {ssh_user}@{cluster_hostname}\" +\n                f\"\"\" \"screen -ls ; sleep 2; echo Files for this benchmark: ; ls {self.benchmark_id}*\" \"\"\",\n                stream_output=True, throw_on_error=False)\n        print(f\">>> Benchmark id {self.benchmark_id} started in a screen. Stdout piped into {self.output_file}. \"\n              f\"Final report will be generated on completion in {self.json_report_file}.\\n\")\n\n    @staticmethod\n    def copy_script_via_ssh(cluster_hostname, ssh_id_file, ssh_user, script_file_name, script_file_text):\n        try:\n            script_file = open(script_file_name, \"w\")\n            script_file.write(script_file_text)\n            script_file.close()\n\n            scp_cmd = (\n                    f\"scp -i {ssh_id_file} {script_file_name}\" +\n                    f\" {ssh_user}@{cluster_hostname}:{script_file_name}\"\n            )\n            print(scp_cmd)\n            run_cmd(scp_cmd, stream_output=True)\n            run_cmd_over_ssh(f\"chmod +x {script_file_name}\", cluster_hostname, ssh_id_file, ssh_user,\n                             throw_on_error=False)\n        finally:\n            if os.path.exists(script_file_name):\n                os.remove(script_file_name)\n\n    @staticmethod\n    def output_file(benchmark_id):\n        return f\"{benchmark_id}-out.txt\"\n\n    @staticmethod\n    def json_report_file(benchmark_id):\n        return f\"{benchmark_id}-report.json\"\n\n    @staticmethod\n    def csv_report_file(benchmark_id):\n        return f\"{benchmark_id}-report.csv\"\n\n    @staticmethod\n    def completed_file(benchmark_id):\n        return f\"{benchmark_id}-completed.txt\"\n\n    @staticmethod\n    def wait_for_completion(cluster_hostname, ssh_id_file, benchmark_id, ssh_user, copy_report=True):\n        completed = False\n        succeeded = False\n        output_file = Benchmark.output_file(benchmark_id)\n        completed_file = Benchmark.completed_file(benchmark_id)\n        json_report_file = Benchmark.json_report_file(benchmark_id)\n        csv_report_file = Benchmark.csv_report_file(benchmark_id)\n\n        print(f\"\\nWaiting for completion of benchmark id {benchmark_id}\")\n        while not completed:\n            # Print the size of the output file to show progress\n            (_, out, _) = run_cmd_over_ssh(f\"stat -c '%n:   [%y]   [%s bytes]' {output_file}\",\n                                           cluster_hostname, ssh_id_file, ssh_user,\n                                           throw_on_error=False)\n            out = out.decode(\"utf-8\").strip()\n            print(out)\n            if \"No such file\" in out:\n                print(\">>> Benchmark failed to start\")\n                return\n\n            # Check for the existence of the completed file\n            (_, out, _) = run_cmd_over_ssh(f\"ls {completed_file}\", cluster_hostname, ssh_id_file, ssh_user,\n                                           throw_on_error=False)\n            if completed_file in out.decode(\"utf-8\"):\n                completed = True\n            else:\n                time.sleep(60)\n\n        # Check the last few lines of output files to identify success\n        (_, out, _) = run_cmd_over_ssh(f\"tail {output_file}\", cluster_hostname, ssh_id_file, ssh_user,\n                                       throw_on_error=False)\n        if \"SUCCESS\" in out.decode(\"utf-8\"):\n            succeeded = True\n            print(\">>> Benchmark completed with success\\n\")\n        else:\n            print(\">>> Benchmark completed with failure\\n\")\n\n        # Download reports\n        if copy_report:\n            Benchmark.download_file(output_file, cluster_hostname, ssh_id_file, ssh_user)\n            if succeeded:\n                report_files = [json_report_file, csv_report_file]\n                for report_file in report_files:\n                    Benchmark.download_file(report_file, cluster_hostname, ssh_id_file, ssh_user)\n            print(\">>> Downloaded reports to local directory\")\n\n\n    @staticmethod\n    def download_file(file, cluster_hostname, ssh_id_file, ssh_user):\n        run_cmd(f\"scp -C -i {ssh_id_file} \" +\n                f\"{ssh_user}@{cluster_hostname}:{file} {file}\",\n                stream_output=True)\n\n    def upload_delta_jars_to_cluster_and_get_version(self, cluster_hostname, ssh_id_file, ssh_user):\n        if not self.local_delta_dir:\n            raise Exception(\"Path to delta repo not specified\")\n        delta_repo_dir = os.path.abspath(self.local_delta_dir)\n\n        with WorkingDirectory(delta_repo_dir):\n            # Compile Delta JARs by publishing to local maven cache\n            print(f\"Compiling Delta to local dir {delta_repo_dir}\")\n            local_maven_delta_dir = os.path.expanduser(\"~/.ivy2/local/io.delta/\")\n            if os.path.exists(local_maven_delta_dir):\n                run_cmd(f\"rm -rf {local_maven_delta_dir}\", stream_output=True)\n                print(f\"Cleared local maven cache at {local_maven_delta_dir}\")\n            run_cmd(\"build/sbt publishLocal\", stream_output=False, throw_on_error=True)\n\n            # Get the new version\n            (_, out, _) = run_cmd(\"\"\"build/sbt \"show version\" \"\"\")\n            version = out.decode(\"utf-8\").strip().rsplit(\"\\n\", 1)[-1].rsplit(\" \", 1)[-1].strip()\n            if not version:\n                raise Exception(f\"Could not find the version from the sbt output:\\n--\\n{out}\\n-\")\n\n            # Upload JARs to cluster's local maven cache\n            remote_maven_dir = \".ivy2/local/\"  # must have \"/\" at the end\n            run_cmd_over_ssh(\n                f\"rm -rf {remote_maven_dir}/* .ivy2/cache/io.delta .ivy2/jars/io.delta*\",\n                cluster_hostname, ssh_id_file, ssh_user, stream_output=True, throw_on_error=False)\n            run_cmd_over_ssh(f\"mkdir -p {remote_maven_dir}\", cluster_hostname,\n                             ssh_id_file, ssh_user, stream_output=True)\n            scp_cmd = f\"\"\"scp -r -C -i {ssh_id_file} {local_maven_delta_dir.rstrip(\"/\")} \"\"\" +\\\n                      f\"{ssh_user}@{cluster_hostname}:{remote_maven_dir}\"\n            print(scp_cmd)\n            run_cmd(scp_cmd, stream_output=True)\n            print(f\">>> Delta {version} JAR uploaded to cluster\\n\")\n            return version\n"
  },
  {
    "path": "benchmarks/scripts/utils.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport shlex\nimport subprocess\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, **kwargs):\n    if isinstance(cmd, str):\n        cmd = shlex.split(cmd)\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % (exit_code))\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return exit_code, stdout, stderr\n\n\ndef run_cmd_over_ssh(cmd, host, ssh_id_file, user, **kwargs):\n    full_cmd = f\"\"\"ssh -i {ssh_id_file} {user}@{host} \"{cmd}\" \"\"\"\n    return run_cmd(full_cmd, **kwargs)\n\n\n# pylint: disable=too-few-public-methods\nclass WorkingDirectory(object):\n    def __init__(self, working_directory):\n        self.working_directory = working_directory\n        self.old_workdir = os.getcwd()\n\n    def __enter__(self):\n        os.chdir(self.working_directory)\n\n    def __exit__(self, tpe, value, traceback):\n        os.chdir(self.old_workdir)\n\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/Benchmark.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\nimport java.net.URI\nimport java.nio.file.{Files, Paths}\nimport java.nio.charset.StandardCharsets\n\nimport scala.collection.mutable\nimport scala.language.postfixOps\nimport scala.sys.process._\nimport scala.util.control.NonFatal\n\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder\nimport com.fasterxml.jackson.databind.{DeserializationFeature, MapperFeature, ObjectMapper}\nimport com.fasterxml.jackson.module.scala.{DefaultScalaModule, ScalaObjectMapper}\n\nimport org.apache.spark.SparkUtils\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\n\ntrait BenchmarkConf extends Product {\n  /** Cloud path where benchmark data is going to be written. */\n  def benchmarkPath: Option[String]\n\n  /** Get the database location given the database name and the benchmark path. */\n  def dbLocation(dbName: String, suffix: String = \"\"): String = {\n    benchmarkPath.map(p => s\"$p/databases/${dbName}_${suffix}\").getOrElse {\n      throw new IllegalArgumentException(\"Benchmark path must be specified\")\n    }\n  }\n\n  /** Cloud path where benchmark reports will be uploaded. */\n  def reportUploadPath: String = {\n    benchmarkPath.map(p => s\"$p/reports/\").getOrElse {\n      throw new IllegalArgumentException(\"Benchmark path must be specified\")\n    }\n  }\n  def jsonReportUploadPath: String = s\"$reportUploadPath/json/\"\n  def csvReportUploadPath: String = s\"$reportUploadPath/csv/\"\n\n  /** Get the benchmark conf details as a map. */\n  def asMap: Map[String, String] = SparkUtils.caseClassToMap(this)\n}\n\n@JsonPropertyOrder(alphabetic=true)\ncase class QueryResult(\n    name: String,\n    iteration: Option[Int],\n    durationMs: Option[Long],\n    errorMsg: Option[String])\n\n@JsonPropertyOrder(alphabetic=true)\ncase class SparkEnvironmentInfo(\n    @JsonPropertyOrder(alphabetic=true)\n    sparkBuildInfo: Map[String, String],\n    @JsonPropertyOrder(alphabetic=true)\n    runtimeInfo: Map[String, String],\n    @JsonPropertyOrder(alphabetic=true)\n    sparkProps: Map[String, String],\n    @JsonPropertyOrder(alphabetic=true)\n    hadoopProps: Map[String, String],\n    @JsonPropertyOrder(alphabetic=true)\n    systemProps: Map[String, String],\n    @JsonPropertyOrder(alphabetic=true)\n    classpathEntries: Map[String, String])\n\n@JsonPropertyOrder(alphabetic=true)\ncase class BenchmarkReport(\n    @JsonPropertyOrder(alphabetic=true)\n    benchmarkSpecs: Map[String, String],\n    queryResults: Array[QueryResult],\n    extraMetrics: Map[String, Double],\n    sparkEnvInfo: SparkEnvironmentInfo)\n\n/**\n * Base class for any benchmark with the core functionality of measuring SQL query durations\n * and printing the details as json in a report file.\n */\nabstract class Benchmark(private val conf: BenchmarkConf) {\n\n  /* Methods that implementations should override. */\n\n  protected def runInternal(): Unit\n\n  /* Fields and methods that implementations should not have to override */\n\n  final protected lazy val spark = {\n    val s = SparkSession.builder()\n      .config(\"spark.ui.proxyBase\", \"\")\n      .getOrCreate()\n    log(\"Spark started with configuration:\\n\" +\n      s.conf.getAll.toSeq.sortBy(_._1).map(x => x._1 + \": \" + x._2).mkString(\"\\t\", \"\\n\\t\", \"\\n\"))\n    s.sparkContext.setLogLevel(\"WARN\")\n    sys.props.update(\"spark.ui.proxyBase\", \"\")\n    s\n  }\n\n  val extraConfs: Map[String, String] = Map(\n    SQLConf.BROADCAST_TIMEOUT.key -> \"7200\",\n    SQLConf.CROSS_JOINS_ENABLED.key -> \"true\"\n  )\n\n  private val queryResults = new mutable.ArrayBuffer[QueryResult]\n  private val extraMetrics = new mutable.HashMap[String, Double]\n\n  protected def run(): Unit = {\n    try {\n      log(\"=\" * 80)\n      log(\"=\" * 80)\n      runInternal()\n      log(\"=\" * 80)\n    } finally {\n      generateReport()\n    }\n    println(s\"SUCCESS\")\n  }\n\n  protected def runQuery(\n      sqlCmd: String,\n      queryName: String = \"\",\n      iteration: Option[Int] = None,\n      printRows: Boolean = false,\n      ignoreError: Boolean = true): Seq[Row] = synchronized {\n    val iterationStr = iteration.map(i => s\" - iteration $i\").getOrElse(\"\")\n    var banner = s\"$queryName$iterationStr\"\n    if (banner.trim.isEmpty) {\n      banner = sqlCmd.split(\"\\n\")(0).trim + (if (sqlCmd.split(\"\\n\").size > 1) \"...\" else \"\")\n    }\n    log(\"=\" * 80)\n    log(s\"START: $banner\")\n    log(\"SQL: \" + sqlCmd.replaceAll(\"\\n\\\\s*\", \" \"))\n    spark.sparkContext.setJobGroup(banner, banner, interruptOnCancel = true)\n    try {\n      val before = System.nanoTime()\n      val df = spark.sql(sqlCmd)\n      val r = df.collect()\n      val after = System.nanoTime()\n      if (printRows) df.show(false)\n      val durationMs = (after - before) / (1000 * 1000)\n      queryResults += QueryResult(queryName, iteration, Some(durationMs), errorMsg = None)\n      log(s\"END took $durationMs ms: $banner\")\n      log(\"=\" * 80)\n      r\n    } catch {\n      case NonFatal(e) =>\n        log(s\"ERROR: $banner\\n${e.getMessage}\")\n        queryResults +=\n          QueryResult(queryName, iteration, durationMs = None, errorMsg = Some(e.getMessage))\n        if (!ignoreError) throw e else Nil\n    }\n  }\n\n\n  protected def runFunc(\n      queryName: String = \"\",\n      iteration: Option[Int] = None,\n      ignoreError: Boolean = true)(f: => Unit): Unit = synchronized {\n    val iterationStr = iteration.map(i => s\" - iteration $i\").getOrElse(\"\")\n    var banner = s\"$queryName$iterationStr\"\n    log(\"=\" * 80)\n    log(s\"START: $banner\")\n    spark.sparkContext.setJobGroup(banner, banner, interruptOnCancel = true)\n    try {\n      val before = System.nanoTime()\n      f\n      val after = System.nanoTime()\n      val durationMs = (after - before) / (1000 * 1000)\n      queryResults += QueryResult(queryName, iteration, Some(durationMs), errorMsg = None)\n      log(s\"END took $durationMs ms: $banner\")\n      log(\"=\" * 80)\n    } catch {\n      case NonFatal(e) =>\n        log(s\"ERROR: $banner\\n${e.getMessage}\")\n        queryResults +=\n          QueryResult(queryName, iteration, durationMs = None, errorMsg = Some(e.getMessage))\n        if (!ignoreError) throw e else spark.emptyDataFrame\n    }\n  }\n\n\n  protected def reportExtraMetric(name: String, value: Double): Unit = synchronized {\n    extraMetrics += (name -> value)\n  }\n\n  protected def getQueryResults(): Array[QueryResult] = synchronized { queryResults.toArray }\n\n  private def generateJSONReport(report: BenchmarkReport): Unit = synchronized {\n    import Benchmark._\n\n    val resultJson = toPrettyJson(report)\n    val resultFileName =\n      if (benchmarkId.trim.isEmpty) \"report.json\" else s\"$benchmarkId-report.json\"\n    val reportLocalPath = Paths.get(resultFileName).toAbsolutePath()\n    Files.write(reportLocalPath, resultJson.getBytes(StandardCharsets.UTF_8))\n    println(s\"RESULT:\\n$resultJson\")\n    uploadFile(reportLocalPath.toString, conf.jsonReportUploadPath)\n  }\n\n  private def generateCSVReport(): Unit = synchronized {\n    val csvHeader = \"name,iteration,durationMs\"\n    val csvRows = queryResults.map { r =>\n      s\"${r.name},${r.iteration.getOrElse(1)},${r.durationMs.getOrElse(-1)}\"\n    }\n    val csvText = (Seq(csvHeader) ++ csvRows).mkString(\"\\n\")\n    val resultFileName =\n      if (benchmarkId.trim.isEmpty) \"report.csv\" else s\"$benchmarkId-report.csv\"\n    val reportLocalPath = Paths.get(resultFileName).toAbsolutePath()\n    Files.write(reportLocalPath, csvText.getBytes(StandardCharsets.UTF_8))\n    uploadFile(reportLocalPath.toString, conf.csvReportUploadPath)\n  }\n\n  private def generateReport(): Unit = synchronized {\n    val report = BenchmarkReport(\n      benchmarkSpecs = conf.asMap + (\"benchmarkId\" -> benchmarkId),\n      queryResults = queryResults.toArray,\n      extraMetrics = extraMetrics.toMap,\n      sparkEnvInfo = SparkUtils.getEnvironmentInfo(spark.sparkContext)\n    )\n    generateJSONReport(report)\n    generateCSVReport()\n  }\n\n  private def uploadFile(localPath: String, targetPath: String): Unit = {\n    val targetUri = new URI(targetPath)\n    val sanitizedTargetPath = targetUri.normalize().toString\n    val scheme = new URI(targetPath).getScheme\n    try {\n      if (scheme.equals(\"s3\")) s\"aws s3 cp $localPath $sanitizedTargetPath/\" !\n      else if (scheme.equals(\"gs\")) s\"gsutil cp $localPath $sanitizedTargetPath/\" !\n      else throw new IllegalArgumentException(String.format(\"Unsupported scheme %s.\", scheme))\n\n      println(s\"FILE UPLOAD: Uploaded $localPath to $sanitizedTargetPath\")\n    } catch {\n      case NonFatal(e) =>\n        log(s\"FILE UPLOAD: Failed to upload $localPath to $sanitizedTargetPath: $e\")\n    }\n  }\n\n  protected def benchmarkId: String =\n    sys.env.getOrElse(\"BENCHMARK_ID\", spark.conf.getOption(\"spark.benchmarkId\").getOrElse(\"\"))\n\n  protected def log(str: => String): Unit = {\n    println(s\"${java.time.LocalDateTime.now} $str\")\n  }\n}\n\nobject Benchmark {\n  private lazy val mapper = {\n    val _mapper = new ObjectMapper with ScalaObjectMapper\n    _mapper.setSerializationInclusion(Include.NON_ABSENT)\n    _mapper.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)\n    _mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n    _mapper.registerModule(DefaultScalaModule)\n    _mapper\n  }\n\n  def toJson[T: Manifest](obj: T): String = {\n    mapper.writeValueAsString(obj)\n  }\n\n  def toPrettyJson[T: Manifest](obj: T): String = {\n    mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/MergeBenchmark.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\nimport java.util.UUID\n\nimport org.apache.spark.SparkUtils\nimport org.apache.spark.sql.Row\n\ntrait MergeConf extends BenchmarkConf {\n  def scaleInGB: Int\n  def tableName: String = \"web_returns\"\n  def userDefinedDbName: Option[String]\n  def dbName: String = userDefinedDbName.getOrElse(s\"merge_sf${scaleInGB}\")\n  def dbLocation: String = dbLocation(dbName)\n}\n\ncase class MergeBenchmarkConf(\n     scaleInGB: Int = 0,\n     userDefinedDbName: Option[String] = None,\n     iterations: Int = 3,\n     benchmarkPath: Option[String] = None) extends MergeConf {\n}\n\nobject MergeBenchmarkConf {\n  import scopt.OParser\n  private val builder = OParser.builder[MergeBenchmarkConf]\n  private val argParser = {\n    import builder._\n    OParser.sequence(\n      programName(\"Merge Benchmark\"),\n      opt[String](\"scale-in-gb\")\n        .required()\n        .valueName(\"<scale of benchmark in GBs>\")\n        .action((x, c) => c.copy(scaleInGB = x.toInt))\n        .text(\"Scale factor in GBs of the TPCDS benchmark\"),\n      opt[String](\"benchmark-path\")\n        .required()\n        .valueName(\"<cloud storage path>\")\n        .action((x, c) => c.copy(benchmarkPath = Some(x)))\n        .text(\"Cloud path to be used for creating table and generating reports\"),\n      opt[String](\"iterations\")\n        .optional()\n        .valueName(\"<number of iterations>\")\n        .action((x, c) => c.copy(iterations = x.toInt))\n        .text(\"Number of times to run the queries\"))\n  }\n\n  def parse(args: Array[String]): Option[MergeBenchmarkConf] = {\n    OParser.parse(argParser, args, MergeBenchmarkConf())\n  }\n}\n\nclass MergeBenchmark(conf: MergeBenchmarkConf) extends Benchmark(conf) {\n  /**\n   * Runs every merge test case multiple times and records the duration.\n   */\n  override def runInternal(): Unit = {\n    for ((k, v) <- extraConfs) spark.conf.set(k, v)\n    spark.sparkContext.setLogLevel(\"WARN\")\n    log(\"All configs:\\n\\t\" + spark.conf.getAll.toSeq.sortBy(_._1).mkString(\"\\n\\t\"))\n    spark.sql(s\"USE ${conf.dbName}\")\n\n    val targetRowCount = spark.read.table(s\"`${conf.dbName}`.`target_${conf.tableName}`\").count\n\n    for (iteration <- 1 to conf.iterations) {\n      MergeTestCases.testCases.foreach { runMerge(_, targetRowCount, iteration = Some(iteration)) }\n    }\n    val results = getQueryResults().filter(_.name.startsWith(\"q\"))\n    if (results.forall(x => x.errorMsg.isEmpty && x.durationMs.nonEmpty) ) {\n      val medianDurationSecPerQuery = results.groupBy(_.name).map { case (q, results) =>\n        assert(results.length == conf.iterations)\n        val medianMs = SparkUtils.median(results.map(_.durationMs.get), alreadySorted = false)\n        (q, medianMs / 1000.0)\n      }\n      val sumOfMedians = medianDurationSecPerQuery.values.sum\n      reportExtraMetric(\"merge-result-seconds\", sumOfMedians)\n    }\n  }\n\n  /**\n   * Merge test runner performing the following steps:\n   * - Clone a fresh target table.\n   * - Run the merge test case.\n   * - Check invariants.\n   * - Drop the cloned table.\n   */\n  protected def runMerge(\n      testCase: MergeTestCase,\n      targetRowCount: Long,\n      iteration: Option[Int] = None,\n      printRows: Boolean = false,\n      ignoreError: Boolean = true): Seq[Row] = synchronized {\n    withCloneTargetTable(testCase.name) { targetTable =>\n      val result = super.runQuery(\n        testCase.sqlCmd(targetTable),\n        testCase.name,\n        iteration,\n        printRows,\n        ignoreError)\n      testCase.validate(result, targetRowCount)\n      result\n    }\n  }\n\n  /**\n   * Clones the target table before each test case to use a fresh target table and drops the clone\n   * afterwards.\n   */\n  protected def withCloneTargetTable[T](testCaseName: String)(f: String => T): T = {\n    val target = s\"`${conf.dbName}`.`target_${conf.tableName}`\"\n    val clonedTableName = s\"`${conf.dbName}`.`${conf.tableName}_${generateShortUUID()}`\"\n    runQuery(s\"CREATE TABLE $clonedTableName SHALLOW CLONE $target\", s\"clone-target-$testCaseName\")\n    try {\n      f(clonedTableName)\n    } finally {\n      runQuery(s\"DROP TABLE IF EXISTS $clonedTableName\", s\"drop-target-clone-$testCaseName\")\n    }\n  }\n\n  protected def generateShortUUID(): String =\n    UUID.randomUUID.toString.replace(\"-\", \"_\").take(8)\n}\n\nobject MergeBenchmark {\n  def main(args: Array[String]): Unit = {\n    MergeBenchmarkConf.parse(args).foreach { conf =>\n      new MergeBenchmark(conf).run()\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/MergeDataLoad.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\nimport java.util.Locale\n\nimport org.apache.spark.sql.functions.{col, countDistinct, hash, isnull, max, rand}\n\n\ncase class MergeDataLoadConf(\n    scaleInGB: Int = 0,\n    userDefinedDbName: Option[String] = None,\n    loadFromPath: Option[String] = None,\n    benchmarkPath: Option[String] = None,\n    excludeNulls: Boolean = true) extends MergeConf {\n}\n\n/**\n * Represents a table configuration used as a source in merge test cases. Each [[MergeTestCase]] has\n * one [[MergeSourceTable]] associated with it, the data loader will collect all source table\n * configurations for all tests and create the required source tables.\n * @param filesMatchedFraction Fraction of files from the base table that will get sampled to\n *                             create the source table.\n * @param rowsMatchedFraction Fraction of rows from the selected files that will get sampled to form\n *                            the part of the source table that matches the merge condition.\n * @param rowsNotMatchedFraction Fraction of rows from the selected files that will get sampled to\n *                               form the part of the source table that doesn't match the merge\n *                               condition.\n */\ncase class MergeSourceTable(\n    filesMatchedFraction: Double,\n    rowsMatchedFraction: Double,\n    rowsNotMatchedFraction: Double) {\n  def name: String = formatTableName(s\"source_\" +\n    s\"_filesMatchedFraction_$filesMatchedFraction\" +\n    s\"_rowsMatchedFraction_$rowsMatchedFraction\" +\n    s\"_rowsNotMatchedFraction_$rowsNotMatchedFraction\")\n\n  protected def formatTableName(s: String): String = {\n    s.toLowerCase(Locale.ROOT).replaceAll(\"\\\\s+\", \"_\").replaceAll(\"[-,.]\", \"_\")\n  }\n}\n\nobject MergeDataLoadConf {\n  import scopt.OParser\n  private val builder = OParser.builder[MergeDataLoadConf]\n  private val argParser = {\n    import builder._\n    OParser.sequence(\n      programName(\"Merge Data Load\"),\n      opt[String](\"scale-in-gb\")\n        .required()\n        .valueName(\"<scale of benchmark in GBs>\")\n        .action((x, c) => c.copy(scaleInGB = x.toInt))\n        .text(\"Scale factor of the Merge benchmark\"),\n      opt[String](\"benchmark-path\")\n        .required()\n        .valueName(\"<cloud storage path>\")\n        .action((x, c) => c.copy(benchmarkPath = Some(x)))\n        .text(\"Cloud storage path to be used for creating table and generating reports\"),\n      opt[String](\"db-name\")\n        .optional()\n        .valueName(\"<database name>\")\n        .action((x, c) => c.copy(userDefinedDbName = Some(x)))\n        .text(\"Name of the target database to create with TPC-DS tables in necessary format\"),\n      opt[String](\"load-from-path\")\n        .optional()\n        .valueName(\"<path to the TPC-DS raw input data>\")\n        .action((x, c) => c.copy(loadFromPath = Some(x)))\n        .text(\"The location of the TPC-DS raw input data\"),\n      opt[String](\"exclude-nulls\")\n        .optional()\n        .valueName(\"true/false\")\n        .action((x, c) => c.copy(excludeNulls = x.toBoolean))\n        .text(\"Whether to remove null primary keys when loading data, default = false\"))\n  }\n\n  def parse(args: Array[String]): Option[MergeDataLoadConf] = {\n    OParser.parse(argParser, args, MergeDataLoadConf())\n  }\n}\n\nclass MergeDataLoad(conf: MergeDataLoadConf) extends Benchmark(conf) {\n\n  protected def targetTableFullName = s\"`${conf.dbName}`.`target_${conf.tableName}`\"\n\n  protected def dataLoadFromPath: String = conf.loadFromPath.getOrElse {\n    s\"s3://devrel-delta-datasets/tpcds-2.13/tpcds_sf${conf.scaleInGB}_parquet/${conf.tableName}/\"\n  }\n\n  /**\n   * Creates the target table and all source table configuration used in merge test cases.\n   */\n  def runInternal(): Unit = {\n    val dbName = conf.dbName\n    val dbLocation = conf.dbLocation(dbName, suffix = benchmarkId.replace(\"-\", \"_\"))\n    val dbCatalog = \"spark_catalog\"\n\n    require(Seq(1, 3000).contains(conf.scaleInGB), \"\")\n\n    log(s\"====== Creating database =======\")\n    runQuery(s\"DROP DATABASE IF EXISTS ${dbName} CASCADE\", s\"drop-database\")\n    runQuery(s\"CREATE DATABASE IF NOT EXISTS ${dbName}\", s\"create-database\")\n\n    log(s\"====== Creating merge target table =======\")\n    loadMergeTargetTable()\n    log(s\"====== Creating merge source tables =======\")\n    MergeTestCases.testCases.map(_.sourceTable).distinct.foreach(loadMergeSourceTable)\n    log(s\"====== Created all tables in database ${dbName} at '${dbLocation}' =======\")\n\n    runQuery(s\"USE $dbCatalog.$dbName;\")\n    runQuery(\"SHOW TABLES\", printRows = true)\n  }\n\n  /**\n   * Creates the target Delta table and performs sanity checks. This table will be cloned before\n   * each merge test case and the clone serves as a single-use merge target table.\n   */\n  protected def loadMergeTargetTable(): Unit = {\n    val dbLocation = conf.dbLocation(conf.dbName, suffix = benchmarkId.replace(\"-\", \"_\"))\n    val location = s\"${dbLocation}/${conf.tableName}/\"\n    val format = \"parquet\"\n\n    runQuery(s\"DROP TABLE IF EXISTS $targetTableFullName\", s\"drop-table-$targetTableFullName\")\n\n    runQuery(\n      s\"\"\"CREATE TABLE $targetTableFullName\n                 USING DELTA\n                 LOCATION '$location'\n                 SELECT * FROM `${format}`.`$dataLoadFromPath`\n              \"\"\", s\"create-table-$targetTableFullName\", ignoreError = true)\n\n    val sourceRowCount =\n      spark.sql(s\"SELECT * FROM `${format}`.`$dataLoadFromPath`\").count()\n    val targetRowCount = spark.table(targetTableFullName).count()\n    val targetFileCount =\n      spark.table(targetTableFullName).select(countDistinct(\"_metadata.file_path\"))\n    log(s\"Target file count: $targetFileCount\")\n    log(s\"Target row count: $targetRowCount\")\n\n    assert(targetRowCount == sourceRowCount,\n      s\"Row count mismatch: source table = $sourceRowCount, \" +\n      s\"target $targetTableFullName = $targetRowCount\")\n  }\n\n  /**\n   * Creates a table that will be used as a merge source table in the merge test cases. The table is\n   * created by sampling the merge target table created by [[loadMergeTargetTable]]. The merge test\n   * cases don't modify the source table and a single source table is reused across different test\n   * cases if the same source table configuration is used.\n   */\n  protected def loadMergeSourceTable(sourceTableConf: MergeSourceTable): Unit = {\n    val fullTableName = s\"`${conf.dbName}`.`${sourceTableConf.name}`\"\n    val dbLocation = conf.dbLocation(conf.dbName, suffix = benchmarkId.replace(\"-\", \"_\"))\n\n    runQuery(s\"DROP TABLE IF EXISTS $fullTableName\", s\"drop-table-${sourceTableConf.name}\")\n\n    val fullTableDF = spark.read.format(\"delta\")\n      .load(s\"${dbLocation}/${conf.tableName}/\")\n    // Sample files based on their file path.\n    val sampledFilesDF = fullTableDF\n      .select(\"_metadata.file_path\")\n      .distinct\n      .sample(sourceTableConf.filesMatchedFraction)\n\n    // Read the data from the sampled files and sample two sets of rows for MATCHED clauses and\n    // NOT MATCHED clauses respectively.\n    val sampledDataDF = fullTableDF\n      .withColumn(\"file_path\", col(\"_metadata.file_path\"))\n      .join(sampledFilesDF, \"file_path\")\n    log(s\"Matching files row count: ${sampledDataDF.count}\")\n\n    val numberOfNulls = sampledDataDF.filter(isnull(col(\"wr_order_number\"))).count\n    log(s\"wr_order_number contains $numberOfNulls null values\")\n    val matchedData = sampledDataDF.sample(sourceTableConf.rowsMatchedFraction)\n    val notMatchedData = sampledDataDF.sample(sourceTableConf.rowsNotMatchedFraction)\n      .withColumn(\"wr_order_number\", rand())\n      .withColumn(\"wr_item_sk\", rand())\n\n    val data = matchedData.union(notMatchedData)\n\n    val dupes = data.groupBy(\"wr_order_number\", \"wr_item_sk\").count.filter(\"count > 1\")\n    log(s\"Duplicates: ${dupes.collect().mkString(\"Array(\", \",\\n\", \")\")}\")\n    data.write.format(\"delta\").saveAsTable(fullTableName)\n  }\n}\n\nobject MergeDataLoad {\n  def main(args: Array[String]): Unit = {\n    MergeDataLoadConf.parse(args).foreach { conf =>\n      new MergeDataLoad(conf).run()\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/MergeTestCases.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\nimport org.apache.spark.sql.Row\n\ntrait MergeTestCase {\n  /**\n   * Name of the test case used e.p. in the test results.\n   */\n  def name: String\n\n  /**\n   * The source table configuration to use for the test case. When a test case is defined,\n   * [[MergeDataLoad]] will collect all source table configuration and create the source tables\n   * required by all tests.\n   */\n  def sourceTable: MergeSourceTable\n\n  /**\n   * The merge command to execute as a SQL string.\n   */\n  def sqlCmd(targetTable: String): String\n\n  /**\n   * Each test case can define invariants to check after the merge command runs to ensure that the\n   * benchmark results are valid.\n   */\n  def validate(mergeStats: Seq[Row], targetRowCount: Long): Unit\n}\n\n/**\n * Trait shared by all insert-only merge test cases.\n */\ntrait InsertOnlyTestCase extends MergeTestCase {\n    val filesMatchedFraction: Double\n    val rowsNotMatchedFraction: Double\n\n  override def sourceTable: MergeSourceTable = MergeSourceTable(\n    filesMatchedFraction,\n    rowsMatchedFraction = 0,\n    rowsNotMatchedFraction)\n\n  override def validate(mergeStats: Seq[Row], targetRowCount: Long): Unit = {\n    assert(mergeStats.length == 1)\n    assert(mergeStats.head.getAs[Long](\"num_updated_rows\") == 0)\n    assert(mergeStats.head.getAs[Long](\"num_deleted_rows\") == 0)\n  }\n}\n\n/**\n * A merge test case with a single WHEN NOT MATCHED THEN INSERT * clause.\n */\ncase class SingleInsertOnlyTestCase(\n    filesMatchedFraction: Double,\n    rowsNotMatchedFraction: Double) extends InsertOnlyTestCase {\n\n  override val name: String = \"single_insert_only\" +\n    s\"_filesMatchedFraction_$filesMatchedFraction\" +\n    s\"_rowsNotMatchedFraction_$rowsNotMatchedFraction\"\n\n\n  override def sqlCmd(targetTable: String): String = {\n    s\"\"\"MERGE INTO $targetTable t\n        |USING ${sourceTable.name} s\n        |ON t.wr_order_number = s.wr_order_number AND t.wr_item_sk = s.wr_item_sk\n        |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin\n   }\n}\n\n/**\n * A merge test case with two WHEN NOT MATCHED (AND condition) THEN INSERT * clauses.\n */\ncase class MultipleInsertOnlyTestCase(\n    filesMatchedFraction: Double,\n    rowsNotMatchedFraction: Double) extends InsertOnlyTestCase {\n\n  override val name: String = \"multiple_insert_only\" +\n    s\"_filesMatchedFraction_$filesMatchedFraction\" +\n    s\"_rowsNotMatchedFraction_$rowsNotMatchedFraction\"\n\n  override def sqlCmd(targetTable: String): String = {\n    s\"\"\"MERGE INTO $targetTable t\n        |USING ${sourceTable.name} s\n        |ON t.wr_order_number = s.wr_order_number AND t.wr_item_sk = s.wr_item_sk\n        |WHEN NOT MATCHED AND s.wr_item_sk % 2 = 0 THEN INSERT *\n        |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin\n   }\n}\n\n/**\n * A merge test case with a single WHEN MATCHED THEN DELETED clause.\n */\ncase class DeleteOnlyTestCase(\n    filesMatchedFraction: Double,\n    rowsMatchedFraction: Double) extends MergeTestCase {\n\n  override val name: String = \"delete_only\" +\n    s\"_filesMatchedFraction_$filesMatchedFraction\" +\n    s\"_rowsMatchedFraction_$rowsMatchedFraction\"\n\n  override def sourceTable: MergeSourceTable = MergeSourceTable(\n    filesMatchedFraction,\n    rowsMatchedFraction,\n    rowsNotMatchedFraction = 0)\n\n  override def sqlCmd(targetTable: String): String = {\n    s\"\"\"MERGE INTO $targetTable t\n        |USING ${sourceTable.name} s\n        |ON t.wr_order_number = s.wr_order_number AND t.wr_item_sk = s.wr_item_sk\n        |WHEN MATCHED THEN DELETE\"\"\".stripMargin\n   }\n\n  override def validate(mergeStats: Seq[Row], targetRowCount: Long): Unit = {\n    assert(mergeStats.length == 1)\n    assert(mergeStats.head.getAs[Long](\"num_updated_rows\") == 0)\n    assert(mergeStats.head.getAs[Long](\"num_inserted_rows\") == 0)\n  }\n}\n\n/**\n * A merge test case with a MATCHED UPDATE and a NOT MATCHED INSERT clause.\n */\ncase class UpsertTestCase(\n    filesMatchedFraction: Double,\n    rowsMatchedFraction: Double,\n    rowsNotMatchedFraction: Double) extends MergeTestCase {\n\n  override val name: String = \"upsert\" +\n    s\"_filesMatchedFraction_$filesMatchedFraction\" +\n    s\"_rowsMatchedFraction_$rowsMatchedFraction\" +\n    s\"_rowsNotMatchedFraction_$rowsNotMatchedFraction\"\n\n  override def sourceTable: MergeSourceTable = MergeSourceTable(\n    filesMatchedFraction,\n    rowsMatchedFraction,\n    rowsNotMatchedFraction)\n\n  override def sqlCmd(targetTable: String): String = {\n    s\"\"\"MERGE INTO $targetTable t\n        |USING ${sourceTable.name} s\n        |ON t.wr_order_number = s.wr_order_number AND t.wr_item_sk = s.wr_item_sk\n        |WHEN MATCHED THEN UPDATE SET *\n        |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin\n   }\n\n  override def validate(mergeStats: Seq[Row], targetRowCount: Long): Unit = {\n    assert(mergeStats.length == 1)\n    assert(mergeStats.head.getAs[Long](\"num_deleted_rows\") == 0)\n  }\n}\n\nobject MergeTestCases {\n  def testCases: Seq[MergeTestCase] =\n    insertOnlyTestCases ++\n    deleteOnlyTestCases ++\n    upsertTestCases\n\n  def insertOnlyTestCases: Seq[MergeTestCase] =\n    Seq(0.05, 0.5, 1.0).flatMap { rowsNotMatchedFraction =>\n      Seq(\n        SingleInsertOnlyTestCase(\n          filesMatchedFraction = 0.05,\n          rowsNotMatchedFraction),\n\n        MultipleInsertOnlyTestCase(\n          filesMatchedFraction = 0.05,\n          rowsNotMatchedFraction)\n      )\n    }\n\n  def deleteOnlyTestCases: Seq[MergeTestCase] = Seq(\n    DeleteOnlyTestCase(\n      filesMatchedFraction = 0.05,\n      rowsMatchedFraction = 0.05))\n\n  def upsertTestCases: Seq[MergeTestCase] = Seq(\n    Seq(0.0, 0.01, 0.1).map { rowsMatchedFraction =>\n      UpsertTestCase(\n        filesMatchedFraction = 0.05,\n        rowsMatchedFraction,\n        rowsNotMatchedFraction = 0.1)\n    },\n\n    Seq(0.5, 0.99, 1.0).map { rowsMatchedFraction =>\n      UpsertTestCase(\n        filesMatchedFraction = 0.05,\n        rowsMatchedFraction,\n        rowsNotMatchedFraction = 0.001)\n    },\n\n    Seq(\n      UpsertTestCase(\n        filesMatchedFraction = 0.05,\n        rowsMatchedFraction = 0.1,\n        rowsNotMatchedFraction = 0.0),\n\n      UpsertTestCase(\n        filesMatchedFraction = 0.5,\n        rowsMatchedFraction = 0.01,\n        rowsNotMatchedFraction = 0.001),\n\n      UpsertTestCase(\n        filesMatchedFraction = 1.0,\n        rowsMatchedFraction = 0.01,\n        rowsNotMatchedFraction = 0.001)\n    )\n  ).flatten\n}\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/TPCDSBenchmark.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\nimport benchmark.TPCDSBenchmarkQueries._\n\ntrait TPCDSConf extends BenchmarkConf {\n  protected def format: Option[String]\n  def scaleInGB: Int\n  def userDefinedDbName: Option[String]\n\n  def formatName: String = format.getOrElse {\n    throw new IllegalArgumentException(\"format must be specified\")\n  }\n  def dbName: String = userDefinedDbName.getOrElse(s\"tpcds_sf${scaleInGB}_${formatName}\")\n  def dbLocation: String = dbLocation(dbName)\n}\n\ncase class TPCDSBenchmarkConf(\n     protected val format: Option[String] = None,\n     scaleInGB: Int = 0,\n     userDefinedDbName: Option[String] = None,\n     iterations: Int = 3,\n     benchmarkPath: Option[String] = None) extends TPCDSConf\n\nobject TPCDSBenchmarkConf {\n  import scopt.OParser\n  private val builder = OParser.builder[TPCDSBenchmarkConf]\n  private val argParser = {\n    import builder._\n    OParser.sequence(\n      programName(\"TPC-DS Benchmark\"),\n      opt[String](\"format\")\n        .required()\n        .action((x, c) => c.copy(format = Some(x)))\n        .text(\"Spark's short name for the file format to use\"),\n      opt[String](\"scale-in-gb\")\n        .required()\n        .valueName(\"<scale of benchmark in GBs>\")\n        .action((x, c) => c.copy(scaleInGB = x.toInt))\n        .text(\"Scale factor of the TPCDS benchmark\"),\n      opt[String](\"benchmark-path\")\n        .required()\n        .valueName(\"<cloud storage path>\")\n        .action((x, c) => c.copy(benchmarkPath = Some(x)))\n        .text(\"Cloud path to be used for creating table and generating reports\"),\n      opt[String](\"iterations\")\n        .optional()\n        .valueName(\"<number of iterations>\")\n        .action((x, c) => c.copy(iterations = x.toInt))\n        .text(\"Number of times to run the queries\"),\n    )\n  }\n\n  def parse(args: Array[String]): Option[TPCDSBenchmarkConf] = {\n    OParser.parse(argParser, args, TPCDSBenchmarkConf())\n  }\n}\n\nclass TPCDSBenchmark(conf: TPCDSBenchmarkConf) extends Benchmark(conf) {\n  val queries: Map[String, String] = {\n    if (conf.scaleInGB <= 3000) TPCDSQueries3TB\n    else if (conf.scaleInGB == 10) TPCDSQueries10TB\n    else throw new IllegalArgumentException(\n      s\"Unsupported scale factor of ${conf.scaleInGB} GB\")\n  }\n\n  val dbName = conf.dbName\n\n  def runInternal(): Unit = {\n    for ((k, v) <- extraConfs) spark.conf.set(k, v)\n    spark.sparkContext.setLogLevel(\"WARN\")\n    log(\"All configs:\\n\\t\" + spark.conf.getAll.toSeq.sortBy(_._1).mkString(\"\\n\\t\"))\n    spark.sql(s\"USE $dbName\")\n    for (iteration <- 1 to conf.iterations) {\n      queries.toSeq.sortBy(_._1).foreach { case (name, sql) =>\n        runQuery(sql, iteration = Some(iteration), queryName = name)\n      }\n    }\n    val results = getQueryResults().filter(_.name.startsWith(\"q\"))\n    if (results.forall(x => x.errorMsg.isEmpty && x.durationMs.nonEmpty) ) {\n      val medianDurationSecPerQuery = results.groupBy(_.name).map { case (q, results) =>\n        assert(results.size == conf.iterations)\n        val medianMs = results.map(_.durationMs.get).sorted\n            .drop(math.floor(conf.iterations / 2.0).toInt).head\n        (q, medianMs / 1000.0)\n      }\n      val sumOfMedians = medianDurationSecPerQuery.map(_._2).sum\n      reportExtraMetric(\"tpcds-result-seconds\", sumOfMedians)\n    }\n  }\n}\n\nobject TPCDSBenchmark {\n  def main(args: Array[String]): Unit = {\n    TPCDSBenchmarkConf.parse(args).foreach { conf =>\n      new TPCDSBenchmark(conf).run()\n    }\n  }\n}\n\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/TPCDSBenchmarkQueries.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\nobject TPCDSBenchmarkQueries {\n  val TPCDSQueries3TB = Map(\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'TN'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 2972190\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 111711138\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 127958920\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 41162107\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 25211875\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Allen County','Jefferson County','Lamar County','Dakota County','Park County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2001 and\n                d_moy between 4 and 4+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_moy between 4 ANd 4+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_moy between 4 and 4+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_login\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 1998\n         and t_s_secyear.dyear = 1998+1\n         and t_w_firstyear.dyear = 1998\n         and t_w_secyear.dyear = 1998+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_login\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Men', 'Books', 'Children')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('1998-03-28' as date)\n\t\t\t\tand (cast('1998-03-28' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'U'\n  and cd_education_status = 'Unknown'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'W'\n  and cd_education_status = '2 yr Degree'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'S'\n  and cd_education_status = 'College'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('WV', 'GA', 'TX')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('TN', 'KY', 'SC')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('OK', 'NE', 'CA')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998 + 1\n                       and d_moy = 12\n                       and d_dom = 20)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998\n                       and d_moy = 12\n                       and d_dom = 20)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 2000\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '2001-2-01' and\n           (cast('2001-2-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_skq\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'MS'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Jackson County','Daviess County','Walker County','Dauphin County',\n                  'Mobile County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '1999Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'F' and\n       cd1.cd_education_status = 'Primary' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (6,7,3,11,12,8) and\n       d_year = 1999 and\n       ca_state in ('IL','WV','KS'\n                   ,'GA','LA','PA','TX')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=26\n   and d_moy=12\n   and d_year=2000\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Books', 'Home', 'Jewelry')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('1998-05-08' as date)\n \t\t\t\tand (cast('1998-05-08' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('2000-05-22' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('2000-05-22' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('2000-05-22' as date) - INTERVAL 30 days)\n                    and (cast ('2000-05-22' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1199 and 1199 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000+1,2000+2,2000+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 5\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 5\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000 + 1,2000 + 2,2000 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 2000\n         and d_moy = 5\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 5\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'navy'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'beige'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,sum(ss_net_profit) as store_sales_profit\n ,sum(sr_net_loss) as store_returns_loss\n ,sum(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2002\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2002\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2002\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'M' and\n       cd_education_status = '2 yr Degree' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2002\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'S' and\n       cd_education_status = 'Advanced Degree' and\n       d_year = 2000 and\n       s_state in ('WA','LA', 'LA', 'TX', 'AL', 'PA')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 189 and 189+10\n             or ss_coupon_amt between 4483 and 4483+1000\n             or ss_wholesale_cost between 24 and 24+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 71 and 71+10\n          or ss_coupon_amt between 14775 and 14775+1000\n          or ss_wholesale_cost between 38 and 38+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 183 and 183+10\n          or ss_coupon_amt between 13456 and 13456+1000\n          or ss_wholesale_cost between 31 and 31+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 135 and 135+10\n          or ss_coupon_amt between 4905 and 4905+1000\n          or ss_wholesale_cost between 27 and 27+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 180 and 180+10\n          or ss_coupon_amt between 17430 and 17430+1000\n          or ss_wholesale_cost between 57 and 57+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 49 and 49+10\n          or ss_coupon_amt between 2950 and 2950+1000\n          or ss_wholesale_cost between 52 and 52+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,stddev_samp(ss_quantity)        as store_sales_quantity\n    ,stddev_samp(sr_return_quantity) as store_returns_quantity\n    ,stddev_samp(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 1998\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 1998\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (1998,1998+1,1998+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'GA'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 1998\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 1998\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 1998\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 1998\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 1998\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =1998\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by ss1.ca_county\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 948\nand i_item_sk = cs_item_sk\nand d_date between '1998-02-03' and\n        (cast('1998-02-03' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '1998-02-03' and\n                             (cast('1998-02-03' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 2\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 2\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 2\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = '5001-10000')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Jefferson Davis Parish','Levy County','Coal County','Oglethorpe County',\n                           'Mobile County','Gage County','Richland County','Gogebic County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  stddev_samp(cd_dep_count),\n  stddev_samp(cd_dep_count),\n  min(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  stddev_samp(cd_dep_employed_count),\n  stddev_samp(cd_dep_employed_count),\n  min(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  stddev_samp(cd_dep_college_count),\n  stddev_samp(cd_dep_college_count),\n  min(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2002 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 1998\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('OH','WV','PA','TN',\n                 'MN','MO','NM','MI')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 35 and 35 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-01-20' as date) and (cast('2001-01-20' as date) + interval 60 days)\n and i_manufact_id in (928,715,942,861)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1222 and 1222 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1222 and 1222 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1222 and 1222 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1998\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=4\n  and inv2.d_moy=4+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1998\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=4\n  and inv2.d_moy=4+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('1999-02-02' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('1999-02-02' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('1999-02-02' as date) - INTERVAL 30 days)\n                and (cast ('1999-02-02' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 732 and 732+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'beige' or i_color = 'spring') and\n        (i_units = 'Tsp' or i_units = 'Ton') and\n        (i_size = 'petite' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'white' or i_color = 'pale') and\n        (i_units = 'Box' or i_units = 'Dram') and\n        (i_size = 'large' or i_size = 'economy')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'midnight' or i_color = 'frosted') and\n        (i_units = 'Bunch' or i_units = 'Carton') and\n        (i_size = 'small' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'azure' or i_color = 'goldenrod') and\n        (i_units = 'Pallet' or i_units = 'Gross') and\n        (i_size = 'petite' or i_size = 'extra large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'brown' or i_color = 'hot') and\n        (i_units = 'Tbl' or i_units = 'Cup') and\n        (i_size = 'petite' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'powder' or i_color = 'honeydew') and\n        (i_units = 'Bundle' or i_units = 'Unknown') and\n        (i_size = 'large' or i_size = 'economy')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'antique' or i_color = 'purple') and\n        (i_units = 'N/A' or i_units = 'Dozen') and\n        (i_size = 'small' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'lavender' or i_color = 'tomato') and\n        (i_units = 'Lb' or i_units = 'Oz') and\n        (i_size = 'petite' or i_size = 'extra large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=11\n \tand dt.d_year=2002\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -6 and\n       d_year = 1999\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 321\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 321\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 321\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 321\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_county, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 2 and d_year = 1999\n group by ca_zip, ca_county\n order by ca_zip, ca_county\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 2 or\n         household_demographics.hd_vehicle_count= 2)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_city in ('Antioch','Mount Vernon','Jamestown','Wilson','Farmington')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 2001 or\n         ( d_year = 2001-1 and d_moy =12) or\n         ( d_year = 2001+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.s_company_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2001 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, avg_monthly_sales\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 1999\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'D'\n   and\n   cd_education_status = 'College'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'W'\n   and\n   cd_education_status = 'Secondary'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'M'\n   and\n   cd_education_status = '2 yr Degree'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('NE', 'IA', 'NY')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('IN', 'TN', 'OH')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('KS', 'CA', 'CO')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 1999\nand d2.d_moy  = 9\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1176 and 1176+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1176 and 1176+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=11\n    and dt.d_year=2001\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1218,1218+1,1218+2,1218+3,1218+4,1218+5,1218+6,1218+7,1218+8,1218+9,1218+10,1218+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Music'\n         and i_class = 'country'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 7\n         and d_year = 2001\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 2001 and d_moy = 7)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 2001 and d_moy = 7)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=87\n \tand d_moy=11\n \tand d_year=2001\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('tan','lace','gainsboro'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 3\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('tan','lace','gainsboro'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 3\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('tan','lace','gainsboro'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 3\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 2001 or\n         ( d_year = 2001-1 and d_moy =12) or\n         ( d_year = 2001+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand, v1.cc_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2001 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, avg_monthly_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-03-26'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-03-26'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '2000-03-26'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1199 and 1199 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1199+ 12 and 1199 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Men'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 9\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Men'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 9\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Men'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 9\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Electronics'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -7\n   and   d_year = 2001\n   and   d_moy  = 11) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Electronics'\n   and   s_gmt_offset = -7\n   and   d_year = 2001\n   and   d_moy  = 11) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1194 and 1194 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1205,1205+1,1205+2,1205+3,1205+4,1205+5,1205+6,1205+7,1205+8,1205+9,1205+10,1205+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('peach','misty','drab','chocolate','almond','saddle') and\n         i_current_price between 75 and 75 + 10 and\n         i_current_price between 75 + 1 and 75 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 2000 and\n     cs2.syear = 2000 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1208 and 1208+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1208 and 1208+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'HARMSTORF' || ',' || 'USPS' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_sales_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_sales_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_sales_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_sales_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_sales_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_sales_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_sales_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_sales_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_sales_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 24285 and 24285+28800\n \tand sm_carrier in ('HARMSTORF','USPS')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'HARMSTORF' || ',' || 'USPS' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_paid * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_paid * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_paid * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_paid * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_paid * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_paid * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_paid * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_paid * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_paid * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_paid * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_paid * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_paid * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 24285 AND 24285+28800\n \tand sm_carrier in ('HARMSTORF','USPS')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1196 and 1196+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 1 or\n             household_demographics.hd_vehicle_count= -1)\n        and date_dim.d_year in (1998,1998+1,1998+2)\n        and store.s_city in ('Bethel','Summit')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('OK','GA','VA') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2004 and\n                d_moy between 4 and 4+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2004 and\n                  d_moy between 4 and 4+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2004 and\n                  d_moy between 4 and 4+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1197 and 1197+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1197 and 1197+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=12\n                   and d_year=1999\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=12\n                   and d_year=1999\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=12\n                   and d_year=1999\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '>10000'\n  and d1.d_year = 2002\n  and cd_marital_status = 'D'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '501-1000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Franklin Parish','Ziebach County','Luce County','Williamson County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 2001\n         and t_s_secyear.year = 2001+1\n         and t_w_firstyear.year = 2001\n         and t_w_secyear.year = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 3,1,2\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Sports') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_cdemo_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_cdemo_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_ship_hdemo_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_ship_hdemo_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_ship_customer_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_ship_customer_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_sold_year, ss_item_sk, ss_customer_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2002\norder by\n  ss_sold_year, ss_item_sk, ss_customer_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 0 or household_demographics.hd_vehicle_count > 0)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('1999-08-12' as date)\n                  and (cast('1999-08-12' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('1999-08-12' as date)\n                  and (cast('1999-08-12' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('1999-08-12' as date)\n                  and (cast('1999-08-12' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =2001\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'NC'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 82 and 82+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2002-03-10' as date) and (cast('2002-03-10' as date) +  INTERVAL 60 days)\n and i_manufact_id in (941,920,105,693)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('1999-04-14','1999-09-28','1999-11-12')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('1999-04-14','1999-09-28','1999-11-12')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('1999-04-14','1999-09-28','1999-11-12')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Antioch'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  55019\n   and ib_upper_bound   <=  55019 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'S'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '2 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Advanced Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'W'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '4 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('OK', 'TX', 'MO')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('GA', 'KS', 'NC')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('VA', 'WI', 'WV')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1180 and 1180+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1204 and 1204+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1204 and 1204+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1204 and 1204+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2001) and\n        ((i_category in ('Women','Music','Home') and\n          i_class in ('fragrances','pop','bedding')\n         )\n      or (i_category in ('Books','Men','Children') and\n          i_class in ('home repair','sports-apparel','infants')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 8 and 8+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 19 and 19+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 2002\nand     d_moy                   = 11\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like '5001-10000%'\nand     ca_gmt_offset           = -6\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 561\nand i_item_sk = ws_item_sk\nand d_date between '2001-03-13' and\n        (cast('2001-03-13' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '2001-03-13' and\n                             (cast('2001-03-13' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 64') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-5-01' and\n           (cast('2001-5-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'TX'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2000-3-01' and\n           (cast('2000-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'TN'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 16\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 4\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1209 and 1209 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1209 and 1209 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Jewelry', 'Home', 'Shoes')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2001-04-12' as date)\n\t\t\t\tand (cast('2001-04-12' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1203 and 1203 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\",\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'NM'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 98972190\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 160856845\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 12733327\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 96251173\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 80049606\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Fillmore County','McPherson County','Bonneville County','Boone County','Brown County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2000 and\n                d_moy between 3 and 3+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 3 ANd 3+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 3 and 3+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 1999\n         and t_s_secyear.dyear = 1999+1\n         and t_w_firstyear.dyear = 1999\n         and t_w_secyear.dyear = 1999+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Electronics', 'Books', 'Women')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('1998-01-06' as date)\n\t\t\t\tand (cast('1998-01-06' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'U'\n  and cd_education_status = 'Secondary'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'W'\n  and cd_education_status = 'College'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'D'\n  and cd_education_status = 'Primary'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('TX', 'OK', 'MI')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('WA', 'NC', 'OH')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('MT', 'FL', 'GA')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 2000 AND 2000 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 2000 AND 2000 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 2000 AND 2000 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 2000+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 2000+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 2000+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 2000 AND 2000 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 2000 AND 2000 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 2000 AND 2000 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 2000 + 1\n                       and d_moy = 12\n                       and d_dom = 15)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 2000\n                       and d_moy = 12\n                       and d_dom = 15)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 2 and d_year = 1998\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '1999-4-01' and\n           (cast('1999-4-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'IL'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Richland County','Bronx County','Maverick County','Mesa County',\n                  'Raleigh County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '2000Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('2000Q1','2000Q2','2000Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('2000Q1','2000Q2','2000Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'M' and\n       cd1.cd_education_status = 'Unknown' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (5,1,4,7,8,9) and\n       d_year = 2002 and\n       ca_state in ('AR','TX','NC'\n                   ,'GA','MS','WV','AL')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=16\n   and d_moy=12\n   and d_year=1998\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Shoes', 'Electronics', 'Children')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('2001-03-14' as date)\n \t\t\t\tand (cast('2001-03-14' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('1999-03-20' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('1999-03-20' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('1999-03-20' as date) - INTERVAL 30 days)\n                    and (cast ('1999-03-20' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1186 and 1186 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000+1,2000+2,2000+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 3\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 3\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000 + 1,2000 + 2,2000 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 2000\n         and d_moy = 3\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 3\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'snow'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'chiffon'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,sum(ss_net_profit) as store_sales_profit\n ,sum(sr_net_loss) as store_returns_loss\n ,sum(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2000\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2000\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2000\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'S' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 1998\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'U' and\n       cd_education_status = '2 yr Degree' and\n       d_year = 2000 and\n       s_state in ('AL','IN', 'SC', 'NY', 'OH', 'FL')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 73 and 73+10\n             or ss_coupon_amt between 7826 and 7826+1000\n             or ss_wholesale_cost between 70 and 70+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 152 and 152+10\n          or ss_coupon_amt between 2196 and 2196+1000\n          or ss_wholesale_cost between 56 and 56+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 53 and 53+10\n          or ss_coupon_amt between 3430 and 3430+1000\n          or ss_wholesale_cost between 13 and 13+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 182 and 182+10\n          or ss_coupon_amt between 3262 and 3262+1000\n          or ss_wholesale_cost between 20 and 20+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 85 and 85+10\n          or ss_coupon_amt between 3310 and 3310+1000\n          or ss_wholesale_cost between 37 and 37+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 180 and 180+10\n          or ss_coupon_amt between 12592 and 12592+1000\n          or ss_wholesale_cost between 22 and 22+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,stddev_samp(ss_quantity)        as store_sales_quantity\n    ,stddev_samp(sr_return_quantity) as store_returns_quantity\n    ,stddev_samp(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 1998\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 1998\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (1998,1998+1,1998+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'GA'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 1999\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 1999\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 1999\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 1999\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 1999\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =1999\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by ss1.d_year\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 66\nand i_item_sk = cs_item_sk\nand d_date between '2002-03-29' and\n        (cast('2002-03-29' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '2002-03-29' and\n                             (cast('2002-03-29' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Home'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 5\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Home'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 5\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Home'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 5\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_county in ('Salem County','Terrell County','Arthur County','Oglethorpe County',\n                           'Lunenburg County','Perry County','Halifax County','Sumner County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  avg(cd_dep_count),\n  min(cd_dep_count),\n  stddev_samp(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  avg(cd_dep_employed_count),\n  min(cd_dep_employed_count),\n  stddev_samp(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  avg(cd_dep_college_count),\n  min(cd_dep_college_count),\n  stddev_samp(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2001 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 1999\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('IN','AL','MI','MN',\n                 'TN','LA','FL','NM')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 39 and 39 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-01-16' as date) and (cast('2001-01-16' as date) + interval 60 days)\n and i_manufact_id in (765,886,889,728)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1186 and 1186 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1186 and 1186 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1186 and 1186 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=2\n  and inv2.d_moy=2+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=2\n  and inv2.d_moy=2+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('2000-03-18' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('2000-03-18' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('2000-03-18' as date) - INTERVAL 30 days)\n                and (cast ('2000-03-18' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 970 and 970+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'frosted' or i_color = 'rose') and\n        (i_units = 'Lb' or i_units = 'Gross') and\n        (i_size = 'medium' or i_size = 'large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'chocolate' or i_color = 'black') and\n        (i_units = 'Box' or i_units = 'Dram') and\n        (i_size = 'economy' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'slate' or i_color = 'magenta') and\n        (i_units = 'Carton' or i_units = 'Bundle') and\n        (i_size = 'N/A' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'cornflower' or i_color = 'firebrick') and\n        (i_units = 'Pound' or i_units = 'Oz') and\n        (i_size = 'medium' or i_size = 'large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'almond' or i_color = 'steel') and\n        (i_units = 'Tsp' or i_units = 'Case') and\n        (i_size = 'medium' or i_size = 'large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'purple' or i_color = 'aquamarine') and\n        (i_units = 'Bunch' or i_units = 'Gram') and\n        (i_size = 'economy' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'lavender' or i_color = 'papaya') and\n        (i_units = 'Pallet' or i_units = 'Cup') and\n        (i_size = 'N/A' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'maroon' or i_color = 'cyan') and\n        (i_units = 'Each' or i_units = 'N/A') and\n        (i_size = 'medium' or i_size = 'large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=12\n \tand dt.d_year=1998\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -6 and\n       d_year = 2001\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 366\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 366\n                                                    and ss_cdemo_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 366\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 366\n                                                    and ss_cdemo_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_county, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 1998\n group by ca_zip, ca_county\n order by ca_zip, ca_county\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 0 or\n         household_demographics.hd_vehicle_count= 1)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_city in ('Five Forks','Oakland','Fairview','Winchester','Farmington')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 1999 or\n         ( d_year = 1999-1 and d_moy =12) or\n         ( d_year = 1999+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.s_store_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 1999 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 1998\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'M'\n   and\n   cd_education_status = 'Unknown'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'W'\n   and\n   cd_education_status = 'College'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'D'\n   and\n   cd_education_status = 'Primary'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('MI', 'GA', 'NH')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('TX', 'KY', 'SD')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('NY', 'OH', 'FL')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 12\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 12\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 12\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 1998\nand d2.d_moy  = 9\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1214 and 1214+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1214 and 1214+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=12\n    and dt.d_year=2000\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1212,1212+1,1212+2,1212+3,1212+4,1212+5,1212+6,1212+7,1212+8,1212+9,1212+10,1212+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Books'\n         and i_class = 'business'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 2\n         and d_year = 2000\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 2000 and d_moy = 2)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 2000 and d_moy = 2)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=13\n \tand d_moy=11\n \tand d_year=1999\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('chiffon','smoke','lace'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 5\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('chiffon','smoke','lace'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 5\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('chiffon','smoke','lace'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 5\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 1999 or\n         ( d_year = 1999-1 and d_moy =12) or\n         ( d_year = 1999+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 1999 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, avg_monthly_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '1998-02-21'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '1998-02-21'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '1998-02-21'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1205 and 1205 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1205+ 12 and 1205 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Children'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Children'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Children'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Sports'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -6\n   and   d_year = 2001\n   and   d_moy  = 12) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Sports'\n   and   s_gmt_offset = -6\n   and   d_year = 2001\n   and   d_moy  = 12) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1215 and 1215 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1211,1211+1,1211+2,1211+3,1211+4,1211+5,1211+6,1211+7,1211+8,1211+9,1211+10,1211+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('azure','gainsboro','misty','blush','hot','lemon') and\n         i_current_price between 80 and 80 + 10 and\n         i_current_price between 80 + 1 and 80 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 1999 and\n     cs2.syear = 1999 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1186 and 1186+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1186 and 1186+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'MSC' || ',' || 'GERMA' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_sales_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_sales_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_sales_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_sales_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_sales_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_sales_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_sales_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_sales_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_sales_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2001\n \tand t_time between 9453 and 9453+28800\n \tand sm_carrier in ('MSC','GERMA')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'MSC' || ',' || 'GERMA' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2001\n \tand t_time between 9453 AND 9453+28800\n \tand sm_carrier in ('MSC','GERMA')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1185 and 1185+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 4 or\n             household_demographics.hd_vehicle_count= 0)\n        and date_dim.d_year in (1999,1999+1,1999+2)\n        and store.s_city in ('Pleasant Hill','Bethel')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('MO','MN','AZ') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2003 and\n                d_moy between 2 and 2+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1218 and 1218+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1218 and 1218+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=12\n                   and d_year=2000\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=12\n                   and d_year=2000\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=12\n                   and d_year=2000\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '1001-5000'\n  and d1.d_year = 2000\n  and cd_marital_status = 'D'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = '5001-10000')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_county in ('Lea County','Furnas County','Pennington County','Bronx County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,sum(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (1998,1998+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,sum(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (1998,1998+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 1998\n         and t_s_secyear.year = 1998+1\n         and t_w_firstyear.year = 1998\n         and t_w_secyear.year = 1998+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 3,1,2\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Sports') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_customer_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_customer_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_ship_addr_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_ship_addr_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_ship_mode_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_ship_mode_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_customer_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2001\norder by\n  ss_customer_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 0 or household_demographics.hd_vehicle_count > 3)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2002-08-06' as date)\n                  and (cast('2002-08-06' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2002-08-06' as date)\n                  and (cast('2002-08-06' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2002-08-06' as date)\n                  and (cast('2002-08-06' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =1998\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'TX'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 49 and 49+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-01-28' as date) and (cast('2001-01-28' as date) +  INTERVAL 60 days)\n and i_manufact_id in (80,675,292,17)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-06-17','2000-08-22','2000-11-17')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-06-17','2000-08-22','2000-11-17')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('2000-06-17','2000-08-22','2000-11-17')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Hopewell'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  37855\n   and ib_upper_bound   <=  37855 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'M'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '4 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'S'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'College'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Secondary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('TX', 'VA', 'CA')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('AR', 'NE', 'MO')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('IA', 'MS', 'WA')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1215 and 1215+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1221 and 1221+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1221 and 1221+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1221 and 1221+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2000) and\n        ((i_category in ('Home','Music','Books') and\n          i_class in ('glassware','classical','fiction')\n         )\n      or (i_category in ('Jewelry','Sports','Women') and\n          i_class in ('semi-precious','baseball','dresses')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 9 and 9+1\n         and household_demographics.hd_dep_count = 3\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 16 and 16+1\n         and household_demographics.hd_dep_count = 3\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 2000\nand     d_moy                   = 12\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like 'Unknown%'\nand     ca_gmt_offset           = -7\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 356\nand i_item_sk = ws_item_sk\nand d_date between '2001-03-12' and\n        (cast('2001-03-12' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '2001-03-12' and\n                             (cast('2001-03-12' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 66') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '1999-4-01' and\n           (cast('1999-4-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'NE'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2002-4-01' and\n           (cast('2002-4-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'AL'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 16\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 6\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1190 and 1190 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1190 and 1190 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Home', 'Sports', 'Men')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2002-01-05' as date)\n\t\t\t\tand (cast('2002-01-05' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1178 and 1178 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\",\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'NY'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 578972190\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 536856786\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 12733327\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 205136171\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 1192341092\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Baltimore city','Stafford County','Greene County','Ballard County','Franklin County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2000 and\n                d_moy between 1 and 1+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 1 ANd 1+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 1 and 1+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_preferred_cust_flag\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 2001\n         and t_s_secyear.dyear = 2001+1\n         and t_w_firstyear.dyear = 2001\n         and t_w_secyear.dyear = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_preferred_cust_flag\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Children', 'Shoes', 'Women')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('1998-06-19' as date)\n\t\t\t\tand (cast('1998-06-19' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'D'\n  and cd_education_status = '2 yr Degree'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'U'\n  and cd_education_status = 'College'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'S'\n  and cd_education_status = 'Primary'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('GA', 'IN', 'NY')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('ND', 'WV', 'TX')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('KS', 'NC', 'NM')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998 + 1\n                       and d_moy = 12\n                       and d_dom = 17)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998\n                       and d_moy = 12\n                       and d_dom = 17)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 2002\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '2001-3-01' and\n           (cast('2001-3-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'PA'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Luce County','Franklin Parish','Sierra County','Williamson County',\n                  'Kittitas County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '2001Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('2001Q1','2001Q2','2001Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('2001Q1','2001Q2','2001Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'M' and\n       cd1.cd_education_status = 'Unknown' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (5,7,8,6,12,4) and\n       d_year = 2000 and\n       ca_state in ('MO','NY','ME'\n                   ,'MI','IA','OH','MS')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=55\n   and d_moy=11\n   and d_year=1998\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Shoes', 'Electronics', 'Home')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('2000-05-15' as date)\n \t\t\t\tand (cast('2000-05-15' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('2002-02-15' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('2002-02-15' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('2002-02-15' as date) - INTERVAL 30 days)\n                    and (cast ('2002-02-15' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1202 and 1202 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000+1,2000+2,2000+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 4\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 4\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000 + 1,2000 + 2,2000 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 2000\n         and d_moy = 4\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 4\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=5\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'cyan'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 5\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'ivory'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,stddev_samp(ss_net_profit) as store_sales_profit\n ,stddev_samp(sr_net_loss) as store_returns_loss\n ,stddev_samp(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2000\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2000\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2000\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'M' and\n       cd_education_status = 'Unknown' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'D' and\n       cd_education_status = '2 yr Degree' and\n       d_year = 1999 and\n       s_state in ('MI','WV', 'MI', 'NY', 'TN', 'MI')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 151 and 151+10\n             or ss_coupon_amt between 4349 and 4349+1000\n             or ss_wholesale_cost between 75 and 75+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 45 and 45+10\n          or ss_coupon_amt between 12490 and 12490+1000\n          or ss_wholesale_cost between 37 and 37+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 54 and 54+10\n          or ss_coupon_amt between 13038 and 13038+1000\n          or ss_wholesale_cost between 17 and 17+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 178 and 178+10\n          or ss_coupon_amt between 10744 and 10744+1000\n          or ss_wholesale_cost between 51 and 51+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 49 and 49+10\n          or ss_coupon_amt between 8494 and 8494+1000\n          or ss_wholesale_cost between 56 and 56+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 0 and 0+10\n          or ss_coupon_amt between 17854 and 17854+1000\n          or ss_wholesale_cost between 31 and 31+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,max(ss_quantity)        as store_sales_quantity\n    ,max(sr_return_quantity) as store_returns_quantity\n    ,max(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 1999\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 1999\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (1999,1999+1,1999+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'MD'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 1999\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 1999\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 1999\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 1999\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 1999\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =1999\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by store_q2_q3_increase\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 7\nand i_item_sk = cs_item_sk\nand d_date between '2000-01-21' and\n        (cast('2000-01-21' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '2000-01-21' and\n                             (cast('2000-01-21' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Books'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Books'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Books'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '501-1000' or\n         household_demographics.hd_buy_potential = '5001-10000')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Levy County','Val Verde County','Porter County','Nowata County',\n                           'Lincoln County','Brazos County','Franklin Parish','Pipestone County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  sum(cd_dep_count),\n  sum(cd_dep_count),\n  sum(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  sum(cd_dep_employed_count),\n  sum(cd_dep_employed_count),\n  sum(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  sum(cd_dep_college_count),\n  sum(cd_dep_college_count),\n  sum(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2001 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 1999\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('MO','AL','OH','WV',\n                 'AL','MN','TN','WA')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 57 and 57 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-04-19' as date) and (cast('2001-04-19' as date) + interval 60 days)\n and i_manufact_id in (804,916,707,680)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1189 and 1189 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1189 and 1189 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1189 and 1189 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('2000-04-09' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('2000-04-09' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('2000-04-09' as date) - INTERVAL 30 days)\n                and (cast ('2000-04-09' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 917 and 917+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'antique' or i_color = 'pale') and\n        (i_units = 'Tbl' or i_units = 'Case') and\n        (i_size = 'small' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'snow' or i_color = 'lemon') and\n        (i_units = 'Box' or i_units = 'Ounce') and\n        (i_size = 'economy' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'green' or i_color = 'blue') and\n        (i_units = 'Gross' or i_units = 'Ton') and\n        (i_size = 'large' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'cream' or i_color = 'frosted') and\n        (i_units = 'Bundle' or i_units = 'Gram') and\n        (i_size = 'small' or i_size = 'extra large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'orange' or i_color = 'spring') and\n        (i_units = 'Lb' or i_units = 'Carton') and\n        (i_size = 'small' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'lawn' or i_color = 'violet') and\n        (i_units = 'Oz' or i_units = 'Cup') and\n        (i_size = 'economy' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'navy' or i_color = 'linen') and\n        (i_units = 'Pound' or i_units = 'Unknown') and\n        (i_size = 'large' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'almond' or i_color = 'olive') and\n        (i_units = 'Pallet' or i_units = 'Bunch') and\n        (i_size = 'small' or i_size = 'extra large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=11\n \tand dt.d_year=1998\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -6 and\n       d_year = 2000\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 731\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 731\n                                                    and ss_promo_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 731\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 731\n                                                    and ss_promo_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_city, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 2000\n group by ca_zip, ca_city\n order by ca_zip, ca_city\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 1 or\n         household_demographics.hd_vehicle_count= 2)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_city in ('Buena Vista','Friendship','Monroe','Oak Hill','Randolph')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 1999 or\n         ( d_year = 1999-1 and d_moy =12) or\n         ( d_year = 1999+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.i_category\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 1999 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 1999\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'S'\n   and\n   cd_education_status = 'Primary'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'D'\n   and\n   cd_education_status = 'College'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'U'\n   and\n   cd_education_status = '2 yr Degree'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('ND', 'NC', 'TX')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('VA', 'IA', 'AR')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('MA', 'FL', 'TN')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 2002\nand d2.d_moy  = 10\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1213 and 1213+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1213 and 1213+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=11\n    and dt.d_year=1998\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1219,1219+1,1219+2,1219+3,1219+4,1219+5,1219+6,1219+7,1219+8,1219+9,1219+10,1219+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Men'\n         and i_class = 'shirts'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 2\n         and d_year = 1999\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 1999 and d_moy = 2)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 1999 and d_moy = 2)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=96\n \tand d_moy=11\n \tand d_year=2000\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('antique','white','smoke'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 6\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('antique','white','smoke'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 6\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('antique','white','smoke'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 6\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 2000 or\n         ( d_year = 2000-1 and d_moy =12) or\n         ( d_year = 2000+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand, v1.cc_name\n        ,v1.d_year\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2000 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2001-01-27'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2001-01-27'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '2001-01-27'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1177 and 1177 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1177+ 12 and 1177 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2002\n and     d_moy                   = 8\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2002\n and     d_moy                   = 8\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2002\n and     d_moy                   = 8\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Home'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -6\n   and   d_year = 1999\n   and   d_moy  = 12) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Home'\n   and   s_gmt_offset = -6\n   and   d_year = 1999\n   and   d_moy  = 12) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1191 and 1191 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1193,1193+1,1193+2,1193+3,1193+4,1193+5,1193+6,1193+7,1193+8,1193+9,1193+10,1193+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('orange','aquamarine','olive','linen','smoke','coral') and\n         i_current_price between 74 and 74 + 10 and\n         i_current_price between 74 + 1 and 74 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 2001 and\n     cs2.syear = 2001 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1195 and 1195+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1195 and 1195+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'LATVIAN' || ',' || 'ALLIANCE' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 1998\n \tand t_time between 16224 and 16224+28800\n \tand sm_carrier in ('LATVIAN','ALLIANCE')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'LATVIAN' || ',' || 'ALLIANCE' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 1998\n \tand t_time between 16224 AND 16224+28800\n \tand sm_carrier in ('LATVIAN','ALLIANCE')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1203 and 1203+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 3 or\n             household_demographics.hd_vehicle_count= -1)\n        and date_dim.d_year in (1999,1999+1,1999+2)\n        and store.s_city in ('Jamestown','Pine Hill')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('CA','MT','SD') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2003 and\n                d_moy between 2 and 2+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1215 and 1215+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1215 and 1215+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=11\n                   and d_year=1998\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=11\n                   and d_year=1998\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=11\n                   and d_year=1998\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '1001-5000'\n  and d1.d_year = 1998\n  and cd_marital_status = 'S'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_county in ('Van Buren County','Terrell County','Belknap County','Kootenai County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 2001\n         and t_s_secyear.year = 2001+1\n         and t_w_firstyear.year = 2001\n         and t_w_secyear.year = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 2,3,1\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Music'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Music'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Music') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_promo_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_promo_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_web_site_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_web_site_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_bill_addr_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_bill_addr_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_sold_year, ss_item_sk, ss_customer_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2001\norder by\n  ss_sold_year, ss_item_sk, ss_customer_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 6 or household_demographics.hd_vehicle_count > -1)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-25' as date)\n                  and (cast('2000-08-25' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-25' as date)\n                  and (cast('2000-08-25' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-25' as date)\n                  and (cast('2000-08-25' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'SC'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 6 and 6+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-02-23' as date) and (cast('2001-02-23' as date) +  INTERVAL 60 days)\n and i_manufact_id in (669,623,578,379)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2001-01-15','2001-09-03','2001-11-17')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2001-01-15','2001-09-03','2001-11-17')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('2001-01-15','2001-09-03','2001-11-17')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Walnut Grove'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  53669\n   and ib_upper_bound   <=  53669 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'S'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Secondary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Advanced Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'W'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Primary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('AZ', 'SD', 'TN')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('TX', 'GA', 'IA')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('WI', 'VT', 'AL')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1195 and 1195+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1194 and 1194+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1194 and 1194+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1194 and 1194+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2000) and\n        ((i_category in ('Home','Shoes','Electronics') and\n          i_class in ('flatware','mens','televisions')\n         )\n      or (i_category in ('Women','Sports','Music') and\n          i_class in ('maternity','camping','rock')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 8 and 8+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 20 and 20+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 2001\nand     d_moy                   = 12\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like 'Unknown%'\nand     ca_gmt_offset           = -6\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 7\nand i_item_sk = ws_item_sk\nand d_date between '2000-01-16' and\n        (cast('2000-01-16' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '2000-01-16' and\n                             (cast('2000-01-16' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 24') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-2-01' and\n           (cast('2001-2-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'VT'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-3-01' and\n           (cast('2001-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'TN'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 20\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 6\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1206 and 1206 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1206 and 1206 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Sports', 'Books', 'Electronics')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2002-06-29' as date)\n\t\t\t\tand (cast('2002-06-29' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1199 and 1199 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\",\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'MO'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 4502397049\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 4756228269\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 4101835064\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 4583261513\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 4208819283\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Grady County','Marion County','Decatur County','Lyman County','Beaver County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 1999 and\n                d_moy between 2 and 2+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 2 ANd 2+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 2 and 2+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_preferred_cust_flag\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 2001\n         and t_s_secyear.dyear = 2001+1\n         and t_w_firstyear.dyear = 2001\n         and t_w_secyear.dyear = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_preferred_cust_flag\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Children', 'Jewelry', 'Music')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('2001-05-11' as date)\n\t\t\t\tand (cast('2001-05-11' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'M'\n  and cd_education_status = 'Primary'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'S'\n  and cd_education_status = '4 yr Degree'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'W'\n  and cd_education_status = '2 yr Degree'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('SC', 'WY', 'TX')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('NY', 'NE', 'GA')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('AL', 'AR', 'MI')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1999 AND 1999 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1999 AND 1999 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1999 AND 1999 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 1999+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 1999+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 1999+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1999 AND 1999 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1999 AND 1999 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1999 AND 1999 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1999 + 1\n                       and d_moy = 12\n                       and d_dom = 5)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1999\n                       and d_moy = 12\n                       and d_dom = 5)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 1998\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '2000-3-01' and\n           (cast('2000-3-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'IA'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Luce County','Wadena County','Jefferson Davis Parish','Daviess County',\n                  'Williamson County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '1999Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'F' and\n       cd1.cd_education_status = 'Unknown' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (4,8,12,10,11,9) and\n       d_year = 2001 and\n       ca_state in ('AR','IA','TX'\n                   ,'KS','LA','NC','SD')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=63\n   and d_moy=11\n   and d_year=2002\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Electronics', 'Children', 'Home')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('2002-03-19' as date)\n \t\t\t\tand (cast('2002-03-19' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('1999-04-12' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('1999-04-12' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('1999-04-12' as date) - INTERVAL 30 days)\n                    and (cast ('1999-04-12' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1188 and 1188 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (1998,1998+1,1998+2,1998+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (1998,1998+1,1998+2,1998+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 1998\n         and d_moy = 7\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 1998\n         and d_moy = 7\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (1998,1998 + 1,1998 + 2,1998 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (1998,1998+1,1998+2,1998+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 1998\n         and d_moy = 7\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 1998\n         and d_moy = 7\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=7\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'goldenrod'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 7\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'magenta'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,min(ss_net_profit) as store_sales_profit\n ,min(sr_net_loss) as store_returns_loss\n ,min(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2002\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2002\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2002\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'M' and\n       cd_education_status = '4 yr Degree' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 1998\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'M' and\n       cd_marital_status = 'M' and\n       cd_education_status = 'Secondary' and\n       d_year = 1999 and\n       s_state in ('AL','FL', 'TX', 'NM', 'MI', 'GA')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 74 and 74+10\n             or ss_coupon_amt between 2949 and 2949+1000\n             or ss_wholesale_cost between 49 and 49+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 136 and 136+10\n          or ss_coupon_amt between 10027 and 10027+1000\n          or ss_wholesale_cost between 53 and 53+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 73 and 73+10\n          or ss_coupon_amt between 1451 and 1451+1000\n          or ss_wholesale_cost between 78 and 78+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 87 and 87+10\n          or ss_coupon_amt between 17007 and 17007+1000\n          or ss_wholesale_cost between 55 and 55+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 112 and 112+10\n          or ss_coupon_amt between 17243 and 17243+1000\n          or ss_wholesale_cost between 2 and 2+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 119 and 119+10\n          or ss_coupon_amt between 4954 and 4954+1000\n          or ss_wholesale_cost between 22 and 22+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,stddev_samp(ss_quantity)        as store_sales_quantity\n    ,stddev_samp(sr_return_quantity) as store_returns_quantity\n    ,stddev_samp(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 2000\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 2000\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (2000,2000+1,2000+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2001\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'MI'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 2000\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 2000\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 2000\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 2000\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 2000\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =2000\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by store_q1_q2_increase\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 490\nand i_item_sk = cs_item_sk\nand d_date between '1999-01-27' and\n        (cast('1999-01-27' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '1999-01-27' and\n                             (cast('1999-01-27' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 1\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 1\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 1\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '1001-5000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Nez Perce County','Murray County','Surry County','Calhoun County',\n                           'Wilkinson County','Brown County','Wallace County','Carter County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  stddev_samp(cd_dep_count),\n  sum(cd_dep_count),\n  min(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  stddev_samp(cd_dep_employed_count),\n  sum(cd_dep_employed_count),\n  min(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  stddev_samp(cd_dep_college_count),\n  sum(cd_dep_college_count),\n  min(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2002 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 2000\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('MN','TX','TX','IN',\n                 'CA','LA','NM','TX')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 16 and 16 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2002-06-05' as date) and (cast('2002-06-05' as date) + interval 60 days)\n and i_manufact_id in (841,790,796,739)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1203 and 1203 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1203 and 1203 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1203 and 1203 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1999\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1999\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('1999-04-27' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('1999-04-27' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('1999-04-27' as date) - INTERVAL 30 days)\n                and (cast ('1999-04-27' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 841 and 841+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'bisque' or i_color = 'khaki') and\n        (i_units = 'Carton' or i_units = 'Box') and\n        (i_size = 'large' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'antique' or i_color = 'sandy') and\n        (i_units = 'Pallet' or i_units = 'Cup') and\n        (i_size = 'petite' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'forest' or i_color = 'brown') and\n        (i_units = 'Dram' or i_units = 'Ton') and\n        (i_size = 'economy' or i_size = 'medium')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'chartreuse' or i_color = 'light') and\n        (i_units = 'Pound' or i_units = 'Dozen') and\n        (i_size = 'large' or i_size = 'extra large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'turquoise' or i_color = 'chocolate') and\n        (i_units = 'Bundle' or i_units = 'Unknown') and\n        (i_size = 'large' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'maroon' or i_color = 'pale') and\n        (i_units = 'Each' or i_units = 'Tbl') and\n        (i_size = 'petite' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'almond' or i_color = 'floral') and\n        (i_units = 'Gross' or i_units = 'N/A') and\n        (i_size = 'economy' or i_size = 'medium')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'drab' or i_color = 'plum') and\n        (i_units = 'Bunch' or i_units = 'Case') and\n        (i_size = 'large' or i_size = 'extra large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=11\n \tand dt.d_year=2002\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -5 and\n       d_year = 2002\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 709\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 709\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 709\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 709\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_state, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 2 and d_year = 2002\n group by ca_zip, ca_state\n order by ca_zip, ca_state\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 0 or\n         household_demographics.hd_vehicle_count= 1)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_city in ('Johnson','Norwood','Cambridge','Klondike','Rock Hill')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 2001 or\n         ( d_year = 2001-1 and d_moy =12) or\n         ( d_year = 2001+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand, v1.s_store_name, v1.s_company_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2001 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, psum\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2000\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'U'\n   and\n   cd_education_status = '2 yr Degree'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'S'\n   and\n   cd_education_status = 'Primary'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'W'\n   and\n   cd_education_status = '4 yr Degree'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('MT', 'OH', 'GA')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('WV', 'AZ', 'NM')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('NY', 'PA', 'KY')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 1999\n                         and d_moy = 11\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 1999\n                         and d_moy = 11\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 1999\n                         and d_moy = 11\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 2000\nand d2.d_moy  = 9\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1177 and 1177+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1177 and 1177+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=12\n    and dt.d_year=2001\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1188,1188+1,1188+2,1188+3,1188+4,1188+5,1188+6,1188+7,1188+8,1188+9,1188+10,1188+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Men'\n         and i_class = 'pants'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 5\n         and d_year = 2002\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 2002 and d_moy = 5)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 2002 and d_moy = 5)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=67\n \tand d_moy=11\n \tand d_year=2001\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('blanched','spring','seashell'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('blanched','spring','seashell'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('blanched','spring','seashell'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 2000 or\n         ( d_year = 2000-1 and d_moy =12) or\n         ( d_year = 2000+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand\n        ,v1.d_year\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2000 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-05-24'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-05-24'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '2000-05-24'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1197 and 1197 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1197+ 12 and 1197 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Jewelry'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -7\n   and   d_year = 2002\n   and   d_moy  = 11) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Jewelry'\n   and   s_gmt_offset = -7\n   and   d_year = 2002\n   and   d_moy  = 11) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1194 and 1194 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1222,1222+1,1222+2,1222+3,1222+4,1222+5,1222+6,1222+7,1222+8,1222+9,1222+10,1222+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('ivory','purple','almond','bisque','lawn','azure') and\n         i_current_price between 60 and 60 + 10 and\n         i_current_price between 60 + 1 and 60 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 2001 and\n     cs2.syear = 2001 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1185 and 1185+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1185 and 1185+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'FEDEX' || ',' || 'MSC' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_profit * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_profit * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_profit * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_profit * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_profit * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_profit * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_profit * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_profit * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_profit * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_profit * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_profit * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_profit * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 2662 and 2662+28800\n \tand sm_carrier in ('FEDEX','MSC')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'FEDEX' || ',' || 'MSC' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_profit * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_profit * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_profit * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_profit * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_profit * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_profit * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_profit * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_profit * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_profit * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_profit * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_profit * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_profit * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 2662 AND 2662+28800\n \tand sm_carrier in ('FEDEX','MSC')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1177 and 1177+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 5 or\n             household_demographics.hd_vehicle_count= 4)\n        and date_dim.d_year in (1999,1999+1,1999+2)\n        and store.s_city in ('Lodi','Richmond')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('IL','FL','SD') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 1999 and\n                d_moy between 1 and 1+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 1 and 1+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 1 and 1+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1206 and 1206+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1206 and 1206+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=11\n                   and d_year=1999\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=11\n                   and d_year=1999\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=11\n                   and d_year=1999\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '1001-5000'\n  and d1.d_year = 2000\n  and cd_marital_status = 'S'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '1001-5000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Humboldt County','Hickman County','Galax city','Abbeville County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 2001\n         and t_s_secyear.year = 2001+1\n         and t_w_firstyear.year = 2001\n         and t_w_secyear.year = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 3,1,2\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Books'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Books'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Books') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_promo_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_promo_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_ship_addr_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_ship_addr_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_ship_customer_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_ship_customer_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_item_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2000\norder by\n  ss_item_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 5 or household_demographics.hd_vehicle_count > -1)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-19' as date)\n                  and (cast('2001-08-19' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-19' as date)\n                  and (cast('2001-08-19' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-19' as date)\n                  and (cast('2001-08-19' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =1999\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'MO'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 68 and 68+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2002-05-08' as date) and (cast('2002-05-08' as date) +  INTERVAL 60 days)\n and i_manufact_id in (562,370,230,182)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-02-20','2000-10-08','2000-11-04')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-02-20','2000-10-08','2000-11-04')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('2000-02-20','2000-10-08','2000-11-04')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Buena Vista'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  49786\n   and ib_upper_bound   <=  49786 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '4 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'M'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Primary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'U'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '2 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('IA', 'ND', 'FL')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('OH', 'MS', 'VA')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('MN', 'LA', 'TX')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1217 and 1217+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1224 and 1224+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1224 and 1224+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1224 and 1224+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2001) and\n        ((i_category in ('Children','Home','Women') and\n          i_class in ('toddlers','flatware','fragrances')\n         )\n      or (i_category in ('Music','Electronics','Shoes') and\n          i_class in ('country','dvd/vcr players','mens')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 7 and 7+1\n         and household_demographics.hd_dep_count = 1\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 20 and 20+1\n         and household_demographics.hd_dep_count = 1\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 1998\nand     d_moy                   = 12\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like 'Unknown%'\nand     ca_gmt_offset           = -6\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 172\nand i_item_sk = ws_item_sk\nand d_date between '1999-01-12' and\n        (cast('1999-01-12' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '1999-01-12' and\n                             (cast('1999-01-12' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 58') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2002-3-01' and\n           (cast('2002-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'GA'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-3-01' and\n           (cast('2001-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'NE'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 16\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 0\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1219 and 1219 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1219 and 1219 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Books', 'Children', 'Sports')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2001-03-10' as date)\n\t\t\t\tand (cast('2001-03-10' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1205 and 1205 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\"\n  )\n\n  val TPCDSQueries10TB = Map(\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'TN'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 2972190\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 111711138\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 127958920\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 41162107\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 25211875\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Allen County','Jefferson County','Lamar County','Dakota County','Park County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2001 and\n                d_moy between 4 and 4+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_moy between 4 ANd 4+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_moy between 4 and 4+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_login\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 1998\n         and t_s_secyear.dyear = 1998+1\n         and t_w_firstyear.dyear = 1998\n         and t_w_secyear.dyear = 1998+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_login\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Men', 'Books', 'Children')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('1998-03-28' as date)\n\t\t\t\tand (cast('1998-03-28' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'U'\n  and cd_education_status = 'Unknown'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'W'\n  and cd_education_status = '2 yr Degree'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'S'\n  and cd_education_status = 'College'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('WV', 'GA', 'TX')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('TN', 'KY', 'SC')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('OK', 'NE', 'CA')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998 + 1\n                       and d_moy = 12\n                       and d_dom = 20)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998\n                       and d_moy = 12\n                       and d_dom = 20)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 2000\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '2001-2-01' and\n           (cast('2001-2-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'MS'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Jackson County','Daviess County','Walker County','Dauphin County',\n                  'Mobile County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '1999Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'F' and\n       cd1.cd_education_status = 'Primary' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (6,7,3,11,12,8) and\n       d_year = 1999 and\n       ca_state in ('IL','WV','KS'\n                   ,'GA','LA','PA','TX')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=26\n   and d_moy=12\n   and d_year=2000\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Books', 'Home', 'Jewelry')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('1998-05-08' as date)\n \t\t\t\tand (cast('1998-05-08' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('2000-05-22' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('2000-05-22' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('2000-05-22' as date) - INTERVAL 30 days)\n                    and (cast ('2000-05-22' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1199 and 1199 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000+1,2000+2,2000+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 5\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 5\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000 + 1,2000 + 2,2000 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 2000\n         and d_moy = 5\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 5\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'navy'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'beige'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,sum(ss_net_profit) as store_sales_profit\n ,sum(sr_net_loss) as store_returns_loss\n ,sum(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2002\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2002\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2002\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'M' and\n       cd_education_status = '2 yr Degree' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2002\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'S' and\n       cd_education_status = 'Advanced Degree' and\n       d_year = 2000 and\n       s_state in ('WA','LA', 'LA', 'TX', 'AL', 'PA')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 189 and 189+10\n             or ss_coupon_amt between 4483 and 4483+1000\n             or ss_wholesale_cost between 24 and 24+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 71 and 71+10\n          or ss_coupon_amt between 14775 and 14775+1000\n          or ss_wholesale_cost between 38 and 38+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 183 and 183+10\n          or ss_coupon_amt between 13456 and 13456+1000\n          or ss_wholesale_cost between 31 and 31+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 135 and 135+10\n          or ss_coupon_amt between 4905 and 4905+1000\n          or ss_wholesale_cost between 27 and 27+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 180 and 180+10\n          or ss_coupon_amt between 17430 and 17430+1000\n          or ss_wholesale_cost between 57 and 57+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 49 and 49+10\n          or ss_coupon_amt between 2950 and 2950+1000\n          or ss_wholesale_cost between 52 and 52+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,stddev_samp(ss_quantity)        as store_sales_quantity\n    ,stddev_samp(sr_return_quantity) as store_returns_quantity\n    ,stddev_samp(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 1998\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 1998\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (1998,1998+1,1998+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'GA'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 1998\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 1998\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 1998\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 1998\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 1998\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =1998\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by ss1.ca_county\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 948\nand i_item_sk = cs_item_sk\nand d_date between '1998-02-03' and\n        (cast('1998-02-03' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '1998-02-03' and\n                             (cast('1998-02-03' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 2\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 2\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 2\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = '5001-10000')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Jefferson Davis Parish','Levy County','Coal County','Oglethorpe County',\n                           'Mobile County','Gage County','Richland County','Gogebic County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  stddev_samp(cd_dep_count),\n  stddev_samp(cd_dep_count),\n  min(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  stddev_samp(cd_dep_employed_count),\n  stddev_samp(cd_dep_employed_count),\n  min(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  stddev_samp(cd_dep_college_count),\n  stddev_samp(cd_dep_college_count),\n  min(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2002 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 1998\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('OH','WV','PA','TN',\n                 'MN','MO','NM','MI')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 35 and 35 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-01-20' as date) and (cast('2001-01-20' as date) + interval 60 days)\n and i_manufact_id in (928,715,942,861)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1222 and 1222 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1222 and 1222 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1222 and 1222 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1998\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=4\n  and inv2.d_moy=4+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1998\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=4\n  and inv2.d_moy=4+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('1999-02-02' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('1999-02-02' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('1999-02-02' as date) - INTERVAL 30 days)\n                and (cast ('1999-02-02' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 732 and 732+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'beige' or i_color = 'spring') and\n        (i_units = 'Tsp' or i_units = 'Ton') and\n        (i_size = 'petite' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'white' or i_color = 'pale') and\n        (i_units = 'Box' or i_units = 'Dram') and\n        (i_size = 'large' or i_size = 'economy')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'midnight' or i_color = 'frosted') and\n        (i_units = 'Bunch' or i_units = 'Carton') and\n        (i_size = 'small' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'azure' or i_color = 'goldenrod') and\n        (i_units = 'Pallet' or i_units = 'Gross') and\n        (i_size = 'petite' or i_size = 'extra large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'brown' or i_color = 'hot') and\n        (i_units = 'Tbl' or i_units = 'Cup') and\n        (i_size = 'petite' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'powder' or i_color = 'honeydew') and\n        (i_units = 'Bundle' or i_units = 'Unknown') and\n        (i_size = 'large' or i_size = 'economy')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'antique' or i_color = 'purple') and\n        (i_units = 'N/A' or i_units = 'Dozen') and\n        (i_size = 'small' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'lavender' or i_color = 'tomato') and\n        (i_units = 'Lb' or i_units = 'Oz') and\n        (i_size = 'petite' or i_size = 'extra large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=11\n \tand dt.d_year=2002\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -6 and\n       d_year = 1999\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 321\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 321\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 321\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 321\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_county, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 2 and d_year = 1999\n group by ca_zip, ca_county\n order by ca_zip, ca_county\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 2 or\n         household_demographics.hd_vehicle_count= 2)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_city in ('Antioch','Mount Vernon','Jamestown','Wilson','Farmington')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 2001 or\n         ( d_year = 2001-1 and d_moy =12) or\n         ( d_year = 2001+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.s_company_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2001 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, avg_monthly_sales\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 1999\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'D'\n   and\n   cd_education_status = 'College'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'W'\n   and\n   cd_education_status = 'Secondary'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'M'\n   and\n   cd_education_status = '2 yr Degree'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('NE', 'IA', 'NY')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('IN', 'TN', 'OH')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('KS', 'CA', 'CO')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 1999\nand d2.d_moy  = 9\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1176 and 1176+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1176 and 1176+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=11\n    and dt.d_year=2001\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1218,1218+1,1218+2,1218+3,1218+4,1218+5,1218+6,1218+7,1218+8,1218+9,1218+10,1218+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Music'\n         and i_class = 'country'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 7\n         and d_year = 2001\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 2001 and d_moy = 7)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 2001 and d_moy = 7)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=87\n \tand d_moy=11\n \tand d_year=2001\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('tan','lace','gainsboro'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 3\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('tan','lace','gainsboro'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 3\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('tan','lace','gainsboro'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 3\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 2001 or\n         ( d_year = 2001-1 and d_moy =12) or\n         ( d_year = 2001+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand, v1.cc_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2001 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, avg_monthly_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-03-26'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-03-26'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '2000-03-26'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1199 and 1199 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1199+ 12 and 1199 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Men'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 9\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Men'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 9\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Men'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 9\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Electronics'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -7\n   and   d_year = 2001\n   and   d_moy  = 11) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Electronics'\n   and   s_gmt_offset = -7\n   and   d_year = 2001\n   and   d_moy  = 11) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1194 and 1194 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1205,1205+1,1205+2,1205+3,1205+4,1205+5,1205+6,1205+7,1205+8,1205+9,1205+10,1205+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('peach','misty','drab','chocolate','almond','saddle') and\n         i_current_price between 75 and 75 + 10 and\n         i_current_price between 75 + 1 and 75 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 2000 and\n     cs2.syear = 2000 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1208 and 1208+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1208 and 1208+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'HARMSTORF' || ',' || 'USPS' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_sales_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_sales_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_sales_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_sales_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_sales_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_sales_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_sales_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_sales_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_sales_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_paid_inc_tax * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 24285 and 24285+28800\n \tand sm_carrier in ('HARMSTORF','USPS')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'HARMSTORF' || ',' || 'USPS' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_paid * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_paid * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_paid * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_paid * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_paid * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_paid * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_paid * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_paid * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_paid * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_paid * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_paid * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_paid * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 24285 AND 24285+28800\n \tand sm_carrier in ('HARMSTORF','USPS')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1196 and 1196+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 1 or\n             household_demographics.hd_vehicle_count= -1)\n        and date_dim.d_year in (1998,1998+1,1998+2)\n        and store.s_city in ('Bethel','Summit')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('OK','GA','VA') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2004 and\n                d_moy between 4 and 4+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2004 and\n                  d_moy between 4 and 4+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2004 and\n                  d_moy between 4 and 4+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1197 and 1197+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1197 and 1197+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=12\n                   and d_year=1999\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=12\n                   and d_year=1999\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=12\n                   and d_year=1999\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '>10000'\n  and d1.d_year = 2002\n  and cd_marital_status = 'D'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '501-1000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Franklin Parish','Ziebach County','Luce County','Williamson County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 2001\n         and t_s_secyear.year = 2001+1\n         and t_w_firstyear.year = 2001\n         and t_w_secyear.year = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 3,1,2\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Sports') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_cdemo_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_cdemo_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_ship_hdemo_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_ship_hdemo_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_ship_customer_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_ship_customer_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-27' as date)\n                  and (cast('2001-08-27' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_sold_year, ss_item_sk, ss_customer_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2002\norder by\n  ss_sold_year, ss_item_sk, ss_customer_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 0 or household_demographics.hd_vehicle_count > 0)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('1999-08-12' as date)\n                  and (cast('1999-08-12' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('1999-08-12' as date)\n                  and (cast('1999-08-12' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('1999-08-12' as date)\n                  and (cast('1999-08-12' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =2001\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'NC'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 82 and 82+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2002-03-10' as date) and (cast('2002-03-10' as date) +  INTERVAL 60 days)\n and i_manufact_id in (941,920,105,693)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('1999-04-14','1999-09-28','1999-11-12')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('1999-04-14','1999-09-28','1999-11-12')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('1999-04-14','1999-09-28','1999-11-12')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Antioch'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  55019\n   and ib_upper_bound   <=  55019 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'S'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '2 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Advanced Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'W'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '4 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('OK', 'TX', 'MO')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('GA', 'KS', 'NC')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('VA', 'WI', 'WV')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1180 and 1180+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1204 and 1204+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1204 and 1204+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1204 and 1204+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2001) and\n        ((i_category in ('Women','Music','Home') and\n          i_class in ('fragrances','pop','bedding')\n         )\n      or (i_category in ('Books','Men','Children') and\n          i_class in ('home repair','sports-apparel','infants')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 8 and 8+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 19 and 19+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 2002\nand     d_moy                   = 11\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like '5001-10000%'\nand     ca_gmt_offset           = -6\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 561\nand i_item_sk = ws_item_sk\nand d_date between '2001-03-13' and\n        (cast('2001-03-13' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '2001-03-13' and\n                             (cast('2001-03-13' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 64') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-5-01' and\n           (cast('2001-5-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'TX'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2000-3-01' and\n           (cast('2000-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'TN'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 16\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 4\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1209 and 1209 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1209 and 1209 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Jewelry', 'Home', 'Shoes')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2001-04-12' as date)\n\t\t\t\tand (cast('2001-04-12' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1203 and 1203 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\",\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'NM'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 98972190\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 160856845\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 12733327\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 96251173\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 80049606\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Fillmore County','McPherson County','Bonneville County','Boone County','Brown County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2000 and\n                d_moy between 3 and 3+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 3 ANd 3+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 3 and 3+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 1999\n         and t_s_secyear.dyear = 1999+1\n         and t_w_firstyear.dyear = 1999\n         and t_w_secyear.dyear = 1999+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Electronics', 'Books', 'Women')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('1998-01-06' as date)\n\t\t\t\tand (cast('1998-01-06' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'U'\n  and cd_education_status = 'Secondary'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'W'\n  and cd_education_status = 'College'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'D'\n  and cd_education_status = 'Primary'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('TX', 'OK', 'MI')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('WA', 'NC', 'OH')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('MT', 'FL', 'GA')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 2000 AND 2000 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 2000 AND 2000 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 2000 AND 2000 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 2000+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 2000+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 2000+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 2000 AND 2000 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 2000 AND 2000 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 2000 AND 2000 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 2000 and 2000 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 2000 + 1\n                       and d_moy = 12\n                       and d_dom = 15)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 2000\n                       and d_moy = 12\n                       and d_dom = 15)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 2 and d_year = 1998\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '1999-4-01' and\n           (cast('1999-4-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'IL'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Richland County','Bronx County','Maverick County','Mesa County',\n                  'Raleigh County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '2000Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('2000Q1','2000Q2','2000Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('2000Q1','2000Q2','2000Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'M' and\n       cd1.cd_education_status = 'Unknown' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (5,1,4,7,8,9) and\n       d_year = 2002 and\n       ca_state in ('AR','TX','NC'\n                   ,'GA','MS','WV','AL')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=16\n   and d_moy=12\n   and d_year=1998\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Shoes', 'Electronics', 'Children')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('2001-03-14' as date)\n \t\t\t\tand (cast('2001-03-14' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('1999-03-20' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('1999-03-20' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('1999-03-20' as date) - INTERVAL 30 days)\n                    and (cast ('1999-03-20' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1186 and 1186 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000+1,2000+2,2000+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 3\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 3\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000 + 1,2000 + 2,2000 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 2000\n         and d_moy = 3\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 3\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'snow'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 10\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'chiffon'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,sum(ss_net_profit) as store_sales_profit\n ,sum(sr_net_loss) as store_returns_loss\n ,sum(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2000\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2000\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2000\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'S' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 1998\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'U' and\n       cd_education_status = '2 yr Degree' and\n       d_year = 2000 and\n       s_state in ('AL','IN', 'SC', 'NY', 'OH', 'FL')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 73 and 73+10\n             or ss_coupon_amt between 7826 and 7826+1000\n             or ss_wholesale_cost between 70 and 70+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 152 and 152+10\n          or ss_coupon_amt between 2196 and 2196+1000\n          or ss_wholesale_cost between 56 and 56+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 53 and 53+10\n          or ss_coupon_amt between 3430 and 3430+1000\n          or ss_wholesale_cost between 13 and 13+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 182 and 182+10\n          or ss_coupon_amt between 3262 and 3262+1000\n          or ss_wholesale_cost between 20 and 20+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 85 and 85+10\n          or ss_coupon_amt between 3310 and 3310+1000\n          or ss_wholesale_cost between 37 and 37+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 180 and 180+10\n          or ss_coupon_amt between 12592 and 12592+1000\n          or ss_wholesale_cost between 22 and 22+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,stddev_samp(ss_quantity)        as store_sales_quantity\n    ,stddev_samp(sr_return_quantity) as store_returns_quantity\n    ,stddev_samp(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 1998\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 1998\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (1998,1998+1,1998+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'GA'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 1999\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 1999\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 1999\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 1999\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 1999\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =1999\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by ss1.d_year\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 66\nand i_item_sk = cs_item_sk\nand d_date between '2002-03-29' and\n        (cast('2002-03-29' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '2002-03-29' and\n                             (cast('2002-03-29' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Home'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 5\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Home'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 5\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Home'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 5\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_county in ('Salem County','Terrell County','Arthur County','Oglethorpe County',\n                           'Lunenburg County','Perry County','Halifax County','Sumner County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  avg(cd_dep_count),\n  min(cd_dep_count),\n  stddev_samp(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  avg(cd_dep_employed_count),\n  min(cd_dep_employed_count),\n  stddev_samp(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  avg(cd_dep_college_count),\n  min(cd_dep_college_count),\n  stddev_samp(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2001 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 1999\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('IN','AL','MI','MN',\n                 'TN','LA','FL','NM')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 39 and 39 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-01-16' as date) and (cast('2001-01-16' as date) + interval 60 days)\n and i_manufact_id in (765,886,889,728)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1186 and 1186 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1186 and 1186 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1186 and 1186 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=2\n  and inv2.d_moy=2+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=2\n  and inv2.d_moy=2+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('2000-03-18' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('2000-03-18' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('2000-03-18' as date) - INTERVAL 30 days)\n                and (cast ('2000-03-18' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 970 and 970+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'frosted' or i_color = 'rose') and\n        (i_units = 'Lb' or i_units = 'Gross') and\n        (i_size = 'medium' or i_size = 'large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'chocolate' or i_color = 'black') and\n        (i_units = 'Box' or i_units = 'Dram') and\n        (i_size = 'economy' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'slate' or i_color = 'magenta') and\n        (i_units = 'Carton' or i_units = 'Bundle') and\n        (i_size = 'N/A' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'cornflower' or i_color = 'firebrick') and\n        (i_units = 'Pound' or i_units = 'Oz') and\n        (i_size = 'medium' or i_size = 'large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'almond' or i_color = 'steel') and\n        (i_units = 'Tsp' or i_units = 'Case') and\n        (i_size = 'medium' or i_size = 'large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'purple' or i_color = 'aquamarine') and\n        (i_units = 'Bunch' or i_units = 'Gram') and\n        (i_size = 'economy' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'lavender' or i_color = 'papaya') and\n        (i_units = 'Pallet' or i_units = 'Cup') and\n        (i_size = 'N/A' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'maroon' or i_color = 'cyan') and\n        (i_units = 'Each' or i_units = 'N/A') and\n        (i_size = 'medium' or i_size = 'large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=12\n \tand dt.d_year=1998\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -6 and\n       d_year = 2001\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 366\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 366\n                                                    and ss_cdemo_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 366\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 366\n                                                    and ss_cdemo_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_county, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 1998\n group by ca_zip, ca_county\n order by ca_zip, ca_county\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 0 or\n         household_demographics.hd_vehicle_count= 1)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_city in ('Five Forks','Oakland','Fairview','Winchester','Farmington')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 1999 or\n         ( d_year = 1999-1 and d_moy =12) or\n         ( d_year = 1999+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.s_store_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 1999 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 1998\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'M'\n   and\n   cd_education_status = 'Unknown'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'W'\n   and\n   cd_education_status = 'College'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'D'\n   and\n   cd_education_status = 'Primary'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('MI', 'GA', 'NH')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('TX', 'KY', 'SD')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('NY', 'OH', 'FL')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 12\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 12\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 12\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 1998\nand d2.d_moy  = 9\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1214 and 1214+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1214 and 1214+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=12\n    and dt.d_year=2000\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1212,1212+1,1212+2,1212+3,1212+4,1212+5,1212+6,1212+7,1212+8,1212+9,1212+10,1212+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Books'\n         and i_class = 'business'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 2\n         and d_year = 2000\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 2000 and d_moy = 2)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 2000 and d_moy = 2)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=13\n \tand d_moy=11\n \tand d_year=1999\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('chiffon','smoke','lace'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 5\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('chiffon','smoke','lace'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 5\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('chiffon','smoke','lace'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 5\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 1999 or\n         ( d_year = 1999-1 and d_moy =12) or\n         ( d_year = 1999+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 1999 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, avg_monthly_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '1998-02-21'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '1998-02-21'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '1998-02-21'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1205 and 1205 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1205+ 12 and 1205 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Children'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Children'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Children'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Sports'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -6\n   and   d_year = 2001\n   and   d_moy  = 12) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Sports'\n   and   s_gmt_offset = -6\n   and   d_year = 2001\n   and   d_moy  = 12) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1215 and 1215 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1211,1211+1,1211+2,1211+3,1211+4,1211+5,1211+6,1211+7,1211+8,1211+9,1211+10,1211+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('azure','gainsboro','misty','blush','hot','lemon') and\n         i_current_price between 80 and 80 + 10 and\n         i_current_price between 80 + 1 and 80 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 1999 and\n     cs2.syear = 1999 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1186 and 1186+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1186 and 1186+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'MSC' || ',' || 'GERMA' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_sales_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_sales_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_sales_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_sales_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_sales_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_sales_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_sales_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_sales_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_sales_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_sales_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_paid_inc_ship_tax * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2001\n \tand t_time between 9453 and 9453+28800\n \tand sm_carrier in ('MSC','GERMA')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'MSC' || ',' || 'GERMA' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_paid_inc_ship * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2001\n \tand t_time between 9453 AND 9453+28800\n \tand sm_carrier in ('MSC','GERMA')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1185 and 1185+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 4 or\n             household_demographics.hd_vehicle_count= 0)\n        and date_dim.d_year in (1999,1999+1,1999+2)\n        and store.s_city in ('Pleasant Hill','Bethel')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('MO','MN','AZ') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2003 and\n                d_moy between 2 and 2+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1218 and 1218+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1218 and 1218+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=12\n                   and d_year=2000\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=12\n                   and d_year=2000\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=12\n                   and d_year=2000\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '1001-5000'\n  and d1.d_year = 2000\n  and cd_marital_status = 'D'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = '5001-10000')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_county in ('Lea County','Furnas County','Pennington County','Bronx County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,sum(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (1998,1998+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,sum(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (1998,1998+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 1998\n         and t_s_secyear.year = 1998+1\n         and t_w_firstyear.year = 1998\n         and t_w_secyear.year = 1998+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 3,1,2\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Sports'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Sports') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_customer_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_customer_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_ship_addr_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_ship_addr_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_ship_mode_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_ship_mode_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2000-08-16' as date)\n                  and (cast('2000-08-16' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_customer_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2001\norder by\n  ss_customer_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 0 or household_demographics.hd_vehicle_count > 3)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2002-08-06' as date)\n                  and (cast('2002-08-06' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2002-08-06' as date)\n                  and (cast('2002-08-06' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2002-08-06' as date)\n                  and (cast('2002-08-06' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =1998\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'TX'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 49 and 49+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-01-28' as date) and (cast('2001-01-28' as date) +  INTERVAL 60 days)\n and i_manufact_id in (80,675,292,17)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-06-17','2000-08-22','2000-11-17')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-06-17','2000-08-22','2000-11-17')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('2000-06-17','2000-08-22','2000-11-17')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Hopewell'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  37855\n   and ib_upper_bound   <=  37855 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'M'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '4 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'S'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'College'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Secondary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('TX', 'VA', 'CA')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('AR', 'NE', 'MO')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('IA', 'MS', 'WA')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1215 and 1215+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1221 and 1221+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1221 and 1221+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1221 and 1221+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2000) and\n        ((i_category in ('Home','Music','Books') and\n          i_class in ('glassware','classical','fiction')\n         )\n      or (i_category in ('Jewelry','Sports','Women') and\n          i_class in ('semi-precious','baseball','dresses')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 9 and 9+1\n         and household_demographics.hd_dep_count = 3\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 16 and 16+1\n         and household_demographics.hd_dep_count = 3\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 2000\nand     d_moy                   = 12\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like 'Unknown%'\nand     ca_gmt_offset           = -7\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 356\nand i_item_sk = ws_item_sk\nand d_date between '2001-03-12' and\n        (cast('2001-03-12' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '2001-03-12' and\n                             (cast('2001-03-12' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 66') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '1999-4-01' and\n           (cast('1999-4-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'NE'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2002-4-01' and\n           (cast('2002-4-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'AL'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 16\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 6\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1190 and 1190 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1190 and 1190 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Home', 'Sports', 'Men')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2002-01-05' as date)\n\t\t\t\tand (cast('2002-01-05' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1178 and 1178 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\",\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'NY'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 578972190\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 536856786\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 12733327\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 205136171\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 1192341092\n            then (select avg(ss_ext_list_price)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_paid_inc_tax)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Baltimore city','Stafford County','Greene County','Ballard County','Franklin County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2000 and\n                d_moy between 1 and 1+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 1 ANd 1+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2000 and\n                  d_moy between 1 and 1+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_preferred_cust_flag\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 2001\n         and t_s_secyear.dyear = 2001+1\n         and t_w_firstyear.dyear = 2001\n         and t_w_secyear.dyear = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_preferred_cust_flag\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Children', 'Shoes', 'Women')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('1998-06-19' as date)\n\t\t\t\tand (cast('1998-06-19' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'D'\n  and cd_education_status = '2 yr Degree'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'U'\n  and cd_education_status = 'College'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'S'\n  and cd_education_status = 'Primary'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('GA', 'IN', 'NY')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('ND', 'WV', 'TX')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('KS', 'NC', 'NM')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 1998+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1998 AND 1998 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1998 AND 1998 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1998 AND 1998 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1998 and 1998 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998 + 1\n                       and d_moy = 12\n                       and d_dom = 17)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1998\n                       and d_moy = 12\n                       and d_dom = 17)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 2002\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '2001-3-01' and\n           (cast('2001-3-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'PA'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Luce County','Franklin Parish','Sierra County','Williamson County',\n                  'Kittitas County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '2001Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('2001Q1','2001Q2','2001Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('2001Q1','2001Q2','2001Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'M' and\n       cd1.cd_education_status = 'Unknown' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (5,7,8,6,12,4) and\n       d_year = 2000 and\n       ca_state in ('MO','NY','ME'\n                   ,'MI','IA','OH','MS')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=55\n   and d_moy=11\n   and d_year=1998\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Shoes', 'Electronics', 'Home')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('2000-05-15' as date)\n \t\t\t\tand (cast('2000-05-15' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('2002-02-15' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('2002-02-15' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('2002-02-15' as date) - INTERVAL 30 days)\n                    and (cast ('2002-02-15' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1202 and 1202 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000+1,2000+2,2000+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 4\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 4\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (2000,2000 + 1,2000 + 2,2000 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (2000,2000+1,2000+2,2000+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 2000\n         and d_moy = 4\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 2000\n         and d_moy = 4\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=5\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'cyan'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_net_paid_inc_tax) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 5\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'ivory'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,stddev_samp(ss_net_profit) as store_sales_profit\n ,stddev_samp(sr_net_loss) as store_returns_loss\n ,stddev_samp(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2000\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2000\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2000\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'M' and\n       cd_education_status = 'Unknown' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'D' and\n       cd_education_status = '2 yr Degree' and\n       d_year = 1999 and\n       s_state in ('MI','WV', 'MI', 'NY', 'TN', 'MI')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 151 and 151+10\n             or ss_coupon_amt between 4349 and 4349+1000\n             or ss_wholesale_cost between 75 and 75+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 45 and 45+10\n          or ss_coupon_amt between 12490 and 12490+1000\n          or ss_wholesale_cost between 37 and 37+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 54 and 54+10\n          or ss_coupon_amt between 13038 and 13038+1000\n          or ss_wholesale_cost between 17 and 17+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 178 and 178+10\n          or ss_coupon_amt between 10744 and 10744+1000\n          or ss_wholesale_cost between 51 and 51+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 49 and 49+10\n          or ss_coupon_amt between 8494 and 8494+1000\n          or ss_wholesale_cost between 56 and 56+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 0 and 0+10\n          or ss_coupon_amt between 17854 and 17854+1000\n          or ss_wholesale_cost between 31 and 31+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,max(ss_quantity)        as store_sales_quantity\n    ,max(sr_return_quantity) as store_returns_quantity\n    ,max(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 1999\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 1999\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (1999,1999+1,1999+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'MD'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 1999\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 1999\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 1999\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 1999\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 1999\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =1999\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by store_q2_q3_increase\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 7\nand i_item_sk = cs_item_sk\nand d_date between '2000-01-21' and\n        (cast('2000-01-21' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '2000-01-21' and\n                             (cast('2000-01-21' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Books'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Books'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Books'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '501-1000' or\n         household_demographics.hd_buy_potential = '5001-10000')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Levy County','Val Verde County','Porter County','Nowata County',\n                           'Lincoln County','Brazos County','Franklin Parish','Pipestone County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  sum(cd_dep_count),\n  sum(cd_dep_count),\n  sum(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  sum(cd_dep_employed_count),\n  sum(cd_dep_employed_count),\n  sum(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  sum(cd_dep_college_count),\n  sum(cd_dep_college_count),\n  sum(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2001 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2001 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 1999\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('MO','AL','OH','WV',\n                 'AL','MN','TN','WA')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 57 and 57 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-04-19' as date) and (cast('2001-04-19' as date) + interval 60 days)\n and i_manufact_id in (804,916,707,680)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1189 and 1189 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1189 and 1189 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1189 and 1189 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =2000\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('2000-04-09' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('2000-04-09' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('2000-04-09' as date) - INTERVAL 30 days)\n                and (cast ('2000-04-09' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 917 and 917+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'antique' or i_color = 'pale') and\n        (i_units = 'Tbl' or i_units = 'Case') and\n        (i_size = 'small' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'snow' or i_color = 'lemon') and\n        (i_units = 'Box' or i_units = 'Ounce') and\n        (i_size = 'economy' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'green' or i_color = 'blue') and\n        (i_units = 'Gross' or i_units = 'Ton') and\n        (i_size = 'large' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'cream' or i_color = 'frosted') and\n        (i_units = 'Bundle' or i_units = 'Gram') and\n        (i_size = 'small' or i_size = 'extra large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'orange' or i_color = 'spring') and\n        (i_units = 'Lb' or i_units = 'Carton') and\n        (i_size = 'small' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'lawn' or i_color = 'violet') and\n        (i_units = 'Oz' or i_units = 'Cup') and\n        (i_size = 'economy' or i_size = 'N/A')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'navy' or i_color = 'linen') and\n        (i_units = 'Pound' or i_units = 'Unknown') and\n        (i_size = 'large' or i_size = 'petite')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'almond' or i_color = 'olive') and\n        (i_units = 'Pallet' or i_units = 'Bunch') and\n        (i_size = 'small' or i_size = 'extra large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=11\n \tand dt.d_year=1998\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -6 and\n       d_year = 2000\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 731\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 731\n                                                    and ss_promo_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 731\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 731\n                                                    and ss_promo_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_city, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 2000\n group by ca_zip, ca_city\n order by ca_zip, ca_city\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 1 or\n         household_demographics.hd_vehicle_count= 2)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (2000,2000+1,2000+2)\n    and store.s_city in ('Buena Vista','Friendship','Monroe','Oak Hill','Randolph')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 1999 or\n         ( d_year = 1999-1 and d_moy =12) or\n         ( d_year = 1999+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.i_category\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 1999 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 1999\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'S'\n   and\n   cd_education_status = 'Primary'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'D'\n   and\n   cd_education_status = 'College'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'U'\n   and\n   cd_education_status = '2 yr Degree'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('ND', 'NC', 'TX')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('VA', 'IA', 'AR')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('MA', 'FL', 'TN')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 2000\n                         and d_moy = 11\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 2002\nand d2.d_moy  = 10\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1213 and 1213+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1213 and 1213+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=11\n    and dt.d_year=1998\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1219,1219+1,1219+2,1219+3,1219+4,1219+5,1219+6,1219+7,1219+8,1219+9,1219+10,1219+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Men'\n         and i_class = 'shirts'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 2\n         and d_year = 1999\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 1999 and d_moy = 2)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 1999 and d_moy = 2)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=96\n \tand d_moy=11\n \tand d_year=2000\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('antique','white','smoke'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 6\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('antique','white','smoke'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 6\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('antique','white','smoke'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2000\n and     d_moy                   = 6\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 2000 or\n         ( d_year = 2000-1 and d_moy =12) or\n         ( d_year = 2000+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand, v1.cc_name\n        ,v1.d_year\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2000 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2001-01-27'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2001-01-27'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '2001-01-27'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1177 and 1177 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1177+ 12 and 1177 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2002\n and     d_moy                   = 8\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2002\n and     d_moy                   = 8\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2002\n and     d_moy                   = 8\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Home'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -6\n   and   d_year = 1999\n   and   d_moy  = 12) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -6\n   and   i_category = 'Home'\n   and   s_gmt_offset = -6\n   and   d_year = 1999\n   and   d_moy  = 12) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1191 and 1191 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1193,1193+1,1193+2,1193+3,1193+4,1193+5,1193+6,1193+7,1193+8,1193+9,1193+10,1193+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('orange','aquamarine','olive','linen','smoke','coral') and\n         i_current_price between 74 and 74 + 10 and\n         i_current_price between 74 + 1 and 74 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 2001 and\n     cs2.syear = 2001 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1195 and 1195+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1195 and 1195+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'LATVIAN' || ',' || 'ALLIANCE' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_paid_inc_ship * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 1998\n \tand t_time between 16224 and 16224+28800\n \tand sm_carrier in ('LATVIAN','ALLIANCE')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'LATVIAN' || ',' || 'ALLIANCE' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_sales_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_paid_inc_ship_tax * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 1998\n \tand t_time between 16224 AND 16224+28800\n \tand sm_carrier in ('LATVIAN','ALLIANCE')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1203 and 1203+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 3 or\n             household_demographics.hd_vehicle_count= -1)\n        and date_dim.d_year in (1999,1999+1,1999+2)\n        and store.s_city in ('Jamestown','Pine Hill')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('CA','MT','SD') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2003 and\n                d_moy between 2 and 2+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2003 and\n                  d_moy between 2 and 2+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1215 and 1215+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1215 and 1215+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=11\n                   and d_year=1998\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=11\n                   and d_year=1998\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=11\n                   and d_year=1998\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '1001-5000'\n  and d1.d_year = 1998\n  and cd_marital_status = 'S'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '>10000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_county in ('Van Buren County','Terrell County','Belknap County','Kootenai County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 2001\n         and t_s_secyear.year = 2001+1\n         and t_w_firstyear.year = 2001\n         and t_w_secyear.year = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 2,3,1\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Music'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Music'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Music') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_promo_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_promo_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_web_site_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_web_site_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_bill_addr_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_bill_addr_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-22' as date)\n                  and (cast('2001-08-22' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_sold_year, ss_item_sk, ss_customer_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2001\norder by\n  ss_sold_year, ss_item_sk, ss_customer_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 6 or household_demographics.hd_vehicle_count > -1)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (1998,1998+1,1998+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-25' as date)\n                  and (cast('2000-08-25' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-25' as date)\n                  and (cast('2000-08-25' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2000-08-25' as date)\n                  and (cast('2000-08-25' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =2000\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'SC'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 6 and 6+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2001-02-23' as date) and (cast('2001-02-23' as date) +  INTERVAL 60 days)\n and i_manufact_id in (669,623,578,379)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2001-01-15','2001-09-03','2001-11-17')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2001-01-15','2001-09-03','2001-11-17')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('2001-01-15','2001-09-03','2001-11-17')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Walnut Grove'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  53669\n   and ib_upper_bound   <=  53669 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'S'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Secondary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Advanced Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'W'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Primary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('AZ', 'SD', 'TN')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('TX', 'GA', 'IA')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('WI', 'VT', 'AL')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1195 and 1195+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1194 and 1194+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1194 and 1194+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1194 and 1194+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2) or\n          (household_demographics.hd_dep_count = 4 and household_demographics.hd_vehicle_count<=4+2) or\n          (household_demographics.hd_dep_count = 1 and household_demographics.hd_vehicle_count<=1+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2000) and\n        ((i_category in ('Home','Shoes','Electronics') and\n          i_class in ('flatware','mens','televisions')\n         )\n      or (i_category in ('Women','Sports','Music') and\n          i_class in ('maternity','camping','rock')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 8 and 8+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 20 and 20+1\n         and household_demographics.hd_dep_count = 4\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 2001\nand     d_moy                   = 12\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like 'Unknown%'\nand     ca_gmt_offset           = -6\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 7\nand i_item_sk = ws_item_sk\nand d_date between '2000-01-16' and\n        (cast('2000-01-16' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '2000-01-16' and\n                             (cast('2000-01-16' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 24') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-2-01' and\n           (cast('2001-2-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'VT'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-3-01' and\n           (cast('2001-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'TN'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 20\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 6\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1206 and 1206 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1206 and 1206 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Sports', 'Books', 'Electronics')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2002-06-29' as date)\n\t\t\t\tand (cast('2002-06-29' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1199 and 1199 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\",\n    \"q1\" ->\n      \"\"\"\nwith customer_total_return as\n(select sr_customer_sk as ctr_customer_sk\n,sr_store_sk as ctr_store_sk\n,sum(SR_FEE) as ctr_total_return\nfrom store_returns\n,date_dim\nwhere sr_returned_date_sk = d_date_sk\nand d_year =2000\ngroup by sr_customer_sk\n,sr_store_sk)\n select  c_customer_id\nfrom customer_total_return ctr1\n,store\n,customer\nwhere ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\nfrom customer_total_return ctr2\nwhere ctr1.ctr_store_sk = ctr2.ctr_store_sk)\nand s_store_sk = ctr1.ctr_store_sk\nand s_state = 'MO'\nand ctr1.ctr_customer_sk = c_customer_sk\norder by c_customer_id\nlimit 100\"\"\",\n    \"q2\" ->\n      \"\"\"\nwith wscs as\n (select sold_date_sk\n        ,sales_price\n  from (select ws_sold_date_sk sold_date_sk\n              ,ws_ext_sales_price sales_price\n        from web_sales\n        union all\n        select cs_sold_date_sk sold_date_sk\n              ,cs_ext_sales_price sales_price\n        from catalog_sales)),\n wswscs as\n (select d_week_seq,\n        sum(case when (d_day_name='Sunday') then sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then sales_price else null end) sat_sales\n from wscs\n     ,date_dim\n where d_date_sk = sold_date_sk\n group by d_week_seq)\n select d_week_seq1\n       ,round(sun_sales1/sun_sales2,2)\n       ,round(mon_sales1/mon_sales2,2)\n       ,round(tue_sales1/tue_sales2,2)\n       ,round(wed_sales1/wed_sales2,2)\n       ,round(thu_sales1/thu_sales2,2)\n       ,round(fri_sales1/fri_sales2,2)\n       ,round(sat_sales1/sat_sales2,2)\n from\n (select wswscs.d_week_seq d_week_seq1\n        ,sun_sales sun_sales1\n        ,mon_sales mon_sales1\n        ,tue_sales tue_sales1\n        ,wed_sales wed_sales1\n        ,thu_sales thu_sales1\n        ,fri_sales fri_sales1\n        ,sat_sales sat_sales1\n  from wswscs,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998) y,\n (select wswscs.d_week_seq d_week_seq2\n        ,sun_sales sun_sales2\n        ,mon_sales mon_sales2\n        ,tue_sales tue_sales2\n        ,wed_sales wed_sales2\n        ,thu_sales thu_sales2\n        ,fri_sales fri_sales2\n        ,sat_sales sat_sales2\n  from wswscs\n      ,date_dim\n  where date_dim.d_week_seq = wswscs.d_week_seq and\n        d_year = 1998+1) z\n where d_week_seq1=d_week_seq2-53\n order by d_week_seq1\"\"\",\n    \"q3\" ->\n      \"\"\"\nselect  dt.d_year\n       ,item.i_brand_id brand_id\n       ,item.i_brand brand\n       ,sum(ss_sales_price) sum_agg\n from  date_dim dt\n      ,store_sales\n      ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n   and store_sales.ss_item_sk = item.i_item_sk\n   and item.i_manufact_id = 816\n   and dt.d_moy=11\n group by dt.d_year\n      ,item.i_brand\n      ,item.i_brand_id\n order by dt.d_year\n         ,sum_agg desc\n         ,brand_id\n limit 100\"\"\",\n    \"q4\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(((ss_ext_list_price-ss_ext_wholesale_cost-ss_ext_discount_amt)+ss_ext_sales_price)/2) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((cs_ext_list_price-cs_ext_wholesale_cost-cs_ext_discount_amt)+cs_ext_sales_price)/2) ) year_total\n       ,'c' sale_type\n from customer\n     ,catalog_sales\n     ,date_dim\n where c_customer_sk = cs_bill_customer_sk\n   and cs_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\nunion all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum((((ws_ext_list_price-ws_ext_wholesale_cost-ws_ext_discount_amt)+ws_ext_sales_price)/2) ) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_birth_country\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_c_firstyear\n     ,year_total t_c_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_c_secyear.customer_id\n   and t_s_firstyear.customer_id = t_c_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n   and t_s_firstyear.customer_id = t_w_secyear.customer_id\n   and t_s_firstyear.sale_type = 's'\n   and t_c_firstyear.sale_type = 'c'\n   and t_w_firstyear.sale_type = 'w'\n   and t_s_secyear.sale_type = 's'\n   and t_c_secyear.sale_type = 'c'\n   and t_w_secyear.sale_type = 'w'\n   and t_s_firstyear.dyear =  1999\n   and t_s_secyear.dyear = 1999+1\n   and t_c_firstyear.dyear =  1999\n   and t_c_secyear.dyear =  1999+1\n   and t_w_firstyear.dyear = 1999\n   and t_w_secyear.dyear = 1999+1\n   and t_s_firstyear.year_total > 0\n   and t_c_firstyear.year_total > 0\n   and t_w_firstyear.year_total > 0\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n   and case when t_c_firstyear.year_total > 0 then t_c_secyear.year_total / t_c_firstyear.year_total else null end\n           > case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_birth_country\nlimit 100\"\"\",\n    \"q5\" ->\n      \"\"\"\nwith ssr as\n (select s_store_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ss_store_sk as store_sk,\n            ss_sold_date_sk  as date_sk,\n            ss_ext_sales_price as sales_price,\n            ss_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from store_sales\n    union all\n    select sr_store_sk as store_sk,\n           sr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           sr_return_amt as return_amt,\n           sr_net_loss as net_loss\n    from store_returns\n   ) salesreturns,\n     date_dim,\n     store\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and store_sk = s_store_sk\n group by s_store_id)\n ,\n csr as\n (select cp_catalog_page_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  cs_catalog_page_sk as page_sk,\n            cs_sold_date_sk  as date_sk,\n            cs_ext_sales_price as sales_price,\n            cs_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from catalog_sales\n    union all\n    select cr_catalog_page_sk as page_sk,\n           cr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           cr_return_amount as return_amt,\n           cr_net_loss as net_loss\n    from catalog_returns\n   ) salesreturns,\n     date_dim,\n     catalog_page\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and page_sk = cp_catalog_page_sk\n group by cp_catalog_page_id)\n ,\n wsr as\n (select web_site_id,\n        sum(sales_price) as sales,\n        sum(profit) as profit,\n        sum(return_amt) as returns,\n        sum(net_loss) as profit_loss\n from\n  ( select  ws_web_site_sk as wsr_web_site_sk,\n            ws_sold_date_sk  as date_sk,\n            ws_ext_sales_price as sales_price,\n            ws_net_profit as profit,\n            cast(0 as decimal(7,2)) as return_amt,\n            cast(0 as decimal(7,2)) as net_loss\n    from web_sales\n    union all\n    select ws_web_site_sk as wsr_web_site_sk,\n           wr_returned_date_sk as date_sk,\n           cast(0 as decimal(7,2)) as sales_price,\n           cast(0 as decimal(7,2)) as profit,\n           wr_return_amt as return_amt,\n           wr_net_loss as net_loss\n    from web_returns left outer join web_sales on\n         ( wr_item_sk = ws_item_sk\n           and wr_order_number = ws_order_number)\n   ) salesreturns,\n     date_dim,\n     web_site\n where date_sk = d_date_sk\n       and d_date between cast('2000-08-19' as date)\n                  and (cast('2000-08-19' as date) +  INTERVAL 14 days)\n       and wsr_web_site_sk = web_site_sk\n group by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || s_store_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || cp_catalog_page_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q6\" ->\n      \"\"\"\nselect  a.ca_state state, count(*) cnt\n from customer_address a\n     ,customer c\n     ,store_sales s\n     ,date_dim d\n     ,item i\n where       a.ca_address_sk = c.c_current_addr_sk\n \tand c.c_customer_sk = s.ss_customer_sk\n \tand s.ss_sold_date_sk = d.d_date_sk\n \tand s.ss_item_sk = i.i_item_sk\n \tand d.d_month_seq =\n \t     (select distinct (d_month_seq)\n \t      from date_dim\n               where d_year = 2002\n \t        and d_moy = 3 )\n \tand i.i_current_price > 1.2 *\n             (select avg(j.i_current_price)\n \t     from item j\n \t     where j.i_category = i.i_category)\n group by a.ca_state\n having count(*) >= 10\n order by cnt, a.ca_state\n limit 100\"\"\",\n    \"q7\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, item, promotion\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       ss_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'W' and\n       cd_education_status = 'College' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 2001\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q8\" ->\n      \"\"\"\nselect  s_store_name\n      ,sum(ss_net_profit)\n from store_sales\n     ,date_dim\n     ,store,\n     (select ca_zip\n     from (\n      SELECT substr(ca_zip,1,5) ca_zip\n      FROM customer_address\n      WHERE substr(ca_zip,1,5) IN (\n                          '47602','16704','35863','28577','83910','36201',\n                          '58412','48162','28055','41419','80332',\n                          '38607','77817','24891','16226','18410',\n                          '21231','59345','13918','51089','20317',\n                          '17167','54585','67881','78366','47770',\n                          '18360','51717','73108','14440','21800',\n                          '89338','45859','65501','34948','25973',\n                          '73219','25333','17291','10374','18829',\n                          '60736','82620','41351','52094','19326',\n                          '25214','54207','40936','21814','79077',\n                          '25178','75742','77454','30621','89193',\n                          '27369','41232','48567','83041','71948',\n                          '37119','68341','14073','16891','62878',\n                          '49130','19833','24286','27700','40979',\n                          '50412','81504','94835','84844','71954',\n                          '39503','57649','18434','24987','12350',\n                          '86379','27413','44529','98569','16515',\n                          '27287','24255','21094','16005','56436',\n                          '91110','68293','56455','54558','10298',\n                          '83647','32754','27052','51766','19444',\n                          '13869','45645','94791','57631','20712',\n                          '37788','41807','46507','21727','71836',\n                          '81070','50632','88086','63991','20244',\n                          '31655','51782','29818','63792','68605',\n                          '94898','36430','57025','20601','82080',\n                          '33869','22728','35834','29086','92645',\n                          '98584','98072','11652','78093','57553',\n                          '43830','71144','53565','18700','90209',\n                          '71256','38353','54364','28571','96560',\n                          '57839','56355','50679','45266','84680',\n                          '34306','34972','48530','30106','15371',\n                          '92380','84247','92292','68852','13338',\n                          '34594','82602','70073','98069','85066',\n                          '47289','11686','98862','26217','47529',\n                          '63294','51793','35926','24227','14196',\n                          '24594','32489','99060','49472','43432',\n                          '49211','14312','88137','47369','56877',\n                          '20534','81755','15794','12318','21060',\n                          '73134','41255','63073','81003','73873',\n                          '66057','51184','51195','45676','92696',\n                          '70450','90669','98338','25264','38919',\n                          '59226','58581','60298','17895','19489',\n                          '52301','80846','95464','68770','51634',\n                          '19988','18367','18421','11618','67975',\n                          '25494','41352','95430','15734','62585',\n                          '97173','33773','10425','75675','53535',\n                          '17879','41967','12197','67998','79658',\n                          '59130','72592','14851','43933','68101',\n                          '50636','25717','71286','24660','58058',\n                          '72991','95042','15543','33122','69280',\n                          '11912','59386','27642','65177','17672',\n                          '33467','64592','36335','54010','18767',\n                          '63193','42361','49254','33113','33159',\n                          '36479','59080','11855','81963','31016',\n                          '49140','29392','41836','32958','53163',\n                          '13844','73146','23952','65148','93498',\n                          '14530','46131','58454','13376','13378',\n                          '83986','12320','17193','59852','46081',\n                          '98533','52389','13086','68843','31013',\n                          '13261','60560','13443','45533','83583',\n                          '11489','58218','19753','22911','25115',\n                          '86709','27156','32669','13123','51933',\n                          '39214','41331','66943','14155','69998',\n                          '49101','70070','35076','14242','73021',\n                          '59494','15782','29752','37914','74686',\n                          '83086','34473','15751','81084','49230',\n                          '91894','60624','17819','28810','63180',\n                          '56224','39459','55233','75752','43639',\n                          '55349','86057','62361','50788','31830',\n                          '58062','18218','85761','60083','45484',\n                          '21204','90229','70041','41162','35390',\n                          '16364','39500','68908','26689','52868',\n                          '81335','40146','11340','61527','61794',\n                          '71997','30415','59004','29450','58117',\n                          '69952','33562','83833','27385','61860',\n                          '96435','48333','23065','32961','84919',\n                          '61997','99132','22815','56600','68730',\n                          '48017','95694','32919','88217','27116',\n                          '28239','58032','18884','16791','21343',\n                          '97462','18569','75660','15475')\n     intersect\n      select ca_zip\n      from (SELECT substr(ca_zip,1,5) ca_zip,count(*) cnt\n            FROM customer_address, customer\n            WHERE ca_address_sk = c_current_addr_sk and\n                  c_preferred_cust_flag='Y'\n            group by ca_zip\n            having count(*) > 10)A1)A2) V1\n where ss_store_sk = s_store_sk\n  and ss_sold_date_sk = d_date_sk\n  and d_qoy = 2 and d_year = 1998\n  and (substr(s_zip,1,2) = substr(V1.ca_zip,1,2))\n group by s_store_name\n order by s_store_name\n limit 100\"\"\",\n    \"q9\" ->\n      \"\"\"\nselect case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 1 and 20) > 4502397049\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 1 and 20)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 1 and 20) end bucket1 ,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 21 and 40) > 4756228269\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 21 and 40)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 21 and 40) end bucket2,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 41 and 60) > 4101835064\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 41 and 60)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 41 and 60) end bucket3,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 61 and 80) > 4583261513\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 61 and 80)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 61 and 80) end bucket4,\n       case when (select count(*)\n                  from store_sales\n                  where ss_quantity between 81 and 100) > 4208819283\n            then (select avg(ss_ext_discount_amt)\n                  from store_sales\n                  where ss_quantity between 81 and 100)\n            else (select avg(ss_net_profit)\n                  from store_sales\n                  where ss_quantity between 81 and 100) end bucket5\nfrom reason\nwhere r_reason_sk = 1\"\"\",\n    \"q10\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3,\n  cd_dep_count,\n  count(*) cnt4,\n  cd_dep_employed_count,\n  count(*) cnt5,\n  cd_dep_college_count,\n  count(*) cnt6\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_county in ('Grady County','Marion County','Decatur County','Lyman County','Beaver County') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 1999 and\n                d_moy between 2 and 2+3) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 2 ANd 2+3) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 2 and 2+3))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\nlimit 100\"\"\",\n    \"q11\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ss_ext_list_price-ss_ext_discount_amt) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,c_preferred_cust_flag customer_preferred_cust_flag\n       ,c_birth_country customer_birth_country\n       ,c_login customer_login\n       ,c_email_address customer_email_address\n       ,d_year dyear\n       ,sum(ws_ext_list_price-ws_ext_discount_amt) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,c_preferred_cust_flag\n         ,c_birth_country\n         ,c_login\n         ,c_email_address\n         ,d_year\n         )\n  select\n                  t_s_secyear.customer_id\n                 ,t_s_secyear.customer_first_name\n                 ,t_s_secyear.customer_last_name\n                 ,t_s_secyear.customer_preferred_cust_flag\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.dyear = 2001\n         and t_s_secyear.dyear = 2001+1\n         and t_w_firstyear.dyear = 2001\n         and t_w_secyear.dyear = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else 0.0 end\n             > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else 0.0 end\n order by t_s_secyear.customer_id\n         ,t_s_secyear.customer_first_name\n         ,t_s_secyear.customer_last_name\n         ,t_s_secyear.customer_preferred_cust_flag\nlimit 100\"\"\",\n    \"q12\" ->\n      \"\"\"\nselect  i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ws_ext_sales_price) as itemrevenue\n      ,sum(ws_ext_sales_price)*100/sum(sum(ws_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tweb_sales\n    \t,item\n    \t,date_dim\nwhere\n\tws_item_sk = i_item_sk\n  \tand i_category in ('Children', 'Jewelry', 'Music')\n  \tand ws_sold_date_sk = d_date_sk\n\tand d_date between cast('2001-05-11' as date)\n\t\t\t\tand (cast('2001-05-11' as date) + INTERVAL 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\nlimit 100\"\"\",\n    \"q13\" ->\n      \"\"\"\nselect avg(ss_quantity)\n       ,avg(ss_ext_sales_price)\n       ,avg(ss_ext_wholesale_cost)\n       ,sum(ss_ext_wholesale_cost)\n from store_sales\n     ,store\n     ,customer_demographics\n     ,household_demographics\n     ,customer_address\n     ,date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2001\n and((ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'M'\n  and cd_education_status = 'Primary'\n  and ss_sales_price between 100.00 and 150.00\n  and hd_dep_count = 3\n     )or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'S'\n  and cd_education_status = '4 yr Degree'\n  and ss_sales_price between 50.00 and 100.00\n  and hd_dep_count = 1\n     ) or\n     (ss_hdemo_sk=hd_demo_sk\n  and cd_demo_sk = ss_cdemo_sk\n  and cd_marital_status = 'W'\n  and cd_education_status = '2 yr Degree'\n  and ss_sales_price between 150.00 and 200.00\n  and hd_dep_count = 1\n     ))\n and((ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('SC', 'WY', 'TX')\n  and ss_net_profit between 100 and 200\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('NY', 'NE', 'GA')\n  and ss_net_profit between 150 and 300\n     ) or\n     (ss_addr_sk = ca_address_sk\n  and ca_country = 'United States'\n  and ca_state in ('AL', 'AR', 'MI')\n  and ss_net_profit between 50 and 250\n     ))\"\"\",\n    \"q14a\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1999 AND 1999 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1999 AND 1999 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1999 AND 1999 + 2)\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n (select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2) x)\n  select  channel, i_brand_id,i_class_id,i_category_id,sum(sales), sum(number_sales)\n from(\n       select 'store' channel, i_brand_id,i_class_id\n             ,i_category_id,sum(ss_quantity*ss_list_price) sales\n             , count(*) number_sales\n       from store_sales\n           ,item\n           ,date_dim\n       where ss_item_sk in (select ss_item_sk from cross_items)\n         and ss_item_sk = i_item_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year = 1999+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'catalog' channel, i_brand_id,i_class_id,i_category_id, sum(cs_quantity*cs_list_price) sales, count(*) number_sales\n       from catalog_sales\n           ,item\n           ,date_dim\n       where cs_item_sk in (select ss_item_sk from cross_items)\n         and cs_item_sk = i_item_sk\n         and cs_sold_date_sk = d_date_sk\n         and d_year = 1999+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(cs_quantity*cs_list_price) > (select average_sales from avg_sales)\n       union all\n       select 'web' channel, i_brand_id,i_class_id,i_category_id, sum(ws_quantity*ws_list_price) sales , count(*) number_sales\n       from web_sales\n           ,item\n           ,date_dim\n       where ws_item_sk in (select ss_item_sk from cross_items)\n         and ws_item_sk = i_item_sk\n         and ws_sold_date_sk = d_date_sk\n         and d_year = 1999+2\n         and d_moy = 11\n       group by i_brand_id,i_class_id,i_category_id\n       having sum(ws_quantity*ws_list_price) > (select average_sales from avg_sales)\n ) y\n group by rollup (channel, i_brand_id,i_class_id,i_category_id)\n order by channel,i_brand_id,i_class_id,i_category_id\n limit 100\"\"\",\n    \"q14b\" ->\n      \"\"\"\nwith  cross_items as\n (select i_item_sk ss_item_sk\n from item,\n (select iss.i_brand_id brand_id\n     ,iss.i_class_id class_id\n     ,iss.i_category_id category_id\n from store_sales\n     ,item iss\n     ,date_dim d1\n where ss_item_sk = iss.i_item_sk\n   and ss_sold_date_sk = d1.d_date_sk\n   and d1.d_year between 1999 AND 1999 + 2\n intersect\n select ics.i_brand_id\n     ,ics.i_class_id\n     ,ics.i_category_id\n from catalog_sales\n     ,item ics\n     ,date_dim d2\n where cs_item_sk = ics.i_item_sk\n   and cs_sold_date_sk = d2.d_date_sk\n   and d2.d_year between 1999 AND 1999 + 2\n intersect\n select iws.i_brand_id\n     ,iws.i_class_id\n     ,iws.i_category_id\n from web_sales\n     ,item iws\n     ,date_dim d3\n where ws_item_sk = iws.i_item_sk\n   and ws_sold_date_sk = d3.d_date_sk\n   and d3.d_year between 1999 AND 1999 + 2) x\n where i_brand_id = brand_id\n      and i_class_id = class_id\n      and i_category_id = category_id\n),\n avg_sales as\n(select avg(quantity*list_price) average_sales\n  from (select ss_quantity quantity\n             ,ss_list_price list_price\n       from store_sales\n           ,date_dim\n       where ss_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select cs_quantity quantity\n             ,cs_list_price list_price\n       from catalog_sales\n           ,date_dim\n       where cs_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2\n       union all\n       select ws_quantity quantity\n             ,ws_list_price list_price\n       from web_sales\n           ,date_dim\n       where ws_sold_date_sk = d_date_sk\n         and d_year between 1999 and 1999 + 2) x)\n  select  this_year.channel ty_channel\n                           ,this_year.i_brand_id ty_brand\n                           ,this_year.i_class_id ty_class\n                           ,this_year.i_category_id ty_category\n                           ,this_year.sales ty_sales\n                           ,this_year.number_sales ty_number_sales\n                           ,last_year.channel ly_channel\n                           ,last_year.i_brand_id ly_brand\n                           ,last_year.i_class_id ly_class\n                           ,last_year.i_category_id ly_category\n                           ,last_year.sales ly_sales\n                           ,last_year.number_sales ly_number_sales\n from\n (select 'store' channel, i_brand_id,i_class_id,i_category_id\n        ,sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1999 + 1\n                       and d_moy = 12\n                       and d_dom = 5)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) this_year,\n (select 'store' channel, i_brand_id,i_class_id\n        ,i_category_id, sum(ss_quantity*ss_list_price) sales, count(*) number_sales\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk in (select ss_item_sk from cross_items)\n   and ss_item_sk = i_item_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_week_seq = (select d_week_seq\n                     from date_dim\n                     where d_year = 1999\n                       and d_moy = 12\n                       and d_dom = 5)\n group by i_brand_id,i_class_id,i_category_id\n having sum(ss_quantity*ss_list_price) > (select average_sales from avg_sales)) last_year\n where this_year.i_brand_id= last_year.i_brand_id\n   and this_year.i_class_id = last_year.i_class_id\n   and this_year.i_category_id = last_year.i_category_id\n order by this_year.channel, this_year.i_brand_id, this_year.i_class_id, this_year.i_category_id\n limit 100\"\"\",\n    \"q15\" ->\n      \"\"\"\nselect  ca_zip\n       ,sum(cs_sales_price)\n from catalog_sales\n     ,customer\n     ,customer_address\n     ,date_dim\n where cs_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475',\n                                   '85392', '85460', '80348', '81792')\n \t      or ca_state in ('CA','WA','GA')\n \t      or cs_sales_price > 500)\n \tand cs_sold_date_sk = d_date_sk\n \tand d_qoy = 1 and d_year = 1998\n group by ca_zip\n order by ca_zip\n limit 100\"\"\",\n    \"q16\" ->\n      \"\"\"\nselect\n   count(distinct cs_order_number) as `order count`\n  ,sum(cs_ext_ship_cost) as `total shipping cost`\n  ,sum(cs_net_profit) as `total net profit`\nfrom\n   catalog_sales cs1\n  ,date_dim\n  ,customer_address\n  ,call_center\nwhere\n    d_date between '2000-3-01' and\n           (cast('2000-3-01' as date) + INTERVAL 60 days)\nand cs1.cs_ship_date_sk = d_date_sk\nand cs1.cs_ship_addr_sk = ca_address_sk\nand ca_state = 'IA'\nand cs1.cs_call_center_sk = cc_call_center_sk\nand cc_county in ('Luce County','Wadena County','Jefferson Davis Parish','Daviess County',\n                  'Williamson County'\n)\nand exists (select *\n            from catalog_sales cs2\n            where cs1.cs_order_number = cs2.cs_order_number\n              and cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk)\nand not exists(select *\n               from catalog_returns cr1\n               where cs1.cs_order_number = cr1.cr_order_number)\norder by count(distinct cs_order_number)\nlimit 100\"\"\",\n    \"q17\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,s_state\n       ,count(ss_quantity) as store_sales_quantitycount\n       ,avg(ss_quantity) as store_sales_quantityave\n       ,stddev_samp(ss_quantity) as store_sales_quantitystdev\n       ,stddev_samp(ss_quantity)/avg(ss_quantity) as store_sales_quantitycov\n       ,count(sr_return_quantity) as store_returns_quantitycount\n       ,avg(sr_return_quantity) as store_returns_quantityave\n       ,stddev_samp(sr_return_quantity) as store_returns_quantitystdev\n       ,stddev_samp(sr_return_quantity)/avg(sr_return_quantity) as store_returns_quantitycov\n       ,count(cs_quantity) as catalog_sales_quantitycount ,avg(cs_quantity) as catalog_sales_quantityave\n       ,stddev_samp(cs_quantity) as catalog_sales_quantitystdev\n       ,stddev_samp(cs_quantity)/avg(cs_quantity) as catalog_sales_quantitycov\n from store_sales\n     ,store_returns\n     ,catalog_sales\n     ,date_dim d1\n     ,date_dim d2\n     ,date_dim d3\n     ,store\n     ,item\n where d1.d_quarter_name = '1999Q1'\n   and d1.d_date_sk = ss_sold_date_sk\n   and i_item_sk = ss_item_sk\n   and s_store_sk = ss_store_sk\n   and ss_customer_sk = sr_customer_sk\n   and ss_item_sk = sr_item_sk\n   and ss_ticket_number = sr_ticket_number\n   and sr_returned_date_sk = d2.d_date_sk\n   and d2.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n   and sr_customer_sk = cs_bill_customer_sk\n   and sr_item_sk = cs_item_sk\n   and cs_sold_date_sk = d3.d_date_sk\n   and d3.d_quarter_name in ('1999Q1','1999Q2','1999Q3')\n group by i_item_id\n         ,i_item_desc\n         ,s_state\n order by i_item_id\n         ,i_item_desc\n         ,s_state\nlimit 100\"\"\",\n    \"q18\" ->\n      \"\"\"\nselect  i_item_id,\n        ca_country,\n        ca_state,\n        ca_county,\n        avg( cast(cs_quantity as decimal(12,2))) agg1,\n        avg( cast(cs_list_price as decimal(12,2))) agg2,\n        avg( cast(cs_coupon_amt as decimal(12,2))) agg3,\n        avg( cast(cs_sales_price as decimal(12,2))) agg4,\n        avg( cast(cs_net_profit as decimal(12,2))) agg5,\n        avg( cast(c_birth_year as decimal(12,2))) agg6,\n        avg( cast(cd1.cd_dep_count as decimal(12,2))) agg7\n from catalog_sales, customer_demographics cd1,\n      customer_demographics cd2, customer, customer_address, date_dim, item\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd1.cd_demo_sk and\n       cs_bill_customer_sk = c_customer_sk and\n       cd1.cd_gender = 'F' and\n       cd1.cd_education_status = 'Unknown' and\n       c_current_cdemo_sk = cd2.cd_demo_sk and\n       c_current_addr_sk = ca_address_sk and\n       c_birth_month in (4,8,12,10,11,9) and\n       d_year = 2001 and\n       ca_state in ('AR','IA','TX'\n                   ,'KS','LA','NC','SD')\n group by rollup (i_item_id, ca_country, ca_state, ca_county)\n order by ca_country,\n        ca_state,\n        ca_county,\n\ti_item_id\n limit 100\"\"\",\n    \"q19\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand, i_manufact_id, i_manufact,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item,customer,customer_address,store\n where d_date_sk = ss_sold_date_sk\n   and ss_item_sk = i_item_sk\n   and i_manager_id=63\n   and d_moy=11\n   and d_year=2002\n   and ss_customer_sk = c_customer_sk\n   and c_current_addr_sk = ca_address_sk\n   and substr(ca_zip,1,5) <> substr(s_zip,1,5)\n   and ss_store_sk = s_store_sk\n group by i_brand\n      ,i_brand_id\n      ,i_manufact_id\n      ,i_manufact\n order by ext_price desc\n         ,i_brand\n         ,i_brand_id\n         ,i_manufact_id\n         ,i_manufact\nlimit 100 \"\"\",\n    \"q20\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_category\n       ,i_class\n       ,i_current_price\n       ,sum(cs_ext_sales_price) as itemrevenue\n       ,sum(cs_ext_sales_price)*100/sum(sum(cs_ext_sales_price)) over\n           (partition by i_class) as revenueratio\n from\tcatalog_sales\n     ,item\n     ,date_dim\n where cs_item_sk = i_item_sk\n   and i_category in ('Electronics', 'Children', 'Home')\n   and cs_sold_date_sk = d_date_sk\n and d_date between cast('2002-03-19' as date)\n \t\t\t\tand (cast('2002-03-19' as date) + INTERVAL 30 days)\n group by i_item_id\n         ,i_item_desc\n         ,i_category\n         ,i_class\n         ,i_current_price\n order by i_category\n         ,i_class\n         ,i_item_id\n         ,i_item_desc\n         ,revenueratio\nlimit 100\"\"\",\n    \"q21\" ->\n      \"\"\"\nselect  *\n from(select w_warehouse_name\n            ,i_item_id\n            ,sum(case when (cast(d_date as date) < cast ('1999-04-12' as date))\n\t                then inv_quantity_on_hand\n                      else 0 end) as inv_before\n            ,sum(case when (cast(d_date as date) >= cast ('1999-04-12' as date))\n                      then inv_quantity_on_hand\n                      else 0 end) as inv_after\n   from inventory\n       ,warehouse\n       ,item\n       ,date_dim\n   where i_current_price between 0.99 and 1.49\n     and i_item_sk          = inv_item_sk\n     and inv_warehouse_sk   = w_warehouse_sk\n     and inv_date_sk    = d_date_sk\n     and d_date between (cast ('1999-04-12' as date) - INTERVAL 30 days)\n                    and (cast ('1999-04-12' as date) + INTERVAL 30 days)\n   group by w_warehouse_name, i_item_id) x\n where (case when inv_before > 0\n             then inv_after / inv_before\n             else null\n             end) between 2.0/3.0 and 3.0/2.0\n order by w_warehouse_name\n         ,i_item_id\n limit 100\"\"\",\n    \"q22\" ->\n      \"\"\"\nselect  i_product_name\n             ,i_brand\n             ,i_class\n             ,i_category\n             ,avg(inv_quantity_on_hand) qoh\n       from inventory\n           ,date_dim\n           ,item\n       where inv_date_sk=d_date_sk\n              and inv_item_sk=i_item_sk\n              and d_month_seq between 1188 and 1188 + 11\n       group by rollup(i_product_name\n                       ,i_brand\n                       ,i_class\n                       ,i_category)\norder by qoh, i_product_name, i_brand, i_class, i_category\nlimit 100\"\"\",\n    \"q23a\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (1998,1998+1,1998+2,1998+3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (1998,1998+1,1998+2,1998+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\nfrom\n max_store_sales))\n  select  sum(sales)\n from (select cs_quantity*cs_list_price sales\n       from catalog_sales\n           ,date_dim\n       where d_year = 1998\n         and d_moy = 7\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n      union all\n      select ws_quantity*ws_list_price sales\n       from web_sales\n           ,date_dim\n       where d_year = 1998\n         and d_moy = 7\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer))\n limit 100\"\"\",\n    \"q23b\" ->\n      \"\"\"\nwith frequent_ss_items as\n (select substr(i_item_desc,1,30) itemdesc,i_item_sk item_sk,d_date solddate,count(*) cnt\n  from store_sales\n      ,date_dim\n      ,item\n  where ss_sold_date_sk = d_date_sk\n    and ss_item_sk = i_item_sk\n    and d_year in (1998,1998 + 1,1998 + 2,1998 + 3)\n  group by substr(i_item_desc,1,30),i_item_sk,d_date\n  having count(*) >4),\n max_store_sales as\n (select max(csales) tpcds_cmax\n  from (select c_customer_sk,sum(ss_quantity*ss_sales_price) csales\n        from store_sales\n            ,customer\n            ,date_dim\n        where ss_customer_sk = c_customer_sk\n         and ss_sold_date_sk = d_date_sk\n         and d_year in (1998,1998+1,1998+2,1998+3)\n        group by c_customer_sk)),\n best_ss_customer as\n (select c_customer_sk,sum(ss_quantity*ss_sales_price) ssales\n  from store_sales\n      ,customer\n  where ss_customer_sk = c_customer_sk\n  group by c_customer_sk\n  having sum(ss_quantity*ss_sales_price) > (95/100.0) * (select\n  *\n from max_store_sales))\n  select  c_last_name,c_first_name,sales\n from (select c_last_name,c_first_name,sum(cs_quantity*cs_list_price) sales\n        from catalog_sales\n            ,customer\n            ,date_dim\n        where d_year = 1998\n         and d_moy = 7\n         and cs_sold_date_sk = d_date_sk\n         and cs_item_sk in (select item_sk from frequent_ss_items)\n         and cs_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and cs_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name\n      union all\n      select c_last_name,c_first_name,sum(ws_quantity*ws_list_price) sales\n       from web_sales\n           ,customer\n           ,date_dim\n       where d_year = 1998\n         and d_moy = 7\n         and ws_sold_date_sk = d_date_sk\n         and ws_item_sk in (select item_sk from frequent_ss_items)\n         and ws_bill_customer_sk in (select c_customer_sk from best_ss_customer)\n         and ws_bill_customer_sk = c_customer_sk\n       group by c_last_name,c_first_name)\n     order by c_last_name,c_first_name,sales\n  limit 100\"\"\",\n    \"q24a\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\nand s_market_id=7\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'goldenrod'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                                 from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q24b\" ->\n      \"\"\"\nwith ssales as\n(select c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,ca_state\n      ,s_state\n      ,i_color\n      ,i_current_price\n      ,i_manager_id\n      ,i_units\n      ,i_size\n      ,sum(ss_sales_price) netpaid\nfrom store_sales\n    ,store_returns\n    ,store\n    ,item\n    ,customer\n    ,customer_address\nwhere ss_ticket_number = sr_ticket_number\n  and ss_item_sk = sr_item_sk\n  and ss_customer_sk = c_customer_sk\n  and ss_item_sk = i_item_sk\n  and ss_store_sk = s_store_sk\n  and c_current_addr_sk = ca_address_sk\n  and c_birth_country <> upper(ca_country)\n  and s_zip = ca_zip\n  and s_market_id = 7\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\n        ,ca_state\n        ,s_state\n        ,i_color\n        ,i_current_price\n        ,i_manager_id\n        ,i_units\n        ,i_size)\nselect c_last_name\n      ,c_first_name\n      ,s_store_name\n      ,sum(netpaid) paid\nfrom ssales\nwhere i_color = 'magenta'\ngroup by c_last_name\n        ,c_first_name\n        ,s_store_name\nhaving sum(netpaid) > (select 0.05*avg(netpaid)\n                           from ssales)\norder by c_last_name\n        ,c_first_name\n        ,s_store_name\"\"\",\n    \"q25\" ->\n      \"\"\"\nselect\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n ,min(ss_net_profit) as store_sales_profit\n ,min(sr_net_loss) as store_returns_loss\n ,min(cs_net_profit) as catalog_sales_profit\n from\n store_sales\n ,store_returns\n ,catalog_sales\n ,date_dim d1\n ,date_dim d2\n ,date_dim d3\n ,store\n ,item\n where\n d1.d_moy = 4\n and d1.d_year = 2002\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk = ss_item_sk\n and s_store_sk = ss_store_sk\n and ss_customer_sk = sr_customer_sk\n and ss_item_sk = sr_item_sk\n and ss_ticket_number = sr_ticket_number\n and sr_returned_date_sk = d2.d_date_sk\n and d2.d_moy               between 4 and  10\n and d2.d_year              = 2002\n and sr_customer_sk = cs_bill_customer_sk\n and sr_item_sk = cs_item_sk\n and cs_sold_date_sk = d3.d_date_sk\n and d3.d_moy               between 4 and  10\n and d3.d_year              = 2002\n group by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n order by\n i_item_id\n ,i_item_desc\n ,s_store_id\n ,s_store_name\n limit 100\"\"\",\n    \"q26\" ->\n      \"\"\"\nselect  i_item_id,\n        avg(cs_quantity) agg1,\n        avg(cs_list_price) agg2,\n        avg(cs_coupon_amt) agg3,\n        avg(cs_sales_price) agg4\n from catalog_sales, customer_demographics, date_dim, item, promotion\n where cs_sold_date_sk = d_date_sk and\n       cs_item_sk = i_item_sk and\n       cs_bill_cdemo_sk = cd_demo_sk and\n       cs_promo_sk = p_promo_sk and\n       cd_gender = 'F' and\n       cd_marital_status = 'M' and\n       cd_education_status = '4 yr Degree' and\n       (p_channel_email = 'N' or p_channel_event = 'N') and\n       d_year = 1998\n group by i_item_id\n order by i_item_id\n limit 100\"\"\",\n    \"q27\" ->\n      \"\"\"\nselect  i_item_id,\n        s_state, grouping(s_state) g_state,\n        avg(ss_quantity) agg1,\n        avg(ss_list_price) agg2,\n        avg(ss_coupon_amt) agg3,\n        avg(ss_sales_price) agg4\n from store_sales, customer_demographics, date_dim, store, item\n where ss_sold_date_sk = d_date_sk and\n       ss_item_sk = i_item_sk and\n       ss_store_sk = s_store_sk and\n       ss_cdemo_sk = cd_demo_sk and\n       cd_gender = 'M' and\n       cd_marital_status = 'M' and\n       cd_education_status = 'Secondary' and\n       d_year = 1999 and\n       s_state in ('AL','FL', 'TX', 'NM', 'MI', 'GA')\n group by rollup (i_item_id, s_state)\n order by i_item_id\n         ,s_state\n limit 100\"\"\",\n    \"q28\" ->\n      \"\"\"\nselect  *\nfrom (select avg(ss_list_price) B1_LP\n            ,count(ss_list_price) B1_CNT\n            ,count(distinct ss_list_price) B1_CNTD\n      from store_sales\n      where ss_quantity between 0 and 5\n        and (ss_list_price between 74 and 74+10\n             or ss_coupon_amt between 2949 and 2949+1000\n             or ss_wholesale_cost between 49 and 49+20)) B1,\n     (select avg(ss_list_price) B2_LP\n            ,count(ss_list_price) B2_CNT\n            ,count(distinct ss_list_price) B2_CNTD\n      from store_sales\n      where ss_quantity between 6 and 10\n        and (ss_list_price between 136 and 136+10\n          or ss_coupon_amt between 10027 and 10027+1000\n          or ss_wholesale_cost between 53 and 53+20)) B2,\n     (select avg(ss_list_price) B3_LP\n            ,count(ss_list_price) B3_CNT\n            ,count(distinct ss_list_price) B3_CNTD\n      from store_sales\n      where ss_quantity between 11 and 15\n        and (ss_list_price between 73 and 73+10\n          or ss_coupon_amt between 1451 and 1451+1000\n          or ss_wholesale_cost between 78 and 78+20)) B3,\n     (select avg(ss_list_price) B4_LP\n            ,count(ss_list_price) B4_CNT\n            ,count(distinct ss_list_price) B4_CNTD\n      from store_sales\n      where ss_quantity between 16 and 20\n        and (ss_list_price between 87 and 87+10\n          or ss_coupon_amt between 17007 and 17007+1000\n          or ss_wholesale_cost between 55 and 55+20)) B4,\n     (select avg(ss_list_price) B5_LP\n            ,count(ss_list_price) B5_CNT\n            ,count(distinct ss_list_price) B5_CNTD\n      from store_sales\n      where ss_quantity between 21 and 25\n        and (ss_list_price between 112 and 112+10\n          or ss_coupon_amt between 17243 and 17243+1000\n          or ss_wholesale_cost between 2 and 2+20)) B5,\n     (select avg(ss_list_price) B6_LP\n            ,count(ss_list_price) B6_CNT\n            ,count(distinct ss_list_price) B6_CNTD\n      from store_sales\n      where ss_quantity between 26 and 30\n        and (ss_list_price between 119 and 119+10\n          or ss_coupon_amt between 4954 and 4954+1000\n          or ss_wholesale_cost between 22 and 22+20)) B6\nlimit 100\"\"\",\n    \"q29\" ->\n      \"\"\"\nselect\n     i_item_id\n    ,i_item_desc\n    ,s_store_id\n    ,s_store_name\n    ,stddev_samp(ss_quantity)        as store_sales_quantity\n    ,stddev_samp(sr_return_quantity) as store_returns_quantity\n    ,stddev_samp(cs_quantity)        as catalog_sales_quantity\n from\n    store_sales\n   ,store_returns\n   ,catalog_sales\n   ,date_dim             d1\n   ,date_dim             d2\n   ,date_dim             d3\n   ,store\n   ,item\n where\n     d1.d_moy               = 4\n and d1.d_year              = 2000\n and d1.d_date_sk           = ss_sold_date_sk\n and i_item_sk              = ss_item_sk\n and s_store_sk             = ss_store_sk\n and ss_customer_sk         = sr_customer_sk\n and ss_item_sk             = sr_item_sk\n and ss_ticket_number       = sr_ticket_number\n and sr_returned_date_sk    = d2.d_date_sk\n and d2.d_moy               between 4 and  4 + 3\n and d2.d_year              = 2000\n and sr_customer_sk         = cs_bill_customer_sk\n and sr_item_sk             = cs_item_sk\n and cs_sold_date_sk        = d3.d_date_sk\n and d3.d_year              in (2000,2000+1,2000+2)\n group by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n order by\n    i_item_id\n   ,i_item_desc\n   ,s_store_id\n   ,s_store_name\n limit 100\"\"\",\n    \"q30\" ->\n      \"\"\"\nwith customer_total_return as\n (select wr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(wr_return_amt) as ctr_total_return\n from web_returns\n     ,date_dim\n     ,customer_address\n where wr_returned_date_sk = d_date_sk\n   and d_year =2001\n   and wr_returning_addr_sk = ca_address_sk\n group by wr_returning_customer_sk\n         ,ca_state)\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n       ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n       ,c_last_review_date,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'MI'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,c_preferred_cust_flag\n                  ,c_birth_day,c_birth_month,c_birth_year,c_birth_country,c_login,c_email_address\n                  ,c_last_review_date,ctr_total_return\nlimit 100\"\"\",\n    \"q31\" ->\n      \"\"\"\nwith ss as\n (select ca_county,d_qoy, d_year,sum(ss_ext_sales_price) as store_sales\n from store_sales,date_dim,customer_address\n where ss_sold_date_sk = d_date_sk\n  and ss_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year),\n ws as\n (select ca_county,d_qoy, d_year,sum(ws_ext_sales_price) as web_sales\n from web_sales,date_dim,customer_address\n where ws_sold_date_sk = d_date_sk\n  and ws_bill_addr_sk=ca_address_sk\n group by ca_county,d_qoy, d_year)\n select\n        ss1.ca_county\n       ,ss1.d_year\n       ,ws2.web_sales/ws1.web_sales web_q1_q2_increase\n       ,ss2.store_sales/ss1.store_sales store_q1_q2_increase\n       ,ws3.web_sales/ws2.web_sales web_q2_q3_increase\n       ,ss3.store_sales/ss2.store_sales store_q2_q3_increase\n from\n        ss ss1\n       ,ss ss2\n       ,ss ss3\n       ,ws ws1\n       ,ws ws2\n       ,ws ws3\n where\n    ss1.d_qoy = 1\n    and ss1.d_year = 2000\n    and ss1.ca_county = ss2.ca_county\n    and ss2.d_qoy = 2\n    and ss2.d_year = 2000\n and ss2.ca_county = ss3.ca_county\n    and ss3.d_qoy = 3\n    and ss3.d_year = 2000\n    and ss1.ca_county = ws1.ca_county\n    and ws1.d_qoy = 1\n    and ws1.d_year = 2000\n    and ws1.ca_county = ws2.ca_county\n    and ws2.d_qoy = 2\n    and ws2.d_year = 2000\n    and ws1.ca_county = ws3.ca_county\n    and ws3.d_qoy = 3\n    and ws3.d_year =2000\n    and case when ws1.web_sales > 0 then ws2.web_sales/ws1.web_sales else null end\n       > case when ss1.store_sales > 0 then ss2.store_sales/ss1.store_sales else null end\n    and case when ws2.web_sales > 0 then ws3.web_sales/ws2.web_sales else null end\n       > case when ss2.store_sales > 0 then ss3.store_sales/ss2.store_sales else null end\n order by store_q1_q2_increase\"\"\",\n    \"q32\" ->\n      \"\"\"\nselect  sum(cs_ext_discount_amt)  as `excess discount amount`\nfrom\n   catalog_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 490\nand i_item_sk = cs_item_sk\nand d_date between '1999-01-27' and\n        (cast('1999-01-27' as date) + INTERVAL 90 days)\nand d_date_sk = cs_sold_date_sk\nand cs_ext_discount_amt\n     > (\n         select\n            1.3 * avg(cs_ext_discount_amt)\n         from\n            catalog_sales\n           ,date_dim\n         where\n              cs_item_sk = i_item_sk\n          and d_date between '1999-01-27' and\n                             (cast('1999-01-27' as date) + INTERVAL 90 days)\n          and d_date_sk = cs_sold_date_sk\n      )\nlimit 100\"\"\",\n    \"q33\" ->\n      \"\"\"\nwith ss as (\n select\n          i_manufact_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 1\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n cs as (\n select\n          i_manufact_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 1\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id),\n ws as (\n select\n          i_manufact_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_manufact_id               in (select\n  i_manufact_id\nfrom\n item\nwhere i_category in ('Electronics'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 2001\n and     d_moy                   = 1\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -6\n group by i_manufact_id)\n  select  i_manufact_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_manufact_id\n order by total_sales\nlimit 100\"\"\",\n    \"q34\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (date_dim.d_dom between 1 and 3 or date_dim.d_dom between 25 and 28)\n    and (household_demographics.hd_buy_potential = '1001-5000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and (case when household_demographics.hd_vehicle_count > 0\n\tthen household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count\n\telse null\n\tend)  > 1.2\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Nez Perce County','Murray County','Surry County','Calhoun County',\n                           'Wilkinson County','Brown County','Wallace County','Carter County')\n    group by ss_ticket_number,ss_customer_sk) dn,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 15 and 20\n    order by c_last_name,c_first_name,c_salutation,c_preferred_cust_flag desc, ss_ticket_number\"\"\",\n    \"q35\" ->\n      \"\"\"\nselect\n  ca_state,\n  cd_gender,\n  cd_marital_status,\n  cd_dep_count,\n  count(*) cnt1,\n  stddev_samp(cd_dep_count),\n  sum(cd_dep_count),\n  min(cd_dep_count),\n  cd_dep_employed_count,\n  count(*) cnt2,\n  stddev_samp(cd_dep_employed_count),\n  sum(cd_dep_employed_count),\n  min(cd_dep_employed_count),\n  cd_dep_college_count,\n  count(*) cnt3,\n  stddev_samp(cd_dep_college_count),\n  sum(cd_dep_college_count),\n  min(cd_dep_college_count)\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 2002 and\n                d_qoy < 4) and\n   (exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4) or\n    exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 2002 and\n                  d_qoy < 4))\n group by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n order by ca_state,\n          cd_gender,\n          cd_marital_status,\n          cd_dep_count,\n          cd_dep_employed_count,\n          cd_dep_college_count\n limit 100\"\"\",\n    \"q36\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit)/sum(ss_ext_sales_price) as gross_margin\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ss_net_profit)/sum(ss_ext_sales_price) asc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,item\n   ,store\n where\n    d1.d_year = 2000\n and d1.d_date_sk = ss_sold_date_sk\n and i_item_sk  = ss_item_sk\n and s_store_sk  = ss_store_sk\n and s_state in ('MN','TX','TX','IN',\n                 'CA','LA','NM','TX')\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then i_category end\n  ,rank_within_parent\n  limit 100\"\"\",\n    \"q37\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, catalog_sales\n where i_current_price between 16 and 16 + 30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2002-06-05' as date) and (cast('2002-06-05' as date) + interval 60 days)\n and i_manufact_id in (841,790,796,739)\n and inv_quantity_on_hand between 100 and 500\n and cs_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q38\" ->\n      \"\"\"\nselect  count(*) from (\n    select distinct c_last_name, c_first_name, d_date\n    from store_sales, date_dim, customer\n          where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n      and store_sales.ss_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1203 and 1203 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from catalog_sales, date_dim, customer\n          where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n      and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1203 and 1203 + 11\n  intersect\n    select distinct c_last_name, c_first_name, d_date\n    from web_sales, date_dim, customer\n          where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n      and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n      and d_month_seq between 1203 and 1203 + 11\n) hot_cust\nlimit 100\"\"\",\n    \"q39a\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1999\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q39b\" ->\n      \"\"\"\nwith inv as\n(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n       ,stdev,mean, case mean when 0 then null else stdev/mean end cov\n from(select w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy\n            ,stddev_samp(inv_quantity_on_hand) stdev,avg(inv_quantity_on_hand) mean\n      from inventory\n          ,item\n          ,warehouse\n          ,date_dim\n      where inv_item_sk = i_item_sk\n        and inv_warehouse_sk = w_warehouse_sk\n        and inv_date_sk = d_date_sk\n        and d_year =1999\n      group by w_warehouse_name,w_warehouse_sk,i_item_sk,d_moy) foo\n where case mean when 0 then 0 else stdev/mean end > 1)\nselect inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean, inv1.cov\n        ,inv2.w_warehouse_sk,inv2.i_item_sk,inv2.d_moy,inv2.mean, inv2.cov\nfrom inv inv1,inv inv2\nwhere inv1.i_item_sk = inv2.i_item_sk\n  and inv1.w_warehouse_sk =  inv2.w_warehouse_sk\n  and inv1.d_moy=3\n  and inv2.d_moy=3+1\n  and inv1.cov > 1.5\norder by inv1.w_warehouse_sk,inv1.i_item_sk,inv1.d_moy,inv1.mean,inv1.cov\n        ,inv2.d_moy,inv2.mean, inv2.cov\"\"\",\n    \"q40\" ->\n      \"\"\"\nselect\n   w_state\n  ,i_item_id\n  ,sum(case when (cast(d_date as date) < cast ('1999-04-27' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_before\n  ,sum(case when (cast(d_date as date) >= cast ('1999-04-27' as date))\n \t\tthen cs_sales_price - coalesce(cr_refunded_cash,0) else 0 end) as sales_after\n from\n   catalog_sales left outer join catalog_returns on\n       (cs_order_number = cr_order_number\n        and cs_item_sk = cr_item_sk)\n  ,warehouse\n  ,item\n  ,date_dim\n where\n     i_current_price between 0.99 and 1.49\n and i_item_sk          = cs_item_sk\n and cs_warehouse_sk    = w_warehouse_sk\n and cs_sold_date_sk    = d_date_sk\n and d_date between (cast ('1999-04-27' as date) - INTERVAL 30 days)\n                and (cast ('1999-04-27' as date) + INTERVAL 30 days)\n group by\n    w_state,i_item_id\n order by w_state,i_item_id\nlimit 100\"\"\",\n    \"q41\" ->\n      \"\"\"\nselect  distinct(i_product_name)\n from item i1\n where i_manufact_id between 841 and 841+40\n   and (select count(*) as item_cnt\n        from item\n        where (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'bisque' or i_color = 'khaki') and\n        (i_units = 'Carton' or i_units = 'Box') and\n        (i_size = 'large' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'antique' or i_color = 'sandy') and\n        (i_units = 'Pallet' or i_units = 'Cup') and\n        (i_size = 'petite' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'forest' or i_color = 'brown') and\n        (i_units = 'Dram' or i_units = 'Ton') and\n        (i_size = 'economy' or i_size = 'medium')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'chartreuse' or i_color = 'light') and\n        (i_units = 'Pound' or i_units = 'Dozen') and\n        (i_size = 'large' or i_size = 'extra large')\n        ))) or\n       (i_manufact = i1.i_manufact and\n        ((i_category = 'Women' and\n        (i_color = 'turquoise' or i_color = 'chocolate') and\n        (i_units = 'Bundle' or i_units = 'Unknown') and\n        (i_size = 'large' or i_size = 'extra large')\n        ) or\n        (i_category = 'Women' and\n        (i_color = 'maroon' or i_color = 'pale') and\n        (i_units = 'Each' or i_units = 'Tbl') and\n        (i_size = 'petite' or i_size = 'small')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'almond' or i_color = 'floral') and\n        (i_units = 'Gross' or i_units = 'N/A') and\n        (i_size = 'economy' or i_size = 'medium')\n        ) or\n        (i_category = 'Men' and\n        (i_color = 'drab' or i_color = 'plum') and\n        (i_units = 'Bunch' or i_units = 'Case') and\n        (i_size = 'large' or i_size = 'extra large')\n        )))) > 0\n order by i_product_name\n limit 100\"\"\",\n    \"q42\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_category_id\n \t,item.i_category\n \t,sum(ss_ext_sales_price)\n from \tdate_dim dt\n \t,store_sales\n \t,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n \tand store_sales.ss_item_sk = item.i_item_sk\n \tand item.i_manager_id = 1\n \tand dt.d_moy=11\n \tand dt.d_year=2002\n group by \tdt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\n order by       sum(ss_ext_sales_price) desc,dt.d_year\n \t\t,item.i_category_id\n \t\t,item.i_category\nlimit 100 \"\"\",\n    \"q43\" ->\n      \"\"\"\nselect  s_store_name, s_store_id,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from date_dim, store_sales, store\n where d_date_sk = ss_sold_date_sk and\n       s_store_sk = ss_store_sk and\n       s_gmt_offset = -5 and\n       d_year = 2002\n group by s_store_name, s_store_id\n order by s_store_name, s_store_id,sun_sales,mon_sales,tue_sales,wed_sales,thu_sales,fri_sales,sat_sales\n limit 100\"\"\",\n    \"q44\" ->\n      \"\"\"\nselect  asceding.rnk, i1.i_product_name best_performing, i2.i_product_name worst_performing\nfrom(select *\n     from (select item_sk,rank() over (order by rank_col asc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 709\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 709\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V1)V11\n     where rnk  < 11) asceding,\n    (select *\n     from (select item_sk,rank() over (order by rank_col desc) rnk\n           from (select ss_item_sk item_sk,avg(ss_net_profit) rank_col\n                 from store_sales ss1\n                 where ss_store_sk = 709\n                 group by ss_item_sk\n                 having avg(ss_net_profit) > 0.9*(select avg(ss_net_profit) rank_col\n                                                  from store_sales\n                                                  where ss_store_sk = 709\n                                                    and ss_addr_sk is null\n                                                  group by ss_store_sk))V2)V21\n     where rnk  < 11) descending,\nitem i1,\nitem i2\nwhere asceding.rnk = descending.rnk\n  and i1.i_item_sk=asceding.item_sk\n  and i2.i_item_sk=descending.item_sk\norder by asceding.rnk\nlimit 100\"\"\",\n    \"q45\" ->\n      \"\"\"\nselect  ca_zip, ca_state, sum(ws_sales_price)\n from web_sales, customer, customer_address, date_dim, item\n where ws_bill_customer_sk = c_customer_sk\n \tand c_current_addr_sk = ca_address_sk\n \tand ws_item_sk = i_item_sk\n \tand ( substr(ca_zip,1,5) in ('85669', '86197','88274','83405','86475', '85392', '85460', '80348', '81792')\n \t      or\n \t      i_item_id in (select i_item_id\n                             from item\n                             where i_item_sk in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n                             )\n \t    )\n \tand ws_sold_date_sk = d_date_sk\n \tand d_qoy = 2 and d_year = 2002\n group by ca_zip, ca_state\n order by ca_zip, ca_state\n limit 100\"\"\",\n    \"q46\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,amt,profit\n from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,ca_city bought_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics,customer_address\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and store_sales.ss_addr_sk = customer_address.ca_address_sk\n    and (household_demographics.hd_dep_count = 0 or\n         household_demographics.hd_vehicle_count= 1)\n    and date_dim.d_dow in (6,0)\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_city in ('Johnson','Norwood','Cambridge','Klondike','Rock Hill')\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,ca_city) dn,customer,customer_address current_addr\n    where ss_customer_sk = c_customer_sk\n      and customer.c_current_addr_sk = current_addr.ca_address_sk\n      and current_addr.ca_city <> bought_city\n  order by c_last_name\n          ,c_first_name\n          ,ca_city\n          ,bought_city\n          ,ss_ticket_number\n  limit 100\"\"\",\n    \"q47\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        s_store_name, s_company_name,\n        d_year, d_moy,\n        sum(ss_sales_price) sum_sales,\n        avg(sum(ss_sales_price)) over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     s_store_name, s_company_name\n           order by d_year, d_moy) rn\n from item, store_sales, date_dim, store\n where ss_item_sk = i_item_sk and\n       ss_sold_date_sk = d_date_sk and\n       ss_store_sk = s_store_sk and\n       (\n         d_year = 2001 or\n         ( d_year = 2001-1 and d_moy =12) or\n         ( d_year = 2001+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          s_store_name, s_company_name,\n          d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand, v1.s_store_name, v1.s_company_name\n        ,v1.d_year, v1.d_moy\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1.s_store_name = v1_lag.s_store_name and\n       v1.s_store_name = v1_lead.s_store_name and\n       v1.s_company_name = v1_lag.s_company_name and\n       v1.s_company_name = v1_lead.s_company_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2001 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, psum\n limit 100\"\"\",\n    \"q48\" ->\n      \"\"\"\nselect sum (ss_quantity)\n from store_sales, store, customer_demographics, customer_address, date_dim\n where s_store_sk = ss_store_sk\n and  ss_sold_date_sk = d_date_sk and d_year = 2000\n and\n (\n  (\n   cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'U'\n   and\n   cd_education_status = '2 yr Degree'\n   and\n   ss_sales_price between 100.00 and 150.00\n   )\n or\n  (\n  cd_demo_sk = ss_cdemo_sk\n   and\n   cd_marital_status = 'S'\n   and\n   cd_education_status = 'Primary'\n   and\n   ss_sales_price between 50.00 and 100.00\n  )\n or\n (\n  cd_demo_sk = ss_cdemo_sk\n  and\n   cd_marital_status = 'W'\n   and\n   cd_education_status = '4 yr Degree'\n   and\n   ss_sales_price between 150.00 and 200.00\n )\n )\n and\n (\n  (\n  ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('MT', 'OH', 'GA')\n  and ss_net_profit between 0 and 2000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('WV', 'AZ', 'NM')\n  and ss_net_profit between 150 and 3000\n  )\n or\n  (ss_addr_sk = ca_address_sk\n  and\n  ca_country = 'United States'\n  and\n  ca_state in ('NY', 'PA', 'KY')\n  and ss_net_profit between 50 and 25000\n  )\n )\"\"\",\n    \"q49\" ->\n      \"\"\"\nselect  channel, item, return_ratio, return_rank, currency_rank from\n (select\n 'web' as channel\n ,web.item\n ,web.return_ratio\n ,web.return_rank\n ,web.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect ws.ws_item_sk as item\n \t\t,(cast(sum(coalesce(wr.wr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(wr.wr_return_amt,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(ws.ws_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\t web_sales ws left outer join web_returns wr\n \t\t\ton (ws.ws_order_number = wr.wr_order_number and\n \t\t\tws.ws_item_sk = wr.wr_item_sk)\n                 ,date_dim\n \t\twhere\n \t\t\twr.wr_return_amt > 10000\n \t\t\tand ws.ws_net_profit > 1\n                         and ws.ws_net_paid > 0\n                         and ws.ws_quantity > 0\n                         and ws_sold_date_sk = d_date_sk\n                         and d_year = 1999\n                         and d_moy = 11\n \t\tgroup by ws.ws_item_sk\n \t) in_web\n ) web\n where\n (\n web.return_rank <= 10\n or\n web.currency_rank <= 10\n )\n union\n select\n 'catalog' as channel\n ,catalog.item\n ,catalog.return_ratio\n ,catalog.return_rank\n ,catalog.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect\n \t\tcs.cs_item_sk as item\n \t\t,(cast(sum(coalesce(cr.cr_return_quantity,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(cr.cr_return_amount,0)) as decimal(15,4))/\n \t\tcast(sum(coalesce(cs.cs_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tcatalog_sales cs left outer join catalog_returns cr\n \t\t\ton (cs.cs_order_number = cr.cr_order_number and\n \t\t\tcs.cs_item_sk = cr.cr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tcr.cr_return_amount > 10000\n \t\t\tand cs.cs_net_profit > 1\n                         and cs.cs_net_paid > 0\n                         and cs.cs_quantity > 0\n                         and cs_sold_date_sk = d_date_sk\n                         and d_year = 1999\n                         and d_moy = 11\n                 group by cs.cs_item_sk\n \t) in_cat\n ) catalog\n where\n (\n catalog.return_rank <= 10\n or\n catalog.currency_rank <=10\n )\n union\n select\n 'store' as channel\n ,store.item\n ,store.return_ratio\n ,store.return_rank\n ,store.currency_rank\n from (\n \tselect\n \t item\n \t,return_ratio\n \t,currency_ratio\n \t,rank() over (order by return_ratio) as return_rank\n \t,rank() over (order by currency_ratio) as currency_rank\n \tfrom\n \t(\tselect sts.ss_item_sk as item\n \t\t,(cast(sum(coalesce(sr.sr_return_quantity,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_quantity,0)) as decimal(15,4) )) as return_ratio\n \t\t,(cast(sum(coalesce(sr.sr_return_amt,0)) as decimal(15,4))/cast(sum(coalesce(sts.ss_net_paid,0)) as decimal(15,4) )) as currency_ratio\n \t\tfrom\n \t\tstore_sales sts left outer join store_returns sr\n \t\t\ton (sts.ss_ticket_number = sr.sr_ticket_number and sts.ss_item_sk = sr.sr_item_sk)\n                ,date_dim\n \t\twhere\n \t\t\tsr.sr_return_amt > 10000\n \t\t\tand sts.ss_net_profit > 1\n                         and sts.ss_net_paid > 0\n                         and sts.ss_quantity > 0\n                         and ss_sold_date_sk = d_date_sk\n                         and d_year = 1999\n                         and d_moy = 11\n \t\tgroup by sts.ss_item_sk\n \t) in_store\n ) store\n where  (\n store.return_rank <= 10\n or\n store.currency_rank <= 10\n )\n )\n order by 1,4,5,2\n limit 100\"\"\",\n    \"q50\" ->\n      \"\"\"\nselect\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 30) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 60) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk > 90) and\n                 (sr_returned_date_sk - ss_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (sr_returned_date_sk - ss_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   store_sales\n  ,store_returns\n  ,store\n  ,date_dim d1\n  ,date_dim d2\nwhere\n    d2.d_year = 2000\nand d2.d_moy  = 9\nand ss_ticket_number = sr_ticket_number\nand ss_item_sk = sr_item_sk\nand ss_sold_date_sk   = d1.d_date_sk\nand sr_returned_date_sk   = d2.d_date_sk\nand ss_customer_sk = sr_customer_sk\nand ss_store_sk = s_store_sk\ngroup by\n   s_store_name\n  ,s_company_id\n  ,s_street_number\n  ,s_street_name\n  ,s_street_type\n  ,s_suite_number\n  ,s_city\n  ,s_county\n  ,s_state\n  ,s_zip\norder by s_store_name\n        ,s_company_id\n        ,s_street_number\n        ,s_street_name\n        ,s_street_type\n        ,s_suite_number\n        ,s_city\n        ,s_county\n        ,s_state\n        ,s_zip\nlimit 100\"\"\",\n    \"q51\" ->\n      \"\"\"\nWITH web_v1 as (\nselect\n  ws_item_sk item_sk, d_date,\n  sum(sum(ws_sales_price))\n      over (partition by ws_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom web_sales\n    ,date_dim\nwhere ws_sold_date_sk=d_date_sk\n  and d_month_seq between 1177 and 1177+11\n  and ws_item_sk is not NULL\ngroup by ws_item_sk, d_date),\nstore_v1 as (\nselect\n  ss_item_sk item_sk, d_date,\n  sum(sum(ss_sales_price))\n      over (partition by ss_item_sk order by d_date rows between unbounded preceding and current row) cume_sales\nfrom store_sales\n    ,date_dim\nwhere ss_sold_date_sk=d_date_sk\n  and d_month_seq between 1177 and 1177+11\n  and ss_item_sk is not NULL\ngroup by ss_item_sk, d_date)\n select  *\nfrom (select item_sk\n     ,d_date\n     ,web_sales\n     ,store_sales\n     ,max(web_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) web_cumulative\n     ,max(store_sales)\n         over (partition by item_sk order by d_date rows between unbounded preceding and current row) store_cumulative\n     from (select case when web.item_sk is not null then web.item_sk else store.item_sk end item_sk\n                 ,case when web.d_date is not null then web.d_date else store.d_date end d_date\n                 ,web.cume_sales web_sales\n                 ,store.cume_sales store_sales\n           from web_v1 web full outer join store_v1 store on (web.item_sk = store.item_sk\n                                                          and web.d_date = store.d_date)\n          )x )y\nwhere web_cumulative > store_cumulative\norder by item_sk\n        ,d_date\nlimit 100\"\"\",\n    \"q52\" ->\n      \"\"\"\nselect  dt.d_year\n \t,item.i_brand_id brand_id\n \t,item.i_brand brand\n \t,sum(ss_ext_sales_price) ext_price\n from date_dim dt\n     ,store_sales\n     ,item\n where dt.d_date_sk = store_sales.ss_sold_date_sk\n    and store_sales.ss_item_sk = item.i_item_sk\n    and item.i_manager_id = 1\n    and dt.d_moy=12\n    and dt.d_year=2001\n group by dt.d_year\n \t,item.i_brand\n \t,item.i_brand_id\n order by dt.d_year\n \t,ext_price desc\n \t,brand_id\nlimit 100 \"\"\",\n    \"q53\" ->\n      \"\"\"\nselect  * from\n(select i_manufact_id,\nsum(ss_sales_price) sum_sales,\navg(sum(ss_sales_price)) over (partition by i_manufact_id) avg_quarterly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\nss_sold_date_sk = d_date_sk and\nss_store_sk = s_store_sk and\nd_month_seq in (1188,1188+1,1188+2,1188+3,1188+4,1188+5,1188+6,1188+7,1188+8,1188+9,1188+10,1188+11) and\n((i_category in ('Books','Children','Electronics') and\ni_class in ('personal','portable','reference','self-help') and\ni_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t'exportiunivamalg #9','scholaramalgamalg #9'))\nor(i_category in ('Women','Music','Men') and\ni_class in ('accessories','classical','fragrances','pants') and\ni_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t'importoamalg #1')))\ngroup by i_manufact_id, d_qoy ) tmp1\nwhere case when avg_quarterly_sales > 0\n\tthen abs (sum_sales - avg_quarterly_sales)/ avg_quarterly_sales\n\telse null end > 0.1\norder by avg_quarterly_sales,\n\t sum_sales,\n\t i_manufact_id\nlimit 100\"\"\",\n    \"q54\" ->\n      \"\"\"\nwith my_customers as (\n select distinct c_customer_sk\n        , c_current_addr_sk\n from\n        ( select cs_sold_date_sk sold_date_sk,\n                 cs_bill_customer_sk customer_sk,\n                 cs_item_sk item_sk\n          from   catalog_sales\n          union all\n          select ws_sold_date_sk sold_date_sk,\n                 ws_bill_customer_sk customer_sk,\n                 ws_item_sk item_sk\n          from   web_sales\n         ) cs_or_ws_sales,\n         item,\n         date_dim,\n         customer\n where   sold_date_sk = d_date_sk\n         and item_sk = i_item_sk\n         and i_category = 'Men'\n         and i_class = 'pants'\n         and c_customer_sk = cs_or_ws_sales.customer_sk\n         and d_moy = 5\n         and d_year = 2002\n )\n , my_revenue as (\n select c_customer_sk,\n        sum(ss_ext_sales_price) as revenue\n from   my_customers,\n        store_sales,\n        customer_address,\n        store,\n        date_dim\n where  c_current_addr_sk = ca_address_sk\n        and ca_county = s_county\n        and ca_state = s_state\n        and ss_sold_date_sk = d_date_sk\n        and c_customer_sk = ss_customer_sk\n        and d_month_seq between (select distinct d_month_seq+1\n                                 from   date_dim where d_year = 2002 and d_moy = 5)\n                           and  (select distinct d_month_seq+3\n                                 from   date_dim where d_year = 2002 and d_moy = 5)\n group by c_customer_sk\n )\n , segments as\n (select cast((revenue/50) as int) as segment\n  from   my_revenue\n )\n  select  segment, count(*) as num_customers, segment*50 as segment_base\n from segments\n group by segment\n order by segment, num_customers\n limit 100\"\"\",\n    \"q55\" ->\n      \"\"\"\nselect  i_brand_id brand_id, i_brand brand,\n \tsum(ss_ext_sales_price) ext_price\n from date_dim, store_sales, item\n where d_date_sk = ss_sold_date_sk\n \tand ss_item_sk = i_item_sk\n \tand i_manager_id=67\n \tand d_moy=11\n \tand d_year=2001\n group by i_brand, i_brand_id\n order by ext_price desc, i_brand_id\nlimit 100 \"\"\",\n    \"q56\" ->\n      \"\"\"\nwith ss as (\n select i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where i_item_id in (select\n     i_item_id\nfrom item\nwhere i_color in ('blanched','spring','seashell'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n cs as (\n select i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('blanched','spring','seashell'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id),\n ws as (\n select i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom item\nwhere i_color in ('blanched','spring','seashell'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1999\n and     d_moy                   = 6\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -7\n group by i_item_id)\n  select  i_item_id ,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by total_sales,\n          i_item_id\n limit 100\"\"\",\n    \"q57\" ->\n      \"\"\"\nwith v1 as(\n select i_category, i_brand,\n        cc_name,\n        d_year, d_moy,\n        sum(cs_sales_price) sum_sales,\n        avg(sum(cs_sales_price)) over\n          (partition by i_category, i_brand,\n                     cc_name, d_year)\n          avg_monthly_sales,\n        rank() over\n          (partition by i_category, i_brand,\n                     cc_name\n           order by d_year, d_moy) rn\n from item, catalog_sales, date_dim, call_center\n where cs_item_sk = i_item_sk and\n       cs_sold_date_sk = d_date_sk and\n       cc_call_center_sk= cs_call_center_sk and\n       (\n         d_year = 2000 or\n         ( d_year = 2000-1 and d_moy =12) or\n         ( d_year = 2000+1 and d_moy =1)\n       )\n group by i_category, i_brand,\n          cc_name , d_year, d_moy),\n v2 as(\n select v1.i_category, v1.i_brand\n        ,v1.d_year\n        ,v1.avg_monthly_sales\n        ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum\n from v1, v1 v1_lag, v1 v1_lead\n where v1.i_category = v1_lag.i_category and\n       v1.i_category = v1_lead.i_category and\n       v1.i_brand = v1_lag.i_brand and\n       v1.i_brand = v1_lead.i_brand and\n       v1. cc_name = v1_lag. cc_name and\n       v1. cc_name = v1_lead. cc_name and\n       v1.rn = v1_lag.rn + 1 and\n       v1.rn = v1_lead.rn - 1)\n  select  *\n from v2\n where  d_year = 2000 and\n        avg_monthly_sales > 0 and\n        case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\n order by sum_sales - avg_monthly_sales, sum_sales\n limit 100\"\"\",\n    \"q58\" ->\n      \"\"\"\nwith ss_items as\n (select i_item_id item_id\n        ,sum(ss_ext_sales_price) ss_item_rev\n from store_sales\n     ,item\n     ,date_dim\n where ss_item_sk = i_item_sk\n   and d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-05-24'))\n   and ss_sold_date_sk   = d_date_sk\n group by i_item_id),\n cs_items as\n (select i_item_id item_id\n        ,sum(cs_ext_sales_price) cs_item_rev\n  from catalog_sales\n      ,item\n      ,date_dim\n where cs_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq = (select d_week_seq\n                                      from date_dim\n                                      where d_date = '2000-05-24'))\n  and  cs_sold_date_sk = d_date_sk\n group by i_item_id),\n ws_items as\n (select i_item_id item_id\n        ,sum(ws_ext_sales_price) ws_item_rev\n  from web_sales\n      ,item\n      ,date_dim\n where ws_item_sk = i_item_sk\n  and  d_date in (select d_date\n                  from date_dim\n                  where d_week_seq =(select d_week_seq\n                                     from date_dim\n                                     where d_date = '2000-05-24'))\n  and ws_sold_date_sk   = d_date_sk\n group by i_item_id)\n  select  ss_items.item_id\n       ,ss_item_rev\n       ,ss_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ss_dev\n       ,cs_item_rev\n       ,cs_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 cs_dev\n       ,ws_item_rev\n       ,ws_item_rev/((ss_item_rev+cs_item_rev+ws_item_rev)/3) * 100 ws_dev\n       ,(ss_item_rev+cs_item_rev+ws_item_rev)/3 average\n from ss_items,cs_items,ws_items\n where ss_items.item_id=cs_items.item_id\n   and ss_items.item_id=ws_items.item_id\n   and ss_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n   and ss_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and cs_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and cs_item_rev between 0.9 * ws_item_rev and 1.1 * ws_item_rev\n   and ws_item_rev between 0.9 * ss_item_rev and 1.1 * ss_item_rev\n   and ws_item_rev between 0.9 * cs_item_rev and 1.1 * cs_item_rev\n order by item_id\n         ,ss_item_rev\n limit 100\"\"\",\n    \"q59\" ->\n      \"\"\"\nwith wss as\n (select d_week_seq,\n        ss_store_sk,\n        sum(case when (d_day_name='Sunday') then ss_sales_price else null end) sun_sales,\n        sum(case when (d_day_name='Monday') then ss_sales_price else null end) mon_sales,\n        sum(case when (d_day_name='Tuesday') then ss_sales_price else  null end) tue_sales,\n        sum(case when (d_day_name='Wednesday') then ss_sales_price else null end) wed_sales,\n        sum(case when (d_day_name='Thursday') then ss_sales_price else null end) thu_sales,\n        sum(case when (d_day_name='Friday') then ss_sales_price else null end) fri_sales,\n        sum(case when (d_day_name='Saturday') then ss_sales_price else null end) sat_sales\n from store_sales,date_dim\n where d_date_sk = ss_sold_date_sk\n group by d_week_seq,ss_store_sk\n )\n  select  s_store_name1,s_store_id1,d_week_seq1\n       ,sun_sales1/sun_sales2,mon_sales1/mon_sales2\n       ,tue_sales1/tue_sales2,wed_sales1/wed_sales2,thu_sales1/thu_sales2\n       ,fri_sales1/fri_sales2,sat_sales1/sat_sales2\n from\n (select s_store_name s_store_name1,wss.d_week_seq d_week_seq1\n        ,s_store_id s_store_id1,sun_sales sun_sales1\n        ,mon_sales mon_sales1,tue_sales tue_sales1\n        ,wed_sales wed_sales1,thu_sales thu_sales1\n        ,fri_sales fri_sales1,sat_sales sat_sales1\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1197 and 1197 + 11) y,\n (select s_store_name s_store_name2,wss.d_week_seq d_week_seq2\n        ,s_store_id s_store_id2,sun_sales sun_sales2\n        ,mon_sales mon_sales2,tue_sales tue_sales2\n        ,wed_sales wed_sales2,thu_sales thu_sales2\n        ,fri_sales fri_sales2,sat_sales sat_sales2\n  from wss,store,date_dim d\n  where d.d_week_seq = wss.d_week_seq and\n        ss_store_sk = s_store_sk and\n        d_month_seq between 1197+ 12 and 1197 + 23) x\n where s_store_id1=s_store_id2\n   and d_week_seq1=d_week_seq2-52\n order by s_store_name1,s_store_id1,d_week_seq1\nlimit 100\"\"\",\n    \"q60\" ->\n      \"\"\"\nwith ss as (\n select\n          i_item_id,sum(ss_ext_sales_price) total_sales\n from\n \tstore_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ss_item_sk              = i_item_sk\n and     ss_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ss_addr_sk              = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n cs as (\n select\n          i_item_id,sum(cs_ext_sales_price) total_sales\n from\n \tcatalog_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     cs_item_sk              = i_item_sk\n and     cs_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     cs_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id),\n ws as (\n select\n          i_item_id,sum(ws_ext_sales_price) total_sales\n from\n \tweb_sales,\n \tdate_dim,\n         customer_address,\n         item\n where\n         i_item_id               in (select\n  i_item_id\nfrom\n item\nwhere i_category in ('Shoes'))\n and     ws_item_sk              = i_item_sk\n and     ws_sold_date_sk         = d_date_sk\n and     d_year                  = 1998\n and     d_moy                   = 10\n and     ws_bill_addr_sk         = ca_address_sk\n and     ca_gmt_offset           = -5\n group by i_item_id)\n  select\n  i_item_id\n,sum(total_sales) total_sales\n from  (select * from ss\n        union all\n        select * from cs\n        union all\n        select * from ws) tmp1\n group by i_item_id\n order by i_item_id\n      ,total_sales\n limit 100\"\"\",\n    \"q61\" ->\n      \"\"\"\nselect  promotions,total,cast(promotions as decimal(15,4))/cast(total as decimal(15,4))*100\nfrom\n  (select sum(ss_ext_sales_price) promotions\n   from  store_sales\n        ,store\n        ,promotion\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_promo_sk = p_promo_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Jewelry'\n   and   (p_channel_dmail = 'Y' or p_channel_email = 'Y' or p_channel_tv = 'Y')\n   and   s_gmt_offset = -7\n   and   d_year = 2002\n   and   d_moy  = 11) promotional_sales,\n  (select sum(ss_ext_sales_price) total\n   from  store_sales\n        ,store\n        ,date_dim\n        ,customer\n        ,customer_address\n        ,item\n   where ss_sold_date_sk = d_date_sk\n   and   ss_store_sk = s_store_sk\n   and   ss_customer_sk= c_customer_sk\n   and   ca_address_sk = c_current_addr_sk\n   and   ss_item_sk = i_item_sk\n   and   ca_gmt_offset = -7\n   and   i_category = 'Jewelry'\n   and   s_gmt_offset = -7\n   and   d_year = 2002\n   and   d_moy  = 11) all_sales\norder by promotions, total\nlimit 100\"\"\",\n    \"q62\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 30) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 60) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk > 90) and\n                 (ws_ship_date_sk - ws_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (ws_ship_date_sk - ws_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   web_sales\n  ,warehouse\n  ,ship_mode\n  ,web_site\n  ,date_dim\nwhere\n    d_month_seq between 1194 and 1194 + 11\nand ws_ship_date_sk   = d_date_sk\nand ws_warehouse_sk   = w_warehouse_sk\nand ws_ship_mode_sk   = sm_ship_mode_sk\nand ws_web_site_sk    = web_site_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,web_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n       ,web_name\nlimit 100\"\"\",\n    \"q63\" ->\n      \"\"\"\nselect  *\nfrom (select i_manager_id\n             ,sum(ss_sales_price) sum_sales\n             ,avg(sum(ss_sales_price)) over (partition by i_manager_id) avg_monthly_sales\n      from item\n          ,store_sales\n          ,date_dim\n          ,store\n      where ss_item_sk = i_item_sk\n        and ss_sold_date_sk = d_date_sk\n        and ss_store_sk = s_store_sk\n        and d_month_seq in (1222,1222+1,1222+2,1222+3,1222+4,1222+5,1222+6,1222+7,1222+8,1222+9,1222+10,1222+11)\n        and ((    i_category in ('Books','Children','Electronics')\n              and i_class in ('personal','portable','reference','self-help')\n              and i_brand in ('scholaramalgamalg #14','scholaramalgamalg #7',\n\t\t                  'exportiunivamalg #9','scholaramalgamalg #9'))\n           or(    i_category in ('Women','Music','Men')\n              and i_class in ('accessories','classical','fragrances','pants')\n              and i_brand in ('amalgimporto #1','edu packscholar #1','exportiimporto #1',\n\t\t                 'importoamalg #1')))\ngroup by i_manager_id, d_moy) tmp1\nwhere case when avg_monthly_sales > 0 then abs (sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1\norder by i_manager_id\n        ,avg_monthly_sales\n        ,sum_sales\nlimit 100\"\"\",\n    \"q64\" ->\n      \"\"\"\nwith cs_ui as\n (select cs_item_sk\n        ,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund\n  from catalog_sales\n      ,catalog_returns\n  where cs_item_sk = cr_item_sk\n    and cs_order_number = cr_order_number\n  group by cs_item_sk\n  having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),\ncross_sales as\n (select i_product_name product_name\n     ,i_item_sk item_sk\n     ,s_store_name store_name\n     ,s_zip store_zip\n     ,ad1.ca_street_number b_street_number\n     ,ad1.ca_street_name b_street_name\n     ,ad1.ca_city b_city\n     ,ad1.ca_zip b_zip\n     ,ad2.ca_street_number c_street_number\n     ,ad2.ca_street_name c_street_name\n     ,ad2.ca_city c_city\n     ,ad2.ca_zip c_zip\n     ,d1.d_year as syear\n     ,d2.d_year as fsyear\n     ,d3.d_year s2year\n     ,count(*) cnt\n     ,sum(ss_wholesale_cost) s1\n     ,sum(ss_list_price) s2\n     ,sum(ss_coupon_amt) s3\n  FROM   store_sales\n        ,store_returns\n        ,cs_ui\n        ,date_dim d1\n        ,date_dim d2\n        ,date_dim d3\n        ,store\n        ,customer\n        ,customer_demographics cd1\n        ,customer_demographics cd2\n        ,promotion\n        ,household_demographics hd1\n        ,household_demographics hd2\n        ,customer_address ad1\n        ,customer_address ad2\n        ,income_band ib1\n        ,income_band ib2\n        ,item\n  WHERE  ss_store_sk = s_store_sk AND\n         ss_sold_date_sk = d1.d_date_sk AND\n         ss_customer_sk = c_customer_sk AND\n         ss_cdemo_sk= cd1.cd_demo_sk AND\n         ss_hdemo_sk = hd1.hd_demo_sk AND\n         ss_addr_sk = ad1.ca_address_sk and\n         ss_item_sk = i_item_sk and\n         ss_item_sk = sr_item_sk and\n         ss_ticket_number = sr_ticket_number and\n         ss_item_sk = cs_ui.cs_item_sk and\n         c_current_cdemo_sk = cd2.cd_demo_sk AND\n         c_current_hdemo_sk = hd2.hd_demo_sk AND\n         c_current_addr_sk = ad2.ca_address_sk and\n         c_first_sales_date_sk = d2.d_date_sk and\n         c_first_shipto_date_sk = d3.d_date_sk and\n         ss_promo_sk = p_promo_sk and\n         hd1.hd_income_band_sk = ib1.ib_income_band_sk and\n         hd2.hd_income_band_sk = ib2.ib_income_band_sk and\n         cd1.cd_marital_status <> cd2.cd_marital_status and\n         i_color in ('ivory','purple','almond','bisque','lawn','azure') and\n         i_current_price between 60 and 60 + 10 and\n         i_current_price between 60 + 1 and 60 + 15\ngroup by i_product_name\n       ,i_item_sk\n       ,s_store_name\n       ,s_zip\n       ,ad1.ca_street_number\n       ,ad1.ca_street_name\n       ,ad1.ca_city\n       ,ad1.ca_zip\n       ,ad2.ca_street_number\n       ,ad2.ca_street_name\n       ,ad2.ca_city\n       ,ad2.ca_zip\n       ,d1.d_year\n       ,d2.d_year\n       ,d3.d_year\n)\nselect cs1.product_name\n     ,cs1.store_name\n     ,cs1.store_zip\n     ,cs1.b_street_number\n     ,cs1.b_street_name\n     ,cs1.b_city\n     ,cs1.b_zip\n     ,cs1.c_street_number\n     ,cs1.c_street_name\n     ,cs1.c_city\n     ,cs1.c_zip\n     ,cs1.syear\n     ,cs1.cnt\n     ,cs1.s1 as s11\n     ,cs1.s2 as s21\n     ,cs1.s3 as s31\n     ,cs2.s1 as s12\n     ,cs2.s2 as s22\n     ,cs2.s3 as s32\n     ,cs2.syear\n     ,cs2.cnt\nfrom cross_sales cs1,cross_sales cs2\nwhere cs1.item_sk=cs2.item_sk and\n     cs1.syear = 2001 and\n     cs2.syear = 2001 + 1 and\n     cs2.cnt <= cs1.cnt and\n     cs1.store_name = cs2.store_name and\n     cs1.store_zip = cs2.store_zip\norder by cs1.product_name\n       ,cs1.store_name\n       ,cs2.cnt\n       ,cs1.s1\n       ,cs2.s1\"\"\",\n    \"q65\" ->\n      \"\"\"\nselect\n\ts_store_name,\n\ti_item_desc,\n\tsc.revenue,\n\ti_current_price,\n\ti_wholesale_cost,\n\ti_brand\n from store, item,\n     (select ss_store_sk, avg(revenue) as ave\n \tfrom\n \t    (select  ss_store_sk, ss_item_sk,\n \t\t     sum(ss_sales_price) as revenue\n \t\tfrom store_sales, date_dim\n \t\twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1185 and 1185+11\n \t\tgroup by ss_store_sk, ss_item_sk) sa\n \tgroup by ss_store_sk) sb,\n     (select  ss_store_sk, ss_item_sk, sum(ss_sales_price) as revenue\n \tfrom store_sales, date_dim\n \twhere ss_sold_date_sk = d_date_sk and d_month_seq between 1185 and 1185+11\n \tgroup by ss_store_sk, ss_item_sk) sc\n where sb.ss_store_sk = sc.ss_store_sk and\n       sc.revenue <= 0.1 * sb.ave and\n       s_store_sk = sc.ss_store_sk and\n       i_item_sk = sc.ss_item_sk\n order by s_store_name, i_item_desc\nlimit 100\"\"\",\n    \"q66\" ->\n      \"\"\"\nselect\n         w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n        ,ship_carriers\n        ,year\n \t,sum(jan_sales) as jan_sales\n \t,sum(feb_sales) as feb_sales\n \t,sum(mar_sales) as mar_sales\n \t,sum(apr_sales) as apr_sales\n \t,sum(may_sales) as may_sales\n \t,sum(jun_sales) as jun_sales\n \t,sum(jul_sales) as jul_sales\n \t,sum(aug_sales) as aug_sales\n \t,sum(sep_sales) as sep_sales\n \t,sum(oct_sales) as oct_sales\n \t,sum(nov_sales) as nov_sales\n \t,sum(dec_sales) as dec_sales\n \t,sum(jan_sales/w_warehouse_sq_ft) as jan_sales_per_sq_foot\n \t,sum(feb_sales/w_warehouse_sq_ft) as feb_sales_per_sq_foot\n \t,sum(mar_sales/w_warehouse_sq_ft) as mar_sales_per_sq_foot\n \t,sum(apr_sales/w_warehouse_sq_ft) as apr_sales_per_sq_foot\n \t,sum(may_sales/w_warehouse_sq_ft) as may_sales_per_sq_foot\n \t,sum(jun_sales/w_warehouse_sq_ft) as jun_sales_per_sq_foot\n \t,sum(jul_sales/w_warehouse_sq_ft) as jul_sales_per_sq_foot\n \t,sum(aug_sales/w_warehouse_sq_ft) as aug_sales_per_sq_foot\n \t,sum(sep_sales/w_warehouse_sq_ft) as sep_sales_per_sq_foot\n \t,sum(oct_sales/w_warehouse_sq_ft) as oct_sales_per_sq_foot\n \t,sum(nov_sales/w_warehouse_sq_ft) as nov_sales_per_sq_foot\n \t,sum(dec_sales/w_warehouse_sq_ft) as dec_sales_per_sq_foot\n \t,sum(jan_net) as jan_net\n \t,sum(feb_net) as feb_net\n \t,sum(mar_net) as mar_net\n \t,sum(apr_net) as apr_net\n \t,sum(may_net) as may_net\n \t,sum(jun_net) as jun_net\n \t,sum(jul_net) as jul_net\n \t,sum(aug_net) as aug_net\n \t,sum(sep_net) as sep_net\n \t,sum(oct_net) as oct_net\n \t,sum(nov_net) as nov_net\n \t,sum(dec_net) as dec_net\n from (\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'FEDEX' || ',' || 'MSC' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen ws_ext_list_price* ws_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen ws_net_profit * ws_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen ws_net_profit * ws_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen ws_net_profit * ws_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen ws_net_profit * ws_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen ws_net_profit * ws_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen ws_net_profit * ws_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen ws_net_profit * ws_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen ws_net_profit * ws_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen ws_net_profit * ws_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen ws_net_profit * ws_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen ws_net_profit * ws_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen ws_net_profit * ws_quantity else 0 end) as dec_net\n     from\n          web_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t  ,ship_mode\n     where\n            ws_warehouse_sk =  w_warehouse_sk\n        and ws_sold_date_sk = d_date_sk\n        and ws_sold_time_sk = t_time_sk\n \tand ws_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 2662 and 2662+28800\n \tand sm_carrier in ('FEDEX','MSC')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n union all\n     select\n \tw_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,'FEDEX' || ',' || 'MSC' as ship_carriers\n       ,d_year as year\n \t,sum(case when d_moy = 1\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jan_sales\n \t,sum(case when d_moy = 2\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as feb_sales\n \t,sum(case when d_moy = 3\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as mar_sales\n \t,sum(case when d_moy = 4\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as apr_sales\n \t,sum(case when d_moy = 5\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as may_sales\n \t,sum(case when d_moy = 6\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jun_sales\n \t,sum(case when d_moy = 7\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as jul_sales\n \t,sum(case when d_moy = 8\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as aug_sales\n \t,sum(case when d_moy = 9\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as sep_sales\n \t,sum(case when d_moy = 10\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as oct_sales\n \t,sum(case when d_moy = 11\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as nov_sales\n \t,sum(case when d_moy = 12\n \t\tthen cs_ext_list_price* cs_quantity else 0 end) as dec_sales\n \t,sum(case when d_moy = 1\n \t\tthen cs_net_profit * cs_quantity else 0 end) as jan_net\n \t,sum(case when d_moy = 2\n \t\tthen cs_net_profit * cs_quantity else 0 end) as feb_net\n \t,sum(case when d_moy = 3\n \t\tthen cs_net_profit * cs_quantity else 0 end) as mar_net\n \t,sum(case when d_moy = 4\n \t\tthen cs_net_profit * cs_quantity else 0 end) as apr_net\n \t,sum(case when d_moy = 5\n \t\tthen cs_net_profit * cs_quantity else 0 end) as may_net\n \t,sum(case when d_moy = 6\n \t\tthen cs_net_profit * cs_quantity else 0 end) as jun_net\n \t,sum(case when d_moy = 7\n \t\tthen cs_net_profit * cs_quantity else 0 end) as jul_net\n \t,sum(case when d_moy = 8\n \t\tthen cs_net_profit * cs_quantity else 0 end) as aug_net\n \t,sum(case when d_moy = 9\n \t\tthen cs_net_profit * cs_quantity else 0 end) as sep_net\n \t,sum(case when d_moy = 10\n \t\tthen cs_net_profit * cs_quantity else 0 end) as oct_net\n \t,sum(case when d_moy = 11\n \t\tthen cs_net_profit * cs_quantity else 0 end) as nov_net\n \t,sum(case when d_moy = 12\n \t\tthen cs_net_profit * cs_quantity else 0 end) as dec_net\n     from\n          catalog_sales\n         ,warehouse\n         ,date_dim\n         ,time_dim\n \t ,ship_mode\n     where\n            cs_warehouse_sk =  w_warehouse_sk\n        and cs_sold_date_sk = d_date_sk\n        and cs_sold_time_sk = t_time_sk\n \tand cs_ship_mode_sk = sm_ship_mode_sk\n        and d_year = 2002\n \tand t_time between 2662 AND 2662+28800\n \tand sm_carrier in ('FEDEX','MSC')\n     group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n       ,d_year\n ) x\n group by\n        w_warehouse_name\n \t,w_warehouse_sq_ft\n \t,w_city\n \t,w_county\n \t,w_state\n \t,w_country\n \t,ship_carriers\n       ,year\n order by w_warehouse_name\n limit 100\"\"\",\n    \"q67\" ->\n      \"\"\"\nselect  *\nfrom (select i_category\n            ,i_class\n            ,i_brand\n            ,i_product_name\n            ,d_year\n            ,d_qoy\n            ,d_moy\n            ,s_store_id\n            ,sumsales\n            ,rank() over (partition by i_category order by sumsales desc) rk\n      from (select i_category\n                  ,i_class\n                  ,i_brand\n                  ,i_product_name\n                  ,d_year\n                  ,d_qoy\n                  ,d_moy\n                  ,s_store_id\n                  ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales\n            from store_sales\n                ,date_dim\n                ,store\n                ,item\n       where  ss_sold_date_sk=d_date_sk\n          and ss_item_sk=i_item_sk\n          and ss_store_sk = s_store_sk\n          and d_month_seq between 1177 and 1177+11\n       group by  rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2\nwhere rk <= 100\norder by i_category\n        ,i_class\n        ,i_brand\n        ,i_product_name\n        ,d_year\n        ,d_qoy\n        ,d_moy\n        ,s_store_id\n        ,sumsales\n        ,rk\nlimit 100\"\"\",\n    \"q68\" ->\n      \"\"\"\nselect  c_last_name\n       ,c_first_name\n       ,ca_city\n       ,bought_city\n       ,ss_ticket_number\n       ,extended_price\n       ,extended_tax\n       ,list_price\n from (select ss_ticket_number\n             ,ss_customer_sk\n             ,ca_city bought_city\n             ,sum(ss_ext_sales_price) extended_price\n             ,sum(ss_ext_list_price) list_price\n             ,sum(ss_ext_tax) extended_tax\n       from store_sales\n           ,date_dim\n           ,store\n           ,household_demographics\n           ,customer_address\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_store_sk = store.s_store_sk\n        and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n        and store_sales.ss_addr_sk = customer_address.ca_address_sk\n        and date_dim.d_dom between 1 and 2\n        and (household_demographics.hd_dep_count = 5 or\n             household_demographics.hd_vehicle_count= 4)\n        and date_dim.d_year in (1999,1999+1,1999+2)\n        and store.s_city in ('Lodi','Richmond')\n       group by ss_ticket_number\n               ,ss_customer_sk\n               ,ss_addr_sk,ca_city) dn\n      ,customer\n      ,customer_address current_addr\n where ss_customer_sk = c_customer_sk\n   and customer.c_current_addr_sk = current_addr.ca_address_sk\n   and current_addr.ca_city <> bought_city\n order by c_last_name\n         ,ss_ticket_number\n limit 100\"\"\",\n    \"q69\" ->\n      \"\"\"\nselect\n  cd_gender,\n  cd_marital_status,\n  cd_education_status,\n  count(*) cnt1,\n  cd_purchase_estimate,\n  count(*) cnt2,\n  cd_credit_rating,\n  count(*) cnt3\n from\n  customer c,customer_address ca,customer_demographics\n where\n  c.c_current_addr_sk = ca.ca_address_sk and\n  ca_state in ('IL','FL','SD') and\n  cd_demo_sk = c.c_current_cdemo_sk and\n  exists (select *\n          from store_sales,date_dim\n          where c.c_customer_sk = ss_customer_sk and\n                ss_sold_date_sk = d_date_sk and\n                d_year = 1999 and\n                d_moy between 1 and 1+2) and\n   (not exists (select *\n            from web_sales,date_dim\n            where c.c_customer_sk = ws_bill_customer_sk and\n                  ws_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 1 and 1+2) and\n    not exists (select *\n            from catalog_sales,date_dim\n            where c.c_customer_sk = cs_ship_customer_sk and\n                  cs_sold_date_sk = d_date_sk and\n                  d_year = 1999 and\n                  d_moy between 1 and 1+2))\n group by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n order by cd_gender,\n          cd_marital_status,\n          cd_education_status,\n          cd_purchase_estimate,\n          cd_credit_rating\n limit 100\"\"\",\n    \"q70\" ->\n      \"\"\"\nselect\n    sum(ss_net_profit) as total_sum\n   ,s_state\n   ,s_county\n   ,grouping(s_state)+grouping(s_county) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(s_state)+grouping(s_county),\n \tcase when grouping(s_county) = 0 then s_state end\n \torder by sum(ss_net_profit) desc) as rank_within_parent\n from\n    store_sales\n   ,date_dim       d1\n   ,store\n where\n    d1.d_month_seq between 1206 and 1206+11\n and d1.d_date_sk = ss_sold_date_sk\n and s_store_sk  = ss_store_sk\n and s_state in\n             ( select s_state\n               from  (select s_state as s_state,\n \t\t\t    rank() over ( partition by s_state order by sum(ss_net_profit) desc) as ranking\n                      from   store_sales, store, date_dim\n                      where  d_month_seq between 1206 and 1206+11\n \t\t\t    and d_date_sk = ss_sold_date_sk\n \t\t\t    and s_store_sk  = ss_store_sk\n                      group by s_state\n                     ) tmp1\n               where ranking <= 5\n             )\n group by rollup(s_state,s_county)\n order by\n   lochierarchy desc\n  ,case when lochierarchy = 0 then s_state end\n  ,rank_within_parent\n limit 100\"\"\",\n    \"q71\" ->\n      \"\"\"\nselect i_brand_id brand_id, i_brand brand,t_hour,t_minute,\n \tsum(ext_price) ext_price\n from item, (select ws_ext_sales_price as ext_price,\n                        ws_sold_date_sk as sold_date_sk,\n                        ws_item_sk as sold_item_sk,\n                        ws_sold_time_sk as time_sk\n                 from web_sales,date_dim\n                 where d_date_sk = ws_sold_date_sk\n                   and d_moy=11\n                   and d_year=1999\n                 union all\n                 select cs_ext_sales_price as ext_price,\n                        cs_sold_date_sk as sold_date_sk,\n                        cs_item_sk as sold_item_sk,\n                        cs_sold_time_sk as time_sk\n                 from catalog_sales,date_dim\n                 where d_date_sk = cs_sold_date_sk\n                   and d_moy=11\n                   and d_year=1999\n                 union all\n                 select ss_ext_sales_price as ext_price,\n                        ss_sold_date_sk as sold_date_sk,\n                        ss_item_sk as sold_item_sk,\n                        ss_sold_time_sk as time_sk\n                 from store_sales,date_dim\n                 where d_date_sk = ss_sold_date_sk\n                   and d_moy=11\n                   and d_year=1999\n                 ) tmp,time_dim\n where\n   sold_item_sk = i_item_sk\n   and i_manager_id=1\n   and time_sk = t_time_sk\n   and (t_meal_time = 'breakfast' or t_meal_time = 'dinner')\n group by i_brand, i_brand_id,t_hour,t_minute\n order by ext_price desc, i_brand_id\n \"\"\",\n    \"q72\" ->\n      \"\"\"\nselect  i_item_desc\n      ,w_warehouse_name\n      ,d1.d_week_seq\n      ,sum(case when p_promo_sk is null then 1 else 0 end) no_promo\n      ,sum(case when p_promo_sk is not null then 1 else 0 end) promo\n      ,count(*) total_cnt\nfrom catalog_sales\njoin inventory on (cs_item_sk = inv_item_sk)\njoin warehouse on (w_warehouse_sk=inv_warehouse_sk)\njoin item on (i_item_sk = cs_item_sk)\njoin customer_demographics on (cs_bill_cdemo_sk = cd_demo_sk)\njoin household_demographics on (cs_bill_hdemo_sk = hd_demo_sk)\njoin date_dim d1 on (cs_sold_date_sk = d1.d_date_sk)\njoin date_dim d2 on (inv_date_sk = d2.d_date_sk)\njoin date_dim d3 on (cs_ship_date_sk = d3.d_date_sk)\nleft outer join promotion on (cs_promo_sk=p_promo_sk)\nleft outer join catalog_returns on (cr_item_sk = cs_item_sk and cr_order_number = cs_order_number)\nwhere d1.d_week_seq = d2.d_week_seq\n  and inv_quantity_on_hand < cs_quantity\n  and d3.d_date > d1.d_date + interval 5 days\n  and hd_buy_potential = '1001-5000'\n  and d1.d_year = 2000\n  and cd_marital_status = 'S'\ngroup by i_item_desc,w_warehouse_name,d1.d_week_seq\norder by total_cnt desc, i_item_desc, w_warehouse_name, d_week_seq\nlimit 100\"\"\",\n    \"q73\" ->\n      \"\"\"\nselect c_last_name\n       ,c_first_name\n       ,c_salutation\n       ,c_preferred_cust_flag\n       ,ss_ticket_number\n       ,cnt from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,count(*) cnt\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and date_dim.d_dom between 1 and 2\n    and (household_demographics.hd_buy_potential = '1001-5000' or\n         household_demographics.hd_buy_potential = 'Unknown')\n    and household_demographics.hd_vehicle_count > 0\n    and case when household_demographics.hd_vehicle_count > 0 then\n             household_demographics.hd_dep_count/ household_demographics.hd_vehicle_count else null end > 1\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_county in ('Humboldt County','Hickman County','Galax city','Abbeville County')\n    group by ss_ticket_number,ss_customer_sk) dj,customer\n    where ss_customer_sk = c_customer_sk\n      and cnt between 1 and 5\n    order by cnt desc, c_last_name asc\"\"\",\n    \"q74\" ->\n      \"\"\"\nwith year_total as (\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ss_net_paid) year_total\n       ,'s' sale_type\n from customer\n     ,store_sales\n     ,date_dim\n where c_customer_sk = ss_customer_sk\n   and ss_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n union all\n select c_customer_id customer_id\n       ,c_first_name customer_first_name\n       ,c_last_name customer_last_name\n       ,d_year as year\n       ,max(ws_net_paid) year_total\n       ,'w' sale_type\n from customer\n     ,web_sales\n     ,date_dim\n where c_customer_sk = ws_bill_customer_sk\n   and ws_sold_date_sk = d_date_sk\n   and d_year in (2001,2001+1)\n group by c_customer_id\n         ,c_first_name\n         ,c_last_name\n         ,d_year\n         )\n  select\n        t_s_secyear.customer_id, t_s_secyear.customer_first_name, t_s_secyear.customer_last_name\n from year_total t_s_firstyear\n     ,year_total t_s_secyear\n     ,year_total t_w_firstyear\n     ,year_total t_w_secyear\n where t_s_secyear.customer_id = t_s_firstyear.customer_id\n         and t_s_firstyear.customer_id = t_w_secyear.customer_id\n         and t_s_firstyear.customer_id = t_w_firstyear.customer_id\n         and t_s_firstyear.sale_type = 's'\n         and t_w_firstyear.sale_type = 'w'\n         and t_s_secyear.sale_type = 's'\n         and t_w_secyear.sale_type = 'w'\n         and t_s_firstyear.year = 2001\n         and t_s_secyear.year = 2001+1\n         and t_w_firstyear.year = 2001\n         and t_w_secyear.year = 2001+1\n         and t_s_firstyear.year_total > 0\n         and t_w_firstyear.year_total > 0\n         and case when t_w_firstyear.year_total > 0 then t_w_secyear.year_total / t_w_firstyear.year_total else null end\n           > case when t_s_firstyear.year_total > 0 then t_s_secyear.year_total / t_s_firstyear.year_total else null end\n order by 3,1,2\nlimit 100\"\"\",\n    \"q75\" ->\n      \"\"\"\nWITH all_sales AS (\n SELECT d_year\n       ,i_brand_id\n       ,i_class_id\n       ,i_category_id\n       ,i_manufact_id\n       ,SUM(sales_cnt) AS sales_cnt\n       ,SUM(sales_amt) AS sales_amt\n FROM (SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,cs_quantity - COALESCE(cr_return_quantity,0) AS sales_cnt\n             ,cs_ext_sales_price - COALESCE(cr_return_amount,0.0) AS sales_amt\n       FROM catalog_sales JOIN item ON i_item_sk=cs_item_sk\n                          JOIN date_dim ON d_date_sk=cs_sold_date_sk\n                          LEFT JOIN catalog_returns ON (cs_order_number=cr_order_number\n                                                    AND cs_item_sk=cr_item_sk)\n       WHERE i_category='Books'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ss_quantity - COALESCE(sr_return_quantity,0) AS sales_cnt\n             ,ss_ext_sales_price - COALESCE(sr_return_amt,0.0) AS sales_amt\n       FROM store_sales JOIN item ON i_item_sk=ss_item_sk\n                        JOIN date_dim ON d_date_sk=ss_sold_date_sk\n                        LEFT JOIN store_returns ON (ss_ticket_number=sr_ticket_number\n                                                AND ss_item_sk=sr_item_sk)\n       WHERE i_category='Books'\n       UNION\n       SELECT d_year\n             ,i_brand_id\n             ,i_class_id\n             ,i_category_id\n             ,i_manufact_id\n             ,ws_quantity - COALESCE(wr_return_quantity,0) AS sales_cnt\n             ,ws_ext_sales_price - COALESCE(wr_return_amt,0.0) AS sales_amt\n       FROM web_sales JOIN item ON i_item_sk=ws_item_sk\n                      JOIN date_dim ON d_date_sk=ws_sold_date_sk\n                      LEFT JOIN web_returns ON (ws_order_number=wr_order_number\n                                            AND ws_item_sk=wr_item_sk)\n       WHERE i_category='Books') sales_detail\n GROUP BY d_year, i_brand_id, i_class_id, i_category_id, i_manufact_id)\n SELECT  prev_yr.d_year AS prev_year\n                          ,curr_yr.d_year AS year\n                          ,curr_yr.i_brand_id\n                          ,curr_yr.i_class_id\n                          ,curr_yr.i_category_id\n                          ,curr_yr.i_manufact_id\n                          ,prev_yr.sales_cnt AS prev_yr_cnt\n                          ,curr_yr.sales_cnt AS curr_yr_cnt\n                          ,curr_yr.sales_cnt-prev_yr.sales_cnt AS sales_cnt_diff\n                          ,curr_yr.sales_amt-prev_yr.sales_amt AS sales_amt_diff\n FROM all_sales curr_yr, all_sales prev_yr\n WHERE curr_yr.i_brand_id=prev_yr.i_brand_id\n   AND curr_yr.i_class_id=prev_yr.i_class_id\n   AND curr_yr.i_category_id=prev_yr.i_category_id\n   AND curr_yr.i_manufact_id=prev_yr.i_manufact_id\n   AND curr_yr.d_year=2001\n   AND prev_yr.d_year=2001-1\n   AND CAST(curr_yr.sales_cnt AS DECIMAL(17,2))/CAST(prev_yr.sales_cnt AS DECIMAL(17,2))<0.9\n ORDER BY sales_cnt_diff,sales_amt_diff\n limit 100\"\"\",\n    \"q76\" ->\n      \"\"\"\nselect  channel, col_name, d_year, d_qoy, i_category, COUNT(*) sales_cnt, SUM(ext_sales_price) sales_amt FROM (\n        SELECT 'store' as channel, 'ss_promo_sk' col_name, d_year, d_qoy, i_category, ss_ext_sales_price ext_sales_price\n         FROM store_sales, item, date_dim\n         WHERE ss_promo_sk IS NULL\n           AND ss_sold_date_sk=d_date_sk\n           AND ss_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'web' as channel, 'ws_ship_addr_sk' col_name, d_year, d_qoy, i_category, ws_ext_sales_price ext_sales_price\n         FROM web_sales, item, date_dim\n         WHERE ws_ship_addr_sk IS NULL\n           AND ws_sold_date_sk=d_date_sk\n           AND ws_item_sk=i_item_sk\n        UNION ALL\n        SELECT 'catalog' as channel, 'cs_ship_customer_sk' col_name, d_year, d_qoy, i_category, cs_ext_sales_price ext_sales_price\n         FROM catalog_sales, item, date_dim\n         WHERE cs_ship_customer_sk IS NULL\n           AND cs_sold_date_sk=d_date_sk\n           AND cs_item_sk=i_item_sk) foo\nGROUP BY channel, col_name, d_year, d_qoy, i_category\nORDER BY channel, col_name, d_year, d_qoy, i_category\nlimit 100\"\"\",\n    \"q77\" ->\n      \"\"\"\nwith ss as\n (select s_store_sk,\n         sum(ss_ext_sales_price) as sales,\n         sum(ss_net_profit) as profit\n from store_sales,\n      date_dim,\n      store\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and ss_store_sk = s_store_sk\n group by s_store_sk)\n ,\n sr as\n (select s_store_sk,\n         sum(sr_return_amt) as returns,\n         sum(sr_net_loss) as profit_loss\n from store_returns,\n      date_dim,\n      store\n where sr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and sr_store_sk = s_store_sk\n group by s_store_sk),\n cs as\n (select cs_call_center_sk,\n        sum(cs_ext_sales_price) as sales,\n        sum(cs_net_profit) as profit\n from catalog_sales,\n      date_dim\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n group by cs_call_center_sk\n ),\n cr as\n (select cr_call_center_sk,\n         sum(cr_return_amount) as returns,\n         sum(cr_net_loss) as profit_loss\n from catalog_returns,\n      date_dim\n where cr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n group by cr_call_center_sk\n ),\n ws as\n ( select wp_web_page_sk,\n        sum(ws_ext_sales_price) as sales,\n        sum(ws_net_profit) as profit\n from web_sales,\n      date_dim,\n      web_page\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and ws_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk),\n wr as\n (select wp_web_page_sk,\n        sum(wr_return_amt) as returns,\n        sum(wr_net_loss) as profit_loss\n from web_returns,\n      date_dim,\n      web_page\n where wr_returned_date_sk = d_date_sk\n       and d_date between cast('2001-08-16' as date)\n                  and (cast('2001-08-16' as date) +  INTERVAL 30 days)\n       and wr_web_page_sk = wp_web_page_sk\n group by wp_web_page_sk)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , ss.s_store_sk as id\n        , sales\n        , coalesce(returns, 0) as returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ss left join sr\n        on  ss.s_store_sk = sr.s_store_sk\n union all\n select 'catalog channel' as channel\n        , cs_call_center_sk as id\n        , sales\n        , returns\n        , (profit - profit_loss) as profit\n from  cs\n       , cr\n union all\n select 'web channel' as channel\n        , ws.wp_web_page_sk as id\n        , sales\n        , coalesce(returns, 0) returns\n        , (profit - coalesce(profit_loss,0)) as profit\n from   ws left join wr\n        on  ws.wp_web_page_sk = wr.wp_web_page_sk\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q78\" ->\n      \"\"\"\nwith ws as\n  (select d_year AS ws_sold_year, ws_item_sk,\n    ws_bill_customer_sk ws_customer_sk,\n    sum(ws_quantity) ws_qty,\n    sum(ws_wholesale_cost) ws_wc,\n    sum(ws_sales_price) ws_sp\n   from web_sales\n   left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk\n   join date_dim on ws_sold_date_sk = d_date_sk\n   where wr_order_number is null\n   group by d_year, ws_item_sk, ws_bill_customer_sk\n   ),\ncs as\n  (select d_year AS cs_sold_year, cs_item_sk,\n    cs_bill_customer_sk cs_customer_sk,\n    sum(cs_quantity) cs_qty,\n    sum(cs_wholesale_cost) cs_wc,\n    sum(cs_sales_price) cs_sp\n   from catalog_sales\n   left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk\n   join date_dim on cs_sold_date_sk = d_date_sk\n   where cr_order_number is null\n   group by d_year, cs_item_sk, cs_bill_customer_sk\n   ),\nss as\n  (select d_year AS ss_sold_year, ss_item_sk,\n    ss_customer_sk,\n    sum(ss_quantity) ss_qty,\n    sum(ss_wholesale_cost) ss_wc,\n    sum(ss_sales_price) ss_sp\n   from store_sales\n   left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk\n   join date_dim on ss_sold_date_sk = d_date_sk\n   where sr_ticket_number is null\n   group by d_year, ss_item_sk, ss_customer_sk\n   )\n select\nss_item_sk,\nround(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,\nss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,\ncoalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,\ncoalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,\ncoalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price\nfrom ss\nleft join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)\nleft join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)\nwhere (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2000\norder by\n  ss_item_sk,\n  ss_qty desc, ss_wc desc, ss_sp desc,\n  other_chan_qty,\n  other_chan_wholesale_cost,\n  other_chan_sales_price,\n  ratio\nlimit 100\"\"\",\n    \"q79\" ->\n      \"\"\"\nselect\n  c_last_name,c_first_name,substr(s_city,1,30),ss_ticket_number,amt,profit\n  from\n   (select ss_ticket_number\n          ,ss_customer_sk\n          ,store.s_city\n          ,sum(ss_coupon_amt) amt\n          ,sum(ss_net_profit) profit\n    from store_sales,date_dim,store,household_demographics\n    where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n    and store_sales.ss_store_sk = store.s_store_sk\n    and store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk\n    and (household_demographics.hd_dep_count = 5 or household_demographics.hd_vehicle_count > -1)\n    and date_dim.d_dow = 1\n    and date_dim.d_year in (1999,1999+1,1999+2)\n    and store.s_number_employees between 200 and 295\n    group by ss_ticket_number,ss_customer_sk,ss_addr_sk,store.s_city) ms,customer\n    where ss_customer_sk = c_customer_sk\n order by c_last_name,c_first_name,substr(s_city,1,30), profit\nlimit 100\"\"\",\n    \"q80\" ->\n      \"\"\"\nwith ssr as\n (select  s_store_id as store_id,\n          sum(ss_ext_sales_price) as sales,\n          sum(coalesce(sr_return_amt, 0)) as returns,\n          sum(ss_net_profit - coalesce(sr_net_loss, 0)) as profit\n  from store_sales left outer join store_returns on\n         (ss_item_sk = sr_item_sk and ss_ticket_number = sr_ticket_number),\n     date_dim,\n     store,\n     item,\n     promotion\n where ss_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-19' as date)\n                  and (cast('2001-08-19' as date) +  INTERVAL 60 days)\n       and ss_store_sk = s_store_sk\n       and ss_item_sk = i_item_sk\n       and i_current_price > 50\n       and ss_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\n group by s_store_id)\n ,\n csr as\n (select  cp_catalog_page_id as catalog_page_id,\n          sum(cs_ext_sales_price) as sales,\n          sum(coalesce(cr_return_amount, 0)) as returns,\n          sum(cs_net_profit - coalesce(cr_net_loss, 0)) as profit\n  from catalog_sales left outer join catalog_returns on\n         (cs_item_sk = cr_item_sk and cs_order_number = cr_order_number),\n     date_dim,\n     catalog_page,\n     item,\n     promotion\n where cs_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-19' as date)\n                  and (cast('2001-08-19' as date) +  INTERVAL 60 days)\n        and cs_catalog_page_sk = cp_catalog_page_sk\n       and cs_item_sk = i_item_sk\n       and i_current_price > 50\n       and cs_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by cp_catalog_page_id)\n ,\n wsr as\n (select  web_site_id,\n          sum(ws_ext_sales_price) as sales,\n          sum(coalesce(wr_return_amt, 0)) as returns,\n          sum(ws_net_profit - coalesce(wr_net_loss, 0)) as profit\n  from web_sales left outer join web_returns on\n         (ws_item_sk = wr_item_sk and ws_order_number = wr_order_number),\n     date_dim,\n     web_site,\n     item,\n     promotion\n where ws_sold_date_sk = d_date_sk\n       and d_date between cast('2001-08-19' as date)\n                  and (cast('2001-08-19' as date) +  INTERVAL 60 days)\n        and ws_web_site_sk = web_site_sk\n       and ws_item_sk = i_item_sk\n       and i_current_price > 50\n       and ws_promo_sk = p_promo_sk\n       and p_channel_tv = 'N'\ngroup by web_site_id)\n  select  channel\n        , id\n        , sum(sales) as sales\n        , sum(returns) as returns\n        , sum(profit) as profit\n from\n (select 'store channel' as channel\n        , 'store' || store_id as id\n        , sales\n        , returns\n        , profit\n from   ssr\n union all\n select 'catalog channel' as channel\n        , 'catalog_page' || catalog_page_id as id\n        , sales\n        , returns\n        , profit\n from  csr\n union all\n select 'web channel' as channel\n        , 'web_site' || web_site_id as id\n        , sales\n        , returns\n        , profit\n from   wsr\n ) x\n group by rollup (channel, id)\n order by channel\n         ,id\n limit 100\"\"\",\n    \"q81\" ->\n      \"\"\"\nwith customer_total_return as\n (select cr_returning_customer_sk as ctr_customer_sk\n        ,ca_state as ctr_state,\n \tsum(cr_return_amt_inc_tax) as ctr_total_return\n from catalog_returns\n     ,date_dim\n     ,customer_address\n where cr_returned_date_sk = d_date_sk\n   and d_year =1999\n   and cr_returning_addr_sk = ca_address_sk\n group by cr_returning_customer_sk\n         ,ca_state )\n  select  c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n from customer_total_return ctr1\n     ,customer_address\n     ,customer\n where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2\n \t\t\t  from customer_total_return ctr2\n                  \t  where ctr1.ctr_state = ctr2.ctr_state)\n       and ca_address_sk = c_current_addr_sk\n       and ca_state = 'MO'\n       and ctr1.ctr_customer_sk = c_customer_sk\n order by c_customer_id,c_salutation,c_first_name,c_last_name,ca_street_number,ca_street_name\n                   ,ca_street_type,ca_suite_number,ca_city,ca_county,ca_state,ca_zip,ca_country,ca_gmt_offset\n                  ,ca_location_type,ctr_total_return\n limit 100\"\"\",\n    \"q82\" ->\n      \"\"\"\nselect  i_item_id\n       ,i_item_desc\n       ,i_current_price\n from item, inventory, date_dim, store_sales\n where i_current_price between 68 and 68+30\n and inv_item_sk = i_item_sk\n and d_date_sk=inv_date_sk\n and d_date between cast('2002-05-08' as date) and (cast('2002-05-08' as date) +  INTERVAL 60 days)\n and i_manufact_id in (562,370,230,182)\n and inv_quantity_on_hand between 100 and 500\n and ss_item_sk = i_item_sk\n group by i_item_id,i_item_desc,i_current_price\n order by i_item_id\n limit 100\"\"\",\n    \"q83\" ->\n      \"\"\"\nwith sr_items as\n (select i_item_id item_id,\n        sum(sr_return_quantity) sr_item_qty\n from store_returns,\n      item,\n      date_dim\n where sr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-02-20','2000-10-08','2000-11-04')))\n and   sr_returned_date_sk   = d_date_sk\n group by i_item_id),\n cr_items as\n (select i_item_id item_id,\n        sum(cr_return_quantity) cr_item_qty\n from catalog_returns,\n      item,\n      date_dim\n where cr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t  where d_date in ('2000-02-20','2000-10-08','2000-11-04')))\n and   cr_returned_date_sk   = d_date_sk\n group by i_item_id),\n wr_items as\n (select i_item_id item_id,\n        sum(wr_return_quantity) wr_item_qty\n from web_returns,\n      item,\n      date_dim\n where wr_item_sk = i_item_sk\n and   d_date    in\n\t(select d_date\n\tfrom date_dim\n\twhere d_week_seq in\n\t\t(select d_week_seq\n\t\tfrom date_dim\n\t\twhere d_date in ('2000-02-20','2000-10-08','2000-11-04')))\n and   wr_returned_date_sk   = d_date_sk\n group by i_item_id)\n  select  sr_items.item_id\n       ,sr_item_qty\n       ,sr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 sr_dev\n       ,cr_item_qty\n       ,cr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 cr_dev\n       ,wr_item_qty\n       ,wr_item_qty/(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 * 100 wr_dev\n       ,(sr_item_qty+cr_item_qty+wr_item_qty)/3.0 average\n from sr_items\n     ,cr_items\n     ,wr_items\n where sr_items.item_id=cr_items.item_id\n   and sr_items.item_id=wr_items.item_id\n order by sr_items.item_id\n         ,sr_item_qty\n limit 100\"\"\",\n    \"q84\" ->\n      \"\"\"\nselect  c_customer_id as customer_id\n       , coalesce(c_last_name,'') || ', ' || coalesce(c_first_name,'') as customername\n from customer\n     ,customer_address\n     ,customer_demographics\n     ,household_demographics\n     ,income_band\n     ,store_returns\n where ca_city\t        =  'Buena Vista'\n   and c_current_addr_sk = ca_address_sk\n   and ib_lower_bound   >=  49786\n   and ib_upper_bound   <=  49786 + 50000\n   and ib_income_band_sk = hd_income_band_sk\n   and cd_demo_sk = c_current_cdemo_sk\n   and hd_demo_sk = c_current_hdemo_sk\n   and sr_cdemo_sk = cd_demo_sk\n order by c_customer_id\n limit 100\"\"\",\n    \"q85\" ->\n      \"\"\"\nselect  substr(r_reason_desc,1,20)\n       ,avg(ws_quantity)\n       ,avg(wr_refunded_cash)\n       ,avg(wr_fee)\n from web_sales, web_returns, web_page, customer_demographics cd1,\n      customer_demographics cd2, customer_address, date_dim, reason\n where ws_web_page_sk = wp_web_page_sk\n   and ws_item_sk = wr_item_sk\n   and ws_order_number = wr_order_number\n   and ws_sold_date_sk = d_date_sk and d_year = 2001\n   and cd1.cd_demo_sk = wr_refunded_cdemo_sk\n   and cd2.cd_demo_sk = wr_returning_cdemo_sk\n   and ca_address_sk = wr_refunded_addr_sk\n   and r_reason_sk = wr_reason_sk\n   and\n   (\n    (\n     cd1.cd_marital_status = 'D'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '4 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 100.00 and 150.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'M'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = 'Primary'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 50.00 and 100.00\n    )\n   or\n    (\n     cd1.cd_marital_status = 'U'\n     and\n     cd1.cd_marital_status = cd2.cd_marital_status\n     and\n     cd1.cd_education_status = '2 yr Degree'\n     and\n     cd1.cd_education_status = cd2.cd_education_status\n     and\n     ws_sales_price between 150.00 and 200.00\n    )\n   )\n   and\n   (\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('IA', 'ND', 'FL')\n     and ws_net_profit between 100 and 200\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('OH', 'MS', 'VA')\n     and ws_net_profit between 150 and 300\n    )\n    or\n    (\n     ca_country = 'United States'\n     and\n     ca_state in ('MN', 'LA', 'TX')\n     and ws_net_profit between 50 and 250\n    )\n   )\ngroup by r_reason_desc\norder by substr(r_reason_desc,1,20)\n        ,avg(ws_quantity)\n        ,avg(wr_refunded_cash)\n        ,avg(wr_fee)\nlimit 100\"\"\",\n    \"q86\" ->\n      \"\"\"\nselect\n    sum(ws_net_paid) as total_sum\n   ,i_category\n   ,i_class\n   ,grouping(i_category)+grouping(i_class) as lochierarchy\n   ,rank() over (\n \tpartition by grouping(i_category)+grouping(i_class),\n \tcase when grouping(i_class) = 0 then i_category end\n \torder by sum(ws_net_paid) desc) as rank_within_parent\n from\n    web_sales\n   ,date_dim       d1\n   ,item\n where\n    d1.d_month_seq between 1217 and 1217+11\n and d1.d_date_sk = ws_sold_date_sk\n and i_item_sk  = ws_item_sk\n group by rollup(i_category,i_class)\n order by\n   lochierarchy desc,\n   case when lochierarchy = 0 then i_category end,\n   rank_within_parent\n limit 100\"\"\",\n    \"q87\" ->\n      \"\"\"\nselect count(*)\nfrom ((select distinct c_last_name, c_first_name, d_date\n       from store_sales, date_dim, customer\n       where store_sales.ss_sold_date_sk = date_dim.d_date_sk\n         and store_sales.ss_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1224 and 1224+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from catalog_sales, date_dim, customer\n       where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk\n         and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1224 and 1224+11)\n       except\n      (select distinct c_last_name, c_first_name, d_date\n       from web_sales, date_dim, customer\n       where web_sales.ws_sold_date_sk = date_dim.d_date_sk\n         and web_sales.ws_bill_customer_sk = customer.c_customer_sk\n         and d_month_seq between 1224 and 1224+11)\n) cool_cust\"\"\",\n    \"q88\" ->\n      \"\"\"\nselect  *\nfrom\n (select count(*) h8_30_to_9\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 8\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s1,\n (select count(*) h9_to_9_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s2,\n (select count(*) h9_30_to_10\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 9\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s3,\n (select count(*) h10_to_10_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s4,\n (select count(*) h10_30_to_11\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 10\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s5,\n (select count(*) h11_to_11_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s6,\n (select count(*) h11_30_to_12\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 11\n     and time_dim.t_minute >= 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s7,\n (select count(*) h12_to_12_30\n from store_sales, household_demographics , time_dim, store\n where ss_sold_time_sk = time_dim.t_time_sk\n     and ss_hdemo_sk = household_demographics.hd_demo_sk\n     and ss_store_sk = s_store_sk\n     and time_dim.t_hour = 12\n     and time_dim.t_minute < 30\n     and ((household_demographics.hd_dep_count = 0 and household_demographics.hd_vehicle_count<=0+2) or\n          (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or\n          (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2))\n     and store.s_store_name = 'ese') s8\"\"\",\n    \"q89\" ->\n      \"\"\"\nselect  *\nfrom(\nselect i_category, i_class, i_brand,\n       s_store_name, s_company_name,\n       d_moy,\n       sum(ss_sales_price) sum_sales,\n       avg(sum(ss_sales_price)) over\n         (partition by i_category, i_brand, s_store_name, s_company_name)\n         avg_monthly_sales\nfrom item, store_sales, date_dim, store\nwhere ss_item_sk = i_item_sk and\n      ss_sold_date_sk = d_date_sk and\n      ss_store_sk = s_store_sk and\n      d_year in (2001) and\n        ((i_category in ('Children','Home','Women') and\n          i_class in ('toddlers','flatware','fragrances')\n         )\n      or (i_category in ('Music','Electronics','Shoes') and\n          i_class in ('country','dvd/vcr players','mens')\n        ))\ngroup by i_category, i_class, i_brand,\n         s_store_name, s_company_name, d_moy) tmp1\nwhere case when (avg_monthly_sales <> 0) then (abs(sum_sales - avg_monthly_sales) / avg_monthly_sales) else null end > 0.1\norder by sum_sales - avg_monthly_sales, s_store_name\nlimit 100\"\"\",\n    \"q90\" ->\n      \"\"\"\nselect  cast(amc as decimal(15,4))/cast(pmc as decimal(15,4)) am_pm_ratio\n from ( select count(*) amc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 7 and 7+1\n         and household_demographics.hd_dep_count = 1\n         and web_page.wp_char_count between 5000 and 5200) at,\n      ( select count(*) pmc\n       from web_sales, household_demographics , time_dim, web_page\n       where ws_sold_time_sk = time_dim.t_time_sk\n         and ws_ship_hdemo_sk = household_demographics.hd_demo_sk\n         and ws_web_page_sk = web_page.wp_web_page_sk\n         and time_dim.t_hour between 20 and 20+1\n         and household_demographics.hd_dep_count = 1\n         and web_page.wp_char_count between 5000 and 5200) pt\n order by am_pm_ratio\n limit 100\"\"\",\n    \"q91\" ->\n      \"\"\"\nselect\n        cc_call_center_id Call_Center,\n        cc_name Call_Center_Name,\n        cc_manager Manager,\n        sum(cr_net_loss) Returns_Loss\nfrom\n        call_center,\n        catalog_returns,\n        date_dim,\n        customer,\n        customer_address,\n        customer_demographics,\n        household_demographics\nwhere\n        cr_call_center_sk       = cc_call_center_sk\nand     cr_returned_date_sk     = d_date_sk\nand     cr_returning_customer_sk= c_customer_sk\nand     cd_demo_sk              = c_current_cdemo_sk\nand     hd_demo_sk              = c_current_hdemo_sk\nand     ca_address_sk           = c_current_addr_sk\nand     d_year                  = 1998\nand     d_moy                   = 12\nand     ( (cd_marital_status       = 'M' and cd_education_status     = 'Unknown')\n        or(cd_marital_status       = 'W' and cd_education_status     = 'Advanced Degree'))\nand     hd_buy_potential like 'Unknown%'\nand     ca_gmt_offset           = -6\ngroup by cc_call_center_id,cc_name,cc_manager,cd_marital_status,cd_education_status\norder by sum(cr_net_loss) desc\"\"\",\n    \"q92\" ->\n      \"\"\"\nselect\n   sum(ws_ext_discount_amt)  as `Excess Discount Amount`\nfrom\n    web_sales\n   ,item\n   ,date_dim\nwhere\ni_manufact_id = 172\nand i_item_sk = ws_item_sk\nand d_date between '1999-01-12' and\n        (cast('1999-01-12' as date) + INTERVAL 90 days)\nand d_date_sk = ws_sold_date_sk\nand ws_ext_discount_amt\n     > (\n         SELECT\n            1.3 * avg(ws_ext_discount_amt)\n         FROM\n            web_sales\n           ,date_dim\n         WHERE\n              ws_item_sk = i_item_sk\n          and d_date between '1999-01-12' and\n                             (cast('1999-01-12' as date) + INTERVAL 90 days)\n          and d_date_sk = ws_sold_date_sk\n      )\norder by sum(ws_ext_discount_amt)\nlimit 100\"\"\",\n    \"q93\" ->\n      \"\"\"\nselect  ss_customer_sk\n            ,sum(act_sales) sumsales\n      from (select ss_item_sk\n                  ,ss_ticket_number\n                  ,ss_customer_sk\n                  ,case when sr_return_quantity is not null then (ss_quantity-sr_return_quantity)*ss_sales_price\n                                                            else (ss_quantity*ss_sales_price) end act_sales\n            from store_sales left outer join store_returns on (sr_item_sk = ss_item_sk\n                                                               and sr_ticket_number = ss_ticket_number)\n                ,reason\n            where sr_reason_sk = r_reason_sk\n              and r_reason_desc = 'reason 58') t\n      group by ss_customer_sk\n      order by sumsales, ss_customer_sk\nlimit 100\"\"\",\n    \"q94\" ->\n      \"\"\"\nselect\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2002-3-01' and\n           (cast('2002-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'GA'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand exists (select *\n            from web_sales ws2\n            where ws1.ws_order_number = ws2.ws_order_number\n              and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\nand not exists(select *\n               from web_returns wr1\n               where ws1.ws_order_number = wr1.wr_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q95\" ->\n      \"\"\"\nwith ws_wh as\n(select ws1.ws_order_number,ws1.ws_warehouse_sk wh1,ws2.ws_warehouse_sk wh2\n from web_sales ws1,web_sales ws2\n where ws1.ws_order_number = ws2.ws_order_number\n   and ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk)\n select\n   count(distinct ws_order_number) as `order count`\n  ,sum(ws_ext_ship_cost) as `total shipping cost`\n  ,sum(ws_net_profit) as `total net profit`\nfrom\n   web_sales ws1\n  ,date_dim\n  ,customer_address\n  ,web_site\nwhere\n    d_date between '2001-3-01' and\n           (cast('2001-3-01' as date) + INTERVAL 60 days)\nand ws1.ws_ship_date_sk = d_date_sk\nand ws1.ws_ship_addr_sk = ca_address_sk\nand ca_state = 'NE'\nand ws1.ws_web_site_sk = web_site_sk\nand web_company_name = 'pri'\nand ws1.ws_order_number in (select ws_order_number\n                            from ws_wh)\nand ws1.ws_order_number in (select wr_order_number\n                            from web_returns,ws_wh\n                            where wr_order_number = ws_wh.ws_order_number)\norder by count(distinct ws_order_number)\nlimit 100\"\"\",\n    \"q96\" ->\n      \"\"\"\nselect  count(*)\nfrom store_sales\n    ,household_demographics\n    ,time_dim, store\nwhere ss_sold_time_sk = time_dim.t_time_sk\n    and ss_hdemo_sk = household_demographics.hd_demo_sk\n    and ss_store_sk = s_store_sk\n    and time_dim.t_hour = 16\n    and time_dim.t_minute >= 30\n    and household_demographics.hd_dep_count = 0\n    and store.s_store_name = 'ese'\norder by count(*)\nlimit 100\"\"\",\n    \"q97\" ->\n      \"\"\"\nwith ssci as (\nselect ss_customer_sk customer_sk\n      ,ss_item_sk item_sk\nfrom store_sales,date_dim\nwhere ss_sold_date_sk = d_date_sk\n  and d_month_seq between 1219 and 1219 + 11\ngroup by ss_customer_sk\n        ,ss_item_sk),\ncsci as(\n select cs_bill_customer_sk customer_sk\n      ,cs_item_sk item_sk\nfrom catalog_sales,date_dim\nwhere cs_sold_date_sk = d_date_sk\n  and d_month_seq between 1219 and 1219 + 11\ngroup by cs_bill_customer_sk\n        ,cs_item_sk)\n select  sum(case when ssci.customer_sk is not null and csci.customer_sk is null then 1 else 0 end) store_only\n      ,sum(case when ssci.customer_sk is null and csci.customer_sk is not null then 1 else 0 end) catalog_only\n      ,sum(case when ssci.customer_sk is not null and csci.customer_sk is not null then 1 else 0 end) store_and_catalog\nfrom ssci full outer join csci on (ssci.customer_sk=csci.customer_sk\n                               and ssci.item_sk = csci.item_sk)\nlimit 100\"\"\",\n    \"q98\" ->\n      \"\"\"\nselect i_item_id\n      ,i_item_desc\n      ,i_category\n      ,i_class\n      ,i_current_price\n      ,sum(ss_ext_sales_price) as itemrevenue\n      ,sum(ss_ext_sales_price)*100/sum(sum(ss_ext_sales_price)) over\n          (partition by i_class) as revenueratio\nfrom\n\tstore_sales\n    \t,item\n    \t,date_dim\nwhere\n\tss_item_sk = i_item_sk\n  \tand i_category in ('Books', 'Children', 'Sports')\n  \tand ss_sold_date_sk = d_date_sk\n\tand d_date between cast('2001-03-10' as date)\n\t\t\t\tand (cast('2001-03-10' as date) + interval 30 days)\ngroup by\n\ti_item_id\n        ,i_item_desc\n        ,i_category\n        ,i_class\n        ,i_current_price\norder by\n\ti_category\n        ,i_class\n        ,i_item_id\n        ,i_item_desc\n        ,revenueratio\"\"\",\n    \"q99\" ->\n      \"\"\"\nselect\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk <= 30 ) then 1 else 0 end)  as `30 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 30) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 60) then 1 else 0 end )  as `31-60 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 60) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 90) then 1 else 0 end)  as `61-90 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk > 90) and\n                 (cs_ship_date_sk - cs_sold_date_sk <= 120) then 1 else 0 end)  as `91-120 days`\n  ,sum(case when (cs_ship_date_sk - cs_sold_date_sk  > 120) then 1 else 0 end)  as `>120 days`\nfrom\n   catalog_sales\n  ,warehouse\n  ,ship_mode\n  ,call_center\n  ,date_dim\nwhere\n    d_month_seq between 1205 and 1205 + 11\nand cs_ship_date_sk   = d_date_sk\nand cs_warehouse_sk   = w_warehouse_sk\nand cs_ship_mode_sk   = sm_ship_mode_sk\nand cs_call_center_sk = cc_call_center_sk\ngroup by\n   substr(w_warehouse_name,1,20)\n  ,sm_type\n  ,cc_name\norder by substr(w_warehouse_name,1,20)\n        ,sm_type\n        ,cc_name\nlimit 100\"\"\"\n  )\n}"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/TPCDSDataLoad.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage benchmark\n\ncase class TPCDSDataLoadConf(\n    protected val format: Option[String] = None,\n    scaleInGB: Int = 0,\n    userDefinedDbName: Option[String] = None,\n    sourcePath: Option[String] = None,\n    benchmarkPath: Option[String] = None,\n    excludeNulls: Boolean = true) extends TPCDSConf\n\nobject TPCDSDataLoadConf {\n  import scopt.OParser\n  private val builder = OParser.builder[TPCDSDataLoadConf]\n  private val argParser = {\n    import builder._\n    OParser.sequence(\n      programName(\"TPC-DS Data Load\"),\n      opt[String](\"format\")\n        .required()\n        .action((x, c) => c.copy(format = Some(x)))\n        .text(\"file format to use\"),\n      opt[String](\"scale-in-gb\")\n        .required()\n        .valueName(\"<scale of benchmark in GBs>\")\n        .action((x, c) => c.copy(scaleInGB = x.toInt))\n        .text(\"Scale factor of the TPCDS benchmark\"),\n      opt[String](\"benchmark-path\")\n        .required()\n        .valueName(\"<cloud storage path>\")\n        .action((x, c) => c.copy(benchmarkPath = Some(x)))\n        .text(\"Cloud storage path to be used for creating table and generating reports\"),\n      opt[String](\"db-name\")\n        .optional()\n        .valueName(\"<database name>\")\n        .action((x, c) => c.copy(userDefinedDbName = Some(x)))\n        .text(\"Name of the target database to create with TPC-DS tables in necessary format\"),\n      opt[String](\"source-path\")\n        .optional()\n        .valueName(\"<path to the TPC-DS raw input data>\")\n        .action((x, c) => c.copy(sourcePath = Some(x)))\n        .text(\"The location of the TPC-DS raw input data\"),\n      opt[String](\"exclude-nulls\")\n        .optional()\n        .valueName(\"true/false\")\n        .action((x, c) => c.copy(excludeNulls = x.toBoolean))\n        .text(\"Whether to remove null primary keys when loading data, default = false\"),\n    )\n  }\n\n  def parse(args: Array[String]): Option[TPCDSDataLoadConf] = {\n    OParser.parse(argParser, args, TPCDSDataLoadConf())\n  }\n}\n\nclass TPCDSDataLoad(conf: TPCDSDataLoadConf) extends Benchmark(conf) {\n  import TPCDSDataLoad._\n\n  def runInternal(): Unit = {\n    val dbName = conf.dbName\n    val dbLocation = conf.dbLocation(dbName, suffix=benchmarkId.replace(\"-\", \"_\"))\n    val dbCatalog = \"spark_catalog\"\n\n    val partitionTables = true\n    val primaryKeys = true\n\n    val sourceFormat = \"parquet\"\n    require(conf.scaleInGB > 0)\n    require(Seq(1, 3000).contains(conf.scaleInGB), \"\")\n    val sourceLocation = conf.sourcePath.getOrElse {\n      s\"s3://devrel-delta-datasets/tpcds-2.13/tpcds_sf${conf.scaleInGB}_parquet/\"\n    }\n\n    runQuery(s\"DROP DATABASE IF EXISTS ${dbName} CASCADE\", s\"drop-database\")\n    runQuery(s\"CREATE DATABASE IF NOT EXISTS ${dbName}\", s\"create-database\")\n\n    // Iterate through all the source tables\n    tableNamesTpcds.foreach { tableName =>\n      val sourceTableLocation = s\"${sourceLocation}/${tableName}/\"\n      val targetLocation = s\"${dbLocation}/${tableName}/\"\n      val fullTableName = s\"`$dbName`.`$tableName`\"\n      log(s\"Generating $tableName at $dbLocation/$tableName\")\n      val partitionedBy =\n        if (!partitionTables || tablePartitionKeys(tableName)(0).isEmpty) \"\"\n        else \"PARTITIONED BY \" + tablePartitionKeys(tableName).mkString(\"(\", \", \", \")\")\n\n      // Excluding nulls automatically when n\n      val excludeNulls =\n        if (!partitionTables || tablePartitionKeys(tableName)(0).isEmpty) \"\"\n        else \"WHERE \" + tablePartitionKeys(tableName)(0) + \" IS NOT NULL\"\n\n      var tableOptions = \"\"\n      runQuery(s\"DROP TABLE IF EXISTS $fullTableName\", s\"drop-table-$tableName\")\n\n      runQuery(s\"\"\"CREATE TABLE $fullTableName\n                   USING ${conf.formatName}\n                   $partitionedBy $tableOptions\n                   LOCATION '$targetLocation'\n                   SELECT * FROM `${sourceFormat}`.`$sourceTableLocation` $excludeNulls\n                \"\"\", s\"create-table-$tableName\", ignoreError = true)\n\n      val sourceCount =\n        spark.sql(s\"SELECT * FROM `${sourceFormat}`.`$sourceTableLocation` ${excludeNulls}\").count()\n      val targetCount = spark.table(fullTableName).count()\n      assert(targetCount == sourceCount,\n        s\"Row count mismatch: source table = $sourceCount, target $fullTableName = $targetCount\")\n    }\n    log(s\"====== Created all tables in database ${dbName} at '${dbLocation}' =======\")\n\n    runQuery(s\"USE ${dbCatalog}.${dbName};\")\n    runQuery(\"SHOW TABLES\", printRows = true)\n\n  }\n}\nobject TPCDSDataLoad {\n  def main(args: Array[String]): Unit = {\n    TPCDSDataLoadConf.parse(args).foreach { conf =>\n      new TPCDSDataLoad(conf).run()\n    }\n  }\n\n  val tableNamesTpcds = Seq(\n    // with partitions\n    \"inventory\", \"catalog_returns\", \"catalog_sales\", \"store_returns\",  \"web_returns\", \"web_sales\",  \"store_sales\",\n    // no partitions\n    \"call_center\", \"catalog_page\", \"customer_address\", \"customer_demographics\", \"customer\", \"date_dim\",\n    \"household_demographics\", \"income_band\", \"item\", \"promotion\", \"reason\", \"ship_mode\", \"store\", \"time_dim\",\n    \"warehouse\", \"web_page\", \"web_site\"\n  ).sorted\n\n\n  val tableColumnSchemas = Map(\n    \"dbgen_version\" -> \"\"\"\n    dv_version                varchar(16)                   ,\n    dv_create_date            date                          ,\n    dv_create_time            time                          ,\n    dv_cmdline_args           varchar(200)\n\"\"\",\n    \"call_center\" -> \"\"\"\n    cc_call_center_sk         integer               not null,\n    cc_call_center_id         char(16)              not null,\n    cc_rec_start_date         date                          ,\n    cc_rec_end_date           date                          ,\n    cc_closed_date_sk         integer                       ,\n    cc_open_date_sk           integer                       ,\n    cc_name                   varchar(50)                   ,\n    cc_class                  varchar(50)                   ,\n    cc_employees              integer                       ,\n    cc_sq_ft                  integer                       ,\n    cc_hours                  char(20)                      ,\n    cc_manager                varchar(40)                   ,\n    cc_mkt_id                 integer                       ,\n    cc_mkt_class              char(50)                      ,\n    cc_mkt_desc               varchar(100)                  ,\n    cc_market_manager         varchar(40)                   ,\n    cc_division               integer                       ,\n    cc_division_name          varchar(50)                   ,\n    cc_company                integer                       ,\n    cc_company_name           char(50)                      ,\n    cc_street_number          char(10)                      ,\n    cc_street_name            varchar(60)                   ,\n    cc_street_type            char(15)                      ,\n    cc_suite_number           char(10)                      ,\n    cc_city                   varchar(60)                   ,\n    cc_county                 varchar(30)                   ,\n    cc_state                  char(2)                       ,\n    cc_zip                    char(10)                      ,\n    cc_country                varchar(20)                   ,\n    cc_gmt_offset             decimal(5,2)                  ,\n    cc_tax_percentage         decimal(5,2)\n\"\"\",\n    \"catalog_page\" -> \"\"\"\n    cp_catalog_page_sk        integer               not null,\n    cp_catalog_page_id        char(16)              not null,\n    cp_start_date_sk          integer                       ,\n    cp_end_date_sk            integer                       ,\n    cp_department             varchar(50)                   ,\n    cp_catalog_number         integer                       ,\n    cp_catalog_page_number    integer                       ,\n    cp_description            varchar(100)                  ,\n    cp_type                   varchar(100)\n\"\"\",\n    \"catalog_returns\" -> \"\"\"\n    cr_returned_date_sk       integer                       ,\n    cr_returned_time_sk       integer                       ,\n    cr_item_sk                integer               not null,\n    cr_refunded_customer_sk   integer                       ,\n    cr_refunded_cdemo_sk      integer                       ,\n    cr_refunded_hdemo_sk      integer                       ,\n    cr_refunded_addr_sk       integer                       ,\n    cr_returning_customer_sk  integer                       ,\n    cr_returning_cdemo_sk     integer                       ,\n    cr_returning_hdemo_sk     integer                       ,\n    cr_returning_addr_sk      integer                       ,\n    cr_call_center_sk         integer                       ,\n    cr_catalog_page_sk        integer                       ,\n    cr_ship_mode_sk           integer                       ,\n    cr_warehouse_sk           integer                       ,\n    cr_reason_sk              integer                       ,\n    cr_order_number           bigint                not null,\n    cr_return_quantity        integer                       ,\n    cr_return_amount          decimal(7,2)                  ,\n    cr_return_tax             decimal(7,2)                  ,\n    cr_return_amt_inc_tax     decimal(7,2)                  ,\n    cr_fee                    decimal(7,2)                  ,\n    cr_return_ship_cost       decimal(7,2)                  ,\n    cr_refunded_cash          decimal(7,2)                  ,\n    cr_reversed_charge        decimal(7,2)                  ,\n    cr_store_credit           decimal(7,2)                  ,\n    cr_net_loss               decimal(7,2)\n\"\"\",\n    \"catalog_sales\" -> \"\"\"\n    cs_sold_date_sk           integer                       ,\n    cs_sold_time_sk           integer                       ,\n    cs_ship_date_sk           integer                       ,\n    cs_bill_customer_sk       integer                       ,\n    cs_bill_cdemo_sk          integer                       ,\n    cs_bill_hdemo_sk          integer                       ,\n    cs_bill_addr_sk           integer                       ,\n    cs_ship_customer_sk       integer                       ,\n    cs_ship_cdemo_sk          integer                       ,\n    cs_ship_hdemo_sk          integer                       ,\n    cs_ship_addr_sk           integer                       ,\n    cs_call_center_sk         integer                       ,\n    cs_catalog_page_sk        integer                       ,\n    cs_ship_mode_sk           integer                       ,\n    cs_warehouse_sk           integer                       ,\n    cs_item_sk                integer               not null,\n    cs_promo_sk               integer                       ,\n    cs_order_number           bigint                not null,\n    cs_quantity               integer                       ,\n    cs_wholesale_cost         decimal(7,2)                  ,\n    cs_list_price             decimal(7,2)                  ,\n    cs_sales_price            decimal(7,2)                  ,\n    cs_ext_discount_amt       decimal(7,2)                  ,\n    cs_ext_sales_price        decimal(7,2)                  ,\n    cs_ext_wholesale_cost     decimal(7,2)                  ,\n    cs_ext_list_price         decimal(7,2)                  ,\n    cs_ext_tax                decimal(7,2)                  ,\n    cs_coupon_amt             decimal(7,2)                  ,\n    cs_ext_ship_cost          decimal(7,2)                  ,\n    cs_net_paid               decimal(7,2)                  ,\n    cs_net_paid_inc_tax       decimal(7,2)                  ,\n    cs_net_paid_inc_ship      decimal(7,2)                  ,\n    cs_net_paid_inc_ship_tax  decimal(7,2)                  ,\n    cs_net_profit             decimal(7,2)\n\"\"\",\n    \"customer\" -> \"\"\"\n    c_customer_sk             integer               not null,\n    c_customer_id             char(16)              not null,\n    c_current_cdemo_sk        integer                       ,\n    c_current_hdemo_sk        integer                       ,\n    c_current_addr_sk         integer                       ,\n    c_first_shipto_date_sk    integer                       ,\n    c_first_sales_date_sk     integer                       ,\n    c_salutation              char(10)                      ,\n    c_first_name              char(20)                      ,\n    c_last_name               char(30)                      ,\n    c_preferred_cust_flag     char(1)                       ,\n    c_birth_day               integer                       ,\n    c_birth_month             integer                       ,\n    c_birth_year              integer                       ,\n    c_birth_country           varchar(20)                   ,\n    c_login                   char(13)                      ,\n    c_email_address           char(50)                      ,\n    c_last_review_date_sk     integer\n\"\"\",\n    \"customer_address\" -> \"\"\"\n    ca_address_sk             integer               not null,\n    ca_address_id             char(16)              not null,\n    ca_street_number          char(10)                      ,\n    ca_street_name            varchar(60)                   ,\n    ca_street_type            char(15)                      ,\n    ca_suite_number           char(10)                      ,\n    ca_city                   varchar(60)                   ,\n    ca_county                 varchar(30)                   ,\n    ca_state                  char(2)                       ,\n    ca_zip                    char(10)                      ,\n    ca_country                varchar(20)                   ,\n    ca_gmt_offset             decimal(5,2)                  ,\n    ca_location_type          char(20)\n\"\"\",\n    \"customer_demographics\" -> \"\"\"\n    cd_demo_sk                integer               not null,\n    cd_gender                 char(1)                       ,\n    cd_marital_status         char(1)                       ,\n    cd_education_status       char(20)                      ,\n    cd_purchase_estimate      integer                       ,\n    cd_credit_rating          char(10)                      ,\n    cd_dep_count              integer                       ,\n    cd_dep_employed_count     integer                       ,\n    cd_dep_college_count      integer\n\"\"\",\n    \"date_dim\" -> \"\"\"\n    d_date_sk                 integer               not null,\n    d_date_id                 char(16)              not null,\n    d_date                    date                          ,\n    d_month_seq               integer                       ,\n    d_week_seq                integer                       ,\n    d_quarter_seq             integer                       ,\n    d_year                    integer                       ,\n    d_dow                     integer                       ,\n    d_moy                     integer                       ,\n    d_dom                     integer                       ,\n    d_qoy                     integer                       ,\n    d_fy_year                 integer                       ,\n    d_fy_quarter_seq          integer                       ,\n    d_fy_week_seq             integer                       ,\n    d_day_name                char(9)                       ,\n    d_quarter_name            char(6)                       ,\n    d_holiday                 char(1)                       ,\n    d_weekend                 char(1)                       ,\n    d_following_holiday       char(1)                       ,\n    d_first_dom               integer                       ,\n    d_last_dom                integer                       ,\n    d_same_day_ly             integer                       ,\n    d_same_day_lq             integer                       ,\n    d_current_day             char(1)                       ,\n    d_current_week            char(1)                       ,\n    d_current_month           char(1)                       ,\n    d_current_quarter         char(1)                       ,\n    d_current_year            char(1)\n\"\"\",\n    \"household_demographics\" -> \"\"\"\n    hd_demo_sk                integer               not null,\n    hd_income_band_sk         integer                       ,\n    hd_buy_potential          char(15)                      ,\n    hd_dep_count              integer                       ,\n    hd_vehicle_count          integer\n\"\"\",\n\n    \"income_band\" -> \"\"\"\n    ib_income_band_sk         integer               not null,\n    ib_lower_bound            integer                       ,\n    ib_upper_bound            integer\n\"\"\",\n    \"inventory\" -> \"\"\"\n    inv_date_sk               integer               not null,\n    inv_item_sk               integer               not null,\n    inv_warehouse_sk          integer               not null,\n    inv_quantity_on_hand      integer\n\"\"\",\n    \"item\" -> \"\"\"\n    i_item_sk                 integer               not null,\n    i_item_id                 char(16)              not null,\n    i_rec_start_date          date                          ,\n    i_rec_end_date            date                          ,\n    i_item_desc               varchar(200)                  ,\n    i_current_price           decimal(7,2)                  ,\n    i_wholesale_cost          decimal(7,2)                  ,\n    i_brand_id                integer                       ,\n    i_brand                   char(50)                      ,\n    i_class_id                integer                       ,\n    i_class                   char(50)                      ,\n    i_category_id             integer                       ,\n    i_category                char(50)                      ,\n    i_manufact_id             integer                       ,\n    i_manufact                char(50)                      ,\n    i_size                    char(20)                      ,\n    i_formulation             char(20)                      ,\n    i_color                   char(20)                      ,\n    i_units                   char(10)                      ,\n    i_container               char(10)                      ,\n    i_manager_id              integer                       ,\n    i_product_name            char(50)\n\"\"\",\n    \"promotion\" -> \"\"\"\n    p_promo_sk                integer               not null,\n    p_promo_id                char(16)              not null,\n    p_start_date_sk           integer                       ,\n    p_end_date_sk             integer                       ,\n    p_item_sk                 integer                       ,\n    p_cost                    decimal(15,2)                 ,\n    p_response_target         integer                       ,\n    p_promo_name              char(50)                      ,\n    p_channel_dmail           char(1)                       ,\n    p_channel_email           char(1)                       ,\n    p_channel_catalog         char(1)                       ,\n    p_channel_tv              char(1)                       ,\n    p_channel_radio           char(1)                       ,\n    p_channel_press           char(1)                       ,\n    p_channel_event           char(1)                       ,\n    p_channel_demo            char(1)                       ,\n    p_channel_details         varchar(100)                  ,\n    p_purpose                 char(15)                      ,\n    p_discount_active         char(1)\n\"\"\",\n    \"reason\" -> \"\"\"\n    r_reason_sk               integer               not null,\n    r_reason_id               char(16)              not null,\n    r_reason_desc             char(100)\n\"\"\",\n    \"ship_mode\" -> \"\"\"\n    sm_ship_mode_sk           integer               not null,\n    sm_ship_mode_id           char(16)              not null,\n    sm_type                   char(30)                      ,\n    sm_code                   char(10)                      ,\n    sm_carrier                char(20)                      ,\n    sm_contract               char(20)\n\"\"\",\n    \"store\" -> \"\"\"\n    s_store_sk                integer               not null,\n    s_store_id                char(16)              not null,\n    s_rec_start_date          date                          ,\n    s_rec_end_date            date                          ,\n    s_closed_date_sk          integer                       ,\n    s_store_name              varchar(50)                   ,\n    s_number_employees        integer                       ,\n    s_floor_space             integer                       ,\n    s_hours                   char(20)                      ,\n    s_manager                 varchar(40)                   ,\n    s_market_id               integer                       ,\n    s_geography_class         varchar(100)                  ,\n    s_market_desc             varchar(100)                  ,\n    s_market_manager          varchar(40)                   ,\n    s_division_id             integer                       ,\n    s_division_name           varchar(50)                   ,\n    s_company_id              integer                       ,\n    s_company_name            varchar(50)                   ,\n    s_street_number           varchar(10)                   ,\n    s_street_name             varchar(60)                   ,\n    s_street_type             char(15)                      ,\n    s_suite_number            char(10)                      ,\n    s_city                    varchar(60)                   ,\n    s_county                  varchar(30)                   ,\n    s_state                   char(2)                       ,\n    s_zip                     char(10)                      ,\n    s_country                 varchar(20)                   ,\n    s_gmt_offset              decimal(5,2)                  ,\n    s_tax_precentage          decimal(5,2)\n\"\"\",\n    \"store_returns\" -> \"\"\"\n    sr_returned_date_sk       integer                       ,\n    sr_return_time_sk         integer                       ,\n    sr_item_sk                integer               not null,\n    sr_customer_sk            integer                       ,\n    sr_cdemo_sk               integer                       ,\n    sr_hdemo_sk               integer                       ,\n    sr_addr_sk                integer                       ,\n    sr_store_sk               integer                       ,\n    sr_reason_sk              integer                       ,\n    sr_ticket_number          bigint                not null,\n    sr_return_quantity        integer                       ,\n    sr_return_amt             decimal(7,2)                  ,\n    sr_return_tax             decimal(7,2)                  ,\n    sr_return_amt_inc_tax     decimal(7,2)                  ,\n    sr_fee                    decimal(7,2)                  ,\n    sr_return_ship_cost       decimal(7,2)                  ,\n    sr_refunded_cash          decimal(7,2)                  ,\n    sr_reversed_charge        decimal(7,2)                  ,\n    sr_store_credit           decimal(7,2)                  ,\n    sr_net_loss               decimal(7,2)\n\"\"\",\n\n    \"store_sales\" -> \"\"\"\n    ss_sold_date_sk           integer                       ,\n    ss_sold_time_sk           integer                       ,\n    ss_item_sk                integer               not null,\n    ss_customer_sk            integer                       ,\n    ss_cdemo_sk               integer                       ,\n    ss_hdemo_sk               integer                       ,\n    ss_addr_sk                integer                       ,\n    ss_store_sk               integer                       ,\n    ss_promo_sk               integer                       ,\n    ss_ticket_number          bigint                not null,\n    ss_quantity               integer                       ,\n    ss_wholesale_cost         decimal(7,2)                  ,\n    ss_list_price             decimal(7,2)                  ,\n    ss_sales_price            decimal(7,2)                  ,\n    ss_ext_discount_amt       decimal(7,2)                  ,\n    ss_ext_sales_price        decimal(7,2)                  ,\n    ss_ext_wholesale_cost     decimal(7,2)                  ,\n    ss_ext_list_price         decimal(7,2)                  ,\n    ss_ext_tax                decimal(7,2)                  ,\n    ss_coupon_amt             decimal(7,2)                  ,\n    ss_net_paid               decimal(7,2)                  ,\n    ss_net_paid_inc_tax       decimal(7,2)                  ,\n    ss_net_profit             decimal(7,2)\n\"\"\",\n    \"time_dim\" -> \"\"\"\n    t_time_sk                 integer               not null,\n    t_time_id                 char(16)              not null,\n    t_time                    integer                       ,\n    t_hour                    integer                       ,\n    t_minute                  integer                       ,\n    t_second                  integer                       ,\n    t_am_pm                   char(2)                       ,\n    t_shift                   char(20)                      ,\n    t_sub_shift               char(20)                      ,\n    t_meal_time               char(20)\n\"\"\",\n    \"warehouse\" -> \"\"\"\n    w_warehouse_sk            integer               not null,\n    w_warehouse_id            char(16)              not null,\n    w_warehouse_name          varchar(20)                   ,\n    w_warehouse_sq_ft         integer                       ,\n    w_street_number           char(10)                      ,\n    w_street_name             varchar(60)                   ,\n    w_street_type             char(15)                      ,\n    w_suite_number            char(10)                      ,\n    w_city                    varchar(60)                   ,\n    w_county                  varchar(30)                   ,\n    w_state                   char(2)                       ,\n    w_zip                     char(10)                      ,\n    w_country                 varchar(20)                   ,\n    w_gmt_offset              decimal(5,2)\n\"\"\",\n    \"web_page\" -> \"\"\"\n    wp_web_page_sk            integer               not null,\n    wp_web_page_id            char(16)              not null,\n    wp_rec_start_date         date                          ,\n    wp_rec_end_date           date                          ,\n    wp_creation_date_sk       integer                       ,\n    wp_access_date_sk         integer                       ,\n    wp_autogen_flag           char(1)                       ,\n    wp_customer_sk            integer                       ,\n    wp_url                    varchar(100)                  ,\n    wp_type                   char(50)                      ,\n    wp_char_count             integer                       ,\n    wp_link_count             integer                       ,\n    wp_image_count            integer                       ,\n    wp_max_ad_count           integer\n\"\"\",\n    \"web_returns\" -> \"\"\"\n    wr_returned_date_sk       integer                       ,\n    wr_returned_time_sk       integer                       ,\n    wr_item_sk                integer               not null,\n    wr_refunded_customer_sk   integer                       ,\n    wr_refunded_cdemo_sk      integer                       ,\n    wr_refunded_hdemo_sk      integer                       ,\n    wr_refunded_addr_sk       integer                       ,\n    wr_returning_customer_sk  integer                       ,\n    wr_returning_cdemo_sk     integer                       ,\n    wr_returning_hdemo_sk     integer                       ,\n    wr_returning_addr_sk      integer                       ,\n    wr_web_page_sk            integer                       ,\n    wr_reason_sk              integer                       ,\n    wr_order_number           bigint                not null,\n    wr_return_quantity        integer                       ,\n    wr_return_amt             decimal(7,2)                  ,\n    wr_return_tax             decimal(7,2)                  ,\n    wr_return_amt_inc_tax     decimal(7,2)                  ,\n    wr_fee                    decimal(7,2)                  ,\n    wr_return_ship_cost       decimal(7,2)                  ,\n    wr_refunded_cash          decimal(7,2)                  ,\n    wr_reversed_charge        decimal(7,2)                  ,\n    wr_account_credit         decimal(7,2)                  ,\n    wr_net_loss               decimal(7,2)\n\"\"\",\n    \"web_sales\" -> \"\"\"\n    ws_sold_date_sk           integer                       ,\n    ws_sold_time_sk           integer                       ,\n    ws_ship_date_sk           integer                       ,\n    ws_item_sk                integer               not null,\n    ws_bill_customer_sk       integer                       ,\n    ws_bill_cdemo_sk          integer                       ,\n    ws_bill_hdemo_sk          integer                       ,\n    ws_bill_addr_sk           integer                       ,\n    ws_ship_customer_sk       integer                       ,\n    ws_ship_cdemo_sk          integer                       ,\n    ws_ship_hdemo_sk          integer                       ,\n    ws_ship_addr_sk           integer                       ,\n    ws_web_page_sk            integer                       ,\n    ws_web_site_sk            integer                       ,\n    ws_ship_mode_sk           integer                       ,\n    ws_warehouse_sk           integer                       ,\n    ws_promo_sk               integer                       ,\n    ws_order_number           bigint                not null,\n    ws_quantity               integer                       ,\n    ws_wholesale_cost         decimal(7,2)                  ,\n    ws_list_price             decimal(7,2)                  ,\n    ws_sales_price            decimal(7,2)                  ,\n    ws_ext_discount_amt       decimal(7,2)                  ,\n    ws_ext_sales_price        decimal(7,2)                  ,\n    ws_ext_wholesale_cost     decimal(7,2)                  ,\n    ws_ext_list_price         decimal(7,2)                  ,\n    ws_ext_tax                decimal(7,2)                  ,\n    ws_coupon_amt             decimal(7,2)                  ,\n    ws_ext_ship_cost          decimal(7,2)                  ,\n    ws_net_paid               decimal(7,2)                  ,\n    ws_net_paid_inc_tax       decimal(7,2)                  ,\n    ws_net_paid_inc_ship      decimal(7,2)                  ,\n    ws_net_paid_inc_ship_tax  decimal(7,2)                  ,\n    ws_net_profit             decimal(7,2)\n\"\"\",\n    \"web_site\" -> \"\"\"\n    web_site_sk               integer               not null,\n    web_site_id               char(16)              not null,\n    web_rec_start_date        date                          ,\n    web_rec_end_date          date                          ,\n    web_name                  varchar(50)                   ,\n    web_open_date_sk          integer                       ,\n    web_close_date_sk         integer                       ,\n    web_class                 varchar(50)                   ,\n    web_manager               varchar(40)                   ,\n    web_mkt_id                integer                       ,\n    web_mkt_class             varchar(50)                   ,\n    web_mkt_desc              varchar(100)                  ,\n    web_market_manager        varchar(40)                   ,\n    web_company_id            integer                       ,\n    web_company_name          char(50)                      ,\n    web_street_number         char(10)                      ,\n    web_street_name           varchar(60)                   ,\n    web_street_type           char(15)                      ,\n    web_suite_number          char(10)                      ,\n    web_city                  varchar(60)                   ,\n    web_county                varchar(30)                   ,\n    web_state                 char(2)                       ,\n    web_zip                   char(10)                      ,\n    web_country               varchar(20)                   ,\n    web_gmt_offset            decimal(5,2)                  ,\n    web_tax_percentage        decimal(5,2)\n\"\"\"\n  )\n\n  val tablePrimaryKeys = Map(\n    \"dbgen_version\" -> Seq(\"\"),\n    \"call_center\" -> Seq(\"cc_call_center_sk\"),\n    \"catalog_page\" -> Seq(\"cp_catalog_page_sk\"),\n    \"catalog_returns\" -> Seq(\"cr_item_sk\", \"cr_order_number\"),\n    \"catalog_sales\" -> Seq(\"cs_item_sk\", \"cs_order_number\"),\n    \"customer\" -> Seq(\"c_customer_sk\"),\n    \"customer_address\" -> Seq(\"ca_address_sk\"),\n    \"customer_demographics\" -> Seq(\"cd_demo_sk\"),\n    \"date_dim\" -> Seq(\"d_date_sk\"),\n    \"household_demographics\" -> Seq(\"hd_demo_sk\"),\n    \"income_band\" -> Seq(\"ib_income_band_sk\"),\n    \"inventory\" -> Seq(\"inv_date_sk\", \"inv_item_sk\", \"inv_warehouse_sk\"),\n    \"item\" -> Seq(\"i_item_sk\"),\n    \"promotion\" -> Seq(\"p_promo_sk\"),\n    \"reason\" -> Seq(\"r_reason_sk\"),\n    \"ship_mode\" -> Seq(\"sm_ship_mode_sk\"),\n    \"store\" -> Seq(\"s_store_sk\"),\n    \"store_returns\" -> Seq(\"sr_item_sk\", \"sr_ticket_number\"),\n    \"store_sales\" -> Seq(\"ss_item_sk\", \"ss_ticket_number\"),\n    \"time_dim\" -> Seq(\"t_time_sk\"),\n    \"warehouse\" -> Seq(\"w_warehouse_sk\"),\n    \"web_page\" -> Seq(\"wp_web_page_sk\"),\n    \"web_returns\" -> Seq(\"wr_item_sk\", \"wr_order_number\"),\n    \"web_sales\" -> Seq(\"ws_item_sk\", \"ws_order_number\"),\n    \"web_site\" -> Seq(\"web_site_sk\")\n  )\n\n\n  val tablePartitionKeys = Map(\n    \"dbgen_version\" -> Seq(\"\"),\n    \"call_center\" -> Seq(\"\"),\n    \"catalog_page\" -> Seq(\"\"),\n    \"catalog_returns\" -> Seq(\"cr_returned_date_sk\"),\n    \"catalog_sales\" -> Seq(\"cs_sold_date_sk\"),\n    \"customer\" -> Seq(\"\"),\n    \"customer_address\" -> Seq(\"\"),\n    \"customer_demographics\" -> Seq(\"\"),\n    \"date_dim\" -> Seq(\"\"),\n    \"household_demographics\" -> Seq(\"\"),\n    \"income_band\" -> Seq(\"\"),\n    \"inventory\" -> Seq(\"inv_date_sk\"),\n    \"item\" -> Seq(\"\"),\n    \"promotion\" -> Seq(\"\"),\n    \"reason\" -> Seq(\"\"),\n    \"ship_mode\" -> Seq(\"\"),\n    \"store\" -> Seq(\"\"),\n    \"store_returns\" -> Seq(\"sr_returned_date_sk\"),\n    \"store_sales\" -> Seq(\"ss_sold_date_sk\"),\n    \"time_dim\" -> Seq(\"\"),\n    \"warehouse\" -> Seq(\"\"),\n    \"web_page\" -> Seq(\"\"),\n    \"web_returns\" -> Seq(\"wr_returned_date_sk\"),\n    \"web_sales\" -> Seq(\"ws_sold_date_sk\"),\n    \"web_site\" -> Seq(\"\")\n  )\n}\n"
  },
  {
    "path": "benchmarks/src/main/scala/benchmark/TestBenchmark.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage benchmark\n\ncase class TestBenchmarkConf(\n    dbName: Option[String] = None,\n    benchmarkPath: Option[String] = None) extends BenchmarkConf\n\nobject TestBenchmarkConf {\n  import scopt.OParser\n  private val builder = OParser.builder[TestBenchmarkConf]\n  private val argParser = {\n    import builder._\n    OParser.sequence(\n      programName(\"Test Benchmark\"),\n      opt[String](\"test-param\")\n        .required()\n        .action((x, c) => c) // ignore\n        .text(\"Name of the target database to create with TPC-DS tables in necessary format\"),\n      opt[String](\"benchmark-path\")\n        .optional()\n        .action((x, c) => c.copy(benchmarkPath = Some(x)))\n        .text(\"Cloud path to be used for creating table and generating reports\"),\n      opt[String](\"db-name\")\n        .optional()\n        .action((x, c) => c.copy(dbName = Some(x)))\n        .text(\"Name of the test database to create\")\n    )\n  }\n\n  def parse(args: Array[String]): Option[TestBenchmarkConf] = {\n    OParser.parse(argParser, args, TestBenchmarkConf())\n  }\n}\n\nclass TestBenchmark(conf: TestBenchmarkConf) extends Benchmark(conf) {\n  def runInternal(): Unit = {\n    // Test Spark SQL\n    runQuery(\"SELECT 1 AS X\", \"sql-test\")\n    if (conf.benchmarkPath.isEmpty) {\n      log(\"Skipping the delta read / write test as benchmark path has not been provided\")\n      return\n    }\n\n    val dbName = conf.dbName.getOrElse(benchmarkId.replaceAll(\"-\", \"_\"))\n    val dbLocation = conf.dbLocation(dbName)\n\n    // Run database management tests\n    runQuery(\"SHOW DATABASES\", \"db-list-test\")\n    runQuery(s\"\"\"CREATE DATABASE IF NOT EXISTS $dbName LOCATION \"$dbLocation\" \"\"\", \"db-create-test\")\n    runQuery(s\"USE $dbName\", \"db-use-test\")\n\n    // Run table tests\n    val tableName = \"test\"\n    runQuery(s\"DROP TABLE IF EXISTS $tableName\", \"table-drop-test\")\n    runQuery(s\"CREATE TABLE $tableName USING delta SELECT 1 AS x\", \"table-create-test\")\n    runQuery(s\"SELECT * FROM $tableName\", \"table-query-test\")\n  }\n}\n\nobject TestBenchmark {\n  def main(args: Array[String]): Unit = {\n    println(\"All command line args = \" + args.toSeq)\n    TestBenchmarkConf.parse(args).foreach { conf =>\n      new TestBenchmark(conf).run()\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/main/scala/org/apache/spark/SparkUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark\n\nimport benchmark.SparkEnvironmentInfo\n\nimport org.apache.spark.util.Utils\n\nobject SparkUtils {\n  def getEnvironmentInfo(sc: SparkContext): SparkEnvironmentInfo = {\n    val info = sc.statusStore.environmentInfo()\n    val sparkBuildInfo = Map(\n      \"sparkBuildBranch\" -> SPARK_BRANCH,\n      \"sparkBuildVersion\" -> SPARK_VERSION,\n      \"sparkBuildDate\" -> SPARK_BUILD_DATE,\n      \"sparkBuildUser\" -> SPARK_BUILD_USER,\n      \"sparkBuildRevision\" -> SPARK_REVISION\n    )\n\n    SparkEnvironmentInfo(\n      sparkBuildInfo = sparkBuildInfo,\n      runtimeInfo = caseClassToMap(info.runtime),\n      sparkProps = Utils.redact(sc.conf, info.sparkProperties).toMap,\n      hadoopProps =  Utils.redact(sc.conf, info.hadoopProperties).toMap\n        .filterKeys(k => !k.startsWith(\"mapred\") && !k.startsWith(\"yarn\")),\n      systemProps =  Utils.redact(sc.conf,  info.systemProperties).toMap,\n      classpathEntries = info.classpathEntries.toMap\n    )\n  }\n\n  def caseClassToMap(obj: Object): Map[String, String] = {\n    obj.getClass.getDeclaredFields.flatMap { f =>\n      f.setAccessible(true)\n      val valueOption = f.get(obj) match {\n        case o: Option[_] => o.map(_.toString)\n        case s => Some(s.toString)\n      }\n      valueOption.map(value => f.getName -> value)\n    }.toMap\n  }\n\n  def median(sizes: Array[Long], alreadySorted: Boolean): Long = Utils.median(sizes, alreadySorted)\n}\n"
  },
  {
    "path": "build/sbt",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n\n# When creating new tests for Spark SQL Hive, the HADOOP_CLASSPATH must contain the hive jars so\n# that we can run Hive to generate the golden answer.  This is not required for normal development\n# or testing.\nif [ -n \"$HIVE_HOME\" ]; then\n    for i in \"$HIVE_HOME\"/lib/*\n    do HADOOP_CLASSPATH=\"$HADOOP_CLASSPATH:$i\"\n    done\n    export HADOOP_CLASSPATH\nfi\n\nrealpath () {\n(\n  TARGET_FILE=\"$1\"\n\n  cd \"$(dirname \"$TARGET_FILE\")\"\n  TARGET_FILE=\"$(basename \"$TARGET_FILE\")\"\n\n  COUNT=0\n  while [ -L \"$TARGET_FILE\" -a $COUNT -lt 100 ]\n  do\n      TARGET_FILE=\"$(readlink \"$TARGET_FILE\")\"\n      cd $(dirname \"$TARGET_FILE\")\n      TARGET_FILE=\"$(basename $TARGET_FILE)\"\n      COUNT=$(($COUNT + 1))\n  done\n\n  echo \"$(pwd -P)/\"$TARGET_FILE\"\"\n)\n}\n\n# Make Jenkins use Google Mirror first as Maven Central may ban us\nSBT_REPOSITORIES_CONFIG=\"$(dirname \"$(realpath \"$0\")\")/sbt-config/repositories\"\nexport SBT_OPTS=\"-Dsbt.override.build.repos=true -Dsbt.repository.config=$SBT_REPOSITORIES_CONFIG\"\n\n. \"$(dirname \"$(realpath \"$0\")\")\"/sbt-launch-lib.bash\n\n\ndeclare -r noshare_opts=\"-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy\"\ndeclare -r sbt_opts_file=\".sbtopts\"\ndeclare -r etc_sbt_opts_file=\"/etc/sbt/sbtopts\"\n\nusage() {\n cat <<EOM\nUsage: $script_name [options]\n\n  -h | -help         print this message\n  -v | -verbose      this runner is chattier\n  -d | -debug        set sbt log level to debug\n  -no-colors         disable ANSI color codes\n  -sbt-create        start sbt even if current directory contains no sbt project\n  -sbt-dir   <path>  path to global settings/plugins directory (default: ~/.sbt)\n  -sbt-boot  <path>  path to shared boot directory (default: ~/.sbt/boot in 0.11 series)\n  -ivy       <path>  path to local Ivy repository (default: ~/.ivy2)\n  -mem    <integer>  set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem))\n  -no-share          use all local caches; no sharing\n  -no-global         uses global caches, but does not use global ~/.sbt directory.\n  -jvm-debug <port>  Turn on JVM debugging, open at the given port.\n  -batch             Disable interactive mode\n\n  # sbt version (default: from project/build.properties if present, else latest release)\n  -sbt-version  <version>   use the specified version of sbt\n  -sbt-jar      <path>      use the specified jar as the sbt launcher\n  -sbt-rc                   use an RC version of sbt\n  -sbt-snapshot             use a snapshot version of sbt\n\n  # java version (default: java from PATH, currently $(java -version 2>&1 | grep version))\n  -java-home <path>         alternate JAVA_HOME\n\n  # jvm options and output control\n  JAVA_OPTS          environment variable, if unset uses \"$java_opts\"\n  SBT_OPTS           environment variable, if unset uses \"$default_sbt_opts\"\n  .sbtopts           if this file exists in the current directory, it is\n                     prepended to the runner args\n  /etc/sbt/sbtopts   if this file exists, it is prepended to the runner args\n  -Dkey=val          pass -Dkey=val directly to the java runtime\n  -J-X               pass option -X directly to the java runtime\n                     (-J is stripped)\n  -S-X               add -X to sbt's scalacOptions (-S is stripped)\n  -PmavenProfiles    Enable a maven profile for the build.\n\nIn the case of duplicated or conflicting options, the order above\nshows precedence: JAVA_OPTS lowest, command line options highest.\nEOM\n}\n\nprocess_my_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n     -no-colors) addJava \"-Dsbt.log.noformat=true\" && shift ;;\n      -no-share) addJava \"$noshare_opts\" && shift ;;\n     -no-global) addJava \"-Dsbt.global.base=$(pwd)/project/.sbtboot\" && shift ;;\n      -sbt-boot) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.boot.directory=$2\" && shift 2 ;;\n       -sbt-dir) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.global.base=$2\" && shift 2 ;;\n     -debug-inc) addJava \"-Dxsbt.inc.debug=true\" && shift ;;\n         -batch) exec </dev/null && shift ;;\n\n    -sbt-create) sbt_create=true && shift ;;\n\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  # Now, ensure sbt version is used.\n  [[ \"${sbt_version}XXX\" != \"XXX\" ]] && addJava \"-Dsbt.version=$sbt_version\"\n}\n\nloadConfigFile() {\n  cat \"$1\" | sed '/^\\#/d'\n}\n\n# if sbtopts files exist, prepend their contents to $@ so it can be processed by this runner\n[[ -f \"$etc_sbt_opts_file\" ]] && set -- $(loadConfigFile \"$etc_sbt_opts_file\") \"$@\"\n[[ -f \"$sbt_opts_file\" ]] && set -- $(loadConfigFile \"$sbt_opts_file\") \"$@\"\n\nexit_status=127\nsaved_stty=\"\"\n\nrestoreSttySettings() {\n  stty $saved_stty\n  saved_stty=\"\"\n}\n\nonExit() {\n  if [[ \"$saved_stty\" != \"\" ]]; then\n    restoreSttySettings\n  fi\n  exit $exit_status\n}\n\nsaveSttySettings() {\n  saved_stty=$(stty -g 2>/dev/null)\n  if [[ ! $? ]]; then\n    saved_stty=\"\"\n  fi\n}\n\nsaveSttySettings\ntrap onExit INT\n\nrun \"$@\"\n\nexit_status=$?\nonExit\n"
  },
  {
    "path": "build/sbt-config/repositories",
    "content": "[repositories]\n  local\n  local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]\n  local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}\n  mulesoft: https://repository.mulesoft.org/nexus/content/groups/public/\n  gcs-maven-central-mirror: https://maven-central.storage-download.googleapis.com/maven2/\n  maven-central\n  typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly\n  sbt-ivy-snapshots: https://repo.scala-sbt.org/scalasbt/ivy-snapshots/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly\n  sbt-plugin-releases: https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]\n  bintray-typesafe-sbt-plugin-releases: https://dl.bintray.com/typesafe/sbt-plugins/, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]\n  repos-spark-packages: https://repos.spark-packages.org\n  typesafe-releases: https://repo.typesafe.com/typesafe/releases/\n  apache-snapshot: https://repository.apache.org/content/groups/snapshots/\n  jitpack: https://jitpack.io\n"
  },
  {
    "path": "build/sbt-launch-lib.bash",
    "content": "#!/usr/bin/env bash\n#\n\n# A library to simplify using the SBT launcher from other packages.\n# Note: This should be used by tools like giter8/conscript etc.\n\n# TODO - Should we merge the main SBT script with this library?\n\nif test -z \"$HOME\"; then\n  declare -r script_dir=\"$(dirname \"$script_path\")\"\nelse\n  declare -r script_dir=\"$HOME/.sbt\"\nfi\n\ndeclare -a residual_args\ndeclare -a java_args\ndeclare -a scalac_args\ndeclare -a sbt_commands\ndeclare -a maven_profiles\n\nif test -x \"$JAVA_HOME/bin/java\"; then\n    echo -e \"Using $JAVA_HOME as default JAVA_HOME.\"\n    echo \"Note, this will be overridden by -java-home if it is set.\"\n    declare java_cmd=\"$JAVA_HOME/bin/java\"\nelse\n    declare java_cmd=java\nfi\n\nechoerr () {\n  echo 1>&2 \"$@\"\n}\nvlog () {\n  [[ $verbose || $debug ]] && echoerr \"$@\"\n}\ndlog () {\n  [[ $debug ]] && echoerr \"$@\"\n}\n\ndownload_sbt () {\n  local url=$1\n  local output=$2\n  local temp_file=\"${output}.part\"\n\n  if [ $(command -v curl) ]; then\n    curl --fail --location --silent ${url} > \"${temp_file}\" &&\\\n      mv \"${temp_file}\" \"${output}\"\n  elif [ $(command -v wget) ]; then\n    wget --quiet ${url} -O \"${temp_file}\" &&\\\n      mv \"${temp_file}\" \"${output}\"\n  else\n    printf \"You do not have curl or wget installed, unable to downlaod ${url}\\n\"\n    exit -1\n  fi\n}\n\n\nacquire_sbt_jar () {\n  SBT_VERSION=`awk -F \"=\" '/sbt\\.version/ {print $2}' ./project/build.properties`\n\n  # Set primary and fallback URLs\n  if [[ \"${SBT_VERSION}\" == \"0.13.18\" ]] && [[ -n \"${SBT_MIRROR_JAR_URL}\" ]]; then\n    URL1=\"${SBT_MIRROR_JAR_URL}\"\n  elif [[ \"${SBT_VERSION}\" == \"1.5.5\" ]] && [[ -n \"${SBT_1_5_5_MIRROR_JAR_URL}\" ]]; then\n    URL1=\"${SBT_1_5_5_MIRROR_JAR_URL}\"\n  else\n    URL1=${DEFAULT_ARTIFACT_REPOSITORY:-https://maven-central.storage-download.googleapis.com/maven2/}org/scala-sbt/sbt-launch/${SBT_VERSION}/sbt-launch-${SBT_VERSION}.jar\n  fi\n  BACKUP_URL=\"https://repo1.maven.org/maven2/org/scala-sbt/sbt-launch/${SBT_VERSION}/sbt-launch-${SBT_VERSION}.jar\"\n\n  JAR=build/sbt-launch-${SBT_VERSION}.jar\n  sbt_jar=$JAR\n\n  if [[ ! -f \"$sbt_jar\" ]]; then\n    printf 'Attempting to fetch sbt from %s\\n' \"${URL1}\"\n    download_sbt \"${URL1}\" \"${JAR}\"\n\n    if [[ ! -f \"${JAR}\" ]]; then\n      printf 'Download from %s failed. Retrying from %s\\n' \"${URL1}\" \"${BACKUP_URL}\"\n      download_sbt \"${BACKUP_URL}\" \"${JAR}\"\n    fi\n\n    if [[ ! -f \"${JAR}\" ]]; then\n      printf \"Failed to download sbt. Please install sbt manually from https://www.scala-sbt.org/\\n\"\n      exit 1\n    fi\n    printf \"Launching sbt from ${JAR}\\n\"\n  fi\n}\n\nexecRunner () {\n  # print the arguments one to a line, quoting any containing spaces\n  [[ $verbose || $debug ]] && echo \"# Executing command line:\" && {\n    for arg; do\n      if printf \"%s\\n\" \"$arg\" | grep -q ' '; then\n        printf \"\\\"%s\\\"\\n\" \"$arg\"\n      else\n        printf \"%s\\n\" \"$arg\"\n      fi\n    done\n    echo \"\"\n  }\n\n  \"$@\"\n}\n\naddJava () {\n  dlog \"[addJava] arg = '$1'\"\n  java_args=( \"${java_args[@]}\" \"$1\" )\n}\n\nenableProfile () {\n  dlog \"[enableProfile] arg = '$1'\"\n  maven_profiles=( \"${maven_profiles[@]}\" \"$1\" )\n  export SBT_MAVEN_PROFILES=\"${maven_profiles[@]}\"\n}\n\naddSbt () {\n  dlog \"[addSbt] arg = '$1'\"\n  sbt_commands=( \"${sbt_commands[@]}\" \"$1\" )\n}\naddResidual () {\n  dlog \"[residual] arg = '$1'\"\n  residual_args=( \"${residual_args[@]}\" \"$1\" )\n}\naddDebugger () {\n  addJava \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1\"\n}\n\n# a ham-fisted attempt to move some memory settings in concert\n# so they need not be dicked around with individually.\nget_mem_opts () {\n  local mem=${1:-1000}\n  local perm=$(( $mem / 4 ))\n  (( $perm > 256 )) || perm=256\n  (( $perm < 4096 )) || perm=4096\n  local codecache=$(( $perm / 2 ))\n\n  echo \"-Xms${mem}m -Xmx${mem}m -XX:ReservedCodeCacheSize=${codecache}m\"\n}\n\nrequire_arg () {\n  local type=\"$1\"\n  local opt=\"$2\"\n  local arg=\"$3\"\n  if [[ -z \"$arg\" ]] || [[ \"${arg:0:1}\" == \"-\" ]]; then\n    echo \"$opt requires <$type> argument\" 1>&2\n    exit 1\n  fi\n}\n\nis_function_defined() {\n  declare -f \"$1\" > /dev/null\n}\n\nprocess_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n       -h|-help) usage; exit 1 ;;\n    -v|-verbose) verbose=1 && shift ;;\n      -d|-debug) debug=1 && shift ;;\n\n           -ivy) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.ivy.home=$2\" && shift 2 ;;\n           -mem) require_arg integer \"$1\" \"$2\" && sbt_mem=\"$2\" && shift 2 ;;\n     -jvm-debug) require_arg port \"$1\" \"$2\" && addDebugger $2 && shift 2 ;;\n         -batch) exec </dev/null && shift ;;\n\n       -sbt-jar) require_arg path \"$1\" \"$2\" && sbt_jar=\"$2\" && shift 2 ;;\n   -sbt-version) require_arg version \"$1\" \"$2\" && sbt_version=\"$2\" && shift 2 ;;\n     -java-home) require_arg path \"$1\" \"$2\" && java_cmd=\"$2/bin/java\" && export JAVA_HOME=$2 && shift 2 ;;\n\n            -D*) addJava \"$1\" && shift ;;\n            -J*) addJava \"${1:2}\" && shift ;;\n            -P*) enableProfile \"$1\" && shift ;;\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  is_function_defined process_my_args && {\n    myargs=(\"${residual_args[@]}\")\n    residual_args=()\n    process_my_args \"${myargs[@]}\"\n  }\n}\n\nrun() {\n  # no jar? download it.\n  [[ -f \"$sbt_jar\" ]] || acquire_sbt_jar \"$sbt_version\" || {\n    # still no jar? uh-oh.\n    echo \"Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar\"\n    exit 1\n  }\n\n  # process the combined args, then reset \"$@\" to the residuals\n  process_args \"$@\"\n  set -- \"${residual_args[@]}\"\n  argumentCount=$#\n\n  # run sbt\n  execRunner \"$java_cmd\" \\\n    ${SBT_OPTS:-$default_sbt_opts} \\\n    $(get_mem_opts $sbt_mem) \\\n    ${java_opts} \\\n    ${java_args[@]} \\\n    -jar \"$sbt_jar\" \\\n    \"${sbt_commands[@]}\" \\\n    \"${residual_args[@]}\"\n}\n"
  },
  {
    "path": "build.sbt",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// scalastyle:off line.size.limit\n\nimport java.nio.file.Files\n\nimport sbtprotoc.ProtocPlugin.autoImport._\n\nimport xsbti.compile.CompileAnalysis\n\nimport Checkstyle._\nimport ShadedIcebergBuild._\nimport Mima._\nimport Unidoc._\n\n// Scala versions\nval scala213 = \"2.13.17\"\nval all_scala_versions = Seq(scala213)\n\n// Due to how publishArtifact is determined for javaOnlyReleaseSettings, incl. storage\n// It was necessary to change default_scala_version to scala213 in build.sbt\n// to build the project with Scala 2.13 only\n// As a setting, it's possible to set it on command line easily\n// sbt 'set default_scala_version := 2.13.16' [commands]\n// FIXME Why not use scalaVersion?\nval default_scala_version = settingKey[String](\"Default Scala version\")\nGlobal / default_scala_version := scala213\n\n// Scala version to use for all projects\nscalaVersion := default_scala_version.value\n\n// crossScalaVersions must be set to Nil on the root project to avoid conflicts\ncrossScalaVersions := Nil\n\nval internalModuleNames = settingKey[Set[String]](\"Internal module artifact names to exclude from POM\")\n\n// Spark version to delta-spark and its dependent modules\n// For more information see CrossSparkVersions.scala\nval sparkVersion = settingKey[String](\"Spark version\")\n\n// Dependent library versions\nval defaultSparkVersion = SparkVersionSpec.DEFAULT.fullVersion // Spark version to use for testing in non-delta-spark related modules\nval hadoopVersion = \"3.4.2\"\nval sparkVersionForKernelTest = \"4.0.0\"\nval scalaTestVersion = \"3.2.15\"\nval scalaTestVersionForConnectors = \"3.0.8\"\nval parquet4sVersion = \"1.9.4\"\nval protoVersion = \"3.25.1\"\nval grpcVersion = \"1.62.2\"\nval flinkVersion = \"2.0.1\"\n\n// Define the ecosystem support flags.\nval supportIceberg = CrossSparkVersions.getSparkVersionSpec().supportIceberg\nval supportHudi = CrossSparkVersions.getSparkVersionSpec().supportHudi\n\n// For Java 11 use the following on command line\n// sbt 'set targetJvm := \"11\"' [commands]\nval targetJvm = settingKey[String](\"Target JVM version\")\nGlobal / targetJvm := \"11\"\n\nlazy val javaVersion = sys.props.getOrElse(\"java.version\", \"Unknown\")\nlazy val javaVersionInt = javaVersion.split(\"\\\\.\")(0).toInt\n\nlazy val commonSettings = Seq(\n  organization := \"io.delta\",\n  scalaVersion := default_scala_version.value,\n  crossScalaVersions := all_scala_versions,\n  fork := true,\n  scalacOptions ++= Seq(\"-Ywarn-unused:imports\"),\n  javacOptions ++= {\n    if (javaVersion.startsWith(\"1.8\")) {\n      Seq.empty // `--release` is supported since JDK 9 and the minimum supported JDK is 8\n    } else {\n      Seq(\"--release\", targetJvm.value) // generated bytecode should be usable with JVM 1.8\n    }\n  },\n\n  // Make sure any tests in any project that uses Spark is configured for running well locally\n  Test / javaOptions ++= Seq(\n    \"-Dspark.ui.enabled=false\",\n    \"-Dspark.ui.showConsoleProgress=false\",\n    \"-Dspark.databricks.delta.snapshotPartitions=2\",\n    \"-Dspark.sql.shuffle.partitions=5\",\n    \"-Ddelta.log.cacheSize=3\",\n    \"-Dspark.databricks.delta.delta.log.cacheSize=3\",\n    \"-Dspark.sql.sources.parallelPartitionDiscovery.parallelism=5\",\n    \"-Xmx1024m\"\n  ) ++ {\n    if (javaVersionInt >= 17) {\n      Seq(  // For Java 17 +\n        \"--add-opens=java.base/java.nio=ALL-UNNAMED\",\n        \"--add-opens=java.base/java.lang=ALL-UNNAMED\",\n        \"--add-opens=java.base/java.net=ALL-UNNAMED\",\n        \"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED\",\n        \"--add-opens=java.base/sun.util.calendar=ALL-UNNAMED\"\n      )\n    } else {\n      Seq.empty\n    }\n  },\n\n  testOptions += Tests.Argument(\"-oF\"),\n\n  // Unidoc settings: by default dont document any source file\n  unidocSourceFilePatterns := Nil,\n)\n\n////////////////////////////\n// START: Code Formatting //\n////////////////////////////\n\n/** Enforce java code style on compile. */\ndef javafmtCheckSettings(): Seq[Def.Setting[Task[CompileAnalysis]]] = Seq(\n  (Compile / compile) := ((Compile / compile) dependsOn (Compile / javafmtCheckAll)).value\n)\n\n/** Enforce scala code style on compile. */\ndef scalafmtCheckSettings(): Seq[Def.Setting[Task[CompileAnalysis]]] = Seq(\n  (Compile / compile) := ((Compile / compile) dependsOn (Compile / scalafmtCheckAll)).value,\n)\n\n// TODO: define fmtAll and fmtCheckAll tasks that run both scala and java fmts/checks\n\n//////////////////////////\n// END: Code Formatting //\n//////////////////////////\n\n/**\n * Note: we cannot access sparkVersion.value here, since that can only be used within a task or\n *       setting macro.\n */\ndef runTaskOnlyOnSparkMaster[T](\n    task: sbt.TaskKey[T],\n    taskName: String,\n    projectName: String,\n    emptyValue: => T): Def.Initialize[Task[T]] = {\n  if (CrossSparkVersions.getSparkVersionSpec().isMaster) {\n    Def.task(task.value)\n  } else {\n    Def.task {\n      // scalastyle:off println\n      val masterVersion = SparkVersionSpec.MASTER.map(_.fullVersion).getOrElse(\"(no master version configured)\")\n      println(s\"Project $projectName: Skipping `$taskName` as Spark version \" +\n        s\"${CrossSparkVersions.getSparkVersion()} does not equal $masterVersion.\")\n      // scalastyle:on println\n      emptyValue\n    }\n  }\n}\n\nlazy val connectCommon = (project in file(\"spark-connect/common\"))\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings(\n    name := \"delta-connect-common\",\n    commonSettings,\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n    releaseSettings,\n    // Export as JAR instead of classes directory. This ensures protobuf-generated classes\n    // (e.g., io.delta.connect.proto.DeltaCommand) are available as a JAR file in fullClasspath,\n    // which can be symlinked and picked up by Spark Submit's jars/* wildcard in connectClient tests.\n    exportJars := true,\n    libraryDependencies ++= Seq(\n      \"io.grpc\" % \"protoc-gen-grpc-java\" % grpcVersion asProtocPlugin(),\n      \"io.grpc\" % \"grpc-protobuf\" % grpcVersion,\n      \"io.grpc\" % \"grpc-stub\" % grpcVersion,\n      \"com.google.protobuf\" % \"protobuf-java\" % protoVersion % \"protobuf\",\n      \"javax.annotation\" % \"javax.annotation-api\" % \"1.3.2\",\n\n      \"org.apache.spark\" %% \"spark-connect-common\" % sparkVersion.value % \"provided\",\n    ),\n    PB.protocVersion := protoVersion,\n    Compile / PB.targets := Seq(\n      PB.gens.java -> (Compile / sourceManaged).value,\n      PB.gens.plugin(\"grpc-java\") -> (Compile / sourceManaged).value\n    )\n  )\n\nlazy val connectClient = (project in file(\"spark-connect/client\"))\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .dependsOn(connectCommon % \"compile->compile;test->test;provided->provided\")\n  .settings(\n    name := \"delta-connect-client\",\n    commonSettings,\n    releaseSettings,\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n    libraryDependencies ++= Seq(\n      \"com.google.protobuf\" % \"protobuf-java\" % protoVersion % \"protobuf\",\n      \"org.apache.spark\" %% \"spark-connect-client-jvm\" % sparkVersion.value % \"provided\",\n\n      // Test deps\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"org.apache.spark\" %% \"spark-connect-client-jvm\" % sparkVersion.value % \"test\" classifier \"tests\"\n    ),\n    (Test / javaOptions) += {\n      // Create a (mini) Spark Distribution based on the server classpath.\n      val serverClassPath = (connectServer / Compile / fullClasspath).value\n      val distributionDir = crossTarget.value / \"test-dist\"\n      val jarsDir = distributionDir / \"jars\"\n\n      if (!distributionDir.exists()) {\n        IO.createDirectory(jarsDir)\n        // Create symlinks for all dependencies (filter to only JAR files)\n        serverClassPath.distinct.filter(_.data.isFile).foreach { entry =>\n          val jarFile = entry.data.toPath\n          val linkedJarFile = jarsDir / entry.data.getName\n          if (!java.nio.file.Files.exists(linkedJarFile.toPath)) {\n            Files.createSymbolicLink(linkedJarFile.toPath, jarFile)\n          }\n        }\n        // Create a symlink for the log4j properties\n        val confDir = distributionDir / \"conf\"\n        IO.createDirectory(confDir)\n        val log4jProps = (sparkV1 / Test / resourceDirectory).value / \"log4j2.properties\"\n        val linkedLog4jProps = confDir / \"log4j2.properties\"\n        if (!java.nio.file.Files.exists(linkedLog4jProps.toPath)) {\n          Files.createSymbolicLink(linkedLog4jProps.toPath, log4jProps.toPath)\n        }\n      }\n      // Return the location of the distribution directory.\n      \"-Ddelta.spark.home=\" + distributionDir\n    },\n    // Required for testing addFeatureSupport/dropFeatureSupport.\n    Test / envVars += (\"DELTA_TESTING\", \"1\")\n  )\n\nlazy val connectServer = (project in file(\"spark-connect/server\"))\n  .dependsOn(connectCommon % \"compile->compile;test->test;provided->provided\")\n  .dependsOn(spark % \"compile->compile;test->test;provided->provided\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings(\n    name := \"delta-connect-server\",\n    commonSettings,\n    releaseSettings,\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n    // Export as JAR instead of classes directory. Required for connectClient test setup so that\n    // classes like SimpleDeltaConnectService are available as a JAR file that can be symlinked\n    // and picked up by Spark Submit's jars/* wildcard. Also prevents classpath conflicts.\n    exportJars := true,\n    assembly / assemblyMergeStrategy := {\n      // Discard module-info.class files from Java 9+ modules and multi-release JARs\n      case \"module-info.class\" => MergeStrategy.discard\n      case PathList(\"META-INF\", \"versions\", _, \"module-info.class\") => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (assembly / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n    libraryDependencies ++= Seq(\n      \"com.google.protobuf\" % \"protobuf-java\" % protoVersion % \"protobuf\",\n\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-connect\" % sparkVersion.value % \"provided\",\n\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-connect\" % sparkVersion.value % \"test\" classifier \"tests\",\n    ),\n    excludeDependencies ++= Seq(\n      // Exclude connect common because a properly shaded version of it is included in the\n      // spark-connect jar. Including it causes classpath problems.\n      ExclusionRule(\"org.apache.spark\", \"spark-connect-common_2.13\"),\n      // Exclude connect shims because we have spark-core on the classpath. The shims are only\n      // needed for the client. Including it causes classpath problems.\n      ExclusionRule(\"org.apache.spark\", \"spark-connect-shims_2.13\")\n    ),\n    // Required for testing addFeatureSupport/dropFeatureSupport.\n    Test / envVars += (\"DELTA_TESTING\", \"1\"),\n    // Force Spark to bind to localhost to avoid network issues\n    Test / envVars += (\"SPARK_LOCAL_IP\", \"127.0.0.1\")\n  )\n\nlazy val deltaSuiteGenerator = (project in file(\"spark/delta-suite-generator\"))\n  .disablePlugins(ScalafmtPlugin)\n  .settings (\n    name := \"delta-suite-generator\",\n    commonSettings,\n    scalaStyleSettings,\n    skipReleaseSettings, // Internal module - not published to Maven\n    libraryDependencies ++= Seq(\n      \"org.scala-lang.modules\" %% \"scala-collection-compat\" % \"2.11.0\",\n      \"org.scalameta\" %% \"scalameta\" % \"4.13.5\",\n      \"org.scalameta\" %% \"scalafmt-core\" % \"3.9.6\",\n      \"commons-cli\" % \"commons-cli\" % \"1.9.0\",\n      \"commons-codec\" % \"commons-codec\" % \"1.17.2\",\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n    ),\n    Compile / mainClass := Some(\"io.delta.suitegenerator.ModularSuiteGenerator\"),\n    Test / baseDirectory := (ThisBuild / baseDirectory).value,\n  )\n\n// ============================================================\n// Spark Module 1: sparkV1 (prod code only, no tests)\n// ============================================================\nlazy val sparkV1 = (project in file(\"spark\"))\n  .dependsOn(storage)\n  .enablePlugins(Antlr4Plugin)\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-spark-v1\",\n    commonSettings,\n    scalaStyleSettings,\n    skipReleaseSettings, // Internal module - not published to Maven\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n\n    // Export as JAR instead of classes directory. This prevents dependent projects\n    // (e.g., connectServer) from seeing multiple 'classes' directories with the same\n    // name in their classpath, which would cause FileAlreadyExistsException.\n    exportJars := true,\n\n    // Tests are compiled in the final 'spark' module to avoid circular dependencies\n    Test / sources := Seq.empty,\n    Test / resources := Seq.empty,\n\n    libraryDependencies ++= Seq(\n      // Adding test classifier seems to break transitive resolution of the core dependencies\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"provided\",\n      // For DynamoDBCommitStore\n      \"com.amazonaws\" % \"aws-java-sdk\" % \"1.12.262\" % \"provided\",\n\n      // Test deps\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"org.scalatestplus\" %% \"scalacheck-1-15\" % \"3.2.9.0\" % \"test\",\n      \"junit\" % \"junit\" % \"4.13.2\" % \"test\",\n      \"com.novocode\" % \"junit-interface\" % \"0.11\" % \"test\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.mockito\" % \"mockito-inline\" % \"4.11.0\" % \"test\",\n    ),\n    Compile / packageBin / mappings := (Compile / packageBin / mappings).value ++\n        listPythonFiles(baseDirectory.value.getParentFile / \"python\"),\n    Antlr4 / antlr4PackageName := Some(\"io.delta.sql.parser\"),\n    Antlr4 / antlr4GenListener := true,\n    Antlr4 / antlr4GenVisitor := true,\n\n    // Introduced in https://github.com/delta-io/delta/commit/d2990624d34b6b86fa5cf230e00a89b095fde254\n    //\n    // Hack to avoid errors related to missing repo-root/target/scala-2.13/classes/\n    // In multi-module sbt projects, some dependencies may attempt to locate this directory\n    // at the repository root, causing build failures if it doesn't exist.\n    createTargetClassesDir := {\n      val dir = baseDirectory.value.getParentFile / \"target\" / \"scala-2.13\" / \"classes\"\n      Files.createDirectories(dir.toPath)\n    },\n    // Generate Python version.py file with hardcoded version.\n    // This file is committed to git and auto-updated during build.\n    generatePythonVersion := {\n      val versionValue = version.value\n      // Trim -SNAPSHOT suffix to get PyPI-compatible version (like setup.py does)\n      val trimmedVersion = versionValue.split(\"-SNAPSHOT\")(0)\n      val versionFile = baseDirectory.value.getParentFile / \"python\" / \"delta\" / \"version.py\"\n      val content =\n        s\"\"\"#\n           |# Copyright (2026) The Delta Lake Project Authors.\n           |#\n           |# Licensed under the Apache License, Version 2.0 (the \"License\");\n           |# you may not use this file except in compliance with the License.\n           |# You may obtain a copy of the License at\n           |#\n           |# http://www.apache.org/licenses/LICENSE-2.0\n           |#\n           |# Unless required by applicable law or agreed to in writing, software\n           |# distributed under the License is distributed on an \"AS IS\" BASIS,\n           |# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n           |# See the License for the specific language governing permissions and\n           |# limitations under the License.\n           |#\n           |\n           |# This file is auto-generated by the build.sbt generatePythonVersion task.\n           |# Do not edit manually - edit version.sbt instead and run:\n           |#   build/sbt sparkV1/generatePythonVersion\n           |\n           |__version__ = \"$trimmedVersion\"\n           |\"\"\".stripMargin\n      IO.write(versionFile, content)\n      versionFile\n    },\n    // Hook both createTargetClassesDir and generatePythonVersion into compile task\n    Compile / compile := ((Compile / compile) dependsOn createTargetClassesDir dependsOn generatePythonVersion).value,\n    // Generate the package object to provide the version information in runtime.\n    Compile / sourceGenerators += Def.task {\n      val file = (Compile / sourceManaged).value / \"io\" / \"delta\" / \"package.scala\"\n      IO.write(file,\n        s\"\"\"package io\n           |\n           |package object delta {\n           |  val VERSION = \"${version.value}\"\n           |}\n           |\"\"\".stripMargin)\n      Seq(file)\n    },\n  )\n\n// ============================================================\n// Spark Module 2: sparkV1Filtered (v1 without DeltaLog for v2 dependency)\n// This filtered version of sparkV1 is needed because sparkV2 (spark/v2) depends on some\n// V1 classes for utilities and common functionality, but must NOT have access to DeltaLog,\n// Snapshot, OptimisticTransaction, or actions that belongs to core V1 delta libraries.\n// We should use Kernel as the Delta implementation.\n// ============================================================\nlazy val sparkV1Filtered = (project in file(\"spark-v1-filtered\"))\n  .dependsOn(sparkV1)\n  .dependsOn(storage)\n  .settings(\n    name := \"delta-spark-v1-filtered\",\n    commonSettings,\n    skipReleaseSettings, // Internal module - not published to Maven\n    exportJars := true,  // Export as JAR to avoid classpath conflicts\n\n    // No source code - just repackage sparkV1 without DeltaLog classes\n    Compile / sources := Seq.empty,\n    Test / sources := Seq.empty,\n\n    // Repackage sparkV1 jar but exclude DeltaLog and related classes\n    Compile / packageBin / mappings := {\n      val v1Mappings = (sparkV1 / Compile / packageBin / mappings).value\n\n      // Filter out DeltaLog, Snapshot, OptimisticTransaction, and actions.scala classes\n      v1Mappings.filterNot { case (file, path) =>\n        path.contains(\"org/apache/spark/sql/delta/DeltaLog\") ||\n        path.contains(\"org/apache/spark/sql/delta/Snapshot\") ||\n        path.contains(\"org/apache/spark/sql/delta/OptimisticTransaction\") ||\n        path.contains(\"org/apache/spark/sql/delta/actions/actions\")\n      }\n    },\n  )\n\n// ============================================================\n// Spark Module 3: sparkV2 (Kernel-based DSv2 connector, depends on v1-filtered)\n// ============================================================\nlazy val sparkV2 = (project in file(\"spark/v2\"))\n  .dependsOn(sparkV1Filtered)\n  .dependsOn(kernelDefaults)\n  .dependsOn(kernelUnityCatalog % \"compile->compile;test->test\")\n  .dependsOn(goldenTables % \"test\")\n  .settings(\n    name := \"delta-spark-v2\",\n    commonSettings,\n    javafmtCheckSettings,\n    skipReleaseSettings, // Internal module - not published to Maven\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n    exportJars := true,  // Export as JAR to avoid classpath conflicts\n\n    Test / javaOptions ++= Seq(\"-ea\"),\n    // make sure shaded kernel-api jar exists before compiling/testing\n    Compile / compile := (Compile / compile)\n      .dependsOn(kernelApi / Compile / packageBin).value,\n    Test / test := (Test / test)\n      .dependsOn(kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Test / packageBin).value,\n    Compile / unmanagedJars ++= Seq(\n      (kernelApi / Compile / packageBin).value\n    ),\n    libraryDependencies ++= Seq(\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"provided\",\n\n      // Test dependencies\n      \"org.junit.jupiter\" % \"junit-jupiter-api\" % \"5.11.4\" % \"test\",\n      \"org.junit.jupiter\" % \"junit-jupiter-engine\" % \"5.11.4\" % \"test\",\n      \"org.junit.jupiter\" % \"junit-jupiter-params\" % \"5.11.4\" % \"test\",\n      \"com.github.sbt.junit\" % \"jupiter-interface\" % \"0.17.0\" % \"test\",\n      // Spark test classes for Scala/Java test utilities\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"test\" classifier \"tests\",\n      // ScalaTest for test utilities (needed by Spark test classes)\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\"\n    ),\n    Test / testOptions += Tests.Argument(TestFrameworks.JUnit, \"-v\", \"-a\"),\n    TestParallelization.settings\n  )\n\n\n// ============================================================\n// Spark Module 4: delta-spark (final published module - unified v1+v2)\n// ============================================================\nlazy val spark = (project in file(\"spark-unified\"))\n  .dependsOn(sparkV1)\n  .dependsOn(sparkV2)\n  .dependsOn(storage)\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-spark\",\n    commonSettings,\n    scalaStyleSettings,\n    sparkMimaSettings,\n    releaseSettings, // Published to Maven as delta-spark.jar\n\n    // Set Test baseDirectory before sparkDependentSettings() so it uses the correct directory\n    Test / baseDirectory := (sparkV1 / baseDirectory).value,\n\n    // Test sources from spark/ directory (sparkV1's directory) AND spark-unified's own directory\n    // MUST be set BEFORE crossSparkSettings() to avoid overwriting version-specific directories\n    Test / unmanagedSourceDirectories := {\n      val sparkDir = (sparkV1 / baseDirectory).value\n      val unifiedDir = baseDirectory.value\n      Seq(\n        sparkDir / \"src\" / \"test\" / \"scala\",\n        sparkDir / \"src\" / \"test\" / \"java\",\n        unifiedDir / \"src\" / \"test\" / \"scala\",\n        unifiedDir / \"src\" / \"test\" / \"java\"\n      )\n    },\n    Test / unmanagedResourceDirectories := Seq(\n      (sparkV1 / baseDirectory).value / \"src\" / \"test\" / \"resources\",\n      baseDirectory.value / \"src\" / \"test\" / \"resources\"\n    ),\n\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n\n    // MiMa should use the generated JAR (not classDirectory) because we merge classes at package time\n    mimaCurrentClassfiles := (Compile / packageBin).value,\n\n    // Export as JAR to dependent projects (e.g., connectServer, connectClient).\n    // This prevents classpath conflicts from internal module 'classes' directories.\n    exportJars := true,\n\n    // Internal module artifact names to exclude from published POM\n    internalModuleNames := Set(\"delta-spark-v1\", \"delta-spark-v1-shaded\", \"delta-spark-v2\"),\n\n    // Merge classes from internal modules (v1, v2) into final JAR\n    // kernel modules are kept as separate JARs and listed as dependencies in POM\n    Compile / packageBin / mappings ++= {\n      val log = streams.value.log\n\n      // Collect mappings from internal modules\n      val v1Mappings = (sparkV1 / Compile / packageBin / mappings).value\n      val v2Mappings = (sparkV2 / Compile / packageBin / mappings).value\n\n      // Include Python files (from spark/ directory)\n      val pythonMappings = listPythonFiles(baseDirectory.value.getParentFile / \"python\")\n\n      // Combine all mappings\n      val allMappings = v1Mappings ++ v2Mappings ++ pythonMappings\n\n      // Detect duplicate class files\n      val classFiles = allMappings.filter(_._2.endsWith(\".class\"))\n      val duplicates = classFiles.groupBy(_._2).filter(_._2.size > 1)\n\n      if (duplicates.nonEmpty) {\n        log.error(s\"Found ${duplicates.size} duplicate class(es) in packageBin mappings:\")\n        duplicates.foreach { case (className, entries) =>\n          log.error(s\"  - $className:\")\n          entries.foreach { case (file, path) => log.error(s\"      from: $file\") }\n        }\n        sys.error(\"Duplicate classes found. This indicates overlapping code between sparkV1, sparkV2, and storage modules.\")\n      }\n\n      allMappings.distinct\n    },\n\n    // Exclude internal modules from published POM and add kernel dependencies.\n    // Kernel modules are transitive through sparkV2 (an internal module), so they\n    // are lost when sparkV2 is filtered out. We re-add them explicitly here.\n    pomPostProcess := { node =>\n      val internalModules = internalModuleNames.value\n      val ver = version.value\n      import scala.xml._\n      import scala.xml.transform._\n\n      def kernelDependencyNode(artifactId: String): Elem = {\n        <dependency>\n          <groupId>io.delta</groupId>\n          <artifactId>{artifactId}</artifactId>\n          <version>{ver}</version>\n        </dependency>\n      }\n\n      val kernelDeps = Seq(\n        kernelDependencyNode(\"delta-kernel-api\"),\n        kernelDependencyNode(\"delta-kernel-defaults\"),\n        kernelDependencyNode(\"delta-kernel-unitycatalog\")\n      )\n\n      new RuleTransformer(new RewriteRule {\n        override def transform(n: Node): Seq[Node] = n match {\n          case e: Elem if e.label == \"dependencies\" =>\n            val filtered = e.child.filter {\n              case child: Elem if child.label == \"dependency\" =>\n                val artifactId = (child \\ \"artifactId\").text\n                !internalModules.exists(module => artifactId.startsWith(module))\n              case _ => true\n            }\n            Seq(e.copy(child = filtered ++ kernelDeps))\n          case _ => Seq(n)\n        }\n      }).transform(node).head\n    },\n\n    pomIncludeRepository := { _ => false },\n\n    // Filter internal modules from project dependencies\n    // This works together with pomPostProcess to ensure internal modules\n    // (sparkV1, sparkV2, sparkV1Filtered) are not listed as dependencies in POM\n    projectDependencies := {\n      val internalModules = internalModuleNames.value\n      projectDependencies.value.filterNot(dep => internalModules.contains(dep.name))\n    },\n\n    libraryDependencies ++= Seq(\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"provided\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"provided\",\n      \"com.amazonaws\" % \"aws-java-sdk\" % \"1.12.262\" % \"provided\",\n\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"org.scalatestplus\" %% \"scalacheck-1-15\" % \"3.2.9.0\" % \"test\",\n      \"junit\" % \"junit\" % \"4.13.2\" % \"test\",\n      \"com.novocode\" % \"junit-interface\" % \"0.11\" % \"test\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.mockito\" % \"mockito-inline\" % \"4.11.0\" % \"test\",\n    ),\n\n    Test / testOptions += Tests.Argument(\"-oDF\"),\n    Test / testOptions += Tests.Argument(TestFrameworks.JUnit, \"-v\", \"-a\"),\n\n    // Don't execute in parallel since we can't have multiple Sparks in the same JVM\n    Test / parallelExecution := false,\n\n    javaOptions += \"-Xmx1024m\",\n\n    // Configurations to speed up tests and reduce memory footprint\n    Test / javaOptions ++= Seq(\n      \"-Dspark.ui.enabled=false\",\n      \"-Dspark.ui.showConsoleProgress=false\",\n      \"-Dspark.databricks.delta.snapshotPartitions=2\",\n      \"-Dspark.sql.shuffle.partitions=5\",\n      \"-Ddelta.log.cacheSize=3\",\n      \"-Dspark.databricks.delta.delta.log.cacheSize=3\",\n      \"-Dspark.sql.sources.parallelPartitionDiscovery.parallelism=5\",\n      \"-Xmx1024m\"\n    ),\n\n    // Required for testing table features see https://github.com/delta-io/delta/issues/1602\n    Test / envVars += (\"DELTA_TESTING\", \"1\"),\n\n    TestParallelization.settings,\n  )\n  .configureUnidoc(\n    generatedJavaDoc = CrossSparkVersions.getSparkVersionSpec().generateDocs,\n    generateScalaDoc = CrossSparkVersions.getSparkVersionSpec().generateDocs,\n    // spark-connect has classes with the same name as spark-core, this causes compilation issues\n    // with unidoc since it concatenates the classpaths from all modules\n    // ==> thus we exclude such sources\n    // (mostly) relevant github issue: https://github.com/sbt/sbt-unidoc/issues/77\n    classPathToSkip = \"spark-connect\"\n  )\n\nlazy val contribs = (project in file(\"contribs\"))\n  .dependsOn(spark % \"compile->compile;test->test;provided->provided\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-contribs\",\n    commonSettings,\n    scalaStyleSettings,\n    releaseSettings,\n    // Set sparkVersion directly (not sparkDependentModuleName) so that\n    // runOnlyForReleasableSparkModules discovers this module, but without adding a Spark\n    // suffix to the artifact name. delta-contribs is only published as delta-contribs_2.13.\n    sparkVersion := CrossSparkVersions.getSparkVersion(),\n    Compile / packageBin / mappings := (Compile / packageBin / mappings).value ++\n      listPythonFiles(baseDirectory.value.getParentFile / \"python\"),\n\n    Test / testOptions += Tests.Argument(\"-oDF\"),\n    Test / testOptions += Tests.Argument(TestFrameworks.JUnit, \"-v\", \"-a\"),\n\n    // Don't execute in parallel since we can't have multiple Sparks in the same JVM\n    Test / parallelExecution := false,\n\n    javaOptions += \"-Xmx1024m\",\n\n    // Configurations to speed up tests and reduce memory footprint\n    Test / javaOptions ++= Seq(\n      \"-Dspark.ui.enabled=false\",\n      \"-Dspark.ui.showConsoleProgress=false\",\n      \"-Dspark.databricks.delta.snapshotPartitions=2\",\n      \"-Dspark.sql.shuffle.partitions=5\",\n      \"-Ddelta.log.cacheSize=3\",\n      \"-Dspark.databricks.delta.delta.log.cacheSize=3\",\n      \"-Dspark.sql.sources.parallelPartitionDiscovery.parallelism=5\",\n      \"-Xmx1024m\"\n    ),\n\n    // Introduced in https://github.com/delta-io/delta/commit/d2990624d34b6b86fa5cf230e00a89b095fde254\n    //\n    // Hack to avoid errors related to missing repo-root/target/scala-2.13/classes/\n    // In multi-module sbt projects, some dependencies may attempt to locate this directory\n    // at the repository root, causing build failures if it doesn't exist.\n    createTargetClassesDir := {\n      val dir = baseDirectory.value.getParentFile / \"target\" / \"scala-2.13\" / \"classes\"\n      Files.createDirectories(dir.toPath)\n    },\n    Compile / compile := ((Compile / compile) dependsOn createTargetClassesDir).value,\n    TestParallelization.settings\n  ).configureUnidoc()\n\n\nval unityCatalogVersion = \"0.4.0\"\nval sparkUnityCatalogJacksonVersion = \"2.15.4\" // We are using Spark 4.0's Jackson version 2.15.x, to override Unity Catalog 0.3.0's version 2.18.x\n\nlazy val sparkUnityCatalog = (project in file(\"spark/unitycatalog\"))\n  .dependsOn(spark % \"compile->compile;test->test;provided->provided\")\n  .disablePlugins(ScalafmtPlugin)\n  .settings(\n    name := \"delta-spark-unitycatalog\",\n    commonSettings,\n    skipReleaseSettings,\n    javafmtCheckSettings(),\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n\n    // This is a test-only module - no production sources\n    Compile / sources := Seq.empty,\n\n    // Ensure Java sources are picked up\n    Test / unmanagedSourceDirectories += baseDirectory.value / \"src\" / \"test\" / \"java\",\n\n    Test / javaOptions ++= Seq(\"-ea\"),\n\n    // Don't execute in parallel since we can't have multiple Sparks in the same JVM\n    Test / parallelExecution := false,\n\n    // Force ALL Jackson dependencies to match Spark's Jackson version\n    // This overrides Jackson from Unity Catalog's transitive dependencies (e.g., Armeria)\n    dependencyOverrides ++= Seq(\n      \"com.fasterxml.jackson.core\" % \"jackson-core\" % sparkUnityCatalogJacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-annotations\" % sparkUnityCatalogJacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % sparkUnityCatalogJacksonVersion,\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % sparkUnityCatalogJacksonVersion,\n      \"com.fasterxml.jackson.dataformat\" % \"jackson-dataformat-yaml\" % sparkUnityCatalogJacksonVersion,\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jsr310\" % sparkUnityCatalogJacksonVersion,\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % sparkUnityCatalogJacksonVersion\n    ),\n\n    libraryDependencies ++= Seq(\n      \"org.assertj\" % \"assertj-core\" % \"3.26.3\" % \"test\",\n      // JUnit 5 test dependencies\n      \"org.junit.jupiter\" % \"junit-jupiter-api\" % \"5.11.4\" % \"test\",\n      \"org.junit.jupiter\" % \"junit-jupiter-engine\" % \"5.11.4\" % \"test\",\n      \"org.junit.jupiter\" % \"junit-jupiter-params\" % \"5.11.4\" % \"test\",\n      \"com.github.sbt.junit\" % \"jupiter-interface\" % \"0.17.0\" % \"test\",\n      // Lombok for generating boilerplate code\n      \"org.projectlombok\" % \"lombok\" % \"1.18.34\" % \"test\",\n\n      // Unity Catalog dependencies - exclude Jackson to use Spark's Jackson 2.15.x\n      \"io.unitycatalog\" %% \"unitycatalog-spark\" % unityCatalogVersion % \"test\" excludeAll(\n        ExclusionRule(organization = \"com.fasterxml.jackson.core\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.module\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.datatype\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.dataformat\")\n      ),\n      \"io.unitycatalog\" % \"unitycatalog-server\" % unityCatalogVersion % \"test\" excludeAll(\n        ExclusionRule(organization = \"com.fasterxml.jackson.core\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.module\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.datatype\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.dataformat\")\n      ),\n\n      // Spark test dependencies\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"test\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"test\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"test\",\n    ),\n\n    // Conditionally add hadoop-aws dependency only when UC_REMOTE=true\n    // Please see: https://github.com/delta-io/delta/issues/5624#issuecomment-3673383736\n    // Once we release the relocated unitycatalog-server, we can remove this.\n    libraryDependencies ++= {\n      if (sys.env.get(\"UC_REMOTE\").contains(\"true\")) {\n        Seq(\n          \"org.apache.hadoop\" % \"hadoop-aws\" % hadoopVersion % \"test\",\n          \"org.apache.hadoop\" % \"hadoop-common\" % hadoopVersion % \"test\",\n          \"org.apache.hadoop\" % \"hadoop-client-api\" % hadoopVersion % \"test\",\n          \"org.apache.hadoop\" % \"hadoop-client-runtime\" % hadoopVersion % \"test\"\n        )\n      } else {\n        Seq.empty\n      }\n    },\n\n    Test / testOptions += Tests.Argument(\"-oDF\"),\n    Test / testOptions += Tests.Argument(TestFrameworks.JUnit, \"-v\", \"-a\")\n  )\n\nlazy val sharing = (project in file(\"sharing\"))\n  .dependsOn(spark % \"compile->compile;test->test;provided->provided\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings(\n    name := \"delta-sharing-spark\",\n    commonSettings,\n    scalaStyleSettings,\n    releaseSettings,\n    CrossSparkVersions.sparkDependentSettings(sparkVersion),\n    Test / javaOptions ++= Seq(\"-ea\"),\n    libraryDependencies ++= Seq(\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"provided\",\n\n      \"io.delta\" %% \"delta-sharing-client\" % \"1.3.10\",\n\n      // Test deps\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"org.scalatestplus\" %% \"scalacheck-1-15\" % \"3.2.9.0\" % \"test\",\n      \"junit\" % \"junit\" % \"4.13.2\" % \"test\",\n      \"com.novocode\" % \"junit-interface\" % \"0.11\" % \"test\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersion.value % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersion.value % \"test\" classifier \"tests\",\n    ),\n    TestParallelization.settings\n  ).configureUnidoc()\n\nlazy val kernelApi = (project in file(\"kernel/kernel-api\"))\n  .enablePlugins(ScalafmtPlugin)\n  .settings(\n    name := \"delta-kernel-api\",\n    commonSettings,\n    scalaStyleSettings,\n    javaOnlyReleaseSettings,\n    javafmtCheckSettings,\n    scalafmtCheckSettings,\n\n    // Use unique classDirectory name to avoid conflicts in connectClient test setup\n    // This allows connectClient to create symlinks without FileAlreadyExistsException\n    Compile / classDirectory := target.value / \"scala-2.13\" / \"kernel-api-classes\",\n\n    Test / javaOptions ++= Seq(\"-ea\"),\n\n    // Also publish a test-jar (classifier = \"tests\") so consumers (e.g. kernelDefault)\n    // can depend on test utilities via a published artifact instead of depending on raw class directories.\n    Test / publishArtifact := true,\n    Test / packageBin / artifactClassifier := Some(\"tests\"),\n    libraryDependencies ++= Seq(\n      \"org.roaringbitmap\" % \"RoaringBitmap\" % \"0.9.25\",\n      \"org.slf4j\" % \"slf4j-api\" % \"1.7.36\",\n\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % \"2.13.5\",\n      \"com.fasterxml.jackson.core\" % \"jackson-core\" % \"2.13.5\",\n      \"com.fasterxml.jackson.core\" % \"jackson-annotations\" % \"2.13.5\",\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % \"2.13.5\",\n\n      // JSR-305 annotations for @Nullable\n      \"com.google.code.findbugs\" % \"jsr305\" % \"3.0.2\",\n\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"junit\" % \"junit\" % \"4.13.2\" % \"test\",\n      \"com.novocode\" % \"junit-interface\" % \"0.11\" % \"test\",\n      \"org.apache.logging.log4j\" % \"log4j-slf4j-impl\" % \"2.25.3\" % \"test\",\n      \"org.apache.logging.log4j\" % \"log4j-core\" % \"2.25.3\" % \"test\",\n      \"org.assertj\" % \"assertj-core\" % \"3.26.3\" % \"test\",\n      // JMH dependencies allow writing micro-benchmarks for testing performance of components.\n      // JMH has framework to define benchmarks and takes care of many common functionalities\n      // such as warm runs, cold runs, defining benchmark parameter variables etc.\n      \"org.openjdk.jmh\" % \"jmh-core\" % \"1.37\" % \"test\",\n      \"org.openjdk.jmh\" % \"jmh-generator-annprocess\" % \"1.37\" % \"test\"\n    ),\n    // Shade jackson libraries so that connector developers don't have to worry\n    // about jackson version conflicts.\n    Compile / packageBin := assembly.value,\n    assembly / assemblyJarName := s\"${name.value}-${version.value}.jar\",\n    assembly / logLevel := Level.Info,\n    assembly / test := {},\n    assembly / assemblyExcludedJars := {\n      val cp = (assembly / fullClasspath).value\n      val allowedPrefixes = Set(\"META_INF\", \"io\", \"jackson\")\n      cp.filter { f =>\n        !allowedPrefixes.exists(prefix => f.data.getName.startsWith(prefix))\n      }\n    },\n     assembly / assemblyShadeRules := Seq(\n      ShadeRule.rename(\"com.fasterxml.jackson.**\" -> \"io.delta.kernel.shaded.com.fasterxml.jackson.@1\").inAll\n    ),\n    assembly / assemblyMergeStrategy := {\n      // Discard `module-info.class` to fix the `different file contents found` error.\n      // TODO Upgrade SBT to 1.5 which will do this automatically\n      case \"module-info.class\" => MergeStrategy.discard\n      case PathList(\"META-INF\", \"services\", xs @ _*) => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (assembly / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n    // Generate the package object to provide the version information in runtime.\n    Compile / sourceGenerators += Def.task {\n      val file = (Compile / sourceManaged).value / \"io\" / \"delta\" / \"kernel\" / \"Meta.java\"\n      IO.write(file,\n        s\"\"\"/*\n           | * Copyright (2024) The Delta Lake Project Authors.\n           | *\n           | * Licensed under the Apache License, Version 2.0 (the \"License\");\n           | * you may not use this file except in compliance with the License.\n           | * You may obtain a copy of the License at\n           | *\n           | * http://www.apache.org/licenses/LICENSE-2.0\n           | *\n           | * Unless required by applicable law or agreed to in writing, software\n           | * distributed under the License is distributed on an \"AS IS\" BASIS,\n           | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n           | * See the License for the specific language governing permissions and\n           | * limitations under the License.\n           | */\n           |package io.delta.kernel;\n           |\n           |public final class Meta {\n           |    public static final String KERNEL_VERSION = \"${version.value}\";\n           |}\n           |\"\"\".stripMargin)\n      Seq(file)\n    },\n    MultiShardMultiJVMTestParallelization.settings,\n    javaCheckstyleSettings(\"dev/kernel-checkstyle.xml\"),\n    // Unidoc settings\n    unidocSourceFilePatterns := Seq(SourceFilePattern(\"io/delta/kernel/\")),\n  ).configureUnidoc(docTitle = \"Delta Kernel\")\n\nlazy val kernelDefaults = (project in file(\"kernel/kernel-defaults\"))\n  .enablePlugins(ScalafmtPlugin)\n  .dependsOn(storage)\n  .dependsOn(storage % \"test->test\") // Required for InMemoryCommitCoordinator for tests\n  .dependsOn(goldenTables % \"test\")\n  .settings(\n    name := \"delta-kernel-defaults\",\n    commonSettings,\n    scalaStyleSettings,\n    javaOnlyReleaseSettings,\n    javafmtCheckSettings,\n    scalafmtCheckSettings,\n\n    // Use unique classDirectory name to avoid conflicts in connectClient test setup\n    // This allows connectClient to create symlinks without FileAlreadyExistsException\n    Compile / classDirectory := target.value / \"scala-2.13\" / \"kernel-defaults-classes\",\n\n    Test / javaOptions ++= Seq(\"-ea\"),\n    // This allows generating tables with unsupported test table features in delta-spark\n    Test / envVars += (\"DELTA_TESTING\", \"1\"),\n\n    // Put the shaded kernel-api JAR on the classpath (compile & test)\n    Compile / unmanagedJars += (kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Compile / packageBin).value,\n\n    // Make sure the shaded JAR is produced before we compile/run tests\n    Compile / compile := (Compile / compile).dependsOn(kernelApi / Compile / packageBin).value,\n    Test / test       := (Test    / test).dependsOn(kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Test / packageBin).value,\n\n    libraryDependencies ++= Seq(\n      \"org.assertj\" % \"assertj-core\" % \"3.26.3\" % Test,\n      \"org.apache.hadoop\" % \"hadoop-client-runtime\" % hadoopVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % \"2.13.5\",\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % \"2.13.5\",\n      \"org.apache.parquet\" % \"parquet-hadoop\" % \"1.12.3\",\n\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"junit\" % \"junit\" % \"4.13.2\" % \"test\",\n      \"commons-io\" % \"commons-io\" % \"2.8.0\" % \"test\",\n      \"com.novocode\" % \"junit-interface\" % \"0.11\" % \"test\",\n      \"org.apache.logging.log4j\" % \"log4j-slf4j-impl\" % \"2.25.3\" % \"test\",\n      \"org.apache.logging.log4j\" % \"log4j-core\" % \"2.25.3\" % \"test\",\n      // JMH dependencies allow writing micro-benchmarks for testing performance of components.\n      // JMH has framework to define benchmarks and takes care of many common functionalities\n      // such as warm runs, cold runs, defining benchmark parameter variables etc.\n      \"org.openjdk.jmh\" % \"jmh-core\" % \"1.37\" % \"test\",\n      \"org.openjdk.jmh\" % \"jmh-generator-annprocess\" % \"1.37\" % \"test\",\n\n      // The delta-spark and spark dependencies are mainly used for catalog-based table creation.\n      // Instead of using the latest snapshot, those are fine to use the released 4.0.0.\n      \"io.delta\" %% \"delta-spark\" % \"4.0.0\" % \"test\",\n      \"org.apache.spark\" %% \"spark-hive\" % sparkVersionForKernelTest % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % sparkVersionForKernelTest % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % sparkVersionForKernelTest % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % sparkVersionForKernelTest % \"test\" classifier \"tests\",\n    ),\n    MultiShardMultiJVMTestParallelization.settings,\n    javaCheckstyleSettings(\"dev/kernel-checkstyle.xml\"),\n      // Unidoc settings\n    unidocSourceFilePatterns += SourceFilePattern(\"io/delta/kernel/\"),\n  ).configureUnidoc(docTitle = \"Delta Kernel Defaults\")\n\nlazy val kernelBenchmarks = (project in file(\"kernel/kernel-benchmarks\"))\n  .enablePlugins(ScalafmtPlugin)\n  .dependsOn(kernelDefaults % \"test->test\")\n  .dependsOn(kernelApi % \"test->test\")\n  .dependsOn(storage % \"test->test\")\n  .dependsOn(kernelUnityCatalog % \"test->test\")\n  .settings(\n    name := \"delta-kernel-benchmarks\",\n    commonSettings,\n    skipReleaseSettings,\n    exportJars := false,\n    javafmtCheckSettings,\n    scalafmtCheckSettings,\n    \n    libraryDependencies ++= Seq(\n      \"org.openjdk.jmh\" % \"jmh-core\" % \"1.37\" % \"test\",\n      \"org.openjdk.jmh\" % \"jmh-generator-annprocess\" % \"1.37\" % \"test\",\n    ),\n  )\n\nlazy val kernelUnityCatalog = (project in file(\"kernel/unitycatalog\"))\n  .enablePlugins(ScalafmtPlugin)\n  .dependsOn(kernelDefaults % \"test->test\")\n  .dependsOn(storage)\n  .settings (\n    name := \"delta-kernel-unitycatalog\",\n    commonSettings,\n    javaOnlyReleaseSettings,\n    javafmtCheckSettings,\n    javaCheckstyleSettings(\"dev/kernel-checkstyle.xml\"),\n    scalaStyleSettings,\n    scalafmtCheckSettings,\n\n    // Put the shaded kernel-api JAR on the classpath (compile & test)\n    Compile / unmanagedJars += (kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Compile / packageBin).value,\n\n    // Make sure the shaded JAR is produced before we compile/run tests\n    Compile / compile := (Compile / compile).dependsOn(kernelApi / Compile / packageBin).value,\n    Test / test       := (Test    / test).dependsOn(kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Test / packageBin).value,\n\n    libraryDependencies ++= Seq(\n      \"org.apache.hadoop\" % \"hadoop-common\" % hadoopVersion % \"provided\",\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"org.apache.logging.log4j\" % \"log4j-slf4j-impl\" % \"2.25.3\" % \"test\",\n      \"org.apache.logging.log4j\" % \"log4j-core\" % \"2.25.3\" % \"test\",\n    ),\n    unidocSourceFilePatterns += SourceFilePattern(\"src/main/java/io/delta/unity/\"),\n  ).configureUnidoc()\n\n// TODO javastyle tests\n// TODO unidoc\n// TODO(scott): figure out a better way to include tests in this project\nlazy val storage = (project in file(\"storage\"))\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-storage\",\n    commonSettings,\n    exportJars := true,\n    javaOnlyReleaseSettings,\n    libraryDependencies ++= Seq(\n      // User can provide any 2.x or 3.x version. We don't use any new fancy APIs. Watch out for\n      // versions with known vulnerabilities.\n      \"org.apache.hadoop\" % \"hadoop-common\" % hadoopVersion % \"provided\",\n\n      // Note that the org.apache.hadoop.fs.s3a.Listing::createFileStatusListingIterator 3.3.1 API\n      // is not compatible with 3.3.2.\n      \"org.apache.hadoop\" % \"hadoop-aws\" % hadoopVersion % \"provided\",\n      \"io.unitycatalog\" % \"unitycatalog-client\" % unityCatalogVersion excludeAll(\n        ExclusionRule(organization = \"org.openapitools\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.core\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.module\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.datatype\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.dataformat\")\n      ),\n\n      // Test Deps\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      // Jackson datatype module needed for UC SDK tests (excluded from main compile scope)\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jsr310\" % \"2.15.4\" % \"test\",\n    ),\n\n    // Unidoc settings\n    unidocSourceFilePatterns += SourceFilePattern(\"/LogStore.java\", \"/CloseableIterator.java\"),\n    TestParallelization.settings\n  ).configureUnidoc()\n\nlazy val storageS3DynamoDB = (project in file(\"storage-s3-dynamodb\"))\n  .dependsOn(storage % \"compile->compile;test->test;provided->provided\")\n  .dependsOn(spark % \"test->test\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-storage-s3-dynamodb\",\n    commonSettings,\n    javaOnlyReleaseSettings,\n\n    // uncomment only when testing FailingS3DynamoDBLogStore. this will include test sources in\n    // a separate test jar.\n    // Test / publishArtifact := true,\n\n    libraryDependencies ++= Seq(\n      \"com.amazonaws\" % \"aws-java-sdk\" % \"1.12.262\" % \"provided\",\n\n      // Test Deps\n      \"org.apache.hadoop\" % \"hadoop-aws\" % hadoopVersion % \"test\", // RemoteFileChangedException\n    ),\n    TestParallelization.settings\n  ).configureUnidoc()\n\nval icebergSparkRuntimeArtifactName = {\n val currentSparkVersion = CrossSparkVersions.getSparkVersion()\n val (expMaj, expMin, _) = getMajorMinorPatch(currentSparkVersion)\n s\"iceberg-spark-runtime-$expMaj.$expMin\"\n}\n\nlazy val testDeltaIcebergJar = (project in file(\"testDeltaIcebergJar\"))\n  // delta-iceberg depends on delta-spark! So, we need to include it during our test.\n  .dependsOn(spark % \"test\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings(\n    name := \"test-delta-iceberg-jar\",\n    commonSettings,\n    skipReleaseSettings,\n    exportJars := true,\n    Compile / unmanagedJars += (iceberg / assembly).value,\n    libraryDependencies ++= Seq(\n      \"org.apache.hadoop\" % \"hadoop-client\" % hadoopVersion,\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"org.apache.spark\" %% \"spark-core\" % defaultSparkVersion % \"test\"\n    )\n  )\n\nval deltaIcebergSparkIncludePrefixes = Seq(\n  // We want everything from this package\n  \"org/apache/spark/sql/delta/icebergShaded\",\n  // Server-side planning support\n  \"org/apache/spark/sql/delta/serverSidePlanning\",\n\n  // We only want the files in this project from this package. e.g. we want to exclude\n  // org/apache/spark/sql/delta/commands/convert/ConvertTargetFile.class (from delta-spark project).\n  \"org/apache/spark/sql/delta/commands/convert/IcebergFileManifest\",\n  \"org/apache/spark/sql/delta/commands/convert/IcebergSchemaUtils\",\n  \"org/apache/spark/sql/delta/commands/convert/IcebergTable\"\n)\n\n// Build using: build/sbt clean icebergShaded/compile iceberg/compile\n// It will fail the first time, just re-run it.\n// scalastyle:off println\nlazy val iceberg = (project in file(\"iceberg\"))\n  .dependsOn(spark % \"compile->compile;test->test;provided->provided\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-iceberg\",\n    commonSettings,\n    scalaStyleSettings,\n    releaseSettings,\n    // Set sparkVersion directly (not sparkDependentModuleName) so that\n    // runOnlyForReleasableSparkModules discovers this module, but without adding a Spark\n    // suffix to the artifact name. delta-iceberg is only published as delta-iceberg_2.13.\n    sparkVersion := CrossSparkVersions.getSparkVersion(),\n    libraryDependencies ++= {\n      if (supportIceberg) {\n        Seq(\n          // Fix Iceberg's legacy java.lang.NoClassDefFoundError: scala/jdk/CollectionConverters$ error\n          // due to legacy scala.\n          \"org.scala-lang.modules\" %% \"scala-collection-compat\" % \"2.1.1\",\n          \"com.github.ben-manes.caffeine\" % \"caffeine\" % \"2.9.3\",\n          \"com.jolbox\" % \"bonecp\" % \"0.8.0.RELEASE\" % \"test\",\n          \"org.eclipse.jetty\" % \"jetty-server\" % \"11.0.26\" % \"test\",\n          \"org.eclipse.jetty\" % \"jetty-servlet\" % \"11.0.26\" % \"test\",\n          \"org.xerial\" % \"sqlite-jdbc\" % \"3.45.0.0\" % \"test\",\n          \"org.apache.httpcomponents.core5\" % \"httpcore5\" % \"5.2.4\" % \"test\",\n          \"org.apache.httpcomponents.client5\" % \"httpclient5\" % \"5.3.1\" % \"test\",\n          \"org.apache.iceberg\" %% icebergSparkRuntimeArtifactName % \"1.10.0\" % \"provided\",\n          // For FixedGcsAccessTokenProvider (GCS server-side planning credentials)\n          \"com.google.cloud.bigdataoss\" % \"util-hadoop\" % \"hadoop3-2.2.26\" % \"provided\"\n        )\n      } else {\n        Seq.empty\n      }\n    },\n    // Skip compilation and publishing when supportIceberg is false\n    Compile / skip := !supportIceberg,\n    Test / skip := !supportIceberg,\n    publish / skip := !supportIceberg,\n    publishLocal / skip := !supportIceberg,\n    publishM2 / skip := !supportIceberg,\n    Compile / unmanagedJars += (icebergShaded / assembly).value,\n    // Generate the assembly JAR as the package JAR\n    Compile / packageBin := assembly.value,\n    Compile / scalacOptions += \"-nowarn\",\n    Test / unmanagedJars += (icebergTestsShaded / assembly).value,\n    Test / scalacOptions += \"-nowarn\",\n    assembly / assemblyJarName := {\n      s\"${moduleName.value}_${scalaBinaryVersion.value}-${version.value}.jar\"\n    },\n    assembly / logLevel := Level.Info,\n    assembly / test := {},\n    assembly / assemblyExcludedJars := {\n      // Note: the input here is only `libraryDependencies` jars, not `.dependsOn(_)` jars.\n      val allowedJars = Seq(\n        s\"iceberg-shaded_${scalaBinaryVersion.value}-${version.value}.jar\",\n        s\"scala-library-${scala213}.jar\",\n        s\"scala-collection-compat_${scalaBinaryVersion.value}-2.1.1.jar\",\n        \"caffeine-2.9.3.jar\",\n        // Note: We are excluding\n        // - antlr4-runtime-4.9.3.jar\n        // - checker-qual-3.19.0.jar\n        // - error_prone_annotations-2.10.0.jar\n      )\n      val cp = (assembly / fullClasspath).value\n\n      // Return `true` when we want the jar `f` to be excluded from the assembly jar\n      cp.filter { f =>\n        val doExclude = !allowedJars.contains(f.data.getName)\n        println(s\"Excluding jar: ${f.data.getName} ? $doExclude\")\n        doExclude\n      }\n    },\n    assembly / assemblyMergeStrategy := {\n      // Project iceberg `dependsOn` spark and accidentally brings in it, along with its\n      // compile-time dependencies (like delta-storage). We want these excluded from the\n      // delta-iceberg jar.\n      case PathList(\"io\", \"delta\", xs @ _*) =>\n        // - delta-storage will bring in classes: io/delta/storage\n        // - delta-spark will bring in classes: io/delta/exceptions/, io/delta/implicits,\n        //   io/delta/package, io/delta/sql, io/delta/tables,\n        MergeStrategy.discard\n      case PathList(\"com\", \"databricks\", xs @ _*) =>\n        // delta-spark will bring in com/databricks/spark/util\n        MergeStrategy.discard\n      case PathList(\"org\", \"apache\", \"spark\", xs @ _*)\n        if !deltaIcebergSparkIncludePrefixes.exists { prefix =>\n          s\"org/apache/spark/${xs.mkString(\"/\")}\".startsWith(prefix) } =>\n        MergeStrategy.discard\n      case PathList(\"scoverage\", xs @ _*) =>\n        MergeStrategy.discard\n      case x =>\n        (assembly / assemblyMergeStrategy).value(x)\n    },\n    assemblyPackageScala / assembleArtifact := false\n  )\n// scalastyle:on println\n\nval icebergShadedVersion = \"1.10.1\"\nlazy val icebergShaded = (project in file(\"icebergShaded\"))\n  .dependsOn(spark % \"provided\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"iceberg-shaded\",\n    commonSettings,\n    skipReleaseSettings,\n    // must exclude all dependencies from Iceberg that delta-spark includes\n    libraryDependencies ++= Seq(\n      // Fix Iceberg's legacy java.lang.NoClassDefFoundError: scala/jdk/CollectionConverters$ error\n      // due to legacy scala.\n      \"org.scala-lang.modules\" %% \"scala-collection-compat\" % \"2.1.1\" % \"provided\",\n      \"org.apache.iceberg\" % \"iceberg-core\" % icebergShadedVersion excludeAll (\n        icebergExclusionRules: _*\n      ),\n      \"org.apache.iceberg\" % \"iceberg-hive-metastore\" % icebergShadedVersion excludeAll (\n        icebergExclusionRules: _*\n      ),\n      // the hadoop client and hive metastore versions come from this file in the\n      // iceberg repo of icebergShadedVersion: iceberg/gradle/libs.versions.toml\n      \"org.apache.hadoop\" % \"hadoop-client\" % \"2.7.3\" % \"provided\" excludeAll (\n        hadoopClientExclusionRules: _*\n      ),\n      \"org.apache.hive\" % \"hive-metastore\" % \"2.3.8\" % \"provided\" excludeAll (\n        hiveMetastoreExclusionRules: _*\n      )\n    ),\n    // Generated shaded Iceberg JARs\n    Compile / packageBin := assembly.value,\n    assembly / assemblyJarName := s\"${name.value}_${scalaBinaryVersion.value}-${version.value}.jar\",\n    assembly / logLevel := Level.Info,\n    assembly / test := {},\n    assembly / assemblyShadeRules := Seq(\n      ShadeRule.rename(\"org.apache.iceberg.**\" -> \"shadedForDelta.@0\").inAll\n    ),\n    assembly / assemblyExcludedJars := {\n      val cp = (assembly / fullClasspath).value\n      cp.filter { jar =>\n        val doExclude = jar.data.getName.contains(\"jackson-annotations\") ||\n          jar.data.getName.contains(\"RoaringBitmap\")\n        doExclude\n      }\n    },\n    // all following clases have Delta customized implementation under icebergShaded/src and thus\n    // require them to be 'first' to replace the class from iceberg jar\n    assembly / assemblyMergeStrategy := updateMergeStrategy((assembly / assemblyMergeStrategy).value),\n    assemblyPackageScala / assembleArtifact := false,\n  )\n\nlazy val icebergTestsShaded = (project in file(\"icebergTestsShaded\"))\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"iceberg-tests-shaded\",\n    commonSettings,\n    skipReleaseSettings,\n    // must exclude all dependencies from Iceberg that delta-spark includes\n    libraryDependencies ++= Seq(\n      \"org.apache.iceberg\" % \"iceberg-core\" % icebergShadedVersion classifier \"tests\" excludeAll (\n        icebergExclusionRules: _*\n      ),\n    ),\n    // Generated shaded Iceberg JARs\n    Compile / packageBin := assembly.value,\n    assembly / assemblyJarName := s\"${name.value}_${scalaBinaryVersion.value}-${version.value}.jar\",\n    assembly / logLevel := Level.Info,\n    assembly / test := {},\n    assembly / assemblyShadeRules := Seq(\n      ShadeRule.rename(\"org.apache.iceberg.**\" -> \"shadedForDelta.@0\").inAll\n    ),\n    assembly / assemblyExcludedJars := {\n      val cp = (fullClasspath in assembly).value\n      cp.filter { jar =>\n        val doExclude = jar.data.getName.contains(\"jackson-annotations\") ||\n          jar.data.getName.contains(\"RoaringBitmap\")\n        doExclude\n      }\n    },\n    assembly / assemblyMergeStrategy := updateMergeStrategy((assembly / assemblyMergeStrategy).value),\n    assemblyPackageScala / assembleArtifact := false,\n  )\n\n\nlazy val hudi = (project in file(\"hudi\"))\n  .dependsOn(spark % \"compile->compile;test->test;provided->provided\")\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings (\n    name := \"delta-hudi\",\n    commonSettings,\n    scalaStyleSettings,\n    releaseSettings,\n    // Set sparkVersion directly (not sparkDependentModuleName) so that\n    // runOnlyForReleasableSparkModules discovers this module, but without adding a Spark\n    // suffix to the artifact name. delta-hudi is only published as delta-hudi_2.13.\n    sparkVersion := CrossSparkVersions.getSparkVersion(),\n    libraryDependencies ++= {\n      if (supportHudi) {\n        Seq(\n          \"org.apache.hudi\" % \"hudi-java-client\" % \"0.15.0\" % \"compile\" excludeAll(\n            ExclusionRule(organization = \"org.apache.hadoop\"),\n            ExclusionRule(organization = \"org.apache.zookeeper\"),\n          ),\n          \"org.apache.spark\" %% \"spark-avro\" % sparkVersion.value % \"test\" excludeAll ExclusionRule(organization = \"org.apache.hadoop\"),\n          \"org.apache.parquet\" % \"parquet-avro\" % \"1.12.3\" % \"compile\"\n        )\n      } else {\n        Seq.empty\n      }\n    },\n    // Skip compilation and publishing when supportHudi is false\n    Compile / skip := !supportHudi,\n    Test / skip := !supportHudi,\n    publish / skip := !supportHudi,\n    publishLocal / skip := !supportHudi,\n    publishM2 / skip := !supportHudi,\n    assembly / assemblyJarName := s\"${name.value}-assembly_${scalaBinaryVersion.value}-${version.value}.jar\",\n    assembly / logLevel := Level.Info,\n    assembly / test := {},\n    assembly / assemblyMergeStrategy := {\n      // Project hudi `dependsOn` spark and accidentally brings in it, along with its\n      // compile-time dependencies (like delta-storage). We want these excluded from the\n      // delta-hudi jar.\n      case PathList(\"io\", \"delta\", xs @ _*) =>\n        // - delta-storage will bring in classes: io/delta/storage\n        // - delta-spark will bring in classes: io/delta/exceptions/, io/delta/implicits,\n        //   io/delta/package, io/delta/sql, io/delta/tables,\n        MergeStrategy.discard\n      case PathList(\"com\", \"databricks\", xs @ _*) =>\n        // delta-spark will bring in com/databricks/spark/util\n        MergeStrategy.discard\n      case PathList(\"org\", \"apache\", \"spark\", \"sql\", \"delta\", \"hudi\", xs @ _*) =>\n        MergeStrategy.first\n      case PathList(\"org\", \"apache\", \"spark\", xs @ _*) =>\n        MergeStrategy.discard\n      // Discard `module-info.class` to fix the `different file contents found` error.\n      // TODO Upgrade SBT to 1.5 which will do this automatically\n      case \"module-info.class\" => MergeStrategy.discard\n      // Discard unused `parquet.thrift` so that we don't conflict the file used by the user\n      case \"parquet.thrift\" => MergeStrategy.discard\n      // Hudi metadata writer requires this service file to be present on the classpath\n      case \"META-INF/services/org.apache.hadoop.hbase.regionserver.MetricsRegionServerSourceFactory\" => MergeStrategy.first\n      // Discard the jackson service configs that we don't need. These files are not shaded so\n      // adding them may conflict with other jackson version used by the user.\n      case PathList(\"META-INF\", \"services\", xs @ _*) => MergeStrategy.discard\n      case x =>\n        MergeStrategy.first\n    },\n    // Make the 'compile' invoke the 'assembly' task to generate the uber jar.\n    Compile / packageBin := assembly.value,\n    TestParallelization.settings\n  )\n\nlazy val flink = (project in file(\"flink\"))\n//  .dependsOn(kernelApi)\n  .dependsOn(kernelDefaults)\n  .dependsOn(kernelUnityCatalog)\n  .settings(\n    name := \"delta-flink\",\n    commonSettings,\n    skipReleaseSettings,\n    javafmtCheckSettings(),\n    publishArtifact := scalaBinaryVersion.value == \"2.12\", // only publish once\n    autoScalaLibrary := false, // exclude scala-library from dependencies\n    assembly / assemblyJarName := s\"delta-flink-$flinkVersion-${version.value}.jar\",\n    assembly / assemblyMergeStrategy := {\n      // Discard module-info.class files from Java 9+ modules and multi-release JARs\n      case \"module-info.class\" => MergeStrategy.discard\n      case \"parquet.thrift\" => MergeStrategy.discard\n      case PathList(\"META-INF\", \"versions\", _, \"module-info.class\") => MergeStrategy.discard\n      case PathList(\"mozilla\", \"public-suffix-list.txt\") => MergeStrategy.discard\n      case x => MergeStrategy.first\n    },\n    assembly / assemblyExcludedJars := {\n      val cp = (assembly / fullClasspath).value\n      cp.filter { entry =>\n        entry.data.getName.startsWith(\"bundle-\") &&\n          entry.data.getName.endsWith(\".jar\")\n      }\n    },\n    Compile / unmanagedJars += (kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Compile / packageBin).value,\n\n    // Make sure the shaded JAR is produced before we compile/run tests\n    Compile / compile := (Compile / compile).dependsOn(kernelApi / Compile / packageBin).value,\n    Test / test       := (Test    / test).dependsOn(kernelApi / Compile / packageBin).value,\n    Test / unmanagedJars += (kernelApi / Test / packageBin).value,\n\n    Test / publishArtifact := false,\n    Test / javaOptions ++= Seq(\n      \"--add-opens=java.base/java.util=ALL-UNNAMED\" // for Flink with Java 17.\n    ),\n    crossPaths := false,\n    libraryDependencies ++= Seq(\n      \"org.apache.flink\" % \"flink-core\" % flinkVersion % \"provided\",\n      \"org.apache.flink\" % \"flink-table-common\" % flinkVersion % \"provided\",\n      \"org.apache.flink\" % \"flink-streaming-java\" % flinkVersion % \"provided\",\n      \"org.apache.flink\" % \"flink-table-api-java-bridge\" % flinkVersion % \"provided\",\n      \"io.unitycatalog\" % \"unitycatalog-client\" % \"0.3.1\",\n      \"org.apache.httpcomponents\" % \"httpclient\" % \"4.5.14\" % Runtime,\n      \"dev.failsafe\" % \"failsafe\" % \"3.2.0\",\n      \"com.github.ben-manes.caffeine\" % \"caffeine\" % \"3.1.8\",\n      \"org.apache.hadoop\" % \"hadoop-aws\" % hadoopVersion,\n\n      // Test dependencies\n      \"org.junit.jupiter\" % \"junit-jupiter-api\" % \"5.11.4\" % \"test\",\n      \"org.junit.jupiter\" % \"junit-jupiter-engine\" % \"5.11.4\" % \"test\",\n      \"org.junit.jupiter\" % \"junit-jupiter-params\" % \"5.11.4\" % \"test\",\n      \"com.github.sbt.junit\" % \"jupiter-interface\" % \"0.17.0\" % \"test\",\n      \"org.apache.flink\" % \"flink-test-utils\" % flinkVersion % \"test\",\n      \"org.apache.flink\" % \"flink-clients\" % flinkVersion % \"test\",\n      \"org.apache.flink\" % \"flink-table-api-java-bridge\" % flinkVersion % Test,\n      \"org.apache.flink\" % \"flink-table-planner-loader\" % flinkVersion % Test,\n      \"org.apache.flink\" % \"flink-table-runtime\" % flinkVersion % Test,\n      \"org.apache.flink\" % \"flink-test-utils-junit\" % flinkVersion  % Test,\n      \"org.slf4j\" % \"slf4j-log4j12\" % \"2.0.17\" % \"test\",\n      \"com.github.tomakehurst\" % \"wiremock-jre8\" % \"2.35.0\" % Test\n    ),\n    // Use jupiter\n    excludeDependencies ++= Seq(\n      ExclusionRule(\"junit\", \"junit\"),\n      ExclusionRule(\"org.junit.vintage\", \"junit-vintage-engine\")\n    )\n  )\n\n\nlazy val goldenTables = (project in file(\"connectors/golden-tables\"))\n  .disablePlugins(JavaFormatterPlugin, ScalafmtPlugin)\n  .settings(\n    name := \"golden-tables\",\n    commonSettings,\n    skipReleaseSettings,\n    libraryDependencies ++= Seq(\n      // Test Dependencies\n      \"org.scalatest\" %% \"scalatest\" % scalaTestVersion % \"test\",\n      \"commons-io\" % \"commons-io\" % \"2.8.0\" % \"test\",\n\n      \"io.delta\" %% \"delta-spark\" % \"3.3.2\" % \"test\",\n      \"org.apache.spark\" %% \"spark-sql\" % defaultSparkVersion % \"test\",\n      \"org.apache.spark\" %% \"spark-catalyst\" % defaultSparkVersion % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-core\" % defaultSparkVersion % \"test\" classifier \"tests\",\n      \"org.apache.spark\" %% \"spark-sql\" % defaultSparkVersion % \"test\" classifier \"tests\"\n    )\n  )\n\n/**\n * Get list of python files and return the mapping between source files and target paths\n * in the generated package JAR.\n */\ndef listPythonFiles(pythonBase: File): Seq[(File, String)] = {\n  val pythonExcludeDirs = pythonBase / \"lib\" :: pythonBase / \"doc\" :: pythonBase / \"bin\" :: Nil\n  import scala.collection.JavaConverters._\n  val pythonFiles = Files.walk(pythonBase.toPath).iterator().asScala\n    .map { path => path.toFile() }\n    .filter { file => file.getName.endsWith(\".py\") && ! file.getName.contains(\"test\") }\n    .filter { file => ! pythonExcludeDirs.exists { base => IO.relativize(base, file).nonEmpty} }\n    .toSeq\n\n  pythonFiles pair Path.relativeTo(pythonBase)\n}\n\nThisBuild / parallelExecution := false\n\nval createTargetClassesDir = taskKey[Unit](\"create target classes dir\")\nval generatePythonVersion = taskKey[File](\"Generate Python version.py file\")\n\n/*\n ******************\n * Project groups *\n ******************\n */\n\n// Don't use these groups for any other projects\nlazy val sparkGroup = {\n  val baseProjects = Seq(spark, sparkV1, sparkV1Filtered, sparkV2, contribs, sparkUnityCatalog, storage, storageS3DynamoDB, sharing, connectCommon, connectClient, connectServer)\n  val allProjects = if (supportHudi) {\n    baseProjects :+ hudi\n  } else {\n    baseProjects\n  }\n\n  Project(\"sparkGroup\", file(\"sparkGroup\"))\n    .aggregate(allProjects.map(_.project): _*)\n    .settings(\n      // crossScalaVersions must be set to Nil on the aggregating project\n      crossScalaVersions := Nil,\n      publishArtifact := false,\n      publish / skip := true,\n    )\n}\n\nlazy val icebergGroup = {\n  val allProjects = if (supportIceberg) {\n    Seq(iceberg, testDeltaIcebergJar)\n  } else {\n    Seq.empty\n  }\n\n  Project(\"icebergGroup\", file(\"icebergGroup\"))\n    .aggregate(allProjects.map(_.project): _*)\n    .settings(\n      // crossScalaVersions must be set to Nil on the aggregating project\n      crossScalaVersions := Nil,\n      publishArtifact := false,\n      publish / skip := true,\n    )\n}\n\nlazy val kernelGroup = project\n  .aggregate(kernelApi, kernelDefaults, kernelBenchmarks)\n  .settings(\n    // crossScalaVersions must be set to Nil on the aggregating project\n    crossScalaVersions := Nil,\n    publishArtifact := false,\n    publish / skip := true,\n    unidocSourceFilePatterns := {\n      (kernelApi / unidocSourceFilePatterns).value.scopeToProject(kernelApi) ++\n      (kernelDefaults / unidocSourceFilePatterns).value.scopeToProject(kernelDefaults)\n    }\n  ).configureUnidoc(docTitle = \"Delta Kernel\")\n\nlazy val flinkGroup = project\n  .aggregate(flink)\n  .settings(\n    // crossScalaVersions must be set to Nil on the aggregating project\n    crossScalaVersions := Nil,\n    publishArtifact := false,\n    publish / skip := true,\n  )\n\n/*\n ********************\n * Release settings *\n ********************\n */\nimport ReleaseTransformations._\n\nlazy val skipReleaseSettings = Seq(\n  publishArtifact := false,\n  publish / skip := true\n)\n\n\n// Release settings for artifact that contains only Java source code\nlazy val javaOnlyReleaseSettings = releaseSettings ++ Seq(\n  // drop off Scala suffix from artifact names\n  crossPaths := false,\n\n  // we publish jars for each scalaVersion in crossScalaVersions. however, we only need to publish\n  // one java jar. thus, only do so when the current scala version == default scala version\n  publishArtifact := {\n    val (expMaj, expMin, _) = getMajorMinorPatch(default_scala_version.value)\n    s\"$expMaj.$expMin\" == scalaBinaryVersion.value\n  },\n\n  // exclude scala-library from dependencies in generated pom.xml\n  autoScalaLibrary := false,\n)\n\nlazy val releaseSettings = Seq(\n  publishMavenStyle := true,\n  publishArtifact := true,\n  Test / publishArtifact := false,\n  releasePublishArtifactsAction := PgpKeys.publishSigned.value,\n  releaseCrossBuild := true,\n  pgpPassphrase := sys.env.get(\"PGP_PASSPHRASE\").map(_.toArray),\n\n  // TODO: This isn't working yet ...\n  sonatypeProfileName := \"io.delta\", // sonatype account domain name prefix / group ID\n  credentials += Credentials(\n    \"OSSRH Staging API Service\",\n    \"ossrh-staging-api.central.sonatype.com\",\n    sys.env.getOrElse(\"SONATYPE_USERNAME\", \"\"),\n    sys.env.getOrElse(\"SONATYPE_PASSWORD\", \"\")\n  ),\n  credentials += Credentials(\n    \"Sonatype Nexus Repository Manager\",\n    \"central.sonatype.com\",\n    sys.env.getOrElse(\"SONATYPE_USERNAME\", \"\"),\n    sys.env.getOrElse(\"SONATYPE_PASSWORD\", \"\")\n  ),\n  publishTo := {\n    val ossrhBase = \"https://ossrh-staging-api.central.sonatype.com/\"\n    val centralSnapshots = \"https://central.sonatype.com/repository/maven-snapshots/\"\n    if (isSnapshot.value) {\n      Some(\"snapshots\" at centralSnapshots)\n    } else {\n      Some(\"releases\"  at ossrhBase + \"service/local/staging/deploy/maven2\")\n    }\n  },\n  licenses += (\"Apache-2.0\", url(\"http://www.apache.org/licenses/LICENSE-2.0\")),\n  pomExtra :=\n    <url>https://delta.io/</url>\n      <scm>\n        <url>git@github.com:delta-io/delta.git</url>\n        <connection>scm:git:git@github.com:delta-io/delta.git</connection>\n      </scm>\n      <developers>\n        <developer>\n          <id>marmbrus</id>\n          <name>Michael Armbrust</name>\n          <url>https://github.com/marmbrus</url>\n        </developer>\n        <developer>\n          <id>brkyvz</id>\n          <name>Burak Yavuz</name>\n          <url>https://github.com/brkyvz</url>\n        </developer>\n        <developer>\n          <id>jose-torres</id>\n          <name>Jose Torres</name>\n          <url>https://github.com/jose-torres</url>\n        </developer>\n        <developer>\n          <id>liwensun</id>\n          <name>Liwen Sun</name>\n          <url>https://github.com/liwensun</url>\n        </developer>\n        <developer>\n          <id>mukulmurthy</id>\n          <name>Mukul Murthy</name>\n          <url>https://github.com/mukulmurthy</url>\n        </developer>\n        <developer>\n          <id>tdas</id>\n          <name>Tathagata Das</name>\n          <url>https://github.com/tdas</url>\n        </developer>\n        <developer>\n          <id>zsxwing</id>\n          <name>Shixiong Zhu</name>\n          <url>https://github.com/zsxwing</url>\n        </developer>\n        <developer>\n          <id>scottsand-db</id>\n          <name>Scott Sandre</name>\n          <url>https://github.com/scottsand-db</url>\n        </developer>\n        <developer>\n          <id>windpiger</id>\n          <name>Jun Song</name>\n          <url>https://github.com/windpiger</url>\n        </developer>\n      </developers>\n)\n\n// Looks like some of release settings should be set for the root project as well.\npublishArtifact := false  // Don't release the root project\npublish / skip := true\npublishTo := Some(\"snapshots\" at \"https://central.sonatype.com/repository/maven-snapshots/\")\nreleaseCrossBuild := false  // Don't use sbt-release's cross facility\nreleaseProcess := Seq[ReleaseStep](\n  checkSnapshotDependencies,\n  inquireVersions,\n  runTest,\n  setReleaseVersion,\n  commitReleaseVersion,\n  tagRelease\n) ++ CrossSparkVersions.crossSparkReleaseSteps(\"publishSigned\") ++ Seq[ReleaseStep](\n\n  // Do NOT use `sonatypeBundleRelease` - it will actually release to Maven! We want to do that\n  // manually.\n  //\n  // Do NOT use `sonatypePromote` - it will promote the closed staging repository (i.e. sync to\n  //                                Maven central)\n  //\n  // See https://github.com/xerial/sbt-sonatype#publishing-your-artifact.\n  //\n  // - sonatypePrepare: Drop the existing staging repositories (if exist) and create a new staging\n  //                    repository using sonatypeSessionName as a unique key\n  // - sonatypeBundleUpload: Upload your local staging folder contents to a remote Sonatype\n  //                         repository\n  // - sonatypeClose: closes your staging repository at Sonatype. This step verifies Maven central\n  //                  sync requirement, GPG-signature, javadoc and source code presence, pom.xml\n  //                  settings, etc\n  // TODO: this isn't working yet\n  // releaseStepCommand(\"sonatypePrepare; sonatypeBundleUpload; sonatypeClose\"),\n  setNextVersion,\n  commitNextVersion\n)\n"
  },
  {
    "path": "connectors/.gitignore",
    "content": "*#*#\n*.#*\n*.iml\n*.ipr\n*.iws\n*.pyc\n*.pyo\n*.swp\n*~\n.DS_Store\n.bsp\n.cache\n.classpath\n.ensime\n.ensime_cache/\n.ensime_lucene\n.generated-mima*\n.idea/\n.idea_modules/\n.project\n.pydevproject\n.scala_dependencies\n.settings\n*.pbix\n/lib/\nR-unit-tests.log\nR/unit-tests.out\nR/cran-check.out\nR/pkg/vignettes/sparkr-vignettes.html\nR/pkg/tests/fulltests/Rplots.pdf\nbuild/*.jar\nbuild/apache-maven*\nbuild/scala*\nbuild/zinc*\ncache\nconf/*.cmd\nconf/*.conf\nconf/*.properties\nconf/*.sh\nconf/*.xml\nconf/java-opts\nconf/slaves\ndependency-reduced-pom.xml\nderby.log\ndev/create-release/*final\ndev/create-release/*txt\ndev/pr-deps/\ndist/\ndocs/_site\ndocs/api\nsql/docs\nsql/site\nlib_managed/\nlint-r-report.log\nlog/\nlogs/\nout/\nproject/boot/\nproject/build/target/\nproject/plugins/lib_managed/\nproject/plugins/project/build.properties\nproject/plugins/src_managed/\nproject/plugins/target/\npython/lib/pyspark.zip\npython/deps\ndocs/python/_static/\ndocs/python/_templates/\ndocs/python/_build/\npython/test_coverage/coverage_data\npython/test_coverage/htmlcov\npython/pyspark/python\nreports/\nscalastyle-on-compile.generated.xml\nscalastyle-output.xml\nscalastyle.txt\nspark-*-bin-*.tgz\nspark-tests.log\nsrc_managed/\nstreaming-tests.log\ntarget/\nunit-tests.log\nwork/\ndocs/.jekyll-metadata\n\n# For Hive\nTempStatsStore/\nmetastore/\nmetastore_db/\nsql/hive-thriftserver/test_warehouses\nwarehouse/\nspark-warehouse/\n\n# For R session data\n.RData\n.RHistory\n.Rhistory\n*.Rproj\n*.Rproj.*\n\n.Rproj.user\n\n**/src/main/resources/js\n\n# For SBT\n.jvmopts\n\n# For VS\n/.vs\n/obj\n/bin\n"
  },
  {
    "path": "connectors/README.md",
    "content": "Connectors projects are no longer maintained in the `master` branch and new releases due to migration to the Delta Kernel project. Projects will continue to be supported in maintanence mode from the `spark-3.5-support` branch.\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/124-decimal-decode-bug/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1636689272898,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"844\",\"numOutputRows\":\"1\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"large_decimal\\\",\\\"type\\\":\\\"decimal(10,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1636689270919}}\n{\"add\":{\"path\":\"part-00000-2abbde89-2d0f-465e-a2f0-3e84f1b84654-c000.snappy.parquet\",\"partitionValues\":{},\"size\":333,\"modificationTime\":1636689272000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-5419c9a2-bb44-454f-a109-6e6c6f000a24-c000.snappy.parquet\",\"partitionValues\":{},\"size\":511,\"modificationTime\":1636689272000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728454095,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1633728453099}}\n{\"add\":{\"path\":\"part-00000-2a248db5-8f96-423c-a0f7-c503fe640c6a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728454000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728458439,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"433\",\"numOutputRows\":\"1\"}}}\n{\"add\":{\"path\":\"part-00000-15088d9b-5348-490b-933d-5bf9b7d0b223-c000.snappy.parquet\",\"partitionValues\":{},\"size\":433,\"modificationTime\":1633728458000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728459288,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"433\",\"numOutputRows\":\"1\"}}}\n{\"add\":{\"path\":\"part-00000-c855206c-f42a-4b53-a526-08a9a957ad58-c000.snappy.parquet\",\"partitionValues\":{},\"size\":433,\"modificationTime\":1633728459000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728460020,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"add\":{\"path\":\"part-00000-3f0f0396-41aa-4fa7-954a-c5b22f5b157a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728460000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728460726,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"433\",\"numOutputRows\":\"1\"}}}\n{\"add\":{\"path\":\"part-00000-c4738537-d851-4caa-9596-d543afa47196-c000.snappy.parquet\",\"partitionValues\":{},\"size\":433,\"modificationTime\":1633728460000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728461405,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":4,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"add\":{\"path\":\"part-00000-f9490ff6-f374-4b40-9d76-22addae085d1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728461000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728462063,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":5,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"add\":{\"path\":\"part-00000-66d18d0c-8cab-4cfa-a2c6-7e90df860b5a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728462000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728462739,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":6,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"433\",\"numOutputRows\":\"1\"}}}\n{\"add\":{\"path\":\"part-00000-1b8ea57e-424b-4068-8d0e-707edf853376-c000.snappy.parquet\",\"partitionValues\":{},\"size\":433,\"modificationTime\":1633728462000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728463394,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":7,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"add\":{\"path\":\"part-00000-93beced9-3a9d-4519-b31a-5602a972ffa4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728463000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728464026,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":8,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"add\":{\"path\":\"part-00000-d8e947c6-4f26-455b-a25f-84acb1240f3a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728464000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728464667,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":9,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"303\",\"numOutputRows\":\"0\"}}}\n{\"add\":{\"path\":\"part-00000-f0b12818-15f5-4476-8ebc-9235c74408d2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":303,\"modificationTime\":1633728464000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1633728465909,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":10,\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"433\",\"numOutputRows\":\"1\"}}}\n{\"add\":{\"path\":\"part-00000-223768c3-2e58-4e8a-9d15-54fa113e8c21-c000.snappy.parquet\",\"partitionValues\":{},\"size\":433,\"modificationTime\":1633728465000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/125-iterator-bug/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":13}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-decimal-table/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1690853005164,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"4\",\"numOutputBytes\":\"4131\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"451ba03f-e80c-4fda-9bba-8fdfda856925\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"decimal(12,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"decimal(5,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"decimal(10,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col3\\\",\\\"type\\\":\\\"decimal(20,10)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{},\"createdTime\":1690852998865}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part=-2342342.23423/part-00000-8f850371-9b03-42c4-9d22-f83bc81c9b68.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"-2342342.23423\"},\"size\":1032,\"modificationTime\":1690853004000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":-999.99,\\\"col2\\\":-99999.99999,\\\"col3\\\":-9999999999.9999999999},\\\"maxValues\\\":{\\\"col1\\\":-999.99,\\\"col2\\\":-99999.99999,\\\"col3\\\":-9999999999.9999999999},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n{\"add\":{\"path\":\"part=0.00004/part-00000-1cb60e36-6cd4-4191-a318-ae9355f877c3.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"0.00004\"},\"size\":1033,\"modificationTime\":1690853004000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":0.00,\\\"col2\\\":0.00000,\\\"col3\\\":0E-10},\\\"maxValues\\\":{\\\"col1\\\":0.00,\\\"col2\\\":0.00000,\\\"col3\\\":0E-10},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n{\"add\":{\"path\":\"part=234.00000/part-00000-ac109189-97e5-49af-947f-335a5e46ee5c.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"234.00000\"},\"size\":1033,\"modificationTime\":1690853004000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":1.00,\\\"col2\\\":2.00000,\\\"col3\\\":3.0000000000},\\\"maxValues\\\":{\\\"col1\\\":1.00,\\\"col2\\\":2.00000,\\\"col3\\\":3.0000000000},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2342222.23454/part-00000-d5a0c70f-7cd3-4d32-a9c0-7171a06547c6.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2342222.23454\"},\"size\":1033,\"modificationTime\":1690853004000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":111.11,\\\"col2\\\":22222.22222,\\\"col3\\\":3333333333.3333333333},\\\"maxValues\\\":{\\\"col1\\\":111.11,\\\"col2\\\":22222.22222,\\\"col3\\\":3333333333.3333333333},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-decimal-table-legacy/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1690853019754,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"4\",\"numOutputBytes\":\"4036\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"01c3f3ee-9b79-4245-93f1-7f43ce7afa9c\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"decimal(12,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"decimal(5,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"decimal(10,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col3\\\",\\\"type\\\":\\\"decimal(20,10)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{},\"createdTime\":1690853018509}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part=-2342342.23423/part-00000-ba2f74ac-7b9b-47b9-a287-97d92bd20efc.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"-2342342.23423\"},\"size\":1009,\"modificationTime\":1690853019000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":-999.99,\\\"col2\\\":-99999.99999,\\\"col3\\\":-9999999999.9999999999},\\\"maxValues\\\":{\\\"col1\\\":-999.99,\\\"col2\\\":-99999.99999,\\\"col3\\\":-9999999999.9999999999},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n{\"add\":{\"path\":\"part=0.00004/part-00000-3de65390-7061-47d6-8995-cbb632b4b203.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"0.00004\"},\"size\":1009,\"modificationTime\":1690853019000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":0.00,\\\"col2\\\":0.00000,\\\"col3\\\":0E-10},\\\"maxValues\\\":{\\\"col1\\\":0.00,\\\"col2\\\":0.00000,\\\"col3\\\":0E-10},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n{\"add\":{\"path\":\"part=234.00000/part-00000-654d80b0-611a-4ff3-a8e6-2328dd21cf11.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"234.00000\"},\"size\":1009,\"modificationTime\":1690853019000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":1.00,\\\"col2\\\":2.00000,\\\"col3\\\":3.0000000000},\\\"maxValues\\\":{\\\"col1\\\":1.00,\\\"col2\\\":2.00000,\\\"col3\\\":3.0000000000},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2342222.23454/part-00000-fe848a88-0465-4b4f-8414-25e6da7062f8.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2342222.23454\"},\"size\":1009,\"modificationTime\":1690853019000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":111.11,\\\"col2\\\":22222.22222,\\\"col3\\\":3333333333.3333333333},\\\"maxValues\\\":{\\\"col1\\\":111.11,\\\"col2\\\":22222.22222,\\\"col3\\\":3333333333.3333333333},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426732135,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"539\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"8c9f4d2d-645f-4f02-b13b-aa6da098716c\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1691426730560}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-b09fdf65-0ae3-44d0-96d0-1d85a121b76a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":539,\"modificationTime\":1691426732072,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426734180,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1529cdd3-5178-47ce-b13d-962cbfdcb028\"}}\n{\"add\":{\"path\":\"part-00000-0869ab64-e69d-407f-80d4-1a2ea1f69d11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426734175,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":19},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426734787,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"d1bc1416-99e2-4aba-9e4b-1b157d9d2b49\"}}\n{\"add\":{\"path\":\"part-00000-bd8763c3-45e4-435e-acd6-8e599aa840bc-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426734784,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":20},\\\"maxValues\\\":{\\\"id\\\":29},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426735371,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"3dfdeb21-169d-4197-a49b-411fc956153a\"}}\n{\"add\":{\"path\":\"part-00000-60f14460-c8e0-41b4-a33f-1a83bb59f13c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426735367,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":30},\\\"maxValues\\\":{\\\"id\\\":39},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426735942,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"56a9553b-8f9d-44e0-a236-25081a0083ad\"}}\n{\"add\":{\"path\":\"part-00000-b326e43b-3e01-4cf1-b8ff-c73c8abd1616-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426735939,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":40},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426737153,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"((id#1904L >= 5) AND (id#1904L <= 9))\\\"]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"539\",\"numCopiedRows\":\"5\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"681\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"508\",\"numAddedFiles\":\"1\",\"numAddedBytes\":\"500\",\"rewriteTimeMs\":\"172\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"8deec26c-c828-46dc-962d-e324783081c0\"}}\n{\"remove\":{\"path\":\"part-00000-b09fdf65-0ae3-44d0-96d0-1d85a121b76a-c000.snappy.parquet\",\"deletionTimestamp\":1691426737143,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":539}}\n{\"add\":{\"path\":\"part-00000-ca2d0b26-c15c-454f-a933-fc724e15e5f1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1691426737139,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426737814,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"((id#2606L >= 15) AND (id#2606L <= 19))\\\"]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"527\",\"numCopiedRows\":\"5\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"284\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"168\",\"numAddedFiles\":\"1\",\"numAddedBytes\":\"503\",\"rewriteTimeMs\":\"116\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"dc2ad0e6-7337-4501-a47b-c6af0138b7dc\"}}\n{\"remove\":{\"path\":\"part-00000-0869ab64-e69d-407f-80d4-1a2ea1f69d11-c000.snappy.parquet\",\"deletionTimestamp\":1691426737813,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":527}}\n{\"add\":{\"path\":\"part-00000-c92cba9e-6c07-4a93-916a-0a6e115e39b3-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1691426737811,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":14},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426738561,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"((id#3297L >= 25) AND (id#3297L <= 29))\\\"]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"527\",\"numCopiedRows\":\"5\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"293\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"158\",\"numAddedFiles\":\"1\",\"numAddedBytes\":\"503\",\"rewriteTimeMs\":\"135\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"dcdfb390-1424-44a8-a1ef-9713726bc6cb\"}}\n{\"remove\":{\"path\":\"part-00000-bd8763c3-45e4-435e-acd6-8e599aa840bc-c000.snappy.parquet\",\"deletionTimestamp\":1691426738560,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":527}}\n{\"add\":{\"path\":\"part-00000-1b0098ea-c696-4470-84cc-d43bb7afb833-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1691426738558,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":20},\\\"maxValues\\\":{\\\"id\\\":24},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426739285,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"((id#3988L >= 35) AND (id#3988L <= 39))\\\"]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"527\",\"numCopiedRows\":\"5\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"345\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"175\",\"numAddedFiles\":\"1\",\"numAddedBytes\":\"503\",\"rewriteTimeMs\":\"170\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"8e184ae5-6338-43db-96d1-c54619c2e20f\"}}\n{\"remove\":{\"path\":\"part-00000-60f14460-c8e0-41b4-a33f-1a83bb59f13c-c000.snappy.parquet\",\"deletionTimestamp\":1691426739284,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":527}}\n{\"add\":{\"path\":\"part-00000-f80053c6-2b0d-41ed-ab5f-61ef1503cae6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1691426739280,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":30},\\\"maxValues\\\":{\\\"id\\\":34},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426740025,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"((id#4679L >= 45) AND (id#4679L <= 49))\\\"]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"527\",\"numCopiedRows\":\"5\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"324\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"158\",\"numAddedFiles\":\"1\",\"numAddedBytes\":\"503\",\"rewriteTimeMs\":\"166\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"85e2749a-937c-4c55-8234-0342ae82a54f\"}}\n{\"remove\":{\"path\":\"part-00000-b326e43b-3e01-4cf1-b8ff-c73c8abd1616-c000.snappy.parquet\",\"deletionTimestamp\":1691426740025,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":527}}\n{\"add\":{\"path\":\"part-00000-4b448490-06f4-4c74-9f65-9f36ae68e3b2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1691426740023,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":40},\\\"maxValues\\\":{\\\"id\\\":44},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426740500,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"5f7d481e-9635-43e1-a4a1-584bf0ae63f9\"}}\n{\"add\":{\"path\":\"part-00000-da82aeb5-4edb-4cc1-91ef-970c75c965cc-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426740498,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":59},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426741681,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"a99db5a3-bc5b-44f8-abc5-ce30bda4c0b0\"}}\n{\"add\":{\"path\":\"part-00000-26da113c-2e45-4aba-b1ce-6eb5e46c53f7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426741411,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":60},\\\"maxValues\\\":{\\\"id\\\":69},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000012.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426742288,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":11,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"527\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"6c4dae3d-b3b4-4359-9abc-34883703faba\"}}\n{\"add\":{\"path\":\"part-00000-c967edfa-f104-44ea-b0da-8bc1f5402af4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1691426742286,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":70},\\\"maxValues\\\":{\\\"id\\\":79},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/00000000000000000013.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691426743015,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#6991L >= 66)\\\"]\"},\"readVersion\":12,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"2\",\"numRemovedBytes\":\"1054\",\"numCopiedRows\":\"6\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"270\",\"numDeletedRows\":\"14\",\"scanTimeMs\":\"151\",\"numAddedFiles\":\"1\",\"numAddedBytes\":\"510\",\"rewriteTimeMs\":\"119\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"153f81dc-ba06-46e8-81c7-75c26e500a57\"}}\n{\"remove\":{\"path\":\"part-00000-26da113c-2e45-4aba-b1ce-6eb5e46c53f7-c000.snappy.parquet\",\"deletionTimestamp\":1691426743014,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":527}}\n{\"remove\":{\"path\":\"part-00000-c967edfa-f104-44ea-b0da-8bc1f5402af4-c000.snappy.parquet\",\"deletionTimestamp\":1691426743014,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":527}}\n{\"add\":{\"path\":\"part-00000-7d1a368c-74ea-42df-9527-2c9a7c8292b9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":510,\"modificationTime\":1691426743013,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":6,\\\"minValues\\\":{\\\"id\\\":60},\\\"maxValues\\\":{\\\"id\\\":65},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-deletes-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":13,\"sizeInBytes\":16479,\"numOfAddFiles\":6,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"6872b3692f168925bdd80e3f92163949\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-merge/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697587480772,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"2191\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"f7ec08aa-a91e-49c6-97e8-a1d13f4e20af\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"str\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697587476380}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-b4335bad-f5f0-4426-9ec4-14ed854f862b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1091,\"modificationTime\":1697587480000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0,\\\"str\\\":\\\"val=0\\\"},\\\"maxValues\\\":{\\\"id\\\":49,\\\"str\\\":\\\"val=9\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-b80a2dea-5a83-4580-96d5-4977d14195ab-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1100,\"modificationTime\":1697587480000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50,\\\"str\\\":\\\"val=50\\\"},\\\"maxValues\\\":{\\\"id\\\":99,\\\"str\\\":\\\"val=99\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-merge/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697587497062,\"operation\":\"MERGE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#480L = cast(id#482 as bigint))\\\"]\",\"matchedPredicates\":\"[{\\\"actionType\\\":\\\"update\\\"}]\",\"notMatchedPredicates\":\"[{\\\"actionType\\\":\\\"insert\\\"}]\",\"notMatchedBySourcePredicates\":\"[{\\\"predicate\\\":\\\"(id#482 < 10)\\\",\\\"actionType\\\":\\\"delete\\\"}]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numTargetRowsCopied\":\"40\",\"numTargetRowsDeleted\":\"10\",\"numTargetFilesAdded\":\"1\",\"numTargetBytesAdded\":\"1495\",\"numTargetBytesRemoved\":\"2191\",\"numTargetRowsMatchedUpdated\":\"50\",\"executionTimeMs\":\"5573\",\"numTargetRowsInserted\":\"50\",\"numTargetRowsMatchedDeleted\":\"0\",\"scanTimeMs\":\"3600\",\"numTargetRowsUpdated\":\"50\",\"numOutputRows\":\"140\",\"numTargetRowsNotMatchedBySourceUpdated\":\"0\",\"numTargetChangeFilesAdded\":\"0\",\"numSourceRows\":\"100\",\"numTargetFilesRemoved\":\"2\",\"numTargetRowsNotMatchedBySourceDeleted\":\"10\",\"rewriteTimeMs\":\"1284\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"70de6669-546f-4f21-884c-96762f8bb154\"}}\n{\"remove\":{\"path\":\"part-00001-b80a2dea-5a83-4580-96d5-4977d14195ab-c000.snappy.parquet\",\"deletionTimestamp\":1697587496997,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1100,\"stats\":\"{\\\"numRecords\\\":50}\"}}\n{\"remove\":{\"path\":\"part-00000-b4335bad-f5f0-4426-9ec4-14ed854f862b-c000.snappy.parquet\",\"deletionTimestamp\":1697587496998,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1091,\"stats\":\"{\\\"numRecords\\\":50}\"}}\n{\"add\":{\"path\":\"part-00000-992247c6-6cf4-45f8-8367-11a5e14b8ea9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1697587496000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":140,\\\"minValues\\\":{\\\"id\\\":10,\\\"str\\\":\\\"EXT\\\"},\\\"maxValues\\\":{\\\"id\\\":149,\\\"str\\\":\\\"val=49\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-overwrite-restore/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697588664008,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"1454\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"0a02e21a-7f3f-4945-92c7-5761336283de\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697588658132}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-d24c9b15-187d-4542-90ef-7834bfaa4971-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697588663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-66f56273-e583-4a88-9da6-2c199bdaf665-c000.snappy.parquet\",\"partitionValues\":{},\"size\":689,\"modificationTime\":1697588663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-overwrite-restore/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697588676444,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"1380\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"64533fbc-52a0-4d99-a4dc-cf7e90c4f8f4\"}}\n{\"add\":{\"path\":\"part-00000-5e752668-638c-4e95-9521-5e88926e3169-c000.snappy.parquet\",\"partitionValues\":{},\"size\":689,\"modificationTime\":1697588676000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":100},\\\"maxValues\\\":{\\\"id\\\":149},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-180c081a-f358-4bf9-8daa-4d04a5aa7f51-c000.snappy.parquet\",\"partitionValues\":{},\"size\":691,\"modificationTime\":1697588676000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":150},\\\"maxValues\\\":{\\\"id\\\":199},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-overwrite-restore/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697588681190,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"500\",\"numOutputBytes\":\"2995\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"9ad1c5a7-c402-416b-90a9-dc22d4fa2b81\"}}\n{\"add\":{\"path\":\"part-00000-79fa68ed-3d70-4f61-95da-9eb676b24a98-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1496,\"modificationTime\":1697588679000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":500},\\\"maxValues\\\":{\\\"id\\\":749},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-bc9b37c2-a201-499d-b604-93623e2de1d6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1499,\"modificationTime\":1697588679000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":750},\\\"maxValues\\\":{\\\"id\\\":999},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"remove\":{\"path\":\"part-00001-180c081a-f358-4bf9-8daa-4d04a5aa7f51-c000.snappy.parquet\",\"deletionTimestamp\":1697588681179,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":691}}\n{\"remove\":{\"path\":\"part-00001-66f56273-e583-4a88-9da6-2c199bdaf665-c000.snappy.parquet\",\"deletionTimestamp\":1697588681180,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":689}}\n{\"remove\":{\"path\":\"part-00000-5e752668-638c-4e95-9521-5e88926e3169-c000.snappy.parquet\",\"deletionTimestamp\":1697588681180,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":689}}\n{\"remove\":{\"path\":\"part-00000-d24c9b15-187d-4542-90ef-7834bfaa4971-c000.snappy.parquet\",\"deletionTimestamp\":1697588681180,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":765}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-overwrite-restore/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697588691204,\"operation\":\"RESTORE\",\"operationParameters\":{\"version\":1,\"timestamp\":null},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRestoredFiles\":\"4\",\"removedFilesSize\":\"2995\",\"numRemovedFiles\":\"2\",\"restoredFilesSize\":\"2834\",\"numOfFilesAfterRestore\":\"4\",\"tableSizeAfterRestore\":\"2834\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ec6a69f9-8721-4043-ae13-84e1c26c1e1d\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697588658132}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00001-66f56273-e583-4a88-9da6-2c199bdaf665-c000.snappy.parquet\",\"partitionValues\":{},\"size\":689,\"modificationTime\":1697588663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-180c081a-f358-4bf9-8daa-4d04a5aa7f51-c000.snappy.parquet\",\"partitionValues\":{},\"size\":691,\"modificationTime\":1697588676000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":150},\\\"maxValues\\\":{\\\"id\\\":199},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00000-5e752668-638c-4e95-9521-5e88926e3169-c000.snappy.parquet\",\"partitionValues\":{},\"size\":689,\"modificationTime\":1697588676000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":100},\\\"maxValues\\\":{\\\"id\\\":149},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00000-d24c9b15-187d-4542-90ef-7834bfaa4971-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697588663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"remove\":{\"path\":\"part-00001-bc9b37c2-a201-499d-b604-93623e2de1d6-c000.snappy.parquet\",\"deletionTimestamp\":1697588691416,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1499,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":750},\\\"maxValues\\\":{\\\"id\\\":999},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"remove\":{\"path\":\"part-00000-79fa68ed-3d70-4f61-95da-9eb676b24a98-c000.snappy.parquet\",\"deletionTimestamp\":1697588691418,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1496,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":500},\\\"maxValues\\\":{\\\"id\\\":749},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-overwrite-restore/_delta_log/_last_checkpoint",
    "content": "{\"version\":3,\"size\":8,\"sizeInBytes\":16053,\"numOfAddFiles\":4,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"a68904381fcbd9efd0ac76b9756166c7\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-updates/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697587748016,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"2191\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1a8d27d3-4e3d-4d34-af7d-88cdd71e1b99\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"str\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697587743472}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-f9886fc2-20a0-42fe-8b30-c3abb5e3c720-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1091,\"modificationTime\":1697587747000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0,\\\"str\\\":\\\"val=0\\\"},\\\"maxValues\\\":{\\\"id\\\":49,\\\"str\\\":\\\"val=9\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-13a6bfd9-3835-44dd-b4f1-465aa95b2bf4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1100,\"modificationTime\":1697587747000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50,\\\"str\\\":\\\"val=50\\\"},\\\"maxValues\\\":{\\\"id\\\":99,\\\"str\\\":\\\"val=99\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-inserts-updates/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697587762295,\"operation\":\"UPDATE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#480 < 50)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"1091\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"4190\",\"numDeletionVectorsUpdated\":\"0\",\"scanTimeMs\":\"3438\",\"numAddedFiles\":\"1\",\"numUpdatedRows\":\"50\",\"numAddedBytes\":\"912\",\"rewriteTimeMs\":\"750\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"2a6cf901-2357-430d-a046-aaf95aa527de\"}}\n{\"add\":{\"path\":\"part-00000-6dfaec75-bd45-4fd6-b20f-7d58c9341479-c000.snappy.parquet\",\"partitionValues\":{},\"size\":912,\"modificationTime\":1697587762000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0,\\\"str\\\":\\\"N/A\\\"},\\\"maxValues\\\":{\\\"id\\\":49,\\\"str\\\":\\\"N/A\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n{\"remove\":{\"path\":\"part-00000-f9886fc2-20a0-42fe-8b30-c3abb5e3c720-c000.snappy.parquet\",\"deletionTimestamp\":1697587762273,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1091}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-vacuum-protocol-check-feature/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1711484004670,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"2191\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"aa418762-a98f-4d46-af25-30c4ac4b18a1\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"str\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1711484003431}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-e719b63b-4142-4bad-9776-45642d5858ae-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1091,\"modificationTime\":1711484004548,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0,\\\"str\\\":\\\"val=0\\\"},\\\"maxValues\\\":{\\\"id\\\":49,\\\"str\\\":\\\"val=9\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-fd905e0a-6d0c-4ce3-bb41-147517448b3b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1100,\"modificationTime\":1711484004548,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50,\\\"str\\\":\\\"val=50\\\"},\\\"maxValues\\\":{\\\"id\\\":99,\\\"str\\\":\\\"val=99\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"str\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/basic-with-vacuum-protocol-check-feature/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1711484006901,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.feature.vacuumprotocolcheck\\\":\\\"supported\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"b1e2fa88-d44e-4fd6-9e2a-a1fa020f0ba6\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"str\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1711484003431}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"vacuumProtocolCheck\"],\"writerFeatures\":[\"appendOnly\",\"invariants\",\"vacuumProtocolCheck\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-normal-a/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"7afc4b76-09fb-4b06-836d-f9972b9c1f91\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723990637}}\n{\"add\":{\"path\":\"/some/unqualified/absolute/path\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-normal-a/_delta_log/00000000000000000001.json",
    "content": "{\"remove\":{\"path\":\"file:/some/unqualified/absolute/path\",\"deletionTimestamp\":200,\"dataChange\":false}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-normal-b/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"6b8e62a0-dd56-4453-b00b-9f9669076189\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723991085}}\n{\"add\":{\"path\":\"/some/unqualified/absolute/path\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-normal-b/_delta_log/00000000000000000001.json",
    "content": "{\"remove\":{\"path\":\"file:///some/unqualified/absolute/path\",\"deletionTimestamp\":200,\"dataChange\":false}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-special-a/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"b2facba1-1669-43f3-9b1d-7580c207873e\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723991525}}\n{\"add\":{\"path\":\"/some/unqualified/with%20space/p@%23h\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-special-a/_delta_log/00000000000000000001.json",
    "content": "{\"remove\":{\"path\":\"file:/some/unqualified/with%20space/p@%23h\",\"deletionTimestamp\":200,\"dataChange\":false}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-special-b/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"c23cd784-bc31-46e5-a95b-73bcbe1111a5\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723991975}}\n{\"add\":{\"path\":\"/some/unqualified/with%20space/p@%23h\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/canonicalized-paths-special-b/_delta_log/00000000000000000001.json",
    "content": "{\"remove\":{\"path\":\"file:///some/unqualified/with%20space/p@%23h\",\"deletionTimestamp\":200,\"dataChange\":false}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/commit-info-containing-arbitrary-operationParams-types/_delta_log/00000000000000000000.crc",
    "content": "{\"txnId\":\"bda32d72-442d-4705-9a8c-16093eb31744\",\"tableSizeBytes\":452,\"numFiles\":1,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"da00fe29-8b6e-4f3b-b91f-a3729283bc1a\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"month\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"month\"],\"configuration\":{\"delta.enableChangeDataFeed\":\"true\"},\"createdTime\":1740185389028},\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":7,\"writerFeatures\":[\"changeDataFeed\",\"appendOnly\",\"invariants\"]},\"allFiles\":[{\"path\":\"month=1/part-00000-22d25ea7-a383-44df-ad22-6b06d871b547.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"1\"},\"size\":452,\"modificationTime\":1740185390672,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0}}\"}]}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/commit-info-containing-arbitrary-operationParams-types/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1740185390903,\"operation\":\"CREATE TABLE AS SELECT\",\"operationParameters\":{\"partitionBy\":\"[\\\"month\\\"]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.enableChangeDataFeed\\\":\\\"true\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"452\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"bda32d72-442d-4705-9a8c-16093eb31744\"}}\n{\"metaData\":{\"id\":\"da00fe29-8b6e-4f3b-b91f-a3729283bc1a\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"month\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"month\"],\"configuration\":{\"delta.enableChangeDataFeed\":\"true\"},\"createdTime\":1740185389028}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":7,\"writerFeatures\":[\"changeDataFeed\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"month=1/part-00000-22d25ea7-a383-44df-ad22-6b06d871b547.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"1\"},\"size\":452,\"modificationTime\":1740185390672,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/commit-info-containing-arbitrary-operationParams-types/_delta_log/00000000000000000001.crc",
    "content": "{\"txnId\":\"0d7d28b8-55c2-4d8b-b48e-88b22c90aed1\",\"tableSizeBytes\":904,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"da00fe29-8b6e-4f3b-b91f-a3729283bc1a\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"month\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"month\"],\"configuration\":{\"delta.enableChangeDataFeed\":\"true\"},\"createdTime\":1740185389028},\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":7,\"writerFeatures\":[\"changeDataFeed\",\"appendOnly\",\"invariants\"]},\"allFiles\":[{\"path\":\"month=2/part-00000-cc2a9650-0450-4879-9757-873b7f544510.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"2\"},\"size\":452,\"modificationTime\":1740185395663,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":2},\\\"nullCount\\\":{\\\"id\\\":0}}\"},{\"path\":\"month=1/part-00000-22d25ea7-a383-44df-ad22-6b06d871b547.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"1\"},\"size\":452,\"modificationTime\":1740185390672,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0}}\"}]}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/commit-info-containing-arbitrary-operationParams-types/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1740185395669,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"452\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"0d7d28b8-55c2-4d8b-b48e-88b22c90aed1\"}}\n{\"add\":{\"path\":\"month=2/part-00000-cc2a9650-0450-4879-9757-873b7f544510.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"2\"},\"size\":452,\"modificationTime\":1740185395663,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":2},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/commit-info-containing-arbitrary-operationParams-types/_delta_log/00000000000000000002.crc",
    "content": "{\"txnId\":\"79b3e3aa-82dc-4c18-b95e-8b50089b55c7\",\"tableSizeBytes\":904,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"da00fe29-8b6e-4f3b-b91f-a3729283bc1a\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"month\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"month\"],\"configuration\":{\"delta.enableChangeDataFeed\":\"true\"},\"createdTime\":1740185389028},\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":7,\"writerFeatures\":[\"changeDataFeed\",\"appendOnly\",\"invariants\"]},\"allFiles\":[{\"path\":\"month=2/part-00000-129a0441-5f41-4e46-be33-fd0289e53614.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"2\"},\"size\":452,\"modificationTime\":1740185397380,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":2},\\\"nullCount\\\":{\\\"id\\\":0}}\"},{\"path\":\"month=1/part-00000-c5babbd8-6013-484c-818f-22d546976866.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"1\"},\"size\":452,\"modificationTime\":1740185397384,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0}}\"}]}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/commit-info-containing-arbitrary-operationParams-types/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1740185397394,\"operation\":\"OPTIMIZE\",\"operationParameters\":{\"predicate\":\"[]\",\"zOrderBy\":\"[\\\"id\\\"]\",\"clusterBy\":\"[]\",\"auto\":false},\"readVersion\":1,\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"2\",\"numRemovedBytes\":\"904\",\"p25FileSize\":\"452\",\"numDeletionVectorsRemoved\":\"0\",\"minFileSize\":\"452\",\"numAddedFiles\":\"2\",\"maxFileSize\":\"452\",\"p75FileSize\":\"452\",\"p50FileSize\":\"452\",\"numAddedBytes\":\"904\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"79b3e3aa-82dc-4c18-b95e-8b50089b55c7\"}}\n{\"add\":{\"path\":\"month=1/part-00000-c5babbd8-6013-484c-818f-22d546976866.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"1\"},\"size\":452,\"modificationTime\":1740185397384,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"remove\":{\"path\":\"month=1/part-00000-22d25ea7-a383-44df-ad22-6b06d871b547.c000.snappy.parquet\",\"deletionTimestamp\":1740185396708,\"dataChange\":false,\"extendedFileMetadata\":true,\"partitionValues\":{\"month\":\"1\"},\"size\":452,\"stats\":\"{\\\"numRecords\\\":1}\"}}\n{\"add\":{\"path\":\"month=2/part-00000-129a0441-5f41-4e46-be33-fd0289e53614.c000.snappy.parquet\",\"partitionValues\":{\"month\":\"2\"},\"size\":452,\"modificationTime\":1740185397380,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":2},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"remove\":{\"path\":\"month=2/part-00000-cc2a9650-0450-4879-9757-873b7f544510.c000.snappy.parquet\",\"deletionTimestamp\":1740185396708,\"dataChange\":false,\"extendedFileMetadata\":true,\"partitionValues\":{\"month\":\"2\"},\"size\":452,\"stats\":\"{\\\"numRecords\\\":1}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723979876,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"5756e7b1-4b09-4c4e-a3b8-da3c214613d0\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723979876}}\n{\"add\":{\"path\":\"0\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723980484,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"1\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723981300,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":1,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"2\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723982125,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"3\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723982971,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":3,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"4\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723984006,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":4,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"5\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723985117,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":5,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"6\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723986119,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":6,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"7\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723987024,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":7,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"8\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723987921,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":8,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"9\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723988863,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":9,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"10\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":13}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654175172,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"68e91815-2b2a-4bc9-9d5c-0c88abe5bb49\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697654170283}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-bca1b163-25a1-4130-b74c-b905c61018ca-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654174000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-c1199313-5eb1-4d9d-9cec-a43245621024-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654174000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654186351,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"a9dc1765-ff32-4c44-9df0-0716157df530\"}}\n{\"add\":{\"path\":\"part-00000-cd63e6e7-227f-4bae-8ffc-fad3bfea242c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654186000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-89b7b3e6-d076-43af-963f-3a4055a1eca6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654186000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654189454,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"c2955e78-34d2-48d8-a438-9e3d94240fef\"}}\n{\"add\":{\"path\":\"part-00000-51f8ff2c-8e81-4031-94c9-93eae615d3e3-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654189000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-81d22bd7-311e-4934-839e-f635ea6f364f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654189000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654191877,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ff507eb6-5061-4d9b-9e05-1f4a52d19bac\"}}\n{\"add\":{\"path\":\"part-00000-59a396e0-b0f4-4685-80f1-f58e07601862-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654191000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-a68acb2a-ac4f-46c2-940b-f962480a6517-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654191000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654194175,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1df7a487-0697-4474-a8ec-0f0911216e68\"}}\n{\"add\":{\"path\":\"part-00000-99f8ecc2-cc99-4e3e-866e-07135df25e52-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654194000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-8e839ba6-38f3-4093-8eb4-bc894159348c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654194000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654195998,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"49c01705-e0fc-419c-ac2c-6962d449438b\"}}\n{\"add\":{\"path\":\"part-00000-a57ecbd0-7dad-4b6c-a3fe-8ab4f7e73f5a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654195000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-ef16b167-3dda-4681-bdd0-cd6bb9f07c30-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654195000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654197786,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"43fdd9d9-cfef-4cd7-982c-e0bb41114f62\"}}\n{\"add\":{\"path\":\"part-00000-d9d02879-5155-46d4-84a8-41c83c5df9e4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654197000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-84978e4c-0e36-40d7-a3e0-c69204409c28-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654197000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654200341,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"a845999b-ec65-424a-a62a-ace1b2d7336c\"}}\n{\"add\":{\"path\":\"part-00000-45ddfb64-1797-4618-a4e4-58d687ae9d21-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654200000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-e471a872-a1ee-4610-9454-062854327ad6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654200000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654202124,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"fe722b89-1814-46ad-84dd-3d0b3148dd90\"}}\n{\"add\":{\"path\":\"part-00000-69f4e384-139f-4b75-b51f-09213866a62a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654202000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-400931d7-721c-4dbc-82e6-5c29f1dfcde1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654202000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654203783,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"40c60253-5a25-4e4c-a4a1-47795ea78217\"}}\n{\"add\":{\"path\":\"part-00000-82c1686f-287a-4e6f-8a7a-0099d54d7738-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654203000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-71b04841-d4e6-4cd6-930a-5e33fd1bd7a0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654203000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654205382,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"b6376238-762c-4644-90ad-6fee36425722\"}}\n{\"add\":{\"path\":\"part-00000-cbc535a8-3499-4339-be3f-9df89091871e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1697654205000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-c4cbb8cf-9c18-4bab-bfa7-967faa14e15d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1697654205000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697654211950,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"1454\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"6f33f474-6422-4edf-83fb-d97f276fd8d2\"}}\n{\"add\":{\"path\":\"part-00000-45318b19-5a29-4bb9-b273-1738e817d63e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697654210000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-4eeaf77f-87b7-45bb-8e1f-1faf9c957918-c000.snappy.parquet\",\"partitionValues\":{},\"size\":689,\"modificationTime\":1697654210000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"remove\":{\"path\":\"part-00000-45ddfb64-1797-4618-a4e4-58d687ae9d21-c000.snappy.parquet\",\"deletionTimestamp\":1697654211945,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-69f4e384-139f-4b75-b51f-09213866a62a-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-82c1686f-287a-4e6f-8a7a-0099d54d7738-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-99f8ecc2-cc99-4e3e-866e-07135df25e52-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-a57ecbd0-7dad-4b6c-a3fe-8ab4f7e73f5a-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-bca1b163-25a1-4130-b74c-b905c61018ca-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-cbc535a8-3499-4339-be3f-9df89091871e-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-cd63e6e7-227f-4bae-8ffc-fad3bfea242c-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00001-400931d7-721c-4dbc-82e6-5c29f1dfcde1-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-71b04841-d4e6-4cd6-930a-5e33fd1bd7a0-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-a68acb2a-ac4f-46c2-940b-f962480a6517-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-c1199313-5eb1-4d9d-9cec-a43245621024-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-c4cbb8cf-9c18-4bab-bfa7-967faa14e15d-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-e471a872-a1ee-4610-9454-062854327ad6-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-ef16b167-3dda-4681-bdd0-cd6bb9f07c30-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00000-51f8ff2c-8e81-4031-94c9-93eae615d3e3-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-59a396e0-b0f4-4685-80f1-f58e07601862-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00000-d9d02879-5155-46d4-84a8-41c83c5df9e4-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n{\"remove\":{\"path\":\"part-00001-81d22bd7-311e-4934-839e-f635ea6f364f-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-84978e4c-0e36-40d7-a3e0-c69204409c28-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-89b7b3e6-d076-43af-963f-3a4055a1eca6-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n{\"remove\":{\"path\":\"part-00001-8e839ba6-38f3-4093-8eb4-bc894159348c-c000.snappy.parquet\",\"deletionTimestamp\":1697654211946,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":503}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/corrupted-last-checkpoint-kernel/_delta_log/_last_checkpoint",
    "content": ""
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-absolute-paths-escaped-chars/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603387084639,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"..//Users/scott.sandre/connectors/golden-tables/src/test/resources/golden/data-reader-absolute-paths-escaped-chars/foo.snappy.parquet\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1603387084631,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-absolute-paths-escaped-chars/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603387085189,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"bar%2Dbar.snappy.parquet\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1603387085181,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-array-complex-objects/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724039052,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"38be1738-32ad-448f-9e29-912a7536d4ca\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"i\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"3d_int_list\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"4d_int_list\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"list_of_maps\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"list_of_records\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"val\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724038935}}\n{\"add\":{\"path\":\"part-00000-a7d58b1a-7743-4bb0-b208-438bbe179c93-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2830,\"modificationTime\":1603724039000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-7b211746-0a31-4e77-9822-b0985158cd66-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2832,\"modificationTime\":1603724039000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-array-primitives/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724038064,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"caaa1362-3717-449b-ab9b-f7d8d536018d\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_array_int\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_long\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"long\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_byte\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"byte\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_short\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"short\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_boolean\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"boolean\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_float\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"float\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_double\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"double\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_string\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"string\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_binary\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"binary\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_array_big_decimal\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"decimal(1,0)\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724037970}}\n{\"add\":{\"path\":\"part-00000-182665f0-30df-470d-a5cb-8d9d483ed390-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3627,\"modificationTime\":1603724038000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-2e274fe7-eb75-4b73-8c72-423ee747abc0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3644,\"modificationTime\":1603724038000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-America/Los_Angeles/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724034349,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"475dbe77-c782-43a9-830b-d1777f3a7244\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724034283}}\n{\"add\":{\"path\":\"part-00000-e85ca549-604b-4340-b56d-868e9acc78e8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724034000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-1e808610-ee7f-44e7-be9b-be02c2bc5895-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724034000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-Asia/Beirut/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724036152,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"7575fa96-acd9-4e2b-9f29-ce44fac98c60\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724036096}}\n{\"add\":{\"path\":\"part-00000-58828e3c-041e-47b4-80dd-196ae1b1d1a6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724036000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-8590d66f-6907-40a9-9e97-a4a098321340-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724036000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-Etc/GMT+9/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724035263,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"8684eda0-16ef-4527-a298-798fef1e87f4\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724035206}}\n{\"add\":{\"path\":\"part-00000-23e032bb-e586-4573-9fc0-1c9a4c9a5081-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724035000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-d91bf3dd-78c9-4abf-aa54-e89228e8316c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724035000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-Iceland/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724032094,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"fac6661d-d03f-4dca-954d-f3546571c198\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724032022}}\n{\"add\":{\"path\":\"part-00000-8be8ec9f-d9af-474e-8ec9-35ec76debc6a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724032000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-56f07a95-04d4-4c12-bf08-fd89cedc8559-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724032000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-JST/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724037072,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"b6b84722-3b6d-4f69-8870-48baebf70fe7\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724037013}}\n{\"add\":{\"path\":\"part-00000-3f9100ce-0b94-43cb-bb23-f0e36dc7af2b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724037000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-dc211b29-0c30-41e8-8700-f8bb374964e1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724037000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-PST/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724033415,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"6b978932-93d9-431e-a7a2-b572472b09c6\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724033355}}\n{\"add\":{\"path\":\"part-00000-0a103e9a-6236-470c-94f7-5f60926f01da-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724033000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-980a117f-027e-4396-81ce-3a5a8ac70815-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724033000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-date-types-UTC/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724030655,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"d33c8691-c845-46c4-bb93-1ae64db706b5\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724030532}}\n{\"add\":{\"path\":\"part-00000-803e1cfa-c859-4ce7-977b-ff150d6e138c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":358,\"modificationTime\":1603724030000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-0108113a-2933-41b3-b9a6-e68bb9ed25cc-c000.snappy.parquet\",\"partitionValues\":{},\"size\":717,\"modificationTime\":1603724030000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-escaped-chars/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724042582,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"_2\\\"]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"ccdc1b2a-f27e-47a6-aadb-dab6b88ac899\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"_1\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"_2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"_2\"],\"configuration\":{},\"createdTime\":1603724042500}}\n{\"add\":{\"path\":\"_2=bar+%252521/part-00000-af08f887-922f-4c31-82a7-8e142c4280a6.c000.snappy.parquet\",\"partitionValues\":{\"_2\":\"bar+%21\"},\"size\":398,\"modificationTime\":1603724042000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-escaped-chars/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724043128,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"_2\\\"]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"_2=bar+%252522/part-00000-c1bfd944-5e0d-4133-af16-7851061e37aa.c000.snappy.parquet\",\"partitionValues\":{\"_2\":\"bar+%22\"},\"size\":398,\"modificationTime\":1603724043000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-escaped-chars/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724043721,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"_2\\\"]\"},\"readVersion\":1,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"_2=bar+%252523/part-00000-92352854-5503-4ba5-8c29-b11777034eb7.c000.snappy.parquet\",\"partitionValues\":{\"_2\":\"bar+%23\"},\"size\":398,\"modificationTime\":1603724043000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-map/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724039953,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"e52f2c3e-fac0-4b28-9627-2e33e6b85dc0\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"i\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"a\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"integer\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"b\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":\\\"byte\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"short\\\",\\\"valueType\\\":\\\"boolean\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"d\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"float\\\",\\\"valueType\\\":\\\"double\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"e\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"decimal(1,0)\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"f\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"val\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724039866}}\n{\"add\":{\"path\":\"part-00000-d9004e55-077b-4728-9ee6-b3401faa46ba-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3638,\"modificationTime\":1603724039000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-3d30d085-4cde-471e-a396-12af34a70812-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3655,\"modificationTime\":1603724039000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-nested-struct/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724040818,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"975ef365-8dec-4bbf-ab88-264c10987001\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"acb\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"b\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724040747}}\n{\"add\":{\"path\":\"part-00000-f2547b28-9219-4628-8462-cc9c56edfebb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1432,\"modificationTime\":1603724040000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-0f755735-3b5b-449a-8f93-92a40d9f065d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1439,\"modificationTime\":1603724040000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-nullable-field-invalid-schema-key/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724041694,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"ab05c2c1-6f1c-421b-815b-0f04dbf34814\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"array_can_contain_null\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"string\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724041628}}\n{\"add\":{\"path\":\"part-00000-d1f74401-ecb8-494e-96d6-adb95ec7e1c2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":385,\"modificationTime\":1603724041000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-d6454547-1a50-4f43-910d-2f84c5aedae1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1603724041000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-partition-values/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1636147668568,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"as_int\\\",\\\"as_long\\\",\\\"as_byte\\\",\\\"as_short\\\",\\\"as_boolean\\\",\\\"as_float\\\",\\\"as_double\\\",\\\"as_string\\\",\\\"as_string_lit_null\\\",\\\"as_date\\\",\\\"as_timestamp\\\",\\\"as_big_decimal\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"3\",\"numOutputBytes\":\"5832\",\"numOutputRows\":\"3\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_boolean\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_string_lit_null\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_list_of_records\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"val\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_nested_struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"acb\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"as_int\",\"as_long\",\"as_byte\",\"as_short\",\"as_boolean\",\"as_float\",\"as_double\",\"as_string\",\"as_string_lit_null\",\"as_date\",\"as_timestamp\",\"as_big_decimal\"],\"configuration\":{},\"createdTime\":1636147666386}}\n{\"add\":{\"path\":\"as_int=0/as_long=0/as_byte=0/as_short=0/as_boolean=true/as_float=0.0/as_double=0.0/as_string=0/as_string_lit_null=null/as_date=2021-09-08/as_timestamp=2021-09-08%2011%253A11%253A11/as_big_decimal=0/part-00000-b9dc86ae-0134-4363-bd87-19cfb3403e9a.c000.snappy.parquet\",\"partitionValues\":{\"as_big_decimal\":\"0\",\"as_int\":\"0\",\"as_byte\":\"0\",\"as_long\":\"0\",\"as_date\":\"2021-09-08\",\"as_string\":\"0\",\"as_timestamp\":\"2021-09-08 11:11:11\",\"as_float\":\"0.0\",\"as_short\":\"0\",\"as_boolean\":\"true\",\"as_string_lit_null\":\"null\",\"as_double\":\"0.0\"},\"size\":1944,\"modificationTime\":1636147668000,\"dataChange\":true}}\n{\"add\":{\"path\":\"as_int=__HIVE_DEFAULT_PARTITION__/as_long=__HIVE_DEFAULT_PARTITION__/as_byte=__HIVE_DEFAULT_PARTITION__/as_short=__HIVE_DEFAULT_PARTITION__/as_boolean=__HIVE_DEFAULT_PARTITION__/as_float=__HIVE_DEFAULT_PARTITION__/as_double=__HIVE_DEFAULT_PARTITION__/as_string=__HIVE_DEFAULT_PARTITION__/as_string_lit_null=__HIVE_DEFAULT_PARTITION__/as_date=__HIVE_DEFAULT_PARTITION__/as_timestamp=__HIVE_DEFAULT_PARTITION__/as_big_decimal=__HIVE_DEFAULT_PARTITION__/part-00001-9ee474eb-385b-43cf-9acb-0fbed63e011c.c000.snappy.parquet\",\"partitionValues\":{\"as_big_decimal\":null,\"as_int\":null,\"as_byte\":null,\"as_long\":null,\"as_date\":null,\"as_string\":null,\"as_timestamp\":null,\"as_float\":null,\"as_short\":null,\"as_boolean\":null,\"as_string_lit_null\":null,\"as_double\":null},\"size\":1944,\"modificationTime\":1636147668000,\"dataChange\":true}}\n{\"add\":{\"path\":\"as_int=1/as_long=1/as_byte=1/as_short=1/as_boolean=false/as_float=1.0/as_double=1.0/as_string=1/as_string_lit_null=null/as_date=2021-09-08/as_timestamp=2021-09-08%2011%253A11%253A11/as_big_decimal=1/part-00001-cb007d48-a9f5-40e7-adbe-60920680770f.c000.snappy.parquet\",\"partitionValues\":{\"as_big_decimal\":\"1\",\"as_int\":\"1\",\"as_byte\":\"1\",\"as_long\":\"1\",\"as_date\":\"2021-09-08\",\"as_string\":\"1\",\"as_timestamp\":\"2021-09-08 11:11:11\",\"as_float\":\"1.0\",\"as_short\":\"1\",\"as_boolean\":\"false\",\"as_string_lit_null\":\"null\",\"as_double\":\"1.0\"},\"size\":1944,\"modificationTime\":1636147668000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-primitives/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1607520163636,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"5050\",\"numOutputRows\":\"11\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_boolean\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_binary\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1607520161353}}\n{\"add\":{\"path\":\"part-00000-4f2f0b9f-50b3-4e7b-96a1-e2bb0f246b06-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2482,\"modificationTime\":1607520163000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-09e47b80-36c2-4475-a810-fbd8e7994971-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2568,\"modificationTime\":1607520163000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-timestamp_ntz/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712333988110,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[\\\"tsNtzPartition\\\"]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"fecbfd56-6849-421b-8439-070f0d694787\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"tsNtz\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"tsNtzPartition\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"tsNtzPartition\"],\"configuration\":{},\"createdTime\":1712333987987}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"timestampNtz\"],\"writerFeatures\":[\"timestampNtz\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-timestamp_ntz/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712333992682,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"9\",\"numOutputBytes\":\"2940\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"39f277cb-1414-419a-b634-f6a983ed9b37\"}}\n{\"add\":{\"path\":\"tsNtzPartition=2013-07-05%2017%253A01%253A00.123456/part-00000-6240e68e-2304-449a-a1e6-0e24866d3508.c000.snappy.parquet\",\"partitionValues\":{\"tsNtzPartition\":\"2013-07-05 17:01:00.123456\"},\"size\":726,\"modificationTime\":1712333992612,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":3,\\\"tsNtz\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"maxValues\\\":{\\\"id\\\":3,\\\"tsNtz\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"tsNtz\\\":0}}\"}}\n{\"add\":{\"path\":\"tsNtzPartition=2021-11-18%2002%253A30%253A00.123456/part-00000-65fcd5cb-f2f3-44f4-96ef-f43825143ba9.c000.snappy.parquet\",\"partitionValues\":{\"tsNtzPartition\":\"2021-11-18 02:30:00.123456\"},\"size\":742,\"modificationTime\":1712333992666,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":0,\\\"tsNtz\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"id\\\":2,\\\"tsNtz\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"tsNtz\\\":1}}\"}}\n{\"add\":{\"path\":\"tsNtzPartition=__HIVE_DEFAULT_PARTITION__/part-00001-53fd3b3b-7773-459a-921c-bb64bf0bbd03.c000.snappy.parquet\",\"partitionValues\":{\"tsNtzPartition\":null},\"size\":742,\"modificationTime\":1712333992612,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":6,\\\"tsNtz\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"id\\\":8,\\\"tsNtz\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"tsNtz\\\":1}}\"}}\n{\"add\":{\"path\":\"tsNtzPartition=2013-07-05%2017%253A01%253A00.123456/part-00001-336e3e5f-a202-4bd9-b117-28d871bbb639.c000.snappy.parquet\",\"partitionValues\":{\"tsNtzPartition\":\"2013-07-05 17:01:00.123456\"},\"size\":730,\"modificationTime\":1712333992659,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":4,\\\"tsNtz\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"id\\\":5,\\\"tsNtz\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"tsNtz\\\":1}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-timestamp_ntz-id-mode/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712333993464,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[\\\"tsNtzPartition\\\"]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.columnMapping.mode\\\":\\\"id\\\",\\\"delta.columnMapping.maxColumnId\\\":\\\"3\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"1b8ef756-4197-4263-b9a2-4da05afa76af\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\"}},{\\\"name\\\":\\\"tsNtz\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\"}},{\\\"name\\\":\\\"tsNtzPartition\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698\\\"}}]}\",\"partitionColumns\":[\"tsNtzPartition\"],\"configuration\":{\"delta.columnMapping.mode\":\"id\",\"delta.columnMapping.maxColumnId\":\"3\"},\"createdTime\":1712333993412}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"timestampNtz\",\"columnMapping\"],\"writerFeatures\":[\"timestampNtz\",\"columnMapping\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-timestamp_ntz-id-mode/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712333994313,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"9\",\"numOutputBytes\":\"4832\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"a4cb2faa-bc24-4374-a1c6-99764812a400\"}}\n{\"add\":{\"path\":\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698=2013-07-05%2017%253A01%253A00.123456/part-00000-468b79b5-ef3e-40ee-b077-8d7b48ef8385.c000.snappy.parquet\",\"partitionValues\":{\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698\":\"2013-07-05 17:01:00.123456\"},\"size\":1199,\"modificationTime\":1712333994274,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":3,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"maxValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":3,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":0,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":0}}\"}}\n{\"add\":{\"path\":\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698=2021-11-18%2002%253A30%253A00.123456/part-00000-80e4d2e9-69f2-420e-8152-8d5bb810b259.c000.snappy.parquet\",\"partitionValues\":{\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698\":\"2021-11-18 02:30:00.123456\"},\"size\":1215,\"modificationTime\":1712333994310,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":0,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":2,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":0,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":1}}\"}}\n{\"add\":{\"path\":\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698=__HIVE_DEFAULT_PARTITION__/part-00001-047834e2-8a38-47ff-9f1c-01f94a618369.c000.snappy.parquet\",\"partitionValues\":{\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698\":null},\"size\":1215,\"modificationTime\":1712333994276,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":6,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":8,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":0,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":1}}\"}}\n{\"add\":{\"path\":\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698=2013-07-05%2017%253A01%253A00.123456/part-00001-94a2fe48-a4c5-4d3e-823c-d76b59b9f597.c000.snappy.parquet\",\"partitionValues\":{\"col-31f31113-4fc7-437c-b8e8-b7bca8a2f698\":\"2013-07-05 17:01:00.123456\"},\"size\":1203,\"modificationTime\":1712333994303,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":4,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":5,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"nullCount\\\":{\\\"col-10623d2b-e242-4e70-bda5-826469ad0d2a\\\":0,\\\"col-3095b00d-efaa-493e-b85d-8db894dffffc\\\":1}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-timestamp_ntz-name-mode/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712333994816,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[\\\"tsNtzPartition\\\"]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.columnMapping.mode\\\":\\\"name\\\",\\\"delta.columnMapping.maxColumnId\\\":\\\"3\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"a70ffe6d-47af-4356-9571-3b4a42168511\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\"}},{\\\"name\\\":\\\"tsNtz\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\"}},{\\\"name\\\":\\\"tsNtzPartition\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee\\\"}}]}\",\"partitionColumns\":[\"tsNtzPartition\"],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.columnMapping.maxColumnId\":\"3\"},\"createdTime\":1712333994784}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"timestampNtz\",\"columnMapping\"],\"writerFeatures\":[\"timestampNtz\",\"columnMapping\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-reader-timestamp_ntz-name-mode/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712333995537,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"9\",\"numOutputBytes\":\"4832\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"e8276040-535a-4261-826f-8d71cae6773d\"}}\n{\"add\":{\"path\":\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee=2013-07-05%2017%253A01%253A00.123456/part-00000-19009b69-d0d2-4c9c-9994-770c77ce5c1e.c000.snappy.parquet\",\"partitionValues\":{\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee\":\"2013-07-05 17:01:00.123456\"},\"size\":1199,\"modificationTime\":1712333995499,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":3,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"maxValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":3,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":0,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":0}}\"}}\n{\"add\":{\"path\":\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee=2021-11-18%2002%253A30%253A00.123456/part-00000-55eb3e92-fedb-4a0e-a327-d44ee8e356b2.c000.snappy.parquet\",\"partitionValues\":{\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee\":\"2021-11-18 02:30:00.123456\"},\"size\":1215,\"modificationTime\":1712333995535,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":0,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":2,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":0,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":1}}\"}}\n{\"add\":{\"path\":\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee=__HIVE_DEFAULT_PARTITION__/part-00001-4325cf1b-146e-4e85-b36f-ab9c4a9d8125.c000.snappy.parquet\",\"partitionValues\":{\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee\":null},\"size\":1215,\"modificationTime\":1712333995502,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":6,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":8,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2021-11-18T02:30:00.123\\\"},\\\"nullCount\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":0,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":1}}\"}}\n{\"add\":{\"path\":\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee=2013-07-05%2017%253A01%253A00.123456/part-00001-459a6750-6f78-44ff-9706-03448c1dde8b.c000.snappy.parquet\",\"partitionValues\":{\"col-805808af-d12a-42e5-a7ec-f1a99abb82ee\":\"2013-07-05 17:01:00.123456\"},\"size\":1203,\"modificationTime\":1712333995527,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":4,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"maxValues\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":5,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":\\\"2013-07-05T17:01:00.123\\\"},\\\"nullCount\\\":{\\\"col-70450211-7268-473d-95c1-6d05710dfafa\\\":0,\\\"col-c14bb714-7e9e-4229-8f81-5d395c21ce7f\\\":1}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-basic-stats-all-types/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847172421,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"3865\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"a56746fa-7289-4e45-8f2d-83c31b34d932\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1704847166710}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00001-93fc8b78-4b92-45c7-ad3f-bb766e6d2e28-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2738,\"modificationTime\":1704847171000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"as_int\\\":0,\\\"as_long\\\":0,\\\"as_byte\\\":0,\\\"as_short\\\":0,\\\"as_float\\\":0.0,\\\"as_double\\\":0.0,\\\"as_string\\\":\\\"0\\\",\\\"as_date\\\":\\\"2000-01-01\\\",\\\"as_timestamp\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"as_big_decimal\\\":0},\\\"maxValues\\\":{\\\"as_int\\\":0,\\\"as_long\\\":0,\\\"as_byte\\\":0,\\\"as_short\\\":0,\\\"as_float\\\":0.0,\\\"as_double\\\":0.0,\\\"as_string\\\":\\\"0\\\",\\\"as_date\\\":\\\"2000-01-01\\\",\\\"as_timestamp\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"as_big_decimal\\\":0},\\\"nullCount\\\":{\\\"as_int\\\":0,\\\"as_long\\\":0,\\\"as_byte\\\":0,\\\"as_short\\\":0,\\\"as_float\\\":0,\\\"as_double\\\":0,\\\"as_string\\\":0,\\\"as_date\\\":0,\\\"as_timestamp\\\":0,\\\"as_big_decimal\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-basic-stats-all-types-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847197221,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"3865\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"101278f9-eaad-4623-9341-e7a0441e6f56\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"1\"},\"createdTime\":1704847196699}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00001-ed0f17f3-dab5-4131-8ff8-5a5f4399d0ef-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2738,\"modificationTime\":1704847197000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"as_int\\\":0,\\\"as_long\\\":0,\\\"as_byte\\\":0,\\\"as_short\\\":0,\\\"as_float\\\":0.0,\\\"as_double\\\":0.0,\\\"as_string\\\":\\\"0\\\",\\\"as_date\\\":\\\"2000-01-01\\\",\\\"as_timestamp\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"as_big_decimal\\\":0},\\\"maxValues\\\":{\\\"as_int\\\":0,\\\"as_long\\\":0,\\\"as_byte\\\":0,\\\"as_short\\\":0,\\\"as_float\\\":0.0,\\\"as_double\\\":0.0,\\\"as_string\\\":\\\"0\\\",\\\"as_date\\\":\\\"2000-01-01\\\",\\\"as_timestamp\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"as_big_decimal\\\":0},\\\"nullCount\\\":{\\\"as_int\\\":0,\\\"as_long\\\":0,\\\"as_byte\\\":0,\\\"as_short\\\":0,\\\"as_float\\\":0,\\\"as_double\\\":0,\\\"as_string\\\":0,\\\"as_date\\\":0,\\\"as_timestamp\\\":0,\\\"as_big_decimal\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-basic-stats-all-types-columnmapping-id/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847194834,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"7974\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"6ee73132-d762-4f51-92f2-b21dd90d75be\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-57160bcb-7e76-4076-bfcf-bd8f51835098\\\"}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-176e8b51-c3b4-411a-b4ba-7e9e47683d42\\\"}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-dc5127f8-f533-44b2-b17b-c50e16f7d83f\\\"}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.physicalName\\\":\\\"col-79b0e87b-98d5-443a-86e4-1ae4a81bba3d\\\"}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.physicalName\\\":\\\"col-2cfa4761-5937-41fb-a388-d60cdfb38987\\\"}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.physicalName\\\":\\\"col-7a5ff7b6-b289-4935-8939-01cdd5f1d011\\\"}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.physicalName\\\":\\\"col-c1358712-7cd8-4069-b0d0-c8f32297fbc9\\\"}},{\\\"name\\\":\\\"as_date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.physicalName\\\":\\\"col-01ea1e58-bb43-4b64-afbf-7b9b1a08b232\\\"}},{\\\"name\\\":\\\"as_timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.physicalName\\\":\\\"col-b396c7aa-feb5-451a-a7ce-9123986b7723\\\"}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.physicalName\\\":\\\"col-e1cb6194-e2f7-4014-adb6-1821881014dc\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"id\",\"delta.columnMapping.maxColumnId\":\"10\"},\"createdTime\":1704847193977}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"add\":{\"path\":\"part-00001-4596bea2-786f-404e-bc15-5adc99f00e30-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4949,\"modificationTime\":1704847194000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-57160bcb-7e76-4076-bfcf-bd8f51835098\\\":0,\\\"col-176e8b51-c3b4-411a-b4ba-7e9e47683d42\\\":0,\\\"col-dc5127f8-f533-44b2-b17b-c50e16f7d83f\\\":0,\\\"col-79b0e87b-98d5-443a-86e4-1ae4a81bba3d\\\":0,\\\"col-2cfa4761-5937-41fb-a388-d60cdfb38987\\\":0.0,\\\"col-7a5ff7b6-b289-4935-8939-01cdd5f1d011\\\":0.0,\\\"col-c1358712-7cd8-4069-b0d0-c8f32297fbc9\\\":\\\"0\\\",\\\"col-01ea1e58-bb43-4b64-afbf-7b9b1a08b232\\\":\\\"2000-01-01\\\",\\\"col-b396c7aa-feb5-451a-a7ce-9123986b7723\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"col-e1cb6194-e2f7-4014-adb6-1821881014dc\\\":0},\\\"maxValues\\\":{\\\"col-57160bcb-7e76-4076-bfcf-bd8f51835098\\\":0,\\\"col-176e8b51-c3b4-411a-b4ba-7e9e47683d42\\\":0,\\\"col-dc5127f8-f533-44b2-b17b-c50e16f7d83f\\\":0,\\\"col-79b0e87b-98d5-443a-86e4-1ae4a81bba3d\\\":0,\\\"col-2cfa4761-5937-41fb-a388-d60cdfb38987\\\":0.0,\\\"col-7a5ff7b6-b289-4935-8939-01cdd5f1d011\\\":0.0,\\\"col-c1358712-7cd8-4069-b0d0-c8f32297fbc9\\\":\\\"0\\\",\\\"col-01ea1e58-bb43-4b64-afbf-7b9b1a08b232\\\":\\\"2000-01-01\\\",\\\"col-b396c7aa-feb5-451a-a7ce-9123986b7723\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"col-e1cb6194-e2f7-4014-adb6-1821881014dc\\\":0},\\\"nullCount\\\":{\\\"col-57160bcb-7e76-4076-bfcf-bd8f51835098\\\":0,\\\"col-176e8b51-c3b4-411a-b4ba-7e9e47683d42\\\":0,\\\"col-dc5127f8-f533-44b2-b17b-c50e16f7d83f\\\":0,\\\"col-79b0e87b-98d5-443a-86e4-1ae4a81bba3d\\\":0,\\\"col-2cfa4761-5937-41fb-a388-d60cdfb38987\\\":0,\\\"col-7a5ff7b6-b289-4935-8939-01cdd5f1d011\\\":0,\\\"col-c1358712-7cd8-4069-b0d0-c8f32297fbc9\\\":0,\\\"col-01ea1e58-bb43-4b64-afbf-7b9b1a08b232\\\":0,\\\"col-b396c7aa-feb5-451a-a7ce-9123986b7723\\\":0,\\\"col-e1cb6194-e2f7-4014-adb6-1821881014dc\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-basic-stats-all-types-columnmapping-name/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847190800,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"7974\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"0ab95e02-ce9c-463b-9bfd-640d92be62c0\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-51c5f1e8-f3d9-4504-86fb-003ea4d1b703\\\"}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-e0a075f2-1847-4b21-a0f7-3974f2442f08\\\"}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-3101512b-834d-41e9-83b6-e7a2c0ef318a\\\"}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.physicalName\\\":\\\"col-d89267e7-eaa8-492b-81c6-9b36fb9e3434\\\"}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.physicalName\\\":\\\"col-44632733-0978-43d3-bcb5-872df4fb3ace\\\"}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.physicalName\\\":\\\"col-7303ee44-17dd-403f-bb0e-1ccd77c17fab\\\"}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.physicalName\\\":\\\"col-4d17e3b0-1801-48d1-89c8-76a280dd8224\\\"}},{\\\"name\\\":\\\"as_date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.physicalName\\\":\\\"col-5c129760-5d64-4673-a0df-434863f0ea1a\\\"}},{\\\"name\\\":\\\"as_timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.physicalName\\\":\\\"col-7296be94-39f4-4703-a791-e1ce50396a17\\\"}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.physicalName\\\":\\\"col-715bf6a4-ad83-483e-9703-01a3e9fc23fb\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.columnMapping.maxColumnId\":\"10\"},\"createdTime\":1704847189822}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"add\":{\"path\":\"part-00001-97ba0cfd-25fe-4911-a28f-29d37288fdd0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4949,\"modificationTime\":1704847190000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-51c5f1e8-f3d9-4504-86fb-003ea4d1b703\\\":0,\\\"col-e0a075f2-1847-4b21-a0f7-3974f2442f08\\\":0,\\\"col-3101512b-834d-41e9-83b6-e7a2c0ef318a\\\":0,\\\"col-d89267e7-eaa8-492b-81c6-9b36fb9e3434\\\":0,\\\"col-44632733-0978-43d3-bcb5-872df4fb3ace\\\":0.0,\\\"col-7303ee44-17dd-403f-bb0e-1ccd77c17fab\\\":0.0,\\\"col-4d17e3b0-1801-48d1-89c8-76a280dd8224\\\":\\\"0\\\",\\\"col-5c129760-5d64-4673-a0df-434863f0ea1a\\\":\\\"2000-01-01\\\",\\\"col-7296be94-39f4-4703-a791-e1ce50396a17\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"col-715bf6a4-ad83-483e-9703-01a3e9fc23fb\\\":0},\\\"maxValues\\\":{\\\"col-51c5f1e8-f3d9-4504-86fb-003ea4d1b703\\\":0,\\\"col-e0a075f2-1847-4b21-a0f7-3974f2442f08\\\":0,\\\"col-3101512b-834d-41e9-83b6-e7a2c0ef318a\\\":0,\\\"col-d89267e7-eaa8-492b-81c6-9b36fb9e3434\\\":0,\\\"col-44632733-0978-43d3-bcb5-872df4fb3ace\\\":0.0,\\\"col-7303ee44-17dd-403f-bb0e-1ccd77c17fab\\\":0.0,\\\"col-4d17e3b0-1801-48d1-89c8-76a280dd8224\\\":\\\"0\\\",\\\"col-5c129760-5d64-4673-a0df-434863f0ea1a\\\":\\\"2000-01-01\\\",\\\"col-7296be94-39f4-4703-a791-e1ce50396a17\\\":\\\"2000-01-01T00:00:00.000-08:00\\\",\\\"col-715bf6a4-ad83-483e-9703-01a3e9fc23fb\\\":0},\\\"nullCount\\\":{\\\"col-51c5f1e8-f3d9-4504-86fb-003ea4d1b703\\\":0,\\\"col-e0a075f2-1847-4b21-a0f7-3974f2442f08\\\":0,\\\"col-3101512b-834d-41e9-83b6-e7a2c0ef318a\\\":0,\\\"col-d89267e7-eaa8-492b-81c6-9b36fb9e3434\\\":0,\\\"col-44632733-0978-43d3-bcb5-872df4fb3ace\\\":0,\\\"col-7303ee44-17dd-403f-bb0e-1ccd77c17fab\\\":0,\\\"col-4d17e3b0-1801-48d1-89c8-76a280dd8224\\\":0,\\\"col-5c129760-5d64-4673-a0df-434863f0ea1a\\\":0,\\\"col-7296be94-39f4-4703-a791-e1ce50396a17\\\":0,\\\"col-715bf6a4-ad83-483e-9703-01a3e9fc23fb\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-change-stats-collected-across-versions/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847199416,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1065\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"e12d6dfe-db6a-4039-9fcf-6c13606d416d\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1704847199041}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00001-c09e5ddb-2337-4e49-b8be-83fd96008375-c000.snappy.parquet\",\"partitionValues\":{},\"size\":684,\"modificationTime\":1704847199000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-change-stats-collected-across-versions/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847201143,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.dataSkippingNumIndexedCols\\\":\\\"1\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"992a1ee1-a32a-432c-9721-1c22c01cb90a\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.dataSkippingNumIndexedCols\":\"1\"},\"createdTime\":1704847199041}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-change-stats-collected-across-versions/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847203311,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1065\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"307a5044-5868-4c05-9356-694a9c181a57\"}}\n{\"add\":{\"path\":\"part-00001-cb335794-98b0-43c3-a3a1-a4c86e3da38d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":684,\"modificationTime\":1704847203000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col1\\\":0},\\\"maxValues\\\":{\\\"col1\\\":0},\\\"nullCount\\\":{\\\"col1\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-change-stats-collected-across-versions/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847204921,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.dataSkippingNumIndexedCols\\\":\\\"0\\\"}\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"aaeb218d-6c17-41bb-a36d-35d75507b2c4\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.dataSkippingNumIndexedCols\":\"0\"},\"createdTime\":1704847199041}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-change-stats-collected-across-versions/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847206522,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1065\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"7f7a7132-aba4-49f2-b618-aab176e978a7\"}}\n{\"add\":{\"path\":\"part-00001-e5d736b6-2ecd-457a-8bb2-947b61f9c67e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":684,\"modificationTime\":1704847206000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-partition-and-data-column/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847208280,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1055\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"0003f637-f343-4c70-80bd-be1c8e7b0c59\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1704847207926}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00001-6dedd756-e903-46d7-9e6c-01b3c4ebeab3-c000.snappy.parquet\",\"partitionValues\":{},\"size\":678,\"modificationTime\":1704847208000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"part\\\":1,\\\"id\\\":0},\\\"maxValues\\\":{\\\"part\\\":1,\\\"id\\\":0},\\\"nullCount\\\":{\\\"part\\\":0,\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-partition-and-data-column/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847209741,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1055\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"4a790d64-c653-421f-9423-137c982d9810\"}}\n{\"add\":{\"path\":\"part-00001-2c0ee02a-8591-4026-a5ab-952bdb347fc5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":678,\"modificationTime\":1704847209000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"part\\\":1,\\\"id\\\":1},\\\"maxValues\\\":{\\\"part\\\":1,\\\"id\\\":1},\\\"nullCount\\\":{\\\"part\\\":0,\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-partition-and-data-column/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847211152,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1055\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"ade7341b-c742-4861-afdd-43cfa39aaa54\"}}\n{\"add\":{\"path\":\"part-00001-442a6473-8d9a-41d3-8172-e2248e8be169-c000.snappy.parquet\",\"partitionValues\":{},\"size\":678,\"modificationTime\":1704847211000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"part\\\":0,\\\"id\\\":1},\\\"maxValues\\\":{\\\"part\\\":0,\\\"id\\\":1},\\\"nullCount\\\":{\\\"part\\\":0,\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/data-skipping-partition-and-data-column/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704847212753,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1055\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"91386c19-6a82-4708-bbe1-3049e2dbfd57\"}}\n{\"add\":{\"path\":\"part-00001-2822cff2-34ab-4b93-9cbb-4e751084a422-c000.snappy.parquet\",\"partitionValues\":{},\"size\":678,\"modificationTime\":1704847212000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"part\\\":0,\\\"id\\\":0},\\\"maxValues\\\":{\\\"part\\\":0,\\\"id\\\":0},\\\"nullCount\\\":{\\\"part\\\":0,\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/decimal-various-scale-precision/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1717778521300,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"3\",\"numOutputBytes\":\"9126\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"5e3bfa16-cf0f-4d40-ad7d-b6426a6b4b7a\"}}\n{\"metaData\":{\"id\":\"7f750aff-9bf2-4e52-bfce-39811932da26\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"decimal_4_0\\\",\\\"type\\\":\\\"decimal(4,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_7_0\\\",\\\"type\\\":\\\"decimal(7,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_7_6\\\",\\\"type\\\":\\\"decimal(7,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_12_0\\\",\\\"type\\\":\\\"decimal(12,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_12_6\\\",\\\"type\\\":\\\"decimal(12,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_15_0\\\",\\\"type\\\":\\\"decimal(15,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_15_6\\\",\\\"type\\\":\\\"decimal(15,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_15_12\\\",\\\"type\\\":\\\"decimal(15,12)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_18_0\\\",\\\"type\\\":\\\"decimal(18,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_18_6\\\",\\\"type\\\":\\\"decimal(18,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_18_12\\\",\\\"type\\\":\\\"decimal(18,12)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_25_0\\\",\\\"type\\\":\\\"decimal(25,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_25_6\\\",\\\"type\\\":\\\"decimal(25,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_25_12\\\",\\\"type\\\":\\\"decimal(25,12)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_25_18\\\",\\\"type\\\":\\\"decimal(25,18)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_25_24\\\",\\\"type\\\":\\\"decimal(25,24)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_35_0\\\",\\\"type\\\":\\\"decimal(35,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_35_6\\\",\\\"type\\\":\\\"decimal(35,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_35_12\\\",\\\"type\\\":\\\"decimal(35,12)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_35_18\\\",\\\"type\\\":\\\"decimal(35,18)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_35_24\\\",\\\"type\\\":\\\"decimal(35,24)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_35_30\\\",\\\"type\\\":\\\"decimal(35,30)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_0\\\",\\\"type\\\":\\\"decimal(38,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_6\\\",\\\"type\\\":\\\"decimal(38,6)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_12\\\",\\\"type\\\":\\\"decimal(38,12)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_18\\\",\\\"type\\\":\\\"decimal(38,18)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_24\\\",\\\"type\\\":\\\"decimal(38,24)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_30\\\",\\\"type\\\":\\\"decimal(38,30)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_38_36\\\",\\\"type\\\":\\\"decimal(38,36)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1717778519308}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-bb4b3e59-ddb9-4d26-beaf-de9554e14517-c000.snappy.parquet\",\"partitionValues\":{},\"size\":9126,\"modificationTime\":1717778521237,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"decimal_4_0\\\":-13,\\\"decimal_7_0\\\":0,\\\"decimal_12_0\\\":0,\\\"decimal_12_6\\\":-0.000098,\\\"decimal_15_0\\\":-157,\\\"decimal_15_6\\\":-3.346000,\\\"decimal_15_12\\\":-0.002162000000,\\\"decimal_18_0\\\":0,\\\"decimal_18_6\\\":-22641.000000,\\\"decimal_18_12\\\":-5.190000000000,\\\"decimal_25_0\\\":0,\\\"decimal_25_6\\\":-0.000013,\\\"decimal_25_12\\\":-3.1661E-8,\\\"decimal_25_18\\\":-24199.000000000000000000,\\\"decimal_35_0\\\":0,\\\"decimal_35_6\\\":-0.000161,\\\"decimal_35_12\\\":-2.59176E-7,\\\"decimal_35_18\\\":-1.36744000E-10,\\\"decimal_35_24\\\":-22827907.000000000000000000000000,\\\"decimal_35_30\\\":-32805.309000000000000000000000000000,\\\"decimal_38_0\\\":-17,\\\"decimal_38_6\\\":-0.027994,\\\"decimal_38_12\\\":-0.000024695819,\\\"decimal_38_18\\\":-4.614771000E-9,\\\"decimal_38_24\\\":-9.718032000000E-12,\\\"decimal_38_30\\\":-2.6626087000000000E-14,\\\"decimal_38_36\\\":-2.9546424000000000000E-17},\\\"maxValues\\\":{\\\"decimal_4_0\\\":4,\\\"decimal_7_0\\\":0,\\\"decimal_12_0\\\":0,\\\"decimal_12_6\\\":0.000062,\\\"decimal_15_0\\\":481,\\\"decimal_15_6\\\":3.302000,\\\"decimal_15_12\\\":0.001469000000,\\\"decimal_18_0\\\":0,\\\"decimal_18_6\\\":7998.000000,\\\"decimal_18_12\\\":10.994000000000,\\\"decimal_25_0\\\":0,\\\"decimal_25_6\\\":0.000021,\\\"decimal_25_12\\\":5.925E-9,\\\"decimal_25_18\\\":234942.000000000000000000,\\\"decimal_35_0\\\":0,\\\"decimal_35_6\\\":0.000161,\\\"decimal_35_12\\\":1.65519E-7,\\\"decimal_35_18\\\":1.52896000E-10,\\\"decimal_35_24\\\":14797356.000000000000000000000000,\\\"decimal_35_30\\\":8083.687000000000000000000000000000,\\\"decimal_38_0\\\":26,\\\"decimal_38_6\\\":0.021882,\\\"decimal_38_12\\\":0.000032950993,\\\"decimal_38_18\\\":1.2783803000E-8,\\\"decimal_38_24\\\":2.395564000000E-12,\\\"decimal_38_30\\\":2.9414203000000000E-14,\\\"decimal_38_36\\\":3.241836000000000000E-18},\\\"nullCount\\\":{\\\"decimal_4_0\\\":1,\\\"decimal_7_0\\\":1,\\\"decimal_7_6\\\":3,\\\"decimal_12_0\\\":1,\\\"decimal_12_6\\\":1,\\\"decimal_15_0\\\":1,\\\"decimal_15_6\\\":1,\\\"decimal_15_12\\\":1,\\\"decimal_18_0\\\":1,\\\"decimal_18_6\\\":1,\\\"decimal_18_12\\\":1,\\\"decimal_25_0\\\":1,\\\"decimal_25_6\\\":1,\\\"decimal_25_12\\\":1,\\\"decimal_25_18\\\":1,\\\"decimal_25_24\\\":3,\\\"decimal_35_0\\\":1,\\\"decimal_35_6\\\":1,\\\"decimal_35_12\\\":1,\\\"decimal_35_18\\\":1,\\\"decimal_35_24\\\":1,\\\"decimal_35_30\\\":1,\\\"decimal_38_0\\\":1,\\\"decimal_38_6\\\":1,\\\"decimal_38_12\\\":1,\\\"decimal_38_18\\\":1,\\\"decimal_38_24\\\":1,\\\"decimal_38_30\\\":1,\\\"decimal_38_36\\\":1}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/delete-re-add-same-file-different-transactions/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697064953062,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"fc238989-fb88-4fd1-8be4-328e0b719260\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"intCol\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"foo\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1600000000000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/delete-re-add-same-file-different-transactions/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697064967361,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1bd8568f-6a12-41ac-983c-cd5e1b4e1caa\"}}\n{\"remove\":{\"path\":\"foo\",\"deletionTimestamp\":1697064967349,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/delete-re-add-same-file-different-transactions/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697064970033,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"2c2bb7f0-5a0f-4670-80b0-f787f7b23a80\"}}\n{\"add\":{\"path\":\"foo\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1700000000000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/delete-re-add-same-file-different-transactions/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697064972273,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"52827868-a12a-4ea7-9d38-24bfe5e94e05\"}}\n{\"add\":{\"path\":\"bar\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1697064972263,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-commit-info/_delta_log/00000000000000000000.json",
    "content": "{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1607452026918}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"commitInfo\":{\"timestamp\":1540415658000,\"userId\":\"user_0\",\"userName\":\"username_0\",\"operation\":\"WRITE\",\"operationParameters\":{\"test\":\"test\"},\"job\":{\"jobId\":\"job_id_0\",\"jobName\":\"job_name_0\",\"runId\":\"run_id_0\",\"jobOwnerId\":\"job_owner_0\",\"triggerType\":\"trigger_type_0\"},\"notebook\":{\"notebookId\":\"notebook_id_0\"},\"clusterId\":\"cluster_id_0\",\"readVersion\":-1,\"isolationLevel\":\"default\",\"isBlindAppend\":true,\"operationMetrics\":{\"test\":\"test\"},\"userMetadata\":\"foo\"}}\n{\"add\":{\"path\":\"abc\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-getChanges/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704392842074,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"95ec924a-6859-4433-8008-6d6b4a0e3ba5\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"fake/path/1\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-getChanges/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704392846030,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"01d40235-c8b4-4f8e-8f19-8c97872217fd\"}}\n{\"cdc\":{\"path\":\"fake/path/2\",\"partitionValues\":{\"partition_foo\":\"partition_bar\"},\"size\":1,\"tags\":{\"tag_foo\":\"tag_bar\"},\"dataChange\":false}}\n{\"remove\":{\"path\":\"fake/path/1\",\"deletionTimestamp\":100,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-getChanges/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1704392846603,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"6cef7579-ca93-4427-988e-9269e8db50c7\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"txn\":{\"appId\":\"fakeAppId\",\"version\":3,\"lastUpdated\":200}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-invalid-protocol-version/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":99,\"minWriterVersion\":7,\"readerFeatures\":[],\"writerFeatures\":[]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"abc\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724008326,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"d847eb65-8196-4f17-b2f8-021454e7a6b9\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724008326}}\n{\"add\":{\"path\":\"0\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724009009,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"1\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724009827,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":1,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"2\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724010438,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"3\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724011089,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":3,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"4\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724011784,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":4,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"5\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724012518,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":5,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"6\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724013308,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":6,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"7\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724014139,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":7,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"8\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724015017,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":8,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"9\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724016018,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":9,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"10\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-metadata/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":13}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723997752,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"8e276544-6bc2-4935-ac73-873ff9347d05\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723997752}}\n{\"add\":{\"path\":\"0\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723998268,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"1\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723998848,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":1,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"2\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723999470,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"3\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724000137,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":3,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"4\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724000823,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":4,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"5\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724001567,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":5,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"6\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724002323,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":6,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"7\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724003208,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":7,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"8\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724004087,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":8,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"9\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724005049,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":9,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"10\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-from-checkpoint-missing-protocol/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":13}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-without-metadata/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[],\"writerFeatures\":[]}}\n{\"add\":{\"path\":\"abc\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/deltalog-state-reconstruction-without-protocol/_delta_log/00000000000000000000.json",
    "content": "{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"intCol\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"abc\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691721441,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"20\",\"numOutputRows\":\"50\",\"numOutputBytes\":\"14678\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"bc4bfee2-8faf-4995-bfa1-a3c4930cbf22\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1688691715853}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"add\":{\"path\":\"part=0/part-00000-8387c699-30b1-4734-a791-9278d560ec19.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"0\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":0,\\\"col2\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col1\\\":20,\\\"col2\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=1/part-00000-a1586fa1-50e8-4f06-858a-b43b2e83010b.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"1\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":1,\\\"col2\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col1\\\":21,\\\"col2\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=2/part-00000-ad58cb56-93db-4374-91ba-e65e7fa68e76.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":2,\\\"col2\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col1\\\":22,\\\"col2\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=3/part-00000-319bea86-657f-4431-9b26-949dba99cf2c.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"3\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":3,\\\"col2\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col1\\\":23,\\\"col2\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=4/part-00000-69ec928d-3737-4eb3-a3d8-9555a6b55ff5.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"4\"},\"size\":735,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":4,\\\"col2\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col1\\\":24,\\\"col2\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=5/part-00000-5c963f16-d5b8-4f8b-8d8a-0e3403228be2.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"5\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":5,\\\"col2\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col1\\\":15,\\\"col2\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=6/part-00000-be524334-115d-4d01-8614-e1bc8c630926.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"6\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":6,\\\"col2\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col1\\\":16,\\\"col2\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=7/part-00000-33cc19fc-3607-4ea7-ab6d-af4e3ebf62c4.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"7\"},\"size\":732,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":7,\\\"col2\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col1\\\":17,\\\"col2\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=8/part-00000-02c66988-3465-4483-9f85-7155e6aee1f4.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"8\"},\"size\":732,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":8,\\\"col2\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col1\\\":18,\\\"col2\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=9/part-00000-e4012c8c-cc60-44c0-babb-8c5d264a3a31.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"9\"},\"size\":732,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":9,\\\"col2\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col1\\\":19,\\\"col2\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=0/part-00001-24cdbe06-d3dc-449f-bd38-575228ca42a7.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"0\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":30,\\\"col2\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col1\\\":40,\\\"col2\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=1/part-00001-d7e5d32a-55fa-410a-afee-adcdf46bc859.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"1\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":31,\\\"col2\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col1\\\":41,\\\"col2\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=2/part-00001-ab1247be-1f77-41e6-a392-50a99b2db864.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":32,\\\"col2\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col1\\\":42,\\\"col2\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=3/part-00001-afeef1dd-2517-49b9-873e-e9e6e8a74b19.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"3\"},\"size\":731,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":33,\\\"col2\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col1\\\":43,\\\"col2\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=4/part-00001-e63d3db6-9e97-4472-aacc-6af9fa44e73d.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"4\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":34,\\\"col2\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col1\\\":44,\\\"col2\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=5/part-00001-f344b457-fbd0-4bc4-9502-2c07025e5bb1.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"5\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":25,\\\"col2\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col1\\\":45,\\\"col2\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=6/part-00001-6fc16401-ac51-4b89-bf08-bb86cecb5cc2.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"6\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":26,\\\"col2\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col1\\\":46,\\\"col2\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=7/part-00001-986abb06-e672-4134-83d4-261752b236b8.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"7\"},\"size\":736,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":27,\\\"col2\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col1\\\":47,\\\"col2\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=8/part-00001-7c58de64-d72f-4373-8d86-dfdc00fb264e.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"8\"},\"size\":736,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":28,\\\"col2\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col1\\\":48,\\\"col2\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part=9/part-00001-c0430af8-a8e0-4b23-8776-b2fc549b3e4e.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"9\"},\"size\":736,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":29,\\\"col2\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col1\\\":49,\\\"col2\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":true}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691744588,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#463 = 0)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"13565\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"33e9e33f-49f5-421c-bf62-c3e71c55cd32\"}}\n{\"add\":{\"path\":\"part=0/part-00000-8387c699-30b1-4734-a791-9278d560ec19.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"0\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":0,\\\"col2\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col1\\\":20,\\\"col2\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"0X9F0q2<2yJ-f)Gm2!e0\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=0/part-00000-8387c699-30b1-4734-a791-9278d560ec19.c000.snappy.parquet\",\"deletionTimestamp\":1688691741121,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"0\"},\"size\":736}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691752166,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#1939 = 2)\\\"]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"5256\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"a9487f5e-84c9-44d8-a6d6-f7444e8405a9\"}}\n{\"add\":{\"path\":\"part=2/part-00000-ad58cb56-93db-4374-91ba-e65e7fa68e76.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":2,\\\"col2\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col1\\\":22,\\\"col2\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"]m5]-UtmB0Rl<C?0(qO5\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=2/part-00000-ad58cb56-93db-4374-91ba-e65e7fa68e76.c000.snappy.parquet\",\"deletionTimestamp\":1688691750163,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"2\"},\"size\":736}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691758484,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#3223 = 4)\\\"]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"4361\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"c2f17198-9407-4c2d-a24d-f43ee33f74aa\"}}\n{\"add\":{\"path\":\"part=4/part-00000-69ec928d-3737-4eb3-a3d8-9555a6b55ff5.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"4\"},\"size\":735,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":4,\\\"col2\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col1\\\":24,\\\"col2\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"SZYoUv7OW&PVt[Vm4y.s\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=4/part-00000-69ec928d-3737-4eb3-a3d8-9555a6b55ff5.c000.snappy.parquet\",\"deletionTimestamp\":1688691757817,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"4\"},\"size\":735}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691762356,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#4505 = 6)\\\"]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2577\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"e3ef111c-fd87-4b14-a1a6-08ccdc62ca3e\"}}\n{\"add\":{\"path\":\"part=6/part-00000-be524334-115d-4d01-8614-e1bc8c630926.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"6\"},\"size\":732,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":6,\\\"col2\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col1\\\":16,\\\"col2\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"sl^HN8g+F2G%ix@bXmgx\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=6/part-00000-be524334-115d-4d01-8614-e1bc8c630926.c000.snappy.parquet\",\"deletionTimestamp\":1688691761909,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"6\"},\"size\":732}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691766019,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#5787 = 8)\\\"]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2416\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"b496350c-3b26-4f55-8340-d668a11556de\"}}\n{\"add\":{\"path\":\"part=8/part-00000-02c66988-3465-4483-9f85-7155e6aee1f4.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"8\"},\"size\":732,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col1\\\":8,\\\"col2\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col1\\\":18,\\\"col2\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"TG?RwHuR9iGfiz)NQ>$j\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=8/part-00000-02c66988-3465-4483-9f85-7155e6aee1f4.c000.snappy.parquet\",\"deletionTimestamp\":1688691765550,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"8\"},\"size\":732}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691770236,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#7069 = 10)\\\"]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"3110\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"7b578eea-e45c-4308-8c86-3c4c4f2bb9f9\"}}\n{\"add\":{\"path\":\"part=0/part-00000-8387c699-30b1-4734-a791-9278d560ec19.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"0\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":0,\\\"col2\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col1\\\":20,\\\"col2\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Zcq?bs*HKQWCB[Sjf[.o\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part=0/part-00000-8387c699-30b1-4734-a791-9278d560ec19.c000.snappy.parquet\",\"deletionTimestamp\":1688691769658,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"0\"},\"size\":736,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"0X9F0q2<2yJ-f)Gm2!e0\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691774168,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#8351 = 12)\\\"]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2645\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"970ae4f8-d941-4029-b282-20e244e00cd1\"}}\n{\"add\":{\"path\":\"part=2/part-00000-ad58cb56-93db-4374-91ba-e65e7fa68e76.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":2,\\\"col2\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col1\\\":22,\\\"col2\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"NZQXpd#3xxK!oBujr/=:\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part=2/part-00000-ad58cb56-93db-4374-91ba-e65e7fa68e76.c000.snappy.parquet\",\"deletionTimestamp\":1688691773564,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"2\"},\"size\":736,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"]m5]-UtmB0Rl<C?0(qO5\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691777427,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#9633 = 14)\\\"]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2022\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"b273bb0f-6f97-447d-9152-b3b73cce0e93\"}}\n{\"add\":{\"path\":\"part=4/part-00000-69ec928d-3737-4eb3-a3d8-9555a6b55ff5.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"4\"},\"size\":735,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":4,\\\"col2\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col1\\\":24,\\\"col2\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"+V9Oq([R6mTaoM.}Isa)\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part=4/part-00000-69ec928d-3737-4eb3-a3d8-9555a6b55ff5.c000.snappy.parquet\",\"deletionTimestamp\":1688691776979,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"4\"},\"size\":735,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"SZYoUv7OW&PVt[Vm4y.s\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691780488,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#10915 = 16)\\\"]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1575\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1050c09b-28d7-489b-8752-ddeb697eb6b8\"}}\n{\"remove\":{\"path\":\"part=6/part-00000-be524334-115d-4d01-8614-e1bc8c630926.c000.snappy.parquet\",\"deletionTimestamp\":1688691780390,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"6\"},\"size\":732,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"sl^HN8g+F2G%ix@bXmgx\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691783299,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#12197 = 18)\\\"]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1582\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"f1fe09aa-7b1e-42e8-85c6-459a8446f2d7\"}}\n{\"remove\":{\"path\":\"part=8/part-00000-02c66988-3465-4483-9f85-7155e6aee1f4.c000.snappy.parquet\",\"deletionTimestamp\":1688691783203,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"8\"},\"size\":732,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"TG?RwHuR9iGfiz)NQ>$j\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691788332,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#13579 = 20)\\\"]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2084\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ad17e138-a1c2-4b6a-a373-e334c138c9e0\"}}\n{\"remove\":{\"path\":\"part=0/part-00000-8387c699-30b1-4734-a791-9278d560ec19.c000.snappy.parquet\",\"deletionTimestamp\":1688691788291,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"0\"},\"size\":736,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Zcq?bs*HKQWCB[Sjf[.o\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000012.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691791603,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#15220 = 22)\\\"]\"},\"readVersion\":11,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1870\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"68c7ac7c-6e73-4aa4-864f-ef374e110de6\"}}\n{\"remove\":{\"path\":\"part=2/part-00000-ad58cb56-93db-4374-91ba-e65e7fa68e76.c000.snappy.parquet\",\"deletionTimestamp\":1688691791568,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"2\"},\"size\":736,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"NZQXpd#3xxK!oBujr/=:\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000013.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691796131,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#16548 = 24)\\\"]\"},\"readVersion\":12,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"3461\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"4e6f5590-7630-44d1-bc1c-5a2bfd637c4f\"}}\n{\"remove\":{\"path\":\"part=4/part-00000-69ec928d-3737-4eb3-a3d8-9555a6b55ff5.c000.snappy.parquet\",\"deletionTimestamp\":1688691796068,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"4\"},\"size\":735,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"+V9Oq([R6mTaoM.}Isa)\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000014.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691798638,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#17876 = 26)\\\"]\"},\"readVersion\":13,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1451\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"34f3a5ca-2166-4b49-a92b-f6b70ce4cd89\"}}\n{\"add\":{\"path\":\"part=6/part-00001-6fc16401-ac51-4b89-bf08-bb86cecb5cc2.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"6\"},\"size\":736,\"modificationTime\":1688691720000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":26,\\\"col2\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col1\\\":46,\\\"col2\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"]g#Gi8g29gLy&KMkGJr?\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=6/part-00001-6fc16401-ac51-4b89-bf08-bb86cecb5cc2.c000.snappy.parquet\",\"deletionTimestamp\":1688691798060,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"6\"},\"size\":736}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/00000000000000000015.json",
    "content": "{\"commitInfo\":{\"timestamp\":1688691801431,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#19379 = 28)\\\"]\"},\"readVersion\":14,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1804\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ace020dc-2202-4814-bbd4-36fc7a2a7988\"}}\n{\"add\":{\"path\":\"part=8/part-00001-7c58de64-d72f-4373-8d86-dfdc00fb264e.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"8\"},\"size\":736,\"modificationTime\":1688691721000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col1\\\":28,\\\"col2\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col1\\\":48,\\\"col2\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"7NnC4LX-RqU.S:B4VD9n\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part=8/part-00001-7c58de64-d72f-4373-8d86-dfdc00fb264e.c000.snappy.parquet\",\"deletionTimestamp\":1688691800672,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"part\":\"8\"},\"size\":736}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-partitioned-with-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":30,\"sizeInBytes\":20573,\"numOfAddFiles\":18,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues_parsed\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"part\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"822eda70bb966b38d646f351ea753463\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177838187,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"20\",\"numOutputRows\":\"50\",\"numOutputBytes\":\"24078\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ccd115ea-c565-49a1-8b44-d97619864edc\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\\\"}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\"}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\"}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.enableDeletionVectors\":\"true\",\"delta.columnMapping.maxColumnId\":\"3\"},\"createdTime\":1697177835151}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\",\"columnMapping\"],\"writerFeatures\":[\"deletionVectors\",\"columnMapping\"]}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00000-d1888b8a-150e-4fe3-a397-1514739499b4.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":20,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=1/part-00000-19513938-badc-4bd4-9513-3d043d1491dc.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"1\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":1,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":21,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00000-3f054c46-7f8f-45f3-a541-25525787b631.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":2,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":22,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=3/part-00000-1a0ac64e-0ce2-493e-b1b0-6cf15c1988f5.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"3\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":3,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":23,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00000-dbfb557b-b778-454e-bef4-bab9481bcea7.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1205,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":4,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":24,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=5/part-00000-6d057276-2da0-45c3-86eb-aed7fd3429b8.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"5\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":5,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":15,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00000-c0ca807e-59eb-4c84-a67d-c65a2e03c3c5.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":6,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":16,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=7/part-00000-12e816f9-daa3-4197-98f2-217a983bdafd.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"7\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":7,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":17,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00000-ee6122b8-1474-4764-8bdf-8f8b95c734af.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":8,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":18,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=9/part-00000-f2e5dc2f-b7c6-4772-85d7-23b273a9e54d.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"9\"},\"size\":1202,\"modificationTime\":1697177838000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":9,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":19,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00001-0e48fbde-daec-44ff-b579-d5c49b6c827f.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":30,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":40,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=1/part-00001-fb5e7c74-75ab-4bee-8234-400040ae127a.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"1\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":31,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":41,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00001-4825f848-06bb-4b91-94e8-deb40f05feca.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":32,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":42,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=3/part-00001-d1fc7b93-6ec3-4c75-8363-ffd8f1f43420.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"3\"},\"size\":1201,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":33,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":43,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00001-ee7c50c6-4119-41ff-9c0a-285f844e7c31.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":34,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":44,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=5/part-00001-ee535eb0-972e-470f-b705-61884acbbe39.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"5\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":25,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":45,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00001-8c38a718-ea0d-4ac1-9515-3a6ec23cc86b.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":26,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":46,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=7/part-00001-e2c8fd65-f478-4738-89f2-b4f63bdc166f.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"7\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":27,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":47,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00001-0878dadb-c875-4347-92a3-8739c303d7bd.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":28,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":48,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=9/part-00001-8bbcb266-0863-4b31-adc0-e1c4d1194cec.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"9\"},\"size\":1206,\"modificationTime\":1697177838000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":29,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":49,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":true}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177845210,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#493 = 0)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2463\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"d0e5f096-0e13-4e72-a6c1-97e4d4786b8d\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00000-d1888b8a-150e-4fe3-a397-1514739499b4.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":20,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"5DxE90SCJoMhWOS&LEbJ\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00000-d1888b8a-150e-4fe3-a397-1514739499b4.c000.snappy.parquet\",\"deletionTimestamp\":1697177844817,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1206,\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177847623,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#1991 = 2)\\\"]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1314\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"d4007f70-5f27-40a0-9c4e-47e579230a67\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00000-3f054c46-7f8f-45f3-a541-25525787b631.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":2,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":22,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"&9sA9{u>E#S[qd3XLIaZ\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00000-3f054c46-7f8f-45f3-a541-25525787b631.c000.snappy.parquet\",\"deletionTimestamp\":1697177847357,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1206,\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177849801,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#3297 = 4)\\\"]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1201\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ec2e636d-e0a8-4936-b754-ef2175810a1a\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00000-dbfb557b-b778-454e-bef4-bab9481bcea7.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1205,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":4,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":24,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"bmfc5TmwY>TJV$^4Zs$+\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00000-dbfb557b-b778-454e-bef4-bab9481bcea7.c000.snappy.parquet\",\"deletionTimestamp\":1697177849598,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1205,\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177851920,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#4601 = 6)\\\"]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1180\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"542a35b5-23c2-4193-96df-9e23284a1ad1\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00000-c0ca807e-59eb-4c84-a67d-c65a2e03c3c5.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":6,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":16,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"GOBG7L]@&bKr%xe4PF6?\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00000-c0ca807e-59eb-4c84-a67d-c65a2e03c3c5.c000.snappy.parquet\",\"deletionTimestamp\":1697177851705,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1202,\"stats\":\"{\\\"numRecords\\\":2}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177853860,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#5905 = 8)\\\"]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1119\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"b9b62b66-67fe-4e93-be55-4a6d077ec1b9\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00000-ee6122b8-1474-4764-8bdf-8f8b95c734af.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1202,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":8,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":18,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m?5^MJ=4G^SC%md8dbcb\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00000-ee6122b8-1474-4764-8bdf-8f8b95c734af.c000.snappy.parquet\",\"deletionTimestamp\":1697177853670,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1202,\"stats\":\"{\\\"numRecords\\\":2}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177855845,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#7209 = 10)\\\"]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1181\",\"numDeletionVectorsUpdated\":\"1\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"f211797b-f38e-4b65-bcec-e24c59d34b97\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00000-d1888b8a-150e-4fe3-a397-1514739499b4.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":20,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo0\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"0Mb{.bIUQYOtd%By%1f@\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00000-d1888b8a-150e-4fe3-a397-1514739499b4.c000.snappy.parquet\",\"deletionTimestamp\":1697177855619,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1206,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"5DxE90SCJoMhWOS&LEbJ\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1},\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177857441,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#8513 = 12)\\\"]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"973\",\"numDeletionVectorsUpdated\":\"1\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"8615ed9c-6975-4b52-8877-1e7ee0f6bdf8\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00000-3f054c46-7f8f-45f3-a541-25525787b631.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":2,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":22,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo2\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"oAq1>cIQ1AV0k$:hf0RA\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00000-3f054c46-7f8f-45f3-a541-25525787b631.c000.snappy.parquet\",\"deletionTimestamp\":1697177857274,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1206,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"&9sA9{u>E#S[qd3XLIaZ\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1},\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177859306,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#9817 = 14)\\\"]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1015\",\"numDeletionVectorsUpdated\":\"1\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"dacd6651-46ce-44da-9097-42ff7c3ba430\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00000-dbfb557b-b778-454e-bef4-bab9481bcea7.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1205,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":4,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":24,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo4\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"kqleN6eh%pR:{MHoS{Q]\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00000-dbfb557b-b778-454e-bef4-bab9481bcea7.c000.snappy.parquet\",\"deletionTimestamp\":1697177859118,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1205,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"bmfc5TmwY>TJV$^4Zs$+\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1},\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177860988,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#11121 = 16)\\\"]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"906\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"b5fcd099-63f8-4673-bf68-2e4954a26c80\"}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00000-c0ca807e-59eb-4c84-a67d-c65a2e03c3c5.c000.snappy.parquet\",\"deletionTimestamp\":1697177860958,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1202,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"GOBG7L]@&bKr%xe4PF6?\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1},\"stats\":\"{\\\"numRecords\\\":2}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177862500,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#12425 = 18)\\\"]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"808\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"14d135d4-e072-40f1-9fb2-9137d54ac5c2\"}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00000-ee6122b8-1474-4764-8bdf-8f8b95c734af.c000.snappy.parquet\",\"deletionTimestamp\":1697177862473,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1202,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m?5^MJ=4G^SC%md8dbcb\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1},\"stats\":\"{\\\"numRecords\\\":2}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177865395,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#13877 = 20)\\\"]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1201\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"c35e56bf-cadb-4c9f-8b76-d14ed0527dee\"}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=0/part-00000-d1888b8a-150e-4fe3-a397-1514739499b4.c000.snappy.parquet\",\"deletionTimestamp\":1697177865367,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"0\"},\"size\":1206,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"0Mb{.bIUQYOtd%By%1f@\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2},\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000012.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177867270,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#15575 = 22)\\\"]\"},\"readVersion\":11,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"721\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"aedbce9b-48f2-4384-b65b-a07752818854\"}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=2/part-00000-3f054c46-7f8f-45f3-a541-25525787b631.c000.snappy.parquet\",\"deletionTimestamp\":1697177867243,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"2\"},\"size\":1206,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"oAq1>cIQ1AV0k$:hf0RA\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2},\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000013.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177868948,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#16936 = 24)\\\"]\"},\"readVersion\":12,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"1\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"814\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"65206981-895e-4279-a10d-ce15017dfa43\"}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=4/part-00000-dbfb557b-b778-454e-bef4-bab9481bcea7.c000.snappy.parquet\",\"deletionTimestamp\":1697177868918,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"4\"},\"size\":1205,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"kqleN6eh%pR:{MHoS{Q]\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2},\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000014.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177870510,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#18297 = 26)\\\"]\"},\"readVersion\":13,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"872\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"dd7b833f-ff28-440e-8dd0-edb6cdfe10b0\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00001-8c38a718-ea0d-4ac1-9515-3a6ec23cc86b.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":26,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":46,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo1\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"^wV}@i>E%:Tv:EZ=7q>&\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=6/part-00001-8c38a718-ea0d-4ac1-9515-3a6ec23cc86b.c000.snappy.parquet\",\"deletionTimestamp\":1697177870283,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"6\"},\"size\":1206,\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/00000000000000000015.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697177872306,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(col1#19833 = 28)\\\"]\"},\"readVersion\":14,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"948\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"a4b3afa7-3ad9-487f-bbe0-81fb606647a8\"}}\n{\"add\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00001-0878dadb-c875-4347-92a3-8739c303d7bd.c000.snappy.parquet\",\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1206,\"modificationTime\":1697177837000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":28,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"maxValues\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":48,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":\\\"foo3\\\"},\\\"nullCount\\\":{\\\"col-9f0743fd-a52e-44a7-92e0-72a3d34231b1\\\":0,\\\"col-ae46e8ed-607b-4b7c-8a13-44991fe0a2e4\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"*Cr(.vsy[nS2z}u{${-v\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037=8/part-00001-0878dadb-c875-4347-92a3-8739c303d7bd.c000.snappy.parquet\",\"deletionTimestamp\":1697177872136,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\":\"8\"},\"size\":1206,\"stats\":\"{\\\"numRecords\\\":3}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/dv-with-columnmapping/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":30,\"sizeInBytes\":23367,\"numOfAddFiles\":18,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues_parsed\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"col-60c949ca-b8bc-4330-b931-b73fb4c60037\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"0e712f2b035400d1c5b97f96fcef739f\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-column-names-case-insensitive/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629874535433,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"BarFoo\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputBytes\":\"1782\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"FooBar\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"BarFoo\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"BarFoo\"],\"configuration\":{},\"createdTime\":1629874533636}}\n{\"add\":{\"path\":\"BarFoo=foo0/part-00000-36c1f69c-21dc-4374-a89e-1c4468eff784.c000.snappy.parquet\",\"partitionValues\":{\"BarFoo\":\"foo0\"},\"size\":448,\"modificationTime\":1629874535000,\"dataChange\":true}}\n{\"add\":{\"path\":\"BarFoo=foo1/part-00000-5c80a439-70eb-435a-92eb-04549d3f220e.c000.snappy.parquet\",\"partitionValues\":{\"BarFoo\":\"foo1\"},\"size\":443,\"modificationTime\":1629874535000,\"dataChange\":true}}\n{\"add\":{\"path\":\"BarFoo=foo0/part-00001-27f5c1f6-2393-4021-9a0f-44d143761f88.c000.snappy.parquet\",\"partitionValues\":{\"BarFoo\":\"foo0\"},\"size\":443,\"modificationTime\":1629874535000,\"dataChange\":true}}\n{\"add\":{\"path\":\"BarFoo=foo1/part-00001-b6134dd2-aa40-4868-a708-bec69fc562a2.c000.snappy.parquet\",\"partitionValues\":{\"BarFoo\":\"foo1\"},\"size\":448,\"modificationTime\":1629874535000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-deleted-path/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629874421524,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1318\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1629874419356}}\n{\"add\":{\"path\":\"part-00000-377b2930-7ed7-41e6-bab2-d565a7ca5bfb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":659,\"modificationTime\":1629874421000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-6537e97d-662a-430d-9ad9-f6d087ae7cb8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":659,\"modificationTime\":1629874421000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-incorrect-format-config/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629874375835,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1306\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"b\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1629874374114}}\n{\"add\":{\"path\":\"part-00000-7b3124df-d8a4-4a4a-9d99-e98cfde281cf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":653,\"modificationTime\":1629874375000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e8582398-602e-4697-a508-fc046c1c57cf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":653,\"modificationTime\":1629874375000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-map-types-correctly/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629873175558,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"4156\",\"numOutputRows\":\"1\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c3\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c4\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c5\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c6\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c7\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c8\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c9\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c10\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c11\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c12\\\",\\\"type\\\":\\\"decimal(38,18)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c13\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"string\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c14\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c15\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"f1\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"f2\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1629873173115}}\n{\"add\":{\"path\":\"part-00000-c9259a22-ce39-45df-8d76-768bd813c3ff-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4156,\"modificationTime\":1629873175000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-non-partitioned/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629872975334,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1318\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1629872972259}}\n{\"add\":{\"path\":\"part-00000-e24c5388-1621-46bd-94eb-fea5209018d0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":659,\"modificationTime\":1629872975000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-f2126b8d-1594-451b-9c89-c4c2481bfd93-c000.snappy.parquet\",\"partitionValues\":{},\"size\":659,\"modificationTime\":1629872975000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-not-allow-write/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629872770300,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1306\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"b\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1629872768383}}\n{\"add\":{\"path\":\"part-00000-fab61bc4-5175-46ea-ac35-249c0f5750ff-c000.snappy.parquet\",\"partitionValues\":{},\"size\":653,\"modificationTime\":1629872770000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-6eb569ba-9300-49e7-9b5a-d064e8c5be2d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":653,\"modificationTime\":1629872770000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-partition-prune/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629873077420,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"date\\\",\\\"city\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputBytes\":\"3195\",\"numOutputRows\":\"5\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"city\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"cnt\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"date\",\"city\"],\"configuration\":{},\"createdTime\":1629873075437}}\n{\"add\":{\"path\":\"date=20180520/city=hz/part-00000-de1d5bcd-ad7e-4b88-ba9b-31fb8aeb8093.c000.snappy.parquet\",\"partitionValues\":{\"date\":\"20180520\",\"city\":\"hz\"},\"size\":628,\"modificationTime\":1629873077000,\"dataChange\":true}}\n{\"add\":{\"path\":\"date=20180718/city=hz/part-00000-f888e95b-c831-43fe-bba8-3dbf43b4eb86.c000.snappy.parquet\",\"partitionValues\":{\"date\":\"20180718\",\"city\":\"hz\"},\"size\":639,\"modificationTime\":1629873077000,\"dataChange\":true}}\n{\"add\":{\"path\":\"date=20180512/city=sh/part-00001-c87aeb63-6d9c-4511-b8b3-71d02178554f.c000.snappy.parquet\",\"partitionValues\":{\"date\":\"20180512\",\"city\":\"sh\"},\"size\":628,\"modificationTime\":1629873077000,\"dataChange\":true}}\n{\"add\":{\"path\":\"date=20180520/city=bj/part-00001-4c732f0f-a473-400a-8ba3-1499f599b8f1.c000.snappy.parquet\",\"partitionValues\":{\"date\":\"20180520\",\"city\":\"bj\"},\"size\":650,\"modificationTime\":1629873077000,\"dataChange\":true}}\n{\"add\":{\"path\":\"date=20181212/city=sz/part-00001-529ff89b-55c6-4405-a6cc-04759d5f692b.c000.snappy.parquet\",\"partitionValues\":{\"date\":\"20181212\",\"city\":\"sz\"},\"size\":650,\"modificationTime\":1629873077000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-partitioned/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629873032991,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"c2\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputBytes\":\"1734\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"c2\"],\"configuration\":{},\"createdTime\":1629873029858}}\n{\"add\":{\"path\":\"c2=foo0/part-00000-2bcc9ff6-0551-4401-bd22-d361a60627e3.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo0\"},\"size\":436,\"modificationTime\":1629873032000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=foo1/part-00000-786c7455-9587-454f-9a4c-de0b22b62bbd.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo1\"},\"size\":431,\"modificationTime\":1629873032000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=foo0/part-00001-ca647ee7-f1ad-4d70-bf02-5d1872324d6f.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo0\"},\"size\":431,\"modificationTime\":1629873032000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=foo1/part-00001-1c702e73-89b5-465a-9c6a-25f7559cd150.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo1\"},\"size\":436,\"modificationTime\":1629873032000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-schema-match/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629872936115,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"b\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputBytes\":\"2494\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"b\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"b\"],\"configuration\":{},\"createdTime\":1629872933338}}\n{\"add\":{\"path\":\"b=foo0/part-00000-531fe778-e359-44c9-8c35-7ed2416c5ff5.c000.snappy.parquet\",\"partitionValues\":{\"b\":\"foo0\"},\"size\":629,\"modificationTime\":1629872935000,\"dataChange\":true}}\n{\"add\":{\"path\":\"b=foo1/part-00000-7dad1d59-f42c-46c1-992e-35c2fb4d9c09.c000.snappy.parquet\",\"partitionValues\":{\"b\":\"foo1\"},\"size\":618,\"modificationTime\":1629872936000,\"dataChange\":true}}\n{\"add\":{\"path\":\"b=foo0/part-00001-923b258c-b34c-4cb9-8da9-622005e49f2c.c000.snappy.parquet\",\"partitionValues\":{\"b\":\"foo0\"},\"size\":618,\"modificationTime\":1629872935000,\"dataChange\":true}}\n{\"add\":{\"path\":\"b=foo1/part-00001-e44bca08-b26b-4f4d-8a22-5bb45a598dcf.c000.snappy.parquet\",\"partitionValues\":{\"b\":\"foo1\"},\"size\":629,\"modificationTime\":1629872936000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-special-chars-in-partition-column/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629873142667,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"c2\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputBytes\":\"1734\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"c2\"],\"configuration\":{},\"createdTime\":1629873139851}}\n{\"add\":{\"path\":\"c2=+%20%253D%25250/part-00000-88ad45a3-9b80-4e66-b474-1748ba085060.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"+ =%0\"},\"size\":436,\"modificationTime\":1629873142000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=+%20%253D%25251/part-00000-180d1a36-4ba9-4321-8145-1e0d73406b02.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"+ =%1\"},\"size\":431,\"modificationTime\":1629873142000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=+%20%253D%25250/part-00001-aff2b410-c566-4e51-a968-acfa96d6f1e9.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"+ =%0\"},\"size\":431,\"modificationTime\":1629873142000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=+%20%253D%25251/part-00001-3379bbbf-1ab8-4781-8b7e-29038d983f83.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"+ =%1\"},\"size\":436,\"modificationTime\":1629873142000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/hive/deltatbl-touch-files-needed-for-partitioned/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1629873109640,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"c2\\\"]\"},\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputBytes\":\"1734\",\"numOutputRows\":\"10\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"c2\"],\"configuration\":{},\"createdTime\":1629873107868}}\n{\"add\":{\"path\":\"c2=foo0/part-00000-f1acd078-4e44-4d47-91b2-6568396e2ec3.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo0\"},\"size\":436,\"modificationTime\":1629873109000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=foo1/part-00000-1bb7c99b-be0e-4c49-ae73-9baf5a8a08d0.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo1\"},\"size\":431,\"modificationTime\":1629873109000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=foo0/part-00001-e7f40ed6-fefa-41f5-b8a6-c6e9b78a1448.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo0\"},\"size\":431,\"modificationTime\":1629873109000,\"dataChange\":true}}\n{\"add\":{\"path\":\"c2=foo1/part-00001-c357f264-a317-4e93-a530-a8b1360ca9f6.c000.snappy.parquet\",\"partitionValues\":{\"c2\":\"foo1\"},\"size\":436,\"modificationTime\":1629873109000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/kernel-timestamp-INT96/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1692156979734,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"5\",\"numOutputBytes\":\"3535\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"80d90401-5d96-4c45-8f57-2c4488eb4e78\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"time\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{},\"createdTime\":1692156974547}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part=2020-01-01%2008%253A09%253A10.001/part-00000-bd889aef-417c-4493-b5f7-a9884ba4b247.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2020-01-01 08:09:10.001\"},\"size\":728,\"modificationTime\":1692156979000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00000-57e97070-8fc8-485a-95c6-af55daf5e09b.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":728,\"modificationTime\":1692156979000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=__HIVE_DEFAULT_PARTITION__/part-00001-7cb5f53e-936c-4d24-bca1-9fa0fc7a66e4.c000.snappy.parquet\",\"partitionValues\":{\"part\":null},\"size\":623,\"modificationTime\":1692156979000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":1}}\"}}\n{\"add\":{\"path\":\"part=1969-01-01%2000%253A00%253A00/part-00001-75ac07ae-d2e8-4030-be59-c490d47c4496.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"1969-01-01 00:00:00\"},\"size\":728,\"modificationTime\":1692156979000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00001-bd0c6fb8-aafd-48dc-9bba-331c1c6f137b.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":728,\"modificationTime\":1692156979000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/kernel-timestamp-PST/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1692156996195,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"5\",\"numOutputBytes\":\"3535\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"40ad66e4-27a5-4882-96c7-c30c9f8d6fe9\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"time\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{},\"createdTime\":1692156995593}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part=2020-01-01%2008%253A09%253A10.001/part-00000-a8be3fd2-1fd5-4dd7-84d2-6899a62d99e8.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2020-01-01 08:09:10.001\"},\"size\":728,\"modificationTime\":1692156995000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000-08:00\\\"},\\\"maxValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000-08:00\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00000-321ea6ca-841e-4654-9844-2d4041b6d0d6.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":728,\"modificationTime\":1692156996000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000-08:00\\\"},\\\"maxValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000-08:00\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=__HIVE_DEFAULT_PARTITION__/part-00001-18484b3d-01e6-48bc-9e8b-2a75d36d9f7a.c000.snappy.parquet\",\"partitionValues\":{\"part\":null},\"size\":623,\"modificationTime\":1692156995000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":1}}\"}}\n{\"add\":{\"path\":\"part=1969-01-01%2000%253A00%253A00/part-00001-48d8c27a-3661-4e1e-95cb-02ef244c1cf4.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"1969-01-01 00:00:00\"},\"size\":728,\"modificationTime\":1692156996000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000-08:00\\\"},\\\"maxValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000-08:00\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00001-b223f8fd-9d33-465b-b139-36c41abb10e8.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":728,\"modificationTime\":1692156996000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000-08:00\\\"},\\\"maxValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000-08:00\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/kernel-timestamp-TIMESTAMP_MICROS/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1692156990276,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"5\",\"numOutputBytes\":\"3530\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"7c789624-c173-4a06-8abf-fc07d3cf45dc\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"time\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{},\"createdTime\":1692156989451}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part=2020-01-01%2008%253A09%253A10.001/part-00000-3cac2575-d0b4-4647-a7a3-b4a9d910cb32.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2020-01-01 08:09:10.001\"},\"size\":719,\"modificationTime\":1692156989000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00000-038fb25c-ca6b-43b6-b0dc-d987f38d0ab9.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":719,\"modificationTime\":1692156990000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=__HIVE_DEFAULT_PARTITION__/part-00001-107828e6-a4b9-42b1-9f1f-244c0efc1b08.c000.snappy.parquet\",\"partitionValues\":{\"part\":null},\"size\":654,\"modificationTime\":1692156989000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":1}}\"}}\n{\"add\":{\"path\":\"part=1969-01-01%2000%253A00%253A00/part-00001-2b5694f1-b839-4037-b264-353b31af6e7b.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"1969-01-01 00:00:00\"},\"size\":719,\"modificationTime\":1692156990000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00001-226faf2a-427a-40ee-bfb5-5d53c8642c8a.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":719,\"modificationTime\":1692156990000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/kernel-timestamp-TIMESTAMP_MILLIS/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1692156993531,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"part\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"5\",\"numOutputBytes\":\"3529\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"d84260a3-a773-4f10-8079-2bb1c1bf1303\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"time\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part\"],\"configuration\":{},\"createdTime\":1692156992918}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part=2020-01-01%2008%253A09%253A10.001/part-00000-4b5188f5-4784-47ce-b4ad-1d3eae80710e.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2020-01-01 08:09:10.001\"},\"size\":719,\"modificationTime\":1692156993000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":0,\\\"time\\\":\\\"2020-02-01T08:09:10.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00000-086f164a-4d32-4631-b9f9-8aeab485f19c.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":719,\"modificationTime\":1692156993000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":1,\\\"time\\\":\\\"1999-01-01T09:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=__HIVE_DEFAULT_PARTITION__/part-00001-f81daebf-3993-4686-bf72-470e1fe078d9.c000.snappy.parquet\",\"partitionValues\":{\"part\":null},\"size\":654,\"modificationTime\":1692156993000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":1}}\"}}\n{\"add\":{\"path\":\"part=1969-01-01%2000%253A00%253A00/part-00001-4c527a95-ca90-4aeb-a61c-8d89b6330772.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"1969-01-01 00:00:00\"},\"size\":718,\"modificationTime\":1692156993000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":3,\\\"time\\\":\\\"1969-01-01T00:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n{\"add\":{\"path\":\"part=2021-10-01%2008%253A09%253A20/part-00001-94d3f0af-754c-4cde-bc6e-08338a03a32e.c000.snappy.parquet\",\"partitionValues\":{\"part\":\"2021-10-01 08:09:20\"},\"size\":719,\"modificationTime\":1692156993000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000Z\\\"},\\\"maxValues\\\":{\\\"id\\\":2,\\\"time\\\":\\\"2000-01-01T09:00:00.000Z\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"time\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/kernel-timestamp-partition-col-ISO8601/_delta_log/00000000000000000000.crc",
    "content": "{\"txnId\":\"a069b528-fe6e-4f37-b4e7-37ff2105a1c2\",\"tableSizeBytes\":1140,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"ede3d9ad-553d-4fcb-9d4e-22ce34c83e26\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"str\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ts\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"ts\"],\"configuration\":{},\"createdTime\":1748542296251},\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2},\"allFiles\":[{\"path\":\"ts=2024-01-01%2010%253A00%253A00/part-00000-9630b3f5-7ab4-4688-9822-3ef93a9d0559.c000.snappy.parquet\",\"partitionValues\":{\"ts\":\"2024-01-01T10:00:00.000000Z\"},\"size\":570,\"modificationTime\":1748542297686,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"str\\\":\\\"2024-01-01 10:00:00\\\"},\\\"maxValues\\\":{\\\"str\\\":\\\"2024-01-01 10:00:00\\\"},\\\"nullCount\\\":{\\\"str\\\":0}}\"},{\"path\":\"ts=2024-01-02%2012%253A30%253A00/part-00000-17b5fc05-b487-4b8b-82ff-9ef4352767a5.c000.snappy.parquet\",\"partitionValues\":{\"ts\":\"2024-01-02T12:30:00.000000Z\"},\"size\":570,\"modificationTime\":1748542297742,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"str\\\":\\\"2024-01-02 12:30:00\\\"},\\\"maxValues\\\":{\\\"str\\\":\\\"2024-01-02 12:30:00\\\"},\\\"nullCount\\\":{\\\"str\\\":0}}\"}]}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/kernel-timestamp-partition-col-ISO8601/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1748542298780,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[\\\"ts\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"1140\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"a069b528-fe6e-4f37-b4e7-37ff2105a1c2\"}}\n{\"metaData\":{\"id\":\"ede3d9ad-553d-4fcb-9d4e-22ce34c83e26\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"str\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ts\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"ts\"],\"configuration\":{},\"createdTime\":1748542296251}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"ts=2024-01-01%2010%253A00%253A00/part-00000-9630b3f5-7ab4-4688-9822-3ef93a9d0559.c000.snappy.parquet\",\"partitionValues\":{\"ts\":\"2024-01-01T10:00:00.000000Z\"},\"size\":570,\"modificationTime\":1748542297686,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"str\\\":\\\"2024-01-01 10:00:00\\\"},\\\"maxValues\\\":{\\\"str\\\":\\\"2024-01-01 10:00:00\\\"},\\\"nullCount\\\":{\\\"str\\\":0}}\"}}\n{\"add\":{\"path\":\"ts=2024-01-02%2012%253A30%253A00/part-00000-17b5fc05-b487-4b8b-82ff-9ef4352767a5.c000.snappy.parquet\",\"partitionValues\":{\"ts\":\"2024-01-02T12:30:00.000000Z\"},\"size\":570,\"modificationTime\":1748542297742,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"str\\\":\\\"2024-01-02 12:30:00\\\"},\\\"maxValues\\\":{\\\"str\\\":\\\"2024-01-02 12:30:00\\\"},\\\"nullCount\\\":{\\\"str\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-dv-key-cases/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697571663834,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"50\",\"numOutputBytes\":\"765\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1d19dc4c-3b35-4417-845a-0f44acef5056\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1697571659174}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"add\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697571663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-dv-key-cases/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697571681016,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#443L = 0)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"6668\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"7acb06d6-7473-406d-989d-275f025ef11f\"}}\n{\"add\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697571663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"h{&8fAg]=QYJvl-}c!yH\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"deletionTimestamp\":1697571680016,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":765,\"stats\":\"{\\\"numRecords\\\":50}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-dv-key-cases/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697571686556,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#1913L = 7)\\\"]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"3376\",\"numDeletionVectorsUpdated\":\"1\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"4a09810b-39c6-4b29-875e-3562816750dd\"}}\n{\"add\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697571663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"j=hZPftg7qJYIw^L+Oz9\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"deletionTimestamp\":1697571685983,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":765,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"h{&8fAg]=QYJvl-}c!yH\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1},\"stats\":\"{\\\"numRecords\\\":50}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-dv-key-cases/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697571690963,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#3191L = 14)\\\"]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"1\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2740\",\"numDeletionVectorsUpdated\":\"1\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"c2f503a1-043c-4bbc-af37-b6c2de878a01\"}}\n{\"add\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"partitionValues\":{},\"size\":765,\"modificationTime\":1697571663000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"^jP?.<zvDfIGb{C.FPij\",\"offset\":1,\"sizeInBytes\":38,\"cardinality\":3}}}\n{\"remove\":{\"path\":\"part-00000-90177277-75c2-48db-92a2-20dcba39fd06-c000.snappy.parquet\",\"deletionTimestamp\":1697571690494,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":765,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"j=hZPftg7qJYIw^L+Oz9\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2},\"stats\":\"{\\\"numRecords\\\":50}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-latest-metadata-protocol/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697070245495,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"20\",\"numOutputBytes\":\"1078\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"21d6f130-906a-4ff8-9491-65b1c51ff458\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697070240252}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-fc7f7936-944d-472b-9e1e-2cb7464e668a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":545,\"modificationTime\":1697070245000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col1\\\":0},\\\"maxValues\\\":{\\\"col1\\\":9},\\\"nullCount\\\":{\\\"col1\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-9624cca6-2238-4f36-a6c1-707b86b81b81-c000.snappy.parquet\",\"partitionValues\":{},\"size\":533,\"modificationTime\":1697070245000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col1\\\":10},\\\"maxValues\\\":{\\\"col1\\\":19},\\\"nullCount\\\":{\\\"col1\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-latest-metadata-protocol/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697070257696,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"20\",\"numOutputBytes\":\"1666\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"f72e15df-7d8d-406f-8789-e516c011c7c0\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697070240252}}\n{\"add\":{\"path\":\"part-00000-66f9221e-0720-45f9-910a-0e81885c93e7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":839,\"modificationTime\":1697070257000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"col1\\\":9,\\\"col2\\\":1},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-a54b97f9-bd3c-4724-917b-2730fd9b6c3a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":827,\"modificationTime\":1697070257000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col1\\\":10,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"col1\\\":19,\\\"col2\\\":1},\\\"nullCount\\\":{\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-latest-metadata-protocol/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697070260886,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.minReaderVersion\\\":\\\"3\\\",\\\"delta.minWriterVersion\\\":\\\"7\\\"}\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"b2aedf09-daca-42fb-90f0-88e687a8be1d\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1697070240252}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[],\"writerFeatures\":[\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-special-characters/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697588004461,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"515ee391-a84b-4962-82de-0e61bbe2910e\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"intCol\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"special%20p@%23h\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-special-characters/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697588018432,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"ced15ca8-7fe5-450e-884f-b2db5f993549\"}}\n{\"remove\":{\"path\":\"special%20p@%23h\",\"deletionTimestamp\":1697588004164,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":100}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-special-characters-a/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1698260653476,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1e1f08ff-7478-48e5-a22b-af963321fe50\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"intCol\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"special%20p@%23h\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-special-characters-a/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1698260667979,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"bb1d82f6-b3b4-42cc-bc55-e99aec4c1522\"}}\n{\"remove\":{\"path\":\"special%20p@%23h\",\"deletionTimestamp\":1698260653191,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":100}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-replay-special-characters-b/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1698260670525,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"48c7c145-6445-49e4-ae47-5a51d00c5a3a\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"intCol\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"special%20p@%23h\",\"partitionValues\":{},\"size\":100,\"modificationTime\":10,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-store-listFrom/1",
    "content": "zero\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-store-listFrom/2",
    "content": "one\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-store-listFrom/3",
    "content": "two\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-store-read/0",
    "content": "zero\nnone\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/log-store-read/1",
    "content": "one\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/multi-part-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1692318903998,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"478\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"712a5003-397c-4e78-8f4b-7595fb85702d\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"1\"},\"createdTime\":1692318898914}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-7f49f4e3-2c9c-4ea7-b6c3-42c9a6fc6070-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1692318903000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/multi-part-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1692318915049,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"9\",\"numOutputRows\":\"30\",\"numOutputBytes\":\"4430\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"738bac93-3034-4ef1-9317-2c1202878552\"}}\n{\"add\":{\"path\":\"part-00000-e3cd9d97-2f4e-40c4-825f-8ecf456540b0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":491,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":3},\\\"maxValues\\\":{\\\"id\\\":16},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-a5222079-1b7e-4bab-a747-ccc4f88b9915-c000.snappy.parquet\",\"partitionValues\":{},\"size\":496,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":21},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-3a12664c-2859-4236-b718-6c9e03f6496f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":494,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":29},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-1bb5a769-f4c6-4672-a94a-68ed6788ca78-c000.snappy.parquet\",\"partitionValues\":{},\"size\":491,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":22},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00004-b7080e6d-bc43-43da-becf-7c9bedffee68-c000.snappy.parquet\",\"partitionValues\":{},\"size\":491,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":24},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00005-a0ce8d21-d9b6-44b9-803b-a4085a4b43cd-c000.snappy.parquet\",\"partitionValues\":{},\"size\":491,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":6},\\\"maxValues\\\":{\\\"id\\\":28},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00006-9093b02c-22e2-4505-bb37-104a4825137f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":496,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":8},\\\"maxValues\\\":{\\\"id\\\":27},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00007-8cea4b0f-450b-444f-936d-e2695b1adca6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":489,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":25},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00008-470c16a2-bd1d-45e5-9cfc-5741ba5b57e1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":491,\"modificationTime\":1692318914000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"id\\\":9},\\\"maxValues\\\":{\\\"id\\\":23},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/multi-part-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":1,\"size\":12,\"parts\":2,\"sizeInBytes\":30499,\"numOfAddFiles\":10,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"21fd80b88fa17d8aaa03d210bdc5a17d\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/only-checkpoint-files/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697593123749,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"10\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"4780\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"7d74fc39-15e0-4bd9-a864-52e9917894a0\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"1\"},\"createdTime\":1697593117126}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-dfc22a82-c022-4a82-86e5-1893449a9ac9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593122000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-43c03a40-f6fe-4cc7-80d5-b1273adab930-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593122000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":2},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-896896b5-bff2-4d67-a74a-46dbb9730710-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593122000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-885e84c4-be75-485c-8459-257a0a552a2d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593122000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00004-dcf3f384-2139-4406-af81-89aff10b612d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593123000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":7},\\\"maxValues\\\":{\\\"id\\\":7},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00005-e3a5e9cc-e036-41cb-952d-fd3e374af794-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593123000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":5},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00006-c048e558-898c-43b3-b144-47efbbab72d1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593123000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":6},\\\"maxValues\\\":{\\\"id\\\":6},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00007-50c77d47-31b2-4e0a-a43d-2f6be5ff15ee-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593123000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":8},\\\"maxValues\\\":{\\\"id\\\":8},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00008-9fcc1da0-688a-4be6-a296-21da16557267-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593123000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":9},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00009-ac03e5b4-bd86-48e0-a9a0-094180656170-c000.snappy.parquet\",\"partitionValues\":{},\"size\":478,\"modificationTime\":1697593123000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":3},\\\"maxValues\\\":{\\\"id\\\":3},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/only-checkpoint-files/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697593139603,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#478L < 5)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"5\",\"numRemovedBytes\":\"2390\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"0\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"4796\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"4098\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"695\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"f9cd1c69-03c9-4bf0-9088-49415a063738\"}}\n{\"remove\":{\"path\":\"part-00003-885e84c4-be75-485c-8459-257a0a552a2d-c000.snappy.parquet\",\"deletionTimestamp\":1697593139529,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":478}}\n{\"remove\":{\"path\":\"part-00000-dfc22a82-c022-4a82-86e5-1893449a9ac9-c000.snappy.parquet\",\"deletionTimestamp\":1697593139529,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":478}}\n{\"remove\":{\"path\":\"part-00001-43c03a40-f6fe-4cc7-80d5-b1273adab930-c000.snappy.parquet\",\"deletionTimestamp\":1697593139529,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":478}}\n{\"remove\":{\"path\":\"part-00009-ac03e5b4-bd86-48e0-a9a0-094180656170-c000.snappy.parquet\",\"deletionTimestamp\":1697593139529,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":478}}\n{\"remove\":{\"path\":\"part-00002-896896b5-bff2-4d67-a74a-46dbb9730710-c000.snappy.parquet\",\"deletionTimestamp\":1697593139529,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":478}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/only-checkpoint-files/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1697593147504,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"20\",\"numOutputBytes\":\"1066\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"298bfe52-0470-44d5-b201-fa320ba5b392\"}}\n{\"add\":{\"path\":\"part-00000-b4e80ee6-4cbd-4cc6-b565-d2c625d0731a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":539,\"modificationTime\":1697593145000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-b50e4584-a496-4a37-a227-f7b3e9705aee-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1697593145000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":19},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/only-checkpoint-files/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":14,\"sizeInBytes\":16699,\"numOfAddFiles\":7,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"04d8d8ab205ab1627b632a278513c609\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/parquet-all-types/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1715358308005,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"200\",\"numOutputBytes\":\"21057\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"c84f1a78-0895-4f01-b00e-f3a984c8afca\"}}\n{\"metaData\":{\"id\":\"ab49cd9e-a908-4aad-a15b-9dd117d3e0ab\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ByteType\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ShortType\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"IntegerType\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"LongType\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"FloatType\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"DoubleType\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"BooleanType\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"StringType\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"BinaryType\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"DateType\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"TimestampType\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"TimestampNTZType\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"nested_struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_rows\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1715358307675}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"timestampNtz\"],\"writerFeatures\":[\"timestampNtz\"]}}\n{\"add\":{\"path\":\"part-00000-bf6680d4-5e83-4fce-8ebb-d2b60d7e69c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":21057,\"modificationTime\":1715358307997,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"ByteType\\\":-128,\\\"ShortType\\\":1,\\\"IntegerType\\\":1,\\\"LongType\\\":2,\\\"FloatType\\\":0.234,\\\"DoubleType\\\":234234.23,\\\"decimal\\\":123.52,\\\"StringType\\\":\\\"1\\\",\\\"DateType\\\":\\\"1970-01-01\\\",\\\"TimestampType\\\":\\\"1970-01-01T06:30:23.523Z\\\",\\\"TimestampNTZType\\\":\\\"1970-01-03T17:03:54.000\\\",\\\"nested_struct\\\":{\\\"aa\\\":\\\"1\\\",\\\"ac\\\":{\\\"aca\\\":1}}},\\\"maxValues\\\":{\\\"ByteType\\\":127,\\\"ShortType\\\":199,\\\"IntegerType\\\":199,\\\"LongType\\\":200,\\\"FloatType\\\":46.566,\\\"DoubleType\\\":4.661261177E7,\\\"decimal\\\":24580.48,\\\"StringType\\\":\\\"99\\\",\\\"DateType\\\":\\\"1970-02-16\\\",\\\"TimestampType\\\":\\\"1970-02-23T22:48:01.077Z\\\",\\\"TimestampNTZType\\\":\\\"1971-06-24T11:56:06.000\\\",\\\"nested_struct\\\":{\\\"aa\\\":\\\"99\\\",\\\"ac\\\":{\\\"aca\\\":199}}},\\\"nullCount\\\":{\\\"ByteType\\\":3,\\\"ShortType\\\":4,\\\"IntegerType\\\":9,\\\"LongType\\\":8,\\\"FloatType\\\":8,\\\"DoubleType\\\":4,\\\"decimal\\\":3,\\\"BooleanType\\\":3,\\\"StringType\\\":4,\\\"BinaryType\\\":4,\\\"DateType\\\":4,\\\"TimestampType\\\":4,\\\"TimestampNTZType\\\":3,\\\"nested_struct\\\":{\\\"aa\\\":14,\\\"ac\\\":{\\\"aca\\\":22}},\\\"array_of_prims\\\":200,\\\"array_of_arrays\\\":200,\\\"array_of_structs\\\":200,\\\"map_of_prims\\\":200,\\\"map_of_rows\\\":200,\\\"map_of_arrays\\\":200}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/parquet-all-types-legacy-format/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1715358358979,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"200\",\"numOutputBytes\":\"20934\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"01d57c6f-6073-484f-b832-5cf368644e4b\"}}\n{\"metaData\":{\"id\":\"fb723383-fb90-4346-a846-ccdac9a3204b\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ByteType\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ShortType\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"IntegerType\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"LongType\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"FloatType\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"DoubleType\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"BooleanType\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"StringType\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"BinaryType\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"DateType\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"TimestampType\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"TimestampNTZType\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"nested_struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_rows\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1715358356845}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"timestampNtz\"],\"writerFeatures\":[\"timestampNtz\"]}}\n{\"add\":{\"path\":\"part-00000-5afb67f1-094a-4a15-922e-c1eb96683964-c000.snappy.parquet\",\"partitionValues\":{},\"size\":20934,\"modificationTime\":1715358358904,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"ByteType\\\":-128,\\\"ShortType\\\":1,\\\"IntegerType\\\":1,\\\"LongType\\\":2,\\\"FloatType\\\":0.234,\\\"DoubleType\\\":234234.23,\\\"decimal\\\":123.52,\\\"StringType\\\":\\\"1\\\",\\\"DateType\\\":\\\"1970-01-01\\\",\\\"TimestampType\\\":\\\"1970-01-01T06:30:23.523Z\\\",\\\"TimestampNTZType\\\":\\\"1970-01-03T17:03:54.000\\\",\\\"nested_struct\\\":{\\\"aa\\\":\\\"1\\\",\\\"ac\\\":{\\\"aca\\\":1}}},\\\"maxValues\\\":{\\\"ByteType\\\":127,\\\"ShortType\\\":199,\\\"IntegerType\\\":199,\\\"LongType\\\":200,\\\"FloatType\\\":46.566,\\\"DoubleType\\\":4.661261177E7,\\\"decimal\\\":24580.48,\\\"StringType\\\":\\\"99\\\",\\\"DateType\\\":\\\"1970-02-16\\\",\\\"TimestampType\\\":\\\"1970-02-23T22:48:01.077Z\\\",\\\"TimestampNTZType\\\":\\\"1971-06-24T11:56:06.000\\\",\\\"nested_struct\\\":{\\\"aa\\\":\\\"99\\\",\\\"ac\\\":{\\\"aca\\\":199}}},\\\"nullCount\\\":{\\\"ByteType\\\":3,\\\"ShortType\\\":4,\\\"IntegerType\\\":9,\\\"LongType\\\":8,\\\"FloatType\\\":8,\\\"DoubleType\\\":4,\\\"decimal\\\":3,\\\"BooleanType\\\":3,\\\"StringType\\\":4,\\\"BinaryType\\\":4,\\\"DateType\\\":4,\\\"TimestampType\\\":4,\\\"TimestampNTZType\\\":3,\\\"nested_struct\\\":{\\\"aa\\\":14,\\\"ac\\\":{\\\"aca\\\":22}},\\\"array_of_prims\\\":200,\\\"array_of_arrays\\\":200,\\\"array_of_structs\\\":200,\\\"map_of_prims\\\":200,\\\"map_of_rows\\\":200,\\\"map_of_arrays\\\":200}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/parquet-decimal-dictionaries/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691112043897,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1000000\",\"numOutputBytes\":\"60952\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"8ebef15f-98d0-43ed-aa26-e1f9f1de7f4e\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"decimal(9,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"decimal(12,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col3\\\",\\\"type\\\":\\\"decimal(25,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1691112034932}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-60b8c840-c0d4-428e-9005-89f02233be85-c000.snappy.parquet\",\"partitionValues\":{},\"size\":60952,\"modificationTime\":1691112043000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1000000,\\\"minValues\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0},\\\"maxValues\\\":{\\\"id\\\":999999,\\\"col1\\\":4,\\\"col2\\\":5,\\\"col3\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/parquet-decimal-dictionaries-v1/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691113099049,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1000000\",\"numOutputBytes\":\"4571289\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"62d25bc1-8193-4fb8-89ad-eae3dc17617a\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"decimal(9,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"decimal(12,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col3\\\",\\\"type\\\":\\\"decimal(25,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1691113089928}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-92f97f0b-304f-4587-9d25-088cb386fa64-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4571289,\"modificationTime\":1691113098000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1000000,\\\"minValues\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0},\\\"maxValues\\\":{\\\"id\\\":999999,\\\"col1\\\":4,\\\"col2\\\":5,\\\"col3\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/parquet-decimal-dictionaries-v2/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1691113113890,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1000000\",\"numOutputBytes\":\"60952\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"9b3ef6e5-cdfd-4890-9a4c-21fc81ffa0f9\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"decimal(9,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"decimal(12,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col3\\\",\\\"type\\\":\\\"decimal(25,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1691113109835}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-2509b8ef-98ac-42da-98ee-9d2c58ac6031-c000.snappy.parquet\",\"partitionValues\":{},\"size\":60952,\"modificationTime\":1691113113000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1000000,\\\"minValues\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0},\\\"maxValues\\\":{\\\"id\\\":999999,\\\"col1\\\":4,\\\"col2\\\":5,\\\"col3\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/parquet-decimal-type/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1690695618191,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"99998\",\"numOutputBytes\":\"2303393\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/3.0.0-SNAPSHOT\",\"txnId\":\"1c5a0408-3851-4994-843d-56e12e802dff\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"decimal(5,1)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"decimal(10,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col3\\\",\\\"type\\\":\\\"decimal(20,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1690695615966}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-8c8ffc0f-9259-478b-9b1b-ea6d37ce5889-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2303393,\"modificationTime\":1690695618000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":99998,\\\"minValues\\\":{\\\"id\\\":0,\\\"col1\\\":-9999.0,\\\"col2\\\":-99990.99990,\\\"col3\\\":-999929997299970.99990},\\\"maxValues\\\":{\\\"id\\\":99997,\\\"col1\\\":9999.7,\\\"col2\\\":99997.99997,\\\"col3\\\":999999999399991.99997},\\\"nullCount\\\":{\\\"id\\\":0,\\\"col1\\\":0,\\\"col2\\\":0,\\\"col3\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data0/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data1/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data1/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723969055,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723969055,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723970832,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2-deleted/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2-deleted/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723969055,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2-deleted/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723970832,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2-deleted/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723972251,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":687,\"modificationTime\":1603723972000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":705,\"modificationTime\":1603723972000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data2-deleted/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723974057,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"`col2` LIKE 'data-2-%'\\\"]\"},\"readVersion\":3,\"isBlindAppend\":false}}\n{\"remove\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"deletionTimestamp\":1603723974056,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"deletionTimestamp\":1603723974056,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"partitionValues\":{},\"size\":348,\"modificationTime\":1603723974000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data3/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data3/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723969055,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data3/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723970832,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-data3/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723972251,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":687,\"modificationTime\":1603723972000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":705,\"modificationTime\":1603723972000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-repartitioned/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-repartitioned/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723969055,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-repartitioned/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723970832,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-repartitioned/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723972251,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":687,\"modificationTime\":1603723972000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":705,\"modificationTime\":1603723972000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-repartitioned/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723974057,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"`col2` LIKE 'data-2-%'\\\"]\"},\"readVersion\":3,\"isBlindAppend\":false}}\n{\"remove\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"deletionTimestamp\":1603723974056,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"deletionTimestamp\":1603723974056,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"partitionValues\":{},\"size\":348,\"modificationTime\":1603723974000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-repartitioned/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723975830,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":4,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-f95c1140-7256-4bfa-b651-e7a7eb6208bb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":695,\"modificationTime\":1603723975000,\"dataChange\":false}}\n{\"add\":{\"path\":\"part-00001-0b5675f1-d9b2-4240-914f-250ae37e8fa4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":697,\"modificationTime\":1603723975000,\"dataChange\":false}}\n{\"remove\":{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"deletionTimestamp\":1603723975829,\"dataChange\":false}}\n{\"remove\":{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"deletionTimestamp\":1603723975829,\"dataChange\":false}}\n{\"remove\":{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"deletionTimestamp\":1603723975829,\"dataChange\":false}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-vacuumed/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723967632,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"93351cf1-c931-4326-88f0-d10e29e71b21\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723967515}}\n{\"add\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-vacuumed/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723969055,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723969000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-vacuumed/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723970832,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-64680d94-9e18-4fa1-9ca9-f0cd8a9cfd11-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-b8249b87-0b7a-4461-8a8a-fa958802b523-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"deletionTimestamp\":1603723970832,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-vacuumed/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723972251,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":687,\"modificationTime\":1603723972000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":705,\"modificationTime\":1603723972000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-vacuumed/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723974057,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"`col2` LIKE 'data-2-%'\\\"]\"},\"readVersion\":3,\"isBlindAppend\":false}}\n{\"remove\":{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"deletionTimestamp\":1603723974056,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"deletionTimestamp\":1603723974056,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"partitionValues\":{},\"size\":348,\"modificationTime\":1603723974000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/snapshot-vacuumed/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723975830,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":4,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-f95c1140-7256-4bfa-b651-e7a7eb6208bb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":695,\"modificationTime\":1603723975000,\"dataChange\":false}}\n{\"add\":{\"path\":\"part-00001-0b5675f1-d9b2-4240-914f-250ae37e8fa4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":697,\"modificationTime\":1603723975000,\"dataChange\":false}}\n{\"remove\":{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"deletionTimestamp\":1603723975829,\"dataChange\":false}}\n{\"remove\":{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"deletionTimestamp\":1603723975829,\"dataChange\":false}}\n{\"remove\":{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"deletionTimestamp\":1603723975829,\"dataChange\":false}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/spark-variant-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723768497710,\"operation\":\"CREATE OR REPLACE TABLE AS SELECT\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"14767\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"2cc10429-f586-4c74-805c-8d19fd180c87\"}}\n{\"metaData\":{\"id\":\"d7eb0848-b002-4e0b-9d8d-dd335c90946f\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1723768495302}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType-preview\"],\"writerFeatures\":[\"variantType-preview\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"part-00000-16c852df-ba66-4080-be25-530a05922422-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7443,\"modificationTime\":1723768496908,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n{\"add\":{\"path\":\"part-00001-664313d3-14b4-4dbf-8110-77001b877182-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7324,\"modificationTime\":1723768496908,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/spark-variant-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723768498557,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"5072\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"78417efa-a13f-45df-add0-f96aa113fd68\"}}\n{\"add\":{\"path\":\"part-00000-9a9c570c-ee32-4322-ad2f-8c837a77d398-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5072,\"modificationTime\":1723768498551,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/spark-variant-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723768498990,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"5072\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"d90393d5-9cdd-40f1-8861-121f2169808b\"}}\n{\"add\":{\"path\":\"part-00000-1e14ba22-3114-46d1-96fb-48b4912507ce-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5072,\"modificationTime\":1723768498986,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/spark-variant-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":6,\"sizeInBytes\":21929,\"numOfAddFiles\":4,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"clusteringProvider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"a8d400a03ead8a86dbb412f2a693e26e\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/spark-variant-checkpoint/info.txt",
    "content": "This file contains the code used to generate this golden table \"spark-variant-checkpoint\"\n\nUsing delta-spark 4.0, run the following scala script:\n\nval tableName = \"<REPLACE WITH THE TABLE NAME OR PATH>\"\nval query = \"\"\"\n  with jsonStrings as (\n    select\n      id,\n      format_string('{\"key\": %s}', id) as jsonString\n    from\n      range(0, 100)\n  )\n  select\n    id,\n    parse_json(jsonString) as v,\n    array(\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString)\n    ) as array_of_variants,\n    named_struct('v', parse_json(jsonString)) as struct_of_variants,\n    map(\n      cast(id as string),\n      parse_json(jsonString),\n      'nullKey',\n      null\n    ) as map_of_variants,\n    array(\n      named_struct('v', parse_json(jsonString)),\n      named_struct('v', null),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      ),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      )\n    ) as array_of_struct_of_variants,\n    named_struct(\n      'v',\n      array(\n        null,\n        parse_json(jsonString)\n      )\n    ) as struct_of_array_of_variants\n  from\n    jsonStrings\n\"\"\"\n\nval writeToTableSql = s\"\"\"\n  create or replace table $tableName\n  USING DELTA TBLPROPERTIES (delta.checkpointInterval = 2)\n\"\"\"\n\nspark.sql(s\"${writeToTableSql}\\n${query}\")\n// Write two additional rows to create a checkpoint.\n(0 until 2).foreach { v =>\n  spark\n    .sql(query)\n    .where(s\"id = $v\")\n    .write\n    .format(\"delta\")\n    .mode(\"append\")\n    .insertInto(tableName)\n}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/table-with-columnmapping-mode-id/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723094980674,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"6\",\"numOutputBytes\":\"35024\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"ad640169-85ab-4030-ada7-b70661040863\"}}\n{\"metaData\":{\"id\":\"6fb2dbf2-52a9-4632-8952-71c976b4bf77\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ByteType\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\"}},{\\\"name\\\":\\\"ShortType\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\"}},{\\\"name\\\":\\\"IntegerType\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\"}},{\\\"name\\\":\\\"LongType\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.physicalName\\\":\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\"}},{\\\"name\\\":\\\"FloatType\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.physicalName\\\":\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\"}},{\\\"name\\\":\\\"DoubleType\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.physicalName\\\":\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\"}},{\\\"name\\\":\\\"decimal\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.physicalName\\\":\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\"}},{\\\"name\\\":\\\"BooleanType\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.physicalName\\\":\\\"col-836e8d56-f73a-4203-9c0f-a50476468c2c\\\"}},{\\\"name\\\":\\\"StringType\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.physicalName\\\":\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\"}},{\\\"name\\\":\\\"BinaryType\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.physicalName\\\":\\\"col-da9f099c-f6bc-459c-a740-b02f658221e2\\\"}},{\\\"name\\\":\\\"DateType\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":11,\\\"delta.columnMapping.physicalName\\\":\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\"}},{\\\"name\\\":\\\"TimestampType\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":12,\\\"delta.columnMapping.physicalName\\\":\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\"}},{\\\"name\\\":\\\"nested_struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":14,\\\"delta.columnMapping.physicalName\\\":\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\"}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":16,\\\"delta.columnMapping.physicalName\\\":\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":15,\\\"delta.columnMapping.physicalName\\\":\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":13,\\\"delta.columnMapping.physicalName\\\":\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\"}},{\\\"name\\\":\\\"array_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":17,\\\"delta.columnMapping.physicalName\\\":\\\"col-720537b9-44e0-4829-a189-f3742da4f095\\\"}},{\\\"name\\\":\\\"array_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":18,\\\"delta.columnMapping.physicalName\\\":\\\"col-9f8d855c-cc39-426c-b579-f1b85b1b6991\\\"}},{\\\"name\\\":\\\"array_of_map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":19,\\\"delta.columnMapping.physicalName\\\":\\\"col-37f1b990-e228-4046-b98e-23934eb972b0\\\"}},{\\\"name\\\":\\\"array_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":21,\\\"delta.columnMapping.physicalName\\\":\\\"col-87e5a113-d6ee-40d5-b647-fadedfb84adb\\\"}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":20,\\\"delta.columnMapping.physicalName\\\":\\\"col-11bac662-f43a-44e3-b419-17b2ee3ee612\\\"}},{\\\"name\\\":\\\"struct_of_arrays_maps_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":23,\\\"delta.columnMapping.physicalName\\\":\\\"col-c4278c59-01d8-4f81-be2e-9d3ead4ddc9e\\\"}},{\\\"name\\\":\\\"ab\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":25,\\\"delta.columnMapping.physicalName\\\":\\\"col-56a3728f-7e4b-4c2f-ac2d-98f9c3d8031e\\\"}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":24,\\\"delta.columnMapping.physicalName\\\":\\\"col-10aa4b27-4657-4ee4-a43a-ecf2f8fbb4d3\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":22,\\\"delta.columnMapping.physicalName\\\":\\\"col-dbc4c186-5c6b-4309-82a2-fce1be10512f\\\"}},{\\\"name\\\":\\\"map_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":26,\\\"delta.columnMapping.physicalName\\\":\\\"col-ee9a73a7-e466-49ee-9a1f-69d01b60b88e\\\"}},{\\\"name\\\":\\\"map_of_rows\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":28,\\\"delta.columnMapping.physicalName\\\":\\\"col-7643d991-48d1-4d3d-a737-46be59e9b200\\\"}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":27,\\\"delta.columnMapping.physicalName\\\":\\\"col-9697fd7e-5f66-43c0-9fee-3cf2bc9c2d4b\\\"}},{\\\"name\\\":\\\"map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":29,\\\"delta.columnMapping.physicalName\\\":\\\"col-e146f9dd-9ce2-4dbb-8a91-54a393bab453\\\"}},{\\\"name\\\":\\\"map_of_maps\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"integer\\\",\\\"valueContainsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":30,\\\"delta.columnMapping.physicalName\\\":\\\"col-a6fcf96f-53a6-4a16-9df1-59eb1f6e06bf\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"id\",\"delta.columnMapping.maxColumnId\":\"30\"},\"createdTime\":1723094978338}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"add\":{\"path\":\"part-00000-37fc7686-b5a9-432d-8cdc-8caa8cf999e5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":17281,\"modificationTime\":1723094980594,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\":0,\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\":0,\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\":0,\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\":0,\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\":0.0,\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\":0.0,\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\":0.00,\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\":\\\"0\\\",\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\":\\\"2021-11-18\\\",\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\":\\\"1970-01-01T00:00:00.000Z\\\",\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\":{\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\":\\\"0\\\",\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\":{\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\":0}}},\\\"maxValues\\\":{\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\":4,\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\":4,\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\":4,\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\":4,\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\":4.0,\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\":4.0,\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\":4.00,\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\":\\\"4\\\",\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\":\\\"2021-11-18\\\",\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\":\\\"1970-01-01T00:00:00.004Z\\\",\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\":{\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\":\\\"4\\\",\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\":{\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\":4}}},\\\"nullCount\\\":{\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\":0,\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\":0,\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\":0,\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\":0,\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\":0,\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\":0,\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\":0,\\\"col-836e8d56-f73a-4203-9c0f-a50476468c2c\\\":0,\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\":0,\\\"col-da9f099c-f6bc-459c-a740-b02f658221e2\\\":0,\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\":0,\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\":0,\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\":{\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\":0,\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\":{\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\":0}},\\\"col-720537b9-44e0-4829-a189-f3742da4f095\\\":0,\\\"col-9f8d855c-cc39-426c-b579-f1b85b1b6991\\\":0,\\\"col-37f1b990-e228-4046-b98e-23934eb972b0\\\":0,\\\"col-11bac662-f43a-44e3-b419-17b2ee3ee612\\\":0,\\\"col-dbc4c186-5c6b-4309-82a2-fce1be10512f\\\":{\\\"col-c4278c59-01d8-4f81-be2e-9d3ead4ddc9e\\\":0,\\\"col-10aa4b27-4657-4ee4-a43a-ecf2f8fbb4d3\\\":0},\\\"col-ee9a73a7-e466-49ee-9a1f-69d01b60b88e\\\":0,\\\"col-9697fd7e-5f66-43c0-9fee-3cf2bc9c2d4b\\\":0,\\\"col-e146f9dd-9ce2-4dbb-8a91-54a393bab453\\\":0,\\\"col-a6fcf96f-53a6-4a16-9df1-59eb1f6e06bf\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-0321adc4-f601-4c9d-bb7c-a0ddf759c7b2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":17743,\"modificationTime\":1723094980594,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\":1,\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\":1,\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\":1,\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\":1,\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\":1.0,\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\":1.0,\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\":1.00,\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\":\\\"1\\\",\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\":\\\"2021-11-18\\\",\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\":\\\"1970-01-01T00:00:00.001Z\\\",\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\":{\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\":\\\"1\\\",\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\":{\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\":1}}},\\\"maxValues\\\":{\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\":3,\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\":3,\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\":3,\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\":3,\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\":3.0,\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\":3.0,\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\":3.00,\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\":\\\"3\\\",\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\":\\\"2021-11-18\\\",\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\":\\\"1970-01-01T00:00:00.003Z\\\",\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\":{\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\":\\\"3\\\",\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\":{\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\":3}}},\\\"nullCount\\\":{\\\"col-91e8b36f-2559-4aa7-b451-d30f24d5bf28\\\":1,\\\"col-d1a51f0a-855a-4752-8a35-76f52cda06b9\\\":1,\\\"col-0aa7e907-848d-47b7-9805-e014c0a09d83\\\":1,\\\"col-b55a2771-5f84-432a-9fe4-39e1ec1edfd7\\\":1,\\\"col-7722f52d-c253-4df9-96f4-a87a1dbef4dd\\\":1,\\\"col-1dda278c-a501-4499-a580-f3dff1b79834\\\":1,\\\"col-30298583-3b87-4b4d-ab7c-f1102050f720\\\":1,\\\"col-836e8d56-f73a-4203-9c0f-a50476468c2c\\\":1,\\\"col-ca55bcbb-453c-433c-995f-80e0ccc13cde\\\":1,\\\"col-da9f099c-f6bc-459c-a740-b02f658221e2\\\":1,\\\"col-7139e00c-abc4-4434-893d-1cf11d2b68bf\\\":1,\\\"col-292ce280-d844-4477-b078-87a64a6972d2\\\":1,\\\"col-ee8dc621-b36b-4784-bdb5-26c5c364e254\\\":{\\\"col-b47b5204-0100-46bc-92df-aa446f634191\\\":1,\\\"col-94e598c8-0480-4710-a288-fc332ed449de\\\":{\\\"col-75cd857b-0152-438b-a0b3-43db0d4daa55\\\":1}},\\\"col-720537b9-44e0-4829-a189-f3742da4f095\\\":1,\\\"col-9f8d855c-cc39-426c-b579-f1b85b1b6991\\\":1,\\\"col-37f1b990-e228-4046-b98e-23934eb972b0\\\":1,\\\"col-11bac662-f43a-44e3-b419-17b2ee3ee612\\\":1,\\\"col-dbc4c186-5c6b-4309-82a2-fce1be10512f\\\":{\\\"col-c4278c59-01d8-4f81-be2e-9d3ead4ddc9e\\\":1,\\\"col-10aa4b27-4657-4ee4-a43a-ecf2f8fbb4d3\\\":1},\\\"col-ee9a73a7-e466-49ee-9a1f-69d01b60b88e\\\":1,\\\"col-9697fd7e-5f66-43c0-9fee-3cf2bc9c2d4b\\\":1,\\\"col-e146f9dd-9ce2-4dbb-8a91-54a393bab453\\\":1,\\\"col-a6fcf96f-53a6-4a16-9df1-59eb1f6e06bf\\\":1}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/table-with-columnmapping-mode-name/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723094941755,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"6\",\"numOutputBytes\":\"35024\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"244f65e5-5ecb-4563-a425-bf680f1c0546\"}}\n{\"metaData\":{\"id\":\"5c372fe7-d0fd-48b9-ae93-5fc346eea359\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ByteType\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\"}},{\\\"name\\\":\\\"ShortType\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\"}},{\\\"name\\\":\\\"IntegerType\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\"}},{\\\"name\\\":\\\"LongType\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.physicalName\\\":\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\"}},{\\\"name\\\":\\\"FloatType\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.physicalName\\\":\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\"}},{\\\"name\\\":\\\"DoubleType\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.physicalName\\\":\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\"}},{\\\"name\\\":\\\"decimal\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.physicalName\\\":\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\"}},{\\\"name\\\":\\\"BooleanType\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.physicalName\\\":\\\"col-a9fd703e-4984-4977-a0ac-72f2f880095d\\\"}},{\\\"name\\\":\\\"StringType\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.physicalName\\\":\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\"}},{\\\"name\\\":\\\"BinaryType\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.physicalName\\\":\\\"col-05929041-8bb8-4db8-a4b3-0744cbef8116\\\"}},{\\\"name\\\":\\\"DateType\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":11,\\\"delta.columnMapping.physicalName\\\":\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\"}},{\\\"name\\\":\\\"TimestampType\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":12,\\\"delta.columnMapping.physicalName\\\":\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\"}},{\\\"name\\\":\\\"nested_struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":14,\\\"delta.columnMapping.physicalName\\\":\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\"}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":16,\\\"delta.columnMapping.physicalName\\\":\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":15,\\\"delta.columnMapping.physicalName\\\":\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":13,\\\"delta.columnMapping.physicalName\\\":\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\"}},{\\\"name\\\":\\\"array_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":17,\\\"delta.columnMapping.physicalName\\\":\\\"col-98146e9e-2472-4c0f-b1d5-a01e9a41bc46\\\"}},{\\\"name\\\":\\\"array_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":18,\\\"delta.columnMapping.physicalName\\\":\\\"col-1b6de8ee-deb0-43b4-8c94-bc9f7a556da8\\\"}},{\\\"name\\\":\\\"array_of_map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":19,\\\"delta.columnMapping.physicalName\\\":\\\"col-ad0356f5-43c1-401c-b291-6a5d211bc63c\\\"}},{\\\"name\\\":\\\"array_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":21,\\\"delta.columnMapping.physicalName\\\":\\\"col-93cac05c-2edd-409e-83f1-cebfd09bfc75\\\"}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":20,\\\"delta.columnMapping.physicalName\\\":\\\"col-5d038bad-e2b1-4fe7-a444-22fc3ac6e52a\\\"}},{\\\"name\\\":\\\"struct_of_arrays_maps_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":23,\\\"delta.columnMapping.physicalName\\\":\\\"col-fae23ffb-8aaa-43b7-982b-0930667ddae0\\\"}},{\\\"name\\\":\\\"ab\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":25,\\\"delta.columnMapping.physicalName\\\":\\\"col-6cb288b5-d45e-46aa-98be-e2aa06665232\\\"}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":24,\\\"delta.columnMapping.physicalName\\\":\\\"col-b816c867-4d4f-402f-85cf-786b58d14d05\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":22,\\\"delta.columnMapping.physicalName\\\":\\\"col-d4888d0e-973f-486e-b345-d69a53dd193c\\\"}},{\\\"name\\\":\\\"map_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":26,\\\"delta.columnMapping.physicalName\\\":\\\"col-ccb0e770-7cce-4293-88bb-d98d74883f21\\\"}},{\\\"name\\\":\\\"map_of_rows\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":28,\\\"delta.columnMapping.physicalName\\\":\\\"col-d8438cc9-31ef-4b23-ac1c-1dff3e0a5530\\\"}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":27,\\\"delta.columnMapping.physicalName\\\":\\\"col-5e848aab-e51a-46f0-8650-eee8f46f1149\\\"}},{\\\"name\\\":\\\"map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":29,\\\"delta.columnMapping.physicalName\\\":\\\"col-990f4ddc-316c-4548-83e2-3e1d7868706c\\\"}},{\\\"name\\\":\\\"map_of_maps\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"integer\\\",\\\"valueContainsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":30,\\\"delta.columnMapping.physicalName\\\":\\\"col-cca40ef1-caeb-4e56-ab0c-0d68b63f6785\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.columnMapping.maxColumnId\":\"30\"},\"createdTime\":1723094939386}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"add\":{\"path\":\"part-00000-2887cf52-61be-4009-afba-00b218602665-c000.snappy.parquet\",\"partitionValues\":{},\"size\":17281,\"modificationTime\":1723094941664,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\":0,\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\":0,\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\":0,\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\":0,\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\":0.0,\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\":0.0,\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\":0.00,\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\":\\\"0\\\",\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\":\\\"2021-11-18\\\",\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\":\\\"1970-01-01T00:00:00.000Z\\\",\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\":{\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\":\\\"0\\\",\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\":{\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\":0}}},\\\"maxValues\\\":{\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\":4,\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\":4,\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\":4,\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\":4,\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\":4.0,\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\":4.0,\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\":4.00,\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\":\\\"4\\\",\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\":\\\"2021-11-18\\\",\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\":\\\"1970-01-01T00:00:00.004Z\\\",\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\":{\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\":\\\"4\\\",\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\":{\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\":4}}},\\\"nullCount\\\":{\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\":0,\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\":0,\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\":0,\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\":0,\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\":0,\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\":0,\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\":0,\\\"col-a9fd703e-4984-4977-a0ac-72f2f880095d\\\":0,\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\":0,\\\"col-05929041-8bb8-4db8-a4b3-0744cbef8116\\\":0,\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\":0,\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\":0,\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\":{\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\":0,\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\":{\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\":0}},\\\"col-98146e9e-2472-4c0f-b1d5-a01e9a41bc46\\\":0,\\\"col-1b6de8ee-deb0-43b4-8c94-bc9f7a556da8\\\":0,\\\"col-ad0356f5-43c1-401c-b291-6a5d211bc63c\\\":0,\\\"col-5d038bad-e2b1-4fe7-a444-22fc3ac6e52a\\\":0,\\\"col-d4888d0e-973f-486e-b345-d69a53dd193c\\\":{\\\"col-fae23ffb-8aaa-43b7-982b-0930667ddae0\\\":0,\\\"col-b816c867-4d4f-402f-85cf-786b58d14d05\\\":0},\\\"col-ccb0e770-7cce-4293-88bb-d98d74883f21\\\":0,\\\"col-5e848aab-e51a-46f0-8650-eee8f46f1149\\\":0,\\\"col-990f4ddc-316c-4548-83e2-3e1d7868706c\\\":0,\\\"col-cca40ef1-caeb-4e56-ab0c-0d68b63f6785\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-b664b3db-62d8-4e02-9dc5-26dbce3abfc1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":17743,\"modificationTime\":1723094941664,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\":1,\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\":1,\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\":1,\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\":1,\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\":1.0,\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\":1.0,\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\":1.00,\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\":\\\"1\\\",\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\":\\\"2021-11-18\\\",\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\":\\\"1970-01-01T00:00:00.001Z\\\",\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\":{\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\":\\\"1\\\",\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\":{\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\":1}}},\\\"maxValues\\\":{\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\":3,\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\":3,\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\":3,\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\":3,\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\":3.0,\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\":3.0,\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\":3.00,\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\":\\\"3\\\",\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\":\\\"2021-11-18\\\",\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\":\\\"1970-01-01T00:00:00.003Z\\\",\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\":{\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\":\\\"3\\\",\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\":{\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\":3}}},\\\"nullCount\\\":{\\\"col-33314a5e-7dc1-438a-8f4d-df8c417071d6\\\":1,\\\"col-7237d244-656d-402c-a889-5540aa23d418\\\":1,\\\"col-267caf03-cf2f-450d-a6ee-5dbe81c86497\\\":1,\\\"col-f92689f0-399a-46e5-84b6-604670849d66\\\":1,\\\"col-9bd8f339-6e15-4ffb-8b53-ed3efe8e3482\\\":1,\\\"col-d3069408-bcd3-4bf4-a125-166cfd56841f\\\":1,\\\"col-d2821ced-9890-4ac8-b196-4649c82547f4\\\":1,\\\"col-a9fd703e-4984-4977-a0ac-72f2f880095d\\\":1,\\\"col-7f007ffd-c4e2-4d7e-84c2-af985d4e3feb\\\":1,\\\"col-05929041-8bb8-4db8-a4b3-0744cbef8116\\\":1,\\\"col-af3ec5e1-41cc-4ccd-ad38-b094fb567ec5\\\":1,\\\"col-50ab9e52-7811-4057-96b0-cf61d5be358e\\\":1,\\\"col-e08b8668-6c8f-4081-aa77-357325212630\\\":{\\\"col-7295e744-4c2c-48a1-baf5-3bb131ec68ea\\\":1,\\\"col-15ecbe5f-906d-4d64-a627-eb16eb4b4410\\\":{\\\"col-562033a2-86d8-4eb3-83e6-87eb2f27314f\\\":1}},\\\"col-98146e9e-2472-4c0f-b1d5-a01e9a41bc46\\\":1,\\\"col-1b6de8ee-deb0-43b4-8c94-bc9f7a556da8\\\":1,\\\"col-ad0356f5-43c1-401c-b291-6a5d211bc63c\\\":1,\\\"col-5d038bad-e2b1-4fe7-a444-22fc3ac6e52a\\\":1,\\\"col-d4888d0e-973f-486e-b345-d69a53dd193c\\\":{\\\"col-fae23ffb-8aaa-43b7-982b-0930667ddae0\\\":1,\\\"col-b816c867-4d4f-402f-85cf-786b58d14d05\\\":1},\\\"col-ccb0e770-7cce-4293-88bb-d98d74883f21\\\":1,\\\"col-5e848aab-e51a-46f0-8650-eee8f46f1149\\\":1,\\\"col-990f4ddc-316c-4548-83e2-3e1d7868706c\\\":1,\\\"col-cca40ef1-caeb-4e56-ab0c-0d68b63f6785\\\":1}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/table-with-icebegCompatV2Enabled/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723094912799,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"6\",\"numOutputBytes\":\"44076\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"9e32df60-b1a0-4229-9820-bd9b56ccb304\"}}\n{\"metaData\":{\"id\":\"5d389c1e-778b-45c2-b1d9-01b7e60f63ec\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ByteType\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\"}},{\\\"name\\\":\\\"ShortType\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\"}},{\\\"name\\\":\\\"IntegerType\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\"}},{\\\"name\\\":\\\"LongType\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\"}},{\\\"name\\\":\\\"FloatType\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\"}},{\\\"name\\\":\\\"DoubleType\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\"}},{\\\"name\\\":\\\"decimal\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\"}},{\\\"name\\\":\\\"BooleanType\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-8dfa7127-0477-4ddf-9ce0-87baae1ca166\\\"}},{\\\"name\\\":\\\"StringType\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\"}},{\\\"name\\\":\\\"BinaryType\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-14d8b433-13d3-40b4-85bf-698b93a15edd\\\"}},{\\\"name\\\":\\\"DateType\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":11,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\"}},{\\\"name\\\":\\\"TimestampType\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":12,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\"}},{\\\"name\\\":\\\"nested_struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":14,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\"}},{\\\"name\\\":\\\"ac\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":16,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":15,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":13,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\"}},{\\\"name\\\":\\\"array_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":17,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-9c4dbf49-152b-4f5d-8dfb-3a8884f78cb7.element\\\":31},\\\"delta.columnMapping.physicalName\\\":\\\"col-9c4dbf49-152b-4f5d-8dfb-3a8884f78cb7\\\"}},{\\\"name\\\":\\\"array_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":18,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-77d17ccf-6d7d-4129-a93a-b6d4fd9c3483.element.element\\\":33,\\\"col-77d17ccf-6d7d-4129-a93a-b6d4fd9c3483.element\\\":32},\\\"delta.columnMapping.physicalName\\\":\\\"col-77d17ccf-6d7d-4129-a93a-b6d4fd9c3483\\\"}},{\\\"name\\\":\\\"array_of_map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":19,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80.element.value.element\\\":37,\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80.element.value\\\":36,\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80.element.key\\\":35,\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80.element\\\":34},\\\"delta.columnMapping.physicalName\\\":\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80\\\"}},{\\\"name\\\":\\\"array_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":21,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-f52ebe0f-00e0-4602-a142-192ec849afa2\\\"}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":20,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-ef476911-2c9b-49c5-8c76-7fc03c8953f8.element\\\":38},\\\"delta.columnMapping.physicalName\\\":\\\"col-ef476911-2c9b-49c5-8c76-7fc03c8953f8\\\"}},{\\\"name\\\":\\\"struct_of_arrays_maps_of_structs\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aa\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":23,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-53e9356f-7271-4a56-8586-77976034c213.element\\\":39},\\\"delta.columnMapping.physicalName\\\":\\\"col-53e9356f-7271-4a56-8586-77976034c213\\\"}},{\\\"name\\\":\\\"ab\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"aca\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":25,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-4cb3734b-e335-4aab-9d8c-decacaf891fc\\\"}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":24,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-70da66c1-651b-4562-908b-330e91ee6db2.key\\\":40,\\\"col-70da66c1-651b-4562-908b-330e91ee6db2.value\\\":42,\\\"col-70da66c1-651b-4562-908b-330e91ee6db2.key.element\\\":41},\\\"delta.columnMapping.physicalName\\\":\\\"col-70da66c1-651b-4562-908b-330e91ee6db2\\\"}}]},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":22,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-7431b180-3300-4771-a556-693c9be39683\\\"}},{\\\"name\\\":\\\"map_of_prims\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":26,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-c5aff22c-e8de-4618-8318-31386aa7721f.key\\\":43,\\\"col-c5aff22c-e8de-4618-8318-31386aa7721f.value\\\":44},\\\"delta.columnMapping.physicalName\\\":\\\"col-c5aff22c-e8de-4618-8318-31386aa7721f\\\"}},{\\\"name\\\":\\\"map_of_rows\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"ab\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":28,\\\"delta.columnMapping.nested.ids\\\":{},\\\"delta.columnMapping.physicalName\\\":\\\"col-be842636-62e7-4d96-b26c-64081616556b\\\"}}]},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":27,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-8701e73b-45d6-4b23-87ac-c871996ad7a9.key\\\":45,\\\"col-8701e73b-45d6-4b23-87ac-c871996ad7a9.value\\\":46},\\\"delta.columnMapping.physicalName\\\":\\\"col-8701e73b-45d6-4b23-87ac-c871996ad7a9\\\"}},{\\\"name\\\":\\\"map_of_arrays\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":29,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-8e1bb9a3-0c4d-4f5c-b95f-568c0703c77f.value\\\":48,\\\"col-8e1bb9a3-0c4d-4f5c-b95f-568c0703c77f.value.element\\\":49,\\\"col-8e1bb9a3-0c4d-4f5c-b95f-568c0703c77f.key\\\":47},\\\"delta.columnMapping.physicalName\\\":\\\"col-8e1bb9a3-0c4d-4f5c-b95f-568c0703c77f\\\"}},{\\\"name\\\":\\\"map_of_maps\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"integer\\\",\\\"valueContainsNull\\\":true},\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":30,\\\"delta.columnMapping.nested.ids\\\":{\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1.value.key\\\":52,\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1.key\\\":50,\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1.value.value\\\":53,\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1.value\\\":51},\\\"delta.columnMapping.physicalName\\\":\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableIcebergCompatV2\":\"true\",\"delta.columnMapping.mode\":\"id\",\"delta.columnMapping.maxColumnId\":\"53\"},\"createdTime\":1723094910279}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":7,\"writerFeatures\":[\"columnMapping\",\"icebergCompatV2\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"part-00000-cbb3f19e-57e0-4922-a6c3-f211a65d918f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":21807,\"modificationTime\":1723094912648,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\":0,\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\":0,\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\":0,\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\":0,\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\":0.0,\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\":0.0,\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\":0.00,\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\":\\\"0\\\",\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\":\\\"2021-11-18\\\",\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\":\\\"1970-01-01T00:00:00.000Z\\\",\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\":{\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\":\\\"0\\\",\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\":{\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\":0}}},\\\"maxValues\\\":{\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\":4,\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\":4,\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\":4,\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\":4,\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\":4.0,\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\":4.0,\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\":4.00,\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\":\\\"4\\\",\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\":\\\"2021-11-18\\\",\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\":\\\"1970-01-01T00:00:00.004Z\\\",\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\":{\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\":\\\"4\\\",\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\":{\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\":4}}},\\\"nullCount\\\":{\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\":0,\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\":0,\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\":0,\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\":0,\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\":0,\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\":0,\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\":0,\\\"col-8dfa7127-0477-4ddf-9ce0-87baae1ca166\\\":0,\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\":0,\\\"col-14d8b433-13d3-40b4-85bf-698b93a15edd\\\":0,\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\":0,\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\":0,\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\":{\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\":0,\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\":{\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\":0}},\\\"col-9c4dbf49-152b-4f5d-8dfb-3a8884f78cb7\\\":0,\\\"col-77d17ccf-6d7d-4129-a93a-b6d4fd9c3483\\\":0,\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80\\\":0,\\\"col-ef476911-2c9b-49c5-8c76-7fc03c8953f8\\\":0,\\\"col-7431b180-3300-4771-a556-693c9be39683\\\":{\\\"col-53e9356f-7271-4a56-8586-77976034c213\\\":0,\\\"col-70da66c1-651b-4562-908b-330e91ee6db2\\\":0},\\\"col-c5aff22c-e8de-4618-8318-31386aa7721f\\\":0,\\\"col-8701e73b-45d6-4b23-87ac-c871996ad7a9\\\":0,\\\"col-8e1bb9a3-0c4d-4f5c-b95f-568c0703c77f\\\":0,\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1\\\":0}}\",\"tags\":{\"ICEBERG_COMPAT_VERSION\":\"2\"}}}\n{\"add\":{\"path\":\"part-00001-5bf41539-fbc6-4b96-9f42-946d36a7f4c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":22269,\"modificationTime\":1723094912648,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\":1,\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\":1,\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\":1,\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\":1,\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\":1.0,\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\":1.0,\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\":1.00,\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\":\\\"1\\\",\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\":\\\"2021-11-18\\\",\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\":\\\"1970-01-01T00:00:00.001Z\\\",\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\":{\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\":\\\"1\\\",\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\":{\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\":1}}},\\\"maxValues\\\":{\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\":3,\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\":3,\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\":3,\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\":3,\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\":3.0,\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\":3.0,\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\":3.00,\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\":\\\"3\\\",\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\":\\\"2021-11-18\\\",\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\":\\\"1970-01-01T00:00:00.003Z\\\",\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\":{\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\":\\\"3\\\",\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\":{\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\":3}}},\\\"nullCount\\\":{\\\"col-150baa08-2441-4182-b1d2-6c33e9b2f6e6\\\":1,\\\"col-aaf5094f-3435-407f-bc7c-8d5cbd2bce22\\\":1,\\\"col-a9123e30-97e4-428e-a179-36af4908b4f3\\\":1,\\\"col-6a302d0e-d156-40e2-93b0-a26ee0a54eb4\\\":1,\\\"col-34f01baf-8cff-4abc-bf78-0494ba88070d\\\":1,\\\"col-0ef81992-71a5-47da-9c4c-45c76084bd54\\\":1,\\\"col-2c4cb239-3129-476c-8c0b-8e1f9ef575b8\\\":1,\\\"col-8dfa7127-0477-4ddf-9ce0-87baae1ca166\\\":1,\\\"col-d588bf1d-e372-4f26-8572-5dd32730bc20\\\":1,\\\"col-14d8b433-13d3-40b4-85bf-698b93a15edd\\\":1,\\\"col-43df1513-711d-42c0-ae37-1988c0c7478f\\\":1,\\\"col-1a000afa-7d0f-4822-85fc-6285cf51e460\\\":1,\\\"col-8219db36-f0b0-4529-ac76-2e0da218668c\\\":{\\\"col-c0d2b42d-365a-40ce-ac1d-e33605b4b595\\\":1,\\\"col-d2e13fb9-c6ae-4483-a580-7a27b4226700\\\":{\\\"col-99fc36e3-1691-4d36-bc4b-91f63779d1c2\\\":1}},\\\"col-9c4dbf49-152b-4f5d-8dfb-3a8884f78cb7\\\":1,\\\"col-77d17ccf-6d7d-4129-a93a-b6d4fd9c3483\\\":1,\\\"col-1755e9a3-fa11-4062-8092-fe8f1664be80\\\":1,\\\"col-ef476911-2c9b-49c5-8c76-7fc03c8953f8\\\":1,\\\"col-7431b180-3300-4771-a556-693c9be39683\\\":{\\\"col-53e9356f-7271-4a56-8586-77976034c213\\\":1,\\\"col-70da66c1-651b-4562-908b-330e91ee6db2\\\":1},\\\"col-c5aff22c-e8de-4618-8318-31386aa7721f\\\":1,\\\"col-8701e73b-45d6-4b23-87ac-c871996ad7a9\\\":1,\\\"col-8e1bb9a3-0c4d-4f5c-b95f-568c0703c77f\\\":1,\\\"col-9da911c6-054c-4167-881e-4c03ce7a3fd1\\\":1}}\",\"tags\":{\"ICEBERG_COMPAT_VERSION\":\"2\"}}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-partition-changes-a/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724026157,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"part5\\\"]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"9ce7bb6f-507b-4925-a820-f33601e5d700\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part5\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part5\"],\"configuration\":{},\"createdTime\":1603724025794}}\n{\"add\":{\"path\":\"part5=0/part-00000-67b6882e-f49f-4df5-9850-b5e8a72f4917.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"0\"},\"size\":429,\"modificationTime\":1603724025000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=1/part-00000-8a40c3d2-f658-4131-a17f-388265ab04b7.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"1\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=2/part-00000-ec6e3a2e-ecbf-4d39-9076-37e523cd62f1.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"2\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=3/part-00000-eaf1edf4-b9da-4df8-b957-08583e2a1d1b.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"3\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=4/part-00000-ce66c2ca-8fdf-48d3-a6e7-5980a370461a.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"4\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=0/part-00001-4f02a740-31dc-46c6-bc0e-c19d164ac82d.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"0\"},\"size\":429,\"modificationTime\":1603724025000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=1/part-00001-3dcad520-b001-4829-a6e5-3d578b0964f4.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"1\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=2/part-00001-e20bae81-3f27-4c5c-aeca-5cfa6b38615c.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"2\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=3/part-00001-b9c6b926-a274-4d8e-b882-31c4aac05038.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"3\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=4/part-00001-5705917d-d837-4d7f-b8c4-f0ada8cf9663.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"4\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-partition-changes-b/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724026157,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"part5\\\"]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"9ce7bb6f-507b-4925-a820-f33601e5d700\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part5\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part5\"],\"configuration\":{},\"createdTime\":1603724025794}}\n{\"add\":{\"path\":\"part5=0/part-00000-67b6882e-f49f-4df5-9850-b5e8a72f4917.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"0\"},\"size\":429,\"modificationTime\":1603724025000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=1/part-00000-8a40c3d2-f658-4131-a17f-388265ab04b7.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"1\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=2/part-00000-ec6e3a2e-ecbf-4d39-9076-37e523cd62f1.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"2\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=3/part-00000-eaf1edf4-b9da-4df8-b957-08583e2a1d1b.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"3\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=4/part-00000-ce66c2ca-8fdf-48d3-a6e7-5980a370461a.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"4\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=0/part-00001-4f02a740-31dc-46c6-bc0e-c19d164ac82d.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"0\"},\"size\":429,\"modificationTime\":1603724025000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=1/part-00001-3dcad520-b001-4829-a6e5-3d578b0964f4.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"1\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=2/part-00001-e20bae81-3f27-4c5c-aeca-5cfa6b38615c.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"2\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=3/part-00001-b9c6b926-a274-4d8e-b882-31c4aac05038.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"3\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part5=4/part-00001-5705917d-d837-4d7f-b8c4-f0ada8cf9663.c000.snappy.parquet\",\"partitionValues\":{\"part5\":\"4\"},\"size\":429,\"modificationTime\":1603724026000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-partition-changes-b/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724028432,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[\\\"part2\\\"]\"},\"readVersion\":0,\"isBlindAppend\":false}}\n{\"metaData\":{\"id\":\"9ce7bb6f-507b-4925-a820-f33601e5d700\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part2\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part2\"],\"configuration\":{},\"createdTime\":1603724025794}}\n{\"add\":{\"path\":\"part2=0/part-00000-7bce012e-f358-4a97-91da-55c4d3266fbe.c000.snappy.parquet\",\"partitionValues\":{\"part2\":\"0\"},\"size\":442,\"modificationTime\":1603724028000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part2=1/part-00000-82368d1d-588b-487a-be01-16dc85260296.c000.snappy.parquet\",\"partitionValues\":{\"part2\":\"1\"},\"size\":437,\"modificationTime\":1603724028000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part2=0/part-00001-2a830e69-78f3-4d09-9b2c-3bfd9debc2f0.c000.snappy.parquet\",\"partitionValues\":{\"part2\":\"0\"},\"size\":437,\"modificationTime\":1603724028000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part2=1/part-00001-0a72544a-fb83-4eaa-8d62-9e6ab59afa8b.c000.snappy.parquet\",\"partitionValues\":{\"part2\":\"1\"},\"size\":442,\"modificationTime\":1603724028000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=0/part-00000-67b6882e-f49f-4df5-9850-b5e8a72f4917.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=0/part-00001-4f02a740-31dc-46c6-bc0e-c19d164ac82d.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=1/part-00001-3dcad520-b001-4829-a6e5-3d578b0964f4.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=2/part-00000-ec6e3a2e-ecbf-4d39-9076-37e523cd62f1.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=2/part-00001-e20bae81-3f27-4c5c-aeca-5cfa6b38615c.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=4/part-00000-ce66c2ca-8fdf-48d3-a6e7-5980a370461a.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=1/part-00000-8a40c3d2-f658-4131-a17f-388265ab04b7.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=3/part-00000-eaf1edf4-b9da-4df8-b957-08583e2a1d1b.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=3/part-00001-b9c6b926-a274-4d8e-b882-31c4aac05038.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part5=4/part-00001-5705917d-d837-4d7f-b8c4-f0ada8cf9663.c000.snappy.parquet\",\"deletionTimestamp\":1603724028432,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-schema-changes-a/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724023478,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"37664cd7-239f-4dbc-a56b-d47437be8ddb\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724023419}}\n{\"add\":{\"path\":\"part-00000-83680aa8-547c-40bc-8ca9-5c10997e307b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":449,\"modificationTime\":1603724023000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-3c1f89ce-a996-4d44-a79c-21a6f3d53138-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724023000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-schema-changes-b/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724023478,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"37664cd7-239f-4dbc-a56b-d47437be8ddb\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724023419}}\n{\"add\":{\"path\":\"part-00000-83680aa8-547c-40bc-8ca9-5c10997e307b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":449,\"modificationTime\":1603724023000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-3c1f89ce-a996-4d44-a79c-21a6f3d53138-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724023000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-schema-changes-b/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724024783,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"metaData\":{\"id\":\"37664cd7-239f-4dbc-a56b-d47437be8ddb\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724023419}}\n{\"add\":{\"path\":\"part-00000-a830a49c-6cc8-4caf-80a5-7ff8a959bd53-c000.snappy.parquet\",\"partitionValues\":{},\"size\":711,\"modificationTime\":1603724024000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-5fdfd303-d5e8-4e77-9b5d-4e831fa723e1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":711,\"modificationTime\":1603724024000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-start/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724019870,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"d49dc19d-c206-4b38-be18-d8b7bdb07a07\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724019791}}\n{\"add\":{\"path\":\"part-00000-c6271e23-2077-455c-94f9-52866f930213-c000.snappy.parquet\",\"partitionValues\":{},\"size\":449,\"modificationTime\":1603724019000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e6177404-aaf5-4e07-8dc0-543a90f4657f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724019000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-start-start20/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724019870,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"d49dc19d-c206-4b38-be18-d8b7bdb07a07\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724019791}}\n{\"add\":{\"path\":\"part-00000-c6271e23-2077-455c-94f9-52866f930213-c000.snappy.parquet\",\"partitionValues\":{},\"size\":449,\"modificationTime\":1603724019000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e6177404-aaf5-4e07-8dc0-543a90f4657f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724019000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-start-start20/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724021190,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-632e29c6-fedf-4822-9223-233d6d8d9086-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724021000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-90fee26a-1483-44e3-b239-805343fec254-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724021000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-start-start20-start40/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724019870,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"d49dc19d-c206-4b38-be18-d8b7bdb07a07\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603724019791}}\n{\"add\":{\"path\":\"part-00000-c6271e23-2077-455c-94f9-52866f930213-c000.snappy.parquet\",\"partitionValues\":{},\"size\":449,\"modificationTime\":1603724019000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-e6177404-aaf5-4e07-8dc0-543a90f4657f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724019000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-start-start20-start40/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724021190,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-632e29c6-fedf-4822-9223-233d6d8d9086-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724021000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-90fee26a-1483-44e3-b239-805343fec254-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724021000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/time-travel-start-start20-start40/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603724022561,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-aef3cbc1-92ef-43b1-8258-284d13163fbb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724022000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-2b364e64-4212-4a35-a95f-ab64504f7c5c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":451,\"modificationTime\":1603724022000,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/type-widening/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1727266110116,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"3694\"},\"engineInfo\":\"Apache-Spark/3.5.2 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"80c33fca-d936-40cf-81fb-7ef52b67e25b\"}}\n{\"metaData\":{\"id\":\"db0018ee-037b-41f7-8266-85058ceafb06\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"byte_long\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"int_long\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"float_double\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"byte_double\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"short_double\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"int_double\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_decimal_same_scale\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_decimal_greater_scale\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"byte_decimal\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"short_decimal\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"int_decimal\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"long_decimal\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date_timestamp_ntz\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1727266102938}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-1045efe0-45bb-4b99-9f83-5ffa04a63ab2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3694,\"modificationTime\":1727266109760,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"byte_long\\\":1,\\\"int_long\\\":2,\\\"float_double\\\":3.4,\\\"byte_double\\\":5,\\\"short_double\\\":6,\\\"int_double\\\":7,\\\"decimal_decimal_same_scale\\\":123.45,\\\"decimal_decimal_greater_scale\\\":67.89,\\\"byte_decimal\\\":1,\\\"short_decimal\\\":2,\\\"int_decimal\\\":3,\\\"long_decimal\\\":4,\\\"date_timestamp_ntz\\\":\\\"2024-09-09\\\"},\\\"maxValues\\\":{\\\"byte_long\\\":1,\\\"int_long\\\":2,\\\"float_double\\\":3.4,\\\"byte_double\\\":5,\\\"short_double\\\":6,\\\"int_double\\\":7,\\\"decimal_decimal_same_scale\\\":123.45,\\\"decimal_decimal_greater_scale\\\":67.89,\\\"byte_decimal\\\":1,\\\"short_decimal\\\":2,\\\"int_decimal\\\":3,\\\"long_decimal\\\":4,\\\"date_timestamp_ntz\\\":\\\"2024-09-09\\\"},\\\"nullCount\\\":{\\\"byte_long\\\":0,\\\"int_long\\\":0,\\\"float_double\\\":0,\\\"byte_double\\\":0,\\\"short_double\\\":0,\\\"int_double\\\":0,\\\"decimal_decimal_same_scale\\\":0,\\\"decimal_decimal_greater_scale\\\":0,\\\"byte_decimal\\\":0,\\\"short_decimal\\\":0,\\\"int_decimal\\\":0,\\\"long_decimal\\\":0,\\\"date_timestamp_ntz\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/type-widening/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1727266114275,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.enableTypeWidening\\\":\\\"true\\\",\\\"delta.feature.timestampntz\\\":\\\"supported\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.2 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"7b869171-851a-4a8d-96e6-baee5496b98f\"}}\n{\"metaData\":{\"id\":\"db0018ee-037b-41f7-8266-85058ceafb06\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"byte_long\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"int_long\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"float_double\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"byte_double\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"short_double\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"int_double\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_decimal_same_scale\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"decimal_decimal_greater_scale\\\",\\\"type\\\":\\\"decimal(10,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"byte_decimal\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"short_decimal\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"int_decimal\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"long_decimal\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"date_timestamp_ntz\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableTypeWidening\":\"true\"},\"createdTime\":1727266102938}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"timestampNtz\",\"typeWidening-preview\"],\"writerFeatures\":[\"timestampNtz\",\"typeWidening-preview\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/type-widening/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1727266116833,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"4059\"},\"engineInfo\":\"Apache-Spark/3.5.2 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"2e63edee-6d96-4d12-90af-85b90f4fa9e5\"}}\n{\"metaData\":{\"id\":\"db0018ee-037b-41f7-8266-85058ceafb06\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"byte_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"long\\\",\\\"fromType\\\":\\\"byte\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"int_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"long\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"float_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"double\\\",\\\"fromType\\\":\\\"float\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"byte_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"double\\\",\\\"fromType\\\":\\\"byte\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"short_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"double\\\",\\\"fromType\\\":\\\"short\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"int_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"double\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"decimal_decimal_same_scale\\\",\\\"type\\\":\\\"decimal(20,2)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"decimal(20,2)\\\",\\\"fromType\\\":\\\"decimal(10,2)\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"decimal_decimal_greater_scale\\\",\\\"type\\\":\\\"decimal(20,5)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"decimal(20,5)\\\",\\\"fromType\\\":\\\"decimal(10,2)\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"byte_decimal\\\",\\\"type\\\":\\\"decimal(11,1)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"decimal(11,1)\\\",\\\"fromType\\\":\\\"byte\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"short_decimal\\\",\\\"type\\\":\\\"decimal(11,1)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"decimal(11,1)\\\",\\\"fromType\\\":\\\"short\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"int_decimal\\\",\\\"type\\\":\\\"decimal(11,1)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"decimal(11,1)\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"long_decimal\\\",\\\"type\\\":\\\"decimal(21,1)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"decimal(21,1)\\\",\\\"fromType\\\":\\\"long\\\",\\\"tableVersion\\\":2}]}},{\\\"name\\\":\\\"date_timestamp_ntz\\\",\\\"type\\\":\\\"timestamp_ntz\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"timestamp_ntz\\\",\\\"fromType\\\":\\\"date\\\",\\\"tableVersion\\\":2}]}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableTypeWidening\":\"true\"},\"createdTime\":1727266102938}}\n{\"add\":{\"path\":\"part-00000-cd317895-4ae0-4292-b918-62d4ca832bd7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4059,\"modificationTime\":1727266116789,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"byte_long\\\":9223372036854775807,\\\"int_long\\\":9223372036854775807,\\\"float_double\\\":1.234567890123,\\\"byte_double\\\":1.234567890123,\\\"short_double\\\":1.234567890123,\\\"int_double\\\":1.234567890123,\\\"decimal_decimal_same_scale\\\":12345678901234.56,\\\"decimal_decimal_greater_scale\\\":12345678901.23456,\\\"byte_decimal\\\":123.4,\\\"short_decimal\\\":12345.6,\\\"int_decimal\\\":1234567890.1,\\\"long_decimal\\\":123456789012345678.9,\\\"date_timestamp_ntz\\\":\\\"2024-09-09T12:34:56.123\\\"},\\\"maxValues\\\":{\\\"byte_long\\\":9223372036854775807,\\\"int_long\\\":9223372036854775807,\\\"float_double\\\":1.234567890123,\\\"byte_double\\\":1.234567890123,\\\"short_double\\\":1.234567890123,\\\"int_double\\\":1.234567890123,\\\"decimal_decimal_same_scale\\\":12345678901234.56,\\\"decimal_decimal_greater_scale\\\":12345678901.23456,\\\"byte_decimal\\\":123.4,\\\"short_decimal\\\":12345.6,\\\"int_decimal\\\":1234567890.1,\\\"long_decimal\\\":123456789012345678.9,\\\"date_timestamp_ntz\\\":\\\"2024-09-09T12:34:56.123\\\"},\\\"nullCount\\\":{\\\"byte_long\\\":0,\\\"int_long\\\":0,\\\"float_double\\\":0,\\\"byte_double\\\":0,\\\"short_double\\\":0,\\\"int_double\\\":0,\\\"decimal_decimal_same_scale\\\":0,\\\"decimal_decimal_greater_scale\\\":0,\\\"byte_decimal\\\":0,\\\"short_decimal\\\":0,\\\"int_decimal\\\":0,\\\"long_decimal\\\":0,\\\"date_timestamp_ntz\\\":0}}\",\"defaultRowCommitVersion\":2}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/type-widening-nested/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1727266119620,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1416\"},\"engineInfo\":\"Apache-Spark/3.5.2 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"034f1fec-b6d9-4957-93c1-09a19c323fc2\"}}\n{\"metaData\":{\"id\":\"43c8feba-0140-4d91-8c16-52f627a79cfe\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"integer\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1727266118466}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-138244f1-b939-40db-a4bd-d57cf3d214d2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1416,\"modificationTime\":1727266119587,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"struct\\\":{\\\"a\\\":1}},\\\"maxValues\\\":{\\\"struct\\\":{\\\"a\\\":1}},\\\"nullCount\\\":{\\\"struct\\\":{\\\"a\\\":0},\\\"map\\\":0,\\\"array\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/type-widening-nested/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1727266120320,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.enableTypeWidening\\\":\\\"true\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.2 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"f4db4fbf-a05b-41b4-9049-e1f28a08ec5b\"}}\n{\"metaData\":{\"id\":\"43c8feba-0140-4d91-8c16-52f627a79cfe\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"integer\\\",\\\"valueType\\\":\\\"integer\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"integer\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableTypeWidening\":\"true\"},\"createdTime\":1727266118466}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"typeWidening-preview\"],\"writerFeatures\":[\"typeWidening-preview\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/type-widening-nested/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1727266121897,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"1519\"},\"engineInfo\":\"Apache-Spark/3.5.2 Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"55211a5a-9d2b-4367-929a-ac5850f91b78\"}}\n{\"metaData\":{\"id\":\"43c8feba-0140-4d91-8c16-52f627a79cfe\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"struct\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"long\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2}]}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"long\\\",\\\"valueType\\\":\\\"long\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"long\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2,\\\"fieldPath\\\":\\\"key\\\"},{\\\"toType\\\":\\\"long\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2,\\\"fieldPath\\\":\\\"value\\\"}]}},{\\\"name\\\":\\\"array\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"long\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.typeChanges\\\":[{\\\"toType\\\":\\\"long\\\",\\\"fromType\\\":\\\"integer\\\",\\\"tableVersion\\\":2,\\\"fieldPath\\\":\\\"element\\\"}]}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableTypeWidening\":\"true\"},\"createdTime\":1727266118466}}\n{\"add\":{\"path\":\"part-00000-1f777f86-350c-4181-b7ef-73df70847eac-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1519,\"modificationTime\":1727266121853,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"struct\\\":{\\\"a\\\":9223372036854775807}},\\\"maxValues\\\":{\\\"struct\\\":{\\\"a\\\":9223372036854775807}},\\\"nullCount\\\":{\\\"struct\\\":{\\\"a\\\":0},\\\"map\\\":0,\\\"array\\\":0}}\",\"defaultRowCommitVersion\":2}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/update-deleted-directory/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723978664,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"94c8d2b0-fbad-439b-a31f-17e17d93c2c7\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723978664}}\n{\"add\":{\"path\":\"1\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"2\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"3\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"4\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"5\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"6\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"7\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"8\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"9\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n{\"add\":{\"path\":\"10\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/update-deleted-directory/_delta_log/_last_checkpoint",
    "content": "{\"version\":0,\"size\":12}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-json/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496114594,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"f6282e54-afc6-4669-939b-0f8ba73062a0\"}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1714496114564}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-json/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496114748,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.checkpointPolicy\\\":\\\"v2\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"fddb3112-ca9b-48af-bf19-be23f1c36c22\"}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-json/_delta_log/00000000000000000002.checkpoint.6374b053-df23-479b-b2cf-c9c550132b49.json",
    "content": "{\"checkpointMetadata\":{\"version\":2}}\n{\"sidecar\":{\"path\":\"00000000000000000002.checkpoint.0000000001.0000000002.bd1885fd-6ec0-4370-b0f5-43b5162fd4de.parquet\",\"sizeInBytes\":9367,\"modificationTime\":1714496115780}}\n{\"sidecar\":{\"path\":\"00000000000000000002.checkpoint.0000000002.0000000002.0a8d73ee-aa83-49d0-9583-c99db75b89b2.parquet\",\"sizeInBytes\":9296,\"modificationTime\":1714496115788}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-json/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496115090,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1952\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"a76e8fca-8bab-42cc-9618-77f8c536968c\"}}\n{\"add\":{\"path\":\"part-00000-240b5dd6-323b-4f74-b6bc-ab9fdcacc630-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496115046,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":8},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-534ea355-2edd-4046-8d49-d932469170c7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":496,\"modificationTime\":1714496115048,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-4438bc9d-9c60-4dd2-9343-574743ea4ca8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":486,\"modificationTime\":1714496115087,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":5},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-ae431d66-23d5-4dc7-b961-136ce33e63da-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496115087,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":6},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-json/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":9,\"sizeInBytes\":19554,\"numOfAddFiles\":4,\"v2Checkpoint\":{\"path\":\"00000000000000000002.checkpoint.6374b053-df23-479b-b2cf-c9c550132b49.json\",\"sizeInBytes\":891,\"modificationTime\":1714496115810,\"nonFileActions\":[{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}},{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}},{\"checkpointMetadata\":{\"version\":2}}],\"sidecarFiles\":[{\"path\":\"00000000000000000002.checkpoint.0000000001.0000000002.bd1885fd-6ec0-4370-b0f5-43b5162fd4de.parquet\",\"sizeInBytes\":9367,\"modificationTime\":1714496115780},{\"path\":\"00000000000000000002.checkpoint.0000000002.0000000002.0a8d73ee-aa83-49d0-9583-c99db75b89b2.parquet\",\"sizeInBytes\":9296,\"modificationTime\":1714496115788}]},\"checksum\":\"d09f95a326aab562c60d415a32ddd216\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-parquet/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496109365,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"7517176e-cff7-46ac-b133-3cf096e2620d\"}}\n{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1714496109258}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-parquet/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496110834,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.checkpointPolicy\\\":\\\"v2\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"12ea26b9-c620-4104-95f6-654bcaabdda6\"}}\n{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496109258}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-parquet/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496112086,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1952\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"c9f86c17-1b30-44e7-873d-1e2102f54b0f\"}}\n{\"add\":{\"path\":\"part-00000-485b0fff-1c7b-4f14-92e9-a72300fcdf88-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496111974,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":8},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-f7a80035-0622-431e-832e-a756c65cb2a5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":496,\"modificationTime\":1714496111974,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-5754df9c-5a25-43a6-947b-f27840fddb1a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":486,\"modificationTime\":1714496112068,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":5},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-6ab7bbbb-e14d-4fa3-8767-06b509e0a666-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496112071,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":6},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/v2-checkpoint-parquet/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":9,\"sizeInBytes\":37269,\"numOfAddFiles\":4,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"clusteringProvider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"checkpointMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"sidecar\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"v2Checkpoint\":{\"path\":\"00000000000000000002.checkpoint.e8fa2696-9728-4e9c-b285-634743fdd4fb.parquet\",\"sizeInBytes\":18634,\"modificationTime\":1714496114276,\"nonFileActions\":[{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}},{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496109258}},{\"checkpointMetadata\":{\"version\":2}}],\"sidecarFiles\":[{\"path\":\"00000000000000000002.checkpoint.0000000001.0000000002.055454d8-329c-4e0e-864d-7f867075af33.parquet\",\"sizeInBytes\":9268,\"modificationTime\":1714496113961},{\"path\":\"00000000000000000002.checkpoint.0000000002.0000000002.33321cc1-9c55-4d1f-8511-fafe6d2e1133.parquet\",\"sizeInBytes\":9367,\"modificationTime\":1714496113961}]},\"checksum\":\"f81aaf268542b71bb3fc9b63f754f9df\"}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/versions-not-contiguous/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723995084,\"operation\":\"Manual Update\",\"operationParameters\":{},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"a564e335-d717-4a71-a1eb-25541a0f8d15\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1603723995084}}\n{\"add\":{\"path\":\"foo\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1603723995077,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/resources/golden/versions-not-contiguous/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1603723996094,\"operation\":\"Manual Update\",\"operationParameters\":{},\"readVersion\":1,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"foo\",\"partitionValues\":{},\"size\":1,\"modificationTime\":1603723996088,\"dataChange\":true}}\n"
  },
  {
    "path": "connectors/golden-tables/src/main/scala/io/delta/golden/GoldenTableUtils.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.golden\n\nimport java.io.File\n\nobject GoldenTableUtils {\n\n  lazy val classLoader = GoldenTableUtils.getClass.getClassLoader()\n  lazy val goldenResourceURL = classLoader.getResource(\"golden\")\n\n  def goldenTablePath(name: String): String = {\n    classLoader.getResource(s\"golden/$name\").getPath\n  }\n\n  def goldenTableFile(name: String): File = {\n    new File(classLoader.getResource(s\"golden/$name\").getFile)\n  }\n\n  def allTableNames(): Seq[String] = {\n    val root = new File(goldenResourceURL.getFile)\n\n    def loop(dir: File): Seq[File] = {\n      val children = Option(dir.listFiles()).getOrElse(Array.empty[File])\n      val subdirs = children.filter(_.isDirectory)\n      val here = if (new File(dir, \"_delta_log\").isDirectory) Seq(dir) else Seq.empty[File]\n      here ++ subdirs.flatMap(loop)\n    }\n\n    val rootPath = root.toPath\n    // Find all directories containing `_delta_log` under the `golden` resource and return their\n    // relative paths (to the golden root), sorted.\n    loop(root).map(f => rootPath.relativize(f.toPath).toString).sorted\n  }\n}\n"
  },
  {
    "path": "connectors/golden-tables/src/test/scala/io/delta/golden/GoldenTables.scala",
    "content": "/*\n * Copyright (2020-present) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.golden\n\nimport java.io.File\nimport java.math.{BigInteger, BigDecimal => JBigDecimal}\nimport java.sql.Timestamp\nimport java.time.ZoneOffset.UTC\nimport java.time.LocalDateTime\nimport java.util.{Locale, Random, TimeZone}\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.duration._\nimport scala.language.implicitConversions\n\nimport io.delta.tables.DeltaTable\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.network.util.JavaUtils\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.delta.{DeltaLog, OptimisticTransaction}\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.{Metadata, _}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\n/**\n * This is a special class to generate golden tables for other projects. Run the following commands\n * to re-generate all golden tables:\n * ```\n * GENERATE_GOLDEN_TABLES=1 build/sbt 'goldenTables/test'\n * ```\n *\n * To generate a single table (that is specified below) run:\n * ```\n * GENERATE_GOLDEN_TABLES=1 build/sbt 'goldenTables/testOnly *GoldenTables -- -z \"tbl_name\"'\n * ```\n *\n * After generating golden tables, be sure to package or test project standalone, otherwise the\n * test resources won't be available when running tests with IntelliJ.\n */\nclass GoldenTables extends QueryTest with SharedSparkSession {\n  import testImplicits._\n\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n    .set(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n    // disable _SUCCESS files\n    .set(\"spark.hadoop.mapreduce.fileoutputcommitter.marksuccessfuljobs\", \"false\")\n\n  // Timezone is fixed to America/Los_Angeles for timezone-sensitive tests\n  TimeZone.setDefault(TimeZone.getTimeZone(\"America/Los_Angeles\"))\n  // Add Locale setting\n  Locale.setDefault(Locale.US)\n\n  private val shouldGenerateGoldenTables = sys.env.contains(\"GENERATE_GOLDEN_TABLES\")\n\n  private lazy val goldenTablePath = {\n    val dir = new File(\"src/main/resources/golden\").getCanonicalFile\n    require(dir.exists(),\n      s\"Cannot find $dir. Please run `GENERATE_GOLDEN_TABLES=1 build/sbt 'goldenTables/test'`.\")\n    dir\n  }\n\n  private def copyDir(src: String, dest: String): Unit = {\n    FileUtils.copyDirectory(createGoldenTableFile(src), createGoldenTableFile(dest))\n  }\n\n  private def createGoldenTableFile(name: String): File = new File(goldenTablePath, name)\n\n  private def createHiveGoldenTableFile(name: String): File =\n    new File(createGoldenTableFile(\"hive\"), name)\n\n  private def generateGoldenTable(name: String,\n      createTableFile: String => File = createGoldenTableFile) (generator: String => Unit): Unit = {\n    if (shouldGenerateGoldenTables) {\n      test(name) {\n        val tablePath = createTableFile(name)\n        JavaUtils.deleteRecursively(tablePath)\n        generator(tablePath.getCanonicalPath)\n      }\n    }\n  }\n\n  /**\n   * Helper class for to ensure initial commits contain a Metadata action.\n   */\n  private implicit class OptimisticTxnTestHelper(txn: OptimisticTransaction) {\n    def commitManually(actions: Action*): Long = {\n      if (txn.readVersion == -1 && !actions.exists(_.isInstanceOf[Metadata])) {\n        val schema = new StructType()\n          .add(\"intCol\", IntegerType)\n          .json\n        txn.commit(Metadata(schemaString = schema) +: actions, ManualUpdate)\n      } else {\n        txn.commit(actions, ManualUpdate)\n      }\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // io.delta.standalone.internal.DeltaLogSuite\n  ///////////////////////////////////////////////////////////////////////////\n\n  /** TEST: DeltaLogSuite > checkpoint */\n  generateGoldenTable(\"checkpoint\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    (1 to 15).foreach { i =>\n      val txn = log.startTransaction()\n      val file = AddFile(i.toString, Map.empty, 1, 1, true) :: Nil\n      val delete: Seq[Action] = if (i > 1) {\n        RemoveFile((i - 1).toString, Some(System.currentTimeMillis()), true) :: Nil\n      } else {\n        Nil\n      }\n      txn.commitManually(delete ++ file: _*)\n    }\n  }\n\n  /** TEST: DeltaLogSuite > snapshot */\n  private def writeData(data: Seq[(Int, String)], mode: String, tablePath: String): Unit = {\n    data.toDS\n      .toDF(\"col1\", \"col2\")\n      .write\n      .mode(mode)\n      .format(\"delta\")\n      .save(tablePath)\n  }\n\n  generateGoldenTable(\"snapshot-data0\") { tablePath =>\n    writeData((0 until 10).map(x => (x, s\"data-0-$x\")), \"append\", tablePath)\n  }\n\n  generateGoldenTable(\"snapshot-data1\") { tablePath =>\n    copyDir(\"snapshot-data0\", \"snapshot-data1\")\n    writeData((0 until 10).map(x => (x, s\"data-1-$x\")), \"append\", tablePath)\n  }\n\n  generateGoldenTable(\"snapshot-data2\") { tablePath =>\n    copyDir(\"snapshot-data1\", \"snapshot-data2\")\n    writeData((0 until 10).map(x => (x, s\"data-2-$x\")), \"overwrite\", tablePath)\n  }\n\n  generateGoldenTable(\"snapshot-data3\") { tablePath =>\n    copyDir(\"snapshot-data2\", \"snapshot-data3\")\n    writeData((0 until 20).map(x => (x, s\"data-3-$x\")), \"append\", tablePath)\n  }\n\n  generateGoldenTable(\"snapshot-data2-deleted\") { tablePath =>\n    copyDir(\"snapshot-data3\", \"snapshot-data2-deleted\")\n    DeltaTable.forPath(spark, tablePath).delete(\"col2 like 'data-2-%'\")\n  }\n\n  generateGoldenTable(\"snapshot-repartitioned\") { tablePath =>\n    copyDir(\"snapshot-data2-deleted\", \"snapshot-repartitioned\")\n    spark.read\n      .format(\"delta\")\n      .load(tablePath)\n      .repartition(2)\n      .write\n      .option(\"dataChange\", \"false\")\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .save(tablePath)\n  }\n\n  generateGoldenTable(\"snapshot-vacuumed\") { tablePath =>\n    copyDir(\"snapshot-repartitioned\", \"snapshot-vacuumed\")\n    withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n      DeltaTable.forPath(spark, tablePath).vacuum(0.0)\n    }\n  }\n\n  /** TEST: DeltaLogSuite > SC-8078: update deleted directory */\n  generateGoldenTable(\"update-deleted-directory\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    val txn = log.startTransaction()\n    val files = (1 to 10).map(f => AddFile(f.toString, Map.empty, 1, 1, true))\n    txn.commitManually(files: _*)\n    log.checkpoint()\n  }\n\n  /** TEST: DeltaLogSuite > handle corrupted '_last_checkpoint' file */\n  generateGoldenTable(\"corrupted-last-checkpoint\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    val checkpointInterval = log.checkpointInterval(log.unsafeVolatileSnapshot.metadata)\n    for (f <- 0 to checkpointInterval) {\n      val txn = log.startTransaction()\n      txn.commitManually(AddFile(f.toString, Map.empty, 1, 1, true))\n    }\n  }\n\n  generateGoldenTable(\"corrupted-last-checkpoint-kernel\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    val checkpointInterval = log.checkpointInterval(log.unsafeVolatileSnapshot.metadata)\n    for (f <- 0 to checkpointInterval) {\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n    }\n    spark.range(100).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n\n    // Create an empty \"_last_checkpoint\" (corrupted)\n    val fs = log.LAST_CHECKPOINT.getFileSystem(log.newDeltaHadoopConf())\n    fs.create(log.LAST_CHECKPOINT, true /* overwrite */).close()\n  }\n\n  /** TEST: DeltaLogSuite > paths should be canonicalized */\n  {\n    def helper(scheme: String, path: String, tableSuffix: String): Unit = {\n      generateGoldenTable(s\"canonicalized-paths-$tableSuffix\") { tablePath =>\n        val log = DeltaLog.forTable(spark, new Path(tablePath))\n        new File(log.logPath.toUri).mkdirs()\n\n        val add = AddFile(path, Map.empty, 100L, 10L, dataChange = true)\n        val rm = RemoveFile(s\"$scheme$path\", Some(200L))\n\n        log.startTransaction().commitManually(add)\n        log.startTransaction().commitManually(rm)\n      }\n    }\n\n    // normal characters\n    helper(\"file:\", \"/some/unqualified/absolute/path\", \"normal-a\")\n    helper(\"file://\", \"/some/unqualified/absolute/path\", \"normal-b\")\n\n    // special characters\n    helper(\"file:\", new Path(\"/some/unqualified/with space/p@#h\").toUri.toString, \"special-a\")\n    helper(\"file://\", new Path(\"/some/unqualified/with space/p@#h\").toUri.toString, \"special-b\")\n  }\n\n  /** TEST: DeltaLogSuite > delete and re-add the same file in different transactions */\n  generateGoldenTable(s\"delete-re-add-same-file-different-transactions\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    assert(new File(log.logPath.toUri).mkdirs())\n\n    val add1 = AddFile(\"foo\", Map.empty, 1L, 1600000000000L, dataChange = true)\n    log.startTransaction().commitManually(add1)\n\n    val rm = add1.remove\n    log.startTransaction().commit(rm :: Nil, ManualUpdate)\n\n    val add2 = AddFile(\"foo\", Map.empty, 1L, 1700000000000L, dataChange = true)\n    log.startTransaction().commit(add2 :: Nil, ManualUpdate)\n\n    // Add a new transaction to replay logs using the previous snapshot. If it contained\n    // AddFile(\"foo\") and RemoveFile(\"foo\"), \"foo\" would get removed and fail this test.\n    val otherAdd = AddFile(\"bar\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n    log.startTransaction().commit(otherAdd :: Nil, ManualUpdate)\n  }\n\n  /** TEST: DeltaLogSuite > error - versions not contiguous */\n  generateGoldenTable(\"versions-not-contiguous\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    assert(new File(log.logPath.toUri).mkdirs())\n\n    val add1 = AddFile(\"foo\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n    log.startTransaction().commitManually(add1)\n\n    val add2 = AddFile(\"foo\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n    log.startTransaction().commit(add2 :: Nil, ManualUpdate)\n\n    val add3 = AddFile(\"foo\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n    log.startTransaction().commit(add3 :: Nil, ManualUpdate)\n\n    new File(new Path(log.logPath, \"00000000000000000001.json\").toUri).delete()\n  }\n\n  /** TEST: DeltaLogSuite > state reconstruction without Protocol/Metadata should fail */\n  Seq(\"protocol\", \"metadata\").foreach { action =>\n    generateGoldenTable(s\"deltalog-state-reconstruction-without-$action\") { tablePath =>\n      val log = DeltaLog.forTable(spark, new Path(tablePath))\n      assert(new File(log.logPath.toUri).mkdirs())\n\n      val selectedAction = if (action == \"metadata\") {\n        Protocol()\n      } else {\n        val schema = new StructType()\n          .add(\"intCol\", IntegerType)\n          .json\n        Metadata(schemaString = schema)\n      }\n\n      val file = AddFile(\"abc\", Map.empty, 1, 1, true)\n      log.store.write(\n        FileNames.unsafeDeltaFile(log.logPath, 0L),\n        Iterator(selectedAction, file).map(a => JsonUtils.toJson(a.wrap)))\n    }\n  }\n\n  /**\n   * TEST: DeltaLogSuite > state reconstruction from checkpoint with missing Protocol/Metadata\n   * should fail\n   */\n  Seq(\"protocol\", \"metadata\").foreach { action =>\n    generateGoldenTable(s\"deltalog-state-reconstruction-from-checkpoint-missing-$action\") {\n      tablePath =>\n        val log = DeltaLog.forTable(spark, tablePath)\n        val checkpointInterval = log.checkpointInterval(log.unsafeVolatileSnapshot.metadata)\n        // Create a checkpoint regularly\n        for (f <- 0 to checkpointInterval) {\n          val txn = log.startTransaction()\n          if (f == 0) {\n            txn.commitManually(AddFile(f.toString, Map.empty, 1, 1, true))\n          } else {\n            txn.commit(Seq(AddFile(f.toString, Map.empty, 1, 1, true)), ManualUpdate)\n          }\n        }\n\n        // Create an incomplete checkpoint without the action and overwrite the\n        // original checkpoint\n        val checkpointPath = FileNames.checkpointFileSingular(log.logPath, log.snapshot.version)\n        withTempDir { tmpCheckpoint =>\n          val takeAction = if (action == \"metadata\") {\n            \"protocol\"\n          } else {\n            \"metadata\"\n          }\n          val corruptedCheckpointData = spark.read.parquet(checkpointPath.toString)\n            .where(s\"add is not null or $takeAction is not null\")\n            .as[SingleAction].collect()\n\n          // Keep the add files and also filter by the additional condition\n          corruptedCheckpointData.toSeq.toDS().coalesce(1).write\n            .mode(\"overwrite\").parquet(tmpCheckpoint.toString)\n          val writtenCheckpoint =\n            tmpCheckpoint.listFiles().toSeq.filter(_.getName.startsWith(\"part\")).head\n          val checkpointFile = new File(checkpointPath.toUri)\n          new File(log.logPath.toUri).listFiles().toSeq.foreach { file =>\n            if (file.getName.startsWith(\".0\")) {\n              // we need to delete checksum files, otherwise trying to replace our incomplete\n              // checkpoint file fails due to the LocalFileSystem's checksum checks.\n              require(file.delete(), \"Failed to delete checksum file\")\n            }\n          }\n          require(checkpointFile.delete(), \"Failed to delete old checkpoint\")\n          require(writtenCheckpoint.renameTo(checkpointFile),\n            \"Failed to rename corrupt checkpoint\")\n        }\n    }\n  }\n\n  /** TEST: DeltaLogSuite > table protocol version greater than client reader protocol version */\n  generateGoldenTable(\"deltalog-invalid-protocol-version\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    assert(new File(log.logPath.toUri).mkdirs())\n\n    val file = AddFile(\"abc\", Map.empty, 1, 1, true)\n    val metadata = Metadata(\n      schemaString = new StructType().add(\"id\", IntegerType).json\n    )\n    log.store.write(FileNames.unsafeDeltaFile(log.logPath, 0L),\n\n      // Protocol reader version explicitly set too high\n      // Also include a Metadata\n      Iterator(Protocol(99), metadata, file).map(a => JsonUtils.toJson(a.wrap)))\n  }\n\n  /** TEST: DeltaLogSuite > get commit info */\n  generateGoldenTable(\"deltalog-commit-info\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    assert(new File(log.logPath.toUri).mkdirs())\n\n    val commitInfoFile = CommitInfo(\n      version = Some(0L),\n      inCommitTimestamp = None,\n      timestamp = new Timestamp(1540415658000L),\n      userId = Some(\"user_0\"),\n      userName = Some(\"username_0\"),\n      operation = \"WRITE\",\n      operationParameters = Map(\"test\" -> \"\\\"test\\\"\"),\n      job = Some(JobInfo(\n        \"job_id_0\", \"job_name_0\", \"job_run_id_0\", \"run_id_0\", \"job_owner_0\", \"trigger_type_0\")),\n      notebook = Some(NotebookInfo(\"notebook_id_0\")),\n      clusterId = Some(\"cluster_id_0\"),\n      readVersion = Some(-1L),\n      isolationLevel = Some(\"default\"),\n      isBlindAppend = Some(true),\n      operationMetrics = Some(Map(\"test\" -> \"test\")),\n      userMetadata = Some(\"foo\"),\n      tags = Some(Map(\"test\" -> \"test\")),\n      engineInfo = Some(\"OSS\"),\n      txnId = Some(\"txn_id_0\")\n    )\n\n    val addFile = AddFile(\"abc\", Map.empty, 1, 1, true)\n    log.store.write(\n      FileNames.unsafeDeltaFile(log.logPath, 0L),\n      Iterator(Metadata(), Protocol(), commitInfoFile, addFile).map(a => JsonUtils.toJson(a.wrap)))\n  }\n\n  /** TEST: DeltaLogSuite > getChanges - no data loss */\n  generateGoldenTable(\"deltalog-getChanges\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n\n    val schema = new StructType()\n      .add(\"part\", IntegerType)\n      .add(\"id\", IntegerType)\n    val metadata = Metadata(schemaString = schema.json)\n\n    val add1 = AddFile(\"fake/path/1\", Map.empty, 1, 1, dataChange = true)\n    val txn1 = log.startTransaction()\n    txn1.commitManually(metadata :: add1 :: Nil: _*)\n\n    val addCDC2 = AddCDCFile(\"fake/path/2\", Map(\"partition_foo\" -> \"partition_bar\"), 1,\n      Map(\"tag_foo\" -> \"tag_bar\"))\n    val remove2 = RemoveFile(\"fake/path/1\", Some(100), dataChange = true)\n    val txn2 = log.startTransaction()\n    txn2.commitManually(addCDC2 :: remove2 :: Nil: _*)\n\n    val setTransaction3 = SetTransaction(\"fakeAppId\", 3L, Some(200))\n    val txn3 = log.startTransaction()\n    txn3.commitManually(Protocol(1, 2) :: setTransaction3 :: Nil: _*)\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // io.delta.standalone.internal.ReadOnlyLogStoreSuite\n  ///////////////////////////////////////////////////////////////////////////\n\n  /** TEST: ReadOnlyLogStoreSuite > read */\n  generateGoldenTable(\"log-store-read\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    assert(new File(log.logPath.toUri).mkdirs())\n\n    val deltas = Seq(0, 1).map(i => new File(tablePath, i.toString)).map(_.getCanonicalPath)\n    log.store.write(deltas.head, Iterator(\"zero\", \"none\"))\n    log.store.write(deltas(1), Iterator(\"one\"))\n  }\n\n  /** TEST: ReadOnlyLogStoreSuite > listFrom */\n  generateGoldenTable(\"log-store-listFrom\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    assert(new File(log.logPath.toUri).mkdirs())\n\n    val deltas = Seq(0, 1, 2, 3, 4)\n      .map(i => new File(tablePath, i.toString))\n      .map(_.getCanonicalPath)\n\n    log.store.write(deltas(1), Iterator(\"zero\"))\n    log.store.write(deltas(2), Iterator(\"one\"))\n    log.store.write(deltas(3), Iterator(\"two\"))\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // io.delta.standalone.internal.DeltaTimeTravelSuite\n  ///////////////////////////////////////////////////////////////////////////\n\n  private implicit def durationToLong(duration: FiniteDuration): Long = {\n    duration.toMillis\n  }\n\n  /** Generate commits with the given timestamp in millis. */\n  private def generateCommits(location: String, commits: Long*): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, location)\n    var startVersion = deltaLog.snapshot.version + 1\n    commits.foreach { ts =>\n      val rangeStart = startVersion * 10\n      val rangeEnd = rangeStart + 10\n      spark.range(rangeStart, rangeEnd).write.format(\"delta\").mode(\"append\").save(location)\n      val file = new File(FileNames.unsafeDeltaFile(deltaLog.logPath, startVersion).toUri)\n      file.setLastModified(ts)\n      startVersion += 1\n    }\n  }\n\n  val start = 1540415658000L\n\n  generateGoldenTable(\"time-travel-start\") { tablePath =>\n    generateCommits(tablePath, start)\n  }\n\n  generateGoldenTable(\"time-travel-start-start20\") { tablePath =>\n    copyDir(\"time-travel-start\", \"time-travel-start-start20\")\n    generateCommits(tablePath, start + 20.minutes)\n  }\n\n  generateGoldenTable(\"time-travel-start-start20-start40\") { tablePath =>\n    copyDir(\"time-travel-start-start20\", \"time-travel-start-start20-start40\")\n    generateCommits(tablePath, start + 40.minutes)\n  }\n\n  /**\n   * TEST: DeltaTimeTravelSuite > time travel with schema changes - should instantiate old schema\n   */\n  generateGoldenTable(\"time-travel-schema-changes-a\") { tablePath =>\n    spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n  }\n\n  generateGoldenTable(\"time-travel-schema-changes-b\") { tablePath =>\n    copyDir(\"time-travel-schema-changes-a\", \"time-travel-schema-changes-b\")\n    spark.range(10, 20).withColumn(\"part\", 'id)\n      .write.format(\"delta\").mode(\"append\").option(\"mergeSchema\", true).save(tablePath)\n  }\n\n  /**\n   * TEST: DeltaTimeTravelSuite > time travel with partition changes - should instantiate old schema\n   */\n  generateGoldenTable(\"time-travel-partition-changes-a\") { tablePath =>\n    spark.range(10).withColumn(\"part5\", 'id % 5).write.format(\"delta\")\n      .partitionBy(\"part5\").mode(\"append\").save(tablePath)\n  }\n\n  generateGoldenTable(\"time-travel-partition-changes-b\") { tablePath =>\n    copyDir(\"time-travel-partition-changes-a\", \"time-travel-partition-changes-b\")\n    spark.range(10, 20).withColumn(\"part2\", 'id % 2)\n      .write\n      .format(\"delta\")\n      .partitionBy(\"part2\")\n      .mode(\"overwrite\")\n      .option(\"overwriteSchema\", true)\n      .save(tablePath)\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // io.delta.standalone.internal.DeltaDataReaderSuite\n  ///////////////////////////////////////////////////////////////////////////\n\n  private def writeDataWithSchema(tblLoc: String, data: Seq[Row], schema: StructType): Unit = {\n    val df = spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n    df.write.format(\"delta\").mode(\"append\").save(tblLoc)\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - primitives */\n  generateGoldenTable(\"data-reader-primitives\") { tablePath =>\n    def createRow(i: Int): Row = {\n      Row(i, i.longValue, i.toByte, i.shortValue, i % 2 == 0, i.floatValue, i.doubleValue,\n        i.toString, Array[Byte](i.toByte, i.toByte), new JBigDecimal(i))\n    }\n\n    def createRowWithNullValues(): Row = {\n      Row(null, null, null, null, null, null, null, null, null, null)\n    }\n\n    val schema = new StructType()\n      .add(\"as_int\", IntegerType)\n      .add(\"as_long\", LongType)\n      .add(\"as_byte\", ByteType)\n      .add(\"as_short\", ShortType)\n      .add(\"as_boolean\", BooleanType)\n      .add(\"as_float\", FloatType)\n      .add(\"as_double\", DoubleType)\n      .add(\"as_string\", StringType)\n      .add(\"as_binary\", BinaryType)\n      .add(\"as_big_decimal\", DecimalType(1, 0))\n\n    val data = createRowWithNullValues() +: (0 until 10).map(createRow)\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > data reader can read partition values */\n  generateGoldenTable(\"data-reader-partition-values\") { tablePath =>\n    def createRow(i: Int): Row = {\n      Row(i, i.longValue, i.toByte, i.shortValue, i % 2 == 0, i.floatValue, i.doubleValue,\n        i.toString, \"null\", java.sql.Date.valueOf(\"2021-09-08\"),\n        java.sql.Timestamp.valueOf(\"2021-09-08 11:11:11\"), new JBigDecimal(i),\n        Array(Row(i), Row(i), Row(i)),\n        Row(i.toString, i.toString, Row(i, i.toLong)),\n        i.toString)\n    }\n\n    def createRowWithNullPartitionValues(): Row = {\n      Row(\n        // partition values\n        null, null, null, null, null, null, null, null, null, null, null, null,\n        // data values\n        Array(Row(2), Row(2), Row(2)),\n        Row(\"2\", \"2\", Row(2, 2L)),\n        \"2\")\n    }\n\n    val schema = new StructType()\n      // partition fields\n      .add(\"as_int\", IntegerType)\n      .add(\"as_long\", LongType)\n      .add(\"as_byte\", ByteType)\n      .add(\"as_short\", ShortType)\n      .add(\"as_boolean\", BooleanType)\n      .add(\"as_float\", FloatType)\n      .add(\"as_double\", DoubleType)\n      .add(\"as_string\", StringType)\n      .add(\"as_string_lit_null\", StringType)\n      .add(\"as_date\", DateType)\n      .add(\"as_timestamp\", TimestampType)\n      .add(\"as_big_decimal\", DecimalType(1, 0))\n      // data fields\n      .add(\"as_list_of_records\", ArrayType(new StructType().add(\"val\", IntegerType)))\n      .add(\"as_nested_struct\", new StructType()\n        .add(\"aa\", StringType)\n        .add(\"ab\", StringType)\n        .add(\"ac\", new StructType()\n          .add(\"aca\", IntegerType)\n          .add(\"acb\", LongType)\n        )\n      )\n      .add(\"value\", StringType)\n\n    val data = (0 until 2).map(createRow) :+ createRowWithNullPartitionValues()\n\n    val df = spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n    df.write\n      .format(\"delta\")\n      .partitionBy(\"as_int\", \"as_long\", \"as_byte\", \"as_short\", \"as_boolean\", \"as_float\",\n        \"as_double\", \"as_string\", \"as_string_lit_null\", \"as_date\", \"as_timestamp\", \"as_big_decimal\")\n      .save(tablePath)\n  }\n\n  Seq(\"name\", \"id\").foreach { columnMappingMode =>\n    generateGoldenTable(s\"table-with-columnmapping-mode-$columnMappingMode\") { tablePath =>\n      withSQLConf(\n        (\"spark.databricks.delta.properties.defaults.columnMapping.mode\", columnMappingMode)) {\n        generateCMIcebegCompatTableHelper(tablePath)\n      }\n    }\n  }\n\n  generateGoldenTable(\"table-with-icebegCompatV2Enabled\") { tablePath =>\n    withSQLConf(\n      (\"spark.databricks.delta.properties.defaults.columnMapping.mode\", \"id\"),\n      (\"spark.databricks.delta.properties.defaults.enableIcebergCompatV2\", \"true\")) {\n      generateCMIcebegCompatTableHelper(tablePath)\n    }\n  }\n\n  def generateCMIcebegCompatTableHelper(tablePath: String): Unit = {\n    val timeZone = java.util.TimeZone.getTimeZone(\"UTC\")\n    java.util.TimeZone.setDefault(timeZone)\n    import java.sql._\n\n    val decimalType = DecimalType(10, 2)\n\n    val allDataTypes = Seq(\n      ByteType,\n      ShortType,\n      IntegerType,\n      LongType,\n      FloatType,\n      DoubleType,\n      decimalType,\n      BooleanType,\n      StringType,\n      BinaryType,\n      DateType,\n      TimestampType\n    )\n\n    var fields = allDataTypes.map(dt => {\n      val name = if (dt.isInstanceOf[DecimalType]) {\n        \"decimal\"\n      } else {\n        dt.toString\n      }\n      StructField(name, dt)\n    })\n\n    fields = fields :+ StructField(\"nested_struct\", new StructType()\n      .add(\"aa\", StringType)\n      .add(\"ac\", new StructType()\n        .add(\"aca\", IntegerType)\n      )\n    )\n\n    fields = fields :+ StructField(\"array_of_prims\", ArrayType(IntegerType))\n    fields = fields :+ StructField(\"array_of_arrays\", ArrayType(ArrayType(IntegerType)))\n    fields = fields :+ StructField(\"array_of_map_of_arrays\",\n      ArrayType(MapType(IntegerType, ArrayType(IntegerType))))\n    fields = fields :+ StructField(\n      \"array_of_structs\",\n      ArrayType(new StructType().add(\"ab\", IntegerType)))\n    fields = fields :+ StructField(\n      \"struct_of_arrays_maps_of_structs\",\n      new StructType()\n        .add(\"aa\", ArrayType(IntegerType))\n        .add(\"ab\", MapType(ArrayType(IntegerType), new StructType().add(\"aca\", IntegerType)))\n    )\n\n    fields = fields :+ StructField(\n      \"map_of_prims\",\n      MapType(IntegerType, LongType)\n    )\n    fields = fields :+ StructField(\n      \"map_of_rows\",\n      MapType(IntegerType, new StructType().add(\"ab\", LongType))\n    )\n    fields = fields :+ StructField(\n      \"map_of_arrays\",\n      MapType(LongType, ArrayType(IntegerType))\n    )\n\n    fields = fields :+ StructField(\n      \"map_of_maps\",\n      MapType(LongType, MapType(IntegerType, IntegerType))\n    )\n\n    val schema = StructType(fields)\n\n    def createRow(i: Int): Row = {\n      Row(\n        i.toByte, // byte\n        i.toShort, // short\n        i, // integer\n        i.toLong, // long\n        i.toFloat, // float\n        i.toDouble, // double\n        new java.math.BigDecimal(i), // decimal\n        i % 2 == 0, // boolean\n        i.toString, // string\n        i.toString.getBytes, // binary\n        Date.valueOf(\"2021-11-18\"), // date\n        new Timestamp(i.toLong), // timestamp\n        Row(i.toString, Row(i)), // nested_struct\n        scala.Array(i, i + 1), // array_of_prims\n        scala.Array(scala.Array(i, i + 1), scala.Array(i + 2, i + 3)), // array_of_arrays\n        scala.Array(\n          Map(i -> scala.Array(2, 3), i + 1 -> scala.Array(4, 5))), // array_of_map_of_arrays\n        scala.Array(Row(i), Row(i)), // array_of_structs\n        Row( // struct_of_arrays_maps_of_structs\n          scala.Array(i, i + 1),\n          Map(scala.Array(i, i + 1) -> Row(i + 2))\n        ),\n        Map(i -> (i + 1).toLong, (i + 2) -> (i + 3).toLong), // map_of_prims\n        Map(i + 1 -> Row((i * 20).toLong)), // map_of_rows\n        {\n          val val1 = scala.Array(i, null, i + 1)\n          val val2 = scala.Array[Integer]()\n          Map(\n            i.longValue() -> val1,\n            (i + 1).longValue() -> val2\n          ) // map_of_arrays\n        },\n        Map( // map_of_maps\n          i.toLong -> Map(i -> i),\n          (i + 1).toLong -> Map(i + 2 -> i)\n        )\n      )\n    }\n\n    def createNullRow(): Row = {\n      Row(Seq.fill(schema.length)(null): _*)\n    }\n\n    val rows = Seq.range(0, 5).map(i => createRow(i)) ++ Seq(createNullRow())\n\n    val df = spark.createDataFrame(spark.sparkContext.parallelize(rows), schema)\n    df.repartition(2)\n      .write\n      .format(\"delta\")\n      .save(tablePath)\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - date types */\n  Seq(\"UTC\", \"Iceland\", \"PST\", \"America/Los_Angeles\", \"Etc/GMT+9\", \"Asia/Beirut\",\n    \"JST\").foreach { timeZoneId =>\n    generateGoldenTable(s\"data-reader-date-types-$timeZoneId\") { tablePath =>\n      val timeZone = TimeZone.getTimeZone(timeZoneId)\n      TimeZone.setDefault(timeZone)\n\n      val timestamp = Timestamp.valueOf(\"2020-01-01 08:09:10\")\n      val date = java.sql.Date.valueOf(\"2020-01-01\")\n\n      val data = Row(timestamp, date) :: Nil\n      val schema = new StructType()\n        .add(\"timestamp\", TimestampType)\n        .add(\"date\", DateType)\n\n      writeDataWithSchema(tablePath, data, schema)\n    }\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - array of primitives */\n  generateGoldenTable(\"data-reader-array-primitives\") { tablePath =>\n    def createRow(i: Int): Row = {\n      Row(Array(i), Array(i.longValue), Array(i.toByte), Array(i.shortValue),\n        Array(i % 2 == 0), Array(i.floatValue), Array(i.doubleValue), Array(i.toString),\n        Array(Array(i.toByte, i.toByte)),\n        Array(new JBigDecimal(i))\n      )\n    }\n\n    val schema = new StructType()\n      .add(\"as_array_int\", ArrayType(IntegerType))\n      .add(\"as_array_long\", ArrayType(LongType))\n      .add(\"as_array_byte\", ArrayType(ByteType))\n      .add(\"as_array_short\", ArrayType(ShortType))\n      .add(\"as_array_boolean\", ArrayType(BooleanType))\n      .add(\"as_array_float\", ArrayType(FloatType))\n      .add(\"as_array_double\", ArrayType(DoubleType))\n      .add(\"as_array_string\", ArrayType(StringType))\n      .add(\"as_array_binary\", ArrayType(BinaryType))\n      .add(\"as_array_big_decimal\", ArrayType(DecimalType(1, 0)))\n\n    val data = (0 until 10).map(createRow)\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - array of complex objects */\n  generateGoldenTable(\"data-reader-array-complex-objects\") { tablePath =>\n    def createRow(i: Int): Row = {\n      Row(\n        i,\n        Array(Array(Array(i, i, i), Array(i, i, i)), Array(Array(i, i, i), Array(i, i, i))),\n        Array(\n          Array(Array(Array(i, i, i), Array(i, i, i)), Array(Array(i, i, i), Array(i, i, i))),\n          Array(Array(Array(i, i, i), Array(i, i, i)), Array(Array(i, i, i), Array(i, i, i)))\n        ),\n        Array(\n          Map[String, Long](i.toString -> i.toLong),\n          Map[String, Long](i.toString -> i.toLong)\n        ),\n        Array(Row(i), Row(i), Row(i))\n      )\n    }\n\n    val schema = new StructType()\n      .add(\"i\", IntegerType)\n      .add(\"3d_int_list\", ArrayType(ArrayType(ArrayType(IntegerType))))\n      .add(\"4d_int_list\", ArrayType(ArrayType(ArrayType(ArrayType(IntegerType)))))\n      .add(\"list_of_maps\", ArrayType(MapType(StringType, LongType)))\n      .add(\"list_of_records\", ArrayType(new StructType().add(\"val\", IntegerType)))\n\n    val data = (0 until 10).map(createRow)\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - map */\n  generateGoldenTable(\"data-reader-map\") { tablePath =>\n    def createRow(i: Int): Row = {\n      Row(\n        i,\n        Map(i -> i),\n        Map(i.toLong -> i.toByte),\n        Map(i.toShort -> (i % 2 == 0)),\n        Map(i.toFloat -> i.toDouble),\n        Map(i.toString -> new JBigDecimal(i)),\n        Map(i -> Array(Row(i), Row(i), Row(i)))\n      )\n    }\n\n    val schema = new StructType()\n      .add(\"i\", IntegerType)\n      .add(\"a\", MapType(IntegerType, IntegerType))\n      .add(\"b\", MapType(LongType, ByteType))\n      .add(\"c\", MapType(ShortType, BooleanType))\n      .add(\"d\", MapType(FloatType, DoubleType))\n      .add(\"e\", MapType(StringType, DecimalType(1, 0)))\n      .add(\"f\", MapType(IntegerType, ArrayType(new StructType().add(\"val\", IntegerType))))\n\n    val data = (0 until 10).map(createRow)\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - nested struct */\n  generateGoldenTable(\"data-reader-nested-struct\") { tablePath =>\n    def createRow(i: Int): Row = Row(Row(i.toString, i.toString, Row(i, i.toLong)), i)\n\n    val schema = new StructType()\n      .add(\"a\", new StructType()\n        .add(\"aa\", StringType)\n        .add(\"ab\", StringType)\n        .add(\"ac\", new StructType()\n          .add(\"aca\", IntegerType)\n          .add(\"acb\", LongType)\n        )\n      )\n      .add(\"b\", IntegerType)\n\n    val data = (0 until 10).map(createRow)\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > read - nullable field, invalid schema column key */\n  generateGoldenTable(\"data-reader-nullable-field-invalid-schema-key\") { tablePath =>\n    val data = Row(Seq(null, null, null)) :: Nil\n    val schema = new StructType()\n      .add(\"array_can_contain_null\", ArrayType(StringType, containsNull = true))\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > test escaped char sequences in path */\n  generateGoldenTable(\"data-reader-escaped-chars\") { tablePath =>\n    val data = Seq(\"foo1\" -> \"bar+%21\", \"foo2\" -> \"bar+%22\", \"foo3\" -> \"bar+%23\")\n\n    data.foreach { row =>\n      Seq(row).toDF().write.format(\"delta\").mode(\"append\").partitionBy(\"_2\").save(tablePath)\n    }\n  }\n\n  /** TEST: DeltaDataReaderSuite > #124: decimal decode bug */\n  generateGoldenTable(\"124-decimal-decode-bug\") { tablePath =>\n    val data = Seq(Row(new JBigDecimal(1000000)))\n    val schema = new StructType().add(\"large_decimal\", DecimalType(10, 0))\n    writeDataWithSchema(tablePath, data, schema)\n  }\n\n  /** TEST: DeltaDataReaderSuite > #125: iterator bug */\n  generateGoldenTable(\"125-iterator-bug\") { tablePath =>\n    val datas = Seq(\n      Seq(),\n      Seq(1),\n      Seq(2), Seq(),\n      Seq(3), Seq(), Seq(),\n      Seq(4), Seq(), Seq(), Seq(),\n      Seq(5)\n    )\n    datas.foreach { data =>\n      data.toDF(\"col1\").write.format(\"delta\").mode(\"append\").save(tablePath)\n    }\n  }\n\n  generateGoldenTable(\"deltatbl-not-allow-write\", createHiveGoldenTableFile) { tablePath =>\n    val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n    data.toDF(\"a\", \"b\").write.format(\"delta\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-schema-match\", createHiveGoldenTableFile) { tablePath =>\n    val data = (0 until 10).map(x => (x, s\"foo${x % 2}\", s\"test${x % 3}\"))\n    data.toDF(\"a\", \"b\", \"c\").write.format(\"delta\").partitionBy(\"b\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-non-partitioned\", createHiveGoldenTableFile) { tablePath =>\n    val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n    data.toDF(\"c1\", \"c2\").write.format(\"delta\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-partitioned\", createHiveGoldenTableFile) { tablePath =>\n    val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n    data.toDF(\"c1\", \"c2\").write.format(\"delta\").partitionBy(\"c2\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-partition-prune\", createHiveGoldenTableFile) { tablePath =>\n    val data = Seq(\n      (\"hz\", \"20180520\", \"Jim\", 3),\n      (\"hz\", \"20180718\", \"Jone\", 7),\n      (\"bj\", \"20180520\", \"Trump\", 1),\n      (\"sh\", \"20180512\", \"Jay\", 4),\n      (\"sz\", \"20181212\", \"Linda\", 8)\n    )\n    data.toDF(\"city\", \"date\", \"name\", \"cnt\")\n    .write.format(\"delta\").partitionBy(\"date\", \"city\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-touch-files-needed-for-partitioned\", createHiveGoldenTableFile) {\n    tablePath =>\n      val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n      data.toDF(\"c1\", \"c2\").write.format(\"delta\").partitionBy(\"c2\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-special-chars-in-partition-column\", createHiveGoldenTableFile) {\n    tablePath =>\n      val data = (0 until 10).map(x => (x, s\"+ =%${x % 2}\"))\n      data.toDF(\"c1\", \"c2\").write.format(\"delta\").partitionBy(\"c2\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-map-types-correctly\", createHiveGoldenTableFile) { tablePath =>\n    val data = Seq(\n      TestClass(\n        97.toByte,\n        Array(98.toByte, 99.toByte),\n        true,\n        4,\n        5L,\n        \"foo\",\n        6.0f,\n        7.0,\n        8.toShort,\n        new java.sql.Date(60000000L),\n        new java.sql.Timestamp(60000000L),\n        new java.math.BigDecimal(12345.6789),\n        Array(\"foo\", \"bar\"),\n        Map(\"foo\" -> 123L),\n        TestStruct(\"foo\", 456L)\n      )\n    )\n    data.toDF.write.format(\"delta\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-column-names-case-insensitive\", createHiveGoldenTableFile) {\n    tablePath =>\n      val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n      data.toDF(\"FooBar\", \"BarFoo\").write.format(\"delta\").partitionBy(\"BarFoo\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-deleted-path\", createHiveGoldenTableFile) {\n    tablePath =>\n      val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n      data.toDF(\"c1\", \"c2\").write.format(\"delta\").save(tablePath)\n  }\n\n  generateGoldenTable(\"deltatbl-incorrect-format-config\", createHiveGoldenTableFile) { tablePath =>\n    val data = (0 until 10).map(x => (x, s\"foo${x % 2}\"))\n    data.toDF(\"a\", \"b\").write.format(\"delta\").save(tablePath)\n  }\n\n  generateGoldenTable(\"dv-partitioned-with-checkpoint\") { tablePath =>\n    withSQLConf((\"spark.databricks.delta.properties.defaults.enableDeletionVectors\", \"true\")) {\n      val data = (0 until 50).map(x => (x%10, x, s\"foo${x % 5}\"))\n      data.toDF(\"part\", \"col1\", \"col2\").write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(tablePath)\n      (0 until 15).foreach { n =>\n        spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE col1 = ${n*2}\")\n      }\n    }\n  }\n\n  generateGoldenTable(\"dv-with-columnmapping\") { tablePath =>\n    withSQLConf(\n      (\"spark.databricks.delta.properties.defaults.columnMapping.mode\", \"name\"),\n      (\"spark.databricks.delta.properties.defaults.enableDeletionVectors\", \"true\")) {\n      val data = (0 until 50).map(x => (x%10, x, s\"foo${x % 5}\"))\n      data.toDF(\"part\", \"col1\", \"col2\").write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(tablePath)\n      (0 until 15).foreach { n =>\n        spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE col1 = ${n*2}\")\n      }\n    }\n  }\n\n  def writeBasicTimestampTable(path: String, timeZone: TimeZone): Unit = {\n    TimeZone.setDefault(timeZone)\n    // Create a partition value of both {year}-{month}-{day} {hour}:{minute}:{second} format and\n    // {year}-{month}-{day} {hour}:{minute}:{second}.{microsecond}\n    val data = Row(0, Timestamp.valueOf(\"2020-01-01 08:09:10.001\"), Timestamp.valueOf(\"2020-02-01 08:09:10\")) ::\n      Row(1, Timestamp.valueOf(\"2021-10-01 08:09:20\"), Timestamp.valueOf(\"1999-01-01 09:00:00\")) ::\n      Row(2, Timestamp.valueOf(\"2021-10-01 08:09:20\"), Timestamp.valueOf(\"2000-01-01 09:00:00\")) ::\n      Row(3, Timestamp.valueOf(\"1969-01-01 00:00:00\"), Timestamp.valueOf(\"1969-01-01 00:00:00\")) ::\n      Row(4, null, null) :: Nil\n\n    val schema = new StructType()\n      .add(\"id\", IntegerType)\n      .add(\"part\", TimestampType)\n      .add(\"time\", TimestampType)\n\n    spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n      .write\n      .format(\"delta\")\n      .partitionBy(\"part\")\n      .save(path)\n  }\n\n  for (parquetTimestampType <- SQLConf.ParquetOutputTimestampType.values) {\n    generateGoldenTable(s\"kernel-timestamp-${parquetTimestampType.toString}\") { tablePath =>\n      withSQLConf((\"spark.sql.parquet.outputTimestampType\", parquetTimestampType.toString)) {\n        writeBasicTimestampTable(tablePath, TimeZone.getTimeZone(\"UTC\"))\n      }\n    }\n  }\n\n  generateGoldenTable(\"kernel-timestamp-PST\") { tablePath =>\n    writeBasicTimestampTable(tablePath, TimeZone.getTimeZone(\"PST\"))\n  }\n\n  generateGoldenTable(\"parquet-all-types-legacy-format\") { tablePath =>\n    withSQLConf((\"spark.sql.parquet.writeLegacyFormat\", \"true\")) {\n      generateAllTypesTable(tablePath)\n    }\n  }\n\n  generateGoldenTable(\"parquet-all-types\") { tablePath =>\n    // generating using the standard parquet format\n    generateAllTypesTable(tablePath)\n  }\n\n  def generateAllTypesTable(tablePath: String): Unit = {\n    val timeZone = java.util.TimeZone.getTimeZone(\"UTC\")\n    java.util.TimeZone.setDefault(timeZone)\n    import java.sql._\n\n    val decimalType = DecimalType(10, 2)\n\n    val allDataTypes = Seq(\n      ByteType,\n      ShortType,\n      IntegerType,\n      LongType,\n      FloatType,\n      DoubleType,\n      decimalType,\n      BooleanType,\n      StringType,\n      BinaryType,\n      DateType,\n      TimestampType,\n      TimestampNTZType\n    )\n\n    var fields = allDataTypes.map(dt => {\n      val name = if (dt.isInstanceOf[DecimalType]) {\n        \"decimal\"\n      } else {\n        dt.toString\n      }\n      StructField(name, dt)\n    })\n\n    fields = fields :+ StructField(\"nested_struct\", new StructType()\n      .add(\"aa\", StringType)\n      .add(\"ac\", new StructType()\n        .add(\"aca\", IntegerType)\n      )\n    )\n\n    fields = fields :+ StructField(\"array_of_prims\", ArrayType(IntegerType))\n    fields = fields :+ StructField(\"array_of_arrays\", ArrayType(ArrayType(IntegerType)))\n    fields = fields :+ StructField(\n      \"array_of_structs\",\n      ArrayType(new StructType().add(\"ab\", LongType)))\n\n    fields = fields :+ StructField(\n      \"map_of_prims\",\n      MapType(IntegerType, LongType)\n    )\n    fields = fields :+ StructField(\n      \"map_of_rows\",\n      MapType(IntegerType, new StructType().add(\"ab\", LongType))\n    )\n    fields = fields :+ StructField(\n      \"map_of_arrays\",\n      MapType(LongType, ArrayType(IntegerType))\n    )\n\n    val schema = StructType(fields)\n\n    def createRow(i: Int): Row = {\n      Row(\n        if (i % 72 != 0) i.byteValue() else null,\n        if (i % 56 != 0) i.shortValue() else null,\n        if (i % 23 != 0) i else null,\n        if (i % 25 != 0) (i + 1).longValue() else null,\n        if (i % 28 != 0) (i * 0.234).floatValue() else null,\n        if (i % 54 != 0) (i * 234234.23).doubleValue() else null,\n        if (i % 67 != 0) new java.math.BigDecimal(i * 123.52) else null,\n        if (i % 87 != 0) i % 2 == 0 else null,\n        if (i % 57 != 0) (i).toString else null,\n        if (i % 59 != 0) (i).toString.getBytes else null,\n        if (i % 61 != 0) new java.sql.Date(i * 20000000L) else null,\n        if (i % 62 != 0) new Timestamp(i * 23423523L) else null,\n        if (i % 69 != 0) LocalDateTime.ofEpochSecond(i * 234234L, 200012, UTC) else null,\n        // nested_struct\n        if (i % 63 != 0) {\n          if (i % 19 == 0) {\n            // write a struct with all fields null\n            Row(null, null)\n          } else {\n            Row(i.toString, if (i % 23 != 0) Row(i) else null)\n          }\n        } else null,\n        // array_of_prims\n        if (i % 25 != 0) {\n          if (i % 29 == 0) {\n            scala.Array()\n          } else {\n            scala.Array(i, null, i + 1)\n          }\n        } else null,\n        // array_of_arrays\n        if (i % 8 != 0) {\n          val singleElemArray = scala.Array(i)\n          val doubleElemArray = scala.Array(i + 10, i + 20)\n          val arrayWithNulls = scala.Array(null, i + 200)\n          val singleElemNullArray = scala.Array(null)\n          val emptyArray = scala.Array()\n          (i % 7) match {\n            case 0 => scala.Array(singleElemArray, singleElemArray, arrayWithNulls)\n            case 1 => scala.Array(singleElemArray, doubleElemArray, emptyArray)\n            case 2 => scala.Array(arrayWithNulls)\n            case 3 => scala.Array(singleElemNullArray)\n            case 4 => scala.Array(null)\n            case 5 => scala.Array(emptyArray)\n            case 6 => scala.Array()\n          }\n        } else null,\n        // array_of_structs\n        if (i % 10 != 0) {\n          scala.Array(Row(i.longValue()), null)\n        } else null,\n        // map_of_prims\n        if (i % 28 != 0) {\n          if (i % 30 == 0) {\n            Map()\n          } else {\n            Map(\n              i -> (if (i % 29 != 0) (i + 2).longValue() else null),\n              (if (i % 27 != 0) i + 2 else i + 3) -> (i + 9).longValue()\n            )\n          }\n        } else null,\n        // map_of_rows\n        if (i % 25 != 0) {\n          Map(i + 1 -> (if (i % 10 == 0) Row((i * 20).longValue()) else null))\n        } else null,\n        // map_of_arrays\n        if (i % 30 != 0) {\n          if (i % 24 == 0) {\n            Map()\n          } else {\n            val val1 = if (i % 4 == 0) scala.Array(i, null, i + 1) else scala.Array()\n            val val2 = if (i % 7 == 0) scala.Array[Integer]() else scala.Array[Integer](null)\n            Map(\n              i.longValue() -> val1,\n              (i + 1).longValue() -> val2\n            )\n          }\n        } else null\n      )\n    }\n\n    val rows = Seq.range(0, 200).map(i => createRow(i))\n\n    val df = spark.createDataFrame(spark.sparkContext.parallelize(rows), schema)\n    df.repartition(1)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(tablePath)\n  }\n\n  def writeBasicDecimalTable(tablePath: String): Unit = {\n    val data = Seq(\n      Seq(\"234\", \"1\", \"2\", \"3\"),\n      Seq(\"2342222.23454\", \"111.11\", \"22222.22222\", \"3333333333.3333333333\"),\n      Seq(\"0.00004\", \"0.001\", \"0.000002\", \"0.00000000003\"),\n      Seq(\"-2342342.23423\", \"-999.99\", \"-99999.99999\", \"-9999999999.9999999999\")\n    ).map(_.map(new JBigDecimal(_))).map(Row(_: _*))\n\n    val schema = new StructType()\n      .add(\"part\", new DecimalType(12, 5)) // serialized to a string\n      .add(\"col1\", new DecimalType(5, 2)) // INT32: 1 <= precision <= 9\n      .add(\"col2\", new DecimalType(10, 5)) // INT64: 10 <= precision <= 18\n      .add(\"col3\", new DecimalType(20, 10)) // FIXED_LEN_BYTE_ARRAY\n\n    spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n      .repartition(1)\n      .write\n      .format(\"delta\")\n      .partitionBy(\"part\")\n      .save(tablePath)\n  }\n\n  generateGoldenTable(\"basic-decimal-table\") { tablePath =>\n    writeBasicDecimalTable(tablePath)\n  }\n\n  generateGoldenTable(\"basic-decimal-table-legacy\") { tablePath =>\n    withSQLConf((\"spark.sql.parquet.writeLegacyFormat\", \"true\")) {\n      writeBasicDecimalTable(tablePath)\n    }\n  }\n\n  generateGoldenTable(\"decimal-various-scale-precision\") { tablePath =>\n    val fields = ArrayBuffer[StructField]()\n    Seq(0, 4, 7, 12, 15, 18, 25, 35, 38).foreach { precision =>\n      Seq.range(start = 0, end = precision, step = 6).foreach { scale =>\n        fields.append(\n          StructField(s\"decimal_${precision}_${scale}\", DecimalType(precision, scale)))\n      }\n    }\n\n    val schema = StructType(fields)\n\n    val random = new Random(27 /* seed */)\n    def generateRandomBigDecimal(precision: Int, scale: Int): JBigDecimal = {\n      // Generate a random BigInteger with the specified precision\n      val unscaledValue = new BigInteger(precision, random)\n\n      // Create a BigDecimal with the unscaled value and the specified scale\n      new JBigDecimal(unscaledValue, scale)\n    }\n\n    val rows = ArrayBuffer[Row]()\n    Seq.range(start = 0, end = 3).foreach { i =>\n      val rowValues = ArrayBuffer[BigDecimal]()\n      Seq(0, 4, 7, 12, 15, 18, 25, 35, 38).foreach { precision =>\n        Seq.range(start = 0, end = precision, step = 3).foreach { scale =>\n          i match {\n            case 0 =>\n              rowValues.append(null)\n            case 1 =>\n              // Generate a positive random BigDecimal with the specified precision and scale\n              rowValues.append(generateRandomBigDecimal(precision, scale))\n            case 2 =>\n              // Generate a negative random BigDecimal with the specified precision and scale\n              rowValues.append(generateRandomBigDecimal(precision, scale).negate())\n          }\n        }\n      }\n      rows.append(Row(rowValues: _*))\n    }\n\n    spark.createDataFrame(spark.sparkContext.parallelize(rows), schema)\n      .repartition(1)\n      .write\n      .format(\"delta\")\n      .save(tablePath)\n  }\n\n  for (parquetFormat <- Seq(\"v1\", \"v2\")) {\n    // PARQUET_1_0 doesn't support dictionary encoding for FIXED_LEN_BYTE_ARRAY (only PARQUET_2_0)\n    generateGoldenTable(s\"parquet-decimal-dictionaries-$parquetFormat\") { tablePath =>\n\n      def withHadoopConf(key: String, value: String)(f: => Unit): Unit = {\n        try {\n          spark.sparkContext.hadoopConfiguration.set(key, value)\n          f\n        } finally {\n          spark.sparkContext.hadoopConfiguration.unset(key)\n        }\n      }\n\n      withHadoopConf(\"parquet.writer.version\", parquetFormat) {\n        val data = (0 until 1000000).map { i =>\n          Row(i, JBigDecimal.valueOf(i % 5), JBigDecimal.valueOf(i % 6), JBigDecimal.valueOf(i % 2))\n        }\n\n        val schema = new StructType()\n          .add(\"id\", IntegerType)\n          .add(\"col1\", new DecimalType(9, 0)) // INT32: 1 <= precision <= 9\n          .add(\"col2\", new DecimalType(12, 0)) // INT64: 10 <= precision <= 18\n          .add(\"col3\", new DecimalType(25, 0)) // FIXED_LEN_BYTE_ARRAY\n\n        spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n          .repartition(1)\n          .write\n          .format(\"delta\")\n          .save(tablePath)\n      }\n    }\n  }\n\n  generateGoldenTable(\"parquet-decimal-type\") { tablePath =>\n\n    def expand(n: JBigDecimal): JBigDecimal = {\n      n.scaleByPowerOfTen(5).add(n)\n    }\n\n    val data = (0 until 99998).map { i =>\n      if (i % 85 == 0) {\n        val n = JBigDecimal.valueOf(i)\n        Row(i, n.movePointLeft(1), n, n)\n      } else {\n        val negation = if (i % 33 == 0) {\n          -1\n        } else {\n          1\n        }\n        val n = JBigDecimal.valueOf(i*negation)\n\n        Row(\n          i,\n          n.movePointLeft(1),\n          expand(n).movePointLeft(5),\n          expand(expand(expand(n))).movePointLeft(5)\n        )\n      }\n    }\n\n    val schema = new StructType()\n      .add(\"id\", IntegerType)\n      .add(\"col1\", new DecimalType(5, 1)) // INT32: 1 <= precision <= 9\n      .add(\"col2\", new DecimalType(10, 5)) // INT64: 10 <= precision <= 18\n      .add(\"col3\", new DecimalType(20, 5)) // FIXED_LEN_BYTE_ARRAY\n\n    spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n      .repartition(1)\n      .write\n      .format(\"delta\")\n      .save(tablePath)\n  }\n\n  /* START: TIMESTAMP_NTZ golden tables */\n  def generateTimestampNtzTable(tablePath: String): Unit = {\n    spark.sql(\n      s\"\"\"\n         | CREATE TABLE delta.`$tablePath`(id INTEGER, tsNtz TIMESTAMP_NTZ, tsNtzPartition TIMESTAMP_NTZ)\n         | USING DELTA\n         | PARTITIONED BY(tsNtzPartition)\n    \"\"\".stripMargin)\n\n  spark.sql(\n      s\"\"\"\n         | INSERT INTO delta.`$tablePath` VALUES\n         |   (0, '2021-11-18 02:30:00.123456','2021-11-18 02:30:00.123456'),\n         |   (1, '2013-07-05 17:01:00.123456','2021-11-18 02:30:00.123456'),\n         |   (2, NULL,'2021-11-18 02:30:00.123456'),\n         |   (3, '2021-11-18 02:30:00.123456','2013-07-05 17:01:00.123456'),\n         |   (4, '2013-07-05 17:01:00.123456','2013-07-05 17:01:00.123456'),\n         |   (5, NULL,'2013-07-05 17:01:00.123456'),\n         |   (6, '2021-11-18 02:30:00.123456', NULL),\n         |   (7, '2013-07-05 17:01:00.123456', NULL),\n         |   (8, NULL, NULL)\n         |\"\"\".stripMargin)\n  }\n\n  generateGoldenTable(\"data-reader-timestamp_ntz\") { tablePath =>\n    generateTimestampNtzTable(tablePath)\n  }\n\n  Seq(\"id\", \"name\").foreach {\n    columnMappingMode => {\n      generateGoldenTable(s\"data-reader-timestamp_ntz-$columnMappingMode-mode\") { tablePath =>\n        withSQLConf(\n          (\"spark.databricks.delta.properties.defaults.columnMapping.mode\", columnMappingMode)) {\n          generateTimestampNtzTable(tablePath)\n        }\n      }\n    }\n  }\n  /* END: TIMESTAMP_NTZ golden tables */\n\n  generateGoldenTable(\"basic-with-inserts-deletes-checkpoint\") { tablePath =>\n    // scalastyle:off line.size.limit\n    spark.range(0, 10).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(10, 20).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(20, 30).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(30, 40).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(40, 50).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    sql(s\"DELETE FROM delta.`$tablePath` WHERE id >= 5 AND id <= 9\")\n    sql(s\"DELETE FROM delta.`$tablePath` WHERE id >= 15 AND id <= 19\")\n    sql(s\"DELETE FROM delta.`$tablePath` WHERE id >= 25 AND id <= 29\")\n    sql(s\"DELETE FROM delta.`$tablePath` WHERE id >= 35 AND id <= 39\")\n    sql(s\"DELETE FROM delta.`$tablePath` WHERE id >= 45 AND id <= 49\")\n    spark.range(50, 60).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(60, 70).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(70, 80).repartition(1).write.format(\"delta\").mode(\"append\").save(tablePath)\n    sql(s\"DELETE FROM delta.`$tablePath` WHERE id >= 66\")\n    // scalastyle:on line.size.limit\n  }\n\n  generateGoldenTable(\"multi-part-checkpoint\") { tablePath =>\n    withSQLConf(\n      (\"spark.databricks.delta.checkpoint.partSize\", \"5\"),\n      (\"spark.databricks.delta.properties.defaults.checkpointInterval\", \"1\")\n    ) {\n      spark.range(1).repartition(1).write.format(\"delta\").save(tablePath)\n      spark.range(30).repartition(9).write.format(\"delta\").mode(\"append\").save(tablePath)\n    }\n  }\n\n  Seq(\"parquet\", \"json\").foreach { ckptFormat =>\n    val tbl = \"tbl\"\n    generateGoldenTable(s\"v2-checkpoint-$ckptFormat\") { tablePath =>\n      withTable(tbl) {\n        withSQLConf(\n          (DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key, \"2\"),\n          (\"spark.databricks.delta.properties.defaults.checkpointInterval\", \"2\"),\n          (DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key, ckptFormat)) {\n          spark.conf.set(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key, ckptFormat)\n          sql(s\"CREATE TABLE $tbl (id LONG) USING delta LOCATION '$tablePath'\")\n          sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES('delta.checkpointPolicy' = 'v2')\")\n          spark.range(10).repartition(4)\n            .write.format(\"delta\").mode(\"append\").saveAsTable(tbl)\n        }\n      }\n    }\n  }\n\n  generateGoldenTable(\"no-delta-log-folder\") { tablePath =>\n    spark.range(20).write.format(\"parquet\").save(tablePath)\n  }\n\n  generateGoldenTable(\"log-replay-latest-metadata-protocol\") { tablePath =>\n    spark.range(20).toDF(\"col1\")\n      .write.format(\"delta\").save(tablePath)\n    // update the table schema\n    spark.range(20).toDF(\"col1\").withColumn(\"col2\", 'col1 % 2)\n      .write.format(\"delta\").mode(\"append\").option(\"mergeSchema\", \"true\").save(tablePath)\n    // update the protocol version\n    DeltaTable.forPath(spark, tablePath).upgradeTableProtocol(3, 7)\n  }\n\n  generateGoldenTable(\"only-checkpoint-files\") { tablePath =>\n    withSQLConf((\"spark.databricks.delta.properties.defaults.checkpointInterval\", \"1\")) {\n      spark.range(10).repartition(10).write.format(\"delta\").save(tablePath)\n      spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE id < 5\")\n      spark.range(20).write.format(\"delta\").mode(\"append\").save(tablePath)\n    }\n  }\n\n  generateGoldenTable(\"log-replay-special-characters-a\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    new File(log.logPath.toUri).mkdirs()\n\n    val add = AddFile(new Path(\"special p@#h\").toUri.toString, Map.empty, 100L,\n      10L, dataChange = true)\n    val remove = add.remove\n\n    log.startTransaction().commitManually(add)\n    log.startTransaction().commitManually(remove)\n  }\n\n  generateGoldenTable(\"log-replay-special-characters-b\") { tablePath =>\n    val log = DeltaLog.forTable(spark, new Path(tablePath))\n    new File(log.logPath.toUri).mkdirs()\n\n    val add = AddFile(new Path(\"special p@#h\").toUri.toString, Map.empty, 100L,\n      10L, dataChange = true)\n\n    log.startTransaction().commitManually(add)\n  }\n\n  generateGoldenTable(\"log-replay-dv-key-cases\") { tablePath =>\n    withSQLConf((\"spark.databricks.delta.properties.defaults.enableDeletionVectors\", \"true\")) {\n      spark.range(50).repartition(1).write.format(\"delta\").save(tablePath)\n      (0 until 3).foreach { n =>\n        spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE id = ${n*7}\")\n      }\n    }\n  }\n\n  generateGoldenTable(\"basic-with-vacuum-protocol-check-feature\") { tablePath =>\n    val data = (0 until 100).map(x => (x, s\"val=$x\"))\n    data.toDF(\"id\", \"str\").write.format(\"delta\").save(tablePath)\n    sql(s\"\"\"\n         |ALTER TABLE delta.`$tablePath`\n         |SET TBLPROPERTIES('delta.feature.vacuumProtocolCheck' = 'supported')\n         |\"\"\".stripMargin)\n  }\n\n  generateGoldenTable(\"basic-with-inserts-updates\") { tablePath =>\n    val data = (0 until 100).map(x => (x, s\"val=$x\"))\n    data.toDF(\"id\", \"str\").write.format(\"delta\").save(tablePath)\n    sql(s\"UPDATE delta.`$tablePath` SET str = 'N/A' WHERE id < 50\")\n  }\n\n  generateGoldenTable(\"basic-with-inserts-merge\") { tablePath =>\n    val data = (0 until 100).map(x => (x, s\"val=$x\"))\n    data.toDF(\"id\", \"str\").write.format(\"delta\").save(tablePath)\n    spark.range(50, 150).createTempView(\"source\")\n    sql(\n      s\"\"\"\n         |MERGE INTO delta.`$tablePath` t\n         |USING source\n         |ON source.id = t.id\n         |WHEN MATCHED\n         |  THEN UPDATE SET str = 'N/A'\n         |WHEN NOT MATCHED\n         |  THEN INSERT (id, str) VALUES (source.id, 'EXT')\n         |WHEN NOT MATCHED BY SOURCE AND t.id < 10\n         |  THEN DELETE\n         |\"\"\".stripMargin)\n  }\n\n  generateGoldenTable(\"basic-with-inserts-overwrite-restore\") { tablePath =>\n    spark.range(100).write.format(\"delta\").save(tablePath)\n    spark.range(100, 200).write.format(\"delta\").mode(\"append\").save(tablePath)\n    spark.range(500, 1000).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n    sql(s\"RESTORE TABLE delta.`$tablePath` TO VERSION AS OF 1\")\n  }\n\n  /* ----- Data skipping tables for Kernel ------ */\n\n  def writeBasicStatsAllTypesTable(tablePath: String): Unit = {\n    val schema = new StructType()\n      .add(\"as_int\", IntegerType)\n      .add(\"as_long\", LongType)\n      .add(\"as_byte\", ByteType)\n      .add(\"as_short\", ShortType)\n      .add(\"as_float\", FloatType)\n      .add(\"as_double\", DoubleType)\n      .add(\"as_string\", StringType)\n      .add(\"as_date\", DateType)\n      .add(\"as_timestamp\", TimestampType)\n      .add(\"as_big_decimal\", DecimalType(1, 0))\n\n    writeDataWithSchema(\n      tablePath,\n      Row(0, 0.longValue, 0.byteValue, 0.shortValue, 0.floatValue, 0.doubleValue, \"0\",\n        java.sql.Date.valueOf(\"2000-01-01\"), Timestamp.valueOf(\"2000-01-01 00:00:00\"),\n        new JBigDecimal(0)) :: Nil,\n      schema\n    )\n  }\n  generateGoldenTable(\"data-skipping-basic-stats-all-types\") { tablePath =>\n    writeBasicStatsAllTypesTable(tablePath)\n  }\n  Seq(\"name\", \"id\").foreach { columnMappingMode =>\n    generateGoldenTable(s\"data-skipping-basic-stats-all-types-columnmapping-$columnMappingMode\") {\n      tablePath =>\n        withSQLConf(\n          (\"spark.databricks.delta.properties.defaults.columnMapping.mode\", columnMappingMode)) {\n          writeBasicStatsAllTypesTable(tablePath)\n        }\n    }\n  }\n  generateGoldenTable(\"data-skipping-basic-stats-all-types-checkpoint\") { tablePath =>\n    withSQLConf(\n      (\"spark.databricks.delta.properties.defaults.checkpointInterval\", \"1\")\n    ) {\n      writeBasicStatsAllTypesTable(tablePath)\n    }\n  }\n\n  generateGoldenTable(\"data-skipping-change-stats-collected-across-versions\") { tablePath =>\n    val schema = new StructType()\n      .add(\"col1\", IntegerType)\n      .add(\"col2\", IntegerType)\n    // write stats for all columns\n    writeDataWithSchema(\n      tablePath,\n      Row(0, 0) :: Nil,\n      schema\n    )\n    // write stats for just 1 column\n    sql(\n      s\"\"\"\n        |ALTER TABLE delta.`$tablePath`\n        |SET TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 1)\n        |\"\"\".stripMargin)\n    writeDataWithSchema(\n      tablePath,\n      Row(0, 0) :: Nil,\n      schema)\n    // write stats for no columns\n    sql(\n      s\"\"\"\n         |ALTER TABLE delta.`$tablePath`\n         |SET TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 0)\n         |\"\"\".stripMargin)\n    writeDataWithSchema(\n      tablePath,\n      Row(0, 0) :: Nil,\n      schema)\n  }\n\n  generateGoldenTable(\"data-skipping-partition-and-data-column\") { tablePath =>\n    val schema = new StructType()\n      .add(\"part\", IntegerType)\n      .add(\"id\", IntegerType)\n    writeDataWithSchema(\n      tablePath,\n      Row(1, 0) :: Nil,\n      schema\n    )\n    writeDataWithSchema(\n      tablePath,\n      Row(1, 1) :: Nil,\n      schema)\n    writeDataWithSchema(\n      tablePath,\n      Row(0, 1) :: Nil,\n      schema)\n    writeDataWithSchema(\n      tablePath,\n      Row(0, 0) :: Nil,\n      schema)\n  }\n\n  generateGoldenTable(\"commit-info-containing-arbitrary-operationParams-types\") { tablePath =>\n    spark.sql(\n      f\"\"\"\n         |CREATE TABLE delta.`$tablePath`\n         |USING DELTA\n         |PARTITIONED BY (month)\n         |TBLPROPERTIES (delta.enableChangeDataFeed = true)\n         |AS\n         |SELECT 1 AS id, 1 AS month\"\"\".stripMargin)\n\n    // Add some data\n    spark.sql(\"INSERT INTO delta.`%s` VALUES (2, 2)\".format(tablePath))\n\n    // Run optimize that generates a commitInfo with arbitrary value types\n    // operationParameters\n    spark.sql(\"OPTIMIZE delta.`%s` ZORDER BY id\".format(tablePath))\n  }\n}\n\ncase class TestStruct(f1: String, f2: Long)\n\n/** A special test class that covers all Spark types we support in the Hive connector. */\ncase class TestClass(\n  c1: Byte,\n  c2: Array[Byte],\n  c3: Boolean,\n  c4: Int,\n  c5: Long,\n  c6: String,\n  c7: Float,\n  c8: Double,\n  c9: Short,\n  c10: java.sql.Date,\n  c11: java.sql.Timestamp,\n  c12: BigDecimal,\n  c13: Array[String],\n  c14: Map[String, Long],\n  c15: TestStruct\n)\n\ncase class OneItem[T](t: T)\n"
  },
  {
    "path": "connectors/licenses/LICENSE-apache-spark.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "connectors/licenses/LICENSE-parquet4s.txt",
    "content": "MIT License\n\nCopyright (c) 2018 Marcin Jakubowski\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "contribs/src/main/scala/io/delta/storage/IBMCOSLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage\n\nimport com.google.common.base.Throwables\nimport java.io.IOException\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.nio.file.FileAlreadyExistsException\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.annotation.Unstable\n\n/**\n * :: Unstable ::\n *\n * LogStore implementation for IBM Cloud Object Storage.\n *\n * We assume the following from COS's [[FileSystem]] implementations:\n * - Write on COS is all-or-nothing, whether overwrite or not.\n * - Write is atomic.\n *   Note: Write is atomic when using the Stocator v1.1.1+ - Storage Connector for Apache Spark\n *   (https://github.com/CODAIT/stocator) by setting the configuration `fs.cos.atomic.write` to true\n *   (for more info see the documentation for Stocator)\n * - List-after-write is consistent.\n *\n * @note This class is not meant for direct access but for configuration based on storage system.\n *       See https://docs.delta.io/latest/delta-storage.html for details.\n */\n@Unstable\nclass IBMCOSLogStore(sparkConf: SparkConf, initHadoopConf: Configuration)\n  extends org.apache.spark.sql.delta.storage.HadoopFileSystemLogStore(sparkConf, initHadoopConf) {\n  val preconditionFailedExceptionMessage =\n    \"At least one of the preconditions you specified did not hold\"\n\n  assert(initHadoopConf.getBoolean(\"fs.cos.atomic.write\", false) == true,\n    \"'fs.cos.atomic.write' must be set to true to use IBMCOSLogStore \" +\n      \"in order to enable atomic write\")\n\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    write(path, actions, overwrite, getHadoopConfiguration)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    val fs = path.getFileSystem(hadoopConf)\n\n    val exists = fs.exists(path)\n    if (exists && overwrite == false) {\n      throw new FileAlreadyExistsException(path.toString)\n    } else {\n      // write is atomic when overwrite == false\n      val stream = fs.create(path, overwrite)\n      try {\n        actions.map(_ + \"\\n\").map(_.getBytes(UTF_8)).foreach(stream.write)\n        stream.close()\n      } catch {\n        case e: IOException if isPreconditionFailure(e) =>\n          if (fs.exists(path)) {\n            throw new FileAlreadyExistsException(path.toString)\n          } else {\n            throw new IllegalStateException(s\"Failed due to concurrent write\", e)\n          }\n      }\n    }\n  }\n\n  private def isPreconditionFailure(x: Throwable): Boolean = {\n    Throwables.getCausalChain(x)\n      .stream()\n      .filter(p => p != null)\n      .filter(p => p.getMessage != null)\n      .filter(p => p.getMessage.contains(preconditionFailedExceptionMessage))\n      .findFirst\n      .isPresent;\n  }\n\n  override def invalidateCache(): Unit = {}\n\n  override def isPartialWriteVisible(path: Path): Boolean = false\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = false\n}\n"
  },
  {
    "path": "contribs/src/main/scala/io/delta/storage/OracleCloudLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.annotation.Unstable\n\n/**\n * :: Unstable ::\n *\n * LogStore implementation for OCI (Oracle Cloud Infrastructure).\n *\n * We assume the following from OCI (Oracle Cloud Infrastructure)'s BmcFilesystem implementations:\n * - Rename without overwrite is atomic.\n * - List-after-write is consistent.\n *\n * Regarding file creation, this implementation:\n * - Uses atomic rename when overwrite is false; if the destination file exists or the rename\n *   fails, throws an exception.\n * - Uses create-with-overwrite when overwrite is true. This does not make the file atomically\n *   visible and therefore the caller must handle partial files.\n *\n * @note This class is not meant for direct access but for configuration based on storage system.\n *       See https://docs.delta.io/latest/delta-storage.html for details.\n */\n@Unstable\nclass OracleCloudLogStore(sparkConf: SparkConf, initHadoopConf: Configuration)\n  extends org.apache.spark.sql.delta.storage.HadoopFileSystemLogStore(sparkConf, initHadoopConf) {\n\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    write(path, actions, overwrite, getHadoopConfiguration)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    writeWithRename(path, actions, overwrite, hadoopConf)\n  }\n\n  override def invalidateCache(): Unit = {}\n\n  override def isPartialWriteVisible(path: Path): Boolean = true\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = true\n}\n"
  },
  {
    "path": "contribs/src/test/scala/io/delta/storage/IBMCOSLogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage\n\nimport org.apache.spark.sql.delta.{FakeFileSystem, LogStoreSuiteBase}\n\nclass IBMCOSLogStoreSuite extends LogStoreSuiteBase {\n\n  protected override def sparkConf = {\n    super.sparkConf.set(logStoreClassConfKey, logStoreClassName)\n      .set(\"spark.hadoop.fs.cos.atomic.write\", \"true\")\n  }\n\n  override val logStoreClassName: String = classOf[IBMCOSLogStore].getName\n\n  testHadoopConf(\n    expectedErrMsg = \".*No FileSystem for scheme.*fake.*\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = false\n}\n"
  },
  {
    "path": "contribs/src/test/scala/io/delta/storage/OracleCloudLogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage\n\nimport org.apache.spark.sql.delta.{FakeFileSystem, LogStoreSuiteBase}\n\nclass OracleCloudLogStoreSuite extends LogStoreSuiteBase {\n\n  override val logStoreClassName: String = classOf[OracleCloudLogStore].getName\n\n  testHadoopConf(\n    expectedErrMsg = \"No FileSystem for scheme \\\"fake\\\"\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = true\n}\n"
  },
  {
    "path": "dev/check-delta-connect-codegen-python.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Utility for checking whether generated the Delta Connect Python protobuf codes are in sync.\n#   usage: ./dev/check-delta-connect-codegen-python.py\n\nimport os\nimport sys\nimport filecmp\nimport tempfile\nimport subprocess\n\n# Location of your Delta git development area\nDELTA_HOME = os.environ.get(\"DELTA_HOME\", os.path.abspath(os.path.join(__file__, os.pardir, os.pardir)))\n\n\ndef fail(msg):\n    print(msg)\n    sys.exit(-1)\n\n\ndef run_cmd(cmd):\n    print(f\"RUN: {cmd}\")\n    if isinstance(cmd, list):\n        return subprocess.check_output(cmd).decode(\"utf-8\")\n    else:\n        return subprocess.check_output(cmd.split(\" \")).decode(\"utf-8\")\n\n\ndef check_connect_protos():\n    generated_python_proto_codes_path = os.path.join(DELTA_HOME, \"python\", \"delta\", \"connect\", \"proto\")\n    print(f\"Start checking the generated codes in {generated_python_proto_codes_path}\")\n    generate_python_proto_codes_file = os.path.join(DELTA_HOME, \"dev\", \"delta-connect-gen-protos.sh\")\n    with tempfile.TemporaryDirectory() as tmp:\n        run_cmd([generate_python_proto_codes_file, tmp])\n        result = filecmp.dircmp(\n            generated_python_proto_codes_path,\n            tmp,\n            ignore=[\"__init__.py\", \"__pycache__\"],\n        )\n        success = True\n\n        if len(result.left_only) > 0:\n            print(f\"Unexpected files: {result.left_only}\")\n            success = False\n\n        if len(result.right_only) > 0:\n            print(f\"Missing files: {result.right_only}\")\n            success = False\n\n        if len(result.funny_files) > 0:\n            print(f\"Incomparable files: {result.funny_files}\")\n            success = False\n\n        if len(result.diff_files) > 0:\n            print(f\"Different files: {result.diff_files}\")\n            success = False\n\n        if success:\n            print(f\"Finish checking the generated codes in {generated_python_proto_codes_path}: SUCCESS\")\n        else:\n            fail(\n                f\"Generated files for {generated_python_proto_codes_path} are out of sync! \" +\n                f\"Please run {generate_python_proto_codes_file}.\"\n            )\n\n\ncheck_connect_protos()\n"
  },
  {
    "path": "dev/checkstyle-suppressions.xml",
    "content": "<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~    http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<!DOCTYPE suppressions PUBLIC\n        \"-//Puppy Crawl//DTD Suppressions 1.1//EN\"\n        \"https://checkstyle.org/dtds/suppressions_1_1.dtd\">\n\n<!--\n    This file contains suppression rules for Checkstyle checks.\n    Ideally only files that cannot be modified (e.g. third-party code)\n    should be added here. All other violations should be fixed.\n-->\n\n<suppressions>\n</suppressions>\n"
  },
  {
    "path": "dev/connectors-checkstyle.xml",
    "content": "<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~    http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<!DOCTYPE module PUBLIC\n          \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n          \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n\n    Checkstyle configuration based on the Google coding conventions from:\n\n    -  Google Java Style\n       https://google.github.io/styleguide/javaguide.html\n\n    with Spark-specific changes from:\n\n    http://spark.apache.org/contributing.html#code-style-guide\n\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.sf.net (or in your downloaded distribution).\n\n    Most Checks are configurable, be sure to consult the documentation.\n\n    To completely disable a check, just comment it out or delete it from the file.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n\n -->\n\n<module name = \"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <property name=\"severity\" value=\"error\"/>\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n\n    <module name=\"SuppressionFilter\">\n      <property name=\"file\" value=\"dev/checkstyle-suppressions.xml\"/>\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <module name=\"RegexpSingleline\">\n        <!-- \\s matches whitespace character, $ matches end of line. -->\n        <property name=\"format\" value=\"\\s+$\"/>\n        <property name=\"message\" value=\"No trailing whitespace allowed.\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"max\" value=\"100\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"NewlineAtEndOfFile\"/>\n\n    <module name=\"TreeWalker\">\n        <!--\n        If you wish to turn off checking for a section of code, you can put a comment in the source\n        before and after the section, with the following syntax:\n\n          // checkstyle.off: XXX (such as checkstyle.off: NoFinalizer)\n          ...  // stuff that breaks the styles\n          // checkstyle.on: XXX (such as checkstyle.on: NoFinalizer)\n        -->\n        <module name=\"SuppressionCommentFilter\">\n            <property name=\"offCommentFormat\" value=\"checkstyle\\.off\\: ([\\w\\|]+)\"/>\n            <property name=\"onCommentFormat\" value=\"checkstyle\\.on\\: ([\\w\\|]+)\"/>\n            <property name=\"checkFormat\" value=\"$1\"/>\n        </module>\n        <module name=\"OuterTypeFilename\"/>\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\"/>\n            <property name=\"format\" value=\"\\\\u00(08|09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\" value=\"Avoid using corresponding octal or Unicode escape.\"/>\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n        <module name=\"NoLineWrap\"/>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\" value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"allowSingleLineStatement\" value=\"true\"/>\n        </module>\n        <module name=\"OneStatementPerLine\"/>\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"FallThrough\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"SeparatorWrap\">\n            <property name=\"tokens\" value=\"DOT\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"tokens\" value=\"COMMA\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n             value=\"Package name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"([A-Z][a-zA-Z0-9]*$)\"/>\n            <message key=\"name.invalidPattern\"\n             value=\"Class type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"([A-Z][a-zA-Z0-9]*)\"/>\n            <message key=\"name.invalidPattern\"\n             value=\"Method type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n             value=\"GenericWhitespace ''{0}'' is followed by whitespace.\"/>\n             <message key=\"ws.preceded\"\n             value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\"/>\n             <message key=\"ws.illegalFollow\"\n             value=\"GenericWhitespace ''{0}'' should followed by whitespace.\"/>\n             <message key=\"ws.notPreceded\"\n             value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"4\"/>\n            <property name=\"braceAdjustment\" value=\"0\"/>\n            <property name=\"caseIndent\" value=\"4\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"4\"/>\n            <property name=\"arrayInitIndent\" value=\"4\"/>\n        </module>\n\n        <!--\n        We use the following import order:\n        import java.*\n        import javax.*\n        <blank line>\n        import scala.*\n        <blank line>\n        import all other imports\n        <blank line>\n        import io.delta.standalone.*\n        import io.delta.standalone.internal.*\n        -->\n        <module name=\"ImportOrder\">\n            <property name=\"separated\" value=\"true\"/>\n            <property name=\"ordered\" value=\"true\"/>\n            <property name=\"groups\" value=\"java.,javax.,scala,*,io.delta.standalone,io.delta.standalone.internal\"/>\n        </module>\n\n        <!--\n        As per https://checkstyle.sourceforge.io/config_imports.html, \"There is no flexibility to\n        enforce empty lines between some groups and no empty lines between other groups.\" However,\n        sometimes we want import X to come before import Y, yet we do not want them to be separated\n        by a blank line. This suppression is how we achieve that.\n        -->\n        <!-- io.delta.standalone and io.delta.standalone.internal -->\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"ImportOrder\"/>\n            <property name=\"message\" value=\"^'io.delta.standalone.internal\\..*'.*\"/>\n        </module>\n        <!-- java and javax -->\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"ImportOrder\"/>\n            <property name=\"message\" value=\"^'javax\\..*'.*\"/>\n        </module>\n\n        <module name=\"MethodParamPad\"/>\n        <module name=\"AnnotationLocation\">\n            <property name=\"tokens\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF\"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"tokens\" value=\"VARIABLE_DEF\"/>\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\"/>\n        </module>\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\"/>\n            <message key=\"name.invalidPattern\"\n             value=\"Method name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"expected\"/>\n        </module>\n        <module name=\"CommentsIndentation\"/>\n        <module name=\"UnusedImports\"/>\n        <module name=\"RedundantImport\"/>\n        <module name=\"RedundantModifier\"/>\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"format\" value=\"throw new \\w+Error\\(\"/>\n            <property name=\"message\" value=\"Avoid throwing error in application code.\"/>\n        </module>\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"format\" value=\"Objects\\.toStringHelper\"/>\n            <property name=\"message\" value=\"Avoid using Object.toStringHelper. Use ToStringBuilder instead.\" />\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "dev/copyrightHeader",
    "content": "\\/\\*\n \\* Copyright \\(\\d\\d\\d\\d\\) The Delta Lake Project Authors\\.\n \\*\n \\* Licensed under the Apache License, Version 2\\.0 \\(the \"License\"\\);\n \\* you may not use this file except in compliance with the License\\.\n \\* You may obtain a copy of the License at\n \\*\n \\* http:\\/\\/www\\.apache\\.org\\/licenses\\/LICENSE-2\\.0\n \\*\n \\* Unless required by applicable law or agreed to in writing, software\n \\* distributed under the License is distributed on an \"AS IS\" BASIS,\n \\* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\\.\n \\* See the License for the specific language governing permissions and\n \\* limitations under the License\\.\n \\*\\/"
  },
  {
    "path": "dev/delta-connect-gen-protos.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nexport PATH=\"$PATH:~/buf/bin\"\n\nset -ex\n\nif [[ $# -gt 1 ]]; then\n  echo \"Illegal number of parameters.\"\n  echo \"Usage: $0 [path]\"\n  exit -1\nfi\n\nDELTA_HOME=\"$(cd \"`dirname $0`\"/..; pwd)\"\ncd \"$DELTA_HOME\"\n\n\nOUTPUT_PATH=${DELTA_HOME}/python/delta/connect/proto/\nif [[ $# -eq 1 ]]; then\n  rm -Rf $1\n  mkdir -p $1\n  OUTPUT_PATH=$1\nfi\n\npushd ${DELTA_HOME}/spark-connect/common/src/main\n\nLICENSE=$(cat <<'EOF'\n#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nEOF)\necho \"$LICENSE\" > /tmp/tmp_licence\n\n\n# Delete the old generated protobuf files.\nrm -Rf gen\n\n# Now, regenerate the new files\nbuf generate --debug -vvv\n\n# We need to edit the generated python files to account for the actual package location and not\n# the location generated by proto.\nfor f in `find gen/proto/python/delta/connect -name \"*.py*\"`; do\n  # First fix the imports.\n  if [[ $f == *_pb2.py || $f == *_pb2_grpc.py ]]; then\n    sed \\\n      -e 's/import spark.connect./import pyspark.sql.connect.proto./g' \\\n      -e \"s/DESCRIPTOR, 'spark.connect/DESCRIPTOR, 'pyspark.sql.connect.proto/g\" \\\n      -e 's/from spark.connect import/from pyspark.sql.connect.proto import/g' \\\n      -e \"s/DESCRIPTOR, 'delta.connect/DESCRIPTOR, 'delta.connect.proto/g\" \\\n      -e 's/from delta.connect import/from delta.connect.proto import/g' \\\n      $f > $f.tmp\n    mv $f.tmp $f\n  elif [[ $f == *.pyi ]]; then\n    sed \\\n      -e 's/import spark.connect./import pyspark.sql.connect.proto./g' \\\n      -e 's/spark.connect./pyspark.sql.connect.proto./g' \\\n      -e 's/import delta.connect./import delta.connect.proto./g' \\\n      -e 's/delta.connect./delta.connect.proto./g' \\\n      $f > $f.tmp\n    mv $f.tmp $f\n  fi\n\n\n  # Prepend the Apache licence header to the files.\n  cp $f $f.bak\n  cat /tmp/tmp_licence $f.bak > $f\n  LC=$(wc -l < $f)\n  echo $LC\n  if [[ $f == *_grpc.py && $LC -eq 20 ]]; then\n    rm $f\n  fi\n  rm $f.bak\ndone\n\nblack --config $DELTA_HOME/dev/pyproject.toml gen/proto/python/delta/connect\n\n# Last step copy the result files to the destination module.\nfor f in `find gen/proto/python/delta/connect -name \"*.py*\"`; do\n  cp $f $OUTPUT_PATH\ndone\n\n# Clean up everything.\nrm -Rf gen\n"
  },
  {
    "path": "dev/kernel-checkstyle.xml",
    "content": "<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~    http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<!DOCTYPE module PUBLIC\n        \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n\n    Checkstyle configuration based on the Google coding conventions from:\n\n    -  Google Java Style\n       https://google.github.io/styleguide/javaguide.html\n\n    with Spark-specific changes from:\n\n    https://spark.apache.org/contributing.html#code-style-guide\n\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.sf.net (or in your downloaded distribution).\n\n    Most Checks are configurable, be sure to consult the documentation.\n\n    To completely disable a check, just comment it out or delete it from the file.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n\n -->\n\n<module name = \"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <property name=\"severity\" value=\"error\"/>\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"dev/checkstyle-suppressions.xml\"/>\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <module name=\"RegexpSingleline\">\n        <!-- \\s matches whitespace character, $ matches end of line. -->\n        <property name=\"format\" value=\"\\s+$\"/>\n        <property name=\"message\" value=\"No trailing whitespace allowed.\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"max\" value=\"100\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"NewlineAtEndOfFile\"/>\n\n    <module name=\"RegexpHeader\">\n        <property name=\"headerFile\" value=\"dev/copyrightHeader\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <!--\n        If you wish to turn off checking for a section of code, you can put a comment in the source\n        before and after the section, with the following syntax:\n\n          // checkstyle.off: XXX (such as checkstyle.off: NoFinalizer)\n          ...  // stuff that breaks the styles\n          // checkstyle.on: XXX (such as checkstyle.on: NoFinalizer)\n        -->\n        <module name=\"SuppressionCommentFilter\">\n            <property name=\"offCommentFormat\" value=\"checkstyle\\.off\\: ([\\w\\|]+)\"/>\n            <property name=\"onCommentFormat\" value=\"checkstyle\\.on\\: ([\\w\\|]+)\"/>\n            <property name=\"checkFormat\" value=\"$1\"/>\n        </module>\n        <module name=\"OuterTypeFilename\"/>\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\"/>\n            <property name=\"format\" value=\"\\\\u00(08|09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\" value=\"Avoid using corresponding octal or Unicode escape.\"/>\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n        <module name=\"NoLineWrap\"/>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\" value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"allowSingleLineStatement\" value=\"true\"/>\n        </module>\n        <module name=\"OneStatementPerLine\"/>\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"FallThrough\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"SeparatorWrap\">\n            <property name=\"tokens\" value=\"DOT\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"tokens\" value=\"COMMA\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Package name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"([A-Z][a-zA-Z0-9]*$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Class type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"([A-Z][a-zA-Z0-9]*)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                     value=\"GenericWhitespace ''{0}'' is followed by whitespace.\"/>\n            <message key=\"ws.preceded\"\n                     value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\"/>\n            <message key=\"ws.illegalFollow\"\n                     value=\"GenericWhitespace ''{0}'' should followed by whitespace.\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"MethodParamPad\"/>\n        <module name=\"AnnotationLocation\">\n            <property name=\"tokens\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF\"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"tokens\" value=\"VARIABLE_DEF\"/>\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\"/>\n        </module>\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"expected\"/>\n        </module>\n        <module name=\"UnusedImports\"/>\n        <module name=\"RedundantImport\"/>\n        <module name=\"RedundantModifier\"/>\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"format\" value=\"throw new \\w+Error\\(\"/>\n            <property name=\"message\" value=\"Avoid throwing error in application code.\"/>\n        </module>\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"format\" value=\"Objects\\.toStringHelper\"/>\n            <property name=\"message\" value=\"Avoid using Object.toStringHelper. Use ToStringBuilder instead.\" />\n        </module>\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"format\" value=\"new (java\\.lang\\.)?(Byte|Integer|Long|Short)\\(\"/>\n            <property name=\"message\" value=\"Use static factory 'valueOf' or 'parseXXX' instead of the deprecated constructors.\" />\n        </module>\n\n        <!--\n        As per https://checkstyle.sourceforge.io/config_imports.html, \"There is no flexibility to\n        enforce empty lines between some groups and no empty lines between other groups.\" However,\n        sometimes we want import X to come before import Y, yet we do not want them to be separated\n        by a blank line. This suppression is how we achieve that.\n        -->\n        <!-- io.delta.kernel and io.delta.kernel.internal -->\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"ImportOrder\"/>\n            <property name=\"message\" value=\"^'io.delta.kernel.internal\\..*'.*\"/>\n        </module>\n\n        <!-- io.delta.kernel.defaults and io.delta.kernel.defaults.internal -->\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"ImportOrder\"/>\n            <property name=\"message\" value=\"^'io.delta.kernel.defaults.internal\\..*'.*\"/>\n        </module>\n\n        <!-- java and javax -->\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"ImportOrder\"/>\n            <property name=\"message\" value=\"^'javax\\..*'.*\"/>\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "dev/lint-python",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# define test binaries + versions\nPYDOCSTYLE_BUILD=\"pydocstyle\"\nMINIMUM_PYDOCSTYLE=\"3.0.0\"\n\nFLAKE8_BUILD=\"flake8\"\nMINIMUM_FLAKE8=\"3.5.0\"\n\nPYCODESTYLE_BUILD=\"pycodestyle\"\nMINIMUM_PYCODESTYLE=\"2.4.0\"\n\n\nfunction compile_python_test {\n    local COMPILE_STATUS=\n    local COMPILE_REPORT=\n\n    if [[ ! \"$1\" ]]; then\n        echo \"No python files found!  Something is very wrong -- exiting.\"\n        exit 1;\n    fi\n\n    # compileall: https://docs.python.org/2/library/compileall.html\n    echo \"starting python compilation test...\"\n    COMPILE_REPORT=$( (python3 -B -mcompileall -q -l $1) 2>&1)\n    COMPILE_STATUS=$?\n\n    if [ $COMPILE_STATUS -ne 0 ]; then\n        echo \"Python compilation failed with the following errors:\"\n        echo \"$COMPILE_REPORT\"\n        echo \"$COMPILE_STATUS\"\n        exit \"$COMPILE_STATUS\"\n    else\n        echo \"python compilation succeeded.\"\n        echo\n    fi\n}\n\nfunction pycodestyle_test {\n    local PYCODESTYLE_STATUS=\n    local PYCODESTYLE_REPORT=\n    local RUN_LOCAL_PYCODESTYLE=\n    local VERSION=\n    local EXPECTED_PYCODESTYLE=\n    local PYCODESTYLE_SCRIPT_PATH=\"$DELTA_ROOT_DIR/dev/pycodestyle-$MINIMUM_PYCODESTYLE.py\"\n    local PYCODESTYLE_SCRIPT_REMOTE_PATH=\"https://raw.githubusercontent.com/PyCQA/pycodestyle/$MINIMUM_PYCODESTYLE/pycodestyle.py\"\n\n    if [[ ! \"$1\" ]]; then\n        echo \"No python files found!  Something is very wrong -- exiting.\"\n        exit 1;\n    fi\n\n    # check for locally installed pycodestyle & version\n    RUN_LOCAL_PYCODESTYLE=\"False\"\n    if hash \"$PYCODESTYLE_BUILD\" 2> /dev/null; then\n        VERSION=$( $PYCODESTYLE_BUILD --version 2> /dev/null)\n        EXPECTED_PYCODESTYLE=$( (python3 -c 'from distutils.version import LooseVersion;\n                                print(LooseVersion(\"\"\"'${VERSION[0]}'\"\"\") >= LooseVersion(\"\"\"'$MINIMUM_PYCODESTYLE'\"\"\"))')\\\n                                2> /dev/null)\n\n        if [ \"$EXPECTED_PYCODESTYLE\" == \"True\" ]; then\n            RUN_LOCAL_PYCODESTYLE=\"True\"\n        fi\n    fi\n\n    # download the right version or run locally\n    if [ $RUN_LOCAL_PYCODESTYLE == \"False\" ]; then\n        # Get pycodestyle at runtime so that we don't rely on it being installed on the build server.\n        # See: https://github.com/apache/spark/pull/1744#issuecomment-50982162\n        # Updated to the latest official version of pep8. pep8 is formally renamed to pycodestyle.\n        echo \"downloading pycodestyle from $PYCODESTYLE_SCRIPT_REMOTE_PATH...\"\n        if [ ! -e \"$PYCODESTYLE_SCRIPT_PATH\" ]; then\n            curl --silent -o \"$PYCODESTYLE_SCRIPT_PATH\" \"$PYCODESTYLE_SCRIPT_REMOTE_PATH\"\n            local curl_status=\"$?\"\n\n            if [ \"$curl_status\" -ne 0 ]; then\n                echo \"Failed to download pycodestyle.py from $PYCODESTYLE_SCRIPT_REMOTE_PATH\"\n                exit \"$curl_status\"\n            fi\n        fi\n\n        echo \"starting pycodestyle test...\"\n        PYCODESTYLE_REPORT=$( (python3 \"$PYCODESTYLE_SCRIPT_PATH\" --config=dev/tox.ini $1) 2>&1)\n        PYCODESTYLE_STATUS=$?\n    else\n        # we have the right version installed, so run locally\n        echo \"starting pycodestyle test...\"\n        PYCODESTYLE_REPORT=$( ($PYCODESTYLE_BUILD --config=dev/tox.ini $1) 2>&1)\n        PYCODESTYLE_STATUS=$?\n    fi\n\n    if [ $PYCODESTYLE_STATUS -ne 0 ]; then\n        echo \"pycodestyle checks failed:\"\n        echo \"$PYCODESTYLE_REPORT\"\n        exit \"$PYCODESTYLE_STATUS\"\n    else\n        echo \"pycodestyle checks passed.\"\n        echo\n    fi\n}\n\nfunction flake8_test {\n    local FLAKE8_VERSION=\n    local VERSION=\n    local EXPECTED_FLAKE8=\n    local FLAKE8_REPORT=\n    local FLAKE8_STATUS=\n\n    if ! hash \"$FLAKE8_BUILD\" 2> /dev/null; then\n        echo \"The flake8 command was not found.\"\n        echo \"flake8 checks failed.\"\n        exit 1\n    fi\n\n    FLAKE8_VERSION=\"$($FLAKE8_BUILD --version  2> /dev/null)\"\n    VERSION=($FLAKE8_VERSION)\n    EXPECTED_FLAKE8=$( (python3 -c 'from distutils.version import LooseVersion;\n                       print(LooseVersion(\"\"\"'${VERSION[0]}'\"\"\") >= LooseVersion(\"\"\"'$MINIMUM_FLAKE8'\"\"\"))') \\\n                       2> /dev/null)\n\n    if [[ \"$EXPECTED_FLAKE8\" == \"False\" ]]; then\n        echo \"\\\nThe minimum flake8 version needs to be $MINIMUM_FLAKE8. Your current version is $FLAKE8_VERSION\n\nflake8 checks failed.\"\n        exit 1\n    fi\n\n    echo \"starting $FLAKE8_BUILD test...\"\n    FLAKE8_REPORT=$( ($FLAKE8_BUILD $1 --count --select=E901,E999,F821,F822,F823 \\\n                     --max-line-length=100 --show-source --statistics) 2>&1)\n    FLAKE8_STATUS=$?\n\n    if [ \"$FLAKE8_STATUS\" -ne 0 ]; then\n        echo \"flake8 checks failed:\"\n        echo \"$FLAKE8_REPORT\"\n        echo \"$FLAKE8_STATUS\"\n        exit \"$FLAKE8_STATUS\"\n    else\n        echo \"flake8 checks passed.\"\n        echo\n    fi\n}\n\nfunction pydocstyle_test {\n    local PYDOCSTYLE_REPORT=\n    local PYDOCSTYLE_STATUS=\n    local PYDOCSTYLE_VERSION=\n    local EXPECTED_PYDOCSTYLE=\n\n    # Exclude auto-generated configuration file.\n    local DOC_PATHS_TO_CHECK=\"$( cd \"${DELTA_ROOT_DIR}\" && find . -name \"*.py\" | grep -vF 'functions.py' )\"\n\n    # Check python document style, skip check if pydocstyle is not installed.\n    if ! hash \"$PYDOCSTYLE_BUILD\" 2> /dev/null; then\n        echo \"The pydocstyle command was not found. Skipping pydocstyle checks for now.\"\n        echo\n        return\n    fi\n\n    PYDOCSTYLE_VERSION=\"$($PYDOCSTYLEBUILD --version 2> /dev/null)\"\n    EXPECTED_PYDOCSTYLE=$(python3 -c 'from distutils.version import LooseVersion; \\\n                             print(LooseVersion(\"\"\"'$PYDOCSTYLE_VERSION'\"\"\") >= LooseVersion(\"\"\"'$MINIMUM_PYDOCSTYLE'\"\"\"))' \\\n                             2> /dev/null)\n\n    if [[ \"$EXPECTED_PYDOCSTYLE\" == \"False\" ]]; then\n        echo \"\\\nThe minimum version of pydocstyle needs to be $MINIMUM_PYDOCSTYLE.\nYour current version is $PYDOCSTYLE_VERSION.\nSkipping pydocstyle checks for now.\"\n        echo\n        return\n    fi\n\n    echo \"starting $PYDOCSTYLE_BUILD test...\"\n    PYDOCSTYLE_REPORT=$( ($PYDOCSTYLE_BUILD --config=dev/tox.ini $DOC_PATHS_TO_CHECK) 2>&1)\n    PYDOCSTYLE_STATUS=$?\n\n    if [ \"$PYDOCSTYLE_STATUS\" -ne 0 ]; then\n        echo \"pydocstyle checks failed:\"\n        echo \"$PYDOCSTYLE_REPORT\"\n        exit \"$PYDOCSTYLE_STATUS\"\n    else\n        echo \"pydocstyle checks passed.\"\n        echo\n    fi\n}\n\nSCRIPT_DIR=\"$( cd \"$( dirname \"$0\" )\" && pwd )\"\nDELTA_ROOT_DIR=\"$(dirname \"${SCRIPT_DIR}\")\"\n\npushd \"$DELTA_ROOT_DIR\" &> /dev/null\n\nPYTHON_SOURCE=\"$(find \"${DELTA_ROOT_DIR}/python\" -name \"*.py\")\"\n\ncompile_python_test \"$PYTHON_SOURCE\"\npycodestyle_test \"$PYTHON_SOURCE\"\n#flake8_test \"$PYTHON_SOURCE\"\npydocstyle_test\n\necho\necho \"all lint-python tests passed!\"\n\npopd &> /dev/null\n"
  },
  {
    "path": "dev/pyproject.toml",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n[tool.black]\n# When changing the version, we have to update\n# GitHub workflow version\nrequired-version = \"23.12.1\"\nline-length = 100\ntarget-version = ['py38']\ninclude = '\\.pyi?$'\n"
  },
  {
    "path": "dev/requirements.txt",
    "content": "# Linter\nmypy==1.8.0\nflake8==3.9.0\n\n# Code Formatter\nblack==23.12.1\n\n# Spark Connect (required)\ngrpcio>=1.67.0\ngrpcio-status>=1.67.0\ngoogleapis-common-protos>=1.65.0\n\n# Spark and Delta Connect python proto generation plugin (optional)\nmypy-protobuf==3.3.0\n"
  },
  {
    "path": "dev/spark_structured_logging_style.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport sys\nimport re\nimport glob\n\n\ndef main():\n    log_pattern = r\"log(?:Info|Warning|Error)\\(.*?\\)\\n\"\n    inner_log_pattern = r'\".*?\"\\.format\\(.*\\)|s?\".*?(?:\\$|\\\"\\+(?!s?\")).*|[^\"]+\\+\\s*\".*?\"'\n    compiled_inner_log_pattern = re.compile(inner_log_pattern)\n\n    # Regex patterns for file paths to exclude from the Structured Logging style check\n    excluded_file_patterns = [\n        \"[Tt]est\",\n        \"sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen\"\n        \"/CodeGenerator.scala\",\n        \"streaming/src/main/scala/org/apache/spark/streaming/scheduler/JobScheduler.scala\",\n        \"sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver\"\n        \"/SparkSQLCLIService.scala\",\n        \"core/src/main/scala/org/apache/spark/deploy/SparkSubmit.scala\",\n    ]\n\n    nonmigrated_files = {}\n\n    target_directories = [\"../spark\", \"../iceberg\", \"../hudi\"]\n    scala_files = []\n    for directory in target_directories:\n        scala_files.extend(glob.glob(os.path.join(directory, \"**\", \"*.scala\"), recursive=True))\n\n    for file in scala_files:\n        skip_file = False\n        for exclude_pattern in excluded_file_patterns:\n            if re.search(exclude_pattern, file):\n                skip_file = True\n                break\n\n        if not skip_file and not os.path.isdir(file):\n            with open(file, \"r\") as f:\n                content = f.read()\n\n                log_statements = re.finditer(log_pattern, content, re.DOTALL)\n\n                if log_statements:\n                    nonmigrated_files[file] = []\n                    for log_statement in log_statements:\n                        log_statement_str = log_statement.group(0).strip()\n                        # trim first ( and last )\n                        first_paren_index = log_statement_str.find(\"(\")\n                        inner_log_statement = re.sub(\n                            r\"\\s+\", \"\", log_statement_str[first_paren_index + 1 : -1]\n                        )\n\n                        if compiled_inner_log_pattern.fullmatch(inner_log_statement):\n                            start_pos = log_statement.start()\n                            preceding_content = content[:start_pos]\n                            line_number = preceding_content.count(\"\\n\") + 1\n                            start_char = start_pos - preceding_content.rfind(\"\\n\") - 1\n                            nonmigrated_files[file].append((line_number, start_char))\n\n    if all(len(issues) == 0 for issues in nonmigrated_files.values()):\n        print(\"Structured logging style check passed.\", file=sys.stderr)\n        sys.exit(0)\n    else:\n        for file_path, issues in nonmigrated_files.items():\n            for line_number, start_char in issues:\n                print(f\"[error] {file_path}:{line_number}:{start_char}\", file=sys.stderr)\n                print(\n                    \"[error]\\tPlease use the Structured Logging Framework for logging messages \"\n                    'with variables. For example: log\"...${{MDC(TASK_ID, taskId)}}...\"'\n                    \"\\n\\tRefer to the guidelines in the file `shims/LoggingShims.scala`.\",\n                    file=sys.stderr,\n                )\n\n        sys.exit(-1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "dev/tox.ini",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[pycodestyle]\nignore=E226,E231,E241,E305,E402,E722,E731,E741,W503,W504,W604\nmax-line-length=100\nexclude=cloudpickle.py,heapq3.py,shared.py,python/docs/conf.py,work/*/*.py,python/.eggs/*,dist/*,*python/delta/connect*\n[pydocstyle]\nignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D201,D202,D203,D204,D205,D206,D207,D208,D209,D210,D211,D212,D213,D214,D215,D300,D301,D302,D400,D401,D402,D403,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D415,D417\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# build output\ndist/\npublic/api\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n# Local Netlify folder\n.netlify\n"
  },
  {
    "path": "docs/.nvmrc",
    "content": "v22.18.0"
  },
  {
    "path": "docs/.prettierignore",
    "content": "coverage\npublic\ndist\n.astro\npnpm-lock.yaml\n.DS_Store\nsrc/content/**/*.mdx"
  },
  {
    "path": "docs/.prettierrc.json",
    "content": "{\n  \"plugins\": [\"prettier-plugin-astro\"],\n  \"overrides\": [\n    {\n      \"files\": \"*.astro\",\n      \"options\": {\n        \"parser\": \"astro\"\n      }\n    },\n    {\n      \"files\": [\"*.md\", \"*.mdx\"],\n      \"options\": {\n        \"parser\": \"mdx\",\n        \"proseWrap\": \"never\",\n        \"embeddedLanguageFormatting\": \"auto\",\n        \"htmlWhitespaceSensitivity\": \"css\",\n        \"bracketSameLine\": false,\n        \"singleAttributePerLine\": true,\n        \"tabWidth\": 2\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Docs Generation Scripts\n\nThis directory contains scripts to generate docs for https://docs.delta.io, including the API Docs for Scala, Java, and Python APIs.\n\n## Setup Environment\n\n### Install Node environment\n\nInstall node v22.14.0 using [nvm](https://github.com/nvm-sh/nvm):\n\n```\nnvm install\n```\n\nThen, install [pnpm](https://pnpm.io/):\n\n```\nnpm install --global corepack@latest\ncorepack enable pnpm\n```\n\nFinally, install dependencies:\n\n```\npnpm i\n```\n\n### Install Conda environment\n\nFollow [Conda Download](https://www.anaconda.com/download/) to install Anaconda.\n\nThen, follow [Create Environment From Environment file](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-from-file) to create a Conda environment from `/delta/docs/environment.yml` and activate the newly created `delta_docs` environment.\n\n```\n# Note the `--file` argument should be a fully qualified path. Using `~` in file\n# path doesn't work. Example valid path: `/Users/macuser/delta/docs/environment.yml`\n\nconda env create --name delta_docs --file=<absolute_path_to_delta_repo>/docs/environment.yml`\n```\n\n### JDK Setup\n\nAPI doc generation needs JDK 1.8. Make sure to setup `JAVA_HOME` that points to JDK 1.8.\n\n### Set the Delta Lake version\n\nSet the version of Delta Lake release these docs are being generated for.\n\n```\nexport _DELTA_LAKE_RELEASE_VERSION_=3.3.0\n```\n\n## Usage\n\nRun the command from the `delta` repo root directory:\n\n```\npython3 docs/generate_docs.py --livehtml --api-docs\n```\n\nAbove command will print a URL to preview the docs.\n\n### Skip generating API docs\n\nAbove command generates API docs which take time. If you are just interested in the docs\nthat go on https://docs.delta.io, use the following command.\n\n```\npython3 docs/generate_docs.py --livehtml\n```\n\n### Building for production\n\nTo build the docs for production, run the following command:\n\n```python\npython3 docs/generate_docs.py --api-docs\n```\n\nThe resulting files will be found in `docs/dist`.\n\n### Additional docs site commands\n\nThe docs site is built on [Astro](https://astro.build/). Using pnpm, you can run a variety of commands:\n\n| Command               | Description                                     |\n| --------------------- | ----------------------------------------------- |\n| `pnpm run lint`       | Run ESLint on the docs site code                |\n| `pnpm run format`     | Format docs site code using Prettier            |\n| `pnpm run dev`        | Start Astro in development mode                 |\n| `pnpm run build`      | Build the Astro site for production             |\n| `pnpm run preview`    | Preview the built Astro site                    |\n| `pnpm run astro`      | Run Astro CLI                                   |\n"
  },
  {
    "path": "docs/apis/api-docs.css",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/* Dynamically injected style for the API docs */\n\n.unstable {\n  background-color: #EE4B2B;\n}\n\n.developer {\n  background-color: #0047AB;\n}\n\n.evolving {\n  background-color: #44751E;\n}\n\n.badge {\n  font-family: Arial, san-serif;\n  float: right;\n}\n"
  },
  {
    "path": "docs/apis/api-docs.js",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/* Dynamically injected post-processing code for the API docs */\n\n$(document).ready(function() {\n  var annotations = $(\"dt:contains('Annotations')\").next(\"dd\").children(\"span.name\");\n  addBadges(annotations, \"Unstable\", \":: Unstable ::\", '<span class=\"unstable badge\">Unstable API</span>');\n  addBadges(annotations, \"Developer\", \":: DeveloperApi ::\", '<span class=\"developer badge\">Developer API</span>');\n  addBadges(annotations, \"Evolving\", \":: Evolving ::\", '<span class=\"evolving badge\">Evolving API</span>');\n});\n\nfunction addBadges(allAnnotations, name, tag, html) {\n  var annotations = allAnnotations.filter(\":contains('\" + name + \"')\")\n  var tags = $(\".cmt:contains(\" + tag + \")\")\n\n  // Remove identifier tags from comments\n  tags.each(function(index) {\n    var oldHTML = $(this).html();\n    var newHTML = oldHTML.replace(tag, \"\");\n    $(this).html(newHTML);\n  });\n\n  // Add badges to all containers\n  tags\n    // Scala 2.11 docs require these\n    .prevAll(\"h4.signature\")\n    .add(annotations.closest(\"div.fullcommenttop\"))\n    .add(annotations.closest(\"div.fullcomment\").prevAll(\"h4.signature\"))\n    // Scala 2.12 docs require this\n    .add(tags.prevAll(\"span.symbol\"))\n    .add(annotations.closest(\"div.fullcomment\").prevAll(\"span.symbol\"))\n    .prepend(html);\n}\n"
  },
  {
    "path": "docs/apis/api-javadocs.css",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/* Dynamically injected style for the API docs */\n\n.badge {\n  font-family: Arial, san-serif;\n  float: right;\n  margin: 4px;\n  /* The following declarations are taken from the ScalaDoc template.css */\n  display: inline-block;\n  padding: 2px 4px;\n  font-size: 11.844px;\n  font-weight: bold;\n  line-height: 14px;\n  color: #ffffff;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n  white-space: nowrap;\n  vertical-align: baseline;\n  background-color: #999999;\n  padding-right: 9px;\n  padding-left: 9px;\n  -webkit-border-radius: 9px;\n     -moz-border-radius: 9px;\n          border-radius: 9px;\n}\n\n.unstable {\n  background-color: #EE4B2B;\n}\n\n.developer {\n  background-color: #0047AB;\n}\n\n.evolving {\n  background-color: #44751E;\n}\n\n"
  },
  {
    "path": "docs/apis/api-javadocs.js",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/* Dynamically injected post-processing code for the API docs */\n\n$(document).ready(function() {\n  addBadges(\":: Unstable ::\", '<span class=\"unstable badge\">Unstable API</span>');\n  addBadges(\":: DeveloperApi ::\", '<span class=\"developer badge\">Developer API</span>');\n  addBadges(\":: Evolving ::\", '<span class=\"evolving badge\">Evolving API</span>');\n});\n\nfunction addBadges(tag, html) {\n  var tags = $(\".block:contains(\" + tag + \")\")\n\n  // Remove identifier tags\n  tags.each(function(index) {\n    var oldHTML = $(this).html();\n    var newHTML = oldHTML.replace(tag, \"\");\n    $(this).html(newHTML);\n  });\n\n  // Add html badge tags\n  tags.each(function(index) {\n    if ($(this).parent().is('td.colLast')) {\n      $(this).parent().prepend(html);\n    } else if ($(this).parent('li.blockList')\n                      .parent('ul.blockList')\n                      .parent('div.description')\n                      .parent().is('div.contentContainer')) {\n      var contentContainer = $(this).parent('li.blockList')\n                                    .parent('ul.blockList')\n                                    .parent('div.description')\n                                    .parent('div.contentContainer')\n      var header = contentContainer.prev('div.header');\n      if (header.length > 0) {\n        header.prepend(html);\n      } else {\n        contentContainer.prepend(html);\n      }\n    } else if ($(this).parent().is('li.blockList')) {\n      $(this).parent().prepend(html);\n    } else {\n      $(this).prepend(html);\n    }\n  });\n}\n"
  },
  {
    "path": "docs/apis/generate_api_docs.py",
    "content": "# !/usr/bin/env python3\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\nimport os\nimport sys\nimport subprocess\nimport argparse\n\n\ndef main():\n    # Parse arguments\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-v\", \"--verbose\", default=False, action='store_true')\n    args = parser.parse_args()\n    global verbose\n    verbose = args.verbose\n\n    # Set up the directories\n    docs_root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))\n    repo_root_dir = os.path.dirname(docs_root_dir)\n\n    # --- dirs where docs are generated\n    spark_scaladoc_gen_dir = repo_root_dir + \"/spark/target/scala-2.13/unidoc\"\n    spark_javadoc_gen_dir = repo_root_dir + \"/spark/target/javaunidoc\"\n    spark_pythondoc_dir = repo_root_dir + \"/docs/apis/python\"\n    spark_pythondoc_gen_dir = spark_pythondoc_dir + \"/_build/html\"\n\n    kernel_javadoc_gen_dir = repo_root_dir + \"/kernelGroup/target/javaunidoc\"\n\n    # --- final dirs where the docs will be copied to\n    all_docs_final_dir = docs_root_dir + \"/apis/_site/api\"\n    all_javadocs_final_dir = all_docs_final_dir + \"/java\"\n    all_scaladocs_final_dir = all_docs_final_dir + \"/scala\"\n    all_pythondocs_final_dir = all_docs_final_dir + \"/python\"\n\n    spark_javadoc_final_dir = all_javadocs_final_dir + \"/spark\"\n    spark_scaladoc_final_dir = all_scaladocs_final_dir + \"/spark\"\n    spark_pythondoc_final_dir = all_pythondocs_final_dir + \"/spark\"\n\n    kernel_javadoc_final_dir = all_javadocs_final_dir + \"/kernel\"\n\n\n    # Generate Java and Scala docs\n    print(\"## Generating Scala and Java docs ...\")\n    with WorkingDirectory(repo_root_dir):\n        run_cmd([\"build/sbt\", \";clean;unidoc\"], stream_output=verbose)\n\n    # Update Scala docs\n    print(\"## Patching Scala docs ...\")\n    patch_scala_docs(spark_scaladoc_gen_dir, docs_root_dir)\n\n    # Update Java docs\n    print(\"## Patching Java docs ...\")\n    jquery_path = spark_scaladoc_gen_dir + \"/lib/jquery.min.js\" # grab the JQuery library from Scaladocs\n    all_javadoc_gen_dirs = [\n        spark_javadoc_gen_dir,\n        kernel_javadoc_gen_dir,\n    ]\n    for javadoc_gen_dir in all_javadoc_gen_dirs:\n        patch_java_docs(javadoc_gen_dir, docs_root_dir, jquery_path)\n\n    # Generate Python docs\n    print('## Generating Python docs ...')\n    with WorkingDirectory(spark_pythondoc_dir):\n        run_cmd([\"make\", \"html\"], stream_output=verbose)\n\n    # Copy to final location\n    log(\"## Copying to API doc directory %s\" % all_docs_final_dir)\n    src_dst_dirs = [\n        (spark_javadoc_gen_dir, spark_javadoc_final_dir),\n        (spark_scaladoc_gen_dir, spark_scaladoc_final_dir),\n        (spark_pythondoc_gen_dir, spark_pythondoc_final_dir),\n        (kernel_javadoc_gen_dir, kernel_javadoc_final_dir),\n    ]\n\n    run_cmd([\"rm\", \"-rf\", all_docs_final_dir])\n    run_cmd([\"mkdir\", \"-p\", all_docs_final_dir])\n    for (src_dir, dst_dir) in src_dst_dirs:\n        run_cmd([\"mkdir\", \"-p\", dst_dir])\n        run_cmd([\"cp\", \"-r\", src_dir.rstrip(\"/\") + \"/\", dst_dir])\n\n    print(\"## API docs generated in \" + all_docs_final_dir)\n\n\ndef patch_scala_docs(scaladoc_dir, docs_root_dir):\n    with WorkingDirectory(scaladoc_dir):\n        # Patch the js and css files\n        append(docs_root_dir + \"/apis/api-docs.js\", \"./lib/template.js\")  # append new js functions\n        append(docs_root_dir + \"/apis/api-docs.css\", \"./lib/template.css\")  # append new styles\n\n\ndef patch_java_docs(javadoc_dir, docs_root_dir, jquery_path):\n    print(\"### Patching JavaDoc in %s ...\" % javadoc_dir)\n    with WorkingDirectory(javadoc_dir):\n        # Find html files to patch\n        (_, stdout, _) = run_cmd([\"find\", \".\", \"-name\", \"*.html\", \"-mindepth\", \"2\"])\n        log(\"HTML files found:\\n\" + stdout)\n        javadoc_files = [line for line in stdout.split('\\n') if line.strip() != '']\n\n        js_script_start = '<script defer=\"defer\" type=\"text/javascript\" src=\"'\n        js_script_end = '\"></script>'\n\n        # Patch the html files\n        for javadoc_file in javadoc_files:\n            # Generate relative path to js files based on how deep the html file is\n            slash_count = javadoc_file.count(\"/\")\n            i = 1\n            path_to_js_file = \"\"\n            while i < slash_count:\n                path_to_js_file = path_to_js_file + \"../\"\n                i += 1\n\n            # Create script elements to load new js files\n            javadoc_jquery_script = \\\n                js_script_start + path_to_js_file + \"lib/jquery.min.js\" + js_script_end\n            javadoc_api_docs_script = \\\n                js_script_start + path_to_js_file + \"lib/api-javadocs.js\" + js_script_end\n            javadoc_script_elements = javadoc_jquery_script + javadoc_api_docs_script\n\n            # Add script elements to body of the html file\n            replace(javadoc_file, \"</body>\", javadoc_script_elements + \"</body>\")\n\n        # Patch the js and css files\n        run_cmd([\"mkdir\", \"-p\", \"./lib\"])\n        run_cmd([\"cp\", jquery_path, \"./lib/\"])  # copy from ScalaDocs\n        run_cmd([\"cp\", docs_root_dir + \"/apis/api-javadocs.js\", \"./lib/\"])   # copy new js file\n        append(docs_root_dir + \"/apis/api-javadocs.css\", \"./stylesheet.css\")  # append new styles\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, **kwargs):\n    \"\"\"Runs a command as a child process.\n\n    A convenience wrapper for running a command from a Python script.\n    Keyword arguments:\n    cmd -- the command to run, as a list of strings\n    throw_on_error -- if true, raises an Exception if the exit code of the program is nonzero\n    env -- additional environment variables to be defined when running the child process\n    stream_output -- if true, does not capture standard output and error; if false, captures these\n      streams and returns them\n\n    Note on the return value: If stream_output is true, then only the exit code is returned. If\n    stream_output is false, then a tuple of the exit code, standard output and standard error is\n    returned.\n    \"\"\"\n    log(\"Running command %s\" % str(cmd))\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % exit_code)\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        if sys.version_info >= (3, 0):\n            stdout = stdout.decode(\"UTF-8\")\n            stderr = stderr.decode(\"UTF-8\")\n\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return (exit_code, stdout, stderr)\n\n\ndef append(src, dst):\n    log(\"Appending %s to %s\" % (src, dst))\n    fin = open(src, \"r\")\n    str = fin.read()\n    fin.close()\n    fout = open(dst, \"a\")\n    fout.write(str)\n    fout.close()\n\n\ndef replace(file, pattern, replacement):\n    log(\"Replacing %s with %s in file %s\" % (pattern, replacement, file))\n    fin = open(file, \"r\")\n    str = fin.read()\n    fin.close()\n    str = str.replace(pattern, replacement)\n    fout = open(file, \"w\")\n    fout.write(str)\n    fout.close()\n\n\n# pylint: disable=too-few-public-methods\nclass WorkingDirectory(object):\n    def __init__(self, working_directory):\n        self.working_directory = working_directory\n        self.old_workdir = os.getcwd()\n\n    def __enter__(self):\n        os.chdir(self.working_directory)\n\n    def __exit__(self, tpe, value, traceback):\n        os.chdir(self.old_workdir)\n\n\ndef log(str):\n    if verbose:\n        print(str)\n\n\nverbose = False\n\nif __name__ == \"__main__\":\n    # pylint: disable=e1120\n    main()\n"
  },
  {
    "path": "docs/apis/python/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?= -W\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/apis/python/conf.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nimport pathlib\n\npython_docs_root_dir = os.path.dirname(os.path.realpath(__file__))\ndocs_dir = os.path.dirname(python_docs_root_dir)\nroot_dir = os.path.dirname(docs_dir)\ndelta_root_dir = pathlib.Path(root_dir).parent\nversion_file_path = os.path.join(delta_root_dir, 'version.sbt')\n\nsys.path.insert(0, os.path.abspath(os.path.join(root_dir, 'python')))\n\n\n# -- Project information -----------------------------------------------------\n\nproject = 'delta-spark'\n\n# The full version, including alpha/beta/rc tags\nrelease = '0.0.0'\nfor line in open(version_file_path):\n    if \"ThisBuild\" in line:\n        release = line.split(\"\\\"\")[1]\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'nature'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = []  # default value was ['_static']\n\n\n# Fix for doc generation to work on older version of sphinx as well.\nmaster_doc = 'index'\n\n# Display the classes in the generated in the same order as the classes appear in the source files.`\nautodoc_member_order = 'bysource'\n"
  },
  {
    "path": "docs/apis/python/index.rst",
    "content": ".. delta documentation master file, created by\n   sphinx-quickstart on Fri Sep 20 16:32:12 2019.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to Delta Lake's Python documentation page\n=================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n\nDeltaTable\n==========\n.. automodule:: delta.tables\n   :members:\n   :undoc-members:\n\nExceptions\n==========\n.. automodule:: delta.exceptions\n   :members:\n   :undoc-members:\n\nOthers\n======\n.. automodule:: delta.pip_utils\n   :members:\n   :undoc-members:\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/astro.config.mjs",
    "content": "// @ts-check\nimport { defineConfig } from \"astro/config\";\nimport starlight from \"@astrojs/starlight\";\nimport netlify from \"@astrojs/netlify\";\n\n// https://astro.build/config\nexport default defineConfig({\n  site: \"https://docs.delta.io/\",\n  image: {\n    service: {\n      entrypoint: \"astro/assets/services/sharp\",\n    },\n  },\n  adapter: netlify(),\n  redirects: {\n    \"/latest/api/*\": \"/api/latest/:splat\",\n    \"/:version/api/*\": \"/api/:version/:splat\",\n    \"/latest/*\": \"/:splat\",\n    \"/delta-intro.html\": \"/\",\n    \"/delta-spark.html\": \"/\",\n    \"/quick-start.html\": \"/quick-start\",\n    \"/delta-batch.html\": \"/delta-batch\",\n    \"/delta-streaming.html\": \"/delta-streaming\",\n    \"/delta-update.html\": \"/delta-update\",\n    \"/delta-change-data-feed.html\": \"/delta-change-data-feed\",\n    \"/delta-utility.html\": \"/delta-utility\",\n    \"/delta-constraints.html\": \"/delta-constraints\",\n    \"/versioning.html\": \"/versioning\",\n    \"/delta-default-columns.html\": \"/delta-default-columns\",\n    \"/delta-column-mapping.html\": \"/delta-column-mapping\",\n    \"/delta-clustering.html\": \"/delta-clustering\",\n    \"/delta-deletion-vectors.html\": \"/delta-deletion-vectors\",\n    \"/delta-drop-feature.html\": \"/delta-drop-feature\",\n    \"/delta-row-tracking.html\": \"/delta-row-tracking\",\n    \"/delta-spark-connect.html\": \"/delta-spark-connect\",\n    \"/delta-storage.html\": \"/delta-storage\",\n    \"/delta-type-widening.html\": \"/delta-type-widening\",\n    \"/delta-uniform.html\": \"/delta-uniform\",\n    \"/delta-sharing.html\": \"/delta-sharing\",\n    \"/concurrency-control.html\": \"/concurrency-control\",\n    \"/porting.html\": \"/porting\",\n    \"/best-practices.html\": \"/best-practices\",\n    \"/delta-faq.html\": \"/delta-faq\",\n    \"/optimizations-oss.html\": \"/optimizations-oss\",\n    \"/delta-trino-integration.html\": \"/delta-trino-integration\",\n    \"/delta-presto-integration.html\": \"/delta-presto-integration\",\n    \"/presto-integration.html\": \"/presto-integration\",\n    \"/redshift-spectrum-integration.html\": \"/redshift-spectrum-integration\",\n    \"/snowflake-integration.html\": \"/snowflake-integration\",\n    \"/bigquery-integration.html\": \"/bigquery-integration\",\n    \"/flink-integration.html\": \"/flink-integration\",\n    \"/delta-more-connectors.html\": \"/delta-more-connectors\",\n    \"/delta-kernel.html\": \"/delta-kernel\",\n    \"/delta-standalone.html\": \"/delta-standalone\",\n    \"/delta-apidoc.html\": \"/delta-apidoc\",\n    \"/releases.html\": \"/releases\",\n    \"/delta-resources.html\": \"/delta-resources\",\n    \"/table-properties.html\": \"/table-properties\",\n  },\n  integrations: [\n    starlight({\n      customCss: [\"./src/styles/custom.css\"],\n      title: \"Delta Lake\",\n      social: [\n        {\n          icon: \"github\",\n          label: \"GitHub\",\n          href: \"https://github.com/delta-io/delta\",\n        },\n      ],\n      editLink: {\n        baseUrl:\n          \"https://github.com/jakebellacera/db-site-staging/tree/main/sites/delta-docs\",\n      },\n      lastUpdated: true,\n      favicon: \"/favicon.svg\",\n      logo: {\n        light: \"./src/assets/delta-lake-logo-light.svg\",\n        dark: \"./src/assets/delta-lake-logo-dark.svg\",\n        replacesTitle: true,\n      },\n      sidebar: [\n        { label: \"Introduction\", link: \"/\" },\n        {\n          label: \"Apache Spark connector\",\n          collapsed: true,\n          items: [\n            {\n              slug: \"quick-start\",\n            },\n            {\n              slug: \"delta-batch\",\n            },\n            {\n              slug: \"delta-streaming\",\n            },\n            {\n              slug: \"delta-update\",\n            },\n            {\n              slug: \"delta-change-data-feed\",\n            },\n            {\n              slug: \"delta-utility\",\n            },\n            {\n              slug: \"delta-constraints\",\n            },\n            {\n              slug: \"versioning\",\n            },\n            {\n              slug: \"delta-default-columns\",\n            },\n            {\n              slug: \"delta-column-mapping\",\n            },\n            {\n              slug: \"delta-clustering\",\n            },\n            {\n              slug: \"delta-deletion-vectors\",\n            },\n            {\n              slug: \"delta-catalog-managed-tables\",\n            },\n            {\n              slug: \"delta-drop-feature\",\n            },\n            {\n              slug: \"delta-row-tracking\",\n            },\n            {\n              slug: \"delta-spark-connect\",\n            },\n            {\n              slug: \"delta-storage\",\n            },\n            {\n              slug: \"delta-type-widening\",\n            },\n            {\n              slug: \"delta-uniform\",\n            },\n            {\n              slug: \"delta-sharing\",\n            },\n            {\n              slug: \"concurrency-control\",\n            },\n            {\n              slug: \"porting\",\n            },\n            {\n              slug: \"best-practices\",\n            },\n            {\n              slug: \"delta-faq\",\n            },\n            {\n              slug: \"optimizations-oss\",\n            },\n          ],\n        },\n        {\n          slug: \"delta-trino-integration\",\n        },\n        {\n          slug: \"delta-starburst-integration\",\n        },\n        {\n          slug: \"delta-presto-integration\",\n        },\n        {\n          slug: \"redshift-spectrum-integration\",\n        },\n        {\n          slug: \"snowflake-integration\",\n        },\n        {\n          slug: \"bigquery-integration\",\n        },\n        {\n          slug: \"flink-integration\",\n        },\n        {\n          slug: \"delta-more-connectors\",\n        },\n        {\n          slug: \"delta-kernel\",\n        },\n        {\n          slug: \"delta-standalone\",\n        },\n        {\n          slug: \"delta-apidoc\",\n        },\n        {\n          slug: \"releases\",\n        },\n        {\n          slug: \"delta-resources\",\n        },\n        {\n          slug: \"table-properties\",\n        },\n        {\n          label: \"Contribute\",\n          link: \"https://github.com/delta-io/delta/blob/master/CONTRIBUTING.md\",\n          attrs: { target: \"_blank\" },\n        },\n      ],\n      head: [\n        {\n          tag: \"script\",\n          attrs: {\n            async: true,\n            src: \"https://www.googletagmanager.com/gtag/js?id=UA-138952006-1\",\n          },\n        },\n        {\n          tag: \"script\",\n          content: `\n          window.dataLayer = window.dataLayer || [];\n          function gtag(){dataLayer.push(arguments);}\n          gtag('js', new Date());\n\n          gtag('config', 'UA-138952006-1');\n          `,\n        },\n      ],\n    }),\n  ],\n});\n"
  },
  {
    "path": "docs/environment.yml",
    "content": "name: delta_docs\nchannels:\n  - defaults\ndependencies:\n  - ca-certificates=2023.08.22\n  - libcxx=14.0.6\n  - libffi=3.4.4\n  - ncurses=6.4\n  - openssl=3.0.11\n  - pip=23.2.1\n  - python=3.8.18\n  - readline=8.2\n  - setuptools=68.0.0\n  - sqlite=3.41.2\n  - tk=8.6.12\n  - wheel=0.41.2\n  - xz=5.4.2\n  - zlib=1.2.13\n  - pip:\n      - alabaster==0.7.13\n      - babel==2.13.0\n      - certifi==2023.7.22\n      - charset-normalizer==3.3.0\n      - colorama==0.4.6\n      - delta-spark==3.0.0\n      - docutils==0.15.2\n      - idna==3.4\n      - imagesize==1.4.1\n      - importlib-metadata==7.0.0\n      - jinja2==2.11.3\n      - livereload==2.6.3\n      - markupsafe==2.0.0\n      - packaging==23.2\n      - py4j==0.10.9.7\n      - pygments==2.16.1\n      - pyspark==3.5.3\n      - pytz==2023.3.post1\n      - requests==2.31.0\n      - six==1.16.0\n      - snowballstemmer==2.2.0\n      - sphinx==2.0.1\n      - sphinx-autobuild==2021.3.14\n      - sphinxcontrib-applehelp==1.0.4\n      - sphinxcontrib-devhelp==1.0.2\n      - sphinxcontrib-htmlhelp==2.0.1\n      - sphinxcontrib-jsmath==1.0.1\n      - sphinxcontrib-qthelp==1.0.3\n      - sphinxcontrib-serializinghtml==1.1.5\n      - tornado==6.3.3\n      - urllib3==2.0.6\n      - zipp==3.17.0\nprefix: <home>/anaconda3/envs/delta_docs\n"
  },
  {
    "path": "docs/eslint.config.mjs",
    "content": "import globals from \"globals\";\nimport pluginJs from \"@eslint/js\";\nimport eslintPluginPrettierRecommended from \"eslint-plugin-prettier/recommended\";\nimport tseslint from \"typescript-eslint\";\nimport eslintPluginAstro from \"eslint-plugin-astro\";\n\nexport default [\n  {\n    ignores: [\n      \"apis\",\n      \"coverage\",\n      \"**/public\",\n      \"**/dist\",\n      \"**/.astro\",\n      \"pnpm-lock.yaml\",\n      \"pnpm-workspace.yaml\",\n    ],\n  },\n  { files: [\"**/*.{js,mjs,cjs,ts}\"] },\n  { languageOptions: { globals: globals.browser } },\n  pluginJs.configs.recommended,\n  eslintPluginPrettierRecommended,\n  ...tseslint.configs.recommended,\n  ...eslintPluginAstro.configs.recommended,\n];\n"
  },
  {
    "path": "docs/generate_docs.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport os\nimport subprocess\nimport random\nimport shutil\nimport string\nimport tempfile\n\n\ndef main():\n    \"\"\"Script to manage the deployment of Delta Lake docs to the hosting bucket.\n       To build the docs:\n       $ generate_docs --livehtml\n    \n    \"\"\"\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--livehtml\",\n        action=\"store_true\",\n        help=\"Build and serve a local build of docs\")\n    parser.add_argument(\n        \"--api-docs\",\n        action=\"store_true\",\n        help=\"Generate the API docs\")\n\n    args = parser.parse_args()\n    \n    docs_root_dir = os.path.dirname(os.path.realpath(__file__))\n    api_docs_root_dir = os.path.join(docs_root_dir, \"apis\")\n\n    with WorkingDirectory(docs_root_dir):\n        api_html_output = os.path.join(docs_root_dir, 'public', 'api', 'latest')\n\n        print(\"Building content\")\n\n        build_docs_cmd = \"pnpm run build\"\n        if args.livehtml:\n            build_docs_cmd = \"pnpm dev\"\n        if args.api_docs:\n            # Assert that env var _DELTA_LAKE_RELEASE_VERSION_ (used by conf.py) is set\n            try:\n                os.environ[\"_DELTA_LAKE_RELEASE_VERSION_\"]\n            except KeyError:\n                raise KeyError(f\"Environment variable _DELTA_LAKE_RELEASE_VERSION_ not set.\")\n            generate_and_copy_api_docs(api_docs_root_dir, api_html_output)\n        run_cmd(build_docs_cmd, shell=True, stream_output=True)\n\n\ndef generate_and_copy_api_docs(api_docs_root_dir, target_loc):\n    print(\"Building API docs\")\n\n    with WorkingDirectory(target_loc):\n        script_path = os.path.join(api_docs_root_dir, \"generate_api_docs.py\")\n        api_docs_dir = os.path.join(api_docs_root_dir,  \"_site\", \"api\")\n        run_cmd([\"python3\", script_path], stream_output=True)\n        assert os.path.exists(api_docs_dir), \\\n            \"Doc generation didn't create the expected api directory\"\n        api_docs_dest_dir = target_loc\n        shutil.copytree(api_docs_dir, api_docs_dest_dir)\n\n\nclass WorkingDirectory(object):\n    def __init__(self, working_directory):\n        self.working_directory = working_directory\n        self.old_workdir = os.getcwd()\n\n    def __enter__(self):\n        os.chdir(self.working_directory)\n\n    def __exit__(self, type, value, traceback):\n        os.chdir(self.old_workdir)\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, **kwargs):\n    \"\"\"Runs a command as a child process.\n\n    A convenience wrapper for running a command from a Python script.\n    Keyword arguments:\n    cmd -- the command to run, as a list of strings\n    throw_on_error -- if true, raises an Exception if the exit code of the program is nonzero\n    env -- additional environment variables to be defined when running the child process\n    stream_output -- if true, does not capture standard output and error; if false, captures these\n      streams and returns them\n\n    Note on the return value: If stream_output is true, then only the exit code is returned. If\n    stream_output is false, then a tuple of the exit code, standard output and standard error is\n    returned.\n    \"\"\"\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % exit_code)\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return exit_code, stdout.decode(\"utf-8\"), stderr.decode(\"utf-8\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"delta-docs\",\n  \"private\": true,\n  \"description\": \"The official documentation for delta.io\",\n  \"main\": \"index.js\",\n  \"engines\": {\n    \"node\": \">=22.18.0\"\n  },\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"format\": \"prettier --write .\",\n    \"dev\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"keywords\": [],\n  \"author\": \"delta-io\",\n  \"license\": \"Apache-2.0\",\n  \"packageManager\": \"pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b\",\n  \"dependencies\": {\n    \"@astrojs/check\": \"^0.9.5\",\n    \"@astrojs/netlify\": \"^6.6.1\",\n    \"@astrojs/starlight\": \"^0.36.2\",\n    \"astro\": \"^5.15.9\",\n    \"sharp\": \"^0.34.5\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.39.1\",\n    \"@typescript-eslint/parser\": \"^8.47.0\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-astro\": \"^1.5.0\",\n    \"eslint-plugin-prettier\": \"^5.5.4\",\n    \"globals\": \"^16.5.0\",\n    \"prettier\": \"^3.6.2\",\n    \"prettier-plugin-astro\": \"^0.14.1\",\n    \"typescript-eslint\": \"^8.47.0\"\n  }\n}\n"
  },
  {
    "path": "docs/scripts/download-api-docs",
    "content": "#!/bin/bash\n\n# GitHub repository details\nGITHUB_OWNER=\"delta-incubator\"\nGITHUB_REPO=\"delta-docs\"\n\n# Check if GitHub token is available\nif [ -z \"$GITHUB_TOKEN\" ]; then\n    echo \"Error: GITHUB_TOKEN environment variable is required for downloading artifacts\"\n    echo \"Please set GITHUB_TOKEN with a personal access token that has scope: public_repo, actions:read\"\n    exit 1\nfi\n\n# Check if an argument was provided\nif [ -z \"$1\" ]; then\n    echo \"Error: No version argument provided\"\n    echo \"Usage: $0 <version>\"\n    exit 1\nfi\n\n# Get the first argument (e.g., \"latest\")\narg=\"$1\"\n\n# Concatenate with the environment variable prefix\nenv_var_name=\"npm_package_config_apidocs_${arg}\"\n\n# Get the value of the dynamically constructed environment variable\napidocs_version=\"${!env_var_name}\"\n\n# Check if the environment variable exists and has a value\nif [ -z \"$apidocs_version\" ]; then\n    echo \"Error: No configuration found for version '$arg'\"\n    echo \"Environment variable '$env_var_name' is not set or empty\"\n    echo \"Supported versions should be configured in package.json\"\n    echo \"Available versions: latest, v3, v2\"\n    exit 1\nfi\n\necho \"Resolved version '$arg' to '$apidocs_version'\"\n\n# GitHub API base URL\napi_base_url=\"https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}\"\n\n# Create target directory path\ntarget_dir=\"public/api/${apidocs_version}\"\nzip_file=\"${apidocs_version}.zip\"\n\n# Create public/api directory if it doesn't exist\nmkdir -p public/api\n\n# Remove existing target directory if it exists (for idempotence)\nif [ -d \"$target_dir\" ]; then\n    echo \"Removing existing directory: $target_dir\"\n    rm -rf \"$target_dir\"\nfi\n\n# First, get the list of artifacts to find the one matching our version\necho \"Searching for artifact with name containing '$apidocs_version'...\"\nartifacts_response=$(curl -s -H \"Authorization: token $GITHUB_TOKEN\" \\\n    -H \"Accept: application/vnd.github.v3+json\" \\\n    \"$api_base_url/actions/artifacts\")\n\nif [ $? -ne 0 ]; then\n    echo \"Error: Failed to fetch artifacts list from GitHub API\"\n    exit 1\nfi\n\n# Extract artifact ID for the matching version (assumes artifact name contains the version)\nartifact_id=$(echo \"$artifacts_response\" | grep -A 10 -B 10 \"\\\"name\\\".*$apidocs_version\" | grep '\"id\"' | head -1 | sed 's/.*\"id\": *\\([0-9]*\\).*/\\1/')\n\nif [ -z \"$artifact_id\" ]; then\n    echo \"Error: No artifact found with name containing '$apidocs_version'\"\n    echo \"Available artifacts:\"\n    echo \"$artifacts_response\" | grep '\"name\"' | sed 's/.*\"name\": *\"\\([^\"]*\\)\".*/  - \\1/'\n    exit 1\nfi\n\necho \"Found artifact ID: $artifact_id\"\n\n# Download the artifact\necho \"Downloading artifact $artifact_id...\"\ndownload_url=\"$api_base_url/actions/artifacts/$artifact_id/zip\"\necho \"Download URL: $download_url\"\n\nif ! curl -L -H \"Authorization: token $GITHUB_TOKEN\" \\\n    -H \"Accept: application/vnd.github.v3+json\" \\\n    -o \"$zip_file\" \"$download_url\"; then\n    echo \"Error: Failed to download artifact from $download_url\"\n    echo \"Please check if the artifact exists and your token has proper permissions\"\n    exit 1\nfi\n\n# Verify the downloaded file exists and is not empty\nif [ ! -s \"$zip_file\" ]; then\n    echo \"Error: Downloaded file is empty or does not exist\"\n    rm -f \"$zip_file\"\n    exit 1\nfi\n\n# Create target directory\nmkdir -p \"$target_dir\"\n\n# Extract the archive\necho \"Extracting to $target_dir...\"\nif ! unzip -o \"$zip_file\" -d \"$target_dir\"; then\n    echo \"Error: Failed to extract $zip_file\"\n    rm -f \"$zip_file\"\n    rm -rf \"$target_dir\"\n    exit 1\nfi\n\n# Clean up the zip file\nrm -f \"$zip_file\"\n\necho \"Successfully extracted API docs for version '$apidocs_version' to '$target_dir'\"\necho \"Done!\""
  },
  {
    "path": "docs/scripts/upgrade-dependencies",
    "content": "#!/bin/bash\n\n# Function to prompt for user confirmation\nconfirm() {\n    while true; do\n        read -p \"Confirm that you want to upgrade packages? (y/n): \" yn\n        case $yn in\n            [Yy]* ) return 0;;\n            [Nn]* ) echo \"Upgrade cancelled.\"; exit 1;;\n            * ) echo \"Please answer yes or no.\";;\n        esac\n    done\n}\n\n# Check for outdated dependencies\necho \"Checking for outdated dependencies...\"\nif pnpm outdated --recursive; then\n    echo \"✅ All packages are up to date! No upgrades needed.\"\n    exit 0\nfi\n\n# Ask for confirmation before proceeding\necho \"⚠️ This script will upgrade Astro and all other dependencies to their latest versions.\"\nconfirm\necho \"Starting upgrade process...\"\n\n# Upgrade astro first\npnpm dlx @astrojs/upgrade\n\n# Upgrade remaining packages\npnpm upgrade --latest\n\necho \"✅ Upgrade process completed! Please verify that things work as expected.\""
  },
  {
    "path": "docs/src/content/docs/best-practices.mdx",
    "content": "---\ntitle: Best practices\ndescription: Learn best practices when using Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n## Choose the right partition column\n\nYou can partition a Delta table by a column. The most commonly used partition column is `date`. Follow these two rules of thumb for deciding on what column to partition by:\n\n- If the cardinality of a column will be very high, do not use that column for partitioning. For example, if you partition by a column `userId` and if there can be 1M distinct user IDs, then that is a bad partitioning strategy.\n- Amount of data in each partition: You can partition by a column if you expect data in that partition to be at least 1 GB.\n\n## Compact files\n\nIf you continuously write data to a Delta table, it will over time accumulate a large number of files, especially if you add data in small batches. This can have an adverse effect on the efficiency of table reads, and it can also affect the performance of your file system. Ideally, a large number of small files should be rewritten into a smaller number of larger files on a regular basis. This is known as compaction.\n\nYou can compact a table by repartitioning it to smaller number of files. In addition, you can specify the option `dataChange` to be `false` indicates that the operation does not change the data, only rearranges the data layout. This would ensure that other concurrent operations are minimally affected due to this compaction operation.\n\nFor example, you can compact a table into 16 files:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Scala\">\n    ```scala\n    val path = \"...\"\n    val numFiles = 16\n\n    spark.read\n     .format(\"delta\")\n     .load(path)\n     .repartition(numFiles)\n     .write\n     .option(\"dataChange\", \"false\")\n     .format(\"delta\")\n     .mode(\"overwrite\")\n     .save(path)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    path = \"...\"\n    numFiles = 16\n\n    (spark.read\n     .format(\"delta\")\n     .load(path)\n     .repartition(numFiles)\n     .write\n     .option(\"dataChange\", \"false\")\n     .format(\"delta\")\n     .mode(\"overwrite\")\n     .save(path))\n    ```\n\n  </TabItem>\n</Tabs>\n\nIf your table is partitioned and you want to repartition just one partition based on a predicate, you can read only the partition using `where` and write back to that using `replaceWhere`:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Scala\">\n    ```scala\n    val path = \"...\"\n    val partition = \"year = '2019'\"\n    val numFilesPerPartition = 16\n\n    spark.read\n     .format(\"delta\")\n     .load(path)\n     .where(partition)\n     .repartition(numFilesPerPartition)\n     .write\n     .option(\"dataChange\", \"false\")\n     .format(\"delta\")\n     .mode(\"overwrite\")\n     .option(\"replaceWhere\", partition)\n     .save(path)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    path = \"...\"\n    partition = \"year = '2019'\"\n    numFilesPerPartition = 16\n\n    (spark.read\n     .format(\"delta\")\n     .load(path)\n     .where(partition)\n     .repartition(numFilesPerPartition)\n     .write\n     .option(\"dataChange\", \"false\")\n     .format(\"delta\")\n     .mode(\"overwrite\")\n     .option(\"replaceWhere\", partition)\n     .save(path))\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"caution\">\n  Using `dataChange = false` on an operation that changes data can corrupt the\n  data in the table.\n</Aside>\n\n<Aside type=\"note\">\n  This operation does not remove the old files. To remove them, run the\n  [VACUUM](/delta-utility/#remove-files-no-longer-referenced-by-a-delta-table)\n  command.\n</Aside>\n\n## Replace the content or schema of a table\n\nSometimes you may want to replace a Delta table. For example:\n\n- You discover the data in the table is incorrect and want to replace the content.\n- You want to rewrite the whole table to do incompatible schema changes (such as changing column types).\n\nWhile you can delete the entire directory of a Delta table and create a new table on the same path, it's _not recommended_ because:\n\n- Deleting a directory is not efficient. A directory containing very large files can take hours or even days to delete.\n- You lose all of content in the deleted files; it's hard to recover if you delete the wrong table.\n- The directory deletion is not atomic. While you are deleting the table a concurrent query reading the table can fail or see a partial table.\n\nIf you don't need to change the table schema, you can [delete](/delta-update/#delete-from-a-table) data from a Delta table and insert your new data, or [update](/delta-update/#update-a-table) the table to fix the incorrect values.\n\nIf you want to change the table schema, you can replace the whole table atomically. For example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n    ```python\n    dataframe.write \\\n      .format(\"delta\") \\\n      .mode(\"overwrite\") \\\n      .option(\"overwriteSchema\", \"true\") \\\n      .partitionBy(<your-partition-columns>) \\\n      .saveAsTable(\"<your-table>\") # Managed table\n    dataframe.write \\\n      .format(\"delta\") \\\n      .mode(\"overwrite\") \\\n      .option(\"overwriteSchema\", \"true\") \\\n      .option(\"path\", \"<your-table-path>\") \\\n      .partitionBy(<your-partition-columns>) \\\n      .saveAsTable(\"<your-table>\") # External table\n    ```\n  </TabItem>\n  <TabItem label=\"SQL\">\n    ```sql\n    REPLACE TABLE <your-table> USING DELTA PARTITIONED BY (<your-partition-columns>) AS SELECT ... -- Managed table\n    REPLACE TABLE <your-table> USING DELTA PARTITIONED BY (<your-partition-columns>) LOCATION \"<your-table-path>\" AS SELECT ... -- External table\n    ```\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    dataframe.write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"overwriteSchema\", \"true\")\n      .partitionBy(<your-partition-columns>)\n      .saveAsTable(\"<your-table>\") // Managed table\n    dataframe.write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"overwriteSchema\", \"true\")\n      .option(\"path\", \"<your-table-path>\")\n      .partitionBy(<your-partition-columns>)\n      .saveAsTable(\"<your-table>\") // External table\n    ```\n  </TabItem>\n</Tabs>\n\nThere are multiple benefits with this approach:\n\n- Overwriting a table is much faster because it doesn't need to list the directory recursively or delete any files.\n- The old version of the table still exists. If you delete the wrong table you can easily retrieve the old data using [Time Travel](/delta-batch/#query-an-older-snapshot-of-a-table-time-travel).\n- It's an atomic operation. Concurrent queries can still read the table while you are deleting the table.\n- Because of Delta Lake ACID transaction guarantees, if overwriting the table fails, the table will be in its previous state.\n\nIn addition, if you want to delete old files to save storage cost after overwriting the table, you can use [VACUUM](/delta-utility/#remove-files-no-longer-referenced-by-a-delta-table) to delete them. It's optimized for file deletion and usually faster than deleting the entire directory.\n\n## Spark caching\n\nYou should not use Spark caching for the following reasons:\n\n- You lose any data skipping that can come from additional filters added on top of the cached `DataFrame`.\n- The data that gets cached may not be updated if the table is accessed using a different identifier (for example, you do `spark.table(x).cache()` but then write to the table using `spark.write.save(/some/path)`.\n"
  },
  {
    "path": "docs/src/content/docs/bigquery-integration.mdx",
    "content": "---\ntitle: Google BigQuery connector\ndescription: Learn how to read Delta Lake tables from Google BigQuery.\n---\n\nGoogle BigQuery supports reading Delta Lake (reader version 3 with [Deletion Vectors](/delta-deletion-vectors) and [Column Mapping](/delta-column-mapping/)). Please refer to [Delta Lake BigLake tables documentation](https://cloud.google.com/bigquery/docs/create-delta-lake-table) for more details.\n"
  },
  {
    "path": "docs/src/content/docs/concurrency-control.mdx",
    "content": "---\ntitle: Concurrency control\ndescription: Learn about the ACID transaction guarantees between reads and writes provided by Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\nDelta Lake provides ACID transaction guarantees between reads and writes. This means that:\n\n- For supported [storage systems](/delta-storage), multiple writers across multiple clusters can simultaneously modify a table partition and see a consistent snapshot view of the table and there will be a serial order for these writes.\n- Readers continue to see a consistent snapshot view of the table that the Apache Spark job started with, even when a table is modified during a job.\n\n## Optimistic concurrency control\n\nDelta Lake uses [optimistic concurrency control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) to provide transactional guarantees between writes. Under this mechanism, writes operate in three stages:\n\n1. **Read**: Reads (if needed) the latest available version of the table to identify which files need to be modified (that is, rewritten).\n2. **Write**: Stages all the changes by writing new data files.\n3. **Validate and commit**: Before committing the changes, checks whether the proposed changes conflict with any other changes that may have been concurrently committed since the snapshot that was read. If there are no conflicts, all the staged changes are committed as a new versioned snapshot, and the write operation succeeds. However, if there are conflicts, the write operation fails with a concurrent modification exception rather than corrupting the table as would happen with the write operation on a Parquet table.\n\n## Write conflicts\n\nThe following table describes which pairs of write operations can conflict. Compaction refers to [file compaction operation](/best-practices/#compact-files) written with the option `dataChange` set to `false`.\n\n|  | INSERT | UPDATE, DELETE, MERGE INTO | COMPACTION |\n| --- | --- | --- | --- |\n| **INSERT** | Cannot conflict |  |  |\n| **UPDATE, DELETE, MERGE INTO** | Can conflict | Can conflict |  |\n| **COMPACTION** | Cannot conflict | Can conflict | Can conflict |\n\n## Avoid conflicts using partitioning and disjoint command conditions\n\nIn all cases marked \"can conflict\", whether the two operations will conflict depends on whether they operate on the same set of files. You can make the two sets of files disjoint by partitioning the table by the same columns as those used in the conditions of the operations. For example, the two commands `UPDATE table WHERE date > '2010-01-01' ...` and `DELETE table WHERE date < '2010-01-01'` will conflict if the table is not partitioned by date, as both can attempt to modify the same set of files. Partitioning the table by `date` will avoid the conflict. Hence, partitioning a table according to the conditions commonly used on the command can reduce conflicts significantly. However, partitioning a table by a column that has high cardinality can lead to other performance issues due to large number of subdirectories.\n\n## Conflict exceptions\n\nWhen a transaction conflict occurs, you will observe one of the following exceptions:\n\n### ConcurrentAppendException\n\nThis exception occurs when a concurrent operation adds files in the same partition (or anywhere in an unpartitioned table) that your operation reads. The file additions can be caused by `INSERT`, `DELETE`, `UPDATE`, or `MERGE` operations.\n\nThis exception is often thrown during concurrent `DELETE`, `UPDATE`, or `MERGE` operations. While the concurrent operations may be physically updating different partition directories, one of them may read the same partition that the other one concurrently updates, thus causing a conflict. You can avoid this by making the separation explicit in the operation condition. Consider the following example.\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    // Target 'deltaTable' is partitioned by date and country\n    deltaTable.as(\"t\")\n      .merge(\n        source.as(\"s\"),\n        \"s.user_id = t.user_id AND s.date = t.date AND s.country = t.country\"\n      )\n      .whenMatched()\n      .updateAll()\n      .whenNotMatched()\n      .insertAll()\n      .execute()\n    ```\n  </TabItem>\n</Tabs>\n\nSuppose you run the above code concurrently for different dates or countries. Since each job is working on an independent partition on the target Delta table, you don't expect any conflicts. However, the condition is not explicit enough and can scan the entire table and can conflict with concurrent operations updating any other partitions. Instead, you can rewrite your statement to add specific date and country to the merge condition, as shown in the following example.\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    // Target 'deltaTable' is partitioned by date and country\n    deltaTable.as(\"t\")\n      .merge(\n        source.as(\"s\"),\n        \"s.user_id = t.user_id AND s.date = t.date AND s.country = t.country AND t.date = '\" + <date> + \"' AND t.country = '\" + <country> + \"'\"\n      )\n      .whenMatched()\n      .updateAll()\n      .whenNotMatched()\n      .insertAll()\n      .execute()\n    ```\n  </TabItem>\n</Tabs>\n\nThis operation is now safe to run concurrently on different dates and countries.\n\n### ConcurrentDeleteReadException\n\nThis exception occurs when a concurrent operation deleted a file that your operation read. Common causes are a `DELETE`, `UPDATE`, or `MERGE` operation that rewrites files.\n\n### ConcurrentDeleteDeleteException\n\nThis exception occurs when a concurrent operation deleted a file that your operation also deletes. This could be caused by two concurrent compaction operations rewriting the same files.\n\n### MetadataChangedException\n\nThis exception occurs when a concurrent transaction updates the metadata of a Delta table. Common causes are `ALTER TABLE` operations or writes to your Delta table that update the schema of the table.\n\n### ConcurrentTransactionException\n\nIf a streaming query using the same checkpoint location is started multiple times concurrently and tries to write to the Delta table at the same time. You should never have two streaming queries use the same checkpoint location and run at the same time.\n\n### ProtocolChangedException\n\nThis exception can occur in the following cases:\n\n- When your Delta table is upgraded to a new version. For future operations to succeed you may need to upgrade your Delta Lake version.\n- When multiple writers are creating or replacing a table at the same time.\n- When multiple writers are writing to an empty path at the same time.\n"
  },
  {
    "path": "docs/src/content/docs/delta-apidoc.mdx",
    "content": "---\ntitle: Delta Lake APIs\ndescription: Learn about the APIs provided by Delta Lake.\n---\n\nimport { Aside } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  Some Delta Lake APIs are still evolving and are indicated with the\n  **Evolving** qualifier or annotation in the API docs.\n</Aside>\n\n## Delta Spark\n\nDelta Spark is a library for reading and writing Delta tables using Apache Spark™. For most read and write operations on Delta tables, you can use Apache Spark reader and writer APIs. For examples, see [Table batch reads and writes](/delta-batch/) and [Table streaming reads and writes](/delta-streaming/).\n\nHowever, there are some operations that are specific to Delta Lake and you must use Delta Lake APIs. For examples, see [Table utility commands](/delta-utility/).\n\n- [Scala API docs](/api/latest/scala/spark/io/delta/tables/index.html)\n- [Java API docs](/api/latest/java/spark/index.html)\n- [Python API docs](/api/latest/python/spark/index.html)\n\n## Delta Kernel\n\nDelta Kernel is a library for operating on Delta tables. Specifically, it provides simple and narrow APIs for reading and writing to Delta tables without the need to understand the [Delta protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md) details. You can use this library to do the following:\n\n- Read Delta tables from your applications.\n- Build a connector for a distributed engine like Apache Spark™, Apache Flink, or Trino for reading massive Delta tables.\n\nMore details refer [here](https://github.com/delta-io/delta/blob/branch-3.0/kernel/USER_GUIDE.md).\n\n- [Java API docs](/api/latest/java/kernel/index.html)\n\n## Delta Rust\n\nThis [library](https://docs.rs/deltalake/latest/deltalake/) allows Rust (with Python bindings) low level access to Delta tables and is intended to be used with data processing frameworks like `datafusion`, `ballista`, `rust-dataframe`, `vega`, etc.\n\n## Delta Standalone\n\n<Aside type=\"caution\">\n  The Delta Standalone is deprecated in favor of [Delta Kernel](/delta-kernel/)\n  which has support for reading from or writing into Delta tables with advanced\n  features.\n</Aside>\n\nDelta Standalone, formerly known as the Delta Standalone Reader (DSR), is a JVM library to read and write Delta tables. Unlike Delta-Spark, this library doesn't use Spark to read or write tables and it has only a few transitive dependencies. It can be used by any application that cannot use a Spark cluster. More details refer [here](https://github.com/delta-io/delta/blob/master/connectors/README.md).\n\n- [Java API docs](/api/3.3.2/java/standalone/index.html)\n\n## Delta Flink\n\nFlink/Delta Connector is a JVM library to read and write data from Apache Flink applications to Delta tables utilizing the Delta Standalone JVM library. More details refer [here](https://github.com/delta-io/delta/blob/master/connectors/flink/README.md).\n\n- [Java API docs](/api/3.3.2/java/flink/index.html)\n"
  },
  {
    "path": "docs/src/content/docs/delta-athena-integration.mdx",
    "content": "---\ntitle: AWS Athena Delta Connector\ndescription: Learn how to set up an integration to enable you to read Delta tables from AWS Athena.\n---\n\n# AWS Athena Delta Connector\n\nSince Athena [version 3](https://docs.aws.amazon.com/athena/latest/ug/engine-versions-reference-0003.html), Athena natively supports reading Delta Lake tables. For details on using the native Delta Lake connector, see [Querying Delta Lake tables](https://docs.aws.amazon.com/athena/latest/ug/delta-lake-tables.html). For Athena versions lower than [version 3](https://docs.aws.amazon.com/athena/latest/ug/engine-versions-reference-0003.html), you can use the manifest-based approach detailed in [Presto, Trino, and Athena to Delta Lake integration using manifests](/presto-integration).\n"
  },
  {
    "path": "docs/src/content/docs/delta-batch.mdx",
    "content": "---\ntitle: Table batch reads and writes\ndescription: Learn how to perform batch reads and writes on Delta tables.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nDelta Lake supports most of the options provided by Apache Spark DataFrame read and write APIs for performing batch reads and writes on tables.\n\nFor many Delta Lake operations on tables, you enable integration with Apache Spark DataSourceV2 and Catalog APIs (since 3.0) by setting configurations when you create a new `SparkSession`. See [Configure SparkSession](#configure-sparksession).\n\n## Create a table\n\nDelta Lake supports creating two types of tables—tables defined in the metastore and tables defined by path.\n\nTo work with metastore-defined tables, you must enable integration with Apache Spark DataSourceV2 and Catalog APIs by setting configurations when you create a new `SparkSession`. See [Configure SparkSession](#configure-sparksession).\n\nYou can create tables in the following ways:\n\n- **SQL DDL commands**: You can use standard SQL DDL commands supported in Apache Spark (for example, `CREATE TABLE` and `REPLACE TABLE`) to create Delta tables.\n\n    <Tabs syncKey=\"code-examples\">\n      <TabItem label=\"SQL\" active>\n\n        ```sql\n        CREATE TABLE IF NOT EXISTS default.people10m (\n          id INT,\n          firstName STRING,\n          middleName STRING,\n          lastName STRING,\n          gender STRING,\n          birthDate TIMESTAMP,\n          ssn STRING,\n          salary INT\n        ) USING DELTA\n\n        CREATE OR REPLACE TABLE default.people10m (\n          id INT,\n          firstName STRING,\n          middleName STRING,\n          lastName STRING,\n          gender STRING,\n          birthDate TIMESTAMP,\n          ssn STRING,\n          salary INT\n        ) USING DELTA\n        ```\n      \n      </TabItem>\n    </Tabs>\n\n    SQL also supports creating a table at a path, without creating an entry in the Hive metastore.\n\n    <Tabs syncKey=\"code-examples\">\n      <TabItem label=\"SQL\" active>\n\n        ```sql\n        -- Create or replace table with path\n        CREATE OR REPLACE TABLE delta.`/tmp/delta/people10m` (\n          id INT,\n          firstName STRING,\n          middleName STRING,\n          lastName STRING,\n          gender STRING,\n          birthDate TIMESTAMP,\n          ssn STRING,\n          salary INT\n        ) USING DELTA\n        ```\n      \n      </TabItem>\n    </Tabs>\n\n- **`DataFrameWriter` API**: If you want to simultaneously create a table and insert data into it from Spark DataFrames or Datasets, you can use the Spark `DataFrameWriter` ([Scala or Java](https://spark.apache.org/docs/latest/api/latest/scala/org/apache/spark/sql/DataFrameWriter.html) and [Python](https://spark.apache.org/docs/latest/api/latest/python/reference/pyspark.sql/io.html)).\n\n    <Tabs syncKey=\"code-examples\">\n      <TabItem label=\"Python\">\n\n        ```python\n        # Create table in the metastore using DataFrame's schema and write data to it\n        df.write.format(\"delta\").saveAsTable(\"default.people10m\")\n\n        # Create or replace partitioned table with path using DataFrame's schema and write/overwrite data to it\n        df.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta/people10m\")\n        ```\n\n      </TabItem>\n      <TabItem label=\"Scala\">\n\n        ```scala\n        // Create table in the metastore using DataFrame's schema and write data to it\n        df.write.format(\"delta\").saveAsTable(\"default.people10m\")\n\n        // Create table with path using DataFrame's schema and write data to it\n        df.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta/people10m\")\n        ```\n\n      </TabItem>\n    </Tabs>\n\n    You can also create Delta tables using the Spark `DataFrameWriterV2` API.\n\n- **`DeltaTableBuilder` API**: You can also use the `DeltaTableBuilder` API in Delta Lake to create tables. Compared to the DataFrameWriter APIs, this API makes it easier to specify additional information like column comments, table properties, and [generated columns](#use-generated-columns).\n\n    <Aside type=\"note\">\n\n      This feature is new and is in Preview.\n\n      <Tabs syncKey=\"code-examples\">\n        <TabItem label=\"Python\">\n          \n          ```python\n          # Create table in the metastore\n          DeltaTable.createIfNotExists(spark) \\\n            .tableName(\"default.people10m\") \\\n            .addColumn(\"id\", \"INT\") \\\n            .addColumn(\"firstName\", \"STRING\") \\\n            .addColumn(\"middleName\", \"STRING\") \\\n            .addColumn(\"lastName\", \"STRING\", comment = \"surname\") \\\n            .addColumn(\"gender\", \"STRING\") \\\n            .addColumn(\"birthDate\", \"TIMESTAMP\") \\\n            .addColumn(\"ssn\", \"STRING\") \\\n            .addColumn(\"salary\", \"INT\") \\\n            .execute()\n\n          # Create or replace table with path and add properties\n          DeltaTable.createOrReplace(spark) \\\n            .addColumn(\"id\", \"INT\") \\\n            .addColumn(\"firstName\", \"STRING\") \\\n            .addColumn(\"middleName\", \"STRING\") \\\n            .addColumn(\"lastName\", \"STRING\", comment = \"surname\") \\\n            .addColumn(\"gender\", \"STRING\") \\\n            .addColumn(\"birthDate\", \"TIMESTAMP\") \\\n            .addColumn(\"ssn\", \"STRING\") \\\n            .addColumn(\"salary\", \"INT\") \\\n            .property(\"description\", \"table with people data\") \\\n            .location(\"/tmp/delta/people10m\") \\\n            .execute()\n          ```\n\n        </TabItem>\n        <TabItem label=\"Scala\">\n\n          ```scala\n          // Create table in the metastore\n          DeltaTable.createOrReplace(spark)\n            .tableName(\"default.people10m\")\n            .addColumn(\"id\", \"INT\")\n            .addColumn(\"firstName\", \"STRING\")\n            .addColumn(\"middleName\", \"STRING\")\n            .addColumn(\n              DeltaTable.columnBuilder(\"lastName\")\n                .dataType(\"STRING\")\n                .comment(\"surname\")\n                .build())\n            .addColumn(\"lastName\", \"STRING\", comment = \"surname\")\n            .addColumn(\"gender\", \"STRING\")\n            .addColumn(\"birthDate\", \"TIMESTAMP\")\n            .addColumn(\"ssn\", \"STRING\")\n            .addColumn(\"salary\", \"INT\")\n            .execute()\n\n          // Create or replace table with path and add properties\n          DeltaTable.createOrReplace(spark)\n            .addColumn(\"id\", \"INT\")\n            .addColumn(\"firstName\", \"STRING\")\n            .addColumn(\"middleName\", \"STRING\")\n            .addColumn(\n              DeltaTable.columnBuilder(\"lastName\")\n                .dataType(\"STRING\")\n                .comment(\"surname\")\n                .build())\n            .addColumn(\"lastName\", \"STRING\", comment = \"surname\")\n            .addColumn(\"gender\", \"STRING\")\n            .addColumn(\"birthDate\", \"TIMESTAMP\")\n            .addColumn(\"ssn\", \"STRING\")\n            .addColumn(\"salary\", \"INT\")\n            .property(\"description\", \"table with people data\")\n            .location(\"/tmp/delta/people10m\")\n            .execute()\n          ```\n\n        </TabItem>\n      </Tabs>\n    </Aside>\n\nSee the [API documentation](/delta-apidoc/) for details.\n\n### Partition data\n\nYou can partition data to speed up queries or DML that have predicates involving the partition columns. To partition data when you create a Delta table, specify a partition by columns. The following example partitions by gender.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    -- Create table in the metastore\n    CREATE TABLE default.people10m (\n      id INT,\n      firstName STRING,\n      middleName STRING,\n      lastName STRING,\n      gender STRING,\n      birthDate TIMESTAMP,\n      ssn STRING,\n      salary INT\n    )\n    USING DELTA\n    PARTITIONED BY (gender)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    df.write.format(\"delta\").partitionBy(\"gender\").saveAsTable(\"default.people10m\")\n\n    DeltaTable.create(spark) \\\n      .tableName(\"default.people10m\") \\\n      .addColumn(\"id\", \"INT\") \\\n      .addColumn(\"firstName\", \"STRING\") \\\n      .addColumn(\"middleName\", \"STRING\") \\\n      .addColumn(\"lastName\", \"STRING\", comment = \"surname\") \\\n      .addColumn(\"gender\", \"STRING\") \\\n      .addColumn(\"birthDate\", \"TIMESTAMP\") \\\n      .addColumn(\"ssn\", \"STRING\") \\\n      .addColumn(\"salary\", \"INT\") \\\n      .partitionedBy(\"gender\") \\\n      .execute()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    df.write.format(\"delta\").partitionBy(\"gender\").saveAsTable(\"default.people10m\")\n\n    DeltaTable.createOrReplace(spark)\n      .tableName(\"default.people10m\")\n      .addColumn(\"id\", \"INT\")\n      .addColumn(\"firstName\", \"STRING\")\n      .addColumn(\"middleName\", \"STRING\")\n      .addColumn(\n        DeltaTable.columnBuilder(\"lastName\")\n          .dataType(\"STRING\")\n          .comment(\"surname\")\n          .build())\n      .addColumn(\"lastName\", \"STRING\", comment = \"surname\")\n      .addColumn(\"gender\", \"STRING\")\n      .addColumn(\"birthDate\", \"TIMESTAMP\")\n      .addColumn(\"ssn\", \"STRING\")\n      .addColumn(\"salary\", \"INT\")\n      .partitionedBy(\"gender\")\n      .execute()\n    ```\n\n  </TabItem>\n</Tabs>\n\nTo determine whether a table contains a specific partition, use the statement `SELECT COUNT(*) > 0 FROM <table-name> WHERE <partition-column> = <value>`. If the partition exists, `true` is returned. For example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    SELECT COUNT(*) > 0 AS `Partition exists` FROM default.people10m WHERE gender = \"M\"\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    display(spark.sql(\"SELECT COUNT(*) > 0 AS `Partition exists` FROM default.people10m WHERE gender = 'M'\"))\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    display(spark.sql(\"SELECT COUNT(*) > 0 AS `Partition exists` FROM default.people10m WHERE gender = 'M'\"))\n    ```\n\n  </TabItem>\n</Tabs>\n\n### Control data location\n\nFor tables defined in the metastore, you can optionally specify the `LOCATION` as a path. Tables created with a specified `LOCATION` are considered unmanaged by the metastore. Unlike a managed table, where no path is specified, an unmanaged table's files are not deleted when you `DROP` the table.\n\nWhen you run `CREATE TABLE` with a `LOCATION` that _already_ contains data stored using Delta Lake, Delta Lake does the following:\n\n- If you specify _only the table name and location_, for example:\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"SQL\" active>\n\n      ```sql\n      CREATE TABLE default.people10m\n      USING DELTA\n      LOCATION '/tmp/delta/people10m'\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n  the table in the metastore automatically inherits the schema, partitioning, and table properties of the existing data. This functionality can be used to \"import\" data into the metastore.\n\n- If you specify _any configuration_ (schema, partitioning, or table properties), Delta Lake verifies that the specification exactly matches the configuration of the existing data.\n\n  <Aside\n    type=\"caution\"\n    title=\"Important\"\n  >\n    If the specified configuration does not _exactly_ match the configuration of\n    the data, Delta Lake throws an exception that describes the discrepancy.\n  </Aside>\n\n<Aside type=\"note\">\n  The metastore is not the source of truth about the latest information of a\n  Delta table. In fact, the table definition in the metastore may not contain\n  all the metadata like schema and properties. It contains the location of the\n  table, and the table's transaction log at the location is the source of truth.\n  If you query the metastore from a system that is not aware of this\n  Delta-specific customization, you may see incomplete or stale table\n  information.\n</Aside>\n\n### Use generated columns\n\n<Aside type=\"note\">This feature is new and is in Preview.</Aside>\n\nDelta Lake supports generated columns which are a special type of columns whose values are automatically generated based on a user-specified function over other columns in the Delta table. When you write to a table with generated columns and you do not explicitly provide values for them, Delta Lake automatically computes the values. For example, you can automatically generate a date column (for partitioning the table by date) from the timestamp column; any writes into the table need only specify the data for the timestamp column. However, if you explicitly provide values for them, the values must satisfy the [constraint](/delta-constraints/) `(<value> <=> <generation expression>) IS TRUE` or the write will fail with an error.\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Tables created with generated columns have a higher table writer protocol\n  version than the default. See [How does Delta Lake manage feature\n  compatibility?](/versioning/) to understand table protocol versioning and what\n  it means to have a higher version of a table protocol version.\n</Aside>\n\nThe following example shows how to create a table with generated columns:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    DeltaTable.create(spark) \\\n      .tableName(\"default.people10m\") \\\n      .addColumn(\"id\", \"INT\") \\\n      .addColumn(\"firstName\", \"STRING\") \\\n      .addColumn(\"middleName\", \"STRING\") \\\n      .addColumn(\"lastName\", \"STRING\", comment = \"surname\") \\\n      .addColumn(\"gender\", \"STRING\") \\\n      .addColumn(\"birthDate\", \"TIMESTAMP\") \\\n      .addColumn(\"dateOfBirth\", DateType(), generatedAlwaysAs=\"CAST(birthDate AS DATE)\") \\\n      .addColumn(\"ssn\", \"STRING\") \\\n      .addColumn(\"salary\", \"INT\") \\\n      .partitionedBy(\"gender\") \\\n      .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    DeltaTable.create(spark)\n      .tableName(\"default.people10m\")\n      .addColumn(\"id\", \"INT\")\n      .addColumn(\"firstName\", \"STRING\")\n      .addColumn(\"middleName\", \"STRING\")\n      .addColumn(\n        DeltaTable.columnBuilder(\"lastName\")\n          .dataType(\"STRING\")\n          .comment(\"surname\")\n          .build())\n      .addColumn(\"lastName\", \"STRING\", comment = \"surname\")\n      .addColumn(\"gender\", \"STRING\")\n      .addColumn(\"birthDate\", \"TIMESTAMP\")\n      .addColumn(\n        DeltaTable.columnBuilder(\"dateOfBirth\")\n        .dataType(DateType)\n        .generatedAlwaysAs(\"CAST(dateOfBirth AS DATE)\")\n        .build())\n      .addColumn(\"ssn\", \"STRING\")\n      .addColumn(\"salary\", \"INT\")\n      .partitionedBy(\"gender\")\n      .execute()\n    ```\n\n  </TabItem>\n</Tabs>\n\nGenerated columns are stored as if they were normal columns. That is, they occupy storage.\n\nThe following restrictions apply to generated columns:\n\n- A generation expression can use any SQL functions in Spark that always return the same result when given the same argument values, except the following types of functions:\n  - User-defined functions.\n  - Aggregate functions.\n  - Window functions.\n  - Functions returning multiple rows.\n- For Delta Lake 1.1.0 and above, `MERGE` operations support generated columns when you set `spark.databricks.delta.schema.autoMerge.enabled` to true.\n\nDelta Lake may be able to generate partition filters for a query whenever a partition column is defined by one of the following expressions:\n\n- `CAST(col AS DATE)` and the type of `col` is `TIMESTAMP`.\n- `YEAR(col)` and the type of `col` is `TIMESTAMP`.\n- Two partition columns defined by `YEAR(col), MONTH(col)` and the type of `col` is `TIMESTAMP`.\n- Three partition columns defined by `YEAR(col), MONTH(col), DAY(col)` and the type of `col` is `TIMESTAMP`.\n- Four partition columns defined by `YEAR(col), MONTH(col), DAY(col), HOUR(col)` and the type of `col` is `TIMESTAMP`.\n- `SUBSTRING(col, pos, len)` and the type of `col` is `STRING`\n- `DATE_FORMAT(col, format)` and the type of `col` is `TIMESTAMP`.\n- `DATE_TRUNC(format, col)` and the type of the `col` is `TIMESTAMP` or `DATE`.\n- `TRUNC(col, format)` and type of the `col` is either `TIMESTAMP` or `DATE`.\n\nIf a partition column is defined by one of the preceding expressions, and a query filters data using the underlying base column of a generation expression, Delta Lake looks at the relationship between the base column and the generated column, and populates partition filters based on the generated partition column if possible. For example, given the following table:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    DeltaTable.create(spark) \\\n      .tableName(\"default.events\") \\\n      .addColumn(\"eventId\", \"BIGINT\") \\\n      .addColumn(\"data\", \"STRING\") \\\n      .addColumn(\"eventType\", \"STRING\") \\\n      .addColumn(\"eventTime\", \"TIMESTAMP\") \\\n      .addColumn(\"eventDate\", \"DATE\", generatedAlwaysAs=\"CAST(eventTime AS DATE)\") \\\n      .partitionedBy(\"eventType\", \"eventDate\") \\\n      .execute()\n    ```\n  \n  </TabItem>\n</Tabs>\n\nIf you then run the following query:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    spark.sql('SELECT * FROM default.events WHERE eventTime >= \"2020-10-01 00:00:00\" <= \"2020-10-01 12:00:00\"')\n    ```\n  \n  </TabItem>\n</Tabs>\n\nDelta Lake automatically generates a partition filter so that the preceding query only reads the data in partition `date=2020-10-01` even if a partition filter is not specified.\n\nAs another example, given the following table:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    DeltaTable.create(spark) \\\n      .tableName(\"default.events\") \\\n      .addColumn(\"eventId\", \"BIGINT\") \\\n      .addColumn(\"data\", \"STRING\") \\\n      .addColumn(\"eventType\", \"STRING\") \\\n      .addColumn(\"eventTime\", \"TIMESTAMP\") \\\n      .addColumn(\"year\", \"INT\", generatedAlwaysAs=\"YEAR(eventTime)\") \\\n      .addColumn(\"month\", \"INT\", generatedAlwaysAs=\"MONTH(eventTime)\") \\\n      .addColumn(\"day\", \"INT\", generatedAlwaysAs=\"DAY(eventTime)\") \\\n      .partitionedBy(\"eventType\", \"year\", \"month\", \"day\") \\\n      .execute()\n    ```\n  \n  </TabItem>\n</Tabs>\n\nIf you then run the following query:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    spark.sql('SELECT * FROM default.events WHERE eventTime >= \"2020-10-01 00:00:00\" <= \"2020-10-01 12:00:00\"')\n    ```\n  \n  </TabItem>\n</Tabs>\n\nDelta Lake automatically generates a partition filter so that the preceding query only reads the data in partition `year=2020/month=10/day=01` even if a partition filter is not specified.\n\nYou can use an [EXPLAIN](https://spark.apache.org/docs/latest/sql-ref-syntax-qry-explain.html) clause and check the provided plan to see whether Delta Lake automatically generates any partition filters.\n\n### Use identity columns\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Declaring an identity column on a Delta table disables concurrent\n  transactions. Only use identity columns in use cases where concurrent writes\n  to the target table are not required.\n</Aside>\n\nDelta Lake identity columns are supported in Delta Lake 3.3 and above. They are a type of generated column that assigns unique values for each record inserted into a table. The following example shows how to declare an identity column during a create table command:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import DeltaTable, IdentityGenerator\n    from pyspark.sql.types import LongType\n\n    DeltaTable.create()\n      .tableName(\"table_name\")\n      .addColumn(\"id_col1\", dataType=LongType(), generatedAlwaysAs=IdentityGenerator())\n      .addColumn(\"id_col2\", dataType=LongType(), generatedAlwaysAs=IdentityGenerator(start=-1, step=1))\n      .addColumn(\"id_col3\", dataType=LongType(), generatedByDefaultAs=IdentityGenerator())\n      .addColumn(\"id_col4\", dataType=LongType(), generatedByDefaultAs=IdentityGenerator(start=-1, step=1))\n      .execute()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables.DeltaTable\n    import org.apache.spark.sql.types.LongType\n\n    DeltaTable.create(spark)\n      .tableName(\"table_name\")\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"id_col1\")\n          .dataType(LongType)\n          .generatedAlwaysAsIdentity().build())\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"id_col2\")\n          .dataType(LongType)\n          .generatedAlwaysAsIdentity(start = -1L, step = 1L).build())\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"id_col3\")\n          .dataType(LongType)\n          .generatedByDefaultAsIdentity().build())\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"id_col4\")\n          .dataType(LongType)\n          .generatedByDefaultAsIdentity(start = -1L, step = 1L).build())\n      .execute()\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  SQL APIs for identity columns are not supported yet.\n</Aside>\n\nYou can optionally specify the following:\n\n- A starting value.\n- A step size, which can be positive or negative.\n\nBoth the starting value and step size default to `1`. You cannot specify a step size of `0`.\n\nValues assigned by identity columns are unique and increment in the direction of the specified step, and in multiples of the specified step size, but are not guaranteed to be contiguous. For example, with a starting value of `0` and a step size of `2`, all values are positive even numbers but some even numbers might be skipped.\n\nWhen the identity column is specified to be `generated by default as identity`, insert operations can specify values for the identity column. Specify it to be `generated always as identity` to override the ability to manually set values.\n\nIdentity columns only support `LongType`, and operations fail if the assigned value exceeds the range supported by `LongType`.\n\nYou can use `ALTER TABLE table_name ALTER COLUMN column_name SYNC IDENTITY` to synchronize the metadata of an identity column with the actual data. When you write your own values to an identity column, it might not comply with the metadata. This option evaluates the state and updates the metadata to be consistent with the actual data. After this command, the next automatically assigned identity value will start from `start + (n + 1) * step`, where `n` is the smallest value that satisfies `start + n * step >= max()` (for a positive step).\n\n#### CTAS and identity columns\n\nYou cannot define schema, identity column constraints, or any other table specifications when using a `CREATE TABLE table_name AS SELECT` (CTAS) statement.\n\nTo create a new table with an identity column and populate it with existing data, do the following:\n\n1. Create a table with the correct schema, including the identity column definition and other table properties.\n2. Run an insertion operation.\n\nThe following example define the identity column to be `generated by default as identity`. If data inserted into the table includes valid values for the identity column, these values are used.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python \n    from delta.tables import DeltaTable, IdentityGenerator\n    from pyspark.sql.types import LongType, DateType\n\n    DeltaTable.create(spark)\n      .tableName(\"new_table\")\n      .addColumn(\"id\", dataType=LongType(), generatedByDefaultAs=IdentityGenerator(start=5, step=1))\n      .addColumn(\"event_date\", dataType=DateType())\n      .addColumn(\"some_value\", dataType=LongType())\n      .execute()\n\n    # Insert records including existing IDs\n    old_table_df = spark.table(\"old_table\").select(\"id\", \"event_date\", \"some_value\")\n    old_table_df.write\n      .format(\"delta\")\n      .mode(\"append\")\n      .saveAsTable(\"new_table\")\n\n    # Insert records and generate new IDs\n    new_records_df = spark.table(\"new_records\").select(\"event_date\", \"some_value\")\n    new_records_df.write\n      .format(\"delta\")\n      .mode(\"append\")\n      .saveAsTable(\"new_table\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import org.apache.spark.sql.types._\n    import io.delta.tables.DeltaTable\n\n    DeltaTable.createOrReplace(spark)\n      .tableName(\"new_table\")\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"id\")\n          .dataType(LongType)\n          .generatedByDefaultAsIdentity(start = 5L, step = 1L)\n          .build())\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"event_date\")\n          .dataType(DateType)\n          .nullable(true)\n          .build())\n      .addColumn(\n        DeltaTable.columnBuilder(spark, \"some_value\")\n          .dataType(LongType)\n          .nullable(true)\n          .build())\n      .execute()\n\n    // Insert records including existing IDs\n    val oldTableDF = spark.table(\"old_table\").select(\"id\", \"event_date\", \"some_value\")\n    oldTableDF.write\n      .format(\"delta\")\n      .mode(\"append\")\n      .saveAsTable(\"new_table\")\n\n    // Insert records and generate new IDs\n    val newRecordsDF = spark.table(\"new_records\").select(\"event_date\", \"some_value\")\n    newRecordsDF.write\n      .format(\"delta\")\n      .mode(\"append\")\n      .saveAsTable(\"new_table\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n#### Identity column limitations\n\nThe following limitations exist when working with identity columns:\n\n- Concurrent transactions are not supported on tables with identity columns enabled.\n- You cannot partition a table by an identity column.\n- You cannot `ADD`, `REPLACE`, or `CHANGE` an identity column.\n- You cannot update the value of an identity column for an existing record.\n\n<Aside type=\"note\">\n  To change the `IDENTITY` value for an existing record, you must delete the\n  record and `INSERT` it as a new record.\n</Aside>\n\n### Specify default values for columns\n\nDelta enables the specification of [default expressions](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#default-columns) for columns in Delta tables. When users write to these tables without explicitly providing values for certain columns, or when they explicitly use the `DEFAULT` SQL keyword for a column, Delta automatically generates default values for those columns. For more information, please refer to the dedicated documentation page.\n\n### Use special characters in column names\n\nBy default, special characters such as spaces and any of the characters `,;{}()\\n\\t=` are not supported in table column names. To include these special characters in a table's column name, enable column mapping.\n\n### Default table properties\n\nDelta Lake configurations set in the SparkSession override the default [table properties](/table-properties/) for new Delta Lake tables created in the session. The prefix used in the SparkSession is different from the configurations used in the table properties.\n\n| Delta Lake conf | SparkSession conf                                   |\n| --------------- | --------------------------------------------------- |\n| `delta.<conf>`  | `spark.databricks.delta.properties.defaults.<conf>` |\n\nFor example, to set the `delta.appendOnly = true` property for all new Delta Lake tables created in a session, set the following:\n\n```sql\nSET spark.databricks.delta.properties.defaults.appendOnly = true\n```\n\n## Read a table\n\nYou can load a Delta table as a DataFrame by specifying a table name or a path:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    SELECT * FROM default.people10m -- query table in the metastore\n\n    SELECT * FROM delta.`/tmp/delta/people10m` -- query table by path\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.table(\"default.people10m\") # query table in the metastore\n\n    spark.read.format(\"delta\").load(\"/tmp/delta/people10m\") # query table by path\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    spark.table(\"default.people10m\") // query table in the metastore\n\n    spark.read.format(\"delta\").load(\"/tmp/delta/people10m\") // create table by path\n\n    import io.delta.implicits._\n    spark.read.delta(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nThe DataFrame returned automatically reads the most recent snapshot of the table for any query; you never need to run `REFRESH TABLE`. Delta Lake automatically uses partitioning and statistics to read the minimum amount of data when there are applicable predicates in the query.\n\n## Query an older snapshot of a table (time travel)\n\nDelta Lake time travel allows you to query an older snapshot of a Delta table. Time travel has many use cases, including:\n\n- Re-creating analyses, reports, or outputs (for example, the output of a machine learning model). This could be useful for debugging or auditing, especially in regulated industries.\n- Writing complex temporal queries.\n- Fixing mistakes in your data.\n- Providing snapshot isolation for a set of queries for fast changing tables.\n\nThis section describes the supported methods for querying older versions of tables, data retention concerns, and provides examples.\n\n<Aside type=\"note\">\n  The timestamp of each version N depends on the timestamp of the log file\n  corresponding to the version N in Delta table log. Hence, time travel by\n  timestamp can break if you copy the entire Delta table directory to a new\n  location. Time travel by version will be unaffected.\n</Aside>\n\n### Syntax\n\nThis section shows how to query an older version of a Delta table.\n\n#### SQL `AS OF` syntax\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    SELECT * FROM table_name TIMESTAMP AS OF timestamp_expression\n    SELECT * FROM table_name VERSION AS OF version\n    ```\n  \n  </TabItem>\n</Tabs>\n\n- `timestamp_expression` can be any one of:\n\n  - `'2018-10-18T22:15:12.013Z'`, that is, a string that can be cast to a timestamp\n  - `cast('2018-10-18 13:36:32 CEST' as timestamp)`\n  - `'2018-10-18'`, that is, a date string\n  - `current_timestamp() - interval 12 hours`\n  - `date_sub(current_date(), 1)`\n  - Any other expression that is or can be cast to a timestamp\n\n- `version` is a long value that can be obtained from the output of `DESCRIBE HISTORY table_spec`.\n\nNeither `timestamp_expression` nor `version` can be subqueries.\n\n##### Example\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    SELECT * FROM default.people10m TIMESTAMP AS OF '2018-10-18T22:15:12.013Z'\n    SELECT * FROM delta.`/tmp/delta/people10m` VERSION AS OF 123\n    ```\n  \n  </TabItem>\n</Tabs>\n\n#### DataFrameReader options\n\nDataFrameReader options allow you to create a DataFrame from a Delta table that is fixed to a specific version of the table.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    df1 = spark.read.format(\"delta\").option(\"timestampAsOf\", timestamp_string).load(\"/tmp/delta/people10m\")\n    df2 = spark.read.format(\"delta\").option(\"versionAsOf\", version).load(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nFor `timestamp_string`, only date or timestamp strings are accepted. For example, `\"2019-01-01\"` and `\"2019-01-01T00:00:00.000Z\"`.\n\nA common pattern is to use the latest state of the Delta table throughout the execution of a job to update downstream applications.\n\nBecause Delta tables auto update, a DataFrame loaded from a Delta table may return different results across invocations if the underlying data is updated. By using time travel, you can fix the data returned by the DataFrame across invocations:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    history = spark.sql(\"DESCRIBE HISTORY delta.`/tmp/delta/people10m`\")\n    latest_version = history.selectExpr(\"max(version)\").collect()\n    df = spark.read.format(\"delta\").option(\"versionAsOf\", latest_version[0][0]).load(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n##### Examples\n\n- Fix accidental deletes to a table for the user 111:\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"Python\" active>\n\n      ```python\n      yesterday = spark.sql(\"SELECT CAST(date_sub(current_date(), 1) AS STRING)\").collect()[0][0]\n      df = spark.read.format(\"delta\").option(\"timestampAsOf\", yesterday).load(\"/tmp/delta/events\")\n      df.where(\"userId = 111\").write.format(\"delta\").mode(\"append\").save(\"/tmp/delta/events\")\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n\n- Fix accidental incorrect updates to a table:\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"Python\" active>\n\n      ```python\n      yesterday = spark.sql(\"SELECT CAST(date_sub(current_date(), 1) AS STRING)\").collect()[0][0]\n      df = spark.read.format(\"delta\").option(\"timestampAsOf\", yesterday).load(\"/tmp/delta/events\")\n      df.createOrReplaceTempView(\"my_table_yesterday\")\n      spark.sql('''\n      MERGE INTO delta.`/tmp/delta/events` target\n        USING my_table_yesterday source\n        ON source.userId = target.userId\n        WHEN MATCHED THEN UPDATE SET *\n      ''')\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n- Query the number of new customers added over the last week:\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"Python\" active>\n\n      ```python\n      last_week = spark.sql(\"SELECT CAST(date_sub(current_date(), 7) AS STRING)\").collect()[0][0]\n      df = spark.read.format(\"delta\").option(\"timestampAsOf\", last_week).load(\"/tmp/delta/events\")\n      last_week_count = df.select(\"userId\").distinct().count()\n      count = spark.read.format(\"delta\").load(\"/tmp/delta/events\").select(\"userId\").distinct().count()\n      new_customers_count = count - last_week_count\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n### Data retention\n\nTo time travel to a previous version, you must retain _both_ the log and the data files for that version.\n\nThe data files backing a Delta table are _never_ deleted automatically; data files are deleted only when you run [VACUUM](/delta-utility#remove-files-no-longer-referenced-by-a-delta-table). `VACUUM` _does not_ delete Delta log files; log files are automatically cleaned up after checkpoints are written.\n\nBy default you can time travel to a Delta table up to 30 days old unless you have:\n\n- Run `VACUUM` on your Delta table.\n- Changed the data or log file retention periods using the following [table properties](/table-properties/):\n  - `delta.logRetentionDuration = \"interval <interval>\"`: controls how long the history for a table is kept. The default is `interval 30 days`.\n\nEach time a checkpoint is written, Delta automatically cleans up log entries older than the retention interval. If you set this config to a large enough value, many log entries are retained. This should not impact performance as operations against the log are constant time. Operations on history are parallel but will become more expensive as the log size increases.\n\n- `delta.deletedFileRetentionDuration = \"interval <interval>\"`: controls how long ago a file must have been deleted _before being a candidate for_ `VACUUM`. The default is `interval 7 days`.\n\n  To access 30 days of historical data even if you run `VACUUM` on the Delta table, set `delta.deletedFileRetentionDuration = \"interval 30 days\"`. This setting may cause your storage costs to go up.\n\n<Aside type=\"note\">\n  Due to log entry cleanup, instances can arise where you cannot time travel to\n  a version that is less than the retention interval. Delta Lake requires all\n  consecutive log entries since the previous checkpoint to time travel to a\n  particular version. For example, with a table initially consisting of log\n  entries for versions [0, 19] and a checkpoint at verison 10, if the log entry\n  for version 0 is cleaned up, then you cannot time travel to versions [1, 9].\n  Increasing the table property `delta.logRetentionDuration` can help avoid\n  these situations.\n</Aside>\n\n## Write to table\n\n### Append\n\nTo atomically add new data to an existing Delta table, use `append` mode:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem\n    label=\"SQL\"\n    active\n  >\n\n    ```sql\n    INSERT INTO default.people10m SELECT * FROM morePeople\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    df.write.format(\"delta\").mode(\"append\").save(\"/tmp/delta/people10m\")\n    df.write.format(\"delta\").mode(\"append\").saveAsTable(\"default.people10m\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    df.write.format(\"delta\").mode(\"append\").save(\"/tmp/delta/people10m\")\n    df.write.format(\"delta\").mode(\"append\").saveAsTable(\"default.people10m\")\n\n    import io.delta.implicits._\n    df.write.mode(\"append\").delta(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n### Overwrite\n\nTo atomically replace all the data in a table, use `overwrite` mode:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem\n    label=\"SQL\"\n    active\n  >\n\n    ```sql\n    INSERT OVERWRITE TABLE default.people10m SELECT * FROM morePeople\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    df.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta/people10m\")\n    df.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"default.people10m\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    df.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta/people10m\")\n    df.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"default.people10m\")\n\n    import io.delta.implicits._\n    df.write.mode(\"overwrite\").delta(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nYou can selectively overwrite only the data that matches an arbitrary expression. This feature is available with DataFrames in Delta Lake 1.1.0 and above and supported in SQL in Delta Lake 2.4.0 and above.\n\nThe following command atomically replaces events in January in the target table, which is partitioned by `start_date`, with the data in `replace_data`:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    INSERT INTO TABLE events REPLACE WHERE start_data >= '2017-01-01' AND end_date <= '2017-01-31' SELECT * FROM replace_data\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    replace_data.write \\\n      .format(\"delta\") \\\n      .mode(\"overwrite\") \\\n      .option(\"replaceWhere\", \"start_date >= '2017-01-01' AND end_date <= '2017-01-31'\") \\\n      .save(\"/tmp/delta/events\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    replace_data.write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"replaceWhere\", \"start_date >= '2017-01-01' AND end_date <= '2017-01-31'\")\n      .save(\"/tmp/delta/events\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nThis sample code writes out the data in `replace_data`, validates that it all matches the predicate, and performs an atomic replacement. If you want to write out data that doesn't all match the predicate, to replace the matching rows in the target table, you can disable the constraint check by setting `spark.databricks.delta.replaceWhere.constraintCheck.enabled` to false:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    SET spark.databricks.delta.replaceWhere.constraintCheck.enabled=false\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.conf.set(\"spark.databricks.delta.replaceWhere.constraintCheck.enabled\", False)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    spark.conf.set(\"spark.databricks.delta.replaceWhere.constraintCheck.enabled\", false)\n    ```\n\n  </TabItem>\n</Tabs>\n\nIn Delta Lake 1.0.0 and below, `replaceWhere` overwrites data matching a predicate over partition columns only. The following command atomically replaces the month in January in the target table, which is partitioned by `date`, with the data in `df`:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    df.write \\\n      .format(\"delta\") \\\n      .mode(\"overwrite\") \\\n      .option(\"replaceWhere\", \"birthDate >= '2017-01-01' AND birthDate <= '2017-01-31'\") \\\n      .save(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    df.write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"replaceWhere\", \"birthDate >= '2017-01-01' AND birthDate <= '2017-01-31'\")\n      .save(\"/tmp/delta/people10m\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nIn Delta Lake 1.1.0 and above, if you want to fall back to the old behavior, you can disable the `spark.databricks.delta.replaceWhere.dataColumns.enabled` flag:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    SET spark.databricks.delta.replaceWhere.dataColumns.enabled=false\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.conf.set(\"spark.databricks.delta.replaceWhere.dataColumns.enabled\", False)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"spark.databricks.delta.replaceWhere.dataColumns.enabled\", false)\n    ```\n\n  </TabItem>\n</Tabs>\n\n#### Dynamic Partition Overwrites\n\nDelta Lake 2.0 and above supports _dynamic_ partition overwrite mode for partitioned tables.\n\nWhen in dynamic partition overwrite mode, we overwrite all existing data in each logical partition for which the write will commit new data. Any existing logical partitions for which the write does not contain data will remain unchanged. This mode is only applicable when data is being written in overwrite mode: either `INSERT OVERWRITE` in SQL, or a DataFrame write with `df.write.mode(\"overwrite\")`.\n\nConfigure dynamic partition overwrite mode by setting the Spark session configuration `spark.sql.sources.partitionOverwriteMode` to `dynamic`. You can also enable this by setting the `DataFrameWriter` option `partitionOverwriteMode` to `dynamic`. If present, the query-specific option overrides the mode defined in the session configuration. The default for `partitionOverwriteMode` is `static`.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem\n    label=\"SQL\"\n    active\n  >\n\n    ```sql\n    SET spark.sql.sources.partitionOverwriteMode=dynamic;\n    INSERT OVERWRITE TABLE default.people10m SELECT * FROM morePeople;\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    df.write \\\n      .format(\"delta\") \\\n      .mode(\"overwrite\") \\\n      .option(\"partitionOverwriteMode\", \"dynamic\") \\\n      .saveAsTable(\"default.people10m\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    df.write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"partitionOverwriteMode\", \"dynamic\")\n      .saveAsTable(\"default.people10m\")\n    ``` \n  \n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n\nDynamic partition overwrite conflicts with the option `replaceWhere` for partitioned tables.\n\n- If dynamic partition overwrite is enabled in the Spark session configuration, and `replaceWhere` is provided as a `DataFrameWriter` option, then Delta Lake overwrites the data according to the `replaceWhere` expression (query-specific options override session configurations).\n- You'll receive an error if the `DataFrameWriter` options have both dynamic partition overwrite and `replaceWhere` enabled.\n\n</Aside>\n\n<Aside type=\"caution\" title=\"Important\">\n\nValidate that the data written with dynamic partition overwrite touches only the expected partitions. A single row in the incorrect partition can lead to unintentionally overwriting an entire partition. We recommend using `replaceWhere` to specify which data to overwrite.\n\nIf a partition has been accidentally overwritten, you can use [Restore a Delta table to an earlier state](/delta-utility/#restore-a-delta-table-to-an-earlier-state) to undo the change.\n\n</Aside>\n\nFor Delta Lake support for updating tables, see [Table deletes, updates, and merges](/delta-update/).\n\n### Limit rows written in a file\n\nYou can use the SQL session configuration `spark.sql.files.maxRecordsPerFile` to specify the maximum number of records to write to a single file for a Delta Lake table. Specifying a value of zero or a negative value represents no limit.\n\nYou can also use the DataFrameWriter option `maxRecordsPerFile` when using the DataFrame APIs to write to a Delta Lake table. When `maxRecordsPerFile` is specified, the value of the SQL session configuration `spark.sql.files.maxRecordsPerFile` is ignored.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem\n    label=\"Python\"\n    active\n  >\n    ```python\n    df.write.format(\"delta\") \\\n      .mode(\"append\") \\\n      .option(\"maxRecordsPerFile\", \"10000\") \\\n      .save(\"/tmp/delta/people10m\")\n    ```\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    df.write.format(\"delta\")\n      .mode(\"append\")\n      .option(\"maxRecordsPerFile\", \"10000\")\n      .save(\"/tmp/delta/people10m\")\n    ```\n  </TabItem>\n</Tabs>\n\n### Idempotent writes\n\nSometimes a job that writes data to a Delta table is restarted due to various reasons (for example, job encounters a failure). The failed job may or may not have written the data to Delta table before terminating. In the case where the data is written to the Delta table, the restarted job writes the same data to the Delta table which results in duplicate data.\n\nTo address this, Delta tables support the following `DataFrameWriter` options to make the writes idempotent:\n\n- `txnAppId`: A unique string that you can pass on each `DataFrame` write. For example, this can be the name of the job.\n- `txnVersion`: A monotonically increasing number that acts as transaction version. This number needs to be unique for data that is being written to the Delta table(s). For example, this can be the epoch seconds of the instant when the query is attempted for the first time. Any subsequent restarts of the same job needs to have the same value for `txnVersion`.\n\nThe above combination of options needs to be unique for each new data that is being ingested into the Delta table and the `txnVersion` needs to be higher than the last data that was ingested into the Delta table. For example:\n\n- Last successfully written data contains option values as `dailyETL:23423` (`txnAppId:txnVersion`).\n- Next write of data should have `txnAppId = dailyETL` and `txnVersion` as at least `23424` (one more than the last written data `txnVersion`).\n- Any attempt to write data with `txnAppId = dailyETL` and `txnVersion` as `23422` or less is ignored because the `txnVersion` is less than the last recorded `txnVersion` in the table.\n- Attempt to write data with `txnAppId:txnVersion` as `anotherETL:23424` is successful writing data to the table as it contains a different `txnAppId` compared to the same option value in last ingested data.\n\nYou can also configure idempotent writes by setting the Spark session configuration `spark.databricks.delta.write.txnAppId` and `spark.databricks.delta.write.txnVersion`. In addition, you can set `spark.databricks.delta.write.txnVersion.autoReset.enabled` to true to automatically reset `spark.databricks.delta.write.txnVersion` after every write. When both the writer options and session configuration are set, we will use the writer option values.\n\n<Aside type=\"danger\" title=\"Warning\">\n\nThis solution assumes that the data being written to Delta table(s) in multiple retries of the job is same. If a write attempt in a Delta table succeeds but due to some downstream failure there is a second write attempt with same txn options but different data, then that second write attempt will be ignored. This can cause unexpected results.\n\n</Aside>\n\n#### Example\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    SET spark.databricks.delta.write.txnAppId = ...;\n    SET spark.databricks.delta.write.txnVersion = ...;\n    SET spark.databricks.delta.write.txnVersion.autoReset.enabled = true; -- if set to true, this will reset txnVersion after every write\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    app_id = ... # A unique string that is used as an application ID.\n    version = ... # A monotonically increasing number that acts as transaction version.\n\n    dataFrame.write.format(...).option(\"txnVersion\", version).option(\"txnAppId\", app_id).save(...)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val appId = ... // A unique string that is used as an application ID.\n    version = ... // A monotonically increasing number that acts as transaction version.\n\n    dataFrame.write.format(...).option(\"txnVersion\", version).option(\"txnAppId\", appId).save(...)\n    ```\n\n  </TabItem>\n</Tabs>\n\n### Set user-defined commit metadata\n\nYou can specify user-defined strings as metadata in commits made by these operations, either using the DataFrameWriter option `userMetadata` or the SparkSession configuration `spark.databricks.delta.commitInfo.userMetadata`. If both of them have been specified, then the option takes preference. This user-defined metadata is readable in the [history](/delta-utility/#retrieve-delta-table-history) operation.\n\n<Tabs syncKey=\"code-examples\">\n<TabItem\n  label=\"SQL\"\n  active\n>\n\n  ```sql\n  SET spark.databricks.delta.commitInfo.userMetadata=overwritten-for-fixing-incorrect-data\n  INSERT OVERWRITE default.people10m SELECT * FROM morePeople\n  ```\n\n</TabItem>\n<TabItem label=\"Python\">\n\n  ```python\n  df.write.format(\"delta\") \\\n    .mode(\"overwrite\") \\\n    .option(\"userMetadata\", \"overwritten-for-fixing-incorrect-data\") \\\n    .save(\"/tmp/delta/people10m\")\n  ```\n\n</TabItem>\n<TabItem label=\"Scala\">\n\n  ```scala\n  df.write.format(\"delta\")\n    .mode(\"overwrite\")\n    .option(\"userMetadata\", \"overwritten-for-fixing-incorrect-data\")\n    .save(\"/tmp/delta/people10m\")\n  ```\n\n</TabItem>\n</Tabs>\n\n## Schema validation\n\nDelta Lake automatically validates that the schema of the DataFrame being written is compatible with the schema of the table. Delta Lake uses the following rules to determine whether a write from a DataFrame to a table is compatible:\n\n- All DataFrame columns must exist in the target table. If there are columns in the DataFrame not present in the table, an exception is raised. Columns present in the table but not in the DataFrame are set to null.\n- DataFrame column data types must match the column data types in the target table. If they don't match, an exception is raised.\n- DataFrame column names cannot differ only by case. This means that you cannot have columns such as \"Foo\" and \"foo\" defined in the same table. While you can use Spark in case sensitive or insensitive (default) mode, Parquet is case sensitive when storing and returning column information. Delta Lake is case-preserving but insensitive when storing the schema and has this restriction to avoid potential mistakes, data corruption, or loss issues.\n\nDelta Lake support DDL to add new columns explicitly and the ability to update schema automatically.\n\nIf you specify other options, such as `partitionBy`, in combination with append mode, Delta Lake validates that they match and throws an error for any mismatch. When `partitionBy` is not present, appends automatically follow the partitioning of the existing data.\n\n## Update table schema\n\nDelta Lake lets you update the schema of a table. The following types of changes are supported:\n\n- Adding new columns (at arbitrary positions)\n- Reordering existing columns\n\nYou can make these changes explicitly using DDL or implicitly using DML.\n\n<Aside type=\"caution\" title=\"Important\">\n  When you update a Delta table schema, streams that read from that table\nterminate. If you want the stream to continue you must restart it.\n</Aside>\n\n### Explicitly update schema\n\nYou can use the following DDL to explicitly change the schema of a table.\n\n#### Add columns\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name ADD COLUMNS (col_name data_type [COMMENT col_comment] [FIRST|AFTER colA_name], ...)\n    ```\n  \n  </TabItem>\n</Tabs>\n\nBy default, nullability is `true`.\n\nTo add a column to a nested field, use:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name ADD COLUMNS (col_name.nested_col_name data_type [COMMENT col_comment] [FIRST|AFTER colA_name], ...)\n    ```\n  \n  </TabItem>\n</Tabs>\n\n##### Example\n\nIf the schema before running `ALTER TABLE boxes ADD COLUMNS (colB.nested STRING AFTER field1)` is:\n\n```\n- root\n| - colA\n| - colB\n| +-field1\n| +-field2\n```\n\nthe schema after is:\n\n```\n- root\n| - colA\n| - colB\n| +-field1\n| +-nested\n| +-field2\n```\n\n<Aside type=\"note\">\n  Adding nested columns is supported only for structs. Arrays and maps are not\n  supported.\n</Aside>\n\n#### Change column comment or ordering\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name ALTER [COLUMN] col_name col_name data_type [COMMENT col_comment] [FIRST|AFTER colA_name]\n    ```\n  \n  </TabItem>\n</Tabs>\n\nTo change a column in a nested field, use:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name ALTER [COLUMN] col_name.nested_col_name nested_col_name data_type [COMMENT col_comment] [FIRST|AFTER colA_name]\n    ```\n  \n  </TabItem>\n</Tabs>\n\n##### Example\n\nIf the schema before running `ALTER TABLE boxes CHANGE COLUMN colB.field2 field2 STRING FIRST` is:\n\n```\n- root\n| - colA\n| - colB\n| +-field1\n| +-field2\n```\n\nthe schema after is:\n\n```\n- root\n| - colA\n| - colB\n| +-field2\n| +-field1\n```\n\n#### Replace columns\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name REPLACE COLUMNS (col_name1 col_type1 [COMMENT col_comment1], ...)\n    ```\n  \n  </TabItem>\n</Tabs>\n\n##### Example\n\nWhen running the following DDL:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE boxes REPLACE COLUMNS (colC STRING, colB STRUCT<field2:STRING, nested:STRING, field1:STRING>, colA STRING)\n    ```\n  \n  </TabItem>\n</Tabs>\n\nif the schema before is:\n\n```\n- root\n| - colA\n| - colB\n| +-field1\n| +-field2\n```\n\nthe schema after is:\n\n```\n- root\n| - colC\n| - colB\n| +-field2\n| +-nested\n| +-field1\n| - colA\n```\n\n#### Rename columns\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 1.2.0 and above. This feature is\n  currently experimental.\n</Aside>\n\nTo rename columns without rewriting any of the columns' existing data, you must enable column mapping for the table. See [enable column mapping](/delta-column-mapping/).\n\nTo rename a column:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name RENAME COLUMN old_col_name TO new_col_name\n    ```\n\n  </TabItem>\n</Tabs>\n\nTo rename a nested field:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name RENAME COLUMN col_name.old_nested_field TO new_nested_field\n    ```\n  \n  </TabItem>\n</Tabs>\n\n##### Example\n\nWhen you run the following command:\n\n```sql\nALTER TABLE boxes RENAME COLUMN colB.field1 TO field001\n```\n\nIf the schema before is:\n\n```\n- root\n| - colA\n| - colB\n| +-field1\n| +-field2\n```\n\nThen the schema after is:\n\n```\n- root\n| - colA\n| - colB\n| +-field001\n| +-field2\n```\n\n#### Drop columns\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 2.0 and above. This feature is\n  currently experimental.\n</Aside>\n\nTo drop columns as a metadata-only operation without rewriting any data files, you must enable column mapping for the table. See [enable column mapping](/delta-column-mapping/).\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Dropping a column from metadata does not delete the underlying data for the\n  column in files.\n</Aside>\n\nTo drop a column:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name DROP COLUMN col_name\n    ```\n  \n  </TabItem>\n</Tabs>\n\nTo drop multiple columns:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n    ```sql\n    ALTER TABLE table_name DROP COLUMNS (col_name_1, col_name_2)\n    ```\n\n  </TabItem>\n</Tabs>\n\n#### Change column type or name\n\nYou can change a column's type or name or drop a column by rewriting the table. To do this, use the `overwriteSchema` option:\n\n##### Change a column type\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n  ```python\n  spark.read.table(...) \\\n    .withColumn(\"birthDate\", col(\"birthDate\").cast(\"date\")) \\\n    .write \\\n    .format(\"delta\") \\\n    .mode(\"overwrite\")\n    .option(\"overwriteSchema\", \"true\") \\\n    .saveAsTable(...)\n  ```\n\n  </TabItem>\n</Tabs>\n\n##### Change a column name\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n  ```python\n  spark.read.table(...) \\\n    .withColumnRenamed(\"dateOfBirth\", \"birthDate\") \\\n    .write \\\n    .format(\"delta\") \\\n    .mode(\"overwrite\") \\\n    .option(\"overwriteSchema\", \"true\") \\\n    .saveAsTable(...)\n  ```\n\n  </TabItem>\n</Tabs>\n\n### Automatic schema update\n\nDelta Lake can automatically update the schema of a table as part of a DML transaction (either appending or overwriting), and make the schema compatible with the data being written.\n\n#### Add columns\n\nColumns that are present in the DataFrame but missing from the table are automatically added as part of a write transaction when:\n\n- `write` or `writeStream` have `.option(\"mergeSchema\", \"true\")`\n- `spark.databricks.delta.schema.autoMerge.enabled` is `true`\n\nWhen both options are specified, the option from the `DataFrameWriter` takes precedence. The added columns are appended to the end of the struct they are present in. Case is preserved when appending a new column.\n\n#### `NullType` columns\n\nBecause Parquet doesn't support `NullType`, `NullType` columns are dropped from the DataFrame when writing into Delta tables, but are still stored in the schema. When a different data type is received for that column, Delta Lake merges the schema to the new data type. If Delta Lake receives a `NullType` for an existing column, the old schema is retained and the new column is dropped during the write.\n\n`NullType` in streaming is not supported. Since you must set schemas when using streaming this should be very rare. `NullType` is also not accepted for complex types such as `ArrayType` and `MapType`.\n\n## Replace table schema\n\nBy default, overwriting the data in a table does not overwrite the schema. When overwriting a table using mode `overwrite` without `replaceWhere`, you may still want to overwrite the schema of the data being written. You replace the schema and partitioning of the table by setting the `overwriteSchema` option to `true`:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n  ```python\n  df.write.option(\"overwriteSchema\", \"true\")\n  ```\n\n  </TabItem>\n</Tabs>\n\n## Views on tables\n\nDelta Lake supports the creation of views on top of Delta tables just like you might with a data source table.\n\nThe core challenge when you operate with views is resolving the schemas. If you alter a Delta table schema, you must recreate derivative views to account for any additions to the schema. For instance, if you add a new column to a Delta table, you must make sure that this column is available in the appropriate views built on top of that base table.\n\n## Table properties\n\nYou can store your own metadata as a table property using `TBLPROPERTIES` in `CREATE` and `ALTER`. You can then `SHOW` that metadata. For example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\" active>\n\n  ```sql\n  ALTER TABLE default.people10m SET TBLPROPERTIES ('department' = 'accounting', 'delta.appendOnly' = 'true');\n\n  -- Show the table's properties.\n  SHOW TBLPROPERTIES default.people10m;\n\n  -- Show just the 'department' table property.\n  SHOW TBLPROPERTIES default.people10m ('department');\n  ```\n\n  </TabItem>\n</Tabs>\n\n\n`TBLPROPERTIES` are stored as part of Delta table metadata. You cannot define new `TBLPROPERTIES` in a `CREATE` statement if a Delta table already exists in a given location.\n\nIn addition, to tailor behavior and performance, Delta Lake supports certain Delta table properties:\n\n- Block deletes and updates in a Delta table: `delta.appendOnly=true`.\n- Configure the [time travel](#query-an-older-snapshot-of-a-table-time-travel) retention properties: `delta.logRetentionDuration=<interval-string>` and `delta.deletedFileRetentionDuration=<interval-string>`. For details, see [Data retention](#data-retention).\n- Configure the number of columns for which statistics are collected: `delta.dataSkippingNumIndexedCols=n`. This property indicates to the writer that statistics are to be collected only for the first `n` columns in the table. Also the data skipping code ignores statistics for any column beyond this column index. This property takes affect only for new data that is written out.\n\n<Aside type=\"note\">\n  Modifying a Delta table property is a write operation that will conflict\n  with other [concurrent write operations](/concurrency-control), causing them to fail. We recommend\n  that you modify a table property only when there are no concurrent write\n  operations on the table.\n</Aside>\n\nYou can also set `delta.`-prefixed properties during the first commit to a Delta table using Spark configurations. For example, to initialize a Delta table with the property `delta.appendOnly=true`, set the Spark configuration `spark.databricks.delta.properties.defaults.appendOnly` to `true`. For example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    spark.sql(\"SET spark.databricks.delta.properties.defaults.appendOnly = true\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.conf.set(\"spark.databricks.delta.properties.defaults.appendOnly\", \"true\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    spark.conf.set(\"spark.databricks.delta.properties.defaults.appendOnly\", \"true\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nSee also the [Delta table properties reference](/table-properties/).\n\n## Syncing table schema and properties to the Hive metastore\n\nYou can enable asynchronous syncing of table schema and properties to the metastore by setting `spark.databricks.delta.catalog.update.enabled` to `true`. Whenever the Delta client detects that either of these two were changed due to an update, it will sync the changes to the metastore.\n\nThe schema is stored in the table properties in HMS. If the schema is small, it will be stored directly under the key `spark.sql.sources.schema`:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"JSON\">\n\n    ```json\n    {\n      \"spark.sql.sources.schema\": \"{'name':'col1','type':'string','nullable':true, 'metadata':{}},{'name':'col2','type':'string','nullable':true,'metadata':{}}\"\n    }\n    ```\n\n  </TabItem>\n</Tabs>\n\nIf Schema is large, the schema will be broken down into multiple parts. Appending them together should give the correct schema. For example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"JSON\">\n\n    ```json\n    {\n      \"spark.sql.sources.schema.numParts\": \"4\",\n      \"spark.sql.sources.schema.part.1\": \"{'name':'col1','type':'string','nullable':tr\",\n      \"spark.sql.sources.schema.part.2\": \"ue, 'metadata':{}},{'name':'co\",\n      \"spark.sql.sources.schema.part.3\": \"l2','type':'string','nullable':true,'meta\",\n      \"spark.sql.sources.schema.part.4\": \"data':{}}\"\n    }\n    ```\n\n  </TabItem>\n</Tabs>\n\n## Table metadata\n\nDelta Lake has rich features for exploring table metadata.\n\nIt supports `SHOW COLUMNS` and `DESCRIBE TABLE`.\n\nIt also provides the following unique commands:\n\n### `DESCRIBE DETAIL`\n\nProvides information about schema, partitioning, table size, and so on. For details, see [Retrieve Delta table details](/delta-utility/#retrieve-delta-table-history).\n\n### `DESCRIBE HISTORY`\n\nProvides provenance information, including the operation, user, and so on, and operation metrics for each write to a table. Table history is retained for 30 days. For details, see [Retrieve Delta table history](/delta-utility/#retrieve-delta-table-history).\n\n## Configure SparkSession\n\nFor many Delta Lake operations, you enable integration with Apache Spark DataSourceV2 and Catalog APIs (since 3.0) by setting the following configurations when you create a new `SparkSession`.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    from pyspark.sql import SparkSession\n\n    spark = SparkSession \\\n      .builder \\\n      .appName(\"...\") \\\n      .master(\"...\") \\\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n      .getOrCreate()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    import org.apache.spark.sql.SparkSession\n\n    val spark = SparkSession\n      .builder()\n      .appName(\"...\")\n      .master(\"...\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    import org.apache.spark.sql.SparkSession;\n\n    SparkSession spark = SparkSession\n      .builder()\n      .appName(\"...\")\n      .master(\"...\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate();\n    ```\n\n  </TabItem>\n</Tabs>\n\nAlternatively, you can add configurations when submitting your Spark application using `spark-submit` or when starting `spark-shell` or `pyspark` by specifying them as command-line parameters.\n\n```bash\nspark-submit --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"  ...\n```\n\n```bash\npyspark --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n```\n\n## Configure storage credentials\n\nDelta Lake uses Hadoop FileSystem APIs to access storage systems. The credentials for storage systems usually can be set through Hadoop configurations. Delta Lake provides multiple ways to set Hadoop configurations similar to Apache Spark.\n\n### Spark configurations\n\nWhen you start a Spark application on a cluster, you can set the Spark configurations in the form of `spark.hadoop.*` to pass your custom Hadoop configurations. For example, setting a value for `spark.hadoop.a.b.c` will pass the value as a Hadoop configuration `a.b.c`, and Delta Lake will use it to access Hadoop FileSystem APIs.\n\nSee [Spark documentation](http://spark.apache.org/docs/latest/configuration.html#custom-hadoophive-configuration) for more details.\n\n### SQL session configurations\n\nSpark SQL will pass all of the current [SQL session configurations](http://spark.apache.org/docs/latest/configuration.html#runtime-sql-configuration) to Delta Lake, and Delta Lake will use them to access Hadoop FileSystem APIs. For example, `SET a.b.c=x.y.z` will tell Delta Lake to pass the value `x.y.z` as a Hadoop configuration `a.b.c`, and Delta Lake will use it to access Hadoop FileSystem APIs.\n\n### DataFrame options\n\nBesides setting Hadoop file system configurations through the Spark (cluster) configurations or SQL session configurations, Delta supports reading Hadoop file system configurations from `DataFrameReader` and `DataFrameWriter` options (that is, option keys that start with the `fs.` prefix) when the table is read or written, by using `DataFrameReader.load(path)` or `DataFrameWriter.save(path)`.\n\nFor example, you can pass your storage credentials through DataFrame options:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    df1 = spark.read.format(\"delta\") \\\n      .option(\"fs.azure.account.key.<storage-account-name>.dfs.core.windows.net\", \"<storage-account-access-key-1>\") \\\n      .read(\"...\")\n    df2 = spark.read.format(\"delta\") \\\n      .option(\"fs.azure.account.key.<storage-account-name>.dfs.core.windows.net\", \"<storage-account-access-key-2>\") \\\n      .read(\"...\")\n    df1.union(df2).write.format(\"delta\") \\\n      .mode(\"overwrite\") \\\n      .option(\"fs.azure.account.key.<storage-account-name>.dfs.core.windows.net\", \"<storage-account-access-key-3>\") \\\n      .save(\"...\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val df1 = spark.read.format(\"delta\")\n      .option(\"fs.azure.account.key.<storage-account-name>.dfs.core.windows.net\", \"<storage-account-access-key-1>\")\n      .read(\"...\")\n    val df2 = spark.read.format(\"delta\")\n      .option(\"fs.azure.account.key.<storage-account-name>.dfs.core.windows.net\", \"<storage-account-access-key-2>\")\n      .read(\"...\")\n    df1.union(df2).write.format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"fs.azure.account.key.<storage-account-name>.dfs.core.windows.net\", \"<storage-account-access-key-3>\")\n      .save(\"...\")\n    ```\n\n  </TabItem>\n</Tabs>\n\nYou can find the details of the Hadoop file system configurations for your storage in [Storage configuration](/delta-storage/).\n"
  },
  {
    "path": "docs/src/content/docs/delta-catalog-managed-tables.mdx",
    "content": "---\ntitle: Use catalog-managed tables\ndescription: Learn how to enable and use catalog-managed commits in Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 4.0.1 and above.\n</Aside>\n\n[Catalog-Managed Tables](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#catalog-managed-tables) introduce a `catalogManaged` reader-writer table feature that changes how Delta Lake discovers and accesses tables. With this feature enabled, the catalog coordinates commit atomicity, allowing for features like multi-table transactions that are difficult to achieve with filesystem-only primitives.\n\n## Overview\n\nBy default, Delta Lake relies entirely on the filesystem for read-time discovery and write-time commit atomicity. Each table manages its own transaction logs and conflict detection independently. Catalog-managed tables shift this responsibility to the managing catalog, which allows the catalog to orchestrate commits across multiple tables within a single transaction boundary while maintaining Delta Lake's ACID guarantees.\n\n<Aside type=\"caution\" title=\"Important\">\nFilesystem-based access to catalog-managed tables is not supported. Delta clients must discover and access these tables through the managing catalog, not by direct path-based access. This ensures consistency across distributed environments. Users can use any catalog implementation that supports the Delta Catalog-Managed Table protocol.\n</Aside>\n\n## Requirements\n\n- Catalog-managed tables requires the following Delta protocols:\n    - Reader version 3 or above.\n    - Writer version 7 or above.\n- The [In-Commit Timestamps](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#in-commit-timestamps) table feature must be enabled, as commit publishing can occur asynchronously and file modification timestamps may not reflect actual commit times.\n- The [VACUUM Protocol Check](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#vacuum-protocol-check) table feature must be enabled to provide proper vacuum operations on catalog-managed tables.\n\n## Enable catalog-managed commits\n\nYou can enable catalog-managed commits for new tables when using a catalog that supports this feature, such as [Unity Catalog](https://www.unitycatalog.io/).\n\n### Enable catalog-managed commits for new tables\n\nEnable the `catalogManaged` table feature by setting the following table property when creating a table:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE TABLE sales_data (\n      sale_id BIGINT,\n      amount DECIMAL(10,2),\n      sale_date DATE\n    )\n    TBLPROPERTIES ('delta.feature.catalogManaged' = 'supported');\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside type=\"caution\" title=\"Warning\">\nWhen you enable catalog-managed commits, the table protocol version is upgraded. After upgrading, the table will not be readable by Delta Lake clients that do not support catalog-managed tables.\n</Aside>\n\n## Check if catalog-managed commits are enabled\n\nTo verify whether a table has catalog-managed commits enabled:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    DESCRIBE DETAIL sales_data;\n    ```\n  </TabItem>\n</Tabs>\n\nIf enabled, `catalogManaged` appears in the `tableFeatures` column.\n\n## Limitations\n\n- Catalog-managed tables cannot be enabled on existing tables. Once enabled, the feature cannot be disabled.\n- `CREATE OR REPLACE TABLE` is not supported for tables with catalog-managed commits enabled.\n"
  },
  {
    "path": "docs/src/content/docs/delta-change-data-feed.mdx",
    "content": "---\ntitle: Change data feed\ndescription: Learn how to get row-level change information from Delta tables using the Delta change data feed.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nChange Data Feed (CDF) feature allows Delta tables to track row-level changes between versions of a Delta table. When enabled on a Delta table, the runtime records \"change events\" for all the data written into the table. This includes the row data along with metadata indicating whether the specified row was inserted, deleted, or updated.\n\nYou can read the change events in batch queries using DataFrame APIs (that is, `df.read`) and in streaming queries using DataFrame APIs (that is, `df.readStream`).\n\n## Use cases\n\nChange Data Feed is not enabled by default. The following use cases should drive when you enable the change data feed.\n\n- **Silver and Gold tables**: Improve Delta performance by processing only row-level changes following initial `MERGE`, `UPDATE`, or `DELETE` operations to accelerate and simplify ETL and ELT operations.\n- **Transmit changes**: Send a change data feed to downstream systems such as Kafka or RDBMS that can use it to incrementally process in later stages of data pipelines.\n- **Audit trail table**: Capture the change data feed as a Delta table provides perpetual storage and efficient query capability to see all changes over time, including when deletes occur and what updates were made.\n\n## Enable change data feed\n\nYou must explicitly enable the change data feed option using one of the following methods:\n\n- **New table**: Set the table property `delta.enableChangeDataFeed = true` in the `CREATE TABLE` command.\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"SQL\" active>\n\n      ```sql\n      CREATE TABLE student (id INT, name STRING, age INT) TBLPROPERTIES (delta.enableChangeDataFeed = true)\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n- **Existing table**: Set the table property `delta.enableChangeDataFeed = true` in the `ALTER TABLE` command.\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"SQL\" active>\n\n      ```sql\n      ALTER TABLE myDeltaTable SET TBLPROPERTIES (delta.enableChangeDataFeed = true)\n      ```\n\n    </TabItem>\n  </Tabs>\n\n- **All new tables**:\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"SQL\" active>\n\n      ```sql\n      set spark.databricks.delta.properties.defaults.enableChangeDataFeed = true;\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n<Aside type=\"caution\" title=\"Important\">\n  Once you enable the change data feed option for a table, you can no longer write to the table using Delta Lake 1.2.1 or below. You can always read the table.\n\n  Only changes made _after_ you enable the change data feed are recorded; past changes to a table are not captured.\n</Aside>\n\n### Change data storage\n\nDelta Lake records change data for `UPDATE`, `DELETE`, and `MERGE` operations in the `_change_data` folder under the Delta table directory. These records may be skipped when Delta Lake detects it can efficiently compute the change data feed directly from the transaction log. In particular, insert-only operations and full partition deletes will not generate data in the `_change_data` directory.\n\nThe files in the `_change_data` folder follow the retention policy of the table. Therefore, if you run the [VACUUM](/delta-utility/#remove-files-no-longer-referenced-by-a-delta-table) command, change data feed data is also deleted.\n\n## Read changes in batch queries\n\nYou can provide either version or timestamp for the start and end. The start and end versions and timestamps are inclusive in the queries. To read the changes from a particular start version to the _latest_ version of the table, specify only the starting version or timestamp.\n\nYou specify a version as an integer and a timestamps as a string in the format `yyyy-MM-dd[ HH:mm:ss[.SSS]]`.\n\nIf you provide a version lower or timestamp older than one that has recorded change events, that is, when the change data feed was enabled, an error is thrown indicating that the change data feed was not enabled.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    -- version as ints or longs e.g. changes from version 0 to 10\n    SELECT * FROM table_changes('tableName', 0, 10)\n\n    -- timestamp as string formatted timestamps\n    SELECT * FROM table_changes('tableName', '2021-04-21 05:45:46', '2021-05-21 12:00:00')\n\n    -- providing only the startingVersion/timestamp\n    SELECT * FROM table_changes('tableName', 0)\n\n    -- database/schema names inside the string for table name, with backticks for escaping dots and special characters\n    SELECT * FROM table_changes('dbName.`dotted.tableName`', '2021-04-21 06:45:46' , '2021-05-21 12:00:00')\n\n    -- path based tables\n    SELECT * FROM table_changes_by_path('\\path', '2021-04-21 05:45:46')\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Python\">\n    \n    ```python\n    # version as ints or longs\n    spark.read.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingVersion\", 0) \\\n      .option(\"endingVersion\", 10) \\\n      .table(\"myDeltaTable\")\n\n    # timestamps as formatted timestamp\n    spark.read.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingTimestamp\", '2021-04-21 05:45:46') \\\n      .option(\"endingTimestamp\", '2021-05-21 12:00:00') \\\n      .table(\"myDeltaTable\")\n\n    # providing only the startingVersion/timestamp\n    spark.read.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingVersion\", 0) \\\n      .table(\"myDeltaTable\")\n\n\n    # path based tables\n    spark.read.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingTimestamp\", '2021-04-21 05:45:46') \\\n      .load(\"pathToMyDeltaTable\")\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Scala\">\n\n    ```scala\n    // version as ints or longs\n    spark.read.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", 0)\n      .option(\"endingVersion\", 10)\n      .table(\"myDeltaTable\")\n\n    // timestamps as formatted timestamp\n    spark.read.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingTimestamp\", \"2021-04-21 05:45:46\")\n      .option(\"endingTimestamp\", \"2021-05-21 12:00:00\")\n      .table(\"myDeltaTable\")\n\n    // providing only the startingVersion/timestamp\n    spark.read.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", 0)\n      .table(\"myDeltaTable\")\n\n    // path based tables\n    spark.read.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingTimestamp\", \"2021-04-21 05:45:46\")\n      .load(\"pathToMyDeltaTable\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\n## Read changes in streaming queries\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    # providing a starting version\n    spark.readStream.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingVersion\", 0) \\\n      .table(\"myDeltaTable\")\n\n    # providing a starting timestamp\n    spark.readStream.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingTimestamp\", \"2021-04-21 05:35:43\") \\\n      .load(\"/pathToMyDeltaTable\")\n\n    # not providing a starting version/timestamp will result in the latest snapshot being fetched first\n    spark.readStream.format(\"delta\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .table(\"myDeltaTable\")\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    // providing a starting version\n    spark.readStream.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", 0)\n      .table(\"myDeltaTable\")\n\n    // providing a starting timestamp\n    spark.readStream.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", \"2021-04-21 05:35:43\")\n      .load(\"/pathToMyDeltaTable\")\n\n    // not providing a starting version/timestamp will result in the latest snapshot being fetched first\n    spark.readStream.format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .table(\"myDeltaTable\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\nTo get the change data while reading the table, set the option `readChangeFeed` to `true`. The `startingVersion` or `startingTimestamp` are optional and if not provided the stream returns the latest snapshot of the table at the time of streaming as an `INSERT` and future changes as change data. Options like rate limits (`maxFilesPerTrigger`, `maxBytesPerTrigger`) and `excludeRegex` are also supported when reading change data.\n\n<Aside type=\"note\">\n  Rate limiting can be atomic for versions other than the starting snapshot version. That is, the entire commit version will be rate limited or the entire commit will be returned.\n\n  By default if a user passes in a version or timestamp exceeding the last commit on a table, the error `timestampGreaterThanLatestCommit` will be thrown. CDF can handle the out of range version case, if the user sets the following configuration to `true`.\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"Python\">\n\n      ```sql\n      set spark.databricks.delta.changeDataFeed.timestampOutOfRange.enabled = true;\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n  If you provide a start version greater than the last commit on a table or a start timestamp newer than the last commit on a table, then when the preceding configuration is enabled, an empty read result is returned.\n\n  If you provide an end version greater than the last commit on a table or an end timestamp newer than the last commit on a table, then when the preceding configuration is enabled in batch read mode, all changes between the start version and the last commit are be returned.\n</Aside>\n\n## What is the schema for the change data feed?\n\nWhen you read from the change data feed for a table, the schema for the latest table version is used.\n\n<Aside type=\"note\">\n  Most schema change and evolution operations are fully supported. Tables with\n  column mapping enabled do not support all use cases and demonstrate different\n  behavior. See [Change data feed limitations for tables with column mapping\n  enabled](#change-data-feed-limitations-for-tables-with-column-mapping-enabled).\n</Aside>\n\nIn addition to the data columns from the schema of the Delta table, change data feed contains metadata columns that identify the type of change event:\n\n| Column name | Type | Values |\n| :-- | :-- | :-- |\n| `_change_type` | String | `insert`, `update_preimage` , `update_postimage`, `delete` [(1)](#-1) |\n| `_commit_version` | Long | The Delta log or table version containing the change. |\n| `_commit_timestamp` | Timestamp | The timestamp associated when the commit was created. |\n\n<a id=\"-1\"></a>**(1)** `preimage` is the value before the update, `postimage` is\nthe value after the update.\n\n## Change data feed limitations for tables with column mapping enabled\n\nWith column mapping enabled on a Delta table, you can drop or rename columns in the table without rewriting data files for existing data. With column mapping enabled, change data feed has limitations after performing non-additive schema changes such as renaming or dropping a column, changing data type, or nullability changes.\n\n<Aside type=\"caution\" title=\"Important\">\n  In Delta Lake 2.0 and before, tables with column mapping enabled do not support streaming reads or batch reads on change data feed.\n\n  In Delta Lake 2.1, tables with column mapping enabled support batch reads on change data feed as long as there are no non-additive schema changes. Streaming reads of change data feed of tables with column mapping enabled is not supported.\n\n  In Delta Lake 2.2, tables with column mapping enabled support both batch and streaming reads on change data feed as long as there are no non-additive schema changes.\n\n  In Delta Lake 2.3 and above, you can perform batch reads on change data feed for tables with column mapping enabled that have experienced non-additive schema changes. Instead of using the schema of the latest version of the table, read operations use the schema of the end version of the table specified in the query. Queries still fail if the version range specified spans a non-additive schema change.\n\n  In Delta Lake 3.0 and above, you can perform streaming read on change data feed for tables with column mapping enabled that have experienced non-additive schema changes by enabling [schema tracking](/delta-streaming/#tracking-non-additive-schema-changes).\n</Aside>\n\n## Frequently asked questions (FAQ)\n\n### What is the overhead of enabling the change data feed?\n\nThere is no significant impact. The change data records are generated in line during the query execution process, and are generally much smaller than the total size of rewritten files.\n\n### What is the retention policy for change records?\n\nChange records follow the same retention policy as out-of-date table versions, and will be cleaned up through VACUUM if they are outside the specified retention period.\n\n### When do new records become available in the change data feed?\n\nChange data is committed along with the Delta Lake transaction, and will become available at the same time as the new data is available in the table.\n"
  },
  {
    "path": "docs/src/content/docs/delta-clustering.mdx",
    "content": "---\ntitle: Use liquid clustering for Delta tables\ndescription: Learn about liquid clustering in Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\nLiquid clustering improves the existing partitioning and `ZORDER` techniques by simplifying data layout decisions in order to optimize query performance. Liquid clustering provides flexibility to redefine clustering columns without rewriting existing data, allowing data layout to evolve alongside analytic needs over time.\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 3.1.0 and above. See\n  [Limitations](#limitations).\n</Aside>\n\n## What is liquid clustering used for?\n\nThe following are examples of scenarios that benefit from clustering:\n\n- Tables often filtered by high cardinality columns.\n- Tables with significant skew in data distribution.\n- Tables that grow quickly and require maintenance and tuning effort.\n- Tables with access patterns that change over time.\n- Tables where a typical partition column could leave the table with too many or too few partitions.\n\n## Enable liquid clustering\n\nYou can enable liquid clustering on an existing table or during table creation. Clustering is not compatible with partitioning or `ZORDER`. Once enabled, run `OPTIMIZE` jobs as usual to incrementally cluster data. See [How to trigger clustering](#how-to-trigger-clustering).\n\nTo enable liquid clustering, add the `CLUSTER BY` phrase to a table creation statement, as in the examples below:\n\n<Aside type=\"note\">\n  In Delta Lake 3.2 and above, you can use DeltaTable API in Python or Scala to\n  enable liquid clustering.\n</Aside>\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    -- Create an empty table\n    CREATE TABLE table1(col0 int, col1 string) USING DELTA CLUSTER BY (col0);\n\n    -- Using a CTAS statement (Delta 3.3+)\n    CREATE EXTERNAL TABLE table2 CLUSTER BY (col0)  -- specify clustering after table name, not in subquery\n    LOCATION 'table_location'\n    AS SELECT * FROM table1;\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    # Create an empty table\n    DeltaTable.create()\n      .tableName(\"table1\")\n      .addColumn(\"col0\", dataType = \"INT\")\n      .addColumn(\"col1\", dataType = \"STRING\")\n      .clusterBy(\"col0\")\n      .execute()\n    ```\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    // Create an empty table\n    DeltaTable.create()\n      .tableName(\"table1\")\n      .addColumn(\"col0\", dataType = \"INT\")\n      .addColumn(\"col1\", dataType = \"STRING\")\n      .clusterBy(\"col0\")\n      .execute()\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside\n  type=\"caution\"\n  title=\"Warning\"\n>\n  Tables created with liquid clustering have `Clustering` and `DomainMetadata`\n  table features enabled (both writer features) and use Delta writer version 7\n  and reader version 1. Table protocol versions cannot be downgraded. See [How\n  does Delta Lake manage feature compatibility?](/versioning/).\n</Aside>\n\nIn Delta Lake 3.3 and above you can enable liquid clustering on an existing unpartitioned Delta table using the following syntax:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name>\n    CLUSTER BY (<clustering_columns>)\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Default behavior does not apply clustering to previously written data. To\n  force reclustering for all records, you must use `OPTIMIZE FULL`. See\n  [Recluster entire table](#recluster-entire-table).\n</Aside>\n\n## Choose clustering columns\n\nClustering columns can be defined in any order. If two columns are correlated, you only need to add one of them as a clustering column.\n\nIf you're converting an existing table, consider the following recommendations:\n\n| Current data optimization technique | Recommendation for clustering columns |\n| --- | --- |\n| Hive-style partitioning | Use partition columns as clustering columns. |\n| Z-order indexing | Use the `ZORDER BY` columns as clustering columns. |\n| Hive-style partitioning and Z-order | Use both partition columns and `ZORDER BY` columns as clustering columns. |\n| Generated columns to reduce cardinality (for example, date for a timestamp) | Use the original column as a clustering column, and don't create a generated column. |\n\n## Write data to a clustered table\n\nYou must use a Delta writer client that supports `Clustering` and `DomainMetadata` table features.\n\n## How to trigger clustering\n\nUse the `OPTIMIZE` command on your table, as in the following example:\n\n<Tabs>\n  <TabItem label=\"SQL\">```sql OPTIMIZE table_name; ```</TabItem>\n</Tabs>\n\nLiquid clustering is incremental, meaning that data is only rewritten as necessary to accommodate data that needs to be clustered. Already clustered data files with different clustering columns are not rewritten.\n\n### Recluster entire table\n\nIn Delta Lake 3.3 and above, you can force reclustering of all records in a table with the following syntax:\n\n<Tabs>\n  <TabItem label=\"SQL\">```sql OPTIMIZE table_name FULL; ```</TabItem>\n</Tabs>\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Running `OPTIMIZE FULL` reclusters all existing data as necessary. For large\n  tables that have not previously been clustered on the specified columns, this\n  operation might take hours.\n</Aside>\n\nRun `OPTIMIZE FULL` when you change clustering columns. If you have previously run `OPTIMIZE FULL` and there has been no change to clustering columns, `OPTIMIZE FULL` runs the same as `OPTIMIZE`. Always use `OPTIMIZE FULL` to ensure that data layout reflects the current clustering columns.\n\n## Read data from a clustered table\n\nYou can read data in a clustered table using any Delta Lake client. For best query results, include clustering columns in your query filters, as in the following example:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql SELECT * FROM table_name WHERE clustering_column_name = \"some_value\";\n    ```\n  </TabItem>\n</Tabs>\n\n## Change clustering columns\n\nYou can change clustering columns for a table at any time by running an `ALTER TABLE` command, as in the following example:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql ALTER TABLE table_name CLUSTER BY (new_column1, new_column2); ```\n  </TabItem>\n</Tabs>\n\nWhen you change clustering columns, subsequent `OPTIMIZE` and write operations use the new clustering approach, but existing data is not rewritten.\n\nYou can also turn off clustering by setting the columns to `NONE`, as in the following example:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql ALTER TABLE table_name CLUSTER BY NONE; ```\n  </TabItem>\n</Tabs>\n\nSetting cluster columns to `NONE` does not rewrite data that has already been clustered, but prevents future `OPTIMIZE` operations from using clustering columns.\n\n## See how table is clustered\n\nYou can use `DESCRIBE DETAIL` commands to see the clustering columns for a table, as in the following examples:\n\n<Tabs>\n  <TabItem label=\"SQL\">```sql DESCRIBE DETAIL table_name; ```</TabItem>\n</Tabs>\n\n## Limitations\n\nThe following limitations exist:\n\n- You can only specify columns with statistics collected for clustering columns. By default, the first 32 columns in a Delta table have statistics collected.\n- You can specify up to 4 clustering columns.\n\n<Aside type=\"caution\" title=\"Important\">\nIn Delta Lake 3.1, users needs to enable the feature flag `spark.databricks.delta.clusteredTable.enableClusteringTablePreview` to use liquid clustering. The following features are not supported in this preview:\n\n- ZCube based incremental clustering\n- `ALTER TABLE ... CLUSTER BY` to change clustering columns\n- `DESCRIBE DETAIL` to inspect the current clustering columns\n\nIn Delta Lake 3.2, the preview flag is removed and the above features are supported.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/delta-column-mapping.mdx",
    "content": "---\ntitle: Delta column mapping\ndescription: Learn about column mapping in Delta.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 1.2.0 and above. This feature is\n  currently experimental with [known limitations](#known-limitations).\n</Aside>\n\n[Column mapping feature](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#column-mapping) allows Delta table columns and the underlying Parquet file columns to use different names. This enables Delta schema evolution operations such as `RENAME COLUMN` and `DROP COLUMNS` on a Delta table without the need to rewrite the underlying Parquet files. It also allows users to name Delta table columns by using [characters that are not allowed](#supported-characters-in-column-names) by Parquet, such as spaces, so that users can directly ingest CSV or JSON data into Delta without the need to rename columns due to previous character constraints.\n\n## How to enable Delta Lake column mapping\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Enabling column mapping for a table upgrades the Delta [table\n  version](/versioning/#what-is-a-protocol-version). This protocol upgrade is\n  irreversible. Tables with column mapping enabled can only be read in Delta\n  Lake 1.2 and above.\n</Aside>\n\nColumn mapping requires the following Delta protocols:\n\n- Reader version 2 or above.\n- Writer version 5 or above.\n\nFor a Delta table with the required protocol versions, you can enable column mapping by setting `delta.columnMapping.mode` to `name`.\n\nYou can use the following command to upgrade the table version and enable column mapping:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> SET TBLPROPERTIES (\n      'delta.minReaderVersion' = '2',\n      'delta.minWriterVersion' = '5',\n      'delta.columnMapping.mode' = 'name'\n    )\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  You cannot turn off column mapping after you enable it. If you try to set\n  `'delta.columnMapping.mode' = 'none'`, you'll get an error.\n</Aside>\n\n## Rename a column\n\nWhen column mapping is enabled for a Delta table, you can rename a column:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> RENAME COLUMN old_col_name TO new_col_name\n    ```\n  </TabItem>\n</Tabs>\n\nFor more examples, see [Rename columns](/delta-batch/#rename-columns).\n\n## Drop columns\n\nWhen column mapping is enabled for a Delta table, you can drop one or more columns:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n```sql\nALTER TABLE table_name DROP COLUMN col_name;\n\nALTER TABLE table_name DROP COLUMNS (col_name_1, col_name_2, ...);\n\n```\n  </TabItem>\n</Tabs>\n\nFor more details, see\n[Drop columns](/delta-batch/#drop-columns).\n\n## Supported characters in column names\n\nWhen column mapping is enabled for a Delta table, you can include spaces as well\nas any of these characters in the table's column names: `,;{}()\\n\\t=`.\n\n## Known limitations\n\n- Enabling column mapping on tables might break downstream operations that rely\n  on Delta change data feed. See\n  [Change data feed limitations for tables with column mapping enabled](/delta-change-data-feed/#change-data-feed-limitations-for-tables-with-column-mapping-enabled).\n- In Delta Lake 2.1 and below,\n  [Spark Structured Streaming](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html)\n  reads are explicitly blocked on a column mapping enabled table.\n- In Delta Lake 2.2 and above,\n  [Spark Structured Streaming](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html)\n  reads are explicitly blocked on a column mapping enabled table that underwent\n  column renaming or column dropping.\n- In Delta Lake 3.0 and above,\n  [Spark Structured Streaming](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html)\n  reads require schema tracking to be enabled on a column mapping enabled table\n  that underwent column renaming or column dropping. See\n  [Tracking non-additive schema changes](/delta-streaming/#schema-tracking)\n- The Delta table protocol specifies two modes of column mapping, by `name` and\n  by `id`. Delta Lake 2.1 and below do not support `id` mode.\n```\n"
  },
  {
    "path": "docs/src/content/docs/delta-constraints.mdx",
    "content": "---\ntitle: Constraints\ndescription: Learn how Delta tables apply constraints.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nDelta tables support standard SQL constraint management clauses that ensure that the quality and integrity of data added to a table is automatically verified. When a constraint is violated, Delta Lake throws an `InvariantViolationException` to signal that the new data can't be added.\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Adding a constraint automatically upgrades the table writer protocol version.\n  See [How does Delta Lake manage feature compatibility?](/versioning/) to\n  understand table protocol versioning and what it means to upgrade the protocol\n  version.\n</Aside>\n\nTwo types of constraints are supported:\n\n- `NOT NULL`: indicates that values in specific columns cannot be null.\n- `CHECK`: indicates that a specified Boolean expression must be true for each input row.\n\n### `NOT NULL` constraint\n\nYou specify `NOT NULL` constraints in the schema when you create a table and drop `NOT NULL` constraints using the `ALTER TABLE CHANGE COLUMN` command.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n\n    ```sql\n    CREATE TABLE default.people10m (\n        id INT NOT NULL,\n        firstName STRING,\n        middleName STRING NOT NULL,\n        lastName STRING,\n        gender STRING,\n        birthDate TIMESTAMP,\n        ssn STRING,\n        salary INT\n    ) USING DELTA;\n\n    ALTER TABLE default.people10m CHANGE COLUMN middleName DROP NOT NULL;\n    ```\n\n  </TabItem>\n</Tabs>\n\nIf you specify a `NOT NULL` constraint on a column nested within a struct, the parent struct is also constrained to not be null. However, columns nested within array or map types do not accept `NOT NULL` constraints.\n\n### `CHECK` constraint\n\nYou manage `CHECK` constraints using the `ALTER TABLE ADD CONSTRAINT` and `ALTER TABLE DROP CONSTRAINT` commands. `ALTER TABLE ADD CONSTRAINT` verifies that all existing rows satisfy the constraint before adding it to the table.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n\n    ```sql\n    CREATE TABLE default.people10m (\n       id INT,\n       firstName STRING,\n       middleName STRING,\n       lastName STRING,\n       gender STRING,\n       birthDate TIMESTAMP,\n       ssn STRING,\n       salary INT\n    ) USING DELTA;\n\n    ALTER TABLE default.people10m ADD CONSTRAINT dateWithinRange CHECK (birthDate > '1900-01-01');\n    ALTER TABLE default.people10m DROP CONSTRAINT dateWithinRange;\n    ```\n\n  </TabItem>\n</Tabs>\n\n`CHECK` constraints are table properties in the output of the `DESCRIBE DETAIL` and `SHOW TBLPROPERTIES` commands.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n\n    ```sql\n    ALTER TABLE default.people10m ADD CONSTRAINT validIds CHECK (id > 1 and id < 99999999);\n\n    DESCRIBE DETAIL default.people10m;\n\n    SHOW TBLPROPERTIES default.people10m;\n    ```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content/docs/delta-default-columns.mdx",
    "content": "---\ntitle: Delta default column values\ndescription: Learn about default column values in Delta.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 3.1.0 and above and is enabled using\n  the `allowColumnDefaults` writer [table\n  feature](/versioning/#what-are-table-features).\n</Aside>\n\nDelta enables the specification of [default expressions](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#default-columns) for columns in Delta tables. When users write to these tables without explicitly providing values for certain columns, or when they explicitly use the DEFAULT SQL keyword for a column, Delta automatically generates default values for those columns.\n\nThis information is stored in the [StructField](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#struct-field) corresponding to the column of interest.\n\n## How to enable Delta Lake default column values\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Enabling default column values for a Delta table will upgrade its [protocol\n  version](/versioning/#what-is-a-protocol-version) to support [table\n  features](/versioning/#what-are-table-features). This protocol upgrade is\n  irreversible. Tables with default column values enabled can only be written to\n  in Delta Lake 3.1 and above.\n</Aside>\n\nYou can enable default column values for a table by setting `delta.feature.allowColumnDefaults` to `enabled`:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> SET TBLPROPERTIES (\n      'delta.feature.allowColumnDefaults' = 'enabled'\n    )\n    ```\n  </TabItem>\n</Tabs>\n\n## How to use default columns in SQL commands\n\n- For SQL commands that perform table writes, such as `INSERT`, `UPDATE`, and `MERGE` commands, the `DEFAULT` keyword resolves to the most recently assigned default value for the corresponding column (or NULL if no default value exists). For instance, the following SQL command will use the default value for the second column in the table: `INSERT INTO t VALUES (16, DEFAULT);`\n\n- It is also possible for INSERT commands to specify lists of fewer columns than the target table, in which case the engine will assign default values for the remaining columns (or NULL for any columns where no defaults yet exist).\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  The metadata discussed here apply solely to write operations, not read\n  operations.\n</Aside>\n\n- The `ALTER TABLE ... ADD COLUMN` command that introduces a new column to an existing table may not specify a default value for the new column. For instance, the following SQL command is not supported in Delta Lake: `ALTER TABLE t ADD COLUMN c INT DEFAULT 16;`\n\n- It is permissible, however, to assign or update default values for columns that were created in previous commands. For example, the following SQL command is valid: `ALTER TABLE t ALTER COLUMN c SET DEFAULT 16;`\n"
  },
  {
    "path": "docs/src/content/docs/delta-deletion-vectors.mdx",
    "content": "---\ntitle: What are deletion vectors?\ndescription: Learn about deletion vectors in Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 2.3.0 and above. This feature is in\n  experimental support mode.\n</Aside>\n\nDeletion vectors are a storage optimization feature that can be enabled on Delta Lake tables. By default, when a single row in a data file is deleted, the entire Parquet file containing the record must be rewritten. With deletion vectors enabled for the table, some Delta operations use deletion vectors to mark existing rows as removed without rewriting the Parquet file. Subsequent reads on the table resolve current table state by applying the deletions noted by deletion vectors to the most recent table version.\n\nSupport for deletion vectors was incrementally added with each Delta Lake version. The table below depicts the supported operations for each Delta Lake version.\n\n| Operation | First available Delta Lake version | Enabled by default since Delta Lake version |\n| --- | --- | --- |\n| `SCAN` | 2.3.0 | 2.3.0 |\n| `DELETE` | 2.4.0 | 2.4.0 |\n| `UPDATE` | 3.0.0 | 3.1.0 |\n| `MERGE` | 3.1.0 | 3.1.0 |\n\n## Enable deletion vectors\n\nYou enable support for deletion vectors on a Delta Lake table by setting a Delta Lake table property:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> SET TBLPROPERTIES('delta.enableDeletionVectors' = true);\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside type=\"caution\" title=\"Warning\">\nWhen you enable deletion vectors, the table protocol version is upgraded. After upgrading, the table will not be readable by Delta Lake clients that do not support deletion vectors. See [How does Delta Lake manage feature compatibility?](/versioning/).\n\nIn Delta Lake 3.0 and above, you can drop the deletion vectors table feature to enable compatibility with other Delta clients. See [Drop Delta table features](/delta-drop-feature/).\n\n</Aside>\n\n## Apply changes to Parquet data files\n\nDeletion vectors indicate changes to rows as soft-deletes that logically modify existing Parquet data files in the Delta Lake tables. These changes are applied physically when data files are rewritten, as triggered by one of the following events:\n\n- A DML command with deletion vectors disabled (by a command flag or a table property) is run on the table.\n- An `OPTIMIZE` command is run on the table.\n- `REORG TABLE ... APPLY (PURGE)` is run against the table.\n\n`UPDATE`, `MERGE`, and `OPTIMIZE` do not have strict guarantees for resolving changes recorded in deletion vectors, and some changes recorded in deletion vectors might not be applied if target data files contain no updated records, or would not otherwise be candidates for file compaction. `REORG TABLE ... APPLY (PURGE)` rewrites all data files containing records with modifications recorded using deletion vectors. See [Apply changes with REORG TABLE](#apply-changes-with-reorg-table)\n\n<Aside type=\"note\">\n  Modified data might still exist in the old files. You can run `VACUUM` to\n  physically delete the old files. `REORG TABLE ... APPLY (PURGE)` creates a new\n  version of the table at the time it completes, which is the timestamp you must\n  consider for the retention threshold for your `VACUUM` operation to fully\n  remove deleted files.\n</Aside>\n\n### Apply changes with REORG TABLE\n\nReorganize a Delta Lake table by rewriting files to purge soft-deleted data, such as rows marked as deleted by deletion vectors with `REORG TABLE`:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    REORG TABLE events APPLY (PURGE);\n\n    -- If you have a large amount of data and only want to purge a subset of it, you can specify an optional partition predicate using `WHERE`:\n    REORG TABLE events WHERE date >= '2022-01-01' APPLY (PURGE);\n\n    REORG TABLE events\n      WHERE date >= current_timestamp() - INTERVAL '1' DAY\n      APPLY (PURGE);\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  - `REORG TABLE` only rewrites files that contain soft-deleted data. - When\n  resulting files of the purge are small, `REORG TABLE` will coalesce them into\n  larger ones. See [OPTIMIZE](/optimizations-oss/) for more info. - `REORG\n  TABLE` is *idempotent*, meaning that if it is run twice on the same dataset,\n  the second run has no effect. - After running `REORG TABLE`, the soft-deleted\n  data may still exist in the old files. You can run\n  [VACUUM](/delta-utility/#vacuum) to physically delete the old files.\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/delta-drop-feature.mdx",
    "content": "---\ntitle: Drop Delta table features\ndescription: Learn how to drop table features in Delta Lake to downgrade reader and writer protocol requirements and resolve compatibility issues.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\nThis article describes how to drop Delta Lake table features and downgrade protocol versions.\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 4.0.0 and above. A legacy\n  implementation of the feature is available since Delta Lake 3.0.0. Not all\n  Delta table features can be dropped. See [What Delta table features can be\n  dropped?](#what-delta-table-features-can-be-dropped)\n</Aside>\n\nYou should only use this functionality to support compatibility with earlier Delta Lake versions, Delta Sharing, or other Delta Lake reader or writer clients.\n\n## How can I drop a Delta table feature?\n\nTo remove a Delta table feature, you run an `ALTER TABLE <table-name> DROP FEATURE <feature-name>` command.\n\n## What Delta table features can be dropped?\n\nYou can drop the following Delta table features:\n\n- `deletionVectors`. See [What are deletion vectors?](/delta-deletion-vectors/). Drop support for deletion vectors is available in Delta Lake 4.0.0 and above.\n- `typeWidening-preview`. See [Delta type widening](/delta-type-widening/). Type widening is available in preview in Delta Lake 3.2.0 and above.\n- `typeWidening`. See [Delta type widening](/delta-type-widening/). Type widening is available in preview in Delta Lake 4.0.0 and above.\n- `v2Checkpoint`. See [V2 Checkpoint Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#v2-spec). Drop support for V2 Checkpoints is available in Delta Lake 3.1.0 and above.\n- `columnMapping`. See [Delta column mapping](/delta-column-mapping/). Drop support for column mapping is available in Delta Lake 3.3.0 and above.\n- `vacuumProtocolCheck`. See [Vacuum Protocol Check Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#vacuum-protocol-check). Drop support for vacuum protocol check is available in Delta Lake 3.3.0 and above.\n- `checkConstraints`. See [Constraints](/delta-constraints/). Drop support for check constraints is available in Delta Lake 3.3.0 and above.\n- `inCommitTimestamp`. See [In-Commit Timestamps](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#in-commit-timestamps). Drop support for In-Commit Timestamp is available in Delta Lake 3.3.0 and above.\n- `checkpointProtection`. See [Checkpoint Protection Spec](https://github.com/delta-io/delta/blob/master/protocol_rfcs/checkpoint-protection.md). Drop support for checkpoint protection is available in Delta Lake 4.0.0 and above.\n\nYou cannot drop other [Delta table features](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#valid-feature-names-in-table-features).\n\n## What happens when a table feature is dropped?\n\nWhen you drop a table feature, Delta Lake performs a series of atomic operations:\n\n- Disable table properties that use the table feature.\n- Rewrite data files as necessary to remove all traces of the table feature from the data files backing the table in the current version.\n- Create a set of protected checkpoints that allow reader clients to interpret table history correctly.\n- Add the writer table feature checkpointProtection to the table protocol.\n- Downgrade the table protocol to the lowest reader and writer versions that support all remaining table features.\n\n## What is the checkpointProtection table feature?\n\nWhen you drop a feature, Delta Lake rewrites data and metadata in the table's history as protected checkpoints to respect the protocol downgrade. After the downgrade, the table should always be readable by more clients. This is because the protocol for the table now reflects that support for the dropped feature is no longer required to read the table. The protected checkpoints and the checkpointProtection feature accomplish the following:\n\n- Reader clients that understand the dropped table feature can access all available table history.\n- Reader clients that do not support the dropped table feature only need to read the table history starting from the protocol downgrade version.\n- Writer clients do not rewrite checkpoints prior to the protocol downgrade.\n- Table maintenance operations respect requirements set by `checkpointProtection`, which mark protocol downgrade checkpoints as protected.\n- While you can only drop one table feature with each DROP FEATURE command, a table can have multiple protected checkpoints and dropped features in its table history.\n\nThe table feature `checkpointProtection` should not block read-only access from Delta Lake clients. To fully downgrade the table and remove the `checkpointProtection` table feature, you must use TRUNCATE HISTORY. The recommendation is to only use this pattern if you need to write to tables with external Delta clients that do not support checkpointProtection.\n\n## Fully downgrade table protocols for legacy clients\n\nIf integrations with external Delta Lake clients require writes that don't support the checkpointProtection table feature, you must use TRUNCATE HISTORY to fully remove all traces of the disabled table features and fully downgrade the table protocol.\n\nIt is recommended to always test the default behavior for DROP FEATURE before proceeding with TRUNCATE HISTORY. Running TRUNCATE HISTORY removes all table history greater than 24 hours.\n\nFull table downgrade occurs in two steps that must occur at least 24 hours apart.\n\n### Step 1: Prepare to drop a table feature\n\nDuring the first stage, the user prepares to drop the table feature. The following describes what happens during this stage:\n\n1. You run the `ALTER TABLE <table-name> DROP FEATURE <feature-name> TRUNCATE HISTORY` command.\n2. Table properties that specifically enable a table feature have values set to disable the feature.\n3. Table properties that control behaviors associated with the dropped feature have options set to default values before the feature was introduced.\n4. As necessary, data and metadata files are rewritten respecting the updated table properties.\n5. The command finishes running and returns an error message informing the user they must wait 24 hours to proceed with feature removal.\n\nAfter first disabling a feature, you can continue writing to the target table before completing the protocol downgrade, but you cannot use the table feature you are removing.\n\n<Aside type=\"note\">\n  If you leave the table in this state, operations against the table do not use\n  the table feature, but the protocol still supports the table feature. Until\n  you complete the final downgrade step, the table is not readable by Delta\n  clients that do not understand the table feature.\n</Aside>\n\n### Step 2: Downgrade the protocol and drop a table feature\n\nTo fully remove all transaction history associated with the feature and downgrade the protocol:\n\n1. After at least 24 hours have passed, you run the `ALTER TABLE <table-name> DROP FEATURE <feature-name> TRUNCATE HISTORY` command.\n2. The client confirms that no transactions in the specified retention threshold use the table feature, then truncates the table history to that threshold.\n3. The protocol is downgraded, dropping the table feature.\n4. If the table features that are present in the table can be represented by a legacy protocol version, the `minReaderVersion` and `minWriterVersion` for the table are downgraded to the lowest version that supports exactly all remaining features in use by the Delta table.\n\n<Aside type=\"caution\" title=\"Important\">\n  Running `ALTER TABLE <table-name> DROP FEATURE <feature-name> TRUNCATE HISTORY` removes all transaction log data older than 24 hours. After dropping a Delta table feature, you do not have access to table history or time travel.\n</Aside>\n\nSee [How does Delta Lake manage feature compatibility?](/versioning/).\n"
  },
  {
    "path": "docs/src/content/docs/delta-faq.mdx",
    "content": "---\ntitle: Frequently asked questions (FAQ)\ndescription: Find answers to commonly asked questions about Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n## What is Delta Lake?\n\n[Delta Lake](https://delta.io/) is an [open source storage layer](https://github.com/delta-io/delta) that brings reliability to [data lakes](https://databricks.com/discover/data-lakes/introduction). Delta Lake provides ACID transactions, scalable metadata handling, and unifies streaming and batch data processing. Delta Lake runs on top of your existing data lake and is fully compatible with Apache Spark APIs.\n\n## How is Delta Lake related to Apache Spark?\n\nDelta Lake sits on top of Apache Spark. The format and the compute layer helps to simplify building big data pipelines and increase the overall efficiency of your pipelines.\n\n## What format does Delta Lake use to store data?\n\nDelta Lake uses versioned Parquet files to store your data in your cloud storage. Apart from the versions, Delta Lake also stores a transaction log to keep track of all the commits made to the table or blob store directory to provide ACID transactions.\n\n## How can I read and write data with Delta Lake?\n\nYou can use your favorite Apache Spark APIs to read and write data with Delta Lake. See [Read a table](/delta-batch/#read-a-table) and [Write to a table](/delta-batch/#write-to-table).\n\n## Where does Delta Lake store the data?\n\nWhen writing data, you can specify the location in your cloud storage. Delta Lake stores the data in that location in Parquet format.\n\n## Can I copy my Delta Lake table to another location?\n\nYes you can copy your Delta Lake table to another location. Remember to copy files without changing the timestamps to ensure that the time travel with timestamps will be consistent.\n\n## Can I stream data directly into and from Delta tables?\n\nYes, you can use Structured Streaming to directly write data into Delta tables and read from Delta tables. See [Stream data into Delta tables](/delta-streaming/#delta-table-as-a-sink) and [Stream data from Delta tables](/delta-streaming/#delta-table-as-a-source).\n\n## Does Delta Lake support writes or reads using the Spark Streaming DStream API?\n\nDelta does not support the DStream API. We recommend [Table streaming reads and writes](/delta-streaming/).\n\n## When I use Delta Lake, will I be able to port my code to other Spark platforms easily?\n\nYes. When you use Delta Lake, you are using open Apache Spark APIs so you can easily port your code to other Spark platforms. To port your code, replace `delta` format with `parquet` format.\n\n## Does Delta Lake support multi-table transactions?\n\nDelta Lake does not support multi-table transactions and foreign keys. Delta Lake supports transactions at the _table_ level.\n\n## How can I change the type of a column?\n\nChanging a column's type or dropping a column requires rewriting the table. For an example, see [Change column type](/delta-batch/#change-column-type-or-name).\n"
  },
  {
    "path": "docs/src/content/docs/delta-kernel-java.mdx",
    "content": "---\ntitle: Delta Kernel Java User Guide\ndescription: Learn how to build connectors to read and write Delta tables using Delta Kernel Java.\n---\n\nimport { Tabs, TabItem, Aside, Steps } from \"@astrojs/starlight/components\";\n\n## What is Delta Kernel?\nDelta Kernel is a library for operating on Delta tables. Specifically, it provides simple and narrow APIs for reading and writing to Delta tables without the need to understand the [Delta protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md) details. You can use this library to do the following:\n\n* Read and write Delta tables from your applications.\n* Build a connector for a distributed engine like [Apache Spark™](https://github.com/apache/spark), [Apache Flink](https://github.com/apache/flink), or [Trino](https://github.com/trinodb/trino) for reading or writing massive Delta tables.\n\n## Set up Delta Kernel for your project\nYou need to `io.delta:delta-kernel-api` and `io.delta:delta-kernel-defaults` dependencies. Following is an example Maven `pom` file dependency list.\n\nThe `delta-kernel-api` module contains the core of the Kernel that abstracts out the Delta protocol to enable reading and writing into Delta tables. It makes use of the `Engine` interface that is being passed to the Kernel API by the connector for heavy-lift operations such as reading/writing Parquet or JSON files, evaluating expressions or file system operations such as listing contents of the Delta Log directory, etc. Kernel supplies a default implementation of `Engine` in module `delta-kernel-defaults`. The connectors can implement their own version of `Engine` to make use of their native implementation of functionalities the `Engine` provides. For example: the connector can make use of their Parquet reader instead of using the reader from the `DefaultEngine`. More details on this [later](#step-2-build-your-own-engine).\n\n```xml\n<dependencies>\n  <dependency>\n    <groupId>io.delta</groupId>\n    <artifactId>delta-kernel-api</artifactId>\n    <version>${delta-kernel.version}</version>\n  </dependency>\n\n  <dependency>\n    <groupId>io.delta</groupId>\n    <artifactId>delta-kernel-defaults</artifactId>\n    <version>${delta-kernel.version}</version>\n  </dependency>\n</dependencies>\n```\n\nIf your connector is not using the [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) provided by the Kernel, the dependency `delta-kernel-defaults` from the above list can be skipped.\n\n\n## Read a Delta table in a single process\nIn this section, we will walk through how to build a very simple single-process Delta connector that can read a Delta table using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\n### Step 1: Full scan on a Delta table\nThe main entry point is [`io.delta.kernel.Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html) which is a programmatic representation of a Delta table. Say you have a Delta table at the directory `myTablePath`. You can create a `Table` object as follows:\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.*;\nimport org.apache.hadoop.conf.Configuration;\n\nString myTablePath = <my-table-path>; // fully qualified table path. Ex: file:/user/tables/myTable\nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n```\n\nNote the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) we are creating to bootstrap the `myTable` object. This object allows you to plug in your own libraries for computationally intensive operations like Parquet file reading, JSON parsing, etc. You can ignore it for now. We will discuss more about this later when we discuss how to build more complex connectors for distributed processing engines.\n\nFrom this `myTable` object you can create a [`Snapshot`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Snapshot.html) object which represents the consistent state (a.k.a. a snapshot consistency) in a specific version of the table.\n\n```java\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\n```\n\nNow that we have a consistent snapshot view of the table, we can query more details about the table. For example, you can get the version and schema of this snapshot.\n\n```java\nlong version = mySnapshot.getVersion();\nStructType tableSchema = mySnapshot.getSchema();\n```\n\nNext, to read the table data, we have to *build* a [`Scan`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html) object. In order to build a `Scan` object, create a [`ScanBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/ScanBuilder.html) object which optionally allows selecting a subset of columns to read or setting a query filter. For now, ignore these optional settings.\n\n```java\nScan myScan = mySnapshot.getScanBuilder().build()\n\n// Common information about scanning for all data files to read.\nRow scanState = myScan.getScanState(myEngine)\n\n// Information about the list of scan files to read\nCloseableIterator<FilteredColumnarBatch> scanFiles = myScan.getScanFiles(myEngine)\n```\n\nThis [`Scan`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html) object has all the necessary metadata to start reading the table. There are two crucial pieces of information needed for reading data from a file in the table.\n\n* `myScan.getScanFiles(Engine)`:  Returns scan files as columnar batches (represented as an iterator of `FilteredColumnarBatch`es, more on that later) where each selected row in the batch has information about a single file containing the table data.\n* `myScan.getScanState(Engine)`: Returns the snapshot-level information needed for reading any file. Note that this is a single row and common to all scan files.\n\nFor each scan file the physical data must be read from the file. The columns to read are specified in the scan file state. Once the physical data is read, you have to call [`ScanFile.transformPhysicalData(...)`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html#transformPhysicalData-io.delta.kernel.engine.Engine-io.delta.kernel.data.Row-io.delta.kernel.data.Row-io.delta.kernel.utils.CloseableIterator-) with the scan state and the physical data read from scan file. This API takes care of transforming (e.g. adding partition columns) the physical data into logical data of the table. Here is an example of reading all the table data in a single thread.\n\n```java\nCloserableIterator<FilteredColumnarBatch> fileIter = scanObject.getScanFiles(myEngine);\n\nRow scanStateRow = scanObject.getScanState(myEngine);\n\nwhile(fileIter.hasNext()) {\n  FilteredColumnarBatch scanFileColumnarBatch = fileIter.next();\n\n  // Get the physical read schema of columns to read from the Parquet data files\n  StructType physicalReadSchema =\n    ScanStateRow.getPhysicalDataReadSchema(engine, scanStateRow);\n\n  try (CloseableIterator<Row> scanFileRows = scanFileColumnarBatch.getRows()) {\n    while (scanFileRows.hasNext()) {\n      Row scanFileRow = scanFileRows.next();\n\n      // From the scan file row, extract the file path, size and modification time metadata\n      // needed to read the file.\n      FileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow);\n\n      // Open the scan file which is a Parquet file using connector's own\n      // Parquet reader or default Parquet reader provided by the Kernel (which\n      // is used in this example).\n      CloseableIterator<ColumnarBatch> physicalDataIter =\n        engine.getParquetHandler().readParquetFiles(\n          singletonCloseableIterator(fileStatus),\n          physicalReadSchema,\n          Optional.empty() /* optional predicate the connector can apply to filter data from the reader */\n        );\n\n      // Now the physical data read from the Parquet data file is converted to a table\n      // logical data. Logical data may include the addition of partition columns and/or\n      // subset of rows deleted\n      try (\n         CloseableIterator<FilteredColumnarBatch> transformedData =\n           Scan.transformPhysicalData(\n             engine,\n             scanStateRow,\n             scanFileRow,\n             physicalDataIter)) {\n        while (transformedData.hasNext()) {\n          FilteredColumnarBatch logicalData = transformedData.next();\n          ColumnarBatch dataBatch = logicalData.getData();\n\n          // Not all rows in `dataBatch` are in the selected output.\n          // An optional selection vector determines whether a row with a\n          // specific row index is in the final output or not.\n          Optional<ColumnVector> selectionVector = dataReadResult.getSelectionVector();\n\n          // access the data for the column at ordinal 0\n          ColumnVector column0 = dataBatch.getColumnVector(0);\n          for (int rowIndex = 0; rowIndex < column0.getSize(); rowIndex++) {\n            // check if the row is selected or not\n            if (!selectionVector.isPresent() || // there is no selection vector, all records are selected\n               (!selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId)))  {\n              // Assuming the column type is String.\n              // If it is a different type, call the relevant function on the `ColumnVector`\n              System.out.println(column0.getString(rowIndex));\n            }\n          }\n\n\t  // access the data for column at ordinal 1\n\t  ColumnVector column1 = dataBatch.getColumnVector(1);\n\t  for (int rowIndex = 0; rowIndex < column1.getSize(); rowIndex++) {\n            // check if the row is selected or not\n            if (!selectionVector.isPresent() || // there is no selection vector, all records are selected\n               (!selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId)))  {\n              // Assuming the column type is Long.\n              // If it is a different type, call the relevant function on the `ColumnVector`\n              System.out.println(column1.getLong(rowIndex));\n            }\n          }\n\t  // .. more ..\n        }\n      }\n    }\n  }\n}\n```\n\nA few working examples to read Delta tables within a single process are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n<Aside type=\"note\" title=\"important\">\n All the Delta protocol-level details are encoded in the rows returned by `Scan.getScanFiles` API, but you do not have to understand them in order to read the table data correctly. All you need is to get the Parquet file status from each scan file row and read the data from the Parquet file into the `ColumnarBatch` format. The physical data is converted into the logical data of the table using [`Scan.transformPhysicalData`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html#transformPhysicalData-io.delta.kernel.engine.Engine-io.delta.kernel.data.Row-io.delta.kernel.data.Row-io.delta.kernel.utils.CloseableIterator-). Transformation to logical data is dictated by the protocol and the metadata of the table and the scan file. As the Delta protocol evolves this transformation step will evolve with it and your code will not have to change to accommodate protocol changes. This is the major advantage of the abstractions provided by Delta Kernel.\n</Aside>\n\n<Aside type=\"note\">\n  Observe that the same <code>Engine</code> instance <code>myEngine</code> is passed multiple times whenever a call to Delta Kernel API is made. The reason for passing this instance for every call is because it is the connector context; it should be maintained outside of the Delta Kernel APIs to give the connector control over the <code>Engine</code>.\n</Aside>\n\n### Step 2: Improve scan performance with file skipping\nWe have explored how to do a full table scan. However, the real advantage of using the Delta format is that you can skip files using your query filters. To make this possible, Delta Kernel provides an [expression framework](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/package-summary.html) to encode your filters and provide them to Delta Kernel to skip files during the scan file generation. For example, say your table is partitioned by `columnX`, you want to query only the partition `columnX=1`. You can generate the expression and use it to build the scan as follows:\n\n```java\nimport io.delta.kernel.expressions.*;\n\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = DefaultEngine.create(new Configuration());\n\nPredicate filter = new Predicate(\n  \"=\",\n  Arrays.asList(new Column(\"columnX\"), Literal.ofInt(1)));\n\nScan myFilteredScan = mySnapshot.getScanBuilder().withFilter(filter).build()\n\n// Subset of the given filter that is not guaranteed to be satisfied by\n// Delta Kernel when it returns data. This filter is used by Delta Kernel\n// to do data skipping as much as possible. The connector should use this filter\n// on top of the data returned by Delta Kernel in order for further filtering.\nOptional<Predicate> remainingFilter = myFilteredScan.getRemainingFilter();\n```\n\nThe scan files returned by  `myFilteredScan.getScanFiles(myEngine)` will have rows representing files only of the required partition. Similarly, you can provide filters for non-partition columns, and if the data in the table is well clustered by those columns, then Delta Kernel will be able to skip files as much as possible.\n\n## Create a Delta table\nIn this section, we will walk through how to build a Delta connector that can create a Delta table using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\nThe main entry point is [`io.delta.kernel.Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html) which is a programmatic representation of a Delta table. Say you want to create Delta table at the directory `myTablePath`. You can create a `Table` object as follows:\n\n```java\npackage io.delta.kernel.examples;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterable;\n\nString myTablePath = <my-table-path>; \nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n```\n\nNote the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) we are creating to bootstrap the `myTable` object. This object allows you to plug in your own libraries for computationally intensive operations like Parquet file reading, JSON parsing, etc. You can ignore it for now. We will discuss more about this later when we discuss how to build more complex connectors for distributed processing engines.\n\nFrom this `myTable` object you can create a [`TransactionBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionBuilder.html) object which allows you to construct a [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Transaction.html) object\n\n```java\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */ \n    Operation.CREATE_TABLE /* What is the operation we are trying to perform. This is noted in the Delta Log */\n  );\n```\n\nNow that you have the `TransactionBuilder` object, you can set the table schema and partition columns of the table.\n\n```java\nStructType mySchema = new StructType()\n  .add(\"id\", IntegerType.INTEGER)\n  .add(\"name\", StringType.STRING)\n  .add(\"city\", StringType.STRING)\n  .add(\"salary\", DoubleType.DOUBLE);\n\n// Partition columns are optional. Use it only if you are creating a partitioned table.\nList<String> myPartitionColumns = Collections.singletonList(\"city\");\n\n// Set the schema of the new table on the transaction builder\ntxnBuilder = txnBuilder\n  .withSchema(engine, mySchema);\n\n// Set the partition columns of the new table only if you are creating\n// a partitioned table; otherwise, this step can be skipped.\ntxnBuilder = txnBuilder\n  .withPartitionColumns(engine, examplePartitionColumns);\n```\n\n`TransactionBuilder` allows setting additional properties of the table such as enabling a certain Delta feature or setting identifiers for idempotent writes. We will be visiting these in the next sections. The next step is to build `Transaction` out of the `TransactionBuilder` object.\n\n\n```java\n// Build the transaction\nTransaction txn = txnBuilder.build(engine);\n```\n\n`Transaction` object allows the connector to optionally add any data and finally commit the transaction. A successful commit ensures that the table is created with the given schema. In this example, we are just creating a table and not adding any data as part of the table.\n\n```java\n// Commit the transaction.\n// As we are just creating the table and not adding any data, the `dataActions` is empty.\nTransactionCommitResult commitResult =\n  txn.commit(\n    engine,\n    CloseableIterable.emptyIterable() /* dataActions */\n  );\n```\n\nThe [`TransactionCommitResult`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html) contains the what version the transaction is committed as and whether the table is ready for a checkpoint. As we are creating a table the version will be `0`. We will be discussing later on what a checkpoint is and what it means for the table to be ready for the checkpoint.\n\nA few working examples to create partitioned and un-partitioned Delta tables are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n## Create a table and insert data into it\nIn this section, we will walk through how to build a Delta connector that can create a Delta table and insert data into the table (similar to `CREATE TABLE <table> AS <query>` construct in SQL) using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\nThe first step is to construct a `Transaction`. Below is the code for that. For more details on what each step of the code means, please read the [create table](#create-a-delta-table) section.\n\n```\npackage io.delta.kernel.examples;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterable;\n\nString myTablePath = <my-table-path>; \nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n\nStructType mySchema = new StructType()\n  .add(\"id\", IntegerType.INTEGER)\n  .add(\"name\", StringType.STRING)\n  .add(\"city\", StringType.STRING)\n  .add(\"salary\", DoubleType.DOUBLE);\n\n// Partition columns are optional. Use it only if you are creating a partitioned table.\nList<String> myPartitionColumns = Collections.singletonList(\"city\");\n\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */ \n    Operation.WRITE /* What is the operation we are trying to perform? This is noted in the Delta Log */\n  );\n\n// Set the schema of the new table on the transaction builder\ntxnBuilder = txnBuilder\n  .withSchema(engine, mySchema);\n\n// Set the partition columns of the new table only if you are creating\n// a partitioned table; otherwise, this step can be skipped.\ntxnBuilder = txnBuilder\n  .withPartitionColumns(engine, examplePartitionColumns);\n\n// Build the transaction\nTransaction txn = txnBuilder.build(engine);\n```\n\nNow that we have the [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Transaction.html) object, the next step is generating the data that confirms the table schema and partitioned according to the table partitions.\n\n```java\nStructType dataSchema = txn.getSchema(engine)\n\n// Optional for un-partitioned tables\nList<String> partitionColumnNames = txn.getPartitionColumns(engine)\n```\n\nUsing the data schema and partition column names the connector can plan the query and generate data. At tasks that actually have the data to write to the table, the connector can ask the Kernel to transform the data given in the table schema into physical data that can actually be written to the Parquet data files. For partitioned tables, the data needs to be first partitioned by the partition columns, and then the connector should ask the Kernel to transform the data for each partition separately. The partitioning step is needed because any given data file in the Delta table contains data belonging to exactly one partition.\n\nGet the state of the transaction. The transaction state contains the information about how to convert the data in the table schema into physical data that needs to be written. The transformations depend on the protocol and features the table has.\n\n```java\nRow txnState = txn.getTransactionState(engine);\n```\n\nPrepare the data.\n\n```java\n// The data generated by the connector to write into a table\nCloseableIterator<FilteredColumnarBatch> data = ... \n\n// Create partition value map\nMap<String, Literal> partitionValues =\n  Collections.singletonMap(\n    \"city\", // partition column name\n     // partition value. Depending upon the partition column type, the\n     // partition value should be created. In this example, the partition\n     // column is of type StringType, so we are creating a string literal.\n     Literal.ofString(city)\n  );\n```\n\nThe connector data is passed as an iterator of [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html). Each of the `FilteredColumnarBatch` contains a [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) which actually contains the data in columnar access format and an optional section vector that allows the connector to specify which rows from the `ColumnarBatch` to write to the table.\n\nPartition values are passed as a map of the partition column name to the partition value. For an un-partitioned table, the map should be empty as it has no partition columns.\n\n```\n// Transform the logical data to physical data that needs to be written to the Parquet\n// files\nCloseableIterator<FilteredColumnarBatch> physicalData =\n  Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n```\n\nThe above code converts the given data for partitions into an iterator of `FilteredColumnarBatch` that needs to be written to the Parquet data files. In order to write the data files, the connector needs to get the [`WriteContext`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html) from Kernel, which tells the connector where to write the data files and what columns to collect statistics from each data file.\n\n```java\n// Get the write context\nDataWriteContext writeContext = Transaction.getWriteContext(engine, txnState, partitionValues);\n```\n\nNow, the connector has the physical data that needs to be written to Parquet data files, and where those files should be written, it can start writing the data files.\n\n```java\nCloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n  .writeParquetFiles(\n    writeContext.getTargetDirectory(),\n    physicalData,\n    writeContext.getStatisticsColumns()\n  );\n```\n\nIn the above code, the connector is making use of the `Engine` provided `ParquetHandler` to write the data, but the connector can choose its own Parquet file writer to write the data. Also note that the return of the above call is an iterator of [`DataFileStatus`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/DataFileStatus.html) for each data file written. It basically contains the file path, file metadata, and optional file-level statistics for columns specified by the [`WriteContext.getStatisticsColumns()`]([https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html#getStatisticsColumns--))\n\nConvert each `DataFileStatus` into a Delta log action that can be written to the Delta table log.\n\n```java\nCloseableIterator<Row> dataActions =\n  Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);\n```\n\nThe next step is constructing [`CloseableIterable`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/CloseableIterable.html) out of the all the Delta log actions generated above. The reason for constructing an `Iterable` is that the transaction committing involves accessing the list of Delta log actions more than one time (in order to resolve conflicts when there are multiple writes to the table). Kernel provides a [utility method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/CloseableIterable.html#inMemoryIterable-io.delta.kernel.utils.CloseableIterator-) to create an in-memory version of `CloseableIterable`. This interface also gives the connector an option to implement a custom implementation that spills the data actions to disk when the contents are too big to fit in memory.\n\n```java\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(dataActions);\n```\n\nThe final step is committing the transaction!\n\n```java\nTransactionCommitStatus commitStatus = txn.commit(engine, dataActionsIterable)\n```\n\nThe [`TransactionCommitResult`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html) contains the what version the transaction is committed as and whether the table is ready for a checkpoint. As we are creating a table the version will be `0`. We will be discussing later on what a checkpoint is and what it means for the table to be ready for the checkpoint.\n\nA few working examples to create and insert data into partitioned and un-partitioned Delta tables are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n## Blind append into an existing Delta table\nIn this section, we will walk through how to build a Delta connector that inserts data into an existing Delta table (similar to `INSERT INTO <table> <query>` construct in SQL) using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository. The steps are exactly similar to [Create table and insert data into it](#create-a-table-and-insert-data-into-it) except that we won't be providing any schema or partition columns when building the `TransactionBuilder`\n\n```java\n// Create a `Table` object with the given destination table path\nTable table = Table.forPath(engine, tablePath);\n\n// Create a transaction builder to build the transaction\nTransactionBuilder txnBuilder =\n  table.createTransactionBuilder(\n    engine,\n    \"Examples\", /* engineInfo */\n    Operation.WRITE\n  );\n\n/ Build the transaction - no need to provide the schema as the table already exists.\nTransaction txn = txnBuilder.build(engine);\n\n// Get the transaction state\nRow txnState = txn.getTransactionState(engine);\n\nList<Row> dataActions = new ArrayList<>();\n\n// Generate the sample data for three partitions. Process each partition separately.\n// This is just an example. In a real-world scenario, the data may come from different\n// partitions. Connectors already have the capability to partition by partition values\n// before writing to the table\n\n// In the test data `city` is a partition column\nfor (String city : Arrays.asList(\"San Francisco\", \"Campbell\", \"San Jose\")) {\n  FilteredColumnarBatch batch1 = generatedPartitionedDataBatch(\n\t    5 /* offset */, city /* partition value */);\n  FilteredColumnarBatch batch2 = generatedPartitionedDataBatch(\n\t    5 /* offset */, city /* partition value */);\n  FilteredColumnarBatch batch3 = generatedPartitionedDataBatch(\n\t    10 /* offset */, city /* partition value */);\n\n    CloseableIterator<FilteredColumnarBatch> data =\n\t    toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n    // Create partition value map\n    Map<String, Literal> partitionValues =\n\t    Collections.singletonMap(\n\t\t    \"city\", // partition column name\n\t\t    // partition value. Depending upon the parition column type, the\n\t\t    // partition value should be created. In this example, the partition\n\t\t    // column is of type StringType, so we are creating a string literal.\n\t\t    Literal.ofString(city));\n\n\n    // First transform the logical data to physical data that needs to be written\n    // to the Parquet\n    // files\n    CloseableIterator<FilteredColumnarBatch> physicalData =\n\t    Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n    // Get the write context\n    DataWriteContext writeContext =\n\t    Transaction.getWriteContext(engine, txnState, partitionValues);\n\n\n    // Now write the physical data to Parquet files\n    CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n\t    .writeParquetFiles(\n\t\t    writeContext.getTargetDirectory(),\n\t\t    physicalData,\n\t\t    writeContext.getStatisticsColumns());\n\n\n    // Now convert the data file status to data actions that needs to be written to the Delta\n    // table log\n    CloseableIterator<Row> partitionDataActions = Transaction.generateAppendActions(\n\t    engine,\n\t    txnState,\n\t    dataFiles,\n\t    writeContext);\n\n    // Now add all the partition data actions to the main data actions list. In a\n    // distributed query engine, the partition data is written to files at tasks on executor\n    // nodes. The data actions are collected at the driver node and then written to the\n    // Delta table log using the `Transaction.commit`\n    while (partitionDataActions.hasNext()) {\n\tdataActions.add(partitionDataActions.next());\n    }\n}\n\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n\ttoCloseableIterator(dataActions.iterator()));\n\n// Commit the transaction.\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n```\n\n## Idempotent Blind Appends to a Delta Table\nIdempotent writes allow the connector to make sure the data belonging to a particular transaction version and application id is inserted into the table at most once. In incremental processing systems (e.g. streaming systems), track progress using their own application-specific versions need to record what progress has been made in order to avoid duplicating data in the face of failures and retries during writes. By setting the transaction identifier, the Delta table can ensure that the data with the same identifier is not written multiple times. For more information refer to the Delta protocol section [Transaction Identifiers](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#transaction-identifiers)\n\nTo make the data append idempotent, set the transaction identifier on the [`TransactionBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionBuilder.html#withTransactionId-io.delta.kernel.engine.Engine-java.lang.String-long-)\n\n```java\n// Set the transaction identifiers for idempotent writes\n// Delta/Kernel makes sure that there exists only one transaction in the Delta log\n// with the given application id and txn version\ntxnBuilder =\n  txnBuilder.withTransactionId(\n    engine,\n    \"my app id\", /* application id */\n    100 /* monotonically increasing txn version with each new data insert */\n  );\n```\n\nThat's all the connector need to do for idempotent blind appends.\n\n## Checkpointing a Delta table\n[Checkpoints](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#checkpoints) are an optimization in Delta Log in order to construct the state of the Delta table faster. It basically contains the state of the table at the version the checkpoint is created. Delta Kernel allows the connector to optionally make the checkpoints. It is created for every few commits (configurable table property) on the table.\n\nThe result of `Transaction.commit` returns a `TransactionCommitResult` that contains the version the transaction is committed as and whether the table is [read for checkpoint](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html#isReadyForCheckpoint--). Creating a checkpoint takes time as it needs to construct the entire state of the table. If the connector doesn't want to checkpoint by itself but uses other connectors that are faster in creating a checkpoint, it can skip the checkpointing step.\n\nIf it wants to checkpoint, the `Table` object has an [API](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html#checkpoint-io.delta.kernel.engine.Engine-long-) to checkpoint the table.\n\n```java\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\nif (commitResult.isReadyForCheckpoint()) {\n  // Checkpoint the table\n  Table.forPath(engine, tablePath).checkpoint(engine, commitResult.getVersion());\n}\n```\n\n## Build a Delta connector for a distributed processing engine\nUnlike simple applications that just read the table in a single process, building a connector for complex processing engines like Apache Spark™ and Trino can require quite a bit of additional effort. For example, to build a connector for an SQL engine you have to do the following\n\n* Understand the APIs provided by the engine to build connectors and how Delta Kernel can be used to provide the information necessary for the connector + engine to operate on a Delta table.\n* Decide what libraries to use to do computationally expensive operations like reading Parquet files, parsing JSON, computing expressions, etc. Delta Kernel provides all the extension points to allow you to plug in any library without having to understand all the low-level details of the Delta protocol.\n* Deal with details specific to distributed engines. For example,\n    * Serialization of Delta table metadata provided by Delta Kernel.\n    * Efficiently transforming data read from Parquet into the engine in-memory processing format.\n\nIn this section, we are going to outline the steps needed to build a connector.\n\n### Step 0: Validate the prerequisites\nIn the previous section showing how to read a simple table, we were briefly introduced to the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html). This is the main extension point where you can plug in your implementations of computationally-expensive operations like reading Parquet files, parsing JSON, etc. For the simple case, we were using a default implementation of the helper that works in most cases. However, for building a high-performance connector for a complex processing engine, you will very likely need to provide your own implementation using the libraries that work with your engine. So before you start building your connector, it is important to understand these requirements and plan for building your own engine.\n\nHere are the libraries/capabilities you need to build a connector that can read the Delta table\n\n* Perform file listing and file reads from your storage/file system.\n* Read Parquet files in columnar data, preferably in an in-memory columnar format.\n* Parse JSON data\n* Read JSON files\n* Evaluate expressions on in-memory columnar batches\n\nFor each of these capabilities, you can choose to build your own implementation or reuse the default implementation.\n\n### Step 1: Set up Delta Kernel in your connector project\nIn the Delta Kernel project, there are multiple dependencies you can choose to depend on.\n\n1. Delta Kernel core APIs - This is a must-have dependency, which contains all the main APIs like Table, Snapshot, and Scan that you will use to access the metadata and data of the Delta table. This has very few dependencies reducing the chance of conflicts with any dependencies in your connector and engine. This also provides the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface which allows you to plug in your implementations of computationally expensive operations, but it does not provide any implementation of this interface.\n2. Delta Kernel default- This has a default implementation called [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultEngine.html) and additional dependencies such as `Hadoop`. If you wish to reuse all or parts of this implementation, then you can optionally depend on this.\n\n#### Set up Java projects\nAs discussed above, you can import one or both of the artifacts as follows:\n\n```xml\n<!-- Must have dependency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-api</artifactId>\n  <version>${delta-kernel.version}</version>\n</dependency>\n\n<!-- Optional depdendency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-defaults</artifactId>\n  <version>${delta-kernel.version}</version>\n</dependency>\n```\n\n### Step 2: Build your own Engine\nIn this section, we are going to explore the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface and walk through how to implement your own implementation so that you can plug in your connector/engine-specific implementations of computationally-intensive operations, threading model, resource management, etc.\n\n<Aside type=\"note\" title=\"important\">\n  During the validation process, if you believe that all the dependencies of the default <code>Engine</code> implementation can work with your connector and engine, then you can skip this step and jump to Step 3 of implementing your connector using the default engine. If later you have the need to customize the helper for your connector, you can revisit this step.\n</Aside>\n\n#### Step 2.1: Implement the `Engine` interface\n\nThe [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface combines a bunch of sub-interfaces each of which is designed for a specific purpose. Here is a brief overview of the subinterfaces. See the API docs (Java) for a more detailed view.\n\n```java\ninterface Engine {\n  /**\n   * Get the connector provided {@link ExpressionHandler}.\n   * @return An implementation of {@link ExpressionHandler}.\n  */\n  ExpressionHandler getExpressionHandler();\n\n  /**\n   * Get the connector provided {@link JsonHandler}.\n   * @return An implementation of {@link JsonHandler}.\n   */\n  JsonHandler getJsonHandler();\n\n  /**\n   * Get the connector provided {@link FileSystemClient}.\n   * @return An implementation of {@link FileSystemClient}.\n   */\n  FileSystemClient getFileSystemClient();\n\n  /**\n   * Get the connector provided {@link ParquetHandler}.\n   * @return An implementation of {@link ParquetHandler}.\n   */\n  ParquetHandler getParquetHandler();\n}\n```\n\nTo build your own `Engine` implementation, you can choose to either use the default implementations of each sub-interface or completely build every one from scratch.\n\n```java\nclass MyEngine extends DefaultEngine {\n\n  FileSystemClient getFileSystemClient() {\n    // Build a new implementation from scratch\n    return new MyFileSystemClient();\n  }\n  \n  // For all other sub-clients, use the default implementations provided by the `DefaultEngine`.\n}\n```\n\nNext, we will walk through how to implement each interface.\n\n#### Step 2.2: Implement `FileSystemClient` interface\n\nThe [`FileSystemClient`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/FileSystemClient.html) interface contains basic file system operations like listing directories, resolving paths into a fully qualified path and reading bytes from files. Implementation of this interface must take care of the following when interacting with storage systems such as S3, Hadoop, or ADLS:\n\n* Credentials and permissions: The connector must populate its `FileSystemClient` with the necessary configurations and credentials for the client to retrieve the necessary data from the storage system. For example, an implementation based on Hadoop's FileSystem abstractions can be passed S3 credentials via the Hadoop configurations.\n* Decryption: If file system objects are encrypted, then the implementation must decrypt the data before returning the data.\n\n#### Step 2.3: Implement `ParquetHandler`\n\nAs the name suggests, this interface contains everything related to reading and writing Parquet files. It has been designed such that a connector can plug in a wide variety of implementations, from a simple single-threaded reader to a very advanced multi-threaded reader with pre-fetching and advanced connector-specific expression pushdown. Let's explore the methods to implement, and the guarantees associated with them.\n\n##### Method `readParquetFiles(CloseableIterator<FileStatus> fileIter, StructType physicalSchema, java.util.Optional<Predicate> predicate)`\n\nThis [method]((https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#readParquetFiles-io.delta.kernel.utils.CloseableIterator-io.delta.kernel.types.StructType-)) takes as input `FileStatus`s which contains metadata such as file path, size etc. of the Parquet file to read. The columns to be read from the Parquet file are defined by the physical schema. To implement this method, you may have to first implement your own [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) which is used to represent the in-memory data generated from the Parquet files.\n\nWhen identifying the columns to read, note that there are multiple types of columns in the physical schema (represented as a [`StructType`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructType.html)).\n\n* Data columns: Columns that are expected to be read from the Parquet file. Based on the `StructField` object defining the column, read the column in the Parquet file that matches the same name or field id. If the column has a field id (stored as `parquet.field.id` in the `StructField` metadata) then the field id should be used to match the column in the Parquet file. Otherwise, the column name should be used for matching.\n* Metadata columns: These are special columns that must be populated using metadata about the Parquet file ([`StructField#isMetadataColumn`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructField.html#isMetadataColumn--) tells whether a column in `StructType` is a metadata column). To understand how to populate such a column, first match the column name against the set of standard metadata column name constants. For example,\n    * `StructFileld#isMetadataColumn()` returns true and the column name is `StructField.METADATA_ROW_INDEX_COLUMN_NAME`, then you have to a generate column vector populated with the actual index of each row in the Parquet file (that is, not indexed by the possible subset of rows returned after Parquet data skipping).\n\n##### Requirements and guarantees\nAny implementation must adhere to the following guarantees.\n\n* The schema of the returned `ColumnarBatch`es must match the physical schema.\n    * If a data column is not found and the `StructField.isNullable = true`, then return a `ColumnVector` of nulls. Throw an error if it is not nullable.\n* The output iterator must maintain ordering as the input iterator. That is, if `file1` is before `file2` in the input iterator, then columnar batches of `file1` must be before those of `file2` in the output iterator.\n\n##### Method `writeParquetFiles(String directoryPath, CloseableIterator<FilteredColumnarBatch> dataIter, java.util.List<Column> statsColumns)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#writeParquetFiles-java.lang.String-io.delta.kernel.utils.CloseableIterator-java.util.List-) takes given data writes it into one or more Parquet files into the given directory. The data is given as an iterator of [FilteredColumnarBatches](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) which contains a [ColumnarBatch](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and an optional selection vector containing one entry for each row in `ColumnarBatch` indicating whether a row is selected or not selected. The `ColumnarBatch` also contains the schema of the data. This schema should be converted to Parquet schema, including any field IDs present [`FieldMetadata`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/FieldMetadata.html) for each column `StructField`.\n\nThere is also the parameter `statsColumns`, which is a hint to the Parquet writer on what set of columns to collect stats for each file. The statistics include `min`, `max` and `null_count` for each column in the `statsColumns` list. Statistics collection is optional, but when present it is used by Kernel to persist the stats as part of the Delta table commit. This will help read queries prune un-needed data files based on the query predicate.\n\nFor each written data file, the caller is expecting a [`DataFileStatus`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/DataFileStatus.html) object. It contains the data file path, size, modification time, and optional column statistics.\n\n#### Method `writeParquetFileAtomically(String filePath, CloseableIterator<FilteredColumnarBatch> data)`\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#writeParquetFiles-java.lang.String-io.delta.kernel.utils.CloseableIterator-java.util.List-) writes the given `data` into Parquet file at location `filePath`. The write is an atomic write i.e., either a Parquet file is created with all given content or no Parquet file is created at all. This should not create a file with partial content in it.\n\nThe default implementation makes use of [`LogStore`](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) implementations from the [`delta-storage`](https://github.com/delta-io/delta/tree/master/storage) module to accomplish the atomicity. A connector that wants to implement their own version of `ParquetHandler` can take a look at the default implementation for details.\n\n##### Performance suggestions\n* The representation of data as `ColumnVector`s and `ColumnarBatch`es can have a significant impact on the query performance and it's best to read the Parquet file data directly into vectors and batches of the engine-native format to avoid potentially costly in-memory data format conversion. Create a Kernel `ColumnVector` and `ColumnarBatch` wrappers around the engine-native format equivalent classes.\n\n#### Step 2.4: Implement `ExpressionHandler` interface\nThe [`ExpressionHandler`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html) interface has all the methods needed for handling expressions that may be applied on columnar data.\n\n##### Method `getEvaluator(StructType batchSchema, Expression expresion, DataType outputType)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#getEvaluator-io.delta.kernel.types.StructType-io.delta.kernel.expressions.Expression-io.delta.kernel.types.DataType-) generates an object of type [`ExpressionEvaluator`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/ExpressionEvaluator.html) that can evaluate the `expression` on a batch of row data to produce a result of a single column vector. To generate this function, the `getEvaluator()` method takes as input the expression and the schema of the `ColumnarBatch`es of data on which the expressions will be applied. The same object can be used to evaluate multiple columnar batches of input with the same schema and expression the evaluator is created for.\n\n##### Method `getPredicateEvaluator(StructType inputSchema, Predicate predicate)`\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#createSelectionVector-boolean:A-int-int-) is for creating an expression evaluator for `Predicate` type expressions. The `Predicate` type expressions return a boolean value as output.\n\nThe returned object is of type [`PredicateEvaluator`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/PredicateEvaluator.html). This is a special interface for evaluating Predicate on input batch returns a selection vector containing one value for each row in input batch indicating whether the row has passed the predicate or not. Optionally it takes an existing selection vector along with the input batch for evaluation. The result selection vector is combined with the given existing selection vector and a new selection vector is returned. This mechanism allows running an input batch through several predicate evaluations without rewriting the input batch to remove rows that do not pass the predicate after each predicate evaluation. The new selection should be the same or more selective as the existing selection vector. For example, if a row is marked as unselected in the existing selection vector, then it should remain unselected in the returned selection vector even when the given predicate returns true for the row.\n\n##### Method `createSelectionVector(boolean[] values, int from, int to)`\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#createSelectionVector-boolean:A-int-int-) allows creating `ColumnVector` for boolean type values given as input. This allows the connector to maintain all `ColumnVector`s created in the desired memory format.\n\n##### Requirements and guarantees\nAny implementation must adhere to the following guarantees.\n\n* Implementation must handle all possible variations of expressions. If the implementation encounters an expression type that it does not know how to handle, then it must throw a specific language-dependent exception.\n    * Java: [NotSupportedException](https://docs.oracle.com/javaee/7/api/latest/javax/resource/NotSupportedException.html)\n* The `ColumnarBatch`es on which the generated `ExpressionEvaluator` is going to be used are guaranteed to have the schema provided during generation. Hence, it is safe to bind the expression evaluation logic to column ordinals instead of column names, thus making the actual evaluation faster.\n\n#### Step 2.5: Implement `JsonHandler`\n[This](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html) engine interface allows the connector to use plug-in their own JSON handling code and expose it to the Delta Kernel.\n\n##### Method `readJsonFiles(CloseableIterator<FileStatus> fileIter, StructType physicalSchema, java.util.Optional<Predicate> predicate)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#readJsonFiles-io.delta.kernel.utils.CloseableIterator-io.delta.kernel.types.StructType-) takes as input `FileStatus`s of the JSON files and returns the data in a series of columnar batches. The columns to be read from the JSON file are defined by the physical schema, and the return batches must match that schema. To implement this method, you may have to first implement your own [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html)and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) which is used to represent the in-memory data generated from the JSON files.\n\nWhen identifying the columns to read, note that there are multiple types of columns in the physical schema (represented as a [`StructType`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructType.html)).\n\n##### Method `parseJson(ColumnVector jsonStringVector, StructType outputSchema, java.util.Optional<ColumnVector> selectionVector)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#parseJson-io.delta.kernel.data.ColumnVector-io.delta.kernel.types.StructType-) allows parsing a `ColumnVector` of string values which are in JSON format into the output format specified by the `outputSchema`. If a given column in `outputSchema` is not found, then a null value is returned. It optionally takes a selection vector which indicates what entries in the input `ColumnVector` of strings to parse. If an entry is not selected then a `null` value is returned as parsed output for that particular entry in the output.\n\n##### Method `deserializeStructType(String structTypeJson)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#deserializeStructType-java.lang.String-) allows parsing JSON encoded (according to [Delta schema serialization rules](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#schema-serialization-format)) `StructType` schema into a `StructType`. Most implementations of `JsonHandler` do not need to implement this method and instead use the one in the [default `JsonHandler`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultJsonHandler.html) implementation.\n\n#### Method `writeJsonFileAtomically(String filePath, CloseableIterator<Row> data, boolean overwrite)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#writeJsonFileAtomically-java.lang.String-io.delta.kernel.utils.CloseableIterator-boolean-) writes the given `data` into a JSON file at location `filePath`. The write is an atomic write i.e., either a JSON file is created with all given content or no Parquet file is created at all. This should not create a file with partial content in it.\n\nThe default implementation makes use of [`LogStore`](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) implementations from the [`delta-storage`](https://github.com/delta-io/delta/tree/master/storage) module to accomplish the atomicity. A connector that wants to implement their own version of `JsonHandler` can take a look at the default implementation for details.\n\nThe implementation is expected to handle the serialization rules (converting the `Row` object to JSON string) as described in the [API Javadoc](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#writeJsonFileAtomically-java.lang.String-io.delta.kernel.utils.CloseableIterator-boolean-).\n\n#### Step 2.6: Implement `ColumnarBatch` and `ColumnVector`\n\n[`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) are two interfaces to represent the data read into memory from files. This representation can have a significant impact on query performance. Each engine likely has a native representation of in-memory data with which it applies data transformation operations. For example, in Apache Spark™, the row data is internally represented as `UnsafeRow` for efficient processing. So it's best to read the Parquet file data directly into vectors and batches of the native format to avoid potentially costly in-memory data format conversions. So the recommended approach is to build wrapper classes that extend the two interfaces but internally use engine-native classes to store the data. When the connector has to forward the columnar batches received from the kernel to the engine, it has to be smart enough to skip converting vectors and batches that are already in the engine-native format.\n\n### Step 3: Build read support in your connector\nIn this section, we are going to walk through the likely sequence of Kernel API calls your connector will have to make to read a table. The exact timing of making these calls in your connector in the context of connector-engine interactions depends entirely on the engine-connector APIs and is therefore beyond the scope of this guide. However, we will try to provide broad guidelines that are likely (but not guaranteed) to apply to your connector-engine setup. For this purpose, we are going to assume that the engine goes through the following phases when processing a read/scan query - logical plan analysis, physical plan generation, and physical plan execution. Based on these broad characterizations, a typical control and data flow for reading a Delta table is going to be as follows:\n\n.. list-table::\n:header-rows: 1\n:widths: 30 70\nStep\nTypical query phase when this step occurs\nResolve the table snapshot to query\nLogical plan analysis phase when the plan's schema and other details need to be resolved and validated\nResolve files to scan based on query parameters\nPhysical plan generation, when the final parameters of the scan are available. For example:\nSchema of data to read after pruning away unused columns\nQuery filters to apply after filter rearrangement\nDistribute the file information to workers\nPhysical plan execution, only if it is a distributed engine.\nRead the columnar data using the file information\nPhysical plan execution, when the data is being processed by the engine\n\nLet's understand the details of each step.\n\n#### Step 3.1: Resolve the table snapshot to query\nThe first step is to resolve the consistent snapshot and the schema associated with it. This is often required by the connector/ engine to resolve and validate the logical plan of the scan query (if the concept of logical plan exists in your engine). To achieve this, the connector has to do the following.\n\n* Resolve the table path from the query: If the path is directly available, then this is easy. Otherwise, if it is a query based on a catalog table (for example, a Delta table defined in Hive Metastore), then the connector has to resolve the table path from the catalog.\n* Initialize the `Engine` object: Create a new instance of the `Engine` that you have chosen in [Step 2](#build-your-own Engine).\n* Initialize the Kernel objects and get the schema: Assuming the query is on the latest available version/snapshot of the table, you can get the table schema as follows:\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = new MyEngine();\nTable myTable = Table.forPath(myTablePath);\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\nStructType mySchema = mySnapshot.getSchema(myEngine);\n```\n\nIf you want to query a specific version of the table (that is, not the schema), then you can get the required snapshot as `myTable.getSnapshot(version)`.\n\n#### Step 3.2: Resolve files to scan\n\nNext, we need to build a Scan object using more information from the query. Here we are going to assume that the connector/engine has been able to extract the following details from the query (say, after optimizing the logical plan):\n\n* Read schema: The columns in the table that the query needs to read. This may be the full set of columns or a subset of columns.\n* Query filters: The filters on partitions or data columns that can be used skip reading table data.\n\nTo provide this information to Kernel, you have to do the following:\n\n* Convert the engine-specific schema and filter expressions to Kernel schema and expressions: For schema, you have to create a `StructType` object. For the filters, you have to create an `Expression` object using all the available subclasses of `Expression`.\n* Build the scan with the converted information: Build the scan as follows:\n\n```java\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.types.*;\n\nStructType readSchema = ... ;  // convert engine schema\nPredicate filterExpr = ... ;   // convert engine filter expression\n\nScan myScan = mySnapshot.getScanBuilder().withFilter(filterExpr).withReadSchema(readSchema).build();\n\n```\n\n* Resolve the information required to file reads: The generated Scan object has two sets of information.\n    * Scan files: `myScan.getScanFiles()` returns an iterator of `ColumnarBatch`es. Each batch in the iterator contains rows and each row has information about a single file that has been selected based on the query filter.\n    * Scan state: `myScan.getScanState()` returns a `Row` that contains all the information that is common across all the files that need to be read.\n\n```java\nRow myScanStateRow = myScan.getScanState();\nCloseableIterator<FilteredColumnarBatch> myScanFilesAsBatches = myScan.getScanFiles();\n\nwhile (myScanFilesAsBatches.hasNext()) {\n  FilteredColumnarBatch scanFileBatch = myScanFilesAsBatches.next();\n\n  CloseableIterator<Row> myScanFilesAsRows = scanFileBatch.getRows();\n}\n```\n\nAs we will soon see, reading the columnar data from a selected file will need to use both, the scan state row, and a scan file row with the file information.\n\n##### Requirements and guarantees\nHere are the details you need to ensure when defining this scan.\n\n* The provided `readSchema` must be the exact schema of the data that the engine will expect when executing the query. Any mismatch in the schema defined during this query planning and the query execution will result in runtime failures. Hence you must build the scan with the readSchema only after the engine has finalized the logical plan after any optimizations like column pruning.\n* When applicable (for example, with Java Kernel APIs), you have to make sure to call the close() method as you consume the `ColumnarBatch`es of scan files (that is, either serialize the rows or use them to read the table data).\n\n#### Step 3.3: Distribute the file information to the workers\nIf you are building a connector for a distributed engine like Spark/Presto/Trino/Flink, then your connector has to send all the scan metadata from the query planning machine (henceforth called the driver) to task execution machines (henceforth called the workers). You will have to serialize and deserialize the scan state and scan file rows. It is the connector job to implement serialization and deserialization utilities for a [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html). If the connector wants to split reading one scan file into multiple tasks, it can add additional connector specific split context to the task. At the task, the connector can use its own Parquet reader to read the specific part of the file indicated by the split info.\n\n##### Custom `Row` Serializer/Deserializer\nHere are steps on how to build your own serializer/deserializer such that it will work with any [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html) of any schema.\n\n* Serializing\n    * First serialize the row schema, that is, `StructType` object.\n    * Then, use the schema to identify types of each column/ordinal in the `Row` and use that to serialize all the values one by one.\n\n* Deserializing\n    * Define your own class that extends the Row interface. It must be able to handle complex types like arrays, nested structs and maps.\n    * First deserialize the schema.\n    * Then, use the schema to deserialize the values and put them in an instance of your custom Row class.\n\n```java\nimport io.delta.kernel.utils.*;\n\n// In the driver where query planning is being done\nByte[] scanStateRowBytes = RowUtils.serialize(scanStateRow);\nByte[] scanFileRowBytes = RowUtils.serialize(scanFileRow);\n\n// Optionally the connector adds a split info to the task (scan file, scan state) to\n// split reading of a Parquet file into multiple tasks. The task gets split info\n// along with the scan file row and scan state row.\nSplit split = ...; // connector specific class, not related to Kernel\n\n// Send these over to the worker\n\n// In the worker when data will be read, after rowBytes have been sent over\nRow scanStateRow = RowUtils.deserialize(scanStateRowBytes);\nRow scanFileRow = RowUtils.deserialize(scanFileRowBytes);\nSplit split = ... deserialize split info ...;\n```\n\n#### Step 3.4: Read the columnar data\nFinally, we are ready to read the columnar data. You will have to do the following:\n\n* Read the physical data from Parquet file as indicated by the scan file row, scan state, and optionally the split info\n* Convert the physical data into logical data of the table using the Kernel's APIs.\n\n```java\nRow scanStateRow = ... ;\nRow scanFileRow = ... ;\nSplit split = ...;\n\n// Additional option predicate such as dynamic filters the connector wants to\n// pass to the reader when reading files.\nPredicate optPredicate = ...;\n\n// Get the physical read schema of columns to read from the Parquet data files\nStructType physicalReadSchema =\n  ScanStateRow.getPhysicalDataReadSchema(engine, scanStateRow);\n\n// From the scan file row, extract the file path, size and modification metadata\n// needed to read the file.\nFileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow);\n\n// Open the scan file which is a Parquet file using connector's own\n// Parquet reader which supports reading specific parts (split) of the file.\n// If the connector doesn't have its own Parquet reader, it can use the\n// default Parquet reader provider which at the moment doesn't support reading\n// a specific part of the file, but reads the entire file from the beginning.\nCloseableIterator<ColumnarBatch> physicalDataIter =\n  connectParquetReader.readParquetFile(\n    fileStatus\n    physicalReadSchema,\n    split, // what part of the Parquet file to read data from\n    optPredicate /* additional predicate the connector can apply to filter data from the reader */\n  );\n\n// Now the physical data read from the Parquet data file is converted to logical data\n// the table represents.\n// Logical data may include the addition of partition columns and/or\n// subset of rows deleted\nCloseableIterator<FilteredColumnarBatch> transformedData =\n  Scan.transformPhysicalData(\n    engine,\n    scanState,\n    scanFileRow,\n    physicalDataIter));\n```\n\n* Resolve the data in the batches: Each [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) has two components:\n    * Columnar batch (returned by `FilteredColumnarBatch.getData()`): This is the data read from the files having the schema matching the readSchema provided when the Scan object was built in the earlier step.\n    * Optional selection vector (returned by `FilteredColumnarBatch.getSelectionVector()`): Optionally, a boolean vector that will define which rows in the batch are valid and should be consumed by the engine.\n\nIf the selection vector is present, then you will have to apply it to the batch to resolve the final consumable data.\n\n* Convert to engine-specific data format: Each connector/engine has its own native row / columnar batch formats and interfaces. To return the read data batches to the engine, you have to convert them to fit those engine-specific formats and/or interfaces. Here are a few tips that you can follow to make this efficient.\n    * Matching the engine-specific format: Some engines may expect the data in an in-memory format that may be different from the data produced by `getData()`. So you will have to do the data conversion for each column vector in the batch as needed.\n    * Matching the engine-specific interfaces: You may have to implement wrapper classes that extend the engine-specific interfaces and appropriately encapsulate the row data.\n\nFor best performance, you can implement your own Parquet reader and other `Engine` implementations to make sure that every `ColumnVector` generated is already in the engine-native format thus eliminating any need to convert.\n\nNow you should be able to read the Delta table correctly.\n\n### Step 4: Build append support in your connector\nIn this section, we are going to walk through the likely sequence of Kernel API calls your connector will have to make to append data to a table. The exact timing of making these calls in your connector in the context of connector-engine interactions depends entirely on the engine-connector APIs and is, therefore, beyond the scope of this guide. However, we will try to provide broad guidelines that are likely (but not guaranteed) to apply to your connector-engine setup. For this purpose, we are going to assume that the engine goes through the following phases when processing a write query - logical plan analysis, physical plan generation, and physical plan execution. Based on these broad characterizations, a typical control and data flow for reading a Delta table is going to be as follows:\n\n<Tabs>\n  <TabItem label=\"Typical steps and query phases\">\n    <table>\n      <thead>\n        <tr>\n          <th>Step</th>\n          <th>Typical query phase when this step occurs</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr>\n          <td>\n            Determine the schema of the data that needs to be written to the table. Schema is derived from the existing table or from the parent operation of the <code>write</code> operator in the query plan when the table doesn't exist yet.\n          </td>\n          <td>\n            Logical plan analysis phase when the plan's schema (<code>write</code> operator schema matches the table schema, etc.) and other details need to be resolved and validated.\n          </td>\n        </tr>\n        <tr>\n          <td>\n            Determine the physical partitioning of the data based on the table schema and partition columns either from the existing table or from the query plan (for new tables)\n          </td>\n          <td>\n            Physical plan generation, where the number of writer tasks, data schema and partitioning is determined\n          </td>\n        </tr>\n        <tr>\n          <td>\n            Distribute the writer tasks definitions (which include the transaction state) to workers.\n          </td>\n          <td>\n            Physical plan execution, only if it is a distributed engine.\n          </td>\n        </tr>\n        <tr>\n          <td>\n            Tasks write the data to data files and send the data file info to the driver.\n          </td>\n          <td>\n            Physical plan execution, when the data is actually written to the table location\n          </td>\n        </tr>\n        <tr>\n          <td>\n            Finalize the query. Here, all the info of the data files written by the tasks is aggregated and committed to the transaction created at the beginning of the physical execution.\n          </td>\n          <td>\n            Finalize the query. This happens on the driver where the query has started.\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </TabItem>\n</Tabs>\n\nLet's understand the details of each step.\n\n#### Step 4.1: Determine the schema of the data that needs to be written to the table\nThe first step is to resolve the output data schema. This is often required by the connector/ engine to resolve and validate the logical plan of the  query (if the concept of logical plan exists in your engine). To achieve this, the connector has to do the following. At a high level query plan is a tree of operators where the leaf-level operators generate or read data from storage/tables and feed it upwards towards the parent operator nodes. This data transfer happens until it reaches the root operator node where the query is finalized (either the results are sent to the client or data is written to another table).\n\n* Create the `Table` object\n* From the `Table` object try to get the schema.\n    * If the table is not found\n        * the query includes creating the table (e.g., `CREATE TABLE AS` SQL query);\n            * the schema is derived from the operator above the `write` that feeds the data to the `write` operator.\n        * the query doesn't include creating new table, an exception is thrown saying the table is not found\n    * If the table already exists\n        * get the schema from the table and check if it matches the schema of the `write` operator. If not throw an exception.\n* Create a `TransactionBuilder` - this basically begins the steps of transaction construction.\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = new MyEngine();\nTable myTable = Table.forPath(myTablePath);\n\nStructType writeOperatorSchema = // ... derived from the query operator tree ...\nStructType dataSchema;\nboolean isNewTable = false;\n\ntry {\n  Snapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\n  dataSchema = mySnapshot.getSchema(myEngine);\n\n  // .. check dataSchema and writeOperatorSchema match ...\n} catch(TableNotFoundException e) {\n  isNewTable = true;\n  dataSchema = writeOperatorSchema;\n}\n\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */ \n    Operation /* What is the operation we are trying to perform? This is noted in the Delta Log */\n  );\n\nif (isNewTable) {\n  // For a new table set the table schema in the transaction builder\n  txnBuilder = txnBuilder.withSchema(engine, dataSchema)\n}\n\n```\n\n#### Step 4.2: Determine the physical partitioning of the data based on the table schema and partition columns\n\nPartition columns are found either from the query (for new tables, the query defines the partition columns) or from the existing table.\n\n```java\nTransactionBuilder txnBuilder = ... from the last step ...\nTransaction txn;\n\nList<String> partitionColumns = ...\nif (newTable) {\n  partitionColumns = ... derive from the query parameters (ex. PARTITION BY clause in SQL) ...\n  txnBuilder = txnBuilder.withPartitionColumns(engine, partitionColumns);\n  txn = txnBuilder.build(engine);\n} else {\n  txn = txnBuilder.build(engine);\n  partitionColumns = txn.getPartitionColumns(engine);\n}\n```\n\nAt the end of this step, we have the `Transaction` and schema of the data to generate and its partitioning.\n\n#### Step 4.3: Distribute the writer tasks definitions (which include the transaction state) to workers\nIf you are building a connector for a distributed engine like Spark/Presto/Trino/Flink, then your connector has to send all the writer metadata from the query planning machine (henceforth called the driver) to task execution machines (henceforth called the workers). You will have to serialize and deserialize the transaction state. It is the connector job to implement serialization and deserialization utilities for a [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html). More details on a custom `Row` SerDe are found [here](#custom-row-serializerdeserializer).\n\n```java\nRow txnState = txn.getState(engine);\n\nString jsonTxnState = serializeToJson(txnState);\n```\n\n#### Step 4.4: Tasks write the data to data files and send the data file info to the driver.\nIn this step (which is executed on the worker nodes inside each task):\n* Deserialize the transaction state\n* Writer operator within the task gets the data from its parent operator.\n* The data is converted into a `FilteredColumnarBatch`. Each [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) has two components:\n    * Columnar batch (returned by `FilteredColumnarBatch.getData()`): This is the data read from the files having the schema matching the readSchema provided when the Scan object was built in the earlier step.\n    * Optional selection vector (returned by `FilteredColumnarBatch.getSelectionVector()`): Optionally, a boolean vector that will define which rows in the batch are valid and should be consumed by the engine.\n* The connector can create `FilteredColumnBatch` wrapper around data in its own in-memory format.\n* Check if the data is partitioned or not. If not partitioned, partition the data by partition values.\n* For each partition generate the map of the partition column to the partition value\n* Use Kernel to convert the partitioned data into physical data that should go into the data files\n* Write the physical data into one or more data files.\n* Convert data file statues into a Delta log actions\n* Serialize the Delta log action `Row` objects and send them to the driver node\n\n```\nRow txnState = ... deserialize from JSON string sent by the driver ...\n\nCloseableIterator<FilteredColumnarBatch> data = ... generate data ...\n\n// If the table is un-partitioned then this is an empty map\nMap<String, Literal> partitionValues = ... prepare the partition values ...\n\n\n// First transform the logical data to physical data that needs to be written\n// to the Parquet files\nCloseableIterator<FilteredColumnarBatch> physicalData =\n  Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n// Get the write context\nDataWriteContext writeContext = Transaction.getWriteContext(engine, txnState, partitionValues);\n\n// Now write the physical data to Parquet files\nCloseableIterator<DataFileStatus> dataFiles =\n  engine.getParquetHandler()\n    .writeParquetFiles(\n      writeContext.getTargetDirectory(),\n      physicalData,\n      writeContext.getStatisticsColumns());\n\n// Now convert the data file status to data actions that needs to be written to the Delta table log\nCloseableIterator<Row> partitionDataActions =\n  Transaction.generateAppendActions(\n    engine,\n    txnState,\n    dataFiles,\n    writeContext);\n\n.... serialize `partitionDataActions` and send them to driver node\n```\n\n#### Step 4.5: Finalize the query.\nAt the driver node, the delta log actions from all the tasks are received and committed to the transaction. The tasks send the Delta log actions as a serialized JSON and deserialize them back to `Row` objects.\n\n```\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n\ttoCloseableIterator(dataActions.iterator()));\n\n// Commit the transaction.\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n// Optional step\nif (commitResult.isReadyForCheckpoint()) {\n  // Checkpoint the table\n  Table.forPath(engine, tablePath).checkpoint(engine, commitResult.getVersion());\n}\n```\n\nThats it. Now you should be able to append data to Delta tables using the Kernel APIs.\n\n\n## Migration guide\nKernel APIs are still evolving and new features are being added. Kernel authors try to make the API changes backward compatible as much as they can with each new release, but sometimes it is hard to maintain the backward compatibility for a project that is evolving rapidly.\n\nThis section provides guidance on how to migrate your connector to the latest version of Delta Kernel. With each new release the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) are kept up-to-date with the latest API changes. You can refer to the examples to understand how to use the new APIs.\n\n### Migration from Delta Lake version 3.1.0 to 3.2.0\nFollowing are API changes in Delta Kernel 3.2.0 that may require changes in your connector.\n\n#### Rename `TableClient` to [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html)\nThe `TableClient` interface has been renamed to `Engine`. This is the most significant API change in this release. The `TableClient` interface name is not exactly representing the functionality it provides. At a high level it provides capabilities such as reading Parquet files, JSON files, evaluating expressions on data and file system functionality. These are basically the heavy lift operations that Kernel depends on as a separate interface to allow the connectors to substitute their own custom implementation of the same functionality (e.g. custom Parquet reader). Essentially, these functionalities are the core of the `engine` functionalities. By renaming to `Engine`, we are representing the interface functionality with a proper name that is easy to understand.\n\nThe `DefaultTableClient` has been renamed to [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultEngine.html).\n\n#### [`Table.forPath(Engine engine, String tablePath)`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html#forPath-io.delta.kernel.engine.Engine-java.lang.String-) behavior change\nEarlier when a non-existent table path is passed, the API used to throw `TableNotFoundException`. Now it doesn't throw the exception. Instead, it returns a `Table` object. When trying to get a `Snapshot` from the table object it throws the `TableNotFoundException`.\n\n#### [`FileSystemClient.resolvePath`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultFileSystemClient.html#resolvePath-java.lang.String-) behavior change\nEarlier when a non-existent path is passed, the API used to throw `FileNotFoundException`. Now it doesn't throw the exception. It still resolves the given path into a fully qualified path."
  },
  {
    "path": "docs/src/content/docs/delta-kernel-rust.mdx",
    "content": "---\ntitle: Delta Kernel Rust\ndescription: Learn how to build connectors to read and write Delta tables using Delta Kernel Rust.\n---\n\nWork In Progress"
  },
  {
    "path": "docs/src/content/docs/delta-kernel.mdx",
    "content": "---\ntitle: Delta Kernel\ndescription: Learn how to build connectors to read and write Delta tables.\n---\n\nimport { Tabs, TabItem, Aside, Steps } from \"@astrojs/starlight/components\";\n\nThe Delta Kernel project is a set of libraries ([Java](#kernel-java) and [Rust](#kernel-rust)) for building Delta connectors that can read from and write into Delta tables without the need to understand the [Delta protocol details](https://github.com/delta-io/delta/blob/master/PROTOCOL.md).\n\nYou can use this library to do the following:\n\n- Read data from small Delta tables in a single thread in a single process.\n- Read data from large Delta tables using multiple threads in a single process.\n- Build a complex connector for a distributed processing engine and read very large Delta tables.\n- Insert data into a Delta table either from a single process or a complex distributed engine.\n\nHere is an example of a simple table scan with a filter:\n\n<Tabs>\n  <TabItem label=\"Java\">\n    ```java\n    Engine myEngine = DefaultEngine.create() ;                  // define a engine (more details below)\n    Table myTable = Table.forPath(\"/delta/table/path\");         // define what table to scan\n    Snapshot mySnapshot = myTable.getLatestSnapshot(myEngine);  // define which version of table to scan\n    Scan myScan = mySnapshot.getScanBuilder(myEngine)           // specify the scan details\n      .withFilters(myEngine, scanFilter)\n      .build();\n    CloseableIterator<ColumnarBatch> physicalData =             // read the Parquet data files\n      .. read from Parquet data files ...\n    Scan.transformPhysicalData(...)                             // returns the table data\n    ```\n  </TabItem>\n</Tabs>\n\nA complete version of the above example program and more examples of reading from and writing into a Delta table are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\nNotice that there are two sets of public APIs to build connectors.\n\n- **Table APIs** - Interfaces like [`Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/index.html?io/delta/kernel/Table.html) and [`Snapshot`](https://delta-io.github.io/delta/snapshot/kernel-api/java/index.html?io/delta/kernel/Snapshot.html) that allow you to read (and soon write to) Delta tables\n- **Engine APIs** - The [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java//index.html?io/delta/kernel/engine/Engine.html) interface allows you to plug in connector-specific optimizations to compute-intensive components in the Kernel. For example, Delta Kernel provides a _default_ Parquet file reader via the `DefaultEngine`, but you may choose to replace that default with a custom `Engine` implementation that has a faster Parquet reader for your connector/processing engine.\n\n## Kernel Java\n\n## What is Delta Kernel?\n\nDelta Kernel is a library for operating on Delta tables. Specifically, it provides simple and narrow APIs for reading and writing to Delta tables without the need to understand the [Delta protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md) details. You can use this library to do the following:\n\n- Read and write Delta tables from your applications.\n- Build a connector for a distributed engine like [Apache Spark™](https://github.com/apache/spark), [Apache Flink](https://github.com/apache/flink), or [Trino](https://github.com/trinodb/trino) for reading or writing massive Delta tables.\n\n## Set up Delta Kernel for your project\n\nYou need to `io.delta:delta-kernel-api` and `io.delta:delta-kernel-defaults` dependencies. Following is an example Maven `pom` file dependency list.\n\nThe `delta-kernel-api` module contains the core of the Kernel that abstracts out the Delta protocol to enable reading and writing into Delta tables. It makes use of the `Engine` interface that is being passed to the Kernel API by the connector for heavy-lift operations such as reading/writing Parquet or JSON files, evaluating expressions or file system operations such as listing contents of the Delta Log directory, etc. Kernel supplies a default implementation of `Engine` in module `delta-kernel-defaults`. The connectors can implement their own version of `Engine` to make use of their native implementation of functionalities the `Engine` provides. For example: the connector can make use of their Parquet reader instead of using the reader from the `DefaultEngine`. More details on this [later](#step-2-build-your-own-engine).\n\n```xml\n<dependencies>\n  <dependency>\n    <groupId>io.delta</groupId>\n    <artifactId>delta-kernel-api</artifactId>\n    <version>${delta-kernel.version}</version>\n  </dependency>\n\n  <dependency>\n    <groupId>io.delta</groupId>\n    <artifactId>delta-kernel-defaults</artifactId>\n    <version>${delta-kernel.version}</version>\n  </dependency>\n</dependencies>\n```\n\nIf your connector is not using the [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) provided by the Kernel, the dependency `delta-kernel-defaults` from the above list can be skipped.\n\n## Read a Delta table in a single process\n\nIn this section, we will walk through how to build a very simple single-process Delta connector that can read a Delta table using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\n### Step 1: Full scan on a Delta table\n\nThe main entry point is [`io.delta.kernel.Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html) which is a programmatic representation of a Delta table. Say you have a Delta table at the directory `myTablePath`. You can create a `Table` object as follows:\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.*;\nimport org.apache.hadoop.conf.Configuration;\n\nString myTablePath = <my-table-path>; // fully qualified table path. Ex: file:/user/tables/myTable\nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n```\n\nNote the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) we are creating to bootstrap the `myTable` object. This object allows you to plug in your own libraries for computationally intensive operations like Parquet file reading, JSON parsing, etc. You can ignore it for now. We will discuss more about this later when we discuss how to build more complex connectors for distributed processing engines.\n\nFrom this `myTable` object you can create a [`Snapshot`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Snapshot.html) object which represents the consistent state (a.k.a. a snapshot consistency) in a specific version of the table.\n\n```java\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\n```\n\nNow that we have a consistent snapshot view of the table, we can query more details about the table. For example, you can get the version and schema of this snapshot.\n\n```java\nlong version = mySnapshot.getVersion(myEngine);\nStructType tableSchema = mySnapshot.getSchema(myEngine);\n```\n\nNext, to read the table data, we have to _build_ a [`Scan`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html) object. In order to build a `Scan` object, create a [`ScanBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/ScanBuilder.html) object which optionally allows selecting a subset of columns to read or setting a query filter. For now, ignore these optional settings.\n\n```java\nScan myScan = mySnapshot.getScanBuilder(myEngine).build()\n\n// Common information about scanning for all data files to read.\nRow scanState = myScan.getScanState(myEngine)\n\n// Information about the list of scan files to read\nCloseableIterator<FilteredColumnarBatch> scanFiles = myScan.getScanFiles(myEngine)\n```\n\nThis [`Scan`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html) object has all the necessary metadata to start reading the table. There are two crucial pieces of information needed for reading data from a file in the table.\n\n- `myScan.getScanFiles(Engine)`: Returns scan files as columnar batches (represented as an iterator of `FilteredColumnarBatch`es, more on that later) where each selected row in the batch has information about a single file containing the table data.\n- `myScan.getScanState(Engine)`: Returns the snapshot-level information needed for reading any file. Note that this is a single row and common to all scan files.\n\nFor each scan file the physical data must be read from the file. The columns to read are specified in the scan file state. Once the physical data is read, you have to call [`ScanFile.transformPhysicalData(…)`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html#transformPhysicalData-io.delta.kernel.engine.Engine-io.delta.kernel.data.Row-io.delta.kernel.data.Row-io.delta.kernel.utils.CloseableIterator-) with the scan state and the physical data read from scan file. This API takes care of transforming (e.g. adding partition columns) the physical data into logical data of the table. Here is an example of reading all the table data in a single thread.\n\n```java\nCloserableIterator<FilteredColumnarBatch> fileIter = scanObject.getScanFiles(myEngine);\n\nRow scanStateRow = scanObject.getScanState(myEngine);\n\nwhile(fileIter.hasNext()) {\n  FilteredColumnarBatch scanFileColumnarBatch = fileIter.next();\n\n  // Get the physical read schema of columns to read from the Parquet data files\n  StructType physicalReadSchema =\n    ScanStateRow.getPhysicalDataReadSchema(engine, scanStateRow);\n\n  try (CloseableIterator<Row> scanFileRows = scanFileColumnarBatch.getRows()) {\n    while (scanFileRows.hasNext()) {\n      Row scanFileRow = scanFileRows.next();\n\n      // From the scan file row, extract the file path, size and modification time metadata\n      // needed to read the file.\n      FileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow);\n\n      // Open the scan file which is a Parquet file using connector's own\n      // Parquet reader or default Parquet reader provided by the Kernel (which\n      // is used in this example).\n      CloseableIterator<ColumnarBatch> physicalDataIter =\n        engine.getParquetHandler().readParquetFiles(\n          singletonCloseableIterator(fileStatus),\n          physicalReadSchema,\n          Optional.empty() /* optional predicate the connector can apply to filter data from the reader */\n        );\n\n      // Now the physical data read from the Parquet data file is converted to a table\n      // logical data. Logical data may include the addition of partition columns and/or\n      // subset of rows deleted\n      try (\n         CloseableIterator<FilteredColumnarBatch> transformedData =\n           Scan.transformPhysicalData(\n             engine,\n             scanStateRow,\n             scanFileRow,\n             physicalDataIter)) {\n        while (transformedData.hasNext()) {\n          FilteredColumnarBatch logicalData = transformedData.next();\n          ColumnarBatch dataBatch = logicalData.getData();\n\n          // Not all rows in `dataBatch` are in the selected output.\n          // An optional selection vector determines whether a row with a\n          // specific row index is in the final output or not.\n          Optional<ColumnVector> selectionVector = dataReadResult.getSelectionVector();\n\n          // access the data for the column at ordinal 0\n          ColumnVector column0 = dataBatch.getColumnVector(0);\n          for (int rowIndex = 0; rowIndex < column0.getSize(); rowIndex++) {\n            // check if the row is selected or not\n            if (!selectionVector.isPresent() || // there is no selection vector, all records are selected\n               (!selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId)))  {\n              // Assuming the column type is String.\n              // If it is a different type, call the relevant function on the `ColumnVector`\n              System.out.println(column0.getString(rowIndex));\n            }\n          }\n\n          // access the data for column at ordinal 1\n          ColumnVector column1 = dataBatch.getColumnVector(1);\n          for (int rowIndex = 0; rowIndex < column1.getSize(); rowIndex++) {\n            // check if the row is selected or not\n            if (!selectionVector.isPresent() || // there is no selection vector, all records are selected\n               (!selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId)))  {\n              // Assuming the column type is Long.\n              // If it is a different type, call the relevant function on the `ColumnVector`\n              System.out.println(column1.getLong(rowIndex));\n            }\n          }\n          // .. more ..\n        }\n      }\n    }\n  }\n}\n```\n\nA few working examples to read Delta tables within a single process are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  All the Delta protocol-level details are encoded in the rows returned by\n  `Scan.getScanFiles` API, but you do not have to understand them in order to\n  read the table data correctly. All you need is to get the Parquet file status\n  from each scan file row and read the data from the Parquet file into the\n  `ColumnarBatch` format. The physical data is converted into the logical data\n  of the table using\n  [`Scan.transformPhysicalData`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html#transformPhysicalData-io.delta.kernel.engine.Engine-io.delta.kernel.data.Row-io.delta.kernel.data.Row-io.delta.kernel.utils.CloseableIterator-).\n  Transformation to logical data is dictated by the protocol and the metadata of\n  the table and the scan file. As the Delta protocol evolves this transformation\n  step will evolve with it and your code will not have to change to accommodate\n  protocol changes. This is the major advantage of the abstractions provided by\n  Delta Kernel.\n</Aside>\n\n<Aside type=\"note\">\n  Observe that the same `Engine` instance `myEngine` is passed multiple times\n  whenever a call to Delta Kernel API is made. The reason for passing this\n  instance for every call is because it is the connector context, it should\n  maintained outside of the Delta Kernel APIs to give the connector control over\n  the `Engine`.\n</Aside>\n\n### Step 2: Improve scan performance with file skipping\n\nWe have explored how to do a full table scan. However, the real advantage of using the Delta format is that you can skip files using your query filters. To make this possible, Delta Kernel provides an [expression framework](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/package-summary.html) to encode your filters and provide them to Delta Kernel to skip files during the scan file generation. For example, say your table is partitioned by `columnX`, you want to query only the partition `columnX=1`. You can generate the expression and use it to build the scan as follows:\n\n<Tabs>\n  <TabItem label=\"Java\">\n    ```java\n    import io.delta.kernel.expressions.*;\n    import io.delta.kernel.defaults.engine.*;\n\n    Engine myEngine = DefaultEngine.create(new Configuration());\n\n    Predicate filter = new Predicate(\n      \"=\",\n      Arrays.asList(new Column(\"columnX\"), Literal.ofInt(1)));\n\n    Scan myFilteredScan = mySnapshot.buildScan(engine)\n      .withFilter(myEngine, filter)\n      .build()\n\n    // Subset of the given filter that is not guaranteed to be satisfied by\n    // Delta Kernel when it returns data. This filter is used by Delta Kernel\n    // to do data skipping as much as possible. The connector should use this filter\n    // on top of the data returned by Delta Kernel in order for further filtering.\n    Optional<Predicate> remainingFilter = myFilteredScan.getRemainingFilter();\n    ```\n\n  </TabItem>\n</Tabs>\n\nThe scan files returned by `myFilteredScan.getScanFiles(myEngine)` will have rows representing files only of the required partition. Similarly, you can provide filters for non-partition columns, and if the data in the table is well clustered by those columns, then Delta Kernel will be able to skip files as much as possible.\n\n## Create a Delta table\n\nIn this section, we will walk through how to build a Delta connector that can create a Delta table using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\nThe main entry point is [`io.delta.kernel.Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html) which is a programmatic representation of a Delta table. Say you want to create Delta table at the directory `myTablePath`. You can create a `Table` object as follows:\n\n<Tabs>\n  <TabItem label=\"Java\">\n    ```java\n    package io.delta.kernel.examples;\n\n    import io.delta.kernel.*;\n    import io.delta.kernel.types.*;\n    import io.delta.kernel.utils.CloseableIterable;\n\n    String myTablePath = <my-table-path>;\n    Configuration hadoopConf = new Configuration();\n    Engine myEngine = DefaultEngine.create(hadoopConf);\n    Table myTable = Table.forPath(myEngine, myTablePath);\n    ```\n\n  </TabItem>\n</Tabs>\n\nNote the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) we are creating to bootstrap the `myTable` object. This object allows you to plug in your own libraries for computationally intensive operations like Parquet file reading, JSON parsing, etc. You can ignore it for now. We will discuss more about this later when we discuss how to build more complex connectors for distributed processing engines.\n\nFrom this `myTable` object you can create a [`TransactionBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionBuilder.html) object which allows you to construct a [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Transaction.html) object\n\n```java\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */\n    Operation.CREATE_TABLE /* What is the operation we are trying to perform. This is noted in the Delta Log */\n  );\n```\n\nNow that you have the `TransactionBuilder` object, you can set the table schema and partition columns of the table.\n\n```java\nStructType mySchema = new StructType()\n  .add(\"id\", IntegerType.INTEGER)\n  .add(\"name\", StringType.STRING)\n  .add(\"city\", StringType.STRING)\n  .add(\"salary\", DoubleType.DOUBLE);\n\n// Partition columns are optional. Use it only if you are creating a partitioned table.\nList<String> myPartitionColumns = Collections.singletonList(\"city\");\n\n// Set the schema of the new table on the transaction builder\ntxnBuilder = txnBuilder\n  .withSchema(engine, mySchema);\n\n// Set the partition columns of the new table only if you are creating\n// a partitioned table; otherwise, this step can be skipped.\ntxnBuilder = txnBuilder\n  .withPartitionColumns(engine, examplePartitionColumns);\n```\n\n`TransactionBuilder` allows setting additional properties of the table such as enabling a certain Delta feature or setting identifiers for idempotent writes. We will be visiting these in the next sections. The next step is to build `Transaction` out of the `TransactionBuilder` object.\n\n```java\n// Build the transaction\nTransaction txn = txnBuilder.build(engine);\n```\n\n`Transaction` object allows the connector to optionally add any data and finally commit the transaction. A successful commit ensures that the table is created with the given schema. In this example, we are just creating a table and not adding any data as part of the table.\n\n```java\n// Commit the transaction.\n// As we are just creating the table and not adding any data, the `dataActions` is empty.\nTransactionCommitResult commitResult =\n  txn.commit(\n    engine,\n    CloseableIterable.emptyIterable() /* dataActions */\n  );\n```\n\nThe [`TransactionCommitResult`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html) contains the what version the transaction is committed as and whether the table is ready for a checkpoint. As we are creating a table the version will be `0`. We will be discussing later on what a checkpoint is and what it means for the table to be ready for the checkpoint.\n\nA few working examples to create partitioned and un-partitioned Delta tables are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n## Create a table and insert data into it\n\nIn this section, we will walk through how to build a Delta connector that can create a Delta table and insert data into the table (similar to `CREATE TABLE <table> AS <query>` construct in SQL) using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\nThe first step is to construct a `Transaction`. Below is the code for that. For more details on what each step of the code means, please read the [create table](#create-a-delta-table) section.\n\n```\npackage io.delta.kernel.examples;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterable;\n\nString myTablePath = <my-table-path>;\nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n\nStructType mySchema = new StructType()\n  .add(\"id\", IntegerType.INTEGER)\n  .add(\"name\", StringType.STRING)\n  .add(\"city\", StringType.STRING)\n  .add(\"salary\", DoubleType.DOUBLE);\n\n// Partition columns are optional. Use it only if you are creating a partitioned table.\nList<String> myPartitionColumns = Collections.singletonList(\"city\");\n\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */\n    Operation.WRITE /* What is the operation we are trying to perform? This is noted in the Delta Log */\n  );\n\n// Set the schema of the new table on the transaction builder\ntxnBuilder = txnBuilder\n  .withSchema(engine, mySchema);\n\n// Set the partition columns of the new table only if you are creating\n// a partitioned table; otherwise, this step can be skipped.\ntxnBuilder = txnBuilder\n  .withPartitionColumns(engine, examplePartitionColumns);\n\n// Build the transaction\nTransaction txn = txnBuilder.build(engine);\n```\n\nNow that we have the [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Transaction.html) object, the next step is generating the data that confirms the table schema and partitioned according to the table partitions.\n\n```java\nStructType dataSchema = txn.getSchema(engine)\n\n// Optional for un-partitioned tables\nList<String> partitionColumnNames = txn.getPartitionColumns(engine)\n```\n\nUsing the data schema and partition column names the connector can plan the query and generate data. At tasks that actually have the data to write to the table, the connector can ask the Kernel to transform the data given in the table schema into physical data that can actually be written to the Parquet data files. For partitioned tables, the data needs to be first partitioned by the partition columns, and then the connector should ask the Kernel to transform the data for each partition separately. The partitioning step is needed because any given data file in the Delta table contains data belonging to exactly one partition.\n\nGet the state of the transaction. The transaction state contains the information about how to convert the data in the table schema into physical data that needs to be written. The transformations depend on the protocol and features the table has.\n\n```java\nRow txnState = txn.getTransactionState(engine);\n```\n\nPrepare the data.\n\n```java\n// The data generated by the connector to write into a table\nCloseableIterator<FilteredColumnarBatch> data = ...\n\n// Create partition value map\nMap<String, Literal> partitionValues =\n  Collections.singletonMap(\n    \"city\", // partition column name\n     // partition value. Depending upon the partition column type, the\n     // partition value should be created. In this example, the partition\n     // column is of type StringType, so we are creating a string literal.\n     Literal.ofString(city)\n  );\n```\n\nThe connector data is passed as an iterator of [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html). Each of the `FilteredColumnarBatch` contains a [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) which actually contains the data in columnar access format and an optional section vector that allows the connector to specify which rows from the `ColumnarBatch` to write to the table.\n\nPartition values are passed as a map of the partition column name to the partition value. For an un-partitioned table, the map should be empty as it has no partition columns.\n\n```\n// Transform the logical data to physical data that needs to be written to the Parquet\n// files\nCloseableIterator<FilteredColumnarBatch> physicalData =\n  Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n```\n\nThe above code converts the given data for partitions into an iterator of `FilteredColumnarBatch` that needs to be written to the Parquet data files. In order to write the data files, the connector needs to get the [`WriteContext`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html) from Kernel, which tells the connector where to write the data files and what columns to collect statistics from each data file.\n\n```java\n// Get the write context\nDataWriteContext writeContext = Transaction.getWriteContext(engine, txnState, partitionValues);\n```\n\nNow, the connector has the physical data that needs to be written to Parquet data files, and where those files should be written, it can start writing the data files.\n\n```java\nCloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n  .writeParquetFiles(\n    writeContext.getTargetDirectory(),\n    physicalData,\n    writeContext.getStatisticsColumns()\n  );\n```\n\nIn the above code, the connector is making use of the `Engine` provided `ParquetHandler` to write the data, but the connector can choose its own Parquet file writer to write the data. Also note that the return of the above call is an iterator of [`DataFileStatus`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/DataFileStatus.html) for each data file written. It basically contains the file path, file metadata, and optional file-level statistics for columns specified by the [`WriteContext.getStatisticsColumns()`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html#getStatisticsColumns--))\n\nConvert each `DataFileStatus` into a Delta log action that can be written to the Delta table log.\n\n```java\nCloseableIterator<Row> dataActions =\n  Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);\n```\n\nThe next step is constructing [`CloseableIterable`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/CloseableIterable.html) out of the all the Delta log actions generated above. The reason for constructing an `Iterable` is that the transaction committing involves accessing the list of Delta log actions more than one time (in order to resolve conflicts when there are multiple writes to the table). Kernel provides a [utility method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/CloseableIterable.html#inMemoryIterable-io.delta.kernel.utils.CloseableIterator-) to create an in-memory version of `CloseableIterable`. This interface also gives the connector an option to implement a custom implementation that spills the data actions to disk when the contents are too big to fit in memory.\n\n```java\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(dataActions);\n```\n\nThe final step is committing the transaction!\n\n```java\nTransactionCommitStatus commitStatus = txn.commit(engine, dataActionsIterable)\n```\n\nThe [`TransactionCommitResult`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html) contains the what version the transaction is committed as and whether the table is ready for a checkpoint. As we are creating a table the version will be `0`. We will be discussing later on what a checkpoint is and what it means for the table to be ready for the checkpoint.\n\nA few working examples to create and insert data into partitioned and un-partitioned Delta tables are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n## Blind append into an existing Delta table\n\nIn this section, we will walk through how to build a Delta connector that inserts data into an existing Delta table (similar to `INSERT INTO <table> <query>` construct in SQL) using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository. The steps are exactly similar to [Create table and insert data into it](#create-a-table-and-insert-data-into-it) except that we won't be providing any schema or partition columns when building the `TransactionBuilder`\n\n```java\n// Create a `Table` object with the given destination table path\nTable table = Table.forPath(engine, tablePath);\n\n// Create a transaction builder to build the transaction\nTransactionBuilder txnBuilder =\n  table.createTransactionBuilder(\n    engine,\n    \"Examples\", /* engineInfo */\n    Operation.WRITE\n  );\n\n/ Build the transaction - no need to provide the schema as the table already exists.\nTransaction txn = txnBuilder.build(engine);\n\n// Get the transaction state\nRow txnState = txn.getTransactionState(engine);\n\nList<Row> dataActions = new ArrayList<>();\n\n// Generate the sample data for three partitions. Process each partition separately.\n// This is just an example. In a real-world scenario, the data may come from different\n// partitions. Connectors already have the capability to partition by partition values\n// before writing to the table\n\n// In the test data `city` is a partition column\nfor (String city : Arrays.asList(\"San Francisco\", \"Campbell\", \"San Jose\")) {\n  FilteredColumnarBatch batch1 = generatedPartitionedDataBatch(\n            5 /* offset */, city /* partition value */);\n  FilteredColumnarBatch batch2 = generatedPartitionedDataBatch(\n            5 /* offset */, city /* partition value */);\n  FilteredColumnarBatch batch3 = generatedPartitionedDataBatch(\n            10 /* offset */, city /* partition value */);\n\n    CloseableIterator<FilteredColumnarBatch> data =\n            toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n    // Create partition value map\n    Map<String, Literal> partitionValues =\n            Collections.singletonMap(\n                    \"city\", // partition column name\n                    // partition value. Depending upon the parition column type, the\n                    // partition value should be created. In this example, the partition\n                    // column is of type StringType, so we are creating a string literal.\n                    Literal.ofString(city));\n\n\n    // First transform the logical data to physical data that needs to be written\n    // to the Parquet\n    // files\n    CloseableIterator<FilteredColumnarBatch> physicalData =\n            Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n    // Get the write context\n    DataWriteContext writeContext =\n            Transaction.getWriteContext(engine, txnState, partitionValues);\n\n\n    // Now write the physical data to Parquet files\n    CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n            .writeParquetFiles(\n                    writeContext.getTargetDirectory(),\n                    physicalData,\n                    writeContext.getStatisticsColumns());\n\n\n    // Now convert the data file status to data actions that needs to be written to the Delta\n    // table log\n    CloseableIterator<Row> partitionDataActions = Transaction.generateAppendActions(\n            engine,\n            txnState,\n            dataFiles,\n            writeContext);\n\n// Now add all the partition data actions to the main data actions list. In a\n    // distributed query engine, the partition data is written to files at tasks on executor\n    // nodes. The data actions are collected at the driver node and then written to the\n    // Delta table log using the `Transaction.commit`\n    while (partitionDataActions.hasNext()) {\n        dataActions.add(partitionDataActions.next());\n    }\n}\n\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n        toCloseableIterator(dataActions.iterator()));\n\n// Commit the transaction.\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n```\n\n## Idempotent Blind Appends to a Delta Table\n\nIdempotent writes allow the connector to make sure the data belonging to a particular transaction version and application id is inserted into the table at most once. In incremental processing systems (e.g. streaming systems), track progress using their own application-specific versions need to record what progress has been made in order to avoid duplicating data in the face of failures and retries during writes. By setting the transaction identifier, the Delta table can ensure that the data with the same identifier is not written multiple times. For more information refer to the Delta protocol section [Transaction Identifiers](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#transaction-identifiers)\n\nTo make the data append idempotent, set the transaction identifier on the [`TransactionBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionBuilder.html#withTransactionId-io.delta.kernel.engine.Engine-java.lang.String-long-)\n\n```java\n// Set the transaction identifiers for idempotent writes\n// Delta/Kernel makes sure that there exists only one transaction in the Delta log\n// with the given application id and txn version\ntxnBuilder =\n  txnBuilder.withTransactionId(\n    engine,\n    \"my app id\", /* application id */\n    100 /* monotonically increasing txn version with each new data insert */\n  );\n```\n\nThat's all the connector need to do for idempotent blind appends.\n\n## Checkpointing a Delta table\n\n[Checkpoints](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#checkpoints) are an optimization in Delta Log in order to construct the state of the Delta table faster. It basically contains the state of the table at the version the checkpoint is created. Delta Kernel allows the connector to optionally make the checkpoints. It is created for every few commits (configurable table property) on the table.\n\nThe result of `Transaction.commit` returns a `TransactionCommitResult` that contains the version the transaction is committed as and whether the table is [read for checkpoint](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html#isReadyForCheckpoint--). Creating a checkpoint takes time as it needs to construct the entire state of the table. If the connector doesn't want to checkpoint by itself but uses other connectors that are faster in creating a checkpoint, it can skip the checkpointing step.\n\nIf it wants to checkpoint, the `Table` object has an [API](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html#checkpoint-io.delta.kernel.engine.Engine-long-) to checkpoint the table.\n\n```java\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\nif (commitResult.isReadyForCheckpoint()) {\n  // Checkpoint the table\n  Table.forPath(engine, tablePath).checkpoint(engine, commitResult.getVersion());\n}\n```\n\n## Build a Delta connector for a distributed processing engine\n\nUnlike simple applications that just read the table in a single process, building a connector for complex processing engines like Apache Spark™ and Trino can require quite a bit of additional effort. For example, to build a connector for an SQL engine you have to do the following\n\n- Understand the APIs provided by the engine to build connectors and how Delta Kernel can be used to provide the information necessary for the connector + engine to operate on a Delta table.\n- Decide what libraries to use to do computationally expensive operations like reading Parquet files, parsing JSON, computing expressions, etc. Delta Kernel provides all the extension points to allow you to plug in any library without having to understand all the low-level details of the Delta protocol.\n- Deal with details specific to distributed engines. For example,\n  - Serialization of Delta table metadata provided by Delta Kernel.\n  - Efficiently transforming data read from Parquet into the engine in-memory processing format.\n\nIn this section, we are going to outline the steps needed to build a connector.\n\n### Step 0: Validate the prerequisites\n\nIn the previous section showing how to read a simple table, we were briefly introduced to the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html). This is the main extension point where you can plug in your implementations of computationally-expensive operations like reading Parquet files, parsing JSON, etc. For the simple case, we were using a default implementation of the helper that works in most cases. However, for building a high-performance connector for a complex processing engine, you will very likely need to provide your own implementation using the libraries that work with your engine. So before you start building your connector, it is important to understand these requirements and plan for building your own engine.\n\nHere are the libraries/capabilities you need to build a connector that can read the Delta table\n\n- Perform file listing and file reads from your storage/file system.\n- Read Parquet files in columnar data, preferably in an in-memory columnar format.\n- Parse JSON data\n- Read JSON files\n- Evaluate expressions on in-memory columnar batches\n\nFor each of these capabilities, you can choose to build your own implementation or reuse the default implementation.\n\n### Step 1: Set up Delta Kernel in your connector project\n\nIn the Delta Kernel project, there are multiple dependencies you can choose to depend on.\n\n1. Delta Kernel core APIs - This is a must-have dependency, which contains all the main APIs like Table, Snapshot, and Scan that you will use to access the metadata and data of the Delta table. This has very few dependencies reducing the chance of conflicts with any dependencies in your connector and engine. This also provides the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface which allows you to plug in your implementations of computationally expensive operations, but it does not provide any implementation of this interface.\n2. Delta Kernel default- This has a default implementation called [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultEngine.html) and additional dependencies such as `Hadoop`. If you wish to reuse all or parts of this implementation, then you can optionally depend on this.\n\n#### Set up Java projects\n\nAs discussed above, you can import one or both of the artifacts as follows:\n\n```xml\n<!-- Must have dependency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-api</artifactId>\n  <version>${delta-kernel.version}</version>\n</dependency>\n\n<!-- Optional depdendency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-defaults</artifactId>\n  <version>${delta-kernel.version}</version>\n</dependency>\n```\n\n### Step 2: Build your own Engine\n\nIn this section, we are going to explore the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface and walk through how to implement your own implementation so that you can plug in your connector/engine-specific implementations of computationally-intensive operations, threading model, resource management, etc.\n\n> [!IMPORTANT] During the validation process, if you believe that all the dependencies of the default `Engine` implementation can work with your connector and engine, then you can skip this step and jump to Step 3 of implementing your connector using the default engine. If later you have the need to customize the helper for your connector, you can revisit this step.\n\n#### Step 2.1: Implement the `Engine` interface\n\nThe [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface combines a bunch of sub-interfaces each of which is designed for a specific purpose. Here is a brief overview of the subinterfaces. See the API docs (Java) for a more detailed view.\n\n```java\ninterface Engine {\n  /**\n   * Get the connector provided {@link ExpressionHandler}.\n   * @return An implementation of {@link ExpressionHandler}.\n  */\n  ExpressionHandler getExpressionHandler();\n\n  /**\n   * Get the connector provided {@link JsonHandler}.\n   * @return An implementation of {@link JsonHandler}.\n   */\n  JsonHandler getJsonHandler();\n\n  /**\n   * Get the connector provided {@link FileSystemClient}.\n   * @return An implementation of {@link FileSystemClient}.\n   */\n  FileSystemClient getFileSystemClient();\n\n  /**\n   * Get the connector provided {@link ParquetHandler}.\n   * @return An implementation of {@link ParquetHandler}.\n   */\n  ParquetHandler getParquetHandler();\n}\n```\n\nTo build your own `Engine` implementation, you can choose to either use the default implementations of each sub-interface or completely build every one from scratch.\n\n```java\nclass MyEngine extends DefaultEngine {\n\n  FileSystemClient getFileSystemClient() {\n    // Build a new implementation from scratch\n    return new MyFileSystemClient();\n  }\n\n  // For all other sub-clients, use the default implementations provided by the `DefaultEngine`.\n}\n```\n\nNext, we will walk through how to implement each interface.\n\n#### Step 2.2: Implement `FileSystemClient` interface\n\nThe [`FileSystemClient`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/FileSystemClient.html) interface contains basic file system operations like listing directories, resolving paths into a fully qualified path and reading bytes from files. Implementation of this interface must take care of the following when interacting with storage systems such as S3, Hadoop, or ADLS:\n\n- Credentials and permissions: The connector must populate its `FileSystemClient` with the necessary configurations and credentials for the client to retrieve the necessary data from the storage system. For example, an implementation based on Hadoop's FileSystem abstractions can be passed S3 credentials via the Hadoop configurations.\n- Decryption: If file system objects are encrypted, then the implementation must decrypt the data before returning the data.\n\n#### Step 2.3: Implement `ParquetHandler`\n\nAs the name suggests, this interface contains everything related to reading and writing Parquet files. It has been designed such that a connector can plug in a wide variety of implementations, from a simple single-threaded reader to a very advanced multi-threaded reader with pre-fetching and advanced connector-specific expression pushdown. Let's explore the methods to implement, and the guarantees associated with them.\n\n##### Method `readParquetFiles(CloseableIterator<FileStatus> fileIter, StructType physicalSchema, java.util.Optional<Predicate> predicate)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#readParquetFiles-io.delta.kernel.utils.CloseableIterator-io.delta.kernel.types.StructType-)) takes as input `FileStatus`s which contains metadata such as file path, size etc. of the Parquet file to read. The columns to be read from the Parquet file are defined by the physical schema. To implement this method, you may have to first implement your own [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) which is used to represent the in-memory data generated from the Parquet files.\n\nWhen identifying the columns to read, note that there are multiple types of columns in the physical schema (represented as a [`StructType`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructType.html)).\n\n- Data columns: Columns that are expected to be read from the Parquet file. Based on the `StructField` object defining the column, read the column in the Parquet file that matches the same name or field id. If the column has a field id (stored as `parquet.field.id` in the `StructField` metadata) then the field id should be used to match the column in the Parquet file. Otherwise, the column name should be used for matching.\n- Metadata columns: These are special columns that must be populated using metadata about the Parquet file ([`StructField#isMetadataColumn`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructField.html#isMetadataColumn--) tells whether a column in `StructType` is a metadata column). To understand how to populate such a column, first match the column name against the set of standard metadata column name constants. For example,\n  - `StructFileld#isMetadataColumn()` returns true and the column name is `StructField.METADATA_ROW_INDEX_COLUMN_NAME`, then you have to a generate column vector populated with the actual index of each row in the Parquet file (that is, not indexed by the possible subset of rows returned after Parquet data skipping).\n\n##### Requirements and guarantees\n\nAny implementation must adhere to the following guarantees.\n\n- The schema of the returned `ColumnarBatch`es must match the physical schema.\n  - If a data column is not found and the `StructField.isNullable = true`, then return a `ColumnVector` of nulls. Throw an error if it is not nullable.\n- The output iterator must maintain ordering as the input iterator. That is, if `file1` is before `file2` in the input iterator, then columnar batches of `file1` must be before those of `file2` in the output iterator.\n\n##### Method `writeParquetFiles(String directoryPath, CloseableIterator<FilteredColumnarBatch> dataIter, java.util.List<Column> statsColumns)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#writeParquetFiles-java.lang.String-io.delta.kernel.utils.CloseableIterator-java.util.List-) takes given data writes it into one or more Parquet files into the given directory. The data is given as an iterator of [FilteredColumnarBatches](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) which contains a [ColumnarBatch](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and an optional selection vector containing one entry for each row in `ColumnarBatch` indicating whether a row is selected or not selected. The `ColumnarBatch` also contains the schema of the data. This schema should be converted to Parquet schema, including any field IDs present [`FieldMetadata`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/FieldMetadata.html) for each column `StructField`.\n\nThere is also the parameter `statsColumns`, which is a hint to the Parquet writer on what set of columns to collect stats for each file. The statistics include `min`, `max` and `null_count` for each column in the `statsColumns` list. Statistics collection is optional, but when present it is used by Kernel to persist the stats as part of the Delta table commit. This will help read queries prune un-needed data files based on the query predicate.\n\nFor each written data file, the caller is expecting a [`DataFileStatus`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/DataFileStatus.html) object. It contains the data file path, size, modification time, and optional column statistics.\n\n#### Method `writeParquetFileAtomically(String filePath, CloseableIterator<FilteredColumnarBatch> data)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#writeParquetFiles-java.lang.String-io.delta.kernel.utils.CloseableIterator-java.util.List-) writes the given `data` into Parquet file at location `filePath`. The write is an atomic write i.e., either a Parquet file is created with all given content or no Parquet file is created at all. This should not create a file with partial content in it.\n\nThe default implementation makes use of [`LogStore`](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) implementations from the [`delta-storage`](https://github.com/delta-io/delta/tree/master/storage) module to accomplish the atomicity. A connector that wants to implement their own version of `ParquetHandler` can take a look at the default implementation for details.\n\n##### Performance suggestions\n\n- The representation of data as `ColumnVector`s and `ColumnarBatch`es can have a significant impact on the query performance and it's best to read the Parquet file data directly into vectors and batches of the engine-native format to avoid potentially costly in-memory data format conversion. Create a Kernel `ColumnVector` and `ColumnarBatch` wrappers around the engine-native format equivalent classes.\n\n#### Step 2.4: Implement `ExpressionHandler` interface\n\nThe [`ExpressionHandler`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html) interface has all the methods needed for handling expressions that may be applied on columnar data.\n\n##### Method `getEvaluator(StructType batchSchema, Expression expresion, DataType outputType)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#getEvaluator-io.delta.kernel.types.StructType-io.delta.kernel.expressions.Expression-io.delta.kernel.types.DataType-) generates an object of type [`ExpressionEvaluator`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/ExpressionEvaluator.html) that can evaluate the `expression` on a batch of row data to produce a result of a single column vector. To generate this function, the `getEvaluator()` method takes as input the expression and the schema of the `ColumnarBatch`es of data on which the expressions will be applied. The same object can be used to evaluate multiple columnar batches of input with the same schema and expression the evaluator is created for.\n\n##### Method `getPredicateEvaluator(StructType inputSchema, Predicate predicate)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#createSelectionVector-boolean:A-int-int-) is for creating an expression evaluator for `Predicate` type expressions. The `Predicate` type expressions return a boolean value as output.\n\nThe returned object is of type [`PredicateEvaluator`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/PredicateEvaluator.html). This is a special interface for evaluating Predicate on input batch returns a selection vector containing one value for each row in input batch indicating whether the row has passed the predicate or not. Optionally it takes an existing selection vector along with the input batch for evaluation. The result selection vector is combined with the given existing selection vector and a new selection vector is returned. This mechanism allows running an input batch through several predicate evaluations without rewriting the input batch to remove rows that do not pass the predicate after each predicate evaluation. The new selection should be the same or more selective as the existing selection vector. For example, if a row is marked as unselected in the existing selection vector, then it should remain unselected in the returned selection vector even when the given predicate returns true for the row.\n\n##### Method `createSelectionVector(boolean[] values, int from, int to)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#createSelectionVector-boolean:A-int-int-) allows creating `ColumnVector` for boolean type values given as input. This allows the connector to maintain all `ColumnVector`s created in the desired memory format.\n\n##### Requirements and guarantees\n\nAny implementation must adhere to the following guarantees.\n\n- Implementation must handle all possible variations of expressions. If the implementation encounters an expression type that it does not know how to handle, then it must throw a specific language-dependent exception.\n  - Java: [NotSupportedException](https://docs.oracle.com/javaee/7/api/latest/javax/resource/NotSupportedException.html)\n- The `ColumnarBatch`es on which the generated `ExpressionEvaluator` is going to be used are guaranteed to have the schema provided during generation. Hence, it is safe to bind the expression evaluation logic to column ordinals instead of column names, thus making the actual evaluation faster.\n\n#### Step 2.5: Implement `JsonHandler`\n\n[This](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html) engine interface allows the connector to use plug-in their own JSON handling code and expose it to the Delta Kernel.\n\n##### Method `readJsonFiles(CloseableIterator<FileStatus> fileIter, StructType physicalSchema, java.util.Optional<Predicate> predicate)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#readJsonFiles-io.delta.kernel.utils.CloseableIterator-io.delta.kernel.types.StructType-) takes as input `FileStatus`s of the JSON files and returns the data in a series of columnar batches. The columns to be read from the JSON file are defined by the physical schema, and the return batches must match that schema. To implement this method, you may have to first implement your own [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html)and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) which is used to represent the in-memory data generated from the JSON files.\n\nWhen identifying the columns to read, note that there are multiple types of columns in the physical schema (represented as a [`StructType`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructType.html)).\n\n##### Method `parseJson(ColumnVector jsonStringVector, StructType outputSchema, java.util.Optional<ColumnVector> selectionVector)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#parseJson-io.delta.kernel.data.ColumnVector-io.delta.kernel.types.StructType-) allows parsing a `ColumnVector` of string values which are in JSON format into the output format specified by the `outputSchema`. If a given column in `outputSchema` is not found, then a null value is returned. It optionally takes a selection vector which indicates what entries in the input `ColumnVector` of strings to parse. If an entry is not selected then a `null` value is returned as parsed output for that particular entry in the output.\n\n##### Method `deserializeStructType(String structTypeJson)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#deserializeStructType-java.lang.String-) allows parsing JSON encoded (according to [Delta schema serialization rules](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#schema-serialization-format)) `StructType` schema into a `StructType`. Most implementations of `JsonHandler` do not need to implement this method and instead use the one in the [default `JsonHandler`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultJsonHandler.html) implementation.\n\n#### Method `writeJsonFileAtomically(String filePath, CloseableIterator<Row> data, boolean overwrite)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#writeJsonFileAtomically-java.lang.String-io.delta.kernel.utils.CloseableIterator-boolean-) writes the given `data` into a JSON file at location `filePath`. The write is an atomic write i.e., either a JSON file is created with all given content or no Parquet file is created at all. This should not create a file with partial content in it.\n\nThe default implementation makes use of [`LogStore`](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) implementations from the [`delta-storage`](https://github.com/delta-io/delta/tree/master/storage) module to accomplish the atomicity. A connector that wants to implement their own version of `JsonHandler` can take a look at the default implementation for details.\n\nThe implementation is expected to handle the serialization rules (converting the `Row` object to JSON string) as described in the [API Javadoc](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#writeJsonFileAtomically-java.lang.String-io.delta.kernel.utils.CloseableIterator-boolean-).\n\n#### Step 2.6: Implement `ColumnarBatch` and `ColumnVector`\n\n[`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) are two interfaces to represent the data read into memory from files. This representation can have a significant impact on query performance. Each engine likely has a native representation of in-memory data with which it applies data transformation operations. For example, in Apache Spark™, the row data is internally represented as `UnsafeRow` for efficient processing. So it's best to read the Parquet file data directly into vectors and batches of the native format to avoid potentially costly in-memory data format conversions. So the recommended approach is to build wrapper classes that extend the two interfaces but internally use engine-native classes to store the data. When the connector has to forward the columnar batches received from the kernel to the engine, it has to be smart enough to skip converting vectors and batches that are already in the engine-native format.\n\n### Step 3: Build read support in your connector\n\nIn this section, we are going to walk through the likely sequence of Kernel API calls your connector will have to make to read a table. The exact timing of making these calls in your connector in the context of connector-engine interactions depends entirely on the engine-connector APIs and is therefore beyond the scope of this guide. However, we will try to provide broad guidelines that are likely (but not guaranteed) to apply to your connector-engine setup. For this purpose, we are going to assume that the engine goes through the following phases when processing a read/scan query - logical plan analysis, physical plan generation, and physical plan execution. Based on these broad characterizations, a typical control and data flow for reading a Delta table is going to be as follows:\n\n| Step | Typical query phase when this step occurs |\n| --- | --- |\n| Resolve the table snapshot to query | Logical plan analysis phase when the plan's schema and other details need to be resolved and validated |\n| Resolve files to scan based on query parameters | Physical plan generation, when the final parameters of the scan are available. For example: Schema of data to read after pruning away unused columns. Query filters to apply after filter rearrangement |\n| Distribute the file information to workers | Physical plan execution, only if it is a distributed engine. |\n| Read the columnar data using the file information | Physical plan execution, when the data is being processed by the engine |\n\nLet's understand the details of each step.\n\n#### Step 3.1: Resolve the table snapshot to query\n\nThe first step is to resolve the consistent snapshot and the schema associated with it. This is often required by the connector/ engine to resolve and validate the logical plan of the scan query (if the concept of logical plan exists in your engine). To achieve this, the connector has to do the following.\n\n- Resolve the table path from the query: If the path is directly available, then this is easy. Otherwise, if it is a query based on a catalog table (for example, a Delta table defined in Hive Metastore), then the connector has to resolve the table path from the catalog.\n- Initialize the `Engine` object: Create a new instance of the `Engine` that you have chosen in Step 2.\n- Initialize the Kernel objects and get the schema: Assuming the query is on the latest available version/snapshot of the table, you can get the table schema as follows:\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = new MyEngine();\nTable myTable = Table.forPath(myTablePath);\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\nStructType mySchema = mySnapshot.getSchema(myEngine);\n```\n\nIf you want to query a specific version of the table (that is, not the schema), then you can get the required snapshot as `myTable.getSnapshot(version)`.\n\n#### Step 3.2: Resolve files to scan\n\nNext, we need to build a Scan object using more information from the query. Here we are going to assume that the connector/engine has been able to extract the following details from the query (say, after optimizing the logical plan):\n\n- Read schema: The columns in the table that the query needs to read. This may be the full set of columns or a subset of columns.\n- Query filters: The filters on partitions or data columns that can be used skip reading table data.\n\nTo provide this information to Kernel, you have to do the following:\n\n- Convert the engine-specific schema and filter expressions to Kernel schema and expressions: For schema, you have to create a `StructType` object. For the filters, you have to create an `Expression` object using all the available subclasses of `Expression`.\n- Build the scan with the converted information: Build the scan as follows:\n\n```java\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.types.*;\n\nStructType readSchema = ... ;  // convert engine schema\nPredicate filterExpr = ... ;   // convert engine filter expression\n\nScan myScan = mySnapshot.buildScan(engine)\n  .withFilter(myEngine, filterExpr)\n  .withReadSchema(myEngine, readSchema)\n  .build()\n```\n\n- Resolve the information required to file reads: The generated Scan object has two sets of information.\n  - Scan files: `myScan.getScanFiles()` returns an iterator of `ColumnarBatch`es. Each batch in the iterator contains rows and each row has information about a single file that has been selected based on the query filter.\n  - Scan state: `myScan.getScanState()` returns a `Row` that contains all the information that is common across all the files that need to be read.\n\n````java\nRow myScanStateRow = myScan.getScanState();\nCloseableIterator<FilteredColumnarBatch> myScanFilesAsBatches = myScan.getScanFiles();\n\n```java\nRow myScanStateRow = myScan.getScanState();\nCloseableIterator<FilteredColumnarBatch> myScanFilesAsBatches = myScan.getScanFiles();\n\nwhile (myScanFilesAsBatches.hasNext()) {\n  FilteredColumnarBatch scanFileBatch = myScanFilesAsBatches.next();\n\n  CloseableIterator<Row> myScanFilesAsRows = scanFileBatch.getRows();\n}\n````\n\nAs we will soon see, reading the columnar data from a selected file will need to use both, the scan state row, and a scan file row with the file information.\n\n##### Requirements and guarantees\n\nHere are the details you need to ensure when defining this scan.\n\n- The provided `readSchema` must be the exact schema of the data that the engine will expect when executing the query. Any mismatch in the schema defined during this query planning and the query execution will result in runtime failures. Hence you must build the scan with the readSchema only after the engine has finalized the logical plan after any optimizations like column pruning.\n- When applicable (for example, with Java Kernel APIs), you have to make sure to call the close() method as you consume the `ColumnarBatch`es of scan files (that is, either serialize the rows or use them to read the table data).\n\n#### Step 3.3: Distribute the file information to the workers\n\nIf you are building a connector for a distributed engine like Spark/Presto/Trino/Flink, then your connector has to send all the scan metadata from the query planning machine (henceforth called the driver) to task execution machines (henceforth called the workers). You will have to serialize and deserialize the scan state and scan file rows. It is the connector job to implement serialization and deserialization utilities for a [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html). If the connector wants to split reading one scan file into multiple tasks, it can add additional connector specific split context to the task. At the task, the connector can use its own Parquet reader to read the specific part of the file indicated by the split info.\n\n##### Custom `Row` Serializer/Deserializer\n\nHere are steps on how to build your own serializer/deserializer such that it will work with any [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html) of any schema.\n\n- Serializing\n  - First serialize the row schema, that is, `StructType` object.\n  - Then, use the schema to identify types of each column/ordinal in the `Row` and use that to serialize all the values one by one.\n- Deserializing\n  - Define your own class that extends the Row interface. It must be able to handle complex types like arrays, nested structs and maps.\n  - First deserialize the schema.\n  - Then, use the schema to deserialize the values and put them in an instance of your custom Row class.\n\n```java\nimport io.delta.kernel.utils.*;\n\n// In the driver where query planning is being done\nByte[] scanStateRowBytes = RowUtils.serialize(scanStateRow);\nByte[] scanFileRowBytes = RowUtils.serialize(scanFileRow);\n\n// Optionally the connector adds a split info to the task (scan file, scan state) to\n// split reading of a Parquet file into multiple tasks. The task gets split info\n// along with the scan file row and scan state row.\nSplit split = ...; // connector specific class, not related to Kernel\n\n// Send these over to the worker\n\n// In the worker when data will be read, after rowBytes have been sent over\nRow scanStateRow = RowUtils.deserialize(scanStateRowBytes);\nRow scanFileRow = RowUtils.deserialize(scanFileRowBytes);\nSplit split = ... deserialize split info ...;\n```\n\n#### Step 3.4: Read the columnar data\n\nFinally, we are ready to read the columnar data. You will have to do the following:\n\n- Read the physical data from Parquet file as indicated by the scan file row, scan state, and optionally the split info\n- Convert the physical data into logical data of the table using the Kernel's APIs.\n\n```java\nRow scanStateRow = ... ;\nRow scanFileRow = ... ;\nSplit split = ...;\n\n// Additional option predicate such as dynamic filters the connector wants to\n// pass to the reader when reading files.\nPredicate optPredicate = ...;\n\n// Get the physical read schema of columns to read from the Parquet data files\nStructType physicalReadSchema =\n  ScanStateRow.getPhysicalDataReadSchema(engine, scanStateRow);\n\n// From the scan file row, extract the file path, size and modification metadata\n// needed to read the file.\nFileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow);\n\n// Open the scan file which is a Parquet file using connector's own\n// Parquet reader which supports reading specific parts (split) of the file.\n// If the connector doesn't have its own Parquet reader, it can use the\n// default Parquet reader provider which at the moment doesn't support reading\n// a specific part of the file, but reads the entire file from the beginning.\nCloseableIterator<ColumnarBatch> physicalDataIter =\n  connectParquetReader.readParquetFile(\n    fileStatus\n    physicalReadSchema,\n    split, // what part of the Parquet file to read data from\n    optPredicate /* additional predicate the connector can apply to filter data from the reader */\n  );\n\n// Now the physical data read from the Parquet data file is converted to logical data\n// the table represents.\n// Logical data may include the addition of partition columns and/or\n// subset of rows deleted\nCloseableIterator<FilteredColumnarBatch> transformedData =\n  Scan.transformPhysicalData(\n    engine,\n    scanState,\n    scanFileRow,\n    physicalDataIter));\n```\n\n- Resolve the data in the batches: Each [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) has two components:\n  - Columnar batch (returned by `FilteredColumnarBatch.getData()`): This is the data read from the files having the schema matching the readSchema provided when the Scan object was built in the earlier step.\n  - Optional selection vector (returned by `FilteredColumnarBatch.getSelectionVector()`): Optionally, a boolean vector that will define which rows in the batch are valid and should be consumed by the engine.\n\nIf the selection vector is present, then you will have to apply it to the batch to resolve the final consumable data.\n\n- Convert to engine-specific data format: Each connector/engine has its own native row / columnar batch formats and interfaces. To return the read data batches to the engine, you have to convert them to fit those engine-specific formats and/or interfaces. Here are a few tips that you can follow to make this efficient.\n  - Matching the engine-specific format: Some engines may expect the data in an in-memory format that may be different from the data produced by `getData()`. So you will have to do the data conversion for each column vector in the batch as needed.\n  - Matching the engine-specific interfaces: You may have to implement wrapper classes that extend the engine-specific interfaces and appropriately encapsulate the row data.\n\nFor best performance, you can implement your own Parquet reader and other `Engine` implementations to make sure that every `ColumnVector` generated is already in the engine-native format thus eliminating any need to convert.\n\nNow you should be able to read the Delta table correctly.\n\n### Step 4: Build append support in your connector\n\nIn this section, we are going to walk through the likely sequence of Kernel API calls your connector will have to make to append data to a table. The exact timing of making these calls in your connector in the context of connector-engine interactions depends entirely on the engine-connector APIs and is, therefore, beyond the scope of this guide. However, we will try to provide broad guidelines that are likely (but not guaranteed) to apply to your connector-engine setup. For this purpose, we are going to assume that the engine goes through the following phases when processing a write query - logical plan analysis, physical plan generation, and physical plan execution. Based on these broad characterizations, a typical control and data flow for reading a Delta table is going to be as follows:\n\n| Step | Typical query phase when this step occurs |\n| --- | --- |\n| Determine the schema of the data that needs to be written to the table. Schema is derived from the existing table or from the parent operation of the `write` operator in the query plan when the table doesn't exist yet. | Logical plan analysis phase when the plan's schema (`write` operator schema matches the table schema, etc.) and other details need to be resolved and validated. |\n| Determine the physical partitioning of the data based on the table schema and partition columns either from the existing table or from the query plan (for new tables) | Physical plan generation, where the number of writer tasks, data schema and partitioning is determined |\n| Distribute the writer tasks definitions (which include the transaction state) to workers. | Physical plan execution, only if it is a distributed engine. |\n| Tasks write the data to data files and send the data file info to the driver. | Physical plan execution, when the data is actually written to the table location |\n| Finalize the query. Here, all the info of the data files written by the tasks is aggregated and committed to the transaction created at the beginning of the physical execution. | Finalize the query. This happens on the driver where the query has started. |\n\nLet's understand the details of each step.\n\n#### Step 4.1: Determine the schema of the data that needs to be written to the table\n\nThe first step is to resolve the output data schema. This is often required by the connector/ engine to resolve and validate the logical plan of the query (if the concept of logical plan exists in your engine). To achieve this, the connector has to do the following. At a high level query plan is a tree of operators where the leaf-level operators generate or read data from storage/tables and feed it upwards towards the parent operator nodes. This data transfer happens until it reaches the root operator node where the query is finalized (either the results are sent to the client or data is written to another table).\n\n- Create the `Table` object\n- From the `Table` object try to get the schema.\n  - If the table is not found\n    - the query includes creating the table (e.g., `CREATE TABLE AS` SQL query);\n      - the schema is derived from the operator above the `write` that feeds the data to the `write` operator.\n    - the query doesn't include creating new table, an exception is thrown saying the table is not found\n  - If the table already exists\n    - get the schema from the table and check if it matches the schema of the `write` operator. If not throw an exception.\n- Create a `TransactionBuilder` - this basically begins the steps of transaction construction.\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = new MyEngine();\nTable myTable = Table.forPath(myTablePath);\n\nStructType writeOperatorSchema = // ... derived from the query operator tree ...\nStructType dataSchema;\nboolean isNewTable = false;\n\ntry {\n  Snapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\n  dataSchema = mySnapshot.getSchema(myEngine);\n\n  // .. check dataSchema and writeOperatorSchema match ...\n} catch(TableNotFoundException e) {\n  isNewTable = true;\n  dataSchema = writeOperatorSchema;\n}\n\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */\n    Operation /* What is the operation we are trying to perform? This is noted in the Delta Log */\n  );\n\nif (isNewTable) {\n  // For a new table set the table schema in the transaction builder\n  txnBuilder = txnBuilder.withSchema(engine, dataSchema)\n}\n```\n\n#### Step 4.2: Determine the physical partitioning of the data based on the table schema and partition columns\n\nPartition columns are found either from the query (for new tables, the query defines the partition columns) or from the existing table.\n\n```java\nTransactionBuilder txnBuilder = ... from the last step ...\nTransaction txn;\n\nList<String> partitionColumns = ...\nif (newTable) {\n  partitionColumns = ... derive from the query parameters (ex. PARTITION BY clause in SQL) ...\n  txnBuilder = txnBuilder.withPartitionColumns(engine, partitionColumns);\n  txn = txnBuilder.build(engine);\n} else {\n  txn = txnBuilder.build(engine);\n  partitionColumns = txn.getPartitionColumns(engine);\n}\n```\n\nAt the end of this step, we have the `Transaction` and schema of the data to generate and its partitioning.\n\n#### Step 4.3: Distribute the writer tasks definitions (which include the transaction state) to workers\n\nIf you are building a connector for a distributed engine like Spark/Presto/Trino/Flink, then your connector has to send all the writer metadata from the query planning machine (henceforth called the driver) to task execution machines (henceforth called the workers). You will have to serialize and deserialize the transaction state. It is the connector job to implement serialization and deserialization utilities for a [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html). More details on a custom `Row` SerDe are found [here](#custom-row-serializerdeserializer).\n\n```java\nRow txnState = txn.getState(engine);\n\nString jsonTxnState = serializeToJson(txnState);\n```\n\n#### Step 4.4: Tasks write the data to data files and send the data file info to the driver\n\nIn this step (which is executed on the worker nodes inside each task):\n\n- Deserialize the transaction state\n- Writer operator within the task gets the data from its parent operator.\n- The data is converted into a `FilteredColumnarBatch`. Each [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) has two components:\n  - Columnar batch (returned by `FilteredColumnarBatch.getData()`): This is the data read from the files having the schema matching the readSchema provided when the Scan object was built in the earlier step.\n  - Optional selection vector (returned by `FilteredColumnarBatch.getSelectionVector()`): Optionally, a boolean vector that will define which rows in the batch are valid and should be consumed by the engine.\n- The connector can create `FilteredColumnBatch` wrapper around data in its own in-memory format.\n- Check if the data is partitioned or not. If not partitioned, partition the data by partition values.\n- For each partition generate the map of the partition column to the partition value\n- Use Kernel to convert the partitioned data into physical data that should go into the data files\n- Write the physical data into one or more data files.\n- Convert data file statues into a Delta log actions\n- Serialize the Delta log action `Row` objects and send them to the driver node\n\n```\nRow txnState = ... deserialize from JSON string sent by the driver ...\n\nCloseableIterator<FilteredColumnarBatch> data = ... generate data ...\n\n// If the table is un-partitioned then this is an empty map\nMap<String, Literal> partitionValues = ... prepare the partition values ...\n\n\n// First transform the logical data to physical data that needs to be written\n// to the Parquet files\nCloseableIterator<FilteredColumnarBatch> physicalData =\n  Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n// Get the write context\nDataWriteContext writeContext = Transaction.getWriteContext(engine, txnState, partitionValues);\n\n// Now write the physical data to Parquet files\nCloseableIterator<DataFileStatus> dataFiles =\n  engine.getParquetHandler()\n    .writeParquetFiles(\n      writeContext.getTargetDirectory(),\n      physicalData,\n      writeContext.getStatisticsColumns());\n\n// Now convert the data file status to data actions that needs to be written to the Delta table log\nCloseableIterator<Row> partitionDataActions =\n  Transaction.generateAppendActions(\n    engine,\n    txnState,\n    dataFiles,\n    writeContext);\n\n.... serialize `partitionDataActions` and send them to driver node\n```\n\n#### Step 4.5: Finalize the query\n\nAt the driver node, the delta log actions from all the tasks are received and committed to the transaction. The tasks send the Delta log actions as a serialized JSON and deserialize them back to `Row` objects.\n\n```\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n        toCloseableIterator(dataActions.iterator()));\n\n// Commit the transaction.\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n// Optional step\nif (commitResult.isReadyForCheckpoint()) {\n  // Checkpoint the table\n  Table.forPath(engine, tablePath).checkpoint(engine, commitResult.getVersion());\n}\n```\n\nThats it. Now you should be able to append data to Delta tables using the Kernel APIs.\n\n## Migration guide\n\nKernel APIs are still evolving and new features are being added. Kernel authors try to make the API changes backward compatible as much as they can with each new release, but sometimes it is hard to maintain the backward compatibility for a project that is evolving rapidly.\n\nThis section provides guidance on how to migrate your connector to the latest version of Delta Kernel. With each new release the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) are kept up-to-date with the latest API changes. You can refer to the examples to understand how to use the new APIs.\n\n### Migration from Delta Lake version 3.1.0 to 3.2.0\n\nFollowing are API changes in Delta Kernel 3.2.0 that may require changes in your connector.\n\n#### Rename `TableClient` to [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html)\n\nThe `TableClient` interface has been renamed to `Engine`. This is the most significant API change in this release. The `TableClient` interface name is not exactly representing the functionality it provides. At a high level it provides capabilities such as reading Parquet files, JSON files, evaluating expressions on data and file system functionality. These are basically the heavy lift operations that Kernel depends on as a separate interface to allow the connectors to substitute their own custom implementation of the same functionality (e.g. custom Parquet reader). Essentially, these functionalities are the core of the `engine` functionalities. By renaming to `Engine`, we are representing the interface functionality with a proper name that is easy to understand.\n\nThe `DefaultTableClient` has been renamed to [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultEngine.html).\n\n#### [`Table.forPath(Engine engine, String tablePath)`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html#forPath-io.delta.kernel.engine.Engine-java.lang.String-) behavior change\n\nEarlier when a non-existent table path is passed, the API used to throw `TableNotFoundException`. Now it doesn't throw the exception. Instead, it returns a `Table` object. When trying to get a `Snapshot` from the table object it throws the `TableNotFoundException`.\n\n#### [`FileSystemClient.resolvePath`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultFileSystemClient.html#resolvePath-java.lang.String-) behavior change\n\nEarlier when a non-existent path is passed, the API used to throw `FileNotFoundException`. Now it doesn't throw the exception. It still resolves the given path into a fully qualified path.\n\n## Kernel Rust\n\nThe Rust Kernel is a set of libraries for building Delta connectors in native languages. Work in progress.\n\n## More Information\n\n- [Talk](https://www.youtube.com/watch?v=KVUMFv7470I) explaining the rationale behind Kernel and the API design (slides are available [here](https://docs.google.com/presentation/d/1PGSSuJ8ndghucSF9GpYgCi9oeRpWolFyehjQbPh92-U/edit) which are kept up-to-date with the changes).\n- [User guide](https://github.com/delta-io/delta/blob/master/kernel/USER_GUIDE.md) on the step-by-step process of using Kernel in a standalone Java program or in a distributed processing connector for reading and writing to Delta tables.\n- Example [Java programs](https://github.com/delta-io/delta/tree/master/kernel/examples) that illustrate how to read and write Delta tables using the Kernel APIs.\n- Table and default Engine API Java [documentation](/api/latest/java/kernel/index.html)\n- [Migration guide](https://github.com/delta-io/delta/blob/master/kernel/USER_GUIDE.md#migration-guide)\n"
  },
  {
    "path": "docs/src/content/docs/delta-more-connectors.mdx",
    "content": "---\ntitle: Other connectors\n---\nimport { Tabs, TabItem, Aside, Steps } from \"@astrojs/starlight/components\";\n\n## Apache Druid\n\nThis [connector](https://druid.apache.org/docs/latest/development/extensions-contrib/delta-lake/) allows [Apache Druid](https://druid.apache.org/) to read from Delta Lake.\n\n## Apache Pulsar\n\nThis [connector](https://github.com/streamnative/pulsar-io-lakehouse/blob/master/docs/delta-lake-demo.md) allows [Apache Pulsar](https://pulsar.apache.org/) to read from and write to Delta Lake.\n\n## ClickHouse\n\n[ClickHouse](https://clickhouse.com/) is a column-oriented database that allows users to run SQL queries on Delta Lake tables. This [connector](https://clickhouse.com/docs/en/engines/table-engines/integrations/deltalake) provides a read-only integration with existing Delta Lake tables in Amazon S3.\n\n## Dagster\n\nUse the [Delta Lake IO Manager](https://delta-io.github.io/delta-rs/integrations/delta-lake-dagster/) to read from and write to Delta Lake tables in your [Dagster](https://dagster.io/) orchestration pipelines.\n\n## FINOS Legend\n\nAn [extension](https://github.com/finos/legend-community-delta/blob/main/README.md) to the [FINOS](https://landscape.finos.org/) Legend framework for Apache Spark™ / Delta Lake based environment, combining best of open data standards with open source technologies. This connector allows Trino to read from and write to Delta Lake.\n\n## Hopsworks\n\nThis [connectors](https://docs.hopsworks.ai/latest/user_guides/fs/feature_group/create/#batch-write-api) allows [Hopsworks Feature Store](https://www.hopsworks.ai/dictionary/feature-store) store, manage, and serve feature data in Delta Lake.\n\n## Apache Hive\n\nThis integration enables reading Delta tables from Apache Hive. For details on installing the integration, see the [Delta Lake repository](https://github.com/delta-io/delta/tree/master/connectors/hive).\n\n## Kafka Delta Ingest\n\nThis [project](https://github.com/delta-io/kafka-delta-ingest) builds a highly efficient daemon for streaming data through Apache Kafka into Delta Lake.\n\n## SQL Delta Import\n\nThis [utility](https://github.com/delta-io/delta/blob/master/connectors/sql-delta-import/readme.md) is for importing data from a JDBC source into a Delta Lake table.\n\n## StarRocks\n\n[StarRocks](https://www.starrocks.io/), a Linux Foundation project, is a next-generation sub-second MPP OLAP database for full analytics scenarios, including multi-dimensional analytics, real-time analytics, and ad-hoc queries. StarRocks has the [ability to read](https://docs.starrocks.io/docs/introduction/StarRocks_intro/) from Delta Lake."
  },
  {
    "path": "docs/src/content/docs/delta-presto-integration.mdx",
    "content": "---\ntitle: Presto connector\ndescription: Learn how to set up an integration to enable you to read Delta tables from Presto.\n---\n\nSince Presto [version 0.269](https://prestodb.io/docs/0.269/release/release-0.269.html#delta-lake-connector-changes), Presto natively supports reading Delta Lake tables. For details on using the native Delta Lake connector, see [Delta Lake Connector - Presto](https://prestodb.io/docs/current/connector/deltalake.html). For Presto versions lower than [0.269](https://prestodb.io/docs/0.269/release/release-0.269.html#delta-lake-connector-changes), you can use the manifest-based approach detailed in [Presto, Trino, and Athena to Delta Lake integration using manifests](/presto-integration/).\n"
  },
  {
    "path": "docs/src/content/docs/delta-resources.mdx",
    "content": "---\ntitle: Delta Lake resources\ndescription: Learn about resources for understanding Delta Lake.\n---\n\n## Blog posts and talks\n\n[Delta Lake blog posts](https://delta.io/blog)\n\n[Delta Lake tutorials](https://delta.io/learn/tutorials/)\n\n[Delta Lake videos](https://delta.io/learn/videos/)\n\n## VLDB 2020 paper\n\n[Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores](https://databricks.com/wp-content/uploads/2020/08/p975-armbrust.pdf)\n\n## Examples\n\nThe Delta Lake GitHub repository has [Scala and Python examples](https://github.com/delta-io/delta/tree/master/examples/).\n\n## Delta Lake transaction log specification\n\nThe Delta Lake transaction log has a well-defined open protocol that can be used by any system to read the log. See [Delta Transaction Log Protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md).\n"
  },
  {
    "path": "docs/src/content/docs/delta-row-tracking.mdx",
    "content": "---\ntitle: Use row tracking for Delta tables\ndescription: Learn how Delta Lake row tracking allows tracking how rows change across table versions.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\nRow tracking allows Delta Lake to track row-level lineage in a Delta Lake table. When enabled on a Delta Lake table, row tracking adds two new metadata fields to the table:\n\n- **Row IDs** provide rows with an identifier that is unique within the table. A row keeps the same ID whenever it is modified using a `MERGE` or `UPDATE` statement.\n- **Row commit versions** record the last version of the table in which the row was modified. A row is assigned a new version whenever it is modified using a `MERGE` or `UPDATE` statement.\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 3.2.0 and above. Enabling this feature\n  on existing non-empty tables is available in Delta Lake 3.3.0 and above.\n</Aside>\n\n## Enable row tracking\n\n<Aside type=\"caution\">\n  Tables created with row tracking enabled have the row tracking Delta Lake\n  table feature enabled at creation and use Delta Lake writer version 7. Table\n  protocol versions cannot be downgraded, and tables with row tracking enabled\n  are not writeable by Delta Lake clients that do not support all enabled Delta\n  Lake writer protocol table features. See [How does Delta Lake manage feature\n  compatibility?](/versioning/).\n</Aside>\n\nYou must explicitly enable row tracking using one of the following methods:\n\n- **New table**: Set the table property `delta.enableRowTracking = true` in the `CREATE TABLE` command.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    -- Create an empty table\n    CREATE TABLE student (id INT, name STRING, age INT)\n    TBLPROPERTIES ('delta.enableRowTracking' = 'true');\n\n    -- Using a CTAS statement\n    CREATE TABLE course_new\n    TBLPROPERTIES ('delta.enableRowTracking' = 'true')\n    AS SELECT * FROM course_old;\n\n    -- Using a LIKE statement to copy configuration\n    CREATE TABLE graduate LIKE student;\n\n    -- Using a CLONE statement to copy configuration\n    CREATE TABLE graduate CLONE student;\n    ```\n\n  </TabItem>\n</Tabs>\n\n- **Existing table**: Available from Delta 3.3 and above, set the table property `'delta.enableRowTracking' = 'true'` in the `ALTER TABLE` command.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    ALTER TABLE grade SET TBLPROPERTIES ('delta.enableRowTracking' = 'true');\n    ```\n\n  </TabItem>\n</Tabs>\n\n- **All new tables**: Set the configuration `spark.databricks.delta.properties.defaults.enableRowTracking = true` for the current session in the `SET` command.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    SET spark.databricks.delta.properties.defaults.enableRowTracking = true;\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.conf.set(\"spark.databricks.delta.properties.defaults.enableRowTracking\", True)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    spark.conf.set(\"spark.databricks.delta.properties.defaults.enableRowTracking\", true)\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"caution\">\n  Because cloning a Delta Lake table creates a separate history, the row ids and\n  row commit versions on cloned tables do not match that of the original table.\n</Aside>\n\n<Aside type=\"caution\">\n  Enabling row tracking on existing table will automatically assign row ids and\n  row commit versions to all existing rows in the table. This process may cause\n  multiple new versions of the table to be created and may take a long time.\n</Aside>\n\n### Row tracking storage\n\nEnabling row tracking may increase the size of the table. Delta Lake stores row tracking metadata fields in hidden metadata columns in the data files. Some operations, such as insert-only operations do not use these hidden columns and instead track the row ids and row commit versions using metadata in the Delta Lake log. Data reorganization operations such as `OPTIMIZE` and `REORG` cause the row ids and row commit versions to be tracked using the hidden metadata column, even when they were stored using metadata.\n\n## Read row tracking metadata fields\n\nRow tracking adds the following metadata fields that can be accessed when reading a table:\n\n| Column name | Type | Values |\n| --- | --- | --- |\n| `_metadata.row_id` | Long | The unique identifier of the row. |\n| `_metadata.row_commit_version` | Long | The table version at which the row was last inserted or updated. |\n\nThe row ids and row commit versions metadata fields are not automatically included when reading the table. Instead, these metadata fields must be manually selected from the hidden `_metadata` column which is available for all tables in Apache Spark.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    SELECT _metadata.row_id, _metadata.row_commit_version, * FROM table_name;\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.read.table(\"table_name\") \\\n      .select(\"_metadata.row_id\", \"_metadata.row_commit_version\", \"*\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    spark.read.table(\"table_name\")\n      .select(\"_metadata.row_id\", \"_metadata.row_commit_version\", \"*\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n## Disable row tracking\n\nRow tracking can be disabled to reduce the storage overhead of the metadata fields. After disabling row tracking the metadata fields remain available, but all rows always get assigned a new id and commit version whenever they are touched by an operation.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    ALTER TABLE table_name SET TBLPROPERTIES (delta.enableRowTracking = false);\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    spark.sql(\"ALTER TABLE table_name SET TBLPROPERTIES (delta.enableRowTracking = false)\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    spark.sql(\"ALTER TABLE table_name SET TBLPROPERTIES (delta.enableRowTracking = false)\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"caution\">\n  Disabling row tracking does not remove the corresponding table feature and\n  does not downgrade the table protocol version.\n</Aside>\n\n## Limitations\n\nThe following limitations exist:\n\n- The row ids and row commit versions metadata fields cannot be accessed while reading the [Change data feed](/delta-change-data-feed/).\n- Once the Row Tracking feature is added to the table it cannot be removed without recreating the table.\n"
  },
  {
    "path": "docs/src/content/docs/delta-sharing.mdx",
    "content": "---\ntitle: Read Delta Sharing Tables\ndescription: Learn how to perform reads on Delta Sharing tables.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n[Delta Sharing](https://delta.io/sharing/) is an open protocol for secure real-time exchange of large datasets, which enables organizations to share data in real time regardless of which computing platforms they use. It is a simple REST protocol that securely grants access to part of a cloud dataset and leverages modern cloud storage systems, such as S3, ADLS, GCS or R2, to reliably transfer data.\n\nIn Delta Sharing, data provider is the one who owns the original dataset or table, and shares it with a broad range of recipients. Each table can be configured to be shared with different options (history, filtering, etc.) We will focus on consuming the shared table in this doc.\n\nDelta Sharing data source supports most of the options provided by Apache Spark DataFrame for performing reads through [batch](/delta-batch), [streaming](/delta-streaming), or [table changes (CDF)](/delta-change-data-feed) APIs on shared tables. Delta Sharing doesn't support writing to a shared table. Please refer to the [Delta Sharing Repo](https://github.com/delta-io/delta-sharing/blob/main/README.md) for more details. Please follow the [quick start](https://github.com/delta-io/delta-sharing?tab=readme-ov-file#quick-start) to leverage the Delta Sharing python connector to discover the shared tables.\n\nFor Delta Sharing reads on shared tables with advanced Delta Lake features such as [Deletion Vectors](/delta-deletion-vectors) and [Column Mapping](/delta-column-mapping), you need to enable integration with Apache Spark DataSourceV2 and Catalog APIs (since delta-sharing-spark 3.1) by setting the same configurations as Delta Lake when you create a new `SparkSession`. See [Configure SparkSession](/delta-batch/#configure-sparksession).\n\n## Read a snapshot\n\nAfter you save the [Profile File](https://github.com/delta-io/delta-sharing/blob/main/PROTOCOL.md#profile-file-format) locally and launch Spark with the connector library, you can access shared tables. A profile file is provided by the data provider to the data recipient.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    -- A table path is the profile file path followed by `#` and the fully qualified name\n    -- of a table (`<share-name>.<schema-name>.<table-name>`).\n    CREATE TABLE mytable USING deltaSharing LOCATION '<profile-file-path>#<share-name>.<schema-name>.<table-name>';\n    SELECT * FROM mytable;\n    ```\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    # A table path is the profile file path followed by `#` and the fully qualified name\n    # of a table (`<share-name>.<schema-name>.<table-name>`).\n    table_path = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\"\n    df = spark.read.format(\"deltaSharing\").load(table_path)\n    ```\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    // A table path is the profile file path followed by `#` and the fully qualified name\n    // of a table (`<share-name>.<schema-name>.<table-name>`).\n    val tablePath = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\"\n    val df = spark.read.format(\"deltaSharing\").load(tablePath)\n    ```\n  </TabItem>\n  <TabItem label=\"Java\">\n    ```java\n    // A table path is the profile file path followed by `#` and the fully qualified name\n    // of a table (`<share-name>.<schema-name>.<table-name>`).\n    String tablePath = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\";\n    Dataset<Row> df = spark.read.format(\"deltaSharing\").load(tablePath);\n    ```\n  </TabItem>\n</Tabs>\n\nThe DataFrame returned automatically reads the most recent snapshot of the table for any query.\n\nDelta Sharing supports [predicate pushdown](https://github.com/delta-io/delta-sharing/blob/main/PROTOCOL.md#json-predicates-for-filtering) to efficiently fetch data from the Delta Sharing server when there are applicable predicates in the query.\n\n## Query an older snapshot of a shared table (time travel)\n\nOnce the data provider enables history sharing of the shared table, Delta Sharing time travel allows you to query an older snapshot of a shared table.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    SELECT * FROM mytable TIMESTAMP AS OF timestamp_expression\n    SELECT * FROM mytable VERSION AS OF version\n    ```\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    spark.read.format(\"deltaSharing\").option(\"timestampAsOf\", timestamp_string).load(tablePath)\n    spark.read.format(\"deltaSharing\").option(\"versionAsOf\", version).load(tablePath)\n    ```\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.read.format(\"deltaSharing\").option(\"timestampAsOf\", timestamp_string).load(tablePath)\n    spark.read.format(\"deltaSharing\").option(\"versionAsOf\", version).load(tablePath)\n    ```\n  </TabItem>\n</Tabs>\n\nThe `timestamp_expression` and `version` share the same syntax as [Delta](/delta-batch#timestamp-and-version-syntax).\n\n## Read Table Changes (CDF)\n\nOnce the data provider turns on CDF on the original Delta Lake table and shares it with history through Delta Sharing, the recipient can query CDF of a Delta Sharing table similar to [CDF of a Delta table](/delta-change-data-feed).\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE TABLE mytable USING deltaSharing LOCATION '<profile-file-path>#<share-name>.<schema-name>.<table-name>';\n    \n    -- version as ints or longs e.g. changes from version 0 to 10\n    SELECT * FROM table_changes('mytable', 0, 10)\n    \n    -- timestamp as string formatted timestamps\n    SELECT * FROM table_changes('mytable', '2021-04-21 05:45:46', '2021-05-21 12:00:00')\n    \n    -- providing only the startingVersion/timestamp\n    SELECT * FROM table_changes('mytable', 0)\n    ```\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    table_path = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\"\n    \n    # version as ints or longs\n    spark.read.format(\"deltaSharing\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingVersion\", 0) \\\n      .option(\"endingVersion\", 10) \\\n      .load(tablePath)\n    \n    # timestamps as formatted timestamp\n    spark.read.format(\"deltaSharing\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingTimestamp\", '2021-04-21 05:45:46') \\\n      .option(\"endingTimestamp\", '2021-05-21 12:00:00') \\\n      .load(tablePath)\n    \n    # providing only the startingVersion/timestamp\n    spark.read.format(\"deltaSharing\") \\\n      .option(\"readChangeFeed\", \"true\") \\\n      .option(\"startingVersion\", 0) \\\n      .load(tablePath)\n    ```\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    val tablePath = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\"\n    \n    // version as ints or longs\n    spark.read.format(\"deltaSharing\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", 0)\n      .option(\"endingVersion\", 10)\n      .load(tablePath)\n    \n    // timestamps as formatted timestamp\n    spark.read.format(\"deltaSharing\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingTimestamp\", \"2024-01-18 05:45:46\")\n      .option(\"endingTimestamp\", \"2024-01-18 12:00:00\")\n      .load(tablePath)\n    \n    // providing only the startingVersion/timestamp\n    spark.read.format(\"deltaSharing\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", 0)\n      .load(tablePath)\n    ```\n  </TabItem>\n</Tabs>\n\n## Streaming\n\nDelta Sharing Streaming is deeply integrated with [Spark Structured Streaming](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html) through `readStream`, and able to connect with any sink that is able to perform `writeStream`.\n\nOnce the data provider shares a table with history, the recipient can perform a streaming query on the table. When you load a Delta Sharing table as a stream source and use it in a streaming query, the query processes all of the data present in the shared table as well as any new data that arrives after the stream has started.\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    val tablePath = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\"\n    \n    spark.readStream.format(\"deltaSharing\").load(tablePath)\n    ```\n  </TabItem>\n</Tabs>\n\nDelta Sharing Streaming supports the following functionalities in the same way as Delta Streaming: [Limit input rate](/delta-streaming/#limit-input-rate), [Ignore updates and deletes](/delta-streaming/#ignore-updates-and-deletes), [Specify initial position](/delta-streaming/#specify-initial-position)\n\nIn addition, `maxVersionsPerRpc` is provided to decide how many versions of files are requested from the server in every Delta Sharing rpc. This is to help reduce the per rpc workload and make the Delta sharing streaming job more stable, especially when many new versions have accumulated when the streaming resumes from a checkpoint. The default is 100.\n\n<Aside type=\"note\">\n  Trigger.AvailableNow is not supported in Delta Sharing Streaming. You can use\n  Trigger.Once as a workaround, and at a proper frequency to catch up with the\n  changes in the server.\n</Aside>\n\n## Read Advanced Delta Lake Features in Delta Sharing\n\nIn order to support advanced Delta Lake features in Delta Sharing, \"Delta Format Sharing\" was introduced since delta-sharing-client 1.0 and delta-sharing-spark 3.1, in which the actions of a shared table are returned in Delta Lake format, allowing a Delta Lake library to read it.\n\nPlease remember to set the spark configurations mentioned in [Configure SparkSession](/delta-batch/#configure-sparksession) in order to read shared tables with Deletion Vectors and Column Mapping.\n\n| Read Table Feature | Available since version |\n| --- | --- |\n| [Deletion Vectors](/delta-deletion-vectors) | 3.1.0 |\n| [Column Mapping](/delta-column-mapping) | 3.1.0 |\n| [Timestamp without Timezone](https://spark.apache.org/docs/latest/sql-ref-datatypes.html) | 3.3.0 |\n| [Type widening (Preview)](/delta-type-widening) | 3.3.0 |\n| [Variant Type (Preview)](https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md) | 3.3.0 |\n\nBatch queries can be performed as is, because it can automatically resolve the `responseFormat` based on the table features of the shared table. An additional option `responseFormat=delta` needs to be set for cdf and streaming queries when reading shared tables with Deletion Vectors or Column Mapping enabled.\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    import org.apache.spark.sql.SparkSession\n    \n    val spark = SparkSession\n            .builder()\n            .appName(\"...\")\n            .master(\"...\")\n            .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n            .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n            .getOrCreate()\n    \n    val tablePath = \"<profile-file-path>#<share-name>.<schema-name>.<table-name>\"\n    \n    // Batch query\n    spark.read.format(\"deltaSharing\").load(tablePath)\n    \n    // CDF query\n    spark.read.format(\"deltaSharing\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"responseFormat\", \"delta\")\n      .option(\"startingVersion\", 1)\n      .load(tablePath)\n    \n    // Streaming query\n    spark.readStream.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n    ```\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content/docs/delta-spark-connect.mdx",
    "content": "---\ntitle: Delta Connect (aka Spark Connect Support in Delta)\ndescription: Learn about Delta Connect - Spark Connect Support in Delta.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 4.0.0 and above. Please note, Delta Connect is currently in preview and not recommended for production workloads.\n</Aside>\n\nDelta Connect adds [Spark Connect](https://spark.apache.org/docs/latest/spark-connect-overview.html) support to Delta Lake for Apache Spark. Spark Connect is a new initiative that adds a decoupled client-server infrastructure which allows remote connectivity from Spark from everywhere. Delta Connect allows all Delta Lake operations to work in your application running as a client connected to the Spark server.\n\n## Motivation\n\nDelta Connect is expected to br0ng the same benefits as Spark Connect:\n\n1. Upgrading to more recent versions of Spark and Delta Lake is now easier because the client interface is being completely decoupled from the server.\n2. Simpler integration of Spark and Delta Lake with developer tooling. IDEs no longer have to integrate with the full Spark and Delta Lake implementation, and instead can integrate with a thin-client.\n3. Support for languages other than Java/Scala and Python. Clients \"merely\" have to generate Protocol Buffers and therefore become simpler to implement.\n4. Spark and Delta Lake will become more stable, as user code is no longer running in the same JVM as Spark's driver.\n5. Remote connectivity. Code can run anywhere now, as there is a gRPC layer between the user interface and the driver.\n\n## How to start the Spark Server with Delta\n\n1. Download `spark-4.0.0-bin-hadoop3.tgz` from [Spark 4.0.0](https://archive.apache.org/dist/spark/spark-4.0.0).\n2. Start the Spark Connect server with the Delta Lake Connect plugins:\n\n    ```bash\n    sbin/start-connect-server.sh \\ \n      --packages io.delta:delta-connect-server_2.13:4.0.0,com.google.protobuf:protobuf-java:3.25.1 \\ \n      --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n      --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\" \\\n      --conf \"spark.connect.extensions.relation.classes=org.apache.spark.sql.connect.delta.DeltaRelationPlugin\" \\\n      --conf \"spark.connect.extensions.command.classes=org.apache.spark.sql.connect.delta.DeltaCommandPlugin\"\n    ```\n\n## How to use the Python Spark Connect Client with Delta\n\nThe Delta Lake Connect Python client is included in the same PyPi package as Delta Lake Spark.\n\n1. `pip install pyspark==4.0.0`.\n2. `pip install delta-spark==4.0.0`.\n3. The usage is the same as Spark Connect (e.g. `./bin/pyspark --remote \"sc://localhost\"`). We just need to pass in a remote `SparkSession` (instead of a local one) to the `DeltaTable` API.\n\nAn example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    from delta.tables import DeltaTable\n    from pyspark.sql import SparkSession\n    from pyspark.sql.functions import *\n\n    deltaTable = DeltaTable.forName(spark, \"my_table\")\n    deltaTable.toDF().show()\n\n    deltaTable.update(\n      condition = \"id % 2 == 0\",\n      set = {\"id\": \"id + 100\"}\n    )\n    ```\n  \n  </TabItem>\n</Tabs>\n\n## How to use the Scala Spark Connect Client with Delta\n\nMake sure you are using Java 17!\n\n```bash\n./bin/spark-shell --remote \"sc://localhost\" --packages io.delta:delta-connect-client_2.13:4.0.0,com.google.protobuf:protobuf-java:3.25.1\n```\n\nAn example:\n    \n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Scala\">\n\n    ```scala\n    import io.delta.tables.DeltaTable\n\n    val deltaTable = DeltaTable.forName(spark, \"my_table\")\n    deltaTable.toDF.show()\n\n    deltaTable.updateExpr(\n      condition = \"id % 2 == 0\",\n      set = Map(\"id\" -> \"id + 100\")\n    )\n    ```\n  \n  </TabItem>\n</Tabs>"
  },
  {
    "path": "docs/src/content/docs/delta-standalone.mdx",
    "content": "---\ntitle: Delta Standalone (deprecated)\ndescription: Learn how to read and write Delta tables from JVM applications without Apache Spark.\n---\n\nimport { Aside } from \"@astrojs/starlight/components\";\n\n<Aside\n  type=\"danger\"\n  title=\"Warning!\"\n>\n  Delta Standalone is deprecated and will be removed in a future release. We\n  recommend using the [Delta Kernel](/delta-kernel) APIs.\n</Aside>\n\nThe Delta Standalone library is a single-node Java library that can be used to read from and write to Delta tables. Specifically, this library provides APIs to interact with a table's metadata in the transaction log, implementing the [Delta Transaction Log Protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md) to achieve the transactional guarantees of the Delta Lake format. Notably, this project doesn't depend on Apache Spark and has only a few transitive dependencies. Therefore, it can be used by any processing engine or application to access Delta tables.\n\n## Use cases\n\nDelta Standalone is optimized for cases when you want to read and write Delta tables by using a non-Spark engine of your choice. It is a \"low-level\" library, and we encourage developers to contribute open-source, higher-level connectors for their desired engines that use Delta Standalone for all Delta Lake metadata interaction. You can find a Hive source connector and Flink sink/source connector in the [Delta Lake](https://github.com/delta-io/delta) repository. Additional connectors are in development.\n\n### Caveats\n\nDelta Standalone minimizes memory usage in the JVM by loading the Delta Lake transaction log incrementally, using an iterator. However, Delta Standalone runs in a single JVM, and is limited to the processing and memory capabilities of that JVM. Users must configure the JVM to avoid out of memory (OOM) issues.\n\nDelta Standalone does provide basic APIs for reading Parquet data, but does not include APIs for writing Parquet data. Users must write out new Parquet data files themselves and then use Delta Standalone to commit those changes to the Delta table and make the new data visible to readers.\n\n## APIs\n\nDelta Standalone provides classes and entities to read data, query metadata, and commit to the transaction log. A few of them are highlighted here and with their key interfaces. See the [Java API docs](/api/latest/java/standalone/index.html) for the full set of classes and entities.\n\n### DeltaLog\n\n[DeltaLog](/api/latest/java/io/delta/standalone/DeltaLog.html) is the main interface for programmatically interacting with the metadata in the transaction log of a Delta table.\n\n- Instantiate a `DeltaLog` with `DeltaLog.forTable(hadoopConf, path)` and pass in the `path` of the root location of the Delta table.\n- Access the current snapshot with `DeltaLog::snapshot`.\n- Get the latest snapshot, including any new data files that were added to the log, with `DeltaLog::update`.\n- Get the snapshot at some historical state of the log with `DeltaLog::getSnapshotForTimestampAsOf` or `DeltaLog::getSnapshotForVersionAsOf`.\n- Start a new transaction to commit to the transaction log by using `DeltaLog::startTransaction`.\n- Get all metadata actions without computing a full Snapshot using `DeltaLog::getChanges`.\n\n### Snapshot\n\nA [Snapshot](/api/latest/java/io/delta/standalone/Snapshot.html) represents the state of the table at a specific version.\n\n- Get a list of the metadata files by using `Snapshot::getAllFiles`.\n- For a memory-optimized iterator over the metadata files, use `Snapshot::scan` to get a `DeltaScan` (as described later), optionally by passing in a `predicate` for partition filtering.\n- Read actual data with `Snapshot::open`, which returns an iterator over the rows of the Delta table.\n\n### OptimisticTransaction\n\nThe main class for committing a set of updates to the transaction log is [OptimisticTransaction](/api/latest/java/io/delta/standalone/OptimisticTransaction.html). During a transaction, all reads must go through the `OptimisticTransaction` instance rather than the `DeltaLog` in order to detect logical conflicts and concurrent updates.\n\n- Read metadata files during a transaction with `OptimisticTransaction::markFilesAsRead`, which returns a `DeltaScan` of files that match the `readPredicate`.\n- Commit to the transaction log with `OptimisticTransaction::commit`.\n- Get the latest version committed for a given application ID (for example, for idempotency) with `OptimisticTransaction::txnVersion`. (Note that this API requires users to commit `SetTransaction` actions.)\n- Update the medadata of the table upon committing with `OptimisticTransaction::updateMetadata`.\n\n### DeltaScan\n\n[DeltaScan](/api/latest/java/io/delta/standalone/DeltaScan.html) is a wrapper class for the files inside a `Snapshot` that match a given `readPredicate`.\n\n- Access the files that match the partition filter portion of the `readPredicate` with `DeltaScan::getFiles`. This returns a memory-optimized iterator over the metadata files in the table.\n- To further filter the returned files on non-partition columns, get the portion of input predicate not applied with `DeltaScan::getResidualPredicate`.\n\n## API compatibility\n\nThe only public APIs currently provided by Delta Standalone are in the `io.delta.standalone` package. Classes and methods in the `io.delta.standalone.internal` package are considered internal and are subject to change across minor and patch releases.\n\n## Project setup\n\nYou can add the Delta Standalone library as a dependency by using your preferred build tool. Delta Standalone depends upon the `hadoop-client` and `parquet-hadoop` packages. Example build files are listed in the following sections.\n\n### Environment requirements\n\n- JDK 8 or above.\n- Scala 2.11 or 2.12.\n\n### Build files\n\n#### Maven\n\nReplace the version of `hadoop-client` with the one you are using.\n\nScala 2.12:\n\n```xml\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-standalone_2.12</artifactId>\n  <version>0.5.0</version>\n</dependency>\n<dependency>\n  <groupId>org.apache.hadoop</groupId>\n  <artifactId>hadoop-client</artifactId>\n  <version>3.1.0</version>\n</dependency>\n```\n\nScala 2.11:\n\n```xml\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-standalone_2.11</artifactId>\n  <version>0.5.0</version>\n</dependency>\n<dependency>\n  <groupId>org.apache.hadoop</groupId>\n  <artifactId>hadoop-client</artifactId>\n  <version>3.1.0</version>\n</dependency>\n```\n\n#### SBT\n\nReplace the version of `hadoop-client` with the one you are using.\n\n```\nlibraryDependencies ++= Seq(\n  \"io.delta\" %% \"delta-standalone\" % \"0.5.0\",\n  \"org.apache.hadoop\" % \"hadoop-client\" % \"3.1.0)\n```\n\n#### `ParquetSchemaConverter` caveat\n\nDelta Standalone shades its own Parquet dependencies so that it works out-of-the-box and reduces dependency conflicts in your environment. However, if you would like to use utility class `io.delta.standalone.util.ParquetSchemaConverter`, then you must provide your own version of `org.apache.parquet:parquet-hadoop`.\n\n### Storage configuration\n\nDelta Lake ACID guarantees are based on the atomicity and durability guarantees of the storage system. Not all storage systems provide all the necessary guarantees.\n\nBecause storage systems do not necessarily provide all of these guarantees out-of-the-box, Delta Lake transactional operations typically go through the [LogStore API](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) instead of accessing the storage system directly. To provide the ACID guarantees for different storage systems, you may have to use different `LogStore` implementations. This section covers how to configure Delta Standalone for various storage systems. There are two categories of storage systems:\n\n- **Storage systems with built-in support**: For some storage systems, you do not need additional configurations. Delta Standalone uses the scheme of the path (that is, `s3a` in `s3a://path`) to dynamically identify the storage system and use the corresponding `LogStore` implementation that provides the transactional guarantees. However, for S3, there are additional caveats on concurrent writes. See the [section on S3](#amazon-s3-configuration) for details.\n\n- **Other storage systems**: The `LogStore`, similar to Apache Spark, uses the Hadoop `FileSystem` API to perform reads and writes. Delta Standalone supports concurrent reads on any storage system that provides an implementation of the `FileSystem` API. For concurrent writes with transactional guarantees, there are two cases based on the guarantees provided by the `FileSystem` implementation. If the implementation provides consistent listing and atomic renames-without-overwrite (that is, `rename(... , overwrite = false)` will either generate the target file atomically or fail if it already exists with `java.nio.file.FileAlreadyExistsException`), then the default `LogStore` implementation using renames will allow concurrent writes with guarantees. Otherwise, you must configure a custom implementation of `LogStore` by setting the following Hadoop configuration when you instantiate a `DeltaLog` with `DeltaLog.forTable(hadoopConf, path)`:\n\n  ```java\n  delta.logStore.<scheme>.impl=<full-qualified-class-name>\n  ```\n\n  Here, `<scheme>` is the scheme of the paths of your storage system. This configures Delta Standalone to dynamically use the given `LogStore` implementation only for those paths. You can have multiple such configurations for different schemes in your application, thus allowing it to simultaneously read and write from different storage systems.\n\n  <Aside\n    type=\"note\"\n    title=\"Note\"\n  >\n    Before version 0.5.0, Delta Standalone supported configuring LogStores by\n    setting `io.delta.standalone.LOG_STORE_CLASS_KEY`. This approach is now\n    deprecated. Setting this configuration will use the configured `LogStore`\n    for all paths, thereby disabling the dynamic scheme-based delegation.\n  </Aside>\n\n#### Amazon S3 configuration\n\nDelta Standalone supports reads and writes to S3 in two different modes: Single-cluster and Multi-cluster.\n\n|  | Single-cluster | Multi-cluster |\n| --- | --- | --- |\n| Configuration | Comes out-of-the-box | Is experimental and requires extra configuration |\n| Reads | Supports concurrent reads from multiple clusters | Supports concurrent reads from multiple clusters |\n| Writes | Supports concurrent writes from a single cluster | Supports multi-cluster writes |\n| Permissions | S3 credentials | S3 and DynamoDB operating permissions |\n\n##### Single-cluster setup (default)\n\nBy default, Delta Standalone supports concurrent reads from multiple clusters. However, concurrent writes to S3 must originate from a single cluster to provide transactional guarantees. This is because S3 currently does not provide mutual exclusion, that is, there is no way to ensure that only one writer is able to create a file.\n\n<Aside\n  type=\"danger\"\n  title=\"Warning!\"\n>\n  Concurrent writes to the same Delta table from multiple Spark drivers can lead\n  to data loss.\n</Aside>\n\nTo use Delta Standalone with S3, you must meet the following requirements. If you are using access keys for authentication and authorization, you must configure a Hadoop Configuration specified as follows when you instantiate a `DeltaLog` with `DeltaLog.forTable(hadoopConf, path)`.\n\n###### Requirements (S3 single-cluster)\n\n- S3 credentials: [IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) (recommended) or access keys.\n- Hadoop's [AWS connector (hadoop-aws)](https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-aws) for the version of Hadoop that Delta Standalone is compiled with.\n\n###### Configuration (S3 single-cluster)\n\n1. Include `hadoop-aws` JAR in the classpath.\n2. Set up S3 credentials. We recommend that you use [IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) for authentication and authorization. But if you want to use keys, configure your `org.apache.hadoop.conf.Configuration` with:\n\n   ```java\n   conf.set(\"fs.s3a.access.key\", \"<your-s3-access-key>\");\n   conf.set(\"fs.s3a.secret.key\", \"<your-s3-secret-key>\");\n   ```\n\n##### Multi-cluster setup\n\n<Aside\n  type=\"note\"\n  title=\"Note\"\n>\nThis support is new and experimental.\n\nThis mode supports concurrent writes to S3 from multiple clusters. Enable multi-cluster support by configuring Delta Standalone to use the correct `LogStore` implementation. This implementation uses [DynamoDB](https://aws.amazon.com/dynamodb/) to provide mutual exclusion.\n\n</Aside>\n\n<Aside\n  type=\"danger\"\n  title=\"Warning!\"\n>\n  When writing from multiple clusters, all drivers must use this `LogStore`\n  implementation and the same DynamoDB table and region. If some drivers use the\n  default `LogStore` while others use this experimental `LogStore` then data\n  loss can occur.\n</Aside>\n\n###### Requirements (S3 multi-cluster)\n\n- All of the requirements listed in the [Requirements (S3 single-cluster)](#requirements-s3-single-cluster) section\n- In additon to S3 credentials, you also need DynamoDB operating permissions\n\n###### Configuration (S3 multi-cluster)\n\n1. Create the DynamoDB table. See [Create the DynamoDB table](/delta-storage#setup-configuration-s3-multi-cluster) for more details on creating a table yourself (recommended) or having it created for you automatically.\n2. Follow the configuration steps listed in [Configuration (S3 single-cluster)](#configuration-s3-single-cluster) section.\n3. Include the `delta-storage-s3-dynamodb` JAR in the classpath.\n4. Configure the `LogStore` implementation.\n\n   First, configure this `LogStore` implementation for the scheme `s3`. You can replicate this command for schemes `s3a` and `s3n` as well.\n\n   ```java\n   conf.set(\"delta.logStore.s3.impl\", \"io.delta.storage.S3DynamoDBLogStore\");\n   ```\n\n| Configuration Key | Description | Default |\n| --- | --- | --- |\n| io.delta.storage.S3DynamoDBLogStore.ddb.tableName | The name of the DynamoDB table to use | delta_log |\n| io.delta.storage.S3DynamoDBLogStore.ddb.region | The region to be used by the client | us-east-1 |\n| io.delta.storage.S3DynamoDBLogStore.credentials.provider | The AWSCredentialsProvider\\* used by the client | DefaultAWSCredentialsProviderChain |\n| io.delta.storage.S3DynamoDBLogStore.provisionedThroughput.rcu | (Table-creation-only\\*\\*) Read Capacity Units | 5 |\n| io.delta.storage.S3DynamoDBLogStore.provisionedThroughput.wcu | (Table-creation-only\\*\\*) Write Capacity Units | 5 |\n\n<br />\n\n<sup>\n  \\*For more details on AWS credential providers, see the [AWS\n  documentation](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html).\n</sup>\n\n<sup>\n  \\*\\*These configurations are only used when the given DynamoDB table doesn't\n  already exist and needs to be automatically created.\n</sup>\n\n###### Production Configuration (S3 multi-cluster)\n\nBy this point, this multi-cluster setup is fully operational. However, there is extra configuration you may do to improve performance and optimize storage when running in production. See the [Delta Lake documentation](/delta-storage#production-configuration-s3-multi-cluster) for more details.\n\n#### Microsoft Azure configuration\n\nDelta Standalone supports concurrent reads and writes from multiple clusters with full transactional guarantees for various Azure storage systems. To use an Azure storage system, you must satisfy the following requirements, and configure a Hadoop Configuration as specified when you instantiate a `DeltaLog` with `DeltaLog.forTable(hadoopConf, path)`.\n\n##### Azure Blob Storage\n\n###### Requirements (Azure Blob storage)\n\n- A [shared key](https://docs.microsoft.com/rest/api/latest/storageservices/authorize-with-shared-key) or [shared access signature (SAS)](https://docs.microsoft.com/azure/storage/common/storage-sas-overview).\n- Hadoop’s [Azure Blob Storage libraries](https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-azure) for a version compatible with the Hadoop version Delta Standalone was compiled with.\n  - 2.9.1+ for Hadoop 2\n  - 3.0.1+ for Hadoop 3\n\n###### Configuration (Azure Blob storage)\n\n1. Include `hadoop-azure` JAR in the classpath.\n2. Set up credentials.\n\n   - For an SAS token, configure `org.apache.hadoop.conf.Configuration`:\n\n     ```java\n     conf.set(\n       \"fs.azure.sas.<your-container-name>.<your-storage-account-name>.blob.core.windows.net\",\n       \"<complete-query-string-of-your-sas-for-the-container>\");\n     ```\n\n   - To specify an account access key:\n\n     ```java\n     conf.set(\n       \"fs.azure.account.key.<your-storage-account-name>.blob.core.windows.net\",\n       \"<your-storage-account-access-key>\");\n     ```\n\n##### Azure Data Lake Storage Gen1\n\n###### Requirements (ADLS Gen 1)\n\n- A [service principal](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) for OAuth 2.0 access.\n- Hadoop's [Azure Data Lake Storage Gen1 libraries](https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-azure-datalake) for a version that is compatible with the Hadoop version that was used to compile Delta Standalone.\n  - 2.9.1+ for Hadoop 2\n  - 3.0.1+ for Hadoop 3\n\n###### Configuration (ADLS Gen 1)\n\n1. Include `hadoop-azure-datalake` JAR in the classpath.\n2. Set up Azure Data Lake Storage Gen1 credentials. Configure `org.apache.hadoop.conf.Configuration`:\n\n   ```java\n   conf.set(\"dfs.adls.oauth2.access.token.provider.type\", \"ClientCredential\");\n   conf.set(\"dfs.adls.oauth2.client.id\", \"<your-oauth2-client-id>\");\n   conf.set(\"dfs.adls.oauth2.credential\", \"<your-oauth2-credential>\");\n   conf.set(\"dfs.adls.oauth2.refresh.url\", \"https://login.microsoftonline.com/<your-directory-id>/oauth2/token\");\n   ```\n\n##### Azure Data Lake Storage Gen2\n\n###### Requirements (ADLS Gen 2)\n\n- Account created in [Azure Data Lake Storage Gen2](https://docs.microsoft.com/azure/storage/blobs/create-data-lake-storage-account).\n- Service principal [created](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal) and [assigned the Storage Blob Data Contributor role](https://docs.microsoft.com/azure/storage/blobs/assign-azure-role-data-access) for the storage account.\n  - Make a note of the storage-account-name, directory-id (also known as tenant-id), application-id, and password of the principal. These will be used for configuration.\n- Hadoop's [Azure Data Lake Storage Gen2 libraries](https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-azure-datalake) version 3.2+ and Delta Standalone compiled with Hadoop 3.2+.\n\n###### Configuration (ADLS Gen 2)\n\n1. Include `hadoop-azure-datalake` JAR in the classpath. In addition, you may also have to include JARs for Maven artifacts `hadoop-azure` and `wildfly-openssl`.\n2. Set up Azure Data Lake Storage Gen2 credentials. Configure your `org.apache.hadoop.conf.Configuration` with:\n\n   ```java\n   conf.set(\"fs.azure.account.auth.type.<storage-account-name>.dfs.core.windows.net\", \"OAuth\");\n   conf.set(\"fs.azure.account.oauth.provider.type.<storage-account-name>.dfs.core.windows.net\", \"org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider\");\n   conf.set(\"fs.azure.account.oauth2.client.id.<storage-account-name>.dfs.core.windows.net\", \"<application-id>\");\n   conf.set(\"fs.azure.account.oauth2.client.secret.<storage-account-name>.dfs.core.windows.net\",\"<password>\");\n   conf.set(\"fs.azure.account.oauth2.client.endpoint.<storage-account-name>.dfs.core.windows.net\", \"https://login.microsoftonline.com/<directory-id>/oauth2/token\");\n   ```\n\n   where `<storage-account-name>`, `<application-id>`, `<directory-id>` and `<password>` are details of the service principal we set as requirements earlier.\n\n#### HDFS\n\nDelta Standalone has built-in support for HDFS with full transactional guarantees on concurrent reads and writes from multiple clusters. See [Hadoop documentation](https://hadoop.apache.org/docs/stable/) for configuring credentials.\n\n#### Google Cloud Storage\n\n##### Requirements (GCS)\n\n- JAR of the [GCS Connector (gcs-connector)](https://search.maven.org/search?q=a:gcs-connector) Maven artifact.\n- Google Cloud Storage account and credentials\n\n##### Configuration (GCS)\n\n1. Include the JAR for `gcs-connector` in the classpath. See the [documentation](https://cloud.google.com/dataproc/docs/tutorials/gcs-connector-spark-tutorial) for details on how to configure your project with the GCS connector.\n\n## Usage\n\nThis example shows how to use Delta Standalone to:\n\n- Find parquet files.\n- Write parquet data.\n- Commit to the transaction log.\n- Read from the transaction log.\n- Read back the Parquet data.\n\nPlease note that this example uses a fictitious, non-Spark engine `Zappy` to write the actual parquet data, as Delta Standalone does not provide any data-writing APIs. Instead, Delta Standalone Writer lets you commit metadata to the Delta log after you've written your data. This is why Delta Standalone works well with so many connectors (e.g. Flink, Presto, Trino, etc.) since they provide the parquet-writing functionality instead.\n\n### 1. SBT configuration\n\nThe following SBT project configuration is used:\n\n```scala\n// <project-root>/build.sbt\n\nscalaVersion := \"2.12.8\"\n\nlibraryDependencies ++= Seq(\n  \"io.delta\" %% \"delta-standalone\" % \"0.5.0\",\n  \"org.apache.hadoop\" % \"hadoop-client\" % \"3.1.0\")\n```\n\n### 2. Mock situation\n\nWe have a Delta table `Sales` storing sales data, but have realized all the data written on November 2021 for customer `XYZ` had incorrect `total_cost` values. Thus, we need to update all those records with the correct values. We will use a fictious distributed engine `Zappy` and Delta Standalone to update our Delta table.\n\nThe sales table schema is given below.\n\n```\nSales\n |-- year: int          // partition column\n |-- month: int         // partition column\n |-- day: int           // partition column\n |-- customer: string\n |-- sale_id: string\n |-- total_cost: float\n```\n\n### 3. Starting a transaction and finding relevant files\n\nSince we must read existing data in order to perform the desired update operation, we must use `OptimisticTransaction::markFilesAsRead` in order to automatically detect any concurrent modifications made to our read partitions. Since Delta Standalone only supports partition pruning, we must apply the residual predicate to further filter the returned files.\n\n```java\nimport io.delta.standalone.DeltaLog;\nimport io.delta.standalone.DeltaScan;\nimport io.delta.standalone.OptimisticTransaction;\nimport io.delta.standalone.actions.AddFile;\nimport io.delta.standalone.data.CloseableIterator;\nimport io.delta.standalone.expressions.And;\nimport io.delta.standalone.expressions.EqualTo;\nimport io.delta.standalone.expressions.Literal;\n\nDeltaLog log = DeltaLog.forTable(new Configuration(), \"/data/sales\");\nOptimisticTransaction txn = log.startTransaction();\n\nDeltaScan scan = txn.markFilesAsRead(\n    new And(\n        new And(\n            new EqualTo(schema.column(\"year\"), Literal.of(2021)),  // partition filter\n            new EqualTo(schema.column(\"month\"), Literal.of(11))),  // partition filter\n        new EqualTo(schema.column(\"customer\"), Literal.of(\"XYZ\"))  // non-partition filter\n    )\n);\n\nCloseableIterator<AddFile> iter = scan.getFiles();\nMap<String, AddFile> addFileMap = new HashMap<String, AddFile>();  // partition filtered files: year=2021, month=11\nwhile (iter.hasNext()) {\n    AddFile addFile = iter.next();\n    addFileMap.put(addFile.getPath(), addFile);\n}\niter.close();\n\nList<String> filteredFiles = ZappyReader.filterFiles( // fully filtered files: year=2021, month=11, customer=XYZ\n    addFileMap.keySet(),\n    toZappyExpression(scan.getResidualPredicate())\n);\n```\n\n### 4. Writing updated Parquet data\n\nSince Delta Standalone does not provide any Parquet data write APIs, we use `Zappy` to write the data.\n\n```java\nZappyDataFrame correctedSaleIdToTotalCost = ...;\n\nZappyDataFrame invalidSales = ZappyReader.readParquet(filteredFiles);\nZappyDataFrame correctedSales = invalidSales.join(correctedSaleIdToTotalCost, \"id\");\n\nZappyWriteResult dataWriteResult = ZappyWritter.writeParquet(\"/data/sales\", correctedSales);\n```\n\nThe written data files from the preceding code will have a hierarchy similar to the following:\n\n```shell\n$ tree /data/sales\n.\n├── _delta_log\n│   └── ...\n│   └── 00000000000000001082.json\n│   └── 00000000000000001083.json\n├── year=2019\n│   └── month=1\n...\n├── year=2020\n│   └── month=1\n│       └── day=1\n│           └── part-00000-195768ae-bad8-4c53-b0c2-e900e0f3eaee-c000.snappy.parquet // previous\n│           └── part-00001-53c3c553-f74b-4384-b9b5-7aa45bc2291b-c000.snappy.parquet // new\n|           ...\n│       └── day=2\n│           └── part-00000-b9afbcf5-b90d-4f92-97fd-a2522aa2d4f6-c000.snappy.parquet // previous\n│           └── part-00001-c0569730-5008-42fa-b6cb-5a152c133fde-c000.snappy.parquet // new\n|           ...\n```\n\n### 5. Committing to our Delta table\n\nNow that we've written the correct data, we need to commit to the transaction log to add the new files, and remove the old incorrect files.\n\n```java\nimport io.delta.standalone.Operation;\nimport io.delta.standalone.actions.RemoveFile;\nimport io.delta.standalone.exceptions.DeltaConcurrentModificationException;\nimport io.delta.standalone.types.StructType;\n\nList<RemoveFile> removeOldFiles = filteredFiles.stream()\n    .map(path -> addFileMap.get(path).remove())\n    .collect(Collectors.toList());\n\nList<AddFile> addNewFiles = dataWriteResult.getNewFiles()\n    .map(file ->\n        new AddFile(\n            file.getPath(),\n            file.getPartitionValues(),\n            file.getSize(),\n            System.currentTimeMillis(),\n            true, // isDataChange\n            null, // stats\n            null  // tags\n        );\n    ).collect(Collectors.toList());\n\nList<Action> totalCommitFiles = new ArrayList<>();\ntotalCommitFiles.addAll(removeOldFiles);\ntotalCommitFiles.addAll(addNewFiles);\n\ntry {\n    txn.commit(totalCommitFiles, new Operation(Operation.Name.UPDATE), \"Zippy/1.0.0\");\n} catch (DeltaConcurrentModificationException e) {\n    // handle exception here\n}\n```\n\n### 6. Reading from the Delta table\n\nDelta Standalone provides APIs that read both metadata and data, as follows.\n\n#### 6.1. Reading Parquet data (distributed)\n\nFor most use cases, and especially when you deal with large volumes of data, we recommend that you use the Delta Standalone library as your metadata-only reader, and then perform the Parquet data reading yourself, most likely in a distributed manner.\n\nDelta Standalone provides two APIs for reading the files in a given table snapshot. `Snapshot::getAllFiles` returns an in-memory list. As of 0.3.0, we also provide `Snapshot::scan(filter)::getFiles`, which supports partition pruning and an optimized internal iterator implementation. We will use the latter here.\n\n```java\nimport io.delta.standalone.Snapshot;\n\nDeltaLog log = DeltaLog.forTable(new Configuration(), \"/data/sales\");\nSnapshot latestSnapshot = log.update();\nStructType schema = latestSnapshot.getMetadata().getSchema();\nDeltaScan scan = latestSnapshot.scan(\n    new And(\n        new And(\n            new EqualTo(schema.column(\"year\"), Literal.of(2021)),\n            new EqualTo(schema.column(\"month\"), Literal.of(11))),\n        new EqualTo(schema.column(\"customer\"), Literal.of(\"XYZ\"))\n    )\n);\n\nCloseableIterator<AddFile> iter = scan.getFiles();\n\ntry {\n    while (iter.hasNext()) {\n        AddFile addFile = iter.next();\n\n        // Zappy engine to handle reading data in `addFile.getPath()` and apply any `scan.getResidualPredicate()`\n    }\n} finally {\n    iter.close();\n}\n```\n\n#### 6.2. Reading Parquet data (single-JVM)\n\nDelta Standalone allows reading the Parquet data directly, using `Snapshot::open`.\n\n```java\nimport io.delta.standalone.data.RowRecord;\n\nCloseableIterator<RowRecord> dataIter = log.update().open();\n\ntry {\n    while (dataIter.hasNext()) {\n        RowRecord row = dataIter.next();\n        int year = row.getInt(\"year\");\n        String customer = row.getString(\"customer\");\n        float totalCost = row.getFloat(\"total_cost\");\n    }\n} finally {\n    dataIter.close();\n}\n```\n\n## Reporting issues\n\nWe use [GitHub Issues](https://github.com/delta-io/connectors/issues) to track community reported issues. You can also [contact](#community) the community for getting answers.\n\n## Contributing\n\nWe welcome contributions to Delta Lake repository. We use [GitHub Pull Requests](https://github.com/delta-io/delta/pulls) for accepting changes.\n\n## Community\n\nThere are two ways to communicate with the Delta Lake community:\n\n- Public Slack Channel\n  - [Register to join the Slack channel](https://join.slack.com/t/delta-users/shared_invite/enQtNTY1NDg0ODcxOTI1LWJkZGU3ZmQ3MjkzNmY2ZDM0NjNlYjE4MWIzYjg2OWM1OTBmMWIxZTllMjg3ZmJkNjIwZmE1ZTZkMmQ0OTk5ZjA)\n  - [Sign in to the Slack channel](https://delta-users.slack.com/)\n- Public [mailing list](https://groups.google.com/forum/#!forum/delta-users)\n\n## Local development\n\nBefore local debugging of `standalone` tests in IntelliJ, run all tests with `build/sbt standalone/test`. This helps IntelliJ recognize the golden tables as class resources.\n"
  },
  {
    "path": "docs/src/content/docs/delta-starburst-integration.mdx",
    "content": "---\ntitle: Starburst connector\ndescription: Learn how to set up an integration to enable you to read Delta tables from Starburst.\n---\n\nStarburst natively supports reading and writing Delta Lake tables. For details on using the native Delta Lake connector, see [Delta Lake Connector - Starburst](https://docs.starburst.io/latest/connector/delta-lake.html)."
  },
  {
    "path": "docs/src/content/docs/delta-storage.mdx",
    "content": "---\ntitle: Storage configuration\ndescription: Learn how to configure Delta Lake on different storage systems.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\nDelta Lake ACID guarantees are predicated on the atomicity and durability guarantees of the storage system. Specifically, Delta Lake relies on the following when interacting with storage systems:\n\n- **Atomic visibility**: There must a way for a file to visible in its entirety or not visible at all.\n- **Mutual exclusion**: Only one writer must be able to create (or rename) a file at the final destination.\n- **Consistent listing**: Once a file has been written in a directory, all future listings for that directory must return that file.\n\nBecause storage systems do not necessarily provide all of these guarantees out-of-the-box, Delta Lake transactional operations typically go through the [LogStore API](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) instead of accessing the storage system directly. To provide the ACID guarantees for different storage systems, you may have to use different `LogStore` implementations. This article covers how to configure Delta Lake for various storage systems. There are two categories of storage systems:\n\n- **Storage systems with built-in support**: For some storage systems, you do not need additional configurations. Delta Lake uses the scheme of the path (that is, `s3a` in `s3a://path`) to dynamically identify the storage system and use the corresponding `LogStore` implementation that provides the transactional guarantees. However, for S3, there are additional caveats on concurrent writes. See the [section on S3](#amazon-s3) for details.\n\n- **Other storage systems**: The `LogStore`, similar to Apache Spark, uses Hadoop `FileSystem` API to perform reads and writes. So Delta Lake supports concurrent reads on any storage system that provides an implementation of `FileSystem` API. For concurrent writes with transactional guarantees, there are two cases based on the guarantees provided by `FileSystem` implementation. If the implementation provides consistent listing and atomic renames-without-overwrite (that is, `rename(..., overwrite = false)` will either generate the target file atomically or fail if it already exists with `java.nio.file.FileAlreadyExistsException`), then the default `LogStore` implementation using renames will allow concurrent writes with guarantees. Otherwise, you must configure a custom implementation of `LogStore` by setting the following Spark configuration:\n\n```ini\nspark.delta.logStore.<scheme>.impl=<full-qualified-class-name>\n```\n\nwhere `<scheme>` is the scheme of the paths of your storage system. This configures Delta Lake to dynamically use the given `LogStore` implementation only for those paths. You can have multiple such configurations for different schemes in your application, thus allowing it to simultaneously read and write from different storage systems.\n\n<Aside type=\"note\">\n  - Delta Lake on local file system may not support concurrent transactional\n  writes. This is because the local file system may or may not provide atomic\n  renames. So you should not use the local file system for testing concurrent\n  writes. - Before version 1.0, Delta Lake supported configuring LogStores by\n  setting `spark.delta.logStore.class`. This approach is now deprecated. Setting\n  this configuration will use the configured `LogStore` for all paths, thereby\n  disabling the dynamic scheme-based delegation.\n</Aside>\n\n\n## Troubleshooting Delta Storage dependency error\n\nIf you see an error like `java.lang.NoClassDefFoundError: io/delta/storage/LogStore` it usually means the **Delta Storage** dependency is missing from the Spark classpath.\n\n##### Error Message with stack trace\n\n```text\ncom.google.common.util.concurrent.ExecutionError: java.lang.NoClassDefFoundError: io/delta/storage/LogStore\nPlease ensure that the delta-storage dependency is included.\n\nIf using Python, please ensure you call `configure_spark_with_delta_pip` or use\n`--packages io.delta:delta-spark_<scala-version>:<delta-lake-version>`.\nSee https://docs.delta.io/latest/quick-start.html#python.\n\nMore information about this dependency and how to include it can be found here:\nhttps://docs.delta.io/latest/porting.html#delta-lake-1-1-or-below-to-delta-lake-1-2-or-above.\n\n  at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2084)\n  at com.google.common.cache.LocalCache.get(LocalCache.java:4017)\n  at com.google.common.cache.LocalCache$LocalManualCache.get(LocalCache.java:4898)\n  at org.apache.spark.sql.delta.DeltaLog$.getDeltaLogFromCache$1(DeltaLog.scala:995)\n  at org.apache.spark.sql.delta.DeltaLog$.initializeDeltaLog$1(DeltaLog.scala:1006)\n  at org.apache.spark.sql.delta.DeltaLog$.apply(DeltaLog.scala:1017)\n  at org.apache.spark.sql.delta.DeltaLog$.forTable(DeltaLog.scala:801)\n  at org.apache.spark.sql.delta.sources.DeltaDataSource.createRelation(DeltaDataSource.scala:197)\n  at org.apache.spark.sql.execution.datasources.SaveIntoDataSourceCommand.run(SaveIntoDataSourceCommand.scala:55)\n  at org.apache.spark.sql.execution.command.ExecutedCommandExec.sideEffectResult$lzycompute(commands.scala:79)\n  at org.apache.spark.sql.execution.command.ExecutedCommandExec.sideEffectResult(commands.scala:77)\n  at org.apache.spark.sql.execution.command.ExecutedCommandExec.executeCollect(commands.scala:88)\n  at org.apache.spark.sql.execution.QueryExecution.$anonfun$eagerlyExecuteCommands$2(QueryExecution.scala:155)\n  at org.apache.spark.sql.execution.SQLExecution$.$anonfun$withNewExecutionId0$8(SQLExecution.scala:162)\n  at org.apache.spark.sql.execution.SQLExecution$.withSessionTagsApplied(SQLExecution.scala:268)\n  at org.apache.spark.sql.execution.SQLExecution$.$anonfun$withNewExecutionId0$7(SQLExecution.scala:124)\n  at org.apache.spark.JobArtifactSet$.withActiveJobArtifactState(JobArtifactSet.scala:94)\n  at org.apache.spark.sql.artifact.ArtifactManager.$anonfun$withResources$1(ArtifactManager.scala:112)\n  at org.apache.spark.sql.artifact.ArtifactManager.withClassLoaderIfNeeded(ArtifactManager.scala:106)\n  at org.apache.spark.sql.artifact.ArtifactManager.withResources(ArtifactManager.scala:111)\n  at org.apache.spark.sql.execution.SQLExecution$.$anonfun$withNewExecutionId0$6(SQLExecution.scala:124)\n  at org.apache.spark.sql.execution.SQLExecution$.withSQLConfPropagated(SQLExecution.scala:291)\n  at org.apache.spark.sql.execution.SQLExecution$.$anonfun$withNewExecutionId0$1(SQLExecution.scala:123)\n  at org.apache.spark.sql.SparkSession.withActive(SparkSession.scala:804)\n  at org.apache.spark.sql.execution.SQLExecution$.withNewExecutionId0(SQLExecution.scala:77)\n  at org.apache.spark.sql.execution.SQLExecution$.withNewExecutionId(SQLExecution.scala:233)\n  at org.apache.spark.sql.execution.QueryExecution.$anonfun$eagerlyExecuteCommands$1(QueryExecution.scala:155)\n  at org.apache.spark.sql.execution.QueryExecution$.withInternalError(QueryExecution.scala:654)\n  at org.apache.spark.sql.execution.QueryExecution.org$apache$spark$sql$execution$QueryExecution$$eagerlyExecute$1(QueryExecution.scala:154)\n  at org.apache.spark.sql.execution.QueryExecution$$anonfun$eagerlyExecuteCommands$3.applyOrElse(QueryExecution.scala:169)\n  at org.apache.spark.sql.execution.QueryExecution$$anonfun$eagerlyExecuteCommands$3.applyOrElse(QueryExecution.scala:164)\n  at org.apache.spark.sql.catalyst.trees.TreeNode.$anonfun$transformDownWithPruning$1(TreeNode.scala:470)\n  at org.apache.spark.sql.catalyst.trees.CurrentOrigin$.withOrigin(origin.scala:86)\n  at org.apache.spark.sql.catalyst.trees.TreeNode.transformDownWithPruning(TreeNode.scala:470)\n  at org.apache.spark.sql.catalyst.plans.logical.LogicalPlan.org$apache$spark$sql$catalyst$plans$logical$AnalysisHelper$$super$transformDownWithPruning(LogicalPlan.scala:37)\n  at org.apache.spark.sql.catalyst.plans.logical.AnalysisHelper.transformDownWithPruning(AnalysisHelper.scala:360)\n  at org.apache.spark.sql.catalyst.plans.logical.AnalysisHelper.transformDownWithPruning$(AnalysisHelper.scala:356)\n  at org.apache.spark.sql.catalyst.plans.logical.LogicalPlan.transformDownWithPruning(LogicalPlan.scala:37)\n  at org.apache.spark.sql.catalyst.plans.logical.LogicalPlan.transformDownWithPruning(LogicalPlan.scala:37)\n  at org.apache.spark.sql.catalyst.trees.TreeNode.transformDown(TreeNode.scala:446)\n  at org.apache.spark.sql.execution.QueryExecution.eagerlyExecuteCommands(QueryExecution.scala:164)\n  at org.apache.spark.sql.execution.QueryExecution.$anonfun$lazyCommandExecuted$1(QueryExecution.scala:126)\n  at scala.util.Try$.apply(Try.scala:217)\n  at org.apache.spark.util.Utils$.doTryWithCallerStacktrace(Utils.scala:1378)\n  at org.apache.spark.util.Utils$.getTryWithCallerStacktrace(Utils.scala:1439)\n  at org.apache.spark.util.LazyTry.get(LazyTry.scala:58)\n  at org.apache.spark.sql.execution.QueryExecution.commandExecuted(QueryExecution.scala:131)\n  at org.apache.spark.sql.execution.QueryExecution.assertCommandExecuted(QueryExecution.scala:192)\n  at org.apache.spark.sql.classic.DataFrameWriter.runCommand(DataFrameWriter.scala:622)\n  at org.apache.spark.sql.classic.DataFrameWriter.saveToV1Source(DataFrameWriter.scala:273)\n  at org.apache.spark.sql.classic.DataFrameWriter.saveInternal(DataFrameWriter.scala:235)\n  at org.apache.spark.sql.classic.DataFrameWriter.save(DataFrameWriter.scala:118)\n  ... 42 elided\n\n```\n\n### Why this happens\n\nWhen you provide the Delta Spark JAR using the `--jars` option (for example, when testing a locally-built JAR), Spark **does not automatically fetch transitive dependencies**. In this case, `delta-spark` may be present, but `delta-storage` is not.\n\nAs a result, operations that need to initialize the Delta log (for example, writing a Delta table) can fail when Delta attempts to load the LogStore API:\n\n```scala\ndf.write.format(\"delta\").save(\"/tmp/delta\")\n```\n\n### How to fix\n\nWhen using the `--jars` option, you can do either of the following:\n\n- Include both `delta-spark` and `delta-storage` JARs:\n\n```bash\nspark-shell \\\n  --jars delta-spark_<scala-version>-<delta-version>.jar,delta-storage-<delta-version>.jar \\\n  --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n  --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n```\n\n<Aside type=\"note\">\n  -  When specifying the JARs with `--jars`, do not include spaces after the comma. \n</Aside>\n\n- Use the assembly JAR (build it using `build/sbt \"spark/assembly\"`):\n\n```bash\nspark-shell \\\n  --jars delta-spark-assembly-<delta-version>.jar \\\n  --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n  --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n```\n\n## Amazon S3\n\nDelta Lake supports reads and writes to S3 in two different modes: Single-cluster and Multi-cluster.\n\n|  | Single-cluster | Multi-cluster |\n| --- | --- | --- |\n| Configuration | Comes with Delta Lake out-of-the-box | Is experimental and requires extra configuration |\n| Reads | Supports concurrent reads from multiple clusters | Supports concurrent reads from multiple clusters |\n| Writes | Supports concurrent writes from a _single_ Spark driver | Supports multi-cluster writes |\n| Permissions | S3 credentials | S3 and DynamoDB operating permissions |\n\n### Single-cluster setup (default)\n\nIn this default mode, Delta Lake supports concurrent reads from multiple clusters, but concurrent writes to S3 must originate from a _single_ Spark driver in order for Delta Lake to provide transactional guarantees. This is because S3 currently does not provide mutual exclusion, that is, there is no way to ensure that only one writer is able to create a file.\n\n<Aside type=\"caution\">\n  Concurrent writes to the same Delta table on S3 storage from multiple Spark\n  drivers can lead to data loss. For a multi-cluster solution, please see the\n  [Multi-cluster setup](#multi-cluster-setup) section below.\n</Aside>\n\n#### Requirements (S3 single-cluster)\n\n- S3 credentials: [IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) (recommended) or access keys\n- Apache Spark associated with the corresponding Delta Lake version.\n- Hadoop's [AWS connector (hadoop-aws)](https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-aws/) for the version of Hadoop that Apache Spark is compiled for.\n\n#### Quickstart (S3 single-cluster)\n\nThis section explains how to quickly start reading and writing Delta tables on S3 using single-cluster mode. For a detailed explanation of the configuration, see [Setup Configuration (S3 multi-cluster)](#setup-configuration-s3-multi-cluster).\n\n1. Use the following command to launch a Spark shell with Delta Lake and S3 support (assuming you use Spark 4.0.0 which is pre-built for Hadoop 3.4.0):\n\n<Tabs>\n  <TabItem label=\"Bash\">\n\n    ```bash\n    bin/spark-shell \\\n     --packages io.delta:delta-spark_2.13:4.0.0,org.apache.hadoop:hadoop-aws:3.4.0 \\\n     --conf spark.hadoop.fs.s3a.access.key=<your-s3-access-key> \\\n     --conf spark.hadoop.fs.s3a.secret.key=<your-s3-secret-key>\n    ```\n\n  </TabItem>\n</Tabs>\n\n2. Try out some basic Delta table operations on S3 (in Scala):\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    // Create a Delta table on S3:\n    spark.range(5).write.format(\"delta\").save(\"s3a://<your-s3-bucket>/<path-to-delta-table>\")\n\n    // Read a Delta table on S3:\n    spark.read.format(\"delta\").load(\"s3a://<your-s3-bucket>/<path-to-delta-table>\").show()\n    ```\n\n  </TabItem>\n</Tabs>\n\nFor other languages and more examples of Delta table operations, see the [Quickstart](/quick-start/) page.\n\nFor efficient listing of Delta Lake metadata files on S3, set the configuration `delta.enableFastS3AListFrom=true`. This performance optimization is in experimental support mode. It will only work on `S3A` filesystems and will not work on [Amazon's EMR](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-file-systems.html) default filesystem `S3`.\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    bin/spark-shell \\\n      --packages io.delta:delta-spark_2.13:4.0.0,org.apache.hadoop:hadoop-aws:3.4.0 \\\n      --conf spark.hadoop.fs.s3a.access.key=<your-s3-access-key> \\\n      --conf spark.hadoop.fs.s3a.secret.key=<your-s3-secret-key> \\\n      --conf \"spark.hadoop.delta.enableFastS3AListFrom=true\n    ```\n  </TabItem>\n</Tabs>\n\n### Multi-cluster setup\n\n<Aside type=\"note\">This support is new and experimental.</Aside>\n\nThis mode supports concurrent writes to S3 from multiple clusters and has to be explicitly enabled by configuring Delta Lake to use the right `LogStore` implementation. This implementation uses [DynamoDB](https://aws.amazon.com/dynamodb/) to provide the mutual exclusion that S3 is lacking.\n\n<Aside type=\"caution\">\n  This multi-cluster writing solution is only safe when all writers use this\n  `LogStore` implementation as well as the same DynamoDB table and region. If\n  some drivers use out-of-the-box Delta Lake while others use this experimental\n  `LogStore`, then data loss can occur.\n</Aside>\n\n#### Requirements (S3 multi-cluster)\n\n- All of the requirements listed in [Requirements (S3 single-cluster)](#requirements-s3-single-cluster) section\n- In additon to S3 credentials, you also need DynamoDB operating permissions\n\n#### Quickstart (S3 multi-cluster)\n\nThis section explains how to quickly start reading and writing Delta tables on S3 using multi-cluster mode.\n\n1. Use the following command to launch a Spark shell with Delta Lake and S3 support (assuming you use Spark 4.0.0 which is pre-built for Hadoop 3.4.0):\n\n<Tabs>\n  <TabItem label=\"Bash\">\n    ```bash\n    bin/spark-shell \\\n     --packages io.delta:delta-spark_2.13:3,org.apache.hadoop:hadoop-aws:3.4.0,io.delta:delta-storage-s3-dynamodb:4.0.0 \\\n     --conf spark.hadoop.fs.s3a.access.key=<your-s3-access-key> \\\n     --conf spark.hadoop.fs.s3a.secret.key=<your-s3-secret-key> \\\n     --conf spark.delta.logStore.s3a.impl=io.delta.storage.S3DynamoDBLogStore \\\n     --conf spark.io.delta.storage.S3DynamoDBLogStore.ddb.region=us-west-2\n    ```\n  </TabItem>\n</Tabs>\n\n2. Try out some basic Delta table operations on S3 (in Scala):\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    // Create a Delta table on S3:\n    spark.range(5).write.format(\"delta\").save(\"s3a://<your-s3-bucket>/<path-to-delta-table>\")\n\n    // Read a Delta table on S3:\n    spark.read.format(\"delta\").load(\"s3a://<your-s3-bucket>/<path-to-delta-table>\").show()\n    ```\n\n  </TabItem>\n</Tabs>\n\nFor other languages and more examples of Delta table operations, see the [Quickstart](/quick-start/) page.\n\n#### Setup Configuration (S3 multi-cluster)\n\n1. Create the DynamoDB table.\n\n   You have the choice of creating the DynamoDB table yourself (recommended) or having it created for you automatically.\n\n   - Creating the DynamoDB table yourself\n\n     This DynamoDB table will maintain commit metadata for multiple Delta tables, and it is important that it is configured with the [Read/Write Capacity Mode](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html) (for example, on-demand or provisioned) that is right for your use cases. As such, we strongly recommend that you create your DynamoDB table yourself. The following example uses the AWS CLI. To learn more, see the [create-table](https://docs.aws.amazon.com/cli/latest/reference/dynamodb/create-table.html) command reference.\n\n<Tabs>\n  <TabItem label=\"Bash\">\n    ```bash \n    aws dynamodb create-table \\\n      --region us-east-1 \\\n      --table-name delta_log \\\n      --attribute-definitions AttributeName=tablePath,AttributeType=S \\\n      AttributeName=fileName,AttributeType=S \\\n      --key-schema AttributeName=tablePath,KeyType=HASH \\\n      AttributeName=fileName,KeyType=RANGE \\\n      --billing-mode PAY_PER_REQUEST \n    ```\n  </TabItem>\n</Tabs>\n\n2. Follow the configuration steps listed in [Configuration (S3 single-cluster)](#configuration-s3-single-cluster) section.\n\n3. Include the `delta-storage-s3-dynamodb` JAR in the classpath.\n\n4. Configure the `LogStore` implementation in your Spark session.\n\n   First, configure this `LogStore` implementation for the scheme `s3`. You can replicate this command for schemes `s3a` and `s3n` as well.\n\n<Tabs>\n  <TabItem label=\"ini\">\n    ```ini \n    spark.delta.logStore.s3.impl=io.delta.storage.S3DynamoDBLogStore \n    ```\n  </TabItem>\n</Tabs>\n\nNext, specify additional information necessary to instantiate the DynamoDB client. You must instantiate the DynamoDB client with the same `tableName` and `region` each Spark session for this multi-cluster mode to work correctly. A list of per-session configurations and their defaults is given below:\n\n<Tabs>\n  <TabItem label=\"ini\">\n    ```ini\n    spark.io.delta.storage.S3DynamoDBLogStore.ddb.tableName=delta_log\n    spark.io.delta.storage.S3DynamoDBLogStore.ddb.region=us-east-1\n    spark.io.delta.storage.S3DynamoDBLogStore.credentials.provider=<AWSCredentialsProvider* used by the client>\n    spark.io.delta.storage.S3DynamoDBLogStore.provisionedThroughput.rcu=5\n    spark.io.delta.storage.S3DynamoDBLogStore.provisionedThroughput.wcu=5\n    ```\n  </TabItem>\n</Tabs>\n\n#### Production Configuration (S3 multi-cluster)\n\nBy this point, this multi-cluster setup is fully operational. However, there is extra configuration you may do to improve performance and optimize storage when running in production.\n\n1. Adjust your Read and Write Capacity Mode.\n\n   If you are using the default DynamoDB table created for you by this `LogStore` implementation, its default RCU and WCU might not be enough for your workloads. You can [adjust the provisioned throughput](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ProvisionedThroughput.html#ProvisionedThroughput.CapacityUnits.Modifying) or [update to On-Demand Mode](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.Basics.html#WorkingWithTables.Basics.UpdateTable).\n\n2. Cleanup old DynamoDB entries using Time to Live (TTL).\n\n   Once a DynamoDB metadata entry is marked as complete, and after sufficient time such that we can now rely on S3 alone to prevent accidental overwrites on its corresponding Delta file, it is safe to delete that entry from DynamoDB. The cheapest way to do this is using [DynamoDB's TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) feature which is a free, automated means to delete items from your DynamoDB table.\n\n   Run the following command on your given DynamoDB table to enable TTL:\n\n<Tabs>\n  <TabItem label=\"Bash\">\n    ```bash \n    aws dynamodb update-time-to-live \\\n      --region us-east-1 \\\n      --table-name delta_log \\\n      --time-to-live-specification \"Enabled=true, AttributeName=expireTime\" \n    ```\n  </TabItem>\n</Tabs>\n\nThe default `expireTime` will be one day after the DynamoDB entry was marked as completed.\n\n3. Cleanup old AWS S3 temp files using S3 Lifecycle Expiration.\n\n   In this `LogStore` implementation, a temp file is created containing a copy of the metadata to be committed into the Delta log. Once that commit to the Delta log is complete, and after the corresponding DynamoDB entry has been removed, it is safe to delete this temp file. In practice, only the latest temp file will ever be used during recovery of a failed commit.\n\n   Here are two simple options for deleting these temp files:\n\n   1. Delete manually using S3 CLI.\n\n      This is the safest option. The following command will delete all but the latest temp file in your given `<bucket>` and `<table>`:\n\n<Tabs>\n  <TabItem label=\"Bash\">\n    ```bash\n    aws s3 ls s3://<bucket>/<delta_table_path>/_delta_log/.tmp/ --recursive | awk 'NF>1{print $4}' | grep . | sort | head -n -1  | while read -r line ; do\n        echo \"Removing ${line}\"\n        aws s3 rm s3://<bucket>/<delta_table_path>/_delta_log/.tmp/${line}\n    done\n    ```\n  </TabItem>\n</Tabs>\n\n2.  Delete using an S3 Lifecycle Expiration Rule\n\n    A more automated option is to use an [S3 Lifecycle Expiration rule](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html), with filter prefix pointing to the `<delta_table_path>/_delta_log/.tmp/` folder located in your table path, and an expiration value of 30 days.\n\n  <Aside type=\"note\">\n  It is important that you choose a sufficiently large expiration value. As stated above, the latest temp file will be used during recovery of a failed commit. If this temp file is deleted, then your DynamoDB table and S3 `<delta_table_path>/_delta_log/.tmp/` folder will be out of sync.\n  </Aside>\n\n    There are a variety of ways to configuring a bucket lifecycle configuration, described in AWS docs [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-set-lifecycle-configuration-intro.html).\n\n    One way to do this is using S3's `put-bucket-lifecycle-configuration` command. See [S3 Lifecycle Configuration](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-lifecycle-configuration.html) for details. An example rule and command invocation is given below:\n\n    In a file referenced as `file://lifecycle.json`:\n\n<Tabs>\n  <TabItem label=\"JSON\">\n    ```json\n    {\n      \"Rules\":[\n        {\n          \"ID\":\"expire_tmp_files\",\n          \"Filter\":{\n            \"Prefix\":\"path/to/table/_delta_log/.tmp/\"\n          },\n          \"Status\":\"Enabled\",\n          \"Expiration\":{\n            \"Days\":30\n          }\n        }\n      ]\n    }\n    ```\n  </TabItem>\n</Tabs>\n\n<Tabs>\n  <TabItem label=\"Bash\">\n    ```bash\n    aws s3api put-bucket-lifecycle-configuration \\\n      --bucket my-bucket \\\n      --lifecycle-configuration file://lifecycle.json\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  AWS S3 may have a limit on the number of rules per bucket. See\n  [PutBucketLifecycleConfiguration](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html)\n  for details.\n</Aside>\n\n## Microsoft Azure storage\n\nDelta Lake has built-in support for the various Azure storage systems with full transactional guarantees for concurrent reads and writes from multiple clusters.\n\nDelta Lake relies on Hadoop `FileSystem` APIs to access Azure storage services. Specifically, Delta Lake requires the implementation of `FileSystem.rename()` to be atomic, which is only supported in newer Hadoop versions ([Hadoop-15156](https://issues.apache.org/jira/browse/HADOOP-15156) and [Hadoop-15086](https://issues.apache.org/jira/browse/HADOOP-15086)). For this reason, you may need to build Spark with newer Hadoop versions and use them for deploying your application. See [Specifying the Hadoop Version and Enabling YARN](https://spark.apache.org/docs/latest/building-spark.html#specifying-the-hadoop-version-and-enabling-yarn) for building Spark with a specific Hadoop version and [Quickstart](/quick-start/) for setting up Spark with Delta Lake.\n\nHere is a list of requirements specific to each type of Azure storage system:\n\n### Azure Blob storage\n\n#### Requirements (Azure Blob storage)\n\n- A [shared key](https://docs.microsoft.com/rest/api/latest/storageservices/authorize-with-shared-key) or [shared access signature (SAS)](https://docs.microsoft.com/azure/storage/common/storage-dotnet-shared-access-signature-part-1)\n- Delta Lake 0.2.0 or above\n- Hadoop's Azure Blob Storage libraries for deployment with the following versions:\n  - 2.9.1+ for Hadoop 2\n  - 3.0.1+ for Hadoop 3\n- Apache Spark associated with the corresponding Delta Lake version and [compiled with Hadoop version](https://spark.apache.org/docs/latest/building-spark.html#specifying-the-hadoop-version-and-enabling-yarn) that is compatible with the chosen Hadoop libraries.\n\nFor example, a possible combination that will work is Delta 0.7.0 or above, along with Apache Spark 3.0 compiled and deployed with Hadoop 3.2.\n\n#### Configuration (Azure Blob storage)\n\nHere are the steps to configure Delta Lake on Azure Blob storage.\n\n1. Include `hadoop-azure` JAR in the classpath. See the requirements above for version details.\n\n2. Set up credentials.\n\n   You can set up your credentials in the [Spark configuration property](https://spark.apache.org/docs/latest/configuration.html).\n\n   We recommend that you use a SAS token. In Scala, you can use the following:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\n      \"fs.azure.sas.<your-container-name>.<your-storage-account-name>.blob.core.windows.net\",\n       \"<complete-query-string-of-your-sas-for-the-container>\")\n    ```\n  </TabItem>\n</Tabs>\n\nOr you can specify an account access key:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\n      \"fs.azure.account.key.<your-storage-account-name>.blob.core.windows.net\",\n       \"<your-storage-account-access-key>\")\n    ```\n  </TabItem>\n</Tabs>\n\n#### Usage (Azure Blob storage)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"wasbs://<your-container-name>@<your-storage-account-name>.blob.core.windows.net/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"wasbs://<your-container-name>@<your-storage-account-name>.blob.core.windows.net/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n\n### Azure Data Lake Storage Gen1\n\n#### Requirements (ADLS Gen1)\n\n- A [service principal](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) for OAuth 2.0 access\n- Delta Lake 0.2.0 or above\n- Hadoop's Azure Data Lake Storage Gen1 libraries for deployment with the following versions:\n  - 2.9.1+ for Hadoop 2\n  - 3.0.1+ for Hadoop 3\n- Apache Spark associated with the corresponding Delta Lake version and [compiled with Hadoop version](https://spark.apache.org/docs/latest/building-spark.html#specifying-the-hadoop-version-and-enabling-yarn) that is compatible with the chosen Hadoop libraries.\n\n#### Configuration (ADLS Gen1)\n\nHere are the steps to configure Delta Lake on Azure Data Lake Storage Gen1.\n\n1. Include `hadoop-azure-datalake` JAR in the classpath. See the requirements above for version details.\n\n2. Set up Azure Data Lake Storage Gen1 credentials.\n\n   You can set the following [Hadoop configurations](https://spark.apache.org/docs/latest/configuration.html#custom-hadoophive-configuration) with your credentials (in Scala):\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"dfs.adls.oauth2.access.token.provider.type\", \"ClientCredential\")\n    spark.conf.set(\"dfs.adls.oauth2.client.id\", \"<your-oauth2-client-id>\")\n    spark.conf.set(\"dfs.adls.oauth2.credential\", \"<your-oauth2-credential>\")\n    spark.conf.set(\"dfs.adls.oauth2.refresh.url\", \"https://login.microsoftonline.com/<your-directory-id>/oauth2/token\")\n    ```\n  </TabItem>\n</Tabs>\n\n#### Usage (ADLS Gen1)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"adl://<your-adls-account>.azuredatalakestore.net/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"adl://<your-adls-account>.azuredatalakestore.net/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n\n### Azure Data Lake Storage Gen2\n\n#### Requirements (ADLS Gen2)\n\n- A [service principal](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) for OAuth 2.0 access or a [shared key](https://docs.microsoft.com/rest/api/latest/storageservices/authorize-with-shared-key)\n- Delta Lake 0.2.0 or above\n- Hadoop's Azure Data Lake Storage Gen2 libraries for deployment with the following versions:\n  - 3.2.0+ for Hadoop 3\n- Apache Spark associated with the corresponding Delta Lake version and [compiled with Hadoop version](https://spark.apache.org/docs/latest/building-spark.html#specifying-the-hadoop-version-and-enabling-yarn) that is compatible with the chosen Hadoop libraries.\n\n#### Configuration (ADLS Gen2)\n\nHere are the steps to configure Delta Lake on Azure Data Lake Storage Gen2.\n\n1. Include `hadoop-azure` and `azure-storage` JARs in the classpath. See the requirements above for version details.\n\n2. Set up credentials.\n\n   You can use either OAuth 2.0 with service principal or shared key authentication:\n\n   For OAuth 2.0 with service principal (recommended):\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"fs.azure.account.auth.type.<storage-account>.dfs.core.windows.net\", \"OAuth\")\n    spark.conf.set(\"fs.azure.account.oauth.provider.type.<storage-account>.dfs.core.windows.net\", \"org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider\")\n    spark.conf.set(\"fs.azure.account.oauth2.client.id.<storage-account>.dfs.core.windows.net\", \"<application-id>\")\n    spark.conf.set(\"fs.azure.account.oauth2.client.secret.<storage-account>.dfs.core.windows.net\", \"<service-credential>\")\n    spark.conf.set(\"fs.azure.account.oauth2.client.endpoint.<storage-account>.dfs.core.windows.net\", \"https://login.microsoftonline.com/<directory-id>/oauth2/token\")\n    ```\n  </TabItem>\n</Tabs>\n\nFor shared key authentication:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"fs.azure.account.key.<storage-account>.dfs.core.windows.net\", \"<storage-account-access-key>\")\n    ```\n  </TabItem>\n</Tabs>\n\n#### Usage (ADLS Gen2)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"abfss://<container-name>@<storage-account>.dfs.core.windows.net/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"abfss://<container-name>@<storage-account>.dfs.core.windows.net/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n\n## HDFS\n\nDelta Lake has built-in support for HDFS with full transactional guarantees for concurrent reads and writes from multiple clusters. No additional configuration is required.\n\n#### Usage (HDFS)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"hdfs://<namenode>:<port>/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"hdfs://<namenode>:<port>/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n\n## Google Cloud Storage\n\nDelta Lake has built-in support for Google Cloud Storage (GCS) with full transactional guarantees for concurrent reads and writes from multiple clusters.\n\n### Requirements (GCS)\n\n- Google Cloud Storage credentials\n- Delta Lake 0.2.0 or above\n- Hadoop's [GCS connector](https://cloud.google.com/dataproc/docs/concepts/connectors/cloud-storage) for the version of Hadoop that Apache Spark is compiled for\n\n### Configuration (GCS)\n\n1. Include the GCS connector JAR in the classpath.\n\n2. Set up credentials using one of the following methods:\n   - Use [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)\n   - Configure service account credentials in Spark configuration\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"google.cloud.auth.service.account.json.keyfile\", \"<path-to-json-key-file>\")\n    ```\n  </TabItem>\n</Tabs>\n\n### Usage (GCS)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"gs://<bucket>/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"gs://<bucket>/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n\n## Oracle Cloud Infrastructure\n\nDelta Lake supports Oracle Cloud Infrastructure (OCI) Object Storage with full transactional guarantees for concurrent reads and writes from multiple clusters.\n\n### Requirements (OCI)\n\n- OCI credentials\n- Delta Lake 0.2.0 or above\n- Hadoop's [OCI connector](https://docs.oracle.com/en-us/iaas/Content/api/latest/SDKDocs/hdfsconnector.htm) for the version of Hadoop that Apache Spark is compiled for\n\n### Configuration (OCI)\n\n1. Include the OCI connector JAR in the classpath.\n\n2. Set up credentials in Spark configuration:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"fs.oci.client.auth.tenantId\", \"<tenant-ocid>\")\n    spark.conf.set(\"fs.oci.client.auth.userId\", \"<user-ocid>\")\n    spark.conf.set(\"fs.oci.client.auth.fingerprint\", \"<api-key-fingerprint>\")\n    spark.conf.set(\"fs.oci.client.auth.pemfilepath\", \"<path-to-private-key-file>\")\n    ```\n  </TabItem>\n</Tabs>\n\n### Usage (OCI)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"oci://<bucket>@<namespace>/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"oci://<bucket>@<namespace>/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n\n## IBM Cloud Object Storage\n\nDelta Lake supports IBM Cloud Object Storage with full transactional guarantees for concurrent reads and writes from multiple clusters.\n\n### Requirements (IBM COS)\n\n- IBM Cloud Object Storage credentials\n- Delta Lake 0.2.0 or above\n- Hadoop's [Stocator connector](https://github.com/CODAIT/stocator) for the version of Hadoop that Apache Spark is compiled for\n\n### Configuration (IBM COS)\n\n1. Include the Stocator connector JAR in the classpath.\n\n2. Set up credentials in Spark configuration:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.conf.set(\"fs.cos.service.endpoint\", \"<endpoint-url>\")\n    spark.conf.set(\"fs.cos.service.access.key\", \"<access-key>\")\n    spark.conf.set(\"fs.cos.service.secret.key\", \"<secret-key>\")\n    ```\n  </TabItem>\n</Tabs>\n\n### Usage (IBM COS)\n\n<Tabs>\n  <TabItem label=\"Scala\">\n    ```scala\n    spark.range(5).write.format(\"delta\").save(\"cos://<bucket>.<service>/<path-to-delta-table>\")\n    spark.read.format(\"delta\").load(\"cos://<bucket>.<service>/<path-to-delta-table>\").show()\n    ```\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content/docs/delta-streaming/index.mdx",
    "content": "---\ntitle: Table streaming reads and writes\ndescription: Learn how to use Delta tables as streaming sources and sinks.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nDelta Lake is deeply integrated with [Spark Structured Streaming](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html) through `readStream` and `writeStream`. Delta Lake overcomes many of the limitations typically associated with streaming systems and files, including:\n\n- Maintaining \"exactly-once\" processing with more than one stream (or concurrent batch jobs)\n\n- Efficiently discovering which files are new when using files as the source for a stream\n\nFor many Delta Lake operations on tables, you enable integration with Apache Spark DataSourceV2 and Catalog APIs (since 3.0) by setting configurations when you create a new `SparkSession`. See [Configure SparkSession](/delta-batch/#configure-sparksession).\n\n## Delta table as a source\n\nWhen you load a Delta table as a stream source and use it in a streaming query, the query processes all of the data present in the table as well as any new data that arrives after the stream is started.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Scala\">\n    \n    ```scala\n    spark.readStream.format(\"delta\")\n      .load(\"/tmp/delta/events\")\n\n    import io.delta.implicits._\n    spark.readStream.delta(\"/tmp/delta/events\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n### Limit input rate\n\nThe following options are available to control micro-batches:\n\n- `maxFilesPerTrigger`: How many new files to be considered in every micro-batch. The default is 1000.\n- `maxBytesPerTrigger`: How much data gets processed in each micro-batch. This option sets a \"soft max\", meaning that a batch processes approximately this amount of data and may process more than the limit in order to make the streaming query move forward in cases when the smallest input unit is larger than this limit. If you use `Trigger.Once` for your streaming, this option is ignored. This is not set by default.\n\nIf you use `maxBytesPerTrigger` in conjunction with `maxFilesPerTrigger`, the micro-batch processes data until either the `maxFilesPerTrigger` or `maxBytesPerTrigger` limit is reached.\n\n<Aside type=\"note\">\n  In cases when the source table transactions are cleaned up due to the\n  `logRetentionDuration` [configuration](/delta-batch/#data-retention) and the\n  stream lags in processing, Delta Lake processes the data corresponding to the\n  latest available transaction history of the source table but does not fail the\n  stream. This can result in data being dropped.\n</Aside>\n\n### Ignore updates and deletes\n\nStructured Streaming does not handle input that is not an append and throws an exception if any modifications occur on the table being used as a source. There are two main strategies for dealing with changes that cannot be automatically propagated downstream:\n\n- You can delete the output and checkpoint and restart the stream from the beginning.\n- You can set either of these two options:\n  - `ignoreDeletes`: ignore transactions that delete data at partition boundaries.\n  - `ignoreChanges`: re-process updates if files had to be rewritten in the source table due to a data changing operation such as `UPDATE`, `MERGE INTO`, `DELETE` (within partitions), or `OVERWRITE`. Unchanged rows may still be emitted, therefore your downstream consumers should be able to handle duplicates. Deletes are not propagated downstream. `ignoreChanges` subsumes `ignoreDeletes`. Therefore if you use `ignoreChanges`, your stream will not be disrupted by either deletions or updates to the source table.\n\n#### Example\n\nFor example, suppose you have a table `user_events` with `date`, `user_email`, and `action` columns that is partitioned by `date`. You stream out of the `user_events` table and you need to delete data from it due to GDPR.\n\nWhen you delete at partition boundaries (that is, the `WHERE` is on a partition column), the files are already segmented by value so the delete just drops those files from the metadata. Thus, if you just want to delete data from some partitions, you can use:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    spark.readStream.format(\"delta\")\n      .option(\"ignoreDeletes\", \"true\")\n      .load(\"/tmp/delta/user_events\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\nHowever, if you have to delete data based on `user_email`, then you will need to use:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    spark.readStream.format(\"delta\")\n      .option(\"ignoreChanges\", \"true\")\n      .load(\"/tmp/delta/user_events\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\nIf you update a `user_email` with the `UPDATE` statement, the file containing the `user_email` in question is rewritten. When you use `ignoreChanges`, the new record is propagated downstream with all other unchanged records that were in the same file. Your logic should be able to handle these incoming duplicate records.\n\n### Specify initial position\n\nYou can use the following options to specify the starting point of the Delta Lake streaming source without processing the entire table.\n\n- `startingVersion`: The Delta Lake version to start from. All table changes starting from this version (inclusive) will be read by the streaming source. You can obtain the commit versions from the `version` column of the [DESCRIBE HISTORY](/delta-utility/#retrieve-delta-table-history) command output.\n- To return only the latest changes, specify `latest`.\n- `startingTimestamp`: The timestamp to start from. All table changes committed at or after the timestamp (inclusive) will be read by the streaming source. One of:\n  - A timestamp string. For example, `\"2019-01-01T00:00:00.000Z\"`.\n  - A date string. For example, `\"2019-01-01\"`.\n\nYou cannot set both options at the same time; you can use only one of them. They take effect only when starting a new streaming query. If a streaming query has started and the progress has been recorded in its checkpoint, these options are ignored.\n\n<Aside type=\"caution\">\n  Although you can start the streaming source from a specified version or\n  timestamp, the schema of the streaming source is always the latest schema of\n  the Delta table. You must ensure there is no incompatible schema change to the\n  Delta table after the specified version or timestamp. Otherwise, the streaming\n  source may return incorrect results when reading the data with an incorrect\n  schema.\n</Aside>\n\n#### Example\n\nFor example, suppose you have a table `user_events`. If you want to read changes since version 5, use:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    spark.readStream.format(\"delta\")\n      .option(\"startingVersion\", \"5\")\n      .load(\"/tmp/delta/user_events\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\nIf you want to read changes since 2018-10-18, use:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    spark.readStream.format(\"delta\")\n      .option(\"startingTimestamp\", \"2018-10-18\")\n      .load(\"/tmp/delta/user_events\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Process initial snapshot without data being dropped\n\nWhen using a Delta table as a stream source, the query first processes all of the data present in the table. The Delta table at this version is called the initial snapshot. By default, the Delta table's data files are processed based on which file was last modified. However, the last modification time does not necessarily represent the record event time order.\n\nIn a stateful streaming query with a defined watermark, processing files by modification time can result in records being processed in the wrong order. This could lead to records dropping as late events by the watermark.\n\nYou can avoid the data drop issue by enabling the following option:\n\n- withEventTimeOrder: Whether the initial snapshot should be processed with event time order.\n\nWith event time order enabled, the event time range of initial snapshot data is divided into time buckets. Each micro batch processes a bucket by filtering data within the time range. The maxFilesPerTrigger and maxBytesPerTrigger configuration options are still applicable to control the microbatch size but only in an approximate way due to the nature of the processing.\n\nThe graphic below shows this process:\n\n![Initial Snapshot](./delta-initial-snapshot-data-drop.png)\n\nNotable information about this feature:\n\n- The data drop issue only happens when the initial Delta snapshot of a stateful streaming query is processed in the default order.\n- You cannot change `withEventTimeOrder` once the stream query is started while the initial snapshot is still being processed. To restart with `withEventTimeOrder` changed, you need to delete the checkpoint.\n- If you are running a stream query with withEventTimeOrder enabled, you cannot downgrade it to a Delta version which doesn't support this feature until the initial snapshot processing is completed. If you need to downgrade, you can wait for the initial snapshot to finish, or delete the checkpoint and restart the query.\n- This feature is not supported in the following uncommon scenarios:\n  - The event time column is a generated column and there are non-projection transformations between the Delta source and watermark.\n  - There is a watermark that has more than one Delta source in the stream query.\n- With event time order enabled, the performance of the Delta initial snapshot processing might be slower.\n- Each micro batch scans the initial snapshot to filter data within the corresponding event time range. For faster filter action, it is advised to use a Delta source column as the event time so that data skipping can be applied (check \\_ for when it's applicable). Additionally, table partitioning along the event time column can further speed the processing. You can check Spark UI to see how many delta files are scanned for a specific micro batch.\n\n#### Example\n\nSuppose you have a table `user_events` with an `event_time` column. Your streaming query is an aggregation query. If you want to ensure no data drop during the initial snapshot processing, you can use:\n\n<Tabs>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    spark.readStream.format(\"delta\")\n      .option(\"withEventTimeOrder\", \"true\")\n      .load(\"/tmp/delta/user_events\")\n      .withWatermark(\"event_time\", \"10 seconds\")\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  You can also enable this with Spark config on the cluster which will apply to all streaming queries:\n\n  ```\n  spark.databricks.delta.withEventTimeOrder.enabled true\n  ```\n\n</Aside>\n\n### Tracking non-additive schema changes\n\nYou can provide a schema tracking location to enable streaming from Delta tables with column mapping enabled. This overcomes an issue in which non-additive schema changes could result in broken streams by allowing streams to read past table data in their exact schema as if the table is time-travelled.\n\nEach streaming read against a data source must have its own `schemaTrackingLocation` specified. The specified `schemaTrackingLocation` must be contained within the directory specified for the `checkpointLocation` of the target table for streaming write.\n\n<Aside type=\"note\">\n  For streaming workloads that combine data from multiple source Delta tables,\n  you need to specify unique directories within the `checkpointLocation` for\n  each source table.\n</Aside>\n\n#### Example\n\nThe option `schemaTrackingLocation` is used to specify the path for schema tracking, as shown in the following code example:\n\n<Tabs>\n\t<TabItem label=\"Python\">\n\n    ```python\n    checkpoint_path = \"/path/to/checkpointLocation\"\n\n    (spark.readStream\n      .option(\"schemaTrackingLocation\", checkpoint_path)\n      .table(\"delta_source_table\")\n      .writeStream\n      .option(\"checkpointLocation\", checkpoint_path)\n      .toTable(\"output_table\")\n    )\n    ```\n  \n  </TabItem>\n</Tabs>\n\n## Delta table as a sink\n\nYou can also write data into a Delta table using Structured Streaming. The transaction log enables Delta Lake to guarantee exactly-once processing, even when there are other streams or batch queries running concurrently against the table.\n\n<Aside type=\"note\">\n  The Delta Lake `VACUUM` function removes all files not managed by Delta Lake but skips any directories that begin with `_`. You can safely store checkpoints alongside other data and metadata for a Delta table using a directory structure such as `<table_name>/_checkpoints`.\n</Aside>\n\n### Append mode\n\nBy default, streams run in append mode, which adds new records to the table.\n\nYou can use the path method:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    events.writeStream\n      .format(\"delta\")\n      .outputMode(\"append\")\n      .option(\"checkpointLocation\", \"/tmp/delta/_checkpoints/\")\n      .start(\"/delta/events\")\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    events.writeStream\n      .format(\"delta\")\n      .outputMode(\"append\")\n      .option(\"checkpointLocation\", \"/tmp/delta/events/_checkpoints/\")\n      .start(\"/tmp/delta/events\")\n\n    import io.delta.implicits._\n    events.writeStream\n      .outputMode(\"append\")\n      .option(\"checkpointLocation\", \"/tmp/delta/events/_checkpoints/\")\n      .delta(\"/tmp/delta/events\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\nor the `toTable` method (in Spark 3.1 and higher) as follows:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    events.writeStream\n      .format(\"delta\")\n      .outputMode(\"append\")\n      .option(\"checkpointLocation\", \"/tmp/delta/events/_checkpoints/\")\n      .toTable(\"events\")\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    events.writeStream\n      .outputMode(\"append\")\n      .option(\"checkpointLocation\", \"/tmp/delta/events/_checkpoints/\")\n      .toTable(\"events\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Complete mode\n\nYou can also use Structured Streaming to replace the entire table with every batch. One example use case is to compute a summary using aggregation:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    (spark.readStream\n      .format(\"delta\")\n      .load(\"/tmp/delta/events\")\n      .groupBy(\"customerId\")\n      .count()\n      .writeStream\n      .format(\"delta\")\n      .outputMode(\"complete\")\n      .option(\"checkpointLocation\", \"/tmp/delta/eventsByCustomer/_checkpoints/\")\n      .start(\"/tmp/delta/eventsByCustomer\")\n    )\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    spark.readStream\n      .format(\"delta\")\n      .load(\"/tmp/delta/events\")\n      .groupBy(\"customerId\")\n      .count()\n      .writeStream\n      .format(\"delta\")\n      .outputMode(\"complete\")\n      .option(\"checkpointLocation\", \"/tmp/delta/eventsByCustomer/_checkpoints/\")\n      .start(\"/tmp/delta/eventsByCustomer\")\n    ```\n  \n  </TabItem>\n</Tabs>\n\nThe preceding example continuously updates a table that contains the aggregate number of events by customer.\n\nFor applications with more lenient latency requirements, you can save computing resources with one-time triggers. Use these to update summary aggregation tables on a given schedule, processing only new data that has arrived since the last update.\n\n## Idempotent table writes in `foreachBatch`\n\n<Aside type=\"note\">Available in Delta Lake 2.0.0 and above.</Aside>\n\nThe command foreachBatch allows you to specify a function that is executed on the output of every micro-batch after arbitrary transformations in the streaming query. This allows implementating a `foreachBatch` function that can write the micro-batch output to one or more target Delta table destinations. However, `foreachBatch` does not make those writes idempotent as those write attempts lack the information of whether the batch is being re-executed or not. For example, rerunning a failed batch could result in duplicate data writes.\n\nTo address this, Delta tables support the following `DataFrameWriter` options to make the writes idempotent:\n\n- `txnAppId`: A unique string that you can pass on each `DataFrame` write. For example, you can use the StreamingQuery ID as `txnAppId`.\n- `txnVersion`: A monotonically increasing number that acts as transaction version.\n\nDelta table uses the combination of `txnAppId` and `txnVersion` to identify duplicate writes and ignore them.\n\nIf a batch write is interrupted with a failure, rerunning the batch uses the same application and batch ID, which would help the runtime correctly identify duplicate writes and ignore them. Application ID (`txnAppId`) can be any user-generated unique string and does not have to be related to the stream ID.\n\n<Aside type=\"caution\">\n  If you delete the streaming checkpoint and restart the query with a new\n  checkpoint, you must provide a different `appId`; otherwise, writes from the\n  restarted query will be ignored because it will contain the same `txnAppId`\n  and the batch ID would start from 0.\n</Aside>\n\nThe same `DataFrameWriter` options can be used to achieve the idempotent writes in non-Streaming job. For details [Idempotent writes](/delta-batch/#idempotent-writes).\n\n### Example\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    app_id = ... # A unique string that is used as an application ID.\n\n    def writeToDeltaLakeTableIdempotent(batch_df, batch_id):\n      batch_df.write.format(...).option(\"txnVersion\", batch_id).option(\"txnAppId\", app_id).save(...) # location 1\n      batch_df.write.format(...).option(\"txnVersion\", batch_id).option(\"txnAppId\", app_id).save(...) # location 2\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    val appId = ... // A unique string that is used as an application ID.\n    streamingDF.writeStream.foreachBatch { (batchDF: DataFrame, batchId: Long) =>\n      batchDF.write.format(...).option(\"txnVersion\", batchId).option(\"txnAppId\", appId).save(...)  // location 1\n      batchDF.write.format(...).option(\"txnVersion\", batchId).option(\"txnAppId\", appId).save(...)  // location 2\n    }\n    ```\n  \n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content/docs/delta-trino-integration.mdx",
    "content": "---\ntitle: Trino connector\ndescription: Learn how to set up an integration to enable you to read Delta tables from Trino.\n---\n\nSince Trino [version 373](https://trino.io/docs/current/release/release-373.html), Trino natively supports reading and writing the Delta Lake tables. For details on using the native Delta Lake connector, see [Delta Lake Connector - Trino](https://trino.io/docs/current/connector/delta-lake.html). For Trino versions lower than [version 373](https://trino.io/docs/current/release/release-373.html), you can use the manifest-based approach detailed in [Presto, Trino, and Athena to Delta Lake integration using manifests](/presto-integration/).\n"
  },
  {
    "path": "docs/src/content/docs/delta-type-widening.mdx",
    "content": "---\ntitle: Delta type widening\ndescription: Learn about type widening in Delta.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n<Aside type=\"note\">\n  This feature is available in preview in Delta Lake 3.2 and above, and fully supported in Delta Lake 4.0 and above.\n</Aside>\n\nThe type widening feature allows changing the type of columns in a Delta table to a wider type. This enables manual type changes using the `ALTER TABLE ALTER COLUMN` command and automatic type migration with schema evolution during write operations.\n\n## Supported type changes\n\nThe feature introduces a limited set of supported type changes in Delta Lake 3.2 and expands it in Delta Lake 4.0 and above.\n\n| Source type | Supported wider types - Delta 3.2 | Supported wider types - Delta 4.0           |\n|-------------|-----------------------------------|---------------------------------------------|\n| `byte`      | `short`, `int`                    | `short`, `int`, `long`, `decimal`, `double` |\n| `short`     | `int`                             | `int`, `long`, `decimal`, `double`          |\n| `int`       |                                   | `long`, `decimal`, `double`                 |\n| `long`      |                                   | `decimal`                                   |\n| `float`     |                                   | `double`                                    |\n| `decimal`   |                                   | `decimal` with greater precision and scale  |\n| `date`      |                                   | `timestampNTZ`                              |\n\nTo avoid accidentally promoting integer values to decimals, you must **manually commit** type changes from `byte`, `short`, `int`, or `long` to `decimal` or `double`. When promoting an integer type to `decimal` or `double`, if any downstream ingestion writes this value back to an integer column, Spark will truncate the fractional part of the values by default.\n\n<Aside type=\"note\">\n  When changing an integer or decimal type to decimal, the total precision must be equal to or greater than the starting precision. If you also increase the scale, the total precision must increase by a corresponding amount.\n  That is, `decimal(p, s)` can be changed to `decimal(p + k1, s + k2)` iff `k1 >= k2 >= 0`.\n\n  For example, if you want to add two decimal places to a field with `decimal(10,1)`, the minimum target is `decimal(12,3)`.\n\n  The minimum target for `byte`, `short`, and `int` types is `decimal(10,0)`. The minimum target for `long` is `decimal(20,0)`.\n</Aside>\n\nType changes are supported for top-level columns as well as fields nested inside structs, maps and arrays.\n\n## How to enable Delta Lake type widening\n\n<Aside\n  type=\"note\"\n  title=\"Important\"\n>\n  Enabling type widening using Delta Lake 3.3 and above sets the Delta table feature `typeWidening`, a reader/writer protocol feature. Only clients that support this table feature can read and write to the table once the table feature is set. You must use Delta Lake 3.3 or above to read and write to such Delta tables.\n  \n  Enabling type widening using Delta Lake 3.2 sets the Delta table feature `typeWidening-preview` on the table instead. You must use Delta Lake 3.2 or above to read and write to such Delta tables.\n</Aside>\n\nYou can enable type widening on an existing table by setting the `delta.enableTypeWidening` table property to `true`:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> SET TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\n    ```\n  </TabItem>\n</Tabs>\n\nAlternatively, you can enable type widening during table creation:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE TABLE <table_name> USING DELTA TBLPROPERTIES('delta.enableTypeWidening' = 'true')\n    ```\n  </TabItem>\n</Tabs>\n\nTo disable type widening:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> SET TBLPROPERTIES ('delta.enableTypeWidening' = 'false')\n    ```\n  </TabItem>\n</Tabs>\n\nDisabling type widening prevents future type changes from being applied to the table. It doesn't affect type changes previously applied and in particular, it doesn't remove the type widening table feature and doesn't allow clients that don't support the type widening table feature to read and write to the table.\n\nTo remove the type widening table feature from the table and allow other clients that don't support this feature to read and write to the table, see [Removing the type widening table feature](#removing-the-type-widening-table-feature).\n\n## Manually applying a type change\n\nWhen type widening is enabled on a Delta table, you can change the type of a column using the `ALTER COLUMN` command:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE <table_name> ALTER COLUMN <col_name> TYPE <new_type>\n    ```\n  </TabItem>\n</Tabs>\n\nThe table schema is updated without rewriting the underlying Parquet files.\n\n## Type changes with automatic schema evolution\n\nSchema evolution works with type widening to update data types in target tables to match the type of incoming data.\n\n<Aside type=\"note\">\n  Without type widening enabled, schema evolution always attempts to downcast data to match column types in the target table. If you don't want to automatically widen data types in your target tables, disable type widening before you run workloads with schema evolution enabled.\n</Aside>\n\nTo use schema evolution to widen the data type of a column during ingestion, you must meet the following conditions:\n\n- The write command runs with automatic schema evolution enabled.\n- The target table has type widening enabled.\n- The source column type is wider than the target column type.\n- Type widening supports the type change.\n- The type change is not one of `byte`, `short`, `int`, or `long` to `decimal` or `double`. These type changes can only be applied manually using ALTER TABLE to avoid accidental promotion of integers to decimals.\n\nType mismatches that don't meet all of these conditions follow normal schema enforcement rules.\n\n## Removing the type widening table feature\n\nThe type widening feature can be removed from a Delta table using the `DROP FEATURE` command:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n\n    ```sql\n    ALTER TABLE <table_name> DROP FEATURE 'typeWidening' [TRUNCATE HISTORY]\n    ```\n\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  Tables that enabled type widening using Delta Lake 3.2 require dropping feature `typeWidening-preview` instead.\n</Aside>\n\nSee [Drop Delta table features](/delta-drop-feature/) for more information on dropping Delta table features.\n\nWhen dropping the type widening feature, the underlying Parquet files are rewritten when necessary to ensure that the column types in the files match the column types in the Delta table schema. After the type widening feature is removed from the table, Delta clients that don't support the feature can read and write to the table.\n\n## Limitations\n\n### Iceberg Compatibility\n\nIceberg doesn't support all type changes covered by type widening, see [Iceberg Schema Evolution](https://iceberg.apache.org/spec/#schema-evolution). In particular, Iceberg V2 does not support the following type changes:\n\n- `byte`, `short`, `int`, `long` to `decimal` or `double`\n- decimal scale increase\n- `date` to `timestampNTZ`\n\nWhen [UniForm with Iceberg compatibility](/delta-uniform) is enabled on a Delta table, applying one of these type changes results in an error.\n\nIf you apply one of these unsupported type changes to a Delta table, enabling [Uniform with Iceberg compatibility](/delta-uniform) on the table results in an error.\nTo resolve the error, you must [drop the type widening table feature](#removing-the-type-widening-table-feature)."
  },
  {
    "path": "docs/src/content/docs/delta-uniform.mdx",
    "content": "---\ntitle: Universal Format (UniForm)\ndescription: Configure Delta tables to be read as Iceberg/Hudi tables using UniForm.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\nDelta Universal Format (UniForm) allows you to read Delta tables with Iceberg and Hudi clients.\n\nUniForm takes advantage of the fact that Delta Lake, Iceberg, and Hudi all consist of Parquet data files and a metadata layer. UniForm automatically generates Iceberg metadata asynchronously, allowing Iceberg clients to read Delta tables as if they were Iceberg or Hudi tables. You can expect negligible Delta write overhead when UniForm is enabled, as the metadata conversion and transaction occurs asynchronously after the Delta commit.\n\nA single copy of the data files provides access to clients of all formats.\n\n## Requirements\n\nTo enable UniForm, you must fulfill the following requirements:\n\n### Uniform Iceberg\n\n- The table must have column mapping enabled. See [Delta column mapping](/delta-column-mapping).\n- The Delta table must have a `minReaderVersion` >= 2 and `minWriterVersion` >= 7.\n- Writes to the table must use Delta Lake 3.1 or above.\n- Hive Metastore (HMS) must be configured as the catalog. See [the HMS documentation](https://spark.apache.org/docs/latest/sql-data-sources-hive-tables.html) for how to configure Apache Spark to use Hive Metastore.\n\n### Uniform Hudi (preview)\n\n- Writes to the table must use Delta Lake 3.2 or above.\n\n## Enable Delta Lake UniForm\n\n<Aside type=\"caution\" title=\"Important\">\n  Enabling Delta UniForm Iceberg requires the Delta table feature `IcebergCompatV2`, a write protocol feature. Only clients that support this table feature can write to enabled tables. You must use Delta Lake 3.1 or above to write to Delta tables with this feature enabled.\n\nEnabling Delta UniForm Iceberg requires \"delta-iceberg\" to be provided to Spark shell:\n\n```\n--packages io.delta:io.delta:delta-iceberg_2.12:<version>\n```\n\nEnabling Delta UniForm Hudi requires \"delta-hudi\" to be provided to Spark shell:\n\n```\n--packages io.delta:io.delta:delta-hudi_2.12:<version>\n```\n\n</Aside>\n\nThe following table properties enable UniForm support for Iceberg.\n\n```\n'delta.enableIcebergCompatV2' = 'true'\n'delta.universalFormat.enabledFormats' = 'iceberg'\n```\n\nThe following table properties enable UniForm support for Hudi.\n\n```\n'delta.universalFormat.enabledFormats' = 'hudi'\n```\n\nThe following table properties enable UniForm support for both.\n\n```\n'delta.enableIcebergCompatV2' = 'true'\n'delta.universalFormat.enabledFormats' = 'iceberg,hudi'\n```\n\nYou must also enable column mapping to use UniForm. It is set automatically during table creation, as in the following example:\n\n<Tabs>\n<TabItem label=\"SQL\">\n\n```sql\nCREATE TABLE T(c1 INT) USING DELTA TBLPROPERTIES(\n  'delta.enableIcebergCompatV2' = 'true',\n  'delta.universalFormat.enabledFormats' = 'iceberg');\n```\n\n</TabItem>\n</Tabs>\n\nIn Delta 3.3 and above, you can enable or upgrade UniForm Iceberg on an existing table using the following syntax:\n\n<Tabs>\n<TabItem label=\"SQL\">\n\n```sql\nALTER TABLE table_name SET TBLPROPERTIES(\n  'delta.enableIcebergCompatV2' = 'true',\n  'delta.universalFormat.enabledFormats' = 'iceberg');\n```\n\n</TabItem>\n</Tabs>\n\nYou can also use REORG to enable UniForm Iceberg and rewrite underlying data files, as in the following example:\n\n<Tabs>\n<TabItem label=\"SQL\">\n\n```sql\nREORG TABLE table_name APPLY (UPGRADE UNIFORM(ICEBERG_COMPAT_VERSION=2));\n```\n\n</TabItem>\n</Tabs>\n\nUse REORG if any of following are true:\n\n- Your table has deletion vectors enabled.\n- You previously enabled the IcebergCompatV1 version of UniForm Iceberg.\n- You need to read from Iceberg engines that don't support Hive-style Parquet files, such as Athena or Redshift.\n\nYou can enable UniForm Hudi on an existing table using the following syntax:\n\n<Tabs>\n<TabItem label=\"SQL\">\n\n```sql\nALTER TABLE table_name SET TBLPROPERTIES ('delta.universalFormat.enabledFormats' = 'hudi');\n```\n\n</TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  This syntax requires [Delta column mapping](/delta-column-mapping) to be\n  enabled on the table prior to running on Delta 3.1. This syntax also works to\n  upgrade from the IcbergCompatV1. It may rewrite existing files to make those\n  Iceberg compatible, and it automatically disables and purges Deletion Vectors\n  from the table.\n</Aside>\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  When you first enable UniForm, asynchronous metadata generation begins. This\n  task must complete before external clients can query the table using Iceberg\n  or Hudi. See [Check Iceberg/Hudi metadata generation\n  status](#check-iceberghudi-metadata-generation-status).\n</Aside>\n\n<Aside type=\"caution\">\n  You can turn off UniForm by unsetting the\n  `delta.universalFormat.enabledFormats` table property. You cannot turn off\n  column mapping once enabled, and upgrades to Delta Lake reader and writer\n  protocol versions cannot be undone.\n</Aside>\n\nSee [Limitations](#limitations).\n\n## When does UniForm generate metadata?\n\nDelta Lake triggers Iceberg/Hudi metadata generation asynchronously after a Delta Lake write transaction completes using the same compute that completed the Delta transaction.\n\nIceberg/Hudi can have significantly higher write latencies than Delta Lake. Delta tables with frequent commits might bundle multiple Delta commits into a single Iceberg/Hudi commit.\n\nDelta Lake ensures that only one metadata generation process per format is in progress at any time in a single cluster. Commits that would trigger a second concurrent metadata generation process successfully commit to Delta, but do not trigger asynchronous metadata generation. This prevents cascading latency for metadata generation for workloads with frequent commits (seconds to minutes between commits).\n\n## Check Iceberg/Hudi metadata generation status\n\nUniForm adds the following properties to Iceberg/Hudi table metadata to track metadata generation status:\n\n| Table property | Description |\n| --- | --- |\n| `converted_delta_version` | The latest version of the Delta table for which metadata was successfully generated. |\n| `converted_delta_timestamp` | The timestamp of the latest Delta commit for which metadata was successfully generated. |\n\nSee documentation for your Iceberg/Hudi reader client for how to review table properties outside Delta Lake. For Apache Spark, you can see these properties using the following syntax:\n\n<Tabs>\n<TabItem label=\"SQL\">\n\n```sql\nSHOW TBLPROPERTIES <table-name>;\n```\n\n</TabItem>\n</Tabs>\n\n## Read UniForm tables as Iceberg tables in Apache Spark\n\nYou are able to read UniForm tables as Iceberg tables in Apache Spark with the following steps:\n\n- Start Apache Spark with Iceberg, and connect to the Hive Metastore used by UniForm. Please refer to the [Iceberg documentation](https://iceberg.apache.org/docs/latest/spark-configuration/#catalogs) for how to run Iceberg with Apache Spark and connect to a Hive Metastore.\n- Use the `SHOW TABLES` command to see a list of available Iceberg tables in the catalog.\n- Read an Iceberg table using standard SQL such as `SELECT`.\n\n## Read UniForm tables as Iceberg tables using a metadata JSON path\n\nSome Iceberg clients allow you to register external Iceberg tables by providing a path to versioned metadata files. Each time UniForm converts a new version of the Delta table to Iceberg, it creates a new metadata JSON file.\n\nClients that use metadata JSON paths for configuring Iceberg include BigQuery. Refer to documentation for the Iceberg reader client for configuration details.\n\nDelta Lake stores Iceberg metadata under the table directory, using the following pattern:\n\n```ini\n<table-path>/metadata/v<version-number>-uuid.metadata.json\n```\n\n## Read UniForm tables as Hudi tables in Apache Spark\n\nYou are able to read UniForm tables as Hudi tables in Apache Spark with the following steps:\n\n- See [Hudi documentation](https://hudi.apache.org/docs/quick-start-guide#spark-shellsql) for how to run Hudi on Apache Spark\n\n<Tabs>\n<TabItem label=\"Scala\">\n\n```scala\nspark.read.format(\"hudi\")\n  .option(\"hoodie.metadata.enable\", \"true\")\n  .load(\"PATH_TO_UNIFORM_TABLE_DIRECTORY\")\n```\n\n</TabItem>\n</Tabs>\n\n## Delta and Iceberg/Hudi table versions\n\nAll Delta Lake, Iceberg and Hudi allow time travel queries using table versions or timestamps stored in table metadata.\n\nDelta and Iceberg table versions do not align by either the commit timestamp or the version ID. However, Delta and Hudi commit timestamp align, but version ID does not. If you wish to verify which version of a Delta table a given version of an Iceberg/Hudi table corresponds to, you can use the corresponding table properties set on the Iceberg/Hudi table. See [Check Iceberg/Hudi metadata generation status](#check-iceberghudi-metadata-generation-status).\n\n## Limitations\n\n<Aside type=\"caution\">\n  UniForm is read-only from an Iceberg and Hudi perspective. This, however,\n  cannot be enforced as for Iceberg, UniForm uses HMS as an Iceberg catalog and\n  for Hudi, metadata is stored on the file system. If any external writer (not\n  Delta Lake) writes to this Iceberg/Hudi table, this may destroy your Delta\n  table and cause data loss, as the Iceberg/Hudi writer may perform data cleanup\n  or garbage collection that Delta is unaware of.\n</Aside>\n\nThe following limitations exist:\n\n- UniForm does not work on tables with deletion vectors enabled. See [What are deletion vectors?](/delta-deletion-vectors).\n- Delta tables with UniForm enabled do not support `VOID` type.\n- Iceberg/Hudi clients can only read from UniForm. Writes are not supported.\n- Iceberg/Hudi reader clients might have individual limitations, regardless of UniForm. See documentation for your target client.\n\nThe following Delta Lake features work for Delta clients when UniForm is enabled, but do not have support in Iceberg:\n\n- Change Data Feed\n- Delta Sharing\n"
  },
  {
    "path": "docs/src/content/docs/delta-update.mdx",
    "content": "---\ntitle: Table deletes, updates, and merges\ndescription: Learn how to delete data from and update data in Delta tables.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nDelta Lake supports several statements to facilitate deleting data from and updating data in Delta tables.\n\n## Delete from a table\n\nYou can remove data that matches a predicate from a Delta table. For instance, in a table named `people10m` or a path at `/tmp/delta/people-10m`, to delete all rows corresponding to people with a value in the `birthDate` column from before `1955`, you can run the following:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    DELETE FROM people10m WHERE birthDate < '1955-01-01'\n\n    DELETE FROM delta.`/tmp/delta/people-10m` WHERE birthDate < '1955-01-01'\n    ```\n\n  </TabItem>\n</Tabs>\n\nSee [Configure SparkSession](/delta-batch#configure-sparksession) for the steps to enable support for SQL commands.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import *\n    from pyspark.sql.functions import *\n\n    deltaTable = DeltaTable.forPath(spark, '/tmp/delta/people-10m')\n\n    # Declare the predicate by using a SQL-formatted string.\n    deltaTable.delete(\"birthDate < '1955-01-01'\")\n\n    # Declare the predicate by using Spark SQL functions.\n    deltaTable.delete(col('birthDate') < '1960-01-01')\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, \"/tmp/delta/people-10m\")\n\n    // Declare the predicate by using a SQL-formatted string.\n    deltaTable.delete(\"birthDate < '1955-01-01'\")\n\n    import org.apache.spark.sql.functions._\n    import spark.implicits._\n\n    // Declare the predicate by using Spark SQL functions and implicits.\n    deltaTable.delete(col(\"birthDate\") < \"1955-01-01\")\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Java\">\n\n    ```java\n    import io.delta.tables.*;\n    import org.apache.spark.sql.functions;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, \"/tmp/delta/people-10m\");\n\n    // Declare the predicate by using a SQL-formatted string.\n    deltaTable.delete(\"birthDate < '1955-01-01'\");\n\n    // Declare the predicate by using Spark SQL functions.\n    deltaTable.delete(functions.col(\"birthDate\").lt(functions.lit(\"1955-01-01\")));\n    ``` \n  \n  </TabItem>\n</Tabs>\n\nSee the [Delta Lake APIs](/delta-apidoc/) for details.\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  `delete` removes the data from the latest version of the Delta table but does\n  not remove it from the physical storage until the old versions are explicitly\n  vacuumed. See\n  [vacuum](/delta-utility/#remove-files-no-longer-referenced-by-a-delta-table)\n  for details.\n</Aside>\n\n<Aside type=\"tip\">\n  When possible, provide predicates on the partition columns for a partitioned\n  Delta table as such predicates can significantly speed up the operation.\n</Aside>\n\n## Update a table\n\nYou can update data that matches a predicate in a Delta table. For example, in a table named `people10m` or a path at `/tmp/delta/people-10m`, to change an abbreviation in the `gender` column from `M` or `F` to `Male` or `Female`, you can run the following:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    UPDATE people10m SET gender = 'Female' WHERE gender = 'F';\n    UPDATE people10m SET gender = 'Male' WHERE gender = 'M';\n\n    UPDATE delta.`/tmp/delta/people-10m` SET gender = 'Female' WHERE gender = 'F';\n    UPDATE delta.`/tmp/delta/people-10m` SET gender = 'Male' WHERE gender = 'M';\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee [Configure SparkSession](/delta-batch#configure-sparksession) for the steps to enable support for SQL commands.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    from delta.tables import *\n    from pyspark.sql.functions import *\n\n    deltaTable = DeltaTable.forPath(spark, '/tmp/delta/people-10m')\n\n    # Declare the predicate by using a SQL-formatted string.\n    deltaTable.update(\n      condition = \"gender = 'F'\",\n      set = { \"gender\": \"'Female'\" }\n    )\n\n    # Declare the predicate by using Spark SQL functions.\n    deltaTable.update(\n      condition = col('gender') == 'M',\n      set = { 'gender': lit('Male') }\n    )\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, \"/tmp/delta/people-10m\")\n\n    // Declare the predicate by using a SQL-formatted string.\n    deltaTable.updateExpr(\n      \"gender = 'F'\",\n      Map(\"gender\" -> \"'Female'\")\n\n    import org.apache.spark.sql.functions._\n    import spark.implicits._\n\n    // Declare the predicate by using Spark SQL functions and implicits.\n    deltaTable.update(\n      col(\"gender\") === \"M\",\n      Map(\"gender\" -> lit(\"Male\")));\n    ```\n  \n  </TabItem>\n\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n    import org.apache.spark.sql.functions;\n    import java.util.HashMap;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, \"/data/events/\");\n\n    // Declare the predicate by using a SQL-formatted string.\n    deltaTable.updateExpr(\n      \"gender = 'F'\",\n      new HashMap<String, String>() {{\n        put(\"gender\", \"'Female'\");\n      }}\n    );\n\n    // Declare the predicate by using Spark SQL functions.\n    deltaTable.update(\n      functions.col(gender).eq(\"M\"),\n      new HashMap<String, Column>() {{\n        put(\"gender\", functions.lit(\"Male\"));\n      }}\n    );\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee the [Delta Lake APIs](/delta-apidoc/) for details.\n\n<Aside type=\"tip\">\n  Similar to delete, update operations can get a significant speedup with\n  predicates on partitions.\n</Aside>\n\n## Upsert into a table using merge\n\nYou can upsert data from a source table, view, or DataFrame into a target Delta table by using the `MERGE` SQL operation. Delta Lake supports inserts, updates and deletes in `MERGE`, and it supports extended syntax beyond the SQL standards to facilitate advanced use cases.\n\nSuppose you have a source table named `people10mupdates` or a source path at `/tmp/delta/people-10m-updates` that contains new data for a target table named `people10m` or a target path at `/tmp/delta/people-10m`. Some of these new records may already be present in the target data. To merge the new data, you want to update rows where the person's `id` is already present and insert the new rows where no matching `id` is present. You can run the following:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    MERGE INTO people10m\n    USING people10mupdates\n    ON people10m.id = people10mupdates.id\n    WHEN MATCHED THEN\n      UPDATE SET\n        id = people10mupdates.id,\n        firstName = people10mupdates.firstName,\n        middleName = people10mupdates.middleName,\n        lastName = people10mupdates.lastName,\n        gender = people10mupdates.gender,\n        birthDate = people10mupdates.birthDate,\n        ssn = people10mupdates.ssn,\n        salary = people10mupdates.salary\n    WHEN NOT MATCHED\n      THEN INSERT (\n        id,\n        firstName,\n        middleName,\n        lastName,\n        gender,\n        birthDate,\n        ssn,\n        salary\n      )\n      VALUES (\n        people10mupdates.id,\n        people10mupdates.firstName,\n        people10mupdates.middleName,\n        people10mupdates.lastName,\n        people10mupdates.gender,\n        people10mupdates.birthDate,\n        people10mupdates.ssn,\n        people10mupdates.salary\n      )\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee [Configure SparkSession](/delta-batch/#configure-sparksession) for the steps to enable support for SQL commands.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    from delta.tables import *\n\n    deltaTablePeople = DeltaTable.forPath(spark, '/tmp/delta/people-10m')\n    deltaTablePeopleUpdates = DeltaTable.forPath(spark, '/tmp/delta/people-10m-updates')\n\n    dfUpdates = deltaTablePeopleUpdates.toDF()\n\n    deltaTablePeople.alias('people') \\\n      .merge(\n        dfUpdates.alias('updates'),\n        'people.id = updates.id'\n      ) \\\n      .whenMatchedUpdate(set =\n        {\n          \"id\": \"updates.id\",\n          \"firstName\": \"updates.firstName\",\n          \"middleName\": \"updates.middleName\",\n          \"lastName\": \"updates.lastName\",\n          \"gender\": \"updates.gender\",\n          \"birthDate\": \"updates.birthDate\",\n          \"ssn\": \"updates.ssn\",\n          \"salary\": \"updates.salary\"\n        }\n      ) \\\n      .whenNotMatchedInsert(values =\n        {\n          \"id\": \"updates.id\",\n          \"firstName\": \"updates.firstName\",\n          \"middleName\": \"updates.middleName\",\n          \"lastName\": \"updates.lastName\",\n          \"gender\": \"updates.gender\",\n          \"birthDate\": \"updates.birthDate\",\n          \"ssn\": \"updates.ssn\",\n          \"salary\": \"updates.salary\"\n        }\n      ) \\\n      .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n    import org.apache.spark.sql.functions._\n\n    val deltaTablePeople = DeltaTable.forPath(spark, \"/tmp/delta/people-10m\")\n    val deltaTablePeopleUpdates = DeltaTable.forPath(spark, \"tmp/delta/people-10m-updates\")\n    val dfUpdates = deltaTablePeopleUpdates.toDF()\n\n    deltaTablePeople\n      .as(\"people\")\n      .merge(\n        dfUpdates.as(\"updates\"),\n        \"people.id = updates.id\")\n      .whenMatched\n      .updateExpr(\n        Map(\n          \"id\" -> \"updates.id\",\n          \"firstName\" -> \"updates.firstName\",\n          \"middleName\" -> \"updates.middleName\",\n          \"lastName\" -> \"updates.lastName\",\n          \"gender\" -> \"updates.gender\",\n          \"birthDate\" -> \"updates.birthDate\",\n          \"ssn\" -> \"updates.ssn\",\n          \"salary\" -> \"updates.salary\"\n        ))\n      .whenNotMatched\n      .insertExpr(\n        Map(\n          \"id\" -> \"updates.id\",\n          \"firstName\" -> \"updates.firstName\",\n          \"middleName\" -> \"updates.middleName\",\n          \"lastName\" -> \"updates.lastName\",\n          \"gender\" -> \"updates.gender\",\n          \"birthDate\" -> \"updates.birthDate\",\n          \"ssn\" -> \"updates.ssn\",\n          \"salary\" -> \"updates.salary\"\n        ))\n      .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n    import org.apache.spark.sql.functions;\n    import java.util.HashMap;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, \"/tmp/delta/people-10m\")\n    Dataset<Row> dfUpdates = spark.read(\"delta\").load(\"/tmp/delta/people-10m-updates\")\n\n    deltaTable\n      .as(\"people\")\n      .merge(\n        dfUpdates.as(\"updates\"),\n        \"people.id = updates.id\")\n      .whenMatched()\n      .updateExpr(\n        new HashMap<String, String>() {{\n          put(\"id\", \"updates.id\");\n          put(\"firstName\", \"updates.firstName\");\n          put(\"middleName\", \"updates.middleName\");\n          put(\"lastName\", \"updates.lastName\");\n          put(\"gender\", \"updates.gender\");\n          put(\"birthDate\", \"updates.birthDate\");\n          put(\"ssn\", \"updates.ssn\");\n          put(\"salary\", \"updates.salary\");\n        }})\n      .whenNotMatched()\n      .insertExpr(\n        new HashMap<String, String>() {{\n          put(\"id\", \"updates.id\");\n          put(\"firstName\", \"updates.firstName\");\n          put(\"middleName\", \"updates.middleName\");\n          put(\"lastName\", \"updates.lastName\");\n          put(\"gender\", \"updates.gender\");\n          put(\"birthDate\", \"updates.birthDate\");\n          put(\"ssn\", \"updates.ssn\");\n          put(\"salary\", \"updates.salary\");\n        }})\n      .execute();\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee the [Delta Lake APIs](/delta-apidoc/) for Scala, Java, and Python syntax details.\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Delta Lake merge operations typically require two passes over the source data.\n  If your source data contains nondeterministic expressions, multiple passes on\n  the source data can produce different rows causing incorrect results. Some\n  common examples of nondeterministic expressions include the `current_date` and\n  `current_timestamp` functions. In Delta Lake 2.2 and above this issue is\n  solved by automatically materializing the source data as part of the merge\n  command, so that the source data is deterministic in multiple passes. In Delta\n  Lake 2.1 and below if you cannot avoid using non-deterministic functions,\n  consider saving the source data to storage, for example as a temporary Delta\n  table. Caching the source data may not address this issue, as cache\n  invalidation can cause the source data to be recomputed partially or\n  completely (for example when a cluster loses some of it executors when scaling\n  down).\n</Aside>\n\n### Modify all unmatched rows using merge\n\n<Aside type=\"note\">\n  `WHEN NOT MATCHED BY SOURCE` clauses are supported by the Scala, Python and\n  Java [Delta Lake APIs](/delta-apidoc/) in Delta 2.3 and above. SQL is\n  supported in Delta 2.4 and above.\n</Aside>\n\nYou can use the `WHEN NOT MATCHED BY SOURCE` clause to `UPDATE` or `DELETE` records in the target table that do not have corresponding records in the source table. We recommend adding an optional conditional clause to avoid fully rewriting the target table.\n\nThe following code example shows the basic syntax of using this for deletes, overwriting the target table with the contents of the source table and deleting unmatched records in the target table.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    MERGE INTO target\n    USING source\n    ON source.key = target.key\n    WHEN MATCHED\n      UPDATE SET *\n    WHEN NOT MATCHED\n      INSERT *\n    WHEN NOT MATCHED BY SOURCE\n      DELETE\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    (targetDF\n      .merge(sourceDF, \"source.key = target.key\")\n      .whenMatchedUpdateAll()\n      .whenNotMatchedInsertAll()\n      .whenNotMatchedBySourceDelete()\n      .execute()\n    )\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    targetDF\n      .merge(sourceDF, \"source.key = target.key\")\n      .whenMatched()\n      .updateAll()\n      .whenNotMatched()\n      .insertAll()\n      .whenNotMatchedBySource()\n      .delete()\n      .execute()\n    ```\n  \n  </TabItem>\n</Tabs>\n\nThe following example adds conditions to the `WHEN NOT MATCHED BY SOURCE` clause and specifies values to update in unmatched target rows.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    MERGE INTO target\n    USING source\n    ON source.key = target.key\n    WHEN MATCHED THEN\n      UPDATE SET target.lastSeen = source.timestamp\n    WHEN NOT MATCHED THEN\n      INSERT (key, lastSeen, status) VALUES (source.key,  source.timestamp, 'active')\n    WHEN NOT MATCHED BY SOURCE AND target.lastSeen >= (current_date() - INTERVAL '5' DAY) THEN\n      UPDATE SET target.status = 'inactive'\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python  \n    (targetDF\n      .merge(sourceDF, \"source.key = target.key\")\n      .whenMatchedUpdate(\n        set = {\"target.lastSeen\": \"source.timestamp\"}\n      )\n      .whenNotMatchedInsert(\n        values = {\n          \"target.key\": \"source.key\",\n          \"target.lastSeen\": \"source.timestamp\",\n          \"target.status\": \"'active'\"\n        }\n      )\n      .whenNotMatchedBySourceUpdate(\n        condition=\"target.lastSeen >= (current_date() - INTERVAL '5' DAY)\",\n        set = {\"target.status\": \"'inactive'\"}\n      )\n      .execute()\n    )\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    targetDF\n      .merge(sourceDF, \"source.key = target.key\")\n      .whenMatched()\n      .updateExpr(Map(\"target.lastSeen\" -> \"source.timestamp\"))\n      .whenNotMatched()\n      .insertExpr(Map(\n        \"target.key\" -> \"source.key\",\n        \"target.lastSeen\" -> \"source.timestamp\",\n        \"target.status\" -> \"'active'\",\n        )\n      )\n      .whenNotMatchedBySource(\"target.lastSeen >= (current_date() - INTERVAL '5' DAY)\")\n      .updateExpr(Map(\"target.status\" -> \"'inactive'\"))\n      .execute()\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Operation semantics\n\nHere is a detailed description of the `merge` programmatic operation.\n\n- There can be any number of `whenMatched` and `whenNotMatched` clauses.\n\n- `whenMatched` clauses are executed when a source row matches a target table row based on the match condition. These clauses have the following semantics.\n\n  - `whenMatched` clauses can have at most one `update` and one `delete` action. The `update` action in `merge` only updates the specified columns (similar to the `update` [operation](/delta-update/#update-a-table)) of the matched target row. The `delete` action deletes the matched row.\n  - Each `whenMatched` clause can have an optional condition. If this clause condition exists, the `update` or `delete` action is executed for any matching source-target row pair only when the clause condition is true.\n  - If there are multiple `whenMatched` clauses, then they are evaluated in the order they are specified. All `whenMatched` clauses, except the last one, must have conditions.\n  - If none of the `whenMatched` conditions evaluate to true for a source and target row pair that matches the merge condition, then the target row is left unchanged.\n  - To update all the columns of the target Delta table with the corresponding columns of the source dataset, use `whenMatched(...).updateAll()`. This is equivalent to:\n\n    <Tabs syncKey=\"code-examples\">\n      <TabItem label=\"Scala\">\n\n        ```scala\n        whenMatched(...).updateExpr(Map(\"col1\" -> \"source.col1\", \"col2\" -> \"source.col2\", ...))\n        ```\n      \n      </TabItem>\n    </Tabs>\n\n    for all the columns of the target Delta table. Therefore, this action assumes that the source table has the same columns as those in the target table, otherwise the query throws an analysis error.\n\n    <Aside type=\"note\">\n      This behavior changes when automatic schema migration is enabled. See [Automatic schema evolution](/delta-update/#automatic-schema-evolution) for details.\n    </Aside>\n\n- `whenNotMatched` clauses are executed when a source row does not match any target row based on the match condition. These clauses have the following semantics.\n\n  - `whenNotMatched` clauses can have only the `insert` action. The new row is generated based on the specified column and corresponding expressions. You do not need to specify all the columns in the target table. For unspecified target columns, `NULL` is inserted.\n  - Each `whenNotMatched` clause can have an optional condition. If the clause condition is present, a source row is inserted only if that condition is true for that row. Otherwise, the source column is ignored.\n  - If there are multiple `whenNotMatched` clauses, then they are evaluated in the order they are specified. All `whenNotMatched` clauses, except the last one, must have conditions.\n  - To insert all the columns of the target Delta table with the corresponding columns of the source dataset, use `whenNotMatched(...).insertAll()`. This is equivalent to:\n    \n    <Tabs syncKey=\"code-examples\">\n      <TabItem label=\"Scala\">\n\n        ```scala\n        whenNotMatched(...).insertExpr(Map(\"col1\" -> \"source.col1\", \"col2\" -> \"source.col2\", ...))\n        ```\n      \n      </TabItem>\n    </Tabs>\n\n    for all the columns of the target Delta table. Therefore, this action assumes that the source table has the same columns as those in the target table, otherwise the query throws an analysis error.\n\n    <Aside type=\"note\">\n      This behavior changes when automatic schema migration is enabled. See [Automatic schema evolution](/delta-update/#automatic-schema-evolution) for details.\n    </Aside>\n\n- `whenNotMatchedBySource` clauses are executed when a target row does not match any source row based on the merge condition. These clauses have the following semantics.\n  - `whenNotMatchedBySource` clauses can specify `delete` and `update` actions.\n  - Each `whenNotMatchedBySource` clause can have an optional condition. If the clause condition is present, a target row is modified only if that condition is true for that row. Otherwise, the target row is left unchanged.\n  - If there are multiple `whenNotMatchedBySource` clauses, then they are evaluated in the order they are specified. All `whenNotMatchedBySource` clauses, except the last one, must have conditions.\n  - By definition, `whenNotMatchedBySource` clauses do not have a source row to pull column values from, and so source columns can't be referenced. For each column to be modified, you can either specify a literal or perform an action on the target column, such as `SET target.deleted_count = target.deleted_count + 1`.\n\n<Aside type=\"caution\" title=\"Important!\">\n  - A `merge` operation can fail if multiple rows of the source dataset match\n  and the merge attempts to update the same rows of the target Delta table.\n  According to the SQL semantics of merge, such an update operation is ambiguous\n  as it is unclear which source row should be used to update the matched target\n  row. You can preprocess the source table to eliminate the possibility of\n  multiple matches. See the [change data capture\n  example](/delta-update/#write-change-data-into-a-delta-table)—it shows how to\n  preprocess the change dataset (that is, the source dataset) to retain only the\n  latest change for each key before applying that change into the target Delta\n  table.\n  - You can apply a SQL `MERGE` operation on a SQL VIEW only if the view\n  has been defined as `CREATE VIEW viewName AS SELECT * FROM deltaTable`.\n</Aside>\n\n### Schema validation\n\n`merge` automatically validates that the schema of the data generated by insert and update expressions are compatible with the schema of the table. It uses the following rules to determine whether the `merge` operation is compatible:\n\n- For `update` and `insert` actions, the specified target columns must exist in the target Delta table.\n- For `updateAll` and `insertAll` actions, the source dataset must have all the columns of the target Delta table. The source dataset can have extra columns and they are ignored.\n\nIf you do not want the extra columns to be ignored and instead want to update the target table schema to include new columns, see [Automatic schema evolution](/delta-update/#automatic-schema-evolution).\n\n- For all actions, if the data type generated by the expressions producing the target columns are different from the corresponding columns in the target Delta table, `merge` tries to cast them to the types in the table.\n\n### Automatic schema evolution\n\nSchema evolution allows users to resolve schema mismatches between the target and source table in merge. It handles the following two cases:\n\n1. A column in the source table is not present in the target table. The new column is added to the target schema, and its values are inserted or updated using the source values.\n2. A column in the target table is not present in the source table. The target schema is left unchanged; the values in the additional target column are either left unchanged (for `UPDATE`) or set to `NULL` (for `INSERT`).\n\n<Aside type=\"caution\">\n  To use schema evolution, you must set the Spark session\n  configuration`spark.databricks.delta.schema.autoMerge.enabled` to `true`\n  before you run the `merge` command.\n</Aside>\n\n<Aside type=\"note\">\n  In Delta 2.3 and above, columns present in the source table can be specified\n  by name in insert or update actions. In Delta 2.2 and below, only `INSERT *`\n  or `UPDATE SET *` actions can be used for schema evolution with merge.\n</Aside>\n\nHere are a few examples of the effects of `merge` operation with and without schema evolution.\n\n| Columns | Query (in SQL) | Behavior without schema evolution (default) | Behavior with schema evolution |\n| :-- | :-- | :-- | :-- |\n| Target: `key, value` Source: `key, value, new_value` | `sql MERGE INTO target_table t USING source_table s ON t.key = s.key WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *` | The table schema remains unchanged; only columns `key`, `value` are updated/inserted. | The table schema is changed to `(key, value, new_value)`. Existing records with matches are updated with the `value` and `new_value` in the source. New rows are inserted with the schema `(key, value, new_value)`. |\n| Target: `key, old_value` Source: `key, new_value` | `sql MERGE INTO target_table t USING source_table s ON t.key = s.key WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *` | `UPDATE` and `INSERT` actions throw an error because the target column `old_value` is not in the source. | The table schema is changed to `(key, old_value, new_value)`. Existing records with matches are updated with the `new_value` in the source leaving `old_value` unchanged. New records are inserted with the specified `key`, `new_value`, and `NULL` for the `old_value`. |\n| Target: `key, old_value` Source: `key, new_value` | `sql MERGE INTO target_table t USING source_table s ON t.key = s.key WHEN MATCHED THEN UPDATE SET new_value = s.new_value` | `UPDATE` throws an error because column `new_value` does not exist in the target table. | The table schema is changed to `(key, old_value, new_value)`. Existing records with matches are updated with the `new_value` in the source leaving `old_value` unchanged, and unmatched records have `NULL` entered for `new_value`. |\n| Target: `key, old_value` Source: `key, new_value` | `sql MERGE INTO target_table t USING source_table s ON t.key = s.key WHEN NOT MATCHED THEN INSERT (key, new_value) VALUES (s.key, s.new_value)` | `INSERT`throws an error because column`new_value`does not exist in the target table. | The table schema is changed to`(key, old_value, new_value)`. New records are inserted with the specified `key`, `new_value`, and `NULL`for the`old_value`. Existing records have `NULL`entered for`new_value`leaving`old_value` unchanged. See note (1). |\n\n## Special considerations for schemas that contain arrays of structs\n\nDelta `MERGE INTO` supports resolving struct fields by name and evolving schemas for arrays of structs. With schema evolution enabled, target table schemas will evolve for arrays of structs, which also works with any nested structs inside of arrays.\n\n<Aside type=\"note\">\n  In Delta 2.3 and above, struct fields present in the source table can be specified by name in insert or update commands. In Delta 2.2 and below, only `INSERT *` or `UPDATE SET *` commands can be used for schema evolution with merge.\n</Aside>\n\nHere are a few examples of the effects of merge operations with and without schema evolution for arrays of structs.\n\n| Source schema | Target schema | Behavior without schema evolution (default) | Behavior with schema evolution |\n| :-- | :-- | :-- | :-- |\n| array&lt;struct&lt;b: string, a: string&gt;&gt; | array&lt;struct&lt;a: int, b: int&gt;&gt; | The table schema remains unchanged. Columns will be resolved by name and updated or inserted. | The table schema remains unchanged. Columns will be resolved by name and updated or inserted. |\n| array&lt;struct&lt;a: int, c: string, d: string&gt;&gt; | array&lt;struct&lt;a: string, b: string&gt;&gt; | `update` and `insert` throw errors because `c` and `d` do not exist in the target table. | The table schema is changed to array&lt;struct&lt;a: string, b: string, c: string, d: string&gt;&gt;. `c` and `d` are inserted as `NULL` for existing entries in the target table. `update` and `insert` fill entries in the source table with `a` casted to string and `b` as `NULL`. |\n| array&lt;struct&lt;a: string, b: struct&lt;c: string, d: string&gt;&gt;&gt; | array&lt;struct&lt;a: string, b: struct&lt;c: string&gt;&gt;&gt; | `update` and `insert` throw errors because `d` does not exist in the target table. | The target table schema is changed to array&lt;struct&lt;a: string, b: struct&lt;c: string, d: string&gt;&gt;&gt;. `d` is inserted as `NULL` for existing entries in the target table. |\n\n### Performance tuning\n\nYou can reduce the time taken by merge using the following approaches:\n\n- **Reduce the search space for matches**: By default, the `merge` operation searches the entire Delta table to find matches in the source table. One way to speed up `merge` is to reduce the search space by adding known constraints in the match condition. For example, suppose you have a table that is partitioned by `country` and `date` and you want to use `merge` to update information for the last day and a specific country. Adding the condition\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"SQL\">\n\n      ```sql\n      events.date = current_date() AND events.country = 'USA'\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n  will make the query faster as it looks for matches only in the relevant partitions. Furthermore, it will also reduce the chances of conflicts with other concurrent operations. See [Concurrency control](/concurrency-control/) for more details.\n\n- **Compact files**: If the data is stored in many small files, reading the data to search for matches can become slow. You can compact small files into larger files to improve read throughput. See [Compact files](/best-practices/#compact-files) for details.\n- **Control the shuffle partitions for writes**: The `merge` operation shuffles data multiple times to compute and write the updated data. The number of tasks used to shuffle is controlled by the Spark session configuration `spark.sql.shuffle.partitions`. Setting this parameter not only controls the parallelism but also determines the number of output files. Increasing the value increases parallelism but also generates a larger number of smaller data files.\n- **Repartition output data before write**: For partitioned tables, `merge` can produce a much larger number of small files than the number of shuffle partitions. This is because every shuffle task can write multiple files in multiple partitions, and can become a performance bottleneck. In many cases, it helps to repartition the output data by the table's partition columns before writing it. You enable this by setting the Spark session configuration `spark.databricks.delta.merge.repartitionBeforeWrite.enabled` to `true`.\n\n## Merge examples\n\nHere are a few examples on how to use `merge` in different scenarios.\n\n### Data deduplication when writing into Delta tables\n\nA common ETL use case is to collect logs into Delta table by appending them to a table. However, often the sources can generate duplicate log records and downstream deduplication steps are needed to take care of them. With `merge`, you can avoid inserting the duplicate records.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    MERGE INTO logs\n    USING newDedupedLogs\n    ON logs.uniqueId = newDedupedLogs.uniqueId AND logs.date > current_date() - INTERVAL 7 DAYS\n    WHEN NOT MATCHED AND newDedupedLogs.date > current_date() - INTERVAL 7 DAYS\n      THEN INSERT *\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    deltaTable.alias(\"logs\").merge(\n      newDedupedLogs.alias(\"newDedupedLogs\"),\n      \"logs.uniqueId = newDedupedLogs.uniqueId\") \\\n    .whenNotMatchedInsertAll() \\\n    .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    deltaTable\n      .as(\"logs\")\n      .merge(\n        newDedupedLogs.as(\"newDedupedLogs\"),\n        \"logs.uniqueId = newDedupedLogs.uniqueId\")\n      .whenNotMatched()\n      .insertAll()\n      .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    deltaTable\n      .as(\"logs\")\n      .merge(\n        newDedupedLogs.as(\"newDedupedLogs\"),\n        \"logs.uniqueId = newDedupedLogs.uniqueId\")\n      .whenNotMatched()\n      .insertAll()\n      .execute();\n    ```\n  \n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  The dataset containing the new logs needs to be deduplicated within itself.\n</Aside>\n\nIf you know that you may get duplicate records only for a few days, you can optimized your query further by partitioning the table by date, and then specifying the date range of the target table to match on.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    MERGE INTO logs\n    USING newDedupedLogs\n    ON logs.uniqueId = newDedupedLogs.uniqueId AND logs.date > current_date() - INTERVAL 7 DAYS\n    WHEN NOT MATCHED AND newDedupedLogs.date > current_date() - INTERVAL 7 DAYS\n      THEN INSERT *\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    deltaTable.alias(\"logs\").merge(\n      newDedupedLogs.alias(\"newDedupedLogs\"),\n      \"logs.uniqueId = newDedupedLogs.uniqueId AND logs.date > current_date() - INTERVAL 7 DAYS\") \\\n    .whenNotMatchedInsertAll(\"newDedupedLogs.date > current_date() - INTERVAL 7 DAYS\") \\\n    .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    deltaTable.as(\"logs\").merge(\n      newDedupedLogs.as(\"newDedupedLogs\"),\n      \"logs.uniqueId = newDedupedLogs.uniqueId AND logs.date > current_date() - INTERVAL 7 DAYS\")\n    .whenNotMatched(\"newDedupedLogs.date > current_date() - INTERVAL 7 DAYS\")\n    .insertAll()\n    .execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    deltaTable.as(\"logs\").merge(\n      newDedupedLogs.as(\"newDedupedLogs\"),\n      \"logs.uniqueId = newDedupedLogs.uniqueId AND logs.date > current_date() - INTERVAL 7 DAYS\")\n    .whenNotMatched(\"newDedupedLogs.date > current_date() - INTERVAL 7 DAYS\")\n    .insertAll()\n    .execute();\n    ```\n  \n  </TabItem>\n</Tabs>\n\nThis is more efficient than the previous command as it looks for duplicates only in the last 7 days of logs, not the entire table. Furthermore, you can use this insert-only merge with Structured Streaming to perform continuous deduplication of the logs.\n\n- In a streaming query, you can use merge operation in `foreachBatch` to continuously write any streaming data to a Delta table with deduplication. See the following [streaming example](#upsert-from-streaming-queries-using-foreachbatch) for more information on `foreachBatch`.\n- In another streaming query, you can continuously read deduplicated data from this Delta table. This is possible because an insert-only merge only appends new data to the Delta table.\n\n### Slowly changing data (SCD) Type 2 operation into Delta tables\n\nAnother common operation is SCD Type 2, which maintains history of all changes made to each key in a dimensional table. Such operations require updating existing rows to mark previous values of keys as old, and the inserting the new rows as the latest values. Given a source table with updates and the target table with the dimensional data, SCD Type 2 can be expressed with `merge`.\n\nHere is a concrete example of maintaining the history of addresses for a customer along with the active date range of each address. When a customer's address needs to be updated, you have to mark the previous address as not the current one, update its active date range, and add the new address as the current one.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    customersTable = ...  # DeltaTable with schema (customerId, address, current, effectiveDate, endDate)\n\n    updatesDF = ...       # DataFrame with schema (customerId, address, effectiveDate)\n\n    # Rows to INSERT new addresses of existing customers\n    newAddressesToInsert = updatesDF \\\n      .alias(\"updates\") \\\n      .join(customersTable.toDF().alias(\"customers\"), \"customerid\") \\\n      .where(\"customers.current = true AND updates.address <> customers.address\")\n\n    # Stage the update by unioning two sets of rows\n    # 1. Rows that will be inserted in the whenNotMatched clause\n    # 2. Rows that will either update the current addresses of existing customers or insert the new addresses of new customers\n    stagedUpdates = (\n      newAddressesToInsert\n      .selectExpr(\"NULL as mergeKey\", \"updates.*\")   # Rows for 1\n      .union(updatesDF.selectExpr(\"updates.customerId as mergeKey\", \"*\"))  # Rows for 2.\n    )\n\n    # Apply SCD Type 2 operation using merge\n    customersTable.alias(\"customers\").merge(\n      stagedUpdates.alias(\"staged_updates\"),\n      \"customers.customerId = mergeKey\") \\\n    .whenMatchedUpdate(\n      condition = \"customers.current = true AND customers.address <> staged_updates.address\",\n      set = {                                      # Set current to false and endDate to source's effective date.\n        \"current\": \"false\",\n        \"endDate\": \"staged_updates.effectiveDate\"\n      }\n    ).whenNotMatchedInsert(\n      values = {\n        \"customerid\": \"staged_updates.customerId\",\n        \"address\": \"staged_updates.address\",\n        \"current\": \"true\",\n        \"effectiveDate\": \"staged_updates.effectiveDate\",  # Set current to true along with the new address and its effective date.\n        \"endDate\": \"null\"\n      }\n    ).execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    val customersTable: DeltaTable = ...   // table with schema (customerId, address, current, effectiveDate, endDate)\n\n    val updatesDF: DataFrame = ...          // DataFrame with schema (customerId, address, effectiveDate)\n\n    // Rows to INSERT new addresses of existing customers\n    val newAddressesToInsert = updatesDF\n      .as(\"updates\")\n      .join(customersTable.toDF.as(\"customers\"), \"customerid\")\n      .where(\"customers.current = true AND updates.address <> customers.address\")\n\n    // Stage the update by unioning two sets of rows\n    // 1. Rows that will be inserted in the whenNotMatched clause\n    // 2. Rows that will either update the current addresses of existing customers or insert the new addresses of new customers\n    val stagedUpdates = newAddressesToInsert\n      .selectExpr(\"NULL as mergeKey\", \"updates.*\")   // Rows for 1.\n      .union(\n        updatesDF.selectExpr(\"updates.customerId as mergeKey\", \"*\")  // Rows for 2.\n      )\n\n    // Apply SCD Type 2 operation using merge\n    customersTable\n      .as(\"customers\")\n      .merge(\n        stagedUpdates.as(\"staged_updates\"),\n        \"customers.customerId = mergeKey\")\n      .whenMatched(\"customers.current = true AND customers.address <> staged_updates.address\")\n      .updateExpr(Map(                                      // Set current to false and endDate to source's effective date.\n        \"current\" -> \"false\",\n        \"endDate\" -> \"staged_updates.effectiveDate\"))\n      .whenNotMatched()\n      .insertExpr(Map(\n        \"customerid\" -> \"staged_updates.customerId\",\n        \"address\" -> \"staged_updates.address\",\n        \"current\" -> \"true\",\n        \"effectiveDate\" -> \"staged_updates.effectiveDate\",  // Set current to true along with the new address and its effective date.\n        \"endDate\" -> \"null\"))\n      .execute()\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Write change data into a Delta table\n\nSimilar to SCD, another common use case, often called change data capture (CDC), is to apply all data changes generated from an external database into a Delta table. In other words, a set of updates, deletes, and inserts applied to an external table needs to be applied to a Delta table. You can do this using `merge` as follows.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n  \n    ```python\n    deltaTable = ... # DeltaTable with schema (key, value)\n\n    # DataFrame with changes having following columns\n    # - key: key of the change\n    # - time: time of change for ordering between changes (can replaced by other ordering id)\n    # - newValue: updated or inserted value if key was not deleted\n    # - deleted: true if the key was deleted, false if the key was inserted or updated\n    changesDF = spark.table(\"changes\")\n\n    # Find the latest change for each key based on the timestamp\n    # Note: For nested structs, max on struct is computed as\n    # max on first struct field, if equal fall back to second fields, and so on.\n    latestChangeForEachKey = changesDF \\\n      .selectExpr(\"key\", \"struct(time, newValue, deleted) as otherCols\") \\\n      .groupBy(\"key\") \\\n      .agg(max(\"otherCols\").alias(\"latest\")) \\\n      .select(\"key\", \"latest.*\") \\\n\n    deltaTable.alias(\"t\").merge(\n        latestChangeForEachKey.alias(\"s\"),\n        \"s.key = t.key\") \\\n      .whenMatchedDelete(condition = \"s.deleted = true\") \\\n      .whenMatchedUpdate(set = {\n        \"key\": \"s.key\",\n        \"value\": \"s.newValue\"\n      }) \\\n      .whenNotMatchedInsert(\n        condition = \"s.deleted = false\",\n        values = {\n          \"key\": \"s.key\",\n          \"value\": \"s.newValue\"\n        }\n      ).execute()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    val deltaTable: DeltaTable = ... // DeltaTable with schema (key, value)\n\n    // DataFrame with changes having following columns\n    // - key: key of the change\n    // - time: time of change for ordering between changes (can replaced by other ordering id)\n    // - newValue: updated or inserted value if key was not deleted\n    // - deleted: true if the key was deleted, false if the key was inserted or updated\n    val changesDF: DataFrame = ...\n\n    // Find the latest change for each key based on the timestamp\n    // Note: For nested structs, max on struct is computed as\n    // max on first struct field, if equal fall back to second fields, and so on.\n    val latestChangeForEachKey = changesDF\n      .selectExpr(\"key\", \"struct(time, newValue, deleted) as otherCols\" )\n      .groupBy(\"key\")\n      .agg(max(\"otherCols\").as(\"latest\"))\n      .selectExpr(\"key\", \"latest.*\")\n\n    deltaTable.as(\"t\")\n      .merge(\n        latestChangeForEachKey.as(\"s\"),\n        \"s.key = t.key\")\n      .whenMatched(\"s.deleted = true\")\n      .delete()\n      .whenMatched()\n      .updateExpr(Map(\"key\" -> \"s.key\", \"value\" -> \"s.newValue\"))\n      .whenNotMatched(\"s.deleted = false\")\n      .insertExpr(Map(\"key\" -> \"s.key\", \"value\" -> \"s.newValue\"))\n      .execute()\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Upsert from streaming queries using `foreachBatch`\n\nYou can use a combination of `merge` and `foreachBatch` (see [foreachbatch](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#foreachbatch) for more information) to write complex upserts from a streaming query into a Delta table. For example:\n\n- **Write streaming aggregates in Update Mode**: This is much more efficient than Complete Mode.\n\n  <Tabs syncKey=\"code-examples\">\n    <TabItem label=\"Python\">\n    \n      ```python\n      from delta.tables import *\n\n      deltaTable = DeltaTable.forPath(spark, \"/data/aggregates\")\n\n      # Function to upsert microBatchOutputDF into Delta table using merge\n      def upsertToDelta(microBatchOutputDF, batchId):\n        deltaTable.alias(\"t\").merge(\n            microBatchOutputDF.alias(\"s\"),\n            \"s.key = t.key\") \\\n          .whenMatchedUpdateAll() \\\n          .whenNotMatchedInsertAll() \\\n          .execute()\n      }\n\n      # Write the output of a streaming aggregation query into Delta table\n      streamingAggregatesDF.writeStream \\\n        .format(\"delta\") \\\n        .foreachBatch(upsertToDelta) \\\n        .outputMode(\"update\") \\\n        .start()\n      ```\n    \n    </TabItem>\n    <TabItem label=\"Scala\">\n    \n      ```scala\n      import io.delta.tables.*\n\n      val deltaTable = DeltaTable.forPath(spark, \"/data/aggregates\")\n\n      // Function to upsert microBatchOutputDF into Delta table using merge\n      def upsertToDelta(microBatchOutputDF: DataFrame, batchId: Long) {\n        deltaTable.as(\"t\")\n          .merge(\n            microBatchOutputDF.as(\"s\"),\n            \"s.key = t.key\")\n          .whenMatched().updateAll()\n          .whenNotMatched().insertAll()\n          .execute()\n      }\n\n      // Write the output of a streaming aggregation query into Delta table\n      streamingAggregatesDF.writeStream\n        .format(\"delta\")\n        .foreachBatch(upsertToDelta _)\n        .outputMode(\"update\")\n        .start()\n      ```\n    \n    </TabItem>\n  </Tabs>\n\n- **Write a stream of database changes into a Delta table**: The [merge query for writing change data](#write-change-data-into-a-delta-table) can be used in `foreachBatch` to continuously apply a stream of changes to a Delta table.\n- **Write a stream data into Delta table with deduplication**: The [insert-only merge query for deduplication](#data-deduplication-when-writing-into-delta-tables) can be used in `foreachBatch` to continuously write data (with duplicates) to a Delta table with automatic deduplication.\n\n<Aside type=\"note\">\n  - Make sure that your `merge` statement inside `foreachBatch` is idempotent as\n  restarts of the streaming query can apply the operation on the same batch of\n  data multiple times. - When `merge` is used in `foreachBatch`, the input data\n  rate of the streaming query (reported through `StreamingQueryProgress` and\n  visible in the notebook rate graph) may be reported as a multiple of the\n  actual rate at which data is generated at the source. This is because `merge`\n  reads the input data multiple times causing the input metrics to be\n  multiplied. If this is a bottleneck, you can cache the batch DataFrame before\n  `merge` and then uncache it after `merge`.\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/delta-utility/index.mdx",
    "content": "---\ntitle: Table utility commands\ndescription: Learn about Delta Lake utility commands.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nDelta tables support a number of utility commands.\n\nFor many Delta Lake operations, you enable integration with Apache Spark DataSourceV2 and Catalog APIs (since 3.0) by setting configurations when you create a new `SparkSession`. See [Configure SparkSession](/delta-batch/#configure-sparksession).\n\n## Remove files no longer referenced by a Delta table\n\nYou can remove files no longer referenced by a Delta table and are older than the retention threshold by running the `vacuum` command on the table. `vacuum` is not triggered automatically. The default retention threshold for the files is 7 days. To change this behavior, see [Data retention](/delta-batch/#data-retention).\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  - `vacuum` removes all files from directories not managed by Delta Lake,\n  ignoring directories beginning with `_`. If you are storing additional\n  metadata like Structured Streaming checkpoints within a Delta table directory,\n  use a directory name such as `_checkpoints`.\n  - `vacuum` deletes only data files, not log files. Log files are deleted automatically and asynchronously\n  after checkpoint operations. The default retention period of log files is 30\n  days, configurable through the `delta.logRetentionDuration` property which you\n  set with the `ALTER TABLE SET TBLPROPERTIES` SQL method. See [Table\n  properties](/delta-batch/#table-properties).\n  - The ability to [time travel](/delta-batch/#query-an-older-snapshot-of-a-table-time-travel) back to\n  a version older than the retention period is lost after running `vacuum`.\n</Aside>\n\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    VACUUM eventsTable   -- This runs VACUUM in ‘FULL’ mode and deletes data files outside of the retention duration and all files in the table directory not referenced by the table.\n\n    VACUUM eventsTable LITE   -- This VACUUM in ‘LITE’ mode runs faster.\n                              -- Instead of finding all files in the table directory, `VACUUM LITE` uses the Delta transaction log to identify and remove files no longer referenced by any table versions within the retention duration.\n                              -- If `VACUUM LITE` cannot be completed because the Delta log has been pruned a `DELTA_CANNOT_VACUUM_LITE` exception is raised.\n                              -- This mode is available only in Delta 3.3 and above.\n\n    VACUUM '/data/events' -- vacuum files in path-based table\n\n    VACUUM delta.`/data/events/`\n\n    VACUUM delta.`/data/events/` RETAIN 100 HOURS  -- vacuum files not required by versions more than 100 hours old\n\n    VACUUM eventsTable DRY RUN    -- do dry run to get the list of files to be deleted\n\n    VACUUM eventsTable USING INVENTORY inventoryTable  —- vacuum files based on a provided reservoir of files as a delta table\n\n    VACUUM eventsTable USING INVENTORY (select * from inventoryTable)  —- vacuum files based on a provided reservoir of files as spark SQL query\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import *\n\n    deltaTable = DeltaTable.forPath(spark, pathToTable)  # path-based tables, or\n    deltaTable = DeltaTable.forName(spark, tableName)    # Hive metastore-based tables\n\n    deltaTable.vacuum()        # vacuum files not required by versions older than the default retention period\n\n    deltaTable.vacuum(100)     # vacuum files not required by versions more than 100 hours old\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, pathToTable)\n\n    deltaTable.vacuum()        // vacuum files not required by versions older than the default retention period\n\n    deltaTable.vacuum(100)     // vacuum files not required by versions more than 100 hours old\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n    import org.apache.spark.sql.functions;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, pathToTable);\n\n    deltaTable.vacuum();        // vacuum files not required by versions older than the default retention period\n\n    deltaTable.vacuum(100);     // vacuum files not required by versions more than 100 hours old\n    ```\n  \n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  When using `VACUUM`, to configure Spark to delete files in parallel (based on\n  the number of shuffle partitions) set the session configuration\n  `\"spark.databricks.delta.vacuum.parallelDelete.enabled\"` to `\"true\"`.\n</Aside>\n\nSee the [Delta Lake APIs](/delta-apidoc/) for Scala, Java, and Python syntax details.\n\n<Aside type=\"caution\">\n  It is recommended that you set a retention interval to be at least 7 days,\n  because old snapshots and uncommitted files can still be in use by concurrent\n  readers or writers to the table. If `VACUUM` cleans up active files,\n  concurrent readers can fail or, worse, tables can be corrupted when `VACUUM`\n  deletes files that have not yet been committed. You must choose an interval\n  that is longer than the longest running concurrent transaction and the longest\n  period that any stream can lag behind the most recent update to the table.\n\n  Delta Lake has a safety check to prevent you from running a dangerous `VACUUM` command. If you are certain that there are no operations being performed on this table that take longer than the retention interval you plan to specify, you can turn off this safety check by setting the Spark configuration property `spark.databricks.delta.retentionDurationCheck.enabled` to `false`.\n</Aside>\n\n### Inventory Table\n\nAn inventory table contains a list of file paths together with their size, type (directory or not), and the last modification time. When an INVENTORY option is provided, VACUUM will consider the files listed there instead of doing the full listing of the table directory, which can be time consuming for very large tables. The inventory table can be specified as a delta table or a spark SQL query that gives the expected table schema. The schema should be as follows:\n\n| Column Name      | Type    | Description                             |\n| :--------------- | :------ | :-------------------------------------- |\n| path             | string  | fully qualified uri                     |\n| length           | integer | size in bytes                           |\n| isDir            | boolean | boolean indicating if it is a directory |\n| modificationTime | integer | file update time in milliseconds        |\n\n## Retrieve Delta table history\n\nYou can retrieve information on the operations, user, timestamp, and so on for each write to a Delta table by running the `history` command. The operations are returned in reverse chronological order. By default table history is retained for 30 days.\n\nSee [Configure SparkSession](/delta-batch/#configure-sparksession) for the steps to enable support for SQL commands in Apache Spark.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    DESCRIBE HISTORY '/data/events/'          -- get the full history of the table\n\n    DESCRIBE HISTORY delta.`/data/events/`\n\n    DESCRIBE HISTORY '/data/events/' LIMIT 1  -- get the last operation only\n\n    DESCRIBE HISTORY eventsTable\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import *\n\n    deltaTable = DeltaTable.forPath(spark, pathToTable)\n\n    fullHistoryDF = deltaTable.history()    # get the full history of the table\n\n    lastOperationDF = deltaTable.history(1) # get the last operation\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, pathToTable)\n\n    val fullHistoryDF = deltaTable.history()    // get the full history of the table\n\n    val lastOperationDF = deltaTable.history(1) // get the last operation\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, pathToTable);\n\n    DataFrame fullHistoryDF = deltaTable.history();       // get the full history of the table\n\n    DataFrame lastOperationDF = deltaTable.history(1);    // fetch the last operation on the DeltaTable\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee the [Delta Lake APIs](/delta-apidoc/) for Scala/Java/Python syntax details.\n\nThe output of the `history` operation has the following columns.\n\n| Column | Type | Description |\n| :-- | :-- | :-- |\n| version | long | Table version generated by the operation. |\n| timestamp | timestamp | When this version was committed. |\n| userId | string | ID of the user that ran the operation. |\n| userName | string | Name of the user that ran the operation. |\n| operation | string | Name of the operation. |\n| operationParameters | map | Parameters of the operation (for example, predicates.) |\n| job | struct | Details of the job that ran the operation. |\n| notebook | struct | Details of notebook from which the operation was run. |\n| clusterId | string | ID of the cluster on which the operation ran. |\n| readVersion | long | Version of the table that was read to perform the write operation. |\n| isolationLevel | string | Isolation level used for this operation. |\n| isBlindAppend | boolean | Whether this operation appended data. |\n| operationMetrics | map | Metrics of the operation (for example, number of rows and files modified.) |\n| userMetadata | string | User-defined commit metadata if it was specified |\n\n```\n+-------+-------------------+------+--------+---------+--------------------+----+--------+---------+-----------+--------------+-------------+--------------------+\n|version|          timestamp|userId|userName|operation| operationParameters| job|notebook|clusterId|readVersion|isolationLevel|isBlindAppend|    operationMetrics|\n+-------+-------------------+------+--------+---------+--------------------+----+--------+---------+-----------+--------------+-------------+--------------------+\n|      5|2019-07-29 14:07:47|  null|    null|   DELETE|[predicate -> [\"(...|null|    null|     null|          4|  Serializable|        false|[numTotalRows -> ...|\n|      4|2019-07-29 14:07:41|  null|    null|   UPDATE|[predicate -> (id...|null|    null|     null|          3|  Serializable|        false|[numTotalRows -> ...|\n|      3|2019-07-29 14:07:29|  null|    null|   DELETE|[predicate -> [\"(...|null|    null|     null|          2|  Serializable|        false|[numTotalRows -> ...|\n|      2|2019-07-29 14:06:56|  null|    null|   UPDATE|[predicate -> (id...|null|    null|     null|          1|  Serializable|        false|[numTotalRows -> ...|\n|      1|2019-07-29 14:04:31|  null|    null|   DELETE|[predicate -> [\"(...|null|    null|     null|          0|  Serializable|        false|[numTotalRows -> ...|\n|      0|2019-07-29 14:01:40|  null|    null|    WRITE|[mode -> ErrorIfE...|null|    null|     null|       null|  Serializable|         true|[numFiles -> 2, n...|\n+-------+-------------------+------+--------+---------+--------------------+----+--------+---------+-----------+--------------+-------------+--------------------+\n```\n\n<Aside type=\"note\">\n  Some of the columns may be nulls because the corresponding information may\n  not be available in your environment. - Columns added in the future will\n  always be added after the last column.\n</Aside>\n\nThe `history` operation returns a collection of operations metrics in the `operationMetrics` column map.\n\nThe following table lists the map key definitions by operation.\n\n| Operation | Metric name | Description |\n| :-- | :-- | :-- |\n| WRITE, CREATE TABLE AS SELECT, REPLACE TABLE AS SELECT, COPY INTO |  |  |\n|  | numFiles | Number of files written. |\n|  | numOutputBytes | Size in bytes of the written contents. |\n|  | numOutputRows | Number of rows written. |\n| STREAMING UPDATE |  |  |\n|  | numAddedFiles | Number of files added. |\n|  | numRemovedFiles | Number of files removed. |\n|  | numOutputRows | Number of rows written. |\n|  | numOutputBytes | Size of write in bytes. |\n| DELETE |  |  |\n|  | numAddedFiles | Number of files added. Not provided when partitions of the table are deleted. |\n|  | numRemovedFiles | Number of files removed. |\n|  | numDeletedRows | Number of rows removed. Not provided when partitions of the table are deleted. |\n|  | numCopiedRows | Number of rows copied in the process of deleting files. |\n|  | executionTimeMs | Time taken to execute the entire operation. |\n|  | scanTimeMs | Time taken to scan the files for matches. |\n|  | rewriteTimeMs | Time taken to rewrite the matched files. |\n| TRUNCATE |  |  |\n|  | numRemovedFiles | Number of files removed. |\n|  | executionTimeMs | Time taken to execute the entire operation. |\n| MERGE |  |  |\n|  | numSourceRows | Number of rows in the source DataFrame. |\n|  | numTargetRowsInserted | Number of rows inserted into the target table. |\n|  | numTargetRowsUpdated | Number of rows updated in the target table. |\n|  | numTargetRowsDeleted | Number of rows deleted in the target table. |\n|  | numTargetRowsCopied | Number of target rows copied. |\n|  | numOutputRows | Total number of rows written out. |\n|  | numTargetFilesAdded | Number of files added to the sink(target). |\n|  | numTargetFilesRemoved | Number of files removed from the sink(target). |\n|  | executionTimeMs | Time taken to execute the entire operation. |\n|  | scanTimeMs | Time taken to scan the files for matches. |\n|  | rewriteTimeMs | Time taken to rewrite the matched files. |\n| UPDATE |  |  |\n|  | numAddedFiles | Number of files added. |\n|  | numRemovedFiles | Number of files removed. |\n|  | numUpdatedRows | Number of rows updated. |\n|  | numCopiedRows | Number of rows just copied over in the process of updating files. |\n|  | executionTimeMs | Time taken to execute the entire operation. |\n|  | scanTimeMs | Time taken to scan the files for matches. |\n|  | rewriteTimeMs | Time taken to rewrite the matched files. |\n| FSCK | numRemovedFiles | Number of files removed. |\n| CONVERT | numConvertedFiles | Number of Parquet files that have been converted. |\n| OPTIMIZE |  |  |\n|  | numAddedFiles | Number of files added. |\n|  | numRemovedFiles | Number of files optimized. |\n|  | numAddedBytes | Number of bytes added after the table was optimized. |\n|  | numRemovedBytes | Number of bytes removed. |\n|  | minFileSize | Size of the smallest file after the table was optimized. |\n|  | p25FileSize | Size of the 25th percentile file after the table was optimized. |\n|  | p50FileSize | Median file size after the table was optimized. |\n|  | p75FileSize | Size of the 75th percentile file after the table was optimized. |\n|  | maxFileSize | Size of the largest file after the table was optimized. |\n| VACUUM |  |  |\n|  | numDeletedFiles | Number of deleted files. |\n|  | numVacuumedDirectories | Number of vacuumed directories. |\n|  | numFilesToDelete | Number of files to delete. |\n| RESTORE |  |  |\n|  | tableSizeAfterRestore | Table size in bytes after restore. |\n|  | numOfFilesAfterRestore | Number of files in the table after restore. |\n|  | numRemovedFiles | Number of files removed by the restore operation. |\n|  | numRestoredFiles | Number of files that were added as a result of the restore. |\n|  | removedFilesSize | Size in bytes of files removed by the restore. |\n|  | restoredFilesSize | Size in bytes of files added by the restore. |\n\n## Retrieve Delta table details\n\nYou can retrieve detailed information about a Delta table (for example, number of files, data size) using `DESCRIBE DETAIL`.\n\nSee [Configure SparkSession](/delta-batch/#configure-sparksession) for the steps to enable support for SQL commands in Apache Spark.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    DESCRIBE DETAIL '/data/events/'\n\n    DESCRIBE DETAIL eventsTable\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import *\n\n    deltaTable = DeltaTable.forPath(spark, pathToTable)\n\n    detailDF = deltaTable.detail()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, pathToTable)\n\n    val detailDF = deltaTable.detail()\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, pathToTable);\n\n    DataFrame detailDF = deltaTable.detail();\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee the [Delta Lake APIs](/delta-apidoc/) for Scala/Java/Python syntax details.\n\nThe output of this operation has only one row with the following schema.\n\n| Column | Type | Description |\n| :-- | :-- | :-- |\n| format | string | Format of the table, that is, `delta`. |\n| id | string | Unique ID of the table. |\n| name | string | Name of the table as defined in the metastore. |\n| description | string | Description of the table. |\n| location | string | Location of the table. |\n| createdAt | timestamp | When the table was created. |\n| lastModified | timestamp | When the table was last modified. |\n| partitionColumns | array of strings | Names of the partition columns if the table is partitioned. |\n| numFiles | long | Number of the files in the latest version of the table. |\n| sizeInBytes | int | The size of the latest snapshot of the table in bytes. |\n| properties | string-string map | All the properties set for this table. |\n| minReaderVersion | int | Minimum version of readers (according to the log protocol) that can read the table. |\n| minWriterVersion | int | Minimum version of writers (according to the log protocol) that can write to the table. |\n\n```\n+------+--------------------+------------------+-----------+--------------------+--------------------+-------------------+----------------+--------+-----------+----------+----------------+----------------+\n|format|                  id|              name|description|            location|           createdAt|       lastModified|partitionColumns|numFiles|sizeInBytes|properties|minReaderVersion|minWriterVersion|\n+------+--------------------+------------------+-----------+--------------------+--------------------+-------------------+----------------+--------+-----------+----------+----------------+----------------+\n| delta|d31f82d2-a69f-42e...|default.deltatable|       null|file:/Users/tuor/...|2020-06-05 12:20:...|2020-06-05 12:20:20|              []|      10|      12345|        []|               1|               2|\n+------+--------------------+------------------+-----------+--------------------+--------------------+-------------------+----------------+--------+-----------+----------+----------------+----------------+\n```\n\n## Generate a manifest file\n\nYou can a generate manifest file for a Delta table that can be used by other processing engines (that is, other than Apache Spark) to read the Delta table. For example, to generate a manifest file that can be used by Presto and Athena to read a Delta table, you run the following:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    GENERATE symlink_format_manifest FOR TABLE delta.`<path-to-delta-table>`\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    deltaTable = DeltaTable.forPath(<path-to-delta-table>)\n    deltaTable.generate(\"symlink_format_manifest\")\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    val deltaTable = DeltaTable.forPath(<path-to-delta-table>)\n    deltaTable.generate(\"symlink_format_manifest\")\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    DeltaTable deltaTable = DeltaTable.forPath(<path-to-delta-table>);\n    deltaTable.generate(\"symlink_format_manifest\");\n    ```\n  \n  </TabItem>\n</Tabs>\n\nSee [Configure SparkSession](/delta-batch/#configure-sparksession) for the steps to enable support for SQL commands in Apache Spark.\n\n## Convert a Parquet table to a Delta table\n\nConvert a Parquet table to a Delta table in-place. This command lists all the files in the directory, creates a Delta Lake transaction log that tracks these files, and automatically infers the data schema by reading the footers of all Parquet files. If your data is partitioned, you must specify the schema of the partition columns as a DDL-formatted string (that is, `<column-name1> <type>, <column-name2> <type>, ...`).\n\nBy default, this command will collect per-file statistics (e.g. minimum and maximum values for each column). These statistics will be used at query time to provide faster queries. You can disable this statistics collection in the SQL API using `NO STATISTICS`.\n\n<Aside type=\"note\">\n  If a Parquet table was created by Structured Streaming, the listing of files\n  can be avoided by using the `_spark_metadata` sub-directory as the source of\n  truth for files contained in the table setting the SQL configuration\n  `spark.databricks.delta.convert.useMetadataLog` to `true`.\n</Aside>\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    -- Convert unpartitioned Parquet table at path '<path-to-table>'\n    CONVERT TO DELTA parquet.`<path-to-table>`\n\n    -- Convert unpartitioned Parquet table and disable statistics collection\n    CONVERT TO DELTA parquet.`<path-to-table>` NO STATISTICS\n\n    -- Convert partitioned Parquet table at path '<path-to-table>' and partitioned by integer columns named 'part' and 'part2'\n    CONVERT TO DELTA parquet.`<path-to-table>` PARTITIONED BY (part int, part2 int)\n\n    -- Convert partitioned Parquet table and disable statistics collection\n    CONVERT TO DELTA parquet.`<path-to-table>` NO STATISTICS PARTITIONED BY (part int, part2 int)\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import *\n\n    # Convert unpartitioned Parquet table at path '<path-to-table>'\n    deltaTable = DeltaTable.convertToDelta(spark, \"parquet.`<path-to-table>`\")\n\n    # Convert partitioned parquet table at path '<path-to-table>' and partitioned by integer column named 'part'\n    partitionedDeltaTable = DeltaTable.convertToDelta(spark, \"parquet.`<path-to-table>`\", \"part int\")\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    // Convert unpartitioned Parquet table at path '<path-to-table>'\n    val deltaTable = DeltaTable.convertToDelta(spark, \"parquet.`<path-to-table>`\")\n\n    // Convert partitioned Parquet table at path '<path-to-table>' and partitioned by integer columns named 'part' and 'part2'\n    val partitionedDeltaTable = DeltaTable.convertToDelta(spark, \"parquet.`<path-to-table>`\", \"part int, part2 int\")\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n\n    // Convert unpartitioned Parquet table at path '<path-to-table>'\n    DeltaTable deltaTable = DeltaTable.convertToDelta(spark, \"parquet.`<path-to-table>`\");\n\n    // Convert partitioned Parquet table at path '<path-to-table>' and partitioned by integer columns named 'part' and 'part2'\n    DeltaTable deltaTable = DeltaTable.convertToDelta(spark, \"parquet.`<path-to-table>`\", \"part int, part2 int\");\n    ```\n  \n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  Any file not tracked by Delta Lake is invisible and can be deleted when you\n  run `vacuum`. You should avoid updating or appending data files during the\n  conversion process. After the table is converted, make sure all writes go\n  through Delta Lake.\n</Aside>\n\n## Convert an Iceberg table to a Delta table\n\n<Aside type=\"note\">It is available from Delta Lake 2.3 and above.</Aside>\n\nYou can convert an Iceberg table to a Delta table in place if the underlying file format of the Iceberg table is Parquet. Similar to a conversion from a Parquet table, the conversion is in-place and there won't be any data copy or data rewrite. The original Iceberg table and the converted Delta table have separate history, so modifying the Delta table should not affect the Iceberg table as long as the source data Parquet files are not touched or deleted.\n\nThe following command creates a Delta Lake transaction log based on the Iceberg table's native file manifest, schema and partitioning information. The converter also collects column stats during the conversion, unless `NO STATISTICS` is specified.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    -- Convert the Iceberg table in the path <path-to-table>.\n    CONVERT TO DELTA iceberg.\\`<path-to-table>\\`\n\n    -- Convert the Iceberg table in the path <path-to-table> without collecting statistics.\n    CONVERT TO DELTA iceberg.\\`<path-to-table>\\` NO STATISTICS\n    ```\n  \n  </TabItem>\n</Tabs>\n\n<Aside type=\"caution\" title=\"Important\">\n  An additional jar `delta-iceberg` is needed to use the converter. For example, `bin/spark-sql --packages io.delta:delta-spark_2.12:3.0.0,io.delta:delta-iceberg_2.12:3.0.0:...`.\n\n  `delta-iceberg` is currently not available for the Delta Lake 2.4.0 release since `iceberg-spark-runtime` does not support Spark 3.4 yet. It is available for Delta Lake 2.3.0.\n</Aside>\n\n<Aside type=\"note\">\n  Converting Iceberg metastore tables is not supported. - Converting Iceberg\n  tables that have experienced [partition\n  evolution](https://iceberg.apache.org/docs/latest/evolution/#partition-evolution)\n  is not supported. - Converting Iceberg merge-on-read tables that have\n  experienced updates, deletions, or merges is not supported.\n</Aside>\n\n## Convert a Delta table to a Parquet table\n\nYou can easily convert a Delta table back to a Parquet table using the following steps:\n\n1. If you have performed Delta Lake operations that can change the data files (for example, `delete` or `merge`), run [vacuum](#remove-files-no-longer-referenced-by-a-delta-table) ) with retention of 0 hours to delete all data files that do not belong to the latest version of the table.\n2. Delete the `_delta_log` directory in the table directory.\n\n## Restore a Delta table to an earlier state\n\nYou can restore a Delta table to its earlier state by using the `RESTORE` command. A Delta table internally maintains historic versions of the table that enable it to be restored to an earlier state. A version corresponding to the earlier state or a timestamp of when the earlier state was created are supported as options by the `RESTORE` command.\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  - You can restore an already restored table.\n  - Restoring a table to an older\n  version where the data files were deleted manually or by `vacuum` will fail.\n  Restoring to this version partially is still possible if\n  `spark.sql.files.ignoreMissingFiles` is set to `true`.\n  - The timestamp format\n  for restoring to an earlier state is `yyyy-MM-dd HH:mm:ss`. Providing only a\n  date(`yyyy-MM-dd`) string is also supported.\n</Aside>\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    RESTORE TABLE db.target_table TO VERSION AS OF <version>\n    RESTORE TABLE delta.`/data/target/` TO TIMESTAMP AS OF <timestamp>\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n  \n    ```python\n    from delta.tables import *\n\n    deltaTable = DeltaTable.forPath(spark, <path-to-table>)  # path-based tables, or\n    deltaTable = DeltaTable.forName(spark, <table-name>)    # Hive metastore-based tables\n\n    deltaTable.restoreToVersion(0) # restore table to oldest version\n\n    deltaTable.restoreToTimestamp('2019-02-14') # restore to a specific timestamp\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Scala\">\n  \n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, <path-to-table>)\n    val deltaTable = DeltaTable.forName(spark, <table-name>)\n\n    deltaTable.restoreToVersion(0) // restore table to oldest version\n\n    deltaTable.restoreToTimestamp(\"2019-02-14\") // restore to a specific timestamp\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Java\">\n  \n    ```java\n    import io.delta.tables.*;\n\n    DeltaTable deltaTable = DeltaTable.forPath(spark, <path-to-table>);\n    DeltaTable deltaTable = DeltaTable.forName(spark, <table-name>);\n\n    deltaTable.restoreToVersion(0) // restore table to oldest version\n\n    deltaTable.restoreToTimestamp(\"2019-02-14\") // restore to a specific timestamp\n    ```\n  \n  </TabItem>\n</Tabs>\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  Restore is considered a data-changing operation. Delta Lake log entries added\n  by the `RESTORE` command contain\n  [dataChange](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#add-file-and-remove-file)\n  set to true. If there is a downstream application, such as a [Structured\n  streaming](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html)\n  job that processes the updates to a Delta Lake table, the data change log\n  entries added by the restore operation are considered as new data updates, and\n  processing them may result in duplicate data.\n</Aside>\nFor example:\n\n| Table version | Operation | Delta log updates | Records in data change log updates |\n| :-- | :-- | :-- | :-- |\n| 0 | INSERT | AddFile(/path/to/file-1, dataChange = true) | (name = Viktor, age = 29), (name = George, age = 55) |\n| 1 | INSERT | AddFile(/path/to/file-2, dataChange = true) | (name = George, age = 39) |\n| 2 | OPTIMIZE | AddFile(/path/to/file-3, dataChange = false), RemoveFile(/path/to/file-1), RemoveFile(/path/to/file-2) | (No records as Optimize compaction does not change the data in the table) |\n| 3 | RESTORE(version=1) | RemoveFile(/path/to/file-3), AddFile(/path/to/file-1, dataChange = true), AddFile(/path/to/file-2, dataChange = true) | (name = Viktor, age = 29), (name = George, age = 55), (name = George, age = 39) |\n\nIn the preceding example, the `RESTORE` command results in updates that were already seen when reading the Delta table version 0 and 1. If a streaming query was reading this table, then these files will be considered as newly added data and will be processed again.\n\n`RESTORE` reports the following metrics as a single row DataFrame once the operation is complete:\n\n- `table_size_after_restore`: The size of the table after restoring.\n- `num_of_files_after_restore`: The number of files in the table after restoring.\n- `num_removed_files`: Number of files removed (logically deleted) from the table.\n- `num_restored_files`: Number of files restored due to rolling back.\n- `removed_files_size`: Total size in bytes of the files that are removed from the table.\n- `restored_files_size`: Total size in bytes of the files that are restored.\n\n![Restore metrics example](./restore-metrics.png)\n\n## Shallow clone a Delta table\n\n<Aside type=\"note\">It is available from Delta Lake 2.3 and above.</Aside>\n\nYou can create a shallow copy of an existing Delta table at a specific version using the `shallow clone` command.\n\nAny changes made to shallow clones affect only the clones themselves and not the source table, as long as they don't touch the source data Parquet files.\n\nThe metadata that is cloned includes: schema, partitioning information, invariants, nullability. For shallow clones, stream metadata is not cloned. Metadata not cloned are the table description and [user-defined commit metadata](/delta-batch/#set-user-defined-commit-metadata).\n\n<Aside\n  type=\"caution\"\n  title=\"Important\"\n>\n  - Shallow clones reference data files in the source directory. If you run\n  `vacuum` on the source table, clients will no longer be able to read the\n  referenced data files and a `FileNotFoundException` will be thrown. In this\n  case, running clone with `replace` over the shallow clone will repair the\n  clone.\n  - If a target already has a non-Delta table at that path, cloning with\n  `replace` to that target will create a Delta log. Then, you can clean up any\n  existing data by running `vacuum`.\n  - If a Delta table exists in the target\n  path, a new commit is created that includes the new metadata and new data from\n  the source table. In the case of `replace`, the target table needs to be\n  emptied first to avoid data duplication.\n  - Cloning a table is not the same as\n  `Create Table As Select` or `CTAS`. A shallow clone takes the metadata of the\n  source table. Cloning also has simpler syntax: you don't need to specify\n  partitioning, format, invariants, nullability and so on as they are taken from\n  the source table.\n  - A cloned table has an independent history from its source\n  table. Time travel queries on a cloned table will not work with the same\n  inputs as they work on its source table. For example, if the source table was\n  at version 100 and we are creating a new table by cloning it, the new table\n  will have version 0, and therefore we could not run time travel queries on the\n  new table such as `SELECT * FROM tbl AS OF VERSION 99`.\n</Aside>\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    CREATE TABLE delta.`/data/target/` SHALLOW CLONE delta.`/data/source/` -- Create a shallow clone of /data/source at /data/target\n\n    CREATE OR REPLACE TABLE db.target_table SHALLOW CLONE db.source_table -- Replace the target. target needs to be emptied\n\n    CREATE TABLE IF NOT EXISTS delta.`/data/target/` SHALLOW CLONE db.source_table -- No-op if the target table exists\n\n    CREATE TABLE db.target_table SHALLOW CLONE delta.`/data/source`\n\n    CREATE TABLE db.target_table SHALLOW CLONE delta.`/data/source` VERSION AS OF version\n\n    CREATE TABLE db.target_table SHALLOW CLONE delta.`/data/source` TIMESTAMP AS OF timestamp_expression -- timestamp can be like “2019-01-01” or like date_sub(current_date(), 1)\n    ```\n  \n  </TabItem>\n</Tabs>\n\n`CLONE` reports the following metrics as a single row DataFrame once the operation is complete:\n\n- `source_table_size`: Size of the source table that's being cloned in bytes.\n- `source_num_of_files`: The number of files in the source table.\n\n### Cloud provider permissions\n\nIf you have created a shallow clone, any user that reads the shallow clone needs permission to read the files in the original table, since the data files remain in the source table's directory where we cloned from. To make changes to the clone, users will need write access to the clone's directory.\n\n#### Clone use cases\n\n### Machine learning flow reproduction\n\nWhen doing machine learning, you may want to archive a certain version of a table on which you trained an ML model. Future models can be tested using this archived data set.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    -- Trained model on version 15 of Delta table\n    CREATE TABLE delta.`/model/dataset` SHALLOW CLONE entire_dataset VERSION AS OF 15\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Short-term experiments on a production table\n\nTo test a workflow on a production table without corrupting the table, you can easily create a shallow clone. This allows you to run arbitrary workflows on the cloned table that contains all the production data but does not affect any production workloads.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    -- Perform shallow clone\n    CREATE OR REPLACE TABLE my_test SHALLOW CLONE my_prod_table;\n\n    UPDATE my_test WHERE user_id is null SET invalid=true;\n    -- Run a bunch of validations. Once happy:\n\n    -- This should leverage the update information in the clone to prune to only\n    -- changed files in the clone if possible\n    MERGE INTO my_prod_table\n    USING my_test\n    ON my_test.user_id <=> my_prod_table.user_id\n    WHEN MATCHED AND my_test.user_id is null THEN UPDATE *;\n\n    DROP TABLE my_test;\n    ```\n  \n  </TabItem>\n</Tabs>\n\n### Table property overrides\n\nTable property overrides are particularly useful for:\n\n- Annotating tables with owner or user information when sharing data with different business units.\n- Archiving Delta tables and time travel is required. You can specify the log retention period independently for the archive table. For example:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    CREATE OR REPLACE TABLE archive.my_table SHALLOW CLONE prod.my_table\n    TBLPROPERTIES (\n      delta.logRetentionDuration = '3650 days',\n      delta.deletedFileRetentionDuration = '3650 days'\n    )\n    LOCATION 'xx://archive/my_table'\n    ```\n  \n  </TabItem>\n</Tabs>\n\n## Clone Parquet or Iceberg table to Delta\n\n<Aside type=\"note\">It is available from Delta Lake 2.3 and above.</Aside>\n\nShallow clone for Parquet and Iceberg combines functionality used to clone Delta tables and convert tables to Delta Lake, you can use clone functionality to convert data from Parquet or Iceberg data sources to managed or external Delta tables with the same basic syntax.\n\n`replace` has the same limitation as Delta shallow clone, the target table must be emptied before applying replace.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    CREATE OR REPLACE TABLE <target_table_name> SHALLOW CLONE parquet.`/path/to/data`;\n\n    CREATE OR REPLACE TABLE <target_table_name> SHALLOW CLONE iceberg.`/path/to/data`;\n    ```\n  \n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content/docs/flink-integration.mdx",
    "content": "---\ntitle: Apache Flink connector\ndescription: Learn how to set up an integration to enable you to write Delta tables from Apache Flink.\n---\n\nThis integration enables reading from and writing to Delta tables from Apache Flink. For details on using the Flink/Delta Connector, see the [Delta Lake repository](https://github.com/delta-io/delta/tree/master/connectors/flink).\n"
  },
  {
    "path": "docs/src/content/docs/hive-integration.mdx",
    "content": "---\ntitle: Apache Hive\ndescription: Learn how to set up an integration to enable you to read Delta tables from <Hive>.\n---\n\nPage moved to [Apache Hive](/delta-more-connectors#apache-hive)"
  },
  {
    "path": "docs/src/content/docs/index.md",
    "content": "---\ntitle: Welcome to the Delta Lake documentation\ndescription: Learn how to use Delta Lake\nsidebar:\n  label: Welcome\n---\n\n[Delta Lake](https://delta.io) is an [open source project](https://github.com/delta-io/delta) that enables building a [Lakehouse architecture](https://www.databricks.com/blog/2020/01/30/what-is-a-data-lakehouse.html) on top of [data lakes](https://www.databricks.com/discover/data-lakes). Delta Lake provides [ACID transactions](/concurrency-control), scalable metadata handling, and unifies [streaming](/delta-streaming) and [batch](/delta-batch) data processing on top of existing data lakes, such as S3, ADLS, GCS, and HDFS.\n\nSpecifically, Delta Lake offers:\n\n- [ACID transactions](/concurrency-control) on Spark: Serializable isolation levels ensure that readers never see inconsistent data.\n- Scalable metadata handling: Leverages Spark distributed processing power to handle all the metadata for petabyte-scale tables with billions of files at ease.\n- [Streaming](/delta-streaming) and [batch](/delta-batch) unification: A table in Delta Lake is a batch table as well as a streaming source and sink. Streaming data ingest, batch historic backfill, interactive queries all just work out of the box.\n- Schema enforcement: Automatically handles schema variations to prevent insertion of bad records during ingestion.\n- [Time travel](/delta-batch#query-an-older-snapshot-of-a-table-time-travel): Data versioning enables rollbacks, full historical audit trails, and reproducible machine learning experiments.\n- [Upserts](/delta-update#upsert-into-a-table-using-merge) and [deletes](/delta-update#delete-from-a-table): Supports merge, update and delete operations to enable complex use cases like change-data-capture, slowly-changing-dimension (SCD) operations, streaming upserts, and so on.\n- Vibrant connector ecosystem: Delta Lake has connectors read and write Delta tables from various data processing engines like Apache Spark, Apache Flink, Apache Hive, Apache Trino, AWS Athena, and more.\n\nTo get started follow the [quickstart guide](/quick-start) to learn how to use Delta Lake with Apache Spark.\n"
  },
  {
    "path": "docs/src/content/docs/integrations.mdx",
    "content": "---\ntitle: Integrations\ndescription: Learn how to access Delta tables from external data processing engines.\n---\n\nThis page is moved to [Welcome to the Delta Lake documentation](/).\n\n"
  },
  {
    "path": "docs/src/content/docs/optimizations-oss/index.mdx",
    "content": "---\ntitle: Optimizations\ndescription: Learn about the optimizations available with Delta Lake.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\nimport { Image } from \"astro:assets\";\n\n## Optimize performance with file management\n\nTo improve query speed, Delta Lake supports the ability to optimize the layout of data in storage. There are various ways to optimize the layout.\n\n### Compaction (bin-packing)\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 1.2.0 and above.\n</Aside>\n\nDelta Lake can improve the speed of read queries from a table by coalescing small files into larger ones.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    OPTIMIZE '/path/to/delta/table' -- Optimizes the path-based Delta Lake table\n\n    OPTIMIZE delta_table_name;\n\n    OPTIMIZE delta.`/path/to/delta/table`;\n\n    -- If you have a large amount of data and only want to optimize a subset of it, you can specify an optional partition predicate using `WHERE`:\n    OPTIMIZE delta_table_name WHERE date >= '2017-01-01'\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    from delta.tables import *\n\n    deltaTable = DeltaTable.forPath(spark, pathToTable)  # For path-based tables\n    # For Hive metastore-based tables: deltaTable = DeltaTable.forName(spark, tableName)\n\n    deltaTable.optimize().executeCompaction()\n\n    # If you have a large amount of data and only want to optimize a subset of it, you can specify an optional partition predicate using `where`\n    deltaTable.optimize().where(\"date='2021-11-18'\").executeCompaction()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, pathToTable)  // For path-based tables\n    // For Hive metastore-based tables: val deltaTable = DeltaTable.forName(spark, tableName)\n\n    deltaTable.optimize().executeCompaction()\n\n    // If you have a large amount of data and only want to optimize a subset of it, you can specify an optional partition predicate using `where`\n    deltaTable.optimize().where(\"date='2021-11-18'\").executeCompaction()\n    ```\n\n  </TabItem>\n</Tabs>\n\nFor Scala, Java, and Python API syntax details, see the [Delta Lake APIs](/delta-apidoc/).\n\n<Aside type=\"note\">\n  - Bin-packing optimization is *idempotent*, meaning that if it is run twice on\n  the same dataset, the second run has no effect. - Bin-packing aims to produce\n  evenly-balanced data files with respect to their size on disk, but not\n  necessarily number of tuples per file. However, the two measures are most\n  often correlated. - Python and Scala APIs for executing `OPTIMIZE` operation\n  are available from Delta Lake 2.0 and above. - Set Spark session configuration\n  `spark.databricks.delta.optimize.repartition.enabled=true` to use\n  `repartition(1)` instead of `coalesce(1)` for better performance when\n  compacting many small files.\n</Aside>\n\nReaders of Delta tables use snapshot isolation, which means that they are not interrupted when `OPTIMIZE` removes unnecessary files from the transaction log. `OPTIMIZE` makes no data related changes to the table, so a read before and after an `OPTIMIZE` has the same results. Performing `OPTIMIZE` on a table that is a streaming source does not affect any current or future streams that treat this table as a source. `OPTIMIZE` returns the file statistics (min, max, total, and so on) for the files removed and the files added by the operation. Optimize stats also contains the number of batches, and partitions optimized.\n\n## Auto compaction\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 3.1.0 and above.\n</Aside>\n\nAuto compaction combines small files within Delta table partitions to automatically reduce small file problems. Auto compaction occurs after a write to a table has succeeded and runs synchronously on the cluster that has performed the write. Auto compaction only compacts files that haven't been compacted previously.\n\nYou can control the output file size by setting the configuration `spark.databricks.delta.autoCompact.maxFileSize`.\n\nAuto compaction is only triggered for partitions or tables that have at least a certain number of small files. You can optionally change the minimum number of files required to trigger auto compaction by setting `spark.databricks.delta.autoCompact.minNumFiles`.\n\nAuto compaction can be enabled at the table or session level using the following settings:\n\n- Table property: `delta.autoOptimize.autoCompact`\n- SparkSession setting: `spark.databricks.delta.autoCompact.enabled`\n\nThese settings accept the following options:\n\n| Options | Behavior |\n| --- | --- |\n| `true` | Enable auto compaction. By default will use 128 MB as the target file size. |\n| `false` | Turns off auto compaction. Can be set at the session level to override auto compaction for all Delta tables modified in the workload. |\n\n## Data skipping\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 1.2.0 and above.\n</Aside>\n\nData skipping information is collected automatically when you write data into a Delta Lake table. Delta Lake takes advantage of this information (minimum and maximum values for each column) at query time to provide faster queries. You do not need to configure data skipping; the feature is activated whenever applicable. However, its effectiveness depends on the layout of your data. For best results, apply [Z-Ordering](#z-ordering-multi-dimensional-clustering).\n\nCollecting statistics on a column containing long values such as `string` or `binary` is an expensive operation. To avoid collecting statistics on such columns you can configure the [table property](/delta-batch/#table-properties) `delta.dataSkippingNumIndexedCols`. This property indicates the position index of a column in the table's schema. All columns with a position index less than the `delta.dataSkippingNumIndexedCols` property will have statistics collected. For the purposes of collecting statistics, each field within a nested column is considered as an individual column. To avoid collecting statistics on columns containing long values, either set the `delta.dataSkippingNumIndexedCols` property so that the long value columns are after this index in the table's schema, or move columns containing long strings to an index position greater than the `delta.dataSkippingNumIndexedCols` property by using [ALTER TABLE ALTER COLUMN](https://spark.apache.org/docs/latest/sql-ref-syntax-ddl-alter-table.html#alter-or-change-column).\n\n## Z-Ordering (multi-dimensional clustering)\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 2.0.0 and above.\n</Aside>\n\nZ-Ordering is a [technique](https://en.wikipedia.org/wiki/Z-order_curve) to colocate related information in the same set of files. This co-locality is automatically used by Delta Lake in data-skipping algorithms. This behavior dramatically reduces the amount of data that Delta Lake on Apache Spark needs to read. To Z-Order data, you specify the columns to order on in the `ZORDER BY` clause:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    OPTIMIZE events ZORDER BY (eventType)\n\n    -- If you have a large amount of data and only want to optimize a subset of it, you can specify an optional partition predicate by using \"where\".\n    OPTIMIZE events WHERE date = '2021-11-18' ZORDER BY (eventType)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Python\">\n    ```python\n    from delta.tables import *\n\n    deltaTable = DeltaTable.forPath(spark, pathToTable)  # path-based table\n    # For Hive metastore-based tables: deltaTable = DeltaTable.forName(spark, tableName)\n\n    deltaTable.optimize().executeZOrderBy(eventType)\n\n    # If you have a large amount of data and only want to optimize a subset of it, you can specify an optional partition predicate using `where`\n    deltaTable.optimize().where(\"date='2021-11-18'\").executeZOrderBy(eventType)\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n    ```scala\n    import io.delta.tables._\n\n    val deltaTable = DeltaTable.forPath(spark, pathToTable)  // path-based table\n    // For Hive metastore-based tables: val deltaTable = DeltaTable.forName(spark, tableName)\n\n    deltaTable.optimize().executeZOrderBy(eventType)\n\n    // If you have a large amount of data and only want to optimize a subset of it, you can specify an optional partition predicate by using \"where\".\n    deltaTable.optimize().where(\"date='2021-11-18'\").executeZOrderBy(eventType)\n    ```\n\n  </TabItem>\n</Tabs>\n\nFor Scala, Java, and Python API syntax details, see the [Delta Lake APIs](/delta-apidoc/)\n\nIf you expect a column to be commonly used in query predicates and if that column has high cardinality (that is, a large number of distinct values), then use `ZORDER BY`.\n\nYou can specify multiple columns for `ZORDER BY` as a comma-separated list. However, the effectiveness of the locality drops with each extra column. Z-Ordering on columns that do not have statistics collected on them would be ineffective and a waste of resources. This is because data skipping requires column-local stats such as min, max, and count. You can configure statistics collection on certain columns by reordering columns in the schema, or you can increase the number of columns to collect statistics on. See [Data skipping](#data-skipping).\n\n<Aside type=\"note\">\n- Z-Ordering is *not idempotent*. Everytime the Z-Ordering is executed it will try to create a new clustering of data in all files (new and existing files that were part of previous Z-Ordering) in a partition.\n\n- Z-Ordering aims to produce evenly-balanced data files with respect to the number of tuples, but not necessarily data size on disk. The two measures are most often correlated, but there can be situations when that is not the case, leading to skew in optimize task times.\n\nFor example, if you `ZORDER BY` _date_ and your most recent records are all much wider (for example longer arrays or string values) than the ones in the past, it is expected that the `OPTIMIZE` job's task durations will be skewed, as well as the resulting file sizes. This is, however, only a problem for the `OPTIMIZE` command itself; it should not have any negative impact on subsequent queries.\n\n</Aside>\n\n## Multi-part checkpointing\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 2.0.0 and above. This feature is in\n  experimental support mode.\n</Aside>\n\nDelta Lake table periodically and automatically compacts all the incremental updates to the Delta log into a Parquet file. This \"checkpointing\" allows read queries to quickly reconstruct the current state of the table (that is, which files to process, what is the current schema) without reading too many files having incremental updates.\n\nDelta Lake protocol allows [splitting the checkpoint](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#checkpoints) into multiple Parquet files. This parallelizes and speeds up writing the checkpoint. In Delta Lake, by default each checkpoint is written as a single Parquet file. To to use this feature, set the SQL configuration `spark.databricks.delta.checkpoint.partSize=<n>`, where `n` is the limit of number of actions (such as `AddFile`) at which Delta Lake on Apache Spark will start parallelizing the checkpoint and attempt to write a maximum of this many actions per checkpoint file.\n\n<Aside type=\"note\">\n  This feature requires no reader side configuration changes. The existing\n  reader already supports reading a checkpoint with multiple files.\n</Aside>\n\n## Log compactions\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 3.0.0 and above.\n</Aside>\n\nDelta Lake protocol allows new log compaction files with the format `<x>.<y>.compact.json`. These files contain the aggregated actions for commit range `[x, y]`. Log compactions reduce the need for frequent checkpoints and minimize the latency spikes caused by them.\n\nThe read support for the log compaction files is available in Delta Lake 3.0.0 and above. It is enabled by default and can be disabled using the SQL conf `spark.databricks.delta.deltaLog.minorCompaction.useForReads=<value>` where `value` can be `true/false`. The write support for the log compaction will be added in a future version of Delta.\n\n## Optimized Write\n\n<Aside type=\"note\">\n  This feature is available in Delta Lake 3.1.0 and above.\n</Aside>\n\nOptimized writes improve file size as data is written and benefit subsequent reads on the table.\n\nOptimized writes are most effective for partitioned tables, as they reduce the number of small files written to each partition. Writing fewer large files is more efficient than writing many small files, but you might still see an increase in write latency because data is shuffled before being written.\n\nThe following image demonstrates how optimized writes works:\n\n![Optimized writes](./optimized-writes.png)\n\n<Aside type=\"note\">\n  You might have code that runs coalesce(n) or repartition(n) just before you\n  write out your data to control the number of files written. Optimized writes\n  eliminates the need to use this pattern.\n</Aside>\n\nThe optimized write feature is **disabled** by default. It can be enabled at the table, SQL session, and/or DataFrameWriter level using the following settings (in order of precedence from low to high):\n\n- The `delta.autoOptimize.optimizeWrite` table property (default=None);\n- The `spark.databricks.delta.optimizeWrite.enabled` SQL configuration (default=None);\n- The DataFrameWriter option `optimizeWrite` (default=None).\n\nBesides the above, the following advanced SQL configurations can be used to further fine-tune the number and size of files written:\n\n- `spark.databricks.delta.optimizeWrite.binSize` (default=512MiB), which controls the target in-memory size of each output file;\n- `spark.databricks.delta.optimizeWrite.numShuffleBlocks` (default=50,000,000), which controls \"maximum number of shuffle blocks to target\";\n- `spark.databricks.delta.optimizeWrite.maxShufflePartitions` (default=2,000), which controls \"max number of output buckets (reducers) that can be used by optimized writes\".\n"
  },
  {
    "path": "docs/src/content/docs/porting.mdx",
    "content": "---\ntitle: Migration guide\ndescription: Learn how to migrate existing workloads to Delta Lake.\n---\n\nimport { Tabs, TabItem, Aside } from \"@astrojs/starlight/components\";\n\n## Migrate workloads to Delta Lake\n\nWhen you migrate workloads to Delta Lake, you should be aware of the following simplifications and differences compared with the data sources provided by Apache Spark and Apache Hive.\n\nDelta Lake handles the following operations automatically, which you should never perform manually:\n\n- **Add and remove partitions**: Delta Lake automatically tracks the set of partitions present in a table and updates the list as data is added or removed. As a result, there is no need to run `ALTER TABLE [ADD|DROP] PARTITION` or `MSCK`.\n- **Load a single partition**: As an optimization, you may sometimes directly load the partition of data you are interested in. For example, `spark.read.format(\"parquet\").load(\"/data/date=2017-01-01\")`. This is unnecessary with Delta Lake, since it can quickly read the list of files from the transaction log to find the relevant ones. If you are interested in a single partition, specify it using a `WHERE` clause. For example, `spark.read.delta(\"/data\").where(\"date = '2017-01-01'\")`. For large tables with many files in the partition, this can be much faster than loading a single partition (with direct partition path, or with `WHERE`) from a Parquet table because listing the files in the directory is often slower than reading the list of files from the transaction log.\n\nWhen you port an existing application to Delta Lake, you should avoid the following operations, which bypass the transaction log:\n\n- **Manually modify data**: Delta Lake uses the transaction log to atomically commit changes to the table. Because the log is the source of truth, files that are written out but not added to the transaction log are not read by Spark. Similarly, even if you manually delete a file, a pointer to the file is still present in the transaction log. Instead of manually modifying files stored in a Delta table, always use the commands that are described in this guide.\n- **External readers**: Directly reading the data stored in Delta Lake. For information on how to read Delta tables, see [read a table](/delta-batch/#read-a-table).\n\n### Example\n\nSuppose you have Parquet data stored in a directory named `/data-pipeline`, and you want to create a Delta table named `events`.\n\nThe [first example](#save-as-delta-table) shows how to:\n\n- Read the Parquet data from its original location, `/data-pipeline`, into a DataFrame.\n- Save the DataFrame's contents in Delta format in a separate location, `/tmp/delta/data-pipeline/`.\n- Create the `events` table based on that separate location, `/tmp/delta/data-pipeline/`.\n\nThe [second example](#convert-to-delta-table) shows how to use `CONVERT TO TABLE` to convert data from Parquet to Delta format without changing its original location, `/data-pipeline/`.\n\n#### Save as Delta table\n\n1. Read the Parquet data into a DataFrame and then save the DataFrame's contents to a new directory in `delta` format:\n\n   ```python\n   data = spark.read.format(\"parquet\").load(\"/data-pipeline\")\n   data.write.format(\"delta\").save(\"/tmp/delta/data-pipeline/\")\n   ```\n\n2. Create a Delta table named `events` that refers to the files in the new directory:\n\n   ```python\n   spark.sql(\"CREATE TABLE events USING DELTA LOCATION '/tmp/delta/data-pipeline/'\")\n   ```\n\n#### Convert to Delta table\n\nYou have two options for converting a Parquet table to a Delta table:\n\n- Convert files to Delta Lake format and then create a Delta table:\n\n  ```sql\n  CONVERT TO DELTA parquet.`/data-pipeline/`\n  CREATE TABLE events USING DELTA LOCATION '/data-pipeline/'\n  ```\n\n- Create a Parquet table and then convert it to a Delta table:\n\n  ```sql\n  CREATE TABLE events USING PARQUET OPTIONS (path '/data-pipeline/')\n  CONVERT TO DELTA events\n  ```\n\nFor details, see [Convert a Parquet table to a Delta table](/delta-utility/#convert-a-parquet-table-to-a-delta-table).\n\n## Migrate Delta Lake workloads to newer versions\n\nThis section discusses any changes that may be required in the user code when migrating from older to newer versions of Delta Lake.\n\n### Below Delta Lake 3.0 to Delta Lake 3.0 or above\n\nPlease note that the Delta Lake on Spark Maven artifact has been renamed from `delta-core` (before 3.0) to `delta-spark` (3.0 and above).\n\n### Delta Lake 2.1.1 or below to Delta Lake 2.2 or above\n\nDelta Lake 2.2 collects statistics by default when converting a parquet table to a Delta Lake table (e.g. using the `CONVERT TO DELTA` command). To opt out of statistics collection and revert to the 2.1.1 or below default behavior, use the `NO STATISTICS` SQL API (e.g. `CONVERT TO DELTA parquet.`/path-to-table` NO STATISTICS`)\n\n### Delta Lake 1.2.1, 2.0.0, or 2.1.0 to Delta Lake 2.0.1, 2.1.1 or above\n\nDelta Lake 1.2.1, 2.0.0 and 2.1.0 have a bug in their DynamoDB-based S3 multi-cluster configuration implementations where an incorrect timestamp value was written to DynamoDB. This caused [DynamoDB's TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) feature to cleanup completed items before it was safe to do so. This has been fixed in Delta Lake versions 2.0.1 and 2.1.1, and the TTL attribute has been renamed from `commitTime` to `expireTime`.\n\nIf you _already_ have TTL enabled on your DynamoDB table using the old attribute, you need to disable TTL for that attribute and then enable it for the new one. You may need to wait an hour between these two operations, as TTL settings changes may take some time to propagate. See the DynamoDB docs [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-before-you-start.html). If you don't do this, DyanmoDB's TTL feature will not remove any new and expired entries. There is no risk of data loss.\n\n```bash\n# Disable TTL on old attribute\naws dynamodb update-time-to-live \\\n  --region <region> \\\n  --table-name <table-name> \\\n  --time-to-live-specification \"Enabled=false, AttributeName=commitTime\"\n\n# Enable TTL on new attribute\naws dynamodb update-time-to-live \\\n  --region <region> \\\n  --table-name <table-name> \\\n  --time-to-live-specification \"Enabled=true, AttributeName=expireTime\"\n```\n\n### Delta Lake 2.0 or below to Delta Lake 2.1 or above\n\nWhen calling `CONVERT TO DELTA` on a catalog table Delta Lake 2.1 infers the data schema from the catalog. In version 2.0 and below, Delta Lake infers the data schema from the data. This means in Delta 2.1 data columns that are not defined in the original catalog table will not be present in the converted Delta table. This behavior can be disabled by setting the Spark session configuration `spark.databricks.delta.convert.useCatalogSchema=false`.\n\n### Delta Lake 1.2 or below to Delta Lake 2.0 or above\n\nDelta Lake 2.0.0 introduced a behavior change for [DROP CONSTRAINT](/delta-constraints/#check-constraint). In version 1.2 and below, no error was thrown when trying to drop a non-existent constraint. In version 2.0.0 and above, the behavior is changed to throw a constraint not exists error. To avoid the error, use `IF EXISTS` construct (for example, `ALTER TABLE events DROP CONSTRAINT IF EXISTS constraint_name`). There is no change in behavior in dropping an existing constraint.\n\nDelta Lake 2.0.0 introduced support for [Dynamic Partition Overwrites](/delta-batch/#overwrite). In version 1.2 and below, enabling dynamic partition overwrite mode in either the Spark session configuration or a `DataFrameWriter` option was a no-op, and writes in `overwrite` mode replaced all existing data in every partition of the table. In version 2.0.0 and above, when dynamic partition overwrite mode is enabled, Delta Lake replaces all existing data in each logical partition for which the write will commit new data.\n\n### Delta Lake 1.1 or below to Delta Lake 1.2 or above\n\nThe [LogStore](/api/latest/java/index.html) related code is extracted out from the `delta-core` Maven module into a new module `delta-storage` as part of the issue [#951](https://github.com/delta-io/delta/issues/951) for better code manageability. This results in an additional JAR `delta-storage-<version>.jar` dependency for `delta-core`. By default, the additional JAR is downloaded as part of the `delta-core-<version>_<scala-version>.jar` dependency. In clusters where there is _no internet connectivity_, `delta-storage-<version>.jar` cannot be downloaded. It is advised to download the `delta-storage-<version>.jar` manually and place it in the Java classpath.\n\n### Delta Lake 1.0 or below to Delta Lake 1.1 or above\n\nIf the name of a partition column in a Delta table contains invalid characters (` ,;{}()\\n\\t=`), you cannot read it in Delta Lake 1.1 and above, due to [SPARK-36271](https://issues.apache.org/jira/browse/SPARK-36271). However, this should be rare as you cannot create such tables by using Delta Lake 0.6 and above. If you still have such legacy tables, you can overwrite your tables with new valid column names by using Delta Lake 1.0 and below before upgrading Delta Lake to 1.1 and above, such as the following:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n```python\nspark.read \\\n  .format(\"delta\") \\\n  .load(\"/the/delta/table/path\") \\\n  .withColumnRenamed(\"column name\", \"column-name\") \\\n  .write \\\n  .format(\"delta\")\\\n  .mode(\"overwrite\") \\\n  .option(\"overwriteSchema\", \"true\") \\\n  .save(\"/the/delta/table/path\")\n```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n```scala\nspark.read\n  .format(\"delta\")\n  .load(\"/the/delta/table/path\")\n  .withColumnRenamed(\"column name\", \"column-name\")\n  .write\n  .format(\"delta\")\n  .mode(\"overwrite\")\n  .option(\"overwriteSchema\", \"true\")\n  .save(\"/the/delta/table/path\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Delta Lake 0.6 or below to Delta Lake 0.7 or above\n\nIf you are using `DeltaTable` APIs in Scala, Java, or Python to [update](/delta-update/) or [run utility operations](/delta-utility/) on them, then you may have to add the following configurations when creating the `SparkSession` used to perform those operations.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    from pyspark.sql import SparkSession\n\n    spark = SparkSession \\\n      .builder \\\n      .appName(\"...\") \\\n      .master(\"...\") \\\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n      .getOrCreate()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    import org.apache.spark.sql.SparkSession\n\n    val spark = SparkSession\n      .builder()\n      .appName(\"...\")\n      .master(\"...\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    import org.apache.spark.sql.SparkSession;\n\n    SparkSession spark = SparkSession\n      .builder()\n      .appName(\"...\")\n      .master(\"...\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate();\n    ```\n\n  </TabItem>\n</Tabs>\n\nAlternatively, you can add additional configurations when submitting you Spark application using `spark-submit` or when starting `spark-shell`/`pyspark` by specifying them as command line parameters.\n\n```bash\nspark-submit \\\n  --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n  --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\" \\\n  ...\n```\n\n```bash\npyspark \\\n  --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n  --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\" \\\n  ...\n```\n"
  },
  {
    "path": "docs/src/content/docs/presto-integration.mdx",
    "content": "---\ntitle: Presto, Trino, and Athena to Delta Lake integration using manifests\ndescription: Learn how to set up an integration to enable you to read Delta tables from Presto, Trino, and Athena.\n---\n\nimport { Tabs, TabItem, Aside, Steps } from \"@astrojs/starlight/components\";\n\n<Aside type=\"caution\" title=\"Important\">\n\nPresto, Trino and Athena all have native support for Delta Lake. Support is as follows:\n\n- Presto [version 0.269](https://prestodb.io/docs/0.269/release/release-0.269.html#delta-lake-connector-changes) and above natively supports reading the Delta Lake tables. For details on using the native Delta Lake connector, see [Delta Lake Connector - Presto](https://prestodb.io/docs/current/connector/deltalake.html). For Presto versions lower than [0.269](https://prestodb.io/docs/0.269/release/release-0.269.html#delta-lake-connector-changes), you can use the manifest-based approach in this article.\n- Trino [version 373](https://trino.io/docs/current/release/release-373.html) and above natively supports reading and writing the Delta Lake tables. For details on using the native Delta Lake connector, see [Delta Lake Connector - Trino](https://trino.io/docs/current/connector/delta-lake.html). For Trino versions lower than [version 373](https://trino.io/docs/current/release/release-373.html), you can use the manifest-based approach detailed in this article.\n- Athena [version 3](https://docs.aws.amazon.com/athena/latest/ug/engine-versions-reference-0003.html) and above natively supports reading Delta Lake tables. For details on using the native Delta Lake connector, see [Querying Delta Lake tables](https://docs.aws.amazon.com/athena/latest/ug/delta-lake-tables.html). For Athena versions lower than [version 3](https://docs.aws.amazon.com/athena/latest/ug/engine-versions-reference-0003.html), you can use the manifest-based approach detailed in this article.\n\n</Aside>\n\nPresto, Trino, and Athena support reading from external tables using a _manifest file_, which is a text file containing the list of data files to read for querying a table. When an external table is defined in the Hive metastore using manifest files, Presto, Trino, and Athena can use the list of files in the manifest rather than finding the files by directory listing. This article describes how to set up a Presto, Trino, and Athena to Delta Lake integration using manifest files and query Delta tables.\n\n## Set up the Presto, Trino, or Athena to Delta Lake integration and query Delta tables\n\nYou set up a Presto, Trino, or Athena to Delta Lake integration using the following steps.\n\n### Step 1: Generate manifests of a Delta table using Apache Spark\n\nUsing Spark [configured](/quick-start#set-up-apache-spark-with-delta-lake) with Delta Lake, run any of the following commands on a Delta table at location `<path-to-delta-table>`:\n\n<Tabs>\n<TabItem value=\"sql\" label=\"SQL\" default>\n\n```sql\nGENERATE symlink_format_manifest FOR TABLE delta.`<path-to-delta-table>`\n```\n\n</TabItem>\n<TabItem value=\"scala\" label=\"Scala\">\n\n```scala\nval deltaTable = DeltaTable.forPath(<path-to-delta-table>)\ndeltaTable.generate(\"symlink_format_manifest\")\n```\n\n</TabItem>\n<TabItem value=\"java\" label=\"Java\">\n\n```java\nDeltaTable deltaTable = DeltaTable.forPath(<path-to-delta-table>);\ndeltaTable.generate(\"symlink_format_manifest\");\n```\n\n</TabItem>\n<TabItem value=\"python\" label=\"Python\">\n\n```python\ndeltaTable = DeltaTable.forPath(<path-to-delta-table>)\ndeltaTable.generate(\"symlink_format_manifest\")\n```\n\n</TabItem>\n</Tabs>\n\nSee [Generate a manifest file](delta-utility.html#generate-a-manifest-file) for details.\n\nThe `generate` command generates manifest files at `<path-to-delta-table>/_symlink_format_manifest/`. In other words, the files in this directory will contain the names of the data files (that is, Parquet files) that should be read for reading a snapshot of the Delta table.\n\n### Step 2: Configure Presto, Trino, or Athena to read the generated manifests\n\n1. Define a new table in the Hive metastore connected to Presto, Trino, or Athena using the format `SymlinkTextInputFormat` and the manifest location `<path-to-delta-table>/_symlink_format_manifest/`.\n\n```sql\nCREATE EXTERNAL TABLE mytable ([(col_name1 col_datatype1, ...)])\n[PARTITIONED BY (col_name2 col_datatype2, ...)]\nROW FORMAT SERDE 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'\nSTORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat'\nOUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'\nLOCATION '<path-to-delta-table>/_symlink_format_manifest/'  -- location of the generated manifest\n```\n\n`SymlinkTextInputFormat` configures Presto, Trino, or Athena to compute file splits for `mytable` by reading the manifest file instead of using a directory listing to find data files. Replace `mytable` with the name of the external table and `<path-to-delta-table>` with the absolute path to the Delta table.\n\n<Aside type=\"caution\" title=\"Important\">\n  - `mytable` must be the same schema and have the same partitions as the Delta table.\n  - The set of `PARTITIONED BY` columns must be distinct from the set of non-partitioned columns. Furthermore, you cannot specify partitioned columns with `AS <select-statement>`.\n</Aside>\n\n- You cannot use this table definition in Apache Spark; it can be used only by Presto, Trino, and Athena.\n\n  The tool you use to run the command depends on whether Apache Spark and Presto, Trino, or Athena use the same Hive metastore.\n\n  - **Same metastore**: If both Apache Spark and Presto, Trino, or Athena use the same Hive metastore, you can define the table using Apache Spark.\n\n- **Different metastores**: If Apache Spark and Presto, Trino, or Athena use different metastores, you must define the table using other tools.\n\n  - Athena: You can define the external table in Athena.\n  - Presto: Presto does not support the syntax `CREATE EXTERNAL TABLE ... STORED AS ...`, so you must use another tool (for example, Spark or Hive) connected to the same metastore as Presto to create the table.\n\n2. If the Delta table is partitioned, run `MSCK REPAIR TABLE mytable` after generating the manifests to force the metastore (connected to Presto, Trino, or Athena) to discover the partitions. This is needed because the manifest of a partitioned table is itself partitioned in the same directory structure as the table. Run this command using _the same tool_ used to create the table. Furthermore, you should run this command:\n\n   - **After every manifest generation**: New partitions are likely to be visible immediately after the manifest files have been updated. However, doing this too frequently can cause high load for the Hive metastore.\n   - **As frequently as new partitions are expected**: For example, if a table is partitioned by date, then you can run repair once after every midnight, after the new partition has been created in the table and its corresponding manifest files have been generated.\n\n### Step 3: Update manifests\n\nWhen the data in a Delta table is updated you must regenerate the manifests using either of the following approaches:\n\n- **Update explicitly**: After all the data updates, you can run the `generate` operation to update the manifests.\n\n- **Update automatically**: You can configure a Delta table so that all write operations on the table automatically update the manifests. To enable this automatic mode, set the corresponding table property using the following SQL command.\n\n```sql\nALTER TABLE delta.`<path-to-delta-table>` SET TBLPROPERTIES(delta.compatibility.symlinkFormatManifest.enabled=true)\n```\n\nTo disable this automatic mode, set this property to `false`. In addition, for partitioned tables, you have to run `MSCK REPAIR` to ensure the metastore connected to Presto, Trino, or Athena to update partitions.\n\n<Aside type=\"note\">\n  After enabling automatic mode on a partitioned table, each write operation\n  updates only manifests corresponding to the partitions that operation wrote\n  to. This incremental update ensures that the overhead of manifest generation\n  is low for write operations. However, this also means that if the manifests in\n  other partitions are stale, enabling automatic mode will not automatically fix\n  it. Therefore, it is recommended that you explicitly run `GENERATE` to update\n  manifests for the entire table immediately after enabling automatic mode.\n</Aside>\n\nWhether to update automatically or explicitly depends on the concurrent nature of write operations on the Delta table and the desired data consistency. For example, if automatic mode is enabled, concurrent write operations lead to concurrent overwrites to the manifest files. With such unordered writes, the manifest files are not guaranteed to point to the latest version of the table after the write operations complete. Hence, if concurrent writes are expected and you want to avoid stale manifests, you should consider explicitly updating the manifest after the expected write operations have completed.\n\n## Limitations\n\nThe Presto, Trino, and Athena integration has known limitations in its behavior.\n\n### Data consistency\n\nWhenever Delta Lake generates updated manifests, it atomically overwrites existing manifest files. Therefore, Presto, Trino, and Athena will always see a consistent view of the data files; it will see all of the old version files or all of the new version files. However, the granularity of the consistency guarantees depends on whether or not the table is partitioned.\n\n- **Unpartitioned tables**: All the files names are written in one manifest file which is updated atomically. In this case Presto, Trino, and Athena will see full table snapshot consistency.\n- **Partitioned tables**: A manifest file is partitioned in the same Hive-partitioning-style directory structure as the original Delta table. This means that each partition is updated atomically, and Presto, Trino, or Athena will see a consistent view of each partition but not a consistent view across partitions. Furthermore, since all manifests of all partitions cannot be updated together, concurrent attempts to generate manifests can lead to different partitions having manifests of different versions. While this consistency guarantee under data change is weaker than that of reading Delta tables with Spark, it is still stronger than formats like Parquet as they do not provide partition-level consistency.\n\nDepending on what storage system you are using for Delta tables, it is possible to get incorrect results when Presto, Trino, or Athena concurrently queries the manifest while the manifest files are being rewritten. In file system implementations that lack atomic file overwrites, a manifest file may be momentarily unavailable. Hence, use manifests with caution if their updates are likely to coincide with queries from Presto, Trino, or Athena.\n\n### Performance\n\nVery large numbers of files can hurt the performance of Presto, Trino, and Athena. Hence it is recommended that you [compact the files](/best-practices#compact-files) of the table before generating the manifests. The number of files should not exceed 1000 (for the entire unpartitioned table or for each partition in a partitioned table).\n\n### Schema evolution\n\nDelta Lake supports schema evolution and queries on a Delta table automatically use the latest schema regardless of the schema defined in the table in the Hive metastore. However, Presto, Trino, or Athena uses the schema defined in the Hive metastore and will not query with the updated schema until the table used by Presto, Trino, or Athena is redefined to have the updated schema.\n\n### Encrypted tables\n\nAthena does not support reading manifests from [CSE-KMS](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-emrfs-encryption-cse.html) encrypted tables. See the AWS documentation for the latest information.\n"
  },
  {
    "path": "docs/src/content/docs/quick-start.mdx",
    "content": "---\ntitle: Quick Start\ndescription: Learn how to get started quickly with Delta Lake.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nThis guide helps you quickly explore the main features of Delta Lake. It provides code snippets that show how to read from and write to Delta tables from interactive, batch, and streaming queries.\n\n## Set up Apache Spark with Delta Lake\n\nFollow these instructions to set up Delta Lake with Spark. You can run the steps in this guide on your local machine in the following two ways:\n\n1. **Run interactively**: Start the Spark shell (Scala or Python) with Delta Lake and run the code snippets interactively in the shell.\n2. **Run as a project**: Set up a Maven or SBT project (Scala or Java) with Delta Lake, copy the code snippets into a source file, and run the project. Alternatively, you can use the [examples provided in the Github repository](https://github.com/delta-io/delta/tree/master/examples).\n\n<Aside type=\"caution\" title=\"Important!\">\n  For all of the following instructions, make sure to install the correct\n  version of Spark or PySpark that is compatible with Delta Lake `4.0.0`. See the\n  [release compatibility matrix](/releases) for details.\n</Aside>\n\n### Prerequisite: set up Java\n\nAs mentioned in the official Apache Spark installation instructions [here](https://spark.apache.org/docs/latest/index.html#downloading), make sure you have a valid Java version installed (8, 11, or 17) and that Java is configured correctly on your system using either the system `PATH` or `JAVA_HOME` environmental variable.\n\nWindows users should follow the instructions in this [blog](https://phoenixnap.com/kb/install-spark-on-windows-10), making sure to use the correct version of Apache Spark that is compatible with Delta Lake `4.0.0`.\n\n### Set up interactive shell\n\nTo use Delta Lake interactively within the Spark SQL, Scala, or Python shell, you need a local installation of Apache Spark. Depending on whether you want to use SQL, Python, or Scala, you can set up either the SQL, PySpark, or Spark shell, respectively.\n\n#### Spark SQL Shell\n\nDownload the [compatible version](/releases) of Apache Spark by following instructions from [Downloading Spark](https://spark.apache.org/downloads.html), either using `pip` or by downloading and extracting the archive and running `spark-sql` in the extracted directory.\n\n```bash\nbin/spark-sql --packages io.delta:delta-spark_2.13:4.0.0 --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n```\n\n#### PySpark Shell\n\n1. Install the PySpark version that is [compatible](/releases) with the Delta Lake version by running the following:\n\n    ```bash\n    pip install pyspark==<compatible-spark-version>\n    ```\n2. Run PySpark with the Delta Lake package and additional configurations:\n\n    ```bash\n    pyspark --packages io.delta:delta-spark_2.13:4.0.0 --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n    ```\n\n#### Spark Scala Shell\n\nDownload the [compatible version](/releases/) of Apache Spark by following instructions from [Downloading Spark](https://spark.apache.org/downloads.html), either using `pip` or by downloading and extracting the archive and running `spark-shell` in the extracted directory.\n\n```bash\nbin/spark-shell --packages io.delta:delta-spark_2.13:4.0.0 --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n```\n\n### Set up project\n\nIf you want to build a project using Delta Lake binaries from Maven Central Repository, you can use the following Maven coordinates.\n\n#### Maven\n\nYou include Delta Lake in your Maven project by adding it as a dependency in your POM file. Delta Lake compiled with Scala 2.13.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"XML\" active>\n\n    ```xml\n    <dependency>\n      <groupId>io.delta</groupId>\n      <artifactId>delta-spark_2.13</artifactId>\n      <version>4.0.0</version>\n    </dependency>\n    ```\n  \n  </TabItem>\n</Tabs>\n\n#### SBT\n\nYou include Delta Lake in your SBT project by adding the following line to your `build.sbt` file:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Scala\" active>\n\n    ```scala\n    libraryDependencies += \"io.delta\" %% \"delta-spark\" % \"4.0.0\"\n    ```\n  \n  </TabItem>\n</Tabs>\n\n#### Python\n\nTo set up a Python project (for example, for unit testing), you can install Delta Lake using `pip install delta-spark==4.0.0` and then configure the SparkSession with the `configure_spark_with_delta_pip()` utility function in Delta Lake.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\" active>\n\n    ```python\n    import pyspark\n    from delta import *\n\n    builder = pyspark.sql.SparkSession.builder.appName(\"MyApp\") \\\n        .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n        .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n\n    spark = configure_spark_with_delta_pip(builder).getOrCreate()\n    ```\n  \n  </TabItem>\n</Tabs>\n\n## Create a table\n\nTo create a Delta table, write a DataFrame out in the `delta` format. You can use existing Spark SQL code and change the format from `parquet`, `csv`, `json`, and so on, to `delta`.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    CREATE TABLE delta.`/tmp/delta-table` USING DELTA AS SELECT col1 as id FROM VALUES 0,1,2,3,4;\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Python\">\n    \n    ```python\n    data = spark.range(0, 5)\n    data.write.format(\"delta\").save(\"/tmp/delta-table\")\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Scala\">\n    \n    ```scala\n    val data = spark.range(0, 5)\n    data.write.format(\"delta\").save(\"/tmp/delta-table\")\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Java\">\n\n    ```java\n    import org.apache.spark.sql.SparkSession;\n    import org.apache.spark.sql.Dataset;\n    import org.apache.spark.sql.Row;\n\n    SparkSession spark = ...   // create SparkSession\n\n    Dataset<Row> data = spark.range(0, 5);\n    data.write().format(\"delta\").save(\"/tmp/delta-table\");\n    ```\n\n  </TabItem>\n</Tabs>\n\nThese operations create a new Delta table using the schema that was _inferred_ from your DataFrame. For the full set of options available when you create a new Delta table, see [Create a table](/delta-batch/#create-a-table) and [Write to a table](/delta-batch/#write-to-table).\n\n<Aside type=\"note\">\n  This quickstart uses local paths for Delta table locations. For configuring\n  HDFS or cloud storage for Delta tables, see [Storage\n  configuration](/delta-storage).\n</Aside>\n\n## Read data\n\nYou read data in your Delta table by specifying the path to the files: `\"/tmp/delta-table\"`:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n\n    ```sql\n    SELECT * FROM delta.`/tmp/delta-table`;\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Python\">\n    \n    ```python\n    df = spark.read.format(\"delta\").load(\"/tmp/delta-table\")\n    df.show()\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val df = spark.read.format(\"delta\").load(\"/tmp/delta-table\")\n    df.show()\n    ```\n\n  </TabItem>\n\n  <TabItem label=\"Java\">\n  \n    ```java\n    Dataset<Row> df = spark.read().format(\"delta\").load(\"/tmp/delta-table\");\n    df.show();\n    ```\n  \n  </TabItem>\n</Tabs>\n\n## Update table data\n\nDelta Lake supports several operations to modify tables using standard DataFrame APIs. This example runs a batch job to overwrite the data in the table:\n\n### Overwrite\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    INSERT OVERWRITE delta.`/tmp/delta-table` SELECT col1 as id FROM VALUES 5,6,7,8,9;\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    data = spark.range(5, 10)\n    data.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta-table\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val data = spark.range(5, 10)\n    data.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta-table\")\n    df.show()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    Dataset<Row> data = spark.range(5, 10);\n    data.write().format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta-table\");\n    ```\n\n  </TabItem>\n</Tabs>\n\nIf you read this table again, you should see only the values `5-9` you have added because you overwrote the previous data.\n\n### Conditional update without overwrite\n\nDelta Lake provides programmatic APIs to conditional update, delete, and merge (upsert) data into tables. Here are a few examples.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    -- Update every even value by adding 100 to it\n    UPDATE delta.`/tmp/delta-table` SET id = id + 100 WHERE id % 2 == 0;\n\n    -- Delete every even value\n    DELETE FROM delta.`/tmp/delta-table` WHERE id % 2 == 0;\n\n    -- Upsert (merge) new data\n    CREATE TEMP VIEW newData AS SELECT col1 AS id FROM VALUES 1,3,5,7,9,11,13,15,17,19;\n\n    MERGE INTO delta.`/tmp/delta-table` AS oldData\n    USING newData\n    ON oldData.id = newData.id\n    WHEN MATCHED\n      THEN UPDATE SET id = newData.id\n    WHEN NOT MATCHED\n      THEN INSERT (id) VALUES (newData.id);\n\n    SELECT * FROM delta.`/tmp/delta-table`;\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    from delta.tables import *\n    from pyspark.sql.functions import *\n\n    deltaTable = DeltaTable.forPath(spark, \"/tmp/delta-table\")\n\n    # Update every even value by adding 100 to it\n    deltaTable.update(\n      condition = expr(\"id % 2 == 0\"),\n      set = { \"id\": expr(\"id + 100\") })\n\n    # Delete every even value\n    deltaTable.delete(condition = expr(\"id % 2 == 0\"))\n\n    # Upsert (merge) new data\n    newData = spark.range(0, 20)\n\n    deltaTable.alias(\"oldData\") \\\n      .merge(\n        newData.alias(\"newData\"),\n        \"oldData.id = newData.id\") \\\n      .whenMatchedUpdate(set = { \"id\": col(\"newData.id\") }) \\\n      .whenNotMatchedInsert(values = { \"id\": col(\"newData.id\") }) \\\n      .execute()\n\n    deltaTable.toDF().show()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    import io.delta.tables._\n    import org.apache.spark.sql.functions._\n\n    val deltaTable = DeltaTable.forPath(\"/tmp/delta-table\")\n\n    // Update every even value by adding 100 to it\n    deltaTable.update(\n      condition = expr(\"id % 2 == 0\"),\n      set = Map(\"id\" -> expr(\"id + 100\")))\n\n    // Delete every even value\n    deltaTable.delete(condition = expr(\"id % 2 == 0\"))\n\n    // Upsert (merge) new data\n    val newData = spark.range(0, 20).toDF\n\n    deltaTable.as(\"oldData\")\n      .merge(\n        newData.as(\"newData\"),\n        \"oldData.id = newData.id\")\n      .whenMatched\n      .update(Map(\"id\" -> col(\"newData.id\")))\n      .whenNotMatched\n      .insert(Map(\"id\" -> col(\"newData.id\")))\n      .execute()\n\n    deltaTable.toDF.show()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    import io.delta.tables.*;\n    import org.apache.spark.sql.functions;\n    import java.util.HashMap;\n\n    DeltaTable deltaTable = DeltaTable.forPath(\"/tmp/delta-table\");\n\n    // Update every even value by adding 100 to it\n    deltaTable.update(\n      functions.expr(\"id % 2 == 0\"),\n      new HashMap<String, Column>() {{\n        put(\"id\", functions.expr(\"id + 100\"));\n      }}\n    );\n\n    // Delete every even value\n    deltaTable.delete(condition = functions.expr(\"id % 2 == 0\"));\n\n    // Upsert (merge) new data\n    Dataset<Row> newData = spark.range(0, 20).toDF();\n\n    deltaTable.as(\"oldData\")\n      .merge(\n        newData.as(\"newData\"),\n        \"oldData.id = newData.id\")\n      .whenMatched()\n      .update(\n        new HashMap<String, Column>() {{\n          put(\"id\", functions.col(\"newData.id\"));\n        }})\n      .whenNotMatched()\n      .insertExpr(\n        new HashMap<String, Column>() {{\n          put(\"id\", functions.col(\"newData.id\"));\n        }})\n      .execute();\n\n    deltaTable.toDF().show();\n    ```\n\n  </TabItem>\n</Tabs>\n\nYou should see that some of the existing rows have been updated and new rows have been inserted.\n\nFor more information on these operations, see [Table deletes, updates, and merges](/delta-update).\n\n## Read older versions of data using time travel\n\nYou can query previous snapshots of your Delta table by using time travel. If you want to access the data that you overwrote, you can query a snapshot of the table before you overwrote the first set of data using the `versionAsOf` option.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n  \n    ```sql\n    SELECT * FROM delta.`/tmp/delta-table` VERSION AS OF 0;\n    ```\n  \n  </TabItem>\n  <TabItem label=\"Python\">\n\n    ```python\n    df = spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(\"/tmp/delta-table\")\n    df.show()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val df = spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(\"/tmp/delta-table\")\n    df.show()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    Dataset<Row> df = spark.read().format(\"delta\").option(\"versionAsOf\", 0).load(\"/tmp/delta-table\");\n    df.show();\n    ```\n\n  </TabItem>\n</Tabs>\n\nYou should see the first set of data, from before you overwrote it. Time travel takes advantage of the power of the Delta Lake transaction log to access data that is no longer in the table. Removing the version `0` option (or specifying version `1`) would let you see the newer data again. For more information, see [Query an older snapshot of a table (time travel)](/delta-batch#query-an-older-snapshot-of-a-table-time-travel).\n\n## Write a stream of data to a table\n\nYou can also write to a Delta table using Structured Streaming. The Delta Lake transaction log guarantees exactly-once processing, even when there are other streams or batch queries running concurrently against the table. By default, streams run in append mode, which adds new records to the table:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    streamingDf = spark.readStream.format(\"rate\").load()\n    stream = streamingDf.selectExpr(\"value as id\").writeStream.format(\"delta\").option(\"checkpointLocation\", \"/tmp/checkpoint\").start(\"/tmp/delta-table\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val streamingDf = spark.readStream.format(\"rate\").load()\n    val stream = streamingDf.select($\"value\" as \"id\").writeStream.format(\"delta\").option(\"checkpointLocation\", \"/tmp/checkpoint\").start(\"/tmp/delta-table\")\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    import org.apache.spark.sql.streaming.StreamingQuery;\n\n    Dataset<Row> streamingDf = spark.readStream().format(\"rate\").load();\n    StreamingQuery stream = streamingDf.selectExpr(\"value as id\").writeStream().format(\"delta\").option(\"checkpointLocation\", \"/tmp/checkpoint\").start(\"/tmp/delta-table\");\n    ```\n\n  </TabItem>\n</Tabs>\n\nWhile the stream is running, you can read the table using the earlier commands.\n\n<Aside type=\"note\">\n  If you're running this in a shell, you may see the streaming task progress,\n  which make it hard to type commands in that shell. It may be useful to start\n  another shell in a new terminal for querying the table.\n</Aside>\n\nYou can stop the stream by running `stream.stop()` in the same terminal that started the stream.\n\nFor more information about Delta Lake integration with Structured Streaming, see [Table streaming reads and writes](/delta-streaming). See also the [Structured Streaming Programming Guide](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html) on the Apache Spark website.\n\n## Read a stream of changes from a table\n\nWhile the stream is writing to the Delta table, you can also read from that table as streaming source. For example, you can start another streaming query that prints all the changes made to the Delta table. You can specify which version Structured Streaming should start from by providing the `startingVersion` or `startingTimestamp` option to get changes from that point onwards. See [Structured Streaming](/delta-streaming/#specify-initial-position) for details.\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"Python\">\n\n    ```python\n    stream2 = spark.readStream.format(\"delta\").load(\"/tmp/delta-table\").writeStream.format(\"console\").start()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Scala\">\n\n    ```scala\n    val stream2 = spark.readStream.format(\"delta\").load(\"/tmp/delta-table\").writeStream.format(\"console\").start()\n    ```\n\n  </TabItem>\n  <TabItem label=\"Java\">\n\n    ```java\n    StreamingQuery stream2 = spark.readStream().format(\"delta\").load(\"/tmp/delta-table\").writeStream().format(\"console\").start();\n    ```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content/docs/redshift-spectrum-integration.mdx",
    "content": "---\ntitle: AWS Redshift Spectrum connector\ndescription: Learn how to set up an integration to enable you to read Delta tables from AWS Redshift.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\n<Aside\n  type=\"note\"\n  title=\"Experimental\"\n>\n  This is an experimental integration. Use with caution.\n</Aside>\n\nA Delta table can be read by AWS Redshift Spectrum using a _manifest file_, which is a text file containing the list of data files to read for querying a Delta table. This article describes how to set up a AWS Redshift Spectrum to Delta Lake integration using manifest files and query Delta tables.\n\n## Set up a AWS Redshift Spectrum to Delta Lake integration and query Delta tables\n\nYou set up a AWS Redshift Spectrum to Delta Lake integration using the following steps.\n\n### Step 1: Generate manifests of a Delta table using Apache Spark\n\nRun the `generate` operation on a Delta table at location `<path-to-delta-table>`:\n\n<Tabs syncKey=\"code-examples\">\n<TabItem value=\"sql\" label=\"SQL\" default>\n\n```sql\nGENERATE symlink_format_manifest FOR TABLE delta.`<path-to-delta-table>`\n```\n\n</TabItem>\n<TabItem value=\"scala\" label=\"Scala\">\n\n```scala\nval deltaTable = DeltaTable.forPath(<path-to-delta-table>)\ndeltaTable.generate(\"symlink_format_manifest\")\n```\n\n</TabItem>\n<TabItem value=\"java\" label=\"Java\">\n\n```java\nDeltaTable deltaTable = DeltaTable.forPath(<path-to-delta-table>);\ndeltaTable.generate(\"symlink_format_manifest\");\n```\n\n</TabItem>\n<TabItem value=\"python\" label=\"Python\">\n\n```python\ndeltaTable = DeltaTable.forPath(<path-to-delta-table>)\ndeltaTable.generate(\"symlink_format_manifest\")\n```\n\n</TabItem>\n</Tabs>\n\nSee [Generate a manifest file](/delta-utility/#generate-a-manifest-file) for details.\n\nThe `generate` operation generates manifest files at `<path-to-delta-table>/_symlink_format_manifest/`. In other words, the files in this directory contain the names of the data files (that is, Parquet files) that should be read for reading a snapshot of the Delta table.\n\n<Aside type=\"note\">\n  We recommend that you define the Delta table in a location that AWS Redshift\n  Spectrum can read directly.\n</Aside>\n\n### Step 2: Configure AWS Redshift Spectrum to read the generated manifests\n\nRun the following commands in your AWS Redshift Spectrum environment.\n\n1. Define a new external table in AWS Redshift Spectrum using the format `SymlinkTextInputFormat` and the manifest location `<path-to-delta-table>/_symlink_format_manifest/`.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE EXTERNAL TABLE mytable ([(col_name1 col_datatype1, ...)])\n    [PARTITIONED BY (col_name2 col_datatype2, ...)]\n    ROW FORMAT SERDE 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'\n    STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat'\n    OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'\n    LOCATION '<path-to-delta-table>/_symlink_format_manifest/'  -- location of the generated manifest\n    ```\n  </TabItem>\n</Tabs>\n\n`SymlinkTextInputFormat` configures AWS Redshift Spectrum to compute file splits for `mytable` by reading the manifest file instead of using a directory listing to find data files. Replace `mytable` with the name of the external table and `<path-to-delta-table>` with the absolute path to the Delta table.\n\n<Aside type=\"caution\" title=\"Important\">\n- `mytable` must be the same schema and have the same partitions as the Delta table.\n- The set of `PARTITIONED BY` columns must be distinct from the set of non-partitioned columns. Furthermore, you cannot specify partitioned columns with `AS <select-statement>`.\n</Aside>\n\n- You cannot use this table definition in Apache Spark; it can be used only by AWS Redshift Spectrum.\n\n2. If the Delta table is partitioned, you must add the partitions explicitly to the AWS Redshift Spectrum table. This is needed because the manifest of a partitioned table is itself partitioned in the same directory structure as the table.\n\n   - For every partition in the table, run the following in AWS Redshift Spectrum, either directly in AWS Redshift Spectrum, or using the AWS CLI or [Data API](https://docs.aws.amazon.com/redshift/latest/mgmt/data-api.html):\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE mytable.redshiftdeltatable ADD IF NOT EXISTS PARTITION (col_name=col_value) LOCATION '<path-to-delta-table>/_symlink_format_manifest/col_name=col_value'\n    ```\n  </TabItem>\n</Tabs>\n\nThis steps will provide you with a [consistent](#data-consistency) view of the Delta table.\n\n### Step 3: Update manifests\n\nWhen data in a Delta table is updated, you must regenerate the manifests using either of the following approaches:\n\n- **Update explicitly**: After all the data updates, you can run the `generate` operation to update the manifests.\n\n- **Update automatically**: You can configure a Delta table so that all write operations on the table automatically update the manifests. To enable this automatic mode, set the corresponding table property using the following SQL command.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE delta.`<path-to-delta-table>` SET TBLPROPERTIES(delta.compatibility.symlinkFormatManifest.enabled=true)\n    ```\n  </TabItem>\n</Tabs>\n\nTo disable this automatic mode, set this property to `false`.\n\n<Aside type=\"note\">\n  After enabling automatic mode on a partitioned table, each write operation\n  updates only manifests corresponding to the partitions that operation wrote\n  to. This incremental update ensures that the overhead of manifest generation\n  is low for write operations. However, this also means that if the manifests in\n  other partitions are stale, enabling automatic mode will not automatically fix\n  it. Therefore, you should explicitly run `GENERATE` to update manifests for\n  the entire table immediately after enabling automatic mode.\n</Aside>\n\nWhether to update automatically or explicitly depends on the concurrent nature of write operations on the Delta table and the desired data consistency. For example, if automatic mode is enabled, concurrent write operations lead to concurrent overwrites to the manifest files. With such unordered writes, the manifest files are not guaranteed to point to the latest version of the table after the write operations complete. Hence, if concurrent writes are expected and you want to avoid stale manifests, you should consider explicitly updating the manifest after the expected write operations have completed.\n\nIn addition, if your table is partitioned, then you must add any new partitions or remove deleted partitions by following the same process as described in the preceding step.\n\n## Limitations\n\nThe AWS Redshift Spectrum integration has known limitations in its behavior.\n\n### Data consistency\n\nWhenever Delta Lake generates updated manifests, it atomically overwrites existing manifest files. Therefore, AWS Redshift Spectrum will always see a consistent view of the data files; it will see all of the old version files or all of the new version files. However, the granularity of the consistency guarantees depends on whether or not the table is partitioned.\n\n- **Unpartitioned tables**: All the files names are written in one manifest file which is updated atomically. In this case AWS Redshift Spectrum will see full table snapshot consistency.\n- **Partitioned tables**: A manifest file is partitioned in the same Hive-partitioning-style directory structure as the original Delta table. This means that each partition is updated atomically, and AWS Redshift Spectrum will see a consistent view of each partition but not a consistent view across partitions. Furthermore, since all manifests of all partitions cannot be updated together, concurrent attempts to generate manifests can lead to different partitions having manifests of different versions. While this consistency guarantee under data change is weaker than that of reading Delta tables with Spark, it is still stronger than formats like Parquet as they do not provide partition-level consistency.\n\nDepending on what storage system you are using for Delta tables, it is possible to get incorrect results when AWS Redshift Spectrum concurrently queries the manifest while the manifest files are being rewritten. In file system implementations that lack atomic file overwrites, a manifest file may be momentarily unavailable. Hence, use manifests with caution if their updates are likely to coincide with queries from AWS Redshift Spectrum.\n\n### Performance\n\nThis is an experimental integration and its performance and scalability characteristics have not yet been tested.\n\n### Schema evolution\n\nDelta Lake supports schema evolution and queries on a Delta table automatically use the latest schema regardless of the schema defined in the table in the Hive metastore. However, AWS Redshift Spectrum uses the schema defined in its table definition, and will not query with the updated schema until the table definition is updated to the new schema.\n"
  },
  {
    "path": "docs/src/content/docs/releases.mdx",
    "content": "---\ntitle: Releases\ndescription: Learn about Delta Lake releases.\n---\n\n## Release notes\n\nThe [GitHub releases page](https://github.com/delta-io/delta/releases/) describes features of each release.\n\n## Compatibility with Apache Spark\n\nThe following table lists Delta Lake versions and their compatible Apache Spark versions.\n\n| Delta Lake version | Apache Spark version    |\n| ------------------ | ----------------------- |\n| 4.0.x              | 4.0.x                   |\n| 3.3.x              | 3.5.x                   |\n| 3.2.x              | 3.5.x                   |\n| 3.1.x              | 3.5.x                   |\n| 3.0.x              | 3.5.x                   |\n| 2.4.x              | 3.4.x                   |\n| 2.3.x              | 3.3.x                   |\n| 2.2.x              | 3.3.x                   |\n| 2.1.x              | 3.3.x                   |\n| 2.0.x              | 3.2.x                   |\n| 1.2.x              | 3.2.x                   |\n| 1.1.x              | 3.2.x                   |\n| 1.0.x              | 3.1.x                   |\n| 0.7.x and 0.8.x    | 3.0.x                   |\n| Below 0.7.0        | 2.4.2 - 2.4._\\<latest>_ |\n"
  },
  {
    "path": "docs/src/content/docs/snowflake-integration.mdx",
    "content": "---\ntitle: Snowflake connector\ndescription: Learn how to set up an integration to enable you to read Delta tables from Snowflake.\n---\n\nimport { Aside, Tabs, TabItem, Steps } from \"@astrojs/starlight/components\";\n\nVisit the [Snowflake Delta Lake support](https://docs.snowflake.com/en/user-guide/tables-external-intro.html#delta-lake-support) documentation to use the connector.\n\n<Aside type=\"caution\" title=\"Important\">\nSome users in the community have reported that Snowflake, unlike [Trino](https://trino.io/docs/current/connector/delta-lake.html) or [Spark](/delta-batch/), is not using [Delta statistics](/optimizations-oss/#data-skipping) to do data skipping when reading Delta tables. Due to this bug, Snowflake may read a lot of unnecessary parquet files resulting in poor query performance and increased API call requests from cloud providers.\n\nIf you are a Snowflake customer and have subscribed to their enterprise support, please [open a support case](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge).\n\nAs a workaround, you can enable [Delta UniForm](/delta-uniform/) to generate Iceberg metadata and read these tables as Iceberg tables from Snowflake.\n\n</Aside>\n\nA Delta table can be read by Snowflake using a _manifest file_, which is a text file containing the list of data files to read for querying a Delta table. This article describes how to set up a Delta Lake to Snowflake integration using manifest files and query Delta tables.\n\n## Set up a Delta Lake to Snowflake integration and query Delta tables\n\nYou set up a Delta Lake to Snowflake integration using the following steps.\n\n### Step 1: Generate manifests of a Delta table using Apache Spark\n\nRun the `generate` operation on a Delta table at location `<path-to-delta-table>`:\n\n<Tabs syncKey=\"code-examples\">\n<TabItem value=\"sql\" label=\"SQL\" default>\n\n```sql\nGENERATE symlink_format_manifest FOR TABLE delta.`<path-to-delta-table>`\n```\n\n</TabItem>\n<TabItem value=\"scala\" label=\"Scala\">\n\n```scala\nval deltaTable = DeltaTable.forPath(<path-to-delta-table>)\ndeltaTable.generate(\"symlink_format_manifest\")\n```\n\n</TabItem>\n<TabItem value=\"java\" label=\"Java\">\n\n```java\nDeltaTable deltaTable = DeltaTable.forPath(<path-to-delta-table>);\ndeltaTable.generate(\"symlink_format_manifest\");\n```\n\n</TabItem>\n<TabItem value=\"python\" label=\"Python\">\n\n```python\ndeltaTable = DeltaTable.forPath(<path-to-delta-table>)\ndeltaTable.generate(\"symlink_format_manifest\")\n```\n\n</TabItem>\n</Tabs>\n\nSee [Generate a manifest file](/delta-utility/#generate-a-manifest-file) for details.\n\nThe `generate` operation generates manifest files at `<path-to-delta-table>/_symlink_format_manifest/`. In other words, the files in this directory contain the names of the data files (that is, Parquet files) that should be read for reading a snapshot of the Delta table.\n\n<Aside type=\"note\">\n  We recommend that you define the Delta table in a location that Snowflake can\n  read directly.\n</Aside>\n\n### Step 2: Configure Snowflake to read the generated manifests\n\nRun the following commands in your Snowflake environment.\n\n#### Define an external table on the manifest files\n\nTo define an external table in Snowflake, you must first [define a external stage](https://docs.snowflake.net/manuals/user-guide/data-load-s3-create-stage.html) `my_staged_table` that points to the Delta table. In Snowflake, run the following.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    create or replace stage my_staged_table url='<path-to-delta-table>'\n    ```\n  </TabItem>\n</Tabs>\n\nReplace `<path-to-delta-table>` with the full path to the Delta table. Using this stage, you can [define a table](https://docs.snowflake.net/manuals/sql-reference/sql/create-external-table.html) `delta_manifest_table` that reads the file names specified in the manifest files as follows:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql CREATE OR REPLACE EXTERNAL TABLE delta_manifest_table( filename\n    VARCHAR AS split_part(VALUE:c1, '/', -1) ) WITH LOCATION =\n    @my_staged_table/_symlink_format_manifest/ FILE_FORMAT = (TYPE = CSV)\n    PATTERN = '.*[/]manifest' AUTO_REFRESH = true; ```\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\nIn this query:\n\n- The location is the manifest subdirectory.\n- The `filename` column contains the name of the files (not the full path) defined in the manifest.\n  </Aside>\n\n#### Define an external table on Parquet files\n\nYou can define a table `my_parquet_data_table` that reads all the Parquet files in the Delta table.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE OR REPLACE EXTERNAL TABLE my_parquet_data_table(\n      id INT AS (VALUE:id::INT),\n      part INT AS (VALUE:part::INT),\n      ...,\n      parquet_filename VARCHAR AS split_part(metadata$filename, '/', -1)\n    )\n    WITH LOCATION = @my_staged_table/\n    FILE_FORMAT = (TYPE = PARQUET)\n    PATTERN = '.*[/]part-[^/]*[.]parquet'\n    AUTO_REFRESH = true;\n    ```\n  </TabItem>\n</Tabs>\n\n<Aside type=\"note\">\n  In this query:\n  - The location is the Delta table path.\n  - The `parquet_filename` column contains the name of the file that contains each row of the table.\n</Aside>\n\nIf your Delta table is partitioned, then you will have to explicitly extract the partition values in the table definition. For example, if the table was partitioned by a single integer column named `part`, you can extract the values as follows:\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE OR REPLACE EXTERNAL TABLE my_parquet_data_partitioned_table(\n      id INT AS (VALUE:id::INT),\n      part INT AS (\n        nullif(\n          regexp_replace(metadata$filename, '.*part\\\\=(.*)\\\\/.*', '\\\\1'),\n          '__HIVE_DEFAULT_PARTITION__'\n        )::INT\n      ),\n      ...,\n      parquet_filename VARCHAR AS split_part(metadata$filename, '/', -1)\n    )\n    WITH LOCATION = @my_staged_partitioned_table/\n    FILE_FORMAT = (TYPE = PARQUET)\n    PATTERN = '.*[/]part-[^/]*[.]parquet'\n    AUTO_REFRESH = true;\n    ```\n  </TabItem>\n</Tabs>\n\nThe regular expression is used to extract the partition value for the column `part`.\n\nQuerying the Delta table as this Parquet table will produce incorrect results because this query will read all the Parquet files in this table rather than only those that define a consistent snapshot of the table. You can use the manifest table to get a consistent snapshot data.\n\n#### Define view to get correct contents of the Delta table using the manifest table\n\nTo read only the rows belonging to the consistent snapshot defined in the generated manifests, you can apply a filter to keep only the rows in the Parquet table that came from the files defined in the manifest table.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    CREATE OR REPLACE VIEW my_delta_table AS\n    SELECT id, part, ...\n    FROM my_parquet_data_table\n    WHERE parquet_filename IN (\n      SELECT filename \n      FROM delta-manifest-table\n    );\n    ```\n  </TabItem>\n</Tabs>\n\nQuerying this view will provide you with a [consistent](#data-consistency) view of the Delta table.\n\n### Step 3: Update manifests\n\nWhen data in a Delta table is updated, you must regenerate the manifests using either of the following approaches:\n\n- **Update explicitly**: After all the data updates, you can run the `generate` operation to update the manifests.\n\n- **Update automatically**: You can configure a Delta table so that all write operations on the table automatically update the manifests. To enable this automatic mode, set the corresponding table property using the following SQL command.\n\n<Tabs>\n  <TabItem label=\"SQL\">\n    ```sql\n    ALTER TABLE delta.`<path-to-delta-table>` SET TBLPROPERTIES(delta.compatibility.symlinkFormatManifest.enabled=true)\n    ```\n  </TabItem>\n</Tabs>\n\n## Limitations\n\nThe Snowflake integration has known limitations in its behavior.\n\n### Data consistency\n\nWhenever Delta Lake generates updated manifests, it atomically overwrites existing manifest files. Therefore, Snowflake will always see a consistent view of the data files; it will see all of the old version files or all of the new version files. However, the granularity of the consistency guarantees depends on whether the table is partitioned or not.\n\n- **Unpartitioned tables**: All the files names are written in one manifest file which is updated atomically. In this case Snowflake will see full table snapshot consistency.\n- **Partitioned tables**: A manifest file is partitioned in the same Hive-partitioning-style directory structure as the original Delta table. This means that each partition is updated atomically, and Snowflake will see a consistent view of each partition but not a consistent view across partitions. Furthermore, since all manifests of all partitions cannot be updated together, concurrent attempts to generate manifests can lead to different partitions having manifests of different versions.\n\nDepending on what storage system you are using for Delta tables, it is possible to get incorrect results when Snowflake concurrently queries the manifest while the manifest files are being rewritten. In file system implementations that lack atomic file overwrites, a manifest file may be momentarily unavailable. Hence, use manifests with caution if their updates are likely to coincide with queries from Snowflake.\n\n### Performance\n\nThis is an experimental integration and its performance and scalability characteristics have not yet been tested.\n\n### Schema evolution\n\nDelta Lake supports schema evolution and queries on a Delta table automatically use the latest schema regardless of the schema defined in the table in the Hive metastore. However, Snowflake uses the schema defined in its table definition, and will not query with the updated schema until the table definition is updated to the new schema.\n"
  },
  {
    "path": "docs/src/content/docs/table-properties.mdx",
    "content": "---\ntitle: Delta Table Properties Reference\ndescription: Access the list of available Delta table properties.\n---\n\n| Property | Description | Data type | Default |\n|----------|-------------|-----------|---------|\n| `delta.appendOnly` | `true` for this Delta table to be append-only. If append-only, existing records cannot be deleted, and existing values cannot be updated. See [Table properties](/delta-batch/#table-properties). | `Boolean` | `false` |\n| `delta.checkpoint.writeStatsAsJson` | `true` for Delta Lake to write file statistics in checkpoints in JSON format for the `stats` column. | `Boolean` | `true` |\n| `delta.checkpoint.writeStatsAsStruct` | `true` for Delta Lake to write file statistics to checkpoints in struct format for the `stats_parsed` column and to write partition values as a struct for `partitionValues_parsed`. | `Boolean` | (none) |\n| `delta.compatibility.symlinkFormatManifest.enabled` | `true` for Delta Lake to configure the Delta table so that all write operations on the table automatically update the manifests. See [Update manifests](/presto-integration/#step-3-update-manifests). | `Boolean` | `false` |\n| `delta.dataSkippingNumIndexedCols` | The number of columns for Delta Lake to collect statistics about for data skipping. A value of `-1` means to collect statistics for all columns. Updating this property does not automatically collect statistics again; instead, it redefines the statistics schema of the Delta table. For example, it changes the behavior of future statistics collection (such as during appends and optimizations) as well as data skipping (such as ignoring column statistics beyond this number, even when such statistics exist). See [Data skipping](/optimizations-oss/#data-skipping). | `Int` | `32` |\n| `delta.deletedFileRetentionDuration` | The shortest duration for Delta Lake to keep logically deleted data files before deleting them physically. This is to prevent failures in stale readers after compactions or partition overwrites. This value should be large enough to ensure that: - It is larger than the longest possible duration of a job if you run `VACUUM` when there are concurrent readers or writers accessing the Delta table. - If you run a streaming query that reads from the table, that the query does not stop for longer than this value. Otherwise, the query may not be able to restart, as it must still read old files. See [Data retention](/delta-batch/#data-retention). | `CalendarInterval` | `interval 1 week` |\n| `delta.enableChangeDataFeed` | `true` to enable change data feed. See [Enable change data feed](/delta-change-data-feed/#enable-change-data-feed). | `Boolean` | `false` |\n| `delta.logRetentionDuration` | How long the history for a Delta table is kept. Each time a checkpoint is written, Delta Lake automatically cleans up log entries older than the retention interval. If you set this property to a large enough value, many log entries are retained. This should not impact performance as operations against the log are constant time. Operations on history are parallel but will become more expensive as the log size increases. See [Data retention](/delta-batch/#data-retention). | `CalendarInterval` | `interval 30 days` |\n| `delta.minReaderVersion` | The minimum required protocol reader version for a reader that allows to read from this Delta table. See [Versioning](/versioning). | `Int` | `1` |\n| `delta.minWriterVersion` | The minimum required protocol writer version for a writer that allows to write to this Delta table. See [Versioning](/versioning). | `Int` | `2` |\n| `delta.setTransactionRetentionDuration` | The shortest duration within which new snapshots will retain transaction identifiers (for example, `SetTransaction`s). When a new snapshot sees a transaction identifier older than or equal to the duration specified by this property, the snapshot considers it expired and ignores it. The `SetTransaction` identifier is used when making the writes idempotent. See [Idempotent table writes in foreachBatch](/delta-streaming/#idempotent-table-writes-in-foreachbatch) for details. | `CalendarInterval` | (none) |\n| `delta.checkpointPolicy` | `classic` for classic Delta Lake checkpoints. `v2` for v2 checkpoints. See [V2 Checkpoint Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#v2-spec) for details. See [Versioning](/versioning) for details around compatibility. | `String` | `classic` |\n"
  },
  {
    "path": "docs/src/content/docs/versioning.mdx",
    "content": "---\ntitle: How does Delta Lake manage feature compatibility?\ndescription: Learn how Delta table protocols are versioned.\n---\n\nimport { Aside, Tabs, TabItem } from \"@astrojs/starlight/components\";\n\nMany Delta Lake optimizations require enabling Delta Lake features on a table. Delta Lake features are always backwards compatible, so tables written by a lower Delta Lake version can always be read and written by a higher Delta Lake version. Enabling some features breaks forward compatibility with workloads running in a lower Delta Lake version. For features that break forward compatibility, you must update all workloads that reference the upgraded tables to use a compliant Delta Lake version.\n\n## What Delta Lake features require client upgrades?\n\nThe following Delta Lake features break forward compatibility. Features are enabled on a table-by-table basis.\n\n| Feature | Requires Delta Lake version or later | Documentation |\n| --- | --- | --- |\n| `CHECK` constraints | [Delta Lake 0.8.0](https://github.com/delta-io/delta/releases/tag/v0.8.0) | [CHECK constraint](/delta-constraints/#check-constraint) |\n| Generated columns | [Delta Lake 1.0.0](https://github.com/delta-io/delta/releases/tag/v1.0.0) | [Use generated columns](/delta-batch/#use-generated-columns) |\n| Column mapping | [Delta Lake 1.2.0](https://github.com/delta-io/delta/releases/tag/v1.2.0) | [Delta column mapping](/delta-column-mapping/) |\n| Change data feed | [Delta Lake 2.0.0](https://github.com/delta-io/delta/releases/tag/v2.0.0) | [Change data feed](/delta-change-data-feed/) |\n| Deletion vectors | [Delta Lake 2.3.0](https://github.com/delta-io/delta/releases/tag/v2.3.0) | [What are deletion vectors?](/delta-deletion-vectors/) |\n| Table features | [Delta Lake 2.3.0](https://github.com/delta-io/delta/releases/tag/v2.3.0) | [What are table features?](#what-are-table-features) |\n| Timestamp without Timezone | [Delta Lake 2.4.0](https://github.com/delta-io/delta/releases/tag/v2.4.0) | [TimestampNTZType](https://spark.apache.org/docs/latest/sql-ref-datatypes.html) |\n| Iceberg Compatibility V1 | [Delta Lake 3.0.0](https://github.com/delta-io/delta/releases/tag/v3.0.0) | [IcebergCompatV1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1) |\n| Iceberg Compatibility V2 | [Delta Lake 3.1.0](https://github.com/delta-io/delta/releases/tag/v3.1.0) | [IcebergCompatV2](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v2) |\n| V2 Checkpoints | [Delta Lake 3.0.0](https://github.com/delta-io/delta/releases/tag/v3.0.0) | [V2 Checkpoint Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#v2-spec) |\n| Domain metadata | [Delta Lake 3.0.0](https://github.com/delta-io/delta/releases/tag/v3.0.0) | [Domain Metadata Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#domain-metadata) |\n| Clustering | [Delta Lake 3.1.0](https://github.com/delta-io/delta/releases/tag/v3.1.0) | [Use liquid clustering for Delta tables](/delta-clustering/) |\n| Row Tracking | [Delta Lake 3.2.0](https://github.com/delta-io/delta/releases/tag/v3.2.0) | [Use row tracking for Delta tables](/row-tracking/) |\n| Type widening (Preview) | [Delta Lake 3.2.0](https://github.com/delta-io/delta/releases/tag/v3.2.0) | [Delta type widening](/delta-type-widening/) |\n| Type widening | [Delta Lake 4.0.0](https://github.com/delta-io/delta/releases/tag/v4.0.0) | [Delta type widening](/delta-type-widening/) |\n| Identity columns | [Delta Lake 3.3.0](https://github.com/delta-io/delta/releases/tag/v3.3.0) | [Use identity columns](/delta-batch/#use-identity-columns) |\n| Variant Type | [Delta Lake 4.0.0](https://github.com/delta-io/delta/releases/tag/v4.0.0) | [Delta type widening](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#variant-data-type) |\n| Variant Shredding (Preview) | [Delta Lake 4.0.0](https://github.com/delta-io/delta/releases/tag/v4.0.0) | [Variant Shredding](https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-shredding.md) |\n| Checkpoint Protection | [Delta Lake 4.0.0](https://github.com/delta-io/delta/releases/tag/v4.0.0) | [Checkpoint Protection](https://github.com/delta-io/delta/blob/master/protocol_rfcs/checkpoint-protection.md)\n\n## What is a table protocol specification?\n\nEvery Delta table has a protocol specification which indicates the set of features that the table supports. The protocol specification is used by applications that read or write the table to determine if they can handle all the features that the table supports. If an application does not know how to handle a feature that is listed as supported in the protocol of a table, then that application is not be able to read or write that table.\n\nThe protocol specification is separated into two components: the _read protocol_ and the _write protocol_.\n\n### Read protocol\n\nThe read protocol lists all features that a table supports and that an application must understand in order to read the table correctly. Upgrading the read protocol of a table requires that all reader applications support the added features.\n\n<Aside type=\"caution\" title=\"Important\">\nAll applications that write to a Delta table must be able to construct a snapshot of the table. As such, workloads that write to Delta tables must respect both reader and writer protocol requirements.\n\nIf you encounter a protocol that is unsupported by a workload on Delta Lake, you must upgrade to a higher Delta Lake implementation with more comprehensive support.\n\n</Aside>\n\n### Write protocol\n\nThe write protocol lists all features that a table supports and that an application must understand in order to write to the table correctly. Upgrading the write protocol of a table requires that all writer applications support the added features. It does not affect read-only applications, unless the read protocol is also upgraded.\n\n## Which protocols must be upgraded?\n\nSome features require upgrading both the read protocol and the write protocol. Other features only require upgrading the write protocol.\n\nAs an example, support for `CHECK` constraints is a write protocol feature: only writing applications need to know about `CHECK` constraints and enforce them.\n\nIn contrast, column mapping requires upgrading both the read and write protocols. Because the data is stored differently in the table, reader applications must understand column mapping so they can read the data correctly.\n\nFor more on upgrading, see [Upgrading protocol versions](#upgrading-protocol-versions).\n\n## What are table features?\n\nIn Delta Lake 2.3.0 and above, Delta Lake table features introduce granular flags specifying which features are supported by a given table. Table features are the successor to protocol versions and are designed with the goal of improved flexibility for clients that read and write Delta Lake. See [What is a protocol version?](#what-is-a-protocol-version).\n\n<Aside type=\"note\">\n  Table features have protocol version requirements. See [Features by protocol\n  version](#features-by-protocol-version).\n</Aside>\n\nA Delta table feature is a marker that indicates that the table supports a particular feature. Every feature is either a write protocol feature (meaning it only upgrades the write protocol) or a read/write protocol feature (meaning both read and write protocols are upgraded to enable the feature).\n\nTo learn more about supported table features in Delta Lake, see the [Delta Lake protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#valid-feature-names-in-table-features).\n\n## Do table features change how Delta Lake features are enabled?\n\nIf you only interact with Delta tables through Delta Lake, you can continue to track support for Delta Lake features using minimum Delta Lake requirements. If you read and write from Delta tables using other systems, you might need to consider how table features impact compatibility, because there is a risk that the system could not understand the upgraded protocol versions.\n\n## What is a protocol version?\n\nA protocol version is a protocol number that indicates a particular grouping of table features. In Delta Lake 2.3.0 and below, you cannot enable table features individually. Protocol versions bundle a group of features.\n\nDelta tables specify a separate protocol version for read protocol and write protocol. The transaction log for a Delta table contains protocol versioning information that supports Delta Lake evolution.\n\nThe protocol versions bundle all features from previous protocols. See [Features by protocol version](#features-by-protocol-version).\n\n<Aside type=\"note\">\n  Starting with writer version 7 and reader version 3, Delta Lake has introduced\n  the concept of table features. Using table features, you can now choose to\n  only enable those features that are supported by other clients in your data\n  ecosystem. See [What are table features?](#what-are-table-features).\n</Aside>\n\n## Features by protocol version\n\nThe following table shows minimum protocol versions required for Delta Lake features.\n\n| Feature | `minWriterVersion` | `minReaderVersion` | Documentation |\n| --- | --- | --- | --- |\n| Basic functionality | 2 | 1 | [Welcome to the Delta Lake documentation](/) |\n| `CHECK` constraints | 3 | 1 | [CHECK constraint](/delta-constraints/#check-constraint) |\n| Change data feed | 4 | 1 | [Change data feed](/delta-change-data-feed/) |\n| Generated columns | 4 | 1 | [Use generated columns](/delta-batch/#use-generated-columns) |\n| Column mapping | 5 | 2 | [Delta column mapping](/delta-column-mapping/) |\n| Identity columns | 6 | 1 | [Use identity columns](/delta-batch/#use-identity-columns) |\n| Table features read | 7 | 1 | [What are table features?](#what-are-table-features) |\n| Table features write | 7 | 3 | [What are table features?](#what-are-table-features) |\n| Deletion vectors | 7 | 3 | [What are deletion vectors?](/delta-deletion-vectors/) |\n| Timestamp without Timezone | 7 | 3 | [TimestampNTZType](https://spark.apache.org/docs/latest/sql-ref-datatypes.html) |\n| Iceberg Compatibility V1 | 7 | 2 | [IcebergCompatV1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1) |\n| V2 Checkpoints | 7 | 3 | [V2 Checkpoint Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#v2-spec) |\n| Vacuum Protocol Check | 7 | 3 | [Vacuum Protocol Check Spec](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#vacuum-protocol-check) |\n| Row Tracking | 7 | 3 | [Use row tracking for Delta tables](/delta-row-tracking/) |\n| Type widening (Preview) | 7 | 3 | [Delta type widening](/delta-type-widening/) |\n| Type widening | 7 | 3 | [Delta type widening](/delta-type-widening/) |\n| Variant Type | 7 | 3 | [Variant Type](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#variant-data-type) |\n| Variant Shredding (Preview) | 7 | 3 | [Variant Shredding](https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-shredding.md) |\n\n## Upgrading protocol versions\n\nYou can choose to manually update a table to a newer protocol version. We recommend using the lowest protocol versions that support the Delta Lake features required for your table. Upgrading the writer protocol might cause less disruption than upgrading the reader protocol since systems and workloads using older Delta Lake versions can still read from tables, even if they do not support the updated writer protocol.\n\n<Aside\n  type=\"caution\"\n  title=\"Warning\"\n>\n  Protocol version upgrades are irreversible, and upgrading the protocol version\n  might break the existing Delta Lake table readers, writers, or both. We\n  recommend you upgrade specific tables only when needed, such as to opt-in to\n  new features in Delta Lake. You should also check to make sure that all of\n  your current and future production tools support Delta Lake tables with the\n  new protocol version.\n</Aside>\n\nTo upgrade a table to a newer protocol version, use the `DeltaTable.upgradeTableProtocol` method:\n\n<Tabs syncKey=\"code-examples\">\n  <TabItem label=\"SQL\">\n    ```sql\n    -- Upgrades the reader protocol version to 1 and the writer protocol version to 3.\n    ALTER TABLE <table_identifier> SET TBLPROPERTIES('delta.minReaderVersion' = '1', 'delta.minWriterVersion' = '3')\n    ```\n  </TabItem>\n\n<TabItem label=\"Python\">\n  ```python \n  from delta.tables import DeltaTable delta =\n  DeltaTable.forPath(spark, \"path_to_table\") # or DeltaTable.forName\n  delta.upgradeTableProtocol(1, 3) # upgrades to readerVersion=1,\n  writerVersion=3 \n  ```\n</TabItem>\n\n  <TabItem label=\"Scala\">\n    ```scala\n    import io.delta.tables.DeltaTable\n    val delta = DeltaTable.forPath(spark, \"path_to_table\") // or DeltaTable.forName\n    delta.upgradeTableProtocol(1, 3) // Upgrades to readerVersion=1, writerVersion=3.\n    ```\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/src/content.config.ts",
    "content": "import { defineCollection } from \"astro:content\";\nimport { docsLoader } from \"@astrojs/starlight/loaders\";\nimport { docsSchema } from \"@astrojs/starlight/schema\";\n\nexport const collections = {\n  docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),\n};\n"
  },
  {
    "path": "docs/src/env.d.ts",
    "content": "/* eslint-disable @typescript-eslint/triple-slash-reference */\n/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />\n"
  },
  {
    "path": "docs/src/pages/robots.txt.ts",
    "content": "import type { APIRoute } from \"astro\";\n\nconst getRobotsTxt = (sitemapURL: URL) => `\nUser-agent: *\nAllow: /\n\nSitemap: ${sitemapURL.href}\n`;\n\nexport const GET: APIRoute = ({ site }) => {\n  const sitemapURL = new URL(\"sitemap-index.xml\", site);\n  return new Response(getRobotsTxt(sitemapURL));\n};\n"
  },
  {
    "path": "docs/src/styles/custom.css",
    "content": "/* Dark mode colors. */\n:root {\n  --sl-color-accent-low: #00273c;\n  --sl-color-accent: #0072a4;\n  --sl-color-accent-high: #90d2fd;\n  --sl-color-white: #ffffff;\n  --sl-color-gray-1: #eceef2;\n  --sl-color-gray-2: #c0c2c7;\n  --sl-color-gray-3: #888b96;\n  --sl-color-gray-4: #545861;\n  --sl-color-gray-5: #353841;\n  --sl-color-gray-6: #24272f;\n  --sl-color-black: #17181c;\n}\n/* Light mode colors. */\n:root[data-theme=\"light\"] {\n  --sl-color-accent-low: #aedeff;\n  --sl-color-accent: #0074a7;\n  --sl-color-accent-high: #003652;\n  --sl-color-white: #17181c;\n  --sl-color-gray-1: #24272f;\n  --sl-color-gray-2: #353841;\n  --sl-color-gray-3: #545861;\n  --sl-color-gray-4: #888b96;\n  --sl-color-gray-5: #c0c2c7;\n  --sl-color-gray-6: #eceef2;\n  --sl-color-gray-7: #f5f6f8;\n  --sl-color-black: #ffffff;\n}\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strictest\"\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "## Delta Lake examples\nIn this folder there are examples taken from the delta.io quickstart guide and docs. They are available in both Scala and Python and can be run if the prerequisites are satisfied.\n\n### Prerequisites\n* See [Set up Apache Spark with Delta Lake](https://docs.delta.io/latest/quick-start.html#set-up-apache-spark-with-delta-lake).\n\n### Instructions\n* To run an example in Python run `spark-submit --packages io.delta:delta-spark_2.12:{Delta Lake version} PATH/TO/EXAMPLE`\n* To run the Scala examples, `cd examples/scala` and run `./build/sbt \"runMain example.{Example class name}\"` e.g. `./build/sbt \"runMain example.Quickstart\"`\n"
  },
  {
    "path": "examples/python/change_data_feed.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql.functions import col, expr\nfrom delta.tables import DeltaTable\nimport shutil\n\n\npath = \"/tmp/delta-change-data-feed/student\"\notherPath = \"/tmp/delta-change-data-feed/student_source\"\n\n# Enable SQL commands and Update/Delete/Merge for the current spark session.\n# we need to set the following configs\nspark = SparkSession.builder \\\n    .appName(\"Change Data Feed\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .getOrCreate()\n\n\ndef cleanup():\n    shutil.rmtree(path, ignore_errors=True)\n    shutil.rmtree(otherPath, ignore_errors=True)\n    spark.sql(\"DROP TABLE IF EXISTS student\")\n    spark.sql(\"DROP TABLE IF EXISTS student_source\")\n\n\ndef read_cdc_by_table_name(starting_version):\n    return spark.read.format(\"delta\") \\\n        .option(\"readChangeFeed\", \"true\") \\\n        .option(\"startingVersion\", str(starting_version)) \\\n        .table(\"student\") \\\n        .orderBy(\"_change_type\", \"id\")\n\n\ndef stream_cdc_by_table_name(starting_version):\n    return spark.readStream.format(\"delta\") \\\n        .option(\"readChangeFeed\", \"true\") \\\n        .option(\"startingVersion\", str(starting_version)) \\\n        .table(\"student\") \\\n        .writeStream \\\n        .format(\"console\") \\\n        .option(\"numRows\", 1000) \\\n        .start()\n\n\ncleanup()\n\ntry:\n    # =============== Create student table ===============\n    spark.sql('''CREATE TABLE student (id INT, name STRING, age INT)\n                 USING DELTA\n                 PARTITIONED BY (age)\n                 TBLPROPERTIES (delta.enableChangeDataFeed = true)\n                 LOCATION '{0}'\n             '''.format(path))\n\n    spark.range(0, 10) \\\n        .selectExpr(\n            \"CAST(id as INT) as id\",\n            \"CAST(id as STRING) as name\",\n            \"CAST(id % 4 + 18 as INT) as age\") \\\n        .write.format(\"delta\").mode(\"append\").save(path)  # v1\n\n    # =============== Show table data + changes ===============\n\n    print(\"(v1) Initial Table\")\n    spark.read.format(\"delta\").load(path).orderBy(\"id\").show()\n\n    print(\"(v1) CDC changes\")\n    read_cdc_by_table_name(1).show()\n\n    table = DeltaTable.forPath(spark, path)\n\n    # =============== Perform UPDATE ===============\n\n    print(\"(v2) Updated id -> id + 1\")\n    table.update(set={\"id\": expr(\"id + 1\")})  # v2\n    read_cdc_by_table_name(2).show()\n\n    # =============== Perform DELETE ===============\n\n    print(\"(v3) Deleted where id >= 7\")\n    table.delete(condition=expr(\"id >= 7\"))  # v3\n    read_cdc_by_table_name(3).show()\n\n    # =============== Perform partition DELETE ===============\n\n    print(\"(v4) Deleted where age = 18\")\n    table.delete(condition=expr(\"age = 18\"))  # v4, partition delete\n    read_cdc_by_table_name(4).show()\n\n    # =============== Create source table for MERGE ===============\n\n    spark.sql('''CREATE TABLE student_source (id INT, name STRING, age INT)\n                 USING DELTA\n                 LOCATION '{0}'\n             '''.format(otherPath))\n    spark.range(0, 3) \\\n        .selectExpr(\n            \"CAST(id as INT) as id\",\n            \"CAST(id as STRING) as name\",\n            \"CAST(id % 4 + 18 as INT) as age\") \\\n        .write.format(\"delta\").mode(\"append\").saveAsTable(\"student_source\")\n    source = spark.sql(\"SELECT * FROM student_source\")\n\n    # =============== Perform MERGE ===============\n\n    table.alias(\"target\") \\\n        .merge(\n            source.alias(\"source\"),\n            \"target.id = source.id\")\\\n        .whenMatchedUpdate(set={\"id\": \"source.id\", \"age\": \"source.age + 10\"}) \\\n        .whenNotMatchedInsertAll() \\\n        .execute() # v5\n    print(\"(v5) Merged with a source table\")\n    read_cdc_by_table_name(5).show()\n\n    # =============== Stream changes ===============\n\n    print(\"Streaming by table name\")\n    cdfStream = stream_cdc_by_table_name(0)\n    cdfStream.awaitTermination(10)\n    cdfStream.stop()\n\nfinally:\n    cleanup()\n    spark.stop()\n"
  },
  {
    "path": "examples/python/delta_connect.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"\nTo run this example you must follow these steps:\n\nRequirements:\n- Using Java 17\n- Spark 4.0.0-preview1+\n- delta-spark (python package) 4.0.0rc1+ and pyspark 4.0.0.dev1+\n\n(1) Start a local Spark connect server using this command:\nsbin/start-connect-server.sh \\\n  --packages org.apache.spark:spark-connect_2.13:4.0.0-preview1,io.delta:delta-connect-server_2.13:{DELTA_VERSION},io.delta:delta-spark_2.13:{DELTA_VERSION},com.google.protobuf:protobuf-java:3.25.1 \\\n  --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n  --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\" \\\n  --conf \"spark.connect.extensions.relation.classes\"=\"org.apache.spark.sql.connect.delta.DeltaRelationPlugin\" \\\n  --conf \"spark.connect.extensions.command.classes\"=\"org.apache.spark.sql.connect.delta.DeltaCommandPlugin\"\n* Be sure to replace DELTA_VERSION with the version you are using\n\n(2) Set the SPARK_REMOTE environment variable to point to your local Spark server\nexport SPARK_REMOTE=\"sc://localhost:15002\"\n\n(3) Run this file i.e. python3 examples/python/delta_connect.py\n\"\"\"\n\nimport os\nfrom pyspark.sql import SparkSession\nfrom delta.tables import DeltaTable\nimport shutil\n\nfilePath = \"/tmp/delta_connect\"\ntableName = \"delta_connect_table\"\n\ndef assert_dataframe_equals(df1, df2):\n    assert(df1.collect().sort() == df2.collect().sort())\n\ndef cleanup(spark):\n    shutil.rmtree(filePath, ignore_errors=True)\n    spark.sql(f\"DROP TABLE IF EXISTS {tableName}\")\n\n# --------------------- Set up Spark Connect spark session ------------------------\n\nassert os.getenv(\"SPARK_REMOTE\"), \"Must point to Spark Connect server using SPARK_REMOTE\"\n\nspark = SparkSession.builder \\\n    .appName(\"delta_connect\") \\\n    .remote(os.getenv(\"SPARK_REMOTE\")) \\\n    .getOrCreate()\n\n# Clean up any previous runs\ncleanup(spark)\n\n# -------------- Try reading non-existent table (should fail with an exception) ----------------\n\n# Using forPath\ntry:\n    DeltaTable.forPath(spark, filePath).toDF().show()\nexcept Exception as e:\n    assert \"DELTA_MISSING_DELTA_TABLE\" in str(e)\nelse:\n    assert False, \"Expected exception to be thrown for missing table\"\n\n# Using forName\ntry:\n    DeltaTable.forName(spark, tableName).toDF().show()\nexcept Exception as e:\n    assert \"DELTA_MISSING_DELTA_TABLE\" in str(e)\nelse:\n    assert False, \"Expected exception to be thrown for missing table\"\n\n# ------------------------ Write basic table and check that results match ----------------------\n\n# By table name\nspark.range(5).write.format(\"delta\").saveAsTable(tableName)\nassert_dataframe_equals(DeltaTable.forName(spark, tableName).toDF(), spark.range(5))\nassert_dataframe_equals(spark.read.format(\"delta\").table(tableName), spark.range(5))\nassert_dataframe_equals(spark.sql(f\"SELECT * FROM {tableName}\"), spark.range(5))\n\n# By table path\nspark.range(10).write.format(\"delta\").save(filePath)\nassert_dataframe_equals(DeltaTable.forPath(spark, filePath).toDF(), spark.range(10))\nassert_dataframe_equals(spark.read.format(\"delta\").load(filePath), spark.range(10))\nassert_dataframe_equals(spark.sql(f\"SELECT * FROM delta.`{filePath}`\"), spark.range(10))\n\n# ---------------------------------- Clean up ----------------------------------------\ncleanup(spark)\n"
  },
  {
    "path": "examples/python/image_storage.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This example shows\n# 1 - How to load the TensorFlow Flowers Images into a dataframe\n# 2 - Manipulate the dataframe\n# 3 - Write the dataframe to a Delta Lake table\n# 4 - Read the new Delta Lake table\n\nimport pyspark.sql.functions as fn\nfrom pyspark.sql import SparkSession\nfrom delta import configure_spark_with_delta_pip\nimport shutil\nfrom urllib import request\nimport os\n\n# To run this example directly, set up the spark session using the following 2 commands\n# You will need to run using Python3\n# You will also need to install the python packages pyspark and delta-spark, we advise using pip\nbuilder = (\n  SparkSession.builder\n    .appName('image_storage')\n    .config('spark.sql.extensions', 'io.delta.sql.DeltaSparkSessionExtension')\n    .config('spark.sql.catalog.spark_catalog', 'org.apache.spark.sql.delta.catalog.DeltaCatalog')\n)\n\n# This is only for testing staged release artifacts. Ignore this completely.\nif os.getenv('EXTRA_MAVEN_REPO'):\n    builder = builder.config(\"spark.jars.repositories\", os.getenv('EXTRA_MAVEN_REPO'))\n\nspark = configure_spark_with_delta_pip(builder).getOrCreate()\n\n# Flowers dataset from the TensorFlow team - https://www.tensorflow.org/datasets/catalog/tf_flowers\nimageGzipUrl = \"https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz\"\nimageGzipPath = \"/tmp/flower_photos.tgz\"\nimagePath = \"/tmp/image-folder\"\ndeltaPath = \"/tmp/delta-table\"\n\n# Clear previous run's zipper file, image folder and delta tables\nif os.path.exists(imageGzipPath):\n  os.remove(imageGzipPath)\nshutil.rmtree(imagePath, ignore_errors=True)\nshutil.rmtree(deltaPath, ignore_errors=True)\n\nrequest.urlretrieve(imageGzipUrl, imageGzipPath)\nshutil.unpack_archive(imageGzipPath, imagePath)\n\n# read the images from the flowers dataset\nimages = spark.read.format(\"binaryFile\").\\\n  option(\"recursiveFileLookup\", \"true\").\\\n  option(\"pathGlobFilter\", \"*.jpg\").\\\n  load(imagePath)\n\n# Knowing the file path, extract the flower type and filename using substring_index\n# Remember, Spark dataframes are immutable, here we are just reusing the images dataframe\nimages = images.withColumn(\"flowerType_filename\", fn.substring_index(images.path, \"/\", -2))\nimages = images.withColumn(\"flowerType\", fn.substring_index(images.flowerType_filename, \"/\", 1))\nimages = images.withColumn(\"filename\", fn.substring_index(images.flowerType_filename, \"/\", -1))\nimages = images.drop(\"flowerType_filename\")\nimages.show()\n\n# Select the columns we want to write out to\ndf = images.select(\"path\", \"content\", \"flowerType\", \"filename\").repartition(4)\ndf.show()\n\n# Write out the delta table to the given path, this will overwrite any table that is currently there\ndf.write.format(\"delta\").mode(\"overwrite\").save(deltaPath)\n\n# Reads the delta table that was just written\ndfDelta = spark.read.format(\"delta\").load(deltaPath)\ndfDelta.show()\n\n# Cleanup\nif os.path.exists(imageGzipPath):\n  os.remove(imageGzipPath)\nshutil.rmtree(imagePath)\nshutil.rmtree(deltaPath)\n"
  },
  {
    "path": "examples/python/missing_delta_storage_jar.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nimport shutil\n\npath = \"/tmp/delta-table/missing_logstore_jar\"\n\ntry:\n    # Clear any previous runs\n    shutil.rmtree(path, ignore_errors=True)\n\n    spark = SparkSession.builder \\\n        .appName(\"missing logstore jar\") \\\n        .master(\"local[*]\") \\\n        .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n        .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n        .getOrCreate()\n\n    spark.range(0, 5).write.format(\"delta\").save(path)\n\nexcept Exception as e:\n    assert \"Please ensure that the delta-storage dependency is included.\" in str(e)\n    print(\"SUCCESS - error was thrown, as expected\")\n\nelse:\n    assert False, \"The write to the delta table should have thrown without the delta-storage JAR.\"\n\nfinally:\n    # cleanup\n    shutil.rmtree(path, ignore_errors=True)\n"
  },
  {
    "path": "examples/python/quickstart.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql.functions import col, expr\nfrom delta.tables import DeltaTable\nimport shutil\n\n# Clear any previous runs\nshutil.rmtree(\"/tmp/delta-table\", ignore_errors=True)\n\n# Enable SQL commands and Update/Delete/Merge for the current spark session.\n# we need to set the following configs\nspark = SparkSession.builder \\\n    .appName(\"quickstart\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .getOrCreate()\n\n# Create a table\nprint(\"############# Creating a table ###############\")\ndata = spark.range(0, 5)\ndata.write.format(\"delta\").save(\"/tmp/delta-table\")\n\n# Read the table\nprint(\"############ Reading the table ###############\")\ndf = spark.read.format(\"delta\").load(\"/tmp/delta-table\")\ndf.show()\n\n# Upsert (merge) new data\nprint(\"########### Upsert new data #############\")\nnewData = spark.range(0, 20)\n\ndeltaTable = DeltaTable.forPath(spark, \"/tmp/delta-table\")\n\ndeltaTable.alias(\"oldData\")\\\n    .merge(\n    newData.alias(\"newData\"),\n    \"oldData.id = newData.id\")\\\n    .whenMatchedUpdate(set={\"id\": col(\"newData.id\")})\\\n    .whenNotMatchedInsert(values={\"id\": col(\"newData.id\")})\\\n    .execute()\n\ndeltaTable.toDF().show()\n\n# Update table data\nprint(\"########## Overwrite the table ###########\")\ndata = spark.range(5, 10)\ndata.write.format(\"delta\").mode(\"overwrite\").save(\"/tmp/delta-table\")\ndeltaTable.toDF().show()\n\ndeltaTable = DeltaTable.forPath(spark, \"/tmp/delta-table\")\n\n# Update every even value by adding 100 to it\nprint(\"########### Update to the table(add 100 to every even value) ##############\")\ndeltaTable.update(\n    condition=expr(\"id % 2 == 0\"),\n    set={\"id\": expr(\"id + 100\")})\n\ndeltaTable.toDF().show()\n\n# Delete every even value\nprint(\"######### Delete every even value ##############\")\ndeltaTable.delete(condition=expr(\"id % 2 == 0\"))\ndeltaTable.toDF().show()\n\n# Read old version of data using time travel\nprint(\"######## Read old data using time travel ############\")\ndf = spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(\"/tmp/delta-table\")\ndf.show()\n\n# cleanup\nshutil.rmtree(\"/tmp/delta-table\")\n"
  },
  {
    "path": "examples/python/quickstart_sql.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\n\ntableName = \"tbltestpython\"\n\n# Enable SQL/DML commands and Metastore tables for the current spark session.\n# We need to set the following configs\n\nspark = SparkSession.builder \\\n    .appName(\"quickstart_sql\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .getOrCreate()\n\n# Clear any previous runs\nspark.sql(\"DROP TABLE IF EXISTS \" + tableName)\nspark.sql(\"DROP TABLE IF EXISTS newData\")\n\ntry:\n    # Create a table\n    print(\"############# Creating a table ###############\")\n    spark.sql(\"CREATE TABLE %s(id LONG) USING delta\" % tableName)\n    spark.sql(\"INSERT INTO %s VALUES 0, 1, 2, 3, 4\" % tableName)\n\n    # Read the table\n    print(\"############ Reading the table ###############\")\n    spark.sql(\"SELECT * FROM %s\" % tableName).show()\n\n    # Upsert (merge) new data\n    print(\"########### Upsert new data #############\")\n    spark.sql(\"CREATE TABLE newData(id LONG) USING parquet\")\n    spark.sql(\"INSERT INTO newData VALUES 3, 4, 5, 6\")\n\n    spark.sql('''MERGE INTO {0} USING newData\n            ON {0}.id = newData.id\n            WHEN MATCHED THEN\n              UPDATE SET {0}.id = newData.id\n            WHEN NOT MATCHED THEN INSERT *\n        '''.format(tableName))\n\n    spark.sql(\"SELECT * FROM %s\" % tableName).show()\n\n    # Update table data\n    print(\"########## Overwrite the table ###########\")\n    spark.sql(\"INSERT OVERWRITE %s select * FROM (VALUES 5, 6, 7, 8, 9) x (id)\" % tableName)\n    spark.sql(\"SELECT * FROM %s\" % tableName).show()\n\n    # Update every even value by adding 100 to it\n    print(\"########### Update to the table(add 100 to every even value) ##############\")\n    spark.sql(\"UPDATE {0} SET id = (id + 100) WHERE (id % 2 == 0)\".format(tableName))\n    spark.sql(\"SELECT * FROM %s\" % tableName).show()\n\n    # Delete every even value\n    print(\"######### Delete every even value ##############\")\n    spark.sql(\"DELETE FROM {0} WHERE (id % 2 == 0)\".format(tableName))\n    spark.sql(\"SELECT * FROM %s\" % tableName).show()\n\n    # Read old version of data using time travel\n    print(\"######## Read old data using time travel ############\")\n    df = spark.read.format(\"delta\").option(\"versionAsOf\", 0).table(tableName)\n    df.show()\n\nfinally:\n    # cleanup\n    spark.sql(\"DROP TABLE \" + tableName)\n    spark.sql(\"DROP TABLE IF EXISTS newData\")\n    spark.stop()\n"
  },
  {
    "path": "examples/python/quickstart_sql_on_paths.py",
    "content": "\nfrom pyspark.sql import SparkSession\nimport shutil\n\ntable_dir = \"/tmp/delta-table\"\n# Clear any previous runs\nshutil.rmtree(table_dir, ignore_errors=True)\n\n# Enable SQL/DML commands and Metastore tables for the current spark session.\n# We need to set the following configs\n\nspark = SparkSession.builder \\\n    .appName(\"quickstart_sql_on_paths\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .getOrCreate()\n\n# Clear any previous runs\nspark.sql(\"DROP TABLE IF EXISTS newData\")\n\ntry:\n    # Create a table\n    print(\"############# Creating a table ###############\")\n    spark.sql(\"CREATE TABLE delta.`%s`(id LONG) USING delta\" % table_dir)\n    spark.sql(\"INSERT INTO delta.`%s` VALUES 0, 1, 2, 3, 4\" % table_dir)\n\n    # Read the table\n    print(\"############ Reading the table ###############\")\n    spark.sql(\"SELECT * FROM delta.`%s`\" % table_dir).show()\n\n    # Upsert (merge) new data\n    print(\"########### Upsert new data #############\")\n    spark.sql(\"CREATE TABLE newData(id LONG) USING parquet\")\n    spark.sql(\"INSERT INTO newData VALUES 3, 4, 5, 6\")\n\n    spark.sql('''MERGE INTO delta.`{0}` AS data USING newData\n            ON data.id = newData.id\n            WHEN MATCHED THEN\n              UPDATE SET data.id = newData.id\n            WHEN NOT MATCHED THEN INSERT *\n        '''.format(table_dir))\n\n    spark.sql(\"SELECT * FROM delta.`%s`\" % table_dir).show()\n\n    # Update table data\n    print(\"########## Overwrite the table ###########\")\n    spark.sql(\"INSERT OVERWRITE delta.`%s` select * FROM (VALUES 5, 6, 7, 8, 9) x (id)\" % table_dir)\n    spark.sql(\"SELECT * FROM delta.`%s`\" % table_dir).show()\n\n    # Update every even value by adding 100 to it\n    print(\"########### Update to the table(add 100 to every even value) ##############\")\n    spark.sql(\"UPDATE delta.`{0}` SET id = (id + 100) WHERE (id % 2 == 0)\".format(table_dir))\n    spark.sql(\"SELECT * FROM delta.`%s`\" % table_dir).show()\n\n    # Delete every even value\n    print(\"######### Delete every even value ##############\")\n    spark.sql(\"DELETE FROM delta.`{0}` WHERE (id % 2 == 0)\".format(table_dir))\n    spark.sql(\"SELECT * FROM delta.`%s`\" % table_dir).show()\n\nfinally:\n    # cleanup\n    spark.sql(\"DROP TABLE IF EXISTS newData\")\n    spark.stop()\n"
  },
  {
    "path": "examples/python/streaming.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql.functions import col\nfrom delta.tables import DeltaTable\nimport shutil\nimport random\n\n\n# Enable SQL commands and Update/Delete/Merge for the current spark session.\n# we need to set the following configs\nspark = SparkSession.builder \\\n    .appName(\"streaming\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .getOrCreate()\n\nshutil.rmtree(\"/tmp/delta-streaming/\", ignore_errors=True)\n\n# Create a table(key, value) of some data\ndata = spark.range(8)\ndata = data.withColumn(\"value\", data.id + random.randint(0, 5000))\ndata.write.format(\"delta\").save(\"/tmp/delta-streaming/delta-table\")\n\n# Stream writes to the table\nprint(\"####### Streaming write ######\")\nstreamingDf = spark.readStream.format(\"rate\").load()\nstream = streamingDf.selectExpr(\"value as id\").writeStream\\\n    .format(\"delta\")\\\n    .option(\"checkpointLocation\", \"/tmp/delta-streaming/checkpoint\")\\\n    .start(\"/tmp/delta-streaming/delta-table2\")\nstream.awaitTermination(10)\nstream.stop()\n\n# Stream reads from a table\nprint(\"##### Reading from stream ######\")\nstream2 = spark.readStream.format(\"delta\").load(\"/tmp/delta-streaming/delta-table2\")\\\n    .writeStream\\\n    .format(\"console\")\\\n    .start()\nstream2.awaitTermination(10)\nstream2.stop()\n\n# Streaming aggregates in Update mode\nprint(\"####### Streaming upgrades in update mode ########\")\n\n\n# Function to upsert microBatchOutputDF into Delta Lake table using merge\ndef upsertToDelta(microBatchOutputDF, batchId):\n    t = deltaTable.alias(\"t\").merge(microBatchOutputDF.alias(\"s\"), \"s.id = t.id\")\\\n        .whenMatchedUpdateAll()\\\n        .whenNotMatchedInsertAll()\\\n        .execute()\n\n\nstreamingAggregatesDF = spark.readStream.format(\"rate\").load()\\\n    .withColumn(\"id\", col(\"value\") % 10)\\\n    .drop(\"timestamp\")\n# Write the output of a streaming aggregation query into Delta Lake table\ndeltaTable = DeltaTable.forPath(spark, \"/tmp/delta-streaming/delta-table\")\nprint(\"#############  Original Delta Table ###############\")\ndeltaTable.toDF().show()\nstream3 = streamingAggregatesDF.writeStream\\\n    .format(\"delta\") \\\n    .foreachBatch(upsertToDelta) \\\n    .outputMode(\"update\") \\\n    .start()\nstream3.awaitTermination(10)\nstream3.stop()\nprint(\"########### DeltaTable after streaming upsert #########\")\ndeltaTable.toDF().show()\n\n# Streaming append and concurrent repartition using  data change = false\n# tbl1 is the sink and tbl2 is the source\nprint(\"############ Streaming appends with concurrent table repartition  ##########\")\ntbl1 = \"/tmp/delta-streaming/delta-table4\"\ntbl2 = \"/tmp/delta-streaming/delta-table5\"\nnumRows = 10\nspark.range(numRows).write.mode(\"overwrite\").format(\"delta\").save(tbl1)\nspark.read.format(\"delta\").load(tbl1).show()\nspark.range(numRows, numRows * 10).write.mode(\"overwrite\").format(\"delta\").save(tbl2)\n\n\n# Start reading tbl2 as a stream and do a streaming write to tbl1\n# Prior to Delta 0.5.0 this would throw StreamingQueryException: Detected a data update in the\n# source table. This is currently not supported.\nstream4 = spark.readStream.format(\"delta\").load(tbl2).writeStream.format(\"delta\")\\\n    .option(\"checkpointLocation\", \"/tmp/delta-streaming/checkpoint/tbl1\") \\\n    .outputMode(\"append\") \\\n    .start(tbl1)\n\n# repartition table while streaming job is running\nspark.read.format(\"delta\").load(tbl2).repartition(10).write\\\n    .format(\"delta\")\\\n    .mode(\"overwrite\")\\\n    .option(\"dataChange\", \"false\")\\\n    .save(tbl2)\n\nstream4.awaitTermination(10)\nstream4.stop()\nprint(\"######### After streaming write #########\")\nspark.read.format(\"delta\").load(tbl1).show()\n\n# cleanup\nshutil.rmtree(\"/tmp/delta-streaming/\", ignore_errors=True)\n"
  },
  {
    "path": "examples/python/table_exists.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql.utils import AnalysisException\nimport shutil\n\ndef exists(spark, filepath):\n    \"\"\"Checks if a delta table exists at `filepath`\"\"\"\n    try:\n        spark.read.load(path=filepath, format=\"delta\")\n    except AnalysisException as exception:\n        if \"is not a Delta table\" in str(exception) or \"Path does not exist\" in str(exception):\n            return False\n        raise exception\n    return True\n\nspark = SparkSession.builder \\\n    .appName(\"table_exists\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .getOrCreate()\n\nfilepath = \"/tmp/table_exists\"\n\n# Clear any previous runs\nshutil.rmtree(filepath, ignore_errors=True)\n\n# Verify table doesn't exist yet\nprint(f\"Verifying table does not exist at {filepath}\")\nassert not exists(spark, filepath)\n\n# Create a delta table at filepath\nprint(f\"Creating delta table at {filepath}\")\ndata = spark.range(0, 5)\ndata.write.format(\"delta\").save(filepath)\n\n# Verify table now exists\nprint(f\"Verifying table exists at {filepath}\")\nassert exists(spark, filepath)\n\n# Clean up\nshutil.rmtree(filepath)\n"
  },
  {
    "path": "examples/python/using_with_pip.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport shutil\n\n# flake8: noqa\nimport os\nfrom pyspark.sql import SparkSession\nfrom delta import *\n\nbuilder = SparkSession.builder \\\n    .appName(\"with-pip\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(\"spark.jars.repositories\", \\\n        (\"https://maven-central.storage-download.googleapis.com/maven2/,\"\n            \"https://repo1.maven.org/maven2/\")\\\n    )\n\n# This is only for testing staged release artifacts. Ignore this completely.\nif os.getenv('EXTRA_MAVEN_REPO'):\n    builder = builder.config(\"spark.jars.repositories\", os.getenv('EXTRA_MAVEN_REPO'))\n\n# This configuration tells Spark to download the Delta Lake JAR that is needed to operate\n# in Spark. Use this only when the Pypi package Delta Lake is locally installed with pip.\n# This configuration is not needed if the this python program is executed with\n# spark-submit or pyspark shell with the --package arguments.\nspark = configure_spark_with_delta_pip(builder).getOrCreate()\n\n\n# Clear previous run's delta-tables\nshutil.rmtree(\"/tmp/delta-table\", ignore_errors=True)\n\nprint(\"########### Create a Parquet table ##############\")\ndata = spark.range(0, 5)\ndata.write.format(\"parquet\").save(\"/tmp/delta-table\")\n\nprint(\"########### Convert to Delta ###########\")\nDeltaTable.convertToDelta(spark, \"parquet.`/tmp/delta-table`\")\n\nprint(\"########### Read table with DataFrames ###########\")\ndf = spark.read.format(\"delta\").load(\"/tmp/delta-table\")\ndf.show()\n\nprint(\"########### Read table with DeltaTable ###########\")\ndeltaTable = DeltaTable.forPath(spark, \"/tmp/delta-table\")\ndeltaTable.toDF().show()\n\n\nprint(\"########### All import submodules work ###########\")\nfrom delta.exceptions import MetadataChangedException\n\nspark.stop()\n\n# cleanup\nshutil.rmtree(\"/tmp/delta-table\")\n"
  },
  {
    "path": "examples/python/utilities.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nfrom delta.tables import DeltaTable\nimport shutil\n\nspark = SparkSession.builder \\\n    .appName(\"utilities\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(\"spark.sql.sources.parallelPartitionDiscovery.parallelism\", \"4\") \\\n    .getOrCreate()\n\n# Clear previous run's delta-tables\nshutil.rmtree(\"/tmp/delta-table\", ignore_errors=True)\n\n# Create a table\nprint(\"########### Create a Parquet table ##############\")\ndata = spark.range(0, 5)\ndata.write.format(\"parquet\").save(\"/tmp/delta-table\")\n\n# Convert to delta\nprint(\"########### Convert to Delta ###########\")\nDeltaTable.convertToDelta(spark, \"parquet.`/tmp/delta-table`\")\n\n# Read the table\ndf = spark.read.format(\"delta\").load(\"/tmp/delta-table\")\ndf.show()\n\ndeltaTable = DeltaTable.forPath(spark, \"/tmp/delta-table\")\nprint(\"######## Vacuum the table ########\")\ndeltaTable.vacuum()\n\nprint(\"######## Describe history for the table ######\")\ndeltaTable.history().show()\n\nprint(\"######## Describe details for the table ######\")\ndeltaTable.detail().show()\n\n# Generate manifest\nprint(\"######## Generating manifest ######\")\ndeltaTable.generate(\"SYMLINK_FORMAT_MANIFEST\")\n\n# SQL Vacuum\nprint(\"####### SQL Vacuum #######\")\nspark.sql(\"VACUUM '%s' RETAIN 169 HOURS\" % \"/tmp/delta-table\").collect()\n\n# SQL describe history\nprint(\"####### SQL Describe History ########\")\nprint(spark.sql(\"DESCRIBE HISTORY delta.`%s`\" % (\"/tmp/delta-table\")).collect())\n\n# cleanup\nshutil.rmtree(\"/tmp/delta-table\")\n"
  },
  {
    "path": "examples/scala/.scalafmt.conf",
    "content": "version = \"3.4.0\"\nrunner.dialect = scala213"
  },
  {
    "path": "examples/scala/README.md",
    "content": "# delta scala examples\n\nThis directory contains a set of spark & delta examples.\n\nExecute `./build/sbt run` and choose which main class to run.\n\n```\nMultiple main classes detected. Select one to run:\n [1] example.Quickstart\n [2] example.QuickstartSQL\n [3] example.QuickstartSQLOnPaths\n [4] example.Streaming\n [5] example.Utilities\n```\n\nYou can specify delta lake version and scala version with environment variables `DELTA_VERSION`, `SCALA_VERSION` or editing `build.sbt`.\n\nIf you are faced with `java.lang.IllegalAccessError: class org.apache.spark.storage.StorageUtils$ (in unnamed module @0x******) cannot access class sun.nio.ch.DirectBuffer (in module java.base) because module java.base does not export sun.nio.ch to unnamed module` when you use Java 9 or later, add jvm option in `build.sbt`.\n\n```diff\nlazy val root = (project in file(\".\"))\n  .settings(\n    run / fork := true,\n+   run / javaOptions ++= Seq(\n+     \"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED\"\n+   ),\n```\n"
  },
  {
    "path": "examples/scala/build/sbt",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n\n# When creating new tests for Spark SQL Hive, the HADOOP_CLASSPATH must contain the hive jars so\n# that we can run Hive to generate the golden answer.  This is not required for normal development\n# or testing.\nif [ -n \"$HIVE_HOME\" ]; then\n    for i in \"$HIVE_HOME\"/lib/*\n    do HADOOP_CLASSPATH=\"$HADOOP_CLASSPATH:$i\"\n    done\n    export HADOOP_CLASSPATH\nfi\n\nrealpath () {\n(\n  TARGET_FILE=\"$1\"\n\n  cd \"$(dirname \"$TARGET_FILE\")\"\n  TARGET_FILE=\"$(basename \"$TARGET_FILE\")\"\n\n  COUNT=0\n  while [ -L \"$TARGET_FILE\" -a $COUNT -lt 100 ]\n  do\n      TARGET_FILE=\"$(readlink \"$TARGET_FILE\")\"\n      cd $(dirname \"$TARGET_FILE\")\n      TARGET_FILE=\"$(basename $TARGET_FILE)\"\n      COUNT=$(($COUNT + 1))\n  done\n\n  echo \"$(pwd -P)/\"$TARGET_FILE\"\"\n)\n}\n\nif [[ \"$JENKINS_URL\" != \"\" ]]; then\n  # Make Jenkins use Google Mirror first as Maven Central may ban us\n  SBT_REPOSITORIES_CONFIG=\"$(dirname \"$(realpath \"$0\")\")/sbt-config/repositories\"\n  export SBT_OPTS=\"-Dsbt.override.build.repos=true -Dsbt.repository.config=$SBT_REPOSITORIES_CONFIG\"\nfi\n\n. \"$(dirname \"$(realpath \"$0\")\")\"/sbt-launch-lib.bash\n\n\ndeclare -r noshare_opts=\"-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy\"\ndeclare -r sbt_opts_file=\".sbtopts\"\ndeclare -r etc_sbt_opts_file=\"/etc/sbt/sbtopts\"\n\nusage() {\n cat <<EOM\nUsage: $script_name [options]\n\n  -h | -help         print this message\n  -v | -verbose      this runner is chattier\n  -d | -debug        set sbt log level to debug\n  -no-colors         disable ANSI color codes\n  -sbt-create        start sbt even if current directory contains no sbt project\n  -sbt-dir   <path>  path to global settings/plugins directory (default: ~/.sbt)\n  -sbt-boot  <path>  path to shared boot directory (default: ~/.sbt/boot in 0.11 series)\n  -ivy       <path>  path to local Ivy repository (default: ~/.ivy2)\n  -mem    <integer>  set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem))\n  -no-share          use all local caches; no sharing\n  -no-global         uses global caches, but does not use global ~/.sbt directory.\n  -jvm-debug <port>  Turn on JVM debugging, open at the given port.\n  -batch             Disable interactive mode\n\n  # sbt version (default: from project/build.properties if present, else latest release)\n  -sbt-version  <version>   use the specified version of sbt\n  -sbt-jar      <path>      use the specified jar as the sbt launcher\n  -sbt-rc                   use an RC version of sbt\n  -sbt-snapshot             use a snapshot version of sbt\n\n  # java version (default: java from PATH, currently $(java -version 2>&1 | grep version))\n  -java-home <path>         alternate JAVA_HOME\n\n  # jvm options and output control\n  JAVA_OPTS          environment variable, if unset uses \"$java_opts\"\n  SBT_OPTS           environment variable, if unset uses \"$default_sbt_opts\"\n  .sbtopts           if this file exists in the current directory, it is\n                     prepended to the runner args\n  /etc/sbt/sbtopts   if this file exists, it is prepended to the runner args\n  -Dkey=val          pass -Dkey=val directly to the java runtime\n  -J-X               pass option -X directly to the java runtime\n                     (-J is stripped)\n  -S-X               add -X to sbt's scalacOptions (-S is stripped)\n  -PmavenProfiles    Enable a maven profile for the build.\n\nIn the case of duplicated or conflicting options, the order above\nshows precedence: JAVA_OPTS lowest, command line options highest.\nEOM\n}\n\nprocess_my_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n     -no-colors) addJava \"-Dsbt.log.noformat=true\" && shift ;;\n      -no-share) addJava \"$noshare_opts\" && shift ;;\n     -no-global) addJava \"-Dsbt.global.base=$(pwd)/project/.sbtboot\" && shift ;;\n      -sbt-boot) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.boot.directory=$2\" && shift 2 ;;\n       -sbt-dir) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.global.base=$2\" && shift 2 ;;\n     -debug-inc) addJava \"-Dxsbt.inc.debug=true\" && shift ;;\n         -batch) exec </dev/null && shift ;;\n\n    -sbt-create) sbt_create=true && shift ;;\n\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  # Now, ensure sbt version is used.\n  [[ \"${sbt_version}XXX\" != \"XXX\" ]] && addJava \"-Dsbt.version=$sbt_version\"\n}\n\nloadConfigFile() {\n  cat \"$1\" | sed '/^\\#/d'\n}\n\n# if sbtopts files exist, prepend their contents to $@ so it can be processed by this runner\n[[ -f \"$etc_sbt_opts_file\" ]] && set -- $(loadConfigFile \"$etc_sbt_opts_file\") \"$@\"\n[[ -f \"$sbt_opts_file\" ]] && set -- $(loadConfigFile \"$sbt_opts_file\") \"$@\"\n\nexit_status=127\nsaved_stty=\"\"\n\nrestoreSttySettings() {\n  stty $saved_stty\n  saved_stty=\"\"\n}\n\nonExit() {\n  if [[ \"$saved_stty\" != \"\" ]]; then\n    restoreSttySettings\n  fi\n  exit $exit_status\n}\n\nsaveSttySettings() {\n  saved_stty=$(stty -g 2>/dev/null)\n  if [[ ! $? ]]; then\n    saved_stty=\"\"\n  fi\n}\n\nsaveSttySettings\ntrap onExit INT\n\nrun \"$@\"\n\nexit_status=$?\nonExit\n"
  },
  {
    "path": "examples/scala/build/sbt-config/repositories",
    "content": "[repositories]\n  local\n  local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]\n  local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}\n  gcs-maven-central-mirror: https://maven-central.storage-download.googleapis.com/repos/central/data/\n  maven-central\n  typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly\n  sbt-ivy-snapshots: https://repo.scala-sbt.org/scalasbt/ivy-snapshots/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly\n  sbt-plugin-releases: https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]\n  repos-spark-packages: https://repos.spark-packages.org\n  typesafe-releases: https://repo.typesafe.com/typesafe/releases/\n"
  },
  {
    "path": "examples/scala/build/sbt-launch-lib.bash",
    "content": "#!/usr/bin/env bash\n#\n\n# A library to simplify using the SBT launcher from other packages.\n# Note: This should be used by tools like giter8/conscript etc.\n\n# TODO - Should we merge the main SBT script with this library?\n\nif test -z \"$HOME\"; then\n  declare -r script_dir=\"$(dirname \"$script_path\")\"\nelse\n  declare -r script_dir=\"$HOME/.sbt\"\nfi\n\ndeclare -a residual_args\ndeclare -a java_args\ndeclare -a scalac_args\ndeclare -a sbt_commands\ndeclare -a maven_profiles\n\nif test -x \"$JAVA_HOME/bin/java\"; then\n    echo -e \"Using $JAVA_HOME as default JAVA_HOME.\"\n    echo \"Note, this will be overridden by -java-home if it is set.\"\n    declare java_cmd=\"$JAVA_HOME/bin/java\"\nelse\n    declare java_cmd=java\nfi\n\nechoerr () {\n  echo 1>&2 \"$@\"\n}\nvlog () {\n  [[ $verbose || $debug ]] && echoerr \"$@\"\n}\ndlog () {\n  [[ $debug ]] && echoerr \"$@\"\n}\n\nacquire_sbt_jar () {\n  SBT_VERSION=`awk -F \"=\" '/sbt\\.version/ {print $2}' ./project/build.properties`\n  URL1=${DEFAULT_ARTIFACT_REPOSITORY:-https://repo1.maven.org/maven2/}org/scala-sbt/sbt-launch/${SBT_VERSION}/sbt-launch-${SBT_VERSION}.jar\n  JAR=build/sbt-launch-${SBT_VERSION}.jar\n\n  sbt_jar=$JAR\n\n  if [[ ! -f \"$sbt_jar\" ]]; then\n    # Download sbt launch jar if it hasn't been downloaded yet\n    if [ ! -f \"${JAR}\" ]; then\n    # Download\n    printf \"Attempting to fetch sbt\\n\"\n    JAR_DL=\"${JAR}.part\"\n    if [ $(command -v curl) ]; then\n      curl --fail --location --silent ${URL1} > \"${JAR_DL}\" &&\\\n        mv \"${JAR_DL}\" \"${JAR}\"\n    elif [ $(command -v wget) ]; then\n      wget --quiet ${URL1} -O \"${JAR_DL}\" &&\\\n        mv \"${JAR_DL}\" \"${JAR}\"\n    else\n      printf \"You do not have curl or wget installed, please install sbt manually from http://www.scala-sbt.org/\\n\"\n      exit -1\n    fi\n    fi\n    if [ ! -f \"${JAR}\" ]; then\n    # We failed to download\n    printf \"Our attempt to download sbt locally to ${JAR} failed. Please install sbt manually from http://www.scala-sbt.org/\\n\"\n    exit -1\n    fi\n    printf \"Launching sbt from ${JAR}\\n\"\n  fi\n}\n\nexecRunner () {\n  # print the arguments one to a line, quoting any containing spaces\n  [[ $verbose || $debug ]] && echo \"# Executing command line:\" && {\n    for arg; do\n      if printf \"%s\\n\" \"$arg\" | grep -q ' '; then\n        printf \"\\\"%s\\\"\\n\" \"$arg\"\n      else\n        printf \"%s\\n\" \"$arg\"\n      fi\n    done\n    echo \"\"\n  }\n\n  \"$@\"\n}\n\naddJava () {\n  dlog \"[addJava] arg = '$1'\"\n  java_args=( \"${java_args[@]}\" \"$1\" )\n}\n\nenableProfile () {\n  dlog \"[enableProfile] arg = '$1'\"\n  maven_profiles=( \"${maven_profiles[@]}\" \"$1\" )\n  export SBT_MAVEN_PROFILES=\"${maven_profiles[@]}\"\n}\n\naddSbt () {\n  dlog \"[addSbt] arg = '$1'\"\n  sbt_commands=( \"${sbt_commands[@]}\" \"$1\" )\n}\naddResidual () {\n  dlog \"[residual] arg = '$1'\"\n  residual_args=( \"${residual_args[@]}\" \"$1\" )\n}\naddDebugger () {\n  addJava \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1\"\n}\n\n# a ham-fisted attempt to move some memory settings in concert\n# so they need not be dicked around with individually.\nget_mem_opts () {\n  local mem=${1:-1000}\n  local perm=$(( $mem / 4 ))\n  (( $perm > 256 )) || perm=256\n  (( $perm < 4096 )) || perm=4096\n  local codecache=$(( $perm / 2 ))\n\n  echo \"-Xms${mem}m -Xmx${mem}m -XX:ReservedCodeCacheSize=${codecache}m\"\n}\n\nrequire_arg () {\n  local type=\"$1\"\n  local opt=\"$2\"\n  local arg=\"$3\"\n  if [[ -z \"$arg\" ]] || [[ \"${arg:0:1}\" == \"-\" ]]; then\n    echo \"$opt requires <$type> argument\" 1>&2\n    exit 1\n  fi\n}\n\nis_function_defined() {\n  declare -f \"$1\" > /dev/null\n}\n\nprocess_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n       -h|-help) usage; exit 1 ;;\n    -v|-verbose) verbose=1 && shift ;;\n      -d|-debug) debug=1 && shift ;;\n\n           -ivy) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.ivy.home=$2\" && shift 2 ;;\n           -mem) require_arg integer \"$1\" \"$2\" && sbt_mem=\"$2\" && shift 2 ;;\n     -jvm-debug) require_arg port \"$1\" \"$2\" && addDebugger $2 && shift 2 ;;\n         -batch) exec </dev/null && shift ;;\n\n       -sbt-jar) require_arg path \"$1\" \"$2\" && sbt_jar=\"$2\" && shift 2 ;;\n   -sbt-version) require_arg version \"$1\" \"$2\" && sbt_version=\"$2\" && shift 2 ;;\n     -java-home) require_arg path \"$1\" \"$2\" && java_cmd=\"$2/bin/java\" && export JAVA_HOME=$2 && shift 2 ;;\n\n            -D*) addJava \"$1\" && shift ;;\n            -J*) addJava \"${1:2}\" && shift ;;\n            -P*) enableProfile \"$1\" && shift ;;\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  is_function_defined process_my_args && {\n    myargs=(\"${residual_args[@]}\")\n    residual_args=()\n    process_my_args \"${myargs[@]}\"\n  }\n}\n\nrun() {\n  # no jar? download it.\n  [[ -f \"$sbt_jar\" ]] || acquire_sbt_jar \"$sbt_version\" || {\n    # still no jar? uh-oh.\n    echo \"Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar\"\n    exit 1\n  }\n\n  # process the combined args, then reset \"$@\" to the residuals\n  process_args \"$@\"\n  set -- \"${residual_args[@]}\"\n  argumentCount=$#\n\n  # run sbt\n  execRunner \"$java_cmd\" \\\n    ${SBT_OPTS:-$default_sbt_opts} \\\n    $(get_mem_opts $sbt_mem) \\\n    ${java_opts} \\\n    ${java_args[@]} \\\n    -jar \"$sbt_jar\" \\\n    \"${sbt_commands[@]}\" \\\n    \"${residual_args[@]}\"\n}\n"
  },
  {
    "path": "examples/scala/build.sbt",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport scala.util.{Success, Try}\nname := \"example\"\norganization := \"com.example\"\norganizationName := \"example\"\n\nval scala213 = \"2.13.17\"\nval icebergVersion = \"1.4.1\"\nval unityCatalogVersion = \"0.4.0\"\nval jacksonVersion = \"2.15.4\"\n\nval defaultDeltaVersion = {\n  val versionFileContent = IO.read(file(\"../../version.sbt\"))\n  val versionRegex = \"\"\".*version\\s*:=\\s*\"([^\"]+)\".*\"\"\".r\n  versionRegex.findFirstMatchIn(versionFileContent) match {\n    case Some(m) => m.group(1)\n    case None => throw new Exception(\"Could not parse version from version.sbt\")\n  }\n}\n\ndef getMajorMinor(version: String): (Int, Int) = {\n  val majorMinor = Try {\n    val splitVersion = version.split('.')\n    (splitVersion(0).toInt, splitVersion(1).toInt)\n  }\n  majorMinor match {\n    case Success(_) => (majorMinor.get._1, majorMinor.get._2)\n    case _ =>\n      throw new RuntimeException(s\"Unsupported delta version: $version. \" +\n        s\"Please check https://docs.delta.io/latest/releases.html\")\n  }\n}\n// Maps Delta version (major, minor) to the compatible Spark version.\n// Used as a fallback for local dev when SPARK_VERSION env var is not set.\nval lookupSparkVersion: PartialFunction[(Int, Int), String] = {\n  // TODO: how to run integration tests for multiple Spark versions\n  case (major, minor) if major >= 4 && minor >= 1 => \"4.1.0\"\n  // version 4.0.0\n  case (major, minor) if major >= 4 => \"4.0.0\"\n  // versions 3.3.x+\n  case (major, minor) if major >= 3 && minor >=3 => \"3.5.3\"\n  // versions 3.0.0 to 3.2.x\n  case (major, minor) if major >= 3 && minor <=2 => \"3.5.0\"\n  // versions 2.4.x\n  case (major, minor) if major == 2 && minor == 4 => \"3.4.0\"\n  // versions 2.3.x\n  case (major, minor) if major == 2  && minor == 3 => \"3.3.2\"\n  // versions 2.2.x\n  case (major, minor) if major == 2  && minor == 2 => \"3.3.1\"\n  // versions 2.1.x\n  case (major, minor) if major == 2  && minor == 1 => \"3.3.0\"\n  // versions 1.0.0 to 2.0.x\n  case (major, minor) if major == 1 || (major == 2 && minor == 0) => \"3.2.1\"\n  // versions 0.7.x to 0.8.x\n  case (major, minor) if major == 0 && (minor == 7 || minor == 8) => \"3.0.2\"\n  // versions below 0.7\n  case (major, minor) if major == 0 && minor < 7 => \"2.4.4\"\n}\n\nval getScalaVersion = settingKey[String](\n  s\"get scala version from environment variable SCALA_VERSION. If it doesn't exist, use $scala213\"\n)\nval getDeltaVersion = settingKey[String](\n  s\"get delta version from environment variable DELTA_VERSION. If it doesn't exist, use $defaultDeltaVersion\"\n)\nval getDeltaArtifactName = settingKey[String](\n  s\"get delta artifact name based on the delta version. either `delta-core` or `delta-spark`.\"\n)\nval getIcebergSparkRuntimeArtifactName = settingKey[String](\n  s\"get iceberg-spark-runtime name based on the delta version.\"\n)\ngetScalaVersion := {\n  sys.env.get(\"SCALA_VERSION\") match {\n    case Some(\"2.13\") | Some(`scala213`) =>\n      scala213\n    case Some(v) =>\n      println(\n        s\"[warn] Invalid  SCALA_VERSION. Expected one of {2.13, $scala213} but \" +\n        s\"got $v. Fallback to $scala213.\"\n      )\n      scala213\n    case None =>\n      scala213\n  }\n}\n\nscalaVersion := getScalaVersion.value\nversion := \"0.1.0\"\n\ngetDeltaVersion := {\n  sys.env.get(\"DELTA_VERSION\") match {\n    case Some(v) =>\n      println(s\"Using DELTA_VERSION Delta version $v\")\n      v\n    case None =>\n      println(s\"Using default Delta version $defaultDeltaVersion\")\n      defaultDeltaVersion\n  }\n}\n\ngetDeltaArtifactName := {\n  val deltaVersion = getDeltaVersion.value\n  if (deltaVersion.charAt(0).asDigit >= 3) \"delta-spark\" else \"delta-core\"\n}\n\nval getSparkPackageSuffix = settingKey[String](\n  s\"get package suffix for cross-build artifact name from environment variable SPARK_PACKAGE_SUFFIX. \" +\n  s\"This is derived from CrossSparkVersions.scala (single source of truth).\"\n)\n\ngetSparkPackageSuffix := {\n  sys.env.getOrElse(\"SPARK_PACKAGE_SUFFIX\", \"\")\n}\n\nval getSupportIceberg = settingKey[String](\n  s\"get supportIceberg for cross-build artifact name from environment variable SUPPORT_ICEBERG. \" +\n  s\"This is derived from CrossSparkVersions.scala (single source of truth).\"\n)\n\ngetSupportIceberg := {\n  sys.env.getOrElse(\"SUPPORT_ICEBERG\", \"false\")\n}\n\ngetIcebergSparkRuntimeArtifactName := {\n  val (expMaj, expMin) = getMajorMinor(lookupSparkVersion.apply(\n    getMajorMinor(getDeltaVersion.value)))\n  s\"iceberg-spark-runtime-$expMaj.$expMin\"\n}\n\nlazy val extraMavenRepo = sys.env.get(\"EXTRA_MAVEN_REPO\").toSeq.map { repo =>\n  resolvers += \"Delta\" at repo\n}\n\nlazy val java17Settings = Seq(\n  fork := true,\n  javaOptions ++= Seq(\n    \"--add-exports=java.base/sun.nio.ch=ALL-UNNAMED\"\n  )\n)\n\n// Use SPARK_VERSION env var if set, otherwise fall back to lookupSparkVersion (for local dev)\ndef resolveSparkVersion(deltaVersion: String): String = {\n  val envVersion = sys.env.getOrElse(\"SPARK_VERSION\", \"\")\n  if (envVersion.nonEmpty) envVersion\n  else lookupSparkVersion.apply(getMajorMinor(deltaVersion))\n}\n\ndef getLibraryDependencies(\n    deltaVersion: String,\n    deltaArtifactName: String,\n    icebergSparkRuntimeArtifactName: String,\n    sparkPackageSuffix: String,\n    scalaBinVersion: String,\n    supportIceberg: String): Seq[ModuleID] = {\n\n  // Package suffix comes from CrossSparkVersions.scala (single source of truth)\n  // e.g., \"\" for default Spark, \"_4.1\" for Spark 4.1\n  val deltaCoreDep = \"io.delta\" % s\"${deltaArtifactName}${sparkPackageSuffix}_${scalaBinVersion}\" % deltaVersion\n  val deltaIcebergDep = \"io.delta\" % s\"delta-iceberg_${scalaBinVersion}\" % deltaVersion\n\n  val resolvedSparkVersion = resolveSparkVersion(deltaVersion)\n\n  val baseDeps = Seq(\n    deltaCoreDep,\n    \"org.apache.spark\" %% \"spark-sql\" % resolvedSparkVersion,\n    \"org.apache.spark\" %% \"spark-hive\" % resolvedSparkVersion,\n    \"org.apache.iceberg\" % \"iceberg-hive-metastore\" % icebergVersion\n  )\n\n  // Include Iceberg dependencies only if supportIceberg is enabled\n  val icebergDeps = if (supportIceberg == \"true\") {\n    getMajorMinor(deltaVersion) match {\n      case (major, _) if major >= 4 =>\n        // Don't include the iceberg dependencies for 4.0.0rc1 and later\n        Seq.empty\n      case _ =>\n        Seq(\n          deltaIcebergDep,\n          \"org.apache.iceberg\" %% icebergSparkRuntimeArtifactName % icebergVersion,\n        )\n    }\n  } else {\n    Seq.empty\n  }\n\n  baseDeps ++ icebergDeps\n}\n\nlazy val root = (project in file(\".\"))\n  .settings(\n    run / fork := true,\n    name := \"hello-world\",\n    crossScalaVersions := Seq(scala213),\n    libraryDependencies ++= getLibraryDependencies(\n      getDeltaVersion.value,\n      getDeltaArtifactName.value,\n      getIcebergSparkRuntimeArtifactName.value,\n      getSparkPackageSuffix.value,\n      scalaBinaryVersion.value,\n      getSupportIceberg.value),\n    libraryDependencies ++= Seq(\n      \"io.unitycatalog\" %% \"unitycatalog-spark\" % unityCatalogVersion excludeAll(\n        ExclusionRule(organization = \"com.fasterxml.jackson.core\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.module\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.datatype\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.dataformat\")\n      ),\n      \"io.unitycatalog\" % \"unitycatalog-server\" % unityCatalogVersion excludeAll(\n        ExclusionRule(organization = \"com.fasterxml.jackson.core\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.module\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.datatype\"),\n        ExclusionRule(organization = \"com.fasterxml.jackson.dataformat\")\n      )\n    ),\n    dependencyOverrides ++= Seq(\n      \"com.fasterxml.jackson.core\" % \"jackson-core\" % jacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-annotations\" % jacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,\n      \"com.fasterxml.jackson.dataformat\" % \"jackson-dataformat-yaml\" % jacksonVersion,\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jsr310\" % jacksonVersion,\n      \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % jacksonVersion\n    ),\n    extraMavenRepo,\n    resolvers += Resolver.mavenLocal,\n    scalacOptions ++= Seq(\n      \"-deprecation\",\n      \"-feature\"\n    ),\n    // Conditionally exclude IcebergCompatV2.scala when supportIceberg is \"false\"\n    Compile / unmanagedSources / excludeFilter := {\n      if (getSupportIceberg.value == \"false\") {\n        HiddenFileFilter || \"IcebergCompatV2.scala\"\n      } else {\n        HiddenFileFilter\n      }\n    },\n    java17Settings\n  )\n"
  },
  {
    "path": "examples/scala/project/build.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\nsbt.version=1.9.9\n"
  },
  {
    "path": "examples/scala/src/main/resources/log4j2.properties",
    "content": "#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nrootLogger.level = error\nrootLogger.appenderRef.stdout.ref = console\n\nappender.console.type = Console\nappender.console.name = console\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = [%t] %-5p %c %x - %m%n\n\n\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/ChangeDataFeed.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport org.apache.spark.{SparkConf, SparkContext}\nimport org.apache.spark.sql.{DataFrame, SQLContext, SparkSession}\nimport org.apache.spark.sql.streaming.{StreamingQuery}\nimport io.delta.tables._\n\nimport org.apache.spark.sql.functions._\nimport org.apache.commons.io.FileUtils\nimport java.io.File\n\nobject ChangeDataFeed {\n  def main(args: Array[String]): Unit = {\n    val spark = SparkSession\n      .builder()\n      .appName(\"ChangeDataFeed\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\n        \"spark.sql.catalog.spark_catalog\",\n        \"org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n      )\n      .getOrCreate()\n\n    val path = \"/tmp/delta-change-data-feed/student\"\n    val otherPath = \"/tmp/delta-change-data-feed/student_source\"\n\n    def cleanup(): Unit = {\n      Seq(path, otherPath).foreach { p =>\n        val file = new File(p)\n        if (file.exists()) FileUtils.deleteDirectory(file)\n      }\n      spark.sql(s\"DROP TABLE IF EXISTS student\")\n      spark.sql(s\"DROP TABLE IF EXISTS student_source\")\n    }\n\n    // Note: one could also read by path using `.load(path)`\n    def readCDCByTableName(startingVersion: Int): DataFrame = {\n      spark.read.format(\"delta\")\n        .option(\"readChangeFeed\", \"true\")\n        .option(\"startingVersion\", startingVersion.toString)\n        .table(\"student\")\n        .orderBy(\"_change_type\", \"id\")\n    }\n\n    // Note: one could also stream by path using `.load(path)`\n    def streamCDCByTableName(startingVersion: Int): StreamingQuery = {\n      spark.readStream.format(\"delta\")\n        .option(\"readChangeFeed\", \"true\")\n        .option(\"startingVersion\", startingVersion.toString)\n        .table(\"student\")\n        .writeStream\n        .format(\"console\")\n        .option(\"numRows\", 1000)\n        .start()\n    }\n\n    cleanup()\n\n    try {\n      // =============== Create student table ===============\n\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE student (id INT, name STRING, age INT)\n           |USING DELTA\n           |PARTITIONED BY (age)\n           |TBLPROPERTIES (delta.enableChangeDataFeed = true)\n           |LOCATION '$path'\"\"\".stripMargin) // v0\n\n      spark.range(0, 10)\n        .selectExpr(\n          \"CAST(id as INT) as id\",\n          \"CAST(id as STRING) as name\",\n          \"CAST(id % 4 + 18 as INT) as age\")\n        .write.format(\"delta\").mode(\"append\").save(path)  // v1\n\n      // =============== Show table data + changes ===============\n\n      println(\"(v1) Initial Table\")\n      spark.read.format(\"delta\").load(path).orderBy(\"id\").show()\n\n      println(\"(v1) CDC changes\")\n      readCDCByTableName(1).show()\n\n      val table = io.delta.tables.DeltaTable.forPath(path)\n\n      // =============== Perform UPDATE ===============\n\n      println(\"(v2) Updated id -> id + 1\")\n      table.update(Map(\"id\" -> expr(\"id + 1\"))) // v2\n      readCDCByTableName(2).show()\n\n      // =============== Perform DELETE ===============\n\n      println(\"(v3) Deleted where id >= 7\")\n      table.delete(expr(\"id >= 7\")) // v3\n      readCDCByTableName(3).show()\n\n      // =============== Perform partition DELETE ===============\n\n      println(\"(v4) Deleted where age = 18\")\n      table.delete(expr(\"age = 18\")) // v4, partition delete\n      readCDCByTableName(4).show()\n\n      // =============== Create source table for MERGE ===============\n\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE student_source (id INT, name STRING, age INT)\n           |USING DELTA\n           |LOCATION '$otherPath'\"\"\".stripMargin)\n      spark.range(0, 3).selectExpr(\n        \"CAST(id as INT) as id\",\n        \"CAST(id as STRING) as name\",\n        \"CAST(id % 4 + 18 as INT) as age\")\n        .write.format(\"delta\").mode(\"append\").saveAsTable(\"student_source\")\n      val source = spark.sql(\"SELECT * FROM student_source\")\n\n      // =============== Perform MERGE ===============\n\n      table\n        .as(\"target\")\n        .merge(source.as(\"source\"), \"target.id = source.id\")\n        .whenMatched()\n        .updateExpr(\n          Map(\"id\" -> \"source.id\", \"age\" -> \"source.age + 10\"))\n        .whenNotMatched()\n        .insertAll()\n        .execute() // v5\n      println(\"(v5) Merged with a source table\")\n      readCDCByTableName(5).show()\n\n      // =============== Stream changes ===============\n\n      println(\"Streaming by table name\")\n      val cdfStream = streamCDCByTableName(0)\n      cdfStream.awaitTermination(5000)\n      cdfStream.stop()\n    } finally {\n      cleanup()\n      spark.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/Clustering.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage example\n\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.SparkSession\n\nobject Clustering {\n\n  def main(args: Array[String]): Unit = {\n    val tableName = \"deltatable\"\n\n    val deltaSpark = SparkSession\n      .builder()\n      .appName(\"Clustering-Delta\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n\n    // Clear up old session\n    deltaSpark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n\n    // Enable preview config for clustering\n    deltaSpark.conf.set(\n      \"spark.databricks.delta.clusteredTable.enableClusteringTablePreview\", \"true\")\n\n    try {\n      // Create a table\n      println(\"Creating a table\")\n      deltaSpark.sql(\n        s\"\"\"CREATE TABLE $tableName (col1 INT, col2 STRING) using DELTA\n           |CLUSTER BY (col1, col2)\"\"\".stripMargin)\n\n      // Insert new data\n      println(\"Insert new data\")\n      deltaSpark.sql(s\"INSERT INTO $tableName VALUES (123, '123')\")\n\n      // Optimize the table\n      println(\"Optimize the table\")\n      deltaSpark.sql(s\"OPTIMIZE $tableName\")\n\n      // Change the clustering columns\n      println(\"Change the clustering columns\")\n      deltaSpark.sql(\n        s\"\"\"ALTER TABLE $tableName CLUSTER BY (col2, col1)\"\"\".stripMargin)\n\n\n      // Check the clustering columns\n      println(\"Check the clustering columns\")\n      deltaSpark.sql(s\"DESCRIBE DETAIL $tableName\").show(false)\n    } finally {\n      // Cleanup\n      deltaSpark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n    }\n\n    // DeltaTable clusterBy Scala API\n    try {\n      val table = io.delta.tables.DeltaTable.create()\n        .tableName(tableName)\n        .addColumn(\"col1\", \"INT\")\n        .addColumn(\"col2\", \"STRING\")\n        .clusterBy(\"col1\", \"col2\")\n        .execute()\n    } finally {\n      // Cleanup\n      deltaSpark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n      deltaSpark.stop()\n    }\n  }\n}\n\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/EvolutionWithMap.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.SparkSession\n\nobject EvolutionWithMap {\n  def main(args: Array[String]): Unit = {\n    val spark = SparkSession.builder()\n      .appName(\"EvolutionWithMap\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n\n    import spark.implicits._\n\n    val tableName = \"insert_map_schema_evolution\"\n\n    try {\n      // Define initial schema\n      val initialSchema = StructType(Seq(\n          StructField(\"key\", IntegerType, nullable = false),\n          StructField(\"metrics\", MapType(StringType, StructType(Seq(\n              StructField(\"id\", IntegerType, nullable = false),\n              StructField(\"value\", IntegerType, nullable = false)\n          ))))\n          ))\n\n      val data = Seq(\n      Row(1, Map(\"event\" -> Row(1, 1)))\n      )\n\n      val rdd = spark.sparkContext.parallelize(data)\n\n      val initialDf = spark.createDataFrame(rdd, initialSchema)\n\n      initialDf.write\n        .option(\"overwriteSchema\", \"true\")\n        .mode(\"overwrite\")\n        .format(\"delta\")\n        .saveAsTable(s\"$tableName\")\n\n      // Define the schema with simulteneous change in a StructField name\n      // And additional field in a map column\n      val evolvedSchema = StructType(Seq(\n      StructField(\"renamed_key\", IntegerType, nullable = false),\n      StructField(\"metrics\", MapType(StringType, StructType(Seq(\n          StructField(\"id\", IntegerType, nullable = false),\n          StructField(\"value\", IntegerType, nullable = false),\n          StructField(\"comment\", StringType, nullable = true)\n      ))))\n      ))\n\n      val evolvedData = Seq(\n      Row(1, Map(\"event\" -> Row(1, 1, \"deprecated\")))\n      )\n\n      val evolvedRDD = spark.sparkContext.parallelize(evolvedData)\n\n      val modifiedDf = spark.createDataFrame(evolvedRDD, evolvedSchema)\n\n      // The below would fail without schema evolution for map types\n      modifiedDf.write\n        .mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .format(\"delta\")\n        .insertInto(s\"$tableName\")\n\n      spark.sql(s\"SELECT * FROM $tableName\").show(false)\n\n    } finally {\n\n      // Cleanup\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n\n      spark.stop()\n    }\n\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/IcebergCompatV2.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage example\n\nimport java.io.{File, IOException}\nimport java.net.ServerSocket\n\nimport org.apache.commons.io.FileUtils\n\nimport org.apache.spark.sql.SparkSession\n/**\n * This example relies on an external Hive metastore (HMS) instance to run.\n *\n * A standalone HMS can be created using the following docker command.\n *  ************************************************************\n *  docker run -d -p 9083:9083 --env SERVICE_NAME=metastore \\\n *  --name metastore-standalone apache/hive:4.0.0-beta-1\n *  ************************************************************\n *  The URL of this standalone HMS is thrift://localhost:9083\n *\n *  By default this hms will use `/opt/hive/data/warehouse` as warehouse path.\n *  Please make sure this path exists or change it prior to running the example.\n */\nobject IcebergCompatV2 {\n\n  def main(args: Array[String]): Unit = {\n    // Update this according to the metastore config\n    val port = 9083\n    val warehousePath = \"/opt/hive/data/warehouse/\"\n\n    if (!UniForm.hmsReady(port)) {\n      print(\"HMS not available. Exit.\")\n      return\n    }\n\n    val testTableName = \"uniform_table3\"\n    FileUtils.deleteDirectory(new File(s\"${warehousePath}${testTableName}\"))\n\n    val deltaSpark = SparkSession\n      .builder()\n      .appName(\"UniForm-Delta\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .config(\"hive.metastore.uris\", s\"thrift://localhost:$port\")\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n\n    deltaSpark.sql(s\"DROP TABLE IF EXISTS ${testTableName}\")\n    deltaSpark.sql(\n      s\"\"\"CREATE TABLE `${testTableName}`\n         | (id INT, ts TIMESTAMP, array_data array<int>, map_data map<int, int>)\n         | using DELTA\"\"\".stripMargin)\n    deltaSpark.sql(\n      s\"\"\"\n         |INSERT INTO `$testTableName` (id, ts, array_data, map_data)\n         | VALUES (123, '2024-01-01 00:00:00', array(2, 3, 4, 5), map(3, 6, 8, 7))\"\"\".stripMargin)\n    deltaSpark.sql(\n      s\"\"\"REORG TABLE `$testTableName` APPLY (UPGRADE UNIFORM\n         | (ICEBERG_COMPAT_VERSION = 2))\"\"\".stripMargin)\n\n    val icebergSpark = SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"UniForm-Iceberg\")\n      .config(\"spark.sql.extensions\",\n        \"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.iceberg.spark.SparkSessionCatalog\")\n      .config(\"hive.metastore.uris\", s\"thrift://localhost:$port\")\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n\n    icebergSpark.sql(s\"SELECT * FROM ${testTableName}\").show()\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/Quickstart.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport org.apache.spark.{SparkConf, SparkContext}\nimport org.apache.spark.sql.{SparkSession, SQLContext}\nimport io.delta.tables._\n\nimport org.apache.spark.sql.functions._\nimport org.apache.commons.io.FileUtils\nimport java.io.File\n\nobject Quickstart {\n  def main(args: Array[String]): Unit = {\n\n    val spark = SparkSession\n      .builder()\n      .appName(\"Quickstart\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\n        \"spark.sql.catalog.spark_catalog\",\n        \"org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n      )\n      .getOrCreate()\n\n    val file = new File(\"/tmp/delta-table\")\n    if (file.exists()) FileUtils.deleteDirectory(file)\n\n    // Create a table\n    println(\"Creating a table\")\n    val path = file.getCanonicalPath\n    var data = spark.range(0, 5)\n    data.write.format(\"delta\").save(path)\n\n    // Read table\n    println(\"Reading the table\")\n    val df = spark.read.format(\"delta\").load(path)\n    df.show()\n\n    // Upsert (merge) new data\n    println(\"Upsert new data\")\n    val newData = spark.range(0, 20).toDF()\n    val deltaTable = DeltaTable.forPath(path)\n\n    deltaTable\n      .as(\"oldData\")\n      .merge(newData.as(\"newData\"), \"oldData.id = newData.id\")\n      .whenMatched()\n      .update(Map(\"id\" -> col(\"newData.id\")))\n      .whenNotMatched()\n      .insert(Map(\"id\" -> col(\"newData.id\")))\n      .execute()\n\n    deltaTable.toDF.show()\n\n    // Update table data\n    println(\"Overwrite the table\")\n    data = spark.range(5, 10)\n    data.write.format(\"delta\").mode(\"overwrite\").save(path)\n    deltaTable.toDF.show()\n\n    // Update every even value by adding 100 to it\n    println(\"Update to the table (add 100 to every even value)\")\n    deltaTable.update(\n      condition = expr(\"id % 2 == 0\"),\n      set = Map(\"id\" -> expr(\"id + 100\"))\n    )\n    deltaTable.toDF.show()\n\n    // Delete every even value\n    deltaTable.delete(condition = expr(\"id % 2 == 0\"))\n    deltaTable.toDF.show()\n\n    // Read old version of the data using time travel\n    print(\"Read old data using time travel\")\n    val df2 = spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(path)\n    df2.show()\n\n    // Cleanup\n    FileUtils.deleteDirectory(file)\n    spark.stop()\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/QuickstartSQL.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport org.apache.spark.sql.SparkSession\nimport io.delta.tables._\n\nimport org.apache.spark.sql.functions._\nimport org.apache.commons.io.FileUtils\nimport java.io.File\n\nobject QuickstartSQL {\n  def main(args: Array[String]): Unit = {\n    // Create Spark Conf\n    val spark = SparkSession\n      .builder()\n      .appName(\"QuickstartSQL\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n    \n    val tableName = \"tblname\"\n\n    // Clear up old session\n    spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n    spark.sql(s\"DROP TABLE IF EXISTS newData\")\n\n    try {\n      // Create a table\n      println(\"Creating a table\")\n      spark.sql(s\"CREATE TABLE $tableName(id LONG) USING delta\")\n      spark.sql(s\"INSERT INTO $tableName VALUES 0, 1, 2, 3, 4\")\n\n      // Read table\n      println(\"Reading the table\")\n      spark.sql(s\"SELECT * FROM $tableName\").show()\n\n      // Upsert (merge) new data\n      println(\"Upsert new data\")\n      spark.sql(\"CREATE TABLE newData(id LONG) USING parquet\")\n      spark.sql(\"INSERT INTO newData VALUES 3, 4, 5, 6\")\n      \n      spark.sql(s\"\"\"MERGE INTO $tableName USING newData\n          ON ${tableName}.id = newData.id\n          WHEN MATCHED THEN\n            UPDATE SET ${tableName}.id = newData.id\n          WHEN NOT MATCHED THEN INSERT *\n      \"\"\")\n\n      spark.sql(s\"SELECT * FROM $tableName\").show()\n\n      // Update table data\n      println(\"Overwrite the table\")\n      spark.sql(s\"INSERT OVERWRITE $tableName VALUES 5, 6, 7, 8, 9\")\n      spark.sql(s\"SELECT * FROM $tableName\").show()\n\n      // Update every even value by adding 100 to it\n      println(\"Update to the table (add 100 to every even value)\")\n      spark.sql(s\"UPDATE $tableName SET id = (id + 100) WHERE (id % 2 == 0)\")\n      spark.sql(s\"SELECT * FROM $tableName\").show()\n\n      // Delete every even value\n      spark.sql(s\"DELETE FROM $tableName WHERE (id % 2 == 0)\")\n      spark.sql(s\"SELECT * FROM $tableName\").show()\n\n      // Read old version of the data using time travel\n      print(\"Read old data using time travel\")\n      spark.sql(s\"SELECT * FROM $tableName VERSION AS OF 0\").show()\n    } finally {\n      // Cleanup\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n      spark.sql(s\"DROP TABLE IF EXISTS newData\")\n      spark.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/QuickstartSQLOnPaths.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport org.apache.spark.sql.SparkSession\nimport io.delta.tables._\n\nimport org.apache.spark.sql.functions._\nimport org.apache.commons.io.FileUtils\nimport java.io.File\n\nobject QuickstartSQLOnPaths {\n  def main(args: Array[String]): Unit = {\n    // Create Spark Conf\n    val spark = SparkSession\n      .builder()\n      .appName(\"QuickstartSQLOnPaths\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n    \n    val tablePath = new File(\"/tmp/delta-table\")\n    if (tablePath.exists()) FileUtils.deleteDirectory(tablePath)\n\n    // Clear up old session\n    spark.sql(s\"DROP TABLE IF EXISTS newData\")\n\n    try {\n      // Create a table\n      println(\"Creating a table\")\n      spark.sql(s\"CREATE TABLE delta.`$tablePath`(id LONG) USING delta\")\n      spark.sql(s\"INSERT INTO delta.`$tablePath` VALUES 0, 1, 2, 3, 4\")\n\n      // Read table\n      println(\"Reading the table\")\n      spark.sql(s\"SELECT * FROM delta.`$tablePath`\").show()\n\n      // Upsert (merge) new data\n      println(\"Upsert new data\")\n      spark.sql(\"CREATE TABLE newData(id LONG) USING parquet\")\n      spark.sql(\"INSERT INTO newData VALUES 3, 4, 5, 6\")\n      \n      spark.sql(s\"\"\"MERGE INTO delta.`$tablePath` data USING newData\n          ON data.id = newData.id\n          WHEN MATCHED THEN\n            UPDATE SET data.id = newData.id\n          WHEN NOT MATCHED THEN INSERT *\n      \"\"\")\n\n      spark.sql(s\"SELECT * FROM delta.`$tablePath`\").show()\n\n      // Update table data\n      println(\"Overwrite the table\")\n      spark.sql(s\"INSERT OVERWRITE delta.`$tablePath` VALUES 5, 6, 7, 8, 9\")\n      spark.sql(s\"SELECT * FROM delta.`$tablePath`\").show()\n\n      // Update every even value by adding 100 to it\n      println(\"Update to the table (add 100 to every even value)\")\n      spark.sql(s\"UPDATE delta.`$tablePath` SET id = (id + 100) WHERE (id % 2 == 0)\")\n      spark.sql(s\"SELECT * FROM delta.`$tablePath`\").show()\n\n      // Delete every even value\n      spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE (id % 2 == 0)\")\n      spark.sql(s\"SELECT * FROM delta.`$tablePath`\").show()\n    } finally {\n      // Cleanup\n      spark.sql(s\"DROP TABLE IF EXISTS newData\")\n      spark.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/Streaming.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport java.io.File\n\nimport io.delta.tables.DeltaTable\nimport org.apache.commons.io.FileUtils\n\nimport org.apache.spark.sql.{DataFrame, SparkSession}\nimport org.apache.spark.sql.functions.col\n\nobject Streaming {\n\n  def main(args: Array[String]): Unit = {\n    // Create a Spark Session\n    val spark = SparkSession\n      .builder()\n      .appName(\"Streaming\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\n        \"spark.sql.catalog.spark_catalog\",\n        \"org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n      )\n      .getOrCreate()\n\n    import spark.implicits._\n\n    val exampleDir = new File(\"/tmp/delta-streaming/\")\n    if (exampleDir.exists()) FileUtils.deleteDirectory(exampleDir)\n\n    println(\n      \"=== Section 1: write and read delta table using batch queries, and initialize table for later sections\"\n    )\n    // Create a table\n    val data = spark.range(0, 5)\n    val path = new File(\"/tmp/delta-streaming/delta-table\").getAbsolutePath\n    data.write.format(\"delta\").save(path)\n\n    // Read table\n    val df = spark.read.format(\"delta\").load(path)\n    df.show()\n\n    println(\"=== Section 2: write and read delta using structured streaming\")\n    val streamingDf = spark.readStream.format(\"rate\").load()\n    val tablePath2 = new File(\n      \"/tmp/delta-streaming/delta-table2\"\n    ).getCanonicalPath\n    val checkpointPath = new File(\n      \"/tmp/delta-streaming/checkpoint\"\n    ).getCanonicalPath\n    val stream = streamingDf\n      .select($\"value\" as \"id\")\n      .writeStream\n      .format(\"delta\")\n      .option(\"checkpointLocation\", checkpointPath)\n      .start(tablePath2)\n\n    stream.awaitTermination(10000)\n    stream.stop()\n\n    val stream2 = spark.readStream\n      .format(\"delta\")\n      .load(tablePath2)\n      .writeStream\n      .format(\"console\")\n      .start()\n\n    stream2.awaitTermination(10000)\n    stream2.stop()\n\n    println(\"=== Section 3: Streaming upserts using MERGE\")\n    // Function to upsert microBatchOutputDF into Delta Lake table using merge\n    def upsertToDelta(microBatchOutputDF: DataFrame, batchId: Long): Unit = {\n      val deltaTable = DeltaTable.forPath(path)\n      deltaTable\n        .as(\"t\")\n        .merge(\n          microBatchOutputDF.select($\"value\" as \"id\").as(\"s\"),\n          \"s.id = t.id\"\n        )\n        .whenMatched()\n        .updateAll()\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n\n    val streamingAggregatesDf = spark.readStream\n      .format(\"rate\")\n      .load()\n      .withColumn(\"key\", col(\"value\") % 10)\n      .drop(\"timestamp\")\n\n    // Write the output of a streaming aggregation query into Delta Lake table\n    println(\"Original Delta Table\")\n    val deltaTable = DeltaTable.forPath(path)\n    deltaTable.toDF.show()\n\n    val stream3 = streamingAggregatesDf.writeStream\n      .format(\"delta\")\n      .foreachBatch(upsertToDelta _)\n      .outputMode(\"update\")\n      .start()\n\n    stream3.awaitTermination(20000)\n    stream3.stop()\n\n    println(\"Delta Table after streaming upsert\")\n    deltaTable.toDF.show()\n\n    // Streaming append and concurrent repartition using  data change = false\n    // tbl1 is the sink and tbl2 is the source\n    println(\n      \"############ Streaming appends with concurrent table repartition  ##########\"\n    )\n    val tbl1 = \"/tmp/delta-streaming/delta-table4\"\n    val tbl2 = \"/tmp/delta-streaming/delta-table5\"\n    val numRows = 10\n    spark.range(numRows).write.mode(\"overwrite\").format(\"delta\").save(tbl1)\n    spark.read.format(\"delta\").load(tbl1).show()\n    spark\n      .range(numRows, numRows * 10)\n      .write\n      .mode(\"overwrite\")\n      .format(\"delta\")\n      .save(tbl2)\n\n    // Start reading tbl2 as a stream and do a streaming write to tbl1\n    // Prior to Delta 0.5.0 this would throw StreamingQueryException: Detected a data update in the source table. This is currently not supported.\n    val stream4 = spark.readStream\n      .format(\"delta\")\n      .load(tbl2)\n      .writeStream\n      .format(\"delta\")\n      .option(\n        \"checkpointLocation\",\n        new File(\"/tmp/delta-streaming/checkpoint/tbl1\").getCanonicalPath\n      )\n      .outputMode(\"append\")\n      .start(tbl1)\n\n    Thread.sleep(10 * 1000)\n    // repartition table while streaming job is running\n    spark.read\n      .format(\"delta\")\n      .load(tbl2)\n      .repartition(10)\n      .write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"dataChange\", \"false\")\n      .save(tbl2)\n\n    stream4.awaitTermination(5 * 1000)\n    stream4.stop()\n    println(\"######### After streaming write #########\")\n    spark.read.format(\"delta\").load(tbl1).show()\n\n    println(\"=== In the end, clean all paths\")\n    // Cleanup\n    if (exampleDir.exists()) FileUtils.deleteDirectory(exampleDir)\n    spark.stop()\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/UniForm.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage example\n\nimport java.io.{File, IOException}\nimport java.net.ServerSocket\n\nimport org.apache.commons.io.FileUtils\n\nimport org.apache.spark.sql.SparkSession\n\n/**\n * This example relies on an external Hive metastore (HMS) instance to run.\n *\n * A standalone HMS can be created using the following docker command.\n *  ************************************************************\n *  docker run -d -p 9083:9083 --env SERVICE_NAME=metastore \\\n *  --name metastore-standalone apache/hive:4.0.0-beta-1\n *  ************************************************************\n *  The URL of this standalone HMS is thrift://localhost:9083\n *\n *  By default this hms will use `/opt/hive/data/warehouse` as warehouse path.\n *  Please make sure this path exists or change it prior to running the example.\n */\nobject UniForm {\n\n  def main(args: Array[String]): Unit = {\n    // Update this according to the metastore config\n    val port = 9083\n    val warehousePath = \"/opt/hive/data/warehouse/\"\n\n    if (!hmsReady(port)) {\n      print(\"HMS not available. Exit.\")\n      return\n    }\n\n    val testTableName = \"deltatable\"\n    FileUtils.deleteDirectory(new File(s\"${warehousePath}${testTableName}\"))\n\n    val deltaSpark = SparkSession\n      .builder()\n      .appName(\"UniForm-Delta\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .config(\"hive.metastore.uris\", s\"thrift://localhost:$port\")\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n\n    val schema =\n      \"\"\"\n        |col0 INT,\n        |col1 STRUCT<\n        |  col2: MAP<INT, INT>,\n        |  col3: ARRAY<INT>,\n        |  col4: STRUCT<col5: STRING>\n        |>,\n        |col6 INT,\n        |col7 INT\n        |\"\"\".stripMargin\n\n    def getRowToInsertStr(id: Int): String = {\n      s\"\"\"\n         |$id,\n         |struct(map($id, $id), array($id), struct($id)),\n         |$id,\n         |$id\n         |\"\"\".stripMargin\n    }\n\n    deltaSpark.sql(s\"DROP TABLE IF EXISTS ${testTableName}\")\n    deltaSpark.sql(\n      s\"\"\"CREATE TABLE `${testTableName}` ($schema) using DELTA\n         |PARTITIONED BY (col0, col6, col7)\n         |TBLPROPERTIES (\n         |  'delta.columnMapping.mode' = 'name',\n         |  'delta.enableIcebergCompatV2' = 'true',\n         |  'delta.universalFormat.enabledFormats' = 'iceberg'\n         |)\"\"\".stripMargin)\n    deltaSpark.sql(s\"INSERT INTO $testTableName VALUES (${getRowToInsertStr(1)})\")\n\n    // Wait for the conversion to be done\n    Thread.sleep(10000)\n\n    val icebergSpark = SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"UniForm-Iceberg\")\n      .config(\"spark.sql.extensions\",\n        \"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.iceberg.spark.SparkSessionCatalog\")\n      .config(\"hive.metastore.uris\", s\"thrift://localhost:$port\")\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n\n    icebergSpark.sql(s\"SELECT * FROM ${testTableName}\").show()\n  }\n\n  def hmsReady(port: Int): Boolean = {\n    var ss: ServerSocket = null\n    try {\n      ss = new ServerSocket(port)\n      ss.setReuseAddress(true)\n      return false\n    } catch {\n      case e: IOException =>\n    } finally {\n      if (ss != null) {\n        try ss.close()\n        catch {\n          case e: IOException =>\n        }\n      }\n    }\n    true\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/UnityCatalogQuickstart.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport io.unitycatalog.client.ApiClientBuilder\nimport io.unitycatalog.client.api.{CatalogsApi, SchemasApi}\nimport io.unitycatalog.client.auth.TokenProvider\nimport io.unitycatalog.client.model.{CreateCatalog, CreateSchema}\nimport io.unitycatalog.server.UnityCatalogServer\nimport io.unitycatalog.server.utils.ServerProperties\nimport org.apache.commons.io.FileUtils\nimport org.apache.spark.sql.SparkSession\n\nimport java.io.File\nimport java.net.ServerSocket\nimport java.nio.file.Files\nimport java.util.Properties\nimport scala.collection.JavaConverters._\n\n/**\n * Example of testing streaming read from UC managed table with OSS UC \n */\nobject UnityCatalogQuickstart {\n\n  private val StaticToken = \"static-token\"\n\n  def main(args: Array[String]): Unit = {\n    val serverDir = Files.createTempDirectory(\"uc-integration-test-\").toFile\n    val tableDir = Files.createTempDirectory(\"uc-table-location-\").toFile\n\n    val port = {\n      val socket = new ServerSocket(0)\n      val p = socket.getLocalPort\n      socket.close()\n      p\n    }\n\n    val serverProps = new Properties()\n    serverProps.setProperty(\"server.env\", \"test\")\n    serverProps.setProperty(\"server.managed-table.enabled\", \"true\")\n    serverProps.setProperty(\"storage-root.tables\", new File(serverDir, \"ucroot\").getAbsolutePath)\n\n    val server = UnityCatalogServer.builder()\n      .port(port)\n      .serverProperties(new ServerProperties(serverProps))\n      .build()\n    server.start()\n\n    val serverUri = s\"http://localhost:$port/\"\n\n    try {\n      waitForServer(serverUri)\n      createCatalogAndSchema(serverUri)\n      runDeltaWorkload(serverUri, tableDir)\n      println(\"SUCCESS: Unity Catalog + Delta integration test passed\")\n    } finally {\n      server.stop()\n      FileUtils.deleteQuietly(serverDir)\n      FileUtils.deleteQuietly(tableDir)\n    }\n  }\n\n  private def waitForServer(serverUri: String): Unit = {\n    var ready = false\n    var retries = 0\n    while (!ready && retries < 30) {\n      try {\n        new CatalogsApi(createApiClient(serverUri)).listCatalogs(null, null)\n        ready = true\n      } catch {\n        case _: Exception =>\n          Thread.sleep(500)\n          retries += 1\n      }\n    }\n    if (!ready) {\n      throw new RuntimeException(\"Unity Catalog server did not become ready within 15 seconds\")\n    }\n  }\n\n  private def createCatalogAndSchema(serverUri: String): Unit = {\n    val client = createApiClient(serverUri)\n    new CatalogsApi(client).createCatalog(\n      new CreateCatalog().name(\"unity\").comment(\"Integration test catalog\"))\n    new SchemasApi(client).createSchema(\n      new CreateSchema().name(\"default\").catalogName(\"unity\"))\n  }\n\n  private def runDeltaWorkload(serverUri: String, tableDir: File): Unit = {\n    val spark = SparkSession.builder()\n      .appName(\"UC Delta Integration Test\")\n      .master(\"local[2]\")\n      .config(\"spark.ui.enabled\", \"false\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\",\n        \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .config(\"spark.sql.catalog.unity\", \"io.unitycatalog.spark.UCSingleCatalog\")\n      .config(\"spark.sql.catalog.unity.uri\", serverUri)\n      .config(\"spark.sql.catalog.unity.token\", StaticToken)\n      .getOrCreate()\n\n    try {\n      val checkpointPath = new File(tableDir, \"checkpoint\").getAbsolutePath\n\n      spark.sql(\n        s\"\"\"CREATE TABLE unity.default.test_table (id BIGINT, data STRING)\n           |USING DELTA\n           |TBLPROPERTIES('delta.feature.catalogManaged' = 'supported')\"\"\".stripMargin)\n\n      spark.sql(\"INSERT INTO unity.default.test_table VALUES (1, 'hello'), (2, 'world')\")\n\n      val stream = spark.readStream\n        .table(\"unity.default.test_table\")\n        .writeStream\n        .format(\"console\")\n        .option(\"checkpointLocation\", checkpointPath)\n        .start()\n\n      stream.awaitTermination(10000)\n      stream.stop()\n\n      spark.sql(\"DROP TABLE IF EXISTS unity.default.test_table\")\n    } finally {\n      spark.stop()\n    }\n  }\n\n  private def createApiClient(serverUri: String) = {\n    ApiClientBuilder.create()\n      .uri(serverUri)\n      .tokenProvider(\n        TokenProvider.create(Map(\"type\" -> \"static\", \"token\" -> StaticToken).asJava))\n      .build()\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/Utilities.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage example\n\nimport java.io.File\n\nimport io.delta.tables.DeltaTable\nimport org.apache.commons.io.FileUtils\n\nimport org.apache.spark.sql.SparkSession\n\nobject Utilities {\n  def main(args: Array[String]): Unit = {\n    // Create a Spark Session with SQL enabled\n    val spark = SparkSession\n      .builder()\n      .appName(\"Utilities\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      // control the parallelism for vacuum\n      .config(\"spark.sql.sources.parallelPartitionDiscovery.parallelism\", \"4\")\n      .getOrCreate()\n\n    // Create a table\n    println(\"Create a parquet table\")\n    val data = spark.range(0, 5)\n    val file = new File(\"/tmp/parquet-table\")\n    val path = file.getAbsolutePath\n    data.write.format(\"parquet\").save(path)\n\n    // Convert to delta\n    println(\"Convert to Delta\")\n    DeltaTable.convertToDelta(spark, s\"parquet.`$path`\")\n\n    // Read table as delta\n    var df = spark.read.format(\"delta\").load(path)\n\n    // Read old version of data using time travel\n    df = spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(path)\n    df.show()\n\n    val deltaTable = DeltaTable.forPath(path)\n\n    // Utility commands\n    println(\"Vacuum the table\")\n    deltaTable.vacuum()\n\n    println(\"Describe History for the table\")\n    deltaTable.history().show()\n\n    println(\"Describe Details for the table\")\n    deltaTable.detail().show()\n\n    // Generate manifest\n    println(\"Generate Manifest files\")\n    deltaTable.generate(\"SYMLINK_FORMAT_MANIFEST\")\n\n    // SQL utility commands\n    println(\"SQL Vacuum\")\n    spark.sql(s\"VACUUM '$path' RETAIN 169 HOURS\")\n\n    println(\"SQL Describe History\")\n    println(spark.sql(s\"DESCRIBE HISTORY '$path'\").collect())\n\n    // Cleanup\n    FileUtils.deleteDirectory(new File(path))\n    spark.stop()\n  }\n}\n"
  },
  {
    "path": "examples/scala/src/main/scala/example/Variant.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage example\n\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.SparkSession\n\nobject Variant {\n\n  def main(args: Array[String]): Unit = {\n    val tableName = \"tbl\"\n\n    val spark = SparkSession\n      .builder()\n      .appName(\"Variant-Delta\")\n      .master(\"local[*]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n\n    // Only run this example for Spark versions >= 4.0.0\n    if (spark.version.split(\"\\\\.\").head.toInt < 4) {\n      println(s\"Skipping Variant.scala since Spark version ${spark.version} is too low\")\n      return\n    }\n\n    // Create and insert variant values.\n    try {\n      println(\"Creating and inserting variant values\")\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n      spark.sql(s\"CREATE TABLE $tableName(v VARIANT) USING DELTA\")\n      spark.sql(s\"INSERT INTO $tableName VALUES (parse_json('1'))\")\n      spark.sql(s\"\"\"INSERT INTO $tableName SELECT parse_json(format_string('{\\\"k\\\": %s}', id))\n                FROM range(0, 10)\"\"\")\n      val ids = spark.sql(\"SELECT variant_get(v, '$.k', 'INT') out \" +\n                          s\"\"\"FROM $tableName WHERE contains(schema_of_variant(v), 'k')\n                          ORDER BY out\"\"\")\n                          .collect().map { r => r.getInt(0) }.toSeq\n      val expected = (0 until 10).toSeq\n      assert(expected == ids)\n\n      spark.sql(s\"DELETE FROM $tableName WHERE variant_get(v, '$$.k', 'INT') = 0\")\n      val idsWithDelete = spark.sql(\"SELECT variant_get(v, '$.k', 'INT') out \" +\n                                    s\"\"\"FROM $tableName WHERE contains(schema_of_variant(v), 'k')\n                                    ORDER BY out\"\"\")\n                                    .collect().map { r => r.getInt(0) }.toSeq\n      val expectedWithDelete = (1 until 10).toSeq\n      assert(idsWithDelete == expectedWithDelete)\n    } finally {\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n    }\n    \n    // Convert Parquet table with variant values to Delta.\n    try {\n      println(\"Converting a parquet table with variant values to Delta\")\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n      spark.sql(s\"\"\"CREATE TABLE $tableName USING PARQUET AS (\n        SELECT parse_json(format_string('%s', id)) v FROM range(0, 10))\"\"\")\n      spark.sql(s\"CONVERT TO DELTA $tableName\")\n      val convertToDeltaIds = spark.sql(s\"SELECT v::int v FROM $tableName ORDER BY v\")\n        .collect()\n        .map { r => r.getInt(0) }\n        .toSeq\n      val convertToDeltaExpected = (0 until 10).toSeq\n      assert(convertToDeltaIds == convertToDeltaExpected)\n    } finally {\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n    }\n\n    // DeltaTable create with variant Scala API.\n    try {\n      println(\"Creating a delta table with variant type using the DeltaTable API\")\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n      val table = io.delta.tables.DeltaTable.create()\n        .tableName(tableName)\n        .addColumn(\"v\", \"VARIANT\")\n        .execute()\n\n      table\n        .as(\"tgt\")\n        .merge(\n          spark.sql(\"select parse_json(format_string('%s', id)) v from range(0, 10)\").as(\"source\"),\n          \"source.v::int == tgt.v::int\"\n        )\n        .whenMatched()\n        .updateAll()\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n      val insertedVals = spark.sql(s\"SELECT v::int v FROM $tableName ORDER BY v\")\n        .collect()\n        .map { r => r.getInt(0) }\n        .toSeq\n      val expected = (0 until 10).toSeq\n      assert(insertedVals == expected)\n    } finally {\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n      spark.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/.placeholder",
    "content": ""
  },
  {
    "path": "flink/src/main/java/io/delta/flink/Conf.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Global configuration for Delta Flink sinks.\n *\n * <p>This class loads process-wide configuration from the {@code delta-flink.properties} file and\n * exposes shared settings that apply to <em>all</em> {@code DeltaSink} instances within the JVM.\n * These configurations are intended for operational tuning and common defaults, such as retry\n * behavior, thread pool sizing, caching, and credential refresh.\n *\n * <p>The configuration is loaded once and managed as a singleton. All sinks created in the same\n * process observe the same global configuration values.\n *\n * <p>Per-sink or per-table behavior should be configured explicitly when constructing the {@code\n * DeltaSink} instance. Sink-level configuration takes precedence over global defaults defined here.\n *\n * <p>This class is not intended to be instantiated or mutated directly by users.\n */\npublic final class Conf {\n\n  private static final Logger LOG = LoggerFactory.getLogger(Conf.class);\n\n  public static String SINK_RETRY_MAX_ATTEMPT = \"sink.retry.max_attempt\";\n  // The i-th retry will have a delay of `delay-ms * (2 ^ i)`\n  public static String SINK_RETRY_DELAY_MS = \"sink.retry.delay_ms\";\n  // Retry will stop if the delay exceeds max-delay\n  public static String SINK_RETRY_MAX_DELAY_MS = \"sink.retry.max_delay_ms\";\n\n  public static String SINK_WRITER_NUM_CONCURRENT_FILE = \"sink.writer.num_concurrent_file\";\n\n  public static String TABLE_THREAD_POOL_SIZE = \"table.thread_pool_size\";\n\n  public static String TABLE_CACHE_ENABLE = \"table.cache.enable\";\n  public static String TABLE_CACHE_SIZE = \"table.cache.size\";\n  public static String TABLE_CACHE_EXPIRE_MS = \"table.cache.expire_ms\";\n\n  public static String CREDENTIALS_REFRESH_THREAD_POOL_SIZE =\n      \"credentials.refresh.thread_pool_size\";\n  public static String CREDENTIALS_REFRESH_AHEAD_MS = \"credentials.refresh.ahead_ms\";\n\n  private static final String CONFIG_FILE = \"delta-flink.properties\";\n  private static final Conf INSTANCE = new Conf();\n\n  private final Map<String, String> props;\n  // For debug purpose\n  private URL sourcePath;\n\n  private Conf() {\n    this.props = load();\n  }\n\n  public static Conf getInstance() {\n    return INSTANCE;\n  }\n\n  /*================\n   * Confs\n   *================*/\n\n  public int getSinkRetryMaxAttempt() {\n    return Integer.parseInt(getOrDefault(SINK_RETRY_MAX_ATTEMPT, \"4\"));\n  }\n\n  public long getSinkRetryDelayMs() {\n    return Long.parseLong(getOrDefault(SINK_RETRY_DELAY_MS, \"200\"));\n  }\n\n  public long getSinkRetryMaxDelayMs() {\n    return Long.parseLong(getOrDefault(SINK_RETRY_MAX_DELAY_MS, \"20000\"));\n  }\n\n  public int getSinkWriterNumConcurrentFiles() {\n    return Integer.parseInt(getOrDefault(SINK_WRITER_NUM_CONCURRENT_FILE, \"1000\"));\n  }\n\n  public int getTableThreadPoolSize() {\n    return Integer.parseInt(getOrDefault(TABLE_THREAD_POOL_SIZE, \"5\"));\n  }\n\n  public boolean getTableCacheEnable() {\n    return Boolean.parseBoolean(getOrDefault(TABLE_CACHE_ENABLE, \"true\"));\n  }\n\n  public int getTableCacheSize() {\n    return Integer.parseInt(getOrDefault(TABLE_CACHE_SIZE, \"100\"));\n  }\n\n  public long getTableCacheExpireInMs() {\n    return Long.parseLong(getOrDefault(TABLE_CACHE_EXPIRE_MS, \"300000\"));\n  }\n\n  public int getCredentialsRefreshThreadPoolSize() {\n    return Integer.parseInt(getOrDefault(CREDENTIALS_REFRESH_THREAD_POOL_SIZE, \"10\"));\n  }\n\n  public long getCredentialsRefreshAheadInMs() {\n    return Long.parseLong(getOrDefault(CREDENTIALS_REFRESH_AHEAD_MS, \"60000\"));\n  }\n\n  /** Returns an immutable view of all configuration entries. */\n  public Map<String, String> asMap() {\n    return props;\n  }\n\n  /** Returns a configuration value or null if missing. */\n  public String get(String key) {\n    return props.get(key);\n  }\n\n  /** Returns a configuration value or default if missing. */\n  public String getOrDefault(String key, String defaultValue) {\n    return props.getOrDefault(key, defaultValue);\n  }\n\n  // ----------------- internals -----------------\n  private Map<String, String> load() {\n    Properties p = new Properties();\n    try (InputStream in = Conf.class.getClassLoader().getResourceAsStream(CONFIG_FILE)) {\n      if (in == null) {\n        return Map.of();\n      }\n      sourcePath = Conf.class.getClassLoader().getResource(CONFIG_FILE);\n      LOG.info(\"Loaded configuration from {}\", sourcePath);\n      p.load(in);\n    } catch (IOException e) {\n      throw new RuntimeException(\"Failed to load \" + CONFIG_FILE, e);\n    }\n    Map<String, String> map = new HashMap<>();\n    for (String name : p.stringPropertyNames()) {\n      map.put(name, p.getProperty(name));\n    }\n    return Collections.unmodifiableMap(map);\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/kernel/CheckpointActionRow.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.kernel;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.checkpoints.*;\nimport io.delta.kernel.types.StructType;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.function.Function;\n\n/** Represent a row in a v2 checkpoint */\npublic class CheckpointActionRow implements Row {\n\n  /** Schema used to read/write v2 checkpoint files produced by this writer. */\n  public static final StructType CHECKPOINT_SCHEMA =\n      new StructType()\n          .add(\"checkpointMetadata\", CheckpointMetadataAction.FULL_SCHEMA)\n          .add(\"metaData\", Metadata.FULL_SCHEMA)\n          .add(\"protocol\", Protocol.FULL_SCHEMA)\n          .add(\"txn\", SetTransaction.FULL_SCHEMA)\n          .add(\"sidecar\", SidecarFile.READ_SCHEMA)\n          .add(\"domainMetadata\", DomainMetadata.FULL_SCHEMA)\n          .add(\"add\", AddFile.FULL_SCHEMA);\n\n  static final List<Function<Object, Row>> ROW_MAPPERS =\n      List.of(\n          obj -> ((CheckpointMetadataAction) obj).toRow(),\n          obj -> ((Metadata) obj).toRow(),\n          obj -> ((Protocol) obj).toRow(),\n          obj -> ((SetTransaction) obj).toRow(),\n          obj -> ((SidecarFile) obj).toRow(),\n          obj -> ((DomainMetadata) obj).toRow());\n\n  private final Object action;\n\n  public CheckpointActionRow(Object action) {\n    this.action = action;\n  }\n\n  @Override\n  public StructType getSchema() {\n    return CHECKPOINT_SCHEMA;\n  }\n\n  @Override\n  public boolean isNullAt(int ordinal) {\n    if (ordinal >= ROW_MAPPERS.size()) {\n      return true;\n    }\n    try {\n      ROW_MAPPERS.get(ordinal).apply(action);\n      return false;\n    } catch (ClassCastException e) {\n      return true;\n    }\n  }\n\n  @Override\n  public Row getStruct(int ordinal) {\n    try {\n      return ROW_MAPPERS.get(ordinal).apply(action);\n    } catch (ClassCastException e) {\n      return null;\n    }\n  }\n\n  @Override\n  public boolean getBoolean(int ordinal) {\n    return false;\n  }\n\n  @Override\n  public byte getByte(int ordinal) {\n    return 0;\n  }\n\n  @Override\n  public short getShort(int ordinal) {\n    return 0;\n  }\n\n  @Override\n  public int getInt(int ordinal) {\n    return 0;\n  }\n\n  @Override\n  public long getLong(int ordinal) {\n    return 0;\n  }\n\n  @Override\n  public float getFloat(int ordinal) {\n    return 0;\n  }\n\n  @Override\n  public double getDouble(int ordinal) {\n    return 0;\n  }\n\n  @Override\n  public String getString(int ordinal) {\n    return \"\";\n  }\n\n  @Override\n  public BigDecimal getDecimal(int ordinal) {\n    return null;\n  }\n\n  @Override\n  public byte[] getBinary(int ordinal) {\n    return new byte[0];\n  }\n\n  @Override\n  public ArrayValue getArray(int ordinal) {\n    return null;\n  }\n\n  @Override\n  public MapValue getMap(int ordinal) {\n    return null;\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/kernel/CheckpointWriter.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.kernel;\n\nimport static io.delta.flink.kernel.CheckpointActionRow.CHECKPOINT_SCHEMA;\nimport static io.delta.kernel.internal.checkpoints.Checkpointer.LAST_CHECKPOINT_FILE_NAME;\nimport static io.delta.kernel.internal.util.FileNames.*;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\n\nimport io.delta.flink.table.ExceptionUtils;\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.internal.data.DefaultRowBasedColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.engine.FileReadResult;\nimport io.delta.kernel.internal.DeltaLogActionUtils;\nimport io.delta.kernel.internal.DeltaLogActionUtils.DeltaAction;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.SetTransaction;\nimport io.delta.kernel.internal.checkpoints.CheckpointMetaData;\nimport io.delta.kernel.internal.checkpoints.CheckpointMetadataAction;\nimport io.delta.kernel.internal.checkpoints.SidecarFile;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.LongStream;\nimport java.util.stream.Stream;\nimport org.apache.flink.util.Preconditions;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Flink-specialized Delta v2 checkpoint writer.\n *\n * <p>This writer assumes the table is configured to use v2 checkpoints with sidecar files and is\n * most efficient when used for incremental checkpoint creation on Flink sink tables.\n *\n * <p>Because the Flink sink performs blind appends, the writer generates a new sidecar file\n * containing all {@code AddFile} and {@code SetTransaction} actions in the range {@code\n * (previousCheckpointVersion + 1, currentSnapshotVersion]}. It then writes a new singular v2\n * checkpoint that includes:\n *\n * <ul>\n *   <li>protocol, metadata, and checkpointMetadata actions\n *   <li>the newly generated sidecar file\n *   <li>all sidecars referenced by the previous checkpoint, if that checkpoint was written by this\n *       class\n *   <li>aggregated {@code SetTransaction} actions computed from commits in the version range\n * </ul>\n *\n * If the writer detects that the number of existing sidecar files exceeds a threshold, it will\n * merge them into a new single sidecar files to reduce the total number of files.\n *\n * <p>## Fallback If any of the condition is true, the writer will ignore all existing sidecars, and\n * create a new checkpoint with a single sidecar containing all actions up to the version.\n *\n * <ul>\n *   <li>No previous checkpoint exists\n *   <li>{@code _last_checkpoint} is not tagged with {@link #TAG_DELTASINK_CHECKPOINT}.\n *   <li>Remove files appear since the last checkpoint.\n *   <li>this writer is invoked on a snapshot version earlier than the version recorded in {@code *\n *       _last_checkpoint}\n * </ul>\n *\n * We choose to not support reading checkpoints written by other writers assuming that the case is\n * rare. The support can be added in the future if being requested.\n *\n * <p>## Limitations This writer does not support tables with domain metadata feature. The support\n * will be added in the future.\n */\npublic class CheckpointWriter {\n\n  /**\n   * Tag written into {@code _last_checkpoint} to indicate the checkpoint was produced by DeltaSink.\n   */\n  public static final String TAG_DELTASINK_CHECKPOINT = \"io.delta.flink.sink.checkpoint\";\n\n  public static final String TAG_SIDECAR_COUNT = \"io.delta.flink.num_sidecar\";\n\n  private static final Logger LOG = LoggerFactory.getLogger(CheckpointWriter.class);\n\n  private static final StructType SIDECAR_SCHEMA = new StructType().add(\"add\", AddFile.FULL_SCHEMA);\n  private static final CheckpointMetaData EMPTY_META =\n      new CheckpointMetaData(-1L, 0, Optional.empty(), Map.of());\n\n  private static <T> CloseableIterator<T> EMPTY_ITERATOR() {\n    return toCloseableIterator(Collections.emptyIterator());\n  }\n\n  private final Engine engine;\n  private final SnapshotImpl snapshot;\n  private final Path lastCheckpointFilePath;\n  private final int sidecarMergeThreshold;\n\n  private final CheckpointMetaData lastCheckpointMeta;\n  private final boolean lastCheckpointByMe;\n  private int lastSidecarCount;\n  /** Guard to prevent reusing a single writer instance. */\n  private boolean used = false;\n\n  /** Max transaction version per appId observed while scanning commits. */\n  private final Map<String, Long> transactionIds = new HashMap<>();\n\n  private final Map<String, String> domainMetadatas = new HashMap<>();\n\n  /**\n   * Creates a checkpoint writer bound to a snapshot.\n   *\n   * @param engine kernel engine\n   * @param snapshot snapshot to checkpoint\n   * @param sidecarMergeThreshold threshold to merge sidecars. Set negative to disable merging.\n   */\n  public CheckpointWriter(Engine engine, Snapshot snapshot, int sidecarMergeThreshold) {\n    this.engine = engine;\n    this.snapshot = (SnapshotImpl) snapshot;\n\n    Preconditions.checkArgument(\n        this.snapshot.getProtocol().supportsFeature(TableFeatures.CHECKPOINT_V2_RW_FEATURE));\n    if (this.snapshot.getProtocol().supportsFeature(TableFeatures.CATALOG_MANAGED_RW_FEATURE)) {\n      // Make sure we are creating checkpoint for a published version\n      Preconditions.checkArgument(\n          this.snapshot.getLogSegment().getMaxPublishedDeltaVersion().orElse(-1L)\n              >= this.snapshot.getVersion());\n    }\n\n    Preconditions.checkArgument(sidecarMergeThreshold < 0 || sidecarMergeThreshold >= 2);\n    this.sidecarMergeThreshold = sidecarMergeThreshold;\n\n    // Read information from _last_checkpoint\n    this.lastCheckpointFilePath = new Path(this.snapshot.getLogPath(), LAST_CHECKPOINT_FILE_NAME);\n    lastCheckpointMeta = readLastCheckpointInfo();\n    lastCheckpointByMe =\n        Boolean.parseBoolean(\n            lastCheckpointMeta.tags.getOrDefault(TAG_DELTASINK_CHECKPOINT, \"false\"));\n    lastSidecarCount = 0;\n    try {\n      lastSidecarCount =\n          Integer.parseInt(lastCheckpointMeta.tags.getOrDefault(TAG_SIDECAR_COUNT, \"0\"));\n    } catch (Exception ignore) {\n    }\n  }\n\n  public CheckpointWriter(Engine engine, Snapshot snapshot) {\n    this(engine, snapshot, -1);\n  }\n\n  /**\n   * Writes an incremental v2 checkpoint for the bound snapshot.\n   *\n   * <p>This operation includes the following steps:\n   *\n   * <ul>\n   *   <li>1. Determine the baseCheckpoint.\n   *   <li>2. Fetch the actions between baseCheckpoint and current version.\n   *   <li>3. If the actions do not contain remove files, generate a new sidecar from them, and read\n   *       existing sidecars from baseCheckpoint. Otherwise, generate a new sidecar for all actions\n   *       in current snapshot, and DO NOT read existing sidecars.\n   *   <li>4. Write existing sidecars and new sidecar as a new V2 checkpoint.\n   *   <li>5. Update _last_checkpoint\n   * </ul>\n   *\n   * <p><b>Note:</b> This method is single-use. Calling it more than once on the same instance\n   * throws {@link IllegalStateException}.\n   *\n   * @throws IOException if reading commit files or writing checkpoint/sidecar files fails\n   */\n  public void write() throws IOException {\n    if (used) {\n      throw new IllegalStateException(\"Checkpoint writer must not be reused.\");\n    }\n    used = true;\n    transactionIds.clear();\n\n    Path logPath = snapshot.getLogPath();\n    long version = snapshot.getVersion();\n\n    // ===========\n    // Step 1:\n    // ===========\n    // Use _last_checkpoint as baseCheckpoint when\n    // 1. It is written by this writer\n    // 2. _last_checkpoint version is smaller than this snapshot version\n    // Otherwise assume there's no baseCheckpoint\n    // ====================================================================\n    Optional<CheckpointMetaData> baseCheckpointMeta =\n        (lastCheckpointByMe && lastCheckpointMeta.version < version)\n            ? Optional.of(lastCheckpointMeta)\n            : Optional.empty();\n    long baseVersion = baseCheckpointMeta.map(m -> m.version).orElse(-1L);\n\n    Path baseCheckpointPath = checkpointFileSingular(logPath, baseVersion);\n    Path newCheckpointPath = checkpointFileSingular(logPath, version);\n\n    // ============\n    // Step 2\n    // ============\n    // Read actions between (baseCheckpoint.version, version]. We can assume this since we require\n    // that the snapshot be fully published on catalog-managed tables\n    List<FileStatus> deltaFiles =\n        LongStream.range(baseVersion + 1, version + 1)\n            .mapToObj(v -> FileStatus.of(FileNames.deltaFile(snapshot.getLogPath(), v)))\n            .collect(Collectors.toList());\n\n    AtomicInteger addFileCounter = new AtomicInteger();\n    AtomicInteger removeFileCounter = new AtomicInteger();\n\n    // ===========\n    // Step 3\n    // ===========\n    // Read AddFile and txn actions from incremental commit files, and generate a new sidecar\n    // including AddFiles. It also checks if remove file exists.\n    SidecarFile newSidecar;\n    CloseableIterator<FilteredColumnarBatch> existingSidecars = EMPTY_ITERATOR();\n\n    try (CloseableIterator<FilteredColumnarBatch> actions =\n        DeltaLogActionUtils.getActionsFromCommitFilesWithProtocolValidation(\n                engine,\n                snapshot.getPath(),\n                deltaFiles,\n                Set.of(\n                    DeltaAction.ADD,\n                    DeltaAction.REMOVE,\n                    DeltaAction.TXN,\n                    DeltaAction.DOMAINMETADATA))\n            .flatMap(CommitActions::getActions)\n            .map(filterActions(Map.of(\"add\", addFileCounter, \"remove\", removeFileCounter)))) {\n      newSidecar = sidecarFromAddFiles(actions);\n      // If remove file exists, fallback to generating a new sidecar including everything.\n      if (removeFileCounter.get() > 0) {\n        try (CloseableIterator<FilteredColumnarBatch> allActions =\n            snapshot.getCreateCheckpointIterator(engine).map(filterAddFiles())) {\n          newSidecar = sidecarFromAddFiles(allActions);\n        }\n      } else if (baseCheckpointMeta.isPresent()) {\n        // When there's no remove files, read existing sidecars from the base checkpoint if it\n        // exists\n        existingSidecars = sidecarsFromCheckpoint(baseCheckpointPath);\n      }\n    }\n    // ==========\n    // Step 4\n    // ==========\n    // Build new checkpoint including:\n    // - protocol\n    // - metadata\n    // - checkpointMetadata\n    // - txn\n    // - domainMetadata\n    // - existing sidecars\n    // - new sidecar.\n    CheckpointMetadataAction checkpointMetadata = new CheckpointMetadataAction(version, Map.of());\n\n    try (CloseableIterator<FilteredColumnarBatch> merged =\n        rowsToBatch(\n                Stream.of(\n                    snapshot.getProtocol(), snapshot.getMetadata(), checkpointMetadata, newSidecar))\n            .combine(existingSidecars)\n            .combine(rowsToBatch(getTransactions()))\n            .combine(rowsToBatch(getDomainMetadatas()))) {\n      engine\n          .getParquetHandler()\n          .writeParquetFileAtomically(String.valueOf(newCheckpointPath), merged);\n    }\n\n    // ==========\n    // Step 5\n    // ==========\n    // Write _last_checkpoint file with our tag, so we can recognize our own checkpoints later.\n    if (version > lastCheckpointMeta.version) {\n      engine\n          .getJsonHandler()\n          .writeJsonFileAtomically(\n              lastCheckpointFilePath.toString(),\n              singletonCloseableIterator(\n                  new CheckpointMetaData(\n                          version,\n                          lastCheckpointMeta.size + addFileCounter.get() - removeFileCounter.get(),\n                          Optional.empty(),\n                          Map.of(\n                              TAG_DELTASINK_CHECKPOINT,\n                              \"true\",\n                              TAG_SIDECAR_COUNT,\n                              String.valueOf(lastSidecarCount + 1)))\n                      .toRow()),\n              true /* overwrite */);\n    }\n  }\n\n  /**\n   * Read existing sidecars from the given checkpoint Path. If the total number of sidecars exceeds\n   * the threshold, merge existing sidecars into one to reduce the number of files to read.\n   *\n   * @param checkpointPath path for the checkpoint\n   * @return an iterator of sidecars fetched from the checkpoint.\n   * @throws IOException when exception happens during read or write.\n   */\n  private CloseableIterator<FilteredColumnarBatch> sidecarsFromCheckpoint(Path checkpointPath)\n      throws IOException {\n    AtomicInteger sidecarCounter = new AtomicInteger();\n    CloseableIterator<FilteredColumnarBatch> existingSidecars;\n    existingSidecars =\n        engine\n            .getParquetHandler()\n            .readParquetFiles(\n                singletonCloseableIterator(getFileStatus(checkpointPath)),\n                CHECKPOINT_SCHEMA,\n                Optional.empty())\n            .map(FileReadResult::getData)\n            .map(filterActions(Map.of(\"sidecar\", sidecarCounter)));\n\n    if (sidecarMergeThreshold > 0 && lastSidecarCount >= sidecarMergeThreshold - 1) {\n      // Too many existing sidecars. Merge them into one.\n      try (CloseableIterator<FileStatus> sidecarFiles =\n              existingSidecars\n                  .flatMap(FilteredColumnarBatch::getRows)\n                  .map(\n                      row -> {\n                        Row sidecar = row.getStruct(CHECKPOINT_SCHEMA.indexOf(\"sidecar\"));\n                        String path = sidecar.getString(SidecarFile.READ_SCHEMA.indexOf(\"path\"));\n                        Path fullPath = new Path(sidecarFile(snapshot.getLogPath(), path));\n                        return getFileStatus(fullPath);\n                      });\n          CloseableIterator<FilteredColumnarBatch> addFileRows =\n              engine\n                  .getParquetHandler()\n                  .readParquetFiles(sidecarFiles, SIDECAR_SCHEMA, Optional.empty())\n                  .map(FileReadResult::getData)\n                  .map(ColumnVectorUtils::wrap)) {\n        existingSidecars = rowsToBatch(Stream.of(sidecarFromAddFiles(addFileRows)));\n        lastSidecarCount = 1;\n      }\n    }\n    return existingSidecars;\n  }\n\n  /**\n   * Return a mapping function that further filter add files from a filtered column batch\n   *\n   * @return a mapping function that applies to a filtered column batch\n   */\n  private Function<FilteredColumnarBatch, FilteredColumnarBatch> filterAddFiles() {\n    return (input) -> {\n      int addOrdinal = input.getData().getSchema().indexOf(\"add\");\n      return new FilteredColumnarBatch(\n          input.getData(),\n          ColumnVectorUtils.filter(\n              input.getData().getSize(),\n              (rowId) ->\n                  input.getSelectionVector().map(cv -> cv.getBoolean(rowId)).orElse(true)\n                      && !input.getData().getColumnVector(addOrdinal).isNullAt(rowId)));\n    };\n  }\n\n  /**\n   * Returns a mapping function that:\n   *\n   * <ul>\n   *   <li>Aggregates {@code txn} actions into {@link #transactionIds} (max version per appId)\n   *   <li>Filters rows where {@code notNullName} is non-null\n   *   <li>Optionally increments {@code counter} for each retained row\n   * </ul>\n   *\n   * <p>This is used both for selecting {@code AddFile} rows (\"add\") when generating the sidecar and\n   * for selecting {@code sidecar} rows when merging sidecar references from a prior checkpoint.\n   *\n   * @param nameToCounters a map of key: name of the column that must be non-null for a row to be\n   *     retained. value: counter incremented for each retained row;\n   * @return mapping function that applies the filter (and aggregation side-effects) to a batch\n   */\n  private Function<ColumnarBatch, FilteredColumnarBatch> filterActions(\n      Map<String, AtomicInteger> nameToCounters) {\n    return (columnarBatch) -> {\n      int txnOrdinal = columnarBatch.getSchema().indexOf(\"txn\");\n      int dmOrdinal = columnarBatch.getSchema().indexOf(\"domainMetadata\");\n\n      var entries = new ArrayList<>(nameToCounters.entrySet());\n      Integer[] ordinals = new Integer[nameToCounters.size()];\n      AtomicInteger[] counters = new AtomicInteger[nameToCounters.size()];\n      for (int i = 0; i < entries.size(); i++) {\n        ordinals[i] = columnarBatch.getSchema().indexOf(entries.get(i).getKey());\n        counters[i] = entries.get(i).getValue();\n      }\n\n      return new FilteredColumnarBatch(\n          columnarBatch,\n          ColumnVectorUtils.filter(\n              columnarBatch.getSize(),\n              (rowId) -> {\n                ColumnVector txnVector = columnarBatch.getColumnVector(txnOrdinal);\n                if (!txnVector.isNullAt(rowId)) {\n                  String appId = txnVector.getChild(0).getString(rowId);\n                  long txnVersion = txnVector.getChild(1).getLong(rowId);\n                  transactionIds.merge(appId, txnVersion, Math::max);\n                }\n                ColumnVector dmVector = columnarBatch.getColumnVector(dmOrdinal);\n                if (!dmVector.isNullAt(rowId)) {\n                  String domain = dmVector.getChild(0).getString(rowId);\n                  String configuration = dmVector.getChild(1).getString(rowId);\n                  boolean removed = dmVector.getChild(2).getBoolean(rowId);\n                  if (removed) {\n                    domainMetadatas.remove(domain);\n                  } else {\n                    domainMetadatas.put(domain, configuration);\n                  }\n                }\n                for (int i = 0; i < ordinals.length; i++) {\n                  if (!columnarBatch.getColumnVector(ordinals[i]).isNullAt(rowId)) {\n                    counters[i].incrementAndGet();\n                    return true;\n                  }\n                }\n                return false;\n              }));\n    };\n  }\n\n  /** Reads {@code _last_checkpoint} and returns the checkpoint metadata. */\n  private CheckpointMetaData readLastCheckpointInfo() {\n    try (CloseableIterator<ColumnarBatch> jsonIter =\n        engine\n            .getJsonHandler()\n            .readJsonFiles(\n                singletonCloseableIterator(FileStatus.of(lastCheckpointFilePath.toString())),\n                CheckpointMetaData.READ_SCHEMA,\n                Optional.empty())) {\n      return InternalUtils.getSingularRow(jsonIter)\n          .map(CheckpointMetaData::fromRow)\n          .orElse(EMPTY_META);\n    } catch (Exception ignore) {\n      // Best-effort: absence or parse errors mean \"no usable previous checkpoint.\"\n      return EMPTY_META;\n    }\n  }\n\n  /* Wrap a stream of actions as column batches */\n  private CloseableIterator<FilteredColumnarBatch> rowsToBatch(Stream<Object> input) {\n    FilteredColumnarBatch checkpointContent =\n        new FilteredColumnarBatch(\n            new DefaultRowBasedColumnarBatch(\n                CHECKPOINT_SCHEMA,\n                input.map(CheckpointActionRow::new).collect(Collectors.toList())),\n            Optional.empty());\n    return singletonCloseableIterator(checkpointContent);\n  }\n\n  /* write a new sidecar from the given addfiles */\n  private SidecarFile sidecarFromAddFiles(CloseableIterator<FilteredColumnarBatch> actions)\n      throws IOException {\n    String sidecarName = UUID.randomUUID().toString();\n    Path sidecarPath = v2CheckpointSidecarFile(snapshot.getLogPath(), sidecarName);\n    engine.getParquetHandler().writeParquetFileAtomically(String.valueOf(sidecarPath), actions);\n    FileStatus fileStatus = getFileStatus(sidecarPath);\n    return new SidecarFile(\n        String.format(\"%s.parquet\", sidecarName), // Kernel does not support absolute paths.\n        fileStatus.getSize(),\n        fileStatus.getModificationTime());\n  }\n\n  /**\n   * Resolves the {@link FileStatus} for a path using the engine filesystem client.\n   *\n   * @param path file path\n   * @return file status\n   */\n  private FileStatus getFileStatus(Path path) {\n    try {\n      return engine.getFileSystemClient().getFileStatus(path.toString());\n    } catch (IOException e) {\n      throw ExceptionUtils.wrap(e);\n    }\n  }\n\n  private Stream<Object> getTransactions() {\n    return transactionIds.entrySet().stream()\n        .map(e -> new SetTransaction(e.getKey(), e.getValue(), Optional.empty()));\n  }\n\n  private Stream<Object> getDomainMetadatas() {\n    return domainMetadatas.entrySet().stream()\n        .map(e -> new DomainMetadata(e.getKey(), e.getValue(), false));\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/kernel/ColumnVectorUtils.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.kernel;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\npublic class ColumnVectorUtils {\n\n  public static FilteredColumnarBatch wrap(ColumnarBatch data) {\n    return new FilteredColumnarBatch(data, Optional.empty());\n  }\n\n  public static Function<ColumnarBatch, FilteredColumnarBatch> notNullAt(int ordinal) {\n    return (batch) -> new FilteredColumnarBatch(batch, notNull(batch.getColumnVector(ordinal)));\n  }\n\n  public static Function<FilteredColumnarBatch, FilteredColumnarBatch> child(String childName) {\n    return (batch) -> {\n      StructType childSchema =\n          (StructType) batch.getData().getSchema().get(childName).getDataType();\n      int childIndex = batch.getData().getSchema().indexOf(childName);\n      ColumnarBatch newData =\n          new ColumnarBatch() {\n            @Override\n            public StructType getSchema() {\n              return childSchema;\n            }\n\n            @Override\n            public ColumnVector getColumnVector(int ordinal) {\n              return batch.getData().getColumnVector(childIndex).getChild(ordinal);\n            }\n\n            @Override\n            public int getSize() {\n              return batch.getData().getSize();\n            }\n          };\n      return new FilteredColumnarBatch(newData, batch.getSelectionVector());\n    };\n  }\n\n  /**\n   * Create a column vector that filter out data based on the pred(data, rowId)\n   *\n   * @param size the filter size\n   * @param pred a predicate taking (data, rowId) as input\n   * @return a column vector masking out the rows with pred returning false\n   */\n  public static Optional<ColumnVector> filter(int size, Predicate<Integer> pred) {\n    return Optional.of(\n        new ColumnVector() {\n          @Override\n          public DataType getDataType() {\n            return BooleanType.BOOLEAN;\n          }\n\n          @Override\n          public int getSize() {\n            return size;\n          }\n\n          @Override\n          public void close() {}\n\n          @Override\n          public boolean isNullAt(int rowId) {\n            return false;\n          }\n\n          @Override\n          public boolean getBoolean(int rowId) {\n            return pred.test(rowId);\n          }\n        });\n  }\n\n  public static Optional<ColumnVector> notNull(ColumnVector input) {\n    return filter(input.getSize(), (rowId) -> !input.isNullAt(rowId));\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/AbstractKernelTable.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport dev.failsafe.Failsafe;\nimport dev.failsafe.Fallback;\nimport dev.failsafe.RetryPolicy;\nimport dev.failsafe.function.CheckedRunnable;\nimport dev.failsafe.function.CheckedSupplier;\nimport io.delta.flink.Conf;\nimport io.delta.flink.table.postcommit.ChecksumListener;\nimport io.delta.flink.table.postcommit.MaintenanceListener;\nimport io.delta.kernel.*;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.defaults.internal.json.JsonUtils;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.TableAlreadyExistsException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.DeltaLogActionUtils;\nimport io.delta.kernel.internal.data.TransactionStateRow;\nimport io.delta.kernel.transaction.CreateTableTransactionBuilder;\nimport io.delta.kernel.transaction.DataLayoutSpec;\nimport io.delta.kernel.transaction.UpdateTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.io.File;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * An abstract base class for {@link DeltaTable} implementations backed by the Delta Kernel.\n *\n * <p>{@code AbstractKernelTable} provides common functionality for interacting with Delta tables,\n * including access to table metadata, schema, partitioning information, and commit operations.\n * Concrete subclasses are responsible for supplying catalog-specific or filesystem-specific logic\n * such as table discovery, path resolution, and storage I/O.\n *\n * <p>This class centralizes shared behavior so that different table backends (e.g., Hadoop-based\n * tables, catalog-managed tables, custom catalogs) can implement only the backend-specific portions\n * while inheriting consistent Delta table semantics.\n *\n * <p>Subclasses must provide their own mechanisms for interpreting table identifiers and resolving\n * them into physical locations or catalog entries. See also @link{io.delta.flink.table.Catalog}\n */\npublic abstract class AbstractKernelTable implements DeltaTable {\n\n  protected static String ENGINE_INFO = \"DeltaSink\";\n  protected static Logger LOG = LoggerFactory.getLogger(AbstractKernelTable.class);\n\n  /**\n   * Normalizes the given URI string to a canonical form. The normalization includes:\n   *\n   * <ul>\n   *   <li>Ensuring file URIs use the standard triple-slash form (e.g., {@code file:/abc/def} →\n   *       {@code file:///abc/def}).\n   *   <li>Appending a trailing slash to paths that do not already end with {@code /}.\n   * </ul>\n   *\n   * <p>This method is useful for making URI comparisons consistent and avoiding issues caused by\n   * variations in file URI formatting or missing trailing path delimiters.\n   *\n   * @param input the URI to normalize;\n   * @return the normalized URI\n   */\n  public static URI normalize(URI input) {\n    if (input == null) {\n      return null;\n    }\n    URI target = input;\n    if (target.getScheme() == null) {\n      target = new File(input.toString()).toPath().toUri();\n    } else if (target.getScheme().equals(\"file\")) {\n      // Normalize \"file:/xxx/\" to \"file:///xxx/\"\n      target = new File(input).toPath().toUri();\n    }\n    try {\n      // Normalize \"abc://def/xxx\" to \"abc://def/xxx/\"\n      if (!target.getPath().endsWith(\"/\")) {\n        target =\n            new URI(\n                target.getScheme(),\n                Optional.ofNullable(target.getHost()).orElse(\"\"),\n                target.getPath() + \"/\",\n                target.getFragment());\n      }\n    } catch (URISyntaxException e) {\n      throw new RuntimeException(e);\n    }\n    return target;\n  }\n\n  /**\n   * Normalize the provided partition column names.\n   *\n   * @param rawPartitions input list of column names.\n   * @return a partition info that does not contain null or empty string.\n   */\n  protected static List<String> normalize(List<String> rawPartitions) {\n    if (rawPartitions == null) {\n      return List.of();\n    }\n    return rawPartitions.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toList());\n  }\n\n  protected final DeltaCatalog catalog;\n  protected String tableId;\n  protected String tableUUID;\n  protected URI tablePath;\n  protected final TableConf conf;\n  /*\n   * This is the TransactionStateRow in json. Needed mainly by {@link #writeParquet}\n   */\n  protected String serializedTableState;\n  protected List<String> partitionColumns;\n\n  private SnapshotCacheManager cacheManager;\n  protected final List<MetricListener> metricListeners;\n  protected final List<TableEventListener> eventListeners;\n\n  // Engine is not serializable, it will be lazily re-created\n  protected transient volatile Engine engine;\n\n  // These fields are not serializable. They will be reinitialized in {@link #open}\n  protected transient StructType schema;\n  protected transient Row tableState;\n  protected transient CredentialManager credentialManager;\n  // Single-thread thread pool for executing interruptible operation.\n  protected transient ExecutorService refreshThreadPool = null;\n  // Thread pool for all kinds of async works\n  protected transient ExecutorService generalThreadPool = null;\n\n  public AbstractKernelTable(\n      DeltaCatalog catalog,\n      String tableId,\n      Map<String, String> conf,\n      StructType schema,\n      List<String> partitionColumns) {\n    this.catalog = catalog;\n    this.tableId = tableId;\n\n    // Allow subclasses to provide extra confs\n    Map<String, String> mergedConfs = new HashMap<>(conf);\n    mergedConfs.putAll(extraConf());\n    this.conf = new TableConf(mergedConfs);\n\n    this.schema = schema;\n    this.partitionColumns = normalize(partitionColumns);\n\n    this.cacheManager = SnapshotCacheManager.getInstance();\n    this.metricListeners = new ArrayList<>();\n    this.eventListeners = new ArrayList<>();\n\n    addEventListener(new MaintenanceListener());\n    addEventListener(new ChecksumListener());\n  }\n\n  public AbstractKernelTable(DeltaCatalog catalog, String tableId, Map<String, String> conf) {\n    this(catalog, tableId, conf, null, null);\n  }\n\n  // =====================\n  // Override methods\n  // =====================\n  @Override\n  public String getId() {\n    return tableId;\n  }\n\n  @Override\n  public StructType getSchema() {\n    return schema;\n  }\n\n  @Override\n  public List<String> getPartitionColumns() {\n    return partitionColumns;\n  }\n\n  @Override\n  public void open() {\n    catalog.open();\n    // init all transient variables\n    if (refreshThreadPool == null) {\n      refreshThreadPool = Executors.newSingleThreadExecutor();\n    }\n    if (generalThreadPool == null) {\n      generalThreadPool = Executors.newFixedThreadPool(Conf.getInstance().getTableThreadPoolSize());\n    }\n    if (credentialManager == null) {\n      credentialManager = createCredentialManager();\n    }\n    if (serializedTableState == null) {\n      withRetry(\n          () -> {\n            loadDeltaTable();\n            return null;\n          });\n    }\n    if (tableState == null) {\n      tableState = JsonUtils.rowFromJson(serializedTableState, TransactionStateRow.SCHEMA);\n    }\n    if (schema == null) {\n      schema = TransactionStateRow.getLogicalSchema(tableState);\n    }\n  }\n\n  @Override\n  public synchronized void close() throws InterruptedException {\n    LOG.info(\"Closing table : {}\", getId());\n    if (refreshThreadPool != null) {\n      withTiming(\n          \"close\",\n          () -> {\n            refreshThreadPool.shutdownNow();\n            // This should return quickly if all tasks are interruptible\n            refreshThreadPool.awaitTermination(10, TimeUnit.MINUTES);\n            refreshThreadPool = null;\n          });\n    }\n  }\n\n  @Override\n  public void refresh() {\n    refresh(null);\n  }\n\n  @Override\n  public Optional<Snapshot> commit(\n      CloseableIterable<Row> actions, String appId, long txnId, Map<String, String> properties) {\n    return withTiming(\n        \"commit\",\n        () ->\n            withRetry(\n                () -> {\n                  Engine localEngine = getEngine();\n                  Optional<Snapshot> snapshotOpt = snapshot();\n                  if (snapshotOpt.isEmpty()) {\n                    throw new IllegalStateException(\"Snapshot should exist\");\n                  }\n                  Snapshot snapshot = snapshotOpt.get();\n                  UpdateTableTransactionBuilder txnBuilder =\n                      snapshot.buildUpdateTableTransaction(ENGINE_INFO, Operation.WRITE);\n                  txnBuilder.withTransactionId(appId, txnId);\n                  txnBuilder.withTablePropertiesAdded(properties);\n                  Transaction txn = txnBuilder.build(engine);\n\n                  TransactionCommitResult result =\n                      withTiming(\"commit.txn\", () -> txn.commit(localEngine, actions));\n                  return result\n                      .getPostCommitSnapshot()\n                      .map(\n                          pcSnapshot -> {\n                            this.refresh(pcSnapshot);\n                            onPostCommit(pcSnapshot);\n                            return pcSnapshot;\n                          });\n                }));\n  }\n\n  @Override\n  public CloseableIterator<Row> writeParquet(\n      String pathSuffix,\n      CloseableIterator<FilteredColumnarBatch> data,\n      Map<String, Literal> partitionValues) {\n    return withRetry(\n        () -> {\n          Engine localEngine = getEngine();\n          Row writeState = getWriteState();\n\n          final CloseableIterator<FilteredColumnarBatch> physicalData =\n              Transaction.transformLogicalData(localEngine, writeState, data, partitionValues);\n\n          final DataWriteContext writeContext =\n              Transaction.getWriteContext(localEngine, writeState, partitionValues);\n          LOG.debug(\"Writing file to path {} with suffix {}\", getTablePath(), pathSuffix);\n          final CloseableIterator<DataFileStatus> dataFiles =\n              localEngine\n                  .getParquetHandler()\n                  .writeParquetFiles(\n                      getTablePath().resolve(pathSuffix).toString(),\n                      physicalData,\n                      writeContext.getStatisticsColumns());\n          return Transaction.generateAppendActions(\n              localEngine, writeState, dataFiles, writeContext);\n        });\n  }\n\n  /**\n   * Load snapshot using a separated thread. This will allow external request to interrupt the\n   * thread during time-consuming operations in loading snapshot, such as log replay.\n   *\n   * @return loaded snapshot, null if the table does not exist\n   */\n  protected Optional<Snapshot> snapshot() {\n    Function<String, Optional<Snapshot>> body =\n        (key) -> {\n          try {\n            return withTiming(\n                \"loadLatestSnapshot\",\n                () -> Optional.of(refreshThreadPool.submit(this::loadLatestSnapshot).get()));\n          } catch (Exception e) {\n            if (ExceptionUtils.isTableNotFound.test(e)) {\n              return Optional.empty();\n            }\n            throw ExceptionUtils.wrap(e);\n          }\n        };\n    String path = tablePath.toString();\n    LOG.debug(\"Loading snapshot for path {}\", path);\n    return cacheManager.get(path, this::versionExists, body);\n  }\n\n  /**\n   * Subclass must implement this method to fetch a Kernel snapshot\n   *\n   * @return latest snapshot of the table\n   */\n  protected abstract Snapshot loadLatestSnapshot();\n\n  /**\n   * Subclass may implement this to achieve fast cache validation. This method is expected to be\n   * faster than {@link #loadLatestSnapshot()}. The default implementation checks if a file with the\n   * given version exists. NOTE: catalog-managed tables need to override this method to check\n   * against catalog.\n   *\n   * @return the latest version of the table, null if unknown / not supported\n   */\n  protected boolean versionExists(Long version) {\n    try {\n      return !DeltaLogActionUtils.getCommitFilesForVersionRange(\n              getEngine(),\n              new io.delta.kernel.internal.fs.Path(tablePath),\n              version,\n              Optional.empty())\n          .isEmpty();\n    } catch (Exception e) {\n      return false;\n    }\n  }\n\n  /** Refresh with the provided snapshot */\n  protected void refresh(Snapshot snapshot) {\n    withTiming(\n        \"refresh\",\n        () ->\n            withRetry(\n                () -> {\n                  Snapshot currentSnapshot = snapshot;\n                  if (currentSnapshot == null) {\n                    currentSnapshot = snapshot().orElse(null);\n                  }\n                  if (currentSnapshot == null) {\n                    return null;\n                  }\n                  this.schema = currentSnapshot.getSchema();\n                  this.partitionColumns = currentSnapshot.getPartitionColumnNames();\n                  // Refresh table state\n                  this.tableState =\n                      currentSnapshot\n                          .buildUpdateTableTransaction(\"dummy\", Operation.WRITE)\n                          .build(getEngine())\n                          .getTransactionState(getEngine());\n                  this.serializedTableState = JsonUtils.rowToJson(this.tableState);\n                  return null;\n                }));\n  }\n\n  protected CreateTableTransactionBuilder buildCreateTableTransaction() {\n    return TableManager.buildCreateTableTransaction(tablePath.toString(), schema, ENGINE_INFO);\n  }\n\n  /** Create a new Delta snapshot representing the empty table at the given location. */\n  protected void createDeltaTable() {\n    Engine engine = getEngine();\n    CreateTableTransactionBuilder txnBuilder =\n        buildCreateTableTransaction().withTableProperties(conf.catalogConf());\n    if (!partitionColumns.isEmpty()) {\n      txnBuilder.withDataLayoutSpec(\n          DataLayoutSpec.partitioned(\n              Optional.of(partitionColumns)\n                  .map(nonEmpty -> nonEmpty.stream().map(Column::new).collect(Collectors.toList()))\n                  .orElseGet(Collections::emptyList)));\n    }\n    try {\n      TransactionCommitResult result =\n          txnBuilder.build(engine).commit(engine, CloseableIterable.emptyIterable());\n      result.getPostCommitSnapshot().ifPresent(this::onPostCommit);\n    } catch (TableAlreadyExistsException ignore) {\n      // Concurrent open may cause this. Ignore it safely.\n    }\n  }\n\n  /**\n   * Load table information from the delta table. This method loads the table if it exists, or\n   * creates a new table entry in catalog if the table does not exist\n   */\n  protected void loadDeltaTable() {\n    DeltaCatalog.TableDescriptor info;\n    try {\n      info = catalog.getTable(tableId);\n      tableUUID = info.uuid;\n      tablePath = normalize(info.tablePath);\n    } catch (ExceptionUtils.ResourceNotFoundException notFound) {\n      catalog.createTable(\n          tableId,\n          schema,\n          partitionColumns,\n          conf.catalogConf(),\n          tableDesc -> {\n            this.tablePath = normalize(tableDesc.tablePath);\n            this.tableUUID = tableDesc.uuid;\n            createDeltaTable();\n          });\n    }\n    final Optional<Snapshot> latestSnapshotOpt = snapshot();\n    if (latestSnapshotOpt.isEmpty()) {\n      throw new IllegalStateException(\"Snapshot not initialized\");\n    }\n    Snapshot latestSnapshot = latestSnapshotOpt.get();\n    // We use a temporary transaction to generate a TransactionStateRow.\n    // It serves as a holder for schema and partition columns.\n    // The transaction will not be committed, and is discarded afterward.\n    Row existingTableState =\n        latestSnapshot\n            .buildUpdateTableTransaction(ENGINE_INFO, Operation.WRITE)\n            .build(getEngine())\n            .getTransactionState(getEngine());\n    this.serializedTableState = JsonUtils.rowToJson(existingTableState);\n    this.schema = latestSnapshot.getSchema();\n    this.partitionColumns = latestSnapshot.getPartitionColumnNames();\n  }\n\n  // Engine will be invalidated when credentials expire\n  public Engine getEngine() {\n    if (engine == null) {\n      synchronized (this) {\n        if (engine == null) {\n          engine = createEngine();\n        }\n      }\n    }\n    return engine;\n  }\n\n  /**\n   * Subclass may implement this method to generate an engine.\n   *\n   * @return engine to access the tables\n   */\n  protected Engine createEngine() {\n    Configuration conf = new Configuration();\n\n    // Built-in configurations for common file system access\n    conf.set(\"fs.file.impl\", \"org.apache.hadoop.fs.LocalFileSystem\");\n    conf.set(\"fs.AbstractFileSystem.file.impl\", \"org.apache.hadoop.fs.local.LocalFs\");\n\n    conf.set(\"fs.s3.impl\", \"org.apache.hadoop.fs.s3a.S3AFileSystem\");\n    conf.set(\"fs.s3a.impl\", \"org.apache.hadoop.fs.s3a.S3AFileSystem\");\n    conf.set(\"fs.s3a.path.style.access\", \"false\");\n    conf.set(\"fs.s3.impl.disable.cache\", \"true\");\n    conf.set(\"fs.s3a.impl.disable.cache\", \"true\");\n\n    conf.set(\"fs.abfs.impl\", \"org.apache.hadoop.fs.azurebfs.AzureBlobFileSystem\");\n    conf.set(\"fs.abfss.impl\", \"org.apache.hadoop.fs.azurebfs.SecureAzureBlobFileSystem\");\n    conf.set(\"fs.AbstractFileSystem.abfs.impl\", \"org.apache.hadoop.fs.azurebfs.Abfs\");\n    conf.set(\"fs.AbstractFileSystem.abfss.impl\", \"org.apache.hadoop.fs.azurebfs.Abfss\");\n\n    conf.set(\"fs.gs.impl\", \"com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem\");\n    conf.set(\"fs.AbstractFileSystem.gs.impl\", \"com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS\");\n\n    this.conf.engineConf().forEach(conf::set);\n    this.credentialManager.getCredentials().forEach(conf::set);\n\n    // Explicitly load external conf files\n    // TODO this is because Flink does not auto load this file in Docker\n    conf.addResource(new Path(\"/opt/flink/conf/core-site.xml\"));\n\n    return DefaultEngine.create(conf);\n  }\n\n  public SnapshotCacheManager getCacheManager() {\n    return cacheManager;\n  }\n\n  public void setCacheManager(SnapshotCacheManager cacheManager) {\n    this.cacheManager = cacheManager;\n  }\n\n  public DeltaCatalog getCatalog() {\n    return catalog;\n  }\n\n  public String getTableUUID() {\n    return tableUUID;\n  }\n\n  public TableConf getConf() {\n    return conf;\n  }\n\n  protected Row getWriteState() {\n    return tableState;\n  }\n\n  /** The table storage location where all data and metadata files should be stored. */\n  public URI getTablePath() {\n    return tablePath;\n  }\n\n  protected Map<String, String> extraConf() {\n    return Map.of();\n  }\n\n  private CredentialManager createCredentialManager() {\n    return new CredentialManager(\n        () -> catalog.getCredentials(this.getTableUUID()), this::refreshCredential);\n  }\n\n  /**\n   * Retry on retryable exceptions. It must be used on all methods that need storage credentials.\n   * {@see ExceptionUtils.isRetryableException}\n   *\n   * @param body the execution body.\n   * @return the return value from body\n   */\n  protected <RET> RET withRetry(CheckedSupplier<RET> body) {\n    RetryPolicy<Object> retryPolicy =\n        RetryPolicy.builder()\n            .handleIf(ExceptionUtils::isRetryableException)\n            .withBackoff(\n                Duration.ofMillis(Conf.getInstance().getSinkRetryDelayMs()),\n                Duration.ofMillis(Conf.getInstance().getSinkRetryMaxDelayMs()),\n                2.0)\n            .withMaxAttempts(Conf.getInstance().getSinkRetryMaxAttempt())\n            .onRetry(\n                e -> {\n                  LOG.warn(\n                      \"Retrying attempt {} on exception {}\",\n                      e.getAttemptCount(),\n                      e.getLastFailure());\n                  if (CredentialManager.isCredentialsExpired.test(e.getLastFailure())) {\n                    refreshCredential();\n                  } else {\n                    reloadSnapshot();\n                  }\n                })\n            .build();\n    Fallback<Object> fallback =\n        Fallback.builder((Object) Optional.empty()).handleIf(ExceptionUtils.isSwallowable).build();\n    return Failsafe.with(retryPolicy, fallback).get(body);\n  }\n\n  public <RET> RET withTiming(String name, Callable<RET> body) {\n    long start = System.nanoTime();\n    try {\n      return body.call();\n    } catch (Throwable t) {\n      throw ExceptionUtils.wrap(t);\n    } finally {\n      long elapse = System.nanoTime() - start;\n      onMetric(name, elapse);\n    }\n  }\n\n  public void withTiming(String name, CheckedRunnable body) {\n    long start = System.nanoTime();\n    try {\n      body.run();\n    } catch (Throwable t) {\n      throw ExceptionUtils.wrap(t);\n    } finally {\n      long elapse = System.nanoTime() - start;\n      onMetric(name, elapse);\n    }\n  }\n\n  public <V> Future<V> executeWithTiming(String name, Callable<V> body) {\n    return generalThreadPool.submit(() -> withTiming(name, body));\n  }\n\n  public Future<?> executeWithTiming(String name, CheckedRunnable body) {\n    return generalThreadPool.submit(() -> withTiming(name, body));\n  }\n\n  // ===================\n  // Table Listeners\n  // ===================\n  public void addMetricListener(MetricListener listener) {\n    this.metricListeners.add(listener);\n  }\n\n  public void removeMetricListener(MetricListener listener) {\n    this.metricListeners.remove(listener);\n  }\n\n  protected void onMetric(String event, long time) {\n    this.metricListeners.forEach(listener -> listener.onEvent(event, time));\n  }\n\n  public void addEventListener(TableEventListener listener) {\n    this.eventListeners.add(listener);\n  }\n\n  public void removeEventListener(TableEventListener listener) {\n    this.eventListeners.remove(listener);\n  }\n\n  public void onPostCommit(Snapshot snapshot) {\n    eventListeners.forEach(\n        listener -> {\n          try {\n            listener.onPostCommit(this, snapshot);\n          } catch (Exception e) {\n            LOG.error(\"Suppressed exception from listener\", e);\n          }\n        });\n  }\n\n  /** Callback invoked when retry need to refresh credentials (credential exception) */\n  protected void refreshCredential() {\n    // Force the recreation of engine (and reload credentials) next time on use.\n    synchronized (this) {\n      this.engine = null;\n    }\n  }\n\n  /** Callback invoked when retry need to reload snapshot (concurrent exception). */\n  protected void reloadSnapshot() {\n    // Client need to clean up snapshot cache if any\n    cacheManager.invalidate(getTablePath().toString());\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/CredentialManager.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.flink.Conf;\nimport java.util.Map;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n/**\n * Manages credentials with proactive refresh semantics.\n *\n * <p>{@code CredentialManager} is responsible for fetching credentials from an external source and\n * refreshing them before expiration. The expiration time is expected to be encoded in the\n * credential map under the key {@link #CREDENTIAL_EXPIRATION_KEY}.\n *\n * <p>The manager caches the most recently fetched credentials and re-fetches them when the\n * expiration time is approaching.\n *\n * <p>This class is intentionally agnostic of the underlying credential source and refresh strategy.\n * Callers provide:\n *\n * <ul>\n *   <li>A {@link Supplier} that fetches the latest credentials\n *   <li>A {@link Runnable} callback that is invoked when a refresh occurs (for example, to notify\n *       dependent components)\n * </ul>\n *\n * Typical usage: <code>\n *     CredentialManager credManager = new CredentialManager(loadCredFromCatalog, callback);\n *     credManager.getCredentials(); // Guaranteed to be an refreshed credential\n *     // Wait for a long time\n *     credManager.getCredentials(); // Internally refreshed, still return valid credentials.\n * </code>\n *\n * <p>This class is thread-safe.\n */\npublic class CredentialManager {\n\n  /**\n   * Key in the credential map that represents the credential expiration time.\n   *\n   * <p>The value is expected to be a string representation of a timestamp (for example, epoch\n   * milliseconds), interpretable by the implementation.\n   */\n  protected static String CREDENTIAL_EXPIRATION_KEY = \"credential.expiration\";\n\n  protected static ScheduledExecutorService refreshExecutors =\n      Executors.newScheduledThreadPool(Conf.getInstance().getCredentialsRefreshThreadPoolSize());\n\n  /**\n   * Determines whether the given exception indicates a credential-related failure.\n   *\n   * <p>This predicate can be used by callers to detect failures that should trigger a credential\n   * refresh or retry logic.\n   *\n   * @return a Predicate that returns {@code true} if the exception is related to invalid or expired\n   *     credentials; {@code false} otherwise\n   */\n  public static Predicate<Throwable> isCredentialsExpired =\n      ExceptionUtils.recursiveCheck(ex -> ex instanceof java.nio.file.AccessDeniedException);\n\n  /** Supplier used to fetch the latest credentials from the underlying source. */\n  private final Supplier<Map<String, String>> credSupplier;\n\n  /**\n   * Callback invoked after credentials are refreshed.\n   *\n   * <p>This can be used to trigger downstream updates or reconfiguration when credentials change.\n   */\n  private final Runnable refreshCallback;\n\n  private AtomicReference<Map<String, String>> cachedCredentials = new AtomicReference<>();\n\n  public CredentialManager(Supplier<Map<String, String>> supplier, Runnable refreshCallback) {\n    this.credSupplier = supplier;\n    this.refreshCallback = refreshCallback;\n  }\n\n  /**\n   * Returns the current credentials, refreshing them if expiration is approaching.\n   *\n   * <p>If no credentials have been fetched yet, this method will fetch and cache them. On\n   * subsequent calls, the cached credentials are returned unless they are near expiration, in which\n   * case a refresh is triggered.\n   *\n   * <p>The refresh strategy (for example, how close to expiration a refresh occurs) is\n   * implementation-defined.\n   *\n   * @return the current valid credentials\n   */\n  Map<String, String> getCredentials() {\n    Map<String, String> cached = cachedCredentials.get();\n    if (cached != null) return cached;\n    Map<String, String> newCredentials = this.credSupplier.get();\n    if (cachedCredentials.compareAndSet(null, newCredentials)) {\n      scheduleNextRefresh(newCredentials);\n      return newCredentials;\n    }\n    return cachedCredentials.get();\n  }\n\n  protected void scheduleNextRefresh(Map<String, String> newCredentials) {\n    long expiration = -1;\n    try {\n      expiration = Long.parseLong(newCredentials.getOrDefault(CREDENTIAL_EXPIRATION_KEY, \"-1\"));\n    } catch (NumberFormatException ignore) {\n    }\n    if (expiration >= 0) {\n      long refreshDelay =\n          Math.max(\n              100, // A minimal wait of 100ms if the refresh delay is too small\n              expiration\n                  - Conf.getInstance().getCredentialsRefreshAheadInMs()\n                  - System.currentTimeMillis());\n      refreshExecutors.schedule(\n          () -> {\n            Map<String, String> existingCredential = cachedCredentials.get();\n            Map<String, String> refreshedCredential = this.credSupplier.get();\n            if (cachedCredentials.compareAndSet(existingCredential, refreshedCredential)) {\n              this.refreshCallback.run();\n              scheduleNextRefresh(refreshedCredential);\n            }\n          },\n          refreshDelay,\n          TimeUnit.MILLISECONDS);\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/DeltaCatalog.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.kernel.types.StructType;\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * A {@code Catalog} abstracts interaction with an external table catalog or metadata service.\n *\n * <p>The catalog is responsible for resolving logical table identifiers into concrete table\n * metadata and for providing the credentials required to access the underlying storage system. This\n * abstraction allows different catalog implementations (e.g., filesystem-based catalogs,\n * metastore-backed catalogs, or REST-based catalogs) to be used interchangeably by higher-level\n * components.\n *\n * <p>Typical responsibilities of a {@code Catalog} include:\n *\n * <ul>\n *   <li>mapping table identifiers to physical table locations,\n *   <li>providing stable table UUIDs for identification and caching, and\n *   <li>supplying credential or configuration information required for table access.\n * </ul>\n */\npublic interface DeltaCatalog extends Serializable {\n\n  /**\n   * Init the catalog instance and make it ready for use. Should be called at least once before the\n   * catalog can be safely used. Calling open on an already opened table has no effect.\n   */\n  default void open() {}\n\n  /**\n   * Loads metadata for a table identified by the given table identifier.\n   *\n   * <p>The identifier format and naming conventions are defined by the specific catalog\n   * implementation. Implementations may interpret the identifier as a logical name, a\n   * fully-qualified path, or another catalog-specific reference.\n   *\n   * @param tableId the logical identifier of the table to load; must not be {@code null}\n   * @return a {@link TableDescriptor} object describing the resolved table\n   * @throws IllegalArgumentException if the identifier is invalid\n   * @throws ExceptionUtils.ResourceNotFoundException if the table cannot be resolved or loaded\n   */\n  TableDescriptor getTable(String tableId);\n\n  /**\n   * Creates a new table in the catalog with the given schema, partitioning, and properties.\n   *\n   * <p>The table is identified by {@code tableId} and is initialized with the provided {@link\n   * StructType} schema. Optional partition columns define how the table data is physically\n   * organized, and table properties supply additional configuration such as format-specific options\n   * or metadata.\n   *\n   * @param tableId The unique identifier of the table to create within the catalog.\n   * @param schema The logical schema of the table, describing column names, data types, and\n   *     nullability.\n   * @param partitions A list of column names used for partitioning the table; an empty list\n   *     indicates an unpartitioned table.\n   * @param properties A map of table properties for configuration and metadata; may be empty but\n   *     must not be {@code null}.\n   * @param callback for the caller to init the table when the storage URI is allocated.\n   * @throws ExceptionUtils.ResourceAlreadyExistException If a table with the same identifier\n   *     already exists in the catalog.\n   */\n  void createTable(\n      String tableId,\n      StructType schema,\n      List<String> partitions,\n      Map<String, String> properties,\n      Consumer<TableDescriptor> callback);\n\n  /**\n   * Returns the credentials or configuration properties required to access the table identified by\n   * the given UUID.\n   *\n   * <p>The returned map may contain authentication information, endpoint configuration, or other\n   * filesystem- or catalog-specific properties. The exact contents and semantics are defined by the\n   * catalog implementation.\n   *\n   * @param uuid the unique identifier of the table\n   * @return a map of credential or configuration properties; may be empty but never {@code null}\n   */\n  Map<String, String> getCredentials(String uuid);\n\n  /**\n   * A container for table metadata resolved by a {@link DeltaCatalog}.\n   *\n   * <p>{@code TableInfo} describes the essential properties needed to locate and access a table,\n   * independent of the underlying catalog implementation.\n   */\n  class TableDescriptor {\n\n    /** The logical identifier used to resolve the table. */\n    String tableId;\n\n    /** A stable UUID that uniquely identifies the table. */\n    String uuid;\n\n    /** The normalized physical location of the table. */\n    URI tablePath;\n\n    public TableDescriptor() {}\n\n    public TableDescriptor(String tableId, String uuid, URI tablePath) {\n      this.tableId = tableId;\n      this.uuid = uuid;\n      this.tablePath = tablePath;\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/DeltaTable.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * A {@code DeltaTable} represents a logical view of a Delta table and provides access to both table\n * metadata (such as schema and partitioning) and operations for reading and writing table data.\n *\n * <p>A {@code DeltaTable} instance abstracts the underlying Delta transaction log and storage\n * layout. Implementations are responsible for:\n *\n * <ul>\n *   <li>exposing immutable table metadata (schema, partition information)\n *   <li>managing transaction boundaries and versioned commits\n *   <li>coordinating reads and writes against the physical table storage\n *   <li>serializing table changes into Delta {@code actions} and committing them atomically\n * </ul>\n *\n * <p>All implementations must be {@link Serializable} to allow use in distributed execution\n * environments.\n */\npublic interface DeltaTable extends Serializable, AutoCloseable {\n\n  /**\n   * Returns a stable identifier that uniquely represents this table within its catalog or storage\n   * system.\n   *\n   * <p>These are some examples that may be used as identifiers, depending on the subclass\n   * implementation and the catalog in use.\n   *\n   * <ul>\n   *   <li>a logical table name (e.g., {@code \"catalog.database.table\"})\n   *   <li>a filesystem or object-store URI\n   * </ul>\n   *\n   * @return a unique logical identifier for the table\n   */\n  String getId();\n\n  /**\n   * Returns the table schema as a {@link StructType}.\n   *\n   * <p>The schema defines the logical column structure of the table. Implementations should\n   * guarantee that the schema corresponds to the latest committed version unless otherwise\n   * documented.\n   *\n   * @return the table schema\n   */\n  StructType getSchema();\n\n  /**\n   * Returns the list of partition columns for this table.\n   *\n   * <p>The returned list defines the physical partitioning strategy used by the table. The ordering\n   * of columns follows the table’s partition specification and should be stable across versions.\n   *\n   * @return an ordered list of partition column names\n   */\n  List<String> getPartitionColumns();\n\n  /**\n   * Init the table instance and make it ready for use. Should be called at least once before the\n   * table can be safely used. Calling open on an already opened table has no effect.\n   */\n  void open();\n\n  /**\n   * Commits a new version to the table by applying the provided Delta actions.\n   *\n   * <p>Actions may include (but are not limited to):\n   *\n   * <ul>\n   *   <li>{@code AddFile} records representing new data files\n   *   <li>{@code RemoveFile} records removing obsolete files\n   *   <li>metadata updates or protocol changes\n   * </ul>\n   *\n   * <p>Implementations must ensure atomicity: either all provided actions are committed as part of\n   * a new table version, or none are. Commit conflicts should be detected and surfaced as\n   * exceptions.\n   *\n   * @param actions an iterable collection of Delta actions to commit; the caller is responsible for\n   *     closing the iterable\n   * @param appId application id used for this commit. See transaction identifier in Delta protocol.\n   * @param txnId the transaction identifier to be used for this commit.\n   * @param properties table properties to be updated with this commit.\n   */\n  Optional<Snapshot> commit(\n      CloseableIterable<Row> actions, String appId, long txnId, Map<String, String> properties);\n\n  /**\n   * Refreshes the table state by reloading the latest snapshot metadata.\n   *\n   * <p>This method updates the in-memory view of the table to reflect the most recently committed\n   * version, including:\n   *\n   * <ul>\n   *   <li>the latest table schema,\n   *   <li>partition column definitions, and\n   *   <li>any other metadata derived from the current Delta log snapshot.\n   * </ul>\n   *\n   * <p>{@code refresh()} should be invoked when external changes to the table may have occurred\n   * (for example, commits from other writers) and the caller requires an up-to-date view before\n   * performing read or write operations.\n   *\n   * <p>Implementations may perform I/O and metadata parsing as part of this operation.\n   */\n  void refresh();\n\n  /**\n   * Writes one or more Parquet files as part of the table and emits the corresponding {@code\n   * AddFile} action describing the newly written data.\n   *\n   * <p>This operation is responsible for:\n   *\n   * <ul>\n   *   <li>writes to the underlying storage layer using the specified {@code data}\n   *   <li>constructing physical file paths by appending {@code pathSuffix} to the table root\n   *   <li>materializing partition values into the file metadata\n   *   <li>returning a Row describing the resulting {@code AddFile} action\n   * </ul>\n   *\n   * <p>The returned iterator typically contains exactly one row (the AddFile action), but\n   * implementations may return multiple actions depending on file-splitting behavior.\n   *\n   * @param pathSuffix a suffix appended to the table path when generating file locations. The\n   *     result path will be `<table_root>/<path_suffix>/<paquet_file>`\n   * @param data an iterator over row batches to be written as Parquet files; this method will close\n   *     it on consumption.\n   * @param partitionValues a mapping of partition column names to their literal values\n   * @return an iterator over {@code Row} objects representing the AddFile actions generated during\n   *     the write\n   * @throws IOException if data writing or file creation fails\n   */\n  CloseableIterator<Row> writeParquet(\n      String pathSuffix,\n      CloseableIterator<FilteredColumnarBatch> data,\n      Map<String, Literal> partitionValues)\n      throws IOException;\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/ExceptionUtils.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport dev.failsafe.function.CheckedConsumer;\nimport io.delta.kernel.exceptions.ConcurrentTransactionException;\nimport io.delta.kernel.exceptions.ConcurrentWriteException;\nimport io.delta.kernel.exceptions.TableAlreadyExistsException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport java.util.ConcurrentModificationException;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\n\n/** Utility methods and common exception types for exception inspection and handling. */\npublic class ExceptionUtils {\n\n  public static Predicate<Throwable> isTableNotFound =\n      ExceptionUtils.recursiveCheck(ex -> ex instanceof TableNotFoundException);\n\n  public static Predicate<Throwable> isSnapshotUpdated =\n      ExceptionUtils.recursiveCheck(\n          ex ->\n              ex instanceof ConcurrentModificationException\n                  || ex instanceof ConcurrentWriteException\n                  || ex instanceof TableAlreadyExistsException);\n\n  public static Predicate<Throwable> isSwallowable =\n      ExceptionUtils.recursiveCheck(ex -> ex instanceof ConcurrentTransactionException);\n\n  /**\n   * Check if an exception is retryable.\n   *\n   * @param e exception\n   * @return true if the exception is Authentication or Concurrency related.\n   */\n  public static boolean isRetryableException(Throwable e) {\n    return CredentialManager.isCredentialsExpired.test(e) || isSnapshotUpdated.test(e);\n  }\n  /**\n   * Creates a predicate that applies the given predicate recursively to an exception and its causal\n   * chain.\n   *\n   * <p>The returned predicate returns {@code true} if the supplied predicate matches the exception\n   * itself or any of its causes.\n   *\n   * @param pred the predicate to apply to each exception in the causal chain\n   * @return a predicate that recursively checks the exception and its causes\n   */\n  public static Predicate<Throwable> recursiveCheck(Predicate<Throwable> pred) {\n    return new Predicate<Throwable>() {\n      @Override\n      public boolean test(Throwable e) {\n        if (e == null) {\n          return false;\n        }\n        if (pred.test(e)) {\n          return true;\n        }\n        return test(e.getCause());\n      }\n    };\n  }\n\n  /** Exception indicating that a requested resource does not exist. */\n  public static class ResourceNotFoundException extends RuntimeException {\n    public ResourceNotFoundException(String message) {\n      super(message);\n    }\n  }\n\n  /** Exception indicating that a resource already exists and cannot be created again. */\n  public static class ResourceAlreadyExistException extends RuntimeException {\n    public ResourceAlreadyExistException(String message) {\n      super(message);\n    }\n  }\n\n  public static RuntimeException wrap(Throwable t) {\n    if (t instanceof RuntimeException) {\n      return (RuntimeException) t;\n    }\n    return new RuntimeException(t);\n  }\n\n  public static <T> Consumer<T> wrap(CheckedConsumer<T> body) {\n    return t -> {\n      try {\n        body.accept(t);\n      } catch (Throwable e) {\n        throw new RuntimeException(e);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/MetricListener.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * A listener interface for receiving performance-related metric events emitted by Delta components.\n *\n * <p>Each event represents a completed operation identified by {@code eventName}, along with its\n * execution time in nanoseconds.\n *\n * <p>Implementations of this interface are expected to be lightweight and non-blocking, as\n * callbacks may be invoked on performance-critical execution paths.\n */\npublic interface MetricListener extends Serializable {\n\n  /**\n   * Called when a performance-related event occurs.\n   *\n   * @param eventName a logical name identifying the metric or operation (e.g., {@code\n   *     \"snapshot.load\"}, {@code \"commit.retry\"})\n   * @param elapseNano the elapsed time of the operation in nanoseconds\n   */\n  void onEvent(String eventName, long elapseNano);\n\n  /**\n   * A {@link MetricListener} implementation that aggregates basic statistics (minimum, maximum, and\n   * average elapsed time) for each metric.\n   *\n   * <p>Statistics are maintained per {@code eventName} and updated incrementally as events are\n   * received.\n   *\n   * <p>This implementation is intended for lightweight in-memory aggregation and diagnostics.\n   * Thread-safety guarantees depend on the concrete implementation and should be documented\n   * accordingly.\n   */\n  class StatsListener implements MetricListener {\n\n    // (metricName, [count, max, min, sum])\n    Map<String, long[]> summary = new HashMap<>();\n\n    @Override\n    public void onEvent(String eventName, long elapseNano) {\n      summary.merge(\n          eventName,\n          new long[] {1, elapseNano, elapseNano, elapseNano},\n          (existing, newvalue) -> {\n            existing[0] += newvalue[0];\n            existing[1] = Math.max(existing[1], newvalue[1]);\n            existing[2] = Math.min(existing[2], newvalue[2]);\n            existing[3] += newvalue[3];\n            return existing;\n          });\n    }\n\n    /**\n     * Get the statistical results of metrics\n     *\n     * @return a map of (metricName, [count, max, min, average])\n     */\n    public Map<String, long[]> report() {\n      return summary.entrySet().stream()\n          .collect(\n              Collectors.toMap(\n                  Map.Entry::getKey,\n                  entry ->\n                      new long[] {\n                        entry.getValue()[0],\n                        entry.getValue()[1],\n                        entry.getValue()[2],\n                        entry.getValue()[3] / entry.getValue()[0]\n                      }));\n    }\n  }\n\n  /** Record the point-wise data points. */\n  class PointListener implements MetricListener {\n\n    private final Map<String, List<Long>> data = new HashMap<>();\n\n    @Override\n    public void onEvent(String eventName, long elapseNano) {\n      data.computeIfAbsent(eventName, (key) -> new ArrayList<>()).add(elapseNano);\n    }\n\n    public Map<String, List<Long>> report() {\n      return data;\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/SnapshotCacheManager.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport io.delta.flink.Conf;\nimport io.delta.kernel.Snapshot;\nimport java.io.Serializable;\nimport java.util.Optional;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n/**\n * A {@code SnapshotCacheManager} provides a pluggable abstraction for caching {@link Snapshot}\n * instances keyed by a string identifier (for example, a table path).\n *\n * <p>The cache is primarily used to avoid repeatedly loading and reconstructing Delta table\n * snapshots from remote storage, which can be expensive for tables with long histories or high\n * access frequency.\n *\n * <p>Implementations may choose different caching strategies, including no-op caching or in-memory\n * eviction-based caching.\n *\n * <p>This interface is {@link Serializable} so that it can be safely used in distributed Flink jobs\n * and reconstructed across task restarts.\n */\npublic interface SnapshotCacheManager extends Serializable {\n\n  /**\n   * Inserts or updates a cached snapshot for the given key.\n   *\n   * @param key the cache key (for example, a table path)\n   * @param snapshot the snapshot to cache\n   */\n  default void put(String key, Snapshot snapshot) {}\n\n  /**\n   * Invalidates the cached snapshot associated with the given key.\n   *\n   * @param key the cache key to invalidate\n   */\n  default void invalidate(String key) {}\n\n  /**\n   * Retrieves a cached snapshot for the given key, loading it on demand if it is not already\n   * present in the cache.\n   *\n   * <p>If the snapshot is not cached, the provided {@code body} is invoked to compute the value,\n   * which may then be stored in the cache depending on the implementation.\n   *\n   * <p>If a version exists, versionProbe is used to quickly verify if the version is up-to-date.\n   *\n   * @param key the cache key\n   * @param versionProbe checks if a version exists. Expected to be faster than body. Used to\n   *     quickly verify if the cached snapshot is updated. It takes a version number and return true\n   *     if the version already exists.\n   * @param body a callable used to compute the snapshot on cache miss\n   * @return the cached or newly loaded snapshot, empty means no snapshot exists ( empty table )\n   */\n  default Optional<Snapshot> get(\n      String key, Predicate<Long> versionProbe, Function<String, Optional<Snapshot>> body) {\n    try {\n      return body.apply(key);\n    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  static SnapshotCacheManager getInstance() {\n    return Conf.getInstance().getTableCacheEnable()\n        ? new LocalCacheManager()\n        : new NoCacheManager();\n  }\n\n  /**\n   * A no-op {@link SnapshotCacheManager} implementation that performs no caching.\n   *\n   * <p>This implementation always bypasses caching logic and is useful in scenarios where caching\n   * is undesirable or needs to be disabled explicitly.\n   */\n  class NoCacheManager implements SnapshotCacheManager {\n    // Intentionally empty\n  }\n\n  /**\n   * The default {@link SnapshotCacheManager} implementation backed by an in-memory, path-based\n   * cache.\n   *\n   * <p>This implementation uses a bounded cache with time-based eviction to store {@link Snapshot}\n   * instances, providing faster snapshot access while preventing unbounded memory growth.\n   *\n   * <p>Cache size and expiration are controlled via {@link Conf}.\n   */\n  class LocalCacheManager implements SnapshotCacheManager {\n\n    /**\n     * A path-based snapshot cache used to speed up snapshot loading.\n     *\n     * <p>The cache is bounded in size and evicts entries based on access time to balance\n     * performance and memory usage.\n     */\n    static final Cache<String, Optional<Snapshot>> SNAPSHOT_CACHE =\n        Caffeine.newBuilder()\n            .maximumSize(Conf.getInstance().getTableCacheSize())\n            .expireAfterAccess(Conf.getInstance().getTableCacheExpireInMs(), TimeUnit.MILLISECONDS)\n            .build();\n\n    @Override\n    public void put(String key, Snapshot snapshot) {\n      SNAPSHOT_CACHE.put(key, Optional.ofNullable(snapshot));\n    }\n\n    @Override\n    public void invalidate(String key) {\n      SNAPSHOT_CACHE.invalidate(key);\n    }\n\n    @Override\n    public Optional<Snapshot> get(\n        String key, Predicate<Long> versionProbe, Function<String, Optional<Snapshot>> body) {\n      Optional<Snapshot> cached = SNAPSHOT_CACHE.get(key, body);\n      // Probe if `version + 1` already exists.\n      long versionToProbe = cached.map(Snapshot::getVersion).orElse(-1L) + 1;\n      // `version + 1` exists. It means current cache at `version` is outdated.\n      if (versionProbe.test(versionToProbe)) {\n        // Cache is outdated and needs reload\n        SNAPSHOT_CACHE.invalidate(key);\n        return SNAPSHOT_CACHE.get(key, body);\n      }\n      return cached;\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/TableConf.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport java.io.Serializable;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Random;\nimport java.util.stream.Collectors;\nimport org.apache.flink.configuration.ConfigOption;\nimport org.apache.flink.configuration.ConfigOptions;\nimport org.apache.flink.configuration.Configuration;\n\n/**\n * Per-table configuration for DeltaSink table maintenance behavior.\n *\n * <p>This class parses a raw string map (typically table options) into typed Flink {@link\n * ConfigOption} values and exposes:\n *\n * <ul>\n *   <li><b>Catalog config</b>: options that should be persisted with the table definition.\n *   <li><b>Engine config</b>: options that should be forwarded to the Delta Kernel engine at\n *       runtime.\n * </ul>\n */\npublic class TableConf implements Serializable {\n\n  private static final long serialVersionUID = 1L;\n\n  /** Probability in [0.0, 1.0] to create a checkpoint on a commit. */\n  public static final ConfigOption<Double> CHECKPOINT_FREQUENCY =\n      ConfigOptions.key(\"checkpoint.frequency\")\n          .doubleType()\n          .defaultValue(0.0)\n          .withDescription(\n              \"Probability in [0.0, 1.0] to create a checkpoint on a commit. \"\n                  + \"0.0 disables checkpoint creation; 1.0 creates a checkpoint on every commit.\");\n\n  /** Whether checksum file creation is enabled for this table. */\n  public static final ConfigOption<Boolean> CHECKSUM_ENABLED =\n      ConfigOptions.key(\"checksum.enable\")\n          .booleanType()\n          .defaultValue(true)\n          .withDescription(\"Whether to generate checksum files for commits on this table.\");\n\n  private static final Map<String, String> DEFAULT_CONFS =\n      Map.of(\"delta.feature.v2Checkpoint\", \"supported\");\n\n  private final Map<String, String> raw;\n  private final Configuration cfg;\n\n  private final Random randgen = new Random(System.currentTimeMillis());\n\n  /**\n   * Creates a {@link TableConf} from a raw key/value map (e.g., table options).\n   *\n   * <p>Unknown keys are preserved in {@link #raw} but ignored by typed accessors unless explicitly\n   * surfaced via {@link #catalogConf()} or {@link #engineConf()}.\n   *\n   * @param conf raw configuration map; must not be null\n   */\n  public TableConf(Map<String, String> conf) {\n    raw = Map.copyOf(Objects.requireNonNull(conf, \"conf\"));\n    cfg = Configuration.fromMap(raw);\n\n    validate();\n  }\n\n  /**\n   * Configuration to be persisted in the catalog.\n   *\n   * <p>This returns a subset of options that are intended to be stored with the table definition.\n   * Now it includes configuration starts with \"delta.\"\n   *\n   * @return a map of catalog-persisted configuration entries\n   */\n  public Map<String, String> catalogConf() {\n    Map<String, String> merged = new HashMap<>(DEFAULT_CONFS);\n    merged.putAll(\n        raw.entrySet().stream()\n            .filter(entry -> entry.getKey().startsWith(\"delta.\"))\n            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));\n    return merged;\n  }\n\n  /**\n   * Configuration to be forwarded to the Kernel engine.\n   *\n   * <p>This returns the subset of configuration entries that are relevant to engine-side behavior.\n   * If your engine uses different option names, translate them here.\n   *\n   * @return a map of engine configuration entries\n   */\n  public Map<String, String> engineConf() {\n    return Map.of();\n  }\n\n  /** @return whether checksum file creation is enabled for this table */\n  public boolean isChecksumEnabled() {\n    return cfg.get(CHECKSUM_ENABLED);\n  }\n\n  /**\n   * Returns the checkpoint creation frequency as a probability.\n   *\n   * @return probability in [0.0, 1.0]\n   */\n  public double getCheckpointFrequency() {\n    return cfg.get(CHECKPOINT_FREQUENCY);\n  }\n\n  /**\n   * Returns whether a checkpoint should be created for the current commit attempt.\n   *\n   * <p>The decision is made by sampling a uniform random number in [0.0, 1.0) and comparing it to\n   * {@link #getCheckpointFrequency()}.\n   *\n   * @return {@code true} if a random number is smaller than the configured frequency\n   */\n  public boolean shouldCreateCheckpoint() {\n    double p = getCheckpointFrequency();\n    if (p <= 0.0) return false;\n    if (p >= 1.0) return true;\n    return randgen.nextDouble() < p;\n  }\n\n  private void validate() {\n    double p = cfg.get(CHECKPOINT_FREQUENCY);\n    if (Double.isNaN(p) || p < 0.0 || p > 1.0) {\n      throw new IllegalArgumentException(\n          \"Invalid checkpoint-frequency: \" + p + \" (expected a probability in [0.0, 1.0])\");\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/TableEventListener.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.kernel.Snapshot;\nimport java.io.Serializable;\n\npublic interface TableEventListener extends Serializable {\n\n  default void onPostCommit(AbstractKernelTable source, Snapshot snapshot) {}\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/postcommit/ChecksumListener.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.table.postcommit;\n\nimport io.delta.flink.table.AbstractKernelTable;\nimport io.delta.flink.table.ExceptionUtils;\nimport io.delta.flink.table.TableEventListener;\nimport io.delta.kernel.Snapshot;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Post-commit listener that generate checksum for the snapshot */\npublic class ChecksumListener implements TableEventListener {\n\n  public static final Logger LOG = LoggerFactory.getLogger(ChecksumListener.class);\n\n  @Override\n  public void onPostCommit(AbstractKernelTable source, Snapshot snapshot) {\n    if (source.getConf().isChecksumEnabled()) {\n      // Write checksum asynchronously\n      source.executeWithTiming(\n          \"postcommit.checksum\",\n          () -> {\n            snapshot\n                .getStatistics()\n                .getChecksumWriteMode()\n                .ifPresent(\n                    ExceptionUtils.wrap(mode -> snapshot.writeChecksum(source.getEngine(), mode)));\n          });\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/java/io/delta/flink/table/postcommit/MaintenanceListener.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table.postcommit;\n\nimport dev.failsafe.function.CheckedRunnable;\nimport io.delta.flink.kernel.CheckpointWriter;\nimport io.delta.flink.table.AbstractKernelTable;\nimport io.delta.flink.table.TableEventListener;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** This post-commit listener will publish the snapshot, and optionally create checkpoints. */\npublic class MaintenanceListener implements TableEventListener {\n\n  @Override\n  public void onPostCommit(AbstractKernelTable source, Snapshot snapshot) {\n    source.executeWithTiming(\"postcommit.maintenance\", new MaintenanceTask(source, snapshot));\n  }\n\n  static class MaintenanceTask implements CheckedRunnable {\n\n    static final Logger LOG = LoggerFactory.getLogger(MaintenanceTask.class);\n    final AbstractKernelTable table;\n    final Snapshot snapshot;\n\n    public MaintenanceTask(AbstractKernelTable table, Snapshot snapshot) {\n      this.table = table;\n      this.snapshot = snapshot;\n    }\n\n    public void run() {\n      Engine engine = table.getEngine();\n      try {\n        // Publish commits\n        Snapshot published =\n            table.withTiming(\"postcommit.maintenance.publish\", () -> snapshot.publish(engine));\n        // Update cache\n        table.getCacheManager().put(table.getTablePath().toString(), published);\n        // Checkpoint can be done only on published snapshots\n        if (table.getConf().shouldCreateCheckpoint()) {\n          if (snapshot instanceof SnapshotImpl\n              && ((SnapshotImpl) snapshot)\n                  .getProtocol()\n                  .getWriterFeatures()\n                  .contains(TableFeatures.CHECKPOINT_V2_RW_FEATURE.featureName())) {\n            // Use v2 incremental checkpoint when possible\n            table.withTiming(\n                \"postcommit.maintenance.checkpoint\",\n                () -> new CheckpointWriter(engine, snapshot).write());\n          } else {\n            table.withTiming(\n                \"postcommit.maintenance.checkpoint\", () -> snapshot.writeCheckpoint(engine));\n          }\n        }\n      } catch (Exception e) {\n        LOG.error(\"Exception while maintenance\", e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/main/resources/delta-flink.properties",
    "content": "#\n#  Copyright (2026) The Delta Lake Project Authors.\n#\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#\n#  http://www.apache.org/licenses/LICENSE-2.0\n#\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n#sink.retry.max_attempt=4\n#sink.retry.delay_ms=200\n#sink.retry.max_delay_ms=20000\n\n#table.cache.enable=true\n#table.cache.size=100\n#table.cache.expire_ms=300000\n\n#credentials.refresh.thread_pool_size=10\n#credentials.refresh.ahead_ms=60000\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/DummyHttp.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\n\nimport com.github.tomakehurst.wiremock.WireMockServer;\nimport com.github.tomakehurst.wiremock.core.WireMockConfiguration;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class DummyHttp {\n\n  private final WireMockServer wireMockServer;\n\n  public DummyHttp(Map<String, String> returns) {\n    this.wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort());\n    this.wireMockServer.start();\n    configureFor(\"localhost\", this.wireMockServer.port());\n\n    returns.forEach(\n        (url, content) -> {\n          this.wireMockServer.stubFor(\n              any(urlPathMatching(url))\n                  .willReturn(\n                      aResponse()\n                          .withStatus(200)\n                          .withHeader(\"Content-Type\", \"application/json\")\n                          .withBody(content)));\n        });\n  }\n\n  public int port() {\n    return this.wireMockServer.port();\n  }\n\n  public static DummyHttp forUC(String tablePath) {\n    Map<String, String> stubs = new HashMap<>();\n    stubs.put(\"/api/2.1/unity-catalog/tables\", \"{}\"); // For write\n    stubs.put(\n        \"/api/2.1/unity-catalog/tables/.*\", // For read\n        String.format(\"{\\\"storage_location\\\": \\\"%s\\\", \\\"table_id\\\": \\\"dummy_id\\\"}\", tablePath));\n    stubs.put(\"/api/2.1/unity-catalog/temporary-table-credentials\", \"{}\");\n    stubs.put(\n        \"/api/2.1/unity-catalog/delta/preview/commits\",\n        \"{\\\"commits\\\": [], \\\"latest_table_version\\\": 1230}\");\n\n    return new DummyHttp(stubs);\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/TestHelper.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink;\n\nimport dev.failsafe.function.CheckedConsumer;\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.ScanImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.SingleAction;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.transaction.DataLayoutSpec;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.DataFileStatus;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.*;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.StreamSupport;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.shaded.org.apache.commons.io.FileUtils;\n\n/**\n * Abstract base class for JUnit 6 (Jupiter) tests providing common test utilities for Delta Lake\n * operations.\n */\npublic abstract class TestHelper {\n\n  protected final Random random = new Random(System.currentTimeMillis());\n\n  protected static <T> Consumer<T> wrap(CheckedConsumer<T> body) {\n    return t -> {\n      try {\n        body.accept(t);\n      } catch (Throwable e) {\n        throw new RuntimeException(e);\n      }\n    };\n  }\n\n  /**\n   * Executes the given function with a temporary directory. The directory is automatically deleted\n   * after the function completes.\n   *\n   * @param f Consumer function that receives the temporary directory\n   */\n  protected void withTempDir(CheckedConsumer<File> f) {\n    File tempDir = null;\n    try {\n      tempDir = Files.createTempDirectory(UUID.randomUUID().toString()).toFile();\n      f.accept(tempDir);\n    } catch (Throwable e) {\n      throw new RuntimeException(e);\n    } finally {\n      if (tempDir != null) {\n        FileUtils.deleteQuietly(tempDir);\n      }\n    }\n  }\n\n  /**\n   * Creates a dummy row with a random ID.\n   *\n   * @return A Row containing a random integer ID\n   */\n  protected Row dummyRow() {\n    int id = random.nextInt(1048576);\n    Map<Integer, Object> map = new HashMap<>();\n    map.put(0, id);\n    StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n    return new GenericRow(schema, map);\n  }\n\n  /**\n   * Creates dummy file statistics with the specified number of records.\n   *\n   * @param numRecords Number of records in the file\n   * @return DataFileStatistics with the given record count\n   */\n  protected DataFileStatistics dummyStatistics(long numRecords) {\n    return new DataFileStatistics(\n        numRecords,\n        Collections.emptyMap(),\n        Collections.emptyMap(),\n        Collections.emptyMap(),\n        Optional.empty());\n  }\n\n  /**\n   * Creates a dummy AddFile row for testing.\n   *\n   * @param schema Schema of the table\n   * @param numRows Number of rows in the file\n   * @param partitionValues Partition column values\n   * @return Row representing an AddFile action\n   */\n  protected Row dummyAddFileRow(\n      StructType schema, long numRows, Map<String, Literal> partitionValues) {\n    AddFile addFile =\n        AddFile.convertDataFileStatus(\n            schema,\n            URI.create(\"s3://abc/def\"),\n            new DataFileStatus(\n                \"s3://abc/def/\" + UUID.randomUUID().toString(),\n                1000L,\n                2000L,\n                Optional.of(dummyStatistics(numRows))),\n            partitionValues,\n            /* dataChange= */ true,\n            /* tags= */ Collections.emptyMap(),\n            /* baseRowId= */ Optional.empty(),\n            /* defaultRowCommitVersion= */ Optional.empty(),\n            /* deletionVectorDescriptor= */ Optional.empty());\n    return SingleAction.createAddFileSingleAction(addFile.toRow());\n  }\n\n  /** Create Multiple add files rows */\n  protected List<Row> dummyAddFileRows(\n      StructType schema, int count, Function<Integer, Map<String, Literal>> partgen) {\n    return IntStream.range(0, count)\n        .mapToObj(i -> dummyAddFileRow(schema, 10 + i, partgen.apply(i)))\n        .collect(Collectors.toList());\n  }\n\n  /**\n   * Creates a dummy writer context for testing.\n   *\n   * @param engine Delta Engine instance\n   * @param tablePath Path to the table\n   * @param schema Table schema\n   * @param partitionCols Partition column names\n   * @return Transaction state row\n   */\n  protected Row dummyWriterContext(\n      Engine engine, String tablePath, StructType schema, List<String> partitionCols) {\n    var txnBuilder = TableManager.buildCreateTableTransaction(tablePath, schema, \"dummy\");\n    if (!partitionCols.isEmpty()) {\n      List<Column> partitionColumns =\n          partitionCols.stream().map(Column::new).collect(Collectors.toList());\n      txnBuilder.withDataLayoutSpec(DataLayoutSpec.partitioned(partitionColumns));\n    }\n    var txn = txnBuilder.build(engine);\n    return txn.getTransactionState(engine);\n  }\n\n  /** Overload with empty partition columns. */\n  protected Row dummyWriterContext(Engine engine, String tablePath, StructType schema) {\n    return dummyWriterContext(engine, tablePath, schema, Collections.emptyList());\n  }\n\n  /**\n   * Creates a non-empty table with dummy data.\n   *\n   * @param engine Delta Engine instance\n   * @param tablePath Path to the table\n   * @param schema Table schema\n   * @param partitionCols Partition column names\n   * @param numRows Number of rows to add\n   * @param properties Table properties\n   * @return Optional snapshot after creation\n   */\n  protected Optional<Snapshot> createNonEmptyTable(\n      Engine engine,\n      String tablePath,\n      StructType schema,\n      List<String> partitionCols,\n      long numRows,\n      Map<String, String> properties) {\n    var txnBuilder =\n        TableManager.buildCreateTableTransaction(tablePath, schema, \"dummy\")\n            .withTableProperties(properties);\n    if (!partitionCols.isEmpty()) {\n      List<Column> partitionColumns =\n          partitionCols.stream().map(Column::new).collect(Collectors.toList());\n      txnBuilder.withDataLayoutSpec(DataLayoutSpec.partitioned(partitionColumns));\n    }\n    var txn = txnBuilder.build(engine);\n\n    Map<String, Literal> partitionMap = new HashMap<>();\n    for (String colName : partitionCols) {\n      partitionMap.put(colName, dummyRandomLiteral(schema.get(colName).getDataType()));\n    }\n\n    // Prepare some dummy AddFile\n    AddFile dummyAddFile =\n        AddFile.convertDataFileStatus(\n            schema,\n            URI.create(tablePath),\n            new DataFileStatus(\n                UUID.randomUUID().toString(), 1000L, 2000L, Optional.of(dummyStatistics(numRows))),\n            partitionMap,\n            true,\n            Collections.emptyMap(),\n            Optional.empty(),\n            Optional.empty(),\n            Optional.empty());\n\n    return txn.commit(\n            engine,\n            CloseableIterable.inMemoryIterable(\n                Utils.singletonCloseableIterator(\n                    SingleAction.createAddFileSingleAction(dummyAddFile.toRow()))))\n        .getPostCommitSnapshot();\n  }\n\n  /** Overloads with default values. */\n  protected Optional<Snapshot> createNonEmptyTable(\n      Engine engine, String tablePath, StructType schema) {\n    return createNonEmptyTable(\n        engine, tablePath, schema, Collections.emptyList(), 0L, Collections.emptyMap());\n  }\n\n  protected Optional<Snapshot> createNonEmptyTable(\n      Engine engine, String tablePath, StructType schema, List<String> partitionCols) {\n    return createNonEmptyTable(\n        engine, tablePath, schema, partitionCols, 0L, Collections.emptyMap());\n  }\n\n  protected Optional<Snapshot> createNonEmptyTable(\n      Engine engine,\n      String tablePath,\n      StructType schema,\n      List<String> partitionCols,\n      long numRows) {\n    return createNonEmptyTable(\n        engine, tablePath, schema, partitionCols, numRows, Collections.emptyMap());\n  }\n\n  protected Optional<Snapshot> createNonEmptyTable(\n      Engine engine,\n      String tablePath,\n      StructType schema,\n      List<String> partitionCols,\n      Map<String, String> properties) {\n    return createNonEmptyTable(engine, tablePath, schema, partitionCols, 0L, properties);\n  }\n\n  /**\n   * Makes a random write to an existing table.\n   *\n   * @param engine Delta Engine instance\n   * @param tablePath Path to the table\n   * @param schema Table schema\n   * @param partitionCols Partition column names\n   * @return Optional snapshot after write\n   */\n  protected Optional<Snapshot> writeTable(\n      Engine engine, String tablePath, StructType schema, List<String> partitionCols) {\n    Map<String, Literal> partitionMap = new HashMap<>();\n    for (String colName : partitionCols) {\n      partitionMap.put(colName, dummyRandomLiteral(schema.get(colName).getDataType()));\n    }\n\n    // Prepare some dummy AddFile\n    AddFile dummyAddFile =\n        AddFile.convertDataFileStatus(\n            schema,\n            URI.create(tablePath),\n            new DataFileStatus(UUID.randomUUID().toString(), 1000L, 2000L, Optional.empty()),\n            partitionMap,\n            true,\n            Collections.emptyMap(),\n            Optional.empty(),\n            Optional.empty(),\n            Optional.empty());\n\n    var txn =\n        TableManager.loadSnapshot(tablePath)\n            .build(engine)\n            .buildUpdateTableTransaction(\"dummy\", Operation.WRITE)\n            .build(engine);\n\n    return txn.commit(\n            engine,\n            CloseableIterable.inMemoryIterable(\n                Utils.singletonCloseableIterator(\n                    SingleAction.createAddFileSingleAction(dummyAddFile.toRow()))))\n        .getPostCommitSnapshot();\n  }\n\n  /** Overload with empty partition columns. */\n  protected Optional<Snapshot> writeTable(Engine engine, String tablePath, StructType schema) {\n    return writeTable(engine, tablePath, schema, Collections.emptyList());\n  }\n\n  /**\n   * Verifies table content using a custom checker function.\n   *\n   * @param tablePath Path to the table\n   * @param checker Consumer that receives version, AddFiles, and properties\n   */\n  protected void verifyTableContent(String tablePath, TableContentChecker checker) {\n    Engine engine = DefaultEngine.create(new Configuration());\n    Snapshot snapshot = TableManager.loadSnapshot(tablePath).build(engine);\n    var filesList =\n        ((ScanImpl) snapshot.getScanBuilder().build()).getScanFiles(engine, true).toInMemoryList();\n\n    List<AddFile> actions =\n        StreamSupport.stream(filesList.spliterator(), false)\n            .flatMap(\n                scanFile ->\n                    StreamSupport.stream(scanFile.getRows().toInMemoryList().spliterator(), false))\n            .map(row -> new AddFile(row.getStruct(0)))\n            .collect(Collectors.toList());\n\n    Map<String, String> properties = snapshot.getTableProperties();\n    checker.check(snapshot.getVersion(), actions, properties);\n  }\n\n  /** Functional interface for table content verification. */\n  @FunctionalInterface\n  protected interface TableContentChecker {\n    void check(long version, Iterable<AddFile> addFiles, Map<String, String> properties);\n  }\n\n  /**\n   * Reads a Parquet file and returns its rows.\n   *\n   * @param filePath Path to the Parquet file\n   * @param schema Expected schema\n   * @return List of rows from the file\n   */\n  protected List<Row> readParquet(Path filePath, StructType schema) {\n    try {\n      FileStatus fileStatus =\n          FileStatus.of(\n              filePath.toString(),\n              Files.size(filePath),\n              Files.getLastModifiedTime(filePath).toMillis());\n\n      var results =\n          DefaultEngine.create(new Configuration())\n              .getParquetHandler()\n              .readParquetFiles(\n                  Utils.singletonCloseableIterator(fileStatus), schema, Optional.empty());\n\n      return StreamSupport.stream(results.toInMemoryList().spliterator(), false)\n          .flatMap(\n              result ->\n                  StreamSupport.stream(\n                      result.getData().getRows().toInMemoryList().spliterator(), false))\n          .collect(Collectors.toList());\n    } catch (IOException e) {\n      throw new UncheckedIOException(\"Failed to read Parquet file: \" + filePath, e);\n    }\n  }\n\n  /**\n   * Creates a random literal value for the given data type.\n   *\n   * @param dataType Data type for the literal\n   * @return Random literal of the specified type\n   */\n  protected Literal dummyRandomLiteral(DataType dataType) {\n    if (dataType.equals(IntegerType.INTEGER)) {\n      return Literal.ofInt(random.nextInt());\n    } else if (dataType.equals(StringType.STRING)) {\n      return Literal.ofString(\"p\" + random.nextInt());\n    } else if (dataType.equals(LongType.LONG)) {\n      return Literal.ofLong(random.nextLong());\n    } else {\n      throw new UnsupportedOperationException(\n          \"Unsupported data type for random literal: \" + dataType);\n    }\n  }\n\n  /**\n   * Checks if an object is serializable by attempting to serialize and deserialize it.\n   *\n   * @param input Object to check for serializability\n   * @throws AssertionError if the object cannot be serialized or deserialized correctly\n   */\n  protected void checkSerializability(Object input) {\n    try {\n      ByteArrayOutputStream baos = new ByteArrayOutputStream();\n      ObjectOutputStream oos = new ObjectOutputStream(baos);\n      oos.writeObject(input);\n      oos.close();\n\n      byte[] bytes = baos.toByteArray();\n      ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));\n      Object restored = ois.readObject();\n      ois.close();\n\n      if (!restored.getClass().equals(input.getClass())) {\n        throw new AssertionError(\n            \"Restored object class \"\n                + restored.getClass()\n                + \" does not match input class \"\n                + input.getClass());\n      }\n    } catch (IOException | ClassNotFoundException e) {\n      throw new AssertionError(\"Object is not serializable\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/kernel/CheckpointWriterTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.kernel;\n\nimport static io.delta.flink.kernel.CheckpointWriter.TAG_SIDECAR_COUNT;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.delta.flink.TestHelper;\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.SingleAction;\nimport io.delta.kernel.internal.checkpoints.CheckpointMetaData;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.DataFileStatus;\nimport io.delta.kernel.utils.FileStatus;\nimport java.net.URI;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.apache.hadoop.conf.Configuration;\nimport org.junit.jupiter.api.Test;\n\n/** JUnit test suite for checkpoint creation and validation. */\nclass CheckpointWriterTest extends TestHelper {\n\n  /**\n   * Helper method to create a table and write commits with a callback on each commit.\n   *\n   * @param engine Delta engine\n   * @param tablePath path to the table\n   * @param schema table schema\n   * @param partitionCols partition columns\n   * @param numCommits number of commits to write\n   * @param callback callback invoked after each commit with the commit index (0-based) and snapshot\n   */\n  private void createTableAndWriteCommits(\n      Engine engine,\n      String tablePath,\n      StructType schema,\n      List<String> partitionCols,\n      int numCommits,\n      CommitCallback callback) {\n    Map<String, String> properties = Map.of(\"delta.feature.v2Checkpoint\", \"supported\");\n\n    createNonEmptyTable(engine, tablePath, schema, partitionCols, properties);\n\n    for (int i = 0; i < numCommits; i++) {\n      Optional<Snapshot> snapshot = writeTable(engine, tablePath, schema, partitionCols);\n      if (snapshot.isPresent() && callback != null) {\n        callback.onCommit(i, snapshot.get());\n      }\n    }\n  }\n\n  @FunctionalInterface\n  interface CommitCallback {\n    void onCommit(int commitIndex, Snapshot snapshot);\n  }\n\n  private Optional<Snapshot> writeRemoveFile(\n      Engine engine, String tablePath, StructType schema, List<String> partitionCols) {\n    Map<String, Literal> partitionMap = new HashMap<>();\n    for (String colName : partitionCols) {\n      partitionMap.put(colName, dummyRandomLiteral(schema.get(colName).getDataType()));\n    }\n\n    // Prepare some dummy AddFile\n    AddFile dummyAddFile =\n        AddFile.convertDataFileStatus(\n            schema,\n            URI.create(tablePath),\n            new DataFileStatus(UUID.randomUUID().toString(), 1000L, 2000L, Optional.empty()),\n            partitionMap,\n            true,\n            Collections.emptyMap(),\n            Optional.empty(),\n            Optional.empty(),\n            Optional.empty());\n\n    var txn =\n        TableManager.loadSnapshot(tablePath)\n            .build(engine)\n            .buildUpdateTableTransaction(\"dummy\", Operation.WRITE)\n            .build(engine);\n\n    txn.commit(\n            engine,\n            CloseableIterable.inMemoryIterable(\n                Utils.singletonCloseableIterator(\n                    SingleAction.createAddFileSingleAction(dummyAddFile.toRow()))))\n        .getPostCommitSnapshot();\n\n    txn =\n        TableManager.loadSnapshot(tablePath)\n            .build(engine)\n            .buildUpdateTableTransaction(\"dummy\", Operation.WRITE)\n            .build(engine);\n\n    return txn.commit(\n            engine,\n            CloseableIterable.inMemoryIterable(\n                Utils.singletonCloseableIterator(\n                    SingleAction.createRemoveFileSingleAction(\n                        dummyAddFile.toRemoveFileRow(true, Optional.empty())))))\n        .getPostCommitSnapshot();\n  }\n\n  private Optional<Snapshot> writeDomainMetadata(\n      Engine engine, String tablePath, String domain, String conf) {\n\n    // Prepare some dummy AddFile\n    DomainMetadata dm = new DomainMetadata(domain, conf, false);\n\n    var txn =\n        TableManager.loadSnapshot(tablePath)\n            .build(engine)\n            .buildUpdateTableTransaction(\"dummy\", Operation.WRITE)\n            .build(engine);\n\n    return txn.commit(\n            engine,\n            CloseableIterable.inMemoryIterable(\n                Utils.singletonCloseableIterator(\n                    SingleAction.createDomainMetadataSingleAction(dm.toRow()))))\n        .getPostCommitSnapshot();\n  }\n\n  private void assertSnapshotRead(\n      Engine engine, String tablePath, long version, int numSidecars, int numActions) {\n    SnapshotImpl latest = (SnapshotImpl) TableManager.loadSnapshot(tablePath).build(engine);\n    assertSnapshotRead(engine, latest, version, numSidecars, numActions, Map.of());\n  }\n\n  private void assertSnapshotRead(\n      Engine engine, SnapshotImpl snapshot, long version, int numSidecars, int numActions) {\n    assertSnapshotRead(engine, snapshot, version, numSidecars, numActions, Map.of());\n  }\n\n  private void assertSnapshotRead(\n      Engine engine,\n      SnapshotImpl snapshot,\n      long version,\n      int numSidecars,\n      int numActions,\n      Map<String, String> dms) {\n    if (version >= 0) {\n      assertEquals(version, snapshot.getSnapshotReport().getCheckpointVersion().orElse(-1L));\n    }\n\n    var files = snapshot.getScanBuilder().build().getScanFiles(engine).toInMemoryList();\n    if (numSidecars >= 0) {\n      long sidecarCount =\n          files.stream()\n              .map(FilteredColumnarBatch::getFilePath)\n              .filter(path -> path.orElse(\"\").contains(\"_sidecar\"))\n              .count();\n      assertEquals(numSidecars, sidecarCount);\n    }\n\n    if (numActions >= 0) {\n      List<AddFile> actions =\n          files.stream()\n              .flatMap(scanFile -> scanFile.getRows().toInMemoryList().stream())\n              .map(row -> new AddFile(row.getStruct(0)))\n              .collect(Collectors.toList());\n\n      assertEquals(numActions, actions.size());\n    }\n\n    for (Map.Entry<String, String> e : dms.entrySet()) {\n      assertTrue(snapshot.getDomainMetadata(e.getKey()).isPresent());\n      assertEquals(e.getValue(), snapshot.getDomainMetadata(e.getKey()).get());\n    }\n  }\n\n  /**\n   * Reads and verifies the _last_checkpoint file.\n   *\n   * @param engine Delta engine\n   * @param snapshot snapshot to get table path from\n   * @param expectedVersion expected checkpoint version (-1 to skip verification)\n   * @param expectedNumSidecars expected number of sidecars (-1 to skip verification)\n   */\n  private void assertLastCheckpointFile(\n      Engine engine, Snapshot snapshot, int expectedVersion, int expectedNumSidecars) {\n    String tablePath = snapshot.getPath();\n    String lastCheckpointPath = tablePath + \"/_delta_log/_last_checkpoint\";\n\n    try {\n      var content =\n          engine\n              .getJsonHandler()\n              .readJsonFiles(\n                  singletonCloseableIterator(FileStatus.of(lastCheckpointPath)),\n                  CheckpointMetaData.READ_SCHEMA,\n                  Optional.empty())\n              .flatMap(ColumnarBatch::getRows)\n              .toInMemoryList()\n              .get(0);\n\n      CheckpointMetaData metadata = CheckpointMetaData.fromRow(content);\n\n      // Verify version\n      if (expectedVersion >= 0) {\n        assertEquals(expectedVersion, metadata.version, \"Checkpoint version mismatch\");\n      }\n\n      // Verify tags contain TAG_FLINK_DELTASINK_CHECKPOINT\n      assertTrue(\n          Boolean.parseBoolean(\n              metadata.tags.getOrDefault(CheckpointWriter.TAG_DELTASINK_CHECKPOINT, \"false\")),\n          \"Expected TAG_DELTASINK_CHECKPOINT to be true in _last_checkpoint\");\n\n      // Verify number of sidecars if parts are present\n      if (expectedNumSidecars >= 0 && metadata.parts.isPresent()) {\n        assertEquals(\n            expectedNumSidecars,\n            Integer.parseInt(metadata.tags.getOrDefault(TAG_SIDECAR_COUNT, \"0\")),\n            \"Number of sidecars mismatch in _last_checkpoint\");\n      }\n    } catch (Exception e) {\n      throw new AssertionError(\"Failed to read or verify _last_checkpoint\", e);\n    }\n  }\n\n  @Test\n  void testCreateIncrementalCheckpoint() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n          createTableAndWriteCommits(\n              engine,\n              tablePath,\n              schema,\n              Collections.emptyList(),\n              25,\n              (i, snapshot) -> {\n                if (i % 7 == 6) {\n                  TestHelper.<Snapshot>wrap(s -> new CheckpointWriter(engine, s).write())\n                      .accept(snapshot);\n                  assertLastCheckpointFile(\n                      engine, snapshot, /* version */ i + 1, /* numSidecar */ (i + 1) / 7);\n                }\n              });\n\n          assertSnapshotRead(engine, tablePath, 21, 3, 26);\n        });\n  }\n\n  @Test\n  void testIgnoreNotMyCheckpoints() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n          createTableAndWriteCommits(\n              engine,\n              tablePath,\n              schema,\n              Collections.emptyList(),\n              25,\n              (i, snapshot) -> {\n                if (i % 7 == 6) {\n                  TestHelper.<Snapshot>wrap(s -> s.writeCheckpoint(engine)).accept(snapshot);\n                }\n                if (i == 24) {\n                  TestHelper.<Snapshot>wrap(s -> new CheckpointWriter(engine, s).write())\n                      .accept(snapshot);\n                }\n              });\n\n          // 1 checkpoint, 1 sidecars\n          assertSnapshotRead(engine, tablePath, 25, 1, 26);\n        });\n  }\n\n  @Test\n  void testCreateOnOlderSnapshots() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n          createTableAndWriteCommits(engine, tablePath, schema, Collections.emptyList(), 25, null);\n\n          SnapshotImpl snapshot = (SnapshotImpl) TableManager.loadSnapshot(tablePath).build(engine);\n          snapshot.writeCheckpoint(engine);\n\n          SnapshotImpl oldSnapshot =\n              (SnapshotImpl) TableManager.loadSnapshot(tablePath).atVersion(20).build(engine);\n          new CheckpointWriter(engine, oldSnapshot).write();\n\n          SnapshotImpl oldSnapshotAgain =\n              (SnapshotImpl) TableManager.loadSnapshot(tablePath).atVersion(20).build(engine);\n\n          assertEquals(20, oldSnapshotAgain.getSnapshotReport().getCheckpointVersion().orElse(-1L));\n\n          assertSnapshotRead(engine, oldSnapshotAgain, -1, 1, 21);\n\n          // Make sure _last_checkpoint is not changed\n          String fileName = tablePath + \"/_delta_log/_last_checkpoint\";\n          var content =\n              engine\n                  .getJsonHandler()\n                  .readJsonFiles(\n                      singletonCloseableIterator(FileStatus.of(fileName)),\n                      CheckpointMetaData.READ_SCHEMA,\n                      Optional.empty())\n                  .toInMemoryList();\n\n          List<CheckpointMetaData> checkpoints =\n              content.stream()\n                  .flatMap(columnarBatch -> columnarBatch.getRows().toInMemoryList().stream())\n                  .map(CheckpointMetaData::fromRow)\n                  .collect(Collectors.toList());\n\n          assertEquals(25, checkpoints.get(0).version);\n        });\n  }\n\n  @Test\n  void testFallbackOnRemoveFiles() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n          createTableAndWriteCommits(\n              engine,\n              tablePath,\n              schema,\n              Collections.emptyList(),\n              25,\n              (i, snapshot) -> {\n                if (i % 7 == 6) {\n                  TestHelper.<Snapshot>wrap(s -> new CheckpointWriter(engine, s).write())\n                      .accept(snapshot);\n                }\n              });\n\n          var s = writeRemoveFile(engine, tablePath, schema, Collections.emptyList());\n          s.ifPresent(wrap(sn -> new CheckpointWriter(engine, sn).write()));\n\n          assertSnapshotRead(engine, tablePath, 27, 1, 26);\n        });\n  }\n\n  @Test\n  void testMergeManySidecars() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n          // Create multiple checkpoints to accumulate 5 sidecars\n          createTableAndWriteCommits(\n              engine,\n              tablePath,\n              schema,\n              Collections.emptyList(),\n              40,\n              (i, snapshot) -> {\n                if (i % 7 == 6) {\n                  TestHelper.<Snapshot>wrap(s -> new CheckpointWriter(engine, s).write())\n                      .accept(snapshot);\n                }\n              });\n\n          // Verify we have accumulated the expected sidecars\n          assertSnapshotRead(engine, tablePath, -1L, 5, -1);\n\n          // Write one more checkpoint - this should merge the 5 old sidecars\n          SnapshotImpl finalSnapshot =\n              (SnapshotImpl) TableManager.loadSnapshot(tablePath).build(engine);\n          new CheckpointWriter(engine, finalSnapshot, 5).write();\n\n          // Load the snapshot again to get the latest checkpoint\n          SnapshotImpl snapshotAfter =\n              (SnapshotImpl) TableManager.loadSnapshot(tablePath).build(engine);\n\n          assertSnapshotRead(engine, snapshotAfter, -1, 2, 41);\n        });\n  }\n\n  @Test\n  public void testMergeDomainMetadata() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n          Map<String, String> properties =\n              Map.of(\n                  \"delta.feature.v2Checkpoint\", \"supported\",\n                  \"delta.feature.domainMetadata\", \"supported\");\n          Optional<Snapshot> snapshot =\n              createNonEmptyTable(engine, tablePath, schema, List.of(), properties);\n\n          writeDomainMetadata(engine, tablePath, \"domain1\", \"conf1\");\n          writeDomainMetadata(engine, tablePath, \"domain2\", \"conf2\");\n          snapshot = writeDomainMetadata(engine, tablePath, \"domain1\", \"conf2\");\n\n          new CheckpointWriter(engine, snapshot.get()).write();\n          // Write a new commit then read the table\n          writeTable(engine, tablePath, schema).get();\n          SnapshotImpl snapshotAfter =\n              (SnapshotImpl) TableManager.loadSnapshot(tablePath).build(engine);\n          assertSnapshotRead(\n              engine, snapshotAfter, 3, 1, 2, Map.of(\"domain1\", \"conf2\", \"domain2\", \"conf2\"));\n        });\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/table/AbstractKernelTableTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport dev.failsafe.function.CheckedConsumer;\nimport io.delta.flink.TestHelper;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.SingleAction;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.File;\nimport java.net.URI;\nimport java.nio.file.AccessDeniedException;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.apache.flink.util.InstantiationUtil;\nimport org.apache.hadoop.conf.Configuration;\nimport org.junit.jupiter.api.Test;\n\n/** JUnit 6 test suite for AbstractKernelTable. */\nclass AbstractKernelTableTest extends TestHelper {\n\n  /**\n   * Helper method to create a test table with default configuration.\n   *\n   * @param schema table schema\n   * @param partitionCols partition columns\n   * @param callback callback invoked with the created table\n   */\n  private void withTestTable(\n      StructType schema,\n      List<String> partitionCols,\n      CheckedConsumer<LocalFileSystemTable> callback) {\n    withTestTable(schema, partitionCols, Collections.emptyMap(), callback);\n  }\n\n  /**\n   * Helper method to create a test table with custom configuration.\n   *\n   * @param schema table schema\n   * @param partitionCols partition columns\n   * @param tableConfig table configuration\n   * @param callback callback invoked with the created table\n   */\n  private void withTestTable(\n      StructType schema,\n      List<String> partitionCols,\n      Map<String, String> tableConfig,\n      CheckedConsumer<LocalFileSystemTable> callback) {\n    withTempDir(\n        dir -> {\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(dir.toURI(), tableConfig, schema, partitionCols);\n          table.open();\n\n          callback.accept(table);\n        });\n  }\n\n  @Test\n  void testNormalizeURI() {\n    assertEquals(\n        \"file:///var/char/good/\",\n        AbstractKernelTable.normalize(URI.create(\"file:/var/char/good\")).toString());\n    assertEquals(\n        \"file:///var/char/good/\",\n        AbstractKernelTable.normalize(URI.create(\"/var/char/good\")).toString());\n    assertEquals(\n        \"file:///var/char/good/\",\n        AbstractKernelTable.normalize(URI.create(\"file:///var/char/good\")).toString());\n    assertEquals(\n        \"s3://host/var/\", AbstractKernelTable.normalize(URI.create(\"s3://host/var\")).toString());\n  }\n\n  @Test\n  void testTableIsSerializable() throws Exception {\n    StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n    withTestTable(\n        schema,\n        Collections.emptyList(),\n        table -> {\n          byte[] serialized = InstantiationUtil.serializeObject(table);\n          AbstractKernelTable copy =\n              InstantiationUtil.deserializeObject(serialized, getClass().getClassLoader());\n          assertNotNull(copy);\n          assertNull(copy.getSchema());\n          assertNull(copy.tableState);\n          assertEquals(copy.getId(), table.getId());\n          assertEquals(copy.getTablePath(), table.getTablePath());\n          assertEquals(copy.getTableUUID(), table.getTableUUID());\n          assertEquals(copy.getPartitionColumns(), table.getPartitionColumns());\n        });\n  }\n\n  @Test\n  void testTableStoredConfIntoDeltaLogs() {\n    StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n\n    Map<String, String> tableConfig = new HashMap<>();\n    tableConfig.put(\"delta.enableDeletionVectors\", \"true\");\n    tableConfig.put(\"showme\", \"themoney\");\n    tableConfig.put(\"something\", \"fornothing\");\n\n    withTestTable(\n        schema,\n        Collections.emptyList(),\n        tableConfig,\n        table -> {\n          table.commit(\n              CloseableIterable.inMemoryIterable(\n                  singletonCloseableIterator(dummyAddFileRow(schema, 1, Collections.emptyMap()))),\n              \"app\",\n              100L,\n              Collections.emptyMap());\n\n          Snapshot snapshot = table.snapshot().get();\n          assertEquals(\"true\", snapshot.getTableProperties().get(\"delta.enableDeletionVectors\"));\n          assertFalse(snapshot.getTableProperties().containsKey(\"showme\"));\n          assertFalse(snapshot.getTableProperties().containsKey(\"something\"));\n          assertTrue(\n              ((SnapshotImpl) snapshot).getProtocol().getWriterFeatures().contains(\"v2Checkpoint\"));\n        });\n  }\n\n  @Test\n  void testCreateTableAndCommitWithoutPartition() {\n    StructType schema =\n        new StructType().add(\"id\", IntegerType.INTEGER).add(\"part\", StringType.STRING);\n\n    withTestTable(\n        schema,\n        Collections.emptyList(),\n        table -> {\n          List<Row> actions =\n              dummyAddFileRows(schema, 5, (i) -> Map.of(\"part\", Literal.ofString(\"p\" + i)));\n\n          CloseableIterable<Row> dataActions =\n              new CloseableIterable<Row>() {\n                @Override\n                public CloseableIterator<Row> iterator() {\n                  return Utils.toCloseableIterator(actions.iterator());\n                }\n\n                @Override\n                public void close() {\n                  // Nothing to close\n                }\n              };\n\n          table.commit(dataActions, \"a\", 100, Collections.emptyMap());\n\n          // The target table should have one version\n          verifyTableContent(\n              table.getTablePath().toString(),\n              (version, addFiles, properties) -> {\n                assertEquals(1L, version);\n                // There should be 5 files to scan\n                List<AddFile> actionsList = new ArrayList<>();\n                addFiles.forEach(actionsList::add);\n                assertEquals(5, actionsList.size());\n                long sum = actionsList.stream().mapToLong(af -> af.getNumRecords().get()).sum();\n                assertEquals(60, sum);\n              });\n        });\n  }\n\n  @Test\n  void testCreateNewTableAndCommitWithPartition() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"part\", StringType.STRING);\n\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  URI.create(tablePath), Collections.emptyMap(), schema, List.of(\"part\"));\n          table.open();\n\n          List<Row> actions =\n              dummyAddFileRows(schema, 5, (i) -> Map.of(\"part\", Literal.ofString(\"p\" + i)));\n\n          CloseableIterable<Row> dataActions =\n              new CloseableIterable<Row>() {\n                @Override\n                public CloseableIterator<Row> iterator() {\n                  return Utils.toCloseableIterator(actions.iterator());\n                }\n\n                @Override\n                public void close() {\n                  // Nothing to close\n                }\n              };\n\n          table.commit(dataActions, \"a\", 100, Collections.emptyMap());\n\n          // The target table should have one version\n          verifyTableContent(\n              dir.toString(),\n              (version, addFiles, properties) -> {\n                assertEquals(1L, version);\n                // There should be 5 files to scan\n                List<AddFile> actionsList = new ArrayList<>();\n                addFiles.forEach(actionsList::add);\n                assertEquals(5, actionsList.size());\n\n                Set<String> partitionValues =\n                    actionsList.stream()\n                        .map(af -> af.getPartitionValues().getValues().getString(0))\n                        .collect(Collectors.toSet());\n                assertEquals(Set.of(\"p0\", \"p1\", \"p2\", \"p3\", \"p4\"), partitionValues);\n\n                long sum = actionsList.stream().mapToLong(af -> af.getNumRecords().get()).sum();\n                assertEquals(60, sum);\n              });\n        });\n  }\n\n  @Test\n  void testCommitToExistingTableWithoutPartition() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"part\", StringType.STRING);\n          createNonEmptyTable(\n              DefaultEngine.create(new Configuration()),\n              tablePath,\n              schema,\n              Collections.emptyList(),\n              30);\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  URI.create(tablePath), Collections.emptyMap(), schema, Collections.emptyList());\n          table.open();\n\n          List<Row> actions = dummyAddFileRows(schema, 5, (i) -> Map.of());\n\n          CloseableIterable<Row> dataActions =\n              CloseableIterable.inMemoryIterable(Utils.toCloseableIterator(actions.iterator()));\n\n          table.commit(dataActions, \"a\", 100, Collections.emptyMap());\n\n          // The target table should have one version\n          verifyTableContent(\n              dir.toString(),\n              (version, addFiles, properties) -> {\n                assertEquals(1L, version);\n                // There should be 6 files to scan\n                List<AddFile> actionsList = new ArrayList<>();\n                addFiles.forEach(actionsList::add);\n                assertEquals(6, actionsList.size());\n                long sum = actionsList.stream().mapToLong(af -> af.getNumRecords().get()).sum();\n                assertEquals(90, sum);\n              });\n        });\n  }\n\n  @Test\n  void testCommitToExistingTable() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"part\", StringType.STRING);\n          createNonEmptyTable(\n              DefaultEngine.create(new Configuration()), tablePath, schema, List.of(\"part\"), 30);\n\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  URI.create(tablePath), Collections.emptyMap(), schema, List.of(\"part\"));\n          table.open();\n\n          List<Row> actions =\n              dummyAddFileRows(schema, 5, (i) -> Map.of(\"part\", Literal.ofString(\"p\" + i)));\n\n          CloseableIterable<Row> dataActions =\n              CloseableIterable.inMemoryIterable(Utils.toCloseableIterator(actions.iterator()));\n\n          table.commit(dataActions, \"a\", 100, Collections.emptyMap());\n\n          // The target table should have one version\n          verifyTableContent(\n              dir.toString(),\n              (version, addFiles, properties) -> {\n                assertEquals(1L, version);\n                // There should be 6 files to scan\n                List<AddFile> actionsList = new ArrayList<>();\n                addFiles.forEach(actionsList::add);\n                assertEquals(6, actionsList.size());\n\n                Set<String> partitionValues =\n                    actionsList.stream()\n                        .map(af -> af.getPartitionValues().getValues().getString(0))\n                        .collect(Collectors.toSet());\n                assertTrue(partitionValues.containsAll(Set.of(\"p0\", \"p1\", \"p2\", \"p3\", \"p4\")));\n\n                long sum = actionsList.stream().mapToLong(af -> af.getNumRecords().get()).sum();\n                assertEquals(90, sum);\n              });\n        });\n  }\n\n  @Test\n  void testRefreshOnEmptyTable() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"part\", StringType.STRING);\n\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  URI.create(tablePath), Collections.emptyMap(), schema, List.of(\"part\"));\n          table.open();\n          table.refresh();\n          assertTrue(table.snapshot().isPresent());\n          Snapshot snapshot = table.snapshot().get();\n          assertEquals(0, snapshot.getVersion());\n        });\n  }\n\n  @Test\n  void testRefreshOnExistingTable() {\n    withTempDir(\n        dir -> {\n          String tablePath = dir.getAbsolutePath();\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"part\", StringType.STRING);\n\n          createNonEmptyTable(\n              DefaultEngine.create(new Configuration()), tablePath, schema, List.of(\"part\"), 30);\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  URI.create(tablePath), Collections.emptyMap(), schema, List.of(\"part\"));\n          table.open();\n\n          table.refresh();\n          assertEquals(0, table.snapshot().get().getVersion());\n        });\n  }\n\n  @Test\n  void testCloseCancelOngoingOperations() {\n    withTempDir(\n        dir -> {\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n          createNonEmptyTable(engine, dir.getAbsolutePath(), schema);\n\n          int[] callCounter = {0};\n\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  dir.toURI(), Collections.emptyMap(), schema, Collections.emptyList()) {\n\n                @Override\n                protected Snapshot loadLatestSnapshot() {\n                  Snapshot snapshot = super.loadLatestSnapshot();\n                  callCounter[0]++;\n                  if (callCounter[0] >= 2) {\n                    for (int i = 0; i < 50; i++) {\n                      try {\n                        Thread.sleep(100);\n                      } catch (InterruptedException e) {\n                        Thread.currentThread().interrupt();\n                        break;\n                      }\n                    }\n                  }\n                  return snapshot;\n                }\n              };\n          table.open();\n\n          // With cache, load will not be called again\n          table.setCacheManager(new SnapshotCacheManager.NoCacheManager());\n\n          // this thread will refresh the table\n          Thread thread1 =\n              new Thread(\n                  () -> {\n                    table.refresh();\n                  });\n          thread1.start();\n\n          // If we do not call close, the refresh will take ~5s to stop\n          long wcstart = System.currentTimeMillis();\n          while (thread1.isAlive()) {\n            try {\n              Thread.sleep(100);\n            } catch (InterruptedException e) {\n              Thread.currentThread().interrupt();\n              break;\n            }\n          }\n          long elapse = System.currentTimeMillis() - wcstart;\n          assertTrue(elapse >= 4500);\n\n          // this thread will refresh the table\n          Thread thread2 =\n              new Thread(\n                  () -> {\n                    try {\n                      table.refresh();\n                    } catch (Exception e) {\n                      // Ignore the InterruptException\n                    }\n                  });\n          thread2.start();\n          // If we call close, the refresh was interrupted quickly\n          try {\n            Thread.sleep(100);\n          } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n          }\n          wcstart = System.currentTimeMillis();\n          table.close();\n          while (thread2.isAlive()) {\n            try {\n              Thread.sleep(100);\n            } catch (InterruptedException e) {\n              Thread.currentThread().interrupt();\n              break;\n            }\n          }\n          elapse = System.currentTimeMillis() - wcstart;\n          assertTrue(elapse < 200);\n        });\n  }\n\n  @Test\n  void testRetryConcurrencyException() {\n    withTempDir(\n        dir -> {\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n          createNonEmptyTable(engine, dir.getAbsolutePath(), schema);\n\n          int[] retryCounter = {0};\n          int[] loadCounter = {0};\n\n          LocalFileSystemTable testHadoopTable =\n              new LocalFileSystemTable(\n                  dir.toURI(), Collections.emptyMap(), schema, Collections.emptyList()) {\n\n                @Override\n                protected Snapshot loadLatestSnapshot() {\n                  loadCounter[0]++;\n                  Snapshot result = super.loadLatestSnapshot();\n                  if (loadCounter[0] == 2) {\n                    throw new ConcurrentModificationException();\n                  }\n                  return result;\n                }\n\n                @Override\n                public void reloadSnapshot() {\n                  // This should be called once\n                  retryCounter[0]++;\n                }\n              };\n          testHadoopTable.open();\n          // Disable cache for retry to work\n          testHadoopTable.setCacheManager(new SnapshotCacheManager.NoCacheManager());\n\n          testHadoopTable.commit(\n              CloseableIterable.inMemoryIterable(\n                  Utils.singletonCloseableIterator(dummyAddFileRow(schema, 4, Map.of()))),\n              \"a\",\n              1000L,\n              Collections.emptyMap());\n\n          assertEquals(1, retryCounter[0]);\n          assertEquals(3, loadCounter[0]);\n        });\n  }\n\n  @Test\n  void testRetryCredentialExceptionToSucceed() {\n    withTempDir(\n        dir -> {\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n          createNonEmptyTable(engine, dir.getAbsolutePath(), schema);\n\n          int[] retryCounter = {0};\n          int[] loadCounter = {0};\n\n          LocalFileSystemTable testHadoopTable =\n              new LocalFileSystemTable(\n                  dir.toURI(), Collections.emptyMap(), schema, Collections.emptyList()) {\n\n                @Override\n                protected Snapshot loadLatestSnapshot() {\n                  loadCounter[0]++;\n                  Snapshot result = super.loadLatestSnapshot();\n                  if (loadCounter[0] == 2) {\n                    throw new RuntimeException(new AccessDeniedException(\"\"));\n                  }\n                  return result;\n                }\n\n                @Override\n                public void refreshCredential() {\n                  // This should be called once\n                  retryCounter[0]++;\n                }\n              };\n          testHadoopTable.open();\n          testHadoopTable.setCacheManager(new SnapshotCacheManager.NoCacheManager());\n          testHadoopTable.refresh();\n          assertEquals(1, retryCounter[0]);\n        });\n  }\n\n  @Test\n  void testRetryCredentialExceptionToExceedMaxAttempts() {\n    withTempDir(\n        dir -> {\n          Engine engine = DefaultEngine.create(new Configuration());\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n          createNonEmptyTable(engine, dir.getAbsolutePath(), schema);\n\n          int[] retryCounter = {0};\n          int[] loadCounter = {0};\n\n          LocalFileSystemTable testHadoopTable =\n              new LocalFileSystemTable(\n                  dir.toURI(), Collections.emptyMap(), schema, Collections.emptyList()) {\n\n                @Override\n                protected Snapshot loadLatestSnapshot() {\n                  loadCounter[0]++;\n                  Snapshot result = super.loadLatestSnapshot();\n                  if (loadCounter[0] >= 2) {\n                    throw new RuntimeException(new AccessDeniedException(\"\"));\n                  }\n                  return result;\n                }\n\n                @Override\n                public void refreshCredential() {\n                  // This should be called three times\n                  retryCounter[0]++;\n                }\n              };\n          testHadoopTable.open();\n          // Disable cache for retry to work\n          testHadoopTable.setCacheManager(new SnapshotCacheManager.NoCacheManager());\n\n          Exception e = assertThrows(Exception.class, () -> testHadoopTable.refresh());\n          assertTrue(\n              ExceptionUtils.recursiveCheck(ex -> ex instanceof AccessDeniedException).test(e));\n          assertEquals(3, retryCounter[0]);\n        });\n  }\n\n  @Test\n  void testWriteResultHasProperStats() {\n    StructType schema =\n        new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n\n    withTestTable(\n        schema,\n        Collections.emptyList(),\n        table -> {\n          int numColumns = 2;\n          ColumnVector[] columnVectors = new ColumnVector[numColumns];\n\n          List<List<?>> dataBuffer = List.of(List.of(1, \"Jack\"), List.of(2, \"Amy\"));\n\n          for (int colIdx = 0; colIdx < numColumns; colIdx++) {\n            var colDataType = schema.at(colIdx).getDataType();\n            columnVectors[colIdx] = new DataColumnVectorView(dataBuffer, colIdx, colDataType);\n          }\n\n          CloseableIterator<FilteredColumnarBatch> data =\n              Utils.singletonCloseableIterator(\n                  new FilteredColumnarBatch(\n                      new DefaultColumnarBatch(dataBuffer.size(), schema, columnVectors),\n                      Optional.empty()));\n\n          CloseableIterator<Row> result = table.writeParquet(\"\", data, Collections.emptyMap());\n\n          result.toInMemoryList().stream()\n              .map(r -> new AddFile(r.getStruct(SingleAction.ADD_FILE_ORDINAL)))\n              .forEach(\n                  file -> {\n                    assertFalse(file.getStatsJson().isEmpty());\n                    assertEquals(\n                        Optional.of(\n                            \"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":1,\\\"name\\\":\\\"Amy\\\"},\"\n                                + \"\\\"maxValues\\\":{\\\"id\\\":2,\\\"name\\\":\\\"Jack\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"name\\\":0}}\"),\n                        file.getStatsJson());\n                  });\n        });\n  }\n\n  @Test\n  public void testGenerateChecksum() {\n    withTempDir(\n        dir -> {\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  dir.toURI(), Collections.emptyMap(), schema, Collections.emptyList());\n          table.open();\n\n          for (int i = 0; i < 10; i++) {\n            table.commit(\n                CloseableIterable.inMemoryIterable(\n                    Utils.singletonCloseableIterator(dummyAddFileRow(schema, 10, Map.of()))),\n                \"a\",\n                1000L + i,\n                Collections.emptyMap());\n\n            String checksumPath =\n                String.format(\"%s/_delta_log/%020d.crc\", dir.getAbsolutePath(), i);\n            File checksumFile = new File(checksumPath);\n            // Async creation, wait for file to appear\n            for (int j = 0; j < 100; j++) {\n              if (checksumFile.exists()) {\n                break;\n              }\n              Thread.sleep(100);\n            }\n            assertTrue(checksumFile.exists(), checksumPath);\n          }\n        });\n  }\n\n  @Test\n  public void testCheckpoint() {\n    withTempDir(\n        dir -> {\n          StructType schema =\n              new StructType().add(\"id\", IntegerType.INTEGER).add(\"name\", StringType.STRING);\n          LocalFileSystemTable table =\n              new LocalFileSystemTable(\n                  dir.toURI(),\n                  Map.of(TableConf.CHECKPOINT_FREQUENCY.key(), \"1.0\"),\n                  schema,\n                  Collections.emptyList());\n          table.open();\n\n          for (int i = 0; i < 10; i++) {\n            table.commit(\n                CloseableIterable.inMemoryIterable(\n                    Utils.singletonCloseableIterator(dummyAddFileRow(schema, 10, Map.of()))),\n                \"a\",\n                1000L + i,\n                Collections.emptyMap());\n\n            String checkpointPath =\n                String.format(\"%s/_delta_log/%020d.checkpoint.parquet\", dir.getAbsolutePath(), i);\n            File checkpointFile = new File(checkpointPath);\n            // Async creation, wait for file to appear\n            for (int j = 0; j < 100; j++) {\n              if (checkpointFile.exists()) {\n                break;\n              }\n              Thread.sleep(100);\n            }\n            assertTrue(checkpointFile.exists(), checkpointPath);\n            // Ensure cache is updated\n            var cachedSnapshot = table.snapshot().get();\n            assertEquals(i + 1, cachedSnapshot.getVersion());\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/table/CredentialManagerTest.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport io.delta.flink.Conf;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Supplier;\nimport org.junit.jupiter.api.Test;\n\n/** JUnit test suite for CredentialManager. */\nclass CredentialManagerTest {\n\n  @Test\n  void testGetAndAutoRefreshCredentials() throws InterruptedException {\n    Supplier<Map<String, String>> supplier =\n        new Supplier<Map<String, String>>() {\n          private int callCount = 0;\n\n          @Override\n          public Map<String, String> get() {\n            long currentTime = System.currentTimeMillis();\n            long refreshInterval = Conf.getInstance().getCredentialsRefreshAheadInMs();\n            Map<String, String> result = new HashMap<>();\n            result.put(\"authKey\", \"authValue\" + callCount);\n            // Refresh after around 100 ms\n            result.put(\n                CredentialManager.CREDENTIAL_EXPIRATION_KEY,\n                String.valueOf(currentTime + refreshInterval + 100));\n            callCount++;\n            return result;\n          }\n        };\n\n    CredentialManager manager = new CredentialManager(supplier, () -> {});\n\n    // Initial values\n    Map<String, String> initialResult = manager.getCredentials();\n    Thread.sleep(150);\n    // Refreshed values\n    Map<String, String> refreshedResult = manager.getCredentials();\n\n    assertEquals(\"authValue0\", initialResult.get(\"authKey\"));\n    assertEquals(\"authValue1\", refreshedResult.get(\"authKey\"));\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/table/DataColumnVectorView.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.List;\n\n/**\n * A wrapper to provide a ColumnVector view backed by a nested list. Usage example: <code>\n *     val dataBuffer = java.util.List.of(\n *         java.util.List.of(1, \"Jack\"),\n *         java.util.List.of(2, \"Amy\"))\n *     val cv1 = new DataColumnVectorView(dataBuffer, 0, IntegerType.INTEGER)\n *     val cv2 = new DataColumnVectorView(dataBuffer, 1, StringType.STRING)\n * </code>\n */\npublic class DataColumnVectorView implements ColumnVector {\n\n  private final List<List<?>> rows;\n  private final int colIdx;\n  private final DataType dataType;\n\n  public DataColumnVectorView(List<List<?>> rows, int colIdx, DataType dataType) {\n    this.rows = rows;\n    this.colIdx = colIdx;\n    this.dataType = dataType;\n  }\n\n  @Override\n  public DataType getDataType() {\n    return this.dataType;\n  }\n\n  @Override\n  public int getSize() {\n    return this.rows.size();\n  }\n\n  @Override\n  public void close() {}\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    checkValidRowId(rowId);\n    return rows.get(rowId).get(colIdx) == null;\n  }\n\n  protected void checkValidRowId(int rowId) {\n    if (rowId < 0 || rowId >= getSize()) {\n      throw new IllegalArgumentException(\"RowId out of range: \" + rowId + \" <-> \" + getSize());\n    }\n  }\n\n  protected void checkValidDataType(io.delta.kernel.types.DataType dataType) {\n    if (!this.getDataType().equivalent(dataType)) {\n      throw new UnsupportedOperationException(\"Invalid value request for data type\");\n    }\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    checkValidRowId(rowId);\n    checkValidDataType(IntegerType.INTEGER);\n    return (Integer) rows.get(rowId).get(colIdx);\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    checkValidRowId(rowId);\n    checkValidDataType(LongType.LONG);\n    return (Long) rows.get(rowId).get(colIdx);\n  }\n\n  @Override\n  public String getString(int rowId) {\n    checkValidRowId(rowId);\n    checkValidDataType(StringType.STRING);\n    return rows.get(rowId).get(colIdx).toString();\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    checkValidRowId(rowId);\n    checkValidDataType(FloatType.FLOAT);\n    return (Float) rows.get(rowId).get(colIdx);\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    checkValidRowId(rowId);\n    checkValidDataType(DoubleType.DOUBLE);\n    return (Double) rows.get(rowId).get(colIdx);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    checkValidRowId(rowId);\n    // Do not check precision and scale here because RowData support conversion\n    if (!(this.getDataType() instanceof DecimalType)) {\n      throw new UnsupportedOperationException(\"Invalid value request for data type\");\n    }\n    DecimalType actualType = (DecimalType) dataType;\n    return (BigDecimal) rows.get(rowId).get(colIdx);\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/table/LocalFileSystemCatalog.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.kernel.types.StructType;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class LocalFileSystemCatalog implements DeltaCatalog {\n\n  private final Map<String, String> configurations;\n\n  public LocalFileSystemCatalog(Map<String, String> conf) {\n    this.configurations = conf;\n  }\n\n  @Override\n  public TableDescriptor getTable(String tableId) {\n    URI tablePath = AbstractKernelTable.normalize(URI.create(tableId));\n    if (!Files.exists(Path.of(tablePath.resolve(\"_delta_log\")))) {\n      throw new ExceptionUtils.ResourceNotFoundException(\"\");\n    }\n    TableDescriptor info = new TableDescriptor();\n    info.tableId = tableId;\n    info.tablePath = tablePath;\n    info.uuid = tableId;\n    return info;\n  }\n\n  @Override\n  public void createTable(\n      String tableId,\n      StructType schema,\n      List<String> partitions,\n      Map<String, String> properties,\n      Consumer<TableDescriptor> callback) {\n    TableDescriptor desc = new TableDescriptor(tableId, tableId, URI.create(tableId));\n    callback.accept(desc);\n  }\n\n  @Override\n  public Map<String, String> getCredentials(String uuid) {\n    return configurations;\n  }\n}\n"
  },
  {
    "path": "flink/src/test/java/io/delta/flink/table/LocalFileSystemTable.java",
    "content": "/*\n *  Copyright (2026) The Delta Lake Project Authors.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage io.delta.flink.table;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.types.StructType;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\n\npublic class LocalFileSystemTable extends AbstractKernelTable {\n\n  public LocalFileSystemTable(\n      URI tablePath, Map<String, String> conf, StructType schema, List<String> partitionColumns) {\n    super(new LocalFileSystemCatalog(conf), tablePath.toString(), conf, schema, partitionColumns);\n  }\n\n  @Override\n  protected Snapshot loadLatestSnapshot() {\n    return TableManager.loadSnapshot(getTablePath().toString()).build(getEngine());\n  }\n}\n"
  },
  {
    "path": "flink/src/test/resources/log4j2-test.properties",
    "content": "#\n#  Copyright (2026) The Delta Lake Project Authors.\n#\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#\n#  http://www.apache.org/licenses/LICENSE-2.0\n#\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n# Console appender\nappender.console.type = Console\nappender.console.name = console\nappender.console.target = SYSTEM_OUT\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = %d{HH:mm:ss} %-5level %c{1} - %msg%n\n\n# Root logger\nrootLogger.level = warn\nrootLogger.appenderRefs = console\nrootLogger.appenderRef.console.ref = console\n\n# Package-specific log levels\nlogger.delta.name = io.delta.kernel\nlogger.delta.level = warn\n\nlogger.flink.name = org.apache.flink\nlogger.flink.level = warn\n\nlogger.kafka.name = org.apache.kafka\nlogger.kafka.level = warn\n\nlogger.hadoop.name = org.apache.hadoop\nlogger.hadoop.level = warn\n"
  },
  {
    "path": "hudi/README.md",
    "content": "# Converting to Hudi with UniForm\n## Create a table with Hudi UniForm enabled\nUsing spark-sql you can create a table and insert a few records into it. You will need to include the delta-hudi-assembly jar on the path.\n```\nspark-sql --packages io.delta:delta-spark_2.12:3.2.0-SNAPSHOT --jars delta-hudi-assembly_2.12-3.2.0-SNAPSHOT.jar --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n```\nThen you can create a table with Hudi UniForm enabled.\n```\nCREATE TABLE `delta_table_with_hudi` (col1 INT) USING DELTA TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi') LOCATION '/tmp/delta-table-with-hudi';\n```\nAnd insert a record into it.\n```\nINSERT INTO delta_table_with_hudi VALUES (1);\n```\n\n## Read the table with Hudi\nHudi does not currently support spark 3.5.X so you will need to launch a spark shell with spark 3.4.X or earlier.  \nInstructions for launching the spark-shell with Hudi can be found [here](https://hudi.apache.org/docs/quick-start-guide#spark-shellsql).  \nAfter launching the shell, you can read the table by enabling the hudi metadata table in the reader and loading from the path used in the create table step.\n```scala\nval df = spark.read.format(\"hudi\").option(\"hoodie.metadata.enable\", \"true\").load(\"/tmp/delta-table-with-hudi\")\n```"
  },
  {
    "path": "hudi/integration_tests/write_uniform_hudi.py",
    "content": "from pyspark.sql import SparkSession\nfrom pyspark.sql.functions import current_date, current_timestamp\nfrom pyspark.testing import assertDataFrameEqual\nfrom delta.tables import DeltaTable\nimport shutil\nimport random\nimport os\nimport time\n\n###################### Setup ######################\n\ntest_root = \"/tmp/delta-uniform-hudi/\"\nwarehouse_path = test_root + \"uniform_tables\"\nshutil.rmtree(test_root, ignore_errors=True)\nhudi_table_base_name = \"delta_table_with_hudi\"\n\n# we need to set the following configs\nspark_delta = SparkSession.builder \\\n    .appName(\"delta-uniform-hudi-writer\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(\"spark.sql.warehouse.dir\", warehouse_path) \\\n    .getOrCreate()\n\n###################### Helper functions ######################\n\ndef get_delta_df(spark, table_name):\n    hudi_table_path = os.path.join(warehouse_path, table_name)\n    print('hudi_table_path:', hudi_table_path)\n    df_delta = spark.read.format(\"delta\").load(hudi_table_path)\n    return df_delta\n\ndef get_hudi_df(spark, table_name):\n    hudi_table_path = os.path.join(warehouse_path, table_name)\n    df_hudi = (spark.read.format(\"hudi\")\n               .option(\"hoodie.metadata.enable\", \"true\")\n               .option(\"hoodie.datasource.write.hive_style_partitioning\", \"true\")\n               .load(hudi_table_path))\n    return df_hudi\n\n###################### Create tables in Delta ######################\nprint('Delta tables:')\n\n# validate various data types\nspark_delta.sql(f\"\"\"CREATE TABLE `{hudi_table_base_name}_0` (col1 BIGINT, col2 BOOLEAN, col3 DATE,\n    col4 DOUBLE, col5 FLOAT, col6 INT, col7 STRING, col8 TIMESTAMP,\n    col9 BINARY, col10 DECIMAL(5, 2), \n    col11 STRUCT<field1: INT, field2: STRING,\n    field3: STRUCT<field4: INT, field5: INT, field6: STRING>>,\n    col12 ARRAY<STRUCT<field1: INT, field2: STRING>>,\n    col13 MAP<STRING, STRUCT<field1: INT, field2: STRING>>) USING DELTA\n    TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi') \"\"\")\nspark_delta.sql(f\"\"\"INSERT INTO `{hudi_table_base_name}_0` VALUES \n    (123, true, date(current_timestamp()), 32.1, 1.23, 456, 'hello world', \n    current_timestamp(), X'1ABF', -999.99,\n    STRUCT(1, 'hello', STRUCT(2, 3, 'world')),\n    ARRAY(\n        STRUCT(1, 'first'),\n        STRUCT(2, 'second')\n    ),\n    MAP(\n        'key1', STRUCT(1, 'delta'),\n        'key2', STRUCT(1, 'lake')\n    )); \"\"\")\n\ndf_delta_0 = get_delta_df(spark_delta, f\"{hudi_table_base_name}_0\")\ndf_delta_0.show()\n\n# conversion happens correctly when enabling property after table creation\nspark_delta.sql(f\"CREATE TABLE {hudi_table_base_name}_1 (col1 INT, col2 STRING) USING DELTA\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_1 VALUES (1, 'a'), (2, 'b')\")\nspark_delta.sql(f\"ALTER TABLE {hudi_table_base_name}_1 SET TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi')\")\n\ndf_delta_1 = get_delta_df(spark_delta, f\"{hudi_table_base_name}_1\")\ndf_delta_1.show()\n\n# validate deletes\nspark_delta.sql(f\"\"\"CREATE TABLE {hudi_table_base_name}_2 (col1 INT, col2 STRING) USING DELTA\n                TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi')\"\"\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_2 VALUES (1, 'a'), (2, 'b')\")\nspark_delta.sql(f\"DELETE FROM {hudi_table_base_name}_2 WHERE col1 = 1\")\n\ndf_delta_2 = get_delta_df(spark_delta, f\"{hudi_table_base_name}_2\")\ndf_delta_2.show()\n\n# basic schema evolution\nspark_delta.sql(f\"\"\"CREATE TABLE {hudi_table_base_name}_3 (col1 INT, col2 STRING) USING DELTA\n                TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi')\"\"\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_3 VALUES (1, 'a'), (2, 'b')\")\nspark_delta.sql(f\"ALTER TABLE {hudi_table_base_name}_3 ADD COLUMN col3 INT FIRST\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_3 VALUES (3, 4, 'c')\")\n\ndf_delta_3 = get_delta_df(spark_delta, f\"{hudi_table_base_name}_3\")\ndf_delta_3.show()\n\n# schema evolution for nested fields\nspark_delta.sql(f\"\"\"CREATE TABLE {hudi_table_base_name}_4 (col1 STRUCT<field1: INT, field2: STRING>) \n                USING DELTA\n                TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi')\"\"\")\nspark_delta.sql(f\"\"\"INSERT INTO {hudi_table_base_name}_4 VALUES \n                    (named_struct('field1', 1, 'field2', 'hello'))\n                    \"\"\")\nspark_delta.sql(f\"ALTER TABLE {hudi_table_base_name}_4 ADD COLUMN col1.field3 INT AFTER field1\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_4 VALUES (named_struct('field1', 3, 'field3', 4, 'field2', 'delta'))\")\n\ndf_delta_4 = get_delta_df(spark_delta, f\"{hudi_table_base_name}_4\")\ndf_delta_4.show()\n\n# time travel\nspark_delta.sql(f\"\"\"CREATE TABLE {hudi_table_base_name}_5 (col1 INT, col2 STRING) USING DELTA\n                TBLPROPERTIES('delta.universalFormat.enabledFormats' = 'hudi',\n                              'delta.columnMapping.mode' = 'name')\"\"\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_5 VALUES (1, 'a')\")\nspark_delta.sql(f\"INSERT INTO {hudi_table_base_name}_5 VALUES (2, 'b')\")\n\ndf_history_5 = spark_delta.sql(f\"DESCRIBE HISTORY {hudi_table_base_name}_5\")\ntimestamp = df_history_5.collect()[0]['timestamp'] # get the timestamp of the first commit\ndf_delta_5 = spark_delta.sql(f\"\"\"\n    SELECT * FROM {hudi_table_base_name}_5\n    TIMESTAMP AS OF '{timestamp}'\"\"\")\ndf_delta_5.show()\n\ntime.sleep(5)\n\n###################### Read tables from Hudi engine ######################\nprint('Hudi tables:')\n\nspark_hudi = SparkSession.builder \\\n    .appName(\"delta-uniform-hudi-reader\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"org.apache.spark.sql.hudi.HoodieSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.hudi.catalog.HoodieCatalog\") \\\n    .config(\"spark.sql.warehouse.dir\", warehouse_path) \\\n    .config(\"spark.serializer\", \"org.apache.spark.serializer.KryoSerializer\") \\\n    .config(\"spark.kryo.registrator\", \"org.apache.spark.HoodieSparkKryoRegistrar\") \\\n    .getOrCreate()\n\ndf_hudi_0 = get_hudi_df(spark_hudi, f\"{hudi_table_base_name}_0\")\ndf_hudi_0.show()\nassertDataFrameEqual(df_delta_0, df_hudi_0)\n\ndf_hudi_1 = get_hudi_df(spark_hudi, f\"{hudi_table_base_name}_1\")\ndf_hudi_1.show()\nassertDataFrameEqual(df_delta_1, df_hudi_1)\n\ndf_hudi_2 = get_hudi_df(spark_hudi, f\"{hudi_table_base_name}_2\")\ndf_hudi_2.show()\nassertDataFrameEqual(df_delta_2, df_hudi_2)\n\ndf_hudi_3 = get_hudi_df(spark_hudi, f\"{hudi_table_base_name}_3\")\ndf_hudi_3.show()\nassertDataFrameEqual(df_delta_3, df_hudi_3)\n\ndf_hudi_4 = get_hudi_df(spark_hudi, f\"{hudi_table_base_name}_4\")\ndf_hudi_4.show()\nassertDataFrameEqual(df_delta_4, df_hudi_4)\n\ndf_hudi_5 = spark_hudi.sql(f\"\"\"\n    SELECT * FROM {hudi_table_base_name}_5\n    TIMESTAMP AS OF '{timestamp}'\"\"\")\ndf_hudi_5.show()\nassertDataFrameEqual(df_delta_5, df_hudi_5)\n\nprint('UniForm Hudi integration test passed!')"
  },
  {
    "path": "hudi/src/main/scala/org/apache/spark/sql/delta/hudi/HudiConversionTransaction.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hudi\n\nimport java.io.{IOException, UncheckedIOException}\nimport java.time.{Instant, LocalDateTime, ZoneId}\nimport java.time.format.{DateTimeFormatterBuilder, DateTimeParseException}\nimport java.time.temporal.{ChronoField, ChronoUnit}\nimport java.util\nimport java.util.{Collections, Properties}\nimport java.util.stream.Collectors\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Snapshot\nimport org.apache.spark.sql.delta.actions.Action\nimport org.apache.spark.sql.delta.hudi.HudiSchemaUtils._\nimport org.apache.spark.sql.delta.hudi.HudiTransactionUtils._\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.avro.Schema\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hudi.avro.model.HoodieActionInstant\nimport org.apache.hudi.avro.model.HoodieCleanerPlan\nimport org.apache.hudi.avro.model.HoodieCleanFileInfo\nimport org.apache.hudi.client.HoodieJavaWriteClient\nimport org.apache.hudi.client.HoodieTimelineArchiver\nimport org.apache.hudi.client.WriteStatus\nimport org.apache.hudi.client.common.HoodieJavaEngineContext\nimport org.apache.hudi.common.HoodieCleanStat\nimport org.apache.hudi.common.config.HoodieMetadataConfig\nimport org.apache.hudi.common.engine.HoodieEngineContext\nimport org.apache.hudi.common.model.{HoodieAvroPayload, HoodieBaseFile, HoodieCleaningPolicy}\nimport org.apache.hudi.common.table.HoodieTableMetaClient\nimport org.apache.hudi.common.table.timeline.{HoodieInstant, HoodieInstantTimeGenerator, HoodieTimeline, TimelineMetadataUtils}\nimport org.apache.hudi.common.table.timeline.HoodieInstantTimeGenerator.{MILLIS_INSTANT_TIMESTAMP_FORMAT_LENGTH, SECS_INSTANT_ID_LENGTH, SECS_INSTANT_TIMESTAMP_FORMAT}\nimport org.apache.hudi.common.util.{Option => HudiOption}\nimport org.apache.hudi.common.util.CleanerUtils\nimport org.apache.hudi.common.util.ExternalFilePathUtil\nimport org.apache.hudi.common.util.collection.Pair\nimport org.apache.hudi.config.HoodieArchivalConfig\nimport org.apache.hudi.config.HoodieCleanConfig\nimport org.apache.hudi.config.HoodieIndexConfig\nimport org.apache.hudi.config.HoodieWriteConfig\nimport org.apache.hudi.exception.{HoodieException, HoodieRollbackException}\nimport org.apache.hudi.index.HoodieIndex.IndexType.INMEMORY\nimport org.apache.hudi.table.HoodieJavaTable\nimport org.apache.hudi.table.action.clean.CleanPlanner\n\nimport org.apache.spark.internal.MDC\n\n/**\n * Used to prepare (convert) and then commit a set of Delta actions into the Hudi table located\n * at the same path as [[postCommitSnapshot]]\n *\n *\n * @param conf Configuration for Hudi Hadoop interactions.\n * @param postCommitSnapshot Latest Delta snapshot associated with this Hudi commit.\n */\nclass HudiConversionTransaction(\n    protected val conf: Configuration,\n    protected val postCommitSnapshot: Snapshot,\n    protected val providedMetaClient: HoodieTableMetaClient,\n    protected val lastConvertedDeltaVersion: Option[Long] = None) extends DeltaLogging {\n\n  //////////////////////\n  // Member variables //\n  //////////////////////\n\n  private val tablePath = postCommitSnapshot.deltaLog.dataPath\n  private val hudiSchema: Schema =\n    convertDeltaSchemaToHudiSchema(postCommitSnapshot.metadata.schema)\n  private var metaClient = providedMetaClient\n  private val instantTime = convertInstantToCommit(\n    Instant.ofEpochMilli(postCommitSnapshot.timestamp))\n  private var writeStatuses: util.List[WriteStatus] =\n    new util.ArrayList[WriteStatus]()\n  private var partitionToReplacedFileIds: util.Map[String, util.List[String]] =\n    new util.HashMap[String, util.List[String]]()\n\n  private val version = postCommitSnapshot.version\n  /** Tracks if this transaction has already committed. You can only commit once. */\n  private var committed = false\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  def setCommitFileUpdates(actions: scala.collection.Seq[Action]): Unit = {\n    // for all removed files, group by partition path and then map to\n    // the file group ID (name in this case)\n    val newPartitionToReplacedFileIds = actions\n      .map(_.wrap)\n      .filter(action => action.remove != null)\n      .map(_.remove)\n      .map(remove => {\n        val path = remove.toPath\n        val partitionPath = getPartitionPath(tablePath, path)\n        (partitionPath, path.getName)})\n      .groupBy(_._1).map(v => (v._1, v._2.map(_._2).asJava))\n      .asJava\n    partitionToReplacedFileIds.putAll(newPartitionToReplacedFileIds)\n    // Convert the AddFiles to write statuses for the commit\n    val newWriteStatuses = actions\n      .map(_.wrap)\n      .filter(action => action.add != null)\n      .map(_.add)\n      .map(add => {\n        convertAddFile(add, tablePath, instantTime)\n      })\n      .asJava\n    writeStatuses.addAll(newWriteStatuses)\n  }\n\n  def commit(): Unit = {\n    assert(!committed, \"Cannot commit. Transaction already committed.\")\n    val writeConfig = getWriteConfig(hudiSchema, getNumInstantsToRetain, 10, 7*24)\n    val engineContext: HoodieEngineContext = new HoodieJavaEngineContext(metaClient.getStorageConf)\n    val writeClient = new HoodieJavaWriteClient[AnyRef](engineContext, writeConfig)\n    try {\n      writeClient.startCommitWithTime(instantTime, HoodieTimeline.REPLACE_COMMIT_ACTION)\n      metaClient.getActiveTimeline.transitionReplaceRequestedToInflight(\n        new HoodieInstant(HoodieInstant.State.REQUESTED, HoodieTimeline.REPLACE_COMMIT_ACTION,\n          instantTime),\n        HudiOption.empty[Array[Byte]])\n      val syncMetadata: Map[String, String] = Map(\n        HudiConverter.DELTA_VERSION_PROPERTY -> version.toString,\n        HudiConverter.DELTA_TIMESTAMP_PROPERTY -> postCommitSnapshot.timestamp.toString)\n      writeClient.commit(instantTime,\n        writeStatuses,\n        HudiOption.of(syncMetadata.asJava),\n        HoodieTimeline.REPLACE_COMMIT_ACTION,\n        partitionToReplacedFileIds)\n      // if the metaclient was created before the table's first commit, we need to reload it to\n      // pick up the metadata table context\n      if (!metaClient.getTableConfig.isMetadataTableAvailable) {\n        metaClient = HoodieTableMetaClient.reload(metaClient)\n      }\n      val table = HoodieJavaTable.create(writeClient.getConfig, engineContext, metaClient)\n      // clean up old commits and archive them\n      markInstantsAsCleaned(table, writeClient.getConfig, engineContext)\n      runArchiver(table, writeClient.getConfig, engineContext)\n    } catch {\n      case e: HoodieException if e.getMessage == \"Failed to update metadata\"\n        || e.getMessage == \"Error getting all file groups in pending clustering\"\n        || e.getMessage == \"Error fetching partition paths from metadata table\" =>\n        logInfo(log\"[Thread=${MDC(DeltaLogKeys.THREAD_NAME, Thread.currentThread().getName)}] \" +\n          log\"Failed to fully update Hudi metadata table for Delta snapshot version \" +\n          log\"${MDC(DeltaLogKeys.VERSION, version)}. This is likely due to a concurrent \" +\n          log\"commit and should not lead to data corruption.\")\n      case e: HoodieRollbackException =>\n        logInfo(log\"[Thread=${MDC(DeltaLogKeys.THREAD_NAME, Thread.currentThread().getName)}] \" +\n          log\"Failed to rollback Hudi metadata table for Delta snapshot version \" +\n          log\"${MDC(DeltaLogKeys.VERSION, version)}. This is likely due to a concurrent \" +\n          log\"commit and should not lead to data corruption.\")\n      case NonFatal(e) =>\n        recordHudiCommit(Some(e))\n        throw e\n    } finally {\n      if (writeClient != null) writeClient.close()\n      recordHudiCommit()\n    }\n    committed = true\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  private def getNumInstantsToRetain = {\n    val commitCutoff = convertInstantToCommit(\n      parseFromInstantTime(instantTime).minus(7*24, ChronoUnit.HOURS))\n    // count number of completed commits after the cutoff\n    metaClient\n      .getActiveTimeline\n      .filterCompletedInstants\n      .findInstantsAfter(commitCutoff)\n      .countInstants\n  }\n\n  private def markInstantsAsCleaned(table: HoodieJavaTable[_],\n      writeConfig: HoodieWriteConfig, engineContext: HoodieEngineContext): Unit = {\n    val planner = new CleanPlanner(engineContext, table, writeConfig)\n    val earliestInstant = planner.getEarliestCommitToRetain\n    // since we're retaining based on time, we should exit early if earliestInstant is empty\n    if (!earliestInstant.isPresent) return\n    var partitionsToClean: util.List[String] = null\n    try partitionsToClean = planner.getPartitionPathsToClean(earliestInstant)\n    catch {\n      case ex: IOException =>\n        throw new UncheckedIOException(\"Unable to get partitions to clean\", ex)\n    }\n    if (partitionsToClean.isEmpty) return\n    val activeTimeline = metaClient.getActiveTimeline\n    val fsView = table.getHoodieView\n    val cleanInfoPerPartition = partitionsToClean.asScala.map(partition =>\n        Pair.of(partition, planner.getDeletePaths(partition, earliestInstant)))\n      .filter(deletePaths => !deletePaths.getValue.getValue.isEmpty)\n      .map(deletePathsForPartition => deletePathsForPartition.getKey -> {\n          val partition = deletePathsForPartition.getKey\n          // we need to manipulate the path to properly clean from the metadata table,\n          // so we map the file path to the base file\n          val baseFiles = fsView.getAllReplacedFileGroups(partition)\n            .flatMap(fileGroup => fileGroup.getAllBaseFiles)\n            .collect(Collectors.toList[HoodieBaseFile])\n          val baseFilesByPath = baseFiles.asScala\n            .map(baseFile => baseFile.getPath -> baseFile).toMap\n          deletePathsForPartition.getValue.getValue.asScala.map(cleanFileInfo => {\n            val baseFile = baseFilesByPath.getOrElse(cleanFileInfo.getFilePath, null)\n            new HoodieCleanFileInfo(ExternalFilePathUtil.appendCommitTimeAndExternalFileMarker(\n                baseFile.getFileName, baseFile.getCommitTime), false)\n          }).asJava\n    }).toMap.asJava\n    // there is nothing to clean, so exit early\n    if (cleanInfoPerPartition.isEmpty) return\n    // create a clean instant write after this latest commit\n    val cleanTime = convertInstantToCommit(parseFromInstantTime(instantTime)\n      .plus(1, ChronoUnit.SECONDS))\n    // create a metadata table writer in order to mark files as deleted in the table\n    // the deleted entries are cleaned up in the metadata table during compaction to control the\n    // growth of the table\n    val hoodieTableMetadataWriter = table.getMetadataWriter(cleanTime).get\n    try {\n      val earliestInstantToRetain = earliestInstant\n        .map[HoodieActionInstant]((earliestInstantToRetain: HoodieInstant) =>\n          new HoodieActionInstant(\n            earliestInstantToRetain.getTimestamp,\n            earliestInstantToRetain.getAction,\n            earliestInstantToRetain.getState.name))\n        .orElse(null)\n      val cleanerPlan = new HoodieCleanerPlan(earliestInstantToRetain, instantTime,\n        writeConfig.getCleanerPolicy.name, Collections.emptyMap[String, util.List[String]],\n        CleanPlanner.LATEST_CLEAN_PLAN_VERSION, cleanInfoPerPartition,\n        Collections.emptyList[String], Collections.emptyMap[String, String])\n      // create a clean instant and mark it as requested with the clean plan\n      val requestedCleanInstant = new HoodieInstant(HoodieInstant.State.REQUESTED,\n        HoodieTimeline.CLEAN_ACTION, cleanTime)\n      activeTimeline.saveToCleanRequested(\n        requestedCleanInstant, TimelineMetadataUtils.serializeCleanerPlan(cleanerPlan))\n      val inflightClean = activeTimeline\n        .transitionCleanRequestedToInflight(requestedCleanInstant, HudiOption.empty[Array[Byte]])\n      val cleanStats = cleanInfoPerPartition.entrySet.asScala.map(entry => {\n        val partitionPath = entry.getKey\n        val deletePaths = entry.getValue.asScala.map(_.getFilePath).asJava\n        new HoodieCleanStat(HoodieCleaningPolicy.KEEP_LATEST_COMMITS, partitionPath, deletePaths,\n          deletePaths, Collections.emptyList[String], earliestInstant.get.getTimestamp, instantTime)\n      }).toSeq.asJava\n      val cleanMetadata =\n        CleanerUtils.convertCleanMetadata(cleanTime, HudiOption.empty[java.lang.Long], cleanStats,\n          java.util.Collections.emptyMap[String, String])\n      // update the metadata table with the clean metadata so the files' metadata are marked for\n      // deletion\n      hoodieTableMetadataWriter.performTableServices(HudiOption.empty[String])\n      hoodieTableMetadataWriter.update(cleanMetadata, cleanTime)\n      // mark the commit as complete on the table timeline\n      activeTimeline.transitionCleanInflightToComplete(inflightClean,\n        TimelineMetadataUtils.serializeCleanMetadata(cleanMetadata))\n    } catch {\n      case ex: IOException =>\n        throw new UncheckedIOException(\"Unable to clean Hudi timeline\", ex)\n    } finally if (hoodieTableMetadataWriter != null) hoodieTableMetadataWriter.close()\n  }\n\n  private def runArchiver(table: HoodieJavaTable[_ <: HoodieAvroPayload],\n      config: HoodieWriteConfig, engineContext: HoodieEngineContext): Unit = {\n    // trigger archiver manually\n    val archiver = new HoodieTimelineArchiver(config, table)\n    archiver.archiveIfRequired(engineContext, true)\n  }\n\n  private def getWriteConfig(schema: Schema, numCommitsToKeep: Int,\n      maxNumDeltaCommitsBeforeCompaction: Int, timelineRetentionInHours: Int) = {\n    val properties = new Properties\n    properties.setProperty(HoodieMetadataConfig.AUTO_INITIALIZE.key, \"false\")\n    HoodieWriteConfig.newBuilder\n      .withIndexConfig(HoodieIndexConfig.newBuilder.withIndexType(INMEMORY).build)\n      .withPath(metaClient.getBasePathV2.toString)\n      .withPopulateMetaFields(metaClient.getTableConfig.populateMetaFields)\n      .withEmbeddedTimelineServerEnabled(false)\n      .withSchema(if (schema == null) \"\" else schema.toString)\n      .withArchivalConfig(HoodieArchivalConfig.newBuilder\n        .archiveCommitsWith(Math.max(0, numCommitsToKeep - 1), Math.max(1, numCommitsToKeep))\n        .withAutoArchive(false)\n        .build)\n      .withCleanConfig(\n        HoodieCleanConfig.newBuilder\n          .withCleanerPolicy(HoodieCleaningPolicy.KEEP_LATEST_BY_HOURS)\n          .cleanerNumHoursRetained(timelineRetentionInHours)\n          .withAutoClean(false)\n          .build)\n      .withMetadataConfig(HoodieMetadataConfig.newBuilder\n        .enable(true)\n        .withProperties(properties)\n        .withMetadataIndexColumnStats(true)\n        .withMaxNumDeltaCommitsBeforeCompaction(maxNumDeltaCommitsBeforeCompaction)\n        .build)\n      .build\n  }\n\n  /**\n   * Copied mostly from {@link\n   * org.apache.hudi.common.table.timeline.HoodieActiveTimeline#parseDateFromInstantTime(String)}\n   * but forces the timestamp to use UTC unlike the Hudi code.\n   *\n   * @param timestamp input commit timestamp\n   * @return timestamp parsed as Instant\n   */\n  private def parseFromInstantTime(timestamp: String): Instant = {\n    try {\n      var timestampInMillis: String = timestamp\n      if (isSecondGranularity(timestamp)) {\n        timestampInMillis = timestamp + \"999\"\n      }\n      else {\n        if (timestamp.length > MILLIS_INSTANT_TIMESTAMP_FORMAT_LENGTH) {\n          timestampInMillis = timestamp.substring(0, MILLIS_INSTANT_TIMESTAMP_FORMAT_LENGTH)\n        }\n      }\n      val dt: LocalDateTime = LocalDateTime.parse(timestampInMillis, MILLIS_INSTANT_TIME_FORMATTER)\n      dt.atZone(ZoneId.of(\"UTC\")).toInstant\n    } catch {\n      case ex: DateTimeParseException =>\n        throw new RuntimeException(\"Unable to parse date from commit timestamp: \" + timestamp, ex)\n    }\n  }\n\n  private def isSecondGranularity(instant: String) = instant.length == SECS_INSTANT_ID_LENGTH\n\n  private def convertInstantToCommit(instant: Instant): String = {\n    val instantTime = instant.atZone(ZoneId.of(\"UTC\")).toLocalDateTime\n    HoodieInstantTimeGenerator.getInstantFromTemporalAccessor(instantTime)\n  }\n\n  private def recordHudiCommit(errorOpt: Option[Throwable] = None): Unit = {\n\n    val errorData = errorOpt.map { e =>\n      Map(\n        \"exception\" -> ExceptionUtils.getMessage(e),\n        \"stackTrace\" -> ExceptionUtils.getStackTrace(e)\n      )\n    }.getOrElse(Map.empty)\n\n\n    recordDeltaEvent(\n      postCommitSnapshot.deltaLog,\n      s\"delta.hudi.conversion.commit.${if (errorOpt.isEmpty) \"success\" else \"error\"}\",\n      data = Map(\n        \"version\" -> postCommitSnapshot.version,\n        \"timestamp\" -> postCommitSnapshot.timestamp,\n        \"prevConvertedDeltaVersion\" -> lastConvertedDeltaVersion\n      ) ++ errorData\n    )\n  }\n\n  private val MILLIS_INSTANT_TIME_FORMATTER = new DateTimeFormatterBuilder()\n    .appendPattern(SECS_INSTANT_TIMESTAMP_FORMAT)\n    .appendValue(ChronoField.MILLI_OF_SECOND, 3)\n    .toFormatter\n    .withZone(ZoneId.of(\"UTC\"))\n}\n"
  },
  {
    "path": "hudi/src/main/scala/org/apache/spark/sql/delta/hudi/HudiConverter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hudi\n\nimport java.io.{IOException, UncheckedIOException}\nimport java.util.concurrent.atomic.AtomicReference\nimport javax.annotation.concurrent.GuardedBy\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.Snapshot\nimport org.apache.spark.sql.delta.UniversalFormatConverter\nimport org.apache.spark.sql.delta.actions.Action\nimport org.apache.spark.sql.delta.hooks.HudiConverterHook\nimport org.apache.spark.sql.delta.hudi.HudiTransactionUtils._\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hudi.common.model.{HoodieCommitMetadata, HoodieReplaceCommitMetadata}\nimport org.apache.hudi.common.table.HoodieTableMetaClient\nimport org.apache.hudi.common.table.timeline.{HoodieInstant, HoodieTimeline}\nimport org.apache.hudi.storage.hadoop.HadoopStorageConfiguration\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\nobject HudiConverter {\n  /**\n   * Property to be set in translated Hudi commit metadata.\n   * Indicates the delta commit version # that it corresponds to.\n   */\n  val DELTA_VERSION_PROPERTY = \"delta-version\"\n\n  /**\n   * Property to be set in translated Hudi commit metadata.\n   * Indicates the timestamp (milliseconds) of the delta commit that it corresponds to.\n   */\n  val DELTA_TIMESTAMP_PROPERTY = \"delta-timestamp\"\n}\n\n/**\n * This class manages the transformation of delta snapshots into their Hudi equivalent.\n */\nclass HudiConverter\n  extends UniversalFormatConverter\n  with DeltaLogging {\n\n  // Save an atomic reference of the snapshot being converted, and the txn that triggered\n  // resulted in the specified snapshot\n  protected val currentConversion =\n    new AtomicReference[(Snapshot, CommittedTransaction)]()\n  protected val standbyConversion =\n    new AtomicReference[(Snapshot, CommittedTransaction)]()\n\n  // Whether our async converter thread is active. We may already have an alive thread that is\n  // about to shutdown, but in such cases this value should return false.\n  @GuardedBy(\"asyncThreadLock\")\n  private var asyncConverterThreadActive: Boolean = false\n  private val asyncThreadLock = new Object\n\n  /**\n   * Enqueue the specified snapshot to be converted to Hudi. This will start an async\n   * job to run the conversion, unless there already is an async conversion running for\n   * this table. In that case, it will queue up the provided snapshot to be run after\n   * the existing job completes.\n   * Note that if there is another snapshot already queued, the previous snapshot will get\n   * removed from the wait queue. Only one snapshot is queued at any point of time.\n   *\n   */\n  override def enqueueSnapshotForConversion(\n      snapshotToConvert: Snapshot,\n      txn: CommittedTransaction): Unit = {\n    if (!UniversalFormat.hudiEnabled(snapshotToConvert.metadata)) {\n      return\n    }\n    val log = snapshotToConvert.deltaLog\n    // Replace any previously queued snapshot\n    val previouslyQueued = standbyConversion.getAndSet((snapshotToConvert, txn))\n    asyncThreadLock.synchronized {\n      if (!asyncConverterThreadActive) {\n        val threadName = HudiConverterHook.ASYNC_HUDI_CONVERTER_THREAD_NAME +\n          s\" [id=${snapshotToConvert.metadata.id}]\"\n        val asyncConverterThread: Thread = new Thread(threadName) {\n          setDaemon(true)\n\n          override def run(): Unit =\n              try {\n                var snapshotAndTxn = getNextSnapshot\n                  while (snapshotAndTxn != null) {\n                    val snapshotVal = snapshotAndTxn._1\n                    val prevTxn = snapshotAndTxn._2\n                    try {\n                      logInfo(log\"Converting Delta table [path=\" +\n                        log\"${MDC(DeltaLogKeys.PATH, log.logPath)}, \" +\n                        log\"tableId=${MDC(DeltaLogKeys.TABLE_ID, log.unsafeVolatileTableId)}, \" +\n                        log\"version=${MDC(DeltaLogKeys.VERSION, snapshotVal.version)}] into Hudi\")\n                      convertSnapshot(snapshotVal, prevTxn)\n                    } catch {\n                      case NonFatal(e) =>\n                        logWarning(log\"Error when writing Hudi metadata asynchronously\", e)\n                        recordDeltaEvent(\n                          log,\n                          \"delta.hudi.conversion.async.error\",\n                          data = Map(\n                            \"exception\" -> ExceptionUtils.getMessage(e),\n                            \"stackTrace\" -> ExceptionUtils.getStackTrace(e)\n                          )\n                        )\n                    }\n                    currentConversion.set(null)\n                    // Pick next snapshot to convert if there's a new one\n                    snapshotAndTxn = getNextSnapshot\n                  }\n              } finally {\n                // shuttingdown thread\n                asyncThreadLock.synchronized {\n                  asyncConverterThreadActive = false\n                }\n              }\n\n          // Get a snapshot to convert from the hudiQueue. Sets the queue to null after.\n          private def getNextSnapshot: (Snapshot, CommittedTransaction) =\n            asyncThreadLock.synchronized {\n              val potentialSnapshotAndTxn = standbyConversion.get()\n              currentConversion.set(potentialSnapshotAndTxn)\n              standbyConversion.compareAndSet(potentialSnapshotAndTxn, null)\n              if (potentialSnapshotAndTxn == null) {\n                asyncConverterThreadActive = false\n              }\n              potentialSnapshotAndTxn\n            }\n        }\n        asyncConverterThread.start()\n        asyncConverterThreadActive = true\n      }\n    }\n\n    // If there already was a snapshot waiting to be converted, log that snapshot info.\n    if (previouslyQueued != null) {\n      recordDeltaEvent(\n        snapshotToConvert.deltaLog,\n        \"delta.hudi.conversion.async.backlog\",\n        data = Map(\n          \"newVersion\" -> snapshotToConvert.version,\n          \"replacedVersion\" -> previouslyQueued._1.version)\n      )\n    }\n  }\n\n  /**\n   * Convert the specified snapshot into Hudi for the given catalogTable\n   * @param snapshotToConvert the snapshot that needs to be converted to Hudi\n   * @param catalogTable the catalogTable this conversion targets.\n   * @return Converted Delta version and commit timestamp\n   */\n  override def convertSnapshot(\n      snapshotToConvert: Snapshot, catalogTable: CatalogTable): Option[(Long, Long)] = {\n    if (!UniversalFormat.hudiEnabled(snapshotToConvert.metadata)) {\n      return None\n    }\n    convertSnapshot(snapshotToConvert, None, Some(catalogTable))\n  }\n\n  /**\n   * Convert the specified snapshot into Hudi when performing an OptimisticTransaction\n   * on a delta table.\n   * @param snapshotToConvert the snapshot that needs to be converted to Hudi\n   * @param txn               the transaction that triggers the conversion. It must\n   *                          contain the catalogTable this conversion targets.\n   * @return Converted Delta version and commit timestamp\n   */\n  override def convertSnapshot(\n      snapshotToConvert: Snapshot, txn: CommittedTransaction): Option[(Long, Long)] = {\n    if (!UniversalFormat.hudiEnabled(snapshotToConvert.metadata)) {\n      return None\n    }\n    convertSnapshot(snapshotToConvert, Some(txn), txn.catalogTable)\n  }\n\n  /**\n   * Convert the specified snapshot into Hudi. NOTE: This operation is blocking. Call\n   * enqueueSnapshotForConversion to run the operation asynchronously.\n   * @param snapshotToConvert the snapshot that needs to be converted to Hudi\n   * @param txnOpt the OptimisticTransaction that created snapshotToConvert.\n   *            Used as a hint to avoid recomputing old metadata.\n   * @param catalogTable the catalogTable this conversion targets\n   * @return Converted Delta version and commit timestamp\n   */\n  private def convertSnapshot(\n      snapshotToConvert: Snapshot,\n      txnOpt: Option[CommittedTransaction],\n      catalogTable: Option[CatalogTable]): Option[(Long, Long)] =\n      recordFrameProfile(\"Delta\", \"HudiConverter.convertSnapshot\") {\n    val log = snapshotToConvert.deltaLog\n    val metaClient = loadTableMetaClient(\n      snapshotToConvert.deltaLog.dataPath.toString,\n      catalogTable.flatMap(ct => Option(ct.identifier.table)),\n      snapshotToConvert.metadata.partitionColumns,\n      new HadoopStorageConfiguration(log.newDeltaHadoopConf()))\n    val lastDeltaVersionConverted: Option[Long] = loadLastDeltaVersionConverted(metaClient)\n    val maxCommitsToConvert =\n      spark.sessionState.conf.getConf(DeltaSQLConf.HUDI_MAX_COMMITS_TO_CONVERT)\n\n    // Nth to convert\n    if (lastDeltaVersionConverted.exists(_ == snapshotToConvert.version)) {\n      return None\n    }\n\n    // Get the most recently converted delta snapshot, if applicable\n    val prevConvertedSnapshotOpt = (lastDeltaVersionConverted, txnOpt) match {\n      case (Some(version), Some(txn)) if version == txn.readSnapshot.version =>\n        Some(txn.readSnapshot)\n      // Check how long it has been since we last converted to Hudi. If outside the threshold,\n      // fall back to state reconstruction to get the actions, to protect driver from OOMing.\n      case (Some(version), _) if snapshotToConvert.version - version <= maxCommitsToConvert =>\n        try {\n          // TODO: We can optimize this by providing a checkpointHint to getSnapshotAt. Check if\n          //  txn.snapshot.version < version. If true, use txn.snapshot's checkpoint as a hint.\n          Some(log.getSnapshotAt(version, catalogTableOpt = catalogTable))\n        } catch {\n          // If we can't load the file since the last time Hudi was converted, it's likely that\n          // the commit file expired. Treat this like a new Hudi table conversion.\n          case _: DeltaFileNotFoundException => None\n        }\n      case (_, _) => None\n    }\n\n    val hudiTxn = new HudiConversionTransaction(log.newDeltaHadoopConf(),\n      snapshotToConvert, metaClient, lastDeltaVersionConverted)\n\n    // Write out the actions taken since the last conversion (or since table creation).\n    // This is done in batches, with each batch corresponding either to one delta file,\n    // or to the specified batch size.\n    val actionBatchSize =\n      spark.sessionState.conf.getConf(DeltaSQLConf.HUDI_MAX_COMMITS_TO_CONVERT)\n    prevConvertedSnapshotOpt match {\n      case Some(prevSnapshot) =>\n        // Read the actions directly from the delta json files.\n        // TODO: Run this as a spark job on executors\n        val deltaFiles = DeltaFileProviderUtils.getDeltaFilesInVersionRange(\n          spark = spark,\n          deltaLog = log,\n          startVersion = prevSnapshot.version + 1,\n          endVersion = snapshotToConvert.version,\n          catalogTableOpt = catalogTable)\n\n        recordDeltaEvent(\n          snapshotToConvert.deltaLog,\n          \"delta.hudi.conversion.deltaCommitRange\",\n          data = Map(\n            \"fromVersion\" -> (prevSnapshot.version + 1),\n            \"toVersion\" -> snapshotToConvert.version,\n            \"numDeltaFiles\" -> deltaFiles.length\n          )\n        )\n\n        val actionsToConvert = DeltaFileProviderUtils.parallelReadAndParseDeltaFilesAsIterator(\n          log, spark, deltaFiles)\n        actionsToConvert.foreach { actionsIter =>\n          try {\n            actionsIter.grouped(actionBatchSize).foreach { actionStrs =>\n              runHudiConversionForActions(\n                hudiTxn,\n                actionStrs.map(Action.fromJson))\n            }\n          } finally {\n            actionsIter.close()\n          }\n        }\n\n      // If we don't have a snapshot of the last converted version, get all the table addFiles\n      // (via state reconstruction).\n      case None =>\n        val actionsToConvert = snapshotToConvert.allFiles.toLocalIterator().asScala\n\n        recordDeltaEvent(\n          snapshotToConvert.deltaLog,\n          \"delta.hudi.conversion.batch\",\n          data = Map(\n            \"version\" -> snapshotToConvert.version,\n            \"numDeltaFiles\" -> snapshotToConvert.numOfFiles\n          )\n        )\n\n        actionsToConvert.grouped(actionBatchSize)\n          .foreach { actions =>\n            runHudiConversionForActions(hudiTxn, actions)\n          }\n    }\n    hudiTxn.commit()\n    Some(snapshotToConvert.version, snapshotToConvert.timestamp)\n  }\n\n  def loadLastDeltaVersionConverted(snapshot: Snapshot, table: CatalogTable): Option[Long] = {\n    val metaClient = loadTableMetaClient(snapshot.deltaLog.dataPath.toString,\n      Option.apply(table.identifier.table), snapshot.metadata.partitionColumns,\n      new HadoopStorageConfiguration(snapshot.deltaLog.newDeltaHadoopConf()))\n    loadLastDeltaVersionConverted(metaClient)\n  }\n\n  private def loadLastDeltaVersionConverted(metaClient: HoodieTableMetaClient): Option[Long] = {\n    val lastCompletedCommit = metaClient.getCommitsTimeline.filterCompletedInstants.lastInstant\n    if (!lastCompletedCommit.isPresent) {\n      return None\n    }\n    val extraMetadata = parseCommitExtraMetadata(lastCompletedCommit.get(), metaClient)\n    extraMetadata.get(HudiConverter.DELTA_VERSION_PROPERTY).map(_.toLong)\n  }\n\n  private def parseCommitExtraMetadata(instant: HoodieInstant,\n                                       metaClient: HoodieTableMetaClient): Map[String, String] = {\n    try {\n      if (instant.getAction == HoodieTimeline.REPLACE_COMMIT_ACTION) {\n        HoodieReplaceCommitMetadata.fromBytes(\n          metaClient.getActiveTimeline.getInstantDetails(instant).get,\n          classOf[HoodieReplaceCommitMetadata]).getExtraMetadata.asScala.toMap\n      } else {\n        HoodieCommitMetadata.fromBytes(\n          metaClient.getActiveTimeline.getInstantDetails(instant).get,\n          classOf[HoodieCommitMetadata]).getExtraMetadata.asScala.toMap\n      }\n    } catch {\n      case ex: IOException =>\n        throw new UncheckedIOException(\"Unable to read Hudi commit metadata\", ex)\n    }\n  }\n\n  private[delta] def runHudiConversionForActions(\n      hudiTxn: HudiConversionTransaction,\n      actionsToCommit: Seq[Action]): Unit = {\n    hudiTxn.setCommitFileUpdates(actionsToCommit)\n  }\n}\n"
  },
  {
    "path": "hudi/src/main/scala/org/apache/spark/sql/delta/hudi/HudiSchemaUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hudi\n\nimport java.util\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.avro.{LogicalTypes, Schema}\n\nimport org.apache.spark.sql.types._\n\nobject HudiSchemaUtils extends DeltaLogging {\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n  def convertDeltaSchemaToHudiSchema(deltaSchema: StructType): Schema = {\n    /**\n     * Recursively (i.e. for all nested elements) transforms the delta DataType `elem` into its\n     * corresponding Avro type.\n     */\n    def transform[E <: DataType](elem: E, isNullable: Boolean, currentPath: String): Schema =\n    elem match {\n      case StructType(fields) =>\n\n        val avroFields: util.List[Schema.Field] = fields.map(f =>\n          new Schema.Field(\n            f.name,\n            transform(f.dataType, f.nullable, s\"$currentPath.${f.name}\"),\n            f.getComment().orNull)).toList.asJava\n        finalizeSchema(\n          Schema.createRecord(currentPath, null, null, false, avroFields),\n          isNullable)\n\n      case ArrayType(elementType, containsNull) =>\n        finalizeSchema(\n          Schema.createArray(transform(elementType, containsNull, currentPath)),\n          isNullable)\n\n      case MapType(keyType, valueType, valueContainsNull) =>\n        finalizeSchema(\n          Schema.createMap(transform(valueType, valueContainsNull, currentPath)),\n          isNullable)\n\n      case atomicType: AtomicType => convertAtomic(atomicType, isNullable)\n\n      case other =>\n        throw new UnsupportedOperationException(s\"Cannot convert Delta type $other to Hudi\")\n    }\n\n    transform(deltaSchema, false, \"root\")\n  }\n\n  private def finalizeSchema(targetSchema: Schema, isNullable: Boolean): Schema = {\n    if (isNullable) return Schema.createUnion(Schema.create(Schema.Type.NULL), targetSchema)\n    targetSchema\n  }\n\n  private def convertAtomic[E <: DataType](elem: E, isNullable: Boolean) = elem match {\n    case StringType => finalizeSchema(Schema.create(Schema.Type.STRING), isNullable)\n    case LongType => finalizeSchema(Schema.create(Schema.Type.LONG), isNullable)\n    case IntegerType => finalizeSchema(\n      Schema.create(Schema.Type.INT), isNullable)\n    case FloatType => finalizeSchema(Schema.create(Schema.Type.FLOAT), isNullable)\n    case DoubleType => finalizeSchema(Schema.create(Schema.Type.DOUBLE), isNullable)\n    case d: DecimalType => finalizeSchema(LogicalTypes.decimal(d.precision, d.scale)\n      .addToSchema(Schema.create(Schema.Type.BYTES)), isNullable)\n    case BooleanType => finalizeSchema(Schema.create(Schema.Type.BOOLEAN), isNullable)\n    case BinaryType => finalizeSchema(Schema.create(Schema.Type.BYTES), isNullable)\n    case DateType => finalizeSchema(\n      LogicalTypes.date.addToSchema(Schema.create(Schema.Type.INT)), isNullable)\n    case TimestampType => finalizeSchema(\n      LogicalTypes.timestampMicros.addToSchema(Schema.create(Schema.Type.LONG)), isNullable)\n    case _ => throw new UnsupportedOperationException(s\"Could not convert atomic type $elem\")\n  }\n}\n"
  },
  {
    "path": "hudi/src/main/scala/org/apache/spark/sql/delta/hudi/HudiTransactionUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hudi\n\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.hadoop.fs.Path\nimport org.apache.hudi.client.WriteStatus\nimport org.apache.hudi.common.model.{HoodieAvroPayload, HoodieDeltaWriteStat, HoodieTableType, HoodieTimelineTimeZone}\nimport org.apache.hudi.common.table.HoodieTableMetaClient\nimport org.apache.hudi.common.util.ExternalFilePathUtil\nimport org.apache.hudi.exception.TableNotFoundException\nimport org.apache.hudi.storage.StorageConfiguration\n\nobject HudiTransactionUtils extends DeltaLogging {\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n  def convertAddFile(addFile: AddFile,\n                     tablePath: Path,\n                     commitTime: String): WriteStatus = {\n\n    val writeStatus = new WriteStatus\n    val path = addFile.toPath\n    val partitionPath = getPartitionPath(tablePath, path)\n    val fileName = path.getName\n    val fileId = fileName\n    val filePath = if (partitionPath.isEmpty) fileName else partitionPath + \"/\" + fileName\n    writeStatus.setFileId(fileId)\n    writeStatus.setPartitionPath(partitionPath)\n    val writeStat = new HoodieDeltaWriteStat\n    writeStat.setFileId(fileId)\n    writeStat.setPath(\n      ExternalFilePathUtil.appendCommitTimeAndExternalFileMarker(filePath, commitTime))\n    writeStat.setPartitionPath(partitionPath)\n    writeStat.setNumWrites(addFile.numLogicalRecords.getOrElse(0L))\n    writeStat.setTotalWriteBytes(addFile.getFileSize)\n    writeStat.setFileSizeInBytes(addFile.getFileSize)\n    writeStatus.setStat(writeStat)\n\n    writeStatus\n  }\n\n  def getPartitionPath(tableBasePath: Path, filePath: Path): String = {\n    val fileName = filePath.getName\n    val pathStr = filePath.toUri.getPath\n    val tableBasePathStr = tableBasePath.toUri.getPath\n    if (pathStr.contains(tableBasePathStr)) {\n      // input file path is absolute\n      val startIndex = tableBasePath.toUri.getPath.length + 1\n      val endIndex = pathStr.length - fileName.length - 1\n      if (endIndex <= startIndex) \"\"\n      else pathStr.substring(startIndex, endIndex)\n    } else {\n      val lastSlash = pathStr.lastIndexOf(\"/\")\n      if (lastSlash <= 0) \"\"\n      else pathStr.substring(0, pathStr.lastIndexOf(\"/\"))\n    }\n  }\n\n  /**\n   * Loads the meta client for the table at the base path if it exists.\n   * If it does not exist, initializes the Hudi table and returns the meta client.\n   *\n   * @param tableDataPath the path for the table\n   * @param tableName the name of the table\n   * @param partitionFields the fields used for partitioning\n   * @param conf the hadoop configuration\n   * @return {@link HoodieTableMetaClient} for the existing table or that was created\n   */\n  def loadTableMetaClient(tableDataPath: String,\n                          tableName: Option[String],\n                          partitionFields: Seq[String],\n                          conf: StorageConfiguration[_]): HoodieTableMetaClient = {\n    try HoodieTableMetaClient.builder\n      .setBasePath(tableDataPath).setConf(conf)\n      .setLoadActiveTimelineOnLoad(false)\n      .build\n    catch {\n      case ex: TableNotFoundException =>\n        log.debug(\"Hudi table does not exist, creating now.\")\n        if (tableName.isEmpty) {\n          log.warn(\"No name is specified for the table. \"\n            + \"Creating a new Hudi table with a default name: 'table'.\")\n        }\n        initializeHudiTable(tableDataPath, tableName.getOrElse(\"table\"), partitionFields, conf)\n    }\n  }\n\n    /**\n     * Initializes a Hudi table with the provided properties\n     *\n     * @param tableDataPath the base path for the data files in the table\n     * @param tableName the name of the table\n     * @param partitionFields the fields used for partitioning\n     * @param conf the hadoop configuration\n     * @return {@link HoodieTableMetaClient} for the table that was created\n     */\n    private def initializeHudiTable(tableDataPath: String,\n                                    tableName: String,\n                                    partitionFields: Seq[String],\n                                    conf: StorageConfiguration[_]): HoodieTableMetaClient = {\n      val keyGeneratorClass = getKeyGeneratorClass(partitionFields)\n      HoodieTableMetaClient\n        .withPropertyBuilder\n        .setCommitTimezone(HoodieTimelineTimeZone.UTC)\n        .setHiveStylePartitioningEnable(true)\n        .setTableType(HoodieTableType.COPY_ON_WRITE)\n        .setTableName(tableName)\n        .setPayloadClass(classOf[HoodieAvroPayload])\n        .setKeyGeneratorClassProp(keyGeneratorClass)\n        .setPopulateMetaFields(false)\n        .setPartitionFields(partitionFields.mkString(\",\"))\n        .initTable(conf, tableDataPath)\n    }\n\n    private def getKeyGeneratorClass(partitionFields: Seq[String]): String = {\n      if (partitionFields.isEmpty) {\n        \"org.apache.hudi.keygen.NonpartitionedKeyGenerator\"\n      } else if (partitionFields.size > 1) {\n        \"org.apache.hudi.keygen.CustomKeyGenerator\"\n      } else {\n        \"org.apache.hudi.keygen.SimpleKeyGenerator\"\n      }\n    }\n}\n"
  },
  {
    "path": "hudi/src/test/scala/org/apache/spark/sql/delta/hudi/ConvertToHudiSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hudi\n\nimport java.time.Instant\nimport java.util.UUID\nimport java.util.stream.Collectors\n\nimport scala.collection.JavaConverters\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, DeltaUnsupportedOperationException, OptimisticTransaction}\nimport org.apache.spark.sql.delta.DeltaOperations.Truncate\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, Metadata}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.hudi.common.config.HoodieMetadataConfig\nimport org.apache.hudi.common.engine.HoodieLocalEngineContext\nimport org.apache.hudi.common.fs.FSUtils\nimport org.apache.hudi.common.model.HoodieBaseFile\nimport org.apache.hudi.common.table.{HoodieTableMetaClient, TableSchemaResolver}\nimport org.apache.hudi.metadata.HoodieMetadataFileSystemView\nimport org.apache.hudi.storage.StorageConfiguration\nimport org.apache.hudi.storage.hadoop.{HadoopStorageConfiguration, HoodieHadoopStorage}\nimport org.scalatest.concurrent.Eventually\nimport org.scalatest.time.SpanSugar._\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.avro.SchemaConverters\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.ManualClock\n\ntrait HudiTestBase extends QueryTest\n  with Eventually {\n\n  /**\n   * Executes `f` with params (tableId, tempPath).\n   *\n   * We want to use a temp directory in addition to a unique temp table so that when the async\n   * Hudi conversion runs and completes, the parent folder is still removed.\n   */\n  def withTempTableAndDir(f: (String, String) => Unit): Unit\n\n  protected def spark: SparkSession\n\n  def buildHudiMetaClient(testTablePath: String): HoodieTableMetaClient = {\n    // scalastyle:off deltahadoopconfiguration\n    val hadoopConf: Configuration = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    val storageConf : StorageConfiguration[_] = new HadoopStorageConfiguration(hadoopConf)\n    HoodieTableMetaClient.builder\n      .setConf(storageConf).setBasePath(testTablePath)\n      .setLoadActiveTimelineOnLoad(true)\n      .build\n  }\n\n  def verifyFilesAndSchemaMatch(testTableName: String, testTablePath: String): Unit = {\n    eventually(timeout(30.seconds)) {\n      // To avoid requiring Hudi spark dependencies, we first lookup the active base files and then\n      // assert by reading those active base files (parquet) directly\n      // scalastyle:off deltahadoopconfiguration\n      val hadoopConf: Configuration = spark.sessionState.newHadoopConf()\n      // scalastyle:on deltahadoopconfiguration\n      val storageConf : StorageConfiguration[_] = new HadoopStorageConfiguration(hadoopConf)\n      val metaClient: HoodieTableMetaClient = buildHudiMetaClient(testTablePath)\n      val engContext: HoodieLocalEngineContext = new HoodieLocalEngineContext(storageConf)\n      val fsView: HoodieMetadataFileSystemView = new HoodieMetadataFileSystemView(engContext,\n        metaClient, metaClient.getActiveTimeline.getCommitsTimeline.filterCompletedInstants,\n        HoodieMetadataConfig.newBuilder.enable(true).build)\n      val hoodieStorage = new HoodieHadoopStorage(testTablePath, storageConf)\n      val paths = JavaConverters.asScalaBuffer(\n          FSUtils.getAllPartitionPaths(engContext, hoodieStorage, testTablePath, true, false))\n        .flatMap(partition => JavaConverters.asScalaBuffer(fsView.getLatestBaseFiles(partition)\n          .collect(Collectors.toList[HoodieBaseFile])))\n        .map(baseFile => baseFile.getPath).sorted\n      val avroSchema = new TableSchemaResolver(metaClient).getTableAvroSchema\n      val hudiSchemaAsStruct = SchemaConverters.toSqlType(avroSchema).dataType\n        .asInstanceOf[StructType]\n\n      val deltaDF = spark.sql(s\"SELECT * FROM $testTableName\")\n      // Assert file paths are equivalent\n      val expectedFiles = deltaDF.inputFiles.map(path => path.substring(5)).toSeq.sorted\n      assert(paths.equals(expectedFiles),\n        s\"Files do not match.\\nExpected: $expectedFiles\\nActual: $paths\")\n      // Assert schemas are equal\n      val expectedSchema = deltaDF.schema\n\n      assert(hudiSchemaAsStruct.equals(expectedSchema),\n        s\"Schemas do not match.\\nExpected: $expectedSchema\\nActual: $hudiSchemaAsStruct\")\n    }\n  }\n\n  def withDefaultTablePropsInSQLConf(enableInCommitTimestamp: Boolean, f: => Unit): Unit = {\n    withSQLConf(\n      DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\",\n      DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.defaultTablePropertyKey -> \"hudi\",\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey ->\n        enableInCommitTimestamp.toString\n    ) { f }\n  }\n\n  protected def startTxnWithManualLogCleanup(log: DeltaLog): OptimisticTransaction = {\n    val txn = log.startTransaction()\n    // This will pick up `spark.databricks.delta.properties.defaults.enableExpiredLogCleanup` to\n    // disable log cleanup.\n    txn.updateMetadata(Metadata())\n    txn\n  }\n\n  def verifyNumHudiCommits(count: Integer, testTablePath: String): Unit = {\n    eventually(timeout(30.seconds)) {\n      val metaClient: HoodieTableMetaClient = buildHudiMetaClient(testTablePath)\n      val activeCommits = metaClient.getActiveTimeline.getCommitsTimeline\n        .filterCompletedInstants.countInstants\n      val archivedCommits = metaClient.getArchivedTimeline.getCommitsTimeline\n        .filterCompletedInstants.countInstants\n      assert(activeCommits + archivedCommits == count)\n    }\n  }\n}\n\ntrait ConvertToHudiTestBase extends HudiTestBase {\n  test(\"basic test - managed table created with SQL\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE $testTableName (ID INT) USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $testTableName VALUES (123)\")\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  test(\"basic test - catalog table created with DataFrame\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      withDefaultTablePropsInSQLConf(false, {\n        spark.range(10).write.format(\"delta\")\n          .option(\"path\", testTablePath)\n          .saveAsTable(testTableName)\n      })\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n      withDefaultTablePropsInSQLConf(false, {\n        spark.range(10, 20, 1)\n          .write.format(\"delta\").mode(\"append\")\n          .save(testTablePath)\n      })\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  for (isPartitioned <- Seq(true, false)) {\n    test(s\"validate multiple commits (partitioned = $isPartitioned)\") {\n      withTempTableAndDir { case (testTableName, testTablePath) =>\n        spark.sql(\n          s\"\"\"CREATE TABLE $testTableName (col1 INT, col2 STRING, col3 STRING) USING DELTA\n             |${if (isPartitioned) \"PARTITIONED BY (col3)\" else \"\"}\n             |LOCATION '$testTablePath'\n             |TBLPROPERTIES (\n             |  'delta.universalFormat.enabledFormats' = 'hudi'\n             |)\"\"\".stripMargin)\n        // perform some inserts\n        spark.sql(s\"INSERT INTO $testTableName VALUES (1, 'instant1', 'a'), (2, 'instant1', 'a')\")\n        verifyFilesAndSchemaMatch(testTableName, testTablePath)\n\n        spark.sql(s\"INSERT INTO `$testTableName` VALUES (3, 'instant2', 'b'), (4, 'instant2', 'b')\")\n        verifyFilesAndSchemaMatch(testTableName, testTablePath)\n\n        spark.sql(s\"INSERT INTO `$testTableName` VALUES (5, 'instant3', 'b'), (6, 'instant3', 'a')\")\n        verifyFilesAndSchemaMatch(testTableName, testTablePath)\n\n        // update the data from the first instant\n        spark.sql(s\"UPDATE `$testTableName` SET col2 = 'instant4' WHERE col2 = 'instant1'\")\n        verifyFilesAndSchemaMatch(testTableName, testTablePath)\n\n        // delete a single row\n        spark.sql(s\"DELETE FROM `$testTableName` WHERE col1 = 5\")\n        verifyFilesAndSchemaMatch(testTableName, testTablePath)\n      }\n    }\n  }\n\n  test(\"Enabling Delete Vector Throws Exception\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      intercept[DeltaUnsupportedOperationException] {\n        spark.sql(\n          s\"\"\"CREATE TABLE `$testTableName` (col1 INT, col2 STRING) USING DELTA\n             |LOCATION '$testTablePath'\n             |TBLPROPERTIES (\n             |  'delta.universalFormat.enabledFormats' = 'hudi',\n             |  'delta.enableDeletionVectors' = true\n             |)\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"Enabling Delete Vector After Hudi Enabled Already Throws Exception\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName` (col1 INT, col2 STRING) USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      intercept[DeltaUnsupportedOperationException] {\n        spark.sql(\n          s\"\"\"ALTER TABLE `$testTableName` SET TBLPROPERTIES (\n             |  'delta.enableDeletionVectors' = true\n             |)\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(s\"Conversion behavior for lists\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName` (col1 ARRAY<INT>) USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO `$testTableName` VALUES (array(1, 2, 3))\")\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  test(s\"Conversion behavior for lists of structs\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName`\n           |(col1 ARRAY<STRUCT<field1: INT, field2: STRING>>) USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO `$testTableName` \" +\n        s\"VALUES (array(named_struct('field1', 1, 'field2', 'hello'), \" +\n        s\"named_struct('field1', 2, 'field2', 'world')))\")\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  test(s\"Conversion behavior for lists of lists\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName`\n           |(col1 ARRAY<ARRAY<INT>>) USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO `$testTableName` \" +\n        s\"VALUES (array(array(1, 2, 3), array(4, 5, 6)))\")\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  test(s\"Conversion behavior for maps\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName` (col1 MAP<STRING, INT>) USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      spark.sql(\n        s\"INSERT INTO `$testTableName` VALUES (map('a', 1, 'b', 2, 'c', 3))\"\n      )\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  test(s\"Conversion behavior for nested structs\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName` (col1 STRUCT<field1: INT, field2: STRING,\n           |field3: STRUCT<field4: INT, field5: INT, field6: STRING>>)\n           |USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      spark.sql(\n        s\"INSERT INTO `$testTableName` VALUES (named_struct('field1', 1, 'field2', 'hello', \" +\n          \"'field3', named_struct('field4', 2, 'field5', 3, 'field6', 'world')))\"\n      )\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  test(\"validate Hudi timeline archival and cleaning\") {\n    withTempTableAndDir { case (_, testTablePath) =>\n      val testOp = Truncate()\n      withDefaultTablePropsInSQLConf(true, {\n        val startTime = System.currentTimeMillis() - 12 * 24 * 60 * 60 * 1000\n        val clock = new ManualClock(startTime)\n        val actualTestStartTime = System.currentTimeMillis()\n        val log = DeltaLog.forTable(spark, new Path(testTablePath), clock)\n        (1 to 20).foreach { i =>\n          val txn = if (i == 1) startTxnWithManualLogCleanup(log) else log.startTransaction()\n          val file = AddFile(i.toString + \".parquet\", Map.empty, 1, 1, true) :: Nil\n          val delete: Seq[Action] = if (i > 1) {\n            val timestamp = startTime + (System.currentTimeMillis() - actualTestStartTime)\n            val prevFile = AddFile((i - 1).toString + \".parquet\", Map.empty, 1, 1, true)\n            prevFile.removeWithTimestamp(timestamp) :: Nil\n          } else {\n            Nil\n          }\n          txn.commit(delete ++ file, testOp)\n          clock.advance(12.hours.toMillis)\n          // wait for each Hudi sync to complete\n          verifyNumHudiCommits(i, testTablePath)\n        }\n\n        val metaClient: HoodieTableMetaClient = HoodieTableMetaClient.builder\n          .setConf(new HadoopStorageConfiguration(log.newDeltaHadoopConf()))\n          .setBasePath(log.dataPath.toString)\n          .setLoadActiveTimelineOnLoad(true)\n          .build\n        // Timeline requires a clean commit for proper removal of entries from the Hudi\n        // Metadata Table\n        assert(metaClient.getActiveTimeline.getCleanerTimeline.countInstants() == 1,\n          \"Cleaner timeline should have 1 instant\")\n        // Older commits should move from active to archive timeline\n        // TODO Fix the flaky tests\n        /*\n        assert(metaClient.getArchivedTimeline.getCommitsTimeline.filterInflights.countInstants == 2,\n          \"Archived timeline should have 2 instants\")\n\n         */\n      })\n    }\n  }\n\n  test(\"validate various data types\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      spark.sql(\n        s\"\"\"CREATE TABLE `$testTableName` (col1 BIGINT, col2 BOOLEAN, col3 DATE,\n           | col4 DOUBLE, col5 FLOAT, col6 INT, col7 STRING, col8 TIMESTAMP,\n           | col9 BINARY, col10 DECIMAL(5, 2),\n           | col11 STRUCT<field1: INT, field2: STRING,\n           | field3: STRUCT<field4: INT, field5: INT, field6: STRING>>)\n           | USING DELTA\n           |LOCATION '$testTablePath'\n           |TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'hudi'\n           |)\"\"\".stripMargin)\n      val nowSeconds = Instant.now().getEpochSecond\n      spark.sql(s\"INSERT INTO `$testTableName` VALUES (123, true, \"\n        + s\"date(from_unixtime($nowSeconds)), 32.1, 1.23, 456, 'hello world', \"\n        + s\"timestamp(from_unixtime($nowSeconds)), X'1ABF', -999.99,\"\n        + s\"STRUCT(1, 'hello', STRUCT(2, 3, 'world')))\")\n      verifyFilesAndSchemaMatch(testTableName, testTablePath)\n    }\n  }\n\n  for (invalidType <- Seq(\"SMALLINT\", \"TINYINT\", \"TIMESTAMP_NTZ\", \"VOID\")) {\n    test(s\"Unsupported Type $invalidType Throws Exception\") {\n      withTempTableAndDir { case (testTableName, testTablePath) =>\n        intercept[DeltaUnsupportedOperationException] {\n          spark.sql(\n            s\"\"\"CREATE TABLE `$testTableName` (col1 $invalidType) USING DELTA\n               |LOCATION '$testTablePath'\n               |TBLPROPERTIES (\n               |  'delta.universalFormat.enabledFormats' = 'hudi'\n               |)\"\"\".stripMargin)\n        }\n      }\n    }\n  }\n\n\n  test(\"all batches of actions are converted\") {\n    withTempTableAndDir { case (testTableName, testTablePath) =>\n      withSQLConf(\n        DeltaSQLConf.HUDI_MAX_COMMITS_TO_CONVERT.key -> \"3\"\n      ) {\n        spark.sql(\n          s\"\"\"CREATE TABLE `$testTableName` (col1 INT)\n             | USING DELTA\n             |LOCATION '$testTablePath'\"\"\".stripMargin)\n        for (i <- 1 to 10) {\n          spark.sql(s\"INSERT INTO `$testTableName` VALUES ($i)\")\n        }\n        spark.sql(\n          s\"\"\"ALTER TABLE `$testTableName` SET TBLPROPERTIES (\n             |  'delta.universalFormat.enabledFormats' = 'hudi'\n             |)\"\"\".stripMargin)\n        verifyFilesAndSchemaMatch(testTableName, testTablePath)\n      }\n    }\n  }\n}\n\nclass ConvertToHudiSuite\n  extends ConvertToHudiTestBase {\n\n  private var _sparkSession: SparkSession = null\n\n  override def spark: SparkSession = _sparkSession\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    _sparkSession = createSparkSession()\n    _sparkSession.conf.set(\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey, \"true\")\n  }\n\n  def createSparkSession(): SparkSession = {\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"UniformSession\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n  }\n\n  override def withTempTableAndDir(f: (String, String) => Unit): Unit = {\n    val tableId = s\"testTable${UUID.randomUUID()}\".replace(\"-\", \"_\")\n    withTempDir { externalLocation =>\n      val tablePath = new Path(externalLocation.toString, \"table\")\n      f(tableId, s\"$tablePath\")\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/integration_tests/iceberg_converter.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql.functions import col\nfrom delta.tables import DeltaTable\nimport shutil\nimport random\n\ntestRoot = \"/tmp/delta-iceberg-converter/\"\nwarehousePath = testRoot + \"iceberg_tables\"\nshutil.rmtree(testRoot, ignore_errors=True)\n\n# we need to set the following configs\nspark = SparkSession.builder \\\n    .appName(\"delta-iceberg-converter\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(\"spark.sql.catalog.local\", \"org.apache.iceberg.spark.SparkCatalog\") \\\n    .config(\"spark.sql.catalog.local.type\", \"hadoop\") \\\n    .config(\"spark.sql.catalog.local.warehouse\", warehousePath) \\\n    .getOrCreate()\n\ntable = \"local.db.table\"\ntablePath = \"file://\" + warehousePath + \"/db/table\"\n\ntry:\n    print(\"Creating Iceberg table with partitions...\")\n    spark.sql(\n        \"CREATE TABLE {} (id BIGINT, data STRING) USING ICEBERG PARTITIONED BY (data)\".format(table))\n    spark.sql(\"INSERT INTO {} VALUES (1, 'a'), (2, 'b')\".format(table))\n    spark.sql(\"INSERT INTO {} VALUES (3, 'c')\".format(table))\n\n    print(\"Converting Iceberg table to Delta table...\")\n    spark.sql(\"CONVERT TO DELTA iceberg.`{}`\".format(tablePath))\n\n    print(\"Reading from converted Delta table...\")\n    spark.read.format(\"delta\").load(tablePath).show()\n\n    print(\"Modifying the converted table...\")\n    spark.sql(\"INSERT INTO delta.`{}` VALUES (4, 'd')\".format(tablePath))\n\n    print(\"Reading the final Delta table...\")\n    spark.read.format(\"delta\").load(tablePath).show()\n\n    print(\"Create an external catalog table using Delta...\")\n    spark.sql(\"CREATE TABLE converted_delta_table USING delta LOCATION '{}'\".format(tablePath))\n\n    print(\"Read from the catalog table...\")\n    spark.read.table(\"converted_delta_table\").show()\nfinally:\n    # cleanup\n    shutil.rmtree(testRoot, ignore_errors=True)\n"
  },
  {
    "path": "iceberg/src/main/java/org/apache/spark/sql/delta/serverSidePlanning/FixedGcsAccessTokenProvider.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning;\n\nimport com.google.cloud.hadoop.util.AccessTokenProvider;\nimport org.apache.hadoop.conf.Configuration;\n\n/**\n * A custom AccessTokenProvider used for server-side planning with temporary GCS credentials from\n * credential vending services.\n *\n * Configuration keys:\n * - fs.gs.auth.access.token: The OAuth2 access token\n * - fs.gs.auth.access.token.expiration.ms: Optional expiration timestamp in epoch milliseconds\n *\n * If no expiration is provided, defaults to 1 hour from current time. This default does not\n * guarantee that the token will be valid for the entire duration of the query. If the actual token expires earlier, queries will fail.\n */\npublic class FixedGcsAccessTokenProvider implements AccessTokenProvider {\n\n  private static final String CONFIG_TOKEN = \"fs.gs.auth.access.token\";\n  private static final String CONFIG_EXPIRATION_MS = \"fs.gs.auth.access.token.expiration.ms\";\n  private static final long FALLBACK_EXPIRATION_MS = 3600_000L;\n\n  private Configuration conf;\n\n  @Override\n  public AccessTokenProvider.AccessToken getAccessToken() {\n    String token = conf.get(CONFIG_TOKEN);\n    if (token == null || token.isEmpty()) {\n      throw new RuntimeException(\"Missing GCS access token in configuration: \" + CONFIG_TOKEN);\n    }\n\n    // Read expiration timestamp from config, or use fallback\n    long expirationMs;\n    String expirationStr = conf.get(CONFIG_EXPIRATION_MS);\n    if (expirationStr != null && !expirationStr.isEmpty()) {\n      try {\n        expirationMs = Long.parseLong(expirationStr);\n      } catch (NumberFormatException e) {\n        // If parsing fails, use fallback\n        expirationMs = System.currentTimeMillis() + FALLBACK_EXPIRATION_MS;\n      }\n    } else {\n      // No expiration provided, use fallback.\n      expirationMs = System.currentTimeMillis() + FALLBACK_EXPIRATION_MS;\n    }\n\n    return new AccessTokenProvider.AccessToken(token, expirationMs);\n  }\n\n  @Override\n  public void refresh() {\n    // Refresh is not supported, token is static and expires.\n  }\n\n  @Override\n  public void setConf(Configuration conf) {\n    this.conf = conf;\n  }\n\n  @Override\n  public Configuration getConf() {\n    return conf;\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/iceberg/transforms/IcebergPartitionUtil.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage shadedForDelta.org.apache.iceberg.transforms\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping\nimport org.apache.spark.sql.delta.commands.convert.TypeToSparkTypeWithCustomCast\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.util.{DateFormatter, TimestampFormatter}\nimport shadedForDelta.org.apache.iceberg.{PartitionField, PartitionSpec, Schema, StructLike}\nimport shadedForDelta.org.apache.iceberg.types.Type.TypeID\nimport shadedForDelta.org.apache.iceberg.types.Types\nimport shadedForDelta.org.apache.iceberg.types.TypeUtil\n\nimport org.apache.spark.sql.types.{DateType, IntegerType, MetadataBuilder, StringType, StructField}\n\n/**\n * Utils to translate Iceberg's partition expressions to Delta generated column expressions.\n */\nobject IcebergPartitionUtil {\n\n  // scalastyle:off line.size.limit\n  /**\n   * Convert the partition values stored in Iceberg metadata to string values, which we will\n   * directly use in the partitionValues field of AddFiles. Here is how we generate the string\n   * value from the Iceberg stored partition value for each of the transforms:\n   *\n   * Identity\n   *   - Iceberg source code: https://github.com/apache/iceberg/blob/4c98a0f6408d4ccd0d47b076b2f7743d836d28ec/api/src/main/java/org/apache/iceberg/transforms/Identity.java\n   *   - Source column type: any\n   *   - Stored partition value type: same as source type\n   *   - String value generation: for timestamp and date, use our Spark formatter; other types use toString\n   *\n   * Timestamps (year, month, day, hour)\n   *  - Iceberg source code: https://github.com/apache/iceberg/blob/4c98a0f6408d4ccd0d47b076b2f7743d836d28ec/api/src/main/java/org/apache/iceberg/transforms/Timestamps.java\n   *  - Source column type: timestamp\n   *  - Stored partition value type: integer\n   *  - String value generation: use Iceberg's Timestamps.toHumanString (which uses yyyy-MM-dd-HH format)\n   *\n   * Dates (year, month, day)\n   *  - Iceberg source code: https://github.com/apache/iceberg/blob/4c98a0f6408d4ccd0d47b076b2f7743d836d28ec/api/src/main/java/org/apache/iceberg/transforms/Dates.java\n   *  - Source column type: date\n   *  - Stored partition value type: integer\n   *  - String value generation: use Iceberg's Dates.toHumanString (which uses yyyy-MM-dd format)\n   *\n   * Truncate\n   *  - Iceberg source code: https://github.com/apache/iceberg/blob/4c98a0f6408d4ccd0d47b076b2f7743d836d28ec/api/src/main/java/org/apache/iceberg/transforms/Truncate.java\n   *  - Source column type: string, long and int\n   *  - Stored partition value type: string, long and int\n   *  - String value generation: directly use toString\n   */\n  // scalastyle:on line.size.limit\n  def partitionValueToString(\n      partField: PartitionField,\n      partValue: Object,\n      schema: Schema,\n      dateFormatter: DateFormatter,\n      timestampFormatter: TimestampFormatter): String = {\n    if (partValue == null) return null\n    partField.transform() match {\n      case _: Identity[_] =>\n        // Identity transform\n        // We use our own date and timestamp formatter for date and timestamp types, while simply\n        // use toString for other input types.\n        val sourceField = schema.findField(partField.sourceId())\n        val sourceType = sourceField.`type`()\n        if (sourceType.typeId() == TypeID.DATE) {\n          // convert epoch days to Spark date formatted string\n          dateFormatter.format(partValue.asInstanceOf[Int])\n        } else if (sourceType.typeId == TypeID.TIMESTAMP) {\n          // convert timestamps to Spark timestamp formatted string\n          timestampFormatter.format(partValue.asInstanceOf[Long])\n        } else {\n          // all other types can directly toString\n          partValue.toString\n        }\n      case ts: Timestamps =>\n        // Matches all transforms on Timestamp input type: YEAR, MONTH, DAY, HOUR\n        // We directly use Iceberg's toHumanString(), which takes a timestamp type source column and\n        // generates the partition value in the string format as follows:\n        //  - YEAR: yyyy\n        //  - MONTH: yyyy-MM\n        //  - DAY: yyyy-MM-dd\n        //  - HOUR: yyyy-MM-dd-HH\n        ts.toHumanString(Types.TimestampType.withoutZone(), partValue.asInstanceOf[Int])\n      case dt: Dates =>\n        // Matches all transform on Date input type: YEAR, MONTH, DAY\n        // We directly use Iceberg's toHumanString(), which takes a date type source column and\n        // generates the partition value in the string format as follows:\n        //  - YEAR: yyyy\n        //  - MONTH: yyyy-MM\n        //  - DAY: yyyy-MM-dd\n        dt.toHumanString(Types.DateType.get(), partValue.asInstanceOf[Int])\n      case _: Truncate[_] =>\n        // Truncate transform\n        // While Iceberg Truncate transform supports multiple input types, our converter\n        // only supports string and block all other input types. So simply toString suffices.\n        partValue.toString\n      case other =>\n        throw new UnsupportedOperationException(\n          s\"unsupported partition transform expression when converting to Delta: $other\")\n    }\n  }\n\n  def getPartitionFields(\n      partSpec: PartitionSpec, schema: Schema, castTimeType: Boolean): Seq[StructField] = {\n    // Skip removed partition fields due to partition evolution.\n    partSpec.fields.asScala.toSeq.collect {\n      case partField if !partField.transform().isInstanceOf[VoidTransform[_]] &&\n          !partField.transform().isInstanceOf[Bucket[_]] =>\n        val sourceColumnName = schema.findColumnName(partField.sourceId())\n        val sourceField = schema.findField(partField.sourceId())\n        val sourceType = sourceField.`type`()\n\n        val metadataBuilder = new MetadataBuilder()\n\n        // TODO: Support truncate[Decimal] in partition\n        val (transformExpr, targetType) = partField.transform() match {\n          // binary partition values are problematic in Delta, so we block converting if the iceberg\n          // table has a binary type partition column\n          case _: Identity[_] if sourceType.typeId() != TypeID.BINARY =>\n            // copy id only for identity transform because source id will be the converted column id\n            // ids for other columns will be assigned later automatically during schema evolution\n            metadataBuilder\n              .putLong(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY, sourceField.fieldId())\n            (\"\", TypeUtil.visit(sourceType, new TypeToSparkTypeWithCustomCast(castTimeType)))\n\n          case Timestamps.MICROS_TO_YEAR | Dates.YEAR =>\n            (s\"year($sourceColumnName)\", IntegerType)\n\n          case Timestamps.MICROS_TO_DAY | Dates.DAY =>\n            (s\"cast($sourceColumnName as date)\", DateType)\n\n          case t: Truncate[_] if sourceType.typeId() == TypeID.STRING =>\n            (s\"substring($sourceColumnName, 0, ${t.width()})\", StringType)\n\n          case t: Truncate[_]\n            if sourceType.typeId() == TypeID.LONG || sourceType.typeId() == TypeID.INTEGER =>\n            (icebergNumericTruncateExpression(sourceColumnName, t.width().toLong),\n              TypeUtil.visit(sourceType, new TypeToSparkTypeWithCustomCast(castTimeType)))\n\n          case Timestamps.MICROS_TO_MONTH | Dates.MONTH =>\n            (s\"date_format($sourceColumnName, 'yyyy-MM')\", StringType)\n\n          case Timestamps.MICROS_TO_HOUR =>\n            (s\"date_format($sourceColumnName, 'yyyy-MM-dd-HH')\", StringType)\n\n          case other =>\n            throw new UnsupportedOperationException(\n              s\"Unsupported partition transform expression when converting to Delta: \" +\n                s\"transform: $other, source data type: ${sourceType.typeId()}\")\n        }\n\n        if (transformExpr != \"\") {\n          metadataBuilder.putString(GENERATION_EXPRESSION_METADATA_KEY, transformExpr)\n        }\n\n        Option(sourceField.doc()).foreach { comment =>\n          metadataBuilder.putString(\"comment\", comment)\n        }\n\n        val metadata = metadataBuilder.build()\n\n        StructField(partField.name(),\n          targetType,\n          nullable = sourceField.isOptional(),\n          metadata = metadata)\n    }\n  }\n\n  /**\n   * Returns the iceberg transform function of truncate[Integer] and truncate[Long] as an\n   * expression string, please check the iceberg documents for more details:\n   *\n   *    https://iceberg.apache.org/spec/#truncate-transform-details\n   *\n   * TODO: make this partition expression optimizable.\n   */\n  private def icebergNumericTruncateExpression(colName: String, width: Long): String =\n    s\"$colName - (($colName % $width) + $width) % $width\"\n\n  def hasBucketPartition(partSpec: PartitionSpec): Boolean = {\n    partSpec.fields.asScala.toSeq.exists(spec => spec.transform().isInstanceOf[Bucket[_]])\n  }\n\n  // return true if the partition spec has a partition that is not a bucket partition\n  def hasNonBucketPartition(partSpec: PartitionSpec): Boolean = {\n    partSpec.isPartitioned && partSpec.fields().asScala.exists { field =>\n      !field.transform().isInstanceOf[Bucket[_]]\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/catalyst/analysis/NoSuchProcedureException.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.analysis\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.connector.catalog.Identifier\n\nclass NoSuchProcedureException(ident: Identifier)\n  extends AnalysisException(\"Procedure \" + ident + \" not found\")\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/IcebergFileManifest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaLog, SerializableFileStatus, Snapshot => DeltaSnapshot}\nimport org.apache.spark.sql.delta.DeltaErrors.cloneFromIcebergSourceWithPartitionEvolution\nimport org.apache.spark.sql.delta.commands.convert.IcebergTable.ERR_MULTIPLE_PARTITION_SPECS\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\nimport shadedForDelta.org.apache.iceberg.{BaseTable, DataFile, DataFiles, DeleteFile, FileContent, FileFormat, ManifestContent, ManifestFile, ManifestFiles, PartitionData, PartitionSpec, RowLevelOperationMode, Schema, StructLike, Table, TableProperties}\nimport shadedForDelta.org.apache.iceberg.transforms.IcebergPartitionUtil\nimport shadedForDelta.org.apache.iceberg.types.Type.TypeID\n\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.types.StructType\n\n\nclass IcebergFileManifest(\n    spark: SparkSession,\n    table: IcebergTableLike,\n    partitionSchema: StructType,\n    convertStats: Boolean = true) extends ConvertTargetFileManifest {\n\n  // scalastyle:off sparkimplicits\n  import spark.implicits._\n  // scalastyle:on sparkimplicits\n\n  private var fileSparkResults: Option[Dataset[ConvertTargetFile]] = None\n\n  private var _numFiles: Option[Long] = None\n\n  private var _sizeInBytes: Option[Long] = None\n\n  private val specIdsToIfSpecHasNonBucketPartition =\n    table.specs().asScala.map { case (specId, spec) =>\n      specId.toInt -> IcebergPartitionUtil.hasNonBucketPartition(spec)\n  }\n\n  private val partitionEvolutionEnabled =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_PARTITION_EVOLUTION_ENABLED)\n\n  private val statsAllowTypes: Set[TypeID] = IcebergStatsUtils.typesAllowStatsConversion(spark)\n  private val allowPartialStatsConverted: Boolean =\n    spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_CLONE_ICEBERG_ALLOW_PARTIAL_STATS\n    )\n\n  val basePath = table.location()\n\n  override def numFiles: Long = {\n    if (_numFiles.isEmpty) _numFiles = Some(allFiles.count())\n    _numFiles.get\n  }\n\n  override def sizeInBytes: Long = {\n    if (_sizeInBytes.isEmpty) {\n      _sizeInBytes =\n        Some(if (allFiles.isEmpty) 0L else allFiles.map(_.fileStatus.length).reduce(_ + _))\n    }\n    _sizeInBytes.get\n  }\n\n  def allFiles: Dataset[ConvertTargetFile] = {\n    if (fileSparkResults.isEmpty) fileSparkResults = Some(getFileSparkResults())\n    fileSparkResults.get\n  }\n\n  private def getFileSparkResults(): Dataset[ConvertTargetFile] = {\n    val format = table\n      .properties()\n      .getOrDefault(\n        TableProperties.DEFAULT_FILE_FORMAT, TableProperties.DEFAULT_FILE_FORMAT_DEFAULT)\n\n    if (format.toLowerCase() != \"parquet\") {\n      throw new UnsupportedOperationException(\n        s\"Cannot convert Iceberg tables with file format $format. Only parquet is supported.\")\n    }\n\n    if (table.currentSnapshot() == null) {\n      return spark.emptyDataset[ConvertTargetFile]\n    }\n\n    val hasMergeOnReadDeletionFiles = table.currentSnapshot().deleteManifests(table.io()).size() > 0\n    if (hasMergeOnReadDeletionFiles) {\n      throw new UnsupportedOperationException(\n        s\"Cannot support convert Iceberg table with row-level deletes.\" +\n          s\"Please trigger an Iceberg compaction and retry the command.\")\n    }\n\n    // Localize variables so we don't need to serialize the File Manifest class\n    // Some contexts: Spark needs all variables in closure to be serializable\n    // while class members carry the entire class, so they require serialization of the class\n    // As IcebergFileManifest is not serializable,\n    // we localize member variables to avoid serialization of the class\n    val localTable = table\n    // We use the latest snapshot timestamp for all generated Delta AddFiles due to the fact that\n    // retrieving timestamp for each DataFile is non-trivial time-consuming. This can be improved\n    // in the future.\n    val snapshotTs = table.currentSnapshot().timestampMillis\n\n    val shouldConvertPartition = spark.sessionState.conf\n      .getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_USE_NATIVE_PARTITION_VALUES)\n    val convertPartition = if (shouldConvertPartition) {\n      new IcebergPartitionConverter(localTable, partitionSchema, partitionEvolutionEnabled)\n    } else {\n      null\n    }\n\n    val shouldConvertStats = convertStats\n    val partialStatsConvertedEnabled = allowPartialStatsConverted\n    val statsAllowTypesSet = statsAllowTypes\n\n    val shouldCheckPartitionEvolution = !partitionEvolutionEnabled\n    val specIdsToIfSpecHasNonBucketPartitionMap = specIdsToIfSpecHasNonBucketPartition\n    val tableSpecsSize = table.specs().size()\n\n    val dataFiles = loadIcebergFiles()\n\n    dataFiles.map { dataFile: DataFileWrapper =>\n        if (shouldCheckPartitionEvolution) {\n          IcebergFileManifest.validateLimitedPartitionEvolution(\n            dataFile.specId,\n            tableSpecsSize,\n            specIdsToIfSpecHasNonBucketPartitionMap\n          )\n        }\n        ConvertTargetFile(\n          SerializableFileStatus(\n            path = dataFile.path,\n            length = dataFile.fileSizeInBytes,\n            isDir = false,\n            modificationTime = snapshotTs\n          ),\n          partitionValues = if (shouldConvertPartition) {\n            Some(convertPartition.toDelta(dataFile.partition()))\n          } else None,\n          stats = if (shouldConvertStats) {\n            IcebergStatsUtils.icebergStatsToDelta(\n              localTable.schema,\n              dataFile,\n              statsAllowTypesSet,\n              shouldSkipForFile = (df: DataFile) => {\n                !partialStatsConvertedEnabled && IcebergStatsUtils.hasPartialStats(df)\n              }\n            )\n          } else None\n        )\n      }\n      .cache()\n  }\n\n  private def loadIcebergFiles(): (\n    Dataset[DataFileWrapper]\n  ) = {\n    val localTable = table\n    val manifestFiles =\n          localTable\n            .currentSnapshot()\n            .dataManifests(localTable.io())\n            .asScala\n            .map(new ManifestFileWrapper(_))\n            .toSeq\n    val dataFiles =\n          spark\n            .createDataset(manifestFiles)\n            .flatMap(\n              ManifestFiles.read(_, localTable.io(), localTable.specs())\n                .asScala.map(new DataFileWrapper(_)\n            )\n          )\n    dataFiles\n  }\n\n\n  override def close(): Unit = {\n    fileSparkResults.map(_.unpersist())\n    fileSparkResults = None\n  }\n}\n\nobject IcebergFileManifest {\n  // scalastyle:off\n  /**\n   * Validates on partition evolution for proposed partitionSpecId\n   * We don't support the conversion of tables with partition evolution\n   *\n   * However, we allow one special case where\n   *  all data files have either no-partition or bucket-partition\n   *  regardless of multiple partition spec present in the table\n   */\n  // scalastyle:on\n  private def validateLimitedPartitionEvolution(\n      partitionSpecId: Int,\n      tableSpecsSize: Int,\n      specIdsToIfSpecHasNonBucketPartition: mutable.Map[Int, Boolean]): Unit = {\n    if (hasPartitionEvolved(\n      partitionSpecId, tableSpecsSize, specIdsToIfSpecHasNonBucketPartition)\n    ) {\n      throw cloneFromIcebergSourceWithPartitionEvolution()\n    }\n  }\n\n  private def hasPartitionEvolved(\n      partitionSpecID: Int,\n      tableSpecsSize: Int,\n      specIdsToIfSpecHasNonBucketPartition: mutable.Map[Int, Boolean]): Boolean = {\n    val isSpecPartitioned = specIdsToIfSpecHasNonBucketPartition(partitionSpecID)\n    isSpecPartitioned && tableSpecsSize > 1\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/IcebergPartitionConverter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.lang.{Integer => JInt, Long => JLong}\nimport java.nio.ByteBuffer\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping\nimport org.apache.spark.sql.delta.util.{DateFormatter, TimestampFormatter}\nimport shadedForDelta.org.apache.iceberg.{PartitionData, PartitionField, PartitionSpec, Schema, StructLike, Table}\nimport shadedForDelta.org.apache.iceberg.transforms.IcebergPartitionUtil\nimport shadedForDelta.org.apache.iceberg.types.{Conversions, Type => IcebergType}\nimport shadedForDelta.org.apache.iceberg.types.Type.{PrimitiveType => IcebergPrimitiveType, TypeID}\nimport shadedForDelta.org.apache.iceberg.types.Types.{\n  ListType => IcebergListType,\n  MapType => IcebergMapType,\n  NestedField,\n  StringType => IcebergStringType,\n  StructType => IcebergStructType\n}\n\nimport org.apache.spark.sql.types.StructType\n\n\nobject IcebergPartitionConverter {\n  // we must use field id to look up the partition value; consider scenario with iceberg\n  // behavior chance since 1.4.0:\n  // 1) create table with partition schema (a[col_name]: 1[field_id]), add file1;\n  //    The partition data for file1 is (a:1:some_part_value)\n  // 2) add new partition col b and the partition schema becomes (a: 1, b: 2), add file2;\n  //    the partition data for file2 is (a:1:some_part_value, b:2:some_part_value)\n  // 3) remove partition col a, then add file3;\n  //    for iceberg < 1.4.0: the partFields is (a:1(void), b:2); the partition data for\n  //                         file3 is (a:1(void):null, b:2:some_part_value);\n  //    for iceberg 1.4.0:   the partFields is (b:2); When it reads file1 (a:1:some_part_value),\n  //                         it must use the field_id instead of index to look up the partition\n  //                         value, as the partField and partitionData from file1 have different\n  //                         ordering and thus same index indicates different column.\n  def physicalNameToPartitionField(\n      table: IcebergTableLike, partitionSchema: StructType): Map[String, PartitionField] =\n    table.spec().fields().asScala.collect {\n      case field if field.transform().toString != \"void\" &&\n          !field.transform().toString.contains(\"bucket\") =>\n        DeltaColumnMapping.getPhysicalName(partitionSchema(field.name)) -> field\n    }.toMap\n}\n\ncase class IcebergPartitionConverter(\n    icebergSchema: Schema,\n    physicalNameToPartitionField: Map[String, PartitionField]) {\n\n  val dateFormatter: DateFormatter = DateFormatter()\n  val timestampFormatter: TimestampFormatter =\n      TimestampFormatter(ConvertUtils.timestampPartitionPattern, java.util.TimeZone.getDefault)\n\n  def this(\n      table: IcebergTableLike,\n      partitionSchema: StructType,\n      partitionEvolutionEnabled: Boolean) =\n    this(table.schema(),\n      // We only allow empty partition when partition evolution happened\n      // This is an extra safety mechanism as we should have already passed\n      // a non-bucket partitionSchema when table has >1 specs\n      if (table.specs().size() > 1 && !partitionEvolutionEnabled) {\n        Map.empty[String, PartitionField]\n      } else {\n        IcebergPartitionConverter.physicalNameToPartitionField(table, partitionSchema)\n      }\n    )\n\n  /**\n   * Convert an Iceberg [[PartitionData]] into a Map of (columnID -> partitionValue) used by Delta\n   */\n  def toDelta(partition: StructLike): Map[String, String] = {\n    val icebergPartitionData = partition.asInstanceOf[PartitionData]\n    val fieldIdToIdx = icebergPartitionData.getPartitionType\n      .fields()\n      .asScala\n      .zipWithIndex\n      .map(kv => kv._1.fieldId() -> kv._2)\n      .toMap\n    val physicalNameToPartValueMap = physicalNameToPartitionField\n      .map {\n        case (physicalName, field) =>\n          val fieldIndex = fieldIdToIdx.get(field.fieldId())\n          val partValueAsString = fieldIndex\n            .map { idx =>\n              val partValue = icebergPartitionData.get(idx)\n              IcebergPartitionUtil.partitionValueToString(\n                field,\n                partValue,\n                icebergSchema,\n                dateFormatter,\n                timestampFormatter\n              )\n            }\n            .getOrElse(null)\n          physicalName -> partValueAsString\n      }\n    physicalNameToPartValueMap\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/IcebergSchemaUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport shadedForDelta.org.apache.iceberg.Schema\nimport shadedForDelta.org.apache.iceberg.types.TypeUtil\n\nimport org.apache.spark.sql.types.{MetadataBuilder, StructType}\n\nobject IcebergSchemaUtils {\n\n  /**\n   * Given an iceberg schema, convert it to a Spark schema. This conversion will keep the Iceberg\n   * column IDs (used to read Parquet files) in the field metadata\n   *\n   * @param icebergSchema Iceberg schema\n   * @param castTimeType  cast Iceberg TIME type to Spark Long\n   * @return              Spark schema converted from Iceberg schema\n   */\n  def convertIcebergSchemaToSpark(icebergSchema: Schema,\n      castTimeType: Boolean = false): StructType = {\n    // Convert from Iceberg schema to Spark schema but without the column IDs\n    val baseConvertedSchema =\n      TypeUtil.visit(\n        icebergSchema, new TypeToSparkTypeWithCustomCast(castTimeType)\n      ).asInstanceOf[StructType]\n\n    // For each field, find the column ID (fieldId) and add to the StructField metadata\n    SchemaMergingUtils.transformColumns(baseConvertedSchema) { (path, field, _) =>\n      // This should be safe to access fields\n      // scalastyle:off\n      // https://github.com/apache/iceberg/blob/d98224a82b104888281d4e901ccf948f9642590b/api/src/main/java/org/apache/iceberg/types/IndexByName.java#L171\n      // scalastyle:on\n      val fieldPath = (path :+ field.name).mkString(\".\")\n      val id = icebergSchema.findField(fieldPath).fieldId()\n      field.copy(\n        metadata = new MetadataBuilder()\n          .withMetadata(field.metadata)\n          .putLong(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY, id)\n          .build())\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/IcebergSparkWrappers.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}\nimport java.lang.{Integer => JInt, Long => JLong}\nimport java.nio.ByteBuffer\nimport java.util.{List => JList, Map => JMap}\nimport java.util.stream.Collectors\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor\nimport shadedForDelta.org.apache.iceberg.{DataFile, DeleteFile, FileContent, FileFormat, ManifestContent, ManifestFile, PartitionData, StructLike}\nimport shadedForDelta.org.apache.iceberg.ManifestFile.PartitionFieldSummary\n\n/**\n * The classes in this file are wrappers of Iceberg classes\n * in the format of case classes so they can be serialized by\n * Spark automatically.\n */\n\ncase class ManifestFileWrapper(\n    path: String,\n    length: Long,\n    partitionSpecId: Int,\n    content: ManifestContent,\n    sequenceNumber: Long,\n    minSequenceNumber: Long,\n    snapshotId: JLong,\n    addedFilesCount: JInt,\n    addedRowsCount: JLong,\n    existingFilesCount: JInt,\n    existingRowsCount: JLong,\n    deletedFilesCount: JInt,\n    deletedRowsCount: JLong,\n    _partitions: Option[Seq[PartitionFieldSummaryWrapper]])\n  extends ManifestFile {\n\n  def this(manifest: ManifestFile) =\n    this(\n      manifest.path,\n      manifest.length,\n      manifest.partitionSpecId,\n      manifest.content(),\n      manifest.sequenceNumber,\n      manifest.minSequenceNumber,\n      manifest.snapshotId,\n      manifest.addedFilesCount,\n      manifest.addedRowsCount,\n      manifest.existingFilesCount,\n      manifest.existingRowsCount,\n      manifest.deletedFilesCount,\n      manifest.deletedRowsCount,\n      Option(manifest.partitions).map(_.asScala.map(new PartitionFieldSummaryWrapper(_)).toSeq)\n    )\n  override def partitions: JList[PartitionFieldSummary] =\n    _partitions.map(_.asJava.asInstanceOf[JList[PartitionFieldSummary]]).orNull\n  override def copy: ManifestFile = this.copy\n}\n\ncase class PartitionFieldSummaryWrapper(\n    containsNull: Boolean,\n    _lowerBound: Option[Array[Byte]],\n    _upperBound: Option[Array[Byte]]) extends PartitionFieldSummary {\n\n  def this(src: PartitionFieldSummary) =\n    this(\n      src.containsNull,\n      IcebergSparkWrappers.serializeByteBuffer(src.lowerBound),\n      IcebergSparkWrappers.serializeByteBuffer(src.upperBound)\n    )\n  override def lowerBound: ByteBuffer = IcebergSparkWrappers.deserializeByteBuffer(_lowerBound)\n  override def upperBound: ByteBuffer = IcebergSparkWrappers.deserializeByteBuffer(_upperBound)\n  override def copy: PartitionFieldSummary = this.copy\n}\n\ncase class DataFileWrapper(\n    pos: JLong,\n    specId: Int,\n    path: String,\n    recordCount: Long,\n    fileSizeInBytes: Long,\n    _partition: Array[Byte],\n    _columnSizes: Option[Map[JInt, JLong]],\n    _valueCounts: Option[Map[JInt, JLong]],\n    _nullValueCounts: Option[Map[JInt, JLong]],\n    _nanValueCounts: Option[Map[JInt, JLong]],\n    _lowerBounds: Option[Map[JInt, Option[Array[Byte]]]],\n    _upperBounds: Option[Map[JInt, Option[Array[Byte]]]],\n    _keyMetadata: Option[Array[Byte]],\n    _splitOffsets: Option[Seq[JLong]]) extends DataFile {\n\n  def this(df: DataFile) = {\n    this(\n      df.pos,\n      df.specId,\n      df.path.toString,\n      df.recordCount,\n      df.fileSizeInBytes,\n      IcebergSparkWrappers.serialize(df.partition.asInstanceOf[java.io.Serializable]),\n      Option(df.columnSizes).map(_.asScala.toMap),\n      Option(df.valueCounts).map(_.asScala.toMap),\n      Option(df.nullValueCounts).map(_.asScala.toMap),\n      Option(df.nanValueCounts).map(_.asScala.toMap),\n      IcebergSparkWrappers.serializeMap(df.lowerBounds),\n      IcebergSparkWrappers.serializeMap(df.upperBounds),\n      IcebergSparkWrappers.serializeByteBuffer(df.keyMetadata),\n      Option(df.splitOffsets).map(_.asScala.toSeq))\n    require(df.content == FileContent.DATA)\n    require(df.format == FileFormat.PARQUET)\n  }\n\n  override def content: FileContent = FileContent.DATA\n  override def format: FileFormat = FileFormat.PARQUET\n  override def partition: StructLike =\n    IcebergSparkWrappers.deserialize[PartitionData](this._partition)\n  override def columnSizes: JMap[JInt, JLong] = _columnSizes.map(_.asJava).orNull\n  override def valueCounts: JMap[JInt, JLong] = _valueCounts.map(_.asJava).orNull\n  override def nullValueCounts: JMap[JInt, JLong] = _nullValueCounts.map(_.asJava).orNull\n  override def nanValueCounts: JMap[JInt, JLong] = _nanValueCounts.map(_.asJava).orNull\n  override def lowerBounds: JMap[JInt, ByteBuffer] =\n    IcebergSparkWrappers.deserializeMap(_lowerBounds)\n  override def upperBounds: JMap[JInt, ByteBuffer] =\n    IcebergSparkWrappers.deserializeMap(_upperBounds)\n  override def keyMetadata: ByteBuffer = IcebergSparkWrappers.deserializeByteBuffer(_keyMetadata)\n  override def splitOffsets: JList[JLong] = _splitOffsets.map(_.asJava).orNull\n  override def copy: DataFile = this.copy\n  override def copyWithoutStats: DataFile = this.copy\n}\n\nobject IcebergSparkWrappers {\n  def serialize(obj: java.io.Serializable): Array[Byte] = {\n    val baos = new ByteArrayOutputStream()\n    val oos = new ObjectOutputStream(baos)\n    try {\n      oos.writeObject(obj)\n      oos.flush()\n      baos.toByteArray\n    } finally {\n      oos.close()\n      baos.close()\n    }\n  }\n  def deserialize[T](bytes: Array[Byte]): T = {\n    val bais = new ByteArrayInputStream(bytes)\n    val ois = new ObjectInputStream(bais)\n    try {\n      ois.readObject().asInstanceOf[T] // Cast to the expected type\n    } finally {\n      ois.close()\n      bais.close()\n    }\n  }\n  def serializeByteBuffer(byteBuffer: ByteBuffer): Option[Array[Byte]] = {\n    Option(byteBuffer).map(_.array())\n  }\n  def deserializeByteBuffer(byteBufferWrapper: Option[Array[Byte]]): ByteBuffer = {\n    byteBufferWrapper.map(ByteBuffer.wrap).orNull\n  }\n  def serializeMap[T](map: JMap[T, ByteBuffer]): Option[Map[T, Option[Array[Byte]]]] =\n    Option(map).map(_.asScala.mapValues(serializeByteBuffer).toMap)\n  def deserializeMap[T](map: Option[Map[T, Option[Array[Byte]]]]): JMap[T, ByteBuffer] =\n    map.map(_.mapValues(deserializeByteBuffer).toMap.asJava).orNull\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/IcebergStatsUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.lang.{Integer => JInt, Long => JLong}\nimport java.nio.ByteBuffer\nimport java.util.{Map => JMap}\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeltaStatistics._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport shadedForDelta.org.apache.iceberg.{DataFile, PartitionData, PartitionField, Schema, StructLike, Table}\nimport shadedForDelta.org.apache.iceberg.types.{Conversions, Type => IcebergType}\nimport shadedForDelta.org.apache.iceberg.types.Type.{PrimitiveType => IcebergPrimitiveType, TypeID}\nimport shadedForDelta.org.apache.iceberg.types.Types.{\n  DateType => IcebergDateType,\n  ListType => IcebergListType,\n  MapType => IcebergMapType,\n  NestedField,\n  StringType => IcebergStringType,\n  StructType => IcebergStructType,\n  TimestampType => IcebergTimestampType\n}\nimport shadedForDelta.org.apache.iceberg.util.DateTimeUtil\n\nimport org.apache.spark.sql.SparkSession\n\nobject IcebergStatsUtils extends DeltaLogging {\n\n  // Types that are currently supported for converting stats to delta\n  // The stats for these types will be converted to Delta stats\n  // except for following types:\n  //  DECIMAL (decided by DeltaSQLConf.DELTA_CONVERT_ICEBERG_DECIMAL_STATS)\n  //  DATE (decided by DeltaSQLConf.DELTA_CONVERT_ICEBERG_DATE_STATS)\n  //  TIMESTAMP (decided by DeltaSQLConf.DELTA_CONVERT_ICEBERG_TIMESTAMP_STATS)\n  // which are decided by spark configs dynamically\n  private val STATS_ALLOW_TYPES = Set[TypeID](\n    TypeID.BOOLEAN,\n    TypeID.INTEGER,\n    TypeID.LONG,\n    TypeID.FLOAT,\n    TypeID.DOUBLE,\n    TypeID.DATE,\n//    TypeID.TIME,\n    TypeID.TIMESTAMP,\n//    TypeID.TIMESTAMP_NANO,\n    TypeID.STRING,\n//    TypeID.UUID,\n//    TypeID.FIXED,\n    TypeID.BINARY,\n    TypeID.DECIMAL\n  )\n\n  private val STATS_NULLCOUNT_ALLOW_TYPES = Set[TypeID](\n    TypeID.LIST,\n    TypeID.MAP,\n    TypeID.DATE,\n    TypeID.TIMESTAMP,\n    TypeID.DECIMAL\n  )\n\n  private val CONFIGS_TO_STATS_ALLOW_TYPES = Map(\n    DeltaSQLConf.DELTA_CONVERT_ICEBERG_DATE_STATS -> TypeID.DATE,\n    DeltaSQLConf.DELTA_CONVERT_ICEBERG_TIMESTAMP_STATS -> TypeID.TIMESTAMP,\n    DeltaSQLConf.DELTA_CONVERT_ICEBERG_DECIMAL_STATS -> TypeID.DECIMAL\n  )\n\n  def typesAllowStatsConversion(spark: SparkSession): Set[TypeID] = {\n    val statsDisallowTypes = CONFIGS_TO_STATS_ALLOW_TYPES.filter {\n      case (conf, _) => !spark.sessionState.conf.getConf(conf)\n    }.values.toSet\n\n    typesAllowStatsConversion(statsDisallowTypes)\n  }\n\n  def typesAllowStatsConversion(statsDisallowTypes: Set[TypeID]): Set[TypeID] = {\n    STATS_ALLOW_TYPES -- statsDisallowTypes\n  }\n\n  /**\n   * Convert Iceberg DataFile stats into a Json string containing Delta stats.\n   * We will abandon conversion if Iceberg DataFile has a null or empty stats for\n   * any criteria used in the conversion.\n   *\n   * @param icebergSchema            Iceberg table schema\n   * @param dataFile                 Iceberg DataFile that contains stats info\n   * @param statsAllowTypes          Iceberg types that are allowed to convert stats\n   * @param shouldSkipForFile        Function => true if a data file should be skipped\n   * @return None if stats is missing on the DataFile or error occurs during conversion\n   */\n  def icebergStatsToDelta(\n      icebergSchema: Schema,\n      dataFile: DataFile,\n      statsAllowTypes: Set[TypeID],\n      shouldSkipForFile: DataFile => Boolean\n  ): Option[String] = {\n    if (shouldSkipForFile(dataFile)) {\n      return None\n    }\n    try {\n      Some(icebergStatsToDelta(\n        icebergSchema,\n        dataFile.recordCount,\n        Option(dataFile.upperBounds).map(_.asScala.toMap).filter(_.nonEmpty),\n        Option(dataFile.lowerBounds).map(_.asScala.toMap).filter(_.nonEmpty),\n        Option(dataFile.nullValueCounts).map(_.asScala.toMap).filter(_.nonEmpty),\n        statsAllowTypes\n      ))\n    } catch {\n      case NonFatal(e) =>\n        logInfo(\"[Iceberg-Stats-Conversion] \" +\n          \"Exception while converting Iceberg stats to Delta format\", e)\n        None\n    }\n  }\n\n  def hasPartialStats(dataFile: DataFile): Boolean = {\n    def nonEmptyMap[K, V](m: JMap[K, V]): Boolean = {\n      m != null && !m.isEmpty\n    }\n    // nullValueCounts is less common, so we ignore it\n    val hasPartialStats =\n      !nonEmptyMap(dataFile.upperBounds()) ||\n      !nonEmptyMap(dataFile.lowerBounds())\n    if (hasPartialStats) {\n      logInfo(s\"[Iceberg-Stats-Conversion] $dataFile only has partial stats:\" +\n        s\"upperBounds=${dataFile.upperBounds}, lowerBounds = ${dataFile.lowerBounds()}\")\n    }\n    hasPartialStats\n  }\n\n  /**\n   * Convert Iceberg DataFile stats into Delta stats.\n   *\n   * Iceberg stats consist of multiple maps from field_id to value. The maps include\n   * max_value, min_value and null_counts.\n   * Delta stats is a Json string.\n   *\n   **********************************************************\n   * Example:\n   **********************************************************\n   * Assume we have an Iceberg table of schema\n   * ( col1: int, field_id = 1, col2: string, field_id = 2 )\n   *\n   * The following Iceberg stats:\n   *    numRecords 100\n   *    max_value { 1 -> 200, 2 -> \"max value\" }\n   *    min_value { 1 -> 10, 2 -> \"min value\" }\n   *    null_counts { 1 -> 0, 2 -> 20 }\n   * will be converted into the following Delta style stats as a Json str\n   *\n   * {\n   *    numRecords: 100,\n   *    maxValues: {\n   *      \"col1\": 200,\n   *      \"col2\" \"max value\"\n   *    },\n   *    minValues: {\n   *      \"col1\": 10,\n   *      \"col2\": \"min value\"\n   *    },\n   *    nullCount: {\n   *      \"col1\": 0,\n   *      \"col2\": 20\n   *    }\n   * }\n   **********************************************************\n   *\n   * See also [[org.apache.spark.sql.delta.stats.StatsCollectionUtils]] for more\n   * about Delta stats.\n   *\n   * @param icebergSchema          Iceberg table schema\n   * @param numRecords             Iceberg stats of numRecords\n   * @param maxMap                 Iceberg stats of max value ( field_id -> value )\n   * @param minMap                 Iceberg stats of min value ( field_id -> value )\n   * @param nullCountMap           Iceberg stats of null count ( field_id -> value )\n   * @param statsAllowTypes        dataTypes that will convert stats to Delta\n   * @return json string representing Delta stats\n   */\n  private[convert] def icebergStatsToDelta(\n      icebergSchema: Schema,\n      numRecords: Long,\n      maxMap: Option[Map[JInt, ByteBuffer]],\n      minMap: Option[Map[JInt, ByteBuffer]],\n      nullCountMap: Option[Map[JInt, JLong]],\n      statsAllowTypes: Set[TypeID]\n  ): String = {\n\n    def deserialize(ftype: IcebergType, value: Any): Any = {\n      (ftype, value) match {\n        case (_, null) => null\n        case (_: IcebergStringType, bb: ByteBuffer) =>\n          Conversions.fromByteBuffer(ftype, bb).toString\n        case (_: IcebergDateType, bb: ByteBuffer) =>\n          val daysFromEpoch = Conversions.fromByteBuffer(ftype, bb).asInstanceOf[Int]\n          DateTimeUtil.dateFromDays(daysFromEpoch).toString\n        case (tsType: IcebergTimestampType, bb: ByteBuffer) =>\n          val microts = Conversions.fromByteBuffer(tsType, bb).asInstanceOf[JLong]\n          microTimestampToString(microts, tsType)\n        case (_, bb: ByteBuffer) =>\n          Conversions.fromByteBuffer(ftype, bb)\n        case _ => throw new IllegalArgumentException(\"unable to deserialize unknown values\")\n      }\n    }\n\n    // Recursively collect stats from the given fields list and values and\n    // use the given deserializer to format the value.\n    // The result is a map of ( delta column physical name -> value )\n    def collectStats(\n        fields: java.util.List[NestedField],\n        valueMap: Map[JInt, Any],\n        deserializer: (IcebergType, Any) => Any,\n        statsAllowTypes: Set[TypeID]): Map[String, Any] = {\n      fields.asScala.flatMap { field =>\n        field.`type`() match {\n          case st: IcebergStructType =>\n            Some(field.name ->\n              collectStats(st.fields, valueMap, deserializer, statsAllowTypes))\n          case pt: IcebergPrimitiveType\n            if valueMap.contains(field.fieldId) && statsAllowTypes.contains(pt.typeId) =>\n            Option(deserializer(pt, valueMap(field.fieldId))).map(field.name -> _)\n          case pt: IcebergListType\n            if valueMap.contains(field.fieldId) && statsAllowTypes.contains(pt.typeId) =>\n            Option(deserializer(pt, valueMap(field.fieldId))).map(field.name -> _)\n          case pt: IcebergMapType\n            if valueMap.contains(field.fieldId) && statsAllowTypes.contains(pt.typeId) =>\n            Option(deserializer(pt, valueMap(field.fieldId))).map(field.name -> _)\n          case _ => None\n        }\n      }.toMap\n    }\n\n    JsonUtils.toJson(\n      Map(\n        NUM_RECORDS -> numRecords\n      ) ++ maxMap.map(\n        MAX -> collectStats(icebergSchema.columns, _, deserialize, statsAllowTypes)\n      ) ++ minMap.map(\n        MIN -> collectStats(icebergSchema.columns, _, deserialize, statsAllowTypes)\n      ) ++ nullCountMap.map(\n        NULL_COUNT -> collectStats(\n          icebergSchema.columns, _, (_: IcebergType, v: Any) => v,\n            statsAllowTypes ++ STATS_NULLCOUNT_ALLOW_TYPES\n        )\n      )\n    )\n  }\n\n  private def microTimestampToString(\n      microTS: JLong, tsType: IcebergTimestampType): String = {\n    // iceberg timestamptz will have shouldAdjustToUTC() as true\n    if (tsType.shouldAdjustToUTC()) {\n      DateTimeUtil.microsToIsoTimestamptz(microTS)\n    } else {\n    // iceberg timestamp doesn't need to adjust to UTC\n      DateTimeUtil.microsToIsoTimestamp(microTS)\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/IcebergTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.util.Locale\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaColumnMappingMode, DeltaConfigs, IdMapping, SerializableFileStatus, Snapshot}\nimport org.apache.spark.sql.delta.DeltaErrors.{cloneFromIcebergSourceWithoutSpecs, cloneFromIcebergSourceWithPartitionEvolution}\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport shadedForDelta.org.apache.iceberg.{PartitionSpec, Schema, Snapshot => IcebergSnapshot, Table, TableProperties}\nimport shadedForDelta.org.apache.iceberg.hadoop.HadoopTables\nimport shadedForDelta.org.apache.iceberg.io.FileIO\nimport shadedForDelta.org.apache.iceberg.transforms.{Bucket, IcebergPartitionUtil}\nimport shadedForDelta.org.apache.iceberg.util.PropertyUtil\n\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.execution.datasources.PartitioningUtils\nimport org.apache.spark.sql.types.StructType\n\n/** Subset of [[Table]] functionality required for conversion to Delta. */\ntrait IcebergTableLike {\n  def location(): String\n  def schema(): Schema\n  def properties(): java.util.Map[String, String]\n  def specs(): java.util.Map[Integer, PartitionSpec]\n  def spec(): PartitionSpec\n  def currentSnapshot(): IcebergSnapshot\n  def snapshot(id: Long): IcebergSnapshot\n  def io(): FileIO\n}\n\n/**\n * Implementation of [[IcebergTableLike]] that can safely rely on the functionality of an\n * underlying [[Table]].\n */\ncase class DelegatingIcebergTable(table: Table) extends IcebergTableLike {\n  override def location(): String = table.location()\n  override def schema(): Schema = table.schema()\n  override def properties(): java.util.Map[String, String] = table.properties()\n  override def specs(): java.util.Map[Integer, PartitionSpec] = table.specs()\n  override def spec(): PartitionSpec = table.spec()\n  override def currentSnapshot(): IcebergSnapshot = table.currentSnapshot()\n  override def snapshot(id: Long): IcebergSnapshot = table.snapshot(id)\n  override def io(): FileIO = table.io()\n}\n\n/**\n * A target Iceberg table for conversion to a Delta table.\n *\n * @param icebergTable the Iceberg table underneath.\n * @param deltaSnapshot the delta snapshot used for incremental update, none for initial conversion.\n * @param convertStats flag for disabling convert iceberg stats directly into Delta stats.\n *                     If you wonder why we need this flag, you are not alone.\n *                     This flag is only used by the old, obsolete, legacy command\n *                     `CONVERT TO DELTA NO STATISTICS`.\n *                     We believe that back then the CONVERT command suffered performance\n *                     problem due to stats collection and design `NO STATISTICS` as a workaround.\n *                     Now we are able to generate stats much faster, but when this flag is true,\n *                     we still have to honor it and give up generating stats. What a pity!\n */\nclass IcebergTable(\n    spark: SparkSession,\n    icebergTable: IcebergTableLike,\n    deltaSnapshot: Option[Snapshot],\n    convertStats: Boolean) extends ConvertTargetTable {\n\n  def this(spark: SparkSession, basePath: String, deltaTable: Option[Snapshot],\n           convertStats: Boolean = true) =\n    // scalastyle:off deltahadoopconfiguration\n    this(\n      spark,\n      DelegatingIcebergTable(new HadoopTables(spark.sessionState.newHadoopConf).load(basePath)),\n      deltaTable,\n      convertStats)\n    // scalastyle:on deltahadoopconfiguration\n\n  protected val existingSchema: Option[StructType] = deltaSnapshot.map(_.schema)\n\n  private val partitionEvolutionEnabled =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_PARTITION_EVOLUTION_ENABLED)\n\n  private val bucketPartitionEnabled =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_BUCKET_PARTITION_ENABLED) ||\n      deltaSnapshot.exists(s =>\n        DeltaConfigs.IGNORE_ICEBERG_BUCKET_PARTITION.fromMetaData(s.metadata)\n      )\n\n  // When a table is CLONED/federated with the session conf ON, it will have the table property\n  // set and will continue to support CAST TIME TYPE even when later the session conf is OFF.\n  private val castTimeType =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_CAST_TIME_TYPE) ||\n      deltaSnapshot.exists(s => DeltaConfigs.CAST_ICEBERG_TIME_TYPE.fromMetaData(s.metadata))\n\n  protected val fieldPathToPhysicalName: Map[Seq[String], String] =\n    existingSchema.map {\n      SchemaMergingUtils.explode(_).collect {\n        case (path, field) if DeltaColumnMapping.hasPhysicalName(field) =>\n          path.map(_.toLowerCase(Locale.ROOT)) -> DeltaColumnMapping.getPhysicalName(field)\n      }.toMap\n    }.getOrElse(Map.empty[Seq[String], String])\n\n  private val convertedSchema = {\n    // Reuse physical names of existing columns.\n    val mergedSchema = DeltaColumnMapping.setPhysicalNames(\n      IcebergSchemaUtils.convertIcebergSchemaToSpark(icebergTable.schema(), castTimeType),\n      fieldPathToPhysicalName)\n\n    // Assign physical names to new columns.\n    DeltaColumnMapping.assignPhysicalNames(mergedSchema, reuseLogicalName = true)\n  }\n\n  override val requiredColumnMappingMode: DeltaColumnMappingMode = IdMapping\n\n  override val properties: Map[String, String] = {\n    val maxSnapshotAgeMs = PropertyUtil.propertyAsLong(icebergTable.properties,\n      TableProperties.MAX_SNAPSHOT_AGE_MS, TableProperties.MAX_SNAPSHOT_AGE_MS_DEFAULT)\n    val castTimeTypeConf = if (castTimeType) {\n      Some((DeltaConfigs.CAST_ICEBERG_TIME_TYPE.key -> \"true\"))\n    } else {\n      None\n    }\n    val bucketPartitionToNonPartition = if (bucketPartitionEnabled) {\n      Some((DeltaConfigs.IGNORE_ICEBERG_BUCKET_PARTITION.key -> \"true\"))\n    } else {\n      None\n    }\n    val timestampNtz = if (SchemaUtils.checkForTimestampNTZColumnsRecursively(convertedSchema)) {\n      Some(\"delta.feature.timestampNtz\" -> \"supported\")\n    } else None\n    icebergTable.properties().asScala.toMap + (DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"id\") +\n    (DeltaConfigs.LOG_RETENTION.key -> s\"$maxSnapshotAgeMs millisecond\") ++\n      castTimeTypeConf ++\n      timestampNtz ++\n      bucketPartitionToNonPartition\n  }\n\n  val tablePartitionSpec: PartitionSpec = {\n    // Validate && Get Partition Spec from Iceberg table\n    // We don't support conversion from iceberg tables with partition evolution\n    // So normally we only allow table having one partition spec\n    //\n    // However, we allow one special case where\n    //  all data files have either no-partition or bucket-partition\n    //  in this case we will convert them into non-partition, so\n    //  we will use an arbitrary non-bucket-partition spec as table's spec\n    if (icebergTable.specs().size() == 1 || partitionEvolutionEnabled || !bucketPartitionEnabled) {\n      icebergTable.spec()\n    } else if (icebergTable.specs().isEmpty) {\n      throw cloneFromIcebergSourceWithoutSpecs()\n    } else {\n      icebergTable.specs().asScala.values.find(\n        !IcebergPartitionUtil.hasNonBucketPartition(_)\n      ).getOrElse {\n        throw cloneFromIcebergSourceWithPartitionEvolution()\n      }\n    }\n  }\n\n  override val partitionSchema: StructType = {\n    // Reuse physical names of existing columns.\n    val mergedPartitionSchema = DeltaColumnMapping.setPhysicalNames(\n      StructType(\n        IcebergPartitionUtil.getPartitionFields(\n          tablePartitionSpec, icebergTable.schema(), castTimeType\n        )\n      ),\n      fieldPathToPhysicalName)\n\n    // Assign physical names to new partition columns.\n    DeltaColumnMapping.assignPhysicalNames(mergedPartitionSchema, reuseLogicalName = true)\n  }\n\n  val tableSchema: StructType = PartitioningUtils.mergeDataAndPartitionSchema(\n    convertedSchema,\n    partitionSchema,\n    spark.sessionState.conf.caseSensitiveAnalysis)._1\n\n  checkConvertible()\n\n  val fileManifest = new IcebergFileManifest(spark, icebergTable, partitionSchema, convertStats)\n\n  lazy val numFiles: Long =\n    Option(icebergTable.currentSnapshot())\n      .flatMap { snapshot =>\n        Option(snapshot.summary()).flatMap(_.asScala.get(\"total-data-files\").map(_.toLong))\n      }\n      .getOrElse(fileManifest.numFiles)\n\n  lazy val sizeInBytes: Long =\n    Option(icebergTable.currentSnapshot())\n      .flatMap { snapshot =>\n        Option(snapshot.summary()).flatMap(_.asScala.get(\"total-files-size\").map(_.toLong))\n      }\n      .getOrElse(fileManifest.sizeInBytes)\n\n  override val format: String = \"iceberg\"\n\n  def checkConvertible(): Unit = {\n    /**\n     * If the sql conf bucketPartitionEnabled is true, then convert iceberg table with\n     * bucket partition to unpartitioned delta table; if bucketPartitionEnabled is false,\n     * block conversion.\n     */\n    if (!bucketPartitionEnabled && IcebergPartitionUtil.hasBucketPartition(icebergTable.spec())) {\n      throw new UnsupportedOperationException(IcebergTable.ERR_BUCKET_PARTITION)\n    }\n\n    /**\n     * Existing Iceberg Table that has data imported from table without field ids will need\n     * to add a custom property to enable the mapping for Iceberg.\n     * Therefore, we can simply check for the existence of this property to see if there was\n     * a custom mapping within Iceberg.\n     *\n     * Ref: https://www.mail-archive.com/dev@iceberg.apache.org/msg01638.html\n     */\n    if (icebergTable.properties().containsKey(TableProperties.DEFAULT_NAME_MAPPING)) {\n      throw new UnsupportedOperationException(IcebergTable.ERR_CUSTOM_NAME_MAPPING)\n    }\n\n    /**\n     * Delta does not support case sensitive columns while Iceberg does. We should check for\n     * this here to throw a better message tailored to converting to Delta than the default\n     * AnalysisException\n     */\n     try {\n       SchemaMergingUtils.checkColumnNameDuplication(tableSchema, \"during convert to Delta\")\n     } catch {\n       case e: AnalysisException if e.getMessage.contains(\"during convert to Delta\") =>\n         throw new UnsupportedOperationException(\n           IcebergTable.caseSensitiveConversionExceptionMsg(e.getMessage))\n     }\n  }\n}\n\nobject IcebergTable {\n  /** Error message constants */\n  val ERR_MULTIPLE_PARTITION_SPECS =\n    s\"Source iceberg table has undergone partition evolution\"\n  val ERR_CUSTOM_NAME_MAPPING = \"Cannot convert Iceberg tables with column name mapping\"\n\n  val ERR_BUCKET_PARTITION = \"Cannot convert Iceberg tables with bucket partition\"\n\n  def caseSensitiveConversionExceptionMsg(conflictingColumns: String): String =\n    s\"\"\"Cannot convert table to Delta as the table contains column names that only differ by case.\n       |$conflictingColumns. Delta does not support case sensitive column names.\n       |Please rename these columns before converting to Delta.\n       \"\"\".stripMargin\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/TypeToSparkTypeWithCustomCast.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.util\n\nimport scala.collection.JavaConverters._\n\nimport shadedForDelta.org.apache.iceberg.MetadataColumns\nimport shadedForDelta.org.apache.iceberg.Schema\nimport shadedForDelta.org.apache.iceberg.relocated.com.google.common.collect.Lists\nimport shadedForDelta.org.apache.iceberg.types.Type\nimport shadedForDelta.org.apache.iceberg.types.Type.TypeID._\nimport shadedForDelta.org.apache.iceberg.types.Types\nimport shadedForDelta.org.apache.iceberg.types.TypeUtil\n\nimport org.apache.spark.sql.types.ArrayType\nimport org.apache.spark.sql.types.BinaryType\nimport org.apache.spark.sql.types.BooleanType\nimport org.apache.spark.sql.types.DataType\nimport org.apache.spark.sql.types.DateType\nimport org.apache.spark.sql.types.DecimalType\nimport org.apache.spark.sql.types.DoubleType\nimport org.apache.spark.sql.types.FloatType\nimport org.apache.spark.sql.types.IntegerType\nimport org.apache.spark.sql.types.LongType\nimport org.apache.spark.sql.types.MapType\nimport org.apache.spark.sql.types.Metadata\nimport org.apache.spark.sql.types.MetadataBuilder\nimport org.apache.spark.sql.types.StringType\nimport org.apache.spark.sql.types.StructField\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.sql.types.TimestampNTZType\nimport org.apache.spark.sql.types.TimestampType\n\n/**\n * This class is copied from [[org.apache.iceberg.spark.TypeToSparkType]] to\n * add custom type casting. Currently, it supports the following casting\n * * Iceberg TIME -> Spark Long\n */\nclass TypeToSparkTypeWithCustomCast(castTimeType: Boolean)\n  extends TypeUtil.SchemaVisitor[DataType] {\n\n  val METADATA_COL_ATTR_KEY = \"__metadata_col\";\n\n  override def schema(schema: Schema, structType: DataType): DataType = structType\n\n  override def struct(struct: Types.StructType, fieldResults: util.List[DataType]): DataType = {\n    val fields = struct.fields();\n    val sparkFields: util.List[StructField] =\n      Lists.newArrayListWithExpectedSize(fieldResults.size())\n    for (i <- 0 until fields.size()) {\n      val field = fields.get(i)\n      val `type` = fieldResults.get(i)\n      val metadata = fieldMetadata(field.fieldId())\n      var sparkField = StructField.apply(field.name(), `type`, field.isOptional(), metadata)\n      if (field.doc() != null) {\n        sparkField = sparkField.withComment(field.doc())\n      }\n      sparkFields.add(sparkField)\n    }\n\n    StructType.apply(sparkFields)\n  }\n\n  override def field(field: Types.NestedField, fieldResult: DataType): DataType = fieldResult\n\n  override def list(list: Types.ListType, elementResult: DataType): DataType =\n    ArrayType.apply(elementResult, list.isElementOptional())\n\n  override def map(map: Types.MapType, keyResult: DataType, valueResult: DataType): DataType =\n    MapType.apply(keyResult, valueResult, map.isValueOptional())\n\n  override def primitive(primitive: Type.PrimitiveType): DataType = {\n    primitive.typeId() match {\n      case BOOLEAN => BooleanType\n      case INTEGER => IntegerType\n      case LONG => LongType\n      case FLOAT => FloatType\n      case DOUBLE => DoubleType\n      case DATE => DateType\n      // Changed to allow casting TIME to Spark Long.\n      // The result is microseconds since midnight.\n      case TIME =>\n        if (castTimeType) {\n          LongType\n        } else {\n          throw new UnsupportedOperationException(\"Spark does not support time fields\")\n        }\n      case TIMESTAMP =>\n        val ts = primitive.asInstanceOf[Types.TimestampType]\n        if (ts.shouldAdjustToUTC()) {\n          TimestampType\n        } else {\n          TimestampNTZType\n        }\n      case STRING => StringType\n      case UUID => // use String\n        StringType\n      case FIXED => BinaryType\n      case BINARY => BinaryType\n      case DECIMAL =>\n        val decimal = primitive.asInstanceOf[Types.DecimalType]\n        DecimalType.apply(decimal.precision(), decimal.scale());\n      case _ =>\n        throw new UnsupportedOperationException(\n            \"Cannot convert unknown type to Spark: \" + primitive);\n    }\n  }\n\n  private def fieldMetadata(fieldId: Int): Metadata = {\n    if (MetadataColumns.metadataFieldIds().contains(fieldId)) {\n      return new MetadataBuilder().putBoolean(METADATA_COL_ATTR_KEY, value = true).build()\n    }\n\n    Metadata.empty\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/icebergShaded/DeltaToIcebergConvert.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.icebergShaded\n\nimport java.nio.ByteBuffer\nimport java.sql.Timestamp\nimport java.time.{LocalDateTime, OffsetDateTime, ZoneOffset}\nimport java.time.format._\nimport java.util.{Base64, List => JList}\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeltaConfig, DeltaConfigs, IcebergCompat, NoMapping, Snapshot, SnapshotDescriptor}\nimport org.apache.spark.sql.delta.DeltaConfigs.{LOG_RETENTION, TOMBSTONE_RETENTION}\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport shadedForDelta.org.apache.iceberg.{FileMetadata, PartitionData, PartitionSpec, Schema => IcebergSchema, StructLike, TableProperties => IcebergTableProperties}\nimport shadedForDelta.org.apache.iceberg.expressions.Literal\nimport shadedForDelta.org.apache.iceberg.types.{Conversions, Type => IcebergType, Types => IcebergTypes}\n\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.util.ResolveDefaultColumns.CURRENT_DEFAULT_COLUMN_METADATA_KEY\nimport org.apache.spark.sql.types._\nimport org.apache.spark.unsafe.types.CalendarInterval\n\n/**\n * Generate Iceberg table metadata (schema, partition, etc.) from a Delta [[Snapshot]]\n */\nclass DeltaToIcebergConverter(val snapshot: SnapshotDescriptor, val catalogTable: CatalogTable) {\n\n  private val schemaUtils: IcebergSchemaUtils =\n    IcebergSchemaUtils(snapshot.metadata.columnMappingMode == NoMapping)\n\n  def maxFieldId: Int = schemaUtils.maxFieldId(snapshot)\n\n  val schema: IcebergSchema = IcebergCompat\n    .getEnabledVersion(snapshot.metadata)\n    .orElse(Some(0))\n    .map { compatVersion =>\n      val icebergStruct = schemaUtils.convertStruct(snapshot.schema)(compatVersion)\n      new IcebergSchema(icebergStruct.fields())\n    }.getOrElse(throw new IllegalArgumentException(\"No IcebergCompat available\"))\n\n  val partition: PartitionSpec = IcebergTransactionUtils\n    .createPartitionSpec(schema, snapshot.metadata.partitionColumns)\n\n  val properties: Map[String, String] =\n    DeltaToIcebergConvert.TableProperties(snapshot.metadata.configuration)\n}\n/**\n * Utils for converting a Delta Table to Iceberg Table\n */\nobject DeltaToIcebergConvert\n  extends DeltaLogging\n  {\n  object Action\n    extends DeltaLogging\n    {\n      def buildPartitionValues(\n          builder: FileMetadata.Builder,\n          fileAction: FileAction,\n          partitionSpec: PartitionSpec,\n          snapshot: Snapshot,\n          logicalToPhysicalPartitionNames: Map[String, String]): Unit = {\n        if (partitionSpec.isPartitioned) {\n            builder.withPartition(\n              DeltaToIcebergConvert.Partition.convertPartitionValues(\n                snapshot,\n                partitionSpec,\n                fileAction.partitionValues,\n                logicalToPhysicalPartitionNames))\n        }\n      }\n    }\n  /**\n   * Utils used when converting Delta schema to Iceberg\n   */\n  object Schema {\n    /**\n     * Extract Delta Column Default values in Iceberg Literal format\n     * @param field column\n     * @return Right(Some(Literal)) if the column contains a literal default\n     *         Right(None) if the column does not have a default\n     *         Left(errorMessage) if the column contains a non-literal default\n     */\n    def extractLiteralDefault(field: StructField): Either[String, Option[Literal[_]]] = {\n      if (field.metadata.contains(CURRENT_DEFAULT_COLUMN_METADATA_KEY)) {\n        val defaultValueStr = field.metadata.getString(CURRENT_DEFAULT_COLUMN_METADATA_KEY)\n        try {\n            Right(Some(stringToLiteral(defaultValueStr, field.dataType)))\n        } catch {\n          case NonFatal(e) =>\n            Left(\"Unsupported default value:\" +\n              s\"${field.dataType.typeName}:$defaultValueStr:${e.getMessage}\")\n          case unknown: Throwable => throw unknown\n        }\n      } else {\n        Right(None)\n      }\n    }\n\n    /**\n     * Follow Spark's string escape rule to unescape a default value string\n     * @param input string to be unescaped\n     * @return unescaped string\n     */\n    def unescapeString(input: String): String = {\n      val table = Map[Char, String](\n        'b' -> \"\\u0008\", 't' -> \"\\t\", 'n' -> \"\\n\", 'r' -> \"\\r\",\n        'Z' -> \"\\u001A\", '\\\\' -> \"\\\\\", '%' -> \"\\\\%\", '_' -> \"\\\\_\", '\\'' -> \"'\"\n      )\n\n      def isHex(c: Char): Boolean = Character.digit(c, 16) >= 0\n      def isOct(c: Char): Boolean = c >= '0' && c <= '7'\n\n      def hexAt(pos: Int, n: Int): String = {\n        if (pos + n <= input.length && (0 until n).forall(k => isHex(input.charAt(pos + k)))) {\n          val cp = Integer.parseInt(input.substring(pos, pos + n), 16)\n          new String(Character.toChars(cp))\n        } else null\n      }\n\n      def octAt(pos: Int): String = {\n        if (pos + 3 <= input.length && (0 until 3).forall(k => isOct(input.charAt(pos + k)))) {\n          val cp = Integer.parseInt(input.substring(pos, pos + 3), 8)\n          if (cp <= 255) new String(Character.toChars(cp)) else null\n        } else null\n      }\n\n      val out = new StringBuilder\n      var i = 0\n      while (i < input.length) {\n        val c = input.charAt(i)\n        if (c != '\\\\') { out.append(c); i += 1 }\n        else {\n          if (i + 1 >= input.length) throw new IllegalStateException(\"dangling escape\")\n          val d = input.charAt(i + 1)\n\n          if (d >= '0' && d <= '7') {\n            val oct = octAt(i + 1)\n            if (oct != null) { out.append(oct); i += 4 }\n            else if (d == '0') { out.append(\"\\u0000\"); i += 2 }\n            else { out.append(d); i += 2 }\n          } else if (d == 'u' || d == 'U') {\n            val h8 = hexAt(i + 2, 8)\n            val h4 = if (h8 == null) hexAt(i + 2, 4) else null\n            val h = if (h8 != null) h8 else h4\n            if (h != null) { out.append(h); i += (if (h8 != null) 10 else 6) }\n            else { out.append(d); i += 2 } // \\x => x rule\n          } else {\n            out.append(table.getOrElse(d, d.toString))\n            i += 2\n          }\n        }\n      }\n      out.toString\n    }\n\n    /**\n     * Convert Delta default value string to an Iceberg Literal based on data type.\n     * @param str default value in Delta column metadata\n     * @param dataType Delta column data type\n     * @return converted Literal\n     */\n    def stringToLiteral(str: String, dataType: DataType): Literal[_] = {\n      def parseString(input: String) = {\n        if (input.length > 1 && ((input.head == '\\'' && input.last == '\\'')\n          || (input.head == '\"' && input.last == '\"'))) {\n          Literal.of(unescapeString(input.substring(1, input.length - 1)))\n        } else {\n          throw new UnsupportedOperationException(s\"String missing quotation marks: $input\")\n        }\n      }\n      // Parse either hex encoded literal x'....' or string literal(utf8) into binary\n      def parseBinary(input: String) = {\n        if (input.startsWith(\"x\") || input.startsWith(\"X\")) {\n          // Hex encoded literal\n          var hexString = parseString(input.substring(1)).value().toString\n          if (hexString.length % 2 == 1) {\n            hexString = hexString.substring(1) + \"00\"\n          }\n          Literal.of(hexString.sliding(2, 2).map(Integer.parseInt(_, 16).toByte).toArray)\n        } else {\n          Literal.of(parseString(input).value().toString\n            .getBytes(java.nio.charset.StandardCharsets.UTF_8))\n        }\n      }\n      // Parse timestamp string without time zone info\n      def parseLocalTimestamp(input: String) = {\n        val formats = Seq(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\"),\n          DateTimeFormatter.ISO_LOCAL_DATE_TIME)\n        val stripped = parseString(input).value()\n        val parsed = formats.flatMap { format =>\n          try {\n            val ldt = LocalDateTime.parse(stripped, format)\n            Some(\n              Literal.of(\n                ldt.toInstant(ZoneOffset.UTC).getEpochSecond * 1000000\n                  + ldt.getNano / 1000\n              )\n            )\n          } catch {\n            case NonFatal(_) => None\n          }\n        }\n        if (parsed.nonEmpty) {\n          parsed.head\n        } else {\n          throw new IllegalArgumentException(input)\n        }\n      }\n      // Parse string with time zone info. If the input has no time zone, assume its UTC.\n      def parseTimestamp(input: String) = {\n        val stripped = parseString(input).value()\n        try {\n          val instant = OffsetDateTime.parse(stripped, DateTimeFormatter.ISO_DATE_TIME).toInstant\n          Literal.of(instant.getEpochSecond * 1000000 + instant.getNano / 1000)\n        } catch {\n          case NonFatal(_) => parseLocalTimestamp(input)\n        }\n      }\n\n      dataType match {\n        case StringType => parseString(str)\n        case LongType => Literal.of(java.lang.Long.valueOf(str.replaceAll(\"[lL]$\", \"\")))\n        case IntegerType | ShortType | ByteType => Literal.of(Integer.valueOf(str))\n        case FloatType => Literal.of(java.lang.Float.valueOf(str))\n        case DoubleType => Literal.of(java.lang.Double.valueOf(str))\n        // The number should be correctly formatted without need to rounding\n        case d: DecimalType => Literal.of(\n          new java.math.BigDecimal(str, new java.math.MathContext(d.precision)).setScale(d.scale)\n        )\n        case BooleanType => Literal.of(java.lang.Boolean.valueOf(str))\n        case BinaryType => parseBinary(str)\n        case DateType => parseString(str).to(IcebergTypes.DateType.get())\n        case TimestampType => parseTimestamp(str)\n        case TimestampNTZType => parseLocalTimestamp(str)\n        case _ =>\n          throw new UnsupportedOperationException(\n            s\"Could not convert default value: $dataType: $str\")\n      }\n    }\n  }\n\n  object TableProperties\n  {\n    /**\n     * We generate Iceberg Table properties from Delta table properties\n     * using two methods.\n     * 1. If a Delta property key starts with \"delta.universalformat.config.iceberg\"\n     * we strip the prefix from the key and include the property pair.\n     * Note the key is already normalized to lower case.\n     * 2. We compute Iceberg properties from Delta using custom logic\n     * This now includes\n     * a) Iceberg format version\n     * b) Iceberg snapshot retention\n     */\n    def apply(deltaProperties: Map[String, String]): Map[String, String] = {\n      val prefix = DeltaConfigs.DELTA_UNIVERSAL_FORMAT_ICEBERG_CONFIG_PREFIX\n      val copiedFromDelta =\n        deltaProperties\n          .filterKeys(_.startsWith(prefix))\n          .map { case (key, value) => key.stripPrefix(prefix) -> value }\n          .toSeq\n          .toMap\n      val computers = Seq(FormatVersionComputer, RetentionPeriodComputer)\n      val computed: Map[String, String] = computers\n        .map(_.apply(deltaProperties ++ copiedFromDelta))\n        .reduce((a, b) => a ++ b)\n\n      copiedFromDelta ++ computed\n    }\n\n    private trait IcebergPropertiesComputer {\n      /**\n       * Compute Iceberg properties from Delta properties.\n       */\n      def apply(deltaProperties: Map[String, String]): Map[String, String]\n    }\n\n    /**\n     * Compute Iceberg FORMAT_VERSION from IcebergCompat\n     */\n    private object FormatVersionComputer extends IcebergPropertiesComputer {\n      override def apply(deltaProperties: Map[String, String]): Map[String, String] =\n        IcebergCompat\n          .anyEnabled(deltaProperties)\n          .map(IcebergTableProperties.FORMAT_VERSION -> _.icebergFormatVersion.toString)\n          .toMap\n    }\n\n    /**\n     * Compute Iceberg MAX_SNAPSHOT_AGE_MS as the minimal of\n     * Delta's LOG_RETENTION and TOMBSTONE_RETENTION.\n     * If users explicitly provide a MAX_SNAPSHOT_AGE_MS, also ensure the provided\n     * value is no larger than Delta's retention.\n     */\n    private object RetentionPeriodComputer extends IcebergPropertiesComputer {\n      override def apply(deltaProperties: Map[String, String]): Map[String, String] = {\n        def getAsMilliSeconds(conf: DeltaConfig[CalendarInterval],\n                              properties: Map[String, String],\n                              useDefault: Boolean = false): Option[Long] =\n          properties.get(conf.key)\n            .orElse(if (useDefault) Some(conf.defaultValue) else None)\n            .map(conf.fromString)\n            .map(DeltaConfigs.getMilliSeconds)\n\n        // Set Iceberg max snapshot age as minimal of Delta log retention and tombstone retention\n        val deltaRetention = (\n          getAsMilliSeconds(LOG_RETENTION, deltaProperties),\n          getAsMilliSeconds(TOMBSTONE_RETENTION, deltaProperties)\n        ) match {\n          case (Some(a), Some(b)) => Some(a min b)\n          case (a, b) => a orElse b\n        }\n\n        // If user provided max snapshot age, check that it is smaller than Delta's retention\n        lazy val maxAllowedRetention =\n          getAsMilliSeconds(LOG_RETENTION, deltaProperties, useDefault = true).get min\n          getAsMilliSeconds(TOMBSTONE_RETENTION, deltaProperties, useDefault = true).get\n\n        deltaProperties.get(IcebergTableProperties.MAX_SNAPSHOT_AGE_MS)\n          .foreach { providedRetention =>\n            if (providedRetention.toLong > maxAllowedRetention) {\n              throw new IllegalArgumentException(\n                s\"\"\"Uniform iceberg's ${IcebergTableProperties.MAX_SNAPSHOT_AGE_MS} should be\n                    | no less than the min of delta's ${LOG_RETENTION.key} and\n                    | ${TOMBSTONE_RETENTION.key}.\n                    | Current delta retention min in MS: $maxAllowedRetention.\n                    | Proposed iceberg retention in Ms: $providedRetention\"\"\".stripMargin)\n            }\n          }\n\n        deltaRetention\n          .filter(_ < IcebergTableProperties.MAX_SNAPSHOT_AGE_MS_DEFAULT)\n          .map { IcebergTableProperties.MAX_SNAPSHOT_AGE_MS -> _.toString }\n          .toMap\n      }\n    }\n  }\n\n  object Partition {\n\n    private[delta] def convertPartitionValues(\n        snapshot: Snapshot,\n        partitionSpec: PartitionSpec,\n        partitionValues: Map[String, String],\n        logicalToPhysicalPartitionNames: Map[String, String]): StructLike = {\n      val schema = snapshot.schema\n      val ICEBERG_NULL_PARTITION_VALUE = \"__HIVE_DEFAULT_PARTITION__\"\n      val partitionPath = partitionSpec.fields()\n      val partitionVals = new Array[Any](partitionSpec.fields().size())\n      val nameToDataTypes: Map[String, DataType] =\n        schema.fields.map(f => f.name -> f.dataType).toMap\n      for (i <- partitionVals.indices) {\n        val logicalPartCol = partitionPath.get(i).name()\n        val physicalPartKey = logicalToPhysicalPartitionNames(logicalPartCol)\n        // ICEBERG_NULL_PARTITION_VALUE is referred in Iceberg lib to mark NULL partition value\n        val partValue = Option(partitionValues.getOrElse(physicalPartKey, null))\n          .getOrElse(ICEBERG_NULL_PARTITION_VALUE)\n        val partitionColumnDataType = nameToDataTypes(logicalPartCol)\n        val icebergPartitionValue =\n          IcebergTransactionUtils.stringToIcebergPartitionValue(\n            partitionColumnDataType, partValue, snapshot.version)\n        partitionVals(i) = icebergPartitionValue\n      }\n      new IcebergTransactionUtils.Row(partitionVals)\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/icebergShaded/IcebergConversionTransaction.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.icebergShaded\n\nimport java.util.ConcurrentModificationException\nimport java.util.function.Consumer\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.jdk.OptionConverters._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeltaFileProviderUtils, DummySnapshot, IcebergConstants, NoMapping, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, RemoveFile}\nimport org.apache.spark.sql.delta.icebergShaded.IcebergTransactionUtils._\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.conf.Configuration\nimport shadedForDelta.org.apache.iceberg.{AppendFiles, BaseTransaction, DataFile, DeleteFiles, ExpireSnapshots, OverwriteFiles, PartitionSpec, PendingUpdate, RewriteFiles, Schema => IcebergSchema, TableMetadata, Transaction => IcebergTransaction}\nimport shadedForDelta.org.apache.iceberg.MetadataUpdate\nimport shadedForDelta.org.apache.iceberg.MetadataUpdate.{AddPartitionSpec, AddSchema}\nimport shadedForDelta.org.apache.iceberg.mapping.MappingUtil\nimport shadedForDelta.org.apache.iceberg.mapping.NameMappingParser\nimport shadedForDelta.org.apache.iceberg.unityCatalog.{UnityCatalog, UnityCatalogTableOperations}\nimport shadedForDelta.org.apache.iceberg.util.LocationUtil\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\nsealed trait IcebergTableOp\ncase object CREATE_TABLE extends IcebergTableOp\ncase object WRITE_TABLE extends IcebergTableOp\ncase object REPLACE_TABLE extends IcebergTableOp\n\nsealed trait IcebergConversionMode  {\n}\n// Used by Post-commit Delta UniForm (Iceberg conversion in Delta post commit hook)\ncase object UNIFORM_POST_COMMIT_MODE extends IcebergConversionMode {\n}\n// Used by atomic Delta UniForm\ncase object UNIFORM_CC_MODE extends IcebergConversionMode {\n}\n/**\n * Used to prepare (convert) and then commit a set of Delta actions into the Iceberg table located\n * at the same path as [[postCommitSnapshot]]\n *\n *\n * @param conf Configuration for Iceberg Hadoop interactions.\n * @param postCommitSnapshot Latest Delta snapshot associated with this Iceberg commit.\n * @param tableOp How to instantiate the underlying Iceberg table. Defaults to WRITE_TABLE.\n * @param lastConvertedIcebergSnapshotId the iceberg snapshot this Iceberg txn should write to.\n * @param lastConvertedDeltaVersion the delta version this Iceberg txn starts from.\n */\nclass IcebergConversionTransaction(\n    protected val spark: SparkSession,\n    protected val catalogTable: CatalogTable,\n    protected val conf: Configuration,\n    protected val postCommitSnapshot: Snapshot,\n    protected val tableOp: IcebergTableOp = WRITE_TABLE,\n    protected val lastConvertedIcebergSnapshotId: Option[Long] = None,\n    protected val lastConvertedDeltaVersion: Option[Long] = None,\n    protected val lastConvertedIcebergMetadataPath: Option[String] = None,\n    protected val metadataUpdates: java.util.ArrayList[MetadataUpdate] =\n      new java.util.ArrayList[MetadataUpdate]()\n    ) extends DeltaLogging {\n\n  ///////////////////////////\n  // Nested Helper Classes //\n  ///////////////////////////\n\n  implicit class AddFileConversion(addFile: AddFile) {\n    def toDataFile: DataFile =\n      convertDeltaAddFileToIcebergDataFile(\n        addFile,\n        tablePath,\n        currentPartitionSpec,\n        logicalToPhysicalPartitionNames,\n        statsParser,\n        postCommitSnapshot)\n  }\n\n  implicit class RemoveFileConversion(removeFile: RemoveFile) {\n    def toDataFile: DataFile =\n      convertDeltaRemoveFileToIcebergDataFile(\n        removeFile,\n        tablePath,\n        currentPartitionSpec,\n        logicalToPhysicalPartitionNames,\n        postCommitSnapshot)\n  }\n\n  protected abstract class TransactionHelper(protected val impl: PendingUpdate[_]) {\n    protected var committed = false\n    var writeSize = 0L\n\n    def opType: String\n\n    def add(add: AddFile): Unit = throw new UnsupportedOperationException\n    def add(remove: RemoveFile): Unit = throw new UnsupportedOperationException\n\n    def commit(expectedSequenceNumber: Long): Unit = {\n      assert(!committed, \"Already committed.\")\n      impl.commit()\n      committed = true\n    }\n\n    private[icebergShaded]def hasCommitted: Boolean = committed\n\n    protected def currentSnapshotId: Option[Long] =\n      Option(txn.table().currentSnapshot()).map(_.snapshotId())\n  }\n\n  class NullHelper extends TransactionHelper(null) {\n    override def opType: String = \"null\"\n    override def add(add: AddFile): Unit = {}\n    override def add(remove: RemoveFile): Unit = {}\n    override def commit(deltaCommitVersion: Long): Unit = {}\n  }\n  /**\n   * API for appending new files in a table.\n   *\n   * e.g. INSERT\n   */\n  class AppendOnlyHelper(appender: AppendFiles) extends TransactionHelper(appender) {\n\n    override def opType: String = \"append\"\n\n    override def add(add: AddFile): Unit = {\n      writeSize += add.size\n      appender.appendFile(add.toDataFile)\n    }\n  }\n\n  /**\n   * API for deleting files from a table.\n   *\n   * e.g. DELETE\n   */\n  class RemoveOnlyHelper(deleter: DeleteFiles) extends TransactionHelper(deleter) {\n\n    override def opType: String = \"delete\"\n\n    override def add(remove: RemoveFile): Unit = {\n      // We can just use the canonical RemoveFile.path instead of converting RemoveFile to DataFile.\n      // Note that in other helper APIs, converting a FileAction to a DataFile will also take care\n      // of canonicalizing the path.\n      deleter.deleteFile(canonicalizeFilePath(remove, tablePath))\n    }\n  }\n\n  /**\n   * API for overwriting files in a table. Replaces all the deleted files with the set of additions.\n   *\n   * e.g. UPDATE, MERGE\n   */\n  class OverwriteHelper(overwriter: OverwriteFiles) extends TransactionHelper(overwriter) {\n\n    override def opType: String = \"overwrite\"\n\n    override def add(add: AddFile): Unit = {\n      writeSize += add.size\n      overwriter.addFile(add.toDataFile)\n    }\n\n    override def add(remove: RemoveFile): Unit = {\n      overwriter.deleteFile(remove.toDataFile)\n    }\n  }\n\n  /**\n   * API for rewriting existing files in the table (i.e. replaces one set of data files with another\n   * set that contains the same data).\n   *\n   * e.g. OPTIMIZE\n   */\n  class RewriteHelper(rewriter: RewriteFiles) extends TransactionHelper(rewriter) {\n\n    override def opType: String = \"rewrite\"\n\n    private val addBuffer: mutable.HashSet[DataFile] = new mutable.HashSet[DataFile]\n    private val removeBuffer: mutable.HashSet[DataFile] = new mutable.HashSet[DataFile]\n\n    override def add(add: AddFile): Unit = {\n      writeSize += add.size\n      assert(!add.dataChange, \"Rewrite operation should not add data\")\n      addBuffer += add.toDataFile\n    }\n\n    override def add(remove: RemoveFile): Unit = {\n      assert(!remove.dataChange, \"Rewrite operation should not add data\")\n      removeBuffer += remove.toDataFile\n    }\n\n    override def commit(deltaCommitVersion: Long): Unit = {\n      if (removeBuffer.nonEmpty) {\n        rewriter.rewriteFiles(removeBuffer.asJava, addBuffer.asJava, 0)\n      }\n      currentSnapshotId.foreach(rewriter.validateFromSnapshot)\n      super.commit(deltaCommitVersion)\n    }\n  }\n\n  class ExpireSnapshotHelper(expireSnapshot: ExpireSnapshots)\n      extends TransactionHelper(expireSnapshot) {\n\n    def cleanExpiredFiles(clean: Boolean): ExpireSnapshotHelper = {\n      expireSnapshot.cleanExpiredFiles(clean)\n      this\n    }\n\n    def deleteWith(newDeleteFunc: Consumer[String]): ExpireSnapshotHelper = {\n      expireSnapshot.deleteWith(newDeleteFunc)\n      this\n    }\n\n    override def opType: String = \"expireSnapshot\"\n  }\n\n  //////////////////////\n  // Member variables //\n  //////////////////////\n\n  protected val tablePath = postCommitSnapshot.deltaLog.dataPath\n\n  protected val convert = new DeltaToIcebergConverter(postCommitSnapshot, catalogTable)\n\n  protected def icebergSchema: IcebergSchema = convert.schema\n\n  // Initial partition spec converted from Delta\n  protected def partitionSpec: PartitionSpec = convert.partition\n\n  // Current partition spec from iceberg table\n  def currentPartitionSpec: PartitionSpec = {\n    Some(txn.table()).map(_.spec()).getOrElse(partitionSpec)\n  }\n\n  protected val logicalToPhysicalPartitionNames =\n    getPartitionPhysicalNameMapping(postCommitSnapshot.metadata.partitionSchema)\n\n  /** Parses the stats JSON string to convert Delta stats to Iceberg stats. */\n  private val statsParser =\n    DeltaFileProviderUtils.createJsonStatsParser(postCommitSnapshot.statsSchema)\n\n  /** Visible for testing. */\n  private[icebergShaded]val (txn, startFromSnapshotId) = withStartSnapshotId(createIcebergTxn())\n\n  /** Tracks if this transaction has already committed. You can only commit once. */\n  private var committed = false\n\n  /** Tracks the file updates (add, remove, overwrite, rewrite) made to this table. */\n  protected val fileUpdates = new ArrayBuffer[TransactionHelper]()\n\n  /** Tracks if this transaction updates only the differences between a prev and new metadata. */\n  private var isMetadataUpdate = false\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n  def getNullHelper: NullHelper = new NullHelper()\n\n  def getAppendOnlyHelper: AppendOnlyHelper = {\n    val ret = new AppendOnlyHelper(txn.newAppend())\n    fileUpdates += ret\n    ret\n  }\n\n  def getRemoveOnlyHelper: RemoveOnlyHelper = {\n    val ret = new RemoveOnlyHelper(txn.newDelete())\n    fileUpdates += ret\n    ret\n  }\n\n  def getOverwriteHelper: OverwriteHelper = {\n    val ret = new OverwriteHelper(txn.newOverwrite())\n    fileUpdates += ret\n    ret\n  }\n\n  def getRewriteHelper: RewriteHelper = {\n    val ret = new RewriteHelper(txn.newRewrite())\n    fileUpdates += ret\n    ret\n  }\n\n  def getExpireSnapshotHelper(): ExpireSnapshotHelper = {\n    val ret = new ExpireSnapshotHelper(txn.expireSnapshots())\n    fileUpdates += ret\n    ret\n  }\n\n  /**\n   * Handles the following update scenarios\n   * - partition update -> throws\n   * - schema update -> sets the full new schema\n   * - properties update -> applies only the new properties\n   */\n  def updateTableMetadata(prevMetadata: Metadata): Unit = {\n    assert(!isMetadataUpdate, \"updateTableMetadata already called\")\n    isMetadataUpdate = true\n\n    val newMetadata = postCommitSnapshot.metadata\n\n    // Throws if partition evolution detected\n    if (newMetadata.partitionColumns != prevMetadata.partitionColumns) {\n      throw new IllegalStateException(\"Delta does not support partition evolution\")\n    }\n\n\n    // As we do not have a second set schema txn for REPLACE_TABLE, we need to set\n    // the schema as part of this transaction\n    if (newMetadata.schema != prevMetadata.schema || tableOp == REPLACE_TABLE) {\n      val differenceStr = SchemaUtils.reportDifferences(prevMetadata.schema, newMetadata.schema)\n      logInfo(\n        log\"Detected schema update for table with name=\" +\n        log\"${MDC(DeltaLogKeys.TABLE_NAME, newMetadata.name)}, \" +\n        log\"id=${MDC(DeltaLogKeys.METADATA_ID, newMetadata.id)}:\\n\" +\n        log\"${MDC(DeltaLogKeys.SCHEMA_DIFF, differenceStr)}, \" +\n        s\"tableOp=$tableOp, \" +\n        log\"Setting new Iceberg schema:\\n \" +\n        log\"${MDC(DeltaLogKeys.SCHEMA, icebergSchema)}\"\n      )\n      metadataUpdates.add(new AddSchema(icebergSchema, convert.maxFieldId))\n\n      recordDeltaEvent(\n        postCommitSnapshot.deltaLog,\n        \"delta.iceberg.conversion.schemaChange\",\n        data = Map(\n          \"version\" -> postCommitSnapshot.version,\n          \"deltaSchemaDiff\" -> differenceStr,\n          \"icebergSchema\" -> icebergSchema.toString.replace('\\n', ';')\n        )\n      )\n    }\n\n    // Compute and apply properties changes\n    val (propertyDeletes, propertyAdditions) = {\n      val newIcebergProperties = convert.properties\n      val prevIcebergProperties = new DeltaToIcebergConverter(\n        new DummySnapshot(\n          logPath = postCommitSnapshot.path,\n          deltaLog = postCommitSnapshot.deltaLog,\n          metadata = prevMetadata),\n        catalogTable\n      ).properties\n\n      if (prevIcebergProperties == newIcebergProperties) {\n        (Set.empty, Map.empty)\n      } else {\n        (\n          prevIcebergProperties.keySet.diff(newIcebergProperties.keySet),\n          newIcebergProperties\n        )\n      }\n    }\n\n    if (propertyDeletes.nonEmpty || propertyAdditions.nonEmpty) {\n      val updater = txn.updateProperties()\n      propertyDeletes.foreach(updater.remove)\n      propertyAdditions.foreach(kv => updater.set(kv._1, kv._2))\n      updater.commit()\n\n      recordDeltaEvent(\n        postCommitSnapshot.deltaLog,\n        \"delta.iceberg.conversion.propertyChange\",\n        data = Map(\"version\" -> postCommitSnapshot.version) ++\n          (if (propertyDeletes.nonEmpty) Map(\"deletes\" -> propertyDeletes.toSeq) else Map.empty) ++\n          (if (propertyAdditions.nonEmpty) Map(\"adds\" -> propertyAdditions) else Map.empty)\n      )\n    }\n  }\n\n  def commit(): Unit = {\n    assert(!committed, \"Cannot commit. Transaction already committed.\")\n\n    // At least one file or metadata updates is required when writing to an existing table. If\n    // creating or replacing a table, we can create an empty table with just the table metadata\n    // (schema, properties, etc.)\n    if (tableOp == WRITE_TABLE) {\n      assert(fileUpdates.nonEmpty || isMetadataUpdate, \"Cannot commit WRITE. Transaction is empty.\")\n    }\n    assert(fileUpdates.forall(_.hasCommitted), \"Cannot commit. You have uncommitted changes.\")\n\n    val nameMapping = NameMappingParser.toJson(MappingUtil.create(icebergSchema))\n\n    var updateTxn = txn.updateProperties()\n    updateTxn = updateTxn.set(IcebergConverter.DELTA_VERSION_PROPERTY,\n        postCommitSnapshot.version.toString)\n      .set(IcebergConverter.DELTA_TIMESTAMP_PROPERTY, postCommitSnapshot.timestamp.toString)\n      .set(IcebergConstants.ICEBERG_NAME_MAPPING_PROPERTY, nameMapping)\n\n    val includeBaseVersion = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_UNIFORM_ICEBERG_INCLUDE_BASE_CONVERTED_VERSION)\n    updateTxn = lastConvertedDeltaVersion match {\n      case Some(v) if includeBaseVersion =>\n        updateTxn.set(IcebergConverter.BASE_DELTA_VERSION_PROPERTY, v.toString)\n      case _ =>\n        updateTxn.remove(IcebergConverter.BASE_DELTA_VERSION_PROPERTY)\n    }\n    updateTxn.commit()\n\n    // We ensure the iceberg txns are serializable by only allowing them to commit against\n    // lastConvertedIcebergSnapshotId.\n    //\n    // If the startFromSnapshotId is non-empty and not the same as lastConvertedIcebergSnapshotId,\n    // there is a new iceberg transaction committed after we read lastConvertedIcebergSnapshotId,\n    // and before this check. We explicitly abort by throwing exceptions.\n    //\n    // If startFromSnapshotId is empty, the txn must be one of the following:\n    // 1. CREATE_TABLE\n    // 2. Writing to an empty table\n    // 3. REPLACE_TABLE\n    // In either case this txn is safe to commit.\n    //\n    // Iceberg will further guarantee that txns passed this check are serializable.\n    if (startFromSnapshotId.isDefined && lastConvertedIcebergSnapshotId != startFromSnapshotId) {\n      throw new ConcurrentModificationException(\"Cannot commit because the converted \" +\n        s\"metadata is based on a stale iceberg snapshot $lastConvertedIcebergSnapshotId\"\n      )\n    }\n    try {\n      // Iceberg CREATE_TABLE reassigns the field id in schema, which\n      // is overwritten by setting Delta schema with Delta generated field id to ensure\n      // consistency between field id in Iceberg schema after conversion and field id in\n      // parquet files written by Delta.\n      if (tableOp == CREATE_TABLE) {\n        metadataUpdates.add(\n          new AddSchema(icebergSchema, postCommitSnapshot.metadata.columnMappingMaxId.toInt)\n        )\n        if (postCommitSnapshot.metadata.partitionColumns.nonEmpty) {\n          metadataUpdates.add(\n            new AddPartitionSpec(partitionSpec)\n          )\n        }\n      }\n      txn.commitTransaction()\n      recordIcebergCommit()\n    } catch {\n      case NonFatal(e) =>\n        recordIcebergCommit(Some(e))\n        throw e\n    }\n\n    committed = true\n  }\n\n  /**\n   * Retrieves the converted Iceberg metadata location and its current snapshot.\n   * This method should only be called after a successful table conversion operation\n   *\n   * @return A tuple containing:\n   *         - String: The path where the Iceberg metadata file was written\n   *         - IcebergMetadata: The converted Iceberg metadata\n   * @throws IllegalStateException if the Iceberg metadata has not been converted\n   * @throws UnsupportedOperationException if called on non-UnityCatalogTableOperations\n   */\n  def getConvertedIcebergMetadata: (String, TableMetadata) =\n    txn.asInstanceOf[BaseTransaction].underlyingOps() match {\n      case ops: UnityCatalogTableOperations =>\n        ops.getLastWrittenTableMetadataWithLocation.toScala match {\n          case Some((metadataPath, tableMetadata)) =>\n            (metadataPath, tableMetadata)\n          case _ => throw new IllegalStateException(\n            \"Could not get converted Iceberg metadata: new written metadata not found\")\n        }\n      case _ =>\n        throw new IllegalStateException(\n          \"Could not get converted Iceberg metadata:\" +\n            \" underlying UnityCatalogTableOperations not found\"\n        )\n    }\n\n  ///////////////////////\n  // Protected Methods //\n  ///////////////////////\n\n  protected def createIcebergTxn(tableOpOpt: Option[IcebergTableOp] = None):\n      IcebergTransaction = {\n    val baseMetadataPath =\n      (tableOpOpt.getOrElse(tableOp), lastConvertedIcebergMetadataPath) match {\n        case (CREATE_TABLE, None) => None\n        case (CREATE_TABLE, Some(_)) =>\n          throw new IllegalStateException(\n            \"Unexpected base metadata path for CREATE_TABLE operation\")\n        case (op, None) =>\n          throw new IllegalStateException(s\"Missing base metadata path for $op operation\")\n        case (_, Some(path)) => Some(path)\n      }\n\n    val ucTable = new UnityCatalog(\n      metadataUpdates,\n      baseMetadataPath.toJava\n    )\n    ucTable.initialize(null, new java.util.HashMap[String, String]())\n    ucTable.setConf(conf)\n\n    val icebergIdentifier =\n      IcebergTransactionUtils.convertSparkTableIdentifierToIceberg(catalogTable.identifier)\n\n    val tableExists = ucTable.tableExists(icebergIdentifier)\n\n    def tableBuilder = {\n      val tableLocation = postCommitSnapshot.deltaLog.dataPath.toString\n      ucTable\n        .buildTable(icebergIdentifier, icebergSchema)\n        .withPartitionSpec(partitionSpec)\n        .withProperties(convert.properties.asJava)\n        .withLocation(tableLocation)\n    }\n\n    val txn = tableOpOpt.getOrElse(tableOp) match {\n      case WRITE_TABLE =>\n        if (tableExists) {\n          recordFrameProfile(\"IcebergConversionTransaction\", \"loadTable\") {\n            ucTable.loadTable(icebergIdentifier).newTransaction()\n          }\n        } else {\n          throw new IllegalStateException(s\"Cannot write to table $tablePath. Table doesn't exist.\")\n        }\n      case CREATE_TABLE =>\n        if (tableExists) {\n          throw new IllegalStateException(s\"Cannot create table $tablePath. Table already exists.\")\n        } else {\n          recordFrameProfile(\"IcebergConversionTransaction\", \"createTable\") {\n            tableBuilder.createTransaction()\n          }\n        }\n      case REPLACE_TABLE =>\n        if (tableExists) {\n          recordFrameProfile(\"IcebergConversionTransaction\", \"replaceTable\") {\n            tableBuilder.replaceTransaction()\n          }\n        } else {\n          throw new IllegalStateException(s\"Cannot replace table $tablePath. Table doesn't exist.\")\n        }\n    }\n    txn\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  /**\n   * We fetch the txn table's current snapshot id before any writing is made on the transaction.\n   * This id should equal [[lastConvertedIcebergSnapshotId]] for the transaction to commit.\n   *\n   * @param txn the iceberg transaction\n   * @return txn and the snapshot id just before this txn\n   */\n  private def withStartSnapshotId(txn: IcebergTransaction): (IcebergTransaction, Option[Long]) =\n    (txn, Option(txn.table().currentSnapshot()).map(_.snapshotId()))\n\n  private def recordIcebergCommit(errorOpt: Option[Throwable] = None): Unit = {\n    val icebergTxnTypes =\n      if (fileUpdates.nonEmpty) Map(\"icebergTxnTypes\" -> fileUpdates.map(_.opType)) else Map.empty\n\n    val errorData = errorOpt.map { e =>\n      Map(\n        \"exception\" -> ExceptionUtils.getMessage(e),\n        \"stackTrace\" -> ExceptionUtils.getStackTrace(e)\n      )\n    }.getOrElse(Map.empty)\n\n\n    recordDeltaEvent(\n      postCommitSnapshot.deltaLog,\n      s\"delta.iceberg.conversion.commit.${if (errorOpt.isEmpty) \"success\" else \"error\"}\",\n      data = Map(\n        \"version\" -> postCommitSnapshot.version,\n        \"timestamp\" -> postCommitSnapshot.timestamp,\n        \"tableOp\" -> tableOp.getClass.getSimpleName.stripSuffix(\"$\"),\n        \"prevConvertedDeltaVersion\" -> lastConvertedDeltaVersion,\n        \"tableSize\" -> postCommitSnapshot.sizeInBytes,\n        \"commitWriteSize\" -> fileUpdates.map(_.writeSize).sum\n      ) ++ icebergTxnTypes ++ errorData\n    )\n  }\n\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/icebergShaded/IcebergConverter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.icebergShaded\n\nimport java.util.concurrent.atomic.AtomicReference\nimport javax.annotation.concurrent.GuardedBy\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.control.Breaks._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{CommittedTransaction, CurrentTransactionInfo, DeltaErrors, DeltaFileNotFoundException, DeltaFileProviderUtils, DeltaLog, DeltaOperations, DummySnapshot, IcebergCompat, IcebergConstants, Snapshot, SnapshotDescriptor, UniversalFormat, UniversalFormatConverter}\nimport org.apache.spark.sql.delta.DeltaOperations.OPTIMIZE_OPERATION_NAME\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, CommitInfo, DomainMetadata, FileAction, InMemoryLogReplay, Metadata, Protocol, RemoveFile}\nimport org.apache.spark.sql.delta.hooks.IcebergConverterHook\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.TransactionHelper\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.fs.Path\nimport shadedForDelta.org.apache.iceberg.{Table => IcebergTable, TableProperties}\nimport shadedForDelta.org.apache.iceberg.exceptions.CommitFailedException\nimport shadedForDelta.org.apache.iceberg.hadoop.HadoopTables\nimport shadedForDelta.org.apache.iceberg.hive.{HiveCatalog, HiveTableOperations}\nimport shadedForDelta.org.apache.iceberg.util.LocationUtil\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\nobject IcebergConverter {\n\n\n  /**\n   * Property to be set in translated Iceberg metadata files.\n   * Indicates the delta commit version # that it corresponds to.\n   */\n  val DELTA_VERSION_PROPERTY = \"delta-version\"\n\n  /**\n   * Property to be set in translated Iceberg metadata files.\n   * Indicates the timestamp (milliseconds) of the delta commit that it corresponds to.\n   */\n  val DELTA_TIMESTAMP_PROPERTY = \"delta-timestamp\"\n\n  /**\n   * Property to be set in translated Iceberg metadata files.\n   * Indicates the base delta commit version # that the conversion started from\n   */\n  val BASE_DELTA_VERSION_PROPERTY = \"base-delta-version\"\n\n  def getLastConvertedDeltaVersion(table: Option[IcebergTable]): Option[Long] =\n    table.flatMap(_.properties().asScala.get(DELTA_VERSION_PROPERTY)).map(_.toLong)\n\n  def getLastConvertedDeltaTimestamp(table: Option[IcebergTable]): Option[Long] =\n    table.flatMap(_.properties().asScala.get(DELTA_TIMESTAMP_PROPERTY)).map(_.toLong)\n}\n\n\n/**\n * This class manages the transformation of delta snapshots into their Iceberg equivalent.\n */\nclass IcebergConverter\n  extends UniversalFormatConverter\n  with DeltaLogging {\n\n  // Save an atomic reference of the snapshot being converted, and the txn that triggered\n  // resulted in the specified snapshot\n  protected val currentConversion =\n    new AtomicReference[(Snapshot, CommittedTransaction)]()\n  protected val standbyConversion =\n    new AtomicReference[(Snapshot, CommittedTransaction)]()\n\n  // Whether our async converter thread is active. We may already have an alive thread that is\n  // about to shutdown, but in such cases this value should return false.\n  @GuardedBy(\"asyncThreadLock\")\n  private var asyncConverterThreadActive: Boolean = false\n  private val asyncThreadLock = new Object\n\n  private[icebergShaded]var targetSnapshot: SnapshotDescriptor = _\n  /**\n   * Enqueue the specified snapshot to be converted to Iceberg. This will start an async\n   * job to run the conversion, unless there already is an async conversion running for\n   * this table. In that case, it will queue up the provided snapshot to be run after\n   * the existing job completes.\n   * Note that if there is another snapshot already queued, the previous snapshot will get\n   * removed from the wait queue. Only one snapshot is queued at any point of time.\n   *\n   */\n  override def enqueueSnapshotForConversion(\n      snapshotToConvert: Snapshot,\n      txn: CommittedTransaction): Unit = {\n    throw new IllegalStateException(\"enqueueSnapshotForConversion is no longer supported\")\n  }\n\n  /**\n   * Convert the specified snapshot into Iceberg for the given catalogTable\n   * @param snapshotToConvert the snapshot that needs to be converted to Iceberg\n   * @param catalogTable the catalogTable this conversion targets.\n   * @return Converted Delta version and commit timestamp\n   */\n  override def convertSnapshot(\n      snapshotToConvert: Snapshot, catalogTable: CatalogTable): Option[(Long, Long)] = {\n    throw new IllegalStateException(\"convertSnapshot is no longer supported\")\n  }\n\n  /**\n   * Convert the specified snapshot into Iceberg when performing an OptimisticTransaction\n   * on a delta table.\n   * @param snapshotToConvert the snapshot that needs to be converted to Iceberg\n   * @param txn               the transaction that triggers the conversion. It must\n   *                          contain the catalogTable this conversion targets.\n   * @return Converted Delta version and commit timestamp\n   */\n  override def convertSnapshot(\n      snapshotToConvert: Snapshot, txn: CommittedTransaction): Option[(Long, Long)] = {\n    throw new IllegalStateException(\"convertSnapshot is no longer supported\")\n  }\n\n\n  // Used for tracking last converted Iceberg metadata information\n  // It would be used for incremental conversion for all Iceberg conversion modes\n  protected case class LastConvertedIcebergInfo(\n    icebergTable: Option[IcebergTable],\n    icebergSnapshotId: Option[Long],\n    deltaVersionConverted: Option[Long],\n    baseMetadataLocationOpt: Option[String]\n  )\n\n  /**\n   * Used for tracking Iceberg Conversion Context by Conversion Mode\n   * UNIFORM_POST_COMMIT_MODE => no context required\n   * @param conversionMode\n   * @param additionalDeltaActionsToCommit\n   */\n  protected class ConversionContext(\n    val conversionMode: IcebergConversionMode,\n    val additionalDeltaActionsToCommit: Option[Seq[Action]],\n    val opType: String\n  ) {\n    validate()\n    // Validation on parameters\n    def validate(): Unit = {\n      conversionMode match {\n        case _ =>\n          assert(additionalDeltaActionsToCommit.isEmpty)\n      }\n    }\n\n    def hasAdditionalDeltaActionsToCommit: Boolean = {\n      additionalDeltaActionsToCommit.nonEmpty\n    }\n\n    def getAdditionalDeltaActionsToCommit: Seq[Action] = {\n      additionalDeltaActionsToCommit.get\n    }\n  }\n\n\n  /**\n   * The core implementation of convertSnapshot\n   * 'delta.iceberg.conversion.convertSnapshot' ->\n   *    Convert Iceberg Metadata for a complete snapshot. Used for conversion\n   *    after delta commits and in create table\n   */\n  protected def convertSnapshotInternal(\n      snapshotToConvert: Snapshot,\n      readSnapshotOpt: Option[Snapshot],\n      lastConvertedInfo: LastConvertedIcebergInfo,\n      conversionContext: ConversionContext,\n      catalogTable: CatalogTable\n  ): IcebergConversionTransaction =\n    recordFrameProfile(\"Delta\", \"IcebergConverter.convertSnapshotImpl\") {\n      val conversionMode = conversionContext.conversionMode\n      val log = snapshotToConvert.deltaLog\n      targetSnapshot = snapshotToConvert\n      val lastConvertedIcebergTable = lastConvertedInfo.icebergTable\n      val lastConvertedIcebergSnapshotId = lastConvertedInfo.icebergSnapshotId\n      val lastDeltaVersionConverted = lastConvertedInfo.deltaVersionConverted\n      val baseMetadataLocation = lastConvertedInfo.baseMetadataLocationOpt\n      val maxCommitsToConvert =\n        spark.sessionState.conf.getConf(DeltaSQLConf.ICEBERG_MAX_COMMITS_TO_CONVERT)\n\n      val conversionStartTime = System.currentTimeMillis()\n      val prevConvertedSnapshotOpt = (lastDeltaVersionConverted, readSnapshotOpt) match {\n        // The provided Snapshot is the last converted Snapshot\n        case (Some(version), Some(readSnapshot)) if version == readSnapshot.version =>\n          Some(readSnapshot)\n        // Some snapshots are pending conversion since last conversion\n        case (Some(version), _) if snapshotToConvert.version - version <= maxCommitsToConvert =>\n          try {\n            Some(log.getSnapshotAt(version, catalogTableOpt = Some(catalogTable)))\n          } catch {\n            // If we can't load the file since the last time Iceberg was converted, it's likely that\n            // the commit file expired. Treat this like a new Iceberg table conversion.\n            case _: DeltaFileNotFoundException => None\n          }\n        // Never converted before\n        case _ => None\n      }\n\n      val tableOp = (lastDeltaVersionConverted, prevConvertedSnapshotOpt) match {\n        case (Some(_), Some(_)) => WRITE_TABLE\n        case (Some(_), None) => REPLACE_TABLE\n        case (None, None) => CREATE_TABLE\n      }\n\n      val icebergTxn = new IcebergConversionTransaction(\n        spark, catalogTable, log.newDeltaHadoopConf(), snapshotToConvert, tableOp,\n        lastConvertedIcebergSnapshotId, lastDeltaVersionConverted\n      )\n\n      val convertedCommits: Seq[Option[CommitInfo]] = prevConvertedSnapshotOpt match {\n        case Some(prevSnapshot) =>\n          // Read the actions directly from the delta json files.\n          // TODO: Run this as a spark job on executors\n          val endVersion = conversionMode match {\n            case _ => snapshotToConvert.version\n          }\n          val deltaFiles = DeltaFileProviderUtils.getDeltaFilesInVersionRange(\n            spark = spark,\n            deltaLog = log,\n            startVersion = prevSnapshot.version + 1,\n            endVersion = endVersion,\n            catalogTableOpt = Some(catalogTable))\n\n          recordDeltaEvent(\n            snapshotToConvert.deltaLog,\n            \"delta.iceberg.conversion.deltaCommitRange\",\n            data = Map(\n              \"fromVersion\" -> (prevSnapshot.version + 1),\n              \"toVersion\" -> snapshotToConvert.version,\n              \"numDeltaFiles\" -> deltaFiles.length\n            )\n          )\n\n          val actionsToConvert = DeltaFileProviderUtils.parallelReadAndParseDeltaFilesAsIterator(\n            log, spark, deltaFiles)\n          var deltaVersion = prevSnapshot.version\n          val commitInfos = actionsToConvert.map { actionsIter =>\n            try {\n              deltaVersion += 1\n              runIcebergConversionForActions(\n                icebergTxn,\n                actionsIter.map(Action.fromJson).toSeq,\n                prevConvertedSnapshotOpt,\n                deltaVersion)\n            } finally {\n              actionsIter.close()\n            }\n          }\n          val additionalCommitInfo = if (conversionContext.hasAdditionalDeltaActionsToCommit) {\n            runIcebergConversionForActions(\n              icebergTxn,\n              actionsToCommit = conversionContext.getAdditionalDeltaActionsToCommit,\n              prevConvertedSnapshotOpt,\n              deltaVersion + 1\n            )\n          } else {\n            None\n          }\n          // If the metadata hasn't changed, this will no-op.\n          icebergTxn.updateTableMetadata(prevSnapshot.metadata)\n          commitInfos :+ additionalCommitInfo\n        case None =>\n          // If we don't have a snapshot of the last converted version, get all the AddFiles\n          // (via state reconstruction).\n          // Batch is always disabled but we still want to reuse the event for conversion\n          recordDeltaEvent(\n            snapshotToConvert.deltaLog,\n            \"delta.iceberg.conversion.batch\",\n            data = Map(\n              \"version\" -> snapshotToConvert.version,\n              \"numOfFiles\" -> snapshotToConvert.numOfFiles,\n              \"actionBatchSize\" -> -1, // This param is ignored as batch is deprecated\n              \"numOfPartitions\" -> 1\n            )\n          )\n          runIcebergConversionForActions(\n            icebergTxn,\n            snapshotToConvert.allFiles.toLocalIterator().asScala.toSeq,\n            None,\n            snapshotToConvert.version)\n\n          // Always attempt to update table metadata (schema/properties) for REPLACE_TABLE\n          if (tableOp == REPLACE_TABLE) {\n            icebergTxn.updateTableMetadata(snapshotToConvert.metadata)\n          }\n          Nil\n      }\n\n\n      // OPTIMIZE will trigger snapshot expiration for iceberg table\n      val OPR_TRIGGER_EXPIRE = Set(DeltaOperations.OPTIMIZE_OPERATION_NAME)\n      val needsExpireSnapshot =\n        OPR_TRIGGER_EXPIRE.intersect(convertedCommits.flatten.map(_.operation).toSet).nonEmpty\n      if (needsExpireSnapshot) {\n        logInfo(log\"Committing iceberg snapshot expiration for uniform table \" +\n          log\"[path = ${MDC(DeltaLogKeys.PATH, log.logPath)}] tableId=\" +\n          log\"${MDC(DeltaLogKeys.TABLE_ID, log.unsafeVolatileTableId)}]\")\n        expireIcebergSnapshot(snapshotToConvert, icebergTxn)\n      }\n\n      icebergTxn.commit()\n      logInfo(s\"icebergTxn committed for table ${Option(catalogTable).map(_.identifier)} \" +\n        s\"with converted delta version ${snapshotToConvert.version}\")\n\n      recordDeltaEvent(\n        snapshotToConvert.deltaLog,\n        conversionContext.opType,\n        data = Map(\n          \"deltaVersion\" -> snapshotToConvert.version,\n          \"compatVersion\" -> IcebergCompat.getEnabledVersion(snapshotToConvert.metadata)\n            .getOrElse(0),\n          \"elapsedTimeMs\" -> (System.currentTimeMillis() - conversionStartTime)\n        )\n      )\n\n      icebergTxn\n    }\n\n  /**\n   * Helper function to execute and commit Iceberg snapshot expiry\n   * @param snapshotToConvert the Delta snapshot that needs to be converted to Iceberg\n   * @param icebergTxn the IcebergConversionTransaction created in convertSnapshot, used\n   *                   to create a table object and expiration helper\n   */\n  private def expireIcebergSnapshot(\n      snapshotToConvert: Snapshot,\n      icebergTxn: IcebergConversionTransaction): Unit = {\n    val expireSnapshotHelper = icebergTxn.getExpireSnapshotHelper()\n    val table = icebergTxn.txn.table()\n    val tableLocation = LocationUtil.stripTrailingSlash(table.location)\n    val defaultWriteMetadataLocation = s\"$tableLocation/metadata\"\n    val writeMetadataLocation = LocationUtil.stripTrailingSlash(\n      table.properties().getOrDefault(\n        TableProperties.WRITE_METADATA_LOCATION, defaultWriteMetadataLocation))\n\n    val shouldKeepPhysicalFiles =\n      // Don't attempt any file cleanup in the edge-case configuration\n      // that the data location (in Uniform the table root location)\n      // is the same as the Iceberg metadata location\n      (snapshotToConvert.path.toString == writeMetadataLocation)\n    if (shouldKeepPhysicalFiles) {\n      expireSnapshotHelper.cleanExpiredFiles(false)\n    } else {\n      expireSnapshotHelper.deleteWith(path => {\n        if (path.startsWith(writeMetadataLocation)) {\n          table.io().deleteFile(path)\n        }\n      })\n    }\n\n    expireSnapshotHelper.commit(snapshotToConvert.version)\n  }\n\n  // This is for newly enabling uniform table to\n  // start a new history line for iceberg metadata\n  // so that if a uniform table is corrupted,\n  // user can unset and re-enable to unblock\n  private def cleanCatalogTableIfEnablingUniform(\n      table: CatalogTable,\n      snapshotToConvert: Snapshot,\n      txnOpt: Option[CommittedTransaction]): CatalogTable = {\n    val disabledIceberg = txnOpt.map(txn =>\n      !UniversalFormat.icebergEnabled(txn.readSnapshot.metadata)\n    ).getOrElse(!UniversalFormat.icebergEnabled(table.properties))\n    val enablingUniform =\n      disabledIceberg && UniversalFormat.icebergEnabled(snapshotToConvert.metadata)\n    if (enablingUniform) {\n      clearDeltaUniformMetadata(table)\n    } else {\n      table\n    }\n  }\n\n  protected def clearDeltaUniformMetadata(table: CatalogTable): CatalogTable = {\n    val metadata_key = IcebergConstants.ICEBERG_TBLPROP_METADATA_LOCATION\n    if (table.properties.contains(metadata_key)) {\n      val cleanedCatalogTable = table.copy(properties = table.properties\n        - metadata_key\n        - IcebergConverter.DELTA_VERSION_PROPERTY\n        - IcebergConverter.DELTA_TIMESTAMP_PROPERTY\n      )\n      spark.sessionState.catalog.alterTable(cleanedCatalogTable)\n      cleanedCatalogTable\n    } else {\n      table\n    }\n  }\n\n  override def loadLastDeltaVersionConverted(\n      snapshot: Snapshot, catalogTable: CatalogTable): Option[Long] =\n    recordFrameProfile(\"Delta\", \"IcebergConverter.loadLastDeltaVersionConverted\") {\n      IcebergConverter.getLastConvertedDeltaVersion(loadIcebergTable(snapshot, catalogTable))\n    }\n\n  protected def loadIcebergTable(\n      snapshot: Snapshot, catalogTable: CatalogTable): Option[IcebergTable] = {\n    recordFrameProfile(\"Delta\", \"IcebergConverter.loadLastConvertedIcebergTable\") {\n      val hiveCatalog = IcebergTransactionUtils\n        .createHiveCatalog(snapshot.deltaLog.newDeltaHadoopConf())\n      val icebergTableId = IcebergTransactionUtils\n        .convertSparkTableIdentifierToIcebergHive(catalogTable.identifier)\n      if (hiveCatalog.tableExists(icebergTableId)) {\n        Some(hiveCatalog.loadTable(icebergTableId))\n      } else {\n        None\n      }\n    }\n  }\n  /**\n   * Commit the set of changes into an Iceberg snapshot. Each call to this function will\n   * build exactly one Iceberg Snapshot.\n   *\n   * We determine what type of [[IcebergConversionTransaction.TransactionHelper]] to use\n   * (and what type of Iceberg snapshot to create) based on the types of actions and\n   * whether they contain data change. An [[UnsupportedOperationException]] will be\n   * thrown for cases not listed in the table below. It means the combination of actions are\n   * not recognized/supported. IcebergConverter will do a re-try with REPLACE TABLE, which\n   * collects all valid data files from the target Delta snapshot and commit to Iceberg.\n   *\n   * Some Delta operations are known to contain only AddFiles(dataChange=false), intended to\n   * replace/overwrite existing AddFiles. They rely on Delta's dedup in state reconstruction\n   * and cannot be action-to-action translated to Iceberg, which lacks dedup abilities.\n   * We create corresponding RemoveFile entries for the AddFiles so these operations can be\n   * properly translated into [[RewriteFiles]] in Iceberg. These operations are marked as\n   * [[needAutoRewrite]] in the code and the table below.\n   *\n   * The following table demonstrates how to choose the appropriate TransactionHelper.\n   * The conditions can overlap and should be checked in order.\n   * +-------------------+---------------+---------------------+--------------------+\n   * |  Type of actions  |  Data Change  |   TransactionHelper | Example / Note     |\n   * +-------------------+---------------+---------------------+--------------------+\n   * |  Create table     |  Any          |   AppendHelper      |  Note 1            |\n   * +-------------------+---------------+---------------------+--------------------+\n   * |                   |  All          |   AppendHelper      |  INSERT            |\n   * |  Add only         +---------------+---------------------+--------------------+\n   * |                   |  None         |   needAutoRewrite   |  Note 2            |\n   * |                   |               |   else              |                    |\n   * |                   |               |       NullHelper    |  Add Tag           |\n   * |                   +---------------+---------------------+--------------------+\n   * |                   |  Some         |   Unsupported       |  (unknown)         |\n   * +-------------------+---------------+---------------------+--------------------+\n   * |  Remove only      |  Any          |   RemoveHelper      |  DELETE            |\n   * +-------------------+---------------+---------------------+--------------------+\n   * |                   |  All          |   OverwriteHelper   |  UPDATE            |\n   * |  Add + Remove     +---------------+---------------------+--------------------+\n   * |                   |  None         |   RewriteHelper     |  OPTIMIZE          |\n   * |                   +---------------+---------------------+--------------------+\n   * |                   |  Some         |   Unsupported       |  (unknown)         |\n   * +-------------------+---------------+---------------------+--------------------+\n   * Note:\n   * 1. We assume a Create/Replace table operation will only contain AddFiles.\n   * 2. DV is allowed but ignored as known operations (ComputeStats) do not touch DV.\n   */\n  private[delta] def runIcebergConversionForActions(\n      icebergTxn: IcebergConversionTransaction,\n      actionsToCommit: Seq[Action],\n      prevSnapshotOpt: Option[SnapshotDescriptor],\n      deltaVersion: Long): Option[CommitInfo] = {\n\n    var commitInfo: Option[CommitInfo] = None\n    var addFiles: Seq[AddFile] = Nil\n    var removeFiles: Seq[RemoveFile] = Nil\n    // Determining what txnHelper to use for this group of Actions requires a full-scan\n    // of [[actionsToCommit]], which is not too expensive as the actions are already in-memory.\n\n    val txnHelper = prevSnapshotOpt match {\n      // Having no previous Snapshot implies that the table is either being created or replaced.\n      // This guarantees that the actions are fetched via [[Snapshot.allFiles]] and are unique.\n      case None =>\n        addFiles = actionsToCommit.asInstanceOf[Seq[AddFile]]\n        if (addFiles.isEmpty) {\n          icebergTxn.getNullHelper\n        } else if (addFiles.exists(_.deletionVector != null)) {\n          throw new UnsupportedOperationException(\"Deletion Vector is not supported\")\n        } else {\n          icebergTxn.getAppendOnlyHelper\n        }\n      case Some(_) =>\n        val addBuffer = new ArrayBuffer[AddFile]()\n        val removeBuffer = new ArrayBuffer[RemoveFile]()\n        // Scan the actions to collect info needed to determine which txnHelper to use\n        object DataChange extends Enumeration {\n          val Empty = Value(0, \"Empty\")\n          val None = Value(1, \"None\")\n          val All = Value(2, \"All\")\n          val Some = Value(3, \"Some\")\n        }\n        var dataChangeBits = 0\n        var hasDv: Boolean = false\n        val autoRewriteOprs = Set(\"COMPUTE STATS\")\n        var needAutoRewrite = false\n\n        actionsToCommit.foreach {\n          case file: FileAction =>\n            addBuffer ++= Option(file.wrap.add)\n            removeBuffer ++= Option(file.wrap.remove)\n            if (file.wrap.add != null || file.wrap.remove != null) {\n              // We only care about data changes in add and remove actions\n              dataChangeBits |= (1 << (if (file.dataChange) 1 else 0))\n            }\n            hasDv |= file.deletionVector != null\n          case c: CommitInfo =>\n            commitInfo = Some(c)\n            needAutoRewrite = autoRewriteOprs.contains(c.operation)\n          case _ => // Ignore other actions\n        }\n        addFiles = addBuffer.toSeq\n        removeFiles = removeBuffer.toSeq\n        val dataChange = DataChange(dataChangeBits)\n\n        (addFiles.nonEmpty, removeFiles.nonEmpty, dataChange) match {\n          case (true, false, DataChange.All) if !hasDv =>\n            icebergTxn.getAppendOnlyHelper\n          case (true, false, DataChange.None) =>\n            if (!needAutoRewrite) {\n              icebergTxn.getNullHelper // Ignore\n            } else {\n              // Create RemoveFiles to refresh these AddFiles without data change\n              removeFiles = addBuffer.map(_.removeWithTimestamp(dataChange = false)).toSeq\n              icebergTxn.getRewriteHelper\n            }\n          case (false, true, _) =>\n            icebergTxn.getRemoveOnlyHelper\n          case (true, true, DataChange.All) if !hasDv =>\n            icebergTxn.getOverwriteHelper\n          case (true, true, DataChange.None) if !hasDv =>\n            icebergTxn.getRewriteHelper\n          case (false, false, _) =>\n            icebergTxn.getNullHelper\n          case _ =>\n            recordDeltaEvent(\n              targetSnapshot.deltaLog,\n              \"delta.iceberg.conversion.unsupportedActions\",\n              data = Map(\n                \"version\" -> targetSnapshot.version,\n                \"commitInfo\" -> commitInfo.map(_.operation).getOrElse(\"\"),\n                \"hasAdd\" -> addFiles.nonEmpty.toString,\n                \"hasRemove\" -> removeFiles.nonEmpty.toString,\n                \"dataChange\" -> dataChange.toString,\n                \"hasDv\" -> hasDv.toString\n              )\n            )\n            logError(\n              s\"\"\"Unsupported combination of actions for incremental conversion. Context:\n                 |version -> ${targetSnapshot.version},\n                 |commitInfo -> ${commitInfo.map(_.operation).getOrElse(\"\")},\n                 |hasAdd -> ${addFiles.nonEmpty.toString},\n                 |hasRemove -> ${removeFiles.nonEmpty.toString},\n                 |dataChange -> ${dataChange.toString},\n                 |hasDv -> ${hasDv.toString}\"\"\".stripMargin)\n            throw new UnsupportedOperationException(\n              \"Unsupported combination of actions for incremental conversion.\")\n        }\n    }\n    recordDeltaEvent(\n      targetSnapshot.deltaLog,\n      \"delta.iceberg.conversion.convertActions\",\n      data = Map(\n        \"version\" -> targetSnapshot.version,\n        \"commitInfo\" -> commitInfo.map(_.operation).getOrElse(\"\"),\n        \"txnHelper\" -> txnHelper.getClass.getSimpleName\n      )\n    )\n\n    removeFiles.foreach(txnHelper.add)\n    addFiles.foreach(txnHelper.add)\n    // Make sure the next snapshot sequence number is deltaVersion\n    txnHelper.commit(deltaVersion)\n\n    commitInfo\n  }\n\n  /**\n   * Validate the Iceberg conversion by comparing the number of files and size in bytes\n   * between the converted Iceberg table and the Delta table.\n   * TODO: throw exception and proactively abort conversion transaction\n   */\n  private def validateIcebergCommit(snapshotToConvert: Snapshot, catalogTable: CatalogTable) = {\n    val table = loadIcebergTable(snapshotToConvert, catalogTable)\n    val lastConvertedDeltaVersion = IcebergConverter.getLastConvertedDeltaVersion(table)\n    table.map {t =>\n      if (lastConvertedDeltaVersion.contains(snapshotToConvert.version) &&\n          t.currentSnapshot() != null) {\n        val icebergNumOfFiles = t.currentSnapshot().summary().asScala\n          .getOrElse(\"total-data-files\", \"-1\").toLong\n        val icebergTotalBytes = t.currentSnapshot().summary().asScala\n          .getOrElse(\"total-files-size\", \"-1\").toLong\n\n        if (icebergNumOfFiles != snapshotToConvert.numOfFiles ||\n            icebergTotalBytes != snapshotToConvert.sizeInBytes) {\n          recordDeltaEvent(\n            snapshotToConvert.deltaLog, \"delta.iceberg.conversion.mismatch\",\n            data = Map(\n              \"lastConvertedDeltaVersion\" -> snapshotToConvert.version,\n              \"numOfFiles\" -> snapshotToConvert.numOfFiles,\n              \"icebergNumOfFiles\" -> icebergNumOfFiles,\n              \"sizeInBytes\" -> snapshotToConvert.sizeInBytes,\n              \"icebergTotalBytes\" -> icebergTotalBytes\n            )\n          )\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/icebergShaded/IcebergSchemaUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.icebergShaded\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, SnapshotDescriptor}\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport shadedForDelta.org.apache.iceberg.{Schema => IcebergSchema}\nimport shadedForDelta.org.apache.iceberg.types.{Type => IcebergType, Types => IcebergTypes}\n\nimport org.apache.spark.sql.types._\n\ntrait IcebergSchemaUtils extends DeltaLogging {\n\n  import IcebergSchemaUtils._\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  // scalastyle:off line.size.limit\n  /**\n   * Delta types are defined here: https://github.com/delta-io/delta/blob/master/PROTOCOL.md#schema-serialization-format\n   *\n   * Iceberg types are defined here: https://iceberg.apache.org/spec/#schemas-and-data-types\n   */\n  // scalastyle:on line.size.limit\n  def convertDeltaSchemaToIcebergSchema(deltaSchema: StructType): IcebergSchema = {\n    val icebergStruct = convertStruct(deltaSchema)\n    new IcebergSchema(icebergStruct.fields())\n  }\n\n  def maxFieldId(snapshot: SnapshotDescriptor): Int\n\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  protected def getFieldId(field: Option[StructField]): Int\n\n  private[delta] def getNestedFieldId(field: Option[StructField], path: Seq[String]): Int\n\n  /** Visible for testing */\n  private[delta] def convertStruct(deltaSchema: StructType)(\n      implicit compatVersion: Int = 0): IcebergTypes.StructType = {\n    /**\n     * Recursively (i.e. for all nested elements) transforms the delta DataType `elem` into its\n     * corresponding Iceberg type.\n     *\n     * - StructType -> IcebergTypes.StructType\n     * - ArrayType -> IcebergTypes.ListType\n     * - MapType -> IcebergTypes.MapType\n     * - primitive -> IcebergType.PrimitiveType\n     */\n    def transform[E <: DataType](elem: E, field: Option[StructField], name: Seq[String])\n        : IcebergType = elem match {\n      case StructType(fields) =>\n        IcebergTypes.StructType.of(fields.map { f =>\n          val icebergField = IcebergTypes.NestedField.of(\n            getFieldId(Some(f)),\n            f.nullable,\n            f.name,\n            transform(f.dataType, Some(f), Seq(DeltaColumnMapping.getPhysicalName(f))),\n            f.getComment().orNull\n          )\n          // Translate column default value\n          if (compatVersion >= 3) {\n            DeltaToIcebergConvert.Schema.extractLiteralDefault(f) match {\n              case Left(errorMsg) =>\n                throw new UnsupportedOperationException(errorMsg)\n              case _ => icebergField\n            }\n          } else {\n            icebergField\n          }\n        }.toList.asJava)\n\n      case ArrayType(elementType, containsNull) =>\n        val currName = name :+ DeltaColumnMapping.PARQUET_LIST_ELEMENT_FIELD_NAME\n        val id = getNestedFieldId(field, currName)\n        if (containsNull) {\n          IcebergTypes.ListType.ofOptional(id, transform(elementType, field, currName))\n        } else {\n          IcebergTypes.ListType.ofRequired(id, transform(elementType, field, currName))\n        }\n\n      case MapType(keyType, valueType, valueContainsNull) =>\n        val currKeyName = name :+ DeltaColumnMapping.PARQUET_MAP_KEY_FIELD_NAME\n        val currValName = name :+ DeltaColumnMapping.PARQUET_MAP_VALUE_FIELD_NAME\n        val keyId = getNestedFieldId(field, currKeyName)\n        val valId = getNestedFieldId(field, currValName)\n        if (valueContainsNull) {\n          IcebergTypes.MapType.ofOptional(\n            keyId,\n            valId,\n            transform(keyType, field, currKeyName),\n            transform(valueType, field, currValName)\n          )\n        } else {\n          IcebergTypes.MapType.ofRequired(\n            keyId,\n            valId,\n            transform(keyType, field, currKeyName),\n            transform(valueType, field, currValName)\n          )\n        }\n\n      case atomicType: AtomicType => convertAtomic(atomicType)\n\n      case other =>\n        throw new UnsupportedOperationException(s\"Cannot convert Delta type $other to Iceberg\")\n    }\n\n    transform(deltaSchema, None, Seq.empty).asStructType()\n  }\n}\n\nobject IcebergSchemaUtils {\n\n  /**\n   * Creates a schema utility for Delta to Iceberg schema conversion.\n   * @param icebergDefaultNameMapping: whether to generate schemas for Iceberg default name mapping,\n   *                                   where the column name is the ground of truth.\n   * @return an Iceberg schema utility.\n   */\n  def apply(icebergDefaultNameMapping: Boolean = false): IcebergSchemaUtils = {\n    if (icebergDefaultNameMapping) new IcebergSchemaUtilsNameMapping()\n    else new IcebergSchemaUtilsIdMapping()\n  }\n\n  private class IcebergSchemaUtilsNameMapping() extends IcebergSchemaUtils {\n\n    // Dummy field ID to support Delta table with NoMapping mode, where logical column name is the\n    // ground of truth and no column Id is available.\n    private var dummyId: Int = 1\n\n    def maxFieldId(snapshot: SnapshotDescriptor): Int = dummyId\n\n    def getFieldId(field: Option[StructField]): Int = {\n      val fieldId = dummyId\n      dummyId += 1\n      fieldId\n    }\n\n    def getNestedFieldId(field: Option[StructField], path: Seq[String]): Int =\n      getFieldId(field)\n  }\n\n  private class IcebergSchemaUtilsIdMapping() extends IcebergSchemaUtils {\n\n    def maxFieldId(snapshot: SnapshotDescriptor): Int =\n      snapshot.metadata.columnMappingMaxId.toInt\n\n    def getFieldId(field: Option[StructField]): Int = {\n      if (!field.exists(f => DeltaColumnMapping.hasColumnId(f))) {\n        throw new UnsupportedOperationException(\"UniForm requires Column Mapping\")\n      }\n\n      DeltaColumnMapping.getColumnId(field.get)\n    }\n\n    def getNestedFieldId(field: Option[StructField], path: Seq[String]): Int = {\n      field.get.metadata\n        .getMetadata(DeltaColumnMapping.COLUMN_MAPPING_METADATA_NESTED_IDS_KEY)\n        .getLong(path.mkString(\".\"))\n        .toInt\n    }\n  }\n\n  /**\n   * Converts delta atomic into an iceberg primitive.\n   *\n   * Visible for testing.\n   *\n   * https://github.com/delta-io/delta/blob/master/PROTOCOL.md#primitive-types\n   */\n  private[delta] def convertAtomic[E <: DataType](elem: E): IcebergType.PrimitiveType = elem match {\n    case StringType => IcebergTypes.StringType.get()\n    case LongType => IcebergTypes.LongType.get()\n    case IntegerType | ShortType | ByteType => IcebergTypes.IntegerType.get()\n    case FloatType => IcebergTypes.FloatType.get()\n    case DoubleType => IcebergTypes.DoubleType.get()\n    case d: DecimalType => IcebergTypes.DecimalType.of(d.precision, d.scale)\n    case BooleanType => IcebergTypes.BooleanType.get()\n    case BinaryType => IcebergTypes.BinaryType.get()\n    case DateType => IcebergTypes.DateType.get()\n    case TimestampType => IcebergTypes.TimestampType.withZone()\n    case TimestampNTZType => IcebergTypes.TimestampType.withoutZone()\n    case _ => throw new UnsupportedOperationException(s\"Could not convert atomic type $elem\")\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/icebergShaded/IcebergStatsConverter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.icebergShaded\n\nimport java.lang.{Long => JLong}\nimport java.nio.ByteBuffer\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping\nimport org.apache.spark.sql.delta.stats.{DeltaStatistics, SkippingEligibleDataType}\nimport shadedForDelta.org.apache.iceberg.types.Conversions\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.types._\nimport org.apache.spark.unsafe.types.UTF8String\n\n/**\n * Converts Delta stats to Iceberg stats given an Internal Row representing Delta stats and the\n * row's schema.\n *\n * Iceberg stores stats as a map from column ID to the statistic. For example, lower/upper bound\n * statistics are represented as a map from column ID to byte buffer where the byte buffer stores\n * any type.\n *\n * For example, given the following Delta stats schema with column IDs:\n * | -- id(0): INT\n * | -- person(1): STRUCT\n *     | name(2): STRUCT\n *         | -- first(3): STRING\n *         | -- last(4): STRING\n *     | height(5): LONG\n *\n * Iceberg's upper bound statistic map will be:\n * {0 -> MAX_ID, 3 -> MAX_FIRST, 4 -> MAX_LAST, 5 -> MAX_HEIGHT}\n *\n * Iceberg requires the \"record count\" stat while the \"upper bounds\", \"lower bounds\", and\n * \"null value counts\" are optional. See iceberg/DataFile.java.\n * Iceberg's \"record count\" metric is set in `convertFileAction` before the stats conversion.\n * If additional metrics are attached to the Iceberg data file, the \"record count\" metric must be\n * left non-null.\n */\ncase class IcebergStatsConverter(statsRow: InternalRow, statsSchema: StructType) {\n\n  val numRecordsStat: JLong = statsSchema.getFieldIndex(DeltaStatistics.NUM_RECORDS) match {\n    case Some(fieldIndex) => new JLong(statsRow.getLong(fieldIndex))\n    case None => throw new IllegalArgumentException(\"Delta is missing the 'num records' stat. \" +\n    \"Iceberg requires this stat when attaching statistics to the output data file.\")\n  }\n\n  val lowerBoundsStat: Option[Map[Integer, ByteBuffer]] =\n    getByteBufferBackedColStats(DeltaStatistics.MIN)\n\n  val upperBoundsStat: Option[Map[Integer, ByteBuffer]] =\n    getByteBufferBackedColStats(DeltaStatistics.MAX)\n\n  val nullValueCountsStat: Option[Map[Integer, JLong]] =\n    statsSchema.getFieldIndex(DeltaStatistics.NULL_COUNT) match {\n      case Some(nullCountFieldIdx) =>\n        val nullCountStatSchema =\n          statsSchema.fields(nullCountFieldIdx).dataType.asInstanceOf[StructType]\n        Some(\n          generateIcebergLongMetricMap(\n            statsRow.getStruct(nullCountFieldIdx, nullCountStatSchema.fields.length),\n            nullCountStatSchema\n          )\n        )\n      case None => None\n    }\n\n  /**\n   * Generates Iceberg's metric representation by recursively flattening the Delta stat struct\n   * (represented as an internal row) and converts the column's physical name to its ID.\n   *\n   * Ignores null Delta stats.\n   *\n   * @param stats An internal row holding the `ByteBuffer`-based Delta column stats\n   *              (i.e. lower bound).\n   * @param statsSchema The schema of the `stats` internal row.\n   * @return Iceberg's ByteBuffer-backed metric representation.\n   */\n  private def generateIcebergByteBufferMetricMap(\n      stats: InternalRow,\n      statsSchema: StructType): Map[Integer, ByteBuffer] = {\n    // If the entire Delta stats struct is missing (for example, min or max values are missing for\n    // all columns), then the stats row may be null.\n    if (stats == null) return Map.empty\n\n    statsSchema.fields.zipWithIndex.flatMap { case (field, idx) =>\n      field.dataType match {\n        // Iceberg statistics cannot be null.\n        case _ if stats.isNullAt(idx) => Map[Integer, ByteBuffer]().empty\n        // If the stats schema contains a struct type, there is a corresponding struct in the data\n        // schema. The struct's per-field stats are also stored in the Delta stats struct. See the\n        // `StatisticsCollection` trait comment for more.\n        case st: StructType =>\n          generateIcebergByteBufferMetricMap(stats.getStruct(idx, st.fields.length), st)\n        // Ignore the Delta statistic if the conversion doesn't support the given data type or the\n        // column ID for this field is missing.\n        case dt if !DeltaColumnMapping.hasColumnId(field) ||\n            !IcebergStatsConverter.isMinMaxStatTypeSupported(dt) => Map[Integer, ByteBuffer]().empty\n        case b: ByteType =>\n          // Iceberg stores bytes using integers.\n          val statVal = stats.getByte(idx).toInt\n          Map[Integer, ByteBuffer](Integer.valueOf(DeltaColumnMapping.getColumnId(field)) ->\n            Conversions.toByteBuffer(IcebergSchemaUtils.convertAtomic(b), statVal))\n        case s: ShortType =>\n          // Iceberg stores shorts using integers.\n          val statVal = stats.getShort(idx).toInt\n          Map[Integer, ByteBuffer](Integer.valueOf(DeltaColumnMapping.getColumnId(field)) ->\n            Conversions.toByteBuffer(IcebergSchemaUtils.convertAtomic(s), statVal))\n        case dt if IcebergStatsConverter.isMinMaxStatTypeSupported(dt) =>\n          val statVal = stats.get(idx, dt)\n\n          // Iceberg's `Conversions.toByteBuffer` method expects the Java object representation\n          // for string and decimal types.\n          // Other types supported by Delta's min/max stat such as int, long, boolean, etc., do not\n          // require a different representation.\n          val compatibleStatsVal = statVal match {\n            case u: UTF8String => u.toString\n            case d: Decimal => d.toJavaBigDecimal\n            case _ => statVal\n          }\n          Map[Integer, ByteBuffer](Integer.valueOf(DeltaColumnMapping.getColumnId(field)) ->\n            Conversions.toByteBuffer(IcebergSchemaUtils.convertAtomic(dt), compatibleStatsVal))\n      }\n    }.toMap\n  }\n\n  /**\n   * Generates Iceberg's metric representation by recursively flattening the Delta stat struct\n   * (represented as an internal row) and converts the column's physical name to its ID.\n   *\n   * @param stats An internal row holding the long-backed Delta column stats (i.e. null counts).\n   * @param statsSchema The schema of the `stats` internal row.\n   * @return a map in Iceberg's metric representation.\n   */\n  private def generateIcebergLongMetricMap(\n      stats: InternalRow,\n      statsSchema: StructType): Map[Integer, JLong] = {\n    // If the entire Delta stats struct is missing, then the Iceberg stats would be empty map.\n    if (stats == null) return Map.empty\n\n    statsSchema.fields.zipWithIndex.flatMap { case (field, idx) =>\n      field.dataType match {\n        // If the stats schema contains a struct type, there is a corresponding struct in the data\n        // schema. The struct's per-field stats are also stored in the Delta stats struct. See the\n        // `StatisticsCollection` trait comment for more.\n        case st: StructType =>\n          generateIcebergLongMetricMap(stats.getStruct(idx, st.fields.length), st)\n        case lt: LongType =>\n          // Skip null values - InternalRow.getLong returns 0 for nulls, which would incorrectly\n          // add 0 to Iceberg stats instead of omitting them\n          if (!stats.isNullAt(idx) && DeltaColumnMapping.hasColumnId(field)) {\n            Map[Integer, JLong](Integer.valueOf(DeltaColumnMapping.getColumnId(field)) ->\n              new JLong(stats.getLong(idx)))\n          } else {\n            Map[Integer, JLong]().empty\n          }\n        case _ => throw new UnsupportedOperationException(\"Expected metric to be a long type.\")\n      }\n    }.toMap\n  }\n\n  /**\n   * @param statName The name of the Delta stat that is being converted. Must be one of the field\n   *                 names in the `DeltaStatistics` object.\n   * @return An option holding Iceberg's statistic representation. Returns `None` if the output\n   *         would otherwise be empty.\n   */\n  private def getByteBufferBackedColStats(statName: String): Option[Map[Integer, ByteBuffer]] = {\n    statsSchema.getFieldIndex(statName) match {\n      case Some(statFieldIdx) =>\n        val colStatSchema = statsSchema.fields(statFieldIdx).dataType.asInstanceOf[StructType]\n        val icebergMetricsMap = generateIcebergByteBufferMetricMap(\n          statsRow.getStruct(statFieldIdx, colStatSchema.fields.length),\n          colStatSchema\n        )\n        if (icebergMetricsMap.nonEmpty) {\n          Some(icebergMetricsMap)\n        } else {\n          // The iceberg metrics map may be empty when all Delta stats are null.\n          None\n        }\n      case None => None\n    }\n  }\n}\n\nobject IcebergStatsConverter {\n  /**\n   * Returns true if a min/max statistic of the given Delta data type can be converted into an\n   * Iceberg metric of equivalent data type.\n   *\n   * Currently, nested types and null types are unsupported.\n   */\n  def isMinMaxStatTypeSupported(dt: DataType): Boolean = {\n    if (!SkippingEligibleDataType(dt)) return false\n\n    dt match {\n      case _: StringType | _: IntegerType | _: FloatType | _: DoubleType |\n        _: DoubleType | _: DecimalType | _: BooleanType | _: DateType | _: TimestampType |\n        // _: LongType TODO: enable after https://github.com/apache/spark/pull/42083 is released\n        _: TimestampNTZType | _: ByteType | _: ShortType => true\n      case _ => false\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/icebergShaded/IcebergTransactionUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.icebergShaded\n\nimport java.nio.ByteBuffer\nimport java.time.Instant\nimport java.time.format.DateTimeParseException\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaErrors, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.PartitionUtils.{timestampPartitionPattern, utcFormatter}\nimport org.apache.spark.sql.delta.util.TimestampFormatter\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport shadedForDelta.org.apache.iceberg.{DataFile, DataFiles, FileFormat, MetadataUpdate, PartitionSpec, Schema => IcebergSchema}\nimport shadedForDelta.org.apache.iceberg.Metrics\nimport shadedForDelta.org.apache.iceberg.StructLike\nimport shadedForDelta.org.apache.iceberg.catalog.{Namespace, TableIdentifier => IcebergTableIdentifier}\nimport shadedForDelta.org.apache.iceberg.hive.HiveCatalog\nimport shadedForDelta.org.apache.iceberg.util.DateTimeUtil\n\nimport org.apache.spark.sql.catalyst.{InternalRow, TableIdentifier => SparkTableIdentifier}\nimport org.apache.spark.sql.types.{BinaryType, BooleanType, ByteType, DataType, DateType, DecimalType, DoubleType, FloatType, IntegerType, LongType, ShortType, StringType, StructType, TimestampNTZType, TimestampType}\n\nobject IcebergTransactionUtils\n    extends DeltaLogging\n  {\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  def createPartitionSpec(\n      icebergSchema: IcebergSchema,\n      partitionColumns: Seq[String]): PartitionSpec = {\n    if (partitionColumns.isEmpty) {\n      PartitionSpec.unpartitioned\n    } else {\n      val builder = PartitionSpec.builderFor(icebergSchema)\n      for (partitionName <- partitionColumns) {\n        builder.identity(partitionName)\n      }\n      builder.build()\n    }\n  }\n\n  /**\n   * We expose this as a public API since APIs like\n   * [[shadedForDelta.org.apache.iceberg.DeleteFiles#deleteFile]] actually only need to take in\n   * a file path String, thus we don't need to actually convert a [[RemoveFile]] into a [[DataFile]]\n   * in this case.\n   */\n  def canonicalizeFilePath(f: FileAction, tablePath: Path): String = {\n    // Recall that FileActions can have either relative paths or absolute paths (i.e. from shallow-\n    // cloned files).\n    // Iceberg spec requires path be fully qualified path, suitable for constructing a Hadoop Path\n    if (f.pathAsUri.isAbsolute) new Path(f.pathAsUri).toString\n    else new Path(tablePath, f.toPath.toString).toString\n  }\n\n  /** Returns the mapping of logicalPartitionColName -> physicalPartitionColName */\n  def getPartitionPhysicalNameMapping(partitionSchema: StructType): Map[String, String] = {\n    partitionSchema.fields.map(f => f.name -> DeltaColumnMapping.getPhysicalName(f)).toMap\n  }\n\n  class Row (val values: Array[Any]) extends StructLike {\n    override def size: Int = values.length\n    override def get[T <: Any](pos: Int, javaClass: Class[T]): T = javaClass.cast(values(pos))\n    override def set[T <: Any](pos: Int, value: T): Unit = {\n      values(pos) = value\n    }\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  /** Visible for testing. */\n  private[delta] def convertDeltaAddFileToIcebergDataFile(\n      add: AddFile,\n      tablePath: Path,\n      partitionSpec: PartitionSpec,\n      logicalToPhysicalPartitionNames: Map[String, String],\n      statsParser: String => InternalRow,\n      snapshot: Snapshot): DataFile = {\n    var dataFileBuilder =\n      convertFileAction(\n        add, tablePath, partitionSpec, logicalToPhysicalPartitionNames, snapshot)\n        // Attempt to attach the number of records metric regardless of whether the Delta stats\n        // string is null/empty or not because this metric is required by Iceberg. If the number\n        // of records is both unavailable here and unavailable in the Delta stats, Iceberg will\n        // throw an exception when building the data file.\n        .withRecordCount(add.numLogicalRecords.getOrElse(-1L))\n\n    try {\n      if (add.stats != null && add.stats.nonEmpty) {\n        dataFileBuilder = dataFileBuilder.withMetrics(\n          getMetricsForIcebergDataFile(statsParser, add.stats, snapshot.statsSchema))\n      }\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Failed to convert Delta stats to Iceberg stats. Iceberg conversion will \" +\n          \"attempt to proceed without stats.\", e)\n    }\n\n    dataFileBuilder.build()\n  }\n\n  private[delta] def convertDeltaRemoveFileToIcebergDataFile(\n      remove: RemoveFile,\n      tablePath: Path,\n      partitionSpec: PartitionSpec,\n      logicalToPhysicalPartitionNames: Map[String, String],\n      snapshot: Snapshot): DataFile = {\n    convertFileAction(\n      remove, tablePath, partitionSpec, logicalToPhysicalPartitionNames, snapshot)\n      .withRecordCount(remove.numLogicalRecords.getOrElse(0L))\n      .build()\n  }\n\n  private[delta] def convertFileAction(\n      f: FileAction,\n      tablePath: Path,\n      partitionSpec: PartitionSpec,\n      logicalToPhysicalPartitionNames: Map[String, String],\n      snapshot: Snapshot): DataFiles.Builder = {\n    val absPath = canonicalizeFilePath(f, tablePath)\n    var builder = DataFiles\n      .builder(partitionSpec)\n      .withPath(absPath)\n      .withFileSizeInBytes(f.getFileSize)\n      .withFormat(FileFormat.PARQUET)\n    if (partitionSpec.isPartitioned) {\n      builder = builder.withPartition(\n        DeltaToIcebergConvert.Partition.convertPartitionValues(\n          snapshot, partitionSpec, f.partitionValues, logicalToPhysicalPartitionNames))\n    }\n    builder\n  }\n\n  private lazy val timestampFormatter =\n    TimestampFormatter(timestampPartitionPattern, java.util.TimeZone.getDefault)\n\n  /**\n   * Follows deserialization as specified here\n   * https://github.com/delta-io/delta/blob/master/PROTOCOL.md#Partition-Value-Serialization\n   */\n  private[delta] def stringToIcebergPartitionValue(\n      elemType: DataType,\n      partitionVal: String,\n      version: Long): Any = {\n    if (partitionVal == null || partitionVal == \"__HIVE_DEFAULT_PARTITION__\") {\n      return null\n    }\n\n    elemType match {\n      case _: StringType => partitionVal\n      case _: DateType =>\n        java.sql.Date.valueOf(partitionVal).toLocalDate.toEpochDay.asInstanceOf[Int]\n      case _: IntegerType => partitionVal.toInt.asInstanceOf[Integer]\n      case _: ShortType => partitionVal.toInt.asInstanceOf[Integer]\n      case _: ByteType => partitionVal.toInt.asInstanceOf[Integer]\n      case _: LongType => partitionVal.toLong\n      case _: BooleanType => partitionVal.toBoolean\n      case _: FloatType => partitionVal.toFloat\n      case _: DoubleType => partitionVal.toDouble\n      case _: DecimalType => new java.math.BigDecimal(partitionVal)\n      case _: BinaryType => ByteBuffer.wrap(partitionVal.getBytes(\"UTF-8\"))\n      case _: TimestampNTZType =>\n        DateTimeUtil.isoTimestampToMicros(\n          partitionVal.replace(\" \", \"T\"))\n      case _: TimestampType =>\n        try {\n          getMicrosSinceEpoch(partitionVal)\n        } catch {\n          case _: DateTimeParseException =>\n            // In case of non-ISO timestamps, parse and interpret the timestamp as system time\n            // and then convert to UTC\n            val utcInstant = utcFormatter.format(timestampFormatter.parse(partitionVal))\n            getMicrosSinceEpoch(utcInstant)\n        }\n      case _ =>\n        throw DeltaErrors.universalFormatConversionFailedException(\n          version, \"iceberg\", \"Unexpected partition data type \" + elemType)\n    }\n  }\n\n  private def getMicrosSinceEpoch(instant: String): Long = {\n    DateTimeUtil.microsFromInstant(\n      Instant.parse(instant))\n  }\n\n  private def getMetricsForIcebergDataFile(\n      statsParser: String => InternalRow,\n      stats: String,\n      statsSchema: StructType): Metrics = {\n    val statsRow = statsParser(stats)\n    val metricsConverter = IcebergStatsConverter(statsRow, statsSchema)\n    new Metrics(\n      metricsConverter.numRecordsStat, // rowCount\n      null, // columnSizes\n      null, // valueCounts\n      metricsConverter.nullValueCountsStat.getOrElse(null).asJava, // nullValueCounts\n      null, // nanValueCounts\n      metricsConverter.lowerBoundsStat.getOrElse(null).asJava, // lowerBounds\n      metricsConverter.upperBoundsStat.getOrElse(null).asJava // upperBounds\n    )\n  }\n\n  /**\n   * Create an Iceberg HiveCatalog\n   * @param conf: Hadoop Configuration\n   * @return\n   */\n  def createHiveCatalog(\n      conf: Configuration,\n      metadataUpdates: java.util.ArrayList[MetadataUpdate]\n        = new java.util.ArrayList[MetadataUpdate]()) : HiveCatalog = {\n    val catalog = new HiveCatalog()\n    catalog.setConf(conf)\n    catalog.initialize(\"spark_catalog\", Map.empty[String, String].asJava, metadataUpdates)\n    catalog\n  }\n\n  /**\n  * Encode Spark table identifier to Iceberg table identifier by putting \"database\" and \"catalog\"\n  * to the \"namespace\" in Iceberg table identifier\n  */\n  def convertSparkTableIdentifierToIceberg(\n      identifier: SparkTableIdentifier): IcebergTableIdentifier = {\n    val namespace = (identifier.database, identifier.catalog) match {\n      case (Some(database), Some(catalog)) => Namespace.of(database, catalog)\n      case (Some(database), None) => Namespace.of(database)\n      case (None, Some(catalog)) =>\n        throw new IllegalArgumentException(\n          \"Spark does not allow the constructors to skip the `database` when `catalog` is used\"\n        )\n      case (None, None) => Namespace.empty()\n    }\n    IcebergTableIdentifier.of(namespace, identifier.table)\n  }\n\n  /**\n   * Encode Spark table identifier to Iceberg table identifier by putting\n   * only \"database\" to the \"namespace\" in Iceberg table identifier.\n   * See [[HiveCatalog.isValidateNamespace]]\n   */\n  def convertSparkTableIdentifierToIcebergHive(\n      identifier: SparkTableIdentifier): IcebergTableIdentifier = {\n    val namespace = (identifier.database) match {\n      case Some(database) => Namespace.of(database)\n      case _ => Namespace.empty()\n    }\n    IcebergTableIdentifier.of(namespace, identifier.table)\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/IcebergRESTCatalogPlanningClient.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport java.io.IOException\nimport java.lang.reflect.Method\nimport java.util.Locale\n\nimport scala.jdk.CollectionConverters._\nimport scala.util.Try\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.http.client.methods.{HttpGet, HttpPost}\nimport org.apache.http.entity.{ContentType, StringEntity}\nimport org.apache.http.util.EntityUtils\nimport org.apache.http.{HttpHeaders, HttpResponse, HttpStatus}\nimport org.apache.http.client.ServiceUnavailableRetryStrategy\nimport org.apache.http.impl.client.{DefaultHttpRequestRetryHandler, HttpClientBuilder}\nimport org.apache.http.protocol.HttpContext\nimport org.apache.http.message.BasicHeader\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.sources.Filter\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\nimport org.json4s._\nimport org.json4s.jackson.JsonMethods._\nimport shadedForDelta.org.apache.iceberg.PartitionSpec\nimport shadedForDelta.org.apache.iceberg.expressions.Expressions\nimport shadedForDelta.org.apache.iceberg.rest.requests.{PlanTableScanRequest, PlanTableScanRequestParser}\nimport shadedForDelta.org.apache.iceberg.rest.responses.PlanTableScanResponse\n\n/**\n * Case class for parsing Iceberg REST catalog /v1/config response.\n * Per the Iceberg REST spec, the config endpoint returns defaults and overrides.\n * The optional \"prefix\" in overrides is used for multi-tenant catalog paths.\n */\nprivate case class CatalogConfigResponse(\n    defaults: Map[String, String],\n    overrides: Map[String, String])\n\n/**\n * Iceberg REST implementation of ServerSidePlanningClient that calls Iceberg REST catalog server.\n *\n * This implementation calls the Iceberg REST catalog's `/plan` endpoint to perform server-side\n * scan planning. The server returns the list of data files to read, which eliminates the need\n * for client-side listing operations.\n *\n * Thread safety: This class creates a shared HTTP client that is thread-safe for concurrent\n * requests. The HTTP client should be explicitly closed by calling close() when done.\n *\n * @param baseUriRaw Base URI of the Iceberg REST catalog up to /v1, e.g.,\n *                   \"http://<catalog-URL>/iceberg/v1\". Trailing slashes are handled automatically.\n * @param catalogName Name of the catalog for config endpoint query parameter.\n * @param token Authentication token for the catalog server.\n */\nclass IcebergRESTCatalogPlanningClient(\n    baseUriRaw: String,\n    catalogName: String,\n    token: String) extends ServerSidePlanningClient with Logging {\n\n  // Normalize baseUri to handle trailing slashes\n  private val baseUri = baseUriRaw.stripSuffix(\"/\")\n\n  // Sentinel value indicating \"use current snapshot\" in Iceberg REST API\n  private val CURRENT_SNAPSHOT_ID = 0L\n\n  // Partition spec ID for unpartitioned tables\n  private val UNPARTITIONED_SPEC_ID = 0\n\n  // IRC config key mappings for each credential type\n  private val S3_KEYS = Seq(\"s3.access-key-id\", \"s3.secret-access-key\", \"s3.session-token\")\n  private val AZURE_SAS_TOKEN_KEY_PREFIX = \"adls.sas-token.\"\n  private val GCS_TOKEN_KEY = \"gcs.oauth2.token\"\n  private val GCS_EXPIRY_KEY = \"gcs.oauth2.token-expires-at\"\n\n  private case class S3Credentials(\n      accessKeyId: String,\n      secretAccessKey: String,\n      sessionToken: String) extends ScanPlanStorageCredentials {\n    override def configure(conf: Configuration): Unit = {\n      conf.set(\"fs.s3a.path.style.access\", \"true\")\n      conf.set(\"fs.s3.impl.disable.cache\", \"true\")\n      conf.set(\"fs.s3a.impl.disable.cache\", \"true\")\n      conf.set(\"fs.s3a.access.key\", accessKeyId)\n      conf.set(\"fs.s3a.secret.key\", secretAccessKey)\n      conf.set(\"fs.s3a.session.token\", sessionToken)\n    }\n  }\n\n  private case class AzureCredentials(\n      accountName: String,\n      sasToken: String) extends ScanPlanStorageCredentials {\n    override def configure(conf: Configuration): Unit = {\n      val accountSuffix = s\"$accountName.dfs.core.windows.net\"\n      conf.set(\"fs.abfs.impl.disable.cache\", \"true\")\n      conf.set(\"fs.abfss.impl.disable.cache\", \"true\")\n      conf.set(s\"fs.azure.account.auth.type.$accountSuffix\", \"SAS\")\n      conf.set(s\"fs.azure.sas.fixed.token.$accountSuffix\", sasToken)\n    }\n  }\n\n  private case class GcsCredentials(\n      oauth2Token: String,\n      expirationEpochMs: Option[Long] = None) extends ScanPlanStorageCredentials {\n    override def configure(conf: Configuration): Unit = {\n      conf.set(\"fs.gs.impl.disable.cache\", \"true\")\n      conf.set(\"fs.gs.auth.type\", \"ACCESS_TOKEN_PROVIDER\")\n      conf.set(\"fs.gs.auth.access.token.provider.impl\",\n        classOf[FixedGcsAccessTokenProvider].getName)\n      conf.set(\"fs.gs.auth.access.token\", oauth2Token)\n      expirationEpochMs.foreach { ms =>\n        conf.set(\"fs.gs.auth.access.token.expiration.ms\", ms.toString)\n      }\n    }\n  }\n\n  private def hasAzureKeys(config: Map[String, String]): Boolean =\n    config.keys.exists(_.startsWith(AZURE_SAS_TOKEN_KEY_PREFIX))\n\n  private def buildAzureCredentials(config: Map[String, String]): AzureCredentials = {\n    val sasTokenKey = config.keys\n      .find(_.startsWith(AZURE_SAS_TOKEN_KEY_PREFIX))\n      .getOrElse(throw new IllegalStateException(\n        s\"Missing Azure SAS token key starting with: $AZURE_SAS_TOKEN_KEY_PREFIX\"))\n\n    val accountName = sasTokenKey\n      .stripPrefix(AZURE_SAS_TOKEN_KEY_PREFIX)\n      .stripSuffix(\".dfs.core.windows.net\")\n\n    val sasToken = config(sasTokenKey)\n\n    AzureCredentials(accountName = accountName, sasToken = sasToken)\n  }\n\n  private def fromConfig(config: Map[String, String]): ScanPlanStorageCredentials = {\n    def get(key: String): String =\n      config.getOrElse(key, throw new IllegalStateException(s\"Missing required credential: $key\"))\n\n    def hasAny(keys: Seq[String]): Boolean = keys.exists(config.contains)\n\n    if (hasAny(S3_KEYS)) {\n      S3Credentials(\n        get(\"s3.access-key-id\"),\n        get(\"s3.secret-access-key\"),\n        get(\"s3.session-token\"))\n    } else if (hasAzureKeys(config)) {\n      buildAzureCredentials(config)\n    } else if (config.contains(GCS_TOKEN_KEY)) {\n      val token = get(GCS_TOKEN_KEY)\n      val expirationEpochMs = config.get(GCS_EXPIRY_KEY)\n        .flatMap(s => scala.util.Try(s.toLong).toOption)\n      GcsCredentials(token, expirationEpochMs)\n    } else {\n      throw new IllegalStateException(\n        \"Unrecognized credential keys. \" +\n          \"Expected S3 (s3.*), Azure (adls.*), or GCS (gcs.*) properties.\")\n    }\n  }\n\n  /**\n   * Lazily fetch the catalog configuration and construct the endpoint URI root.\n   * Calls /v1/config?warehouse=<catalogName> per Iceberg REST catalog spec to get the prefix.\n   * If no prefix is returned, uses baseUri directly without any prefix per Iceberg spec.\n   */\n  private lazy val icebergRestCatalogUriRoot: String = {\n    fetchCatalogPrefix() match {\n      case Some(prefix) => s\"$baseUri/$prefix\"\n      case None => baseUri\n    }\n  }\n\n  /**\n   * Fetch catalog prefix from /v1/config endpoint per Iceberg REST catalog spec.\n   * Returns None on any error or if no prefix is defined in the config.\n   */\n  private def fetchCatalogPrefix(): Option[String] = {\n    val configUri = s\"$baseUri/config?warehouse=$catalogName\"\n    try {\n      val httpGet = new HttpGet(configUri)\n      val response = httpClient.execute(httpGet)\n      try {\n        if (response.getStatusLine.getStatusCode == HttpStatus.SC_OK) {\n          val body = EntityUtils.toString(response.getEntity)\n          val config = JsonUtils.fromJson[CatalogConfigResponse](body)\n          // Apply overrides on top of defaults per Iceberg REST spec\n          config.overrides.get(\"prefix\").orElse(config.defaults.get(\"prefix\"))\n        } else {\n          None\n        }\n      } finally {\n        response.close()\n      }\n    } catch {\n      case e: Exception =>\n        logWarning(s\"Failed to fetch catalog prefix from $configUri. \" +\n          s\"Falling back to base URI. Error: ${e.getMessage}\")\n        None\n    }\n  }\n\n  private val httpHeaders = {\n    val baseHeaders = Map(\n      HttpHeaders.ACCEPT -> ContentType.APPLICATION_JSON.getMimeType,\n      HttpHeaders.CONTENT_TYPE -> ContentType.APPLICATION_JSON.getMimeType,\n      HttpHeaders.USER_AGENT -> buildUserAgent()\n    )\n    // Add Bearer token authentication if token is provided\n    val headersWithAuth = if (token.nonEmpty) {\n      baseHeaders + (HttpHeaders.AUTHORIZATION -> s\"Bearer $token\")\n    } else {\n      baseHeaders\n    }\n    headersWithAuth.map { case (k, v) => new BasicHeader(k, v) }.toSeq.asJava\n  }\n\n  /**\n   * Build User-Agent header with Delta, Spark, Java and Scala version information.\n   * Format: \"Delta/<version> Spark/<version> Java/<version> Scala/<version>\"\n   * Example: \"Delta/4.0.0 Spark/3.5.0 Java/17.0.10 Scala/2.12.18\"\n   */\n  private def buildUserAgent(): String = {\n    val deltaVersion = getDeltaVersion().getOrElse(\"unknown\")\n    val sparkVersion = getSparkVersion().getOrElse(\"unknown\")\n    val javaVersion = getJavaVersion()\n    val scalaVersion = getScalaVersion()\n    s\"Delta/$deltaVersion Spark/$sparkVersion Java/$javaVersion Scala/$scalaVersion\"\n  }\n\n  /**\n   * Get the User-Agent header value used by this client.\n   * Format: \"Delta/<version> Spark/<version> Java/<version> Scala/<version>\"\n   *\n   * @return The User-Agent string used in HTTP requests\n   */\n  def getUserAgent(): String = {\n    buildUserAgent()\n  }\n\n  /**\n   * Get Spark version. Returns None if Spark version cannot be determined.\n   */\n  private def getSparkVersion(): Option[String] = {\n    try {\n      val packageClass = Utils.classForName(\"org.apache.spark.package$\")\n      val moduleField = packageClass.getField(\"MODULE$\")\n      val moduleObj = moduleField.get(null)\n      val versionObj = packageClass.getMethod(\"SPARK_VERSION\").invoke(moduleObj)\n      if (versionObj != null) {\n        Some(versionObj.toString)\n      } else {\n        None\n      }\n    } catch {\n      case _: Exception => None\n    }\n  }\n\n  /**\n   * Get Delta version. Returns None if Delta is not available or version cannot be determined.\n   */\n  private def getDeltaVersion(): Option[String] = {\n    // Try io.delta.Version.getVersion() first (preferred method)\n    try {\n      val versionClass = Utils.classForName(\"io.delta.Version\")\n      val versionObj = versionClass.getMethod(\"getVersion\").invoke(null)\n      if (versionObj != null) {\n        return Some(versionObj.toString)\n      }\n    } catch {\n      case _: Exception => // Fall through to fallback\n    }\n\n    // Fall back to io.delta.VERSION constant\n    try {\n      val packageClass = Utils.classForName(\"io.delta.package$\")\n      val moduleField = packageClass.getField(\"MODULE$\")\n      val moduleObj = moduleField.get(null)\n      val versionObj = packageClass.getMethod(\"VERSION\").invoke(moduleObj)\n      if (versionObj != null) {\n        return Some(versionObj.toString)\n      }\n    } catch {\n      case _: Exception => // Delta not available or version not accessible\n    }\n\n    None\n  }\n\n  /**\n   * Get Java version from system properties.\n   */\n  private def getJavaVersion(): String = {\n    System.getProperty(\"java.version\", \"unknown\")\n  }\n\n  /**\n   * Get Scala version from the scala.util.Properties.versionNumberString property.\n   */\n  private def getScalaVersion(): String = {\n    scala.util.Properties.versionNumberString\n  }\n\n  // Maximum number of retries for transient HTTP failures (IOException, 5xx server errors)\n  private val HTTP_MAX_RETRIES = 3\n\n  private lazy val httpClient = HttpClientBuilder.create()\n    .setDefaultHeaders(httpHeaders)\n    .setConnectionTimeToLive(30, java.util.concurrent.TimeUnit.SECONDS)\n    // requestSentRetryEnabled=true: safe to retry already-sent requests because\n    // planScan is a read-only operation (idempotent POST to /plan endpoint)\n    .setRetryHandler(new DefaultHttpRequestRetryHandler(HTTP_MAX_RETRIES, true))\n    .setServiceUnavailableRetryStrategy(new ServerErrorRetryStrategy(HTTP_MAX_RETRIES))\n    .build()\n\n  override def canConvertFilters(filters: Array[Filter]): Boolean = {\n    // Check if all filters can be converted to Iceberg expressions\n    // Returns true only if ALL filters successfully convert\n    filters.forall { filter =>\n      SparkToIcebergExpressionConverter.convert(filter).isDefined\n    }\n  }\n\n  override def planScan(\n      database: String,\n      table: String,\n      sparkFilterOption: Option[Filter] = None,\n      sparkProjectionOption: Option[Seq[String]] = None,\n      sparkLimitOption: Option[Int] = None): ScanPlan = {\n    // Construct the /plan endpoint URI. For Unity Catalog tables, the\n    // Call /v1/config to get the catalog prefix, then construct the full endpoint.\n    // icebergRestCatalogUriRoot is lazily constructed as: {baseUri}/{prefix}\n    // where prefix comes from /v1/config?warehouse=<catalogName> per Iceberg REST spec.\n    // See: https://iceberg.apache.org/rest-catalog-spec/\n    val planTableScanUri = s\"$icebergRestCatalogUriRoot/namespaces/$database/tables/$table/plan\"\n\n    // Request planning for current snapshot. snapshotId = 0 means \"use current snapshot\"\n    // in the Iceberg REST API spec. Time-travel queries are not yet supported.\n    val builder = new PlanTableScanRequest.Builder()\n      .withSnapshotId(CURRENT_SNAPSHOT_ID)\n      // Set caseSensitive=false (defaults to true in spec) to match Spark's case-insensitive\n      // column handling. Server should validate and block requests with caseSensitive=true.\n      .withCaseSensitive(false)\n\n    // Convert Spark Filter to Iceberg Expression and add to request if filter is present.\n    sparkFilterOption.foreach { sparkFilter =>\n      SparkToIcebergExpressionConverter.convert(sparkFilter).foreach { icebergExpr =>\n        builder.withFilter(icebergExpr)\n      }\n    }\n\n    // Add projection to request if present.\n    sparkProjectionOption.foreach { columnNames =>\n      builder.withSelect(columnNames.asJava)\n    }\n\n    val request = builder.build()\n\n    // Iceberg 1.11 adds withMinRowsRequested() support. For now, manually inject the field.\n    val requestJson = sparkLimitOption match {\n      case Some(limit) =>\n        implicit val formats: Formats = DefaultFormats\n        val jsonAst = parse(PlanTableScanRequestParser.toJson(request))\n        val modifiedJson = jsonAst merge JObject(\"min-rows-requested\" -> JLong(limit.toLong))\n        compact(render(modifiedJson))\n      case None =>\n        PlanTableScanRequestParser.toJson(request)\n    }\n    val httpPost = new HttpPost(planTableScanUri)\n    httpPost.setEntity(new StringEntity(requestJson, ContentType.APPLICATION_JSON))\n    val httpResponse = httpClient.execute(httpPost)\n\n    // Only unpartitioned tables are supported. This map is used when parsing the response\n    // to resolve partition specs. The validation that the table is actually unpartitioned\n    // happens later in convertToScanPlan when we check file.partition().size().\n    val unpartitionedSpecMap = Map(UNPARTITIONED_SPEC_ID -> PartitionSpec.unpartitioned())\n\n    try {\n      val statusCode = httpResponse.getStatusLine.getStatusCode\n      val responseBody = EntityUtils.toString(httpResponse.getEntity)\n      if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED) {\n        // Parse response with caseSensitive=false to match request and Spark's case-insensitive\n        // column handling\n        val icebergResponse = parsePlanTableScanResponse(\n          responseBody, unpartitionedSpecMap, caseSensitive = false)\n\n        // Verify plan status is \"completed\". The Iceberg REST spec allows async planning\n        // where the server returns \"submitted\" status and the client must poll for results.\n        // We don't support async planning yet, so we require \"completed\" status.\n        val planStatus = icebergResponse.planStatus()\n        if (planStatus != null && planStatus.toString.toLowerCase(Locale.ROOT) != \"completed\") {\n          throw new UnsupportedOperationException(\n            s\"Async planning not supported. Plan status was '$planStatus' but \" +\n            s\"expected 'completed'. Table: $database.$table\")\n        }\n\n        convertToScanPlan(icebergResponse, responseBody)\n      } else {\n        // TODO: Parse structured ErrorResponse JSON from Iceberg REST spec instead of raw body\n        throw new IOException(\n          s\"Failed to plan table scan for $database.$table. \" +\n          s\"HTTP status: $statusCode, Response: $responseBody\")\n      }\n    } finally {\n      httpResponse.close()\n    }\n  }\n\n  /**\n   * Convert Iceberg PlanTableScanResponse to simple ScanPlan data class.\n   *\n   * Validates response structure and ensures the table is unpartitioned.\n   */\n  private def convertToScanPlan(\n      response: PlanTableScanResponse,\n      responseBody: String): ScanPlan = {\n    require(response != null, \"PlanTableScanResponse cannot be null\")\n    require(response.fileScanTasks() != null, \"File scan tasks cannot be null\")\n\n    val files = response.fileScanTasks().asScala.map { task =>\n      require(task != null, \"FileScanTask cannot be null\")\n      require(task.file() != null, \"DataFile cannot be null\")\n\n      // Validate that the server does not expect the application of a residual. The application of\n      // a residual filter is currently not supported, and its ignorance leads to wrong results.\n      val residual = task.residual()\n      if (residual != null && !residual.isEquivalentTo(Expressions.alwaysTrue)) {\n        throw new UnsupportedOperationException(\n          s\"Found FileScanTask with residual: ${residual}. \" +\n            s\"Only FileScanTasks with no or alwaysTrue residual are currently supported.\")\n      }\n\n      val file = task.file()\n\n      // Validate that table is unpartitioned. Partitioned tables are not supported yet.\n      if (file.partition().size() > 0) {\n        throw new UnsupportedOperationException(\n          s\"Table has partition data: ${file.partition()}. \" +\n          s\"Only unpartitioned tables (spec ID $UNPARTITIONED_SPEC_ID) are currently supported.\")\n      }\n\n      ScanFile(\n        filePath = file.path().toString,\n        fileSizeInBytes = file.fileSizeInBytes(),\n        fileFormat = file.format().toString.toLowerCase(Locale.ROOT)\n      )\n    }.toSeq\n\n    val credentials = extractCredentials(responseBody)\n    ScanPlan(files = files, credentials = credentials)\n  }\n\n  /**\n   * Extract storage credentials from IRC server response.\n   * Uses sealed trait pattern - tries each credential type in priority order.\n   *\n   * JSON structure:\n   * {\n   *   \"storage-credentials\": [{\n   *     \"config\": {\n   *       \"s3.access-key-id\": \"...\",\n   *       \"azure.account-name\": \"...\",\n   *       \"gcs.oauth2.token\": \"...\",\n   *       ...\n   *     }\n   *   }]\n   * }\n   */\n  /**\n   * Extract storage credentials from response using sealed trait factory.\n   * Returns None if no credentials section exists.\n   * Throws IllegalStateException if credentials are incomplete or malformed.\n   */\n  private def extractCredentials(responseBody: String): Option[ScanPlanStorageCredentials] = {\n    implicit val formats: Formats = DefaultFormats\n    val json = parse(responseBody)\n\n    // Extract config map from storage-credentials[0].config\n    val config: Option[Map[String, String]] = try {\n      (json \\ \"storage-credentials\")(0) \\ \"config\" match {\n        case JNothing | JNull => None\n        case c => Some(c.extract[Map[String, String]])\n      }\n    } catch {\n      case _: Exception => None // No credentials section in response\n    }\n\n    // If config exists and is non-empty, use factory (throws on incomplete credentials)\n    config.filter(_.nonEmpty).map(fromConfig)\n  }\n\n  /**\n   * Close the HTTP client and release resources.\n   *\n   * This should be called when the client is no longer needed to prevent resource leaks.\n   * After calling close(), this client instance should not be used for further requests.\n   */\n  override def close(): Unit = {\n    if (httpClient != null) {\n      httpClient.close()\n    }\n  }\n\n  /**\n   * Retry strategy for server errors (5xx status codes) with exponential backoff.\n   * Retries up to maxRetries times with doubling intervals (1s, 2s, 4s, ...).\n   * Does NOT retry on client errors (4xx) since those indicate request-level issues.\n   *\n   * The ServiceUnavailableRetryStrategy interface calls retryRequest() first, then\n   * getRetryInterval(), so we capture the execution count in retryRequest() and\n   * use it to compute the backoff in getRetryInterval().\n   */\n  private class ServerErrorRetryStrategy(maxRetries: Int)\n      extends ServiceUnavailableRetryStrategy {\n\n    // ThreadLocal so concurrent planScan calls each track their own retry attempt.\n    // The HTTP client is shared and thread-safe (see class doc), so multiple threads\n    // can be retrying independently through the same strategy instance.\n    private val lastExecutionCount = new ThreadLocal[Int] {\n      override def initialValue(): Int = 1\n    }\n\n    override def retryRequest(\n        response: HttpResponse,\n        executionCount: Int,\n        context: HttpContext): Boolean = {\n      lastExecutionCount.set(executionCount)\n      val statusCode = response.getStatusLine.getStatusCode\n      statusCode >= 500 && executionCount <= maxRetries\n    }\n\n    // Exponential backoff: 1s, 2s, 4s, ...\n    override def getRetryInterval: Long =\n      java.util.concurrent.TimeUnit.SECONDS.toMillis(1L << (lastExecutionCount.get() - 1))\n  }\n\n  private def parsePlanTableScanResponse(\n    json: String,\n    specsById: Map[Int, PartitionSpec],\n    caseSensitive: Boolean): PlanTableScanResponse = {\n\n    // Use reflection to access the private fromJson method in the Iceberg parser class.\n    // The method is not part of the public API, so we need reflection and setAccessible.\n    val parserClass = Utils.classForName(\n      \"shadedForDelta.org.apache.iceberg.rest.responses.PlanTableScanResponseParser\")\n\n    val fromJsonMethod: Method = parserClass.getDeclaredMethod(\n      \"fromJson\",\n      classOf[String],\n      classOf[java.util.Map[_, _]],\n      classOf[Boolean])\n\n    fromJsonMethod.setAccessible(true)\n\n    fromJsonMethod.invoke(\n      null,  // static method\n      json,\n      specsById.map { case (k, v) => Int.box(k) -> v }.asJava,\n      Boolean.box(caseSensitive)\n    ).asInstanceOf[PlanTableScanResponse]\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/IcebergRESTCatalogPlanningClientFactory.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.SparkSession\n\n/**\n * Factory that creates IcebergRESTCatalogPlanningClient instances.\n * Lives in the iceberg module alongside the implementation.\n */\nclass IcebergRESTCatalogPlanningClientFactory extends ServerSidePlanningClientFactory {\n  override def buildClient(\n      spark: SparkSession,\n      metadata: ServerSidePlanningMetadata): ServerSidePlanningClient = {\n\n    val baseUri = metadata.planningEndpointUri\n    val token = metadata.authToken.getOrElse(\"\")\n    val catalogName = metadata.catalogName\n\n    new IcebergRESTCatalogPlanningClient(baseUri, catalogName, token)\n  }\n}\n"
  },
  {
    "path": "iceberg/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/SparkToIcebergExpressionConverter.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.sources._\nimport shadedForDelta.org.apache.iceberg.expressions.{Expression, Expressions}\n\n/**\n * Converts Spark Filter expressions to Iceberg Expression objects for server-side planning.\n *\n * Filter Mapping Table:\n * {{{\n * +--------------------------+--------------------------------+\n * | Spark Filter             | Iceberg Expression             |\n * +--------------------------+--------------------------------+\n * | EqualTo                  | Expressions.equal()            |\n * |   EqualTo(col, null)     | Expressions.isNull()           |\n * |   EqualTo(col, NaN)      | Expressions.isNaN()            |\n * | NotEqualTo               | Expressions.notEqual()         |\n * |   NotEqualTo(col, NaN)   | Expressions.notNaN()           |\n * | LessThan                 | Expressions.lessThan()         |\n * | GreaterThan              | Expressions.greaterThan()      |\n * | LessThanOrEqual          | Expressions.lessThanOrEqual()  |\n * | GreaterThanOrEqual       | Expressions.greaterThanOrEqual()|\n * | In                       | Expressions.in()               |\n * | Not(In)                  | Expressions.notIn()            |\n * | IsNull                   | Expressions.isNull()           |\n * | IsNotNull                | Expressions.notNull()          |\n * | Not(IsNull)              | Expressions.notNull()          |\n * | And                      | Expressions.and()              |\n * | Or                       | Expressions.or()               |\n * | StringStartsWith         | Expressions.startsWith()       |\n * | Not(StringStartsWith)    | Expressions.notStartsWith()    |\n * | AlwaysTrue               | Expressions.alwaysTrue()       |\n * | AlwaysFalse              | Expressions.alwaysFalse()      |\n * +--------------------------+--------------------------------+\n * }}}\n *\n *\n * Example usage:\n * {{{\n *   val sparkFilter = EqualTo(\"id\", 5)\n *   SparkToIcebergExpressionConverter.convert(sparkFilter) match {\n *     case Some(icebergExpr) => // Use expression\n *     case None => // Filter not supported\n *   }\n * }}}\n */\nprivate[serverSidePlanning] object SparkToIcebergExpressionConverter extends Logging {\n\n  /**\n   * Convert a Spark Filter to an Iceberg Expression.\n   *\n   * @param sparkFilter The Spark filter to convert\n   * @return Some(Expression) if the filter is supported, None otherwise\n   */\n  private[serverSidePlanning] def convert(sparkFilter: Filter): Option[Expression] = {\n    logInfo(s\"Converting Spark filter to Iceberg expression: $sparkFilter\")\n    val result = try {\n      sparkFilter match {\n    // Equality and Comparison Operators\n    case EqualTo(attribute, sparkValue) =>\n      Some(convertEqualTo(attribute, sparkValue))\n    case LessThan(attribute, sparkValue) =>\n      Some(convertLessThan(attribute, sparkValue))\n    case GreaterThan(attribute, sparkValue) =>\n      Some(convertGreaterThan(attribute, sparkValue))\n    case LessThanOrEqual(attribute, sparkValue) =>\n      Some(convertLessThanOrEqual(attribute, sparkValue))\n    case GreaterThanOrEqual(attribute, sparkValue) =>\n      Some(convertGreaterThanOrEqual(attribute, sparkValue))\n    case In(attribute, values) =>\n      Some(convertIn(attribute, values))\n\n    // Null Checks\n    case IsNull(attribute) =>\n      Some(Expressions.isNull(attribute))\n    case IsNotNull(attribute) =>\n      Some(Expressions.notNull(attribute))\n\n    // Logical Combinators\n    case And(left, right) =>\n      for {\n        leftIcebergExpr <- convert(left)\n        rightIcebergExpr <- convert(right)\n      } yield Expressions.and(leftIcebergExpr, rightIcebergExpr)\n\n    case Or(left, right) =>\n      for {\n        leftIcebergExpr <- convert(left)\n        rightIcebergExpr <- convert(right)\n      } yield Expressions.or(leftIcebergExpr, rightIcebergExpr)\n\n    // NOT Operator (special case)\n    case Not(innerFilter) =>\n      convertNot(innerFilter)\n\n    // String Operations\n    case StringStartsWith(attribute, value) =>\n      Some(Expressions.startsWith(attribute, value))\n\n    // Always True/False\n    case AlwaysTrue() =>\n      Some(Expressions.alwaysTrue())\n    case AlwaysFalse() =>\n      Some(Expressions.alwaysFalse())\n\n      /*\n       * Unsupported Filters:\n       * - StringEndsWith, StringContains: Iceberg API doesn't provide these predicates\n       */\n      case _ =>\n        logInfo(s\"Unsupported Spark filter (no Iceberg equivalent): \" +\n          s\"${sparkFilter.getClass.getSimpleName} - $sparkFilter\")\n        None\n      }\n    } catch {\n      case e: IllegalArgumentException =>\n        /*\n         * The filter is supported but conversion failed as the type or value is unsupported.\n         * - NaN in comparison operators (LessThan, GreaterThan, etc.)\n         * - Unsupported types (e.g., Array, Map, binary types)\n         */\n        logWarning(s\"Failed to convert Spark filter due to unsupported type or value: \" +\n          s\"$sparkFilter\", e)\n        None\n    }\n    logDebug(s\"Conversion result for $sparkFilter: \" +\n      s\"${result.map(_.toString).getOrElse(\"None (unsupported)\")}\")\n    result\n  }\n\n  // Private helper methods for type-specific conversions\n\n  private def isNaN(value: Any): Boolean = value match {\n    case v: Float => v.isNaN\n    case v: Double => v.isNaN\n    case _ => false\n  }\n\n  /**\n   * Convert a Spark value to Iceberg-compatible type with proper coercion.\n   * @param supportBoolean if true, also handles Boolean type.\n   *        Note: Comparison operators (LessThan, GreaterThan, etc.) don't support Boolean.\n   *        Only equality operators (EqualTo, NotEqualTo) should set this to true.\n   * @throws IllegalArgumentException if the value is an unsupported type (complex types,\n   *         unknown types, or Boolean when supportBoolean=false)\n   */\n  private[serverSidePlanning] def toIcebergValue(\n      value: Any,\n      supportBoolean: Boolean = false): Any = {\n    value match {\n    // Date/Timestamp conversion (semantic change) because\n    // Iceberg Literals.from() doesn't accept java.sql.Date/Timestamp, expects Int/Long\n    case v: java.sql.Date =>\n      // Iceberg expects days since epoch (1970-01-01) as Int\n      DateTimeUtils.fromJavaDate(v): Integer\n    case v: java.sql.Timestamp =>\n      // Iceberg expects microseconds since epoch as Long\n      DateTimeUtils.fromJavaTimestamp(v): java.lang.Long\n    case v: java.time.Instant =>\n      // Iceberg expects microseconds since epoch as Long (for TIMESTAMP WITH TIMEZONE)\n      DateTimeUtils.instantToMicros(v): java.lang.Long\n    case v: java.time.LocalDateTime =>\n      // Iceberg expects microseconds since epoch as Long (for TIMESTAMP_NTZ)\n      DateTimeUtils.localDateTimeToMicros(v): java.lang.Long\n    case v: java.time.LocalDate =>\n      // Iceberg expects days since epoch (1970-01-01) as Int (for DATE)\n      v.toEpochDay.toInt: Integer\n    // Type coercion (Scala to Java boxed types)\n    case v: Int => v: Integer\n    case v: Long => v: java.lang.Long\n    case v: Float => v: java.lang.Float\n    case v: Double => v: java.lang.Double\n    case v: java.math.BigDecimal => v\n    case v: String => v\n    case v: Boolean if supportBoolean => v: java.lang.Boolean\n    case _ =>\n      throw new IllegalArgumentException(\n        s\"Unsupported type for Iceberg filter pushdown: ${value.getClass.getName}\")\n    }\n  }\n\n  /*\n   * Convert EqualTo with special handling for null and NaN.\n   * Note: We cannot use Expressions.equal(col, null/NaN) because Iceberg models these\n   * with specialized predicates (isNull/isNaN) that have different evaluation semantics:\n   * - SQL: col = NULL returns NULL (unknown), but col IS NULL returns TRUE/FALSE\n   * Reference: OSS Iceberg SparkV2Filters.handleEqual()\n   */\n  private def convertEqualTo(attribute: String, sparkValue: Any): Expression = {\n    sparkValue match {\n      case null => Expressions.isNull(attribute)\n      case _ if isNaN(sparkValue) => Expressions.isNaN(attribute)\n      case _ => Expressions.equal(attribute, toIcebergValue(sparkValue, supportBoolean = true))\n    }\n  }\n\n  /*\n   * Convert NotEqualTo with special handling for null and NaN.\n   * Note: Not(EqualTo(col, null)) from Spark (representing IS NOT NULL) is converted here.\n   */\n  private def convertNotEqualTo(attribute: String, sparkValue: Any): Expression = {\n    sparkValue match {\n      case null => Expressions.notNull(attribute)\n      case _ if isNaN(sparkValue) => Expressions.notNaN(attribute)\n      case _ => Expressions.notEqual(attribute, toIcebergValue(sparkValue, supportBoolean = true))\n    }\n  }\n\n  /**\n   * Convert a Spark NOT filter to an Iceberg Expression.\n   *\n   * Supported conversions:\n   * - Not(EqualTo(col, value)) -> Expressions.notEqual\n   * - Not(EqualTo(col, null)) -> Expressions.notNull\n   * - Not(EqualTo(col, NaN)) -> Expressions.notNaN\n   * - Not(In(col, values)) -> Expressions.notIn\n   * - Not(IsNull(col)) -> Expressions.notNull\n   * - Not(StringStartsWith(col, value)) -> Expressions.notStartsWith\n   *\n   * All other NOT expressions (Not(LessThan), Not(And), etc.) are unsupported because Iceberg\n   * doesn't provide equivalent predicates. This is consistent with OSS Iceberg's SparkV2Filters.\n   */\n  private def convertNot(sparkInnerFilter: Filter): Option[Expression] = {\n    sparkInnerFilter match {\n      case EqualTo(attribute, sparkValue) =>\n        Some(convertNotEqualTo(attribute, sparkValue))\n\n      case In(attribute, values) =>\n        Some(convertNotIn(attribute, values))\n\n      case IsNull(attribute) =>\n        Some(Expressions.notNull(attribute))\n\n      case StringStartsWith(attribute, value) =>\n        Some(Expressions.notStartsWith(attribute, value))\n\n      case _ =>\n        None  // All other NOT expressions are unsupported\n    }\n  }\n\n  private def convertIn(attribute: String, values: Array[Any]): Expression = {\n    // Iceberg expects IN to filter out null values and convert Date/Timestamp to Int/Long\n    val nonNullValues = values.filter(_ != null).map(v =>\n      toIcebergValue(v, supportBoolean = true)\n    )\n    Expressions.in(attribute, nonNullValues: _*)\n  }\n\n  /**\n   * Convert NOT IN filter to Iceberg notIn expression.\n   * Example: NOT IN (\"id\", [1, 2, 3]) -> Expressions.notIn(\"id\", 1, 2, 3)\n   */\n  private def convertNotIn(attribute: String, values: Array[Any]): Expression = {\n    // Iceberg expects NOT IN to filter out null values and convert Date/Timestamp to Int/Long\n    val nonNullValues = values.filter(_ != null).map(v =>\n      toIcebergValue(v, supportBoolean = true)\n    )\n    Expressions.notIn(attribute, nonNullValues: _*)\n  }\n\n  private def convertLessThan(attribute: String, sparkValue: Any): Expression =\n    Expressions.lessThan(attribute, toIcebergValue(sparkValue, supportBoolean = false))\n\n  private def convertGreaterThan(attribute: String, sparkValue: Any): Expression =\n    Expressions.greaterThan(attribute, toIcebergValue(sparkValue, supportBoolean = false))\n\n  private def convertLessThanOrEqual(attribute: String, sparkValue: Any): Expression =\n    Expressions.lessThanOrEqual(attribute, toIcebergValue(sparkValue, supportBoolean = false))\n\n  private def convertGreaterThanOrEqual(attribute: String, sparkValue: Any): Expression =\n    Expressions.greaterThanOrEqual(attribute, toIcebergValue(sparkValue, supportBoolean = false))\n}\n"
  },
  {
    "path": "iceberg/src/test/java/shadedForDelta/org/apache/iceberg/rest/IcebergRESTCatalogAdapterWithPlanSupport.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage shadedForDelta.org.apache.iceberg.rest;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Consumer;\n\nimport shadedForDelta.org.apache.iceberg.BaseFileScanTask;\nimport shadedForDelta.org.apache.iceberg.DeleteFile;\nimport shadedForDelta.org.apache.iceberg.FileScanTask;\nimport shadedForDelta.org.apache.iceberg.PartitionSpecParser;\nimport shadedForDelta.org.apache.iceberg.SchemaParser;\nimport shadedForDelta.org.apache.iceberg.Table;\nimport shadedForDelta.org.apache.iceberg.TableScan;\nimport shadedForDelta.org.apache.iceberg.catalog.Catalog;\nimport shadedForDelta.org.apache.iceberg.catalog.TableIdentifier;\nimport shadedForDelta.org.apache.iceberg.io.CloseableIterable;\nimport shadedForDelta.org.apache.iceberg.rest.HTTPRequest;\nimport shadedForDelta.org.apache.iceberg.rest.RESTCatalogAdapter;\nimport shadedForDelta.org.apache.iceberg.rest.requests.PlanTableScanRequest;\nimport shadedForDelta.org.apache.iceberg.rest.requests.PlanTableScanRequestParser;\nimport shadedForDelta.org.apache.iceberg.rest.responses.ErrorResponse;\nimport shadedForDelta.org.apache.iceberg.rest.PlanStatus;\nimport shadedForDelta.org.apache.iceberg.rest.responses.PlanTableScanResponse;\nimport shadedForDelta.org.apache.iceberg.expressions.Expression;\nimport shadedForDelta.org.apache.iceberg.expressions.ResidualEvaluator;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Extends RESTCatalogAdapter to add support for server-side scan planning via the /plan endpoint.\n * This adapter intercepts /plan requests and handles them by executing Iceberg table scans locally,\n * returning file scan tasks to the client. Other catalog operations are delegated to the parent\n * RESTCatalogAdapter implementation.\n */\nclass IcebergRESTCatalogAdapterWithPlanSupport extends RESTCatalogAdapter {\n  private static final Logger LOG = LoggerFactory.getLogger(IcebergRESTCatalogAdapterWithPlanSupport.class);\n\n  private final Catalog catalog;\n  // Catalog prefix returned in /v1/config that gets inserted into REST paths.\n  // Example: prefix=\"iceberg\" transforms /v1/namespaces/db/tables/t1/plan\n  //          to /v1/iceberg/namespaces/db/tables/t1/plan\n  private String catalogPrefix = null;  // null = no prefix (fallback case)\n\n  // Static fields for test verification - captures filter and projection from requests\n  // Volatile is used to guarantee correct cross-thread access (test thread and Jetty server thread).\n  private static volatile Expression capturedFilter = null;\n  private static volatile List<String> capturedProjection = null;\n  private static volatile Long capturedMinRowsRequested = null;\n  private static volatile Boolean capturedCaseSensitive = null;\n\n  // Static field for test credential injection - credentials to inject into /plan responses\n  // Volatile is used to guarantee correct cross-thread access (test thread and Jetty server thread).\n  private static volatile Map<String, String> testCredentials = null;\n\n  // Static field for test residual injection - residual expression to override in /plan responses.\n  // When set, all FileScanTasks in the response will have this residual instead of the default.\n  // Volatile is used to guarantee correct cross-thread access (test thread and Jetty server thread).\n  private static volatile Expression testResidual = null;\n  \n  // Static field to capture the request path of /plan requests for test verification\n  // Volatile is used to guarantee correct cross-thread access (test thread and Jetty server thread).\n  private static volatile String capturedPlanRequestPath = null;\n\n  // Failure injection fields for testing HTTP retry logic.\n  // planRequestFailCount: number of remaining /plan requests to fail before allowing success.\n  // planRequestFailStatusCode: HTTP status code to return for injected failures.\n  // planRequestCount: total number of /plan requests received (for verifying retry behavior).\n  private static final AtomicInteger planRequestFailCount = new AtomicInteger(0);\n  private static volatile int planRequestFailStatusCode = 503;\n  private static final AtomicInteger planRequestCount = new AtomicInteger(0);\n\n  IcebergRESTCatalogAdapterWithPlanSupport(Catalog catalog) {\n    super(catalog);\n    this.catalog = catalog;\n  }\n\n  /**\n   * Set the catalog prefix to be returned by /v1/config endpoint.\n   * The prefix is inserted into REST paths: /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan\n   * Used for testing prefix-based endpoint construction.\n   * Package-private as this is an implementation detail - tests should use\n   * IcebergRESTServer.setCatalogPrefix() instead.\n   *\n   * @param prefix The prefix to return in config.overrides, or null for no prefix\n   */\n  void setCatalogPrefix(String prefix) {\n    this.catalogPrefix = prefix;\n  }\n\n  /**\n   * Get the catalog prefix for testing.\n   * Package-private for servlet access.\n   */\n  String getCatalogPrefix() {\n    return this.catalogPrefix;\n  }\n\n  /**\n   * Get the filter captured from the most recent /plan request.\n   * Package-private for test access.\n   */\n  static Expression getCapturedFilter() {\n    return capturedFilter;\n  }\n\n  /**\n   * Get the projection (list of column names) captured from the most recent /plan request.\n   * Package-private for test access.\n   */\n  static List<String> getCapturedProjection() {\n    return capturedProjection;\n  }\n\n  /**\n   * Get the min-rows-requested captured from the most recent /plan request.\n   * Package-private for test access.\n   */\n  static Long getCapturedMinRowsRequested() {\n    return capturedMinRowsRequested;\n  }\n\n  /**\n   * Get the caseSensitive flag captured from the most recent /plan request.\n   * Package-private for test access.\n   */\n  static Boolean getCapturedCaseSensitive() {\n    return capturedCaseSensitive;\n  }\n\n  /**\n   * Get the request path captured from the most recent /plan request.\n   * Package-private for test access.\n   */\n  static String getCapturedPlanRequestPath() {\n    return capturedPlanRequestPath;\n  }\n\n  /**\n   * Set test credentials to inject into /plan responses.\n   * Package-private for test access.\n   *\n   * @param credentials Map of credential config (e.g., \"s3.access-key-id\" -> \"...\")\n   */\n  static void setTestCredentials(Map<String, String> credentials) {\n    testCredentials = credentials;\n  }\n\n  /**\n   * Get the test credentials configured for injection into /plan responses.\n   * Package-private for servlet access.\n   */\n  static Map<String, String> getTestCredentials() {\n    return testCredentials;\n  }\n\n  /**\n   * Set test residual expression to inject into /plan responses.\n   * When set, all FileScanTasks in the response will have this residual expression\n   * instead of the default (alwaysTrue). Used for testing client-side residual validation.\n   * Package-private for test access via IcebergRESTServer.\n   *\n   * @param residual The residual expression to inject, or null to use the default\n   */\n  static void setTestResidual(Expression residual) {\n    testResidual = residual;\n  }\n\n  /**\n   * Clear captured filter, projection, and limit. Call between tests to avoid pollution.\n   * Package-private for test access.\n   */\n  static void clearCaptured() {\n    capturedFilter = null;\n    capturedProjection = null;\n    capturedMinRowsRequested = null;\n    capturedCaseSensitive = null;\n    testCredentials = null;\n    testResidual = null;\n    capturedPlanRequestPath = null;\n    planRequestFailCount.set(0);\n    planRequestCount.set(0);\n  }\n\n  /**\n   * Configure the server to fail the next N /plan requests with the specified HTTP status code.\n   * After N failures, subsequent requests proceed normally.\n   * Used for testing HTTP retry logic in the client.\n   *\n   * @param count Number of /plan requests to fail\n   * @param statusCode HTTP status code to return for injected failures (e.g., 503, 404)\n   */\n  static void setFailNextPlanRequests(int count, int statusCode) {\n    planRequestFailCount.set(count);\n    planRequestFailStatusCode = statusCode;\n  }\n\n  /**\n   * Atomically get and decrement the remaining failure count.\n   * Returns the value before decrement. If > 0, the request should be failed.\n   * Package-private for servlet access.\n   */\n  static int getAndDecrementFailCount() {\n    return planRequestFailCount.getAndDecrement();\n  }\n\n  /**\n   * Get the HTTP status code to use for injected failures.\n   * Package-private for servlet access.\n   */\n  static int getPlanRequestFailStatusCode() {\n    return planRequestFailStatusCode;\n  }\n\n  /**\n   * Increment and return the total /plan request count.\n   * Package-private for servlet access.\n   */\n  static void incrementPlanRequestCount() {\n    planRequestCount.incrementAndGet();\n  }\n\n  /**\n   * Get the total number of /plan requests received.\n   * Package-private for test access.\n   */\n  static int getPlanRequestCount() {\n    return planRequestCount.get();\n  }\n\n  @Override\n  protected <T extends RESTResponse> T execute(\n          HTTPRequest request,\n          Class<T> responseType,\n          Consumer<ErrorResponse> errorHandler,\n          Consumer<Map<String, String>> responseHeaders,\n          ParserContext parserContext) {\n    LOG.debug(\"Executing request: {} {}\", request.method(), request.path());\n\n    // Intercept /plan requests before they reach the base adapter\n    if (isPlanTableScanRequest(request)) {\n      capturedPlanRequestPath = request.path();  // Capture the path for test verification\n      try {\n        PlanTableScanResponse response = handlePlanTableScan(request, parserContext);\n        return (T) response;\n      } catch (Exception e) {\n        LOG.error(\"Error handling plan table scan: {}\", e.getMessage(), e);\n        ErrorResponse error = ErrorResponse.builder()\n            .responseCode(500)\n            .withType(\"InternalServerError\")\n            .withMessage(\"Failed to plan table scan: \" + e.getMessage())\n            .build();\n        errorHandler.accept(error);\n        return null;\n      }\n    }\n\n    return super.execute(\n        request, responseType, errorHandler, responseHeaders, parserContext);\n  }\n\n  private boolean isPlanTableScanRequest(HTTPRequest request) {\n    return HTTPRequest.HTTPMethod.POST.equals(request.method()) &&\n           request.path().endsWith(\"/plan\");\n  }\n\n  private TableIdentifier extractTableIdentifier(String path) {\n    // Path format: /v1/namespaces/{namespace}/tables/{table}/plan\n    // or: /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan\n\n    String[] parts = path.split(\"/\");\n    int namespacesIdx = -1;\n    for (int i = 0; i < parts.length; i++) {\n      if (\"namespaces\".equals(parts[i])) {\n        namespacesIdx = i;\n        break;\n      }\n    }\n\n    if (namespacesIdx == -1 || namespacesIdx + 3 >= parts.length) {\n      throw new IllegalArgumentException(\"Invalid path format: \" + path);\n    }\n\n    String namespace = parts[namespacesIdx + 1];\n    String tableName = parts[namespacesIdx + 3]; // skip \"tables\"\n\n    return TableIdentifier.of(namespace, tableName);\n  }\n\n  /**\n   * Extract min-rows-requested from JSON string using Jackson.\n   * Iceberg 1.11 added this field, but we're on 1.10.0, so we parse it from JSON.\n   */\n  private Long extractMinRowsRequested(String jsonBody) {\n    if (jsonBody == null || jsonBody.trim().isEmpty()) {\n      return null;\n    }\n    try {\n      ObjectMapper mapper = new ObjectMapper();\n      JsonNode root = mapper.readTree(jsonBody);\n      JsonNode minRowsNode = root.get(\"min-rows-requested\");\n      return minRowsNode != null ? minRowsNode.asLong() : null;\n    } catch (Exception e) {\n      LOG.warn(\"Failed to extract min-rows-requested from JSON: {}\", e.getMessage());\n      return null;\n    }\n  }\n\n  private PlanTableScanRequest parsePlanRequest(HTTPRequest request) {\n    // The request body should be a JSON string\n    Object body = request.body();\n    if (body == null) {\n      throw new IllegalArgumentException(\"Request body is null\");\n    }\n    String jsonBody = body.toString();\n    return PlanTableScanRequestParser.fromJson(jsonBody);\n  }\n\n  private PlanTableScanResponse handlePlanTableScan(\n      HTTPRequest request,\n      ParserContext parserContext) throws Exception {\n\n    LOG.debug(\"Handling plan table scan request\");\n\n    // Extract table identifier\n    TableIdentifier tableIdent = extractTableIdentifier(request.path());\n    LOG.debug(\"Table identifier: {}\", tableIdent);\n\n    // Extract JSON body for parsing both the request and min-rows-requested\n    Object body = request.body();\n    if (body == null) {\n      throw new IllegalArgumentException(\"Request body is null\");\n    }\n    String jsonBody = body.toString();\n\n    // Extract min-rows-requested (not supported in Iceberg 1.10, so parse from JSON)\n    Long minRowsRequested = extractMinRowsRequested(jsonBody);\n    LOG.debug(\"Extracted min-rows-requested: {}\", minRowsRequested);\n\n    // Parse request\n    PlanTableScanRequest planRequest = PlanTableScanRequestParser.fromJson(jsonBody);\n    LOG.debug(\"Plan request parsed: snapshotId={}\", planRequest.snapshotId());\n\n    // Load table from catalog\n    Table table = catalog.loadTable(tableIdent);\n    LOG.debug(\"Table loaded: {}\", table);\n\n    // Create table scan\n    TableScan tableScan = table.newScan();\n\n    // Apply snapshot if specified and valid\n    if (planRequest.snapshotId() != null && planRequest.snapshotId() != 0) {\n      tableScan = tableScan.useSnapshot(planRequest.snapshotId());\n      LOG.debug(\"Using snapshot: {}\", planRequest.snapshotId());\n    } else {\n      LOG.debug(\"Using current snapshot (snapshotId was null or 0)\");\n    }\n\n    // Capture filter, projection, and limit for test verification\n    capturedFilter = planRequest.filter();\n    capturedProjection = planRequest.select();\n    capturedMinRowsRequested = minRowsRequested;\n    capturedCaseSensitive = planRequest.caseSensitive();\n    LOG.debug(\"Captured filter: {}\", capturedFilter);\n    LOG.debug(\"Captured projection: {}\", capturedProjection);\n    LOG.debug(\"Captured min-rows-requested: {}\", capturedMinRowsRequested);\n    LOG.debug(\"Captured caseSensitive: {}\", capturedCaseSensitive);\n\n    // Validate caseSensitive=false requirement\n    if (planRequest.caseSensitive()) {\n      throw new IllegalArgumentException(\"caseSensitive=true is not supported\");\n    }\n\n    // Validate that unsupported features are not requested\n    if (planRequest.startSnapshotId() != null) {\n      throw new UnsupportedOperationException(\n          \"Incremental scans are not supported in this test implementation\");\n    }\n    if (planRequest.endSnapshotId() != null) {\n      throw new UnsupportedOperationException(\n          \"Incremental scans are not supported in this test implementation\");\n    }\n    if (planRequest.statsFields() != null && !planRequest.statsFields().isEmpty()) {\n      throw new UnsupportedOperationException(\n          \"Column stats are not supported in this test implementation\");\n    }\n\n    // Execute scan planning\n    List<FileScanTask> fileScanTasks = new ArrayList<>();\n    try (CloseableIterable<FileScanTask> tasks = tableScan.planFiles()) {\n      tasks.forEach(task -> fileScanTasks.add(task));\n    }\n    LOG.debug(\"Planned {} file scan tasks\", fileScanTasks.size());\n\n    // If a test residual is configured, rebuild tasks with the injected residual\n    Expression residualOverride = testResidual;\n    List<FileScanTask> tasksToReturn;\n    if (residualOverride != null) {\n      tasksToReturn = new ArrayList<>();\n      for (FileScanTask task : fileScanTasks) {\n        tasksToReturn.add(new BaseFileScanTask(\n            task.file(),\n            task.deletes().toArray(new DeleteFile[0]),\n            SchemaParser.toJson(task.spec().schema()),\n            PartitionSpecParser.toJson(task.spec()),\n            ResidualEvaluator.of(task.spec(), residualOverride, capturedCaseSensitive)));\n      }\n      LOG.debug(\"Injected test residual into {} file scan tasks\", tasksToReturn.size());\n    } else {\n      tasksToReturn = fileScanTasks;\n    }\n\n    // Get partition specs for serialization\n    Map<Integer, shadedForDelta.org.apache.iceberg.PartitionSpec> specsById = table.specs();\n    LOG.debug(\"Table has {} partition specs\", specsById.size());\n\n    // Build response (Pattern 1: COMPLETED with direct tasks)\n    return PlanTableScanResponse.builder()\n        .withPlanStatus(PlanStatus.COMPLETED)\n        .withFileScanTasks(tasksToReturn)\n        .withSpecsById(specsById)\n        .build();\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/java/shadedForDelta/org/apache/iceberg/rest/IcebergRESTServer.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage shadedForDelta.org.apache.iceberg.rest;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Map;\nimport org.apache.hadoop.conf.Configuration;\nimport shadedForDelta.org.apache.iceberg.CatalogProperties;\nimport shadedForDelta.org.apache.iceberg.CatalogUtil;\nimport shadedForDelta.org.apache.iceberg.catalog.Catalog;\nimport shadedForDelta.org.apache.iceberg.jdbc.JdbcCatalog;\nimport shadedForDelta.org.apache.iceberg.relocated.com.google.common.collect.Maps;\nimport shadedForDelta.org.apache.iceberg.util.PropertyUtil;\nimport shadedForDelta.org.apache.iceberg.expressions.Expression;\nimport java.util.List;\n\nimport org.eclipse.jetty.server.Connector;\nimport org.eclipse.jetty.server.Server;\nimport org.eclipse.jetty.server.ServerConnector;\nimport org.eclipse.jetty.server.handler.gzip.GzipHandler;\nimport org.eclipse.jetty.servlet.ServletContextHandler;\nimport org.eclipse.jetty.servlet.ServletHolder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * HTTP server for testing Iceberg REST catalog operations with server-side scan planning support.\n * Uses Jetty to serve REST catalog endpoints and extends the standard REST catalog with a /plan\n * endpoint for server-side table scan planning. This implementation is suitable for integration\n * tests and does not require external services.\n */\npublic class IcebergRESTServer {\n  private static final Logger LOG = LoggerFactory.getLogger(IcebergRESTServer.class);\n\n  public static final String REST_PORT = \"rest.port\";\n  static final int REST_PORT_DEFAULT = 8181;\n\n  public static final String CATALOG_NAME = \"catalog.name\";\n  static final String CATALOG_NAME_DEFAULT = \"rest_backend\";\n\n  private Server httpServer;\n  private final Map<String, String> config;\n  private Catalog catalog;\n  private Map<String, String> catalogConfiguration;\n  private IcebergRESTCatalogAdapterWithPlanSupport adapter;\n\n  public IcebergRESTServer() {\n    this.config = Maps.newHashMap();\n  }\n\n  public IcebergRESTServer(Map<String, String> config) {\n    this.config = config;\n  }\n\n  private void initializeBackendCatalog() throws IOException {\n    // Translate environment variables to catalog properties\n    Map<String, String> catalogProperties = Maps.newHashMap();\n    catalogProperties.putAll(config);\n\n    // Fallback to a JDBCCatalog impl if one is not set\n    catalogProperties.putIfAbsent(CatalogProperties.CATALOG_IMPL, JdbcCatalog.class.getName());\n    catalogProperties.putIfAbsent(CatalogProperties.URI, \"jdbc:sqlite::memory:\");\n    catalogProperties.putIfAbsent(\"jdbc.schema-version\", \"V1\");\n\n    // Configure a default location if one is not specified\n    String warehouseLocation = catalogProperties.get(CatalogProperties.WAREHOUSE_LOCATION);\n\n    if (warehouseLocation == null) {\n      File tmp = java.nio.file.Files.createTempDirectory(\"iceberg_warehouse\").toFile();\n      tmp.deleteOnExit();\n      warehouseLocation = new File(tmp, \"iceberg_data\").getAbsolutePath();\n      catalogProperties.put(CatalogProperties.WAREHOUSE_LOCATION, warehouseLocation);\n\n      LOG.info(\"No warehouse location set. Defaulting to temp location: {}\", warehouseLocation);\n    }\n\n    String catalogName =\n        PropertyUtil.propertyAsString(catalogProperties, CATALOG_NAME, CATALOG_NAME_DEFAULT);\n\n    LOG.info(\"Creating {} catalog with properties: {}\", catalogName, catalogProperties);\n    this.catalog = CatalogUtil.buildIcebergCatalog(catalogName, catalogProperties, new Configuration());\n    this.catalogConfiguration = catalogProperties;\n  }\n\n  public void start(boolean join) throws Exception {\n    initializeBackendCatalog();\n\n    this.adapter = new IcebergRESTCatalogAdapterWithPlanSupport(catalog);\n    // Use custom servlet that supports the /plan endpoint\n    RESTCatalogServlet servlet = new IcebergRESTServletWithPlanSupport(adapter);\n\n    ServletContextHandler servletContext = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);\n    ServletHolder servletHolder = new ServletHolder(servlet);\n    // Serve on root path for IcebergRESTCatalogPlanningClient tests\n    servletContext.addServlet(servletHolder, \"/*\");\n    servletContext.insertHandler(new GzipHandler());\n\n    this.httpServer =\n        new Server(\n            PropertyUtil.propertyAsInt(catalogConfiguration, REST_PORT, REST_PORT_DEFAULT));\n    httpServer.setHandler(servletContext);\n    for (Connector connector : httpServer.getConnectors()) {\n      ((ServerConnector) connector).setReusePort(true);\n    }\n    httpServer.start();\n\n    if (join) {\n      httpServer.join();\n    }\n  }\n\n  public Catalog getCatalog() {\n    return catalog;\n  }\n\n  public Map<String, String> getConfiguration() {\n    return catalogConfiguration;\n  }\n\n  public int getPort() {\n    Connector[] connectors = httpServer.getConnectors();\n    if (connectors.length > 0) {\n      return ((ServerConnector) connectors[0]).getLocalPort();\n    } else {\n      throw new IllegalStateException(\"HTTP server has no connectors\");\n    }\n  }\n\n  /**\n   * Set the catalog prefix to be returned by /v1/config endpoint.\n   * Used for testing prefix-based endpoint construction in Unity Catalog metadata.\n   * Delegates to the adapter which handles the actual /v1/config request interception.\n   *\n   * @param prefix The prefix to return in config.overrides, or null for no prefix\n   */\n  public void setCatalogPrefix(String prefix) {\n    if (adapter != null) {\n      adapter.setCatalogPrefix(prefix);\n    }\n  }\n\n  /**\n   * Get the filter captured from the most recent /plan request.\n   * Delegates to adapter. For test verification.\n   */\n  public Expression getCapturedFilter() {\n    return IcebergRESTCatalogAdapterWithPlanSupport.getCapturedFilter();\n  }\n\n  /**\n   * Get the projection (list of column names) captured from the most recent /plan request.\n   * Delegates to adapter. For test verification.\n   */\n  public List<String> getCapturedProjection() {\n    return IcebergRESTCatalogAdapterWithPlanSupport.getCapturedProjection();\n  }\n\n  /**\n   * Get the limit (min-rows-requested) captured from the most recent /plan request.\n   * Delegates to adapter. For test verification.\n   */\n  public Long getCapturedLimit() {\n    return IcebergRESTCatalogAdapterWithPlanSupport.getCapturedMinRowsRequested();\n  }\n\n  /**\n   * Get the caseSensitive flag captured from the most recent /plan request.\n   * For test verification.\n   */\n  public Boolean getCapturedCaseSensitive() {\n    return IcebergRESTCatalogAdapterWithPlanSupport.getCapturedCaseSensitive();\n  }\n\n  /**\n   * Get the request path captured from the most recent /plan request.\n   * Delegates to adapter. For test verification of endpoint construction.\n   */\n  public String getCapturedPlanRequestPath() {\n    return IcebergRESTCatalogAdapterWithPlanSupport.getCapturedPlanRequestPath();\n  }\n\n  /**\n   * Set test credentials to inject into /plan responses.\n   * Used for testing credential extraction in clients.\n   *\n   * @param credentials Map of credential config (e.g., \"s3.access-key-id\" -> \"...\")\n   */\n  public void setTestCredentials(Map<String, String> credentials) {\n    IcebergRESTCatalogAdapterWithPlanSupport.setTestCredentials(credentials);\n  }\n\n  /**\n   * Set test residual expression to inject into /plan responses.\n   * When set, all FileScanTasks in the response will have this residual expression\n   * instead of the default (alwaysTrue). Used for testing client-side residual validation.\n   *\n   * @param residual The residual expression to inject, or null to use the default\n   */\n  public void setTestResidual(shadedForDelta.org.apache.iceberg.expressions.Expression residual) {\n    IcebergRESTCatalogAdapterWithPlanSupport.setTestResidual(residual);\n  }\n\n  /**\n   * Configure the server to fail the next N /plan requests with the specified HTTP status code.\n   * After N failures, subsequent requests proceed normally.\n   * Used for testing HTTP retry logic in the client.\n   *\n   * @param count Number of /plan requests to fail\n   * @param statusCode HTTP status code to return for injected failures (e.g., 503, 404)\n   */\n  public void setFailNextPlanRequests(int count, int statusCode) {\n    IcebergRESTCatalogAdapterWithPlanSupport.setFailNextPlanRequests(count, statusCode);\n  }\n\n  /**\n   * Get the total number of /plan requests received since last clearCaptured().\n   * Used for verifying retry behavior in tests.\n   */\n  public int getPlanRequestCount() {\n    return IcebergRESTCatalogAdapterWithPlanSupport.getPlanRequestCount();\n  }\n\n  /**\n   * Clear captured filter and projection. Call between tests.\n   */\n  public void clearCaptured() {\n    IcebergRESTCatalogAdapterWithPlanSupport.clearCaptured();\n  }\n\n  public void stop() throws Exception {\n    if (httpServer != null) {\n      httpServer.stop();\n    }\n  }\n\n  public static void main(String[] args) throws Exception {\n    new IcebergRESTServer().start(true);\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/java/shadedForDelta/org/apache/iceberg/rest/IcebergRESTServletWithPlanSupport.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage shadedForDelta.org.apache.iceberg.rest;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport shadedForDelta.org.apache.iceberg.rest.responses.ConfigResponse;\nimport shadedForDelta.org.apache.iceberg.rest.responses.ErrorResponse;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Extension of RESTCatalogServlet that adds support for the /plan endpoint.\n */\npublic class IcebergRESTServletWithPlanSupport extends RESTCatalogServlet {\n  private static final Logger LOG = LoggerFactory.getLogger(IcebergRESTServletWithPlanSupport.class);\n\n  private final RESTCatalogAdapter adapter;\n  private final ObjectMapper mapper;\n\n  public IcebergRESTServletWithPlanSupport(RESTCatalogAdapter adapter) {\n    super(adapter);\n    this.adapter = adapter;\n    this.mapper = RESTObjectMapper.mapper();\n  }\n\n  /**\n   * Override GET to handle /v1/config requests with catalog prefix.\n   * Note: We handle this at servlet level because the shaded RESTCatalogAdapter\n   * doesn't expose the server-side execute() method needed for interception.\n   */\n  @Override\n  protected void doGet(HttpServletRequest req, HttpServletResponse resp)\n      throws IOException {\n\n    String path = req.getPathInfo();\n\n    // Check if this is a /v1/config request\n    if (path != null && (path.equals(\"/v1/config\") || path.endsWith(\"/v1/config\"))) {\n      LOG.debug(\"Custom servlet handling /v1/config request\");\n      handleConfigRequest(req, resp);\n    } else {\n      // For all other GET requests, use standard handling\n      super.doGet(req, resp);\n    }\n  }\n\n  @Override\n  protected void doPost(HttpServletRequest req, HttpServletResponse resp)\n      throws IOException {\n\n    String path = req.getPathInfo();\n\n    // Check if this is a /plan endpoint request\n    if (path != null && path.endsWith(\"/plan\")) {\n      LOG.debug(\"Custom servlet handling /plan request for path: {}\", path);\n      handlePlanRequest(req, resp);\n    } else {\n      // For all other requests, use standard handling\n      super.doPost(req, resp);\n    }\n  }\n\n  /**\n   * Test helper for Iceberg REST /v1/config endpoint that returns optional catalog\n   * prefix, following the Iceberg REST catalog spec pattern.\n   */\n  private void handleConfigRequest(HttpServletRequest req, HttpServletResponse resp)\n      throws IOException {\n    try {\n      // Build ConfigResponse with prefix if adapter has one set\n      ConfigResponse.Builder builder = ConfigResponse.builder();\n\n      // If adapter is our custom type, get the prefix and add it to overrides\n      if (adapter instanceof IcebergRESTCatalogAdapterWithPlanSupport) {\n        IcebergRESTCatalogAdapterWithPlanSupport customAdapter =\n            (IcebergRESTCatalogAdapterWithPlanSupport) adapter;\n        String prefix = customAdapter.getCatalogPrefix();\n        if (prefix != null && !prefix.isEmpty()) {\n          LOG.info(\"Adding prefix to /v1/config response: {}\", prefix);\n          builder.withOverride(\"prefix\", prefix);\n        }\n      }\n\n      ConfigResponse config = builder.build();\n\n      // Write JSON response\n      resp.setStatus(200);\n      resp.setContentType(\"application/json\");\n      mapper.writeValue(resp.getWriter(), config);\n\n    } catch (Exception e) {\n      LOG.error(\"Error handling /v1/config request: {}\", e.getMessage(), e);\n      resp.setStatus(500);\n    }\n  }\n\n  private void handlePlanRequest(HttpServletRequest req, HttpServletResponse resp)\n      throws IOException {\n    // Track plan request count for testing retry behavior\n    IcebergRESTCatalogAdapterWithPlanSupport.incrementPlanRequestCount();\n\n    // Check if we should inject a failure for testing HTTP retry logic\n    int remainingFailures = IcebergRESTCatalogAdapterWithPlanSupport.getAndDecrementFailCount();\n    if (remainingFailures > 0) {\n      int failStatusCode = IcebergRESTCatalogAdapterWithPlanSupport.getPlanRequestFailStatusCode();\n      LOG.info(\"Injecting test failure: returning HTTP {} ({} failures remaining)\",\n          failStatusCode, remainingFailures - 1);\n      resp.setStatus(failStatusCode);\n      resp.setContentType(\"application/json\");\n      resp.getWriter().write(\n          \"{\\\"error\\\": {\\\"message\\\": \\\"Injected test failure\\\", \\\"type\\\": \\\"TestError\\\", \\\"code\\\": \"\n          + failStatusCode + \"}}\");\n      return;\n    }\n\n    try {\n      // Extract request components\n      String path = req.getPathInfo();\n      // HTTPRequest paths should not start with /, so strip it\n      if (path != null && path.startsWith(\"/\")) {\n        path = path.substring(1);\n      }\n      Map<String, String> headers = extractHeaders(req);\n      Map<String, String> queryParams = extractQueryParams(req);\n      String body = extractBody(req);\n\n      LOG.debug(\"Plan request - path: {}\", path);\n      LOG.debug(\"Plan request - body: {}\", body);\n\n      // Build HTTPRequest - body should be kept as string, not parsed\n      HTTPRequest httpRequest = adapter.buildRequest(\n          HTTPRequest.HTTPMethod.POST,\n          path,\n          queryParams,\n          headers,\n          body  // Pass body as string, not parsed\n      );\n\n      // Set up response handling\n      resp.setStatus(200);\n      resp.setContentType(\"application/json\");\n\n      // Execute the request through the adapter\n      RESTResponse response = adapter.execute(\n          httpRequest,\n          RESTResponse.class,\n          error -> handleError(resp, error),\n          responseHeaders -> responseHeaders.forEach((k, v) -> resp.setHeader(k, v))\n      );\n\n      // Write response\n      if (response != null) {\n        PrintWriter writer = resp.getWriter();\n        \n        // Check if we need to inject test credentials\n        Map<String, String> testCredentials = \n            IcebergRESTCatalogAdapterWithPlanSupport.getTestCredentials();\n        \n        if (testCredentials != null && !testCredentials.isEmpty()) {\n          // Inject storage-credentials into the response JSON\n          String responseJson = mapper.writeValueAsString(response);\n          String modifiedJson = injectStorageCredentials(responseJson, testCredentials);\n          writer.write(modifiedJson);\n        } else {\n          // No credentials to inject, write response as-is\n          mapper.writeValue(writer, response);\n        }\n        \n        writer.flush();\n      }\n\n    } catch (Exception e) {\n      LOG.error(\"Error handling /plan request: {}\", e.getMessage(), e);\n      resp.setStatus(500);\n      ErrorResponse error = ErrorResponse.builder()\n          .responseCode(500)\n          .withType(\"InternalServerError\")\n          .withMessage(\"Failed to process plan request: \" + e.getMessage())\n          .build();\n      mapper.writeValue(resp.getWriter(), error);\n    }\n  }\n\n  private void handleError(HttpServletResponse resp, ErrorResponse error) {\n    try {\n      resp.setStatus(error.code());\n      mapper.writeValue(resp.getWriter(), error);\n    } catch (IOException e) {\n      LOG.error(\"Failed to write error response: {}\", e.getMessage(), e);\n    }\n  }\n\n  private Map<String, String> extractHeaders(HttpServletRequest req) {\n    Map<String, String> headers = new HashMap<>();\n    Enumeration<String> headerNames = req.getHeaderNames();\n    while (headerNames.hasMoreElements()) {\n      String name = headerNames.nextElement();\n      headers.put(name, req.getHeader(name));\n    }\n    return headers;\n  }\n\n  private Map<String, String> extractQueryParams(HttpServletRequest req) {\n    Map<String, String> params = new HashMap<>();\n    if (req.getQueryString() != null) {\n      for (String param : req.getQueryString().split(\"&\")) {\n        String[] pair = param.split(\"=\", 2);\n        if (pair.length == 2) {\n          params.put(pair[0], pair[1]);\n        }\n      }\n    }\n    return params;\n  }\n\n  private String extractBody(HttpServletRequest req) throws IOException {\n    BufferedReader reader = req.getReader();\n    return reader.lines().collect(Collectors.joining());\n  }\n  \n  /**\n   * Inject storage-credentials section into the plan response JSON.\n   * Follows Iceberg REST catalog spec structure for credentials:\n   * {\n   *   \"storage-credentials\": [{\n   *     \"config\": {\n   *       \"s3.access-key-id\": \"...\",\n   *       ...\n   *     }\n   *   }]\n   * }\n   */\n  private String injectStorageCredentials(\n      String originalJson, \n      Map<String, String> credentials) throws IOException {\n    \n    // Parse original JSON\n    Map<String, Object> responseMap = mapper.readValue(originalJson, Map.class);\n    \n    // Build storage-credentials structure\n    Map<String, Object> credConfig = new HashMap<>(credentials);\n    Map<String, Object> credWrapper = new HashMap<>();\n    credWrapper.put(\"config\", credConfig);\n    \n    // Add as array (spec requires array even with single element)\n    responseMap.put(\"storage-credentials\", Collections.singletonList(credWrapper));\n    \n    // Serialize back to JSON\n    return mapper.writeValueAsString(responseMap);\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister",
    "content": "org.apache.iceberg.spark.source.IcebergSource\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/CloneIcebergSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.sql.Date\nimport java.sql.Timestamp\nimport java.time.LocalDateTime\nimport java.time.LocalTime\nimport java.time.format.DateTimeFormatter\nimport java.util.TimeZone\n\nimport scala.collection.JavaConverters._\nimport scala.util.Try\n\nimport org.apache.spark.sql.delta.commands.convert.ConvertUtils\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.{DataSkippingDeltaTestsUtils, StatisticsCollection}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.iceberg.Schema\nimport org.apache.iceberg.hadoop.HadoopTables\nimport org.apache.iceberg.spark.{SparkSchemaUtil => IcebergSparkSchemaUtil}\nimport org.apache.iceberg.types.Types\nimport org.apache.iceberg.types.Types.NestedField\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils.{getZoneId, microsToLocalDateTime, stringToDate, stringToTimestamp, stringToTimestampWithoutTimeZone, toJavaDate, toJavaTimestamp}\nimport org.apache.spark.sql.functions.{col, expr, from_json, lit, struct, substring}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{Decimal, DecimalType, LongType, StringType, StructField, StructType, TimestampType}\nimport org.apache.spark.unsafe.types.UTF8String\n// scalastyle:on import.ordering.noEmptyLine\n\ncase class DeltaStatsClass(\n    numRecords: Int,\n    maxValues: Map[String, String],\n    minValues: Map[String, String],\n    nullCount: Map[String, Int])\n\ntrait CloneIcebergSuiteBase extends QueryTest\n  with DataSkippingDeltaTestsUtils\n  with ConvertIcebergToDeltaUtils {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_CONVERT_ICEBERG_PARTITION_EVOLUTION_ENABLED.key, \"true\")\n  }\n\n  protected val cloneTable = \"clone\"\n\n  // The identifier of clone source, can be either path-based or name-based.\n  protected def sourceIdentifier: String\n  protected def supportedModes: Seq[String] = Seq(\"SHALLOW\")\n\n  protected def toDate(date: String): Date = {\n    toJavaDate(stringToDate(UTF8String.fromString(date)).get)\n  }\n\n  protected def physicalNamesAreEqual(\n    sourceSchema: StructType, targetSchema: StructType): Boolean = {\n\n    val sourcePathToPhysicalName = SchemaMergingUtils.explode(sourceSchema).map {\n      case (path, field) => path -> DeltaColumnMapping.getPhysicalName(field)\n    }.toMap\n\n    val targetPathToPhysicalName = SchemaMergingUtils.explode(targetSchema).map {\n      case (path, field) => path -> DeltaColumnMapping.getPhysicalName(field)\n    }.toMap\n\n    targetPathToPhysicalName.foreach {\n      case (path, physicalName) =>\n        if (!sourcePathToPhysicalName.contains(path) ||\n            physicalName != sourcePathToPhysicalName(path)) {\n          return false\n        }\n    }\n\n    sourcePathToPhysicalName.size == targetPathToPhysicalName.size\n  }\n\n  protected def testClone(testName: String)(f: String => Unit): Unit =\n    supportedModes.foreach { mode => test(s\"$testName - $mode\") { f(mode) } }\n\n  testClone(\"table with deleted files\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\n           |TBLPROPERTIES ('write.format.default' = 'PARQUET')\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n      spark.sql(s\"DELETE FROM $table WHERE data > 'a'\")\n      checkAnswer(spark.sql(s\"SELECT * from $table\"), Row(1, \"a\") :: Nil)\n\n      spark.sql(s\"CREATE TABLE $cloneTable $mode CLONE $sourceIdentifier\")\n\n      assert(SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n        DeltaLog.forTable(spark, TableIdentifier(cloneTable)).snapshot.schema,\n        new StructType().add(\"id\", LongType).add(\"data\", StringType)))\n\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Nil)\n    }\n  }\n\n  protected def runCreateOrReplace(mode: String, source: String): DataFrame = {\n    Try(spark.sql(s\"DELETE FROM $cloneTable\"))\n    spark.sql(s\"CREATE OR REPLACE TABLE $cloneTable $mode CLONE $source\")\n  }\n\n  testClone(\"table with renamed columns\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n      spark.sql(\"ALTER TABLE local.db.table RENAME COLUMN id TO id2\")\n      spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n\n      // Parquet files still have the old schema\n        assert(\n          SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n            spark.read.format(\"parquet\").load(tablePath + \"/data\").schema,\n            new StructType().add(\"id\", LongType).add(\"data\", StringType)))\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      // The converted delta table will get the updated schema\n      assert(\n        SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n          DeltaLog.forTable(spark, TableIdentifier(cloneTable)).snapshot.schema,\n          new StructType().add(\"id2\", LongType).add(\"data\", StringType)))\n\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n    }\n  }\n\n  testClone(\"create or replace table - same schema\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n\n      // Add some rows to check the initial CLONE.\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n      runCreateOrReplace(mode, sourceIdentifier)\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Row(2, \"b\") :: Nil)\n\n      // Add more rows to check incremental update with REPLACE.\n      spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n      runCreateOrReplace(mode, sourceIdentifier)\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n    }\n  }\n\n  testClone(\"create or replace table - renamed column\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n      runCreateOrReplace(mode, sourceIdentifier)\n      assert(\n        SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n          DeltaLog.forTable(spark, TableIdentifier(cloneTable)).snapshot.schema,\n          new StructType().add(\"id\", LongType).add(\"data\", StringType)))\n\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Row(2, \"b\") :: Nil)\n\n      // Rename column 'id' into column 'id2'.\n      spark.sql(\"ALTER TABLE local.db.table RENAME COLUMN id TO id2\")\n      spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n\n      // Update the cloned delta table with REPLACE.\n      runCreateOrReplace(mode, sourceIdentifier)\n      assert(\n        SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n          DeltaLog.forTable(spark, TableIdentifier(cloneTable)).snapshot.schema,\n          new StructType().add(\"id2\", LongType).add(\"data\", StringType)))\n\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n    }\n  }\n\n  testClone(\"create or replace table - deleted rows\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n      runCreateOrReplace(mode, sourceIdentifier)\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n\n      // Delete some rows from the iceberg table.\n      spark.sql(s\"DELETE FROM $table WHERE data > 'a'\")\n      checkAnswer(\n        spark.sql(s\"SELECT * from $table\"), Row(1, \"a\") :: Nil)\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      checkAnswer(spark.table(cloneTable), Row(1, \"a\") :: Nil)\n    }\n  }\n\n  testClone(\"create or replace table - schema with nested column\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, person struct<name:string,phone:int>)\n           |USING iceberg PARTITIONED BY (truncate(person.name, 2))\"\"\".stripMargin)\n\n      spark.sql(s\"INSERT INTO $table VALUES (1, ('AaAaAa', 10)), (2, ('BbBbBb', 20))\")\n      runCreateOrReplace(mode, sourceIdentifier)\n      checkAnswer(\n        spark.table(cloneTable),\n        Row(1, Row(\"AaAaAa\", 10), \"Aa\") :: Row(2, Row(\"BbBbBb\", 20), \"Bb\") :: Nil)\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(cloneTable))\n      val schemaBefore = deltaLog.update().schema\n\n      spark.sql(s\"INSERT INTO $table VALUES (3, ('AaZzZz', 30)), (4, ('CcCcCc', 40))\")\n      runCreateOrReplace(mode, sourceIdentifier)\n      checkAnswer(\n        spark.table(cloneTable),\n        Row(1, Row(\"AaAaAa\", 10), \"Aa\") :: Row(2, Row(\"BbBbBb\", 20), \"Bb\") ::\n          Row(3, Row(\"AaZzZz\", 30), \"Aa\") :: Row(4, Row(\"CcCcCc\", 40), \"Cc\") :: Nil)\n\n      assert(physicalNamesAreEqual(schemaBefore, deltaLog.update().schema))\n    }\n  }\n\n  testClone(\"create or replace table - add partition field\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (date date, id bigint, category string, price double)\n           | USING iceberg PARTITIONED BY (date)\"\"\".stripMargin)\n\n      // scalastyle:off deltahadoopconfiguration\n      val hadoopTables = new HadoopTables(spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      val icebergTable = hadoopTables.load(tablePath)\n      val icebergTableSchema =\n        IcebergSparkSchemaUtil.convert(icebergTable.schema())\n\n      val df1 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-01-01\"), 1L, \"toy\", 2.5D),\n          Row(toDate(\"2022-01-01\"), 2L, \"food\", 0.6D),\n          Row(toDate(\"2022-02-05\"), 3L, \"food\", 1.4D),\n          Row(toDate(\"2022-02-05\"), 4L, \"toy\", 10.2D)).asJava,\n        icebergTableSchema)\n\n      df1.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(cloneTable))\n      assert(deltaLog.snapshot.metadata.partitionColumns == Seq(\"date\"))\n      checkAnswer(spark.table(cloneTable), df1)\n\n      // Add a new partition field from the existing column \"category\"\n      icebergTable.refresh()\n      icebergTable.updateSpec().addField(\"category\").commit()\n\n      // Invalidate cache and load the updated partition spec\n      spark.sql(s\"REFRESH TABLE $table\")\n      val df2 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-02-05\"), 5L, \"toy\", 5.8D),\n          Row(toDate(\"2022-06-04\"), 6L, \"toy\", 20.1D)).asJava,\n        icebergTableSchema)\n\n      df2.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      assert(deltaLog.update().metadata.partitionColumns == Seq(\"date\", \"category\"))\n      // Old data of cloned Delta table has null on the new partition field.\n      checkAnswer(spark.table(cloneTable), df1.withColumn(\"category\", lit(null)).union(df2))\n      // Iceberg table projects existing value of old data to the new partition field though.\n      checkAnswer(spark.sql(s\"SELECT * FROM $table\"), df1.union(df2))\n    }\n  }\n\n  testClone(\"create or replace table - remove partition field\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (date date, id bigint, category string, price double)\n           | USING iceberg PARTITIONED BY (date)\"\"\".stripMargin)\n\n      // scalastyle:off deltahadoopconfiguration\n      val hadoopTables = new HadoopTables(spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      val icebergTable = hadoopTables.load(tablePath)\n      val icebergTableSchema =\n        IcebergSparkSchemaUtil.convert(icebergTable.schema())\n\n      val df1 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-01-01\"), 1L, \"toy\", 2.5D),\n          Row(toDate(\"2022-01-01\"), 2L, \"food\", 0.6D),\n          Row(toDate(\"2022-02-05\"), 3L, \"food\", 1.4D),\n          Row(toDate(\"2022-02-05\"), 4L, \"toy\", 10.2D)).asJava,\n        icebergTableSchema)\n\n      df1.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(cloneTable))\n      assert(deltaLog.snapshot.metadata.partitionColumns == Seq(\"date\"))\n      checkAnswer(spark.table(cloneTable), df1)\n\n      // Remove the partition field \"date\"\n      icebergTable.refresh()\n      icebergTable.updateSpec().removeField(\"date\").commit()\n\n      // Invalidate cache and load the updated partition spec\n      spark.sql(s\"REFRESH TABLE $table\")\n      val df2 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-02-05\"), 5L, \"toy\", 5.8D),\n          Row(toDate(\"2022-06-04\"), 6L, \"toy\", 20.1D)).asJava,\n        icebergTableSchema)\n\n      df2.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      assert(deltaLog.update().metadata.partitionColumns.isEmpty)\n      // Both cloned Delta table and Iceberg table has data for the removed partition field.\n      checkAnswer(spark.table(cloneTable), df1.union(df2))\n      checkAnswer(spark.table(cloneTable), spark.sql(s\"SELECT * FROM $table\"))\n    }\n  }\n\n  testClone(\"create or replace table - replace partition field\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (date date, id bigint, category string, price double)\n           | USING iceberg PARTITIONED BY (date)\"\"\".stripMargin)\n\n      // scalastyle:off deltahadoopconfiguration\n      val hadoopTables = new HadoopTables(spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      val icebergTable = hadoopTables.load(tablePath)\n      val icebergTableSchema =\n        IcebergSparkSchemaUtil.convert(icebergTable.schema())\n\n      val df1 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-01-01\"), 1L, \"toy\", 2.5D),\n          Row(toDate(\"2022-01-01\"), 2L, \"food\", 0.6D),\n          Row(toDate(\"2022-02-05\"), 3L, \"food\", 1.4D),\n          Row(toDate(\"2022-02-05\"), 4L, \"toy\", 10.2D)).asJava,\n        icebergTableSchema)\n\n      df1.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(cloneTable))\n      assert(deltaLog.snapshot.metadata.partitionColumns == Seq(\"date\"))\n      checkAnswer(spark.table(cloneTable), df1)\n\n      // Replace the partition field \"date\" with a transformed field \"month(date)\"\n      icebergTable.refresh()\n      icebergTable.updateSpec().removeField(\"date\")\n        .addField(org.apache.iceberg.expressions.Expressions.month(\"date\"))\n        .commit()\n\n      // Invalidate cache and load the updated partition spec\n      spark.sql(s\"REFRESH TABLE $table\")\n      val df2 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-02-05\"), 5L, \"toy\", 5.8D),\n          Row(toDate(\"2022-06-04\"), 6L, \"toy\", 20.1D)).asJava,\n        icebergTableSchema)\n\n      df2.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      assert(deltaLog.update().metadata.partitionColumns == Seq(\"date_month\"))\n      // Old data of cloned Delta table has null on the new partition field.\n      checkAnswer(spark.table(cloneTable),\n        df1.withColumn(\"date_month\", lit(null))\n          .union(df2.withColumn(\"date_month\", substring(col(\"date\") cast \"String\", 1, 7))))\n      // The new partition field is a hidden metadata column in Iceberg.\n      checkAnswer(\n        spark.table(cloneTable).drop(\"date_month\"),\n        spark.sql(s\"SELECT * FROM $table\"))\n    }\n  }\n\n  testClone(\"Enables column mapping table feature\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n\n      spark.sql(s\"CREATE TABLE $cloneTable $mode CLONE $sourceIdentifier\")\n      val log = DeltaLog.forTable(spark, TableIdentifier(cloneTable))\n      val protocol = log.update().protocol\n      assert(protocol.isFeatureSupported(ColumnMappingTableFeature))\n    }\n  }\n\n  testClone(\"Iceberg bucket partition should be converted to unpartitioned delta table\") { mode =>\n    withTable(table, cloneTable) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (date date, id bigint, category string, price double)\n           | USING iceberg PARTITIONED BY (bucket(2, id))\"\"\".stripMargin)\n\n      // scalastyle:off deltahadoopconfiguration\n      val hadoopTables = new HadoopTables(spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      val icebergTable = hadoopTables.load(tablePath)\n      val icebergTableSchema =\n        IcebergSparkSchemaUtil.convert(icebergTable.schema())\n\n      val df1 = spark.createDataFrame(\n        Seq(\n          Row(toDate(\"2022-01-01\"), 1L, \"toy\", 2.5D),\n          Row(toDate(\"2022-01-01\"), 2L, \"food\", 0.6D),\n          Row(toDate(\"2022-02-05\"), 3L, \"food\", 1.4D),\n          Row(toDate(\"2022-02-05\"), 4L, \"toy\", 10.2D)).asJava,\n        icebergTableSchema)\n\n      df1.writeTo(table).append()\n\n      runCreateOrReplace(mode, sourceIdentifier)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(cloneTable))\n      assert(deltaLog.snapshot.metadata.partitionColumns.isEmpty)\n      checkAnswer(spark.table(cloneTable), df1)\n      checkAnswer(spark.sql(s\"select * from $cloneTable where id = 1\"), df1.where(\"id = 1\"))\n\n      // clone should fail with flag off\n      withSQLConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_BUCKET_PARTITION_ENABLED.key -> \"false\") {\n        df1.writeTo(table).append()\n        val ae = intercept[UnsupportedOperationException] {\n          runCreateOrReplace(mode, sourceIdentifier)\n        }\n        assert(ae.getMessage.contains(\"bucket partition\"))\n      }\n    }\n  }\n\n  private def assertStats(deltaLog: DeltaLog, expectedStats: Seq[String]): Unit = {\n    val addFiles = deltaLog.update().allFiles.collectAsList().iterator().asScala\n    val addFilesSortedByIndices = addFiles.toList.sortBy { f =>\n      f.partitionValues.head._2\n    }\n\n    addFilesSortedByIndices.zip(expectedStats).foreach { case (f, expectedStat) =>\n      val parsedStats = JsonUtils.fromJson[DeltaStatsClass](\n        f.stats\n      )\n      assert(parsedStats.numRecords == 1)\n      assert(parsedStats.minValues(\"col2\") == expectedStat)\n      assert(parsedStats.maxValues(\"col2\") == expectedStat)\n    }\n  }\n\n  private def assertStateReconstruction(\n      deltaLog: DeltaLog, extractFunc: Row => String, expectedStats: Seq[String]): Unit = {\n    val snapshot = deltaLog.update()\n    val analyzedDf = snapshot.withStatsDeduplicated.queryExecution.analyzed.toString\n    val statsCol = if (analyzedDf.contains(\"stats_parsed\")) \"stats_parsed\" else \"stats\"\n    val stats = snapshot.withStats.select(statsCol)\n    val minStats = stats.select(s\"$statsCol.minValues.col2\").collect()\n    assert(minStats.map(extractFunc(_)).toSet == expectedStats.toSet)\n    val maxStats = stats.select(s\"$statsCol.maxValues.col2\").collect()\n    assert(maxStats.map(extractFunc(_)).toSet == expectedStats.toSet)\n  }\n\n  private case class DataSkippingTestParam(\n      predicate: String,\n      expectedFilesReadNum: Int,\n      expectedFilesReadIndices: Set[Int])\n\n  /**\n   * E2E test stats conversions and dataSkipping for an iceberg dataType\n   * It will write data into the iceberg table,\n   * verify stats of the addFiles and results of dataSkipping on cloned delta table\n   *\n   * @param icebergDataType Iceberg data type to test\n   *  For example, \"date\" for date dataType\n   * @param tableData Data to write into the table corresponding to data type\n   *  For example, Seq(\n   *    toDate(\"2015-01-25\"), // index 1\n   *    toDate(\"1917-02-10\") // index 2\n   *   )\n   *  It will be written into col2\n   * @param extractFunc Function to extract the value from the row containing only stat\n   *  For example, for date type, it would be row => row.getDate(0).toString\n   * @param expectedStats: Expected stat values in json string after extraction\n   *  For example, for Date(\"2025-01-25\"), it would be \"2025-01-25\"\n   * @param dataSkippingTestParams DataSkipping performed and what to verify\n   *  For example,\n   *   DataSkippingTestParam(\n   *    predicate = \"col2 < '1918-01-25'\",\n   *    expectedFilesReadNum = 1,\n   *    expectedFilesReadIndices = Set(2) // indices of files expected to select out\n   *   )\n   * @param mode Clone mode, for example, by path\n   */\n  private def testStatsConversionAndDataSkipping(\n      icebergDataType: String,\n      tableData: Seq[Any],\n      extractFunc: Row => String,\n      expectedStats: Seq[String],\n      dataSkippingTestParams: Seq[DataSkippingTestParam],\n      mode: String): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_STATS.key-> \"true\") {\n      withTable(table, cloneTable) {\n        // Create Iceberg table with date type\n        spark.sql(\n          s\"\"\"CREATE TABLE $table (col1 int, col2 $icebergDataType)\n             | USING iceberg PARTITIONED BY (col1)\"\"\".stripMargin)\n        // Write into Iceberg table\n        // scalastyle:off deltahadoopconfiguration\n        val hadoopTables = new HadoopTables(spark.sessionState.newHadoopConf())\n        // scalastyle:on deltahadoopconfiguration\n        val icebergTable = hadoopTables.load(tablePath)\n        val icebergTableSchema =\n          IcebergSparkSchemaUtil.convert(icebergTable.schema())\n        val df = spark.createDataFrame(\n          tableData.zipWithIndex.map { case (elem, index) =>\n            Row(index + 1, elem)\n          }.asJava,\n          icebergTableSchema\n        )\n        df.writeTo(table).append()\n        runCreateOrReplace(mode, sourceIdentifier)\n        val deltaLog = DeltaLog.forTable(\n          spark,\n          spark.sessionState.catalog.getTableMetadata(TableIdentifier(cloneTable))\n        )\n        // Verify converted stats against expected stats\n        assertStats(deltaLog, expectedStats)\n        assertStateReconstruction(deltaLog, extractFunc, expectedStats)\n        // Check table read results\n        checkAnswer(spark.table(cloneTable), df)\n        // Check data skipping results\n        dataSkippingTestParams.foreach { dataSkippingParam =>\n          val (predicate, expectedFilesReadNum, expectedFilesReadIndices) =\n            (dataSkippingParam.predicate, dataSkippingParam.expectedFilesReadNum,\n              dataSkippingParam.expectedFilesReadIndices)\n          val filesRead =\n            getFilesRead(spark, deltaLog, predicate, checkEmptyUnusedFilters = false)\n          try {\n            checkAnswer(\n              spark.sql(s\"select * from $cloneTable where $predicate\"), df.where(predicate)\n            )\n            assert(filesRead.size == expectedFilesReadNum)\n            assert(filesRead.map(_.partitionValues.head._2).toSet ==\n              expectedFilesReadIndices.map(_.toString))\n          } catch {\n            case e: Throwable =>\n              throw new RuntimeException(\n                s\"DataSkipping Failed for predicate: $predicate: \" +\n                  s\"expectedFilesReadNum: $expectedFilesReadNum, \" +\n                  s\"expectedFilesReadIndices: $expectedFilesReadIndices \" +\n                  s\"actualFilesRead: $filesRead\" +\n                  s\"actualFilesIndices: ${filesRead.map(_.partitionValues.head._2)}\",\n                e)\n          }\n        }\n      }\n    }\n  }\n\n  testClone(\"Convert Iceberg date type\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"date\",\n      tableData = Seq(\n        toDate(\"2015-01-25\"), // index 1\n        toDate(\"1917-02-10\"), // index 2\n        toDate(\"2050-06-23\")  // index 3\n      ),\n      extractFunc = row => row.getDate(0).toString,\n      expectedStats = Seq(\"2015-01-25\", \"1917-02-10\", \"2050-06-23\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > '2030-01-25'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(3)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 < '1917-02-11'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(2)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // int32 for 1 <= precision <= 9\n  testClone(\"Convert Iceberg decimal type - int32 in parquet\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"decimal(6, 5)\",\n      tableData = Seq(Decimal(0.123)),\n      extractFunc = row => row.getDecimal(0).toString,\n      expectedStats = Seq(\"0.12300\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > 0.123\",\n          expectedFilesReadNum = 0,\n          expectedFilesReadIndices = Set()\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 >= 0.123\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // int64 for 10 <= precision <= 18\n  testClone(\"Convert Iceberg decimal type - int64 in parquet\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"decimal(16, 4)\",\n      tableData = Seq(BigDecimal(\"123456789123.4567\")),\n      extractFunc = row => row.getDecimal(0).toString,\n      expectedStats = Seq(\"123456789123.4567\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 < 123456789123.4567\",\n          expectedFilesReadNum = 0,\n          expectedFilesReadIndices = Set()\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 <= 123456789123.4567\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // array for precision > 18\n  testClone(\"Convert Iceberg decimal type - array in parquet\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"decimal(20, 8)\",\n      tableData = Seq(\n        BigDecimal(\"111111.111\"), // index 1\n        BigDecimal(\"111111.112\"), // index 2\n        Decimal(123.5) // index 3\n      ),\n      extractFunc = row => row.getDecimal(0).toString,\n      expectedStats = Seq(\"111111.11100000\", \"111111.11200000\", \"123.50000000\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > 111111.111\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(2)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 <= 111111.111\",\n          expectedFilesReadNum = 2,\n          expectedFilesReadIndices = Set(1, 3)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 < 123.5001\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(3)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 < 123.5\",\n          expectedFilesReadNum = 0,\n          expectedFilesReadIndices = Set()\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // common decimal type used in iceberg\n  testClone(\"Convert Iceberg decimal type - mixed\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"decimal(38, 0)\",\n      tableData = Seq(\n        BigDecimal(\"123456789\"), // index 1\n        BigDecimal(\"123456789123456789\"), // index 2\n        BigDecimal(\"123456789123456789123456789\"), // index 3\n        BigDecimal(\"123456789123456789123456789123456789\"), // index 4\n        BigDecimal(\"12345678912345678912345678912345678912\") // index 5\n      ),\n      extractFunc = row => row.getDecimal(0).toString,\n      expectedStats = Seq(\n        \"123456789\",\n        \"123456789123456789\",\n        \"123456789123456789123456789\",\n        \"123456789123456789123456789123456789\",\n        \"12345678912345678912345678912345678912\"\n      ),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 <= 123456789\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 == 123456789123456789\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(2)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 < 12345678912345678912345678912345678912\",\n          expectedFilesReadNum = 4,\n          expectedFilesReadIndices = Set(1, 2, 3, 4)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 >= 123456789123456789123456789123456789\",\n          expectedFilesReadNum = 2,\n          expectedFilesReadIndices = Set(4, 5)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // Exactly on minutes\n  testClone(\"Convert Iceberg timestamptz type - 1\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"timestamp\", // spark timestamp => iceberg timestamptz\n      tableData = Seq(\n        toTimestamp(\"1908-03-15 10:1:17\")\n      ),\n      extractFunc = row => {\n        timestamptzExtracter(row, pattern = \"yyyy-MM-dd'T'HH:mm:ssXXX\")\n      },\n      expectedStats = Seq(\"1908-03-15T10:01:17+00:00\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > TIMESTAMP'1908-03-15T10:01:18+00:00'\",\n          expectedFilesReadNum = 0,\n          expectedFilesReadIndices = Set()\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 <= TIMESTAMP'1908-03-15T10:01:17+00:00'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // Fractional time\n  testClone(\"Convert Iceberg timestamptz type - 2\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"timestamp\", // spark timestamp => iceberg timestamptz\n      tableData = Seq(\n        toTimestamp(\"1997-12-11 5:40:19.23349\")\n      ),\n      extractFunc = row => {\n        timestamptzExtracter(row, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSSSXXX\")\n      },\n      expectedStats = Seq(\"1997-12-11T05:40:19.23349+00:00\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > TIMESTAMP'1997-12-11T05:40:19.233+00:00'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 <= TIMESTAMP'1997-12-11T05:40:19.10+00:00'\",\n          expectedFilesReadNum = 0,\n          expectedFilesReadIndices = Set()\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // Customized timezone\n  testClone(\"Convert Iceberg timestamptz type - 3\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"timestamp\", // spark timestamp => iceberg timestamptz\n      tableData = Seq(\n        toTimestamp(\"2077-11-11 3:23:11.23456+02:15\")\n      ),\n      extractFunc = row => {\n        timestamptzExtracter(row, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSSSXXX\")\n      },\n      expectedStats = Seq(\"2077-11-11T01:08:11.23456+00:00\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > TIMESTAMP'2077-11-11T03:23:11.23456+02:16'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        ),\n        DataSkippingTestParam(\n          predicate = \"col2 < TIMESTAMP'2077-11-11T03:23:11.23456+02:16'\",\n          expectedFilesReadNum = 0,\n          expectedFilesReadIndices = Set()\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // Exactly on minutes\n  testClone(\"Convert Iceberg timestamp type - 1\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"timestamp_ntz\", // spark timestamp_ntz => iceberg timestamp\n      tableData = Seq(\n        toTimestampNTZ(\"2024-01-02T02:04:05.123456\")\n      ),\n      extractFunc = row => {\n        row.get(0).asInstanceOf[LocalDateTime].toString\n      },\n      expectedStats = Seq(\"2024-01-02T02:04:05.123456\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > TIMESTAMP'2024-01-02T02:04:04.123456'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  // Fractional time\n  testClone(\"Convert Iceberg timestamp type - 2\") { mode =>\n    testStatsConversionAndDataSkipping(\n      icebergDataType = \"timestamp_ntz\", // spark timestamp_ntz => iceberg timestamp\n      tableData = Seq(\n        toTimestampNTZ(\"1712-4-29T06:23:49.12\")\n      ),\n      extractFunc = row => {\n        row.get(0).asInstanceOf[LocalDateTime].toString\n          .replaceAll(\"0+$\", \"\") // remove trailing zeros\n      },\n      expectedStats = Seq(\"1712-04-29T06:23:49.12\"),\n      dataSkippingTestParams = Seq(\n        DataSkippingTestParam(\n          predicate = \"col2 > TIMESTAMP'1712-04-29T06:23:49.11'\",\n          expectedFilesReadNum = 1,\n          expectedFilesReadIndices = Set(1)\n        )\n      ),\n      mode = mode\n    )\n  }\n\n  private def toTimestamp(timestamp: String): Timestamp = {\n    toJavaTimestamp(stringToTimestamp(UTF8String.fromString(timestamp),\n      getZoneId(SQLConf.get.sessionLocalTimeZone)).get)\n  }\n\n  private def toTimestampNTZ(timestampNTZ: String): LocalDateTime = {\n    microsToLocalDateTime(\n      stringToTimestampWithoutTimeZone(\n        UTF8String.fromString(timestampNTZ)\n      ).get\n    )\n  }\n\n  private def timestamptzExtracter(row: Row, pattern: String): String = {\n    val ts = row.getTimestamp(0).toLocalDateTime.atZone(\n      getZoneId(TimeZone.getDefault.getID)\n    )\n    ts.withZoneSameInstant(getZoneId(SQLConf.get.sessionLocalTimeZone))\n      .format(DateTimeFormatter.ofPattern(pattern))\n      .replace(\"UTC\", \"+00:00\")\n      .replace(\"Z\", \"+00:00\")\n  }\n}\n\nclass CloneIcebergByPathSuite extends CloneIcebergSuiteBase\n{\n  override def sourceIdentifier: String = s\"iceberg.`$tablePath`\"\n\n  test(\"negative case: select from iceberg table using path\") {\n    withTable(table) {\n      val ae = intercept[AnalysisException] {\n        sql(s\"SELECT * FROM $sourceIdentifier\")\n      }\n      assert(ae.getMessage.contains(\"does not support batch scan\"))\n    }\n  }\n}\n\n/**\n * This suite test features in Iceberg that is not directly supported by Spark.\n * See also [[NonSparkIcebergTestUtils]].\n * We do not put these tests in or extend from [[CloneIcebergSuiteBase]] because they\n * use non-Spark way to create test data.\n */\nclass CloneNonSparkIcebergByPathSuite extends QueryTest\n  with ConvertIcebergToDeltaUtils {\n\n  protected val cloneTable = \"clone\"\n\n  private def sourceIdentifier: String = s\"iceberg.`$tablePath`\"\n\n  private def runCreateOrReplace(mode: String, source: String): DataFrame = {\n    Try(spark.sql(s\"DELETE FROM $cloneTable\"))\n    spark.sql(s\"CREATE OR REPLACE TABLE $cloneTable $mode CLONE $source\")\n  }\n\n  private val mode = \"SHALLOW\"\n\n  test(\"cast Iceberg TIME to Spark long\") {\n    withTable(table, cloneTable) {\n      val schema = new Schema(\n        Seq[NestedField](\n          NestedField.required(1, \"id\", Types.IntegerType.get),\n          NestedField.required(2, \"event_time\", Types.TimeType.get)\n        ).asJava\n      )\n      val rows = Seq(\n        Map(\n          \"id\" -> 1,\n          \"event_time\" -> LocalTime.of(14, 30, 11)\n        )\n      )\n      NonSparkIcebergTestUtils.createIcebergTable(spark, tablePath, schema, rows)\n      intercept[UnsupportedOperationException] {\n        runCreateOrReplace(mode, sourceIdentifier)\n      }\n      withSQLConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_CAST_TIME_TYPE.key -> \"true\") {\n        runCreateOrReplace(mode, sourceIdentifier)\n        val expectedMicrosec = (14 * 3600 + 30 * 60 + 11) * 1000000L\n        checkAnswer(spark.table(cloneTable), Row(1, expectedMicrosec) :: Nil)\n        val clonedDeltaTable = DeltaLog.forTable(\n          spark,\n          spark.sessionState.catalog.getTableMetadata(TableIdentifier(cloneTable))\n        )\n        assert(DeltaConfigs.CAST_ICEBERG_TIME_TYPE.fromMetaData(clonedDeltaTable.update().metadata))\n      }\n    }\n  }\n\n  test(\"block data path not under table path\") {\n    withTable(table, cloneTable) {\n      val schema = new Schema(\n        Seq[NestedField](\n          NestedField.required(1, \"id\", Types.IntegerType.get),\n          NestedField.required(2, \"name\", Types.StringType.get)\n        ).asJava\n      )\n      val rows = Seq(\n        Map(\n          \"id\" -> 1,\n          \"name\" -> \"alice\"\n        )\n      )\n      val table = NonSparkIcebergTestUtils.createIcebergTable(spark, tablePath, schema, rows)\n\n      // Create a new data file not under the table path\n      withTempDir { dir =>\n        val dataPath = dir.toPath.resolve(\"out_of_table.parquet\").toAbsolutePath.toString\n        NonSparkIcebergTestUtils.writeIntoIcebergTable(\n          table,\n          Seq(Map(\"id\" -> 2, \"name\" -> \"bob\")),\n          2,\n          Some(dataPath)\n        )\n        val e = intercept[org.apache.spark.SparkException] {\n          runCreateOrReplace(mode, sourceIdentifier)\n        }\n        assert(e.getMessage.contains(\"assertion failed: Fail to relativize path\"))\n      }\n    }\n  }\n}\n\nclass CloneIcebergByNameSuite extends CloneIcebergSuiteBase\n{\n  override def sourceIdentifier: String = table\n}\n\ntrait DisablingConvertIcebergStats extends CloneIcebergSuiteBase {\n  override def sparkConf: SparkConf =\n    super.sparkConf.set(DeltaSQLConf.DELTA_CONVERT_ICEBERG_STATS.key, \"false\")\n}\n\nclass CloneIcebergByPathNoConvertStatsSuite\n  extends CloneIcebergByPathSuite\n    with DisablingConvertIcebergStats\n\nclass CloneIcebergByNameNoConvertStatsSuite\n  extends CloneIcebergByNameSuite\n    with DisablingConvertIcebergStats\n\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/ConvertIcebergToDeltaPartitionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.sql.Timestamp\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.commands.ConvertToDeltaCommand\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\nimport org.apache.iceberg.Table\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types._\n// scalastyle:on import.ordering.noEmptyLine\n\nabstract class ConvertIcebergToDeltaPartitioningUtils\n    extends QueryTest\n    with ConvertIcebergToDeltaUtils {\n\n  override protected val schemaDDL = \"id bigint, data string, size int, ts timestamp, dt date\"\n\n  protected lazy val schemaColumnNames: Seq[String] = schema.map(_.name)\n\n  /** Original iceberg data used to check the correctness of conversion. */\n  protected def initRows: Seq[String] = Seq(\n    \"1L, 'abc', 100, cast('2021-06-01 18:00:00' as timestamp), cast('2021-06-01' as date)\",\n    \"2L, 'ace', 200, cast('2022-07-01 20:00:00' as timestamp), cast('2022-07-01' as date)\"\n  )\n\n  /** Data added into both iceberg and converted delta to check post-conversion consistency. */\n  protected def incrRows: Seq[String] = Seq(\n    \"3L, 'acf', 300, cast('2023-07-01 03:00:00' as timestamp), cast('2023-07-01' as date)\"\n  )\n\n  protected override def test(testName: String, testTags: org.scalatest.Tag*)\n    (testFun: => Any)\n    (implicit pos: org.scalactic.source.Position): Unit = {\n    Seq(\"true\", \"false\").foreach { flag =>\n      val msg = if (flag == \"true\") \"- with native partition values\"\n      else \"- with inferred partition values\"\n      super.test(testName + msg, testTags : _*) {\n        withSQLConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_USE_NATIVE_PARTITION_VALUES.key -> flag) {\n          testFun\n        }\n      }(pos)\n    }\n  }\n\n  /**\n   * Creates an iceberg table with the default schema and the provided partition columns, writes\n   * some original rows into the iceberg table for conversion.\n   */\n  protected def createIcebergTable(\n      tableName: String,\n      partitionColumns: Seq[String],\n      withRows: Seq[String] = initRows): Unit = {\n    val partitionClause =\n      if (partitionColumns.nonEmpty) s\"PARTITIONED BY (${partitionColumns.mkString(\",\")})\" else \"\"\n    spark.sql(s\"CREATE TABLE $tableName ($schemaDDL) USING iceberg $partitionClause\")\n\n    withRows.foreach{ row => spark.sql(s\"INSERT INTO $tableName VALUES ($row)\") }\n  }\n\n  /**\n   * Tests ConvertToDelta on the provided iceberg table, and checks both schema and data of the\n   * converted delta table.\n   *\n   * @param tableName: the iceberg table name.\n   * @param tablePath: the iceberg table path.\n   * @param partitionSchemaDDL: the expected partition schema DDL.\n   * @param deltaPath: the location for the converted delta table.\n   */\n  protected def testConvertToDelta(\n      tableName: String,\n      tablePath: String,\n      partitionSchemaDDL: String,\n      deltaPath: String): Unit = {\n    // Convert at an external location to ease testing.\n    ConvertToDeltaCommand(\n      tableIdentifier = TableIdentifier(tablePath, Some(\"iceberg\")),\n      partitionSchema = None,\n      collectStats = true,\n      Some(deltaPath)).run(spark)\n\n    // Check the converted table schema.\n    validateConvertedSchema(\n      readIcebergHadoopTable(tablePath),\n      DeltaLog.forTable(spark, new Path(deltaPath)),\n      StructType.fromDDL(partitionSchemaDDL))\n\n    // Check converted data.\n    checkAnswer(\n      // The converted delta table will have partition columns.\n      spark.sql(s\"select ${schemaColumnNames.mkString(\",\")} from delta.`$deltaPath`\"),\n      spark.sql(s\"select * from $tableName\"))\n  }\n\n  /**\n   * Checks partition-based file skipping on the iceberg table (as parquet) and the converted delta\n   * table to verify post-conversion partition consistency.\n   *\n   * @param icebergTableName: the iceberg table name.\n   * @param icebergTablePath: the iceberg table path.\n   * @param deltaTablePath: the converted delta table path.\n   * @param filterAndFiles: a map from filter expression to the expected number of scanned files.\n   */\n  protected def checkSkipping(\n      icebergTableName: String,\n      icebergTablePath: String,\n      deltaTablePath: String,\n      filterAndFiles: Map[String, Int] = Map.empty[String, Int]): Unit = {\n    // Add the same data into both iceberg table and converted delta table.\n    writeRows(icebergTableName, deltaTablePath, incrRows)\n\n    // Disable file stats to check file skipping solely based on partition, please note this only\n    // works for optimizable partition expressions, check 'optimizablePartitionExpressions.scala'\n    // for the whole list of supported partition expressions.\n    sql(\n      s\"\"\"\n         |ALTER TABLE delta.`$deltaTablePath`\n         |SET TBLPROPERTIES (\n         |  '${DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.key}' = '0')\"\"\".stripMargin)\n\n    // Always check full scan.\n    (filterAndFiles ++ Map(\"\" -> 3)).foreach { case (filter, numFilesScanned) =>\n      val filterExpr = if (filter == \"\") \"\" else s\"where $filter\"\n      checkAnswer(\n        // The converted delta table will have partition columns.\n        spark.sql(\n          s\"\"\"SELECT ${schemaColumnNames.mkString(\",\")} FROM delta.`$deltaTablePath`\n             | WHERE $filter\"\"\".stripMargin),\n        spark.sql(s\"SELECT * FROM $icebergTableName $filterExpr\"))\n\n      // Check the raw parquet partition directories written out by Iceberg\n      checkAnswer(\n        spark.sql(s\"select * from parquet.`$icebergTablePath/data` $filterExpr\"),\n        spark.sql(s\"select * from delta.`$deltaTablePath` $filterExpr\"))\n\n      assert(\n        spark.sql(s\"select * from delta.`$deltaTablePath` $filterExpr\").inputFiles.length ==\n          numFilesScanned)\n    }\n  }\n\n  /**\n   * Validates the table schema and partition schema of the iceberg table and the converted delta\n   * table.\n   */\n  private def validateConvertedSchema(\n      icebergTable: Table,\n      convertedDeltaLog: DeltaLog,\n      expectedPartitionSchema: StructType): Unit = {\n\n    def mergeSchema(dataSchema: StructType, partitionSchema: StructType): StructType = {\n      StructType(dataSchema.fields ++\n        partitionSchema.fields.filter { partField =>\n          !dataSchema.fields.exists(f => spark.sessionState.conf.resolver(partField.name, f.name))})\n    }\n\n    val columnIds = mutable.Set[Long]()\n    val schemaWithoutMetadata =\n      SchemaMergingUtils.transformColumns(convertedDeltaLog.update().schema) { (_, field, _) =>\n        // all columns should have the columnID metadata\n        assert(DeltaColumnMapping.hasColumnId(field))\n        // all columns should have physical name metadata\n        assert(DeltaColumnMapping.hasPhysicalName(field))\n        // nest column ids should be distinct\n        val id = DeltaColumnMapping.getColumnId(field)\n        assert(!columnIds.contains(id))\n        columnIds.add(id)\n        // the id can either be a data schema id or a identity transform partition field\n        // or it is generated because it's a non-identity transform partition field\n        assert(\n          Option(icebergTable.schema().findField(id)).map(_.name()).contains(field.name) ||\n            icebergTable.spec().fields().asScala.map(_.name()).contains(field.name)\n        )\n        field.copy(metadata = Metadata.empty)\n      }\n\n    assert(schemaWithoutMetadata == mergeSchema(schema, expectedPartitionSchema))\n\n    // check partition columns\n    assert(\n      expectedPartitionSchema.map(_.name) == convertedDeltaLog.update().metadata.partitionColumns)\n  }\n\n  /**\n   * Writes the same rows into both the iceberg table and the converted delta table using the\n   * default schema.\n   */\n  protected def writeRows(\n      icebergTableName: String,\n      deltaTablePath: String,\n      rows: Seq[String]): Unit = {\n\n    // Write Iceberg\n    rows.foreach { row => spark.sql(s\"INSERT INTO $icebergTableName VALUES ($row)\") }\n\n    // Write Delta\n    rows.foreach { row =>\n      val values = row.split(\",\")\n      assert(values.length == schemaColumnNames.length)\n      val valueAsColumns =\n        values.zip(schemaColumnNames).map { case (value, column) => s\"$value AS $column\" }\n\n      val df = spark.sql(valueAsColumns.mkString(\"SELECT \", \",\", \"\"))\n      df.write.format(\"delta\").mode(\"append\").save(deltaTablePath)\n    }\n  }\n}\n\nclass ConvertIcebergToDeltaPartitioningSuite extends ConvertIcebergToDeltaPartitioningUtils {\n\n  import testImplicits._\n\n  test(\"partition by timestamp year\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"years(ts)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"ts_year int\", dir.getCanonicalPath)\n        checkSkipping(\n          table, tablePath, dir.getCanonicalPath,\n          Map(\n            \"ts < cast('2021-06-01 00:00:00' as timestamp)\" -> 1,\n            \"ts <= cast('2021-06-01 00:00:00' as timestamp)\" -> 1,\n            \"ts > cast('2021-06-01 00:00:00' as timestamp)\" -> 3,\n            \"ts > cast('2022-01-01 00:00:00' as timestamp)\" -> 2)\n        )\n      }\n    }\n  }\n\n  test(\"partition by date year\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"years(dt)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"dt_year int\", dir.getCanonicalPath)\n        checkSkipping(\n          table, tablePath, dir.getCanonicalPath,\n          Map(\n            \"dt < cast('2021-06-01' as date)\" -> 1,\n            \"dt <= cast('2021-06-01' as date)\" -> 1,\n            \"dt > cast('2021-06-01' as date)\" -> 3,\n            \"dt = cast('2022-08-01' as date)\" -> 1)\n        )\n      }\n    }\n  }\n\n  test(\"partition by timestamp day\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"days(ts)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"ts_day date\", dir.getCanonicalPath)\n        checkSkipping(\n          table, tablePath, dir.getCanonicalPath,\n          Map(\"ts < cast('2021-07-01 00:00:00' as timestamp)\" -> 1))\n      }\n    }\n  }\n\n  test(\"partition by date day\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"days(dt)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"dt_day date\", dir.getCanonicalPath)\n        checkSkipping(\n          table, tablePath, dir.getCanonicalPath,\n          Map(\n            \"dt < cast('2021-06-01' as date)\" -> 1,\n            \"dt <= cast('2021-06-01' as date)\" -> 1,\n            \"dt > cast('2021-06-01' as date)\" -> 3,\n            \"dt = cast('2022-07-01' as date)\" -> 1)\n        )\n      }\n    }\n  }\n\n  test(\"partition by truncate string\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"truncate(data, 2)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"data_trunc string\", dir.getCanonicalPath)\n        checkSkipping(\n          table, tablePath, dir.getCanonicalPath,\n          Map(\n            \"data >= 'ac'\" -> 2,\n            \"data >= 'ad'\" -> 0\n          )\n        )\n      }\n    }\n  }\n\n  test(\"partition by truncate long and int\") {\n    withTable(table) {\n      // Include both positive and negative long values in the rows: positive will be rounded up\n      // while negative will be rounded down.\n      val sampleRows = Seq(\n        \"111L, 'abc', 100, cast('2021-06-01 18:00:00' as timestamp), cast('2021-06-01' as date)\",\n        \"-11L, 'ace', -10, cast('2022-07-01 20:00:00' as timestamp), cast('2022-07-01' as date)\")\n      createIcebergTable(table, Seq(\"truncate(id, 10)\", \"truncate(size, 8)\"), sampleRows)\n\n      withTempDir { dir =>\n        val deltaPath = dir.getCanonicalPath\n        testConvertToDelta(table, tablePath, \"id_trunc long, size_trunc int\", deltaPath)\n        // TODO: make iceberg truncate partition expression optimizable and check file skipping.\n\n        // Write the same rows again into the converted delta table and make sure the partition\n        // value computed by delta are the same with iceberg.\n        writeRows(table, deltaPath, sampleRows)\n        checkAnswer(\n          spark.sql(s\"SELECT id_trunc, size_trunc FROM delta.`$deltaPath`\"),\n          Row(110L, 96) :: Row(-20L, -16) :: Row(110L, 96) :: Row(-20L, -16) :: Nil)\n      }\n    }\n  }\n\n  test(\"partition by identity\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"data\"))\n\n      withTempDir { dir =>\n        val deltaPath = new File(dir, \"delta-table\").getCanonicalPath\n        testConvertToDelta(table, tablePath, \"data string\", deltaPath)\n        checkSkipping(table, tablePath, deltaPath)\n\n        spark.read.format(\"delta\").load(deltaPath).inputFiles.foreach { fileName =>\n          val sourceFile = new File(fileName.stripPrefix(\"file:\"))\n          val targetFile = new File(dir, sourceFile.getName)\n          FileUtils.copyFile(sourceFile, targetFile)\n          val parquetFileSchema =\n            spark.read.format(\"parquet\").load(targetFile.getCanonicalPath).schema\n          if (fileName.contains(\"acf\")) { // new file written by delta\n            SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n              parquetFileSchema, StructType(schema.fields.filter(_.name != \"data\")))\n          } else {\n            SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(parquetFileSchema, schema)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"df writes and Insert Into with composite partitioning\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"years(dt), truncate(data, 3), id\"))\n\n      withTempDir { dir =>\n        val deltaPath = new File(dir, \"/delta\").getCanonicalPath\n        testConvertToDelta(\n          table,\n          tablePath,\n          \"dt_year int, data_trunc string, id bigint\",\n          deltaPath)\n\n        checkSkipping(\n          table, tablePath, deltaPath,\n          Map(\n            \"data >= 'ac'\" -> 2,\n            \"data >= 'acg'\" -> 0,\n            \"dt = cast('2022-07-01' as date) and data >= 'ac'\" -> 1\n          )\n        )\n\n        // for Dataframe, we don't need to explicitly mention partition columns\n        Seq((4L, \"bcddddd\", 400,\n          new Timestamp(TimeUnit.DAYS.toMillis(10)),\n          new java.sql.Date(TimeUnit.DAYS.toMillis(10))))\n          .toDF(schemaColumnNames: _*)\n          .write.format(\"delta\").mode(\"append\").save(deltaPath)\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(deltaPath).where(\"id = 4\")\n            .select(\"id\", \"data\", \"dt_year\", \"data_trunc\"),\n          Row(\n            4,\n            \"bcddddd\",\n            // generated partition columns\n            1970, \"bcd\") :: Nil)\n\n        val tempTablePath = dir.getCanonicalPath + \"/temp\"\n        Seq((5, \"c\", 500,\n          new Timestamp(TimeUnit.DAYS.toMillis(20)),\n          new java.sql.Date(TimeUnit.DAYS.toMillis(20)))\n        ).toDF(schemaColumnNames: _*)\n          .write.format(\"delta\").save(tempTablePath)\n\n        val e = intercept[AnalysisException] {\n          spark.sql(\n            s\"\"\"\n               | INSERT INTO delta.`$deltaPath`\n               | SELECT * from delta.`$tempTablePath`\n               |\"\"\".stripMargin)\n        }\n        assert(e.getMessage.contains(\"not enough data columns\"))\n      }\n    }\n  }\n\n  test(\"partition by timestamp month\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"months(ts)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"ts_month string\", dir.getCanonicalPath)\n        // Do NOT infer partition column type for ts_month and dt_month since: 2020-01 will be\n        // inferred as a date and cast it to 2020-01-01.\n        withSQLConf(\"spark.sql.sources.partitionColumnTypeInference.enabled\" -> \"false\") {\n          checkSkipping(\n            table,\n            tablePath,\n            dir.getCanonicalPath,\n            Map(\n              \"ts < cast('2021-06-01 00:00:00' as timestamp)\" -> 1,\n              \"ts <= cast('2021-06-01 00:00:00' as timestamp)\" -> 1,\n              \"ts > cast('2021-06-01 00:00:00' as timestamp)\" -> 3,\n              \"ts >= cast('2021-06-01 00:00:00' as timestamp)\" -> 3,\n              \"ts < cast('2021-05-01 00:00:00' as timestamp)\" -> 0,\n              \"ts > cast('2021-07-01 00:00:00' as timestamp)\" -> 2,\n              \"ts = cast('2023-07-30 00:00:00' as timestamp)\" -> 1,\n              \"ts > cast('2023-08-01 00:00:00' as timestamp)\" -> 0))\n        }\n      }\n    }\n  }\n\n  test(\"partition by date month\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"months(dt)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"dt_month string\", dir.getCanonicalPath)\n        // Do NOT infer partition column type for ts_month and dt_month since: 2020-01 will be\n        // inferred as a date and cast it to 2020-01-01.\n        withSQLConf(\"spark.sql.sources.partitionColumnTypeInference.enabled\" -> \"false\") {\n          checkSkipping(\n            table, tablePath, dir.getCanonicalPath,\n            Map(\n              \"dt < cast('2021-06-01' as date)\" -> 1,\n              \"dt <= cast('2021-06-01' as date)\" -> 1,\n              \"dt > cast('2021-06-01' as date)\" -> 3,\n              \"dt >= cast('2021-06-01' as date)\" -> 3,\n              \"dt < cast('2021-05-01' as date)\" -> 0,\n              \"dt > cast('2021-07-01' as date)\" -> 2,\n              \"dt = cast('2023-07-30' as date)\" -> 1,\n              \"dt > cast('2023-08-01' as date)\" -> 0))\n        }\n      }\n    }\n  }\n\n  test(\"partition by timestamp hour\") {\n    withTable(table) {\n      createIcebergTable(table, Seq(\"hours(ts)\"))\n\n      withTempDir { dir =>\n        testConvertToDelta(table, tablePath, \"ts_hour string\", dir.getCanonicalPath)\n        checkSkipping(table, tablePath, dir.getCanonicalPath,\n          Map(\n            \"ts < cast('2021-06-01 18:00:00' as timestamp)\" -> 1,\n            \"ts <= cast('2021-06-01 18:00:00' as timestamp)\" -> 1,\n            \"ts > cast('2021-06-01 18:00:00' as timestamp)\" -> 3,\n            \"ts >= cast('2021-06-01 18:30:00' as timestamp)\" -> 3,\n            \"ts < cast('2021-06-01 17:59:59' as timestamp)\" -> 0,\n            \"ts = cast('2021-06-01 18:30:10' as timestamp)\" -> 1,\n            \"ts > cast('2022-07-01 20:00:00' as timestamp)\" -> 2,\n            \"ts > cast('2023-07-01 02:00:00' as timestamp)\" -> 1,\n            \"ts > cast('2023-07-01 04:00:00' as timestamp)\" -> 0))\n      }\n    }\n  }\n}\n\n/////////////////////////////////\n// 5-DIGIT-YEAR TIMESTAMP TEST //\n/////////////////////////////////\nclass ConvertIcebergToDeltaPartitioningFiveDigitYearSuite\n  extends ConvertIcebergToDeltaPartitioningUtils {\n\n  override protected def initRows: Seq[String] = Seq(\n    \"1, 'abc', 100, cast('13168-11-15 18:00:00' as timestamp), cast('13168-11-15' as date)\",\n    \"2, 'abc', 200, cast('2021-08-24 18:00:00' as timestamp), cast('2021-08-24' as date)\"\n  )\n\n  override protected def incrRows: Seq[String] = Seq(\n    \"3, 'acf', 300, cast('11267-07-15 18:00:00' as timestamp), cast('11267-07-15' as date)\",\n    \"4, 'acf', 400, cast('2008-07-15 18:00:00' as timestamp), cast('2008-07-15' as date)\"\n  )\n\n  /**\n   * Checks filtering on 5-digit year based on different policies.\n   *\n   * @param icebergTableName: the iceberg table name.\n   * @param deltaTablePath: the converted delta table path.\n   * @param partitionSchemaDDL: the partition schema DDL.\n   * @param policy: time parser policy to determine 5-digit year handling.\n   * @param filters: a list of filter expressions to check.\n   */\n  private def checkFiltering(\n      icebergTableName: String,\n      deltaTablePath: String,\n      partitionSchemaDDL: String,\n      policy: String,\n      filters: Seq[String]): Unit = {\n    filters.foreach { filter =>\n      val filterExpr = if (filter == \"\") \"\" else s\"where $filter\"\n      if (policy == \"EXCEPTION\" && filterExpr != \"\" &&\n        partitionSchemaDDL != \"ts_year int\" && partitionSchemaDDL != \"ts_day date\") {\n        var thrownError = false\n        val msg = try {\n          spark.sql(s\"select * from delta.`$deltaTablePath` $filterExpr\").collect()\n        } catch {\n          case e: Throwable if e.isInstanceOf[org.apache.spark.SparkThrowable] &&\n            e.getMessage.contains(\"spark.sql.legacy.timeParserPolicy\") =>\n            thrownError = true\n          case other: Throwable => throw other\n        }\n        assert(thrownError, s\"Error message $msg is incorrect.\")\n      } else {\n        // check results of iceberg == delta\n        checkAnswer(\n          // the converted delta table will have partition columns\n          spark.sql(\n            s\"select ${schema.fields.map(_.name).mkString(\",\")} from delta.`$deltaTablePath`\"),\n          spark.sql(s\"select * from $icebergTableName\"))\n      }\n    }\n  }\n\n  Seq(\"EXCEPTION\", \"CORRECTED\", \"LEGACY\").foreach { policy =>\n    test(s\"future timestamp: partition by month when timeParserPolicy is: $policy\") {\n      withSQLConf(\"spark.sql.legacy.timeParserPolicy\" -> policy) {\n        withTable(table) {\n          createIcebergTable(table, Seq(\"months(ts)\"))\n\n          withTempDir { dir =>\n            val partitionSchemaDDL = \"ts_month string\"\n            testConvertToDelta(table, tablePath, partitionSchemaDDL, dir.getCanonicalPath)\n            checkFiltering(\n              table, dir.getCanonicalPath, partitionSchemaDDL, policy,\n              Seq(\"\",\n                \"ts > cast('2021-06-01 00:00:00' as timestamp)\",\n                \"ts < cast('12000-06-01 00:00:00' as timestamp)\",\n                \"ts >= cast('13000-06-01 00:00:00' as timestamp)\",\n                \"ts <= cast('2009-06-01 00:00:00' as timestamp)\",\n                \"ts = cast('11267-07-15 00:00:00' as timestamp)\"\n              )\n            )\n          }\n        }\n      }\n    }\n\n    test(s\"future timestamp: partition by hour when timeParserPolicy is: $policy\") {\n      withSQLConf(\"spark.sql.legacy.timeParserPolicy\" -> policy) {\n        withTable(table) {\n          createIcebergTable(table, Seq(\"hours(ts)\"))\n\n          withTempDir { dir =>\n            val partitionSchemaDDL = \"ts_hour string\"\n            testConvertToDelta(table, tablePath, partitionSchemaDDL, dir.getCanonicalPath)\n            checkFiltering(\n              table, dir.getCanonicalPath, partitionSchemaDDL, policy,\n              Seq(\"\",\n                \"ts > cast('2021-06-01 18:00:00' as timestamp)\",\n                \"ts < cast('12000-06-01 18:00:00' as timestamp)\",\n                \"ts >= cast('13000-06-01 19:00:00' as timestamp)\",\n                \"ts <= cast('2009-06-01 16:00:00' as timestamp)\",\n                \"ts = cast('11267-07-15 18:30:00' as timestamp)\"\n              )\n            )\n          }\n        }\n      }\n    }\n\n    test(s\"future timestamp: partition by year when timeParserPolicy is: $policy\") {\n      withSQLConf(\"spark.sql.legacy.timeParserPolicy\" -> policy) {\n        withTable(table) {\n          createIcebergTable(table, Seq(\"years(ts)\"))\n\n          withTempDir { dir =>\n            val partitionSchemaDDL = \"ts_year int\"\n            testConvertToDelta(table, tablePath, partitionSchemaDDL, dir.getCanonicalPath)\n            checkFiltering(\n              table, dir.getCanonicalPath, partitionSchemaDDL, policy,\n              Seq(\"\",\n                \"ts > cast('2021-06-01 18:00:00' as timestamp)\",\n                \"ts < cast('12000-06-01 18:00:00' as timestamp)\",\n                \"ts >= cast('13000-06-01 19:00:00' as timestamp)\",\n                \"ts <= cast('2009-06-01 16:00:00' as timestamp)\",\n                \"ts = cast('11267-07-15 18:30:00' as timestamp)\"\n              )\n            )\n          }\n        }\n      }\n    }\n\n    test(s\"future timestamp: partition by day when timeParserPolicy is: $policy\") {\n      withSQLConf(\"spark.sql.legacy.timeParserPolicy\" -> policy) {\n        withTable(table) {\n          createIcebergTable(table, Seq(\"days(ts)\"))\n\n          withTempDir { dir =>\n            val partitionSchemaDDL = \"ts_day date\"\n            testConvertToDelta(table, tablePath, partitionSchemaDDL, dir.getCanonicalPath)\n            checkFiltering(\n              table, dir.getCanonicalPath, partitionSchemaDDL, policy,\n              Seq(\"\",\n                \"ts > cast('2021-06-01 18:00:00' as timestamp)\",\n                \"ts < cast('12000-06-01 18:00:00' as timestamp)\",\n                \"ts >= cast('13000-06-01 19:00:00' as timestamp)\",\n                \"ts <= cast('2009-06-01 16:00:00' as timestamp)\",\n                \"ts = cast('11267-07-15 18:30:00' as timestamp)\"\n              )\n            )\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/ConvertIcebergToDeltaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.{ByteArrayOutputStream, File}\nimport java.text.SimpleDateFormat\nimport java.util.TimeZone\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.delta.commands.ConvertToDeltaCommand\nimport org.apache.spark.sql.delta.commands.convert.{ConvertUtils, IcebergTable}\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatsUtils\nimport io.delta.sql.DeltaSparkSessionExtension\nimport org.apache.hadoop.fs.Path\nimport org.apache.avro.file.{DataFileReader, DataFileWriter, SeekableByteArrayInput}\nimport org.apache.avro.generic.{GenericDatumReader, GenericDatumWriter, GenericRecord}\nimport org.apache.iceberg.{Table, TableProperties}\nimport org.apache.iceberg.hadoop.HadoopTables\nimport org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row, SparkSession, SparkSessionExtensions}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{col, expr, from_json}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.{SharedSparkSession, TestSparkSession}\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n// scalastyle:on import.ordering.noEmptyLine\n\nclass IcebergCompatibleDeltaTestSparkSession(sparkConf: SparkConf)\n    extends TestSparkSession(sparkConf) {\n  override val extensions: SparkSessionExtensions = {\n    val extensions = new SparkSessionExtensions\n    new DeltaSparkSessionExtension().apply(extensions)\n    new IcebergSparkSessionExtensions().apply(extensions)\n    extensions\n  }\n}\n\ntrait ConvertIcebergToDeltaUtils extends SharedSparkSession {\n\n  protected var warehousePath: File = null\n  protected lazy val table: String = \"local.db.table\"\n  protected lazy val tablePath: String = \"file://\" + warehousePath.getCanonicalPath + \"/db/table\"\n  protected lazy val nestedTable: String = \"local.db.nested_table\"\n  protected lazy val nestedTablePath: String =\n    \"file://\" + warehousePath.getCanonicalPath + \"/db/nested_table\"\n\n  protected def collectStatisticsStringOption(collectStats: Boolean): String = Option(collectStats)\n    .filterNot(identity).map(_ => \"NO STATISTICS\").getOrElse(\"\")\n\n\n  override def beforeAll(): Unit = {\n    warehousePath = Utils.createTempDir()\n    super.beforeAll()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    if (warehousePath != null) Utils.deleteRecursively(warehousePath)\n  }\n\n  override def afterEach(): Unit = {\n    sql(s\"DROP TABLE IF EXISTS $table\")\n    super.afterEach()\n  }\n\n  /**\n   * Setting the java default timezone, as we use java.util.TimeZone.getDefault for partition\n   * values...\n   *\n   * In production clusters, the default timezone is always set as UTC.\n   */\n  def withDefaultTimeZone(timeZoneId: String)(func: => Unit): Unit = {\n    val previousTimeZone = TimeZone.getDefault()\n    try {\n      TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId))\n      func\n    } finally {\n      TimeZone.setDefault(previousTimeZone)\n    }\n  }\n\n  override protected def createSparkSession: TestSparkSession = {\n    // Clean up any existing session (Spark 4.0 API)\n    SparkSession.getActiveSession.foreach(_.stop())\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    val session = new IcebergCompatibleDeltaTestSparkSession(sparkConf)\n    session.conf.set(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key, classOf[DeltaCatalog].getName)\n    session\n  }\n\n  protected override def sparkConf = super.sparkConf\n    .set(\n      \"spark.sql.catalog.local\", \"org.apache.iceberg.spark.SparkCatalog\")\n    .set(\n      \"spark.sql.catalog.local.type\", \"hadoop\")\n    .set(\n      \"spark.sql.catalog.local.warehouse\", warehousePath.getCanonicalPath)\n    .set(\"spark.sql.session.timeZone\", \"UTC\")\n\n  protected val schemaDDL = \"id bigint, data string, ts timestamp, dt date\"\n  protected lazy val schema = StructType.fromDDL(schemaDDL)\n\n  protected def readIcebergHadoopTable(tablePath: String): Table = {\n    // scalastyle:off deltahadoopconfiguration\n    new HadoopTables(spark.sessionState.newHadoopConf).load(tablePath)\n     // scalastyle:on deltahadoopconfiguration\n  }\n}\n\ntrait ConvertIcebergToDeltaSuiteBase\n  extends QueryTest\n  with ConvertIcebergToDeltaUtils\n  with StatsUtils {\n\n  import testImplicits._\n\n  protected def convert(tableIdentifier: String, partitioning: Option[String] = None,\n      collectStats: Boolean = true): Unit\n\n  test(\"convert with statistics\") {\n      withTable(table) {\n        spark.sql(\n          s\"\"\"CREATE TABLE $table (id bigint, data string)\n             |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n        spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n        spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n        convert(s\"iceberg.`$tablePath`\", collectStats = true)\n\n        // Check statistics\n        val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n        val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select(\n            from_json(col(\"stats\"), deltaLog.unsafeVolatileSnapshot.statsSchema).as(\"stats\"))\n          .select(\"stats.*\")\n        assert(statsDf.filter(col(\"numRecords\").isNull).count == 0)\n        val history = io.delta.tables.DeltaTable.forPath(tablePath).history()\n        assert(history.count == 1)\n      }\n  }\n\n\n  test(\"table with deleted files\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n      spark.sql(s\"DELETE FROM $table WHERE data > 'a'\")\n      checkAnswer(\n        spark.sql(s\"SELECT * from $table\"), Row(1, \"a\") :: Nil)\n\n      convert(s\"iceberg.`$tablePath`\")\n      assert(SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n        spark.read.format(\"delta\").load(tablePath).schema,\n        new StructType().add(\"id\", LongType).add(\"data\", StringType)))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tablePath),\n        Row(1, \"a\") :: Nil)\n    }\n  }\n\n\n  test(\"missing iceberg library should throw a sensical error\") {\n    val validIcebergSparkTableClassPath = ConvertUtils.icebergSparkTableClassPath\n\n    Seq(\n      () => {\n        ConvertUtils.icebergSparkTableClassPath = validIcebergSparkTableClassPath + \"2\"\n      }).foreach { makeInvalid =>\n      try {\n        makeInvalid()\n        withTable(table) {\n          spark.sql(\n            s\"\"\"CREATE TABLE $table (`1 id` bigint, 2data string)\n               |USING iceberg PARTITIONED BY (2data)\"\"\".stripMargin)\n          spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n          val e = intercept[DeltaIllegalStateException] {\n            convert(s\"iceberg.`$tablePath`\")\n          }\n          assert(e.getErrorClass == \"DELTA_MISSING_ICEBERG_CLASS\")\n        }\n      } finally {\n        ConvertUtils.icebergSparkTableClassPath = validIcebergSparkTableClassPath\n      }\n    }\n  }\n\n  test(\"non-parquet table\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\n           |TBLPROPERTIES ('write.format.default'='orc')\n           |\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n      val e = intercept[UnsupportedOperationException] {\n        convert(s\"iceberg.`$tablePath`\")\n      }\n      assert(e.getMessage.contains(\"Cannot convert\") && e.getMessage.contains(\"orc\"))\n    }\n  }\n\n  test(\"external location\") {\n    withTempDir { dir =>\n      withTable(table) {\n        spark.sql(\n          s\"\"\"CREATE TABLE $table (id bigint, data string)\n             |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n        spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n        spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n        ConvertToDeltaCommand(\n          TableIdentifier(tablePath, Some(\"iceberg\")),\n          None,\n          collectStats = true,\n          Some(dir.getCanonicalPath)).run(spark)\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getCanonicalPath),\n          Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n      }\n    }\n  }\n\n  test(\"table with renamed columns\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n      spark.sql(\"ALTER TABLE local.db.table RENAME COLUMN id TO id2\")\n      spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n      convert(s\"iceberg.`$tablePath`\")\n\n      // The converted delta table will get the updated schema\n      assert(\n        SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n          spark.read.format(\"delta\").load(tablePath).schema,\n          new StructType().add(\"id2\", LongType).add(\"data\", StringType)))\n\n      // Parquet files still have the old schema\n        assert(\n          SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n            spark.read.format(\"parquet\").load(tablePath + \"/data\").schema,\n            new StructType().add(\"id\", LongType).add(\"data\", StringType)))\n\n      val properties = readIcebergHadoopTable(tablePath).properties()\n\n      // This confirms that name mapping is not used for this case\n      assert(properties.get(TableProperties.DEFAULT_NAME_MAPPING) == null)\n\n      // As of right now, the data added before rename will be nulls.\n      checkAnswer(\n        spark.read.format(\"delta\").load(tablePath),\n        Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n    }\n  }\n\n  test(\"columns starting with numbers\") {\n    val table2 = \"local.db.table2\"\n    val tablePath2 = tablePath + \"2\"\n    withTable(table2) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table2 (1id bigint, 2data string)\n           |USING iceberg PARTITIONED BY (2data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table2 VALUES (1, 'a'), (2, 'b')\")\n      spark.sql(s\"INSERT INTO $table2 VALUES (3, 'c')\")\n      assert(spark.sql(s\"select * from $table2\").schema ==\n        new StructType().add(\"1id\", LongType).add(\"2data\", StringType))\n\n      checkAnswer(\n        spark.sql(s\"select * from $table2\"),\n        Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n\n      val properties = readIcebergHadoopTable(tablePath2).properties()\n\n      // This confirms that name mapping is not used for this case\n      assert(properties.get(TableProperties.DEFAULT_NAME_MAPPING) == null)\n\n      convert(s\"iceberg.`$tablePath2`\")\n      // The converted delta table gets the updated schema\n      assert(\n        SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n          spark.read.format(\"delta\").load(tablePath2).schema,\n          new StructType().add(\"1id\", LongType).add(\"2data\", StringType)))\n\n        // parquet file schema has been modified\n        assert(\n          spark.read.format(\"parquet\").load(tablePath2 + \"/data\").schema ==\n            new StructType()\n              .add(\"_1id\", LongType)\n              .add(\"_2data\", StringType)\n              // this is the partition column, which stays as-is\n              .add(\"2data\", StringType))\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tablePath2),\n        Row(1, \"a\") :: Row(2, \"b\") :: Row(3, \"c\") :: Nil)\n    }\n  }\n\n  test(\"nested schema\") {\n    withTable(table) {\n      def createDDL(tname: String): String =\n        s\"\"\"CREATE TABLE $tname (id bigint, person struct<name:string,phone:int>)\n           |USING iceberg PARTITIONED BY (truncate(person.name, 2))\"\"\".stripMargin\n      def insertDDL(tname: String): String =\n        s\"INSERT INTO $tname VALUES (1, ('aaaaa', 10)), (2, ('bbbbb', 20))\"\n      testNestedColumnIDs(createDDL(nestedTable), insertDDL(nestedTable))\n\n      spark.sql(createDDL(table))\n\n      spark.sql(s\"INSERT INTO $table VALUES (1, ('aaaaa', 10)), (2, ('bbbbb', 20))\")\n      checkAnswer(\n        spark.sql(s\"SELECT * from $table\"),\n        Row(1, Row(\"aaaaa\", 10)) :: Row(2, Row(\"bbbbb\", 20)) :: Nil)\n\n      convert(s\"iceberg.`$tablePath`\")\n\n      val tblSchema = spark.read.format(\"delta\").load(tablePath).schema\n\n      val expectedSchema = new StructType()\n        .add(\"id\", LongType)\n        .add(\"person\", new StructType().add(\"name\", StringType).add(\"phone\", IntegerType))\n        .add(\"person.name_trunc\", StringType)\n\n      assert(SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(expectedSchema, tblSchema))\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tablePath),\n        Row(1, Row(\"aaaaa\", 10), \"aa\") :: Row(2, Row(\"bbbbb\", 20), \"bb\") :: Nil)\n\n      assert(\n        spark.sql(s\"select * from delta.`$tablePath` where person.name > 'b'\")\n          .inputFiles.length == 1)\n\n      spark.sql(\n        s\"\"\"\n           |insert into $table (id, person)\n           |values (3, struct(\"ccccc\", 30))\n           |\"\"\".stripMargin)\n\n      val insertDataSchema = StructType.fromDDL(\"id bigint, person struct<name:string,phone:int>\")\n      val df = spark.createDataFrame(Seq(Row(3L, Row(\"ccccc\", 30))).asJava, insertDataSchema)\n      df.write.format(\"delta\").mode(\"append\").save(tablePath)\n\n        checkAnswer(\n          // check the raw parquet partition directories written out by Iceberg\n          spark.sql(s\"select * from parquet.`$tablePath/data`\"),\n          spark.sql(s\"select * from delta.`$tablePath`\")\n        )\n      assert(\n        spark.sql(s\"select * from delta.`$tablePath` where person.name > 'b'\")\n          .inputFiles.length == 2)\n    }\n  }\n\n  private def schemaTestNoDataSkipping(\n      createTableSql: String,\n      initialInsertValuesSql: String,\n      expectedInitialRows: Seq[Row],\n      expectedSchema: StructType,\n      finalInsertValuesSql: String) : Unit = {\n    withTable(table) {\n      spark.sql(s\"DROP TABLE IF EXISTS $table\")\n      spark.sql(s\"CREATE TABLE $table $createTableSql USING iceberg\")\n      spark.sql(s\"INSERT INTO $table VALUES $initialInsertValuesSql\")\n      checkAnswer(spark.sql(s\"SELECT * FROM $table\"), expectedInitialRows)\n\n      convert(s\"iceberg.`$tablePath`\")\n\n      val tblSchema = spark.read.format(\"delta\").load(tablePath).schema\n\n      assert(SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(expectedSchema, tblSchema))\n\n      checkAnswer(spark.read.format(\"delta\").load(tablePath), expectedInitialRows)\n\n      spark.sql(\n        s\"\"\"\n           |INSERT INTO $table\n           |VALUES $finalInsertValuesSql\n           |\"\"\".stripMargin)\n\n      spark.sql(\n        s\"\"\"\n           |INSERT INTO delta.`$tablePath`\n           |VALUES $finalInsertValuesSql\n           |\"\"\".stripMargin)\n\n        checkAnswer(\n          // check the raw parquet partition directories written out by Iceberg\n          spark.sql(s\"SELECT * FROM parquet.`$tablePath/data`\"),\n          spark.sql(s\"SELECT * FROM delta.`$tablePath`\")\n        )\n    }\n  }\n\n  test(\"array of struct schema\") {\n    val createTableSql = \"(id bigint, grades array<struct<class:string, score:int>>)\"\n    val initialInsertValuesSql = \"(1, array(('mat', 10), ('cs', 90))), (2, array(('eng', 80)))\"\n    val expectedInitialRows = Row(1, Seq(Row(\"mat\", 10), Row(\"cs\", 90))) ::\n      Row(2, Seq(Row(\"eng\", 80))) :: Nil\n    val arrayType = ArrayType(new StructType().add(\"class\", StringType).add(\"score\", IntegerType))\n    val expectedSchema = new StructType()\n      .add(\"id\", LongType)\n      .add(\"grades\", arrayType)\n    val finalInsertValuesSql = \"(3, array(struct(\\\"mat\\\", 100), struct(\\\"cs\\\", 100)))\"\n\n    schemaTestNoDataSkipping(createTableSql, initialInsertValuesSql, expectedInitialRows,\n      expectedSchema, finalInsertValuesSql)\n  }\n\n  test(\"map schema\") {\n    val createTableSql = \"(id bigint, grades map<string,int>)\"\n    val initialInsertValuesSql = \"(1, map('mat', 10, 'cs', 90)), (2, map('eng', 80))\"\n    val expectedInitialRows = Row(1, Map[String, Int](\"mat\" ->  10, \"cs\" -> 90)) ::\n      Row(2, Map[String, Int](\"eng\" -> 80)) :: Nil\n    val expectedSchema = new StructType()\n      .add(\"id\", LongType)\n      .add(\"grades\", MapType(StringType, IntegerType))\n    val finalInsertValuesSql = \"(3, map(\\\"mat\\\", 100, \\\"cs\\\", 100))\"\n\n    schemaTestNoDataSkipping(createTableSql, initialInsertValuesSql, expectedInitialRows,\n      expectedSchema, finalInsertValuesSql)\n  }\n\n  test(\"partition schema is not allowed\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\n           |\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n      val e = intercept[IllegalArgumentException] {\n        convert(s\"iceberg.`$tablePath`\", Some(\"data string\"))\n      }\n      assert(e.getMessage.contains(\"Partition schema cannot be specified\"))\n    }\n  }\n\n  test(\"copy over Iceberg table properties\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n      spark.sql(\n        s\"\"\"ALTER TABLE $table SET TBLPROPERTIES(\n           |  'read.split.target-size'='268435456'\n           |)\"\"\".stripMargin)\n      convert(s\"iceberg.`$tablePath`\")\n      checkAnswer(\n        spark.sql(s\"SHOW TBLPROPERTIES delta.`$tablePath`\")\n          .filter(col(\"key\").startsWith(\"read.\")),\n        Row(\"read.split.target-size\", \"268435456\") :: Nil\n      )\n    }\n  }\n\n  test(\"converted table columns have metadata containing iceberg column ids\") {\n\n    val nested1 = s\"\"\"CREATE TABLE $nestedTable (name string, age int,\n                   |pokemon array<struct<name:string,type:string>>)\n                   |USING iceberg\"\"\".stripMargin\n\n    val insert1 = s\"\"\"INSERT INTO $nestedTable VALUES ('Ash', 10,\n                   |array(struct('Charizard', 'Fire/Flying'), struct('Pikachu', 'Electric')))\n                   \"\"\".stripMargin\n    testNestedColumnIDs(nested1, insert1)\n\n    val nested2 = s\"\"\"CREATE TABLE $nestedTable (name string,\n                     |info struct<region:struct<name:string,rarity:string>, id:int>)\n                     |USING iceberg\"\"\".stripMargin\n\n    val insert2 = s\"\"\"INSERT INTO $nestedTable VALUES ('Zigzagoon',\n                     |struct(struct('Hoenn', 'Common'), 263))\n                   \"\"\".stripMargin\n    testNestedColumnIDs(nested2, insert2)\n\n    val nested3 = s\"\"\"CREATE TABLE $nestedTable (name string,\n                     |moves map<string, struct<level:int, gen:int>>)\n                     |USING iceberg\"\"\".stripMargin\n\n    val insert3 = s\"\"\"INSERT INTO $nestedTable VALUES ('Heatran',\n                     |map('Fire Fang', struct(17, 7)))\n                   \"\"\".stripMargin\n    testNestedColumnIDs(nested3, insert3)\n  }\n\n  test(\"comments are retained from Iceberg\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint comment \"myexample\", data string comment \"myexample\")\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n\n      convert(s\"iceberg.`$tablePath`\")\n\n      val readSchema = spark.read.format(\"delta\").load(tablePath).schema\n      readSchema.foreach { field =>\n        assert(field.getComment().contains(\"myexample\"))\n      }\n    }\n  }\n\n  private def testNestedColumnIDs(createString: String, insertString: String): Unit = {\n    // Nested schema\n    withTable(nestedTable) {\n      // Create table and insert into it\n      spark.sql(createString)\n\n      spark.sql(insertString)\n\n      // Convert to Delta\n      convert(s\"iceberg.`$nestedTablePath`\")\n\n      // Check Delta schema\n      val schema = DeltaLog.forTable(spark, new Path(nestedTablePath)).update().schema\n\n      // Get initial Iceberg schema\n      val icebergTable = readIcebergHadoopTable(nestedTablePath)\n      val icebergSchema = icebergTable.schema()\n\n      // Check all nested fields to see if they all have a column ID then check the iceberg schema\n      // for whether that column ID corresponds to the same column name\n      val columnIds = mutable.Set[Long]()\n      SchemaMergingUtils.transformColumns(schema) { (_, field, _) =>\n        assert(DeltaColumnMapping.hasColumnId(field))\n        // nest column ids should be distinct\n        val id = DeltaColumnMapping.getColumnId(field)\n        assert(!columnIds.contains(id))\n        columnIds.add(id)\n        // the id can either be a data schema id or a identity transform partition field\n        // or it is generated bc it's a non-identity transform partition field\n        assert(\n          Option(icebergSchema.findField(id)).map(_.name()).contains(field.name) ||\n          icebergTable.spec().fields().asScala.map(_.name()).contains(field.name)\n        )\n        field\n      }\n    }\n  }\n\n  test(\"conversion should fail if had partition evolution / multiple partition specs\") {\n    /**\n     * Per https://iceberg.apache.org/evolution/#partition-evolution, if partition evolution happens\n     * in Iceberg, multiple partition specs are persisted, thus convert to Delta cannot be\n     * supported w/o repartitioning because Delta only supports one consistent spec\n     */\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string, data2 string)\n           |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a', 'x'), (2, 'b', 'y'), (3, 'c', 'z')\")\n      // add new partition spec\n      readIcebergHadoopTable(tablePath).updateSpec().addField(\"data2\").commit()\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a', 'x'), (2, 'b', 'y'), (3, 'c', 'z')\")\n      // partition evolution happens, convert will fail\n      val e1 = intercept[DeltaAnalysisException] {\n        convert(s\"iceberg.`$tablePath`\")\n      }\n      assert(e1.getMessage.contains(IcebergTable.ERR_MULTIPLE_PARTITION_SPECS))\n\n      // drop old partition spec\n      readIcebergHadoopTable(tablePath).updateSpec().removeField(\"data2\").commit()\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a', 'x'), (2, 'b', 'y'), (3, 'c', 'z')\")\n      // partition spec is reverted, but partition evolution happens already\n      // use assert explicitly bc we do not want checks in IcebergPartitionUtils to run first\n      assert(readIcebergHadoopTable(tablePath).specs().size() > 1)\n    }\n  }\n\n  /**\n   * Strips the \"schema\" metadata key from all manifest Avro files of the table's current snapshot.\n   * This simulates a V2 writer that omits the optional schema field. Without passing specsById\n   * to ManifestFiles.read, reading such manifests causes an NPE in SchemaParser.fromJson.\n   */\n  private def stripSchemaFromManifests(table: Table): Unit = {\n    val manifests = table.currentSnapshot().dataManifests(table.io()).asScala\n    // scalastyle:off deltahadoopconfiguration\n    val conf = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    manifests.foreach { manifest =>\n      val path = new Path(manifest.path())\n      val fs = path.getFileSystem(conf)\n\n      // Read the entire manifest file into a byte array\n      val inputStream = fs.open(path)\n      val bytes = try {\n        org.apache.commons.io.IOUtils.toByteArray(inputStream)\n      } finally {\n        inputStream.close()\n      }\n\n      val datumReader = new GenericDatumReader[GenericRecord]()\n      val reader = new DataFileReader[GenericRecord](\n        new SeekableByteArrayInput(bytes), datumReader)\n\n      // Collect records and metadata\n      val records = new java.util.ArrayList[GenericRecord]()\n      while (reader.hasNext) {\n        records.add(reader.next())\n      }\n      val avroSchema = reader.getSchema\n      val metaKeys = reader.getMetaKeys.asScala.toSeq\n\n      // Write back without the \"schema\" metadata key\n      val out = new ByteArrayOutputStream()\n      val datumWriter = new GenericDatumWriter[GenericRecord](avroSchema)\n      val writer = new DataFileWriter[GenericRecord](datumWriter)\n      val reservedKeys = Set(\"schema\", \"avro.schema\", \"avro.codec\")\n      metaKeys.filterNot(reservedKeys.contains).foreach { key =>\n        writer.setMeta(key, reader.getMeta(key))\n      }\n      writer.create(avroSchema, out)\n      records.asScala.foreach(writer.append)\n      writer.close()\n      reader.close()\n\n      // Overwrite the original file\n      val outputStream = fs.create(path, true)\n      try {\n        outputStream.write(out.toByteArray)\n      } finally {\n        outputStream.close()\n      }\n    }\n  }\n\n  test(\"convert Iceberg table with manifest missing schema metadata\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\n           |TBLPROPERTIES (\"format-version\" = \"2\")\"\"\".stripMargin)\n      Seq((1L, \"a\"), (2L, \"b\")).toDF(\"id\", \"data\")\n        .write.format(\"iceberg\").mode(\"append\").saveAsTable(table)\n      // Strip the \"schema\" metadata from manifest Avro files to simulate a V2 writer\n      // that omits the optional schema field. Without passing specsById to\n      // ManifestFiles.read, this causes an NPE in SchemaParser.fromJson.\n      val iceTable = readIcebergHadoopTable(tablePath)\n      stripSchemaFromManifests(iceTable)\n      convert(s\"iceberg.`$tablePath`\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(tablePath),\n        Row(1, \"a\") :: Row(2, \"b\") :: Nil)\n    }\n  }\n\n  test(\"convert Iceberg table with not null columns\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint NOT NULL, data string, name string NOT NULL)\n           |USING iceberg PARTITIONED BY (id)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a', 'b'), (2, 'b', 'c'), (3, 'c', 'd')\")\n      convert(s\"iceberg.`$tablePath`\")\n      val data = spark.read.format(\"delta\").load(tablePath)\n      // verify data is converted properly\n      checkAnswer(data, Seq(Row(1, \"a\", \"b\"), Row(2, \"b\", \"c\"), Row(3, \"c\", \"d\")))\n\n      // Verify schema contains not null constraint where appropriate\n      val dataSchema = data.schema\n      dataSchema.foreach { field =>\n        // both partition columns and data columns should have the correct nullability\n        if (field.name == \"id\" || field.name == \"name\") {\n          assert(!field.nullable)\n        } else {\n          assert(field.nullable)\n        }\n      }\n\n      // Should not be able to write nulls to not null data column\n      var ex = intercept[Exception] {\n        spark.sql(s\"INSERT INTO $table VALUES (4, 'd', null)\")\n      }\n      // Spark 4.0+ uses uppercase NULL, 3.4+ uses capitalized Null, <3.4 has column name\n      assert(ex.getMessage.contains(\"NULL value appeared in non-nullable field\") ||\n        ex.getMessage.contains(\"Null value appeared in non-nullable field\") ||\n        ex.getMessage.contains(\"\"\"Cannot write nullable values to non-null column 'name'\"\"\"))\n\n      // Should not be able to write nulls to not null partition column\n      ex = intercept[Exception] {\n        spark.sql(s\"INSERT INTO $table VALUES (null, 'e', 'e')\")\n      }\n      // Spark 4.0+ uses uppercase NULL, 3.4+ uses capitalized Null, <3.4 has column name\n      assert(ex.getMessage.contains(\"NULL value appeared in non-nullable field\") ||\n        ex.getMessage.contains(\"Null value appeared in non-nullable field\") ||\n        ex.getMessage.contains(\"\"\"Cannot write nullable values to non-null column 'id'\"\"\"))\n\n      // Should be able to write nulls to nullable column\n      spark.sql(s\"INSERT INTO $table VALUES (5, null, 'e')\")\n    }\n  }\n\n  test(\"convert Iceberg table with case sensitive columns\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      withTable(table) {\n        spark.sql(\n          s\"\"\"CREATE TABLE $table (i bigint NOT NULL, I string)\n             |USING iceberg PARTITIONED BY (I)\"\"\".stripMargin)\n        spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b'), (3, 'c')\")\n        val ex = intercept[UnsupportedOperationException] {\n          convert(s\"iceberg.`$tablePath`\")\n        }\n\n        assert(ex.getMessage.contains(\"contains column names that only differ by case\"))\n      }\n    }\n  }\n\n  test(\"should block converting Iceberg table with name mapping\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg PARTITIONED BY (data)\n           |\"\"\".stripMargin\n      )\n      spark.sql(\n        s\"\"\"ALTER TABLE $table SET TBLPROPERTIES(\n           |  'schema.name-mapping.default' =\n           |  '[{\"field-id\": 1, \"names\": [\"my_id\"]},{\"field-id\": 2, \"names\": [\"my_data\"]}]'\n           |)\"\"\".stripMargin)\n\n      val e = intercept[UnsupportedOperationException] {\n        convert(s\"iceberg.`$tablePath`\")\n      }\n      assert(e.getMessage.contains(IcebergTable.ERR_CUSTOM_NAME_MAPPING))\n\n    }\n  }\n\n  private def testNullPartitionValues(): Unit = {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string, dt date)\n           |USING iceberg PARTITIONED BY (dt)\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table\" +\n        s\" VALUES (1, 'a', null), (2, 'b', null), (3, 'c', cast('2021-01-03' as date))\")\n      convert(s\"iceberg.`$tablePath`\")\n      val data = spark.read.format(\"delta\").load(tablePath)\n      val fmt = new SimpleDateFormat(\"yyyy-MM-dd\")\n      checkAnswer(data,\n        Seq(\n          Row(1, \"a\", null),\n          Row(2, \"b\", null),\n          Row(3, \"c\", new java.sql.Date(fmt.parse(\"2021-01-03\").getTime))))\n    }\n  }\n\n  test(\"partition columns are null\") {\n    withSQLConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_USE_NATIVE_PARTITION_VALUES.key -> \"false\") {\n      val e = intercept[RuntimeException] {\n        testNullPartitionValues()\n      }\n      assert(e.getMessage.contains(\"Failed to cast partition value\"))\n    }\n\n    withSQLConf(\n      DeltaSQLConf.DELTA_CONVERT_PARTITION_VALUES_IGNORE_CAST_FAILURE.key -> \"true\",\n      DeltaSQLConf.DELTA_CONVERT_ICEBERG_USE_NATIVE_PARTITION_VALUES.key -> \"false\") {\n      testNullPartitionValues()\n    }\n\n    // default setting should work\n    testNullPartitionValues()\n  }\n\n  test(\"arbitrary name\") {\n    def col(name: String): String = name + \"with_special_chars_;{}()\\n\\t=\"\n\n    // turns out Iceberg would fail when partition col names have special chars\n    def partCol(name: String): String = \"0123\" + name\n\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (\n          |  `${col(\"data\")}` int,\n          |  `${partCol(\"part1\")}` bigint,\n          |  `${partCol(\"part2\")}` string)\n          |USING iceberg\n          |PARTITIONED BY (\n          |  `${partCol(\"part1\")}`,\n          |   truncate(`${partCol(\"part2\")}`, 4))\n          |\"\"\".stripMargin)\n\n      spark.sql(\n        s\"\"\"\n           |INSERT INTO $table\n           |VALUES (123, 1234567890123, 'str11')\n           |\"\"\".stripMargin)\n\n      convert(s\"iceberg.`$tablePath`\")\n\n      spark.sql(\n        s\"\"\"\n           |INSERT INTO delta.`$tablePath`\n           |VALUES (456, 4567890123456, 'str22', 'str2')\n           |\"\"\".stripMargin)\n\n      checkAnswer(spark.sql(s\"select * from delta.`$tablePath`\"),\n        Seq(\n          Row(123, 1234567890123L, \"str11\", \"str1\"),\n          Row(456, 4567890123456L, \"str22\", \"str2\")))\n\n      // projection and filter\n      checkAnswer(\n        spark.table(s\"delta.`$tablePath`\")\n          .select(s\"`${col(\"data\")}`\", s\"`${partCol(\"part1\")}`\")\n          .where(s\"`${partCol(\"part2\")}` = 'str22'\"),\n        Seq(Row(456, 4567890123456L)))\n    }\n  }\n\n  test(\"partition by identity, using native partition values\") {\n    withDefaultTimeZone(\"UTC\") {\n      withTable(table) {\n        spark.sql(\n          s\"\"\"CREATE TABLE $table (\n             | data_binary binary,\n             | part_ts timestamp,\n             | part_date date,\n             | part_bool boolean,\n             | part_int integer,\n             | part_long long,\n             | part_float float,\n             | part_double double,\n             | part_decimal decimal(3, 2),\n             | part_string string\n             | )\n             |USING iceberg PARTITIONED BY (part_ts, part_date, part_bool, part_int, part_long,\n             | part_float, part_double, part_decimal, part_string)\"\"\".stripMargin)\n\n        def insertData(targetTable: String): Unit = {\n          spark.sql(\n            s\"\"\"\n               |INSERT INTO $targetTable\n               |VALUES (cast('this is binary' as binary),\n               |        cast(1635728400000 as timestamp),\n               |        cast('2021-11-15' as date),\n               |        true,\n               |        123,\n               |        12345678901234,\n               |        123.4,\n               |        123.4,\n               |        1.23,\n               |        'this is a string')\"\"\".stripMargin)\n        }\n\n        insertData(table)\n        withTempDir { dir =>\n          val deltaPath = dir.getCanonicalPath\n          ConvertToDeltaCommand(\n            tableIdentifier = TableIdentifier(tablePath, Some(\"iceberg\")),\n            partitionSchema = None,\n            collectStats = true,\n            Some(deltaPath)).run(spark)\n          // check that all the partition value types can be converted correctly\n          checkAnswer(spark.table(s\"delta.`$deltaPath`\"), spark.table(table))\n\n          insertData(s\"delta.`$deltaPath`\")\n          insertData(table)\n          // check that new writes to both Delta and Iceberg can be read back the same\n          checkAnswer(spark.table(s\"delta.`$deltaPath`\"), spark.table(table))\n        }\n      }\n    }\n  }\n\n  test(\"mor table without deletion files\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg\n           |TBLPROPERTIES (\n           |  \"format-version\" = \"2\",\n           |  \"write.delete.mode\" = \"merge-on-read\"\n           |)\n           |\"\"\".stripMargin)\n      spark.sql(s\"INSERT INTO $table VALUES (1, 'a')\")\n      spark.sql(s\"INSERT INTO $table VALUES (2, 'b')\")\n      spark.sql(s\"DELETE FROM $table WHERE id = 1\")\n      // The two rows above should've been in separate files, and DELETE will remove all rows from\n      // one file completely, in this case, we could still convert the table as Spark scan will\n      // ignore the completely deleted file.\n      convert(s\"iceberg.`$tablePath`\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(tablePath),\n        Row(2, \"b\") :: Nil\n      )\n    }\n  }\n\n  test(\"block convert: mor table with deletion files\") {\n    def setupBulkMorTable(): Unit = {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (id bigint, data string)\n           |USING iceberg\n           |TBLPROPERTIES (\n           |  \"format-version\" = \"2\",\n           |  \"write.delete.mode\" = \"merge-on-read\",\n           |  \"write.update.mode\" = \"merge-on-read\",\n           |  \"write.merge.mode\" = \"merge-on-read\"\n           |)\n           |\"\"\".stripMargin)\n      // Now we need to write a considerable amount of data in a dataframe fashion so Iceberg can\n      // combine multiple records in one Parquet file.\n      (0 until 100).map(i => (i.toLong, s\"name_$i\")).toDF(\"id\", \"data\")\n        .write.format(\"iceberg\").mode(\"append\").saveAsTable(table)\n    }\n\n    def assertConversionFailed(): Unit = {\n      // By default, conversion should fail because it is unsafe.\n      val e = intercept[UnsupportedOperationException] {\n        convert(s\"iceberg.`$tablePath`\")\n      }\n      assert(e.getMessage.contains(\"convert Iceberg table with row-level deletes\"))\n    }\n\n    // --- DELETE\n    withTable(table) {\n      setupBulkMorTable()\n      // This should touch part of one Parquet file\n      spark.sql(s\"DELETE FROM $table WHERE id = 1\")\n      // By default, conversion should fail because it is unsafe.\n      assertConversionFailed()\n    }\n\n    // --- UPDATE\n    withTable(table) {\n      setupBulkMorTable()\n      // This should touch part of one Parquet file\n      spark.sql(s\"UPDATE $table SET id = id * 2 WHERE id = 1\")\n      // By default, conversion should fail because it is unsafe.\n      assertConversionFailed()\n    }\n\n    // --- MERGE\n    withTable(table) {\n      setupBulkMorTable()\n      (0 until 100).filter(_ % 2 == 0)\n        .toDF(\"id\")\n        .createOrReplaceTempView(\"tempdata\")\n\n      // This should touch part of one Parquet file\n      spark.sql(\n        s\"\"\"\n           |MERGE INTO $table t\n           |USING tempdata s\n           |ON t.id = s.id\n           |WHEN MATCHED THEN UPDATE SET t.data = \"some_other\"\n           |\"\"\".stripMargin)\n      // By default, conversion should fail because it is unsafe.\n      assertConversionFailed()\n    }\n  }\n\n  test(\"block convert: binary type partition columns\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (\n           |  data int,\n           |  part binary)\n           |USING iceberg\n           |PARTITIONED BY (part)\n           |\"\"\".stripMargin)\n      spark.sql(s\"insert into $table values (123, cast('str1' as binary))\")\n      val e = intercept[UnsupportedOperationException] {\n        convert(s\"iceberg.`$tablePath`\")\n      }\n      assert(e.getMessage.contains(\"Unsupported partition transform expression\"))\n    }\n  }\n\n  test(\"block convert: partition transform truncate decimal type\") {\n    withTable(table) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $table (\n           |  data int,\n           |  part decimal)\n           |USING iceberg\n           |PARTITIONED BY (truncate(part, 3))\n           |\"\"\".stripMargin)\n      spark.sql(s\"insert into $table values (123, 123456)\")\n      val e = intercept[UnsupportedOperationException] {\n        convert(s\"iceberg.`$tablePath`\")\n      }\n      assert(e.getMessage.contains(\"Unsupported partition transform expression\"))\n    }\n  }\n}\n\nclass ConvertIcebergToDeltaScalaSuite extends ConvertIcebergToDeltaSuiteBase {\n  override protected def convert(\n      tableIdentifier: String,\n      partitioning: Option[String] = None,\n      collectStats: Boolean = true): Unit = {\n    if (partitioning.isDefined) {\n      io.delta.tables.DeltaTable.convertToDelta(spark, tableIdentifier, partitioning.get)\n    } else {\n      io.delta.tables.DeltaTable.convertToDelta(spark, tableIdentifier)\n    }\n  }\n}\n\nclass ConvertIcebergToDeltaSQLSuite extends ConvertIcebergToDeltaSuiteBase {\n  override protected def convert(\n      tableIdentifier: String,\n      partitioning: Option[String] = None,\n      collectStats: Boolean = true): Unit = {\n    val statement = partitioning.map(p => s\" PARTITIONED BY ($p)\").getOrElse(\"\")\n    spark.sql(s\"CONVERT TO DELTA ${tableIdentifier}${statement} \" +\n      s\"${collectStatisticsStringOption(collectStats)}\")\n  }\n\n  // TODO: Move to base once DeltaAPI support collectStats parameter\n  test(\"convert without statistics\") {\n    withTempDir { dir =>\n      withTable(table) {\n        spark.sql(\n          s\"\"\"CREATE TABLE $table (id bigint, data string)\n             |USING iceberg PARTITIONED BY (data)\"\"\".stripMargin)\n        spark.sql(s\"INSERT INTO $table VALUES (1, 'a'), (2, 'b')\")\n        spark.sql(s\"INSERT INTO $table VALUES (3, 'c')\")\n        ConvertToDeltaCommand(\n          TableIdentifier(tablePath, Some(\"iceberg\")),\n          None,\n          collectStats = false,\n          Some(dir.getCanonicalPath)).run(spark)\n\n        // Check statistics\n        val deltaLog = DeltaLog.forTable(spark, new Path(dir.getPath))\n        val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select(from_json(col(\"stats\"), deltaLog.unsafeVolatileSnapshot.statsSchema).as(\"stats\"))\n          .select(\"stats.*\")\n        assert(statsDf.filter(col(\"numRecords\").isNotNull).count == 0)\n        val history = io.delta.tables.DeltaTable.forPath(dir.getPath).history()\n        assert(history.count == 1)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/ConvertToIcebergSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, IOException}\nimport java.net.ServerSocket\n\nimport org.scalatest.concurrent.Eventually\nimport org.scalatest.time.SpanSugar._\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.sql.{QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType, CatalogStorageFormat}\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.types.{IntegerType, StringType, StructType, StructField}\nimport org.apache.spark.util.Utils\n\n/**\n * This test suite relies on an external Hive metastore (HMS) instance to run.\n *\n * A standalone HMS can be created using the following docker command.\n *  ************************************************************\n *  docker run -d -p 9083:9083 --env SERVICE_NAME=metastore \\\n *  --name metastore-standalone apache/hive:4.0.0-beta-1\n *  ************************************************************\n *  The URL of this standalone HMS is thrift://localhost:9083\n *\n *  By default this hms will use `/opt/hive/data/warehouse` as warehouse path.\n *  Please make sure this path exists prior to running the suite.\n */\nclass ConvertToIcebergSuite extends QueryTest with Eventually {\n\n  private var _sparkSession: SparkSession = null\n  private var _sparkSessionWithDelta: SparkSession = null\n  private var _sparkSessionWithIceberg: SparkSession = null\n\n  private val PORT = 9083\n  private val WAREHOUSE_PATH = \"/opt/hive/data/warehouse/\"\n\n  private val testTableName: String = \"deltatable\"\n  private var testTablePath: String = s\"$WAREHOUSE_PATH$testTableName\"\n\n  override def spark: SparkSession = _sparkSession\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    if (hmsReady(PORT)) {\n      _sparkSessionWithDelta = createSparkSessionWithDelta()\n      _sparkSessionWithIceberg = createSparkSessionWithIceberg()\n      require(!_sparkSessionWithDelta.eq(_sparkSessionWithIceberg), \"separate sessions expected\")\n    }\n  }\n\n  override def afterEach(): Unit = {\n    super.afterEach()\n    if (hmsReady(PORT)) {\n      _sparkSessionWithDelta.sql(s\"DROP TABLE IF EXISTS $testTableName\")\n    }\n    Utils.deleteRecursively(new File(testTablePath))\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    SparkContext.getActive.foreach(_.stop())\n  }\n\n  test(\"enforceSupportInCatalog\") {\n    var testTable = new CatalogTable(\n      TableIdentifier(\"table\"),\n      CatalogTableType.EXTERNAL,\n      CatalogStorageFormat(None, None, None, None, compressed = false, Map.empty),\n      new StructType(Array(StructField(\"col1\", IntegerType), StructField(\"col2\", StringType))))\n    var testMetadata = Metadata()\n\n    assert(UniversalFormat.enforceSupportInCatalog(testTable, testMetadata).isEmpty)\n\n    testTable = testTable.copy(properties = Map(\"table_type\" -> \"iceberg\"))\n    var resultTable = UniversalFormat.enforceSupportInCatalog(testTable, testMetadata)\n    assert(resultTable.nonEmpty)\n    assert(!resultTable.get.properties.contains(\"table_type\"))\n\n    testMetadata = testMetadata.copy(\n      configuration = Map(\"delta.universalFormat.enabledFormats\" -> \"iceberg\"))\n    assert(UniversalFormat.enforceSupportInCatalog(testTable, testMetadata).isEmpty)\n\n    testTable = testTable.copy(properties = Map.empty)\n    resultTable = UniversalFormat.enforceSupportInCatalog(testTable, testMetadata)\n    assert(resultTable.isEmpty)\n  }\n\n  test(\"basic test - managed table created with SQL\") {\n    if (hmsReady(PORT)) {\n      runDeltaSql(\n        s\"\"\"CREATE TABLE `${testTableName}` (col1 INT) USING DELTA\n           |TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.enableIcebergCompatV2' = 'true',\n           |  'delta.universalFormat.enabledFormats' = 'iceberg'\n           |)\"\"\".stripMargin)\n      runDeltaSql(s\"INSERT INTO `$testTableName` VALUES (123)\")\n      verifyReadWithIceberg(testTableName, Seq(Row(123)))\n    }\n  }\n\n  test(\"basic test - catalog table created with DataFrame\") {\n    if (hmsReady(PORT)) {\n      withDeltaSparkSession { deltaSpark =>\n        withDefaultTablePropsInSQLConf {\n          deltaSpark.range(10).write.format(\"delta\")\n            .option(\"path\", testTablePath)\n            .saveAsTable(testTableName)\n        }\n      }\n      withDeltaSparkSession { deltaSpark =>\n        deltaSpark.range(10, 20, 1)\n          .write.format(\"delta\").mode(\"append\")\n          .option(\"path\", testTablePath)\n          .saveAsTable(testTableName)\n      }\n      verifyReadWithIceberg(testTableName, 0 to 19 map (Row(_)))\n    }\n  }\n\n  def runDeltaSql(sqlStr: String): Unit = {\n    withDeltaSparkSession { deltaSpark =>\n      deltaSpark.sql(sqlStr)\n    }\n  }\n\n  def verifyReadWithIceberg(tableName: String, expectedAnswer: Seq[Row]): Unit = {\n    withIcebergSparkSession { icebergSparkSession =>\n      eventually(timeout(10.seconds)) {\n        icebergSparkSession.sql(s\"REFRESH TABLE ${tableName}\")\n        val icebergDf = icebergSparkSession.read.format(\"iceberg\").load(tableName)\n        checkAnswer(icebergDf, expectedAnswer)\n      }\n    }\n  }\n\n\n  def withDefaultTablePropsInSQLConf(f: => Unit): Unit = {\n    withSQLConf(\n      DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\",\n      DeltaConfigs.ICEBERG_COMPAT_V1_ENABLED.defaultTablePropertyKey -> \"true\",\n      DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.defaultTablePropertyKey -> \"iceberg\"\n    ) { f }\n  }\n\n  def withDeltaSparkSession[T](f: SparkSession => T): T = {\n    withSparkSession(_sparkSessionWithDelta, f)\n  }\n\n  def withIcebergSparkSession[T](f: SparkSession => T): T = {\n    withSparkSession(_sparkSessionWithIceberg, f)\n  }\n\n  def withSparkSession[T](sessionToUse: SparkSession, f: SparkSession => T): T = {\n    try {\n      SparkSession.setDefaultSession(sessionToUse)\n      SparkSession.setActiveSession(sessionToUse)\n      _sparkSession = sessionToUse\n      f(sessionToUse)\n    } finally {\n      SparkSession.clearActiveSession()\n      SparkSession.clearDefaultSession()\n      _sparkSession = null\n    }\n  }\n\n  protected def createSparkSessionWithDelta(): SparkSession = {\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    val sparkSession = SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"DeltaSession\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .config(\"hive.metastore.uris\", s\"thrift://localhost:$PORT\")\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    sparkSession\n  }\n\n  protected def createSparkSessionWithIceberg(): SparkSession = {\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    val sparkSession = SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"IcebergSession\")\n      .config(\"spark.sql.extensions\",\n        \"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.iceberg.spark.SparkSessionCatalog\")\n      .config(\"hive.metastore.uris\", s\"thrift://localhost:$PORT\")\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    sparkSession\n  }\n\n  def hmsReady(port: Int): Boolean = {\n    var ss: ServerSocket = null\n    try {\n      ss = new ServerSocket(port)\n      ss.setReuseAddress(true)\n      logWarning(\"No HMS detected, test suite will not run\")\n      return false\n    } catch {\n      case e: IOException =>\n    } finally {\n      if (ss != null) {\n        try ss.close()\n        catch {\n          case e: IOException =>\n        }\n      }\n    }\n    true\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/NonSparkIcebergTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.iceberg.{DataFile, DataFiles, Files, PartitionSpec, Schema, Table}\nimport org.apache.iceberg.data.GenericRecord\nimport org.apache.iceberg.data.parquet.GenericParquetWriter\nimport org.apache.iceberg.hadoop.HadoopTables\nimport org.apache.iceberg.io.FileAppender\nimport org.apache.iceberg.parquet.Parquet\nimport org.apache.iceberg.types.Types\nimport org.apache.iceberg.types.Types.NestedField\n\nimport org.apache.spark.sql.SparkSession\n\nobject NonSparkIcebergTestUtils {\n\n  /**\n   * Create an Iceberg table with formats/data types not supported by Spark.\n   * This is primarily used for compatibility tests. It includes the following features\n   * * TIME data type that is not supported by Spark.\n   * @param location Iceberg table root path\n   * @param schema   Iceberg table schema\n   * @param rows     Data rows we write into the table\n   * @param dataFileIdx index of the parquet file going to be written in the data folder\n   */\n  def createIcebergTable(\n       spark: SparkSession,\n       location: String,\n       schema: Schema,\n       rows: Seq[Map[String, Any]],\n       dataFileIdx: Int = 1): Table = {\n    // scalastyle:off deltahadoopconfiguration\n    val tables = new HadoopTables(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    val table = tables.create(\n      schema,\n      PartitionSpec.unpartitioned(),\n      location\n    )\n\n    writeIntoIcebergTable(table, rows, dataFileIdx)\n    table\n  }\n\n  /**\n   * Writes into an Iceberg table with formats/data types not supported by Spark.\n   * This is primarily used for compatibility tests. It includes the following features\n   * * TIME data type that is not supported by Spark.\n   * @param table Iceberg table\n   * @param rows  Data rows we write into the table\n   * @param dataFileIdx index of the parquet file going to be written in the data folder\n   * @param dataPath Optional path to write the data file\n   */\n  def writeIntoIcebergTable(\n      table: Table,\n      rows: Seq[Map[String, Any]],\n      dataFileIdx: Int,\n      dataPath: Option[String] = None): Unit = {\n    val schema = table.schema()\n    val records = rows.map { row =>\n      val record = GenericRecord.create(schema)\n      row.foreach {\n        case (key, value) => record.setField(key, value)\n      }\n      record\n    }\n\n    val parquetLocation = dataPath.getOrElse(table.location() + s\"/data/$dataFileIdx.parquet\")\n\n    val fileAppender: FileAppender[GenericRecord] = Parquet\n      .write(table.io().newOutputFile(parquetLocation))\n      .schema(schema)\n      .createWriterFunc(GenericParquetWriter.create _)  // Iceberg 1.10.0 API\n      .overwrite()\n      .build();\n    try {\n      fileAppender.addAll(records.asJava)\n    } finally {\n      fileAppender.close\n    }\n\n    val dataFile = DataFiles.builder(PartitionSpec.unpartitioned())\n      .withInputFile(table.io().newInputFile(parquetLocation))\n      .withMetrics(fileAppender.metrics())\n      .build();\n\n    table\n      .newAppend\n      .appendFile(dataFile)\n      .commit\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/commands/convert/IcebergPartitionConverterSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.lang.{Integer => JInt, Long => JLong}\nimport java.math.BigDecimal\nimport java.util.{List => JList}\n\nimport scala.collection.JavaConverters._\n\nimport shadedForDelta.org.apache.iceberg.{PartitionData, PartitionSpec, Schema}\nimport shadedForDelta.org.apache.iceberg.transforms._\nimport shadedForDelta.org.apache.iceberg.types.Conversions\nimport shadedForDelta.org.apache.iceberg.types.Types._\n\nimport org.apache.spark.SparkFunSuite\n\nclass IcebergPartitionConverterSuite extends SparkFunSuite {\n\n  test(\"convert partition simple case, including empty and null\") {\n    val icebergSchema = new Schema(10, Seq[NestedField](\n      NestedField.required(1, \"col_int\", IntegerType.get),\n      NestedField.required(2, \"col_long\", LongType.get),\n      NestedField.required(3, \"col_st\", StringType.get)\n    ).asJava)\n\n    val icebergPartSpec = PartitionSpec\n      .builderFor(icebergSchema)\n      .identity(\"col_int\")\n      .truncate(\"col_st\", 3)\n      .identity(\"col_long\")\n      .build\n\n    val physicalNameToField = Map(\n      \"pname1\" -> icebergPartSpec.fields().get(0),\n      \"pname2\" -> icebergPartSpec.fields().get(1),\n      \"pname3\" -> icebergPartSpec.fields().get(2)\n    )\n\n    val partitionConverter = IcebergPartitionConverter(icebergSchema, physicalNameToField)\n\n    val partData = new PartitionData(\n      StructType.of(\n        NestedField.required(1000, \"col_int\", IntegerType.get),\n        NestedField.required(1001, \"col_st\", StringType.get)\n      )\n    )\n    partData.put(0, 100)\n    partData.put(1, \"alo\")\n    assertResult(\"Map(pname1 -> 100, pname2 -> alo, pname3 -> null)\")(\n      partitionConverter.toDelta(partData).toString)\n\n    val partData2 = new PartitionData(\n      StructType.of(\n        NestedField.required(1000, \"col_int\", IntegerType.get),\n        NestedField.required(1001, \"col_long\", LongType.get),\n        NestedField.required(1002, \"col_st\", StringType.get)\n      )\n    )\n    partData2.put(2, 100000000000000L)\n    partData2.put(1, null)\n    assertResult(\"Map(pname1 -> null, pname2 -> null, pname3 -> 100000000000000)\")(\n      partitionConverter.toDelta(partData2).toString)\n  }\n\n  test(\"convert partition with complex types\") {\n    val icebergSchema = new Schema(10, Seq[NestedField](\n      NestedField.required(4, \"col_date\", DateType.get),\n      NestedField.required(5, \"col_ts\", TimestampType.withZone),\n      NestedField.required(6, \"col_tsnz\", TimestampType.withoutZone)\n    ).asJava)\n\n    val icebergPartSpec = PartitionSpec\n      .builderFor(icebergSchema)\n      .identity(\"col_date\")\n      .identity(\"col_ts\")\n      .identity(\"col_tsnz\")\n      .build\n\n    val physicalNameToField = Map(\n      \"pname1\" -> icebergPartSpec.fields().get(0),\n      \"pname2\" -> icebergPartSpec.fields().get(1),\n      \"pname3\" -> icebergPartSpec.fields().get(2)\n    )\n\n    val partitionConverter = IcebergPartitionConverter(icebergSchema, physicalNameToField)\n\n    val partData = new PartitionData(\n      StructType.of(\n        NestedField.required(1000, \"col_date\", DateType.get),\n        NestedField.required(1001, \"col_ts\", TimestampType.withZone),\n        NestedField.required(1002, \"col_tsnz\", TimestampType.withoutZone)\n      )\n    )\n    partData.put(0, 12800)\n    partData.put(1, 1790040414914000L)\n    partData.put(2, 1790040414914000L)\n    assertResult(\"Map(pname1 -> 2005-01-17, \" +\n      \"pname2 -> 2026-09-21 18:26:54.9, pname3 -> 2026-09-21 18:26:54.9)\")(\n      partitionConverter.toDelta(partData).toString)\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/commands/convert/IcebergStatsUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.lang.{Boolean => JBoolean, Double => JDouble, Float => JFloat, Integer => JInt, Long => JLong}\nimport java.math.BigDecimal\nimport java.nio.ByteBuffer\nimport java.util.{HashMap => JHashMap, List => JList, Map => JMap}\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport shadedForDelta.org.apache.iceberg.{DataFile, FileContent, FileFormat, PartitionData, PartitionSpec, Schema, StructLike}\nimport shadedForDelta.org.apache.iceberg.transforms._\nimport shadedForDelta.org.apache.iceberg.types.Conversions\nimport shadedForDelta.org.apache.iceberg.types.Type\nimport shadedForDelta.org.apache.iceberg.types.Type.TypeID\nimport shadedForDelta.org.apache.iceberg.types.Types._\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass IcebergStatsUtilsSuite extends SparkFunSuite with SharedSparkSession {\n\n  private val StatsAllowTypes =\n    IcebergStatsUtils.typesAllowStatsConversion(statsDisallowTypes = Set.empty)\n\n  test(\"stats conversion from basic columns\") {\n    val icebergSchema = new Schema(10, Seq[NestedField](\n      NestedField.required(1, \"col_int\", IntegerType.get),\n      NestedField.required(2, \"col_long\", LongType.get),\n      NestedField.required(3, \"col_st\", StringType.get),\n      NestedField.required(4, \"col_boolean\", BooleanType.get),\n      NestedField.required(5, \"col_float\", FloatType.get),\n      NestedField.required(6, \"col_double\", DoubleType.get),\n      NestedField.required(7, \"col_date\", DateType.get),\n      NestedField.required(8, \"col_binary\", BinaryType.get),\n      NestedField.required(9, \"col_strt\", StructType.of(\n        NestedField.required(10, \"sc_int\", IntegerType.get),\n        NestedField.required(11, \"sc_int2\", IntegerType.get)\n      )),\n      NestedField.required(12, \"col_array\",\n        ListType.ofRequired(13, IntegerType.get)),\n      NestedField.required(14, \"col_map\",\n        MapType.ofRequired(15, 16, IntegerType.get, StringType.get))).asJava\n    )\n\n    val minMap = Map(\n      Integer.valueOf(1) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(-5)),\n      Integer.valueOf(2) -> Conversions.toByteBuffer(LongType.get, JLong.valueOf(-4)),\n      Integer.valueOf(3) -> Conversions.toByteBuffer(StringType.get, \"minval\"),\n      Integer.valueOf(4) -> Conversions.toByteBuffer(BooleanType.get, JBoolean.FALSE),\n      Integer.valueOf(5) -> Conversions.toByteBuffer(FloatType.get, JFloat.valueOf(\"0.001\")),\n      Integer.valueOf(6) -> Conversions.toByteBuffer(DoubleType.get, JDouble.valueOf(\"0.0001\")),\n      Integer.valueOf(7) -> Conversions.toByteBuffer(DateType.get, JInt.valueOf(12800)),\n      Integer.valueOf(8) -> Conversions.toByteBuffer(BinaryType.get,\n        ByteBuffer.wrap(Array(1, 2, 3, 4))),\n      Integer.valueOf(10) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(-1)),\n      Integer.valueOf(11) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(-1))\n    )\n    val maxMap = Map(\n      Integer.valueOf(1) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(5)),\n      Integer.valueOf(2) -> Conversions.toByteBuffer(LongType.get, JLong.valueOf(4)),\n      Integer.valueOf(3) -> Conversions.toByteBuffer(StringType.get, \"maxval\"),\n      Integer.valueOf(4) -> Conversions.toByteBuffer(BooleanType.get, JBoolean.TRUE),\n      Integer.valueOf(5) -> Conversions.toByteBuffer(FloatType.get, JFloat.valueOf(\"10.001\")),\n      Integer.valueOf(6) -> Conversions.toByteBuffer(DoubleType.get, JDouble.valueOf(\"10.0001\")),\n      Integer.valueOf(7) -> Conversions.toByteBuffer(DateType.get, JInt.valueOf(13800)),\n      Integer.valueOf(8) -> Conversions.toByteBuffer(BinaryType.get,\n        ByteBuffer.wrap(Array(2, 2, 3, 4))),\n      Integer.valueOf(10) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(128)),\n      Integer.valueOf(11) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(512))\n    )\n    val nullCountMap = Map(\n      Integer.valueOf(1) -> JLong.valueOf(0),\n      Integer.valueOf(2) -> JLong.valueOf(1),\n      Integer.valueOf(3) -> JLong.valueOf(2),\n      Integer.valueOf(5) -> JLong.valueOf(3),\n      Integer.valueOf(6) -> JLong.valueOf(4),\n      Integer.valueOf(7) -> JLong.valueOf(5),\n      Integer.valueOf(8) -> JLong.valueOf(6),\n      Integer.valueOf(10) -> JLong.valueOf(7),\n      Integer.valueOf(11) -> JLong.valueOf(8),\n      Integer.valueOf(12) -> JLong.valueOf(9),\n      Integer.valueOf(14) -> JLong.valueOf(10)\n    )\n\n    val deltaStats = IcebergStatsUtils.icebergStatsToDelta(\n      icebergSchema,\n      1251,\n      Some(minMap),\n      Some(maxMap),\n      Some(nullCountMap),\n      statsAllowTypes = StatsAllowTypes\n    )\n\n    val actualStatsObj = JsonUtils.fromJson[StatsObject](deltaStats)\n    val expectedStatsObj = JsonUtils.fromJson[StatsObject](\n      \"\"\"{\"numRecords\":1251,\n        |\"maxValues\":{\"col_date\":\"2005-01-17\",\"col_int\":-5,\"col_double\":1.0E-4,\n        |\"col_float\":0.001,\"col_long\":-4,\"col_strt\":{\"sc_int\":-1,\"sc_int2\":-1},\n        |\"col_boolean\":false,\"col_st\":\"minval\",\"col_binary\":\"AQIDBA==\"},\n        |\"minValues\":{\"col_date\":\"2007-10-14\",\"col_int\":5,\"col_double\":10.0001,\n        |\"col_float\":10.001,\"col_long\":4,\"col_strt\":{\"sc_int\":128,\"sc_int2\":512},\n        |\"col_boolean\":true,\"col_st\":\"maxval\",\"col_binary\":\"AgIDBA==\"},\n        |\"nullCount\":{\"col_int\":0,\"col_double\":4,\"col_date\":5,\"col_float\":3,\"col_long\":1,\n        |\"col_strt\":{\"sc_int\":7,\"sc_int2\":8},\"col_st\":2,\"col_binary\":6,\"col_array\":9,\"col_map\":10}}\n        |\"\"\".stripMargin.replaceAll(\"\\n\", \"\"))\n    assertResult(expectedStatsObj)(actualStatsObj)\n  }\n\n  test(\"stats conversion for decimal and timestamp\") {\n    val icebergSchema = new Schema(10, Seq[NestedField](\n      NestedField.required(1, \"col_ts\", TimestampType.withZone),\n      NestedField.required(2, \"col_tsnz\", TimestampType.withoutZone),\n      NestedField.required(3, \"col_decimal\", DecimalType.of(10, 5))\n    ).asJava)\n    val deltaStats = IcebergStatsUtils.icebergStatsToDelta(\n      icebergSchema,\n      1251,\n      minMap = Some(Map(\n        Integer.valueOf(1) ->\n          Conversions.toByteBuffer(TimestampType.withZone, JLong.valueOf(1734391979000000L)),\n        Integer.valueOf(2) ->\n          Conversions.toByteBuffer(TimestampType.withoutZone, JLong.valueOf(1734391979000000L)),\n        Integer.valueOf(3) ->\n          Conversions.toByteBuffer(DecimalType.of(10, 5), new BigDecimal(\"3.44141\"))\n      )),\n      maxMap = Some(Map(\n        Integer.valueOf(1) ->\n          Conversions.toByteBuffer(TimestampType.withZone, JLong.valueOf(1734394979000000L)),\n        Integer.valueOf(2) ->\n          Conversions.toByteBuffer(TimestampType.withoutZone, JLong.valueOf(1734394979000000L)),\n        Integer.valueOf(3) ->\n          Conversions.toByteBuffer(DecimalType.of(10, 5), new BigDecimal(\"9.99999\"))\n      )),\n      nullCountMap = Some(Map(\n        Integer.valueOf(1) -> JLong.valueOf(20),\n        Integer.valueOf(2) -> JLong.valueOf(10),\n        Integer.valueOf(3) -> JLong.valueOf(31)\n      )),\n      statsAllowTypes = StatsAllowTypes\n    )\n    assertResult(\n      JsonUtils.fromJson[StatsObject](\n        \"\"\"{\"numRecords\":1251,\n          |\"maxValues\":{\n          | \"col_ts\":\"2024-12-17T00:22:59+00:00\",\n          | \"col_tsnz\":\"2024-12-17T00:22:59\",\n          | \"col_decimal\":9.99999\n          | },\n          |\"minValues\":{\n          | \"col_ts\":\"2024-12-16T23:32:59+00:00\",\n          | \"col_tsnz\":\"2024-12-16T23:32:59\",\n          | \"col_decimal\":3.44141\n          | },\n          |\"nullCount\":{\"col_ts\":20,\"col_tsnz\":10,\"col_decimal\":31}}\"\"\".stripMargin))(\n      JsonUtils.fromJson[StatsObject](deltaStats))\n  }\n\n  test(\"stats conversion when value is missing or is null\") {\n    val icebergSchema = new Schema(10, Seq[NestedField](\n      NestedField.required(1, \"col_int\", IntegerType.get),\n      NestedField.required(2, \"col_long\", LongType.get),\n      NestedField.required(3, \"col_st\", StringType.get)\n    ).asJava)\n    val deltaStats = IcebergStatsUtils.icebergStatsToDelta(\n      icebergSchema,\n      1251,\n      minMap = Some(Map(\n        Integer.valueOf(1) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(-5)),\n        Integer.valueOf(2) -> Conversions.toByteBuffer(LongType.get, null),\n        Integer.valueOf(3) -> null\n      )),\n      maxMap = Some(Map(\n        Integer.valueOf(1) -> Conversions.toByteBuffer(IntegerType.get, JInt.valueOf(5)),\n        // stats for value 2 is missing\n        Integer.valueOf(3) -> Conversions.toByteBuffer(StringType.get, \"maxval\"),\n        Integer.valueOf(5) -> Conversions.toByteBuffer(StringType.get, \"maxval\")\n      )),\n      nullCountMap = Some(Map(\n        Integer.valueOf(1) -> JLong.valueOf(0),\n        Integer.valueOf(2) -> null,\n        Integer.valueOf(3) -> JLong.valueOf(2),\n        Integer.valueOf(5) -> JLong.valueOf(3)\n      )),\n      statsAllowTypes = StatsAllowTypes\n    )\n    assertResult(\n      JsonUtils.fromJson[StatsObject](\n        \"\"\"{\"numRecords\":1251,\n          |\"maxValues\":{\"col_int\":5,\"col_st\":\"maxval\"},\n          |\"minValues\":{\"col_int\":-5},\n          |\"nullCount\":{\"col_int\":0,\"col_st\":2}}\n          |\"\"\".stripMargin))(\n      JsonUtils.fromJson[StatsObject](deltaStats))\n  }\n\n  private def testStatsConversion(\n      expectedStatsJson: String, dataFile: DataFile, icebergSchema: Schema): Unit = {\n    val expectedStats = JsonUtils.fromJson[StatsObject](expectedStatsJson)\n    val actualStats =\n      IcebergStatsUtils.icebergStatsToDelta(\n          icebergSchema, dataFile, StatsAllowTypes, shouldSkipForFile = _ => false\n        )\n        .map(JsonUtils.fromJson[StatsObject](_))\n        .get\n    assertResult(expectedStats)(actualStats)\n  }\n\n  test(\"stats conversion while DataFile misses the stats fields\") {\n    val icebergSchema = new Schema(10, Seq[NestedField](\n      NestedField.required(1, \"col_int\", IntegerType.get),\n      NestedField.required(2, \"col_long\", LongType.get),\n      NestedField.required(3, \"col_st\", StringType.get)\n    ).asJava)\n    val expectedStatsJson =\n      \"\"\"{\"numRecords\":0,\"maxValues\":{\"col_int\":100992003},\n        |\"minValues\":{\"col_int\":100992003},\"nullCount\":{\"col_int\":2}}\"\"\"\n        .stripMargin\n    testStatsConversion(expectedStatsJson, DummyDataFile(), icebergSchema)\n\n    val expectedStatsWithoutUpperBound =\n      \"\"\"{\"numRecords\":0,\"minValues\":{\"col_int\":100992003},\n        |\"nullCount\":{\"col_int\":2}}\"\"\"\n        .stripMargin\n    testStatsConversion(\n      expectedStatsWithoutUpperBound, DummyDataFile(upperBounds = null), icebergSchema\n    )\n    testStatsConversion(\n      expectedStatsWithoutUpperBound,\n      DummyDataFile(upperBounds = new JHashMap[Integer, ByteBuffer]()),\n      icebergSchema\n    )\n\n    val expectedStatsWithoutLowerBound =\n      \"\"\"{\"numRecords\":0,\"maxValues\":{\"col_int\":100992003},\n        |\"nullCount\":{\"col_int\":2}}\"\"\"\n        .stripMargin\n    testStatsConversion(\n      expectedStatsWithoutLowerBound, DummyDataFile(lowerBounds = null), icebergSchema\n    )\n    testStatsConversion(\n      expectedStatsWithoutLowerBound,\n      DummyDataFile(lowerBounds = new JHashMap[Integer, ByteBuffer]()),\n      icebergSchema\n    )\n\n    val expectedStatsWithoutNullCounts =\n      \"\"\"{\"numRecords\":0,\"maxValues\":{\"col_int\":100992003},\n        |\"minValues\":{\"col_int\":100992003}}\"\"\"\n        .stripMargin\n    testStatsConversion(\n      expectedStatsWithoutNullCounts, DummyDataFile(nullValueCounts = null), icebergSchema\n    )\n    testStatsConversion(\n      expectedStatsWithoutNullCounts,\n      DummyDataFile(nullValueCounts = new JHashMap[Integer, JLong]()),\n      icebergSchema\n    )\n  }\n}\n\nprivate case class StatsObject(\n    numRecords: Long,\n    maxValues: Map[String, Any],\n    minValues: Map[String, Any],\n    nullCount: Map[String, Long])\n\nprivate case class DummyDataFile(\n    upperBounds: JMap[JInt, ByteBuffer] =\n    Map(JInt.valueOf(1) -> ByteBuffer.wrap(Array(3, 4, 5, 6))).asJava,\n    lowerBounds: JMap[JInt, ByteBuffer] =\n    Map(JInt.valueOf(1) -> ByteBuffer.wrap(Array(3, 4, 5, 6))).asJava,\n    nullValueCounts: JMap[JInt, JLong] =\n    Map(JInt.valueOf(1) -> JLong.valueOf(2)).asJava) extends DataFile {\n  override def pos: JLong = 0L\n  override def specId: Int = 0\n  override def path: String = \"dummy\"\n  override def recordCount: Long = 0\n  override def fileSizeInBytes: Long = 0\n  override def content: FileContent = FileContent.DATA\n  override def format: FileFormat = FileFormat.PARQUET\n  override def partition: StructLike = null\n  override def columnSizes: JMap[JInt, JLong] = null\n  override def valueCounts: JMap[JInt, JLong] = null\n  override def nanValueCounts: JMap[JInt, JLong] = null\n  override def keyMetadata: ByteBuffer = null\n  override def splitOffsets: JList[JLong] = null\n  override def copy: DataFile = this.copy\n  override def copyWithoutStats: DataFile = this.copy\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/IcebergRESTCatalogPlanningClientSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport scala.jdk.CollectionConverters._\n\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.sources._\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, LongType, StringType, StructField, StructType}\nimport shadedForDelta.org.apache.iceberg.{PartitionSpec, Schema, Table}\nimport shadedForDelta.org.apache.iceberg.catalog._\nimport shadedForDelta.org.apache.iceberg.expressions.{Binder, Expressions}\nimport shadedForDelta.org.apache.iceberg.rest.IcebergRESTServer\nimport shadedForDelta.org.apache.iceberg.types.Types\n\nclass IcebergRESTCatalogPlanningClientSuite extends QueryTest with SharedSparkSession {\n\n  import testImplicits._\n\n  private val defaultNamespace = Namespace.of(\"testDatabase\")\n  private val defaultSchema = TestSchemas.testSchema\n  private val defaultSpec = PartitionSpec.unpartitioned()\n\n  private lazy val server = IcebergRESTServerTestUtils.startServer()\n  private lazy val catalog = server.getCatalog()\n  private lazy val serverUri = s\"http://localhost:${server.getPort}\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // Configure Spark to use the Iceberg REST catalog\n    spark.conf.set(s\"spark.sql.catalog.rest_catalog\", \"org.apache.iceberg.spark.SparkCatalog\")\n    spark.conf.set(s\"spark.sql.catalog.rest_catalog.type\", \"rest\")\n    spark.conf.set(s\"spark.sql.catalog.rest_catalog.uri\", serverUri)\n\n    if (catalog.isInstanceOf[SupportsNamespaces]) {\n      catalog.asInstanceOf[SupportsNamespaces].createNamespace(defaultNamespace)\n    } else {\n      throw new IllegalStateException(\"Catalog does not support namespaces\")\n    }\n  }\n\n  override def afterAll(): Unit = {\n    try {\n      if (server != null) {\n        server.clearCaptured()\n        server.stop()\n      }\n    } finally {\n      super.afterAll()\n    }\n  }\n\n  test(\"IcebergRESTCatalogPlanningClientFactory is auto-registered by default\") {\n    // Verify that calling getFactory() returns the Iceberg factory via auto-registration\n    val factory = ServerSidePlanningClientFactory.getFactory()\n    assert(factory != null, \"Factory should not be null after auto-registration\")\n    assert(factory.getClass.getName.contains(\"IcebergRESTCatalogPlanningClientFactory\"),\n      s\"Expected IcebergRESTCatalogPlanningClientFactory, got: ${factory.getClass.getName}\")\n  }\n\n  // Tests that the REST /plan endpoint returns 0 files for an empty table.\n  test(\"basic plan table scan via IcebergRESTCatalogPlanningClient\") {\n    withTempTable(\"testTable\") { table =>\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        val scanPlan = client.planScan(defaultNamespace.toString, \"testTable\")\n        assert(scanPlan != null, \"Scan plan should not be null\")\n        assert(scanPlan.files != null, \"Scan plan files should not be null\")\n        assert(scanPlan.files.isEmpty,\n          s\"Empty table should have 0 files, got ${scanPlan.files.length}\")\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  // Tests that the REST /plan endpoint returns the correct number of files for a non-empty table.\n  // Creates a table, writes actual parquet files with data, then verifies the response includes\n  // them.\n  test(\"plan scan on non-empty table with data files\") {\n    withTempTable(\"tableWithData\") { table =>\n      val tableName = s\"rest_catalog.${defaultNamespace}.tableWithData\"\n      populateTestData(tableName)\n\n      // Get the actual data files from the table metadata to verify against scan plan\n      val expectedFiles = spark.sql(\n        s\"SELECT file_path, file_size_in_bytes FROM ${tableName}.files\")\n        .collect()\n        .map(row => (new Path(row.getString(0)).getName, row.getLong(1)))\n        .toMap\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        val scanPlan = client.planScan(defaultNamespace.toString, \"tableWithData\")\n        assert(scanPlan != null, \"Scan plan should not be null\")\n        assert(scanPlan.files != null, \"Scan plan files should not be null\")\n        assert(scanPlan.files.length == 2, s\"Expected 2 files but got ${scanPlan.files.length}\")\n\n        // Get scanned files as map of filename -> size\n        val scannedFiles = scanPlan.files.map { file =>\n          (new Path(file.filePath).getName, file.fileSizeInBytes)\n        }.toMap\n\n        // Verify scan plan files match expected files\n        assert(scannedFiles == expectedFiles,\n          s\"Scan plan files don't match expected files.\\n\" +\n            s\"Expected: $expectedFiles\\n\" +\n            s\"Got: $scannedFiles\")\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  // TODO: Add test for partitioned table rejection\n  // Once the test server (IcebergRESTCatalogAdapterWithPlanSupport) properly retains and serves\n  // partition data through the commit/serialize/deserialize cycle, add a test that verifies:\n  // 1. Creates a partitioned table with data files containing partition info\n  // 2. Calls client.planScan() and expects UnsupportedOperationException\n  // 3. Verifies exception message contains \"partition data\"\n  // This will test the client's partition validation logic at\n  // IcebergRESTCatalogPlanningClient:160-164\n\n  test(\"IcebergRESTCatalogPlanningClient uses prefix from /v1/config endpoint\") {\n    server.clearCaptured()  // Clear any previous state\n\n    server.setCatalogPrefix(\"catalogs/test-catalog-prefix\")\n\n    withTempTable(\"testTable\") { table =>\n      // Client expects baseUri to include the /v1 path (per Iceberg REST spec)\n      val client = new IcebergRESTCatalogPlanningClient(s\"$serverUri/v1\", \"test_catalog\", \"\")\n      try {\n        // Make a call that will trigger the lazy initialization of icebergRestCatalogUriRoot\n        // which internally calls fetchCatalogPrefix()\n        val scanPlan = client.planScan(defaultNamespace.toString, \"testTable\")\n        assert(scanPlan != null, \"Scan plan should not be null\")\n\n        // Verify the server received a /plan request with the correct prefix\n        // This confirms that the config endpoint returned the correct prefix and that the client\n        // correctly constructed the full plan request path.\n        val capturedPath = server.getCapturedPlanRequestPath()\n        assert(capturedPath != null, \"Server should have captured the request path\")\n        assert(capturedPath.startsWith(\"v1/catalogs/test-catalog-prefix/\"),\n          s\"Expected path to start with 'v1/catalogs/test-catalog-prefix/' but got: $capturedPath\")\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"IcebergRESTCatalogPlanningClient uses baseUri directly when /v1/config returns no prefix\") {\n    server.clearCaptured()  // Clear any previous state\n\n    // Configure server to return no prefix\n    server.setCatalogPrefix(null)\n\n    withTempTable(\"testTable\") { table =>\n      // Client expects baseUri to include the /v1 path (per Iceberg REST spec)\n      val client = new IcebergRESTCatalogPlanningClient(s\"$serverUri/v1\", \"test_catalog\", \"\")\n      try {\n        // Make a call that will trigger the lazy initialization\n        val scanPlan = client.planScan(defaultNamespace.toString, \"testTable\")\n        assert(scanPlan != null, \"Scan plan should not be null\")\n\n        // Verify the server received a /plan request using baseUri directly (no prefix)\n        val capturedPath = server.getCapturedPlanRequestPath()\n        assert(capturedPath != null, \"Server should have captured the request path\")\n        // When no prefix is returned, use baseUri directly without adding prefix\n        assert(\n          !capturedPath.contains(\"catalogs/\"),\n          s\"Expected path to NOT contain 'catalogs/' when no prefix, but got: $capturedPath\")\n        assert(\n          capturedPath.startsWith(\"v1/namespaces/\"),\n          s\"Expected path to start with 'v1/namespaces/' (using baseUri directly), but got: \" +\n            s\"$capturedPath\")\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"filter sent to IRC server over HTTP\") {\n    withTempTable(\"filterTest\") { table =>\n      populateTestData(s\"rest_catalog.${defaultNamespace}.filterTest\")\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        val testCases = Seq(\n          (EqualTo(\"longCol\", 2L), \"EqualTo numeric (long)\"),\n          (EqualTo(\"intCol\", 30), \"EqualTo numeric (int)\"),\n          (EqualTo(\"stringCol\", \"bob\"), \"EqualTo string\"),\n          (EqualTo(\"boolCol\", true), \"EqualTo boolean\"),\n          (Not(EqualTo(\"longCol\", 2L)), \"NotEqualTo numeric (long)\"),\n          (Not(EqualTo(\"stringCol\", \"bob\")), \"NotEqualTo string\"),\n          (LessThan(\"longCol\", 10L), \"LessThan (long)\"),\n          (LessThan(\"floatCol\", 4.5f), \"LessThan (float)\"),\n          (GreaterThan(\"longCol\", 5L), \"GreaterThan (long)\"),\n          (GreaterThan(\"doubleCol\", 100.0), \"GreaterThan (double)\"),\n          (LessThanOrEqual(\"intCol\", 30), \"LessThanOrEqual (int)\"),\n          (GreaterThanOrEqual(\"doubleCol\", 100.0), \"GreaterThanOrEqual (double)\"),\n          (In(\"longCol\", Array(1L, 2L, 3L)), \"In numeric (long)\"),\n          (In(\"stringCol\", Array(\"alice\", \"bob\", \"charlie\")), \"In string\"),\n          (IsNull(\"stringCol\"), \"IsNull\"),\n          (IsNotNull(\"stringCol\"), \"IsNotNull\"),\n          (StringStartsWith(\"stringCol\", \"ali\"), \"StringStartsWith\"),\n          (AlwaysTrue(), \"AlwaysTrue\"),\n          (AlwaysFalse(), \"AlwaysFalse\"),\n          (And(EqualTo(\"longCol\", 2L), EqualTo(\"stringCol\", \"bob\")), \"And\"),\n          (Or(EqualTo(\"longCol\", 1L), EqualTo(\"longCol\", 3L)), \"Or\"),\n          (EqualTo(\"address.intCol\", 200), \"EqualTo on nested numeric field\"),\n          (EqualTo(\"metadata.stringCol\", \"meta_bob\"), \"EqualTo on nested string field\"),\n          (GreaterThan(\"address.intCol\", 500), \"GreaterThan on nested numeric field\"))\n\n        testCases.foreach { case (filter, description) =>\n          // Clear previous captured filter\n          server.clearCaptured()\n\n          // Convert Spark filter to expected Iceberg expression\n          val expectedExpr = SparkToIcebergExpressionConverter.convert(filter)\n          assert(\n            expectedExpr.isDefined,\n            s\"[$description] Filter conversion should succeed for: $filter\")\n\n          // Call client with filter\n          client.planScan(\n            defaultNamespace.toString,\n            \"filterTest\",\n            sparkFilterOption = Some(filter))\n\n          // Verify server captured the filter\n          val capturedFilter = server.getCapturedFilter\n          assert(capturedFilter != null, s\"[$description] Server should have captured filter\")\n\n          // isEquivalentTo() only works on bound expressions, so bind both to schema for comparison\n          // Binding resolves field references from names to schema-specific field IDs and types\n          val boundExpected = Binder.bind(defaultSchema.asStruct(), expectedExpr.get, true)\n          val boundCaptured = Binder.bind(defaultSchema.asStruct(), capturedFilter, true)\n\n          assert(\n            boundCaptured.isEquivalentTo(boundExpected),\n            s\"[$description] Expected expression: $boundExpected, got: $boundCaptured\")\n        }\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  // Test case classes for structured test data\n  private case class ProjectionTestCase(\n    description: String,\n    projection: Seq[String],\n    expected: Set[String])\n\n  private case class PushdownTestCase(\n    description: String,\n    filter: Filter,\n    projection: Seq[String],\n    limit: Option[Int])\n\n  test(\"projection sent to IRC server over HTTP\") {\n    withTempTable(\"projectionTest\") { table =>\n      // Populate test data using the shared helper method\n      val tableName = s\"rest_catalog.${defaultNamespace}.projectionTest\"\n      populateTestData(tableName)\n\n      // Test cases covering different projection scenarios\n      // Note: At this HTTP layer, we're only testing that column name strings are correctly\n      // sent and received. Type serialization and data reading are tested end-to-end.\n      val testCases = Seq(\n        // Basic projections\n        ProjectionTestCase(\n          \"single column\",\n          Seq(\"intCol\"),\n          Set(\"intCol\")),\n        ProjectionTestCase(\n          \"multiple columns\",\n          Seq(\"intCol\", \"stringCol\"),\n          Set(\"intCol\", \"stringCol\")),\n\n        // Nested field projections - test dot-notation string handling\n        ProjectionTestCase(\n          \"individual nested field\",\n          Seq(\"address.intCol\"),\n          Set(\"address.intCol\")),\n        ProjectionTestCase(\n          \"dotted field name inside struct with escaping\",\n          Seq(\"parent.`child.name`\"),\n          Set(\"parent.`child.name`\")),\n        ProjectionTestCase(\n          \"dotted column name with escaping\",\n          Seq(\"`address.city`\"),\n          Set(\"`address.city`\"))\n      )\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        testCases.foreach { testCase =>\n          // Clear previous captured projection\n          server.clearCaptured()\n\n          client.planScan(\n            defaultNamespace.toString,\n            \"projectionTest\",\n            sparkProjectionOption = Some(testCase.projection))\n\n          // Verify server captured the projection\n          val capturedProjection = server.getCapturedProjection\n          assert(capturedProjection != null,\n            s\"[${testCase.description}] Server should have captured projection\")\n\n          // Verify field names match expected\n          val fieldNames = capturedProjection.asScala.toSet\n          assert(fieldNames == testCase.expected,\n            s\"[${testCase.description}] Expected ${testCase.expected}, got: $fieldNames\")\n        }\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"limit sent to IRC server over HTTP\") {\n    withTempTable(\"limitTest\") { table =>\n      // Populate test data using the shared helper method\n      val tableName = s\"rest_catalog.${defaultNamespace}.limitTest\"\n      populateTestData(tableName)\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, null, \"\")\n      try {\n        // Test different limit values\n        val testCases = Seq(\n          (Some(10), Some(10L), \"limit = 10\"),\n          (Some(100), Some(100L), \"limit = 100\"),\n          (Some(1), Some(1L), \"limit = 1\"),\n          (None, None, \"no limit\"))\n\n        testCases.foreach { case (limitOption, expectedCaptured, description) =>\n          // Clear previous captured state\n          server.clearCaptured()\n\n          client.planScan(\n            defaultNamespace.toString,\n            \"limitTest\",\n            sparkLimitOption = limitOption)\n\n          // Verify server captured the limit\n          val capturedLimit = Option(server.getCapturedLimit)\n          assert(capturedLimit == expectedCaptured,\n            s\"[$description] Expected $expectedCaptured, got: $capturedLimit\")\n        }\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"filter, projection, and limit sent together to IRC server over HTTP\") {\n    withTempTable(\"filterProjectionLimitTest\") { table =>\n      // Populate test data using the shared helper method\n      val tableName = s\"rest_catalog.${defaultNamespace}.filterProjectionLimitTest\"\n      populateTestData(tableName)\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        // Note: Filter types are already tested in \"filter sent to IRC server\" test.\n        // Here we verify filter, projection, AND limit are sent together correctly.\n        val testCases = Seq(\n          PushdownTestCase(\n            \"filter + projection + limit\",\n            EqualTo(\"longCol\", 2L),\n            Seq(\"intCol\", \"stringCol\"),\n            Some(10)),\n          PushdownTestCase(\n            \"nested field in both filter and projection + limit\",\n            EqualTo(\"address.intCol\", 200),\n            Seq(\"intCol\", \"address.intCol\"),\n            Some(5))\n        )\n\n        testCases.foreach { testCase =>\n          // Clear previous captured state\n          server.clearCaptured()\n\n          // Convert Spark filter to expected Iceberg expression\n          val expectedExpr = SparkToIcebergExpressionConverter.convert(testCase.filter)\n          assert(\n            expectedExpr.isDefined,\n            s\"[${testCase.description}] Filter conversion should succeed for: ${testCase.filter}\")\n\n          // Call client with filter, projection, and limit\n          client.planScan(\n            defaultNamespace.toString,\n            \"filterProjectionLimitTest\",\n            sparkFilterOption = Some(testCase.filter),\n            sparkProjectionOption = Some(testCase.projection),\n            sparkLimitOption = testCase.limit)\n\n          // Verify server captured filter, projection, and limit\n          val capturedFilter = server.getCapturedFilter\n          val capturedProjection = server.getCapturedProjection\n          val capturedLimit = server.getCapturedLimit\n\n          assert(capturedFilter != null,\n            s\"[${testCase.description}] Server should have captured filter\")\n          assert(capturedProjection != null,\n            s\"[${testCase.description}] Server should have captured projection\")\n          assert(capturedLimit != null,\n            s\"[${testCase.description}] Server should have captured limit\")\n\n          // Verify filter is correct\n          val boundExpected = Binder.bind(defaultSchema.asStruct(), expectedExpr.get, true)\n          val boundCaptured = Binder.bind(defaultSchema.asStruct(), capturedFilter, true)\n          assert(\n            boundCaptured.isEquivalentTo(boundExpected),\n            s\"[${testCase.description}] Filter mismatch. Expected: $boundExpected, \" +\n            s\"got: $boundCaptured\")\n\n          // Verify projection is correct\n          val projectionFields = capturedProjection.asScala.toSet\n          val expectedFields = testCase.projection.toSet\n          assert(projectionFields == expectedFields,\n            s\"[${testCase.description}] Projection mismatch. Expected: $expectedFields, \" +\n            s\"got: $projectionFields\")\n\n          // Verify limit is correct\n          val expectedLimit = testCase.limit.map(_.toLong)\n          assert(Option(capturedLimit) == expectedLimit,\n            s\"[${testCase.description}] Limit mismatch. Expected: $expectedLimit, \" +\n            s\"got: ${Option(capturedLimit)}\")\n        }\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"caseSensitive=false sent to IRC server\") {\n    withTempTable(\"caseSensitiveTest\") { table =>\n      populateTestData(s\"rest_catalog.${defaultNamespace}.caseSensitiveTest\")\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, null, \"\")\n      try {\n        server.clearCaptured()\n\n        // Call planScan - the client sets caseSensitive=false in the request\n        val scanPlan = client.planScan(defaultNamespace.toString, \"caseSensitiveTest\")\n\n        // Verify the scan succeeds and returns files\n        assert(scanPlan.files.nonEmpty,\n          \"Expected planScan to return files for the test table\")\n\n        // Verify server captured caseSensitive=false\n        val capturedCaseSensitive = server.getCapturedCaseSensitive()\n        assert(capturedCaseSensitive == false,\n          s\"Expected server to capture caseSensitive=false, got $capturedCaseSensitive\")\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"rejects FileScanTask with non-trivial residual\") {\n    withTempTable(\"residualTest\") { table =>\n      populateTestData(s\"rest_catalog.${defaultNamespace}.residualTest\")\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        // Verify that a trivial (alwaysTrue) residual is accepted\n        server.setTestResidual(Expressions.alwaysTrue())\n        val scanPlan = client.planScan(defaultNamespace.toString, \"residualTest\")\n        assert(scanPlan.files.nonEmpty,\n          \"Scan with alwaysTrue residual should succeed and return files\")\n\n        // Configure server to inject a non-trivial residual expression into the response.\n        // This simulates a server that expects the client to apply a residual filter,\n        // which is currently unsupported.\n        server.setTestResidual(Expressions.greaterThan(\"longCol\", 42L))\n\n        val exception = intercept[UnsupportedOperationException] {\n          client.planScan(defaultNamespace.toString, \"residualTest\")\n        }\n\n        assert(exception.getMessage.contains(\"residual\"),\n          s\"Error message should mention 'residual'. Got: ${exception.getMessage}\")\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  /**\n   * Convenience wrapper for withTempTable that uses the test suite's default values.\n   */\n  private def withTempTable[T](tableName: String)(func: Table => T): T = {\n    IcebergRESTServerTestUtils.withTempTable(\n      catalog, defaultNamespace, tableName,\n      defaultSchema, defaultSpec, Some(server)\n    )(func)\n  }\n\n  /**\n   * Convenience wrapper for populateTestData that uses the test suite's SparkSession.\n   */\n  private def populateTestData(tableName: String): Unit = {\n    IcebergRESTServerTestUtils.populateTestData(spark, tableName)\n  }\n\n  test(\"retry on transient 503 server error\") {\n    withTempTable(\"retryTest503\") { table =>\n      populateTestData(s\"rest_catalog.${defaultNamespace}.retryTest503\")\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        server.clearCaptured()\n        // Configure server to fail the first plan request with 503\n        server.setFailNextPlanRequests(1, 503)\n\n        // Client should retry and succeed on the second attempt\n        val scanPlan = client.planScan(defaultNamespace.toString, \"retryTest503\")\n        assert(scanPlan != null, \"Scan plan should not be null after retry\")\n        assert(scanPlan.files.nonEmpty, \"Scan plan should have files after successful retry\")\n\n        // Verify 2 requests were made: 1 failed (503) + 1 success\n        assert(server.getPlanRequestCount() == 2,\n          s\"Expected 2 plan requests (1 retry), got ${server.getPlanRequestCount()}\")\n      } finally {\n        server.clearCaptured()\n        client.close()\n      }\n    }\n  }\n\n  test(\"retries exhausted on persistent 503 server error\") {\n    // No populateTestData needed: failure injection intercepts at the servlet level before\n    // table data is accessed, so we only need the table to exist for a valid URI.\n    withTempTable(\"retryTestExhausted\") { table =>\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        server.clearCaptured()\n        // Configure server to fail more requests than the client will retry (max 3 retries = 4\n        // total attempts). Setting 10 failures ensures all retries see 503.\n        server.setFailNextPlanRequests(10, 503)\n\n        val exception = intercept[java.io.IOException] {\n          client.planScan(defaultNamespace.toString, \"retryTestExhausted\")\n        }\n        assert(exception.getMessage.contains(\"503\"),\n          s\"Error should mention 503 status code. Got: ${exception.getMessage}\")\n\n        // Verify 4 requests were made: 1 original + 3 retries (max retries = 3)\n        assert(server.getPlanRequestCount() == 4,\n          s\"Expected 4 plan requests (1 + 3 retries), got ${server.getPlanRequestCount()}\")\n      } finally {\n        server.clearCaptured()\n        client.close()\n      }\n    }\n  }\n\n  test(\"no retry on 404 client error\") {\n    // No populateTestData needed: failure injection intercepts at the servlet level before\n    // table data is accessed, so we only need the table to exist for a valid URI.\n    withTempTable(\"retryTest404\") { table =>\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        server.clearCaptured()\n        // Configure server to fail all plan requests with 404\n        // Using a high count ensures the test fails if the client retries\n        server.setFailNextPlanRequests(10, 404)\n\n        // Client should NOT retry 404 and should throw immediately\n        val exception = intercept[java.io.IOException] {\n          client.planScan(defaultNamespace.toString, \"retryTest404\")\n        }\n        assert(exception.getMessage.contains(\"404\"),\n          s\"Error should mention 404 status code. Got: ${exception.getMessage}\")\n\n        // Verify only 1 request was made (no retry for 404)\n        assert(server.getPlanRequestCount() == 1,\n          s\"Expected 1 plan request (no retry for 404), got ${server.getPlanRequestCount()}\")\n      } finally {\n        server.clearCaptured()\n        client.close()\n      }\n    }\n  }\n\n  test(\"fetchCatalogPrefix falls back to baseUri on connection failure\") {\n    // Use a port that's expected to have no listener. fetchCatalogPrefix() makes an HTTP GET\n    // to /config which will fail with a connection error. It should catch the exception, log a\n    // warning, and return None — causing icebergRestCatalogUriRoot to fall back to baseUri.\n    // The subsequent planScan HTTP POST will also fail (same unreachable host).\n    val unreachableUri = \"http://localhost:1\"\n    val client = new IcebergRESTCatalogPlanningClient(unreachableUri, \"test_catalog\", \"\")\n    try {\n      val ex = intercept[Exception] {\n        client.planScan(\"test_db\", \"test_table\")\n      }\n      // Verify the exception is a connection error. This confirms fetchCatalogPrefix()\n      // did not throw a different exception type (e.g., NPE, parse error) and that the\n      // client progressed past the config fetch to attempt the plan HTTP POST.\n      assert(ex.getMessage != null,\n        \"Expected a connection error with a message from the HTTP client\")\n    } finally {\n      client.close()\n    }\n  }\n\n  test(\"User-Agent header format\") {\n    val client = new IcebergRESTCatalogPlanningClient(\"http://localhost:8080\", \"test_catalog\", \"\")\n    try {\n      val userAgent = client.getUserAgent()\n\n      // Verify the format follows RFC 7231: product/version [product/version ...]\n      val parts = userAgent.split(\" \")\n      assert(parts.length == 4,\n        s\"User-Agent should have 4 space-separated components, got ${parts.length}: $userAgent\")\n\n      // First part should be Delta/version\n      assert(parts(0).matches(\"Delta/.*\"),\n        s\"First component should match 'Delta/<version>', got: ${parts(0)}\")\n\n      // Second part should be Spark/version\n      assert(parts(1).matches(\"Spark/.*\"),\n        s\"Second component should match 'Spark/<version>', got: ${parts(1)}\")\n\n      // Third part should be Java/version\n      assert(parts(2).matches(\"Java/.*\"),\n        s\"Third component should match 'Java/<version>', got: ${parts(2)}\")\n\n      // Fourth part should be Scala/version\n      assert(parts(3).matches(\"Scala/.*\"),\n        s\"Fourth component should match 'Scala/<version>', got: ${parts(3)}\")\n\n      // Verify versions are not \"unknown\" in test environment where all dependencies are available\n      assert(!userAgent.contains(\"Spark/unknown\"),\n        s\"Spark version should not be 'unknown' in test environment, got: $userAgent\")\n      assert(!userAgent.contains(\"Delta/unknown\"),\n        s\"Delta version should not be 'unknown' in test environment, got: $userAgent\")\n      assert(!userAgent.contains(\"Java/unknown\"),\n        s\"Java version should not be 'unknown' in test environment, got: $userAgent\")\n      assert(!userAgent.contains(\"Scala/unknown\"),\n        s\"Scala version should not be 'unknown' in test environment, got: $userAgent\")\n    } finally {\n      client.close()\n    }\n  }\n\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/IcebergRESTServerTestUtils.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport scala.jdk.CollectionConverters._\n\nimport org.apache.http.HttpHeaders\nimport org.apache.http.client.methods.HttpGet\nimport org.apache.http.entity.ContentType\nimport org.apache.http.impl.client.HttpClientBuilder\nimport org.apache.http.message.BasicHeader\nimport org.apache.spark.sql.{Row, SparkSession}\nimport shadedForDelta.org.apache.iceberg.{PartitionSpec, Table}\nimport shadedForDelta.org.apache.iceberg.catalog._\nimport shadedForDelta.org.apache.iceberg.rest.IcebergRESTServer\n\n/**\n * Shared test utilities for IcebergRESTServer-based tests.\n *\n * Provides helper methods for:\n * - Starting and verifying IcebergRESTServer instances\n * - Managing table lifecycle with guaranteed cleanup\n * - Populating test data with consistent schemas\n */\nobject IcebergRESTServerTestUtils {\n\n  /**\n   * Starts an IcebergRESTServer on a dynamic port and verifies it's reachable.\n   *\n   * @return Started and verified IcebergRESTServer instance\n   * @throws IllegalStateException if server fails to start or become reachable\n   */\n  def startServer(): IcebergRESTServer = {\n    val config = Map(IcebergRESTServer.REST_PORT -> \"0\").asJava\n    val newServer = new IcebergRESTServer(config)\n    newServer.start(/* join = */ false)\n    if (!isServerReachable(newServer)) {\n      throw new IllegalStateException(\"Failed to start IcebergRESTServer\")\n    }\n    newServer\n  }\n\n  /**\n   * Checks if an IcebergRESTServer is reachable via HTTP.\n   *\n   * Makes a GET request to /v1/config endpoint to verify server is responding.\n   *\n   * @param server The IcebergRESTServer to check\n   * @return true if server returns 200 OK, false otherwise\n   */\n  def isServerReachable(server: IcebergRESTServer): Boolean = {\n    val httpHeaders = Map(\n      HttpHeaders.ACCEPT -> ContentType.APPLICATION_JSON.getMimeType,\n      HttpHeaders.CONTENT_TYPE -> ContentType.APPLICATION_JSON.getMimeType\n    ).map { case (k, v) => new BasicHeader(k, v) }.toSeq.asJava\n\n    val httpClient = HttpClientBuilder.create()\n      .setDefaultHeaders(httpHeaders)\n      .build()\n\n    try {\n      val httpGet = new HttpGet(s\"http://localhost:${server.getPort}/v1/config\")\n      val httpResponse = httpClient.execute(httpGet)\n      try {\n        val statusCode = httpResponse.getStatusLine.getStatusCode\n        statusCode == 200\n      } finally {\n        httpResponse.close()\n      }\n    } finally {\n      httpClient.close()\n    }\n  }\n\n  /**\n   * Executes a function with a temporary table, guaranteeing cleanup.\n   *\n   * @param catalog The Iceberg catalog to use\n   * @param namespace The namespace for the table\n   * @param tableName The table name\n   * @param schema The table schema\n   * @param spec The partition spec\n   * @param server Optional server to clear captures after cleanup\n   * @param func Function to execute with the created table\n   * @return The result of executing func\n   */\n  def withTempTable[T](\n      catalog: Catalog,\n      namespace: Namespace,\n      tableName: String,\n      schema: shadedForDelta.org.apache.iceberg.Schema,\n      spec: PartitionSpec,\n      server: Option[IcebergRESTServer] = None\n  )(func: Table => T): T = {\n    val tableId = TableIdentifier.of(namespace, tableName)\n    val table = catalog.createTable(tableId, schema, spec)\n    try {\n      func(table)\n    } finally {\n      catalog.dropTable(tableId, false)\n      server.foreach(_.clearCaptured())\n    }\n  }\n\n  /**\n   * Populates an Iceberg table with test data.\n   *\n   * Creates 250 rows of test data using TestSchemas.sparkSchema, distributed\n   * across 2 partitions to create 2 data files.\n   *\n   * @param spark The SparkSession to use\n   * @param tableName The fully-qualified table name (e.g., \"catalog.db.table\")\n   */\n  def populateTestData(spark: SparkSession, tableName: String): Unit = {\n    // scalastyle:off sparkimplicits\n    import spark.implicits._\n    // scalastyle:on sparkimplicits\n\n    val data = spark.sparkContext.parallelize(0 until 250, numSlices = 2)\n      .map(i => Row(\n        i, // intCol\n        i.toLong, // longCol\n        i * 10.0, // doubleCol\n        i.toFloat, // floatCol\n        s\"test_$i\", // stringCol\n        i % 2 == 0, // boolCol\n        BigDecimal(i).bigDecimal, // decimalCol\n        java.sql.Date.valueOf(\"2024-01-01\"), // dateCol\n        java.sql.Timestamp.valueOf(\"2024-01-01 00:00:00\"), // timestampCol\n        java.sql.Date.valueOf(\"2024-01-01\"), // localDateCol\n        java.sql.Timestamp.valueOf(\"2024-01-01 00:00:00\"), // localDateTimeCol\n        java.sql.Timestamp.valueOf(\"2024-01-01 00:00:00\"), // instantCol\n        Row(i * 100), // address.intCol (nested struct)\n        Row(s\"meta_$i\"), // metadata.stringCol (nested struct)\n        Row(s\"child_$i\"), // parent.`child.name` (nested struct with dotted field name)\n        s\"city_$i\", // address.city (literal top-level dotted column)\n        s\"abc_$i\" // a.b.c (literal top-level dotted column)\n      ))\n\n    spark.createDataFrame(data, TestSchemas.sparkSchema)\n      .write\n      .format(\"iceberg\")\n      .mode(\"append\")\n      .save(tableName)\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlanningCredentialsSuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport scala.jdk.CollectionConverters._\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\nimport shadedForDelta.org.apache.iceberg.{PartitionSpec, Table}\nimport shadedForDelta.org.apache.iceberg.catalog._\n\n/**\n * Test suite for server-side planning credential handling.\n * Tests credential parsing and Hadoop configuration injection for S3, Azure, and GCS.\n */\nclass ServerSidePlanningCredentialsSuite extends QueryTest with SharedSparkSession {\n\n  import CredentialTestHelpers._\n\n  private val defaultNamespace = Namespace.of(\"testDatabase\")\n  private val defaultSchema = TestSchemas.testSchema\n  private val defaultSpec = PartitionSpec.unpartitioned()\n\n  private lazy val server = IcebergRESTServerTestUtils.startServer()\n  private lazy val catalog = server.getCatalog()\n  private lazy val serverUri = s\"http://localhost:${server.getPort}\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // Configure Spark to use the Iceberg REST catalog\n    spark.conf.set(s\"spark.sql.catalog.rest_catalog\", \"org.apache.iceberg.spark.SparkCatalog\")\n    spark.conf.set(s\"spark.sql.catalog.rest_catalog.type\", \"rest\")\n    spark.conf.set(s\"spark.sql.catalog.rest_catalog.uri\", serverUri)\n\n    if (catalog.isInstanceOf[SupportsNamespaces]) {\n      catalog.asInstanceOf[SupportsNamespaces].createNamespace(defaultNamespace)\n    } else {\n      throw new IllegalStateException(\"Catalog does not support namespaces\")\n    }\n  }\n\n  override def afterAll(): Unit = {\n    try {\n      if (server != null) {\n        server.clearCaptured()\n        server.stop()\n      }\n    } finally {\n      super.afterAll()\n    }\n  }\n\n  test(\"Credentials: server response parsing and Hadoop configuration\") {\n    withTempTable(\"credentialsTest\") { table =>\n      populateTestData(s\"rest_catalog.${defaultNamespace}.credentialsTest\")\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        // Covers the successful credential extraction and Hadoop configuration injection cases.\n        val testCases: Seq[CredentialTestCase] = Seq(\n          // S3\n          S3CredentialTestCase(\n            description = \"S3 with session token\",\n            accessKeyId = \"test-access-key\",\n            secretAccessKey = \"test-secret-key\",\n            sessionToken = \"test-session-token\"\n          ),\n\n          // Azure without expiration\n          AzureCredentialTestCase(\n            description = \"Azure without expiration\",\n            accountName = \"unitycatalogmetastore\",\n            sasToken = \"sv=2023-01-03&ss=b&srt=sco&sp=rwdlac&se=2025-12-31T23:59:59Z&sig=test\",\n            expirationMs = None\n          ),\n\n          // Azure with expiration\n          AzureCredentialTestCase(\n            description = \"Azure with expiration\",\n            accountName = \"unitycatalogmetastore\",\n            sasToken = \"sv=2023-01-03&ss=b&srt=sco&sp=rwdlac&se=2025-12-31T23:59:59Z&sig=test\",\n            expirationMs = Some(1771456336352L)\n          ),\n\n          // GCS without expiration\n          GcsCredentialTestCase(\n            description = \"GCS without expiration\",\n            token = \"ya29.c.c0AY_VpZg_test_token\",\n            expirationMs = None\n          ),\n\n          // GCS with expiration\n          GcsCredentialTestCase(\n            description = \"GCS with expiration\",\n            token = \"ya29.c.c0AY_VpZg_test_token\",\n            expirationMs = Some(1771456336352L)\n          )\n        )\n\n        testCases.foreach { testCase =>\n          // Set server to return credentials.\n          server.setTestCredentials(testCase.serverResponse.asJava)\n\n          val scanPlan = client.planScan(defaultNamespace.toString, \"credentialsTest\")\n\n          assert(scanPlan.credentials.isDefined,\n            s\"[${testCase.description}] Credentials should be present in ScanPlan\")\n\n          val testConf = new Configuration()\n          scanPlan.credentials.foreach(_.configure(testConf))\n\n          // Validate Hadoop config matches expectation.\n          testCase.expectedHadoopConfig.foreach { case (key, expectedValue) =>\n            val actualValue = testConf.get(key)\n            assert(actualValue == expectedValue,\n              s\"[${testCase.description}] Hadoop config mismatch for key '$key'.\\n\" +\n              s\"Expected: $expectedValue\\n\" +\n              s\"Got: $actualValue\")\n          }\n\n          // Clear for next test case\n          server.clearCaptured()\n        }\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  test(\"incomplete/missing credentials throw errors\") {\n    withTempTable(\"incompleteCredsTest\") { table =>\n      populateTestData(s\"rest_catalog.${defaultNamespace}.incompleteCredsTest\")\n\n      val client = new IcebergRESTCatalogPlanningClient(serverUri, \"test_catalog\", \"\")\n      try {\n        // Test cases for incomplete credentials that should throw errors\n        val errorTestCases = Seq(\n          (\"Incomplete S3 (missing secret and token)\",\n            Map(\"s3.access-key-id\" -> \"test-key\"),\n            \"Missing required credential\"),\n          (\"GCS incomplete: only expiration\",\n            Map(\"gcs.oauth2.token-expires-at\" -> \"1771456336352\"),\n            \"Unrecognized credential keys\"),\n          // Expiration-only Azure entry is unrecognized: without the token key\n          // (adls.sas-token.<account>), hasAzureKeys() returns false and we can't\n          // construct valid credentials.\n          (\"Azure incomplete: expiration key only, no token key\",\n            Map(\"adls.sas-token-expires-at-ms.myaccount.dfs.core.windows.net\" -> \"1771456336352\"),\n            \"Unrecognized credential keys\")\n        )\n\n        errorTestCases.foreach { case (description, incompleteConfig, expectedMessageFragment) =>\n          // Configure server with incomplete credentials\n          server.setTestCredentials(incompleteConfig.asJava)\n\n          // Verify that planScan throws IllegalStateException\n          val exception = intercept[IllegalStateException] {\n            client.planScan(defaultNamespace.toString, \"incompleteCredsTest\")\n          }\n\n          // Verify error message contains relevant fragment\n          assert(exception.getMessage.contains(expectedMessageFragment),\n            s\"[$description] Error message should contain '$expectedMessageFragment'. \" +\n            s\"Got: ${exception.getMessage}\")\n\n          // Clear for next test case\n          server.clearCaptured()\n        }\n      } finally {\n        client.close()\n      }\n    }\n  }\n\n  /**\n   * Convenience wrapper for withTempTable that uses the test suite's default values.\n   */\n  private def withTempTable[T](tableName: String)(func: Table => T): T = {\n    IcebergRESTServerTestUtils.withTempTable(\n      catalog, defaultNamespace, tableName,\n      defaultSchema, defaultSpec, Some(server)\n    )(func)\n  }\n\n  /**\n   * Convenience wrapper for populateTestData that uses the test suite's SparkSession.\n   */\n  private def populateTestData(tableName: String): Unit = {\n    IcebergRESTServerTestUtils.populateTestData(spark, tableName)\n  }\n\n  /**\n   * Credential test helper traits and case classes.\n   * Private to this test suite - these are test-only utilities.\n   */\n  private object CredentialTestHelpers {\n\n    /**\n     * Test case for end-to-end credential validation.\n     *\n     * Flow: serverResponse → (client parses) → creds.configure(conf) → expectedHadoopConfig\n     */\n    sealed trait CredentialTestCase {\n      /** Test case name */\n      def description: String\n\n      /** Cloud provider type (S3, Azure, GCS) */\n      def cloudProvider: String\n\n      /** INPUT: Credential config map that server returns */\n      def serverResponse: Map[String, String]\n\n      /** EXPECTED OUTPUT: Hadoop configuration keys and values */\n      def expectedHadoopConfig: Map[String, String]\n    }\n\n    /**\n     * S3 credential test case.\n     */\n    case class S3CredentialTestCase(\n        description: String,\n        accessKeyId: String,\n        secretAccessKey: String,\n        sessionToken: String\n    ) extends CredentialTestCase {\n      override def cloudProvider: String = \"S3\"\n\n      override def serverResponse: Map[String, String] = Map(\n        \"s3.access-key-id\" -> accessKeyId,\n        \"s3.secret-access-key\" -> secretAccessKey,\n        \"s3.session-token\" -> sessionToken\n      )\n\n      override def expectedHadoopConfig: Map[String, String] = Map(\n        \"fs.s3a.path.style.access\" -> \"true\",\n        \"fs.s3.impl.disable.cache\" -> \"true\",\n        \"fs.s3a.impl.disable.cache\" -> \"true\",\n        \"fs.s3a.access.key\" -> accessKeyId,\n        \"fs.s3a.secret.key\" -> secretAccessKey,\n        \"fs.s3a.session.token\" -> sessionToken\n      )\n    }\n\n    /**\n     * Azure credential test case.\n     */\n    case class AzureCredentialTestCase(\n        description: String,\n        accountName: String,\n        sasToken: String,\n        expirationMs: Option[Long] = None\n    ) extends CredentialTestCase {\n      override def cloudProvider: String = \"Azure\"\n\n      override def serverResponse: Map[String, String] = {\n        val base = Map(\n          s\"adls.sas-token.$accountName.dfs.core.windows.net\" -> sasToken\n        )\n        expirationMs match {\n          case Some(ms) =>\n            val expiryKey = s\"adls.sas-token-expires-at-ms.$accountName.dfs.core.windows.net\"\n            base + (expiryKey -> ms.toString)\n          case None => base\n        }\n      }\n\n      override def expectedHadoopConfig: Map[String, String] = {\n        val accountSuffix = s\"$accountName.dfs.core.windows.net\"\n        Map(\n          \"fs.abfs.impl.disable.cache\" -> \"true\",\n          \"fs.abfss.impl.disable.cache\" -> \"true\",\n          s\"fs.azure.account.auth.type.$accountSuffix\" -> \"SAS\",\n          s\"fs.azure.sas.fixed.token.$accountSuffix\" -> sasToken\n        )\n      }\n    }\n\n    /**\n     * GCS credential test case.\n     */\n    case class GcsCredentialTestCase(\n        description: String,\n        token: String,\n        expirationMs: Option[Long] = None\n    ) extends CredentialTestCase {\n      override def cloudProvider: String = \"GCS\"\n\n      override def serverResponse: Map[String, String] = {\n        val base = Map(\"gcs.oauth2.token\" -> token)\n        expirationMs match {\n          case Some(ms) => base + (\"gcs.oauth2.token-expires-at\" -> ms.toString)\n          case None => base\n        }\n      }\n\n      override def expectedHadoopConfig: Map[String, String] = {\n        val base = Map(\n          \"fs.gs.impl.disable.cache\" -> \"true\",\n          \"fs.gs.auth.type\" -> \"ACCESS_TOKEN_PROVIDER\",\n          \"fs.gs.auth.access.token.provider.impl\" ->\n            \"org.apache.spark.sql.delta.serverSidePlanning.FixedGcsAccessTokenProvider\",\n          \"fs.gs.auth.access.token\" -> token\n        )\n        expirationMs match {\n          case Some(ms) => base + (\"fs.gs.auth.access.token.expiration.ms\" -> ms.toString)\n          case None => base\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/SparkToIcebergExpressionConverterSuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.sources._\nimport org.scalatest.funsuite.AnyFunSuite\nimport shadedForDelta.org.apache.iceberg.expressions.{Expression, ExpressionUtil, Expressions}\n\nclass SparkToIcebergExpressionConverterSuite extends AnyFunSuite {\n\n  private case class ExprConvTestCase(\n    label: String,\n    spark: Filter,\n    iceberg: Option[Expression]\n  )\n\n  // Types that support equality and ordering operations\n  // (EqualTo, NotEqualTo, LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual)\n  //\n  // Note on Date/Timestamp: Spark Filter API sends java.sql.Date/Timestamp, but our converter\n  // transforms them to Int (days since epoch) and Long (microseconds since epoch) for Iceberg.\n  //\n  // Note on escaped column names: Per Spark's Filter documentation, column names with special\n  // characters (dots, spaces, etc.) arrive already escaped with backticks when they represent\n  // literal column names. We verify the converter passes these through unchanged. Examples:\n  //   - \"address.intCol\" = nested field access (struct address, field intCol)\n  //   - \"`address.city`\" = literal column named \"address.city\" (backtick-quoted by Spark)\n  //   - \"parent.`child.name`\" = nested access where child field name is literally \"child.name\"\n  private val orderableTypeTestCases = Seq(\n    (\"intCol\", 42, \"Int\"), // (column name, test value, label to identify test case)\n    (\"longCol\", 100L, \"Long\"),\n    (\"doubleCol\", 99.99, \"Double\"),\n    (\"floatCol\", 10.5f, \"Float\"),\n    (\"decimalCol\", BigDecimal(\"123.45\").bigDecimal, \"Decimal\"),\n    (\"stringCol\", \"test\", \"String\"),\n    (\"dateCol\", java.sql.Date.valueOf(\"2024-01-01\"), \"Date\"),\n    (\"timestampCol\", java.sql.Timestamp.valueOf(\"2024-01-01 12:00:00\"), \"Timestamp\"),\n    (\"localDateCol\", java.time.LocalDate.of(2024, 1, 1), \"LocalDate\"),\n    (\"localDateTimeCol\", java.time.LocalDateTime.of(2024, 1, 1, 12, 0, 0), \"LocalDateTime\"),\n    (\"instantCol\", java.time.Instant.parse(\"2024-01-01T12:00:00Z\"), \"Instant\"),\n    (\"address.intCol\", 42, \"Nested Int\"),\n    (\"metadata.stringCol\", \"test\", \"Nested String\"),\n    (\"`address.city`\", \"Seattle\", \"Escaped Literal String\"),\n    (\"`a.b.c`\", \"value\", \"Escaped Multi-Dot String\"),\n    (\"parent.`child.name`\", \"childValue\", \"Nested with Escaped Field\")\n  )\n\n  // Types that only support equality operators (EqualTo, NotEqualTo, IsNull, IsNotNull)\n  private val equalityOnlyTypesTestCases = Seq(\n    (\"boolCol\", true, \"Boolean\")\n  )\n\n  private val allTypesTestCases = orderableTypeTestCases ++ equalityOnlyTypesTestCases\n  private val testSchema = TestSchemas.testSchema.asStruct()\n\n  private def assertConvert(testCases: Seq[ExprConvTestCase]): Unit = {\n    testCases.foreach { tc =>\n      val result = SparkToIcebergExpressionConverter.convert(tc.spark)\n\n      tc.iceberg match {\n        case Some(expected) =>\n          assert(result.isDefined, s\"[${tc.label}] Should convert: ${tc.spark}\")\n    assert(\n            ExpressionUtil.equivalent(expected, result.get, testSchema, true),\n            s\"[${tc.label}] Expected: $expected, got: ${result.get}\"\n          )\n        case None =>\n          assert(result.isEmpty, s\"[${tc.label}] Should return None for: ${tc.spark}\")\n      }\n    }\n  }\n\n  // ========================================================================\n  // EQUALITY OPERATORS (=, !=)\n  // ========================================================================\n\n  test(\"equality operators (=, !=) on all types including null and NaN handling\") {\n    val equalityOpMappings = Seq(\n      (\"EqualTo\",  // Test case label\n        (col: String, v: Any) => EqualTo(col, v),         // Spark filter builder\n        (col: String, v: Any) => Expressions.equal(col, v)),  // Iceberg expression builder\n      (\"NotEqualTo\",\n        (col: String, v: Any) => Not(EqualTo(col, v)),\n        (col: String, v: Any) => Expressions.notEqual(col, v))\n    )\n\n    // Generate all combinations: all types x equality operators\n    val standardTests = for {\n      (col, value, typeDesc) <- allTypesTestCases\n      (opName, sparkOp, icebergOp) <- equalityOpMappings\n    } yield ExprConvTestCase(\n      s\"$opName $typeDesc\",\n      sparkOp(col, value),\n      // supportBoolean=true because equality operators work on all types including Boolean\n      Some(icebergOp(col, SparkToIcebergExpressionConverter.toIcebergValue(\n        value, supportBoolean = true)))\n    )\n\n    // Null handling: EqualTo(col, null) -> isNull, Not(EqualTo(col, null)) -> notNull\n    val nullHandlingTests = Seq(\n      ExprConvTestCase(\n        \"EqualTo(col, null) converts to isNull\", // Test case label\n        EqualTo(\"stringCol\", null), // Spark filter builder\n        Some(Expressions.isNull(\"stringCol\")) // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"Not(EqualTo(col, null)) converts to notNull (IS NOT NULL)\",\n        Not(EqualTo(\"stringCol\", null)),\n        Some(Expressions.notNull(\"stringCol\"))\n      )\n    )\n\n    // NaN handling: EqualTo/NotEqualTo with NaN convert to isNaN/notNaN predicates\n    val nanHandlingTests = Seq(\n      ExprConvTestCase(\n        \"EqualTo with Double.NaN converts to isNaN\", // Test case label\n        EqualTo(\"doubleCol\", Double.NaN), // Spark filter builder\n        Some(Expressions.isNaN(\"doubleCol\")) // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"EqualTo with Float.NaN converts to isNaN\",\n        EqualTo(\"floatCol\", Float.NaN),\n        Some(Expressions.isNaN(\"floatCol\"))\n      ),\n      ExprConvTestCase(\n        \"Not(EqualTo) with Double.NaN converts to notNaN\",\n        Not(EqualTo(\"doubleCol\", Double.NaN)),\n        Some(Expressions.notNaN(\"doubleCol\"))\n      ),\n      ExprConvTestCase(\n        \"Not(EqualTo) with Float.NaN converts to notNaN\",\n        Not(EqualTo(\"floatCol\", Float.NaN)),\n        Some(Expressions.notNaN(\"floatCol\"))\n      )\n    )\n\n    assertConvert(standardTests ++ nullHandlingTests ++ nanHandlingTests)\n  }\n\n  // ========================================================================\n  // ORDERING COMPARISON OPERATORS (<, >, <=, >=)\n  // ========================================================================\n\n  test(\"ordering comparison operators (<, >, <=, >=) on orderable types\") {\n    // Note: This only tests ordering comparisons (<, >, <=, >=), not equality or other operations\n    val comparisonOpMappings = Seq(\n      (\"LessThan\", // Test case label\n        (col: String, v: Any) => LessThan(col, v), // Spark filter builder\n        (col: String, v: Any) => Expressions.lessThan(col, v)), // Iceberg expression builder\n      (\"GreaterThan\",\n        (col: String, v: Any) => GreaterThan(col, v),\n        (col: String, v: Any) => Expressions.greaterThan(col, v)),\n      (\"LessThanOrEqual\",\n        (col: String, v: Any) => LessThanOrEqual(col, v),\n        (col: String, v: Any) => Expressions.lessThanOrEqual(col, v)),\n      (\"GreaterThanOrEqual\",\n        (col: String, v: Any) => GreaterThanOrEqual(col, v),\n        (col: String, v: Any) => Expressions.greaterThanOrEqual(col, v))\n    )\n\n    // Generate all combinations: orderable types x comparison operators\n    val supportedTests = for {\n      (col, value, typeDesc) <- orderableTypeTestCases\n      (opName, sparkOp, icebergOp) <- comparisonOpMappings\n    } yield ExprConvTestCase(\n      s\"$opName $typeDesc\",\n      sparkOp(col, value),\n      // supportBoolean=false because ordering operators don't work on Boolean type\n      Some(icebergOp(col, SparkToIcebergExpressionConverter.toIcebergValue(\n        value, supportBoolean = false)))\n    )\n\n    // NaN with comparison operators returns None\n    val nanRejectionTests = Seq(\n      ExprConvTestCase(\n        \"LessThan with NaN returns None (undefined)\", // Test case label\n        LessThan(\"doubleCol\", Double.NaN), // Spark filter builder\n        None // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"GreaterThan with NaN returns None (undefined)\",\n        GreaterThan(\"floatCol\", Float.NaN),\n        None\n      ),\n      ExprConvTestCase(\n        \"LessThanOrEqual with NaN returns None (undefined)\",\n        LessThanOrEqual(\"doubleCol\", Double.NaN),\n        None\n      ),\n      ExprConvTestCase(\n        \"GreaterThanOrEqual with NaN returns None (undefined)\",\n        GreaterThanOrEqual(\"floatCol\", Float.NaN),\n        None\n      )\n    )\n\n    assertConvert(supportedTests ++ nanRejectionTests)\n  }\n\n  // ========================================================================\n  // NULL CHECK OPERATORS (IsNull, IsNotNull, Not(IsNull))\n  // ========================================================================\n\n  test(\"null check operators (IsNull, IsNotNull, Not(IsNull)) on all types\") {\n    val nullCheckOpMappings = Seq(\n      (\"IsNull\",  // Test case label\n        (col: String, _: Any) => IsNull(col),              // Spark filter builder\n        (col: String, _: Any) => Expressions.isNull(col)),  // Iceberg expression builder\n      (\"IsNotNull\",\n        (col: String, _: Any) => IsNotNull(col),\n        (col: String, _: Any) => Expressions.notNull(col)),\n      (\"Not(IsNull)\",\n        (col: String, _: Any) => Not(IsNull(col)),\n        (col: String, _: Any) => Expressions.notNull(col))\n    )\n\n    // Generate all combinations: all types x null check operators\n    val testCases = for {\n      (col, value, typeDesc) <- allTypesTestCases\n      (opName, sparkOp, icebergOp) <- nullCheckOpMappings\n    } yield ExprConvTestCase(\n      s\"$opName $typeDesc\",\n      sparkOp(col, value),\n      Some(icebergOp(col, SparkToIcebergExpressionConverter.toIcebergValue(\n        value, supportBoolean = true)))\n    )\n\n    assertConvert(testCases)\n  }\n\n  // ========================================================================\n  // IN AND NOT IN OPERATORS\n  // ========================================================================\n\n  // IN and NOT IN operators require special handling because:\n  // - They accept arrays of values, requiring per-element type coercion\n  // - Null values must be filtered out (SQL semantics: col IN (1, NULL) = col IN (1))\n  // - Empty arrays after null filtering result in always-false/true predicates\n  // - Type conversion needed for each array element (Scala -> Java types)\n  test(\"IN and NOT IN operators with type coercion and null handling\") {\n    // Helper to generate multiple test values for IN/NOT IN operators\n    def generateInValues(value: Any): Array[Any] = value match {\n      case v: Int => Array(v, v + 1, v + 2)\n      case v: Long => Array(v, v + 1L, v + 2L)\n      case v: Float => Array(v, v + 1.0f, v + 2.0f)\n      case v: Double => Array(v, v + 1.0, v + 2.0)\n      case v: String => Array(v, s\"${v}_2\", s\"${v}_3\")\n      case v: java.math.BigDecimal =>\n        Array(v, v.add(java.math.BigDecimal.ONE), v.add(java.math.BigDecimal.TEN))\n      case v: Boolean => Array(v, !v)\n      case v: java.sql.Date =>\n        Array(v, new java.sql.Date(v.getTime + 86400000L)) // +1 day in millis\n      case v: java.sql.Timestamp =>\n        Array(v, new java.sql.Timestamp(v.getTime + 3600000L)) // +1 hour in millis\n      case v: java.time.LocalDate =>\n        Array(v, v.plusDays(1), v.plusDays(2))\n      case v: java.time.LocalDateTime =>\n        Array(v, v.plusHours(1), v.plusHours(2))\n      case v: java.time.Instant =>\n        Array(v, v.plusSeconds(3600), v.plusSeconds(7200)) // +1 hour, +2 hours\n      case _ => Array(value)\n    }\n\n    val inOpMappings = Seq(\n      (\"In\",\n        (col: String, values: Array[Any]) => In(col, values),\n        (col: String, values: Array[Any]) => Expressions.in(col, values: _*)),\n      (\"Not(In)\",\n        (col: String, values: Array[Any]) => Not(In(col, values)),\n        (col: String, values: Array[Any]) => Expressions.notIn(col, values: _*))\n    )\n\n    // Test IN and NOT IN operators for all types\n    val typeTests = for {\n      (col, value, typeDesc) <- allTypesTestCases\n      (opName, sparkOp, icebergOp) <- inOpMappings\n    } yield {\n      val values = generateInValues(value)\n      val icebergValues = values.map(v =>\n        SparkToIcebergExpressionConverter.toIcebergValue(v, supportBoolean = true))\n      ExprConvTestCase(\n        s\"$opName with $typeDesc\",\n        sparkOp(col, values),\n        Some(icebergOp(col, icebergValues))\n      )\n    }\n\n    // Null handling tests for both In and Not(In)\n    val nullHandlingTests = for {\n      (opName, sparkOp, icebergOp) <- inOpMappings\n    } yield Seq(\n      ExprConvTestCase(\n        s\"$opName with null values (nulls filtered)\",\n        sparkOp(\"stringCol\", Array(null, \"value1\", \"value2\")),\n        Some(icebergOp(\"stringCol\", Array(\"value1\", \"value2\")))\n      ),\n      ExprConvTestCase(\n        s\"$opName with null and integers\",\n        sparkOp(\"intCol\", Array(null, 1, 2)),\n        Some(icebergOp(\"intCol\", Array(1: Integer, 2: Integer)))\n      ),\n      ExprConvTestCase(\n        s\"$opName with only null\",\n        sparkOp(\"stringCol\", Array(null)),\n        Some(icebergOp(\"stringCol\", Array()))\n      ),\n      ExprConvTestCase(\n        s\"$opName with empty array\",\n        sparkOp(\"intCol\", Array()),\n        Some(icebergOp(\"intCol\", Array()))\n      )\n    )\n\n    // Specific examples for both In and Not(In)\n    val specificExamples = for {\n      (opName, sparkOp, icebergOp) <- inOpMappings\n    } yield Seq(\n      ExprConvTestCase(\n        s\"$opName with string values\",\n        sparkOp(\"stringCol\", Array(\"value1\", \"value2\")),\n        Some(icebergOp(\"stringCol\", Array(\"value1\", \"value2\")))\n      ),\n      ExprConvTestCase(\n        s\"$opName with single value\",\n        sparkOp(\"intCol\", Array(42)),\n        Some(icebergOp(\"intCol\", Array(42: Integer)))\n      ),\n      ExprConvTestCase(\n        s\"$opName with nested column\",\n        sparkOp(\"address.intCol\", Array(1, 2, 3)),\n        Some(icebergOp(\"address.intCol\", Array(1: Integer, 2: Integer, 3: Integer)))\n      )\n    )\n\n    assertConvert(typeTests ++ nullHandlingTests.flatten ++ specificExamples.flatten)\n  }\n\n  // ========================================================================\n  // STRING OPERATIONS\n  // ========================================================================\n\n  test(\"string operations (startsWith/notStartsWith supported, endsWith/contains unsupported)\") {\n    val stringOpMappings = Seq(\n      (\"StringStartsWith\",\n        (col: String, prefix: String) => StringStartsWith(col, prefix),\n        (col: String, prefix: String) => Expressions.startsWith(col, prefix)),\n      (\"Not(StringStartsWith)\",\n        (col: String, prefix: String) => Not(StringStartsWith(col, prefix)),\n        (col: String, prefix: String) => Expressions.notStartsWith(col, prefix))\n    )\n\n    val stringColumns = Seq(\n      (\"stringCol\", \"string column\"),\n      (\"metadata.stringCol\", \"nested string column\")\n    )\n\n    val prefixTestCases = Seq(\n      (\"prefix\", \"basic prefix\"),\n      (\"\", \"empty prefix\")\n    )\n\n    // Generate all combinations: string columns x prefixes x [startsWith, notStartsWith]\n    val supportedTests = for {\n      (col, colDesc) <- stringColumns\n      (prefix, prefixDesc) <- prefixTestCases\n      (opName, sparkOp, icebergOp) <- stringOpMappings\n    } yield ExprConvTestCase(\n      s\"$opName with $prefixDesc on $colDesc\",\n      sparkOp(col, prefix),\n      Some(icebergOp(col, prefix))\n    )\n\n    // Unsupported: StringEndsWith, StringContains\n    val unsupportedTests = Seq(\n      ExprConvTestCase(\n        \"StringEndsWith (unsupported)\",\n        StringEndsWith(\"stringCol\", \"suffix\"),\n        None\n      ),\n      ExprConvTestCase(\n        \"StringContains (unsupported)\",\n        StringContains(\"stringCol\", \"substr\"),\n        None\n      )\n    )\n\n    assertConvert(supportedTests ++ unsupportedTests)\n  }\n\n  // ========================================================================\n  // LOGICAL OPERATORS (AND, OR)\n  // ========================================================================\n\n  test(\"logical operators (AND, OR) with valid and invalid combinations\") {\n    // Valid combinations: both sides convert successfully\n    val validCombinations = Seq(\n      ExprConvTestCase(\n        \"AND with two different types\", // Test case label\n        And( // Spark filter builder\n          EqualTo(\"intCol\", 42),\n          GreaterThan(\"longCol\", 100L)\n        ),\n        Some( // Iceberg expression builder\n          Expressions.and(\n            Expressions.equal(\"intCol\", 42),\n            Expressions.greaterThan(\"longCol\", 100L))\n        )\n      ),\n      ExprConvTestCase(\n        \"OR with two different types\",\n        Or(\n          LessThan(\"doubleCol\", 99.99), IsNull(\"stringCol\")\n        ),\n        Some(\n          Expressions.or(\n            Expressions.lessThan(\"doubleCol\", 99.99),\n            Expressions.isNull(\"stringCol\")\n          )\n        )\n      ),\n      ExprConvTestCase(\n        \"Nested logical operators\",\n        And(\n          Or(\n            EqualTo(\"intCol\", 1), EqualTo(\"intCol\", 2)\n          ),\n          And(\n            GreaterThan(\"longCol\", 0L), LessThan(\"longCol\", 100L)\n          )\n        ),\n        Some(\n          Expressions.and(\n            Expressions.or(\n              Expressions.equal(\"intCol\", 1), Expressions.equal(\"intCol\", 2)\n      ),\n      Expressions.and(\n              Expressions.greaterThan(\"longCol\", 0L), Expressions.lessThan(\"longCol\", 100L)\n            )\n          )\n        )\n      ),\n      ExprConvTestCase(\n        \"Range filter: 0 < intCol < 100\",\n        And(GreaterThan(\"intCol\", 0), LessThan(\"intCol\", 100)),\n        Some(Expressions.and(\n          Expressions.greaterThan(\"intCol\", 0), Expressions.lessThan(\"intCol\", 100)\n        ))\n      )\n    )\n\n    // Invalid combinations: when one side fails conversion, the whole expression returns None\n    val validFilter = EqualTo(\"intCol\", 42)\n    val unsupportedFilter = StringEndsWith(\"stringCol\", \"suffix\")\n\n    val invalidCombinations = Seq(\n      ExprConvTestCase(\n        \"AND with unsupported right side\", // Test case label\n        And(validFilter, unsupportedFilter), // Spark filter builder\n        None // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"AND with unsupported left side\",\n        And(unsupportedFilter, validFilter),\n        None\n      ),\n      ExprConvTestCase(\n        \"OR with unsupported right side\",\n        Or(validFilter, unsupportedFilter),\n        None\n      ),\n      ExprConvTestCase(\n        \"OR with unsupported left side\",\n        Or(unsupportedFilter, validFilter),\n        None\n      ),\n      ExprConvTestCase(\n        \"Nested AND with unsupported in OR\",\n        And(\n          validFilter, Or(\n            validFilter,\n            unsupportedFilter\n          )\n        ),\n        None\n      )\n    )\n\n    assertConvert(validCombinations ++ invalidCombinations)\n  }\n\n  // ========================================================================\n  // NOT OPERATOR (unsupported cases)\n  // ========================================================================\n\n  test(\"NOT operator with unsupported inner filters\") {\n    // Note: Supported NOT patterns are tested in their respective operator pair tests:\n    // - Not(EqualTo) tested in equality operators\n    // - Not(In) tested in IN and NOT IN operators\n    // - Not(IsNull) tested in null check operators\n    // - Not(StringStartsWith) tested in string operations\n    //\n    // This test only covers unsupported NOT patterns\n    val testCases = Seq(\n      ExprConvTestCase(\n        \"Not(LessThan) is unsupported\",\n        Not(LessThan(\"intCol\", 5)),\n        None\n      ),\n      ExprConvTestCase(\n        \"Not(GreaterThan) is unsupported\",\n        Not(GreaterThan(\"longCol\", 100L)),\n        None\n      ),\n      ExprConvTestCase(\n        \"Not(And) is unsupported\",\n        Not(And(EqualTo(\"intCol\", 1), EqualTo(\"longCol\", 2L))),\n        None\n      )\n    )\n\n    assertConvert(testCases)\n  }\n\n  // ========================================================================\n  // BOOLEAN LITERALS\n  // ========================================================================\n\n  test(\"boolean literals (AlwaysTrue, AlwaysFalse)\") {\n    val testCases = Seq(\n      ExprConvTestCase(\n        \"AlwaysTrue\", // Test case label\n        AlwaysTrue(), // Spark filter builder\n        Some(Expressions.alwaysTrue()) // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"AlwaysFalse\",\n        AlwaysFalse(),\n        Some(Expressions.alwaysFalse())\n      )\n    )\n\n    assertConvert(testCases)\n  }\n\n  // ========================================================================\n  // TYPE CONVERSIONS AND BOUNDARY VALUES\n  // ========================================================================\n\n  test(\"type conversions (Date/Timestamp) and boundary values\") {\n    val testDate = java.sql.Date.valueOf(\"2024-01-01\")\n    val expectedDateDays = (testDate.getTime / (1000L * 60 * 60 * 24)).toInt\n\n    val testTimestamp = java.sql.Timestamp.valueOf(\"2024-01-01 00:00:00\")\n    val expectedTimestampMicros =\n      testTimestamp.getTime * 1000 + (testTimestamp.getNanos % 1000000) / 1000\n\n    // java.time types\n    val testLocalDate = java.time.LocalDate.of(2024, 1, 1)\n    val expectedLocalDateDays = testLocalDate.toEpochDay.toInt\n\n    val testLocalDateTime = java.time.LocalDateTime.of(2024, 1, 1, 12, 30, 45)\n    val expectedLocalDateTimeMicros = testLocalDateTime.toEpochSecond(\n      java.time.ZoneOffset.UTC) * 1000000 + testLocalDateTime.getNano / 1000\n\n    val testInstant = java.time.Instant.parse(\"2024-01-01T12:30:45.123456Z\")\n    val expectedInstantMicros = testInstant.getEpochSecond * 1000000 + testInstant.getNano / 1000\n\n    val testCases = Seq(\n      // Date/Timestamp: Spark sends java.sql types, but we convert to Int/Long for Iceberg\n      ExprConvTestCase(\n        \"Date converted to days since epoch\", // Test case label\n        EqualTo(\"dateCol\", testDate), // Spark filter builder\n        Some(Expressions.equal(\"dateCol\", expectedDateDays: Integer)) // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"Timestamp converted to microseconds since epoch\",\n        EqualTo(\"timestampCol\", testTimestamp),\n        Some(Expressions.equal(\"timestampCol\", expectedTimestampMicros: java.lang.Long))\n      ),\n\n      // java.time types: converted to Int (days) or Long (microseconds)\n      ExprConvTestCase(\n        \"LocalDate converted to days since epoch\",\n        EqualTo(\"localDateCol\", testLocalDate),\n        Some(Expressions.equal(\"localDateCol\", expectedLocalDateDays: Integer))\n      ),\n      ExprConvTestCase(\n        \"LocalDateTime converted to microseconds since epoch\",\n        EqualTo(\"localDateTimeCol\", testLocalDateTime),\n        Some(Expressions.equal(\"localDateTimeCol\", expectedLocalDateTimeMicros: java.lang.Long))\n      ),\n      ExprConvTestCase(\n        \"Instant converted to microseconds since epoch\",\n        EqualTo(\"instantCol\", testInstant),\n        Some(Expressions.equal(\"instantCol\", expectedInstantMicros: java.lang.Long))\n      ),\n\n      // Boundary values\n      ExprConvTestCase(\n        \"Int.MinValue boundary\", // Test case label\n        EqualTo(\"intCol\", Int.MinValue), // Spark filter builder\n        Some(Expressions.equal(\"intCol\", Int.MinValue)) // Iceberg expression builder\n      ),\n      ExprConvTestCase(\n        \"Int.MaxValue boundary\",\n        EqualTo(\"intCol\", Int.MaxValue),\n        Some(Expressions.equal(\"intCol\", Int.MaxValue))\n      ),\n      ExprConvTestCase(\n        \"Long.MinValue boundary\",\n        EqualTo(\"longCol\", Long.MinValue),\n        Some(Expressions.equal(\"longCol\", Long.MinValue))\n      ),\n      ExprConvTestCase(\n        \"Long.MaxValue boundary\",\n        EqualTo(\"longCol\", Long.MaxValue),\n        Some(Expressions.equal(\"longCol\", Long.MaxValue))\n      )\n    )\n\n    assertConvert(testCases)\n  }\n\n  // ========================================================================\n  // UNSUPPORTED FILTERS\n  // ========================================================================\n\n  test(\"unsupported filters return None\") {\n    // This test ensures that all known unsupported Spark Filter types return None\n    // If Spark adds new filter types, our converter will skip them via case _ => None\n    val testCases = Seq(\n      // EqualNullSafe - Iceberg doesn't have null-safe equality\n      ExprConvTestCase(\n        \"EqualNullSafe\", // Test case label\n        EqualNullSafe(\"intCol\", 5), // Spark filter builder\n        None // Iceberg expression builder\n      ),\n      // StringEndsWith - Iceberg API doesn't provide this predicate\n      ExprConvTestCase(\n        \"StringEndsWith\",\n        StringEndsWith(\"stringCol\", \"suffix\"),\n        None\n      ),\n      // StringContains - Iceberg API doesn't provide this predicate\n      ExprConvTestCase(\n        \"StringContains\",\n        StringContains(\"stringCol\", \"substring\"),\n        None\n      ),\n      // Not with non-EqualTo inner filter - Iceberg doesn't support arbitrary NOT\n      // Only Not(EqualTo) is converted as a special case\n      ExprConvTestCase(\n        \"Not(LessThan) - arbitrary NOT unsupported\",\n        Not(LessThan(\"intCol\", 10)),\n        None\n      ),\n      ExprConvTestCase(\n        \"Not(GreaterThan) - arbitrary NOT unsupported\",\n        Not(GreaterThan(\"intCol\", 10)),\n        None\n      ),\n      ExprConvTestCase(\n        \"Not(And) - arbitrary NOT unsupported\",\n        Not(And(EqualTo(\"intCol\", 1), EqualTo(\"longCol\", 2L))),\n        None\n      )\n    )\n\n    assertConvert(testCases)\n  }\n\n  // ========================================================================\n  // UNSUPPORTED VALUE TYPES\n  // ========================================================================\n\n  test(\"filters with unsupported value types return None\") {\n    // Define unsupported types for which conversion must fail\n    val unsupportedTypes = Seq(\n      (Array(1, 2, 3), \"Array\"),\n      (Map(\"key\" -> 1), \"Map\"),\n      (Row(1, \"test\"), \"Row/Struct\"),\n      (Array[Byte](1, 2, 3), \"byte array\"),\n      (5.toByte, \"Byte\"),\n      (5.toShort, \"Short\")\n    )\n\n    // Define operators that must reject unsupported types\n    val operators = Seq(\n      (\"EqualTo\", (col: String, v: Any) => EqualTo(col, v)),\n      (\"LessThan\", (col: String, v: Any) => LessThan(col, v)),\n      (\"GreaterThan\", (col: String, v: Any) => GreaterThan(col, v)),\n      (\"LessThanOrEqual\", (col: String, v: Any) => LessThanOrEqual(col, v)),\n      (\"GreaterThanOrEqual\", (col: String, v: Any) => GreaterThanOrEqual(col, v))\n    )\n\n    // Generate all combinations: unsupported types x operators\n    val operatorTests = for {\n      (value, typeDesc) <- unsupportedTypes\n      (opName, sparkOp) <- operators\n    } yield ExprConvTestCase(\n      s\"$opName with $typeDesc should be unsupported\", // Test case label\n      sparkOp(\"intCol\", value), // Spark filter builder\n      None // Iceberg expression builder\n    )\n\n    // Boolean with comparison operators (supportBoolean=false for these)\n    val booleanComparisonTests = Seq(\n      (\"LessThan\", (col: String, v: Any) => LessThan(col, v)),\n      (\"GreaterThan\", (col: String, v: Any) => GreaterThan(col, v)),\n      (\"LessThanOrEqual\", (col: String, v: Any) => LessThanOrEqual(col, v)),\n      (\"GreaterThanOrEqual\", (col: String, v: Any) => GreaterThanOrEqual(col, v))\n    ).map { case (opName, sparkOp) =>\n      ExprConvTestCase(\n        s\"$opName with Boolean (unsupported for comparison)\",\n        sparkOp(\"boolCol\", true),\n        None\n      )\n    }\n\n    // Special case: In with nested Array values\n    val inWithNestedArrays = ExprConvTestCase(\n      \"In with nested Array values\",\n      In(\"intCol\", Array(Array(1), Array(2))),\n      None\n    )\n\n    assertConvert(operatorTests ++ booleanComparisonTests ++ Seq(inWithNestedArrays))\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/TestSchemas.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport shadedForDelta.org.apache.iceberg.Schema\nimport shadedForDelta.org.apache.iceberg.types.Types\nimport org.apache.spark.sql.types._\n\nprivate[serverSidePlanning] object TestSchemas {\n  /**\n   * Shared test schema used across all server-side planning test suites.\n   * Structure:\n   * - Flat fields (12 types): intCol, longCol, doubleCol, floatCol, stringCol, boolCol,\n   *                           decimalCol, dateCol, timestampCol, localDateCol,\n   *                           localDateTimeCol, instantCol\n   * - Nested struct (ID 13): address with intCol - tests nested field access\n   * - Nested struct (ID 14): metadata with stringCol - tests nested string field\n   * - Nested struct with dotted field (ID 15): parent with \"child.name\" - tests escaping at\n   *   nested level\n   * - Literal top-level dotted columns (IDs 16-17): address.city, a.b.c - tests top-level\n   *   escaping\n   */\n  val testSchema = new Schema(\n    // Flat fields (IDs 1-12)\n    Types.NestedField.required(1, \"intCol\", Types.IntegerType.get),\n    Types.NestedField.required(2, \"longCol\", Types.LongType.get),\n    Types.NestedField.required(3, \"doubleCol\", Types.DoubleType.get),\n    Types.NestedField.required(4, \"floatCol\", Types.FloatType.get),\n    Types.NestedField.required(5, \"stringCol\", Types.StringType.get),\n    Types.NestedField.required(6, \"boolCol\", Types.BooleanType.get),\n    Types.NestedField.required(7, \"decimalCol\", Types.DecimalType.of(10, 2)),\n    Types.NestedField.required(8, \"dateCol\", Types.DateType.get),\n    Types.NestedField.required(9, \"timestampCol\", Types.TimestampType.withoutZone),\n    Types.NestedField.required(10, \"localDateCol\", Types.DateType.get),\n    Types.NestedField.required(11, \"localDateTimeCol\", Types.TimestampType.withoutZone),\n    Types.NestedField.required(12, \"instantCol\", Types.TimestampType.withZone),\n\n    // Nested struct for testing nested field access (ID 13)\n    Types.NestedField.required(13, \"address\", Types.StructType.of(\n      Types.NestedField.required(101, \"intCol\", Types.IntegerType.get)\n    )),\n\n    // Nested struct for testing nested string field (ID 14)\n    Types.NestedField.required(14, \"metadata\", Types.StructType.of(\n      Types.NestedField.required(111, \"stringCol\", Types.StringType.get)\n    )),\n\n    // Nested struct with field that has dots in its name (ID 15)\n    // Tests escaping at nested level: parent.`child.name`\n    Types.NestedField.required(15, \"parent\", Types.StructType.of(\n      Types.NestedField.required(121, \"`child.name`\", Types.StringType.get)\n    )),\n\n    // Literal top-level column names with dots (IDs 16-17) - Test escaping\n    Types.NestedField.required(16, \"`address.city`\", Types.StringType.get),\n    Types.NestedField.required(17, \"`a.b.c`\", Types.StringType.get)\n  )\n\n  /**\n   * Spark StructType corresponding to the testSchema above.\n   * Used for filter conversion in tests.\n   */\n  val sparkSchema: StructType = StructType(Seq(\n    StructField(\"intCol\", IntegerType, nullable = false),\n    StructField(\"longCol\", LongType, nullable = false),\n    StructField(\"doubleCol\", DoubleType, nullable = false),\n    StructField(\"floatCol\", FloatType, nullable = false),\n    StructField(\"stringCol\", StringType, nullable = false),\n    StructField(\"boolCol\", BooleanType, nullable = false),\n    StructField(\"decimalCol\", DecimalType(10, 2), nullable = false),\n    StructField(\"dateCol\", DateType, nullable = false),\n    StructField(\"timestampCol\", TimestampType, nullable = false),\n    StructField(\"localDateCol\", DateType, nullable = false),\n    StructField(\"localDateTimeCol\", TimestampType, nullable = false),\n    StructField(\"instantCol\", TimestampType, nullable = false),\n    // Nested struct for testing nested field access\n    StructField(\"address\", StructType(Seq(\n      StructField(\"intCol\", IntegerType, nullable = false)\n    )), nullable = false),\n    // Nested struct for testing nested string field\n    StructField(\"metadata\", StructType(Seq(\n      StructField(\"stringCol\", StringType, nullable = false)\n    )), nullable = false),\n    // Nested struct with field that has dots in its name\n    // Tests escaping at nested level: parent.`child.name`\n    StructField(\"parent\", StructType(Seq(\n      StructField(\"`child.name`\", StringType, nullable = false)\n    )), nullable = false),\n    // Literal top-level column names with dots - Test escaping\n    StructField(\"`address.city`\", StringType, nullable = false),\n    StructField(\"`a.b.c`\", StringType, nullable = false)\n  ))\n}\n\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/uniform/IcebergCompatV2EnableUniformByAlterTableSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.{ColumnMappingTableFeature, DeltaLog, IcebergCompatV2TableFeature, UniversalFormat}\nimport org.apache.spark.sql.delta.uniform.IcebergCompatV2EnableUniformByAlterTableSuiteBase\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\nclass IcebergCompatV2EnableUniformByAlterTableSuite\n    extends IcebergCompatV2EnableUniformByAlterTableSuiteBase\n    with WriteDeltaHMSReadIceberg {\n  override def withTempTableAndDir(f: (String, String) => Unit): Unit = {\n    val tableId = s\"testTable${UUID.randomUUID()}\".replace(\"-\", \"_\")\n    withTempDir { dir =>\n      val tablePath = new Path(dir.toString, \"table\")\n\n      withTable(tableId) {\n        f(tableId, s\"'$tablePath'\")\n      }\n    }\n  }\n\n  override def executeSql(sqlStr: String): DataFrame = write(sqlStr)\n\n  override def assertUniFormIcebergProtocolAndProperties(id: String): Unit = {\n    val snapshot = DeltaLog.forTable(spark, new TableIdentifier(id)).update()\n    val protocol = snapshot.protocol\n    val tblProperties = snapshot.getProperties\n    val tableFeature = IcebergCompatV2TableFeature\n\n    val expectedMinReaderVersion = Math.max(\n      ColumnMappingTableFeature.minReaderVersion,\n      tableFeature.minReaderVersion\n    )\n\n    val expectedMinWriterVersion = Math.max(\n      ColumnMappingTableFeature.minWriterVersion,\n      tableFeature.minWriterVersion\n    )\n\n    assert(protocol.minReaderVersion >= expectedMinReaderVersion)\n    assert(protocol.minWriterVersion >= expectedMinWriterVersion)\n    assert(protocol.writerFeatures.get.contains(tableFeature.name))\n    assert(tblProperties(s\"delta.enableIcebergCompatV2\") === \"true\")\n    assert(Seq(\"name\", \"id\").contains(tblProperties(\"delta.columnMapping.mode\")))\n    assert(UniversalFormat.icebergEnabled(snapshot.metadata))\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/uniform/TypeWideningUniformSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport org.apache.spark.sql.delta.typewidening.TypeWideningUniformTests\n\n/**\n * Suite running Uniform Iceberg + type widening tests against HMS.\n */\nclass TypeWideningUniformSuite extends TypeWideningUniformTests with WriteDeltaHMSReadIceberg\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/uniform/UniFormConverterSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport shadedForDelta.org.apache.iceberg.hadoop.HadoopTables\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.delta.{DeltaLog, Snapshot}\nimport org.apache.spark.sql.delta.icebergShaded.{IcebergConverter, UNIFORM_CC_MODE}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass IcebergConverterForTest extends IcebergConverter {\n  def convertSnapshotAndReturnMetadataPath(\n      snapshotToConvert: Snapshot,\n      catalogTable: CatalogTable): String = {\n    val icebergTxn = convertSnapshotInternal(\n      snapshotToConvert,\n      readSnapshotOpt = None,\n      lastConvertedInfo = LastConvertedIcebergInfo(\n        icebergTable = None,\n        icebergSnapshotId = None,\n        deltaVersionConverted = None,\n        baseMetadataLocationOpt = None\n      ),\n      conversionContext = new ConversionContext(\n        conversionMode = UNIFORM_CC_MODE,\n        additionalDeltaActionsToCommit = None,\n        opType = \"delta.iceberg.conversion.convertSnapshot\"\n      ),\n      catalogTable\n    )\n    icebergTxn.getConvertedIcebergMetadata._1\n  }\n}\n\nclass UniFormConverterSuite extends QueryTest with SharedSparkSession with DeltaSQLCommandTest {\n  test(\"convertSnapshot writes Iceberg metadata and file count matches Delta snapshot\") {\n    val tableName = \"test_iceberg_converter\"\n    withTable(tableName) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $tableName (id INT, name STRING) USING DELTA\n           |TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.enableIcebergCompatV2' = 'true',\n           |  'delta.universalFormat.enabledFormats' = 'iceberg'\n           |)\"\"\".stripMargin)\n      // Do write\n      spark.sql(s\"INSERT INTO $tableName VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')\")\n      val tableId = TableIdentifier(tableName)\n      val deltaLog = DeltaLog.forTable(spark, tableId)\n      val snapshot = deltaLog.update()\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(tableId)\n      // Trigger conversion\n      val converter = new IcebergConverterForTest()\n      val metadataPath = converter.convertSnapshotAndReturnMetadataPath(snapshot, catalogTable)\n      // Check match\n      val icebergTable =\n        new HadoopTables(deltaLog.newDeltaHadoopConf()).load(metadataPath)\n      val numFilesInIceberg =\n        icebergTable.currentSnapshot().summary().get(\"total-data-files\").toInt\n      assert(\n        numFilesInIceberg == snapshot.numOfFiles,\n        s\"Iceberg total-data-files ($numFilesInIceberg) must equal \" +\n          s\"Delta numOfFiles (${snapshot.numOfFiles})\")\n    }\n  }\n\n  test(\"convertSnapshot file count matches Delta snapshot after multiple inserts\") {\n    val tableName = \"test_iceberg_converter_multi\"\n    withTable(tableName) {\n      spark.sql(\n        s\"\"\"CREATE TABLE $tableName (id INT) USING DELTA\n           |TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.enableIcebergCompatV2' = 'true',\n           |  'delta.universalFormat.enabledFormats' = 'iceberg'\n           |)\"\"\".stripMargin)\n      // Do some writes\n      spark.sql(s\"INSERT INTO $tableName VALUES (1)\")\n      spark.sql(s\"INSERT INTO $tableName VALUES (2)\")\n      spark.sql(s\"INSERT INTO $tableName VALUES (3)\")\n      val tableId = TableIdentifier(tableName)\n      val deltaLog = DeltaLog.forTable(spark, tableId)\n      val snapshot = deltaLog.update()\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(tableId)\n      assert(snapshot.numOfFiles == 3)\n      // Trigger conversion\n      val converter = new IcebergConverterForTest()\n      val metadataPath = converter.convertSnapshotAndReturnMetadataPath(snapshot, catalogTable)\n      // Check match\n      val icebergTable = new HadoopTables(deltaLog.newDeltaHadoopConf()).load(metadataPath)\n      val numFilesInIceberg =\n        icebergTable.currentSnapshot().summary().get(\"total-data-files\").toInt\n      assert(\n        numFilesInIceberg == snapshot.numOfFiles,\n        s\"Iceberg total-data-files ($numFilesInIceberg) must equal \" +\n          s\"Delta numOfFiles (${snapshot.numOfFiles})\")\n    }\n  }\n}\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/uniform/UniFormE2EIcebergSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.{SparkConf, SparkSessionSwitch}\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.uniform.hms.HMSTest\n\n/**\n * This trait allows the tests to write with Delta\n * using a in-memory HiveMetaStore as catalog,\n * and read from the same HiveMetaStore with Iceberg.\n */\ntrait WriteDeltaHMSReadIceberg extends UniFormE2ETest\n  with DeltaSQLCommandTest with HMSTest with SparkSessionSwitch {\n\n  override protected def sparkConf: SparkConf =\n    setupSparkConfWithHMS(super.sparkConf)\n      .set(DeltaSQLConf.DELTA_UNIFORM_ICEBERG_SYNC_CONVERT_ENABLED.key, \"true\")\n      .set(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key, \"true\")\n      .set(DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key, \"true\")\n\n  private var _readerSparkSession: Option[SparkSession] = None\n  /**\n   * Verify the result by reading from the reader session and compare the result to the expected.\n   *\n   * @param table  write table name\n   * @param fields fields to verify, separated by comma. E.g., \"col1, col2\"\n   * @param orderBy fields to order the results, separated by comma.\n   * @param expect expected result\n   */\n  protected override def readAndVerify(\n      table: String, fields: String, orderBy: String, expect: Seq[Row]): Unit = {\n    val translated = tableNameForRead(table)\n    withSession(readerSparkSession) { session =>\n      checkAnswer(session.sql(s\"SELECT $fields FROM $translated ORDER BY $orderBy\"), expect)\n    }\n  }\n\n  protected def readerSparkSession: SparkSession = {\n    if (_readerSparkSession.isEmpty) {\n      // call to newSession makes sure\n      // [[SparkSession.getOrCreate]] gives a new session\n      // and [[SparkContext.getOrCreate]] uses a new context\n      _readerSparkSession = Some(newSession(createIcebergSparkSession))\n    }\n    _readerSparkSession.get\n  }\n}\n\n/**\n * No test should go here. Please add tests in [[UniFormE2EIcebergSuiteBase]]\n */\n"
  },
  {
    "path": "iceberg/src/test/scala/org/apache/spark/sql/delta/uniform/UniversalFormatSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.{\n  DeltaLog,\n  IcebergCompatUtilsBase,\n  UniFormWithIcebergCompatV1SuiteBase,\n  UniFormWithIcebergCompatV2SuiteBase,\n  UniversalFormatMiscSuiteBase,\n  UniversalFormatSuiteBase}\nimport org.apache.spark.sql.delta.commands.DeltaReorgTableCommand\nimport org.apache.spark.sql.delta.icebergShaded.IcebergTransactionUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{DataFrame, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\n\n/** Contains shared utils for both IcebergCompatV1, IcebergCompatV2 and MISC suites. */\ntrait UniversalFormatSuiteUtilsBase\n    extends IcebergCompatUtilsBase\n    with WriteDeltaHMSReadIceberg {\n  override def withTempTableAndDir(f: (String, String) => Unit): Unit = {\n    val tableId = s\"testTable${UUID.randomUUID()}\".replace(\"-\", \"_\")\n    withTempDir { dir =>\n      val tablePath = new Path(dir.toString, \"table\")\n\n      withTable(tableId) {\n        f(tableId, s\"'$tablePath'\")\n      }\n    }\n  }\n\n  override def executeSql(sqlStr: String): DataFrame = write(sqlStr)\n\n  override protected val allReaderWriterVersions: Seq[(Int, Int)] = (1 to 3)\n    .flatMap { r => (1 to 7).filter(_ != 6).map(w => (r, w)) }\n    // can only be at minReaderVersion >= 3 if minWriterVersion is >= 7\n    .filterNot { case (r, w) => w < 7 && r >= 3 }\n}\n\nclass UniversalFormatSuite\n    extends UniversalFormatMiscSuiteBase\n    with UniversalFormatSuiteUtilsBase\n\nclass UniFormWithIcebergCompatV1Suite\n    extends UniversalFormatSuiteUtilsBase\n    with UniFormWithIcebergCompatV1SuiteBase\n\nclass UniFormWithIcebergCompatV2Suite\n    extends UniversalFormatSuiteUtilsBase\n    with UniFormWithIcebergCompatV2SuiteBase\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/MetadataUpdate.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\npackage org.apache.iceberg;\n\nimport java.io.Serializable;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport org.apache.iceberg.encryption.EncryptedKey;\nimport org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet;\nimport org.apache.iceberg.view.ViewMetadata;\nimport org.apache.iceberg.view.ViewVersion;\n\n/** Represents a change to table or view metadata. */\n\n/**\n * This class is directly copied from iceberg repo 1.10.0 with following changes\n * Changes: L91 Added back the deprecated API in 1.9.0\n *              public AddSchema(Schema schema, int lastColumnId)\n */\npublic interface MetadataUpdate extends Serializable {\n  default void applyTo(TableMetadata.Builder metadataBuilder) {\n    throw new UnsupportedOperationException(\n        String.format(\"Cannot apply update %s to a table\", this.getClass().getSimpleName()));\n  }\n\n  default void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n    throw new UnsupportedOperationException(\n        String.format(\"Cannot apply update %s to a view\", this.getClass().getSimpleName()));\n  }\n\n  class AssignUUID implements MetadataUpdate {\n    private final String uuid;\n\n    public AssignUUID(String uuid) {\n      this.uuid = uuid;\n    }\n\n    public String uuid() {\n      return uuid;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.assignUUID(uuid);\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder metadataBuilder) {\n      metadataBuilder.assignUUID(uuid);\n    }\n  }\n\n  class UpgradeFormatVersion implements MetadataUpdate {\n    private final int formatVersion;\n\n    public UpgradeFormatVersion(int formatVersion) {\n      this.formatVersion = formatVersion;\n    }\n\n    public int formatVersion() {\n      return formatVersion;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.upgradeFormatVersion(formatVersion);\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.upgradeFormatVersion(formatVersion);\n    }\n  }\n\n  class AddSchema implements MetadataUpdate {\n    private final Schema schema;\n    private final int lastColumnId;\n\n    public AddSchema(Schema schema) {\n      this(schema, schema.highestFieldId());\n    }\n\n    /**\n     * HACK-HACK This is added\n     * Set the schema\n     * @deprecated in 1.9.0\n     */\n    @Deprecated\n    public AddSchema(Schema schema, int lastColumnId) {\n      this.schema = schema;\n      this.lastColumnId = lastColumnId;\n    }\n\n    public Schema schema() {\n      return schema;\n    }\n\n    // HACK-HACK This is modified\n    public int lastColumnId() {\n      return lastColumnId;\n    }\n\n    // HACK-HACK This is modified\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.addSchema(schema, lastColumnId);\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.addSchema(schema);\n    }\n  }\n\n  class SetCurrentSchema implements MetadataUpdate {\n    private final int schemaId;\n\n    public SetCurrentSchema(int schemaId) {\n      this.schemaId = schemaId;\n    }\n\n    public int schemaId() {\n      return schemaId;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setCurrentSchema(schemaId);\n    }\n  }\n\n  class AddPartitionSpec implements MetadataUpdate {\n    private final UnboundPartitionSpec spec;\n\n    public AddPartitionSpec(PartitionSpec spec) {\n      this(spec.toUnbound());\n    }\n\n    public AddPartitionSpec(UnboundPartitionSpec spec) {\n      this.spec = spec;\n    }\n\n    public UnboundPartitionSpec spec() {\n      return spec;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.addPartitionSpec(spec);\n    }\n  }\n\n  class SetDefaultPartitionSpec implements MetadataUpdate {\n    private final int specId;\n\n    public SetDefaultPartitionSpec(int specId) {\n      this.specId = specId;\n    }\n\n    public int specId() {\n      return specId;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setDefaultPartitionSpec(specId);\n    }\n  }\n\n  class RemovePartitionSpecs implements MetadataUpdate {\n    private final Set<Integer> specIds;\n\n    public RemovePartitionSpecs(Set<Integer> specIds) {\n      this.specIds = specIds;\n    }\n\n    public Set<Integer> specIds() {\n      return specIds;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removeSpecs(specIds);\n    }\n  }\n\n  class RemoveSchemas implements MetadataUpdate {\n    private final Set<Integer> schemaIds;\n\n    public RemoveSchemas(Set<Integer> schemaIds) {\n      this.schemaIds = schemaIds;\n    }\n\n    public Set<Integer> schemaIds() {\n      return schemaIds;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removeSchemas(schemaIds);\n    }\n  }\n\n  class AddSortOrder implements MetadataUpdate {\n    private final UnboundSortOrder sortOrder;\n\n    public AddSortOrder(SortOrder sortOrder) {\n      this(sortOrder.toUnbound());\n    }\n\n    public AddSortOrder(UnboundSortOrder sortOrder) {\n      this.sortOrder = sortOrder;\n    }\n\n    public UnboundSortOrder sortOrder() {\n      return sortOrder;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.addSortOrder(sortOrder);\n    }\n  }\n\n  class SetDefaultSortOrder implements MetadataUpdate {\n    private final int sortOrderId;\n\n    public SetDefaultSortOrder(int sortOrderId) {\n      this.sortOrderId = sortOrderId;\n    }\n\n    public int sortOrderId() {\n      return sortOrderId;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setDefaultSortOrder(sortOrderId);\n    }\n  }\n\n  class SetStatistics implements MetadataUpdate {\n    private final StatisticsFile statisticsFile;\n\n    public SetStatistics(StatisticsFile statisticsFile) {\n      this.statisticsFile = statisticsFile;\n    }\n\n    public long snapshotId() {\n      return statisticsFile.snapshotId();\n    }\n\n    public StatisticsFile statisticsFile() {\n      return statisticsFile;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setStatistics(statisticsFile);\n    }\n  }\n\n  class RemoveStatistics implements MetadataUpdate {\n    private final long snapshotId;\n\n    public RemoveStatistics(long snapshotId) {\n      this.snapshotId = snapshotId;\n    }\n\n    public long snapshotId() {\n      return snapshotId;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removeStatistics(snapshotId);\n    }\n  }\n\n  class SetPartitionStatistics implements MetadataUpdate {\n    private final PartitionStatisticsFile partitionStatisticsFile;\n\n    public SetPartitionStatistics(PartitionStatisticsFile partitionStatisticsFile) {\n      this.partitionStatisticsFile = partitionStatisticsFile;\n    }\n\n    public long snapshotId() {\n      return partitionStatisticsFile.snapshotId();\n    }\n\n    public PartitionStatisticsFile partitionStatisticsFile() {\n      return partitionStatisticsFile;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setPartitionStatistics(partitionStatisticsFile);\n    }\n  }\n\n  class RemovePartitionStatistics implements MetadataUpdate {\n    private final long snapshotId;\n\n    public RemovePartitionStatistics(long snapshotId) {\n      this.snapshotId = snapshotId;\n    }\n\n    public long snapshotId() {\n      return snapshotId;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removePartitionStatistics(snapshotId);\n    }\n  }\n\n  class AddSnapshot implements MetadataUpdate {\n    private final Snapshot snapshot;\n\n    public AddSnapshot(Snapshot snapshot) {\n      this.snapshot = snapshot;\n    }\n\n    public Snapshot snapshot() {\n      return snapshot;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.addSnapshot(snapshot);\n    }\n  }\n\n  class RemoveSnapshots implements MetadataUpdate {\n    private final Set<Long> snapshotIds;\n\n    public RemoveSnapshots(long snapshotId) {\n      this.snapshotIds = ImmutableSet.of(snapshotId);\n    }\n\n    public RemoveSnapshots(Set<Long> snapshotIds) {\n      this.snapshotIds = snapshotIds;\n    }\n\n    public Set<Long> snapshotIds() {\n      return snapshotIds;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removeSnapshots(snapshotIds);\n    }\n  }\n\n  class RemoveSnapshotRef implements MetadataUpdate {\n    private final String refName;\n\n    public RemoveSnapshotRef(String refName) {\n      this.refName = refName;\n    }\n\n    public String name() {\n      return refName;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removeRef(refName);\n    }\n  }\n\n  class SetSnapshotRef implements MetadataUpdate {\n    private final String refName;\n    private final Long snapshotId;\n    private final SnapshotRefType type;\n    private final Integer minSnapshotsToKeep;\n    private final Long maxSnapshotAgeMs;\n    private final Long maxRefAgeMs;\n\n    public SetSnapshotRef(\n        String refName,\n        Long snapshotId,\n        SnapshotRefType type,\n        Integer minSnapshotsToKeep,\n        Long maxSnapshotAgeMs,\n        Long maxRefAgeMs) {\n      this.refName = refName;\n      this.snapshotId = snapshotId;\n      this.type = type;\n      this.minSnapshotsToKeep = minSnapshotsToKeep;\n      this.maxSnapshotAgeMs = maxSnapshotAgeMs;\n      this.maxRefAgeMs = maxRefAgeMs;\n    }\n\n    public String name() {\n      return refName;\n    }\n\n    public String type() {\n      return type.name().toLowerCase(Locale.ROOT);\n    }\n\n    public long snapshotId() {\n      return snapshotId;\n    }\n\n    public Integer minSnapshotsToKeep() {\n      return minSnapshotsToKeep;\n    }\n\n    public Long maxSnapshotAgeMs() {\n      return maxSnapshotAgeMs;\n    }\n\n    public Long maxRefAgeMs() {\n      return maxRefAgeMs;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      SnapshotRef ref =\n          SnapshotRef.builderFor(snapshotId, type)\n              .minSnapshotsToKeep(minSnapshotsToKeep)\n              .maxSnapshotAgeMs(maxSnapshotAgeMs)\n              .maxRefAgeMs(maxRefAgeMs)\n              .build();\n      metadataBuilder.setRef(refName, ref);\n    }\n  }\n\n  class SetProperties implements MetadataUpdate {\n    private final Map<String, String> updated;\n\n    public SetProperties(Map<String, String> updated) {\n      this.updated = updated;\n    }\n\n    public Map<String, String> updated() {\n      return updated;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setProperties(updated);\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.setProperties(updated);\n    }\n  }\n\n  class RemoveProperties implements MetadataUpdate {\n    private final Set<String> removed;\n\n    public RemoveProperties(Set<String> removed) {\n      this.removed = removed;\n    }\n\n    public Set<String> removed() {\n      return removed;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.removeProperties(removed);\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.removeProperties(removed);\n    }\n  }\n\n  class SetLocation implements MetadataUpdate {\n    private final String location;\n\n    public SetLocation(String location) {\n      this.location = location;\n    }\n\n    public String location() {\n      return location;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder metadataBuilder) {\n      metadataBuilder.setLocation(location);\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.setLocation(location);\n    }\n  }\n\n  class AddViewVersion implements MetadataUpdate {\n    private final ViewVersion viewVersion;\n\n    public AddViewVersion(ViewVersion viewVersion) {\n      this.viewVersion = viewVersion;\n    }\n\n    public ViewVersion viewVersion() {\n      return viewVersion;\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.addVersion(viewVersion);\n    }\n  }\n\n  class SetCurrentViewVersion implements MetadataUpdate {\n    private final int versionId;\n\n    public SetCurrentViewVersion(int versionId) {\n      this.versionId = versionId;\n    }\n\n    public int versionId() {\n      return versionId;\n    }\n\n    @Override\n    public void applyTo(ViewMetadata.Builder viewMetadataBuilder) {\n      viewMetadataBuilder.setCurrentVersionId(versionId);\n    }\n  }\n\n  class AddEncryptionKey implements MetadataUpdate {\n    private final EncryptedKey key;\n\n    public AddEncryptionKey(EncryptedKey key) {\n      this.key = key;\n    }\n\n    public EncryptedKey key() {\n      return key;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder builder) {\n      builder.addEncryptionKey(key);\n    }\n  }\n\n  class RemoveEncryptionKey implements MetadataUpdate {\n    private final String keyId;\n\n    public RemoveEncryptionKey(String keyId) {\n      this.keyId = keyId;\n    }\n\n    public String keyId() {\n      return keyId;\n    }\n\n    @Override\n    public void applyTo(TableMetadata.Builder builder) {\n      builder.removeEncryptionKey(keyId);\n    }\n  }\n}\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/PartitionSpec.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\npackage org.apache.iceberg;\n\nimport java.io.Serializable;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.util.AbstractMap;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport org.apache.iceberg.exceptions.ValidationException;\nimport org.apache.iceberg.relocated.com.google.common.base.Preconditions;\nimport org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;\nimport org.apache.iceberg.relocated.com.google.common.collect.ListMultimap;\nimport org.apache.iceberg.relocated.com.google.common.collect.Lists;\nimport org.apache.iceberg.relocated.com.google.common.collect.Maps;\nimport org.apache.iceberg.relocated.com.google.common.collect.Multimaps;\nimport org.apache.iceberg.relocated.com.google.common.collect.Sets;\nimport org.apache.iceberg.transforms.Transform;\nimport org.apache.iceberg.transforms.Transforms;\nimport org.apache.iceberg.transforms.UnknownTransform;\nimport org.apache.iceberg.types.Type;\nimport org.apache.iceberg.types.TypeUtil;\nimport org.apache.iceberg.types.Types;\nimport org.apache.iceberg.types.Types.StructType;\n\n/**\n * Represents how to produce partition data for a table.\n *\n * <p>Partition data is produced by transforming columns in a table. Each column transform is\n * represented by a named {@link PartitionField}.\n *\n * This class is directly copied from iceberg repo 1.10.0; The only change is this sets checkConflicts\n * to false by default for partition spec converted from Delta to honor the field id assigned by Delta\n */\npublic class PartitionSpec implements Serializable {\n  // IDs for partition fields start at 1000\n  private static final int PARTITION_DATA_ID_START = 1000;\n\n  private final Schema schema;\n\n  // this is ordered so that DataFile has a consistent schema\n  private final int specId;\n  private final PartitionField[] fields;\n  private transient volatile ListMultimap<Integer, PartitionField> fieldsBySourceId = null;\n  private transient volatile Class<?>[] lazyJavaClasses = null;\n  private transient volatile StructType lazyPartitionType = null;\n  private transient volatile StructType lazyRawPartitionType = null;\n  private transient volatile List<PartitionField> fieldList = null;\n  private final int lastAssignedFieldId;\n\n  private PartitionSpec(\n      Schema schema, int specId, List<PartitionField> fields, int lastAssignedFieldId) {\n    this.schema = schema;\n    this.specId = specId;\n    this.fields = fields.toArray(new PartitionField[0]);\n    this.lastAssignedFieldId = lastAssignedFieldId;\n  }\n\n  /** Returns the {@link Schema} for this spec. */\n  public Schema schema() {\n    return schema;\n  }\n\n  /** Returns the ID of this spec. */\n  public int specId() {\n    return specId;\n  }\n\n  /** Returns the list of {@link PartitionField partition fields} for this spec. */\n  public List<PartitionField> fields() {\n    return lazyFieldList();\n  }\n\n  public boolean isPartitioned() {\n    return fields.length > 0 && fields().stream().anyMatch(f -> !f.transform().isVoid());\n  }\n\n  public boolean isUnpartitioned() {\n    return !isPartitioned();\n  }\n\n  int lastAssignedFieldId() {\n    return lastAssignedFieldId;\n  }\n\n  public UnboundPartitionSpec toUnbound() {\n    UnboundPartitionSpec.Builder builder = UnboundPartitionSpec.builder().withSpecId(specId);\n\n    for (PartitionField field : fields) {\n      builder.addField(\n          field.transform().toString(), field.sourceId(), field.fieldId(), field.name());\n    }\n\n    return builder.build();\n  }\n\n  /**\n   * Returns the {@link PartitionField field} that partitions the given source field\n   *\n   * @param fieldId a field id from the source schema\n   * @return the {@link PartitionField field} that partitions the given source field\n   */\n  public List<PartitionField> getFieldsBySourceId(int fieldId) {\n    return lazyFieldsBySourceId().get(fieldId);\n  }\n\n  /** Returns a {@link StructType} for partition data defined by this spec. */\n  public StructType partitionType() {\n    if (lazyPartitionType == null) {\n      synchronized (this) {\n        if (lazyPartitionType == null) {\n          List<Types.NestedField> structFields = Lists.newArrayListWithExpectedSize(fields.length);\n\n          for (PartitionField field : fields) {\n            Type sourceType = schema.findType(field.sourceId());\n            Type resultType = field.transform().getResultType(sourceType);\n\n            // When the source field has been dropped we cannot determine the type\n            if (sourceType == null) {\n              resultType = Types.UnknownType.get();\n            }\n\n            structFields.add(Types.NestedField.optional(field.fieldId(), field.name(), resultType));\n          }\n\n          this.lazyPartitionType = Types.StructType.of(structFields);\n        }\n      }\n    }\n\n    return lazyPartitionType;\n  }\n\n  /**\n   * Returns a struct matching partition information as written into manifest files. See {@link\n   * #partitionType()} for a struct with field ID's potentially re-assigned to avoid conflict.\n   */\n  public StructType rawPartitionType() {\n    if (schema.idsToOriginal().isEmpty()) {\n      // not re-assigned.\n      return partitionType();\n    }\n    if (lazyRawPartitionType == null) {\n      synchronized (this) {\n        if (lazyRawPartitionType == null) {\n          this.lazyRawPartitionType =\n              StructType.of(\n                  partitionType().fields().stream()\n                      .map(f -> f.withFieldId(schema.idsToOriginal().get(f.fieldId())))\n                      .collect(Collectors.toList()));\n        }\n      }\n    }\n\n    return lazyRawPartitionType;\n  }\n\n  public Class<?>[] javaClasses() {\n    if (lazyJavaClasses == null) {\n      synchronized (this) {\n        if (lazyJavaClasses == null) {\n          Class<?>[] classes = new Class<?>[fields.length];\n          for (int i = 0; i < fields.length; i += 1) {\n            PartitionField field = fields[i];\n            if (field.transform() instanceof UnknownTransform) {\n              classes[i] = Object.class;\n            } else {\n              Type sourceType = schema.findType(field.sourceId());\n              Type result = field.transform().getResultType(sourceType);\n              classes[i] = result.typeId().javaClass();\n            }\n          }\n\n          this.lazyJavaClasses = classes;\n        }\n      }\n    }\n\n    return lazyJavaClasses;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private <T> T get(StructLike data, int pos, Class<?> javaClass) {\n    return data.get(pos, (Class<T>) javaClass);\n  }\n\n  private String escape(String string) {\n    try {\n      return URLEncoder.encode(string, \"UTF-8\");\n    } catch (UnsupportedEncodingException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  public String partitionToPath(StructLike data) {\n    StringBuilder sb = new StringBuilder();\n    Class<?>[] javaClasses = javaClasses();\n    List<Types.NestedField> outputFields = partitionType().fields();\n    for (int i = 0; i < javaClasses.length; i += 1) {\n      PartitionField field = fields[i];\n      Type type = outputFields.get(i).type();\n      String valueString = field.transform().toHumanString(type, get(data, i, javaClasses[i]));\n\n      if (i > 0) {\n        sb.append(\"/\");\n      }\n      sb.append(escape(field.name())).append(\"=\").append(escape(valueString));\n    }\n    return sb.toString();\n  }\n\n  /**\n   * Returns true if this spec is equivalent to the other, with partition field ids ignored. That\n   * is, if both specs have the same number of fields, field order, field name, source columns, and\n   * transforms.\n   *\n   * @param other another PartitionSpec\n   * @return true if the specs have the same fields, source columns, and transforms.\n   */\n  public boolean compatibleWith(PartitionSpec other) {\n    if (equals(other)) {\n      return true;\n    }\n\n    if (fields.length != other.fields.length) {\n      return false;\n    }\n\n    for (int i = 0; i < fields.length; i += 1) {\n      PartitionField thisField = fields[i];\n      PartitionField thatField = other.fields[i];\n      if (thisField.sourceId() != thatField.sourceId()\n          || !thisField.transform().toString().equals(thatField.transform().toString())\n          || !thisField.name().equals(thatField.name())) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  @Override\n  public boolean equals(Object other) {\n    if (this == other) {\n      return true;\n    } else if (!(other instanceof PartitionSpec)) {\n      return false;\n    }\n\n    PartitionSpec that = (PartitionSpec) other;\n    if (this.specId != that.specId) {\n      return false;\n    }\n    return Arrays.equals(fields, that.fields);\n  }\n\n  @Override\n  public int hashCode() {\n    return 31 * Integer.hashCode(specId) + Arrays.hashCode(fields);\n  }\n\n  private List<PartitionField> lazyFieldList() {\n    if (fieldList == null) {\n      synchronized (this) {\n        if (fieldList == null) {\n          this.fieldList = ImmutableList.copyOf(fields);\n        }\n      }\n    }\n    return fieldList;\n  }\n\n  private ListMultimap<Integer, PartitionField> lazyFieldsBySourceId() {\n    if (fieldsBySourceId == null) {\n      synchronized (this) {\n        if (fieldsBySourceId == null) {\n          ListMultimap<Integer, PartitionField> multiMap =\n              Multimaps.newListMultimap(\n                  Maps.newHashMap(), () -> Lists.newArrayListWithCapacity(fields.length));\n          for (PartitionField field : fields) {\n            multiMap.put(field.sourceId(), field);\n          }\n          this.fieldsBySourceId = multiMap;\n        }\n      }\n    }\n\n    return fieldsBySourceId;\n  }\n\n  /**\n   * Returns the source field ids for identity partitions.\n   *\n   * @return a set of source ids for the identity partitions.\n   */\n  public Set<Integer> identitySourceIds() {\n    Set<Integer> sourceIds = Sets.newHashSet();\n    for (PartitionField field : fields()) {\n      if (\"identity\".equals(field.transform().toString())) {\n        sourceIds.add(field.sourceId());\n      }\n    }\n\n    return sourceIds;\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"[\");\n    for (PartitionField field : fields) {\n      sb.append(\"\\n\");\n      sb.append(\"  \").append(field);\n    }\n    if (fields.length > 0) {\n      sb.append(\"\\n\");\n    }\n    sb.append(\"]\");\n    return sb.toString();\n  }\n\n  private static final PartitionSpec UNPARTITIONED_SPEC =\n      new PartitionSpec(new Schema(), 0, ImmutableList.of(), unpartitionedLastAssignedId());\n\n  /**\n   * Returns a spec for unpartitioned tables.\n   *\n   * @return a partition spec with no partitions\n   */\n  public static PartitionSpec unpartitioned() {\n    return UNPARTITIONED_SPEC;\n  }\n\n  private static int unpartitionedLastAssignedId() {\n    return PARTITION_DATA_ID_START - 1;\n  }\n\n  /**\n   * Creates a new {@link Builder partition spec builder} for the given {@link Schema}.\n   *\n   * @param schema a schema\n   * @return a partition spec builder for the given schema\n   */\n  public static Builder builderFor(Schema schema) {\n    return new Builder(schema);\n  }\n\n  /**\n   * Used to create valid {@link PartitionSpec partition specs}.\n   *\n   * <p>Call {@link #builderFor(Schema)} to create a new builder.\n   */\n  public static class Builder {\n    private final Schema schema;\n    private final List<PartitionField> fields = Lists.newArrayList();\n    private final Set<String> partitionNames = Sets.newHashSet();\n    private final Map<Map.Entry<Integer, String>, PartitionField> dedupFields = Maps.newHashMap();\n    private int specId = 0;\n    private final AtomicInteger lastAssignedFieldId =\n        new AtomicInteger(unpartitionedLastAssignedId());\n    // check if there are conflicts between partition and schema field name\n    // HACK-HACK: disable checkConflicts for partition spec converted from Delta\n    // to honor the field id assigned by Delta\n    private boolean checkConflicts = false;\n    private boolean caseSensitive = true;\n\n    private Builder(Schema schema) {\n      this.schema = schema;\n    }\n\n    private int nextFieldId() {\n      return lastAssignedFieldId.incrementAndGet();\n    }\n\n    private void checkAndAddPartitionName(String name) {\n      checkAndAddPartitionName(name, null);\n    }\n\n    Builder checkConflicts(boolean check) {\n      checkConflicts = check;\n      return this;\n    }\n\n    private void checkAndAddPartitionName(String name, Integer sourceColumnId) {\n      Types.NestedField schemaField =\n          this.caseSensitive ? schema.findField(name) : schema.caseInsensitiveFindField(name);\n      if (checkConflicts) {\n        if (sourceColumnId != null) {\n          // for identity transform case we allow conflicts between partition and schema field name\n          // as\n          //   long as they are sourced from the same schema field\n          Preconditions.checkArgument(\n              schemaField == null || schemaField.fieldId() == sourceColumnId,\n              \"Cannot create identity partition sourced from different field in schema: %s\",\n              name);\n        } else {\n          // for all other transforms we don't allow conflicts between partition name and schema\n          // field name\n          Preconditions.checkArgument(\n              schemaField == null,\n              \"Cannot create partition from name that exists in schema: %s\",\n              name);\n        }\n      }\n      Preconditions.checkArgument(!name.isEmpty(), \"Cannot use empty partition name: %s\", name);\n      Preconditions.checkArgument(\n          !partitionNames.contains(name), \"Cannot use partition name more than once: %s\", name);\n      partitionNames.add(name);\n    }\n\n    private void checkForRedundantPartitions(PartitionField field) {\n      Map.Entry<Integer, String> dedupKey =\n          new AbstractMap.SimpleEntry<>(field.sourceId(), field.transform().dedupName());\n      PartitionField partitionField = dedupFields.get(dedupKey);\n      Preconditions.checkArgument(\n          partitionField == null,\n          \"Cannot add redundant partition: %s conflicts with %s\",\n          partitionField,\n          field);\n      dedupFields.put(dedupKey, field);\n    }\n\n    public Builder caseSensitive(boolean sensitive) {\n      this.caseSensitive = sensitive;\n      return this;\n    }\n\n    public Builder withSpecId(int newSpecId) {\n      this.specId = newSpecId;\n      return this;\n    }\n\n    private Types.NestedField findSourceColumn(String sourceName) {\n      Types.NestedField sourceColumn =\n          this.caseSensitive\n              ? schema.findField(sourceName)\n              : schema.caseInsensitiveFindField(sourceName);\n      Preconditions.checkArgument(\n          sourceColumn != null, \"Cannot find source column: %s\", sourceName);\n      return sourceColumn;\n    }\n\n    public Builder identity(String sourceName, String targetName) {\n      return identity(findSourceColumn(sourceName), targetName);\n    }\n\n    private Builder identity(Types.NestedField sourceColumn, String targetName) {\n      checkAndAddPartitionName(targetName, sourceColumn.fieldId());\n      PartitionField field =\n          new PartitionField(\n              sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.identity());\n      checkForRedundantPartitions(field);\n      fields.add(field);\n      return this;\n    }\n\n    public Builder identity(String sourceName) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      return identity(sourceColumn, schema.findColumnName(sourceColumn.fieldId()));\n    }\n\n    public Builder year(String sourceName, String targetName) {\n      return year(findSourceColumn(sourceName), targetName);\n    }\n\n    private Builder year(Types.NestedField sourceColumn, String targetName) {\n      checkAndAddPartitionName(targetName);\n      PartitionField field =\n          new PartitionField(sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.year());\n      checkForRedundantPartitions(field);\n      fields.add(field);\n      return this;\n    }\n\n    public Builder year(String sourceName) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return year(sourceColumn, columnName + \"_year\");\n    }\n\n    public Builder month(String sourceName, String targetName) {\n      return month(findSourceColumn(sourceName), targetName);\n    }\n\n    private Builder month(Types.NestedField sourceColumn, String targetName) {\n      checkAndAddPartitionName(targetName);\n      PartitionField field =\n          new PartitionField(sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.month());\n      checkForRedundantPartitions(field);\n      fields.add(field);\n      return this;\n    }\n\n    public Builder month(String sourceName) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return month(sourceColumn, columnName + \"_month\");\n    }\n\n    public Builder day(String sourceName, String targetName) {\n      return day(findSourceColumn(sourceName), targetName);\n    }\n\n    private Builder day(Types.NestedField sourceColumn, String targetName) {\n      checkAndAddPartitionName(targetName);\n      PartitionField field =\n          new PartitionField(sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.day());\n      checkForRedundantPartitions(field);\n      fields.add(field);\n      return this;\n    }\n\n    public Builder day(String sourceName) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return day(sourceColumn, columnName + \"_day\");\n    }\n\n    public Builder hour(String sourceName, String targetName) {\n      return hour(findSourceColumn(sourceName), targetName);\n    }\n\n    private Builder hour(Types.NestedField sourceColumn, String targetName) {\n      checkAndAddPartitionName(targetName);\n      PartitionField field =\n          new PartitionField(sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.hour());\n      checkForRedundantPartitions(field);\n      fields.add(field);\n      return this;\n    }\n\n    public Builder hour(String sourceName) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return hour(sourceColumn, columnName + \"_hour\");\n    }\n\n    public Builder bucket(String sourceName, int numBuckets, String targetName) {\n      return bucket(findSourceColumn(sourceName), numBuckets, targetName);\n    }\n\n    private Builder bucket(Types.NestedField sourceColumn, int numBuckets, String targetName) {\n      checkAndAddPartitionName(targetName);\n      fields.add(\n          new PartitionField(\n              sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.bucket(numBuckets)));\n      return this;\n    }\n\n    public Builder bucket(String sourceName, int numBuckets) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return bucket(sourceColumn, numBuckets, columnName + \"_bucket\");\n    }\n\n    public Builder truncate(String sourceName, int width, String targetName) {\n      return truncate(findSourceColumn(sourceName), width, targetName);\n    }\n\n    private Builder truncate(Types.NestedField sourceColumn, int width, String targetName) {\n      checkAndAddPartitionName(targetName);\n      fields.add(\n          new PartitionField(\n              sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.truncate(width)));\n      return this;\n    }\n\n    public Builder truncate(String sourceName, int width) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return truncate(sourceColumn, width, columnName + \"_trunc\");\n    }\n\n    public Builder alwaysNull(String sourceName, String targetName) {\n      return alwaysNull(findSourceColumn(sourceName), targetName);\n    }\n\n    private Builder alwaysNull(Types.NestedField sourceColumn, String targetName) {\n      checkAndAddPartitionName(\n          targetName, sourceColumn.fieldId()); // can duplicate a source column name\n      fields.add(\n          new PartitionField(\n              sourceColumn.fieldId(), nextFieldId(), targetName, Transforms.alwaysNull()));\n      return this;\n    }\n\n    public Builder alwaysNull(String sourceName) {\n      Types.NestedField sourceColumn = findSourceColumn(sourceName);\n      String columnName = schema.findColumnName(sourceColumn.fieldId());\n      return alwaysNull(sourceColumn, columnName + \"_null\");\n    }\n\n    // add a partition field with an auto-increment partition field id starting from\n    // PARTITION_DATA_ID_START\n    Builder add(int sourceId, String name, Transform<?, ?> transform) {\n      return add(sourceId, nextFieldId(), name, transform);\n    }\n\n    Builder add(int sourceId, int fieldId, String name, Transform<?, ?> transform) {\n      checkAndAddPartitionName(name, sourceId);\n      fields.add(new PartitionField(sourceId, fieldId, name, transform));\n      lastAssignedFieldId.getAndAccumulate(fieldId, Math::max);\n      return this;\n    }\n\n    public PartitionSpec build() {\n      return build(false);\n    }\n\n    public PartitionSpec build(boolean allowMissingFields) {\n      PartitionSpec spec = buildUnchecked();\n      checkCompatibility(spec, schema, allowMissingFields);\n      return spec;\n    }\n\n    PartitionSpec buildUnchecked() {\n      return new PartitionSpec(schema, specId, fields, lastAssignedFieldId.get());\n    }\n  }\n\n  static void checkCompatibility(PartitionSpec spec, Schema schema) {\n    checkCompatibility(spec, schema, false);\n  }\n\n  static void checkCompatibility(PartitionSpec spec, Schema schema, boolean allowMissingFields) {\n    final Map<Integer, Integer> parents = TypeUtil.indexParents(schema.asStruct());\n    for (PartitionField field : spec.fields) {\n      Type sourceType = schema.findType(field.sourceId());\n      Transform<?, ?> transform = field.transform();\n      // In the case the underlying field is dropped, we cannot check if they are compatible\n      if (allowMissingFields && sourceType == null) {\n        continue;\n      }\n      // In the case of a Version 1 partition-spec field gets deleted,\n      // it is replaced with a void transform, see:\n      // https://iceberg.apache.org/spec/#partition-transforms\n      // We don't care about the source type since a VoidTransform is always compatible and skip the\n      // checks\n      if (!transform.equals(Transforms.alwaysNull())) {\n        ValidationException.check(\n            sourceType != null, \"Cannot find source column for partition field: %s\", field);\n        ValidationException.check(\n            sourceType.isPrimitiveType(),\n            \"Cannot partition by non-primitive source field: %s\",\n            sourceType);\n        ValidationException.check(\n            transform.canTransform(sourceType),\n            \"Invalid source type %s for transform: %s\",\n            sourceType,\n            transform);\n        // The only valid parent types for a PartitionField are StructTypes. This must be checked\n        // recursively.\n        Integer parentId = parents.get(field.sourceId());\n        while (parentId != null) {\n          Type parentType = schema.findType(parentId);\n          ValidationException.check(\n              parentType.isStructType(), \"Invalid partition field parent: %s\", parentType);\n          parentId = parents.get(parentId);\n        }\n      }\n    }\n  }\n\n  static boolean hasSequentialIds(PartitionSpec spec) {\n    for (int i = 0; i < spec.fields.length; i += 1) {\n      if (spec.fields[i].fieldId() != PARTITION_DATA_ID_START + i) {\n        return false;\n      }\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/TableMetadata.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\npackage org.apache.iceberg;\n\nimport java.io.Serializable;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.iceberg.encryption.EncryptedKey;\nimport org.apache.iceberg.exceptions.ValidationException;\nimport org.apache.iceberg.relocated.com.google.common.base.MoreObjects;\nimport org.apache.iceberg.relocated.com.google.common.base.Objects;\nimport org.apache.iceberg.relocated.com.google.common.base.Preconditions;\nimport org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;\nimport org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;\nimport org.apache.iceberg.relocated.com.google.common.collect.Iterables;\nimport org.apache.iceberg.relocated.com.google.common.collect.Lists;\nimport org.apache.iceberg.relocated.com.google.common.collect.Maps;\nimport org.apache.iceberg.relocated.com.google.common.collect.Sets;\nimport org.apache.iceberg.transforms.Transforms;\nimport org.apache.iceberg.types.TypeUtil;\nimport org.apache.iceberg.util.LocationUtil;\nimport org.apache.iceberg.util.Pair;\nimport org.apache.iceberg.util.PartitionUtil;\nimport org.apache.iceberg.util.PropertyUtil;\nimport org.apache.iceberg.util.SerializableSupplier;\n\n/**\n * This class is directly copied from iceberg repo 1.10.0 with following changes\n * Change: L602 add back the deprecated API\n *         public TableMetadata updateSchema(Schema newSchema, int newLastColumnId)\n *         L848 add the sql conf check to bypass snap sequenceNumber check\n *         L1048 add back the deprecated API\n *         public Builder withNextRowId(Long newRowId)\n *         L1110 add the sql conf check to bypass downgrade check\n *         L1174 add back the deprecated API\n *         public Builder addSchema(Schema schema, int newLastColumnId)\n */\n\n/** Metadata for a table. */\npublic class TableMetadata implements Serializable {\n  static final long INITIAL_SEQUENCE_NUMBER = 0;\n  static final long INVALID_SEQUENCE_NUMBER = -1;\n  static final int DEFAULT_TABLE_FORMAT_VERSION = 2;\n  static final int SUPPORTED_TABLE_FORMAT_VERSION = 4;\n  static final int MIN_FORMAT_VERSION_ROW_LINEAGE = 3;\n  static final int INITIAL_SPEC_ID = 0;\n  static final int INITIAL_SORT_ORDER_ID = 1;\n  static final int INITIAL_SCHEMA_ID = 0;\n  static final int INITIAL_ROW_ID = 0;\n\n  private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);\n\n  public static TableMetadata newTableMetadata(\n      Schema schema,\n      PartitionSpec spec,\n      SortOrder sortOrder,\n      String location,\n      Map<String, String> properties) {\n    int formatVersion =\n        PropertyUtil.propertyAsInt(\n            properties, TableProperties.FORMAT_VERSION, DEFAULT_TABLE_FORMAT_VERSION);\n    return newTableMetadata(\n        schema, spec, sortOrder, location, persistedProperties(properties), formatVersion);\n  }\n\n  public static TableMetadata newTableMetadata(\n      Schema schema, PartitionSpec spec, String location, Map<String, String> properties) {\n    return newTableMetadata(schema, spec, SortOrder.unsorted(), location, properties);\n  }\n\n  private static Map<String, String> unreservedProperties(Map<String, String> rawProperties) {\n    return rawProperties.entrySet().stream()\n        .filter(e -> !TableProperties.RESERVED_PROPERTIES.contains(e.getKey()))\n        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n  }\n\n  private static Map<String, String> persistedProperties(Map<String, String> rawProperties) {\n    Map<String, String> persistedProperties = Maps.newHashMap();\n\n    // explicitly set defaults that apply only to new tables\n    persistedProperties.put(\n        TableProperties.PARQUET_COMPRESSION,\n        TableProperties.PARQUET_COMPRESSION_DEFAULT_SINCE_1_4_0);\n\n    rawProperties.entrySet().stream()\n        .filter(entry -> !TableProperties.RESERVED_PROPERTIES.contains(entry.getKey()))\n        .forEach(entry -> persistedProperties.put(entry.getKey(), entry.getValue()));\n\n    return persistedProperties;\n  }\n\n  static TableMetadata newTableMetadata(\n      Schema schema,\n      PartitionSpec spec,\n      SortOrder sortOrder,\n      String location,\n      Map<String, String> properties,\n      int formatVersion) {\n    Preconditions.checkArgument(\n        properties.keySet().stream().noneMatch(TableProperties.RESERVED_PROPERTIES::contains),\n        \"Table properties should not contain reserved properties, but got %s\",\n        properties);\n\n    // reassign all column ids to ensure consistency\n    AtomicInteger lastColumnId = new AtomicInteger(0);\n    Schema freshSchema =\n        TypeUtil.assignFreshIds(INITIAL_SCHEMA_ID, schema, lastColumnId::incrementAndGet);\n\n    // rebuild the partition spec using the new column ids\n    PartitionSpec.Builder specBuilder =\n        PartitionSpec.builderFor(freshSchema).withSpecId(INITIAL_SPEC_ID);\n    for (PartitionField field : spec.fields()) {\n      // look up the name of the source field in the old schema to get the new schema's id\n      String sourceName = schema.findColumnName(field.sourceId());\n      // reassign all partition fields with fresh partition field Ids to ensure consistency\n      specBuilder.add(freshSchema.findField(sourceName).fieldId(), field.name(), field.transform());\n    }\n    PartitionSpec freshSpec = specBuilder.build();\n\n    // rebuild the sort order using the new column ids\n    int freshSortOrderId = sortOrder.isUnsorted() ? sortOrder.orderId() : INITIAL_SORT_ORDER_ID;\n    SortOrder freshSortOrder = freshSortOrder(freshSortOrderId, freshSchema, sortOrder);\n\n    // Validate the metrics configuration. Note: we only do this on new tables to we don't\n    // break existing tables.\n    MetricsConfig.fromProperties(properties).validateReferencedColumns(schema);\n\n    PropertyUtil.validateCommitProperties(properties);\n\n    return new Builder()\n        .setInitialFormatVersion(formatVersion)\n        .setCurrentSchema(freshSchema, lastColumnId.get())\n        .setDefaultPartitionSpec(freshSpec)\n        .setDefaultSortOrder(freshSortOrder)\n        .setLocation(location)\n        .setProperties(properties)\n        .build();\n  }\n\n  public static class SnapshotLogEntry implements HistoryEntry {\n    private final long timestampMillis;\n    private final long snapshotId;\n\n    SnapshotLogEntry(long timestampMillis, long snapshotId) {\n      this.timestampMillis = timestampMillis;\n      this.snapshotId = snapshotId;\n    }\n\n    @Override\n    public long timestampMillis() {\n      return timestampMillis;\n    }\n\n    @Override\n    public long snapshotId() {\n      return snapshotId;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n      if (this == other) {\n        return true;\n      } else if (!(other instanceof SnapshotLogEntry)) {\n        return false;\n      }\n      SnapshotLogEntry that = (SnapshotLogEntry) other;\n      return timestampMillis == that.timestampMillis && snapshotId == that.snapshotId;\n    }\n\n    @Override\n    public int hashCode() {\n      return Objects.hashCode(timestampMillis, snapshotId);\n    }\n\n    @Override\n    public String toString() {\n      return MoreObjects.toStringHelper(this)\n          .add(\"timestampMillis\", timestampMillis)\n          .add(\"snapshotId\", snapshotId)\n          .toString();\n    }\n  }\n\n  public static class MetadataLogEntry {\n    private final long timestampMillis;\n    private final String file;\n\n    MetadataLogEntry(long timestampMillis, String file) {\n      this.timestampMillis = timestampMillis;\n      this.file = file;\n    }\n\n    public long timestampMillis() {\n      return timestampMillis;\n    }\n\n    public String file() {\n      return file;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n      if (this == other) {\n        return true;\n      } else if (!(other instanceof MetadataLogEntry)) {\n        return false;\n      }\n      MetadataLogEntry that = (MetadataLogEntry) other;\n      return timestampMillis == that.timestampMillis && java.util.Objects.equals(file, that.file);\n    }\n\n    @Override\n    public int hashCode() {\n      return Objects.hashCode(timestampMillis, file);\n    }\n\n    @Override\n    public String toString() {\n      return MoreObjects.toStringHelper(this)\n          .add(\"timestampMillis\", timestampMillis)\n          .add(\"file\", file)\n          .toString();\n    }\n  }\n\n  // stored metadata\n  private final String metadataFileLocation;\n  private final int formatVersion;\n  private final String uuid;\n  private final String location;\n  private final long lastSequenceNumber;\n  private final long lastUpdatedMillis;\n  private final int lastColumnId;\n  private final int currentSchemaId;\n  private final List<Schema> schemas;\n  private final int defaultSpecId;\n  private final List<PartitionSpec> specs;\n  private final int lastAssignedPartitionId;\n  private final int defaultSortOrderId;\n  private final List<SortOrder> sortOrders;\n  private final Map<String, String> properties;\n  private final long currentSnapshotId;\n  private final Map<Integer, Schema> schemasById;\n  private final Map<Integer, PartitionSpec> specsById;\n  private final Map<Integer, SortOrder> sortOrdersById;\n  private final List<HistoryEntry> snapshotLog;\n  private final List<MetadataLogEntry> previousFiles;\n  private final List<StatisticsFile> statisticsFiles;\n  private final List<PartitionStatisticsFile> partitionStatisticsFiles;\n  private final List<MetadataUpdate> changes;\n  private final long nextRowId;\n  private final List<EncryptedKey> encryptionKeys;\n  private SerializableSupplier<List<Snapshot>> snapshotsSupplier;\n  private volatile List<Snapshot> snapshots;\n  private volatile Map<Long, Snapshot> snapshotsById;\n  private volatile Map<String, SnapshotRef> refs;\n  private volatile boolean snapshotsLoaded;\n\n  @SuppressWarnings(\"checkstyle:CyclomaticComplexity\")\n  TableMetadata(\n      String metadataFileLocation,\n      int formatVersion,\n      String uuid,\n      String location,\n      long lastSequenceNumber,\n      long lastUpdatedMillis,\n      int lastColumnId,\n      int currentSchemaId,\n      List<Schema> schemas,\n      int defaultSpecId,\n      List<PartitionSpec> specs,\n      int lastAssignedPartitionId,\n      int defaultSortOrderId,\n      List<SortOrder> sortOrders,\n      Map<String, String> properties,\n      long currentSnapshotId,\n      List<Snapshot> snapshots,\n      SerializableSupplier<List<Snapshot>> snapshotsSupplier,\n      List<HistoryEntry> snapshotLog,\n      List<MetadataLogEntry> previousFiles,\n      Map<String, SnapshotRef> refs,\n      List<StatisticsFile> statisticsFiles,\n      List<PartitionStatisticsFile> partitionStatisticsFiles,\n      long nextRowId,\n      List<EncryptedKey> encryptionKeys,\n      List<MetadataUpdate> changes) {\n    Preconditions.checkArgument(\n        specs != null && !specs.isEmpty(), \"Partition specs cannot be null or empty\");\n    Preconditions.checkArgument(\n        sortOrders != null && !sortOrders.isEmpty(), \"Sort orders cannot be null or empty\");\n    Preconditions.checkArgument(\n        formatVersion <= SUPPORTED_TABLE_FORMAT_VERSION,\n        \"Unsupported format version: v%s (supported: v%s)\",\n        formatVersion,\n        SUPPORTED_TABLE_FORMAT_VERSION);\n    Preconditions.checkArgument(\n        formatVersion == 1 || uuid != null, \"UUID is required in format v%s\", formatVersion);\n    Preconditions.checkArgument(\n        formatVersion > 1 || lastSequenceNumber == 0,\n        \"Sequence number must be 0 in v1: %s\",\n        lastSequenceNumber);\n    Preconditions.checkArgument(\n        metadataFileLocation == null || changes.isEmpty(),\n        \"Cannot create TableMetadata with a metadata location and changes\");\n    Preconditions.checkArgument(encryptionKeys != null, \"Encryption keys cannot be null\");\n\n    this.metadataFileLocation = metadataFileLocation;\n    this.formatVersion = formatVersion;\n    this.uuid = uuid;\n    this.location = location != null ? LocationUtil.stripTrailingSlash(location) : null;\n    this.lastSequenceNumber = lastSequenceNumber;\n    this.lastUpdatedMillis = lastUpdatedMillis;\n    this.lastColumnId = lastColumnId;\n    this.currentSchemaId = currentSchemaId;\n    this.schemas = schemas;\n    this.specs = specs;\n    this.defaultSpecId = defaultSpecId;\n    this.lastAssignedPartitionId = lastAssignedPartitionId;\n    this.defaultSortOrderId = defaultSortOrderId;\n    this.sortOrders = sortOrders;\n    this.properties = properties;\n    this.currentSnapshotId = currentSnapshotId;\n    this.snapshots = snapshots;\n    this.snapshotsSupplier = snapshotsSupplier;\n    this.snapshotsLoaded = snapshotsSupplier == null;\n    this.snapshotLog = snapshotLog;\n    this.previousFiles = previousFiles;\n    this.encryptionKeys = encryptionKeys;\n\n    // changes are carried through until metadata is read from a file\n    this.changes = changes;\n\n    this.snapshotsById = indexAndValidateSnapshots(snapshots, lastSequenceNumber);\n    this.schemasById = indexSchemas();\n    this.specsById = PartitionUtil.indexSpecs(specs);\n    this.sortOrdersById = indexSortOrders(sortOrders);\n    this.refs = validateRefs(currentSnapshotId, refs, snapshotsById);\n    this.statisticsFiles = ImmutableList.copyOf(statisticsFiles);\n    this.partitionStatisticsFiles = ImmutableList.copyOf(partitionStatisticsFiles);\n\n    // row lineage\n    this.nextRowId = nextRowId;\n\n    HistoryEntry last = null;\n    for (HistoryEntry logEntry : snapshotLog) {\n      if (last != null) {\n        Preconditions.checkArgument(\n            (logEntry.timestampMillis() - last.timestampMillis()) >= -ONE_MINUTE,\n            \"[BUG] Expected sorted snapshot log entries.\");\n      }\n      last = logEntry;\n    }\n    if (last != null) {\n      Preconditions.checkArgument(\n          // commits can happen concurrently from different machines.\n          // A tolerance helps us avoid failure for small clock skew\n          lastUpdatedMillis - last.timestampMillis() >= -ONE_MINUTE,\n          \"Invalid update timestamp %s: before last snapshot log entry at %s\",\n          lastUpdatedMillis,\n          last.timestampMillis());\n    }\n\n    MetadataLogEntry previous = null;\n    for (MetadataLogEntry metadataEntry : previousFiles) {\n      if (previous != null) {\n        Preconditions.checkArgument(\n            // commits can happen concurrently from different machines.\n            // A tolerance helps us avoid failure for small clock skew\n            (metadataEntry.timestampMillis() - previous.timestampMillis()) >= -ONE_MINUTE,\n            \"[BUG] Expected sorted previous metadata log entries.\");\n      }\n      previous = metadataEntry;\n    }\n    // Make sure that this update's lastUpdatedMillis is > max(previousFile's timestamp)\n    if (previous != null) {\n      Preconditions.checkArgument(\n          // commits can happen concurrently from different machines.\n          // A tolerance helps us avoid failure for small clock skew\n          lastUpdatedMillis - previous.timestampMillis >= -ONE_MINUTE,\n          \"Invalid update timestamp %s: before the latest metadata log entry timestamp %s\",\n          lastUpdatedMillis,\n          previous.timestampMillis);\n    }\n\n    validateCurrentSnapshot();\n  }\n\n  public int formatVersion() {\n    return formatVersion;\n  }\n\n  public String metadataFileLocation() {\n    return metadataFileLocation;\n  }\n\n  public String uuid() {\n    return uuid;\n  }\n\n  public long lastSequenceNumber() {\n    return lastSequenceNumber;\n  }\n\n  public long nextSequenceNumber() {\n    return formatVersion > 1 ? lastSequenceNumber + 1 : INITIAL_SEQUENCE_NUMBER;\n  }\n\n  public long lastUpdatedMillis() {\n    return lastUpdatedMillis;\n  }\n\n  public int lastColumnId() {\n    return lastColumnId;\n  }\n\n  public Schema schema() {\n    return schemasById.get(currentSchemaId);\n  }\n\n  public List<Schema> schemas() {\n    return schemas;\n  }\n\n  public Map<Integer, Schema> schemasById() {\n    return schemasById;\n  }\n\n  public int currentSchemaId() {\n    return currentSchemaId;\n  }\n\n  public PartitionSpec spec() {\n    return specsById.get(defaultSpecId);\n  }\n\n  public PartitionSpec spec(int id) {\n    return specsById.get(id);\n  }\n\n  public List<PartitionSpec> specs() {\n    return specs;\n  }\n\n  public Map<Integer, PartitionSpec> specsById() {\n    return specsById;\n  }\n\n  public int lastAssignedPartitionId() {\n    return lastAssignedPartitionId;\n  }\n\n  public int defaultSpecId() {\n    return defaultSpecId;\n  }\n\n  public int defaultSortOrderId() {\n    return defaultSortOrderId;\n  }\n\n  public SortOrder sortOrder() {\n    return sortOrdersById.get(defaultSortOrderId);\n  }\n\n  public List<SortOrder> sortOrders() {\n    return sortOrders;\n  }\n\n  public Map<Integer, SortOrder> sortOrdersById() {\n    return sortOrdersById;\n  }\n\n  public String location() {\n    return location;\n  }\n\n  public Map<String, String> properties() {\n    return properties;\n  }\n\n  public String property(String property, String defaultValue) {\n    return properties.getOrDefault(property, defaultValue);\n  }\n\n  public boolean propertyAsBoolean(String property, boolean defaultValue) {\n    return PropertyUtil.propertyAsBoolean(properties, property, defaultValue);\n  }\n\n  public int propertyAsInt(String property, int defaultValue) {\n    return PropertyUtil.propertyAsInt(properties, property, defaultValue);\n  }\n\n  public int propertyTryAsInt(String property, int defaultValue) {\n    return PropertyUtil.propertyTryAsInt(properties, property, defaultValue);\n  }\n\n  public long propertyAsLong(String property, long defaultValue) {\n    return PropertyUtil.propertyAsLong(properties, property, defaultValue);\n  }\n\n  public Snapshot snapshot(long snapshotId) {\n    if (!snapshotsById.containsKey(snapshotId)) {\n      ensureSnapshotsLoaded();\n    }\n\n    return snapshotsById.get(snapshotId);\n  }\n\n  public Snapshot currentSnapshot() {\n    return snapshotsById.get(currentSnapshotId);\n  }\n\n  public List<Snapshot> snapshots() {\n    ensureSnapshotsLoaded();\n\n    return snapshots;\n  }\n\n  private synchronized void ensureSnapshotsLoaded() {\n    if (!snapshotsLoaded) {\n      List<Snapshot> loadedSnapshots = Lists.newArrayList(snapshotsSupplier.get());\n      loadedSnapshots.removeIf(s -> s.sequenceNumber() > lastSequenceNumber);\n\n      this.snapshots = ImmutableList.copyOf(loadedSnapshots);\n      this.snapshotsById = indexAndValidateSnapshots(snapshots, lastSequenceNumber);\n      validateCurrentSnapshot();\n\n      this.refs = validateRefs(currentSnapshotId, refs, snapshotsById);\n\n      this.snapshotsLoaded = true;\n      this.snapshotsSupplier = null;\n    }\n  }\n\n  public SnapshotRef ref(String name) {\n    return refs.get(name);\n  }\n\n  public Map<String, SnapshotRef> refs() {\n    return refs;\n  }\n\n  public List<StatisticsFile> statisticsFiles() {\n    return statisticsFiles;\n  }\n\n  public List<PartitionStatisticsFile> partitionStatisticsFiles() {\n    return partitionStatisticsFiles;\n  }\n\n  public List<HistoryEntry> snapshotLog() {\n    return snapshotLog;\n  }\n\n  public List<MetadataLogEntry> previousFiles() {\n    return previousFiles;\n  }\n\n  public List<MetadataUpdate> changes() {\n    return changes;\n  }\n\n  public TableMetadata withUUID() {\n    return new Builder(this).assignUUID().build();\n  }\n\n  public long nextRowId() {\n    return nextRowId;\n  }\n\n  public List<EncryptedKey> encryptionKeys() {\n    return encryptionKeys;\n  }\n\n  /**\n   * HACK-HACK This is added\n   * Updates the schema\n   * @deprecated in 1.9.0\n   */\n  @Deprecated\n  public TableMetadata updateSchema(Schema newSchema, int newLastColumnId) {\n      return new Builder(this).setCurrentSchema(newSchema, newLastColumnId).build();\n  }\n\n  /** Updates the schema */\n  public TableMetadata updateSchema(Schema newSchema) {\n    return new Builder(this)\n        .setCurrentSchema(newSchema, Math.max(this.lastColumnId, newSchema.highestFieldId()))\n        .build();\n  }\n\n  // The caller is responsible to pass a newPartitionSpec with correct partition field IDs\n  public TableMetadata updatePartitionSpec(PartitionSpec newPartitionSpec) {\n    return new Builder(this).setDefaultPartitionSpec(newPartitionSpec).build();\n  }\n\n  public TableMetadata addPartitionSpec(PartitionSpec newPartitionSpec) {\n    return new Builder(this).addPartitionSpec(newPartitionSpec).build();\n  }\n\n  public TableMetadata replaceSortOrder(SortOrder newOrder) {\n    return new Builder(this).setDefaultSortOrder(newOrder).build();\n  }\n\n  public TableMetadata removeSnapshotsIf(Predicate<Snapshot> removeIf) {\n    List<Snapshot> toRemove = snapshots().stream().filter(removeIf).collect(Collectors.toList());\n    return new Builder(this).removeSnapshots(toRemove).build();\n  }\n\n  public TableMetadata replaceProperties(Map<String, String> rawProperties) {\n    ValidationException.check(rawProperties != null, \"Cannot set properties to null\");\n    Map<String, String> newProperties = unreservedProperties(rawProperties);\n\n    Set<String> removed = Sets.newHashSet(properties.keySet());\n    Map<String, String> updated = Maps.newHashMap();\n    for (Map.Entry<String, String> entry : newProperties.entrySet()) {\n      removed.remove(entry.getKey());\n      String current = properties.get(entry.getKey());\n      if (current == null || !current.equals(entry.getValue())) {\n        updated.put(entry.getKey(), entry.getValue());\n      }\n    }\n\n    int newFormatVersion =\n        PropertyUtil.propertyAsInt(rawProperties, TableProperties.FORMAT_VERSION, formatVersion);\n\n    return new Builder(this)\n        .setProperties(updated)\n        .removeProperties(removed)\n        .upgradeFormatVersion(newFormatVersion)\n        .build();\n  }\n\n  private void validateCurrentSnapshot() {\n    Preconditions.checkArgument(\n        currentSnapshotId < 0 || snapshotsById.containsKey(currentSnapshotId),\n        \"Invalid table metadata: Cannot find current version\");\n  }\n\n  private PartitionSpec reassignPartitionIds(PartitionSpec partitionSpec, TypeUtil.NextID nextID) {\n    PartitionSpec.Builder specBuilder =\n        PartitionSpec.builderFor(partitionSpec.schema()).withSpecId(partitionSpec.specId());\n\n    if (formatVersion > 1) {\n      // for v2 and later, reuse any existing field IDs, but reproduce the same spec\n      Map<Pair<Integer, String>, Integer> transformToFieldId =\n          specs.stream()\n              .flatMap(spec -> spec.fields().stream())\n              .collect(\n                  Collectors.toMap(\n                      field -> Pair.of(field.sourceId(), field.transform().toString()),\n                      PartitionField::fieldId,\n                      Math::max));\n\n      for (PartitionField field : partitionSpec.fields()) {\n        // reassign the partition field ids\n        int partitionFieldId =\n            transformToFieldId.computeIfAbsent(\n                Pair.of(field.sourceId(), field.transform().toString()), k -> nextID.get());\n        specBuilder.add(field.sourceId(), partitionFieldId, field.name(), field.transform());\n      }\n\n    } else {\n      // for v1, preserve the existing spec and carry forward all fields, replacing missing fields\n      // with void\n      Map<Pair<Integer, String>, PartitionField> newFields = Maps.newLinkedHashMap();\n      for (PartitionField newField : partitionSpec.fields()) {\n        newFields.put(Pair.of(newField.sourceId(), newField.transform().toString()), newField);\n      }\n      List<String> newFieldNames =\n          newFields.values().stream().map(PartitionField::name).collect(Collectors.toList());\n\n      for (PartitionField field : spec().fields()) {\n        // ensure each field is either carried forward or replaced with void\n        PartitionField newField =\n            newFields.remove(Pair.of(field.sourceId(), field.transform().toString()));\n        if (newField != null) {\n          // copy the new field with the existing field ID\n          specBuilder.add(\n              newField.sourceId(), field.fieldId(), newField.name(), newField.transform());\n        } else {\n          // Rename old void transforms that would otherwise conflict\n          String voidName =\n              newFieldNames.contains(field.name())\n                  ? field.name() + \"_\" + field.fieldId()\n                  : field.name();\n          specBuilder.add(field.sourceId(), field.fieldId(), voidName, Transforms.alwaysNull());\n        }\n      }\n\n      // add any remaining new fields at the end and assign new partition field IDs\n      for (PartitionField newField : newFields.values()) {\n        specBuilder.add(newField.sourceId(), nextID.get(), newField.name(), newField.transform());\n      }\n    }\n\n    return specBuilder.build();\n  }\n\n  // The caller is responsible to pass a updatedPartitionSpec with correct partition field IDs\n  public TableMetadata buildReplacement(\n      Schema updatedSchema,\n      PartitionSpec updatedPartitionSpec,\n      SortOrder updatedSortOrder,\n      String newLocation,\n      Map<String, String> updatedProperties) {\n    ValidationException.check(\n        formatVersion > 1 || PartitionSpec.hasSequentialIds(updatedPartitionSpec),\n        \"Spec does not use sequential IDs that are required in v1: %s\",\n        updatedPartitionSpec);\n\n    AtomicInteger newLastColumnId = new AtomicInteger(lastColumnId);\n    Schema freshSchema =\n        TypeUtil.assignFreshIds(updatedSchema, schema(), newLastColumnId::incrementAndGet);\n\n    // rebuild the partition spec using the new column ids and reassign partition field ids to align\n    // with existing\n    // partition specs in the table\n    PartitionSpec freshSpec =\n        reassignPartitionIds(\n            freshSpec(INITIAL_SPEC_ID, freshSchema, updatedPartitionSpec),\n            new AtomicInteger(lastAssignedPartitionId)::incrementAndGet);\n\n    // rebuild the sort order using new column ids\n    SortOrder freshSortOrder = freshSortOrder(INITIAL_SORT_ORDER_ID, freshSchema, updatedSortOrder);\n\n    // check if there is format version override\n    int newFormatVersion =\n        PropertyUtil.propertyAsInt(\n            updatedProperties, TableProperties.FORMAT_VERSION, formatVersion);\n\n    return new Builder(this)\n        .upgradeFormatVersion(newFormatVersion)\n        .removeRef(SnapshotRef.MAIN_BRANCH)\n        .setCurrentSchema(freshSchema, newLastColumnId.get())\n        .setDefaultPartitionSpec(freshSpec)\n        .setDefaultSortOrder(freshSortOrder)\n        .setLocation(newLocation)\n        .setProperties(persistedProperties(updatedProperties))\n        .build();\n  }\n\n  public TableMetadata updateLocation(String newLocation) {\n    return new Builder(this).setLocation(newLocation).build();\n  }\n\n  public TableMetadata upgradeToFormatVersion(int newFormatVersion) {\n    return new Builder(this).upgradeFormatVersion(newFormatVersion).build();\n  }\n\n  private static PartitionSpec updateSpecSchema(Schema schema, PartitionSpec partitionSpec) {\n    PartitionSpec.Builder specBuilder =\n        PartitionSpec.builderFor(schema).withSpecId(partitionSpec.specId());\n\n    // add all the fields to the builder. IDs should not change.\n    for (PartitionField field : partitionSpec.fields()) {\n      specBuilder.add(field.sourceId(), field.fieldId(), field.name(), field.transform());\n    }\n\n    // build without validation because the schema may have changed in a way that makes this spec\n    // invalid. the spec\n    // should still be preserved so that older metadata can be interpreted.\n    return specBuilder.buildUnchecked();\n  }\n\n  private static SortOrder updateSortOrderSchema(Schema schema, SortOrder sortOrder) {\n    SortOrder.Builder builder = SortOrder.builderFor(schema).withOrderId(sortOrder.orderId());\n\n    // add all the fields to the builder. IDs should not change.\n    for (SortField field : sortOrder.fields()) {\n      builder.addSortField(\n          field.transform(), field.sourceId(), field.direction(), field.nullOrder());\n    }\n\n    // build without validation because the schema may have changed in a way that makes this order\n    // invalid. the order\n    // should still be preserved so that older metadata can be interpreted.\n    return builder.buildUnchecked();\n  }\n\n  private static PartitionSpec freshSpec(int specId, Schema schema, PartitionSpec partitionSpec) {\n    UnboundPartitionSpec.Builder specBuilder = UnboundPartitionSpec.builder().withSpecId(specId);\n\n    for (PartitionField field : partitionSpec.fields()) {\n      // look up the name of the source field in the old schema to get the new schema's id\n      String sourceName = partitionSpec.schema().findColumnName(field.sourceId());\n\n      final int fieldId;\n      if (sourceName != null) {\n        fieldId = schema.findField(sourceName).fieldId();\n      } else {\n        // In the case of a null sourceName, the column has been deleted.\n        // This only happens in V1 tables where the reference is still around as a void transform\n        fieldId = field.sourceId();\n      }\n      specBuilder.addField(field.transform().toString(), fieldId, field.fieldId(), field.name());\n    }\n\n    return specBuilder.build().bind(schema);\n  }\n\n  private static SortOrder freshSortOrder(int orderId, Schema schema, SortOrder sortOrder) {\n    UnboundSortOrder.Builder builder = UnboundSortOrder.builder();\n\n    if (sortOrder.isSorted()) {\n      builder.withOrderId(orderId);\n    }\n\n    for (SortField field : sortOrder.fields()) {\n      // look up the name of the source field in the old schema to get the new schema's id\n      String sourceName = sortOrder.schema().findColumnName(field.sourceId());\n      // reassign all sort fields with fresh sort field IDs\n      int newSourceId = schema.findField(sourceName).fieldId();\n      builder.addSortField(\n          field.transform().toString(), newSourceId, field.direction(), field.nullOrder());\n    }\n\n    return builder.build().bind(schema);\n  }\n\n  private static Map<Long, Snapshot> indexAndValidateSnapshots(\n      List<Snapshot> snapshots, long lastSequenceNumber) {\n    ImmutableMap.Builder<Long, Snapshot> builder = ImmutableMap.builder();\n    for (Snapshot snap : snapshots) {\n        ValidationException.check(\n          snap.sequenceNumber() <= lastSequenceNumber,\n          \"Invalid snapshot with sequence number %s greater than last sequence number %s\",\n          snap.sequenceNumber(),\n          lastSequenceNumber);\n      builder.put(snap.snapshotId(), snap);\n    }\n    return builder.build();\n  }\n\n  private Map<Integer, Schema> indexSchemas() {\n    ImmutableMap.Builder<Integer, Schema> builder = ImmutableMap.builder();\n    for (Schema schema : schemas) {\n      builder.put(schema.schemaId(), schema);\n    }\n    return builder.build();\n  }\n\n  private static Map<Integer, SortOrder> indexSortOrders(List<SortOrder> sortOrders) {\n    ImmutableMap.Builder<Integer, SortOrder> builder = ImmutableMap.builder();\n    for (SortOrder sortOrder : sortOrders) {\n      builder.put(sortOrder.orderId(), sortOrder);\n    }\n    return builder.build();\n  }\n\n  private static Map<String, SnapshotRef> validateRefs(\n      Long currentSnapshotId,\n      Map<String, SnapshotRef> inputRefs,\n      Map<Long, Snapshot> snapshotsById) {\n    for (SnapshotRef ref : inputRefs.values()) {\n      Preconditions.checkArgument(\n          snapshotsById.containsKey(ref.snapshotId()),\n          \"Snapshot for reference %s does not exist in the existing snapshots list\",\n          ref);\n    }\n\n    SnapshotRef main = inputRefs.get(SnapshotRef.MAIN_BRANCH);\n    if (currentSnapshotId != -1) {\n      Preconditions.checkArgument(\n          main == null || currentSnapshotId == main.snapshotId(),\n          \"Current snapshot ID does not match main branch (%s != %s)\",\n          currentSnapshotId,\n          main != null ? main.snapshotId() : null);\n    } else {\n      Preconditions.checkArgument(\n          main == null, \"Current snapshot is not set, but main branch exists: %s\", main);\n    }\n\n    return inputRefs;\n  }\n\n  public static Builder buildFrom(TableMetadata base) {\n    return new Builder(base);\n  }\n\n  public static Builder buildFromEmpty() {\n    return new Builder(DEFAULT_TABLE_FORMAT_VERSION);\n  }\n\n  public static Builder buildFromEmpty(int formatVersion) {\n    return new Builder(formatVersion);\n  }\n\n  public static class Builder {\n    private static final int LAST_ADDED = -1;\n\n    private final TableMetadata base;\n    private String metadataLocation;\n    private int formatVersion;\n    private String uuid;\n    private Long lastUpdatedMillis;\n    private String location;\n    private long lastSequenceNumber;\n    private int lastColumnId;\n    private int currentSchemaId;\n    private List<Schema> schemas;\n    private int defaultSpecId;\n    private List<PartitionSpec> specs;\n    private int lastAssignedPartitionId;\n    private int defaultSortOrderId;\n    private List<SortOrder> sortOrders;\n    private final Map<String, String> properties;\n    private long currentSnapshotId;\n    private List<Snapshot> snapshots;\n    private SerializableSupplier<List<Snapshot>> snapshotsSupplier;\n    private final Map<String, SnapshotRef> refs;\n    private final Map<Long, List<StatisticsFile>> statisticsFiles;\n    private final Map<Long, List<PartitionStatisticsFile>> partitionStatisticsFiles;\n    private boolean suppressHistoricalSnapshots = false;\n    private long nextRowId;\n    private final List<EncryptedKey> encryptionKeys;\n\n    // change tracking\n    private final List<MetadataUpdate> changes;\n    private final int startingChangeCount;\n    private boolean discardChanges = false;\n    private Integer lastAddedSchemaId = null;\n    private Integer lastAddedSpecId = null;\n    private Integer lastAddedOrderId = null;\n\n    // handled in build\n    private final List<HistoryEntry> snapshotLog;\n    private String previousFileLocation;\n    private final List<MetadataLogEntry> previousFiles;\n\n    // indexes for convenience\n    private final Map<Long, Snapshot> snapshotsById;\n    private final Map<Integer, Schema> schemasById;\n    private final Map<Integer, PartitionSpec> specsById;\n    private final Map<Integer, SortOrder> sortOrdersById;\n    private final Map<String, EncryptedKey> keysById;\n\n    private Builder() {\n      this(DEFAULT_TABLE_FORMAT_VERSION);\n    }\n\n    private Builder(int formatVersion) {\n      this.base = null;\n      this.formatVersion = formatVersion;\n      this.lastSequenceNumber = INITIAL_SEQUENCE_NUMBER;\n      this.uuid = UUID.randomUUID().toString();\n      this.schemas = Lists.newArrayList();\n      this.specs = Lists.newArrayList();\n      this.sortOrders = Lists.newArrayList();\n      this.properties = Maps.newHashMap();\n      this.snapshots = Lists.newArrayList();\n      this.currentSnapshotId = -1;\n      this.changes = Lists.newArrayList();\n      this.startingChangeCount = 0;\n      this.snapshotLog = Lists.newArrayList();\n      this.previousFiles = Lists.newArrayList();\n      this.encryptionKeys = Lists.newArrayList();\n      this.refs = Maps.newHashMap();\n      this.statisticsFiles = Maps.newHashMap();\n      this.partitionStatisticsFiles = Maps.newHashMap();\n      this.snapshotsById = Maps.newHashMap();\n      this.schemasById = Maps.newHashMap();\n      this.specsById = Maps.newHashMap();\n      this.sortOrdersById = Maps.newHashMap();\n      this.keysById = Maps.newHashMap();\n      this.nextRowId = INITIAL_ROW_ID;\n    }\n\n    private Builder(TableMetadata base) {\n      this.base = base;\n      this.formatVersion = base.formatVersion;\n      this.uuid = base.uuid;\n      this.lastUpdatedMillis = null;\n      this.location = base.location;\n      this.lastSequenceNumber = base.lastSequenceNumber;\n      this.lastColumnId = base.lastColumnId;\n      this.currentSchemaId = base.currentSchemaId;\n      this.schemas = Lists.newArrayList(base.schemas);\n      this.defaultSpecId = base.defaultSpecId;\n      this.specs = Lists.newArrayList(base.specs);\n      this.lastAssignedPartitionId = base.lastAssignedPartitionId;\n      this.defaultSortOrderId = base.defaultSortOrderId;\n      this.sortOrders = Lists.newArrayList(base.sortOrders);\n      this.properties = Maps.newHashMap(base.properties);\n      this.currentSnapshotId = base.currentSnapshotId;\n      this.snapshots = Lists.newArrayList(base.snapshots());\n      this.encryptionKeys = Lists.newArrayList(base.encryptionKeys);\n      this.changes = Lists.newArrayList(base.changes);\n      this.startingChangeCount = changes.size();\n\n      this.snapshotLog = Lists.newArrayList(base.snapshotLog);\n      this.previousFileLocation = base.metadataFileLocation;\n      this.previousFiles = base.previousFiles;\n      this.refs = Maps.newHashMap(base.refs);\n      this.statisticsFiles = indexStatistics(base.statisticsFiles);\n      this.partitionStatisticsFiles = indexPartitionStatistics(base.partitionStatisticsFiles);\n      this.snapshotsById = Maps.newHashMap(base.snapshotsById);\n      this.schemasById = Maps.newHashMap(base.schemasById);\n      this.specsById = Maps.newHashMap(base.specsById);\n      this.sortOrdersById = Maps.newHashMap(base.sortOrdersById);\n      this.keysById =\n        encryptionKeys.stream()\n          .collect(Collectors.toMap(EncryptedKey::keyId, Function.identity()));\n\n      this.nextRowId = base.nextRowId;\n    }\n\n    // Hack-Hack This is added\n    public Builder withNextRowId(Long newRowId) {\n        this.nextRowId = newRowId;\n        return this;\n    }\n\n    public Builder withMetadataLocation(String newMetadataLocation) {\n      this.metadataLocation = newMetadataLocation;\n      if (null != base) {\n        // carry over lastUpdatedMillis from base and set previousFileLocation to null to avoid\n        // writing a new metadata log entry\n        // this is safe since setting metadata location doesn't cause any changes and no other\n        // changes can be added when metadata location is configured\n        this.lastUpdatedMillis = base.lastUpdatedMillis();\n        this.previousFileLocation = null;\n      }\n\n      return this;\n    }\n\n    public Builder assignUUID() {\n      if (uuid == null) {\n        this.uuid = UUID.randomUUID().toString();\n        changes.add(new MetadataUpdate.AssignUUID(uuid));\n      }\n\n      return this;\n    }\n\n    public Builder assignUUID(String newUuid) {\n      Preconditions.checkArgument(newUuid != null, \"Cannot set uuid to null\");\n\n      if (!newUuid.equals(uuid)) {\n        this.uuid = newUuid;\n        changes.add(new MetadataUpdate.AssignUUID(uuid));\n      }\n\n      return this;\n    }\n\n    // it is only safe to set the format version directly while creating tables\n    // in all other cases, use upgradeFormatVersion\n    private Builder setInitialFormatVersion(int newFormatVersion) {\n      Preconditions.checkArgument(\n          newFormatVersion <= SUPPORTED_TABLE_FORMAT_VERSION,\n          \"Unsupported format version: v%s (supported: v%s)\",\n          newFormatVersion,\n          SUPPORTED_TABLE_FORMAT_VERSION);\n      this.formatVersion = newFormatVersion;\n      return this;\n    }\n\n    public Builder upgradeFormatVersion(int newFormatVersion) {\n      Preconditions.checkArgument(\n          newFormatVersion <= SUPPORTED_TABLE_FORMAT_VERSION,\n          \"Cannot upgrade table to unsupported format version: v%s (supported: v%s)\",\n          newFormatVersion,\n          SUPPORTED_TABLE_FORMAT_VERSION);\n        Preconditions.checkArgument(\n                newFormatVersion >= formatVersion,\n                \"Cannot downgrade v%s table to v%s\",\n                formatVersion,\n                newFormatVersion);\n\n      if (newFormatVersion == formatVersion) {\n        return this;\n      }\n\n      this.formatVersion = newFormatVersion;\n      changes.add(new MetadataUpdate.UpgradeFormatVersion(newFormatVersion));\n\n      return this;\n    }\n\n    public Builder setCurrentSchema(Schema newSchema, int newLastColumnId) {\n      setCurrentSchema(addSchemaInternal(newSchema, newLastColumnId));\n      return this;\n    }\n\n    public Builder setCurrentSchema(int schemaId) {\n      if (schemaId == -1) {\n        ValidationException.check(\n            lastAddedSchemaId != null, \"Cannot set last added schema: no schema has been added\");\n        return setCurrentSchema(lastAddedSchemaId);\n      }\n\n      if (currentSchemaId == schemaId) {\n        return this;\n      }\n\n      Schema schema = schemasById.get(schemaId);\n      Preconditions.checkArgument(\n          schema != null, \"Cannot set current schema to unknown schema: %s\", schemaId);\n\n      // rebuild all the partition specs and sort orders for the new current schema\n      this.specs =\n          Lists.newArrayList(Iterables.transform(specs, spec -> updateSpecSchema(schema, spec)));\n      specsById.clear();\n      specsById.putAll(PartitionUtil.indexSpecs(specs));\n\n      this.sortOrders =\n          Lists.newArrayList(\n              Iterables.transform(sortOrders, order -> updateSortOrderSchema(schema, order)));\n      sortOrdersById.clear();\n      sortOrdersById.putAll(indexSortOrders(sortOrders));\n\n      this.currentSchemaId = schemaId;\n\n      if (lastAddedSchemaId != null && lastAddedSchemaId == schemaId) {\n        changes.add(new MetadataUpdate.SetCurrentSchema(LAST_ADDED));\n      } else {\n        changes.add(new MetadataUpdate.SetCurrentSchema(schemaId));\n      }\n\n      return this;\n    }\n\n    public Builder addSchema(Schema schema) {\n      addSchemaInternal(schema, Math.max(lastColumnId, schema.highestFieldId()));\n      return this;\n    }\n\n    /**\n     * Hack-Hack This is added\n     * Add a new schema.\n     * @deprecated since 1.8.0, will be removed in 1.9.0 or 2.0.0, use AddSchema(schema).\n     */\n    @Deprecated\n    public Builder addSchema(Schema schema, int newLastColumnId) {\n      addSchemaInternal(schema, newLastColumnId);\n      return this;\n    }\n\n    public Builder setDefaultPartitionSpec(PartitionSpec spec) {\n      setDefaultPartitionSpec(addPartitionSpecInternal(spec));\n      return this;\n    }\n\n    public Builder setDefaultPartitionSpec(int specId) {\n      if (specId == -1) {\n        ValidationException.check(\n            lastAddedSpecId != null, \"Cannot set last added spec: no spec has been added\");\n        return setDefaultPartitionSpec(lastAddedSpecId);\n      }\n\n      if (defaultSpecId == specId) {\n        // the new spec is already current and no change is needed\n        return this;\n      }\n\n      this.defaultSpecId = specId;\n      if (lastAddedSpecId != null && lastAddedSpecId == specId) {\n        changes.add(new MetadataUpdate.SetDefaultPartitionSpec(LAST_ADDED));\n      } else {\n        changes.add(new MetadataUpdate.SetDefaultPartitionSpec(specId));\n      }\n\n      return this;\n    }\n\n    Builder removeSpecs(Iterable<Integer> specIds) {\n      Set<Integer> specIdsToRemove = Sets.newHashSet(specIds);\n      Preconditions.checkArgument(\n          !specIdsToRemove.contains(defaultSpecId), \"Cannot remove the default partition spec\");\n\n      if (!specIdsToRemove.isEmpty()) {\n        this.specs =\n            specs.stream()\n                .filter(s -> !specIdsToRemove.contains(s.specId()))\n                .collect(Collectors.toList());\n        changes.add(new MetadataUpdate.RemovePartitionSpecs(specIdsToRemove));\n      }\n\n      return this;\n    }\n\n    Builder removeSchemas(Iterable<Integer> schemaIds) {\n      Set<Integer> schemaIdsToRemove = Sets.newHashSet(schemaIds);\n      Preconditions.checkArgument(\n          !schemaIdsToRemove.contains(currentSchemaId), \"Cannot remove the current schema\");\n\n      if (!schemaIdsToRemove.isEmpty()) {\n        this.schemas =\n            schemas.stream()\n                .filter(s -> !schemaIdsToRemove.contains(s.schemaId()))\n                .collect(Collectors.toList());\n        changes.add(new MetadataUpdate.RemoveSchemas(schemaIdsToRemove));\n      }\n\n      return this;\n    }\n\n    public Builder addPartitionSpec(UnboundPartitionSpec spec) {\n      addPartitionSpecInternal(spec.bind(schemasById.get(currentSchemaId)));\n      return this;\n    }\n\n    public Builder addPartitionSpec(PartitionSpec spec) {\n      addPartitionSpecInternal(spec);\n      return this;\n    }\n\n    public Builder setDefaultSortOrder(SortOrder order) {\n      setDefaultSortOrder(addSortOrderInternal(order));\n      return this;\n    }\n\n    public Builder setDefaultSortOrder(int sortOrderId) {\n      if (sortOrderId == -1) {\n        ValidationException.check(\n            lastAddedOrderId != null,\n            \"Cannot set last added sort order: no sort order has been added\");\n        return setDefaultSortOrder(lastAddedOrderId);\n      }\n\n      if (sortOrderId == defaultSortOrderId) {\n        return this;\n      }\n\n      this.defaultSortOrderId = sortOrderId;\n      if (lastAddedOrderId != null && lastAddedOrderId == sortOrderId) {\n        changes.add(new MetadataUpdate.SetDefaultSortOrder(LAST_ADDED));\n      } else {\n        changes.add(new MetadataUpdate.SetDefaultSortOrder(sortOrderId));\n      }\n\n      return this;\n    }\n\n    public Builder addSortOrder(UnboundSortOrder order) {\n      addSortOrderInternal(order.bind(schemasById.get(currentSchemaId)));\n      return this;\n    }\n\n    public Builder addSortOrder(SortOrder order) {\n      addSortOrderInternal(order);\n      return this;\n    }\n\n    public Builder addSnapshot(Snapshot snapshot) {\n      if (snapshot == null) {\n        // change is a noop\n        return this;\n      }\n\n      ValidationException.check(\n          !schemas.isEmpty(), \"Attempting to add a snapshot before a schema is added\");\n      ValidationException.check(\n          !specs.isEmpty(), \"Attempting to add a snapshot before a partition spec is added\");\n      ValidationException.check(\n          !sortOrders.isEmpty(), \"Attempting to add a snapshot before a sort order is added\");\n\n      ValidationException.check(\n          !snapshotsById.containsKey(snapshot.snapshotId()),\n          \"Snapshot already exists for id: %s\",\n          snapshot.snapshotId());\n\n      ValidationException.check(\n          formatVersion == 1\n              || snapshot.sequenceNumber() > lastSequenceNumber\n              || snapshot.parentId() == null,\n          \"Cannot add snapshot with sequence number %s older than last sequence number %s\",\n          snapshot.sequenceNumber(),\n          lastSequenceNumber);\n\n      this.lastUpdatedMillis = snapshot.timestampMillis();\n      this.lastSequenceNumber = snapshot.sequenceNumber();\n      snapshots.add(snapshot);\n      snapshotsById.put(snapshot.snapshotId(), snapshot);\n      changes.add(new MetadataUpdate.AddSnapshot(snapshot));\n\n      if (formatVersion >= MIN_FORMAT_VERSION_ROW_LINEAGE) {\n        ValidationException.check(\n            snapshot.firstRowId() != null, \"Cannot add a snapshot: first-row-id is null\");\n        ValidationException.check(\n            snapshot.firstRowId() != null && snapshot.firstRowId() >= nextRowId,\n            \"Cannot add a snapshot, first-row-id is behind table next-row-id: %s < %s\",\n            snapshot.firstRowId(),\n            nextRowId);\n\n        this.nextRowId += snapshot.addedRows();\n      }\n\n      return this;\n    }\n\n    public Builder setSnapshotsSupplier(SerializableSupplier<List<Snapshot>> snapshotsSupplier) {\n      this.snapshotsSupplier = snapshotsSupplier;\n      return this;\n    }\n\n    public Builder setBranchSnapshot(Snapshot snapshot, String branch) {\n      addSnapshot(snapshot);\n      setBranchSnapshotInternal(snapshot, branch);\n      return this;\n    }\n\n    public Builder setBranchSnapshot(long snapshotId, String branch) {\n      SnapshotRef ref = refs.get(branch);\n      if (ref != null && ref.snapshotId() == snapshotId) {\n        // change is a noop\n        return this;\n      }\n\n      Snapshot snapshot = snapshotsById.get(snapshotId);\n      ValidationException.check(\n          snapshot != null, \"Cannot set %s to unknown snapshot: %s\", branch, snapshotId);\n\n      setBranchSnapshotInternal(snapshot, branch);\n\n      return this;\n    }\n\n    public Builder setRef(String name, SnapshotRef ref) {\n      SnapshotRef existingRef = refs.get(name);\n      if (existingRef != null && existingRef.equals(ref)) {\n        return this;\n      }\n\n      long snapshotId = ref.snapshotId();\n      Snapshot snapshot = snapshotsById.get(snapshotId);\n      ValidationException.check(\n          snapshot != null, \"Cannot set %s to unknown snapshot: %s\", name, snapshotId);\n      if (isAddedSnapshot(snapshotId)) {\n        this.lastUpdatedMillis = snapshot.timestampMillis();\n      }\n\n      if (SnapshotRef.MAIN_BRANCH.equals(name)) {\n        this.currentSnapshotId = ref.snapshotId();\n        if (lastUpdatedMillis == null) {\n          this.lastUpdatedMillis = System.currentTimeMillis();\n        }\n\n        snapshotLog.add(new SnapshotLogEntry(lastUpdatedMillis, ref.snapshotId()));\n      }\n\n      refs.put(name, ref);\n      MetadataUpdate.SetSnapshotRef refUpdate =\n          new MetadataUpdate.SetSnapshotRef(\n              name,\n              ref.snapshotId(),\n              ref.type(),\n              ref.minSnapshotsToKeep(),\n              ref.maxSnapshotAgeMs(),\n              ref.maxRefAgeMs());\n      changes.add(refUpdate);\n      return this;\n    }\n\n    public Builder removeRef(String name) {\n      if (SnapshotRef.MAIN_BRANCH.equals(name)) {\n        this.currentSnapshotId = -1;\n      }\n\n      SnapshotRef ref = refs.remove(name);\n      if (ref != null) {\n        changes.add(new MetadataUpdate.RemoveSnapshotRef(name));\n      }\n\n      return this;\n    }\n\n    public Builder setStatistics(StatisticsFile statisticsFile) {\n      Preconditions.checkNotNull(statisticsFile, \"statisticsFile is null\");\n      statisticsFiles.put(statisticsFile.snapshotId(), ImmutableList.of(statisticsFile));\n      changes.add(new MetadataUpdate.SetStatistics(statisticsFile));\n      return this;\n    }\n\n    public Builder removeStatistics(long snapshotId) {\n      if (statisticsFiles.remove(snapshotId) == null) {\n        return this;\n      }\n      changes.add(new MetadataUpdate.RemoveStatistics(snapshotId));\n      return this;\n    }\n\n    /**\n     * Suppresses snapshots that are historical, removing the metadata for lazy snapshot loading.\n     *\n     * <p>Note that the snapshots are not considered removed from metadata and no RemoveSnapshot\n     * changes are created.\n     *\n     * <p>A snapshot is historical if no ref directly references its ID.\n     *\n     * @return this for method chaining\n     */\n    public Builder suppressHistoricalSnapshots() {\n      this.suppressHistoricalSnapshots = true;\n      Set<Long> refSnapshotIds =\n          refs.values().stream().map(SnapshotRef::snapshotId).collect(Collectors.toSet());\n      Set<Long> suppressedSnapshotIds = Sets.difference(snapshotsById.keySet(), refSnapshotIds);\n      rewriteSnapshotsInternal(suppressedSnapshotIds, true);\n      return this;\n    }\n\n    public Builder setPartitionStatistics(PartitionStatisticsFile file) {\n      Preconditions.checkNotNull(file, \"partition statistics file is null\");\n      partitionStatisticsFiles.put(file.snapshotId(), ImmutableList.of(file));\n      changes.add(new MetadataUpdate.SetPartitionStatistics(file));\n      return this;\n    }\n\n    public Builder removePartitionStatistics(long snapshotId) {\n      if (partitionStatisticsFiles.remove(snapshotId) == null) {\n        return this;\n      }\n\n      changes.add(new MetadataUpdate.RemovePartitionStatistics(snapshotId));\n      return this;\n    }\n\n    public Builder removeSnapshots(List<Snapshot> snapshotsToRemove) {\n      Set<Long> idsToRemove =\n          snapshotsToRemove.stream().map(Snapshot::snapshotId).collect(Collectors.toSet());\n      return removeSnapshots(idsToRemove);\n    }\n\n    public Builder removeSnapshots(Collection<Long> idsToRemove) {\n      return rewriteSnapshotsInternal(idsToRemove, false);\n    }\n\n    /**\n     * Rewrite this builder's snapshots by removing the snapshots for a list of IDs.\n     *\n     * <p>If suppress is true, changes are not created.\n     *\n     * @param idsToRemove collection of snapshot IDs to remove from this builder\n     * @param suppress whether the operation is suppressing snapshots (retains history) or removing\n     * @return this for method chaining\n     */\n    private Builder rewriteSnapshotsInternal(Collection<Long> idsToRemove, boolean suppress) {\n      List<Snapshot> retainedSnapshots =\n          Lists.newArrayListWithExpectedSize(snapshots.size() - idsToRemove.size());\n\n      for (Snapshot snapshot : snapshots) {\n        long snapshotId = snapshot.snapshotId();\n        if (idsToRemove.contains(snapshotId)) {\n          snapshotsById.remove(snapshotId);\n          if (!suppress) {\n            changes.add(new MetadataUpdate.RemoveSnapshots(snapshotId));\n          }\n          removeStatistics(snapshotId);\n          removePartitionStatistics(snapshotId);\n        } else {\n          retainedSnapshots.add(snapshot);\n        }\n      }\n\n      this.snapshots = retainedSnapshots;\n\n      // remove any refs that are no longer valid\n      Set<String> danglingRefs = Sets.newHashSet();\n      for (Map.Entry<String, SnapshotRef> refEntry : refs.entrySet()) {\n        if (!snapshotsById.containsKey(refEntry.getValue().snapshotId())) {\n          danglingRefs.add(refEntry.getKey());\n        }\n      }\n\n      danglingRefs.forEach(this::removeRef);\n\n      return this;\n    }\n\n    public Builder setProperties(Map<String, String> updated) {\n      if (updated.isEmpty()) {\n        return this;\n      }\n\n      properties.putAll(updated);\n      changes.add(new MetadataUpdate.SetProperties(updated));\n\n      return this;\n    }\n\n    public Builder removeProperties(Set<String> removed) {\n      if (removed.isEmpty()) {\n        return this;\n      }\n\n      removed.forEach(properties::remove);\n      changes.add(new MetadataUpdate.RemoveProperties(removed));\n\n      return this;\n    }\n\n    public Builder setLocation(String newLocation) {\n      if (location != null && location.equals(newLocation)) {\n        return this;\n      }\n\n      this.location = newLocation;\n      changes.add(new MetadataUpdate.SetLocation(newLocation));\n\n      return this;\n    }\n\n    public Builder addEncryptionKey(EncryptedKey key) {\n      if (keysById.containsKey(key.keyId())) {\n        // already exists\n        return this;\n      }\n\n      encryptionKeys.add(key);\n      keysById.put(key.keyId(), key);\n\n      changes.add(new MetadataUpdate.AddEncryptionKey(key));\n\n      return this;\n    }\n\n    public Builder removeEncryptionKey(String keyId) {\n      boolean removed = encryptionKeys.removeIf(key -> key.keyId().equals(keyId));\n      keysById.remove(keyId);\n\n      if (removed) {\n        changes.add(new MetadataUpdate.RemoveEncryptionKey(keyId));\n      }\n\n      return this;\n    }\n\n    public Builder discardChanges() {\n      this.discardChanges = true;\n      return this;\n    }\n\n    public Builder setPreviousFileLocation(String previousFileLocation) {\n      this.previousFileLocation = previousFileLocation;\n      return this;\n    }\n\n    private boolean hasChanges() {\n      return changes.size() != startingChangeCount\n          || (discardChanges && !changes.isEmpty())\n          || metadataLocation != null\n          || suppressHistoricalSnapshots\n          || null != snapshotsSupplier;\n    }\n\n    public TableMetadata build() {\n      if (!hasChanges()) {\n        return base;\n      }\n\n      if (lastUpdatedMillis == null) {\n        this.lastUpdatedMillis = System.currentTimeMillis();\n      }\n\n      // when associated with a metadata file, table metadata must have no changes so that the\n      // metadata matches exactly\n      // what is in the metadata file, which does not store changes. metadata location with changes\n      // is inconsistent.\n      Preconditions.checkArgument(\n          changes.isEmpty() || discardChanges || metadataLocation == null,\n          \"Cannot set metadata location with changes to table metadata: %s changes\",\n          changes.size());\n\n      Schema schema = schemasById.get(currentSchemaId);\n      PartitionSpec.checkCompatibility(specsById.get(defaultSpecId), schema);\n      SortOrder.checkCompatibility(sortOrdersById.get(defaultSortOrderId), schema);\n\n      List<MetadataLogEntry> metadataHistory;\n      if (base == null) {\n        metadataHistory = Lists.newArrayList();\n      } else {\n        metadataHistory =\n            addPreviousFile(\n                previousFiles, previousFileLocation, base.lastUpdatedMillis(), properties);\n      }\n      List<HistoryEntry> newSnapshotLog =\n          updateSnapshotLog(snapshotLog, snapshotsById, currentSnapshotId, changes);\n\n      return new TableMetadata(\n          metadataLocation,\n          formatVersion,\n          uuid,\n          location,\n          lastSequenceNumber,\n          lastUpdatedMillis,\n          lastColumnId,\n          currentSchemaId,\n          ImmutableList.copyOf(schemas),\n          defaultSpecId,\n          ImmutableList.copyOf(specs),\n          lastAssignedPartitionId,\n          defaultSortOrderId,\n          ImmutableList.copyOf(sortOrders),\n          ImmutableMap.copyOf(properties),\n          currentSnapshotId,\n          ImmutableList.copyOf(snapshots),\n          snapshotsSupplier,\n          ImmutableList.copyOf(newSnapshotLog),\n          ImmutableList.copyOf(metadataHistory),\n          ImmutableMap.copyOf(refs),\n          statisticsFiles.values().stream().flatMap(List::stream).collect(Collectors.toList()),\n          partitionStatisticsFiles.values().stream()\n              .flatMap(List::stream)\n              .collect(Collectors.toList()),\n          nextRowId,\n          encryptionKeys,\n          discardChanges ? ImmutableList.of() : ImmutableList.copyOf(changes));\n    }\n\n    private int addSchemaInternal(Schema schema, int newLastColumnId) {\n      Preconditions.checkArgument(\n          newLastColumnId >= lastColumnId,\n          \"Invalid last column ID: %s < %s (previous last column ID)\",\n          newLastColumnId,\n          lastColumnId);\n\n      Schema.checkCompatibility(schema, formatVersion);\n\n      int newSchemaId = reuseOrCreateNewSchemaId(schema);\n      boolean schemaFound = schemasById.containsKey(newSchemaId);\n      if (schemaFound && newLastColumnId == lastColumnId) {\n        // the new spec and last column id is already current and no change is needed\n        // update lastAddedSchemaId if the schema was added in this set of changes (since it is now\n        // the last)\n        boolean isNewSchema =\n            lastAddedSchemaId != null\n                && changes(MetadataUpdate.AddSchema.class)\n                    .anyMatch(added -> added.schema().schemaId() == newSchemaId);\n        this.lastAddedSchemaId = isNewSchema ? newSchemaId : null;\n        return newSchemaId;\n      }\n\n      this.lastColumnId = newLastColumnId;\n\n      Schema newSchema;\n      if (newSchemaId != schema.schemaId()) {\n        newSchema = new Schema(newSchemaId, schema.columns(), schema.identifierFieldIds());\n      } else {\n        newSchema = schema;\n      }\n\n      if (!schemaFound) {\n        schemas.add(newSchema);\n        schemasById.put(newSchema.schemaId(), newSchema);\n      }\n\n      changes.add(new MetadataUpdate.AddSchema(newSchema));\n\n      this.lastAddedSchemaId = newSchemaId;\n\n      return newSchemaId;\n    }\n\n    private int reuseOrCreateNewSchemaId(Schema newSchema) {\n      // if the schema already exists, use its id; otherwise use the highest id + 1\n      int newSchemaId = currentSchemaId;\n      for (Schema schema : schemas) {\n        if (schema.sameSchema(newSchema)) {\n          return schema.schemaId();\n        } else if (schema.schemaId() >= newSchemaId) {\n          newSchemaId = schema.schemaId() + 1;\n        }\n      }\n      return newSchemaId;\n    }\n\n    private int addPartitionSpecInternal(PartitionSpec spec) {\n      int newSpecId = reuseOrCreateNewSpecId(spec);\n      if (specsById.containsKey(newSpecId)) {\n        // update lastAddedSpecId if the spec was added in this set of changes (since it is now the\n        // last)\n        boolean isNewSpec =\n            lastAddedSpecId != null\n                && changes(MetadataUpdate.AddPartitionSpec.class)\n                    .anyMatch(added -> added.spec().specId() == lastAddedSpecId);\n        this.lastAddedSpecId = isNewSpec ? newSpecId : null;\n        return newSpecId;\n      }\n\n      Schema schema = schemasById.get(currentSchemaId);\n      PartitionSpec.checkCompatibility(spec, schema);\n      ValidationException.check(\n          formatVersion > 1 || PartitionSpec.hasSequentialIds(spec),\n          \"Spec does not use sequential IDs that are required in v1: %s\",\n          spec);\n\n      PartitionSpec newSpec = freshSpec(newSpecId, schema, spec);\n      this.lastAssignedPartitionId =\n          Math.max(lastAssignedPartitionId, newSpec.lastAssignedFieldId());\n      specs.add(newSpec);\n      specsById.put(newSpecId, newSpec);\n\n      changes.add(new MetadataUpdate.AddPartitionSpec(newSpec));\n\n      this.lastAddedSpecId = newSpecId;\n\n      return newSpecId;\n    }\n\n    private int reuseOrCreateNewSpecId(PartitionSpec newSpec) {\n      // if the spec already exists, use the same ID. otherwise, use 1 more than the highest ID.\n      int newSpecId = INITIAL_SPEC_ID;\n      for (PartitionSpec spec : specs) {\n        if (newSpec.compatibleWith(spec)) {\n          return spec.specId();\n        } else if (newSpecId <= spec.specId()) {\n          newSpecId = spec.specId() + 1;\n        }\n      }\n\n      return newSpecId;\n    }\n\n    private int addSortOrderInternal(SortOrder order) {\n      int newOrderId = reuseOrCreateNewSortOrderId(order);\n      if (sortOrdersById.containsKey(newOrderId)) {\n        // update lastAddedOrderId if the order was added in this set of changes (since it is now\n        // the last)\n        boolean isNewOrder =\n            lastAddedOrderId != null\n                && changes(MetadataUpdate.AddSortOrder.class)\n                    .anyMatch(added -> added.sortOrder().orderId() == lastAddedOrderId);\n        this.lastAddedOrderId = isNewOrder ? newOrderId : null;\n        return newOrderId;\n      }\n\n      Schema schema = schemasById.get(currentSchemaId);\n      SortOrder.checkCompatibility(order, schema);\n\n      SortOrder newOrder;\n      if (order.isUnsorted()) {\n        newOrder = SortOrder.unsorted();\n      } else {\n        // rebuild the sort order using new column ids\n        newOrder = freshSortOrder(newOrderId, schema, order);\n      }\n\n      sortOrders.add(newOrder);\n      sortOrdersById.put(newOrderId, newOrder);\n\n      changes.add(new MetadataUpdate.AddSortOrder(newOrder));\n\n      this.lastAddedOrderId = newOrderId;\n\n      return newOrderId;\n    }\n\n    private int reuseOrCreateNewSortOrderId(SortOrder newOrder) {\n      if (newOrder.isUnsorted()) {\n        return SortOrder.unsorted().orderId();\n      }\n\n      // determine the next order id\n      int newOrderId = INITIAL_SORT_ORDER_ID;\n      for (SortOrder order : sortOrders) {\n        if (order.sameOrder(newOrder)) {\n          return order.orderId();\n        } else if (newOrderId <= order.orderId()) {\n          newOrderId = order.orderId() + 1;\n        }\n      }\n\n      return newOrderId;\n    }\n\n    private void setBranchSnapshotInternal(Snapshot snapshot, String branch) {\n      long replacementSnapshotId = snapshot.snapshotId();\n      SnapshotRef ref = refs.get(branch);\n      if (ref != null) {\n        ValidationException.check(ref.isBranch(), \"Cannot update branch: %s is a tag\", branch);\n        if (ref.snapshotId() == replacementSnapshotId) {\n          return;\n        }\n      }\n\n      ValidationException.check(\n          formatVersion == 1 || snapshot.sequenceNumber() <= lastSequenceNumber,\n          \"Last sequence number %s is less than existing snapshot sequence number %s\",\n          lastSequenceNumber,\n          snapshot.sequenceNumber());\n\n      SnapshotRef newRef;\n      if (ref != null) {\n        newRef = SnapshotRef.builderFrom(ref, replacementSnapshotId).build();\n      } else {\n        newRef = SnapshotRef.branchBuilder(replacementSnapshotId).build();\n      }\n\n      setRef(branch, newRef);\n    }\n\n    private static List<MetadataLogEntry> addPreviousFile(\n        List<MetadataLogEntry> previousFiles,\n        String previousFileLocation,\n        long timestampMillis,\n        Map<String, String> properties) {\n      if (previousFileLocation == null) {\n        return previousFiles;\n      }\n\n      int maxSize =\n          Math.max(\n              1,\n              PropertyUtil.propertyAsInt(\n                  properties,\n                  TableProperties.METADATA_PREVIOUS_VERSIONS_MAX,\n                  TableProperties.METADATA_PREVIOUS_VERSIONS_MAX_DEFAULT));\n\n      List<MetadataLogEntry> newMetadataLog;\n      if (previousFiles.size() >= maxSize) {\n        int removeIndex = previousFiles.size() - maxSize + 1;\n        newMetadataLog =\n            Lists.newArrayList(previousFiles.subList(removeIndex, previousFiles.size()));\n      } else {\n        newMetadataLog = Lists.newArrayList(previousFiles);\n      }\n      newMetadataLog.add(new MetadataLogEntry(timestampMillis, previousFileLocation));\n\n      return newMetadataLog;\n    }\n\n    /**\n     * Finds intermediate snapshots that have not been committed as the current snapshot.\n     *\n     * <p>Transactions can create snapshots that are never the current snapshot because several\n     * changes are combined by the transaction into one table metadata update. when each\n     * intermediate snapshot is added to table metadata, it is added to the snapshot log, assuming\n     * that it will be the current snapshot. when there are multiple snapshot updates, the log must\n     * be corrected by suppressing the intermediate snapshot entries.\n     *\n     * <p>A snapshot is an intermediate snapshot if it was added but is not the current snapshot.\n     *\n     * @return a set of snapshot ids for all added snapshots that were later replaced as the current\n     *     snapshot in changes\n     */\n    private static Set<Long> intermediateSnapshotIdSet(\n        List<MetadataUpdate> changes, long currentSnapshotId) {\n      Set<Long> addedSnapshotIds = Sets.newHashSet();\n      Set<Long> intermediateSnapshotIds = Sets.newHashSet();\n      for (MetadataUpdate update : changes) {\n        if (update instanceof MetadataUpdate.AddSnapshot) {\n          // adds must always come before set current snapshot\n          MetadataUpdate.AddSnapshot addSnapshot = (MetadataUpdate.AddSnapshot) update;\n          addedSnapshotIds.add(addSnapshot.snapshot().snapshotId());\n        } else if (update instanceof MetadataUpdate.SetSnapshotRef) {\n          MetadataUpdate.SetSnapshotRef setRef = (MetadataUpdate.SetSnapshotRef) update;\n          long snapshotId = setRef.snapshotId();\n          if (addedSnapshotIds.contains(snapshotId)\n              && SnapshotRef.MAIN_BRANCH.equals(setRef.name())\n              && snapshotId != currentSnapshotId) {\n            intermediateSnapshotIds.add(snapshotId);\n          }\n        }\n      }\n\n      return intermediateSnapshotIds;\n    }\n\n    private static List<HistoryEntry> updateSnapshotLog(\n        List<HistoryEntry> snapshotLog,\n        Map<Long, Snapshot> snapshotsById,\n        long currentSnapshotId,\n        List<MetadataUpdate> changes) {\n      Set<Long> intermediateSnapshotIds = intermediateSnapshotIdSet(changes, currentSnapshotId);\n      boolean hasIntermediateSnapshots = !intermediateSnapshotIds.isEmpty();\n      boolean hasRemovedSnapshots =\n        changes.stream().anyMatch(change -> change instanceof MetadataUpdate.RemoveSnapshots);\n\n      if (!hasIntermediateSnapshots && !hasRemovedSnapshots) {\n        return snapshotLog;\n      }\n\n      // update the snapshot log\n      List<HistoryEntry> newSnapshotLog = Lists.newArrayList();\n      for (HistoryEntry logEntry : snapshotLog) {\n        long snapshotId = logEntry.snapshotId();\n        if (snapshotsById.containsKey(snapshotId)) {\n          if (!intermediateSnapshotIds.contains(snapshotId)) {\n            // copy the log entries that are still valid\n            newSnapshotLog.add(logEntry);\n          }\n        } else if (hasRemovedSnapshots) {\n          // any invalid entry causes the history before it to be removed. otherwise, there could be\n          // history gaps that cause time-travel queries to produce incorrect results. for example,\n          // if history is [(t1, s1), (t2, s2), (t3, s3)] and s2 is removed, the history cannot be\n          // [(t1, s1), (t3, s3)] because it appears that s3 was current during the time between t2\n          // and t3 when in fact s2 was the current snapshot.\n          newSnapshotLog.clear();\n        }\n      }\n\n      if (snapshotsById.get(currentSnapshotId) != null) {\n        ValidationException.check(\n            Iterables.getLast(newSnapshotLog).snapshotId() == currentSnapshotId,\n            \"Cannot set invalid snapshot log: latest entry is not the current snapshot\");\n      }\n\n      return newSnapshotLog;\n    }\n\n    private static Map<Long, List<StatisticsFile>> indexStatistics(List<StatisticsFile> files) {\n      return files.stream().collect(Collectors.groupingBy(StatisticsFile::snapshotId));\n    }\n\n    private static Map<Long, List<PartitionStatisticsFile>> indexPartitionStatistics(\n        List<PartitionStatisticsFile> files) {\n      return files.stream().collect(Collectors.groupingBy(PartitionStatisticsFile::snapshotId));\n    }\n\n    private boolean isAddedSnapshot(long snapshotId) {\n      return changes(MetadataUpdate.AddSnapshot.class)\n          .anyMatch(add -> add.snapshot().snapshotId() == snapshotId);\n    }\n\n    private <U extends MetadataUpdate> Stream<U> changes(Class<U> updateClass) {\n      return changes.stream().filter(updateClass::isInstance).map(updateClass::cast);\n    }\n  }\n}\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/hive/HiveCatalog.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\npackage org.apache.iceberg.hive;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport org.apache.hadoop.conf.Configurable;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.hive.conf.HiveConf;\nimport org.apache.hadoop.hive.metastore.IMetaStoreClient;\nimport org.apache.hadoop.hive.metastore.TableType;\nimport org.apache.hadoop.hive.metastore.api.AlreadyExistsException;\nimport org.apache.hadoop.hive.metastore.api.Database;\nimport org.apache.hadoop.hive.metastore.api.InvalidOperationException;\nimport org.apache.hadoop.hive.metastore.api.NoSuchObjectException;\nimport org.apache.hadoop.hive.metastore.api.PrincipalType;\nimport org.apache.hadoop.hive.metastore.api.Table;\nimport org.apache.hadoop.hive.metastore.api.UnknownDBException;\nimport org.apache.iceberg.BaseMetastoreTableOperations;\nimport org.apache.iceberg.CatalogProperties;\nimport org.apache.iceberg.CatalogUtil;\nimport org.apache.iceberg.ClientPool;\nimport org.apache.iceberg.MetadataUpdate;\nimport org.apache.iceberg.Schema;\nimport org.apache.iceberg.TableMetadata;\nimport org.apache.iceberg.TableOperations;\nimport org.apache.iceberg.Transaction;\nimport org.apache.iceberg.catalog.Namespace;\nimport org.apache.iceberg.catalog.SupportsNamespaces;\nimport org.apache.iceberg.catalog.TableIdentifier;\nimport org.apache.iceberg.exceptions.*;\nimport org.apache.iceberg.hadoop.HadoopFileIO;\nimport org.apache.iceberg.io.FileIO;\nimport org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;\nimport org.apache.iceberg.relocated.com.google.common.base.MoreObjects;\nimport org.apache.iceberg.relocated.com.google.common.base.Preconditions;\nimport org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;\nimport org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;\nimport org.apache.iceberg.relocated.com.google.common.collect.Iterables;\nimport org.apache.iceberg.relocated.com.google.common.collect.Lists;\nimport org.apache.iceberg.relocated.com.google.common.collect.Maps;\nimport org.apache.iceberg.util.LocationUtil;\nimport org.apache.iceberg.view.BaseMetastoreViewCatalog;\nimport org.apache.iceberg.view.View;\nimport org.apache.iceberg.view.ViewBuilder;\nimport org.apache.iceberg.view.ViewMetadata;\nimport org.apache.iceberg.view.ViewOperations;\nimport org.apache.thrift.TException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * This class is directly copied from iceberg 1.10.0; The only change made is\n * 1.\n *  accept metadataUpdates in constructor and pass to HiveTableOperations\n *  to support using schema/partitionSpec with field ids assigned by Delta lake\n * 2.\n *  Validate metadataLocation for validating table as Iceberg in tableExists\n */\npublic class HiveCatalog extends BaseMetastoreViewCatalog\n        implements SupportsNamespaces, Configurable {\n  public static final String LIST_ALL_TABLES = \"list-all-tables\";\n  public static final String LIST_ALL_TABLES_DEFAULT = \"false\";\n\n  public static final String HMS_TABLE_OWNER = \"hive.metastore.table.owner\";\n  public static final String HMS_DB_OWNER = \"hive.metastore.database.owner\";\n  public static final String HMS_DB_OWNER_TYPE = \"hive.metastore.database.owner-type\";\n\n  // MetastoreConf is not available with current Hive version\n  static final String HIVE_CONF_CATALOG = \"metastore.catalog.default\";\n\n  private static final Logger LOG = LoggerFactory.getLogger(HiveCatalog.class);\n\n  private String name;\n  private Configuration conf;\n  private FileIO fileIO;\n  private ClientPool<IMetaStoreClient, TException> clients;\n  private boolean listAllTables = false;\n  private Map<String, String> catalogProperties;\n\n  // HACK-HACK This is newly added\n  private List<MetadataUpdate> metadataUpdates = new ArrayList();\n\n  public HiveCatalog() {}\n\n  // HACK-HACK This is newly added\n  public void initialize(String inputName, Map<String, String> properties, List<MetadataUpdate> metadataUpdates) {\n    initialize(inputName, properties);\n    this.metadataUpdates = metadataUpdates;\n  }\n\n  @Override\n  public void initialize(String inputName, Map<String, String> properties) {\n    this.catalogProperties = ImmutableMap.copyOf(properties);\n    this.name = inputName;\n    if (conf == null) {\n      LOG.warn(\"No Hadoop Configuration was set, using the default environment Configuration\");\n      this.conf = new Configuration();\n    }\n\n    if (properties.containsKey(CatalogProperties.URI)) {\n      this.conf.set(HiveConf.ConfVars.METASTOREURIS.varname, properties.get(CatalogProperties.URI));\n    }\n\n    if (properties.containsKey(CatalogProperties.WAREHOUSE_LOCATION)) {\n      this.conf.set(\n              HiveConf.ConfVars.METASTOREWAREHOUSE.varname,\n              LocationUtil.stripTrailingSlash(properties.get(CatalogProperties.WAREHOUSE_LOCATION)));\n    }\n\n    this.listAllTables =\n            Boolean.parseBoolean(properties.getOrDefault(LIST_ALL_TABLES, LIST_ALL_TABLES_DEFAULT));\n\n    String fileIOImpl = properties.get(CatalogProperties.FILE_IO_IMPL);\n    this.fileIO =\n            fileIOImpl == null\n                    ? new HadoopFileIO(conf)\n                    : CatalogUtil.loadFileIO(fileIOImpl, properties, conf);\n\n    this.clients = new CachedClientPool(conf, properties);\n  }\n\n  @Override\n  public TableBuilder buildTable(TableIdentifier identifier, Schema schema) {\n    return new ViewAwareTableBuilder(identifier, schema);\n  }\n\n  @Override\n  public ViewBuilder buildView(TableIdentifier identifier) {\n    return new TableAwareViewBuilder(identifier);\n  }\n\n  @Override\n  public List<TableIdentifier> listTables(Namespace namespace) {\n    Preconditions.checkArgument(\n            isValidateNamespace(namespace), \"Missing database in namespace: %s\", namespace);\n    String database = namespace.level(0);\n\n    try {\n      List<String> tableNames = clients.run(client -> client.getAllTables(database));\n      List<TableIdentifier> tableIdentifiers;\n\n      if (listAllTables) {\n        tableIdentifiers =\n                tableNames.stream()\n                        .map(t -> TableIdentifier.of(namespace, t))\n                        .collect(Collectors.toList());\n      } else {\n        tableIdentifiers =\n                listIcebergTables(\n                        tableNames, namespace, BaseMetastoreTableOperations.ICEBERG_TABLE_TYPE_VALUE);\n      }\n\n      LOG.debug(\n              \"Listing of namespace: {} resulted in the following tables: {}\",\n              namespace,\n              tableIdentifiers);\n      return tableIdentifiers;\n\n    } catch (UnknownDBException e) {\n      throw new NoSuchNamespaceException(\"Namespace does not exist: %s\", namespace);\n\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to list all tables under namespace \" + namespace, e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted in call to listTables\", e);\n    }\n  }\n\n  @Override\n  public List<TableIdentifier> listViews(Namespace namespace) {\n    Preconditions.checkArgument(\n            isValidateNamespace(namespace), \"Missing database in namespace: %s\", namespace);\n\n    try {\n      String database = namespace.level(0);\n      List<String> viewNames =\n              clients.run(client -> client.getTables(database, \"*\", TableType.VIRTUAL_VIEW));\n\n      // Retrieving the Table objects from HMS in batches to avoid OOM\n      List<TableIdentifier> filteredTableIdentifiers = Lists.newArrayList();\n      Iterable<List<String>> viewNameSets = Iterables.partition(viewNames, 100);\n\n      for (List<String> viewNameSet : viewNameSets) {\n        filteredTableIdentifiers.addAll(\n                listIcebergTables(viewNameSet, namespace, HiveOperationsBase.ICEBERG_VIEW_TYPE_VALUE));\n      }\n\n      return filteredTableIdentifiers;\n    } catch (UnknownDBException e) {\n      throw new NoSuchNamespaceException(\"Namespace does not exist: %s\", namespace);\n\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to list all views under namespace \" + namespace, e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted in call to listViews\", e);\n    }\n  }\n\n  @Override\n  public String name() {\n    return name;\n  }\n\n  @Override\n  public boolean dropTable(TableIdentifier identifier, boolean purge) {\n    if (!isValidIdentifier(identifier)) {\n      return false;\n    }\n\n    String database = identifier.namespace().level(0);\n\n    TableOperations ops = newTableOps(identifier);\n    TableMetadata lastMetadata = null;\n    if (purge) {\n      try {\n        lastMetadata = ops.current();\n      } catch (NotFoundException e) {\n        LOG.warn(\n                \"Failed to load table metadata for table: {}, continuing drop without purge\",\n                identifier,\n                e);\n      }\n    }\n\n    try {\n      clients.run(\n              client -> {\n                client.dropTable(\n                        database,\n                        identifier.name(),\n                        false /* do not delete data */,\n                        false /* throw NoSuchObjectException if the table doesn't exist */);\n                return null;\n              });\n\n      if (purge && lastMetadata != null) {\n        CatalogUtil.dropTableData(ops.io(), lastMetadata);\n      }\n\n      LOG.info(\"Dropped table: {}\", identifier);\n      return true;\n\n    } catch (NoSuchTableException | NoSuchObjectException e) {\n      LOG.info(\"Skipping drop, table does not exist: {}\", identifier, e);\n      return false;\n\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to drop \" + identifier, e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted in call to dropTable\", e);\n    }\n  }\n\n  @Override\n  public boolean dropView(TableIdentifier identifier) {\n    if (!isValidIdentifier(identifier)) {\n      return false;\n    }\n\n    try {\n      String database = identifier.namespace().level(0);\n      String viewName = identifier.name();\n\n      HiveViewOperations ops = (HiveViewOperations) newViewOps(identifier);\n      ViewMetadata lastViewMetadata = null;\n      try {\n        lastViewMetadata = ops.current();\n      } catch (NotFoundException e) {\n        LOG.warn(\"Failed to load view metadata for view: {}\", identifier, e);\n      }\n\n      clients.run(\n              client -> {\n                client.dropTable(database, viewName, false, false);\n                return null;\n              });\n\n      if (lastViewMetadata != null) {\n        CatalogUtil.dropViewMetadata(ops.io(), lastViewMetadata);\n      }\n\n      LOG.info(\"Dropped view: {}\", identifier);\n      return true;\n    } catch (NoSuchObjectException e) {\n      LOG.info(\"Skipping drop, view does not exist: {}\", identifier, e);\n      return false;\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to drop view \" + identifier, e);\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted in call to dropView\", e);\n    }\n  }\n\n  @Override\n  public void renameTable(TableIdentifier from, TableIdentifier originalTo) {\n    renameTableOrView(from, originalTo, HiveOperationsBase.ContentType.TABLE);\n  }\n\n  @Override\n  public void renameView(TableIdentifier from, TableIdentifier to) {\n    renameTableOrView(from, to, HiveOperationsBase.ContentType.VIEW);\n  }\n\n  private List<TableIdentifier> listIcebergTables(\n          List<String> tableNames, Namespace namespace, String tableTypeProp)\n          throws TException, InterruptedException {\n    List<Table> tableObjects =\n            clients.run(client -> client.getTableObjectsByName(namespace.level(0), tableNames));\n    return tableObjects.stream()\n            .filter(\n                    table ->\n                            table.getParameters() != null\n                                    && tableTypeProp.equalsIgnoreCase(\n                                    table.getParameters().get(BaseMetastoreTableOperations.TABLE_TYPE_PROP)))\n            .map(table -> TableIdentifier.of(namespace, table.getTableName()))\n            .collect(Collectors.toList());\n  }\n\n  @SuppressWarnings(\"checkstyle:CyclomaticComplexity\")\n  private void renameTableOrView(\n          TableIdentifier from,\n          TableIdentifier originalTo,\n          HiveOperationsBase.ContentType contentType) {\n    Preconditions.checkArgument(isValidIdentifier(from), \"Invalid identifier: %s\", from);\n\n    TableIdentifier to = removeCatalogName(originalTo);\n    Preconditions.checkArgument(isValidIdentifier(to), \"Invalid identifier: %s\", to);\n    if (!namespaceExists(to.namespace())) {\n      throw new NoSuchNamespaceException(\n              \"Cannot rename %s to %s. Namespace does not exist: %s\", from, to, to.namespace());\n    }\n\n    if (tableExists(to)) {\n      throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n              \"Cannot rename %s to %s. Table already exists\", from, to);\n    }\n\n    if (viewExists(to)) {\n      throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n              \"Cannot rename %s to %s. View already exists\", from, to);\n    }\n\n    String toDatabase = to.namespace().level(0);\n    String fromDatabase = from.namespace().level(0);\n    String fromName = from.name();\n\n    try {\n      Table table = clients.run(client -> client.getTable(fromDatabase, fromName));\n      validateTableIsIcebergTableOrView(contentType, table, CatalogUtil.fullTableName(name, from));\n\n      table.setDbName(toDatabase);\n      table.setTableName(to.name());\n\n      clients.run(\n              client -> {\n                MetastoreUtil.alterTable(client, fromDatabase, fromName, table);\n                return null;\n              });\n\n      LOG.info(\"Renamed {} from {}, to {}\", contentType.value(), from, to);\n\n    } catch (NoSuchObjectException e) {\n      switch (contentType) {\n        case TABLE:\n          throw new NoSuchTableException(\"Cannot rename %s to %s. Table does not exist\", from, to);\n        case VIEW:\n          throw new NoSuchViewException(\"Cannot rename %s to %s. View does not exist\", from, to);\n      }\n\n    } catch (InvalidOperationException e) {\n      if (e.getMessage() != null\n              && e.getMessage().contains(String.format(\"new table %s already exists\", to))) {\n        throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n                \"Table already exists: %s\", to);\n      } else {\n        throw new RuntimeException(\"Failed to rename \" + from + \" to \" + to, e);\n      }\n\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to rename \" + from + \" to \" + to, e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted in call to rename\", e);\n    }\n  }\n\n  private void validateTableIsIcebergTableOrView(\n          HiveOperationsBase.ContentType contentType, Table table, String fullName) {\n    switch (contentType) {\n      case TABLE:\n        HiveOperationsBase.validateTableIsIceberg(table, fullName);\n        break;\n      case VIEW:\n        HiveOperationsBase.validateTableIsIcebergView(table, fullName);\n    }\n  }\n\n  /**\n   * Check whether table or metadata table exists.\n   *\n   * <p>Note: If a hive table with the same identifier exists in catalog, this method will return\n   * {@code false}.\n   *\n   * @param identifier a table identifier\n   * @return true if the table exists, false otherwise\n   */\n  @Override\n  public boolean tableExists(TableIdentifier identifier) {\n    TableIdentifier baseTableIdentifier = identifier;\n    if (!isValidIdentifier(identifier)) {\n      if (!isValidMetadataIdentifier(identifier)) {\n        return false;\n      } else {\n        baseTableIdentifier = TableIdentifier.of(identifier.namespace().levels());\n      }\n    }\n\n    String database = baseTableIdentifier.namespace().level(0);\n    String tableName = baseTableIdentifier.name();\n    try {\n      Table table = clients.run(client -> client.getTable(database, tableName));\n      // HACK-HACK This is modified\n      validateTableIsIceberg(table, fullTableName(name, baseTableIdentifier));\n      return true;\n    } catch (NoSuchTableException | NoSuchObjectException e) {\n      return false;\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to check table existence of \" + baseTableIdentifier, e);\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to check table existence of \" + baseTableIdentifier, e);\n    }\n  }\n\n  // HACK-HACK This is added\n  private void validateTableIsIceberg(Table table, String fullName) {\n    HiveOperationsBase.validateTableIsIceberg(table, fullName);\n    String metadataLocation = table.getParameters().get(BaseMetastoreTableOperations.METADATA_LOCATION_PROP);\n    NoSuchIcebergTableException.check(\n            metadataLocation != null,\n            \"Not an iceberg table: %s, metadataLocation is null\",\n            fullName);\n  }\n\n  @Override\n  public boolean viewExists(TableIdentifier viewIdentifier) {\n    if (!isValidIdentifier(viewIdentifier)) {\n      return false;\n    }\n\n    String database = viewIdentifier.namespace().level(0);\n    String viewName = viewIdentifier.name();\n    try {\n      Table table = clients.run(client -> client.getTable(database, viewName));\n      HiveOperationsBase.validateTableIsIcebergView(table, fullTableName(name, viewIdentifier));\n      return true;\n    } catch (NoSuchIcebergViewException | NoSuchObjectException e) {\n      return false;\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to check view existence of \" + viewIdentifier, e);\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to check view existence of \" + viewIdentifier, e);\n    }\n  }\n\n  @Override\n  public void createNamespace(Namespace namespace, Map<String, String> meta) {\n    Preconditions.checkArgument(\n            !namespace.isEmpty(), \"Cannot create namespace with invalid name: %s\", namespace);\n    Preconditions.checkArgument(\n            isValidateNamespace(namespace),\n            \"Cannot support multi part namespace in Hive Metastore: %s\",\n            namespace);\n    Preconditions.checkArgument(\n            meta.get(HMS_DB_OWNER_TYPE) == null || meta.get(HMS_DB_OWNER) != null,\n            \"Create namespace setting %s without setting %s is not allowed\",\n            HMS_DB_OWNER_TYPE,\n            HMS_DB_OWNER);\n    try {\n      clients.run(\n              client -> {\n                client.createDatabase(convertToDatabase(namespace, meta));\n                return null;\n              });\n\n      LOG.info(\"Created namespace: {}\", namespace);\n\n    } catch (AlreadyExistsException e) {\n      throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n              e, \"Namespace already exists: %s\", namespace);\n\n    } catch (TException e) {\n      throw new RuntimeException(\n              \"Failed to create namespace \" + namespace + \" in Hive Metastore\", e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to createDatabase(name) \" + namespace + \" in Hive Metastore\", e);\n    }\n  }\n\n  @Override\n  public List<Namespace> listNamespaces(Namespace namespace) {\n    if (!namespace.isEmpty() && (!isValidateNamespace(namespace) || !namespaceExists(namespace))) {\n      throw new NoSuchNamespaceException(\"Namespace does not exist: %s\", namespace);\n    }\n    if (!namespace.isEmpty()) {\n      return ImmutableList.of();\n    }\n    try {\n      List<Namespace> namespaces =\n              clients.run(IMetaStoreClient::getAllDatabases).stream()\n                      .map(Namespace::of)\n                      .collect(Collectors.toList());\n\n      LOG.debug(\"Listing namespace {} returned tables: {}\", namespace, namespaces);\n      return namespaces;\n\n    } catch (TException e) {\n      throw new RuntimeException(\n              \"Failed to list all namespace: \" + namespace + \" in Hive Metastore\", e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to getAllDatabases() \" + namespace + \" in Hive Metastore\", e);\n    }\n  }\n\n  @Override\n  public boolean dropNamespace(Namespace namespace) {\n    if (!isValidateNamespace(namespace)) {\n      return false;\n    }\n\n    try {\n      clients.run(\n              client -> {\n                client.dropDatabase(\n                        namespace.level(0),\n                        false /* deleteData */,\n                        false /* ignoreUnknownDb */,\n                        false /* cascade */);\n                return null;\n              });\n\n      LOG.info(\"Dropped namespace: {}\", namespace);\n      return true;\n\n    } catch (InvalidOperationException e) {\n      throw new NamespaceNotEmptyException(\n              e, \"Namespace %s is not empty. One or more tables exist.\", namespace);\n\n    } catch (NoSuchObjectException e) {\n      return false;\n\n    } catch (TException e) {\n      throw new RuntimeException(\"Failed to drop namespace \" + namespace + \" in Hive Metastore\", e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to drop dropDatabase(name) \" + namespace + \" in Hive Metastore\", e);\n    }\n  }\n\n  @Override\n  public boolean setProperties(Namespace namespace, Map<String, String> properties) {\n    Preconditions.checkArgument(\n            (properties.get(HMS_DB_OWNER_TYPE) == null) == (properties.get(HMS_DB_OWNER) == null),\n            \"Setting %s and %s has to be performed together or not at all\",\n            HMS_DB_OWNER_TYPE,\n            HMS_DB_OWNER);\n    Map<String, String> parameter = Maps.newHashMap();\n\n    parameter.putAll(loadNamespaceMetadata(namespace));\n    parameter.putAll(properties);\n    Database database = convertToDatabase(namespace, parameter);\n\n    alterHiveDataBase(namespace, database);\n    LOG.debug(\"Successfully set properties {} for {}\", properties.keySet(), namespace);\n\n    // Always successful, otherwise exception is thrown\n    return true;\n  }\n\n  @Override\n  public boolean removeProperties(Namespace namespace, Set<String> properties) {\n    Preconditions.checkArgument(\n            properties.contains(HMS_DB_OWNER_TYPE) == properties.contains(HMS_DB_OWNER),\n            \"Removing %s and %s has to be performed together or not at all\",\n            HMS_DB_OWNER_TYPE,\n            HMS_DB_OWNER);\n    Map<String, String> parameter = Maps.newHashMap();\n\n    parameter.putAll(loadNamespaceMetadata(namespace));\n    properties.forEach(key -> parameter.put(key, null));\n    Database database = convertToDatabase(namespace, parameter);\n\n    alterHiveDataBase(namespace, database);\n    LOG.debug(\"Successfully removed properties {} from {}\", properties, namespace);\n\n    // Always successful, otherwise exception is thrown\n    return true;\n  }\n\n  private void alterHiveDataBase(Namespace namespace, Database database) {\n    try {\n      clients.run(\n              client -> {\n                client.alterDatabase(namespace.level(0), database);\n                return null;\n              });\n\n    } catch (NoSuchObjectException | UnknownDBException e) {\n      throw new NoSuchNamespaceException(e, \"Namespace does not exist: %s\", namespace);\n\n    } catch (TException e) {\n      throw new RuntimeException(\n              \"Failed to list namespace under namespace: \" + namespace + \" in Hive Metastore\", e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to getDatabase(name) \" + namespace + \" in Hive Metastore\", e);\n    }\n  }\n\n  @Override\n  public Map<String, String> loadNamespaceMetadata(Namespace namespace) {\n    if (!isValidateNamespace(namespace)) {\n      throw new NoSuchNamespaceException(\"Namespace does not exist: %s\", namespace);\n    }\n\n    try {\n      Database database = clients.run(client -> client.getDatabase(namespace.level(0)));\n      Map<String, String> metadata = convertToMetadata(database);\n      LOG.debug(\"Loaded metadata for namespace {} found {}\", namespace, metadata.keySet());\n      return metadata;\n\n    } catch (NoSuchObjectException | UnknownDBException e) {\n      throw new NoSuchNamespaceException(e, \"Namespace does not exist: %s\", namespace);\n\n    } catch (TException e) {\n      throw new RuntimeException(\n              \"Failed to list namespace under namespace: \" + namespace + \" in Hive Metastore\", e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\n              \"Interrupted in call to getDatabase(name) \" + namespace + \" in Hive Metastore\", e);\n    }\n  }\n\n  @Override\n  protected boolean isValidIdentifier(TableIdentifier tableIdentifier) {\n    return tableIdentifier.namespace().levels().length == 1;\n  }\n\n  private TableIdentifier removeCatalogName(TableIdentifier to) {\n    if (isValidIdentifier(to)) {\n      return to;\n    }\n\n    // check if the identifier includes the catalog name and remove it\n    if (to.namespace().levels().length == 2 && name().equalsIgnoreCase(to.namespace().level(0))) {\n      return TableIdentifier.of(Namespace.of(to.namespace().level(1)), to.name());\n    }\n\n    // return the original unmodified\n    return to;\n  }\n\n  private boolean isValidateNamespace(Namespace namespace) {\n    return namespace.levels().length == 1;\n  }\n\n  @Override\n  public TableOperations newTableOps(TableIdentifier tableIdentifier) {\n    String dbName = tableIdentifier.namespace().level(0);\n    String tableName = tableIdentifier.name();\n    // HACK-HACK This is modified\n    return new HiveTableOperations(conf, clients, fileIO, name, dbName, tableName, metadataUpdates);\n  }\n\n  @Override\n  protected ViewOperations newViewOps(TableIdentifier identifier) {\n    return new HiveViewOperations(conf, clients, fileIO, name, identifier);\n  }\n\n  @Override\n  protected String defaultWarehouseLocation(TableIdentifier tableIdentifier) {\n    // This is a little edgy since we basically duplicate the HMS location generation logic.\n    // Sadly I do not see a good way around this if we want to keep the order of events, like:\n    // - Create meta files\n    // - Create the metadata in HMS, and this way committing the changes\n\n    // Create a new location based on the namespace / database if it is set on database level\n    try {\n      Database databaseData =\n              clients.run(client -> client.getDatabase(tableIdentifier.namespace().levels()[0]));\n      if (databaseData.getLocationUri() != null) {\n        // If the database location is set use it as a base.\n        return String.format(\"%s/%s\", databaseData.getLocationUri(), tableIdentifier.name());\n      }\n\n    } catch (NoSuchObjectException e) {\n      throw new NoSuchNamespaceException(\n              e, \"Namespace does not exist: %s\", tableIdentifier.namespace().levels()[0]);\n    } catch (TException e) {\n      throw new RuntimeException(\n              String.format(\"Metastore operation failed for %s\", tableIdentifier), e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted during commit\", e);\n    }\n\n    // Otherwise, stick to the {WAREHOUSE_DIR}/{DB_NAME}.db/{TABLE_NAME} path\n    String databaseLocation = databaseLocation(tableIdentifier.namespace().levels()[0]);\n    return String.format(\"%s/%s\", databaseLocation, tableIdentifier.name());\n  }\n\n  private String databaseLocation(String databaseName) {\n    String warehouseLocation = conf.get(HiveConf.ConfVars.METASTOREWAREHOUSE.varname);\n    Preconditions.checkNotNull(\n            warehouseLocation, \"Warehouse location is not set: hive.metastore.warehouse.dir=null\");\n    warehouseLocation = LocationUtil.stripTrailingSlash(warehouseLocation);\n    return String.format(\"%s/%s.db\", warehouseLocation, databaseName);\n  }\n\n  private Map<String, String> convertToMetadata(Database database) {\n\n    Map<String, String> meta = Maps.newHashMap();\n\n    meta.putAll(database.getParameters());\n    meta.put(\"location\", database.getLocationUri());\n    if (database.getDescription() != null) {\n      meta.put(\"comment\", database.getDescription());\n    }\n    if (database.getOwnerName() != null) {\n      meta.put(HMS_DB_OWNER, database.getOwnerName());\n      if (database.getOwnerType() != null) {\n        meta.put(HMS_DB_OWNER_TYPE, database.getOwnerType().name());\n      }\n    }\n\n    return meta;\n  }\n\n  Database convertToDatabase(Namespace namespace, Map<String, String> meta) {\n    if (!isValidateNamespace(namespace)) {\n      throw new NoSuchNamespaceException(\"Namespace does not exist: %s\", namespace);\n    }\n\n    Database database = new Database();\n    Map<String, String> parameter = Maps.newHashMap();\n\n    database.setName(namespace.level(0));\n    database.setLocationUri(databaseLocation(namespace.level(0)));\n\n    meta.forEach(\n            (key, value) -> {\n              if (key.equals(\"comment\")) {\n                database.setDescription(value);\n              } else if (key.equals(\"location\")) {\n                database.setLocationUri(value);\n              } else if (key.equals(HMS_DB_OWNER)) {\n                database.setOwnerName(value);\n              } else if (key.equals(HMS_DB_OWNER_TYPE) && value != null) {\n                database.setOwnerType(PrincipalType.valueOf(value));\n              } else {\n                if (value != null) {\n                  parameter.put(key, value);\n                }\n              }\n            });\n\n    if (database.getOwnerName() == null) {\n      database.setOwnerName(HiveHadoopUtil.currentUser());\n      database.setOwnerType(PrincipalType.USER);\n    }\n\n    database.setParameters(parameter);\n\n    return database;\n  }\n\n  @Override\n  public String toString() {\n    return MoreObjects.toStringHelper(this)\n            .add(\"name\", name)\n            .add(\"uri\", this.conf == null ? \"\" : this.conf.get(HiveConf.ConfVars.METASTOREURIS.varname))\n            .toString();\n  }\n\n  @Override\n  public void setConf(Configuration conf) {\n    this.conf = new Configuration(conf);\n  }\n\n  @Override\n  public Configuration getConf() {\n    return conf;\n  }\n\n  @Override\n  protected Map<String, String> properties() {\n    return catalogProperties == null ? ImmutableMap.of() : catalogProperties;\n  }\n\n  @VisibleForTesting\n  void setListAllTables(boolean listAllTables) {\n    this.listAllTables = listAllTables;\n  }\n\n  @VisibleForTesting\n  ClientPool<IMetaStoreClient, TException> clientPool() {\n    return clients;\n  }\n\n  /**\n   * The purpose of this class is to add view detection only for Hive-Specific tables. Hive catalog\n   * follows checks at different levels: 1. During refresh, it validates if the table is an iceberg\n   * table or not. 2. During commit, it validates if there is any concurrent commit with table or\n   * table-name already exists. This class helps to do the validation on an early basis.\n   */\n  private class ViewAwareTableBuilder extends BaseMetastoreViewCatalogTableBuilder {\n\n    private final TableIdentifier identifier;\n\n    private ViewAwareTableBuilder(TableIdentifier identifier, Schema schema) {\n      super(identifier, schema);\n      this.identifier = identifier;\n    }\n\n    @Override\n    public Transaction createOrReplaceTransaction() {\n      if (viewExists(identifier)) {\n        throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n                \"View with same name already exists: %s\", identifier);\n      }\n      return super.createOrReplaceTransaction();\n    }\n\n    @Override\n    public org.apache.iceberg.Table create() {\n      if (viewExists(identifier)) {\n        throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n                \"View with same name already exists: %s\", identifier);\n      }\n      return super.create();\n    }\n  }\n\n  /**\n   * The purpose of this class is to add table detection only for Hive-Specific view. Hive catalog\n   * follows checks at different levels: 1. During refresh, it validates if the view is an iceberg\n   * view or not. 2. During commit, it validates if there is any concurrent commit with view or\n   * view-name already exists. This class helps to do the validation on an early basis.\n   */\n  private class TableAwareViewBuilder extends BaseViewBuilder {\n\n    private final TableIdentifier identifier;\n\n    private TableAwareViewBuilder(TableIdentifier identifier) {\n      super(identifier);\n      this.identifier = identifier;\n    }\n\n    @Override\n    public View createOrReplace() {\n      if (tableExists(identifier)) {\n        throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n                \"Table with same name already exists: %s\", identifier);\n      }\n      return super.createOrReplace();\n    }\n\n    @Override\n    public View create() {\n      if (tableExists(identifier)) {\n        throw new org.apache.iceberg.exceptions.AlreadyExistsException(\n                \"Table with same name already exists: %s\", identifier);\n      }\n      return super.create();\n    }\n  }\n}"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\npackage org.apache.iceberg.hive;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.hive.common.StatsSetupConst;\nimport org.apache.hadoop.hive.metastore.IMetaStoreClient;\nimport org.apache.hadoop.hive.metastore.TableType;\nimport org.apache.hadoop.hive.metastore.api.FieldSchema;\nimport org.apache.hadoop.hive.metastore.api.InvalidObjectException;\nimport org.apache.hadoop.hive.metastore.api.NoSuchObjectException;\nimport org.apache.hadoop.hive.metastore.api.StorageDescriptor;\nimport org.apache.hadoop.hive.metastore.api.Table;\nimport org.apache.iceberg.BaseMetastoreOperations;\nimport org.apache.iceberg.BaseMetastoreTableOperations;\nimport org.apache.iceberg.ClientPool;\nimport org.apache.iceberg.MetadataUpdate;\nimport org.apache.iceberg.PartitionSpec;\nimport org.apache.iceberg.Schema;\nimport org.apache.iceberg.TableMetadata;\nimport org.apache.iceberg.TableProperties;\nimport org.apache.iceberg.exceptions.AlreadyExistsException;\nimport org.apache.iceberg.exceptions.CommitFailedException;\nimport org.apache.iceberg.exceptions.CommitStateUnknownException;\nimport org.apache.iceberg.exceptions.NoSuchIcebergTableException;\nimport org.apache.iceberg.exceptions.NoSuchTableException;\nimport org.apache.iceberg.exceptions.ValidationException;\nimport org.apache.iceberg.hadoop.ConfigProperties;\nimport org.apache.iceberg.io.FileIO;\nimport org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;\nimport org.apache.thrift.TException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * TODO we should be able to extract some more commonalities to BaseMetastoreTableOperations to\n * avoid code duplication between this class and Metacat Tables.\n * This class is directly copied from iceberg 1.10.0; The only change made are\n *  1) accept metadataUpdates in constructor apply those before writing metadata\n *  to support using schema/partitionSpec with field ids assigned by Delta lake;\n *  2) handle NoSuchIcebergTableException in doRefresh to regard a table entry\n *  that exists in HMS but does not have \"table_type\" = \"ICEBERG\" as table does\n *  not exist, so Delta lake can correctly start create table transaction\n */\npublic class HiveTableOperations extends BaseMetastoreTableOperations\n        implements HiveOperationsBase {\n  private static final Logger LOG = LoggerFactory.getLogger(HiveTableOperations.class);\n\n  private static final String HIVE_ICEBERG_METADATA_REFRESH_MAX_RETRIES =\n          \"iceberg.hive.metadata-refresh-max-retries\";\n  private static final int HIVE_ICEBERG_METADATA_REFRESH_MAX_RETRIES_DEFAULT = 2;\n\n  private final String fullName;\n  private final String catalogName;\n  private final String database;\n  private final String tableName;\n  private final Configuration conf;\n  private final long maxHiveTablePropertySize;\n  private final int metadataRefreshMaxRetries;\n  private final FileIO fileIO;\n  private final ClientPool<IMetaStoreClient, TException> metaClients;\n  // HACK-HACK This is newly added\n  private List<MetadataUpdate> metadataUpdates = new ArrayList();\n  // HACK-HACK This is newly added\n  protected HiveTableOperations(\n          Configuration conf,\n          ClientPool<IMetaStoreClient, TException> metaClients,\n          FileIO fileIO,\n          String catalogName,\n          String database,\n          String table,\n          List<MetadataUpdate> metadataUpdates) {\n    this(conf, metaClients, fileIO, catalogName, database, table);\n    this.metadataUpdates = metadataUpdates;\n  }\n\n  protected HiveTableOperations(\n          Configuration conf,\n          ClientPool<IMetaStoreClient, TException> metaClients,\n          FileIO fileIO,\n          String catalogName,\n          String database,\n          String table) {\n    this.conf = conf;\n    this.metaClients = metaClients;\n    this.fileIO = fileIO;\n    this.fullName = catalogName + \".\" + database + \".\" + table;\n    this.catalogName = catalogName;\n    this.database = database;\n    this.tableName = table;\n    this.metadataRefreshMaxRetries =\n            conf.getInt(\n                    HIVE_ICEBERG_METADATA_REFRESH_MAX_RETRIES,\n                    HIVE_ICEBERG_METADATA_REFRESH_MAX_RETRIES_DEFAULT);\n    this.maxHiveTablePropertySize =\n            conf.getLong(HIVE_TABLE_PROPERTY_MAX_SIZE, HIVE_TABLE_PROPERTY_MAX_SIZE_DEFAULT);\n  }\n\n  @Override\n  protected String tableName() {\n    return fullName;\n  }\n\n  @Override\n  public FileIO io() {\n    return fileIO;\n  }\n\n  @Override\n  protected void doRefresh() {\n    String metadataLocation = null;\n    try {\n      Table table = metaClients.run(client -> client.getTable(database, tableName));\n\n      // Check if we are trying to load an Iceberg View as a Table\n      HiveOperationsBase.validateIcebergViewNotLoadedAsIcebergTable(table, fullName);\n      // Check if it is a valid Iceberg Table\n      HiveOperationsBase.validateTableIsIceberg(table, fullName);\n\n      metadataLocation = table.getParameters().get(METADATA_LOCATION_PROP);\n\n    } catch (NoSuchObjectException e) {\n      if (currentMetadataLocation() != null) {\n        throw new NoSuchTableException(\"No such table: %s.%s\", database, tableName);\n      }\n    } catch (NoSuchIcebergTableException e) { // HACK-HACK This is newly added\n      // NoSuchIcebergTableException is throw when table exists in catalog but not with\n      // table_type=iceberg; in that case we want to swallow so createTable\n      // txn can proceed with creating the iceberg table/metadata and set table_type=iceberg\n      if (currentMetadataLocation() != null) {\n        throw new NoSuchTableException(\"No such table: %s.%s\", database, tableName);\n      }\n    } catch (TException e) {\n      String errMsg =\n              String.format(\"Failed to get table info from metastore %s.%s\", database, tableName);\n      throw new RuntimeException(errMsg, e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted during refresh\", e);\n    }\n\n    refreshFromMetadataLocation(metadataLocation, metadataRefreshMaxRetries);\n  }\n\n  @SuppressWarnings({\"checkstyle:CyclomaticComplexity\", \"MethodLength\"})\n  @Override\n  protected void doCommit(TableMetadata base, TableMetadata metadata) {\n    boolean newTable = base == null;\n    // HACK-HACK This is newly added\n    // Apply metadata updates so adjustedMetadata has field id and partition spec created\n    // from Delta lake\n    TableMetadata.Builder builder = TableMetadata.buildFrom(metadata);\n    Schema lastAddedSchema = metadata.schema();\n    for (MetadataUpdate update : metadataUpdates) {\n      if (update instanceof MetadataUpdate.AddSchema) {\n        MetadataUpdate.AddSchema addSchema = (MetadataUpdate.AddSchema) update;\n        builder.setCurrentSchema(addSchema.schema(), addSchema.lastColumnId());\n        lastAddedSchema = addSchema.schema();\n      } else if (update instanceof MetadataUpdate.AddPartitionSpec) {\n        // regard AddPartitionSpec as replace all existing specs as Delta Uniform only\n        // support one partition spec\n        PartitionSpec specToAdd = ((MetadataUpdate.AddPartitionSpec) update).spec().bind(lastAddedSchema);\n        if (!specToAdd.compatibleWith(metadata.spec())) {\n          HashSet<Integer> idsToRemove = new HashSet();\n          for (PartitionSpec spec : metadata.specs()) {\n            idsToRemove.add(spec.specId());\n          }\n          builder.setDefaultPartitionSpec(specToAdd);\n          MetadataUpdate.RemovePartitionSpecs removeSpecs = new MetadataUpdate.RemovePartitionSpecs(idsToRemove);\n          removeSpecs.applyTo(builder);\n        }\n      } else {\n        update.applyTo(builder);\n      }\n    }\n    TableMetadata adjustedMetadata = builder.build();\n    // HACK-HACK This is modified\n    String newMetadataLocation = writeNewMetadataIfRequired(newTable, adjustedMetadata);\n    boolean hiveEngineEnabled = hiveEngineEnabled(metadata, conf);\n    boolean keepHiveStats = conf.getBoolean(ConfigProperties.KEEP_HIVE_STATS, false);\n\n    BaseMetastoreOperations.CommitStatus commitStatus =\n            BaseMetastoreOperations.CommitStatus.FAILURE;\n    boolean updateHiveTable = false;\n\n    HiveLock lock = lockObject(base);\n    try {\n      lock.lock();\n\n      Table tbl = loadHmsTable();\n\n      if (tbl != null) {\n        // If we try to create the table but the metadata location is already set, then we had a\n        // concurrent commit\n        if (newTable\n                && tbl.getParameters().get(BaseMetastoreTableOperations.METADATA_LOCATION_PROP)\n                != null) {\n          if (TableType.VIRTUAL_VIEW.name().equalsIgnoreCase(tbl.getTableType())) {\n            throw new AlreadyExistsException(\n                    \"View with same name already exists: %s.%s\", database, tableName);\n          }\n          throw new AlreadyExistsException(\"Table already exists: %s.%s\", database, tableName);\n        }\n\n        updateHiveTable = true;\n        LOG.debug(\"Committing existing table: {}\", fullName);\n      } else {\n        tbl =\n                newHmsTable(\n                        metadata.property(HiveCatalog.HMS_TABLE_OWNER, HiveHadoopUtil.currentUser()));\n        LOG.debug(\"Committing new table: {}\", fullName);\n      }\n\n      // HACK-HACK This is newely added\n      StorageDescriptor newsd = HiveOperationsBase.storageDescriptor(\n              adjustedMetadata.schema(),\n              adjustedMetadata.location(),\n              hiveEngineEnabled);\n      // HACK-HACK This is newely added: use storage descriptor from Delta\n      newsd.getSerdeInfo().setParameters(tbl.getSd().getSerdeInfo().getParameters());\n      tbl.setSd(newsd);\n      // HACK-HACK This is newely added: set schema to be empty to match Delta behavior\n      tbl.getSd().setCols(Collections.singletonList(new FieldSchema(\"col\", \"array<string>\", \"\")));\n\n      String metadataLocation = tbl.getParameters().get(METADATA_LOCATION_PROP);\n      String baseMetadataLocation = base != null ? base.metadataFileLocation() : null;\n      if (!Objects.equals(baseMetadataLocation, metadataLocation)) {\n        throw new CommitFailedException(\n                \"Cannot commit: Base metadata location '%s' is not same as the current table metadata location '%s' for %s.%s\",\n                baseMetadataLocation, metadataLocation, database, tableName);\n      }\n\n      // get Iceberg props that have been removed\n      Set<String> removedProps = Collections.emptySet();\n      if (base != null) {\n        removedProps =\n                base.properties().keySet().stream()\n                        .filter(key -> !metadata.properties().containsKey(key))\n                        .collect(Collectors.toSet());\n      }\n\n      HMSTablePropertyHelper.updateHmsTableForIcebergTable(\n              newMetadataLocation,\n              tbl,\n              metadata,\n              removedProps,\n              hiveEngineEnabled,\n              maxHiveTablePropertySize,\n              currentMetadataLocation());\n\n      if (!keepHiveStats) {\n        tbl.getParameters().remove(StatsSetupConst.COLUMN_STATS_ACCURATE);\n        tbl.getParameters().put(StatsSetupConst.DO_NOT_UPDATE_STATS, StatsSetupConst.TRUE);\n      }\n\n      lock.ensureActive();\n\n      try {\n        persistTable(\n                tbl, updateHiveTable, hiveLockEnabled(base, conf) ? null : baseMetadataLocation);\n        lock.ensureActive();\n\n        commitStatus = BaseMetastoreOperations.CommitStatus.SUCCESS;\n      } catch (LockException le) {\n        commitStatus = BaseMetastoreOperations.CommitStatus.UNKNOWN;\n        throw new CommitStateUnknownException(\n                \"Failed to heartbeat for hive lock while \"\n                        + \"committing changes. This can lead to a concurrent commit attempt be able to overwrite this commit. \"\n                        + \"Please check the commit history. If you are running into this issue, try reducing \"\n                        + \"iceberg.hive.lock-heartbeat-interval-ms.\",\n                le);\n      } catch (org.apache.hadoop.hive.metastore.api.AlreadyExistsException e) {\n        throw new AlreadyExistsException(e, \"Table already exists: %s.%s\", database, tableName);\n\n      } catch (InvalidObjectException e) {\n        throw new ValidationException(e, \"Invalid Hive object for %s.%s\", database, tableName);\n\n      } catch (CommitFailedException | CommitStateUnknownException e) {\n        throw e;\n\n      } catch (Throwable e) {\n        if (e.getMessage() != null\n                && e.getMessage().contains(\"Table/View 'HIVE_LOCKS' does not exist\")) {\n          throw new RuntimeException(\n                  \"Failed to acquire locks from metastore because the underlying metastore \"\n                          + \"table 'HIVE_LOCKS' does not exist. This can occur when using an embedded metastore which does not \"\n                          + \"support transactions. To fix this use an alternative metastore.\",\n                  e);\n        }\n\n        commitStatus = BaseMetastoreOperations.CommitStatus.UNKNOWN;\n        if (e.getMessage() != null\n                && e.getMessage()\n                .contains(\n                        \"The table has been modified. The parameter value for key '\"\n                                + HiveTableOperations.METADATA_LOCATION_PROP\n                                + \"' is\")) {\n          // It's possible the HMS client incorrectly retries a successful operation, due to network\n          // issue for example, and triggers this exception. So we need double-check to make sure\n          // this is really a concurrent modification. Hitting this exception means no pending\n          // requests, if any, can succeed later, so it's safe to check status in strict mode\n          commitStatus = checkCommitStatusStrict(newMetadataLocation, metadata);\n          if (commitStatus == BaseMetastoreOperations.CommitStatus.FAILURE) {\n            throw new CommitFailedException(\n                    e, \"The table %s.%s has been modified concurrently\", database, tableName);\n          }\n        } else {\n          LOG.error(\n                  \"Cannot tell if commit to {}.{} succeeded, attempting to reconnect and check.\",\n                  database,\n                  tableName,\n                  e);\n          commitStatus = checkCommitStatus(newMetadataLocation, metadata);\n        }\n\n        switch (commitStatus) {\n          case SUCCESS:\n            break;\n          case FAILURE:\n            throw e;\n          case UNKNOWN:\n            throw new CommitStateUnknownException(e);\n        }\n      }\n    } catch (TException e) {\n      throw new RuntimeException(\n              String.format(\"Metastore operation failed for %s.%s\", database, tableName), e);\n\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new RuntimeException(\"Interrupted during commit\", e);\n\n    } catch (LockException e) {\n      throw new CommitFailedException(e);\n\n    } finally {\n      HiveOperationsBase.cleanupMetadataAndUnlock(io(), commitStatus, newMetadataLocation, lock);\n    }\n\n    LOG.info(\n            \"Committed to table {} with the new metadata location {}\", fullName, newMetadataLocation);\n  }\n\n  @Override\n  public long maxHiveTablePropertySize() {\n    return maxHiveTablePropertySize;\n  }\n\n  @Override\n  public String database() {\n    return database;\n  }\n\n  @Override\n  public String table() {\n    return tableName;\n  }\n\n  @Override\n  public TableType tableType() {\n    return TableType.EXTERNAL_TABLE;\n  }\n\n  @Override\n  public ClientPool<IMetaStoreClient, TException> metaClients() {\n    return metaClients;\n  }\n\n  /**\n   * Returns if the hive engine related values should be enabled on the table, or not.\n   *\n   * <p>The decision is made like this:\n   *\n   * <ol>\n   *   <li>Table property value {@link TableProperties#ENGINE_HIVE_ENABLED}\n   *   <li>If the table property is not set then check the hive-site.xml property value {@link\n   *       ConfigProperties#ENGINE_HIVE_ENABLED}\n   *   <li>If none of the above is enabled then use the default value {@link\n   *       TableProperties#ENGINE_HIVE_ENABLED_DEFAULT}\n   * </ol>\n   *\n   * @param metadata Table metadata to use\n   * @param conf The hive configuration to use\n   * @return if the hive engine related values should be enabled or not\n   */\n  private static boolean hiveEngineEnabled(TableMetadata metadata, Configuration conf) {\n    if (metadata.properties().get(TableProperties.ENGINE_HIVE_ENABLED) != null) {\n      // We know that the property is set, so default value will not be used,\n      return metadata.propertyAsBoolean(TableProperties.ENGINE_HIVE_ENABLED, false);\n    }\n\n    return conf.getBoolean(\n            ConfigProperties.ENGINE_HIVE_ENABLED, TableProperties.ENGINE_HIVE_ENABLED_DEFAULT);\n  }\n\n  /**\n   * Returns if the hive locking should be enabled on the table, or not.\n   *\n   * <p>The decision is made like this:\n   *\n   * <ol>\n   *   <li>Table property value {@link TableProperties#HIVE_LOCK_ENABLED}\n   *   <li>If the table property is not set then check the hive-site.xml property value {@link\n   *       ConfigProperties#LOCK_HIVE_ENABLED}\n   *   <li>If none of the above is enabled then use the default value {@link\n   *       TableProperties#HIVE_LOCK_ENABLED_DEFAULT}\n   * </ol>\n   *\n   * @param metadata Table metadata to use\n   * @param conf The hive configuration to use\n   * @return if the hive engine related values should be enabled or not\n   */\n  private static boolean hiveLockEnabled(TableMetadata metadata, Configuration conf) {\n    if (metadata != null && metadata.properties().get(TableProperties.HIVE_LOCK_ENABLED) != null) {\n      // We know that the property is set, so default value will not be used,\n      return metadata.propertyAsBoolean(TableProperties.HIVE_LOCK_ENABLED, false);\n    }\n\n    return conf.getBoolean(\n            ConfigProperties.LOCK_HIVE_ENABLED, TableProperties.HIVE_LOCK_ENABLED_DEFAULT);\n  }\n\n  @VisibleForTesting\n  HiveLock lockObject(TableMetadata metadata) {\n    if (hiveLockEnabled(metadata, conf)) {\n      return new MetastoreLock(conf, metaClients, catalogName, database, tableName);\n    } else {\n      return new NoLock();\n    }\n  }\n}\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/rest/RESTFileScanTaskParser.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n/*\n * DELTA LAKE PATCH:\n * This file is patched from Apache Iceberg 1.10.0 to fix a bug in the fromJson method.\n *\n * Bug: The original code crashes with NoSuchElementException when parsing JSON responses\n * containing empty delete-file-references arrays (delete-file-references: []).\n *\n * This occurs because Collections.max(indices) throws NoSuchElementException on empty\n * collections, but empty delete-file-references arrays are valid per the Iceberg REST spec.\n *\n * Fix: Added check for indices.isEmpty() before calling Collections.max() at line 93.\n *\n * Without this patch, the Delta Lake server-side scan planning client cannot parse responses\n * for tables with data files, as the Iceberg REST server always includes the\n * delete-file-references field even when empty.\n */\npackage org.apache.iceberg.rest;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.apache.iceberg.BaseFileScanTask;\nimport org.apache.iceberg.ContentFileParser;\nimport org.apache.iceberg.DataFile;\nimport org.apache.iceberg.DeleteFile;\nimport org.apache.iceberg.FileScanTask;\nimport org.apache.iceberg.PartitionSpec;\nimport org.apache.iceberg.PartitionSpecParser;\nimport org.apache.iceberg.SchemaParser;\nimport org.apache.iceberg.expressions.Expression;\nimport org.apache.iceberg.expressions.ExpressionParser;\nimport org.apache.iceberg.expressions.ResidualEvaluator;\nimport org.apache.iceberg.relocated.com.google.common.base.Preconditions;\nimport org.apache.iceberg.util.JsonUtil;\n\nclass RESTFileScanTaskParser {\n  private static final String DATA_FILE = \"data-file\";\n  private static final String DELETE_FILE_REFERENCES = \"delete-file-references\";\n  private static final String RESIDUAL_FILTER = \"residual-filter\";\n\n  private RESTFileScanTaskParser() {}\n\n  public static void toJson(\n      FileScanTask fileScanTask,\n      Set<Integer> deleteFileReferences,\n      PartitionSpec partitionSpec,\n      JsonGenerator generator)\n      throws IOException {\n    Preconditions.checkArgument(fileScanTask != null, \"Invalid file scan task: null\");\n    Preconditions.checkArgument(generator != null, \"Invalid JSON generator: null\");\n\n    generator.writeStartObject();\n    generator.writeFieldName(DATA_FILE);\n    ContentFileParser.toJson(fileScanTask.file(), partitionSpec, generator);\n    if (deleteFileReferences != null) {\n      JsonUtil.writeIntegerArray(DELETE_FILE_REFERENCES, deleteFileReferences, generator);\n    }\n\n    if (fileScanTask.residual() != null) {\n      generator.writeFieldName(RESIDUAL_FILTER);\n      ExpressionParser.toJson(fileScanTask.residual(), generator);\n    }\n\n    generator.writeEndObject();\n  }\n\n  public static FileScanTask fromJson(\n      JsonNode jsonNode,\n      List<DeleteFile> allDeleteFiles,\n      Map<Integer, PartitionSpec> specsById,\n      boolean isCaseSensitive) {\n    Preconditions.checkArgument(jsonNode != null, \"Invalid JSON node for file scan task: null\");\n    Preconditions.checkArgument(\n        jsonNode.isObject(), \"Invalid JSON node for file scan task: non-object (%s)\", jsonNode);\n\n    DataFile dataFile =\n        (DataFile) ContentFileParser.fromJson(JsonUtil.get(DATA_FILE, jsonNode), specsById);\n    int specId = dataFile.specId();\n\n    DeleteFile[] deleteFiles = null;\n    if (jsonNode.has(DELETE_FILE_REFERENCES)) {\n      List<Integer> indices = JsonUtil.getIntegerList(DELETE_FILE_REFERENCES, jsonNode);\n      // DELTA LAKE PATCH: Added indices.isEmpty() check to fix NoSuchElementException\n      // when delete-file-references is an empty array\n      Preconditions.checkArgument(\n          indices.isEmpty() || Collections.max(indices) < allDeleteFiles.size(),\n          \"Invalid delete file references: %s, expected indices < %s\",\n          indices,\n          allDeleteFiles.size());\n      deleteFiles = indices.stream().map(allDeleteFiles::get).toArray(DeleteFile[]::new);\n    }\n\n    Expression filter = null;\n    if (jsonNode.has(RESIDUAL_FILTER)) {\n      filter = ExpressionParser.fromJson(jsonNode.get(RESIDUAL_FILTER));\n    }\n\n    String schemaString = SchemaParser.toJson(specsById.get(specId).schema());\n    String specString = PartitionSpecParser.toJson(specsById.get(specId));\n    ResidualEvaluator boundResidual =\n        ResidualEvaluator.of(specsById.get(specId), filter, isCaseSensitive);\n\n    return new BaseFileScanTask(dataFile, deleteFiles, schemaString, specString, boundResidual);\n  }\n}\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/unityCatalog/UnityCatalog.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.iceberg.unityCatalog;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport org.apache.iceberg.BaseMetastoreCatalog;\nimport org.apache.iceberg.CatalogUtil;\nimport org.apache.iceberg.MetadataUpdate;\nimport org.apache.iceberg.TableOperations;\nimport org.apache.iceberg.catalog.Namespace;\nimport org.apache.iceberg.catalog.TableIdentifier;\nimport org.apache.iceberg.hadoop.Configurable;\nimport org.apache.iceberg.io.CloseableGroup;\nimport org.apache.iceberg.io.FileIO;\n\nimport org.apache.iceberg.exceptions.NoSuchTableException;\nimport org.apache.iceberg.exceptions.NotFoundException;\n\n\n/**\n * UnityCatalog manages delta table with iceberg metadata conversion in unity catalog\n * Only newTableOps needs implementation as it is used to commit to Iceberg. All other methods are\n * not required (eg, listTables, dropTable, renameTable) because the tables are managed by\n * unity catalog outside of iceberg context.\n */\npublic class UnityCatalog extends BaseMetastoreCatalog implements Closeable, Configurable<Object> {\n    private static final String DEFAULT_FILE_IO_IMPL = \"org.apache.iceberg.hadoop.HadoopFileIO\";\n\n\n    // Injectable factory for testing purposes.\n    static class FileIOFactory {\n        public FileIO newFileIO(String impl, Map<String, String> properties, Object hadoopConf) {\n            return CatalogUtil.loadFileIO(impl, properties, hadoopConf);\n        }\n    }\n\n    private Object conf;\n\n    private Map<String, String> catalogProperties;\n\n    private FileIOFactory fileIOFactory;\n\n    private CloseableGroup closeableGroup;\n\n    private List<MetadataUpdate> metadataUpdates;\n\n    // If set, all table option in this catalog will be built based on the snapshot baseMetadataLocation points to.\n    private final Optional<String> baseMetadataLocation;\n\n    public UnityCatalog(\n        List<MetadataUpdate> metadataUpdates\n        , Optional<String> baseMetadataLocation\n    ) {\n        this.metadataUpdates = metadataUpdates;\n        this.baseMetadataLocation = baseMetadataLocation;\n    }\n\n    @Override\n    protected TableOperations newTableOps(TableIdentifier tableIdentifier) {\n        FileIO fileIO = fileIOFactory.newFileIO(DEFAULT_FILE_IO_IMPL, catalogProperties, conf);\n        closeableGroup.addCloseable(fileIO);\n        return new UnityCatalogTableOperations(\n                fileIO\n                , tableIdentifier\n                , metadataUpdates\n                , baseMetadataLocation\n        );\n    }\n\n    @Override\n    public void initialize(String name, Map<String, String> properties) {\n        this.catalogProperties = properties;\n        this.fileIOFactory = new FileIOFactory();\n        this.closeableGroup = new CloseableGroup();\n    }\n\n    @Override\n    public void close() throws IOException {\n        if (closeableGroup != null) {\n            closeableGroup.close();\n        }\n    }\n\n    @Override\n    protected String defaultWarehouseLocation(TableIdentifier tableIdentifier) {\n        throw new UnsupportedOperationException(\n                \"UnityCatalog does not currently support defaultWarehouseLocation\");\n    }\n\n    @Override\n    public void setConf(Object conf) {\n        this.conf = conf;\n    }\n\n    @Override\n    public List<TableIdentifier> listTables(Namespace namespace) {\n        throw new UnsupportedOperationException(\"UnityCatalog does not currently support listTables\");\n    }\n\n    @Override\n    public boolean dropTable(TableIdentifier identifier, boolean purge) {\n        throw new UnsupportedOperationException(\"UnityCatalog does not currently support dropTable\");\n    }\n\n    @Override\n    public void renameTable(TableIdentifier from, TableIdentifier to) {\n        throw new UnsupportedOperationException(\"UnityCatalog does not currently support renameTable\");\n    }\n\n    // If the given metadataLocation is invalid, we also see it as the table doesn't exist\n    @Override\n    public boolean tableExists(TableIdentifier identifier) {\n        try {\n            loadTable(identifier);\n            return true;\n        } catch (NoSuchTableException | NotFoundException e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "icebergShaded/src/main/java/org/apache/iceberg/unityCatalog/UnityCatalogTableOperations.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.iceberg.unityCatalog;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.time.Instant;\nimport java.util.*;\n\nimport org.apache.iceberg.BaseMetastoreTableOperations;\nimport org.apache.iceberg.MetadataUpdate;\nimport org.apache.iceberg.PartitionSpec;\nimport org.apache.iceberg.Schema;\nimport org.apache.iceberg.TableMetadata;\nimport org.apache.iceberg.TableProperties;\nimport org.apache.iceberg.catalog.TableIdentifier;\nimport org.apache.iceberg.exceptions.CommitFailedException;\nimport org.apache.iceberg.exceptions.NoSuchTableException;\nimport org.apache.iceberg.io.FileIO;\nimport org.apache.iceberg.relocated.com.google.common.base.Preconditions;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport scala.Option;\nimport scala.Tuple2;\n\n/**\n * UnityCatalogTableOperations supports load and commit to Iceberg metadata through unity catalog\n */\npublic class UnityCatalogTableOperations extends BaseMetastoreTableOperations {\n\n    private static final Logger LOG = LoggerFactory.getLogger(UnityCatalogTableOperations.class);\n\n    /**\n     * Property to be set in translated Iceberg metadata files.\n     * Indicates the delta commit version # that it corresponds to.\n     */\n    public static final String DELTA_VERSION_PROPERTY = \"delta-version\";\n    public static final String DELTA_TIMESTAMP_PROPERTY = \"delta-timestamp\";\n    public static final String BASE_DELTA_VERSION_PROPERTY = \"base-delta-version\";\n    public static final String DELTA_HIGH_WATER_MARK_PROPERTY = \"delta-high-water-mark\";\n\n    public static final String DELTA_SQL_CONF_BYPASS_SEQUENCE_NUMBER_CHECK =\n            \"spark.databricks.delta.uniform.bypassSnapshotSequenceNumberCheck\";\n    public static final String DELTA_SQL_CONF_BYPASS_FORMAT_VERSION_DOWNGRAGDE_CHECK =\n            \"spark.databricks.delta.uniform.bypassFormatVersionDowngradeCheck\";\n\n    private final FileIO fileIO;\n    private final TableIdentifier tableIdentifier;\n    private final Optional<String> baseMetadataLocation;\n    private List<MetadataUpdate> metadataUpdates;\n    private Optional<Tuple2<String, TableMetadata>> lastWrittenTableMetadataWithLocation;\n\n    public UnityCatalogTableOperations(\n            FileIO fileIO\n            , TableIdentifier tableIdentifier\n            , List<MetadataUpdate> metadataUpdates\n            , Optional<String> baseMetadataLocation\n    ) {\n        this.fileIO = fileIO;\n        this.tableIdentifier = tableIdentifier;\n        this.metadataUpdates = metadataUpdates;\n        this.baseMetadataLocation = baseMetadataLocation;\n        this.lastWrittenTableMetadataWithLocation = Optional.empty();\n    }\n\n    @Override\n    public FileIO io() {\n        return fileIO;\n    }\n\n    @Override\n    protected String tableName() {\n        return tableIdentifier.toString();\n    }\n\n    @Override\n    public void doCommit(TableMetadata base, TableMetadata metadata) {\n        TableMetadata.Builder builder = TableMetadata.buildFrom(metadata);\n\n        // hasAddPartitionSpec indicates if the current commit attempt have added a new\n        // partition spec through metadata update.\n        boolean hasAddPartitionSpec = false;\n        boolean initialPartitionSpecExistsAndIsPartitioned = false;\n        for (PartitionSpec spec : metadata.specs()) {\n            if (spec.specId() == 0 && spec.isPartitioned()) {\n                initialPartitionSpecExistsAndIsPartitioned = true;\n            }\n        }\n        Long deltaVersion = Long.parseLong(\n                metadata.properties().get(DELTA_VERSION_PROPERTY));\n        String baseDeltaVersionStr =\n                metadata.properties().get(BASE_DELTA_VERSION_PROPERTY);\n\n        String deltaHighWaterMarkStr =\n                metadata.properties().get(DELTA_HIGH_WATER_MARK_PROPERTY);\n\n        Schema lastAddedSchema = metadata.schema();\n        for (MetadataUpdate update : metadataUpdates) {\n            // iceberg-core reassigns field id in its schema when firstly creates table metadata;\n            // we should always use the schema (with field ids assigned by delta) from MetadataUpdate\n            // because the parquet data files are already written with field ids assigned by Delta.\n            if (update instanceof MetadataUpdate.AddSchema) {\n                MetadataUpdate.AddSchema addSchema = (MetadataUpdate.AddSchema) update;\n                // lastColumnId must be monotonically increasing.\n                builder.setCurrentSchema(\n                  addSchema.schema(), Math.max(metadata.lastColumnId(), addSchema.lastColumnId()));\n                lastAddedSchema = addSchema.schema();\n            } else if (update instanceof MetadataUpdate.AddPartitionSpec) {\n                // Use the partition spec from MetadataUpdate because the partition spec contains\n                // the correct field ids assigned by Delta\n                PartitionSpec specToAdd = ((MetadataUpdate.AddPartitionSpec) update).spec().bind(lastAddedSchema);\n                if (!specToAdd.compatibleWith(metadata.spec())) {\n                    builder.setDefaultPartitionSpec(specToAdd);\n                    hasAddPartitionSpec = true;\n                }\n            } else {\n                update.applyTo(builder);\n            }\n        }\n\n        // Remove the initial partitioned partition spec (id=0) if new partition spec is added\n        // because the initial partition spec has field ids assigned by iceberg-core\n        // which may mismatch with field id assigned by Delta\n        if (hasAddPartitionSpec && initialPartitionSpecExistsAndIsPartitioned) {\n            MetadataUpdate.RemovePartitionSpecs removeSpecs = new MetadataUpdate.RemovePartitionSpecs(Set.of(0));\n            removeSpecs.applyTo(builder);\n        }\n\n        metadata = builder.build();\n\n        if (base != current()) {\n            throw new CommitFailedException(\"Cannot commit changes based on stale table metadata\");\n        }\n        if (base == metadata) {\n            LOG.info(\"Nothing to commit.\");\n            return;\n        }\n\n        final int newVersion = currentVersion() + 1;\n        String newMetadataLocation = writeNewMetadata(metadata, newVersion);\n        lastWrittenTableMetadataWithLocation = Optional.of(Tuple2.apply(newMetadataLocation, metadata));\n    }\n\n    @Override\n    public TableMetadata refresh() {\n        // during Delta to iceberg conversion, the table should always exist in catalog and the only\n        // case with NoSuchTableException is the very first metadata conversion where iceberg\n        // metadata and metadata location table prop does not exist yet.\n        try {\n            return super.refresh();\n        } catch (NoSuchTableException e) {\n            return null;\n        }\n    }\n\n    @Override\n    public void doRefresh() {\n        LOG.debug(\"Getting metadata location for table {}\", tableIdentifier);\n        String location = loadTableMetadataLocation();\n        Preconditions.checkState(\n                location != null && !location.isEmpty(),\n                \"Got null or empty location %s for table %s\",\n                location,\n                tableIdentifier);\n        refreshFromMetadataLocation(location);\n    }\n\n    /**\n     * Returns both the location of the Iceberg metadata file and its corresponding table metadata.\n     *\n     * @return An Optional containing a tuple of:\n     *         - String: The path where the metadata file was written\n     *         - TableMetadata: The corresponding Iceberg table metadata\n     */\n    public Optional<Tuple2<String, TableMetadata>> getLastWrittenTableMetadataWithLocation() {\n        return lastWrittenTableMetadataWithLocation;\n    }\n\n    private String loadTableMetadataLocation() {\n        if (baseMetadataLocation.isPresent()) {\n            return baseMetadataLocation.get();\n        }\n        throw new NoSuchTableException(\"Cannot find iceberg table %s. Either the table does \" +\n            \"not exist or the corresponding Delta table has not been converted yet.\",\n            tableIdentifier.toString());\n    }\n\n}\n"
  },
  {
    "path": "kernel/EXCEPTION_PRINCIPLES.md",
    "content": "# Exception principles in Delta Kernel\n## Introduction\nExceptions thrown in Delta Kernel are either user-facing or developer-facing.\n- **User-facing exceptions** are expected to be thrown. Delta Kernel is unable to complete the requested operation for a fundamental reason inherent to the nature of the request, the table of interest, and the capabilities of Delta Kernel and the Delta protocol. These errors are intentional and are used to communicate with the end-user why an operation cannot be completed.\n- **Developer-facing exceptions** are unexpected and generally indicate that something has gone wrong or is incorrect. They can target either Kernel developers or connector developers that are using Kernel APIs. These exceptions should be used for debugging; a perfectly working connector + Kernel should never encounter these.\n\nSee [User-facing vs developer-facing exceptions](#User-facing-vs-developer-facing-exceptions) for examples of these types of exceptions.\n\n## Principles\nThese are the general exception principles to follow and enforce when contributing code or reviewing pull requests.\n- All **user-facing exceptions** should be of type `KernelException`.\n    - Create a new subclass for exceptions that may require special handling (such as `TableNotFoundException`) otherwise just use `KernelException`. Subclasses should expose useful exception parameters on a case-by-case basis.\n    - All `KernelException`s should be instantiated with a method in the [DeltaErrors](https://github.com/delta-io/delta/blob/master/kernel/kernel-api/src/main/java/io/delta/kernel/internal/DeltaErrors.java) file.\n    - Error messages should be clear and actionable.\n        - Clearly state (1) the problem, (2) why it occurred and (3) how it can be solved.\n- **User-facing exceptions** should be consistent across releases. Any changes to user-facing exception classes or messages should be carefully reviewed.\n- Any unchecked exceptions originating from the `Engine` implementation should be wrapped with `KernelEngineException` and should include additional context about the failing operation.\n    - This means all method calls to the `Engine` implementation should be wrapped. See [Wrapping exceptions thrown from the Engine implementation](#Wrapping-exceptions-thrown-from-the-Engine-implementation) for more details.\n- **Developer-facing exceptions** should be informative and provide useful information for debugging.\n\n## Further details\n\n### User-facing vs developer-facing exceptions\n\nUser-facing exceptions:\n- `TableNotFoundException` when there is no Delta table at the provided path.\n- Reading the Change Data Feed from a table without CDF enabled.\n- The input data violates table constraints when writing to the table.\n- Kernel doesn’t support reading a table with XXX table feature.\n\nDeveloper-facing exceptions:\n- `getInt` is called on a boolean `ColumnVector`.\n- A column mapping mode besides “none”, “id”, and “name” is encountered.\n- An empty iterator is returned from the `Engine` implementation when reading files.\n\n### Wrapping exceptions thrown from the Engine implementation\nWe want to wrap any unchecked exceptions thrown from the `Engine` implementation with `KernelEngineException` and include additional context about the failing operation. This makes it clear where the exception is originating from, and the additional context can help future debugging.\n\nThis requires wrapping all method calls into the `Engine` implementation. We do this using helper methods in `DeltaErrors` like [wrapEngineException](https://github.com/delta-io/delta/blob/4fefba182f81d39f1d11e2f2b85bfa140079ea11/kernel/kernel-api/src/main/java/io/delta/kernel/internal/DeltaErrors.java#L228-L240). For usage see [example 1](https://github.com/delta-io/delta/blob/2b2ef732533c707b7ca1af30e2a059da86c3c3ff/kernel/kernel-api/src/main/java/io/delta/kernel/internal/TransactionImpl.java#L246-L256) and [example 2](https://github.com/delta-io/delta/blob/2b2ef732533c707b7ca1af30e2a059da86c3c3ff/kernel/kernel-api/src/main/java/io/delta/kernel/internal/ScanImpl.java#L236-L244).\n\nNote: this does not catch all exceptions originating from the engine implementation, as exceptions that are not thrown until access will not be wrapped (i.e. exceptions thrown within iterators, in `ColumnVector` implementations, etc)\n- When checked exceptions cannot be thrown we instead wrap the checked exception in a `KernelEngineException`. See [here](https://github.com/delta-io/delta/blob/2b2ef732533c707b7ca1af30e2a059da86c3c3ff/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetFileReader.java#L148-L150) for an example.\n"
  },
  {
    "path": "kernel/README.md",
    "content": "# Delta Kernel\n\nThe Delta Kernel project is a set of Java libraries for building Delta connectors that can read from and write into Delta tables without the need to understand the [Delta protocol details](https://github.com/delta-io/delta/blob/master/PROTOCOL.md).\n\nYou can use this library to do the following:\n- Read data from small Delta tables in a single thread in a single process.\n- Read data from large Delta tables using multiple threads in a single process.\n- Build a complex connector for a distributed processing engine and read very large Delta tables.\n- Insert data into a Delta table either from a single process or a complex distributed engine.\n\nHere is an example of a simple table scan with a filter:\n```java\nEngine myEngine = DefaultEngine.create() ;                  // define a engine (more details below)\nTable myTable = Table.forPath(\"/delta/table/path\");         // define what table to scan\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);  // define which version of table to scan\nScan myScan = mySnapshot.getScanBuilder(myEngine)           // specify the scan details\n  .withFilters(myEngine, scanFilter)\n  .build();\nCloseableIterator<ColumnarBatch> physicalData =             // read the Parquet data files\n  .. read from Parquet data files ...\nScan.transformPhysicalData(...)                             // returns the table data\n```\n\nA complete version of the above example program and more examples of reading from and writing into a Delta table are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\nNotice that there are two sets of public APIs to build connectors. \n- **Table APIs** - Interfaces like [`Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/index.html?io/delta/kernel/Table.html), [`Snapshot`](https://delta-io.github.io/delta/snapshot/kernel-api/java/index.html?io/delta/kernel/Snapshot.html), and [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/index.html?io/delta/kernel/Transaction.html) that allow you to read from and write to Delta tables\n- **Engine APIs** - The [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java//index.html?io/delta/kernel/engine/Engine.html) interface allows you to plug in connector-specific optimizations to compute-intensive components in the Kernel. For example, Delta Kernel provides a *default* Parquet file reader via the `DefaultEngine`, but you may choose to replace that default with a custom `Engine` implementation that has a faster Parquet reader for your connector/processing engine.\n\n# Project setup with Delta Kernel \nThe Delta Kernel project provides the following two Maven artifacts:\n- `delta-kernel-api`: This is a must-have dependency and contains all the public `Table` and `Engine` APIs discussed earlier.\n- `delta-kernel-defaults`: This is an optional dependency that contains *default* implementations of the `Engine` interfaces using Hadoop libraries. Developers can optionally use these default implementations to speed up the development of their Delta connector.\n```xml\n<!-- Must have dependency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-api</artifactId>\n  <version>VERSION</version>\n</dependency>\n\n<!-- Optional dependency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-defaults</artifactId>\n  <version>VERSION</version>\n</dependency>\n```\n\n# API Guarantees\n**Note: This project is currently in `preview` and all APIs are currently in an evolving state. We welcome trying out the APIs to build Delta Lake connectors and  providing feedback (see below) to the project authors.**\n\nThe Java API docs are available [here](https://docs.delta.io/latest/api/java/kernel/index.html). Only the classes and interfaces documented here are considered public APIs with backward compatibility guarantees (when marked as **Stable** APIs). All other classes and interfaces available in the JAR are considered private APIs with no backward compatibility guarantees.\n\nKernel APIs are still evolving and new features are being added with every release. Kernel authors try to make the API changes backward compatible as much as they can with each new release even when an API is marked as **Evolving**, but sometimes it is hard to maintain the backward compatibility for a project that is evolving rapidly. With each new release, the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) are kept up-to-date with the latest API changes, and a detailed [migration guide](https://github.com/delta-io/delta/blob/master/kernel/USER_GUIDE.md#migration-guide) is provided for the connectors to upgrade to use the updated APIs.\n\n\n# More Information\n- [Talk](https://www.youtube.com/watch?v=KVUMFv7470I) explaining the rationale behind Kernel and the API design (slides are available [here](https://docs.google.com/presentation/d/1PGSSuJ8ndghucSF9GpYgCi9oeRpWolFyehjQbPh92-U/edit) which are kept up-to-date with the changes).\n- [User guide](https://github.com/delta-io/delta/blob/master/kernel/USER_GUIDE.md) on the step-by-step process of using Kernel in a standalone Java program or in a distributed processing connector for reading and writing to Delta tables.\n- Example [Java programs](https://github.com/delta-io/delta/tree/master/kernel/examples) that illustrate how to read and write Delta tables using the Kernel APIs.\n- Table and default Engine API Java [documentation](https://docs.delta.io/latest/api/java/kernel/index.html)\n- [Migration guide](https://github.com/delta-io/delta/blob/master/kernel/USER_GUIDE.md#migration-guide)\n\n\n# Providing feedback\nWe use [GitHub Issues](https://github.com/delta-io/delta/issues) to track community-reported issues. You can also contact the community to get answers.\n\n# Contributing\nWe welcome contributions to Delta Lake and we accept contributions via Pull Requests. See our [CONTRIBUTING.md](https://github.com/delta-io/delta/blob/master/CONTRIBUTING.md) for more details. We also adhere to the [Delta Lake Code of Conduct](https://github.com/delta-io/delta/blob/master/CODE_OF_CONDUCT.md).\n\n# Setting up IDE\n\nJava code adheres to the [Google style](https://google.github.io/styleguide/javaguide.html), which is verified via `build/sbt javafmtCheckAll` during builds.\nIn order to automatically fix Java code style issues, please use `build/sbt javafmtAll`.\n\n## Configuring Code Formatter for Eclipse/IntelliJ\n\nFollow the instructions for [Eclipse](https://github.com/google/google-java-format#eclipse) or\n[IntelliJ](https://github.com/google/google-java-format#intellij-android-studio-and-other-jetbrains-ides) to install the **google-java-format** plugin (note the required manual actions for IntelliJ).\n"
  },
  {
    "path": "kernel/USER_GUIDE.md",
    "content": "## Delta Kernel User Guide\n\n## What is Delta Kernel?\nDelta Kernel is a library for operating on Delta tables. Specifically, it provides simple and narrow APIs for reading and writing to Delta tables without the need to understand the [Delta protocol](https://github.com/delta-io/delta/blob/master/PROTOCOL.md) details. You can use this library to do the following:\n\n* Read and write Delta tables from your applications.\n* Build a connector for a distributed engine like [Apache Spark™](https://github.com/apache/spark), [Apache Flink](https://github.com/apache/flink), or [Trino](https://github.com/trinodb/trino) for reading or writing massive Delta tables.\n\n   * [Delta Kernel User Guide](#delta-kernel-user-guide)\n   * [What is Delta Kernel?](#what-is-delta-kernel)\n   * [Set up Delta Kernel for your project](#set-up-delta-kernel-for-your-project)\n   * [Read a Delta table in a single process](#read-a-delta-table-in-a-single-process)\n      * [Step 1: Full scan on a Delta table](#step-1-full-scan-on-a-delta-table)\n      * [Step 2: Improve scan performance with file skipping](#step-2-improve-scan-performance-with-file-skipping)\n   * [Create a Delta table](#create-a-delta-table)\n   * [Create a table and insert data into it](#create-a-table-and-insert-data-into-it)\n   * [Blind append into an existing Delta table](#blind-append-into-an-existing-delta-table)\n   * [Idempotent Blind Appends to a Delta Table](#idempotent-blind-appends-to-a-delta-table)\n   * [Checkpointing a Delta table](#checkpointing-a-delta-table)\n   * [Build a Delta connector for a distributed processing engine](#build-a-delta-connector-for-a-distributed-processing-engine)\n      * [Step 0: Validate the prerequisites](#step-0-validate-the-prerequisites)\n      * [Step 1: Set up Delta Kernel in your connector project](#step-1-set-up-delta-kernel-in-your-connector-project)\n         * [Set up Java projects](#set-up-java-projects)\n      * [Step 2: Build your own Engine](#step-2-build-your-own-engine)\n         * [Step 2.1: Implement the Engine interface](#step-21-implement-the-engine-interface)\n         * [Step 2.2: Implement FileSystemClient interface](#step-22-implement-filesystemclient-interface)\n         * [Step 2.3: Implement ParquetHandler](#step-23-implement-parquethandler)\n         * [Step 2.5: Implement JsonHandler](#step-25-implement-jsonhandler)\n         * [Step 2.6: Implement ColumnarBatch and ColumnVector](#step-26-implement-columnarbatch-and-columnvector)\n      * [Step 3: Build read support in your connector](#step-3-build-read-support-in-your-connector)\n         * [Step 3.1: Resolve the table snapshot to query](#step-31-resolve-the-table-snapshot-to-query)\n         * [Step 3.2: Resolve files to scan](#step-32-resolve-files-to-scan)\n         * [Step 3.3: Distribute the file information to the workers](#step-33-distribute-the-file-information-to-the-workers)\n         * [Step 3.4: Read the columnar data](#step-34-read-the-columnar-data)\n      * [Step 4: Build write support in your connector](#step-4-build-write-support-in-your-connector)\n         * [Step 4.1: Determine the schema of the data that needs to be written to the table](#step-41-determine-the-schema-of-the-data-that-needs-to-be-written-to-the-table)\n         * [Step 4.2: Determine the physical partitioning of the data based on the table schema and partition columns](#step-42-determine-the-physical-partitioning-of-the-data-based-on-the-table-schema-and-partition-columns)\n         * [Step 4.3: Distribute the writer tasks definitions (which include the transaction state) to workers](#step-43-distribute-the-writer-tasks-definitions-which-include-the-transaction-state-to-workers)\n         * [Step 4.4: Tasks write the data to data files and send the data file info to the driver.](#step-44-tasks-write-the-data-to-data-files-and-send-the-data-file-info-to-the-driver)\n         * [Step 4.5: Finalize the query.](#step-45-finalize-the-query)\n   * [Migration guide](#migration-guide)\n      * [Migration from Delta Lake version 3.1.0 to 3.2.0](#migration-from-delta-lake-version-310-to-320)\n\n## Set up Delta Kernel for your project\nYou need to `io.delta:delta-kernel-api` and `io.delta:delta-kernel-defaults` dependencies. Following is an example Maven `pom` file dependency list.\n\nThe `delta-kernel-api` module contains the core of the Kernel that abstracts out the Delta protocol to enable reading and writing into Delta tables. It makes use of the `Engine` interface that is being passed to the Kernel API by the connector for heavy-lift operations such as reading/writing Parquet or JSON files, evaluating expressions or file system operations such as listing contents of the Delta Log directory, etc. Kernel supplies a default implementation of `Engine` in module `delta-kernel-defaults`. The connectors can implement their own version of `Engine` to make use of their native implementation of functionalities the `Engine` provides. For example: the connector can make use of their Parquet reader instead of using the reader from the `DefaultEngine`. More details on this [later](#step-2-build-your-own-engine).\n\n```xml\n<dependencies>\n  <dependency>\n    <groupId>io.delta</groupId>\n    <artifactId>delta-kernel-api</artifactId>\n    <version>${delta-kernel.version}</version>\n  </dependency>\n\n  <dependency>\n    <groupId>io.delta</groupId>\n    <artifactId>delta-kernel-defaults</artifactId>\n    <version>${delta-kernel.version}</version>\n  </dependency>\n</dependencies>\n```\n\nIf your connector is not using the [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) provided by the Kernel, the dependency `delta-kernel-defaults` from the above list can be skipped.\n\n\n## Read a Delta table in a single process\nIn this section, we will walk through how to build a very simple single-process Delta connector that can read a Delta table using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\n### Step 1: Full scan on a Delta table\nThe main entry point is [`io.delta.kernel.Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html) which is a programmatic representation of a Delta table. Say you have a Delta table at the directory `myTablePath`. You can create a `Table` object as follows:\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.*;\nimport org.apache.hadoop.conf.Configuration;\n\nString myTablePath = <my-table-path>; // fully qualified table path. Ex: file:/user/tables/myTable\nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n```\n\nNote the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) we are creating to bootstrap the `myTable` object. This object allows you to plug in your own libraries for computationally intensive operations like Parquet file reading, JSON parsing, etc. You can ignore it for now. We will discuss more about this later when we discuss how to build more complex connectors for distributed processing engines.\n\nFrom this `myTable` object you can create a [`Snapshot`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Snapshot.html) object which represents the consistent state (a.k.a. a snapshot consistency) in a specific version of the table.  \n\n```java\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\n```\n\nNow that we have a consistent snapshot view of the table, we can query more details about the table. For example, you can get the version and schema of this snapshot.\n\n```java\nlong version = mySnapshot.getVersion();\nStructType tableSchema = mySnapshot.getSchema();\n```\n\nNext, to read the table data, we have to *build* a [`Scan`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html) object. In order to build a `Scan` object, create a [`ScanBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/ScanBuilder.html) object which optionally allows selecting a subset of columns to read or setting a query filter. For now, ignore these optional settings.\n\n```java\nScan myScan = mySnapshot.getScanBuilder(myEngine).build()\n\n// Common information about scanning for all data files to read.\nRow scanState = myScan.getScanState(myEngine)\n\n// Information about the list of scan files to read\nCloseableIterator<FilteredColumnarBatch> scanFiles = myScan.getScanFiles(myEngine)\n```\n\nThis [`Scan`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html) object has all the necessary metadata to start reading the table. There are two crucial pieces of information needed for reading data from a file in the table. \n\n* `myScan.getScanFiles(Engine)`:  Returns scan files as columnar batches (represented as an iterator of `FilteredColumnarBatch`es, more on that later) where each selected row in the batch has information about a single file containing the table data.\n* `myScan.getScanState(Engine)`: Returns the snapshot-level information needed for reading any file. Note that this is a single row and common to all scan files.\n\nFor each scan file the physical data must be read from the file. The columns to read are specified in the scan file state. Once the physical data is read, you have to call [`ScanFile.transformPhysicalData(...)`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html#transformPhysicalData-io.delta.kernel.engine.Engine-io.delta.kernel.data.Row-io.delta.kernel.data.Row-io.delta.kernel.utils.CloseableIterator-) with the scan state and the physical data read from scan file. This API takes care of transforming (e.g. adding partition columns) the physical data into logical data of the table. Here is an example of reading all the table data in a single thread.\n\n```java\nCloserableIterator<FilteredColumnarBatch> fileIter = scanObject.getScanFiles(myEngine);\n\nRow scanStateRow = scanObject.getScanState(myEngine);\n\nwhile(fileIter.hasNext()) {\n  FilteredColumnarBatch scanFileColumnarBatch = fileIter.next();\n\n  // Get the physical read schema of columns to read from the Parquet data files\n  StructType physicalReadSchema =\n    ScanStateRow.getPhysicalDataReadSchema(engine, scanStateRow);\n\n  try (CloseableIterator<Row> scanFileRows = scanFileColumnarBatch.getRows()) {\n    while (scanFileRows.hasNext()) {\n      Row scanFileRow = scanFileRows.next();\n\n      // From the scan file row, extract the file path, size and modification time metadata\n      // needed to read the file.\n      FileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow);\n\n      // Open the scan file which is a Parquet file using connector's own\n      // Parquet reader or default Parquet reader provided by the Kernel (which\n      // is used in this example).\n      CloseableIterator<ColumnarBatch> physicalDataIter =\n        engine.getParquetHandler().readParquetFiles(\n          singletonCloseableIterator(fileStatus),\n          physicalReadSchema,\n          Optional.empty() /* optional predicate the connector can apply to filter data from the reader */\n        );\n\n      // Now the physical data read from the Parquet data file is converted to a table\n      // logical data. Logical data may include the addition of partition columns and/or\n      // subset of rows deleted\n      try (\n         CloseableIterator<FilteredColumnarBatch> transformedData =\n           Scan.transformPhysicalData(\n             engine,\n             scanStateRow,\n             scanFileRow,\n             physicalDataIter)) {\n        while (transformedData.hasNext()) {\n          FilteredColumnarBatch logicalData = transformedData.next();\n          ColumnarBatch dataBatch = logicalData.getData();\n\n          // Not all rows in `dataBatch` are in the selected output.\n          // An optional selection vector determines whether a row with a\n          // specific row index is in the final output or not.\n          Optional<ColumnVector> selectionVector = dataReadResult.getSelectionVector();\n\n          // access the data for the column at ordinal 0\n          ColumnVector column0 = dataBatch.getColumnVector(0);\n          for (int rowIndex = 0; rowIndex < column0.getSize(); rowIndex++) {\n            // check if the row is selected or not\n            if (!selectionVector.isPresent() || // there is no selection vector, all records are selected\n               (!selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId)))  {\n              // Assuming the column type is String.\n              // If it is a different type, call the relevant function on the `ColumnVector`\n              System.out.println(column0.getString(rowIndex));\n            }\n          }\n\n\t  // access the data for column at ordinal 1\n\t  ColumnVector column1 = dataBatch.getColumnVector(1);\n\t  for (int rowIndex = 0; rowIndex < column1.getSize(); rowIndex++) {\n            // check if the row is selected or not\n            if (!selectionVector.isPresent() || // there is no selection vector, all records are selected\n               (!selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId)))  {\n              // Assuming the column type is Long.\n              // If it is a different type, call the relevant function on the `ColumnVector`\n              System.out.println(column1.getLong(rowIndex));\n            }\n          }\n\t  // .. more ..\n        }\n      }\n    }\n  }\n}\n```\n\nA few working examples to read Delta tables within a single process are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n> [!IMPORTANT]\n> All the Delta protocol-level details are encoded in the rows returned by `Scan.getScanFiles` API, but you do not have to understand them in order to read the table data correctly. All you need is to get the Parquet file status from each scan file row and read the data from the Parquet file into the `ColumnarBatch` format. The physical data is converted into the logical data of the table using [`Scan.transformPhysicalData`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Scan.html#transformPhysicalData-io.delta.kernel.engine.Engine-io.delta.kernel.data.Row-io.delta.kernel.data.Row-io.delta.kernel.utils.CloseableIterator-). Transformation to logical data is dictated by the protocol and the metadata of the table and the scan file. As the Delta protocol evolves this transformation step will evolve with it and your code will not have to change to accommodate protocol changes. This is the major advantage of the abstractions provided by Delta Kernel.\n\n> [!NOTE]\n> Observe that the same `Engine` instance `myEngine` is passed multiple times whenever a call to Delta Kernel API is made. The reason for passing this instance for every call is because it is the connector context, it should maintained outside of the Delta Kernel APIs to give the connector control over the `Engine`.\n\n### Step 2: Improve scan performance with file skipping\nWe have explored how to do a full table scan. However, the real advantage of using the Delta format is that you can skip files using your query filters. To make this possible, Delta Kernel provides an [expression framework](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/package-summary.html) to encode your filters and provide them to Delta Kernel to skip files during the scan file generation. For example, say your table is partitioned by `columnX`, you want to query only the partition `columnX=1`. You can generate the expression and use it to build the scan as follows:\n\n```java\nimport io.delta.kernel.expressions.*;\n\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = DefaultEngine.create(new Configuration());\n\nPredicate filter = new Predicate(\n  \"=\",\n  Arrays.asList(new Column(\"columnX\"), Literal.ofInt(1)));\n\nScan myFilteredScan = mySnapshot.getScanBuilder().withFilter(filter).build()\n\n// Subset of the given filter that is not guaranteed to be satisfied by\n// Delta Kernel when it returns data. This filter is used by Delta Kernel\n// to do data skipping as much as possible. The connector should use this filter\n// on top of the data returned by Delta Kernel in order for further filtering.\nOptional<Predicate> remainingFilter = myFilteredScan.getRemainingFilter();\n```\n\nThe scan files returned by  `myFilteredScan.getScanFiles(myEngine)` will have rows representing files only of the required partition. Similarly, you can provide filters for non-partition columns, and if the data in the table is well clustered by those columns, then Delta Kernel will be able to skip files as much as possible.\n\n## Create a Delta table\nIn this section, we will walk through how to build a Delta connector that can create a Delta table using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\nThe main entry point is [`io.delta.kernel.Table`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html) which is a programmatic representation of a Delta table. Say you want to create Delta table at the directory `myTablePath`. You can create a `Table` object as follows:\n\n```java\npackage io.delta.kernel.examples;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterable;\n\nString myTablePath = <my-table-path>; \nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n```\n\nNote the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) we are creating to bootstrap the `myTable` object. This object allows you to plug in your own libraries for computationally intensive operations like Parquet file reading, JSON parsing, etc. You can ignore it for now. We will discuss more about this later when we discuss how to build more complex connectors for distributed processing engines.\n\nFrom this `myTable` object you can create a [`TransactionBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionBuilder.html) object which allows you to construct a [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Transaction.html) object\n\n```java\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */ \n    Operation.CREATE_TABLE /* What is the operation we are trying to perform. This is noted in the Delta Log */\n  );\n```\n\nNow that you have the `TransactionBuilder` object, you can set the table schema and partition columns of the table.\n\n```java\nStructType mySchema = new StructType()\n  .add(\"id\", IntegerType.INTEGER)\n  .add(\"name\", StringType.STRING)\n  .add(\"city\", StringType.STRING)\n  .add(\"salary\", DoubleType.DOUBLE);\n\n// Partition columns are optional. Use it only if you are creating a partitioned table.\nList<String> myPartitionColumns = Collections.singletonList(\"city\");\n\n// Set the schema of the new table on the transaction builder\ntxnBuilder = txnBuilder\n  .withSchema(engine, mySchema);\n\n// Set the partition columns of the new table only if you are creating\n// a partitioned table; otherwise, this step can be skipped.\ntxnBuilder = txnBuilder\n  .withPartitionColumns(engine, examplePartitionColumns);\n```\n\n`TransactionBuilder` allows setting additional properties of the table such as enabling a certain Delta feature or setting identifiers for idempotent writes. We will be visiting these in the next sections. The next step is to build `Transaction` out of the `TransactionBuilder` object.\n\n\n```java\n// Build the transaction\nTransaction txn = txnBuilder.build(engine);\n```\n\n`Transaction` object allows the connector to optionally add any data and finally commit the transaction. A successful commit ensures that the table is created with the given schema. In this example, we are just creating a table and not adding any data as part of the table.\n\n```java\n// Commit the transaction.\n// As we are just creating the table and not adding any data, the `dataActions` is empty.\nTransactionCommitResult commitResult =\n  txn.commit(\n    engine,\n    CloseableIterable.emptyIterable() /* dataActions */\n  );\n```\n\nThe [`TransactionCommitResult`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html) contains the what version the transaction is committed as and whether the table is ready for a checkpoint. As we are creating a table the version will be `0`. We will be discussing later on what a checkpoint is and what it means for the table to be ready for the checkpoint.\n\nA few working examples to create partitioned and un-partitioned Delta tables are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n## Create a table and insert data into it\nIn this section, we will walk through how to build a Delta connector that can create a Delta table and insert data into the table (similar to `CREATE TABLE <table> AS <query>` construct in SQL) using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository.\n\nThe first step is to construct a `Transaction`. Below is the code for that. For more details on what each step of the code means, please read the [create table](#create-a-delta-table) section.\n\n```\npackage io.delta.kernel.examples;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterable;\n\nString myTablePath = <my-table-path>; \nConfiguration hadoopConf = new Configuration();\nEngine myEngine = DefaultEngine.create(hadoopConf);\nTable myTable = Table.forPath(myEngine, myTablePath);\n\nStructType mySchema = new StructType()\n  .add(\"id\", IntegerType.INTEGER)\n  .add(\"name\", StringType.STRING)\n  .add(\"city\", StringType.STRING)\n  .add(\"salary\", DoubleType.DOUBLE);\n\n// Partition columns are optional. Use it only if you are creating a partitioned table.\nList<String> myPartitionColumns = Collections.singletonList(\"city\");\n\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */ \n    Operation.WRITE /* What is the operation we are trying to perform? This is noted in the Delta Log */\n  );\n\n// Set the schema of the new table on the transaction builder\ntxnBuilder = txnBuilder\n  .withSchema(engine, mySchema);\n\n// Set the partition columns of the new table only if you are creating\n// a partitioned table; otherwise, this step can be skipped.\ntxnBuilder = txnBuilder\n  .withPartitionColumns(engine, examplePartitionColumns);\n\n// Build the transaction\nTransaction txn = txnBuilder.build(engine);\n```\n\nNow that we have the [`Transaction`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Transaction.html) object, the next step is generating the data that confirms the table schema and partitioned according to the table partitions.\n\n```java\nStructType dataSchema = txn.getSchema(engine)\n\n// Optional for un-partitioned tables\nList<String> partitionColumnNames = txn.getPartitionColumns(engine)\n```\n\nUsing the data schema and partition column names the connector can plan the query and generate data. At tasks that actually have the data to write to the table, the connector can ask the Kernel to transform the data given in the table schema into physical data that can actually be written to the Parquet data files. For partitioned tables, the data needs to be first partitioned by the partition columns, and then the connector should ask the Kernel to transform the data for each partition separately. The partitioning step is needed because any given data file in the Delta table contains data belonging to exactly one partition.\n\nGet the state of the transaction. The transaction state contains the information about how to convert the data in the table schema into physical data that needs to be written. The transformations depend on the protocol and features the table has.\n\n```java\nRow txnState = txn.getTransactionState(engine);\n```\n\nPrepare the data.\n\n```java\n// The data generated by the connector to write into a table\nCloseableIterator<FilteredColumnarBatch> data = ... \n\n// Create partition value map\nMap<String, Literal> partitionValues =\n  Collections.singletonMap(\n    \"city\", // partition column name\n     // partition value. Depending upon the partition column type, the\n     // partition value should be created. In this example, the partition\n     // column is of type StringType, so we are creating a string literal.\n     Literal.ofString(city)\n  );\n```\n\nThe connector data is passed as an iterator of [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html). Each of the `FilteredColumnarBatch` contains a [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) which actually contains the data in columnar access format and an optional section vector that allows the connector to specify which rows from the `ColumnarBatch` to write to the table.\n\nPartition values are passed as a map of the partition column name to the partition value. For an un-partitioned table, the map should be empty as it has no partition columns.\n\n```\n// Transform the logical data to physical data that needs to be written to the Parquet\n// files\nCloseableIterator<FilteredColumnarBatch> physicalData =\n  Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n```\n\nThe above code converts the given data for partitions into an iterator of `FilteredColumnarBatch` that needs to be written to the Parquet data files. In order to write the data files, the connector needs to get the [`WriteContext`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html) from Kernel, which tells the connector where to write the data files and what columns to collect statistics from each data file.\n\n```java\n// Get the write context\nDataWriteContext writeContext = Transaction.getWriteContext(engine, txnState, partitionValues);\n```\n\nNow, the connector has the physical data that needs to be written to Parquet data files, and where those files should be written, it can start writing the data files.\n\n```java\nCloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n  .writeParquetFiles(\n    writeContext.getTargetDirectory(),\n    physicalData,\n    writeContext.getStatisticsColumns()\n  );\n```\n\nIn the above code, the connector is making use of the `Engine` provided `ParquetHandler` to write the data, but the connector can choose its own Parquet file writer to write the data. Also note that the return of the above call is an iterator of [`DataFileStatus`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/DataFileStatus.html) for each data file written. It basically contains the file path, file metadata, and optional file-level statistics for columns specified by the [`WriteContext.getStatisticsColumns()`]([https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/DataWriteContext.html#getStatisticsColumns--))\n\nConvert each `DataFileStatus` into a Delta log action that can be written to the Delta table log.\n\n```java\nCloseableIterator<Row> dataActions =\n  Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);\n```\n\nThe next step is constructing [`CloseableIterable`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/CloseableIterable.html) out of the all the Delta log actions generated above. The reason for constructing an `Iterable` is that the transaction committing involves accessing the list of Delta log actions more than one time (in order to resolve conflicts when there are multiple writes to the table). Kernel provides a [utility method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/CloseableIterable.html#inMemoryIterable-io.delta.kernel.utils.CloseableIterator-) to create an in-memory version of `CloseableIterable`. This interface also gives the connector an option to implement a custom implementation that spills the data actions to disk when the contents are too big to fit in memory.\n\n```java\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(dataActions);\n```\n\nThe final step is committing the transaction!\n\n```java\nTransactionCommitStatus commitStatus = txn.commit(engine, dataActionsIterable)\n```\n\nThe [`TransactionCommitResult`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html) contains the what version the transaction is committed as and whether the table is ready for a checkpoint. As we are creating a table the version will be `0`. We will be discussing later on what a checkpoint is and what it means for the table to be ready for the checkpoint.\n\nA few working examples to create and insert data into partitioned and un-partitioned Delta tables are available [here](https://github.com/delta-io/delta/tree/master/kernel/examples).\n\n## Blind append into an existing Delta table\nIn this section, we will walk through how to build a Delta connector that inserts data into an existing Delta table (similar to `INSERT INTO <table> <query>` construct in SQL) using the default [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) implementation provided by Delta Kernel.\n\nYou can either write this code yourself in your project, or you can use the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) present in the Delta code repository. The steps are exactly similar to [Create table and insert data into it](#create-a-table-and-insert-data-into-it) except that we won't be providing any schema or partition columns when building the `TransactionBuilder`\n\n```java\n// Create a `Table` object with the given destination table path\nTable table = Table.forPath(engine, tablePath);\n\n// Create a transaction builder to build the transaction\nTransactionBuilder txnBuilder =\n  table.createTransactionBuilder(\n    engine,\n    \"Examples\", /* engineInfo */\n    Operation.WRITE\n  );\n\n/ Build the transaction - no need to provide the schema as the table already exists.\nTransaction txn = txnBuilder.build(engine);\n\n// Get the transaction state\nRow txnState = txn.getTransactionState(engine);\n\nList<Row> dataActions = new ArrayList<>();\n\n// Generate the sample data for three partitions. Process each partition separately.\n// This is just an example. In a real-world scenario, the data may come from different\n// partitions. Connectors already have the capability to partition by partition values\n// before writing to the table\n\n// In the test data `city` is a partition column\nfor (String city : Arrays.asList(\"San Francisco\", \"Campbell\", \"San Jose\")) {\n  FilteredColumnarBatch batch1 = generatedPartitionedDataBatch(\n\t    5 /* offset */, city /* partition value */);\n  FilteredColumnarBatch batch2 = generatedPartitionedDataBatch(\n\t    5 /* offset */, city /* partition value */);\n  FilteredColumnarBatch batch3 = generatedPartitionedDataBatch(\n\t    10 /* offset */, city /* partition value */);\n\n    CloseableIterator<FilteredColumnarBatch> data =\n\t    toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n    // Create partition value map\n    Map<String, Literal> partitionValues =\n\t    Collections.singletonMap(\n\t\t    \"city\", // partition column name\n\t\t    // partition value. Depending upon the parition column type, the\n\t\t    // partition value should be created. In this example, the partition\n\t\t    // column is of type StringType, so we are creating a string literal.\n\t\t    Literal.ofString(city));\n\n\n    // First transform the logical data to physical data that needs to be written\n    // to the Parquet\n    // files\n    CloseableIterator<FilteredColumnarBatch> physicalData =\n\t    Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n    // Get the write context\n    DataWriteContext writeContext =\n\t    Transaction.getWriteContext(engine, txnState, partitionValues);\n\n\n    // Now write the physical data to Parquet files\n    CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n\t    .writeParquetFiles(\n\t\t    writeContext.getTargetDirectory(),\n\t\t    physicalData,\n\t\t    writeContext.getStatisticsColumns());\n\n\n    // Now convert the data file status to data actions that needs to be written to the Delta\n    // table log\n    CloseableIterator<Row> partitionDataActions = Transaction.generateAppendActions(\n\t    engine,\n\t    txnState,\n\t    dataFiles,\n\t    writeContext);\n\n    // Now add all the partition data actions to the main data actions list. In a\n    // distributed query engine, the partition data is written to files at tasks on executor\n    // nodes. The data actions are collected at the driver node and then written to the\n    // Delta table log using the `Transaction.commit`\n    while (partitionDataActions.hasNext()) {\n\tdataActions.add(partitionDataActions.next());\n    }\n}\n\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n\ttoCloseableIterator(dataActions.iterator()));\n\n// Commit the transaction.\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n```\n\n## Idempotent Blind Appends to a Delta Table\nIdempotent writes allow the connector to make sure the data belonging to a particular transaction version and application id is inserted into the table at most once. In incremental processing systems (e.g. streaming systems), track progress using their own application-specific versions need to record what progress has been made in order to avoid duplicating data in the face of failures and retries during writes. By setting the transaction identifier, the Delta table can ensure that the data with the same identifier is not written multiple times. For more information refer to the Delta protocol section [Transaction Identifiers](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#transaction-identifiers)\n\nTo make the data append idempotent, set the transaction identifier on the [`TransactionBuilder`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionBuilder.html#withTransactionId-io.delta.kernel.engine.Engine-java.lang.String-long-)\n\n```java\n// Set the transaction identifiers for idempotent writes\n// Delta/Kernel makes sure that there exists only one transaction in the Delta log\n// with the given application id and txn version\ntxnBuilder =\n  txnBuilder.withTransactionId(\n    engine,\n    \"my app id\", /* application id */\n    100 /* monotonically increasing txn version with each new data insert */\n  );\n```\n\nThat's all the connector need to do for idempotent blind appends.\n\n## Checkpointing a Delta table\n[Checkpoints](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#checkpoints) are an optimization in Delta Log in order to construct the state of the Delta table faster. It basically contains the state of the table at the version the checkpoint is created. Delta Kernel allows the connector to optionally make the checkpoints. It is created for every few commits (configurable table property) on the table.\n\nThe result of `Transaction.commit` returns a `TransactionCommitResult` that contains the version the transaction is committed as and whether the table is [read for checkpoint](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/TransactionCommitResult.html#isReadyForCheckpoint--). Creating a checkpoint takes time as it needs to construct the entire state of the table. If the connector doesn't want to checkpoint by itself but uses other connectors that are faster in creating a checkpoint, it can skip the checkpointing step. \n\nIf it wants to checkpoint, the `Table` object has an [API](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html#checkpoint-io.delta.kernel.engine.Engine-long-) to checkpoint the table.\n\n```java\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\nif (commitResult.isReadyForCheckpoint()) {\n  // Checkpoint the table\n  Table.forPath(engine, tablePath).checkpoint(engine, commitResult.getVersion());\n}\n```\n\n## Build a Delta connector for a distributed processing engine\nUnlike simple applications that just read the table in a single process, building a connector for complex processing engines like Apache Spark™ and Trino can require quite a bit of additional effort. For example, to build a connector for an SQL engine you have to do the following\n\n* Understand the APIs provided by the engine to build connectors and how Delta Kernel can be used to provide the information necessary for the connector + engine to operate on a Delta table.\n* Decide what libraries to use to do computationally expensive operations like reading Parquet files, parsing JSON, computing expressions, etc. Delta Kernel provides all the extension points to allow you to plug in any library without having to understand all the low-level details of the Delta protocol.\n* Deal with details specific to distributed engines. For example, \n  * Serialization of Delta table metadata provided by Delta Kernel. \n  * Efficiently transforming data read from Parquet into the engine in-memory processing format.\n\nIn this section, we are going to outline the steps needed to build a connector.\n\n### Step 0: Validate the prerequisites\nIn the previous section showing how to read a simple table, we were briefly introduced to the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html). This is the main extension point where you can plug in your implementations of computationally-expensive operations like reading Parquet files, parsing JSON, etc. For the simple case, we were using a default implementation of the helper that works in most cases. However, for building a high-performance connector for a complex processing engine, you will very likely need to provide your own implementation using the libraries that work with your engine. So before you start building your connector, it is important to understand these requirements and plan for building your own engine.\n\nHere are the libraries/capabilities you need to build a connector that can read the Delta table\n\n* Perform file listing and file reads from your storage/file system.\n* Read Parquet files in columnar data, preferably in an in-memory columnar format.\n* Parse JSON data\n* Read JSON files\n* Evaluate expressions on in-memory columnar batches\n\nFor each of these capabilities, you can choose to build your own implementation or reuse the default implementation. \n\n### Step 1: Set up Delta Kernel in your connector project\nIn the Delta Kernel project, there are multiple dependencies you can choose to depend on.\n\n1. Delta Kernel core APIs - This is a must-have dependency, which contains all the main APIs like Table, Snapshot, and Scan that you will use to access the metadata and data of the Delta table. This has very few dependencies reducing the chance of conflicts with any dependencies in your connector and engine. This also provides the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface which allows you to plug in your implementations of computationally expensive operations, but it does not provide any implementation of this interface.\n2. Delta Kernel default- This has a default implementation called [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultEngine.html) and additional dependencies such as `Hadoop`. If you wish to reuse all or parts of this implementation, then you can optionally depend on this.\n\n#### Set up Java projects\nAs discussed above, you can import one or both of the artifacts as follows:\n\n```xml\n<!-- Must have dependency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-api</artifactId>\n  <version>${delta-kernel.version}</version>\n</dependency>\n\n<!-- Optional depdendency -->\n<dependency>\n  <groupId>io.delta</groupId>\n  <artifactId>delta-kernel-defaults</artifactId>\n  <version>${delta-kernel.version}</version>\n</dependency>\n```\n\n### Step 2: Build your own Engine\nIn this section, we are going to explore the [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface and walk through how to implement your own implementation so that you can plug in your connector/engine-specific implementations of computationally-intensive operations, threading model, resource management, etc.\n\n> [!IMPORTANT]\n> During the validation process, if you believe that all the dependencies of the default `Engine` implementation can work with your connector and engine, then you can skip this step and jump to Step 3 of implementing your connector using the default engine. If later you have the need to customize the helper for your connector, you can revisit this step.\n\n#### Step 2.1: Implement the `Engine` interface\n\nThe [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html) interface combines a bunch of sub-interfaces each of which is designed for a specific purpose. Here is a brief overview of the subinterfaces. See the API docs (Java) for a more detailed view.\n\n```java\ninterface Engine {\n  /**\n   * Get the connector provided {@link ExpressionHandler}.\n   * @return An implementation of {@link ExpressionHandler}.\n  */\n  ExpressionHandler getExpressionHandler();\n\n  /**\n   * Get the connector provided {@link JsonHandler}.\n   * @return An implementation of {@link JsonHandler}.\n   */\n  JsonHandler getJsonHandler();\n\n  /**\n   * Get the connector provided {@link FileSystemClient}.\n   * @return An implementation of {@link FileSystemClient}.\n   */\n  FileSystemClient getFileSystemClient();\n\n  /**\n   * Get the connector provided {@link ParquetHandler}.\n   * @return An implementation of {@link ParquetHandler}.\n   */\n  ParquetHandler getParquetHandler();\n}\n```\n\nTo build your own `Engine` implementation, you can choose to either use the default implementations of each sub-interface or completely build every one from scratch.\n\n```java\nclass MyEngine extends DefaultEngine {\n\n  FileSystemClient getFileSystemClient() {\n    // Build a new implementation from scratch\n    return new MyFileSystemClient();\n  }\n  \n  // For all other sub-clients, use the default implementations provided by the `DefaultEngine`.\n}\n```\n\nNext, we will walk through how to implement each interface.\n\n#### Step 2.2: Implement `FileSystemClient` interface\n\nThe [`FileSystemClient`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/FileSystemClient.html) interface contains basic file system operations like listing directories, resolving paths into a fully qualified path and reading bytes from files. Implementation of this interface must take care of the following when interacting with storage systems such as S3, Hadoop, or ADLS:\n\n* Credentials and permissions: The connector must populate its `FileSystemClient` with the necessary configurations and credentials for the client to retrieve the necessary data from the storage system. For example, an implementation based on Hadoop's FileSystem abstractions can be passed S3 credentials via the Hadoop configurations.\n* Decryption: If file system objects are encrypted, then the implementation must decrypt the data before returning the data.\n\n#### Step 2.3: Implement `ParquetHandler`\n\nAs the name suggests, this interface contains everything related to reading and writing Parquet files. It has been designed such that a connector can plug in a wide variety of implementations, from a simple single-threaded reader to a very advanced multi-threaded reader with pre-fetching and advanced connector-specific expression pushdown. Let's explore the methods to implement, and the guarantees associated with them. \n\n##### Method `readParquetFiles(CloseableIterator<FileStatus> fileIter, StructType physicalSchema, java.util.Optional<Predicate> predicate)`\n\nThis [method]((https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#readParquetFiles-io.delta.kernel.utils.CloseableIterator-io.delta.kernel.types.StructType-)) takes as input `FileStatus`s which contains metadata such as file path, size etc. of the Parquet file to read. The columns to be read from the Parquet file are defined by the physical schema. To implement this method, you may have to first implement your own [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) which is used to represent the in-memory data generated from the Parquet files.\n\nWhen identifying the columns to read, note that there are multiple types of columns in the physical schema (represented as a [`StructType`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructType.html)). \n\n* Data columns: Columns that are expected to be read from the Parquet file. Based on the `StructField` object defining the column, read the column in the Parquet file that matches the same name or field id. If the column has a field id (stored as `parquet.field.id` in the `StructField` metadata) then the field id should be used to match the column in the Parquet file. Otherwise, the column name should be used for matching.\n* Metadata columns: These are special columns that must be populated using metadata about the Parquet file ([`StructField#isMetadataColumn`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructField.html#isMetadataColumn--) tells whether a column in `StructType` is a metadata column). To understand how to populate such a column, first match the column name against the set of standard metadata column name constants. For example, \n    * `StructFileld#isMetadataColumn()` returns true and the column name is `StructField.METADATA_ROW_INDEX_COLUMN_NAME`, then you have to a generate column vector populated with the actual index of each row in the Parquet file (that is, not indexed by the possible subset of rows returned after Parquet data skipping).\n\n##### Requirements and guarantees\nAny implementation must adhere to the following guarantees.\n\n* The schema of the returned `ColumnarBatch`es must match the physical schema. \n  * If a data column is not found and the `StructField.isNullable = true`, then return a `ColumnVector` of nulls. Throw an error if it is not nullable.\n* The output iterator must maintain ordering as the input iterator. That is, if `file1` is before `file2` in the input iterator, then columnar batches of `file1` must be before those of `file2` in the output iterator.\n\n##### Method `writeParquetFiles(String directoryPath, CloseableIterator<FilteredColumnarBatch> dataIter, java.util.List<Column> statsColumns)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#writeParquetFiles-java.lang.String-io.delta.kernel.utils.CloseableIterator-java.util.List-) takes given data writes it into one or more Parquet files into the given directory. The data is given as an iterator of [FilteredColumnarBatches](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) which contains a [ColumnarBatch](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and an optional selection vector containing one entry for each row in `ColumnarBatch` indicating whether a row is selected or not selected. The `ColumnarBatch` also contains the schema of the data. This schema should be converted to Parquet schema, including any field IDs present [`FieldMetadata`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/FieldMetadata.html) for each column `StructField`.\n\nThere is also the parameter `statsColumns`, which is a hint to the Parquet writer on what set of columns to collect stats for each file. The statistics include `min`, `max` and `null_count` for each column in the `statsColumns` list. Statistics collection is optional, but when present it is used by Kernel to persist the stats as part of the Delta table commit. This will help read queries prune un-needed data files based on the query predicate.\n\nFor each written data file, the caller is expecting a [`DataFileStatus`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/utils/DataFileStatus.html) object. It contains the data file path, size, modification time, and optional column statistics.\n\n#### Method `writeParquetFileAtomically(String filePath, CloseableIterator<FilteredColumnarBatch> data)`\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ParquetHandler.html#writeParquetFiles-java.lang.String-io.delta.kernel.utils.CloseableIterator-java.util.List-) writes the given `data` into Parquet file at location `filePath`. The write is an atomic write i.e., either a Parquet file is created with all given content or no Parquet file is created at all. This should not create a file with partial content in it.\n\nThe default implementation makes use of [`LogStore`](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) implementations from the [`delta-storage`](https://github.com/delta-io/delta/tree/master/storage) module to accomplish the atomicity. A connector that wants to implement their own version of `ParquetHandler` can take a look at the default implementation for details.\n \n##### Performance suggestions\n* The representation of data as `ColumnVector`s and `ColumnarBatch`es can have a significant impact on the query performance and it's best to read the Parquet file data directly into vectors and batches of the engine-native format to avoid potentially costly in-memory data format conversion. Create a Kernel `ColumnVector` and `ColumnarBatch` wrappers around the engine-native format equivalent classes.\n\n#### Step 2.4: Implement `ExpressionHandler` interface\nThe [`ExpressionHandler`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html) interface has all the methods needed for handling expressions that may be applied on columnar data. \n\n##### Method `getEvaluator(StructType batchSchema, Expression expresion, DataType outputType)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#getEvaluator-io.delta.kernel.types.StructType-io.delta.kernel.expressions.Expression-io.delta.kernel.types.DataType-) generates an object of type [`ExpressionEvaluator`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/ExpressionEvaluator.html) that can evaluate the `expression` on a batch of row data to produce a result of a single column vector. To generate this function, the `getEvaluator()` method takes as input the expression and the schema of the `ColumnarBatch`es of data on which the expressions will be applied. The same object can be used to evaluate multiple columnar batches of input with the same schema and expression the evaluator is created for.\n\n##### Method `getPredicateEvaluator(StructType inputSchema, Predicate predicate)`\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#createSelectionVector-boolean:A-int-int-) is for creating an expression evaluator for `Predicate` type expressions. The `Predicate` type expressions return a boolean value as output.\n\nThe returned object is of type [`PredicateEvaluator`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/expressions/PredicateEvaluator.html). This is a special interface for evaluating Predicate on input batch returns a selection vector containing one value for each row in input batch indicating whether the row has passed the predicate or not. Optionally it takes an existing selection vector along with the input batch for evaluation. The result selection vector is combined with the given existing selection vector and a new selection vector is returned. This mechanism allows running an input batch through several predicate evaluations without rewriting the input batch to remove rows that do not pass the predicate after each predicate evaluation. The new selection should be the same or more selective as the existing selection vector. For example, if a row is marked as unselected in the existing selection vector, then it should remain unselected in the returned selection vector even when the given predicate returns true for the row.\n\n##### Method `createSelectionVector(boolean[] values, int from, int to)`\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/ExpressionHandler.html#createSelectionVector-boolean:A-int-int-) allows creating `ColumnVector` for boolean type values given as input. This allows the connector to maintain all `ColumnVector`s created in the desired memory format.\n\n##### Requirements and guarantees\nAny implementation must adhere to the following guarantees.\n\n* Implementation must handle all possible variations of expressions. If the implementation encounters an expression type that it does not know how to handle, then it must throw a specific language-dependent exception.\n  * Java: [NotSupportedException](https://docs.oracle.com/javaee/7/api/javax/resource/NotSupportedException.html) \n* The `ColumnarBatch`es on which the generated `ExpressionEvaluator` is going to be used are guaranteed to have the schema provided during generation. Hence, it is safe to bind the expression evaluation logic to column ordinals instead of column names, thus making the actual evaluation faster.\n\n#### Step 2.5: Implement `JsonHandler`\n[This](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html) engine interface allows the connector to use plug-in their own JSON handling code and expose it to the Delta Kernel.\n\n##### Method `readJsonFiles(CloseableIterator<FileStatus> fileIter, StructType physicalSchema, java.util.Optional<Predicate> predicate)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#readJsonFiles-io.delta.kernel.utils.CloseableIterator-io.delta.kernel.types.StructType-) takes as input `FileStatus`s of the JSON files and returns the data in a series of columnar batches. The columns to be read from the JSON file are defined by the physical schema, and the return batches must match that schema. To implement this method, you may have to first implement your own [`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html)and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) which is used to represent the in-memory data generated from the JSON files.\n\nWhen identifying the columns to read, note that there are multiple types of columns in the physical schema (represented as a [`StructType`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/types/StructType.html)). \n \n##### Method `parseJson(ColumnVector jsonStringVector, StructType outputSchema, java.util.Optional<ColumnVector> selectionVector)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#parseJson-io.delta.kernel.data.ColumnVector-io.delta.kernel.types.StructType-) allows parsing a `ColumnVector` of string values which are in JSON format into the output format specified by the `outputSchema`. If a given column in `outputSchema` is not found, then a null value is returned. It optionally takes a selection vector which indicates what entries in the input `ColumnVector` of strings to parse. If an entry is not selected then a `null` value is returned as parsed output for that particular entry in the output.\n\n##### Method `deserializeStructType(String structTypeJson)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#deserializeStructType-java.lang.String-) allows parsing JSON encoded (according to [Delta schema serialization rules](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#schema-serialization-format)) `StructType` schema into a `StructType`. Most implementations of `JsonHandler` do not need to implement this method and instead use the one in the [default `JsonHandler`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultJsonHandler.html) implementation.\n\n#### Method `writeJsonFileAtomically(String filePath, CloseableIterator<Row> data, boolean overwrite)`\n\nThis [method](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#writeJsonFileAtomically-java.lang.String-io.delta.kernel.utils.CloseableIterator-boolean-) writes the given `data` into a JSON file at location `filePath`. The write is an atomic write i.e., either a JSON file is created with all given content or no Parquet file is created at all. This should not create a file with partial content in it.\n\nThe default implementation makes use of [`LogStore`](https://github.com/delta-io/delta/blob/master/storage/src/main/java/io/delta/storage/LogStore.java) implementations from the [`delta-storage`](https://github.com/delta-io/delta/tree/master/storage) module to accomplish the atomicity. A connector that wants to implement their own version of `JsonHandler` can take a look at the default implementation for details.\n\nThe implementation is expected to handle the serialization rules (converting the `Row` object to JSON string) as described in the [API Javadoc](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/JsonHandler.html#writeJsonFileAtomically-java.lang.String-io.delta.kernel.utils.CloseableIterator-boolean-).\n\n#### Step 2.6: Implement `ColumnarBatch` and `ColumnVector`\n\n[`ColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnarBatch.html) and [`ColumnVector`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/ColumnVector.html) are two interfaces to represent the data read into memory from files. This representation can have a significant impact on query performance. Each engine likely has a native representation of in-memory data with which it applies data transformation operations. For example, in Apache Spark™, the row data is internally represented as `UnsafeRow` for efficient processing. So it's best to read the Parquet file data directly into vectors and batches of the native format to avoid potentially costly in-memory data format conversions. So the recommended approach is to build wrapper classes that extend the two interfaces but internally use engine-native classes to store the data. When the connector has to forward the columnar batches received from the kernel to the engine, it has to be smart enough to skip converting vectors and batches that are already in the engine-native format.\n\n### Step 3: Build read support in your connector\nIn this section, we are going to walk through the likely sequence of Kernel API calls your connector will have to make to read a table. The exact timing of making these calls in your connector in the context of connector-engine interactions depends entirely on the engine-connector APIs and is therefore beyond the scope of this guide. However, we will try to provide broad guidelines that are likely (but not guaranteed) to apply to your connector-engine setup. For this purpose, we are going to assume that the engine goes through the following phases when processing a read/scan query - logical plan analysis, physical plan generation, and physical plan execution. Based on these broad characterizations, a typical control and data flow for reading a Delta table is going to be as follows:\n\n<table>\n  <tr>\n   <td><strong>Step</strong>\n   </td>\n   <td><strong>Typical query phase when this step occurs</strong>\n   </td>\n  </tr>\n  <tr>\n   <td>Resolve the table snapshot to query\n   </td>\n   <td>Logical plan analysis phase when the plan's schema and other details need to be resolved and validated\n   </td>\n  </tr>\n  <tr>\n   <td>Resolve files to scan based on query parameters\n   </td>\n   <td>Physical plan generation, when the final parameters of the scan are available. For example: \n<ul>\n\n<li>Schema of data to read after pruning away unused columns\n\n<li>Query filters to apply after filter rearrangement\n</li>\n</ul>\n   </td>\n  </tr>\n  <tr>\n   <td>Distribute the file information to workers\n   </td>\n   <td>Physical plan execution, only if it is a distributed engine.\n   </td>\n  </tr>\n  <tr>\n   <td>Read the columnar data using the file information\n   </td>\n   <td>Physical plan execution, when the data is being processed by the engine \n   </td>\n  </tr>\n</table>\n\nLet's understand the details of each step.\n\n#### Step 3.1: Resolve the table snapshot to query\nThe first step is to resolve the consistent snapshot and the schema associated with it. This is often required by the connector/ engine to resolve and validate the logical plan of the scan query (if the concept of logical plan exists in your engine). To achieve this, the connector has to do the following.\n\n* Resolve the table path from the query: If the path is directly available, then this is easy. Otherwise, if it is a query based on a catalog table (for example, a Delta table defined in Hive Metastore), then the connector has to resolve the table path from the catalog.\n* Initialize the `Engine` object: Create a new instance of the `Engine` that you have chosen in [Step 2](#build-your-own Engine).\n* Initialize the Kernel objects and get the schema: Assuming the query is on the latest available version/snapshot of the table, you can get the table schema as follows:\n\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = new MyEngine();\nTable myTable = Table.forPath(myTablePath);\nSnapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\nStructType mySchema = mySnapshot.getSchema(myEngine);\n```\n\nIf you want to query a specific version of the table (that is, not the schema), then you can get the required snapshot as `myTable.getSnapshot(version)`.\n\n#### Step 3.2: Resolve files to scan\n\nNext, we need to build a Scan object using more information from the query. Here we are going to assume that the connector/engine has been able to extract the following details from the query (say, after optimizing the logical plan):\n\n* Read schema: The columns in the table that the query needs to read. This may be the full set of columns or a subset of columns.\n* Query filters: The filters on partitions or data columns that can be used skip reading table data.\n\nTo provide this information to Kernel, you have to do the following:\n\n* Convert the engine-specific schema and filter expressions to Kernel schema and expressions: For schema, you have to create a `StructType` object. For the filters, you have to create an `Expression` object using all the available subclasses of `Expression`. \n* Build the scan with the converted information: Build the scan as follows:\n\n```java\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.types.*;\n\nStructType readSchema = ... ;  // convert engine schema\nPredicate filterExpr = ... ;   // convert engine filter expression\n\nScan myScan = mySnapshot.getScanBuilder().withFilter(filterExpr).withReadSchema(readSchema).build();\n\n```\n\n* Resolve the information required to file reads: The generated Scan object has two sets of information. \n  * Scan files: `myScan.getScanFiles()` returns an iterator of `ColumnarBatch`es. Each batch in the iterator contains rows and each row has information about a single file that has been selected based on the query filter.\n  * Scan state: `myScan.getScanState()` returns a `Row` that contains all the information that is common across all the files that need to be read.\n\n```java\nRow myScanStateRow = myScan.getScanState();\nCloseableIterator<FilteredColumnarBatch> myScanFilesAsBatches = myScan.getScanFiles();\n\nwhile (myScanFilesAsBatches.hasNext()) {\n  FilteredColumnarBatch scanFileBatch = myScanFilesAsBatches.next();\n\n  CloseableIterator<Row> myScanFilesAsRows = scanFileBatch.getRows();\n}\n```\n\nAs we will soon see, reading the columnar data from a selected file will need to use both, the scan state row, and a scan file row with the file information.\n\n##### Requirements and guarantees\nHere are the details you need to ensure when defining this scan.\n\n* The provided `readSchema` must be the exact schema of the data that the engine will expect when executing the query. Any mismatch in the schema defined during this query planning and the query execution will result in runtime failures. Hence you must build the scan with the readSchema only after the engine has finalized the logical plan after any optimizations like column pruning.\n* When applicable (for example, with Java Kernel APIs), you have to make sure to call the close() method as you consume the `ColumnarBatch`es of scan files (that is, either serialize the rows or use them to read the table data).\n\n#### Step 3.3: Distribute the file information to the workers\nIf you are building a connector for a distributed engine like Spark/Presto/Trino/Flink, then your connector has to send all the scan metadata from the query planning machine (henceforth called the driver) to task execution machines (henceforth called the workers). You will have to serialize and deserialize the scan state and scan file rows. It is the connector job to implement serialization and deserialization utilities for a [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html). If the connector wants to split reading one scan file into multiple tasks, it can add additional connector specific split context to the task. At the task, the connector can use its own Parquet reader to read the specific part of the file indicated by the split info.\n\n##### Custom `Row` Serializer/Deserializer\nHere are steps on how to build your own serializer/deserializer such that it will work with any [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html) of any schema. \n\n* Serializing\n  * First serialize the row schema, that is, `StructType` object.\n  * Then, use the schema to identify types of each column/ordinal in the `Row` and use that to serialize all the values one by one.\n\n* Deserializing\n  * Define your own class that extends the Row interface. It must be able to handle complex types like arrays, nested structs and maps.\n  * First deserialize the schema.\n  * Then, use the schema to deserialize the values and put them in an instance of your custom Row class.\n\n```java\nimport io.delta.kernel.utils.*;\n\n// In the driver where query planning is being done\nByte[] scanStateRowBytes = RowUtils.serialize(scanStateRow);\nByte[] scanFileRowBytes = RowUtils.serialize(scanFileRow);\n\n// Optionally the connector adds a split info to the task (scan file, scan state) to\n// split reading of a Parquet file into multiple tasks. The task gets split info\n// along with the scan file row and scan state row.\nSplit split = ...; // connector specific class, not related to Kernel\n\n// Send these over to the worker\n\n// In the worker when data will be read, after rowBytes have been sent over\nRow scanStateRow = RowUtils.deserialize(scanStateRowBytes);\nRow scanFileRow = RowUtils.deserialize(scanFileRowBytes);\nSplit split = ... deserialize split info ...;\n```\n\n#### Step 3.4: Read the columnar data \nFinally, we are ready to read the columnar data. You will have to do the following:\n\n* Read the physical data from Parquet file as indicated by the scan file row, scan state, and optionally the split info\n* Convert the physical data into logical data of the table using the Kernel's APIs.\n\n```java\nRow scanStateRow = ... ;\nRow scanFileRow = ... ;\nSplit split = ...;\n\n// Additional option predicate such as dynamic filters the connector wants to\n// pass to the reader when reading files.\nPredicate optPredicate = ...;\n\n// Get the physical read schema of columns to read from the Parquet data files\nStructType physicalReadSchema =\n  ScanStateRow.getPhysicalDataReadSchema(engine, scanStateRow);\n\n// From the scan file row, extract the file path, size and modification metadata\n// needed to read the file.\nFileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow);\n\n// Open the scan file which is a Parquet file using connector's own\n// Parquet reader which supports reading specific parts (split) of the file.\n// If the connector doesn't have its own Parquet reader, it can use the\n// default Parquet reader provider which at the moment doesn't support reading\n// a specific part of the file, but reads the entire file from the beginning.\nCloseableIterator<ColumnarBatch> physicalDataIter =\n  connectParquetReader.readParquetFile(\n    fileStatus\n    physicalReadSchema,\n    split, // what part of the Parquet file to read data from\n    optPredicate /* additional predicate the connector can apply to filter data from the reader */\n  );\n\n// Now the physical data read from the Parquet data file is converted to logical data\n// the table represents.\n// Logical data may include the addition of partition columns and/or\n// subset of rows deleted\nCloseableIterator<FilteredColumnarBatch> transformedData =\n  Scan.transformPhysicalData(\n    engine,\n    scanState,\n    scanFileRow,\n    physicalDataIter));\n```\n\n* Resolve the data in the batches: Each [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) has two components:\n    * Columnar batch (returned by `FilteredColumnarBatch.getData()`): This is the data read from the files having the schema matching the readSchema provided when the Scan object was built in the earlier step.\n    * Optional selection vector (returned by `FilteredColumnarBatch.getSelectionVector()`): Optionally, a boolean vector that will define which rows in the batch are valid and should be consumed by the engine.\n\nIf the selection vector is present, then you will have to apply it to the batch to resolve the final consumable data. \n\n* Convert to engine-specific data format: Each connector/engine has its own native row / columnar batch formats and interfaces. To return the read data batches to the engine, you have to convert them to fit those engine-specific formats and/or interfaces. Here are a few tips that you can follow to make this efficient.\n  * Matching the engine-specific format: Some engines may expect the data in an in-memory format that may be different from the data produced by `getData()`. So you will have to do the data conversion for each column vector in the batch as needed. \n  * Matching the engine-specific interfaces: You may have to implement wrapper classes that extend the engine-specific interfaces and appropriately encapsulate the row data.\n\nFor best performance, you can implement your own Parquet reader and other `Engine` implementations to make sure that every `ColumnVector` generated is already in the engine-native format thus eliminating any need to convert.\n\nNow you should be able to read the Delta table correctly.\n\n### Step 4: Build append support in your connector\nIn this section, we are going to walk through the likely sequence of Kernel API calls your connector will have to make to append data to a table. The exact timing of making these calls in your connector in the context of connector-engine interactions depends entirely on the engine-connector APIs and is, therefore, beyond the scope of this guide. However, we will try to provide broad guidelines that are likely (but not guaranteed) to apply to your connector-engine setup. For this purpose, we are going to assume that the engine goes through the following phases when processing a write query - logical plan analysis, physical plan generation, and physical plan execution. Based on these broad characterizations, a typical control and data flow for reading a Delta table is going to be as follows:\n\n<table>\n  <tr>\n   <td><strong>Step</strong>\n   </td>\n   <td><strong>Typical query phase when this step occurs</strong>\n   </td>\n  </tr>\n  <tr>\n   <td>Determine the schema of the data that needs to be written to the table. Schema is derived from the existing table or from the parent operation of the `write` operator in the query plan when the table doesn't exist yet.\n   </td>\n   <td>Logical plan analysis phase when the plan's schema (`write` operator schema matches the table schema, etc.) and other details need to be resolved and validated.\n   </td>\n  </tr>\n  <tr>\n   <td>Determine the physical partitioning of the data based on the table schema and partition columns either from the existing table or from the query plan (for new tables)\n   </td>\n   <td>Physical plan generation, where the number of writer tasks, data schema and partitioning is determined</td>\n  </tr>\n  <tr>\n   <td>Distribute the writer tasks definitions (which include the transaction state) to workers. \n   </td>\n   <td>Physical plan execution, only if it is a distributed engine.\n   </td>\n  </tr>\n  <tr>\n   <td>Tasks write the data to data files and send the data file info to the driver.\n   </td>\n   <td>Physical plan execution, when the data is actually written to the table location\n   </td>\n  </tr>\n  <tr>\n   <td>Finalize the query. Here, all the info of the data files written by the tasks is aggregated and committed to the transaction created at the beginning of the physical execution.\n   </td>\n   <td>Finalize the query. This happens on the driver where the query has started.\n   </td>\n  </tr>\n</table>\n\nLet's understand the details of each step.\n\n#### Step 4.1: Determine the schema of the data that needs to be written to the table\nThe first step is to resolve the output data schema. This is often required by the connector/ engine to resolve and validate the logical plan of the  query (if the concept of logical plan exists in your engine). To achieve this, the connector has to do the following. At a high level query plan is a tree of operators where the leaf-level operators generate or read data from storage/tables and feed it upwards towards the parent operator nodes. This data transfer happens until it reaches the root operator node where the query is finalized (either the results are sent to the client or data is written to another table).\n\n* Create the `Table` object\n* From the `Table` object try to get the schema.\n  * If the table is not found\n    * the query includes creating the table (e.g., `CREATE TABLE AS` SQL query);\n      * the schema is derived from the operator above the `write` that feeds the data to the `write` operator.\n    * the query doesn't include creating new table, an exception is thrown saying the table is not found\n  * If the table already exists\n    * get the schema from the table and check if it matches the schema of the `write` operator. If not throw an exception. \n* Create a `TransactionBuilder` - this basically begins the steps of transaction construction.\n```java\nimport io.delta.kernel.*;\nimport io.delta.kernel.defaults.engine.*;\n\nEngine myEngine = new MyEngine();\nTable myTable = Table.forPath(myTablePath);\n\nStructType writeOperatorSchema = // ... derived from the query operator tree ...\nStructType dataSchema;\nboolean isNewTable = false;\n\ntry {\n  Snapshot mySnapshot = myTable.getLatestSnapshot(myEngine);\n  dataSchema = mySnapshot.getSchema(myEngine);\n\n  // .. check dataSchema and writeOperatorSchema match ...\n} catch(TableNotFoundException e) {\n  isNewTable = true;\n  dataSchema = writeOperatorSchema;\n}\n\nTransactionBuilder txnBuilder =\n  myTable.createTransactionBuilder(\n    myEngine,\n    \"Examples\", /* engineInfo - connector can add its own identifier which is noted in the Delta Log */ \n    Operation /* What is the operation we are trying to perform? This is noted in the Delta Log */\n  );\n\nif (isNewTable) {\n  // For a new table set the table schema in the transaction builder\n  txnBuilder = txnBuilder.withSchema(engine, dataSchema)\n}\n\n```\n\n#### Step 4.2: Determine the physical partitioning of the data based on the table schema and partition columns\n\nPartition columns are found either from the query (for new tables, the query defines the partition columns) or from the existing table.\n\n```java\nTransactionBuilder txnBuilder = ... from the last step ...\nTransaction txn;\n\nList<String> partitionColumns = ...\nif (newTable) {\n  partitionColumns = ... derive from the query parameters (ex. PARTITION BY clause in SQL) ...\n  txnBuilder = txnBuilder.withPartitionColumns(engine, partitionColumns);\n  txn = txnBuilder.build(engine);\n} else {\n  txn = txnBuilder.build(engine);\n  partitionColumns = txn.getPartitionColumns(engine);\n}\n```\n\nAt the end of this step, we have the `Transaction` and schema of the data to generate and its partitioning.\n\n#### Step 4.3: Distribute the writer tasks definitions (which include the transaction state) to workers\nIf you are building a connector for a distributed engine like Spark/Presto/Trino/Flink, then your connector has to send all the writer metadata from the query planning machine (henceforth called the driver) to task execution machines (henceforth called the workers). You will have to serialize and deserialize the transaction state. It is the connector job to implement serialization and deserialization utilities for a [`Row`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/Row.html). More details on a custom `Row` SerDe are found [here](#custom-row-serializerdeserializer).\n\n```java\nRow txnState = txn.getState(engine);\n\nString jsonTxnState = serializeToJson(txnState);\n```\n\n#### Step 4.4: Tasks write the data to data files and send the data file info to the driver.\nIn this step (which is executed on the worker nodes inside each task):\n* Deserialize the transaction state\n* Writer operator within the task gets the data from its parent operator.\n* The data is converted into a `FilteredColumnarBatch`. Each [`FilteredColumnarBatch`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/data/FilteredColumnarBatch.html) has two components:\n    * Columnar batch (returned by `FilteredColumnarBatch.getData()`): This is the data read from the files having the schema matching the readSchema provided when the Scan object was built in the earlier step.\n    * Optional selection vector (returned by `FilteredColumnarBatch.getSelectionVector()`): Optionally, a boolean vector that will define which rows in the batch are valid and should be consumed by the engine.\n* The connector can create `FilteredColumnBatch` wrapper around data in its own in-memory format.\n* Check if the data is partitioned or not. If not partitioned, partition the data by partition values.\n* For each partition generate the map of the partition column to the partition value\n* Use Kernel to convert the partitioned data into physical data that should go into the data files\n* Write the physical data into one or more data files.\n* Convert data file statues into a Delta log actions\n* Serialize the Delta log action `Row` objects and send them to the driver node\n\n```\nRow txnState = ... deserialize from JSON string sent by the driver ...\n\nCloseableIterator<FilteredColumnarBatch> data = ... generate data ...\n\n// If the table is un-partitioned then this is an empty map\nMap<String, Literal> partitionValues = ... prepare the partition values ...\n\n\n// First transform the logical data to physical data that needs to be written\n// to the Parquet files\nCloseableIterator<FilteredColumnarBatch> physicalData =\n  Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n// Get the write context\nDataWriteContext writeContext = Transaction.getWriteContext(engine, txnState, partitionValues);\n\n// Now write the physical data to Parquet files\nCloseableIterator<DataFileStatus> dataFiles =\n  engine.getParquetHandler()\n    .writeParquetFiles(\n      writeContext.getTargetDirectory(),\n      physicalData,\n      writeContext.getStatisticsColumns());\n\n// Now convert the data file status to data actions that needs to be written to the Delta table log\nCloseableIterator<Row> partitionDataActions =\n  Transaction.generateAppendActions(\n    engine,\n    txnState,\n    dataFiles,\n    writeContext);\n\n.... serialize `partitionDataActions` and send them to driver node\n```\n\n#### Step 4.5: Finalize the query.\nAt the driver node, the delta log actions from all the tasks are received and committed to the transaction. The tasks send the Delta log actions as a serialized JSON and deserialize them back to `Row` objects.\n\n```\n// Create a iterable out of the data actions. If the contents are too big to fit in memory,\n// the connector may choose to write the data actions to a temporary file and return an\n// iterator that reads from the file.\nCloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n\ttoCloseableIterator(dataActions.iterator()));\n\n// Commit the transaction.\nTransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n// Optional step\nif (commitResult.isReadyForCheckpoint()) {\n  // Checkpoint the table\n  Table.forPath(engine, tablePath).checkpoint(engine, commitResult.getVersion());\n}\n```\n\nThats it. Now you should be able to append data to Delta tables using the Kernel APIs.\n\n\n## Migration guide\nKernel APIs are still evolving and new features are being added. Kernel authors try to make the API changes backward compatible as much as they can with each new release, but sometimes it is hard to maintain the backward compatibility for a project that is evolving rapidly.\n\nThis section provides guidance on how to migrate your connector to the latest version of Delta Kernel. With each new release the [examples](https://github.com/delta-io/delta/tree/master/kernel/examples) are kept up-to-date with the latest API changes. You can refer to the examples to understand how to use the new APIs.\n\n### Migration from Delta Lake version 3.1.0 to 3.2.0\nFollowing are API changes in Delta Kernel 3.2.0 that may require changes in your connector.\n\n#### Rename `TableClient` to [`Engine`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/engine/Engine.html)\nThe `TableClient` interface has been renamed to `Engine`. This is the most significant API change in this release. The `TableClient` interface name is not exactly representing the functionality it provides. At a high level it provides capabilities such as reading Parquet files, JSON files, evaluating expressions on data and file system functionality. These are basically the heavy lift operations that Kernel depends on as a separate interface to allow the connectors to substitute their own custom implementation of the same functionality (e.g. custom Parquet reader). Essentially, these functionalities are the core of the `engine` functionalities. By renaming to `Engine`, we are representing the interface functionality with a proper name that is easy to understand.\n\nThe `DefaultTableClient` has been renamed to [`DefaultEngine`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultEngine.html).\n\n#### [`Table.forPath(Engine engine, String tablePath)`](https://delta-io.github.io/delta/snapshot/kernel-api/java/io/delta/kernel/Table.html#forPath-io.delta.kernel.engine.Engine-java.lang.String-) behavior change\nEarlier when a non-existent table path is passed, the API used to throw `TableNotFoundException`. Now it doesn't throw the exception. Instead, it returns a `Table` object. When trying to get a `Snapshot` from the table object it throws the `TableNotFoundException`.\n\n#### [`FileSystemClient.resolvePath`](https://delta-io.github.io/delta/snapshot/kernel-defaults/java/io/delta/kernel/defaults/engine/DefaultFileSystemClient.html#resolvePath-java.lang.String-) behavior change\nEarlier when a non-existent path is passed, the API used to throw `FileNotFoundException`. Now it doesn't throw the exception. It still resolves the given path into a fully qualified path.\n"
  },
  {
    "path": "kernel/build/sbt",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n\n# When creating new tests for Spark SQL Hive, the HADOOP_CLASSPATH must contain the hive jars so\n# that we can run Hive to generate the golden answer.  This is not required for normal development\n# or testing.\nif [ -n \"$HIVE_HOME\" ]; then\n    for i in \"$HIVE_HOME\"/lib/*\n    do HADOOP_CLASSPATH=\"$HADOOP_CLASSPATH:$i\"\n    done\n    export HADOOP_CLASSPATH\nfi\n\nrealpath () {\n(\n  TARGET_FILE=\"$1\"\n\n  cd \"$(dirname \"$TARGET_FILE\")\"\n  TARGET_FILE=\"$(basename \"$TARGET_FILE\")\"\n\n  COUNT=0\n  while [ -L \"$TARGET_FILE\" -a $COUNT -lt 100 ]\n  do\n      TARGET_FILE=\"$(readlink \"$TARGET_FILE\")\"\n      cd $(dirname \"$TARGET_FILE\")\n      TARGET_FILE=\"$(basename $TARGET_FILE)\"\n      COUNT=$(($COUNT + 1))\n  done\n\n  echo \"$(pwd -P)/\"$TARGET_FILE\"\"\n)\n}\n\nif [[ \"$JENKINS_URL\" != \"\" ]]; then\n  # Make Jenkins use Google Mirror first as Maven Central may ban us\n  SBT_REPOSITORIES_CONFIG=\"$(dirname \"$(realpath \"$0\")\")/sbt-config/repositories\"\n  export SBT_OPTS=\"-Dsbt.override.build.repos=true -Dsbt.repository.config=$SBT_REPOSITORIES_CONFIG\"\nfi\n\n. \"$(dirname \"$(realpath \"$0\")\")\"/sbt-launch-lib.bash\n\n\ndeclare -r noshare_opts=\"-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy\"\ndeclare -r sbt_opts_file=\".sbtopts\"\ndeclare -r etc_sbt_opts_file=\"/etc/sbt/sbtopts\"\n\nusage() {\n cat <<EOM\nUsage: $script_name [options]\n\n  -h | -help         print this message\n  -v | -verbose      this runner is chattier\n  -d | -debug        set sbt log level to debug\n  -no-colors         disable ANSI color codes\n  -sbt-create        start sbt even if current directory contains no sbt project\n  -sbt-dir   <path>  path to global settings/plugins directory (default: ~/.sbt)\n  -sbt-boot  <path>  path to shared boot directory (default: ~/.sbt/boot in 0.11 series)\n  -ivy       <path>  path to local Ivy repository (default: ~/.ivy2)\n  -mem    <integer>  set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem))\n  -no-share          use all local caches; no sharing\n  -no-global         uses global caches, but does not use global ~/.sbt directory.\n  -jvm-debug <port>  Turn on JVM debugging, open at the given port.\n  -batch             Disable interactive mode\n\n  # sbt version (default: from project/build.properties if present, else latest release)\n  -sbt-version  <version>   use the specified version of sbt\n  -sbt-jar      <path>      use the specified jar as the sbt launcher\n  -sbt-rc                   use an RC version of sbt\n  -sbt-snapshot             use a snapshot version of sbt\n\n  # java version (default: java from PATH, currently $(java -version 2>&1 | grep version))\n  -java-home <path>         alternate JAVA_HOME\n\n  # jvm options and output control\n  JAVA_OPTS          environment variable, if unset uses \"$java_opts\"\n  SBT_OPTS           environment variable, if unset uses \"$default_sbt_opts\"\n  .sbtopts           if this file exists in the current directory, it is\n                     prepended to the runner args\n  /etc/sbt/sbtopts   if this file exists, it is prepended to the runner args\n  -Dkey=val          pass -Dkey=val directly to the java runtime\n  -J-X               pass option -X directly to the java runtime\n                     (-J is stripped)\n  -S-X               add -X to sbt's scalacOptions (-S is stripped)\n  -PmavenProfiles    Enable a maven profile for the build.\n\nIn the case of duplicated or conflicting options, the order above\nshows precedence: JAVA_OPTS lowest, command line options highest.\nEOM\n}\n\nprocess_my_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n     -no-colors) addJava \"-Dsbt.log.noformat=true\" && shift ;;\n      -no-share) addJava \"$noshare_opts\" && shift ;;\n     -no-global) addJava \"-Dsbt.global.base=$(pwd)/project/.sbtboot\" && shift ;;\n      -sbt-boot) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.boot.directory=$2\" && shift 2 ;;\n       -sbt-dir) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.global.base=$2\" && shift 2 ;;\n     -debug-inc) addJava \"-Dxsbt.inc.debug=true\" && shift ;;\n         -batch) exec </dev/null && shift ;;\n\n    -sbt-create) sbt_create=true && shift ;;\n\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  # Now, ensure sbt version is used.\n  [[ \"${sbt_version}XXX\" != \"XXX\" ]] && addJava \"-Dsbt.version=$sbt_version\"\n}\n\nloadConfigFile() {\n  cat \"$1\" | sed '/^\\#/d'\n}\n\n# if sbtopts files exist, prepend their contents to $@ so it can be processed by this runner\n[[ -f \"$etc_sbt_opts_file\" ]] && set -- $(loadConfigFile \"$etc_sbt_opts_file\") \"$@\"\n[[ -f \"$sbt_opts_file\" ]] && set -- $(loadConfigFile \"$sbt_opts_file\") \"$@\"\n\nexit_status=127\nsaved_stty=\"\"\n\nrestoreSttySettings() {\n  stty $saved_stty\n  saved_stty=\"\"\n}\n\nonExit() {\n  if [[ \"$saved_stty\" != \"\" ]]; then\n    restoreSttySettings\n  fi\n  exit $exit_status\n}\n\nsaveSttySettings() {\n  saved_stty=$(stty -g 2>/dev/null)\n  if [[ ! $? ]]; then\n    saved_stty=\"\"\n  fi\n}\n\nsaveSttySettings\ntrap onExit INT\n\nrun \"$@\"\n\nexit_status=$?\nonExit\n"
  },
  {
    "path": "kernel/build/sbt-config/repositories",
    "content": "[repositories]\n  local\n  local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]\n  local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}\n  gcs-maven-central-mirror: https://maven-central.storage-download.googleapis.com/repos/central/data/\n  maven-central\n  typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly\n  sbt-ivy-snapshots: https://repo.scala-sbt.org/scalasbt/ivy-snapshots/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly\n  sbt-plugin-releases: https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]\n  bintray-typesafe-sbt-plugin-releases: https://dl.bintray.com/typesafe/sbt-plugins/, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]\n  bintray-spark-packages: https://dl.bintray.com/spark-packages/maven/\n  typesafe-releases: https://repo.typesafe.com/typesafe/releases/\n"
  },
  {
    "path": "kernel/build/sbt-launch-lib.bash",
    "content": "#!/usr/bin/env bash\n#\n\n# A library to simplify using the SBT launcher from other packages.\n# Note: This should be used by tools like giter8/conscript etc.\n\n# TODO - Should we merge the main SBT script with this library?\n\nif test -z \"$HOME\"; then\n  declare -r script_dir=\"$(dirname \"$script_path\")\"\nelse\n  declare -r script_dir=\"$HOME/.sbt\"\nfi\n\ndeclare -a residual_args\ndeclare -a java_args\ndeclare -a scalac_args\ndeclare -a sbt_commands\ndeclare -a maven_profiles\n\nif test -x \"$JAVA_HOME/bin/java\"; then\n    echo -e \"Using $JAVA_HOME as default JAVA_HOME.\"\n    echo \"Note, this will be overridden by -java-home if it is set.\"\n    declare java_cmd=\"$JAVA_HOME/bin/java\"\nelse\n    declare java_cmd=java\nfi\n\nechoerr () {\n  echo 1>&2 \"$@\"\n}\nvlog () {\n  [[ $verbose || $debug ]] && echoerr \"$@\"\n}\ndlog () {\n  [[ $debug ]] && echoerr \"$@\"\n}\n\nacquire_sbt_jar () {\n  SBT_VERSION=`awk -F \"=\" '/sbt\\.version/ {print $2}' ./project/build.properties`\n\n  # Download sbt from mirror URL if the environment variable is provided\n  if [[ \"${SBT_VERSION}\" == \"0.13.18\" ]] && [[ -n \"${SBT_MIRROR_JAR_URL}\" ]]; then\n    URL1=\"${SBT_MIRROR_JAR_URL}\"\n  elif [[ \"${SBT_VERSION}\" == \"1.5.5\" ]] && [[ -n \"${SBT_1_5_5_MIRROR_JAR_URL}\" ]]; then\n    URL1=\"${SBT_1_5_5_MIRROR_JAR_URL}\"\n  else\n    URL1=${DEFAULT_ARTIFACT_REPOSITORY:-https://repo1.maven.org/maven2/}org/scala-sbt/sbt-launch/${SBT_VERSION}/sbt-launch-${SBT_VERSION}.jar\n  fi\n\n  JAR=build/sbt-launch-${SBT_VERSION}.jar\n  sbt_jar=$JAR\n\n  if [[ ! -f \"$sbt_jar\" ]]; then\n    # Download sbt launch jar if it hasn't been downloaded yet\n    if [ ! -f \"${JAR}\" ]; then\n    # Download\n    printf 'Attempting to fetch sbt from %s\\n' \"${URL1}\"\n    JAR_DL=\"${JAR}.part\"\n    if [ $(command -v curl) ]; then\n      curl --fail --location --silent ${URL1} > \"${JAR_DL}\" &&\\\n        mv \"${JAR_DL}\" \"${JAR}\"\n    elif [ $(command -v wget) ]; then\n      wget --quiet ${URL1} -O \"${JAR_DL}\" &&\\\n        mv \"${JAR_DL}\" \"${JAR}\"\n    else\n      printf \"You do not have curl or wget installed, please install sbt manually from https://www.scala-sbt.org/\\n\"\n      exit -1\n    fi\n    fi\n    if [ ! -f \"${JAR}\" ]; then\n    # We failed to download\n    printf \"Our attempt to download sbt locally to ${JAR} failed. Please install sbt manually from https://www.scala-sbt.org/\\n\"\n    exit -1\n    fi\n    printf \"Launching sbt from ${JAR}\\n\"\n  fi\n}\n\nexecRunner () {\n  # print the arguments one to a line, quoting any containing spaces\n  [[ $verbose || $debug ]] && echo \"# Executing command line:\" && {\n    for arg; do\n      if printf \"%s\\n\" \"$arg\" | grep -q ' '; then\n        printf \"\\\"%s\\\"\\n\" \"$arg\"\n      else\n        printf \"%s\\n\" \"$arg\"\n      fi\n    done\n    echo \"\"\n  }\n\n  \"$@\"\n}\n\naddJava () {\n  dlog \"[addJava] arg = '$1'\"\n  java_args=( \"${java_args[@]}\" \"$1\" )\n}\n\nenableProfile () {\n  dlog \"[enableProfile] arg = '$1'\"\n  maven_profiles=( \"${maven_profiles[@]}\" \"$1\" )\n  export SBT_MAVEN_PROFILES=\"${maven_profiles[@]}\"\n}\n\naddSbt () {\n  dlog \"[addSbt] arg = '$1'\"\n  sbt_commands=( \"${sbt_commands[@]}\" \"$1\" )\n}\naddResidual () {\n  dlog \"[residual] arg = '$1'\"\n  residual_args=( \"${residual_args[@]}\" \"$1\" )\n}\naddDebugger () {\n  addJava \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1\"\n}\n\n# a ham-fisted attempt to move some memory settings in concert\n# so they need not be dicked around with individually.\nget_mem_opts () {\n  local mem=${1:-1000}\n  local perm=$(( $mem / 4 ))\n  (( $perm > 256 )) || perm=256\n  (( $perm < 4096 )) || perm=4096\n  local codecache=$(( $perm / 2 ))\n\n  echo \"-Xms${mem}m -Xmx${mem}m -XX:ReservedCodeCacheSize=${codecache}m\"\n}\n\nrequire_arg () {\n  local type=\"$1\"\n  local opt=\"$2\"\n  local arg=\"$3\"\n  if [[ -z \"$arg\" ]] || [[ \"${arg:0:1}\" == \"-\" ]]; then\n    echo \"$opt requires <$type> argument\" 1>&2\n    exit 1\n  fi\n}\n\nis_function_defined() {\n  declare -f \"$1\" > /dev/null\n}\n\nprocess_args () {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n       -h|-help) usage; exit 1 ;;\n    -v|-verbose) verbose=1 && shift ;;\n      -d|-debug) debug=1 && shift ;;\n\n           -ivy) require_arg path \"$1\" \"$2\" && addJava \"-Dsbt.ivy.home=$2\" && shift 2 ;;\n           -mem) require_arg integer \"$1\" \"$2\" && sbt_mem=\"$2\" && shift 2 ;;\n     -jvm-debug) require_arg port \"$1\" \"$2\" && addDebugger $2 && shift 2 ;;\n         -batch) exec </dev/null && shift ;;\n\n       -sbt-jar) require_arg path \"$1\" \"$2\" && sbt_jar=\"$2\" && shift 2 ;;\n   -sbt-version) require_arg version \"$1\" \"$2\" && sbt_version=\"$2\" && shift 2 ;;\n     -java-home) require_arg path \"$1\" \"$2\" && java_cmd=\"$2/bin/java\" && export JAVA_HOME=$2 && shift 2 ;;\n\n            -D*) addJava \"$1\" && shift ;;\n            -J*) addJava \"${1:2}\" && shift ;;\n            -P*) enableProfile \"$1\" && shift ;;\n              *) addResidual \"$1\" && shift ;;\n    esac\n  done\n\n  is_function_defined process_my_args && {\n    myargs=(\"${residual_args[@]}\")\n    residual_args=()\n    process_my_args \"${myargs[@]}\"\n  }\n}\n\nrun() {\n  # no jar? download it.\n  [[ -f \"$sbt_jar\" ]] || acquire_sbt_jar \"$sbt_version\" || {\n    # still no jar? uh-oh.\n    echo \"Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar\"\n    exit 1\n  }\n\n  # process the combined args, then reset \"$@\" to the residuals\n  process_args \"$@\"\n  set -- \"${residual_args[@]}\"\n  argumentCount=$#\n\n  # run sbt\n  execRunner \"$java_cmd\" \\\n    ${SBT_OPTS:-$default_sbt_opts} \\\n    $(get_mem_opts $sbt_mem) \\\n    ${java_opts} \\\n    ${java_args[@]} \\\n    -jar \"$sbt_jar\" \\\n    \"${sbt_commands[@]}\" \\\n    \"${residual_args[@]}\"\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--Copyright (2024) The Delta Lake Project Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.-->\n\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.example</groupId>\n    <artifactId>kernel-examples</artifactId>\n    <version>0.1-SNAPSHOT</version>\n\n    <properties>\n        <maven.compiler.source>1.8</maven.compiler.source>\n        <maven.compiler.target>1.8</maven.compiler.target>\n        <staging.repo.url>\"\"</staging.repo.url>\n        <delta-kernel.version>3.2.0-SNAPSHOT</delta-kernel.version>\n        <hadoop.version>3.3.1</hadoop.version>\n    </properties>\n\n    <repositories>\n        <repository>\n            <id>staging-repo</id>\n            <url>${staging.repo.url}</url>\n        </repository>\n    </repositories>\n\n    <dependencies>\n        <dependency>\n            <groupId>io.delta</groupId>\n            <artifactId>delta-kernel-api</artifactId>\n            <version>${delta-kernel.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>io.delta</groupId>\n            <artifactId>delta-kernel-defaults</artifactId>\n            <version>${delta-kernel.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>io.delta</groupId>\n            <artifactId>delta-storage</artifactId>\n            <version>${delta-kernel.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.hadoop</groupId>\n            <artifactId>hadoop-client-runtime</artifactId>\n            <version>${hadoop.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.hadoop</groupId>\n            <artifactId>hadoop-client-api</artifactId>\n            <version>${hadoop.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>commons-cli</groupId>\n            <artifactId>commons-cli</artifactId>\n            <version>1.5.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>2.13.5</version>\n        </dependency>\n\n    </dependencies>\n</project>\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/BaseTableReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples;\n\nimport java.io.IOException;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport static java.lang.String.format;\nimport static java.util.Objects.requireNonNull;\n\nimport org.apache.commons.cli.CommandLine;\nimport org.apache.commons.cli.Option;\nimport org.apache.commons.cli.Options;\nimport org.apache.commons.cli.ParseException;\nimport org.apache.hadoop.conf.Configuration;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterator;\n\nimport io.delta.kernel.defaults.engine.DefaultEngine;\n\n/**\n * Base class for reading Delta Lake tables using the Delta Kernel APIs.\n */\npublic abstract class BaseTableReader {\n    public static final int DEFAULT_LIMIT = 20;\n\n    protected final String tablePath;\n    protected final Engine engine;\n\n    public BaseTableReader(String tablePath) {\n        this.tablePath = requireNonNull(tablePath);\n        this.engine = DefaultEngine.create(new Configuration());\n    }\n\n    /**\n     * Show the given {@code limit} rows containing the given columns with the predicate from the\n     * table.\n     *\n     * @param limit        Max number of rows to show.\n     * @param columnsOpt   If null, show all columns in the table.\n     * @param predicateOpt Optional predicate\n     * @return Number of rows returned by the query.\n     * @throws TableNotFoundException\n     * @throws IOException\n     */\n    public abstract int show(\n        int limit,\n        Optional<List<String>> columnsOpt,\n        Optional<Predicate> predicateOpt) throws TableNotFoundException, IOException;\n\n    /**\n     * Utility method to return a pruned schema that contains the given {@code columns} from\n     * {@code baseSchema}\n     */\n    protected static StructType pruneSchema(StructType baseSchema, Optional<List<String>> columns) {\n        if (!columns.isPresent()) {\n            return baseSchema;\n        }\n        List<StructField> selectedFields = columns.get().stream().map(column -> {\n            if (baseSchema.indexOf(column) == -1) {\n                throw new IllegalArgumentException(\n                    format(\"Column %s is not found in table\", column));\n            }\n            return baseSchema.get(column);\n        }).collect(Collectors.toList());\n\n        return new StructType(selectedFields);\n    }\n\n    protected static int printData(FilteredColumnarBatch data, int maxRowsToPrint) {\n        int printedRowCount = 0;\n        try (CloseableIterator<Row> rows = data.getRows()) {\n            while (rows.hasNext()) {\n                printRow(rows.next());\n                printedRowCount++;\n                if (printedRowCount == maxRowsToPrint) {\n                    break;\n                }\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        return printedRowCount;\n    }\n\n    protected static void printSchema(StructType schema) {\n        System.out.printf(formatter(schema.length()), schema.fieldNames().toArray(new String[0]));\n    }\n\n    protected static void printRow(Row row){\n        int numCols = row.getSchema().length();\n        Object[] rowValues = IntStream.range(0, numCols)\n            .mapToObj(colOrdinal -> getValue(row, colOrdinal))\n            .toArray();\n\n        // TODO: Need to handle the Row, Map, Array, Timestamp, Date types specially to\n        // print them in the format they need. Copy this code from Spark CLI.\n\n        System.out.printf(formatter(numCols), rowValues);\n    }\n\n    /**\n     * Minimum command line options for any implementation of this reader.\n     */\n    protected static Options baseOptions() {\n        return new Options()\n                .addRequiredOption(\"t\", \"table\", true, \"Fully qualified table path\")\n                .addOption(\"c\", \"columns\", true,\n                        \"Comma separated list of columns to read from the table. \" +\n                                \"Ex. --columns=id,name,address\")\n                .addOption(\n                        Option.builder()\n                                .option(\"l\")\n                                .longOpt(\"limit\")\n                                .hasArg(true)\n                                .desc(\"Maximum number of rows to read from the table (default 20).\")\n                                .type(Number.class)\n                                .build()\n                );\n    }\n\n    protected static Optional<List<String>> parseColumnList(CommandLine cli, String optionName) {\n        return Optional.ofNullable(cli.getOptionValue(optionName))\n            .map(colString -> Arrays.asList(colString.split(\",[ ]*\")));\n    }\n\n    protected static int parseInt(CommandLine cli, String optionName, int defaultValue)\n        throws ParseException {\n        return Optional.ofNullable(cli.getParsedOptionValue(optionName))\n            .map(Number.class::cast)\n            .map(Number::intValue)\n            .orElse(defaultValue);\n    }\n\n    private static String formatter(int length) {\n        return IntStream.range(0, length)\n            .mapToObj(i -> \"%20s\")\n            .collect(Collectors.joining(\"|\")) + \"\\n\";\n    }\n\n    private static String getValue(Row row, int columnOrdinal) {\n        DataType dataType = row.getSchema().at(columnOrdinal).getDataType();\n        if (row.isNullAt(columnOrdinal)) {\n            return null;\n        } else if (dataType instanceof BooleanType) {\n            return Boolean.toString(row.getBoolean(columnOrdinal));\n        } else if (dataType instanceof ByteType) {\n            return Byte.toString(row.getByte(columnOrdinal));\n        } else if (dataType instanceof ShortType) {\n            return Short.toString(row.getShort(columnOrdinal));\n        } else if (dataType instanceof IntegerType) {\n            return Integer.toString(row.getInt(columnOrdinal));\n        } else if (dataType instanceof DateType) {\n            // DateType data is stored internally as the number of days since 1970-01-01\n            int daysSinceEpochUTC = row.getInt(columnOrdinal);\n            return LocalDate.ofEpochDay(daysSinceEpochUTC).toString();\n        } else if (dataType instanceof LongType) {\n            return Long.toString(row.getLong(columnOrdinal));\n        } else if (dataType instanceof TimestampType || dataType instanceof TimestampNTZType) {\n            // Timestamps are stored internally as the number of microseconds since epoch.\n            // TODO: TimestampType should use the session timezone to display values.\n            long microSecsSinceEpochUTC = row.getLong(columnOrdinal);\n            LocalDateTime dateTime = LocalDateTime.ofEpochSecond(\n                microSecsSinceEpochUTC / 1_000_000 /* epochSecond */,\n                (int) (1000 * microSecsSinceEpochUTC % 1_000_000) /* nanoOfSecond */,\n                ZoneOffset.UTC);\n            return dateTime.toString();\n        } else if (dataType instanceof FloatType) {\n            return Float.toString(row.getFloat(columnOrdinal));\n        } else if (dataType instanceof DoubleType) {\n            return Double.toString(row.getDouble(columnOrdinal));\n        } else if (dataType instanceof StringType) {\n            return row.getString(columnOrdinal);\n        } else if (dataType instanceof BinaryType) {\n            return new String(row.getBinary(columnOrdinal));\n        } else if (dataType instanceof DecimalType) {\n            return row.getDecimal(columnOrdinal).toString();\n        } else if (dataType instanceof StructType) {\n            return \"TODO: struct value\";\n        } else if (dataType instanceof ArrayType) {\n            return \"TODO: list value\";\n        } else if (dataType instanceof MapType) {\n            return \"TODO: map value\";\n        } else {\n            throw new UnsupportedOperationException(\"unsupported data type: \" + dataType);\n        }\n    }\n}\n\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/BaseTableWriter.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples;\n\nimport java.util.*;\n\nimport org.apache.hadoop.conf.Configuration;\n\nimport io.delta.kernel.TransactionCommitResult;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.types.*;\n\nimport io.delta.kernel.defaults.engine.DefaultEngine;\n\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch;\n\npublic class BaseTableWriter {\n\n    protected final Engine engine = DefaultEngine.create(new Configuration());\n\n    /**\n     * Schema used in examples for table create and/or writes\n     */\n    protected final StructType exampleTableSchema = new StructType()\n            .add(\"id\", IntegerType.INTEGER)\n            .add(\"name\", StringType.STRING)\n            .add(\"address\", StringType.STRING)\n            .add(\"salary\", DoubleType.DOUBLE);\n\n\n    /**\n     * Schema and partition columns used in examples for partitioned table create and/or writes.\n     */\n    protected final StructType examplePartitionedTableSchema = new StructType()\n            .add(\"id\", IntegerType.INTEGER)\n            .add(\"name\", StringType.STRING)\n            .add(\"city\", StringType.STRING)\n            .add(\"salary\", DoubleType.DOUBLE);\n    protected final List<String> examplePartitionColumns = Collections.singletonList(\"city\");\n\n\n    void verifyCommitSuccess(String tablePath, TransactionCommitResult result) {\n        // Verify the commit was successful\n        if (result.getVersion() >= 0) {\n            System.out.println(\"Table created successfully at: \" + tablePath);\n        } else {\n            // This should never happen. If there is a reason for table be not created\n            // `Transaction.commit` always throws an exception.\n            throw new RuntimeException(\"Table creation failed\");\n        }\n    }\n\n    /**\n     * Create data batch for a un-partitioned table with schema {@link #exampleTableSchema}.\n     *\n     * @param offset Offset that affects the generated data.\n     * @return\n     */\n    FilteredColumnarBatch generateUnpartitionedDataBatch(int offset) {\n        ColumnVector[] vectors = new ColumnVector[exampleTableSchema.length()];\n        // Create a batch with 5 rows\n\n        // id\n        vectors[0] = intVector(\n                Arrays.asList(offset, 1 + offset, 2 + offset, 3 + offset, 4 + offset));\n\n        // name\n        vectors[1] = stringVector(\n                Arrays.asList(\"Alice\", \"Bob\", \"Charlie\", \"David\", \"Eve\"));\n\n        // address\n        vectors[2] = stringVector(\n                Arrays.asList(\n                        \"123 Main St\",\n                        \"456 Elm St\",\n                        \"789 Cedar St\",\n                        \"101 Oak St\",\n                        \"121 Pine St\"));\n\n        // salary\n        vectors[3] = doubleVector(\n                Arrays.asList(\n                        100.0d + offset,\n                        200.0d + offset,\n                        300.0d + offset,\n                        400.0d + offset,\n                        500.0d + offset));\n\n        ColumnarBatch batch = new DefaultColumnarBatch(5, exampleTableSchema, vectors);\n        return new FilteredColumnarBatch(\n                batch, // data\n                // Optional selection vector. If want to write only a subset of rows from the batch.\n                Optional.empty());\n    }\n\n    /**\n     * Create data batch for a partitioned table with schema {@link #examplePartitionedTableSchema}.\n     *\n     * @param offset Offset that affects the generated data.\n     * @param city   City value for the partition column.\n     * @return\n     */\n    FilteredColumnarBatch generatedPartitionedDataBatch(int offset, String city) {\n        ColumnVector[] vectors = new ColumnVector[examplePartitionedTableSchema.length()];\n        // Create a batch with 5 rows\n\n        // id\n        vectors[0] = intVector(\n                Arrays.asList(offset, 1 + offset, 2 + offset, 3 + offset, 4 + offset));\n\n        // name\n        vectors[1] = stringVector(\n                Arrays.asList(\"Alice\", \"Bob\", \"Charlie\", \"David\", \"Eve\"));\n\n        // city - given city is a partition column we expect the batch to contain the same\n        // value for all rows.\n        vectors[2] = stringSingleValueVector(city, 5);\n\n        // salary\n        vectors[3] = doubleVector(\n                Arrays.asList(\n                        100.0d + offset,\n                        200.0d + offset,\n                        300.0d + offset,\n                        400.0d + offset,\n                        500.0d + offset));\n\n        ColumnarBatch batch = new DefaultColumnarBatch(5, examplePartitionedTableSchema, vectors);\n        return new FilteredColumnarBatch(\n                batch, // data\n                // Optional selection vector. If want to write only a subset of rows from the batch.\n                Optional.empty());\n    }\n\n\n    //////////////////////// Helper methods to create ColumnVectors ////////////////////////\n    // These are sample vectors which can be created as wrappers as engine specific       //\n    // vector types.                                                                      //\n    ////////////////////////////////////////////////////////////////////////////////////////\n    static ColumnVector intVector(List<Integer> data) {\n        return new ColumnVector() {\n            @Override\n            public DataType getDataType() {\n                return IntegerType.INTEGER;\n            }\n\n            @Override\n            public int getSize() {\n                return data.size();\n            }\n\n            @Override\n            public void close() {\n            }\n\n            @Override\n            public boolean isNullAt(int rowId) {\n                return data.get(rowId) == null;\n            }\n\n            @Override\n            public int getInt(int rowId) {\n                return data.get(rowId);\n            }\n        };\n    }\n\n    static ColumnVector doubleVector(List<Double> data) {\n        return new ColumnVector() {\n            @Override\n            public DataType getDataType() {\n                return DoubleType.DOUBLE;\n            }\n\n            @Override\n            public int getSize() {\n                return data.size();\n            }\n\n            @Override\n            public void close() {\n            }\n\n            @Override\n            public boolean isNullAt(int rowId) {\n                return data.get(rowId) == null;\n            }\n\n            @Override\n            public double getDouble(int rowId) {\n                return data.get(rowId);\n            }\n        };\n    }\n\n    static ColumnVector stringVector(List<String> data) {\n        return new ColumnVector() {\n            @Override\n            public DataType getDataType() {\n                return StringType.STRING;\n            }\n\n            @Override\n            public int getSize() {\n                return data.size();\n            }\n\n            @Override\n            public void close() {\n            }\n\n            @Override\n            public boolean isNullAt(int rowId) {\n                return data.get(rowId) == null;\n            }\n\n            @Override\n            public String getString(int rowId) {\n                return data.get(rowId);\n            }\n        };\n    }\n\n    static ColumnVector stringSingleValueVector(String value, int size) {\n        return new ColumnVector() {\n            @Override\n            public DataType getDataType() {\n                return StringType.STRING;\n            }\n\n            @Override\n            public int getSize() {\n                return size;\n            }\n\n            @Override\n            public void close() {\n\n            }\n\n            @Override\n            public boolean isNullAt(int rowId) {\n                return value == null;\n            }\n\n            @Override\n            public String getString(int rowId) {\n                return value;\n            }\n        };\n    }\n\n\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/CreateTable.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples;\n\nimport org.apache.commons.cli.Options;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.utils.CloseableIterable;\nimport static io.delta.kernel.examples.utils.Utils.parseArgs;\n\n/**\n * Example program to create a Delta table (no data is written) using the Kernel APIs.\n * <p>\n * Creates two tables with the following schema and partition columns in the input given directory\n * location.\n * <pre>\n *     Table 1: un-partitioned table\n *     CREATE TABLE example (id INT, name STRING, address STRING, salary DECIMAL(1, 3))\n *\n *     Table 2: partitioned table\n *     CREATE TABLE example_partitioned (id INT, name STRING, salary DECIMAL(1, 3), city STRING))\n *     PARTITIONED BY (city)\n * </pre>\n * <p>\n * <p>\n * It prints the table locations at the end of the successful execution.\n */\npublic class CreateTable extends BaseTableWriter {\n    public static void main(String[] args)\n            throws Exception {\n        Options options = new Options()\n                .addOption(\"l\", \"location\", true, \"Locations where the sample tables are created\");\n\n        new CreateTable().runExamples(parseArgs(options, args).getOptionValue(\"location\"));\n    }\n\n    public void runExamples(String location) {\n        createUnpartitionedTable(location + \"/example\");\n        createPartitionedTable(location + \"/example_partitioned\");\n    }\n\n    public void createUnpartitionedTable(String tablePath) {\n        // Create a `Table` object with the given destination table path\n        Table table = Table.forPath(engine, tablePath);\n\n        // Create a transaction builder to build the transaction\n        TransactionBuilder txnBuilder =\n                table.createTransactionBuilder(\n                        engine,\n                        \"Examples\", /* engineInfo */\n                        Operation.CREATE_TABLE);\n\n        // Set the schema of the new table on the transaction builder\n        txnBuilder = txnBuilder.withSchema(engine, exampleTableSchema);\n\n        // Build the transaction\n        Transaction txn = txnBuilder.build(engine);\n\n        // Commit the transaction.\n        // As we are just creating the table and not adding any data, the `dataActions` is empty.\n        TransactionCommitResult commitResult =\n                txn.commit(\n                        engine,\n                        CloseableIterable.emptyIterable() /* dataActions */);\n\n        // Check the transaction commit result\n        verifyCommitSuccess(tablePath, commitResult);\n    }\n\n    public void createPartitionedTable(String tablePath) {\n        // Create a `Table` object with the given destination table path\n        Table table = Table.forPath(engine, tablePath);\n\n        // Create a transaction builder to build the transaction\n        TransactionBuilder txnBuilder =\n                table.createTransactionBuilder(\n                        engine,\n                        \"Examples\", /* engineInfo */\n                        Operation.CREATE_TABLE);\n\n        txnBuilder = txnBuilder\n                // Set the schema of the new table\n                .withSchema(engine, examplePartitionedTableSchema)\n                // set the partition columns of the new table\n                .withPartitionColumns(engine, examplePartitionColumns);\n\n        // Build the transaction\n        Transaction txn = txnBuilder.build(engine);\n\n        // Commit the transaction.\n        // As we are just creating the table and not adding any data, the `dataActions` is empty.\n        TransactionCommitResult commitResult =\n                txn.commit(\n                        engine,\n                        CloseableIterable.emptyIterable() /* dataActions */);\n\n        // Check the transaction commit result\n        verifyCommitSuccess(tablePath, commitResult);\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/CreateTableAndInsertData.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples;\n\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\n\nimport org.apache.commons.cli.Options;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.hook.PostCommitHook.PostCommitHookType;\nimport io.delta.kernel.utils.*;\nimport static io.delta.kernel.examples.utils.Utils.parseArgs;\n\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\n\n/**\n * Example program that demonstrates how to:\n *\n * <ul>\n *     <li>\n *         create a partiitoned and unpartitioned table and insert data into it\n *         (Basically the CREATE TABLE AS <query> command).\n *     </li>\n *     <li>\n *          Insert into an existing table\n *      </li>\n *      <li>\n *          Idempotent data write to a table.\n *      </li>\n * </ul>\n */\npublic class CreateTableAndInsertData extends BaseTableWriter {\n    public static void main(String[] args) throws IOException {\n        Options options = new Options()\n                .addOption(\"l\", \"location\", true, \"Locations where the sample tables are created\");\n\n        new CreateTableAndInsertData().runExamples(\n                parseArgs(options, args).getOptionValue(\"location\"));\n\n    }\n\n    public void runExamples(String location) throws IOException {\n        String unpartitionedTblPath = location + \"/example\";\n        String partitionTblPath = location + \"/example_partitioned\";\n\n        // CTAS example for unpartitioned tables\n        createTableWithSampleData(unpartitionedTblPath);\n\n        // CTAS example for partitioned tables\n        createPartitionedTableWithSampleData(partitionTblPath);\n\n        // Insert into an existing table.\n        insertDataIntoUnpartitionedTable(unpartitionedTblPath);\n\n        // Example of idempotent inserts\n        idempotentInserts(unpartitionedTblPath);\n\n        // Example of checkpointg\n        insertWithOptionalCheckpoint(unpartitionedTblPath);\n    }\n\n    public TransactionCommitResult createTableWithSampleData(String tablePath) throws IOException {\n        // Create a `Table` object with the given destination table path\n        Table table = Table.forPath(engine, tablePath);\n\n        // Create a transaction builder to build the transaction\n        TransactionBuilder txnBuilder =\n                table.createTransactionBuilder(\n                        engine,\n                        \"Examples\", /* engineInfo */\n                        Operation.CREATE_TABLE);\n\n        // Set the schema of the new table on the transaction builder\n        txnBuilder = txnBuilder.withSchema(engine, exampleTableSchema);\n\n        // Build the transaction\n        Transaction txn = txnBuilder.build(engine);\n\n        // Get the transaction state\n        Row txnState = txn.getTransactionState(engine);\n\n        // Generate the sample data for the table that confirms to the table schema\n        FilteredColumnarBatch batch1 = generateUnpartitionedDataBatch(5 /* offset */);\n        FilteredColumnarBatch batch2 = generateUnpartitionedDataBatch(10 /* offset */);\n        FilteredColumnarBatch batch3 = generateUnpartitionedDataBatch(25 /* offset */);\n        CloseableIterator<FilteredColumnarBatch> data =\n                toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n        // First transform the logical data to physical data that needs to be written to the Parquet\n        // files\n        CloseableIterator<FilteredColumnarBatch> physicalData =\n                Transaction.transformLogicalData(\n                        engine,\n                        txnState,\n                        data,\n                        // partition values - as this table is unpartitioned, it should be empty\n                        Collections.emptyMap());\n\n        // Get the write context\n        DataWriteContext writeContext = Transaction.getWriteContext(\n                engine,\n                txnState,\n                // partition values - as this table is unpartitioned, it should be empty\n                Collections.emptyMap());\n\n\n        // Now write the physical data to Parquet files\n        CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n                .writeParquetFiles(\n                        writeContext.getTargetDirectory(),\n                        physicalData,\n                        writeContext.getStatisticsColumns());\n\n\n        // Now convert the data file status to data actions that needs to be written to the Delta\n        // table log\n        CloseableIterator<Row> dataActions =\n                Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);\n\n\n        // Create a iterable out of the data actions. If the contents are too big to fit in memory,\n        // the connector may choose to write the data actions to a temporary file and return an\n        // iterator that reads from the file.\n        CloseableIterable<Row> dataActionsIterable =\n                CloseableIterable.inMemoryIterable(dataActions);\n\n        // Commit the transaction.\n        TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n        // Check the transaction commit result\n        verifyCommitSuccess(tablePath, commitResult);\n\n        return commitResult;\n    }\n\n    public TransactionCommitResult createPartitionedTableWithSampleData(String tablePath) throws IOException {\n        // Create a `Table` object with the given destination table path\n        Table table = Table.forPath(engine, tablePath);\n\n        // Create a transaction builder to build the transaction\n        TransactionBuilder txnBuilder =\n                table.createTransactionBuilder(\n                        engine,\n                        \"Examples\", /* engineInfo */\n                        Operation.CREATE_TABLE);\n\n        txnBuilder = txnBuilder\n                // Set the schema of the new table\n                .withSchema(engine, examplePartitionedTableSchema)\n                // set the partition columns of the new table\n                .withPartitionColumns(engine, examplePartitionColumns);\n\n        // Build the transaction\n        Transaction txn = txnBuilder.build(engine);\n\n        // Get the transaction state\n        Row txnState = txn.getTransactionState(engine);\n\n        List<Row> dataActions = new ArrayList<>();\n\n        // Generate the sample data for three partitions. Process each partition separately.\n        // This is just an example. In a real-world scenario, the data may come from different\n        // partitions. Connectors already have the capability to partition by partition values\n        // before writing to the table\n\n        // In the test data `city` is a partition column\n        for (String city : Arrays.asList(\"San Francisco\", \"Campbell\", \"San Jose\")) {\n            FilteredColumnarBatch batch1 = generatedPartitionedDataBatch(\n                    5 /* offset */, city /* partition value */);\n            FilteredColumnarBatch batch2 = generatedPartitionedDataBatch(\n                    5 /* offset */, city /* partition value */);\n            FilteredColumnarBatch batch3 = generatedPartitionedDataBatch(\n                    10 /* offset */, city /* partition value */);\n\n            CloseableIterator<FilteredColumnarBatch> data =\n                    toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n            // Create partition value map\n            Map<String, Literal> partitionValues =\n                    Collections.singletonMap(\n                            \"city\", // partition column name\n                            // partition value. Depending upon the parition column type, the\n                            // partition value should be created. In this example, the partition\n                            // column is of type StringType, so we are creating a string literal.\n                            Literal.ofString(city));\n\n\n            // First transform the logical data to physical data that needs to be written\n            // to the Parquet\n            // files\n            CloseableIterator<FilteredColumnarBatch> physicalData =\n                    Transaction.transformLogicalData(engine, txnState, data, partitionValues);\n\n            // Get the write context\n            DataWriteContext writeContext =\n                    Transaction.getWriteContext(engine, txnState, partitionValues);\n\n\n            // Now write the physical data to Parquet files\n            CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n                    .writeParquetFiles(\n                            writeContext.getTargetDirectory(),\n                            physicalData,\n                            writeContext.getStatisticsColumns());\n\n\n            // Now convert the data file status to data actions that needs to be written to the Delta\n            // table log\n            CloseableIterator<Row> partitionDataActions = Transaction.generateAppendActions(\n                    engine,\n                    txnState,\n                    dataFiles,\n                    writeContext);\n\n            // Now add all the partition data actions to the main data actions list. In a\n            // distributed query engine, the partition data is written to files at tasks on executor\n            // nodes. The data actions are collected at the driver node and then written to the\n            // Delta table log using the `Transaction.commit`\n            while (partitionDataActions.hasNext()) {\n                dataActions.add(partitionDataActions.next());\n            }\n        }\n\n\n        // Create a iterable out of the data actions. If the contents are too big to fit in memory,\n        // the connector may choose to write the data actions to a temporary file and return an\n        // iterator that reads from the file.\n        CloseableIterable<Row> dataActionsIterable = CloseableIterable.inMemoryIterable(\n                toCloseableIterator(dataActions.iterator()));\n\n        // Commit the transaction.\n        TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n        // Check the transaction commit result\n        verifyCommitSuccess(tablePath, commitResult);\n\n        return commitResult;\n    }\n\n\n    public TransactionCommitResult insertDataIntoUnpartitionedTable(String tablePath) throws IOException {\n        // Create a `Table` object with the given destination table path\n        Table table = Table.forPath(engine, tablePath);\n\n        // Create a transaction builder to build the transaction\n        TransactionBuilder txnBuilder =\n                table.createTransactionBuilder(\n                        engine,\n                        \"Examples\", /* engineInfo */\n                        Operation.WRITE);\n\n        // Build the transaction - no need to provide the schema as the table already exists.\n        Transaction txn = txnBuilder.build(engine);\n\n        // Get the transaction state\n        Row txnState = txn.getTransactionState(engine);\n\n        // Generate the sample data for the table that confirms to the table schema\n        FilteredColumnarBatch batch1 = generateUnpartitionedDataBatch(5 /* offset */);\n        FilteredColumnarBatch batch2 = generateUnpartitionedDataBatch(10 /* offset */);\n        FilteredColumnarBatch batch3 = generateUnpartitionedDataBatch(25 /* offset */);\n        CloseableIterator<FilteredColumnarBatch> data =\n                toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n        // First transform the logical data to physical data that needs to be written to the Parquet\n        // files\n        CloseableIterator<FilteredColumnarBatch> physicalData =\n                Transaction.transformLogicalData(\n                        engine,\n                        txnState,\n                        data,\n                        // partition values - as this table is unpartitioned, it should be empty\n                        Collections.emptyMap());\n\n        // Get the write context\n        DataWriteContext writeContext = Transaction.getWriteContext(\n                engine,\n                txnState,\n                // partition values - as this table is unpartitioned, it should be empty\n                Collections.emptyMap());\n\n\n        // Now write the physical data to Parquet files\n        CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n                .writeParquetFiles(\n                        writeContext.getTargetDirectory(),\n                        physicalData,\n                        writeContext.getStatisticsColumns());\n\n\n        // Now convert the data file status to data actions that needs to be written to the Delta\n        // table log\n        CloseableIterator<Row> dataActions =\n                Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);\n\n\n        // Create a iterable out of the data actions. If the contents are too big to fit in memory,\n        // the connector may choose to write the data actions to a temporary file and return an\n        // iterator that reads from the file.\n        CloseableIterable<Row> dataActionsIterable =\n                CloseableIterable.inMemoryIterable(dataActions);\n\n        // Commit the transaction.\n        TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n        // Check the transaction commit result\n        verifyCommitSuccess(tablePath, commitResult);\n\n        return commitResult;\n    }\n\n    public TransactionCommitResult idempotentInserts(String tablePath) throws IOException {\n        // Create a `Table` object with the given destination table path\n        Table table = Table.forPath(engine, tablePath);\n\n        // Create a transaction builder to build the transaction\n        TransactionBuilder txnBuilder =\n                table.createTransactionBuilder(\n                        engine,\n                        \"Examples\", /* engineInfo */\n                        Operation.WRITE);\n\n        // Set the transaction identifiers for idempotent writes\n        // Delta/Kernel makes sure that there exists only one transaction in the Delta log\n        // with the given application id and txn version\n        txnBuilder = txnBuilder.withTransactionId(\n                engine,\n                \"my app id\", /* application id */\n                100 /* txn version */);\n\n        // Build the transaction - no need to provide the schema as the table already exists.\n        Transaction txn = txnBuilder.build(engine);\n\n        // Get the transaction state\n        Row txnState = txn.getTransactionState(engine);\n\n        // Generate the sample data for the table that confirms to the table schema\n        FilteredColumnarBatch batch1 = generateUnpartitionedDataBatch(5 /* offset */);\n        FilteredColumnarBatch batch2 = generateUnpartitionedDataBatch(10 /* offset */);\n        FilteredColumnarBatch batch3 = generateUnpartitionedDataBatch(25 /* offset */);\n        CloseableIterator<FilteredColumnarBatch> data =\n                toCloseableIterator(Arrays.asList(batch1, batch2, batch3).iterator());\n\n        // First transform the logical data to physical data that needs to be written to the Parquet\n        // files\n        CloseableIterator<FilteredColumnarBatch> physicalData =\n                Transaction.transformLogicalData(\n                        engine,\n                        txnState,\n                        data,\n                        // partition values - as this table is unpartitioned, it should be empty\n                        Collections.emptyMap());\n\n        // Get the write context\n        DataWriteContext writeContext = Transaction.getWriteContext(\n                engine,\n                txnState,\n                // partition values - as this table is unpartitioned, it should be empty\n                Collections.emptyMap());\n\n\n        // Now write the physical data to Parquet files\n        CloseableIterator<DataFileStatus> dataFiles = engine.getParquetHandler()\n                .writeParquetFiles(\n                        writeContext.getTargetDirectory(),\n                        physicalData,\n                        writeContext.getStatisticsColumns());\n\n\n        // Now convert the data file status to data actions that needs to be written to the Delta\n        // table log\n        CloseableIterator<Row> dataActions =\n                Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);\n\n\n        // Create a iterable out of the data actions. If the contents are too big to fit in memory,\n        // the connector may choose to write the data actions to a temporary file and return an\n        // iterator that reads from the file.\n        CloseableIterable<Row> dataActionsIterable =\n                CloseableIterable.inMemoryIterable(dataActions);\n\n        // Commit the transaction.\n        TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);\n\n        // Check the transaction commit result\n        verifyCommitSuccess(tablePath, commitResult);\n        return commitResult;\n    }\n\n    public void insertWithOptionalCheckpoint(String tablePath) throws IOException {\n        boolean didCheckpoint = false;\n        // insert data multiple times to trigger a checkpoint. By default checkpoint is needed\n        // for every 10 versions.\n        for (int i = 0; i < 12; i++) {\n            TransactionCommitResult commitResult = insertDataIntoUnpartitionedTable(tablePath);\n            for(PostCommitHook hook: commitResult.getPostCommitHooks())\n                // Checkpoint the table\n                didCheckpoint = didCheckpoint || CompletableFuture.supplyAsync(() -> {\n                    // run the code async\n                    try{\n                        hook.threadSafeInvoke(engine);\n                    } catch (IOException e) {\n                        return false;\n                    }\n                    return hook.getType().equals(PostCommitHookType.CHECKPOINT);\n                }).join(); // wait async finish.\n        }\n\n        if (!didCheckpoint) {\n            throw new RuntimeException(\"Table should have checkpointed by now\");\n        }\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/MultiThreadedTableReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicReference;\n\nimport org.apache.commons.cli.CommandLine;\nimport org.apache.commons.cli.Option;\nimport org.apache.commons.cli.Options;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.examples.utils.RowSerDe;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\n\nimport static io.delta.kernel.examples.utils.Utils.parseArgs;\n\nimport io.delta.kernel.internal.InternalScanFileUtils;\nimport io.delta.kernel.internal.data.ScanStateRow;\nimport io.delta.kernel.internal.util.Utils;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\n\n/**\n * Multi-threaded Delta Lake table reader using the Delta Kernel APIs. It illustrates\n * how to use the scan files rows received from the Delta Kernel in distributed engine.\n * <p>\n * For this example serialization and deserialization is not needed as the work generator and\n * work executors share the same memory, but it illustrates an example of how Delta Kernel can\n * work in a distributed query engine. High level steps are:\n * - The query engine asks the Delta Kernel APIs for scan file and scan state rows at the driver\n * (or equivalent) node\n * - The query engine serializes the scan file and scan state at the driver node\n * - The driver sends the serialized bytes to remote worker node(s)\n * - Worker nodes deserialize the scan file and scan state rows from the serialized bytes\n * - Worker nodes read the data from given scan file(s) and scan state using the Delta Kernel APIs.\n *\n * <p>\n * Usage:\n * java io.delta.kernel.examples.SingleThreadedTableReader [-c <arg>][-l <arg>] [-p <arg>] -t <arg>\n * -c,--columns <arg>       Comma separated list of columns to read from the\n * table. Ex. --columns=id,name,address\n * -l,--limit <arg>         Maximum number of rows to read from the table (default 20).\n * -p,--parallelism <arg>   Number of parallel readers to use (default 3).\n * -t,--table <arg>         Fully qualified table path\n * </p>\n */\npublic class MultiThreadedTableReader\n    extends BaseTableReader {\n    private static final int DEFAULT_NUM_THREADS = 3;\n\n    private final int numThreads;\n\n    public MultiThreadedTableReader(int numThreads, String tablePath) {\n        super(tablePath);\n        this.numThreads = numThreads;\n    }\n\n    public int show(int limit, Optional<List<String>> columnsOpt, Optional<Predicate> predicate)\n        throws TableNotFoundException {\n        Table table = Table.forPath(engine, tablePath);\n        Snapshot snapshot = table.getLatestSnapshot(engine);\n        StructType readSchema = pruneSchema(snapshot.getSchema(), columnsOpt);\n\n        ScanBuilder scanBuilder = snapshot.getScanBuilder().withReadSchema(readSchema);\n\n        if (predicate.isPresent()) {\n            scanBuilder = scanBuilder.withFilter(predicate.get());\n        }\n\n        return new Reader(limit)\n            .readData(readSchema, scanBuilder.build());\n    }\n\n    public static void main(String[] args)\n        throws Exception {\n        Options cliOptions = baseOptions().addOption(\n            Option.builder()\n                .option(\"p\")\n                .longOpt(\"parallelism\")\n                .hasArg()\n                .desc(\"Number of parallel readers to use (default 3).\")\n                .type(Number.class)\n                .build());\n        CommandLine commandLine = parseArgs(cliOptions, args);\n\n        String tablePath = commandLine.getOptionValue(\"table\");\n        int limit = parseInt(commandLine, \"limit\", DEFAULT_LIMIT);\n        int numThreads = parseInt(commandLine, \"parallelism\", DEFAULT_NUM_THREADS);\n        Optional<List<String>> columns = parseColumnList(commandLine, \"columns\");\n\n        new MultiThreadedTableReader(numThreads, tablePath)\n            .show(limit, columns, Optional.empty());\n    }\n\n    /**\n     * Work unit representing the scan state and scan file in serialized format.\n     */\n    private static class ScanFile {\n        /**\n         * Special instance of the {@link ScanFile} to indicate to the worker that there are no\n         * more scan files to scan and stop the worker thread.\n         */\n        private static final ScanFile POISON_PILL = new ScanFile(\"\", \"\");\n\n        final String stateJson;\n        final String fileJson;\n\n        ScanFile(Row scanStateRow, Row scanFileRow) {\n            this.stateJson = RowSerDe.serializeRowToJson(scanStateRow);\n            this.fileJson = RowSerDe.serializeRowToJson(scanFileRow);\n        }\n\n        ScanFile(String stateJson, String fileJson) {\n            this.stateJson = stateJson;\n            this.fileJson = fileJson;\n        }\n\n        /**\n         * Get the deserialized scan state as {@link Row} object\n         */\n        Row getScanRow(Engine engine) {\n            return RowSerDe.deserializeRowFromJson(stateJson);\n        }\n\n        /**\n         * Get the deserialized scan file as {@link Row} object\n         */\n        Row getScanFileRow(Engine engine) {\n            return RowSerDe.deserializeRowFromJson(fileJson);\n        }\n    }\n\n    private class Reader {\n        private final int limit;\n        private final AtomicBoolean stopSignal = new AtomicBoolean(false);\n        private final CountDownLatch countDownLatch = new CountDownLatch(numThreads);\n        private final ExecutorService executorService =\n            Executors.newFixedThreadPool(numThreads + 1);\n        private final BlockingQueue<ScanFile> workQueue = new ArrayBlockingQueue<>(20);\n\n        private int readRecordCount; // Number of rows read so far, synchronized with `this` object\n        private AtomicReference<Exception> error = new AtomicReference<>();\n\n        Reader(int limit) {\n            this.limit = limit;\n        }\n\n        /**\n         * Read the data from the given {@code snapshot}.\n         *\n         * @param readSchema Subset of columns to read from the snapshot.\n         * @param scan Scan object to read data from.\n         * @return Number of rows read\n         */\n        int readData(StructType readSchema, Scan scan) {\n            printSchema(readSchema);\n            try {\n                executorService.submit(workGenerator(scan));\n                for (int i = 0; i < numThreads; i++) {\n                    executorService.submit(workConsumer(i));\n                }\n\n                countDownLatch.await();\n            } catch (InterruptedException ie) {\n                System.out.println(\"Interrupted exiting now..\");\n                throw new RuntimeException(ie);\n            } finally {\n                stopSignal.set(true);\n                executorService.shutdownNow();\n                if (error.get() != null) {\n                    throw new RuntimeException(error.get());\n                }\n            }\n\n            return readRecordCount;\n        }\n\n        private Runnable workGenerator(Scan scan) {\n            return (() -> {\n                Row scanStateRow = scan.getScanState(engine);\n                try(CloseableIterator<FilteredColumnarBatch> scanFileIter =\n                    scan.getScanFiles(engine)) {\n\n                    while (scanFileIter.hasNext() && !stopSignal.get()) {\n                        try (CloseableIterator<Row> scanFileRows = scanFileIter.next().getRows()) {\n                            while (scanFileRows.hasNext() && !stopSignal.get()) {\n                                workQueue.put(new ScanFile(scanStateRow, scanFileRows.next()));\n                            }\n                        }\n                    }\n\n                    for (int i = 0; i < numThreads; i++) {\n                        // poison pill for each worker threads to stop the work.\n                        workQueue.put(ScanFile.POISON_PILL);\n                    }\n                } catch (InterruptedException ie) {\n                    System.out.print(\"Work generator is interrupted\");\n                } catch (Exception e) {\n                    error.compareAndSet(null /* expected */, e);\n                    throw new RuntimeException(e);\n                }\n            });\n        }\n\n        private Runnable workConsumer(int workerId) {\n            return (() -> {\n                try {\n                    ScanFile work = workQueue.take();\n                    if (work == ScanFile.POISON_PILL) {\n                        return; // exit as there are no more work units\n                    }\n                    Row scanState = work.getScanRow(engine);\n                    Row scanFile = work.getScanFileRow(engine);\n                    FileStatus fileStatus =\n                        InternalScanFileUtils.getAddFileStatus(scanFile);\n                    StructType physicalReadSchema =\n                        ScanStateRow.getPhysicalDataReadSchema(scanState);\n\n                    CloseableIterator<ColumnarBatch> physicalDataIter =\n                        engine.getParquetHandler().readParquetFiles(\n                            singletonCloseableIterator(fileStatus),\n                            physicalReadSchema,\n                            Optional.empty()).map(res -> res.getData());\n\n                    try (\n                        CloseableIterator<FilteredColumnarBatch> dataIter =\n                            Scan.transformPhysicalData(\n                                engine,\n                                scanState,\n                                scanFile,\n                                physicalDataIter)) {\n                        while (dataIter.hasNext()) {\n                            if (printDataBatch(dataIter.next())) {\n                                // Have enough records, exit now.\n                                break;\n                            }\n                        }\n                    }\n                } catch (InterruptedException ie) {\n                    System.out.printf(\"Worker %d is interrupted.\" + workerId);\n                } catch (Exception e) {\n                    error.compareAndSet(null /* expected */, e);\n                    throw new RuntimeException(e);\n                } finally {\n                    countDownLatch.countDown();\n                }\n            });\n        }\n\n        /**\n         * Returns true when sufficient amount of rows are received\n         */\n        private boolean printDataBatch(FilteredColumnarBatch data) {\n            synchronized (this) {\n                if (readRecordCount >= limit) {\n                    return true;\n                }\n                readRecordCount += printData(data, limit - readRecordCount);\n                return readRecordCount >= limit;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/SingleThreadedTableReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.apache.commons.cli.CommandLine;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\n\nimport static io.delta.kernel.examples.utils.Utils.parseArgs;\n\nimport io.delta.kernel.internal.InternalScanFileUtils;\nimport io.delta.kernel.internal.data.ScanStateRow;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\n\n/**\n * Single threaded Delta Lake table reader using the Delta Kernel APIs.\n *\n * <p>\n * Usage: java io.delta.kernel.examples.SingleThreadedTableReader [-c <arg>] [-l <arg>] -t <arg>\n * <p>\n * -c,--columns <arg>   Comma separated list of columns to read from the\n * table. Ex. --columns=id,name,address\n * -l,--limit <arg>     Maximum number of rows to read from the table\n * (default 20).\n * -t,--table <arg>     Fully qualified table path\n * </p>\n */\npublic class SingleThreadedTableReader\n    extends BaseTableReader {\n    public SingleThreadedTableReader(String tablePath) {\n        super(tablePath);\n    }\n\n    @Override\n    public int show(int limit, Optional<List<String>> columnsOpt, Optional<Predicate> predicate)\n        throws TableNotFoundException, IOException {\n        Table table = Table.forPath(engine, tablePath);\n        Snapshot snapshot = table.getLatestSnapshot(engine);\n        StructType readSchema = pruneSchema(snapshot.getSchema(), columnsOpt);\n\n        ScanBuilder scanBuilder = snapshot.getScanBuilder().withReadSchema(readSchema);\n\n        if (predicate.isPresent()) {\n            scanBuilder = scanBuilder.withFilter(predicate.get());\n        }\n\n        return readData(readSchema, scanBuilder.build(), limit);\n    }\n\n    public static void main(String[] args)\n        throws Exception {\n        CommandLine commandLine = parseArgs(baseOptions(), args);\n\n        String tablePath = commandLine.getOptionValue(\"table\");\n        int limit = parseInt(commandLine, \"limit\", DEFAULT_LIMIT);\n        Optional<List<String>> columns = parseColumnList(commandLine, \"columns\");\n\n        new SingleThreadedTableReader(tablePath)\n            .show(limit, columns, Optional.empty());\n    }\n\n    /**\n     * Utility method to read and print the data from the given {@code snapshot}.\n     *\n     * @param readSchema  Subset of columns to read from the snapshot.\n     * @param scan        Table scan object\n     * @param maxRowCount Not a hard limit but use this limit to stop reading more columnar batches\n     *                    once the already read columnar batches have at least these many rows.\n     * @return Number of rows read.\n     * @throws Exception\n     */\n    private int readData(StructType readSchema, Scan scan, int maxRowCount) throws IOException {\n        printSchema(readSchema);\n\n        Row scanState = scan.getScanState(engine);\n        CloseableIterator<FilteredColumnarBatch> scanFileIter = scan.getScanFiles(engine);\n\n        int readRecordCount = 0;\n        try {\n            StructType physicalReadSchema =\n                ScanStateRow.getPhysicalDataReadSchema(scanState);\n            while (scanFileIter.hasNext()) {\n                FilteredColumnarBatch scanFilesBatch = scanFileIter.next();\n                try (CloseableIterator<Row> scanFileRows = scanFilesBatch.getRows()) {\n                    while (scanFileRows.hasNext()) {\n                        Row scanFileRow = scanFileRows.next();\n                        FileStatus fileStatus =\n                            InternalScanFileUtils.getAddFileStatus(scanFileRow);\n                        CloseableIterator<ColumnarBatch> physicalDataIter =\n                            engine.getParquetHandler().readParquetFiles(\n                                singletonCloseableIterator(fileStatus),\n                                physicalReadSchema,\n                                Optional.empty()).map(res -> res.getData());\n                        try (\n                            CloseableIterator<FilteredColumnarBatch> transformedData =\n                                Scan.transformPhysicalData(\n                                    engine,\n                                    scanState,\n                                    scanFileRow,\n                                    physicalDataIter)) {\n                            while (transformedData.hasNext()) {\n                                FilteredColumnarBatch filteredData = transformedData.next();\n                                readRecordCount +=\n                                    printData(filteredData, maxRowCount - readRecordCount);\n                                if (readRecordCount >= maxRowCount) {\n                                    return readRecordCount;\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        } finally {\n            scanFileIter.close();\n        }\n\n        return readRecordCount;\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/utils/RowSerDe.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples.utils;\n\nimport java.io.UncheckedIOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\n\nimport io.delta.kernel.internal.util.VectorUtils;\n\nimport io.delta.kernel.defaults.internal.data.DefaultJsonRow;\n\n/**\n * Utility class to serialize and deserialize {@link Row} object.\n */\npublic class RowSerDe {\n    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n\n    private RowSerDe() {\n    }\n\n    /**\n     * Utility method to serialize a {@link Row} as a JSON string\n     */\n    public static String serializeRowToJson(Row row) {\n        Map<String, Object> rowObject = convertRowToJsonObject(row);\n        try {\n            Map<String, Object> rowWithSchema = new HashMap<>();\n            rowWithSchema.put(\"schema\", row.getSchema().toJson());\n            rowWithSchema.put(\"row\", rowObject);\n            return OBJECT_MAPPER.writeValueAsString(rowWithSchema);\n        } catch (JsonProcessingException e) {\n            throw new UncheckedIOException(e);\n        }\n    }\n\n    /**\n     * Utility method to deserialize a {@link Row} object from the JSON form.\n     */\n    public static Row deserializeRowFromJson(String jsonRowWithSchema) {\n        try {\n            JsonNode jsonNode = OBJECT_MAPPER.readTree(jsonRowWithSchema);\n            JsonNode schemaNode = jsonNode.get(\"schema\");\n            StructType schema = DataTypeJsonSerDe.deserializeStructType(schemaNode.asText());\n            return parseRowFromJsonWithSchema((ObjectNode) jsonNode.get(\"row\"), schema);\n        } catch (JsonProcessingException ex) {\n            throw new UncheckedIOException(ex);\n        }\n    }\n\n    private static Map<String, Object> convertRowToJsonObject(Row row) {\n        StructType rowType = row.getSchema();\n        Map<String, Object> rowObject = new HashMap<>();\n        for (int fieldId = 0; fieldId < rowType.length(); fieldId++) {\n            StructField field = rowType.at(fieldId);\n            DataType fieldType = field.getDataType();\n            String name = field.getName();\n\n            if (row.isNullAt(fieldId)) {\n                rowObject.put(name, null);\n                continue;\n            }\n\n            Object value;\n            if (fieldType instanceof BooleanType) {\n                value = row.getBoolean(fieldId);\n            } else if (fieldType instanceof ByteType) {\n                value = row.getByte(fieldId);\n            } else if (fieldType instanceof ShortType) {\n                value = row.getShort(fieldId);\n            } else if (fieldType instanceof IntegerType) {\n                value = row.getInt(fieldId);\n            } else if (fieldType instanceof LongType) {\n                value = row.getLong(fieldId);\n            } else if (fieldType instanceof FloatType) {\n                value = row.getFloat(fieldId);\n            } else if (fieldType instanceof DoubleType) {\n                value = row.getDouble(fieldId);\n            } else if (fieldType instanceof DateType) {\n                value = row.getInt(fieldId);\n            } else if (fieldType instanceof TimestampType) {\n                value = row.getLong(fieldId);\n            } else if (fieldType instanceof StringType) {\n                value = row.getString(fieldId);\n            } else if (fieldType instanceof ArrayType) {\n                value = VectorUtils.toJavaList(row.getArray(fieldId));\n            } else if (fieldType instanceof MapType) {\n                value = VectorUtils.toJavaMap(row.getMap(fieldId));\n            } else if (fieldType instanceof StructType) {\n                Row subRow = row.getStruct(fieldId);\n                value = convertRowToJsonObject(subRow);\n            } else {\n                throw new UnsupportedOperationException(\"NYI\");\n            }\n\n            rowObject.put(name, value);\n        }\n\n        return rowObject;\n    }\n\n    private static Row parseRowFromJsonWithSchema(ObjectNode rowJsonNode, StructType rowType) {\n        return new DefaultJsonRow(rowJsonNode, rowType);\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/examples/utils/Utils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.examples.utils;\n\nimport org.apache.commons.cli.*;\n\nimport io.delta.kernel.examples.SingleThreadedTableReader;\n\npublic class Utils {\n\n    /**\n     * Helper method to parse the command line arguments.\n     */\n    public static CommandLine parseArgs(Options options, String[] args) {\n        CommandLineParser cliParser = new DefaultParser();\n\n        try {\n            return cliParser.parse(options, args);\n        } catch (ParseException parseException) {\n            new HelpFormatter().printHelp(\n                    \"java \" + SingleThreadedTableReader.class.getCanonicalName(),\n                    options,\n                    true\n            );\n        }\n        System.exit(-1);\n        return null;\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/integration/ReadIntegrationTestSuite.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.integration;\n\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Optional;\n\nimport static java.util.Arrays.asList;\n\nimport io.delta.kernel.examples.SingleThreadedTableReader;\nimport io.delta.kernel.expressions.And;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Predicate;\n\n/**\n * Test suite that runs various integration tests for sanity testing the staged/released artifacts.\n * It only verifies the number of rows in the results and not the specific values of rows.\n * For full scale results verification we rely on unit tests which are run as part of the CI jobs.\n */\npublic class ReadIntegrationTestSuite {\n    private final String goldenTableDir;\n\n    public static void main(String[] args) throws Exception {\n        new ReadIntegrationTestSuite(args[0])\n            .runTests();\n    }\n\n    public ReadIntegrationTestSuite(String goldenTableDir) {\n        this.goldenTableDir = goldenTableDir;\n    }\n\n    public void runTests() throws Exception {\n        // Definitions of golden tables is present in\n        // <root>/connectors/golden-tables/src/test/scala/io/delta/golden/GoldenTables.scala\n\n        // Basic reads: Simple table\n        runAndVerifyRowCount(\n            \"basic_read_simple_table\",\n            \"data-reader-primitives\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            11 /* expected row count */);\n\n        // Basic reads: Partitioned table\n        runAndVerifyRowCount(\n            \"basic_read_partitioned_table\",\n            \"data-reader-array-primitives\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            10 /* expected row count */);\n\n        // Basic reads: Table with DVs\n        runAndVerifyRowCount(\n            \"basic_read_table_with_deletionvectors\",\n            \"dv-partitioned-with-checkpoint\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            35 /* expected row count */);\n\n        // Basic reads: select subset of columns\n        runAndVerifyRowCount(\n            \"basic_read_subset_of_columns\",\n            \"dv-partitioned-with-checkpoint\",\n            Optional.of(asList(\"part\", \"col2\")), /* read schema */\n            Optional.empty(), /* predicate */\n            35 /* expected row count */);\n\n        // Basic reads: Table with DVs and column mapping name\n        runAndVerifyRowCount(\n            \"basic_read_table_with_columnmapping_deletionvectors\",\n            \"dv-with-columnmapping\",\n            Optional.of(asList(\"col1\", \"col2\")), /* read schema */\n            Optional.empty(), /* predicate */\n            35 /* expected row count */);\n\n        // Basic read: table with column mapping mode id\n        runAndVerifyRowCount(\n            \"basic_read_table_columnmapping_id\",\n            \"table-with-columnmapping-mode-id\",\n            Optional.of(\n                asList(\"ByteType\", \"decimal\", \"nested_struct\", \"array_of_prims\", \"map_of_prims\")),\n            Optional.empty(), /* predicate */\n            6 /* expected row count */);\n\n        // Basic read: table with JSON V2 checkpoint\n        runAndVerifyRowCount(\n            \"basic_read_table_v2_checkpoint_json\",\n            \"v2-checkpoint-json\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            10 /* expected row count */);\n\n        // Basic read: table with Parquet V2 checkpoint\n        runAndVerifyRowCount(\n            \"basic_read_table_v2_checkpoint_parquet\",\n            \"v2-checkpoint-parquet\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            10 /* expected row count */);\n\n        // Partition pruning: simple expression\n        runAndVerifyRowCount(\n            \"partition_pruning_simple_filter\",\n            \"basic-decimal-table\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.of(new Predicate(\n                \"=\",\n                asList(\n                    new Column(\"part\"),\n                    Literal.ofDecimal(new BigDecimal(\"2342222.23454\"), 12, 5)))),\n            1 /* expected row count */);\n\n        // Partition pruning: simple expression where nothing is pruned\n        runAndVerifyRowCount(\n            \"partition_pruning_simple_filter_no_pruning\",\n            \"basic-decimal-table\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.of(\n                new Predicate(\n                    \"NOT\",\n                    asList(\n                        new Predicate(\n                            \"=\",\n                            asList(new Column(\"part\"), Literal.ofDecimal(new BigDecimal(0), 12, 5)))\n                    ))),\n            4 /* expected row count */);\n\n        // Partition pruning + data skipping: filter on data and metadata columns where\n        // data filter doesn't prune anything\n        runAndVerifyRowCount(\n            \"partition_pruning_filter_on_data_and_metadata_columns_1\",\n            \"dv-partitioned-with-checkpoint\",\n            Optional.of(asList(\"part\", \"col2\")), /* read schema */\n            Optional.of(\n                new And(\n                    new Predicate(\">=\", asList(new Column(\"part\"), Literal.ofInt(7))),\n                    new Predicate(\">=\", asList(new Column(\"col1\"), Literal.ofInt(0))))),\n            12 /* expected row count */);\n\n        // Partition pruning + data skipping: filter on data and metadata columns where\n        // data filter also prunes few files based on the stats based skipping\n        runAndVerifyRowCount(\n            \"partition_pruning_filter_on_data_and_metadata_columns_2\",\n            \"dv-partitioned-with-checkpoint\",\n            Optional.of(asList(\"part\", \"col2\")), /* read schema */\n            Optional.of(\n                new And(\n                    new Predicate(\">=\", asList(new Column(\"part\"), Literal.ofInt(7))),\n                    new Predicate(\"=\", asList(new Column(\"col1\"), Literal.ofInt(28))))),\n            5 /* expected row count */);\n\n        // Data skipping: filter on a table with checkpoint\n        runAndVerifyRowCount(\n            \"data_skipping_table_with_checkpoint\",\n            \"data-skipping-basic-stats-all-types-checkpoint\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.of(\n                new Predicate(\n                    \">\",\n                    asList(new Column(\"as_int\"), Literal.ofInt(0))\n                )),\n            0 /* expected row count */);\n\n        // Partition pruning: table with column mapping mode name\n        runAndVerifyRowCount(\n            \"partition_pruning_columnmapping_name\",\n            \"dv-with-columnmapping\",\n            Optional.empty(),\n            Optional.of(\n                new Predicate(\n                    \"=\",\n                    asList(new Column(\"part\"), Literal.ofInt(0))\n                )),\n            2 /* expected row count */);\n\n        // Data skipping: table with column mapping mode id\n        runAndVerifyRowCount(\n            \"data_skipping_columnmapping_id\",\n            \"data-skipping-basic-stats-all-types-columnmapping-id\",\n            Optional.empty(),\n            Optional.of(\n                new Predicate(\n                    \"=\",\n                    asList(new Column(\"as_int\"), Literal.ofInt(1))\n                )),\n            0 /* expected row count */);\n\n        // Type widening: table with various type changes.\n        runAndVerifyRowCount(\n            \"type_widening\",\n            \"type-widening\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            2 /* expected row count */);\n\n        // Type widening: table with type changes inside nested struct/array/map.\n        runAndVerifyRowCount(\n            \"type_widening_nested\",\n            \"type-widening-nested\",\n            Optional.empty(), /* read schema - read all columns */\n            Optional.empty(), /* predicate */\n            2 /* expected row count */);\n    }\n\n    private void runAndVerifyRowCount(\n        String testName,\n        String goldenTable,\n        Optional<List<String>> readColumns,\n        Optional<Predicate> predicate,\n        int expectedRowCount) throws Exception {\n        System.out.println(\"\\n========== TEST START: \" + testName + \" ==============\");\n        try {\n            String path = goldenTableDir + \"/\" + goldenTable;\n            SingleThreadedTableReader reader = new SingleThreadedTableReader(path);\n            // Select a large number of rows (1M), so that everything in the table is read.\n            int actRowCount = reader.show(1_000_000, readColumns, predicate);\n            if (actRowCount != expectedRowCount) {\n                throw new RuntimeException(String.format(\n                    \"Test (%s) failed: expected row count = %s, actual row count = %s\",\n                    testName, expectedRowCount, actRowCount));\n            }\n        } finally {\n            System.out.println(\"========== TEST END: \" + testName + \" ==============\\n\");\n        }\n    }\n}\n"
  },
  {
    "path": "kernel/examples/kernel-examples/src/main/java/io/delta/kernel/integration/WriteIntegrationTestSuite.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.integration;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport io.delta.kernel.examples.*;\n\n/**\n * Test suite that runs various integration tests for sanity testing the staged/released artifacts.\n * It only verifies the number of rows in the results and not the specific values of rows. For full\n * scale results verification we rely on unit tests which are run as part of the CI jobs.\n */\npublic class WriteIntegrationTestSuite {\n\n    public static void main(String[] args) throws Exception {\n        new WriteIntegrationTestSuite().runTests();\n    }\n\n\n    public void runTests() throws Exception {\n        verifyRowCount(\n                \"Create un-partitioned table\",\n                0 /* expected row count */,\n                tblLocation -> new CreateTable().createUnpartitionedTable(tblLocation)\n        );\n\n        verifyRowCount(\n                \"Create partitioned table\",\n                0 /* expected row count */,\n                tblLocation -> new CreateTable().createPartitionedTable(tblLocation)\n        );\n\n        verifyRowCount(\n                \"Create un-partitioned table and insert data\",\n                15 /* expected row count */,\n                tblLocation -> new CreateTableAndInsertData().createTableWithSampleData(tblLocation)\n        );\n\n        verifyRowCount(\n                \"Create partitioned table and insert data\",\n                45 /* expected row count */,\n                tblLocation ->\n                        new CreateTableAndInsertData()\n                                .createPartitionedTableWithSampleData(tblLocation)\n        );\n\n        verifyRowCount(\n                \"insert data into an existing table\",\n                30 /* expected row count */,\n                tblLocation -> {\n                    CreateTableAndInsertData createTableAndInsertData =\n                            new CreateTableAndInsertData();\n                    createTableAndInsertData.createTableWithSampleData(tblLocation);\n                    createTableAndInsertData.insertDataIntoUnpartitionedTable(tblLocation);\n                });\n\n        verifyRowCount(\n                \"idempotent inserts into a table\",\n                30 /* expected row count */,\n                tblLocation -> {\n                    CreateTableAndInsertData createTableAndInsertData =\n                            new CreateTableAndInsertData();\n                    createTableAndInsertData.createTableWithSampleData(tblLocation);\n                    createTableAndInsertData.idempotentInserts(tblLocation);\n                });\n\n\n        verifyRowCount(\n                \"inserts with an optional checkpoint\",\n                195 /* expected row count */,\n                tblLocation -> {\n                    CreateTableAndInsertData createTableAndInsertData =\n                            new CreateTableAndInsertData();\n                    createTableAndInsertData.createTableWithSampleData(tblLocation);\n                    createTableAndInsertData.insertWithOptionalCheckpoint(tblLocation);\n                });\n    }\n\n    private void verifyRowCount(String testName, int expectedRowCount, CheckedFunction<String> test)\n            throws Exception {\n        System.out.println(\"\\n========== TEST START: \" + testName + \" ==============\");\n        try {\n            String tblLocation = tmpLocation();\n\n            test.apply(tblLocation);\n\n            SingleThreadedTableReader reader = new SingleThreadedTableReader(tblLocation);\n            // Select a large number of rows (1M), so that everything in the table is read.\n            int actRowCount = reader.show(1_000_000, Optional.empty(), Optional.empty());\n            if (actRowCount != expectedRowCount) {\n                throw new RuntimeException(String.format(\n                        \"Test (%s) failed: expected row count = %s, actual row count = %s\",\n                        testName, expectedRowCount, actRowCount));\n            }\n        } finally {\n            System.out.println(\"========== TEST END: \" + testName + \" ==============\\n\");\n        }\n    }\n\n    private String tmpLocation() throws IOException {\n        return Files.createTempDirectory(\"delta\" + UUID.randomUUID()).toString();\n    }\n\n    interface CheckedFunction<T> {\n        void apply(T tblLocation) throws Exception;\n    }\n}\n"
  },
  {
    "path": "kernel/examples/run-kernel-examples.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n'''\nTo run examples by building the artifacts from code and using them:\n\n```\n<delta-repo-root>/kernel/examples/run-kernel-examples.py --use-local\n```\n\n\nTo run examples using artifacts from a Maven repository:\n```\n<delta-repo-root>/kernel/examples/run-kernel-examples.py --version <version> --maven-repo <staged_repo_url>\n```\n\n'''\n\nimport os\nimport subprocess\nfrom os import path\nimport shutil\nimport argparse\n\ndef run_single_threaded_examples(version, maven_repo, examples_root_dir, golden_tables_dir):\n    main_class = \"io.delta.kernel.examples.SingleThreadedTableReader\"\n    test_cases = [\n        f\"--table={golden_tables_dir}/data-reader-primitives --columns=as_int,as_long --limit=5\",\n        f\"--table={golden_tables_dir}/data-reader-primitives --columns=as_int,as_long,as_double,as_string --limit=20\",\n        f\"--table={golden_tables_dir}/data-reader-partition-values --columns=as_string,as_byte,as_list_of_records,as_nested_struct --limit=20\"\n    ]\n    project_dir = path.join(examples_root_dir, \"kernel-examples\")\n\n    run_example(version, maven_repo, project_dir, main_class, test_cases)\n\n\ndef run_multi_threaded_examples(version, maven_repo, examples_root_dir, golden_tables_dir):\n    main_class = \"io.delta.kernel.examples.MultiThreadedTableReader\"\n    test_cases = [\n        f\"--table={golden_tables_dir}/data-reader-primitives --columns=as_int,as_long --limit=5 --parallelism=5\",\n        f\"--table={golden_tables_dir}/data-reader-primitives --columns=as_int,as_long,as_double,as_string --limit=20 --parallelism=20\",\n        f\"--table={golden_tables_dir}/data-reader-partition-values --columns=as_string,as_byte,as_list_of_records,as_nested_struct --limit=20 --parallelism=2\"\n    ]\n    project_dir = path.join(examples_root_dir, \"kernel-examples\")\n\n    run_example(version, maven_repo, project_dir, main_class, test_cases)\n\n\ndef run_integration_tests(version, maven_repo, examples_root_dir, golden_tables_dir):\n\n    main_classes = [\"io.delta.kernel.integration.ReadIntegrationTestSuite\", \"io.delta.kernel.integration.WriteIntegrationTestSuite\"]\n    for main_class in main_classes:\n        project_dir = path.join(examples_root_dir, \"kernel-examples\")\n        with WorkingDirectory(project_dir):\n            cmd = [\"mvn\", \"package\", \"exec:java\", f\"-Dexec.mainClass={main_class}\",\n                  f\"-Dstaging.repo.url={maven_repo}\",\n                  f\"-Ddelta-kernel.version={version}\",\n                  f\"-Dexec.args={golden_tables_dir}\"]\n            run_cmd(cmd, stream_output=True)\n\n\ndef run_example(version, maven_repo, project_dir, main_class, test_cases):\n    with WorkingDirectory(project_dir):\n        for test in test_cases:\n            cmd = [\"mvn\", \"package\", \"exec:java\", f\"-Dexec.mainClass={main_class}\",\n                   f\"-Dstaging.repo.url={maven_repo}\",\n                   f\"-Ddelta-kernel.version={version}\",\n                   f\"-Dexec.args={test}\"]\n            run_cmd(cmd, stream_output=True)\n\n\ndef clear_artifact_cache():\n    print(\"Clearing Delta Kernel artifacts from ivy2 and mvn cache\")\n    ivy_caches_to_clear = [filepath for filepath in os.listdir(os.path.expanduser(\"~\")) if filepath.startswith(\".ivy\")]\n    print(f\"Clearing Ivy caches in: {ivy_caches_to_clear}\")\n    for filepath in ivy_caches_to_clear:\n        for subpath in [\"io.delta\", \"io.delta.kernel\"]:\n            delete_if_exists(os.path.expanduser(f\"~/{filepath}/cache/{subpath}\"))\n            delete_if_exists(os.path.expanduser(f\"~/{filepath}/local/{subpath}\"))\n    delete_if_exists(os.path.expanduser(\"~/.m2/repository/io/delta/\"))\n\n\ndef delete_if_exists(path):\n    # if path exists, delete it.\n    if os.path.exists(path):\n        shutil.rmtree(path)\n        print(\"Deleted %s \" % path)\n\n\n# pylint: disable=too-few-public-methods\nclass WorkingDirectory(object):\n    def __init__(self, working_directory):\n        self.working_directory = working_directory\n        self.old_workdir = os.getcwd()\n\n    def __enter__(self):\n        os.chdir(self.working_directory)\n\n    def __exit__(self, tpe, value, traceback):\n        os.chdir(self.old_workdir)\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, **kwargs):\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % (exit_code))\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return (exit_code, stdout, stderr)\n\n\nif __name__ == \"__main__\":\n    \"\"\"\n        Script to run Delta Kernel examples which are located in the kernel/examples directory.\n        call this by running `python run-kernel-examples.py`\n        additionally the version can be provided as a command line argument.\n    \"\"\"\n\n    # get the version of the package\n    examples_root_dir = path.abspath(path.dirname(__file__))\n    project_root_dir = path.join(examples_root_dir, \"../../\")\n    with open(path.join(project_root_dir, \"version.sbt\")) as fd:\n        default_version = fd.readline().split('\"')[1]\n\n        parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--version\",\n        required=False,\n        default=default_version,\n        help=\"Delta Kernel version to use to run the examples\")\n\n    parser.add_argument(\n        \"--maven-repo\",\n        required=False,\n        default=None,\n        help=\"Additional Maven repo to resolve staged new release artifacts\")\n\n    parser.add_argument(\n        \"--use-local\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Generate JARs from local source code and use to run tests\")\n\n    args = parser.parse_args()\n\n    if args.use_local and (args.version != default_version):\n        raise Exception(\"Cannot specify --use-local with a --version different than in version.sbt\")\n\n    clear_artifact_cache()\n\n    if args.use_local:\n        with WorkingDirectory(project_root_dir):\n            run_cmd([\"build/sbt\", \"kernelGroup/publishM2\", \"storage/publishM2\"], stream_output=True)\n\n    golden_file_dir = path.join(\n        examples_root_dir,\n        \"../../connectors/golden-tables/src/main/resources/golden/\")\n\n    run_single_threaded_examples(args.version, args.maven_repo, examples_root_dir, golden_file_dir)\n    run_multi_threaded_examples(args.version, args.maven_repo, examples_root_dir, golden_file_dir)\n    run_integration_tests(args.version, args.maven_repo, examples_root_dir, golden_file_dir)\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/CommitActions.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.utils.CloseableIterator;\n\n/**\n * Represents all actions from a single commit version in a table.\n *\n * <p><b>Resource Management:</b>\n *\n * <ul>\n *   <li>Each iterator returned by {@link #getActions()} must be closed after use to release\n *       underlying resources.\n *   <li>The {@code CommitActions} object should be closed correctly (preferably using\n *       try-with-resources) to ensure any resources allocated during construction are properly\n *       released.\n * </ul>\n *\n * <p>Example usage:\n *\n * <pre>{@code\n * try (CommitActions commitActions = ...) {\n *   long version = commitActions.getVersion();\n *   long timestamp = commitActions.getTimestamp();\n *\n *   try (CloseableIterator<ColumnarBatch> actions = commitActions.getActions()) {\n *     while (actions.hasNext()) {\n *       ColumnarBatch batch = actions.next();\n *       // process batch\n *     }\n *   }\n * }\n * }</pre>\n *\n * @since 4.1.0\n */\n@Evolving\npublic interface CommitActions extends AutoCloseable {\n\n  /**\n   * Returns the commit version number.\n   *\n   * @return the version number of this commit\n   */\n  long getVersion();\n\n  /**\n   * Returns the commit timestamp in milliseconds since Unix epoch.\n   *\n   * @return the timestamp of this commit\n   */\n  long getTimestamp();\n\n  /**\n   * Returns an iterator over the action batches for this commit.\n   *\n   * <p>Each {@link ColumnarBatch} contains actions from this commit only.\n   *\n   * <p>Note: All rows within all batches have the same version (returned by {@link #getVersion()}).\n   *\n   * <p>This method can be called multiple times, and each call returns a new iterator over the same\n   * set of batches. This supports use cases like two-pass processing (e.g., validation pass\n   * followed by processing pass).\n   *\n   * <p><b>Callers are responsible for closing each iterator returned by this method.</b> Each\n   * iterator must be closed after use to release underlying resources.\n   *\n   * @return a {@link CloseableIterator} over columnar batches containing this commit's actions\n   */\n  CloseableIterator<ColumnarBatch> getActions();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/CommitRange.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.DeltaLogActionUtils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.util.Optional;\nimport java.util.Set;\n\n/**\n * Represents a range of contiguous commits in a Delta Lake table with a defined start and end\n * version. Supports operation on the range of commits, such as reading the delta actions committed\n * in each commit in the version range.\n *\n * <p>Commit ranges are created using a {@link CommitRangeBuilder}, which supports specifying the\n * start and end boundaries of the range. The boundaries can be defined using either versions or\n * timestamps.\n *\n * @since 3.4.0\n */\n@Evolving\npublic interface CommitRange {\n\n  /**\n   * Returns the starting version number (inclusive) of this commit range.\n   *\n   * @return the starting version number of the commit range\n   */\n  long getStartVersion();\n\n  /**\n   * Returns the ending version number (inclusive) of this commit range.\n   *\n   * @return the ending version number of the commit range\n   */\n  long getEndVersion();\n\n  /**\n   * Returns the original query boundary used to define the start boundary of this commit range.\n   *\n   * <p>The boundary indicates whether the range was defined using a specific version number or a\n   * timestamp.\n   *\n   * @return the start boundary for this commit range\n   */\n  CommitRangeBuilder.CommitBoundary getQueryStartBoundary();\n\n  /**\n   * Returns the original query boundary used to define the end boundary of this commit range, if\n   * available.\n   *\n   * <p>The boundary indicates whether the range was defined using a specific version number or a\n   * timestamp.\n   *\n   * @return an {@link Optional} containing the end boundary, or empty if the range was created with\n   *     default end parameters (latest version)\n   */\n  Optional<CommitRangeBuilder.CommitBoundary> getQueryEndBoundary();\n\n  /**\n   * Returns an iterator of the requested actions for the commits in this commit range.\n   *\n   * <p>For the returned columnar batches:\n   *\n   * <ul>\n   *   <li>Each row within the same batch is guaranteed to have the same commit version\n   *   <li>The batch commit versions are monotonically increasing\n   *   <li>The top-level columns include \"version\", \"timestamp\", and the actions requested in\n   *       actionSet. \"version\" and \"timestamp\" are the first and second columns in the schema,\n   *       respectively. The remaining columns are based on the actions requested and each have the\n   *       schema found in {@code DeltaAction.schema}.\n   * </ul>\n   *\n   * <p>The iterator must be closed after use to release any underlying resources.\n   *\n   * @param engine the {@link Engine} to use for reading the Delta log files\n   * @param startSnapshot the snapshot for startVersion, required to ensure the table is readable by\n   *     Kernel at startVersion\n   * @param actionSet the set of action types to include in the results. Only actions of these types\n   *     will be returned in the iterator\n   * @return a {@link CloseableIterator} over columnar batches containing the requested actions\n   *     within this commit range\n   * @throws IllegalArgumentException if startSnapshot.getVersion() != startVersion\n   * @throws KernelException if the version range contains a version with reader protocol that is\n   *     unsupported by Kernel\n   */\n  CloseableIterator<ColumnarBatch> getActions(\n      Engine engine, Snapshot startSnapshot, Set<DeltaLogActionUtils.DeltaAction> actionSet);\n\n  /**\n   * Returns an iterator of commits in this commit range, where each commit is represented as a\n   * {@link CommitActions} object.\n   *\n   * @param engine the {@link Engine} to use for reading the Delta log files\n   * @param startSnapshot the snapshot for startVersion, required to ensure the table is readable by\n   *     Kernel at startVersion\n   * @param actionSet the set of action types to include in the results. Only actions of these types\n   *     will be returned in each commit's actions iterator\n   * @return a {@link CloseableIterator} over {@link CommitActions}, one per commit version in this\n   *     range\n   * @throws IllegalArgumentException if startSnapshot.getVersion() != startVersion\n   * @throws KernelException if the version range contains a version with reader protocol that is\n   *     unsupported by Kernel\n   */\n  CloseableIterator<CommitActions> getCommitActions(\n      Engine engine, Snapshot startSnapshot, Set<DeltaLogActionUtils.DeltaAction> actionSet);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/CommitRangeBuilder.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * A builder for creating {@link CommitRange} instances that define a contiguous range of commits in\n * a Delta Lake table.\n *\n * <p>The start boundary is required and provided via {@link TableManager#loadCommitRange(String,\n * CommitBoundary)}. If no end specification is provided, the range defaults to the latest available\n * version.\n *\n * @since 3.4.0\n */\n@Experimental\npublic interface CommitRangeBuilder {\n\n  /**\n   * Configures the builder to end the commit range at a specific version or timestamp.\n   *\n   * <p>If not specified, the commit range will default to ending at the latest available version.\n   *\n   * @param endBoundary the boundary specification for the end of the commit range, must not be null\n   * @return this builder instance configured with the specified end boundary\n   */\n  CommitRangeBuilder withEndBoundary(CommitBoundary endBoundary);\n\n  /**\n   * Provides parsed log data to optimize the commit range construction.\n   *\n   * <p><strong>Note:</strong> If no end boundary is provided via {@link\n   * #withEndBoundary(CommitBoundary)}, or a timestamp-based end boundary is provided, the provided\n   * log data must include all available ratified commits. If a version-based end boundary is\n   * provided, the log data must include commits up to at least the end version (i.e., the tail of\n   * the log data must have a version greater than or equal to the end version).\n   *\n   * @param logData the list of pre-parsed log data, must not be null\n   * @return this builder instance configured with the specified log data\n   */\n  // TODO: should we change this to take in a ParsedDeltaData instead?\n  CommitRangeBuilder withLogData(List<ParsedLogData> logData);\n\n  /**\n   * Specifies the maximum table version known by the catalog.\n   *\n   * <p>This method is used by catalog implementations for catalog-managed Delta tables to indicate\n   * the latest ratified version of the table. This ensures that any commit range operations respect\n   * the catalog's view of the table state.\n   *\n   * <p>Important: This method is required for catalog-managed tables and must not be used for\n   * file-system managed tables.\n   *\n   * <p>When specified, the following additional constraints are enforced:\n   *\n   * <ul>\n   *   <li>When the provided startBoundary is version-based, the start version must be less than or\n   *       equal to the max catalog version.\n   *   <li>If {@link #withEndBoundary(CommitBoundary)} is used with a version, the requested version\n   *       must be less than or equal to the max catalog version.\n   *   <li>If the provided startBoundary is timestamp-based, or {@link\n   *       #withEndBoundary(CommitBoundary)} is used with a timestamp, the provided latest snapshot\n   *       must have a version equal to the max catalog version.\n   *   <li>If {@link #withLogData(List)} is provided and no end boundary is specified (resolving to\n   *       latest), the log data must end with the max catalog version.\n   * </ul>\n   *\n   * @param version the maximum table version known by the catalog (must be {@code >= 0})\n   * @return a new builder instance with the specified max catalog version\n   * @throws IllegalArgumentException if version is negative\n   */\n  CommitRangeBuilder withMaxCatalogVersion(long version);\n\n  /**\n   * Builds and returns a {@link CommitRange} instance with the configured specifications.\n   *\n   * <p>This method validates the builder configuration and constructs the commit range by resolving\n   * version numbers from timestamps if necessary and determining the actual commit files that fall\n   * within the specified range.\n   *\n   * @param engine the {@link Engine} to use for file system operations and log parsing\n   * @return a new {@link CommitRange} instance configured according to this builder's\n   *     specifications\n   * @throws IllegalArgumentException if the builder configuration is invalid (e.g., start version\n   *     {@code >} end version)\n   */\n  CommitRange build(Engine engine);\n\n  /**\n   * Defines a boundary (start or end) of a commit range in a Delta Lake table.\n   *\n   * <p>A {@code CommitBoundary} can be based on either a specific version number or a timestamp.\n   * When using timestamps, the boundary requires the latest snapshot to help resolve the timestamp\n   * to the appropriate version.\n   *\n   * <p>Use the static factory methods {@link #atVersion(long)} and {@link #atTimestamp(long,\n   * Snapshot)} to create instances.\n   */\n  final class CommitBoundary {\n\n    /**\n     * Creates a commit boundary based on a specific version number.\n     *\n     * @param version the commit version number, must be non-negative\n     * @return a new {@code CommitBoundary} representing the specified version\n     * @throws IllegalArgumentException if {@code version} is negative\n     */\n    public static CommitBoundary atVersion(long version) {\n      checkArgument(version >= 0, \"Version must be >= 0, but got: %d\", version);\n      return new CommitBoundary(true, version, Optional.empty());\n    }\n\n    /**\n     * Creates a commit boundary based on a timestamp.\n     *\n     * <p>The timestamp represents a point in time, and the boundary will resolve to the appropriate\n     * commit version.\n     *\n     * @param timestamp the timestamp in milliseconds since epoch\n     * @param latestSnapshot the latest snapshot of the table, used for timestamp resolution\n     * @return a new {@code CommitBoundary} representing the specified timestamp\n     */\n    public static CommitBoundary atTimestamp(long timestamp, Snapshot latestSnapshot) {\n      checkArgument(\n          latestSnapshot instanceof SnapshotImpl,\n          \"latestSnapshot must be instance of SnapshotImpl\");\n      return new CommitBoundary(false, timestamp, Optional.of(latestSnapshot));\n    }\n\n    private final boolean isVersion;\n    private final long value;\n    private final Optional<Snapshot> latestSnapshot;\n\n    private CommitBoundary(boolean isVersion, long value, Optional<Snapshot> latestSnapshot) {\n      checkArgument(isVersion || latestSnapshot.isPresent());\n      this.isVersion = isVersion;\n      this.value = value;\n      this.latestSnapshot = latestSnapshot;\n    }\n\n    /** @return {@code true} if this is a version-based boundary, {@code false} otherwise */\n    public boolean isVersion() {\n      return isVersion;\n    }\n\n    /** @return {@code true} if this is a timestamp-based boundary, {@code false} otherwise */\n    public boolean isTimestamp() {\n      return !isVersion;\n    }\n\n    /**\n     * Returns the version number for version-based boundaries. Callers should check {@link\n     * CommitBoundary#isVersion()} before access.\n     *\n     * @return the version number\n     * @throws IllegalStateException if this boundary is timestamp-based\n     */\n    public long getVersion() {\n      if (!isVersion) {\n        throw new IllegalStateException(\"This boundary is not version-based\");\n      }\n      return value;\n    }\n\n    /**\n     * Returns the timestamp for timestamp-based boundaries. Callers should check {@link\n     * CommitBoundary#isTimestamp()} before access.\n     *\n     * @return the timestamp in milliseconds since epoch\n     * @throws IllegalStateException if this boundary is version-based\n     */\n    public long getTimestamp() {\n      if (isVersion) {\n        throw new IllegalStateException(\"This boundary is not timestamp-based\");\n      }\n      return value;\n    }\n\n    /**\n     * Returns the latest snapshot used for timestamp resolution in timestamp-based boundaries.\n     * Callers should check {@link CommitBoundary#isTimestamp()} before access.\n     *\n     * @return the latest snapshot\n     * @throws IllegalStateException if this boundary is version-based\n     */\n    public Snapshot getLatestSnapshot() {\n      if (isVersion) {\n        throw new IllegalStateException(\"This boundary is not timestamp-based\");\n      }\n      return latestSnapshot.get();\n    }\n\n    @Override\n    public String toString() {\n      if (isVersion) {\n        return String.format(\"CommitBoundary{version=%d}\", value);\n      } else {\n        return String.format(\n            \"CommitBoundary{timestamp=%d, latestSnapshot=%s}\", value, latestSnapshot.get());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/DataWriteContext.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Contains the context for writing data to Delta table. The context is created for each partition\n * for partitioned table or once per table for un-partitioned table. It is created using {@link\n * Transaction#getWriteContext(Engine, Row, Map)} (String, Map, List)}.\n *\n * @since 3.2.0\n */\n@Evolving\npublic interface DataWriteContext {\n  /**\n   * Returns the target directory where the data should be written.\n   *\n   * @return fully qualified path of the target directory\n   */\n  String getTargetDirectory();\n\n  /**\n   * Returns the list of {@link Column} that the connector can optionally collect statistics. Each\n   * {@link Column} is a reference to a top-level or nested column in the table.\n   *\n   * <p>Statistics collections can be skipped or collected for a partial list of the returned {@link\n   * Column}s. When stats are present in the written Delta log, they can be used to optimize query\n   * performance.\n   *\n   * @return schema of the statistics\n   */\n  List<Column> getStatisticsColumns();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/Operation.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\n/**\n * An operation that can be performed on a Delta table.\n *\n * <p>An operation is tracked as the first line in commit info action inside the Delta Log It also\n * shows up when {@code DESCRIBE HISTORY} on the table is executed.\n */\npublic enum Operation {\n\n  /** Recorded when the table is created. */\n  CREATE_TABLE(\"CREATE TABLE\"),\n\n  /** Recorded during batch inserts. */\n  WRITE(\"WRITE\"),\n\n  /** Recorded during streaming inserts. */\n  STREAMING_UPDATE(\"STREAMING UPDATE\"),\n\n  /** Recorded during REPLACE operation (may also be considered an overwrite) */\n  REPLACE_TABLE(\"REPLACE TABLE\"),\n\n  /** For any operation that doesn't fit the above categories. */\n  MANUAL_UPDATE(\"Manual Update\");\n\n  /** Actual value that will be recorded in the transaction log */\n  private final String description;\n\n  Operation(String description) {\n    this.description = description;\n  }\n\n  /** Returns the string that will be recorded in the transaction log. */\n  public String getDescription() {\n    return description;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/PaginatedScan.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.engine.Engine;\n\n/**\n * Extension of {@link Scan} that supports pagination.\n *\n * <p>This interface allows consumers to retrieve scan results in discrete, ordered pages rather\n * than all at once. This is particularly useful for large datasets where materializing the full\n * result set would be expensive in terms of memory or compute resources.\n *\n * <p>Pagination is achieved via a combination of {@code pageSize} and {@code pageToken}. The {@code\n * pageSize} controls how many Scan files are returned in each page, while the {@code pageToken}\n * encodes the location of next batch to read and is used to resume the scan from exactly where the\n * last page ended. For the first page, the {@code pageToken} should be {@code Optional.empty()}.\n *\n * <p>Consumers typically use {@link PaginatedScan} in a loop: they call {@code getScanFiles()} to\n * retrieve an iterator over the current page's scan files. After consuming the iterator, users\n * should call {@link PaginatedScanFilesIterator#getCurrentPageToken} to retrieve a token to pass\n * into the next page request. This allows users to scan the dataset incrementally, resuming from\n * where they left off.\n */\npublic interface PaginatedScan extends Scan {\n\n  /**\n   * Get a paginated iterator of Scan files for the current page.\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @return iterator of {@link FilteredColumnarBatch}s for the current page.\n   */\n  @Override\n  PaginatedScanFilesIterator getScanFiles(Engine engine);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/PaginatedScanFilesIterator.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.util.Optional;\n\n/**\n * An iterator over {@link FilteredColumnarBatch}, each representing a batch of Scan Files in a\n * paginated scan. This iterator also exposes the page token that can be used to resume the scan\n * from the exact position current page ends in a subsequent request.\n *\n * <p>This interface extends {@link CloseableIterator} and should be closed when the iteration is\n * complete.\n */\npublic interface PaginatedScanFilesIterator extends CloseableIterator<FilteredColumnarBatch> {\n  /**\n   * Returns an optional page token representing the starting position of next page. This token is\n   * used to resume the scan from the exact position current page ends in a subsequent request. Page\n   * token also contains metadata for validation purpose, such as detecting changes in query\n   * parameters or the underlying log files.\n   *\n   * <p>The page token represents the position of current iterator at the time it's called. If the\n   * iterator is only partially consumed, the returned token will always point to the beginning of\n   * the next unconsumed {@link FilteredColumnarBatch}. This method will return Option.empty() if\n   * all data in the Scan is consumed (no more non-empty pages remain).\n   */\n  Optional<Row> getCurrentPageToken();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/Scan.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.InternalScanFileUtils;\nimport io.delta.kernel.internal.ScanImpl;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.data.ScanStateRow;\nimport io.delta.kernel.internal.data.SelectionColumnVector;\nimport io.delta.kernel.internal.deletionvectors.DeletionVectorUtils;\nimport io.delta.kernel.internal.deletionvectors.RoaringBitmapArray;\nimport io.delta.kernel.internal.rowtracking.MaterializedRowTrackingColumn;\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode;\nimport io.delta.kernel.internal.util.PartitionUtils;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.MetadataColumnSpec;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Represents a scan of a Delta table.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface Scan {\n  /**\n   * Get an iterator of data files to scan.\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @return iterator of {@link FilteredColumnarBatch}s where each selected row in the batch\n   *     corresponds to one scan file. Schema of each row is defined as follows:\n   *     <p>\n   *     <ol>\n   *       <li>\n   *           <ul>\n   *             <li>name: {@code add}, type: {@code struct}\n   *             <li>Description: Represents `AddFile` DeltaLog action\n   *             <li>\n   *                 <ul>\n   *                   <li>name: {@code path}, type: {@code string}, description: location of the\n   *                       file. The path is a URI as specified by RFC 2396 URI Generic Syntax,\n   *                       which needs to be decoded to get the data file path.\n   *                   <li>name: {@code partitionValues}, type: {@code map(string, string)},\n   *                       description: A map from partition column to value for this logical file.\n   *                   <li>name: {@code size}, type: {@code long}, description: size of the file.\n   *                   <li>name: {@code modificationTime}, type: {@code log}, description: the time\n   *                       this logical file was created, as milliseconds since the epoch.\n   *                   <li>name: {@code dataChange}, type: {@code boolean}, description: When false\n   *                       the logical file must already be present in the table or the records in\n   *                       the added file must be contained in one or more remove actions in the\n   *                       same version\n   *                   <li>name: {@code deletionVector}, type: {@code string}, description: Either\n   *                       null (or absent in JSON) when no DV is associated with this data file, or\n   *                       a struct (described below) that contains necessary information about the\n   *                       DV that is part of this logical file. For description of each member\n   *                       variable in `deletionVector` @see <a\n   *                       href=https://github.com/delta-io/delta/blob/master/PROTOCOL.md#Deletion-Vectors>\n   *                       Protocol</a>\n   *                       <ul>\n   *                         <li>name: {@code storageType}, type: {@code string}\n   *                         <li>name: {@code pathOrInlineDv}, type: {@code string}, description:\n   *                             The path is a URI as specified by RFC 2396 URI Generic Syntax,\n   *                             which needs to be decoded to get the data file path.\n   *                         <li>name: {@code offset}, type: {@code log}\n   *                         <li>name: {@code sizeInBytes}, type: {@code log}\n   *                         <li>name: {@code cardinality}, type: {@code log}\n   *                       </ul>\n   *                   <li>name: {@code tags}, type: {@code map(string, string)}, description: Map\n   *                       containing metadata about the scan file.\n   *                 </ul>\n   *           </ul>\n   *       <li>\n   *           <ul>\n   *             <li>name: {@code tableRoot}, type: {@code string}\n   *             <li>Description: Absolute path of the table location. The path is a URI as\n   *                 specified by RFC 2396 URI Generic Syntax, which needs to be decoded to get the\n   *                 data file path. NOTE: this is temporary. Will be removed in the future.\n   *           </ul>\n   *     </ol>\n   *\n   * @see <a href=https://github.com/delta-io/delta/issues/2089></a>\n   */\n  CloseableIterator<FilteredColumnarBatch> getScanFiles(Engine engine);\n\n  /**\n   * Get the remaining filter that is not guaranteed to be satisfied for the data Delta Kernel\n   * returns. This filter is used by Delta Kernel to do data skipping when possible.\n   *\n   * @return the remaining filter as a {@link Predicate}.\n   */\n  Optional<Predicate> getRemainingFilter();\n\n  /**\n   * Get the scan state associated with the current scan. This state is common across all files in\n   * the scan to be read.\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @return Scan state in {@link Row} format.\n   */\n  Row getScanState(Engine engine);\n\n  /**\n   * Transform the physical data read from the table data file to the logical data that are expected\n   * out of the Delta table.\n   *\n   * <p>This iterator effectively reverses the logical-to-physical schema transformation performed\n   * in {@link ScanImpl#getScanState(Engine)} by transforming physical data batches into the logical\n   * data requested by the connector.\n   *\n   * @param engine Connector provided {@link Engine} implementation.\n   * @param scanState Scan state returned by {@link Scan#getScanState(Engine)}\n   * @param scanFile Scan file from where the physical data {@code physicalDataIter} is read from.\n   * @param physicalDataIter Iterator of {@link ColumnarBatch}s containing the physical data read\n   *     from the {@code scanFile}.\n   * @return Data read from the input scan files as an iterator of {@link FilteredColumnarBatch}s.\n   *     Each {@link FilteredColumnarBatch} instance contains the data read and an optional\n   *     selection vector that indicates data rows as valid or invalid. It is the responsibility of\n   *     the caller to close this iterator.\n   * @throws IOException when error occurs while reading the data.\n   */\n  static CloseableIterator<FilteredColumnarBatch> transformPhysicalData(\n      Engine engine, Row scanState, Row scanFile, CloseableIterator<ColumnarBatch> physicalDataIter)\n      throws IOException {\n    return new CloseableIterator<FilteredColumnarBatch>() {\n      boolean inited = false;\n\n      // initialized as part of init()\n      StructType logicalSchema = null;\n      Map<String, String> configuration = null;\n      ColumnMappingMode columnMappingMode = null;\n      String tablePath = null;\n\n      RoaringBitmapArray currBitmap = null;\n      DeletionVectorDescriptor currDV = null;\n\n      private void initIfRequired() {\n        if (inited) {\n          return;\n        }\n        logicalSchema = ScanStateRow.getLogicalSchema(scanState);\n        configuration = ScanStateRow.getConfiguration(scanState);\n        columnMappingMode = ScanStateRow.getColumnMappingMode(scanState);\n        tablePath = ScanStateRow.getTableRoot(scanState).toString();\n        inited = true;\n      }\n\n      @Override\n      public void close() throws IOException {\n        physicalDataIter.close();\n      }\n\n      @Override\n      public boolean hasNext() {\n        initIfRequired();\n        return physicalDataIter.hasNext();\n      }\n\n      @Override\n      public FilteredColumnarBatch next() {\n        initIfRequired();\n        ColumnarBatch nextDataBatch = physicalDataIter.next();\n\n        // Step 1: If row tracking is enabled, check for physical row tracking columns in the data\n        // batch and transform them to logical row tracking columns as needed\n        if (TableConfig.ROW_TRACKING_ENABLED.fromMetadata(configuration)) {\n          nextDataBatch =\n              MaterializedRowTrackingColumn.transformPhysicalData(\n                  nextDataBatch, scanFile, logicalSchema, configuration, engine);\n        }\n\n        // Step 2: Get the selectionVector if DV is present\n        DeletionVectorDescriptor dv =\n            InternalScanFileUtils.getDeletionVectorDescriptorFromRow(scanFile);\n        Optional<ColumnVector> selectionVector;\n        if (dv == null) {\n          selectionVector = Optional.empty();\n        } else {\n          int rowIndexOrdinal = nextDataBatch.getSchema().indexOf(MetadataColumnSpec.ROW_INDEX);\n          if (rowIndexOrdinal == -1) {\n            throw new IllegalArgumentException(\n                \"Row index column is not present in the data read from the Parquet file.\");\n          }\n          if (!dv.equals(currDV)) {\n            Tuple2<DeletionVectorDescriptor, RoaringBitmapArray> dvInfo =\n                DeletionVectorUtils.loadNewDvAndBitmap(engine, tablePath, dv);\n            this.currDV = dvInfo._1;\n            this.currBitmap = dvInfo._2;\n          }\n          ColumnVector rowIndexVector = nextDataBatch.getColumnVector(rowIndexOrdinal);\n          selectionVector = Optional.of(new SelectionColumnVector(currBitmap, rowIndexVector));\n        }\n\n        // Step 3: If a column was only requested to compute other columns, we remove it\n        for (StructField field : nextDataBatch.getSchema().fields()) {\n          if (field.isInternalColumn()) {\n            int columnOrdinal = nextDataBatch.getSchema().indexOf(field.getName());\n            if (columnOrdinal == -1) {\n              // This should never happen since we only interact with a single schema\n              throw new IllegalArgumentException(\n                  String.format(\n                      \"Column %s was requested internally but is not present in the data batch.\",\n                      field.getName()));\n            }\n            nextDataBatch = nextDataBatch.withDeletedColumnAt(columnOrdinal);\n          }\n        }\n\n        // Step 4: Add partition columns back to the data batch\n        nextDataBatch =\n            PartitionUtils.withPartitionColumns(\n                nextDataBatch,\n                logicalSchema,\n                InternalScanFileUtils.getPartitionValues(scanFile),\n                engine.getExpressionHandler());\n\n        // Step 5: Transform column names back to logical names if column mapping is enabled\n        switch (columnMappingMode) {\n          case NAME: // fall through\n          case ID:\n            nextDataBatch = nextDataBatch.withNewSchema(logicalSchema);\n            break;\n          case NONE:\n            break;\n          default:\n            throw new UnsupportedOperationException(\n                \"Column mapping mode is not yet supported: \" + columnMappingMode);\n        }\n\n        return new FilteredColumnarBatch(nextDataBatch, selectionVector);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/ScanBuilder.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\n\n/**\n * Builder to construct {@link Scan} object.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface ScanBuilder {\n\n  /**\n   * Apply the given filter expression to prune any files that do not possibly contain the data that\n   * satisfies the given filter.\n   *\n   * <p>Kernel makes use of the scan file partition values (for partitioned tables) and file-level\n   * column statistics (min, max, null count etc.) in the Delta metadata for filtering. Sometimes\n   * these metadata is not enough to deterministically say a scan file doesn't contain data that\n   * satisfies the filter.\n   *\n   * <p>E.g. given filter is {@code a = 2}. In file A, column {@code a} has min value as -40 and max\n   * value as 200. In file B, column {@code a} has min value as 78 and max value as 323. File B can\n   * be ruled out as it cannot possibly have rows where `a = 2`, but file A cannot be ruled out as\n   * it may contain rows where {@code a = 2}.\n   *\n   * <p>As filtering is a best effort, the {@link Scan} object may return scan files (through {@link\n   * Scan#getScanFiles(Engine)}) that does not satisfy the filter. It is the responsibility of the\n   * caller to apply the remaining filter returned by {@link Scan#getRemainingFilter()} to the data\n   * read from the scan files (returned by {@link Scan#getScanFiles(Engine)}) to completely filter\n   * out the data that doesn't satisfy the filter.```\n   *\n   * @param predicate a {@link Predicate} to prune the metadata or data.\n   * @return A {@link ScanBuilder} with filter applied.\n   */\n  ScanBuilder withFilter(Predicate predicate);\n\n  /**\n   * Apply the given <i>readSchema</i>. If the builder already has a projection applied, calling\n   * this again replaces the existing projection.\n   *\n   * @param readSchema Subset of columns to read from the Delta table.\n   * @return A {@link ScanBuilder} with projection pruning.\n   */\n  ScanBuilder withReadSchema(StructType readSchema);\n\n  /** @return Build the {@link Scan instance} */\n  Scan build();\n\n  /**\n   * Build a Paginated Scan with a required page size and an optional page token.\n   *\n   * @param pageSize Maximum number of Scan Files to return in this page.\n   * @param pageToken Optional page token representing the current scan position; empty to start\n   *     from the beginning.\n   * @return A {@link PaginatedScan} configured for pagination.\n   */\n  PaginatedScan buildPaginated(long pageSize, Optional<Row> pageToken);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/Snapshot.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.commit.PublishFailedException;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.CheckpointAlreadyExistsException;\nimport io.delta.kernel.statistics.SnapshotStatistics;\nimport io.delta.kernel.transaction.UpdateTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Represents a snapshot of a Delta table at a specific version.\n *\n * <p>A {@code Snapshot} is a consistent view of a Delta table at a specific point in time,\n * identified by a version number. It provides access to the table's metadata, schema, and\n * capabilities for both reading and writing data. This interface serves as the entry point for\n * table operations after resolving a table through a {@link SnapshotBuilder}.\n *\n * <p>The snapshot represents a consistent view of the table at the resolved version. All operations\n * on this snapshot will see the same data and metadata, ensuring consistency across reads and\n * writes within the same snapshot.\n *\n * <p>There are two ways to create a {@code Snapshot}:\n *\n * <ul>\n *   <li><b>New API (recommended):</b> Use {@link TableManager#loadSnapshot(String)} to get a {@link\n *       SnapshotBuilder}, which can then be configured and built into a snapshot\n *   <li><b>Legacy API:</b> Use {@code Table.forPath(path)} followed by methods like {@code\n *       getLatestSnapshot()}, {@code getSnapshotAtTimestamp()}, etc.\n * </ul>\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface Snapshot {\n\n  /**\n   * Indicates how a checksum file should be written for this Snapshot. Use with {@link\n   * #writeChecksum(Engine, ChecksumWriteMode)}.\n   */\n  enum ChecksumWriteMode {\n    /**\n     * Checksum info is already loaded in this Snapshot and can be written cheaply. This mode uses\n     * pre-computed CRC information already in memory.\n     */\n    SIMPLE,\n\n    /**\n     * Checksum info is not loaded in this Snapshot and requires replaying the delta log since the\n     * latest checksum (if present) to compute. This mode performs full computation and writing.\n     */\n    FULL\n  }\n\n  /** @return the file system path to this table */\n  String getPath();\n\n  /** @return the version of this snapshot in the Delta table */\n  long getVersion();\n\n  /**\n   * Get the names of the partition columns in the Delta table at this snapshot.\n   *\n   * <p>The partition column names are returned in the order they are defined in the Delta table\n   * schema. If the table does not define any partition columns, this method returns an empty list.\n   *\n   * @return a list of partition column names, or an empty list if the table is not partitioned.\n   */\n  List<String> getPartitionColumnNames();\n\n  /**\n   * Get the timestamp (in milliseconds since the Unix epoch) of the latest commit in this snapshot.\n   *\n   * @param engine the engine to use for IO operations\n   * @return the timestamp of the latest commit\n   */\n  long getTimestamp(Engine engine);\n\n  /** @return the schema of the Delta table at this snapshot */\n  StructType getSchema();\n\n  /**\n   * Returns the configuration for the provided domain if it exists in the snapshot. Returns empty\n   * if the domain is not present in the snapshot.\n   *\n   * @param domain the domain to look up\n   * @return the domain configuration or empty\n   */\n  Optional<String> getDomainMetadata(String domain);\n\n  /**\n   * Get all table properties for the Delta table at this snapshot.\n   *\n   * @return a {@link Map} of table properties.\n   */\n  Map<String, String> getTableProperties();\n\n  /** @return statistics about this snapshot */\n  SnapshotStatistics getStatistics();\n\n  /** @return a scan builder to construct a {@link Scan} to read data from this snapshot */\n  ScanBuilder getScanBuilder();\n\n  /**\n   * @return a {@link UpdateTableTransactionBuilder} to build an update table transaction\n   * @since 3.4.0\n   */\n  UpdateTableTransactionBuilder buildUpdateTableTransaction(String engineInfo, Operation operation);\n\n  /**\n   * Publishes all catalog commits at this table version. Applicable only to catalog-managed tables.\n   * This method is a no-op for filesystem-managed tables, if the committer doesn't support\n   * publishing, or if there's no catalog commits to publish.\n   *\n   * <p>Publishing copies ratified catalog commits to the Delta log as published Delta files,\n   * reducing catalog storage requirements and enabling some table maintenance operations, like\n   * checkpointing.\n   *\n   * @param engine the engine to use for publishing commits\n   * @see io.delta.kernel.commit.CatalogCommitter#publish\n   * @throws PublishFailedException if the publish operation fails\n   * @return a new Snapshot reflecting the published state\n   */\n  Snapshot publish(Engine engine) throws PublishFailedException;\n\n  /**\n   * Writes a checksum file for this snapshot using the specified mode:\n   *\n   * <ul>\n   *   <li>SIMPLE: Uses pre-computed CRC information already loaded in memory. This is the fastest\n   *       approach but requires CRC info to be available. Throws {@link IllegalStateException} if\n   *       CRC information is not available.\n   *   <li>FULL: Computes the necessary CRC information by replaying the delta log since the latest\n   *       checksum (if present). This may be expensive for large tables when CRC information is not\n   *       available.\n   * </ul>\n   *\n   * <p>Use {@link SnapshotStatistics#getChecksumWriteMode()} to check if writing is needed and to\n   * determine the appropriate mode.\n   *\n   * <p>This method should only be called if a checksum file does not already exist at this version.\n   * If it already does, this method is a no-op.\n   *\n   * <p>If a concurrent writer creates the checksum file for this version between when this snapshot\n   * was loaded and when this method is called, the method will detect the existing checksum and\n   * return successfully without error. This ensures safe concurrent checksum writing.\n   *\n   * @param engine the engine to use for writing the checksum file and potentially reading the log\n   * @param mode the mode specifying how to write the checksum (SIMPLE or FULL)\n   * @throws IOException if an I/O error occurs during checksum computation or writing\n   * @throws IllegalStateException if mode is SIMPLE but CRC information is not available\n   * @see SnapshotStatistics#getChecksumWriteMode()\n   */\n  void writeChecksum(Engine engine, ChecksumWriteMode mode) throws IOException;\n\n  /**\n   * Writes a checkpoint for the current snapshot.\n   *\n   * @param engine The execution engine used to write the checkpoint and, if necessary, read log\n   *     entries required to compute it.\n   * @throws IOException If an I/O error occurs while computing or writing the checkpoint.\n   * @throws IllegalStateException If attempting to create a checkpoint on an unpublished catalog\n   *     managed commit.\n   * @throws CheckpointAlreadyExistsException If a checkpoint already exists for the target snapshot\n   *     version.\n   */\n  void writeCheckpoint(Engine engine) throws IOException, CheckpointAlreadyExistsException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/SnapshotBuilder.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport java.util.List;\n\n/**\n * Builder for constructing a {@link Snapshot} instance.\n *\n * <p>This builder allows table managers (filesystems, catalogs) to provide any information they may\n * know about a Delta table and get back a {@link Snapshot}. When {@link #build(Engine)} is invoked,\n * Kernel will automatically fill any missing information needed to construct the {@link Snapshot}\n * by reading from the filesystem as needed.\n *\n * <p>If no version is specified, the builder will resolve to the latest version. Depending on the\n * {@link ParsedLogData} provided, Kernel can avoid expensive filesystem operations to improve\n * performance.\n */\n@Experimental\npublic interface SnapshotBuilder {\n  /**\n   * Configures the builder to resolve the table at a specific version.\n   *\n   * <p>This method is mutually exclusive with {@link #atTimestamp(long, Snapshot)}. If both are\n   * called, an {@link IllegalArgumentException} will be thrown.\n   *\n   * @param version the version number to resolve to\n   * @return a new builder instance configured for the specified version\n   */\n  SnapshotBuilder atVersion(long version);\n\n  /**\n   * Configures the builder to resolve the table at a specific timestamp.\n   *\n   * <p>This returns a Snapshot for the latest version of the table that was committed before or at\n   * the given timestamp. Specifically:\n   *\n   * <ul>\n   *   <li>If a commit version exactly matches the provided timestamp, the snapshot at that version\n   *       is resolved.\n   *   <li>Otherwise, the latest commit version with a timestamp less than the provided one is\n   *       resolved.\n   *   <li>If the provided timestamp is less than the timestamp of any committed version, snapshot\n   *       resolution will fail.\n   *   <li>If the provided timestamp is after (strictly greater than) the timestamp of the latest\n   *       version of the table, snapshot resolution will fail.\n   * </ul>\n   *\n   * <p>This method is mutually exclusive with {@link #atVersion(long)}. If both are called, an\n   * {@link IllegalArgumentException} will be thrown.\n   *\n   * @param millisSinceEpochUTC timestamp to resolve the snapshot for in milliseconds since the unix\n   *     epoch\n   * @return a new builder instance configured for the specified timestamp\n   */\n  SnapshotBuilder atTimestamp(long millisSinceEpochUTC, Snapshot latestSnapshot);\n\n  /**\n   * Provides a custom committer to use at transaction commit time.\n   *\n   * <p>Catalog implementations that wish to support the catalogManaged Delta table feature should\n   * provide to engines their own catalog-specific Committer implementation which may, for example,\n   * send a commit RPC to the catalog service to finalize the commit.\n   *\n   * <p>If no committer is provided, a default committer will be created that only supports writing\n   * into filesystem-managed Delta tables.\n   *\n   * @param committer the committer to use\n   * @return a new builder instance with the provided committer\n   * @see Committer\n   */\n  SnapshotBuilder withCommitter(Committer committer);\n\n  /**\n   * Provides parsed log data to optimize table resolution.\n   *\n   * <p>When log data is provided, Kernel can avoid reading from the filesystem for information that\n   * is already available in the parsed data, improving performance. Currently, only ratified staged\n   * commits are supported.\n   *\n   * @param logData the parsed log data to use for optimization\n   * @return a new builder instance with the provided log data\n   */\n  SnapshotBuilder withLogData(List<ParsedLogData> logData);\n\n  /**\n   * Provides protocol and metadata information to optimize table resolution.\n   *\n   * <p>When protocol and metadata are provided, Kernel can avoid reading this information from the\n   * filesystem, improving performance.\n   *\n   * @param protocol the protocol information\n   * @param metadata the metadata information\n   * @return a new builder instance with the provided protocol and metadata\n   */\n  // TODO: [delta-io/delta#4820] Public Protocol API\n  // TODO: [delta-io/delta#4821] Public Metadata API\n  SnapshotBuilder withProtocolAndMetadata(Protocol protocol, Metadata metadata);\n\n  /**\n   * Specifies the maximum table version known by the catalog.\n   *\n   * <p>This method is used by catalog implementations for catalog-managed Delta tables to indicate\n   * the latest ratified version of the table. This ensures that any snapshot resolution operations\n   * respect the catalog's view of the table state.\n   *\n   * <p>Important: This method is required for catalog-managed tables and must not be used for\n   * file-system managed tables. An {@link IllegalArgumentException} will be thrown at build time if\n   * this constraint is violated.\n   *\n   * <p>When specified, the following additional constraints are enforced:\n   *\n   * <ul>\n   *   <li>If {@link #atVersion(long)} is used for time travel, the requested version must be less\n   *       than or equal to the max catalog version.\n   *   <li>If {@link #atTimestamp(long, Snapshot)} is used for time travel, the provided {@code\n   *       latestSnapshot} must have a version equal to the max catalog version.\n   *   <li>If {@link #withLogData(List)} is provided and {@link #atVersion(long)} is used, the log\n   *       data must include the requested version (i.e., the tail of the log data must have a\n   *       version greater than or equal to the requested version).\n   *   <li>If {@link #withLogData(List)} is provided and no version is specified (resolving to\n   *       latest), the log data must end with the max catalog version.\n   * </ul>\n   *\n   * @param version the maximum table version known by the catalog (must be {@code >= 0})\n   * @return a new builder instance with the specified max catalog version\n   * @throws IllegalArgumentException if version is negative\n   */\n  SnapshotBuilder withMaxCatalogVersion(long version);\n\n  /**\n   * Constructs the {@link Snapshot} using the provided engine.\n   *\n   * <p>This method will read any missing information from the filesystem using the provided engine\n   * to complete the snapshot resolution process.\n   *\n   * @param engine the engine to use for filesystem operations\n   * @return the resolved snapshot instance\n   */\n  Snapshot build(Engine engine);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/Table.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.CheckpointAlreadyExistsException;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.internal.TableImpl;\nimport java.io.IOException;\n\n/**\n * Represents the Delta Lake table for a given path.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface Table {\n  /**\n   * Instantiate a table object for the Delta Lake table at the given path.\n   *\n   * <ul>\n   *   <li>Behavior when the table location doesn't exist:\n   *       <ul>\n   *         <li>Reads will fail with a {@link TableNotFoundException}\n   *         <li>Writes will create the location\n   *       </ul>\n   *   <li>Behavior when the table location exists (with contents or not) but not a Delta table:\n   *       <ul>\n   *         <li>Reads will fail with a {@link TableNotFoundException}\n   *         <li>Writes will create a Delta table at the given location. If there are any existing\n   *             files in the location that are not already part of the Delta table, they will\n   *             remain excluded from the Delta table.\n   *       </ul>\n   * </ul>\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @param path location of the table. Path is resolved to fully qualified path using the given\n   *     {@code engine}.\n   * @return an instance of {@link Table} representing the Delta table at the given path\n   */\n  static Table forPath(Engine engine, String path) {\n    return TableImpl.forPath(engine, path);\n  }\n\n  /**\n   * The fully qualified path of this {@link Table} instance.\n   *\n   * @param engine {@link Engine} instance.\n   * @return the table path.\n   * @since 3.2.0\n   */\n  String getPath(Engine engine);\n\n  /**\n   * Get the latest snapshot of the table.\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @return an instance of {@link Snapshot}\n   * @throws TableNotFoundException if the table is not found\n   */\n  Snapshot getLatestSnapshot(Engine engine) throws TableNotFoundException;\n\n  /**\n   * Get the snapshot at the given {@code versionId}.\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @param versionId snapshot version to retrieve\n   * @return an instance of {@link Snapshot}\n   * @throws TableNotFoundException if the table is not found\n   * @throws KernelException if the provided version is less than the first available version or\n   *     greater than the last available version\n   * @since 3.2.0\n   */\n  Snapshot getSnapshotAsOfVersion(Engine engine, long versionId) throws TableNotFoundException;\n\n  /**\n   * Get the snapshot of the table at the given {@code timestamp}. This is the latest version of the\n   * table that was committed before or at {@code timestamp}.\n   *\n   * <p>Specifically:\n   *\n   * <ul>\n   *   <li>If a commit version exactly matches the provided timestamp, we return the table snapshot\n   *       at that version.\n   *   <li>Else, we return the latest commit version with a timestamp less than the provided one.\n   *   <li>If the provided timestamp is less than the timestamp of any committed version, we throw\n   *       an error.\n   *   <li>If the provided timestamp is after (strictly greater than) the timestamp of the latest\n   *       version of the table, we throw an error\n   * </ul>\n   *\n   * .\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @param millisSinceEpochUTC timestamp to fetch the snapshot for in milliseconds since the unix\n   *     epoch\n   * @return an instance of {@link Snapshot}\n   * @throws TableNotFoundException if the table is not found\n   * @throws KernelException if the provided timestamp is before the earliest available version or\n   *     after the latest available version\n   * @since 3.2.0\n   */\n  Snapshot getSnapshotAsOfTimestamp(Engine engine, long millisSinceEpochUTC)\n      throws TableNotFoundException;\n\n  /**\n   * Create a {@link TransactionBuilder} which can create a {@link Transaction} object to mutate the\n   * table.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param engineInfo information about the engine that is making the updates.\n   * @param operation metadata of operation that is being performed. E.g. \"insert\", \"delete\".\n   * @return {@link TransactionBuilder} instance to build the transaction.\n   * @since 3.2.0\n   */\n  TransactionBuilder createTransactionBuilder(\n      Engine engine, String engineInfo, Operation operation);\n\n  /**\n   * Checkpoint the table at given version. It writes a single checkpoint file.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param version Version to checkpoint.\n   * @throws TableNotFoundException if the table is not found\n   * @throws CheckpointAlreadyExistsException if a checkpoint already exists at the given version\n   * @throws IOException for any I/O error.\n   * @since 3.2.0\n   */\n  void checkpoint(Engine engine, long version)\n      throws TableNotFoundException, CheckpointAlreadyExistsException, IOException;\n\n  /**\n   * Computes and writes a checksum file for the table at given version. If a checksum file already\n   * exists, this method does nothing.\n   *\n   * <p>Note: For very large tables, this operation may be expensive as it requires scanning the log\n   * to compute table statistics.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param version Version to generate checksum file for.\n   * @throws TableNotFoundException if the table is not found\n   * @throws IOException for any I/O error.\n   * @since 4.0.0\n   */\n  void checksum(Engine engine, long version) throws TableNotFoundException, IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/TableManager.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.internal.CreateTableTransactionBuilderImpl;\nimport io.delta.kernel.internal.commitrange.CommitRangeBuilderImpl;\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl;\nimport io.delta.kernel.transaction.CreateTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\n\n/**\n * The entry point for loading and creating Delta tables.\n *\n * <p>TableManager provides static factory methods for creating builders that can resolve Delta\n * tables to specific snapshots. This is the primary interface for table discovery and resolution in\n * the Delta Kernel.\n */\n@Experimental\npublic interface TableManager {\n\n  /**\n   * Creates a builder for loading a snapshot at the given path.\n   *\n   * <p>The returned builder can be configured to load the snapshot at a specific version or with\n   * additional metadata to optimize the loading process. If no version is specified, the builder\n   * will resolve to the latest version of the table.\n   *\n   * @param path the file system path to the Delta table\n   * @return a {@link SnapshotBuilder} that can be used to load a {@link Snapshot} at the given path\n   */\n  static SnapshotBuilder loadSnapshot(String path) {\n    return new SnapshotBuilderImpl(path);\n  }\n\n  /**\n   * Creates a {@link CreateTableTransactionBuilder} to build a create table transaction.\n   *\n   * @param path the file system path for the delta table being created\n   * @param engineInfo information about the engine that is making the update.\n   * @param schema the schema for the delta table being created\n   * @return create table builder instance to build the transaction\n   * @since 3.4.0\n   */\n  static CreateTableTransactionBuilder buildCreateTableTransaction(\n      String path, StructType schema, String engineInfo) {\n    return new CreateTableTransactionBuilderImpl(path, schema, engineInfo);\n  }\n\n  /**\n   * Creates a builder for loading a CommitRange at a given path.\n   *\n   * <p>The returned builder can be configured with an end version or timestamp, and with additional\n   * metadata to optimize the loading process.\n   *\n   * @param path the file system path to the Delta table\n   * @param startBoundary the boundary specification for the start of the commit range, must not be\n   *     null\n   * @return a {@link CommitRangeBuilder} that can be used to load a {@link CommitRange} at the\n   *     given path\n   */\n  static CommitRangeBuilder loadCommitRange(\n      String path, CommitRangeBuilder.CommitBoundary startBoundary) {\n    return new CommitRangeBuilderImpl(path, startBoundary);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/Transaction.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport static io.delta.kernel.internal.DeltaErrors.dataSchemaMismatch;\nimport static io.delta.kernel.internal.DeltaErrors.partitionColumnMissingInData;\nimport static io.delta.kernel.internal.TransactionImpl.getStatisticsColumns;\nimport static io.delta.kernel.internal.data.TransactionStateRow.*;\nimport static io.delta.kernel.internal.util.ColumnMapping.blockIfColumnMappingEnabled;\nimport static io.delta.kernel.internal.util.PartitionUtils.getTargetDirectory;\nimport static io.delta.kernel.internal.util.PartitionUtils.validateAndSanitizePartitionValues;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.SchemaUtils.findColIndex;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.ConcurrentWriteException;\nimport io.delta.kernel.exceptions.DomainDoesNotExistException;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.DataWriteContextImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.actions.SingleAction;\nimport io.delta.kernel.internal.columndefaults.ColumnDefaults;\nimport io.delta.kernel.internal.data.TransactionStateRow;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatV2MetadataValidatorAndUpdater;\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatV3MetadataValidatorAndUpdater;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.types.VariantType;\nimport io.delta.kernel.utils.*;\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\n/**\n * Represents a transaction to mutate a Delta table.\n *\n * @since 3.2.0\n */\n@Evolving\npublic interface Transaction {\n  /**\n   * Get the schema of the table. If the connector is adding any data to the table through this\n   * transaction, it should have the same schema as the table schema.\n   */\n  StructType getSchema(Engine engine);\n\n  /**\n   * Get the list of logical names of the partition columns. This helps the connector to do physical\n   * partitioning of the data before asking the Kernel to stage the data per partition.\n   */\n  List<String> getPartitionColumns(Engine engine);\n\n  /**\n   * Gets the latest version of the table used as the base of this transaction. This returns -1 when\n   * the table is being created in this transaction.\n   *\n   * @return The version of the table as of the beginning of this Transaction\n   */\n  long getReadTableVersion();\n\n  /**\n   * Get the state of the transaction. The state helps Kernel do the transformations to logical data\n   * according to the Delta protocol and table features enabled on the table. The engine should use\n   * this at the data writer task to transform the logical data that the engine wants to write to\n   * the table in to physical data that goes in data files using {@link\n   * Transaction#transformLogicalData(Engine, Row, CloseableIterator, Map)}\n   */\n  Row getTransactionState(Engine engine);\n\n  /** @return a committer that owns and controls commits to this table */\n  @Experimental\n  Committer getCommitter();\n\n  /**\n   * Commit the transaction including the data action rows generated by {@link\n   * Transaction#generateAppendActions}.\n   *\n   * @param engine {@link Engine} instance.\n   * @param dataActions Iterable of data actions to commit. These data actions are generated by the\n   *     {@link Transaction#generateAppendActions(Engine, Row, CloseableIterator,\n   *     DataWriteContext)}. The {@link CloseableIterable} allows the Kernel to access the list of\n   *     actions multiple times (in case of retries to resolve the conflicts due to other writers to\n   *     the table). Kernel provides a in-memory based implementation of {@link CloseableIterable}\n   *     with utility API {@link CloseableIterable#inMemoryIterable(CloseableIterator)}\n   * @return {@link TransactionCommitResult} status of the successful transaction.\n   * @throws ConcurrentWriteException when the transaction has encountered a non-retryable conflicts\n   *     or exceeded the maximum number of retries reached. The connector needs to rerun the query\n   *     on top of the latest table state and retry the transaction.\n   */\n  TransactionCommitResult commit(Engine engine, CloseableIterable<Row> dataActions)\n      throws ConcurrentWriteException;\n\n  /**\n   * Adds custom properties that will be passed through to the committer. These properties allow\n   * connectors to inject catalog-specific metadata without Kernel inspection. Repeated calls to\n   * this method will overwrite any previously set properties.\n   */\n  void withCommitterProperties(Supplier<Map<String, String>> committerProperties);\n\n  /**\n   * Commit the provided domain metadata as part of this transaction. If this is called more than\n   * once with the same {@code domain} the latest provided {@code config} will be committed in the\n   * transaction. Only user-controlled domains are allowed (aka. domains with a `delta.` prefix are\n   * not allowed). Adding and removing a domain with the same identifier in the same txn is not\n   * allowed. Adding domain metadata to a table that does not support the table feature is not\n   * allowed. To enable the table feature, make sure to call {@link\n   * TransactionBuilder#withDomainMetadataSupported}\n   *\n   * @param domain the domain identifier\n   * @param config configuration string for this domain\n   */\n  void addDomainMetadata(String domain, String config);\n\n  /**\n   * Mark the domain metadata with identifier {@code domain} as removed in this transaction. If this\n   * domain does not exist in the latest version of the table, calling {@link\n   * Transaction#commit(Engine, CloseableIterable)} will throw a {@link\n   * DomainDoesNotExistException}. Adding and removing a domain with the same identifier in one txn\n   * is not allowed.\n   *\n   * @param domain the domain identifier for the domain to remove\n   */\n  void removeDomainMetadata(String domain);\n\n  /**\n   * Given the logical data that needs to be written to the table, convert it into the required\n   * physical data depending upon the table Delta protocol and features enabled on the table. Kernel\n   * takes care of adding any additional column or removing existing columns that doesn't need to be\n   * in physical data files. All these transformations are driven by the Delta protocol and table\n   * features enabled on the table.\n   *\n   * <p>The given data should belong to exactly one partition. It is the job of the connector to do\n   * partitioning of the data before calling the API. Partition values are provided as map of column\n   * name to partition value (as {@link Literal}). If the table is an un-partitioned table, then map\n   * should be empty.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param transactionState The transaction state\n   * @param dataIter Iterator of logical data (with schema same as the table schema) to transform to\n   *     physical data. All the data n this iterator should belong to one physical partition and it\n   *     should also include the partition data.\n   * @param partitionValues The partition values for the data. If the table is un-partitioned, the\n   *     map should be empty\n   * @return Iterator of physical data to write to the data files.\n   */\n  static CloseableIterator<FilteredColumnarBatch> transformLogicalData(\n      Engine engine,\n      Row transactionState,\n      CloseableIterator<FilteredColumnarBatch> dataIter,\n      Map<String, Literal> partitionValues) {\n\n    // Note: `partitionValues` are not used as of now in this API, but taking the partition\n    // values as input forces the connector to not pass data from multiple partitions this\n    // API in a single call.\n    StructType tableSchema = getLogicalSchema(transactionState);\n    List<String> partitionColNames = getPartitionColumnsList(transactionState);\n    validateAndSanitizePartitionValues(tableSchema, partitionColNames, partitionValues);\n\n    // TODO: add support for:\n    // - enforcing the constraints\n    // - generating the default value columns\n    // - generating the generated columns\n\n    boolean isIcebergCompatEnabled =\n        isIcebergCompatV2Enabled(transactionState) || isIcebergCompatV3Enabled(transactionState);\n    Protocol protocol = getProtocol(transactionState);\n    boolean materializePartitionColumnsEnabled =\n        protocol.supportsFeature(TableFeatures.MATERIALIZE_PARTITION_COLUMNS_W_FEATURE);\n    blockIfColumnMappingEnabled(transactionState);\n    blockIfVariantDataTypeIsDefined(tableSchema);\n    // We recognize the AllowColumnDefaults feature for Iceberg v3\n    // but do not support writing with it yet\n    ColumnDefaults.blockWriteIfEnabled(transactionState);\n\n    // TODO: set the correct schema once writing into column mapping enabled table is supported.\n    String tablePath = getTablePath(transactionState);\n    return dataIter.map(\n        filteredBatch -> {\n          ColumnarBatch data = filteredBatch.getData();\n          if (!data.getSchema().isWriteCompatible(tableSchema)) {\n            throw dataSchemaMismatch(tablePath, tableSchema, data.getSchema());\n          }\n\n          if (isIcebergCompatEnabled || materializePartitionColumnsEnabled) {\n            // Move partition columns to the end of the schema for iceberg compat enabled tables\n            // or when materialize partition columns feature is enabled.\n            for (String partitionColName : partitionColNames) {\n              int partitionColIndex = findColIndex(data.getSchema(), partitionColName);\n              if (partitionColIndex < 0) {\n                throw partitionColumnMissingInData(tablePath, partitionColName);\n              }\n              StructField partitionColField = data.getSchema().at(partitionColIndex);\n              ColumnVector partitionColVector = data.getColumnVector(partitionColIndex);\n              data = data.withDeletedColumnAt(partitionColIndex);\n              // Add the partition column at the end\n              data =\n                  data.withNewColumn(\n                      data.getSchema().length(), partitionColField, partitionColVector);\n            }\n          } else {\n            // Remove partition columns entirely for non-materialized partitions, and non-iceberg\n            // compat tables.\n            for (String partitionColName : partitionColNames) {\n              int partitionColIndex = findColIndex(data.getSchema(), partitionColName);\n              if (partitionColIndex < 0) {\n                throw partitionColumnMissingInData(tablePath, partitionColName);\n              }\n              data = data.withDeletedColumnAt(partitionColIndex);\n            }\n          }\n          return new FilteredColumnarBatch(data, filteredBatch.getSelectionVector());\n        });\n  }\n\n  /**\n   * Currently Kernel supports only metadata updates for variants (including shredded values). Block\n   * any physical data writes if variant exists in the schema\n   */\n  static void blockIfVariantDataTypeIsDefined(StructType tableSchema) {\n    boolean variantFieldExists =\n        new SchemaIterable(tableSchema)\n            .stream().anyMatch(field -> field.getField().getDataType() instanceof VariantType);\n    if (variantFieldExists) {\n      throw new UnsupportedOperationException(\n          \"Transforming logical data with variant data is currently unsupported\");\n    }\n  }\n\n  /**\n   * Get the context for writing data into a table. The context tells the connector where the data\n   * should be written. For partitioned table context is generated per partition. So, the connector\n   * should call this API for each partition. For un-partitioned table, the context is same for all\n   * the data.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param transactionState The transaction state\n   * @param partitionValues The partition values for the data. If the table is un-partitioned, the\n   *     map should be empty\n   * @return {@link DataWriteContext} containing metadata about where and how the data for partition\n   *     should be written.\n   */\n  static DataWriteContext getWriteContext(\n      Engine engine, Row transactionState, Map<String, Literal> partitionValues) {\n    blockIfColumnMappingEnabled(transactionState);\n    StructType tableSchema = getLogicalSchema(transactionState);\n    List<String> partitionColNames = getPartitionColumnsList(transactionState);\n\n    partitionValues =\n        validateAndSanitizePartitionValues(tableSchema, partitionColNames, partitionValues);\n\n    String targetDirectory =\n        getTargetDirectory(getTablePath(transactionState), partitionColNames, partitionValues);\n\n    return new DataWriteContextImpl(\n        targetDirectory, partitionValues, getStatisticsColumns(transactionState));\n  }\n\n  /**\n   * For given data files, generate Delta actions that can be committed in a transaction. These data\n   * files are the result of writing the data returned by {@link Transaction#transformLogicalData}\n   * with the context returned by {@link Transaction#getWriteContext}.\n   *\n   * @param engine {@link Engine} instance.\n   * @param transactionState State of the transaction.\n   * @param fileStatusIter Iterator of row objects representing each data file written. When {@code\n   *     delta.icebergCompatV2} is enabled, each data file status should contain {@link\n   *     DataFileStatistics} with at least the {@link DataFileStatistics#getNumRecords()} field set.\n   * @param dataWriteContext The context used when writing the data files given in {@code\n   *     fileStatusIter}\n   * @return {@link CloseableIterator} of {@link Row} representing the actions to commit using\n   *     {@link Transaction#commit}.\n   */\n  static CloseableIterator<Row> generateAppendActions(\n      Engine engine,\n      Row transactionState,\n      CloseableIterator<DataFileStatus> fileStatusIter,\n      DataWriteContext dataWriteContext) {\n    checkArgument(\n        dataWriteContext instanceof DataWriteContextImpl,\n        \"DataWriteContext is not created by the `Transaction.getWriteContext()`\");\n\n    boolean isIcebergCompatV2Enabled = isIcebergCompatV2Enabled(transactionState);\n    boolean isIcebergCompatV3Enabled = isIcebergCompatV3Enabled(transactionState);\n\n    URI tableRoot = new Path(getTablePath(transactionState)).toUri();\n    StructType physicalSchema = TransactionStateRow.getPhysicalSchema(transactionState);\n    return fileStatusIter.map(\n        dataFileStatus -> {\n          if (isIcebergCompatV2Enabled) {\n            IcebergCompatV2MetadataValidatorAndUpdater.validateDataFileStatus(dataFileStatus);\n          } else if (isIcebergCompatV3Enabled) {\n            IcebergCompatV3MetadataValidatorAndUpdater.validateDataFileStatus(dataFileStatus);\n          }\n\n          AddFile addFileRow =\n              AddFile.convertDataFileStatus(\n                  physicalSchema,\n                  tableRoot,\n                  dataFileStatus,\n                  ((DataWriteContextImpl) dataWriteContext).getPartitionValues(),\n                  true /* dataChange */,\n                  // TODO: populate tags in generateAppendActions\n                  Collections.emptyMap() /* tags */,\n                  Optional.empty() /* baseRowId */,\n                  Optional.empty() /* defaultRowCommitVersion */,\n                  Optional.empty() /* deletionVectorDescriptor */);\n          return SingleAction.createAddFileSingleAction(addFileRow.toRow());\n        });\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/TransactionBuilder.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.ConcurrentTransactionException;\nimport io.delta.kernel.exceptions.DomainDoesNotExistException;\nimport io.delta.kernel.exceptions.InvalidConfigurationValueException;\nimport io.delta.kernel.exceptions.TableAlreadyExistsException;\nimport io.delta.kernel.exceptions.UnknownConfigurationException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.types.StructType;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Builder for creating a {@link Transaction} to mutate a Delta table.\n *\n * @since 3.2.0\n */\n@Evolving\npublic interface TransactionBuilder {\n  /**\n   * Set the schema of the table. If setting the schema on an existing table for a schema evolution,\n   * then column mapping must be enabled. This API will preserve field metadata for fields such as\n   * field IDs and physical names. If field metadata is not specified for a field, it is considered\n   * as a new column and new IDs/physical names will be specified. The possible schema evolutions\n   * supported include column additions, removals, renames, and moves. If a schema evolution is\n   * performed, implementations must perform the following validations:\n   *\n   * <ul>\n   *   <li>No duplicate columns are allowed\n   *   <li>Column names contain only valid characters\n   *   <li>Data types are supported\n   *   <li>No new non-nullable fields are added\n   *   <li>Physical column name consistency is preserved in the new schema\n   *   <li>No type changes\n   *   <li>ToDo: Nested IDs for array/map types are preserved in the new schema\n   *   <li>ToDo: Validate invalid field reorderings\n   * </ul>\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param schema The new schema of the table.\n   * @return updated {@link TransactionBuilder} instance.\n   * @throws io.delta.kernel.exceptions.KernelException in case column mapping is not enabled\n   * @throws IllegalArgumentException in case of any validation failure\n   */\n  TransactionBuilder withSchema(Engine engine, StructType schema);\n\n  /**\n   * Set the list of partitions columns when create a new partitioned table.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param partitionColumns The partition columns of the table. These should be a subset of the\n   *     columns in the schema. Only top-level columns are allowed to be partitioned. Note:\n   *     Clustering columns and partition columns cannot coexist in a table.\n   * @return updated {@link TransactionBuilder} instance.\n   */\n  TransactionBuilder withPartitionColumns(Engine engine, List<String> partitionColumns);\n\n  /**\n   * Set the list of clustering columns when create a new clustered table.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param clusteringColumns The clustering columns of the table. These should be a subset of the\n   *     columns in the schema. Both top-level and nested columns are allowed to be clustered. Note:\n   *     Clustering columns and partition columns cannot coexist in a table.\n   * @return updated {@link TransactionBuilder} instance.\n   */\n  TransactionBuilder withClusteringColumns(Engine engine, List<Column> clusteringColumns);\n\n  /**\n   * Set the transaction identifier for idempotent writes. Incremental processing systems (e.g.,\n   * streaming systems) that track progress using their own application-specific versions need to\n   * record what progress has been made, in order to avoid duplicating data in the face of failures\n   * and retries during writes. By setting the transaction identifier, the Delta table can ensure\n   * that the data with same identifier is not written multiple times. For more information refer to\n   * the Delta protocol section <a\n   * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#transaction-identifiers\">\n   * Transaction Identifiers</a>.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param applicationId The application ID that is writing to the table.\n   * @param transactionVersion The version of the transaction. This should be monotonically\n   *     increasing with each write for the same application ID.\n   * @return updated {@link TransactionBuilder} instance.\n   */\n  TransactionBuilder withTransactionId(\n      Engine engine, String applicationId, long transactionVersion);\n\n  /**\n   * Set the table properties for the table. When the table already contains the property with same\n   * key, it gets replaced if it doesn't have the same value. Note, user-properties (those without a\n   * '.delta' prefix) are case-sensitive. Delta-properties are case-insensitive and are normalized\n   * to their expected case before writing to the log.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @param properties The table properties to set. These are key-value pairs that can be used to\n   *     configure the table. And these properties are stored in the table metadata.\n   * @return updated {@link TransactionBuilder} instance.\n   * @since 3.3.0\n   */\n  TransactionBuilder withTableProperties(Engine engine, Map<String, String> properties);\n\n  /**\n   * Unset the provided table properties on the table. If a property does not exist this is a no-op.\n   * For now this is only supported for user-properties (in other words, does not support 'delta.'\n   * prefixed properties). An exception will be thrown upon calling {@link\n   * TransactionBuilder#build(Engine)} if the same key is both set and unset in the same\n   * transaction. Note, user-properties (those without a '.delta' prefix) are case-sensitive.\n   *\n   * @param propertyKeys the table property keys to unset (remove from the table properties)\n   * @return updated {@link TransactionBuilder} instance.\n   * @throws IllegalArgumentException if 'delta.' prefixed keys are provided\n   */\n  TransactionBuilder withTablePropertiesRemoved(Set<String> propertyKeys);\n\n  /**\n   * Set the maximum number of times to retry a transaction if a concurrent write is detected. This\n   * defaults to 200\n   *\n   * @param maxRetries The number of times to retry\n   * @return updated {@link TransactionBuilder} instance\n   */\n  TransactionBuilder withMaxRetries(int maxRetries);\n\n  /**\n   * Set the number of commits between log compactions. Defaults to 0 (disabled). For more\n   * information see the Delta protocol section <a\n   * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#log-compaction-files\">Log\n   * Compaction Files</a>.\n   *\n   * @param logCompactionInterval The commits between log compactions\n   * @return updated {@link TransactionBuilder} instance\n   */\n  TransactionBuilder withLogCompactionInverval(int logCompactionInterval);\n\n  /**\n   * Enables support for Domain Metadata on this table if it is not supported already. The table\n   * feature _must_ be supported on the table to add or remove domain metadata using {@link\n   * Transaction#addDomainMetadata} or {@link Transaction#removeDomainMetadata}. See <a\n   * href=\"https://docs.delta.io/latest/versioning.html#how-does-delta-lake-manage-feature-compatibility\">\n   * How does Delta Lake manage feature compatibility?</a> for more details on table feature\n   * support.\n   *\n   * <p>See the Delta protocol for more information on how to use <a\n   * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#domain-metadata\">Domain\n   * Metadata</a>. This may break existing writers that do not support the Domain Metadata feature;\n   * readers will be unaffected.\n   */\n  TransactionBuilder withDomainMetadataSupported();\n\n  /**\n   * Build the transaction. Also validates the given info to ensure that a valid transaction can be\n   * created.\n   *\n   * @param engine {@link Engine} instance to use.\n   * @throws ConcurrentTransactionException if the table already has a committed transaction with\n   *     the same given transaction identifier.\n   * @throws InvalidConfigurationValueException if the value of the property is invalid.\n   * @throws UnknownConfigurationException if any of the properties are unknown to {@link\n   *     TableConfig}.\n   * @throws DomainDoesNotExistException if removing a domain that does not exist in the latest\n   *     version of the table\n   * @throws TableAlreadyExistsException if the operation provided when calling {@link\n   *     Table#createTransactionBuilder(Engine, String, Operation)} is CREATE_TABLE and the table\n   *     already exists\n   */\n  Transaction build(Engine engine);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/TransactionCommitResult.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.metrics.TransactionReport;\nimport io.delta.kernel.utils.CloseableIterable;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Contains the result of a successful transaction commit. Returned by {@link\n * Transaction#commit(Engine, CloseableIterable)}.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class TransactionCommitResult {\n  private final long version;\n  private final List<PostCommitHook> postCommitHooks;\n  private final TransactionReport transactionReport;\n  private final Optional<SnapshotImpl> postCommitSnapshotOpt;\n\n  public TransactionCommitResult(\n      long version,\n      List<PostCommitHook> postCommitHooks,\n      TransactionReport transactionReport,\n      Optional<SnapshotImpl> postCommitSnapshotOpt) {\n    this.version = version;\n    this.postCommitHooks = requireNonNull(postCommitHooks);\n    this.transactionReport = requireNonNull(transactionReport);\n    this.postCommitSnapshotOpt = requireNonNull(postCommitSnapshotOpt);\n  }\n\n  /**\n   * Contains the version of the transaction committed as.\n   *\n   * @return version the transaction is committed as.\n   */\n  public long getVersion() {\n    return version;\n  }\n\n  /**\n   * Operations for connector to trigger post-commit.\n   *\n   * <p>Usage:\n   *\n   * <ul>\n   *   <li>Async: Call {@link PostCommitHook#threadSafeInvoke(Engine)} in separate thread.\n   *   <li>Sync: Direct call {@link PostCommitHook#threadSafeInvoke(Engine)} and block until\n   *       operation ends.\n   * </ul>\n   *\n   * @return list of post-commit operations\n   */\n  public List<PostCommitHook> getPostCommitHooks() {\n    return postCommitHooks;\n  }\n\n  /** @return the report and metrics for this transaction */\n  public TransactionReport getTransactionReport() {\n    return transactionReport;\n  }\n\n  /**\n   * Return the snapshot at the committed version.\n   *\n   * <p>Currently, Kernel does not support getting the post-commit snapshot for transactions that\n   * experienced conflicts.\n   */\n  public Optional<Snapshot> getPostCommitSnapshot() {\n    return postCommitSnapshotOpt.map(s -> s); // Map needed to upcast to Optional<Snapshot>\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/annotation/Evolving.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * APIs that are meant to evolve towards becoming stable APIs, but are not stable APIs yet. Evolving\n * interfaces can change from one feature release to another release (i.e. 3.0 to 3.1).\n */\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({\n  ElementType.TYPE,\n  ElementType.FIELD,\n  ElementType.METHOD,\n  ElementType.PARAMETER,\n  ElementType.CONSTRUCTOR,\n  ElementType.LOCAL_VARIABLE,\n  ElementType.PACKAGE\n})\npublic @interface Evolving {}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/annotation/Experimental.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * APIs that are still under active development and are expected to change. Experimental interfaces\n * can change, break, or be deleted from one feature release to another release (i.e. 3.0 to 3.1).\n */\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({\n  ElementType.TYPE,\n  ElementType.FIELD,\n  ElementType.METHOD,\n  ElementType.PARAMETER,\n  ElementType.CONSTRUCTOR,\n  ElementType.LOCAL_VARIABLE,\n  ElementType.PACKAGE\n})\npublic @interface Experimental {}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/CatalogCommitter.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.engine.Engine;\nimport java.util.Collections;\nimport java.util.Map;\n\n/**\n * {@link Committer} sub-interface for catalog-managed tables. Provides catalog-specific operations\n * not applicable to filesystem-managed tables.\n */\n@Experimental\npublic interface CatalogCommitter extends Committer {\n\n  /**\n   * Returns required catalog table properties that must be set in the Delta metadata.\n   *\n   * <p>These properties are automatically injected during CREATE and REPLACE operations and cannot\n   * be changed or removed by users. Any attempt to set these properties to different values or\n   * remove them will result in a validation error.\n   *\n   * @return a map of required catalog properties\n   */\n  default Map<String, String> getRequiredTableProperties() {\n    return Collections.emptyMap();\n  }\n\n  /**\n   * Publishes catalog commits to the Delta log. Applicable only to catalog-managed tables.\n   *\n   * <p>Publishing is the act of copying ratified catalog commits to the Delta log as published\n   * Delta files (e.g., {@code _delta_log/00000000000000000001.json}).\n   *\n   * <p>The benefits of publishing include:\n   *\n   * <ul>\n   *   <li>Reduces the number of commits the catalog needs to store internally and serve to readers\n   *   <li>Enables table maintenance operations that must operate on published versions only, such\n   *       as checkpointing and log compaction\n   * </ul>\n   *\n   * <p>Requirements:\n   *\n   * <ul>\n   *   <li>This method must ensure that all catalog commits are published to the Delta log up to and\n   *       including the snapshot version specified in {@code publishMetadata}\n   *   <li>Commits must be published in order: version V-1 must be published before version V\n   * </ul>\n   *\n   * <p>Catalog-specific semantics: Each catalog implementation may specify its own rules and\n   * semantics for publishing, including whether it expects to be notified immediately upon\n   * publishing success, whether published deltas must appear with PUT-if-absent semantics in the\n   * Delta log, and whether publishing happens in the client-side or server-side catalog-component.\n   *\n   * @param engine the {@link Engine} instance used for publishing commits\n   * @param publishMetadata the {@link PublishMetadata} containing the snapshot version up to which\n   *     all catalog commits must be published, the log path, and list of catalog commits\n   * @throws PublishFailedException if the publish operation fails\n   */\n  void publish(Engine engine, PublishMetadata publishMetadata) throws PublishFailedException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/CatalogCommitterUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class CatalogCommitterUtils {\n  private CatalogCommitterUtils() {}\n\n  /** Property key that specifies which version last updated the catalog entry. */\n  public static final String METASTORE_LAST_UPDATE_VERSION = \"delta.lastUpdateVersion\";\n\n  /**\n   * Property key that specifies the timestamp (in milliseconds since the Unix epoch) of the last\n   * commit that updated the catalog entry.\n   */\n  public static final String METASTORE_LAST_COMMIT_TIMESTAMP = \"delta.lastCommitTimestamp\";\n\n  /**\n   * Extract protocol-related properties from the given protocol.\n   *\n   * <p>For a Protocol(3, 7) with reader features [\"columnMapping\", \"deletionVectors\"] and writer\n   * features [\"appendOnly\", \"columnMapping\"], this would return properties like:\n   *\n   * <ul>\n   *   <li>delta.minReaderVersion: 3\n   *   <li>delta.minWriterVersion: 7\n   *   <li>delta.feature.columnMapping: supported\n   *   <li>delta.feature.deletionVectors: supported\n   *   <li>delta.feature.appendOnly: supported\n   * </ul>\n   */\n  public static Map<String, String> extractProtocolProperties(Protocol protocol) {\n    final Map<String, String> properties = new HashMap<>();\n\n    properties.put(\n        TableConfig.MIN_PROTOCOL_READER_VERSION_KEY,\n        String.valueOf(protocol.getMinReaderVersion()));\n    properties.put(\n        TableConfig.MIN_PROTOCOL_WRITER_VERSION_KEY,\n        String.valueOf(protocol.getMinWriterVersion()));\n\n    if (protocol.supportsReaderFeatures() || protocol.supportsWriterFeatures()) {\n      for (String featureName : protocol.getReaderAndWriterFeatures()) {\n        properties.put(\n            TableFeatures.getTableFeature(featureName).getTableFeatureSupportKey(),\n            TableFeatures.SET_TABLE_FEATURE_SUPPORTED_VALUE);\n      }\n    }\n\n    return properties;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/CommitFailedException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport io.delta.kernel.annotation.Experimental;\n\n/**\n * Exception raised by {@link Committer#commit}.\n *\n * <pre>\n *  | retryable | conflict  | meaning                                                         |\n *  |   no      |   no      | something bad happened (e.g. auth failure)                      |\n *  |   no      |   yes     | permanent transaction conflict (e.g. multi-table commit failed) |\n *  |   yes     |   no      | transient error (e.g. network hiccup)                           |\n *  |   yes     |   yes     | physical conflict (allowed to rebase and retry)                 |\n * </pre>\n */\n@Experimental\npublic class CommitFailedException extends Exception {\n\n  private final boolean retryable;\n  private final boolean conflict;\n\n  // TODO: [delta-io/delta#4908] Include the winning, conflicting catalog ratified commits here\n\n  public CommitFailedException(boolean retryable, boolean conflict, String message) {\n    super(message);\n    this.retryable = retryable;\n    this.conflict = conflict;\n  }\n\n  public CommitFailedException(\n      boolean retryable, boolean conflict, String message, Throwable cause) {\n    super(message, cause);\n    this.retryable = retryable;\n    this.conflict = conflict;\n  }\n\n  /** Returns whether the commit can be retried. */\n  public boolean isRetryable() {\n    return retryable;\n  }\n\n  /** Returns whether the commit failed due to a conflict. */\n  public boolean isConflict() {\n    return conflict;\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"%s: retryable=%s, conflict=%s, msg=%s\",\n        getClass().getName(), retryable, conflict, getMessage());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/CommitMetadata.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.internal.actions.CommitInfo;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Tuple2;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\n/**\n * Contains all information (excluding the iterator of finalized actions) required to commit changes\n * to a Delta table.\n */\n@Experimental\npublic class CommitMetadata {\n\n  /**\n   * Represents the different types of commits based on filesystem-managed and catalog-managed state\n   * transitions.\n   */\n  public enum CommitType {\n    /** Creating a new filesystem-managed table */\n    FILESYSTEM_CREATE,\n    /** Creating a new catalog-managed table */\n    CATALOG_CREATE,\n    /** Writing to an existing filesystem-managed table */\n    FILESYSTEM_WRITE,\n    /** Writing to an existing catalog-managed table */\n    CATALOG_WRITE,\n    /** Upgrading a filesystem-managed table to a catalog-managed table */\n    FILESYSTEM_UPGRADE_TO_CATALOG,\n    /** Downgrading a catalog-managed table to a filesystem-managed table */\n    CATALOG_DOWNGRADE_TO_FILESYSTEM\n  }\n\n  private final long version;\n  private final String logPath;\n  private final CommitInfo commitInfo;\n  private final List<DomainMetadata> commitDomainMetadatas;\n  private final Supplier<Map<String, String>> committerProperties;\n  private final Optional<Tuple2<Protocol, Metadata>> readPandMOpt;\n  private final Optional<Protocol> newProtocolOpt;\n  private final Optional<Metadata> newMetadataOpt;\n  private final Optional<Long> maxKnownPublishedDeltaVersion;\n\n  public CommitMetadata(\n      long version,\n      String logPath,\n      CommitInfo commitInfo,\n      List<DomainMetadata> commitDomainMetadatas,\n      Supplier<Map<String, String>> committerProperties,\n      Optional<Tuple2<Protocol, Metadata>> readPandMOpt,\n      Optional<Protocol> newProtocolOpt,\n      Optional<Metadata> newMetadataOpt,\n      Optional<Long> maxKnownPublishedDeltaVersion) {\n    checkArgument(version >= 0, \"version must be non-negative: %d\", version);\n    this.version = version;\n    this.logPath = requireNonNull(logPath, \"logPath is null\");\n    this.commitInfo = requireNonNull(commitInfo, \"commitInfo is null\");\n    this.commitDomainMetadatas =\n        Collections.unmodifiableList(\n            requireNonNull(commitDomainMetadatas, \"txnDomainMetadatas is null\"));\n    this.committerProperties = requireNonNull(committerProperties, \"committerProperties is null\");\n    this.readPandMOpt = requireNonNull(readPandMOpt, \"readPandMOpt is null\");\n    this.newProtocolOpt = requireNonNull(newProtocolOpt, \"newProtocolOpt is null\");\n    this.newMetadataOpt = requireNonNull(newMetadataOpt, \"newMetadataOpt is null\");\n    this.maxKnownPublishedDeltaVersion =\n        requireNonNull(maxKnownPublishedDeltaVersion, \"maxKnownPublishedDeltaVersion is null\");\n\n    checkArgument(\n        readPandMOpt.isPresent() || newProtocolOpt.isPresent(),\n        \"At least one of readPandMOpt.protocol or newProtocolOpt must be present\");\n    checkArgument(\n        readPandMOpt.isPresent() || newMetadataOpt.isPresent(),\n        \"At least one of readPandMOpt.metadata or newMetadataOpt must be present\");\n\n    checkReadStateAbsentIfAndOnlyIfVersion0();\n    checkInCommitTimestampPresentIfCatalogManaged();\n  }\n\n  /** The version of the Delta table this commit is targeting. */\n  public long getVersion() {\n    return version;\n  }\n\n  /** The path to the Delta log directory, located at {@code <table_root>/_delta_log}. */\n  public String getDeltaLogDirPath() {\n    return logPath;\n  }\n\n  /** The {@link CommitInfo} that is being written as part of this commit. */\n  public CommitInfo getCommitInfo() {\n    return commitInfo;\n  }\n\n  /**\n   * The {@link DomainMetadata}s that are being written as part of this commit. Includes those that\n   * are being explicitly added and those that are being explicitly removed (tombstoned).\n   *\n   * <p>Does not include the domain metadatas that already exist in the transaction's read snapshot,\n   * if any.\n   */\n  public List<DomainMetadata> getCommitDomainMetadatas() {\n    return commitDomainMetadatas;\n  }\n\n  /**\n   * Returns custom properties provided by the connector to be passed through to the committer.\n   * These properties are not inspected by Kernel and are used for catalog-specific functionality.\n   */\n  public Supplier<Map<String, String>> getCommitterProperties() {\n    return committerProperties;\n  }\n\n  /**\n   * The {@link Protocol} that was read at the beginning of the commit. Empty if a new table is\n   * being created.\n   */\n  public Optional<Protocol> getReadProtocolOpt() {\n    return readPandMOpt.map(x -> x._1);\n  }\n\n  /**\n   * The {@link Metadata} that was read at the beginning of the commit. Empty if a new table is\n   * being created.\n   */\n  public Optional<Metadata> getReadMetadataOpt() {\n    return readPandMOpt.map(x -> x._2);\n  }\n\n  /**\n   * The {@link Protocol} that is being written as part of this commit. Empty if the protocol is not\n   * being changed.\n   */\n  public Optional<Protocol> getNewProtocolOpt() {\n    return newProtocolOpt;\n  }\n\n  /**\n   * The {@link Metadata} that is being written as part of this commit. Empty if the metadata is not\n   * being changed.\n   */\n  public Optional<Metadata> getNewMetadataOpt() {\n    return newMetadataOpt;\n  }\n\n  /**\n   * Returns the maximum known published delta version at commit time.\n   *\n   * <p>This is a best-effort API that returns what was actually seen during Snapshot and\n   * Transaction construction, not the authoritative maximum published delta version in the log.\n   *\n   * <p>{@code Optional.empty()} means \"we don't know\" - not necessarily that no deltas have been\n   * published.\n   *\n   * <p>{@code Optional.of(-1)} means it is known that there are no published deltas (e.g., during\n   * CREATE)\n   *\n   * @return the maximum known published delta version, or empty if unknown\n   */\n  public Optional<Long> getMaxKnownPublishedDeltaVersion() {\n    return maxKnownPublishedDeltaVersion;\n  }\n\n  /**\n   * Returns the effective {@link Protocol} that will be in place after this commit. If a new\n   * protocol is being written as part of this commit, returns the new protocol. Otherwise, returns\n   * the protocol that was read at the beginning of the commit.\n   */\n  public Protocol getEffectiveProtocol() {\n    return newProtocolOpt.orElseGet(() -> getReadProtocolOpt().get());\n  }\n\n  /**\n   * Returns the effective {@link Metadata} that will be in place after this commit. If new metadata\n   * is being written as part of this commit, returns the new metadata. Otherwise, returns the\n   * metadata that was read at the beginning of the commit.\n   */\n  public Metadata getEffectiveMetadata() {\n    return newMetadataOpt.orElseGet(() -> getReadMetadataOpt().get());\n  }\n\n  /**\n   * Determines the type of commit based on whether this is a table creation and the catalog-managed\n   * status of the table before and after the commit.\n   */\n  public CommitType getCommitType() {\n    final boolean isCreate = version == 0;\n    final boolean readVersionCatalogManaged =\n        readPandMOpt.map(x -> x._1).map(TableFeatures::isCatalogManagedSupported).orElse(false);\n    final boolean writeVersionCatalogManaged =\n        TableFeatures.isCatalogManagedSupported(getEffectiveProtocol());\n\n    if (isCreate && writeVersionCatalogManaged) {\n      return CommitType.CATALOG_CREATE;\n    } else if (isCreate && !writeVersionCatalogManaged) {\n      return CommitType.FILESYSTEM_CREATE;\n    } else if (readVersionCatalogManaged && writeVersionCatalogManaged) {\n      return CommitType.CATALOG_WRITE;\n    } else if (readVersionCatalogManaged && !writeVersionCatalogManaged) {\n      return CommitType.CATALOG_DOWNGRADE_TO_FILESYSTEM;\n    } else if (!readVersionCatalogManaged && writeVersionCatalogManaged) {\n      return CommitType.FILESYSTEM_UPGRADE_TO_CATALOG;\n    } else {\n      return CommitType.FILESYSTEM_WRITE;\n    }\n  }\n\n  /**\n   * Returns the corresponding published Delta log file path for this commit, which is in the form\n   * of {@code <table_path>/_delta_log/0000000000000000000<version>.json}.\n   *\n   * <p>Usages:\n   *\n   * <ul>\n   *   <li>Filesystem-managed committers must write to this file path.\n   *   <li>Catalog-managed committers must publish to this file path, if/when they so choose.\n   * </ul>\n   */\n  public String getPublishedDeltaFilePath() {\n    return FileNames.deltaFile(logPath, version);\n  }\n\n  /**\n   * Returns a new staged commit file path with a unique UUID for this commit. Each invocation\n   * returns a new, unique value, in the form of {@code\n   * <table_path>/_delta_log/_staged_commits/0000000000000000000<version>.<uuid>.json}\n   *\n   * <p>Catalog-managed committers may use this path to write new staged commits.\n   */\n  public String generateNewStagedCommitFilePath() {\n    return FileNames.stagedCommitFile(logPath, version);\n  }\n\n  private void checkReadStateAbsentIfAndOnlyIfVersion0() {\n    checkArgument(\n        (version == 0) == (!readPandMOpt.isPresent()),\n        \"Table creation (version 0) requires absent readPandMOpt, while existing table writes \"\n            + \"(version > 0) require present readPandMOpt\");\n  }\n\n  private void checkInCommitTimestampPresentIfCatalogManaged() {\n    if (TableFeatures.isCatalogManagedSupported(getEffectiveProtocol())) {\n      checkArgument(\n          commitInfo.getInCommitTimestamp().isPresent(),\n          \"InCommitTimestamp must be present for commits to catalogManaged tables\");\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/CommitResponse.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.internal.files.ParsedDeltaData;\n\n/** Response container for the result of a commit operation. */\n@Experimental\npublic class CommitResponse {\n\n  // TODO: Create a DeltaLogData extends ParsedLogData that includes commit timestamp information.\n  private final ParsedDeltaData commitLogData;\n\n  public CommitResponse(ParsedDeltaData commitLogData) {\n    this.commitLogData = commitLogData;\n  }\n\n  /**\n   * The parsed log data resulting from the commit operation. Note that for catalog-managed tables,\n   * this may be the ratified staged commit, the ratified inline commit, or even a published Delta\n   * file that the {@link Committer} implementation decided to publish after committing to the\n   * managing catalog.\n   */\n  public ParsedDeltaData getCommitLogData() {\n    return commitLogData;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/Committer.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.utils.CloseableIterator;\n\n/**\n * Interface for committing changes to Delta tables, supporting both filesystem-managed and\n * catalog-managed tables.\n */\n@Experimental\npublic interface Committer {\n\n  /**\n   * Commits the given {@code finalizedActions} and {@code commitMetadata} to the table.\n   *\n   * <p>Filesystem-managed tables: Implementations must write the {@code finalizedActions} into a\n   * new Delta JSON file at version {@link CommitMetadata#getVersion()} using atomic file operations\n   * (PUT-if-absent semantics).\n   *\n   * <p>Catalog-managed tables: Implementations must follow the commit rules and requirements as\n   * dictated by the managing catalog to ensure commit atomicity and consistency. This may involve:\n   *\n   * <ol>\n   *   <li>Writing the finalized actions into a staged commit file\n   *   <li>Calling catalog commit APIs with the staged commit location (or inline content) and\n   *       additional metadata (such as the commit Protocol and Metadata)\n   *   <li>Publishing ratified catalog commits into the Delta log\n   * </ol>\n   *\n   * @param engine the {@link Engine} instance used for committing changes\n   * @param finalizedActions the iterator of finalized actions to be committed\n   * @param commitMetadata the {@link CommitMetadata} associated with this commit, which contains\n   *     additional metadata required to commit the finalized actions to the table, such as the\n   *     commit version, Delta log path, and more.\n   * @return CommitResponse containing the resultant commit\n   * @throws CommitFailedException if the commit operation fails\n   */\n  CommitResponse commit(\n      Engine engine, CloseableIterator<Row> finalizedActions, CommitMetadata commitMetadata)\n      throws CommitFailedException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/PublishFailedException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\n/** Exception thrown when publishing catalog commits to the Delta log fails. */\npublic class PublishFailedException extends RuntimeException {\n\n  /**\n   * Constructs a new PublishFailedException with the specified detail message.\n   *\n   * @param message the detail message\n   */\n  public PublishFailedException(String message) {\n    super(message);\n  }\n\n  /**\n   * Constructs a new PublishFailedException with the specified detail message and cause.\n   *\n   * @param message the detail message\n   * @param cause the cause of the exception\n   */\n  public PublishFailedException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/commit/PublishMetadata.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.internal.files.LogDataUtils;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.lang.ListUtils;\nimport java.util.List;\n\n/** Metadata required for publishing catalog commits to the Delta log. */\n@Experimental\npublic class PublishMetadata {\n\n  private final long snapshotVersion;\n  private final String logPath;\n\n  /**\n   * List of contiguous catalog commits to be published, in ascending order of version number.\n   *\n   * <p>Must be non-empty and must end with a catalog commit whose version matches {@code\n   * snapshotVersion}.\n   */\n  private final List<ParsedCatalogCommitData> ascendingCatalogCommits;\n\n  public PublishMetadata(\n      long snapshotVersion, String logPath, List<ParsedCatalogCommitData> ascendingCatalogCommits) {\n    this.snapshotVersion = snapshotVersion;\n    this.logPath = requireNonNull(logPath, \"logPath is null\");\n    this.ascendingCatalogCommits =\n        requireNonNull(ascendingCatalogCommits, \"ascendingCatalogCommits is null\");\n\n    validateCommitsNonEmpty();\n    validateCommitsContiguous();\n    validateLastCommitMatchesSnapshotVersion();\n  }\n\n  /** @return the snapshot version up to which all catalog commits must be published */\n  public long getSnapshotVersion() {\n    return snapshotVersion;\n  }\n\n  /** @return the path to the Delta log directory, located at {@code <table_root>/_delta_log} */\n  public String getLogPath() {\n    return logPath;\n  }\n\n  /**\n   * @return the list of contiguous catalog commits to be published, in ascending order of version\n   *     number\n   */\n  public List<ParsedCatalogCommitData> getAscendingCatalogCommits() {\n    return ascendingCatalogCommits;\n  }\n\n  private void validateCommitsNonEmpty() {\n    checkArgument(!ascendingCatalogCommits.isEmpty(), \"ascendingCatalogCommits must be non-empty\");\n  }\n\n  private void validateCommitsContiguous() {\n    LogDataUtils.validateLogDataIsSortedContiguous(ascendingCatalogCommits);\n  }\n\n  private void validateLastCommitMatchesSnapshotVersion() {\n    final long lastCommitVersion = ListUtils.getLast(ascendingCatalogCommits).getVersion();\n\n    checkArgument(\n        lastCommitVersion == snapshotVersion,\n        \"Last catalog commit version %d must equal snapshot version %d\",\n        lastCommitVersion,\n        snapshotVersion);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/ArrayValue.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.data;\n\n/** Abstraction to represent a single array value in a {@link ColumnVector}. */\npublic interface ArrayValue {\n  /** The number of elements in the array */\n  int getSize();\n\n  /**\n   * A {@link ColumnVector} containing the array elements with exactly {@link ArrayValue#getSize()}\n   * elements.\n   */\n  ColumnVector getElements();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/ColumnVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.data;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.types.DataType;\nimport java.math.BigDecimal;\n\n/**\n * Represents zero or more values of a single column.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface ColumnVector extends AutoCloseable {\n  /** @return the data type of this column vector. */\n  DataType getDataType();\n\n  /** @return number of elements in the vector */\n  int getSize();\n\n  /** Cleans up memory for this column vector. The column vector is not usable after this. */\n  @Override\n  void close();\n\n  /**\n   * @param rowId\n   * @return whether the value at {@code rowId} is NULL.\n   */\n  boolean isNullAt(int rowId);\n\n  /**\n   * Returns the boolean type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Boolean value at the given row id\n   */\n  default boolean getBoolean(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the byte type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Byte value at the given row id\n   */\n  default byte getByte(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the short type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Short value at the given row id\n   */\n  default short getShort(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the int type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Integer value at the given row id\n   */\n  default int getInt(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the long type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Long value at the given row id\n   */\n  default long getLong(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the float type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Float value at the given row id\n   */\n  default float getFloat(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the double type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Double value at the given row id\n   */\n  default double getDouble(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the binary type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Binary value at the given row id\n   */\n  default byte[] getBinary(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the string type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return String value at the given row id\n   */\n  default String getString(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Returns the decimal type value for {@code rowId}. The return value is undefined and can be\n   * anything, if the slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return Decimal value at the given row id\n   */\n  default BigDecimal getDecimal(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Return the map value located at {@code rowId}. Returns null if the slot for {@code rowId} is\n   * null\n   */\n  default MapValue getMap(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Return the array value located at {@code rowId}. Returns null if the slot for {@code rowId} is\n   * null\n   */\n  default ArrayValue getArray(int rowId) {\n    throw new UnsupportedOperationException(\"Invalid value request for data type\");\n  }\n\n  /**\n   * Get the child vector associated with the given ordinal. This method is applicable only to the\n   * {@code struct} type columns.\n   *\n   * @param ordinal Ordinal of the child vector to return.\n   */\n  default ColumnVector getChild(int ordinal) {\n    throw new UnsupportedOperationException(\n        \"Child vectors are not available for vector of type \" + getDataType());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/ColumnarBatch.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.data;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.internal.data.ColumnarBatchRow;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.util.NoSuchElementException;\n\n/**\n * Represents zero or more rows of records with same schema type.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface ColumnarBatch {\n  /** @return the schema of the data in this batch. */\n  StructType getSchema();\n\n  /**\n   * Return the {@link ColumnVector} for the given ordinal in the columnar batch. If the ordinal is\n   * not valid throws error.\n   *\n   * @param ordinal the ordinal of the column to retrieve\n   * @return the {@link ColumnVector} for the given ordinal in the columnar batch\n   */\n  ColumnVector getColumnVector(int ordinal);\n\n  /** @return the number of rows/records in the columnar batch */\n  int getSize();\n\n  /**\n   * Return a copy of the {@link ColumnarBatch} with given new column vector inserted at the given\n   * {@code columnVector} at given {@code ordinal}. Shift the existing {@link ColumnVector}s located\n   * at from {@code ordinal} to the end by one position. The schema of the new {@link ColumnarBatch}\n   * will also be changed to reflect the newly inserted vector.\n   *\n   * @param ordinal\n   * @param columnSchema Column name and schema details of the new column vector.\n   * @param columnVector\n   * @return {@link ColumnarBatch} with new vector inserted.\n   * @throws IllegalArgumentException If the ordinal is not valid (ie less than zero or greater than\n   *     the current number of vectors).\n   */\n  default ColumnarBatch withNewColumn(\n      int ordinal, StructField columnSchema, ColumnVector columnVector) {\n    throw new UnsupportedOperationException(\"Not yet implemented\");\n  }\n\n  /**\n   * Return a copy of this {@link ColumnarBatch} with the column at given {@code ordinal} removed.\n   * All columns after the {@code ordinal} will be shifted to left by one position.\n   *\n   * @param ordinal Column ordinal to delete.\n   * @return {@link ColumnarBatch} with a column vector deleted.\n   */\n  default ColumnarBatch withDeletedColumnAt(int ordinal) {\n    throw new UnsupportedOperationException(\"Not yet implemented\");\n  }\n\n  /**\n   * Generate a copy of this {@link ColumnarBatch} with the given {@code newSchema}. The data types\n   * of elements in the given new schema and existing schema should be the same. Rest of the details\n   * such as name of the column or column metadata could be different.\n   *\n   * @param newSchema\n   * @return {@link ColumnarBatch} with given new schema.\n   */\n  default ColumnarBatch withNewSchema(StructType newSchema) {\n    throw new UnsupportedOperationException(\"Not yet implemented\");\n  }\n\n  /** @return iterator of {@link Row}s in this batch */\n  default CloseableIterator<Row> getRows() {\n    final ColumnarBatch batch = this;\n    return new CloseableIterator<Row>() {\n      int rowId = 0;\n      int maxRowId = getSize();\n\n      @Override\n      public boolean hasNext() {\n        return rowId < maxRowId;\n      }\n\n      @Override\n      public Row next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        Row row = new ColumnarBatchRow(batch, rowId);\n        rowId += 1;\n        return row;\n      }\n\n      @Override\n      public void close() {}\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/FilteredColumnarBatch.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.data;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.internal.data.ColumnarBatchRow;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.util.NoSuchElementException;\nimport java.util.Optional;\n\n/**\n * Represents a filtered version of {@link ColumnarBatch}. Contains original {@link ColumnarBatch}\n * with an optional selection vector to select only a subset of rows for the original columnar\n * batch.\n *\n * <p>The selection vector is of type boolean and has the same size as the data in the corresponding\n * {@link ColumnarBatch}. For each row index, a value of true in the selection vector indicates the\n * row at the same index in the data {@link ColumnarBatch} is valid; a value of false indicates the\n * row should be ignored. If there is no selection vector then all the rows are valid.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class FilteredColumnarBatch {\n  private final ColumnarBatch data;\n  private final Optional<ColumnVector> selectionVector;\n  private final Optional<String> filePath;\n  private final Optional<Integer> preComputedNumSelectedRows;\n\n  // TODO: use static factory for constructors\n  public FilteredColumnarBatch(ColumnarBatch data, Optional<ColumnVector> selectionVector) {\n    this.data = data;\n    this.selectionVector = selectionVector;\n    this.filePath = Optional.empty();\n    this.preComputedNumSelectedRows =\n        !selectionVector.isPresent() ? Optional.of(data.getSize()) : Optional.empty();\n  }\n\n  public FilteredColumnarBatch(\n      ColumnarBatch data,\n      Optional<ColumnVector> selectionVector,\n      String filePath,\n      int preComputedNumSelectedRows) {\n    this.data = data;\n    this.selectionVector = selectionVector;\n    checkArgument(\n        selectionVector.isPresent() || preComputedNumSelectedRows == data.getSize(),\n        \"Invalid precomputedNumSelectedRows: must be equal to batch size \"\n            + \"when selectionVector is empty.\");\n    checkArgument(\n        preComputedNumSelectedRows >= 0 && preComputedNumSelectedRows <= data.getSize(),\n        \"Invalid precomputedNumSelectedRows: \"\n            + \"must be no less than 0 and no larger than batch size.\");\n    this.filePath = Optional.of(filePath);\n    this.preComputedNumSelectedRows = Optional.of(preComputedNumSelectedRows);\n  }\n\n  /**\n   * Return the data as {@link ColumnarBatch}. Not all rows in the data are valid for this result.\n   * An optional <i>selectionVector</i> determines which rows are selected. If there is no selection\n   * vector that means all rows in this columnar batch are valid for this result.\n   *\n   * @return all the data read from the file\n   */\n  public ColumnarBatch getData() {\n    return data;\n  }\n\n  /**\n   * Returns the file path from which the data originates, if available.\n   *\n   * <p>Note: The file path may not be present. It is only set if explicitly provided in the\n   * constructor.\n   *\n   * @return an {@link Optional} containing the file path if available, otherwise an empty Optional\n   */\n  public Optional<String> getFilePath() {\n    return filePath;\n  }\n\n  /**\n   * Optional selection vector containing one entry for each row in <i>data</i> indicating whether a\n   * row is selected or not selected. If there is no selection vector then all the rows are valid.\n   *\n   * @return an optional {@link ColumnVector} indicating which rows are valid\n   */\n  public Optional<ColumnVector> getSelectionVector() {\n    return selectionVector;\n  }\n\n  /**\n   * Iterator of rows that survived the filter.\n   *\n   * @return Closeable iterator of rows that survived the filter. It is responsibility of the caller\n   *     to the close the iterator.\n   */\n  public CloseableIterator<Row> getRows() {\n    if (!selectionVector.isPresent()) {\n      return data.getRows();\n    }\n\n    return new CloseableIterator<Row>() {\n      private int rowId = 0;\n      private int maxRowId = data.getSize();\n      private int nextRowId = -1;\n\n      @Override\n      public boolean hasNext() {\n        for (; rowId < maxRowId && nextRowId == -1; rowId++) {\n          boolean isSelected =\n              !selectionVector.get().isNullAt(rowId) && selectionVector.get().getBoolean(rowId);\n          if (isSelected) {\n            nextRowId = rowId;\n            rowId++;\n            break;\n          }\n        }\n        return nextRowId != -1;\n      }\n\n      @Override\n      public Row next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        Row row = new ColumnarBatchRow(data, nextRowId);\n        nextRowId = -1;\n        return row;\n      }\n\n      @Override\n      public void close() {}\n    };\n  }\n\n  /**\n   * @return an {@link Optional} containing the pre-computed number of selected rows, if available.\n   *     <p>If present, this value was computed ahead of time and can be used without incurring any\n   *     additional cost. This occurs in two cases:\n   *     <ul>\n   *       <li>When the selection vector is absent, which implies that all rows are selected — in\n   *           this case, the number of selected rows is equal to the batch size.\n   *       <li>When the number of selected rows was explicitly pre-computed and passed in.\n   *     </ul>\n   *     <p>If empty, the caller must compute the number of selected rows manually from the\n   *     selection vector.\n   */\n  public Optional<Integer> getPreComputedNumSelectedRows() {\n    return preComputedNumSelectedRows;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/MapValue.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.data;\n\n/** Abstraction to represent a single map value in a {@link ColumnVector}. */\npublic interface MapValue {\n  /** The number of elements in the map */\n  int getSize();\n\n  /**\n   * A {@link ColumnVector} containing the keys. There are exactly {@link MapValue#getSize()} keys\n   * in the vector, and each key maps one-to-one to the value at the same index in {@link\n   * MapValue#getValues()}.\n   */\n  ColumnVector getKeys();\n\n  /**\n   * A {@link ColumnVector} containing the values. There are exactly {@link MapValue#getSize()}\n   * values in the vector, and maps one-to-one to the keys in {@link MapValue#getKeys()}\n   */\n  ColumnVector getValues();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/Row.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.data;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.types.StructType;\nimport java.math.BigDecimal;\n\n/**\n * Represent a single record\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface Row {\n\n  /** @return Schema of the record. */\n  StructType getSchema();\n\n  /**\n   * @param ordinal the ordinal of the column to check\n   * @return whether the column at {@code ordinal} is null\n   */\n  boolean isNullAt(int ordinal);\n\n  /**\n   * Return boolean value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of boolean type,\n   */\n  boolean getBoolean(int ordinal);\n\n  /**\n   * Return byte value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of boolean type,\n   */\n  byte getByte(int ordinal);\n\n  /**\n   * Return short value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of boolean type,\n   */\n  short getShort(int ordinal);\n\n  /**\n   * Return integer value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of integer type,\n   */\n  int getInt(int ordinal);\n\n  /**\n   * Return long value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of long type,\n   */\n  long getLong(int ordinal);\n\n  /**\n   * Return float value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of long type,\n   */\n  float getFloat(int ordinal);\n\n  /**\n   * Return double value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of long type,\n   */\n  double getDouble(int ordinal);\n\n  /**\n   * Return string value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of varchar type,\n   */\n  String getString(int ordinal);\n\n  /**\n   * Return decimal value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of decimal type,\n   */\n  BigDecimal getDecimal(int ordinal);\n\n  /**\n   * Return binary value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of varchar type,\n   */\n  byte[] getBinary(int ordinal);\n\n  /**\n   * Return struct value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of struct type,\n   */\n  Row getStruct(int ordinal);\n\n  /**\n   * Return array value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of array type,\n   */\n  ArrayValue getArray(int ordinal);\n\n  /**\n   * Return map value of the column located at the given ordinal. Throws error if the column at\n   * given ordinal is not of map type,\n   */\n  MapValue getMap(int ordinal);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/data/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** Delta Kernel interfaces for representing data in columnar and row format. */\npackage io.delta.kernel.data;\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/Engine.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Interface encapsulating all clients needed by the Delta Kernel in order to read the Delta table.\n * Connectors are expected to pass an implementation of this interface when reading a Delta table.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface Engine {\n\n  /**\n   * Get the connector provided {@link ExpressionHandler}.\n   *\n   * @return An implementation of {@link ExpressionHandler}.\n   */\n  ExpressionHandler getExpressionHandler();\n\n  /**\n   * Get the connector provided {@link JsonHandler}.\n   *\n   * @return An implementation of {@link JsonHandler}.\n   */\n  JsonHandler getJsonHandler();\n\n  /**\n   * Get the connector provided {@link FileSystemClient}.\n   *\n   * @return An implementation of {@link FileSystemClient}.\n   */\n  FileSystemClient getFileSystemClient();\n\n  /**\n   * Get the connector provided {@link ParquetHandler}.\n   *\n   * @return An implementation of {@link ParquetHandler}.\n   */\n  ParquetHandler getParquetHandler();\n\n  /** Get the engine's {@link MetricsReporter} instances to push reports to. */\n  default List<MetricsReporter> getMetricsReporters() {\n    return Collections.emptyList();\n  };\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/ExpressionHandler.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.ExpressionEvaluator;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.expressions.PredicateEvaluator;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructType;\n\n/**\n * Provides expression evaluation capability to Delta Kernel. Delta Kernel can use this client to\n * evaluate predicate on partition filters, fill up partition column values and any computation on\n * data using {@link Expression}s.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface ExpressionHandler {\n\n  /**\n   * Create an {@link ExpressionEvaluator} that can evaluate the given <i>expression</i> on {@link\n   * ColumnarBatch}s with the given <i>batchSchema</i>. The <i>expression</i> is expected to be a\n   * scalar expression where for each one input row there is a one output value.\n   *\n   * @param inputSchema Input data schema\n   * @param expression Expression to evaluate.\n   * @param outputType Expected result data type.\n   */\n  ExpressionEvaluator getEvaluator(\n      StructType inputSchema, Expression expression, DataType outputType);\n\n  /**\n   * Create a {@link PredicateEvaluator} that can evaluate the given <i>predicate</i> expression and\n   * return a selection vector ({@link ColumnVector} of {@code boolean} type).\n   *\n   * @param inputSchema Schema of the data referred by the given predicate expression.\n   * @param predicate Predicate expression to evaluate.\n   * @return\n   */\n  PredicateEvaluator getPredicateEvaluator(StructType inputSchema, Predicate predicate);\n\n  /**\n   * Create a selection vector, a boolean type {@link ColumnVector}, on top of the range of values\n   * given in <i>values</i> array.\n   *\n   * @param values Array of initial boolean values for the selection vector. The ownership of this\n   *     array is with the caller and this method shouldn't depend on it after the call is complete.\n   * @param from start index of the range, inclusive.\n   * @param to end index of the range, exclusive.\n   * @return A {@link ColumnVector} of {@code boolean} type values.\n   */\n  ColumnVector createSelectionVector(boolean[] values, int from, int to);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/FileReadRequest.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/** Represents a request to read a range of bytes from a given file. */\n@Evolving\npublic interface FileReadRequest {\n  /** Get the fully qualified path of the file from which to read the data. */\n  String getPath();\n\n  /** Get the start offset in the file from where to start reading the data. */\n  int getStartOffset();\n\n  /** Get the length of the data to read from the file starting at the <i>startOffset</i>. */\n  int getReadLength();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/FileReadResult.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport java.util.Objects;\n\n/**\n * The result of reading a batch of data in a file.\n *\n * <p>Encapsulates both the data read (as a {@link ColumnarBatch}) and the full path of the file\n * from which the data was read.\n */\npublic class FileReadResult {\n\n  private final ColumnarBatch data;\n  private final String filePath;\n\n  /**\n   * Constructs a {@code FileReadResult} object with the given data and file path.\n   *\n   * @param data the columnar batch of data read from the file\n   * @param filePath the path of the file from which the data was read\n   */\n  public FileReadResult(ColumnarBatch data, String filePath) {\n    this.data = Objects.requireNonNull(data, \"data must not be null\");\n    this.filePath = Objects.requireNonNull(filePath, \"filePath must not be null\");\n  }\n\n  /** @return {@link ColumnarBatch} of data that was read from the file. */\n  public ColumnarBatch getData() {\n    return data;\n  }\n\n  /** @return the path of the file that this data was read from. */\n  public String getFilePath() {\n    return filePath;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/FileSystemClient.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.ByteArrayInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\n\n/**\n * Provides file system related functionalities to Delta Kernel. Delta Kernel uses this client\n * whenever it needs to access the underlying file system where the Delta table is present.\n * Connector implementation of this interface can hide filesystem specific details from Delta\n * Kernel.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface FileSystemClient {\n  /**\n   * List the paths in the same directory that are lexicographically greater or equal to (UTF-8\n   * sorting) the given `path`. The result should also be sorted by the file name.\n   *\n   * @param filePath Fully qualified path to a file\n   * @return Closeable iterator of files. It is the responsibility of the caller to close the\n   *     iterator.\n   * @throws FileNotFoundException if the file at the given path is not found\n   * @throws IOException for any other IO error.\n   */\n  CloseableIterator<FileStatus> listFrom(String filePath) throws IOException;\n\n  /**\n   * Resolve the given path to a fully qualified path.\n   *\n   * @param path Input path\n   * @return Fully qualified path.\n   * @throws FileNotFoundException If the given path doesn't exist.\n   * @throws IOException for any other IO error.\n   */\n  String resolvePath(String path) throws IOException;\n\n  /**\n   * Return an iterator of byte streams one for each read request in {@code readRequests}. The\n   * returned streams are in the same order as the given {@link FileReadRequest}s. It is the\n   * responsibility of the caller to close each returned stream.\n   *\n   * @param readRequests Iterator of read requests\n   * @return Data for each request as one {@link ByteArrayInputStream}.\n   * @throws IOException\n   */\n  CloseableIterator<ByteArrayInputStream> readFiles(CloseableIterator<FileReadRequest> readRequests)\n      throws IOException;\n\n  /**\n   * Create a directory at the given path including parent directories. This mimicks the behavior of\n   * `mkdir -p` in Unix.\n   *\n   * @param path Full qualified path to create a directory at.\n   * @return true if the directory was created successfully, false otherwise.\n   * @throws IOException for any IO error.\n   */\n  boolean mkdirs(String path) throws IOException;\n\n  /**\n   * Delete the file at given path.\n   *\n   * @param path the path to delete. If path is a directory throws an exception.\n   * @return true if delete is successful else false.\n   * @throws IOException for any IO error.\n   */\n  boolean delete(String path) throws IOException;\n\n  /**\n   * Get the metadata of the file at the given path.\n   *\n   * @param path Fully qualified path to the file.\n   * @return Metadata of the file.\n   * @throws IOException for any IO error.\n   */\n  FileStatus getFileStatus(String path) throws IOException;\n\n  /**\n   * Atomically copy a file from source path to destination path. The copy operation should be\n   * atomic to ensure that the destination file is either fully copied or not present at all.\n   *\n   * @param srcPath Fully qualified path to the source file to copy\n   * @param destPath Fully qualified path to the destination where the file will be copied\n   * @param overwrite If true, overwrite the destination file if it already exists. If false, throw\n   *     an exception if the destination exists.\n   * @throws java.nio.file.FileAlreadyExistsException if the destination file already exists and\n   *     {@code overwrite} is false.\n   * @throws FileNotFoundException if the source file does not exist\n   * @throws IOException for any other IO error\n   */\n  void copyFileAtomically(String srcPath, String destPath, boolean overwrite) throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/JsonHandler.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.Optional;\n\n/**\n * Provides JSON handling functionality to Delta Kernel. Delta Kernel can use this client to parse\n * JSON strings into {@link ColumnarBatch} or read content from JSON files. Connectors can leverage\n * this interface to provide their best implementation of the JSON parsing capability to Delta\n * Kernel.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface JsonHandler {\n  /**\n   * Parse the given <i>json</i> strings and return the fields requested by {@code outputSchema} as\n   * columns in a {@link ColumnarBatch}.\n   *\n   * <p>There are a couple special cases that should be handled for specific data types:\n   *\n   * <ul>\n   *   <li><b>FloatType and DoubleType:</b> handle non-numeric numbers encoded as strings\n   *       <ul>\n   *         <li>NaN: <code>\"NaN\"</code>\n   *         <li>Positive infinity: <code>\"+INF\", \"Infinity\", \"+Infinity\"</code>\n   *         <li>Negative infinity: <code>\"-INF\", \"-Infinity\"\"</code>\n   *       </ul>\n   *   <li><b>DateType:</b> handle dates encoded as strings in the format <code>\"yyyy-MM-dd\"</code>\n   *   <li><b>TimestampType:</b> handle timestamps encoded as strings in the format <code>\n   *       \"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\"</code>\n   * </ul>\n   *\n   * @param jsonStringVector String {@link ColumnVector} of valid JSON strings.\n   * @param outputSchema Schema of the data to return from the parsed JSON. If any requested fields\n   *     are missing in the JSON string, a <i>null</i> is returned for that particular field in the\n   *     returned {@link Row}. The type for each given field is expected to match the type in the\n   *     JSON.\n   * @param selectionVector Optional selection vector indicating which rows to parse the JSON. If\n   *     present, only the selected rows should be parsed. Unselected rows should be all null in the\n   *     returned batch.\n   * @return a {@link ColumnarBatch} of schema {@code outputSchema} with one row for each entry in\n   *     {@code jsonStringVector}\n   */\n  ColumnarBatch parseJson(\n      ColumnVector jsonStringVector,\n      StructType outputSchema,\n      Optional<ColumnVector> selectionVector);\n\n  /**\n   * Read and parse the JSON format file at given locations and return the data as a {@link\n   * ColumnarBatch} with the columns requested by {@code physicalSchema}.\n   *\n   * @param fileIter Iterator of files to read data from.\n   * @param physicalSchema Select list of columns to read from the JSON file.\n   * @param predicate Optional predicate which the JSON reader can optionally use to prune rows that\n   *     don't satisfy the predicate. Because pruning is optional and may be incomplete, caller is\n   *     still responsible apply the predicate on the data returned by this method.\n   * @return an iterator of {@link ColumnarBatch}s containing the data in columnar format. It is the\n   *     responsibility of the caller to close the iterator. The data returned is in the same as the\n   *     order of files given in {@code scanFileIter}\n   * @throws IOException if an I/O error occurs during the read.\n   */\n  CloseableIterator<ColumnarBatch> readJsonFiles(\n      CloseableIterator<FileStatus> fileIter,\n      StructType physicalSchema,\n      Optional<Predicate> predicate)\n      throws IOException;\n\n  /**\n   * Serialize each {@code Row} in the iterator as JSON and write as a separate line in destination\n   * file. This call either succeeds in creating the file with given contents or no file is created\n   * at all. It won't leave behind a partially written file.\n   *\n   * <p>Following are the supported data types and their serialization rules. At a high-level, the\n   * JSON serialization is similar to that of {@code jackson} JSON serializer.\n   *\n   * <ul>\n   *   <li>Primitive types: @code boolean, byte, short, int, long, float, double, string}\n   *   <li>{@code struct}: any element whose value is null is not written to file\n   *   <li>{@code map}: only a {@code map} with {@code string} key type is supported. If an entry\n   *       value is {@code null}, it should be written to the file.\n   *   <li>{@code array}: {@code null} value elements are written to file\n   * </ul>\n   *\n   * @param filePath Fully qualified destination file path\n   * @param data Iterator of {@link Row} objects where each row should be serialized as JSON and\n   *     written as separate line in the destination file. It is the responsibility of the\n   *     implementation to close this iterator.\n   * @param overwrite If {@code true}, the file is overwritten if it already exists. If {@code\n   *     false} and a file exists {@link FileAlreadyExistsException} is thrown.\n   * @throws FileAlreadyExistsException if the file already exists and {@code overwrite} is false.\n   * @throws IOException if any other I/O error occurs.\n   */\n  void writeJsonFileAtomically(String filePath, CloseableIterator<Row> data, boolean overwrite)\n      throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/MetricsReporter.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.metrics.MetricsReport;\n\n/** Interface for reporting metrics for operations to a Delta table */\npublic interface MetricsReporter {\n\n  /** Indicates that an operation is done by reporting a {@link MetricsReport} */\n  void report(MetricsReport report);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/ParquetHandler.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.engine;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.MetadataColumnSpec;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.*;\nimport java.io.IOException;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Provides Parquet file related functionalities to Delta Kernel. Connectors can leverage this\n * interface to provide their own custom implementation of Parquet data file functionalities to\n * Delta Kernel.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface ParquetHandler {\n  /**\n   * Read the Parquet format files at the given locations and return the data as a {@link\n   * ColumnarBatch} with the columns requested by {@code physicalSchema}.\n   *\n   * <p>If {@code physicalSchema} has a {@link StructField} that is a metadata column {@link\n   * StructField#isMetadataColumn()} of type {@link MetadataColumnSpec#ROW_INDEX}, the column must\n   * be populated with the file row index.\n   *\n   * <p>How does a column in {@code physicalSchema} match to the column in the Parquet file? If the\n   * {@link StructField} has a field id in the {@code metadata} with key `parquet.field.id` the\n   * column is attempted to match by id. If the column is not found by id, the column is matched by\n   * name. When trying to find the column in Parquet by name, first case-sensitive match is used. If\n   * not found then a case-insensitive match is attempted.\n   *\n   * @param fileIter Iterator of files to read data from.\n   * @param physicalSchema Select list of columns to read from the Parquet file.\n   * @param predicate Optional predicate which the Parquet reader can optionally use to prune rows\n   *     that don't satisfy the predicate. Because pruning is optional and may be incomplete, caller\n   *     is still responsible apply the predicate on the data returned by this method.\n   * @return an iterator of {@link FileReadResult}s containing the data in columnar format along\n   *     with metadata. It is the responsibility of the caller to close the iterator. The data\n   *     returned is in the same as the order of files given in {@code scanFileIter}.\n   * @throws IOException if an I/O error occurs during the read.\n   */\n  CloseableIterator<FileReadResult> readParquetFiles(\n      CloseableIterator<FileStatus> fileIter,\n      StructType physicalSchema,\n      Optional<Predicate> predicate)\n      throws IOException;\n\n  /**\n   * Write the given data batches to a Parquet files. Try to keep the Parquet file size to given\n   * size. If the current file exceeds this size close the current file and start writing to a new\n   * file.\n   *\n   * <p>\n   *\n   * @param directoryPath Location where the data files should be written.\n   * @param dataIter Iterator of data batches to write. It is the responsibility of the calle to\n   *     close the iterator.\n   * @param statsColumns List of columns to collect statistics for. The statistics collection is\n   *     optional. If the implementation does not support statistics collection, it is ok to return\n   *     no statistics.\n   * @return an iterator of {@link DataFileStatus} containing the status of the written files. Each\n   *     status contains the file path and the optionally collected statistics for the file It is\n   *     the responsibility of the caller to close the iterator.\n   * @throws IOException if an I/O error occurs during the file writing. This may leave some files\n   *     already written in the directory. It is the responsibility of the caller to clean up.\n   * @since 3.2.0\n   */\n  CloseableIterator<DataFileStatus> writeParquetFiles(\n      String directoryPath,\n      CloseableIterator<FilteredColumnarBatch> dataIter,\n      List<Column> statsColumns)\n      throws IOException;\n\n  /**\n   * Write the given data as a Parquet file. This call either succeeds in creating the file with\n   * given contents or no file is created at all. It won't leave behind a partially written file.\n   *\n   * <p>\n   *\n   * @param filePath Fully qualified destination file path\n   * @param data Iterator of {@link FilteredColumnarBatch}\n   * @throws FileAlreadyExistsException if the file already exists and {@code overwrite} is false.\n   * @throws IOException if any other I/O error occurs.\n   */\n  void writeParquetFileAtomically(String filePath, CloseableIterator<FilteredColumnarBatch> data)\n      throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/engine/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Interfaces to allow the connector to bring their own implementation of functions such as reading\n * parquet files, listing files in a file system, parsing a JSON string etc. to Delta Kernel.\n */\npackage io.delta.kernel.engine;\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/CheckpointAlreadyExistsException.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport static java.lang.String.format;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when trying to create a checkpoint at version {@code v}, but there already exists a\n * checkpoint at version {@code v}.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class CheckpointAlreadyExistsException extends KernelException {\n  public CheckpointAlreadyExistsException(long version) {\n    super(format(\"Checkpoint for given version %d already exists in the table\", version));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/CommitRangeNotFoundException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Optional;\n\n/**\n * Exception thrown when Kernel cannot find any commit files in the requested version range. This\n * can happen when the requested versions don't exist in the table.\n *\n * @since 4.1.0\n */\n@Evolving\npublic class CommitRangeNotFoundException extends KernelException {\n\n  private final String tablePath;\n  private final long startVersion;\n  private final Optional<Long> endVersion;\n\n  public CommitRangeNotFoundException(\n      String tablePath, long startVersion, Optional<Long> endVersion) {\n    super(\n        String.format(\n            \"%s: Requested table changes between [%s, %s] but no log files found in the requested\"\n                + \" version range.\",\n            tablePath, startVersion, endVersion));\n    this.tablePath = tablePath;\n    this.startVersion = startVersion;\n    this.endVersion = endVersion;\n  }\n\n  /** @return the table path where the commit range was not found */\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  /** @return the start version of the requested commit range */\n  public long getStartVersion() {\n    return startVersion;\n  }\n\n  /**\n   * @return the end version of the requested commit range, or empty if no end version was specified\n   */\n  public Optional<Long> getEndVersion() {\n    return endVersion;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/CommitStateUnknownException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.commit.CommitFailedException;\n\n/**\n * Exception thrown when the Delta transaction commit system cannot determine whether a previous\n * commit attempt succeeded or failed, making it unsafe to continue with automatic retries.\n *\n * <p>This exception occurs in a specific sequence during transaction commit retries:\n *\n * <ol>\n *   <li>First commit attempt fails with a retryable, non-conflict exception (e.g., IOException)\n *   <li>Second commit attempt fails with a conflict exception (e.g., FileAlreadyExistsException)\n * </ol>\n *\n * <p>In this scenario, the system cannot determine whether the first attempt actually wrote the\n * commit file successfully but failed to report success, or whether the commit file was never\n * written.\n *\n * <p>Since the system cannot distinguish between these cases, it cannot safely determine whether to\n * retry at the current version or advance to version N+1.\n *\n * <p>Resolution: When this exception occurs, manual intervention is required to:\n *\n * <ul>\n *   <li>Examine the commit history to determine if the first attempt actually succeeded\n *   <li>If the commit succeeded, avoid retrying to prevent duplicate records\n *   <li>If the commit failed, retry the operation from the beginning\n * </ul>\n */\n@Evolving\npublic class CommitStateUnknownException extends RuntimeException {\n  public CommitStateUnknownException(\n      long commitVersion, int commitAttempt, CommitFailedException cfe) {\n    super(\n        String.format(\n            \"Commit attempt %d for version %d failed due to a concurrent write conflict after a \"\n                + \"previous retry. Since Kernel cannot determine if that previous attempt actually \"\n                + \"succeeded, retrying could create duplicate records. Please manually validate \"\n                + \"the commit history to resolve this conflict and retry the operation.\",\n            commitAttempt, commitVersion),\n        cfe);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/ConcurrentTransactionException.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.TransactionBuilder;\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\n\n/**\n * Thrown when concurrent transaction both attempt to update the table with same transaction\n * identifier set through {@link TransactionBuilder#withTransactionId(Engine, String, long)}\n * (String)}.\n *\n * <p>Incremental processing systems (e.g., streaming systems) that track progress using their own\n * application-specific versions need to record what progress has been made, in order to avoid\n * duplicating data in the face of failures and retries during writes. For more information refer to\n * the Delta protocol section <a\n * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#transaction-identifiers\">\n * Transaction Identifiers</a>\n *\n * @since 3.2.0\n */\n@Evolving\npublic class ConcurrentTransactionException extends ConcurrentWriteException {\n  private static final String message =\n      \"This error occurs when multiple updates are \"\n          + \"using the same transaction identifier to write into this table.\\n\"\n          + \"Application ID: %s, Attempted version: %s, Latest version in table: %s\";\n\n  public ConcurrentTransactionException(String appId, long txnVersion, long lastUpdated) {\n    super(String.format(message, appId, txnVersion, lastUpdated));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/ConcurrentWriteException.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when a concurrent transaction has written data after the current transaction has started.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class ConcurrentWriteException extends KernelException {\n  public ConcurrentWriteException() {\n    super(\n        \"Transaction has encountered a conflict and can not be committed. \"\n            + \"Query needs to be re-executed using the latest version of the table.\");\n  }\n\n  public ConcurrentWriteException(String message) {\n    super(message);\n  }\n\n  public ConcurrentWriteException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/DomainDoesNotExistException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/** Thrown when attempting to remove a domain metadata that does not exist in the read snapshot. */\n@Evolving\npublic class DomainDoesNotExistException extends KernelException {\n  public DomainDoesNotExistException(String tablePath, String domain, long snapshotVersion) {\n    super(\n        String.format(\n            \"%s: Cannot remove domain metadata with identifier %s because it does not exist in the \"\n                + \"read snapshot at version %s\",\n            tablePath, domain, snapshotVersion));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/InvalidConfigurationValueException.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when an illegal value is specified for a table property.\n *\n * @since 3.3.0\n */\n@Evolving\npublic class InvalidConfigurationValueException extends KernelException {\n  public InvalidConfigurationValueException(String key, String value, String helpMessage) {\n    super(\n        String.format(\"Invalid value for table property '%s': '%s'. %s\", key, value, helpMessage));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/InvalidTableException.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\n/**\n * Thrown when an invalid table is encountered; the table's log and/or checkpoint files are in an\n * invalid state.\n */\npublic class InvalidTableException extends KernelException {\n\n  private static final String message = \"Invalid table found at %s: %s\";\n\n  public InvalidTableException(String tablePath, String reason) {\n    super(String.format(message, tablePath, reason));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/KernelEngineException.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.engine.Engine;\n\n/** Throws when the {@link Engine} encountered an error while executing an operation. */\npublic class KernelEngineException extends RuntimeException {\n  private static final String msgTemplate =\n      \"Encountered an error from the underlying engine \" + \"implementation while trying to %s: %s\";\n\n  public KernelEngineException(String attemptedOperation, Throwable cause) {\n    super(String.format(msgTemplate, attemptedOperation, cause.getMessage()), cause);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/KernelException.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\n/**\n * Thrown when Kernel cannot execute the requested operation due to the operation being invalid or\n * unsupported.\n */\npublic class KernelException extends RuntimeException {\n\n  public KernelException() {\n    super();\n  }\n\n  public KernelException(String message) {\n    super(message);\n  }\n\n  public KernelException(Throwable cause) {\n    super(cause);\n  }\n\n  public KernelException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/MaxCommitRetryLimitReachedException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n@Evolving\npublic class MaxCommitRetryLimitReachedException extends KernelException {\n  public MaxCommitRetryLimitReachedException(long commitVersion, int maxRetries, Exception cause) {\n    super(\n        String.format(\n            \"Commit attempt for version %d failed with a retryable exception but will not be \"\n                + \"retried because the maximum number of retries (%d) has been reached.\",\n            commitVersion, maxRetries),\n        cause);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/MetadataChangedException.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when the metadata of the Delta table has changed between the time of transaction start and\n * the time of commit.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class MetadataChangedException extends ConcurrentWriteException {\n  public MetadataChangedException() {\n    super(\n        \"The metadata of the Delta table has been changed by a concurrent update. \"\n            + \"Please try the operation again.\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/ProtocolChangedException.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when the protocol of the Delta table has changed between the time of transaction start and\n * the time of commit.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class ProtocolChangedException extends ConcurrentWriteException {\n  private static final String helpfulMsgForNewTables =\n      \" This happens when multiple writers \"\n          + \"are writing to an empty directory. Creating the table ahead of time will avoid this \"\n          + \"conflict.\";\n\n  public ProtocolChangedException(long attemptVersion) {\n    super(\n        String.format(\n            \"Transaction has encountered a conflict and can not be committed. \"\n                + \"Query needs to be re-executed using the latest version of the table.%s\",\n            attemptVersion == 0 ? helpfulMsgForNewTables : \"\"));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/TableAlreadyExistsException.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Optional;\n\n/**\n * Thrown when trying to create a Delta table at a location where a Delta table already exists.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class TableAlreadyExistsException extends KernelException {\n  private final String tablePath;\n  private final Optional<String> context;\n\n  public TableAlreadyExistsException(String tablePath, String context) {\n    this.tablePath = tablePath;\n    this.context = Optional.ofNullable(context);\n  }\n\n  public TableAlreadyExistsException(String tablePath) {\n    this(tablePath, null);\n  }\n\n  @Override\n  public String getMessage() {\n    return String.format(\n        \"Delta table already exists at `%s`.%s\",\n        tablePath, context.map(c -> \" Context: \" + c).orElse(\"\"));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/TableNotFoundException.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when there is no Delta table at the given location.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class TableNotFoundException extends KernelException {\n\n  private final String tablePath;\n\n  public TableNotFoundException(String tablePath) {\n    this(tablePath, null);\n  }\n\n  public TableNotFoundException(String tablePath, String context) {\n    super(\n        String.format(\n            \"Delta table at path `%s` is not found.%s\",\n            tablePath, context == null ? \"\" : \" Context: \" + context));\n    this.tablePath = tablePath;\n  }\n\n  /** @return the provided path where no Delta table was found */\n  public String getTablePath() {\n    return tablePath;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/UnknownConfigurationException.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Thrown when an unknown configuration key is specified.\n *\n * @since 3.3.0\n */\n@Evolving\npublic class UnknownConfigurationException extends KernelException {\n  public UnknownConfigurationException(String confKey) {\n    super(String.format(\"Unknown configuration was specified: %s\", confKey));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/UnsupportedProtocolVersionException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Exception thrown when Kernel encounters unsupported protocol versions.\n *\n * @since 4.1.0\n */\n@Evolving\npublic class UnsupportedProtocolVersionException extends KernelException {\n\n  /** Enum representing the type of Delta protocol version. */\n  public enum ProtocolVersionType {\n    /** Reader protocol version */\n    READER,\n    /** Writer protocol version */\n    WRITER\n  }\n\n  private final String tablePath;\n  private final int version;\n  private final ProtocolVersionType versionType;\n\n  public UnsupportedProtocolVersionException(\n      String tablePath, int version, ProtocolVersionType versionType) {\n    super(\n        String.format(\n            \"Unsupported Delta protocol %s version: table `%s` requires %s version %s \"\n                + \"which is unsupported by this version of Delta Kernel.\",\n            versionType.name().toLowerCase(),\n            tablePath,\n            versionType.name().toLowerCase(),\n            version));\n    this.tablePath = tablePath;\n    this.version = version;\n    this.versionType = versionType;\n  }\n\n  /** @return the table path where the unsupported protocol was encountered */\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  /** @return the unsupported protocol version */\n  public int getVersion() {\n    return version;\n  }\n\n  /** @return the type of protocol version (READER or WRITER) */\n  public ProtocolVersionType getVersionType() {\n    return versionType;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/exceptions/UnsupportedTableFeatureException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Collections;\nimport java.util.Set;\n\n/**\n * Base exception thrown when Kernel encounters unsupported table features.\n *\n * @since 4.1.0\n */\n@Evolving\npublic class UnsupportedTableFeatureException extends KernelException {\n\n  private final String tablePath;\n  private final Set<String> unsupportedFeatures;\n\n  public UnsupportedTableFeatureException(\n      String tablePath, Set<String> unsupportedFeatures, String message) {\n    super(message);\n    this.tablePath = tablePath;\n    this.unsupportedFeatures =\n        unsupportedFeatures != null\n            ? Collections.unmodifiableSet(unsupportedFeatures)\n            : Collections.emptySet();\n  }\n\n  public UnsupportedTableFeatureException(\n      String tablePath, String unsupportedFeature, String message) {\n    this(tablePath, Collections.singleton(unsupportedFeature), message);\n  }\n\n  /**\n   * @return the table path where the unsupported features were encountered, or null if not\n   *     applicable\n   */\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  /** @return an unmodifiable set of unsupported feature names */\n  public Set<String> getUnsupportedFeatures() {\n    return unsupportedFeatures;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/AlwaysFalse.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Collections;\n\n/**\n * Predicate which always evaluates to {@code false}.\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class AlwaysFalse extends Predicate {\n  public static final AlwaysFalse ALWAYS_FALSE = new AlwaysFalse();\n\n  private AlwaysFalse() {\n    super(\"ALWAYS_FALSE\", Collections.emptyList());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/AlwaysTrue.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Collections;\n\n/**\n * Predicate which always evaluates to {@code true}.\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class AlwaysTrue extends Predicate {\n  public static final AlwaysTrue ALWAYS_TRUE = new AlwaysTrue();\n\n  private AlwaysTrue() {\n    super(\"ALWAYS_TRUE\", Collections.emptyList());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/And.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Arrays;\n\n/**\n * {@code AND} expression\n *\n * <p>Definition:\n *\n * <p>\n *\n * <ul>\n *   <li>Logical {@code expr1} AND {@code expr2} on two inputs.\n *   <li>Requires both left and right input expressions of type {@link Predicate}.\n *   <li>Result is null when both inputs are null, or when one input is null and the other is {@code\n *       true}.\n * </ul>\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class And extends Predicate {\n  public And(Predicate left, Predicate right) {\n    super(\"AND\", Arrays.asList(left, right));\n  }\n\n  /** @return Left side operand. */\n  public Predicate getLeft() {\n    return (Predicate) getChildren().get(0);\n  }\n\n  /** @return Right side operand. */\n  public Predicate getRight() {\n    return (Predicate) getChildren().get(1);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Column.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport static java.lang.String.format;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * An expression type that refers to a column (case-sensitive) in the input. The column name is\n * either a single name or array of names (when referring to a nested column).\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class Column implements Expression {\n  private final String[] names;\n\n  /** Create a column expression for referring to a column. */\n  public Column(String name) {\n    this.names = new String[] {name};\n  }\n\n  /** Create a column expression to refer to a nested column. */\n  public Column(String[] names) {\n    this.names = names;\n  }\n\n  /**\n   * @return the column names. Each part in the name correspond to one level of nested reference.\n   */\n  public String[] getNames() {\n    return names;\n  }\n\n  @Override\n  public List<Expression> getChildren() {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public String toString() {\n\n    return \"column(\" + quoteColumnPath(names) + \")\";\n  }\n\n  /**\n   * Returns a new column that appends the input column name to the current column. Corresponds to\n   * an additional level of nested reference.\n   *\n   * @param name the column name to append\n   * @return the new column\n   */\n  public Column appendNestedField(String name) {\n    String[] newNames = new String[names.length + 1];\n    System.arraycopy(names, 0, newNames, 0, names.length);\n    newNames[names.length] = name;\n    return new Column(newNames);\n  }\n\n  private static String quoteColumnPath(String[] names) {\n    return Arrays.stream(names)\n        .map(s -> format(\"`%s`\", s.replace(\"`\", \"``\")))\n        .collect(Collectors.joining(\".\"));\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    Column other = (Column) o;\n    return Arrays.equals(names, other.getNames());\n  }\n\n  @Override\n  public int hashCode() {\n    return Arrays.hashCode(names);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Expression.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.List;\n\n/**\n * Base interface for all Kernel expressions.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface Expression {\n  /** @return a list of expressions that are input to this expression. */\n  List<Expression> getChildren();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/ExpressionEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\n\n/**\n * Interface for implementing an {@link Expression} evaluator. It contains one {@link Expression}\n * which can be evaluated on multiple {@link ColumnarBatch}es Connectors can implement this\n * interface to optimize the evaluation using the connector specific capabilities.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface ExpressionEvaluator extends AutoCloseable {\n  /**\n   * Evaluate the expression on given {@link ColumnarBatch} data.\n   *\n   * @param input input data in columnar format.\n   * @return Result of the expression as a {@link ColumnVector}. Contains one value for each row of\n   *     the input. The data type of the output is same as the type output of the expression this\n   *     evaluator is using.\n   */\n  ColumnVector eval(ColumnarBatch input);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/In.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.types.CollationIdentifier;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * {@code IN} expression\n *\n * <p>Definition:\n *\n * <ul>\n *   <li>SQL semantic: {@code expr IN (expr1, expr2, ...) [COLLATE collationIdentifier]}\n *   <li>Requires the value expression to be evaluated against a list of literal expressions.\n *   <li>Result is true if the value matches any element in the list, false if no matches and no\n *       nulls, null if the value is null or any comparison results in null.\n *   <li>Supports collation for string comparisons.\n *   <li>Only supports primitive types . Nested types are not supported.\n * </ul>\n *\n * @since 4.0.0\n */\n@Evolving\npublic final class In extends Predicate {\n\n  /**\n   * Creates an IN predicate expression.\n   *\n   * @param valueExpression The expression to evaluate (left side of IN)\n   * @param inListElements The list of literal expressions to check against\n   */\n  public In(Expression valueExpression, List<Expression> inListElements) {\n    super(\"IN\", buildChildren(valueExpression, inListElements));\n  }\n\n  /**\n   * Creates an IN predicate expression with collation support.\n   *\n   * @param valueExpression The expression to evaluate (left side of IN)\n   * @param inListElements The list of literal expressions to check against\n   * @param collationIdentifier The collation identifier for string comparisons\n   */\n  public In(\n      Expression valueExpression,\n      List<Expression> inListElements,\n      CollationIdentifier collationIdentifier) {\n    super(\"IN\", buildChildren(valueExpression, inListElements), collationIdentifier);\n  }\n\n  /** @return The value expression to be evaluated (left side of IN). */\n  public Expression getValueExpression() {\n    return getChildren().get(0);\n  }\n\n  /** @return The list of expressions to check against (right side of IN). */\n  public List<Expression> getInListElements() {\n    return Collections.unmodifiableList(\n        new ArrayList<>(getChildren().subList(1, getChildren().size())));\n  }\n\n  @Override\n  public String toString() {\n    String collationSuffix = getCollationIdentifier().map(c -> \" COLLATE \" + c).orElse(\"\");\n    String inValues =\n        getInListElements().stream().map(Object::toString).collect(Collectors.joining(\", \"));\n    return String.format(\"(%s IN (%s)%s)\", getValueExpression(), inValues, collationSuffix);\n  }\n\n  private static List<Expression> buildChildren(\n      Expression valueExpression, List<Expression> inListElements) {\n    List<Expression> children = new ArrayList<>();\n    children.add(valueExpression);\n    children.addAll(inListElements);\n    return children;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Literal.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\n/**\n * A literal value.\n *\n * <p>Definition:\n *\n * <ul>\n *   <li>Represents literal of primitive types as defined in the protocol <a\n *       href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#primitive-types\">Delta\n *       Transaction Log Protocol: Primitive Types</a>\n *   <li>Use {@link #getValue()} to fetch the literal value. Returned value type depends on the type\n *       of the literal data type. See the {@link #getValue()} for further details.\n * </ul>\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class Literal implements Expression {\n  /**\n   * Create a {@code boolean} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link BooleanType}\n   */\n  public static Literal ofBoolean(boolean value) {\n    return new Literal(value, BooleanType.BOOLEAN);\n  }\n\n  /**\n   * Create a {@code byte} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link ByteType}\n   */\n  public static Literal ofByte(byte value) {\n    return new Literal(value, ByteType.BYTE);\n  }\n\n  /**\n   * Create a {@code short} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link ShortType}\n   */\n  public static Literal ofShort(short value) {\n    return new Literal(value, ShortType.SHORT);\n  }\n\n  /**\n   * Create a {@code integer} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link IntegerType}\n   */\n  public static Literal ofInt(int value) {\n    return new Literal(value, IntegerType.INTEGER);\n  }\n\n  /**\n   * Create a {@code long} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link LongType}\n   */\n  public static Literal ofLong(long value) {\n    return new Literal(value, LongType.LONG);\n  }\n\n  /**\n   * Create a {@code float} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link FloatType}\n   */\n  public static Literal ofFloat(float value) {\n    return new Literal(value, FloatType.FLOAT);\n  }\n\n  /**\n   * Create a {@code double} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link DoubleType}\n   */\n  public static Literal ofDouble(double value) {\n    return new Literal(value, DoubleType.DOUBLE);\n  }\n\n  /**\n   * Create a {@code string} type literal expression.\n   *\n   * @param value literal value\n   * @return a {@link Literal} of type {@link StringType}\n   */\n  public static Literal ofString(String value) {\n    return new Literal(value, StringType.STRING);\n  }\n\n  /**\n   * Create a {@code string} type literal expression with collated {@link StringType}.\n   *\n   * @param value literal value\n   * @param collationIdentifier collation identifier for the string literal\n   * @return a {@link Literal} of type {@link StringType} with the given collation\n   */\n  public static Literal ofString(String value, CollationIdentifier collationIdentifier) {\n    return new Literal(value, new StringType(collationIdentifier));\n  }\n\n  /**\n   * Create a {@code binary} type literal expression.\n   *\n   * @param value binary literal value as an array of bytes\n   * @return a {@link Literal} of type {@link BinaryType}\n   */\n  public static Literal ofBinary(byte[] value) {\n    return new Literal(value, BinaryType.BINARY);\n  }\n\n  /**\n   * Create a {@code date} type literal expression.\n   *\n   * @param daysSinceEpochUTC number of days since the epoch in UTC timezone.\n   * @return a {@link Literal} of type {@link DateType}\n   */\n  public static Literal ofDate(int daysSinceEpochUTC) {\n    return new Literal(daysSinceEpochUTC, DateType.DATE);\n  }\n\n  /**\n   * Create a {@code timestamp} type literal expression.\n   *\n   * @param microsSinceEpochUTC microseconds since epoch time in UTC timezone.\n   * @return a {@link Literal} with data type {@link TimestampType}\n   */\n  public static Literal ofTimestamp(long microsSinceEpochUTC) {\n    return new Literal(microsSinceEpochUTC, TimestampType.TIMESTAMP);\n  }\n\n  /**\n   * Create a {@code timestamp_ntz} type literal expression.\n   *\n   * @param microSecondsEpoch Microseconds since epoch with no timezone.\n   * @return a {@link Literal} with data type {@link TimestampNTZType}\n   */\n  public static Literal ofTimestampNtz(long microSecondsEpoch) {\n    return new Literal(microSecondsEpoch, TimestampNTZType.TIMESTAMP_NTZ);\n  }\n\n  /**\n   * Create a {@code decimal} type literal expression.\n   *\n   * @param value decimal literal value\n   * @param precision precision of the decimal literal\n   * @param scale scale of the decimal literal\n   * @return a {@link Literal} with data type {@link DecimalType} with given {@code precision} and\n   *     {@code scale}.\n   */\n  public static Literal ofDecimal(BigDecimal value, int precision, int scale) {\n    // throws an error if rounding is required to set the specified scale\n    BigDecimal valueToStore = value.setScale(scale);\n    checkArgument(\n        valueToStore.precision() <= precision,\n        \"Decimal precision=%s for decimal %s exceeds max precision %s\",\n        valueToStore.precision(),\n        valueToStore,\n        precision);\n    return new Literal(valueToStore, new DecimalType(precision, scale));\n  }\n\n  /**\n   * Create {@code null} value literal.\n   *\n   * @param dataType {@link DataType} of the null literal.\n   * @return a null {@link Literal} with the given data type\n   */\n  public static Literal ofNull(DataType dataType) {\n    return new Literal(null, dataType);\n  }\n\n  private final Object value;\n  private final DataType dataType;\n\n  private Literal(Object value, DataType dataType) {\n    if (dataType instanceof ArrayType\n        || dataType instanceof MapType\n        || dataType instanceof StructType) {\n      throw new IllegalArgumentException(dataType + \" is an invalid data type for Literal.\");\n    }\n    this.value = value;\n    this.dataType = dataType;\n  }\n\n  /**\n   * Get the literal value. If the value is null a {@code null} is returned. For non-null literal\n   * the returned value is one of the following types based on the literal data type.\n   *\n   * <ul>\n   *   <li>BOOLEAN: {@link Boolean}\n   *   <li>BYTE: {@link Byte}\n   *   <li>SHORT: {@link Short}\n   *   <li>INTEGER: {@link Integer}\n   *   <li>LONG: {@link Long}\n   *   <li>FLOAT: {@link Float}\n   *   <li>DOUBLE: {@link Double}\n   *   <li>DATE: {@link Integer} represents the number of days since epoch in UTC\n   *   <li>TIMESTAMP: {@link Long} represents the microseconds since epoch in UTC\n   *   <li>TIMESTAMP_NTZ: {@link Long} represents the microseconds since epoch with no timezone\n   *   <li>DECIMAL: {@link BigDecimal}.Use {@link #getDataType()} to find the precision and scale\n   * </ul>\n   *\n   * @return Literal value.\n   */\n  public Object getValue() {\n    return value;\n  }\n\n  /**\n   * Get the datatype of the literal object. Datatype lets the caller interpret the value of the\n   * literal object returned by {@link #getValue()}\n   *\n   * @return Datatype of the literal object.\n   */\n  public DataType getDataType() {\n    return dataType;\n  }\n\n  @Override\n  public String toString() {\n    return String.valueOf(value);\n  }\n\n  @Override\n  public List<Expression> getChildren() {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    Literal other = (Literal) o;\n    return Objects.equals(dataType, other.dataType) && Objects.equals(value, other.value);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Or.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Arrays;\n\n/**\n * {@code OR} expression\n *\n * <p>Definition:\n *\n * <p>\n *\n * <ul>\n *   <li>Logical {@code expr1} OR {@code expr2} on two inputs.\n *   <li>Requires both left and right input expressions of type {@link Predicate}.\n *   <li>Result is null when both inputs are null, or when one input is null and the other is {@code\n *       false}.\n * </ul>\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class Or extends Predicate {\n  public Or(Predicate left, Predicate right) {\n    super(\"OR\", Arrays.asList(left, right));\n  }\n\n  /** @return Left side operand. */\n  public Predicate getLeft() {\n    return (Predicate) getChildren().get(0);\n  }\n\n  /** @return Right side operand. */\n  public Predicate getRight() {\n    return (Predicate) getChildren().get(1);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/PartitionValueExpression.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport static java.lang.String.format;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.types.DataType;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Expression to decode the serialized partition value into partition type value according the <a\n * href=https://github.com/delta-io/delta/blob/master/PROTOCOL.md#partition-value-serialization>\n * Delta Protocol spec</a>. Currently all valid partition types are supported except the `timestamp`\n * and `timestamp without timezone` types.\n *\n * <p>\n *\n * <ul>\n *   <li>Name: <code>partition_value</code>\n *   <li>Semantic: <code>partition_value(string, datatype)</code>. Decode the partition value of\n *       type <i>datatype</i> from the serialized string format.\n * </ul>\n *\n * @since 3.0.0\n */\n@Evolving\npublic class PartitionValueExpression implements Expression {\n  private final DataType partitionValueType;\n  private final Expression serializedPartitionValue;\n\n  /**\n   * Create {@code partition_value} expression.\n   *\n   * @param serializedPartitionValue Input expression providing the partition values in serialized\n   *     format.\n   * @param partitionDataType Partition data type to which string partition value is deserialized as\n   *     according to the Delta Protocol.\n   */\n  public PartitionValueExpression(Expression serializedPartitionValue, DataType partitionDataType) {\n    this.serializedPartitionValue = requireNonNull(serializedPartitionValue);\n    this.partitionValueType = requireNonNull(partitionDataType);\n  }\n\n  /** Get the expression reference to the serialized partition value. */\n  public Expression getInput() {\n    return serializedPartitionValue;\n  }\n\n  /** Get the data type of the partition value. */\n  public DataType getDataType() {\n    return partitionValueType;\n  }\n\n  @Override\n  public List<Expression> getChildren() {\n    return Collections.singletonList(serializedPartitionValue);\n  }\n\n  @Override\n  public String toString() {\n    return format(\"partition_value(%s, %s)\", serializedPartitionValue, partitionValueType);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Predicate.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.ExpressionHandler;\nimport io.delta.kernel.types.CollationIdentifier;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * Defines predicate scalar expression which is an extension of {@link ScalarExpression} that\n * evaluates to true, false, or null for each input row.\n *\n * <p>Currently, implementations of {@link ExpressionHandler} requires support for at least the\n * following scalar expressions.\n *\n * <ol>\n *   <li>Name: <code>=</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 = expr2 [COLLATE collationIdentifier]</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>&lt;</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 &lt; expr2 [COLLATE collationIdentifier]</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>&lt;=</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 &lt;= expr2 [COLLATE collationIdentifier]</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>&gt;</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 &gt; expr2 [COLLATE collationIdentifier]</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>&gt;=</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 &gt;= expr2 [COLLATE collationIdentifier]</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>ALWAYS_TRUE</code>\n *       <ul>\n *         <li>SQL semantic: <code>Constant expression whose value is `true`</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>ALWAYS_FALSE</code>\n *       <ul>\n *         <li>SQL semantic: <code>Constant expression whose value is `false`</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>AND</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 AND expr2</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>OR</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 OR expr2</code>\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>NOT</code>\n *       <ul>\n *         <li>SQL semantic: <code>NOT expr</code>\n *         <li>Since version: 3.1.0\n *       </ul>\n *   <li>Name: <code>IS_NOT_NULL</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr IS NOT NULL</code>\n *         <li>Since version: 3.1.0\n *       </ul>\n *   <li>Name: <code>IS_NULL</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr IS NULL</code>\n *         <li>Since version: 3.2.0\n *       </ul>\n *   <li>Name: <code>LIKE</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr LIKE expr</code>\n *         <li>Since version: 3.3.0\n *       </ul>\n *   <li>Name: <code>IS NOT DISTINCT FROM</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr1 IS NOT DISTINCT FROM expr2 [COLLATE collationIdentifier]\n *             </code>\n *         <li>Since version: 3.3.0\n *       </ul>\n *   <li>Name: <code>STARTS_WITH</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr STARTS_WITH expr [COLLATE collationIdentifier]</code>\n *         <li>Since version: 3.4.0\n *       </ul>\n *   <li>Name: <code>IN</code>\n *       <ul>\n *         <li>SQL semantic: <code>expr IN (expr1, expr2, ...) [COLLATE collationIdentifier]</code>\n *         <li>Since version: 4.0.0\n *       </ul>\n * </ol>\n *\n * @since 3.0.0\n */\n@Evolving\npublic class Predicate extends ScalarExpression {\n  /** Optional collation to be used for string comparison in this predicate. */\n  private Optional<CollationIdentifier> collationIdentifier;\n\n  public Predicate(String name, List<Expression> children) {\n    super(name, children);\n    collationIdentifier = Optional.empty();\n  }\n\n  /** Constructor for a unary Predicate expression */\n  public Predicate(String name, Expression child) {\n    this(name, Arrays.asList(child));\n  }\n\n  /** Constructor for a binary Predicate expression */\n  public Predicate(String name, Expression left, Expression right) {\n    this(name, Arrays.asList(left, right));\n  }\n\n  /** Constructor for a Predicate expression with collation support. */\n  public Predicate(\n      String name, Expression left, Expression right, CollationIdentifier collationIdentifier) {\n    this(name, Arrays.asList(left, right), collationIdentifier);\n  }\n\n  /** Constructor for a Predicate expression with collation support. */\n  public Predicate(\n      String name, List<Expression> children, CollationIdentifier collationIdentifier) {\n    this(name, children);\n    checkArgument(\n        COLLATION_SUPPORTED_OPERATORS.contains(this.name),\n        \"Collation is not supported for operator %s. Supported operators are %s\",\n        this.name,\n        COLLATION_SUPPORTED_OPERATORS);\n\n    // For operators with multiple children, we need at least 2 children\n    // For other collated expressions, we require exactly 2 children\n    if (OPERATORS_WITH_MULTIPLE_CHILDREN.contains(this.name)) {\n      checkArgument(\n          this.children.size() >= 2,\n          \"Invalid Predicate: collated predicate '%s' with multiple children requires at least 2 \"\n              + \"children, but found %d.\",\n          this.name,\n          this.children.size());\n    } else {\n      checkArgument(\n          this.children.size() == 2,\n          \"Invalid Predicate: collated predicate '%s' requires exactly 2 children, but found %d.\",\n          this.name,\n          this.children.size());\n    }\n    this.collationIdentifier = Optional.of(collationIdentifier);\n  }\n\n  /** Returns the collation identifier used for this predicate, if specified. */\n  public Optional<CollationIdentifier> getCollationIdentifier() {\n    return collationIdentifier;\n  }\n\n  /**\n   * Returns string representation of the predicate.\n   *\n   * <p>Format for binary operators: {@code (left OP right)} or {@code (left OP right COLLATE\n   * collation)}\n   *\n   * <p>Examples:\n   *\n   * <ul>\n   *   <li>{@code (col = 5)}\n   *   <li>{@code (name = 'John' COLLATE SPARK.UTF8_BINARY)}\n   * </ul>\n   *\n   * <p>Note: Specialized operators like IN override this method to provide their own string\n   * representation.\n   */\n  @Override\n  public String toString() {\n    String collationSuffix = collationIdentifier.map(c -> \" COLLATE \" + c).orElse(\"\");\n    if (BINARY_OPERATORS.contains(name) || collationIdentifier.isPresent()) {\n      return String.format(\"(%s %s %s%s)\", children.get(0), name, children.get(1), collationSuffix);\n    }\n    return super.toString();\n  }\n\n  @Override\n  public int hashCode() {\n    return toString().hashCode();\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (!(o instanceof Predicate)) return false;\n    return this.hashCode() == o.hashCode();\n  }\n\n  private static final Set<String> BINARY_OPERATORS =\n      Stream.of(\"<\", \"<=\", \">\", \">=\", \"=\", \"AND\", \"OR\", \"IS NOT DISTINCT FROM\", \"STARTS_WITH\")\n          .collect(Collectors.toSet());\n\n  /** Operators that can have multiple children (more than 2). */\n  private static final Set<String> OPERATORS_WITH_MULTIPLE_CHILDREN = Collections.singleton(\"IN\");\n\n  /** Operators that support collation-based string comparison. */\n  private static final Set<String> COLLATION_SUPPORTED_OPERATORS =\n      Stream.of(\"<\", \"<=\", \">\", \">=\", \"=\", \"IS NOT DISTINCT FROM\", \"STARTS_WITH\", \"IN\")\n          .collect(Collectors.toSet());\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/PredicateEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport java.util.Optional;\n\n/**\n * Special interface for evaluating {@link Predicate} on input batch and return a selection vector\n * containing one value for each row in input batch indicating whether the row has passed the\n * predicate or not.\n *\n * <p>Optionally it takes an existing selection vector along with the input batch for evaluation.\n * Result selection vector is combined with the given existing selection vector and a new selection\n * vector is returned. This mechanism allows running an input batch through several predicate\n * evaluations without rewriting the input batch to remove rows that do not pass the predicate after\n * each predicate evaluation. The new selection should be same or more selective as the existing\n * selection vector. For example if a row is marked as unselected in existing selection vector, then\n * it should remain unselected in the returned selection vector even when the given predicate\n * returns true for the row.\n *\n * @since 3.0.0\n */\n@Evolving\npublic interface PredicateEvaluator {\n  /**\n   * Evaluate the predicate on given inputData. Combine the existing selection vector with the\n   * output of the predicate result and return a new selection vector.\n   *\n   * @param inputData {@link ColumnarBatch} of data to which the predicate expression refers for\n   *     input.\n   * @param existingSelectionVector Optional existing selection vector. If not empty, it is combined\n   *     with the predicate result. The caller is also releasing the ownership of\n   *     `existingSelectionVector` to this callee, and the callee is responsible for closing it.\n   * @return A {@link ColumnVector} of boolean type that captures the predicate result for each row\n   *     together with the existing selection vector.\n   */\n  ColumnVector eval(ColumnarBatch inputData, Optional<ColumnVector> existingSelectionVector);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/ScalarExpression.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.stream.Collectors;\n\n/**\n * Scalar SQL expressions which take zero or more inputs and for each input row generate one output\n * value. A subclass of these expressions are of type {@link Predicate} whose result type is\n * `boolean`. See {@link Predicate} for predicate type scalar expressions. Supported non-predicate\n * type scalar expressions are listed below.\n *\n * <ol>\n *   <li>Name: <code>ELEMENT_AT</code>\n *       <ul>\n *         <li>Semantic: <code>ELEMENT_AT(map, key)</code>. Return the value of given <i>key</i>\n *             from the <i>map</i> type input. Returns <i>null</i> if the given <i>key</i> is not in\n *             the <i>map</i> Ex: `ELEMENT_AT(map(1, 'a', 2, 'b'), 2)` returns 'b'.\n *         <li>Since version: 3.0.0\n *       </ul>\n *   <li>Name: <code>COALESCE</code>\n *       <ul>\n *         <li>Semantic: <code>COALESCE(expr1, ..., exprN)</code>. Return the first non-null\n *             argument. If all arguments are null, returns null.\n *         <li>Since version: 3.1.0\n *       </ul>\n *   <li>Name: <code>ADD</code>\n *       <ul>\n *         <li>Semantic: <code>ADD(expr1, expr2)</code>. Return the sum of two numeric expressions.\n *             If either of the expressions is null, returns null.\n *         <li>Since Version: 4.1.0\n *       </ul>\n *   <li>Name: <code>TIMEADD</code>\n *       <ul>\n *         <li>Semantic: <code>TIMEADD(colExpr, milliseconds)</code>. Add the specified number of\n *             milliseconds to the timestamp represented by <i>colExpr</i>. The adjustment does not\n *             alter the original value but returns a new timestamp increased by the given\n *             milliseconds. Ex: `TIMEADD(timestampColumn, 1000)` returns a timestamp 1 second\n *             later.\n *         <li>Since version: 3.3.0\n *       </ul>\n *   <li>Name: <code>SUBSTRING</code>\n *       <ul>\n *         <li>Semantic: <code>SUBSTRING(colExpr, pos, len)</code>. Returns the slice of byte array\n *             or string, that starts at pos and has the length len.\n *             <ul>\n *               <li>pos is 1 based. If pos is negative the start is determined by counting\n *                   characters (or bytes for BINARY) from the end.\n *               <li>If len is less than 1 the result is empty.\n *               <li>If len is omitted the function returns on characters or bytes starting with\n *                   pos.\n *             </ul>\n *         <li>Since version: 3.4.0\n *       </ul>\n * </ol>\n *\n * @since 3.0.0\n */\n@Evolving\npublic class ScalarExpression implements Expression {\n  protected final String name;\n  protected final List<Expression> children;\n\n  public ScalarExpression(String name, List<Expression> children) {\n    this.name = requireNonNull(name, \"name is null\").toUpperCase(Locale.ENGLISH);\n    this.children = Collections.unmodifiableList(new ArrayList<>(children));\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"%s(%s)\", name, children.stream().map(Object::toString).collect(Collectors.joining(\", \")));\n  }\n\n  public String getName() {\n    return name;\n  }\n\n  @Override\n  public List<Expression> getChildren() {\n    return children;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/expressions/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Expressions framework that defines the most common expressions which the connectors can use to\n * pass predicates to Delta Kernel.\n */\npackage io.delta.kernel.expressions;\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/hook/PostCommitHook.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.hook;\n\nimport io.delta.kernel.engine.Engine;\nimport java.io.IOException;\n\n/**\n * A hook for executing operation after a transaction commit. Hooks are added in the Transaction and\n * engine need to invoke the hook explicitly for executing the operation. Supported operations are\n * listed in {@link PostCommitHookType}.\n */\npublic interface PostCommitHook {\n\n  enum PostCommitHookType {\n    /**\n     * Writes a new checkpoint at the version committed by the transaction. This hook is present\n     * when the table is ready for checkpoint according to its configured checkpoint interval. To\n     * perform this operation, reading previous checkpoint + logs is required to construct a new\n     * checkpoint, with latency scaling based on log size (typically seconds to minutes).\n     */\n    CHECKPOINT,\n    /**\n     * Writes a checksum file at the version committed by the transaction. This hook is present when\n     * all required table statistics (e.g. table size) for checksum file are known when a\n     * transaction commits. This operation has a minimal latency with no requirement of reading\n     * previous checkpoint or logs.\n     */\n    CHECKSUM_SIMPLE,\n\n    /**\n     * Writes a checksum file at the version committed by the transaction. This hook is present when\n     * CHECKSUM_SIMPLE is missing. It requires constructing table stats via full log replay, which\n     * can be expensive for large tables, with latency scaling based on log size. Unlike\n     * CHECKSUM_SIMPLE, this always performs a full table state construction rather than\n     * incrementally computing from a previous CRC.\n     */\n    CHECKSUM_FULL,\n\n    /**\n     * Writes a log compaction file that merges a range of commit JSON files into a single file.\n     * This hook is triggered on a configurable interval (e.g., every 10 commits) and reduces the\n     * number of small log files that need to be read when reconstructing the table state, thereby\n     * improving read performance.\n     */\n    LOG_COMPACTION\n  }\n\n  /** Invokes the post commit operation whose implementation must be thread safe. */\n  void threadSafeInvoke(Engine engine) throws IOException;\n\n  PostCommitHookType getType();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/CommitActionsImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.replay.ActionWrapper;\nimport io.delta.kernel.internal.replay.ActionsIterator;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Preconditions;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * Implementation of {@link CommitActions}.\n *\n * <p>This implementation owns the commit file and supports multiple calls to {@link #getActions()}.\n * The first call reuses initially-read data to avoid double I/O, while subsequent calls re-read the\n * commit file for memory efficiency.\n *\n * <p><b>Resource Management:</b>\n *\n * <ul>\n *   <li>Calling {@link #getActions()} transfers resource ownership to the returned iterator.\n *   <li>Callers MUST close the returned iterator (preferably using try-with-resources) to release\n *       file handles and other resources.\n *   <li>If {@link #getActions()} is never called, callers should explicitly call {@link #close()}\n *       to release internal resources. Otherwise, resources will be released when the object is\n *       garbage collected.\n * </ul>\n */\npublic class CommitActionsImpl implements CommitActions, AutoCloseable {\n\n  private final Engine engine;\n  private final FileStatus commitFile;\n  private final StructType readSchema;\n  private final String tablePath;\n  private final boolean shouldDropProtocolColumn;\n  private final boolean shouldDropCommitInfoColumn;\n  private final long version;\n  private final long timestamp;\n\n  /**\n   * Iterator over ActionWrappers. The first call to {@link #getActions()} uses this iterator which\n   * was created during construction (to extract metadata). Subsequent calls lazily create new\n   * iterators, by constructing an ActionsIterator which does not open the file.\n   */\n  private CloseableIterator<ActionWrapper> iterator;\n\n  /**\n   * Creates a CommitActions from a commit file.\n   *\n   * @param engine the engine for file I/O\n   * @param commitFile the commit file to read\n   * @param tablePath the table path for error messages\n   * @param actionSet the set of actions to read from the commit file\n   */\n  public CommitActionsImpl(\n      Engine engine,\n      FileStatus commitFile,\n      String tablePath,\n      Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n    requireNonNull(engine, \"engine cannot be null\");\n    this.commitFile = requireNonNull(commitFile, \"commitFile cannot be null\");\n    this.tablePath = requireNonNull(tablePath, \"tablePath cannot be null\");\n\n    // Create a new action set which is a super set of the requested actions.\n    // The extra actions are needed either for checks or to extract\n    // extra information. We will strip out the extra actions before\n    // returning the result.\n    Set<DeltaLogActionUtils.DeltaAction> copySet = new HashSet<>(actionSet);\n    copySet.add(DeltaLogActionUtils.DeltaAction.PROTOCOL);\n    // commitInfo is needed to extract the inCommitTimestamp of delta files, this is used in\n    // ActionsIterator to resolve the timestamp when available\n    copySet.add(DeltaLogActionUtils.DeltaAction.COMMITINFO);\n    // Determine whether the additional actions were in the original set.\n    this.shouldDropProtocolColumn = !actionSet.contains(DeltaLogActionUtils.DeltaAction.PROTOCOL);\n    this.shouldDropCommitInfoColumn =\n        !actionSet.contains(DeltaLogActionUtils.DeltaAction.COMMITINFO);\n\n    this.readSchema =\n        new StructType(\n            copySet.stream()\n                .map(action -> new StructField(action.colName, action.schema, true))\n                .collect(Collectors.toList()));\n    this.engine = engine;\n\n    // Create initial iterator and peek at the first element to extract metadata\n    CloseableIterator<ActionWrapper> actionsIter =\n        new ActionsIterator(\n            engine, Collections.singletonList(commitFile), readSchema, Optional.empty());\n\n    Tuple2<Optional<ActionWrapper>, CloseableIterator<ActionWrapper>> headAndIter =\n        peekHeadAndGetFullIterator(actionsIter);\n    this.iterator = headAndIter._2;\n\n    // Extract version and timestamp from first action (or use reading file if not exists)\n    if (headAndIter._1.isPresent()) {\n      ActionWrapper firstWrapper = headAndIter._1.get();\n      this.version = firstWrapper.getVersion();\n      this.timestamp =\n          firstWrapper\n              .getTimestamp()\n              .orElseThrow(\n                  () -> new RuntimeException(\"timestamp should always exist for Delta File\"));\n    } else {\n      // Empty commit file - extract from file metadata\n      this.version = FileNames.deltaVersion(new Path(commitFile.getPath()));\n      this.timestamp = commitFile.getModificationTime();\n    }\n  }\n\n  /**\n   * Helper to peek at the first element and return both the head and a full iterator (head + rest).\n   *\n   * @return Tuple2 where _1 is the head element (Optional) and _2 is the full iterator\n   */\n  private static Tuple2<Optional<ActionWrapper>, CloseableIterator<ActionWrapper>>\n      peekHeadAndGetFullIterator(CloseableIterator<ActionWrapper> iter) {\n    Optional<ActionWrapper> head = iter.hasNext() ? Optional.of(iter.next()) : Optional.empty();\n    CloseableIterator<ActionWrapper> fullIterator =\n        head.isPresent() ? Utils.singletonCloseableIterator(head.get()).combine(iter) : iter;\n    return new Tuple2<>(head, fullIterator);\n  }\n\n  @Override\n  public long getVersion() {\n    return version;\n  }\n\n  @Override\n  public long getTimestamp() {\n    return timestamp;\n  }\n\n  @Override\n  public synchronized CloseableIterator<ColumnarBatch> getActions() {\n    CloseableIterator<ColumnarBatch> result =\n        iterator.map(\n            wrapper ->\n                validateProtocolAndDropInternalColumns(\n                    wrapper.getColumnarBatch(),\n                    tablePath,\n                    shouldDropProtocolColumn,\n                    shouldDropCommitInfoColumn));\n    // Constructing an ActionsIterator does not open the file.\n    iterator =\n        new ActionsIterator(\n            engine, Collections.singletonList(commitFile), readSchema, Optional.empty());\n\n    return result;\n  }\n\n  /** Validates protocol and drops protocol/commitInfo columns if not requested. */\n  private static ColumnarBatch validateProtocolAndDropInternalColumns(\n      ColumnarBatch batch,\n      String tablePath,\n      boolean shouldDropProtocolColumn,\n      boolean shouldDropCommitInfoColumn) {\n\n    // Validate protocol if present in the batch.\n    int protocolIdx = batch.getSchema().indexOf(\"protocol\");\n    Preconditions.checkState(protocolIdx >= 0, \"protocol column must be present in readSchema\");\n    ColumnVector protocolVector = batch.getColumnVector(protocolIdx);\n    for (int rowId = 0; rowId < protocolVector.getSize(); rowId++) {\n      if (!protocolVector.isNullAt(rowId)) {\n        Protocol protocol = Protocol.fromColumnVector(protocolVector, rowId);\n        TableFeatures.validateKernelCanReadTheTable(protocol, tablePath);\n      }\n    }\n\n    // Drop columns if not requested\n    ColumnarBatch result = batch;\n    if (shouldDropProtocolColumn && protocolIdx >= 0) {\n      result = result.withDeletedColumnAt(protocolIdx);\n    }\n\n    int commitInfoIdx = result.getSchema().indexOf(\"commitInfo\");\n    if (shouldDropCommitInfoColumn && commitInfoIdx >= 0) {\n      result = result.withDeletedColumnAt(commitInfoIdx);\n    }\n\n    return result;\n  }\n\n  /**\n   * Closes this CommitActionsImpl and releases any underlying resources.\n   *\n   * @throws IOException if an I/O error occurs while closing resources\n   */\n  @Override\n  public synchronized void close() throws IOException {\n    Utils.closeCloseables(iterator);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/CreateTableTransactionBuilderImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Utils.resolvePath;\nimport static java.util.Collections.emptyMap;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.TableAlreadyExistsException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.Clock;\nimport io.delta.kernel.transaction.CreateTableTransactionBuilder;\nimport io.delta.kernel.transaction.DataLayoutSpec;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\n\npublic class CreateTableTransactionBuilderImpl implements CreateTableTransactionBuilder {\n\n  private Clock clock = System::currentTimeMillis;\n\n  private final String unresolvedPath;\n  private final StructType schema;\n  private final String engineInfo;\n\n  private Optional<Map<String, String>> tableProperties = Optional.empty();\n  private Optional<DataLayoutSpec> dataLayoutSpec = Optional.empty();\n  private Optional<Integer> userProvidedMaxRetries = Optional.empty();\n  private Optional<Committer> userProvidedCommitter = Optional.empty();\n\n  public CreateTableTransactionBuilderImpl(String tablePath, StructType schema, String engineInfo) {\n    this.unresolvedPath = requireNonNull(tablePath, \"tablePath is null\");\n    this.schema = requireNonNull(schema, \"schema is null\");\n    this.engineInfo = requireNonNull(engineInfo, \"engineInfo is null\");\n  }\n\n  @Override\n  public CreateTableTransactionBuilder withTableProperties(Map<String, String> properties) {\n    requireNonNull(properties, \"properties cannot be null\");\n\n    final Map<String, String> normalizedNewProperties =\n        TableConfig.validateAndNormalizeDeltaProperties(properties);\n\n    // Case 1: First time properties are being set\n    if (!this.tableProperties.isPresent()) {\n      this.tableProperties = Optional.of(Collections.unmodifiableMap(normalizedNewProperties));\n      return this;\n    }\n\n    // Case 2: Properties have already been set; ensure no duplicates with different values\n    final Map<String, String> existingProperties = this.tableProperties.get();\n    for (String key : normalizedNewProperties.keySet()) {\n      final String existingValue = existingProperties.get(key);\n      if (existingValue != null) {\n        final String newValue = normalizedNewProperties.get(key);\n        if (!Objects.equals(existingValue, newValue)) {\n          throw new IllegalArgumentException(\n              String.format(\n                  \"Table property '%s' has already been set. Existing value: '%s', New value: '%s'\",\n                  key, existingValue, newValue));\n        }\n      }\n    }\n\n    final Map<String, String> mergedProperties = new HashMap<>(existingProperties);\n    mergedProperties.putAll(normalizedNewProperties);\n    this.tableProperties = Optional.of(Collections.unmodifiableMap(mergedProperties));\n\n    return this;\n  }\n\n  @Override\n  public CreateTableTransactionBuilder withDataLayoutSpec(DataLayoutSpec spec) {\n    requireNonNull(spec, \"spec cannot be null\");\n    this.dataLayoutSpec = Optional.of(spec);\n    return this;\n  }\n\n  @Override\n  public CreateTableTransactionBuilder withMaxRetries(int maxRetries) {\n    checkArgument(maxRetries >= 0, \"maxRetries must be >= 0\");\n    this.userProvidedMaxRetries = Optional.of(maxRetries);\n    return this;\n  }\n\n  @Override\n  public CreateTableTransactionBuilder withCommitter(Committer committer) {\n    userProvidedCommitter = Optional.of(requireNonNull(committer, \"committer cannot be null\"));\n    return this;\n  }\n\n  @VisibleForTesting\n  public CreateTableTransactionBuilder withClock(Clock clock) {\n    this.clock = requireNonNull(clock, \"clock cannot be null\");\n    return this;\n  }\n\n  @Override\n  public Transaction build(Engine engine) {\n    requireNonNull(engine, \"engine cannot be null\");\n    String resolvedPath = resolvePath(engine, unresolvedPath);\n    throwIfTableAlreadyExists(engine, resolvedPath);\n\n    // Extract partition and clustering columns from the data layout spec\n    Optional<List<String>> partitionColumns =\n        dataLayoutSpec\n            .filter(DataLayoutSpec::hasPartitioning)\n            .map(DataLayoutSpec::getPartitionColumnsAsStrings);\n    Optional<List<Column>> clusteringColumns =\n        dataLayoutSpec\n            .filter(DataLayoutSpec::hasClustering)\n            .map(DataLayoutSpec::getClusteringColumns);\n\n    TransactionMetadataFactory.Output txnMetadata =\n        TransactionMetadataFactory.buildCreateTableMetadata(\n            resolvedPath,\n            schema,\n            tableProperties.orElse(emptyMap()),\n            partitionColumns,\n            clusteringColumns,\n            userProvidedCommitter);\n\n    Path dataPath = new Path(resolvedPath);\n    return new TransactionImpl(\n        true, // isCreateOrReplace\n        dataPath,\n        Optional.empty(), // no existing snapshot for create table\n        engineInfo,\n        Operation.CREATE_TABLE,\n        txnMetadata.newProtocol,\n        txnMetadata.newMetadata,\n        userProvidedCommitter.orElse(DefaultFileSystemManagedTableOnlyCommitter.INSTANCE),\n        Optional.empty(), // no setTransaction for create table\n        txnMetadata.physicalNewClusteringColumns,\n        userProvidedMaxRetries,\n        0, // logCompactionInterval - no compaction for new table\n        clock);\n  }\n\n  @VisibleForTesting\n  public Optional<Map<String, String>> getTablePropertiesOpt() {\n    return tableProperties;\n  }\n\n  @VisibleForTesting\n  public Optional<Committer> getCommitterOpt() {\n    return userProvidedCommitter;\n  }\n\n  private void throwIfTableAlreadyExists(Engine engine, String tablePath) {\n    final boolean isCatalogManaged =\n        tableProperties\n            .map(\n                props ->\n                    TableFeatures.isPropertiesManuallySupportingTableFeature(\n                        props, TableFeatures.CATALOG_MANAGED_RW_FEATURE))\n            .orElse(false);\n    if (isCatalogManaged) {\n      // For catalog managed tables we assume the catalog has ensured the table loc is not already\n      // a Delta table; return early\n      return;\n    }\n    // Otherwise, try loading the latest snapshot to ensure the table does not exist\n    try {\n      Snapshot snapshot = TableManager.loadSnapshot(tablePath).build(engine);\n      throw new TableAlreadyExistsException(\n          tablePath, \"Found table with latest version \" + snapshot.getVersion());\n    } catch (TableNotFoundException tblf) {\n      // This is the desired scenario as the table should not exist yet\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/DataWriteContextImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static java.util.Collections.unmodifiableList;\nimport static java.util.Collections.unmodifiableMap;\n\nimport io.delta.kernel.DataWriteContext;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.fs.Path;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Implements the {@link DataWriteContext} interface. In addition to the data needed for the\n * interface, it also contains the partition values of the targeted partition. In case of\n * un-partitioned tables, the partition values will be empty.\n */\npublic class DataWriteContextImpl implements DataWriteContext {\n  private final String targetDirectory;\n  private final Map<String, Literal> partitionValues;\n  private final List<Column> statsColumns;\n\n  /**\n   * Creates a new instance of WriteContext.\n   *\n   * @param targetDirectory fully qualified path of the target directory\n   * @param partitionValues partition values for the data to be written. If the table is\n   *     un-partitioned, this should be an empty map.\n   * @param statsColumns Set of columns that need statistics for the data to be written. The column\n   *     can be a top-level column or a nested column. E.g. \"a.b.c\" is a nested column. \"d\" is a\n   *     top-level column.\n   */\n  public DataWriteContextImpl(\n      String targetDirectory, Map<String, Literal> partitionValues, List<Column> statsColumns) {\n    this.targetDirectory = targetDirectory;\n    this.partitionValues = unmodifiableMap(partitionValues);\n    this.statsColumns = unmodifiableList(statsColumns);\n  }\n\n  /**\n   * Returns the target directory where the data should be written.\n   *\n   * @return fully qualified path of the target directory\n   */\n  public String getTargetDirectory() {\n    // TODO: this is temporary until paths are uniform (i.e. they are actually file system paths\n    // or URIs everywhere, but not a combination of the two).\n    return new Path(targetDirectory).toUri().toString();\n  }\n\n  /**\n   * Returns the partition values for the data to be written. If the table is un-partitioned, this\n   * should be an empty map.\n   *\n   * @return partition values\n   */\n  public Map<String, Literal> getPartitionValues() {\n    return partitionValues;\n  }\n\n  /**\n   * Returns the list of {@link Column} that the connector can optionally collect statistics. Each\n   * {@link Column} is a reference to a top-level or nested column in the table.\n   *\n   * <p>Statistics collections can be skipped or collected for a partial list of the returned {@link\n   * Column}s. When stats are present in the written Delta log, they can be used to optimize query\n   * performance.\n   *\n   * @return schema of the statistics\n   */\n  public List<Column> getStatisticsColumns() {\n    return statsColumns;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/DeltaErrors.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static java.lang.String.format;\n\nimport io.delta.kernel.commit.CommitFailedException;\nimport io.delta.kernel.exceptions.*;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.types.TypeChange;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.io.IOException;\nimport java.sql.Timestamp;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/** Contains methods to create user-facing Delta exceptions. */\npublic final class DeltaErrors {\n  private DeltaErrors() {}\n\n  public static KernelException missingCheckpoint(String tablePath, long checkpointVersion) {\n    return new InvalidTableException(\n        tablePath, String.format(\"Missing checkpoint at version %s\", checkpointVersion));\n  }\n\n  public static KernelException versionBeforeFirstAvailableCommit(\n      String tablePath, long versionToLoad, long earliestVersion) {\n    String message =\n        String.format(\n            \"%s: Cannot load table version %s as the transaction log has been truncated due to \"\n                + \"manual deletion or the log/checkpoint retention policy. The earliest available \"\n                + \"version is %s.\",\n            tablePath, versionToLoad, earliestVersion);\n    return new KernelException(message);\n  }\n\n  public static KernelException versionToLoadAfterLatestCommit(\n      String tablePath, long versionToLoad, long latestVersion) {\n    String message =\n        String.format(\n            \"%s: Cannot load table version %s as it does not exist. \"\n                + \"The latest available version is %s.\",\n            tablePath, versionToLoad, latestVersion);\n    return new KernelException(message);\n  }\n\n  public static KernelException timestampBeforeFirstAvailableCommit(\n      String tablePath,\n      long providedTimestamp,\n      long earliestCommitTimestamp,\n      long earliestCommitVersion) {\n    String message =\n        String.format(\n            \"%s: The provided timestamp %s ms (%s) is before the earliest available version %s. \"\n                + \"Please use a timestamp greater than or equal to %s ms (%s).\",\n            tablePath,\n            providedTimestamp,\n            formatTimestamp(providedTimestamp),\n            earliestCommitVersion,\n            earliestCommitTimestamp,\n            formatTimestamp(earliestCommitTimestamp));\n    return new KernelException(message);\n  }\n\n  public static KernelException timestampAfterLatestCommit(\n      String tablePath,\n      long providedTimestamp,\n      long latestCommitTimestamp,\n      long latestCommitVersion) {\n    String message =\n        String.format(\n            \"%s: The provided timestamp %s ms (%s) is after the latest available version %s. \"\n                + \"Please use a timestamp less than or equal to %s ms (%s).\",\n            tablePath,\n            providedTimestamp,\n            formatTimestamp(providedTimestamp),\n            latestCommitVersion,\n            latestCommitTimestamp,\n            formatTimestamp(latestCommitTimestamp));\n    return new KernelException(message);\n  }\n\n  public static CommitRangeNotFoundException noCommitFilesFoundForVersionRange(\n      String tablePath, long startVersion, Optional<Long> endVersionOpt) {\n    return new CommitRangeNotFoundException(tablePath, startVersion, endVersionOpt);\n  }\n\n  public static KernelException startVersionNotFound(\n      String tablePath, long startVersionRequested, Optional<Long> earliestAvailableVersion) {\n    String message =\n        String.format(\n            \"%s: Requested table changes beginning with startVersion=%s but no log file found for \"\n                + \"version %s.\",\n            tablePath, startVersionRequested, startVersionRequested);\n    if (earliestAvailableVersion.isPresent()) {\n      message =\n          message\n              + String.format(\" Earliest available version is %s\", earliestAvailableVersion.get());\n    }\n    return new KernelException(message);\n  }\n\n  public static KernelException endVersionNotFound(\n      String tablePath, long endVersionRequested, long latestAvailableVersion) {\n    String message =\n        String.format(\n            \"%s: Requested table changes ending with endVersion=%d but no log file found for \"\n                + \"version %d. Latest available version is %d\",\n            tablePath, endVersionRequested, endVersionRequested, latestAvailableVersion);\n    return new KernelException(message);\n  }\n\n  public static KernelException invalidVersionRange(long startVersion, long endVersion) {\n    String message =\n        String.format(\n            \"Invalid version range: requested table changes for version range [%s, %s]. \"\n                + \"Requires startVersion >= 0 and endVersion >= startVersion.\",\n            startVersion, endVersion);\n    return new KernelException(message);\n  }\n\n  public static KernelException invalidResolvedVersionRange(\n      String tablePath, long startVersion, long endVersion) {\n    String message =\n        String.format(\n            \"%s: Invalid resolved version range: after timestamp resolution, \"\n                + \"startVersion=%d > endVersion=%d. \"\n                + \"Please adjust the provided timestamp boundaries.\",\n            tablePath, startVersion, endVersion);\n    return new KernelException(message);\n  }\n\n  public static KernelException resolvedEndVersionAfterMaxCatalogVersion(\n      String tablePath, long resolvedEndVersion, long maxCatalogVersion) {\n    String message =\n        String.format(\n            \"%s: Resolved end version to %s which is after max catalog version %s\",\n            tablePath, resolvedEndVersion, maxCatalogVersion);\n    return new KernelException(message);\n  }\n\n  /* ------------------------ PROTOCOL EXCEPTIONS ----------------------------- */\n  public static UnsupportedProtocolVersionException unsupportedReaderProtocol(\n      String tablePath, int tableReaderVersion) {\n    return new UnsupportedProtocolVersionException(\n        tablePath,\n        tableReaderVersion,\n        UnsupportedProtocolVersionException.ProtocolVersionType.READER);\n  }\n\n  public static UnsupportedProtocolVersionException unsupportedWriterProtocol(\n      String tablePath, int tableWriterVersion) {\n    return new UnsupportedProtocolVersionException(\n        tablePath,\n        tableWriterVersion,\n        UnsupportedProtocolVersionException.ProtocolVersionType.WRITER);\n  }\n\n  public static UnsupportedTableFeatureException unsupportedTableFeature(String feature) {\n    String message =\n        String.format(\n            \"Unsupported Delta table feature: table requires feature \\\"%s\\\" \"\n                + \"which is unsupported by this version of Delta Kernel.\",\n            feature);\n    return new UnsupportedTableFeatureException(null, feature, message);\n  }\n\n  public static UnsupportedTableFeatureException unsupportedReaderFeatures(\n      String tablePath, Set<String> readerFeatures) {\n    String message =\n        String.format(\n            \"Unsupported Delta reader features: table `%s` requires reader table features [%s] \"\n                + \"which is unsupported by this version of Delta Kernel.\",\n            tablePath, String.join(\", \", readerFeatures));\n    return new UnsupportedTableFeatureException(tablePath, readerFeatures, message);\n  }\n\n  public static UnsupportedTableFeatureException unsupportedWriterFeatures(\n      String tablePath, Set<String> writerFeatures) {\n    String message =\n        String.format(\n            \"Unsupported Delta writer features: table `%s` requires writer table features [%s] \"\n                + \"which is unsupported by this version of Delta Kernel.\",\n            tablePath, String.join(\", \", writerFeatures));\n    return new UnsupportedTableFeatureException(tablePath, writerFeatures, message);\n  }\n\n  public static KernelException columnInvariantsNotSupported() {\n    String message =\n        \"This version of Delta Kernel does not support writing to tables with \"\n            + \"column invariants present.\";\n    return new KernelException(message);\n  }\n\n  public static KernelException checkpointOnUnpublishedCommits(\n      String tablePath, long version, long maxPublishedVersion) {\n    String message =\n        String.format(\n            \"Unable to create checkpoint: Snapshot at at path\"\n                + \" `%s` with version %d has unpublished commits. \"\n                + \"Max known published version is %d\",\n            tablePath, version, maxPublishedVersion);\n    return new KernelException(message);\n  }\n\n  public static KernelException unsupportedDataType(DataType dataType) {\n    return new KernelException(\"Kernel doesn't support writing data of type: \" + dataType);\n  }\n\n  public static KernelException unsupportedStatsDataType(DataType dataType) {\n    return new KernelException(\"Kernel doesn't support writing stats data of type: \" + dataType);\n  }\n\n  public static KernelException unsupportedPartitionDataType(String colName, DataType dataType) {\n    String msgT = \"Kernel doesn't support writing data with partition column (%s) of type: %s\";\n    return new KernelException(format(msgT, colName, dataType));\n  }\n\n  public static KernelException duplicateColumnsInSchema(\n      StructType schema, List<String> duplicateColumns) {\n    String msg =\n        format(\n            \"Schema contains duplicate columns: %s.\\nSchema: %s\",\n            String.join(\", \", duplicateColumns), schema);\n    return new KernelException(msg);\n  }\n\n  public static KernelException conflictWithReservedInternalColumnName(String columnName) {\n    return new KernelException(\n        format(\"Cannot use column name '%s' because it is reserved for internal use\", columnName));\n  }\n\n  public static KernelException invalidColumnName(String columnName, String unsupportedChars) {\n    return new KernelException(\n        format(\n            \"Column name '%s' contains one of the unsupported (%s) characters.\",\n            columnName, unsupportedChars));\n  }\n\n  public static KernelException requiresSchemaForNewTable(String tablePath) {\n    return new TableNotFoundException(\n        tablePath, \"Must provide a new schema to write to a new table.\");\n  }\n\n  public static KernelException requireSchemaForReplaceTable() {\n    return new KernelException(\"Must provide a new schema for REPLACE TABLE\");\n  }\n\n  public static KernelException tableAlreadyExists(String tablePath, String message) {\n    return new TableAlreadyExistsException(tablePath, message);\n  }\n\n  public static KernelException dataSchemaMismatch(\n      String tablePath, StructType tableSchema, StructType dataSchema) {\n    String msgT =\n        \"The schema of the data to be written to the table doesn't match \"\n            + \"the table schema. \\nTable: %s\\nTable schema: %s, \\nData schema: %s\";\n    return new KernelException(format(msgT, tablePath, tableSchema, dataSchema));\n  }\n\n  public static KernelException statsTypeMismatch(\n      String fieldName, DataType expected, DataType actual) {\n    String msgFormat =\n        \"Type mismatch for field '%s' when writing statistics: expected %s, but found %s\";\n    return new KernelException(format(msgFormat, fieldName, expected, actual));\n  }\n\n  public static KernelException columnNotFoundInSchema(Column column, StructType tableSchema) {\n    return new KernelException(\n        format(\"Column '%s' was not found in the table schema: %s\", column, tableSchema));\n  }\n\n  public static KernelException overlappingTablePropertiesSetAndUnset(Set<String> violatingKeys) {\n    return new KernelException(\n        format(\n            \"Cannot set and unset the same table property in the same transaction. \"\n                + \"Properties set and unset: %s\",\n            violatingKeys));\n  }\n\n  /// Start: icebergCompat exceptions\n  public static KernelException icebergCompatMissingNumRecordsStats(\n      String compatVersion, DataFileStatus dataFileStatus) {\n    throw new KernelException(\n        format(\n            \"%s compatibility requires 'numRecords' statistic.\\n DataFileStatus: %s\",\n            compatVersion, dataFileStatus));\n  }\n\n  public static KernelException icebergCompatIncompatibleVersionEnabled(\n      String compatVersion, String incompatibleIcebergCompatVersion) {\n    throw new KernelException(\n        format(\n            \"%s: Only one IcebergCompat version can be enabled. Incompatible version enabled: %s\",\n            compatVersion, incompatibleIcebergCompatVersion));\n  }\n\n  public static KernelException icebergCompatUnsupportedTypeColumns(\n      String compatVersion, List<DataType> dataTypes) {\n    throw new KernelException(\n        format(\"%s does not support the data types: %s.\", compatVersion, dataTypes));\n  }\n\n  public static KernelException icebergCompatUnsupportedTypeWidening(\n      String compatVersion, TypeChange typeChange) {\n    throw new KernelException(\n        format(\n            \"%s does not support type widening present in table: %s.\", compatVersion, typeChange));\n  }\n\n  public static KernelException icebergCompatUnsupportedTypePartitionColumn(\n      String compatVersion, DataType dataType) {\n    throw new KernelException(\n        format(\n            \"%s does not support the data type '%s' for a partition column.\",\n            compatVersion, dataType));\n  }\n\n  public static KernelException icebergCompatRequiresLiteralDefaultValue(\n      String compatVersion, DataType dataType, String value) {\n    throw new KernelException(\n        format(\n            \"%s requires the default value to be literal with correct data types for \"\n                + \"a column. '%s: %s' is invalid.\",\n            compatVersion, dataType, value));\n  }\n\n  public static KernelException icebergCompatIncompatibleTableFeatures(\n      String compatVersion, Set<TableFeature> incompatibleFeatures) {\n    throw new KernelException(\n        format(\n            \"Table features %s are incompatible with %s.\",\n            incompatibleFeatures.stream()\n                .map(TableFeature::featureName)\n                .collect(Collectors.toList()),\n            compatVersion));\n  }\n\n  public static KernelException icebergCompatRequiredFeatureMissing(\n      String compatVersion, String feature) {\n    throw new KernelException(\n        format(\"%s: requires the feature '%s' to be enabled.\", compatVersion, feature));\n  }\n\n  public static KernelException enablingIcebergCompatFeatureOnExistingTable(String key) {\n    return new KernelException(\n        String.format(\n            \"Cannot enable %s on an existing table. \"\n                + \"Enablement is only supported upon table creation.\",\n            key));\n  }\n\n  public static KernelException icebergWriterCompatInvalidPhysicalName(List<String> invalidFields) {\n    return new KernelException(\n        String.format(\n            \"IcebergWriterCompatV1 requires column mapping field physical names be equal to \"\n                + \"'col-[fieldId]', but this is not true for the following fields %s\",\n            invalidFields));\n  }\n\n  public static KernelException disablingIcebergCompatFeatureOnExistingTable(String key) {\n    return new KernelException(\n        String.format(\"Disabling %s on an existing table is not allowed.\", key));\n  }\n\n  // End: icebergCompat exceptions\n\n  // Start: Column Defaults Exceptions\n\n  // TODO migrate this to InvalidTableException when table info is available at the call site\n  public static KernelException defaultValueRequiresTableFeature() {\n    return new KernelException(\n        \"Found column defaults in the schema but the table does not support the \"\n            + \"columnDefaults table feature.\");\n  }\n\n  public static KernelException defaultValueRequireIcebergV3() {\n    return new KernelException(\n        \"In Delta Kernel, default values table feature requires \"\n            + \"IcebergCompatV3 to be enabled.\");\n  }\n\n  public static KernelException unsupportedDataTypeForDefaultValue(\n      String fieldName, String fieldType) {\n    return new KernelException(\n        String.format(\n            \"Kernel does not support default value for \" + \"data type %s: %s\",\n            fieldType, fieldName));\n  }\n\n  public static KernelException nonLiteralDefaultValue(String value) {\n    return new KernelException(\n        String.format(\n            \"currently only literal values are supported for default values in Kernel.\"\n                + \" %s is an invalid default value\",\n            value));\n  }\n\n  // End: Column Defaults Exceptions\n\n  public static KernelException partitionColumnMissingInData(\n      String tablePath, String partitionColumn) {\n    String msgT = \"Missing partition column '%s' in the data to be written to the table '%s'.\";\n    return new KernelException(format(msgT, partitionColumn, tablePath));\n  }\n\n  public static KernelException enablingClusteringOnPartitionedTableNotAllowed(\n      String tablePath, Set<String> partitionColNames, List<Column> clusteringCols) {\n    return new KernelException(\n        String.format(\n            \"Cannot enable clustering on a partitioned table '%s'. \"\n                + \"Existing partition columns: '%s', Clustering columns: '%s'.\",\n            tablePath, partitionColNames, clusteringCols));\n  }\n\n  public static RuntimeException nonRetryableCommitException(\n      int attempt, long commitAsVersion, CommitFailedException cause) {\n    throw new RuntimeException(\n        String.format(\n            \"Commit attempt %d for version %d failed with a non-retryable exception.\",\n            attempt, commitAsVersion),\n        cause);\n  }\n\n  public static KernelException concurrentTransaction(\n      String appId, long txnVersion, long lastUpdated) {\n    return new ConcurrentTransactionException(appId, txnVersion, lastUpdated);\n  }\n\n  public static KernelException metadataChangedException() {\n    return new MetadataChangedException();\n  }\n\n  public static KernelException protocolChangedException(long attemptVersion) {\n    return new ProtocolChangedException(attemptVersion);\n  }\n\n  public static KernelException voidTypeEncountered() {\n    return new KernelException(\n        \"Failed to parse the schema. Encountered unsupported Delta data type: VOID\");\n  }\n\n  public static KernelException cannotModifyTableProperty(String key) {\n    String msg =\n        format(\"The Delta table property '%s' is an internal property and cannot be updated.\", key);\n    return new KernelException(msg);\n  }\n\n  public static KernelException unknownConfigurationException(String confKey) {\n    return new UnknownConfigurationException(confKey);\n  }\n\n  public static KernelException invalidConfigurationValueException(\n      String key, String value, String helpMessage) {\n    return new InvalidConfigurationValueException(key, value, helpMessage);\n  }\n\n  public static KernelException domainMetadataUnsupported() {\n    String message =\n        \"Cannot commit DomainMetadata action(s) because the feature 'domainMetadata' \"\n            + \"is not supported on this table.\";\n    return new KernelException(message);\n  }\n\n  public static ConcurrentWriteException concurrentDomainMetadataAction(\n      DomainMetadata domainMetadataAttempt, DomainMetadata winningDomainMetadata) {\n    String message =\n        String.format(\n            \"A concurrent writer added a domainMetadata action for the same domain: %s. \"\n                + \"No domain-specific conflict resolution is available for this domain. \"\n                + \"Attempted domainMetadata: %s. Winning domainMetadata: %s\",\n            domainMetadataAttempt.getDomain(), domainMetadataAttempt, winningDomainMetadata);\n    return new ConcurrentWriteException(message);\n  }\n\n  public static KernelException missingNumRecordsStatsForRowTracking() {\n    return new KernelException(\n        \"Cannot write to a rowTracking-supported table without 'numRecords' statistics. \"\n            + \"Connectors are expected to populate the number of records statistics when \"\n            + \"writing to a Delta table with 'rowTracking' table feature supported.\");\n  }\n\n  public static KernelException rowTrackingSupportedWithDomainMetadataUnsupported() {\n    return new KernelException(\n        \"Feature 'rowTracking' is supported and depends on feature 'domainMetadata',\"\n            + \" but 'domainMetadata' is unsupported\");\n  }\n\n  public static KernelException rowTrackingRequiredForRowIdHighWatermark(\n      String tablePath, String rowIdHighWatermark) {\n    return new KernelException(\n        String.format(\n            \"Cannot assign a row id high water mark (`%s`) to a table `%s` that does not support \"\n                + \"`rowTracking` table feature. Please enable the `rowTracking` table feature.\",\n            rowIdHighWatermark, tablePath));\n  }\n\n  public static KernelException cannotToggleRowTrackingOnExistingTable() {\n    return new KernelException(\"Row tracking support cannot be changed once the table is created.\");\n  }\n\n  public static KernelException missingRowTrackingColumnRequested(String columnName) {\n    return new KernelException(\n        String.format(\n            \"Row tracking is not enabled, but row tracking column '%s' was requested.\",\n            columnName));\n  }\n\n  public static KernelException cannotModifyAppendOnlyTable(String tablePath) {\n    return new KernelException(\n        String.format(\n            \"Cannot modify append-only table. Table `%s` has configuration %s=true.\",\n            tablePath, TableConfig.APPEND_ONLY_ENABLED.getKey()));\n  }\n\n  public static KernelException rowTrackingMetadataMissingInFile(String entry, String filePath) {\n    return new KernelException(\n        String.format(\"Required metadata key %s is not present in scan file %s.\", entry, filePath));\n  }\n\n  public static InvalidTableException tableWithIctMissingCommitInfo(String dataPath, long version) {\n    return new InvalidTableException(\n        dataPath,\n        String.format(\n            \"This table has the feature inCommitTimestamp enabled which requires the presence of \"\n                + \"the CommitInfo action in every commit. However, the CommitInfo action is \"\n                + \"missing from commit version %d.\",\n            version));\n  }\n\n  public static InvalidTableException tableWithIctMissingIct(String dataPath, long version) {\n    return new InvalidTableException(\n        dataPath,\n        String.format(\n            \"This table has the feature inCommitTimestamp enabled which requires the presence of \"\n                + \"inCommitTimestamp in the CommitInfo action. However, this field has not been \"\n                + \"set in commit version %d.\",\n            version));\n  }\n\n  public static KernelException metadataMissingRequiredCatalogTableProperty(\n      String committerClassName,\n      Map<String, String> missingOrViolatingProperties,\n      Map<String, String> requiredCatalogTableProperties) {\n    final String details =\n        missingOrViolatingProperties.entrySet().stream()\n            .map(\n                entry ->\n                    String.format(\n                        \"%s (current: '%s', required: '%s')\",\n                        entry.getKey(),\n                        entry.getValue(),\n                        requiredCatalogTableProperties.get(entry.getKey())))\n            .collect(Collectors.joining(\", \"));\n    return new KernelException(\n        String.format(\n            \"[%s] Metadata is missing or has incorrect values for required catalog properties: %s.\",\n            committerClassName, details));\n  }\n\n  public static KernelException invalidFieldMove(\n      int columnId,\n      Optional<SchemaIterable.ParentStructFieldInfo> currentParent,\n      Optional<SchemaIterable.ParentStructFieldInfo> newParent) {\n    return new KernelException(\n        String.format(\n            \"Cannot move fields between different levels of nesting: \"\n                + \"field with fieldId=%s is nested under %s in the current schema and under %s in \"\n                + \"the new schema\",\n            columnId, formatParentField(currentParent), formatParentField(newParent)));\n  }\n\n  /* ------------------------ HELPER METHODS ----------------------------- */\n\n  private static String formatParentField(Optional<SchemaIterable.ParentStructFieldInfo> parent) {\n    if (!parent.isPresent()) {\n      return \"ROOT\";\n    }\n    StructField parentField = parent.get().getParentField();\n    String pathToParentField = parent.get().getPathFromParent();\n    if (pathToParentField.isEmpty()) {\n      // Example: \"StructField(name=c1, ...)\"\n      return parentField.toString();\n    } else {\n      // Example: \"StructField(name=c1, ...) at path=key.element\"\n      return parentField.toString() + \" at path=\" + pathToParentField;\n    }\n  }\n\n  private static String formatTimestamp(long millisSinceEpochUTC) {\n    return new Timestamp(millisSinceEpochUTC).toInstant().toString();\n  }\n\n  // We use the `Supplier` interface to avoid silently wrapping any checked exceptions\n  public static <T> T wrapEngineException(Supplier<T> f, String msgString, Object... args) {\n    try {\n      return f.get();\n    } catch (KernelException e) {\n      // Let any KernelExceptions fall through (even though these generally shouldn't\n      // originate from the engine implementation there are some edge cases such as\n      // deserializeStructType)\n      throw e;\n    } catch (RuntimeException e) {\n      throw new KernelEngineException(String.format(msgString, args), e);\n    }\n  }\n\n  // Functional interface for a fx that throws an `IOException` (but no other checked exceptions)\n  public interface SupplierWithIOException<T> {\n    T get() throws IOException;\n  }\n\n  public static <T> T wrapEngineExceptionThrowsIO(\n      SupplierWithIOException<T> f, String msgString, Object... args) throws IOException {\n    try {\n      return f.get();\n    } catch (KernelException e) {\n      // Let any KernelExceptions fall through (even though these generally shouldn't\n      // originate from the engine implementation there are some edge cases such as\n      // deserializeStructType)\n      throw e;\n    } catch (RuntimeException e) {\n      throw new KernelEngineException(String.format(msgString, args), e);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/DeltaErrorsInternal.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport io.delta.kernel.exceptions.InvalidTableException;\nimport java.util.List;\n\n/**\n * Contains methods to create developer-facing exceptions. See <a\n * href=\"https://github.com/delta-io/delta/blob/master/kernel/EXCEPTION_PRINCIPLES.md\">Exception\n * Principles</a> for more information on what these are and how to use them.\n */\npublic class DeltaErrorsInternal {\n\n  private DeltaErrorsInternal() {}\n\n  public static IllegalStateException missingRemoveFileSizeDuringCommit() {\n    return new IllegalStateException(\n        \"Kernel APIs for creating remove file rows require that \"\n            + \"file size be provided but found null file size\");\n  }\n\n  public static IllegalStateException invalidTimestampFormatForPartitionValue(\n      String partitionValue) {\n    return new IllegalStateException(\n        String.format(\n            \"Invalid timestamp format for value: %s. Expected formats: \"\n                + \"'yyyy-MM-dd HH:mm:ss[.SSSSSS]' or ISO-8601 (e.g. 2020-01-01T00:00:00Z)'\",\n            partitionValue));\n  }\n\n  public static UnsupportedOperationException defaultCommitterDoesNotSupportCatalogManagedTables() {\n    return new UnsupportedOperationException(\n        \"No io.delta.kernel.commit.Committer has been provided to Kernel, so Kernel is using a \"\n            + \"default Committer that only supports committing to filesystem-managed Delta tables, \"\n            + \"not catalog-managed Delta tables. Since this table is catalog-managed, this \"\n            + \"commit operation is unsupported.\");\n  }\n\n  public static IllegalStateException logicalPhysicalSchemaMismatch(\n      int num_partition_cols, int physical_size, int logical_size) {\n    return new IllegalStateException(\n        String.format(\n            \"The number of partition columns (%s) plus the physical schema size (%s) does not \"\n                + \"equal the logical schema size (%s).\",\n            num_partition_cols, physical_size, logical_size));\n  }\n\n  public static InvalidTableException catalogCommitsPrecedePublishedDeltas(\n      String tablePath, long earliestCatalogCommitVersion, List<Long> foundPublishedVersions) {\n    return new InvalidTableException(\n        tablePath,\n        String.format(\n            \"Missing delta file: found staged ratified commit for version %s but no published \"\n                + \"delta file. Found published deltas for later versions: %s\",\n            earliestCatalogCommitVersion, foundPublishedVersions));\n  }\n\n  public static InvalidTableException publishedDeltasAndCatalogCommitsNotContiguous(\n      String tablePath, List<Long> publishedDeltaVersions, List<Long> catalogCommitVersions) {\n    return new InvalidTableException(\n        tablePath,\n        String.format(\n            \"Missing delta files: found published delta files for versions %s and staged \"\n                + \"ratified commits for versions %s\",\n            publishedDeltaVersions, catalogCommitVersions));\n  }\n\n  public static InvalidTableException publishedDeltasNotContiguous(\n      String tablePath, List<Long> foundVersions) {\n    return new InvalidTableException(\n        tablePath,\n        String.format(\"Missing delta files: versions are not contiguous: (%s)\", foundVersions));\n  }\n\n  public static IllegalArgumentException invalidLatestSnapshotForMaxCatalogVersion(\n      long latestSnapshotVersion, long maxCatalogVersion) {\n    return new IllegalArgumentException(\n        String.format(\n            \"When using timestamp boundaries with maxCatalogVersion, the provided \"\n                + \"snapshot version (%d) must equal maxCatalogVersion (%d)\",\n            latestSnapshotVersion, maxCatalogVersion));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/DeltaHistoryManager.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.TableConfig.*;\nimport static io.delta.kernel.internal.fs.Path.getName;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.internal.actions.CommitInfo;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.checkpoints.CheckpointInstance;\nimport io.delta.kernel.internal.files.LogDataUtils;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.InCommitTimestampUtils;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.function.Function;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic final class DeltaHistoryManager {\n\n  private DeltaHistoryManager() {}\n\n  private static final Logger logger = LoggerFactory.getLogger(DeltaHistoryManager.class);\n\n  /**\n   * Returns the latest version that was committed at or after {@code millisSinceEpochUTC}. If no\n   * version exists, throws a {@link KernelException}\n   *\n   * <p>Specifically:\n   *\n   * <ul>\n   *   <li>if a commit version exactly matches the provided timestamp, we return it\n   *   <li>else, we return the earliest commit version with a timestamp greater than the provided\n   *       one\n   *   <li>If the provided timestamp is larger than the timestamp of any committed version, we throw\n   *       an error.\n   * </ul>\n   *\n   * @param millisSinceEpochUTC the number of milliseconds since midnight, January 1, 1970 UTC\n   * @param catalogCommits parsed log Deltas to use (must be sorted and contiguous)\n   * @return latest commit that happened at or before {@code timestamp}.\n   * @throws KernelException if the timestamp is more than the timestamp of any committed version\n   */\n  public static long getVersionAtOrAfterTimestamp(\n      Engine engine,\n      Path logPath,\n      long millisSinceEpochUTC,\n      SnapshotImpl latestSnapshot,\n      List<ParsedCatalogCommitData> catalogCommits) {\n    DeltaHistoryManager.Commit commit =\n        DeltaHistoryManager.getActiveCommitAtTimestamp(\n            engine,\n            latestSnapshot,\n            logPath,\n            millisSinceEpochUTC,\n            false, /* mustBeRecreatable */\n            // e.g. if we give time T+2 and last commit has time T, then we do NOT want that last\n            // commit\n            false, /* canReturnLastCommit */\n            // e.g. we give time T-1 and first commit has time T, then we DO want that earliest\n            // commit\n            true /* canReturnEarliestCommit */,\n            catalogCommits);\n\n    if (commit.getTimestamp() >= millisSinceEpochUTC) {\n      return commit.getVersion();\n    } else {\n      // this commit.timestamp is before the input timestamp. if this is the last commit, then\n      // the input timestamp is after the last commit and `getActiveCommitAtTimestamp` would have\n      // thrown an KernelException. So, clearly, this can't be the last commit, so we can safely\n      // return commit.version + 1 as the version that is at or after the input timestamp.\n      return commit.getVersion() + 1;\n    }\n  }\n\n  /**\n   * Returns the latest version that was committed before or at {@code millisSinceEpochUTC}. If no\n   * version exists, throws a {@link KernelException}\n   *\n   * <p>Specifically:\n   *\n   * <ul>\n   *   <li>if a commit version exactly matches the provided timestamp, we return it\n   *   <li>else, we return the latest commit version with a timestamp less than the provided one\n   *   <li>If the provided timestamp is less than the timestamp of any committed version, we throw\n   *       an error.\n   * </ul>\n   *\n   * @param millisSinceEpochUTC the number of milliseconds since midnight, January 1, 1970 UTC\n   * @param catalogCommits parsed log Deltas to use (must be sorted and contiguous)\n   * @return latest commit that happened before or at {@code timestamp}.\n   * @throws KernelException if the timestamp is less than the timestamp of any committed version\n   */\n  public static long getVersionBeforeOrAtTimestamp(\n      Engine engine,\n      Path logPath,\n      long millisSinceEpochUTC,\n      SnapshotImpl latestSnapshot,\n      List<ParsedCatalogCommitData> catalogCommits) {\n    return DeltaHistoryManager.getActiveCommitAtTimestamp(\n            engine,\n            latestSnapshot,\n            logPath,\n            millisSinceEpochUTC,\n            false, /* mustBeRecreatable */\n            // e.g. if we give time T+2 and last commit has time T, then we DO want that last commit\n            true, /* canReturnLastCommit */\n            // e.g. we give time T-1 and first commit has time T, then do NOT want that earliest\n            // commit\n            false /* canReturnEarliestCommit */,\n            catalogCommits)\n        .getVersion();\n  }\n\n  /**\n   * Returns the latest commit that happened at or before {@code timestamp}.\n   *\n   * <p>If the timestamp is outside the range of [earliestCommit, latestCommit] then use parameters\n   * {@code canReturnLastCommit} and {@code canReturnEarliestCommit} to control whether an exception\n   * is thrown or the corresponding earliest/latest commit is returned.\n   *\n   * @param engine instance of {@link Engine} to use\n   * @param logPath the _delta_log path of the table\n   * @param timestamp the timestamp find the version for in milliseconds since the unix epoch\n   * @param mustBeRecreatable whether the state at the returned commit should be recreatable\n   * @param canReturnLastCommit whether we can return the latest version of the table if the\n   *     provided timestamp is after the latest commit\n   * @param canReturnEarliestCommit whether we can return the earliest version of the table if the\n   *     provided timestamp is before the earliest commit\n   * @param catalogCommits parsed log Deltas to use (must be sorted and contiguous)\n   * @throws KernelException if the provided timestamp is before the earliest commit and\n   *     canReturnEarliestCommit is false\n   * @throws KernelException if the provided timestamp is after the latest commit and\n   *     canReturnLastCommit is false\n   * @throws TableNotFoundException when there is no Delta table at the given path\n   */\n  public static Commit getActiveCommitAtTimestamp(\n      Engine engine,\n      SnapshotImpl latestSnapshot,\n      Path logPath,\n      long timestamp,\n      boolean mustBeRecreatable,\n      boolean canReturnLastCommit,\n      boolean canReturnEarliestCommit,\n      List<ParsedCatalogCommitData> catalogCommits)\n      throws TableNotFoundException {\n    // For now, we only accept *staged* ratified  commits (not inline)\n    LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(catalogCommits);\n\n    // Create a mapper for delta version -> file status that takes into account ratified commits\n    Function<Long, FileStatus> versionToFileStatusFunction =\n        getVersionToFileStatusFunction(catalogCommits, logPath);\n    Optional<Long> earliestRatifiedCommitVersion =\n        catalogCommits.stream().map(ParsedLogData::getVersion).min(Long::compare);\n\n    long earliestVersion =\n        (mustBeRecreatable)\n            ? getEarliestRecreatableCommit(engine, logPath, earliestRatifiedCommitVersion)\n            : getEarliestDeltaFile(engine, logPath, earliestRatifiedCommitVersion);\n\n    Commit placeholderEarliestCommit = new Commit(earliestVersion, -1L /* timestamp */);\n    Commit ictEnablementCommit = getICTEnablementCommit(latestSnapshot, placeholderEarliestCommit);\n    // Validate our assumptions: ICT must be enabled for all catalog-provided commits\n    earliestRatifiedCommitVersion.ifPresent(\n        v ->\n            checkArgument(\n                ictEnablementCommit.version <= v,\n                \"catalogManaged tables must have ICT enabled but given catalog-provided commit \"\n                    + \"with version < ictEnablementVersion\"));\n\n    Commit searchResult;\n    if (ictEnablementCommit.getTimestamp() <= timestamp) {\n      // The target commit is in the ICT range.\n      long latestSnapshotTimestamp = latestSnapshot.getTimestamp(engine);\n      if (latestSnapshotTimestamp <= timestamp) {\n        // We just proved we should use the latest snapshot\n        // Note that if `latestSnapshotTimestamp` is less than `timestamp`, we only\n        // return this search result if `canReturnLastCommit` is true.\n        // If `canReturnLastCommit` is false, we still need this commit to\n        // throw the timestampAfterLatestCommit error.\n        searchResult = new Commit(latestSnapshot.getVersion(), latestSnapshotTimestamp);\n      } else {\n        // start ICT search over [earliest available ICT version, latestVersion)\n        boolean ictEnabledForEntireWindow = (ictEnablementCommit.version <= earliestVersion);\n        long searchWindowLowerBound =\n            ictEnabledForEntireWindow\n                ? placeholderEarliestCommit.getVersion()\n                : ictEnablementCommit.getVersion();\n        try {\n          searchResult =\n              getActiveCommitAtTimeFromICTRange(\n                  timestamp,\n                  searchWindowLowerBound,\n                  latestSnapshot.getVersion(),\n                  engine,\n                  latestSnapshot.getLogPath(),\n                  versionToFileStatusFunction);\n        } catch (IOException e) {\n          throw new RuntimeException(\n              \"There was an error while reading a historical commit while performing a timestamp-\"\n                  + \"based lookup. This usually happens when there is a parallel operation like \"\n                  + \"metadata cleanup that is deleting commits. Please retry the query.\",\n              e);\n        }\n      }\n    } else {\n      // ICT was NOT enabled as-of the requested time\n      if (ictEnablementCommit.version <= earliestVersion) {\n        // We're searching for a non-ICT time but the non-ICT commits are all missing.\n        // If `canReturnEarliestCommit` is `false`, we need the details of the\n        // earliest commit to populate the timestampBeforeFirstAvailableCommit\n        // error correctly.\n        // Else, when `canReturnEarliestCommit` is `true`, the earliest commit\n        // is the desired result.\n        long ict =\n            CommitInfo.getRequiredIctFromDeltaFile(\n                engine,\n                logPath.getParent(),\n                versionToFileStatusFunction.apply(placeholderEarliestCommit.getVersion()),\n                placeholderEarliestCommit.getVersion());\n        searchResult = new Commit(placeholderEarliestCommit.getVersion(), ict);\n      } else {\n        // We know the table was not catalogManaged here since ICT was not enabled ==> we don't\n        // need to worry about catalogCommits\n        // start non-ICT linear search over [earliestVersion, )\n        List<Commit> commits = getCommits(engine, logPath, earliestVersion);\n        searchResult =\n            lastCommitBeforeOrAtTimestamp(commits, timestamp)\n                .orElse(\n                    commits.get(0)); // This is only returned if canReturnEarliestCommit (see below)\n      }\n    }\n\n    // If timestamp is before the earliest commit\n    if (searchResult.timestamp > timestamp && !canReturnEarliestCommit) {\n      throw DeltaErrors.timestampBeforeFirstAvailableCommit(\n          logPath.getParent().toString(), /* use dataPath */\n          timestamp,\n          searchResult.timestamp,\n          searchResult.version);\n    }\n    // If timestamp is after the last commit of the table\n    if (searchResult.version == latestSnapshot.getVersion()\n        && searchResult.timestamp < timestamp\n        && !canReturnLastCommit) {\n      throw DeltaErrors.timestampAfterLatestCommit(\n          logPath.getParent().toString(), /* use dataPath */\n          timestamp,\n          searchResult.timestamp,\n          searchResult.version);\n    }\n\n    return searchResult;\n  }\n\n  /**\n   * Finds the commit with the latest in-commit timestamp that is less than or equal to the\n   * searchTimestamp. All commits from `startCommitVersionInclusive` till\n   * `endCommitVersionInclusive` must have ICT enabled. Also, this method assumes that we have\n   * already proven that `searchTimestamp` is in the given range.\n   */\n  private static Commit getActiveCommitAtTimeFromICTRange(\n      long searchTimestamp,\n      long startCommitVersionInclusive,\n      long endCommitVersionInclusive,\n      Engine engine,\n      Path logPath,\n      Function<Long, FileStatus> versionToFileStatusFunction)\n      throws IOException {\n    // Now we have a range of commits to search through. We can use binary search to find the\n    // commit that is closest to the search timestamp.\n    Optional<Tuple2<Long, Long>> greatestLowerBoundOpt =\n        InCommitTimestampUtils.greatestLowerBound(\n            searchTimestamp,\n            startCommitVersionInclusive,\n            endCommitVersionInclusive,\n            version ->\n                CommitInfo.getRequiredIctFromDeltaFile(\n                    engine,\n                    logPath.getParent(),\n                    versionToFileStatusFunction.apply(version),\n                    version));\n    // This indicates that the search timestamp is less than the earliest commit.\n    if (!greatestLowerBoundOpt.isPresent()) {\n      long startIct =\n          CommitInfo.getRequiredIctFromDeltaFile(\n              engine,\n              logPath.getParent(),\n              versionToFileStatusFunction.apply(startCommitVersionInclusive),\n              startCommitVersionInclusive);\n      return new Commit(startCommitVersionInclusive, startIct);\n    }\n    Tuple2<Long, Long> greatestLowerBound = greatestLowerBoundOpt.get();\n    return new Commit(greatestLowerBound._1, greatestLowerBound._2);\n  }\n\n  /**\n   * Gets the commit that enabled in-commit timestamps.\n   *\n   * @param snapshot The latest snapshot of the table. This is used to determine when in-commit\n   *     timestamps were enabled.\n   * @param earliestCommit The earliest commit under consideration. If in-commit timestamps were\n   *     enabled for the entire history, this function will return this commit.\n   * @return The commit that enabled in-commit timestamps. If the table does not have in-commit\n   *     timestamps enabled, this will be the commit after the latest version. If in-commit\n   *     timestamps were enabled for the entire history, this will be `earliestCommit`.\n   */\n  private static Commit getICTEnablementCommit(SnapshotImpl snapshot, Commit earliestCommit) {\n    Metadata metadata = snapshot.getMetadata();\n    if (!IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(metadata)) {\n      // Pretend ICT will be enabled after the latest version and requested timestamp.\n      // This will force us to use the non-ICT search path.\n      return new Commit(snapshot.getVersion() + 1, Long.MAX_VALUE);\n    }\n    Optional<Long> enablementTimestampOpt =\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetadata(metadata);\n    Optional<Long> enablementVersionOpt =\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetadata(metadata);\n    if (enablementTimestampOpt.isPresent() && enablementVersionOpt.isPresent()) {\n      return new Commit(enablementVersionOpt.get(), enablementTimestampOpt.get());\n    } else if (!enablementTimestampOpt.isPresent() && !enablementVersionOpt.isPresent()) {\n      // This means that ICT has been enabled for the entire history.\n      return earliestCommit;\n    } else {\n      throw new IllegalStateException(\n          String.format(\n              \"Both %s and %s should be present or absent together\"\n                  + \"when inCommitTimestamp is enabled.\",\n              IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey(),\n              IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey()));\n    }\n  }\n\n  /**\n   * Gets the earliest commit that we can recreate. Note that this version isn't guaranteed to exist\n   * when performing an action as a concurrent operation can delete the file during cleanup. This\n   * value must be used as a lower bound.\n   *\n   * <p>We search for the earliest checkpoint we have, or whether we have the 0th delta file. This\n   * method assumes that the commits are contiguous.\n   */\n  public static long getEarliestRecreatableCommit(\n      Engine engine, Path logPath, Optional<Long> earliestRatifiedCommitVersion)\n      throws TableNotFoundException {\n    // For a catalogManaged table, the only time no published commits exist is when v0 has not yet\n    // been published. Otherwise, since checkpoints must have a published delta file, and log clean\n    // up must always preserve a checkpoint, there must be published commits present on the\n    // file-system\n    if (earliestRatifiedCommitVersion.isPresent() && earliestRatifiedCommitVersion.get() == 0) {\n      return 0;\n    }\n    // Thus, if there is no v0 ratified commit, then there must be published commit\n    try (CloseableIterator<FileStatus> files =\n        listFrom(engine, logPath, 0)\n            .filter(\n                fs ->\n                    FileNames.isCommitFile(getName(fs.getPath()))\n                        || FileNames.isCheckpointFile(getName(fs.getPath())))) {\n\n      if (!files.hasNext()) {\n        // listFrom already throws an error if the directory is truly empty, thus this must\n        // be because no files are checkpoint or delta files\n        throw new RuntimeException(\n            String.format(\"No delta files found in the directory: %s\", logPath));\n      }\n\n      // A map of checkpoint version and number of parts to number of parts observed\n      Map<Tuple2<Long, Integer>, Integer> checkpointMap = new HashMap<>();\n      long smallestDeltaVersion = Long.MAX_VALUE;\n      Optional<Long> lastCompleteCheckpoint = Optional.empty();\n\n      // Iterate through the log files - this will be in order starting from the lowest\n      // version. Checkpoint files come before deltas, so when we see a checkpoint, we\n      // remember it and return it once we detect that we've seen a smaller or equal delta\n      // version.\n      while (files.hasNext()) {\n        String nextFilePath = files.next().getPath();\n        if (FileNames.isCommitFile(getName(nextFilePath))) {\n          long version = FileNames.deltaVersion(nextFilePath);\n          if (version == 0L) {\n            return version;\n          }\n          smallestDeltaVersion = Math.min(version, smallestDeltaVersion);\n\n          // Note that we also check this condition at the end of the function - we check\n          // it here too to try and avoid more file listing when it's unnecessary.\n          if (lastCompleteCheckpoint.isPresent()\n              && lastCompleteCheckpoint.get() >= smallestDeltaVersion) {\n            return lastCompleteCheckpoint.get();\n          }\n        } else if (FileNames.isCheckpointFile(nextFilePath)) {\n          long checkpointVersion = FileNames.checkpointVersion(nextFilePath);\n          CheckpointInstance checkpointInstance = new CheckpointInstance(nextFilePath);\n          if (!checkpointInstance.numParts.isPresent()) {\n            lastCompleteCheckpoint = Optional.of(checkpointVersion);\n          } else {\n            // if we have a multi-part checkpoint, we need to check that all parts exist\n            int numParts = checkpointInstance.numParts.orElse(1);\n            int preCount = checkpointMap.getOrDefault(new Tuple2<>(checkpointVersion, numParts), 0);\n            if (numParts == preCount + 1) {\n              lastCompleteCheckpoint = Optional.of(checkpointVersion);\n            }\n            checkpointMap.put(new Tuple2<>(checkpointVersion, numParts), preCount + 1);\n          }\n        }\n      }\n\n      if (lastCompleteCheckpoint.isPresent()\n          && lastCompleteCheckpoint.get() >= smallestDeltaVersion) {\n        return lastCompleteCheckpoint.get();\n      } else if (smallestDeltaVersion < Long.MAX_VALUE) {\n        // This is a corrupt table where 000.json does not exist and there are no complete\n        // checkpoints OR the earliest complete checkpoint does not have a corresponding\n        // commit file (but there are other later commit files present)\n        throw new RuntimeException(String.format(\"No recreatable commits found at %s\", logPath));\n      } else {\n        throw new RuntimeException(String.format(\"No commits found at %s\", logPath));\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(\"Could not close iterator\", e);\n    }\n  }\n\n  /**\n   * Get the earliest commit available for this table. Note that this version isn't guaranteed to\n   * exist when performing an action as a concurrent operation can delete the file during cleanup.\n   * This value must be used as a lower bound.\n   */\n  public static long getEarliestDeltaFile(\n      Engine engine, Path logPath, Optional<Long> earliestRatifiedCommitVersion)\n      throws TableNotFoundException {\n    // For a catalogManaged table, the only time no published commits exist is when v0 has not yet\n    // been published. Otherwise, since checkpoints must have a published delta file, and log clean\n    // up must always preserve a checkpoint, there must be published commits present on the\n    // file-system\n    if (earliestRatifiedCommitVersion.isPresent() && earliestRatifiedCommitVersion.get() == 0) {\n      return 0;\n    }\n    // Thus, if there is no v0 ratified commit, then there must be published commit.\n    // Due to *ordered backfill* we know the following:\n    // minFSPublishedCommitVersion <= minCatalogProvidedCommitVersion\n    try (CloseableIterator<FileStatus> files =\n        listFrom(engine, logPath, 0).filter(fs -> FileNames.isCommitFile(getName(fs.getPath())))) {\n\n      if (files.hasNext()) {\n        return FileNames.deltaVersion(files.next().getPath());\n      } else {\n        // listFrom already throws an error if the directory is truly empty, thus this must\n        // be because no files are delta files\n        throw new RuntimeException(\n            String.format(\"No delta files found in the directory: %s\", logPath));\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(\"Could not close iterator\", e);\n    }\n  }\n\n  /**\n   * Returns an iterator containing a list of files found in the _delta_log directory starting with\n   * {@code startVersion}. Throws a {@link TableNotFoundException} if the directory doesn't exist or\n   * is empty.\n   */\n  private static CloseableIterator<FileStatus> listFrom(\n      Engine engine, Path logPath, long startVersion) throws TableNotFoundException {\n    Path tablePath = logPath.getParent();\n    try {\n      CloseableIterator<FileStatus> files =\n          wrapEngineExceptionThrowsIO(\n              () ->\n                  engine\n                      .getFileSystemClient()\n                      .listFrom(FileNames.listingPrefix(logPath, startVersion)),\n              \"Listing files in the delta log starting from %s\",\n              FileNames.listingPrefix(logPath, startVersion));\n\n      if (!files.hasNext()) {\n        // We treat an empty directory as table not found\n        throw new TableNotFoundException(tablePath.toString());\n      }\n      return files;\n    } catch (FileNotFoundException e) {\n      throw new TableNotFoundException(tablePath.toString());\n    } catch (IOException io) {\n      throw new UncheckedIOException(\"Failed to list the files in delta log\", io);\n    }\n  }\n\n  /**\n   * Returns the commit version and timestamps of all commits starting from version {@code start}.\n   * Guarantees that the commits returned have both monotonically increasing versions and\n   * timestamps.\n   */\n  private static List<Commit> getCommits(Engine engine, Path logPath, long start)\n      throws TableNotFoundException {\n    CloseableIterator<Commit> commits =\n        listFrom(engine, logPath, start)\n            .filter(fs -> FileNames.isCommitFile(getName(fs.getPath())))\n            .map(fs -> new Commit(FileNames.deltaVersion(fs.getPath()), fs.getModificationTime()));\n    return monotonizeCommitTimestamps(commits);\n  }\n\n  /**\n   * Makes sure that the commit timestamps are monotonically increasing with respect to commit\n   * versions. Requires the input commits to be sorted by the commit version.\n   */\n  private static List<Commit> monotonizeCommitTimestamps(CloseableIterator<Commit> commits) {\n    List<Commit> monotonizedCommits = new ArrayList<>();\n    long prevTimestamp = Long.MIN_VALUE;\n    long prevVersion = Long.MIN_VALUE;\n    while (commits.hasNext()) {\n      Commit newElem = commits.next();\n      assert (prevVersion < newElem.version); // Verify commits are ordered\n      if (prevTimestamp >= newElem.timestamp) {\n        logger.warn(\n            \"Found Delta commit {} with a timestamp {} which is greater than the next \"\n                + \"commit timestamp {}.\",\n            prevVersion,\n            prevTimestamp,\n            newElem.timestamp);\n        newElem = new Commit(newElem.version, prevTimestamp + 1);\n      }\n      monotonizedCommits.add(newElem);\n      prevTimestamp = newElem.timestamp;\n      prevVersion = newElem.version;\n    }\n    return monotonizedCommits;\n  }\n\n  /** Returns the latest commit that happened at or before {@code timestamp} */\n  private static Optional<Commit> lastCommitBeforeOrAtTimestamp(\n      List<Commit> commits, long timestamp) {\n    int i = -1;\n    while (i + 1 < commits.size() && commits.get(i + 1).timestamp <= timestamp) {\n      i++;\n    }\n    return Optional.ofNullable((i < 0) ? null : commits.get(i));\n  }\n\n  public static class Commit {\n\n    private final long version;\n    private final long timestamp;\n\n    Commit(long version, long timestamp) {\n      this.version = version;\n      this.timestamp = timestamp;\n    }\n\n    public long getVersion() {\n      return version;\n    }\n\n    public long getTimestamp() {\n      return timestamp;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n      if (this == o) {\n        return true;\n      }\n      if (o == null || getClass() != o.getClass()) {\n        return false;\n      }\n      Commit other = (Commit) o;\n      return Objects.equals(version, other.version) && Objects.equals(timestamp, other.timestamp);\n    }\n\n    @Override\n    public int hashCode() {\n      return Objects.hash(version, timestamp);\n    }\n  }\n\n  /**\n   * Returns a function for resolving version-to-file-status given a list of ratified staged\n   * commits. We prefer to read a ratified commit over the published file whenever it is present.\n   *\n   * <p>Note, this assumes that for any version provided to the function, it has already been\n   * validated that the version _should_ exist by listing the _delta_log and finding the earliest\n   * available commit.\n   *\n   * <p>DeltaHistoryManager doesn't, and has never, done a full LIST of the _delta_log. Instead, it\n   * only lists to find the earliest commit, exists early, and then relies on the assumption that\n   * any commits after the earliest commit exist and are contiguous. With CCV2, we continue this\n   * assumption, such that for any version after the earliest available commit, the commit must\n   * exist either in the list of ratified commits provided by the catalog or on the file-system.\n   */\n  private static Function<Long, FileStatus> getVersionToFileStatusFunction(\n      List<ParsedCatalogCommitData> catalogCommits, Path logPath) {\n    Map<Long, FileStatus> versionToFileStatusMap = new HashMap<>();\n    for (ParsedCatalogCommitData catalogCommit : catalogCommits) {\n      versionToFileStatusMap.put(catalogCommit.getVersion(), catalogCommit.getFileStatus());\n    }\n    return version -> {\n      if (versionToFileStatusMap.containsKey(version)) {\n        return versionToFileStatusMap.get(version);\n      } else {\n        return FileStatus.of(\n            FileNames.deltaFile(logPath, version), /* path */\n            0, /* size */\n            0 /* modification time */);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/DeltaLogActionUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.*;\nimport static io.delta.kernel.internal.fs.Path.getName;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.InvalidTableException;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.ListUtils;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.FileNames.DeltaLogFileType;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.CloseableIterator.BreakableFilterResult;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Exposes APIs to read the raw actions within the *commit files* of the _delta_log. This is used\n * for CDF, streaming, and more.\n */\npublic class DeltaLogActionUtils {\n\n  private DeltaLogActionUtils() {}\n\n  private static final Logger logger = LoggerFactory.getLogger(DeltaLogActionUtils.class);\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  /**\n   * Represents a Delta action. This is used to request which actions to read from the commit files\n   * in {@link TableImpl#getChanges(Engine, long, long, Set)}.\n   *\n   * <p>See the Delta protocol for more details\n   * https://github.com/delta-io/delta/blob/master/PROTOCOL.md#actions\n   */\n  public enum DeltaAction {\n    REMOVE(\"remove\", RemoveFile.FULL_SCHEMA),\n    ADD(\"add\", AddFile.FULL_SCHEMA),\n    METADATA(\"metaData\", Metadata.FULL_SCHEMA),\n    PROTOCOL(\"protocol\", Protocol.FULL_SCHEMA),\n    COMMITINFO(\"commitInfo\", CommitInfo.FULL_SCHEMA),\n    CDC(\"cdc\", AddCDCFile.FULL_SCHEMA),\n    TXN(\"txn\", SetTransaction.FULL_SCHEMA),\n    DOMAINMETADATA(\"domainMetadata\", DomainMetadata.FULL_SCHEMA);\n\n    public final String colName;\n    public final StructType schema;\n\n    DeltaAction(String colName, StructType schema) {\n      this.colName = colName;\n      this.schema = schema;\n    }\n  }\n\n  /**\n   * For a table get the list of commit log files for the provided version range.\n   *\n   * @param tablePath path for the given table\n   * @param startVersion start version of the range (inclusive)\n   * @param endVersionOpt end version of the range (inclusive)\n   * @return the list of commit files in increasing order between startVersion and endVersion\n   * @throws TableNotFoundException if the table does not exist or if it is not a delta table\n   * @throws KernelException if a commit file does not exist for any of the versions in the provided\n   *     range\n   * @throws KernelException if provided an invalid version range\n   */\n  public static List<FileStatus> getCommitFilesForVersionRange(\n      Engine engine, Path tablePath, long startVersion, Optional<Long> endVersionOpt) {\n    logger.info(\n        \"{}: Getting the commit files for versions [{}, {}]\",\n        tablePath,\n        startVersion,\n        endVersionOpt);\n\n    // Validate arguments\n    endVersionOpt.ifPresent(\n        endVersion -> {\n          if (startVersion < 0 || endVersion < startVersion) {\n            throw invalidVersionRange(startVersion, endVersion);\n          }\n        });\n\n    // Get any available commit files within the version range\n    final List<FileStatus> commitFiles =\n        listDeltaLogFilesAsIter(\n                engine,\n                Collections.singleton(DeltaLogFileType.COMMIT),\n                tablePath,\n                startVersion,\n                endVersionOpt,\n                false /* mustBeRecreatable */)\n            .toInMemoryList();\n\n    // There are no available commit files within the version range.\n    // This can be due to (1) an empty directory, (2) no valid delta files in the directory,\n    // (3) only delta files less than startVersion prefix (4) only delta files after endVersion\n    if (commitFiles.isEmpty()) {\n      throw noCommitFilesFoundForVersionRange(tablePath.toString(), startVersion, endVersionOpt);\n    }\n\n    // Verify commit files found\n    // (check that they are continuous and start with startVersion and end with endVersion)\n    verifyDeltaVersions(commitFiles, startVersion, endVersionOpt, tablePath);\n\n    return commitFiles;\n  }\n\n  /**\n   * Returns a {@link CloseableIterator} of files of type $fileTypes in the _delta_log directory of\n   * the given $tablePath, in increasing order from $startVersion to the optional $endVersion.\n   *\n   * @throws TableNotFoundException if the table or its _delta_log does not exist\n   * @throws KernelException if mustBeRecreatable is true, endVersionOpt is present, and the\n   *     _delta_log history has been truncated so that we cannot load the desired end version\n   */\n  public static CloseableIterator<FileStatus> listDeltaLogFilesAsIter(\n      Engine engine,\n      Set<DeltaLogFileType> fileTypes,\n      Path tablePath,\n      long startVersion,\n      Optional<Long> endVersionOpt,\n      boolean mustBeRecreatable) {\n    checkArgument(!fileTypes.isEmpty(), \"At least one file type must be provided\");\n\n    endVersionOpt.ifPresent(\n        endVersion -> {\n          checkArgument(\n              endVersion >= startVersion,\n              \"endVersion=%s provided is less than startVersion=%s\",\n              endVersion,\n              startVersion);\n        });\n\n    final Path logPath = new Path(tablePath, \"_delta_log\");\n\n    logger.info(\n        \"Listing log files types={} in path={} starting from {} and ending with {}\",\n        fileTypes,\n        logPath,\n        startVersion,\n        endVersionOpt);\n\n    // This variable is used to help determine if we should throw an error if the table history is\n    // not reconstructable. Only commit and checkpoint files are applicable.\n    // Must be final to be used in lambda\n    final AtomicBoolean hasReturnedCommitOrCheckpoint = new AtomicBoolean(false);\n\n    return listLogDir(engine, tablePath, startVersion)\n        .breakableFilter(\n            fs -> {\n              String fileName = getName(fs.getPath());\n              if (fileTypes.contains(DeltaLogFileType.COMMIT) && FileNames.isCommitFile(fileName)) {\n                // Here, we do nothing (we will consume this file).\n              } else if (fileTypes.contains(DeltaLogFileType.LOG_COMPACTION)\n                  && FileNames.isLogCompactionFile(fileName)) {\n                // Here, we do nothing (we will consume this file).\n              } else if (fileTypes.contains(DeltaLogFileType.CHECKPOINT)\n                  && FileNames.isCheckpointFile(fileName)\n                  && fs.getSize() > 0) {\n                // Checkpoint files of 0 size are invalid but may be ignored silently when read,\n                // hence we ignore them so that we never pick up such checkpoints.\n                // Here, we do nothing (we will consume this file).\n              } else if (fileTypes.contains(DeltaLogFileType.CHECKSUM)\n                  && FileNames.isChecksumFile(fileName)) {\n                // Here, we do nothing (we will consume this file).\n              } else {\n                logger.debug(\"Ignoring file {} as it is not of the desired type\", fs.getPath());\n                return BreakableFilterResult.EXCLUDE; // Here, we exclude and filter out this file.\n              }\n\n              final long fileVersion;\n              if (FileNames.isLogCompactionFile(fileName)) {\n                Tuple2<Long, Long> compactionVersions =\n                    FileNames.logCompactionVersions(new Path(fs.getPath()));\n                // We use start version here. Below this is used to determine if we should stop\n                // listing because we've listed past the required version. But with a log compaction\n                // file, if the end version is passed the requested version, we don't want to stop,\n                // we just won't use the compaction file.\n                fileVersion = compactionVersions._1;\n\n                // Now check if the compaction end version is too far in the future, and don't\n                // include this file if it is\n                if (endVersionOpt.isPresent()) {\n                  final long endVersion = endVersionOpt.get();\n                  if (compactionVersions._2 > endVersion) {\n                    logger.debug(\n                        \"Excluding compaction file as it covers past the end version {}\", fileName);\n                    return BreakableFilterResult.EXCLUDE;\n                  }\n                }\n              } else {\n                fileVersion = FileNames.getFileVersion(new Path(fs.getPath()));\n              }\n\n              if (fileVersion < startVersion) {\n                throw new RuntimeException(\n                    String.format(\n                        \"Listing files in %s with startVersion %s yet found file %s.\",\n                        logPath, startVersion, fs.getPath()));\n              }\n\n              if (endVersionOpt.isPresent()) {\n                final long endVersion = endVersionOpt.get();\n\n                if (fileVersion > endVersion) {\n                  if (mustBeRecreatable && !hasReturnedCommitOrCheckpoint.get()) {\n                    final long earliestVersion =\n                        DeltaHistoryManager.getEarliestRecreatableCommit(\n                            engine, logPath, Optional.empty());\n                    throw DeltaErrors.versionBeforeFirstAvailableCommit(\n                        tablePath.toString(), endVersion, earliestVersion);\n                  } else {\n                    logger.debug(\n                        \"Stopping listing; found file {} with version greater than endVersion {}\",\n                        fs.getPath(),\n                        endVersion);\n                    return BreakableFilterResult.BREAK;\n                  }\n                }\n              }\n\n              if (FileNames.isCommitFile(fileName)\n                  || FileNames.isCheckpointFile(fileName)\n                  || FileNames.isLogCompactionFile(fileName)) {\n                hasReturnedCommitOrCheckpoint.set(true);\n              }\n\n              return BreakableFilterResult.INCLUDE;\n            });\n  }\n\n  /**\n   * Returns CommitActions for each commit file. CommitActions are ordered by increasing version.\n   *\n   * <p>This function automatically:\n   *\n   * <ul>\n   *   <li>Performs protocol validation by reading and validating the protocol action\n   *   <li>Extracts commit timestamp using inCommitTimestamp if available, otherwise file\n   *       modification time\n   *   <li>Filters out protocol and commitInfo actions if not requested in actionSet\n   * </ul>\n   */\n  public static CloseableIterator<CommitActions> getActionsFromCommitFilesWithProtocolValidation(\n      Engine engine,\n      String tablePath,\n      List<FileStatus> commitFiles,\n      Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n\n    // For each commit file, create a CommitActions\n    return toCloseableIterator(commitFiles.iterator())\n        .map(commitFile -> new CommitActionsImpl(engine, commitFile, tablePath, actionSet));\n  }\n\n  //////////////////////\n  // Private helpers //\n  /////////////////////\n\n  /** Column name storing the commit version for a given file action */\n  private static final String COMMIT_VERSION_COL_NAME = \"version\";\n\n  private static final DataType COMMIT_VERSION_DATA_TYPE = LongType.LONG;\n  private static final StructField COMMIT_VERSION_STRUCT_FIELD =\n      new StructField(COMMIT_VERSION_COL_NAME, COMMIT_VERSION_DATA_TYPE, false /* nullable */);\n\n  /** Column name storing the commit timestamp for a given file action */\n  private static final String COMMIT_TIMESTAMP_COL_NAME = \"timestamp\";\n\n  private static final DataType COMMIT_TIMESTAMP_DATA_TYPE = LongType.LONG;\n  private static final StructField COMMIT_TIMESTAMP_STRUCT_FIELD =\n      new StructField(COMMIT_TIMESTAMP_COL_NAME, COMMIT_TIMESTAMP_DATA_TYPE, false /* nullable */);\n\n  /**\n   * Given a list of delta versions, verifies that they are (1) contiguous (2) versions starts with\n   * expectedStartVersion and (3) end with expectedEndVersion. Throws an exception if any of these\n   * are not true.\n   *\n   * <p>Public to expose for testing only.\n   *\n   * @param commitFiles in sorted increasing order according to the commit version\n   */\n  static void verifyDeltaVersions(\n      List<FileStatus> commitFiles,\n      long expectedStartVersion,\n      Optional<Long> expectedEndVersionOpt,\n      Path tablePath) {\n\n    List<Long> commitVersions =\n        commitFiles.stream()\n            .map(fs -> FileNames.deltaVersion(new Path(fs.getPath())))\n            .collect(Collectors.toList());\n\n    for (int i = 1; i < commitVersions.size(); i++) {\n      if (commitVersions.get(i) != commitVersions.get(i - 1) + 1) {\n        throw new InvalidTableException(\n            tablePath.toString(),\n            String.format(\n                \"Missing delta files: versions are not contiguous: (%s)\", commitVersions));\n      }\n    }\n\n    if (commitVersions.isEmpty() || !Objects.equals(commitVersions.get(0), expectedStartVersion)) {\n      throw startVersionNotFound(\n          tablePath.toString(),\n          expectedStartVersion,\n          commitVersions.isEmpty() ? Optional.empty() : Optional.of(commitVersions.get(0)));\n    }\n\n    expectedEndVersionOpt.ifPresent(\n        expectedEndVersion -> {\n          if (!Objects.equals(ListUtils.getLast(commitVersions), expectedEndVersion)) {\n            throw endVersionNotFound(\n                tablePath.toString(), expectedEndVersion, ListUtils.getLast(commitVersions));\n          }\n        });\n  }\n\n  /**\n   * Gets an iterator of files in the _delta_log directory starting with the startVersion.\n   *\n   * @throws TableNotFoundException if the directory does not exist\n   */\n  private static CloseableIterator<FileStatus> listLogDir(\n      Engine engine, Path tablePath, long startVersion) {\n    final Path logPath = new Path(tablePath, \"_delta_log\");\n    try {\n      return wrapEngineExceptionThrowsIO(\n          () ->\n              engine.getFileSystemClient().listFrom(FileNames.listingPrefix(logPath, startVersion)),\n          \"Listing from %s\",\n          FileNames.listingPrefix(logPath, startVersion));\n    } catch (FileNotFoundException e) {\n      // Did not find the _delta_log directory.\n      throw new TableNotFoundException(tablePath.toString());\n    } catch (IOException io) {\n      throw new UncheckedIOException(\"Failed to list the files in delta log\", io);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/InternalScanFileUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport io.delta.kernel.Scan;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.FileStatus;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Utilities to extract information out of the scan file rows returned by {@link\n * Scan#getScanFiles(Engine)}.\n */\npublic class InternalScanFileUtils {\n  private InternalScanFileUtils() {}\n\n  private static final String TABLE_ROOT_COL_NAME = \"tableRoot\";\n  private static final DataType TABLE_ROOT_DATA_TYPE = StringType.STRING;\n\n  /** {@link Column} expression referring to the `partitionValues` in scan `add` file. */\n  public static final Column ADD_FILE_PARTITION_COL_REF =\n      new Column(new String[] {\"add\", \"partitionValues\"});\n\n  public static StructField TABLE_ROOT_STRUCT_FIELD =\n      new StructField(TABLE_ROOT_COL_NAME, TABLE_ROOT_DATA_TYPE, false /* nullable */);\n\n  // TODO update this when stats columns are dropped from the returned scan files\n  /**\n   * Schema of the returned scan files. May have an additional column \"add.stats\" at the end of the\n   * \"add\" columns that is not represented in the schema here. This column is conditionally read\n   * when a valid data skipping filter can be generated.\n   */\n  public static final StructType SCAN_FILE_SCHEMA =\n      new StructType()\n          .add(\"add\", AddFile.SCHEMA_WITHOUT_STATS)\n          // NOTE: table root is temporary, until the path in `add.path` is converted to\n          // an absolute path. https://github.com/delta-io/delta/issues/2089\n          .add(TABLE_ROOT_STRUCT_FIELD);\n\n  /**\n   * Schema of the returned scan files when {@link ScanImpl#getScanFiles(Engine, boolean)} is called\n   * with {@code includeStats=true}.\n   */\n  public static final StructType SCAN_FILE_SCHEMA_WITH_STATS =\n      new StructType().add(\"add\", AddFile.SCHEMA_WITH_STATS).add(TABLE_ROOT_STRUCT_FIELD);\n\n  public static final int ADD_FILE_ORDINAL = SCAN_FILE_SCHEMA.indexOf(\"add\");\n\n  private static final StructType ADD_FILE_SCHEMA =\n      (StructType) SCAN_FILE_SCHEMA.get(\"add\").getDataType();\n\n  private static final int ADD_FILE_PATH_ORDINAL = ADD_FILE_SCHEMA.indexOf(\"path\");\n\n  private static final int ADD_FILE_PARTITION_VALUES_ORDINAL =\n      ADD_FILE_SCHEMA.indexOf(\"partitionValues\");\n\n  private static final int ADD_FILE_SIZE_ORDINAL = ADD_FILE_SCHEMA.indexOf(\"size\");\n\n  private static final int ADD_FILE_MOD_TIME_ORDINAL = ADD_FILE_SCHEMA.indexOf(\"modificationTime\");\n\n  private static final int ADD_FILE_DATA_CHANGE_ORDINAL = ADD_FILE_SCHEMA.indexOf(\"dataChange\");\n\n  private static final int ADD_FILE_DV_ORDINAL = ADD_FILE_SCHEMA.indexOf(\"deletionVector\");\n\n  private static final int TABLE_ROOT_ORDINAL = SCAN_FILE_SCHEMA.indexOf(TABLE_ROOT_COL_NAME);\n\n  public static final int ADD_FILE_STATS_ORDINAL = AddFile.SCHEMA_WITH_STATS.indexOf(\"stats\");\n\n  /**\n   * Get the {@link FileStatus} of {@code AddFile} from given scan file {@link Row}. The {@link\n   * FileStatus} contains file metadata about the file.\n   *\n   * @param scanFileInfo {@link Row} representing one scan file.\n   * @return a {@link FileStatus} object created from the given scan file row.\n   */\n  public static FileStatus getAddFileStatus(Row scanFileInfo) {\n    Row addFile = getAddFileEntry(scanFileInfo);\n    String path = addFile.getString(ADD_FILE_PATH_ORDINAL);\n    long size = addFile.getLong(ADD_FILE_SIZE_ORDINAL);\n    long modificationTime = addFile.getLong(ADD_FILE_MOD_TIME_ORDINAL);\n\n    // TODO: this is hack until the path in `add.path` is converted to an absolute path\n    String tableRoot = scanFileInfo.getString(TABLE_ROOT_ORDINAL);\n    String absolutePath =\n        new Path(new Path(URI.create(tableRoot)), new Path(URI.create(path))).toString();\n\n    return FileStatus.of(absolutePath, size, modificationTime);\n  }\n\n  /**\n   * Get the partition columns and values belonging to the {@code AddFile} from given scan file row.\n   *\n   * @param scanFileInfo {@link Row} representing one scan file.\n   * @return Map of partition column name to partition column value.\n   */\n  public static Map<String, String> getPartitionValues(Row scanFileInfo) {\n    Row addFile = getAddFileEntry(scanFileInfo);\n    return VectorUtils.toJavaMap(addFile.getMap(ADD_FILE_PARTITION_VALUES_ORDINAL));\n  }\n\n  /**\n   * Helper method to get the {@code AddFile} struct from the scan file row.\n   *\n   * @param scanFileInfo\n   * @return {@link Row} representing the {@code AddFile}\n   * @throws IllegalArgumentException If the scan file row doesn't contain {@code add} file entry.\n   */\n  protected static Row getAddFileEntry(Row scanFileInfo) {\n    if (scanFileInfo.isNullAt(ADD_FILE_ORDINAL)) {\n      throw new IllegalArgumentException(\"There is no `add` entry in the scan file row\");\n    }\n    return scanFileInfo.getStruct(ADD_FILE_ORDINAL);\n  }\n\n  /**\n   * Create a scan file row conforming to the schema {@link #SCAN_FILE_SCHEMA} for given file\n   * status. This is used when creating the ScanFile row for reading commit or checkpoint files.\n   *\n   * @param fileStatus\n   * @return\n   */\n  public static Row generateScanFileRow(FileStatus fileStatus) {\n    Row addFile =\n        new GenericRow(\n            ADD_FILE_SCHEMA,\n            new HashMap<Integer, Object>() {\n              {\n                put(ADD_FILE_PATH_ORDINAL, fileStatus.getPath());\n                put(ADD_FILE_PARTITION_VALUES_ORDINAL, null); // partitionValues\n                put(ADD_FILE_SIZE_ORDINAL, fileStatus.getSize());\n                put(ADD_FILE_MOD_TIME_ORDINAL, fileStatus.getModificationTime());\n                put(ADD_FILE_DATA_CHANGE_ORDINAL, null); // dataChange\n                put(ADD_FILE_DV_ORDINAL, null); // deletionVector\n              }\n            });\n\n    return new GenericRow(\n        SCAN_FILE_SCHEMA,\n        new HashMap<Integer, Object>() {\n          {\n            put(ADD_FILE_ORDINAL, addFile);\n            put(TABLE_ROOT_ORDINAL, \"/\");\n          }\n        });\n  }\n\n  /**\n   * Create a {@link DeletionVectorDescriptor} from {@code add} entry in the given scan file row.\n   *\n   * @param scanFile {@link Row} representing one scan file.\n   * @return\n   */\n  public static DeletionVectorDescriptor getDeletionVectorDescriptorFromRow(Row scanFile) {\n    Row addFile = getAddFileEntry(scanFile);\n    return DeletionVectorDescriptor.fromRow(addFile.getStruct(ADD_FILE_DV_ORDINAL));\n  }\n\n  /**\n   * Get a references column for given partition column name in partitionValues_parsed column in\n   * scan file row.\n   *\n   * @param partitionColName Partition column name\n   * @return {@link Column} reference\n   */\n  public static Column getPartitionValuesParsedRefInAddFile(String partitionColName) {\n    return new Column(new String[] {\"add\", \"partitionValues_parsed\", partitionColName});\n  }\n\n  public static Optional<Long> getBaseRowId(Row scanFile) {\n    Row addFileRow = getAddFileEntry(scanFile);\n    return new AddFile(addFileRow).getBaseRowId();\n  }\n\n  public static Optional<Long> getDefaultRowCommitVersion(Row scanFile) {\n    Row addFileRow = getAddFileEntry(scanFile);\n    return new AddFile(addFileRow).getDefaultRowCommitVersion();\n  }\n\n  public static String getFilePath(Row scanFile) {\n    Row addFileRow = getAddFileEntry(scanFile);\n    return new AddFile(addFileRow).getPath();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/PaginatedScanImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal;\n\nimport io.delta.kernel.PaginatedScan;\nimport io.delta.kernel.PaginatedScanFilesIterator;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.replay.PageToken;\nimport io.delta.kernel.internal.replay.PaginatedScanFilesIteratorImpl;\nimport io.delta.kernel.internal.replay.PaginationContext;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.util.Optional;\n\n/** Implementation of {@link PaginatedScan} */\npublic class PaginatedScanImpl implements PaginatedScan {\n  private final PaginationContext paginationContext;\n  private final ScanImpl baseScan;\n  private final long pageSize;\n  private final Optional<PageToken> pageTokenOpt;\n\n  public PaginatedScanImpl(\n      ScanImpl baseScan,\n      String tablePath,\n      long tableVersion,\n      long pageSize,\n      LogSegment logSegment,\n      Optional<Predicate> predicate,\n      Optional<Row> pageTokenRowOpt) {\n    this.baseScan = baseScan;\n    this.pageSize = pageSize;\n    this.pageTokenOpt = pageTokenRowOpt.map(PageToken::fromRow);\n    // TODO: get hash value of predicate & log segment and check values in pagination context\n    this.paginationContext =\n        pageTokenOpt\n            .map(\n                token ->\n                    PaginationContext.forPageWithPageToken(\n                        tablePath,\n                        tableVersion,\n                        logSegment.hashCode(),\n                        predicate.hashCode(),\n                        pageSize,\n                        token))\n            .orElseGet(\n                () ->\n                    PaginationContext.forFirstPage(\n                        tablePath,\n                        tableVersion,\n                        logSegment.hashCode(),\n                        predicate.hashCode(),\n                        pageSize));\n  }\n\n  @Override\n  public Optional<Predicate> getRemainingFilter() {\n    return baseScan.getRemainingFilter();\n  }\n\n  @Override\n  public Row getScanState(Engine engine) {\n    return baseScan.getScanState(engine);\n  }\n\n  @Override\n  public PaginatedScanFilesIterator getScanFiles(Engine engine) {\n    return this.getScanFiles(engine, false /* include stats */);\n  }\n\n  public PaginatedScanFilesIterator getScanFiles(Engine engine, boolean includeStates) {\n    CloseableIterator<FilteredColumnarBatch> filteredScanFilesIter =\n        baseScan.getScanFiles(engine, includeStates, Optional.of(paginationContext));\n    return new PaginatedScanFilesIteratorImpl(filteredScanFilesIter, paginationContext);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/ReplaceTableTransactionBuilderImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.requireSchemaForReplaceTable;\n\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport java.util.Optional;\n\npublic class ReplaceTableTransactionBuilderImpl extends TransactionBuilderImpl {\n\n  public ReplaceTableTransactionBuilderImpl(TableImpl table, String engineInfo) {\n    super(table, engineInfo, Operation.REPLACE_TABLE);\n  }\n\n  @Override\n  public Transaction build(Engine engine) {\n    try {\n      withMaxRetries(0); // We don't support conflict resolution yet so disable retries for now\n      schema.orElseThrow(() -> requireSchemaForReplaceTable());\n      SnapshotImpl snapshot = table.getLatestSnapshot(engine);\n      return buildTransactionInternal(engine, true, Optional.of(snapshot));\n    } catch (TableNotFoundException tblf) {\n      throw new TableNotFoundException(\n          tblf.getTablePath(), \"Trying to replace a table that does not exist.\");\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/ReplaceTableTransactionBuilderV2Impl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Collections.emptyMap;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.transaction.DataLayoutSpec;\nimport io.delta.kernel.transaction.ReplaceTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\n\npublic class ReplaceTableTransactionBuilderV2Impl implements ReplaceTableTransactionBuilder {\n\n  /**\n   * Delta-specific properties that should be preserved during REPLACE operations, unless their\n   * value is specifically set (overridden) during the REPLACE. All other properties should be\n   * reset.\n   *\n   * <p>For example, suppose at the time of REPLACE the table has property 'delta.foo' = 'bar' and\n   * that such property is included in this set.\n   *\n   * <ul>\n   *   <li>If the REPLACE statement does not specify 'delta.foo', then the new table will still have\n   *       'delta.foo' = 'bar'.\n   *   <li>If the REPLACE statement specifies 'delta.foo' = 'baz', then the new table will of course\n   *       have 'delta.foo' = 'baz'.\n   * </ul>\n   */\n  static final Set<String> TABLE_PROPERTY_KEYS_TO_PRESERVE =\n      new HashSet<String>() {\n        {\n          add(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.getKey());\n\n          // Must retain all ICT properties, else a client would not know when ICT was enabled,\n          // which could result in a failed query or incorrect results.\n          //\n          // If ICT is explicitly disabled during REPLACE (or during any operation), we should then\n          // explicitly remove the ICT enablement version and timestamp properties.\n          add(TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey());\n          add(TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey());\n          add(TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey());\n        }\n      };\n\n  private final SnapshotImpl snapshot;\n  private final StructType schema;\n  private final String engineInfo;\n\n  private Optional<Map<String, String>> tableProperties = Optional.empty();\n  private Optional<DataLayoutSpec> dataLayoutSpec = Optional.empty();\n  private Optional<Integer> userProvidedMaxRetries = Optional.empty();\n\n  public ReplaceTableTransactionBuilderV2Impl(\n      SnapshotImpl snapshot, StructType schema, String engineInfo) {\n    this.snapshot = requireNonNull(snapshot, \"snapshot is null\");\n    this.schema = requireNonNull(schema, \"schema is null\");\n    this.engineInfo = requireNonNull(engineInfo, \"engineInfo is null\");\n    TableFeatures.validateKernelCanWriteToTable(\n        snapshot.getProtocol(), snapshot.getMetadata(), snapshot.getPath());\n  }\n\n  @Override\n  public ReplaceTableTransactionBuilder withTableProperties(Map<String, String> properties) {\n    requireNonNull(properties, \"properties cannot be null\");\n    this.tableProperties =\n        Optional.of(\n            java.util.Collections.unmodifiableMap(\n                TableConfig.validateAndNormalizeDeltaProperties(properties)));\n    return this;\n  }\n\n  @Override\n  public ReplaceTableTransactionBuilder withDataLayoutSpec(DataLayoutSpec spec) {\n    requireNonNull(spec, \"spec cannot be null\");\n    this.dataLayoutSpec = Optional.of(spec);\n    return this;\n  }\n\n  @Override\n  public ReplaceTableTransactionBuilder withMaxRetries(int maxRetries) {\n    checkArgument(maxRetries >= 0, \"maxRetries must be >= 0\");\n    this.userProvidedMaxRetries = Optional.of(maxRetries);\n    return this;\n  }\n\n  @Override\n  public Transaction build(Engine engine) {\n    requireNonNull(engine, \"engine cannot be null\");\n\n    Optional<List<String>> partitionColumns =\n        dataLayoutSpec\n            .filter(DataLayoutSpec::hasPartitioning)\n            .map(DataLayoutSpec::getPartitionColumnsAsStrings);\n\n    Optional<List<Column>> clusteringColumns =\n        dataLayoutSpec\n            .filter(DataLayoutSpec::hasClustering)\n            .map(DataLayoutSpec::getClusteringColumns);\n\n    TransactionMetadataFactory.Output txnMetadata =\n        TransactionMetadataFactory.buildReplaceTableMetadata(\n            snapshot.getPath(),\n            snapshot,\n            schema,\n            tableProperties.orElse(emptyMap()),\n            partitionColumns,\n            clusteringColumns);\n\n    return new TransactionImpl(\n        true, // isCreateOrReplace\n        snapshot.getDataPath(),\n        Optional.of(snapshot),\n        engineInfo,\n        Operation.REPLACE_TABLE,\n        txnMetadata.newProtocol,\n        txnMetadata.newMetadata,\n        snapshot.getCommitter(),\n        Optional.empty(), // no setTransaction for replace table\n        txnMetadata.physicalNewClusteringColumns,\n        // We don't support conflict resolution yet for replace so disable retries for now\n        Optional.of(0),\n        0, // logCompactionInterval\n        System::currentTimeMillis);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/ScanBuilderImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal;\n\nimport io.delta.kernel.PaginatedScan;\nimport io.delta.kernel.ScanBuilder;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.replay.LogReplay;\nimport io.delta.kernel.metrics.SnapshotReport;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\n\n/** Implementation of {@link ScanBuilder}. */\npublic class ScanBuilderImpl implements ScanBuilder {\n\n  private final Path dataPath;\n  private final long tableVersion;\n  private final Protocol protocol;\n  private final Metadata metadata;\n  private final StructType snapshotSchema;\n  private final LogReplay logReplay;\n  private final SnapshotReport snapshotReport;\n\n  private StructType readSchema;\n  private Optional<Predicate> predicate;\n\n  public ScanBuilderImpl(\n      Path dataPath,\n      long tableVersion,\n      Protocol protocol,\n      Metadata metadata,\n      StructType snapshotSchema,\n      LogReplay logReplay,\n      SnapshotReport snapshotReport) {\n    this.dataPath = dataPath;\n    this.tableVersion = tableVersion;\n    this.protocol = protocol;\n    this.metadata = metadata;\n    this.snapshotSchema = snapshotSchema;\n    this.logReplay = logReplay;\n    this.readSchema = snapshotSchema;\n    this.predicate = Optional.empty();\n    this.snapshotReport = snapshotReport;\n  }\n\n  @Override\n  public ScanBuilder withFilter(Predicate predicate) {\n    if (this.predicate.isPresent()) {\n      throw new IllegalArgumentException(\"There already exists a filter in current builder\");\n    }\n    this.predicate = Optional.of(predicate);\n    return this;\n  }\n\n  @Override\n  public ScanBuilder withReadSchema(StructType readSchema) {\n    // TODO: Validate that readSchema is a subset of the table schema or that extra fields are\n    //  metadata columns.\n    this.readSchema = readSchema;\n    return this;\n  }\n\n  @Override\n  public ScanImpl build() {\n    return new ScanImpl(\n        snapshotSchema,\n        readSchema,\n        protocol,\n        metadata,\n        logReplay,\n        predicate,\n        dataPath,\n        snapshotReport);\n  }\n\n  @Override\n  public PaginatedScan buildPaginated(long pageSize, Optional<Row> pageTokenRowOpt) {\n    ScanImpl baseScan = this.build();\n    return new PaginatedScanImpl(\n        baseScan,\n        dataPath.toString(),\n        tableVersion,\n        pageSize,\n        logReplay.getLogSegment(),\n        predicate,\n        pageTokenRowOpt);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/ScanImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineException;\nimport static io.delta.kernel.internal.skipping.StatsSchemaHelper.getStatsSchema;\nimport static io.delta.kernel.internal.util.PartitionUtils.rewritePartitionPredicateOnCheckpointFileSchema;\nimport static io.delta.kernel.internal.util.PartitionUtils.rewritePartitionPredicateOnScanFileSchema;\nimport static java.util.function.Function.identity;\nimport static java.util.stream.Collectors.toMap;\n\nimport io.delta.kernel.Scan;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.data.ScanStateRow;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.metrics.ScanMetrics;\nimport io.delta.kernel.internal.metrics.ScanReportImpl;\nimport io.delta.kernel.internal.metrics.Timer;\nimport io.delta.kernel.internal.replay.LogReplay;\nimport io.delta.kernel.internal.replay.PaginationContext;\nimport io.delta.kernel.internal.rowtracking.MaterializedRowTrackingColumn;\nimport io.delta.kernel.internal.rowtracking.RowTracking;\nimport io.delta.kernel.internal.skipping.DataSkippingPredicate;\nimport io.delta.kernel.internal.skipping.DataSkippingUtils;\nimport io.delta.kernel.internal.util.*;\nimport io.delta.kernel.metrics.ScanReport;\nimport io.delta.kernel.metrics.SnapshotReport;\nimport io.delta.kernel.types.MetadataColumnSpec;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Implementation of {@link Scan} */\npublic class ScanImpl implements Scan {\n\n  private static final Logger logger = LoggerFactory.getLogger(ScanImpl.class);\n\n  /**\n   * Schema of the snapshot from the Delta log being scanned in this scan. It is a logical schema\n   * with metadata properties to derive the physical schema.\n   */\n  private final StructType snapshotSchema;\n\n  /** Schema that we actually want to read. */\n  private final StructType readSchema;\n\n  private final Protocol protocol;\n  private final Metadata metadata;\n  private final LogReplay logReplay;\n  private final Path dataPath;\n  private final Optional<Predicate> filter;\n  private final Optional<Tuple2<Predicate, Predicate>> partitionAndDataFilters;\n  private final Supplier<Map<String, StructField>> partitionColToStructFieldMap;\n  private boolean accessedScanFiles;\n  private final SnapshotReport snapshotReport;\n  private final ScanMetrics scanMetrics = new ScanMetrics();\n\n  public ScanImpl(\n      StructType snapshotSchema,\n      StructType readSchema,\n      Protocol protocol,\n      Metadata metadata,\n      LogReplay logReplay,\n      Optional<Predicate> filter,\n      Path dataPath,\n      SnapshotReport snapshotReport) {\n    this.snapshotSchema = snapshotSchema;\n    this.readSchema = readSchema;\n    this.protocol = protocol;\n    this.metadata = metadata;\n    this.logReplay = logReplay;\n    this.filter = filter;\n    this.partitionAndDataFilters = splitFilters(filter);\n    this.dataPath = dataPath;\n    this.partitionColToStructFieldMap =\n        () -> {\n          Set<String> partitionColNames = metadata.getPartitionColNames();\n          return metadata.getSchema().fields().stream()\n              .filter(field -> partitionColNames.contains(field.getName().toLowerCase(Locale.ROOT)))\n              .collect(toMap(field -> field.getName().toLowerCase(Locale.ROOT), identity()));\n        };\n    this.snapshotReport = snapshotReport;\n  }\n\n  /**\n   * Get an iterator of data files in this version of scan that survived the predicate pruning.\n   *\n   * @return data in {@link ColumnarBatch} batch format. Each row correspond to one survived file.\n   */\n  @Override\n  public CloseableIterator<FilteredColumnarBatch> getScanFiles(Engine engine) {\n    return getScanFiles(engine, false /* includeStats */);\n  }\n\n  /**\n   * Get an iterator of data files in this version of scan that survived the predicate pruning.\n   *\n   * @return data in {@link ColumnarBatch} batch format. Each row correspond to one survived file.\n   */\n  public CloseableIterator<FilteredColumnarBatch> getScanFiles(\n      Engine engine, boolean includeStats) {\n    return getScanFiles(engine, includeStats, Optional.empty() /* paginationContextOpt */);\n  }\n\n  /**\n   * Get an iterator of data files in this version of scan that survived the predicate pruning.\n   *\n   * <p>When {@code includeStats=true} the JSON file statistics are always read from the log and\n   * included in the returned columnar batches which have schema {@link\n   * InternalScanFileUtils#SCAN_FILE_SCHEMA_WITH_STATS}. When {@code includeStats=false} the JSON\n   * file statistics may or may not be present in the returned columnar batches.\n   *\n   * @param engine the {@link Engine} instance to use\n   * @param includeStats whether to read and include the JSON statistics\n   * @param paginationContextOpt pagination context if present\n   * @return the surviving scan files as {@link FilteredColumnarBatch}s\n   */\n  protected CloseableIterator<FilteredColumnarBatch> getScanFiles(\n      Engine engine, boolean includeStats, Optional<PaginationContext> paginationContextOpt) {\n    if (accessedScanFiles) {\n      throw new IllegalStateException(\"Scan files are already fetched from this instance\");\n    }\n    accessedScanFiles = true;\n\n    // Generate data skipping filter and decide if we should read the stats column\n    logger.info(\n        \"Trying to generate data skipping filter for data filter = {} and data schema = {}\",\n        getDataFilters(),\n        metadata.getDataSchema());\n    Optional<DataSkippingPredicate> dataSkippingFilter = getDataSkippingFilter();\n    boolean hasDataSkippingFilter = dataSkippingFilter.isPresent();\n    boolean shouldReadStats = hasDataSkippingFilter || includeStats;\n    logger.info(\"Generated data skipping filter = {}\", dataSkippingFilter);\n\n    Timer.Timed planningDuration = scanMetrics.totalPlanningTimer.start();\n    // ScanReportReporter stores the current context and can be invoked (in the future) with\n    // `reportError` or `reportSuccess` to stop the planning duration timer and push a report to\n    // the engine\n    ScanReportReporter reportReporter =\n        (exceptionOpt, isFullyConsumed) -> {\n          planningDuration.stop();\n          ScanReport scanReport =\n              new ScanReportImpl(\n                  dataPath.toString() /* tablePath */,\n                  logReplay.getVersion() /* table version */,\n                  snapshotSchema,\n                  snapshotReport.getReportUUID(),\n                  filter,\n                  readSchema,\n                  getPartitionsFilters() /* partitionPredicate */,\n                  dataSkippingFilter.map(p -> p),\n                  isFullyConsumed,\n                  scanMetrics,\n                  exceptionOpt);\n          engine.getMetricsReporters().forEach(reporter -> reporter.report(scanReport));\n        };\n\n    try {\n      // Get active AddFiles via log replay\n      // If there is a partition predicate, construct a predicate to prune checkpoint files\n      // while constructing the table state.\n      CloseableIterator<FilteredColumnarBatch> scanFileIter =\n          logReplay.getAddFilesAsColumnarBatches(\n              engine,\n              shouldReadStats,\n              getPartitionsFilters()\n                  .map(\n                      predicate ->\n                          rewritePartitionPredicateOnCheckpointFileSchema(\n                              predicate, partitionColToStructFieldMap.get())),\n              scanMetrics,\n              paginationContextOpt);\n\n      // Apply partition pruning\n      scanFileIter = applyPartitionPruning(engine, scanFileIter);\n\n      // Apply data skipping\n      if (hasDataSkippingFilter) {\n        // there was a usable data skipping filter --> apply data skipping\n        scanFileIter = applyDataSkipping(engine, scanFileIter, dataSkippingFilter.get());\n      }\n\n      // TODO when !includeStats drop the stats column if present before returning\n      return wrapWithMetricsReporting(scanFileIter, reportReporter);\n\n    } catch (Exception e) {\n      reportReporter.reportError(e);\n      throw e;\n    }\n  }\n\n  @Override\n  public Row getScanState(Engine engine) {\n    StructType physicalSchema = createPhysicalSchema();\n\n    return ScanStateRow.of(\n        metadata,\n        protocol,\n        readSchema.toJson() /* logical schema */,\n        physicalSchema.toJson(),\n        dataPath.toUri().toString());\n  }\n\n  @Override\n  public Optional<Predicate> getRemainingFilter() {\n    return getDataFilters();\n  }\n\n  /**\n   * Transform the logical schema requested by the connector into a physical schema that is passed\n   * to the engine's parquet reader.\n   *\n   * <p>The logical-to-physical conversion is reversed in {@link Scan#transformPhysicalData(Engine,\n   * Row, Row, CloseableIterator)} when physical data batches returned by the parquet reader are\n   * converted into logical data batches requested by the connector.\n   *\n   * <p>The logical-to-physical conversion follows these high-level steps:\n   *\n   * <ul>\n   *   <li>Partition columns are excluded from the physical schema.\n   *   <li>Regular columns are converted based on the column mapping mode.\n   *   <li>Metadata columns are converted to their physical counterparts if applicable.\n   *   <li>Additional columns (such as the row index) are requested if necessary.\n   * </ul>\n   *\n   * @return The physical schema to read data from the data files.\n   */\n  private StructType createPhysicalSchema() {\n    ArrayList<StructField> physicalFields = new ArrayList<>();\n    ColumnMapping.ColumnMappingMode mode =\n        ColumnMapping.getColumnMappingMode(metadata.getConfiguration());\n\n    for (StructField logicalField : readSchema.fields()) {\n      if (!metadata\n          .getPartitionColNames()\n          .contains(logicalField.getName().toLowerCase(Locale.ROOT))) {\n        physicalFields.addAll(convertField(logicalField, mode));\n      }\n    }\n\n    if (protocol.getReaderFeatures().contains(\"deletionVectors\")\n        && physicalFields.stream()\n            .map(StructField::getMetadataColumnSpec)\n            .noneMatch(MetadataColumnSpec.ROW_INDEX::equals)) {\n      // If the row index column is not already present, add it to the physical read schema\n      physicalFields.add(SchemaUtils.asInternalColumn(StructField.DEFAULT_ROW_INDEX_COLUMN));\n    }\n\n    return new StructType(physicalFields);\n  }\n\n  private List<StructField> convertField(\n      StructField logicalField, ColumnMapping.ColumnMappingMode mode) {\n    if (logicalField.isDataColumn()) {\n      return Collections.singletonList(\n          ColumnMapping.convertToPhysicalColumn(logicalField, snapshotSchema, mode));\n    }\n\n    if (RowTracking.isRowTrackingColumn(logicalField)) {\n      return MaterializedRowTrackingColumn.convertToPhysicalColumn(\n          logicalField, readSchema, metadata);\n    }\n\n    // As of now, metadata columns other than row tracking columns do not require any special\n    // handling, so we can just add them to the physical schema as is.\n    return Collections.singletonList(logicalField);\n  }\n\n  private Optional<Tuple2<Predicate, Predicate>> splitFilters(Optional<Predicate> filter) {\n    return filter.map(\n        predicate ->\n            PartitionUtils.splitMetadataAndDataPredicates(\n                predicate, metadata.getPartitionColNames()));\n  }\n\n  private Optional<Predicate> getDataFilters() {\n    return removeAlwaysTrue(partitionAndDataFilters.map(filters -> filters._2));\n  }\n\n  private Optional<Predicate> getPartitionsFilters() {\n    return removeAlwaysTrue(partitionAndDataFilters.map(filters -> filters._1));\n  }\n\n  /** Consider `ALWAYS_TRUE` as no predicate. */\n  private Optional<Predicate> removeAlwaysTrue(Optional<Predicate> predicate) {\n    return predicate.filter(filter -> !filter.getName().equalsIgnoreCase(\"ALWAYS_TRUE\"));\n  }\n\n  private CloseableIterator<FilteredColumnarBatch> applyPartitionPruning(\n      Engine engine, CloseableIterator<FilteredColumnarBatch> scanFileIter) {\n    Optional<Predicate> partitionPredicate = getPartitionsFilters();\n    if (!partitionPredicate.isPresent()) {\n      // There is no partition filter, return the scan file iterator as is.\n      return scanFileIter;\n    }\n\n    Predicate predicateOnScanFileBatch =\n        rewritePartitionPredicateOnScanFileSchema(\n            partitionPredicate.get(), partitionColToStructFieldMap.get());\n\n    return new CloseableIterator<FilteredColumnarBatch>() {\n      PredicateEvaluator predicateEvaluator = null;\n\n      @Override\n      public boolean hasNext() {\n        return scanFileIter.hasNext();\n      }\n\n      @Override\n      public FilteredColumnarBatch next() {\n        FilteredColumnarBatch next = scanFileIter.next();\n        if (predicateEvaluator == null) {\n          predicateEvaluator =\n              wrapEngineException(\n                  () ->\n                      engine\n                          .getExpressionHandler()\n                          .getPredicateEvaluator(\n                              next.getData().getSchema(), predicateOnScanFileBatch),\n                  \"Get the predicate evaluator for partition pruning with schema=%s and\"\n                      + \" filter=%s\",\n                  next.getData().getSchema(),\n                  predicateOnScanFileBatch);\n        }\n        ColumnVector newSelectionVector =\n            wrapEngineException(\n                () -> predicateEvaluator.eval(next.getData(), next.getSelectionVector()),\n                \"Evaluating the partition expression %s\",\n                predicateOnScanFileBatch);\n        return new FilteredColumnarBatch(next.getData(), Optional.of(newSelectionVector));\n      }\n\n      @Override\n      public void close() throws IOException {\n        scanFileIter.close();\n      }\n    };\n  }\n\n  private Optional<DataSkippingPredicate> getDataSkippingFilter() {\n    return getDataFilters()\n        .flatMap(\n            dataFilters ->\n                DataSkippingUtils.constructDataSkippingFilter(\n                    dataFilters, metadata.getDataSchema()));\n  }\n\n  private CloseableIterator<FilteredColumnarBatch> applyDataSkipping(\n      Engine engine,\n      CloseableIterator<FilteredColumnarBatch> scanFileIter,\n      DataSkippingPredicate dataSkippingFilter) {\n    // Get the stats schema\n    // It's possible to instead provide the referenced columns when building the schema but\n    // pruning it after is much simpler\n    StructType prunedStatsSchema =\n        DataSkippingUtils.pruneStatsSchema(\n            getStatsSchema(metadata.getDataSchema(), dataSkippingFilter.getReferencedCollations()),\n            dataSkippingFilter.getReferencedCols());\n    logger.info(\"For stats JSON parsing: prunedStatsSchema={}\", prunedStatsSchema);\n\n    // Skipping happens in two steps:\n    // 1. The predicate produces false for any file whose stats prove we can safely skip it. A\n    //    value of true means the stats say we must keep the file, and null means we could not\n    //    determine whether the file is safe to skip, because its stats were missing/null.\n    // 2. The coalesce(skip, true) converts null (= keep) to true\n    Predicate filterToEval =\n        new Predicate(\n            \"=\",\n            new ScalarExpression(\n                \"COALESCE\", Arrays.asList(dataSkippingFilter, Literal.ofBoolean(true))),\n            AlwaysTrue.ALWAYS_TRUE);\n\n    PredicateEvaluator predicateEvaluator =\n        wrapEngineException(\n            () ->\n                engine\n                    .getExpressionHandler()\n                    .getPredicateEvaluator(prunedStatsSchema, filterToEval),\n            \"Get the predicate evaluator for data skipping with schema=%s and filter=%s\",\n            prunedStatsSchema,\n            filterToEval);\n\n    return scanFileIter.map(\n        filteredScanFileBatch -> {\n          ColumnVector newSelectionVector =\n              wrapEngineException(\n                  () ->\n                      predicateEvaluator.eval(\n                          DataSkippingUtils.parseJsonStats(\n                              engine, filteredScanFileBatch, prunedStatsSchema),\n                          filteredScanFileBatch.getSelectionVector()),\n                  \"Evaluating the data skipping filter %s\",\n                  filterToEval);\n\n          return new FilteredColumnarBatch(\n              filteredScanFileBatch.getData(), Optional.of(newSelectionVector));\n        });\n  }\n\n  /**\n   * Wraps a scan file iterator such that we emit {@link ScanReport} to the engine upon success and\n   * failure. Since most of our scan building code is lazily executed (since it occurs as\n   * maps/filters over an iterator) potential errors don't occur just within `getScanFile`s\n   * execution, but rather may occur as the returned iterator is consumed. Similarly, we cannot\n   * report a successful scan until the iterator has been fully consumed and the log read/filtered\n   * etc. This means we cannot report the successful scan within `getScanFiles` but rather must\n   * report after the iterator has been consumed.\n   *\n   * <p>This method wraps an inner scan file iterator with an outer iterator wrapper that reports\n   * {@link ScanReport}s as needed. It reports a failed {@link ScanReport} in the case of any\n   * exceptions originating from the inner iterator `next` and `hasNext` impl. It reports a complete\n   * or incomplete {@link ScanReport} when the iterator is closed.\n   */\n  private CloseableIterator<FilteredColumnarBatch> wrapWithMetricsReporting(\n      CloseableIterator<FilteredColumnarBatch> scanIter, ScanReportReporter reporter) {\n    return new CloseableIterator<FilteredColumnarBatch>() {\n\n      /* Whether this iterator has reported an error report */\n      private boolean errorReported = false;\n\n      @Override\n      public void close() throws IOException {\n        try {\n          // If a ScanReport has already been pushed in the case of an exception don't double report\n          if (!errorReported) {\n            if (!scanIter.hasNext()) {\n              // The entire scan file iterator has been successfully consumed report a complete Scan\n              reporter.reportCompleteScan();\n            } else {\n              // The scan file iterator has NOT been fully consumed before being closed\n              // We have no way of knowing the reason why, this could be due to an exception in the\n              // connector code, or intentional early termination such as for a LIMIT query\n              reporter.reportIncompleteScan();\n            }\n          }\n        } finally {\n          scanIter.close();\n        }\n      }\n\n      @Override\n      public boolean hasNext() {\n        return wrapWithErrorReporting(() -> scanIter.hasNext());\n      }\n\n      @Override\n      public FilteredColumnarBatch next() {\n        return wrapWithErrorReporting(() -> scanIter.next());\n      }\n\n      private <T> T wrapWithErrorReporting(Supplier<T> s) {\n        try {\n          return s.get();\n        } catch (Exception e) {\n          reporter.reportError(e);\n          errorReported = true;\n          throw e;\n        }\n      }\n    };\n  }\n\n  /**\n   * Defines methods to report {@link ScanReport} to the engine. This allows us to avoid ambiguous\n   * lambdas/anonymous classes as well as reuse the defined default methods.\n   */\n  private interface ScanReportReporter {\n\n    default void reportError(Exception e) {\n      report(Optional.of(e), false /* isFullyConsumed */);\n    }\n\n    default void reportCompleteScan() {\n      report(Optional.empty(), true /* isFullyConsumed */);\n    }\n\n    default void reportIncompleteScan() {\n      report(Optional.empty(), false /* isFullyConsumed */);\n    }\n\n    /** Given an optional exception, reports a {@link ScanReport} to the engine */\n    void report(Optional<Exception> exceptionOpt, boolean isFullyConsumed);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/SnapshotImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.TableConfig.*;\nimport static io.delta.kernel.internal.TableConfig.TOMBSTONE_RETENTION;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.ScanBuilder;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.commit.CatalogCommitter;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.commit.PublishFailedException;\nimport io.delta.kernel.commit.PublishMetadata;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.CommitInfo;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.checkpoints.Checkpointer;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.checksum.ChecksumUtils;\nimport io.delta.kernel.internal.checksum.ChecksumWriter;\nimport io.delta.kernel.internal.clustering.ClusteringMetadataDomain;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext;\nimport io.delta.kernel.internal.metrics.SnapshotReportImpl;\nimport io.delta.kernel.internal.replay.CreateCheckpointIterator;\nimport io.delta.kernel.internal.replay.LogReplay;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.metrics.SnapshotReport;\nimport io.delta.kernel.statistics.SnapshotStatistics;\nimport io.delta.kernel.transaction.ReplaceTableTransactionBuilder;\nimport io.delta.kernel.transaction.UpdateTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Implementation of {@link Snapshot}. */\npublic class SnapshotImpl implements Snapshot {\n\n  private static final Logger logger = LoggerFactory.getLogger(SnapshotImpl.class);\n\n  private final Path logPath;\n  private final Path dataPath;\n  private final long version;\n  private final Lazy<LogSegment> lazyLogSegment;\n  private final LogReplay logReplay;\n  private final Protocol protocol;\n  private final Metadata metadata;\n  private final Committer committer;\n\n  /**\n   * If this snapshot does not have the InCommitTimestamp (ICT) table feature enabled, then this is\n   * always Optional.empty(). If it does, then this is:\n   *\n   * <ul>\n   *   <li>Optional.empty(): if the ICT value is not yet known (i.e. has not yet been read from the\n   *       CRC or CommitInfo)\n   *   <li>Optional.of(timestamp): if the ICT value has been read from the CRC or CommitInfo, or was\n   *       injected into this Snapshot at construction time (e.g. for a post-commit snapshot)\n   * </ul>\n   */\n  private Optional<Long> inCommitTimestampOpt;\n\n  private Lazy<SnapshotReport> lazySnapshotReport;\n  private Lazy<Optional<List<Column>>> lazyClusteringColumns;\n\n  /**\n   * Indicates whether this snapshot was built as a \"latest\" snapshot query (i.e., no time-travel\n   * parameters were provided). This is intent-based - it indicates what the user requested, not\n   * whether the snapshot is actually the latest version.\n   */\n  private final boolean wasBuiltAsLatest;\n\n  // TODO: Do not take in LogReplay as a constructor argument.\n  // TODO: Also take in clustering columns for post-commit snapshot\n  public SnapshotImpl(\n      Path dataPath,\n      long version,\n      Lazy<LogSegment> lazyLogSegment,\n      LogReplay logReplay,\n      Protocol protocol,\n      Metadata metadata,\n      Committer committer,\n      SnapshotQueryContext snapshotContext,\n      Optional<Long> inCommitTimestampOpt) {\n    checkArgument(version >= 0, \"A snapshot cannot have version < 0\");\n    this.logPath = new Path(dataPath, \"_delta_log\");\n    this.dataPath = dataPath;\n    this.version = version;\n    this.lazyLogSegment = lazyLogSegment;\n    this.logReplay = logReplay;\n    this.protocol = requireNonNull(protocol);\n    this.metadata = requireNonNull(metadata);\n    this.committer = committer;\n    this.inCommitTimestampOpt = inCommitTimestampOpt;\n    // TODO: Post-commit snapshots build a version-based SnapshotQueryContext\n    // (see TransactionImpl.buildPostCommitSnapshotOpt), so isLatestQuery() may be false even\n    // when this snapshot is intended to be the latest version.\n    this.wasBuiltAsLatest = snapshotContext.isLatestQuery();\n\n    // We create the actual Snapshot report lazily (on first access) instead of eagerly in this\n    // constructor because some Snapshot metrics, like {@link\n    // io.delta.kernel.metrics.SnapshotMetricsResult#getLoadSnapshotTotalDurationNs}, are only\n    // completed *after* the Snapshot has been constructed.\n    this.lazySnapshotReport = new Lazy<>(() -> SnapshotReportImpl.forSuccess(snapshotContext));\n    this.lazyClusteringColumns =\n        new Lazy<>(\n            () ->\n                ClusteringMetadataDomain.fromSnapshot(this)\n                    .map(ClusteringMetadataDomain::getClusteringColumns));\n  }\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  @Override\n  public String getPath() {\n    return dataPath.toString();\n  }\n\n  @Override\n  public long getVersion() {\n    return version;\n  }\n\n  @Override\n  public List<String> getPartitionColumnNames() {\n    return VectorUtils.toJavaList(getMetadata().getPartitionColumns());\n  }\n\n  /**\n   * Get the timestamp (in milliseconds since the Unix epoch) of the latest commit in this Snapshot.\n   *\n   * <p>When InCommitTimestampTableFeature is enabled, the timestamp is retrieved from the\n   * CommitInfo of the latest commit in this Snapshot, which can result in an IO operation.\n   *\n   * <p>For non-ICT tables, this is the same as the file modification time of the latest commit in\n   * this Snapshot.\n   */\n  // TODO: Support reading from CRC file if available\n  @Override\n  public long getTimestamp(Engine engine) {\n    if (IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(metadata)) {\n      if (!inCommitTimestampOpt.isPresent()) {\n        final Optional<CommitInfo> commitInfoOpt =\n            CommitInfo.tryReadCommitInfoFromDeltaFile(\n                engine, getLogSegment().getDeltaFileAtEndVersion());\n        inCommitTimestampOpt =\n            Optional.of(\n                CommitInfo.extractRequiredIctFromCommitInfoOpt(commitInfoOpt, version, dataPath));\n      }\n      return inCommitTimestampOpt.get();\n    } else {\n      return getLogSegment().getDeltaFileAtEndVersion().getModificationTime();\n    }\n  }\n\n  @Override\n  public StructType getSchema() {\n    return getMetadata().getSchema();\n  }\n\n  @Override\n  public Optional<String> getDomainMetadata(String domain) {\n    return Optional.ofNullable(getActiveDomainMetadataMap().get(domain))\n        .map(DomainMetadata::getConfiguration);\n  }\n\n  @Override\n  public Map<String, String> getTableProperties() {\n    return metadata.getConfiguration();\n  }\n\n  @Override\n  public SnapshotStatistics getStatistics() {\n    return new SnapshotStatisticsImpl();\n  }\n\n  @Override\n  public ScanBuilder getScanBuilder() {\n    return new ScanBuilderImpl(\n        dataPath, version, protocol, metadata, getSchema(), logReplay, getSnapshotReport());\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder buildUpdateTableTransaction(\n      String engineInfo, Operation operation) {\n    return new UpdateTableTransactionBuilderImpl(this, engineInfo, operation);\n  }\n\n  @Override\n  public Snapshot publish(Engine engine) throws PublishFailedException {\n    final List<ParsedCatalogCommitData> allCatalogCommits = getLogSegment().getAllCatalogCommits();\n    final boolean isFileSystemBasedTable = !TableFeatures.isCatalogManagedSupported(protocol);\n    final boolean isCatalogCommitter = committer instanceof CatalogCommitter;\n\n    if (!allCatalogCommits.isEmpty()) {\n      if (isFileSystemBasedTable) {\n        throw new IllegalStateException( // This case should be impossible\n            \"Cannot have catalog commits on a filesystem-managed table\");\n      }\n\n      if (!isCatalogCommitter) {\n        throw new UnsupportedOperationException( // This case should also be impossible\n            String.format(\n                \"[%s] Cannot publish: committer does not support publishing\",\n                committer.getClass().getName()));\n      }\n    } else {\n      if (isFileSystemBasedTable) {\n        logger.info(\"Publishing not applicable: this is a filesystem-managed table\");\n        return this;\n      }\n\n      if (!isCatalogCommitter) {\n        logger.info(\n            \"[{}] Publishing not applicable: committer does not support publishing\",\n            committer.getClass().getName());\n        return this;\n      }\n    }\n\n    // TODO: When we return a post-publish Snapshot, ensure to replace *all* catalog commits with\n    //       their published versions, not just the catalog commits that were published. For\n    //       example: if we have catalog commits v11, v12, and v13 but the maxPublishedVersion is\n    //       12, we will only publish v13. Nonetheless, our post-publish Snapshot must include the\n    //       published versions of v11 and v12, too.\n\n    final long maxPublishedDeltaVersion = getMaxPublishedDeltaVersionOrThrow();\n    final List<ParsedCatalogCommitData> catalogCommitsToPublish =\n        allCatalogCommits.stream()\n            .filter(commit -> commit.getVersion() > maxPublishedDeltaVersion)\n            .collect(Collectors.toList());\n\n    if (catalogCommitsToPublish.isEmpty()) {\n      logger.info(\"No catalog commits need to be published\");\n      return this;\n    }\n\n    final PublishMetadata publishMetadata =\n        new PublishMetadata(version, logPath.toString(), catalogCommitsToPublish);\n\n    ((CatalogCommitter) committer).publish(engine, publishMetadata);\n    LogSegment updatedLogSegment = getLogSegment().newAsPublished();\n    return new SnapshotImpl(\n        dataPath,\n        version,\n        new Lazy<>(() -> updatedLogSegment),\n        logReplay,\n        protocol,\n        metadata,\n        committer,\n        SnapshotQueryContext.forVersionSnapshot(dataPath.toString(), version),\n        this.inCommitTimestampOpt);\n  }\n\n  @Override\n  public void writeChecksum(Engine engine, Snapshot.ChecksumWriteMode mode) throws IOException {\n    final Optional<Snapshot.ChecksumWriteMode> actualOpt = getStatistics().getChecksumWriteMode();\n\n    if (actualOpt.isEmpty()) {\n      logger.warn(\"Not writing checksum: checksum file already exists at version {}\", version);\n      return;\n    }\n\n    final Snapshot.ChecksumWriteMode actual = actualOpt.get();\n\n    switch (mode) {\n      case SIMPLE:\n        if (actual == ChecksumWriteMode.FULL) {\n          throw new IllegalStateException(\n              \"Cannot write checksum in SIMPLE mode: FULL mode required\");\n        }\n\n        final CRCInfo crcInfo = logReplay.getCrcInfoAtSnapshotVersion().get();\n        logger.info(\"Executing checksum write in SIMPLE mode\");\n        new ChecksumWriter(logPath).writeCheckSum(engine, crcInfo);\n        return;\n      case FULL:\n        if (actual == ChecksumWriteMode.SIMPLE) {\n          logger.warn(\"Requested checksum write in FULL mode, but SIMPLE mode is available\");\n        }\n        logger.info(\"Executing checksum write in FULL mode\");\n        ChecksumUtils.computeStateAndWriteChecksum(engine, getLogSegment());\n        return;\n      default:\n        throw new IllegalStateException(\"Unknown checksum write mode: \" + mode);\n    }\n  }\n\n  public void writeCheckpoint(Engine engine) throws IOException {\n    // Refuse to create a checkpoint if the table is CatalogManaged but the current snapshot is not\n    // published\n    if (TableFeatures.isCatalogManagedSupported(protocol)\n        && getLogSegment().getMaxPublishedDeltaVersion().orElse(-1L) < version) {\n      throw DeltaErrors.checkpointOnUnpublishedCommits(\n          getPath(), version, getLogSegment().getMaxPublishedDeltaVersion().orElse(-1L));\n    }\n    Checkpointer.checkpoint(engine, System::currentTimeMillis, this);\n  }\n\n  ///////////////////\n  // Internal APIs //\n  ///////////////////\n\n  // TODO: make this API public after closing open threads for Replace Table operation\n  public ReplaceTableTransactionBuilder buildReplaceTableTransaction(\n      StructType schema, String engineInfo) {\n    return new ReplaceTableTransactionBuilderV2Impl(this, schema, engineInfo);\n  }\n\n  public Committer getCommitter() {\n    return committer;\n  }\n\n  public Path getLogPath() {\n    return logPath;\n  }\n\n  public Path getDataPath() {\n    return dataPath;\n  }\n\n  /**\n   * Returns true if this snapshot was built as a \"latest\" snapshot query (i.e., no time-travel\n   * parameters were provided). This is intent-based - it indicates what the user requested, not\n   * whether the snapshot is actually the latest version.\n   */\n  public boolean wasBuiltAsLatest() {\n    return wasBuiltAsLatest;\n  }\n\n  public Protocol getProtocol() {\n    return protocol;\n  }\n\n  public SnapshotReport getSnapshotReport() {\n    return lazySnapshotReport.get();\n  }\n\n  /**\n   * Returns the clustering columns for this snapshot.\n   *\n   * <ul>\n   *   <li>Optional.empty() - unclustered table (clustering is not enabled)\n   *   <li>Optional.of([]) - clustered table with no clustering columns (clustering is enabled)\n   *   <li>Optional.of([col1, col2]) - clustered table with the given physical clustering columns\n   * </ul>\n   *\n   * @return the physical clustering columns in this snapshot\n   */\n  public Optional<List<Column>> getPhysicalClusteringColumns() {\n    return lazyClusteringColumns.get();\n  }\n\n  /**\n   * Get the domain metadata map from the log replay, which lazily loads and replays a history of\n   * domain metadata actions, resolving them to produce the current state of the domain metadata.\n   * Only active domain metadata are included in this map.\n   *\n   * @return A map where the keys are domain names and the values are {@link DomainMetadata}\n   *     objects.\n   */\n  public Map<String, DomainMetadata> getActiveDomainMetadataMap() {\n    return logReplay.getActiveDomainMetadataMap();\n  }\n\n  /** Returns the crc info for the current snapshot if the checksum file is read */\n  public Optional<CRCInfo> getCurrentCrcInfo() {\n    return logReplay.getCrcInfoAtSnapshotVersion();\n  }\n\n  public Metadata getMetadata() {\n    return metadata;\n  }\n\n  public LogSegment getLogSegment() {\n    return lazyLogSegment.get();\n  }\n\n  @VisibleForTesting\n  public Lazy<LogSegment> getLazyLogSegment() {\n    return lazyLogSegment;\n  }\n\n  public CreateCheckpointIterator getCreateCheckpointIterator(Engine engine) {\n    long minFileRetentionTimestampMillis =\n        System.currentTimeMillis() - TOMBSTONE_RETENTION.fromMetadata(metadata);\n    return new CreateCheckpointIterator(engine, getLogSegment(), minFileRetentionTimestampMillis);\n  }\n\n  /**\n   * Get the latest transaction version for given <i>applicationId</i>. This information comes from\n   * the transactions identifiers stored in Delta transaction log. This API is not a public API. For\n   * now keep this internal to enable Flink upgrade to use Kernel.\n   *\n   * @param applicationId Identifier of the application that put transaction identifiers in Delta\n   *     transaction log\n   * @return Last transaction version or {@link Optional#empty()} if no transaction identifier\n   *     exists for this application.\n   */\n  public Optional<Long> getLatestTransactionVersion(Engine engine, String applicationId) {\n    return logReplay.getLatestTransactionIdentifier(engine, applicationId);\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  private long getMaxPublishedDeltaVersionOrThrow() {\n    // The maxPublishedDeltaVersion is required for publishing to ensure published deltas are\n    // contiguous. The cases where it is unknown should be very rare (e.g. Kernel loaded a\n    // LogSegment consisting only of a checkpoint with no corresponding published delta).\n    // TODO: Kernel should LIST to authoritatively determine the maxPublishedDeltaVersion, or give\n    //       such utilities to CatalogCommitters for them to do this.\n    return getLogSegment()\n        .getMaxPublishedDeltaVersion()\n        .orElseThrow(\n            () ->\n                new IllegalStateException(\n                    \"maxPublishedDeltaVersion is unknown. This is required for publishing.\"));\n  }\n\n  ///////////////////\n  // Inner Classes //\n  ///////////////////\n\n  private class SnapshotStatisticsImpl implements SnapshotStatistics {\n    @Override\n    public Optional<Snapshot.ChecksumWriteMode> getChecksumWriteMode() {\n      final boolean checksumFileExists =\n          getLogSegment()\n              .getLastSeenChecksum()\n              .map(checksumFile -> FileNames.checksumVersion(checksumFile.getPath()) == version)\n              .orElse(false);\n\n      if (checksumFileExists) {\n        return Optional.empty();\n      }\n\n      if (logReplay.getCrcInfoAtSnapshotVersion().isPresent()) {\n        return Optional.of(Snapshot.ChecksumWriteMode.SIMPLE);\n      }\n\n      return Optional.of(Snapshot.ChecksumWriteMode.FULL);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/TableChangesUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineException;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.ExpressionEvaluator;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\n\n/** Utility class for table changes operations. */\npublic class TableChangesUtils {\n\n  /** Column name for the version metadata column added to getActions results. */\n  public static final String VERSION_COLUMN_NAME = \"version\";\n\n  /** Column name for the timestamp metadata column added to getActions results. */\n  public static final String TIMESTAMP_COLUMN_NAME = \"timestamp\";\n\n  /** StructField for the version metadata column. */\n  private static final StructField VERSION_STRUCT_FIELD =\n      new StructField(VERSION_COLUMN_NAME, LongType.LONG, false);\n\n  /** StructField for the timestamp metadata column. */\n  private static final StructField TIMESTAMP_STRUCT_FIELD =\n      new StructField(TIMESTAMP_COLUMN_NAME, LongType.LONG, false);\n\n  private TableChangesUtils() {}\n\n  /**\n   * Adds version and timestamp columns to a columnar batch.\n   *\n   * <p>The version and timestamp columns are added as the first two columns in the batch.\n   *\n   * @param engine the engine for expression evaluation\n   * @param batch the original batch\n   * @param version the version value to add\n   * @param timestamp the timestamp value to add\n   * @return a new batch with version and timestamp columns prepended\n   */\n  public static ColumnarBatch addVersionAndTimestampColumns(\n      Engine engine, ColumnarBatch batch, long version, long timestamp) {\n    StructType schemaForEval = batch.getSchema();\n\n    ExpressionEvaluator commitVersionGenerator =\n        wrapEngineException(\n            () ->\n                engine\n                    .getExpressionHandler()\n                    .getEvaluator(schemaForEval, Literal.ofLong(version), LongType.LONG),\n            \"Get the expression evaluator for the commit version\");\n\n    ExpressionEvaluator commitTimestampGenerator =\n        wrapEngineException(\n            () ->\n                engine\n                    .getExpressionHandler()\n                    .getEvaluator(schemaForEval, Literal.ofLong(timestamp), LongType.LONG),\n            \"Get the expression evaluator for the commit timestamp\");\n\n    ColumnVector commitVersionVector =\n        wrapEngineException(\n            () -> commitVersionGenerator.eval(batch), \"Evaluating the commit version expression\");\n\n    ColumnVector commitTimestampVector =\n        wrapEngineException(\n            () -> commitTimestampGenerator.eval(batch),\n            \"Evaluating the commit timestamp expression\");\n\n    return batch\n        .withNewColumn(0, VERSION_STRUCT_FIELD, commitVersionVector)\n        .withNewColumn(1, TIMESTAMP_STRUCT_FIELD, commitTimestampVector);\n  }\n\n  /**\n   * Flattens an iterator of CommitActions into an iterator of ColumnarBatch, adding version and\n   * timestamp columns to each batch.\n   *\n   * @param engine the engine for expression evaluation\n   * @param commits the iterator of CommitActions to flatten\n   * @return an iterator of ColumnarBatch with version and timestamp columns added\n   */\n  public static CloseableIterator<ColumnarBatch> flattenCommitsAndAddMetadata(\n      Engine engine, CloseableIterator<CommitActions> commits) {\n    CloseableIterator<CloseableIterator<ColumnarBatch>> nestedIterator =\n        commits.map(\n            commit -> {\n              long version = commit.getVersion();\n              long timestamp = commit.getTimestamp();\n              CloseableIterator<ColumnarBatch> actions = commit.getActions();\n\n              // Map each batch to add version and timestamp columns\n              return actions.map(\n                  batch -> addVersionAndTimestampColumns(engine, batch, version, timestamp));\n            });\n\n    return Utils.flatten(nestedIterator);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/TableConfig.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport io.delta.kernel.exceptions.InvalidConfigurationValueException;\nimport io.delta.kernel.exceptions.UnknownConfigurationException;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode;\nimport io.delta.kernel.internal.util.IntervalParserUtils;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n/**\n * Represents the table properties. Also provides methods to access the property values from the\n * table metadata.\n */\npublic class TableConfig<T> {\n\n  public static final String MIN_PROTOCOL_READER_VERSION_KEY = \"delta.minReaderVersion\";\n\n  public static final String MIN_PROTOCOL_WRITER_VERSION_KEY = \"delta.minWriterVersion\";\n\n  //////////////////\n  // TableConfigs //\n  //////////////////\n\n  /**\n   * Whether this Delta table is append-only. Files can't be deleted, or values can't be updated.\n   */\n  public static final TableConfig<Boolean> APPEND_ONLY_ENABLED =\n      new TableConfig<>(\n          \"delta.appendOnly\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * Enable change data feed output. When enabled, DELETE, UPDATE, and MERGE INTO operations will\n   * need to do additional work to output their change data in an efficiently readable format.\n   */\n  public static final TableConfig<Boolean> CHANGE_DATA_FEED_ENABLED =\n      new TableConfig<>(\n          \"delta.enableChangeDataFeed\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  public static final TableConfig<String> CHECKPOINT_POLICY =\n      new TableConfig<>(\n          \"delta.checkpointPolicy\",\n          \"classic\",\n          v -> v,\n          value -> value.equals(\"classic\") || value.equals(\"v2\"),\n          \"needs to be a string and one of 'classic' or 'v2'.\",\n          true);\n\n  /** Whether commands modifying this Delta table are allowed to create new deletion vectors. */\n  public static final TableConfig<Boolean> DELETION_VECTORS_CREATION_ENABLED =\n      new TableConfig<>(\n          \"delta.enableDeletionVectors\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * Whether widening the type of an existing column or field is allowed, either manually using\n   * ALTER TABLE CHANGE COLUMN or automatically if automatic schema evolution is enabled.\n   */\n  public static final TableConfig<Boolean> TYPE_WIDENING_ENABLED =\n      new TableConfig<>(\n          \"delta.enableTypeWidening\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * Indicates whether Row Tracking is enabled on the table. When this flag is turned on, all rows\n   * are guaranteed to have Row IDs and Row Commit Versions assigned to them, and writers are\n   * expected to preserve them by materializing them to hidden columns in the data files.\n   */\n  public static final TableConfig<Boolean> ROW_TRACKING_ENABLED =\n      new TableConfig<>(\n          \"delta.enableRowTracking\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * The shortest duration we have to keep logically deleted data files around before deleting them\n   * physically.\n   *\n   * <p>Note: this value should be large enough:\n   *\n   * <ul>\n   *   <li>It should be larger than the longest possible duration of a job if you decide to run\n   *       \"VACUUM\" when there are concurrent readers or writers accessing the table.\n   *   <li>If you are running a streaming query reading from the table, you should make sure the\n   *       query doesn't stop longer than this value. Otherwise, the query may not be able to\n   *       restart as it still needs to read old files.\n   * </ul>\n   */\n  public static final TableConfig<Long> TOMBSTONE_RETENTION =\n      new TableConfig<>(\n          \"delta.deletedFileRetentionDuration\",\n          \"interval 1 week\",\n          IntervalParserUtils::safeParseIntervalAsMillis,\n          value -> value >= 0,\n          \"needs to be provided as a calendar interval such as '2 weeks'. Months\"\n              + \" and years are not accepted. You may specify '365 days' for a year instead.\",\n          true);\n\n  /**\n   * How often to checkpoint the delta log? For every N (this config) commits to the log, we will\n   * suggest write out a checkpoint file that can speed up the Delta table state reconstruction.\n   */\n  public static final TableConfig<Integer> CHECKPOINT_INTERVAL =\n      new TableConfig<>(\n          \"delta.checkpointInterval\",\n          \"10\",\n          Integer::valueOf,\n          value -> value > 0,\n          \"needs to be a positive integer.\",\n          true);\n\n  /**\n   * The shortest duration we have to keep delta/checkpoint files around before deleting them. We\n   * can only delete delta files that are before a checkpoint.\n   */\n  public static final TableConfig<Long> LOG_RETENTION =\n      new TableConfig<>(\n          \"delta.logRetentionDuration\",\n          \"interval 30 days\",\n          IntervalParserUtils::safeParseIntervalAsMillis,\n          value -> true,\n          \"needs to be provided as a calendar interval such as '2 weeks'. Months \"\n              + \"and years are not accepted. You may specify '365 days' for a year instead.\",\n          true /* editable */);\n\n  /** Whether to clean up expired checkpoints and delta logs. */\n  public static final TableConfig<Boolean> EXPIRED_LOG_CLEANUP_ENABLED =\n      new TableConfig<>(\n          \"delta.enableExpiredLogCleanup\",\n          \"true\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true /* editable */);\n\n  /**\n   * This table property is used to track the enablement of the {@code inCommitTimestamps}.\n   *\n   * <p>When enabled, commit metadata includes a monotonically increasing timestamp that allows for\n   * reliable TIMESTAMP AS OF time travel even if filesystem operations change a commit file's\n   * modification timestamp.\n   */\n  public static final TableConfig<Boolean> IN_COMMIT_TIMESTAMPS_ENABLED =\n      new TableConfig<>(\n          \"delta.enableInCommitTimestamps\",\n          \"false\", /* default values */\n          v -> Boolean.valueOf(v),\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * This table property is used to track the version of the table at which {@code\n   * inCommitTimestamps} were enabled.\n   */\n  public static final TableConfig<Optional<Long>> IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION =\n      new TableConfig<>(\n          \"delta.inCommitTimestampEnablementVersion\",\n          null, /* default values */\n          v -> Optional.ofNullable(v).map(Long::valueOf),\n          value -> true,\n          \"needs to be a long.\",\n          true);\n\n  /**\n   * This table property is used to track the timestamp at which {@code inCommitTimestamps} were\n   * enabled. More specifically, it is the {@code inCommitTimestamps} of the commit with the version\n   * specified in {@link #IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION}.\n   */\n  public static final TableConfig<Optional<Long>> IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP =\n      new TableConfig<>(\n          \"delta.inCommitTimestampEnablementTimestamp\",\n          null, /* default values */\n          v -> Optional.ofNullable(v).map(Long::valueOf),\n          value -> true,\n          \"needs to be a long.\",\n          true);\n\n  /** This table property is used to control the column mapping mode. */\n  public static final TableConfig<ColumnMappingMode> COLUMN_MAPPING_MODE =\n      new TableConfig<>(\n          \"delta.columnMapping.mode\",\n          \"none\", /* default values */\n          ColumnMappingMode::fromTableConfig,\n          value -> true,\n          \"Needs to be one of none, id, name.\",\n          true);\n\n  /** This table property is used to control the maximum column mapping ID. */\n  public static final TableConfig<Long> COLUMN_MAPPING_MAX_COLUMN_ID =\n      new TableConfig<>(\n          \"delta.columnMapping.maxColumnId\", \"0\", Long::valueOf, value -> value >= 0, \"\", false);\n\n  /**\n   * Table property that enables modifying the table in accordance with the Delta-Iceberg\n   * Compatibility V2 protocol.\n   *\n   * @see <a\n   *     href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#delta-iceberg-compatibility-v2\">\n   *     Delta-Iceberg Compatibility V2 Protocol</a>\n   */\n  public static final TableConfig<Boolean> ICEBERG_COMPAT_V2_ENABLED =\n      new TableConfig<>(\n          \"delta.enableIcebergCompatV2\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * Table property that enables modifying the table in accordance with the Delta-Iceberg\n   * Compatibility V3 protocol. TODO: add the delta protocol link once updated\n   * [https://github.com/delta-io/delta/issues/4574]\n   */\n  public static final TableConfig<Boolean> ICEBERG_COMPAT_V3_ENABLED =\n      new TableConfig<>(\n          \"delta.enableIcebergCompatV3\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * The number of columns to collect stats on for data skipping. A value of -1 means collecting\n   * stats for all columns.\n   *\n   * <p>For Struct types, all leaf fields count individually toward this limit in depth-first order.\n   * For example, if a table has columns a, b.c, b.d, and e, then the first three indexed columns\n   * would be a, b.c, and b.d. Map and array types are not supported for statistics collection.\n   */\n  public static final TableConfig<Integer> DATA_SKIPPING_NUM_INDEXED_COLS =\n      new TableConfig<>(\n          \"delta.dataSkippingNumIndexedCols\",\n          \"32\",\n          Integer::valueOf,\n          value -> value >= -1,\n          \"needs to be larger than or equal to -1.\",\n          true);\n\n  /**\n   * IMPORTANT: This table property is recognized but is not yet validated, enforced, or implemented\n   * by Kernel.\n   *\n   * <p>The names of specific columns to collect stats on for data skipping. If present, it takes\n   * precedence over {@link #DATA_SKIPPING_NUM_INDEXED_COLS}, and the system will only collect stats\n   * for columns that exactly match those specified. If a nested column is specified, the system\n   * will collect stats for all leaf fields of that column. If a non-existent column is specified,\n   * it will be ignored. Updating this config does not trigger stats re-collection, but redefines\n   * the stats schema of the table, i.e., it will change the behavior of future stats collection\n   * (e.g., in append and OPTIMIZE) as well as data skipping (e.g., the column stats not mentioned\n   * by this config will be ignored even if they exist).\n   *\n   * <p>The value is a comma-separated list of case-insensitive column identifiers. Each column\n   * identifier can consist of letters, digits, and underscores. If a column identifier includes\n   * special characters, the column name should be enclosed in backticks (`) to escape the special\n   * characters.\n   *\n   * <p>A column identifier can refer to one of the following: the name of a non-struct column, the\n   * leaf field's name of a struct column, or the name of a struct column. When a struct column's\n   * name is specified, statistics for all its leaf fields will be collected.\n   */\n  public static final TableConfig<Optional<String>> DATA_SKIPPING_STATS_COLUMNS =\n      new TableConfig<>(\n          \"delta.dataSkippingStatsColumns\",\n          null,\n          v -> Optional.ofNullable(v),\n          value -> true,\n          \"needs to be a comma-separated list of column identifiers.\",\n          true);\n\n  /**\n   * Table property that enables modifying the table in accordance with the Delta-Iceberg Writer\n   * Compatibility V1 ({@code icebergCompatWriterV1}) protocol.\n   */\n  public static final TableConfig<Boolean> ICEBERG_WRITER_COMPAT_V1_ENABLED =\n      new TableConfig<>(\n          \"delta.enableIcebergWriterCompatV1\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  /**\n   * Table property that enables modifying the table in accordance with the Delta-Iceberg Writer\n   * Compatibility V3 ({@code icebergCompatWriterV3}) protocol. V2 is skipped to align with the\n   * iceberg v3 spec.\n   */\n  public static final TableConfig<Boolean> ICEBERG_WRITER_COMPAT_V3_ENABLED =\n      new TableConfig<>(\n          \"delta.enableIcebergWriterCompatV3\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  public static class UniversalFormats {\n\n    /**\n     * The value that enables uniform exports to Iceberg for {@linkplain\n     * TableConfig#UNIVERSAL_FORMAT_ENABLED_FORMATS}.\n     *\n     * <p>{@link #ICEBERG_COMPAT_V2_ENABLED but also be set to true} to fully enable this feature.\n     */\n    public static final String FORMAT_ICEBERG = \"iceberg\";\n    /**\n     * The value to use to enable uniform exports to Hudi for {@linkplain\n     * TableConfig#UNIVERSAL_FORMAT_ENABLED_FORMATS}.\n     */\n    public static final String FORMAT_HUDI = \"hudi\";\n  }\n\n  private static final Collection<String> ALLOWED_UNIFORM_FORMATS =\n      Collections.unmodifiableList(\n          Arrays.asList(UniversalFormats.FORMAT_HUDI, UniversalFormats.FORMAT_ICEBERG));\n\n  /** Table config that allows for translation of Delta metadata to other table formats metadata. */\n  public static final TableConfig<Set<String>> UNIVERSAL_FORMAT_ENABLED_FORMATS =\n      new TableConfig<>(\n          \"delta.universalFormat.enabledFormats\",\n          null,\n          TableConfig::parseStringSet,\n          value -> ALLOWED_UNIFORM_FORMATS.containsAll(value),\n          String.format(\"each value must in the the set: %s\", ALLOWED_UNIFORM_FORMATS),\n          true);\n\n  /**\n   * Table property that enables modifying the table in accordance with the Delta-Variant Shredding\n   * protocol.\n   *\n   * @see <a\n   *     href=\"https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-shredding.md\">\n   *     Delta-Variant Shredding Protocol</a>\n   */\n  public static final TableConfig<Boolean> VARIANT_SHREDDING_ENABLED =\n      new TableConfig<>(\n          \"delta.enableVariantShredding\",\n          \"false\",\n          Boolean::valueOf,\n          value -> true,\n          \"needs to be a boolean.\",\n          true);\n\n  public static final TableConfig<String> MATERIALIZED_ROW_ID_COLUMN_NAME =\n      new TableConfig<>(\n          \"delta.rowTracking.materializedRowIdColumnName\",\n          null,\n          v -> v,\n          value -> true,\n          \"need to be a string.\",\n          false);\n\n  public static final TableConfig<String> MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME =\n      new TableConfig<>(\n          \"delta.rowTracking.materializedRowCommitVersionColumnName\",\n          null,\n          v -> v,\n          value -> true,\n          \"need to be a string.\",\n          false);\n\n  /** All the valid properties that can be set on the table. */\n  private static final Map<String, TableConfig<?>> VALID_PROPERTIES =\n      Collections.unmodifiableMap(\n          new HashMap<String, TableConfig<?>>() {\n            {\n              addConfig(this, APPEND_ONLY_ENABLED);\n              addConfig(this, CHANGE_DATA_FEED_ENABLED);\n              addConfig(this, CHECKPOINT_POLICY);\n              addConfig(this, DELETION_VECTORS_CREATION_ENABLED);\n              addConfig(this, TYPE_WIDENING_ENABLED);\n              addConfig(this, ROW_TRACKING_ENABLED);\n              addConfig(this, LOG_RETENTION);\n              addConfig(this, EXPIRED_LOG_CLEANUP_ENABLED);\n              addConfig(this, TOMBSTONE_RETENTION);\n              addConfig(this, CHECKPOINT_INTERVAL);\n              addConfig(this, IN_COMMIT_TIMESTAMPS_ENABLED);\n              addConfig(this, IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION);\n              addConfig(this, IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP);\n              addConfig(this, COLUMN_MAPPING_MODE);\n              addConfig(this, ICEBERG_COMPAT_V2_ENABLED);\n              addConfig(this, ICEBERG_COMPAT_V3_ENABLED);\n              addConfig(this, ICEBERG_WRITER_COMPAT_V1_ENABLED);\n              addConfig(this, ICEBERG_WRITER_COMPAT_V3_ENABLED);\n              addConfig(this, COLUMN_MAPPING_MAX_COLUMN_ID);\n              addConfig(this, DATA_SKIPPING_NUM_INDEXED_COLS);\n              addConfig(this, UNIVERSAL_FORMAT_ENABLED_FORMATS);\n              addConfig(this, MATERIALIZED_ROW_ID_COLUMN_NAME);\n              addConfig(this, MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME);\n              addConfig(this, VARIANT_SHREDDING_ENABLED);\n\n              // The below configs do not yet have their behavior correctly implemented in Kernel.\n              addConfig(this, DATA_SKIPPING_STATS_COLUMNS);\n            }\n          });\n\n  ///////////////////////////\n  // Static Helper Methods //\n  ///////////////////////////\n\n  /**\n   * Validates that the given new properties that the txn is trying to update in table. Properties\n   * that have `delta.` prefix in the key name should be in valid list and are editable. The caller\n   * is expected to store the returned properties in the table metadata after further validation\n   * from a protocol point of view. The returned properties will have the key's case normalized as\n   * defined in its {@link TableConfig}.\n   *\n   * @param newProperties the properties to validate\n   * @throws InvalidConfigurationValueException if any of the properties are invalid\n   * @throws UnknownConfigurationException if any of the properties are unknown\n   */\n  public static Map<String, String> validateAndNormalizeDeltaProperties(\n      Map<String, String> newProperties) {\n    Map<String, String> validatedProperties = new HashMap<>();\n    for (Map.Entry<String, String> kv : newProperties.entrySet()) {\n      String key = kv.getKey().toLowerCase(Locale.ROOT);\n      String value = kv.getValue();\n\n      boolean isTableFeatureOverrideKey =\n          key.startsWith(TableFeatures.SET_TABLE_FEATURE_SUPPORTED_PREFIX);\n      boolean isTableConfigKey = key.startsWith(\"delta.\");\n      // TableFeature override properties validation is handled separately in TransactionBuilder.\n      boolean shouldValidateProperties = isTableConfigKey && !isTableFeatureOverrideKey;\n      if (shouldValidateProperties) {\n        // If it is a delta table property, make sure it is a supported property and editable\n        if (!VALID_PROPERTIES.containsKey(key)) {\n          throw DeltaErrors.unknownConfigurationException(kv.getKey());\n        }\n\n        TableConfig<?> tableConfig = VALID_PROPERTIES.get(key);\n        if (!tableConfig.editable) {\n          throw DeltaErrors.cannotModifyTableProperty(kv.getKey());\n        }\n\n        tableConfig.validate(value);\n        validatedProperties.put(tableConfig.getKey(), value);\n      } else {\n        // allow unknown properties to be set (and preserve their original case!)\n        validatedProperties.put(kv.getKey(), value);\n      }\n    }\n    return validatedProperties;\n  }\n\n  private static void addConfig(HashMap<String, TableConfig<?>> configs, TableConfig<?> config) {\n    configs.put(config.getKey().toLowerCase(Locale.ROOT), config);\n  }\n\n  /////////////////////////////\n  // Member Fields / Methods //\n  /////////////////////////////\n\n  private final String key;\n  private final String defaultValue;\n  private final Function<String, T> fromString;\n  private final Predicate<T> validator;\n  private final boolean editable;\n  private final String helpMessage;\n\n  private TableConfig(\n      String key,\n      String defaultValue,\n      Function<String, T> fromString,\n      Predicate<T> validator,\n      String helpMessage,\n      boolean editable) {\n    this.key = key;\n    this.defaultValue = defaultValue;\n    this.fromString = fromString;\n    this.validator = validator;\n    this.helpMessage = helpMessage;\n    this.editable = editable;\n  }\n\n  /**\n   * Returns the value of the table property from the given metadata.\n   *\n   * @param metadata the table metadata\n   * @return the value of the table property\n   */\n  public T fromMetadata(Metadata metadata) {\n    return fromMetadata(metadata.getConfiguration());\n  }\n\n  /**\n   * Returns the value of the table property from the given configuration.\n   *\n   * @param configuration the table configuration\n   * @return the value of the table property\n   */\n  public T fromMetadata(Map<String, String> configuration) {\n    String value = configuration.getOrDefault(key, defaultValue);\n    validate(value);\n    return fromString.apply(value);\n  }\n\n  /**\n   * Returns the key of the table property.\n   *\n   * @return the key of the table property\n   */\n  public String getKey() {\n    return key;\n  }\n\n  private void validate(String value) {\n    T parsedValue = fromString.apply(value);\n    if (!validator.test(parsedValue)) {\n      throw DeltaErrors.invalidConfigurationValueException(key, value, helpMessage);\n    }\n  }\n\n  private static Set<String> parseStringSet(String value) {\n    if (value == null || value.isEmpty()) {\n      return Collections.emptySet();\n    }\n    String[] formats = value.split(\",\");\n    Set<String> config = new HashSet<>();\n\n    for (String format : formats) {\n      config.add(format.trim());\n    }\n    return config;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/TableImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Collections.emptyList;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.CheckpointAlreadyExistsException;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.internal.checkpoints.Checkpointer;\nimport io.delta.kernel.internal.checksum.ChecksumUtils;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.snapshot.SnapshotManager;\nimport io.delta.kernel.internal.util.Clock;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class TableImpl implements Table {\n\n  private static final Logger logger = LoggerFactory.getLogger(TableImpl.class);\n\n  public static Table forPath(Engine engine, String path) {\n    return forPath(engine, path, System::currentTimeMillis);\n  }\n\n  /**\n   * Instantiate a table object for the Delta Lake table at the given path. It takes an additional\n   * parameter called {@link Clock} which helps in testing.\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @param path location of the table.\n   * @param clock {@link Clock} instance to use for time-related operations.\n   * @return an instance of {@link Table} representing the Delta table at the given path\n   */\n  public static Table forPath(Engine engine, String path, Clock clock) {\n    String resolvedPath;\n    try {\n      resolvedPath =\n          wrapEngineExceptionThrowsIO(\n              () -> engine.getFileSystemClient().resolvePath(path), \"Resolving path %s\", path);\n    } catch (IOException io) {\n      throw new UncheckedIOException(io);\n    }\n    return new TableImpl(resolvedPath, clock);\n  }\n\n  private final String tablePath;\n  private final Checkpointer checkpointer;\n  private final SnapshotManager snapshotManager;\n  private final Clock clock;\n\n  public TableImpl(String tablePath, Clock clock) {\n    this.tablePath = tablePath;\n    final Path dataPath = new Path(tablePath);\n    final Path logPath = new Path(dataPath, \"_delta_log\");\n    this.checkpointer = new Checkpointer(logPath);\n    this.snapshotManager = new SnapshotManager(dataPath);\n    this.clock = clock;\n  }\n\n  @Override\n  public String getPath(Engine engine) {\n    return tablePath;\n  }\n\n  @Override\n  public SnapshotImpl getLatestSnapshot(Engine engine) throws TableNotFoundException {\n    SnapshotQueryContext snapshotContext = SnapshotQueryContext.forLatestSnapshot(tablePath);\n    return loadSnapshotWithMetrics(\n        engine,\n        () -> snapshotManager.buildLatestSnapshot(engine, snapshotContext),\n        snapshotContext);\n  }\n\n  @Override\n  public SnapshotImpl getSnapshotAsOfVersion(Engine engine, long versionId)\n      throws TableNotFoundException {\n    SnapshotQueryContext snapshotContext =\n        SnapshotQueryContext.forVersionSnapshot(tablePath, versionId);\n    return loadSnapshotWithMetrics(\n        engine,\n        () -> snapshotManager.getSnapshotAt(engine, versionId, snapshotContext),\n        snapshotContext);\n  }\n\n  @Override\n  public SnapshotImpl getSnapshotAsOfTimestamp(Engine engine, long millisSinceEpochUTC)\n      throws TableNotFoundException {\n    SnapshotQueryContext snapshotContext =\n        SnapshotQueryContext.forTimestampSnapshot(tablePath, millisSinceEpochUTC);\n    SnapshotImpl latestSnapshot = getLatestSnapshot(engine);\n    return loadSnapshotWithMetrics(\n        engine,\n        () ->\n            snapshotManager.getSnapshotForTimestamp(\n                engine, latestSnapshot, millisSinceEpochUTC, snapshotContext),\n        snapshotContext);\n  }\n\n  @Override\n  public void checkpoint(Engine engine, long version)\n      throws TableNotFoundException, CheckpointAlreadyExistsException, IOException {\n    final SnapshotImpl snapshotToCheckpoint = getSnapshotAsOfVersion(engine, version);\n    checkpointer.checkpoint(engine, clock, snapshotToCheckpoint);\n  }\n\n  @Override\n  public void checksum(Engine engine, long version) throws TableNotFoundException, IOException {\n    final LogSegment logSegmentAtVersion =\n        snapshotManager.getLogSegmentForVersion(engine, Optional.of(version));\n    ChecksumUtils.computeStateAndWriteChecksum(engine, logSegmentAtVersion);\n  }\n\n  @Override\n  public TransactionBuilder createTransactionBuilder(\n      Engine engine, String engineInfo, Operation operation) {\n    return new TransactionBuilderImpl(this, engineInfo, operation);\n  }\n\n  public TransactionBuilder createReplaceTableTransactionBuilder(Engine engine, String engineInfo) {\n    return new ReplaceTableTransactionBuilderImpl(this, engineInfo);\n  }\n\n  public Clock getClock() {\n    return clock;\n  }\n\n  /**\n   * Returns delta actions for each version between startVersion and endVersion. Only returns the\n   * actions requested in actionSet.\n   *\n   * <p>For the returned columnar batches:\n   *\n   * <ul>\n   *   <li>Each row within the same batch is guaranteed to have the same commit version\n   *   <li>The batch commit versions are monotonically increasing\n   *   <li>The top-level columns include \"version\", \"timestamp\", and the actions requested in\n   *       actionSet. \"version\" and \"timestamp\" are the first and second columns in the schema,\n   *       respectively. The remaining columns are based on the actions requested and each have the\n   *       schema found in {@code DeltaAction.schema}.\n   * </ul>\n   *\n   * @param engine {@link Engine} instance to use in Delta Kernel.\n   * @param startVersion start version (inclusive)\n   * @param endVersion end version (inclusive)\n   * @param actionSet the actions to read and return from the JSON log files\n   * @return an iterator of batches where each row in the batch has exactly one non-null action and\n   *     its commit version and timestamp\n   * @throws TableNotFoundException if the table does not exist or if it is not a delta table\n   * @throws KernelException if a commit file does not exist for any of the versions in the provided\n   *     range\n   * @throws KernelException if provided an invalid version range\n   * @throws KernelException if the version range contains a version with reader protocol that is\n   *     unsupported by Kernel\n   */\n  public CloseableIterator<ColumnarBatch> getChanges(\n      Engine engine,\n      long startVersion,\n      long endVersion,\n      Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n    checkArgument(startVersion >= 0, \"startVersion must be >= 0\");\n    checkArgument(startVersion <= endVersion, \"startVersion must be <= endVersion\");\n\n    List<FileStatus> commitFiles =\n        DeltaLogActionUtils.getCommitFilesForVersionRange(\n            engine, new Path(tablePath), startVersion, Optional.of(endVersion));\n\n    // Get CommitActions for each file\n    CloseableIterator<io.delta.kernel.CommitActions> commits =\n        DeltaLogActionUtils.getActionsFromCommitFilesWithProtocolValidation(\n            engine, tablePath, commitFiles, actionSet);\n\n    // Flatten and add version/timestamp columns\n    return TableChangesUtils.flattenCommitsAndAddMetadata(engine, commits);\n  }\n\n  protected Path getDataPath() {\n    return new Path(tablePath);\n  }\n\n  protected Path getLogPath() {\n    return new Path(tablePath, \"_delta_log\");\n  }\n\n  /**\n   * Returns the latest version that was committed before or at {@code millisSinceEpochUTC}. If no\n   * version exists, throws a {@link KernelException}\n   *\n   * <p>Specifically:\n   *\n   * <ul>\n   *   <li>if a commit version exactly matches the provided timestamp, we return it\n   *   <li>else, we return the latest commit version with a timestamp less than the provided one\n   *   <li>If the provided timestamp is less than the timestamp of any committed version, we throw\n   *       an error.\n   * </ul>\n   *\n   * .\n   *\n   * @param millisSinceEpochUTC the number of milliseconds since midnight, January 1, 1970 UTC\n   * @return latest commit that happened before or at {@code timestamp}.\n   * @throws KernelException if the timestamp is less than the timestamp of any committed version\n   * @throws TableNotFoundException if no delta table is found\n   */\n  public long getVersionBeforeOrAtTimestamp(Engine engine, long millisSinceEpochUTC) {\n    SnapshotImpl latestSnapshot = (SnapshotImpl) getLatestSnapshot(engine);\n    return DeltaHistoryManager.getVersionBeforeOrAtTimestamp(\n        engine,\n        getLogPath(),\n        millisSinceEpochUTC,\n        latestSnapshot,\n        emptyList() /* catalogCommits */);\n  }\n\n  /**\n   * Returns the latest version that was committed at or after {@code millisSinceEpochUTC}. If no\n   * version exists, throws a {@link KernelException}\n   *\n   * <p>Specifically:\n   *\n   * <ul>\n   *   <li>if a commit version exactly matches the provided timestamp, we return it\n   *   <li>else, we return the earliest commit version with a timestamp greater than the provided\n   *       one\n   *   <li>If the provided timestamp is larger than the timestamp of any committed version, we throw\n   *       an error.\n   * </ul>\n   *\n   * .\n   *\n   * @param millisSinceEpochUTC the number of milliseconds since midnight, January 1, 1970 UTC\n   * @return latest commit that happened at or before {@code timestamp}.\n   * @throws KernelException if the timestamp is more than the timestamp of any committed version\n   * @throws TableNotFoundException if no delta table is found\n   */\n  public long getVersionAtOrAfterTimestamp(Engine engine, long millisSinceEpochUTC) {\n    SnapshotImpl latestSnapshot = (SnapshotImpl) getLatestSnapshot(engine);\n    return DeltaHistoryManager.getVersionAtOrAfterTimestamp(\n        engine,\n        getLogPath(),\n        millisSinceEpochUTC,\n        latestSnapshot,\n        emptyList() /* catalogCommits */);\n  }\n\n  /** Helper method that loads a snapshot with proper metrics recording, logging, and reporting. */\n  private SnapshotImpl loadSnapshotWithMetrics(\n      Engine engine, Supplier<SnapshotImpl> loadSnapshot, SnapshotQueryContext snapshotContext)\n      throws TableNotFoundException {\n    try {\n      final SnapshotImpl snapshot =\n          snapshotContext.getSnapshotMetrics().loadSnapshotTotalTimer.time(loadSnapshot);\n\n      logger.info(\n          \"[{}] Took {}ms to load snapshot (version = {}) for snapshot query {}\",\n          tablePath,\n          snapshotContext.getSnapshotMetrics().loadSnapshotTotalTimer.totalDurationMs(),\n          snapshot.getVersion(),\n          snapshotContext.getQueryDisplayStr());\n\n      engine\n          .getMetricsReporters()\n          .forEach(reporter -> reporter.report(snapshot.getSnapshotReport()));\n\n      return snapshot;\n    } catch (Exception e) {\n      snapshotContext.recordSnapshotErrorReport(engine, e);\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/TransactionBuilderImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.DeltaErrors.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Collections.emptyMap;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.TableAlreadyExistsException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter;\nimport io.delta.kernel.internal.rowtracking.MaterializedRowTrackingColumn;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class TransactionBuilderImpl implements TransactionBuilder {\n  private static final Logger logger = LoggerFactory.getLogger(TransactionBuilderImpl.class);\n\n  private final long currentTimeMillis = System.currentTimeMillis();\n  private final String engineInfo;\n  private final Operation operation;\n  private Optional<List<String>> partitionColumns = Optional.empty();\n  private Optional<SetTransaction> setTxnOpt = Optional.empty();\n  private Optional<Map<String, String>> tableProperties = Optional.empty();\n  private Optional<Set<String>> unsetTablePropertiesKeys = Optional.empty();\n  private boolean needDomainMetadataSupport = false;\n\n  // The original clustering columns provided by the user when building the transaction.\n  // This represents logical column references before schema resolution is applied.\n  // (e.g., case sensitivity, column mapping)\n  private Optional<List<Column>> inputLogicalClusteringColumns = Optional.empty();\n  // The resolved clustering columns that will be written into domain metadata in the txn. This\n  // reflects case-preserved column names or physical column names if column mapping is enabled.\n  // This is set during transaction building after the schema has been updated/resolved with any\n  // column mapping info. These are the physical columns of `inputLogicalClusteringColumns`.\n  private Optional<List<Column>> newResolvedClusteringColumns = Optional.empty();\n\n  protected final TableImpl table;\n  protected Optional<StructType> schema = Optional.empty();\n  private Optional<Integer> userProvidedMaxRetries = Optional.empty();\n\n  /** Number of commits between producing a log compaction file. */\n  private int logCompactionInterval = 0;\n\n  public TransactionBuilderImpl(TableImpl table, String engineInfo, Operation operation) {\n    this.table = table;\n    this.engineInfo = engineInfo;\n    this.operation = operation;\n  }\n\n  @Override\n  public TransactionBuilder withSchema(Engine engine, StructType newSchema) {\n    this.schema = Optional.of(newSchema); // will be verified as part of the build() call\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withPartitionColumns(Engine engine, List<String> partitionColumns) {\n    if (!partitionColumns.isEmpty()) {\n      this.partitionColumns = Optional.of(partitionColumns);\n    }\n    return this;\n  }\n\n  /**\n   * There are three possible cases when handling clustering columns via `withClusteringColumns`:\n   *\n   * <ul>\n   *   <li>Clustering columns are not set (i.e., `withClusteringColumns` is not called):\n   *       <ul>\n   *         <li>No changes are made related to clustering.\n   *         <li>For table creation, the table is initialized as a non-clustered table.\n   *         <li>For table updates, the existing clustered or non-clustered state remains unchanged\n   *             (i.e., no protocol or domain metadata updates).\n   *       </ul>\n   *   <li>Clustering columns are an empty list:\n   *       <ul>\n   *         <li>This is equivalent to executing `ALTER TABLE ... CLUSTER BY NONE` in Delta.\n   *         <li>The table remains a clustered table, but its clustering domain metadata is updated\n   *             to reflect an empty list of clustering columns.\n   *       </ul>\n   *   <li>Clustering columns are a non-empty list:\n   *       <ul>\n   *         <li>The table is treated as a clustered table.\n   *         <li>We update the protocol (if needed) to include clustering writer support and set the\n   *             clustering domain metadata accordingly.\n   *       </ul>\n   * </ul>\n   */\n  @Override\n  public TransactionBuilder withClusteringColumns(Engine engine, List<Column> clusteringColumns) {\n    this.inputLogicalClusteringColumns = Optional.of(clusteringColumns);\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withTransactionId(\n      Engine engine, String applicationId, long transactionVersion) {\n    SetTransaction txnId =\n        new SetTransaction(\n            requireNonNull(applicationId, \"applicationId is null\"),\n            transactionVersion,\n            Optional.of(currentTimeMillis));\n    this.setTxnOpt = Optional.of(txnId);\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withTableProperties(Engine engine, Map<String, String> properties) {\n    this.tableProperties =\n        Optional.of(\n            Collections.unmodifiableMap(\n                TableConfig.validateAndNormalizeDeltaProperties(properties)));\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withTablePropertiesRemoved(Set<String> propertyKeys) {\n    checkArgument(\n        propertyKeys.stream().noneMatch(key -> key.toLowerCase(Locale.ROOT).startsWith(\"delta.\")),\n        \"Unsetting 'delta.' table properties is currently unsupported\");\n    this.unsetTablePropertiesKeys = Optional.of(Collections.unmodifiableSet(propertyKeys));\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withMaxRetries(int maxRetries) {\n    checkArgument(maxRetries >= 0, \"maxRetries must be >= 0\");\n    this.userProvidedMaxRetries = Optional.of(maxRetries);\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withLogCompactionInverval(int logCompactionInterval) {\n    checkArgument(logCompactionInterval >= 0, \"logCompactionInterval must be >= 0\");\n    this.logCompactionInterval = logCompactionInterval;\n    return this;\n  }\n\n  @Override\n  public TransactionBuilder withDomainMetadataSupported() {\n    needDomainMetadataSupport = true;\n    return this;\n  }\n\n  @Override\n  public Transaction build(Engine engine) {\n    if (operation == Operation.REPLACE_TABLE) {\n      throw new UnsupportedOperationException(\"REPLACE TABLE is not yet supported\");\n    }\n    SnapshotImpl snapshot;\n    try {\n      snapshot = table.getLatestSnapshot(engine);\n      if (operation == Operation.CREATE_TABLE) {\n        throw new TableAlreadyExistsException(table.getPath(engine), \"Operation = CREATE_TABLE\");\n      }\n      return buildTransactionInternal(engine, false /* isCreateOrReplace */, Optional.of(snapshot));\n    } catch (TableNotFoundException tblf) {\n      String tablePath = table.getPath(engine);\n      logger.info(\"Table {} doesn't exist yet. Trying to create a new table.\", tablePath);\n      schema.orElseThrow(() -> requiresSchemaForNewTable(tablePath));\n      return buildTransactionInternal(engine, true /* isNewTableDef */, Optional.empty());\n    }\n  }\n\n  /**\n   * Returns a built {@link Transaction} for this transaction builder (with the input provided by\n   * the user) given the provided parameters. This includes validation and updates as defined in the\n   * builder.\n   *\n   * @param isCreateOrReplace whether we are defining a new table definition or not. This determines\n   *     what metadata to commit in the returned transaction, and what operations to allow or block.\n   * @param latestSnapshot the latest snapshot of the table if it exists. For a new table this\n   *     should be empty. For replace table, this should be the latest snapshot of the table. This\n   *     is used to validate that we can write to the table, and to get the protocol/metadata when\n   *     isCreateOrReplace=false.\n   */\n  protected TransactionImpl buildTransactionInternal(\n      Engine engine, boolean isCreateOrReplace, Optional<SnapshotImpl> latestSnapshot) {\n    checkArgument(\n        isCreateOrReplace || latestSnapshot.isPresent(),\n        \"Existing snapshot must be provided if not defining a new table definition\");\n    latestSnapshot.ifPresent(\n        snapshot -> validateWriteToExistingTable(engine, snapshot, isCreateOrReplace));\n    validateTransactionInputs(engine, isCreateOrReplace);\n\n    final Committer committer =\n        latestSnapshot\n            .map(SnapshotImpl::getCommitter)\n            .orElse(DefaultFileSystemManagedTableOnlyCommitter.INSTANCE);\n\n    boolean enablesDomainMetadataSupport =\n        needDomainMetadataSupport\n            && latestSnapshot.isPresent()\n            && !latestSnapshot\n                .get()\n                .getProtocol()\n                .supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE);\n\n    boolean needsMetadataOrProtocolUpdate =\n        isCreateOrReplace\n            || schema.isPresent() // schema evolution\n            || tableProperties.isPresent() // table properties updated\n            || unsetTablePropertiesKeys.isPresent() // table properties unset\n            || inputLogicalClusteringColumns.isPresent() // clustering columns changed\n            || enablesDomainMetadataSupport; // domain metadata support added\n\n    if (!needsMetadataOrProtocolUpdate) {\n      // Return early if there is no metadata or protocol updates and isCreateOrReplace=false\n      return new TransactionImpl(\n          false, // isCreateOrReplace\n          table.getDataPath(),\n          latestSnapshot,\n          engineInfo,\n          operation,\n          Optional.empty(), // newProtocol\n          Optional.empty(), // newMetadata\n          committer,\n          setTxnOpt,\n          Optional.empty(), /* clustering cols=empty */\n          userProvidedMaxRetries,\n          logCompactionInterval,\n          table.getClock());\n    }\n\n    // Instead of special casing enabling domain metadata, we should just add them\n    // to the table properties which we already handle.\n    boolean domainMetadataEnabled =\n        !isCreateOrReplace\n            && latestSnapshot\n                .get()\n                .getProtocol()\n                .supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE);\n    if (needDomainMetadataSupport && !domainMetadataEnabled) {\n      Map<String, String> tablePropertiesWithDomainMetadataEnabled =\n          new HashMap<>(tableProperties.orElse(emptyMap()));\n      tablePropertiesWithDomainMetadataEnabled.put(\n          TableFeatures.DOMAIN_METADATA_W_FEATURE.getTableFeatureSupportKey(), \"supported\");\n      tableProperties = Optional.of(tablePropertiesWithDomainMetadataEnabled);\n    }\n\n    TransactionMetadataFactory.Output outputMetadata;\n    if (!isCreateOrReplace) {\n      outputMetadata =\n          TransactionMetadataFactory.buildUpdateTableMetadata(\n              table.getPath(engine),\n              latestSnapshot.get(),\n              tableProperties,\n              unsetTablePropertiesKeys,\n              schema,\n              inputLogicalClusteringColumns);\n    } else if (latestSnapshot.isPresent()) { // is REPLACE\n      outputMetadata =\n          TransactionMetadataFactory.buildReplaceTableMetadata(\n              table.getPath(engine),\n              latestSnapshot.get(),\n              // when isCreateOrReplace we know schema is present\n              schema.get(),\n              tableProperties.orElse(emptyMap()),\n              partitionColumns,\n              inputLogicalClusteringColumns);\n    } else {\n      outputMetadata =\n          TransactionMetadataFactory.buildCreateTableMetadata(\n              table.getPath(engine),\n              // when isCreateOrReplace we know schema is present\n              schema.get(),\n              tableProperties.orElse(emptyMap()),\n              partitionColumns,\n              inputLogicalClusteringColumns,\n              Optional.empty() /* committerOpt */);\n    }\n\n    return new TransactionImpl(\n        isCreateOrReplace,\n        table.getDataPath(),\n        latestSnapshot,\n        engineInfo,\n        operation,\n        outputMetadata.newProtocol,\n        outputMetadata.newMetadata,\n        committer,\n        setTxnOpt,\n        outputMetadata.physicalNewClusteringColumns,\n        userProvidedMaxRetries,\n        logCompactionInterval,\n        table.getClock());\n  }\n\n  /**\n   * Validates that Kernel can write to the existing table with the latest snapshot as provided.\n   * This means (1) Kernel supports the reader and writer protocol of the table (2) if a transaction\n   * identifier has been provided in this txn builder, a concurrent write has not already committed\n   * this transaction (3) Updating a partitioned table with clustering columns is not allowed (4)\n   * Row tracking configs are present when row tracking is enabled.\n   */\n  protected void validateWriteToExistingTable(\n      Engine engine, SnapshotImpl snapshot, boolean isCreateOrReplace) {\n    // Validate the table has no features that Kernel doesn't yet support writing into it.\n    TableFeatures.validateKernelCanWriteToTable(\n        snapshot.getProtocol(), snapshot.getMetadata(), table.getPath(engine));\n    setTxnOpt.ifPresent(\n        txnId -> {\n          Optional<Long> lastTxnVersion =\n              snapshot.getLatestTransactionVersion(engine, txnId.getAppId());\n          if (lastTxnVersion.isPresent() && lastTxnVersion.get() >= txnId.getVersion()) {\n            throw DeltaErrors.concurrentTransaction(\n                txnId.getAppId(), txnId.getVersion(), lastTxnVersion.get());\n          }\n        });\n    if (!isCreateOrReplace\n        && inputLogicalClusteringColumns.isPresent()\n        && snapshot.getMetadata().getPartitionColumns().getSize() != 0) {\n      throw DeltaErrors.enablingClusteringOnPartitionedTableNotAllowed(\n          table.getPath(engine),\n          snapshot.getMetadata().getPartitionColNames(),\n          inputLogicalClusteringColumns.get());\n    }\n    // Validate row tracking configs are present when row tracking is enabled. This must run\n    // on every write, including the early-return path that skips TransactionMetadataFactory.\n    if (!isCreateOrReplace) {\n      MaterializedRowTrackingColumn.validateRowTrackingConfigsNotMissing(\n          snapshot.getMetadata(), table.getPath(engine));\n    }\n  }\n\n  /**\n   * Validates the inputs to this transaction builder. This includes\n   *\n   * <ul>\n   *   <li>Partition columns are only set for a new table definition.\n   *   <li>Partition columns and clustering columns are not set at the same time.\n   *   <li>The provided schema is valid.\n   *   <li>The provided partition columns are valid.\n   *   <li>The provided table properties to set and unset do not overlap with each other.\n   * </ul>\n   */\n  protected void validateTransactionInputs(Engine engine, boolean isCreateOrReplace) {\n    String tablePath = table.getPath(engine);\n    if (!isCreateOrReplace) {\n      if (partitionColumns.isPresent()) {\n        throw tableAlreadyExists(\n            tablePath,\n            \"Table already exists, but provided new partition columns. \"\n                + \"Partition columns can only be set on a new table.\");\n      }\n    } else {\n      checkArgument(\n          !(partitionColumns.isPresent() && inputLogicalClusteringColumns.isPresent()),\n          \"Partition Columns and Clustering Columns cannot be set at the same time\");\n    }\n\n    if (unsetTablePropertiesKeys.isPresent() && tableProperties.isPresent()) {\n      Set<String> invalidPropertyKeys =\n          unsetTablePropertiesKeys.get().stream()\n              .filter(key -> tableProperties.get().containsKey(key))\n              .collect(toSet());\n      if (!invalidPropertyKeys.isEmpty()) {\n        throw DeltaErrors.overlappingTablePropertiesSetAndUnset(invalidPropertyKeys);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/TransactionImpl.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.TableConfig.*;\nimport static io.delta.kernel.internal.actions.SingleAction.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\nimport static java.util.Collections.emptyMap;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.commit.CommitFailedException;\nimport io.delta.kernel.commit.CommitMetadata;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.*;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.clustering.ClusteringUtils;\nimport io.delta.kernel.internal.compaction.LogCompactionWriter;\nimport io.delta.kernel.internal.data.TransactionStateRow;\nimport io.delta.kernel.internal.files.ParsedDeltaData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.hook.CheckpointHook;\nimport io.delta.kernel.internal.hook.ChecksumFullHook;\nimport io.delta.kernel.internal.hook.ChecksumSimpleHook;\nimport io.delta.kernel.internal.hook.LogCompactionHook;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext;\nimport io.delta.kernel.internal.metrics.TransactionMetrics;\nimport io.delta.kernel.internal.metrics.TransactionReportImpl;\nimport io.delta.kernel.internal.replay.ConflictChecker;\nimport io.delta.kernel.internal.replay.ConflictChecker.TransactionRebaseState;\nimport io.delta.kernel.internal.replay.LogReplay;\nimport io.delta.kernel.internal.rowtracking.RowTracking;\nimport io.delta.kernel.internal.rowtracking.RowTrackingMetadataDomain;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.stats.FileSizeHistogram;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.*;\nimport io.delta.kernel.internal.util.Clock;\nimport io.delta.kernel.internal.util.InCommitTimestampUtils;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.metrics.TransactionMetricsResult;\nimport io.delta.kernel.metrics.TransactionReport;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class TransactionImpl implements Transaction {\n\n  ///////////////////////////////\n  // Static methods and fields //\n  ///////////////////////////////\n\n  /** Get the part of the schema of the table that needs the statistics to be collected per file. */\n  public static List<Column> getStatisticsColumns(Row transactionState) {\n    int numIndexedCols =\n        TableConfig.DATA_SKIPPING_NUM_INDEXED_COLS.fromMetadata(\n            TransactionStateRow.getConfiguration(transactionState));\n\n    // Get the list of partition columns to exclude\n    Set<String> partitionColumns =\n        new HashSet<>(TransactionStateRow.getPartitionColumnsList(transactionState));\n\n    // Collect the leaf-level columns for statistics calculation.\n    // This call selects only the first 'numIndexedCols' leaf columns from the logical schema,\n    // excluding any column whose top-level name appears in 'partitionColumns'.\n    // NOTE: Nested columns (i.e. each leaf within a StructType) count individually toward the\n    // numIndexedCols limit (not Map/ArrayTypes - they're not stats compatible types).\n    //\n    // For example, given the following schema:\n    //   root\n    //     ├─ col1 (int)\n    //     ├─ col2 (string)\n    //     └─ col3 (struct)\n    //           ├─ a (int)\n    //           └─ b (double)\n    //\n    // And if 'numIndexedCols' is set to 2 with no partition columns to exclude, then the returned\n    // stats columns\n    // would be: [col1, col2]. If 'col1' were a partition column, the returned list would be:\n    // [col2, col3.a] (assuming col3.a is encountered before col3.b).\n    return SchemaUtils.collectLeafColumns(\n        TransactionStateRow.getPhysicalSchema(transactionState), partitionColumns, numIndexedCols);\n  }\n\n  private static final Logger logger = LoggerFactory.getLogger(TransactionImpl.class);\n  public static final int DEFAULT_READ_VERSION = 1;\n  public static final int DEFAULT_WRITE_VERSION = 2;\n  /**\n   * Default retries for concurrent write exceptions to resolve conflicts and retry commit. In\n   * Delta-Spark, for historical reasons the number of retries is really high (10m). We are starting\n   * with a lower number by default for now. If this is not sufficient we can update it.\n   */\n  private static final int DEFAULT_MAX_RETRIES = 200;\n\n  /////////////////////\n  // Instance fields //\n  /////////////////////\n\n  private final UUID txnId = UUID.randomUUID();\n\n  /** If the transaction is defining a new table from scratch (i.e. create table, replace table) */\n  private final boolean isCreateOrReplace;\n\n  private final Path dataPath;\n  private final Path logPath;\n  private final Optional<SnapshotImpl> readSnapshotOpt;\n  private final String engineInfo;\n  private final Operation operation;\n  private final Protocol protocol;\n  private final boolean shouldUpdateProtocol;\n  private Metadata metadata;\n  private boolean shouldUpdateMetadata;\n  private final Committer committer;\n  private final Optional<SetTransaction> setTxnOpt;\n  /**\n   * The new clustering columns to write in the domain metadata in this transaction if provided.\n   *\n   * <ul>\n   *   <li>Optional.empty() - do not update the clustering domain metadata in this txn\n   *   <li>Optional.of([]) - update the clustering domain metadata to store an empty list in this\n   *       txn\n   *   <li>Optional.of([col1, col2]) - update the clustering domain metadata to store these columns\n   *       in this txn\n   * </ul>\n   */\n  private final Optional<List<Column>> newClusteringColumnsOpt;\n\n  private int maxRetries;\n  private final int logCompactionInterval;\n  private final Clock clock;\n  private final DomainMetadataState domainMetadataState = new DomainMetadataState();\n  private Optional<CRCInfo> currentCrcInfo;\n  private Optional<Long> providedRowIdHighWatermark = Optional.empty();\n  private Supplier<Map<String, String>> committerProperties = Collections::emptyMap;\n  private boolean closed; // To avoid trying to commit the same transaction again.\n\n  public TransactionImpl(\n      boolean isCreateOrReplace,\n      Path dataPath,\n      Optional<SnapshotImpl> readSnapshotOpt,\n      String engineInfo,\n      Operation operation,\n      Optional<Protocol> newProtocol,\n      Optional<Metadata> newMetadata,\n      Committer committer,\n      Optional<SetTransaction> setTxnOpt,\n      Optional<List<Column>> newClusteringColumnsOpt,\n      Optional<Integer> maxRetriesOpt,\n      int logCompactionInterval,\n      Clock clock) {\n    checkArgument(isCreateOrReplace || readSnapshotOpt.isPresent());\n    // For a new table, a protocol and metadata must be provided\n    checkArgument(\n        (newProtocol.isPresent() && newMetadata.isPresent()) || readSnapshotOpt.isPresent());\n    // TODO: look into migrating entire class into just (newMetadata, newProtocol, readSnapshotOpt)\n\n    this.isCreateOrReplace = isCreateOrReplace;\n    this.dataPath = dataPath;\n    this.logPath = new Path(dataPath, \"_delta_log\");\n    this.readSnapshotOpt = readSnapshotOpt;\n    this.engineInfo = engineInfo;\n    this.operation = operation;\n    this.protocol = newProtocol.orElseGet(() -> readSnapshotOpt.get().getProtocol());\n    this.shouldUpdateProtocol = newProtocol.isPresent();\n    this.metadata = newMetadata.orElseGet(() -> readSnapshotOpt.get().getMetadata());\n    this.shouldUpdateMetadata = newMetadata.isPresent();\n    this.committer = committer;\n    this.setTxnOpt = setTxnOpt;\n    this.newClusteringColumnsOpt = newClusteringColumnsOpt;\n    this.maxRetries = maxRetriesOpt.orElse(DEFAULT_MAX_RETRIES);\n    this.logCompactionInterval = logCompactionInterval;\n    this.clock = clock;\n    this.currentCrcInfo = readSnapshotOpt.flatMap(SnapshotImpl::getCurrentCrcInfo);\n  }\n\n  ////////////////\n  // Public API //\n  ////////////////\n\n  @Override\n  public Row getTransactionState(Engine engine) {\n    return TransactionStateRow.of(metadata, protocol, dataPath.toString(), maxRetries);\n  }\n\n  @Override\n  public Committer getCommitter() {\n    return committer;\n  }\n\n  @Override\n  public List<String> getPartitionColumns(Engine engine) {\n    return VectorUtils.toJavaList(metadata.getPartitionColumns());\n  }\n\n  @Override\n  public StructType getSchema(Engine engine) {\n    return metadata.getSchema();\n  }\n\n  @Override\n  public long getReadTableVersion() {\n    return readSnapshotOpt.map(SnapshotImpl::getVersion).orElse(-1L);\n  }\n\n  @Override\n  public void withCommitterProperties(Supplier<Map<String, String>> committerProperties) {\n    this.committerProperties = requireNonNull(committerProperties, \"committerProperties is null\");\n  }\n\n  @Override\n  public void addDomainMetadata(String domain, String config) {\n    checkState(\n        TableFeatures.isDomainMetadataSupported(protocol),\n        \"Unable to add domain metadata when the domain metadata table feature is disabled\");\n    checkArgument(\n        DomainMetadata.isUserControlledDomain(domain)\n            || DomainMetadata.isSystemDomainSupportedSetFromTxn(domain),\n        \"Setting a non-supported system-controlled domain is not allowed: \" + domain);\n\n    // Specific handling for system domain metadata\n    if (DomainMetadata.isSystemDomainSupportedSetFromTxn(domain)) {\n      handleSystemDomainMetadata(domain, config);\n    } else {\n      domainMetadataState.addDomain(domain, config);\n    }\n  }\n\n  @Override\n  public void removeDomainMetadata(String domain) {\n    checkState(\n        TableFeatures.isDomainMetadataSupported(protocol),\n        \"Unable to add domain metadata when the domain metadata table feature is disabled\");\n    checkArgument(\n        DomainMetadata.isUserControlledDomain(domain),\n        \"Removing a system-controlled domain is not allowed: \" + domain);\n    domainMetadataState.removeDomain(domain);\n  }\n\n  @Override\n  public TransactionCommitResult commit(Engine engine, CloseableIterable<Row> dataActions)\n      throws ConcurrentWriteException {\n    checkState(!closed, \"Transaction is already attempted to commit. Create a new transaction.\");\n    // For a new table or when fileSizeHistogram is available in the CRC of the readSnapshotOpt\n    // we update it in the commit. When it is not available we do nothing.\n    TransactionMetrics txnMetrics =\n        readSnapshotOpt\n            .map(\n                snapshot ->\n                    TransactionMetrics.withExistingTableFileSizeHistogram(\n                        snapshot.getCurrentCrcInfo().flatMap(CRCInfo::getFileSizeHistogram)))\n            .orElse(TransactionMetrics.forNewTable());\n    try {\n      final Tuple2<ParsedDeltaData, Optional<Long>> committedDeltaAndIct =\n          txnMetrics.totalCommitTimer.time(() -> commitWithRetry(engine, dataActions, txnMetrics));\n\n      return buildTransactionCommitResult(\n          engine, committedDeltaAndIct._1, txnMetrics, committedDeltaAndIct._2);\n    } catch (Exception e) {\n      recordTransactionReport(\n          engine,\n          Optional.empty() /* committedVersion */,\n          getEffectiveClusteringColumns(),\n          txnMetrics,\n          Optional.of(e) /* exception */);\n      throw e;\n    }\n  }\n\n  //////////////////\n  // Internal API //\n  //////////////////\n\n  @VisibleForTesting\n  public void addDomainMetadataInternal(String domain, String config) {\n    domainMetadataState.addDomain(domain, config);\n  }\n\n  @VisibleForTesting\n  public void removeDomainMetadataInternal(String domain) {\n    domainMetadataState.removeDomain(domain);\n  }\n\n  public Path getDataPath() {\n    return dataPath;\n  }\n\n  public Path getLogPath() {\n    return logPath;\n  }\n\n  public Protocol getProtocol() {\n    return protocol;\n  }\n\n  public Optional<SetTransaction> getSetTxnOpt() {\n    return setTxnOpt;\n  }\n\n  public Optional<List<Column>> getEffectiveClusteringColumns() {\n    if (isCreateOrReplace) {\n      // if isCreateOrReplace return the columns set in this txn\n      return newClusteringColumnsOpt;\n    } else { // since !isCreateOrReplace must be an update to an existing table\n      if (newClusteringColumnsOpt.isPresent()) {\n        // if the clustering columns are being updated in this txn return those\n        return newClusteringColumnsOpt;\n      } else {\n        // else, return the current existing clustering columns (readSnapshotOpt must be present)\n        return readSnapshotOpt.flatMap(SnapshotImpl::getPhysicalClusteringColumns);\n      }\n    }\n  }\n\n  ///////////////////////////////\n  // Other getters and setters //\n  ///////////////////////////////\n\n  private boolean isReplaceTable() {\n    return isCreateOrReplace && readSnapshotOpt.isPresent();\n  }\n\n  /**\n   * Returns the maximum number of commit attempts, including the first attempt.\n   *\n   * <p>This is explicitly a method instead of a constant as the maxRetries variable is itself\n   * mutable, and can for example be set to 0 when the rowIdHighWatermark is explicitly provided.\n   */\n  private int getMaxCommitAttempts() {\n    return maxRetries + 1; // +1 because the first attempt is a try, not a retry.\n  }\n\n  private Optional<Boolean> isBlindAppend() {\n    // TODO: for now we hard code this to false to avoid erroneously setting this to true for a\n    //  non-blind-append operation. We should revisit how to safely set this to true for actual\n    //  blind appends.\n    return Optional.of(false);\n  }\n\n  private void updateMetadata(Metadata metadata) {\n    logger.info(\n        \"Updated metadata from {} to {}\", shouldUpdateMetadata ? this.metadata : \"-\", metadata);\n    this.metadata = metadata;\n    this.shouldUpdateMetadata = true;\n  }\n\n  private void handleSystemDomainMetadata(String domain, String config) {\n    if (domain.equals(RowTrackingMetadataDomain.DOMAIN_NAME)) {\n      if (!TableFeatures.isRowTrackingSupported(protocol)) {\n        throw DeltaErrors.rowTrackingRequiredForRowIdHighWatermark(dataPath.toString(), config);\n      }\n      long providedHighWaterMark =\n          RowTrackingMetadataDomain.fromJsonConfiguration(config).getRowIdHighWaterMark();\n      checkArgument(providedHighWaterMark >= 0, \"rowIdHighWatermark must be >= 0\");\n      this.providedRowIdHighWatermark = Optional.of(providedHighWaterMark);\n      // Conflict resolution is disabled when providedRowIdHighWatermark is set,\n      // because it must be updated according to the latest table state.\n      maxRetries = 0;\n    }\n  }\n\n  //////////////////////////////////\n  // Commit Execution (Main Flow) //\n  //////////////////////////////////\n\n  /** Returns (commitDeltaData, inCommitTimestamp). */\n  private Tuple2<ParsedDeltaData, Optional<Long>> commitWithRetry(\n      Engine engine, CloseableIterable<Row> dataActions, TransactionMetrics transactionMetrics) {\n    try {\n      long commitAsVersion = getReadTableVersion() + 1;\n      // Generate the commit action with the inCommitTimestamp if ICT is enabled.\n      CommitInfo attemptCommitInfo = generateCommitAction(engine);\n      updateMetadataWithICTIfRequired(\n          engine, attemptCommitInfo.getInCommitTimestamp(), getReadTableVersion());\n      List<DomainMetadata> resolvedDomainMetadatas =\n          domainMetadataState.getComputedDomainMetadatasToCommit();\n\n      // If row tracking is supported, assign base row IDs and default row commit versions to any\n      // AddFile actions that do not yet have them. If the row ID high watermark changes, emit a\n      // DomainMetadata action to update it.\n      if (TableFeatures.isRowTrackingSupported(protocol)) {\n        List<DomainMetadata> updatedDomainMetadata =\n            RowTracking.updateRowIdHighWatermarkIfNeeded(\n                readSnapshotOpt,\n                protocol,\n                Optional.empty() /* winningTxnRowIdHighWatermark */,\n                dataActions,\n                resolvedDomainMetadatas,\n                providedRowIdHighWatermark);\n        domainMetadataState.setComputedDomainMetadatas(updatedDomainMetadata);\n        dataActions =\n            RowTracking.assignBaseRowIdAndDefaultRowCommitVersion(\n                readSnapshotOpt,\n                protocol,\n                Optional.empty() /* winningTxnRowIdHighWatermark */,\n                Optional.empty() /* prevCommitVersion */,\n                commitAsVersion,\n                dataActions);\n      }\n\n      int attempt = 1;\n      boolean seenRetryableNonConflictException = false;\n      while (true) {\n        // This loop exits upon either (a) commit success (return statement) or (b) commit failure.\n        logger.info(\n            \"Attempting to commit transaction at table version {}. Attempt {}/{}\",\n            commitAsVersion,\n            attempt,\n            getMaxCommitAttempts());\n        try {\n          transactionMetrics.commitAttemptsCounter.increment();\n          return doCommit(\n              engine, commitAsVersion, attemptCommitInfo, dataActions, transactionMetrics);\n        } catch (CommitFailedException cfe) {\n          if (!cfe.isRetryable()) {\n            // Case 1: Non-retryable exception. We must throw this. We don't expect connectors to\n            //         be able to recover from this.\n            throw DeltaErrors.nonRetryableCommitException(attempt, commitAsVersion, cfe);\n          }\n          if (attempt >= getMaxCommitAttempts()) {\n            // Case 2: Despite the error being retryable, we have exhausted the maximum number of\n            //         retries. We must throw here, too.\n            throw new MaxCommitRetryLimitReachedException(commitAsVersion, maxRetries, cfe);\n          }\n\n          // We know the commit is retryable.\n\n          if (!cfe.isConflict()) {\n            // Case 3: No conflict => No conflict resolution needed. Just retry with same version.\n            printLogForRetryableNonConflictException(attempt, commitAsVersion, cfe);\n            seenRetryableNonConflictException = true;\n          } else if (seenRetryableNonConflictException) {\n            checkState(cfe.isRetryable() && cfe.isConflict(), \"expect retryable and conflict\");\n\n            // Case 4: There is a conflict, and we have previously seen a retryable exception\n            //         without conflict and then retried. This means that something like the\n            //         following has happened:\n            // - Commit Attempt #1: IOException => CFE(retryable=true, conflict=false). We set\n            //         seenRetryableNonConflictException to true. Here, there's two possible cases:\n            //         (A) N.json was written successfully (and we just never learned about it),\n            //         or (B) N.json was not written.\n            // - Commit Attempt #2: FileAlreadyExistsException => CFE(retryable=true, conflict=true)\n            //         Should we retry this commit? If it's case (A), then we should not, as we are\n            //         just conflicting with our previous commit attempt. If it's case (B), then we\n            //         should retry, since we are conflicting with some *other* writer's commit. In\n            //         the future we can add detection capabilities between these two cases (e.g.\n            //         check if the CommitInfo action is present and has a txnId, else compare the\n            //         other contents of the delta files).\n            throw new CommitStateUnknownException(commitAsVersion, attempt, cfe);\n          } else {\n            checkState(cfe.isRetryable() && cfe.isConflict(), \"expect retryable and conflict\");\n            // Case 5: There is a conflict, and we have not previously seen a retryable and\n            //         non-conflict exception. We will resolve the conflict and retry.\n            printLogForRetryableWithConflictException(attempt, commitAsVersion, cfe);\n\n            TransactionRebaseState rebaseState =\n                resolveConflicts(engine, commitAsVersion, attemptCommitInfo, attempt, dataActions);\n            commitAsVersion = rebaseState.getLatestVersion() + 1;\n            dataActions = rebaseState.getUpdatedDataActions();\n            domainMetadataState.setComputedDomainMetadatas(rebaseState.getUpdatedDomainMetadatas());\n            currentCrcInfo = rebaseState.getUpdatedCrcInfo();\n          }\n        }\n        // We will be retrying the commit (either from case 3 or 5 above).\n        //\n        // Action counters may be partially incremented from previous tries, reset the counters\n        // to 0 and drop fileSizeHistogram\n        // TODO: [delta-io/delta#5047] reconcile fileSizeHistogram\n        transactionMetrics.resetActionMetricsForRetry();\n        attempt++;\n      }\n    } finally {\n      closed = true;\n    }\n  }\n\n  /** Returns (commitDeltaData, inCommitTimestamp). */\n  private Tuple2<ParsedDeltaData, Optional<Long>> doCommit(\n      Engine engine,\n      long commitAsVersion,\n      CommitInfo attemptCommitInfo,\n      CloseableIterable<Row> dataActions,\n      TransactionMetrics transactionMetrics)\n      throws CommitFailedException {\n    List<Row> metadataActions = new ArrayList<>();\n    metadataActions.add(createCommitInfoSingleAction(attemptCommitInfo.toRow()));\n    if (shouldUpdateMetadata) {\n      metadataActions.add(createMetadataSingleAction(metadata.toRow()));\n    }\n    if (shouldUpdateProtocol) {\n      // In the future, we need to add metadata and action when there are any changes to them.\n      metadataActions.add(createProtocolSingleAction(protocol.toRow()));\n    }\n    setTxnOpt.ifPresent(setTxn -> metadataActions.add(createTxnSingleAction(setTxn.toRow())));\n\n    List<DomainMetadata> resolvedDomainMetadatas =\n        domainMetadataState.getComputedDomainMetadatasToCommit();\n\n    // Check for duplicate domain metadata and if the protocol supports\n    DomainMetadataUtils.validateDomainMetadatas(resolvedDomainMetadatas, protocol);\n\n    resolvedDomainMetadatas.forEach(\n        dm -> metadataActions.add(createDomainMetadataSingleAction(dm.toRow())));\n\n    try (CloseableIterator<Row> userStageDataIter = dataActions.iterator()) {\n      final CloseableIterator<Row> completeFileActionIter;\n      if (isReplaceTable()) {\n        // If this is a replace table operation we need to internally generate the remove file\n        // actions to reset the table state\n        completeFileActionIter = getRemoveActionsForReplace(engine).combine(userStageDataIter);\n      } else {\n        completeFileActionIter = userStageDataIter;\n      }\n\n      boolean isAppendOnlyTable = APPEND_ONLY_ENABLED.fromMetadata(metadata);\n\n      // Create a new CloseableIterator that will return the metadata actions followed by the\n      // data actions.\n      CloseableIterator<Row> dataAndMetadataActions =\n          toCloseableIterator(metadataActions.iterator())\n              .combine(completeFileActionIter)\n              .map(\n                  action -> {\n                    incrementMetricsForFileActionRow(transactionMetrics, action);\n                    if (!action.isNullAt(REMOVE_FILE_ORDINAL)) {\n                      RemoveFile removeFile = new RemoveFile(action.getStruct(REMOVE_FILE_ORDINAL));\n                      if (isAppendOnlyTable && removeFile.getDataChange()) {\n                        throw DeltaErrors.cannotModifyAppendOnlyTable(dataPath.toString());\n                      }\n                    }\n                    return action;\n                  });\n\n      final CommitMetadata commitMetadata =\n          new CommitMetadata(\n              commitAsVersion,\n              logPath.toString(),\n              attemptCommitInfo,\n              resolvedDomainMetadatas,\n              committerProperties,\n              readSnapshotOpt.map(x -> new Tuple2<>(x.getProtocol(), x.getMetadata())),\n              shouldUpdateProtocol ? Optional.of(protocol) : Optional.empty(),\n              shouldUpdateMetadata ? Optional.of(metadata) : Optional.empty(),\n              readSnapshotOpt\n                  .map(x -> x.getLogSegment().getMaxPublishedDeltaVersion())\n                  .orElse(Optional.of(-1L)));\n\n      DirectoryCreationUtils.createAllDeltaDirectoriesAsNeeded(\n          engine, logPath, commitAsVersion, commitMetadata.getReadProtocolOpt(), protocol);\n\n      return new Tuple2<>(\n          committer.commit(engine, dataAndMetadataActions, commitMetadata).getCommitLogData(),\n          attemptCommitInfo.getInCommitTimestamp());\n    } catch (IOException ioe) {\n      // Error closing the CloseableIterator of actions or error creating the delta log directory\n      throw new UncheckedIOException(ioe);\n    }\n  }\n\n  ////////////////////////////////\n  // Commit Execution (Helpers) //\n  ////////////////////////////////\n\n  private CommitInfo generateCommitAction(Engine engine) {\n    long commitAttemptStartTime = clock.getTimeMillis();\n    return new CommitInfo(\n        generateInCommitTimestampForFirstCommitAttempt(engine, commitAttemptStartTime),\n        commitAttemptStartTime, /* timestamp */\n        Optional.of(\"Kernel-\" + Meta.KERNEL_VERSION + \"/\" + engineInfo), /* engineInfo */\n        Optional.of(operation.getDescription()), /* description */\n        getOperationParameters(), /* operationParameters */\n        isBlindAppend(), /* isBlindAppend */\n        Optional.of(txnId.toString()), /* txnId */\n        emptyMap() /* operationMetrics */);\n  }\n\n  /**\n   * Generates a timestamp which is greater than the commit timestamp of the readSnapshotOpt. This\n   * can result in an additional file read and that this will only happen if ICT is enabled.\n   */\n  private Optional<Long> generateInCommitTimestampForFirstCommitAttempt(\n      Engine engine, long currentTimestamp) {\n    if (IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(metadata)) {\n      if (readSnapshotOpt.isPresent()) {\n        long lastCommitTimestamp = readSnapshotOpt.get().getTimestamp(engine);\n        return Optional.of(Math.max(currentTimestamp, lastCommitTimestamp + 1));\n      } else { // For a new table this is just the current timestamp\n        return Optional.of(currentTimestamp);\n      }\n    } else {\n      return Optional.empty();\n    }\n  }\n\n  private Map<String, String> getOperationParameters() {\n    if (isCreateOrReplace) {\n      List<String> partitionCols = VectorUtils.toJavaList(metadata.getPartitionColumns());\n      String partitionBy =\n          partitionCols.stream()\n              .map(col -> \"\\\"\" + col + \"\\\"\")\n              .collect(Collectors.joining(\",\", \"[\", \"]\"));\n      return Collections.singletonMap(\"partitionBy\", partitionBy);\n    }\n    return emptyMap();\n  }\n\n  private void updateMetadataWithICTIfRequired(\n      Engine engine, Optional<Long> inCommitTimestampOpt, long lastCommitVersion) {\n    // If ICT is enabled for the current transaction, update the metadata with the ICT\n    // enablement info.\n    inCommitTimestampOpt.ifPresent(\n        inCommitTimestamp -> {\n          Optional<Metadata> metadataWithICTInfo =\n              InCommitTimestampUtils.getUpdatedMetadataWithICTEnablementInfo(\n                  engine, inCommitTimestamp, readSnapshotOpt, metadata, lastCommitVersion + 1L);\n          metadataWithICTInfo.ifPresent(this::updateMetadata);\n        });\n  }\n\n  private void incrementMetricsForFileActionRow(TransactionMetrics txnMetrics, Row fileActionRow) {\n    txnMetrics.totalActionsCounter.increment();\n    if (!fileActionRow.isNullAt(ADD_FILE_ORDINAL)) {\n      txnMetrics.updateForAddFile(new AddFile(fileActionRow.getStruct(ADD_FILE_ORDINAL)).getSize());\n    } else if (!fileActionRow.isNullAt(REMOVE_FILE_ORDINAL)) {\n      RemoveFile removeFile = new RemoveFile(fileActionRow.getStruct(REMOVE_FILE_ORDINAL));\n      long removeFileSize =\n          removeFile.getSize().orElseThrow(DeltaErrorsInternal::missingRemoveFileSizeDuringCommit);\n      txnMetrics.updateForRemoveFile(removeFileSize);\n    }\n  }\n\n  /**\n   * Returns the remove file rows needed to remove every active add file in the table. These rows\n   * are already formatted as {@link SingleAction} rows and are ready to be committed.\n   */\n  private CloseableIterator<Row> getRemoveActionsForReplace(Engine engine) {\n    checkArgument(\n        readSnapshotOpt.isPresent(), \"Cannot generate removes for a snapshot with version < 0\");\n    Scan scan = readSnapshotOpt.get().getScanBuilder().build();\n    return Utils.intoRows(scan.getScanFiles(engine))\n        .map(\n            scanRow -> {\n              AddFile add = new AddFile(scanRow.getStruct(InternalScanFileUtils.ADD_FILE_ORDINAL));\n              return SingleAction.createRemoveFileSingleAction(\n                  add.toRemoveFileRow(true /* dataChange */, Optional.empty()));\n            });\n  }\n\n  /////////////////////////\n  // Conflict Resolution //\n  /////////////////////////\n\n  private TransactionRebaseState resolveConflicts(\n      Engine engine,\n      long commitAsVersion,\n      CommitInfo attemptCommitInfo,\n      int attempt,\n      CloseableIterable<Row> dataActions) {\n    logger.info(\n        \"[{}] Trying to resolve conflicts and retry commit. Attempt {}/{}.\",\n        dataPath,\n        attempt,\n        getMaxCommitAttempts());\n    TransactionRebaseState rebaseState =\n        ConflictChecker.resolveConflicts(\n            engine,\n            readSnapshotOpt,\n            commitAsVersion,\n            this,\n            domainMetadataState.getComputedDomainMetadatasToCommit(),\n            dataActions);\n    long newCommitAsVersion = rebaseState.getLatestVersion() + 1;\n    checkArgument(\n        commitAsVersion < newCommitAsVersion,\n        \"New commit version %d should be greater than the previous commit attempt version %d.\",\n        newCommitAsVersion,\n        commitAsVersion);\n    Optional<Long> updatedInCommitTimestamp =\n        getUpdatedInCommitTimestampAfterConflict(\n            rebaseState.getLatestCommitTimestamp(), attemptCommitInfo.getInCommitTimestamp());\n    updateMetadataWithICTIfRequired(\n        engine, updatedInCommitTimestamp, rebaseState.getLatestVersion());\n    attemptCommitInfo.setInCommitTimestamp(updatedInCommitTimestamp);\n    return rebaseState;\n  }\n\n  private Optional<Long> getUpdatedInCommitTimestampAfterConflict(\n      long winningCommitTimestamp, Optional<Long> attemptInCommitTimestamp) {\n    if (attemptInCommitTimestamp.isPresent()) {\n      long updatedInCommitTimestamp =\n          Math.max(attemptInCommitTimestamp.get(), winningCommitTimestamp + 1);\n      return Optional.of(updatedInCommitTimestamp);\n    }\n    return attemptInCommitTimestamp;\n  }\n\n  ////////////////////////////\n  // Post-Commit Processing //\n  ////////////////////////////\n\n  private TransactionCommitResult buildTransactionCommitResult(\n      Engine engine,\n      ParsedDeltaData committedDelta,\n      TransactionMetrics txnMetrics,\n      Optional<Long> committedIctOpt) {\n    final long committedVersion = committedDelta.getVersion();\n\n    final TransactionReport transactionReport =\n        recordTransactionReport(\n            engine,\n            Optional.of(committedVersion),\n            getEffectiveClusteringColumns(),\n            txnMetrics,\n            Optional.empty() /* exception */);\n\n    final TransactionMetricsResult txnMetricsCaptured =\n        txnMetrics.captureTransactionMetricsResult();\n\n    final Optional<CRCInfo> postCommitCrcOpt =\n        buildPostCommitCrcInfoIfCurrentCrcAvailable(committedVersion, txnMetricsCaptured);\n\n    final Optional<SnapshotImpl> postCommitSnapshotOpt =\n        buildPostCommitSnapshotOpt(engine, committedDelta, committedIctOpt, postCommitCrcOpt);\n\n    return new TransactionCommitResult(\n        committedVersion,\n        generatePostCommitHooks(committedVersion, postCommitCrcOpt),\n        transactionReport,\n        postCommitSnapshotOpt);\n  }\n\n  private Optional<SnapshotImpl> buildPostCommitSnapshotOpt(\n      Engine engine,\n      ParsedDeltaData committedDelta,\n      Optional<Long> committedIctOpt,\n      Optional<CRCInfo> postCommitCrcOpt) {\n    // TODO: Support building post-commit Snapshots after conflicts. If there was a conflict, then\n    //       we'd need to keep track of each of the conflicting commit files in order to build the\n    //       new LogSegment for our post-commit Snapshot. This is currently not done, today. Note\n    //       that for catalogManaged tables, we would need the Committer to provide the conflicting\n    //       commits as part of the CommitFailedException.\n    if (committedDelta.getVersion() != getReadTableVersion() + 1) {\n      return Optional.empty();\n    }\n\n    final LogSegment postCommitLogSegment = buildPostCommitLogSegment(committedDelta);\n    final Lazy<LogSegment> lazyLogSegment = new Lazy<>(() -> postCommitLogSegment);\n    final Lazy<Optional<CRCInfo>> lazyCrcInfo = new Lazy<>(() -> postCommitCrcOpt);\n    final LogReplay logReplay = new LogReplay(engine, dataPath, lazyLogSegment, lazyCrcInfo);\n    // TODO: SnapshotQueryContext.forPostCommitSnapshot\n    final SnapshotQueryContext snapshotContext =\n        SnapshotQueryContext.forVersionSnapshot(dataPath.toString(), committedDelta.getVersion());\n    final SnapshotImpl postCommitSnapshot =\n        new SnapshotImpl(\n            dataPath,\n            committedDelta.getVersion(),\n            lazyLogSegment,\n            logReplay,\n            protocol,\n            metadata,\n            committer,\n            snapshotContext,\n            committedIctOpt);\n\n    return Optional.of(postCommitSnapshot);\n  }\n\n  private LogSegment buildPostCommitLogSegment(ParsedDeltaData committedDelta) {\n    if (readSnapshotOpt.isPresent()) {\n      return readSnapshotOpt\n          .get()\n          .getLogSegment()\n          .newWithAddedDeltas(Collections.singletonList(committedDelta));\n    }\n\n    return LogSegment.createForNewTable(logPath, committedDelta);\n  }\n\n  private List<PostCommitHook> generatePostCommitHooks(\n      long committedVersion, Optional<CRCInfo> postCommitCrcOpt) {\n    final List<PostCommitHook> postCommitHooks = new ArrayList<>();\n\n    if (isReadyForCheckpoint(committedVersion)) {\n      postCommitHooks.add(new CheckpointHook(dataPath, committedVersion));\n    }\n\n    if (postCommitCrcOpt.isPresent()) {\n      postCommitHooks.add(new ChecksumSimpleHook(postCommitCrcOpt.get(), logPath));\n    } else {\n      postCommitHooks.add(new ChecksumFullHook(dataPath, committedVersion));\n    }\n\n    if (logCompactionInterval > 0\n        && LogCompactionWriter.shouldCompact(committedVersion, logCompactionInterval)) {\n      // add one here because commits start a 0\n      long startVersion = committedVersion + 1 - logCompactionInterval;\n      long minFileRetentionTimestampMillis =\n          clock.getTimeMillis() - TOMBSTONE_RETENTION.fromMetadata(metadata);\n      postCommitHooks.add(\n          new LogCompactionHook(\n              dataPath, logPath, startVersion, committedVersion, minFileRetentionTimestampMillis));\n    }\n\n    return postCommitHooks;\n  }\n\n  private boolean isReadyForCheckpoint(long newVersion) {\n    int checkpointInterval = CHECKPOINT_INTERVAL.fromMetadata(metadata);\n    return newVersion > 0 && newVersion % checkpointInterval == 0;\n  }\n\n  private Optional<CRCInfo> buildPostCommitCrcInfoIfCurrentCrcAvailable(\n      long commitAtVersion, TransactionMetricsResult metricsResult) {\n    if (isCreateOrReplace) {\n      // We don't need to worry about conflicting transaction here since new tables always commit\n      // metadata (and thus fail any conflicts)\n      return Optional.of(\n          new CRCInfo(\n              commitAtVersion,\n              metadata,\n              protocol,\n              metricsResult.getTotalAddFilesSizeInBytes(),\n              metricsResult.getNumAddFiles(),\n              Optional.of(txnId.toString()),\n              domainMetadataState.getPostCommitDomainMetadatas(),\n              metricsResult\n                  .getTableFileSizeHistogram()\n                  .map(FileSizeHistogram::fromFileSizeHistogramResult)));\n    }\n\n    return currentCrcInfo\n        // Ensure current currentCrcInfo is exactly commitAtVersion - 1\n        .filter(crcInfo -> commitAtVersion == crcInfo.getVersion() + 1)\n        .map(\n            lastCrcInfo ->\n                new CRCInfo(\n                    commitAtVersion,\n                    metadata,\n                    protocol,\n                    lastCrcInfo.getTableSizeBytes()\n                        + metricsResult.getTotalAddFilesSizeInBytes()\n                        - metricsResult.getTotalRemoveFilesSizeInBytes(),\n                    lastCrcInfo.getNumFiles()\n                        + metricsResult.getNumAddFiles()\n                        - metricsResult.getNumRemoveFiles(),\n                    Optional.of(txnId.toString()),\n                    domainMetadataState.getPostCommitDomainMetadatas(),\n                    metricsResult\n                        .getTableFileSizeHistogram()\n                        .map(FileSizeHistogram::fromFileSizeHistogramResult)));\n  }\n\n  private TransactionReport recordTransactionReport(\n      Engine engine,\n      Optional<Long> committedVersion,\n      Optional<List<Column>> clusteringColumnsOpt,\n      TransactionMetrics transactionMetrics,\n      Optional<Exception> exception) {\n    TransactionReport transactionReport =\n        new TransactionReportImpl(\n            dataPath.toString() /* tablePath */,\n            operation.toString(),\n            engineInfo,\n            committedVersion,\n            clusteringColumnsOpt,\n            transactionMetrics,\n            readSnapshotOpt.map(SnapshotImpl::getSnapshotReport),\n            exception);\n    engine.getMetricsReporters().forEach(reporter -> reporter.report(transactionReport));\n\n    return transactionReport;\n  }\n\n  /////////////////////\n  // Logging Helpers //\n  /////////////////////\n\n  private void printLogForRetryableNonConflictException(\n      int attempt, long commitAsVersion, CommitFailedException cfe) {\n    logger.warn(\n        \"Commit attempt {} for table version {} failed with a retryable exception and without \"\n            + \"conflict. Skipping conflict resolution and trying again. Exception: {}\",\n        attempt,\n        commitAsVersion,\n        cfe);\n  }\n\n  private void printLogForRetryableWithConflictException(\n      int attempt, long commitAsVersion, CommitFailedException cfe) {\n    logger.warn(\n        \"Commit attempt {} for version {} failed with a retryable exception due to a physical \"\n            + \"conflict. Performing conflict resolution and trying again. Exception: {}\",\n        attempt,\n        commitAsVersion,\n        cfe);\n  }\n\n  ////////////////////\n  // Helper Classes //\n  ////////////////////\n\n  /** Encapsulates the state of domain metadata within a transaction. */\n  private class DomainMetadataState {\n    private final Map<String, DomainMetadata> domainsToAdd = new HashMap<>();\n    private final Set<String> domainsToRemove = new HashSet<>();\n    private Optional<List<DomainMetadata>> computedMetadatas = Optional.empty();\n\n    /** Adds a domain metadata. Invalidates any cached computed state. */\n    public void addDomain(String domain, String config) {\n      checkArgument(\n          !domainsToRemove.contains(domain),\n          \"Cannot add a domain that is removed in this transaction\");\n      checkState(!closed, \"Cannot add a domain metadata after the transaction has completed\");\n\n      // Add the domain and invalidate cache\n      domainsToAdd.put(domain, new DomainMetadata(domain, config, false /* removed */));\n      computedMetadatas = Optional.empty();\n    }\n\n    /** Marks a domain for removal. Invalidates any cached computed state. */\n    public void removeDomain(String domain) {\n      checkArgument(\n          !domainsToAdd.containsKey(domain),\n          \"Cannot remove a domain that is added in this transaction\");\n      checkState(!closed, \"Cannot remove a domain after the transaction has completed\");\n\n      // Mark for removal and invalidate cache\n      domainsToRemove.add(domain);\n      computedMetadatas = Optional.empty();\n    }\n\n    /**\n     * Returns a list of the domain metadatas to commit. This consists of the domain metadatas added\n     * in the transaction using {@link Transaction#addDomainMetadata(String, String)} and the\n     * tombstones for the domain metadatas removed in the transaction using {@link\n     * Transaction#removeDomainMetadata(String)}.\n     *\n     * @return A list of {@link DomainMetadata} containing domain metadata to be committed in this\n     *     transaction.\n     */\n    public List<DomainMetadata> getComputedDomainMetadatasToCommit() {\n      if (computedMetadatas.isPresent()) {\n        return computedMetadatas.get();\n      }\n\n      generateClusteringDomainMetadataIfNeeded();\n      if (isReplaceTable()) {\n        // In the case of replace table we need to completely reset the table state by removing\n        // any existing domain metadata\n        readSnapshotOpt\n            .get() // if replaceTable we know snapshot is present\n            .getActiveDomainMetadataMap()\n            .forEach(\n                (domainName, domainMetadata) -> {\n                  if (!domainsToAdd.containsKey(domainName)) {\n                    // We only need to remove the domain if it is not added (& thus overwritten)\n                    // in this current transaction. We cannot add and remove the same domain in\n                    // one transaction.\n                    removeDomain(domainName);\n                  }\n                });\n      }\n      // Add all domains added in the transaction\n      List<DomainMetadata> result = new ArrayList<>(domainsToAdd.values());\n\n      if (domainsToRemove.isEmpty()) {\n        // If no domain metadatas are removed we don't need to load the existing domain metadatas\n        // from the snapshot (which is an expensive operation)\n        computedMetadatas = Optional.of(result);\n        return result;\n      }\n\n      // Generate the tombstones for removed domains\n      Map<String, DomainMetadata> snapshotDomainMetadataMap =\n          readSnapshotOpt.map(SnapshotImpl::getActiveDomainMetadataMap).orElse(emptyMap());\n      for (String domainName : domainsToRemove) {\n        if (snapshotDomainMetadataMap.containsKey(domainName)) {\n          // Note: we know domainName is not already in finalDomainMetadatas because we do not allow\n          // removing and adding a domain with the same identifier in a single txn!\n          DomainMetadata domainToRemove = snapshotDomainMetadataMap.get(domainName);\n          checkState(\n              !domainToRemove.isRemoved(),\n              \"snapshotDomainMetadataMap should only contain active domain metadata\");\n          result.add(domainToRemove.removed());\n        } else {\n          // We must throw an error if the domain does not exist. Otherwise, there could be\n          // unexpected\n          // behavior within conflict resolution. For example, consider the following\n          // 1. Table has no domains set in V0\n          // 2. txnA is started and wants to remove domain \"foo\"\n          // 3. txnB is started and adds domain \"foo\" and commits V1 before txnA\n          // 4. txnA needs to perform conflict resolution against the V1 commit from txnB\n          // Conflict resolution should fail but since the domain does not exist we cannot create\n          // a tombstone to mark it as removed and correctly perform conflict resolution.\n          throw new DomainDoesNotExistException(\n              dataPath.toString(), domainName, getReadTableVersion());\n        }\n      }\n\n      computedMetadatas = Optional.of(result);\n      return result;\n    }\n\n    /** Sets the computed domain metadata list directly. Used during conflict resolution. */\n    public void setComputedDomainMetadatas(List<DomainMetadata> updatedDomainMetadatas) {\n      computedMetadatas = Optional.of(updatedDomainMetadatas);\n    }\n\n    /**\n     * Returns the set of active domain metadata of the table, removed domain metadata are excluded.\n     */\n    public Optional<Set<DomainMetadata>> getPostCommitDomainMetadatas() {\n      if (!readSnapshotOpt.isPresent()) {\n        return Optional.of(\n            getComputedDomainMetadatasToCommit().stream()\n                .filter(dm -> !dm.isRemoved())\n                .collect(Collectors.toSet()));\n      }\n      return currentCrcInfo\n          .flatMap(CRCInfo::getDomainMetadata)\n          .map(\n              oldDomainMetadata -> {\n                Map<String, DomainMetadata> domainMetadataMap =\n                    oldDomainMetadata.stream()\n                        .collect(Collectors.toMap(DomainMetadata::getDomain, Function.identity()));\n                getComputedDomainMetadatasToCommit()\n                    .forEach(\n                        domainMetadata -> {\n                          if (domainMetadata.isRemoved()) {\n                            domainMetadataMap.remove(domainMetadata.getDomain());\n                          } else {\n                            domainMetadataMap.put(domainMetadata.getDomain(), domainMetadata);\n                          }\n                        });\n                return new HashSet<>(domainMetadataMap.values());\n              });\n    }\n\n    /**\n     * Generate the domain metadata for the clustering columns if they are present in the\n     * transaction.\n     */\n    private void generateClusteringDomainMetadataIfNeeded() {\n      if (TableFeatures.isClusteringTableFeatureSupported(protocol)\n          && newClusteringColumnsOpt.isPresent()) {\n        DomainMetadata clusteringDomainMetadata =\n            ClusteringUtils.getClusteringDomainMetadata(newClusteringColumnsOpt.get());\n        addDomain(\n            clusteringDomainMetadata.getDomain(), clusteringDomainMetadata.getConfiguration());\n      } else if (TableFeatures.isClusteringTableFeatureSupported(protocol)\n          && isReplaceTable()\n          && !newClusteringColumnsOpt.isPresent()) {\n        // When clustering is in the writer features we require there to be a clustering domain\n        // metadata present; when the table is no longer a clustered table this means we must have\n        // a domain metadata with clusteringColumns=[]\n        DomainMetadata emptyClusteringDomainMetadata =\n            ClusteringUtils.getClusteringDomainMetadata(Collections.emptyList());\n        addDomain(\n            emptyClusteringDomainMetadata.getDomain(),\n            emptyClusteringDomainMetadata.getConfiguration());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/TransactionMetadataFactory.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.ReplaceTableTransactionBuilderV2Impl.TABLE_PROPERTY_KEYS_TO_PRESERVE;\nimport static io.delta.kernel.internal.TransactionImpl.DEFAULT_READ_VERSION;\nimport static io.delta.kernel.internal.TransactionImpl.DEFAULT_WRITE_VERSION;\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.ALLOW_COLUMN_DEFAULTS_W_FEATURE;\nimport static io.delta.kernel.internal.util.ColumnMapping.isColumnMappingModeEnabled;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\nimport static io.delta.kernel.internal.util.SchemaUtils.casePreservingPartitionColNames;\nimport static io.delta.kernel.internal.util.VectorUtils.buildArrayValue;\nimport static io.delta.kernel.internal.util.VectorUtils.stringStringMapValue;\nimport static java.util.Collections.*;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.commit.CatalogCommitter;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.icebergcompat.*;\nimport io.delta.kernel.internal.rowtracking.MaterializedRowTrackingColumn;\nimport io.delta.kernel.internal.rowtracking.RowTracking;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode;\nimport io.delta.kernel.internal.util.SchemaUtils;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Class for building the Protocol, Metadata, and ResolvedClusteringColumns to commit in a\n * transaction. This means it validates and updates the protocol and metadata according to the\n * inputs.\n */\npublic class TransactionMetadataFactory {\n  private static final Logger logger = LoggerFactory.getLogger(TransactionMetadataFactory.class);\n\n  /**\n   * Expectations with respect to the given operation:\n   *\n   * <ul>\n   *   <li>Create table: both protocol and metadata will be present\n   *   <li>Replace table: both protocol and metadata will be present\n   *   <li>Update table: metadata and protocol may or may not be present depending on whether there\n   *       should be a metadata or protocol update\n   * </ul>\n   */\n  public static class Output {\n    /* New metadata, present if the transaction should commit a new metadata action */\n    public final Optional<Protocol> newProtocol;\n    /* New protocol, present if the transaction should commit a new protocol action */\n    public final Optional<Metadata> newMetadata;\n    /* Resolved _new_ clustering columns if the transaction should update the clustering columns */\n    public final Optional<List<Column>> physicalNewClusteringColumns;\n\n    public Output(\n        Optional<Protocol> newProtocol,\n        Optional<Metadata> newMetadata,\n        Optional<List<Column>> physicalNewClusteringColumns) {\n      this.newProtocol = newProtocol;\n      this.newMetadata = newMetadata;\n      this.physicalNewClusteringColumns = physicalNewClusteringColumns;\n    }\n  }\n\n  ////////////////////////////\n  // Static factory methods //\n  ////////////////////////////\n\n  static Output buildCreateTableMetadata(\n      String tablePath,\n      StructType schema,\n      Map<String, String> userInputTableProperties,\n      Optional<List<String>> partitionColumns,\n      Optional<List<Column>> clusteringColumns,\n      Optional<Committer> committerOpt) {\n    checkArgument(\n        !partitionColumns.isPresent() || !clusteringColumns.isPresent(),\n        \"Cannot provide both partition columns and clustering columns\");\n    validateSchemaAndPartColsCreateOrReplace(\n        userInputTableProperties, schema, partitionColumns.orElse(emptyList()));\n\n    final Map<String, String> requiredCatalogTableProperties =\n        committerOpt\n            .map(TransactionMetadataFactory::getRequiredCatalogTablePropertiesIfApplicable)\n            .orElse(Collections.emptyMap());\n\n    // We put the required catalog table properties *first* so that we persist the intent, if any,\n    // of the user explicitly setting a required catalog table property. If it's set to an invalid\n    // value, we will detect this and fail later inside TransactionMetadataFactory.\n    final Map<String, String> allCreateTableProperties = new HashMap<>();\n    allCreateTableProperties.putAll(requiredCatalogTableProperties);\n    allCreateTableProperties.putAll(userInputTableProperties);\n\n    Output output =\n        new TransactionMetadataFactory(\n                tablePath,\n                Optional.empty() /* readSnapshot */,\n                Optional.of(\n                    getInitialMetadata(\n                        schema, allCreateTableProperties, partitionColumns.orElse(emptyList()))),\n                Optional.of(getInitialProtocol()),\n                userInputTableProperties /* originalUserInputProperties */,\n                true /* isCreateOrReplace */,\n                clusteringColumns,\n                false /* isSchemaEvolution */,\n                committerOpt)\n            .finalOutput;\n    checkState(\n        output.newMetadata.isPresent() && output.newProtocol.isPresent(),\n        \"Expected non-null metadata and protocol for create table\");\n    return output;\n  }\n\n  static Output buildReplaceTableMetadata(\n      String tablePath,\n      SnapshotImpl readSnapshot,\n      StructType schema,\n      Map<String, String> userInputTableProperties,\n      Optional<List<String>> partitionColumns,\n      Optional<List<Column>> clusteringColumns) {\n    checkArgument(\n        !partitionColumns.isPresent() || !clusteringColumns.isPresent(),\n        \"Cannot provide both partition columns and clustering columns\");\n    validateSchemaAndPartColsCreateOrReplace(\n        userInputTableProperties, schema, partitionColumns.orElse(emptyList()));\n    validateNotEnablingCatalogManagedOnReplace(userInputTableProperties);\n\n    final Map<String, String> requiredCatalogTableProperties =\n        getRequiredCatalogTablePropertiesIfApplicable(readSnapshot.getCommitter());\n\n    final Map<String, String> allReplaceTableProperties = new HashMap<>();\n\n    // Step 1: We put the required catalog table properties *first* so that we persist the intent,\n    // if any, of the user explicitly setting a required catalog table property. If it's set to an\n    // invalid value, we will detect this and fail later inside TransactionMetadataFactory.\n    allReplaceTableProperties.putAll(requiredCatalogTableProperties);\n\n    // Step 2: Preserve a few important delta properties\n    allReplaceTableProperties.putAll(\n        readSnapshot.getMetadata().getConfiguration().entrySet().stream()\n            .filter(e -> TABLE_PROPERTY_KEYS_TO_PRESERVE.contains(e.getKey()))\n            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));\n\n    // Step 3: Insert the new user-provided table properties\n    allReplaceTableProperties.putAll(userInputTableProperties);\n\n    Output output =\n        new TransactionMetadataFactory(\n                tablePath,\n                Optional.of(readSnapshot),\n                Optional.of(\n                    getInitialMetadata(\n                        schema, allReplaceTableProperties, partitionColumns.orElse(emptyList()))),\n                Optional.of(readSnapshot.getProtocol()),\n                userInputTableProperties,\n                true /* isCreateOrReplace */,\n                clusteringColumns,\n                false /* isSchemaEvolution */,\n                Optional.of(readSnapshot.getCommitter()))\n            .finalOutput;\n    // TODO: reconsider whether we should always commit a new Protocol action regardless of whether\n    //   there is a protocol upgrade\n    checkState(\n        output.newMetadata.isPresent() && output.newProtocol.isPresent(),\n        \"Expected non-null metadata and protocol for replace table\");\n    if (output\n        .newProtocol\n        .orElse(readSnapshot.getProtocol())\n        .supportsFeature(TableFeatures.ICEBERG_COMPAT_V3_W_FEATURE)) {\n      // Block this for now to be safe, we will return to this in the future\n      // once replace for rowTracking is enabled\n      throw new UnsupportedOperationException(\n          \"REPLACE TABLE is not yet supported on IcebergCompatV3 tables\");\n    }\n    return output;\n  }\n\n  static Output buildUpdateTableMetadata(\n      String tablePath,\n      SnapshotImpl readSnapshot,\n      Optional<Map<String, String>> propertiesAdded,\n      Optional<Set<String>> propertyKeysRemoved,\n      Optional<StructType> newSchema,\n      Optional<List<Column>> clusteringColumns) {\n    if (propertiesAdded.isPresent() && propertyKeysRemoved.isPresent()) {\n      Set<String> overlappingPropertyKeys =\n          propertyKeysRemoved.get().stream()\n              .filter(key -> propertiesAdded.get().containsKey(key))\n              .collect(toSet());\n      if (!overlappingPropertyKeys.isEmpty()) {\n        throw DeltaErrors.overlappingTablePropertiesSetAndUnset(overlappingPropertyKeys);\n      }\n    }\n\n    Optional<Metadata> newMetadata = Optional.empty();\n\n    Map<String, String> newProperties =\n        readSnapshot\n            .getMetadata()\n            .filterOutUnchangedProperties(propertiesAdded.orElse(Collections.emptyMap()));\n\n    if (!newProperties.isEmpty()) {\n      newMetadata = Optional.of(readSnapshot.getMetadata().withMergedConfiguration(newProperties));\n    }\n\n    if (propertyKeysRemoved.isPresent()) {\n      newMetadata =\n          Optional.of(\n              newMetadata\n                  .orElse(readSnapshot.getMetadata())\n                  .withConfigurationKeysUnset(propertyKeysRemoved.get()));\n    }\n\n    if (newSchema.isPresent()) {\n      newMetadata =\n          Optional.of(\n              newMetadata.orElse(readSnapshot.getMetadata()).withNewSchema(newSchema.get()));\n    }\n\n    return new TransactionMetadataFactory(\n            tablePath,\n            Optional.of(readSnapshot),\n            newMetadata,\n            Optional.empty(),\n            propertiesAdded.orElse(Collections.emptyMap()) /* originalUserInputProperties */,\n            false /* isCreateOrReplace */,\n            clusteringColumns,\n            newSchema.isPresent() /* isSchemaEvolution */,\n            Optional.of(readSnapshot.getCommitter()))\n        .finalOutput;\n  }\n\n  ///////////////////////////////\n  // Instance Fields / Methods //\n  ///////////////////////////////\n\n  // ===== Fields that set by input =====\n  private final String tablePath;\n  private final Optional<SnapshotImpl> latestSnapshotOpt;\n\n  /**\n   * The table properties provided in this transaction. i.e. excludes any properties in the read\n   * snapshot.\n   *\n   * <p>This helps validation code understand what the user is trying to do in *this* transaction,\n   * as opposed to what is the current state already in the table.\n   */\n  private final Map<String, String> originalUserInputProperties;\n\n  private final boolean isCreateOrReplace;\n  private final boolean isSchemaEvolution;\n\n  // ===== Fields that are updated by helper methods when updating and validating the metadata =====\n  private Optional<Metadata> newMetadata;\n  private Optional<Protocol> newProtocol;\n  private Optional<List<Column>> physicalNewClusteringColumns;\n\n  // ===== Fields that are fixed after validation and updates are finished =====\n  private final Output finalOutput;\n\n  /**\n   * @param initialNewMetadata the initial metadata that we should validate and transform. It is a\n   *     function of the readSnapshot's metadata (if applicable) joined with any _user provided_\n   *     table property updates. Specifically:\n   *     <ul>\n   *       <li>CREATE: default empty metadata merged with schema, partCols, and user-specified table\n   *           properties\n   *       <li>UPDATE: readSnapshot's metadata merged wth user-specified added/removed table\n   *           properties\n   *       <li>REPLACE: readSnapshot's metadata, with all table properties removed except for those\n   *           that are included in TABLE_PROPERTY_KEYS_TO_PRESERVE, merged with schema, partCols,\n   *           and user-specified table properties\n   *     </ul>\n   *     <p>This class may apply additional updates to transform the {@code initialNewMetadata} the\n   *     final output (e.g. auto-enabling column mapping for iceberg compat, adding column mapping\n   *     metadata to the schema, etc.)\n   */\n  private TransactionMetadataFactory(\n      String tablePath,\n      Optional<SnapshotImpl> latestSnapshotOpt,\n      Optional<Metadata> initialNewMetadata,\n      Optional<Protocol> initialNewProtocol,\n      Map<String, String> originalUserInputProperties,\n      boolean isCreateOrReplace,\n      Optional<List<Column>> userProvidedLogicalClusteringColumns,\n      boolean isSchemaEvolution,\n      Optional<Committer> committerOpt) {\n    checkArgument(\n        (initialNewMetadata.isPresent() && initialNewProtocol.isPresent())\n            || latestSnapshotOpt.isPresent(),\n        \"initial protocol and metadata must be present for create table\");\n    checkArgument(\n        !isSchemaEvolution || !isCreateOrReplace,\n        \"isSchemaEvolution can only be true for update table\");\n    checkArgument(\n        isCreateOrReplace || latestSnapshotOpt.isPresent(),\n        \"update table must provide a latest snapshot\");\n    this.tablePath = tablePath;\n    this.latestSnapshotOpt = latestSnapshotOpt;\n    this.originalUserInputProperties = originalUserInputProperties;\n    this.isCreateOrReplace = isCreateOrReplace;\n    this.isSchemaEvolution = isSchemaEvolution;\n    this.newMetadata = initialNewMetadata;\n    this.newProtocol = initialNewProtocol;\n\n    performProtocolUpgrades(userProvidedLogicalClusteringColumns.isPresent());\n    handleCatalogManagedEnablement();\n    handleInCommitTimestampDisablement();\n    performIcebergCompatUpgradesAndValidation();\n    updateColumnMappingMetadataAndResolveClusteringColumns(userProvidedLogicalClusteringColumns);\n    updateRowTrackingMetadata();\n    validateMetadataChangeAndApplyTypeWidening();\n    validateRequiredCatalogTablePropertiesSet(committerOpt);\n    this.finalOutput = new Output(newProtocol, newMetadata, physicalNewClusteringColumns);\n  }\n\n  private Metadata getEffectiveMetadata() {\n    // Fact: either newMetadata is defined upon initiation or latestSnapshotOpt is present\n    return newMetadata.orElseGet(() -> latestSnapshotOpt.get().getMetadata());\n  }\n\n  private Protocol getEffectiveProtocol() {\n    // Fact: either newProtocol is defined upon initiation or latestSnapshotOpt is present\n    return newProtocol.orElseGet(() -> latestSnapshotOpt.get().getProtocol());\n  }\n\n  private void validateForUpdateTableUsingOldMetadata(Consumer<Metadata> validateFn) {\n    if (!isCreateOrReplace) {\n      // For update table we know latestSnapshotOpt is present\n      Metadata oldMetadata = latestSnapshotOpt.get().getMetadata();\n      validateFn.accept(oldMetadata);\n    }\n  }\n\n  /** STEP 1: Update the PROTOCOL based on the table properties or schema */\n  // TODO: if you only update the feature properties we currently write a new Metadata even though\n  //  this should just be a protocol upgrade (this could be an issue if for example you use\n  //  .withDomainMetadata supported in every txn --- we always write a new Metadata action)\n  private void performProtocolUpgrades(boolean clusteringRequired) {\n    // This is the only place we update the protocol action; takes care of any dependent features\n    // Ex: We enable feature `icebergCompatV2` plus dependent features `columnMapping`\n\n    // This will remove feature properties (i.e. metadata properties in the form of\n    // \"delta.feature.*\") from metadata. There should be one TableFeature in the returned set for\n    // each property removed.\n    Tuple2<Set<TableFeature>, Optional<Metadata>> newFeaturesAndMetadata =\n        TableFeatures.extractFeaturePropertyOverrides(getEffectiveMetadata());\n    if (newFeaturesAndMetadata._2.isPresent()) {\n      newMetadata = newFeaturesAndMetadata._2;\n    }\n\n    // Enable clustering if not already enabled and clustering columns are set. This isn't handled\n    // in autoUpgradeProtocolBasedOnMetadata since clustering is stored in the domain metadata\n    if (clusteringRequired\n        && !getEffectiveProtocol().supportsFeature(TableFeatures.CLUSTERING_W_FEATURE)) {\n      newFeaturesAndMetadata._1.add(TableFeatures.CLUSTERING_W_FEATURE);\n    }\n\n    Optional<Tuple2<Protocol, Set<TableFeature>>> newProtocolAndFeatures =\n        TableFeatures.autoUpgradeProtocolBasedOnMetadata(\n            getEffectiveMetadata(), newFeaturesAndMetadata._1, getEffectiveProtocol());\n    if (newProtocolAndFeatures.isPresent()) {\n      logger.info(\n          \"Automatically enabling table features: {}\",\n          newProtocolAndFeatures.get()._2.stream().map(TableFeature::featureName).collect(toSet()));\n\n      newProtocol = Optional.of(newProtocolAndFeatures.get()._1);\n      TableFeatures.validateKernelCanWriteToTable(\n          getEffectiveProtocol(), getEffectiveMetadata(), tablePath);\n    }\n  }\n\n  /** STEP 1.1: Handle catalogManaged enablement. Updates the METADATA if applicable. */\n  private void handleCatalogManagedEnablement() {\n    final boolean readVersionSupportsCatalogManaged =\n        latestSnapshotOpt\n            .map(x -> TableFeatures.isCatalogManagedSupported(x.getProtocol()))\n            .orElse(false);\n    final boolean writeVersionSupportsCatalogManaged =\n        TableFeatures.isCatalogManagedSupported(getEffectiveProtocol());\n    final boolean txnEnablesCatalogManaged =\n        !readVersionSupportsCatalogManaged && writeVersionSupportsCatalogManaged;\n\n    // Case 1: Txn is not enabling catalogManaged. Exit.\n    if (!txnEnablesCatalogManaged) {\n      return;\n    }\n\n    // catalogManaged is being enabled in this transaction. catalogManaged dependsOn\n    // inCommitTimestamp. The inCommitTimestamp feature should have been auto-supported in the\n    // protocol by now.\n    checkState(\n        getEffectiveProtocol().supportsFeature(TableFeatures.IN_COMMIT_TIMESTAMP_W_FEATURE),\n        \"inCommitTimestamp feature expected to have already been supported\");\n\n    // Case 2: Txn is explicitly disabling ICT. Throw.\n    final String txnIctEnabledValue =\n        originalUserInputProperties.get(TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey());\n    final boolean txnExplicitlyDisablesICT =\n        txnIctEnabledValue != null && txnIctEnabledValue.equalsIgnoreCase(\"false\");\n    if (txnExplicitlyDisablesICT) {\n      throw new KernelException(\"Cannot disable inCommitTimestamp when enabling catalogManaged\");\n    }\n\n    // Case 3: ICT already enabled. Exit.\n    final boolean isIctAlreadyEnabled =\n        TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(getEffectiveMetadata());\n    if (isIctAlreadyEnabled) {\n      return;\n    }\n\n    // Case 4: ICT is not enabled. Enable it.\n    newMetadata =\n        Optional.of(\n            getEffectiveMetadata()\n                .withMergedConfiguration(\n                    Collections.singletonMap(\n                        TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey(), \"true\")));\n  }\n\n  /**\n   * Step 1.2: Handle inCommitTimestamp disablement. Updates the METADATA if applicable.\n   *\n   * <p>If the user explicitly disables inCommitTimestamp in this transaction, we then also\n   * explicitly remove the ICT enablement version and timestamp properties from the metadata.\n   */\n  private void handleInCommitTimestampDisablement() {\n    final String txnIctEnabledValue =\n        originalUserInputProperties.get(TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey());\n    final boolean txnExplicitlyDisablesICT =\n        txnIctEnabledValue != null && txnIctEnabledValue.equalsIgnoreCase(\"false\");\n\n    // Case 1: Txn is not explicitly disabling ICT. Exit.\n    if (!txnExplicitlyDisablesICT) {\n      return;\n    }\n\n    // Case 2: Txn is explicitly disabling ICT on a catalogManaged table. Throw.\n    if (getEffectiveProtocol().supportsFeature(TableFeatures.CATALOG_MANAGED_RW_FEATURE)) {\n      throw new KernelException(\"Cannot disable inCommitTimestamp on a catalogManaged table\");\n    }\n\n    // Case 3 (normal case): Txn is explicitly disabling ICT. Remove the ICT enablement properties.\n    final Set<String> ictKeysToRemove =\n        new HashSet<>(\n            Arrays.asList(\n                TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey(),\n                TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey()));\n\n    newMetadata = Optional.of(getEffectiveMetadata().withConfigurationKeysUnset(ictKeysToRemove));\n  }\n\n  /**\n   * STEP 2: Validate the METADATA and PROTOCOL and possibly update the METADATA for IcebergCompat\n   */\n  private void performIcebergCompatUpgradesAndValidation() {\n    // IcebergCompat validates that the current metadata and protocol is compatible (e.g. all the\n    // required TF are present, no incompatible types, etc). It also updates the metadata for new\n    // tables if needed (e.g. enables column mapping)\n    // Ex: We enable column mapping mode in the configuration such that our properties now include\n    // Map(delta.enableIcebergCompatV2 -> true, delta.columnMapping.mode -> name)\n\n    // Validate this is a valid config change earlier for a clearer error message\n    validateForUpdateTableUsingOldMetadata(\n        oldMetadata -> {\n          newMetadata.ifPresent(\n              metadata ->\n                  IcebergWriterCompatV1MetadataValidatorAndUpdater\n                      .validateIcebergWriterCompatV1Change(\n                          oldMetadata.getConfiguration(), metadata.getConfiguration()));\n          newMetadata.ifPresent(\n              metadata ->\n                  IcebergCompatV3MetadataValidatorAndUpdater.validateIcebergCompatV3Change(\n                      oldMetadata.getConfiguration(), metadata.getConfiguration()));\n        });\n\n    // Pass the previous protocol to IcebergCompat checks as defense-in-depth: if the read\n    // snapshot already had deletion vectors enabled, the check should reject it even when the\n    // new protocol in this transaction does not include DVs. The conflict checker may also\n    // catch this at commit time, but checking early gives a clearer error message.\n    Optional<Protocol> prevProtocol = latestSnapshotOpt.map(s -> s.getProtocol());\n\n    // We must do our icebergWriterCompatV1 checks/updates FIRST since it has stricter column\n    // mapping requirements (id mode) than icebergCompatV2. It also may enable icebergCompatV2.\n    Optional<Metadata> icebergWriterCompatV1 =\n        IcebergWriterCompatV1MetadataValidatorAndUpdater\n            .validateAndUpdateIcebergWriterCompatV1Metadata(\n                isCreateOrReplace, getEffectiveMetadata(), getEffectiveProtocol(), prevProtocol);\n    if (icebergWriterCompatV1.isPresent()) {\n      newMetadata = icebergWriterCompatV1;\n    }\n\n    Optional<Metadata> icebergWriterCompatV3 =\n        IcebergWriterCompatV3MetadataValidatorAndUpdater\n            .validateAndUpdateIcebergWriterCompatV3Metadata(\n                isCreateOrReplace, getEffectiveMetadata(), getEffectiveProtocol(), prevProtocol);\n    if (icebergWriterCompatV3.isPresent()) {\n      newMetadata = icebergWriterCompatV3;\n    }\n\n    // TODO: refactor this method to use a single validator and updater.\n    Optional<Metadata> icebergCompatV2Metadata =\n        IcebergCompatV2MetadataValidatorAndUpdater.validateAndUpdateIcebergCompatV2Metadata(\n            isCreateOrReplace, getEffectiveMetadata(), getEffectiveProtocol(), prevProtocol);\n    if (icebergCompatV2Metadata.isPresent()) {\n      newMetadata = icebergCompatV2Metadata;\n    }\n    Optional<Metadata> icebergCompatV3Metadata =\n        IcebergCompatV3MetadataValidatorAndUpdater.validateAndUpdateIcebergCompatV3Metadata(\n            isCreateOrReplace, getEffectiveMetadata(), getEffectiveProtocol(), prevProtocol);\n    if (icebergCompatV3Metadata.isPresent()) {\n      newMetadata = icebergCompatV3Metadata;\n    }\n  }\n\n  /**\n   * STEP 3: Update the METADATA with column mapping info if applicable, and resolve the provided\n   * clustering columns using the updated metadata.\n   */\n  private void updateColumnMappingMetadataAndResolveClusteringColumns(\n      Optional<List<Column>> userProvidedClusteringColumns) {\n    // We update the column mapping info here after all configuration changes are finished\n    Optional<Metadata> columnMappingMetadata =\n        ColumnMapping.updateColumnMappingMetadataIfNeeded(\n            getEffectiveMetadata(), isCreateOrReplace);\n    if (columnMappingMetadata.isPresent()) {\n      newMetadata = columnMappingMetadata;\n    }\n    // We also resolve the user provided clustering columns here using the updated schema\n    StructType updatedSchema = getEffectiveMetadata().getSchema();\n    this.physicalNewClusteringColumns =\n        userProvidedClusteringColumns.map(\n            cols -> SchemaUtils.casePreservingEligibleClusterColumns(updatedSchema, cols));\n  }\n\n  /** STEP 4: Update the METADATA with materialized row tracking column name if applicable */\n  private void updateRowTrackingMetadata() {\n    if (isCreateOrReplace) {\n      // For new tables, assign materialized column names if row tracking is enabled\n      Optional<Metadata> rowTrackingMetadata =\n          MaterializedRowTrackingColumn.assignMaterializedColumnNamesIfNeeded(\n              getEffectiveMetadata());\n      if (rowTrackingMetadata.isPresent()) {\n        newMetadata = rowTrackingMetadata;\n      }\n    }\n    validateForUpdateTableUsingOldMetadata(\n        oldMetadata -> {\n          // For existing tables, we block enabling/disabling row tracking because:\n          // 1. Enabling requires backfilling row IDs/commit versions, which is not supported in\n          // Kernel\n          // 2. Disabling is irreversible in Kernel (re-enabling not supported)\n          newMetadata.ifPresent(\n              metadata -> RowTracking.throwIfRowTrackingToggled(oldMetadata, metadata));\n\n          // For existing tables, validate that row tracking configs are present when row tracking\n          // is enabled\n          MaterializedRowTrackingColumn.validateRowTrackingConfigsNotMissing(\n              getEffectiveMetadata(), tablePath);\n        });\n  }\n\n  /**\n   * STEP 5: Validate the metadata change and update the type widening metadata if needed. Note: we\n   * update the type widening info at the same time to avoid traversing the schema more than\n   * required.\n   *\n   * <p>Validate that the change from oldMetadata to newMetadata is a valid change. For example,\n   * this checks the following\n   *\n   * <ul>\n   *   <li>Column mapping mode can only go from none->name for existing table\n   *   <li>icebergWriterCompatV1 cannot be enabled on existing tables (only supported upon table\n   *       creation)\n   *   <li>Validates the universal format configs are valid.\n   *   <li>If there is schema evolution validates\n   *       <ul>\n   *         <li>column mapping is enabled\n   *         <li>column mapping mode is not changed in the same txn as schema change\n   *         <li>the new schema is a valid schema\n   *         <li>the schema change is a valid schema change\n   *         <li>the schema change is a valid schema change given the tables partition and\n   *             clustering columns\n   *       </ul>\n   *   <li>Materialized row tracking column names do not conflict with schema\n   * </ul>\n   */\n  private void validateMetadataChangeAndApplyTypeWidening() {\n    validateForUpdateTableUsingOldMetadata(\n        oldMetadata -> {\n          ColumnMapping.verifyColumnMappingChange(\n              oldMetadata.getConfiguration(), getEffectiveMetadata().getConfiguration());\n          IcebergWriterCompatV1MetadataValidatorAndUpdater.validateIcebergWriterCompatV1Change(\n              oldMetadata.getConfiguration(), getEffectiveMetadata().getConfiguration());\n          IcebergCompatV3MetadataValidatorAndUpdater.validateIcebergCompatV3Change(\n              oldMetadata.getConfiguration(), getEffectiveMetadata().getConfiguration());\n        });\n    IcebergUniversalFormatMetadataValidatorAndUpdater.validate(getEffectiveMetadata());\n\n    validateForUpdateTableUsingOldMetadata(\n        oldMetadata -> {\n          if (isSchemaEvolution) {\n            ColumnMappingMode updatedMappingMode =\n                ColumnMapping.getColumnMappingMode(getEffectiveMetadata().getConfiguration());\n            ColumnMappingMode currentMappingMode =\n                ColumnMapping.getColumnMappingMode(oldMetadata.getConfiguration());\n            if (currentMappingMode != updatedMappingMode) {\n              throw new KernelException(\"Cannot update mapping mode and perform schema evolution\");\n            }\n\n            // If the column mapping restriction is removed, clustering columns\n            // will need special handling during schema evolution since they won't have physical\n            // names\n            // ToDo: Support adding clustering columns\n\n            if (!isColumnMappingModeEnabled(updatedMappingMode)) {\n              throw new KernelException(\n                  \"Cannot update schema for table when column mapping is disabled\");\n            }\n\n            // Clustering columns will be guaranteed to have physical names at this point\n            // Only the leaf part of the overall column needs to be taken since\n            // validation is performed on the leaf struct fields\n            // E.g. getClusteringColumns returns <physical_name_of_struct>.<physical_name_inner>,\n            // Only physical_name_inner is required for validation\n            Optional<List<Column>> effectiveClusteringCols =\n                physicalNewClusteringColumns.isPresent()\n                    ? physicalNewClusteringColumns\n                    : latestSnapshotOpt.get().getPhysicalClusteringColumns();\n            Set<String> clusteringColumnPhysicalNames =\n                effectiveClusteringCols.orElse(Collections.emptyList()).stream()\n                    .map(col -> col.getNames()[col.getNames().length - 1])\n                    .collect(toSet());\n\n            Optional<StructType> schemaWithTypeWidening =\n                SchemaUtils.validateUpdatedSchemaAndGetUpdatedSchema(\n                    oldMetadata,\n                    getEffectiveMetadata(),\n                    getEffectiveProtocol(),\n                    clusteringColumnPhysicalNames,\n                    false /* allowNewRequiredFields */);\n\n            schemaWithTypeWidening.ifPresent(\n                structType ->\n                    newMetadata = Optional.of(getEffectiveMetadata().withNewSchema(structType)));\n          }\n        });\n\n    // For replace table we need to do special validation in the case of fieldId re-use\n    if (isCreateOrReplace && latestSnapshotOpt.isPresent()) {\n      // For now, we don't support changing column mapping mode during replace, in a future PR we\n      // will loosen this restriction\n      ColumnMappingMode oldMode =\n          ColumnMapping.getColumnMappingMode(\n              latestSnapshotOpt.get().getMetadata().getConfiguration());\n      ColumnMappingMode newMode =\n          ColumnMapping.getColumnMappingMode(getEffectiveMetadata().getConfiguration());\n      if (oldMode != newMode) {\n        throw new UnsupportedOperationException(\n            String.format(\n                \"Changing column mapping mode from %s to %s is not currently supported in Kernel \"\n                    + \"during REPLACE TABLE operations\",\n                oldMode, newMode));\n      }\n\n      // We only need to check fieldId re-use when cmMode != none\n      if (newMode != ColumnMappingMode.NONE) {\n        Optional<StructType> schemaWithTypeWidening =\n            SchemaUtils.validateUpdatedSchemaAndGetUpdatedSchema(\n                latestSnapshotOpt.get().getMetadata(),\n                getEffectiveMetadata(),\n                getEffectiveProtocol(),\n                // We already validate clustering columns elsewhere for isCreateOrReplace no\n                // need to\n                // duplicate this check here\n                emptySet() /* clusteringCols */,\n                // We allow new non-null fields in REPLACE since we know all existing data is\n                // removed\n                true /* allowNewRequiredFields */);\n        schemaWithTypeWidening.ifPresent(\n            structType ->\n                newMetadata = Optional.of(getEffectiveMetadata().withNewSchema(structType)));\n      }\n    }\n\n    MaterializedRowTrackingColumn.throwIfColumnNamesConflictWithSchema(getEffectiveMetadata());\n  }\n\n  /**\n   * STEP 6: Validate that required catalog table properties are set. Below is a complete summary of\n   * our required-catalog-property setting and validation:\n   *\n   * <p>First, during CREATE and REPLACE, we inject and set the required catalog table properties.\n   * Note that we do this *before* setting any user properties such that if a user overrides a\n   * required catalog property, we will detect that here.\n   *\n   * <p>Next, here, we validate that all required catalog table properties are, in fact, set to\n   * their required values. Thus, if a property was explicitly removed during UPDATE, changed to an\n   * invalid value during UPDATE, or set to an invalid value during CREATE or REPLACE, we will\n   * detect and fail.\n   */\n  private void validateRequiredCatalogTablePropertiesSet(Optional<Committer> committerOpt) {\n    if (!committerOpt.isPresent()) {\n      return;\n    }\n\n    final Committer committer = committerOpt.get();\n    final Map<String, String> requiredCatalogTableProperties =\n        getRequiredCatalogTablePropertiesIfApplicable(committer);\n\n    if (requiredCatalogTableProperties.isEmpty()) {\n      return;\n    }\n\n    final Map<String, String> effectiveTableProperties = getEffectiveMetadata().getConfiguration();\n    final Map<String, String> missingOrViolatingProperties = new HashMap<>();\n\n    for (Map.Entry<String, String> requiredEntry : requiredCatalogTableProperties.entrySet()) {\n      final String requiredKey = requiredEntry.getKey();\n      final String requiredValue = requiredEntry.getValue();\n      final String currentValue = effectiveTableProperties.get(requiredKey);\n      if (!Objects.equals(requiredValue, currentValue)) {\n        missingOrViolatingProperties.put(\n            requiredKey, Optional.ofNullable(currentValue).orElse(\"<not set>\"));\n      }\n    }\n\n    if (!missingOrViolatingProperties.isEmpty()) {\n      throw DeltaErrors.metadataMissingRequiredCatalogTableProperty(\n          committer.getClass().getName(),\n          missingOrViolatingProperties,\n          requiredCatalogTableProperties);\n    }\n  }\n\n  private static Metadata getInitialMetadata(\n      StructType schema, Map<String, String> tableProperties, List<String> partitionColumns) {\n    return new Metadata(\n        java.util.UUID.randomUUID().toString(), /* id */\n        Optional.empty(), /* name */\n        Optional.empty(), /* description */\n        new Format(), /* format */\n        schema.toJson(), /* schemaString */\n        schema, /* schema */\n        buildArrayValue(\n            casePreservingPartitionColNames(schema, partitionColumns),\n            StringType.STRING), /* partitionColumns */\n        Optional.of(System.currentTimeMillis()), /* createdTime */\n        stringStringMapValue(tableProperties) /* configuration */);\n  }\n\n  private static Protocol getInitialProtocol() {\n    return new Protocol(DEFAULT_READ_VERSION, DEFAULT_WRITE_VERSION);\n  }\n\n  private static void validateSchemaAndPartColsCreateOrReplace(\n      Map<String, String> tableProperties, StructType schema, List<String> partitionColumns) {\n    // New table verify the given schema and partition columns\n    ColumnMappingMode mappingMode = ColumnMapping.getColumnMappingMode(tableProperties);\n\n    SchemaUtils.validateSchema(\n        schema,\n        isColumnMappingModeEnabled(mappingMode),\n        TableFeatures.isPropertiesManuallySupportingTableFeature(\n            tableProperties, ALLOW_COLUMN_DEFAULTS_W_FEATURE),\n        TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(tableProperties));\n    SchemaUtils.validatePartitionColumns(schema, partitionColumns);\n  }\n\n  private static void validateNotEnablingCatalogManagedOnReplace(\n      Map<String, String> userInputTableProperties) {\n    if (TableFeatures.isPropertiesManuallySupportingTableFeature(\n        userInputTableProperties, TableFeatures.CATALOG_MANAGED_RW_FEATURE)) {\n      throw new UnsupportedOperationException(\n          \"Cannot enable the catalogManaged feature during a REPLACE command.\");\n    }\n  }\n\n  private static Map<String, String> getRequiredCatalogTablePropertiesIfApplicable(\n      Committer committer) {\n    if (committer instanceof CatalogCommitter) {\n      return ((CatalogCommitter) committer).getRequiredTableProperties();\n    }\n\n    return Collections.emptyMap();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/UpdateTableTransactionBuilderImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.SetTransaction;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.Clock;\nimport io.delta.kernel.transaction.UpdateTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\n\npublic class UpdateTableTransactionBuilderImpl implements UpdateTableTransactionBuilder {\n\n  private Clock clock = System::currentTimeMillis;\n\n  /** Timestamp when this builder was created, used for populating any {@link SetTransaction} */\n  private final long txnBuilderStartTime = System.currentTimeMillis();\n\n  /* Class fields provided in the constructor */\n  private final SnapshotImpl snapshot;\n  private final String engineInfo;\n  private final Operation operation;\n\n  /* Optional metadata configured in this builder by the connector */\n  private Optional<SetTransaction> setTxnOpt = Optional.empty();\n  private Optional<Map<String, String>> tablePropertiesAddedOpt = Optional.empty();\n  private Optional<Set<String>> tablePropertiesRemovedOpt = Optional.empty();\n  private Optional<StructType> updatedSchemaOpt = Optional.empty();\n  private Optional<List<Column>> inputLogicalClusteringColumnsOpt = Optional.empty();\n  private Optional<Integer> userProvidedMaxRetries = Optional.empty();\n\n  /** Number of commits between producing a log compaction file. */\n  private int logCompactionInterval = 0;\n\n  public UpdateTableTransactionBuilderImpl(\n      SnapshotImpl snapshot, String engineInfo, Operation operation) {\n    validateIsUpdateOperation(operation);\n    this.snapshot = snapshot;\n    this.engineInfo = engineInfo;\n    this.operation = operation;\n    TableFeatures.validateKernelCanWriteToTable(\n        snapshot.getProtocol(), snapshot.getMetadata(), snapshot.getPath());\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withUpdatedSchema(StructType schema) {\n    this.updatedSchemaOpt = Optional.of(schema);\n    return this;\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withTablePropertiesAdded(Map<String, String> properties) {\n    this.tablePropertiesAddedOpt =\n        Optional.of(\n            Collections.unmodifiableMap(\n                TableConfig.validateAndNormalizeDeltaProperties(properties)));\n    validateTablePropertiesAddedRemovedNoOverlap();\n    return this;\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withTablePropertiesRemoved(Set<String> propertyKeys) {\n    checkArgument(\n        propertyKeys.stream().noneMatch(key -> key.toLowerCase(Locale.ROOT).startsWith(\"delta.\")),\n        \"Unsetting 'delta.' table properties is currently unsupported\");\n    this.tablePropertiesRemovedOpt = Optional.of(propertyKeys);\n    validateTablePropertiesAddedRemovedNoOverlap();\n    return this;\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withClusteringColumns(List<Column> clusteringColumns) {\n    if (snapshot.getPartitionColumnNames().size() > 0) {\n      throw DeltaErrors.enablingClusteringOnPartitionedTableNotAllowed(\n          snapshot.getPath(), snapshot.getMetadata().getPartitionColNames(), clusteringColumns);\n    }\n    this.inputLogicalClusteringColumnsOpt = Optional.of(clusteringColumns);\n    return this;\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withTransactionId(\n      String applicationId, long transactionVersion) {\n    SetTransaction txnId =\n        new SetTransaction(\n            requireNonNull(applicationId, \"applicationId is null\"),\n            transactionVersion,\n            Optional.of(txnBuilderStartTime));\n    this.setTxnOpt = Optional.of(txnId);\n    return this;\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withMaxRetries(int maxRetries) {\n    checkArgument(maxRetries >= 0, \"maxRetries must be >= 0\");\n    this.userProvidedMaxRetries = Optional.of(maxRetries);\n    return this;\n  }\n\n  @Override\n  public UpdateTableTransactionBuilder withLogCompactionInterval(int logCompactionInterval) {\n    checkArgument(logCompactionInterval >= 0, \"logCompactionInterval must be >= 0\");\n    this.logCompactionInterval = logCompactionInterval;\n    return this;\n  }\n\n  @VisibleForTesting\n  public UpdateTableTransactionBuilder withClock(Clock clock) {\n    this.clock = requireNonNull(clock, \"clock cannot be null\");\n    return this;\n  }\n\n  @Override\n  public Transaction build(Engine engine) {\n    setTxnOpt.ifPresent(\n        txnId -> {\n          Optional<Long> lastTxnVersion =\n              snapshot.getLatestTransactionVersion(engine, txnId.getAppId());\n          if (lastTxnVersion.isPresent() && lastTxnVersion.get() >= txnId.getVersion()) {\n            throw DeltaErrors.concurrentTransaction(\n                txnId.getAppId(), txnId.getVersion(), lastTxnVersion.get());\n          }\n        });\n\n    TransactionMetadataFactory.Output txnMetadata =\n        TransactionMetadataFactory.buildUpdateTableMetadata(\n            snapshot.getPath(),\n            snapshot,\n            tablePropertiesAddedOpt,\n            tablePropertiesRemovedOpt,\n            updatedSchemaOpt,\n            inputLogicalClusteringColumnsOpt);\n\n    return new TransactionImpl(\n        false /* isCreateOrReplace */,\n        snapshot.getDataPath(),\n        Optional.of(snapshot),\n        engineInfo,\n        operation,\n        txnMetadata.newProtocol,\n        txnMetadata.newMetadata,\n        snapshot.getCommitter(),\n        setTxnOpt,\n        txnMetadata.physicalNewClusteringColumns,\n        userProvidedMaxRetries,\n        logCompactionInterval,\n        clock);\n  }\n\n  private void validateTablePropertiesAddedRemovedNoOverlap() {\n    if (tablePropertiesAddedOpt.isPresent() && tablePropertiesRemovedOpt.isPresent()) {\n      Set<String> invalidPropertyKeys =\n          tablePropertiesRemovedOpt.get().stream()\n              .filter(tablePropertiesAddedOpt.get()::containsKey)\n              .collect(toSet());\n      if (!invalidPropertyKeys.isEmpty()) {\n        throw DeltaErrors.overlappingTablePropertiesSetAndUnset(invalidPropertyKeys);\n      }\n    }\n  }\n\n  private void validateIsUpdateOperation(Operation operation) {\n    if (operation == Operation.CREATE_TABLE || operation == Operation.REPLACE_TABLE) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"Operation %s is not compatible with Snapshot::buildUpdateTableTransaction\",\n              operation));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/AddCDCFile.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport io.delta.kernel.types.*;\n\n/** Metadata about {@code cdc} action in the Delta Log. */\npublic class AddCDCFile {\n  /** Full schema of the {@code cdc} action in the Delta Log. */\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"path\", StringType.STRING, false /* nullable */)\n          .add(\n              \"partitionValues\",\n              new MapType(StringType.STRING, StringType.STRING, true),\n              false /* nullable*/)\n          .add(\"size\", LongType.LONG, false /* nullable*/)\n          .add(\n              \"tags\", new MapType(StringType.STRING, StringType.STRING, true), true /* nullable */);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/AddFile.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.InternalUtils.relativizePath;\nimport static io.delta.kernel.internal.util.PartitionUtils.serializePartitionMap;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.VectorUtils.toJavaMap;\n\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.net.URI;\nimport java.util.*;\n\n/** Delta log action representing an `AddFile` */\npublic class AddFile extends RowBackedAction {\n\n  /* We conditionally read this field based on the query filter */\n  private static final StructField JSON_STATS_FIELD =\n      new StructField(\"stats\", StringType.STRING, true /* nullable */);\n\n  /**\n   * Schema of the {@code add} action in the Delta Log without stats. Used for constructing table\n   * snapshot to read data from the table.\n   */\n  public static final StructType SCHEMA_WITHOUT_STATS =\n      new StructType()\n          .add(\"path\", StringType.STRING, false /* nullable */)\n          .add(\n              \"partitionValues\",\n              new MapType(StringType.STRING, StringType.STRING, true),\n              false /* nullable*/)\n          .add(\"size\", LongType.LONG, false /* nullable*/)\n          .add(\"modificationTime\", LongType.LONG, false /* nullable*/)\n          .add(\"dataChange\", BooleanType.BOOLEAN, false /* nullable*/)\n          .add(\"deletionVector\", DeletionVectorDescriptor.READ_SCHEMA, true /* nullable */)\n          .add(\n              \"tags\",\n              new MapType(StringType.STRING, StringType.STRING, true /* valueContainsNull */),\n              true /* nullable */)\n          .add(\"baseRowId\", LongType.LONG, true /* nullable */)\n          .add(\"defaultRowCommitVersion\", LongType.LONG, true /* nullable */);\n\n  public static final StructType SCHEMA_WITH_STATS = SCHEMA_WITHOUT_STATS.add(JSON_STATS_FIELD);\n\n  /** Full schema of the {@code add} action in the Delta Log. */\n  public static final StructType FULL_SCHEMA = SCHEMA_WITH_STATS;\n\n  // There are more fields which are added when row-id tracking and clustering is enabled.\n  // When Kernel starts supporting row-ids and clustering, we should add those fields here.\n\n  /**\n   * Utility to generate {@link AddFile} action instance from the given {@link DataFileStatus} and\n   * partition values.\n   */\n  public static AddFile convertDataFileStatus(\n      StructType physicalSchema,\n      URI tableRoot,\n      DataFileStatus dataFileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Map<String, String> tags,\n      Optional<Long> baseRowId,\n      Optional<Long> defaultRowCommitVersion,\n      Optional<DeletionVectorDescriptor> deletionVectorDescriptor) {\n    Optional<MapValue> tagMapValue =\n        !tags.isEmpty() ? Optional.of(VectorUtils.stringStringMapValue(tags)) : Optional.empty();\n    Row row =\n        createAddFileRow(\n            physicalSchema,\n            relativizePath(new Path(dataFileStatus.getPath()), tableRoot).toUri().toString(),\n            serializePartitionMap(partitionValues),\n            dataFileStatus.getSize(),\n            dataFileStatus.getModificationTime(),\n            dataChange,\n            deletionVectorDescriptor,\n            tagMapValue, // tags\n            baseRowId,\n            defaultRowCommitVersion,\n            dataFileStatus.getStatistics());\n\n    return new AddFile(row);\n  }\n\n  /** Utility to generate an 'AddFile' row from the given fields. */\n  public static Row createAddFileRow(\n      StructType physicalSchema,\n      String path,\n      MapValue partitionValues,\n      long size,\n      long modificationTime,\n      boolean dataChange,\n      Optional<DeletionVectorDescriptor> deletionVector,\n      Optional<MapValue> tags,\n      Optional<Long> baseRowId,\n      Optional<Long> defaultRowCommitVersion,\n      Optional<DataFileStatistics> stats) {\n\n    checkArgument(path != null, \"path is not nullable\");\n    checkArgument(partitionValues != null, \"partitionValues is not nullable\");\n\n    Map<Integer, Object> fieldMap = new HashMap<>();\n    fieldMap.put(FULL_SCHEMA.indexOf(\"path\"), path);\n    fieldMap.put(FULL_SCHEMA.indexOf(\"partitionValues\"), partitionValues);\n    fieldMap.put(FULL_SCHEMA.indexOf(\"size\"), size);\n    fieldMap.put(FULL_SCHEMA.indexOf(\"modificationTime\"), modificationTime);\n    fieldMap.put(FULL_SCHEMA.indexOf(\"dataChange\"), dataChange);\n    tags.ifPresent(tag -> fieldMap.put(FULL_SCHEMA.indexOf(\"tags\"), tag));\n    baseRowId.ifPresent(id -> fieldMap.put(FULL_SCHEMA.indexOf(\"baseRowId\"), id));\n    defaultRowCommitVersion.ifPresent(\n        version -> fieldMap.put(FULL_SCHEMA.indexOf(\"defaultRowCommitVersion\"), version));\n    stats.ifPresent(\n        stat -> fieldMap.put(FULL_SCHEMA.indexOf(\"stats\"), stat.serializeAsJson(physicalSchema)));\n    deletionVector.ifPresent(\n        dv -> {\n          Row dvRow = dv.toRow();\n          fieldMap.put(FULL_SCHEMA.indexOf(\"deletionVector\"), dvRow);\n        });\n    return new GenericRow(FULL_SCHEMA, fieldMap);\n  }\n\n  ////////////////////////////////////\n  // Constructor and Member Methods //\n  ////////////////////////////////////\n\n  /** Constructs an {@link AddFile} action from the given 'AddFile' {@link Row}. */\n  public AddFile(Row row) {\n    super(row);\n  }\n\n  public String getPath() {\n    return row.getString(getFieldIndex(\"path\"));\n  }\n\n  public MapValue getPartitionValues() {\n    return row.getMap(getFieldIndex(\"partitionValues\"));\n  }\n\n  public long getSize() {\n    return row.getLong(getFieldIndex(\"size\"));\n  }\n\n  public long getModificationTime() {\n    return row.getLong(getFieldIndex(\"modificationTime\"));\n  }\n\n  public boolean getDataChange() {\n    return row.getBoolean(getFieldIndex(\"dataChange\"));\n  }\n\n  public Optional<DeletionVectorDescriptor> getDeletionVector() {\n    int index = getFieldIndex(\"deletionVector\");\n    return Optional.ofNullable(\n        row.isNullAt(index) ? null : DeletionVectorDescriptor.fromRow(row.getStruct(index)));\n  }\n\n  public Optional<MapValue> getTags() {\n    int index = getFieldIndex(\"tags\");\n    return Optional.ofNullable(row.isNullAt(index) ? null : row.getMap(index));\n  }\n\n  public Optional<String> getStatsJson() {\n    Optional<Integer> statsIndexOpt = getFieldIndexOpt(\"stats\");\n    return statsIndexOpt.map(\n        index -> {\n          if (row.isNullAt(index)) {\n            return null;\n          }\n          return row.getString(index);\n        });\n  }\n\n  public Optional<Long> getBaseRowId() {\n    int index = getFieldIndex(\"baseRowId\");\n    return Optional.ofNullable(row.isNullAt(index) ? null : row.getLong(index));\n  }\n\n  public Optional<Long> getDefaultRowCommitVersion() {\n    int index = getFieldIndex(\"defaultRowCommitVersion\");\n    return Optional.ofNullable(row.isNullAt(index) ? null : row.getLong(index));\n  }\n\n  public Optional<Long> getNumRecords() {\n    return getFieldIndexOpt(\"stats\")\n        .flatMap(\n            index ->\n                row.isNullAt(index)\n                    ? Optional.empty()\n                    : DataFileStatistics.getNumRecords(row.getString(index)));\n  }\n\n  /**\n   * Returns the file statistics parsed from the stats JSON string using the provided schema. This\n   * method deserializes the statistics JSON with full type information, ensuring that min/max\n   * values and null counts are correctly typed according to the physical schema.\n   *\n   * @param physicalSchema the physical schema of the table, used to correctly parse and type the\n   *     statistics values (min/max values and null counts)\n   * @return an {@link Optional} containing the deserialized {@link DataFileStatistics} if the stats\n   *     field is present and non-null, or {@link Optional#empty()} otherwise\n   * @throws io.delta.kernel.exceptions.KernelException if the stats JSON is malformed or if values\n   *     don't match the expected types from the schema\n   * @see DataFileStatistics#deserializeFromJson(String, StructType) for details on the\n   *     deserialization process\n   */\n  public Optional<DataFileStatistics> getStats(StructType physicalSchema) {\n    return getFieldIndexOpt(\"stats\")\n        .flatMap(\n            index ->\n                row.isNullAt(index)\n                    ? Optional.empty()\n                    : DataFileStatistics.deserializeFromJson(row.getString(index), physicalSchema));\n  }\n\n  /** Returns a new {@link AddFile} with the provided baseRowId. */\n  public AddFile withNewBaseRowId(long baseRowId) {\n    return new AddFile(toRowWithOverriddenValue(\"baseRowId\", baseRowId));\n  }\n\n  /** Returns a new {@link AddFile} with the provided defaultRowCommitVersion. */\n  public AddFile withNewDefaultRowCommitVersion(long defaultRowCommitVersion) {\n    return new AddFile(\n        toRowWithOverriddenValue(\"defaultRowCommitVersion\", defaultRowCommitVersion));\n  }\n\n  /**\n   * Utility to generate a 'RemoveFile' action from this AddFile action.\n   *\n   * @param dataChange this will override the dataChange field in current AddFile\n   * @param deletionTimestamp the deletion timestamp of the operation, this will override the\n   *     modificationTime field in current AddFile\n   */\n  public Row toRemoveFileRow(boolean dataChange, Optional<Long> deletionTimestamp) {\n    Map<Integer, Object> fieldMap = new HashMap<>();\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"path\"), getPath());\n    fieldMap.put(\n        RemoveFile.FULL_SCHEMA.indexOf(\"deletionTimestamp\"),\n        deletionTimestamp.orElse(System.currentTimeMillis()));\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"dataChange\"), dataChange);\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"extendedFileMetadata\"), true);\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"partitionValues\"), getPartitionValues());\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"size\"), getSize());\n    getStatsJson()\n        .ifPresent(statsJson -> fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"stats\"), statsJson));\n    getTags().ifPresent(tags -> fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"tags\"), tags));\n    if (!row.isNullAt(getFieldIndex(\"deletionVector\"))) {\n      fieldMap.put(\n          RemoveFile.FULL_SCHEMA.indexOf(\"deletionVector\"),\n          row.getStruct(getFieldIndex(\"deletionVector\")));\n    }\n    getBaseRowId()\n        .ifPresent(\n            baseRowId -> fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"baseRowId\"), baseRowId));\n    getDefaultRowCommitVersion()\n        .ifPresent(\n            defaultRowCommitVersion ->\n                fieldMap.put(\n                    RemoveFile.FULL_SCHEMA.indexOf(\"defaultRowCommitVersion\"),\n                    defaultRowCommitVersion));\n\n    return new GenericRow(RemoveFile.FULL_SCHEMA, fieldMap);\n  }\n\n  @Override\n  public String toString() {\n    // No specific ordering is guaranteed for partitionValues and tags in the returned string\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"AddFile{\");\n    sb.append(\"path='\").append(getPath()).append('\\'');\n    sb.append(\", partitionValues=\").append(toJavaMap(getPartitionValues()));\n    sb.append(\", size=\").append(getSize());\n    sb.append(\", modificationTime=\").append(getModificationTime());\n    sb.append(\", dataChange=\").append(getDataChange());\n    sb.append(\", deletionVector=\").append(getDeletionVector());\n    sb.append(\", tags=\").append(getTags().map(VectorUtils::toJavaMap));\n    sb.append(\", baseRowId=\").append(getBaseRowId());\n    sb.append(\", defaultRowCommitVersion=\").append(getDefaultRowCommitVersion());\n    sb.append(\", stats=\").append(getStats(null).map(d -> d.serializeAsJson(null)).orElse(\"\"));\n    sb.append('}');\n    return sb.toString();\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (this == obj) return true;\n    if (!(obj instanceof AddFile)) return false;\n    AddFile other = (AddFile) obj;\n\n    // MapValue and DataFileStatistics don't implement equals(), so we need to convert\n    // partitionValues and tags to Java Maps, and stats to strings to compare them\n    return getSize() == other.getSize()\n        && getModificationTime() == other.getModificationTime()\n        && getDataChange() == other.getDataChange()\n        && Objects.equals(getPath(), other.getPath())\n        && Objects.equals(toJavaMap(getPartitionValues()), toJavaMap(other.getPartitionValues()))\n        && Objects.equals(getDeletionVector(), other.getDeletionVector())\n        && Objects.equals(\n            getTags().map(VectorUtils::toJavaMap), other.getTags().map(VectorUtils::toJavaMap))\n        && Objects.equals(getBaseRowId(), other.getBaseRowId())\n        && Objects.equals(getDefaultRowCommitVersion(), other.getDefaultRowCommitVersion())\n        && Objects.equals(getStats(null), other.getStats(null));\n  }\n\n  @Override\n  public int hashCode() {\n    // MapValue and DataFileStatistics don't implement hashCode(), so we need to convert\n    // partitionValues and tags to Java Maps, and stats to strings to compute the hash code\n    return Objects.hash(\n        getPath(),\n        toJavaMap(getPartitionValues()),\n        getSize(),\n        getModificationTime(),\n        getDataChange(),\n        getDeletionVector(),\n        getTags().map(VectorUtils::toJavaMap),\n        getBaseRowId(),\n        getDefaultRowCommitVersion(),\n        getStats(null));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/CommitInfo.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\nimport static io.delta.kernel.internal.util.VectorUtils.stringStringMapValue;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toMap;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.stream.IntStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Delta log action representing a commit information action. According to the Delta protocol there\n * isn't any specific schema for this action, but we use the following schema:\n *\n * <ul>\n *   <li>inCommitTimestamp: Long - A monotonically increasing timestamp that represents the time\n *       since epoch in milliseconds when the commit write was started\n *   <li>timestamp: Long - Milliseconds since epoch UTC of when this commit happened\n *   <li>engineInfo: String - Engine that made this commit\n *   <li>operation: String - Operation (e.g. insert, delete, merge etc.)\n *   <li>operationParameters: Map(String, String) - each operation depending upon the type may add\n *       zero or more parameters about the operation. E.g. when creating a table `partitionBy` key\n *       with list of partition columns is added.\n *   <li>isBlindAppend: Boolean - Is this commit a blind append?\n *   <li>txnId: String - a unique transaction id of this commit\n * </ul>\n *\n * The Delta-Spark connector adds lot more fields to this action. We can add them as needed.\n */\npublic class CommitInfo {\n\n  //////////////////////////////////\n  // Static variables and methods //\n  //////////////////////////////////\n\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"inCommitTimestamp\", LongType.LONG, true /* nullable */)\n          .add(\"timestamp\", LongType.LONG)\n          .add(\"engineInfo\", StringType.STRING)\n          .add(\"operation\", StringType.STRING)\n          .add(\n              \"operationParameters\",\n              new MapType(StringType.STRING, StringType.STRING, true /* nullable */))\n          .add(\"isBlindAppend\", BooleanType.BOOLEAN, true /* nullable */)\n          .add(\"txnId\", StringType.STRING)\n          .add(\n              \"operationMetrics\",\n              new MapType(StringType.STRING, StringType.STRING, true /* nullable */));\n\n  private static final StructType READ_SCHEMA =\n      new StructType().add(\"commitInfo\", CommitInfo.FULL_SCHEMA);\n\n  private static final Map<String, Integer> COL_NAME_TO_ORDINAL =\n      IntStream.range(0, FULL_SCHEMA.length())\n          .boxed()\n          .collect(toMap(i -> FULL_SCHEMA.at(i).getName(), i -> i));\n\n  private static final Logger logger = LoggerFactory.getLogger(CommitInfo.class);\n\n  public static CommitInfo fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n\n    ColumnVector[] children = new ColumnVector[8];\n    for (int i = 0; i < children.length; i++) {\n      children[i] = vector.getChild(i);\n    }\n\n    checkArgument(!children[1].isNullAt(rowId), \"CommitInfo is missing required timestamp field\");\n\n    return new CommitInfo(\n        Optional.ofNullable(children[0].isNullAt(rowId) ? null : children[0].getLong(rowId)),\n        children[1].getLong(rowId),\n        Optional.ofNullable(children[2].isNullAt(rowId) ? null : children[2].getString(rowId)),\n        Optional.ofNullable(children[3].isNullAt(rowId) ? null : children[3].getString(rowId)),\n        children[4].isNullAt(rowId)\n            ? Collections.emptyMap()\n            : VectorUtils.toJavaMap(children[4].getMap(rowId)),\n        Optional.ofNullable(children[5].isNullAt(rowId) ? null : children[5].getBoolean(rowId)),\n        Optional.ofNullable(children[6].isNullAt(rowId) ? null : children[6].getString(rowId)),\n        children[7].isNullAt(rowId)\n            ? Collections.emptyMap()\n            : VectorUtils.toJavaMap(children[7].getMap(rowId)));\n  }\n\n  /**\n   * Returns the `inCommitTimestamp` of delta file at the requested version. Throws an exception if\n   * the delta file does not exist or does not have a commitInfo action or if the commitInfo action\n   * contains an empty `inCommitTimestamp`.\n   *\n   * <p><strong>WARNING: UNSAFE METHOD</strong> because this assumes that 00N.json is published.\n   */\n  // TODO: [delta-io/delta#5147] Can't just use the logPath & version on catalogManaged tables.\n  public static long unsafeGetRequiredIctFromPublishedDeltaFile(\n      Engine engine, Path logPath, long version) {\n    return extractRequiredIctFromCommitInfoOpt(\n        unsafeTryReadCommitInfoFromPublishedDeltaFile(engine, logPath, version), version, logPath);\n  }\n\n  /**\n   * Returns the `inCommitTimestamp` of the provided delta file. Throws an exception if the delta\n   * file does not exist or does not have a commitInfo action or if the commitInfo action contains\n   * an empty `inCommitTimestamp`. The delta file can be either a published or staged commit file.\n   */\n  public static long getRequiredIctFromDeltaFile(\n      Engine engine, Path tablePath, FileStatus deltaFileStatus, long version) {\n    checkArgument(\n        FileNames.isCommitFile(deltaFileStatus.getPath()), \"Must provide a valid commit file\");\n    return extractRequiredIctFromCommitInfoOpt(\n        tryReadCommitInfoFromDeltaFile(engine, deltaFileStatus), version, tablePath);\n  }\n\n  /**\n   * Returns the `inCommitTimestamp` of the given `commitInfoOpt` if it is defined. Throws an\n   * exception if `commitInfoOpt` is empty or contains an empty `inCommitTimestamp`.\n   */\n  // TODO: [delta-io/delta#5147] Can't just use the logPath & version on catalogManaged tables.\n  public static long extractRequiredIctFromCommitInfoOpt(\n      Optional<CommitInfo> commitInfoOpt, long version, Path dataPath) {\n    CommitInfo commitInfo =\n        commitInfoOpt.orElseThrow(\n            () -> DeltaErrors.tableWithIctMissingCommitInfo(dataPath.toString(), version));\n    return commitInfo.inCommitTimestamp.orElseThrow(\n        () -> DeltaErrors.tableWithIctMissingIct(dataPath.toString(), version));\n  }\n\n  /**\n   * Get the CommitInfo action (if available) from the delta file at the given logPath and version.\n   *\n   * <p><strong>WARNING: UNSAFE METHOD</strong> because this assumes that 00N.json is published.\n   */\n  // TODO: [delta-io/delta#5147] Can't just use the logPath & version on catalogManaged tables.\n  public static Optional<CommitInfo> unsafeTryReadCommitInfoFromPublishedDeltaFile(\n      Engine engine, Path logPath, long version) {\n    final FileStatus file =\n        FileStatus.of(\n            FileNames.deltaFile(logPath, version), /* path */\n            0, /* size */\n            0 /* modification time */);\n\n    return tryReadCommitInfoFromDeltaFile(engine, file);\n  }\n\n  /** Read the CommitInfo action (if available) from the given delta file. */\n  public static Optional<CommitInfo> tryReadCommitInfoFromDeltaFile(\n      Engine engine, FileStatus deltaFileStatus) {\n    try (CloseableIterator<ColumnarBatch> columnarBatchIter =\n        wrapEngineExceptionThrowsIO(\n            () ->\n                engine\n                    .getJsonHandler()\n                    .readJsonFiles(\n                        singletonCloseableIterator(deltaFileStatus), READ_SCHEMA, Optional.empty()),\n            \"Reading the CommitInfo with schema=%s from delta file %s\",\n            READ_SCHEMA,\n            deltaFileStatus.getPath())) {\n      while (columnarBatchIter.hasNext()) {\n        final ColumnarBatch columnarBatch = columnarBatchIter.next();\n        assert (columnarBatch.getSchema().equals(READ_SCHEMA));\n        final ColumnVector commitInfoVector = columnarBatch.getColumnVector(0);\n        for (int i = 0; i < commitInfoVector.getSize(); i++) {\n          if (!commitInfoVector.isNullAt(i)) {\n            CommitInfo commitInfo = CommitInfo.fromColumnVector(commitInfoVector, i);\n            if (commitInfo != null) {\n              return Optional.of(commitInfo);\n            }\n          }\n        }\n      }\n    } catch (IOException ex) {\n      throw new UncheckedIOException(\"Could not close iterator\", ex);\n    }\n\n    logger.info(\"No CommitInfo found in delta file {}\", deltaFileStatus.getPath());\n    return Optional.empty();\n  }\n\n  //////////////////////////////////\n  // Member variables and methods //\n  //////////////////////////////////\n\n  private final long timestamp;\n  private final Optional<String> engineInfo;\n  private final Optional<String> operation;\n  private final Map<String, String> operationParameters;\n  private final Optional<Boolean> isBlindAppend;\n  private final Optional<String> txnId;\n  private Optional<Long> inCommitTimestamp;\n  private final Map<String, String> operationMetrics;\n\n  public CommitInfo(\n      Optional<Long> inCommitTimestamp,\n      long timestamp,\n      Optional<String> engineInfo,\n      Optional<String> operation,\n      Map<String, String> operationParameters,\n      Optional<Boolean> isBlindAppend,\n      Optional<String> txnId,\n      Map<String, String> operationMetrics) {\n    this.inCommitTimestamp = requireNonNull(inCommitTimestamp);\n    this.timestamp = timestamp;\n    this.engineInfo = requireNonNull(engineInfo);\n    this.operation = requireNonNull(operation);\n    this.operationParameters = Collections.unmodifiableMap(requireNonNull(operationParameters));\n    this.isBlindAppend = requireNonNull(isBlindAppend);\n    this.txnId = requireNonNull(txnId);\n    this.operationMetrics = Collections.unmodifiableMap(requireNonNull(operationMetrics));\n  }\n\n  public long getTimestamp() {\n    return timestamp;\n  }\n\n  public Optional<String> getEngineInfo() {\n    return engineInfo;\n  }\n\n  public Optional<String> getOperation() {\n    return operation;\n  }\n\n  public Map<String, String> getOperationParameters() {\n    return operationParameters;\n  }\n\n  public Optional<Boolean> getIsBlindAppend() {\n    return isBlindAppend;\n  }\n\n  public Optional<String> getTxnId() {\n    return txnId;\n  }\n\n  public Optional<Long> getInCommitTimestamp() {\n    return inCommitTimestamp;\n  }\n\n  public Map<String, String> getOperationMetrics() {\n    return operationMetrics;\n  }\n\n  public void setInCommitTimestamp(Optional<Long> inCommitTimestamp) {\n    this.inCommitTimestamp = inCommitTimestamp;\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link CommitInfo#FULL_SCHEMA}.\n   *\n   * @return {@link Row} object with the schema {@link CommitInfo#FULL_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> commitInfo = new HashMap<>();\n    commitInfo.put(COL_NAME_TO_ORDINAL.get(\"inCommitTimestamp\"), inCommitTimestamp.orElse(null));\n    commitInfo.put(COL_NAME_TO_ORDINAL.get(\"timestamp\"), timestamp);\n    commitInfo.put(COL_NAME_TO_ORDINAL.get(\"engineInfo\"), engineInfo.orElse(null));\n    commitInfo.put(COL_NAME_TO_ORDINAL.get(\"operation\"), operation.orElse(null));\n    commitInfo.put(\n        COL_NAME_TO_ORDINAL.get(\"operationParameters\"), stringStringMapValue(operationParameters));\n    commitInfo.put(COL_NAME_TO_ORDINAL.get(\"isBlindAppend\"), isBlindAppend.orElse(null));\n    commitInfo.put(COL_NAME_TO_ORDINAL.get(\"txnId\"), txnId.orElse(null));\n    commitInfo.put(\n        COL_NAME_TO_ORDINAL.get(\"operationMetrics\"), stringStringMapValue(operationMetrics));\n\n    return new GenericRow(CommitInfo.FULL_SCHEMA, commitInfo);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/DeletionVectorDescriptor.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.InternalUtils.requireNonNull;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.stream.Collectors.toMap;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.deletionvectors.Base85Codec;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.io.ByteArrayOutputStream;\nimport java.io.DataOutputStream;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.*;\nimport java.util.stream.IntStream;\n\n/** Information about a deletion vector attached to a file action. */\npublic class DeletionVectorDescriptor {\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Static Fields / Methods\n  ////////////////////////////////////////////////////////////////////////////////\n  public static DeletionVectorDescriptor fromRow(Row row) {\n    if (row == null) {\n      return null;\n    }\n\n    final String storageType = requireNonNull(row, 0, \"storageType\").getString(0);\n    final String pathOrInlineDv = requireNonNull(row, 1, \"pathOrInlineDv\").getString(1);\n    final Optional<Integer> offset = Optional.ofNullable(row.isNullAt(2) ? null : row.getInt(2));\n    final int sizeInBytes = requireNonNull(row, 3, \"sizeInBytes\").getInt(3);\n    final long cardinality = requireNonNull(row, 4, \"cardinality\").getLong(4);\n\n    return new DeletionVectorDescriptor(\n        storageType, pathOrInlineDv, offset, sizeInBytes, cardinality);\n  }\n\n  public static DeletionVectorDescriptor fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n\n    final String storageType =\n        requireNonNull(vector.getChild(0), rowId, \"storageType\").getString(rowId);\n    final String pathOrInlineDv =\n        requireNonNull(vector.getChild(1), rowId, \"pathOrInlineDv\").getString(rowId);\n    final Optional<Integer> offset =\n        Optional.ofNullable(\n            vector.getChild(2).isNullAt(rowId) ? null : vector.getChild(2).getInt(rowId));\n    final int sizeInBytes = requireNonNull(vector.getChild(3), rowId, \"sizeInBytes\").getInt(rowId);\n    final long cardinality =\n        requireNonNull(vector.getChild(4), rowId, \"cardinality\").getLong(rowId);\n    return new DeletionVectorDescriptor(\n        storageType, pathOrInlineDv, offset, sizeInBytes, cardinality);\n  }\n\n  // Markers to separate different kinds of DV storage.\n  public static final String PATH_DV_MARKER = \"p\";\n  public static final String INLINE_DV_MARKER = \"i\";\n  public static final String UUID_DV_MARKER = \"u\";\n\n  public static final StructType READ_SCHEMA =\n      new StructType()\n          .add(\"storageType\", StringType.STRING, false /* nullable*/)\n          .add(\"pathOrInlineDv\", StringType.STRING, false /* nullable*/)\n          .add(\"offset\", IntegerType.INTEGER, true /* nullable*/)\n          .add(\"sizeInBytes\", IntegerType.INTEGER, false /* nullable*/)\n          .add(\"cardinality\", LongType.LONG, false /* nullable*/);\n\n  private static final Map<String, Integer> COL_NAME_TO_ORDINAL =\n      IntStream.range(0, READ_SCHEMA.length())\n          .boxed()\n          .collect(toMap(i -> READ_SCHEMA.at(i).getName(), i -> i));\n\n  /** String that is used in all file names generated by deletion vector store */\n  private static final String DELETION_VECTOR_FILE_NAME_CORE = \"deletion_vector\";\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Instance Fields / Methods\n  ////////////////////////////////////////////////////////////////////////////////\n\n  /** Indicates how the DV is stored. Should be a single letter (see [[pathOrInlineDv]] below.) */\n  private final String storageType;\n\n  /**\n   * Contains the actual data that allows accessing the DV.\n   *\n   * <p>Three options are currently supported: - `storageType=\"u\"` format: `<random prefix -\n   * optional><base85 encoded uuid>` The deletion vector is stored in a file with a path relative to\n   * the data directory of this Delta Table, and the file name can be reconstructed from the UUID.\n   * The encoded UUID is always exactly 20 characters, so the random prefix length can be determined\n   * any characters exceeding 20. - `storageType=\"i\"` format: `<base85 encoded bytes>` The deletion\n   * vector is stored inline in the log. - `storageType=\"p\"` format: `<absolute path>` The DV is\n   * stored in a file with an absolute path given by this url.\n   */\n  private final String pathOrInlineDv;\n\n  /**\n   * Start of the data for this DV in number of bytes from the beginning of the file it is stored\n   * in.\n   *\n   * <p>Always None when storageType = \"i\".\n   */\n  private final Optional<Integer> offset;\n\n  /** Size of the serialized DV in bytes (raw data size, i.e. before base85 encoding). */\n  private final int sizeInBytes;\n\n  /** Number of rows the DV logically removes from the file. */\n  private final long cardinality;\n\n  public DeletionVectorDescriptor(\n      String storageType,\n      String pathOrInlineDv,\n      Optional<Integer> offset,\n      int sizeInBytes,\n      long cardinality) {\n    this.storageType = storageType;\n    this.pathOrInlineDv = pathOrInlineDv;\n    this.offset = offset;\n    this.sizeInBytes = sizeInBytes;\n    this.cardinality = cardinality;\n  }\n\n  public String getStorageType() {\n    return storageType;\n  }\n\n  public String getPathOrInlineDv() {\n    return pathOrInlineDv;\n  }\n\n  public Optional<Integer> getOffset() {\n    return offset;\n  }\n\n  public int getSizeInBytes() {\n    return sizeInBytes;\n  }\n\n  public long getCardinality() {\n    return cardinality;\n  }\n\n  public String getUniqueId() {\n    String uniqueFileId = storageType + pathOrInlineDv;\n    if (offset.isPresent()) {\n      return uniqueFileId + \"@\" + offset;\n    } else {\n      return uniqueFileId;\n    }\n  }\n\n  /**\n   * Serialize this DV descriptor to a base64 encoded string.\n   *\n   * <p>Format is compatible with Spark's DeletionVectorDescriptor.serializeToBase64().\n   */\n  public String serializeToBase64() {\n    try (ByteArrayOutputStream bs = new ByteArrayOutputStream();\n        DataOutputStream ds = new DataOutputStream(bs)) {\n      ds.writeLong(cardinality);\n      ds.writeInt(sizeInBytes);\n\n      byte[] storageTypeBytes = storageType.getBytes();\n      checkArgument(storageTypeBytes.length == 1, \"Storage type must be 1 byte: \" + storageType);\n      ds.writeByte(storageTypeBytes[0]);\n\n      // Inline DVs (storageType=\"i\") have no offset\n      if (!storageType.equals(INLINE_DV_MARKER)) {\n        checkArgument(offset.isPresent(), \"Non-inline DV must have offset\");\n        ds.writeInt(offset.get());\n      }\n\n      ds.writeUTF(pathOrInlineDv);\n      return Base64.getEncoder().encodeToString(bs.toByteArray());\n    } catch (IOException e) {\n      throw new KernelException(\"Failed to serialize DeletionVectorDescriptor\", e);\n    }\n  }\n\n  public boolean isInline() {\n    return INLINE_DV_MARKER.equals(storageType);\n  }\n\n  public boolean isOnDisk() {\n    return !isInline();\n  }\n\n  public byte[] inlineData() {\n    checkArgument(isInline(), \"Can't get data for an on-disk DV from the log.\");\n    // The sizeInBytes is used to remove any padding that might have been added during encoding.\n    return Base85Codec.decodeBytes(pathOrInlineDv, sizeInBytes);\n  }\n\n  public String getAbsolutePath(String tableLocation) {\n    checkArgument(isOnDisk(), \"Can't get a path for an inline deletion vector\");\n    if (storageType.equals(UUID_DV_MARKER)) {\n      // If the file was written with a random prefix, we have to extract that,\n      // before decoding the UUID.\n      int randomPrefixLength = pathOrInlineDv.length() - Base85Codec.ENCODED_UUID_LENGTH;\n      String randomPrefix = pathOrInlineDv.substring(0, randomPrefixLength);\n      String encodedUuid = pathOrInlineDv.substring(randomPrefixLength);\n      UUID uuid = Base85Codec.decodeUUID(encodedUuid);\n      return assembleDeletionVectorPath(tableLocation, uuid, randomPrefix).toString();\n    } else if (storageType.equals(PATH_DV_MARKER)) {\n      // Since there is no need for legacy support for relative paths for DVs,\n      // relative DVs should *always* use the UUID variant.\n      try {\n        URI parsedUri = new URI(pathOrInlineDv);\n        checkArgument(parsedUri.isAbsolute(), \"Relative URIs are not supported for DVs\");\n        return new Path(parsedUri).toString();\n      } catch (URISyntaxException e) {\n        throw new RuntimeException(\"Couldn't parse uri:\\n\" + e);\n      }\n    } else {\n      throw new RuntimeException(\n          \"A uri \"\n              + pathOrInlineDv\n              + \" which cannot be turned into a relative path as found in the transaction log\");\n    }\n  }\n\n  /**\n   * Return the unique path under `parentPath` that is based on `id`.\n   *\n   * <p>Optionally, prepend a `prefix` to the name.\n   */\n  private Path assembleDeletionVectorPath(String targetParentPath, UUID id, String prefix) {\n    String fileName = String.format(\"%s_%s.bin\", DELETION_VECTOR_FILE_NAME_CORE, id.toString());\n    if (prefix.length() > 0) {\n      return new Path(new Path(targetParentPath, prefix), fileName);\n    } else {\n      return new Path(targetParentPath, fileName);\n    }\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"DeletionVectorDescriptor(storageType=%s, pathOrInlineDv=%s, offset=%s, \"\n            + \"sizeInBytes=%s, cardinality=%s)\",\n        storageType, pathOrInlineDv, offset, sizeInBytes, cardinality);\n  }\n\n  /** @return Row representation of this deletion vector descriptor */\n  public Row toRow() {\n    Map<Integer, Object> fieldMap = new HashMap<>();\n\n    fieldMap.put(COL_NAME_TO_ORDINAL.get(\"storageType\"), storageType);\n    fieldMap.put(COL_NAME_TO_ORDINAL.get(\"pathOrInlineDv\"), pathOrInlineDv);\n\n    // Only add offset if it's present\n    if (offset.isPresent()) {\n      fieldMap.put(COL_NAME_TO_ORDINAL.get(\"offset\"), offset.get());\n    }\n    // If offset is not present, the field remains null in the map\n\n    fieldMap.put(COL_NAME_TO_ORDINAL.get(\"sizeInBytes\"), sizeInBytes);\n    fieldMap.put(COL_NAME_TO_ORDINAL.get(\"cardinality\"), cardinality);\n\n    return new GenericRow(READ_SCHEMA, fieldMap);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == this) {\n      return true;\n    }\n    if (!(o instanceof DeletionVectorDescriptor)) {\n      return false;\n    }\n    DeletionVectorDescriptor dv = (DeletionVectorDescriptor) o;\n    return Objects.equals(storageType, dv.storageType)\n        && Objects.equals(pathOrInlineDv, dv.pathOrInlineDv)\n        && Objects.equals(offset, dv.offset)\n        && this.sizeInBytes == dv.sizeInBytes\n        && this.cardinality == dv.cardinality;\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(storageType, pathOrInlineDv, offset, sizeInBytes, cardinality);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/DomainMetadata.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.InternalUtils.requireNonNull;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.rowtracking.RowTrackingMetadataDomain;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\n\n/** Delta log action representing an `DomainMetadata` action */\npublic class DomainMetadata {\n\n  private static final Set<String> SUPPORTED_SYSTEM_DOMAINS =\n      Collections.unmodifiableSet(\n          new HashSet<>(Collections.singletonList(RowTrackingMetadataDomain.DOMAIN_NAME)));\n\n  /** Whether the provided {@code domain} is a user-controlled domain */\n  public static boolean isUserControlledDomain(String domain) {\n    // Domain identifiers are case-sensitive, but we don't want to allow users to set domains\n    // with prefixes like `DELTA.` either, so perform case-insensitive check for this purpose\n    return !domain.toLowerCase(Locale.ROOT).startsWith(\"delta.\");\n  }\n\n  /**\n   * Checks whether the provided {@code domain} is a system domain that is supported for set in a\n   * Delta transaction via addDomainMetadata.\n   *\n   * <p>By default, system domains are not allowed to be set through transaction-level domain\n   * metadata due to their reserved nature. However, there are specific system domains—such as\n   * {@link RowTrackingMetadataDomain#DOMAIN_NAME}—that are explicitly allowed to be set in this\n   * context. This method defines the allowlist of such supported domains and checks against it.\n   */\n  public static boolean isSystemDomainSupportedSetFromTxn(String domain) {\n    return SUPPORTED_SYSTEM_DOMAINS.contains(domain);\n  }\n\n  /** Full schema of the {@link DomainMetadata} action in the Delta Log. */\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"domain\", StringType.STRING, false /* nullable */)\n          .add(\"configuration\", StringType.STRING, false /* nullable */)\n          .add(\"removed\", BooleanType.BOOLEAN, false /* nullable */);\n\n  public static DomainMetadata fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n    return new DomainMetadata(\n        requireNonNull(vector.getChild(0), rowId, \"domain\").getString(rowId),\n        requireNonNull(vector.getChild(1), rowId, \"configuration\").getString(rowId),\n        requireNonNull(vector.getChild(2), rowId, \"removed\").getBoolean(rowId));\n  }\n\n  /**\n   * Creates a {@link DomainMetadata} instance from a Row with the schema being {@link\n   * DomainMetadata#FULL_SCHEMA}.\n   *\n   * @param row the Row object containing the DomainMetadata action\n   * @return a DomainMetadata instance or null if the row is null\n   * @throws IllegalArgumentException if the schema of the row does not match {@link\n   *     DomainMetadata#FULL_SCHEMA}\n   */\n  public static DomainMetadata fromRow(Row row) {\n    if (row == null) {\n      return null;\n    }\n    checkArgument(\n        row.getSchema().equals(FULL_SCHEMA),\n        \"Expected schema: %s, found: %s\",\n        FULL_SCHEMA,\n        row.getSchema());\n    return new DomainMetadata(\n        requireNonNull(row, 0, \"domain\").getString(0),\n        requireNonNull(row, 1, \"configuration\").getString(1),\n        requireNonNull(row, 2, \"removed\").getBoolean(2));\n  }\n\n  private final String domain;\n  private final String configuration;\n  private final boolean removed;\n\n  /**\n   * The domain metadata action contains a configuration string for a named metadata domain. Two\n   * overlapping transactions conflict if they both contain a domain metadata action for the same\n   * metadata domain. Per-domain conflict resolution logic can be implemented.\n   *\n   * @param domain A string used to identify a specific domain.\n   * @param configuration A string containing configuration for the metadata domain.\n   * @param removed If it is true it serves as a tombstone to logically delete a {@link\n   *     DomainMetadata} action.\n   */\n  public DomainMetadata(String domain, String configuration, boolean removed) {\n    this.domain = requireNonNull(domain, \"domain is null\");\n    this.configuration = requireNonNull(configuration, \"configuration is null\");\n    this.removed = removed;\n  }\n\n  public String getDomain() {\n    return domain;\n  }\n\n  public String getConfiguration() {\n    return configuration;\n  }\n\n  public boolean isRemoved() {\n    return removed;\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link DomainMetadata#FULL_SCHEMA}.\n   *\n   * @return {@link Row} object with the schema {@link DomainMetadata#FULL_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> domainMetadataMap = new HashMap<>();\n    domainMetadataMap.put(0, domain);\n    domainMetadataMap.put(1, configuration);\n    domainMetadataMap.put(2, removed);\n\n    return new GenericRow(DomainMetadata.FULL_SCHEMA, domainMetadataMap);\n  }\n\n  public DomainMetadata removed() {\n    checkArgument(!removed, \"Cannot remove a domain metadata tombstone (already removed)\");\n    return new DomainMetadata(domain, configuration, true /* removed */);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"DomainMetadata{domain='%s', configuration='%s', removed='%b'}\",\n        domain, configuration, removed);\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (this == obj) return true;\n    if (obj == null || getClass() != obj.getClass()) return false;\n    DomainMetadata that = (DomainMetadata) obj;\n    return removed == that.removed\n        && domain.equals(that.domain)\n        && configuration.equals(that.configuration);\n  }\n\n  @Override\n  public int hashCode() {\n    return java.util.Objects.hash(domain, configuration, removed);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/Format.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.InternalUtils.requireNonNull;\nimport static io.delta.kernel.internal.util.VectorUtils.*;\nimport static java.util.Collections.emptyMap;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.types.*;\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class Format implements Serializable {\n  private static final long serialVersionUID = 1L;\n\n  public static Format fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n    final String provider = requireNonNull(vector.getChild(0), rowId, \"provider\").getString(rowId);\n    final Map<String, String> options =\n        vector.getChild(1).isNullAt(rowId)\n            ? Collections.emptyMap()\n            : toJavaMap(vector.getChild(1).getMap(rowId));\n    return new Format(provider, options);\n  }\n\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"provider\", StringType.STRING, false /* nullable */)\n          .add(\n              \"options\",\n              new MapType(StringType.STRING, StringType.STRING, false),\n              true /* nullable */);\n\n  private final String provider;\n  private final Map<String, String> options;\n\n  public Format(String provider, Map<String, String> options) {\n    this.provider = provider;\n    this.options = options;\n  }\n\n  public Format() {\n    this.provider = \"parquet\";\n    this.options = emptyMap();\n  }\n\n  public String getProvider() {\n    return provider;\n  }\n\n  public Map<String, String> getOptions() {\n    return Collections.unmodifiableMap(options);\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link Format#FULL_SCHEMA}.\n   *\n   * @return {@link Row} object with the schema {@link Format#FULL_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> formatMap = new HashMap<>();\n    formatMap.put(0, provider);\n    formatMap.put(1, stringStringMapValue(options));\n\n    return new GenericRow(Format.FULL_SCHEMA, formatMap);\n  }\n\n  @Override\n  public String toString() {\n    return \"Format{\" + \"provider='\" + provider + '\\'' + \", options=\" + options + '}';\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    Format format = (Format) o;\n    return provider.equals(format.provider) && options.equals(format.options);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(provider, options);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/GenerateIcebergCompatActionUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.InternalUtils.relativizePath;\nimport static io.delta.kernel.internal.util.PartitionUtils.serializePartitionMap;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.data.TransactionStateRow;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatV2MetadataValidatorAndUpdater;\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatV3MetadataValidatorAndUpdater;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\n/** Utilities to convert Iceberg add/removes to Delta Kernel add/removes */\npublic final class GenerateIcebergCompatActionUtils {\n\n  /**\n   * Create an add action {@link Row} that can be passed to {@link Transaction#commit(Engine,\n   * CloseableIterable)} from an Iceberg add.\n   *\n   * @param transactionState the transaction state from the built transaction\n   * @param fileStatus the file status to create the add with (contains path, time, size, and stats)\n   * @param partitionValues the partition values for the add\n   * @param dataChange whether or not the add constitutes a dataChange (i.e. append vs. compaction)\n   * @param tags key-value metadata to be attached to the add action\n   * @param physicalSchemaOpt An optional pre-parsed physical schema. Improves performance for batch\n   *     operations by avoiding repeated JSON parsing. Recommended when generating many actions with\n   *     the same schema.\n   * @return add action row that can be included in the transaction\n   * @throws UnsupportedOperationException if icebergWriterCompatV1 is not enabled\n   * @throws UnsupportedOperationException if maxRetries != 0 in the transaction\n   * @throws KernelException if stats are not present (required for icebergCompatV2)\n   * @throws UnsupportedOperationException if the table is partitioned (currently unsupported)\n   */\n  public static Row generateIcebergCompatWriterV1AddAction(\n      Row transactionState,\n      DataFileStatus fileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Map<String, String> tags,\n      Optional<StructType> physicalSchemaOpt) {\n    Map<String, String> configuration = TransactionStateRow.getConfiguration(transactionState);\n\n    /* ----- Validate that this is a valid usage of this API ----- */\n    validateIcebergWriterCompatV1Enabled(configuration);\n    validateMaxRetriesSetToZero(transactionState);\n\n    /* ----- Validate this is valid write given the table's protocol & configurations ----- */\n    checkState(\n        TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(configuration),\n        \"icebergCompatV2 not enabled despite icebergWriterCompatV1 enabled\");\n    // We require field `numRecords` when icebergCompatV2 is enabled\n    IcebergCompatV2MetadataValidatorAndUpdater.validateDataFileStatus(fileStatus);\n\n    /* --- Validate and update partitionValues ---- */\n    // Currently we don't support partitioned tables; fail here\n    blockPartitionedTables(transactionState, partitionValues);\n\n    URI tableRoot = new Path(TransactionStateRow.getTablePath(transactionState)).toUri();\n    // This takes care of relativizing the file path and serializing the file statistics\n    AddFile addFile =\n        AddFile.convertDataFileStatus(\n            physicalSchemaOpt.orElseGet(\n                () -> TransactionStateRow.getPhysicalSchema(transactionState)),\n            tableRoot,\n            fileStatus,\n            partitionValues,\n            dataChange,\n            tags,\n            Optional.empty() /* baseRowId */,\n            Optional.empty() /* defaultRowCommitVersion */,\n            Optional.empty() /* deletionVectorDescriptor */);\n    return SingleAction.createAddFileSingleAction(addFile.toRow());\n  }\n\n  public static Row generateIcebergCompatWriterV1AddAction(\n      Row transactionState,\n      DataFileStatus fileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Optional<StructType> physicalSchemaOpt) {\n    return generateIcebergCompatWriterV1AddAction(\n        transactionState,\n        fileStatus,\n        partitionValues,\n        dataChange,\n        Collections.emptyMap(),\n        physicalSchemaOpt);\n  }\n\n  /**\n   * Create an add action {@link Row} that can be passed to {@link Transaction#commit(Engine,\n   * CloseableIterable)} from an Iceberg add.\n   *\n   * @param transactionState the transaction state from the built transaction\n   * @param fileStatus the file status to create the add with (contains path, time, size, and stats)\n   * @param partitionValues the partition values for the add\n   * @param dataChange whether or not the add constitutes a dataChange (i.e. append vs. compaction)\n   * @param tags key-value metadata to be attached to the add action\n   * @param deletionVectorDescriptor optional deletion vector descriptor for the add action\n   * @param physicalSchemaOpt An optional pre-parsed physical schema. Improves performance for batch\n   *     operations by avoiding repeated JSON parsing. Recommended when generating many actions with\n   *     the same schema.\n   * @return add action row that can be included in the transaction\n   * @throws UnsupportedOperationException if icebergWriterCompatV3 is not enabled\n   * @throws UnsupportedOperationException if maxRetries != 0 in the transaction\n   * @throws KernelException if stats are not present (required for icebergCompatV3)\n   * @throws UnsupportedOperationException if the table is partitioned (currently unsupported)\n   */\n  public static Row generateIcebergCompatWriterV3AddAction(\n      Row transactionState,\n      DataFileStatus fileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Map<String, String> tags,\n      Optional<Long> baseRowId,\n      Optional<Long> defaultRowCommitVersion,\n      Optional<DeletionVectorDescriptor> deletionVectorDescriptor,\n      Optional<StructType> physicalSchemaOpt) {\n    Map<String, String> configuration = TransactionStateRow.getConfiguration(transactionState);\n\n    /* ----- Validate that this is a valid usage of this API ----- */\n    validateIcebergWriterCompatV3Enabled(configuration);\n    validateMaxRetriesSetToZero(transactionState);\n\n    /* ----- Validate that deletion vector is passed in only when the table supports it ----- */\n    deletionVectorDescriptor.ifPresent(dv -> validateIcebergDeletionVectorsEnabled(configuration));\n\n    /* ----- Validate this is valid write given the table's protocol & configurations ----- */\n    checkState(\n        TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(configuration),\n        \"icebergCompatV3 not enabled despite icebergWriterCompatV3 enabled\");\n    // We require field `numRecords` when icebergCompatV3 is enabled\n    IcebergCompatV3MetadataValidatorAndUpdater.validateDataFileStatus(fileStatus);\n\n    /* --- Validate and update partitionValues ---- */\n    // Currently we don't support partitioned tables; fail here\n    blockPartitionedTables(transactionState, partitionValues);\n\n    URI tableRoot = new Path(TransactionStateRow.getTablePath(transactionState)).toUri();\n    // This takes care of relativizing the file path and serializing the file statistics\n    AddFile addFile =\n        AddFile.convertDataFileStatus(\n            physicalSchemaOpt.orElseGet(\n                () -> TransactionStateRow.getPhysicalSchema(transactionState)),\n            tableRoot,\n            fileStatus,\n            partitionValues,\n            dataChange,\n            tags,\n            baseRowId,\n            defaultRowCommitVersion,\n            deletionVectorDescriptor);\n    return SingleAction.createAddFileSingleAction(addFile.toRow());\n  }\n\n  /**\n   * Create a remove action {@link Row} that can be passed to {@link Transaction#commit(Engine,\n   * CloseableIterable)} from an Iceberg remove.\n   *\n   * @param transactionState the transaction state from the built transaction\n   * @param fileStatus the file status to create the remove with (contains path, time, size, and\n   *     stats)\n   * @param partitionValues the partition values for the remove\n   * @param dataChange whether or not the remove constitutes a dataChange (i.e. delete vs.\n   *     compaction)\n   * @param physicalSchemaOpt An optional pre-parsed physical schema. Improves performance for batch\n   *     operations by avoiding repeated JSON parsing. Recommended when generating many actions with\n   *     the same schema.\n   * @return remove action row that can be committed to the transaction\n   * @throws UnsupportedOperationException if icebergWriterCompatV1 is not enabled\n   * @throws UnsupportedOperationException if maxRetries != 0 in the transaction\n   * @throws KernelException if the table is an append-only table and dataChange=true\n   * @throws UnsupportedOperationException if the table is partitioned (currently unsupported)\n   */\n  public static Row generateIcebergCompatWriterV1RemoveAction(\n      Row transactionState,\n      DataFileStatus fileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Optional<StructType> physicalSchemaOpt) {\n    Map<String, String> config = TransactionStateRow.getConfiguration(transactionState);\n\n    /* ----- Validate that this is a valid usage of this API ----- */\n    validateIcebergWriterCompatV1Enabled(config);\n    validateMaxRetriesSetToZero(transactionState);\n\n    /* ----- Validate this is valid write given the table's protocol & configurations ----- */\n    // We only allow removes with dataChange=false when appendOnly=true\n    blockUpdatingAppendOnlyTables(dataChange, transactionState, config);\n\n    /* --- Validate and update partitionValues ---- */\n    // Currently we don't support partitioned tables; fail here\n    blockPartitionedTables(transactionState, partitionValues);\n\n    URI tableRoot = new Path(TransactionStateRow.getTablePath(transactionState)).toUri();\n    // This takes care of relativizing the file path and serializing the file statistics\n    Row removeFileRow =\n        convertRemoveDataFileStatus(\n            physicalSchemaOpt.orElseGet(\n                () -> TransactionStateRow.getPhysicalSchema(transactionState)),\n            tableRoot,\n            fileStatus,\n            partitionValues,\n            dataChange,\n            Optional.empty() /* baseRowId */,\n            Optional.empty() /* defaultRowCommitVersion */,\n            Optional.empty() /* deletionVectorDescriptor */);\n    return SingleAction.createRemoveFileSingleAction(removeFileRow);\n  }\n\n  /**\n   * Create a remove action {@link Row} that can be passed to {@link Transaction#commit(Engine,\n   * CloseableIterable)} from an Iceberg remove.\n   *\n   * @param transactionState the transaction state from the built transaction\n   * @param fileStatus the file status to create the remove with (contains path, time, size, and\n   *     stats)\n   * @param partitionValues the partition values for the remove\n   * @param dataChange whether or not the remove constitutes a dataChange (i.e. delete vs.\n   *     compaction)\n   * @param deletionVectorDescriptor optional deletion vector descriptor for the add action\n   * @param physicalSchemaOpt An optional pre-parsed physical schema. Improves performance for batch\n   *     operations by avoiding repeated JSON parsing. Recommended when generating many actions with\n   *     the same schema.\n   * @return remove action row that can be committed to the transaction\n   * @throws UnsupportedOperationException if icebergWriterCompatV3 is not enabled\n   * @throws UnsupportedOperationException if maxRetries != 0 in the transaction\n   * @throws KernelException if the table is an append-only table and dataChange=true\n   * @throws UnsupportedOperationException if the table is partitioned (currently unsupported)\n   */\n  public static Row generateIcebergCompatWriterV3RemoveAction(\n      Row transactionState,\n      DataFileStatus fileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Optional<Long> baseRowId,\n      Optional<Long> defaultRowCommitVersion,\n      Optional<DeletionVectorDescriptor> deletionVectorDescriptor,\n      Optional<StructType> physicalSchemaOpt) {\n    Map<String, String> config = TransactionStateRow.getConfiguration(transactionState);\n\n    /* ----- Validate that this is a valid usage of this API ----- */\n    validateIcebergWriterCompatV3Enabled(config);\n    validateMaxRetriesSetToZero(transactionState);\n\n    /* ----- Validate that deletion vector is passed in only when the table supports it ----- */\n    deletionVectorDescriptor.ifPresent(dv -> validateIcebergDeletionVectorsEnabled(config));\n\n    /* ----- Validate this is valid write given the table's protocol & configurations ----- */\n    // We only allow removes with dataChange=false when appendOnly=true\n    if (dataChange && TableConfig.APPEND_ONLY_ENABLED.fromMetadata(config)) {\n      throw DeltaErrors.cannotModifyAppendOnlyTable(\n          TransactionStateRow.getTablePath(transactionState));\n    }\n\n    /* ----- Validate this is valid write given the table's protocol & configurations ----- */\n    // We only allow removes with dataChange=false when appendOnly=true\n    blockUpdatingAppendOnlyTables(dataChange, transactionState, config);\n\n    /* --- Validate and update partitionValues ---- */\n    // Currently we don't support partitioned tables; fail here\n    blockPartitionedTables(transactionState, partitionValues);\n\n    URI tableRoot = new Path(TransactionStateRow.getTablePath(transactionState)).toUri();\n    // This takes care of relativizing the file path and serializing the file statistics\n    Row removeFileRow =\n        convertRemoveDataFileStatus(\n            physicalSchemaOpt.orElseGet(\n                () -> TransactionStateRow.getPhysicalSchema(transactionState)),\n            tableRoot,\n            fileStatus,\n            partitionValues,\n            dataChange,\n            baseRowId,\n            defaultRowCommitVersion,\n            deletionVectorDescriptor);\n    return SingleAction.createRemoveFileSingleAction(removeFileRow);\n  }\n\n  /////////////////////\n  // Private helpers //\n  /////////////////////\n\n  /**\n   * Validates that table feature `icebergWriterCompatV1` is enabled. We restrict usage of these\n   * APIs to require that this table feature is enabled to prevent any unsafe usage due to the table\n   * features that are blocked via `icebergWriterCompatV1` (for example, rowTracking or\n   * deletionVectors).\n   */\n  private static void validateIcebergWriterCompatV1Enabled(Map<String, String> config) {\n    if (!TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.fromMetadata(config)) {\n      throw new UnsupportedOperationException(\n          String.format(\n              \"APIs within GenerateIcebergCompatActionUtils are only supported on tables with\"\n                  + \" '%s' set to true\",\n              TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey()));\n    }\n  }\n\n  /**\n   * Validates that table feature `icebergWriterCompatV3` is enabled. We restrict usage of these\n   * APIs to require that this table feature is enabled to prevent any unsafe usage due to the table\n   * features that are blocked via `icebergWriterCompatV3`.\n   */\n  private static void validateIcebergWriterCompatV3Enabled(Map<String, String> config) {\n    if (!TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.fromMetadata(config)) {\n      throw new UnsupportedOperationException(\n          String.format(\n              \"APIs within GenerateIcebergCompatActionUtils are only supported on tables with\"\n                  + \" '%s' set to true\",\n              TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.getKey()));\n    }\n  }\n\n  /**\n   * Validates that table feature `deletion vectors` is enabled. Checked when a deletion vector\n   * descriptor is passed to generateIcebergCompatWriterV3AddAction.\n   */\n  private static void validateIcebergDeletionVectorsEnabled(Map<String, String> config) {\n    if (!TableConfig.DELETION_VECTORS_CREATION_ENABLED.fromMetadata(config)) {\n      throw new UnsupportedOperationException(\n          String.format(\n              \"APIs within GenerateIcebergCompatActionUtils are only supported on tables with\"\n                  + \" '%s' set to true\",\n              TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey()));\n    }\n  }\n\n  /**\n   * Throws an exception if `maxRetries` was not set to 0 in the transaction. We restrict these APIs\n   * to require `maxRetries = 0` since conflict resolution is not supported for operations other\n   * than blind appends.\n   */\n  private static void validateMaxRetriesSetToZero(Row transactionState) {\n    if (TransactionStateRow.getMaxRetries(transactionState) > 0) {\n      throw new UnsupportedOperationException(\n          String.format(\n              \"Usage of GenerateIcebergCompatActionUtils requires maxRetries=0, \"\n                  + \"found maxRetries=%s\",\n              TransactionStateRow.getMaxRetries(transactionState)));\n    }\n  }\n\n  private static void blockUpdatingAppendOnlyTables(\n      boolean dataChange, Row transactionState, Map<String, String> config) {\n    // We only allow removes with dataChange=false when appendOnly=true\n    if (dataChange && TableConfig.APPEND_ONLY_ENABLED.fromMetadata(config)) {\n      throw DeltaErrors.cannotModifyAppendOnlyTable(\n          TransactionStateRow.getTablePath(transactionState));\n    }\n  }\n\n  private static void blockPartitionedTables(\n      Row transactionState, Map<String, Literal> partitionValues) {\n    if (!TransactionStateRow.getPartitionColumnsList(transactionState).isEmpty()) {\n      throw new UnsupportedOperationException(\n          \"Currently GenerateIcebergCompatActionUtils \"\n              + \"is not supported for partitioned tables\");\n    }\n    checkArgument(\n        partitionValues.isEmpty(), \"Non-empty partitionValues provided for an unpartitioned table\");\n  }\n\n  //////////////////////////////////////////////////\n  // Private methods for creating RemoveFile rows //\n  //////////////////////////////////////////////////\n  // I've added these APIs here since they rely on the assumptions validated within\n  // GenerateIcebergCompatActionUtils such as icebergWriterCompatV1 is enabled --> rowTracking is\n  // disabled. Since these APIs are not valid without these assumptions, holding off on putting them\n  // within RemoveFile.java until we add full support for deletes (which will likely involve\n  // generating RemoveFiles directly from AddFiles anyway)\n\n  @VisibleForTesting\n  public static Row convertRemoveDataFileStatus(\n      StructType physicalSchema,\n      URI tableRoot,\n      DataFileStatus dataFileStatus,\n      Map<String, Literal> partitionValues,\n      boolean dataChange,\n      Optional<Long> baseRowId,\n      Optional<Long> defaultRowCommitVersion,\n      Optional<DeletionVectorDescriptor> deletionVectorDescriptor) {\n    return createRemoveFileRowWithExtendedFileMetadata(\n        relativizePath(new Path(dataFileStatus.getPath()), tableRoot).toUri().toString(),\n        dataFileStatus.getModificationTime(),\n        dataChange,\n        serializePartitionMap(partitionValues),\n        dataFileStatus.getSize(),\n        dataFileStatus.getStatistics(),\n        physicalSchema,\n        baseRowId,\n        defaultRowCommitVersion,\n        deletionVectorDescriptor);\n  }\n\n  @VisibleForTesting\n  public static Row createRemoveFileRowWithExtendedFileMetadata(\n      String path,\n      long deletionTimestamp,\n      boolean dataChange,\n      MapValue partitionValues,\n      long size,\n      Optional<DataFileStatistics> stats,\n      StructType physicalSchema,\n      Optional<Long> baseRowId,\n      Optional<Long> defaultRowCommitVersion,\n      Optional<DeletionVectorDescriptor> deletionVector) {\n    Map<Integer, Object> fieldMap = new HashMap<>();\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"path\"), requireNonNull(path));\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"deletionTimestamp\"), deletionTimestamp);\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"dataChange\"), dataChange);\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"extendedFileMetadata\"), true);\n    fieldMap.put(\n        RemoveFile.FULL_SCHEMA.indexOf(\"partitionValues\"), requireNonNull(partitionValues));\n    fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"size\"), size);\n    stats.ifPresent(\n        stat ->\n            fieldMap.put(\n                RemoveFile.FULL_SCHEMA.indexOf(\"stats\"), stat.serializeAsJson(physicalSchema)));\n    baseRowId.ifPresent(id -> fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"baseRowId\"), id));\n    defaultRowCommitVersion.ifPresent(\n        version ->\n            fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"defaultRowCommitVersion\"), version));\n    deletionVector.ifPresent(\n        dv -> {\n          Row dvRow = dv.toRow();\n          fieldMap.put(RemoveFile.FULL_SCHEMA.indexOf(\"deletionVector\"), dvRow);\n        });\n    return new GenericRow(RemoveFile.FULL_SCHEMA, fieldMap);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/Metadata.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.InternalUtils.requireNonNull;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.*;\nimport java.io.InvalidObjectException;\nimport java.io.ObjectInputStream;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\n\npublic class Metadata implements Serializable {\n  private static final long serialVersionUID = 1L;\n\n  public static Metadata fromRow(Row row) {\n    requireNonNull(row);\n    checkArgument(FULL_SCHEMA.equals(row.getSchema()));\n    return fromColumnVector(\n        VectorUtils.buildColumnVector(Collections.singletonList(row), FULL_SCHEMA), /* rowId */ 0);\n  }\n\n  public static Metadata fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n\n    final String schemaJson =\n        requireNonNull(vector.getChild(4), rowId, \"schemaString\").getString(rowId);\n    Lazy<StructType> lazySchema =\n        new Lazy<>(() -> DataTypeJsonSerDe.deserializeStructType(schemaJson));\n\n    return new Metadata(\n        requireNonNull(vector.getChild(0), rowId, \"id\").getString(rowId),\n        Optional.ofNullable(\n            vector.getChild(1).isNullAt(rowId) ? null : vector.getChild(1).getString(rowId)),\n        Optional.ofNullable(\n            vector.getChild(2).isNullAt(rowId) ? null : vector.getChild(2).getString(rowId)),\n        Format.fromColumnVector(requireNonNull(vector.getChild(3), rowId, \"format\"), rowId),\n        schemaJson,\n        lazySchema,\n        vector.getChild(5).getArray(rowId),\n        Optional.ofNullable(\n            vector.getChild(6).isNullAt(rowId) ? null : vector.getChild(6).getLong(rowId)),\n        vector.getChild(7).getMap(rowId));\n  }\n\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"id\", StringType.STRING, false /* nullable */)\n          .add(\"name\", StringType.STRING, true /* nullable */)\n          .add(\"description\", StringType.STRING, true /* nullable */)\n          .add(\"format\", Format.FULL_SCHEMA, false /* nullable */)\n          .add(\"schemaString\", StringType.STRING, false /* nullable */)\n          .add(\n              \"partitionColumns\",\n              new ArrayType(StringType.STRING, false /* contains null */),\n              false /* nullable */)\n          .add(\"createdTime\", LongType.LONG, true /* contains null */)\n          .add(\n              \"configuration\",\n              new MapType(StringType.STRING, StringType.STRING, false),\n              false /* nullable */);\n\n  private final String id;\n  private final Optional<String> name;\n  private final Optional<String> description;\n  private final Format format;\n  private final String schemaString;\n  private final Lazy<StructType> schema;\n  private final ArrayValue partitionColumns;\n  private final Optional<Long> createdTime;\n  private final MapValue configurationMapValue;\n  private final Lazy<Map<String, String>> configuration;\n  // Partition column names in lower case.\n  private final Lazy<Set<String>> partitionColNames;\n  // Logical data schema excluding partition columns\n  private final Lazy<StructType> dataSchema;\n\n  public Metadata(\n      String id,\n      Optional<String> name,\n      Optional<String> description,\n      Format format,\n      String schemaString,\n      StructType schema,\n      ArrayValue partitionColumns,\n      Optional<Long> createdTime,\n      MapValue configurationMapValue) {\n    this(\n        id,\n        name,\n        description,\n        format,\n        schemaString,\n        new Lazy<>(() -> schema),\n        partitionColumns,\n        createdTime,\n        configurationMapValue);\n  }\n\n  private Metadata(\n      String id,\n      Optional<String> name,\n      Optional<String> description,\n      Format format,\n      String schemaString,\n      Lazy<StructType> lazySchema,\n      ArrayValue partitionColumns,\n      Optional<Long> createdTime,\n      MapValue configurationMapValue) {\n    this.id = requireNonNull(id, \"id is null\");\n    this.name = name;\n    this.description = requireNonNull(description, \"description is null\");\n    this.format = requireNonNull(format, \"format is null\");\n    this.schemaString = requireNonNull(schemaString, \"schemaString is null\");\n    this.schema =\n        new Lazy<>(\n            () -> {\n              StructType s = lazySchema.get();\n              ensureNoMetadataColumns(s);\n              return s;\n            });\n    this.partitionColumns = requireNonNull(partitionColumns, \"partitionColumns is null\");\n    this.createdTime = createdTime;\n    this.configurationMapValue = requireNonNull(configurationMapValue, \"configuration is null\");\n    this.configuration = new Lazy<>(() -> VectorUtils.toJavaMap(configurationMapValue));\n    this.partitionColNames = new Lazy<>(this::loadPartitionColNames);\n    this.dataSchema =\n        new Lazy<>(\n            () ->\n                new StructType(\n                    this.schema.get().fields().stream()\n                        .filter(\n                            field ->\n                                !partitionColNames\n                                    .get()\n                                    .contains(field.getName().toLowerCase(Locale.ROOT)))\n                        .collect(Collectors.toList())));\n  }\n\n  /**\n   * Returns a new metadata object that has a new configuration which is the combination of its\n   * current configuration and {@code configuration}.\n   *\n   * <p>For overlapping keys the values from {@code configuration} take precedence.\n   */\n  public Metadata withMergedConfiguration(Map<String, String> configuration) {\n    Map<String, String> newConfiguration = new HashMap<>(getConfiguration());\n    newConfiguration.putAll(configuration);\n    return withReplacedConfiguration(newConfiguration);\n  }\n\n  /**\n   * Returns a new metadata object that has a new configuration which does not contain any of the\n   * keys provided in {@code keysToUnset}.\n   */\n  public Metadata withConfigurationKeysUnset(Set<String> keysToUnset) {\n    Map<String, String> newConfiguration = new HashMap<>(getConfiguration());\n    keysToUnset.forEach(newConfiguration::remove);\n    return withReplacedConfiguration(newConfiguration);\n  }\n\n  /**\n   * Returns a new Metadata object with the configuration provided with newConfiguration (any prior\n   * configuration is replaced).\n   */\n  public Metadata withReplacedConfiguration(Map<String, String> newConfiguration) {\n    return new Metadata(\n        this.id,\n        this.name,\n        this.description,\n        this.format,\n        this.schemaString,\n        this.schema, // pass Lazy directly to avoid forcing evaluation\n        this.partitionColumns,\n        this.createdTime,\n        VectorUtils.stringStringMapValue(newConfiguration));\n  }\n\n  public Metadata withNewSchema(StructType schema) {\n    return new Metadata(\n        this.id,\n        this.name,\n        this.description,\n        this.format,\n        schema.toJson(),\n        schema,\n        this.partitionColumns,\n        this.createdTime,\n        this.configurationMapValue);\n  }\n\n  @Override\n  public String toString() {\n    List<String> partitionColumnsStr = VectorUtils.toJavaList(partitionColumns);\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"List(\");\n    for (String partitionColumn : partitionColumnsStr) {\n      sb.append(partitionColumn).append(\", \");\n    }\n    if (sb.substring(sb.length() - 2).equals(\", \")) {\n      sb.setLength(sb.length() - 2); // Remove the last comma and space\n    }\n    sb.append(\")\");\n    return \"Metadata{\"\n        + \"id='\"\n        + id\n        + '\\''\n        + \", name=\"\n        + name\n        + \", description=\"\n        + description\n        + \", format=\"\n        + format\n        + \", schemaString='\"\n        + schemaString\n        + '\\''\n        + \", partitionColumns=\"\n        + sb\n        + \", createdTime=\"\n        + createdTime\n        + \", configuration=\"\n        + configuration.get()\n        + '}';\n  }\n\n  public String getSchemaString() {\n    return schemaString;\n  }\n\n  public StructType getSchema() {\n    return schema.get();\n  }\n\n  public ArrayValue getPartitionColumns() {\n    return partitionColumns;\n  }\n\n  /** Set of lowercase partition column names */\n  public Set<String> getPartitionColNames() {\n    return partitionColNames.get();\n  }\n\n  /** The logical data schema which excludes partition columns */\n  public StructType getDataSchema() {\n    return dataSchema.get();\n  }\n\n  public String getId() {\n    return id;\n  }\n\n  public Optional<String> getName() {\n    return name;\n  }\n\n  public Optional<String> getDescription() {\n    return description;\n  }\n\n  public Format getFormat() {\n    return format;\n  }\n\n  public Optional<Long> getCreatedTime() {\n    return createdTime;\n  }\n\n  public MapValue getConfigurationMapValue() {\n    return configurationMapValue;\n  }\n\n  public Map<String, String> getConfiguration() {\n    return Collections.unmodifiableMap(configuration.get());\n  }\n\n  /**\n   * The full schema (including partition columns) with the field names converted to their physical\n   * names (column names used in the data files) based on the table's column mapping mode. When\n   * column mapping mode is ID, fieldId metadata is preserved in the field metadata; all column\n   * metadata is otherwise removed.\n   */\n  public StructType getPhysicalSchema() {\n    ColumnMapping.ColumnMappingMode mappingMode =\n        ColumnMapping.getColumnMappingMode(getConfiguration());\n    return ColumnMapping.convertToPhysicalSchema(getSchema(), getSchema(), mappingMode);\n  }\n\n  /**\n   * Filter out the key-value pair matches exactly with the old properties.\n   *\n   * @param newProperties the new properties to be filtered\n   * @return the filtered properties\n   */\n  public Map<String, String> filterOutUnchangedProperties(Map<String, String> newProperties) {\n    Map<String, String> oldProperties = getConfiguration();\n    return newProperties.entrySet().stream()\n        .filter(\n            entry ->\n                !oldProperties.containsKey(entry.getKey())\n                    || !oldProperties.get(entry.getKey()).equals(entry.getValue()))\n        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link Metadata#FULL_SCHEMA}.\n   *\n   * @return {@link Row} object with the schema {@link Metadata#FULL_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> metadataMap = new HashMap<>();\n    metadataMap.put(0, id);\n    metadataMap.put(1, name.orElse(null));\n    metadataMap.put(2, description.orElse(null));\n    metadataMap.put(3, format.toRow());\n    metadataMap.put(4, schemaString);\n    metadataMap.put(5, partitionColumns);\n    metadataMap.put(6, createdTime.orElse(null));\n    metadataMap.put(7, configurationMapValue);\n\n    return new GenericRow(Metadata.FULL_SCHEMA, metadataMap);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(\n        id,\n        name,\n        description,\n        format,\n        schema.get(),\n        partitionColNames.get(),\n        createdTime,\n        configuration.get());\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof Metadata)) {\n      return false;\n    }\n    Metadata other = (Metadata) o;\n    return id.equals(other.id)\n        && name.equals(other.name)\n        && description.equals(other.description)\n        && format.equals(other.format)\n        && schema.get().equals(other.schema.get())\n        && partitionColNames.get().equals(other.partitionColNames.get())\n        && createdTime.equals(other.createdTime)\n        && configuration.get().equals(other.configuration.get());\n  }\n\n  /** Helper method to load the partition column names. */\n  private Set<String> loadPartitionColNames() {\n    ColumnVector partitionColNameVector = partitionColumns.getElements();\n    Set<String> partitionColumnNames = new HashSet<>();\n    for (int i = 0; i < partitionColumns.getSize(); i++) {\n      checkArgument(\n          !partitionColNameVector.isNullAt(i), \"Expected a non-null partition column name\");\n      String partitionColName = partitionColNameVector.getString(i);\n      checkArgument(\n          partitionColName != null && !partitionColName.isEmpty(),\n          \"Expected non-null and non-empty partition column name\");\n      partitionColumnNames.add(partitionColName.toLowerCase(Locale.ROOT));\n    }\n    return Collections.unmodifiableSet(partitionColumnNames);\n  }\n\n  /** Helper method to ensure that a table schema never contains metadata columns. */\n  private void ensureNoMetadataColumns(StructType schema) {\n    for (StructField field : schema.fields()) {\n      if (field.isMetadataColumn()) {\n        throw new IllegalArgumentException(\n            \"Table schema cannot contain metadata columns: \" + field.getName());\n      }\n    }\n  }\n\n  /**\n   * Serializable representation of Metadata. Converts complex Kernel types (ArrayValue, MapValue)\n   * to simple Java types (List, Map) that are serializable.\n   */\n  private static class SerializableMetadata implements Serializable {\n    private static final long serialVersionUID = 1L;\n\n    private final String id;\n    @Nullable private final String name;\n    @Nullable private final String description;\n    private final String formatProvider;\n    private final Map<String, String> formatOptions;\n    private final String schemaString;\n    private final List<String> partitionColumnsList;\n    @Nullable private final Long createdTime;\n    private final Map<String, String> configuration;\n\n    SerializableMetadata(Metadata metadata) {\n      this.id = metadata.id;\n      this.name = metadata.name.orElse(null);\n      this.description = metadata.description.orElse(null);\n      this.formatProvider = metadata.format.getProvider();\n      this.formatOptions = metadata.format.getOptions();\n      this.schemaString = metadata.schemaString;\n      this.partitionColumnsList = VectorUtils.toJavaList(metadata.partitionColumns);\n      this.createdTime = metadata.createdTime.orElse(null);\n      this.configuration = VectorUtils.toJavaMap(metadata.configurationMapValue);\n    }\n\n    // Reconstruct Metadata from serialized data\n    private Object readResolve() {\n      Format format = new Format(formatProvider, formatOptions);\n      Lazy<StructType> lazySchema =\n          new Lazy<>(() -> DataTypeJsonSerDe.deserializeStructType(schemaString));\n      ArrayValue partitionColumns =\n          VectorUtils.buildArrayValue(partitionColumnsList, StringType.STRING);\n      MapValue configurationMapValue = VectorUtils.stringStringMapValue(configuration);\n\n      return new Metadata(\n          id,\n          Optional.ofNullable(name),\n          Optional.ofNullable(description),\n          format,\n          schemaString,\n          lazySchema,\n          partitionColumns,\n          Optional.ofNullable(createdTime),\n          configurationMapValue);\n    }\n  }\n\n  /**\n   * Replace this Metadata with SerializableMetadata during serialization. This is the standard Java\n   * serialization proxy pattern for immutable objects with complex fields.\n   */\n  private Object writeReplace() {\n    return new SerializableMetadata(this);\n  }\n\n  /** Prevent direct deserialization of Metadata (must use SerializableMetadata). */\n  private void readObject(ObjectInputStream stream) throws InvalidObjectException {\n    throw new InvalidObjectException(\"Use SerializableMetadata\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/Protocol.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.TABLE_FEATURES;\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.TABLE_FEATURES_MIN_WRITER_VERSION;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.VectorUtils.buildArrayValue;\nimport static java.lang.String.format;\nimport static java.util.Collections.emptySet;\nimport static java.util.Collections.unmodifiableSet;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.exceptions.UnsupportedTableFeatureException;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.ArrayType;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class Protocol implements Serializable {\n  private static final long serialVersionUID = 1L;\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  /// Public static variables and methods                                                       ///\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Helper method to get the Protocol from the row representation.\n   *\n   * @param row Row representation of the Protocol.\n   * @return the Protocol object\n   */\n  public static Protocol fromRow(Row row) {\n    requireNonNull(row);\n    Set<String> readerFeatures =\n        row.isNullAt(2)\n            ? Collections.emptySet()\n            : Collections.unmodifiableSet(new HashSet<>(VectorUtils.toJavaList(row.getArray(2))));\n    Set<String> writerFeatures =\n        row.isNullAt(3)\n            ? Collections.emptySet()\n            : Collections.unmodifiableSet(new HashSet<>(VectorUtils.toJavaList(row.getArray(3))));\n    return new Protocol(row.getInt(0), row.getInt(1), readerFeatures, writerFeatures);\n  }\n\n  public static Protocol fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n\n    return new Protocol(\n        vector.getChild(0).getInt(rowId),\n        vector.getChild(1).getInt(rowId),\n        vector.getChild(2).isNullAt(rowId)\n            ? emptySet()\n            : new HashSet<>(VectorUtils.toJavaList(vector.getChild(2).getArray(rowId))),\n        vector.getChild(3).isNullAt(rowId)\n            ? emptySet()\n            : new HashSet<>(VectorUtils.toJavaList(vector.getChild(3).getArray(rowId))));\n  }\n\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"minReaderVersion\", IntegerType.INTEGER, false /* nullable */)\n          .add(\"minWriterVersion\", IntegerType.INTEGER, false /* nullable */)\n          .add(\"readerFeatures\", new ArrayType(StringType.STRING, false /* contains null */))\n          .add(\"writerFeatures\", new ArrayType(StringType.STRING, false /* contains null */));\n\n  private final int minReaderVersion;\n  private final int minWriterVersion;\n  private final Set<String> readerFeatures;\n  private final Set<String> writerFeatures;\n\n  // These are derived fields from minReaderVersion and minWriterVersion\n  private final boolean supportsReaderFeatures;\n  private final boolean supportsWriterFeatures;\n\n  public Protocol(int minReaderVersion, int minWriterVersion) {\n    this(minReaderVersion, minWriterVersion, emptySet(), emptySet());\n  }\n\n  public Protocol(\n      int minReaderVersion,\n      int minWriterVersion,\n      Set<String> readerFeatures,\n      Set<String> writerFeatures) {\n    this.minReaderVersion = minReaderVersion;\n    this.minWriterVersion = minWriterVersion;\n    this.readerFeatures =\n        unmodifiableSet(requireNonNull(readerFeatures, \"readerFeatures cannot be null\"));\n    this.writerFeatures =\n        unmodifiableSet(requireNonNull(writerFeatures, \"writerFeatures cannot be null\"));\n    this.supportsReaderFeatures = TableFeatures.supportsReaderFeatures(minReaderVersion);\n    this.supportsWriterFeatures = TableFeatures.supportsWriterFeatures(minWriterVersion);\n  }\n\n  /** @return The minimum reader version required for this protocol */\n  public int getMinReaderVersion() {\n    return minReaderVersion;\n  }\n\n  /** @return The minimum writer version required for this protocol */\n  public int getMinWriterVersion() {\n    return minWriterVersion;\n  }\n\n  /**\n   * @return The set of explicitly specified reader features for this protocol. Will be empty if\n   *     this protocol does not support reader features.\n   */\n  public Set<String> getReaderFeatures() {\n    return readerFeatures;\n  }\n\n  /**\n   * @return The set of explicitly specified writer features for this protocol. Will be empty if\n   *     this protocol does not support writer features.\n   */\n  public Set<String> getWriterFeatures() {\n    return writerFeatures;\n  }\n\n  /**\n   * @return The combined set of all reader and writer features for this protocol. Will be empty if\n   *     this protocol does not support reader or writer features.\n   */\n  public Set<String> getReaderAndWriterFeatures() {\n    final Set<String> allFeatureNames = new HashSet<>();\n    allFeatureNames.addAll(readerFeatures);\n    allFeatureNames.addAll(writerFeatures);\n    return allFeatureNames;\n  }\n\n  /**\n   * @return Whether this protocol supports explicitly specifying reader features, which occurs when\n   *     the minReaderVersion is greater than or equal to 3.\n   */\n  public boolean supportsReaderFeatures() {\n    return supportsReaderFeatures;\n  }\n\n  /**\n   * @return Whether this protocol supports explicitly specifying writer features, which occurs when\n   *     the minWriterVersion is greater than or equal to 7.\n   */\n  public boolean supportsWriterFeatures() {\n    return supportsWriterFeatures;\n  }\n\n  @Override\n  public String toString() {\n    final StringBuilder sb = new StringBuilder(\"Protocol{\");\n    sb.append(\"minReaderVersion=\").append(minReaderVersion);\n    sb.append(\", minWriterVersion=\").append(minWriterVersion);\n    sb.append(\", readerFeatures=\").append(readerFeatures);\n    sb.append(\", writerFeatures=\").append(writerFeatures);\n    sb.append('}');\n    return sb.toString();\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    Protocol protocol = (Protocol) o;\n    return minReaderVersion == protocol.minReaderVersion\n        && minWriterVersion == protocol.minWriterVersion\n        && Objects.equals(readerFeatures, protocol.readerFeatures)\n        && Objects.equals(writerFeatures, protocol.writerFeatures);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(minReaderVersion, minWriterVersion, readerFeatures, writerFeatures);\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link Protocol#FULL_SCHEMA}. Write any empty\n   * `readerFeatures` and `writerFeatures` as null.\n   *\n   * @return {@link Row} object with the schema {@link Protocol#FULL_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> protocolMap = new HashMap<>();\n    protocolMap.put(0, minReaderVersion);\n    protocolMap.put(1, minWriterVersion);\n    if (supportsReaderFeatures) {\n      protocolMap.put(2, buildArrayValue(new ArrayList<>(readerFeatures), StringType.STRING));\n    }\n    if (supportsWriterFeatures) {\n      protocolMap.put(3, buildArrayValue(new ArrayList<>(writerFeatures), StringType.STRING));\n    }\n\n    return new GenericRow(Protocol.FULL_SCHEMA, protocolMap);\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  /// Public methods related to table features interaction with the protocol                    ///\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  /**\n   * Get the set of features that are implicitly supported by the protocol. Features are implicitly\n   * supported if the reader and/or writer version is less than the versions that supports the\n   * explicit features specified in `readerFeatures` and `writerFeatures` sets. Examples:\n   *\n   * <p>\n   *\n   * <ul>\n   *   <li>(minRV = 1, minWV = 7, readerFeatures=[], writerFeatures=[domainMetadata]) results in []\n   *   <li>(minRV = 1, minWV = 3) results in [appendOnly, invariants, checkConstraints]\n   *   <li>(minRV = 3, minWV = 7, readerFeatures=[v2Checkpoint], writerFeatures=[v2Checkpoint])\n   *       results in []\n   *   <li>(minRV = 2, minWV = 6) results in [appendOnly, invariants, checkConstraints,\n   *       changeDataFeed, generatedColumns, columnMapping, identityColumns]\n   * </ul>\n   */\n  public Set<TableFeature> getImplicitlySupportedFeatures() {\n    if (supportsReaderFeatures && supportsWriterFeatures) {\n      return emptySet();\n    } else {\n      return TABLE_FEATURES.stream()\n          .filter(f -> !supportsReaderFeatures && f.minReaderVersion() <= minReaderVersion)\n          .filter(f -> !supportsWriterFeatures && f.minWriterVersion() <= minWriterVersion)\n          .collect(Collectors.toSet());\n    }\n  }\n\n  /**\n   * Get the set of features that are explicitly supported by the protocol. Features are explicitly\n   * supported if they are present in the `readerFeatures` and/or `writerFeatures` sets. Examples:\n   *\n   * <p>\n   *\n   * <ul>\n   *   <li>(minRV = 1, minWV = 7, writerFeatures=[appendOnly, invariants, checkConstraints]) results\n   *       in [appendOnly, invariants, checkConstraints]\n   *   <li>(minRV = 3, minWV = 7, readerFeatures = [columnMapping], writerFeatures=[columnMapping,\n   *       invariants]) results in [columnMapping, invariants]\n   *   <li>(minRV = 1, minWV = 2, readerFeatures = [], writerFeatures=[]) results in []\n   * </ul>\n   *\n   * @throws UnsupportedTableFeatureException if any table features in the protocol's list of\n   *     readerFeatures or writerFeatures are unsupported by Kernel\n   */\n  public Set<TableFeature> getExplicitlySupportedFeatures() {\n    return Stream.of(readerFeatures, writerFeatures)\n        .flatMap(Set::stream)\n        .map(TableFeatures::getTableFeature) // if a feature is not known, will throw an exception\n        .collect(Collectors.toSet());\n  }\n\n  /**\n   * Get the set of features that are both implicitly and explicitly supported by the protocol.\n   * Usually, the protocol has either implicit or explicit features, but not both. This API provides\n   * a way to get all enabled features.\n   *\n   * @throws UnsupportedTableFeatureException if any table features in the protocol's list of\n   *     readerFeatures or writerFeatures are unsupported by Kernel\n   */\n  public Set<TableFeature> getImplicitlyAndExplicitlySupportedFeatures() {\n    Set<TableFeature> supportedFeatures = new HashSet<>();\n    supportedFeatures.addAll(getImplicitlySupportedFeatures());\n    supportedFeatures.addAll(getExplicitlySupportedFeatures());\n    return supportedFeatures;\n  }\n\n  /**\n   * Get the set of reader writer features that are both implicitly and explicitly supported by the\n   * protocol. Usually, the protocol has either implicit or explicit features, but not both. This\n   * API provides a way to get all enabled reader writer features. It doesn't return any writer only\n   * features.\n   */\n  public Set<TableFeature> getImplicitlyAndExplicitlySupportedReaderWriterFeatures() {\n    return Stream.concat(\n            // implicit supported features\n            TABLE_FEATURES.stream()\n                .filter(\n                    f ->\n                        !supportsReaderFeatures\n                            && f.minReaderVersion() <= this.getMinReaderVersion()),\n            // explicitly supported features\n            readerFeatures.stream().map(TableFeatures::getTableFeature))\n        .collect(toSet());\n  }\n\n  /** Create a new {@link Protocol} object with the given {@link TableFeature} supported. */\n  public Protocol withFeatures(Iterable<TableFeature> newFeatures) {\n    Protocol result = this;\n    for (TableFeature feature : newFeatures) {\n      result = result.withFeature(feature);\n    }\n    return result;\n  }\n\n  /**\n   * Get a new Protocol object that has `feature` supported. Writer-only features will be added to\n   * `writerFeatures` field, and reader-writer features will be added to `readerFeatures` and\n   * `writerFeatures` fields.\n   *\n   * <p>If `feature` is already implicitly supported in the current protocol's legacy reader or\n   * writer protocol version, the new protocol will not modify the original protocol version, i.e.,\n   * the feature will not be explicitly added to the protocol's `readerFeatures` or\n   * `writerFeatures`. This is to avoid unnecessary protocol upgrade for feature that it already\n   * supports.\n   *\n   * <p>Examples:\n   *\n   * <ul>\n   *   <li>current protocol (2, 5) and new feature to add 'invariants` result in (2, 5) as this\n   *       protocol already supports 'invariants' implicitly.\n   *   <li>current protocol is (1, 7, writerFeature='rowTracking,domainMetadata' and the new feature\n   *       to add is 'appendOnly' results in (1, 7,\n   *       writerFeature='rowTracking,domainMetadata,appendOnly')\n   *   <li>current protocol is (1, 7, writerFeature='rowTracking,domainMetadata' and the new feature\n   *       to add is 'columnMapping' results in throwing UnsupportedOperationException as\n   *       'columnMapping' requires higher reader version (2) than the current protocol's reader\n   *       version (1).\n   * </ul>\n   */\n  public Protocol withFeature(TableFeature feature) {\n    // Add required dependencies of the feature\n    Protocol protocolWithDependencies = withFeatures(feature.requiredFeatures());\n\n    if (feature.minReaderVersion() > protocolWithDependencies.minReaderVersion) {\n      throw new UnsupportedOperationException(\n          \"TableFeature requires higher reader protocol version\");\n    }\n\n    if (feature.minWriterVersion() > protocolWithDependencies.minWriterVersion) {\n      throw new UnsupportedOperationException(\n          \"TableFeature requires higher writer protocol version\");\n    }\n\n    boolean shouldAddToReaderFeatures =\n        feature.isReaderWriterFeature()\n            &&\n            // protocol already has support for `readerFeatures` set and the new feature\n            // can be explicitly added to the protocol's `readerFeatures`\n            supportsReaderFeatures;\n\n    Set<String> newReaderFeatures = protocolWithDependencies.readerFeatures;\n    Set<String> newWriterFeatures = protocolWithDependencies.writerFeatures;\n\n    if (shouldAddToReaderFeatures) {\n      newReaderFeatures = new HashSet<>(protocolWithDependencies.readerFeatures);\n      newReaderFeatures.add(feature.featureName());\n    }\n\n    if (supportsWriterFeatures) {\n      newWriterFeatures = new HashSet<>(protocolWithDependencies.writerFeatures);\n      newWriterFeatures.add(feature.featureName());\n    }\n\n    return new Protocol(\n        protocolWithDependencies.minReaderVersion,\n        protocolWithDependencies.minWriterVersion,\n        newReaderFeatures,\n        newWriterFeatures);\n  }\n\n  /**\n   * Determine whether this protocol can be safely upgraded to a new protocol `to`. This means all\n   * features supported by this protocol are supported by `to`.\n   *\n   * <p>Examples regarding feature status:\n   *\n   * <ul>\n   *   <li>`[appendOnly]` to `[appendOnly]` results in allowed.\n   *   <li>`[appendOnly, changeDataFeed]` to `[appendOnly]` results in not allowed.\n   * </ul>\n   */\n  public boolean canUpgradeTo(Protocol to) {\n    return to.getImplicitlyAndExplicitlySupportedFeatures()\n        .containsAll(this.getImplicitlyAndExplicitlySupportedFeatures());\n  }\n\n  /**\n   * Protocol normalization is the process of converting a table features protocol to the weakest\n   * possible form. This primarily refers to converting a table features protocol to a legacy\n   * protocol. A Table Features protocol can be represented with the legacy representation only when\n   * the features set of the former exactly matches a legacy protocol.\n   *\n   * <p>Normalization can also decrease the reader version of a table features protocol when it is\n   * higher than necessary.\n   *\n   * <p>For example:\n   *\n   * <ul>\n   *   <li>(1, 7, AppendOnly, Invariants, CheckConstraints) results in (1, 3)\n   *   <li>(3, 7, RowTracking) results in (1, 7, RowTracking)\n   * </ul>\n   */\n  public Protocol normalized() {\n    // Normalization can only be applied to table feature protocols.\n    if (!isFeatureProtocol()) {\n      return this;\n    }\n\n    Tuple2<Integer, Integer> versions =\n        TableFeatures.minimumRequiredVersions(getExplicitlySupportedFeatures());\n    int minReaderVersion = versions._1;\n    int minWriterVersion = versions._2;\n    Protocol newProtocol = new Protocol(minReaderVersion, minWriterVersion);\n\n    if (this.getImplicitlyAndExplicitlySupportedFeatures()\n        .equals(newProtocol.getImplicitlyAndExplicitlySupportedFeatures())) {\n      return newProtocol;\n    } else {\n      // means we have some feature that is added after table feature support.\n      // Whatever the feature (reader or readerWriter), it is always going to\n      // have minWriterVersion as 7. Required minReaderVersion\n      // should be based on the supported features.\n      return new Protocol(minReaderVersion, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeatures(getExplicitlySupportedFeatures());\n    }\n  }\n\n  /**\n   * Protocol denormalization is the process of converting a legacy protocol to the equivalent table\n   * features protocol. This is the inverse of protocol normalization. It can be used to allow\n   * operations on legacy protocols that yield results which cannot be represented anymore by a\n   * legacy protocol. For example\n   *\n   * <ul>\n   *   <li>(1, 3) results in (1, 7, readerFeatures=[], writerFeatures=[appendOnly, invariants,\n   *       checkConstraints])\n   *   <li>(2, 5) results in (2, 7, readerFeatures=[], writerFeatures=[appendOnly, invariants,\n   *       checkConstraints, changeDataFeed, generatedColumns, columnMapping])\n   * </ul>\n   */\n  public Protocol denormalized() {\n    // Denormalization can only be applied to legacy protocols.\n    if (!isLegacyProtocol()) {\n      return this;\n    }\n\n    Tuple2<Integer, Integer> versions =\n        TableFeatures.minimumRequiredVersions(getImplicitlySupportedFeatures());\n    int minReaderVersion = versions._1;\n\n    return new Protocol(minReaderVersion, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(getImplicitlySupportedFeatures());\n  }\n\n  /**\n   * Helper method that applies both denormalization and normalization. This can be used to\n   * normalize invalid legacy protocols such as (2, 3), (1, 5). A legacy protocol is invalid when\n   * the version numbers are higher than required to support the implied feature set.\n   */\n  public Protocol denormalizedNormalized() {\n    return this.denormalized().normalized();\n  }\n\n  /**\n   * Merge this protocol with multiple `protocols` to have the highest reader and writer versions\n   * plus all explicitly and implicitly supported features.\n   */\n  public Protocol merge(Protocol... others) {\n    List<Protocol> protocols = new ArrayList<>();\n    protocols.add(this);\n    protocols.addAll(Arrays.asList(others));\n\n    int mergedReaderVersion =\n        protocols.stream().mapToInt(Protocol::getMinReaderVersion).max().orElse(0);\n\n    int mergedWriterVersion =\n        protocols.stream().mapToInt(Protocol::getMinWriterVersion).max().orElse(0);\n\n    Set<String> mergedReaderFeatures =\n        protocols.stream().flatMap(p -> p.readerFeatures.stream()).collect(Collectors.toSet());\n\n    Set<String> mergedWriterFeatures =\n        protocols.stream().flatMap(p -> p.writerFeatures.stream()).collect(Collectors.toSet());\n\n    Set<TableFeature> mergedImplicitFeatures =\n        protocols.stream()\n            .flatMap(p -> p.getImplicitlySupportedFeatures().stream())\n            .collect(Collectors.toSet());\n\n    Protocol mergedProtocol =\n        new Protocol(\n                mergedReaderVersion,\n                mergedWriterVersion,\n                mergedReaderFeatures,\n                mergedWriterFeatures)\n            .withFeatures(mergedImplicitFeatures);\n\n    // The merged protocol is always normalized in order to represent the protocol\n    // with the weakest possible form. This enables backward compatibility.\n    // This is preceded by a denormalization step. This allows to fix invalid legacy Protocols.\n    // For example, (2, 3) is normalized to (1, 3). This is because there is no legacy feature\n    // in the set with reader version 2 unless the writer version is at least 5.\n    return mergedProtocol.denormalizedNormalized();\n  }\n\n  /** Check if the protocol supports the given table feature */\n  public boolean supportsFeature(TableFeature feature) {\n    if (feature.isReaderWriterFeature()) {\n      if (supportsReaderFeatures) {\n        return readerFeatures.contains(feature.featureName());\n      } else {\n        return feature.minReaderVersion() <= minReaderVersion;\n      }\n    } else {\n      if (supportsWriterFeatures) {\n        return writerFeatures.contains(feature.featureName());\n      } else {\n        return feature.minWriterVersion() <= minWriterVersion;\n      }\n    }\n  }\n\n  /** Validate the protocol contents represents a valid state */\n  protected void validate() {\n    checkArgument(minReaderVersion >= 1, \"minReaderVersion should be at least 1\");\n    checkArgument(minWriterVersion >= 1, \"minWriterVersion should be at least 1\");\n\n    // expect the reader and writer features to be empty if the protocol version does not support\n    checkArgument(\n        readerFeatures.isEmpty() || supportsReaderFeatures,\n        \"Reader features are not supported for the reader version: \" + minReaderVersion);\n    checkArgument(\n        writerFeatures.isEmpty() || supportsWriterFeatures,\n        \"Writer features are not supported for the writer version: \" + minWriterVersion);\n\n    // If reader versions are supported, expect the writer versions to be supported as well\n    // We don't have any reader only features.\n    if (supportsReaderFeatures) {\n      checkArgument(\n          supportsWriterFeatures,\n          \"writer version doesn't support writer features: \" + minWriterVersion);\n    }\n\n    if (supportsWriterFeatures) {\n      // ensure that the reader version supports all the readerWriter features\n      Set<TableFeature> supportedFeatures = getExplicitlySupportedFeatures();\n      supportedFeatures.stream()\n          .filter(TableFeature::isReaderWriterFeature)\n          .forEach(\n              feature -> {\n                checkArgument(\n                    feature.minReaderVersion() <= minReaderVersion,\n                    format(\n                        \"Reader version %d does not support readerWriter feature %s\",\n                        minReaderVersion, feature.featureName()));\n\n                if (supportsReaderFeatures) {\n                  // if the protocol supports reader features, then it should be part of the\n                  // readerFeatures\n                  checkArgument(\n                      readerFeatures.contains(feature.featureName()),\n                      format(\n                          \"ReaderWriter feature %s is not present in readerFeatures\",\n                          feature.featureName()));\n                }\n              });\n    } else {\n      // ensure we don't get (minReaderVersion, minWriterVersion) that satisfy the readerWriter\n      // feature version requirements. E.g. (1, 5) is invalid as writer version indicates\n      // columnMapping supported but reader version does not support it (requires 2).\n      TABLE_FEATURES.stream()\n          .filter(TableFeature::isReaderWriterFeature)\n          .forEach(\n              f -> {\n                if (f.minWriterVersion() <= minWriterVersion) {\n                  checkArgument(\n                      f.minReaderVersion() <= minReaderVersion,\n                      format(\n                          \"Reader version %d does not support readerWriter feature %s\",\n                          minReaderVersion, f.featureName()));\n                }\n              });\n    }\n  }\n\n  /** is the protocol a legacy protocol, i.e before (3, 7) */\n  private boolean isLegacyProtocol() {\n    return !supportsReaderFeatures && !supportsWriterFeatures;\n  }\n\n  /** is the protocol a table feature protocol, i.e after (3, 7) */\n  private boolean isFeatureProtocol() {\n    // checking for writer feature support is enough as we have\n    // writerOnly or readerWriter features, but no readerOnly features.\n    return supportsWriterFeatures;\n  }\n\n  // Note: Protocol uses default Java serialization because all fields are Serializable:\n  // - int, boolean: primitive types (automatically serializable)\n  // - Set<String>: Set and String both implement Serializable\n  // No need for custom writeObject/readObject!\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/RemoveFile.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.*;\nimport java.util.Optional;\n\n/** Metadata about {@code remove} action in the Delta Log. */\npublic class RemoveFile extends RowBackedAction {\n  /** Full schema of the {@code remove} action in the Delta Log. */\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"path\", StringType.STRING, false /* nullable */)\n          .add(\"deletionTimestamp\", LongType.LONG, true /* nullable */)\n          .add(\"dataChange\", BooleanType.BOOLEAN, false /* nullable*/)\n          .add(\"extendedFileMetadata\", BooleanType.BOOLEAN, true /* nullable */)\n          .add(\n              \"partitionValues\",\n              new MapType(StringType.STRING, StringType.STRING, true),\n              true /* nullable*/)\n          .add(\"size\", LongType.LONG, true /* nullable*/)\n          .add(\"stats\", StringType.STRING, true /* nullable */)\n          .add(\"tags\", new MapType(StringType.STRING, StringType.STRING, true), true /* nullable */)\n          .add(\"deletionVector\", DeletionVectorDescriptor.READ_SCHEMA, true /* nullable */)\n          .add(\"baseRowId\", LongType.LONG, true /* nullable */)\n          .add(\"defaultRowCommitVersion\", LongType.LONG, true /* nullable */);\n  // TODO: Currently Kernel doesn't support RemoveFile actions when rowTracking is enabled (or have\n  //   any public API for generating RemoveFile actions). Once we\n  //   do this we need to ensure that the baseRowId and defaultRowCommitVersion fields are correctly\n  //   populated to match the corresponding AddFile actions\n\n  /** Constructs an {@link RemoveFile} action from the given 'RemoveFile' {@link Row}. */\n  public RemoveFile(Row row) {\n    super(row);\n  }\n\n  public String getPath() {\n    return row.getString(getFieldIndex(\"path\"));\n  }\n\n  public Optional<Long> getDeletionTimestamp() {\n    return row.isNullAt(getFieldIndex(\"deletionTimestamp\"))\n        ? Optional.empty()\n        : Optional.of(row.getLong(getFieldIndex(\"deletionTimestamp\")));\n  }\n\n  public boolean getDataChange() {\n    return row.getBoolean(getFieldIndex(\"dataChange\"));\n  }\n\n  public Optional<Boolean> getExtendedFileMetadata() {\n    return row.isNullAt(getFieldIndex(\"extendedFileMetadata\"))\n        ? Optional.empty()\n        : Optional.of(row.getBoolean(getFieldIndex(\"extendedFileMetadata\")));\n  }\n\n  public Optional<MapValue> getPartitionValues() {\n    return row.isNullAt(getFieldIndex(\"partitionValues\"))\n        ? Optional.empty()\n        : Optional.of(row.getMap(getFieldIndex(\"partitionValues\")));\n  }\n\n  public Optional<Long> getSize() {\n    return row.isNullAt(getFieldIndex(\"size\"))\n        ? Optional.empty()\n        : Optional.of(row.getLong(getFieldIndex(\"size\")));\n  }\n\n  public Optional<String> getStatsJson() {\n    return getFieldIndexOpt(\"stats\")\n        .flatMap(\n            index -> row.isNullAt(index) ? Optional.empty() : Optional.of(row.getString(index)));\n  }\n\n  public Optional<Long> getNumRecords() {\n    return getFieldIndexOpt(\"stats\")\n        .flatMap(\n            index ->\n                row.isNullAt(index)\n                    ? Optional.empty()\n                    : DataFileStatistics.getNumRecords(row.getString(index)));\n  }\n\n  /**\n   * Returns the file statistics parsed from the stats JSON string using the provided schema. This\n   * method deserializes the statistics JSON with full type information, ensuring that min/max\n   * values and null counts are correctly typed according to the physical schema.\n   *\n   * @param physicalSchema the physical schema of the table, used to correctly parse and type the\n   *     statistics values (min/max values and null counts)\n   * @return an {@link Optional} containing the deserialized {@link DataFileStatistics} if the stats\n   *     field is present and non-null, or {@link Optional#empty()} otherwise\n   * @throws io.delta.kernel.exceptions.KernelException if the stats JSON is malformed or if values\n   *     don't match the expected types from the schema\n   * @see DataFileStatistics#deserializeFromJson(String, StructType) for details on the\n   *     deserialization process\n   */\n  public Optional<DataFileStatistics> getStats(StructType physicalSchema) {\n    return getFieldIndexOpt(\"stats\")\n        .flatMap(\n            index ->\n                row.isNullAt(index)\n                    ? Optional.empty()\n                    : DataFileStatistics.deserializeFromJson(row.getString(index), physicalSchema));\n  }\n\n  public Optional<MapValue> getTags() {\n    int index = getFieldIndex(\"tags\");\n    return Optional.ofNullable(row.isNullAt(index) ? null : row.getMap(index));\n  }\n\n  public Optional<DeletionVectorDescriptor> getDeletionVector() {\n    int index = getFieldIndex(\"deletionVector\");\n    return Optional.ofNullable(\n        row.isNullAt(index) ? null : DeletionVectorDescriptor.fromRow(row.getStruct(index)));\n  }\n\n  public Optional<Long> getBaseRowId() {\n    int index = getFieldIndex(\"baseRowId\");\n    return Optional.ofNullable(row.isNullAt(index) ? null : row.getLong(index));\n  }\n\n  public Optional<Long> getDefaultRowCommitVersion() {\n    int index = getFieldIndex(\"defaultRowCommitVersion\");\n    return Optional.ofNullable(row.isNullAt(index) ? null : row.getLong(index));\n  }\n\n  @Override\n  public String toString() {\n    // No specific ordering is guaranteed for partitionValues and tags in the returned string\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"RemoveFile{\");\n    sb.append(\"path='\").append(getPath()).append('\\'');\n    sb.append(\", deletionTimestamp=\").append(getDeletionTimestamp());\n    sb.append(\", dataChange=\").append(getDataChange());\n    sb.append(\", extendedFileMetadata=\").append(getExtendedFileMetadata());\n    sb.append(\", partitionValues=\").append(getPartitionValues().map(VectorUtils::toJavaMap));\n    sb.append(\", size=\").append(getSize());\n    sb.append(\", stats=\").append(getStats(null).map(d -> d.serializeAsJson(null)).orElse(\"\"));\n    sb.append(\", tags=\").append(getTags().map(VectorUtils::toJavaMap));\n    sb.append(\", deletionVector=\").append(getDeletionVector());\n    sb.append(\", baseRowId=\").append(getBaseRowId());\n    sb.append(\", defaultRowCommitVersion=\").append(getDefaultRowCommitVersion());\n    sb.append('}');\n    return sb.toString();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/RowBackedAction.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.DelegateRow;\nimport io.delta.kernel.types.*;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * An abstract base class for Delta Log actions that are backed by a {@link Row}. This design is to\n * avoid materialization of all fields when creating action instances from action rows within\n * Kernel. Actions like {@link AddFile} can extend this class to maintain just a reference to the\n * underlying action row.\n */\npublic abstract class RowBackedAction {\n\n  /** The underlying {@link Row} that represents an action and contains all its field values. */\n  protected final Row row;\n\n  protected RowBackedAction(Row row) {\n    this.row = row;\n  }\n\n  /**\n   * Returns the index of the field with the given name in the schema of the row. Throws an {@link\n   * IllegalArgumentException} if the field is not found.\n   */\n  protected int getFieldIndex(String fieldName) {\n    int index = row.getSchema().indexOf(fieldName);\n    checkArgument(index >= 0, \"Field '%s' not found in schema: %s\", fieldName, row.getSchema());\n    return index;\n  }\n\n  /**\n   * Returns the index of the field with the given name in the schema of the row, or {@link\n   * Optional#empty()} if the field is not found. This should be used when the underlying row may or\n   * may not contain that field.\n   */\n  protected Optional<Integer> getFieldIndexOpt(String fieldName) {\n    int index = row.getSchema().indexOf(fieldName);\n    return index >= 0 ? Optional.of(index) : Optional.empty();\n  }\n\n  /**\n   * Returns a new {@link Row} with the same schema and values as the row backing this action, but\n   * with the value of the field with the given name overridden by the given value.\n   */\n  protected Row toRowWithOverriddenValue(String fieldName, Object value) {\n    Map<Integer, Object> overrides = Collections.singletonMap(getFieldIndex(fieldName), value);\n    return new DelegateRow(row, overrides);\n  }\n\n  /** Returns the underlying {@link Row} that represents this action. */\n  public final Row toRow() {\n    return row;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/SetTransaction.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\n\n/** Delta log action representing a transaction identifier action. */\npublic class SetTransaction {\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"appId\", StringType.STRING, false /* nullable */)\n          .add(\"version\", LongType.LONG, false /* nullable*/)\n          .add(\"lastUpdated\", LongType.LONG, true /* nullable*/);\n\n  public static SetTransaction fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n    return new SetTransaction(\n        vector.getChild(0).getString(rowId),\n        vector.getChild(1).getLong(rowId),\n        vector.getChild(2).isNullAt(rowId)\n            ? Optional.empty()\n            : Optional.of(vector.getChild(2).getLong(rowId)));\n  }\n\n  private final String appId;\n  private final long version;\n  private final Optional<Long> lastUpdated;\n\n  public SetTransaction(String appId, Long version, Optional<Long> lastUpdated) {\n    this.appId = appId;\n    this.version = version;\n    this.lastUpdated = lastUpdated;\n  }\n\n  public String getAppId() {\n    return appId;\n  }\n\n  public long getVersion() {\n    return version;\n  }\n\n  public Optional<Long> getLastUpdated() {\n    return lastUpdated;\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link SetTransaction#FULL_SCHEMA}.\n   *\n   * @return {@link Row} object with the schema {@link SetTransaction#FULL_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> setTransactionMap = new HashMap<>();\n    setTransactionMap.put(0, appId);\n    setTransactionMap.put(1, version);\n    setTransactionMap.put(2, lastUpdated.orElse(null));\n\n    return new GenericRow(SetTransaction.FULL_SCHEMA, setTransactionMap);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/actions/SingleAction.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.types.StructType;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class SingleAction {\n  /**\n   * Get the schema of reading entries from Delta Log delta and checkpoint files for construction of\n   * new checkpoint.\n   */\n  public static StructType CHECKPOINT_SCHEMA =\n      new StructType()\n          .add(\"txn\", SetTransaction.FULL_SCHEMA)\n          .add(\"add\", AddFile.FULL_SCHEMA)\n          .add(\"remove\", RemoveFile.FULL_SCHEMA)\n          .add(\"metaData\", Metadata.FULL_SCHEMA)\n          .add(\"protocol\", Protocol.FULL_SCHEMA)\n          .add(\"domainMetadata\", DomainMetadata.FULL_SCHEMA);\n\n  // Once we start supporting updating CDC or domain metadata enabled tables, we should add the\n  // schema for those fields here.\n\n  /**\n   * Schema to use when reading the winning commit files for conflict resolution. This schema is\n   * just for resolving conflicts when doing a blind append. It doesn't cover case when the txn is\n   * reading data from the table and updating the table.\n   */\n  public static StructType CONFLICT_RESOLUTION_SCHEMA =\n      new StructType()\n          .add(\"txn\", SetTransaction.FULL_SCHEMA)\n          // .add(\"add\", AddFile.FULL_SCHEMA) // not needed for blind appends\n          // .add(\"remove\", RemoveFile.FULL_SCHEMA) // not needed for blind appends\n          .add(\"metaData\", Metadata.FULL_SCHEMA)\n          .add(\"protocol\", Protocol.FULL_SCHEMA)\n          .add(\"commitInfo\", CommitInfo.FULL_SCHEMA)\n          .add(\"domainMetadata\", DomainMetadata.FULL_SCHEMA);\n\n  // Once we start supporting domain metadata/row tracking enabled tables, we should add the\n  // schema for domain metadata fields here.\n\n  // Schema to use when writing out the single action to the Delta Log.\n  public static StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"txn\", SetTransaction.FULL_SCHEMA)\n          .add(\"add\", AddFile.FULL_SCHEMA)\n          .add(\"remove\", RemoveFile.FULL_SCHEMA)\n          .add(\"metaData\", Metadata.FULL_SCHEMA)\n          .add(\"protocol\", Protocol.FULL_SCHEMA)\n          .add(\"cdc\", new StructType())\n          .add(\"commitInfo\", CommitInfo.FULL_SCHEMA)\n          .add(\"domainMetadata\", DomainMetadata.FULL_SCHEMA);\n  // Once we start supporting updating CDC or domain metadata enabled tables, we should add the\n  // schema for those fields here.\n\n  public static final int TXN_ORDINAL = FULL_SCHEMA.indexOf(\"txn\");\n  public static final int ADD_FILE_ORDINAL = FULL_SCHEMA.indexOf(\"add\");\n  public static final int REMOVE_FILE_ORDINAL = FULL_SCHEMA.indexOf(\"remove\");\n  public static final int METADATA_ORDINAL = FULL_SCHEMA.indexOf(\"metaData\");\n  public static final int PROTOCOL_ORDINAL = FULL_SCHEMA.indexOf(\"protocol\");\n  public static final int COMMIT_INFO_ORDINAL = FULL_SCHEMA.indexOf(\"commitInfo\");\n  private static final int DOMAIN_METADATA_ORDINAL = FULL_SCHEMA.indexOf(\"domainMetadata\");\n\n  public static Row createAddFileSingleAction(Row addFile) {\n    Map<Integer, Object> singleActionValueMap = new HashMap<>();\n    singleActionValueMap.put(ADD_FILE_ORDINAL, addFile);\n    return new GenericRow(FULL_SCHEMA, singleActionValueMap);\n  }\n\n  public static Row createProtocolSingleAction(Row protocol) {\n    Map<Integer, Object> singleActionValueMap = new HashMap<>();\n    singleActionValueMap.put(PROTOCOL_ORDINAL, protocol);\n    return new GenericRow(FULL_SCHEMA, singleActionValueMap);\n  }\n\n  public static Row createMetadataSingleAction(Row metadata) {\n    Map<Integer, Object> singleActionValueMap = new HashMap<>();\n    singleActionValueMap.put(METADATA_ORDINAL, metadata);\n    return new GenericRow(FULL_SCHEMA, singleActionValueMap);\n  }\n\n  public static Row createRemoveFileSingleAction(Row remove) {\n    Map<Integer, Object> singleActionValueMap = new HashMap<>();\n    singleActionValueMap.put(REMOVE_FILE_ORDINAL, remove);\n    return new GenericRow(FULL_SCHEMA, singleActionValueMap);\n  }\n\n  public static Row createCommitInfoSingleAction(Row commitInfo) {\n    Map<Integer, Object> singleActionValueMap = new HashMap<>();\n    singleActionValueMap.put(COMMIT_INFO_ORDINAL, commitInfo);\n    return new GenericRow(FULL_SCHEMA, singleActionValueMap);\n  }\n\n  public static Row createDomainMetadataSingleAction(Row domainMetadata) {\n    return new GenericRow(\n        FULL_SCHEMA, Collections.singletonMap(DOMAIN_METADATA_ORDINAL, domainMetadata));\n  }\n\n  public static Row createTxnSingleAction(Row txn) {\n    Map<Integer, Object> singleActionValueMap = new HashMap<>();\n    singleActionValueMap.put(TXN_ORDINAL, txn);\n    return new GenericRow(FULL_SCHEMA, singleActionValueMap);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/annotation/VisibleForTesting.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * Indicates that the visibility of a program element (such as a field, method, or class) is\n * intentionally wider than necessary for testing purposes. This annotation serves as documentation\n * for developers and tooling, clarifying that the element is not intended for production use but\n * must be visible for test code.\n */\n@Documented\n@Retention(RetentionPolicy.CLASS)\n@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})\npublic @interface VisibleForTesting {}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checkpoints/CheckpointInstance.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checkpoints;\n\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Preconditions;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\n\n// TODO: Delete this in favor of ParsedCheckpointData.\n/** Metadata about Delta checkpoint. */\npublic class CheckpointInstance implements Comparable<CheckpointInstance> {\n\n  public enum CheckpointFormat {\n    // Note that the order of these enum values is important for comparison of checkpoint\n    // instances (we prefer V2 > MULTI_PART > CLASSIC).\n    CLASSIC,\n    MULTI_PART,\n    V2;\n\n    // Indicates that the checkpoint (may) contain SidecarFile actions. For compatibility,\n    // V2 checkpoints can be named with classic-style names, so any checkpoint other than a\n    // multipart checkpoint may contain SidecarFile actions.\n    public boolean usesSidecars() {\n      return this == CLASSIC || this == V2;\n    }\n  }\n\n  /** Placeholder to identify the version that is always the latest on timeline */\n  public static final CheckpointInstance MAX_VALUE = new CheckpointInstance(Long.MAX_VALUE);\n\n  public final long version;\n  public final Optional<Integer> numParts;\n\n  public final CheckpointFormat format;\n\n  public final Optional<Path> filePath; // Guaranteed to be present for V2 checkpoints.\n\n  public CheckpointInstance(String path) {\n    Preconditions.checkArgument(\n        FileNames.isCheckpointFile(path), \"not a valid checkpoint file name\");\n\n    String[] pathParts = getPathName(path).split(\"\\\\.\");\n\n    if (pathParts.length == 3 && pathParts[2].equals(\"parquet\")) {\n      // Classic checkpoint 00000000000000000010.checkpoint.parquet\n      this.version = Long.parseLong(pathParts[0]);\n      this.numParts = Optional.empty();\n      this.format = CheckpointFormat.CLASSIC;\n      this.filePath = Optional.empty();\n    } else if (pathParts.length == 5 && pathParts[4].equals(\"parquet\")) {\n      // Multi-part checkpoint 00000000000000000010.checkpoint.0000000001.0000000003.parquet\n      this.version = Long.parseLong(pathParts[0]);\n      this.numParts = Optional.of(Integer.parseInt(pathParts[3]));\n      this.format = CheckpointFormat.MULTI_PART;\n      this.filePath = Optional.empty();\n    } else if (pathParts.length == 4\n        && (pathParts[3].equals(\"parquet\") || pathParts[3].equals(\"json\"))) {\n      // V2 checkpoint 00000000000000000010.checkpoint.UUID.(parquet|json)\n      this.version = Long.parseLong(pathParts[0]);\n      this.numParts = Optional.empty();\n      this.format = CheckpointFormat.V2;\n      this.filePath = Optional.of(new Path(path));\n    } else {\n      throw new RuntimeException(\"Unrecognized checkpoint path format: \" + getPathName(path));\n    }\n  }\n\n  public CheckpointInstance(long version) {\n    this(version, Optional.empty());\n  }\n\n  public CheckpointInstance(long version, Optional<Integer> numParts) {\n    this.version = version;\n    this.numParts = numParts;\n    this.filePath = Optional.empty();\n    if (numParts.orElse(0) == 0) {\n      this.format = CheckpointFormat.CLASSIC;\n    } else {\n      this.format = CheckpointFormat.MULTI_PART;\n    }\n  }\n\n  boolean isNotLaterThan(CheckpointInstance other) {\n    if (other == CheckpointInstance.MAX_VALUE) {\n      return true;\n    }\n    return version <= other.version;\n  }\n\n  boolean isEarlierThan(CheckpointInstance other) {\n    if (other == CheckpointInstance.MAX_VALUE) {\n      return true;\n    }\n    return version < other.version;\n  }\n\n  public List<Path> getCorrespondingFiles(Path path) {\n    if (this == CheckpointInstance.MAX_VALUE) {\n      throw new IllegalStateException(\"Can't get files for CheckpointVersion.MaxValue.\");\n    }\n\n    // This is safe because the only way to construct a V2 CheckpointInstance is with the path.\n    if (format == CheckpointFormat.V2) {\n      return Collections.singletonList(filePath.get());\n    }\n\n    return numParts\n        .map(parts -> FileNames.checkpointFileWithParts(path, version, parts))\n        .orElseGet(\n            () -> Collections.singletonList(FileNames.checkpointFileSingular(path, version)));\n  }\n\n  /**\n   * Comparison rules: 1. A CheckpointInstance with higher version is greater than the one with\n   * lower version. 2. A CheckpointInstance for a V2 checkpoint is greater than a classic checkpoint\n   * (to filter avoid selecting the compatibility file) or a multipart checkpoint. 3. For\n   * CheckpointInstances with same version, a Multi-part checkpoint is greater than a Single part\n   * checkpoint. 4. For Multi-part CheckpointInstance corresponding to same version, the one with\n   * more parts is greater than the one with fewer parts. 5. For V2 checkpoints, use the file path\n   * to break ties.\n   */\n  @Override\n  public int compareTo(CheckpointInstance that) {\n    // Compare versions.\n    if (version != that.version) {\n      return Long.compare(version, that.version);\n    }\n\n    // Compare formats.\n    if (format != that.format) {\n      return Integer.compare(format.ordinal(), that.format.ordinal());\n    }\n\n    // Use format-specific tiebreakers if versions and formats are the same.\n    switch (format) {\n      case CLASSIC:\n        return 0; // No way to break ties if both are classic checkpoints.\n      case MULTI_PART:\n        return Long.compare(numParts.orElse(1), that.numParts.orElse(1));\n      case V2:\n        return filePath.get().getName().compareTo(that.filePath.get().getName());\n      default:\n        throw new IllegalStateException(\"Unexpected format: \" + format);\n    }\n  }\n\n  @Override\n  public String toString() {\n    return \"CheckpointInstance{version=\"\n        + version\n        + \", numParts=\"\n        + numParts\n        + \", format=\"\n        + format\n        + \", filePath=\"\n        + filePath\n        + \"}\";\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n\n    CheckpointInstance checkpointInstance = (CheckpointInstance) o;\n    return this.compareTo(checkpointInstance) == 0;\n  }\n\n  @Override\n  public int hashCode() {\n    // For V2 checkpoints, the filepath is included in the hash of the instance (as we consider\n    // different UUID checkpoints to be different checkpoint instances. Otherwise, ignore\n    // the filepath (which is empty) when hashing.\n    return Objects.hash(version, numParts, format, filePath);\n  }\n\n  private String getPathName(String path) {\n    int slash = path.lastIndexOf(\"/\");\n    return path.substring(slash + 1);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checkpoints/CheckpointMetaData.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checkpoints;\n\nimport static io.delta.kernel.internal.util.VectorUtils.stringStringMapValue;\nimport static io.delta.kernel.internal.util.VectorUtils.toJavaMap;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.MapType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\n\npublic class CheckpointMetaData {\n  public static CheckpointMetaData fromRow(Row row) {\n    return new CheckpointMetaData(\n        row.getLong(0),\n        row.getLong(1),\n        row.isNullAt(2) ? Optional.empty() : Optional.of(row.getLong(2)),\n        row.isNullAt(3) ? Map.of() : toJavaMap(row.getMap(3)));\n  }\n\n  public static StructType READ_SCHEMA =\n      new StructType()\n          .add(\"version\", LongType.LONG, false /* nullable */)\n          .add(\"size\", LongType.LONG, false /* nullable */)\n          .add(\"parts\", LongType.LONG)\n          .add(\"tags\", new MapType(StringType.STRING, StringType.STRING, false));\n\n  public final long version;\n  public final long size;\n  public final Optional<Long> parts;\n  public final Map<String, String> tags;\n\n  public CheckpointMetaData(long version, long size, Optional<Long> parts) {\n    this(version, size, parts, Map.of());\n  }\n\n  public CheckpointMetaData(\n      long version, long size, Optional<Long> parts, Map<String, String> tags) {\n    this.version = version;\n    this.size = size;\n    this.parts = parts;\n    this.tags = tags;\n  }\n\n  public Row toRow() {\n    Map<Integer, Object> dataMap = new HashMap<>();\n    dataMap.put(0, version);\n    dataMap.put(1, size);\n    parts.ifPresent(aLong -> dataMap.put(2, aLong));\n    if (!tags.isEmpty()) {\n      dataMap.put(3, stringStringMapValue(tags));\n    }\n\n    return new GenericRow(READ_SCHEMA, dataMap);\n  }\n\n  @Override\n  public String toString() {\n    return \"CheckpointMetaData{\"\n        + \"version=\"\n        + version\n        + \", size=\"\n        + size\n        + \", parts=\"\n        + parts\n        + \", tags=\"\n        + tags\n        + '}';\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checkpoints/CheckpointMetadataAction.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.checkpoints;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.MapType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/** Action representing a checkpointMetadata action in a top-level V2 checkpoint file */\npublic class CheckpointMetadataAction {\n\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"version\", LongType.LONG, false /* nullable */)\n          .add(\"tags\", new MapType(StringType.STRING, StringType.STRING, false /* nullable */));\n\n  private final long version;\n  private final Map<String, String> tags;\n\n  public CheckpointMetadataAction(long version, Map<String, String> tags) {\n    this.version = version;\n    this.tags = tags;\n  }\n\n  public long getVersion() {\n    return version;\n  }\n\n  public Map<String, String> getTags() {\n    return tags;\n  }\n\n  public Row toRow() {\n    Map<Integer, Object> contentMap = new HashMap<>();\n    contentMap.put(0, version);\n    contentMap.put(1, VectorUtils.stringStringMapValue(tags));\n    return new GenericRow(FULL_SCHEMA, contentMap);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checkpoints/Checkpointer.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checkpoints;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.TableConfig.EXPIRED_LOG_CLEANUP_ENABLED;\nimport static io.delta.kernel.internal.TableConfig.LOG_RETENTION;\nimport static io.delta.kernel.internal.snapshot.MetadataCleanup.cleanupExpiredLogs;\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.CHECKPOINT_PROTECTION_W_FEATURE;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.CheckpointAlreadyExistsException;\nimport io.delta.kernel.exceptions.KernelEngineException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.replay.CreateCheckpointIterator;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.*;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.*;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Class to load and write the {@link CheckpointMetaData} from `_last_checkpoint` file. */\npublic class Checkpointer {\n\n  ////////////////////////////////\n  // Static variables / methods //\n  ////////////////////////////////\n\n  private static final Logger logger = LoggerFactory.getLogger(Checkpointer.class);\n\n  private static final int READ_LAST_CHECKPOINT_FILE_MAX_RETRIES = 3;\n\n  /** The name of the last checkpoint file */\n  public static final String LAST_CHECKPOINT_FILE_NAME = \"_last_checkpoint\";\n\n  public static void checkpoint(Engine engine, Clock clock, SnapshotImpl snapshot)\n      throws TableNotFoundException, IOException {\n    final Path tablePath = snapshot.getDataPath();\n    final Path logPath = snapshot.getLogPath();\n    final long version = snapshot.getVersion();\n\n    logger.info(\"{}: Starting checkpoint for version: {}\", tablePath, version);\n\n    // Check if writing to the given table protocol version/features is supported in Kernel\n    TableFeatures.validateKernelCanWriteToTable(\n        snapshot.getProtocol(), snapshot.getMetadata(), snapshot.getDataPath().toString());\n\n    final Path checkpointPath = FileNames.checkpointFileSingular(logPath, version);\n\n    long numberOfAddFiles = 0;\n    try (CreateCheckpointIterator checkpointDataIter =\n        snapshot.getCreateCheckpointIterator(engine)) {\n      // Write the iterator actions to the checkpoint using the Parquet handler\n      wrapEngineExceptionThrowsIO(\n          () -> {\n            engine\n                .getParquetHandler()\n                .writeParquetFileAtomically(checkpointPath.toString(), checkpointDataIter);\n\n            logger.info(\"{}: Finished writing checkpoint file for version: {}\", tablePath, version);\n\n            return null;\n          },\n          \"Writing checkpoint file %s\",\n          checkpointPath.toString());\n\n      // Get the metadata of the checkpoint file\n      numberOfAddFiles = checkpointDataIter.getNumberOfAddActions();\n    } catch (FileAlreadyExistsException faee) {\n      throw new CheckpointAlreadyExistsException(version);\n    } catch (IOException io) {\n      if (io.getCause() instanceof FileAlreadyExistsException) {\n        throw new CheckpointAlreadyExistsException(version);\n      }\n      throw io;\n    }\n\n    final CheckpointMetaData checkpointMetaData =\n        new CheckpointMetaData(version, numberOfAddFiles, Optional.empty());\n\n    new Checkpointer(logPath).writeLastCheckpointFile(engine, checkpointMetaData);\n\n    logger.info(\n        \"{}: Finished writing last checkpoint metadata file for version: {}\", tablePath, version);\n\n    final Metadata metadata = snapshot.getMetadata();\n    if (shouldPerformLogCleanup(snapshot)) {\n      cleanupExpiredLogs(engine, clock, tablePath, LOG_RETENTION.fromMetadata(metadata));\n    } else {\n      logger.info(\n          \"{}: Log cleanup is disabled. Skipping the deletion of expired log files\", tablePath);\n    }\n  }\n\n  /**\n   * Only clean up expired log files when:\n   *\n   * <ul>\n   *   <li>Snapshot was built as \"latest\" by intent (not time-traveled)\n   *   <li>checkpointProtection feature is not enabled\n   *   <li>delta.enableExpiredLogCleanup table property is set to true\n   * </ul>\n   */\n  private static boolean shouldPerformLogCleanup(SnapshotImpl snapshot) {\n    final boolean hasCheckpointProtection =\n        snapshot\n            .getProtocol()\n            .getWriterFeatures()\n            .contains(CHECKPOINT_PROTECTION_W_FEATURE.featureName());\n    return snapshot.wasBuiltAsLatest()\n        && EXPIRED_LOG_CLEANUP_ENABLED.fromMetadata(snapshot.getMetadata())\n        && !hasCheckpointProtection;\n  }\n\n  /**\n   * Given a list of checkpoint files, pick the latest complete checkpoint instance which is not\n   * later than `notLaterThan`.\n   */\n  public static Optional<CheckpointInstance> getLatestCompleteCheckpointFromList(\n      List<CheckpointInstance> instances, CheckpointInstance notLaterThan) {\n    final List<CheckpointInstance> completeCheckpoints =\n        instances.stream()\n            .filter(c -> c.isNotLaterThan(notLaterThan))\n            .collect(Collectors.groupingBy(c -> c))\n            .entrySet()\n            .stream()\n            .filter(\n                entry -> {\n                  final CheckpointInstance key = entry.getKey();\n                  final List<CheckpointInstance> inst = entry.getValue();\n\n                  if (key.numParts.isPresent()) {\n                    return inst.size() == entry.getKey().numParts.get();\n                  } else {\n                    return inst.size() == 1;\n                  }\n                })\n            .map(Map.Entry::getKey)\n            .collect(Collectors.toList());\n\n    if (completeCheckpoints.isEmpty()) {\n      return Optional.empty();\n    } else {\n      return Optional.of(Collections.max(completeCheckpoints));\n    }\n  }\n\n  /** Find the last complete checkpoint before (strictly less than) a given version. */\n  public static Optional<CheckpointInstance> findLastCompleteCheckpointBefore(\n      Engine engine, Path tableLogPath, long version) {\n    return findLastCompleteCheckpointBeforeHelper(engine, tableLogPath, version)._1;\n  }\n\n  /**\n   * Helper method for `findLastCompleteCheckpointBefore` which also return the number of files\n   * searched. This helps in testing\n   */\n  public static Tuple2<Optional<CheckpointInstance>, Long> findLastCompleteCheckpointBeforeHelper(\n      Engine engine, Path tableLogPath, long version) {\n    CheckpointInstance upperBoundCheckpoint = new CheckpointInstance(version);\n    logger.info(\"Try to find the last complete checkpoint before version {}\", version);\n\n    // This is a just a tracker for testing purposes\n    long numberOfFilesSearched = 0;\n    long currentVersion = version;\n\n    // Some cloud storage APIs make a calls to fetch 1000 at a time.\n    // To make use of that observation and to avoid making more listing calls than\n    // necessary, list 1000 at a time (backwards from the given version). Search\n    // within that list if a checkpoint is found. If found stop, otherwise list the previous\n    // 1000 entries. Repeat until a checkpoint is found or there are no more delta commits.\n    while (currentVersion >= 0) {\n      try {\n        long searchLowerBound = Math.max(0, currentVersion - 1000);\n        CloseableIterator<FileStatus> deltaLogFileIter =\n            wrapEngineExceptionThrowsIO(\n                () ->\n                    engine\n                        .getFileSystemClient()\n                        .listFrom(FileNames.listingPrefix(tableLogPath, searchLowerBound)),\n                \"Listing from %s\",\n                FileNames.listingPrefix(tableLogPath, searchLowerBound));\n\n        List<CheckpointInstance> checkpoints = new ArrayList<>();\n        while (deltaLogFileIter.hasNext()) {\n          FileStatus fileStatus = deltaLogFileIter.next();\n          String fileName = new Path(fileStatus.getPath()).getName();\n\n          long currentFileVersion;\n          if (FileNames.isCommitFile(fileName)) {\n            currentFileVersion = FileNames.deltaVersion(fileName);\n          } else if (FileNames.isCheckpointFile(fileName)) {\n            currentFileVersion = FileNames.checkpointVersion(fileName);\n          } else {\n            // allow all other types of files.\n            currentFileVersion = currentVersion;\n          }\n\n          boolean shouldContinue =\n              // only consider files with version in the range and\n              // before the target version\n              (currentVersion == 0 || currentFileVersion <= currentVersion)\n                  && currentFileVersion < version;\n\n          if (!shouldContinue) {\n            break;\n          }\n          if (validCheckpointFile(fileStatus)) {\n            checkpoints.add(new CheckpointInstance(fileStatus.getPath()));\n          }\n          numberOfFilesSearched++;\n        }\n\n        Optional<CheckpointInstance> latestCheckpoint =\n            getLatestCompleteCheckpointFromList(checkpoints, upperBoundCheckpoint);\n\n        if (latestCheckpoint.isPresent()) {\n          logger.info(\n              \"Found the last complete checkpoint before version {} at {}\",\n              version,\n              latestCheckpoint.get());\n          return new Tuple2<>(latestCheckpoint, numberOfFilesSearched);\n        }\n        currentVersion -= 1000; // search for checkpoint in previous 1000 entries\n      } catch (IOException e) {\n        String msg =\n            String.format(\n                \"Failed to list checkpoint files for version %s in %s.\", version, tableLogPath);\n        logger.warn(msg, e);\n        return new Tuple2<>(Optional.empty(), numberOfFilesSearched);\n      }\n    }\n    logger.info(\"No complete checkpoint found before version {} in {}\", version, tableLogPath);\n    return new Tuple2<>(Optional.empty(), numberOfFilesSearched);\n  }\n\n  private static boolean validCheckpointFile(FileStatus fileStatus) {\n    return FileNames.isCheckpointFile(new Path(fileStatus.getPath()).getName())\n        && fileStatus.getSize() > 0;\n  }\n\n  ////////////////////////////////\n  // Member variables / methods //\n  ////////////////////////////////\n\n  /** The path to the file that holds metadata about the most recent checkpoint. */\n  private final Path lastCheckpointFilePath;\n\n  public Checkpointer(Path logPath) {\n    this.lastCheckpointFilePath = new Path(logPath, LAST_CHECKPOINT_FILE_NAME);\n  }\n\n  /** Returns information about the most recent checkpoint. */\n  public Optional<CheckpointMetaData> readLastCheckpointFile(Engine engine) {\n    return loadMetadataFromFile(engine, 0 /* tries */);\n  }\n\n  /**\n   * Write the given data to last checkpoint metadata file.\n   *\n   * @param engine {@link Engine} instance to use for writing\n   * @param checkpointMetaData Checkpoint metadata to write\n   * @throws IOException For any I/O issues.\n   */\n  public void writeLastCheckpointFile(Engine engine, CheckpointMetaData checkpointMetaData)\n      throws IOException {\n    wrapEngineExceptionThrowsIO(\n        () -> {\n          engine\n              .getJsonHandler()\n              .writeJsonFileAtomically(\n                  lastCheckpointFilePath.toString(),\n                  singletonCloseableIterator(checkpointMetaData.toRow()),\n                  true /* overwrite */);\n          return null;\n        },\n        \"Writing last checkpoint file at `%s`\",\n        lastCheckpointFilePath);\n  }\n\n  /**\n   * Loads the checkpoint metadata from the _last_checkpoint file.\n   *\n   * @param engine {@link Engine instance to use}\n   * @param tries Number of times already tried to load the metadata before this call.\n   */\n  private Optional<CheckpointMetaData> loadMetadataFromFile(Engine engine, int tries) {\n    if (tries >= READ_LAST_CHECKPOINT_FILE_MAX_RETRIES) {\n      // We have tried 3 times and failed. Assume the checkpoint metadata file is corrupt.\n      logger.warn(\n          \"Failed to load checkpoint metadata from file {} after {} attempts.\",\n          lastCheckpointFilePath,\n          READ_LAST_CHECKPOINT_FILE_MAX_RETRIES);\n      return Optional.empty();\n    }\n\n    logger.info(\n        \"Loading last checkpoint from the _last_checkpoint file. Attempt: {} / {}\",\n        tries + 1,\n        READ_LAST_CHECKPOINT_FILE_MAX_RETRIES);\n\n    try {\n      // Use arbitrary values for size and mod time as they are not available.\n      // We could list and find the values, but it is an unnecessary FS call.\n      FileStatus lastCheckpointFile =\n          FileStatus.of(lastCheckpointFilePath.toString(), 0 /* size */, 0 /* modTime */);\n\n      try (CloseableIterator<ColumnarBatch> jsonIter =\n          wrapEngineExceptionThrowsIO(\n              () ->\n                  engine\n                      .getJsonHandler()\n                      .readJsonFiles(\n                          singletonCloseableIterator(lastCheckpointFile),\n                          CheckpointMetaData.READ_SCHEMA,\n                          Optional.empty()),\n              \"Reading the last checkpoint file as JSON\")) {\n        Optional<Row> checkpointRow = InternalUtils.getSingularRow(jsonIter);\n        if (checkpointRow.isPresent()) {\n          return Optional.of(CheckpointMetaData.fromRow(checkpointRow.get()));\n        }\n\n        // Checkpoint has no data. This is a valid case on some file systems where the\n        // contents are not visible until the file stream is closed.\n        // Sleep for one second and retry.\n        logger.warn(\n            \"Last checkpoint file {} has no data. \" + \"Retrying after 1sec. (current attempt = {})\",\n            lastCheckpointFilePath,\n            tries);\n        try {\n          Thread.sleep(1000);\n        } catch (InterruptedException e) {\n          Thread.currentThread().interrupt();\n          return Optional.empty();\n        }\n        return loadMetadataFromFile(engine, tries + 1);\n      }\n    } catch (Exception e) {\n      if (e instanceof FileNotFoundException\n          || (e instanceof KernelEngineException\n              && e.getCause() instanceof FileNotFoundException)) {\n        return Optional.empty(); // there's no point in retrying\n      }\n      String msg =\n          String.format(\n              \"Failed to load checkpoint metadata from file %s. \"\n                  + \"It must be in the process of being written. \"\n                  + \"Retrying after 1sec. (current attempt of %s (max 3)\",\n              lastCheckpointFilePath, tries);\n      logger.warn(msg, e);\n      // we can retry until max tries are exhausted. It saves latency as the alternative\n      // is to list files and find the last checkpoint file. And the `_last_checkpoint`\n      // file is possibly being written to.\n      return loadMetadataFromFile(engine, tries + 1);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checkpoints/SidecarFile.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checkpoints;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/** Action representing a SidecarFile in a top-level V2 checkpoint file. */\npublic class SidecarFile {\n  public static StructType READ_SCHEMA =\n      new StructType()\n          .add(\"path\", StringType.STRING, false /* nullable */)\n          .add(\"sizeInBytes\", LongType.LONG, false /* nullable */)\n          .add(\"modificationTime\", LongType.LONG, false /* nullable */);\n\n  public static SidecarFile fromColumnVector(ColumnVector vector, int rowIndex) {\n    if (vector.isNullAt(rowIndex)) {\n      return null;\n    }\n    return new SidecarFile(\n        vector.getChild(0).getString(rowIndex),\n        vector.getChild(1).getLong(rowIndex),\n        vector.getChild(2).getLong(rowIndex));\n  }\n\n  private final String path;\n  private final long sizeInBytes;\n  private final long modificationTime;\n\n  public SidecarFile(String path, long sizeInBytes, long modificationTime) {\n    this.path = path;\n    this.sizeInBytes = sizeInBytes;\n    this.modificationTime = modificationTime;\n  }\n\n  public String getPath() {\n    return path;\n  }\n\n  public long getSizeInBytes() {\n    return sizeInBytes;\n  }\n\n  public long getModificationTime() {\n    return modificationTime;\n  }\n\n  public Row toRow() {\n    Map<Integer, Object> dataMap = new HashMap<>();\n    dataMap.put(0, path);\n    dataMap.put(1, sizeInBytes);\n    dataMap.put(2, modificationTime);\n\n    return new GenericRow(READ_SCHEMA, dataMap);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checksum/CRCInfo.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checksum;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.data.StructRow;\nimport io.delta.kernel.internal.stats.FileSizeHistogram;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.ArrayType;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class CRCInfo {\n  private static final Logger logger = LoggerFactory.getLogger(CRCInfo.class);\n\n  // Constants for schema field names\n  private static final String TABLE_SIZE_BYTES = \"tableSizeBytes\";\n  private static final String NUM_FILES = \"numFiles\";\n  private static final String NUM_METADATA = \"numMetadata\";\n  private static final String NUM_PROTOCOL = \"numProtocol\";\n  private static final String METADATA = \"metadata\";\n  private static final String PROTOCOL = \"protocol\";\n  private static final String TXN_ID = \"txnId\";\n  private static final String DOMAIN_METADATA = \"domainMetadata\";\n  private static final String FILE_SIZE_HISTOGRAM = \"fileSizeHistogram\";\n  private static final String HISTOGRAM_OPT = \"histogramOpt\";\n\n  public static final StructType CRC_FILE_SCHEMA =\n      new StructType()\n          .add(TABLE_SIZE_BYTES, LongType.LONG)\n          .add(NUM_FILES, LongType.LONG)\n          .add(NUM_METADATA, LongType.LONG)\n          .add(NUM_PROTOCOL, LongType.LONG)\n          .add(METADATA, Metadata.FULL_SCHEMA)\n          .add(PROTOCOL, Protocol.FULL_SCHEMA)\n          .add(TXN_ID, StringType.STRING, /*nullable*/ true)\n          .add(DOMAIN_METADATA, new ArrayType(DomainMetadata.FULL_SCHEMA, false), /*nullable*/ true)\n          .add(FILE_SIZE_HISTOGRAM, FileSizeHistogram.FULL_SCHEMA, /*nullable*/ true);\n\n  // Used by ChecksumReader to support reading CRC files with the legacy \"histogramOpt\" field.\n  public static final StructType CRC_FILE_READ_SCHEMA =\n      CRC_FILE_SCHEMA.add(HISTOGRAM_OPT, FileSizeHistogram.FULL_SCHEMA, /*nullable*/ true);\n\n  public static Optional<CRCInfo> fromColumnarBatch(\n      long version, ColumnarBatch batch, int rowId, String crcFilePath) {\n    // Read required fields.\n    Protocol protocol =\n        Protocol.fromColumnVector(batch.getColumnVector(getSchemaIndex(PROTOCOL)), rowId);\n    Metadata metadata =\n        Metadata.fromColumnVector(batch.getColumnVector(getSchemaIndex(METADATA)), rowId);\n    long tableSizeBytes =\n        InternalUtils.requireNonNull(\n                batch.getColumnVector(getSchemaIndex(TABLE_SIZE_BYTES)), rowId, TABLE_SIZE_BYTES)\n            .getLong(rowId);\n    long numFiles =\n        InternalUtils.requireNonNull(\n                batch.getColumnVector(getSchemaIndex(NUM_FILES)), rowId, NUM_FILES)\n            .getLong(rowId);\n\n    // Read optional fields\n    ColumnVector txnIdColumnVector = batch.getColumnVector(getSchemaIndex(TXN_ID));\n    Optional<String> txnId =\n        txnIdColumnVector.isNullAt(rowId)\n            ? Optional.empty()\n            : Optional.of(txnIdColumnVector.getString(rowId));\n    Optional<FileSizeHistogram> fileSizeHistogram =\n        FileSizeHistogram.fromColumnVector(\n            batch.getColumnVector(getSchemaIndex(FILE_SIZE_HISTOGRAM)), rowId);\n    if (!fileSizeHistogram.isPresent()) {\n      int histogramOptIdx = batch.getSchema().indexOf(HISTOGRAM_OPT);\n      if (histogramOptIdx >= 0) {\n        fileSizeHistogram =\n            FileSizeHistogram.fromColumnVector(batch.getColumnVector(histogramOptIdx), rowId);\n      }\n    }\n    ColumnVector domainMetadataVector = batch.getColumnVector(getSchemaIndex(DOMAIN_METADATA));\n    Optional<Set<DomainMetadata>> domainMetadata =\n        domainMetadataVector.isNullAt(rowId)\n            ? Optional.empty()\n            : Optional.of(\n                VectorUtils.toJavaList(domainMetadataVector.getArray(rowId)).stream()\n                    .map(row -> DomainMetadata.fromRow((StructRow) row))\n                    .collect(Collectors.toSet()));\n\n    //  protocol and metadata are nullable per fromColumnVector's implementation.\n    if (protocol == null || metadata == null) {\n      logger.warn(\"Invalid checksum file missing protocol and/or metadata: {}\", crcFilePath);\n      return Optional.empty();\n    }\n    return Optional.of(\n        new CRCInfo(\n            version,\n            metadata,\n            protocol,\n            tableSizeBytes,\n            numFiles,\n            txnId,\n            domainMetadata,\n            fileSizeHistogram));\n  }\n\n  private final long version;\n  private final Metadata metadata;\n  private final Protocol protocol;\n  private final long tableSizeBytes;\n  private final long numFiles;\n  private final Optional<String> txnId;\n  private final Optional<Set<DomainMetadata>> domainMetadata;\n  private final Optional<FileSizeHistogram> fileSizeHistogram;\n\n  public CRCInfo(\n      long version,\n      Metadata metadata,\n      Protocol protocol,\n      long tableSizeBytes,\n      long numFiles,\n      Optional<String> txnId,\n      Optional<Set<DomainMetadata>> domainMetadata,\n      Optional<FileSizeHistogram> fileSizeHistogram) {\n    checkArgument(tableSizeBytes >= 0);\n    checkArgument(numFiles >= 0);\n    // Live Domain Metadata actions at this version, excluding tombstones.\n    this.domainMetadata = requireNonNull(domainMetadata);\n    domainMetadata.ifPresent(\n        dms ->\n            dms.forEach(\n                dm ->\n                    checkArgument(\n                        !dm.isRemoved(),\n                        String.format(\n                            \"Domain metadata in CRC should exclude tombstones, \"\n                                + \"found removed domain metadata: %s.\",\n                            dm.getDomain()))));\n    this.version = version;\n    this.metadata = requireNonNull(metadata);\n    this.protocol = requireNonNull(protocol);\n    this.tableSizeBytes = tableSizeBytes;\n    this.numFiles = numFiles;\n    this.txnId = requireNonNull(txnId);\n    this.fileSizeHistogram = requireNonNull(fileSizeHistogram);\n  }\n\n  /** The version of the Delta table that this CRCInfo represents. */\n  public long getVersion() {\n    return version;\n  }\n\n  /** The {@link Metadata} stored in this CRCInfo. */\n  public Metadata getMetadata() {\n    return metadata;\n  }\n\n  /** The {@link Protocol} stored in this CRCInfo. */\n  public Protocol getProtocol() {\n    return protocol;\n  }\n\n  public long getNumFiles() {\n    return numFiles;\n  }\n\n  public long getTableSizeBytes() {\n    return tableSizeBytes;\n  }\n\n  public Optional<String> getTxnId() {\n    return txnId;\n  }\n\n  public Optional<Set<DomainMetadata>> getDomainMetadata() {\n    return domainMetadata;\n  }\n\n  /** The {@link FileSizeHistogram} stored in this CRCInfo. */\n  public Optional<FileSizeHistogram> getFileSizeHistogram() {\n    return fileSizeHistogram;\n  }\n\n  /**\n   * Encode as a {@link Row} object with the schema {@link CRCInfo#CRC_FILE_SCHEMA}.\n   *\n   * @return {@link Row} object with the schema {@link CRCInfo#CRC_FILE_SCHEMA}\n   */\n  public Row toRow() {\n    Map<Integer, Object> values = new HashMap<>();\n    // Add required fields\n    values.put(getSchemaIndex(TABLE_SIZE_BYTES), tableSizeBytes);\n    values.put(getSchemaIndex(NUM_FILES), numFiles);\n    values.put(getSchemaIndex(NUM_METADATA), 1L);\n    values.put(getSchemaIndex(NUM_PROTOCOL), 1L);\n    values.put(getSchemaIndex(METADATA), metadata.toRow());\n    values.put(getSchemaIndex(PROTOCOL), protocol.toRow());\n\n    // Add optional fields\n    txnId.ifPresent(txn -> values.put(getSchemaIndex(TXN_ID), txn));\n    domainMetadata.ifPresent(\n        domainMetadataSet ->\n            values.put(\n                getSchemaIndex(DOMAIN_METADATA),\n                VectorUtils.buildArrayValue(\n                    domainMetadataSet.stream()\n                        .map(DomainMetadata::toRow)\n                        .collect(Collectors.toList()),\n                    DomainMetadata.FULL_SCHEMA)));\n    fileSizeHistogram.ifPresent(\n        fileSizeHistogram ->\n            values.put(getSchemaIndex(FILE_SIZE_HISTOGRAM), fileSizeHistogram.toRow()));\n    return new GenericRow(CRC_FILE_SCHEMA, values);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(\n        version,\n        metadata,\n        protocol,\n        tableSizeBytes,\n        numFiles,\n        txnId,\n        domainMetadata,\n        fileSizeHistogram);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof CRCInfo)) {\n      return false;\n    }\n    CRCInfo other = (CRCInfo) o;\n    return version == other.version\n        && tableSizeBytes == other.tableSizeBytes\n        && numFiles == other.numFiles\n        && metadata.equals(other.metadata)\n        && protocol.equals(other.protocol)\n        && txnId.equals(other.txnId)\n        && domainMetadata.equals(other.domainMetadata)\n        && fileSizeHistogram.equals(other.fileSizeHistogram);\n  }\n\n  private static int getSchemaIndex(String fieldName) {\n    return CRC_FILE_SCHEMA.indexOf(fieldName);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checksum/ChecksumReader.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checksum;\n\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Utility method to load protocol and metadata from the Delta log checksum files. */\npublic class ChecksumReader {\n  private static final Logger logger = LoggerFactory.getLogger(ChecksumReader.class);\n\n  /**\n   * Load the CRCInfo from the provided checksum file.\n   *\n   * @param engine the engine to use for reading the checksum file\n   * @param checkSumFile the file status of the checksum file to read\n   * @return Optional {@link CRCInfo} containing the information included in the checksum file, such\n   *     as protocol, metadata.\n   */\n  public static Optional<CRCInfo> tryReadChecksumFile(Engine engine, FileStatus checkSumFile) {\n    try (CloseableIterator<ColumnarBatch> iter =\n        engine\n            .getJsonHandler()\n            .readJsonFiles(\n                singletonCloseableIterator(checkSumFile),\n                CRCInfo.CRC_FILE_READ_SCHEMA,\n                Optional.empty())) {\n      // We do this instead of iterating through the rows or using `getSingularRow` so we\n      // can use the existing fromColumnVector methods in Protocol, Metadata, Format etc\n      if (!iter.hasNext()) {\n        logger.warn(\"Checksum file is empty: {}\", checkSumFile.getPath());\n        return Optional.empty();\n      }\n\n      ColumnarBatch batch = iter.next();\n      if (batch.getSize() != 1) {\n        String msg = \"Expected exactly one row in the checksum file {}, found {} rows\";\n        logger.warn(msg, checkSumFile.getPath(), batch.getSize());\n        return Optional.empty();\n      }\n\n      long crcVersion = FileNames.checksumVersion(new Path(checkSumFile.getPath()));\n\n      return CRCInfo.fromColumnarBatch(crcVersion, batch, 0 /* rowId */, checkSumFile.getPath());\n    } catch (Exception e) {\n      // This can happen when the version does not have a checksum file\n      logger.warn(\"Failed to read checksum file {}\", checkSumFile.getPath(), e);\n      return Optional.empty();\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checksum/ChecksumUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checksum;\n\nimport static io.delta.kernel.internal.actions.SingleAction.CHECKPOINT_SCHEMA;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.replay.ActionWrapper;\nimport io.delta.kernel.internal.replay.ActionsIterator;\nimport io.delta.kernel.internal.replay.CreateCheckpointIterator;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.stats.FileSizeHistogram;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Utility methods for computing and writing checksums for Delta tables. */\npublic class ChecksumUtils {\n\n  private ChecksumUtils() {}\n\n  private static final Logger logger = LoggerFactory.getLogger(ChecksumUtils.class);\n\n  private static final int PROTOCOL_INDEX = CHECKPOINT_SCHEMA.indexOf(\"protocol\");\n  private static final int METADATA_INDEX = CHECKPOINT_SCHEMA.indexOf(\"metaData\");\n  // TODO: simplify the schema to only read size from addFile.\n  private static final int ADD_INDEX = CHECKPOINT_SCHEMA.indexOf(\"add\");\n  private static final int REMOVE_INDEX = CHECKPOINT_SCHEMA.indexOf(\"remove\");\n  private static final int DOMAIN_METADATA_INDEX = CHECKPOINT_SCHEMA.indexOf(\"domainMetadata\");\n  private static final int ADD_SIZE_INDEX = AddFile.FULL_SCHEMA.indexOf(\"size\");\n  private static final int REMOVE_SIZE_INDEX = RemoveFile.FULL_SCHEMA.indexOf(\"size\");\n  // commitInfo is appended after CHECKPOINT_SCHEMA in incremental read\n  private static final int COMMIT_INFO_INDEX = CHECKPOINT_SCHEMA.length();\n\n  private static final Set<String> INCREMENTAL_SUPPORTED_OPS =\n      Collections.unmodifiableSet(\n          new HashSet<>(\n              Arrays.asList(\n                  \"WRITE\",\n                  \"MERGE\",\n                  \"UPDATE\",\n                  \"DELETE\",\n                  \"OPTIMIZE\",\n                  \"CREATE TABLE\",\n                  \"REPLACE TABLE\",\n                  \"CREATE TABLE AS SELECT\",\n                  \"REPLACE TABLE AS SELECT\",\n                  \"CREATE OR REPLACE TABLE AS SELECT\")));\n\n  /**\n   * Computes the state of a Delta table and writes a checksum file for the provided snapshot's\n   * version. If a checksum file already exists for this version, this method returns without any\n   * changes.\n   *\n   * <p>The checksum file contains table statistics including:\n   *\n   * <ul>\n   *   <li>Total table size in bytes\n   *   <li>Total number of files\n   *   <li>File size histogram\n   *   <li>Domain metadata information\n   * </ul>\n   *\n   * <p>Note: For very large tables, this operation may be expensive as it requires scanning the\n   * table state to compute statistics.\n   *\n   * @param engine The Engine instance used to access the underlying storage\n   * @param logSegmentAtVersion The LogSegment instance of the table at a specific version\n   * @throws IOException If an I/O error occurs during checksum computation or writing\n   */\n  public static void computeStateAndWriteChecksum(Engine engine, LogSegment logSegmentAtVersion)\n      throws IOException {\n    requireNonNull(engine);\n    requireNonNull(logSegmentAtVersion);\n\n    // Check for existing checksum for this version\n    Optional<Long> lastSeenCrcVersion =\n        logSegmentAtVersion\n            .getLastSeenChecksum()\n            .map(file -> FileNames.getFileVersion(new Path(file.getPath())));\n\n    if (lastSeenCrcVersion.isPresent()\n        && lastSeenCrcVersion.get().equals(logSegmentAtVersion.getVersion())) {\n      logger.info(\"Checksum file already exists for version {}\", logSegmentAtVersion.getVersion());\n      return;\n    }\n\n    Optional<CRCInfo> lastSeenCrcInfo =\n        logSegmentAtVersion\n            .getLastSeenChecksum()\n            .flatMap(file -> ChecksumReader.tryReadChecksumFile(engine, file));\n    // Try to build CRC incrementally if possible\n    Optional<CRCInfo> incrementallyBuiltCrc =\n        lastSeenCrcInfo.isPresent()\n            ? buildCrcInfoIncrementally(lastSeenCrcInfo.get(), engine, logSegmentAtVersion)\n            : Optional.empty();\n\n    // Use incrementally built CRC if available, otherwise do full log replay\n    CRCInfo crcInfo =\n        incrementallyBuiltCrc.isPresent()\n            ? incrementallyBuiltCrc.get()\n            : buildCrcInfoWithFullLogReplay(engine, logSegmentAtVersion);\n\n    ChecksumWriter checksumWriter = new ChecksumWriter(logSegmentAtVersion.getLogPath());\n    checksumWriter.writeCheckSum(engine, crcInfo);\n  }\n\n  /**\n   * Builds CRC info by replaying the full log.\n   *\n   * @param engine The engine instance\n   * @param logSegmentAtVersion The log segment at the target version\n   * @return The complete CRC info\n   */\n  private static CRCInfo buildCrcInfoWithFullLogReplay(\n      Engine engine, LogSegment logSegmentAtVersion) throws IOException {\n\n    StateTracker state = new StateTracker();\n\n    // Process logs and update state\n    try (CreateCheckpointIterator checkpointIterator =\n        new CreateCheckpointIterator(\n            // Set minFileRetentionTimestampMillis to infinite future to skip all removed files\n            engine, logSegmentAtVersion, Instant.ofEpochMilli(Long.MAX_VALUE).toEpochMilli())) {\n\n      // Process all checkpoint batches\n      while (checkpointIterator.hasNext()) {\n        FilteredColumnarBatch filteredBatch = checkpointIterator.next();\n        ColumnarBatch batch = filteredBatch.getData();\n        Optional<ColumnVector> selectionVector = filteredBatch.getSelectionVector();\n        final int rowCount = batch.getSize();\n\n        ColumnVector metadataVector = batch.getColumnVector(METADATA_INDEX);\n        ColumnVector protocolVector = batch.getColumnVector(PROTOCOL_INDEX);\n        ColumnVector removeVector = batch.getColumnVector(REMOVE_INDEX);\n        ColumnVector addVector = batch.getColumnVector(ADD_INDEX);\n        ColumnVector domainMetadataVector = batch.getColumnVector(DOMAIN_METADATA_INDEX);\n        // Process all selected rows in a single pass for optimal performance\n        for (int i = 0; i < rowCount; i++) {\n          // Fields referenced in the lambda should be effectively final.\n          int rowId = i;\n          boolean isSelected =\n              selectionVector\n                  .map(vec -> !vec.isNullAt(rowId) && vec.getBoolean(rowId))\n                  .orElse(true);\n          if (!isSelected) continue;\n          // Step 1: Ensure there are no remove records\n          // We set minFileRetentionTimestampMillis to infinite future to skip all removed files,\n          // so there should be no remove actions.\n          checkState(\n              removeVector.isNullAt(i),\n              \"unexpected remove row found when \"\n                  + \"setting minFileRetentionTimestampMillis to infinite future\");\n          // Step 2: Process add files, domain metadata, metadata, and protocol\n          processAddRecord(addVector, state, i);\n          processDomainMetadataRecord(domainMetadataVector, state, i);\n          processMetadataRecord(metadataVector, state, i);\n          processProtocolRecord(protocolVector, state, i);\n        }\n      }\n    }\n    // Get final metadata and protocol\n    Metadata finalMetadata =\n        state.metadataFromLog.orElseThrow(() -> new IllegalStateException(\"No metadata found\"));\n\n    Protocol finalProtocol =\n        state.protocolFromLog.orElseThrow(() -> new IllegalStateException(\"No protocol found\"));\n\n    // Filter to only non-removed domain metadata\n    Set<DomainMetadata> finalDomainMetadata = getNonRemovedDomainMetadata(state);\n\n    return new CRCInfo(\n        logSegmentAtVersion.getVersion(),\n        finalMetadata,\n        finalProtocol,\n        state.tableSizeByte.longValue(),\n        state.fileCount.longValue(),\n        Optional.empty(),\n        Optional.of(finalDomainMetadata),\n        Optional.of(state.addedFileSizeHistogram));\n  }\n\n  /**\n   * Attempts to build CRC info incrementally from the last seen checksum. Falls back if incremental\n   * computation is not possible.\n   *\n   * @param lastSeenCrcInfo The last available CRC info to build upon\n   * @param engine The engine to use for file operations\n   * @param logSegment The log segment to process\n   * @return Optional containing the new CRC info, or empty if fallback is needed\n   */\n  private static Optional<CRCInfo> buildCrcInfoIncrementally(\n      CRCInfo lastSeenCrcInfo, Engine engine, LogSegment logSegment) throws IOException {\n    long startTime = System.currentTimeMillis();\n    // Can only build incrementally if we have domain metadata and file size histogram\n    if (!lastSeenCrcInfo.getDomainMetadata().isPresent()) {\n      logger.info(\n          \"Falling back to full replay after {}ms: detected current crc missing domain metadata.\",\n          System.currentTimeMillis() - startTime);\n      return Optional.empty();\n    }\n    if (!lastSeenCrcInfo.getFileSizeHistogram().isPresent()) {\n      logger.info(\n          \"Falling back to full replay after {}ms: \"\n              + \"detected current crc missing file size histogram.\",\n          System.currentTimeMillis() - startTime);\n      return Optional.empty();\n    }\n\n    // Initialize state tracking\n    StateTracker state = new StateTracker();\n\n    // TODO: use compacted logs.\n    List<FileStatus> deltaFiles =\n        logSegment.getDeltas().stream()\n            .filter(\n                file ->\n                    FileNames.getFileVersion(new Path(file.getPath()))\n                        > lastSeenCrcInfo.getVersion())\n            .sorted(\n                Comparator.comparingLong(\n                    (FileStatus file) -> FileNames.getFileVersion(new Path(file.getPath()))))\n            .collect(Collectors.toList());\n    validateDeltaContinuity(deltaFiles, lastSeenCrcInfo.getVersion());\n    Collections.reverse(deltaFiles);\n    // Create iterator for delta files newer than last CRC\n    StructType readSchema = CHECKPOINT_SCHEMA.add(\"commitInfo\", CommitInfo.FULL_SCHEMA);\n    try (CloseableIterator<ActionWrapper> iterator =\n        new ActionsIterator(engine, deltaFiles, readSchema, java.util.Optional.empty())) {\n      Optional<Long> lastSeenVersion = Optional.empty();\n      while (iterator.hasNext()) {\n        ActionWrapper currentAction = iterator.next();\n        ColumnarBatch batch = currentAction.getColumnarBatch();\n        final int rowCount = batch.getSize();\n        if (rowCount == 0) {\n          continue;\n        }\n        ColumnVector addVector = batch.getColumnVector(ADD_INDEX);\n        ColumnVector removeVector = batch.getColumnVector(REMOVE_INDEX);\n        ColumnVector metadataVector = batch.getColumnVector(METADATA_INDEX);\n        ColumnVector protocolVector = batch.getColumnVector(PROTOCOL_INDEX);\n        ColumnVector domainMetadataVector = batch.getColumnVector(DOMAIN_METADATA_INDEX);\n        ColumnVector commitInfoVector = batch.getColumnVector(COMMIT_INFO_INDEX);\n\n        for (int i = 0; i < rowCount; i++) {\n          long newVersion = currentAction.getVersion();\n\n          // Detect version change\n          if (!lastSeenVersion.isPresent() || newVersion != lastSeenVersion.get()) {\n            // New version detected - current row must be commit info\n            if (commitInfoVector.isNullAt(i)) {\n              logger.info(\n                  \"Falling back to full replay: first row of version {} is not commit info\",\n                  newVersion);\n              return Optional.empty();\n            }\n\n            CommitInfo commitInfo = CommitInfo.fromColumnVector(commitInfoVector, i);\n            if (commitInfo == null\n                || !commitInfo\n                    .getOperation()\n                    .filter(INCREMENTAL_SUPPORTED_OPS::contains)\n                    .isPresent()) {\n              logger.info(\n                  \"Falling back to full replay after {}ms: \"\n                      + \"unsupported operation '{}' for version {}\",\n                  System.currentTimeMillis() - startTime,\n                  commitInfo != null ? commitInfo.getOperation().orElse(\"null\") : \"null\",\n                  newVersion);\n              return Optional.empty();\n            }\n\n            lastSeenVersion = Optional.of(newVersion);\n            continue;\n          }\n\n          // Process the row\n          if (!addVector.isNullAt(i)) {\n            processAddRecord(addVector, state, i);\n          }\n\n          // Process remove file records\n          if (!removeVector.isNullAt(i)) {\n            ColumnVector sizeVector = removeVector.getChild(REMOVE_SIZE_INDEX);\n            if (sizeVector.isNullAt(i)) {\n              logger.info(\n                  \"Falling back to full replay after {}ms: \"\n                      + \"detected remove without file size in version {}\",\n                  System.currentTimeMillis() - startTime,\n                  newVersion);\n              return Optional.empty();\n            }\n            long fileSize = sizeVector.getLong(i);\n            state.tableSizeByte.add(-fileSize);\n            state.removedFileSizeHistogram.insert(fileSize);\n            state.fileCount.decrement();\n          }\n\n          // Process domain metadata, protocol, and metadata\n          processDomainMetadataRecord(domainMetadataVector, state, i);\n          processMetadataRecord(metadataVector, state, i);\n          processProtocolRecord(protocolVector, state, i);\n        }\n      }\n    }\n\n    // Merge with existing domain metadata\n    lastSeenCrcInfo\n        .getDomainMetadata()\n        .get()\n        .forEach(\n            dm -> {\n              if (!state.domainMetadataMap.containsKey(dm.getDomain())) {\n                state.domainMetadataMap.put(dm.getDomain(), dm);\n              }\n            });\n\n    // Filter to only non-removed domain metadata\n    Set<DomainMetadata> finalDomainMetadata = getNonRemovedDomainMetadata(state);\n    logger.info(\n        \"Successfully completed incremental CRC computation in {} ms\",\n        System.currentTimeMillis() - startTime);\n    // Build and return the new CRC info\n    return Optional.of(\n        new CRCInfo(\n            logSegment.getVersion(),\n            state.metadataFromLog.orElseGet(lastSeenCrcInfo::getMetadata),\n            state.protocolFromLog.orElseGet(lastSeenCrcInfo::getProtocol),\n            state.tableSizeByte.longValue() + lastSeenCrcInfo.getTableSizeBytes(),\n            state.fileCount.longValue() + lastSeenCrcInfo.getNumFiles(),\n            Optional.empty(),\n            Optional.of(finalDomainMetadata),\n            Optional.of(\n                state\n                    .addedFileSizeHistogram\n                    .plus(lastSeenCrcInfo.getFileSizeHistogram().get())\n                    .minus(state.removedFileSizeHistogram))));\n  }\n\n  /** Processes an add file record and updates the state tracker. */\n  private static void processAddRecord(ColumnVector addVector, StateTracker state, int rowId) {\n    if (!addVector.isNullAt(rowId)) {\n      ColumnVector sizeVector = addVector.getChild(ADD_SIZE_INDEX);\n      checkState(!sizeVector.isNullAt(rowId), \"Add record has null file size\");\n\n      long fileSize = sizeVector.getLong(rowId);\n      checkState(fileSize >= 0, \"Add record has negative file size: \" + fileSize);\n\n      state.tableSizeByte.add(fileSize);\n      state.addedFileSizeHistogram.insert(fileSize);\n      state.fileCount.increment();\n    }\n  }\n\n  /** Processes a domain metadata record and updates the state tracker. */\n  private static void processDomainMetadataRecord(\n      ColumnVector domainMetadataVector, StateTracker state, int rowId) {\n    if (!domainMetadataVector.isNullAt(rowId)) {\n      DomainMetadata domainMetadata = DomainMetadata.fromColumnVector(domainMetadataVector, rowId);\n      if (!state.domainMetadataMap.containsKey(domainMetadata.getDomain())) {\n        state.domainMetadataMap.put(domainMetadata.getDomain(), domainMetadata);\n      }\n    }\n  }\n\n  /** Processes a metadata record and updates the state tracker. */\n  private static void processMetadataRecord(\n      ColumnVector metadataVector, StateTracker state, int rowId) {\n    if (!metadataVector.isNullAt(rowId) && !state.metadataFromLog.isPresent()) {\n      Metadata metadata = Metadata.fromColumnVector(metadataVector, rowId);\n      checkState(metadata != null, \"Metadata is null\");\n      state.metadataFromLog = Optional.of(metadata);\n    }\n  }\n\n  /** Processes a protocol record and updates the state tracker. */\n  private static void processProtocolRecord(\n      ColumnVector protocolVector, StateTracker state, int rowId) {\n    if (!protocolVector.isNullAt(rowId) && !state.protocolFromLog.isPresent()) {\n      Protocol protocol = Protocol.fromColumnVector(protocolVector, rowId);\n      checkState(protocol != null, \"Protocol is null\");\n      state.protocolFromLog = Optional.of(protocol);\n    }\n  }\n\n  /** Get non-removed domain metadata. */\n  private static Set<DomainMetadata> getNonRemovedDomainMetadata(StateTracker state) {\n    return state.domainMetadataMap.values().stream()\n        .filter(dm -> !dm.isRemoved())\n        .collect(Collectors.toSet());\n  }\n\n  /** Class for tracking state during log processing. */\n  private static class StateTracker {\n    Optional<Metadata> metadataFromLog = Optional.empty();\n    Optional<Protocol> protocolFromLog = Optional.empty();\n    LongAdder tableSizeByte = new LongAdder();\n    LongAdder fileCount = new LongAdder();\n    FileSizeHistogram addedFileSizeHistogram = FileSizeHistogram.createDefaultHistogram();\n    FileSizeHistogram removedFileSizeHistogram = FileSizeHistogram.createDefaultHistogram();\n    Map<String, DomainMetadata> domainMetadataMap = new HashMap<>();\n  }\n\n  private static void validateDeltaContinuity(List<FileStatus> deltas, long checksumVersion) {\n    if (deltas.isEmpty()) {\n      return;\n    }\n\n    long expectedVersion = checksumVersion + 1;\n    for (FileStatus delta : deltas) {\n      long version = FileNames.getFileVersion(new Path(delta.getPath()));\n      if (version != expectedVersion) {\n        throw new IllegalStateException(\n            String.format(\n                \"Gap detected in delta files: expected version %d, found %d\",\n                expectedVersion, version));\n      }\n      expectedVersion++;\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/checksum/ChecksumWriter.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checksum;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport java.io.IOException;\nimport java.nio.file.FileAlreadyExistsException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Writers for writing checksum files from a snapshot */\npublic class ChecksumWriter {\n\n  private static final Logger logger = LoggerFactory.getLogger(ChecksumWriter.class);\n\n  private final Path logPath;\n\n  public ChecksumWriter(Path logPath) {\n    this.logPath = requireNonNull(logPath);\n  }\n\n  /** Writes a checksum file */\n  public void writeCheckSum(Engine engine, CRCInfo crcInfo) throws IOException {\n    Path newChecksumPath = FileNames.checksumFile(logPath, crcInfo.getVersion());\n    logger.info(\"Writing checksum file to path: {}\", newChecksumPath);\n\n    try {\n      wrapEngineExceptionThrowsIO(\n          () -> {\n            engine\n                .getJsonHandler()\n                .writeJsonFileAtomically(\n                    newChecksumPath.toString(),\n                    singletonCloseableIterator(crcInfo.toRow()),\n                    false /* overwrite */);\n            logger.info(\"Write checksum file `{}` succeeds\", newChecksumPath);\n            return null;\n          },\n          \"Write checksum file `%s`\",\n          newChecksumPath);\n    } catch (FileAlreadyExistsException e) {\n      logger.info(\"Checksum file already exists for version {}\", crcInfo.getVersion());\n      // Checksum file has been created while we were computing it.\n      // This is fine - the checksum now exists, which was our goal.\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/clustering/ClusteringMetadataDomain.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.clustering;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.metadatadomain.JsonMetadataDomain;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/** Represents the metadata domain for clustering. */\npublic final class ClusteringMetadataDomain extends JsonMetadataDomain {\n\n  public static final String DOMAIN_NAME = \"delta.clustering\";\n\n  /**\n   * Constructs a ClusteringMetadataDomain with the clustering columns.\n   *\n   * @param clusteringColumns the columns used for clustering. If column mapping is enabled, use the\n   *     physical name assigned; otherwise, use the logical column name.\n   */\n  public static ClusteringMetadataDomain fromClusteringColumns(List<Column> clusteringColumns) {\n    return new ClusteringMetadataDomain(\n        clusteringColumns.stream()\n            .map(column -> Arrays.asList(column.getNames()))\n            .collect(Collectors.toList()));\n  }\n\n  /**\n   * Creates an instance of {@link ClusteringMetadataDomain} from a JSON configuration string.\n   *\n   * @param json the JSON configuration string\n   */\n  public static ClusteringMetadataDomain fromJsonConfiguration(String json) {\n    return JsonMetadataDomain.fromJsonConfiguration(json, ClusteringMetadataDomain.class);\n  }\n\n  /**\n   * Creates an optional instance of {@link ClusteringMetadataDomain} from a {@link SnapshotImpl} if\n   * present.\n   *\n   * @param snapshot the snapshot instance\n   */\n  // TODO: Add the test coverage for this function in the integration test.\n  public static Optional<ClusteringMetadataDomain> fromSnapshot(SnapshotImpl snapshot) {\n    return JsonMetadataDomain.fromSnapshot(snapshot, ClusteringMetadataDomain.class, DOMAIN_NAME);\n  }\n\n  /**\n   * The column names used for clustering. If column mapping is enabled, we use physical column\n   * names, otherwise we would store its logical column names. Stored as a List of Lists to avoid\n   * customized serialization and deserialization logic.\n   */\n  @JsonProperty(\"clusteringColumns\")\n  private final List<List<String>> clusteringColumns;\n\n  @JsonCreator\n  private ClusteringMetadataDomain(\n      @JsonProperty(\"clusteringColumns\") List<List<String>> physicalClusteringColumns) {\n    this.clusteringColumns = physicalClusteringColumns;\n  }\n\n  @Override\n  public String getDomainName() {\n    return DOMAIN_NAME;\n  }\n\n  @JsonIgnore\n  public List<Column> getClusteringColumns() {\n    return clusteringColumns.stream()\n        .map(list -> new Column(list.toArray(new String[0])))\n        .collect(Collectors.toList());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/clustering/ClusteringUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.clustering;\n\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport java.util.List;\n\npublic class ClusteringUtils {\n\n  private ClusteringUtils() {\n    // Empty private constructor to prevent instantiation\n  }\n\n  /**\n   * Get the domain metadata for the clustering columns. If column mapping is enabled, pass the list\n   * of physical names assigned; otherwise, use the logical column names.\n   */\n  public static DomainMetadata getClusteringDomainMetadata(List<Column> clusteringColumns) {\n    ClusteringMetadataDomain clusteringMetadataDomain =\n        ClusteringMetadataDomain.fromClusteringColumns(clusteringColumns);\n    return clusteringMetadataDomain.toDomainMetadata();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/columndefaults/ColumnDefaults.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.columndefaults;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.data.TransactionStateRow;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\nimport java.util.stream.Stream;\n\n/**\n * Utilities class for TableFeature \"allowColumnDefaults\". NOTE: As of Aug 2025, kernel only\n * supports reading Delta tables with the table feature, or modifying table metadata. Writing actual\n * data to the table is not allowed.\n */\npublic class ColumnDefaults {\n\n  private static final String DEFAULT_VALUE_METADATA_KEY = \"CURRENT_DEFAULT\";\n\n  /** Don't allow data writes to tables with default values */\n  public static void blockWriteIfEnabled(Row transactionState) {\n    if (extractFieldsWithDefaultValues(TransactionStateRow.getLogicalSchema(transactionState))\n        .findAny()\n        .isPresent()) {\n      throw new UnsupportedOperationException(\n          \"Writing with Column Default values is not supported yet.\");\n    }\n  }\n\n  /**\n   * Validate Column Default values in the provided metadata. Kernel only supports literal default\n   * values. See {validateLiteral}.\n   *\n   * @param schema target table schema\n   * @param isEnabled When the feature is disabled, no column default is allowed.\n   * @param isIcebergCompatV3Enabled Kernel currently requires IcebergCompatV3 to be enabled when\n   *     using Column Defaults\n   * @throws KernelException when the table contains invalid default value\n   */\n  public static void validateSchema(\n      StructType schema, boolean isEnabled, boolean isIcebergCompatV3Enabled) {\n    if (isEnabled && !isIcebergCompatV3Enabled) {\n      throw DeltaErrors.defaultValueRequireIcebergV3();\n    }\n    Stream<StructField> defaultValues = extractFieldsWithDefaultValues(schema);\n    if (!isEnabled) {\n      if (defaultValues.findAny().isPresent()) {\n        throw DeltaErrors.defaultValueRequiresTableFeature();\n      }\n    } else {\n      // This check will be relaxed once kernel supports default values with expression\n      defaultValues.forEach(\n          field -> {\n            String defaultValue = getRawDefaultValue(field);\n            try {\n              validateLiteral(field.getDataType(), defaultValue);\n            } catch (IllegalArgumentException e) {\n              throw DeltaErrors.nonLiteralDefaultValue(defaultValue);\n            } catch (UnsupportedOperationException e) {\n              throw DeltaErrors.unsupportedDataTypeForDefaultValue(\n                  field.getName(), field.getDataType().toString());\n            }\n          });\n    }\n  }\n\n  /**\n   * Validate that the schema only contains literal default values as a requirement of\n   * IcebergCompat.\n   *\n   * @param schema table schema\n   */\n  public static void validateSchemaForIcebergCompat(StructType schema, String compatVersion) {\n    extractFieldsWithDefaultValues(schema)\n        .forEach(\n            field -> {\n              String defaultValue = getRawDefaultValue(field);\n              try {\n                validateLiteral(field.getDataType(), defaultValue);\n              } catch (IllegalArgumentException e) {\n                throw DeltaErrors.icebergCompatRequiresLiteralDefaultValue(\n                    compatVersion, field.getDataType(), defaultValue);\n              }\n            });\n  }\n\n  private static Stream<StructField> extractFieldsWithDefaultValues(StructType schema) {\n    return new SchemaIterable(schema)\n        .stream()\n            .map(SchemaIterable.SchemaElement::getField)\n            .filter(f -> getRawDefaultValue(f) != null);\n  }\n\n  public static String getRawDefaultValue(StructField field) {\n    return field.getMetadata().getString(DEFAULT_VALUE_METADATA_KEY);\n  }\n\n  /**\n   * Validate that the provided default value is a literal (not an expression) and can be cast to\n   * the given data type. We only support a limited set of literals. Example:\n   *\n   * <ul>\n   *   <li>'CURRENT_VALUE()' is a valid String literal. CURRENT_VALUE (no quotes) is not.\n   *   <li>4.95 and '4.95' are both valid Double/Float literals.\n   *   <li>'2022-01-01' is a valid Date literal, '09/01/2022' is not.\n   * </ul>\n   *\n   * @throws IllegalArgumentException if the value is not a literal value matching the data type\n   * @throws UnsupportedOperationException when kernel does not support column defaults for the data\n   *     type\n   */\n  private static void validateLiteral(DataType type, String value) {\n    String stripped = stripQuotes(value, false);\n    if ((type instanceof StringType || type instanceof BinaryType)) {\n      // String literals are required to be enclosed in quotes\n      stripQuotes(value, true);\n    } else if (type instanceof LongType) {\n      Long.parseLong(stripped);\n    } else if (type instanceof IntegerType) {\n      Integer.parseInt(stripped);\n    } else if (type instanceof ShortType) {\n      Short.parseShort(stripped);\n    } else if (type instanceof FloatType) {\n      Float.parseFloat(stripped);\n    } else if (type instanceof DoubleType) {\n      Double.parseDouble(stripped);\n    } else if (type instanceof DecimalType) {\n      DecimalType dtype = (DecimalType) type;\n      BigDecimal input = new BigDecimal(stripped);\n      if (input.scale() > dtype.getScale() || input.precision() > dtype.getPrecision()) {\n        throw new IllegalArgumentException(\"invalid default value \" + value + \" for \" + type);\n      }\n    } else if (type instanceof BooleanType) {\n      Boolean.parseBoolean(stripped);\n    } else if (type instanceof DateType) {\n      try {\n        LocalDate.parse(stripped, DateTimeFormatter.ISO_LOCAL_DATE);\n      } catch (DateTimeParseException e) {\n        throw new IllegalArgumentException(e);\n      }\n    } else if (type instanceof TimestampType) {\n      try {\n        OffsetDateTime.parse(stripped, DateTimeFormatter.ISO_DATE_TIME);\n      } catch (DateTimeParseException e) {\n        throw new IllegalArgumentException(e);\n      }\n    } else if (type instanceof TimestampNTZType) {\n      try {\n        LocalDateTime.parse(stripped, DateTimeFormatter.ISO_LOCAL_DATE_TIME);\n      } catch (DateTimeParseException e) {\n        throw new IllegalArgumentException(e);\n      }\n    } else {\n      throw new UnsupportedOperationException(\n          \"Kernel does not support column defaults for \" + type.toString());\n    }\n  }\n\n  /**\n   * Remove the quotes from input string.\n   *\n   * @param input input to remove quotes\n   * @param require require the input to have quotes\n   * @return string with enclosing quotes removed\n   */\n  private static String stripQuotes(String input, boolean require) {\n    if (input.length() > 1\n        && ((input.charAt(0) == '\\'' && input.charAt(input.length() - 1) == '\\'')\n            || (input.charAt(0) == '\"' && input.charAt(input.length() - 1) == '\"'))) {\n      return input.substring(1, input.length() - 1);\n    }\n    if (require) {\n      throw new IllegalArgumentException(\"String literal not enclosed in quotes: \" + input);\n    }\n    return input;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/commit/DefaultFileSystemManagedTableOnlyCommitter.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.commit;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\n\nimport io.delta.kernel.commit.CommitFailedException;\nimport io.delta.kernel.commit.CommitMetadata;\nimport io.delta.kernel.commit.CommitResponse;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaErrorsInternal;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.files.ParsedPublishedDeltaData;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.nio.file.FileAlreadyExistsException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class DefaultFileSystemManagedTableOnlyCommitter implements Committer {\n\n  private static final Logger logger =\n      LoggerFactory.getLogger(DefaultFileSystemManagedTableOnlyCommitter.class);\n\n  public static final DefaultFileSystemManagedTableOnlyCommitter INSTANCE =\n      new DefaultFileSystemManagedTableOnlyCommitter();\n\n  private DefaultFileSystemManagedTableOnlyCommitter() {}\n\n  @Override\n  public CommitResponse commit(\n      Engine engine, CloseableIterator<Row> finalizedActions, CommitMetadata commitMetadata)\n      throws CommitFailedException {\n    commitMetadata.getReadProtocolOpt().ifPresent(this::validateProtocol);\n    commitMetadata.getNewProtocolOpt().ifPresent(this::validateProtocol);\n\n    final String jsonCommitFile =\n        FileNames.deltaFile(commitMetadata.getDeltaLogDirPath(), commitMetadata.getVersion());\n\n    logger.info(\"Attempting to commit {}\", jsonCommitFile);\n\n    try {\n      return wrapEngineExceptionThrowsIO(\n          () -> {\n            engine\n                .getJsonHandler()\n                .writeJsonFileAtomically(jsonCommitFile, finalizedActions, false /* overwrite */);\n\n            final FileStatus writtenDeltaFileStatus =\n                engine.getFileSystemClient().getFileStatus(jsonCommitFile);\n\n            return new CommitResponse(\n                ParsedPublishedDeltaData.forFileStatus(writtenDeltaFileStatus));\n          },\n          String.format(\"Write file actions to JSON log file `%s`\", jsonCommitFile));\n    } catch (FileAlreadyExistsException e) {\n      throw new CommitFailedException(\n          true /* retryable */,\n          true /* conflict */,\n          \"Concurrent write detected for version \" + commitMetadata.getVersion(),\n          e);\n    } catch (IOException e) {\n      throw new CommitFailedException(\n          true /* retryable */,\n          false /* conflict */,\n          \"Failed to write commit file due to I/O error: \" + e.getMessage(),\n          e);\n    }\n  }\n\n  private void validateProtocol(Protocol protocol) {\n    if (TableFeatures.isCatalogManagedSupported(protocol)) {\n      throw DeltaErrorsInternal.defaultCommitterDoesNotSupportCatalogManagedTables();\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/commitrange/CommitRangeBuilderImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.commitrange;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.CommitRangeBuilder;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaErrorsInternal;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.files.LogDataUtils;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * An implementation of {@link CommitRangeBuilder}.\n *\n * <p>Note: The primary responsibility of this class is to take input, validate that input, and then\n * create a {@link CommitRange} instance with the specified configuration.\n */\npublic class CommitRangeBuilderImpl implements CommitRangeBuilder {\n\n  public static class Context {\n    public final String unresolvedPath;\n    public final CommitBoundary startBoundary;\n    public Optional<CommitBoundary> endBoundaryOpt = Optional.empty();\n    public List<ParsedLogData> logDatas = Collections.emptyList();\n    public Optional<Long> maxCatalogVersion = Optional.empty();\n\n    public Context(String unresolvedPath, CommitBoundary startBoundary) {\n      this.unresolvedPath = requireNonNull(unresolvedPath, \"unresolvedPath is null\");\n      this.startBoundary = requireNonNull(startBoundary, \"startBoundary is null\");\n    }\n  }\n\n  private final Context ctx;\n\n  public CommitRangeBuilderImpl(String unresolvedPath, CommitBoundary startBoundary) {\n    ctx = new Context(unresolvedPath, startBoundary);\n  }\n\n  ///////////////////////////////////////\n  // Public CommitRangeBuilder Methods //\n  ///////////////////////////////////////\n\n  @Override\n  public CommitRangeBuilderImpl withEndBoundary(CommitBoundary endBoundary) {\n    ctx.endBoundaryOpt = Optional.of(requireNonNull(endBoundary, \"endBoundary is null\"));\n    return this;\n  }\n\n  @Override\n  public CommitRangeBuilderImpl withLogData(List<ParsedLogData> logData) {\n    ctx.logDatas = requireNonNull(logData, \"logData is null\");\n    return this;\n  }\n\n  @Override\n  public CommitRangeBuilderImpl withMaxCatalogVersion(long version) {\n    checkArgument(version >= 0, \"maxCatalogVersion must be >= 0, but got: %d\", version);\n    ctx.maxCatalogVersion = Optional.of(version);\n    return this;\n  }\n\n  @Override\n  public CommitRange build(Engine engine) {\n    validateInputOnBuild();\n    return new CommitRangeFactory(engine, ctx).create(engine);\n  }\n\n  ////////////////////////////\n  // Private Helper Methods //\n  ////////////////////////////\n\n  private void validateInputOnBuild() {\n    // Validate that start boundary is less than or equal to end boundary if end boundary is\n    // provided\n    if (ctx.endBoundaryOpt.isPresent()) {\n      CommitBoundary startBoundary = ctx.startBoundary;\n      CommitBoundary endBoundary = ctx.endBoundaryOpt.get();\n\n      // If both are version-based, compare versions\n      if (startBoundary.isVersion() && endBoundary.isVersion()) {\n        checkArgument(\n            startBoundary.getVersion() <= endBoundary.getVersion(),\n            \"startVersion must be <= endVersion\");\n      }\n      // If both are timestamp-based, compare timestamps\n      else if (startBoundary.isTimestamp() && endBoundary.isTimestamp()) {\n        checkArgument(\n            startBoundary.getTimestamp() <= endBoundary.getTimestamp(),\n            \"startTimestamp must be <= endTimestamp\");\n      }\n      // Mixed types are allowed but will need runtime resolution\n    }\n\n    // Validate max catalog version constraints if provided\n    if (ctx.maxCatalogVersion.isPresent()) {\n      long maxVersion = ctx.maxCatalogVersion.get();\n\n      // Validate start boundary against max catalog version\n      if (ctx.startBoundary.isVersion()) {\n        checkArgument(\n            ctx.startBoundary.getVersion() <= maxVersion,\n            String.format(\n                \"startVersion (%d) must be <= maxCatalogVersion (%d)\",\n                ctx.startBoundary.getVersion(), maxVersion));\n      } else if (ctx.startBoundary.isTimestamp()) {\n        long latestSnapshotVersion =\n            ((SnapshotImpl) ctx.startBoundary.getLatestSnapshot()).getVersion();\n        if (latestSnapshotVersion != maxVersion) {\n          throw DeltaErrorsInternal.invalidLatestSnapshotForMaxCatalogVersion(\n              latestSnapshotVersion, maxVersion);\n        }\n      }\n\n      // Validate end boundary against max catalog version\n      if (ctx.endBoundaryOpt.isPresent()) {\n        CommitBoundary endBoundary = ctx.endBoundaryOpt.get();\n        if (endBoundary.isVersion()) {\n          checkArgument(\n              endBoundary.getVersion() <= maxVersion,\n              String.format(\n                  \"endVersion (%d) must be <= maxCatalogVersion (%d)\",\n                  endBoundary.getVersion(), maxVersion));\n        } else if (endBoundary.isTimestamp()) {\n          long latestSnapshotVersion =\n              ((SnapshotImpl) endBoundary.getLatestSnapshot()).getVersion();\n          if (latestSnapshotVersion != maxVersion) {\n            throw DeltaErrorsInternal.invalidLatestSnapshotForMaxCatalogVersion(\n                latestSnapshotVersion, maxVersion);\n          }\n        }\n      }\n\n      // Validate logData ends with maxCatalogVersion when no end boundary is provided\n      if (!ctx.endBoundaryOpt.isPresent() && !ctx.logDatas.isEmpty()) {\n        long lastLogDataVersion = ctx.logDatas.get(ctx.logDatas.size() - 1).getVersion();\n        checkArgument(\n            lastLogDataVersion == maxVersion,\n            String.format(\n                \"When maxCatalogVersion is specified without an end boundary, the last \"\n                    + \"logData version (%d) must equal maxCatalogVersion (%d)\",\n                lastLogDataVersion, maxVersion));\n      }\n    }\n\n    // Validate logData input\n    LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(ctx.logDatas);\n    LogDataUtils.validateLogDataIsSortedContiguous(ctx.logDatas);\n\n    // Validate that when endVersion and logData are both provided, the logData includes endVersion\n    // This is applicable for catalog-managed tables since the catalog must provide sufficient\n    // ratified commits to cover the requested endVersion\n    if (ctx.endBoundaryOpt.isPresent() && !ctx.logDatas.isEmpty()) {\n      CommitBoundary endBoundary = ctx.endBoundaryOpt.get();\n      if (endBoundary.isVersion()) {\n        long endVersion = endBoundary.getVersion();\n        long lastLogDataVersion = ctx.logDatas.get(ctx.logDatas.size() - 1).getVersion();\n        checkArgument(\n            lastLogDataVersion >= endVersion,\n            String.format(\n                \"When endVersion is specified with logData, the last logData version (%d) \"\n                    + \"must be >= endVersion (%d) to cover the requested range\",\n                lastLogDataVersion, endVersion));\n      }\n      // Note: For timestamp boundaries, we can't validate at build time since the timestamp\n      // needs to be resolved to a version first in CommitRangeFactory\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/commitrange/CommitRangeFactory.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.commitrange;\n\nimport static io.delta.kernel.internal.DeltaErrors.*;\nimport static io.delta.kernel.internal.DeltaErrorsInternal.*;\nimport static io.delta.kernel.internal.DeltaLogActionUtils.listDeltaLogFilesAsIter;\nimport static io.delta.kernel.internal.util.Utils.resolvePath;\n\nimport io.delta.kernel.CommitRangeBuilder;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.files.LogDataUtils;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.files.ParsedDeltaData;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.ListUtils;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nclass CommitRangeFactory {\n\n  private static final Logger logger = LoggerFactory.getLogger(CommitRangeFactory.class);\n\n  private final CommitRangeBuilderImpl.Context ctx;\n  private final Path tablePath;\n  private final Path logPath;\n\n  CommitRangeFactory(Engine engine, CommitRangeBuilderImpl.Context ctx) {\n    this.ctx = ctx;\n    this.tablePath = new Path(resolvePath(engine, ctx.unresolvedPath));\n    this.logPath = new Path(tablePath, \"_delta_log\");\n  }\n\n  CommitRangeImpl create(Engine engine) {\n    List<ParsedCatalogCommitData> ratifiedCommits = getFileBasedRatifiedCommits();\n    long startVersion = resolveStartVersion(engine, ratifiedCommits);\n    Optional<Long> endVersionOpt = resolveEndVersionIfSpecified(engine, ratifiedCommits);\n\n    // Apply maxCatalogVersion constraint\n    if (ctx.maxCatalogVersion.isPresent()) {\n      if (!endVersionOpt.isPresent()) {\n        // When maxCatalogVersion is specified and no end boundary is provided,\n        // the end version should be maxCatalogVersion\n        endVersionOpt = ctx.maxCatalogVersion;\n        logger.info(\n            \"{}: Using maxCatalogVersion {} as end version\", tablePath, endVersionOpt.get());\n      } else {\n        // Check that endVersion is <= maxCatalogVersion\n        if (endVersionOpt.get() > ctx.maxCatalogVersion.get()) {\n          throw DeltaErrors.resolvedEndVersionAfterMaxCatalogVersion(\n              tablePath.toString(), endVersionOpt.get(), ctx.maxCatalogVersion.get());\n        }\n      }\n    }\n\n    validateVersionRange(startVersion, endVersionOpt);\n    logResolvedVersions(startVersion, endVersionOpt);\n    List<ParsedDeltaData> deltas =\n        getDeltasForVersionRangeWithCatalogPriority(\n            engine, startVersion, endVersionOpt, ratifiedCommits);\n    // Once we have a list of deltas, we can resolve endVersion=latestVersion for the default case\n    long endVersion = endVersionOpt.orElseGet(() -> extractLatestVersion(deltas));\n    if (!endVersionOpt.isPresent()) {\n      logger.info(\"{}: Resolved end-boundary to the latest version {}\", tablePath, endVersion);\n    }\n    return new CommitRangeImpl(\n        tablePath, ctx.startBoundary, ctx.endBoundaryOpt, startVersion, endVersion, deltas);\n  }\n\n  private long resolveStartVersion(Engine engine, List<ParsedCatalogCommitData> catalogCommits) {\n    if (ctx.startBoundary.isVersion()) {\n      return ctx.startBoundary.getVersion();\n    } else {\n      logger.info(\n          \"{}: Trying to resolve start-boundary timestamp {} to version\",\n          tablePath,\n          ctx.startBoundary.getTimestamp());\n      return DeltaHistoryManager.getVersionAtOrAfterTimestamp(\n          engine,\n          logPath,\n          ctx.startBoundary.getTimestamp(),\n          (SnapshotImpl) ctx.startBoundary.getLatestSnapshot(),\n          catalogCommits);\n    }\n  }\n\n  /**\n   * This method resolves the endBoundary to a version if it is specified. For a version-based\n   * boundary, this just returns the version. For a timestamp-based boundary, this resolves the\n   * timestamp to version. When the boundary is not specified, this returns empty, as the version\n   * cannot be resolved until later after we have performed any listing.\n   */\n  private Optional<Long> resolveEndVersionIfSpecified(\n      Engine engine, List<ParsedCatalogCommitData> catalogCommits) {\n    if (!ctx.endBoundaryOpt.isPresent()) {\n      // When endBoundary is not provided, we default to the latest version. We cannot resolve the\n      // latest version until later after we have performed any listing.\n      return Optional.empty();\n    }\n    CommitRangeBuilder.CommitBoundary endBoundary = ctx.endBoundaryOpt.get();\n\n    if (endBoundary.isVersion()) {\n      return Optional.of(endBoundary.getVersion());\n    } else {\n      logger.info(\n          \"{}: Trying to resolve end-boundary timestamp {} to version\",\n          tablePath,\n          endBoundary.getTimestamp());\n      long resolvedVersion =\n          DeltaHistoryManager.getVersionBeforeOrAtTimestamp(\n              engine,\n              logPath,\n              endBoundary.getTimestamp(),\n              (SnapshotImpl) endBoundary.getLatestSnapshot(),\n              catalogCommits);\n      return Optional.of(resolvedVersion);\n    }\n  }\n\n  private void validateVersionRange(long startVersion, Optional<Long> endVersionOpt) {\n    endVersionOpt.ifPresent(\n        endVersion -> {\n          if (startVersion > endVersion) {\n            throw invalidResolvedVersionRange(tablePath.toString(), startVersion, endVersion);\n          }\n        });\n  }\n\n  private void logResolvedVersions(long startVersion, Optional<Long> endVersionOpt) {\n    logger.info(\n        \"{}: Resolved startVersion={} and endVersion={} from startBoundary={} endBoundary={}\",\n        tablePath,\n        startVersion,\n        endVersionOpt,\n        ctx.startBoundary,\n        ctx.endBoundaryOpt);\n  }\n\n  private long extractLatestVersion(List<ParsedDeltaData> deltaDatas) {\n    return ListUtils.getLast(deltaDatas).getVersion();\n  }\n\n  private List<ParsedCatalogCommitData> getFileBasedRatifiedCommits() {\n    // Note: currently this is all we allow in CommitRangeBuilder anyway, but in the future that\n    // could change\n    return ctx.logDatas.stream()\n        .filter(logData -> logData instanceof ParsedCatalogCommitData)\n        .map(logData -> (ParsedCatalogCommitData) logData)\n        .filter(deltaData -> deltaData.isFile())\n        .collect(Collectors.toList());\n  }\n\n  /**\n   * Lists the _delta_log and combines the found published deltas with the catalog commits to\n   * compile a single contiguous list of deltas. Catalog commits take priority over published deltas\n   * when both are present for the same commit version. Returned deltas are guaranteed to start with\n   * startVersion, end with endVersion if endVersionOpt is non-empty, and are contiguous. Throws an\n   * exception if no deltas are found in the version range, or if startVersion or endVersion cannot\n   * be found.\n   */\n  private List<ParsedDeltaData> getDeltasForVersionRangeWithCatalogPriority(\n      Engine engine,\n      long startVersion,\n      Optional<Long> endVersionOpt,\n      List<ParsedCatalogCommitData> ratifiedCommits) {\n    // Get published deltas between startVersion and endVersionOpt\n    List<ParsedDeltaData> publishedDeltas =\n        getPublishedDeltasInVersionRange(engine, startVersion, endVersionOpt);\n\n    // Get ratified deltas between startVersion and endVersionOpt\n    List<ParsedDeltaData> ratifiedDeltas =\n        ratifiedCommits.stream()\n            .filter(\n                x ->\n                    x.getVersion() >= startVersion\n                        && x.getVersion() <= endVersionOpt.orElse(Long.MAX_VALUE))\n            .collect(Collectors.toList());\n\n    // Validate they are contiguous and valid (i.e. backfill is ordered)\n    validatePublishedPlusRatifiedDeltas(publishedDeltas, ratifiedDeltas);\n    List<ParsedDeltaData> combinedDeltas =\n        LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n            publishedDeltas, ratifiedDeltas);\n    validateDeltasMatchVersionRange(combinedDeltas, startVersion, endVersionOpt);\n    return combinedDeltas;\n  }\n\n  /**\n   * Returns any published deltas found on the file-system within the version range provided and\n   * validates that they are contiguous. Returns a sorted and contiguous list of deltas for the\n   * version range, but does no validation that the list fills the range.\n   */\n  private List<ParsedDeltaData> getPublishedDeltasInVersionRange(\n      Engine engine, long startVersion, Optional<Long> endVersionOpt) {\n    final List<FileStatus> commitFiles =\n        listDeltaLogFilesAsIter(\n                engine,\n                Collections.singleton(FileNames.DeltaLogFileType.COMMIT),\n                tablePath,\n                startVersion,\n                endVersionOpt,\n                false /* mustBeRecreatable */)\n            .toInMemoryList();\n    List<ParsedDeltaData> publishedDeltas =\n        commitFiles.stream().map(ParsedDeltaData::forFileStatus).collect(Collectors.toList());\n\n    // Validate listed delta files are contiguous\n    if (publishedDeltas.size() > 1) {\n      for (int i = 1; i < publishedDeltas.size(); i++) {\n        final ParsedLogData prev = publishedDeltas.get(i - 1);\n        final ParsedLogData curr = publishedDeltas.get(i);\n        if (prev.getVersion() + 1 != curr.getVersion()) {\n          throw publishedDeltasNotContiguous(\n              tablePath.toString(),\n              publishedDeltas.stream()\n                  .map(ParsedDeltaData::getVersion)\n                  .collect(Collectors.toList()));\n        }\n      }\n    }\n    return publishedDeltas;\n  }\n\n  private void validatePublishedPlusRatifiedDeltas(\n      List<ParsedDeltaData> publishedDeltas, List<ParsedDeltaData> ratifiedDeltas) {\n    // Valid example: P0, P1, P2 + R1 (ratified within published)\n    // Valid example: P0, P1 + R2, R3 (no overlap)\n    // Valid example: P0, P1 + R1, R2 (overlap)\n    if (!publishedDeltas.isEmpty() && !ratifiedDeltas.isEmpty()) {\n      long earliestPublishedVersion = publishedDeltas.get(0).getVersion();\n      long earliestRatifiedVersion = ratifiedDeltas.get(0).getVersion();\n      // We cannot have ratifiedDeltas.head.version < publishedDeltas.head.version\n      // Invalid example: P2, P3 + R1, R2, R3\n      if (earliestRatifiedVersion < earliestPublishedVersion) {\n        throw catalogCommitsPrecedePublishedDeltas(\n            tablePath.toString(),\n            earliestRatifiedVersion,\n            publishedDeltas.stream().map(ParsedDeltaData::getVersion).collect(Collectors.toList()));\n      }\n      long lastPublishedVersion = ListUtils.getLast(publishedDeltas).getVersion();\n      // We must have publishedDeltas + ratifiedDeltas be contiguous\n      // Invalid example: P0, P1 + R3, R4\n      if (lastPublishedVersion + 1 < earliestRatifiedVersion) {\n        throw publishedDeltasAndCatalogCommitsNotContiguous(\n            tablePath.toString(),\n            publishedDeltas.stream().map(ParsedDeltaData::getVersion).collect(Collectors.toList()),\n            ratifiedDeltas.stream().map(ParsedDeltaData::getVersion).collect(Collectors.toList()));\n      }\n    }\n  }\n\n  private void validateDeltasMatchVersionRange(\n      List<ParsedDeltaData> deltas, long startVersion, Optional<Long> endVersionOpt) {\n    // This can only happen if publishedDeltas.isEmpty && ratifiedDeltas.isEmpty\n    if (deltas.isEmpty()) {\n      throw noCommitFilesFoundForVersionRange(tablePath.toString(), startVersion, endVersionOpt);\n    }\n\n    long earliestVersion = ListUtils.getFirst(deltas).getVersion();\n    long latestVersion = ListUtils.getLast(deltas).getVersion();\n\n    if (earliestVersion != startVersion) {\n      throw startVersionNotFound(tablePath.toString(), startVersion, Optional.of(earliestVersion));\n    }\n    endVersionOpt.ifPresent(\n        endVersion -> {\n          if (latestVersion != endVersion) {\n            throw endVersionNotFound(tablePath.toString(), endVersion, latestVersion);\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/commitrange/CommitRangeImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.commitrange;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.CommitRangeBuilder;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaLogActionUtils;\nimport io.delta.kernel.internal.TableChangesUtils;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.files.ParsedDeltaData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/** Implementation of {@link CommitRange}. */\npublic class CommitRangeImpl implements CommitRange {\n\n  private final Path dataPath;\n  private final CommitRangeBuilder.CommitBoundary startBoundary;\n  private final Optional<CommitRangeBuilder.CommitBoundary> endBoundaryOpt;\n\n  private final long startVersion;\n  private final long endVersion;\n  private final List<ParsedDeltaData> deltas;\n\n  public CommitRangeImpl(\n      Path dataPath,\n      CommitRangeBuilder.CommitBoundary startBoundary,\n      Optional<CommitRangeBuilder.CommitBoundary> endBoundaryOpt,\n      long startVersion,\n      long endVersion,\n      List<ParsedDeltaData> deltas) {\n    checkArgument(startVersion <= endVersion, \"must have startVersion <= endVersion\");\n    checkArgument(\n        deltas.size() == endVersion - startVersion + 1, \"deltaFiles size must match size of range\");\n    this.dataPath = requireNonNull(dataPath, \"dataPath cannot be null\");\n    this.startBoundary = requireNonNull(startBoundary, \"startBoundary cannot be null\");\n    this.endBoundaryOpt = requireNonNull(endBoundaryOpt, \"endSpecOpt cannot be null\");\n    this.startVersion = startVersion;\n    this.endVersion = endVersion;\n    this.deltas = requireNonNull(deltas, \"deltas cannot be null\");\n  }\n\n  ////////////////////////////////////////\n  // Public CommitRange Implementation //\n  ////////////////////////////////////////\n\n  @Override\n  public long getStartVersion() {\n    return startVersion;\n  }\n\n  @Override\n  public long getEndVersion() {\n    return endVersion;\n  }\n\n  @Override\n  public CommitRangeBuilder.CommitBoundary getQueryStartBoundary() {\n    return startBoundary;\n  }\n\n  @Override\n  public Optional<CommitRangeBuilder.CommitBoundary> getQueryEndBoundary() {\n    return endBoundaryOpt;\n  }\n\n  @VisibleForTesting\n  public List<FileStatus> getDeltaFiles() {\n    return deltas.stream().map(ParsedDeltaData::getFileStatus).collect(Collectors.toList());\n  }\n\n  @Override\n  public CloseableIterator<ColumnarBatch> getActions(\n      Engine engine, Snapshot startSnapshot, Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n    validateParameters(engine, startSnapshot, actionSet);\n    // Build on top of getCommitActions() by flattening and adding version/timestamp columns\n    CloseableIterator<CommitActions> commits = getCommitActions(engine, startSnapshot, actionSet);\n\n    return TableChangesUtils.flattenCommitsAndAddMetadata(engine, commits);\n  }\n\n  @Override\n  public CloseableIterator<CommitActions> getCommitActions(\n      Engine engine, Snapshot startSnapshot, Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n    validateParameters(engine, startSnapshot, actionSet);\n    return DeltaLogActionUtils.getActionsFromCommitFilesWithProtocolValidation(\n        engine, dataPath.toString(), getDeltaFiles(), actionSet);\n  }\n\n  //////////////////////\n  // Private helpers //\n  //////////////////////\n\n  private void validateParameters(\n      Engine engine, Snapshot startSnapshot, Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n    requireNonNull(engine, \"engine cannot be null\");\n    requireNonNull(startSnapshot, \"startSnapshot cannot be null\");\n    requireNonNull(actionSet, \"actionSet cannot be null\");\n    checkArgument(\n        startSnapshot.getVersion() == startVersion,\n        \"startSnapshot must have version = startVersion\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/compaction/LogCompactionWriter.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.compaction;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.lang.ListUtils.getLast;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaLogActionUtils;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.replay.CreateCheckpointIterator;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.FileNames.DeltaLogFileType;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Utility for writing out log compactions. */\npublic class LogCompactionWriter {\n\n  private static final Logger logger = LoggerFactory.getLogger(LogCompactionWriter.class);\n\n  private final Path dataPath;\n  private final Path logPath;\n  private final long startVersion;\n  private final long endVersion;\n  // We need to know after what time we can cleanup remove tombstones. This is pulled from the table\n  // metadata, which we have at hook creation time in TransactionImpl, so we just store it here so\n  // we can use it when we run this hook\n  private final long minFileRetentionTimestampMillis;\n\n  public LogCompactionWriter(\n      Path dataPath,\n      Path logPath,\n      long startVersion,\n      long endVersion,\n      long minFileRetentionTimestampMillis) {\n    this.dataPath = requireNonNull(dataPath);\n    this.logPath = requireNonNull(logPath);\n    this.startVersion = startVersion;\n    this.endVersion = endVersion;\n    this.minFileRetentionTimestampMillis = minFileRetentionTimestampMillis;\n  }\n\n  public void writeLogCompactionFile(Engine engine) throws IOException {\n    Path compactedPath = FileNames.logCompactionPath(logPath, startVersion, endVersion);\n\n    logger.info(\n        \"Writing log compaction file for versions {} to {} to path: {}\",\n        startVersion,\n        endVersion,\n        compactedPath);\n\n    final long startTimeMillis = System.currentTimeMillis();\n    final List<FileStatus> deltas =\n        DeltaLogActionUtils.listDeltaLogFilesAsIter(\n                engine,\n                Collections.singleton(DeltaLogFileType.COMMIT),\n                dataPath,\n                startVersion,\n                Optional.of(endVersion),\n                false /* mustBeRecreatable */)\n            .toInMemoryList();\n\n    logger.info(\n        \"{}: Took {}ms to list commit files for log compaction\",\n        dataPath,\n        System.currentTimeMillis() - startTimeMillis);\n\n    if (deltas.size() != (endVersion - startVersion + 1)) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"Asked to compact between versions %d and %d, but found %d delta files\",\n              startVersion, endVersion, deltas.size()));\n    }\n\n    LogSegment segment =\n        new LogSegment(\n            dataPath,\n            endVersion,\n            deltas,\n            Collections.emptyList(),\n            Collections.emptyList(),\n            getLast(deltas),\n            Optional.empty() /* lastSeemChecksum */,\n            Optional.empty() /* maxPublishedDeltaVersion */);\n    CreateCheckpointIterator checkpointIterator =\n        new CreateCheckpointIterator(engine, segment, minFileRetentionTimestampMillis);\n    wrapEngineExceptionThrowsIO(\n        () -> {\n          try (CloseableIterator<Row> rows = Utils.intoRows(checkpointIterator)) {\n            engine.getJsonHandler().writeJsonFileAtomically(compactedPath.toString(), rows, false);\n          }\n          logger.info(\"Successfully wrote log compaction file `{}`\", compactedPath);\n          return null;\n        },\n        \"Writing log compaction file `%s`\",\n        compactedPath);\n  }\n\n  /** Utility to determine if log compaction should run for the given commit version. */\n  public static boolean shouldCompact(long commitVersion, long compactionInterval) {\n    // commits start at 0, so we add one to the commit version to check if we've hit the interval\n    return commitVersion > 0 && ((commitVersion + 1) % compactionInterval == 0);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/ChildVectorBasedRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.StructType;\nimport java.math.BigDecimal;\n\n/** A {@link Row} implementation that wraps a set of child vectors for a specific {@code rowId}. */\npublic abstract class ChildVectorBasedRow implements Row {\n\n  private final int rowId;\n  private final StructType schema;\n\n  public ChildVectorBasedRow(int rowId, StructType schema) {\n    this.rowId = rowId;\n    this.schema = schema;\n  }\n\n  @Override\n  public StructType getSchema() {\n    return schema;\n  }\n\n  @Override\n  public boolean isNullAt(int ordinal) {\n    return getChild(ordinal).isNullAt(rowId);\n  }\n\n  @Override\n  public boolean getBoolean(int ordinal) {\n    return getChild(ordinal).getBoolean(rowId);\n  }\n\n  @Override\n  public byte getByte(int ordinal) {\n    return getChild(ordinal).getByte(rowId);\n  }\n\n  @Override\n  public short getShort(int ordinal) {\n    return getChild(ordinal).getShort(rowId);\n  }\n\n  @Override\n  public int getInt(int ordinal) {\n    return getChild(ordinal).getInt(rowId);\n  }\n\n  @Override\n  public long getLong(int ordinal) {\n    return getChild(ordinal).getLong(rowId);\n  }\n\n  @Override\n  public float getFloat(int ordinal) {\n    return getChild(ordinal).getFloat(rowId);\n  }\n\n  @Override\n  public double getDouble(int ordinal) {\n    return getChild(ordinal).getDouble(rowId);\n  }\n\n  @Override\n  public String getString(int ordinal) {\n    return getChild(ordinal).getString(rowId);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int ordinal) {\n    return getChild(ordinal).getDecimal(rowId);\n  }\n\n  @Override\n  public byte[] getBinary(int ordinal) {\n    return getChild(ordinal).getBinary(rowId);\n  }\n\n  @Override\n  public Row getStruct(int ordinal) {\n    return StructRow.fromStructVector(getChild(ordinal), rowId);\n  }\n\n  @Override\n  public ArrayValue getArray(int ordinal) {\n    return getChild(ordinal).getArray(rowId);\n  }\n\n  @Override\n  public MapValue getMap(int ordinal) {\n    return getChild(ordinal).getMap(rowId);\n  }\n\n  protected abstract ColumnVector getChild(int ordinal);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/ColumnarBatchRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport java.util.Objects;\n\n/** Row abstraction around a columnar batch and a particular row within the columnar batch. */\npublic class ColumnarBatchRow extends ChildVectorBasedRow {\n\n  private final ColumnarBatch columnarBatch;\n\n  public ColumnarBatchRow(ColumnarBatch columnarBatch, int rowId) {\n    super(rowId, Objects.requireNonNull(columnarBatch, \"columnarBatch is null\").getSchema());\n    this.columnarBatch = columnarBatch;\n  }\n\n  @Override\n  protected ColumnVector getChild(int ordinal) {\n    return columnarBatch.getColumnVector(ordinal);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/DelegateRow.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * This wraps an existing {@link Row} and allows overriding values for some particular ordinals.\n * This enables creating a modified view of a row without mutating the original row.\n */\npublic class DelegateRow implements Row {\n\n  /** The underlying row being delegated to. */\n  private final Row row;\n\n  /**\n   * A map of ordinal-to-value overrides that takes precedence over the underlying row's data. When\n   * accessing data, this map is checked first before falling back to the underlying row.\n   */\n  private final Map<Integer, Object> overrides;\n\n  public DelegateRow(Row row, Map<Integer, Object> overrides) {\n    Objects.requireNonNull(row, \"row is null\");\n    Objects.requireNonNull(overrides, \"map of overrides is null\");\n\n    if (row instanceof DelegateRow) {\n      // If the row is already a delegation of another row, we merge the overrides and keep only\n      // one layer of delegation.\n      DelegateRow delegateRow = (DelegateRow) row;\n      this.row = delegateRow.row;\n      this.overrides = new HashMap<>(delegateRow.overrides);\n      this.overrides.putAll(overrides);\n    } else {\n      this.row = row;\n      this.overrides = new HashMap<>(overrides);\n    }\n  }\n\n  @Override\n  public StructType getSchema() {\n    return row.getSchema();\n  }\n\n  @Override\n  public boolean isNullAt(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      return overrides.get(ordinal) == null;\n    }\n    return row.isNullAt(ordinal);\n  }\n\n  @Override\n  public boolean getBoolean(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, BooleanType.class, \"boolean\");\n      return (boolean) overrides.get(ordinal);\n    }\n    return row.getBoolean(ordinal);\n  }\n\n  @Override\n  public byte getByte(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, ByteType.class, \"byte\");\n      return (byte) overrides.get(ordinal);\n    }\n    return row.getByte(ordinal);\n  }\n\n  @Override\n  public short getShort(int ordinal) {\n    throwIfUnsafeAccess(ordinal, ShortType.class, \"short\");\n    if (overrides.containsKey(ordinal)) {\n      return (short) overrides.get(ordinal);\n    }\n    return row.getShort(ordinal);\n  }\n\n  @Override\n  public int getInt(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, IntegerType.class, \"integer\");\n      return (int) overrides.get(ordinal);\n    }\n    return row.getInt(ordinal);\n  }\n\n  @Override\n  public long getLong(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, LongType.class, \"long\");\n      return (long) overrides.get(ordinal);\n    }\n    return row.getLong(ordinal);\n  }\n\n  @Override\n  public float getFloat(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, FloatType.class, \"float\");\n      return (float) overrides.get(ordinal);\n    }\n    return row.getFloat(ordinal);\n  }\n\n  @Override\n  public double getDouble(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, DoubleType.class, \"double\");\n      return (double) overrides.get(ordinal);\n    }\n    return row.getDouble(ordinal);\n  }\n\n  @Override\n  public String getString(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, StringType.class, \"string\");\n      return (String) overrides.get(ordinal);\n    }\n    return row.getString(ordinal);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, DecimalType.class, \"decimal\");\n      return (BigDecimal) overrides.get(ordinal);\n    }\n    return row.getDecimal(ordinal);\n  }\n\n  @Override\n  public byte[] getBinary(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, BinaryType.class, \"binary\");\n      return (byte[]) overrides.get(ordinal);\n    }\n    return row.getBinary(ordinal);\n  }\n\n  @Override\n  public Row getStruct(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      throwIfUnsafeAccess(ordinal, StructType.class, \"struct\");\n      return (Row) overrides.get(ordinal);\n    }\n    return row.getStruct(ordinal);\n  }\n\n  @Override\n  public ArrayValue getArray(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      // TODO: Not sufficient check, also need to check the element type. This should be revisited\n      //  together with the GenericRow.\n      throwIfUnsafeAccess(ordinal, ArrayType.class, \"array\");\n      return (ArrayValue) overrides.get(ordinal);\n    }\n    return row.getArray(ordinal);\n  }\n\n  @Override\n  public MapValue getMap(int ordinal) {\n    if (overrides.containsKey(ordinal)) {\n      // TODO: Not sufficient check, also need to check the element type. This should be revisited\n      //  together with the GenericRow.\n      throwIfUnsafeAccess(ordinal, MapType.class, \"map\");\n      return (MapValue) overrides.get(ordinal);\n    }\n    return row.getMap(ordinal);\n  }\n\n  private void throwIfUnsafeAccess(\n      int ordinal, Class<? extends DataType> expDataType, String accessType) {\n    final StructType schema = row.getSchema();\n    checkArgument(\n        ordinal >= 0 && ordinal < schema.length(),\n        \"Invalid ordinal %d for schema with length %d\",\n        ordinal,\n        schema.length());\n\n    DataType actualDataType = schema.at(ordinal).getDataType();\n    if (!expDataType.isAssignableFrom(actualDataType.getClass())) {\n      String msg =\n          String.format(\n              \"Fail to access a '%s' value from a field of type '%s'\", accessType, actualDataType);\n      throw new UnsupportedOperationException(msg);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/GenericColumnVector.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/** A generic implementation of {@link ColumnVector} that wraps a list of values. */\npublic class GenericColumnVector implements ColumnVector {\n  private final List<?> values;\n  private final DataType dataType;\n\n  public GenericColumnVector(List<?> values, DataType dataType) {\n    this.values = values;\n    this.dataType = dataType;\n  }\n\n  @Override\n  public DataType getDataType() {\n    return dataType;\n  }\n\n  @Override\n  public int getSize() {\n    return values.size();\n  }\n\n  @Override\n  public void close() {\n    // no-op\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    validateRowId(rowId);\n    return values.get(rowId) == null;\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    checkArgument(BooleanType.BOOLEAN.equals(dataType));\n    return (Boolean) getValidatedValue(rowId, Boolean.class);\n  }\n\n  @Override\n  public byte getByte(int rowId) {\n    checkArgument(ByteType.BYTE.equals(dataType));\n    return (Byte) getValidatedValue(rowId, Byte.class);\n  }\n\n  @Override\n  public short getShort(int rowId) {\n    checkArgument(ShortType.SHORT.equals(dataType));\n    return (Short) getValidatedValue(rowId, Short.class);\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    checkArgument(IntegerType.INTEGER.equals(dataType) || DateType.DATE.equals(dataType));\n    return (Integer) getValidatedValue(rowId, Integer.class);\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    checkArgument(\n        LongType.LONG.equals(dataType)\n            || TimestampType.TIMESTAMP.equals(dataType)\n            || TimestampNTZType.TIMESTAMP_NTZ.equals(dataType));\n    return (Long) getValidatedValue(rowId, Long.class);\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    checkArgument(FloatType.FLOAT.equals(dataType));\n    return (Float) getValidatedValue(rowId, Float.class);\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    checkArgument(DoubleType.DOUBLE.equals(dataType));\n    return (Double) getValidatedValue(rowId, Double.class);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    checkArgument(dataType instanceof DecimalType);\n    return (BigDecimal) getValidatedValue(rowId, BigDecimal.class);\n  }\n\n  @Override\n  public String getString(int rowId) {\n    checkArgument(StringType.STRING.equals(dataType));\n    return (String) getValidatedValue(rowId, String.class);\n  }\n\n  @Override\n  public byte[] getBinary(int rowId) {\n    checkArgument(BinaryType.BINARY.equals(dataType));\n    return (byte[]) getValidatedValue(rowId, byte[].class);\n  }\n\n  @Override\n  public ArrayValue getArray(int rowId) {\n    checkArgument(dataType instanceof ArrayType);\n    return (ArrayValue) getValidatedValue(rowId, ArrayValue.class);\n  }\n\n  @Override\n  public MapValue getMap(int rowId) {\n    checkArgument(dataType instanceof MapType);\n    return (MapValue) getValidatedValue(rowId, MapValue.class);\n  }\n\n  @Override\n  public ColumnVector getChild(int ordinal) {\n    checkArgument(dataType instanceof StructType);\n    checkArgument(ordinal < ((StructType) dataType).length());\n\n    DataType childDatatype = ((StructType) dataType).at(ordinal).getDataType();\n    List<?> childValues = extractChildValues(ordinal, childDatatype);\n\n    return new GenericColumnVector(childValues, childDatatype);\n  }\n\n  private void validateRowId(int rowId) {\n    checkArgument(rowId >= 0 && rowId < values.size(), \"Invalid rowId: %s\", rowId);\n  }\n\n  private Object getValidatedValue(int rowId, Class<?> expectedType) {\n    validateRowId(rowId);\n    Object value = values.get(rowId);\n    checkArgument(\n        expectedType.isInstance(value), \"Value must be of type %s\", expectedType.getSimpleName());\n    return value;\n  }\n\n  private List<?> extractChildValues(int ordinal, DataType childDatatype) {\n    return values.stream()\n        .map(e -> extractChildValue(e, ordinal, childDatatype))\n        .collect(Collectors.toList());\n  }\n\n  private Object extractChildValue(Object element, int ordinal, DataType childDatatype) {\n    checkArgument(element instanceof Row);\n    Row row = (Row) element;\n\n    if (row.isNullAt(ordinal)) {\n      return null;\n    }\n\n    return extractTypedValue(row, ordinal, childDatatype);\n  }\n\n  private Object extractTypedValue(Row row, int ordinal, DataType childDatatype) {\n    // Primitive Types\n    if (childDatatype instanceof BooleanType) {\n      return row.getBoolean(ordinal);\n    }\n    if (childDatatype instanceof ByteType) {\n      return row.getByte(ordinal);\n    }\n    if (childDatatype instanceof ShortType) {\n      return row.getShort(ordinal);\n    }\n    if (childDatatype instanceof IntegerType || childDatatype instanceof DateType) {\n      return row.getInt(ordinal);\n    }\n    if (childDatatype instanceof LongType\n        || childDatatype instanceof TimestampType\n        || childDatatype instanceof TimestampNTZType) {\n      return row.getLong(ordinal);\n    }\n    if (childDatatype instanceof FloatType) {\n      return row.getFloat(ordinal);\n    }\n    if (childDatatype instanceof DoubleType) {\n      return row.getDouble(ordinal);\n    }\n\n    // Complex Types\n    if (childDatatype instanceof StringType) {\n      return row.getString(ordinal);\n    }\n    if (childDatatype instanceof BinaryType) {\n      return row.getBinary(ordinal);\n    }\n    if (childDatatype instanceof DecimalType) {\n      return row.getDecimal(ordinal);\n    }\n\n    // Nested Types\n    if (childDatatype instanceof StructType) {\n      return row.getStruct(ordinal);\n    }\n    if (childDatatype instanceof ArrayType) {\n      return row.getArray(ordinal);\n    }\n    if (childDatatype instanceof MapType) {\n      return row.getMap(ordinal);\n    }\n\n    throw new UnsupportedOperationException(\n        String.format(\"Unsupported data type: %s\", childDatatype.getClass().getSimpleName()));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/GenericRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.data;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.Map;\n\n/** Exposes a given map of values as a {@link Row} */\npublic class GenericRow implements Row {\n  private final StructType schema;\n  private final Map<Integer, Object> ordinalToValue;\n\n  /**\n   * @param schema the schema of the row\n   * @param ordinalToValue a mapping of column ordinal to objects; for each column the object must\n   *     be of the return type corresponding to the data type's getter method in the Row interface\n   */\n  public GenericRow(StructType schema, Map<Integer, Object> ordinalToValue) {\n    this.schema = requireNonNull(schema, \"schema is null\");\n    this.ordinalToValue = requireNonNull(ordinalToValue, \"ordinalToValue is null\");\n  }\n\n  @Override\n  public StructType getSchema() {\n    return schema;\n  }\n\n  @Override\n  public boolean isNullAt(int ordinal) {\n    return getValue(ordinal) == null;\n  }\n\n  @Override\n  public boolean getBoolean(int ordinal) {\n    throwIfUnsafeAccess(ordinal, BooleanType.class, \"boolean\");\n    return (boolean) getValue(ordinal);\n  }\n\n  @Override\n  public byte getByte(int ordinal) {\n    throwIfUnsafeAccess(ordinal, ByteType.class, \"byte\");\n    return (byte) getValue(ordinal);\n  }\n\n  @Override\n  public short getShort(int ordinal) {\n    throwIfUnsafeAccess(ordinal, ShortType.class, \"short\");\n    return (short) getValue(ordinal);\n  }\n\n  @Override\n  public int getInt(int ordinal) {\n    throwIfUnsafeAccess(ordinal, IntegerType.class, \"integer\");\n    return (int) getValue(ordinal);\n  }\n\n  @Override\n  public long getLong(int ordinal) {\n    throwIfUnsafeAccess(ordinal, LongType.class, \"long\");\n    return (long) getValue(ordinal);\n  }\n\n  @Override\n  public float getFloat(int ordinal) {\n    throwIfUnsafeAccess(ordinal, FloatType.class, \"float\");\n    return (float) getValue(ordinal);\n  }\n\n  @Override\n  public double getDouble(int ordinal) {\n    throwIfUnsafeAccess(ordinal, DoubleType.class, \"double\");\n    return (double) getValue(ordinal);\n  }\n\n  @Override\n  public String getString(int ordinal) {\n    throwIfUnsafeAccess(ordinal, StringType.class, \"string\");\n    return (String) getValue(ordinal);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int ordinal) {\n    throwIfUnsafeAccess(ordinal, DecimalType.class, \"decimal\");\n    return (BigDecimal) getValue(ordinal);\n  }\n\n  @Override\n  public byte[] getBinary(int ordinal) {\n    throwIfUnsafeAccess(ordinal, BinaryType.class, \"binary\");\n    return (byte[]) getValue(ordinal);\n  }\n\n  @Override\n  public Row getStruct(int ordinal) {\n    throwIfUnsafeAccess(ordinal, StructType.class, \"struct\");\n    return (Row) getValue(ordinal);\n  }\n\n  @Override\n  public ArrayValue getArray(int ordinal) {\n    // TODO: not sufficient check, also need to check the element type\n    throwIfUnsafeAccess(ordinal, ArrayType.class, \"array\");\n    return (ArrayValue) getValue(ordinal);\n  }\n\n  @Override\n  public MapValue getMap(int ordinal) {\n    // TODO: not sufficient check, also need to check the element types\n    throwIfUnsafeAccess(ordinal, MapType.class, \"map\");\n    return (MapValue) getValue(ordinal);\n  }\n\n  private Object getValue(int ordinal) {\n    return ordinalToValue.get(ordinal);\n  }\n\n  private void throwIfUnsafeAccess(\n      int ordinal, Class<? extends DataType> expDataType, String accessType) {\n\n    DataType actualDataType = dataType(ordinal);\n    if (!expDataType.isAssignableFrom(actualDataType.getClass())) {\n      String msg =\n          String.format(\n              \"Trying to access a `%s` value from vector of type `%s`\", accessType, actualDataType);\n      throw new UnsupportedOperationException(msg);\n    }\n  }\n\n  private DataType dataType(int ordinal) {\n    if (schema.length() <= ordinal) {\n      throw new IllegalArgumentException(\"invalid ordinal: \" + ordinal);\n    }\n\n    return schema.at(ordinal).getDataType();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/ScanStateRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport static java.util.stream.Collectors.toMap;\n\nimport io.delta.kernel.Scan;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.*;\nimport java.net.URI;\nimport java.util.*;\nimport java.util.stream.IntStream;\n\n/** Encapsulate the scan state (common info for all scan files) as a {@link Row} */\npublic class ScanStateRow extends GenericRow {\n  private static final StructType SCHEMA =\n      new StructType()\n          .add(\"configuration\", new MapType(StringType.STRING, StringType.STRING, false))\n          .add(\"logicalSchemaJson\", StringType.STRING)\n          .add(\"physicalSchemaJson\", StringType.STRING)\n          .add(\"partitionColumns\", new ArrayType(StringType.STRING, false))\n          .add(\"minReaderVersion\", IntegerType.INTEGER)\n          .add(\"minWriterVersion\", IntegerType.INTEGER)\n          .add(\"tablePath\", StringType.STRING);\n\n  private static final Map<String, Integer> COL_NAME_TO_ORDINAL =\n      IntStream.range(0, SCHEMA.length())\n          .boxed()\n          .collect(toMap(i -> SCHEMA.at(i).getName(), i -> i));\n\n  public static ScanStateRow of(\n      Metadata metadata,\n      Protocol protocol,\n      String logicalSchemaJson,\n      String physicalSchemaJson,\n      String tablePath) {\n    HashMap<Integer, Object> valueMap = new HashMap<>();\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"configuration\"), metadata.getConfigurationMapValue());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"logicalSchemaJson\"), logicalSchemaJson);\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"physicalSchemaJson\"), physicalSchemaJson);\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"partitionColumns\"), metadata.getPartitionColumns());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"minReaderVersion\"), protocol.getMinReaderVersion());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"minWriterVersion\"), protocol.getMinWriterVersion());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"tablePath\"), tablePath);\n    return new ScanStateRow(valueMap);\n  }\n\n  public ScanStateRow(HashMap<Integer, Object> valueMap) {\n    super(SCHEMA, valueMap);\n  }\n\n  /**\n   * Utility method to get the configuration map from the scan state {@link Row} returned by {@link\n   * Scan#getScanState(Engine)}.\n   *\n   * @param scanState Scan state {@link Row}\n   * @return Map of configuration key-value pairs.\n   */\n  public static Map<String, String> getConfiguration(Row scanState) {\n    return VectorUtils.toJavaMap(scanState.getMap(COL_NAME_TO_ORDINAL.get(\"configuration\")));\n  }\n\n  /**\n   * Utility method to get the logical schema from the scan state {@link Row} returned by {@link\n   * Scan#getScanState(Engine)}.\n   *\n   * @param scanState Scan state {@link Row}\n   * @return Logical schema to read from the data files.\n   */\n  public static StructType getLogicalSchema(Row scanState) {\n    String serializedSchema = scanState.getString(COL_NAME_TO_ORDINAL.get(\"logicalSchemaJson\"));\n    return DataTypeJsonSerDe.deserializeStructType(serializedSchema);\n  }\n\n  /**\n   * Utility method to get the physical schema from the scan state {@link Row} returned by {@link\n   * Scan#getScanState(Engine)}. This schema is used to request data from the scan files for the\n   * query.\n   *\n   * @param scanState Scan state {@link Row}\n   * @return Physical schema to read from the data files.\n   */\n  public static StructType getPhysicalDataReadSchema(Row scanState) {\n    String serializedSchema = scanState.getString(COL_NAME_TO_ORDINAL.get(\"physicalSchemaJson\"));\n    return DataTypeJsonSerDe.deserializeStructType(serializedSchema);\n  }\n\n  /**\n   * Get the list of partition column names from the scan state {@link Row} returned by {@link\n   * Scan#getScanState(Engine)}.\n   *\n   * @param scanState Scan state {@link Row}\n   * @return List of partition column names according to the scan state.\n   */\n  public static List<String> getPartitionColumns(Row scanState) {\n    return VectorUtils.toJavaList(scanState.getArray(COL_NAME_TO_ORDINAL.get(\"partitionColumns\")));\n  }\n\n  /**\n   * Get the column mapping mode from the scan state {@link Row} returned by {@link\n   * Scan#getScanState(Engine)}.\n   */\n  public static ColumnMappingMode getColumnMappingMode(Row scanState) {\n    return ColumnMapping.getColumnMappingMode(getConfiguration(scanState));\n  }\n\n  /**\n   * Get the table root from scan state {@link Row} returned by {@link Scan#getScanState(Engine)}\n   *\n   * @param scanState Scan state {@link Row}\n   * @return Fully qualified path to the location of the table.\n   */\n  public static Path getTableRoot(Row scanState) {\n    return new Path(URI.create(scanState.getString(COL_NAME_TO_ORDINAL.get(\"tablePath\"))));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/SelectionColumnVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.data;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.internal.deletionvectors.RoaringBitmapArray;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.DataType;\n\n/** The selection vector for a columnar batch as a boolean {@link ColumnVector}. */\npublic class SelectionColumnVector implements ColumnVector {\n\n  private final RoaringBitmapArray bitmap;\n  private final ColumnVector rowIndices;\n\n  public SelectionColumnVector(RoaringBitmapArray bitmap, ColumnVector rowIndices) {\n    this.bitmap = bitmap;\n    this.rowIndices = rowIndices;\n  }\n\n  @Override\n  public DataType getDataType() {\n    return BooleanType.BOOLEAN;\n  }\n\n  @Override\n  public int getSize() {\n    return rowIndices.getSize();\n  }\n\n  @Override\n  public void close() {\n    rowIndices.close();\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    return false;\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    return !bitmap.contains(rowIndices.getLong(rowId));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/StructRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.StructType;\n\n/** A {@link Row} abstraction for a struct type column vector and a specific {@code rowId}. */\npublic class StructRow extends ChildVectorBasedRow {\n\n  public static StructRow fromStructVector(ColumnVector columnVector, int rowId) {\n    checkArgument(columnVector.getDataType() instanceof StructType);\n    if (columnVector.isNullAt(rowId)) {\n      return null;\n    } else {\n      return new StructRow(columnVector, rowId, (StructType) columnVector.getDataType());\n    }\n  }\n\n  private final ColumnVector structVector;\n\n  private StructRow(ColumnVector structVector, int rowId, StructType schema) {\n    super(rowId, schema);\n    this.structVector = structVector;\n  }\n\n  @Override\n  protected ColumnVector getChild(int ordinal) {\n    return structVector.getChild(ordinal);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/data/TransactionStateRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.data;\n\nimport static java.util.stream.Collectors.toMap;\n\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.*;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.IntStream;\n\npublic class TransactionStateRow extends GenericRow {\n  public static final StructType SCHEMA =\n      new StructType()\n          .add(\"logicalSchemaString\", StringType.STRING)\n          .add(\"physicalSchemaString\", StringType.STRING)\n          .add(\"partitionColumns\", new ArrayType(StringType.STRING, false /* containsNull */))\n          .add(\n              \"configuration\",\n              new MapType(StringType.STRING, StringType.STRING, false /* valueContainsNull */))\n          .add(\"tablePath\", StringType.STRING)\n          .add(\"maxRetries\", IntegerType.INTEGER)\n          .add(\"protocol\", Protocol.FULL_SCHEMA);\n\n  private static final Map<String, Integer> COL_NAME_TO_ORDINAL =\n      IntStream.range(0, SCHEMA.length())\n          .boxed()\n          .collect(toMap(i -> SCHEMA.at(i).getName(), i -> i));\n\n  public static TransactionStateRow of(\n      Metadata metadata, Protocol protocol, String tablePath, int maxRetries) {\n    HashMap<Integer, Object> valueMap = new HashMap<>();\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"logicalSchemaString\"), metadata.getSchemaString());\n    valueMap.put(\n        COL_NAME_TO_ORDINAL.get(\"physicalSchemaString\"), metadata.getPhysicalSchema().toJson());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"partitionColumns\"), metadata.getPartitionColumns());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"configuration\"), metadata.getConfigurationMapValue());\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"tablePath\"), tablePath);\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"maxRetries\"), maxRetries);\n    valueMap.put(COL_NAME_TO_ORDINAL.get(\"protocol\"), protocol.toRow());\n    return new TransactionStateRow(valueMap);\n  }\n\n  private TransactionStateRow(HashMap<Integer, Object> valueMap) {\n    super(SCHEMA, valueMap);\n  }\n\n  /**\n   * Get the logical schema of the table from the transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return Logical schema of the table as {@link StructType}\n   */\n  public static StructType getLogicalSchema(Row transactionState) {\n    String serializedSchema =\n        transactionState.getString(COL_NAME_TO_ORDINAL.get(\"logicalSchemaString\"));\n    return DataTypeJsonSerDe.deserializeStructType(serializedSchema);\n  }\n\n  /**\n   * Get the physical schema of the table from the transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return Logical schema of the table as {@link StructType}\n   */\n  public static StructType getPhysicalSchema(Row transactionState) {\n    String serializedSchema =\n        transactionState.getString(COL_NAME_TO_ORDINAL.get(\"physicalSchemaString\"));\n    return DataTypeJsonSerDe.deserializeStructType(serializedSchema);\n  }\n\n  /**\n   * Get the configuration from the transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState\n   * @return Configuration as a map of key-value pairs.\n   */\n  public static Map<String, String> getConfiguration(Row transactionState) {\n    return VectorUtils.toJavaMap(transactionState.getMap(COL_NAME_TO_ORDINAL.get(\"configuration\")));\n  }\n\n  /**\n   * Get the iceberg compatibility enabled or not from the transaction state {@link Row} returned by\n   * {@link Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return True if iceberg compatibility is enabled, false otherwise.\n   */\n  public static boolean isIcebergCompatV2Enabled(Row transactionState) {\n    return Boolean.parseBoolean(\n        getConfiguration(transactionState)\n            .getOrDefault(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey(), \"false\"));\n  }\n\n  /**\n   * Get the iceberg compatibility enabled or not from the transaction state {@link Row} returned by\n   * {@link Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return True if iceberg compatibility is enabled, false otherwise.\n   */\n  public static boolean isIcebergCompatV3Enabled(Row transactionState) {\n    return Boolean.parseBoolean(\n        getConfiguration(transactionState)\n            .getOrDefault(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey(), \"false\"));\n  }\n\n  /**\n   * Get the column mapping mode from the transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState\n   * @return ColumnMapping mode as {@link ColumnMapping.ColumnMappingMode}\n   */\n  public static ColumnMapping.ColumnMappingMode getColumnMappingMode(Row transactionState) {\n    String columnMappingModeStr =\n        getConfiguration(transactionState)\n            .getOrDefault(TableConfig.COLUMN_MAPPING_MODE.getKey(), \"none\");\n    return ColumnMapping.ColumnMappingMode.fromTableConfig(columnMappingModeStr);\n  }\n\n  /**\n   * Get the list of partition column names from the transaction state {@link Row} returned by\n   * {@link Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return List of partition column names according to the scan state.\n   */\n  public static List<String> getPartitionColumnsList(Row transactionState) {\n    return VectorUtils.toJavaList(\n        transactionState.getArray(COL_NAME_TO_ORDINAL.get(\"partitionColumns\")));\n  }\n\n  /**\n   * Get the table path from transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return Fully qualified path to the location of the table.\n   */\n  public static String getTablePath(Row transactionState) {\n    return transactionState.getString(COL_NAME_TO_ORDINAL.get(\"tablePath\"));\n  }\n\n  /**\n   * Get the maxRetries from transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}\n   */\n  public static int getMaxRetries(Row transactionState) {\n    return transactionState.getInt(COL_NAME_TO_ORDINAL.get(\"maxRetries\"));\n  }\n\n  /**\n   * Get the Protocol from transaction state {@link Row} returned by {@link\n   * Transaction#getTransactionState(Engine)}\n   *\n   * @param transactionState Transaction state state {@link Row}\n   * @return Protocol object\n   */\n  public static Protocol getProtocol(Row transactionState) {\n    Row protocolRow = transactionState.getStruct(COL_NAME_TO_ORDINAL.get(\"protocol\"));\n    return Protocol.fromRow(protocolRow);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/deletionvectors/Base85Codec.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.deletionvectors;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.nio.charset.StandardCharsets.US_ASCII;\n\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\nimport java.util.UUID;\n\n/**\n * This implements Base85 using the 4 byte block aligned encoding and character set from Z85.\n *\n * @see <a href=\"https://rfc.zeromq.org/spec/32/\">Z85 encoding</a>\n *     <p>Taken from\n *     https://github.com/delta-io/delta/blob/master/spark/src/main/scala/org/apache/spark\n *     /sql/delta/util/Codec.scala\n */\npublic final class Base85Codec {\n\n  static final long BASE = 85L;\n  static final long BASE_2ND_POWER = 7225L; // 85^2\n  static final long BASE_3RD_POWER = 614125L; // 85^3\n  static final long BASE_4TH_POWER = 52200625L; // 85^4\n  static final int ASCII_BITMASK = 0x7F;\n\n  // UUIDs always encode into 20 characters.\n  public static final int ENCODED_UUID_LENGTH = 20;\n\n  private static byte[] getEncodeMap() {\n    byte[] map = new byte[85];\n    int i = 0;\n    for (char c = '0'; c <= '9'; c++) {\n      map[i] = (byte) c;\n      i++;\n    }\n    for (char c = 'a'; c <= 'z'; c++) {\n      map[i] = (byte) c;\n      i++;\n    }\n    for (char c = 'A'; c <= 'Z'; c++) {\n      map[i] = (byte) c;\n      i++;\n    }\n    for (char c : \".-:+=^!/*?&<>()[]{}@%$#\".toCharArray()) {\n      map[i] = (byte) c;\n      i++;\n    }\n    return map;\n  }\n\n  private static byte[] getDecodeMap() {\n    checkArgument(ENCODE_MAP.length - 1 <= Byte.MAX_VALUE);\n    // The bitmask is the same as largest possible value, so the length of the array must\n    // be one greater.\n    byte[] map = new byte[ASCII_BITMASK + 1];\n    Arrays.fill(map, (byte) -1);\n    for (int i = 0; i < ENCODE_MAP.length; i++) {\n      byte b = ENCODE_MAP[i];\n      map[b] = (byte) i;\n    }\n    return map;\n  }\n\n  public static final byte[] ENCODE_MAP = getEncodeMap();\n  public static final byte[] DECODE_MAP = getDecodeMap();\n\n  /** Decode a 16 byte UUID. */\n  public static UUID decodeUUID(String encoded) {\n    ByteBuffer buffer = decodeBlocks(encoded);\n    return uuidFromByteBuffer(buffer);\n  }\n\n  /**\n   * Decode an arbitrary byte array.\n   *\n   * <p>Only `outputLength` bytes will be returned. Any extra bytes, such as padding added because\n   * the input was unaligned, will be dropped.\n   */\n  public static byte[] decodeBytes(String encoded, int outputLength) {\n    ByteBuffer result = decodeBlocks(encoded);\n    if (result.remaining() > outputLength) {\n      // Only read the expected number of bytes\n      byte[] output = new byte[outputLength];\n      result.get(output);\n      return output;\n    } else {\n      return result.array();\n    }\n  }\n\n  /**\n   * Decode an arbitrary byte array.\n   *\n   * <p>Output may contain padding bytes, if the input was not 4 byte aligned. Use [[decodeBytes]]\n   * in that case and specify the expected number of output bytes without padding.\n   */\n  public static byte[] decodeAlignedBytes(String encoded) {\n    return decodeBlocks(encoded).array();\n  }\n\n  /**\n   * Decode an arbitrary byte array.\n   *\n   * <p>Output may contain padding bytes, if the input was not 4 byte aligned.\n   */\n  private static ByteBuffer decodeBlocks(String encoded) {\n    char[] input = encoded.toCharArray();\n    checkArgument(input.length % 5 == 0, \"input should be 5 character aligned\");\n    ByteBuffer buffer = ByteBuffer.allocate(input.length / 5 * 4);\n\n    // A mechanism to detect invalid characters in the input while decoding, that only has a\n    // single conditional at the very end, instead of branching for every character.\n    class InputCharDecoder {\n      int canary = 0;\n\n      long decodeInputChar(int i) {\n        char c = input[i];\n        canary |= c; // non-ascii char has bits outside of ASCII_BITMASK\n        byte b = DECODE_MAP[c & ASCII_BITMASK];\n        canary |= b; // invalid char maps to -1, which has bits outside ASCII_BITMASK\n        return (long) b;\n      }\n    }\n\n    int inputIndex = 0;\n    InputCharDecoder inputCharDecoder = new InputCharDecoder();\n    while (buffer.hasRemaining()) {\n      long sum = 0;\n      sum += inputCharDecoder.decodeInputChar(inputIndex) * BASE_4TH_POWER;\n      sum += inputCharDecoder.decodeInputChar(inputIndex + 1) * BASE_3RD_POWER;\n      sum += inputCharDecoder.decodeInputChar(inputIndex + 2) * BASE_2ND_POWER;\n      sum += inputCharDecoder.decodeInputChar(inputIndex + 3) * BASE;\n      sum += inputCharDecoder.decodeInputChar(inputIndex + 4);\n      buffer.putInt((int) sum);\n      inputIndex += 5;\n    }\n    checkArgument(\n        (inputCharDecoder.canary & ~ASCII_BITMASK) == 0, \"Input is not valid Z85: \" + encoded);\n    buffer.rewind();\n    return buffer;\n  }\n\n  private static UUID uuidFromByteBuffer(ByteBuffer buffer) {\n    checkArgument(buffer.remaining() >= 16);\n    long highBits = buffer.getLong();\n    long lowBits = buffer.getLong();\n    return new UUID(highBits, lowBits);\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Methods implemented for testing only\n  ////////////////////////////////////////////////////////////////////////////////\n\n  /** Encode a 16 byte UUID. */\n  public static String encodeUUID(UUID id) {\n    ByteBuffer buffer = uuidToByteBuffer(id);\n    return encodeBlocks(buffer);\n  }\n\n  private static ByteBuffer uuidToByteBuffer(UUID id) {\n    ByteBuffer buffer = ByteBuffer.allocate(16);\n    buffer.putLong(id.getMostSignificantBits());\n    buffer.putLong(id.getLeastSignificantBits());\n    buffer.rewind();\n    return buffer;\n  }\n\n  /**\n   * Encode an arbitrary byte array using 4 byte blocks.\n   *\n   * <p>Expects the input to be 4 byte aligned.\n   */\n  private static String encodeBlocks(ByteBuffer buffer) {\n    checkArgument(buffer.remaining() % 4 == 0);\n    int numBlocks = buffer.remaining() / 4;\n    // Every 4 byte block gets encoded into 5 bytes/chars\n    int outputLength = numBlocks * 5;\n    byte[] output = new byte[outputLength];\n    int outputIndex = 0;\n\n    while (buffer.hasRemaining()) {\n      long sum = buffer.getInt() & 0x00000000ffffffffL;\n      output[outputIndex] = ENCODE_MAP[(int) (sum / BASE_4TH_POWER)];\n      sum %= BASE_4TH_POWER;\n      output[outputIndex + 1] = ENCODE_MAP[(int) (sum / BASE_3RD_POWER)];\n      sum %= BASE_3RD_POWER;\n      output[outputIndex + 2] = ENCODE_MAP[(int) (sum / BASE_2ND_POWER)];\n      sum %= BASE_2ND_POWER;\n      output[outputIndex + 3] = ENCODE_MAP[(int) (sum / BASE)];\n      output[outputIndex + 4] = ENCODE_MAP[(int) (sum % BASE)];\n      outputIndex += 5;\n    }\n\n    return new String(output, US_ASCII);\n  }\n\n  /**\n   * Encode an arbitrary byte array.\n   *\n   * <p>Unaligned input will be padded to a multiple of 4 bytes.\n   */\n  public static String encodeBytes(byte[] input) {\n    if (input.length % 4 == 0) {\n      return encodeBlocks(ByteBuffer.wrap(input));\n    } else {\n      int alignedLength = ((input.length + 4) / 4) * 4;\n      ByteBuffer buffer = ByteBuffer.allocate(alignedLength);\n      buffer.put(input);\n      while (buffer.hasRemaining()) {\n        buffer.put((byte) 0);\n      }\n      buffer.rewind();\n      return encodeBlocks(buffer);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/deletionvectors/DeletionVectorStoredBitmap.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.deletionvectors;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.engine.FileReadRequest;\nimport io.delta.kernel.engine.FileSystemClient;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.ByteArrayInputStream;\nimport java.io.DataInputStream;\nimport java.io.IOException;\nimport java.util.Optional;\nimport java.util.zip.CRC32;\n\n/**\n * Bitmap for a Deletion Vector, implemented as a thin wrapper around a Deletion Vector Descriptor.\n * The bitmap can be empty, inline or on-disk. In case of on-disk deletion vectors, `tableDataPath`\n * must be set to the data path of the Delta table, which is where deletion vectors are stored.\n */\npublic class DeletionVectorStoredBitmap {\n\n  private final DeletionVectorDescriptor dvDescriptor;\n  private final Optional<String> tableDataPath;\n\n  public DeletionVectorStoredBitmap(\n      DeletionVectorDescriptor dvDescriptor, Optional<String> tableDataPath) {\n    checkArgument(\n        tableDataPath.isPresent() || !dvDescriptor.isOnDisk(),\n        \"Table path is required for on-disk deletion vectors\");\n    this.dvDescriptor = dvDescriptor;\n    this.tableDataPath = tableDataPath;\n  }\n\n  // TODO: for now we request 1 stream at a time\n  public RoaringBitmapArray load(FileSystemClient fileSystemClient) throws IOException {\n    if (dvDescriptor.getCardinality() == 0) { // isEmpty\n      return new RoaringBitmapArray();\n    } else if (dvDescriptor.isInline()) {\n      return RoaringBitmapArray.readFrom(dvDescriptor.inlineData());\n    } else { // isOnDisk\n      String onDiskPath = dvDescriptor.getAbsolutePath(tableDataPath.get());\n\n      FileReadRequest dvToRead =\n          new FileReadRequest() {\n            @Override\n            public String getPath() {\n              return onDiskPath;\n            }\n\n            @Override\n            public int getStartOffset() {\n              return dvDescriptor.getOffset().orElse(0);\n            }\n\n            @Override\n            public int getReadLength() {\n              // We pad 4 bytes in the front for the size and 4 bytes at the end for\n              // CRC-32 checksum\n              return dvDescriptor.getSizeInBytes() + 8;\n            }\n          };\n\n      CloseableIterator<ByteArrayInputStream> streamIter =\n          wrapEngineExceptionThrowsIO(\n              () -> fileSystemClient.readFiles(Utils.singletonCloseableIterator(dvToRead)),\n              \"Reading file %s\",\n              dvToRead);\n      ByteArrayInputStream stream =\n          InternalUtils.getSingularElement(streamIter)\n              .orElseThrow(() -> new IllegalStateException(\"Iterator should not be empty\"));\n      return loadFromStream(stream);\n    }\n  }\n\n  /** Read a serialized deletion vector from a data stream. */\n  private RoaringBitmapArray loadFromStream(ByteArrayInputStream stream) throws IOException {\n    DataInputStream dataStream = new DataInputStream(stream);\n    try {\n      int sizeAccordingToFile = dataStream.readInt();\n      if (dvDescriptor.getSizeInBytes() != sizeAccordingToFile) {\n        throw new RuntimeException(\"DV size mismatch\");\n      }\n\n      byte[] buffer = new byte[sizeAccordingToFile];\n      dataStream.readFully(buffer);\n\n      int expectedChecksum = dataStream.readInt();\n      int actualChecksum = calculateChecksum(buffer);\n      if (expectedChecksum != actualChecksum) {\n        throw new RuntimeException(\"DV checksum mismatch\");\n      }\n      return RoaringBitmapArray.readFrom(buffer);\n    } finally {\n      stream.close();\n      dataStream.close();\n    }\n  }\n\n  /**\n   * Calculate checksum of a serialized deletion vector. We are using CRC32 which has 4bytes size,\n   * but CRC32 implementation conforms to Java Checksum interface which requires a long. However,\n   * the high-order bytes are zero, so here is safe to cast to Int. This will result in negative\n   * checksums, but this is not a problem because we only care about equality.\n   */\n  private int calculateChecksum(byte[] data) {\n    CRC32 crc = new CRC32();\n    crc.update(data);\n    return (int) crc.getValue();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/deletionvectors/DeletionVectorUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.deletionvectors;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.util.Tuple2;\nimport java.io.IOException;\nimport java.util.Optional;\n\n/** Utility methods regarding deletion vectors. */\npublic class DeletionVectorUtils {\n  public static Tuple2<DeletionVectorDescriptor, RoaringBitmapArray> loadNewDvAndBitmap(\n      Engine engine, String tablePath, DeletionVectorDescriptor dv) {\n    DeletionVectorStoredBitmap storedBitmap =\n        new DeletionVectorStoredBitmap(dv, Optional.of(tablePath));\n    try {\n      RoaringBitmapArray bitmap = storedBitmap.load(engine.getFileSystemClient());\n      return new Tuple2<>(dv, bitmap);\n    } catch (IOException e) {\n      throw new RuntimeException(\"Couldn't load dv\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/deletionvectors/RoaringBitmapArray.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.deletionvectors;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.internal.util.Tuple2;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.ArrayList;\nimport org.roaringbitmap.IntIterator;\nimport org.roaringbitmap.RoaringBitmap;\n\n/**\n * A 64-bit extension of [[RoaringBitmap]] that is optimized for cases that usually fit within a\n * 32-bit bitmap, but may run over by a few bits on occasion.\n *\n * <p>This focus makes it different from [[org.roaringbitmap.longlong.Roaring64NavigableMap]] and\n * [[org.roaringbitmap.longlong.Roaring64Bitmap]] which focus on sparse bitmaps over the whole\n * 64-bit range.\n *\n * <p>Structurally, this implementation simply uses the most-significant 4 bytes to index into an\n * array of 32-bit [[RoaringBitmap]] instances. The array is grown as necessary to accommodate the\n * largest value in the bitmap.\n *\n * <p>*Note:* As opposed to the other two 64-bit bitmap implementations mentioned above, this\n * implementation cannot accommodate `Long` values where the most significant bit is non-zero (i.e.,\n * negative `Long` values). It cannot even accommodate values where the 4 high-order bytes are\n * `Int.MaxValue`, because then the length of the `bitmaps` array would be a negative number\n * (`Int.MaxValue + 1`).\n *\n * <p>Taken from https://github.com/delta-io/delta/blob/master/spark/src/main/scala/org/apache/spark\n * /sql/delta/deletionvectors/RoaringBitmapArray.scala\n */\npublic final class RoaringBitmapArray {\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Static Fields / Methods\n  ////////////////////////////////////////////////////////////////////////////////\n\n  /** The largest value a [[RoaringBitmapArray]] can possibly represent. */\n  static final long MAX_REPRESENTABLE_VALUE =\n      composeFromHighLowBytes(Integer.MAX_VALUE - 1, Integer.MIN_VALUE);\n\n  /**\n   * @param value Any `Long`; positive or negative.\n   * @return An `Int` holding the 4 high-order bytes of information of the input `value`.\n   */\n  static int highBytes(long value) {\n    return (int) (value >> 32);\n  }\n\n  /**\n   * @param value Any `Long`; positive or negative.\n   * @return An `Int` holding the 4 low-order bytes of information of the input `value`.\n   */\n  static int lowBytes(long value) {\n    return (int) value;\n  }\n\n  /**\n   * Combine high and low 4 bytes of a pair of `Int`s into a `Long`.\n   *\n   * <p>This is essentially the inverse of [[decomposeHighLowBytes()]].\n   *\n   * @param high An `Int` representing the 4 high-order bytes of the output `Long`\n   * @param low An `Int` representing the 4 low-order bytes of the output `Long`\n   * @return A `Long` composing the `high` and `low` bytes.\n   */\n  static long composeFromHighLowBytes(int high, int low) {\n    // Must bitmask to avoid sign extension.\n    return (((long) high) << 32) | (((long) low) & 0xFFFFFFFFL);\n  }\n\n  /** Deserialize the right instance from the given bytes */\n  static RoaringBitmapArray readFrom(byte[] bytes) throws IOException {\n    ByteBuffer buffer = ByteBuffer.wrap(bytes);\n    buffer.order(ByteOrder.LITTLE_ENDIAN);\n    RoaringBitmapArray bitmap = new RoaringBitmapArray();\n    bitmap.deserialize(buffer);\n    return bitmap;\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Instance Fields / Methods\n  ////////////////////////////////////////////////////////////////////////////////\n\n  private RoaringBitmap[] bitmaps = new RoaringBitmap[0];\n\n  /**\n   * Deserialize the contents of `buffer` into this [[RoaringBitmapArray]].\n   *\n   * <p>All existing content will be discarded!\n   *\n   * <p>== Serialization Format == - A Magic Number indicating the format used (4 bytes) - The\n   * actual data as specified by the format.\n   */\n  void deserialize(ByteBuffer buffer) throws IOException {\n    checkArgument(\n        ByteOrder.LITTLE_ENDIAN == buffer.order(),\n        \"RoaringBitmapArray has to be deserialized using a little endian buffer\");\n\n    int magicNumber = buffer.getInt();\n    if (magicNumber == NativeRoaringBitmapArraySerializationFormat.MAGIC_NUMBER) {\n      bitmaps = NativeRoaringBitmapArraySerializationFormat.deserialize(buffer);\n    } else if (magicNumber == PortableRoaringBitmapArraySerializationFormat.MAGIC_NUMBER) {\n      bitmaps = PortableRoaringBitmapArraySerializationFormat.deserialize(buffer);\n    } else {\n      throw new IOException(\"Unexpected RoaringBitmapArray magic number \" + magicNumber);\n    }\n  }\n\n  /**\n   * Checks whether the value is included, which is equivalent to checking if the corresponding bit\n   * is set.\n   */\n  public boolean contains(long value) {\n    checkArgument(value >= 0 && value <= MAX_REPRESENTABLE_VALUE);\n    int high = highBytes(value);\n    if (high >= bitmaps.length) {\n      return false;\n    } else {\n      RoaringBitmap highBitmap = bitmaps[high];\n      int low = lowBytes(value);\n      return highBitmap.contains(low);\n    }\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Serialization Formats\n  ////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * == Serialization Format == - Number of bitmaps (4 bytes) - For each individual bitmap: - Length\n   * of the serialized bitmap - Serialized bitmap data using the standard format (see\n   * https://github.com/RoaringBitmap/RoaringFormatSpec)\n   */\n  static class NativeRoaringBitmapArraySerializationFormat {\n    /** Magic number prefix for serialization with this format. */\n    static final int MAGIC_NUMBER = 1681511376;\n\n    /** Deserialize all bitmaps from the `buffer` into a fresh array. */\n    static RoaringBitmap[] deserialize(ByteBuffer buffer) throws IOException {\n      int numberOfBitmaps = buffer.getInt();\n      if (numberOfBitmaps < 0) {\n        throw new IOException(\n            String.format(\"Invalid RoaringBitmapArray length (%s < 0)\", numberOfBitmaps));\n      }\n      RoaringBitmap[] bitmaps = new RoaringBitmap[numberOfBitmaps];\n      for (int i = 0; i < numberOfBitmaps; i++) {\n        bitmaps[i] = new RoaringBitmap();\n        int bitmapSize = buffer.getInt();\n        bitmaps[i].deserialize(buffer);\n        // RoaringBitmap.deserialize doesn't move the buffer's pointer\n        buffer.position(buffer.position() + bitmapSize);\n      }\n      return bitmaps;\n    }\n  }\n\n  /**\n   * This is the \"official\" portable format defined in the spec.\n   *\n   * <p>See [[https://github.com/RoaringBitmap/RoaringFormatSpec#extention-for-64-bit\n   * -implementations]]\n   *\n   * <p>== Serialization Format == - Number of bitmaps (8 bytes, upper 4 are basically padding) -\n   * For each individual bitmap, in increasing key order (unsigned, technically, but\n   * RoaringBitmapArray doesn't support negative keys anyway.): - key of the bitmap (upper 32 bit) -\n   * Serialized bitmap data using the standard format (see\n   * https://github.com/RoaringBitmap/RoaringFormatSpec)\n   */\n  static class PortableRoaringBitmapArraySerializationFormat {\n    /** Magic number prefix for serialization with this format. */\n    static final int MAGIC_NUMBER = 1681511377;\n\n    /** Deserialize all bitmaps from the `buffer` into a fresh array. */\n    static RoaringBitmap[] deserialize(ByteBuffer buffer) throws IOException {\n      long numberOfBitmaps = buffer.getLong();\n      if (numberOfBitmaps < 0) {\n        throw new IOException(\n            String.format(\"Invalid RoaringBitmapArray length (%s < 0)\", numberOfBitmaps));\n      }\n      if (numberOfBitmaps > Integer.MAX_VALUE) {\n        throw new IOException(\n            String.format(\n                \"Invalid RoaringBitmapArray length (%s > %s)\", numberOfBitmaps, Integer.MAX_VALUE));\n      }\n      // This format is designed for sparse bitmaps, so numberOfBitmaps is only a lower bound\n      // for the actual size of the array.\n      int minimumArraySize = (int) numberOfBitmaps;\n      ArrayList<RoaringBitmap> bitmaps = new ArrayList<>(minimumArraySize);\n      int lastIndex = 0;\n      for (long i = 0; i < numberOfBitmaps; i++) {\n        int key = buffer.getInt();\n        if (key < 0L) {\n          throw new IOException(\n              String.format(\"Invalid unsigned entry in RoaringBitmapArray (%s)\", key));\n        }\n        assert key >= lastIndex : \"Keys are required to be sorted in ascending order.\";\n        // Fill gaps in sparse data.\n        while (lastIndex < key) {\n          bitmaps.add(new RoaringBitmap());\n          lastIndex += 1;\n        }\n        RoaringBitmap bitmap = new RoaringBitmap();\n        bitmap.deserialize(buffer);\n        bitmaps.add(bitmap);\n        lastIndex += 1;\n        // RoaringBitmap.deserialize doesn't move the buffer's pointer\n        buffer.position(buffer.position() + bitmap.serializedSizeInBytes());\n      }\n      return bitmaps.toArray(new RoaringBitmap[0]);\n    }\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Methods implemented for testing only\n  ////////////////////////////////////////////////////////////////////////////////\n\n  static Tuple2<Integer, Integer> decomposeHighLowBytes(long value) {\n    return new Tuple2<>(highBytes(value), lowBytes(value));\n  }\n\n  public void add(long value) {\n    checkArgument(value >= 0 && value <= MAX_REPRESENTABLE_VALUE);\n    Tuple2<Integer, Integer> tup = decomposeHighLowBytes(value); // (high, low)\n    if (tup._1 >= bitmaps.length) {\n      extendBitmaps(tup._1 + 1);\n    }\n    RoaringBitmap highBitmap = bitmaps[tup._1];\n    highBitmap.add(tup._2);\n  }\n\n  private void extendBitmaps(int newLength) {\n    if (bitmaps.length == 0 && newLength == 1) {\n      bitmaps = new RoaringBitmap[] {new RoaringBitmap()};\n      return;\n    }\n    RoaringBitmap[] newBitmaps = new RoaringBitmap[newLength];\n    System.arraycopy(\n        bitmaps, // source\n        0, // source start pos\n        newBitmaps, // dest\n        0, // dest start pos\n        bitmaps.length); // number of entries to copy\n    for (int i = bitmaps.length; i < newBitmaps.length; i++) {\n      newBitmaps[i] = new RoaringBitmap();\n    }\n    bitmaps = newBitmaps;\n  }\n\n  public static RoaringBitmapArray create(long... values) {\n    RoaringBitmapArray bitmap = new RoaringBitmapArray();\n    for (long value : values) {\n      bitmap.add(value);\n    }\n    return bitmap;\n  }\n\n  public long[] toArray() {\n    long cardinality = 0;\n    for (final RoaringBitmap bitmap : bitmaps) {\n      cardinality += bitmap.getCardinality();\n    }\n    checkArgument(cardinality <= Integer.MAX_VALUE, \"Cardinality higher than max int value\");\n\n    final long[] values = new long[(int) cardinality];\n\n    int valuesIndex = 0;\n    for (int bitmapIndex = 0; bitmapIndex < bitmaps.length; bitmapIndex++) {\n      final IntIterator valueIterator = bitmaps[bitmapIndex].getIntIterator();\n      while (valueIterator.hasNext()) {\n        final int value = valueIterator.next();\n        values[valuesIndex++] = composeFromHighLowBytes(bitmapIndex, value);\n      }\n    }\n\n    return values;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/LogDataUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.internal.lang.ListUtils;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic final class LogDataUtils {\n\n  private LogDataUtils() {}\n\n  public static void validateLogDataContainsOnlyRatifiedStagedCommits(\n      List<? extends ParsedLogData> logDatas) {\n    for (ParsedLogData logData : logDatas) {\n      checkArgument(\n          logData instanceof ParsedCatalogCommitData && logData.isFile(),\n          \"Only staged ratified commits are supported, but found: \" + logData);\n    }\n  }\n\n  public static void validateLogDataIsSortedContiguous(List<? extends ParsedLogData> logDatas) {\n    if (logDatas.size() > 1) {\n      for (int i = 1; i < logDatas.size(); i++) {\n        final ParsedLogData prev = logDatas.get(i - 1);\n        final ParsedLogData curr = logDatas.get(i);\n        checkArgument(\n            prev.getVersion() + 1 == curr.getVersion(),\n            String.format(\n                \"Log data must be sorted and contiguous, but found: %s and %s\", prev, curr));\n      }\n    }\n  }\n\n  /**\n   * Combines a list of published Deltas and ratified Deltas into a single list of Deltas such that\n   * there is exactly one {@link ParsedDeltaData} per version. When there is both a published Delta\n   * and a ratified staged Delta for the same version, prioritizes the ratified Delta.\n   *\n   * <p>The method requires but does not validate the following:\n   *\n   * <ul>\n   *   <li>{@code publishedDeltas} are sorted and contiguous\n   *   <li>{@code ratifiedDeltas} are sorted and contiguous\n   *   <li>the commit versions present in {@code publishedDeltas} and {@code ratifiedDeltas}, when\n   *       combined, reflect a contiguous version range. In other words, if the two do not overlap,\n   *       publishedDeltas.last = ratifiedDeltas.first + 1).\n   * </ul>\n   */\n  public static List<ParsedDeltaData> combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      List<ParsedDeltaData> publishedDeltas, List<ParsedDeltaData> ratifiedDeltas) {\n    if (ratifiedDeltas.isEmpty()) {\n      return publishedDeltas;\n    }\n\n    if (publishedDeltas.isEmpty()) {\n      return ratifiedDeltas;\n    }\n\n    final long firstRatified = ratifiedDeltas.get(0).getVersion();\n    final long lastRatified = ListUtils.getLast(ratifiedDeltas).getVersion();\n\n    return Stream.of(\n            publishedDeltas.stream().filter(x -> x.getVersion() < firstRatified),\n            ratifiedDeltas.stream(),\n            publishedDeltas.stream().filter(x -> x.getVersion() > lastRatified))\n        .flatMap(Function.identity())\n        .collect(Collectors.toList());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedCatalogCommitData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/**\n * A catalog commit represents an atomic change to a table.\n *\n * <p>Can be staged and written to a staged commit file, like so: {@code\n * _delta_log/_staged_commits/00000000000000000001.uuid-1234.json}.\n *\n * <p>Can also be inline.\n */\npublic final class ParsedCatalogCommitData extends ParsedDeltaData {\n\n  public static ParsedCatalogCommitData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isStagedDeltaFile(fileStatus.getPath()),\n        \"Expected a staged commit file but got %s\",\n        fileStatus.getPath());\n\n    final String path = fileStatus.getPath();\n    final long version = FileNames.deltaVersion(path);\n    return new ParsedCatalogCommitData(version, Optional.of(fileStatus), Optional.empty());\n  }\n\n  public static ParsedCatalogCommitData forInlineData(long version, ColumnarBatch inlineData) {\n    return new ParsedCatalogCommitData(version, Optional.empty(), Optional.of(inlineData));\n  }\n\n  private ParsedCatalogCommitData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  @Override\n  public Class<? extends ParsedLogData> getGroupByCategoryClass() {\n    return ParsedCatalogCommitData.class;\n  }\n\n  // TODO: String getPublishedDeltaFilePath(); Requires forInlineData to take in logPath\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedCheckpointData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/**\n * Abstract checkpoint file that contains a complete snapshot of table state at a specific version.\n */\npublic abstract class ParsedCheckpointData extends ParsedLogData\n    implements Comparable<ParsedCheckpointData> {\n\n  public static ParsedCheckpointData forFileStatus(FileStatus fileStatus) {\n    final String path = fileStatus.getPath();\n\n    if (FileNames.isClassicCheckpointFile(path)) {\n      return ParsedClassicCheckpointData.forFileStatus(fileStatus);\n    } else if (FileNames.isV2CheckpointFile(path)) {\n      return ParsedV2CheckpointData.forFileStatus(fileStatus);\n    } else if (FileNames.isMultiPartCheckpointFile(path)) {\n      return ParsedMultiPartCheckpointData.forFileStatus(fileStatus);\n    } else {\n      throw new IllegalArgumentException(\"Unknown checkpoint file type: \" + path);\n    }\n  }\n\n  /**\n   * Enum representing checkpoint type priorities for comparison when multiple checkpoint types\n   * exist at the same version. Higher ordinal values indicate higher priority.\n   */\n  protected enum CheckpointTypePriority {\n    CLASSIC, // priority 0 - least preferred\n    MULTIPART, // priority 1 - better than classic\n    V2 // priority 2 - most preferred\n  }\n\n  protected ParsedCheckpointData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  /**\n   * Returns the checkpoint type priority used as a tiebreaker when multiple checkpoint types exist\n   * at the same version.\n   */\n  protected abstract CheckpointTypePriority getCheckpointTypePriority();\n\n  /**\n   * Compares two checkpoints of the same version and same type. Subclasses should implement\n   * type-specific comparison logic for final tiebreaking.\n   */\n  protected abstract int compareToSameType(ParsedCheckpointData that);\n\n  @Override\n  public Class<? extends ParsedLogData> getGroupByCategoryClass() {\n    return ParsedCheckpointData.class;\n  }\n\n  /**\n   * Compares checkpoints for ordering preference. Returns positive if *this* checkpoint is\n   * preferred over *that* checkpoint, negative if *that* is preferred, or zero if equal.\n   *\n   * <p>Comparison hierarchy:\n   *\n   * <ol>\n   *   <li><strong>Version (most important):</strong> Higher version numbers are always preferred\n   *       over lower ones, as newer checkpoints contain more recent data\n   *   <li><strong>Checkpoint type:</strong> When versions are equal, prefer by type priority based\n   *       on safety and performance characteristics (V2 &gt; MultiPart &gt; Classic)\n   *   <li><strong>Type-specific logic:</strong> When version and type are equal, use type-specific\n   *       comparison (e.g., MultiPart prefers more parts for better parallelization)\n   * </ol>\n   */\n  @Override\n  public int compareTo(ParsedCheckpointData that) {\n    // 1. Compare versions - newer checkpoints are always preferred\n    if (version != that.version) {\n      return Long.compare(version, that.version);\n    }\n\n    // 2. Compare types by priority (V2 > MultiPart > Classic)\n    CheckpointTypePriority thisTypePriority = this.getCheckpointTypePriority();\n    CheckpointTypePriority thatTypePriority = that.getCheckpointTypePriority();\n    if (thisTypePriority != thatTypePriority) {\n      return thisTypePriority.compareTo(thatTypePriority);\n    }\n\n    // 3. Use type-specific comparison when version and type are the same\n    return compareToSameType(that);\n  }\n\n  /**\n   * Compares checkpoints by data source preference and deterministic tiebreaking.\n   *\n   * <p>Prefers inline data to file data because inline data is already loaded in memory, avoiding\n   * the need for additional file I/O operations.\n   *\n   * <p>When both are files or both are inline, uses lexicographic path comparison as an arbitrary\n   * but deterministic tiebreaker to ensure consistent ordering.\n   */\n  protected final int compareByDataSource(ParsedCheckpointData that) {\n    if (this.isInline() && that.isFile()) {\n      return 1; // Prefer this (inline data)\n    } else if (this.isFile() && that.isInline()) {\n      return -1; // Prefer that (inline data)\n    } else if (this.isFile() && that.isFile()) {\n      // Both are files - use path as arbitrary but deterministic tiebreaker\n      return this.getFileStatus().getPath().compareTo(that.getFileStatus().getPath());\n    } else {\n      // Both are inline - no preference\n      return 0;\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedChecksumData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/**\n * Version checksum file containing table state information for integrity validation.\n *\n * <p>These auxiliary files contain important metadata about the table state at a specific version\n * to enable detection of non-compliant modifications to Delta files. Contains information like\n * table size, file counts, and metadata. Example: {@code 00000000000000000001.crc}\n */\npublic final class ParsedChecksumData extends ParsedLogData {\n\n  public static ParsedChecksumData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isChecksumFile(fileStatus.getPath()),\n        \"Expected a checksum file but got %s\",\n        fileStatus.getPath());\n\n    final String path = fileStatus.getPath();\n    final long version = FileNames.checksumVersion(path);\n    return new ParsedChecksumData(version, Optional.of(fileStatus), Optional.empty());\n  }\n\n  private ParsedChecksumData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  @Override\n  public Class<? extends ParsedLogData> getGroupByCategoryClass() {\n    return ParsedChecksumData.class;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedClassicCheckpointData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/**\n * Classic checkpoint stored as a single Parquet file.\n *\n * <p>Example: {@code 00000000000000000001.checkpoint.parquet}\n */\npublic final class ParsedClassicCheckpointData extends ParsedCheckpointData {\n\n  public static ParsedClassicCheckpointData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isClassicCheckpointFile(fileStatus.getPath()),\n        \"Expected a classic checkpoint file but got %s\",\n        fileStatus.getPath());\n\n    final String path = fileStatus.getPath();\n    final long version = FileNames.checkpointVersion(path);\n    return new ParsedClassicCheckpointData(version, Optional.of(fileStatus), Optional.empty());\n  }\n\n  private ParsedClassicCheckpointData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  @Override\n  protected CheckpointTypePriority getCheckpointTypePriority() {\n    return CheckpointTypePriority.CLASSIC;\n  }\n\n  @Override\n  protected int compareToSameType(ParsedCheckpointData that) {\n    return compareByDataSource(that);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedDeltaData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/** Base class for Delta types that represent atomic changes to a table. */\npublic abstract class ParsedDeltaData extends ParsedLogData {\n\n  public static ParsedDeltaData forFileStatus(FileStatus fileStatus) {\n    final String path = fileStatus.getPath();\n\n    if (FileNames.isPublishedDeltaFile(path)) {\n      return ParsedPublishedDeltaData.forFileStatus(fileStatus);\n    } else if (FileNames.isStagedDeltaFile(path)) {\n      return ParsedCatalogCommitData.forFileStatus(fileStatus);\n    } else {\n      throw new IllegalArgumentException(\"Unknown delta file type: \" + path);\n    }\n  }\n\n  protected ParsedDeltaData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedLogCompactionData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/**\n * Log compaction file containing compacted delta entries across a version range.\n *\n * <p>These files compact multiple delta files into a single JSON file to reduce the number of files\n * readers need to process. Example: {@code\n * 00000000000000000001.00000000000000000009.compacted.json} represents compacted entries from\n * version 1 to 9.\n */\n// TODO: Add the comparable logic from CheckpointInstance.\npublic final class ParsedLogCompactionData extends ParsedLogData {\n  public static ParsedLogCompactionData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isLogCompactionFile(fileStatus.getPath()),\n        \"Expected a log compaction file but got %s\",\n        fileStatus.getPath());\n\n    final Tuple2<Long, Long> startEnd = FileNames.logCompactionVersions(fileStatus.getPath());\n    return new ParsedLogCompactionData(\n        startEnd._1, startEnd._2, Optional.of(fileStatus), Optional.empty());\n  }\n\n  public final long startVersion;\n  public final long endVersion;\n\n  private ParsedLogCompactionData(\n      long startVersion,\n      long endVersion,\n      Optional<FileStatus> fileStatusOpt,\n      Optional<ColumnarBatch> inlineDataOpt) {\n    super(endVersion, fileStatusOpt, inlineDataOpt);\n    checkArgument(\n        startVersion >= 0 && endVersion >= 0, \"startVersion and endVersion must be non-negative\");\n    checkArgument(startVersion < endVersion, \"startVersion must be less than endVersion\");\n    this.startVersion = startVersion;\n    this.endVersion = endVersion;\n  }\n\n  @Override\n  public Class<? extends ParsedLogData> getGroupByCategoryClass() {\n    return ParsedLogCompactionData.class;\n  }\n\n  @Override\n  protected void appendAdditionalToStringFields(StringBuilder sb) {\n    sb.append(\", startVersion=\").append(startVersion);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    if (!super.equals(o)) {\n      return false;\n    }\n    ParsedLogCompactionData that = (ParsedLogCompactionData) o;\n    return startVersion == that.startVersion && endVersion == that.endVersion;\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(super.hashCode(), startVersion, endVersion);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedLogData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.NoSuchElementException;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/**\n * Abstract representation of any valid log type in the Delta log.\n *\n * <p>Different child classes are used to represent the different log types.\n *\n * <p>Any given log type can be written as a file or represented inline and given to Kernel as a\n * {@link ColumnarBatch}. That is: Kernel just needs to know how to parse and interpret a given log\n * type (Kernel will of course treat Deltas differently than Checksums) as well as how to get that\n * log type's bytes. This is why a given log type can be represented as either a file or inline.\n *\n * <p>For now, our APIs only allow creating {@link ParsedCatalogCommitData} inline, but we may\n * change and expand this capability in the future.\n *\n * <p>The supported log types are:\n *\n * <ul>\n *   <li>Published Deltas: {@code 00000000000000000001.json}\n *   <li>Catalog Commits: {@code _staged_commits/00000000000000000001.uuid-1234.json}\n *   <li>Log compaction files: {@code 00000000000000000001.00000000000000000009.compacted.json}\n *   <li>Checksum files: {@code 00000000000000000001.crc}\n *   <li>Classic Checkpoint files: {@code 00000000000000000001.checkpoint.parquet}\n *   <li>V2 checkpoint files: {@code 00000000000000000001.checkpoint.uuid-1234.json}\n *   <li>Multi-part checkpoint files: {@code\n *       00000000000000000001.checkpoint.0000000001.0000000010.parquet}\n * </ul>\n */\n// TODO: Move this to be a public API\npublic abstract class ParsedLogData {\n\n  public static ParsedLogData forFileStatus(FileStatus fileStatus) {\n    final String path = fileStatus.getPath();\n\n    if (FileNames.isCommitFile(path)) {\n      return ParsedDeltaData.forFileStatus(fileStatus);\n    } else if (FileNames.isCheckpointFile(path)) {\n      return ParsedCheckpointData.forFileStatus(fileStatus);\n    } else if (FileNames.isLogCompactionFile(path)) {\n      return ParsedLogCompactionData.forFileStatus(fileStatus);\n    } else if (FileNames.isChecksumFile(path)) {\n      return ParsedChecksumData.forFileStatus(fileStatus);\n    } else {\n      throw new IllegalArgumentException(\"Unknown log file type: \" + path);\n    }\n  }\n\n  ///////////////////////////////\n  // Member fields and methods //\n  ///////////////////////////////\n\n  protected final long version;\n  protected final Optional<FileStatus> fileStatusOpt;\n  protected final Optional<ColumnarBatch> inlineDataOpt;\n\n  protected ParsedLogData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    checkArgument(\n        fileStatusOpt.isPresent() ^ inlineDataOpt.isPresent(),\n        \"Exactly one of fileStatusOpt or inlineDataOpt must be present\");\n    checkArgument(version >= 0, \"version must be non-negative\");\n    this.version = version;\n    this.fileStatusOpt = fileStatusOpt;\n    this.inlineDataOpt = inlineDataOpt;\n  }\n\n  /**\n   * Returns true if this log data is stored as a file on disk. When false, the data is stored\n   * inline.\n   */\n  public boolean isFile() {\n    return fileStatusOpt.isPresent();\n  }\n\n  /**\n   * Returns true if this log data is stored inline as a ColumnarBatch. When false, the data is\n   * stored as a file on disk.\n   */\n  public boolean isInline() {\n    return inlineDataOpt.isPresent();\n  }\n\n  /** Return the version of this log data. */\n  public long getVersion() {\n    return version;\n  }\n\n  /**\n   * Callers must check {@link #isFile()} before calling this method.\n   *\n   * @throws NoSuchElementException if {@link #isFile()} is false\n   */\n  public FileStatus getFileStatus() {\n    return fileStatusOpt.get();\n  }\n\n  /**\n   * Callers must check {@link #isInline()} before calling this method.\n   *\n   * @throws NoSuchElementException if {@link #isInline()} is false\n   */\n  public ColumnarBatch getInlineData() {\n    return inlineDataOpt.get();\n  }\n\n  /** Returns the category class used for grouping collections of LISTed ParsedLogData instances. */\n  public abstract Class<? extends ParsedLogData> getGroupByCategoryClass();\n\n  /** Protected method for subclasses to override to add output to {@link #toString}. */\n  protected void appendAdditionalToStringFields(StringBuilder sb) {\n    // Default implementation does nothing\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    ParsedLogData that = (ParsedLogData) o;\n    return version == that.version\n        && Objects.equals(fileStatusOpt, that.fileStatusOpt)\n        && Objects.equals(inlineDataOpt, that.inlineDataOpt);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  @Override\n  public String toString() {\n    final StringBuilder sb =\n        new StringBuilder(getClass().getSimpleName())\n            .append(\"{version=\")\n            .append(version)\n            .append(\", source=\");\n    if (isFile()) {\n      sb.append(fileStatusOpt.get());\n    } else {\n      sb.append(\"inline\");\n    }\n\n    appendAdditionalToStringFields(sb);\n\n    sb.append('}');\n    return sb.toString();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedMultiPartCheckpointData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/**\n * Multi-part checkpoint split across multiple Parquet files for parallel reading.\n *\n * <p>Example: {@code 00000000000000000001.checkpoint.0000000001.0000000010.parquet}\n */\npublic final class ParsedMultiPartCheckpointData extends ParsedCheckpointData {\n  public static ParsedMultiPartCheckpointData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isMultiPartCheckpointFile(fileStatus.getPath()),\n        \"Expected a multi-part checkpoint file but got %s\",\n        fileStatus.getPath());\n\n    final long version = FileNames.checkpointVersion(fileStatus.getPath());\n    final Tuple2<Integer, Integer> partInfo =\n        FileNames.multiPartCheckpointPartAndNumParts(fileStatus.getPath());\n    return new ParsedMultiPartCheckpointData(\n        version, partInfo._1, partInfo._2, Optional.of(fileStatus), Optional.empty());\n  }\n\n  public final int part;\n  public final int numParts;\n\n  private ParsedMultiPartCheckpointData(\n      long version,\n      int part,\n      int numParts,\n      Optional<FileStatus> fileStatusOpt,\n      Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n    checkArgument(numParts > 0, \"numParts must be greater than 0\");\n    checkArgument(part > 0 && part <= numParts, \"part must be between 1 and numParts\");\n    this.part = part;\n    this.numParts = numParts;\n  }\n\n  @Override\n  protected CheckpointTypePriority getCheckpointTypePriority() {\n    return CheckpointTypePriority.MULTIPART;\n  }\n\n  @Override\n  protected int compareToSameType(ParsedCheckpointData that) {\n    // For multi-part checkpoints, prefer more parts as they enable better parallelization\n    if (that instanceof ParsedMultiPartCheckpointData) {\n      ParsedMultiPartCheckpointData other = (ParsedMultiPartCheckpointData) that;\n      int numPartsComparison = Integer.compare(this.numParts, other.numParts);\n      if (numPartsComparison != 0) {\n        return numPartsComparison;\n      }\n    }\n    return compareByDataSource(that);\n  }\n\n  @Override\n  protected void appendAdditionalToStringFields(StringBuilder sb) {\n    sb.append(\", part=\").append(part).append(\", numParts=\").append(numParts);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    if (!super.equals(o)) {\n      return false;\n    }\n    ParsedMultiPartCheckpointData that = (ParsedMultiPartCheckpointData) o;\n    return part == that.part && numParts == that.numParts;\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(super.hashCode(), part, numParts);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedPublishedDeltaData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/**\n * A published Delta commit file represent an atomic change to a table.\n *\n * <p>Example: {@code _delta_log/00000000000000000001.json}\n */\npublic final class ParsedPublishedDeltaData extends ParsedDeltaData {\n\n  public static ParsedPublishedDeltaData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isPublishedDeltaFile(fileStatus.getPath()),\n        \"Expected a published Delta file but got %s\",\n        fileStatus.getPath());\n\n    final String path = fileStatus.getPath();\n    final long version = FileNames.deltaVersion(path);\n    return new ParsedPublishedDeltaData(version, Optional.of(fileStatus), Optional.empty());\n  }\n\n  private ParsedPublishedDeltaData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  @Override\n  public Class<? extends ParsedLogData> getGroupByCategoryClass() {\n    return ParsedPublishedDeltaData.class;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/files/ParsedV2CheckpointData.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.Optional;\n\n/**\n * V2 checkpoint with UUID-based naming.\n *\n * <p>Example: {@code 00000000000000000001.checkpoint.80a083e8-7026-4e79-81be-64bd76c43a11.json}\n */\npublic final class ParsedV2CheckpointData extends ParsedCheckpointData {\n\n  public static ParsedV2CheckpointData forFileStatus(FileStatus fileStatus) {\n    checkArgument(\n        FileNames.isV2CheckpointFile(fileStatus.getPath()),\n        \"Expected a V2 checkpoint file but got %s\",\n        fileStatus.getPath());\n\n    final String path = fileStatus.getPath();\n    final long version = FileNames.checkpointVersion(path);\n    return new ParsedV2CheckpointData(version, Optional.of(fileStatus), Optional.empty());\n  }\n\n  private ParsedV2CheckpointData(\n      long version, Optional<FileStatus> fileStatusOpt, Optional<ColumnarBatch> inlineDataOpt) {\n    super(version, fileStatusOpt, inlineDataOpt);\n  }\n\n  @Override\n  protected CheckpointTypePriority getCheckpointTypePriority() {\n    return CheckpointTypePriority.V2;\n  }\n\n  @Override\n  protected int compareToSameType(ParsedCheckpointData that) {\n    return compareByDataSource(that);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/fs/Path.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.fs;\n\nimport java.io.InvalidObjectException;\nimport java.io.ObjectInputValidation;\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.regex.Pattern;\n\n/**\n * Names a file or directory in a FileSystem. Path strings use slash as the directory separator.\n *\n * <p>Taken from https://github.com/apache/hadoop/blob/branch-3.3\n * .4/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/Path.java\n */\npublic class Path implements Comparable<Path>, Serializable, ObjectInputValidation {\n\n  /** The directory separator, a slash. */\n  public static final String SEPARATOR = \"/\";\n\n  /** The directory separator, a slash, as a character. */\n  public static final char SEPARATOR_CHAR = '/';\n\n  /** The current directory, \".\". */\n  public static final String CUR_DIR = \".\";\n\n  /** Whether the current host is a Windows machine. */\n  public static final boolean WINDOWS = System.getProperty(\"os.name\").startsWith(\"Windows\");\n\n  /** Pre-compiled regular expressions to detect path formats. */\n  private static final Pattern HAS_DRIVE_LETTER_SPECIFIER = Pattern.compile(\"^/?[a-zA-Z]:\");\n\n  /** Pre-compiled regular expressions to detect duplicated slashes. */\n  private static final Pattern SLASHES = Pattern.compile(\"/+\");\n\n  private static final long serialVersionUID = 0xad00f;\n\n  private URI uri; // a hierarchical uri\n\n  /**\n   * Create a new Path based on the child path resolved against the parent path.\n   *\n   * @param parent the parent path\n   * @param child the child path\n   */\n  public Path(String parent, String child) {\n    this(new Path(parent), new Path(child));\n  }\n\n  /**\n   * Create a new Path based on the child path resolved against the parent path.\n   *\n   * @param parent the parent path\n   * @param child the child path\n   */\n  public Path(Path parent, String child) {\n    this(parent, new Path(child));\n  }\n\n  /**\n   * Create a new Path based on the child path resolved against the parent path.\n   *\n   * @param parent the parent path\n   * @param child the child path\n   */\n  public Path(String parent, Path child) {\n    this(new Path(parent), child);\n  }\n\n  /**\n   * Create a new Path based on the child path resolved against the parent path.\n   *\n   * @param parent the parent path\n   * @param child the child path\n   */\n  public Path(Path parent, Path child) {\n    // Add a slash to parent's path so resolution is compatible with URI's\n    URI parentUri = parent.uri;\n    String parentPath = parentUri.getPath();\n    if (!(parentPath.equals(\"/\") || parentPath.isEmpty())) {\n      try {\n        parentUri =\n            new URI(\n                parentUri.getScheme(),\n                parentUri.getAuthority(),\n                parentUri.getPath() + \"/\",\n                null,\n                parentUri.getFragment());\n      } catch (URISyntaxException e) {\n        throw new IllegalArgumentException(e);\n      }\n    }\n    URI resolved = parentUri.resolve(child.uri);\n    initialize(\n        resolved.getScheme(), resolved.getAuthority(), resolved.getPath(), resolved.getFragment());\n  }\n\n  private void checkPathArg(String path) throws IllegalArgumentException {\n    // disallow construction of a Path from an empty string\n    if (path == null) {\n      throw new IllegalArgumentException(\"Can not create a Path from a null string\");\n    }\n    if (path.length() == 0) {\n      throw new IllegalArgumentException(\"Can not create a Path from an empty string\");\n    }\n  }\n\n  /**\n   * Construct a path from a String. Path strings are URIs, but with unescaped elements and some\n   * additional normalization.\n   *\n   * @param pathString the path string\n   */\n  public Path(String pathString) throws IllegalArgumentException {\n    checkPathArg(pathString);\n\n    // We can't use 'new URI(String)' directly, since it assumes things are\n    // escaped, which we don't require of Paths.\n\n    // add a slash in front of paths with Windows drive letters\n    if (hasWindowsDrive(pathString) && pathString.charAt(0) != '/') {\n      pathString = \"/\" + pathString;\n    }\n\n    // parse uri components\n    String scheme = null;\n    String authority = null;\n\n    int start = 0;\n\n    // parse uri scheme, if any\n    int colon = pathString.indexOf(':');\n    int slash = pathString.indexOf('/');\n    if ((colon != -1) && ((slash == -1) || (colon < slash))) { // has a scheme\n      scheme = pathString.substring(0, colon);\n      start = colon + 1;\n    }\n\n    // parse uri authority, if any\n    if (pathString.startsWith(\"//\", start) && (pathString.length() - start > 2)) { // has authority\n      int nextSlash = pathString.indexOf('/', start + 2);\n      int authEnd = nextSlash > 0 ? nextSlash : pathString.length();\n      authority = pathString.substring(start + 2, authEnd);\n      start = authEnd;\n    }\n\n    // uri path is the rest of the string -- query & fragment not supported\n    String path = pathString.substring(start, pathString.length());\n\n    initialize(scheme, authority, path, null);\n  }\n\n  /**\n   * Construct a path from a URI\n   *\n   * @param aUri the source URI\n   */\n  public Path(URI aUri) {\n    uri = aUri.normalize();\n  }\n\n  /**\n   * Construct a Path from components.\n   *\n   * @param scheme the scheme\n   * @param authority the authority\n   * @param path the path\n   */\n  public Path(String scheme, String authority, String path) {\n    checkPathArg(path);\n\n    // add a slash in front of paths with Windows drive letters\n    if (hasWindowsDrive(path) && path.charAt(0) != '/') {\n      path = \"/\" + path;\n    }\n\n    // add \"./\" in front of Linux relative paths so that a path containing\n    // a colon e.q. \"a:b\" will not be interpreted as scheme \"a\".\n    if (!WINDOWS && path.charAt(0) != '/') {\n      path = \"./\" + path;\n    }\n\n    initialize(scheme, authority, path, null);\n  }\n\n  private void initialize(String scheme, String authority, String path, String fragment) {\n    try {\n      this.uri =\n          new URI(scheme, authority, normalizePath(scheme, path), null, fragment).normalize();\n    } catch (URISyntaxException e) {\n      throw new IllegalArgumentException(e);\n    }\n  }\n\n  /**\n   * Normalize a path string to use non-duplicated forward slashes as the path separator and remove\n   * any trailing path separators.\n   *\n   * @param scheme the URI scheme. Used to deduce whether we should replace backslashes or not\n   * @param path the scheme-specific part\n   * @return the normalized path string\n   */\n  private static String normalizePath(String scheme, String path) {\n    // In most cases the path is expected to not have repeated slashes.\n    // Validating this first before applying the regex saves ~40-50% of\n    // Path construction time, with a potentially small overhead for\n    // cases when paths need normalization.\n    if (containsRepeatedSlash(path)) {\n      // Remove duplicated slashes to ensure all equivalent Path's have\n      // the same representation.\n      path = SLASHES.matcher(path).replaceAll(\"/\");\n    }\n\n    // Remove backslashes if this looks like a Windows path. Avoid\n    // the substitution if it looks like a non-local URI.\n    if (WINDOWS\n        && (hasWindowsDrive(path)\n            || (scheme == null)\n            || (scheme.isEmpty())\n            || (scheme.equals(\"file\")))) {\n      path = path.replace(\"\\\\\", \"/\");\n    }\n\n    // trim trailing slash from non-root path (ignoring windows drive)\n    int minLength = startPositionWithoutWindowsDrive(path) + 1;\n    if (path.length() > minLength && path.endsWith(SEPARATOR)) {\n      path = path.substring(0, path.length() - 1);\n    }\n\n    return path;\n  }\n\n  private static boolean containsRepeatedSlash(String path) {\n    // Not inlining this method back into normalizePath() appears\n    // to be slightly faster (there is a high probability this is noise\n    // but keep out-of-line for now until there is definitive evidence\n    // one way or another).\n    return path.contains(\"//\");\n  }\n\n  private static boolean hasWindowsDrive(String path) {\n    return (WINDOWS && HAS_DRIVE_LETTER_SPECIFIER.matcher(path).find());\n  }\n\n  private static int startPositionWithoutWindowsDrive(String path) {\n    if (hasWindowsDrive(path)) {\n      return path.charAt(0) == SEPARATOR_CHAR ? 3 : 2;\n    } else {\n      return 0;\n    }\n  }\n\n  /**\n   * Convert this Path to a URI.\n   *\n   * @return this Path as a URI\n   */\n  public URI toUri() {\n    return uri;\n  }\n\n  /**\n   * Returns true if the path component (i.e. directory) of this URI is absolute.\n   *\n   * @return whether this URI's path is absolute\n   */\n  public boolean isUriPathAbsolute() {\n    int start = startPositionWithoutWindowsDrive(uri.getPath());\n    return uri.getPath().startsWith(SEPARATOR, start);\n  }\n\n  /**\n   * Returns true if the path component (i.e. directory) of this URI is absolute. This method is a\n   * wrapper for {@link #isUriPathAbsolute()}.\n   *\n   * @return whether this URI's path is absolute\n   */\n  public boolean isAbsolute() {\n    return isUriPathAbsolute();\n  }\n\n  /**\n   * Returns true if and only if this path represents the root of a file system.\n   *\n   * @return true if and only if this path represents the root of a file system\n   */\n  public boolean isRoot() {\n    return getParent() == null;\n  }\n\n  /**\n   * Returns the final component of this path.\n   *\n   * @return the final component of this path\n   */\n  public String getName() {\n    String path = uri.getPath();\n    int slash = path.lastIndexOf(SEPARATOR);\n    return path.substring(slash + 1);\n  }\n\n  /**\n   * Returns the parent of a path or null if at root.\n   *\n   * @return the parent of a path or null if at root\n   */\n  public Path getParent() {\n    String path = uri.getPath();\n    int lastSlash = path.lastIndexOf('/');\n    int start = startPositionWithoutWindowsDrive(path);\n    if ((path.length() == start)\n        || // empty path\n        (lastSlash == start && path.length() == start + 1)) { // at root\n      return null;\n    }\n    String parent;\n    if (lastSlash == -1) {\n      parent = CUR_DIR;\n    } else {\n      parent = path.substring(0, lastSlash == start ? start + 1 : lastSlash);\n    }\n    return new Path(uri.getScheme(), uri.getAuthority(), parent);\n  }\n\n  @Override\n  public String toString() {\n    // we can't use uri.toString(), which escapes everything, because we want\n    // illegal characters unescaped in the string, for glob processing, etc.\n    StringBuilder buffer = new StringBuilder();\n    if (uri.getScheme() != null) {\n      buffer.append(uri.getScheme()).append(\":\");\n    }\n    if (uri.getAuthority() != null) {\n      buffer.append(\"//\").append(uri.getAuthority());\n    }\n    if (uri.getPath() != null) {\n      String path = uri.getPath();\n      if (path.indexOf('/') == 0\n          && hasWindowsDrive(path)\n          && // has windows drive\n          uri.getScheme() == null\n          && // but no scheme\n          uri.getAuthority() == null) { // or authority\n        path = path.substring(1); // remove slash before drive\n      }\n      buffer.append(path);\n    }\n    if (uri.getFragment() != null) {\n      buffer.append(\"#\").append(uri.getFragment());\n    }\n    return buffer.toString();\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof Path)) {\n      return false;\n    }\n    Path that = (Path) o;\n    return this.uri.equals(that.uri);\n  }\n\n  @Override\n  public int hashCode() {\n    return uri.hashCode();\n  }\n\n  @Override\n  public int compareTo(Path o) {\n    return this.uri.compareTo(o.uri);\n  }\n\n  /**\n   * Validate the contents of a deserialized Path, so as to defend against malicious object streams.\n   *\n   * @throws InvalidObjectException if there's no URI\n   */\n  @Override\n  public void validateObject() throws InvalidObjectException {\n    if (uri == null) {\n      throw new InvalidObjectException(\"No URI in deserialized Path\");\n    }\n  }\n\n  public static String getName(String pathString) {\n    return new Path(pathString).getName();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/hook/CheckpointHook.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.hook;\n\nimport io.delta.kernel.Table;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.internal.fs.Path;\nimport java.io.IOException;\n\n/** Write a new checkpoint at the version committed by the txn. */\npublic class CheckpointHook implements PostCommitHook {\n\n  private final Path tablePath;\n  private final long checkpointVersion;\n\n  public CheckpointHook(Path tablePath, long checkpointVersion) {\n    this.tablePath = tablePath;\n    this.checkpointVersion = checkpointVersion;\n  }\n\n  @Override\n  public void threadSafeInvoke(Engine engine) throws IOException {\n    Table.forPath(engine, tablePath.toString()).checkpoint(engine, checkpointVersion);\n  }\n\n  @Override\n  public PostCommitHookType getType() {\n    return PostCommitHookType.CHECKPOINT;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/hook/ChecksumFullHook.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.hook;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Table;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.internal.fs.Path;\nimport java.io.IOException;\n\n/**\n * A post-commit hook that writes a new checksum file at the version committed by the transaction.\n * This hook performs a writing checksum operation with table state construction for log replay.\n */\npublic class ChecksumFullHook implements PostCommitHook {\n\n  private final Path tablePath;\n  private final long version;\n\n  public ChecksumFullHook(Path tablePath, long version) {\n    this.tablePath = requireNonNull(tablePath);\n    this.version = version;\n  }\n\n  @Override\n  public void threadSafeInvoke(Engine engine) throws IOException {\n    checkArgument(engine != null);\n    Table.forPath(engine, tablePath.toString()).checksum(engine, version);\n  }\n\n  @Override\n  public PostCommitHookType getType() {\n    return PostCommitHookType.CHECKSUM_FULL;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/hook/ChecksumSimpleHook.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.hook;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.checksum.ChecksumWriter;\nimport io.delta.kernel.internal.fs.Path;\nimport java.io.IOException;\n\n/**\n * A post-commit hook that writes a new checksum file at the version committed by the transaction.\n * This hook performs a simple checksum operation without requiring previous checkpoint or log\n * reading.\n */\npublic class ChecksumSimpleHook implements PostCommitHook {\n\n  private final CRCInfo crcInfo;\n  private final Path logPath;\n\n  public ChecksumSimpleHook(CRCInfo crcInfo, Path logPath) {\n    this.crcInfo = requireNonNull(crcInfo);\n    this.logPath = requireNonNull(logPath);\n  }\n\n  @Override\n  public void threadSafeInvoke(Engine engine) throws IOException {\n    checkArgument(engine != null);\n    new ChecksumWriter(logPath).writeCheckSum(engine, crcInfo);\n  }\n\n  @Override\n  public PostCommitHookType getType() {\n    return PostCommitHookType.CHECKSUM_SIMPLE;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/hook/LogCompactionHook.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.hook;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.hook.PostCommitHook;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.compaction.LogCompactionWriter;\nimport io.delta.kernel.internal.fs.Path;\nimport java.io.IOException;\n\n/**\n * A post-commit hook that performs inline log compaction. It merges commit JSON files over a\n * compaction interval into a single compacted JSON file.\n */\npublic class LogCompactionHook implements PostCommitHook {\n\n  private final Path dataPath;\n  private final Path logPath;\n  private final long startVersion;\n  private final long commitVersion;\n  private final long minFileRetentionTimestampMillis;\n\n  public LogCompactionHook(\n      Path dataPath,\n      Path logPath,\n      long startVersion,\n      long commitVersion,\n      long minFileRetentionTimestampMillis) {\n    this.dataPath = requireNonNull(dataPath, \"dataPath cannot be null\");\n    this.logPath = requireNonNull(logPath, \"logPath cannot be null\");\n    this.startVersion = startVersion;\n    this.commitVersion = commitVersion;\n    this.minFileRetentionTimestampMillis = minFileRetentionTimestampMillis;\n  }\n\n  @Override\n  public void threadSafeInvoke(Engine engine) throws IOException {\n    LogCompactionWriter compactionWriter =\n        new LogCompactionWriter(\n            dataPath, logPath, startVersion, commitVersion, minFileRetentionTimestampMillis);\n    compactionWriter.writeLogCompactionFile(engine);\n  }\n\n  @Override\n  public PostCommitHookType getType() {\n    return PostCommitHookType.LOG_COMPACTION;\n  }\n\n  @VisibleForTesting\n  public Path getDataPath() {\n    return dataPath;\n  }\n\n  @VisibleForTesting\n  public Path getLogPath() {\n    return logPath;\n  }\n\n  @VisibleForTesting\n  public long getStartVersion() {\n    return startVersion;\n  }\n\n  @VisibleForTesting\n  public long getCommitVersion() {\n    return commitVersion;\n  }\n\n  @VisibleForTesting\n  public long getMinFileRetentionTimestampMillis() {\n    return minFileRetentionTimestampMillis;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergCompatMetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Collections.singletonMap;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.internal.types.TypeWideningChecker;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.util.*;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * Contains interfaces and common utility classes for defining the iceberg conversion compatibility\n * checks and metadata updates.\n *\n * <p>Main class is {@link IcebergCompatMetadataValidatorAndUpdater} which takes:\n *\n * <ul>\n *   <li>{@link TableConfig} to check if the table is enabled iceberg compat property enabled. When\n *       enabled, the metadata will be validated and updated.\n *   <li>List of {@link TableFeature}s expected to be supported by the protocol\n *   <li>List of {@link IcebergCompatRequiredTablePropertyEnforcer} that enforce certain properties\n *       must be set for IcebergV2 compatibility. If the property is not set, we will set it to a\n *       default value. It will also update the metadata to make it compatible with Iceberg compat\n *       version targeted.\n *   <li>List of {@link IcebergCompatCheck} to validate the metadata and protocol. The checks can be\n *       like what are the table features not supported and in what cases a certain table feature is\n *       supported (e.g. type widening is enabled, but iceberg compat only if the widening is\n *       supported in the Iceberg).\n * </ul>\n */\npublic abstract class IcebergCompatMetadataValidatorAndUpdater {\n\n  /**\n   * Returns whether Iceberg compatibility is enabled for the given table metadata. This checks if\n   * either `icebergCompatV2` or `icebergCompatV3` table property is enabled.\n   *\n   * @param metadata The table metadata to check.\n   * @return true if either Iceberg compatibility V2 or V3 is enabled; false otherwise.\n   */\n  public static Boolean isIcebergCompatEnabled(Metadata metadata) {\n    return TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(metadata)\n        || TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(metadata);\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Interfaces for defining checks for the compat validation and updating     ///\n  /////////////////////////////////////////////////////////////////////////////////\n  /** Defines the input context for the metadata validator and updater. */\n  public static class IcebergCompatInputContext {\n    final String compatFeatureName;\n    final boolean isCreatingNewTable;\n    final Metadata newMetadata;\n    final Protocol newProtocol;\n    /**\n     * The protocol from the previous snapshot, used to guard against concurrent writers that may\n     * have enabled incompatible features (e.g. deletion vectors). Empty for new tables.\n     */\n    final Optional<Protocol> prevProtocol;\n\n    public IcebergCompatInputContext(\n        String compatFeatureName,\n        boolean isCreatingNewTable,\n        Metadata newMetadata,\n        Protocol newProtocol,\n        Optional<Protocol> prevProtocol) {\n      this.compatFeatureName = compatFeatureName;\n      this.isCreatingNewTable = isCreatingNewTable;\n      this.newMetadata = newMetadata;\n      this.newProtocol = newProtocol;\n      this.prevProtocol = prevProtocol;\n    }\n\n    public IcebergCompatInputContext withUpdatedMetadata(Metadata newMetadata) {\n      return new IcebergCompatInputContext(\n          compatFeatureName, isCreatingNewTable, newMetadata, newProtocol, prevProtocol);\n    }\n  }\n\n  /** Defines a callback to post-process the metadata. */\n  interface PostMetadataProcessor {\n    Optional<Metadata> postProcess(IcebergCompatInputContext inputContext);\n  }\n\n  /**\n   * Defines a required table property that must be set for IcebergV2 compatibility. If the property\n   * is not set, we will set it to a default value. It will also update the metadata to make it\n   * compatible with Iceberg compat version targeted.\n   */\n  protected static class IcebergCompatRequiredTablePropertyEnforcer<T> {\n    public final TableConfig<T> property;\n    public final Predicate<T> validator;\n    public final String autoSetValue;\n    public final PostMetadataProcessor postMetadataProcessor;\n\n    /**\n     * Constructor for RequiredDeltaTableProperty\n     *\n     * @param property DeltaConfig we are checking\n     * @param validator A generic method to validate the given value\n     * @param autoSetValue The value to set if we can auto-set this value (e.g. during table\n     *     creation)\n     * @param postMetadataProcessor A callback to post-process the metadata\n     */\n    IcebergCompatRequiredTablePropertyEnforcer(\n        TableConfig<T> property,\n        Predicate<T> validator,\n        String autoSetValue,\n        PostMetadataProcessor postMetadataProcessor) {\n      this.property = property;\n      this.validator = validator;\n      this.autoSetValue = autoSetValue;\n      this.postMetadataProcessor = postMetadataProcessor;\n    }\n\n    /**\n     * Constructor for RequiredDeltaTableProperty\n     *\n     * @param property DeltaConfig we are checking\n     * @param validator A generic method to validate the given value\n     * @param autoSetValue The value to set if we can auto-set this value (e.g. during table\n     *     creation)\n     */\n    IcebergCompatRequiredTablePropertyEnforcer(\n        TableConfig<T> property, Predicate<T> validator, String autoSetValue) {\n      this(property, validator, autoSetValue, (c) -> Optional.empty());\n    }\n\n    Optional<Metadata> validateAndUpdate(\n        IcebergCompatInputContext inputContext, String compatVersion) {\n      Metadata newMetadata = inputContext.newMetadata;\n      T newestValue = property.fromMetadata(newMetadata);\n      boolean newestValueOkay = validator.test(newestValue);\n      boolean newestValueExplicitlySet =\n          newMetadata.getConfiguration().containsKey(property.getKey());\n\n      if (!newestValueOkay) {\n        if (!newestValueExplicitlySet && inputContext.isCreatingNewTable) {\n          // Covers the case CREATE that did not explicitly specify the required table property.\n          // In these cases, we set the property automatically.\n          newMetadata =\n              newMetadata.withMergedConfiguration(singletonMap(property.getKey(), autoSetValue));\n          return Optional.of(newMetadata);\n        } else {\n          // In all other cases, if the property value is not compatible\n          throw new KernelException(\n              String.format(\n                  \"The value '%s' for the property '%s' is not compatible with \"\n                      + \"%s requirements\",\n                  newestValue, property.getKey(), compatVersion));\n        }\n      }\n\n      return Optional.empty();\n    }\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Implementation of {@link IcebergCompatRequiredTablePropertyEnforcer}      ///\n  /////////////////////////////////////////////////////////////////////////////////\n  protected static final IcebergCompatRequiredTablePropertyEnforcer COLUMN_MAPPING_REQUIREMENT =\n      new IcebergCompatRequiredTablePropertyEnforcer<>(\n          TableConfig.COLUMN_MAPPING_MODE,\n          (value) ->\n              ColumnMapping.ColumnMappingMode.NAME == value\n                  || ColumnMapping.ColumnMappingMode.ID == value,\n          ColumnMapping.ColumnMappingMode.NAME.value);\n\n  protected static final IcebergCompatRequiredTablePropertyEnforcer ROW_TRACKING_ENABLED =\n      new IcebergCompatRequiredTablePropertyEnforcer<>(\n          TableConfig.ROW_TRACKING_ENABLED, (value) -> value, \"true\");\n\n  /**\n   * Defines checks for compatibility with the targeted iceberg features (icebergCompatV1 or\n   * icebergCompatV2 etc.)\n   */\n  protected interface IcebergCompatCheck {\n    void check(IcebergCompatInputContext inputContext);\n  }\n\n  ///////////////////////////////////////////////////////////\n  /// Implementation of {@link IcebergCompatCheck}        ///\n  ///////////////////////////////////////////////////////////\n  protected static IcebergCompatCheck disallowOtherCompatVersions(List<String> incompatibleProps) {\n    return (inputContext) -> {\n      for (String prop : incompatibleProps) {\n        if (Boolean.parseBoolean(\n            inputContext.newMetadata.getConfiguration().getOrDefault(prop, \"false\"))) {\n          throw DeltaErrors.icebergCompatIncompatibleVersionEnabled(\n              inputContext.compatFeatureName, prop);\n        }\n      }\n    };\n  }\n\n  protected static final IcebergCompatCheck CHECK_ONLY_ICEBERG_COMPAT_V2_ENABLED =\n      disallowOtherCompatVersions(\n          Arrays.asList(\"delta.enableIcebergCompatV1\", \"delta.enableIcebergCompatV3\"));\n\n  protected static final IcebergCompatCheck CHECK_ONLY_ICEBERG_COMPAT_V3_ENABLED =\n      disallowOtherCompatVersions(\n          Arrays.asList(\"delta.enableIcebergCompatV1\", \"delta.enableIcebergCompatV2\"));\n\n  protected static IcebergCompatCheck hasOnlySupportedTypes(\n      Set<Class<? extends DataType>> supportedTypes) {\n    return (inputContext) -> {\n      Set<DataType> matches =\n          new SchemaIterable(inputContext.newMetadata.getSchema())\n              .stream()\n                  .map(element -> element.getField().getDataType())\n                  .filter(\n                      dataType -> {\n                        for (Class<? extends DataType> clazz : supportedTypes) {\n                          if (clazz.isInstance(dataType)) return false;\n                        }\n                        return true;\n                      })\n                  .collect(Collectors.toSet());\n\n      if (!matches.isEmpty()) {\n        List<DataType> unsupportedTypes = new ArrayList<>(matches);\n        unsupportedTypes.sort(Comparator.comparing(DataType::toString));\n        throw DeltaErrors.icebergCompatUnsupportedTypeColumns(\n            inputContext.compatFeatureName, unsupportedTypes);\n      }\n    };\n  }\n\n  private static final Set<Class<? extends DataType>> V2_SUPPORTED_TYPES =\n      new HashSet<>(\n          Arrays.asList(\n              ByteType.class,\n              ShortType.class,\n              IntegerType.class,\n              LongType.class,\n              FloatType.class,\n              DoubleType.class,\n              DecimalType.class,\n              StringType.class,\n              BinaryType.class,\n              BooleanType.class,\n              DateType.class,\n              TimestampType.class,\n              TimestampNTZType.class,\n              ArrayType.class,\n              MapType.class,\n              StructType.class));\n\n  private static final Set<Class<? extends DataType>> V3_SUPPORTED_TYPES =\n      Stream.concat(V2_SUPPORTED_TYPES.stream(), Stream.of(VariantType.class))\n          .collect(Collectors.toSet());\n\n  protected static final IcebergCompatCheck V2_CHECK_HAS_SUPPORTED_TYPES =\n      hasOnlySupportedTypes(V2_SUPPORTED_TYPES);\n\n  protected static final IcebergCompatCheck V3_CHECK_HAS_SUPPORTED_TYPES =\n      hasOnlySupportedTypes(V3_SUPPORTED_TYPES);\n\n  // These are the common supported partition types for both Iceberg compat V2 and V3\n  protected static final IcebergCompatCheck CHECK_HAS_ALLOWED_PARTITION_TYPES =\n      (inputContext) ->\n          inputContext\n              .newMetadata\n              .getPartitionColNames()\n              .forEach(\n                  partitionCol -> {\n                    int partitionFieldIndex =\n                        inputContext.newMetadata.getSchema().indexOf(partitionCol);\n                    checkArgument(\n                        partitionFieldIndex != -1,\n                        \"Partition column %s not found in the schema\",\n                        partitionCol);\n                    DataType dataType =\n                        inputContext.newMetadata.getSchema().at(partitionFieldIndex).getDataType();\n                    boolean validType =\n                        dataType instanceof ByteType\n                            || dataType instanceof ShortType\n                            || dataType instanceof IntegerType\n                            || dataType instanceof LongType\n                            || dataType instanceof FloatType\n                            || dataType instanceof DoubleType\n                            || dataType instanceof DecimalType\n                            || dataType instanceof StringType\n                            || dataType instanceof BinaryType\n                            || dataType instanceof BooleanType\n                            || dataType instanceof DateType\n                            || dataType instanceof TimestampType\n                            || dataType instanceof TimestampNTZType;\n                    if (!validType) {\n                      throw DeltaErrors.icebergCompatUnsupportedTypePartitionColumn(\n                          inputContext.compatFeatureName, dataType);\n                    }\n                  });\n\n  protected static final IcebergCompatCheck CHECK_HAS_NO_PARTITION_EVOLUTION =\n      (inputContext) -> {\n        // TODO: Kernel doesn't support replace table yet. When it is supported, extend\n        // this to allow checking the partition columns aren't changed\n      };\n\n  protected static final IcebergCompatCheck CHECK_HAS_NO_DELETION_VECTORS =\n      (inputContext) -> {\n        // Check both newProtocol and prevProtocol as defense-in-depth against cases where\n        // the previous snapshot already had deletion vectors enabled. This matches Spark's\n        // CheckDeletionVectorDisabled which checks both prevSnapshot and newestProtocol.\n        // Note: the conflict checker may also catch concurrent DV enablement at commit time.\n        boolean dvInNewProtocol =\n            inputContext.newProtocol.supportsFeature(DELETION_VECTORS_RW_FEATURE);\n        boolean dvInPrevProtocol =\n            inputContext\n                .prevProtocol\n                .map(prev -> prev.supportsFeature(DELETION_VECTORS_RW_FEATURE))\n                .orElse(false);\n        if (dvInNewProtocol || dvInPrevProtocol) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(DELETION_VECTORS_RW_FEATURE));\n        }\n      };\n\n  protected static final IcebergCompatCheck CHECK_HAS_SUPPORTED_TYPE_WIDENING =\n      (inputContext) -> {\n        Protocol protocol = inputContext.newProtocol;\n        if (!protocol.supportsFeature(TYPE_WIDENING_RW_FEATURE)\n            && !protocol.supportsFeature(TYPE_WIDENING_RW_PREVIEW_FEATURE)) {\n          return;\n        }\n        for (SchemaIterable.SchemaElement element :\n            new SchemaIterable(inputContext.newMetadata.getSchema())) {\n          for (TypeChange typeChange : element.getField().getTypeChanges()) {\n            if (!TypeWideningChecker.isIcebergV2Compatible(\n                typeChange.getFrom(), typeChange.getTo())) {\n              throw DeltaErrors.icebergCompatUnsupportedTypeWidening(\n                  inputContext.compatFeatureName, typeChange);\n            }\n          }\n        }\n      };\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Implementation of {@link IcebergCompatMetadataValidatorAndUpdater}        ///\n  /////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * If the iceberg compat is enabled, validate and update the metadata for Iceberg compatibility.\n   *\n   * @param inputContext input containing the metadata, protocol, if the table is being created etc.\n   * @return the updated metadata. If no updates are done, then returns empty\n   * @throws {@link io.delta.kernel.exceptions.KernelException} for any validation errors\n   */\n  Optional<Metadata> validateAndUpdateMetadata(IcebergCompatInputContext inputContext) {\n    if (!requiredDeltaTableProperty().fromMetadata(inputContext.newMetadata)) {\n      return Optional.empty();\n    }\n\n    boolean metadataUpdated = false;\n\n    // table property checks and metadata updates\n    List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties =\n        requiredDeltaTableProperties();\n    for (IcebergCompatRequiredTablePropertyEnforcer requiredDeltaTableProperty :\n        requiredDeltaTableProperties) {\n      Optional<Metadata> updated =\n          requiredDeltaTableProperty.validateAndUpdate(inputContext, compatFeatureName());\n\n      if (updated.isPresent()) {\n        inputContext = inputContext.withUpdatedMetadata(updated.get());\n        metadataUpdated = true;\n      }\n    }\n\n    // post-process metadata after the table property checks are done and updated\n    for (IcebergCompatRequiredTablePropertyEnforcer requiredDeltaTableProperty :\n        requiredDeltaTableProperties) {\n      Optional<Metadata> updated =\n          requiredDeltaTableProperty.postMetadataProcessor.postProcess(inputContext);\n      if (updated.isPresent()) {\n        metadataUpdated = true;\n        inputContext = inputContext.withUpdatedMetadata(updated.get());\n      }\n    }\n\n    // check for required dependency table features\n    for (TableFeature requiredDependencyTableFeature : requiredDependencyTableFeatures()) {\n      if (!inputContext.newProtocol.supportsFeature(requiredDependencyTableFeature)) {\n        throw DeltaErrors.icebergCompatRequiredFeatureMissing(\n            compatFeatureName(), requiredDependencyTableFeature.featureName());\n      }\n    }\n\n    // check for Iceberg compatibility checks\n    for (IcebergCompatCheck icebergCompatCheck : icebergCompatChecks()) {\n      icebergCompatCheck.check(inputContext);\n    }\n\n    return metadataUpdated ? Optional.of(inputContext.newMetadata) : Optional.empty();\n  }\n\n  abstract String compatFeatureName();\n\n  abstract TableConfig<Boolean> requiredDeltaTableProperty();\n\n  abstract List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties();\n\n  abstract List<TableFeature> requiredDependencyTableFeatures();\n\n  abstract List<IcebergCompatCheck> icebergCompatChecks();\n\n  /////////////////////////////\n  /// Helper function       ///\n  /////////////////////////////\n\n  /**\n   * Validate the given {@link DataFileStatus} that is being added as a {@code add} action to Delta\n   * Log. Currently, it checks that the statistics are not empty.\n   *\n   * @param dataFileStatus The {@link DataFileStatus} to validate.\n   * @param compatFeatureName The name of the compatibility feature being validated (e.g.\n   *     \"icebergCompatV2\").\n   */\n  protected static void validateDataFileStatus(\n      DataFileStatus dataFileStatus, String compatFeatureName) {\n    if (!dataFileStatus.getStatistics().isPresent()) {\n      // presence of stats means always has a non-null `numRecords`\n      throw DeltaErrors.icebergCompatMissingNumRecordsStats(compatFeatureName, dataFileStatus);\n    }\n  }\n\n  /**\n   * Block the Iceberg Compat config related changes that we do not support and for which we throw\n   * an {@link KernelException},\n   *\n   * <ul>\n   *   <li>Disabling on an existing table (true to false)\n   *   <li>Enabling on an existing table (false to true)\n   * </ul>\n   */\n  protected static void blockConfigChangeOnExistingTable(\n      TableConfig<Boolean> tableConfig,\n      Map<String, String> oldConfig,\n      Map<String, String> newConfig) {\n    boolean wasEnabled = tableConfig.fromMetadata(oldConfig);\n    boolean isEnabled = tableConfig.fromMetadata(newConfig);\n    if (!wasEnabled && isEnabled) {\n      throw DeltaErrors.enablingIcebergCompatFeatureOnExistingTable(tableConfig.getKey());\n    }\n    if (wasEnabled && !isEnabled) {\n      throw DeltaErrors.disablingIcebergCompatFeatureOnExistingTable(tableConfig.getKey());\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergCompatV2MetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static java.util.Collections.singletonList;\nimport static java.util.stream.Collectors.toList;\n\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Stream;\n\n/** Utility methods for validation and compatibility checks for Iceberg V2. */\npublic class IcebergCompatV2MetadataValidatorAndUpdater\n    extends IcebergCompatMetadataValidatorAndUpdater {\n  /**\n   * Validate and update the given Iceberg V2 metadata.\n   *\n   * @param newMetadata Metadata after the current updates\n   * @param newProtocol Protocol after the current updates\n   * @return The updated metadata if the metadata is valid and updated, otherwise empty.\n   * @throws UnsupportedOperationException if the metadata is not compatible with Iceberg V2\n   *     requirements\n   */\n  public static Optional<Metadata> validateAndUpdateIcebergCompatV2Metadata(\n      boolean isCreatingNewTable,\n      Metadata newMetadata,\n      Protocol newProtocol,\n      Optional<Protocol> prevProtocol) {\n    return INSTANCE.validateAndUpdateMetadata(\n        new IcebergCompatInputContext(\n            INSTANCE.compatFeatureName(),\n            isCreatingNewTable,\n            newMetadata,\n            newProtocol,\n            prevProtocol));\n  }\n\n  /**\n   * Validate the given {@link DataFileStatus} that is being added as a {@code add} action to Delta\n   * Log. Currently, it checks that the statistics are not empty.\n   *\n   * @param dataFileStatus The {@link DataFileStatus} to validate.\n   */\n  public static void validateDataFileStatus(DataFileStatus dataFileStatus) {\n    validateDataFileStatus(dataFileStatus, INSTANCE.compatFeatureName());\n  }\n\n  /// //////////////////////////////////////////////////////////////////////////////\n  /// Define the compatibility and update checks for icebergCompatV2             ///\n  /// //////////////////////////////////////////////////////////////////////////////\n\n  private static final IcebergCompatV2MetadataValidatorAndUpdater INSTANCE =\n      new IcebergCompatV2MetadataValidatorAndUpdater();\n\n  @Override\n  String compatFeatureName() {\n    return \"icebergCompatV2\";\n  }\n\n  @Override\n  TableConfig<Boolean> requiredDeltaTableProperty() {\n    return TableConfig.ICEBERG_COMPAT_V2_ENABLED;\n  }\n\n  @Override\n  List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties() {\n    return singletonList(COLUMN_MAPPING_REQUIREMENT);\n  }\n\n  @Override\n  List<TableFeature> requiredDependencyTableFeatures() {\n    return Stream.of(ICEBERG_COMPAT_V2_W_FEATURE, COLUMN_MAPPING_RW_FEATURE).collect(toList());\n  }\n\n  @Override\n  List<IcebergCompatCheck> icebergCompatChecks() {\n    return Stream.of(\n            CHECK_ONLY_ICEBERG_COMPAT_V2_ENABLED,\n            V2_CHECK_HAS_SUPPORTED_TYPES,\n            CHECK_HAS_ALLOWED_PARTITION_TYPES,\n            CHECK_HAS_NO_PARTITION_EVOLUTION,\n            CHECK_HAS_NO_DELETION_VECTORS,\n            CHECK_HAS_SUPPORTED_TYPE_WIDENING)\n        .collect(toList());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergCompatV3MetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static java.util.stream.Collectors.toList;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.columndefaults.ColumnDefaults;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Stream;\n\n/** Utility methods for validation and compatibility checks for Iceberg V3. */\npublic class IcebergCompatV3MetadataValidatorAndUpdater\n    extends IcebergCompatMetadataValidatorAndUpdater {\n\n  /**\n   * Validates that any change to property {@link TableConfig#ICEBERG_COMPAT_V3_ENABLED} is valid.\n   * Currently, the changes we support are\n   *\n   * <ul>\n   *   <li>No change in enablement (true to true or false to false)\n   * </ul>\n   *\n   * The changes that we do not support and for which we throw an {@link KernelException} are\n   *\n   * <ul>\n   *   <li>Disabling on an existing table (true to false)\n   *   <li>Enabling on an existing table (false to true)\n   * </ul>\n   */\n  public static void validateIcebergCompatV3Change(\n      Map<String, String> oldConfig, Map<String, String> newConfig) {\n    blockConfigChangeOnExistingTable(TableConfig.ICEBERG_COMPAT_V3_ENABLED, oldConfig, newConfig);\n  }\n\n  /**\n   * Validate and update the given Iceberg V3 metadata.\n   *\n   * @param newMetadata Metadata after the current updates\n   * @param newProtocol Protocol after the current updates\n   * @return The updated metadata if the metadata is valid and updated, otherwise empty.\n   * @throws UnsupportedOperationException if the metadata is not compatible with Iceberg V3\n   *     requirements\n   */\n  public static Optional<Metadata> validateAndUpdateIcebergCompatV3Metadata(\n      boolean isCreatingNewTable,\n      Metadata newMetadata,\n      Protocol newProtocol,\n      Optional<Protocol> prevProtocol) {\n    return INSTANCE.validateAndUpdateMetadata(\n        new IcebergCompatInputContext(\n            INSTANCE.compatFeatureName(),\n            isCreatingNewTable,\n            newMetadata,\n            newProtocol,\n            prevProtocol));\n  }\n\n  /**\n   * Validate the given {@link DataFileStatus} that is being added as a {@code add} action to Delta\n   * Log. Currently, it checks that the statistics are not empty.\n   *\n   * @param dataFileStatus The {@link DataFileStatus} to validate.\n   */\n  public static void validateDataFileStatus(DataFileStatus dataFileStatus) {\n    validateDataFileStatus(dataFileStatus, INSTANCE.compatFeatureName());\n  }\n\n  /// //////////////////////////////////////////////////////////////////////////////\n  /// Define the compatibility and update checks for icebergCompatV3             ///\n  /// //////////////////////////////////////////////////////////////////////////////\n\n  private static final IcebergCompatV3MetadataValidatorAndUpdater INSTANCE =\n      new IcebergCompatV3MetadataValidatorAndUpdater();\n\n  @Override\n  String compatFeatureName() {\n    return \"icebergCompatV3\";\n  }\n\n  @Override\n  TableConfig<Boolean> requiredDeltaTableProperty() {\n    return TableConfig.ICEBERG_COMPAT_V3_ENABLED;\n  }\n\n  @Override\n  List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties() {\n    return Stream.of(COLUMN_MAPPING_REQUIREMENT, ROW_TRACKING_ENABLED).collect(toList());\n  }\n\n  @Override\n  List<TableFeature> requiredDependencyTableFeatures() {\n    return Stream.of(ICEBERG_COMPAT_V3_W_FEATURE, COLUMN_MAPPING_RW_FEATURE, ROW_TRACKING_W_FEATURE)\n        .collect(toList());\n  }\n\n  @Override\n  List<IcebergCompatCheck> icebergCompatChecks() {\n    return Stream.of(\n            V3_CHECK_HAS_SUPPORTED_TYPES,\n            CHECK_ONLY_ICEBERG_COMPAT_V3_ENABLED,\n            CHECK_HAS_ALLOWED_PARTITION_TYPES,\n            CHECK_HAS_NO_PARTITION_EVOLUTION,\n            CHECK_HAS_SUPPORTED_TYPE_WIDENING,\n            CHECK_LITERAL_DEFAULT_VALUE)\n        .collect(toList());\n  }\n\n  protected static IcebergCompatCheck CHECK_LITERAL_DEFAULT_VALUE =\n      (inputContext) ->\n          ColumnDefaults.validateSchemaForIcebergCompat(\n              inputContext.newMetadata.getSchema(), ICEBERG_COMPAT_V3_W_FEATURE.featureName());\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergUniversalFormatMetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport io.delta.kernel.exceptions.InvalidConfigurationValueException;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Utility class that enforces dependencies of UNIVERSAL_FORMAT_* options.\n *\n * <p>This class currently only does validation, in the future it might also update metadata to be\n * conformant and thus has \"Updater suffix\".\n */\npublic class IcebergUniversalFormatMetadataValidatorAndUpdater {\n  private IcebergUniversalFormatMetadataValidatorAndUpdater() {}\n\n  /**\n   * Ensures the metadata is consistent with the enabled Universal output targets.\n   *\n   * <p>If required dependent {@linkplain TableConfig}s are not set in {@code metadata} then an\n   * exception is raised.\n   *\n   * <p>\"hudi\" is trivially compatible with Metadata.\n   *\n   * <p>\"iceberg\" requires that {@linkplain TableConfig#ICEBERG_COMPAT_V2_ENABLED} is set to true.\n   *\n   * @throws InvalidConfigurationValueException metadata has ICEBERG universal format enabled and\n   *     {@linkplain TableConfig#ICEBERG_COMPAT_V2_ENABLED} is not enabled in metadata\n   */\n  public static void validate(Metadata metadata) {\n    if (!metadata\n        .getConfiguration()\n        .containsKey(TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey())) {\n      return;\n    }\n\n    Set<String> targetFormats = TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.fromMetadata(metadata);\n    boolean isIcebergEnabled = targetFormats.contains(TableConfig.UniversalFormats.FORMAT_ICEBERG);\n\n    List<TableConfig<Boolean>> icebergCompatOptions =\n        Arrays.asList(TableConfig.ICEBERG_COMPAT_V2_ENABLED, TableConfig.ICEBERG_COMPAT_V3_ENABLED);\n    long enabledCompatCount =\n        icebergCompatOptions.stream().filter(opt -> opt.fromMetadata(metadata)).count();\n\n    if (isIcebergEnabled && enabledCompatCount == 0) {\n      String optionKeys =\n          String.join(\n              \" or \",\n              icebergCompatOptions.stream().map(TableConfig::getKey).toArray(String[]::new));\n      throw new InvalidConfigurationValueException(\n          TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey(),\n          TableConfig.UniversalFormats.FORMAT_ICEBERG,\n          String.format(\n              \"One of %s must be set to \\\"true\\\" to enable iceberg uniform format.\", optionKeys));\n    }\n\n    if (enabledCompatCount > 1) {\n      String optionKeys =\n          String.join(\n              \"' and '\",\n              icebergCompatOptions.stream().map(TableConfig::getKey).toArray(String[]::new));\n      throw new InvalidConfigurationValueException(\n          TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey(),\n          TableConfig.UniversalFormats.FORMAT_ICEBERG,\n          String.format(\"'%s' cannot be enabled at the same time.\", optionKeys));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergWriterCompatMetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Stream;\n\n/**\n * Contains interfaces and common utility classes performing the validations and updates necessary\n * to support the table feature IcebergWriterCompats when it is enabled by the table properties such\n * as \"delta.enableIcebergWriterCompatV3\".\n */\nabstract class IcebergWriterCompatMetadataValidatorAndUpdater\n    extends IcebergCompatMetadataValidatorAndUpdater {\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Interfaces for defining validations and updates necessary to support IcebergWriterCompats\n  // ///\n  /////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Common property enforcer for Column Mapping ID mode requirement. This is identical across all\n   * Writer Compat versions.\n   */\n  protected static final IcebergCompatRequiredTablePropertyEnforcer CM_ID_MODE_ENABLED =\n      new IcebergCompatRequiredTablePropertyEnforcer<>(\n          TableConfig.COLUMN_MAPPING_MODE,\n          (value) -> ColumnMapping.ColumnMappingMode.ID == value,\n          ColumnMapping.ColumnMappingMode.ID.value,\n          // We need to update the CM info in the schema here because we check that the physical\n          // name is correctly set as part of icebergWriterCompat checks\n          (inputContext) ->\n              ColumnMapping.updateColumnMappingMetadataIfNeeded(\n                  inputContext.newMetadata, inputContext.isCreatingNewTable));\n\n  /**\n   * Creates an IcebergCompatRequiredTablePropertyEnforcer for enabling a specific Iceberg\n   * compatibility version. The enforcer ensures the property is set to \"true\" and delegates\n   * validation to the appropriate metadata validator.\n   *\n   * @param tableConfigProperty the table configuration property to enforce\n   * @param postProcessor the version-specific validation and metadata update processor\n   * @return configured enforcer for the specified Iceberg compatibility version\n   */\n  protected static IcebergCompatRequiredTablePropertyEnforcer<Boolean> createIcebergCompatEnforcer(\n      TableConfig<Boolean> tableConfigProperty, PostMetadataProcessor postProcessor) {\n    return new IcebergCompatRequiredTablePropertyEnforcer<>(\n        tableConfigProperty, (value) -> value, \"true\", postProcessor);\n  }\n\n  /**\n   * Common set of allowed table features shared across all Iceberg writer compatibility versions.\n   * This includes the incompatible legacy features (invariants, changeDataFeed, checkConstraints,\n   * identityColumns, generatedColumns) because they may be present in the table protocol even when\n   * they are not in use. In later checks we validate that these incompatible features are inactive\n   * in the table. See the protocol spec for more details.\n   */\n  protected static final Set<TableFeature> COMMON_ALLOWED_FEATURES =\n      Stream.of(\n              // Incompatible, but not active, legacy table features\n              INVARIANTS_W_FEATURE,\n              CHANGE_DATA_FEED_W_FEATURE,\n              ROW_TRACKING_W_FEATURE,\n              CONSTRAINTS_W_FEATURE,\n              IDENTITY_COLUMNS_W_FEATURE,\n              GENERATED_COLUMNS_W_FEATURE,\n              // Compatible table features\n              APPEND_ONLY_W_FEATURE,\n              COLUMN_MAPPING_RW_FEATURE,\n              DOMAIN_METADATA_W_FEATURE,\n              VACUUM_PROTOCOL_CHECK_RW_FEATURE,\n              CHECKPOINT_V2_RW_FEATURE,\n              CHECKPOINT_PROTECTION_W_FEATURE,\n              IN_COMMIT_TIMESTAMP_W_FEATURE,\n              CLUSTERING_W_FEATURE,\n              TIMESTAMP_NTZ_RW_FEATURE,\n              TYPE_WIDENING_RW_FEATURE,\n              TYPE_WIDENING_RW_PREVIEW_FEATURE,\n              CATALOG_MANAGED_RW_FEATURE)\n          .collect(toSet());\n\n  protected static IcebergCompatCheck createUnsupportedFeaturesCheck(\n      IcebergWriterCompatMetadataValidatorAndUpdater instance) {\n    return (inputContext) -> {\n      Set<TableFeature> allowedTableFeatures = instance.getAllowedTableFeatures();\n      if (!allowedTableFeatures.containsAll(\n          inputContext.newProtocol.getImplicitlyAndExplicitlySupportedFeatures())) {\n        Set<TableFeature> incompatibleFeatures =\n            inputContext.newProtocol.getImplicitlyAndExplicitlySupportedFeatures();\n        incompatibleFeatures.removeAll(allowedTableFeatures);\n        throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n            inputContext.compatFeatureName, incompatibleFeatures);\n      }\n    };\n  }\n\n  /**\n   * Checks that there are no unsupported types in the schema. Data types {@link ByteType} and\n   * {@link ShortType} are unsupported for IcebergWriterCompatV1 and V3 tables.\n   */\n  protected static final IcebergCompatCheck UNSUPPORTED_TYPES_CHECK =\n      (inputContext) -> {\n        Set<DataType> matches =\n            new SchemaIterable(inputContext.newMetadata.getSchema())\n                .stream()\n                    .map(element -> element.getField().getDataType())\n                    .filter(\n                        dataType -> dataType instanceof ByteType || dataType instanceof ShortType)\n                    .collect(toSet());\n\n        if (!matches.isEmpty()) {\n          List<DataType> unsupportedTypes = new ArrayList<>(matches);\n          unsupportedTypes.sort(Comparator.comparing(DataType::toString));\n          throw DeltaErrors.icebergCompatUnsupportedTypeColumns(\n              inputContext.compatFeatureName, unsupportedTypes);\n        }\n      };\n\n  /**\n   * Checks that in the schema column mapping is set up such that the physicalName is equal to\n   * \"col-[fieldId]\". This check assumes column mapping is enabled (and so should be performed after\n   * that check).\n   */\n  protected static final IcebergCompatCheck PHYSICAL_NAMES_MATCH_FIELD_IDS_CHECK =\n      (inputContext) -> {\n        List<String> invalidFields =\n            new SchemaIterable(inputContext.newMetadata.getSchema())\n                .stream()\n                    // ID info is only on struct fields.\n                    .filter(SchemaIterable.SchemaElement::isStructField)\n                    .filter(\n                        element -> {\n                          StructField field = element.getField();\n                          String physicalName = ColumnMapping.getPhysicalName(field);\n                          long columnId = ColumnMapping.getColumnId(field);\n                          return !physicalName.equals(String.format(\"col-%s\", columnId));\n                        })\n                    .map(\n                        element -> {\n                          StructField field = element.getField();\n                          return String.format(\n                              \"%s(physicalName='%s', columnId=%s)\",\n                              element.getNamePath(),\n                              ColumnMapping.getPhysicalName(field),\n                              ColumnMapping.getColumnId(field));\n                        })\n                    .collect(toList());\n\n        if (!invalidFields.isEmpty()) {\n          throw DeltaErrors.icebergWriterCompatInvalidPhysicalName(invalidFields);\n        }\n      };\n\n  /**\n   * Checks that the table feature `invariants` is not active in the table, meaning there are no\n   * invariants stored in the table schema.\n   */\n  protected static final IcebergCompatCheck INVARIANTS_INACTIVE_CHECK =\n      (inputContext) -> {\n        // Note - since Kernel currently does not support the table feature `invariants` we will not\n        // hit this check for E2E writes since we will fail early due to unsupported write\n        // If Kernel starts supporting the feature `invariants` this check will become applicable\n        if (TableFeatures.hasInvariants(inputContext.newMetadata.getSchema())) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(INVARIANTS_W_FEATURE));\n        }\n      };\n\n  /**\n   * Checks that the table feature `changeDataFeed` is not active in the table, meaning the table\n   * property `delta.enableChangeDataFeed` is not enabled.\n   */\n  protected static final IcebergCompatCheck CHANGE_DATA_FEED_INACTIVE_CHECK =\n      (inputContext) -> {\n        // Note - since Kernel currently does not support the table feature `changeDataFeed` we will\n        // not hit this check for E2E writes since we will fail early due to unsupported write\n        // If Kernel starts supporting the feature `changeDataFeed` this check will become\n        // applicable\n        if (TableConfig.CHANGE_DATA_FEED_ENABLED.fromMetadata(inputContext.newMetadata)) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(CHANGE_DATA_FEED_W_FEATURE));\n        }\n      };\n\n  /**\n   * Checks that the table feature `rowTracking` is not active in the table, meaning the table\n   * property `delta.enableRowTracking` is not enabled.\n   */\n  protected static final IcebergCompatCheck ROW_TRACKING_INACTIVE_CHECK =\n      (inputContext) -> {\n        if (TableConfig.ROW_TRACKING_ENABLED.fromMetadata(inputContext.newMetadata)) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(ROW_TRACKING_W_FEATURE));\n        }\n      };\n\n  /**\n   * Checks that the table feature `checkConstraints` is not active in the table, meaning the table\n   * has no check constraints stored in its metadata configuration.\n   */\n  protected static final IcebergCompatCheck CHECK_CONSTRAINTS_INACTIVE_CHECK =\n      (inputContext) -> {\n        // Note - since Kernel currently does not support the table feature `checkConstraints` we\n        // will\n        // not hit this check for E2E writes since we will fail early due to unsupported write\n        // If Kernel starts supporting the feature `checkConstraints` this check will become\n        // applicable\n        if (TableFeatures.hasCheckConstraints(inputContext.newMetadata)) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(CONSTRAINTS_W_FEATURE));\n        }\n      };\n\n  /**\n   * Checks that the table feature `identityColumns` is not active in the table, meaning no identity\n   * columns exist in the table schema.\n   */\n  protected static final IcebergCompatCheck IDENTITY_COLUMNS_INACTIVE_CHECK =\n      (inputContext) -> {\n        // Note - since Kernel currently does not support the table feature `identityColumns` we\n        // will\n        // not hit this check for E2E writes since we will fail early due to unsupported write\n        // If Kernel starts supporting the feature `identityColumns` this check will become\n        // applicable\n        if (TableFeatures.hasIdentityColumns(inputContext.newMetadata)) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(IDENTITY_COLUMNS_W_FEATURE));\n        }\n      };\n\n  /**\n   * Checks that the table feature `generatedColumns` is not active in the table, meaning no\n   * generated columns exist in the table schema.\n   */\n  protected static final IcebergCompatCheck GENERATED_COLUMNS_INACTIVE_CHECK =\n      (inputContext) -> {\n        // Note - since Kernel currently does not support the table feature `generatedColumns` we\n        // will\n        // not hit this check for E2E writes since we will fail early due to unsupported write\n        // If Kernel starts supporting the feature `generatedColumns` this check will become\n        // applicable\n        if (TableFeatures.hasGeneratedColumns(inputContext.newMetadata)) {\n          throw DeltaErrors.icebergCompatIncompatibleTableFeatures(\n              inputContext.compatFeatureName, Collections.singleton(GENERATED_COLUMNS_W_FEATURE));\n        }\n      };\n\n  protected static final List<IcebergCompatCheck> COMMON_CHECKS =\n      Arrays.asList(\n          UNSUPPORTED_TYPES_CHECK,\n          PHYSICAL_NAMES_MATCH_FIELD_IDS_CHECK,\n          INVARIANTS_INACTIVE_CHECK,\n          CHANGE_DATA_FEED_INACTIVE_CHECK,\n          CHECK_CONSTRAINTS_INACTIVE_CHECK,\n          IDENTITY_COLUMNS_INACTIVE_CHECK,\n          GENERATED_COLUMNS_INACTIVE_CHECK);\n\n  @Override\n  abstract String compatFeatureName();\n\n  @Override\n  abstract TableConfig<Boolean> requiredDeltaTableProperty();\n\n  @Override\n  abstract List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties();\n\n  @Override\n  abstract List<TableFeature> requiredDependencyTableFeatures();\n\n  @Override\n  abstract List<IcebergCompatCheck> icebergCompatChecks();\n\n  abstract Set<TableFeature> getAllowedTableFeatures();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergWriterCompatV1MetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Stream;\n\n/**\n * Performs the validations and updates necessary to support the table feature IcebergWriterCompatV1\n * when it is enabled by the table property \"delta.enableIcebergWriterCompatV1\".\n *\n * <p>Requires that the following table properties are set to the specified values. If they are set\n * to an invalid value, throws an exception. If they are not set, enable them if possible.\n *\n * <ul>\n *   <li>Requires ID column mapping mode (cannot be enabled on existing table).\n *   <li>Requires icebergCompatV2 to be enabled.\n * </ul>\n *\n * <p>Checks that required table features are enabled: icebergCompatWriterV1, icebergCompatV2,\n * columnMapping\n *\n * <p>Checks the following:\n *\n * <ul>\n *   <li>Checks that all table features supported in the table's protocol are in the allow-list of\n *       table features. This simultaneously ensures that any unsupported features are not present\n *       (e.g. CDF, variant type, etc).\n *   <li>Checks that there are no fields with data type byte or short.\n *   <li>Checks that the table feature `invariants` is not active in the table (i.e. there are no\n *       invariants in the table schema). This is a special case where the incompatible feature\n *       `invariants` is in the allow-list of features since it is included by default in the table\n *       protocol for new tables. Since it is incompatible we must verify that it is inactive in the\n *       table.\n * </ul>\n *\n * TODO additional enforcements coming in (ie physicalName=fieldId)\n */\npublic class IcebergWriterCompatV1MetadataValidatorAndUpdater\n    extends IcebergWriterCompatMetadataValidatorAndUpdater {\n\n  /**\n   * Validates that any change to property {@link TableConfig#ICEBERG_WRITER_COMPAT_V1_ENABLED} is\n   * valid (for existing table). Currently, the changes we support are\n   *\n   * <ul>\n   *   <li>No change in enablement (true to true or false to false)\n   * </ul>\n   *\n   * The changes that we do not support and for which we throw an {@link KernelException} are\n   *\n   * <ul>\n   *   <li>Disabling on an existing table (true to false)\n   *   <li>Enabling on an existing table (false to true)\n   * </ul>\n   */\n  public static void validateIcebergWriterCompatV1Change(\n      Map<String, String> oldConfig, Map<String, String> newConfig) {\n    blockConfigChangeOnExistingTable(\n        TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED, oldConfig, newConfig);\n  }\n\n  /**\n   * Validate and update the given Iceberg Writer Compat V1 metadata.\n   *\n   * @param newMetadata Metadata after the current updates\n   * @param newProtocol Protocol after the current updates\n   * @return The updated metadata if the metadata is valid and updated, otherwise empty.\n   * @throws UnsupportedOperationException if the metadata is not compatible with Iceberg Writer V1\n   *     requirements\n   */\n  public static Optional<Metadata> validateAndUpdateIcebergWriterCompatV1Metadata(\n      boolean isCreatingNewTable,\n      Metadata newMetadata,\n      Protocol newProtocol,\n      Optional<Protocol> prevProtocol) {\n    return INSTANCE.validateAndUpdateMetadata(\n        new IcebergCompatInputContext(\n            INSTANCE.compatFeatureName(),\n            isCreatingNewTable,\n            newMetadata,\n            newProtocol,\n            prevProtocol));\n  }\n\n  /// //////////////////////////////////////////////////////////////////////////////\n  /// Define the compatibility and update checks for icebergWriterCompatV1       ///\n  /// //////////////////////////////////////////////////////////////////////////////\n\n  private static final IcebergWriterCompatV1MetadataValidatorAndUpdater INSTANCE =\n      new IcebergWriterCompatV1MetadataValidatorAndUpdater();\n\n  /**\n   * Enforcer for Iceberg compatibility V2 (required by V1). Ensures the ICEBERG_COMPAT_V2_ENABLED\n   * property is set to \"true\" and delegates validation to the V2 metadata validator.\n   */\n  private static final IcebergCompatRequiredTablePropertyEnforcer<Boolean>\n      ICEBERG_COMPAT_V2_ENABLED =\n          createIcebergCompatEnforcer(\n              TableConfig.ICEBERG_COMPAT_V2_ENABLED,\n              (inputContext) ->\n                  IcebergCompatV2MetadataValidatorAndUpdater\n                      .validateAndUpdateIcebergCompatV2Metadata(\n                          inputContext.isCreatingNewTable,\n                          inputContext.newMetadata,\n                          inputContext.newProtocol,\n                          inputContext.prevProtocol));\n\n  /**\n   * Current set of allowed table features for Iceberg writer compat V1. This combines the common\n   * features with V1-specific features (ICEBERG_COMPAT_V2_W_FEATURE, ICEBERG_WRITER_COMPAT_V1).\n   */\n  private static Set<TableFeature> ALLOWED_TABLE_FEATURES =\n      Stream.concat(\n              COMMON_ALLOWED_FEATURES.stream(),\n              Stream.of(ICEBERG_COMPAT_V2_W_FEATURE, ICEBERG_WRITER_COMPAT_V1))\n          .collect(toSet());\n\n  @Override\n  String compatFeatureName() {\n    return \"icebergWriterCompatV1\";\n  }\n\n  @Override\n  TableConfig<Boolean> requiredDeltaTableProperty() {\n    return TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED;\n  }\n\n  @Override\n  List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties() {\n    return Stream.of(CM_ID_MODE_ENABLED, ICEBERG_COMPAT_V2_ENABLED).collect(toList());\n  }\n\n  @Override\n  List<TableFeature> requiredDependencyTableFeatures() {\n    return Stream.of(\n            ICEBERG_WRITER_COMPAT_V1, ICEBERG_COMPAT_V2_W_FEATURE, COLUMN_MAPPING_RW_FEATURE)\n        .collect(toList());\n  }\n\n  @Override\n  protected Set<TableFeature> getAllowedTableFeatures() {\n    return ALLOWED_TABLE_FEATURES;\n  }\n\n  @Override\n  List<IcebergCompatCheck> icebergCompatChecks() {\n    return Stream.concat(\n            Stream.of(createUnsupportedFeaturesCheck(this), ROW_TRACKING_INACTIVE_CHECK),\n            COMMON_CHECKS.stream())\n        .collect(toList());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/icebergcompat/IcebergWriterCompatV3MetadataValidatorAndUpdater.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeature;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Stream;\n\npublic class IcebergWriterCompatV3MetadataValidatorAndUpdater\n    extends IcebergWriterCompatMetadataValidatorAndUpdater {\n\n  /**\n   * Validates that any change to property {@link TableConfig#ICEBERG_WRITER_COMPAT_V3_ENABLED} is\n   * valid. Currently, the changes we support are\n   *\n   * <ul>\n   *   <li>No change in enablement (true to true or false to false)\n   * </ul>\n   *\n   * The changes that we do not support and for which we throw an {@link KernelException} are\n   *\n   * <ul>\n   *   <li>Disabling on an existing table (true to false)\n   *   <li>Enabling on an existing table (false to true)\n   * </ul>\n   */\n  public static void validateIcebergWriterCompatV3Change(\n      Map<String, String> oldConfig, Map<String, String> newConfig) {\n    blockConfigChangeOnExistingTable(\n        TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED, oldConfig, newConfig);\n  }\n\n  /**\n   * Validate and update the given Iceberg Writer Compat V3 metadata.\n   *\n   * @param newMetadata Metadata after the current updates\n   * @param newProtocol Protocol after the current updates\n   * @return The updated metadata if the metadata is valid and updated, otherwise empty.\n   * @throws UnsupportedOperationException if the metadata is not compatible with Iceberg Writer V3\n   *     requirements\n   */\n  public static Optional<Metadata> validateAndUpdateIcebergWriterCompatV3Metadata(\n      boolean isCreatingNewTable,\n      Metadata newMetadata,\n      Protocol newProtocol,\n      Optional<Protocol> prevProtocol) {\n    return INSTANCE.validateAndUpdateMetadata(\n        new IcebergCompatInputContext(\n            INSTANCE.compatFeatureName(),\n            isCreatingNewTable,\n            newMetadata,\n            newProtocol,\n            prevProtocol));\n  }\n\n  /// //////////////////////////////////////////////////////////////////////////////\n  /// Define the compatibility and update checks for icebergWriterCompatV3       ///\n  /// //////////////////////////////////////////////////////////////////////////////\n\n  private static final IcebergWriterCompatV3MetadataValidatorAndUpdater INSTANCE =\n      new IcebergWriterCompatV3MetadataValidatorAndUpdater();\n\n  /**\n   * Enforcer for Iceberg compatibility V3. Ensures the ICEBERG_COMPAT_V3_ENABLED property is set to\n   * \"true\" and delegates validation to the V3 metadata validator.\n   */\n  private static final IcebergCompatRequiredTablePropertyEnforcer<Boolean>\n      ICEBERG_COMPAT_V3_ENABLED =\n          createIcebergCompatEnforcer(\n              TableConfig.ICEBERG_COMPAT_V3_ENABLED,\n              (inputContext) ->\n                  IcebergCompatV3MetadataValidatorAndUpdater\n                      .validateAndUpdateIcebergCompatV3Metadata(\n                          inputContext.isCreatingNewTable,\n                          inputContext.newMetadata,\n                          inputContext.newProtocol,\n                          inputContext.prevProtocol));\n\n  /**\n   * Current set of allowed table features for Iceberg writer compat V3. This combines the common\n   * features, v1-specific features with V3-specific features including variant support, deletion\n   * vectors, and row tracking.\n   */\n  private static Set<TableFeature> ALLOWED_TABLE_FEATURES =\n      Stream.concat(\n              COMMON_ALLOWED_FEATURES.stream(),\n              Stream.of(\n                  ICEBERG_COMPAT_V3_W_FEATURE,\n                  ICEBERG_WRITER_COMPAT_V3,\n                  DELETION_VECTORS_RW_FEATURE,\n                  VARIANT_RW_FEATURE,\n                  VARIANT_SHREDDING_RW_FEATURE,\n                  VARIANT_SHREDDING_PREVIEW_RW_FEATURE,\n                  VARIANT_RW_PREVIEW_FEATURE,\n                  // Also allow writerV1 features for backward compatibility.\n                  //\n                  // Note: We already enforce that these features cannot be enabled\n                  // through the CHECK_ONLY_ICEBERG_COMPAT_V3_ENABLED validation in\n                  // IcebergCompatV3MetadataValidatorAndUpdater. This ensures that\n                  // writerV1-related configs remain disabled even though the features\n                  // are listed here for protocol compatibility.\n                  ICEBERG_COMPAT_V2_W_FEATURE,\n                  ICEBERG_WRITER_COMPAT_V1))\n          .collect(toSet());;\n\n  @Override\n  String compatFeatureName() {\n    return \"icebergWriterCompatV3\";\n  }\n\n  @Override\n  TableConfig<Boolean> requiredDeltaTableProperty() {\n    return TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED;\n  }\n\n  @Override\n  List<IcebergCompatRequiredTablePropertyEnforcer> requiredDeltaTableProperties() {\n    return Stream.of(CM_ID_MODE_ENABLED, ICEBERG_COMPAT_V3_ENABLED).collect(toList());\n  }\n\n  @Override\n  List<TableFeature> requiredDependencyTableFeatures() {\n    return Stream.of(\n            ICEBERG_WRITER_COMPAT_V3, ICEBERG_COMPAT_V3_W_FEATURE, COLUMN_MAPPING_RW_FEATURE)\n        .collect(toList());\n  }\n\n  @Override\n  List<IcebergCompatCheck> icebergCompatChecks() {\n    return Stream.concat(Stream.of(createUnsupportedFeaturesCheck(this)), COMMON_CHECKS.stream())\n        .collect(toList());\n  }\n\n  @Override\n  protected Set<TableFeature> getAllowedTableFeatures() {\n    return ALLOWED_TABLE_FEATURES;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/lang/Lazy.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.lang;\n\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic class Lazy<T> {\n  private final Supplier<T> supplier;\n  private Optional<T> instance = Optional.empty();\n\n  public Lazy(Supplier<T> supplier) {\n    this.supplier = supplier;\n  }\n\n  public T get() {\n    if (!instance.isPresent()) {\n      instance = Optional.of(supplier.get());\n    }\n\n    return instance.get();\n  }\n\n  public boolean isPresent() {\n    return instance.isPresent();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/lang/ListUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.lang;\n\nimport io.delta.kernel.internal.util.Tuple2;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\npublic final class ListUtils {\n  private ListUtils() {}\n\n  public static <T> Tuple2<List<T>, List<T>> partition(\n      List<T> list, Predicate<? super T> predicate) {\n    final Map<Boolean, List<T>> partitionMap =\n        list.stream().collect(Collectors.partitioningBy(predicate));\n    return new Tuple2<>(partitionMap.get(true), partitionMap.get(false));\n  }\n\n  /** Remove once supported JDK (build) version is 21 or above */\n  public static <T> T getFirst(List<T> list) {\n    if (list.isEmpty()) {\n      throw new NoSuchElementException();\n    } else {\n      return list.get(0);\n    }\n  }\n\n  /** Remove once supported JDK (build) version is 21 or above */\n  public static <T> T getLast(List<T> list) {\n    if (list.isEmpty()) {\n      throw new NoSuchElementException();\n    } else {\n      return list.get(list.size() - 1);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metadatadomain/JsonMetadataDomain.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metadatadomain;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.rowtracking.RowTrackingMetadataDomain;\nimport java.util.Optional;\n\n/**\n * Abstract class representing a metadata domain, whose configuration string is a JSON serialization\n * of a domain object. This class provides methods to serialize and deserialize a metadata domain to\n * and from JSON. Concrete implementations, such as {@link RowTrackingMetadataDomain}, should extend\n * this class to define a specific metadata domain.\n *\n * <p>A metadata domain differs from {@link DomainMetadata}: {@link DomainMetadata} represents an\n * action that modifies the table's state by updating the configuration of a named metadata domain.\n * A metadata domain is a named domain used to organize configurations related to a specific table\n * feature.\n *\n * <p>For example, the row tracking feature uses a {@link RowTrackingMetadataDomain} to store the\n * highest assigned fresh row id of the table. When updated, the row tracking feature creates and\n * commits a new {@link DomainMetadata} action to reflect the change.\n *\n * <p>Serialization and deserialization are handled using Jackson's annotations. By default, all\n * public fields and getters are included in the serialization. When creating subclasses, ensure\n * that all fields to be serialized are accessible either through public fields or getters.\n *\n * <p>To control this behavior:\n *\n * <ul>\n *   <li>Annotate methods/fields with {@link JsonIgnore} if they should be excluded from\n *       serialization/deserialization.\n *   <li>Annotate constructor with {@link JsonCreator} to specify which constructor to use during\n *       deserialization.\n *   <li>Use {@link JsonProperty} on constructor parameters to define the JSON field names during\n *       deserialization.\n * </ul>\n */\npublic abstract class JsonMetadataDomain {\n  // Configure the ObjectMapper with the same settings used in Delta-Spark\n  private static final ObjectMapper OBJECT_MAPPER =\n      new ObjectMapper()\n          .registerModule(new Jdk8Module()) // To support Optional\n          .setSerializationInclusion(JsonInclude.Include.NON_ABSENT) // Exclude empty Optionals\n          .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false /* state */);\n\n  /**\n   * Deserializes a JSON string into an instance of the specified metadata domain.\n   *\n   * @param json the JSON string to deserialize\n   * @param clazz the concrete class of the metadata domain object to deserialize into\n   * @param <T> the type of the object\n   * @return the deserialized object\n   * @throws KernelException if the JSON string cannot be parsed\n   */\n  protected static <T> T fromJsonConfiguration(String json, Class<T> clazz) {\n    try {\n      return OBJECT_MAPPER.readValue(json, clazz);\n    } catch (JsonProcessingException e) {\n      throw new KernelException(\n          String.format(\n              \"Failed to parse JSON string into a %s instance. JSON content: %s\",\n              clazz.getSimpleName(), json),\n          e);\n    }\n  }\n\n  /**\n   * Retrieves the domain metadata from a snapshot for a given domain, and deserializes it into an\n   * instance of the specified metadata domain class.\n   *\n   * @param snapshot the snapshot to read from\n   * @param clazz the metadata domain class of the object to deserialize into\n   * @param domainName the name of the domain\n   * @param <T> the type of the metadata domain object\n   * @return an Optional containing the deserialized object if the domain metadata is found,\n   *     otherwise an empty Optional\n   */\n  protected static <T> Optional<T> fromSnapshot(\n      SnapshotImpl snapshot, Class<T> clazz, String domainName) {\n    return snapshot\n        .getDomainMetadata(domainName)\n        .map(config -> fromJsonConfiguration(config, clazz));\n  }\n\n  /**\n   * Returns the name of the domain.\n   *\n   * @return the domain name\n   */\n  @JsonIgnore\n  public abstract String getDomainName();\n\n  /**\n   * Serializes this object into a JSON string.\n   *\n   * @return the JSON string representation of this object\n   * @throws KernelException if the object cannot be serialized\n   */\n  public String toJsonConfiguration() {\n    try {\n      return OBJECT_MAPPER.writeValueAsString(this);\n    } catch (JsonProcessingException e) {\n      throw new KernelException(\n          String.format(\n              \"Could not serialize %s (domain: %s) to JSON\",\n              this.getClass().getSimpleName(), getDomainName()),\n          e);\n    }\n  }\n\n  /**\n   * Generate a {@link DomainMetadata} action from this metadata domain.\n   *\n   * @return the DomainMetadata action instance\n   */\n  public DomainMetadata toDomainMetadata() {\n    return new DomainMetadata(getDomainName(), toJsonConfiguration(), false /* removed */);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/Counter.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.LongAdder;\n\n/** A long counter that uses {@link AtomicLong} to count events. */\npublic class Counter {\n\n  private final LongAdder counter = new LongAdder();\n\n  /** Increment the counter by 1. */\n  public void increment() {\n    increment(1L);\n  }\n\n  /**\n   * Increment the counter by the provided amount.\n   *\n   * @param amount to be incremented.\n   */\n  public void increment(long amount) {\n    counter.add(amount);\n  }\n\n  /**\n   * Reports the current count.\n   *\n   * @return The current count.\n   */\n  public long value() {\n    return counter.longValue();\n  }\n\n  /** Resets the current count to 0. */\n  public void reset() {\n    counter.reset();\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"Counter(%s)\", counter.longValue());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/DeltaOperationReportImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.metrics.DeltaOperationReport;\nimport java.util.Optional;\nimport java.util.UUID;\n\n/** Basic POJO implementation of {@link DeltaOperationReport} */\npublic abstract class DeltaOperationReportImpl implements DeltaOperationReport {\n\n  private final String tablePath;\n  private final UUID reportUUID;\n  private final Optional<Exception> exception;\n\n  protected DeltaOperationReportImpl(String tablePath, Optional<Exception> exception) {\n    this.tablePath = requireNonNull(tablePath);\n    this.reportUUID = UUID.randomUUID();\n    this.exception = requireNonNull(exception);\n  }\n\n  @Override\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  @Override\n  public UUID getReportUUID() {\n    return reportUUID;\n  }\n\n  @Override\n  public Optional<Exception> getException() {\n    return exception;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/MetricsReportSerializer.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.ser.std.ToStringSerializer;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.metrics.MetricsReport;\nimport io.delta.kernel.types.StructType;\nimport java.io.IOException;\n\n/** Provides Jackson ObjectMapper configuration for serializing {@link MetricsReport} types */\npublic final class MetricsReportSerializer {\n\n  /**\n   * ObjectMapper configured for serializing metrics reports.\n   *\n   * <p>This ObjectMapper is pre-configured with custom serializers for:\n   *\n   * <ul>\n   *   <li>Java 8 Optional types (serialized as null when empty)\n   *   <li>Exceptions (serialized using their toString() representation)\n   *   <li>Complex types like StructType and Predicate (using string representation)\n   *   <li>Column objects (serialized as arrays of field names)\n   * </ul>\n   */\n  public static final ObjectMapper OBJECT_MAPPER =\n      new ObjectMapper()\n          .registerModule(new Jdk8Module()) // To support Optional\n          .registerModule( // Serialize Exception using toString()\n              new SimpleModule().addSerializer(Exception.class, new ToStringSerializer()))\n          .registerModule( // Serialize StructType using toString\n              new SimpleModule().addSerializer(StructType.class, new ToStringSerializer()))\n          .registerModule( // Serialize Predicate using toString\n              new SimpleModule().addSerializer(Predicate.class, new ToStringSerializer()))\n          .registerModule( // Serialize Column to exclude un-necessary fields\n              new SimpleModule().addSerializer(Column.class, new ColumnSerializer()));\n\n  private static class ColumnSerializer extends JsonSerializer<Column> {\n    @Override\n    public void serialize(Column value, JsonGenerator gen, SerializerProvider serializers)\n        throws IOException {\n      gen.writeStartArray();\n      for (String name : value.getNames()) {\n        gen.writeString(name);\n      }\n      gen.writeEndArray();\n    }\n  }\n\n  private MetricsReportSerializer() {}\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/ScanMetrics.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport io.delta.kernel.metrics.ScanMetricsResult;\n\n/**\n * Stores the metrics for an ongoing scan. These metrics are updated and recorded throughout the\n * scan using this class.\n *\n * <p>At report time, we create an immutable {@link ScanMetricsResult} from an instance of {@link\n * ScanMetrics} to capture the metrics collected during the scan. The {@link ScanMetricsResult}\n * interface exposes getters for any metrics collected in this class.\n */\npublic class ScanMetrics {\n\n  public final Timer totalPlanningTimer = new Timer();\n\n  public final Counter addFilesCounter = new Counter();\n\n  public final Counter addFilesFromDeltaFilesCounter = new Counter();\n\n  public final Counter activeAddFilesCounter = new Counter();\n\n  public final Counter duplicateAddFilesCounter = new Counter();\n\n  public final Counter removeFilesFromDeltaFilesCounter = new Counter();\n\n  public ScanMetricsResult captureScanMetricsResult() {\n    return new ScanMetricsResult() {\n\n      final long totalPlanningDurationNs = totalPlanningTimer.totalDurationNs();\n      final long numAddFilesSeen = addFilesCounter.value();\n      final long numAddFilesSeenFromDeltaFiles = addFilesFromDeltaFilesCounter.value();\n      final long numActiveAddFiles = activeAddFilesCounter.value();\n      final long numDuplicateAddFiles = duplicateAddFilesCounter.value();\n      final long numRemoveFilesSeenFromDeltaFiles = removeFilesFromDeltaFilesCounter.value();\n\n      @Override\n      public long getTotalPlanningDurationNs() {\n        return totalPlanningDurationNs;\n      }\n\n      @Override\n      public long getNumAddFilesSeen() {\n        return numAddFilesSeen;\n      }\n\n      @Override\n      public long getNumAddFilesSeenFromDeltaFiles() {\n        return numAddFilesSeenFromDeltaFiles;\n      }\n\n      @Override\n      public long getNumActiveAddFiles() {\n        return numActiveAddFiles;\n      }\n\n      @Override\n      public long getNumDuplicateAddFiles() {\n        return numDuplicateAddFiles;\n      }\n\n      @Override\n      public long getNumRemoveFilesSeenFromDeltaFiles() {\n        return numRemoveFilesSeenFromDeltaFiles;\n      }\n    };\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"ScanMetrics(totalPlanningTimer=%s, addFilesCounter=%s, addFilesFromDeltaFilesCounter=%s,\"\n            + \" activeAddFilesCounter=%s, duplicateAddFilesCounter=%s, \"\n            + \"removeFilesFromDeltaFilesCounter=%s\",\n        totalPlanningTimer,\n        addFilesCounter,\n        addFilesFromDeltaFilesCounter,\n        activeAddFilesCounter,\n        duplicateAddFilesCounter,\n        removeFilesFromDeltaFilesCounter);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/ScanReportImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport static java.util.Objects.requireNonNull;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.metrics.ScanMetricsResult;\nimport io.delta.kernel.metrics.ScanReport;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\nimport java.util.UUID;\n\n/** A basic POJO implementation of {@link ScanReport} for creating them */\npublic class ScanReportImpl extends DeltaOperationReportImpl implements ScanReport {\n\n  private final long tableVersion;\n  private final StructType tableSchema;\n  private final UUID snapshotReportUUID;\n  private final Optional<Predicate> filter;\n  private final StructType readSchema;\n  private final Optional<Predicate> partitionPredicate;\n  private final Optional<Predicate> dataSkippingFilter;\n  private final boolean isFullyConsumed;\n  private final ScanMetricsResult scanMetricsResult;\n\n  public ScanReportImpl(\n      String tablePath,\n      long tableVersion,\n      StructType tableSchema,\n      UUID snapshotReportUUID,\n      Optional<Predicate> filter,\n      StructType readSchema,\n      Optional<Predicate> partitionPredicate,\n      Optional<Predicate> dataSkippingFilter,\n      boolean isFullyConsumed,\n      ScanMetrics scanMetrics,\n      Optional<Exception> exception) {\n    super(tablePath, exception);\n    this.tableVersion = tableVersion;\n    this.tableSchema = requireNonNull(tableSchema);\n    this.snapshotReportUUID = requireNonNull(snapshotReportUUID);\n    this.filter = requireNonNull(filter);\n    this.readSchema = requireNonNull(readSchema);\n    this.partitionPredicate = requireNonNull(partitionPredicate);\n    this.dataSkippingFilter = requireNonNull(dataSkippingFilter);\n    this.isFullyConsumed = isFullyConsumed;\n    this.scanMetricsResult = requireNonNull(scanMetrics).captureScanMetricsResult();\n  }\n\n  @Override\n  public long getTableVersion() {\n    return tableVersion;\n  }\n\n  @Override\n  public StructType getTableSchema() {\n    return tableSchema;\n  }\n\n  @Override\n  public UUID getSnapshotReportUUID() {\n    return snapshotReportUUID;\n  }\n\n  @Override\n  public Optional<Predicate> getFilter() {\n    return filter;\n  }\n\n  @Override\n  public StructType getReadSchema() {\n    return readSchema;\n  }\n\n  @Override\n  public Optional<Predicate> getPartitionPredicate() {\n    return partitionPredicate;\n  }\n\n  @Override\n  public Optional<Predicate> getDataSkippingFilter() {\n    return dataSkippingFilter;\n  }\n\n  @Override\n  public boolean getIsFullyConsumed() {\n    return isFullyConsumed;\n  }\n\n  @Override\n  public ScanMetricsResult getScanMetrics() {\n    return scanMetricsResult;\n  }\n\n  @Override\n  public String toJson() throws JsonProcessingException {\n    return MetricsReportSerializer.OBJECT_MAPPER.writeValueAsString(this);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/SnapshotMetrics.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport io.delta.kernel.metrics.SnapshotMetricsResult;\nimport java.util.Optional;\n\n/**\n * Stores the metrics for an ongoing snapshot construction. These metrics are updated and recorded\n * throughout the snapshot query using this class.\n *\n * <p>At report time, we create an immutable {@link SnapshotMetricsResult} from an instance of\n * {@link SnapshotMetrics} to capture the metrics collected during the query. The {@link\n * SnapshotMetricsResult} interface exposes getters for any metrics collected in this class.\n */\npublic class SnapshotMetrics {\n\n  public final Timer loadSnapshotTotalTimer = new Timer();\n\n  public final Timer computeTimestampToVersionTotalDurationTimer = new Timer();\n\n  public final Timer loadProtocolMetadataTotalDurationTimer = new Timer();\n\n  public final Timer loadLogSegmentTotalDurationTimer = new Timer();\n\n  public final Timer loadCrcTotalDurationTimer = new Timer();\n\n  public SnapshotMetricsResult captureSnapshotMetricsResult() {\n    return new SnapshotMetricsResult() {\n      final Optional<Long> computeTimestampToVersionTotalDurationResult =\n          computeTimestampToVersionTotalDurationTimer.totalDurationIfRecorded();\n      final long loadSnapshotTotalDurationResult = loadSnapshotTotalTimer.totalDurationNs();\n      final long loadProtocolMetadataTotalDurationResult =\n          loadProtocolMetadataTotalDurationTimer.totalDurationNs();\n      final long loadLogSegmentTotalDurationResult =\n          loadLogSegmentTotalDurationTimer.totalDurationNs();\n      final long loadCrcTotalDurationResult = loadCrcTotalDurationTimer.totalDurationNs();\n\n      @Override\n      public Optional<Long> getComputeTimestampToVersionTotalDurationNs() {\n        return computeTimestampToVersionTotalDurationResult;\n      }\n\n      @Override\n      public long getLoadSnapshotTotalDurationNs() {\n        return loadSnapshotTotalDurationResult;\n      }\n\n      @Override\n      public long getLoadProtocolMetadataTotalDurationNs() {\n        return loadProtocolMetadataTotalDurationResult;\n      }\n\n      @Override\n      public long getLoadLogSegmentTotalDurationNs() {\n        return loadLogSegmentTotalDurationResult;\n      }\n\n      @Override\n      public long getLoadCrcTotalDurationNs() {\n        return loadCrcTotalDurationResult;\n      }\n    };\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"SnapshotMetrics(\"\n            + \"computeTimestampToVersionTotalDurationTimer=%s, \"\n            + \"loadSnapshotTotalTimer=%s,\"\n            + \"loadProtocolMetadataTotalDurationTimer=%s, \"\n            + \"timeToBuildLogSegmentForVersionTimer=%s, \"\n            + \"loadCrcTotalDurationNsTimer=%s)\",\n        computeTimestampToVersionTotalDurationTimer,\n        loadSnapshotTotalTimer,\n        loadProtocolMetadataTotalDurationTimer,\n        loadLogSegmentTotalDurationTimer,\n        loadCrcTotalDurationTimer);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/SnapshotQueryContext.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.engine.MetricsReporter;\nimport io.delta.kernel.metrics.SnapshotReport;\nimport java.util.Optional;\n\n/**\n * Stores the context for a given Snapshot query. This includes information about the query\n * parameters (i.e. table path, time travel parameters), updated state as the snapshot query\n * progresses (i.e. resolved version), and metrics.\n *\n * <p>This is used to generate a {@link SnapshotReport}. It exists from snapshot query initiation\n * until either successful snapshot construction or failure.\n */\npublic class SnapshotQueryContext {\n\n  /** Creates a {@link SnapshotQueryContext} for a Snapshot created by a latest snapshot query */\n  public static SnapshotQueryContext forLatestSnapshot(String tablePath) {\n    return new SnapshotQueryContext(\n        tablePath, Optional.empty(), Optional.empty(), Optional.empty());\n  }\n\n  /** Creates a {@link SnapshotQueryContext} for a Snapshot created by a AS OF VERSION query */\n  public static SnapshotQueryContext forVersionSnapshot(String tablePath, long version) {\n    return new SnapshotQueryContext(\n        tablePath, Optional.of(version), Optional.empty(), Optional.empty());\n  }\n\n  /** Creates a {@link SnapshotQueryContext} for a Snapshot created by a AS OF TIMESTAMP query */\n  public static SnapshotQueryContext forTimestampSnapshot(String tablePath, long timestamp) {\n    return new SnapshotQueryContext(\n        tablePath, Optional.empty(), Optional.empty(), Optional.of(timestamp));\n  }\n\n  private final String tablePath;\n\n  /** The version provided in a time-travel-by-version query, if any. */\n  private final Optional<Long> providedVersion;\n\n  /** The timestamp provided in a time-travel-by-timestamp query, if any. */\n  private final Optional<Long> providedTimestamp;\n\n  private final SnapshotMetrics snapshotMetrics = new SnapshotMetrics();\n\n  /** The table version that this snapshot is actually resolved to. */\n  private Optional<Long> resolvedVersion;\n\n  private Optional<Long> checkpointVersion;\n\n  /**\n   * @param tablePath the table path for the table being queried\n   * @param providedVersion the provided version for a time-travel-by-version query, empty if this\n   *     is not a time-travel-by-version query\n   * @param checkpointVersion the version of the checkpoint used for this snapshot, empty if no\n   *     checkpoint was used or if this is a failed snapshot construction\n   * @param providedTimestamp the provided timestamp for a time-travel-by-timestamp query, empty if\n   *     this is not a time-travel-by-timestamp query\n   */\n  private SnapshotQueryContext(\n      String tablePath,\n      Optional<Long> providedVersion,\n      Optional<Long> checkpointVersion,\n      Optional<Long> providedTimestamp) {\n    this.tablePath = tablePath;\n    this.providedVersion = providedVersion;\n    this.resolvedVersion = providedVersion;\n    this.checkpointVersion = checkpointVersion;\n    this.providedTimestamp = providedTimestamp;\n  }\n\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  public Optional<Long> getResolvedVersion() {\n    return resolvedVersion;\n  }\n\n  public Optional<Long> getCheckpointVersion() {\n    return checkpointVersion;\n  }\n\n  public Optional<Long> getProvidedTimestamp() {\n    return providedTimestamp;\n  }\n\n  /**\n   * Returns true if this snapshot was requested as the latest snapshot (i.e., no time-travel\n   * parameters were provided). Note that this is intent-based - it indicates what the user\n   * requested, not whether the snapshot is actually the latest version.\n   */\n  public boolean isLatestQuery() {\n    return !providedVersion.isPresent() && !providedTimestamp.isPresent();\n  }\n\n  public SnapshotMetrics getSnapshotMetrics() {\n    return snapshotMetrics;\n  }\n\n  public String getQueryDisplayStr() {\n    final String resolvedVersionStr =\n        resolvedVersion.map(v -> String.format(\" (RESOLVED TO VERSION %d)\", v)).orElse(\"\");\n\n    if (providedVersion.isPresent()) {\n      return \"AS OF VERSION \" + resolvedVersion.get();\n    } else if (providedTimestamp.isPresent()) {\n      return \"AS OF TIMESTAMP \" + providedTimestamp.get() + resolvedVersionStr;\n    } else {\n      return \"LATEST SNAPSHOT\" + resolvedVersionStr;\n    }\n  }\n\n  /**\n   * Set the resolved version that was actually loaded for this snapshot query.\n   *\n   * <p>For AS OF TIMESTAMP queries, this should be set upon timestamp-to-version resolution.\n   *\n   * <p>For AS OF LATEST queries, this should be set after log segment construction, when we learn\n   * what the latest version of the table really is.\n   */\n  public void setResolvedVersion(long resolvedVersion) {\n    this.resolvedVersion = Optional.of(resolvedVersion);\n  }\n\n  /** Updates the {@code checkpointVersion} stored in this snapshot context. */\n  public void setCheckpointVersion(Optional<Long> checkpointVersion) {\n    requireNonNull(checkpointVersion, \"checkpointVersion cannot be null\");\n    checkArgument(\n        !checkpointVersion.isPresent() || checkpointVersion.get() >= 0,\n        \"Invalid checkpoint version: %s\",\n        checkpointVersion);\n    this.checkpointVersion = checkpointVersion;\n  }\n\n  /** Creates a {@link SnapshotReport} and pushes it to any {@link MetricsReporter}s. */\n  public void recordSnapshotErrorReport(Engine engine, Exception e) {\n    SnapshotReport snapshotReport = SnapshotReportImpl.forError(this, e);\n    engine.getMetricsReporters().forEach(reporter -> reporter.report(snapshotReport));\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"SnapshotQueryContext(tablePath=%s, version=%s, providedTimestamp=%s, \"\n            + \"checkpointVersion=%s, snapshotMetric=%s)\",\n        tablePath, resolvedVersion, providedTimestamp, checkpointVersion, snapshotMetrics);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/SnapshotReportImpl.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport static java.util.Objects.requireNonNull;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport io.delta.kernel.metrics.SnapshotMetricsResult;\nimport io.delta.kernel.metrics.SnapshotReport;\nimport java.util.Optional;\n\n/** A basic POJO implementation of {@link SnapshotReport} for creating them */\npublic class SnapshotReportImpl extends DeltaOperationReportImpl implements SnapshotReport {\n\n  /**\n   * Creates a {@link SnapshotReport} for a failed snapshot query.\n   *\n   * @param snapshotContext context/metadata about the snapshot query\n   * @param e the exception that was thrown\n   */\n  public static SnapshotReport forError(SnapshotQueryContext snapshotContext, Exception e) {\n    return new SnapshotReportImpl(\n        snapshotContext.getTablePath(),\n        snapshotContext.getSnapshotMetrics(),\n        snapshotContext.getResolvedVersion(),\n        snapshotContext.getCheckpointVersion(),\n        snapshotContext.getProvidedTimestamp(),\n        Optional.of(e));\n  }\n\n  /**\n   * Creates a {@link SnapshotReport} for a successful snapshot query.\n   *\n   * @param snapshotContext context/metadata about the snapshot query\n   */\n  public static SnapshotReport forSuccess(SnapshotQueryContext snapshotContext) {\n    return new SnapshotReportImpl(\n        snapshotContext.getTablePath(),\n        snapshotContext.getSnapshotMetrics(),\n        snapshotContext.getResolvedVersion(),\n        snapshotContext.getCheckpointVersion(),\n        snapshotContext.getProvidedTimestamp(),\n        Optional.empty() /* exception */);\n  }\n\n  private final SnapshotMetricsResult snapshotMetrics;\n  private final Optional<Long> version;\n  private final Optional<Long> checkpointVersion;\n  private final Optional<Long> providedTimestamp;\n\n  private SnapshotReportImpl(\n      String tablePath,\n      SnapshotMetrics snapshotMetrics,\n      Optional<Long> version,\n      Optional<Long> checkpointVersion,\n      Optional<Long> providedTimestamp,\n      Optional<Exception> exception) {\n    super(tablePath, exception);\n    this.snapshotMetrics = requireNonNull(snapshotMetrics).captureSnapshotMetricsResult();\n    this.version = requireNonNull(version);\n    this.checkpointVersion = requireNonNull(checkpointVersion);\n    this.providedTimestamp = requireNonNull(providedTimestamp);\n  }\n\n  @Override\n  public SnapshotMetricsResult getSnapshotMetrics() {\n    return snapshotMetrics;\n  }\n\n  @Override\n  public Optional<Long> getVersion() {\n    return version;\n  }\n\n  @Override\n  public Optional<Long> getCheckpointVersion() {\n    return checkpointVersion;\n  }\n\n  @Override\n  public Optional<Long> getProvidedTimestamp() {\n    return providedTimestamp;\n  }\n\n  @Override\n  public String toJson() throws JsonProcessingException {\n    return MetricsReportSerializer.OBJECT_MAPPER.writeValueAsString(this);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/Timer.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.metrics;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport java.util.Optional;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.function.Supplier;\n\n/** A timer class for measuring the duration of operations in nanoseconds */\npublic class Timer {\n\n  private final LongAdder count = new LongAdder();\n  private final LongAdder totalTime = new LongAdder();\n\n  /** @return the number of times this timer was used to record a duration. */\n  public long count() {\n    return count.longValue();\n  }\n\n  /** @return the total duration that was recorded in nanoseconds */\n  public long totalDurationNs() {\n    return totalTime.longValue();\n  }\n\n  /** @return the total duration that was recorded in milliseconds */\n  public long totalDurationMs() {\n    return TimeUnit.NANOSECONDS.toMillis(totalDurationNs());\n  }\n\n  /**\n   * @return An optional storing the total duration recorded in the timer if the timer has been used\n   *     to record a duration at least once. If the timer has not been used, returns empty.\n   */\n  public Optional<Long> totalDurationIfRecorded() {\n    return count() > 0 ? Optional.of(totalDurationNs()) : Optional.empty();\n  }\n\n  /**\n   * Starts the timer and returns a {@link Timed} instance. Call {@link Timed#stop()} to complete\n   * the timing.\n   *\n   * @return A {@link Timed} instance with the start time recorded.\n   */\n  public Timed start() {\n    return new DefaultTimed(this);\n  }\n\n  /**\n   * Records a custom amount.\n   *\n   * @param amount The amount to record in nanoseconds\n   */\n  public void record(long amount) {\n    checkArgument(amount >= 0, \"Cannot record %s: must be >= 0\", amount);\n    this.totalTime.add(amount);\n    this.count.increment();\n  }\n\n  public <T> T time(Supplier<T> supplier) {\n    try (Timed ignore = start()) {\n      return supplier.get();\n    }\n  }\n\n  public <T> T timeCallable(Callable<T> callable) throws Exception {\n    try (Timed ignore = start()) {\n      return callable.call();\n    }\n  }\n\n  /**\n   * Times an operation that can throw a specific checked exception type.\n   *\n   * @param <T> The return type\n   * @param <E> The exception type that can be thrown\n   */\n  @FunctionalInterface\n  public interface ThrowingSupplier<T, E extends Exception> {\n    T get() throws E;\n  }\n\n  /** Times an operation that can throw a specific checked exception type. */\n  @SuppressWarnings(\"unchecked\")\n  public <T, E extends Exception> T timeChecked(ThrowingSupplier<T, E> operation) throws E {\n    try (Timed ignore = start()) {\n      return operation.get();\n    } catch (RuntimeException | Error e) {\n      throw e;\n    } catch (Exception e) {\n      // Safe cast since operation can only throw E or unchecked exceptions (handled above)\n      throw (E) e;\n    }\n  }\n\n  public void time(Runnable runnable) {\n    try (Timed ignore = start()) {\n      runnable.run();\n    }\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"Timer(duration=%s ns, count=%s)\", totalDurationNs(), count());\n  }\n\n  /**\n   * A timing sample that carries internal state about the Timer's start position. The timing can be\n   * completed by calling {@link Timed#stop()}.\n   */\n  public interface Timed extends AutoCloseable {\n    /** Stops the timer and records the total duration up until {@link Timer#start()} was called. */\n    void stop();\n\n    @Override\n    default void close() {\n      stop();\n    }\n\n    Timed NOOP = () -> {};\n  }\n\n  private static class DefaultTimed implements Timed {\n    private final Timer timer;\n    private final long startTime;\n    private boolean closed;\n\n    private DefaultTimed(Timer timer) {\n      this.timer = timer;\n      this.startTime = System.nanoTime();\n    }\n\n    @Override\n    public void stop() {\n      if (closed) {\n        throw new IllegalStateException(\"called stop() multiple times\");\n      }\n      timer.record(System.nanoTime() - startTime);\n      closed = true;\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/TransactionMetrics.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.internal.stats.FileSizeHistogram;\nimport io.delta.kernel.metrics.FileSizeHistogramResult;\nimport io.delta.kernel.metrics.TransactionMetricsResult;\nimport java.util.Optional;\n\n/**\n * Stores the metrics for an ongoing transaction. These metrics are updated and recorded throughout\n * the transaction using this class.\n *\n * <p>At report time, we create an immutable {@link TransactionMetricsResult} from an instance of\n * {@link TransactionMetrics} to capture the metrics collected during the transaction. The {@link\n * TransactionMetricsResult} interface exposes getters for any metrics collected in this class.\n */\npublic class TransactionMetrics {\n\n  /**\n   * @return a fresh TransactionMetrics object with a default tableFileSizeHistogram (with 0 counts)\n   */\n  public static TransactionMetrics forNewTable() {\n    return new TransactionMetrics(Optional.of(FileSizeHistogram.createDefaultHistogram()));\n  }\n\n  /**\n   * @return a fresh TransactionMetrics object with an initial tableFileSizeHistogram as provided\n   */\n  public static TransactionMetrics withExistingTableFileSizeHistogram(\n      Optional<FileSizeHistogram> tableFileSizeHistogram) {\n    return new TransactionMetrics(tableFileSizeHistogram);\n  }\n\n  public final Timer totalCommitTimer = new Timer();\n\n  public final Counter commitAttemptsCounter = new Counter();\n\n  private final Counter addFilesCounter = new Counter();\n\n  private final Counter removeFilesCounter = new Counter();\n\n  public final Counter totalActionsCounter = new Counter();\n\n  private final Counter addFilesSizeInBytesCounter = new Counter();\n\n  private final Counter removeFilesSizeInBytesCounter = new Counter();\n\n  private Optional<FileSizeHistogram> tableFileSizeHistogram;\n\n  private TransactionMetrics(Optional<FileSizeHistogram> tableFileSizeHistogram) {\n    this.tableFileSizeHistogram = tableFileSizeHistogram;\n  }\n\n  /**\n   * Updates the metrics for a seen AddFile with size {@code addFileSize}. Specifically, updates\n   * addFilesCounter, addFilesSizeInBytesCounter, and tableFileSizeHistogram. Note, it does NOT\n   * increment totalActionsCounter, this needs to be done separately.\n   *\n   * @param addFileSize the size of the add file to update the metrics for\n   */\n  public void updateForAddFile(long addFileSize) {\n    checkArgument(addFileSize >= 0, \"File size must be non-negative, got %s\", addFileSize);\n    addFilesCounter.increment();\n    addFilesSizeInBytesCounter.increment(addFileSize);\n    tableFileSizeHistogram.ifPresent(histogram -> histogram.insert(addFileSize));\n  }\n\n  /**\n   * Updates the metrics for a seen RemoveFile with size {@code removeFileSize}. Specifically,\n   * updates removeFilesCounter, removeFilesSizeInBytesCounter, and tableFileSizeHistogram. Note, it\n   * does NOT increment totalActionsCounter, this needs to be done separately.\n   *\n   * @param removeFileSize the size of the remove file to update the metrics for\n   */\n  public void updateForRemoveFile(long removeFileSize) {\n    checkArgument(removeFileSize >= 0, \"File size must be non-negative, got %s\", removeFileSize);\n    removeFilesCounter.increment();\n    removeFilesSizeInBytesCounter.increment(removeFileSize);\n    tableFileSizeHistogram.ifPresent(histogram -> histogram.remove(removeFileSize));\n  }\n\n  /**\n   * Resets any action metrics for a failed commit to prepare them for retrying. Specifically,\n   *\n   * <ul>\n   *   <li>Resets addFilesCounter, removeFilesCounter, totalActionsCounter,\n   *       addFilesSizeInBytesCounter, and removeFilesSizeInBytesCounter to 0\n   *   <li>Sets tableFileSizeHistogram to be empty since we don't know the updated distribution\n   *       after the conflicting txn committed\n   * </ul>\n   *\n   * Action counters / tableFileSizeHistogram may be partially incremented if an action iterator is\n   * not read to completion (i.e. if an exception interrupts a file write). This allows us to reset\n   * the counters so that we can increment them correctly from 0 on a retry.\n   */\n  public void resetActionMetricsForRetry() {\n    addFilesCounter.reset();\n    addFilesSizeInBytesCounter.reset();\n    removeFilesCounter.reset();\n    totalActionsCounter.reset();\n    removeFilesSizeInBytesCounter.reset();\n    // For now, on retry we set tableFileSizeHistogram = Optional.empty() because we don't know the\n    // correct state of tableFileSizeHistogram after conflicting transaction has committed\n    tableFileSizeHistogram = Optional.empty();\n  }\n\n  public TransactionMetricsResult captureTransactionMetricsResult() {\n    return new TransactionMetricsResult() {\n\n      final long totalCommitDurationNs = totalCommitTimer.totalDurationNs();\n      final long numCommitAttempts = commitAttemptsCounter.value();\n      final long numAddFiles = addFilesCounter.value();\n      final long totalAddFilesSizeInBytes = addFilesSizeInBytesCounter.value();\n      final long numRemoveFiles = removeFilesCounter.value();\n      final long numTotalActions = totalActionsCounter.value();\n      final long totalRemoveFileSizeInBytes = removeFilesSizeInBytesCounter.value();\n      final Optional<FileSizeHistogramResult> tableFileSizeHistogramResult =\n          tableFileSizeHistogram.map(FileSizeHistogram::captureFileSizeHistogramResult);\n\n      @Override\n      public long getTotalCommitDurationNs() {\n        return totalCommitDurationNs;\n      }\n\n      @Override\n      public long getNumCommitAttempts() {\n        return numCommitAttempts;\n      }\n\n      @Override\n      public long getNumAddFiles() {\n        return numAddFiles;\n      }\n\n      @Override\n      public long getNumRemoveFiles() {\n        return numRemoveFiles;\n      }\n\n      @Override\n      public long getNumTotalActions() {\n        return numTotalActions;\n      }\n\n      @Override\n      public long getTotalAddFilesSizeInBytes() {\n        return totalAddFilesSizeInBytes;\n      }\n\n      @Override\n      public long getTotalRemoveFilesSizeInBytes() {\n        return totalRemoveFileSizeInBytes;\n      }\n\n      @Override\n      public Optional<FileSizeHistogramResult> getTableFileSizeHistogram() {\n        return tableFileSizeHistogramResult;\n      }\n    };\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"TransactionMetrics(totalCommitTimer=%s, commitAttemptsCounter=%s, addFilesCounter=%s, \"\n            + \"removeFilesCounter=%s, totalActionsCounter=%s, totalAddFilesSizeInBytes=%s,\"\n            + \"totalRemoveFilesSizeInBytes=%s, tableFileSizeHistogram=%s)\",\n        totalCommitTimer,\n        commitAttemptsCounter,\n        addFilesCounter,\n        removeFilesCounter,\n        totalActionsCounter,\n        addFilesSizeInBytesCounter,\n        removeFilesSizeInBytesCounter,\n        tableFileSizeHistogram);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/metrics/TransactionReportImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.metrics.SnapshotReport;\nimport io.delta.kernel.metrics.TransactionMetricsResult;\nimport io.delta.kernel.metrics.TransactionReport;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\n\n/** A basic POJO implementation of {@link TransactionReport} for creating them */\npublic class TransactionReportImpl extends DeltaOperationReportImpl implements TransactionReport {\n\n  private final String operation;\n  private final String engineInfo;\n  private final long snapshotVersion;\n  private final Optional<UUID> snapshotReportUUID;\n  private final Optional<Long> committedVersion;\n  private final List<Column> clusteringColumns;\n  private final TransactionMetricsResult transactionMetrics;\n\n  /**\n   * @param tablePath the path of the table for the transaction\n   * @param operation the operation provided by the connector when the transaction was created\n   * @param engineInfo the engineInfo provided by the connector when the transaction was created\n   * @param committedVersion the version committed to the table. Empty for a failed transaction.\n   * @param clusteringColumns the clustering columns for the table, if any. Empty if not set.\n   * @param transactionMetrics the metrics for the transaction\n   * @param snapshotReport the SnapshotReport for the base snapshot of this transaction. Note, in\n   *     the case of a new table (when version = -1), this SnapshotReport is just a placeholder and\n   *     was never emitted to the engine's metrics reporters.\n   * @param exception the exception thrown. Empty for a successful transaction.\n   */\n  public TransactionReportImpl(\n      String tablePath,\n      String operation,\n      String engineInfo,\n      Optional<Long> committedVersion,\n      Optional<List<Column>> clusteringColumns,\n      TransactionMetrics transactionMetrics,\n      Optional<SnapshotReport> snapshotReport,\n      Optional<Exception> exception) {\n    super(tablePath, exception);\n    this.operation = requireNonNull(operation);\n    this.engineInfo = requireNonNull(engineInfo);\n    this.transactionMetrics = requireNonNull(transactionMetrics).captureTransactionMetricsResult();\n    this.committedVersion = committedVersion;\n    this.clusteringColumns = requireNonNull(clusteringColumns).orElse(Collections.emptyList());\n    requireNonNull(snapshotReport);\n    if (snapshotReport.isPresent()) {\n      checkArgument(\n          !snapshotReport.get().getException().isPresent(),\n          \"Expected a successful SnapshotReport provided report has exception\");\n      checkArgument(\n          snapshotReport.get().getVersion().isPresent(),\n          \"Expected a successful SnapshotReport but missing version\");\n      this.snapshotVersion = snapshotReport.get().getVersion().get();\n      this.snapshotReportUUID = Optional.of(snapshotReport.get().getReportUUID());\n    } else {\n      this.snapshotVersion = -1;\n      this.snapshotReportUUID = Optional.empty();\n    }\n  }\n\n  @Override\n  public String getOperation() {\n    return operation;\n  }\n\n  @Override\n  public String getEngineInfo() {\n    return engineInfo;\n  }\n\n  @Override\n  public long getBaseSnapshotVersion() {\n    return snapshotVersion;\n  }\n\n  @Override\n  public List<Column> getClusteringColumns() {\n    return clusteringColumns;\n  }\n\n  @Override\n  public Optional<UUID> getSnapshotReportUUID() {\n    return snapshotReportUUID;\n  }\n\n  @Override\n  public Optional<Long> getCommittedVersion() {\n    return committedVersion;\n  }\n\n  @Override\n  public TransactionMetricsResult getTransactionMetrics() {\n    return transactionMetrics;\n  }\n\n  @Override\n  public String toJson() throws JsonProcessingException {\n    return MetricsReportSerializer.OBJECT_MAPPER.writeValueAsString(this);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/ActionWrapper.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport java.util.Optional;\n\n/** Internal wrapper class holding information needed to perform log replay. */\npublic class ActionWrapper {\n  private final ColumnarBatch columnarBatch;\n  private final boolean isFromCheckpoint;\n  private final long version;\n  private final String filePath;\n  /* Timestamp of the commit file if isFromCheckpoint=false */\n  private final Optional<Long> timestamp;\n\n  ActionWrapper(\n      ColumnarBatch data,\n      boolean isFromCheckpoint,\n      long version,\n      Optional<Long> timestamp,\n      String filePath) {\n    this.columnarBatch = data;\n    this.isFromCheckpoint = isFromCheckpoint;\n    this.version = version;\n    this.timestamp = timestamp;\n    this.filePath = filePath;\n  }\n\n  public ColumnarBatch getColumnarBatch() {\n    return columnarBatch;\n  }\n\n  public boolean isFromCheckpoint() {\n    return isFromCheckpoint;\n  }\n\n  public long getVersion() {\n    return version;\n  }\n\n  public Optional<Long> getTimestamp() {\n    return timestamp;\n  }\n\n  public String getFilePath() {\n    return filePath;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/ActionsIterator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.replay.DeltaLogFile.LogType.*;\nimport static io.delta.kernel.internal.util.FileNames.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Utils.singletonCloseableIterator;\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.engine.FileReadResult;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.checkpoints.SidecarFile;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.*;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * This class takes as input a list of delta files (.json, .checkpoint.parquet) and produces an\n * iterator of (ColumnarBatch, isFromCheckpoint) tuples, where the schema of the ColumnarBatch\n * semantically represents actions (or, a subset of action fields) parsed from the Delta Log.\n *\n * <p>Users must pass in a `deltaReadSchema` to select which actions and sub-fields they want to\n * consume.\n *\n * <p>Users can also pass in an optional `checkpointReadSchema` if it is different from\n * `deltaReadSchema`.\n */\npublic class ActionsIterator implements CloseableIterator<ActionWrapper> {\n  private static final Logger logger = LoggerFactory.getLogger(ActionsIterator.class);\n  private final Engine engine;\n\n  private final Optional<Predicate> checkpointPredicate;\n\n  /**\n   * Linked list of iterator files (commit files and/or checkpoint files) {@link LinkedList} to\n   * allow removing the head of the list and also to peek at the head of the list. The {@link\n   * Iterator} doesn't provide a way to peek.\n   *\n   * <p>Each of these files return an iterator of {@link ColumnarBatch} containing the actions\n   */\n  private final LinkedList<DeltaLogFile> filesList;\n\n  /** Schema used for reading delta files. */\n  private final StructType deltaReadSchema;\n\n  /**\n   * Schema to be used for reading checkpoint files. Checkpoint files can be Parquet or JSON in the\n   * case of v2 checkpoints.\n   */\n  private final StructType checkpointReadSchema;\n\n  private final boolean schemaContainsAddOrRemoveFiles;\n\n  /**\n   * The current (ColumnarBatch, isFromCheckpoint) tuple. Whenever this iterator is exhausted, we\n   * will try and fetch the next one from the `filesList`.\n   *\n   * <p>If it is ever empty, that means there are no more batches to produce.\n   */\n  private Optional<CloseableIterator<ActionWrapper>> actionsIter;\n\n  private final Optional<PaginationContext> paginationContextOpt;\n  private long numCheckpointFilesSkipped = 0;\n  private long numSidecarFilesSkipped = 0;\n  private boolean closed;\n\n  public ActionsIterator(\n      Engine engine,\n      List<FileStatus> files,\n      StructType deltaReadSchema,\n      Optional<Predicate> checkpointPredicate) {\n    this(\n        engine,\n        files,\n        deltaReadSchema,\n        deltaReadSchema,\n        checkpointPredicate,\n        Optional.empty() /* paginationContextOpt */);\n  }\n\n  public ActionsIterator(\n      Engine engine,\n      List<FileStatus> files,\n      StructType deltaReadSchema,\n      StructType checkpointReadSchema,\n      Optional<Predicate> checkpointPredicate,\n      Optional<PaginationContext> paginationContextOpt) {\n    this.engine = engine;\n    this.checkpointPredicate = checkpointPredicate;\n    this.filesList = new LinkedList<>();\n    this.paginationContextOpt = paginationContextOpt;\n    this.filesList.addAll(\n        files.stream()\n            .map(DeltaLogFile::forFileStatus)\n            .filter(this::paginatedFilter)\n            .collect(Collectors.toList()));\n    this.deltaReadSchema = deltaReadSchema;\n    this.checkpointReadSchema = checkpointReadSchema;\n    this.actionsIter = Optional.empty();\n    this.schemaContainsAddOrRemoveFiles = LogReplay.containsAddOrRemoveFileActions(deltaReadSchema);\n  }\n\n  /**\n   * Filters a log segment file based on the pagination context.\n   *\n   * <p>If this method returns {@code true}, the current file will be kept; otherwise, it will be\n   * skipped.\n   *\n   * <ul>\n   *   <li>If pagination is not enabled (i.e., {@code paginationContextOpt} is not present), return\n   *       {@code true}.\n   *   <li>If the pagination context is present but doesn't include a last read log file path,\n   *       return {@code true} (indicates reading the first page).\n   *   <li>If the file is a JSON log file, return {@code true} — we never skip JSON files as they're\n   *       needed to build hash sets.\n   *   <li>If the file is a V2 checkpoint manifest, return {@code true} — these should never be\n   *       skipped.\n   *   <li>If the file is a checkpoint file and comes after the last log file recorded in the page\n   *       token, return {@code false} (skip it).\n   * </ul>\n   *\n   * <p><b>Note:</b> The {@code nextLogFile} parameter cannot be a sidecar file because sidecar\n   * files are not included in the log segment list. Sidecar files are handled separately later,\n   * after the V2 manifest file has been read, specifically in the {@code extractSidecarFiles()}\n   * method.\n   *\n   * @param nextLogFile the log file to evaluate\n   * @return {@code true} to include the file; {@code false} to skip it\n   */\n  @VisibleForTesting\n  // TODO: verify numCheckpointFilesSkipped is correct in E2E test\n  // TODO: add unit test for this method\n  public boolean paginatedFilter(DeltaLogFile nextLogFile) {\n    Objects.requireNonNull(paginationContextOpt);\n    // Pagination isn't enabled.\n    if (!paginationContextOpt.isPresent()) return true;\n    String nextFilePath = nextLogFile.getFile().getPath();\n    Optional<String> lastReadLogFilePathOpt = paginationContextOpt.get().getLastReadLogFilePath();\n    // Reading the first page\n    if (!lastReadLogFilePathOpt.isPresent()) {\n      logger.info(\"Pagination: no page token present, reading the first page\");\n      return true;\n    }\n    logger.info(\"Pagination: lastReadLogFilePath in token is {}\", lastReadLogFilePathOpt.get());\n    logger.info(\"Pagination: nextFilePath is {}\", nextFilePath);\n    switch (nextLogFile.getLogType()) {\n      case COMMIT:\n      case LOG_COMPACTION:\n      case CHECKPOINT_CLASSIC:\n      case V2_CHECKPOINT_MANIFEST:\n        return true;\n      case MULTIPART_CHECKPOINT:\n        if (isFullyConsumedFile(nextFilePath, lastReadLogFilePathOpt.get())) {\n          logger.info(\"Pagination: skip reading multi-part checkpoint file {}\", nextFilePath);\n          numCheckpointFilesSkipped++;\n          return false;\n        } else {\n          return true;\n        }\n      case SIDECAR:\n        throw new IllegalArgumentException(\n            \"Sidecar file shouldn't exist in log segment! Path: \" + nextFilePath);\n      default:\n        throw new IllegalArgumentException(\"Unknown log file type!\");\n    }\n  }\n\n  /**\n   * Returns true if the given file was already fully consumed in a previous page that ends at\n   * lastReadLogFilePath.\n   */\n  private boolean isFullyConsumedFile(String filePath, String lastReadLogFilePath) {\n    // Files are sorted in reverse lexicographic order.so if `filePath` is *greater* than\n    // `lastReadLogFilePath`,\n    // it actually comes before lastReadLogFilePath in the log stream, meaning we have already\n    // paginated past it.\n    return filePath.compareTo(lastReadLogFilePath) > 0;\n  }\n\n  @Override\n  public boolean hasNext() {\n    if (closed) {\n      throw new IllegalStateException(\"Can't call `hasNext` on a closed iterator.\");\n    }\n\n    tryEnsureNextActionsIterIsReady();\n\n    // By definition of tryEnsureNextActionsIterIsReady, we know that if actionsIter\n    // is non-empty then it has a next element\n\n    return actionsIter.isPresent();\n  }\n\n  /**\n   * @return a tuple of (ColumnarBatch, isFromCheckpoint), where ColumnarBatch conforms to the\n   *     instance {@link #deltaReadSchema} or {@link #checkpointReadSchema} (the latter when when\n   *     isFromCheckpoint=true).\n   */\n  @Override\n  public ActionWrapper next() {\n    if (closed) {\n      throw new IllegalStateException(\"Can't call `next` on a closed iterator.\");\n    }\n    if (Thread.currentThread().isInterrupted()) {\n      throw new IllegalStateException(\"Thread was interrupted\");\n    }\n\n    if (!hasNext()) {\n      throw new NoSuchElementException(\"No next element\");\n    }\n\n    return actionsIter.get().next();\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (!closed && actionsIter.isPresent()) {\n      actionsIter.get().close();\n      actionsIter = Optional.empty();\n      closed = true;\n    }\n  }\n\n  /**\n   * If the current `actionsIter` has no more elements, this function finds the next non-empty file\n   * in `filesList` and uses it to set `actionsIter`.\n   */\n  private void tryEnsureNextActionsIterIsReady() {\n    if (actionsIter.isPresent()) {\n      // This iterator already has a next element, so we can exit early;\n      if (actionsIter.get().hasNext()) {\n        return;\n      }\n\n      // Clean up resources\n      Utils.closeCloseables(actionsIter.get());\n\n      // Set this to empty since we don't know if there's a next file yet\n      actionsIter = Optional.empty();\n    }\n\n    // Search for the next non-empty file and use that iter\n    while (!filesList.isEmpty()) {\n      actionsIter = Optional.of(getNextActionsIter());\n\n      if (actionsIter.get().hasNext()) {\n        // It is ready, we are done\n        return;\n      }\n\n      // It was an empty file. Clean up resources\n      Utils.closeCloseables(actionsIter.get());\n\n      // Set this to empty since we don't know if there's a next file yet\n      actionsIter = Optional.empty();\n    }\n  }\n\n  /**\n   * Get an iterator of actions from the v2 checkpoint file that may contain sidecar files. If the\n   * current read schema includes Add/Remove files, then inject the sidecar column into this schema\n   * to read the sidecar files from the top-level v2 checkpoint file. When the returned\n   * ColumnarBatches are processed, these sidecars will be appended to the end of the file list and\n   * read as part of a subsequent batch (avoiding reading the top-level v2 checkpoint files more\n   * than once).\n   */\n  private CloseableIterator<ColumnarBatch> getActionsIterFromSinglePartOrV2Checkpoint(\n      FileStatus file, String fileName) throws IOException {\n    // If the sidecars may contain the current action, read sidecars from the top-level v2\n    // checkpoint file(to be read later).\n    StructType modifiedReadSchema = checkpointReadSchema;\n    if (schemaContainsAddOrRemoveFiles) {\n      modifiedReadSchema = LogReplay.withSidecarFileSchema(checkpointReadSchema);\n    }\n\n    long checkpointVersion = checkpointVersion(file.getPath());\n\n    // If the read schema contains Add/Remove files, we should always read the sidecar file\n    // actions from the checkpoint manifest regardless of the checkpoint predicate.\n    Optional<Predicate> checkpointPredicateIncludingSidecars;\n    if (schemaContainsAddOrRemoveFiles) {\n      Predicate containsSidecarPredicate =\n          new Predicate(\"IS_NOT_NULL\", new Column(LogReplay.SIDECAR_FIELD_NAME));\n      checkpointPredicateIncludingSidecars =\n          checkpointPredicate.map(p -> new Or(p, containsSidecarPredicate));\n    } else {\n      checkpointPredicateIncludingSidecars = checkpointPredicate;\n    }\n    final CloseableIterator<ColumnarBatch> topLevelIter;\n    StructType finalReadSchema = modifiedReadSchema;\n\n    if (fileName.endsWith(\".parquet\")) {\n      topLevelIter =\n          wrapEngineExceptionThrowsIO(\n              () ->\n                  engine\n                      .getParquetHandler()\n                      .readParquetFiles(\n                          singletonCloseableIterator(file),\n                          finalReadSchema,\n                          checkpointPredicateIncludingSidecars)\n                      .map(FileReadResult::getData),\n              \"Reading parquet log file `%s` with readSchema=%s and predicate=%s\",\n              file,\n              finalReadSchema,\n              checkpointPredicateIncludingSidecars);\n    } else if (fileName.endsWith(\".json\")) {\n      topLevelIter =\n          wrapEngineExceptionThrowsIO(\n              () ->\n                  engine\n                      .getJsonHandler()\n                      .readJsonFiles(\n                          singletonCloseableIterator(file),\n                          finalReadSchema,\n                          checkpointPredicateIncludingSidecars),\n              \"Reading JSON log file `%s` with readSchema=%s and predicate=%s\",\n              file,\n              finalReadSchema,\n              checkpointPredicateIncludingSidecars);\n    } else {\n      throw new IOException(\"Unrecognized top level v2 checkpoint file format: \" + fileName);\n    }\n    return new CloseableIterator<ColumnarBatch>() {\n      @Override\n      public void close() throws IOException {\n        topLevelIter.close();\n      }\n\n      @Override\n      public boolean hasNext() {\n        return topLevelIter.hasNext();\n      }\n\n      @Override\n      public ColumnarBatch next() {\n        ColumnarBatch batch = topLevelIter.next();\n        if (schemaContainsAddOrRemoveFiles) {\n          return extractSidecarsFromBatch(file, checkpointVersion, batch);\n        }\n        return batch;\n      }\n    };\n  }\n\n  /**\n   * Reads SidecarFile actions from ColumnarBatch, removing sidecar actions from the ColumnarBatch.\n   * Returns a list of SidecarFile actions found.\n   */\n  public ColumnarBatch extractSidecarsFromBatch(\n      FileStatus checkpointFileStatus, long checkpointVersion, ColumnarBatch columnarBatch) {\n    checkArgument(columnarBatch.getSchema().fieldNames().contains(LogReplay.SIDECAR_FIELD_NAME));\n\n    Path deltaLogPath = new Path(checkpointFileStatus.getPath()).getParent();\n\n    // Sidecars will exist in schema. Extract sidecar files, then remove sidecar files from\n    // batch output.\n    List<DeltaLogFile> outputFiles = new ArrayList<>();\n    int sidecarFieldIndexInSchema =\n        columnarBatch.getSchema().fieldNames().indexOf(LogReplay.SIDECAR_FIELD_NAME);\n    ColumnVector sidecarVector = columnarBatch.getColumnVector(sidecarFieldIndexInSchema);\n    int sidecarIndexInV2Manifest = -1; // sidecar file index start from 0 in v2 manifest checkpoint\n    for (int i = 0; i < columnarBatch.getSize(); i++) {\n      SidecarFile sidecarFile = SidecarFile.fromColumnVector(sidecarVector, i);\n      if (sidecarFile == null) {\n        continue;\n      }\n      sidecarIndexInV2Manifest++;\n      if (paginationContextOpt.isPresent()) {\n        // Different from regular log files, name of sidecars are not in order, so we need to use\n        // sidecar index in V2 manifest to compare\n        if (paginationContextOpt.get().getLastReadSidecarFileIdx().isPresent()\n            && sidecarIndexInV2Manifest\n                < paginationContextOpt.get().getLastReadSidecarFileIdx().get()) {\n          logger.info(\n              \"Pagination: skip reading sidecar file: index={}, path={}\",\n              sidecarIndexInV2Manifest,\n              sidecarFile.getPath());\n          numSidecarFilesSkipped++;\n          continue;\n        }\n      }\n\n      FileStatus sideCarFileStatus =\n          FileStatus.of(\n              FileNames.sidecarFile(deltaLogPath, sidecarFile.getPath()),\n              sidecarFile.getSizeInBytes(),\n              sidecarFile.getModificationTime());\n\n      filesList.add(DeltaLogFile.ofSideCar(sideCarFileStatus, checkpointVersion));\n    }\n\n    if (paginationContextOpt.isPresent()) {\n      logger.info(\"Pagination: number of sidecar files skipped is {}\", numSidecarFilesSkipped);\n    }\n\n    // Delete SidecarFile actions from the schema.\n    return columnarBatch.withDeletedColumnAt(sidecarFieldIndexInSchema);\n  }\n\n  /**\n   * Get the next file from `filesList` (.json or .checkpoint.parquet) read it + inject the\n   * `isFromCheckpoint` information.\n   *\n   * <p>Requires that `filesList.isEmpty` is false.\n   */\n  private CloseableIterator<ActionWrapper> getNextActionsIter() {\n    final DeltaLogFile nextLogFile = filesList.pop();\n    final FileStatus nextFile = nextLogFile.getFile();\n    final Path nextFilePath = new Path(nextFile.getPath());\n    final String fileName = nextFilePath.getName();\n    try {\n      switch (nextLogFile.getLogType()) {\n        case COMMIT:\n          {\n            final long fileVersion = FileNames.deltaVersion(nextFilePath);\n            return readCommitOrCompactionFile(fileVersion, nextFile);\n          }\n        case LOG_COMPACTION:\n          {\n            // use end version as this is like a mini checkpoint, and that's what checkpoints do\n            final long fileVersion = FileNames.logCompactionVersions(nextFilePath)._2;\n            return readCommitOrCompactionFile(fileVersion, nextFile);\n          }\n        case CHECKPOINT_CLASSIC:\n        case V2_CHECKPOINT_MANIFEST:\n          {\n            // If the checkpoint file is a UUID or classic checkpoint, read the top-level\n            // checkpoint file and any potential sidecars. Otherwise, look for any other\n            // parts of the current multipart checkpoint.\n            CloseableIterator<FileReadResult> dataIter =\n                getActionsIterFromSinglePartOrV2Checkpoint(nextFile, fileName)\n                    .map(batch -> new FileReadResult(batch, nextFile.getPath()));\n            long version = checkpointVersion(nextFilePath);\n            return combine(dataIter, true /* isFromCheckpoint */, version, Optional.empty());\n          }\n        case MULTIPART_CHECKPOINT:\n        case SIDECAR:\n          {\n            // Try to retrieve the remaining checkpoint files (if there are any) and issue\n            // read request for all in one go, so that the {@link ParquetHandler} can do\n            // optimizations like reading multiple files in parallel.\n            CloseableIterator<FileStatus> checkpointFiles =\n                retrieveRemainingCheckpointFiles(nextLogFile);\n            CloseableIterator<FileReadResult> dataIter =\n                wrapEngineExceptionThrowsIO(\n                    () ->\n                        engine\n                            .getParquetHandler()\n                            .readParquetFiles(\n                                checkpointFiles, deltaReadSchema, checkpointPredicate),\n                    \"Reading checkpoint sidecars [%s] with readSchema=%s and predicate=%s\",\n                    checkpointFiles,\n                    deltaReadSchema,\n                    checkpointPredicate);\n\n            long version = nextLogFile.getVersion();\n            return combine(dataIter, true /* isFromCheckpoint */, version, Optional.empty());\n          }\n        default:\n          throw new IOException(\"Unrecognized log type: \" + nextLogFile.getLogType());\n      }\n    } catch (IOException ex) {\n      throw new UncheckedIOException(ex);\n    }\n  }\n\n  private CloseableIterator<ActionWrapper> readCommitOrCompactionFile(\n      long fileVersion, FileStatus nextFile) throws IOException {\n    // We can not read multiple JSON files in parallel (like the checkpoint files),\n    // because each one has a different version, and we need to associate the\n    // version with actions read from the JSON file for further optimizations later\n    // on (faster metadata & protocol loading in subsequent runs by remembering\n    // the version of the last version where the metadata and protocol are found).\n    CloseableIterator<FileReadResult> dataIter = null;\n    try {\n      dataIter =\n          wrapEngineExceptionThrowsIO(\n              () ->\n                  engine\n                      .getJsonHandler()\n                      .readJsonFiles(\n                          singletonCloseableIterator(nextFile), deltaReadSchema, Optional.empty())\n                      .map(batch -> new FileReadResult(batch, nextFile.getPath())),\n              \"Reading JSON log file `%s` with readSchema=%s\",\n              nextFile,\n              deltaReadSchema);\n      return combine(\n          dataIter,\n          false /* isFromCheckpoint */,\n          fileVersion,\n          Optional.of(nextFile.getModificationTime()) /* timestamp */);\n    } catch (Exception e) {\n      if (dataIter != null) {\n        Utils.closeCloseablesSilently(dataIter); // close it avoid leaking resources\n      }\n      throw e;\n    }\n  }\n\n  /**\n   * Takes an input iterator of actions read from the file and metadata about the file read, and\n   * combines it to return an Iterator<ActionWrapper>. The timestamp in the ActionWrapper is only\n   * set when the input file is not a Checkpoint. The timestamp will be set to be the\n   * inCommitTimestamp of the delta file when available, otherwise it will be the modification time\n   * of the file.\n   */\n  private CloseableIterator<ActionWrapper> combine(\n      CloseableIterator<FileReadResult> fileReadDataIter,\n      boolean isFromCheckpoint,\n      long version,\n      Optional<Long> timestamp) {\n    // For delta files, we want to use the inCommitTimestamp from commitInfo\n    // as the commit timestamp for the file.\n    // Since CommitInfo should be the first action in the delta when inCommitTimestamp is\n    // enabled, we will read the first batch and try to extract the timestamp from it.\n    // We also ensure that rewoundFileReadDataIter is identical to the original\n    // fileReadDataIter before any data was consumed.\n    final CloseableIterator<FileReadResult> rewoundFileReadDataIter;\n    Optional<Long> inCommitTimestampOpt = Optional.empty();\n    if (!isFromCheckpoint && fileReadDataIter.hasNext()) {\n      FileReadResult fileReadResult = fileReadDataIter.next();\n      rewoundFileReadDataIter =\n          singletonCloseableIterator(fileReadResult).combine(fileReadDataIter);\n      inCommitTimestampOpt =\n          InCommitTimestampUtils.tryExtractInCommitTimestamp(fileReadResult.getData());\n    } else {\n      rewoundFileReadDataIter = fileReadDataIter;\n    }\n    final Optional<Long> finalResolvedCommitTimestamp =\n        inCommitTimestampOpt.isPresent() ? inCommitTimestampOpt : timestamp;\n\n    return new CloseableIterator<ActionWrapper>() {\n      @Override\n      public boolean hasNext() {\n        return rewoundFileReadDataIter.hasNext();\n      }\n\n      @Override\n      public ActionWrapper next() {\n        FileReadResult fileReadResult = rewoundFileReadDataIter.next();\n        return new ActionWrapper(\n            fileReadResult.getData(),\n            isFromCheckpoint,\n            version,\n            finalResolvedCommitTimestamp,\n            fileReadResult.getFilePath());\n      }\n\n      @Override\n      public void close() throws IOException {\n        fileReadDataIter.close();\n      }\n    };\n  }\n\n  /**\n   * Given a checkpoint file, retrieve all the files that are part of the same checkpoint or sidecar\n   * files.\n   *\n   * <p>This is done by looking at the log file type and finding all the files that have the same\n   * version number.\n   */\n  private CloseableIterator<FileStatus> retrieveRemainingCheckpointFiles(\n      DeltaLogFile deltaLogFile) {\n\n    // Find the contiguous parquet files that are part of the same checkpoint\n    final List<FileStatus> checkpointFiles = new ArrayList<>();\n\n    // Add the already retrieved checkpoint file to the list.\n    checkpointFiles.add(deltaLogFile.getFile());\n\n    // Sidecar or multipart checkpoint types are the only files that can have multiple parts.\n    if (deltaLogFile.getLogType() == SIDECAR || deltaLogFile.getLogType() == MULTIPART_CHECKPOINT) {\n\n      DeltaLogFile peek = filesList.peek();\n      while (peek != null\n          && deltaLogFile.getLogType() == peek.getLogType()\n          && deltaLogFile.getVersion() == peek.getVersion()) {\n        checkpointFiles.add(filesList.pop().getFile());\n        peek = filesList.peek();\n      }\n    }\n\n    return toCloseableIterator(checkpointFiles.iterator());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/ActiveAddFilesIterator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineException;\nimport static io.delta.kernel.internal.replay.LogReplay.ADD_FILE_DV_ORDINAL;\nimport static io.delta.kernel.internal.replay.LogReplay.ADD_FILE_ORDINAL;\nimport static io.delta.kernel.internal.replay.LogReplay.ADD_FILE_PATH_ORDINAL;\nimport static io.delta.kernel.internal.replay.LogReplay.REMOVE_FILE_DV_ORDINAL;\nimport static io.delta.kernel.internal.replay.LogReplay.REMOVE_FILE_ORDINAL;\nimport static io.delta.kernel.internal.replay.LogReplay.REMOVE_FILE_PATH_ORDINAL;\nimport static io.delta.kernel.internal.replay.LogReplayUtils.pathToUri;\nimport static io.delta.kernel.internal.replay.LogReplayUtils.prepareSelectionVectorBuffer;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.ExpressionEvaluator;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.InternalScanFileUtils;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.metrics.ScanMetrics;\nimport io.delta.kernel.internal.replay.LogReplayUtils.UniqueFileActionTuple;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * This class takes an iterator of ({@link ColumnarBatch}, isFromCheckpoint), where the columnar\n * data inside the columnar batch represents has top level columns \"add\" and \"remove\", and produces\n * an iterator of {@link FilteredColumnarBatch} with only the \"add\" column and with a selection\n * vector indicating which AddFiles are still active in the table (have not been tombstoned).\n */\npublic class ActiveAddFilesIterator implements CloseableIterator<FilteredColumnarBatch> {\n  private static final Logger logger = LoggerFactory.getLogger(ActiveAddFilesIterator.class);\n\n  private final Engine engine;\n  private final Path tableRoot;\n\n  private final CloseableIterator<ActionWrapper> iter;\n\n  private final Set<UniqueFileActionTuple> tombstonesFromJson;\n  private final Set<UniqueFileActionTuple> addFilesFromJson;\n\n  private Optional<FilteredColumnarBatch> next;\n  /**\n   * This buffer is reused across batches to keep the memory allocations minimal. It is resized as\n   * required and the array entries are reset between batches.\n   */\n  private boolean[] selectionVectorBuffer;\n\n  private ExpressionEvaluator tableRootVectorGenerator;\n  private boolean closed;\n\n  /**\n   * Metrics capturing log replay for scan building. These counters are updated as the iterator is\n   * consumed and reported to the {@link Engine#getMetricsReporters()} when the scan is complete.\n   */\n  private ScanMetrics metrics;\n\n  ActiveAddFilesIterator(\n      Engine engine, CloseableIterator<ActionWrapper> iter, Path tableRoot, ScanMetrics metrics) {\n    this.engine = engine;\n    this.tableRoot = tableRoot;\n    this.iter = iter;\n    this.tombstonesFromJson = new HashSet<>();\n    this.addFilesFromJson = new HashSet<>();\n    this.next = Optional.empty();\n    this.metrics = metrics;\n  }\n\n  @Override\n  public boolean hasNext() {\n    if (closed) {\n      throw new IllegalStateException(\"Can't call `hasNext` on a closed iterator.\");\n    }\n    if (!next.isPresent()) {\n      prepareNext();\n    }\n    return next.isPresent();\n  }\n\n  @Override\n  public FilteredColumnarBatch next() {\n    if (closed) {\n      throw new IllegalStateException(\"Can't call `next` on a closed iterator.\");\n    }\n    if (!hasNext()) {\n      throw new NoSuchElementException();\n    }\n\n    // By the definition of `hasNext`, we know that `next` is non-empty\n\n    final FilteredColumnarBatch ret = next.get();\n    next = Optional.empty();\n    return ret;\n  }\n\n  @Override\n  public void close() throws IOException {\n    closed = true;\n    Utils.closeCloseables(iter);\n\n    // Log the metrics of the log replay of actions that are consumed so far. If the iterator\n    // is closed before consuming all the actions, the metrics will be partial.\n    logger.info(\"Active add file finding log replay metrics: {}\", metrics);\n  }\n\n  /**\n   * Grabs the next FileDataReadResult from `iter` and updates the value of `next`.\n   *\n   * <p>Internally, implements the following algorithm: 1. read all the RemoveFiles in the next\n   * ColumnarBatch to update the `tombstonesFromJson` set 2. read all the AddFiles in that same\n   * ColumnarBatch, unselecting ones that have already been removed or returned by updating a\n   * selection vector 3. produces a DataReadResult by dropping that RemoveFile column from the\n   * ColumnarBatch and using that selection vector\n   *\n   * <p>Note that, according to the Delta protocol, \"a valid [Delta] version is restricted to\n   * contain at most one file action of the same type (i.e. add/remove) for any one combination of\n   * path and dvId\". This means that step 2 could actually come before 1 - there's no temporal\n   * dependency between them.\n   *\n   * <p>Ensures that - `next` is non-empty if there is a next result - `next` is empty if there is\n   * no next result\n   */\n  private void prepareNext() {\n    if (next.isPresent()) {\n      return; // already have a next result\n    }\n    if (!iter.hasNext()) {\n      return; // no next result, and no batches to read\n    }\n\n    final ActionWrapper _next = iter.next();\n    final ColumnarBatch addRemoveColumnarBatch = _next.getColumnarBatch();\n    final boolean isFromCheckpoint = _next.isFromCheckpoint();\n\n    // Step 1: Update `tombstonesFromJson` with all the RemoveFiles in this columnar batch, if\n    //         and only if this batch is not from a checkpoint.\n    //\n    //         There's no reason to put a RemoveFile from a checkpoint into `tombstonesFromJson`\n    //         since, when we generate a checkpoint, any corresponding AddFile would have\n    //         been excluded already\n    if (!isFromCheckpoint) {\n      final ColumnVector removesVector =\n          addRemoveColumnarBatch.getColumnVector(REMOVE_FILE_ORDINAL);\n      for (int rowId = 0; rowId < removesVector.getSize(); rowId++) {\n        if (removesVector.isNullAt(rowId)) {\n          continue;\n        }\n\n        // Note: this row doesn't represent the complete RemoveFile schema. It only contains\n        //       the fields we need for this replay.\n        final String path = getRemoveFilePath(removesVector, rowId);\n        final URI pathAsUri = pathToUri(path);\n        final Optional<String> dvId =\n            Optional.ofNullable(getRemoveFileDV(removesVector, rowId))\n                .map(DeletionVectorDescriptor::getUniqueId);\n        final UniqueFileActionTuple key = new UniqueFileActionTuple(pathAsUri, dvId);\n        tombstonesFromJson.add(key);\n        metrics.removeFilesFromDeltaFilesCounter.increment();\n      }\n    }\n\n    // Step 2: Iterate over all the AddFiles in this columnar batch in order to build up the\n    //         selection vector. We unselect an AddFile when it was removed by a RemoveFile\n    final ColumnVector addsVector = addRemoveColumnarBatch.getColumnVector(ADD_FILE_ORDINAL);\n    selectionVectorBuffer =\n        prepareSelectionVectorBuffer(selectionVectorBuffer, addsVector.getSize());\n    boolean atLeastOneUnselected = false;\n    int numSelectedRows = 0;\n\n    for (int rowId = 0; rowId < addsVector.getSize(); rowId++) {\n      if (addsVector.isNullAt(rowId)) {\n        atLeastOneUnselected = true;\n        continue; // selectionVector will be `false` at rowId by default\n      }\n\n      metrics.addFilesCounter.increment();\n      if (!isFromCheckpoint) {\n        metrics.addFilesFromDeltaFilesCounter.increment();\n      }\n\n      final String path = getAddFilePath(addsVector, rowId);\n      final URI pathAsUri = pathToUri(path);\n      final Optional<String> dvId =\n          Optional.ofNullable(getAddFileDV(addsVector, rowId))\n              .map(DeletionVectorDescriptor::getUniqueId);\n      final UniqueFileActionTuple key = new UniqueFileActionTuple(pathAsUri, dvId);\n      final boolean alreadyDeleted = tombstonesFromJson.contains(key);\n      final boolean alreadyReturned = addFilesFromJson.contains(key);\n\n      boolean doSelect = false;\n\n      if (!alreadyReturned) {\n        // Note: No AddFile will appear twice in a checkpoint, so we only need\n        //       non-checkpoint AddFiles in the set. When stats are recomputed the same\n        //       AddFile is added with stats without remove it first.\n        if (!isFromCheckpoint) {\n          addFilesFromJson.add(key);\n        }\n\n        if (!alreadyDeleted) {\n          doSelect = true;\n          selectionVectorBuffer[rowId] = true;\n          numSelectedRows++;\n          metrics.activeAddFilesCounter.increment();\n        }\n      } else {\n        metrics.duplicateAddFilesCounter.increment();\n      }\n\n      if (!doSelect) {\n        atLeastOneUnselected = true;\n      }\n    }\n\n    ColumnarBatch scanAddFiles = addRemoveColumnarBatch;\n    // Step 3: Drop the RemoveFile column and use the selection vector to build a new\n    //         FilteredColumnarBatch\n    // For checkpoint files, we would only have read the adds, not the removes.\n    if (!isFromCheckpoint) {\n      scanAddFiles = scanAddFiles.withDeletedColumnAt(1);\n    }\n\n    // Step 4: TODO: remove this step. This is a temporary requirement until the path\n    //         in `add` is converted to absolute path.\n    final ColumnarBatch finalScanAddFiles = scanAddFiles;\n    if (tableRootVectorGenerator == null) {\n      tableRootVectorGenerator =\n          wrapEngineException(\n              () ->\n                  engine\n                      .getExpressionHandler()\n                      .getEvaluator(\n                          finalScanAddFiles.getSchema(),\n                          Literal.ofString(tableRoot.toUri().toString()),\n                          StringType.STRING),\n              \"Get the expression evaluator for the table root\");\n    }\n    ColumnVector tableRootVector =\n        wrapEngineException(\n            () -> tableRootVectorGenerator.eval(finalScanAddFiles),\n            \"Evaluating the table root expression\");\n    scanAddFiles =\n        scanAddFiles.withNewColumn(\n            1, InternalScanFileUtils.TABLE_ROOT_STRUCT_FIELD, tableRootVector);\n\n    Optional<ColumnVector> selectionColumnVector = Optional.empty();\n    if (atLeastOneUnselected) {\n      selectionColumnVector =\n          Optional.of(\n              wrapEngineException(\n                  () ->\n                      engine\n                          .getExpressionHandler()\n                          .createSelectionVector(selectionVectorBuffer, 0, addsVector.getSize()),\n                  \"Create selection vector for selected scan files\"));\n    }\n    // TODO: skip batch if all AddFiles are unselected; issue #4941\n    next =\n        Optional.of(\n            new FilteredColumnarBatch(\n                scanAddFiles, selectionColumnVector, _next.getFilePath(), numSelectedRows));\n  }\n\n  public static String getAddFilePath(ColumnVector addFileVector, int rowId) {\n    return addFileVector.getChild(ADD_FILE_PATH_ORDINAL).getString(rowId);\n  }\n\n  public static DeletionVectorDescriptor getAddFileDV(ColumnVector addFileVector, int rowId) {\n    return DeletionVectorDescriptor.fromColumnVector(\n        addFileVector.getChild(ADD_FILE_DV_ORDINAL), rowId);\n  }\n\n  public static String getRemoveFilePath(ColumnVector removeFileVector, int rowId) {\n    return removeFileVector.getChild(REMOVE_FILE_PATH_ORDINAL).getString(rowId);\n  }\n\n  public static DeletionVectorDescriptor getRemoveFileDV(ColumnVector removeFileVector, int rowId) {\n    return DeletionVectorDescriptor.fromColumnVector(\n        removeFileVector.getChild(REMOVE_FILE_DV_ORDINAL), rowId);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/ConflictChecker.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.DeltaErrors.concurrentDomainMetadataAction;\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED;\nimport static io.delta.kernel.internal.actions.SingleAction.*;\nimport static io.delta.kernel.internal.util.FileNames.checksumFile;\nimport static io.delta.kernel.internal.util.FileNames.deltaFile;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\nimport static java.lang.String.format;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.ConcurrentWriteException;\nimport io.delta.kernel.internal.*;\nimport io.delta.kernel.internal.actions.CommitInfo;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.SetTransaction;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.checksum.ChecksumReader;\nimport io.delta.kernel.internal.rowtracking.RowTracking;\nimport io.delta.kernel.internal.rowtracking.RowTrackingMetadataDomain;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.DomainMetadataUtils;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/**\n * Class containing the conflict resolution logic when writing to a Delta table.\n *\n * <p>Currently, the support is to allow blind appends. Later on this can be extended to add support\n * for read-after-write scenarios.\n */\npublic class ConflictChecker {\n  private static final int PROTOCOL_ORDINAL = CONFLICT_RESOLUTION_SCHEMA.indexOf(\"protocol\");\n  private static final int METADATA_ORDINAL = CONFLICT_RESOLUTION_SCHEMA.indexOf(\"metaData\");\n  private static final int TXN_ORDINAL = CONFLICT_RESOLUTION_SCHEMA.indexOf(\"txn\");\n  private static final int COMMITINFO_ORDINAL = CONFLICT_RESOLUTION_SCHEMA.indexOf(\"commitInfo\");\n  private static final int DOMAIN_METADATA_ORDINAL =\n      CONFLICT_RESOLUTION_SCHEMA.indexOf(\"domainMetadata\");\n\n  // Snapshot of the table read by the transaction that encountered the conflict\n  // (a.k.a the losing transaction)\n  private final Optional<SnapshotImpl> snapshotOpt;\n\n  // Losing transaction\n  private final TransactionImpl transaction;\n  private final long attemptVersion;\n  private final CloseableIterable<Row> attemptDataActions;\n  private final List<DomainMetadata> attemptDomainMetadatas;\n\n  // Helper states during conflict resolution\n  private Optional<Long> lastWinningRowIdHighWatermark = Optional.empty();\n\n  private ConflictChecker(\n      Optional<SnapshotImpl> snapshotOpt,\n      TransactionImpl transaction,\n      long attemptVersion,\n      List<DomainMetadata> domainMetadatas,\n      CloseableIterable<Row> dataActions) {\n    this.snapshotOpt = snapshotOpt;\n    this.transaction = transaction;\n    this.attemptVersion = attemptVersion;\n    this.attemptDomainMetadatas = domainMetadatas;\n    this.attemptDataActions = dataActions;\n  }\n\n  /**\n   * Resolve conflicts between the losing transaction and the winning transactions and return a\n   * rebase state that the losing transaction needs to rebase against before attempting the commit.\n   *\n   * @param engine {@link Engine} instance to use\n   * @param snapshot {@link SnapshotImpl} of the table when the losing transaction has started\n   * @param transaction {@link TransactionImpl} that encountered the conflict (a.k.a the losing\n   *     transaction)\n   * @param domainMetadatas List of {@link DomainMetadata} that the losing transaction is trying to\n   *     commit\n   * @param dataActions {@link CloseableIterable} of data actions that the losing transaction is\n   *     trying to commit\n   * @return {@link TransactionRebaseState} that the losing transaction needs to rebase against\n   * @throws ConcurrentWriteException if there are logical conflicts between the losing transaction\n   *     and the winning transactions that cannot be resolved.\n   */\n  public static TransactionRebaseState resolveConflicts(\n      Engine engine,\n      Optional<SnapshotImpl> snapshot,\n      long attemptVersion,\n      TransactionImpl transaction,\n      List<DomainMetadata> domainMetadatas,\n      CloseableIterable<Row> dataActions)\n      throws ConcurrentWriteException {\n    // We currently set isBlindAppend=false in our CommitInfo to avoid unsafe resolution by other\n    // connectors. Here, we still can assume that conflict resolution is safe to perform in Kernel.\n    // checkArgument(transaction.isBlindAppend(), \"Current support is for blind appends only.\");\n    return new ConflictChecker(snapshot, transaction, attemptVersion, domainMetadatas, dataActions)\n        .resolveConflicts(engine);\n  }\n\n  public TransactionRebaseState resolveConflicts(Engine engine) throws ConcurrentWriteException {\n    List<FileStatus> winningCommits = getWinningCommitFiles(engine);\n    AtomicReference<Optional<CommitInfo>> winningCommitInfoOpt =\n        new AtomicReference<>(Optional.empty());\n\n    // no winning commits. why did we get the transaction conflict?\n    checkState(!winningCommits.isEmpty(), \"No winning commits found.\");\n\n    FileStatus lastWinningTxn = winningCommits.get(winningCommits.size() - 1);\n    long lastWinningVersion = FileNames.deltaVersion(lastWinningTxn.getPath());\n    // Read the actions from the winning commits\n    try (ActionsIterator actionsIterator =\n        new ActionsIterator(engine, winningCommits, CONFLICT_RESOLUTION_SCHEMA, Optional.empty())) {\n\n      actionsIterator.forEachRemaining(\n          actionBatch -> {\n            checkArgument(!actionBatch.isFromCheckpoint()); // no checkpoints should be read\n            ColumnarBatch batch = actionBatch.getColumnarBatch();\n            if (actionBatch.getVersion() == lastWinningVersion) {\n              Optional<CommitInfo> commitInfo =\n                  getCommitInfo(batch.getColumnVector(COMMITINFO_ORDINAL));\n              winningCommitInfoOpt.set(commitInfo);\n            }\n\n            handleProtocol(batch.getColumnVector(PROTOCOL_ORDINAL));\n            handleMetadata(batch.getColumnVector(METADATA_ORDINAL));\n            handleTxn(batch.getColumnVector(TXN_ORDINAL));\n            handleDomainMetadata(batch.getColumnVector(DOMAIN_METADATA_ORDINAL));\n          });\n    } catch (IOException ioe) {\n      throw new UncheckedIOException(\"Error reading actions from winning commits.\", ioe);\n    }\n\n    // Initialize updated actions for the next commit attempt with the current attempt's actions\n    CloseableIterable<Row> updatedDataActions = attemptDataActions;\n    List<DomainMetadata> updatedDomainMetadatas = attemptDomainMetadatas;\n\n    if (TableFeatures.isRowTrackingSupported(transaction.getProtocol())) {\n      updatedDomainMetadatas =\n          RowTracking.updateRowIdHighWatermarkIfNeeded(\n              snapshotOpt,\n              transaction.getProtocol(),\n              lastWinningRowIdHighWatermark,\n              attemptDataActions,\n              attemptDomainMetadatas,\n              Optional.empty() /* providedRowIdHighWatermark */);\n      updatedDataActions =\n          RowTracking.assignBaseRowIdAndDefaultRowCommitVersion(\n              snapshotOpt,\n              transaction.getProtocol(),\n              lastWinningRowIdHighWatermark,\n              Optional.of(attemptVersion),\n              lastWinningVersion + 1,\n              attemptDataActions);\n    }\n\n    Optional<CRCInfo> updatedCrcInfo =\n        ChecksumReader.tryReadChecksumFile(\n            engine,\n            FileStatus.of(checksumFile(transaction.getLogPath(), lastWinningVersion).toString()));\n\n    // if we get here, we have successfully rebased (i.e no logical conflicts)\n    // against the winning transactions\n    return new TransactionRebaseState(\n        lastWinningVersion,\n        getLastCommitTimestamp(lastWinningVersion, lastWinningTxn, winningCommitInfoOpt.get()),\n        updatedDataActions,\n        updatedDomainMetadatas,\n        updatedCrcInfo);\n  }\n\n  /**\n   * Class containing the rebase state from winning transactions that the current transaction needs\n   * to rebase against before attempting the commit.\n   *\n   * <p>Currently, the rebase state is just the latest winning version of the table plus the updated\n   * data actions and domainMetadata actions to commit. In future once we start supporting\n   * read-after-write, row tracking, etc., we will have more state to add. For example\n   * read-after-write will need to know the files deleted in the winning transactions to make sure\n   * the same files are not deleted by the current (losing) transaction.\n   */\n  public static class TransactionRebaseState {\n    private final long latestVersion;\n    private final long latestCommitTimestamp;\n    private final CloseableIterable<Row> updatedDataActions;\n    private final List<DomainMetadata> updatedDomainMetadatas;\n    private final Optional<CRCInfo> updatedCrcInfo;\n\n    public TransactionRebaseState(\n        long latestVersion,\n        long latestCommitTimestamp,\n        CloseableIterable<Row> updatedDataActions,\n        List<DomainMetadata> updatedDomainMetadatas,\n        Optional<CRCInfo> updatedCrcInfo) {\n      this.latestVersion = latestVersion;\n      this.latestCommitTimestamp = latestCommitTimestamp;\n      this.updatedDataActions = updatedDataActions;\n      this.updatedDomainMetadatas = updatedDomainMetadatas;\n      this.updatedCrcInfo = updatedCrcInfo;\n    }\n\n    /**\n     * Return the latest winning version of the table.\n     *\n     * @return latest winning version of the table.\n     */\n    public long getLatestVersion() {\n      return latestVersion;\n    }\n\n    /**\n     * Return the latest commit timestamp of the table. For ICT enabled tables, this is the ICT of\n     * the latest winning transaction commit file. For non-ICT enabled tables, this is the\n     * modification time of the latest winning transaction commit file.\n     *\n     * @return latest commit timestamp of the table.\n     */\n    public long getLatestCommitTimestamp() {\n      return latestCommitTimestamp;\n    }\n\n    public CloseableIterable<Row> getUpdatedDataActions() {\n      return updatedDataActions;\n    }\n\n    public List<DomainMetadata> getUpdatedDomainMetadatas() {\n      return updatedDomainMetadatas;\n    }\n\n    public Optional<CRCInfo> getUpdatedCrcInfo() {\n      return updatedCrcInfo;\n    }\n  }\n\n  /**\n   * Any protocol changes between the losing transaction and the winning transactions are not\n   * allowed. In future once we start supporting more table features on the write side, this can be\n   * changed to handle safe protocol changes. For now the write support in Kernel is supported at a\n   * very first version of the protocol.\n   *\n   * @param protocolVector protocol rows from the winning transactions\n   */\n  private void handleProtocol(ColumnVector protocolVector) {\n    for (int rowId = 0; rowId < protocolVector.getSize(); rowId++) {\n      if (!protocolVector.isNullAt(rowId)) {\n        throw DeltaErrors.protocolChangedException(attemptVersion);\n      }\n    }\n  }\n\n  /**\n   * Any metadata changes between the losing transaction and the winning transactions are not\n   * allowed.\n   *\n   * @param metadataVector metadata rows from the winning transactions\n   */\n  private void handleMetadata(ColumnVector metadataVector) {\n    for (int rowId = 0; rowId < metadataVector.getSize(); rowId++) {\n      if (!metadataVector.isNullAt(rowId)) {\n        throw DeltaErrors.metadataChangedException();\n      }\n    }\n  }\n\n  /**\n   * Checks whether each of the current transaction's {@link DomainMetadata} conflicts with the\n   * winning transaction at any domain.\n   *\n   * <ol>\n   *   <li>Accept the current transaction if its set of metadata domains does not overlap with the\n   *       winning transaction's set of metadata domains.\n   *   <li>Otherwise, fail the current transaction unless each conflicting domain is associated with\n   *       a domain-specific way of resolving the conflict.\n   * </ol>\n   *\n   * @param domainMetadataVector domainMetadata rows from the winning transactions\n   * @return a map of domain name to {@link DomainMetadata} from the winning transaction\n   */\n  private Map<String, DomainMetadata> handleDomainMetadata(ColumnVector domainMetadataVector) {\n    // Build a domain metadata map from the winning transaction.\n    Map<String, DomainMetadata> winningTxnDomainMetadataMap = new HashMap<>();\n    DomainMetadataUtils.populateDomainMetadataMap(\n        domainMetadataVector, winningTxnDomainMetadataMap);\n\n    for (DomainMetadata currentTxnDM : attemptDomainMetadatas) {\n      // For each domain metadata action in the current transaction, check if it has a conflict with\n      // the winning transaction.\n      String domainName = currentTxnDM.getDomain();\n      DomainMetadata winningTxnDM = winningTxnDomainMetadataMap.get(domainName);\n      if (winningTxnDM != null) {\n        // Conflict - check if the conflict can be resolved.\n        // Domain-specific ways of resolving the conflict can be added here.\n        switch (domainName) {\n          case RowTrackingMetadataDomain.DOMAIN_NAME:\n            // We keep updating the new row ID high watermark we have seen from all winning txns.\n            // The latest one will be used to reassign row IDs later\n            final long winningRowIdHighWatermark =\n                RowTrackingMetadataDomain.fromJsonConfiguration(winningTxnDM.getConfiguration())\n                    .getRowIdHighWaterMark();\n            checkState(\n                !lastWinningRowIdHighWatermark.isPresent()\n                    || lastWinningRowIdHighWatermark.get() <= winningRowIdHighWatermark,\n                \"row ID high watermark should be monotonically increasing\");\n            this.lastWinningRowIdHighWatermark = Optional.of(winningRowIdHighWatermark);\n            break;\n          default:\n            throw concurrentDomainMetadataAction(currentTxnDM, winningTxnDM);\n        }\n      }\n    }\n\n    return winningTxnDomainMetadataMap;\n  }\n\n  /**\n   * Get the commit info from the winning transactions.\n   *\n   * @param commitInfoVector commit info rows from the winning transactions\n   * @return the commit info\n   */\n  private Optional<CommitInfo> getCommitInfo(ColumnVector commitInfoVector) {\n    for (int rowId = 0; rowId < commitInfoVector.getSize(); rowId++) {\n      if (!commitInfoVector.isNullAt(rowId)) {\n        return Optional.of(CommitInfo.fromColumnVector(commitInfoVector, rowId));\n      }\n    }\n    return Optional.empty();\n  }\n\n  private void handleTxn(ColumnVector txnVector) {\n    // Check if the losing transaction has any txn identifier. If it does, go through the\n    // winning transactions and make sure that the losing transaction is valid from a\n    // idempotent perspective.\n    Optional<SetTransaction> losingTxnIdOpt = transaction.getSetTxnOpt();\n    losingTxnIdOpt.ifPresent(\n        losingTxnId -> {\n          for (int rowId = 0; rowId < txnVector.getSize(); rowId++) {\n            SetTransaction winningTxn = SetTransaction.fromColumnVector(txnVector, rowId);\n            if (winningTxn != null\n                && winningTxn.getAppId().equals(losingTxnId.getAppId())\n                && winningTxn.getVersion() >= losingTxnId.getVersion()) {\n              throw DeltaErrors.concurrentTransaction(\n                  losingTxnId.getAppId(), losingTxnId.getVersion(), winningTxn.getVersion());\n            }\n          }\n        });\n  }\n\n  private List<FileStatus> getWinningCommitFiles(Engine engine) {\n    // TODO delta-io/delta#5018 this should be based on attemptVersion not readSnapshot version\n    String firstWinningCommitFile =\n        deltaFile(transaction.getLogPath(), transaction.getReadTableVersion() + 1);\n\n    try (CloseableIterator<FileStatus> files =\n        wrapEngineExceptionThrowsIO(\n            () -> engine.getFileSystemClient().listFrom(firstWinningCommitFile),\n            \"Listing from %s\",\n            firstWinningCommitFile)) {\n      // Select all winning transaction commit files.\n      List<FileStatus> winningCommitFiles = new ArrayList<>();\n      while (files.hasNext()) {\n        FileStatus file = files.next();\n        if (FileNames.isCommitFile(file.getPath())) {\n          winningCommitFiles.add(file);\n        }\n      }\n\n      return ensureNoGapsInWinningCommits(winningCommitFiles);\n    } catch (FileNotFoundException nfe) {\n      // no winning commits. why did we get here?\n      throw new IllegalStateException(\"No winning commits found.\", nfe);\n    } catch (IOException ioe) {\n      throw new UncheckedIOException(\"Error listing files from \" + firstWinningCommitFile, ioe);\n    }\n  }\n\n  /**\n   * Get the last commit timestamp of the table. For ICT enabled tables, this is the ICT of the\n   * latest winning transaction commit file. For non-ICT enabled tables, this is the modification\n   * time of the latest winning transaction commit file.\n   *\n   * @param lastWinningVersion last winning version of the table\n   * @param lastWinningTxn the last winning transaction commit file\n   * @param winningCommitInfoOpt winning commit info\n   * @return last commit timestamp of the table\n   */\n  private long getLastCommitTimestamp(\n      long lastWinningVersion,\n      FileStatus lastWinningTxn,\n      Optional<CommitInfo> winningCommitInfoOpt) {\n    if (!snapshotOpt.isPresent()\n        || !IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(snapshotOpt.get().getMetadata())) {\n      return lastWinningTxn.getModificationTime();\n    } else {\n      return CommitInfo.extractRequiredIctFromCommitInfoOpt(\n          winningCommitInfoOpt, lastWinningVersion, transaction.getDataPath());\n    }\n  }\n\n  private static List<FileStatus> ensureNoGapsInWinningCommits(List<FileStatus> winningCommits) {\n    long lastVersion = -1;\n    for (FileStatus commit : winningCommits) {\n      long version = FileNames.deltaVersion(commit.getPath());\n      checkState(\n          lastVersion == -1 || version == lastVersion + 1,\n          format(\n              \"Gaps in Delta log commit files. Expected version %d but got %d\",\n              (lastVersion + 1), version));\n      lastVersion = version;\n    }\n    return winningCommits;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/CreateCheckpointIterator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineException;\nimport static io.delta.kernel.internal.actions.SingleAction.CHECKPOINT_SCHEMA;\nimport static io.delta.kernel.internal.replay.LogReplayUtils.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.SetTransaction;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.util.*;\n\n/**\n * Replays a history of actions from the transaction log to reconstruct the checkpoint state of the\n * table. The rules for constructing the checkpoint state are defined in the Delta Protocol: <a\n * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#action-reconciliation\">Checkpoint\n * Reconciliation Rules</a>.\n *\n * <p>Currently, the following rules are implemented:\n *\n * <ul>\n *   <li>The latest protocol action seen wins\n *   <li>The latest metaData action seen wins\n *   <li>For txn actions, the latest version seen for a given appId wins\n *   <li>Logical files in a table are identified by their (path, deletionVector.uniqueId) primary\n *       key. File actions (add or remove) reference logical files, and a log can contain any number\n *       of references to a single file.\n *   <li>To replay the log, scan all file actions and keep only the newest reference for each\n *       logical file.\n *   <li>add actions in the result identify logical files currently present in the table (for\n *       queries). remove actions in the result identify tombstones of logical files no longer\n *       present in the table (for VACUUM).\n *   <li>commit info actions are not included\n * </ul>\n *\n * <p>Following rules are not implemented. They will be implemented as we add support for more table\n * features over time.\n *\n * <ul>\n *   <li>For domainMetadata, the latest domainMetadata seen for a given domain wins.\n * </ul>\n */\npublic class CreateCheckpointIterator implements CloseableIterator<FilteredColumnarBatch> {\n\n  private static final int[] ADD_ORDINAL = getPathOrdinals(CHECKPOINT_SCHEMA, \"add\");\n  private static final int[] ADD_PATH_ORDINAL = getPathOrdinals(CHECKPOINT_SCHEMA, \"add\", \"path\");\n  private static final int[] ADD_DV_ORDINAL =\n      getPathOrdinals(CHECKPOINT_SCHEMA, \"add\", \"deletionVector\");\n\n  private static final int[] REMOVE_ORDINAL = getPathOrdinals(CHECKPOINT_SCHEMA, \"remove\");\n  private static final int[] REMOVE_PATH_ORDINAL =\n      getPathOrdinals(CHECKPOINT_SCHEMA, \"remove\", \"path\");\n  private static final int[] REMOVE_DV_ORDINAL =\n      getPathOrdinals(CHECKPOINT_SCHEMA, \"remove\", \"deletionVector\");\n  private static final int[] REMOVE_DELETE_TIMESTAMP_ORDINAL =\n      getPathOrdinals(CHECKPOINT_SCHEMA, \"remove\", \"deletionTimestamp\");\n\n  private static final int[] PROTOCOL_ORDINAL = getPathOrdinals(CHECKPOINT_SCHEMA, \"protocol\");\n  private static final int[] METADATA_ORDINAL = getPathOrdinals(CHECKPOINT_SCHEMA, \"metaData\");\n  private static final int[] TXN_ORDINAL = getPathOrdinals(CHECKPOINT_SCHEMA, \"txn\");\n  private static final int[] DOMAIN_METADATA_DOMAIN_NAME_ORDINAL =\n      getPathOrdinals(CHECKPOINT_SCHEMA, \"domainMetadata\", \"domain\");\n\n  private final Engine engine;\n  private final LogSegment logSegment;\n\n  /**\n   * Tombstones (i.e. RemoveFile) will be still kept in checkpoint until the tombstone timestamp is\n   * earlier than this retention timestamp.\n   */\n  private final long minFileRetentionTimestampMillis;\n\n  // State of the iterator and current batch being worked on\n  private CloseableIterator<ActionWrapper> actionsIter;\n  private boolean closed;\n  private Optional<FilteredColumnarBatch> toReturnNext = Optional.empty();\n  /**\n   * This buffer is reused across batches to keep the memory allocations minimal. It is resized as\n   * required and the array entries are reset between batches.\n   */\n  private boolean[] selectionVectorBuffer;\n\n  // Current state of the tombstones and add files from delta files\n  private final Set<UniqueFileActionTuple> tombstonesFromJson = new HashSet<>();\n  private final Set<UniqueFileActionTuple> addFilesFromJson = new HashSet<>();\n\n  // Current state of the protocol and metadata. Captures whether protocol or metadata is seen.\n  // We traverse the log in reverse, so the first encounter of protocol or metadata is considered\n  // latest.\n  private boolean isMetadataAlreadySeen;\n  private boolean isProtocolAlreadySeen;\n\n  // Current state of the transaction identifier (a.k.a. SetTransaction). We traverse the log in\n  // reverse, so storing the first seen transaction version for each appId is enough for\n  // checkpoint\n  private final Map<String, Long> txnAppIdToVersion = new HashMap<>();\n\n  // Current state of all domains we have seen in {@link DomainMetadata} during the log replay. We\n  // traverse the log in reverse, so remembering the domains we have seen is enough for creating a\n  // checkpoint.\n  private final Set<String> domainSeen = new HashSet<>();\n\n  // Metadata about the checkpoint to store in `_last_checkpoint` file\n  private long numberOfAddActions = 0; // final number of add actions survived in the checkpoint\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  public CreateCheckpointIterator(\n      Engine engine, LogSegment logSegment, long minFileRetentionTimestampMillis) {\n    this.engine = engine;\n    this.logSegment = logSegment;\n    this.minFileRetentionTimestampMillis = minFileRetentionTimestampMillis;\n  }\n\n  @Override\n  public boolean hasNext() {\n    initActionIterIfRequired();\n    checkState(!closed, \"Can't call `hasNext` on a closed iterator.\");\n    return prepareNext();\n  }\n\n  @Override\n  public FilteredColumnarBatch next() {\n    checkState(!closed, \"Can't call `next` on a closed iterator.\");\n    if (!hasNext()) {\n      throw new NoSuchElementException();\n    }\n\n    FilteredColumnarBatch toReturn = toReturnNext.get();\n    toReturnNext = Optional.empty();\n    return toReturn;\n  }\n\n  @Override\n  public void close() throws IOException {\n    closed = true;\n    Utils.closeCloseables(actionsIter);\n  }\n\n  /**\n   * Number of add files in the final checkpoint. Should be called once the entire data of this\n   * iterator is consumed.\n   *\n   * @return Number of add files in checkpoint.\n   */\n  public long getNumberOfAddActions() {\n    checkState(closed, \"Iterator is not fully consumed yet.\");\n    return numberOfAddActions;\n  }\n\n  ////////////////////////////\n  // Private Helper Methods //\n  ////////////////////////////\n\n  private void initActionIterIfRequired() {\n    if (this.actionsIter == null) {\n      this.actionsIter =\n          new ActionsIterator(\n              engine,\n              logSegment.allLogFilesReversed(),\n              CHECKPOINT_SCHEMA,\n              Optional.empty() /* checkpoint predicate */);\n    }\n  }\n\n  /**\n   * Prepare the next batch to return and store it in {@link #toReturnNext}\n   *\n   * @return true if there is data to return, false otherwise.\n   */\n  private boolean prepareNext() {\n    if (toReturnNext.isPresent()) {\n      return true;\n    }\n    if (!actionsIter.hasNext()) {\n      return false;\n    }\n\n    ActionWrapper actionWrapper = actionsIter.next();\n    final ColumnarBatch actionsBatch = actionWrapper.getColumnarBatch();\n    final boolean isFromCheckpoint = actionWrapper.isFromCheckpoint();\n\n    // Prepare the selection vector to attach to the batch to indicate which records to\n    // write to checkpoint and which one or not\n    selectionVectorBuffer =\n        prepareSelectionVectorBuffer(selectionVectorBuffer, actionsBatch.getSize());\n\n    // Step 1: Update `tombstonesFromJson` with all the RemoveFiles in this columnar batch, if\n    // and only if this batch is not from a checkpoint. There's no reason to put a RemoveFile\n    // from a checkpoint into `tombstonesFromJson` since, when we generate a checkpoint,\n    // any corresponding AddFile would have been excluded already\n    if (!isFromCheckpoint) {\n      processRemoves(\n          getVector(actionsBatch, REMOVE_ORDINAL),\n          getVector(actionsBatch, REMOVE_PATH_ORDINAL),\n          getVector(actionsBatch, REMOVE_DV_ORDINAL),\n          getVector(actionsBatch, REMOVE_DELETE_TIMESTAMP_ORDINAL),\n          selectionVectorBuffer);\n    }\n\n    // Step 2: Iterate over all the AddFiles in this columnar batch in order to build up the\n    //         selection vector. We unselect an AddFile when it was removed by a RemoveFile\n    processAdds(\n        getVector(actionsBatch, ADD_ORDINAL),\n        getVector(actionsBatch, ADD_PATH_ORDINAL),\n        getVector(actionsBatch, ADD_DV_ORDINAL),\n        isFromCheckpoint,\n        selectionVectorBuffer);\n\n    // Step 3: Process the protocol\n    final ColumnVector protocolVector = getVector(actionsBatch, PROTOCOL_ORDINAL);\n    processProtocol(protocolVector, selectionVectorBuffer);\n\n    // Step 3: Process the metadata\n    final ColumnVector metadataVector = getVector(actionsBatch, METADATA_ORDINAL);\n    processMetadata(metadataVector, selectionVectorBuffer);\n\n    // Step 4: Process the transaction identifiers\n    final ColumnVector txnVector = getVector(actionsBatch, TXN_ORDINAL);\n    processTxn(txnVector, selectionVectorBuffer);\n\n    // Step 5: Process the domain metadata\n    final ColumnVector domainMetadataDomainNameVector =\n        getVector(actionsBatch, DOMAIN_METADATA_DOMAIN_NAME_ORDINAL);\n    processDomainMetadata(domainMetadataDomainNameVector, selectionVectorBuffer);\n\n    Optional<ColumnVector> selectionVector =\n        Optional.of(createSelectionVector(selectionVectorBuffer, actionsBatch.getSize()));\n    toReturnNext = Optional.of(new FilteredColumnarBatch(actionsBatch, selectionVector));\n    return true;\n  }\n\n  private void processRemoves(\n      ColumnVector removesVector,\n      ColumnVector removePathVector,\n      ColumnVector removeDvVector,\n      ColumnVector removeDeleteTimestampVector,\n      boolean[] selectionVectorBuffer) {\n    for (int rowId = 0; rowId < removesVector.getSize(); rowId++) {\n      if (removesVector.isNullAt(rowId)) {\n        continue; // selectionVector will be `false` at rowId by default\n      }\n\n      final UniqueFileActionTuple key =\n          getUniqueFileAction(removePathVector, removeDvVector, rowId);\n      tombstonesFromJson.add(key);\n\n      // Default is zero. Not sure if this the correct way, but it is same Delta Spark.\n      // Ideally this should never be zero, but we are following the same behavior as Delta\n      // Spark here.\n      long deleteTimestamp = 0;\n      if (!removeDeleteTimestampVector.isNullAt(rowId)) {\n        deleteTimestamp = removeDeleteTimestampVector.getLong(rowId);\n      }\n      if (deleteTimestamp > minFileRetentionTimestampMillis) {\n        // We still keep remove files in checkpoint as tombstones until the minimum\n        // retention period has passed\n        select(selectionVectorBuffer, rowId);\n      }\n    }\n  }\n\n  private void processAdds(\n      ColumnVector addsVector,\n      ColumnVector addPathVector,\n      ColumnVector addDvVector,\n      boolean isFromCheckpoint,\n      boolean[] selectionVectorBuffer) {\n    for (int rowId = 0; rowId < addsVector.getSize(); rowId++) {\n      if (addsVector.isNullAt(rowId)) {\n        continue; // selectionVector will be `false` at rowId by default\n      }\n\n      final UniqueFileActionTuple key = getUniqueFileAction(addPathVector, addDvVector, rowId);\n      final boolean alreadyDeleted = tombstonesFromJson.contains(key);\n      final boolean alreadyReturned = addFilesFromJson.contains(key);\n\n      if (!alreadyReturned) {\n        // Note: No AddFile will appear twice in a checkpoint, so we only need\n        //       non-checkpoint AddFiles in the set\n        if (!isFromCheckpoint) {\n          addFilesFromJson.add(key);\n        }\n\n        if (!alreadyDeleted) {\n          numberOfAddActions++;\n          select(selectionVectorBuffer, rowId);\n        }\n      }\n    }\n  }\n\n  private void processProtocol(ColumnVector protocolVector, boolean[] selectionVectorBuffer) {\n    for (int rowId = 0; rowId < protocolVector.getSize(); rowId++) {\n      if (protocolVector.isNullAt(rowId)) {\n        continue; // selectionVector will be `false` at rowId by default\n      }\n\n      if (isProtocolAlreadySeen) {\n        // We do a reverse log replay. The latest always the one that should be written\n        // to the checkpoint. Anything after the first one shouldn't be in checkpoint\n        unselect(selectionVectorBuffer, rowId);\n      } else {\n        select(selectionVectorBuffer, rowId);\n        isProtocolAlreadySeen = true;\n      }\n    }\n  }\n\n  private void processMetadata(ColumnVector metadataVector, boolean[] selectionVectorBuffer) {\n    for (int rowId = 0; rowId < metadataVector.getSize(); rowId++) {\n      if (metadataVector.isNullAt(rowId)) {\n        continue; // selectionVector will be `false` at rowId by default\n      }\n\n      if (isMetadataAlreadySeen) {\n        // We do a reverse log replay. The latest always the one that should be written\n        // to the checkpoint. Anything after the first one shouldn't be in checkpoint\n        unselect(selectionVectorBuffer, rowId);\n      } else {\n        select(selectionVectorBuffer, rowId);\n        isMetadataAlreadySeen = true;\n      }\n    }\n  }\n\n  private void processTxn(ColumnVector txnVector, boolean[] selectionVectorBuffer) {\n    for (int rowId = 0; rowId < txnVector.getSize(); rowId++) {\n      SetTransaction txn = SetTransaction.fromColumnVector(txnVector, rowId);\n      if (txn == null) {\n        continue; // selectionVector will be `false` at rowId by default\n      }\n      if (txnAppIdToVersion.containsKey(txn.getAppId())) {\n        // We do a reverse log replay. The latest txn version is the one that should be\n        // written to the checkpoint. Anything after the first one shouldn't be in\n        // checkpoint\n        unselect(selectionVectorBuffer, rowId);\n      } else {\n        select(selectionVectorBuffer, rowId);\n        txnAppIdToVersion.put(txn.getAppId(), txn.getVersion());\n      }\n    }\n  }\n\n  /**\n   * Processes domain metadata actions during checkpoint creation. During the reverse log replay,\n   * for each domain, we only keep the first (latest) domain metadata action encountered by\n   * selecting them in the selection vector, and ignore any older ones for the same domain by\n   * unselecting them.\n   *\n   * @param domainMetadataVector Column vector containing domain names of domain metadata actions.\n   * @param selectionVectorBuffer The selection vector to attach to the batch to indicate which\n   *     records to write to the checkpoint and which ones not to.\n   */\n  private void processDomainMetadata(\n      ColumnVector domainMetadataVector, boolean[] selectionVectorBuffer) {\n    final int vectorSize = domainMetadataVector.getSize();\n    for (int rowId = 0; rowId < vectorSize; rowId++) {\n      if (domainMetadataVector.isNullAt(rowId)) {\n        continue; // selectionVector will be `false` at rowId by default\n      }\n\n      final String domain = domainMetadataVector.getString(rowId);\n      if (domainSeen.contains(domain)) {\n        // We do a reverse log replay. The latest domainMetadata seen for a given domain wins and\n        // should be written to the checkpoint. Anything after the first one shouldn't be in\n        // checkpoint.\n        unselect(selectionVectorBuffer, rowId);\n      } else {\n        select(selectionVectorBuffer, rowId);\n        domainSeen.add(domain);\n      }\n    }\n  }\n\n  private void unselect(boolean[] selectionVectorBuffer, int rowId) {\n    // Just use the java assert (which are enabled in tests) for sanity checks. This should\n    // never happen. Given this is going to be on the hot path, we want to avoid cost in\n    // production.\n    assert !selectionVectorBuffer[rowId]\n        : \"Row is already marked for selection, can't unselect now: \" + rowId;\n    selectionVectorBuffer[rowId] = false;\n  }\n\n  private void select(boolean[] selectionVectorBuffer, int rowId) {\n    selectionVectorBuffer[rowId] = true;\n  }\n\n  private ColumnVector createSelectionVector(boolean[] selectionVectorBuffer, int size) {\n    return wrapEngineException(\n        () -> engine.getExpressionHandler().createSelectionVector(selectionVectorBuffer, 0, size),\n        \"Create selection vector for writing actions to checkpoints\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/DeltaLogFile.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.FileStatus;\n\n/**\n * Internal wrapper class holding information needed to perform log replay. Represents either a\n * Delta commit file, classic checkpoint, a multipart checkpoint, a V2 checkpoint, or a sidecar\n * checkpoint.\n */\npublic class DeltaLogFile {\n  public enum LogType {\n    COMMIT,\n    LOG_COMPACTION,\n    CHECKPOINT_CLASSIC,\n    MULTIPART_CHECKPOINT,\n    V2_CHECKPOINT_MANIFEST,\n    SIDECAR\n  }\n\n  public static DeltaLogFile forFileStatus(FileStatus file) {\n    String fileName = new Path(file.getPath()).getName();\n    LogType logType = null;\n    long version = -1;\n    if (FileNames.isCommitFile(fileName)) {\n      logType = LogType.COMMIT;\n      version = FileNames.deltaVersion(fileName);\n    } else if (FileNames.isLogCompactionFile(fileName)) {\n      logType = LogType.LOG_COMPACTION;\n      // use end version, similar to a checkpoint\n      version = FileNames.logCompactionVersions(fileName)._2;\n    } else if (FileNames.isClassicCheckpointFile(fileName)) {\n      logType = LogType.CHECKPOINT_CLASSIC;\n      version = FileNames.checkpointVersion(fileName);\n    } else if (FileNames.isMultiPartCheckpointFile(fileName)) {\n      logType = LogType.MULTIPART_CHECKPOINT;\n      version = FileNames.checkpointVersion(fileName);\n    } else if (FileNames.isV2CheckpointFile(fileName)) {\n      logType = LogType.V2_CHECKPOINT_MANIFEST;\n      version = FileNames.checkpointVersion(fileName);\n    } else {\n      throw new IllegalArgumentException(\n          \"File is not a recognized delta log type: \" + file.getPath());\n    }\n    return new DeltaLogFile(file, logType, version);\n  }\n\n  public static DeltaLogFile ofSideCar(FileStatus file, long version) {\n    return new DeltaLogFile(file, LogType.SIDECAR, version);\n  }\n\n  private final FileStatus file;\n  private final LogType logType;\n  private final long version;\n\n  private DeltaLogFile(FileStatus file, LogType logType, long version) {\n    this.file = file;\n    this.logType = logType;\n    this.version = version;\n  }\n\n  public FileStatus getFile() {\n    return file;\n  }\n\n  public LogType getLogType() {\n    return logType;\n  }\n\n  /**\n   * Get the version for this log file. Note that for LOG_COMPACTION files this returns the end\n   * version, similar to a checkpoint\n   */\n  public long getVersion() {\n    return version;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/LogReplay.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.replay;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.checkpoints.SidecarFile;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.metrics.ScanMetrics;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.util.DomainMetadataUtils;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Replays a history of actions, resolving them to produce the current state of the table. The\n * protocol for resolution is as follows:\n *\n * <ul>\n *   <li>The most recent {@code AddFile} and accompanying metadata for any `(path, dv id)` tuple\n *       wins.\n *   <li>{@code RemoveFile} deletes a corresponding AddFile. A {@code RemoveFile} \"corresponds\" to\n *       the AddFile that matches both the parquet file URI *and* the deletion vector's URI (if\n *       any).\n *   <li>The most recent {@code Metadata} wins.\n *   <li>The most recent {@code Protocol} version wins.\n *   <li>For each `(path, dv id)` tuple, this class should always output only one {@code *\n *       FileAction} (either {@code AddFile} or {@code RemoveFile})\n * </ul>\n */\npublic class LogReplay {\n\n  private static final Logger logger = LoggerFactory.getLogger(LogReplay.class);\n\n  //////////////////////////\n  // Static Schema Fields //\n  /////////////////////////\n\n  /** We don't need to read the entire RemoveFile, only the path and dv info */\n  private static StructType REMOVE_FILE_SCHEMA =\n      new StructType()\n          .add(\"path\", StringType.STRING, false /* nullable */)\n          .add(\"deletionVector\", DeletionVectorDescriptor.READ_SCHEMA, true /* nullable */);\n\n  /** Read schema when searching for just the transaction identifiers */\n  public static final StructType SET_TRANSACTION_READ_SCHEMA =\n      new StructType().add(\"txn\", SetTransaction.FULL_SCHEMA);\n\n  private static StructType getAddSchema(boolean shouldReadStats) {\n    return shouldReadStats ? AddFile.SCHEMA_WITH_STATS : AddFile.SCHEMA_WITHOUT_STATS;\n  }\n\n  /** Read schema when searching for just the domain metadata */\n  public static final StructType DOMAIN_METADATA_READ_SCHEMA =\n      new StructType().add(\"domainMetadata\", DomainMetadata.FULL_SCHEMA);\n\n  public static String SIDECAR_FIELD_NAME = \"sidecar\";\n  public static String ADDFILE_FIELD_NAME = \"add\";\n  public static String REMOVEFILE_FIELD_NAME = \"remove\";\n\n  public static StructType withSidecarFileSchema(StructType schema) {\n    return schema.add(SIDECAR_FIELD_NAME, SidecarFile.READ_SCHEMA);\n  }\n\n  public static boolean containsAddOrRemoveFileActions(StructType schema) {\n    return schema.fieldNames().contains(ADDFILE_FIELD_NAME)\n        || schema.fieldNames().contains(REMOVEFILE_FIELD_NAME);\n  }\n\n  /** Read schema when searching for all the active AddFiles */\n  public static StructType getAddRemoveReadSchema(boolean shouldReadStats) {\n    return new StructType()\n        .add(ADDFILE_FIELD_NAME, getAddSchema(shouldReadStats))\n        .add(REMOVEFILE_FIELD_NAME, REMOVE_FILE_SCHEMA);\n  }\n\n  /** Read schema when searching only for AddFiles */\n  public static StructType getAddReadSchema(boolean shouldReadStats) {\n    return new StructType().add(ADDFILE_FIELD_NAME, getAddSchema(shouldReadStats));\n  }\n\n  public static int ADD_FILE_ORDINAL = 0;\n  public static int ADD_FILE_PATH_ORDINAL = AddFile.SCHEMA_WITHOUT_STATS.indexOf(\"path\");\n  public static int ADD_FILE_DV_ORDINAL = AddFile.SCHEMA_WITHOUT_STATS.indexOf(\"deletionVector\");\n\n  public static int REMOVE_FILE_ORDINAL = 1;\n  public static int REMOVE_FILE_PATH_ORDINAL = REMOVE_FILE_SCHEMA.indexOf(\"path\");\n  public static int REMOVE_FILE_DV_ORDINAL = REMOVE_FILE_SCHEMA.indexOf(\"deletionVector\");\n\n  ///////////////////////////////////\n  // Member fields and constructor //\n  ///////////////////////////////////\n\n  private final Path dataPath;\n  private final Lazy<LogSegment> lazyLogSegment;\n  private final Lazy<Optional<CRCInfo>> lazyLatestCrcInfo;\n  private final Lazy<Map<String, DomainMetadata>> lazyActiveDomainMetadataMap;\n\n  /**\n   * Creates a new LogReplay instance.\n   *\n   * @param dataPath the path to the Delta table\n   * @param engine the engine to use for reading log files\n   * @param lazyLogSegment lazy loader for the log segment\n   * @param lazyCrcInfo lazy loader for the CRC file (shared with ProtocolMetadataLogReplay)\n   */\n  public LogReplay(\n      Engine engine,\n      Path dataPath,\n      Lazy<LogSegment> lazyLogSegment,\n      Lazy<Optional<CRCInfo>> lazyCrcInfo) {\n    this.dataPath = dataPath;\n    this.lazyLogSegment = lazyLogSegment;\n    this.lazyLatestCrcInfo = lazyCrcInfo;\n\n    // TODO: Refactor DomainMetadata loading to static utility, just like P & M loading\n    // Lazy loading of domain metadata only when needed\n    this.lazyActiveDomainMetadataMap =\n        new Lazy<>(\n            () ->\n                loadDomainMetadataMap(engine).entrySet().stream()\n                    .filter(entry -> !entry.getValue().isRemoved())\n                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));\n  }\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  public long getVersion() {\n    return getLogSegment().getVersion();\n  }\n\n  public Optional<Long> getLatestTransactionIdentifier(Engine engine, String applicationId) {\n    return loadLatestTransactionVersion(engine, applicationId);\n  }\n\n  /** Returns map for all active domain metadata. */\n  public Map<String, DomainMetadata> getActiveDomainMetadataMap() {\n    return lazyActiveDomainMetadataMap.get();\n  }\n\n  /**\n   * Returns the CRC info for the current snapshot version if available. Lazily loads and caches the\n   * CRC file on first access. Returns empty if no CRC file exists at the snapshot version.\n   */\n  public Optional<CRCInfo> getCrcInfoAtSnapshotVersion() {\n    // TODO: We should first just check if the checksum file in the LogSegment is at this snapshot\n    //       version.\n    return lazyLatestCrcInfo.get().filter(crcInfo -> crcInfo.getVersion() == getVersion());\n  }\n\n  /**\n   * Returns an iterator of {@link FilteredColumnarBatch} representing all the active AddFiles in\n   * the table.\n   *\n   * <p>Statistics are conditionally read for the AddFiles based on {@code shouldReadStats}. The\n   * returned batches have schema:\n   *\n   * <ol>\n   *   <li>name: {@code add}\n   *       <p>type: {@link AddFile#SCHEMA_WITH_STATS} if {@code shouldReadStats=true}, otherwise\n   *       {@link AddFile#SCHEMA_WITHOUT_STATS}\n   * </ol>\n   */\n  public CloseableIterator<FilteredColumnarBatch> getAddFilesAsColumnarBatches(\n      Engine engine,\n      boolean shouldReadStats,\n      Optional<Predicate> checkpointPredicate,\n      ScanMetrics scanMetrics,\n      Optional<PaginationContext> paginationContextOpt) {\n    // We do not need to look at any `remove` files from the checkpoints. Skip the column to save\n    // I/O. Note that we are still going to process the row groups. Adds and removes are randomly\n    // scattered through checkpoint part files, so row group push down is unlikely to be useful.\n    final CloseableIterator<ActionWrapper> addRemoveIter =\n        new ActionsIterator(\n            engine,\n            getLogReplayFiles(getLogSegment()),\n            getAddRemoveReadSchema(shouldReadStats),\n            getAddReadSchema(shouldReadStats),\n            checkpointPredicate,\n            paginationContextOpt);\n    return new ActiveAddFilesIterator(engine, addRemoveIter, dataPath, scanMetrics);\n  }\n\n  public LogSegment getLogSegment() {\n    return lazyLogSegment.get();\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  // For now we always read log compaction files. Plumb an option through to here if we ever want to\n  // make it configurable\n  private boolean readLogCompactionFiles = true;\n\n  /**\n   * Get the files to use for this log replay, can be configured for example to use or not use log\n   * compaction files\n   */\n  private List<FileStatus> getLogReplayFiles(LogSegment logSegment) {\n    if (readLogCompactionFiles) {\n      return logSegment.allFilesWithCompactionsReversed();\n    } else {\n      return logSegment.allLogFilesReversed();\n    }\n  }\n\n  private Optional<Long> loadLatestTransactionVersion(Engine engine, String applicationId) {\n    try (CloseableIterator<ActionWrapper> reverseIter =\n        new ActionsIterator(\n            engine,\n            getLogReplayFiles(getLogSegment()),\n            SET_TRANSACTION_READ_SCHEMA,\n            Optional.empty())) {\n      while (reverseIter.hasNext()) {\n        final ColumnarBatch columnarBatch = reverseIter.next().getColumnarBatch();\n        assert (columnarBatch.getSchema().equals(SET_TRANSACTION_READ_SCHEMA));\n\n        final ColumnVector txnVector = columnarBatch.getColumnVector(0);\n        for (int rowId = 0; rowId < txnVector.getSize(); rowId++) {\n          if (!txnVector.isNullAt(rowId)) {\n            SetTransaction txn = SetTransaction.fromColumnVector(txnVector, rowId);\n            if (txn != null && applicationId.equals(txn.getAppId())) {\n              return Optional.of(txn.getVersion());\n            }\n          }\n        }\n      }\n    } catch (IOException ex) {\n      throw new RuntimeException(\"Failed to fetch the transaction identifier\", ex);\n    }\n\n    return Optional.empty();\n  }\n\n  /**\n   * Loads the domain metadata map, either from CRC info (if available) or from the transaction log.\n   *\n   * @param engine The engine to use for loading from log when necessary\n   * @return A map of domain names to their metadata\n   */\n  private Map<String, DomainMetadata> loadDomainMetadataMap(Engine engine) {\n    long startTimeMillis = System.currentTimeMillis();\n    // Case 1: CRC does not exist or does not have domain metadata => read DM from log\n    final Optional<CRCInfo> latestCrcInfoOpt = lazyLatestCrcInfo.get();\n    if (!latestCrcInfoOpt.isPresent() || !latestCrcInfoOpt.get().getDomainMetadata().isPresent()) {\n      Map<String, DomainMetadata> domainMetadataMap =\n          loadDomainMetadataMapFromLog(engine, Optional.empty());\n      logger.info(\n          \"{}:No domain metadata available in CRC info,\"\n              + \" loading domain metadata for version {} from logs took {}ms\",\n          dataPath.toString(),\n          getVersion(),\n          System.currentTimeMillis() - startTimeMillis);\n      return domainMetadataMap;\n    }\n\n    final CRCInfo latestCrcInfo = latestCrcInfoOpt.get();\n\n    // Case 2: CRC exists at the snapshot version and has domain metadata => read DM from CRC\n    if (latestCrcInfo.getVersion() == getVersion()) {\n      Map<String, DomainMetadata> domainMetadataMap =\n          latestCrcInfo.getDomainMetadata().get().stream()\n              .collect(Collectors.toMap(DomainMetadata::getDomain, Function.identity()));\n      logger.info(\n          \"{}:CRC for version {} found, loading domain metadata from CRC took {}ms\",\n          dataPath.toString(),\n          getVersion(),\n          System.currentTimeMillis() - startTimeMillis);\n      return domainMetadataMap;\n    }\n\n    // Case 3: CRC exists at an *earlier* version and has domain metadata => read DM from CRC and\n    //         read DM from log for newer versions\n    Map<String, DomainMetadata> finalDomainMetadataMap =\n        loadDomainMetadataMapFromLog(engine, Optional.of(latestCrcInfo.getVersion() + 1));\n    // Add domains from the CRC that don't exist in the incremental log data\n    // - If a domain is updated to the newer versions or removed, it will exist in\n    // finalDomainMetadataMap, use the one in the map.\n    // - If a domain is only in the CRC file, use the one from CRC.\n    latestCrcInfo\n        .getDomainMetadata()\n        .get()\n        .forEach(\n            domainMetadataInCrc -> {\n              if (!finalDomainMetadataMap.containsKey(domainMetadataInCrc.getDomain())) {\n                finalDomainMetadataMap.put(domainMetadataInCrc.getDomain(), domainMetadataInCrc);\n              }\n            });\n    logger.info(\n        \"{}: Loading domain metadata for version {} from logs with crc version {} took {}ms\",\n        dataPath.toString(),\n        getVersion(),\n        latestCrcInfo.getVersion(),\n        System.currentTimeMillis() - startTimeMillis);\n    return finalDomainMetadataMap;\n  }\n\n  /**\n   * Retrieves a map of domainName to {@link DomainMetadata} from the log files.\n   *\n   * <p>Loading domain metadata requires an additional round of log replay so this is done lazily\n   * and only when domain metadata is requested.\n   *\n   * @param engine The engine used to process the log files.\n   * @param minLogVersion The minimum log version to read (inclusive). When provided, only reads log\n   *     files * starting from this version. When not provided, reads the entire log. * For\n   *     incremental loading from crc, this is typically set to (crc version + 1).\n   * @return A map where the keys are domain names and the values are the corresponding {@link\n   *     DomainMetadata} objects.\n   * @throws UncheckedIOException if an I/O error occurs while closing the iterator.\n   */\n  private Map<String, DomainMetadata> loadDomainMetadataMapFromLog(\n      Engine engine, Optional<Long> minLogVersion) {\n    long logReadCount = 0;\n    try (CloseableIterator<ActionWrapper> reverseIter =\n        new ActionsIterator(\n            engine,\n            getLogReplayFiles(getLogSegment()),\n            DOMAIN_METADATA_READ_SCHEMA,\n            Optional.empty() /* checkpointPredicate */)) {\n      Map<String, DomainMetadata> domainMetadataMap = new HashMap<>();\n      while (reverseIter.hasNext()) {\n        final ActionWrapper nextElem = reverseIter.next();\n        final long version = nextElem.getVersion();\n\n        // Stop before processing any batch from a version below minLogVersion. We use\n        // less-than (not equality) to ensure all batches from the minLogVersion file are\n        // fully processed, since a single large log file may produce multiple batches.\n        if (minLogVersion.isPresent() && version < minLogVersion.get()) {\n          break;\n        }\n\n        final ColumnarBatch columnarBatch = nextElem.getColumnarBatch();\n        logReadCount++;\n        assert (columnarBatch.getSchema().equals(DOMAIN_METADATA_READ_SCHEMA));\n\n        final ColumnVector dmVector = columnarBatch.getColumnVector(0);\n\n        // We are performing a reverse log replay. This function ensures that only the first\n        // encountered domain metadata for each domain is added to the map.\n        DomainMetadataUtils.populateDomainMetadataMap(dmVector, domainMetadataMap);\n      }\n      logger.info(\n          \"{}: Loading domain metadata from log for version {}, \"\n              + \"read {} actions, using crc version {}\",\n          dataPath.toString(),\n          getVersion(),\n          logReadCount,\n          minLogVersion.map(String::valueOf).orElse(\"N/A\"));\n      return domainMetadataMap;\n    } catch (IOException ex) {\n      throw new UncheckedIOException(\"Could not close iterator\", ex);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/LogReplayUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructType;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.*;\n\npublic class LogReplayUtils {\n\n  private LogReplayUtils() {}\n\n  public static class UniqueFileActionTuple extends Tuple2<URI, Optional<String>> {\n    UniqueFileActionTuple(URI fileURI, Optional<String> deletionVectorId) {\n      super(fileURI, deletionVectorId);\n    }\n  }\n\n  public static UniqueFileActionTuple getUniqueFileAction(\n      ColumnVector pathVector, ColumnVector dvVector, int rowId) {\n    final String path = pathVector.getString(rowId);\n    final URI pathAsUri = pathToUri(path);\n    final Optional<String> dvId =\n        Optional.ofNullable(DeletionVectorDescriptor.fromColumnVector(dvVector, rowId))\n            .map(DeletionVectorDescriptor::getUniqueId);\n\n    return new UniqueFileActionTuple(pathAsUri, dvId);\n  }\n\n  static boolean[] prepareSelectionVectorBuffer(boolean[] currentSelectionVector, int newSize) {\n    if (currentSelectionVector == null || currentSelectionVector.length < newSize) {\n      currentSelectionVector = new boolean[newSize];\n    } else {\n      // reset the array - if we are reusing the same buffer.\n      Arrays.fill(currentSelectionVector, false);\n    }\n    return currentSelectionVector;\n  }\n\n  static URI pathToUri(String path) {\n    try {\n      return new URI(path);\n    } catch (URISyntaxException ex) {\n      throw new RuntimeException(ex);\n    }\n  }\n\n  /**\n   * Get the ordinals of the column path at each level. Ordinal refers position of a column within a\n   * struct type column. For example: `struct(a: struct(a1: int, b1: long))` and lookup path is\n   * `a.b1` returns `0, 1`.\n   */\n  static int[] getPathOrdinals(StructType schema, String... path) {\n    checkArgument(path.length > 0, \"Invalid path\");\n    int[] pathOrdinals = new int[path.length];\n    DataType currentLevelDataType = schema;\n    for (int level = 0; level < path.length; level++) {\n      checkArgument(currentLevelDataType instanceof StructType, \"Invalid search path\");\n      StructType asStructType = (StructType) currentLevelDataType;\n      pathOrdinals[level] = asStructType.indexOf(path[level]);\n      currentLevelDataType = asStructType.at(pathOrdinals[level]).getDataType();\n    }\n    return pathOrdinals;\n  }\n\n  /** Get the vector corresponding to the given ordinals at each level of the column path. */\n  static ColumnVector getVector(ColumnarBatch batch, int[] pathOrdinals) {\n    checkArgument(pathOrdinals.length > 0, \"Invalid path ordinals size\");\n    ColumnVector vector = null;\n    for (int level = 0; level < pathOrdinals.length; level++) {\n      int levelOrdinal = pathOrdinals[level];\n      vector = (level == 0) ? batch.getColumnVector(levelOrdinal) : vector.getChild(levelOrdinal);\n    }\n\n    return vector;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/PageToken.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.types.*;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/** Page Token Class for Pagination Support */\npublic class PageToken {\n\n  public static PageToken fromRow(Row row) {\n    requireNonNull(row);\n\n    // Check #1: Correct schema\n    checkArgument(\n        PAGE_TOKEN_SCHEMA.equals(row.getSchema()),\n        String.format(\n            \"Invalid Page Token: input row schema does not match expected PageToken schema.\"\n                + \"\\nExpected: %s\\nGot: %s\",\n            PAGE_TOKEN_SCHEMA, row.getSchema()));\n\n    // Check #2: All required fields are present\n    for (int i = 0; i < PAGE_TOKEN_SCHEMA.length(); i++) {\n      if (PAGE_TOKEN_SCHEMA.at(i).getName().equals(\"lastReadSidecarFileIdx\")) continue;\n      checkArgument(\n          !row.isNullAt(i),\n          String.format(\n              \"Invalid Page Token: required field '%s' is null at index %d\",\n              PAGE_TOKEN_SCHEMA.at(i).getName(), i));\n    }\n\n    return new PageToken(\n        row.getString(0), // lastReadLogFilePath\n        row.getLong(1), // lastReturnedRowIndex\n        Optional.ofNullable(row.isNullAt(2) ? null : row.getLong(2)), // lastReadSidecarFileIdx\n        row.getString(3), // kernelVersion\n        row.getString(4), // tablePath\n        row.getLong(5), // tableVersion\n        row.getInt(6), // predicateHash\n        row.getInt(7)); // logSegmentHash\n  }\n\n  public static final StructType PAGE_TOKEN_SCHEMA =\n      new StructType()\n          .add(\"lastReadLogFilePath\", StringType.STRING, false /* nullable */)\n          .add(\"lastReturnedRowIndex\", LongType.LONG, false /* nullable */)\n          .add(\"lastReadSidecarFileIdx\", LongType.LONG, true /* nullable */)\n          .add(\"kernelVersion\", StringType.STRING, false /* nullable */)\n          .add(\"tablePath\", StringType.STRING, false /* nullable */)\n          .add(\"tableVersion\", LongType.LONG, false /* nullable */)\n          .add(\"predicateHash\", IntegerType.INTEGER, false /* nullable */)\n          .add(\"logSegmentHash\", IntegerType.INTEGER, false /* nullable */);\n\n  // ===== Variables to mark where the last page ended (and the current page starts) =====\n\n  /** The last log file read in the previous page. */\n  private final String lastReadLogFilePath;\n\n  /**\n   * The index of the last row that was returned from the last read log file during the previous\n   * page. This row index is relative to the file. The current page should begin from the row\n   * immediately after this row index.\n   */\n  private final long lastReturnedRowIndex;\n\n  /**\n   * Optional index of the last sidecar checkpoint file read in the previous page. This index is\n   * based on the ordering of sidecar files in the V2 manifest checkpoint file. If present, it must\n   * represent the final sidecar file that was read and must correspond to the same file as\n   * `lastReadLogFilePath`.\n   */\n  private final Optional<Long> lastReadSidecarFileIdx;\n\n  // ===== Variables for validating query params and detecting changes in log segment =====\n  private final String kernelVersion;\n  private final String tablePath;\n  private final long tableVersion;\n  private final int predicateHash;\n  private final int logSegmentHash;\n\n  public PageToken(\n      String lastReadLogFilePath,\n      long lastReturnedRowIndex,\n      Optional<Long> lastReadSidecarFileIdx,\n      String kernelVersion,\n      String tablePath,\n      long tableVersion,\n      int predicateHash,\n      int logSegmentHash) {\n    this.lastReadLogFilePath = requireNonNull(lastReadLogFilePath, \"lastReadLogFilePath is null\");\n    this.lastReturnedRowIndex = lastReturnedRowIndex;\n    this.lastReadSidecarFileIdx = lastReadSidecarFileIdx;\n    this.kernelVersion = requireNonNull(kernelVersion, \"kernelVersion is null\");\n    this.tablePath = requireNonNull(tablePath, \"tablePath is null\");\n    this.tableVersion = tableVersion;\n    this.predicateHash = predicateHash;\n    this.logSegmentHash = logSegmentHash;\n  }\n\n  public Row toRow() {\n    Map<Integer, Object> pageTokenMap = new HashMap<>();\n    pageTokenMap.put(0, lastReadLogFilePath);\n    pageTokenMap.put(1, lastReturnedRowIndex);\n    pageTokenMap.put(2, lastReadSidecarFileIdx.orElse(null));\n    pageTokenMap.put(3, kernelVersion);\n    pageTokenMap.put(4, tablePath);\n    pageTokenMap.put(5, tableVersion);\n    pageTokenMap.put(6, predicateHash);\n    pageTokenMap.put(7, logSegmentHash);\n\n    return new GenericRow(PAGE_TOKEN_SCHEMA, pageTokenMap);\n  }\n\n  public String getLastReadLogFilePath() {\n    return lastReadLogFilePath;\n  }\n\n  public long getLastReturnedRowIndex() {\n    return lastReturnedRowIndex;\n  }\n\n  public Optional<Long> getLastReadSidecarFileIdx() {\n    return lastReadSidecarFileIdx;\n  }\n\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  public long getTableVersion() {\n    return tableVersion;\n  }\n\n  public String getKernelVersion() {\n    return kernelVersion;\n  }\n\n  public int getPredicateHash() {\n    return predicateHash;\n  }\n\n  public int getLogSegmentHash() {\n    return logSegmentHash;\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n\n    PageToken other = (PageToken) obj;\n\n    return lastReturnedRowIndex == other.lastReturnedRowIndex\n        && tableVersion == other.tableVersion\n        && predicateHash == other.predicateHash\n        && logSegmentHash == other.logSegmentHash\n        && Objects.equals(lastReadSidecarFileIdx, other.lastReadSidecarFileIdx)\n        && Objects.equals(lastReadLogFilePath, other.lastReadLogFilePath)\n        && Objects.equals(kernelVersion, other.kernelVersion)\n        && Objects.equals(tablePath, other.tablePath);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(\n        lastReadLogFilePath,\n        lastReturnedRowIndex,\n        lastReadSidecarFileIdx,\n        kernelVersion,\n        tablePath,\n        tableVersion,\n        predicateHash,\n        logSegmentHash);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/PaginatedScanFilesIteratorImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\n\nimport io.delta.kernel.Meta;\nimport io.delta.kernel.PaginatedScanFilesIterator;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.util.NoSuchElementException;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Implementation of {@link PaginatedScanFilesIterator} */\npublic class PaginatedScanFilesIteratorImpl implements PaginatedScanFilesIterator {\n\n  private static final Logger logger =\n      LoggerFactory.getLogger(PaginatedScanFilesIteratorImpl.class);\n\n  /**\n   * Filtered ScanFiles iterator from the base scan, excluding batches from fully consumed log\n   * files. For example, if previous pages have fully consumed sidecar files A and B, and partially\n   * consumed sidecar C, this iterator will exclude all batches from A and B, but include all\n   * batches from C.\n   *\n   * <p>Note: When no cached hash sets are available, this iterator will include all batches from\n   * JSON log files.\n   */\n  private final CloseableIterator<FilteredColumnarBatch> baseFilteredScanFilesIter;\n\n  /** Pagination Context that carries page token and page size info. */\n  private final PaginationContext paginationContext;\n\n  /** Maximum number of ScanFiles to include in current page */\n  private final long pageSize;\n\n  /** Total number of ScanFiles returned in current page */\n  private long numScanFilesReturned;\n\n  /**\n   * The name of the last log file that was read during pagination.\n   *\n   * <p>This value is used to track which log file the current scan is processing.\n   *\n   * <p>Initialization:\n   *\n   * <ul>\n   *   <li>If the pagination token includes a log file path, this value is initialized from it.\n   *   <li>If the pagination token does not include a log file path (i.e., the previous page did not\n   *       read any log file), this value is initialized to {@code null}.\n   * </ul>\n   */\n  private String lastReadLogFilePath = null;\n\n  /**\n   * Tracks the index of the last read sidecar file during pagination.\n   *\n   * <p>The index starts from 0 for the first sidecar file read. It is incremented each time a new\n   * sidecar file is encountered during scanning.\n   *\n   * <p>Initialization:\n   *\n   * <ul>\n   *   <li>If the pagination token includes a sidecar index, this value is initialized from it.\n   *   <li>If the pagination token does not include a sidecar index (i.e., no sidecar file was read\n   *       in the previous page), this value is initialized to {@code -1}.\n   * </ul>\n   */\n  private long lastSidecarIndex = -1;\n\n  /**\n   * The index of the last row returned from the last log file that was read.\n   *\n   * <p>For example, if the last page contains 3 batches from the same log file, and each batch has\n   * 10 rows, this value will be 29 (since row indices start at 0).\n   *\n   * <p>This value corresponds to the one saved in the page token if present.\n   */\n  private long lastReturnedRowIndex = -1;\n\n  private Optional<FilteredColumnarBatch> currentBatch = Optional.empty();\n\n  private boolean closed = false;\n\n  private boolean isBaseScanExhausted = false;\n\n  private boolean hasLeastOneBatchConsumed = false;\n\n  /**\n   * Constructs a paginated iterator over scan files on top of a given filtered scan files iterator\n   * and pagination context.\n   *\n   * @param baseFilteredScanFilesIter The underlying scan files iterator with data skipping and\n   *     partition pruning applied. This iterator serves as the source of filtered scan results for\n   *     pagination.\n   * @param paginationContext The pagination context that carries pagination-related information,\n   *     such as the maximum number of files to return in a page.\n   */\n  public PaginatedScanFilesIteratorImpl(\n      CloseableIterator<FilteredColumnarBatch> baseFilteredScanFilesIter,\n      PaginationContext paginationContext) {\n    this.baseFilteredScanFilesIter = baseFilteredScanFilesIter;\n    this.paginationContext = paginationContext;\n    this.pageSize = paginationContext.getPageSize();\n    if (paginationContext.getLastReadLogFilePath().isPresent()) {\n      lastReadLogFilePath = paginationContext.getLastReadLogFilePath().get();\n    }\n    if (paginationContext.getLastReadSidecarFileIdx().isPresent()) {\n      lastSidecarIndex = paginationContext.getLastReadSidecarFileIdx().get();\n    }\n  }\n\n  /**\n   * Returns a page token representing the position of the last consumed batch, corresponding to the\n   * most recent {@code next()} call.\n   *\n   * <p>Note: This method can be called after the paginated iterator has been closed.\n   */\n  @Override\n  public Optional<Row> getCurrentPageToken() {\n    // User must call getCurrentPageToken() after they call next() at least once.\n    checkState(\n        hasLeastOneBatchConsumed,\n        \"Can't call getCurrentPageToken() without consuming any batches!\");\n    // Return empty page token to signal pagination completes.\n    if (isBaseScanExhausted) {\n      return Optional.empty();\n    }\n    // TODO: replace hash value of log segment\n    Row pageTokenRow =\n        new PageToken(\n                lastReadLogFilePath,\n                lastReturnedRowIndex,\n                (lastSidecarIndex == -1) ? Optional.empty() : Optional.of(lastSidecarIndex),\n                Meta.KERNEL_VERSION,\n                paginationContext.getTablePath(),\n                paginationContext.getTableVersion(),\n                paginationContext.getPredicateHash(),\n                paginationContext.getLogSegmentHash())\n            .toRow();\n    return Optional.of(pageTokenRow);\n  }\n\n  @Override\n  public boolean hasNext() {\n    checkState(!closed, \"Can't call `hasNext` on a closed iterator.\");\n    if (!currentBatch.isPresent()) {\n      prepareNext();\n    }\n    return currentBatch.isPresent();\n  }\n\n  /**\n   * Prepares the next FilteredColumnarBatch to return in the current page. Skips batches that have\n   * already been returned in the previous page, based on file path, row index and sidecar index\n   * stored in the pagination context.\n   */\n  private void prepareNext() {\n    if (currentBatch.isPresent()) return;\n    if (!baseFilteredScanFilesIter.hasNext()) {\n      isBaseScanExhausted = true;\n      return;\n    }\n    if (numScanFilesReturned >= pageSize) return;\n\n    Optional<String> tokenLastReadFilePathOpt = paginationContext.getLastReadLogFilePath();\n    Optional<Long> tokenLastReadSidecarFileIdxOpt = paginationContext.getLastReadSidecarFileIdx();\n\n    while (baseFilteredScanFilesIter.hasNext() && numScanFilesReturned < pageSize) {\n      final FilteredColumnarBatch batch = baseFilteredScanFilesIter.next();\n\n      validateBatch(batch);\n\n      final String batchFilePath = batch.getFilePath().get();\n      final long numRowsInBatch = batch.getData().getSize();\n\n      // Case 1: Skip batches from fully consumed files.\n      // A file is considered fully consumed if it appears earlier (in reverse lexicographic order)\n      // than the last read file recorded in the page token.\n      //\n      // Example:\n      //   - Suppose the previous page ends at file 13.json\n      //   - Then, all files after 13.json (e.g., 14.json, 15.json, etc.)\n      //     have already been processed and are considered fully consumed.\n      //   - Any batches from these files should be skipped here.\n      if (isBatchFromFullyConsumedFile(\n          batchFilePath, tokenLastReadFilePathOpt, tokenLastReadSidecarFileIdxOpt)) {\n        // All fully consumed multi-part checkpoints should already be skipped in ActionsIterator.\n        // Fully consumed delta commit, log compaction and V2 checkpoint files won't be skipped in\n        // ActionsIterator.\n        checkArgument(!FileNames.isMultiPartCheckpointFile(batchFilePath));\n\n        logger.info(\"Pagination: skipping batch from a fully consumed file : {}\", batchFilePath);\n        continue;\n      }\n\n      // Case 2: The batch belongs to the same last read log file recorded in the page token.\n      // In this case, we may have partially consumed this file on the previous page,\n      // so we need to decide whether to skip the current batch based on row index.\n      //\n      // Example:\n      //   - Page 1 ends after processing row index 9 in file 13.json (i.e., the first 10 rows).\n      //   - This includes two batches: batch 1 (rows 0–4), batch 2 (rows 5–9).\n      //   - When reading page 2, we may re-encounter these batches.\n      //     * Batch 1 ends at row 4 → skip (already returned).\n      //     * Batch 2 ends at row 9 → skip (already returned).\n      //     * Batch 3 starts at row 10 → keep (new data).\n      else if (isBatchFromLastFileInToken(\n          batchFilePath, tokenLastReadFilePathOpt, tokenLastReadSidecarFileIdxOpt)) {\n        Optional<Long> tokenLastReturnedRowIndexOpt = paginationContext.getLastReturnedRowIndex();\n        // Skip this batch if its last row index is smaller than or equal to last returned row index\n        // in token.\n        if (tokenLastReturnedRowIndexOpt.isPresent()\n            && lastReturnedRowIndex + numRowsInBatch <= tokenLastReturnedRowIndexOpt.get()) {\n          lastReturnedRowIndex += numRowsInBatch;\n          logger.info(\n              \"Pagination: skipping batch from a partially consumed file : {}, \"\n                  + \"last row index is {}\",\n              batchFilePath,\n              lastReturnedRowIndex);\n          continue;\n        }\n      }\n\n      // Case 3: If this batch belongs to an \"unseen file\" — meaning a file whose content was\n      // not read at all in any previous page. In other words, this file is fully unconsumed:\n      // it was neither partially read nor fully read before.\n      // Batches from such files won't be skipped.\n\n      // currentBatch will be included in the current page.\n      currentBatch = Optional.of(batch);\n\n      // Found a valid batch, break out of the loop\n      break;\n    }\n  }\n\n  @Override\n  public FilteredColumnarBatch next() {\n    checkState(!closed, \"Can't call `next` on a closed iterator.\");\n    if (!hasNext()) {\n      throw new NoSuchElementException();\n    }\n    hasLeastOneBatchConsumed = true;\n    final FilteredColumnarBatch ret = currentBatch.get();\n    validateBatch(ret);\n    final long numSelectedAddFilesInBatch = ret.getPreComputedNumSelectedRows().get();\n    final String batchFilePath = ret.getFilePath().get();\n\n    // This batch is the first one we've seen from an \"unseen file\" during the current page\n    // read;\n    // update tracking state to reflect that we're now reading an \"unseen file\".\n    // Example:\n    //   - Page 1 ends midway through 18.json.\n    //   - Page 2 resumes, completes 18.json, then sees 17.json.\n    //   - 17.json was not seen in Page 1, so it's an \"unseen file\".\n    //   - In 17.json, all batches are emitted.\n    //   - Only the first batch triggers state reset via `isFirstBatchFromNewFile`.\n    if (isFirstBatchFromUnseenFile(batchFilePath)) {\n      lastReadLogFilePath = batchFilePath;\n      // Start from -1 so adding the first batch size gives correct 0-based row index.\n      lastReturnedRowIndex = -1;\n      logger.info(\"Pagination: reading new file: {}\", lastReadLogFilePath);\n      if (isSidecar(batchFilePath)) {\n        lastSidecarIndex++;\n      }\n    }\n\n    // Calculate the row index of the last row in current batch within the file.\n    lastReturnedRowIndex += ret.getData().getSize();\n    numScanFilesReturned += numSelectedAddFilesInBatch;\n\n    logger.info(\n        \"Pagination: emit a new batch: lastReadLogFilePath: {}, lastReturnedRowIndex: {}\",\n        lastReadLogFilePath,\n        lastReturnedRowIndex);\n    logger.info(\"Pagination: total numScanFilesReturned: {}\", numScanFilesReturned);\n\n    currentBatch = Optional.empty();\n    return ret;\n  }\n\n  void validateBatch(FilteredColumnarBatch batch) {\n    // FilePath and pre-computed number of selected rows are expected to be present; both are\n    // computed and set in ActiveAddFilesIterator (when building FilteredColumnarBatch from\n    // ActionWrapper)\n    checkArgument(batch.getFilePath().isPresent(), \"File path doesn't exist!\");\n    checkArgument(\n        batch.getPreComputedNumSelectedRows().isPresent(),\n        \"Pre-computed number of selected rows doesn't exist!\");\n  }\n\n  /**\n   * Returns {@code true} if the current batch comes from a fully consumed file in previous pages,\n   * as determined by the page token.\n   */\n  private boolean isBatchFromFullyConsumedFile(\n      String batchFilePath,\n      Optional<String> tokenLastReadLogFilePathOpt,\n      Optional<Long> tokenLastReadSidecarFileIdxOpt) {\n\n    if (tokenLastReadSidecarFileIdxOpt.isPresent()) {\n      // If a sidecar file was read in the previous page, all non-sidecar files (i.e., delta commit,\n      // log compaction and V2 checkpoint) are considered fully consumed.\n      return !isSidecar(batchFilePath);\n    } else if (tokenLastReadLogFilePathOpt.isPresent()) {\n      // Delta log files are ordered in reverse lexicographic order (i.e., higher file names appear\n      // earlier).\n      // If the current batch’s log file name is greater than the last one recorded in the token,\n      // it means this file appeared earlier in the segment and has already been processed.\n      return !isSidecar(batchFilePath)\n          && batchFilePath.compareTo(tokenLastReadLogFilePathOpt.get()) > 0;\n    }\n    return false;\n  }\n\n  /**\n   * Returns true if the current batch is from the same file (either log or sidecar) that the\n   * previous page ended at, based on the file path and sidecar index recorded in the page token.\n   */\n  private boolean isBatchFromLastFileInToken(\n      String batchFilePath,\n      Optional<String> tokenLastReadFilePathOpt,\n      Optional<Long> tokenLastReadSidecarFileIdxOpt) {\n    // Check if batch file path matches last read file path recorded in the page token (if\n    // present).\n    boolean isSameFilePath =\n        tokenLastReadFilePathOpt.isPresent()\n            && batchFilePath.equals(tokenLastReadFilePathOpt.get());\n    if (!isSameFilePath) return false;\n    // For sidecar files, if file path matches, sidecar index must also present and match.\n    if (isSidecar(batchFilePath)) {\n      checkArgument(\n          tokenLastReadSidecarFileIdxOpt.isPresent()\n              && lastSidecarIndex == tokenLastReadSidecarFileIdxOpt.get(),\n          \"Sidecar index mismatch for file: %s\",\n          batchFilePath);\n    }\n    return true;\n  }\n\n  /**\n   * Returns {@code true} if the current batch is the first one from a different file than the last\n   * seen, indicating the start of a new unseen file during pagination.\n   */\n  private boolean isFirstBatchFromUnseenFile(String batchFilePath) {\n    // If the batch's file path differs from {@code lastReadLogFilePath}, it's considered an\n    // unseen file.\n    if (!batchFilePath.equals(lastReadLogFilePath)) {\n      // For non-sidecar files, files must appear in reverse lexicographic order —\n      // i.e., the current file must come *before* the last seen file.\n      checkArgument(\n          isSidecar(batchFilePath)\n              || lastReadLogFilePath == null\n              || batchFilePath.compareTo(lastReadLogFilePath) < 0,\n          \"Expected file '%s' to appear after last read file '%s' in reverse lexicographic order, \"\n              + \"unless it's a sidecar file\",\n          batchFilePath,\n          lastReadLogFilePath);\n      return true;\n    }\n    return false;\n  }\n\n  // TODO: move isSidecar() to FileNames\n  private boolean isSidecar(String filePath) {\n    return filePath.contains(\"/_delta_log/_sidecars/\") && filePath.endsWith(\".parquet\");\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (!closed) {\n      closed = true;\n      Utils.closeCloseables(baseFilteredScanFilesIter);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/PaginationContext.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.Meta;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/** {@code PaginationContext} carries pagination-related information. */\npublic class PaginationContext {\n\n  public static PaginationContext forPageWithPageToken(\n      String tablePath,\n      long tableVersion,\n      int logSegmentHash,\n      int predicateHash,\n      long pageSize,\n      PageToken pageToken) {\n    Objects.requireNonNull(pageToken, \"page token is null\");\n    Objects.requireNonNull(tablePath, \"table path is null\");\n    checkArgument(\n        tablePath.equals(pageToken.getTablePath()),\n        \"Invalid page token: token table path does not match the requested table path. \"\n            + \"Expected: %s, Found: %s\",\n        tablePath,\n        pageToken.getTablePath());\n    checkArgument(\n        tableVersion == pageToken.getTableVersion(),\n        \"Invalid page token: token table version does not match the requested table version. \"\n            + \"Expected: %d, Found: %d\",\n        tableVersion,\n        pageToken.getTableVersion());\n    checkArgument(\n        Meta.KERNEL_VERSION.equals(pageToken.getKernelVersion()),\n        \"Invalid page token: token kernel version does not match the requested kernel version. \"\n            + \"Expected: %s, Found: %s\",\n        Meta.KERNEL_VERSION,\n        pageToken.getKernelVersion());\n    checkArgument(\n        predicateHash == pageToken.getPredicateHash(),\n        \"Invalid page token: token predicate hash does not match the requested predicate hash. \"\n            + \"Expected: %s, Found: %s\",\n        predicateHash,\n        pageToken.getPredicateHash());\n    checkArgument(\n        logSegmentHash == pageToken.getLogSegmentHash(),\n        \"Invalid page token: token log segment hash does not match the requested log segment hash. \"\n            + \"Expected: %s, Found: %s\",\n        logSegmentHash,\n        pageToken.getLogSegmentHash());\n    return new PaginationContext(\n        tablePath, tableVersion, logSegmentHash, predicateHash, pageSize, Optional.of(pageToken));\n  }\n\n  public static PaginationContext forFirstPage(\n      String tablePath, long tableVersion, int logSegmentHash, int predicateHash, long pageSize) {\n    Objects.requireNonNull(tablePath, \"table path is null\");\n    return new PaginationContext(\n        tablePath,\n        tableVersion,\n        logSegmentHash,\n        predicateHash,\n        pageSize,\n        Optional.empty() /* page token */);\n  }\n\n  private final String tablePath;\n\n  private final long tableVersion;\n\n  private final int predicateHash;\n\n  private final int logSegmentHash;\n\n  /** maximum number of ScanFiles to return in the current page */\n  private final long pageSize;\n\n  /** Optional Page Token */\n  private final Optional<PageToken> pageToken;\n\n  // TODO: add cached log replay hashsets related info\n\n  private PaginationContext(\n      String tablePath,\n      long tableVersion,\n      int logSegmentHash,\n      int predicateHash,\n      long pageSize,\n      Optional<PageToken> pageToken) {\n    checkArgument(pageSize > 0, \"Page size must be greater than zero!\");\n    this.tablePath = tablePath;\n    this.tableVersion = tableVersion;\n    this.logSegmentHash = logSegmentHash;\n    this.predicateHash = predicateHash;\n    this.pageSize = pageSize;\n    this.pageToken = pageToken;\n  }\n\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  public long getTableVersion() {\n    return tableVersion;\n  }\n\n  public int getPredicateHash() {\n    return predicateHash;\n  }\n\n  public int getLogSegmentHash() {\n    return logSegmentHash;\n  }\n\n  public long getPageSize() {\n    return pageSize;\n  }\n\n  public Optional<String> getLastReadLogFilePath() {\n    return pageToken.map(PageToken::getLastReadLogFilePath);\n  }\n\n  public Optional<Long> getLastReturnedRowIndex() {\n    if (!pageToken.isPresent()) return Optional.empty();\n    return Optional.of(pageToken.get().getLastReturnedRowIndex());\n  }\n\n  public Optional<Long> getLastReadSidecarFileIdx() {\n    if (!pageToken.isPresent()) return Optional.empty();\n    return pageToken.get().getLastReadSidecarFileIdx();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/replay/ProtocolMetadataLogReplay.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.replay;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.metrics.SnapshotMetrics;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Static utility class for loading Protocol and Metadata from Delta log files. */\npublic class ProtocolMetadataLogReplay {\n\n  private static final Logger logger = LoggerFactory.getLogger(ProtocolMetadataLogReplay.class);\n\n  /** Read schema when searching for the latest Protocol and Metadata. */\n  public static final StructType PROTOCOL_METADATA_READ_SCHEMA =\n      new StructType().add(\"protocol\", Protocol.FULL_SCHEMA).add(\"metaData\", Metadata.FULL_SCHEMA);\n\n  /** Result of loading Protocol and Metadata from a LogSegment. */\n  public static class Result {\n    public final Protocol protocol;\n    public final Metadata metadata;\n    private final long numDeltaFilesRead;\n\n    public Result(Protocol protocol, Metadata metadata, long numDeltaFilesRead) {\n      this.protocol = protocol;\n      this.metadata = metadata;\n      this.numDeltaFilesRead = numDeltaFilesRead;\n    }\n  }\n\n  /**\n   * Loads the latest Protocol and Metadata from the log files in the given LogSegment.\n   *\n   * <p>Uses the provided lazy CRC loader to bound how many delta files it reads, and to ensure we\n   * only read the CRC file if needed.\n   *\n   * <p>We read delta files in reverse order (newest first) searching for the latest Protocol and\n   * Metadata. When we reach the version just before (greater than) the CRC file version (if\n   * present), we lazily load the CRC file to fill in any missing Protocol or Metadata, avoiding\n   * reading older delta files.\n   *\n   * <p>Also validates that the Kernel can read the table at the loaded protocol version.\n   */\n  public static Result loadProtocolAndMetadata(\n      Engine engine,\n      Path dataPath,\n      LogSegment logSegment,\n      Lazy<Optional<CRCInfo>> lazyCrcInfo,\n      SnapshotMetrics snapshotMetrics) {\n    final Result result =\n        snapshotMetrics.loadProtocolMetadataTotalDurationTimer.time(\n            () -> loadProtocolAndMetadataInternal(engine, logSegment, lazyCrcInfo));\n\n    TableFeatures.validateKernelCanReadTheTable(result.protocol, dataPath.toString());\n\n    logger.info(\n        \"[{}] Took {}ms to load Protocol and Metadata at version {}, read {} log files\",\n        dataPath,\n        snapshotMetrics.loadProtocolMetadataTotalDurationTimer.totalDurationMs(),\n        logSegment.getVersion(),\n        result.numDeltaFilesRead);\n\n    return result;\n  }\n\n  private static Result loadProtocolAndMetadataInternal(\n      Engine engine, LogSegment logSegment, Lazy<Optional<CRCInfo>> lazyCrcInfo) {\n    final long snapshotVersion = logSegment.getVersion();\n    final Optional<FileStatus> crcFileOpt = logSegment.getLastSeenChecksum();\n    final Optional<Long> crcVersionOpt =\n        crcFileOpt.map(f -> FileNames.checksumVersion(f.getPath()));\n\n    // If CRC is at this exact snapshot version, use it directly\n    if (crcVersionOpt.isPresent() && crcVersionOpt.get() == snapshotVersion) {\n      final Optional<CRCInfo> crcInfo = lazyCrcInfo.get();\n      if (crcInfo.isPresent()) {\n        validateCrcInfoMatchesExpectedVersion(crcInfo.get(), crcVersionOpt.get());\n\n        final Protocol protocol = crcInfo.get().getProtocol();\n        final Metadata metadata = crcInfo.get().getMetadata();\n        return new Result(protocol, metadata, 0 /* logFilesRead */);\n      }\n    }\n\n    // Otherwise, we need to read log files. The CRC (if present) might still be useful to avoid\n    // reading older files.\n\n    long numDeltaFilesRead = 0;\n    Protocol protocol = null;\n    Metadata metadata = null;\n\n    try (CloseableIterator<ActionWrapper> reverseIter =\n        new ActionsIterator(\n            engine,\n            logSegment.allFilesWithCompactionsReversed(),\n            PROTOCOL_METADATA_READ_SCHEMA,\n            Optional.empty())) {\n      while (reverseIter.hasNext()) {\n        final ActionWrapper nextElem = reverseIter.next();\n        final long version = nextElem.getVersion();\n        numDeltaFilesRead++;\n        // Load this lazily (as needed). We may be able to just use the CRC.\n        ColumnarBatch columnarBatch = null;\n\n        if (protocol == null) {\n          columnarBatch = nextElem.getColumnarBatch();\n          assert (columnarBatch.getSchema().equals(PROTOCOL_METADATA_READ_SCHEMA));\n\n          final ColumnVector protocolVector = columnarBatch.getColumnVector(0);\n\n          for (int i = 0; i < protocolVector.getSize(); i++) {\n            if (!protocolVector.isNullAt(i)) {\n              protocol = Protocol.fromColumnVector(protocolVector, i);\n\n              if (metadata != null) {\n                // Stop since we have found the latest Protocol and Metadata.\n                return new Result(protocol, metadata, numDeltaFilesRead);\n              }\n\n              break; // We just found the protocol, exit this for-loop\n            }\n          }\n        }\n\n        if (metadata == null) {\n          if (columnarBatch == null) {\n            columnarBatch = nextElem.getColumnarBatch();\n            assert (columnarBatch.getSchema().equals(PROTOCOL_METADATA_READ_SCHEMA));\n          }\n          final ColumnVector metadataVector = columnarBatch.getColumnVector(1);\n\n          for (int i = 0; i < metadataVector.getSize(); i++) {\n            if (!metadataVector.isNullAt(i)) {\n              metadata = Metadata.fromColumnVector(metadataVector, i);\n\n              if (protocol != null) {\n                // Stop since we have found the latest Protocol and Metadata.\n                return new Result(protocol, metadata, numDeltaFilesRead);\n              }\n\n              break; // We just found the metadata, exit this for-loop\n            }\n          }\n        }\n\n        // Since we haven't returned, then at least one of P or M is null.\n        // Note: Suppose the CRC is at version N. We check the CRC eagerly at N + 1 so\n        //       that we don't read or open any files at version N.\n        if (crcVersionOpt.isPresent() && version == crcVersionOpt.get() + 1) {\n          final Optional<CRCInfo> crcInfo = lazyCrcInfo.get();\n          if (crcInfo.isPresent()) {\n            validateCrcInfoMatchesExpectedVersion(crcInfo.get(), crcVersionOpt.get());\n\n            if (protocol == null) {\n              protocol = crcInfo.get().getProtocol();\n            }\n            if (metadata == null) {\n              metadata = crcInfo.get().getMetadata();\n            }\n\n            return new Result(protocol, metadata, numDeltaFilesRead);\n          }\n        }\n      }\n    } catch (IOException ex) {\n      throw new RuntimeException(\"Could not close iterator\", ex);\n    }\n\n    if (protocol == null) {\n      throw new IllegalStateException(\n          String.format(\"No protocol found at version %s\", logSegment.getVersion()));\n    }\n\n    if (metadata == null) {\n      throw new IllegalStateException(\n          String.format(\"No metadata found at version %s\", logSegment.getVersion()));\n    }\n\n    return new Result(protocol, metadata, numDeltaFilesRead);\n  }\n\n  private static void validateCrcInfoMatchesExpectedVersion(CRCInfo crcInfo, long expectedVersion) {\n    if (crcInfo.getVersion() != expectedVersion) {\n      throw new IllegalStateException(\n          String.format(\n              \"Expected a CRC at version %d but got one at version %d\",\n              expectedVersion, crcInfo.getVersion()));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/rowtracking/MaterializedRowTrackingColumn.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.rowtracking;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.engine.ExpressionHandler;\nimport io.delta.kernel.exceptions.InvalidTableException;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.InternalScanFileUtils;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.SchemaUtils;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/** A collection of helper methods for working with materialized row tracking columns. */\npublic final class MaterializedRowTrackingColumn {\n\n  /** Static instance for the materialized row ID column. */\n  public static final MaterializedRowTrackingColumn MATERIALIZED_ROW_ID =\n      new MaterializedRowTrackingColumn(\n          TableConfig.MATERIALIZED_ROW_ID_COLUMN_NAME, \"_row-id-col-\");\n\n  /** Static instance for the materialized row commit version column. */\n  public static final MaterializedRowTrackingColumn MATERIALIZED_ROW_COMMIT_VERSION =\n      new MaterializedRowTrackingColumn(\n          TableConfig.MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME, \"_row-commit-version-col-\");\n\n  private final TableConfig<String> tableConfig;\n  private final String materializedColumnNamePrefix;\n\n  /** Private constructor to enforce the use of static instances. */\n  private MaterializedRowTrackingColumn(TableConfig<String> tableConfig, String prefix) {\n    this.tableConfig = tableConfig;\n    this.materializedColumnNamePrefix = prefix;\n  }\n\n  /** Returns the configuration property name associated with this materialized column. */\n  public String getMaterializedColumnNameProperty() {\n    return tableConfig.getKey();\n  }\n\n  /** Returns the prefix to use for generating the materialized column name. */\n  public String getMaterializedColumnNamePrefix() {\n    return materializedColumnNamePrefix;\n  }\n\n  /**\n   * Validates that the materialized column names for ROW_ID and ROW_COMMIT_VERSION do not conflict\n   * with any existing logical or physical column names in the table's schema.\n   *\n   * @param metadata The current {@link Metadata} of the table.\n   */\n  public static void throwIfColumnNamesConflictWithSchema(Metadata metadata) {\n    StructType schema = metadata.getSchema();\n    Set<String> logicalColNames =\n        schema.fields().stream().map(StructField::getName).collect(Collectors.toSet());\n    Set<String> physicalColNames =\n        schema.fields().stream().map(ColumnMapping::getPhysicalName).collect(Collectors.toSet());\n\n    Stream.of(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION)\n        .map(column -> metadata.getConfiguration().get(column.getMaterializedColumnNameProperty()))\n        .filter(Objects::nonNull)\n        .forEach(\n            columnName -> {\n              if (logicalColNames.contains(columnName) || physicalColNames.contains(columnName)) {\n                throw DeltaErrors.conflictWithReservedInternalColumnName(columnName);\n              }\n            });\n  }\n\n  /**\n   * Validates that materialized column names for ROW_ID and ROW_COMMIT_VERSION are not missing when\n   * row tracking is enabled. This should be called for existing tables to ensure that row tracking\n   * configs are present when they should be.\n   *\n   * @param metadata The current {@link Metadata} of the table.\n   */\n  public static void validateRowTrackingConfigsNotMissing(Metadata metadata, String tablePath) {\n    // No validation needed if row tracking is disabled\n    if (!TableConfig.ROW_TRACKING_ENABLED.fromMetadata(metadata)) {\n      return;\n    }\n\n    // Check that both materialized column name configs are present when row tracking is enabled\n    Stream.of(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION)\n        .forEach(\n            column -> {\n              if (!metadata\n                  .getConfiguration()\n                  .containsKey(column.getMaterializedColumnNameProperty())) {\n                throw new InvalidTableException(\n                    tablePath,\n                    String.format(\n                        \"Row tracking is enabled but the materialized column name `%s` is missing.\",\n                        column.getMaterializedColumnNameProperty()));\n              }\n            });\n  }\n\n  /**\n   * Assigns materialized column names for ROW_ID and ROW_COMMIT_VERSION if row tracking is enabled\n   * and the column names have not been assigned yet.\n   *\n   * @param metadata The current Metadata of the table.\n   * @return An Optional containing updated metadata if any assignments occurred; Optional.empty()\n   *     otherwise.\n   */\n  public static Optional<Metadata> assignMaterializedColumnNamesIfNeeded(Metadata metadata) {\n    // No assignment if row tracking is disabled\n    if (!TableConfig.ROW_TRACKING_ENABLED.fromMetadata(metadata)) {\n      return Optional.empty();\n    }\n\n    Map<String, String> configsToAdd = new HashMap<>();\n\n    Stream.of(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION)\n        .filter(\n            column ->\n                !metadata\n                    .getConfiguration()\n                    .containsKey(column.getMaterializedColumnNameProperty()))\n        .forEach(\n            column -> {\n              configsToAdd.put(\n                  column.getMaterializedColumnNameProperty(),\n                  column.generateMaterializedColumnName());\n            });\n\n    return configsToAdd.isEmpty()\n        ? Optional.empty()\n        : Optional.of(metadata.withMergedConfiguration(configsToAdd));\n  }\n\n  /**\n   * Converts a logical row tracking field to its physical counterpart(s).\n   *\n   * <p>Since computing the row ID requires the row index, requesting a row tracking column can\n   * require adding two columns to the physical schema.\n   *\n   * <p>Note that we must not mark the physical columns as metadata columns because as far as the\n   * parquet reader is concerned, these columns are not metadata columns.\n   *\n   * @param logicalField The logical field to convert.\n   * @param logicalSchema The logical schema containing the field.\n   * @param metadata The current metadata of the table.\n   * @return A list of {@link StructField}s representing the physical columns corresponding to the\n   *     logical field.\n   */\n  public static List<StructField> convertToPhysicalColumn(\n      StructField logicalField, StructType logicalSchema, Metadata metadata) {\n    if (!TableConfig.ROW_TRACKING_ENABLED.fromMetadata(metadata)) {\n      throw DeltaErrors.missingRowTrackingColumnRequested(logicalField.getName());\n    }\n\n    if (logicalField.getMetadataColumnSpec() == MetadataColumnSpec.ROW_ID) {\n      List<StructField> physicalFields = new ArrayList<>(2);\n      physicalFields.add(\n          new StructField(\n              MATERIALIZED_ROW_ID.getPhysicalColumnName(metadata.getConfiguration()),\n              LongType.LONG,\n              true /* nullable */));\n      if (!logicalSchema.contains(MetadataColumnSpec.ROW_INDEX)) {\n        physicalFields.add(SchemaUtils.asInternalColumn(StructField.DEFAULT_ROW_INDEX_COLUMN));\n      }\n      return physicalFields;\n    } else if (logicalField.getMetadataColumnSpec() == MetadataColumnSpec.ROW_COMMIT_VERSION) {\n      return Collections.singletonList(\n          new StructField(\n              MATERIALIZED_ROW_COMMIT_VERSION.getPhysicalColumnName(metadata.getConfiguration()),\n              LongType.LONG,\n              true /* nullable */));\n    }\n\n    throw new IllegalArgumentException(\n        String.format(\n            \"Logical field `%s` is not a recognized materialized row tracking column.\",\n            logicalField.getName()));\n  }\n\n  /**\n   * Computes row IDs and row commit versions based on their materialized values if present in the\n   * data returned by the Parquet reader, using the base row ID and default row commit version from\n   * the AddFile otherwise.\n   *\n   * @param dataBatch a batch of physical data read from the table.\n   * @param scanFile the {@link Row} representing the scan file metadata.\n   * @param logicalSchema the logical schema of the query.\n   * @param configuration the configuration map containing table metadata.\n   * @param engine the {@link Engine} to use for expression evaluation.\n   * @return a new {@link ColumnarBatch} with logical row tracking columns\n   */\n  public static ColumnarBatch transformPhysicalData(\n      ColumnarBatch dataBatch,\n      Row scanFile,\n      StructType logicalSchema,\n      Map<String, String> configuration,\n      Engine engine) {\n    StructType physicalSchema = dataBatch.getSchema();\n    ExpressionHandler exprHandler = engine.getExpressionHandler();\n\n    // NOTE: We assume that each column is requested at most once in the read schema. This is\n    // consistent with other parts of the codebase.\n    String rowIdColumnName = MATERIALIZED_ROW_ID.getPhysicalColumnName(configuration);\n    int rowIdOrdinal = physicalSchema.indexOf(rowIdColumnName);\n    if (rowIdOrdinal != -1) {\n      dataBatch =\n          transformPhysicalRowId(\n              dataBatch,\n              scanFile,\n              rowIdColumnName,\n              exprHandler,\n              logicalSchema.at(logicalSchema.indexOf(MetadataColumnSpec.ROW_ID)),\n              physicalSchema,\n              rowIdOrdinal);\n    }\n\n    String rowCommitVersionColumnName =\n        MATERIALIZED_ROW_COMMIT_VERSION.getPhysicalColumnName(configuration);\n    int commitVersionOrdinal = physicalSchema.indexOf(rowCommitVersionColumnName);\n    if (commitVersionOrdinal != -1) {\n      dataBatch =\n          transformPhysicalCommitVersion(\n              dataBatch,\n              scanFile,\n              rowCommitVersionColumnName,\n              exprHandler,\n              logicalSchema.at(logicalSchema.indexOf(MetadataColumnSpec.ROW_COMMIT_VERSION)),\n              physicalSchema,\n              commitVersionOrdinal);\n    }\n\n    return dataBatch;\n  }\n\n  /** Row IDs are computed as <code>COALESCE(materializedRowId, baseRowId + rowIndex)</code>. */\n  private static ColumnarBatch transformPhysicalRowId(\n      ColumnarBatch dataBatch,\n      Row scanFile,\n      String rowIdColumnName,\n      ExpressionHandler exprHandler,\n      StructField logicalRowIdColumn,\n      StructType physicalSchema,\n      int rowIdOrdinal) {\n    long baseRowId =\n        InternalScanFileUtils.getBaseRowId(scanFile)\n            .orElseThrow(\n                () ->\n                    DeltaErrors.rowTrackingMetadataMissingInFile(\n                        \"baseRowId\", InternalScanFileUtils.getFilePath(scanFile)));\n    String rowIndexMetadataColName =\n        dataBatch\n            .getSchema()\n            .at(dataBatch.getSchema().indexOf(MetadataColumnSpec.ROW_INDEX))\n            .getName();\n    Expression rowIdExpr =\n        new ScalarExpression(\n            \"COALESCE\",\n            Arrays.asList(\n                new Column(rowIdColumnName),\n                new ScalarExpression(\n                    \"ADD\",\n                    Arrays.asList(\n                        new Column(rowIndexMetadataColName), Literal.ofLong(baseRowId)))));\n    ColumnVector rowIdVector =\n        exprHandler.getEvaluator(physicalSchema, rowIdExpr, LongType.LONG).eval(dataBatch);\n\n    // Remove the materialized row ID column and replace it with the coalesced vector\n    dataBatch =\n        dataBatch\n            .withDeletedColumnAt(rowIdOrdinal)\n            .withNewColumn(rowIdOrdinal, logicalRowIdColumn, rowIdVector);\n    return dataBatch;\n  }\n\n  /**\n   * Row commit versions are computed as <code>COALESCE(materializedRowCommitVersion,\n   * defaultRowCommitVersion)</code>.\n   */\n  private static ColumnarBatch transformPhysicalCommitVersion(\n      ColumnarBatch dataBatch,\n      Row scanFile,\n      String commitVersionColumnName,\n      ExpressionHandler exprHandler,\n      StructField logicalCommitVersionColumn,\n      StructType physicalSchema,\n      int commitVersionOrdinal) {\n    long defaultRowCommitVersion =\n        InternalScanFileUtils.getDefaultRowCommitVersion(scanFile)\n            .orElseThrow(\n                () ->\n                    DeltaErrors.rowTrackingMetadataMissingInFile(\n                        \"defaultRowCommitVersion\", InternalScanFileUtils.getFilePath(scanFile)));\n    Expression commitVersionExpr =\n        new ScalarExpression(\n            \"COALESCE\",\n            Arrays.asList(\n                new Column(commitVersionColumnName), Literal.ofLong(defaultRowCommitVersion)));\n    ColumnVector commitVersionVector =\n        exprHandler.getEvaluator(physicalSchema, commitVersionExpr, LongType.LONG).eval(dataBatch);\n\n    // Remove the materialized row commit version column and replace it with the coalesced vector\n    dataBatch =\n        dataBatch\n            .withDeletedColumnAt(commitVersionOrdinal)\n            .withNewColumn(commitVersionOrdinal, logicalCommitVersionColumn, commitVersionVector);\n    return dataBatch;\n  }\n\n  /**\n   * Gets the physical column name from the table configuration.\n   *\n   * @param configuration the table configuration map\n   * @return the physical column name\n   * @throws IllegalArgumentException if the materialized column name is missing from the\n   *     configuration\n   */\n  public String getPhysicalColumnName(Map<String, String> configuration) {\n    return Optional.ofNullable(configuration.get(getMaterializedColumnNameProperty()))\n        .orElseThrow(\n            () ->\n                new IllegalArgumentException(\n                    String.format(\n                        \"Materialized column name `%s` is missing in the metadata config: %s\",\n                        getMaterializedColumnNameProperty(), configuration)));\n  }\n\n  /** Generates a random name by concatenating the prefix with a random UUID. */\n  private String generateMaterializedColumnName() {\n    return materializedColumnNamePrefix + UUID.randomUUID().toString();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/rowtracking/RowTracking.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.rowtracking;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.*;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.types.MetadataColumnSpec;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.stream.Collectors;\n\n/** A collection of helper methods for working with row tracking. */\npublic class RowTracking {\n  private RowTracking() {\n    // Empty constructor to prevent instantiation of this class\n  }\n\n  /**\n   * Check if row tracking is enabled for reading.\n   *\n   * @param protocol the protocol to check\n   * @param metadata the metadata to check\n   * @return true if row tracking is enabled\n   * @throws IllegalStateException if row tracking is enabled in metadata but not supported by\n   *     protocol\n   */\n  public static boolean isEnabled(Protocol protocol, Metadata metadata) {\n    boolean isEnabled = TableConfig.ROW_TRACKING_ENABLED.fromMetadata(metadata);\n    if (isEnabled && !TableFeatures.isRowTrackingSupported(protocol)) {\n      throw new IllegalStateException(\n          \"Table property 'delta.enableRowTracking' is set on the table but this table version \"\n              + \"doesn't support table feature 'delta.feature.rowTracking'.\");\n    }\n    return isEnabled;\n  }\n\n  /**\n   * Checks if the provided field is a row tracking column, i.e., either the row ID or the row\n   * commit version column.\n   *\n   * @param field the field to check\n   * @return true if the field is a row tracking column, false otherwise\n   */\n  public static boolean isRowTrackingColumn(StructField field) {\n    return field.isMetadataColumn()\n        && (field.getMetadataColumnSpec() == MetadataColumnSpec.ROW_ID\n            || field.getMetadataColumnSpec() == MetadataColumnSpec.ROW_COMMIT_VERSION);\n  }\n\n  /**\n   * Assigns or reassigns baseRowIds and defaultRowCommitVersions to {@link AddFile} actions in the\n   * provided {@code dataActions}. This method should be invoked only when the 'rowTracking' feature\n   * is supported and is used in two scenarios:\n   *\n   * <ol>\n   *   <li>Initial Assignment: Assigns row tracking fields to AddFile actions during commit\n   *       preparation before they are committed.\n   *   <li>Conflict Resolution: Updates row tracking fields when a transaction conflict occurs.\n   *       Since the losing transaction gets a new commit version and winning transactions may have\n   *       increased the row ID high watermark, this method reassigns the fields for the losing\n   *       transaction using the latest state from winning transactions before retrying the commit.\n   * </ol>\n   *\n   * @param txnReadSnapshotOpt the snapshot of the table that this transaction is reading from\n   * @param txnProtocol the (updated, if any) protocol that will result from this txn\n   * @param winningTxnRowIdHighWatermark the latest row ID high watermark from the winning\n   *     transactions. Should be empty for initial assignment and present for conflict resolution.\n   * @param prevCommitVersion the commit version used by this transaction in the previous commit\n   *     attempt. Should be empty for initial assignment and present for conflict resolution.\n   * @param currCommitVersion the transaction's (latest) commit version\n   * @param txnDataActions a {@link CloseableIterable} of data actions this txn is trying to commit\n   * @return a {@link CloseableIterable} of data actions with baseRowIds and\n   *     defaultRowCommitVersions assigned or reassigned\n   */\n  public static CloseableIterable<Row> assignBaseRowIdAndDefaultRowCommitVersion(\n      Optional<SnapshotImpl> txnReadSnapshotOpt,\n      Protocol txnProtocol,\n      Optional<Long> winningTxnRowIdHighWatermark,\n      Optional<Long> prevCommitVersion,\n      long currCommitVersion,\n      CloseableIterable<Row> txnDataActions) {\n    checkArgument(\n        TableFeatures.isRowTrackingSupported(txnProtocol),\n        \"Base row ID and default row commit version are assigned \"\n            + \"only when feature 'rowTracking' is supported.\");\n\n    return new CloseableIterable<Row>() {\n      @Override\n      public void close() throws IOException {\n        txnDataActions.close();\n      }\n\n      @Override\n      public CloseableIterator<Row> iterator() {\n        // The row ID high watermark from the snapshot of the table that this transaction is reading\n        // at. Any baseRowIds higher than this watermark are assigned by this transaction.\n        final long prevRowIdHighWatermark = readRowIdHighWaterMark(txnReadSnapshotOpt);\n\n        // Used to track the current high watermark as we iterate through the data actions and\n        // assign baseRowIds. Use an AtomicLong to allow for updating in the lambda.\n        final AtomicLong currRowIdHighWatermark =\n            new AtomicLong(winningTxnRowIdHighWatermark.orElse(prevRowIdHighWatermark));\n\n        // The row ID high watermark must increase monotonically, so the winning transaction's high\n        // watermark must be greater than or equal to the high watermark from the current\n        // transaction's read snapshot.\n        checkArgument(\n            currRowIdHighWatermark.get() >= prevRowIdHighWatermark,\n            \"The current row ID high watermark must be greater than or equal to \"\n                + \"the high watermark from the transaction's read snapshot\");\n\n        return txnDataActions\n            .iterator()\n            .map(\n                row -> {\n                  // Non-AddFile actions are returned unchanged\n                  if (row.isNullAt(SingleAction.ADD_FILE_ORDINAL)) {\n                    return row;\n                  }\n\n                  AddFile addFile = new AddFile(row.getStruct(SingleAction.ADD_FILE_ORDINAL));\n\n                  // Assign a baseRowId if not present, or update it if previously assigned\n                  // by this transaction\n                  if (!addFile.getBaseRowId().isPresent()\n                      || addFile.getBaseRowId().get() > prevRowIdHighWatermark) {\n                    addFile = addFile.withNewBaseRowId(currRowIdHighWatermark.get() + 1L);\n                    currRowIdHighWatermark.addAndGet(getNumRecordsOrThrow(addFile));\n                  }\n\n                  // Assign a defaultRowCommitVersion if not present, or update it if previously\n                  // assigned by this transaction\n                  if (!addFile.getDefaultRowCommitVersion().isPresent()\n                      || addFile.getDefaultRowCommitVersion().get()\n                          == prevCommitVersion.orElse(-1L)) {\n                    addFile = addFile.withNewDefaultRowCommitVersion(currCommitVersion);\n                  }\n\n                  return SingleAction.createAddFileSingleAction(addFile.toRow());\n                });\n      }\n    };\n  }\n\n  /**\n   * Inserts or updates the {@link DomainMetadata} action reflecting the new row ID high watermark\n   * when this transaction adds rows and pushed it higher.\n   *\n   * <p>This method should only be called when the 'rowTracking' feature is supported. Similar to\n   * {@link #assignBaseRowIdAndDefaultRowCommitVersion}, it should be called during the initial row\n   * ID assignment or conflict resolution to reflect the change to the row ID high watermark.\n   *\n   * @param txnReadSnapshotOpt the snapshot of the table that this transaction is reading at\n   * @param txnProtocol the (updated, if any) protocol that will result from this txn\n   * @param winningTxnRowIdHighWatermark the latest row ID high watermark from the winning\n   *     transaction. Should be empty for initial assignment and present for conflict resolution.\n   * @param txnDataActions a {@link CloseableIterable} of data actions this txn is trying to commit\n   * @param txnDomainMetadatas a list of domain metadata actions this txn is trying to commit\n   * @param providedRowIdHighWatermark Optional row ID high watermark explicitly provided by the\n   *     transaction builder.\n   * @return Updated list of domain metadata actions for commit\n   */\n  public static List<DomainMetadata> updateRowIdHighWatermarkIfNeeded(\n      Optional<SnapshotImpl> txnReadSnapshotOpt,\n      Protocol txnProtocol,\n      Optional<Long> winningTxnRowIdHighWatermark,\n      CloseableIterable<Row> txnDataActions,\n      List<DomainMetadata> txnDomainMetadatas,\n      Optional<Long> providedRowIdHighWatermark) {\n    checkArgument(\n        TableFeatures.isRowTrackingSupported(txnProtocol),\n        \"Row ID high watermark is updated only when feature 'rowTracking' is supported.\");\n    checkArgument(\n        !(providedRowIdHighWatermark.isPresent() && winningTxnRowIdHighWatermark.isPresent()),\n        \"Conflict resolution is not allowed when an explicit row tracking high \"\n            + \"watermark is provided. Please recommit.\");\n\n    // Filter out existing row tracking domainMetadata action, if any\n    List<DomainMetadata> nonRowTrackingDomainMetadatas =\n        txnDomainMetadatas.stream()\n            .filter(dm -> !dm.getDomain().equals(RowTrackingMetadataDomain.DOMAIN_NAME))\n            .collect(Collectors.toList());\n\n    // The row ID high watermark from the snapshot of the table that this transaction is reading at.\n    // Any baseRowIds higher than this watermark are assigned by this transaction.\n    final long prevRowIdHighWatermark = readRowIdHighWaterMark(txnReadSnapshotOpt);\n\n    // Tracks the new row ID high watermark as we iterate through data actions and counting new rows\n    // added in this transaction.\n    final AtomicLong currCalculatedRowIdHighWatermark =\n        new AtomicLong(winningTxnRowIdHighWatermark.orElse(prevRowIdHighWatermark));\n\n    // The row ID high watermark must increase monotonically, so the winning transaction's high\n    // watermark (if present) must be greater than or equal to the high watermark from the\n    // current transaction's read snapshot.\n    checkArgument(\n        currCalculatedRowIdHighWatermark.get() >= prevRowIdHighWatermark,\n        \"The current row ID high watermark must be greater than or equal to \"\n            + \"the high watermark from the transaction's read snapshot\");\n\n    txnDataActions.forEach(\n        row -> {\n          if (!row.isNullAt(SingleAction.ADD_FILE_ORDINAL)) {\n            AddFile addFile = new AddFile(row.getStruct(SingleAction.ADD_FILE_ORDINAL));\n            if (!addFile.getBaseRowId().isPresent()\n                || addFile.getBaseRowId().get() > prevRowIdHighWatermark) {\n              currCalculatedRowIdHighWatermark.addAndGet(getNumRecordsOrThrow(addFile));\n            }\n          }\n        });\n\n    // If the txn builder has explicitly provided a row ID high watermark, we should use that value\n    // instead of the one calculated from the current row ID high watermark and uncommitted data\n    // actions. Validate that the provided value is greater than or equal to the calculated\n    // watermark to\n    // ensure consistency.\n    providedRowIdHighWatermark.ifPresent(\n        providedHighWatermark ->\n            checkArgument(\n                providedHighWatermark >= currCalculatedRowIdHighWatermark.get(),\n                String.format(\n                    \"The provided row ID high watermark (%d) must be greater than or equal to \"\n                        + \"the calculated row ID high watermark (%d) based on the transaction's \"\n                        + \"data actions.\",\n                    providedHighWatermark, currCalculatedRowIdHighWatermark.get())));\n\n    final long finalRowIdHighWatermark =\n        providedRowIdHighWatermark.orElse(currCalculatedRowIdHighWatermark.get());\n\n    if (finalRowIdHighWatermark != prevRowIdHighWatermark) {\n      nonRowTrackingDomainMetadatas.add(\n          new RowTrackingMetadataDomain(finalRowIdHighWatermark).toDomainMetadata());\n    }\n\n    return nonRowTrackingDomainMetadatas;\n  }\n\n  /**\n   * Throws an exception if row tracking enablement is toggled between the old and the new metadata.\n   */\n  public static void throwIfRowTrackingToggled(Metadata oldMetadata, Metadata newMetadata) {\n    boolean oldRowTrackingEnabledValue = TableConfig.ROW_TRACKING_ENABLED.fromMetadata(oldMetadata);\n    boolean newRowTrackingEnabledValue = TableConfig.ROW_TRACKING_ENABLED.fromMetadata(newMetadata);\n    if (oldRowTrackingEnabledValue != newRowTrackingEnabledValue) {\n      throw DeltaErrors.cannotToggleRowTrackingOnExistingTable();\n    }\n  }\n\n  /**\n   * Reads the current row ID high watermark from the snapshot, or returns a default value if\n   * missing.\n   */\n  private static long readRowIdHighWaterMark(Optional<SnapshotImpl> snapshotOpt) {\n    return snapshotOpt\n        .flatMap(RowTrackingMetadataDomain::fromSnapshot)\n        .map(RowTrackingMetadataDomain::getRowIdHighWaterMark)\n        .orElse(RowTrackingMetadataDomain.MISSING_ROW_ID_HIGH_WATERMARK);\n  }\n\n  /**\n   * Get the number of records from the AddFile's statistics. It errors out if statistics are\n   * missing.\n   */\n  private static long getNumRecordsOrThrow(AddFile addFile) {\n    return addFile.getNumRecords().orElseThrow(DeltaErrors::missingNumRecordsStatsForRowTracking);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/rowtracking/RowTrackingMetadataDomain.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.rowtracking;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.metadatadomain.JsonMetadataDomain;\nimport java.util.Optional;\n\n/** Represents the metadata domain for row tracking. */\npublic final class RowTrackingMetadataDomain extends JsonMetadataDomain {\n\n  public static final String DOMAIN_NAME = \"delta.rowTracking\";\n\n  public static final long MISSING_ROW_ID_HIGH_WATERMARK = -1L;\n\n  /**\n   * Creates an instance of {@link RowTrackingMetadataDomain} from a JSON configuration string.\n   *\n   * @param json the JSON configuration string\n   * @return an instance of {@link RowTrackingMetadataDomain}\n   */\n  public static RowTrackingMetadataDomain fromJsonConfiguration(String json) {\n    return JsonMetadataDomain.fromJsonConfiguration(json, RowTrackingMetadataDomain.class);\n  }\n\n  /**\n   * Creates an instance of {@link RowTrackingMetadataDomain} from a {@link SnapshotImpl}.\n   *\n   * @param snapshot the snapshot instance\n   * @return an {@link Optional} containing the {@link RowTrackingMetadataDomain} if present\n   */\n  public static Optional<RowTrackingMetadataDomain> fromSnapshot(SnapshotImpl snapshot) {\n    return JsonMetadataDomain.fromSnapshot(snapshot, RowTrackingMetadataDomain.class, DOMAIN_NAME);\n  }\n\n  /** The highest assigned fresh row id for the table */\n  private final long rowIdHighWaterMark;\n\n  /**\n   * Constructs a RowTrackingMetadataDomain with the specified row ID high water mark.\n   *\n   * @param rowIdHighWaterMark the row ID high water mark\n   */\n  @JsonCreator\n  public RowTrackingMetadataDomain(@JsonProperty(\"rowIdHighWaterMark\") long rowIdHighWaterMark) {\n    this.rowIdHighWaterMark = rowIdHighWaterMark;\n  }\n\n  @Override\n  public String getDomainName() {\n    return DOMAIN_NAME;\n  }\n\n  public long getRowIdHighWaterMark() {\n    return rowIdHighWaterMark;\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (this == obj) return true;\n    if (obj == null || getClass() != obj.getClass()) return false;\n    RowTrackingMetadataDomain that = (RowTrackingMetadataDomain) obj;\n    return rowIdHighWaterMark == that.rowIdHighWaterMark;\n  }\n\n  @Override\n  public int hashCode() {\n    return java.util.Objects.hash(DOMAIN_NAME, rowIdHighWaterMark);\n  }\n\n  @Override\n  public String toString() {\n    return \"RowTrackingMetadataDomain{\" + \"rowIdHighWaterMark=\" + rowIdHighWaterMark + '}';\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/skipping/DataSkippingPredicate.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.skipping;\n\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.CollationIdentifier;\nimport java.util.*;\n\n/** A {@link Predicate} with a set of columns referenced by the expression. */\npublic class DataSkippingPredicate extends Predicate {\n\n  /** Set of {@link Column}s referenced by the predicate or any of its child expressions */\n  private final Set<Column> referencedCols;\n\n  /**\n   * @param name the predicate name\n   * @param children list of expressions that are input to this predicate.\n   * @param referencedCols set of columns referenced by this predicate or any of its child\n   *     expressions\n   */\n  DataSkippingPredicate(String name, List<Expression> children, Set<Column> referencedCols) {\n    super(name, children);\n    this.referencedCols = Collections.unmodifiableSet(referencedCols);\n  }\n\n  /**\n   * @param name the predicate name\n   * @param children list of expressions that are input to this predicate.\n   * @param collationIdentifier collation identifier used for this predicate\n   * @param referencedCols set of columns referenced by this predicate or any of its child\n   *     expressions\n   */\n  DataSkippingPredicate(\n      String name,\n      List<Expression> children,\n      CollationIdentifier collationIdentifier,\n      Set<Column> referencedCols) {\n    super(name, children, collationIdentifier);\n    this.referencedCols = Collections.unmodifiableSet(referencedCols);\n  }\n\n  /**\n   * Constructor for a binary {@link DataSkippingPredicate} where both children are instances of\n   * {@link DataSkippingPredicate}.\n   *\n   * @param name the predicate name\n   * @param left left input to this predicate\n   * @param right right input to this predicate\n   */\n  DataSkippingPredicate(String name, DataSkippingPredicate left, DataSkippingPredicate right) {\n    super(name, Arrays.asList(left, right));\n    this.referencedCols = immutableUnion(left.referencedCols, right.referencedCols);\n  }\n\n  /** @return set of columns referenced by this predicate or any of its child expressions */\n  public Set<Column> getReferencedCols() {\n    return referencedCols;\n  }\n\n  /**\n   * @return set of collation identifiers referenced by this predicate or any of its child\n   *     expressions\n   */\n  public Set<CollationIdentifier> getReferencedCollations() {\n    Set<CollationIdentifier> referencedCollations = new HashSet<>();\n\n    if (this.getCollationIdentifier().isPresent()) {\n      referencedCollations.add(this.getCollationIdentifier().get());\n    }\n\n    for (Expression child : children) {\n      if (child instanceof DataSkippingPredicate) {\n        referencedCollations.addAll(((DataSkippingPredicate) child).getReferencedCollations());\n      } else if (child instanceof Predicate) {\n        throw new IllegalStateException(\n            String.format(\n                \"Expected child Predicate of DataSkippingPredicate to be an instance of\"\n                    + \" DataSkippingPredicate but found: %s\",\n                child, this));\n      }\n    }\n    return Collections.unmodifiableSet(referencedCollations);\n  }\n\n  /** @return an unmodifiable set containing all elements from both sets. */\n  private <T> Set<T> immutableUnion(Set<T> set1, Set<T> set2) {\n    return Collections.unmodifiableSet(\n        new HashSet<T>() {\n          {\n            addAll(set1);\n            addAll(set2);\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/skipping/DataSkippingUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.skipping;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineException;\nimport static io.delta.kernel.internal.InternalScanFileUtils.ADD_FILE_ORDINAL;\nimport static io.delta.kernel.internal.InternalScanFileUtils.ADD_FILE_STATS_ORDINAL;\nimport static io.delta.kernel.internal.util.ExpressionUtils.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.CollationIdentifier;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\nimport java.util.function.BiFunction;\n\npublic class DataSkippingUtils {\n\n  /**\n   * Given a {@code FilteredColumnarBatch} of scan files and the statistics schema to parse, return\n   * the parsed JSON stats from the scan files.\n   */\n  public static ColumnarBatch parseJsonStats(\n      Engine engine, FilteredColumnarBatch scanFileBatch, StructType statsSchema) {\n    ColumnVector statsVector =\n        scanFileBatch.getData().getColumnVector(ADD_FILE_ORDINAL).getChild(ADD_FILE_STATS_ORDINAL);\n    return wrapEngineException(\n        () ->\n            engine\n                .getJsonHandler()\n                .parseJson(statsVector, statsSchema, scanFileBatch.getSelectionVector()),\n        \"Parsing the JSON statistics with statsSchema=%s\",\n        statsSchema);\n  }\n\n  /**\n   * Prunes the given schema to only include the referenced leaf columns. If a leaf column is a\n   * nested column it must be referenced using the full column path, e.g. \"C_0.C_1.C_leaf\"\n   *\n   * @param schema the schema to prune\n   * @param referencedLeafCols set of leaf columns in {@code schema}\n   */\n  public static StructType pruneStatsSchema(StructType schema, Set<Column> referencedLeafCols) {\n    return pruneSchema(referencedLeafCols, schema, new String[0]);\n  }\n\n  /**\n   * Constructs a data skipping filter to prune files using column statistics given a query data\n   * filter if possible. The returned filter will evaluate to FALSE for any files that can be safely\n   * skipped. If the filter evaluates to NULL or TRUE, the file should not be skipped.\n   *\n   * @param dataFilters query filters on the data columns\n   * @param dataSchema the data schema of the table\n   * @return data skipping filter to prune files if it exists as a {@link DataSkippingPredicate}\n   */\n  public static Optional<DataSkippingPredicate> constructDataSkippingFilter(\n      Predicate dataFilters, StructType dataSchema) {\n    StatsSchemaHelper schemaHelper = new StatsSchemaHelper(dataSchema);\n    return constructDataSkippingFilter(dataFilters, schemaHelper);\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Helper functions\n  //////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Returns a file skipping predicate expression, derived from the user query, which uses column\n   * statistics to prune away files that provably contain no rows the query cares about.\n   *\n   * <p>Specifically, the filter extraction code must obey the following rules:\n   *\n   * <p>1. Given a query predicate `e`, `constructDataSkippingFilter(e)` must return TRUE for a file\n   * unless we can prove `e` will not return TRUE for any row the file might contain. For example,\n   * given `a = 3` and min/max stat values [0, 100], this skipping predicate is safe:\n   *\n   * <p>AND(minValues.a <= 3, maxValues.a >= 3)\n   *\n   * <p>Because that condition must be true for any file that might possibly contain `a = 3`; the\n   * skipping predicate could return FALSE only if the max is too low, or the min too high; it could\n   * return NULL only if a is NULL in every row of the file. In both latter cases, it is safe to\n   * skip the file because `a = 3` can never evaluate to TRUE.\n   *\n   * <p>2. It is unsafe to apply skipping to operators that can evaluate to NULL or produce an error\n   * for non-NULL inputs. For example, consider this query predicate involving integer addition:\n   *\n   * <p>a + 1 = 3\n   *\n   * <p>It might be tempting to apply the standard equality skipping predicate:\n   *\n   * <p>AND(minValues.a + 1 <= 3, 3 <= maxValues.a + 1)\n   *\n   * <p>However, the skipping predicate would be unsound, because the addition operator could\n   * trigger integer overflow (e.g. minValues.a = 0 and maxValues.a = INT_MAX), even though the file\n   * could very well contain rows satisfying a + 1 = 3.\n   *\n   * <p>3. Predicates involving NOT are ineligible for skipping, because\n   * `Not(constructDataSkippingFilter(e))` is seldom equivalent to `constructDataSkippingFilter\n   * (Not(e))`. For example, consider the query predicate:\n   *\n   * <p>NOT(a = 1)\n   *\n   * <p>A simple inversion of the data skipping predicate would be:\n   *\n   * <p>NOT(AND(minValues.a <= 1, maxValues.a >= 1)) ==> OR(NOT(minValues.a <= 1), NOT(maxValues.a\n   * >= 1)) ==> OR(minValues.a > 1, maxValues.a < 1)\n   *\n   * <p>By contrast, if we first combine the NOT with = to obtain\n   *\n   * <p>a != 1\n   *\n   * <p>We get a different skipping predicate:\n   *\n   * <p>NOT(AND(minValues.a = 1, maxValues.a = 1)) ==> OR(NOT(minValues.a = 1), NOT(maxValues.a =\n   * 1)) ==> OR(minValues.a != 1, maxValues.a != 1)\n   *\n   * <p>A truth table confirms that the first (naively inverted) skipping predicate is incorrect:\n   *\n   * <p>minValues.a | maxValues.a | | OR(minValues.a > 1, maxValues.a < 1) | | | OR(minValues.a !=\n   * 1, maxValues.a != 1) 0 0 T T 0 1 F T !! first predicate wrongly skipped a = 0 1 1 F F\n   *\n   * <p>Fortunately, we may be able to eliminate NOT from some (branches of some) predicates:\n   *\n   * <p>a. It is safe to push the NOT into the children of AND and OR using de Morgan's Law, e.g.\n   *\n   * <p>NOT(AND(a, b)) ==> OR(NOT(a), NOT(B)).\n   *\n   * <p>b. It is safe to fold NOT into other operators, when a negated form of the operator exists:\n   *\n   * <p>NOT(NOT(x)) ==> x NOT(a == b) ==> a != b NOT(a > b) ==> a <= b\n   *\n   * <p>NOTE: The skipping predicate must handle the case where min and max stats for a column are\n   * both NULL -- which indicates that all values in the file are NULL. Fortunately, most of the\n   * operators we support data skipping for are NULL intolerant, and thus trivially satisfy this\n   * requirement because they never return TRUE for NULL inputs. The only NULL tolerant operator we\n   * support -- IS [NOT] NULL -- is specifically NULL aware. The predicate evaluates to NULL if any\n   * required statistics are missing.\n   */\n  private static Optional<DataSkippingPredicate> constructDataSkippingFilter(\n      Predicate dataFilters, StatsSchemaHelper schemaHelper) {\n\n    switch (dataFilters.getName().toUpperCase(Locale.ROOT)) {\n\n        // Push skipping predicate generation through the AND:\n        //\n        // constructDataSkippingFilter(AND(a, b))\n        // ==> AND(constructDataSkippingFilter(a), constructDataSkippingFilter(b))\n        //\n        // To see why this transformation is safe, consider that\n        // `constructDataSkippingFilter(a)` must evaluate to TRUE *UNLESS* we can prove that\n        // `a` would not evaluate to TRUE for any\n        // row the file might contain. Thus, if the rewritten form of the skipping predicate\n        // does not evaluate to TRUE, at least one of the skipping predicates must not have\n        // evaluated to TRUE, which in turn means we were able to prove that `a` and/or `b`\n        // will not evaluate to TRUE for any row of the file. If that is the case, then\n        // `AND(a, b)` also cannot evaluate to TRUE for any row of the file, which proves we\n        // have a valid data skipping predicate.\n        //\n        // NOTE: AND is special -- we can safely skip the file if one leg does not evaluate to\n        // TRUE, even if we cannot construct a skipping filter for the other leg.\n      case \"AND\":\n        Optional<DataSkippingPredicate> e1AndFilter =\n            constructDataSkippingFilter(asPredicate(getLeft(dataFilters)), schemaHelper);\n        Optional<DataSkippingPredicate> e2AndFilter =\n            constructDataSkippingFilter(asPredicate(getRight(dataFilters)), schemaHelper);\n        if (e1AndFilter.isPresent() && e2AndFilter.isPresent()) {\n          return Optional.of(\n              new DataSkippingPredicate(\"AND\", e1AndFilter.get(), e2AndFilter.get()));\n        } else if (e1AndFilter.isPresent()) {\n          return e1AndFilter;\n        } else {\n          return e2AndFilter; // possibly none\n        }\n\n        // Push skipping predicate generation through OR (similar to AND case).\n        //\n        // constructDataFilters(OR(a, b))\n        // ==> OR(constructDataFilters(a), constructDataFilters(b))\n        //\n        // Similar to AND case, if the rewritten predicate does not evaluate to TRUE, then it\n        // means that neither `constructDataFilters(a)` nor `constructDataFilters(b)` evaluated\n        // to TRUE, which in turn means that neither `a` nor `b` could evaluate to TRUE for any\n        // row the file might contain, which proves we have a valid data skipping predicate.\n        //\n        // Unlike AND, a single leg of an OR expression provides no filtering power -- we can\n        // only reject a file if both legs evaluate to false.\n      case \"OR\":\n        Optional<DataSkippingPredicate> e1OrFilter =\n            constructDataSkippingFilter(asPredicate(getLeft(dataFilters)), schemaHelper);\n        Optional<DataSkippingPredicate> e2OrFilter =\n            constructDataSkippingFilter(asPredicate(getRight(dataFilters)), schemaHelper);\n        if (e1OrFilter.isPresent() && e2OrFilter.isPresent()) {\n          return Optional.of(new DataSkippingPredicate(\"OR\", e1OrFilter.get(), e2OrFilter.get()));\n        } else {\n          return Optional.empty();\n        }\n\n        // Match any file whose null count is less than the row count.\n      case \"IS_NOT_NULL\":\n        Expression child = getUnaryChild(dataFilters);\n        if (child instanceof Column) {\n          Column childColumn = (Column) child;\n          if (schemaHelper.isSkippingEligibleNullCountColumn((Column) child)) {\n            Column nullCountCol = schemaHelper.getNullCountColumn(childColumn);\n            Column numRecordsCol = schemaHelper.getNumRecordsColumn();\n            return Optional.of(\n                new DataSkippingPredicate(\n                    \"<\",\n                    Arrays.asList(nullCountCol, numRecordsCol),\n                    new HashSet<Column>() {\n                      {\n                        add(nullCountCol);\n                        add(numRecordsCol);\n                      }\n                    }));\n          }\n        }\n        break;\n\n        // Match any file whose null count is larger than zero.\n        // Note DVs might result in a redundant read of a file.\n        // However, they cannot lead to a correctness issue.\n      case \"IS_NULL\":\n        Expression unaryChild = getUnaryChild(dataFilters);\n        if (unaryChild instanceof Column) {\n          Column childColumn = (Column) unaryChild;\n          if (schemaHelper.isSkippingEligibleNullCountColumn((Column) unaryChild)) {\n            Column nullCountCol = schemaHelper.getNullCountColumn(childColumn);\n            Literal zero = Literal.ofLong(0);\n            return Optional.of(\n                new DataSkippingPredicate(\n                    \">\", Arrays.asList(nullCountCol, zero), Collections.singleton(nullCountCol)));\n          }\n        }\n        break;\n\n      case \"=\":\n      case \"<\":\n      case \"<=\":\n      case \">\":\n      case \">=\":\n      case \"IS NOT DISTINCT FROM\":\n        Expression left = getLeft(dataFilters);\n        Expression right = getRight(dataFilters);\n        Optional<CollationIdentifier> collationIdentifier = dataFilters.getCollationIdentifier();\n        if (collationIdentifier\n            .filter(ci -> !ci.isSparkUTF8BinaryCollation() && ci.getVersion().isEmpty())\n            .isPresent()) {\n          // Each collated statistics is stored with a specific version, so collation must specify a\n          // version to be used for data skipping.\n          return Optional.empty();\n        }\n\n        if (left instanceof Column && right instanceof Literal) {\n          Column leftCol = (Column) left;\n          Literal rightLit = (Literal) right;\n          if (schemaHelper.isSkippingEligibleMinMaxColumn(leftCol)\n              && schemaHelper.isSkippingEligibleLiteral(rightLit)) {\n            return constructComparatorDataSkippingFilters(\n                dataFilters.getName(), leftCol, rightLit, collationIdentifier, schemaHelper);\n          }\n        } else if (right instanceof Column && left instanceof Literal) {\n          return constructDataSkippingFilter(reverseComparatorFilter(dataFilters), schemaHelper);\n        }\n        break;\n\n      case \"NOT\":\n        return constructNotDataSkippingFilters(\n            asPredicate(getUnaryChild(dataFilters)), schemaHelper);\n\n        // TODO more expressions\n    }\n    return Optional.empty();\n  }\n\n  /** Construct the skipping predicate for a given comparator */\n  private static Optional<DataSkippingPredicate> constructComparatorDataSkippingFilters(\n      String comparator,\n      Column leftCol,\n      Literal rightLit,\n      Optional<CollationIdentifier> collationIdentifier,\n      StatsSchemaHelper schemaHelper) {\n\n    switch (comparator.toUpperCase(Locale.ROOT)) {\n\n        // Match any file whose min/max range contains the requested point.\n      case \"=\":\n        // For example a = 1 --> minValue.a <= 1 AND maxValue.a >= 1\n        return Optional.of(\n            new DataSkippingPredicate(\n                \"AND\",\n                constructBinaryDataSkippingPredicate(\n                    \"<=\",\n                    schemaHelper.getMinColumn(leftCol, collationIdentifier),\n                    rightLit,\n                    collationIdentifier),\n                constructBinaryDataSkippingPredicate(\n                    \">=\",\n                    schemaHelper.getMaxColumn(leftCol, collationIdentifier),\n                    rightLit,\n                    collationIdentifier)));\n\n        // Match any file whose min is less than the requested upper bound.\n      case \"<\":\n        return Optional.of(\n            constructBinaryDataSkippingPredicate(\n                \"<\",\n                schemaHelper.getMinColumn(leftCol, collationIdentifier),\n                rightLit,\n                collationIdentifier));\n\n        // Match any file whose min is less than or equal to the requested upper bound\n      case \"<=\":\n        return Optional.of(\n            constructBinaryDataSkippingPredicate(\n                \"<=\",\n                schemaHelper.getMinColumn(leftCol, collationIdentifier),\n                rightLit,\n                collationIdentifier));\n\n        // Match any file whose max is larger than the requested lower bound.\n      case \">\":\n        return Optional.of(\n            constructBinaryDataSkippingPredicate(\n                \">\",\n                schemaHelper.getMaxColumn(leftCol, collationIdentifier),\n                rightLit,\n                collationIdentifier));\n\n        // Match any file whose max is larger than or equal to the requested lower bound.\n      case \">=\":\n        return Optional.of(\n            constructBinaryDataSkippingPredicate(\n                \">=\",\n                schemaHelper.getMaxColumn(leftCol, collationIdentifier),\n                rightLit,\n                collationIdentifier));\n      case \"IS NOT DISTINCT FROM\":\n        return constructDataSkippingFilter(\n            rewriteEqualNullSafe(leftCol, rightLit, collationIdentifier), schemaHelper);\n      default:\n        throw new IllegalArgumentException(\n            String.format(\"Unsupported comparator expression %s\", comparator));\n    }\n  }\n\n  /**\n   * Constructs a {@link DataSkippingPredicate} for a binary predicate expression with a left\n   * column, an optional column adjustment expression and a right expression of type {@link\n   * Literal}.\n   */\n  private static DataSkippingPredicate constructBinaryDataSkippingPredicate(\n      String exprName,\n      Tuple2<Column, Optional<Expression>> colExpr,\n      Literal lit,\n      Optional<CollationIdentifier> collationIdentifier) {\n    Column column = colExpr._1;\n    Expression adjColExpr = colExpr._2.isPresent() ? colExpr._2.get() : column;\n    if (collationIdentifier.isPresent()) {\n      return new DataSkippingPredicate(\n          exprName,\n          Arrays.asList(adjColExpr, lit),\n          collationIdentifier.get(),\n          Collections.singleton(column));\n    } else {\n      return new DataSkippingPredicate(\n          exprName, Arrays.asList(adjColExpr, lit), Collections.singleton(column));\n    }\n  }\n\n  private static final Map<String, String> REVERSE_COMPARATORS =\n      new HashMap<String, String>() {\n        {\n          put(\"=\", \"=\");\n          put(\"<\", \">\");\n          put(\"<=\", \">=\");\n          put(\">\", \"<\");\n          put(\">=\", \"<=\");\n          put(\"IS NOT DISTINCT FROM\", \"IS NOT DISTINCT FROM\");\n        }\n      };\n\n  private static Predicate reverseComparatorFilter(Predicate predicate) {\n    return createPredicate(\n        REVERSE_COMPARATORS.get(predicate.getName().toUpperCase(Locale.ROOT)),\n        getRight(predicate),\n        getLeft(predicate),\n        predicate.getCollationIdentifier());\n  }\n\n  /** Construct the skipping predicate for a NOT expression child if possible */\n  private static Optional<DataSkippingPredicate> constructNotDataSkippingFilters(\n      Predicate childPredicate, StatsSchemaHelper schemaHelper) {\n    Optional<CollationIdentifier> collationIdentifier = childPredicate.getCollationIdentifier();\n    switch (childPredicate.getName().toUpperCase(Locale.ROOT)) {\n        // Use deMorgan's law to push the NOT past the AND. This is safe even with SQL\n        // tri-valued logic (see below), and is desirable because we cannot generally push\n        // predicate filters\n        // through NOT, but we *CAN* push predicate filters through AND and OR:\n        //\n        // constructDataFilters(NOT(AND(a, b)))\n        // ==> constructDataFilters(OR(NOT(a), NOT(b)))\n        // ==> OR(constructDataFilters(NOT(a)), constructDataFilters(NOT(b)))\n        //\n        // Assuming we can push the resulting NOT operations all the way down to some leaf\n        // operation it can fold into, the rewrite allows us to create a data skipping filter\n        // from the expression.\n        //\n        // a b AND(a, b)\n        // | | | NOT(AND(a, b))\n        // | | | | OR(NOT(a), NOT(b))\n        // T T T F F\n        // T F F T T\n        // T N N N N\n        // F F F T T\n        // F N F T T\n        // N N N N N\n      case \"AND\":\n        return constructDataSkippingFilter(\n            new Or(\n                new Predicate(\"NOT\", asPredicate(getLeft(childPredicate))),\n                new Predicate(\"NOT\", asPredicate(getRight(childPredicate)))),\n            schemaHelper);\n\n        // Similar to AND, we can (and want to) push the NOT past the OR using deMorgan's law.\n      case \"OR\":\n        return constructDataSkippingFilter(\n            new And(\n                new Predicate(\"NOT\", asPredicate(getLeft(childPredicate))),\n                new Predicate(\"NOT\", asPredicate(getRight(childPredicate)))),\n            schemaHelper);\n\n      case \"IS_NOT_NULL\":\n        return constructDataSkippingFilter(\n            new Predicate(\"IS_NULL\", getUnaryChild(childPredicate)), schemaHelper);\n\n      case \"IS_NULL\":\n        return constructDataSkippingFilter(\n            new Predicate(\"IS_NOT_NULL\", getUnaryChild(childPredicate)), schemaHelper);\n\n      case \"=\":\n        return constructDataSkippingFiltersForNotEqual(\n            childPredicate,\n            schemaHelper,\n            (leftColumn, rightLiteral) -> {\n              // Match any file whose min/max range contains anything other than the\n              // rejected point.\n              // For example a != 1 --> minValue.a < 1 OR maxValue.a > 1\n              return Optional.of(\n                  new DataSkippingPredicate(\n                      \"OR\",\n                      constructBinaryDataSkippingPredicate(\n                          \"<\",\n                          schemaHelper.getMinColumn(leftColumn, collationIdentifier),\n                          rightLiteral,\n                          collationIdentifier),\n                      constructBinaryDataSkippingPredicate(\n                          \">\",\n                          schemaHelper.getMaxColumn(leftColumn, collationIdentifier),\n                          rightLiteral,\n                          collationIdentifier)));\n            });\n      case \"<\":\n        return constructDataSkippingFilter(\n            createPredicate(\">=\", childPredicate.getChildren(), collationIdentifier), schemaHelper);\n      case \"<=\":\n        return constructDataSkippingFilter(\n            createPredicate(\">\", childPredicate.getChildren(), collationIdentifier), schemaHelper);\n      case \">\":\n        return constructDataSkippingFilter(\n            createPredicate(\"<=\", childPredicate.getChildren(), collationIdentifier), schemaHelper);\n      case \">=\":\n        return constructDataSkippingFilter(\n            createPredicate(\"<\", childPredicate.getChildren(), collationIdentifier), schemaHelper);\n      case \"IS NOT DISTINCT FROM\":\n        return constructDataSkippingFiltersForNotEqual(\n            childPredicate,\n            schemaHelper,\n            (leftColumn, rightLiteral) ->\n                constructDataSkippingFilter(\n                    new Predicate(\n                        \"NOT\", rewriteEqualNullSafe(leftColumn, rightLiteral, collationIdentifier)),\n                    schemaHelper));\n      case \"NOT\":\n        // Remove redundant pairs of NOT\n        return constructDataSkippingFilter(\n            asPredicate(getUnaryChild(childPredicate)), schemaHelper);\n    }\n    return Optional.empty();\n  }\n\n  /**\n   * Prunes the given schema to include only the referenced leaf columns to keep. These leaf columns\n   * (possible nested) are relative to the root schema, not to the current level of recursion.\n   * {@code leafColumnsToKeep} is unchanged at any level of recursion.\n   *\n   * <p>For example consider the following schema:\n   *\n   * <pre>\n   * |--level1_struct: struct\n   * |   |--level2_struct: struct\n   * |       |--level3_struct: struct\n   * |           |--level_4_col: int\n   * |       |--level_3_col: int\n   * </pre>\n   *\n   * At the second level of recursion on field {@code level2_struct} we would have parameters\n   *\n   * <ol>\n   *   <li>leafColumnsToKeep=Set(Column(level1_struct.level2_struct.level_3_col))\n   *   <li>schema:\n   *       <pre>\n   *          |--level3_struct: struct\n   *          |   |--level_4_col: int\n   *          |--level_3_col: int\n   *          </pre>\n   *   <li>parentPath=[\"level1_struct\", \"level2_struct\"]\n   * </ol>\n   *\n   * @param leafColumnsToKeep set of leaf columns relative to the schema root\n   * @param schema schema to prune\n   * @param parentPath parent path of the fields in {@code schema} relative to the schema root\n   */\n  private static StructType pruneSchema(\n      Set<Column> leafColumnsToKeep, StructType schema, String[] parentPath) {\n    List<StructField> prunedFields = new ArrayList<>();\n    for (StructField field : schema.fields()) {\n      String[] colPath = appendArray(parentPath, field.getName());\n      if (field.getDataType() instanceof StructType) {\n        StructType prunedNestedSchema =\n            pruneSchema(leafColumnsToKeep, (StructType) field.getDataType(), colPath);\n        if (prunedNestedSchema.length() > 0) {\n          // Only add a struct field it has un-pruned nested columns\n          prunedFields.add(\n              new StructField(\n                  field.getName(), prunedNestedSchema, field.isNullable(), field.getMetadata()));\n        }\n      } else {\n        if (leafColumnsToKeep.contains(new Column(colPath))) {\n          prunedFields.add(field);\n        }\n      }\n    }\n    return new StructType(prunedFields);\n  }\n\n  /**\n   * Given an array {@code arr} and a string element {@code appendElem} return a new array with\n   * {@code appendElem} inserted at the end\n   */\n  private static String[] appendArray(String[] arr, String appendElem) {\n    String[] newNames = new String[arr.length + 1];\n    System.arraycopy(arr, 0, newNames, 0, arr.length);\n    newNames[arr.length] = appendElem;\n    return newNames;\n  }\n\n  /**\n   * Rewrite `EqualNullSafe(a, NotNullLiteral)` as `And(IsNotNull(a), EqualTo(a, NotNullLiteral))`\n   * and rewrite `EqualNullSafe(a, null)` as `IsNull(a)`\n   */\n  private static Predicate rewriteEqualNullSafe(\n      Column leftCol, Literal rightLit, Optional<CollationIdentifier> collationIdentifier) {\n    if (rightLit.getValue() == null) {\n      return new Predicate(\"IS_NULL\", leftCol);\n    }\n    return new Predicate(\n        \"AND\",\n        new Predicate(\"IS_NOT_NULL\", leftCol),\n        createPredicate(\"=\", leftCol, rightLit, collationIdentifier));\n  }\n\n  /** Helper method for building DataSkippingPredicate for NOT =/IS NOT DISTINCT FROM */\n  private static Optional<DataSkippingPredicate> constructDataSkippingFiltersForNotEqual(\n      Predicate equalPredicate,\n      StatsSchemaHelper schemaHelper,\n      BiFunction<Column, Literal, Optional<DataSkippingPredicate>> buildDataSkippingPredicateFunc) {\n    checkArgument(\n        \"=\".equals(equalPredicate.getName())\n            || \"IS NOT DISTINCT FROM\".equals(equalPredicate.getName()),\n        \"Expects predicate to be = or IS NOT DISTINCT FROM\");\n    Expression leftChild = getLeft(equalPredicate);\n    Expression rightChild = getRight(equalPredicate);\n    Optional<CollationIdentifier> collationIdentifier = equalPredicate.getCollationIdentifier();\n    if (rightChild instanceof Column && leftChild instanceof Literal) {\n      return constructDataSkippingFilter(\n          new Predicate(\n              \"NOT\",\n              createPredicate(\n                  equalPredicate.getName(), rightChild, leftChild, collationIdentifier)),\n          schemaHelper);\n    }\n    if (leftChild instanceof Column && rightChild instanceof Literal) {\n      Column leftCol = (Column) leftChild;\n      Literal rightLit = (Literal) rightChild;\n      if (schemaHelper.isSkippingEligibleMinMaxColumn(leftCol)\n          && schemaHelper.isSkippingEligibleLiteral(rightLit)) {\n        return buildDataSkippingPredicateFunc.apply(leftCol, rightLit);\n      }\n    }\n    return Optional.empty();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/skipping/StatsSchemaHelper.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.skipping;\n\nimport static io.delta.kernel.internal.util.ColumnMapping.getPhysicalName;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.ScalarExpression;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n * Provides information and utilities for statistics columns given a table schema. Specifically, it\n * is used to:\n *\n * <ol>\n *   <li>Get the expected statistics schema given a table schema\n *   <li>Check if a {@link Literal} or {@link Column} is skipping eligible\n *   <li>Get the statistics column for a given stat type and logical column\n * </ol>\n */\npublic class StatsSchemaHelper {\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Public static fields and methods\n  //////////////////////////////////////////////////////////////////////////////////\n\n  /* Delta statistics field names for file statistics */\n  public static final String NUM_RECORDS = \"numRecords\";\n  public static final String MIN = \"minValues\";\n  public static final String MAX = \"maxValues\";\n  public static final String NULL_COUNT = \"nullCount\";\n  public static final String TIGHT_BOUNDS = \"tightBounds\";\n  public static final String STATS_WITH_COLLATION = \"statsWithCollation\";\n\n  /**\n   * Returns true if the given literal is skipping-eligible. Delta tracks min/max stats for a\n   * limited set of data types and only literals of those types are skipping eligible.\n   */\n  public static boolean isSkippingEligibleLiteral(Literal literal) {\n    return isSkippingEligibleDataType(literal.getDataType());\n  }\n\n  /** Returns true if the given data type is eligible for MIN/MAX data skipping. */\n  public static boolean isSkippingEligibleDataType(DataType dataType) {\n    return SKIPPING_ELIGIBLE_TYPE_NAMES.contains(dataType.toString())\n        ||\n        // DecimalType is eligible, but since its string includes scale + precision, it needs to\n        // be matched separately.\n        dataType instanceof DecimalType\n        ||\n        // StringType is eligible, but since its string can include collation info, it needs to\n        // be matched separately.\n        dataType instanceof StringType;\n  }\n\n  /**\n   * Returns the expected statistics schema given a table schema.\n   *\n   * <p>Here is an example of a data schema along with the schema of the statistics that would be\n   * collected.\n   *\n   * <p>Data Schema:\n   *\n   * <pre>\n   * |-- a: struct (nullable = true)\n   * |  |-- b: struct (nullable = true)\n   * |  |  |-- c: long (nullable = true)\n   * |  |  |-- d: string (nullable = true)\n   * </pre>\n   *\n   * <p>Collected Statistics:\n   *\n   * <pre>\n   * |-- stats: struct (nullable = true)\n   * |  |-- numRecords: long (nullable = false)\n   * |  |-- minValues: struct (nullable = false)\n   * |  |  |-- a: struct (nullable = false)\n   * |  |  |  |-- b: struct (nullable = false)\n   * |  |  |  |  |-- c: long (nullable = true)\n   * |  |  |  |  |-- d: string (nullable = true)\n   * |  |-- maxValues: struct (nullable = false)\n   * |  |  |-- a: struct (nullable = false)\n   * |  |  |  |-- b: struct (nullable = false)\n   * |  |  |  |  |-- c: long (nullable = true)\n   * |  |  |  |  |-- d: string (nullable = true)\n   * |  |-- nullCount: struct (nullable = false)\n   * |  |  |-- a: struct (nullable = false)\n   * |  |  |  |-- b: struct (nullable = false)\n   * |  |  |  |  |-- c: long (nullable = true)\n   * |  |  |  |  |-- d: string (nullable = true)\n   * |  |-- tightBounds: boolean (nullable = true)\n   * |  |-- statsWithCollation: struct (nullable = true)\n   * |  |  |-- collationName: struct (nullable = true)\n   * |  |  |  |-- min: struct (nullable = true)\n   * |  |  |  |  |-- a: struct (nullable = true)\n   * |  |  |  |  |  |-- b: struct (nullable = true)\n   * |  |  |  |  |  |  |-- d: string (nullable = true)\n   * |  |  |  |-- max: struct (nullable = true)\n   * |  |  |  |  |-- a: struct (nullable = true)\n   * |  |  |  |  |  |-- b: struct (nullable = true)\n   * |  |  |  |  |  |  |-- d: string (nullable = true)\n   * </pre>\n   */\n  public static StructType getStatsSchema(\n      StructType dataSchema, Set<CollationIdentifier> collationIdentifiers) {\n    StructType statsSchema = new StructType().add(NUM_RECORDS, LongType.LONG, true);\n\n    StructType minMaxStatsSchema = getMinMaxStatsSchema(dataSchema);\n    if (minMaxStatsSchema.length() > 0) {\n      statsSchema = statsSchema.add(MIN, minMaxStatsSchema, true).add(MAX, minMaxStatsSchema, true);\n    }\n\n    StructType nullCountSchema = getNullCountSchema(dataSchema);\n    if (nullCountSchema.length() > 0) {\n      statsSchema = statsSchema.add(NULL_COUNT, nullCountSchema, true);\n    }\n\n    statsSchema = statsSchema.add(TIGHT_BOUNDS, BooleanType.BOOLEAN, true);\n\n    StructType collatedMinMaxStatsSchema = getCollatedStatsSchema(dataSchema, collationIdentifiers);\n    if (collatedMinMaxStatsSchema.length() > 0) {\n      statsSchema = statsSchema.add(STATS_WITH_COLLATION, collatedMinMaxStatsSchema, true);\n    }\n\n    return statsSchema;\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Instance fields and public methods\n  //////////////////////////////////////////////////////////////////////////////////\n\n  private final StructType dataSchema;\n  /* Map of all leaf columns from logical to physical names */\n  private final Map<Column, Column> logicalToPhysicalColumn;\n  /* Map of all leaf logical columns to their data type */\n  private final Map<Column, DataType> logicalToDataType;\n\n  public StatsSchemaHelper(StructType dataSchema) {\n    this.dataSchema = dataSchema;\n    Map<Column, Tuple2<Column, DataType>> logicalToPhysicalColumnAndDataType =\n        getLogicalToPhysicalColumnAndDataType(dataSchema);\n    this.logicalToPhysicalColumn =\n        logicalToPhysicalColumnAndDataType.entrySet().stream()\n            .collect(\n                Collectors.toMap(\n                    Map.Entry::getKey, e -> e.getValue()._1 // map to just the column\n                    ));\n    this.logicalToDataType =\n        logicalToPhysicalColumnAndDataType.entrySet().stream()\n            .collect(\n                Collectors.toMap(\n                    Map.Entry::getKey, e -> e.getValue()._2 // map to just the data type\n                    ));\n  }\n\n  /**\n   * Given a logical column in the data schema provided when creating {@code this}, return the\n   * corresponding MIN column and an optional column adjustment expression from the statistic schema\n   * that stores the MIN values for the provided logical column.\n   *\n   * @param column the logical column name.\n   * @param collationIdentifier optional collation identifier if getting a collated stats column.\n   * @return a tuple of the MIN column and an optional adjustment expression.\n   */\n  public Tuple2<Column, Optional<Expression>> getMinColumn(\n      Column column, Optional<CollationIdentifier> collationIdentifier) {\n    checkArgument(\n        isSkippingEligibleMinMaxColumn(column),\n        \"%s is not a valid min column%s for data schema %s\",\n        column,\n        collationIdentifier.isPresent() ? (\" for collation \" + collationIdentifier) : \"\",\n        dataSchema);\n    return new Tuple2<>(getStatsColumn(column, MIN, collationIdentifier), Optional.empty());\n  }\n\n  /**\n   * Given a logical column in the data schema provided when creating {@code this}, return the\n   * corresponding MAX column and an optional column adjustment expression from the statistic schema\n   * that stores the MAX values for the provided logical column.\n   *\n   * @param column the logical column name.\n   * @param collationIdentifier optional collation identifier if getting a collated stats column.\n   * @return a tuple of the MAX column and an optional adjustment expression.\n   */\n  public Tuple2<Column, Optional<Expression>> getMaxColumn(\n      Column column, Optional<CollationIdentifier> collationIdentifier) {\n    checkArgument(\n        isSkippingEligibleMinMaxColumn(column),\n        \"%s is not a valid min column%s for data schema %s\",\n        column,\n        collationIdentifier.isPresent() ? (\" for collation \" + collationIdentifier) : \"\",\n        dataSchema);\n    DataType dataType = logicalToDataType.get(column);\n    Column maxColumn = getStatsColumn(column, MAX, collationIdentifier);\n\n    // If this is a column of type Timestamp or TimestampNTZ\n    // compensate for the truncation from microseconds to milliseconds\n    // by adding 1 millisecond. For example, a file containing only\n    // 01:02:03.456789 will be written with min == max == 01:02:03.456, so we must consider it\n    // to contain the range from 01:02:03.456 to 01:02:03.457.\n    if (dataType instanceof TimestampType || dataType instanceof TimestampNTZType) {\n      return new Tuple2<>(\n          maxColumn,\n          Optional.of(\n              new ScalarExpression(\"TIMEADD\", Arrays.asList(maxColumn, Literal.ofLong(1)))));\n    }\n    return new Tuple2<>(maxColumn, Optional.empty());\n  }\n\n  /**\n   * Given a logical column in the data schema provided when creating {@code this}, return the\n   * corresponding NULL_COUNT column in the statistic schema that stores the null count values for\n   * the provided logical column.\n   */\n  public Column getNullCountColumn(Column column) {\n    checkArgument(\n        isSkippingEligibleNullCountColumn(column),\n        \"%s is not a valid null_count column for data schema %s\",\n        column,\n        dataSchema);\n    return getStatsColumn(column, NULL_COUNT, Optional.empty());\n  }\n\n  /** Returns the NUM_RECORDS column in the statistic schema */\n  public Column getNumRecordsColumn() {\n    return new Column(NUM_RECORDS);\n  }\n\n  /**\n   * Returns true if the given column is skipping-eligible using min/max statistics. This means the\n   * column exists, is a leaf column, and is of a skipping-eligible data-type.\n   */\n  public boolean isSkippingEligibleMinMaxColumn(Column column) {\n    return logicalToDataType.containsKey(column)\n        && isSkippingEligibleDataType(logicalToDataType.get(column));\n  }\n\n  /**\n   * Returns true if the given column is skipping-eligible using null count statistics. This means\n   * the column exists and is a leaf column as we only collect stats for leaf columns.\n   */\n  public boolean isSkippingEligibleNullCountColumn(Column column) {\n    return logicalToPhysicalColumn.containsKey(column);\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Private static fields and methods\n  //////////////////////////////////////////////////////////////////////////////////\n\n  private static final Set<String> SKIPPING_ELIGIBLE_TYPE_NAMES =\n      new HashSet<String>() {\n        {\n          add(\"byte\");\n          add(\"short\");\n          add(\"integer\");\n          add(\"long\");\n          add(\"float\");\n          add(\"double\");\n          add(\"date\");\n          add(\"timestamp\");\n          add(\"timestamp_ntz\");\n        }\n      };\n\n  /**\n   * Given a data schema returns the expected schema for a min or max statistics column. This means\n   * 1) replace logical names with physical names 2) set nullable=true 3) only keep stats eligible\n   * fields (i.e. don't include fields with isSkippingEligibleDataType=false)\n   */\n  private static StructType getMinMaxStatsSchema(StructType dataSchema) {\n    List<StructField> fields = new ArrayList<>();\n    for (StructField field : dataSchema.fields()) {\n      if (isSkippingEligibleDataType(field.getDataType())) {\n        fields.add(new StructField(getPhysicalName(field), field.getDataType(), true));\n      } else if (field.getDataType() instanceof StructType) {\n        fields.add(\n            new StructField(\n                getPhysicalName(field),\n                getMinMaxStatsSchema((StructType) field.getDataType()),\n                true));\n      }\n    }\n    return new StructType(fields);\n  }\n\n  /**\n   * Given a data schema and a set of collation identifiers returns the expected schema for\n   * collation-aware statistics columns.\n   */\n  private static StructType getCollatedStatsSchema(\n      StructType dataSchema, Set<CollationIdentifier> collationIdentifiers) {\n    StructType statsWithCollation = new StructType();\n    StructType collationAwareFields = getCollationAwareFields(dataSchema);\n    for (CollationIdentifier collationIdentifier : collationIdentifiers) {\n      if (collationIdentifier.isSparkUTF8BinaryCollation()) {\n        // For SPARK.UTF8_BINARY collation we use the binary stats\n        continue;\n      }\n      if (collationIdentifier.getVersion().isEmpty()) {\n        throw new IllegalArgumentException(\n            String.format(\n                \"Collation identifier %s must specify a collation version for collation-aware \"\n                    + \"statistics.\",\n                collationIdentifier));\n      }\n\n      if (collationAwareFields.length() > 0) {\n        statsWithCollation =\n            statsWithCollation.add(\n                collationIdentifier.toString(),\n                new StructType()\n                    .add(MIN, collationAwareFields, true)\n                    .add(MAX, collationAwareFields, true),\n                true);\n      }\n    }\n    return statsWithCollation;\n  }\n\n  /** Given a data schema returns its collation aware fields. */\n  private static StructType getCollationAwareFields(StructType dataSchema) {\n    StructType collationAwareFields = new StructType();\n    for (StructField field : dataSchema.fields()) {\n      DataType dataType = field.getDataType();\n      if (dataType instanceof StructType) {\n        StructType nestedCollationAwareFields = getCollationAwareFields((StructType) dataType);\n        if (nestedCollationAwareFields.length() > 0) {\n          collationAwareFields =\n              collationAwareFields.add(getPhysicalName(field), nestedCollationAwareFields, true);\n        }\n      } else if (dataType instanceof StringType) {\n        collationAwareFields = collationAwareFields.add(getPhysicalName(field), dataType, true);\n      }\n    }\n    return collationAwareFields;\n  }\n\n  /**\n   * Given a data schema returns the expected schema for a null_count statistics column. This means\n   * 1) replace logical names with physical names 2) set nullable=true 3) use LongType for all\n   * fields\n   */\n  private static StructType getNullCountSchema(StructType dataSchema) {\n    List<StructField> fields = new ArrayList<>();\n    for (StructField field : dataSchema.fields()) {\n      if (field.getDataType() instanceof StructType) {\n        fields.add(\n            new StructField(\n                getPhysicalName(field),\n                getNullCountSchema((StructType) field.getDataType()),\n                true));\n      } else {\n        fields.add(new StructField(getPhysicalName(field), LongType.LONG, true));\n      }\n    }\n    return new StructType(fields);\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Private class helpers\n  //////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Given a logical column and a stats type returns the corresponding column in the statistics\n   * schema\n   */\n  private Column getStatsColumn(\n      Column column, String statType, Optional<CollationIdentifier> collationIdentifier) {\n    checkArgument(\n        logicalToPhysicalColumn.containsKey(column),\n        \"%s is not a valid leaf column for data schema: %s\",\n        column,\n        dataSchema);\n    Column physicalColumn = logicalToPhysicalColumn.get(column);\n    // Use binary stats if collation is not specified or if it is the default Spark collation.\n    if (collationIdentifier.isPresent()\n        && collationIdentifier.get() != CollationIdentifier.SPARK_UTF8_BINARY) {\n      // Collation-aware stats are stored under `statsWithCollation.collationName.statType`.\n      return getChildColumn(\n          physicalColumn,\n          Arrays.asList(STATS_WITH_COLLATION, collationIdentifier.get().toString(), statType));\n    } else {\n      // Binary stats are stored under `statType`.\n      return getChildColumn(physicalColumn, statType);\n    }\n  }\n\n  /**\n   * Given a data schema returns a map of {logical column -> (physical column, data type)} for all\n   * leaf columns in the schema.\n   */\n  private Map<Column, Tuple2<Column, DataType>> getLogicalToPhysicalColumnAndDataType(\n      StructType dataSchema) {\n    Map<Column, Tuple2<Column, DataType>> result = new HashMap<>();\n    for (StructField field : dataSchema.fields()) {\n      if (field.getDataType() instanceof StructType) {\n        Map<Column, Tuple2<Column, DataType>> nestedCols =\n            getLogicalToPhysicalColumnAndDataType((StructType) field.getDataType());\n        for (Column childLogicalCol : nestedCols.keySet()) {\n          Tuple2<Column, DataType> childCol = nestedCols.get(childLogicalCol);\n          Column childPhysicalCol = childCol._1;\n          DataType childColDataType = childCol._2;\n          result.put(\n              getChildColumn(childLogicalCol, field.getName()),\n              new Tuple2<>(\n                  getChildColumn(childPhysicalCol, getPhysicalName(field)), childColDataType));\n        }\n      } else {\n        result.put(\n            new Column(field.getName()),\n            new Tuple2<>(new Column(getPhysicalName(field)), field.getDataType()));\n      }\n    }\n    return result;\n  }\n\n  /** Returns the provided column as a child column nested under {@code parentName} */\n  private static Column getChildColumn(Column column, String parentName) {\n    return new Column(prependArray(column.getNames(), parentName));\n  }\n\n  /** Returns the provided column as a child column nested under {@code nestedPath} */\n  private static Column getChildColumn(Column column, List<String> nestedPath) {\n    for (int i = nestedPath.size() - 1; i >= 0; i--) {\n      String name = nestedPath.get(i);\n      column = getChildColumn(column, name);\n    }\n    return column;\n  }\n\n  /**\n   * Given an array {@code names} and a string element {@code preElem} return a new array with\n   * {@code preElem} inserted at the beginning\n   */\n  private static String[] prependArray(String[] arr, String preElem) {\n    String[] newNames = new String[arr.length + 1];\n    newNames[0] = preElem;\n    System.arraycopy(arr, 0, newNames, 1, arr.length);\n    return newNames;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/snapshot/LogSegment.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.snapshot;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.files.ParsedDeltaData;\nimport io.delta.kernel.internal.files.ParsedPublishedDeltaData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.lang.ListUtils;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.LongStream;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class LogSegment {\n\n  //////////////////////////////////////////\n  // Static factory methods and constants //\n  //////////////////////////////////////////\n\n  /**\n   * Creates a LogSegment for a newly created table from a single {@link ParsedDeltaData}. Used to\n   * construct a post-commit Snapshot after a CREATE transaction.\n   *\n   * @param logPath The path to the _delta_log directory\n   * @param parsedDeltaVersion0 The ParsedDeltaData that must be for version 0\n   * @return A new LogSegment with just this delta\n   * @throws IllegalArgumentException if the ParsedDeltaData is not file-based\n   */\n  public static LogSegment createForNewTable(Path logPath, ParsedDeltaData parsedDeltaVersion0) {\n    checkArgument(parsedDeltaVersion0.isFile(), \"Currently, only file-based deltas are supported\");\n    checkArgument(\n        parsedDeltaVersion0.getVersion() == 0L,\n        \"Version must be 0 for a LogSegment with only a single delta\");\n\n    final FileStatus deltaFile = parsedDeltaVersion0.getFileStatus();\n    final List<FileStatus> deltas = Collections.singletonList(deltaFile);\n    final List<FileStatus> checkpoints = Collections.emptyList();\n    final List<FileStatus> compactions = Collections.emptyList();\n    final Optional<Long> maxPublishedDeltaVersion =\n        parsedDeltaVersion0 instanceof ParsedPublishedDeltaData\n            ? Optional.of(0L)\n            : Optional.empty();\n\n    return new LogSegment(\n        logPath,\n        0 /* version */,\n        deltas,\n        compactions,\n        checkpoints,\n        deltaFile /* deltaAtEndVersion */,\n        Optional.empty() /* lastSeenChecksum */,\n        maxPublishedDeltaVersion);\n  }\n\n  private static final Logger logger = LoggerFactory.getLogger(LogSegment.class);\n\n  //////////////////////////////////\n  // Member methods and variables //\n  //////////////////////////////////\n\n  private final Path logPath;\n  private final long version;\n  private final List<FileStatus> deltas;\n  private final List<FileStatus> compactions;\n  private final List<FileStatus> checkpoints;\n  private final FileStatus deltaAtEndVersion;\n  private final Optional<Long> checkpointVersionOpt;\n  private final Optional<FileStatus> lastSeenChecksum;\n  private final Optional<Long> maxPublishedDeltaVersion;\n  private final List<FileStatus> deltasAndCheckpoints;\n  private final Lazy<List<FileStatus>> deltasAndCheckpointsReversed;\n  private final Lazy<List<FileStatus>> compactionsReversed;\n  private final Lazy<List<FileStatus>> deltasCheckpointsCompactionsReversed;\n\n  /**\n   * Provides information around which files in the transaction log need to be read to create the\n   * given version of the log.\n   *\n   * <p>This constructor validates and guarantees that:\n   *\n   * <ul>\n   *   <li>All deltas are valid deltas files\n   *   <li>All checkpoints are valid checkpoint files\n   *   <li>All checkpoint files have the same version\n   *   <li>All deltas are contiguous and range from {@link #checkpointVersionOpt} + 1 to version\n   *   <li>If no deltas are present then {@link #checkpointVersionOpt} is equal to version\n   * </ul>\n   *\n   * @param logPath The path to the _delta_log directory\n   * @param version The Snapshot version to generate\n   * @param deltas The delta commit files (.json) to read\n   * @param compactions Any found log compactions files that can be used in place of some or all of\n   *     the deltas\n   * @param checkpoints The checkpoint file(s) to read\n   * @param deltaAtEndVersion The delta file at the end version of this LogSegment. If this\n   *     LogSegment contains only checkpoints (e.g. 10.checkpoint only) then this is the delta at\n   *     that checkpoint version.\n   * @param lastSeenChecksum The most recent checksum file encountered during log directory listing,\n   *     if available.\n   * @param maxPublishedDeltaVersion The maximum version among all published delta files seen during\n   *     log segment construction, if available. Note that the Published Delta file for this version\n   *     may not be included as a Delta in this LogSegment, if there was a catalog commit that took\n   *     priority over it.\n   */\n  public LogSegment(\n      Path logPath,\n      long version,\n      List<FileStatus> deltas,\n      List<FileStatus> compactions,\n      List<FileStatus> checkpoints,\n      FileStatus deltaAtEndVersion,\n      Optional<FileStatus> lastSeenChecksum,\n      Optional<Long> maxPublishedDeltaVersion) {\n\n    ///////////////////////\n    // Input validations //\n    ///////////////////////\n\n    requireNonNull(logPath, \"logPath is null\");\n    requireNonNull(deltas, \"deltas is null\");\n    requireNonNull(compactions, \"compactions is null\");\n    requireNonNull(checkpoints, \"checkpoints is null\");\n    requireNonNull(deltaAtEndVersion, \"deltaAtEndVersion is null\");\n    requireNonNull(lastSeenChecksum, \"lastSeenChecksum null\");\n\n    checkArgument(version >= 0, \"version must be >= 0\");\n    validateDeltasAreDeltas(deltas);\n    validateCompactionsAreCompactions(compactions);\n    validateCheckpointsAreCheckpoints(checkpoints);\n    validateIndividualCompactionVersions(compactions);\n\n    this.checkpointVersionOpt =\n        checkpoints.isEmpty()\n            ? Optional.empty()\n            : Optional.of(FileNames.checkpointVersion(new Path(checkpoints.get(0).getPath())));\n\n    validateCheckpointVersionsAreSame(checkpoints, checkpointVersionOpt);\n    validateLastSeenChecksumWithinLogSegmentStartEndVersionRange(\n        lastSeenChecksum, version, checkpointVersionOpt);\n\n    checkArgument(!deltas.isEmpty() || !checkpoints.isEmpty(), \"No files to read\");\n\n    if (!deltas.isEmpty()) {\n      final List<Long> deltaVersions =\n          deltas.stream()\n              .map(fs -> FileNames.deltaVersion(new Path(fs.getPath())))\n              .collect(Collectors.toList());\n      validateFirstDeltaVersionIsCheckpointVersionPlusOne(deltaVersions, checkpointVersionOpt);\n      validateLastDeltaVersionIsLogSegmentVersion(deltaVersions, version);\n      validateDeltaVersionsAreContiguous(deltaVersions);\n      validateCompactionVersionsAreInRange(compactions, version, checkpointVersionOpt);\n    } else {\n      validateCheckpointVersionEqualsLogSegmentVersion(checkpointVersionOpt, version);\n    }\n\n    validateDeltaAtEndVersion(version, deltaAtEndVersion);\n\n    // Make sure input delta commits (JSON file), checkpoints and log compactions are valid.\n    assertLogFilesBelongToTable(\n        logPath,\n        Stream.concat(checkpoints.stream(), Stream.concat(deltas.stream(), compactions.stream()))\n            .collect(Collectors.toList()));\n\n    ////////////////////////////////\n    // Member variable assignment //\n    ////////////////////////////////\n\n    this.logPath = logPath;\n    this.version = version;\n    this.deltas = deltas;\n    this.compactions = compactions;\n    this.checkpoints = checkpoints;\n    this.deltaAtEndVersion = deltaAtEndVersion;\n    this.lastSeenChecksum = lastSeenChecksum;\n    this.maxPublishedDeltaVersion = maxPublishedDeltaVersion;\n    this.deltasAndCheckpoints =\n        Stream.concat(checkpoints.stream(), deltas.stream()).collect(Collectors.toList());\n\n    this.deltasAndCheckpointsReversed = lazyLoadDeltasAndCheckpointsReversed(deltasAndCheckpoints);\n\n    // We sort by the end version. since we work backward through the list, so this is the same as\n    // lexicographic, except when a compaction has a bigger range, which makes it \"better\", so we\n    // prefer it\n    this.compactionsReversed = lazyLoadCompactionsReversed(compactions);\n\n    this.deltasCheckpointsCompactionsReversed =\n        lazyLoadDeltasCheckpointsCompactionsReversed(\n            deltasAndCheckpointsReversed, compactionsReversed, compactions);\n\n    logger.debug(\"Created LogSegment: {}\", this);\n  }\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  public Path getLogPath() {\n    return logPath;\n  }\n\n  public long getVersion() {\n    return version;\n  }\n\n  public List<FileStatus> getDeltas() {\n    return deltas;\n  }\n\n  public List<FileStatus> getCompactions() {\n    return compactions;\n  }\n\n  public List<FileStatus> getCheckpoints() {\n    return checkpoints;\n  }\n\n  public Optional<Long> getCheckpointVersionOpt() {\n    return checkpointVersionOpt;\n  }\n\n  /**\n   * Returns the most recent checksum file encountered during log directory listing, if available.\n   *\n   * <p>Note: This checksum file's version is guaranteed to:\n   *\n   * <ul>\n   *   <li>Be less than or equal to the LogSegment version (enforced by constructor)\n   *   <li>Be greater than or equal to the checkpoint version if a checkpoint exists (filtered\n   *       during initialization)\n   * </ul>\n   *\n   * @return Optional containing the most recent valid checksum file encountered, or empty if none\n   *     found\n   */\n  public Optional<FileStatus> getLastSeenChecksum() {\n    return lastSeenChecksum;\n  }\n\n  /**\n   * Returns the maximum published delta version observed during log segment construction.\n   *\n   * <p>This is a best-effort API that returns what was actually seen during construction, not the\n   * authoritative maximum published delta version in the log.\n   *\n   * <p>{@code Optional.empty()} means \"we don't know\" - not necessarily that no deltas have been\n   * published. This can occur when:\n   *\n   * <ul>\n   *   <li>Only checkpoint files were found during listing (e.g., due to log cleanup)\n   *   <li>Listing bounds did not include published delta files\n   *   <li>The table contains only catalog commits with no published deltas\n   * </ul>\n   *\n   * @return the maximum published delta version seen during construction, or empty if unknown\n   */\n  public Optional<Long> getMaxPublishedDeltaVersion() {\n    return maxPublishedDeltaVersion;\n  }\n\n  /**\n   * Returns the Delta file at the end {@code version} of this LogSegment.\n   *\n   * <p>If this LogSegment has checkpoints and deltas, then this is the last delta.\n   *\n   * <p>If this LogSegment has only checkpoints (i.e. 10.checkpoint only) then this is the delta at\n   * that checkpoint version.\n   */\n  public FileStatus getDeltaFileAtEndVersion() {\n    return deltaAtEndVersion;\n  }\n\n  /**\n   * @return all deltas (.json) and checkpoint (.checkpoint.parquet) files in this LogSegment,\n   *     sorted in reverse (00012.json, 00011.json, 00010.checkpoint.parquet) order.\n   */\n  public List<FileStatus> allLogFilesReversed() {\n    return deltasAndCheckpointsReversed.get();\n  }\n\n  /**\n   * @return all files sorted in reverse order in this log segment, but omitting the deltas (.json)\n   *     files that are covered by log compaction files. This will include deltas (xxx.json) that\n   *     are not covered by a log compaction, compaction files (xxx.xxx.json), and checkpoints\n   *     (.checkpoint.parquet).\n   */\n  public List<FileStatus> allFilesWithCompactionsReversed() {\n    return deltasCheckpointsCompactionsReversed.get();\n  }\n\n  public List<ParsedCatalogCommitData> getAllCatalogCommits() {\n    return deltas.stream()\n        .map(ParsedDeltaData::forFileStatus)\n        .filter(x -> x instanceof ParsedCatalogCommitData)\n        .map(ParsedCatalogCommitData.class::cast)\n        .collect(Collectors.toList());\n  }\n\n  /**\n   * Creates a new LogSegment by extending this LogSegment with additional deltas. Used to construct\n   * a post-commit Snapshot from a previous Snapshot.\n   *\n   * <p>The additional deltas must be contiguous and start at version + 1.\n   *\n   * @param addedDeltas List of ParsedDeltaData to add (must be contiguous and start at current\n   *     version + 1)\n   * @return A new LogSegment with the additional deltas\n   * @throws IllegalArgumentException if deltas are not contiguous or don't start at version + 1\n   */\n  public LogSegment newWithAddedDeltas(List<ParsedDeltaData> addedDeltas) {\n    if (addedDeltas.isEmpty()) {\n      return this;\n    }\n\n    // Validate file-based (not inline), contiguous, and starts at version + 1. Then, convert to\n    // file status.\n    final List<FileStatus> newDeltaFileStatuses = new ArrayList<>(addedDeltas.size());\n    long expectedVersion = version + 1;\n\n    for (ParsedDeltaData delta : addedDeltas) {\n      checkArgument(delta.isFile(), \"Currently, only file-based deltas are supported\");\n\n      checkArgument(\n          delta.getVersion() == expectedVersion,\n          \"Delta versions must be contiguous. Expected %d but got %d\",\n          expectedVersion,\n          delta.getVersion());\n\n      newDeltaFileStatuses.add(delta.getFileStatus());\n\n      expectedVersion++;\n    }\n\n    final List<FileStatus> combinedDeltas = new ArrayList<>(deltas);\n    combinedDeltas.addAll(newDeltaFileStatuses);\n\n    final ParsedDeltaData lastAddedDelta = ListUtils.getLast(addedDeltas);\n\n    return new LogSegment(\n        logPath,\n        lastAddedDelta.getVersion(), // Use the updated version\n        combinedDeltas,\n        compactions, // Keep existing compactions\n        checkpoints, // Keep existing checkpoints\n        lastAddedDelta.getFileStatus(),\n        lastSeenChecksum, // Keep existing lastSeenChecksum\n        maxPublishedDeltaVersion); // Keep existing maxPublishedDeltaVersion\n  }\n\n  /**\n   * Creates a new LogSegment that reflects the published commits. Used to construct a post-publish\n   * Snapshot from a previous Snapshot.\n   *\n   * @return A new LogSegment with published commits\n   */\n  public LogSegment newAsPublished() {\n    FileStatus lastDeltaFileStatus = FileStatus.of(FileNames.deltaFile(logPath, version));\n    long deltaStartVersion = this.checkpointVersionOpt.map(i -> i + 1).orElse(0L);\n    return new LogSegment(\n        logPath,\n        version,\n        LongStream.rangeClosed(deltaStartVersion, version)\n            .mapToObj(v -> FileStatus.of(FileNames.deltaFile(logPath, v)))\n            .collect(Collectors.toList()),\n        getCompactions(),\n        getCheckpoints(),\n        lastDeltaFileStatus,\n        getLastSeenChecksum(),\n        Optional.of(version));\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"LogSegment {\\n\"\n            + \"  logPath='%s',\\n\"\n            + \"  version=%d,\\n\"\n            + \"  deltas=[%s\\n  ],\\n\"\n            + \"  checkpoints=[%s\\n  ],\\n\"\n            + \"  deltaAtEndVersion=%s,\\n\"\n            + \"  lastSeenChecksum=%s,\\n\"\n            + \"  checkpointVersion=%s,\\n\"\n            + \"  maxPublishedDeltaVersion=%s\\n\"\n            + \"}\",\n        logPath,\n        version,\n        formatList(deltas),\n        formatList(checkpoints),\n        deltaAtEndVersion,\n        lastSeenChecksum.map(FileStatus::toString).orElse(\"None\"),\n        checkpointVersionOpt.map(String::valueOf).orElse(\"None\"),\n        maxPublishedDeltaVersion.map(String::valueOf).orElse(\"None\"));\n  }\n\n  @Override\n  public int hashCode() {\n    // TODO: support staged commits #4927\n    return Objects.hash(deltas, checkpoints, compactions);\n  }\n\n  //////////////////////////////\n  // Input validation methods //\n  //////////////////////////////\n\n  private void validateDeltasAreDeltas(List<FileStatus> deltas) {\n    checkArgument(\n        deltas.stream().allMatch(fs -> FileNames.isCommitFile(fs.getPath())),\n        () -> \"deltas must all be actual delta (commit) files: \" + deltas);\n  }\n\n  private void validateCompactionsAreCompactions(List<FileStatus> compactions) {\n    checkArgument(\n        compactions.stream().allMatch(fs -> FileNames.isLogCompactionFile(fs.getPath())),\n        () -> \"compactions must all be actual log compaction files: \" + compactions);\n  }\n\n  private void validateCheckpointsAreCheckpoints(List<FileStatus> checkpoints) {\n    checkArgument(\n        checkpoints.stream().allMatch(fs -> FileNames.isCheckpointFile(fs.getPath())),\n        () -> \"checkpoints must all be actual checkpoint files: \" + checkpoints);\n  }\n\n  private void validateIndividualCompactionVersions(List<FileStatus> compactions) {\n    checkArgument(\n        compactions.stream()\n            .allMatch(\n                fs -> {\n                  Tuple2<Long, Long> versions = FileNames.logCompactionVersions(fs.getPath());\n                  return versions._1 < versions._2;\n                }),\n        () -> \"compactions must have start version less than end version: \" + compactions);\n  }\n\n  private void validateCheckpointVersionsAreSame(\n      List<FileStatus> checkpoints, Optional<Long> checkpointVersionOpt) {\n    if (!checkpoints.isEmpty()) {\n      checkArgument(\n          checkpoints.stream()\n              .map(fs -> FileNames.checkpointVersion(new Path(fs.getPath())))\n              .allMatch(v -> checkpointVersionOpt.get().equals(v)),\n          () -> \"All checkpoint files must have the same version: \" + checkpoints);\n    }\n  }\n\n  private void validateLastSeenChecksumWithinLogSegmentStartEndVersionRange(\n      Optional<FileStatus> lastSeenChecksum, long version, Optional<Long> checkpointVersionOpt) {\n    lastSeenChecksum.ifPresent(\n        checksumFile -> {\n          long checksumVersion = FileNames.checksumVersion(new Path(checksumFile.getPath()));\n          checkArgument(\n              checksumVersion <= version,\n              \"checksum version (%d) should be less than or equal to LogSegment version (%d)\",\n              checksumVersion,\n              version);\n          checkpointVersionOpt.ifPresent(\n              checkpointVersion ->\n                  checkArgument(\n                      checksumVersion >= checkpointVersion,\n                      \"checksum version (%d) should be greater than or equal to checkpoint \"\n                          + \"version (%d)\",\n                      checksumVersion,\n                      checkpointVersion));\n        });\n  }\n\n  private void validateFirstDeltaVersionIsCheckpointVersionPlusOne(\n      List<Long> deltaVersions, Optional<Long> checkpointVersionOpt) {\n    checkpointVersionOpt.ifPresent(\n        checkpointVersion -> {\n          checkArgument(\n              deltaVersions.get(0) == checkpointVersion + 1,\n              \"First delta file version (%d) must equal checkpointVersion + 1 (%d)\",\n              deltaVersions.get(0),\n              checkpointVersion + 1);\n        });\n  }\n\n  private void validateLastDeltaVersionIsLogSegmentVersion(List<Long> deltaVersions, long version) {\n    checkArgument(\n        ListUtils.getLast(deltaVersions) == version,\n        \"Last delta file version (%d) must equal LogSegment version (%d)\",\n        ListUtils.getLast(deltaVersions),\n        version);\n  }\n\n  private void validateDeltaVersionsAreContiguous(List<Long> deltaVersions) {\n    for (int i = 1; i < deltaVersions.size(); i++) {\n      checkArgument(\n          deltaVersions.get(i) == deltaVersions.get(i - 1) + 1,\n          () -> \"Delta versions must be contiguous: \" + deltaVersions);\n    }\n  }\n\n  private void validateCompactionVersionsAreInRange(\n      List<FileStatus> compactions, long version, Optional<Long> checkpointVersionOpt) {\n    checkArgument(\n        compactions.stream()\n            .allMatch(\n                fs -> {\n                  Tuple2<Long, Long> versions = FileNames.logCompactionVersions(fs.getPath());\n                  boolean checkpointVersionOkay =\n                      checkpointVersionOpt\n                          .map(checkpointVersion -> versions._1 > checkpointVersion)\n                          .orElse(true);\n                  return checkpointVersionOkay && versions._2 <= version;\n                }),\n        () ->\n            String.format(\n                \"compactions must have startVersion > checkpointVersion (%d) AND endVersion <= \"\n                    + \"version (%d): %s\",\n                checkpointVersionOpt.orElse(-1L), version, compactions));\n  }\n\n  private void validateCheckpointVersionEqualsLogSegmentVersion(\n      Optional<Long> checkpointVersionOpt, long version) {\n    checkpointVersionOpt.ifPresent(\n        checkpointVersion -> {\n          checkArgument(\n              checkpointVersion == version,\n              \"If no deltas, then checkpointVersion (%d) must equal LogSegment version (%d)\",\n              checkpointVersion,\n              version);\n        });\n  }\n\n  private void validateDeltaAtEndVersion(long version, FileStatus deltaAtEndVersion) {\n    checkArgument(\n        FileNames.isCommitFile(deltaAtEndVersion.getPath()),\n        \"deltaAtEndVersion must be a delta file: \" + deltaAtEndVersion);\n\n    final long deltaVersion = FileNames.deltaVersion(deltaAtEndVersion.getPath());\n    checkArgument(\n        deltaVersion == version,\n        \"deltaAtEndVersion (%d) must be equal to LogSegment version (%d)\",\n        deltaVersion,\n        version);\n  }\n\n  //////////////////////////\n  // Other helper methods //\n  //////////////////////////\n\n  private Lazy<List<FileStatus>> lazyLoadDeltasAndCheckpointsReversed(\n      List<FileStatus> deltasAndCheckpoints) {\n    return new Lazy<>(\n        () ->\n            deltasAndCheckpoints.stream()\n                .sorted(\n                    Comparator.comparing((FileStatus a) -> new Path(a.getPath()).getName())\n                        .reversed())\n                .collect(Collectors.toList()));\n  }\n\n  private Lazy<List<FileStatus>> lazyLoadCompactionsReversed(List<FileStatus> compactions) {\n    return new Lazy<>(\n        () ->\n            compactions.stream()\n                .sorted(\n                    Comparator.comparing(\n                            (FileStatus a) -> FileNames.logCompactionVersions(a.getPath())._2)\n                        .reversed())\n                .collect(Collectors.toList()));\n  }\n\n  private Lazy<List<FileStatus>> lazyLoadDeltasCheckpointsCompactionsReversed(\n      Lazy<List<FileStatus>> deltasAndCheckpointsReversed,\n      Lazy<List<FileStatus>> compactionsReversed,\n      List<FileStatus> compactions) {\n    return new Lazy<>(\n        () -> {\n          if (compactions.isEmpty()) {\n            return deltasAndCheckpointsReversed.get();\n          } else {\n            LogCompactionResolver resolver =\n                new LogCompactionResolver(\n                    deltasAndCheckpointsReversed.get(), compactionsReversed.get());\n            return resolver.resolveFiles();\n          }\n        });\n  }\n\n  private String formatList(List<FileStatus> list) {\n    if (list.isEmpty()) {\n      return \"\";\n    }\n    return \"\\n    \"\n        + list.stream().map(FileStatus::toString).collect(Collectors.joining(\",\\n    \"));\n  }\n\n  /**\n   * Verifies that a set of delta or checkpoint files to be read actually belongs to this table.\n   * Visible only for testing.\n   */\n  @VisibleForTesting\n  static void assertLogFilesBelongToTable(Path logPath, List<FileStatus> allFiles) {\n    String logPathStr = logPath.toString(); // fully qualified path\n    for (FileStatus fileStatus : allFiles) {\n      String filePath = fileStatus.getPath();\n      if (!filePath.startsWith(logPathStr)) {\n        throw new RuntimeException(\n            String.format(\n                \"File (%s) doesn't belong in the transaction log at %s.\", filePath, logPathStr));\n      }\n    }\n  }\n\n  ////////////////////\n  // Helper classes //\n  ////////////////////\n\n  // Class to resolve the final list of deltas + log compactions to return\n  private class LogCompactionResolver {\n    // note that currentCompactionPos _always_ points to a valid compaction we'll be including, _or_\n    // past the end of the list of compactions (meaning we've consumed them all). The compaction\n    // pointed to will be added to the output when we hit a delta with a version equal to the low\n    // version of the compaction.\n    int currentCompactionPos = 0;\n    long currentCompactionHi = -1;\n    long currentCompactionLo = -1;\n    final List<FileStatus> compactionsReversed;\n\n    final Iterator<FileStatus> deltaIt;\n\n    LogCompactionResolver(List<FileStatus> allFilesReversed, List<FileStatus> compactionsReversed) {\n      this.deltaIt = allFilesReversed.iterator();\n      this.compactionsReversed = compactionsReversed;\n    }\n\n    // We have two lists, one of deltas and one of compactions. Each is sorted in DESCENDING\n    // order. Given this, resolves as follows:\n    // - set a \"hi/lo\" goalpost around the next compactions\n    // - for each delta, if its version is:\n    //   - greater than the current compaction high point, include it, move to next delta\n    //   - less than (but not equal to) the current compaction low point, skip it, move to next\n    //     delta\n    //   - equal to the current compaction low point, we're about to transition out of the\n    //     compaction, so, include the compaction, find the next compaction that has a high\n    //     point lower than our current low point and set that to the current compaction to\n    //     consider. This deals with overlapping compactions in a greedy way, ensuring we\n    //     ignore any overlapping compactions.\n    List<FileStatus> resolveFiles() {\n      ArrayList<FileStatus> ret = new ArrayList<FileStatus>();\n      setHiLo();\n      while (deltaIt.hasNext()) {\n        FileStatus currentDelta = deltaIt.next();\n        long deltaVersion = FileNames.deltaVersion(currentDelta.getPath());\n        if (deltaVersion == currentCompactionLo) {\n          // we're about to cross out of the compaction. insert the compaction and advance to the\n          // next compaction. We don't want to include this delta here.\n          ret.add(compactionsReversed.get(currentCompactionPos));\n          advanceCompactionPos();\n          setHiLo();\n        } else if (deltaVersion > currentCompactionHi) {\n          // this delta is not covered by the next compaction, include it.\n          ret.add(currentDelta);\n        }\n        // just skip the file if none of the above are true, it's covered by the current compaction\n      }\n      return ret;\n    }\n\n    // Advance the compaction pos until we're pointing a compaction that has a end lower than our\n    // current low mark (recall we move backwards through versions). This takes compactions in a\n    // greedy manner, and ensures we don't use any overlapping compactions.\n    private void advanceCompactionPos() {\n      currentCompactionPos += 1;\n      while (currentCompactionPos < compactionsReversed.size()) {\n        Tuple2<Long, Long> versions =\n            FileNames.logCompactionVersions(\n                compactionsReversed.get(currentCompactionPos).getPath());\n        if (versions._2 < currentCompactionLo) {\n          break;\n        }\n        currentCompactionPos += 1;\n      }\n    }\n\n    // Set the high/low position based on the current currentCompactionPos\n    private void setHiLo() {\n      if (currentCompactionPos < compactionsReversed.size()) {\n        Tuple2<Long, Long> versions =\n            FileNames.logCompactionVersions(\n                compactionsReversed.get(currentCompactionPos).getPath());\n        currentCompactionLo = versions._1;\n        currentCompactionHi = versions._2;\n      } else {\n        currentCompactionLo = currentCompactionHi = -1;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/snapshot/MetadataCleanup.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.snapshot;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\nimport static io.delta.kernel.internal.checkpoints.Checkpointer.getLatestCompleteCheckpointFromList;\nimport static io.delta.kernel.internal.lang.ListUtils.getFirst;\nimport static io.delta.kernel.internal.lang.ListUtils.getLast;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.stream.Collectors.toList;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.checkpoints.CheckpointInstance;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.Clock;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class MetadataCleanup {\n  private static final Logger logger = LoggerFactory.getLogger(MetadataCleanup.class);\n\n  private MetadataCleanup() {}\n\n  /**\n   * Delete the Delta log files (delta and checkpoint files) that are expired according to the table\n   * metadata retention settings. While deleting the log files, it makes sure the time travel\n   * continues to work for all unexpired table versions.\n   *\n   * <p>Here is algorithm:\n   *\n   * <ul>\n   *   <li>Initial the potential delete file list: `potentialFilesToDelete` as an empty list\n   *   <li>Initialize the last seen checkpoint file list: `lastSeenCheckpointFiles`. There could be\n   *       one or more checkpoint files for a given version.\n   *   <li>List the delta log files starting with prefix \"00000000000000000000.\" (%020d). For each\n   *       file:\n   *       <ul>\n   *         <li>Step 1: Check if the `lastSeenCheckpointFiles` contains a complete checkpoint, then\n   *             <ul>\n   *               <li>Step 1.1: delete all files in `potentialFilesToDelete`. Now we know there is\n   *                   a checkpoint that contains the compacted Delta log up to the checkpoint\n   *                   version and all commit/checkpoint files before this checkpoint version are\n   *                   not needed.\n   *               <li>Step 1.2: add `lastCheckpointFiles` to `potentialFileStoDelete` list. This\n   *                   checkpoint is potential candidate to delete later if we find another\n   *                   checkpoint\n   *             </ul>\n   *         <li>Step 2: If the timestamp falls within the retention period, stop\n   *         <li>Step 3: If the file is a delta log file, add it to the `potentialFilesToDelete`\n   *             list\n   *         <li>Step 4: If the file is a checkpoint file, add it to the `lastSeenCheckpointFiles`\n   *       </ul>\n   * </ul>\n   *\n   * @param engine {@link Engine} instance to delete the expired log files\n   * @param clock {@link Clock} instance to get the current time. Useful in testing to mock the\n   *     current time.\n   * @param tablePath Table location\n   * @param retentionMillis Log file retention period in milliseconds\n   * @return number of log files deleted\n   * @throws IOException if an error occurs while deleting the log files\n   */\n  public static long cleanupExpiredLogs(\n      Engine engine, Clock clock, Path tablePath, long retentionMillis) throws IOException {\n    checkArgument(retentionMillis >= 0, \"Retention period must be non-negative\");\n\n    List<String> potentialLogFilesToDelete = new ArrayList<>();\n    long lastSeenCheckpointVersion = -1; // -1 indicates no checkpoint seen yet\n    List<String> lastSeenCheckpointFiles = new ArrayList<>();\n\n    long fileCutOffTime = clock.getTimeMillis() - retentionMillis;\n    String tableName = tablePath.getName();\n    logger.info(\n        \"[tableName={}] Starting the deletion of log files older than {}\",\n        tableName,\n        fileCutOffTime);\n    long numDeleted = 0;\n    try (CloseableIterator<FileStatus> files = listDeltaLogs(engine, tablePath)) {\n      while (files.hasNext()) {\n        // Step 1: Check if the `lastSeenCheckpointFiles` contains a complete checkpoint\n        Optional<CheckpointInstance> lastCompleteCheckpoint =\n            getLatestCompleteCheckpointFromList(\n                lastSeenCheckpointFiles.stream().map(CheckpointInstance::new).collect(toList()),\n                CheckpointInstance.MAX_VALUE);\n\n        if (lastCompleteCheckpoint.isPresent()) {\n          // Step 1.1: delete all files in `potentialFilesToDelete`. Now we know there is a\n          //   checkpoint that contains the compacted Delta log up to the checkpoint version and all\n          //   commit/checkpoint files before this checkpoint version are not needed. add\n          //   `lastCheckpointFiles` to `potentialFileStoDelete` list. This checkpoint is potential\n          //   candidate to delete later if we find another checkpoint\n          if (!potentialLogFilesToDelete.isEmpty()) {\n            logger.info(\n                \"[tableName={}] Deleting log files (start = {}, end = {}) because a checkpoint at \"\n                    + \"version {} indicates that these log files are no longer needed.\",\n                tableName,\n                getFirst(potentialLogFilesToDelete),\n                getLast(potentialLogFilesToDelete),\n                lastSeenCheckpointVersion);\n\n            numDeleted += deleteLogFiles(engine, potentialLogFilesToDelete);\n            potentialLogFilesToDelete.clear();\n          }\n\n          // Step 1.2: add `lastCheckpointFiles` to `potentialFileStoDelete` list. This checkpoint\n          // is potential candidate to delete later if we find another checkpoint\n          potentialLogFilesToDelete.addAll(lastSeenCheckpointFiles);\n          lastSeenCheckpointFiles.clear();\n          lastSeenCheckpointVersion = -1;\n        }\n\n        FileStatus nextFile = files.next();\n\n        // Step 2: If the timestamp is earlier than the retention period, stop\n        if (nextFile.getModificationTime() > fileCutOffTime) {\n          if (!potentialLogFilesToDelete.isEmpty()) {\n            logger.info(\n                \"[tableName={}] Skipping deletion of expired log files {}, because there is \"\n                    + \"no checkpoint file that indicates that the log files are no longer \"\n                    + \"needed. \",\n                tableName,\n                potentialLogFilesToDelete.size());\n          }\n          break;\n        }\n\n        if (FileNames.isCommitFile(nextFile.getPath())) {\n          // Step 3: If the file is a delta log file, add it to the `potentialFilesToDelete` list\n          // We can't delete these files until we encounter a checkpoint later that indicates\n          // that the log files are no longer needed.\n          potentialLogFilesToDelete.add(nextFile.getPath());\n        } else if (FileNames.isCheckpointFile(nextFile.getPath())) {\n          // Step 4: If the file is a checkpoint file, add it to the `lastSeenCheckpointFiles`\n          long newLastSeenCheckpointVersion = FileNames.checkpointVersion(nextFile.getPath());\n          checkArgument(\n              lastSeenCheckpointVersion == -1\n                  || newLastSeenCheckpointVersion >= lastSeenCheckpointVersion);\n\n          if (lastSeenCheckpointVersion != -1\n              && newLastSeenCheckpointVersion > lastSeenCheckpointVersion) {\n            // We have found checkpoint file for a new version. This means the files gathered for\n            // the last checkpoint version are not complete (most likely an incomplete multipart\n            // checkpoint). We should delete the files gathered so far and start fresh\n            // last seen checkpoint state\n            logger.info(\n                \"[tableName={}] Incomplete checkpoint files found at version {}, ignoring \"\n                    + \"the checkpoint files and adding them to potential log file delete list\",\n                tableName,\n                lastSeenCheckpointVersion);\n            potentialLogFilesToDelete.addAll(lastSeenCheckpointFiles);\n            lastSeenCheckpointFiles.clear();\n          }\n\n          lastSeenCheckpointFiles.add(nextFile.getPath());\n          lastSeenCheckpointVersion = newLastSeenCheckpointVersion;\n        }\n        // Ignore non-delta and non-checkpoint files.\n      }\n    }\n    logger.info(\n        \"[tableName={}] Deleted {} log files older than {}\", tableName, numDeleted, fileCutOffTime);\n    return numDeleted;\n  }\n\n  private static CloseableIterator<FileStatus> listDeltaLogs(Engine engine, Path tablePath)\n      throws IOException {\n    Path logPath = new Path(tablePath, \"_delta_log\");\n    // TODO: Currently we don't update the timestamps of files to be monotonically increasing.\n    // In future we can do something similar to Delta Spark to make the timestamps monotonically\n    // increasing. See `BufferingLogDeletionIterator` in Delta Spark.\n    return engine.getFileSystemClient().listFrom(FileNames.listingPrefix(logPath, 0));\n  }\n\n  private static int deleteLogFiles(Engine engine, List<String> logFiles) throws IOException {\n    int numDeleted = 0;\n    for (String logFile : logFiles) {\n      if (wrapEngineExceptionThrowsIO(\n          () -> engine.getFileSystemClient().delete(logFile),\n          \"Failed to delete the log file as part of the metadata cleanup %s\",\n          logFile)) {\n        numDeleted++;\n      }\n    }\n    return numDeleted;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/snapshot/SnapshotManager.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.snapshot;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.InvalidTableException;\nimport io.delta.kernel.exceptions.TableNotFoundException;\nimport io.delta.kernel.internal.*;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.checkpoints.*;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter;\nimport io.delta.kernel.internal.files.*;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.lang.ListUtils;\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext;\nimport io.delta.kernel.internal.replay.LogReplay;\nimport io.delta.kernel.internal.replay.ProtocolMetadataLogReplay;\nimport io.delta.kernel.internal.table.SnapshotFactory;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.internal.util.FileNames.DeltaLogFileType;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class SnapshotManager {\n\n  private final Path tablePath;\n  private final Path logPath;\n\n  public SnapshotManager(Path tablePath) {\n    this.tablePath = tablePath;\n    this.logPath = new Path(tablePath, \"_delta_log\");\n  }\n\n  private static final Logger logger = LoggerFactory.getLogger(SnapshotManager.class);\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  /**\n   * Construct the latest snapshot for given table.\n   *\n   * @param engine Instance of {@link Engine} to use.\n   * @return the latest {@link Snapshot} of the table\n   * @throws TableNotFoundException if the table does not exist\n   * @throws InvalidTableException if the table is in an invalid state\n   */\n  public SnapshotImpl buildLatestSnapshot(Engine engine, SnapshotQueryContext snapshotContext)\n      throws TableNotFoundException {\n    final LogSegment logSegment =\n        snapshotContext\n            .getSnapshotMetrics()\n            .loadLogSegmentTotalDurationTimer\n            .time(() -> getLogSegmentForVersion(engine, Optional.empty() /* versionToLoad */));\n    snapshotContext.setResolvedVersion(logSegment.getVersion());\n    snapshotContext.setCheckpointVersion(logSegment.getCheckpointVersionOpt());\n\n    return createSnapshot(logSegment, engine, snapshotContext);\n  }\n\n  /**\n   * Construct the snapshot for the given table at the version provided.\n   *\n   * @param engine Instance of {@link Engine} to use.\n   * @param version The snapshot version to construct\n   * @return a {@link Snapshot} of the table at version {@code version}\n   * @throws TableNotFoundException if the table does not exist\n   * @throws InvalidTableException if the table is in an invalid state\n   */\n  public SnapshotImpl getSnapshotAt(\n      Engine engine, long version, SnapshotQueryContext snapshotContext)\n      throws TableNotFoundException {\n    final LogSegment logSegment =\n        snapshotContext\n            .getSnapshotMetrics()\n            .loadLogSegmentTotalDurationTimer\n            .time(\n                () -> getLogSegmentForVersion(engine, Optional.of(version) /* versionToLoadOpt */));\n\n    snapshotContext.setCheckpointVersion(logSegment.getCheckpointVersionOpt());\n    snapshotContext.setResolvedVersion(logSegment.getVersion());\n\n    return createSnapshot(logSegment, engine, snapshotContext);\n  }\n\n  /**\n   * Construct the snapshot for the given table at the provided timestamp.\n   *\n   * @param engine Instance of {@link Engine} to use.\n   * @param millisSinceEpochUTC timestamp to fetch the snapshot for in milliseconds since the unix\n   *     epoch\n   * @return a {@link Snapshot} of the table at the provided timestamp\n   * @throws TableNotFoundException if the table does not exist\n   * @throws InvalidTableException if the table is in an invalid state\n   */\n  public SnapshotImpl getSnapshotForTimestamp(\n      Engine engine,\n      SnapshotImpl latestSnapshot,\n      long millisSinceEpochUTC,\n      SnapshotQueryContext snapshotContext)\n      throws TableNotFoundException {\n    final long versionToLoad =\n        SnapshotFactory.resolveTimestampToSnapshotVersion(\n            engine,\n            snapshotContext,\n            latestSnapshot,\n            millisSinceEpochUTC,\n            Collections.emptyList() /* logDatas */);\n\n    return getSnapshotAt(engine, versionToLoad, snapshotContext);\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  /**\n   * Verify that a list of delta versions is contiguous.\n   *\n   * @throws InvalidTableException if the versions are not contiguous\n   */\n  @VisibleForTesting\n  public static void verifyDeltaVersionsContiguous(List<Long> versions, Path tablePath) {\n    for (int i = 1; i < versions.size(); i++) {\n      if (versions.get(i) != versions.get(i - 1) + 1) {\n        throw new InvalidTableException(\n            tablePath.toString(),\n            String.format(\"Missing delta files: versions are not contiguous: (%s)\", versions));\n      }\n    }\n  }\n\n  private SnapshotImpl createSnapshot(\n      LogSegment initSegment, Engine engine, SnapshotQueryContext snapshotContext) {\n    final Lazy<LogSegment> lazyLogSegment = new Lazy<>(() -> initSegment);\n\n    final Lazy<Optional<CRCInfo>> lazyCrcInfo =\n        SnapshotFactory.createLazyChecksumFileLoaderWithMetrics(\n            engine, lazyLogSegment, snapshotContext.getSnapshotMetrics());\n\n    final ProtocolMetadataLogReplay.Result protocolMetadataResult =\n        ProtocolMetadataLogReplay.loadProtocolAndMetadata(\n            engine, tablePath, initSegment, lazyCrcInfo, snapshotContext.getSnapshotMetrics());\n\n    // TODO: When LogReplay becomes static utilities, we can create it inside of SnapshotImpl\n    final LogReplay logReplay = new LogReplay(engine, tablePath, lazyLogSegment, lazyCrcInfo);\n\n    final SnapshotImpl snapshot =\n        new SnapshotImpl(\n            tablePath,\n            initSegment.getVersion(),\n            lazyLogSegment,\n            logReplay,\n            protocolMetadataResult.protocol,\n            protocolMetadataResult.metadata,\n            DefaultFileSystemManagedTableOnlyCommitter.INSTANCE,\n            snapshotContext,\n            Optional.empty() /* inCommitTimestampOpt */);\n\n    return snapshot;\n  }\n\n  /**\n   * Generates a {@link LogSegment} for the given `versionToLoadOpt`. If no `versionToLoadOpt` is\n   * provided, generates a {@code LogSegment} for the latest version of the table.\n   *\n   * <p>This primarily consists of three steps:\n   *\n   * <ol>\n   *   <li>First, determine the starting checkpoint version that is at or before `versionToLoadOpt`.\n   *       If no `versionToLoadOpt` is provided, will use the checkpoint pointed to by the\n   *       _last_checkpoint file.\n   *   <li>Second, LIST the _delta_log for all delta and checkpoint files newer than the starting\n   *       checkpoint version.\n   *   <li>Third, process and validate this list of _delta_log files to yield a {@code LogSegment}.\n   * </ol>\n   */\n  public LogSegment getLogSegmentForVersion(Engine engine, Optional<Long> versionToLoadOpt) {\n    return getLogSegmentForVersion(\n        engine,\n        versionToLoadOpt,\n        Collections.emptyList() /* parsedLogDatas */,\n        Optional.empty() /* maxCatalogVersionOpt */);\n  }\n\n  /**\n   * [delta-io/delta#4765]: Right now, we only support sorted and contiguous ratified commit log\n   * data.\n   *\n   * @param timeTravelVersionOpt the version to time-travel to for a time-travel query\n   * @param parsedLogDatas the parsed log data from the catalog\n   * @param maxCatalogVersionOpt the maximum version ratified by the catalog for catalog managed\n   *     tables. Empty for file-system managed tables.\n   */\n  public LogSegment getLogSegmentForVersion(\n      Engine engine,\n      Optional<Long> timeTravelVersionOpt,\n      List<ParsedLogData> parsedLogDatas,\n      Optional<Long> maxCatalogVersionOpt) {\n    // This is the actual version we want to load. For \"latest\" (aka non-time-travel) queries for\n    // catalogManaged tables we want to load the maxCatalogVersion\n    final Optional<Long> versionToLoadOpt =\n        timeTravelVersionOpt.isPresent() ? timeTravelVersionOpt : maxCatalogVersionOpt;\n    final long versionToLoad = versionToLoadOpt.orElse(Long.MAX_VALUE);\n    final String versionToLoadStr = versionToLoadOpt.map(String::valueOf).orElse(\"latest\");\n    logger.info(\"Loading log segment for version {}\", versionToLoadStr);\n    final long logSegmentBuildingStartTimeMillis = System.currentTimeMillis();\n\n    ///////////////////////////////////////////////////////////////////////////////////////////\n    // Step 1: Find the latest checkpoint version. If timeTravelVersionOpt is empty, use the //\n    //         version referenced by the _LAST_CHECKPOINT file. If timeTravelVersionOpt is   //\n    //         present, search for the previous latest complete checkpoint at or before the  //\n    //         version to load                                                               //\n    ///////////////////////////////////////////////////////////////////////////////////////////\n\n    final Optional<Long> startCheckpointVersionOpt =\n        getStartCheckpointVersion(engine, timeTravelVersionOpt, maxCatalogVersionOpt);\n\n    /////////////////////////////////////////////////////////////////\n    // Step 2: Determine the actual version to start listing from. //\n    /////////////////////////////////////////////////////////////////\n\n    final long listFromStartVersion =\n        startCheckpointVersionOpt\n            .map(\n                version -> {\n                  logger.info(\"Found a complete checkpoint at version {}.\", version);\n                  return version;\n                })\n            .orElseGet(\n                () -> {\n                  logger.warn(\"Cannot find a complete checkpoint. Listing from version 0.\");\n                  return 0L;\n                });\n\n    /////////////////////////////////////////////////////////////////\n    // Step 3: List the files from $startVersion to $versionToLoad //\n    /////////////////////////////////////////////////////////////////\n\n    Set<DeltaLogFileType> fileTypes =\n        new HashSet<>(\n            Arrays.asList(\n                DeltaLogFileType.COMMIT,\n                DeltaLogFileType.CHECKPOINT,\n                DeltaLogFileType.CHECKSUM,\n                DeltaLogFileType.LOG_COMPACTION));\n\n    final long listingStartTimeMillis = System.currentTimeMillis();\n    final List<FileStatus> listedFileStatuses =\n        DeltaLogActionUtils.listDeltaLogFilesAsIter(\n                engine,\n                fileTypes,\n                tablePath,\n                listFromStartVersion,\n                versionToLoadOpt,\n                true /* mustBeRecreatable */)\n            .toInMemoryList();\n\n    logger.info(\n        \"{}: Took {}ms to list the files after starting checkpoint\",\n        tablePath,\n        System.currentTimeMillis() - listingStartTimeMillis);\n\n    ////////////////////////////////////////////////////////////////////////\n    // Step 4: Perform some basic validations on the listed file statuses //\n    ////////////////////////////////////////////////////////////////////////\n\n    if (listedFileStatuses.isEmpty()) {\n      if (startCheckpointVersionOpt.isPresent()) {\n        // We either (a) determined this checkpoint version from the _LAST_CHECKPOINT file, or (b)\n        // found the last complete checkpoint before our versionToLoad. In either case, we didn't\n        // see the checkpoint file in the listing.\n        // TODO: throw a more specific error based on case (a) or (b)\n        throw DeltaErrors.missingCheckpoint(tablePath.toString(), startCheckpointVersionOpt.get());\n      } else {\n        // Either no files found OR no *delta* files found even when listing from 0. This means that\n        // the delta table does not exist yet.\n        throw new TableNotFoundException(\n            tablePath.toString(), format(\"No delta files found in the directory: %s\", logPath));\n      }\n    }\n\n    logDebugFileStatuses(\"listedFileStatuses\", listedFileStatuses);\n\n    //////////////////////////////////////////////////////////////////////////////////////////\n    // Step 5: Partition $listedFileStatuses into the checkpoints, deltas, and compactions. //\n    //////////////////////////////////////////////////////////////////////////////////////////\n\n    final Map<Class<? extends ParsedLogData>, List<ParsedLogData>> partitionedFiles =\n        listedFileStatuses.stream()\n            .map(ParsedLogData::forFileStatus)\n            .collect(\n                Collectors.groupingBy(\n                    ParsedLogData::getGroupByCategoryClass,\n                    LinkedHashMap::new, // Ensure order is maintained\n                    Collectors.toList()));\n\n    final List<ParsedPublishedDeltaData> allPublishedDeltas =\n        partitionedFiles.getOrDefault(ParsedPublishedDeltaData.class, Collections.emptyList())\n            .stream()\n            .map(ParsedPublishedDeltaData.class::cast)\n            .collect(Collectors.toList());\n\n    final List<FileStatus> listedCheckpointFileStatuses =\n        partitionedFiles.getOrDefault(ParsedCheckpointData.class, Collections.emptyList()).stream()\n            .map(ParsedLogData::getFileStatus)\n            .collect(Collectors.toList());\n\n    final List<FileStatus> listedCompactionFileStatuses =\n        partitionedFiles.getOrDefault(ParsedLogCompactionData.class, Collections.emptyList())\n            .stream()\n            .map(ParsedLogData::getFileStatus)\n            .collect(Collectors.toList());\n\n    final List<FileStatus> listedChecksumFileStatuses =\n        partitionedFiles.getOrDefault(ParsedChecksumData.class, Collections.emptyList()).stream()\n            .map(ParsedLogData::getFileStatus)\n            .collect(Collectors.toList());\n\n    logDebugParsedLogDatas(\"allPublishedDeltas\", allPublishedDeltas);\n    logDebugFileStatuses(\"listedCheckpointFileStatuses\", listedCheckpointFileStatuses);\n    logDebugFileStatuses(\"listedCompactionFileStatuses\", listedCompactionFileStatuses);\n    logDebugFileStatuses(\"listedCheckSumFileStatuses\", listedChecksumFileStatuses);\n\n    /////////////////////////////////////////////////////////////////////////////////////////////\n    // Step 6: Determine the latest complete checkpoint version. The intuition here is that we //\n    //         LISTed from the startingCheckpoint but may have found a newer complete          //\n    //         checkpoint.                                                                     //\n    /////////////////////////////////////////////////////////////////////////////////////////////\n\n    final List<CheckpointInstance> listedCheckpointInstances =\n        listedCheckpointFileStatuses.stream()\n            .map(f -> new CheckpointInstance(f.getPath()))\n            .collect(Collectors.toList());\n\n    final CheckpointInstance notLaterThanCheckpoint =\n        versionToLoadOpt.map(CheckpointInstance::new).orElse(CheckpointInstance.MAX_VALUE);\n\n    final Optional<CheckpointInstance> latestCompleteCheckpointOpt =\n        Checkpointer.getLatestCompleteCheckpointFromList(\n            listedCheckpointInstances, notLaterThanCheckpoint);\n\n    if (!latestCompleteCheckpointOpt.isPresent() && startCheckpointVersionOpt.isPresent()) {\n      // In Step 1 we found a $startCheckpointVersion but now our LIST of the file system doesn't\n      // see it. This means that the checkpoint we thought should exist no longer does.\n      throw DeltaErrors.missingCheckpoint(tablePath.toString(), startCheckpointVersionOpt.get());\n    }\n\n    final long latestCompleteCheckpointVersion =\n        latestCompleteCheckpointOpt.map(x -> x.version).orElse(-1L);\n\n    logger.info(\"Latest complete checkpoint version: {}\", latestCompleteCheckpointVersion);\n\n    /////////////////////////////////////////////////////////////////////////////////////////////\n    // Step 7: Grab all deltas in range [$latestCompleteCheckpointVersion + 1, $versionToLoad] //\n    /////////////////////////////////////////////////////////////////////////////////////////////\n\n    final List<ParsedDeltaData> allDeltasAfterCheckpoint =\n        getAllDeltasAfterCheckpointWithCatalogPriority(\n            allPublishedDeltas, parsedLogDatas, latestCompleteCheckpointVersion, versionToLoad);\n\n    logDebugParsedLogDatas(\"allDeltasAfterCheckpoint\", allDeltasAfterCheckpoint);\n\n    //////////////////////////////////////////////////////////////////////////////////\n    // Step 8: Grab all compactions in range [$latestCompleteCheckpointVersion + 1, //\n    //         $versionToLoad]                                                      //\n    //////////////////////////////////////////////////////////////////////////////////\n\n    final List<FileStatus> compactionsAfterCheckpoint =\n        listedCompactionFileStatuses.stream()\n            .filter(\n                fs -> {\n                  final Tuple2<Long, Long> compactionVersions =\n                      FileNames.logCompactionVersions(new Path(fs.getPath()));\n                  return latestCompleteCheckpointVersion + 1 <= compactionVersions._1\n                      && compactionVersions._2 <= versionToLoad;\n                })\n            .collect(Collectors.toList());\n\n    logDebugFileStatuses(\"compactionsAfterCheckpoint\", compactionsAfterCheckpoint);\n\n    ////////////////////////////////////////////////////////////////////\n    // Step 9: Determine the version of the snapshot we can now load. //\n    ////////////////////////////////////////////////////////////////////\n\n    final long newVersion =\n        allDeltasAfterCheckpoint.isEmpty()\n            ? latestCompleteCheckpointVersion\n            : ListUtils.getLast(allDeltasAfterCheckpoint).getVersion();\n\n    logger.info(\"New version to load: {}\", newVersion);\n\n    /////////////////////////////////////////////\n    // Step 10: Perform some basic validations. //\n    /////////////////////////////////////////////\n\n    // Check that we have found at least one checkpoint or delta file\n    if (!latestCompleteCheckpointOpt.isPresent() && allDeltasAfterCheckpoint.isEmpty()) {\n      throw new InvalidTableException(\n          tablePath.toString(), \"No complete checkpoint found and no delta files found\");\n    }\n\n    final Optional<ParsedPublishedDeltaData> deltaAtCheckpointVersionOpt =\n        allPublishedDeltas.stream()\n            .filter(x -> x.getVersion() == latestCompleteCheckpointVersion)\n            .findFirst();\n\n    // Check that, for a checkpoint at version N, there's a delta file at N, too.\n    if (latestCompleteCheckpointOpt.isPresent() && !deltaAtCheckpointVersionOpt.isPresent()) {\n      throw new InvalidTableException(\n          tablePath.toString(),\n          String.format(\"Missing delta file for version %s\", latestCompleteCheckpointVersion));\n    }\n\n    // Check that the $newVersion we actually loaded is the desired $versionToLoad\n    if (versionToLoadOpt.isPresent()) {\n      if (newVersion < versionToLoad) {\n        throw DeltaErrors.versionToLoadAfterLatestCommit(\n            tablePath.toString(), versionToLoad, newVersion);\n      } else if (newVersion > versionToLoad) {\n        throw new IllegalStateException(\n            String.format(\n                \"%s: Expected to load version %s but actually loaded version %s\",\n                tablePath, versionToLoad, newVersion));\n      }\n    }\n\n    if (!allDeltasAfterCheckpoint.isEmpty()) {\n      // Check that the delta versions are contiguous\n      verifyDeltaVersionsContiguous(\n          // TODO: refactor `verifyDeltaVersionsContiguous` to operate on ParsedLogData so we can\n          //      avoid making an entirely new list here\n          allDeltasAfterCheckpoint.stream().map(x -> x.getVersion()).collect(Collectors.toList()),\n          tablePath);\n\n      // Check that the delta versions start with $latestCompleteCheckpointVersion + 1. If they\n      // don't, then we have a gap in between the checkpoint and the first delta file.\n      if (allDeltasAfterCheckpoint.get(0).getVersion() != latestCompleteCheckpointVersion + 1) {\n        throw new InvalidTableException(\n            tablePath.toString(),\n            String.format(\n                \"Cannot compute snapshot. Missing delta file version %d.\",\n                latestCompleteCheckpointVersion + 1));\n      }\n\n      // Note: We have already asserted above that $versionToLoad equals $newVersion.\n      // Note: We already know that the last element of deltasAfterCheckpoint is $newVersion IF\n      //       $deltasAfterCheckpoint is not empty.\n\n      logger.info(\n          \"Verified delta files are contiguous from version {} to {}\",\n          latestCompleteCheckpointVersion + 1,\n          newVersion);\n    }\n\n    ////////////////////////////////////////////////////////////////////////////////////////////\n    // Step 11: Grab the actual checkpoint file statuses for latestCompleteCheckpointVersion. //\n    ////////////////////////////////////////////////////////////////////////////////////////////\n\n    final List<FileStatus> latestCompleteCheckpointFileStatuses =\n        latestCompleteCheckpointOpt\n            .map(\n                latestCompleteCheckpoint -> {\n                  final Set<Path> newCheckpointPaths =\n                      new HashSet<>(latestCompleteCheckpoint.getCorrespondingFiles(logPath));\n\n                  final List<FileStatus> newCheckpointFileStatuses =\n                      listedCheckpointFileStatuses.stream()\n                          .filter(f -> newCheckpointPaths.contains(new Path(f.getPath())))\n                          .collect(Collectors.toList());\n\n                  logDebugFileStatuses(\"newCheckpointFileStatuses\", newCheckpointFileStatuses);\n\n                  if (newCheckpointFileStatuses.size() != newCheckpointPaths.size()) {\n                    final String msg =\n                        format(\n                            \"Seems like the checkpoint is corrupted. Failed in getting the file \"\n                                + \"information for:\\n%s\\namong\\n%s\",\n                            newCheckpointPaths.stream()\n                                .map(Path::toString)\n                                .collect(Collectors.joining(\"\\n - \")),\n                            listedCheckpointFileStatuses.stream()\n                                .map(FileStatus::getPath)\n                                .collect(Collectors.joining(\"\\n - \")));\n                    throw new IllegalStateException(msg);\n                  }\n\n                  return newCheckpointFileStatuses;\n                })\n            .orElse(Collections.emptyList());\n\n    ////////////////////////////////////////////////////////\n    // Step 12: Calculate the remaining LogSegment params //\n    ////////////////////////////////////////////////////////\n\n    // If our LogSegment has deltas (allDeltasAfterCheckpoint), we use the last delta.\n    // Else, our LogSegment only has a checkpoint, and we have checked above that if there's a\n    // checkpoint then the `deltaAtCheckpointVersionOpt` exists.\n    final FileStatus deltaAtEndVersion =\n        allDeltasAfterCheckpoint.isEmpty()\n            ? deltaAtCheckpointVersionOpt.get().getFileStatus()\n            : ListUtils.getLast(allDeltasAfterCheckpoint).getFileStatus();\n\n    final Optional<Long> maxPublishedDeltaVersion =\n        allPublishedDeltas.stream().map(ParsedPublishedDeltaData::getVersion).max(Long::compareTo);\n\n    Optional<FileStatus> lastSeenChecksumFile = Optional.empty();\n    if (!listedChecksumFileStatuses.isEmpty()) {\n      FileStatus latestChecksum = ListUtils.getLast(listedChecksumFileStatuses);\n      long checksumVersion = FileNames.checksumVersion(new Path(latestChecksum.getPath()));\n      if (checksumVersion >= latestCompleteCheckpointVersion) {\n        lastSeenChecksumFile = Optional.of(latestChecksum);\n      }\n    }\n\n    ///////////////////////////////////////////////////\n    // Step 13: Construct the LogSegment and return. //\n    ///////////////////////////////////////////////////\n\n    logger.info(\n        \"Successfully constructed LogSegment at version {}, took {}ms\",\n        newVersion,\n        System.currentTimeMillis() - logSegmentBuildingStartTimeMillis);\n\n    return new LogSegment(\n        logPath,\n        newVersion,\n        allDeltasAfterCheckpoint.stream()\n            .map(ParsedLogData::getFileStatus)\n            .collect(Collectors.toList()),\n        compactionsAfterCheckpoint,\n        latestCompleteCheckpointFileStatuses,\n        deltaAtEndVersion,\n        lastSeenChecksumFile,\n        maxPublishedDeltaVersion);\n  }\n\n  /////////////////////////\n  // getLogSegment utils //\n  /////////////////////////\n\n  /**\n   * Filters and concats (a) a list of published Deltas (from cloud LIST call), and (b) a list of\n   * {@link ParsedLogData} injected by the {@link TableManager}, to return a new list of all Deltas\n   * since the latest complete checkpoint, up to and including the target version to load.\n   *\n   * <ul>\n   *   <li>Assumes that {@code allPublishedDeltas} is sorted and contiguous.\n   *   <li>Assumes that {@code parsedLogDatas} is sorted and contiguous.\n   *   <li>[delta-io/delta#4765] For now, only accepts parsedLogData of type {@link\n   *       ParsedCatalogCommitData} (written to file).\n   *   <li>If there is both a published Delta and a ratified staged commit for the same version,\n   *       prioritizes the ratified staged commit\n   * </ul>\n   */\n  private List<ParsedDeltaData> getAllDeltasAfterCheckpointWithCatalogPriority(\n      List<ParsedPublishedDeltaData> allPublishedDeltas,\n      List<ParsedLogData> parsedLogDatas,\n      long latestCompleteCheckpointVersion,\n      long versionToLoad) {\n    final List<ParsedDeltaData> allPublishedDeltasAfterCheckpoint =\n        allPublishedDeltas.stream()\n            .filter(ParsedLogData::isFile)\n            .filter(\n                x ->\n                    latestCompleteCheckpointVersion < x.getVersion()\n                        && x.getVersion() <= versionToLoad)\n            .collect(Collectors.toList());\n\n    if (parsedLogDatas.isEmpty()) {\n      return allPublishedDeltasAfterCheckpoint;\n    }\n\n    final List<ParsedDeltaData> allRatifiedCommitsAfterCheckpoint =\n        parsedLogDatas.stream()\n            .filter(x -> x instanceof ParsedCatalogCommitData && x.isFile())\n            .filter(\n                x ->\n                    latestCompleteCheckpointVersion < x.getVersion()\n                        && x.getVersion() <= versionToLoad)\n            .map(ParsedCatalogCommitData.class::cast)\n            .collect(Collectors.toList());\n\n    return LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n        allPublishedDeltasAfterCheckpoint, allRatifiedCommitsAfterCheckpoint);\n  }\n\n  /**\n   * Determine the starting checkpoint version that is at or before the version to load.\n   *\n   * <p>Version to load: For time-travel queries, this is the time-travel version. For latest\n   * queries on catalog maanged tables, this is the max ratified catalog version. For latest queries\n   * on file-system managed tables, this is the latest available version on the file-system.\n   *\n   * <p>For non-time travel queries we will use the checkpoint pointed to by the _last_checkpoint\n   * file (except for when it is after the maxRatifiedCatalogVersion, in which case we will search\n   * backwards for a checkpoint).\n   */\n  private Optional<Long> getStartCheckpointVersion(\n      Engine engine, Optional<Long> timeTravelVersionOpt, Optional<Long> maxCatalogVersionOpt) {\n    // This is a \"latest\" query, let's try to use the _last_checkpoint file if possible\n    if (!timeTravelVersionOpt.isPresent()) {\n      logger.info(\"Reading the _last_checkpoint file for 'latest' query\");\n      Optional<Long> lastCheckpointFileVersionOpt =\n          new Checkpointer(logPath).readLastCheckpointFile(engine).map(x -> x.version);\n\n      if (!lastCheckpointFileVersionOpt.isPresent()) {\n        logger.info(\"No _last_checkpoint file found, default to listing from 0\");\n        return Optional.empty();\n      }\n\n      long lastCheckpointFileVersion = lastCheckpointFileVersionOpt.get();\n\n      if (!maxCatalogVersionOpt.isPresent()) {\n        // If there is no maxCatalogVersion we don't have to do anything special --> just return\n        return Optional.of(lastCheckpointFileVersion);\n      } else {\n        // When there is a maxCatalogVersion we only want to return the version from the\n        // _last_checkpoint file if it is less than or equal to the maxCatalogVersion. Otherwise,\n        // we should revert to listing backwards from the version to load.\n\n        // This situation is possible due to race conditions. Since fetching the maxCatalogVersion\n        // from the catalog, it is possible that a concurrent writer has committed, published\n        // and checkpointed before this listing code is executed. Thus, it is possible that the\n        // _last_checkpoint file points to a checkpoint later than the maxCatalogVersion.\n        if (lastCheckpointFileVersion <= maxCatalogVersionOpt.get()) {\n          return Optional.of(lastCheckpointFileVersion);\n        }\n        logger.info(\n            \"Found checkpoint at version {} in _last_checkpoint file but cannot be used because \"\n                + \"maxCatalogVersion = {}.\",\n            lastCheckpointFileVersion,\n            maxCatalogVersionOpt.get());\n      }\n    }\n\n    long versionToLoad =\n        timeTravelVersionOpt.orElseGet(\n            () ->\n                maxCatalogVersionOpt.orElseThrow(\n                    () ->\n                        new IllegalStateException(\n                            \"Impossible state: If timeTravelToVersionOpt and maxCatalogVersion \"\n                                + \"is empty we should always have returned earlier\")));\n    logger.info(\"Finding last complete checkpoint at or before version {}\", versionToLoad);\n    final long startTimeMillis = System.currentTimeMillis();\n    return Checkpointer.findLastCompleteCheckpointBefore(engine, logPath, versionToLoad + 1)\n        .map(checkpointInstance -> checkpointInstance.version)\n        .map(\n            checkpointVersion -> {\n              checkArgument(\n                  checkpointVersion <= versionToLoad,\n                  \"Last complete checkpoint version %s was not <= targetVersion %s\",\n                  checkpointVersion,\n                  versionToLoad);\n\n              logger.info(\n                  \"{}: Took {}ms to find last complete checkpoint <= targetVersion {}\",\n                  tablePath,\n                  System.currentTimeMillis() - startTimeMillis,\n                  versionToLoad);\n\n              return checkpointVersion;\n            });\n  }\n\n  private void logDebugFileStatuses(String varName, List<FileStatus> fileStatuses) {\n    if (logger.isDebugEnabled()) {\n      logger.debug(\n          \"{}: {}\",\n          varName,\n          Arrays.toString(\n              fileStatuses.stream().map(x -> new Path(x.getPath()).getName()).toArray()));\n    }\n  }\n\n  private void logDebugParsedLogDatas(String varName, List<? extends ParsedLogData> logDatas) {\n    if (logger.isDebugEnabled()) {\n      logger.debug(\n          \"{}:\\n  {}\",\n          varName,\n          logDatas.stream().map(Object::toString).collect(Collectors.joining(\"\\n  \")));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/stats/FileSizeHistogram.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.stats;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.data.GenericRow;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.metrics.FileSizeHistogramResult;\nimport io.delta.kernel.types.ArrayType;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/** A histogram that tracks file size distributions and their counts. */\npublic class FileSizeHistogram {\n\n  //////////////////////////////////\n  // Static variables and methods //\n  //////////////////////////////////\n\n  private static final long KB = 1024;\n  private static final long MB = KB * 1024;\n  private static final long GB = MB * 1024;\n\n  public static final StructType FULL_SCHEMA =\n      new StructType()\n          .add(\"sortedBinBoundaries\", new ArrayType(LongType.LONG, false))\n          .add(\"fileCounts\", new ArrayType(LongType.LONG, false))\n          .add(\"totalBytes\", new ArrayType(LongType.LONG, false));\n\n  /** Creates a default FileSizeHistogram with predefined bin boundaries and zero counts. */\n  public static FileSizeHistogram createDefaultHistogram() {\n    long[] defaultBoundaries = createDefaultBinBoundaries();\n    long[] zeroCounts = new long[defaultBoundaries.length];\n    long[] zeroBytes = new long[defaultBoundaries.length];\n    return new FileSizeHistogram(defaultBoundaries, zeroCounts, zeroBytes);\n  }\n\n  /** Creates a FileSizeHistogram from a column vector. */\n  public static Optional<FileSizeHistogram> fromColumnVector(ColumnVector vector, int rowId) {\n    if (vector.isNullAt(rowId)) {\n      return Optional.empty();\n    }\n\n    int boundariesIdx = FULL_SCHEMA.indexOf(\"sortedBinBoundaries\");\n    int totalBytesIdx = FULL_SCHEMA.indexOf(\"totalBytes\");\n    int fileCountsIdx = FULL_SCHEMA.indexOf(\"fileCounts\");\n\n    List<Long> boundariesList =\n        VectorUtils.toJavaList(\n            InternalUtils.requireNonNull(\n                    vector.getChild(boundariesIdx), rowId, \"sortedBinBoundaries\")\n                .getArray(rowId));\n    List<Long> totalBytesList =\n        VectorUtils.toJavaList(\n            InternalUtils.requireNonNull(vector.getChild(totalBytesIdx), rowId, \"totalBytes\")\n                .getArray(rowId));\n    List<Long> fileCountsList =\n        VectorUtils.toJavaList(\n            InternalUtils.requireNonNull(vector.getChild(fileCountsIdx), rowId, \"fileCounts\")\n                .getArray(rowId));\n\n    long[] boundaries = boundariesList.stream().mapToLong(Long::longValue).toArray();\n    long[] totalBytesArray = totalBytesList.stream().mapToLong(Long::longValue).toArray();\n    long[] fileCountsArray = fileCountsList.stream().mapToLong(Long::longValue).toArray();\n\n    return Optional.of(new FileSizeHistogram(boundaries, fileCountsArray, totalBytesArray));\n  }\n\n  /**\n   * Creates the default bin boundaries for file size categorization.\n   *\n   * <ul>\n   *   <li>Starts with 0 and powers of 2 from 8KB to 4MB\n   *   <li>4MB jumps from 8MB to 40MB\n   *   <li>8MB jumps from 48MB to 120MB\n   *   <li>4MB jumps from 124MB to 144MB\n   *   <li>16MB jumps from 160MB to 576MB\n   *   <li>64MB jumps from 640MB to 1408MB\n   *   <li>128MB jumps from 1536MB to 2GB\n   *   <li>256MB jumps from 2304MB to 4GB\n   *   <li>Powers of 2 from 8GB to 256GB\n   * </ul>\n   *\n   * @return An array of bin boundaries sorted in ascending order\n   */\n  private static long[] createDefaultBinBoundaries() {\n    // Pre-calculate the size to avoid resizing\n    int totalSize = 95; // Known size from all the boundaries\n    long[] boundaries = new long[totalSize];\n    int idx = 0;\n\n    // 0 and powers of 2 till 4 MB\n    boundaries[idx++] = 0L;\n    for (long size = 8 * KB; size <= 4 * MB; size *= 2) {\n      boundaries[idx++] = size;\n    }\n\n    // 4 MB jumps till 40 MB\n    for (long size = 8 * MB; size <= 40 * MB; size += 4 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // 8 MB jumps till 120 MB\n    for (long size = 48 * MB; size <= 120 * MB; size += 8 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // 4 MB jumps till 144 MB\n    for (long size = 124 * MB; size <= 144 * MB; size += 4 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // 16 MB jumps till 576 MB\n    for (long size = 160 * MB; size <= 576 * MB; size += 16 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // 64 MB jumps till 1408 MB\n    for (long size = 640 * MB; size <= 1408 * MB; size += 64 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // 128 MB jumps till 2 GB\n    for (long size = 1536 * MB; size <= 2048 * MB; size += 128 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // 256 MB jumps till 4 GB\n    for (long size = 2304 * MB; size <= 4096 * MB; size += 256 * MB) {\n      boundaries[idx++] = size;\n    }\n\n    // Power of 2 till 256 GB\n    for (long size = 8 * GB; size <= 256 * GB; size *= 2) {\n      boundaries[idx++] = size;\n    }\n\n    checkArgument(\n        idx == totalSize, \"Incorrect pre-calculated size. Expected %s but got %s\", totalSize, idx);\n    return boundaries;\n  }\n\n  public static FileSizeHistogram fromFileSizeHistogramResult(\n      FileSizeHistogramResult fileSizeHistogramResult) {\n    requireNonNull(fileSizeHistogramResult);\n    return new FileSizeHistogram(\n        fileSizeHistogramResult.getSortedBinBoundaries(),\n        fileSizeHistogramResult.getFileCounts(),\n        fileSizeHistogramResult.getTotalBytes());\n  }\n\n  ////////////////////////////////////\n  // Member variables and methods  //\n  ////////////////////////////////////\n\n  private final long[] sortedBinBoundaries;\n  private final long[] fileCounts;\n  private final long[] totalBytes;\n\n  @VisibleForTesting\n  public FileSizeHistogram(long[] sortedBinBoundaries, long[] fileCounts, long[] totalBytes) {\n    requireNonNull(sortedBinBoundaries, \"sortedBinBoundaries cannot be null\");\n    requireNonNull(fileCounts, \"fileCounts cannot be null\");\n    requireNonNull(totalBytes, \"totalBytes cannot be null\");\n\n    checkArgument(\n        sortedBinBoundaries.length >= 2,\n        \"sortedBinBoundaries must have at least 2 elements to define a range\");\n    checkArgument(\n        sortedBinBoundaries[0] == 0, \"First boundary must be 0, got %s\", sortedBinBoundaries[0]);\n    checkArgument(\n        sortedBinBoundaries.length == fileCounts.length\n            && sortedBinBoundaries.length == totalBytes.length,\n        \"All arrays must have the same length\");\n\n    this.sortedBinBoundaries = sortedBinBoundaries;\n    this.fileCounts = fileCounts;\n    this.totalBytes = totalBytes;\n  }\n\n  /**\n   * Adds a file size to the histogram, incrementing the appropriate bin's count and total bytes.\n   * The appropriate bin refers to a bin with boundary that is less than or equal to the file size.\n   * Files larger than the maximum bin boundary (256 GB) are placed in the last bin.\n   *\n   * @param fileSize The size of the file in bytes\n   * @throws IllegalArgumentException if fileSize is negative or if getBinIndex returns an invalid\n   *     index\n   */\n  public void insert(long fileSize) {\n    checkArgument(fileSize >= 0, \"File size must be non-negative, got %s\", fileSize);\n    int index = getBinIndex(fileSize);\n    checkArgument(\n        index >= 0,\n        \"getBinIndex must return non-negative index for non-negative fileSize, got %s\",\n        index);\n    fileCounts[index]++;\n    totalBytes[index] += fileSize;\n  }\n\n  /**\n   * Removes a file size from the histogram, decrementing the appropriate bin's count and total\n   * bytes.\n   *\n   * @param fileSize The size of the file in bytes\n   * @throws IllegalArgumentException if fileSize is negative\n   */\n  public void remove(long fileSize) {\n    checkArgument(fileSize >= 0, \"File size must be non-negative, got %s\", fileSize);\n    int index = getBinIndex(fileSize);\n    checkArgument(\n        index >= 0,\n        \"getBinIndex must return non-negative index for non-negative fileSize, got %s\",\n        index);\n    checkArgument(\n        totalBytes[index] >= fileSize && fileCounts[index] > 0,\n        \"Cannot remove %s bytes from bin %d which only has %s bytes or does not have any files\",\n        fileSize,\n        index,\n        totalBytes[index]);\n    fileCounts[index]--;\n    totalBytes[index] -= fileSize;\n  }\n\n  private int getBinIndex(long fileSize) {\n    int index = Arrays.binarySearch(sortedBinBoundaries, fileSize);\n    // When fileSize is not found in the array, binarySearch returns -(insertion_point) - 1\n    // We need to get the bin that comes before the insertion point, which is (insertion_point - 1)\n    return index >= 0 ? index : -(index + 1) - 1;\n  }\n\n  /** Encode as a {@link Row} object with the schema {@link FileSizeHistogram#FULL_SCHEMA}. */\n  public Row toRow() {\n    Map<Integer, Object> value = new HashMap<>();\n    value.put(\n        FULL_SCHEMA.indexOf(\"sortedBinBoundaries\"),\n        VectorUtils.buildArrayValue(\n            Arrays.stream(sortedBinBoundaries).boxed().collect(Collectors.toList()),\n            LongType.LONG));\n    value.put(\n        FULL_SCHEMA.indexOf(\"fileCounts\"),\n        VectorUtils.buildArrayValue(\n            Arrays.stream(fileCounts).boxed().collect(Collectors.toList()), LongType.LONG));\n    value.put(\n        FULL_SCHEMA.indexOf(\"totalBytes\"),\n        VectorUtils.buildArrayValue(\n            Arrays.stream(totalBytes).boxed().collect(Collectors.toList()), LongType.LONG));\n    return new GenericRow(FULL_SCHEMA, value);\n  }\n\n  public FileSizeHistogramResult captureFileSizeHistogramResult() {\n    return new FileSizeHistogramResult() {\n      final long[] copiedSortedBinBoundaries =\n          Arrays.copyOf(sortedBinBoundaries, sortedBinBoundaries.length);\n      final long[] copiedFileCounts = Arrays.copyOf(fileCounts, fileCounts.length);\n      final long[] copiedTotalBytes = Arrays.copyOf(totalBytes, totalBytes.length);\n\n      @Override\n      public long[] getSortedBinBoundaries() {\n        return copiedSortedBinBoundaries;\n      }\n\n      @Override\n      public long[] getFileCounts() {\n        return copiedFileCounts;\n      }\n\n      @Override\n      public long[] getTotalBytes() {\n        return copiedTotalBytes;\n      }\n    };\n  }\n\n  /**\n   * Adds two histograms together by combining their counts and total bytes. Both histograms must\n   * have the same bin boundaries.\n   *\n   * @param other The histogram to add to this histogram\n   * @return A new histogram with combined statistics\n   * @throws IllegalArgumentException if the histograms have different bin boundaries\n   */\n  public FileSizeHistogram plus(FileSizeHistogram other) {\n    requireNonNull(other, \"histogram to add cannot be null\");\n    checkArgument(\n        Arrays.equals(this.sortedBinBoundaries, other.sortedBinBoundaries),\n        \"Cannot add histograms with different bin boundaries\");\n\n    int length = this.sortedBinBoundaries.length;\n    long[] combinedCounts = new long[length];\n    long[] combinedBytes = new long[length];\n\n    for (int i = 0; i < length; i++) {\n      combinedCounts[i] = this.fileCounts[i] + other.fileCounts[i];\n      combinedBytes[i] = this.totalBytes[i] + other.totalBytes[i];\n    }\n\n    return new FileSizeHistogram(\n        Arrays.copyOf(this.sortedBinBoundaries, length), combinedCounts, combinedBytes);\n  }\n\n  /**\n   * Subtracts another histogram from this one. Both histograms must have the same bin boundaries.\n   * The result will ensure no negative counts or bytes.\n   *\n   * @param other The histogram to subtract from this histogram\n   * @return A new histogram with the difference in statistics\n   * @throws IllegalArgumentException if the histograms have different bin boundaries\n   * @throws IllegalArgumentException if subtraction would result in negative counts or bytes\n   */\n  public FileSizeHistogram minus(FileSizeHistogram other) {\n    requireNonNull(other, \"histogram to minus cannot be null\");\n    checkArgument(\n        Arrays.equals(this.sortedBinBoundaries, other.sortedBinBoundaries),\n        \"Cannot subtract histograms with different bin boundaries\");\n\n    int length = this.sortedBinBoundaries.length;\n    long[] resultCounts = new long[length];\n    long[] resultBytes = new long[length];\n\n    for (int i = 0; i < length; i++) {\n      resultCounts[i] = this.fileCounts[i] - other.fileCounts[i];\n      resultBytes[i] = this.totalBytes[i] - other.totalBytes[i];\n\n      checkArgument(\n          resultCounts[i] >= 0 && resultBytes[i] >= 0,\n          \"Subtraction would result in negative counts or bytes at bin %d\",\n          i);\n    }\n\n    return new FileSizeHistogram(\n        Arrays.copyOf(this.sortedBinBoundaries, length), resultCounts, resultBytes);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (o == null || getClass() != o.getClass()) return false;\n\n    FileSizeHistogram that = (FileSizeHistogram) o;\n    return Arrays.equals(sortedBinBoundaries, that.sortedBinBoundaries)\n        && Arrays.equals(fileCounts, that.fileCounts)\n        && Arrays.equals(totalBytes, that.totalBytes);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(\n        Arrays.hashCode(sortedBinBoundaries),\n        Arrays.hashCode(fileCounts),\n        Arrays.hashCode(totalBytes));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/table/SnapshotBuilderImpl.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.table;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.SnapshotBuilder;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.files.LogDataUtils;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport io.delta.kernel.internal.lang.ListUtils;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.Tuple2;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * An implementation of {@link SnapshotBuilder}.\n *\n * <p>Note: The primary responsibility of this class is to take input, validate that input, and then\n * pass the input to the {@link SnapshotFactory}, which is then responsible for actually creating\n * the {@link Snapshot} instance.\n */\npublic class SnapshotBuilderImpl implements SnapshotBuilder {\n\n  public static class Context {\n    public final String unresolvedPath;\n    public Optional<Long> versionOpt = Optional.empty();\n    public Optional<Tuple2<SnapshotImpl, Long>> timestampQueryContextOpt = Optional.empty();\n    public Optional<Committer> committerOpt = Optional.empty();\n    public List<ParsedLogData> logDatas = Collections.emptyList();\n    public Optional<Tuple2<Protocol, Metadata>> protocolAndMetadataOpt = Optional.empty();\n    public Optional<Long> maxCatalogVersion = Optional.empty();\n\n    public Context(String unresolvedPath) {\n      this.unresolvedPath = requireNonNull(unresolvedPath, \"unresolvedPath is null\");\n    }\n  }\n\n  private final Context ctx;\n\n  public SnapshotBuilderImpl(String unresolvedPath) {\n    ctx = new Context(unresolvedPath);\n  }\n\n  ////////////////////////////////////\n  // Public SnapshotBuilder Methods //\n  ////////////////////////////////////\n\n  @Override\n  public SnapshotBuilderImpl atVersion(long version) {\n    checkArgument(version >= 0, \"version must be >= 0\");\n    ctx.versionOpt = Optional.of(version);\n    return this;\n  }\n\n  @Override\n  public SnapshotBuilderImpl atTimestamp(long millisSinceEpochUTC, Snapshot latestSnapshot) {\n    requireNonNull(latestSnapshot, \"latestSnapshot is null\");\n    checkArgument(latestSnapshot instanceof SnapshotImpl, \"latestSnapshot must be a SnapshotImpl\");\n    ctx.timestampQueryContextOpt =\n        Optional.of(new Tuple2<>((SnapshotImpl) latestSnapshot, millisSinceEpochUTC));\n    return this;\n  }\n\n  @Override\n  public SnapshotBuilderImpl withCommitter(Committer committer) {\n    ctx.committerOpt = Optional.of(requireNonNull(committer, \"committer is null\"));\n    return this;\n  }\n\n  @Override\n  public SnapshotBuilderImpl withLogData(List<ParsedLogData> logDatas) {\n    ctx.logDatas = requireNonNull(logDatas, \"logDatas is null\");\n    return this;\n  }\n\n  @Override\n  public SnapshotBuilderImpl withProtocolAndMetadata(Protocol protocol, Metadata metadata) {\n    ctx.protocolAndMetadataOpt =\n        Optional.of(\n            new Tuple2<>(\n                requireNonNull(protocol, \"protocol is null\"),\n                requireNonNull(metadata, \"metadata is null\")));\n    return this;\n  }\n\n  @Override\n  public SnapshotBuilderImpl withMaxCatalogVersion(long version) {\n    checkArgument(version >= 0, \"A valid version must be >= 0\");\n    ctx.maxCatalogVersion = Optional.of(version);\n    return this;\n  }\n\n  @Override\n  public SnapshotImpl build(Engine engine) {\n    validateInputOnBuild(engine);\n    return new SnapshotFactory(engine, ctx).create(engine);\n  }\n\n  ////////////////////////////\n  // Private Helper Methods //\n  ////////////////////////////\n\n  private void validateInputOnBuild(Engine engine) {\n    validateTimestampNotGreaterThanLatestSnapshot(engine);\n    validateVersionAndTimestampMutuallyExclusive();\n    validateProtocolAndMetadataOnlyIfVersionProvided();\n    validateProtocolRead();\n    // TODO: delta-io/delta#4765 support other types\n    LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(ctx.logDatas);\n    LogDataUtils.validateLogDataIsSortedContiguous(ctx.logDatas);\n    validateMaxCatalogVersionCompatibleWithTimeTravelParams();\n    validateLogTailEndsWithMaxCatalogVersionOrVersionToLoad();\n  }\n\n  /**\n   * Recall the semantics of time-travel by timestamp: \"If the provided timestamp is after (strictly\n   * greater than) the timestamp of the latest version of the table, snapshot resolution will fail.\"\n   */\n  private void validateTimestampNotGreaterThanLatestSnapshot(Engine engine) {\n    ctx.timestampQueryContextOpt.ifPresent(\n        x -> {\n          final long latestSnapshotVersion = x._1.getVersion();\n          final long latestSnapshotTimestamp = x._1.getTimestamp(engine);\n          final long requestedTimestamp = x._2;\n\n          if (requestedTimestamp > latestSnapshotTimestamp) {\n            throw DeltaErrors.timestampAfterLatestCommit(\n                ctx.unresolvedPath,\n                requestedTimestamp,\n                latestSnapshotTimestamp,\n                latestSnapshotVersion);\n          }\n        });\n  }\n\n  private void validateVersionAndTimestampMutuallyExclusive() {\n    checkArgument(\n        !ctx.timestampQueryContextOpt.isPresent() || !ctx.versionOpt.isPresent(),\n        \"timestamp and version cannot be provided together\");\n  }\n\n  private void validateProtocolAndMetadataOnlyIfVersionProvided() {\n    checkArgument(\n        ctx.versionOpt.isPresent() || !ctx.protocolAndMetadataOpt.isPresent(),\n        \"protocol and metadata can only be provided if a version is provided\");\n  }\n\n  private void validateProtocolRead() {\n    ctx.protocolAndMetadataOpt.ifPresent(\n        x -> TableFeatures.validateKernelCanReadTheTable(x._1, ctx.unresolvedPath));\n  }\n\n  /**\n   * For catalog managed tables we cannot time-travel to a version after the max catalog version. We\n   * also require that the latestSnapshot provided for timestamp-based queries has the max catalog\n   * version.\n   */\n  private void validateMaxCatalogVersionCompatibleWithTimeTravelParams() {\n    ctx.maxCatalogVersion.ifPresent(\n        maxVersion -> {\n          ctx.versionOpt.ifPresent(\n              version ->\n                  checkArgument(\n                      version <= maxVersion,\n                      String.format(\n                          \"Cannot time-travel to version %s after the max catalog version %s\",\n                          version, maxVersion)));\n          ctx.timestampQueryContextOpt.ifPresent(\n              queryContext ->\n                  checkArgument(\n                      queryContext._1.getVersion() == maxVersion,\n                      \"The latestSnapshot provided for timestamp-based time-travel queries \"\n                          + \"must have version = maxCatalogVersion\"));\n        });\n  }\n\n  /**\n   * When a catalog implementation has provided catalog commits we require that they provide up to\n   * and including the version that we will load (which for a latest query is the max catalog\n   * version, and for a time-travel-by-version query is the version to load). This is to validate\n   * that the catalog has queried and provided sufficient catalog commits to correctly read the\n   * table.\n   */\n  private void validateLogTailEndsWithMaxCatalogVersionOrVersionToLoad() {\n    ctx.maxCatalogVersion.ifPresent(\n        maxVersion -> {\n          if (!ctx.logDatas.isEmpty()) {\n            ParsedLogData tailLogData = ListUtils.getLast(ctx.logDatas);\n            if (ctx.versionOpt.isPresent()) {\n              checkArgument(\n                  tailLogData.getVersion() >= ctx.versionOpt.get(),\n                  \"Provided catalog commits must include versionToLoad for time-travel queries\");\n            } else {\n              checkArgument(\n                  maxVersion == tailLogData.getVersion(),\n                  \"Provided catalog commits must end with max catalog version\");\n            }\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/table/SnapshotFactory.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.table;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Utils.resolvePath;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.checksum.CRCInfo;\nimport io.delta.kernel.internal.checksum.ChecksumReader;\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.metrics.SnapshotMetrics;\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext;\nimport io.delta.kernel.internal.replay.LogReplay;\nimport io.delta.kernel.internal.replay.ProtocolMetadataLogReplay;\nimport io.delta.kernel.internal.snapshot.LogSegment;\nimport io.delta.kernel.internal.snapshot.SnapshotManager;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Factory class responsible for creating {@link Snapshot} instances.\n *\n * <p>This factory takes validated parameters from {@link SnapshotBuilderImpl} and orchestrates the\n * actual snapshot creation process. It handles path resolution, log segment loading, and\n * coordinates with various internal components to construct a fully initialized {@link Snapshot}.\n *\n * <p>Note: The {@link SnapshotBuilderImpl} is responsible for receiving and validating all builder\n * parameters, and then passing that information to this factory to actually create the {@link\n * Snapshot}.\n */\npublic class SnapshotFactory {\n\n  //////////////////////////////////////////\n  // Static utility methods and variables //\n  //////////////////////////////////////////\n\n  /**\n   * Resolves the latest table version that exists at or before the given {@code\n   * millisSinceEpochUTC}.\n   *\n   * <p>Updates the given {@code snapshotQueryCtx} with the resolved version and prints out useful\n   * log statements.\n   */\n  public static long resolveTimestampToSnapshotVersion(\n      Engine engine,\n      SnapshotQueryContext snapshotQueryCtx,\n      SnapshotImpl latestSnapshot,\n      long millisSinceEpochUTC,\n      List<ParsedLogData> logDatas) {\n    List<ParsedCatalogCommitData> parsedCatalogCommits =\n        logDatas.stream()\n            .filter(logData -> logData instanceof ParsedCatalogCommitData && logData.isFile())\n            .map(catalogCommit -> (ParsedCatalogCommitData) catalogCommit)\n            .collect(Collectors.toList());\n\n    final long resolvedVersionToLoad =\n        snapshotQueryCtx\n            .getSnapshotMetrics()\n            .computeTimestampToVersionTotalDurationTimer\n            .time(\n                () ->\n                    DeltaHistoryManager.getActiveCommitAtTimestamp(\n                            engine,\n                            latestSnapshot,\n                            latestSnapshot.getLogPath(),\n                            millisSinceEpochUTC,\n                            true /* mustBeRecreatable */,\n                            false /* canReturnLastCommit */,\n                            false /* canReturnEarliestCommit */,\n                            parsedCatalogCommits)\n                        .getVersion());\n\n    snapshotQueryCtx.setResolvedVersion(resolvedVersionToLoad);\n\n    logger.info(\n        \"{}: Took {} ms to resolve timestamp {} to snapshot version {}\",\n        latestSnapshot.getPath(),\n        snapshotQueryCtx\n            .getSnapshotMetrics()\n            .computeTimestampToVersionTotalDurationTimer\n            .totalDurationMs(),\n        millisSinceEpochUTC,\n        resolvedVersionToLoad);\n\n    return resolvedVersionToLoad;\n  }\n\n  /**\n   * Creates a lazy loader for CRC file information. The CRC file is loaded only once when needed.\n   *\n   * <p>If {@link Lazy#isPresent()} is false, then the CRC file was never attempted to be loaded.\n   *\n   * <p>If {@link Lazy#isPresent()} is true, then the result is:\n   *\n   * <ul>\n   *   <li>{@code Optional.empty()} if there is no CRC file in this LogSegment, we failed to read\n   *       it, or we failed to parse it (e.g. missing required fields)\n   *   <li>{@code Optional.of(crcInfo)} if the file exists and was successfully read and parsed\n   * </ul>\n   */\n  public static Lazy<Optional<CRCInfo>> createLazyChecksumFileLoaderWithMetrics(\n      Engine engine, Lazy<LogSegment> lazyLogSegment, SnapshotMetrics snapshotMetrics) {\n    return new Lazy<>(\n        () -> {\n          final Optional<FileStatus> crcFileOpt = lazyLogSegment.get().getLastSeenChecksum();\n          if (!crcFileOpt.isPresent()) {\n            return Optional.empty();\n          }\n          return snapshotMetrics.loadCrcTotalDurationTimer.time(\n              () -> ChecksumReader.tryReadChecksumFile(engine, crcFileOpt.get()));\n        });\n  }\n\n  private static final Logger logger = LoggerFactory.getLogger(SnapshotFactory.class);\n\n  //////////////////////////////////\n  // Member methods and variables //\n  //////////////////////////////////\n\n  private final SnapshotBuilderImpl.Context ctx;\n  private final Path tablePath;\n\n  SnapshotFactory(Engine engine, SnapshotBuilderImpl.Context ctx) {\n    this.ctx = ctx;\n    this.tablePath = new Path(resolvePath(engine, ctx.unresolvedPath));\n  }\n\n  SnapshotImpl create(Engine engine) {\n    final SnapshotQueryContext snapshotCtx = getSnapshotQueryContext();\n\n    try {\n      final SnapshotImpl snapshot =\n          snapshotCtx\n              .getSnapshotMetrics()\n              .loadSnapshotTotalTimer\n              .time(() -> createSnapshot(engine, snapshotCtx));\n\n      logger.info(\n          \"[{}] Took {}ms to load snapshot (version = {}) for snapshot query {}\",\n          tablePath.toString(),\n          snapshotCtx.getSnapshotMetrics().loadSnapshotTotalTimer.totalDurationMs(),\n          snapshot.getVersion(),\n          snapshotCtx.getQueryDisplayStr());\n\n      engine\n          .getMetricsReporters()\n          .forEach(reporter -> reporter.report(snapshot.getSnapshotReport()));\n\n      return snapshot;\n    } catch (Exception e) {\n      snapshotCtx.recordSnapshotErrorReport(engine, e);\n      throw e;\n    }\n  }\n\n  private SnapshotImpl createSnapshot(Engine engine, SnapshotQueryContext snapshotCtx) {\n    final Optional<Long> timeTravelVersion = getTargetTimeTravelVersion(engine, snapshotCtx);\n    final Lazy<LogSegment> lazyLogSegment =\n        getLazyLogSegment(engine, snapshotCtx, timeTravelVersion);\n    final Lazy<Optional<CRCInfo>> lazyCrcInfo =\n        createLazyChecksumFileLoaderWithMetrics(\n            engine, lazyLogSegment, snapshotCtx.getSnapshotMetrics());\n\n    Protocol protocol;\n    Metadata metadata;\n\n    if (ctx.protocolAndMetadataOpt.isPresent()) {\n      protocol = ctx.protocolAndMetadataOpt.get()._1;\n      metadata = ctx.protocolAndMetadataOpt.get()._2;\n    } else {\n      ProtocolMetadataLogReplay.Result result =\n          ProtocolMetadataLogReplay.loadProtocolAndMetadata(\n              engine,\n              tablePath,\n              lazyLogSegment.get(),\n              lazyCrcInfo,\n              snapshotCtx.getSnapshotMetrics());\n      protocol = result.protocol;\n      metadata = result.metadata;\n    }\n\n    // We require maxCatalogVersion to be provided for catalogManaged tables. We cannot validate\n    // this earlier since we need to first load the protocol.\n    validateMaxCatalogVersionPresence(protocol);\n\n    // TODO: When LogReplay becomes static utilities, we can create it inside of SnapshotImpl\n    final LogReplay logReplay = new LogReplay(engine, tablePath, lazyLogSegment, lazyCrcInfo);\n\n    return new SnapshotImpl(\n        tablePath,\n        timeTravelVersion.orElseGet(() -> lazyLogSegment.get().getVersion()),\n        lazyLogSegment,\n        logReplay,\n        protocol,\n        metadata,\n        ctx.committerOpt.orElse(DefaultFileSystemManagedTableOnlyCommitter.INSTANCE),\n        snapshotCtx,\n        Optional.empty() /* inCommitTimestampOpt */);\n  }\n\n  private SnapshotQueryContext getSnapshotQueryContext() {\n    if (ctx.versionOpt.isPresent()) {\n      return SnapshotQueryContext.forVersionSnapshot(tablePath.toString(), ctx.versionOpt.get());\n    }\n    if (ctx.timestampQueryContextOpt.isPresent()) {\n      return SnapshotQueryContext.forTimestampSnapshot(\n          tablePath.toString(), ctx.timestampQueryContextOpt.get()._2);\n    }\n    return SnapshotQueryContext.forLatestSnapshot(tablePath.toString());\n  }\n\n  private Lazy<LogSegment> getLazyLogSegment(\n      Engine engine, SnapshotQueryContext snapshotCtx, Optional<Long> timeTravelVersion) {\n    return new Lazy<>(\n        () -> {\n          final LogSegment logSegment =\n              snapshotCtx\n                  .getSnapshotMetrics()\n                  .loadLogSegmentTotalDurationTimer\n                  .time(\n                      () ->\n                          new SnapshotManager(tablePath)\n                              .getLogSegmentForVersion(\n                                  engine, timeTravelVersion, ctx.logDatas, ctx.maxCatalogVersion));\n\n          snapshotCtx.setResolvedVersion(logSegment.getVersion());\n          snapshotCtx.setCheckpointVersion(logSegment.getCheckpointVersionOpt());\n\n          return logSegment;\n        });\n  }\n\n  private Optional<Long> getTargetTimeTravelVersion(\n      Engine engine, SnapshotQueryContext snapshotCtx) {\n    if (ctx.timestampQueryContextOpt.isPresent()) {\n      return Optional.of(\n          resolveTimestampToSnapshotVersion(\n              engine,\n              snapshotCtx,\n              ctx.timestampQueryContextOpt.get()._1,\n              ctx.timestampQueryContextOpt.get()._2,\n              ctx.logDatas));\n    } else if (ctx.versionOpt.isPresent()) {\n      return ctx.versionOpt;\n    }\n    return Optional.empty();\n  }\n\n  private void validateMaxCatalogVersionPresence(Protocol protocol) {\n    boolean isCatalogManaged = TableFeatures.isCatalogManagedSupported(protocol);\n    if (isCatalogManaged) {\n      checkArgument(\n          ctx.maxCatalogVersion.isPresent(),\n          \"Must provide maxCatalogVersion for catalogManaged tables\");\n    } else {\n      checkArgument(\n          !ctx.maxCatalogVersion.isPresent(),\n          \"Should not provide maxCatalogVersion for file-system managed tables\");\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/tablefeatures/FeatureAutoEnabledByMetadata.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.tablefeatures;\n\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\n\n/**\n * Defines behavior for {@link TableFeature} that can be automatically enabled via a change in a\n * table's metadata, e.g., through setting particular values of certain feature-specific table\n * properties. When the requirements are satisfied, the feature is automatically enabled.\n */\npublic interface FeatureAutoEnabledByMetadata {\n  /**\n   * Determine whether the feature must be supported and enabled because its metadata requirements\n   * are satisfied.\n   *\n   * @param protocol the protocol of the table for features that are already enabled.\n   * @param metadata the metadata of the table for properties that can enable the feature.\n   */\n  boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/tablefeatures/TableFeature.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.tablefeatures;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.internal.actions.Metadata;\nimport java.util.Collections;\nimport java.util.Set;\n\n/**\n * Base class for table features.\n *\n * <p>A feature can be <b>explicitly supported</b> by a table's protocol when the protocol contains\n * a feature's `name`. Writers (for writer-only features) or readers and writers (for reader-writer\n * features) must recognize supported features and must handle them appropriately.\n *\n * <p>A table feature that released before Delta Table Features (reader version 3 and writer version\n * 7) is considered as a <strong>legacy feature</strong>. Legacy features are <strong> implicitly\n * supported</strong> when (a) the protocol does not support table features, i.e., has reader\n * version less than 3 or writer version less than 7 and (b) the feature's minimum reader/writer\n * version is less than or equal to the current protocol's reader/writer version.\n *\n * <p>Separately, a feature can be automatically supported by a table's metadata when certain\n * feature-specific table properties are set. For example, `changeDataFeed` is automatically\n * supported when there's a table property `delta.enableChangeDataFeed=true`. See {@link\n * FeatureAutoEnabledByMetadata} for details on how to define such features. This is independent of\n * the table's enabled features. When a feature is supported (explicitly or implicitly) by the table\n * protocol but its metadata requirements are not satisfied, then clients still have to understand\n * the feature (at least to the extent that they can read and preserve the existing data in the\n * table that uses the feature).\n *\n * <p>Important note: uses the default implementation of `equals` and `hashCode` methods. We expect\n * that the feature instances are singletons, so we don't need to compare the fields.\n */\npublic abstract class TableFeature {\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Instance variables.                                                       ///\n  /////////////////////////////////////////////////////////////////////////////////\n  private final String featureName;\n  private final int minReaderVersion;\n  private final int minWriterVersion;\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Public methods.                                                           ///\n  /////////////////////////////////////////////////////////////////////////////////\n  /**\n   * Constructor. Does validations to make sure:\n   *\n   * <ul>\n   *   <li>Feature name is not null or empty and has valid characters\n   *   <li>minReaderVersion is always 0 for writer features\n   * </ul>\n   *\n   * @param featureName a globally-unique string indicator to represent the feature. All characters\n   *     must be letters (a-z, A-Z), digits (0-9), '-', or '_'. Words must be in camelCase.\n   * @param minReaderVersion the minimum reader version this feature requires. For a feature that\n   *     can only be explicitly supported, this is either `0` (i.e writerOnly feature) or `3` (the\n   *     reader protocol version that supports table features), depending on the feature is\n   *     writer-only or reader-writer. For a legacy feature that can be implicitly supported, this\n   *     is the first protocol version which the feature is introduced.\n   * @param minWriterVersion the minimum writer version this feature requires. For a feature that\n   *     can only be explicitly supported, this is the writer protocol `7` that supports table\n   *     features. For a legacy feature that can be implicitly supported, this is the first protocol\n   *     version which the feature is introduced.\n   */\n  public TableFeature(String featureName, int minReaderVersion, int minWriterVersion) {\n    this.featureName = requireNonNull(featureName, \"name is null\");\n    checkArgument(!featureName.isEmpty(), \"name is empty\");\n    checkArgument(\n        featureName.chars().allMatch(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_'),\n        \"name contains invalid characters: \" + featureName);\n    checkArgument(minReaderVersion >= 0, \"minReaderVersion is negative\");\n    checkArgument(minWriterVersion >= 1, \"minWriterVersion is less than 1\");\n    this.minReaderVersion = minReaderVersion;\n    this.minWriterVersion = minWriterVersion;\n\n    validate();\n  }\n\n  /** @return the name of the table feature. */\n  public String featureName() {\n    return featureName;\n  }\n\n  /**\n   * @return true if this feature is applicable to both reader and writer, false if it is\n   *     writer-only.\n   */\n  public boolean isReaderWriterFeature() {\n    return this instanceof ReaderWriterFeatureType;\n  }\n\n  /** @return the minimum reader version this feature requires */\n  public int minReaderVersion() {\n    return minReaderVersion;\n  }\n\n  /** @return the minimum writer version that this feature requires. */\n  public int minWriterVersion() {\n    return minWriterVersion;\n  }\n\n  /** @return if this feature is a legacy feature? */\n  public boolean isLegacyFeature() {\n    return this instanceof LegacyFeatureType;\n  }\n\n  /**\n   * Set of table features that this table feature depends on. I.e. the set of features that need to\n   * be enabled if this table feature is enabled.\n   *\n   * @return the set of table features that this table feature depends on.\n   */\n  public Set<TableFeature> requiredFeatures() {\n    return Collections.emptySet();\n  }\n\n  /**\n   * Does Kernel has support to read a table containing this feature? Default implementation returns\n   * true. Features should override this method if they have special requirements or not supported\n   * by the Kernel yet.\n   *\n   * @return true if Kernel has support to read a table containing this feature.\n   */\n  public boolean hasKernelReadSupport() {\n    checkArgument(isReaderWriterFeature(), \"Should be called only for reader-writer features\");\n    return true;\n  }\n\n  /**\n   * Does Kernel has support to write a table containing this feature? Default implementation\n   * returns true. Features should override this method if they have special requirements or not\n   * supported by the Kernel yet.\n   *\n   * @param metadata the metadata of the table. Sometimes checking the metadata is necessary to know\n   *     the Kernel can write the table or not.\n   * @return true if Kernel has support to write a table containing this feature.\n   */\n  public boolean hasKernelWriteSupport(Metadata metadata) {\n    return true;\n  }\n\n  /**\n   * Gets the key that turns on support for the respective table feature.\n   *\n   * @return the feature support key for the respective feature.\n   */\n  public String getTableFeatureSupportKey() {\n    return TableFeatures.SET_TABLE_FEATURE_SUPPORTED_PREFIX + featureName();\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Define the {@link TableFeature}s traits that define behavior/attributes.  ///\n  /////////////////////////////////////////////////////////////////////////////////\n  /**\n   * An interface to indicate a feature is legacy, i.e., released before Table Features. All legacy\n   * features are auto enabled by metadata.\n   */\n  public interface LegacyFeatureType extends FeatureAutoEnabledByMetadata {}\n\n  /** An interface to indicate a feature applies to readers and writers. */\n  public interface ReaderWriterFeatureType {}\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Base classes for each of the feature category.                            ///\n  /////////////////////////////////////////////////////////////////////////////////\n  /** A base class for all table legacy writer-only features. */\n  public abstract static class LegacyWriterFeature extends TableFeature\n      implements LegacyFeatureType {\n    public LegacyWriterFeature(String featureName, int minWriterVersion) {\n      super(featureName, /* minReaderVersion= */ 0, minWriterVersion);\n    }\n\n    @Override\n    public boolean hasKernelReadSupport() {\n      return true;\n    }\n  }\n\n  /** A base class for all table legacy reader-writer features. */\n  public abstract static class LegacyReaderWriterFeature extends TableFeature\n      implements LegacyFeatureType, ReaderWriterFeatureType {\n    public LegacyReaderWriterFeature(\n        String featureName, int minReaderVersion, int minWriterVersion) {\n      super(featureName, minReaderVersion, minWriterVersion);\n    }\n  }\n\n  /** A base class for all non-legacy table writer features. */\n  public abstract static class WriterFeature extends TableFeature {\n    public WriterFeature(String featureName, int minWriterVersion) {\n      super(featureName, /* minReaderVersion= */ 0, minWriterVersion);\n    }\n\n    @Override\n    public boolean hasKernelReadSupport() {\n      return true;\n    }\n  }\n\n  /** A base class for all non-legacy table reader-writer features. */\n  public abstract static class ReaderWriterFeature extends TableFeature\n      implements ReaderWriterFeatureType {\n    public ReaderWriterFeature(String featureName, int minReaderVersion, int minWriterVersion) {\n      super(featureName, minReaderVersion, minWriterVersion);\n    }\n  }\n\n  /**\n   * Validate the table feature. This method should throw an exception if the table feature\n   * properties are invalid. Should be called after the object deriving the {@link TableFeature} is\n   * constructed.\n   */\n  private void validate() {\n    if (!isReaderWriterFeature()) {\n      checkArgument(minReaderVersion() == 0, \"Writer-only feature must have minReaderVersion=0\");\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/tablefeatures/TableFeatures.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.tablefeatures;\n\nimport static io.delta.kernel.internal.DeltaErrors.*;\nimport static io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode.NONE;\nimport static io.delta.kernel.types.TimestampNTZType.TIMESTAMP_NTZ;\nimport static io.delta.kernel.types.VariantType.VARIANT;\nimport static java.util.stream.Collectors.toMap;\nimport static java.util.stream.Collectors.toSet;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.util.CaseInsensitiveMap;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Stream;\n\n/** Contains utility methods related to the Delta table feature support in protocol. */\npublic class TableFeatures {\n\n  /**\n   * The prefix for setting an override of a feature option in {@linkplain Metadata} configuration.\n   *\n   * <p>Keys with this prefix should never be persisted in the Metadata action. The keys can be\n   * filtered out by using {@linkplain #extractFeaturePropertyOverrides}.\n   *\n   * <p>These overrides only support add the feature as supported in the Protocol action.\n   *\n   * <p>Disabling features via this method is unsupported.\n   */\n  public static String SET_TABLE_FEATURE_SUPPORTED_PREFIX = \"delta.feature.\";\n\n  /**\n   * Configuration value to turn on support for a table feature when used with {@link\n   * #SET_TABLE_FEATURE_SUPPORTED_PREFIX}.\n   *\n   * <p>Example: {@code \"delta.feature.myFeature\" -> \"supported\"}\n   */\n  public static String SET_TABLE_FEATURE_SUPPORTED_VALUE = \"supported\";\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// START: Define the {@link TableFeature}s                                   ///\n  /// If feature instance variable ends with                                    ///\n  ///  1) `_W_FEATURE` it is a writer only feature.                             ///\n  ///  2) `_RW_FEATURE` it is a reader-writer feature.                          ///\n  /////////////////////////////////////////////////////////////////////////////////\n  public static final TableFeature APPEND_ONLY_W_FEATURE = new AppendOnlyFeature();\n\n  private static class AppendOnlyFeature extends TableFeature.LegacyWriterFeature {\n    AppendOnlyFeature() {\n      super(/* featureName = */ \"appendOnly\", /* minWriterVersion = */ 2);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.APPEND_ONLY_ENABLED.fromMetadata(metadata);\n    }\n\n    @Override\n    public boolean hasKernelWriteSupport(Metadata metadata) {\n      return true;\n    }\n  }\n\n  public static final TableFeature CATALOG_MANAGED_RW_FEATURE =\n      new CatalogManagedFeatureBase(\"catalogManaged\");\n\n  private static class CatalogManagedFeatureBase extends TableFeature.ReaderWriterFeature {\n    CatalogManagedFeatureBase(String featureName) {\n      super(featureName, /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public Set<TableFeature> requiredFeatures() {\n      return Collections.singleton(IN_COMMIT_TIMESTAMP_W_FEATURE);\n    }\n  }\n\n  public static final TableFeature INVARIANTS_W_FEATURE = new InvariantsFeature();\n\n  private static class InvariantsFeature extends TableFeature.LegacyWriterFeature {\n    InvariantsFeature() {\n      super(/* featureName = */ \"invariants\", /* minWriterVersion = */ 2);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return hasInvariants(metadata.getSchema());\n    }\n\n    @Override\n    public boolean hasKernelWriteSupport(Metadata metadata) {\n      // If there is no invariant, then the table is supported\n      return !hasInvariants(metadata.getSchema());\n    }\n  }\n\n  public static final TableFeature CONSTRAINTS_W_FEATURE = new ConstraintsFeature();\n\n  private static class ConstraintsFeature extends TableFeature.LegacyWriterFeature {\n    ConstraintsFeature() {\n      super(\"checkConstraints\", /* minWriterVersion = */ 3);\n    }\n\n    @Override\n    public boolean hasKernelWriteSupport(Metadata metadata) {\n      // Kernel doesn't support table with constraints.\n      return !hasCheckConstraints(metadata);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return hasCheckConstraints(metadata);\n    }\n  }\n\n  public static final TableFeature CHANGE_DATA_FEED_W_FEATURE = new ChangeDataFeedFeature();\n\n  private static class ChangeDataFeedFeature extends TableFeature.LegacyWriterFeature {\n    ChangeDataFeedFeature() {\n      super(\"changeDataFeed\", /* minWriterVersion = */ 4);\n    }\n\n    @Override\n    public boolean hasKernelWriteSupport(Metadata metadata) {\n      // writable if change data feed is disabled\n      return !TableConfig.CHANGE_DATA_FEED_ENABLED.fromMetadata(metadata);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.CHANGE_DATA_FEED_ENABLED.fromMetadata(metadata);\n    }\n  }\n\n  public static final TableFeature COLUMN_MAPPING_RW_FEATURE = new ColumnMappingFeature();\n\n  private static class ColumnMappingFeature extends TableFeature.LegacyReaderWriterFeature {\n    ColumnMappingFeature() {\n      super(\"columnMapping\", /*minReaderVersion = */ 2, /* minWriterVersion = */ 5);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.COLUMN_MAPPING_MODE.fromMetadata(metadata) != NONE;\n    }\n  }\n\n  public static final TableFeature GENERATED_COLUMNS_W_FEATURE = new GeneratedColumnsFeature();\n\n  private static class GeneratedColumnsFeature extends TableFeature.LegacyWriterFeature {\n    GeneratedColumnsFeature() {\n      super(\"generatedColumns\", /* minWriterVersion = */ 4);\n    }\n\n    @Override\n    public boolean hasKernelWriteSupport(Metadata metadata) {\n      // Kernel can write as long as there are no generated columns defined\n      return !hasGeneratedColumns(metadata);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return hasGeneratedColumns(metadata);\n    }\n  }\n\n  public static final TableFeature IDENTITY_COLUMNS_W_FEATURE = new IdentityColumnsFeature();\n\n  private static class IdentityColumnsFeature extends TableFeature.LegacyWriterFeature {\n    IdentityColumnsFeature() {\n      super(\"identityColumns\", /* minWriterVersion = */ 6);\n    }\n\n    @Override\n    public boolean hasKernelWriteSupport(Metadata metadata) {\n      return !hasIdentityColumns(metadata);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return hasIdentityColumns(metadata);\n    }\n  }\n\n  /* ---- Start: variantType ---- */\n  // Base class for variantType and variantType-preview features. Both features are same in terms\n  // of behavior and given the feature is graduated, we will enable the `variantType` by default\n  // if the metadata requirements are satisfied and the table doesn't already contain the\n  // `variantType-preview` feature. Also to note, with this version of Kernel, one can't\n  // auto upgrade to `variantType-preview` with metadata requirements. It can only be set\n  // manually using `delta.feature.variantType-preview=supported` property.\n  private static class VariantTypeTableFeatureBase extends TableFeature.ReaderWriterFeature {\n    VariantTypeTableFeatureBase(String featureName) {\n      super(featureName, /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n  }\n\n  private static class VariantTypeTableFeature extends VariantTypeTableFeatureBase\n      implements FeatureAutoEnabledByMetadata {\n    VariantTypeTableFeature() {\n      super(\"variantType\");\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return hasTypeColumn(metadata.getSchema(), VARIANT)\n          &&\n          // Don't automatically enable the stable feature if the preview feature is\n          // already supported, to avoid possibly breaking old clients that only\n          // support the preview feature.\n          !protocol.supportsFeature(VARIANT_RW_PREVIEW_FEATURE);\n    }\n  }\n\n  public static final TableFeature VARIANT_RW_FEATURE = new VariantTypeTableFeature();\n  public static final TableFeature VARIANT_RW_PREVIEW_FEATURE =\n      new VariantTypeTableFeatureBase(\"variantType-preview\");\n  /* ---- End: variantType ---- */\n\n  /* ---- Start: variantShredding ---- */\n  // Base class for variantShredding and variantShredding-preview features. Both features have\n  // identical behavior. Now that variantShredding has graduated to GA:\n  //\n  // - When `delta.enableVariantShredding` is set to true, the GA feature (`variantShredding`)\n  //   is automatically enabled unless the table already has `variantShredding-preview` in its\n  //   protocol (to avoid breaking clients that only understand the preview feature).\n  // - The preview feature (`variantShredding-preview`) is never auto-enabled. To use it on a\n  //   new table, a user must explicitly set `delta.feature.variantShredding-preview=supported`\n  //   in the table properties.\n  private static class VariantShreddingTableFeatureBase extends TableFeature.ReaderWriterFeature {\n    VariantShreddingTableFeatureBase(String featureName) {\n      super(featureName, /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public Set<TableFeature> requiredFeatures() {\n      return new HashSet<>(Arrays.asList(TableFeatures.VARIANT_RW_FEATURE));\n    }\n  }\n\n  private static class VariantShreddingTableFeature extends VariantShreddingTableFeatureBase\n      implements FeatureAutoEnabledByMetadata {\n    VariantShreddingTableFeature() {\n      super(\"variantShredding\");\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.VARIANT_SHREDDING_ENABLED.fromMetadata(metadata)\n          &&\n          // Don't automatically enable the stable feature if the preview feature is\n          // already supported, to avoid possibly breaking old clients that only\n          // support the preview feature.\n          !protocol.supportsFeature(VARIANT_SHREDDING_PREVIEW_RW_FEATURE);\n    }\n  }\n\n  public static final TableFeature VARIANT_SHREDDING_RW_FEATURE =\n      new VariantShreddingTableFeature();\n  public static final TableFeature VARIANT_SHREDDING_PREVIEW_RW_FEATURE =\n      new VariantShreddingTableFeatureBase(\"variantShredding-preview\");\n  /* ---- End: variantShredding ---- */\n\n  public static final TableFeature DOMAIN_METADATA_W_FEATURE = new DomainMetadataFeature();\n\n  private static class DomainMetadataFeature extends TableFeature.WriterFeature {\n    DomainMetadataFeature() {\n      super(\"domainMetadata\", /* minWriterVersion = */ 7);\n    }\n  }\n\n  public static final TableFeature CLUSTERING_W_FEATURE = new ClusteringTableFeature();\n\n  private static class ClusteringTableFeature extends TableFeature.WriterFeature {\n    ClusteringTableFeature() {\n      super(\"clustering\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public Set<TableFeature> requiredFeatures() {\n      return Collections.singleton(DOMAIN_METADATA_W_FEATURE);\n    }\n  }\n\n  public static final TableFeature ROW_TRACKING_W_FEATURE = new RowTrackingFeature();\n\n  private static class RowTrackingFeature extends TableFeature.WriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    RowTrackingFeature() {\n      super(\"rowTracking\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.ROW_TRACKING_ENABLED.fromMetadata(metadata);\n    }\n\n    @Override\n    public Set<TableFeature> requiredFeatures() {\n      return Collections.singleton(DOMAIN_METADATA_W_FEATURE);\n    }\n  }\n\n  public static final TableFeature DELETION_VECTORS_RW_FEATURE = new DeletionVectorsTableFeature();\n\n  /**\n   * Kernel currently only support blind appends. So we don't need to do anything special for\n   * writing into a table with deletion vectors enabled (i.e a table feature with DV enabled is both\n   * readable and writable).\n   */\n  private static class DeletionVectorsTableFeature extends TableFeature.ReaderWriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    DeletionVectorsTableFeature() {\n      super(\"deletionVectors\", /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.DELETION_VECTORS_CREATION_ENABLED.fromMetadata(metadata);\n    }\n  }\n\n  public static final TableFeature ICEBERG_COMPAT_V2_W_FEATURE = new IcebergCompatV2TableFeature();\n\n  private static class IcebergCompatV2TableFeature extends TableFeature.WriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    IcebergCompatV2TableFeature() {\n      super(\"icebergCompatV2\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(metadata);\n    }\n\n    public @Override Set<TableFeature> requiredFeatures() {\n      return Collections.singleton(COLUMN_MAPPING_RW_FEATURE);\n    }\n  }\n\n  public static final TableFeature ICEBERG_COMPAT_V3_W_FEATURE = new IcebergCompatV3TableFeature();\n\n  private static class IcebergCompatV3TableFeature extends TableFeature.WriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    IcebergCompatV3TableFeature() {\n      super(\"icebergCompatV3\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(metadata);\n    }\n\n    public @Override Set<TableFeature> requiredFeatures() {\n      return Collections.unmodifiableSet(\n          new HashSet<>(Arrays.asList(COLUMN_MAPPING_RW_FEATURE, ROW_TRACKING_W_FEATURE)));\n    }\n  }\n\n  /* ---- Start: type widening ---- */\n  // Base class for typeWidening and typeWidening-preview features. Both features are same in terms\n  // of behavior and given the feature is graduated, we will enable the `typeWidening` by default\n  // if the metadata requirements are satisfied and the table doesn't already contain the\n  // `typeWidening-preview` feature. Also to note, with this version of Kernel, one can't\n  // auto upgrade to `typeWidening-preview` with metadata requirements. It can only be set\n  // manually using `delta.feature.typeWidening-preview=supported` property.\n  private static class TypeWideningTableFeatureBase extends TableFeature.ReaderWriterFeature {\n    TypeWideningTableFeatureBase(String featureName) {\n      super(featureName, /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n  }\n\n  private static class TypeWideningTableFeature extends TypeWideningTableFeatureBase\n      implements FeatureAutoEnabledByMetadata {\n    TypeWideningTableFeature() {\n      super(\"typeWidening\");\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.TYPE_WIDENING_ENABLED.fromMetadata(metadata)\n          &&\n          // Don't automatically enable the stable feature if the preview feature is already\n          // supported, to\n          // avoid possibly breaking old clients that only support the preview feature.\n          !protocol.supportsFeature(TYPE_WIDENING_RW_PREVIEW_FEATURE);\n    }\n  }\n\n  public static final TableFeature TYPE_WIDENING_RW_FEATURE = new TypeWideningTableFeature();\n\n  public static final TableFeature TYPE_WIDENING_RW_PREVIEW_FEATURE =\n      new TypeWideningTableFeatureBase(\"typeWidening-preview\");\n  /* ---- End: type widening ---- */\n\n  public static final TableFeature IN_COMMIT_TIMESTAMP_W_FEATURE =\n      new InCommitTimestampTableFeature();\n\n  private static class InCommitTimestampTableFeature extends TableFeature.WriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    InCommitTimestampTableFeature() {\n      super(\"inCommitTimestamp\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(metadata);\n    }\n  }\n\n  public static final TableFeature TIMESTAMP_NTZ_RW_FEATURE = new TimestampNtzTableFeature();\n\n  private static class TimestampNtzTableFeature extends TableFeature.ReaderWriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    TimestampNtzTableFeature() {\n      super(\"timestampNtz\", /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return hasTypeColumn(metadata.getSchema(), TIMESTAMP_NTZ);\n    }\n  }\n\n  public static final TableFeature CHECKPOINT_V2_RW_FEATURE = new CheckpointV2TableFeature();\n\n  /**\n   * In order to commit, there is no extra work required when v2 checkpoint is enabled. This affects\n   * the checkpoint format only. When v2 is enabled, writing classic checkpoints is still allowed.\n   */\n  private static class CheckpointV2TableFeature extends TableFeature.ReaderWriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    CheckpointV2TableFeature() {\n      super(\"v2Checkpoint\", /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      // TODO: define an enum for checkpoint policy when we start supporting writing v2 checkpoints\n      return \"v2\".equals(TableConfig.CHECKPOINT_POLICY.fromMetadata(metadata));\n    }\n  }\n\n  public static final TableFeature VACUUM_PROTOCOL_CHECK_RW_FEATURE =\n      new VacuumProtocolCheckTableFeature();\n\n  private static class VacuumProtocolCheckTableFeature extends TableFeature.ReaderWriterFeature {\n    VacuumProtocolCheckTableFeature() {\n      super(\"vacuumProtocolCheck\", /* minReaderVersion = */ 3, /* minWriterVersion = */ 7);\n    }\n  }\n\n  public static final TableFeature CHECKPOINT_PROTECTION_W_FEATURE =\n      new CheckpointProtectionTableFeature();\n\n  private static class CheckpointProtectionTableFeature extends TableFeature.WriterFeature {\n    CheckpointProtectionTableFeature() {\n      super(\"checkpointProtection\", /* minWriterVersion = */ 7);\n    }\n  }\n\n  /**\n   * Support reading / metadata writes on tables with the feature. Don't support writing new data\n   * rows with default values. Don't allow updating the types of columns with default values.\n   */\n  public static final TableFeature ALLOW_COLUMN_DEFAULTS_W_FEATURE =\n      new AllowColumnDefaultsTableFeature();\n\n  private static class AllowColumnDefaultsTableFeature extends TableFeature.WriterFeature {\n    AllowColumnDefaultsTableFeature() {\n      super(\"allowColumnDefaults\", /* minWriterVersion = */ 7);\n    }\n  }\n\n  public static final TableFeature ICEBERG_WRITER_COMPAT_V1 = new IcebergWriterCompatV1();\n\n  private static class IcebergWriterCompatV1 extends TableFeature.WriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    IcebergWriterCompatV1() {\n      super(\"icebergWriterCompatV1\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.fromMetadata(metadata);\n    }\n\n    public @Override Set<TableFeature> requiredFeatures() {\n      return Collections.singleton(ICEBERG_COMPAT_V2_W_FEATURE);\n    }\n  }\n\n  public static final TableFeature ICEBERG_WRITER_COMPAT_V3 = new IcebergWriterCompatV3();\n\n  private static class IcebergWriterCompatV3 extends TableFeature.WriterFeature\n      implements FeatureAutoEnabledByMetadata {\n    IcebergWriterCompatV3() {\n      super(\"icebergWriterCompatV3\", /* minWriterVersion = */ 7);\n    }\n\n    @Override\n    public boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata) {\n      return TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.fromMetadata(metadata);\n    }\n\n    public @Override Set<TableFeature> requiredFeatures() {\n      return Collections.singleton(ICEBERG_COMPAT_V3_W_FEATURE);\n    }\n  }\n\n  public static final TableFeature MATERIALIZE_PARTITION_COLUMNS_W_FEATURE =\n      new MaterializePartitionColumnsFeature();\n\n  private static class MaterializePartitionColumnsFeature extends TableFeature.WriterFeature {\n    MaterializePartitionColumnsFeature() {\n      super(\"materializePartitionColumns\", /* minWriterVersion = */ 7);\n    }\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// END: Define the {@link TableFeature}s                                     ///\n  /////////////////////////////////////////////////////////////////////////////////\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Public static variables and methods                                       ///\n  /////////////////////////////////////////////////////////////////////////////////\n  /** Min reader version that supports reader features. */\n  public static final int TABLE_FEATURES_MIN_READER_VERSION = 3;\n\n  /** Min reader version that supports writer features. */\n  public static final int TABLE_FEATURES_MIN_WRITER_VERSION = 7;\n\n  public static final List<TableFeature> TABLE_FEATURES =\n      Collections.unmodifiableList(\n          Arrays.asList(\n              ALLOW_COLUMN_DEFAULTS_W_FEATURE,\n              APPEND_ONLY_W_FEATURE,\n              CATALOG_MANAGED_RW_FEATURE,\n              CHECKPOINT_PROTECTION_W_FEATURE,\n              CHECKPOINT_V2_RW_FEATURE,\n              CHANGE_DATA_FEED_W_FEATURE,\n              CLUSTERING_W_FEATURE,\n              COLUMN_MAPPING_RW_FEATURE,\n              CONSTRAINTS_W_FEATURE,\n              DELETION_VECTORS_RW_FEATURE,\n              GENERATED_COLUMNS_W_FEATURE,\n              DOMAIN_METADATA_W_FEATURE,\n              ICEBERG_COMPAT_V2_W_FEATURE,\n              ICEBERG_COMPAT_V3_W_FEATURE,\n              IDENTITY_COLUMNS_W_FEATURE,\n              IN_COMMIT_TIMESTAMP_W_FEATURE,\n              INVARIANTS_W_FEATURE,\n              MATERIALIZE_PARTITION_COLUMNS_W_FEATURE,\n              ROW_TRACKING_W_FEATURE,\n              TIMESTAMP_NTZ_RW_FEATURE,\n              TYPE_WIDENING_RW_PREVIEW_FEATURE,\n              TYPE_WIDENING_RW_FEATURE,\n              VACUUM_PROTOCOL_CHECK_RW_FEATURE,\n              VARIANT_RW_FEATURE,\n              VARIANT_RW_PREVIEW_FEATURE,\n              VARIANT_SHREDDING_RW_FEATURE,\n              VARIANT_SHREDDING_PREVIEW_RW_FEATURE,\n              ICEBERG_WRITER_COMPAT_V1,\n              ICEBERG_WRITER_COMPAT_V3));\n\n  public static final Map<String, TableFeature> TABLE_FEATURE_MAP =\n      Collections.unmodifiableMap(\n          new CaseInsensitiveMap<TableFeature>() {\n            {\n              for (TableFeature feature : TABLE_FEATURES) {\n                put(feature.featureName(), feature);\n              }\n            }\n          });\n\n  /** Get the table feature by name. Case-insensitive lookup. If not found, throws error. */\n  public static TableFeature getTableFeature(String featureName) {\n    TableFeature tableFeature = TABLE_FEATURE_MAP.get(featureName);\n    if (tableFeature == null) {\n      throw DeltaErrors.unsupportedTableFeature(featureName);\n    }\n    return tableFeature;\n  }\n\n  /** Does reader version supports explicitly specifying reader feature set in protocol? */\n  public static boolean supportsReaderFeatures(int minReaderVersion) {\n    return minReaderVersion >= TABLE_FEATURES_MIN_READER_VERSION;\n  }\n\n  /** Does writer version supports explicitly specifying writer feature set in protocol? */\n  public static boolean supportsWriterFeatures(int minWriterVersion) {\n    return minWriterVersion >= TABLE_FEATURES_MIN_WRITER_VERSION;\n  }\n\n  /** Returns the minimum reader/writer versions required to support all provided features. */\n  public static Tuple2<Integer, Integer> minimumRequiredVersions(Set<TableFeature> features) {\n    int minReaderVersion =\n        features.stream().mapToInt(TableFeature::minReaderVersion).max().orElse(0);\n\n    int minWriterVersion =\n        features.stream().mapToInt(TableFeature::minWriterVersion).max().orElse(0);\n\n    return new Tuple2<>(Math.max(minReaderVersion, 1), Math.max(minWriterVersion, 1));\n  }\n\n  /**\n   * Upgrade the current protocol to satisfy all auto-update capable features required by the given\n   * metadata. If the current protocol already satisfies the metadata requirements, return empty.\n   *\n   * @param newMetadata the new metadata to be applied to the table.\n   * @param manuallyEnabledFeatures features that were requested to be added to the protocol.\n   * @param currentProtocol the current protocol of the table.\n   * @return the upgraded protocol and the set of new features that were enabled in the upgrade.\n   */\n  public static Optional<Tuple2<Protocol, Set<TableFeature>>> autoUpgradeProtocolBasedOnMetadata(\n      Metadata newMetadata,\n      Collection<TableFeature> manuallyEnabledFeatures,\n      Protocol currentProtocol) {\n\n    Set<TableFeature> allNeededTableFeatures =\n        extractAllNeededTableFeatures(newMetadata, currentProtocol);\n    if (manuallyEnabledFeatures != null && !manuallyEnabledFeatures.isEmpty()) {\n      // Note that any dependent features are handled below in the withFeatures call.\n      allNeededTableFeatures =\n          Stream.concat(allNeededTableFeatures.stream(), manuallyEnabledFeatures.stream())\n              .collect(toSet());\n    }\n\n    Protocol required =\n        new Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n            .withFeatures(allNeededTableFeatures)\n            .normalized();\n\n    // See if all the required features are already supported in the current protocol.\n    if (!required.canUpgradeTo(currentProtocol)) {\n      // `required` has one or more features that are not supported in `currentProtocol`.\n      Set<TableFeature> newFeatures =\n          new HashSet<>(required.getImplicitlyAndExplicitlySupportedFeatures());\n      newFeatures.removeAll(currentProtocol.getImplicitlyAndExplicitlySupportedFeatures());\n      return Optional.of(new Tuple2<>(required.merge(currentProtocol), newFeatures));\n    } else {\n      return Optional.empty();\n    }\n  }\n\n  /**\n   * Checks if a table feature is being manually supported through user property {@code\n   * delta.feature.<featureName>=supported}.\n   */\n  public static boolean isPropertiesManuallySupportingTableFeature(\n      Map<String, String> userProperties, TableFeature tableFeature) {\n    final String featurePropKey = SET_TABLE_FEATURE_SUPPORTED_PREFIX + tableFeature.featureName();\n    final String propertyValue = userProperties.get(featurePropKey); // will be null if not found\n    return SET_TABLE_FEATURE_SUPPORTED_VALUE.equals(propertyValue);\n  }\n\n  /**\n   * Extracts features overrides from Metadata properties and returns an updated metadata if any\n   * overrides are present.\n   *\n   * <p>Overrides are specified using a key in th form {@linkplain\n   * #SET_TABLE_FEATURE_SUPPORTED_PREFIX} + {featureName}. (e.g. {@code\n   * delta.feature.icebergWriterCompatV1}). The value must be \"supported\" to add the feature.\n   * Currently, removing values is not handled.\n   *\n   * @return A set of features that had overrides and Metadata object with the properties removed if\n   *     any overrides were present.\n   * @throws KernelException if the feature name for the override is invalid or the value is not\n   *     equal to \"supported\".\n   */\n  public static Tuple2<Set<TableFeature>, Optional<Metadata>> extractFeaturePropertyOverrides(\n      Metadata currentMetadata) {\n    Set<TableFeature> features = new HashSet<>();\n    Map<String, String> properties = currentMetadata.getConfiguration();\n    for (Map.Entry<String, String> entry : properties.entrySet()) {\n      if (entry.getKey().startsWith(SET_TABLE_FEATURE_SUPPORTED_PREFIX)) {\n        String featureName = entry.getKey().substring(SET_TABLE_FEATURE_SUPPORTED_PREFIX.length());\n\n        TableFeature feature = getTableFeature(featureName);\n        features.add(feature);\n        if (!entry.getValue().equals(SET_TABLE_FEATURE_SUPPORTED_VALUE)) {\n          throw DeltaErrors.invalidConfigurationValueException(\n              entry.getKey(),\n              entry.getValue(),\n              \"TableFeature override options may only have \\\"supported\\\" as there value\");\n        }\n      }\n    }\n\n    if (features.isEmpty()) {\n      return new Tuple2<>(features, Optional.empty());\n    }\n\n    Map<String, String> cleanedProperties =\n        properties.entrySet().stream()\n            .filter(e -> !e.getKey().startsWith(SET_TABLE_FEATURE_SUPPORTED_PREFIX))\n            .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));\n    return new Tuple2<>(\n        features, Optional.of(currentMetadata.withReplacedConfiguration(cleanedProperties)));\n  }\n\n  /** Utility method to check if the table with given protocol is readable by the Kernel. */\n  public static void validateKernelCanReadTheTable(Protocol protocol, String tablePath) {\n    if (protocol.getMinReaderVersion() > TABLE_FEATURES_MIN_READER_VERSION) {\n      throw DeltaErrors.unsupportedReaderProtocol(tablePath, protocol.getMinReaderVersion());\n    }\n\n    Set<TableFeature> unsupportedFeatures =\n        protocol.getImplicitlyAndExplicitlySupportedReaderWriterFeatures().stream()\n            .filter(f -> !f.hasKernelReadSupport())\n            .collect(toSet());\n\n    if (!unsupportedFeatures.isEmpty()) {\n      throw unsupportedReaderFeatures(\n          tablePath, unsupportedFeatures.stream().map(TableFeature::featureName).collect(toSet()));\n    }\n  }\n\n  /**\n   * Utility method to check if the table with given protocol and metadata is writable by the\n   * Kernel.\n   */\n  public static void validateKernelCanWriteToTable(\n      Protocol protocol, Metadata metadata, String tablePath) {\n\n    validateKernelCanReadTheTable(protocol, tablePath);\n\n    if (protocol.getMinWriterVersion() > TABLE_FEATURES_MIN_WRITER_VERSION) {\n      throw unsupportedWriterProtocol(tablePath, protocol.getMinWriterVersion());\n    }\n\n    Set<TableFeature> unsupportedFeatures =\n        protocol.getImplicitlyAndExplicitlySupportedFeatures().stream()\n            .filter(f -> !f.hasKernelWriteSupport(metadata))\n            .collect(toSet());\n\n    if (!unsupportedFeatures.isEmpty()) {\n      throw unsupportedWriterFeatures(\n          tablePath, unsupportedFeatures.stream().map(TableFeature::featureName).collect(toSet()));\n    }\n  }\n\n  /////////////////////////////\n  // Is feature X supported? //\n  /////////////////////////////\n\n  public static boolean isCatalogManagedSupported(Protocol protocol) {\n    return protocol.supportsFeature(CATALOG_MANAGED_RW_FEATURE);\n  }\n\n  public static boolean isRowTrackingSupported(Protocol protocol) {\n    return protocol.supportsFeature(ROW_TRACKING_W_FEATURE);\n  }\n\n  public static boolean isDomainMetadataSupported(Protocol protocol) {\n    return protocol.supportsFeature(DOMAIN_METADATA_W_FEATURE);\n  }\n\n  public static boolean isClusteringTableFeatureSupported(Protocol protocol) {\n    return protocol.supportsFeature(CLUSTERING_W_FEATURE);\n  }\n\n  ///////////////////////////\n  // Does protocol have X? //\n  ///////////////////////////\n\n  public static boolean hasInvariants(StructType tableSchema) {\n    return SchemaIterable.newSchemaIterableWithIgnoredRecursion(\n            tableSchema,\n            // invariants are not allowed in maps or arrays\n            new Class<?>[] {MapType.class, ArrayType.class})\n        .stream()\n        .anyMatch(element -> element.getField().getMetadata().contains(\"delta.invariants\"));\n  }\n\n  public static boolean hasCheckConstraints(Metadata metadata) {\n    return metadata.getConfiguration().keySet().stream()\n        .anyMatch(s -> s.startsWith(\"delta.constraints.\"));\n  }\n\n  public static boolean hasIdentityColumns(Metadata metadata) {\n    return SchemaIterable.newSchemaIterableWithIgnoredRecursion(\n            metadata.getSchema(),\n            // invariants are not allowed in maps or arrays\n            new Class<?>[] {MapType.class, ArrayType.class})\n        .stream()\n        .anyMatch(\n            element -> {\n              StructField field = element.getField();\n              FieldMetadata fieldMetadata = field.getMetadata();\n\n              // Check if the metadata contains the required keys\n              boolean hasStart = fieldMetadata.contains(\"delta.identity.start\");\n              boolean hasStep = fieldMetadata.contains(\"delta.identity.step\");\n              boolean hasInsert = fieldMetadata.contains(\"delta.identity.allowExplicitInsert\");\n\n              // Verify that all or none of the three fields are present\n              if (!((hasStart == hasStep) && (hasStart == hasInsert))) {\n                throw new KernelException(\n                    String.format(\n                        \"Inconsistent IDENTITY metadata for column %s detected: %s, %s, %s\",\n                        field.getName(), hasStart, hasStep, hasInsert));\n              }\n\n              // Return true only if all three fields are present\n              return hasStart && hasStep && hasInsert;\n            });\n  }\n\n  public static boolean hasGeneratedColumns(Metadata metadata) {\n    return SchemaIterable.newSchemaIterableWithIgnoredRecursion(\n            metadata.getSchema(),\n            // don't expected generated columns in\n            // nested columns\n            new Class<?>[] {MapType.class, ArrayType.class})\n        .stream()\n        .anyMatch(\n            element -> element.getField().getMetadata().contains(\"delta.generationExpression\"));\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Private methods                                                           ///\n  /////////////////////////////////////////////////////////////////////////////////\n  /**\n   * Extracts all table features (and their dependency features) that are enabled by the given\n   * metadata and supported in existing protocol.\n   */\n  private static Set<TableFeature> extractAllNeededTableFeatures(\n      Metadata newMetadata, Protocol currentProtocol) {\n    Set<TableFeature> protocolSupportedFeatures =\n        currentProtocol.getImplicitlyAndExplicitlySupportedFeatures();\n\n    Set<TableFeature> metadataEnabledFeatures =\n        TableFeatures.TABLE_FEATURES.stream()\n            .filter(f -> f instanceof FeatureAutoEnabledByMetadata)\n            .filter(\n                f ->\n                    ((FeatureAutoEnabledByMetadata) f)\n                        .metadataRequiresFeatureToBeEnabled(currentProtocol, newMetadata))\n            .collect(toSet());\n\n    // Each feature may have dependencies that are not yet enabled in the protocol.\n    Set<TableFeature> newFeatures = getDependencyFeatures(metadataEnabledFeatures);\n    return Stream.concat(protocolSupportedFeatures.stream(), newFeatures.stream()).collect(toSet());\n  }\n\n  /**\n   * Returns the smallest set of table features that contains `features` and that also contains all\n   * dependencies of all features in the returned set.\n   */\n  private static Set<TableFeature> getDependencyFeatures(Set<TableFeature> features) {\n    Set<TableFeature> requiredFeatures = new HashSet<>(features);\n    features.forEach(feature -> requiredFeatures.addAll(feature.requiredFeatures()));\n\n    if (features.equals(requiredFeatures)) {\n      return features;\n    } else {\n      return getDependencyFeatures(requiredFeatures);\n    }\n  }\n\n  /**\n   * Check if the table schema has a column of type. Caution: works only for the primitive types.\n   */\n  private static boolean hasTypeColumn(StructType tableSchema, DataType type) {\n    return new SchemaIterable(tableSchema)\n        .stream().anyMatch(element -> element.getField().getDataType().equals(type));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/types/DataTypeJsonSerDe.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.types;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport io.delta.kernel.types.*;\nimport java.io.IOException;\nimport java.io.StringWriter;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Serialize and deserialize Delta data types {@link DataType} to JSON and from JSON class based on\n * the <a\n * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#primitive-types\">serialization\n * rules </a> outlined in the Delta Protocol.\n */\npublic class DataTypeJsonSerDe {\n  private static final ObjectMapper OBJECT_MAPPER =\n      new ObjectMapper()\n          .registerModule(\n              new SimpleModule().addSerializer(StructType.class, new StructTypeSerializer()));\n\n  private DataTypeJsonSerDe() {}\n\n  /**\n   * Serializes a {@link StructType} to a JSON string\n   *\n   * @param structType\n   * @return\n   */\n  public static String serializeStructType(StructType structType) {\n    try {\n      return OBJECT_MAPPER.writeValueAsString(structType);\n    } catch (JsonProcessingException ex) {\n      throw new KernelException(\"Could not serialize StructType to JSON\", ex);\n    }\n  }\n\n  /**\n   * Serializes a {@link DataType} to a JSON string according to the Delta Protocol. TODO: Only\n   * reason why this API added was due to Flink-Kernel dependency. Currently Flink-Kernel uses the\n   * Kernel DataType.toJson and Standalone DataType.fromJson to convert between types.\n   *\n   * @param dataType\n   * @return JSON string representing the data type\n   */\n  public static String serializeDataType(DataType dataType) {\n    try {\n      StringWriter stringWriter = new StringWriter();\n      JsonGenerator generator = OBJECT_MAPPER.createGenerator(stringWriter);\n      writeDataType(generator, dataType);\n      generator.flush();\n      return stringWriter.toString();\n    } catch (IOException ex) {\n      throw new KernelException(\"Could not serialize DataType to JSON\", ex);\n    }\n  }\n\n  /**\n   * Deserializes a JSON string representing a Delta data type to a {@link DataType}.\n   *\n   * @param structTypeJson JSON string representing a {@link StructType} data type\n   */\n  public static StructType deserializeStructType(String structTypeJson) {\n    try {\n      DataType parsedType = parseDataType(OBJECT_MAPPER.reader().readTree(structTypeJson));\n      if (parsedType instanceof StructType) {\n        return (StructType) parsedType;\n      } else {\n        throw new IllegalArgumentException(\n            String.format(\n                \"Could not parse the following JSON as a valid StructType:\\n%s\", structTypeJson));\n      }\n    } catch (JsonProcessingException ex) {\n      throw new KernelException(\n          format(\"Could not parse schema given as JSON string: %s\", structTypeJson), ex);\n    }\n  }\n\n  /**\n   * Parses a Delta data type from JSON. Data types can either be serialized as strings (for\n   * primitive types) or as objects (for complex types).\n   *\n   * <p>For example:\n   *\n   * <pre>\n   * // Map type field is serialized as:\n   * {\n   *   \"name\" : \"f\",\n   *   \"type\" : {\n   *     \"type\" : \"map\",\n   *     \"keyType\" : \"string\",\n   *     \"valueType\" : \"string\",\n   *     \"valueContainsNull\" : true\n   *   },\n   *   \"nullable\" : true,\n   *   \"metadata\" : { }\n   * }\n   *\n   * // Integer type field serialized as:\n   * {\n   *   \"name\" : \"a\",\n   *   \"type\" : \"integer\",\n   *   \"nullable\" : false,\n   *   \"metadata\" : { }\n   * }\n   *\n   *\n   * </pre>\n   *\n   * Note: Metadata for individual schema element (e.g. collations are handled when parsing\n   * StructField)\n   */\n  static DataType parseDataType(JsonNode json) {\n    switch (json.getNodeType()) {\n      case STRING:\n        // simple types are stored as just a string\n        return nameToType(json.textValue());\n      case OBJECT:\n        // complex types (array, map, or struct are stored as JSON objects)\n        String type = getStringField(json, \"type\");\n        switch (type) {\n          case \"struct\":\n            return parseStructType(json);\n          case \"array\":\n            return parseArrayType(json);\n          case \"map\":\n            return parseMapType(json);\n            // No default case here; fall through to the following error when no match\n        }\n      default:\n        throw new IllegalArgumentException(\n            String.format(\n                \"Could not parse the following JSON as a valid Delta data type:\\n%s\", json));\n    }\n  }\n\n  /**\n   * Parses an <a href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#array-type\">array\n   * type </a>\n   */\n  private static ArrayType parseArrayType(JsonNode json) {\n    checkArgument(\n        json.isObject() && json.size() == 3,\n        \"Expected JSON object with 3 fields for array data type but got:\\n%s\",\n        json);\n    boolean containsNull = getBooleanField(json, \"containsNull\");\n    DataType dataType = parseDataType(getNonNullField(json, \"elementType\"));\n    return new ArrayType(dataType, containsNull);\n  }\n\n  /**\n   * Parses an <a href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#map-type\">map type\n   * </a>\n   */\n  private static MapType parseMapType(JsonNode json) {\n    checkArgument(\n        json.isObject() && json.size() == 4,\n        \"Expected JSON object with 4 fields for map data type but got:\\n%s\",\n        json);\n    boolean valueContainsNull = getBooleanField(json, \"valueContainsNull\");\n    DataType keyType = parseDataType(getNonNullField(json, \"keyType\"));\n    DataType valueType = parseDataType(getNonNullField(json, \"valueType\"));\n    return new MapType(keyType, valueType, valueContainsNull);\n  }\n\n  /**\n   * Parses an <a href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#struct-type\">\n   * struct type </a>\n   */\n  private static StructType parseStructType(JsonNode json) {\n    checkArgument(\n        json.isObject() && json.size() == 2,\n        \"Expected JSON object with 2 fields for struct data type but got:\\n%s\",\n        json);\n    JsonNode fieldsNode = getNonNullField(json, \"fields\");\n    checkArgument(fieldsNode.isArray(), \"Expected array for fieldName=%s in:\\n%s\", \"fields\", json);\n    Iterator<JsonNode> fields = fieldsNode.elements();\n    List<StructField> parsedFields = new ArrayList<>();\n    while (fields.hasNext()) {\n      parsedFields.add(parseStructField(fields.next()));\n    }\n    return new StructType(parsedFields);\n  }\n\n  /**\n   * Parses an <a href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md#struct-field\">\n   * struct field </a>\n   */\n  private static StructField parseStructField(JsonNode json) {\n    checkArgument(json.isObject(), \"Expected JSON object for struct field\");\n    String name = getStringField(json, \"name\");\n    FieldMetadata metadata = parseFieldMetadata(json.get(\"metadata\"));\n    DataType type = parseDataType(getNonNullField(json, \"type\"));\n    boolean nullable = getBooleanField(json, \"nullable\");\n\n    StructField structField = new StructField(name, type, nullable, metadata);\n    return fixupFieldLevelMetadata(structField, metadata);\n  }\n\n  /**\n   *\n   *\n   * <pre>\n   * // Collated string type field serialized as:\n   * {\n   *   \"name\" : \"s\",\n   *   \"type\" : \"string\",\n   *   \"nullable\", false,\n   *   \"metadata\" : {\n   *     \"__COLLATIONS\": { \"s\": \"ICU.de_DE\" }\n   *   }\n   * }\n   *\n   * // Array with collated strings field serialized as:\n   * {\n   *   \"name\" : \"arr\",\n   *   \"type\" : {\n   *     \"type\" : \"array\",\n   *     \"elementType\" : \"string\",\n   *     \"containsNull\" : false\n   *   }\n   *   \"nullable\" : false,\n   *   \"metadata\" : {\n   *     \"__COLLATIONS\": { \"arr.element\": \"ICU.de_DE\" }\n   *   }\n   * }\n   * </pre>\n   */\n  private static StructField fixupFieldLevelMetadata(\n      StructField structField, FieldMetadata metadata) {\n    if (structField.getMetadata().contains(StructField.COLLATIONS_METADATA_KEY)\n        || structField.getMetadata().contains(StructField.DELTA_TYPE_CHANGES_KEY)) {\n      FieldMetadata collationsMetadata = metadata.getMetadata(StructField.COLLATIONS_METADATA_KEY);\n      if (collationsMetadata == null) {\n        collationsMetadata = FieldMetadata.empty();\n      }\n      Map<String, List<TypeChange>> pathToTypeChanges = parseTypeChangesFromMetadata(metadata);\n      // Don't recurse on Structs because they would have already been constructed/fixed up in this\n      // code path when assembling the child structs.\n      SchemaIterable iterable =\n          SchemaIterable.newSchemaIterableWithIgnoredRecursion(\n              new StructType().add(structField), new Class<?>[] {StructType.class});\n      Iterator<SchemaIterable.MutableSchemaElement> elements = iterable.newMutableIterator();\n      String fieldName = structField.getName();\n\n      while (elements.hasNext()) {\n        SchemaIterable.MutableSchemaElement element = elements.next();\n        String pathFromAncestor = element.getPathFromNearestStructFieldAncestor(\"\");\n        String pathWithFieldName =\n            pathFromAncestor.isEmpty()\n                ? fieldName\n                : String.format(\"%s.%s\", fieldName, pathFromAncestor);\n        if (collationsMetadata.contains(pathWithFieldName)) {\n          updateCollation(element, collationsMetadata, pathWithFieldName);\n        }\n        List<TypeChange> changes = pathToTypeChanges.get(pathFromAncestor);\n        if (changes != null) {\n          if (element.getField().getDataType().isNested()) {\n            throw new KernelException(\n                format(\"Invalid data type for type change: \\\"%s\\\"\", element.getField()));\n          }\n          element.updateField(element.getField().withTypeChanges(changes));\n        }\n      }\n\n      structField =\n          iterable\n              .getSchema()\n              .at(0)\n              .withNewMetadata(\n                  FieldMetadata.builder()\n                      .fromMetadata(metadata)\n                      .remove(StructField.COLLATIONS_METADATA_KEY)\n                      .remove(StructField.DELTA_TYPE_CHANGES_KEY)\n                      .build());\n    }\n    return structField;\n  }\n\n  private static void updateCollation(\n      SchemaIterable.MutableSchemaElement element,\n      FieldMetadata collationsMetadata,\n      String pathFromAncestor) {\n    StructField field = element.getField();\n    DataType fieldType = field.getDataType();\n    if (!(fieldType instanceof StringType)) {\n      throw new IllegalArgumentException(\n          format(\"Invalid data type for collations: \\\"%s\\\"\", fieldType));\n    }\n    StringType collatedType = new StringType(collationsMetadata.getString(pathFromAncestor));\n    element.updateField(field.withDataType(collatedType));\n  }\n\n  /**\n   * Processes a FieldMetadata and a map of paths to type changes to create TypeChange objects. This\n   * function parses the type changes from the metadata and organizes them by path.\n   */\n  private static Map<String, List<TypeChange>> parseTypeChangesFromMetadata(\n      FieldMetadata metadata) {\n    Map<String, List<TypeChange>> pathToTypeChanges = new HashMap<>();\n    if (metadata == null || !metadata.contains(StructField.DELTA_TYPE_CHANGES_KEY)) {\n      return pathToTypeChanges;\n    }\n    FieldMetadata[] typeChangesArray =\n        metadata.getMetadataArray(StructField.DELTA_TYPE_CHANGES_KEY);\n    if (typeChangesArray == null) {\n      return pathToTypeChanges;\n    }\n    for (FieldMetadata changeMetadata : typeChangesArray) {\n      String fromTypeStr = changeMetadata.getString(StructField.FROM_TYPE_KEY);\n      String toTypeStr = changeMetadata.getString(StructField.TO_TYPE_KEY);\n      DataType fromType = nameToType(fromTypeStr);\n      DataType toType = nameToType(toTypeStr);\n      TypeChange typeChange = new TypeChange(fromType, toType);\n      // Empty string is default because it means type change is directly on struct field.\n      String path = \"\";\n      if (changeMetadata.contains(StructField.FIELD_PATH_KEY)) {\n        path = changeMetadata.getString(StructField.FIELD_PATH_KEY);\n      }\n      pathToTypeChanges.computeIfAbsent(path, k -> new ArrayList<>()).add(typeChange);\n    }\n    return pathToTypeChanges;\n  }\n\n  /** Parses a {@link FieldMetadata}. */\n  private static FieldMetadata parseFieldMetadata(JsonNode json) {\n    if (json == null || json.isNull()) {\n      return FieldMetadata.empty();\n    }\n\n    checkArgument(json.isObject(), \"Expected JSON object for struct field metadata\");\n    final Iterator<Map.Entry<String, JsonNode>> iterator = json.fields();\n    final FieldMetadata.Builder builder = FieldMetadata.builder();\n    while (iterator.hasNext()) {\n      Map.Entry<String, JsonNode> entry = iterator.next();\n      JsonNode value = entry.getValue();\n      String key = entry.getKey();\n\n      if (key.equals(StructField.METADATA_SPEC_KEY)) {\n        builder.putMetadataColumnSpec(key, MetadataColumnSpec.fromString(value.textValue()));\n      } else if (value.isNull()) {\n        builder.putNull(key);\n      } else if (value.isIntegralNumber()) { // covers both int and long\n        builder.putLong(key, value.longValue());\n      } else if (value.isDouble()) {\n        builder.putDouble(key, value.doubleValue());\n      } else if (value.isBoolean()) {\n        builder.putBoolean(key, value.booleanValue());\n      } else if (value.isTextual()) {\n        builder.putString(key, value.textValue());\n      } else if (value.isObject()) {\n        builder.putFieldMetadata(key, parseFieldMetadata(value));\n      } else if (value.isArray()) {\n        final Iterator<JsonNode> fields = value.elements();\n        if (!fields.hasNext()) {\n          // If it is an empty array, we cannot infer its element type.\n          // We put an empty Array[Long].\n          builder.putLongArray(key, new Long[0]);\n        } else {\n          final JsonNode head = fields.next();\n          if (head.isInt()) {\n            builder.putLongArray(\n                key, buildList(value, node -> (long) node.intValue()).toArray(new Long[0]));\n          } else if (head.isDouble()) {\n            builder.putDoubleArray(\n                key, buildList(value, JsonNode::doubleValue).toArray(new Double[0]));\n          } else if (head.isBoolean()) {\n            builder.putBooleanArray(\n                key, buildList(value, JsonNode::booleanValue).toArray(new Boolean[0]));\n          } else if (head.isTextual()) {\n            builder.putStringArray(\n                key, buildList(value, JsonNode::textValue).toArray(new String[0]));\n          } else if (head.isObject()) {\n            builder.putFieldMetadataArray(\n                key,\n                buildList(value, DataTypeJsonSerDe::parseFieldMetadata)\n                    .toArray(new FieldMetadata[0]));\n          } else {\n            throw new IllegalArgumentException(\n                String.format(\"Unsupported type for Array as field metadata value: %s\", value));\n          }\n        }\n      } else {\n        throw new IllegalArgumentException(\n            String.format(\"Unsupported type for field metadata value: %s\", value));\n      }\n    }\n    return builder.build();\n  }\n\n  /**\n   * For an array JSON node builds a {@link List} using the provided {@code accessor} for each\n   * element.\n   */\n  private static <T> List<T> buildList(JsonNode json, Function<JsonNode, T> accessor) {\n    List<T> result = new ArrayList<>();\n    Iterator<JsonNode> elements = json.elements();\n    while (elements.hasNext()) {\n      result.add(accessor.apply(elements.next()));\n    }\n    return result;\n  }\n\n  private static String FIXED_DECIMAL_REGEX = \"decimal\\\\(\\\\s*(\\\\d+)\\\\s*,\\\\s*(\\\\-?\\\\d+)\\\\s*\\\\)\";\n  private static Pattern FIXED_DECIMAL_PATTERN = Pattern.compile(FIXED_DECIMAL_REGEX);\n\n  // Geometry patterns\n  private static final String GEOMETRY_REGEX = \"geometry\\\\(\\\\s*([\\\\w]+:-?[\\\\w]+)\\\\s*\\\\)\";\n  private static final Pattern GEOMETRY_PATTERN = Pattern.compile(GEOMETRY_REGEX);\n\n  // Geography patterns\n  private static final String GEOGRAPHY_CRS_ALG_REGEX =\n      \"geography\\\\(\\\\s*(\\\\w+:-?\\\\w+)\\\\s*,\\\\s*(\\\\w+)\\\\s*\\\\)\";\n  private static final Pattern GEOGRAPHY_CRS_ALG_PATTERN = Pattern.compile(GEOGRAPHY_CRS_ALG_REGEX);\n  private static final String GEOGRAPHY_CRS_REGEX = \"geography\\\\(\\\\s*(\\\\w+:-?\\\\w+)\\\\s*\\\\)\";\n  private static final Pattern GEOGRAPHY_CRS_PATTERN = Pattern.compile(GEOGRAPHY_CRS_REGEX);\n  private static final String GEOGRAPHY_ALG_REGEX = \"geography\\\\(\\\\s*(\\\\w+)\\\\s*\\\\)\";\n  private static final Pattern GEOGRAPHY_ALG_PATTERN = Pattern.compile(GEOGRAPHY_ALG_REGEX);\n\n  /** Parses primitive string type names to a {@link DataType} */\n  private static DataType nameToType(String name) {\n    if (BasePrimitiveType.isPrimitiveType(name)) {\n      return BasePrimitiveType.createPrimitive(name);\n    } else if (name.equals(\"decimal\")) {\n      return DecimalType.USER_DEFAULT;\n    } else if (name.equals(\"geometry\")) {\n      return GeometryType.ofDefault();\n    } else if (name.equals(\"geography\")) {\n      return GeographyType.ofDefault();\n    } else if (\"void\".equalsIgnoreCase(name)) {\n      // Earlier versions of Delta had VOID type which is not specified in Delta Protocol.\n      // It is not readable or writable. Throw a user-friendly error message.\n      throw DeltaErrors.voidTypeEncountered();\n    } else {\n      // decimal has a special pattern with a precision and scale\n      Matcher decimalMatcher = FIXED_DECIMAL_PATTERN.matcher(name);\n      if (decimalMatcher.matches()) {\n        int precision = Integer.parseInt(decimalMatcher.group(1));\n        int scale = Integer.parseInt(decimalMatcher.group(2));\n        return new DecimalType(precision, scale);\n      }\n\n      // geometry has a special pattern with an SRID\n      Matcher geometryMatcher = GEOMETRY_PATTERN.matcher(name);\n      if (geometryMatcher.matches()) {\n        String srid = geometryMatcher.group(1);\n        return GeometryType.ofSRID(srid);\n      }\n\n      // geography has different patterns:\n      // 1. geography(<srid>, <algorithm>) - both specified\n      // 2. geography(<srid>) - SRID specified (contains colon)\n      // 3. geography(<algorithm>) - algorithm specified (no colon)\n\n      // First check for both CRS and algorithm (contains comma)\n      Matcher geographyCrsAlgMatcher = GEOGRAPHY_CRS_ALG_PATTERN.matcher(name);\n      if (geographyCrsAlgMatcher.matches()) {\n        String srid = geographyCrsAlgMatcher.group(1);\n        String algorithm = geographyCrsAlgMatcher.group(2);\n        return new GeographyType(srid, algorithm);\n      }\n\n      // Check for CRS pattern (contains colon)\n      Matcher geographyCrsMatcher = GEOGRAPHY_CRS_PATTERN.matcher(name);\n      if (geographyCrsMatcher.matches()) {\n        String srid = geographyCrsMatcher.group(1);\n        return GeographyType.ofSRID(srid);\n      }\n\n      // Check for algorithm pattern (no colon)\n      Matcher geographyAlgMatcher = GEOGRAPHY_ALG_PATTERN.matcher(name);\n      if (geographyAlgMatcher.matches()) {\n        String algorithm = geographyAlgMatcher.group(1);\n        return GeographyType.ofAlgorithm(algorithm);\n      }\n\n      // We have encountered a type that is beyond the specification of the protocol\n      // checks. This must be an invalid type (according to protocol) and\n      // not an unsupported data type by Kernel.\n      throw new IllegalArgumentException(\n          String.format(\"%s is not a supported delta data type\", name));\n    }\n  }\n\n  private static JsonNode getNonNullField(JsonNode rootNode, String fieldName) {\n    JsonNode node = rootNode.get(fieldName);\n    if (node == null || node.isNull()) {\n      throw new IllegalArgumentException(\n          String.format(\"Expected non-null for fieldName=%s in:\\n%s\", fieldName, rootNode));\n    }\n    return node;\n  }\n\n  private static String getStringField(JsonNode rootNode, String fieldName) {\n    JsonNode node = getNonNullField(rootNode, fieldName);\n    checkArgument(\n        node.isTextual(), \"Expected string for fieldName=%s in:\\n%s\", fieldName, rootNode);\n    return node.textValue(); // double check this only works for string values! and isTextual()!\n  }\n\n  private static boolean getBooleanField(JsonNode rootNode, String fieldName) {\n    JsonNode node = getNonNullField(rootNode, fieldName);\n    checkArgument(\n        node.isBoolean(), \"Expected boolean for fieldName=%s in:\\n%s\", fieldName, rootNode);\n    return node.booleanValue();\n  }\n\n  protected static class StructTypeSerializer extends StdSerializer<StructType> {\n    public StructTypeSerializer() {\n      super(StructType.class);\n    }\n\n    @Override\n    public void serialize(StructType structType, JsonGenerator gen, SerializerProvider provider)\n        throws IOException {\n      writeDataType(gen, structType);\n    }\n  }\n\n  private static void writeDataType(JsonGenerator gen, DataType dataType) throws IOException {\n    if (dataType instanceof StructType) {\n      writeStructType(gen, (StructType) dataType);\n    } else if (dataType instanceof ArrayType) {\n      writeArrayType(gen, (ArrayType) dataType);\n    } else if (dataType instanceof MapType) {\n      writeMapType(gen, (MapType) dataType);\n    } else if (dataType instanceof DecimalType) {\n      DecimalType decimalType = (DecimalType) dataType;\n      gen.writeString(format(\"decimal(%d,%d)\", decimalType.getPrecision(), decimalType.getScale()));\n    } else if (dataType instanceof StringType) {\n      // Always serialize `StringType` as \"string\" without including collation info, since collation\n      // is stored in the field metadata.\n      gen.writeString(\"string\");\n    } else if (dataType instanceof GeometryType) {\n      GeometryType geometryType = (GeometryType) dataType;\n      gen.writeString(geometryType.simpleString());\n    } else if (dataType instanceof GeographyType) {\n      GeographyType geographyType = (GeographyType) dataType;\n      gen.writeString(geographyType.simpleString());\n    } else {\n      gen.writeString(dataType.toString());\n    }\n  }\n\n  private static void writeArrayType(JsonGenerator gen, ArrayType arrayType) throws IOException {\n    gen.writeStartObject();\n    gen.writeStringField(\"type\", \"array\");\n    gen.writeFieldName(\"elementType\");\n    writeDataType(gen, arrayType.getElementType());\n    gen.writeBooleanField(\"containsNull\", arrayType.containsNull());\n    gen.writeEndObject();\n  }\n\n  private static void writeMapType(JsonGenerator gen, MapType mapType) throws IOException {\n    gen.writeStartObject();\n    gen.writeStringField(\"type\", \"map\");\n    gen.writeFieldName(\"keyType\");\n    writeDataType(gen, mapType.getKeyType());\n    gen.writeFieldName(\"valueType\");\n    writeDataType(gen, mapType.getValueType());\n    gen.writeBooleanField(\"valueContainsNull\", mapType.isValueContainsNull());\n    gen.writeEndObject();\n  }\n\n  private static void writeStructType(JsonGenerator gen, StructType structType) throws IOException {\n    gen.writeStartObject();\n    gen.writeStringField(\"type\", \"struct\");\n    gen.writeArrayFieldStart(\"fields\");\n    for (StructField field : structType.fields()) {\n      writeStructField(gen, field);\n    }\n    gen.writeEndArray();\n    gen.writeEndObject();\n  }\n\n  private static void writeStructField(JsonGenerator gen, StructField field) throws IOException {\n    gen.writeStartObject();\n    gen.writeStringField(\"name\", field.getName());\n    gen.writeFieldName(\"type\");\n    writeDataType(gen, field.getDataType());\n    gen.writeBooleanField(\"nullable\", field.isNullable());\n    gen.writeFieldName(\"metadata\");\n    writeFieldMetadata(gen, field.getMetadata());\n    gen.writeEndObject();\n  }\n\n  private static void writeFieldMetadata(JsonGenerator gen, FieldMetadata metadata)\n      throws IOException {\n    gen.writeStartObject();\n    for (Map.Entry<String, Object> entry : metadata.getEntries().entrySet()) {\n      gen.writeFieldName(entry.getKey());\n      Object value = entry.getValue();\n      if (value instanceof Long) {\n        gen.writeNumber((Long) value);\n      } else if (value instanceof Double) {\n        gen.writeNumber((Double) value);\n      } else if (value instanceof Boolean) {\n        gen.writeBoolean((Boolean) value);\n      } else if (value instanceof String) {\n        gen.writeString((String) value);\n      } else if (value instanceof MetadataColumnSpec) {\n        gen.writeString(value.toString());\n      } else if (value instanceof FieldMetadata) {\n        writeFieldMetadata(gen, (FieldMetadata) value);\n      } else if (value instanceof Long[]) {\n        gen.writeStartArray();\n        for (Long v : (Long[]) value) {\n          gen.writeNumber(v);\n        }\n        gen.writeEndArray();\n      } else if (value instanceof Double[]) {\n        gen.writeStartArray();\n        for (Double v : (Double[]) value) {\n          gen.writeNumber(v);\n        }\n        gen.writeEndArray();\n      } else if (value instanceof Boolean[]) {\n        gen.writeStartArray();\n        for (Boolean v : (Boolean[]) value) {\n          gen.writeBoolean(v);\n        }\n        gen.writeEndArray();\n      } else if (value instanceof String[]) {\n        gen.writeStartArray();\n        for (String v : (String[]) value) {\n          gen.writeString(v);\n        }\n        gen.writeEndArray();\n      } else if (value instanceof FieldMetadata[]) {\n        gen.writeStartArray();\n        for (FieldMetadata v : (FieldMetadata[]) value) {\n          writeFieldMetadata(gen, v);\n        }\n        gen.writeEndArray();\n      } else if (value == null) {\n        gen.writeNull();\n      } else {\n        throw new IllegalArgumentException(\n            format(\"Unsupported type for field metadata value: %s\", value));\n      }\n    }\n    gen.writeEndObject();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/types/TypeWideningChecker.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.types;\n\nimport io.delta.kernel.types.*;\n\n/**\n * Utility class for checking type widening compatibility according to the Delta protocol.\n *\n * <p>The Type Widening feature enables changing the type of a column or field in an existing Delta\n * table to a wider type. This class implements the checks required by the protocol to ensure that\n * only supported type changes are allowed.\n *\n * <p>Supported type changes as per protocol:\n *\n * <ul>\n *   <li>Integer widening: {@code Byte -> Short -> Int -> Long}\n *   <li>Floating-point widening: {@code Float -> Double}\n *   <li>Floating-point widening: {@code Byte, Short or Int -> Double}\n *   <li>Date widening: {@code Date -> Timestamp without timezone}\n *   <li>Decimal widening: {@code Decimal(p, s) -> Decimal(p + k1, s + k2)} where {@code k1 >= k2 >=\n *       0}\n *   <li>Decimal widening: {@code Byte, Short or Int -> Decimal(10 + k1, k2)} where {@code k1 >= k2\n *       >= 0}\n *   <li>Decimal widening: {@code Long -> Decimal(20 + k1, k2)} where {@code k1 >= k2 >= 0}\n * </ul>\n */\npublic class TypeWideningChecker {\n  private TypeWideningChecker() {}\n\n  /**\n   * Checks if a type change from sourceType to targetType is a supported widening operation.\n   *\n   * @param sourceType The original data type\n   * @param targetType The target data type to widen to\n   * @return true if the type change is a supported widening operation (or the types are equal),\n   *     false otherwise\n   */\n  public static boolean isWideningSupported(DataType sourceType, DataType targetType) {\n    // Iceberg V2 type widening is a strict subset of Delta type widening\n    if (isIcebergV2Compatible(sourceType, targetType)) {\n      return true;\n    }\n\n    // Floating-point widening: Byte, Short or Int -> Double\n    if (isIntegerToDoubleWidening(sourceType, targetType)) {\n      return true;\n    }\n\n    // Date widening: Date -> Timestamp without timezone\n    if (isDateToTimestampNtzWidening(sourceType, targetType)) {\n      return true;\n    }\n\n    // Decimal widening\n    if (isDecimalWidening(sourceType, targetType)) {\n      return true;\n    }\n\n    // No supported widening found\n    return false;\n  }\n\n  /**\n   * Checks if the type change is supported by Iceberg V2 schema evolution. This is required when\n   * Iceberg compatibility is enabled.\n   *\n   * @param sourceType The original data type\n   * @param targetType The target data type to widen to\n   * @return true if the type change is supported by Iceberg V2 (sourceType == targetType returns\n   *     true), false otherwise\n   */\n  public static boolean isIcebergV2Compatible(DataType sourceType, DataType targetType) {\n    // If types are the same, it's not a widening operation\n    if (sourceType.equals(targetType)) {\n      return true;\n    }\n\n    // Integer widening: Byte -> Short -> Int -> Long\n    if (isIntegerWidening(sourceType, targetType)) {\n      return true;\n    }\n\n    // Floating-point widening: Float -> Double\n    if (isFloatToDoubleWidening(sourceType, targetType)) {\n      return true;\n    }\n\n    // Check if it's a decimal widening with scale increase\n    if (sourceType instanceof DecimalType && targetType instanceof DecimalType) {\n      DecimalType sourceDecimal = (DecimalType) sourceType;\n      DecimalType targetDecimal = (DecimalType) targetType;\n\n      // If scale changes, are not supported by Iceberg\n      if (targetDecimal.getScale() != sourceDecimal.getScale()) {\n        return false;\n      }\n\n      // Precision increase with same scale is supported.\n      return targetDecimal.getPrecision() >= sourceDecimal.getPrecision();\n    }\n\n    // No other supported widening for Iceberg\n    return false;\n  }\n\n  /** Checks if the type change is an integer widening (Byte -> Short -> Int -> Long). */\n  private static boolean isIntegerWidening(DataType sourceType, DataType targetType) {\n    if (sourceType instanceof ByteType) {\n      return targetType instanceof ShortType\n          || targetType instanceof IntegerType\n          || targetType instanceof LongType;\n    } else if (sourceType instanceof ShortType) {\n      return targetType instanceof IntegerType || targetType instanceof LongType;\n    } else if (sourceType instanceof IntegerType) {\n      return targetType instanceof LongType;\n    }\n    return false;\n  }\n\n  /** Checks if the type change is a Float to Double widening. */\n  private static boolean isFloatToDoubleWidening(DataType sourceType, DataType targetType) {\n    return sourceType instanceof FloatType && targetType instanceof DoubleType;\n  }\n\n  /** Checks if the type change is an integer to Double widening. */\n  private static boolean isIntegerToDoubleWidening(DataType sourceType, DataType targetType) {\n    return (sourceType instanceof ByteType\n            || sourceType instanceof ShortType\n            || sourceType instanceof IntegerType)\n        && targetType instanceof DoubleType;\n  }\n\n  /** Checks if the type change is a Date to TimestampNTZ widening. */\n  private static boolean isDateToTimestampNtzWidening(DataType sourceType, DataType targetType) {\n    return sourceType instanceof DateType && targetType instanceof TimestampNTZType;\n  }\n\n  /** Checks if the type change is a supported decimal widening. */\n  private static boolean isDecimalWidening(DataType sourceType, DataType targetType) {\n    // Decimal(p, s) -> Decimal(p + k1, s + k2) where k1 >= k2 >= 0\n    if (sourceType instanceof DecimalType && targetType instanceof DecimalType) {\n      DecimalType sourceDecimal = (DecimalType) sourceType;\n      DecimalType targetDecimal = (DecimalType) targetType;\n\n      int precisionDiff = targetDecimal.getPrecision() - sourceDecimal.getPrecision();\n      int scaleDiff = targetDecimal.getScale() - sourceDecimal.getScale();\n\n      return precisionDiff >= scaleDiff && scaleDiff >= 0;\n    }\n\n    // Byte, Short or Int -> Decimal(10 + k1, k2) where k1 >= k2 >= 0\n    if ((sourceType instanceof ByteType\n            || sourceType instanceof ShortType\n            || sourceType instanceof IntegerType)\n        && targetType instanceof DecimalType) {\n\n      DecimalType targetDecimal = (DecimalType) targetType;\n      int basePrecision = 10;\n\n      return targetDecimal.getPrecision() >= basePrecision\n          && (targetDecimal.getPrecision() - basePrecision) >= targetDecimal.getScale()\n          && targetDecimal.getScale() >= 0;\n    }\n\n    // Long -> Decimal(20 + k1, k2) where k1 >= k2 >= 0\n    if (sourceType instanceof LongType && targetType instanceof DecimalType) {\n      DecimalType targetDecimal = (DecimalType) targetType;\n      int basePrecision = 20;\n\n      return targetDecimal.getPrecision() >= basePrecision\n          && (targetDecimal.getPrecision() - basePrecision) >= targetDecimal.getScale()\n          && targetDecimal.getScale() >= 0;\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/CaseInsensitiveMap.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * A map that is case-insensitive in its keys. This map is not thread-safe, and key is\n * case-insensitive and of string type.\n *\n * @param <V>\n */\npublic class CaseInsensitiveMap<V> implements Map<String, V> {\n  private final Map<String, V> innerMap = new HashMap<>();\n\n  @Override\n  public V get(Object key) {\n    return innerMap.get(toLowerCase(key));\n  }\n\n  @Override\n  public V put(String key, V value) {\n    return innerMap.put(toLowerCase(key), value);\n  }\n\n  @Override\n  public void putAll(Map<? extends String, ? extends V> m) {\n    // behavior of this method is not defined on how to handle duplicates\n    // don't support this use case, as it is not needed in Kernel\n    throw new UnsupportedOperationException(\"putAll\");\n  }\n\n  @Override\n  public V remove(Object key) {\n    return innerMap.remove(toLowerCase(key));\n  }\n\n  @Override\n  public boolean containsKey(Object key) {\n    return innerMap.containsKey(toLowerCase(key));\n  }\n\n  @Override\n  public boolean containsValue(Object value) {\n    return innerMap.containsValue(value);\n  }\n\n  @Override\n  public Set<String> keySet() {\n    // no need to convert to lower case here as the inserted keys are already in lower case\n    return innerMap.keySet();\n  }\n\n  @Override\n  public Set<Entry<String, V>> entrySet() {\n    // no need to convert to lower case here as the inserted keys are already in lower case\n    return innerMap.entrySet();\n  }\n\n  @Override\n  public Collection<V> values() {\n    return innerMap.values();\n  }\n\n  @Override\n  public int size() {\n    return innerMap.size();\n  }\n\n  @Override\n  public boolean isEmpty() {\n    return innerMap.isEmpty();\n  }\n\n  @Override\n  public void clear() {\n    innerMap.clear();\n  }\n\n  private String toLowerCase(Object key) {\n    if (key == null) {\n      return null;\n    }\n    checkArgument(key instanceof String, \"Key must be a string\");\n    return ((String) key).toLowerCase(Locale.ROOT);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/Clock.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\n/** An interface to represent clocks, so that they can be mocked out in unit tests. */\npublic interface Clock {\n  /** @return Current system time, in ms. */\n  long getTimeMillis();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/ColumnMapping.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.DeltaErrors.columnNotFoundInSchema;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Collections.singletonMap;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.exceptions.InvalidConfigurationValueException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.data.TransactionStateRow;\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatMetadataValidatorAndUpdater;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\n/** Utilities related to the column mapping feature. */\npublic class ColumnMapping {\n  private ColumnMapping() {}\n\n  public enum ColumnMappingMode {\n    NONE(\"none\"),\n    ID(\"id\"),\n    NAME(\"name\");\n\n    public final String value;\n\n    ColumnMappingMode(String value) {\n      this.value = value;\n    }\n\n    public static ColumnMappingMode fromTableConfig(String modeString) {\n      for (ColumnMappingMode mode : ColumnMappingMode.values()) {\n        if (mode.value.equalsIgnoreCase(modeString)) {\n          return mode;\n        }\n      }\n      throw new InvalidConfigurationValueException(\n          COLUMN_MAPPING_MODE_KEY,\n          modeString,\n          String.format(\"Needs to be one of: %s.\", Arrays.toString(ColumnMappingMode.values())));\n    }\n\n    @Override\n    public String toString() {\n      return this.value;\n    }\n  }\n\n  private enum SchemaConversionDirection {\n    LOGICAL_TO_PHYSICAL,\n    PHYSICAL_TO_LOGICAL\n  }\n\n  public static final String COLUMN_MAPPING_MODE_KEY = \"delta.columnMapping.mode\";\n  public static final String COLUMN_MAPPING_PHYSICAL_NAME_KEY = \"delta.columnMapping.physicalName\";\n  public static final String COLUMN_MAPPING_ID_KEY = \"delta.columnMapping.id\";\n  public static final String COLUMN_MAPPING_NESTED_IDS_KEY = \"delta.columnMapping.nested.ids\";\n\n  public static final String PARQUET_FIELD_ID_KEY = \"parquet.field.id\";\n  public static final String PARQUET_FIELD_NESTED_IDS_METADATA_KEY = \"parquet.field.nested.ids\";\n  public static final String COLUMN_MAPPING_MAX_COLUMN_ID_KEY = \"delta.columnMapping.maxColumnId\";\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  /**\n   * Returns the column mapping mode from the given configuration.\n   *\n   * @param configuration Configuration\n   * @return Column mapping mode. One of (\"none\", \"name\", \"id\")\n   */\n  public static ColumnMappingMode getColumnMappingMode(Map<String, String> configuration) {\n    return Optional.ofNullable(configuration.get(COLUMN_MAPPING_MODE_KEY))\n        .map(ColumnMappingMode::fromTableConfig)\n        .orElse(ColumnMappingMode.NONE);\n  }\n\n  /**\n   * Helper method that converts the logical schema (requested by the connector) to physical schema\n   * of the data stored in data files based on the table's column mapping mode. Field-id column\n   * metadata is preserved when cmMode = ID, all column metadata is otherwise removed.\n   *\n   * <p>We require {@code fullSchema} in addition to the pruned schema we want to convert since we\n   * need the complete field metadata as it is stored in the schema in the _delta_log. We cannot be\n   * sure (and do not enforce) that this metadata is preserved by the connector.\n   *\n   * @param prunedSchema the logical read schema requested by the connector\n   * @param fullSchema the full delta schema (with complete metadata) as read from the _delta_log\n   * @param columnMappingMode Column mapping mode\n   */\n  public static StructType convertToPhysicalSchema(\n      StructType prunedSchema, StructType fullSchema, ColumnMappingMode columnMappingMode) {\n    switch (columnMappingMode) {\n      case NONE:\n        return prunedSchema;\n      case ID: // fall through\n      case NAME:\n        boolean includeFieldIds = columnMappingMode == ColumnMappingMode.ID;\n        return convertToPhysicalSchema(prunedSchema, fullSchema, includeFieldIds);\n      default:\n        throw new UnsupportedOperationException(\n            \"Unsupported column mapping mode: \" + columnMappingMode);\n    }\n  }\n\n  /**\n   * Converts a logical column to a physical column based on the table's column mapping mode. The\n   * field-id metadata is preserved when cmMode = ID, all column metadata is otherwise removed.\n   *\n   * <p>We require {@code fullSchema} in addition to the logical field we want to convert since we\n   * need the complete field metadata as it is stored in the schema in the _delta_log. We cannot be\n   * sure (and do not enforce) that this metadata is preserved by the connector.\n   *\n   * @param logicalField the logical read column requested by the connector\n   * @param fullSchema the full delta schema (with complete metadata) as read from the _delta_log\n   * @param columnMappingMode Column mapping mode\n   */\n  public static StructField convertToPhysicalColumn(\n      StructField logicalField, StructType fullSchema, ColumnMappingMode columnMappingMode) {\n    switch (columnMappingMode) {\n      case NONE:\n        return logicalField;\n      case ID: // fall through\n      case NAME:\n        boolean includeFieldIds = columnMappingMode == ColumnMappingMode.ID;\n        return convertToPhysicalColumn(logicalField, fullSchema, includeFieldIds);\n      default:\n        throw new UnsupportedOperationException(\n            \"Unsupported column mapping mode: \" + columnMappingMode);\n    }\n  }\n\n  /** Returns the physical name for a given {@link StructField} */\n  public static String getPhysicalName(StructField field) {\n    if (hasPhysicalName(field)) {\n      return field.getMetadata().getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY);\n    } else {\n      return field.getName();\n    }\n  }\n\n  /** Returns the column id for a given {@link StructField} */\n  public static int getColumnId(StructField field) {\n    checkArgument(\n        field.getMetadata().contains(COLUMN_MAPPING_ID_KEY),\n        \"Field does not have column id set in it's metadata\");\n    return field.getMetadata().getLong(COLUMN_MAPPING_ID_KEY).intValue();\n  }\n\n  public static void verifyColumnMappingChange(\n      Map<String, String> oldConfig, Map<String, String> newConfig) {\n    ColumnMappingMode oldMappingMode = getColumnMappingMode(oldConfig);\n    ColumnMappingMode newMappingMode = getColumnMappingMode(newConfig);\n\n    checkArgument(\n        validModeChange(oldMappingMode, newMappingMode),\n        \"Changing column mapping mode from '%s' to '%s' is not supported\",\n        oldMappingMode,\n        newMappingMode);\n  }\n\n  public static boolean isColumnMappingModeEnabled(ColumnMappingMode columnMappingMode) {\n    return columnMappingMode == ColumnMappingMode.ID || columnMappingMode == ColumnMappingMode.NAME;\n  }\n\n  /**\n   * Updates the column mapping metadata if needed based on the column mapping mode and whether the\n   * icebergCompatV2 is enabled. If column mapping/iceberg compat info is already present in the\n   * metadata, this method does nothing and returns an empty Optional. Callers can avoid updating\n   * the metadata if the metadata has not changed.\n   *\n   * @param metadata Current metadata.\n   * @param isNewTable Whether this is part of a commit that sets the mapping mode on a new table.\n   * @return Optional of the updated metadata if it has changed, Optional.empty() otherwise.\n   */\n  public static Optional<Metadata> updateColumnMappingMetadataIfNeeded(\n      Metadata metadata, boolean isNewTable) {\n    ColumnMappingMode columnMappingMode = getColumnMappingMode(metadata.getConfiguration());\n    switch (columnMappingMode) {\n      case NONE:\n        return Optional.empty();\n      case ID: // fall through\n      case NAME:\n        return assignColumnIdAndPhysicalName(metadata, isNewTable);\n      default:\n        throw new UnsupportedOperationException(\n            \"Unsupported column mapping mode: \" + columnMappingMode);\n    }\n  }\n\n  /** Returns the physical column and data type for a given logical column based on the schema. */\n  public static Tuple2<Column, DataType> getPhysicalColumnNameAndDataType(\n      StructType schema, Column logicalColumn) {\n    return convertColumnName(schema, logicalColumn, SchemaConversionDirection.LOGICAL_TO_PHYSICAL);\n  }\n\n  /** Returns the logical column and data type for a given physical column based on the schema. */\n  public static Tuple2<Column, DataType> getLogicalColumnNameAndDataType(\n      StructType schema, Column physicalColumn) {\n    return convertColumnName(schema, physicalColumn, SchemaConversionDirection.PHYSICAL_TO_LOGICAL);\n  }\n\n  /**\n   * Utility method to block writing into a table with column mapping enabled. Currently Kernel only\n   * supports the metadata updates on tables with column mapping enabled. Data writes into such\n   * tables using the data transformation APIs provided by the Kernel are not supported yet.\n   */\n  public static void blockIfColumnMappingEnabled(Row transactionState) {\n    ColumnMapping.ColumnMappingMode columnMappingMode =\n        TransactionStateRow.getColumnMappingMode(transactionState);\n    if (columnMappingMode != ColumnMapping.ColumnMappingMode.NONE) {\n      throw new UnsupportedOperationException(\n          \"Writing into column mapping enabled table is not supported yet.\");\n    }\n  }\n\n  ////////////////////////////\n  // Private Helper Methods //\n  ////////////////////////////\n\n  /**\n   * Common helper method for column name conversion between logical and physical representations.\n   *\n   * @param schema The schema to traverse\n   * @param inputColumn The column to convert\n   * @param conversionDirection The direction of schema conversion, either from logical to physical\n   *     or physical to logical\n   * @return Tuple of the converted column and its data type\n   */\n  private static Tuple2<Column, DataType> convertColumnName(\n      StructType schema, Column inputColumn, SchemaConversionDirection conversionDirection) {\n    Function<StructField, String> sourceNameExtractor;\n    Function<StructField, String> targetNameExtractor;\n\n    switch (conversionDirection) {\n      case LOGICAL_TO_PHYSICAL:\n        sourceNameExtractor = StructField::getName;\n        targetNameExtractor = ColumnMapping::getPhysicalName;\n        break;\n      case PHYSICAL_TO_LOGICAL:\n        sourceNameExtractor = ColumnMapping::getPhysicalName;\n        targetNameExtractor = StructField::getName;\n        break;\n      default:\n        throw new IllegalArgumentException(\"Unknown conversion direction: \" + conversionDirection);\n    }\n\n    final List<String> outputNameParts = new ArrayList<>();\n    DataType currentType = schema;\n\n    // Traverse through each level to resolve the corresponding name mapping\n    for (String inputNamePart : inputColumn.getNames()) {\n      if (!(currentType instanceof StructType)) {\n        throw columnNotFoundInSchema(inputColumn, schema);\n      }\n\n      final StructType structType = (StructType) currentType;\n\n      // Find the field that matches the input name using the appropriate matching function\n      final StructField field =\n          structType.fields().stream()\n              .filter(f -> sourceNameExtractor.apply(f).equalsIgnoreCase(inputNamePart))\n              .findFirst()\n              .orElseThrow(() -> columnNotFoundInSchema(inputColumn, schema));\n\n      outputNameParts.add(targetNameExtractor.apply(field));\n      currentType = field.getDataType();\n    }\n\n    return new Tuple2<>(new Column(outputNameParts.toArray(new String[0])), currentType);\n  }\n\n  /** Visible for testing */\n  static int findMaxColumnId(StructType schema) {\n    return new SchemaIterable(schema)\n        .stream()\n            .mapToInt(\n                e -> {\n                  int columnId = hasColumnId(e.getField()) ? getColumnId(e.getField()) : 0;\n                  int nestedMaxId =\n                      hasNestedColumnIds(e.getField()) ? getMaxNestedColumnId(e.getField()) : 0;\n                  return Math.max(columnId, nestedMaxId);\n                })\n            .max()\n            .orElse(0);\n  }\n\n  static boolean hasColumnId(StructField field) {\n    return field.getMetadata().contains(COLUMN_MAPPING_ID_KEY);\n  }\n\n  static boolean hasPhysicalName(StructField field) {\n    return field.getMetadata().contains(COLUMN_MAPPING_PHYSICAL_NAME_KEY);\n  }\n\n  /**\n   * Utility method to convert the given logical schema to physical schema, recursively converting\n   * sub-types in case of complex types. When {@code includeFieldId} is true, converted physical\n   * schema will have field ids in the metadata. Column metadata is otherwise removed.\n   */\n  private static StructType convertToPhysicalSchema(\n      StructType prunedSchema, StructType fullSchema, boolean includeFieldId) {\n    StructType newSchema = new StructType();\n    for (StructField prunedField : prunedSchema.fields()) {\n      newSchema = newSchema.add(convertToPhysicalColumn(prunedField, fullSchema, includeFieldId));\n    }\n    return newSchema;\n  }\n\n  /**\n   * Utility method to convert the given logical field to a physical field, recursively converting\n   * sub-types in case of complex types. When {@code includeFieldId} is true, converted physical\n   * schema will have field ids in the metadata. Column metadata is otherwise removed.\n   */\n  private static StructField convertToPhysicalColumn(\n      StructField logicalField, StructType fullSchema, boolean includeFieldId) {\n    StructField completeField = fullSchema.get(logicalField.getName());\n    DataType physicalType =\n        convertToPhysicalType(\n            logicalField.getDataType(), completeField.getDataType(), includeFieldId);\n    String physicalName = completeField.getMetadata().getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY);\n\n    if (!includeFieldId) {\n      return new StructField(physicalName, physicalType, logicalField.isNullable());\n    }\n\n    Long fieldId = completeField.getMetadata().getLong(COLUMN_MAPPING_ID_KEY);\n    FieldMetadata.Builder builder = FieldMetadata.builder().putLong(PARQUET_FIELD_ID_KEY, fieldId);\n\n    // convertToPhysicalSchema(..) gets called when trying to find the read schema\n    // for the Parquet reader. This currently assumes that if the nested field IDs for\n    // the 'element' and 'key'/'value' fields of Arrays/Maps haven been written,\n    // then IcebergCompatV2 is enabled because the schema we are looking at is from\n    // the DeltaLog and has nested field IDs setup\n    if (hasNestedColumnIds(completeField)) {\n      builder.putFieldMetadata(\n          PARQUET_FIELD_NESTED_IDS_METADATA_KEY, getNestedColumnIds(completeField));\n    }\n\n    return new StructField(physicalName, physicalType, logicalField.isNullable(), builder.build());\n  }\n\n  private static DataType convertToPhysicalType(\n      DataType logicalType, DataType physicalType, boolean includeFieldId) {\n    if (logicalType instanceof StructType) {\n      return convertToPhysicalSchema(\n          (StructType) logicalType, (StructType) physicalType, includeFieldId);\n    } else if (logicalType instanceof ArrayType) {\n      ArrayType logicalArrayType = (ArrayType) logicalType;\n      return new ArrayType(\n          convertToPhysicalType(\n              logicalArrayType.getElementType(),\n              ((ArrayType) physicalType).getElementType(),\n              includeFieldId),\n          logicalArrayType.containsNull());\n    } else if (logicalType instanceof MapType) {\n      MapType logicalMapType = (MapType) logicalType;\n      MapType physicalMapType = (MapType) physicalType;\n      return new MapType(\n          convertToPhysicalType(\n              logicalMapType.getKeyType(), physicalMapType.getKeyType(), includeFieldId),\n          convertToPhysicalType(\n              logicalMapType.getValueType(), physicalMapType.getValueType(), includeFieldId),\n          logicalMapType.isValueContainsNull());\n    }\n    return logicalType;\n  }\n\n  private static boolean validModeChange(ColumnMappingMode oldMode, ColumnMappingMode newMode) {\n    // only upgrade from none to name mapping is allowed\n    return oldMode.equals(newMode)\n        || (oldMode == ColumnMappingMode.NONE && newMode == ColumnMappingMode.NAME);\n  }\n\n  /**\n   * For each column/field in a {@link Metadata}'s schema, assign an id using the current maximum id\n   * as the basis and increment from there. Additionally, assign a physical name based on a random\n   * UUID or re-use the old display name if the mapping mode is updated on an existing table. When\n   * `icebergWriterCompatV1` is enabled, we assign physical names as 'col-[colId]'.\n   *\n   * @param metadata The new metadata to assign ids and physical names to\n   * @param isNewTable whether this is part of a commit that sets the mapping mode on a new table\n   * @return Optional {@link Metadata} with a new schema where ids and physical names have been\n   *     assigned if the schema has changed, returns Optional.empty() otherwise\n   */\n  private static Optional<Metadata> assignColumnIdAndPhysicalName(\n      Metadata metadata, boolean isNewTable) {\n    StructType oldSchema = metadata.getSchema();\n\n    // When icebergWriterCompatV1 or icebergWriterCompatV3 is enabled we require\n    // physicalName='col-[columnId]'\n    boolean useColumnIdForPhysicalName =\n        TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.fromMetadata(metadata)\n            || TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.fromMetadata(metadata);\n\n    // This is the maxColumnId to use when assigning any new field-ids; we update this as we\n    // traverse the schema and after traversal this is the value that should be stored in the\n    // metadata. Note - this could be greater than the current value stored in the metadata if\n    // the connector has added new fields with field-ids\n    AtomicInteger maxColumnId =\n        new AtomicInteger(\n            Math.max(\n                Integer.parseInt(\n                    metadata\n                        .getConfiguration()\n                        .getOrDefault(COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"0\")),\n                findMaxColumnId(oldSchema)));\n\n    StructType newSchema = new StructType();\n    for (StructField field : oldSchema.fields()) {\n      newSchema =\n          newSchema.add(\n              transformAndAssignColumnIdAndPhysicalName(\n                  assignColumnIdAndPhysicalNameToField(\n                      field, maxColumnId, isNewTable, useColumnIdForPhysicalName),\n                  maxColumnId,\n                  isNewTable,\n                  useColumnIdForPhysicalName));\n    }\n\n    if (IcebergCompatMetadataValidatorAndUpdater.isIcebergCompatEnabled(metadata)) {\n      newSchema = rewriteFieldIdsForIceberg(newSchema, maxColumnId);\n    }\n\n    // The maxColumnId in the metadata may be out-of-date either due to field-id assignment\n    // performed in this function, or due to connector adding new fields\n    boolean shouldUpdateMaxId =\n        TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(metadata) != maxColumnId.get();\n\n    // We are comparing the old schema with the new schema to determine if the schema has changed.\n    // If this becomes hotspot, we can consider updating the methods to pass around AtomicBoolean\n    // to track if the schema has changed. It is a bit convoluted to pass around and update the\n    // AtomicBoolean in the recursive and multiple methods.\n    if (oldSchema.equals(newSchema) && !shouldUpdateMaxId) {\n      return Optional.empty();\n    }\n\n    String maxFieldId = Integer.toString(maxColumnId.get());\n    return Optional.of(\n        metadata\n            .withNewSchema(newSchema)\n            .withMergedConfiguration(singletonMap(COLUMN_MAPPING_MAX_COLUMN_ID_KEY, maxFieldId)));\n  }\n\n  /**\n   * Recursively visits each nested struct / array / map type and assigns an id using the current\n   * maximum id as the basis and increments from there. Additionally, assigns a physical name based\n   * on a random UUID or re-uses the old display name if the mapping mode is updated on an existing\n   * table. Note that key / value fields of a map and the element field of an array are not assigned\n   * an id / physical name. Such functionality is actually being handled by {@link\n   * ColumnMapping#rewriteFieldIdsForIceberg(StructType, AtomicInteger)}.\n   *\n   * @param field The current {@link StructField}\n   * @param maxColumnId Holds the current maximum id. Value is incremented whenever the current max\n   *     id value is used to keep the current value always the max id\n   * @param isNewTable Whether this is a new or an existing table. For existing tables the physical\n   *     name will be re-used from the old display name\n   * @param useColumnIdForPhysicalName Whether we should assign physical names to 'col-[colId]'.\n   *     When false uses the default behavior described above.\n   * @return A new {@link StructField} with updated metadata under the {@link\n   *     ColumnMapping#COLUMN_MAPPING_ID_KEY} and the {@link\n   *     ColumnMapping#COLUMN_MAPPING_PHYSICAL_NAME_KEY} keys\n   */\n  private static StructField transformAndAssignColumnIdAndPhysicalName(\n      StructField field,\n      AtomicInteger maxColumnId,\n      boolean isNewTable,\n      boolean useColumnIdForPhysicalName) {\n    DataType dataType = field.getDataType();\n    if (dataType instanceof StructType) {\n      StructType type = (StructType) dataType;\n      StructType schema = new StructType();\n      for (StructField f : type.fields()) {\n        schema =\n            schema.add(\n                transformAndAssignColumnIdAndPhysicalName(\n                    assignColumnIdAndPhysicalNameToField(\n                        f, maxColumnId, isNewTable, useColumnIdForPhysicalName),\n                    maxColumnId,\n                    isNewTable,\n                    useColumnIdForPhysicalName));\n      }\n      return new StructField(field.getName(), schema, field.isNullable(), field.getMetadata());\n    } else if (dataType instanceof ArrayType) {\n      ArrayType type = (ArrayType) dataType;\n      StructField elementField =\n          transformAndAssignColumnIdAndPhysicalName(\n              type.getElementField(), maxColumnId, isNewTable, useColumnIdForPhysicalName);\n      return new StructField(\n          field.getName(), new ArrayType(elementField), field.isNullable(), field.getMetadata());\n    } else if (dataType instanceof MapType) {\n      MapType type = (MapType) dataType;\n      StructField key =\n          transformAndAssignColumnIdAndPhysicalName(\n              type.getKeyField(), maxColumnId, isNewTable, useColumnIdForPhysicalName);\n      StructField value =\n          transformAndAssignColumnIdAndPhysicalName(\n              type.getValueField(), maxColumnId, isNewTable, useColumnIdForPhysicalName);\n      return new StructField(\n          field.getName(), new MapType(key, value), field.isNullable(), field.getMetadata());\n    }\n    return field;\n  }\n\n  /**\n   * Assigns an id using the current maximum id as the basis and increments from there.\n   * Additionally, assigns a physical name based on a random UUID or re-uses the old display name if\n   * the mapping mode is updated on an existing table.\n   *\n   * @param field The current {@link StructField} to assign an id / physical name to\n   * @param maxColumnId Holds the current maximum id. Value is incremented whenever the current max\n   *     id value is used to keep the current value always the max id\n   * @param isNewTable Whether this is a new or an existing table. For existing tables the physical\n   *     name will be re-used from the old display name\n   * @param useColumnIdForPhysicalName Whether we should assign physical names to 'col-[colId]'.\n   *     When false uses the default behavior described above.\n   * @return A new {@link StructField} with updated metadata under the {@link\n   *     ColumnMapping#COLUMN_MAPPING_ID_KEY} and the {@link\n   *     ColumnMapping#COLUMN_MAPPING_PHYSICAL_NAME_KEY} keys\n   */\n  private static StructField assignColumnIdAndPhysicalNameToField(\n      StructField field,\n      AtomicInteger maxColumnId,\n      boolean isNewTable,\n      boolean useColumnIdForPhysicalName) {\n    if (hasColumnId(field) ^ hasPhysicalName(field)) {\n      // If a connector is providing column mapping metadata in the given schema we require it to be\n      // complete\n      throw new IllegalArgumentException(\n          String.format(\n              \"Both columnId and physicalName must be present if one is present. \"\n                  + \"Found this field with incomplete column mapping metadata: %s\",\n              field));\n    }\n    if (!hasColumnId(field)) {\n      field =\n          field.withNewMetadata(\n              FieldMetadata.builder()\n                  .fromMetadata(field.getMetadata())\n                  .putLong(COLUMN_MAPPING_ID_KEY, maxColumnId.incrementAndGet())\n                  .build());\n    }\n    if (!hasPhysicalName(field)) {\n      // re-use old display names as physical names when a table is updated\n      String physicalName;\n      if (useColumnIdForPhysicalName) {\n        long columnId = getColumnId(field);\n        physicalName = String.format(\"col-%s\", columnId);\n      } else {\n        physicalName = isNewTable ? \"col-\" + UUID.randomUUID() : field.getName();\n      }\n\n      field =\n          field.withNewMetadata(\n              FieldMetadata.builder()\n                  .fromMetadata(field.getMetadata())\n                  .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, physicalName)\n                  .build());\n    }\n    return field;\n  }\n\n  private static boolean hasNestedColumnIds(StructField field) {\n    return field.getMetadata().contains(COLUMN_MAPPING_NESTED_IDS_KEY);\n  }\n\n  private static FieldMetadata getNestedColumnIds(StructField field) {\n    return field.getMetadata().getMetadata(COLUMN_MAPPING_NESTED_IDS_KEY);\n  }\n\n  private static int getMaxNestedColumnId(StructField field) {\n    return getNestedColumnIds(field).getEntries().values().stream()\n        .filter(Long.class::isInstance)\n        .map(Long.class::cast)\n        .max(Comparator.naturalOrder())\n        .orElse(0L)\n        .intValue();\n  }\n\n  /**\n   * Adds the nested field IDs required by Iceberg.\n   *\n   * <p>In parquet, list-type columns have a nested, implicitly defined {@code element} field and\n   * map-type columns have implicitly defined {@code key} and {@code value} fields. By default,\n   * Spark does not write field IDs for these fields in the parquet files. However, Iceberg requires\n   * these *nested* field IDs to be present. This method rewrites the specified schema to add those\n   * nested field IDs.\n   *\n   * <p>Nested field IDs are stored in a map as part of the metadata of the *nearest* parent {@link\n   * StructField}. For example, consider the following schema:\n   *\n   * <p>col1 ARRAY(INT) col2 MAP(INT, INT) col3 STRUCT(a INT, b ARRAY(STRUCT(c INT, d MAP(INT,\n   * INT))))\n   *\n   * <p>col1 is a list and so requires one nested field ID for the {@code element} field in parquet.\n   * This nested field ID will be stored in a map that is part of col1's {@link\n   * StructField#getMetadata()}. The same applies to the nested field IDs for col2's implicit {@code\n   * key} and {@code value} fields. col3 itself is a Struct, consisting of an integer field and a\n   * list field named 'b'. The nested field ID for the list of 'b' is stored in b's {@link\n   * StructField#getMetadata()}. Finally, the list type itself is again a struct consisting of an\n   * integer field and a map field named 'd'. The nested field IDs for the map of 'd' are stored in\n   * d's {@link StructField#getMetadata()}.\n   *\n   * @param schema The schema to which nested field IDs should be added\n   * @param startId The first field ID to use for the nested field IDs\n   */\n  private static StructType rewriteFieldIdsForIceberg(StructType schema, AtomicInteger startId) {\n    StructType newSchema = new StructType();\n    for (StructField field : schema.fields()) {\n      FieldMetadata.Builder builder = FieldMetadata.builder().fromMetadata(field.getMetadata());\n      newSchema =\n          newSchema.add(\n              transformSchema(\n                      startId,\n                      field,\n                      \"\",\n                      /** current column path */\n                      builder)\n                  .withNewMetadata(builder.build()));\n    }\n    return newSchema;\n  }\n\n  /**\n   * Recursively visits each nested struct / array / map type and returns a new {@link StructField}\n   * with updated {@link FieldMetadata}. For array / map types the field IDs of their nested\n   * elements are under the {@link ColumnMapping#COLUMN_MAPPING_NESTED_IDS_KEY} key. A concrete\n   * schema example can be seen at {@link ColumnMapping#rewriteFieldIdsForIceberg(StructType,\n   * AtomicInteger)}.\n   *\n   * @param currentFieldId The current maximum field id to increment and use for assignment\n   * @param structField The field where to start from\n   * @param path The current field path relative to the parent field (aka most recent ancestor with\n   *     a StructField). An empty path indicates that there's no parent and we're at the root\n   * @param closestStructFieldParentMetadata The metadata builder of the closest struct field parent\n   *     where nested IDs will be stored. For StructFields this is the current field. For\n   *     map/arrays, it is the closest parent that is a struct field.\n   * @return A new {@link StructField} with updated {@link FieldMetadata}\n   */\n  private static StructField transformSchema(\n      AtomicInteger currentFieldId,\n      StructField structField,\n      String path,\n      FieldMetadata.Builder closestStructFieldParentMetadata) {\n    DataType dataType = structField.getDataType();\n    if (dataType instanceof StructType) {\n      StructType type = (StructType) dataType;\n      List<StructField> fields =\n          type.fields().stream()\n              .map(\n                  field -> {\n                    FieldMetadata.Builder metadataBuilder =\n                        FieldMetadata.builder().fromMetadata(field.getMetadata());\n                    return transformSchema(\n                            currentFieldId, field, getPhysicalName(field), metadataBuilder)\n                        .withNewMetadata(metadataBuilder.build());\n                  })\n              .collect(Collectors.toList());\n      return new StructField(\n          structField.getName(),\n          new StructType(fields),\n          structField.isNullable(),\n          structField.getMetadata());\n    } else if (dataType instanceof ArrayType) {\n      ArrayType type = (ArrayType) dataType;\n      String basePath = \"\".equals(path) ? getPhysicalName(structField) : path;\n      // update element type metadata and recurse into element type\n      String elementPath = basePath + \".\" + type.getElementField().getName();\n      maybeUpdateFieldId(closestStructFieldParentMetadata, elementPath, currentFieldId);\n      StructField elementType =\n          transformSchema(\n              currentFieldId,\n              type.getElementField(),\n              elementPath,\n              closestStructFieldParentMetadata);\n      return new StructField(\n          structField.getName(),\n          new ArrayType(elementType),\n          structField.isNullable(),\n          structField.getMetadata());\n    } else if (dataType instanceof MapType) {\n      MapType type = (MapType) dataType;\n      // update key type metadata and recurse into key type\n      String basePath = \"\".equals(path) ? getPhysicalName(structField) : path;\n      String keyPath = basePath + \".\" + type.getKeyField().getName();\n      maybeUpdateFieldId(closestStructFieldParentMetadata, keyPath, currentFieldId);\n      StructField key =\n          transformSchema(\n              currentFieldId, type.getKeyField(), keyPath, closestStructFieldParentMetadata);\n      // update value type metadata and recurse into value type\n      String valuePath = basePath + \".\" + type.getValueField().getName();\n      maybeUpdateFieldId(closestStructFieldParentMetadata, valuePath, currentFieldId);\n      StructField value =\n          transformSchema(\n              currentFieldId, type.getValueField(), valuePath, closestStructFieldParentMetadata);\n      return new StructField(\n          structField.getName(),\n          new MapType(key, value),\n          structField.isNullable(),\n          structField.getMetadata());\n    }\n\n    return structField;\n  }\n\n  /**\n   * The {@code field} being passed here is either {@link ArrayType#getElementField()} or one of\n   * {@link MapType#getKeyField()}, {@link MapType#getValueField()}. For a map the passed in {@code\n   * key} will be one of columnName.key or columnName.value. For an array the passed {@code key}\n   * will be columnName.element. The columnName in this case is either the physical or the display\n   * name of the column.\n   *\n   * <p>Below is an example that shows the {@link FieldMetadata} of an array named <b>b</b>, where\n   * the array itself is assigned id = 2 with a physical name that includes a UUID. That metadata\n   * field then holds a nested {@link FieldMetadata} under the {@code COLUMN_MAPPING_NESTED_IDS_KEY}\n   * key as can be seen below, which in turn contains the assigned id.\n   *\n   * <blockquote>\n   *\n   * <pre>\n   * {\n   *   \"name\": \"b\",\n   *   \"type\": {\n   *     \"type\": \"array\",\n   *     \"elementType\": \"integer\",\n   *     \"containsNull\": true\n   *   },\n   *   \"nullable\": true,\n   *   \"metadata\": {\n   *     \"delta.columnMapping.id\": 2,\n   *     \"delta.columnMapping.physicalName\": \"col-859d81a5-6e36-4e43-9c8e-46aa7d80dce6\"\n   *     \"delta.columnMapping.nested.ids\": {\n   *       \"col-859d81a5-6e36-4e43-9c8e-46aa7d80dce6.element\": 4\n   *     },\n   *   }\n   * }\n   * </pre>\n   *\n   * </blockquote>\n   *\n   * @param fieldMetadataBuilder The FieldMetadata.Builder to update with nested IDs\n   * @param key For a map this is <colName>.key or <colName>.value. For an array this is\n   *     <colName>.element\n   * @param currentFieldId The current maximum field id to increment and use for assignment\n   */\n  private static void maybeUpdateFieldId(\n      FieldMetadata.Builder fieldMetadataBuilder, String key, AtomicInteger currentFieldId) {\n    // init the nested metadata that holds the nested ids\n    FieldMetadata nestedMetadata = fieldMetadataBuilder.getMetadata(COLUMN_MAPPING_NESTED_IDS_KEY);\n    if (fieldMetadataBuilder.getMetadata(COLUMN_MAPPING_NESTED_IDS_KEY) == null) {\n      fieldMetadataBuilder.putFieldMetadata(COLUMN_MAPPING_NESTED_IDS_KEY, FieldMetadata.empty());\n      nestedMetadata = fieldMetadataBuilder.getMetadata(COLUMN_MAPPING_NESTED_IDS_KEY);\n    }\n\n    // assign an id to the nested element and update the metadata\n    if (!nestedMetadata.contains(key)) {\n      FieldMetadata newNestedMeta =\n          FieldMetadata.builder()\n              .fromMetadata(nestedMetadata)\n              .putLong(key, currentFieldId.incrementAndGet())\n              .build();\n      fieldMetadataBuilder.putFieldMetadata(COLUMN_MAPPING_NESTED_IDS_KEY, newNestedMeta);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/DateTimeConstants.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\npublic class DateTimeConstants {\n\n  public static final int MONTHS_PER_YEAR = 12;\n\n  public static final byte DAYS_PER_WEEK = 7;\n\n  public static final long HOURS_PER_DAY = 24L;\n\n  public static final long MINUTES_PER_HOUR = 60L;\n\n  public static final long SECONDS_PER_MINUTE = 60L;\n  public static final long SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE;\n  public static final long SECONDS_PER_DAY = HOURS_PER_DAY * SECONDS_PER_HOUR;\n\n  public static final long MILLIS_PER_SECOND = 1000L;\n  public static final long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND;\n  public static final long MILLIS_PER_HOUR = MINUTES_PER_HOUR * MILLIS_PER_MINUTE;\n  public static final long MILLIS_PER_DAY = HOURS_PER_DAY * MILLIS_PER_HOUR;\n\n  public static final long MICROS_PER_MILLIS = 1000L;\n  public static final long MICROS_PER_SECOND = MILLIS_PER_SECOND * MICROS_PER_MILLIS;\n  public static final long MICROS_PER_MINUTE = SECONDS_PER_MINUTE * MICROS_PER_SECOND;\n  public static final long MICROS_PER_HOUR = MINUTES_PER_HOUR * MICROS_PER_MINUTE;\n  public static final long MICROS_PER_DAY = HOURS_PER_DAY * MICROS_PER_HOUR;\n\n  public static final long NANOS_PER_MICROS = 1000L;\n  public static final long NANOS_PER_MILLIS = MICROS_PER_MILLIS * NANOS_PER_MICROS;\n  public static final long NANOS_PER_SECOND = MILLIS_PER_SECOND * NANOS_PER_MILLIS;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/DirectoryCreationUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.CHECKPOINT_V2_RW_FEATURE;\n\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport java.io.IOException;\nimport java.util.Optional;\n\n/** Utility class for creating Delta directories based on commit version and protocol features. */\npublic class DirectoryCreationUtils {\n  private DirectoryCreationUtils() {}\n\n  /** Creates all required Delta directories based on commit version and protocol features. */\n  public static void createAllDeltaDirectoriesAsNeeded(\n      Engine engine,\n      Path logPath,\n      long commitAsVersion,\n      Optional<Protocol> readProtocol,\n      Protocol writeProtocol)\n      throws IOException {\n    createDeltaLogDirectoryIfNeeded(engine, logPath, commitAsVersion);\n    createStagedCommitDirectoryIfNeeded(engine, logPath, readProtocol, writeProtocol);\n    createSidecarDirectoryIfNeeded(engine, logPath, readProtocol, writeProtocol);\n  }\n\n  /** Creates delta log directory (_delta_log) if this is the initial commit (version 0). */\n  private static void createDeltaLogDirectoryIfNeeded(\n      Engine engine, Path logPath, long commitAsVersion) throws IOException {\n    if (commitAsVersion == 0) {\n      createDirectoryOrThrow(engine, logPath.toString());\n    }\n  }\n\n  /**\n   * Creates staged commit directory (_delta_log/_staged_commits) when enabling catalog managed\n   * feature.\n   */\n  private static void createStagedCommitDirectoryIfNeeded(\n      Engine engine, Path logPath, Optional<Protocol> readProtocol, Protocol writeProtocol)\n      throws IOException {\n    final boolean readVersionSupportsCatalogManaged =\n        readProtocol.map(TableFeatures::isCatalogManagedSupported).orElse(false);\n    final boolean writeVersionSupportsCatalogManaged =\n        TableFeatures.isCatalogManagedSupported(writeProtocol);\n\n    if (!readVersionSupportsCatalogManaged && writeVersionSupportsCatalogManaged) {\n      createDirectoryOrThrow(engine, FileNames.stagedCommitDirectory(logPath));\n    }\n  }\n\n  /** Creates sidecar directory (_delta_log/_sidecar) when enabling v2 checkpoint feature. */\n  private static void createSidecarDirectoryIfNeeded(\n      Engine engine, Path logPath, Optional<Protocol> readProtocol, Protocol writeProtocol)\n      throws IOException {\n    final boolean readVersionSupportsV2Checkpoints =\n        readProtocol.map(p -> p.supportsFeature(CHECKPOINT_V2_RW_FEATURE)).orElse(false);\n    final boolean writeVersionSupportsV2Checkpoints =\n        writeProtocol.supportsFeature(CHECKPOINT_V2_RW_FEATURE);\n\n    if (!readVersionSupportsV2Checkpoints && writeVersionSupportsV2Checkpoints) {\n      createDirectoryOrThrow(engine, FileNames.sidecarDirectory(logPath));\n    }\n  }\n\n  /** Creates directory using engine filesystem client, throws on failure. */\n  private static void createDirectoryOrThrow(Engine engine, String directoryPath)\n      throws IOException {\n    try {\n      if (!engine.getFileSystemClient().mkdirs(directoryPath)) {\n        throw new RuntimeException(\"Failed to create directory: \" + directoryPath);\n      }\n    } catch (Exception e) {\n      throw new IOException(\"Creating directories for path \" + directoryPath, e);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/DomainMetadataUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.actions.DomainMetadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class DomainMetadataUtils {\n\n  private DomainMetadataUtils() {\n    // Empty private constructor to prevent instantiation\n  }\n\n  /**\n   * Populate the map of domain metadata from actions. When encountering duplicate domain metadata\n   * actions for the same domain, this method preserves the first seen entry and skips subsequent\n   * entries. This behavior is especially useful for log replay as we want to ensure that earlier\n   * domain metadata entries take precedence over later ones.\n   *\n   * @param domainMetadataActionVector A {@link ColumnVector} containing the domain metadata rows\n   * @param domainMetadataMap The existing map to be populated with domain metadata entries, where\n   *     the key is the domain name and the value is the domain metadata\n   */\n  public static void populateDomainMetadataMap(\n      ColumnVector domainMetadataActionVector, Map<String, DomainMetadata> domainMetadataMap) {\n    final int vectorSize = domainMetadataActionVector.getSize();\n    for (int rowId = 0; rowId < vectorSize; rowId++) {\n      DomainMetadata dm = DomainMetadata.fromColumnVector(domainMetadataActionVector, rowId);\n      if (dm != null && !domainMetadataMap.containsKey(dm.getDomain())) {\n        // We only add the domain metadata if its domain name not already present in the map\n        domainMetadataMap.put(dm.getDomain(), dm);\n      }\n    }\n  }\n\n  /**\n   * Validates the list of domain metadata actions before committing them. It ensures that\n   *\n   * <ol>\n   *   <li>domain metadata actions are only present when supported by the table protocol\n   *   <li>there are no duplicate domain metadata actions for the same domain in the provided\n   *       actions.\n   * </ol>\n   *\n   * @param domainMetadataActions The list of domain metadata actions to validate\n   * @param protocol The protocol to check for domain metadata support\n   */\n  public static void validateDomainMetadatas(\n      List<DomainMetadata> domainMetadataActions, Protocol protocol) {\n    if (domainMetadataActions.isEmpty()) return;\n\n    // The list of domain metadata is non-empty, so the protocol must support domain metadata\n    if (!TableFeatures.isDomainMetadataSupported(protocol)) {\n      throw DeltaErrors.domainMetadataUnsupported();\n    }\n\n    Map<String, DomainMetadata> domainMetadataMap = new HashMap<>();\n    for (DomainMetadata domainMetadata : domainMetadataActions) {\n      String domain = domainMetadata.getDomain();\n      if (domainMetadataMap.containsKey(domain)) {\n        String message =\n            String.format(\n                \"Multiple actions detected for domain '%s' in single transaction: '%s' and '%s'. \"\n                    + \"Only one action per domain is allowed.\",\n                domain, domainMetadataMap.get(domain).toString(), domainMetadata.toString());\n        throw new IllegalArgumentException(message);\n      }\n      domainMetadataMap.put(domain, domainMetadata);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/ExpressionUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.CollationIdentifier;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class ExpressionUtils {\n  /** Return an expression cast as a predicate, throw an error if it is not a predicate */\n  public static Predicate asPredicate(Expression expression) {\n    checkArgument(expression instanceof Predicate, \"Expected predicate but got %s\", expression);\n    return (Predicate) expression;\n  }\n\n  /** Utility method to return the left child of the binary input expression */\n  public static Expression getLeft(Expression expression) {\n    List<Expression> children = expression.getChildren();\n    checkArgument(\n        children.size() == 2, \"%s: expected two inputs, but got %s\", expression, children.size());\n    return children.get(0);\n  }\n\n  /** Utility method to return the right child of the binary input expression */\n  public static Expression getRight(Expression expression) {\n    List<Expression> children = expression.getChildren();\n    checkArgument(\n        children.size() == 2, \"%s: expected two inputs, but got %s\", expression, children.size());\n    return children.get(1);\n  }\n\n  /** Utility method to return the single child of the unary input expression */\n  public static Expression getUnaryChild(Expression expression) {\n    List<Expression> children = expression.getChildren();\n    checkArgument(\n        children.size() == 1, \"%s: expected one inputs, but got %s\", expression, children.size());\n    return children.get(0);\n  }\n\n  /** Utility method to create a predicate with an optional collation identifier */\n  public static Predicate createPredicate(\n      String name, List<Expression> children, Optional<CollationIdentifier> collationIdentifier) {\n    if (collationIdentifier.isPresent()) {\n      return new Predicate(name, children, collationIdentifier.get());\n    } else {\n      return new Predicate(name, children);\n    }\n  }\n\n  /** Utility method to create a binary predicate with an optional collation identifier */\n  public static Predicate createPredicate(\n      String name,\n      Expression left,\n      Expression right,\n      Optional<CollationIdentifier> collationIdentifier) {\n    return createPredicate(name, Arrays.asList(left, right), collationIdentifier);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/FileNames.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.utils.FileStatus;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class FileNames {\n\n  private FileNames() {}\n\n  ////////////////////////////////////////////////\n  // File name patterns and other static values //\n  ////////////////////////////////////////////////\n\n  // TODO: Delete this in favor of ParsedLogCategory.\n  public enum DeltaLogFileType {\n    COMMIT,\n    LOG_COMPACTION,\n    CHECKPOINT,\n    CHECKSUM\n  }\n\n  /** Example: 00000000000000000001.json */\n  private static final Pattern DELTA_FILE_PATTERN = Pattern.compile(\"\\\\d+\\\\.json\");\n\n  /** Example: 00000000000000000001.00000000000000000009.compacted.json */\n  private static final Pattern COMPACTION_FILE_PATTERN =\n      Pattern.compile(\"\\\\d+\\\\.\\\\d+\\\\.compacted\\\\.json\");\n\n  /** Example: 00000000000000000001.dc0f9f58-a1a0-46fd-971a-bd8b2e9dbb81.json */\n  private static final Pattern UUID_DELTA_FILE_REGEX = Pattern.compile(\"(\\\\d+)\\\\.([^\\\\.]+)\\\\.json\");\n\n  /**\n   * Examples:\n   *\n   * <ul>\n   *   <li>Classic V1 - 00000000000000000001.checkpoint.parquet\n   *   <li>Multi-part V1 - 00000000000000000001.checkpoint.0000000001.0000000010.parquet\n   *   <li>V2 JSON - 00000000000000000001.checkpoint.uuid-1234abcd.json\n   *   <li>V2 Parquet - 00000000000000000001.checkpoint.uuid-1234abcd.parquet\n   * </ul>\n   */\n  private static final Pattern CHECKPOINT_FILE_PATTERN =\n      Pattern.compile(\"(\\\\d+)\\\\.checkpoint((\\\\.\\\\d+\\\\.\\\\d+)?\\\\.parquet|\\\\.[^.]+\\\\.(json|parquet))\");\n\n  /** Example: 00000000000000000001.checkpoint.parquet */\n  private static final Pattern CLASSIC_CHECKPOINT_FILE_PATTERN =\n      Pattern.compile(\"\\\\d+\\\\.checkpoint\\\\.parquet\");\n\n  /** Example: 00000000000000000001.crc */\n  private static final Pattern CHECK_SUM_FILE_PATTERN = Pattern.compile(\"(\\\\d+)\\\\.crc\");\n\n  /**\n   * Examples:\n   *\n   * <ul>\n   *   <li>00000000000000000001.checkpoint.dc0f9f58-a1a0-46fd-971a-bd8b2e9dbb81.json\n   *   <li>00000000000000000001.checkpoint.dc0f9f58-a1a0-46fd-971a-bd8b2e9dbb81.parquet\n   * </ul>\n   */\n  private static final Pattern V2_CHECKPOINT_FILE_PATTERN =\n      Pattern.compile(\"(\\\\d+)\\\\.checkpoint\\\\.[^.]+\\\\.(json|parquet)\");\n\n  /** Example: 00000000000000000001.checkpoint.0000000020.0000000060.parquet */\n  public static final Pattern MULTI_PART_CHECKPOINT_FILE_PATTERN =\n      Pattern.compile(\"(\\\\d+)\\\\.checkpoint\\\\.(\\\\d+)\\\\.(\\\\d+)\\\\.parquet\");\n\n  public static final String STAGED_COMMIT_DIRECTORY = \"_staged_commits\";\n\n  public static final String SIDECAR_DIRECTORY = \"_sidecars\";\n\n  public static DeltaLogFileType determineFileType(FileStatus file) {\n    final String fileName = file.getPath().toString();\n\n    if (isCommitFile(fileName)) {\n      return DeltaLogFileType.COMMIT;\n    } else if (isCheckpointFile(fileName)) {\n      return DeltaLogFileType.CHECKPOINT;\n    } else if (isLogCompactionFile(fileName)) {\n      return DeltaLogFileType.LOG_COMPACTION;\n    } else if (isChecksumFile(fileName)) {\n      return DeltaLogFileType.CHECKSUM;\n    } else {\n      throw new IllegalStateException(\"Unexpected file type: \" + fileName);\n    }\n  }\n\n  ////////////////////////\n  // Version extractors //\n  ////////////////////////\n\n  /**\n   * Get the version of the checkpoint, checksum or delta file. Throws an error if an unexpected\n   * file type is seen. These unexpected files should be filtered out to ensure forward\n   * compatibility in cases where new file types are added, but without an explicit protocol\n   * upgrade.\n   */\n  public static long getFileVersion(Path path) {\n    if (isCheckpointFile(path.getName())) {\n      return checkpointVersion(path);\n    } else if (isCommitFile(path.getName())) {\n      return deltaVersion(path);\n    } else if (isChecksumFile(path.getName())) {\n      return checksumVersion(path);\n    } else {\n      throw new IllegalArgumentException(\n          String.format(\"Unexpected file type found in transaction log: %s\", path));\n    }\n  }\n\n  /** Returns the version for the given delta path. */\n  public static long deltaVersion(Path path) {\n    return Long.parseLong(path.getName().split(\"\\\\.\")[0]);\n  }\n\n  public static long deltaVersion(String path) {\n    final int slashIdx = path.lastIndexOf(Path.SEPARATOR);\n    final String name = path.substring(slashIdx + 1);\n    return Long.parseLong(name.split(\"\\\\.\")[0]);\n  }\n\n  /** Returns the start and end versions for the given compaction path. */\n  public static Tuple2<Long, Long> logCompactionVersions(Path path) {\n    final String[] split = path.getName().split(\"\\\\.\");\n    return new Tuple2<>(Long.parseLong(split[0]), Long.parseLong(split[1]));\n  }\n\n  public static Tuple2<Long, Long> logCompactionVersions(String path) {\n    return logCompactionVersions(new Path(path));\n  }\n\n  /** Returns the version for the given checkpoint path. */\n  public static long checkpointVersion(Path path) {\n    return Long.parseLong(path.getName().split(\"\\\\.\")[0]);\n  }\n\n  public static long checkpointVersion(String path) {\n    final int slashIdx = path.lastIndexOf(Path.SEPARATOR);\n    final String name = path.substring(slashIdx + 1);\n    return Long.parseLong(name.split(\"\\\\.\")[0]);\n  }\n\n  public static Tuple2<Integer, Integer> multiPartCheckpointPartAndNumParts(Path path) {\n    final String fileName = path.getName();\n    final Matcher matcher = MULTI_PART_CHECKPOINT_FILE_PATTERN.matcher(fileName);\n    checkArgument(\n        matcher.matches(), String.format(\"Path is not a multi-part checkpoint file: %s\", fileName));\n    final int partNum = Integer.parseInt(matcher.group(2));\n    final int numParts = Integer.parseInt(matcher.group(3));\n    return new Tuple2<>(partNum, numParts);\n  }\n\n  public static Tuple2<Integer, Integer> multiPartCheckpointPartAndNumParts(String path) {\n    return multiPartCheckpointPartAndNumParts(new Path(path));\n  }\n\n  /////////////////////\n  // Directory paths //\n  /////////////////////\n\n  public static String stagedCommitDirectory(Path logPath) {\n    return new Path(logPath, STAGED_COMMIT_DIRECTORY).toString();\n  }\n\n  public static String sidecarDirectory(Path logPath) {\n    return new Path(logPath, SIDECAR_DIRECTORY).toString();\n  }\n\n  ///////////////////////////////////\n  // File path and prefix builders //\n  ///////////////////////////////////\n\n  /** Returns the delta (json format) path for a given delta file. */\n  public static String deltaFile(Path path, long version) {\n    return String.format(\"%s/%020d.json\", path, version);\n  }\n\n  /** Returns the delta (json format) path for a given delta file. */\n  public static String deltaFile(String path, long version) {\n    return deltaFile(new Path(path), version);\n  }\n\n  public static String stagedCommitFile(Path logPath, long version) {\n    final Path stagedCommitPath = new Path(logPath, STAGED_COMMIT_DIRECTORY);\n    return String.format(\"%s/%020d.%s.json\", stagedCommitPath, version, UUID.randomUUID());\n  }\n\n  public static String stagedCommitFile(String logPath, long version) {\n    return stagedCommitFile(new Path(logPath), version);\n  }\n\n  /** Example: /a/_sidecars/3a0d65cd-4056-49b8-937b-95f9e3ee90e5.parquet */\n  public static String sidecarFile(Path path, String sidecar) {\n    return String.format(\"%s/%s/%s\", path.toString(), SIDECAR_DIRECTORY, sidecar);\n  }\n\n  /** Returns the path to the checksum file for the given version. */\n  public static Path checksumFile(Path path, long version) {\n    return new Path(path, String.format(\"%020d.crc\", version));\n  }\n\n  public static long checksumVersion(Path path) {\n    return Long.parseLong(path.getName().split(\"\\\\.\")[0]);\n  }\n\n  public static long checksumVersion(String path) {\n    return checksumVersion(new Path(path));\n  }\n\n  /**\n   * Returns the prefix of all delta log files for the given version.\n   *\n   * <p>Intended for use with listFrom to get all files from this version onwards. The returned Path\n   * will not exist as a file.\n   */\n  public static String listingPrefix(Path path, long version) {\n    return String.format(\"%s/%020d.\", path, version);\n  }\n\n  /**\n   * Returns the path for a singular checkpoint up to the given version.\n   *\n   * <p>In a future protocol version this path will stop being written.\n   */\n  public static Path checkpointFileSingular(Path path, long version) {\n    return new Path(path, String.format(\"%020d.checkpoint.parquet\", version));\n  }\n\n  /**\n   * Returns the path for a top-level V2 checkpoint file up to the given version with a given UUID\n   * and filetype (JSON or Parquet).\n   */\n  public static Path topLevelV2CheckpointFile(\n      Path path, long version, String uuid, String fileType) {\n    assert (fileType.equals(\"json\") || fileType.equals(\"parquet\"));\n    return new Path(path, String.format(\"%020d.checkpoint.%s.%s\", version, uuid, fileType));\n  }\n\n  /** Returns the path for a V2 sidecar file with a given UUID. */\n  public static Path v2CheckpointSidecarFile(Path path, String uuid) {\n    return new Path(String.format(\"%s/%s/%s.parquet\", path.toString(), SIDECAR_DIRECTORY, uuid));\n  }\n\n  public static Path multiPartCheckpointFile(Path path, long version, int part, int numParts) {\n    return new Path(\n        path, String.format(\"%020d.checkpoint.%010d.%010d.parquet\", version, part, numParts));\n  }\n\n  /**\n   * Returns the paths for all parts of the checkpoint up to the given version.\n   *\n   * <p>In a future protocol version we will write this path instead of checkpointFileSingular.\n   *\n   * <p>Example of the format: 00000000000000004915.checkpoint.0000000020.0000000060.parquet is\n   * checkpoint part 20 out of 60 for the snapshot at version 4915. Zero padding is for\n   * lexicographic sorting.\n   */\n  public static List<Path> checkpointFileWithParts(Path path, long version, int numParts) {\n    final List<Path> output = new ArrayList<>();\n    for (int i = 1; i < numParts + 1; i++) {\n      output.add(multiPartCheckpointFile(path, version, i, numParts));\n    }\n    return output;\n  }\n\n  /**\n   * Return the path that should be used for a log compaction file.\n   *\n   * @param logPath path to the delta log location\n   * @param startVersion the start version for the log compaction\n   * @param endVersion the end version for the log compaction\n   */\n  public static Path logCompactionPath(Path logPath, long startVersion, long endVersion) {\n    String fileName = String.format(\"%020d.%020d.compacted.json\", startVersion, endVersion);\n    return new Path(logPath, fileName);\n  }\n\n  /////////////////////////////\n  // Is <type> file checkers //\n  /////////////////////////////\n\n  public static boolean isCheckpointFile(String path) {\n    return CHECKPOINT_FILE_PATTERN.matcher(new Path(path).getName()).matches();\n  }\n\n  public static boolean isClassicCheckpointFile(String path) {\n    return CLASSIC_CHECKPOINT_FILE_PATTERN.matcher(new Path(path).getName()).matches();\n  }\n\n  public static boolean isMultiPartCheckpointFile(String path) {\n    return MULTI_PART_CHECKPOINT_FILE_PATTERN.matcher(new Path(path).getName()).matches();\n  }\n\n  public static boolean isV2CheckpointFile(String path) {\n    return V2_CHECKPOINT_FILE_PATTERN.matcher(new Path(path).getName()).matches();\n  }\n\n  public static boolean isCommitFile(String path) {\n    final String fileName = new Path(path).getName();\n    return DELTA_FILE_PATTERN.matcher(fileName).matches()\n        || UUID_DELTA_FILE_REGEX.matcher(fileName).matches();\n  }\n\n  public static boolean isPublishedDeltaFile(String path) {\n    final String fileName = new Path(path).getName();\n    return DELTA_FILE_PATTERN.matcher(fileName).matches();\n  }\n\n  public static boolean isStagedDeltaFile(String path) {\n    final Path p = new Path(path);\n    if (!p.getParent().getName().equals(STAGED_COMMIT_DIRECTORY)) {\n      return false;\n    }\n    return UUID_DELTA_FILE_REGEX.matcher(p.getName()).matches();\n  }\n\n  public static boolean isLogCompactionFile(String path) {\n    final String fileName = new Path(path).getName();\n    return COMPACTION_FILE_PATTERN.matcher(fileName).matches();\n  }\n\n  public static boolean isChecksumFile(String checksumFilePath) {\n    return CHECK_SUM_FILE_PATTERN.matcher(new Path(checksumFilePath).getName()).matches();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/InCommitTimestampUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.CommitInfo;\nimport io.delta.kernel.internal.actions.Metadata;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\n\npublic class InCommitTimestampUtils {\n\n  /**\n   * Returns the updated {@link Metadata} with inCommitTimestamp enablement related info (version\n   * and timestamp) correctly set. This is done only 1. If this transaction enables\n   * inCommitTimestamp. 2. If the commit version is not 0. This is because we only need to persist\n   * the enablement info if there are non-ICT commits in the Delta log. Note: This function must\n   * only be called after transaction conflicts have been resolved.\n   */\n  public static Optional<Metadata> getUpdatedMetadataWithICTEnablementInfo(\n      Engine engine,\n      long inCommitTimestamp,\n      Optional<SnapshotImpl> readSnapshotOpt,\n      Metadata metadata,\n      long commitVersion) {\n    if (readSnapshotOpt.isPresent()\n        && didCurrentTransactionEnableICT(engine, metadata, readSnapshotOpt.get())) {\n      Map<String, String> enablementTrackingProperties = new HashMap<>();\n      enablementTrackingProperties.put(\n          TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey(),\n          Long.toString(commitVersion));\n      enablementTrackingProperties.put(\n          TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey(),\n          Long.toString(inCommitTimestamp));\n\n      Metadata newMetadata = metadata.withMergedConfiguration(enablementTrackingProperties);\n      return Optional.of(newMetadata);\n    } else {\n      return Optional.empty();\n    }\n  }\n\n  /**\n   * Tries to extract the inCommitTimestamp from the commitInfo action in the given ColumnarBatch.\n   * When inCommitTimestamp is enabled, the commitInfo action is always the first action in the\n   * delta file. This function assumes that this batch is the leading batch of a single delta file\n   * and attempts to extract the commitInfo action from the first row. If the commitInfo action is\n   * not present or does not contain an inCommitTimestamp, this function returns an empty Optional.\n   */\n  public static Optional<Long> tryExtractInCommitTimestamp(\n      ColumnarBatch firstActionsBatchFromSingleDelta) {\n    final int commitInfoOrdinal =\n        firstActionsBatchFromSingleDelta.getSchema().indexOf(\"commitInfo\");\n    if (commitInfoOrdinal == -1) {\n      return Optional.empty();\n    }\n    ColumnVector commitInfoVector =\n        firstActionsBatchFromSingleDelta.getColumnVector(commitInfoOrdinal);\n    // CommitInfo is always the first action in the batch when inCommitTimestamp is enabled.\n    int expectedRowIdOfCommitInfo = 0;\n    CommitInfo commitInfo =\n        CommitInfo.fromColumnVector(commitInfoVector, expectedRowIdOfCommitInfo);\n    return commitInfo != null ? commitInfo.getInCommitTimestamp() : Optional.empty();\n  }\n\n  /** Returns true if the current transaction implicitly/explicitly enables ICT. */\n  private static boolean didCurrentTransactionEnableICT(\n      Engine engine, Metadata currentTransactionMetadata, SnapshotImpl readSnapshot) {\n    // If ICT is currently enabled, and the read snapshot did not have ICT enabled,\n    // then the current transaction must have enabled it.\n    // In case of a conflict, any winning transaction that enabled it after\n    // our read snapshot would have caused a metadata conflict abort\n    // (see {@link ConflictChecker.handleMetadata}), so we know that\n    // all winning transactions' ICT enablement status must match the read snapshot.\n    //\n    // WARNING: To ensure that this function returns true if ICT is enabled during the first\n    // commit, we explicitly handle the case where the readSnapshot.version is -1.\n    boolean isICTCurrentlyEnabled =\n        IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(currentTransactionMetadata);\n    boolean wasICTEnabledInReadSnapshot =\n        IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(readSnapshot.getMetadata());\n    return isICTCurrentlyEnabled && !wasICTEnabledInReadSnapshot;\n  }\n\n  /**\n   * Finds the greatest lower bound of the target value in the range [lowerBoundInclusive,\n   * upperBoundInclusive] using binary search. The indexToValueMapper function is used to map the\n   * index to the corresponding value. Note that this function assumes that the values are sorted in\n   * ascending order.\n   *\n   * @param target The target value to find the greatest lower bound for.\n   * @param lowerBoundInclusive The lower bound of the search range (inclusive).\n   * @param upperBoundInclusive The upper bound of the search range (inclusive).\n   * @param indexToValueMapper A function that maps an index to its corresponding value.\n   * @return An optional which contains a tuple containing the index and the value of the greatest\n   *     lower bound when found, or an empty optional if not found.\n   */\n  public static Optional<Tuple2<Long, Long>> greatestLowerBound(\n      long target,\n      long lowerBoundInclusive,\n      long upperBoundInclusive,\n      Function<Long, Long> indexToValueMapper) {\n    if (lowerBoundInclusive > upperBoundInclusive) {\n      return Optional.empty();\n    }\n\n    long start = lowerBoundInclusive;\n    long end = upperBoundInclusive;\n    long resultIndex = -1;\n    long resultValue = 0;\n\n    while (start <= end) {\n      long mid = start + (end - start) / 2;\n      long midValue = indexToValueMapper.apply(mid);\n      if (midValue == target) {\n        return Optional.of(new Tuple2<>(mid, midValue));\n      } else if (midValue < target) {\n        resultIndex = mid;\n        resultValue = midValue;\n        start = mid + 1;\n      } else {\n        end = mid - 1;\n      }\n    }\n\n    if (resultIndex == -1) {\n      return Optional.empty();\n    } else {\n      return Optional.of(new Tuple2<>(resultIndex, resultValue));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/InternalUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.sql.Date;\nimport java.sql.Timestamp;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class InternalUtils {\n  private static final LocalDate EPOCH_DAY = LocalDate.ofEpochDay(0);\n  private static final LocalDateTime EPOCH_DATETIME =\n      LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC);\n\n  private InternalUtils() {}\n\n  /**\n   * Utility method to read at most one row from the given data {@link ColumnarBatch} iterator. If\n   * there is more than one row, an exception will be thrown.\n   *\n   * @param dataIter\n   * @return\n   */\n  public static Optional<Row> getSingularRow(CloseableIterator<ColumnarBatch> dataIter)\n      throws IOException {\n    Row row = null;\n    while (dataIter.hasNext()) {\n      try (CloseableIterator<Row> rows = dataIter.next().getRows()) {\n        while (rows.hasNext()) {\n          if (row != null) {\n            throw new IllegalArgumentException(\"Given data batch contains more than one row\");\n          }\n          row = rows.next();\n        }\n      }\n    }\n    return Optional.ofNullable(row);\n  }\n\n  /**\n   * Utility method to read at most one element from a {@link CloseableIterator}. If there is more\n   * than element row, an exception will be thrown.\n   */\n  public static <T> Optional<T> getSingularElement(CloseableIterator<T> iter) throws IOException {\n    try {\n      T result = null;\n      while (iter.hasNext()) {\n        if (result != null) {\n          throw new IllegalArgumentException(\"Iterator contains more than one element\");\n        }\n        result = iter.next();\n      }\n      return Optional.ofNullable(result);\n    } finally {\n      iter.close();\n    }\n  }\n\n  /** Utility method to get the number of days since epoch this given date is. */\n  public static int daysSinceEpoch(Date date) {\n    LocalDate localDate = date.toLocalDate();\n    return (int) localDate.toEpochDay();\n  }\n\n  /**\n   * Utility method to get the number of microseconds since the unix epoch for the given timestamp\n   * interpreted in UTC.\n   */\n  public static long microsSinceEpoch(Timestamp timestamp) {\n    LocalDateTime localTimestamp = timestamp.toLocalDateTime();\n    return TimestampUtils.toEpochMicros(localTimestamp);\n  }\n\n  /**\n   * Utility method to create a singleton string {@link ColumnVector}\n   *\n   * @param value the string element to create the vector with\n   * @return A {@link ColumnVector} with a single element {@code value}\n   */\n  public static ColumnVector singletonStringColumnVector(String value) {\n    return new ColumnVector() {\n      @Override\n      public DataType getDataType() {\n        return StringType.STRING;\n      }\n\n      @Override\n      public int getSize() {\n        return 1;\n      }\n\n      @Override\n      public void close() {}\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return value == null;\n      }\n\n      @Override\n      public String getString(int rowId) {\n        if (rowId != 0) {\n          throw new IllegalArgumentException(\"Invalid row id: \" + rowId);\n        }\n        return value;\n      }\n    };\n  }\n\n  public static Row requireNonNull(Row row, int ordinal, String columnName) {\n    if (row.isNullAt(ordinal)) {\n      throw new IllegalArgumentException(\"Expected a non-null value for column: \" + columnName);\n    }\n    return row;\n  }\n\n  public static ColumnVector requireNonNull(ColumnVector vector, int rowId, String columnName) {\n    if (vector.isNullAt(rowId)) {\n      throw new IllegalArgumentException(\"Expected a non-null value for column: \" + columnName);\n    }\n    return vector;\n  }\n\n  /**\n   * Relativize the given child path with respect to the given root URI. If the child path is\n   * already a relative path, it is returned as is.\n   *\n   * @param child\n   * @param root Root directory as URI. Relativization is done with respect to this root. The\n   *     relativize operation requires conversion to URI, so the caller is expected to convert the\n   *     root directory to URI once and use it for relativizing for multiple child paths.\n   * @return\n   */\n  public static Path relativizePath(Path child, URI root) {\n    if (child.isAbsolute()) {\n      return new Path(root.relativize(child.toUri()));\n    }\n    return child;\n  }\n\n  public static Set<String> toLowerCaseSet(Collection<String> set) {\n    return set.stream().map(String::toLowerCase).collect(Collectors.toSet());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/IntervalParserUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.util.DateTimeConstants.*;\nimport static io.delta.kernel.internal.util.IntervalParserUtils.ParseState.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.util.Locale;\n\n/**\n * Copy of `org/apache/spark/sql/catalyst/util/SparkIntervalUtils.scala` from Apache Spark. Delta\n * table properties store the interval format. We need a parser in order to parse these values in\n * Kernel.\n */\npublic class IntervalParserUtils {\n  private IntervalParserUtils() {}\n\n  /**\n   * Parse the given interval string into milliseconds. For configs accepting an interval, we\n   * require the user specified string must obey:\n   *\n   * <ul>\n   *   <li>Doesn't use months or years, since an internal like this is not deterministic.\n   *   <li>Doesn't use microseconds or nanoseconds part as it too granular to use.\n   * </ul>\n   *\n   * @return parsed interval as milliseconds.\n   */\n  public static long safeParseIntervalAsMillis(String input) {\n    checkArgument(input != null, \"interval string cannot be null\");\n    String inputInLowerCase = input.trim().toLowerCase(Locale.ROOT);\n    checkArgument(!inputInLowerCase.isEmpty(), \"interval string cannot be empty\");\n    if (!inputInLowerCase.startsWith(\"interval \")) {\n      inputInLowerCase = \"interval \" + inputInLowerCase;\n    }\n    return parseIntervalAsMicros(inputInLowerCase) / 1000; // convert to milliseconds\n  }\n\n  public static long parseIntervalAsMicros(String input) {\n    return new IntervalParser(input).toMicroSeconds();\n  }\n\n  enum ParseState {\n    PREFIX,\n    TRIM_BEFORE_SIGN,\n    SIGN,\n    TRIM_BEFORE_VALUE,\n    VALUE,\n    VALUE_FRACTIONAL_PART,\n    TRIM_BEFORE_UNIT,\n    UNIT_BEGIN,\n    UNIT_SUFFIX,\n    UNIT_END;\n  }\n\n  private static final String INTERVAL_STR = \"interval\";\n  private static final String YEAR_STR = \"year\";\n  private static final String MONTH_STR = \"month\";\n  private static final String WEEK_STR = \"week\";\n  private static final String DAY_STR = \"day\";\n  private static final String HOUR_STR = \"hour\";\n  private static final String MINUTE_STR = \"minute\";\n  private static final String SECOND_STR = \"second\";\n  private static final String MILLIS_STR = \"millisecond\";\n  private static final String MICROS_STR = \"microsecond\";\n\n  private static class IntervalParser {\n    private final String input;\n    private String s; // trimmed input in lowercase\n    private ParseState state = ParseState.PREFIX;\n    private int i = 0;\n    private long currentValue = 0;\n    private boolean isNegative = false;\n    private int days = 0;\n    private long microseconds = 0;\n    private int fractionScale = 0;\n    private int fraction = 0;\n    private boolean pointPrefixed = false;\n\n    // Expected trimmed lower case input string.\n    IntervalParser(String input) {\n      this.input = input;\n      if (input == null) {\n        throwIAE(\"interval string cannot be null\");\n      }\n      String inputInLowerCase = input.trim().toLowerCase(Locale.ROOT);\n      if (inputInLowerCase.isEmpty()) {\n        throwIAE(format(\"Error parsing '%s' to interval\", input));\n      }\n      this.s = inputInLowerCase;\n    }\n\n    long toMicroSeconds() {\n      // UTF-8 encoded bytes of the trimmed input\n      byte[] bytes = s.getBytes(UTF_8);\n      checkArgument(bytes.length > 0, \"Interval string cannot be empty\");\n\n      while (i < bytes.length) {\n        byte b = bytes[i];\n        int initialFractionScale = (int) (NANOS_PER_SECOND / 10);\n        switch (state) {\n          case PREFIX:\n            if (s.startsWith(INTERVAL_STR)) {\n              if (s.length() == INTERVAL_STR.length()) {\n                throwIAE(\"interval string cannot be empty\");\n              } else if (!Character.isWhitespace(bytes[i + INTERVAL_STR.length()])) {\n                throwIAE(\"invalid interval prefix \" + currentWord());\n              } else {\n                i += INTERVAL_STR.length();\n              }\n            }\n            state = ParseState.TRIM_BEFORE_SIGN;\n            break;\n          case TRIM_BEFORE_SIGN:\n            trimToNextState(b, SIGN);\n            break;\n          case SIGN:\n            currentValue = 0;\n            fraction = 0;\n            // We preset next state from SIGN to TRIM_BEFORE_VALUE. If we meet '.'\n            // in the SIGN state, it means that the interval value we deal with here is\n            // a numeric with only fractional part, such as '.11 second', which can be\n            // parsed to 0.11 seconds. In this case, we need to reset next state to\n            // `VALUE_FRACTIONAL_PART` to go parse the fraction part of the interval\n            // value.\n            state = TRIM_BEFORE_VALUE;\n            fractionScale = -1;\n            if (b == '-' || b == '+') {\n              i++;\n              isNegative = b == '-';\n            } else if ('0' <= b && b <= '9') {\n              isNegative = false;\n            } else if (b == '.') {\n              isNegative = false;\n              fractionScale = initialFractionScale;\n              pointPrefixed = true;\n              i++;\n              state = VALUE_FRACTIONAL_PART;\n            } else {\n              throwIAE(format(\"unrecognized number '%s'\", currentWord()));\n            }\n            break;\n          case TRIM_BEFORE_VALUE:\n            trimToNextState(b, VALUE);\n            break;\n          case VALUE:\n            if ('0' <= b && b <= '9') {\n              try {\n                currentValue = Math.addExact(Math.multiplyExact(10, currentValue), (b - '0'));\n              } catch (ArithmeticException e) {\n                throwIAE(e.getMessage(), e);\n              }\n            } else if (Character.isWhitespace(b)) {\n              state = TRIM_BEFORE_UNIT;\n            } else if (b == '.') {\n              fractionScale = initialFractionScale;\n              state = VALUE_FRACTIONAL_PART;\n            } else {\n              throwIAE(format(\"invalid value '%s'\", currentWord()));\n            }\n            i++;\n            break;\n          case VALUE_FRACTIONAL_PART:\n            if ('0' <= b && b <= '9' && fractionScale > 0) {\n              fraction += (b - '0') * fractionScale;\n              fractionScale /= 10;\n            } else if (Character.isWhitespace(b)\n                && (!pointPrefixed || fractionScale < initialFractionScale)) {\n              fraction /= ((int) NANOS_PER_MICROS);\n              state = TRIM_BEFORE_UNIT;\n            } else if ('0' <= b && b <= '9') {\n              throwIAE(\n                  format(\n                      \"interval can only support nanosecond \" + \"precision, '%s' is out of range\",\n                      currentWord()));\n            } else {\n              throwIAE(format(\"invalid value '%s'\", currentWord()));\n            }\n            i += 1;\n            break;\n          case TRIM_BEFORE_UNIT:\n            trimToNextState(b, UNIT_BEGIN);\n            break;\n          case UNIT_BEGIN:\n            // Checks that only seconds can have the fractional part\n            if (b != 's' && fractionScale >= 0) {\n              throwIAE(format(\"'%s' cannot have fractional part\", currentWord()));\n            }\n            if (isNegative) {\n              currentValue = -currentValue;\n              fraction = -fraction;\n            }\n            try {\n              if (b == 'y' && matchAt(i, YEAR_STR)) {\n                throwIAE(\"year is not supported, use days instead\");\n              } else if (b == 'w' && matchAt(i, WEEK_STR)) {\n                long daysInWeeks = Math.multiplyExact(DAYS_PER_WEEK, currentValue);\n                days = Math.toIntExact(Math.addExact(days, daysInWeeks));\n                i += WEEK_STR.length();\n              } else if (b == 'd' && matchAt(i, DAY_STR)) {\n                days = Math.addExact(days, Math.toIntExact(currentValue));\n                i += DAY_STR.length();\n              } else if (b == 'h' && matchAt(i, HOUR_STR)) {\n                long hoursUs = Math.multiplyExact(currentValue, MICROS_PER_HOUR);\n                microseconds = Math.addExact(microseconds, hoursUs);\n                i += HOUR_STR.length();\n              } else if (b == 's' && matchAt(i, SECOND_STR)) {\n                long secondsUs = Math.multiplyExact(currentValue, MICROS_PER_SECOND);\n                microseconds = Math.addExact(Math.addExact(microseconds, secondsUs), fraction);\n                i += SECOND_STR.length();\n              } else if (b == 'm') {\n                if (matchAt(i, MONTH_STR)) {\n                  throwIAE(\"month is not supported, use days instead\");\n                } else if (matchAt(i, MINUTE_STR)) {\n                  long minutesUs = Math.multiplyExact(currentValue, MICROS_PER_MINUTE);\n                  microseconds = Math.addExact(microseconds, minutesUs);\n                  i += MINUTE_STR.length();\n                } else if (matchAt(i, MILLIS_STR)) {\n                  long millisUs = Math.multiplyExact(currentValue, MICROS_PER_MILLIS);\n                  microseconds = Math.addExact(microseconds, millisUs);\n                  i += MILLIS_STR.length();\n                } else if (matchAt(i, MICROS_STR)) {\n                  microseconds = Math.addExact(microseconds, currentValue);\n                  i += MICROS_STR.length();\n                } else {\n                  throwIAE(format(\"invalid unit '%s'\", currentWord()));\n                }\n              } else {\n                throwIAE(format(\"invalid unit '%s'\", currentWord()));\n              }\n            } catch (ArithmeticException e) {\n              throwIAE(e.getMessage(), e);\n            }\n            state = UNIT_SUFFIX;\n            break;\n          case UNIT_SUFFIX:\n            if (b == 's') {\n              state = UNIT_END;\n            } else if (Character.isWhitespace(b)) {\n              state = TRIM_BEFORE_SIGN;\n            } else {\n              throwIAE(format(\"invalid unit '%s'\", currentWord()));\n            }\n            i++;\n            break;\n          case UNIT_END:\n            if (Character.isWhitespace(b)) {\n              i++;\n              state = TRIM_BEFORE_SIGN;\n            } else {\n              throwIAE(format(\"invalid unit '%s'\", currentWord()));\n            }\n            break;\n          default:\n            throwIAE(\"invalid input: \" + s);\n        }\n      }\n\n      switch (state) {\n        case UNIT_SUFFIX: // fall through\n        case UNIT_END: // fall through\n        case TRIM_BEFORE_SIGN:\n          return days * MICROS_PER_DAY + microseconds;\n        case TRIM_BEFORE_VALUE:\n          throwIAE(format(\"expect a number after '%s' but hit EOL\", currentWord()));\n          break;\n        case VALUE:\n        case VALUE_FRACTIONAL_PART:\n          throwIAE(format(\"expect a unit name after '%s' but hit EOL\", currentWord()));\n          break;\n        default:\n          throwIAE(format(\"unknown error when parsing '%s'\", currentWord()));\n      }\n\n      throwIAE(\"invalid interval\");\n      return 0; // should never reach.\n    }\n\n    private void trimToNextState(byte b, ParseState next) {\n      if (Character.isWhitespace(b)) {\n        i++;\n      } else {\n        state = next;\n      }\n    }\n\n    private String currentWord() {\n      String sep = \"\\\\s+\";\n      String[] strings = s.split(sep);\n      int lenRight = s.substring(i).split(sep).length;\n      return strings[strings.length - lenRight];\n    }\n\n    private boolean matchAt(int i, String str) {\n      if (i + str.length() > s.length()) {\n        return false;\n      }\n      return s.substring(i, i + str.length()).equals(str);\n    }\n\n    private void throwIAE(String msg, Exception e) {\n      throw new IllegalArgumentException(\n          format(\"Error parsing '%s' to interval, %s\", input, msg), e);\n    }\n\n    private void throwIAE(String msg) {\n      throw new IllegalArgumentException(format(\"Error parsing '%s' to interval, %s\", input, msg));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/JsonUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.DeltaErrors.unsupportedStatsDataType;\nimport static io.delta.kernel.statistics.DataFileStatistics.EPOCH;\nimport static io.delta.kernel.statistics.DataFileStatistics.TIMESTAMP_FORMATTER;\nimport static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.types.*;\nimport java.io.IOException;\nimport java.io.StringWriter;\nimport java.io.UncheckedIOException;\nimport java.math.BigDecimal;\nimport java.nio.charset.StandardCharsets;\nimport java.time.*;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class JsonUtils {\n  private JsonUtils() {}\n\n  private static final ObjectMapper MAPPER = new ObjectMapper();\n  private static final JsonFactory FACTORY = new JsonFactory();\n\n  public static JsonFactory factory() {\n    return FACTORY;\n  }\n\n  public static ObjectMapper mapper() {\n    return MAPPER;\n  }\n\n  @FunctionalInterface\n  public interface ToJson {\n    void generate(JsonGenerator generator) throws IOException;\n  }\n\n  @FunctionalInterface\n  public interface JsonValueWriter<T> {\n    void write(JsonGenerator generator, T value) throws IOException;\n  }\n\n  /**\n   * Utility class for writing JSON with a Jackson {@link JsonGenerator}.\n   *\n   * @param toJson function that produces JSON using a {@link JsonGenerator}\n   * @return a JSON string produced from the generator\n   */\n  public static String generate(ToJson toJson) {\n    try (StringWriter writer = new StringWriter();\n        JsonGenerator generator = factory().createGenerator(writer)) {\n      toJson.generate(generator);\n      generator.flush();\n      return writer.toString();\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  /**\n   * Parses the given JSON string into a map of key-value pairs.\n   *\n   * <p>The JSON string should be in the format:\n   *\n   * <pre>{@code {\"key1\": \"value1\", \"key2\": \"value2\", ...}}</pre>\n   *\n   * where both keys and values are strings.\n   *\n   * @param jsonString The JSON string to parse\n   * @return A map containing the key-value pairs extracted from the JSON string\n   */\n  public static Map<String, String> parseJSONKeyValueMap(String jsonString) {\n    if (jsonString == null || jsonString.trim().isEmpty()) {\n      return Collections.emptyMap();\n    }\n    try {\n      return MAPPER.readValue(jsonString, new TypeReference<Map<String, String>>() {});\n    } catch (Exception e) {\n      throw new KernelException(String.format(\"Failed to parse JSON string: %s\", jsonString), e);\n    }\n  }\n\n  /**\n   * Helper method to convert JSON node value to Literal based on the expected data type from\n   * schema. Uses the schema type information to eliminate ambiguity when parsing JSON values.\n   *\n   * @param valueNode The JSON node containing the value\n   * @param dataType The expected data type from the schema\n   * @return The corresponding Literal, or null if the value is null\n   * @throws KernelException if the JSON value cannot be parsed as the expected type\n   */\n  public static Literal parseJsonValueToLiteral(JsonNode valueNode, DataType dataType) {\n    if (valueNode == null || valueNode.isNull()) {\n      return null;\n    }\n\n    try {\n      if (dataType instanceof BooleanType) {\n        if (!valueNode.isBoolean()) {\n          throw new KernelException(\n              String.format(\"Expected boolean value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofBoolean(valueNode.asBoolean());\n\n      } else if (dataType instanceof ByteType) {\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected byte value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofByte((byte) valueNode.asInt());\n\n      } else if (dataType instanceof ShortType) {\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected short value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofShort(valueNode.shortValue());\n\n      } else if (dataType instanceof IntegerType) {\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected integer value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofInt(valueNode.asInt());\n\n      } else if (dataType instanceof LongType) {\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected long value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofLong(valueNode.asLong());\n\n      } else if (dataType instanceof FloatType) {\n        if (valueNode.isTextual()) {\n          // Special float values are stored as strings during serialization\n          String textValue = valueNode.asText();\n          switch (textValue) {\n            case \"NaN\":\n              return Literal.ofFloat(Float.NaN);\n            case \"Infinity\":\n              return Literal.ofFloat(Float.POSITIVE_INFINITY);\n            case \"-Infinity\":\n              return Literal.ofFloat(Float.NEGATIVE_INFINITY);\n            default:\n              throw new KernelException(\n                  String.format(\"Expected float value but got unexpected string: %s\", textValue));\n          }\n        }\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected float value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofFloat(valueNode.floatValue());\n\n      } else if (dataType instanceof DoubleType) {\n        if (valueNode.isTextual()) {\n          // Special double values are stored as strings during serialization\n          String textValue = valueNode.asText();\n          switch (textValue) {\n            case \"NaN\":\n              return Literal.ofDouble(Double.NaN);\n            case \"Infinity\":\n              return Literal.ofDouble(Double.POSITIVE_INFINITY);\n            case \"-Infinity\":\n              return Literal.ofDouble(Double.NEGATIVE_INFINITY);\n            default:\n              throw new KernelException(\n                  String.format(\"Expected double value but got unexpected string: %s\", textValue));\n          }\n        }\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected double value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofDouble(valueNode.asDouble());\n\n      } else if (dataType instanceof StringType) {\n        if (!valueNode.isTextual()) {\n          throw new KernelException(\n              String.format(\"Expected string value but got: %s\", valueNode.toString()));\n        }\n        return Literal.ofString(valueNode.asText());\n\n      } else if (dataType instanceof BinaryType) {\n        if (!valueNode.isTextual()) {\n          throw new KernelException(\n              String.format(\"Expected binary (as string) value but got: %s\", valueNode.toString()));\n        }\n        // Binary data was stored as UTF-8 string during serialization\n        return Literal.ofBinary(valueNode.asText().getBytes(StandardCharsets.UTF_8));\n\n      } else if (dataType instanceof DecimalType) {\n        if (!valueNode.isNumber()) {\n          throw new KernelException(\n              String.format(\"Expected decimal value but got: %s\", valueNode.toString()));\n        }\n        DecimalType decimalType = (DecimalType) dataType;\n        BigDecimal decimal = valueNode.decimalValue();\n        return Literal.ofDecimal(decimal, decimalType.getPrecision(), decimalType.getScale());\n\n      } else if (dataType instanceof DateType) {\n        if (!valueNode.isTextual()) {\n          throw new KernelException(\n              String.format(\"Expected date (as string) value but got: %s\", valueNode.toString()));\n        }\n        String textValue = valueNode.asText();\n        LocalDate date = LocalDate.parse(textValue, ISO_LOCAL_DATE);\n        return Literal.ofDate((int) date.toEpochDay());\n\n      } else if (dataType instanceof TimestampType) {\n        if (!valueNode.isTextual()) {\n          throw new KernelException(\n              String.format(\n                  \"Expected timestamp (as string) value but got: %s\", valueNode.toString()));\n        }\n        String textValue = valueNode.asText();\n        OffsetDateTime offsetDateTime = OffsetDateTime.parse(textValue, TIMESTAMP_FORMATTER);\n        return Literal.ofTimestamp(TimestampUtils.toEpochMicros(offsetDateTime));\n\n      } else if (dataType instanceof TimestampNTZType) {\n        if (!valueNode.isTextual()) {\n          throw new KernelException(\n              String.format(\n                  \"Expected timestamp NTZ (as string) value but got: %s\", valueNode.toString()));\n        }\n        String textValue = valueNode.asText();\n        LocalDateTime localDateTime =\n            LocalDateTime.parse(textValue, DateTimeFormatter.ISO_LOCAL_DATE_TIME);\n        return Literal.ofTimestampNtz(TimestampUtils.toEpochMicros(localDateTime));\n\n      } else if (dataType instanceof VariantType) {\n        if (!valueNode.isTextual()) {\n          throw new KernelException(\n              String.format(\"Expected variant as string value but got: %s\", valueNode));\n        }\n        String textValue = valueNode.asText();\n        return Literal.ofString(textValue);\n      } else {\n        throw unsupportedStatsDataType(dataType);\n      }\n    } catch (Exception e) {\n      if (e instanceof KernelException) {\n        throw (KernelException) e;\n      }\n      throw new KernelException(\n          String.format(\n              \"Failed to parse value '%s' as %s\", valueNode.toString(), dataType.toString()),\n          e);\n    }\n  }\n\n  /**\n   * Get the timestamp formatter used for parsing/formatting timestamps. Package-private for use by\n   * DataFileStatistics.\n   */\n  static DateTimeFormatter getTimestampFormatter() {\n    return TIMESTAMP_FORMATTER;\n  }\n\n  /** Get the epoch offset date time constant. Package-private for use by DataFileStatistics. */\n  static OffsetDateTime getEpoch() {\n    return EPOCH;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/ManualClock.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\n/** A clock whose time can be manually set and modified. */\npublic class ManualClock implements Clock {\n  private long timeMillis;\n\n  public ManualClock(long timeMillis) {\n    this.timeMillis = timeMillis;\n  }\n\n  /** @param timeToSet new time (in milliseconds) that the clock should represent */\n  public synchronized void setTime(long timeToSet) {\n    this.timeMillis = timeToSet;\n    this.notifyAll();\n  }\n\n  @Override\n  public long getTimeMillis() {\n    return timeMillis;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/PartitionUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.expressions.AlwaysFalse.ALWAYS_FALSE;\nimport static io.delta.kernel.expressions.AlwaysTrue.ALWAYS_TRUE;\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineException;\nimport static io.delta.kernel.internal.util.ExpressionUtils.createPredicate;\nimport static io.delta.kernel.internal.util.InternalUtils.toLowerCaseSet;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.SchemaUtils.casePreservingPartitionColNames;\nimport static java.util.Arrays.asList;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.engine.ExpressionHandler;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.internal.DeltaErrorsInternal;\nimport io.delta.kernel.internal.InternalScanFileUtils;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.nio.charset.StandardCharsets;\nimport java.sql.Date;\nimport java.sql.Timestamp;\nimport java.time.*;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class PartitionUtils {\n  private static final DateTimeFormatter PARTITION_TIMESTAMP_FORMATTER =\n      DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSSSSS\");\n\n  private PartitionUtils() {}\n\n  /**\n   * Utility method to attach partition columns to the given data batch.\n   *\n   * @param dataBatch Data batch to which the partition columns will be added.\n   * @param logicalReadSchema Logical schema of the table scan. Used to insert partition columns at\n   *     the right positions in the data batch. This logical schema must contain column mapping\n   *     metadata if column mapping is enabled.\n   * @param partitionValues Map of partition column name to value.\n   * @param expressionHandler Expression handler used to evaluate the partition values.\n   * @return A new {@link ColumnarBatch} with the partition columns added.\n   */\n  public static ColumnarBatch withPartitionColumns(\n      ColumnarBatch dataBatch,\n      StructType logicalReadSchema,\n      Map<String, String> partitionValues,\n      ExpressionHandler expressionHandler) {\n    if (partitionValues == null || partitionValues.isEmpty()) {\n      // No partition column vectors to attach to the data batch\n      return dataBatch;\n    }\n\n    // We verify that the number of partition columns in the logical schema plus the number of\n    // columns in the data batch schema is equal to the length of the logical schema.\n    // `partitionValues` contains all partition columns of the table (not just the requested ones),\n    // so we first need to count the number of partition columns in the logical schema.\n    int numPartitionColumnsInSchema =\n        (int)\n            logicalReadSchema.fields().stream()\n                .map(ColumnMapping::getPhysicalName)\n                .filter(partitionValues::containsKey)\n                .count();\n    if (numPartitionColumnsInSchema + dataBatch.getSchema().length()\n        != logicalReadSchema.length()) {\n      throw DeltaErrorsInternal.logicalPhysicalSchemaMismatch(\n          numPartitionColumnsInSchema, dataBatch.getSchema().length(), logicalReadSchema.length());\n    }\n\n    for (int colIdx = 0; colIdx < logicalReadSchema.length(); colIdx++) {\n      // We must iterate the logical schema in order since we insert partition columns into the data\n      // batch according to their ordinal in the logical schema.\n      StructField structField = logicalReadSchema.at(colIdx);\n      String physicalName = ColumnMapping.getPhysicalName(structField);\n      if (partitionValues.containsKey(physicalName)) {\n        // Create a partition column vector\n        final ColumnarBatch finalDataBatch = dataBatch;\n        Literal partitionValue =\n            literalForPartitionValue(structField.getDataType(), partitionValues.get(physicalName));\n        ExpressionEvaluator evaluator =\n            wrapEngineException(\n                () ->\n                    expressionHandler.getEvaluator(\n                        finalDataBatch.getSchema(), partitionValue, structField.getDataType()),\n                \"Get the expression evaluator for partition column %s with type=%s and value=%s\",\n                physicalName,\n                structField.getDataType(),\n                partitionValues.get(physicalName));\n\n        ColumnVector partitionVector =\n            wrapEngineException(\n                () -> evaluator.eval(finalDataBatch),\n                \"Evaluating the partition value expression %s\",\n                partitionValue);\n        dataBatch = dataBatch.withNewColumn(colIdx, structField, partitionVector);\n      }\n    }\n\n    return dataBatch;\n  }\n\n  /**\n   * Convert the given partition values to a {@link MapValue} that can be serialized to a Delta\n   * commit file.\n   *\n   * @param partitionValueMap Expected the partition column names to be same case as in the schema.\n   *     We want to preserve the case of the partition column names when serializing to the Delta\n   *     commit file.\n   * @return {@link MapValue} representing the serialized partition values that can be written to a\n   *     Delta commit file.\n   */\n  public static MapValue serializePartitionMap(Map<String, Literal> partitionValueMap) {\n    if (partitionValueMap == null || partitionValueMap.isEmpty()) {\n      return VectorUtils.stringStringMapValue(Collections.emptyMap());\n    }\n\n    Map<String, String> serializedPartValues = new HashMap<>();\n    for (Map.Entry<String, Literal> entry : partitionValueMap.entrySet()) {\n      serializedPartValues.put(\n          entry.getKey(), // partition column name\n          serializePartitionValue(entry.getValue())); // serialized partition value as str\n    }\n\n    return VectorUtils.stringStringMapValue(serializedPartValues);\n  }\n\n  /**\n   * Validate {@code partitionValues} contains values for every partition column in the table and\n   * the type of the value is correct. Once validated the partition values are sanitized to match\n   * the case of the partition column names in the table schema and returned\n   *\n   * @param tableSchema Schema of the table.\n   * @param partitionColNames Partition column name. These should be from the table metadata that\n   *     retain the same case as in the table schema.\n   * @param partitionValues Map of partition column to value map given by the connector\n   * @return Sanitized partition values.\n   */\n  public static Map<String, Literal> validateAndSanitizePartitionValues(\n      StructType tableSchema,\n      List<String> partitionColNames,\n      Map<String, Literal> partitionValues) {\n\n    if (!toLowerCaseSet(partitionColNames).equals(toLowerCaseSet(partitionValues.keySet()))) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"Partition values provided are not matching the partition columns. \"\n                  + \"Partition columns: %s, Partition values: %s\",\n              partitionColNames, partitionValues));\n    }\n\n    // Convert the partition column names in given `partitionValues` to schema case. Schema\n    // case is the exact case the column name was given by the connector when creating the\n    // table. Comparing the column names is case-insensitive, but preserve the case as stored\n    // in the table metadata when writing the partition column name to DeltaLog\n    // (`partitionValues` in `AddFile`) or generating the target directory for writing the\n    // data belonging to a partition.\n    Map<String, Literal> schemaCasePartitionValues =\n        casePreservingPartitionColNames(partitionColNames, partitionValues);\n\n    // validate types are the same\n    schemaCasePartitionValues\n        .entrySet()\n        .forEach(\n            entry -> {\n              String partColName = entry.getKey();\n              Literal partValue = entry.getValue();\n              StructField partColField = tableSchema.get(partColName);\n\n              // this shouldn't happen as we have already validated the partition column names\n              checkArgument(\n                  partColField != null,\n                  \"Partition column %s is not present in the table schema\",\n                  partColName);\n              DataType partColType = partColField.getDataType();\n\n              if (!partColType.isWriteCompatible(partValue.getDataType())) {\n                throw new IllegalArgumentException(\n                    String.format(\n                        \"Partition column %s is of type %s but the value provided is of type %s\",\n                        partColName, partColType, partValue.getDataType()));\n              }\n            });\n\n    return schemaCasePartitionValues;\n  }\n\n  /**\n   * Validate that the given predicate references only (and at least one) partition columns.\n   *\n   * @throws IllegalArgumentException if the predicate does not reference any partition columns or\n   *     if it references any data columns\n   */\n  public static void validatePredicateOnlyOnPartitionColumns(\n      Predicate predicate, Set<String> partitionColNames) {\n    final Tuple2<Predicate, Predicate> metadataAndDataPredicates =\n        splitMetadataAndDataPredicates(predicate, partitionColNames);\n    final Predicate metadataPredicate = metadataAndDataPredicates._1;\n    final Predicate dataPredicate = metadataAndDataPredicates._2;\n\n    if (metadataPredicate == AlwaysTrue.ALWAYS_TRUE) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"Partition predicate must contain at least one partition column: %s\", predicate));\n    }\n\n    if (dataPredicate != AlwaysTrue.ALWAYS_TRUE) {\n      throw new IllegalArgumentException(\n          String.format(\"Partition predicate must contain only partition columns: %s\", predicate));\n    }\n  }\n\n  /**\n   * Split the given predicate into predicate on partition columns and predicate on data columns.\n   *\n   * @param predicate\n   * @param partitionColNames\n   * @return Tuple of partition column predicate and data column predicate.\n   */\n  public static Tuple2<Predicate, Predicate> splitMetadataAndDataPredicates(\n      Predicate predicate, Set<String> partitionColNames) {\n    String predicateName = predicate.getName();\n    List<Expression> children = predicate.getChildren();\n    if (\"AND\".equalsIgnoreCase(predicateName)) {\n      Predicate left = (Predicate) children.get(0);\n      Predicate right = (Predicate) children.get(1);\n      Tuple2<Predicate, Predicate> leftResult =\n          splitMetadataAndDataPredicates(left, partitionColNames);\n      Tuple2<Predicate, Predicate> rightResult =\n          splitMetadataAndDataPredicates(right, partitionColNames);\n\n      return new Tuple2<>(\n          combineWithAndOp(leftResult._1, rightResult._1),\n          combineWithAndOp(leftResult._2, rightResult._2));\n    }\n    if (hasNonPartitionColumns(children, partitionColNames)) {\n      return new Tuple2<>(ALWAYS_TRUE, predicate);\n    } else {\n      return new Tuple2<>(predicate, ALWAYS_TRUE);\n    }\n  }\n\n  /**\n   * Rewrite the given predicate on partition columns on `partitionValues_parsed` in checkpoint\n   * schema. The rewritten predicate can be pushed to the Parquet reader when reading the checkpoint\n   * files.\n   *\n   * @param predicate Predicate on partition columns.\n   * @param partitionColNameToField Map of partition column name (in lower case) to its {@link\n   *     StructField}.\n   * @return Rewritten {@link Predicate} on `partitionValues_parsed` in `add`.\n   */\n  public static Predicate rewritePartitionPredicateOnCheckpointFileSchema(\n      Predicate predicate, Map<String, StructField> partitionColNameToField) {\n    return createPredicate(\n        predicate.getName(),\n        predicate.getChildren().stream()\n            .map(child -> rewriteColRefOnPartitionValuesParsed(child, partitionColNameToField))\n            .collect(Collectors.toList()),\n        predicate.getCollationIdentifier());\n  }\n\n  private static Expression rewriteColRefOnPartitionValuesParsed(\n      Expression expression, Map<String, StructField> partitionColMetadata) {\n    if (expression instanceof Column) {\n      Column column = (Column) expression;\n      String partColName = column.getNames()[0];\n      StructField partColField = partitionColMetadata.get(partColName.toLowerCase(Locale.ROOT));\n      if (partColField == null) {\n        throw new IllegalArgumentException(partColName + \" is not present in metadata\");\n      }\n\n      String partColPhysicalName = ColumnMapping.getPhysicalName(partColField);\n\n      return InternalScanFileUtils.getPartitionValuesParsedRefInAddFile(partColPhysicalName);\n    } else if (expression instanceof Predicate) {\n      return rewritePartitionPredicateOnCheckpointFileSchema(\n          (Predicate) expression, partitionColMetadata);\n    }\n\n    return expression;\n  }\n\n  /**\n   * Utility method to rewrite the partition predicate referring to the table schema as predicate\n   * referring to the {@code partitionValues} in scan files read from Delta log. The scan file batch\n   * is returned by the {@link io.delta.kernel.Scan#getScanFiles(Engine)}.\n   *\n   * <p>E.g. given predicate on partition columns: {@code p1 = 'new york' && p2 >= 26} where p1 is\n   * of type string and p2 is of int Rewritten expression looks like: {@code\n   * element_at(Column('add', 'partitionValues'), 'p1') = 'new york' &&\n   * partition_value(element_at(Column('add', 'partitionValues'), 'p2'), 'integer') >= 26}\n   *\n   * <p>The column `add.partitionValues` is a {@literal map(string -> string)} type. Each partition\n   * values is in string serialization format according to the Delta protocol. Expression\n   * `partition_value` deserializes the string value into the given partition column type value.\n   * String type partition values don't need any deserialization.\n   *\n   * @param predicate Predicate containing filters only on partition columns.\n   * @param partitionColMetadata Map of partition column name (in lower case) to its type.\n   * @return\n   */\n  public static Predicate rewritePartitionPredicateOnScanFileSchema(\n      Predicate predicate, Map<String, StructField> partitionColMetadata) {\n    return createPredicate(\n        predicate.getName(),\n        predicate.getChildren().stream()\n            .map(child -> rewritePartitionColumnRef(child, partitionColMetadata))\n            .collect(Collectors.toList()),\n        predicate.getCollationIdentifier());\n  }\n\n  private static Expression rewritePartitionColumnRef(\n      Expression expression, Map<String, StructField> partitionColMetadata) {\n    Column scanFilePartitionValuesRef = InternalScanFileUtils.ADD_FILE_PARTITION_COL_REF;\n    if (expression instanceof Column) {\n      Column column = (Column) expression;\n      String partColName = column.getNames()[0];\n      StructField partColField = partitionColMetadata.get(partColName.toLowerCase(Locale.ROOT));\n      if (partColField == null) {\n        throw new IllegalArgumentException(partColName + \" is not present in metadata\");\n      }\n      DataType partColType = partColField.getDataType();\n      String partColPhysicalName = ColumnMapping.getPhysicalName(partColField);\n\n      Expression elementAt =\n          new ScalarExpression(\n              \"element_at\",\n              asList(scanFilePartitionValuesRef, Literal.ofString(partColPhysicalName)));\n\n      if (partColType instanceof StringType) {\n        return elementAt;\n      }\n\n      // Add expression to decode the partition value based on the partition column type.\n      return new PartitionValueExpression(elementAt, partColType);\n    } else if (expression instanceof Predicate) {\n      return rewritePartitionPredicateOnScanFileSchema(\n          (Predicate) expression, partitionColMetadata);\n    }\n\n    return expression;\n  }\n\n  /**\n   * Get the target directory for writing data for given partition values. Example: Given partition\n   * values (part1=1, part2='abc'), the target directory will be for a table rooted at\n   * 's3://bucket/table': 's3://bucket/table/part1=1/part2=abc'.\n   *\n   * @param dataRoot Root directory where the data is stored.\n   * @param partitionColNames Partition column names. We need this to create the target directory\n   *     structure that is consistent levels of directories.\n   * @param partitionValues Partition values to create the target directory.\n   * @return Target directory path.\n   */\n  public static String getTargetDirectory(\n      String dataRoot, List<String> partitionColNames, Map<String, Literal> partitionValues) {\n    Path targetDirectory = new Path(dataRoot);\n    for (String partitionColName : partitionColNames) {\n      Literal partitionValue = partitionValues.get(partitionColName);\n      checkArgument(\n          partitionValue != null,\n          \"Partition column value is missing for column: %s\",\n          partitionColName);\n      String serializedValue = serializePartitionValue(partitionValue);\n      if (serializedValue == null) {\n        // Follow the delta-spark behavior to use \"__HIVE_DEFAULT_PARTITION__\" for null\n        serializedValue = \"__HIVE_DEFAULT_PARTITION__\";\n      } else {\n        serializedValue = escapePartitionValue(serializedValue);\n      }\n      String partitionDirectory = partitionColName + \"=\" + serializedValue;\n      targetDirectory = new Path(targetDirectory, partitionDirectory);\n    }\n\n    return targetDirectory.toString();\n  }\n\n  private static boolean hasNonPartitionColumns(\n      List<Expression> children, Set<String> partitionColNames) {\n    for (Expression child : children) {\n      if (child instanceof Column) {\n        String[] names = ((Column) child).getNames();\n        // Partition columns are never of nested types.\n        if (names.length != 1 || !partitionColNames.contains(names[0].toLowerCase(Locale.ROOT))) {\n          return true;\n        }\n      } else {\n        if (hasNonPartitionColumns(child.getChildren(), partitionColNames)) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  private static Predicate combineWithAndOp(Predicate left, Predicate right) {\n    String leftName = left.getName().toUpperCase();\n    String rightName = right.getName().toUpperCase();\n    if (leftName.equals(\"ALWAYS_FALSE\") || rightName.equals(\"ALWAYS_FALSE\")) {\n      return ALWAYS_FALSE;\n    }\n    if (leftName.equals(\"ALWAYS_TRUE\")) {\n      return right;\n    }\n    if (rightName.equals(\"ALWAYS_TRUE\")) {\n      return left;\n    }\n    return new And(left, right);\n  }\n\n  /**\n   * Try parsing the standard formatted timestamp (e.g. 2024-03-11 11:00:00.123456). Return the\n   * number of microseconds since epoch.\n   */\n  private static Optional<Long> tryParseStandardTimestamp(String value) {\n    try {\n      Timestamp ts = Timestamp.valueOf(value);\n      return Optional.of(InternalUtils.microsSinceEpoch(ts));\n    } catch (IllegalArgumentException e) {\n      return Optional.empty();\n    }\n  }\n\n  /**\n   * Try parsing the ISO8601 formatted timestamp (e.g. 1970-01-01T00:00:00.123456Z). Return the\n   * number of microseconds since epoch.\n   */\n  private static Optional<Long> tryParseIsoTimestamp(String value) {\n    try {\n      Instant instant = Instant.parse(value);\n      long micros = instant.getEpochSecond() * 1_000_000L + instant.getNano() / 1000L;\n      return Optional.of(micros);\n    } catch (DateTimeParseException e) {\n      return Optional.empty();\n    }\n  }\n\n  /**\n   * Try parsing the timestamp, could be in the standard format or ISO8601 format. Return the\n   * Literal Object.\n   */\n  public static long tryParseTimestamp(String partitionValue) {\n    // ISO8601 format contains 'T' separator, standard format uses space\n    Optional<Long> micros =\n        partitionValue.contains(\"T\")\n            ? tryParseIsoTimestamp(partitionValue)\n            : tryParseStandardTimestamp(partitionValue);\n\n    // If the first attempt failed, try the other format as fallback (this really shouldn't happen)\n    if (!micros.isPresent()) {\n      micros =\n          partitionValue.contains(\"T\")\n              ? tryParseStandardTimestamp(partitionValue)\n              : tryParseIsoTimestamp(partitionValue);\n    }\n\n    return micros.orElseThrow(\n        () -> DeltaErrorsInternal.invalidTimestampFormatForPartitionValue(partitionValue));\n  }\n\n  protected static Literal literalForPartitionValue(DataType dataType, String partitionValue) {\n    if (partitionValue == null) {\n      return Literal.ofNull(dataType);\n    }\n\n    if (dataType instanceof BooleanType) {\n      return Literal.ofBoolean(Boolean.parseBoolean(partitionValue));\n    }\n    if (dataType instanceof ByteType) {\n      return Literal.ofByte(Byte.parseByte(partitionValue));\n    }\n    if (dataType instanceof ShortType) {\n      return Literal.ofShort(Short.parseShort(partitionValue));\n    }\n    if (dataType instanceof IntegerType) {\n      return Literal.ofInt(Integer.parseInt(partitionValue));\n    }\n    if (dataType instanceof LongType) {\n      return Literal.ofLong(Long.parseLong(partitionValue));\n    }\n    if (dataType instanceof FloatType) {\n      return Literal.ofFloat(Float.parseFloat(partitionValue));\n    }\n    if (dataType instanceof DoubleType) {\n      return Literal.ofDouble(Double.parseDouble(partitionValue));\n    }\n    if (dataType instanceof StringType) {\n      return Literal.ofString(partitionValue);\n    }\n    if (dataType instanceof BinaryType) {\n      return Literal.ofBinary(partitionValue.getBytes());\n    }\n    if (dataType instanceof DateType) {\n      return Literal.ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(partitionValue)));\n    }\n    if (dataType instanceof DecimalType) {\n      DecimalType decimalType = (DecimalType) dataType;\n      return Literal.ofDecimal(\n          new BigDecimal(partitionValue), decimalType.getPrecision(), decimalType.getScale());\n    }\n    if (dataType instanceof TimestampType) {\n      return Literal.ofTimestamp(tryParseTimestamp(partitionValue));\n    }\n    if (dataType instanceof TimestampNTZType) {\n      // Both the timestamp and timestamp_ntz have no timezone info, so they are interpreted\n      // in local time zone.\n      return Literal.ofTimestampNtz(\n          InternalUtils.microsSinceEpoch(Timestamp.valueOf(partitionValue)));\n    }\n\n    throw new UnsupportedOperationException(\"Unsupported partition column: \" + dataType);\n  }\n\n  /**\n   * Serialize the given partition value to a string according to the Delta protocol <a\n   * href=\"https://github.com/delta-io/delta/blob/master/PROTOCOL.md\n   * #partition-value-serialization\">partition value serialization rules</a>.\n   *\n   * @param literal Literal representing the partition value of specific datatype.\n   * @return Serialized string representation of the partition value.\n   */\n  protected static String serializePartitionValue(Literal literal) {\n    Object value = literal.getValue();\n    if (value == null) {\n      return null;\n    }\n    DataType dataType = literal.getDataType();\n    if (dataType instanceof ByteType\n        || dataType instanceof ShortType\n        || dataType instanceof IntegerType\n        || dataType instanceof LongType\n        || dataType instanceof FloatType\n        || dataType instanceof DoubleType\n        || dataType instanceof BooleanType) {\n      return String.valueOf(value);\n    } else if (dataType instanceof StringType) {\n      return (String) value;\n    } else if (dataType instanceof DateType) {\n      int daysSinceEpochUTC = (int) value;\n      return LocalDate.ofEpochDay(daysSinceEpochUTC).toString();\n    } else if (dataType instanceof TimestampType || dataType instanceof TimestampNTZType) {\n      long microsSinceEpochUTC = (long) value;\n      long seconds = microsSinceEpochUTC / 1_000_000;\n      int microsOfSecond = (int) (microsSinceEpochUTC % 1_000_000);\n      if (microsOfSecond < 0) {\n        // also adjust for negative microsSinceEpochUTC\n        microsOfSecond = 1_000_000 + microsOfSecond;\n      }\n      int nanosOfSecond = microsOfSecond * 1_000;\n      LocalDateTime localDateTime =\n          LocalDateTime.ofEpochSecond(seconds, nanosOfSecond, ZoneOffset.UTC);\n      return localDateTime.format(PARTITION_TIMESTAMP_FORMATTER);\n    } else if (dataType instanceof DecimalType) {\n      return ((BigDecimal) value).toString();\n    } else if (dataType instanceof BinaryType) {\n      return new String((byte[]) value, StandardCharsets.UTF_8);\n    }\n    throw new UnsupportedOperationException(\"Unsupported partition column type: \" + dataType);\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////////////////////\n  // The following string escaping code is mainly copied from Spark                             //\n  // (org.apache.spark.sql.catalyst.catalog.ExternalCatalogUtils) which is copied from          //\n  // Hive (o.a.h.h.common.FileUtils).                                                           //\n  ////////////////////////////////////////////////////////////////////////////////////////////////\n  private static final BitSet CHARS_TO_ESCAPE = new BitSet(128);\n\n  static {\n    // ASCII 01-1F are HTTP control characters that need to be escaped.\n    char[] controlChars =\n        new char[] {\n          '\\u0001', '\\u0002', '\\u0003', '\\u0004', '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', '\\n',\n          '\\u000B', '\\f', '\\r', '\\u000E', '\\u000F', '\\u0010', '\\u0011', '\\u0012', '\\u0013',\n          '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', '\\u0019', '\\u001A', '\\u001B', '\\u001C',\n          '\\u001D', '\\u001E', '\\u001F', '\"', '#', '%', '\\'', '*', '/', ':', '=', '?', '\\\\',\n          '\\u007F', '{', '[', ']', '^'\n        };\n\n    for (char c : controlChars) {\n      CHARS_TO_ESCAPE.set(c);\n    }\n  }\n\n  /**\n   * Escapes the given string to be used as a partition value in the path. Basically this escapes\n   *\n   * <ul>\n   *   <li>characters that can't be in a file path. E.g. `a\\nb` will be escaped to `a%0Ab`.\n   *   <li>character that are cause ambiguity in partition value parsing. E.g. For partition column\n   *       `a` having value `b=c`, the path should be `a=b%3Dc`\n   * </ul>\n   *\n   * @param value The partition value to escape.\n   * @return The escaped partition value.\n   */\n  private static String escapePartitionValue(String value) {\n    StringBuilder escaped = new StringBuilder(value.length());\n    for (int i = 0; i < value.length(); i++) {\n      char c = value.charAt(i);\n      if (c >= 0 && c < CHARS_TO_ESCAPE.size() && CHARS_TO_ESCAPE.get(c)) {\n        escaped.append('%');\n        escaped.append(String.format(\"%02X\", (int) c));\n      } else {\n        escaped.append(c);\n      }\n    }\n    return escaped.toString();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/Preconditions.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport java.util.function.Supplier;\n\n/**\n * Static convenience methods that help a method or constructor check whether it was invoked\n * correctly (that is, whether its preconditions were met).\n */\npublic class Preconditions {\n  private Preconditions() {}\n\n  /**\n   * Precondition-style validation that throws {@link IllegalArgumentException}.\n   *\n   * @param isValid {@code true} if valid, {@code false} if an exception should be thrown\n   * @throws IllegalArgumentException if {@code isValid} is false\n   */\n  public static void checkArgument(boolean isValid) throws IllegalArgumentException {\n    if (!isValid) {\n      throw new IllegalArgumentException();\n    }\n  }\n\n  /**\n   * Precondition-style validation that throws {@link IllegalArgumentException}.\n   *\n   * @param isValid {@code true} if valid, {@code false} if an exception should be thrown\n   * @param message A String message for the exception.\n   * @throws IllegalArgumentException if {@code isValid} is false\n   */\n  public static void checkArgument(boolean isValid, String message)\n      throws IllegalArgumentException {\n    if (!isValid) {\n      throw new IllegalArgumentException(message);\n    }\n  }\n\n  /**\n   * Precondition-style validation that throws {@link IllegalArgumentException}. The message is only\n   * evaluated if the validation fails.\n   *\n   * @param isValid {@code true} if valid, {@code false} if an exception should be thrown\n   * @param messageSupplier A supplier that provides the exception message (evaluated lazily)\n   * @throws IllegalArgumentException if {@code isValid} is false\n   */\n  public static void checkArgument(boolean isValid, Supplier<String> messageSupplier)\n      throws IllegalArgumentException {\n    if (!isValid) {\n      throw new IllegalArgumentException(messageSupplier.get());\n    }\n  }\n\n  /**\n   * Precondition-style validation that throws {@link IllegalArgumentException}.\n   *\n   * @param isValid {@code true} if valid, {@code false} if an exception should be thrown\n   * @param message A String message for the exception.\n   * @param args Objects used to fill in {@code %s} placeholders in the message\n   * @throws IllegalArgumentException if {@code isValid} is false\n   */\n  public static void checkArgument(boolean isValid, String message, Object... args)\n      throws IllegalArgumentException {\n    if (!isValid) {\n      throw new IllegalArgumentException(String.format(String.valueOf(message), args));\n    }\n  }\n\n  /**\n   * Ensures the truth of an expression involving the state of the calling instance.\n   *\n   * @param expression a boolean expression\n   * @param errorMessage the exception message to use if the check fails\n   * @throws IllegalStateException if {@code expression} is false\n   */\n  public static void checkState(boolean expression, String errorMessage) {\n    if (!expression) {\n      throw new IllegalStateException(errorMessage);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/SchemaChanges.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * SchemaChanges encapsulates a list of added, removed, renamed, or updated fields in a schema\n * change. Updates include renamed fields, reordered fields, type changes, nullability changes, and\n * metadata attribute changes. This set of updates can apply to nested fields within structs. In\n * case any update is applied to a nested field, an update will be produced for every level of\n * nesting. This includes re-ordered columns in a nested field. Note that SchemaChanges does not\n * capture re-ordered columns in top level schema.\n *\n * <p>For example, given a field struct_col: struct<inner_struct<id: int>> if id is renamed to\n * `renamed_id` 1 update will be produced for the change to struct_col and 1 update will be produced\n * for the change to inner_struct\n *\n * <p>ToDo: Possibly track moves/renames independently, enable capturing re-ordered columns in top\n * level schema\n */\nclass SchemaChanges {\n  public static class SchemaUpdate {\n    private final StructField fieldBefore;\n    private final StructField fieldAfter;\n    // This is a \".\" concatenated path to the field. Names containing \".\" are wrapped in\n    // back-ticks (`).\n    // For example in the schema <a.b : array<StructType<c : Int>>> the path to \"c\" would be:\n    // \"`a.b`.element.c\". In general, though the format should not be relid upon since this\n    // is used for surfacing errors to users.\n    // Note this is a by name. If we want to be able to track changes\n    // at the where an element is moved to a different location in the\n    // schema we need to add more paths here.\n    private final String pathToAfterField;\n\n    SchemaUpdate(StructField fieldBefore, StructField fieldAfter, String pathToAfterField) {\n      this.fieldBefore = fieldBefore;\n      this.fieldAfter = fieldAfter;\n      this.pathToAfterField = pathToAfterField;\n    }\n\n    public StructField before() {\n      return fieldBefore;\n    }\n\n    public StructField after() {\n      return fieldAfter;\n    }\n\n    public String getPathToAfterField() {\n      return pathToAfterField;\n    }\n  }\n\n  private List<StructField> addedFields;\n  private List<StructField> removedFields;\n  private List<SchemaUpdate> updatedFields;\n  private Optional<StructType> updatedSchema;\n\n  private SchemaChanges(\n      List<StructField> addedFields,\n      List<StructField> removedFields,\n      List<SchemaUpdate> updatedFields,\n      Optional<StructType> updatedSchema) {\n    this.addedFields = Collections.unmodifiableList(addedFields);\n    this.removedFields = Collections.unmodifiableList(removedFields);\n    this.updatedFields = Collections.unmodifiableList(updatedFields);\n    this.updatedSchema = updatedSchema;\n  }\n\n  static class Builder {\n    private List<StructField> addedFields = new ArrayList<>();\n    private List<StructField> removedFields = new ArrayList<>();\n    private List<SchemaUpdate> updatedFields = new ArrayList<>();\n    private Optional<StructType> updatedSchema = Optional.empty();\n\n    public Builder withAddedField(StructField addedField) {\n      addedFields.add(addedField);\n      return this;\n    }\n\n    public Builder withRemovedField(StructField removedField) {\n      removedFields.add(removedField);\n      return this;\n    }\n\n    public Builder withUpdatedField(\n        StructField existingField, StructField newField, String pathToAfterField) {\n      updatedFields.add(new SchemaUpdate(existingField, newField, pathToAfterField));\n      return this;\n    }\n\n    public Builder withUpdatedSchema(StructType updatedSchema) {\n      this.updatedSchema = Optional.of(updatedSchema);\n      return this;\n    }\n\n    public SchemaChanges build() {\n      return new SchemaChanges(addedFields, removedFields, updatedFields, updatedSchema);\n    }\n  }\n\n  public static Builder builder() {\n    return new Builder();\n  }\n\n  /* Added Fields */\n  public List<StructField> addedFields() {\n    return addedFields;\n  }\n\n  /* Removed Fields */\n  public List<StructField> removedFields() {\n    return removedFields;\n  }\n\n  /* Updated Fields (e.g. rename, type change) represented */\n  public List<SchemaUpdate> updatedFields() {\n    return updatedFields;\n  }\n\n  public Optional<StructType> updatedSchema() {\n    return updatedSchema;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/SchemaIterable.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\n/**\n * Utility class for iterating over schema structures and modifying them.\n *\n * <p>Sample usage for iterating schemas:\n *\n * <pre>{@code\n * StructType schema = ...;\n * for (SchemaIterable.SchemaElement element : new SchemaIterable(schema)) {\n *    StructField field = element.getField();\n *    // Get info from field in some way.\n * }\n * }</pre>\n *\n * <p>Sample usage for mutating schemas:\n *\n * <pre>{@code\n * StructType schema = ...;\n * SchemaIterable schemaIterable = new SchemaIterable(schema);\n * Iterator<SchemaIterable.MutableSchemaElement> iterator = schemaIterable.newMutableIterator();\n * while (iterator.hasNext()) {\n *    SchemaIterable.MutableSchemaElement element = iterator.next();\n *    // Calculate a new field in some way then call updateField.\n *    element.updateField(...)\n * }\n * updatedSchema = schemaIterable.getSchema();\n * }</pre>\n */\npublic class SchemaIterable implements Iterable<SchemaIterable.SchemaElement> {\n  private final Class<?>[] typesToSkipRecursion;\n  private StructType schema;\n\n  /** Construct a new Iterable for the schema. */\n  public SchemaIterable(StructType schema) {\n    this(schema, new Class[0]);\n  }\n\n  /**\n   * Constructs a new iterable that skips recursion for some data types.\n   *\n   * <p>No recursion will be done on any type that is an instance of a class in\n   * typesToSkipRecursion.\n   *\n   * <p>For example with schema:\n   *\n   * <pre>{@code\n   * a: Map<String, Int>\n   * b: Array<Int>\n   * }</pre>\n   *\n   * <p>If {@code typesToSkipRecursion = {MapType.class}} is provided then the iterator will only\n   * visit \"a\", \"b.element\", and \"b\".\n   *\n   * <p>If {@code typesToSkipRecursion = {ArrayType.class}} is provided then the iterator wil only\n   * visit \"a.key\", \"a.value\", \"a\" and \"b\".\n   */\n  public static SchemaIterable newSchemaIterableWithIgnoredRecursion(\n      StructType schema, Class<?>[] typesToSkipRecursion) {\n    return new SchemaIterable(schema, typesToSkipRecursion);\n  }\n\n  private SchemaIterable(StructType schema, Class<?>[] typesToSkipRecursion) {\n    this.schema = schema;\n    this.typesToSkipRecursion = Arrays.copyOf(typesToSkipRecursion, typesToSkipRecursion.length);\n  }\n\n  /**\n   * Gets the latest schema (either the initial schema or the one set after a mutable iterator is\n   * fully consumed).\n   */\n  public StructType getSchema() {\n    return schema;\n  }\n\n  @Override\n  public Iterator<SchemaElement> iterator() {\n    Iterator<MutableSchemaElement> iterator = newMutableIterator();\n    return new Iterator<SchemaElement>() {\n\n      @Override\n      public boolean hasNext() {\n        return iterator.hasNext();\n      }\n\n      @Override\n      public SchemaElement next() {\n        return iterator.next();\n      }\n    };\n  }\n\n  public Stream<SchemaElement> stream() {\n    return StreamSupport.stream(spliterator(), /* parallel = */ false);\n  }\n\n  public Stream<MutableSchemaElement> mutableStream() {\n    return StreamSupport.stream(\n        Spliterators.spliteratorUnknownSize(newMutableIterator(), /*characteristics = */ 0),\n        /* parallel = */ false);\n  }\n\n  /**\n   * Returns a new iterator that can be used to iterate and update elements in the schema. The\n   * current schema on this iterable is updated once the returned iterator is fully consumed.\n   *\n   * <p>Consuming multiple iterators across different threads concurrently is not thread safe.\n   *\n   * <p>Example usage:\n   *\n   * <pre>{@code\n   * Iterator<SchemaIterable.MutableSchemaElement> iterator = schemaIterable.newMutableIterator();\n   * while (iterator.hasNext()) {\n   *    SchemaIterable.MutableSchemaElement element = iterator.next();\n   *    element.updateField(...)\n   * }\n   * updatedSchema = schemaIterable.getSchema();\n   * }</pre>\n   */\n  public Iterator<MutableSchemaElement> newMutableIterator() {\n    return new SchemaIterator(schema, this::setSchema, typesToSkipRecursion);\n  }\n\n  private void setSchema(StructType newSchema) {\n    this.schema = newSchema;\n  }\n\n  /**\n   * Iterator that performs a depth-first traversal of a schema structure using a zipper pattern.\n   * Each call to next() returns the next zipper in the traversal sequence.\n   */\n  private static class SchemaIterator implements Iterator<MutableSchemaElement> {\n    private final Consumer<StructType> finalizedSchemaConsumer;\n    private SchemaZipper nextZipper;\n    private boolean finishedVisitingCurrent = false;\n\n    SchemaIterator(\n        StructType schema,\n        Consumer<StructType> finalizedSchemaConsumer,\n        Class<?>[] typesToSkipRecursion) {\n      this.nextZipper = SchemaZipper.createZipper(schema, typesToSkipRecursion);\n      this.finalizedSchemaConsumer = finalizedSchemaConsumer;\n      // Special case if struct is empty no force no iteration.\n      this.finishedVisitingCurrent = schema.fields().isEmpty();\n    }\n\n    @Override\n    public boolean hasNext() {\n      boolean nextAvailable =\n          nextZipper != null\n              && (!finishedVisitingCurrent // Implies there are children.\n                  // Without children there must be siblings or parents left to visit to have a next\n                  // value.\n                  || nextZipper.hasMoreSiblings()\n                  || nextZipper.hasParents());\n      if (!nextAvailable && nextZipper != null) {\n        finalizedSchemaConsumer.accept((StructType) nextZipper.extractDataTypeFromFields());\n        nextZipper = null;\n      }\n      return nextAvailable;\n    }\n\n    @Override\n    public MutableSchemaElement next() {\n      if (!hasNext()) {\n        throw new NoSuchElementException();\n      }\n      advanceNext();\n      return nextZipper;\n    }\n\n    private void advanceNext() {\n      while (nextZipper != null) {\n        if (!finishedVisitingCurrent) {\n          if (nextZipper.hasChildren()) {\n            // Try to go deeper if we haven't visited this node yet\n            while (nextZipper.hasChildren()) {\n              nextZipper = nextZipper.childrenZipper();\n            }\n          }\n          // At a leaf node, so by definition it is finished visiting.\n          finishedVisitingCurrent = true;\n          return;\n        }\n\n        // Try moving to sibling if there is no need to go down further.\n        if (nextZipper.hasMoreSiblings()) {\n          nextZipper = nextZipper.moveToSibling();\n          if (nextZipper.hasChildren()) {\n            // Force visiting children first.\n            finishedVisitingCurrent = false;\n            continue;\n          }\n          finishedVisitingCurrent = true;\n          return;\n        }\n\n        // Last remaining direction with no children and no\n        // siblings is to pop back up and note that visiting\n        // has finished on the current value.\n        nextZipper = nextZipper.moveToParent();\n        finishedVisitingCurrent = true;\n        return;\n      }\n    }\n  }\n\n  /**\n   * Container for parent StructField information returned by {@link\n   * SchemaElement#getParentStructFieldAndPath()}.\n   */\n  public static class ParentStructFieldInfo {\n    private final StructField parentField;\n    private final String pathFromParent;\n\n    public ParentStructFieldInfo(StructField parentField, String pathFromParent) {\n      this.parentField = parentField;\n      this.pathFromParent = pathFromParent;\n    }\n\n    /** Returns the nearest parent StructField. */\n    public StructField getParentField() {\n      return parentField;\n    }\n\n    /**\n     * Returns the path from the nearest parent StructField. If the nearest parent StructField is a\n     * direct ancestor, this is \"\". Otherwise, the path is of the format {@code\n     * ((key|value|element).)*(key|value|element)}\n     */\n    public String getPathFromParent() {\n      return pathFromParent;\n    }\n  }\n\n  /**\n   * Interface for representing a schema element as part of a traversal.\n   *\n   * <p>This object should always be treated ephemeral and not be referenced once {@code next()} is\n   * called on the iterator.\n   */\n  public interface SchemaElement {\n    /** Get the current field. */\n    StructField getField();\n\n    /**\n     * Returns the nearest parent StructField (is a member of a StructType) if it exists. For an\n     * element that is at the root-level of the schema, this returns Optional.empty().\n     *\n     * <p>Also returns the path from the nearest parent StructField. If the nearest parent\n     * StructField is a direct ancestor, this is \"\". Otherwise, the path is of the format {@code\n     * ((key|value|element).)*(key|value|element)}\n     */\n    Optional<ParentStructFieldInfo> getParentStructFieldAndPath();\n\n    /** Returns the path to the node via user facing names. */\n    String getNamePath();\n\n    /**\n     * Returns the nearest ancestor that is a member of a StructType (could be the current element).\n     *\n     * <p>Maps Keys and Values and Array elements are skipped over when finding the nearest\n     * ancestor.\n     */\n    StructField getNearestStructFieldAncestor();\n\n    /**\n     * Returns the path to this node from the nearest ancestor that is a member of a StructType.\n     *\n     * <p>Prefix is prepend to any path with an added \".\"\n     *\n     * <p>If this element is a StructField returns prefix\n     *\n     * <p>Otherwise the grammar of the returned field is: {@code\n     * [<prefix>.]((key|value|element).)*(key|value|element)}\n     */\n    String getPathFromNearestStructFieldAncestor(String prefix);\n\n    /**\n     * Returns true if this element is a StructField (as compared to an array element or a map\n     * key/value).\n     */\n    default boolean isStructField() {\n      return false;\n    }\n  }\n  /**\n   * Interface for manipulating Schema elements.\n   *\n   * <p>This object should always be treated ephemeral and not be referenced once {@code next()} is\n   * called on the iterator.\n   */\n  public interface MutableSchemaElement extends SchemaElement {\n    /** Replace the current targeted field with a new field. */\n    void updateField(StructField structField);\n\n    /** Replace the metadata on the nearest struct ancestor with new metadata. */\n    void setMetadataOnNearestStructFieldAncestor(FieldMetadata metadata);\n  }\n\n  /**\n   * SchemaZipper implements an adaptation of the functional zipper pattern for manipulating schema\n   * structures.\n   *\n   * <p>As a high-level summary, keeps state of the path used to get a certain element as it moves\n   * through Schema elements. As it moves back up, it reconstructs the schema data types as\n   * necessary.\n   *\n   * <p>N.B. For clients using this class only one instance of a Zipper should be kept around since\n   * the internal state is mutable.\n   */\n  private abstract static class SchemaZipper implements MutableSchemaElement {\n    // Path to the current zipper. Note parents is shared between all elements\n    // on the path.\n    private final List<SchemaZipper> parents;\n    private final Class<?>[] typesToSkipRecursion;\n\n    abstract DataType constructType();\n\n    protected List<StructField> fields;\n    // Current focus element in fields.\n    private int index = 0;\n    private boolean modified = false;\n\n    private SchemaZipper(List<SchemaZipper> parents, List<StructField> fields) {\n      this(parents, fields, new Class[0]);\n    }\n\n    private SchemaZipper(\n        List<SchemaZipper> parents, List<StructField> fields, Class<?>[] typesToSkipRecursion) {\n      this.parents = parents;\n      this.fields = fields;\n      this.typesToSkipRecursion = typesToSkipRecursion;\n    }\n\n    /** Returns if the zipper has any children can be traversed. */\n    public boolean hasChildren() {\n\n      DataType currentType = currentField().getDataType();\n\n      // Skip recursion for specified types\n      for (Class<?> typeToSkip : typesToSkipRecursion) {\n        if (typeToSkip.isInstance(currentType)) {\n          return false;\n        }\n      }\n\n      boolean isStructType = currentType instanceof StructType;\n\n      // TODO(#4571): this concept should be centralized.\n      boolean isNested =\n          isStructType || currentType instanceof ArrayType || currentType instanceof MapType;\n      boolean isEmptyStruct = isStructType && ((StructType) currentType).fields().isEmpty();\n      return isNested && !isEmptyStruct;\n    }\n\n    static SchemaZipper createZipper(StructType schema, Class<?>[] typesToSkipRecursion) {\n      return createZipper(/*parents=*/ new ArrayList<>(), schema, typesToSkipRecursion);\n    }\n\n    private static SchemaZipper createZipper(\n        List<SchemaZipper> parents, DataType type, Class<?>[] typesToSkipRecursion) {\n      if (type instanceof ArrayType) {\n        ArrayType arrayType = (ArrayType) type;\n        return new ArraySchemaZipper(parents, arrayType, typesToSkipRecursion);\n      } else if (type instanceof MapType) {\n        MapType mapType = (MapType) type;\n        return new MapSchemaZipper(parents, mapType, typesToSkipRecursion);\n      } else if (type instanceof StructType) {\n        StructType structType = (StructType) type;\n        return new StructSchemaZipper(parents, structType, typesToSkipRecursion);\n      } else {\n        throw new KernelException(\"Unsupported data type: \" + type);\n      }\n    }\n\n    private static SchemaZipper createZipper(\n        List<SchemaZipper> parents, StructField field, Class<?>[] typesToSkipRecursion) {\n      return createZipper(parents, field.getDataType(), typesToSkipRecursion);\n    }\n\n    /** Returns a zipper pointing the left-most child field of this zipper. */\n    public SchemaZipper childrenZipper() {\n      if (!hasChildren()) {\n        return null;\n      }\n      parents.add(this);\n      return createZipper(parents, fields.get(index), typesToSkipRecursion);\n    }\n\n    /**\n     * Returns a zipper pointing to the next sibling of this zipper. (moving right across zippers).\n     */\n    public SchemaZipper moveToSibling() {\n      if (!hasMoreSiblings()) {\n        return null;\n      }\n      index++;\n      return this;\n    }\n\n    public boolean hasMoreSiblings() {\n      return index < fields.size() - 1;\n    }\n\n    @Override\n    public StructField getField() {\n      return currentField();\n    }\n\n    private SchemaZipper getParent() {\n      if (parents.isEmpty()) {\n        return null;\n      }\n      return parents.get(parents.size() - 1);\n    }\n\n    @Override\n    public Optional<ParentStructFieldInfo> getParentStructFieldAndPath() {\n      if (parents.isEmpty()) {\n        return Optional.empty();\n      }\n      LinkedList<String> names = new LinkedList<>();\n      for (int i = parents.size() - 1; i >= 0; i--) {\n        SchemaZipper parent = parents.get(i);\n        if (parent.isStructField()) {\n          return Optional.of(\n              new ParentStructFieldInfo(parent.getField(), SchemaUtils.concatWithDot(names)));\n        }\n        // We are traversing parents in reverse so need to insert at the start\n        names.addFirst(parent.currentField().getName());\n      }\n      throw new IllegalStateException(\n          \"At least one parent must be a struct field for a valid schema\");\n    }\n\n    /**\n     * Returns a new zipper pointing to the parent of this zipper.\n     *\n     * <p>If the zipper has any modifications they are propagated up to the parent.\n     */\n    public SchemaZipper moveToParent() {\n      SchemaZipper parent = getParent();\n      if (!parents.isEmpty()) {\n        parents.remove(parents.size() - 1);\n      } else {\n        return null;\n      }\n      if (modified) {\n        // Propagate changes to parent.\n        StructField currentParentField = parent.currentField();\n        StructField newParentField = currentParentField.withDataType(extractDataTypeFromFields());\n        parent.updateField(newParentField);\n      }\n      return parent;\n    }\n\n    public DataType extractDataTypeFromFields() {\n      return constructType();\n    }\n\n    public StructField currentField() {\n      return fields.get(index);\n    }\n\n    @Override\n    public void updateField(StructField structField) {\n      try {\n        fields.set(index, structField);\n      } catch (UnsupportedOperationException e) {\n        // Field might be immutable, copy and set if this is the case.\n        fields = new ArrayList<>(fields);\n        fields.set(index, structField);\n      }\n      modified = true;\n    }\n\n    @Override\n    public String getNamePath() {\n      List<String> names = new ArrayList<>();\n      for (SchemaZipper parent : parents) {\n        names.add(parent.currentField().getName());\n      }\n      names.add(currentField().getName());\n      return SchemaUtils.concatWithDot(names);\n    }\n\n    @Override\n    public StructField getNearestStructFieldAncestor() {\n      if (parents.isEmpty() || this instanceof StructSchemaZipper) {\n        return currentField();\n      }\n      ListIterator<SchemaZipper> iterator = findNearestStructFieldAncestor();\n      return iterator.next().currentField();\n    }\n\n    /** Returns an iterator to a zipper that has a focus on a StructType child StructField. */\n    private ListIterator<SchemaZipper> findNearestStructFieldAncestor() {\n      ListIterator<SchemaZipper> iterator = parents.listIterator(parents.size());\n      while (iterator.hasPrevious()) {\n        SchemaZipper parent = iterator.previous();\n        if (parent instanceof StructSchemaZipper) {\n          return iterator;\n        }\n      }\n      throw new IllegalArgumentException(\"no top level parent struct field, this shouldn't happen\");\n    }\n\n    @Override\n    public void setMetadataOnNearestStructFieldAncestor(FieldMetadata metadata) {\n      if (parents.isEmpty() || this instanceof StructSchemaZipper) {\n        updateField(currentField().withNewMetadata(metadata));\n        return;\n      }\n      ListIterator<SchemaZipper> iterator = findNearestStructFieldAncestor();\n      SchemaZipper parent = iterator.next();\n      parent.updateField(parent.currentField().withNewMetadata(metadata));\n    }\n\n    @Override\n    public String getPathFromNearestStructFieldAncestor(String prefix) {\n      if (parents.isEmpty() || this instanceof StructSchemaZipper) {\n        return prefix;\n      }\n      ListIterator<SchemaZipper> iterator = parents.listIterator(parents.size());\n      int pathSize = prefix.length() + currentField().getName().length();\n      while (iterator.hasPrevious()) {\n        SchemaZipper parent = iterator.previous();\n        if (parent instanceof StructSchemaZipper) {\n          break;\n        }\n        pathSize += parent.currentField().getName().length();\n      }\n      StringBuilder sb =\n          new StringBuilder(\n              pathSize + (prefix.isEmpty() ? 0 : 1 + (parents.size()) - iterator.nextIndex()));\n      if (!prefix.isEmpty()) {\n        sb.append(prefix);\n        sb.append(\".\");\n      }\n      iterator.next();\n      while (iterator.hasNext()) {\n        sb.append(iterator.next().currentField().getName());\n        sb.append(\".\");\n      }\n      sb.append(currentField().getName());\n      return sb.toString();\n    }\n\n    public boolean hasParents() {\n      return !parents.isEmpty();\n    }\n  }\n\n  private static class ArraySchemaZipper extends SchemaZipper {\n    ArraySchemaZipper(List<SchemaZipper> parents, ArrayType arrayType) {\n      super(parents, Collections.singletonList(arrayType.getElementField()));\n      if (!fields.get(0).getName().equals(ArrayType.ARRAY_ELEMENT_NAME)) {\n        throw new KernelException(\n            \"ArrayType must have a single field named 'element', found: \"\n                + fields.get(0).getName());\n      }\n    }\n\n    ArraySchemaZipper(\n        List<SchemaZipper> parents, ArrayType arrayType, Class<?>[] typesToSkipRecursion) {\n      super(parents, Collections.singletonList(arrayType.getElementField()), typesToSkipRecursion);\n    }\n\n    @Override\n    DataType constructType() {\n      return new ArrayType(fields.get(0));\n    }\n  }\n\n  private static class MapSchemaZipper extends SchemaZipper {\n    MapSchemaZipper(List<SchemaZipper> parents, MapType mapType) {\n      super(parents, Arrays.asList(mapType.getKeyField(), mapType.getValueField()));\n      if (!fields.get(0).getName().equals(MapType.MAP_KEY_NAME)\n          || !fields.get(1).getName().equals(MapType.MAP_VALUE_NAME)) {\n        throw new KernelException(\n            \"MapType must have two fields named 'key' and 'value', found: \"\n                + fields.get(0).getName()\n                + \", \"\n                + fields.get(1).getName());\n      }\n    }\n\n    MapSchemaZipper(List<SchemaZipper> parents, MapType mapType, Class<?>[] typesToSkipRecursion) {\n      super(\n          parents,\n          Arrays.asList(mapType.getKeyField(), mapType.getValueField()),\n          typesToSkipRecursion);\n    }\n\n    @Override\n    DataType constructType() {\n      return new MapType(fields.get(0), fields.get(1));\n    }\n  }\n\n  private static class StructSchemaZipper extends SchemaZipper {\n    StructSchemaZipper(List<SchemaZipper> parents, StructType structType) {\n      super(parents, structType.fields());\n    }\n\n    StructSchemaZipper(\n        List<SchemaZipper> parents, StructType structType, Class<?>[] typesToSkipRecursion) {\n      super(parents, structType.fields(), typesToSkipRecursion);\n    }\n\n    @Override\n    public boolean isStructField() {\n      return true;\n    }\n\n    @Override\n    DataType constructType() {\n      return new StructType(fields);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/SchemaUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.DeltaErrors.*;\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.*;\nimport static io.delta.kernel.internal.util.ColumnMapping.*;\nimport static io.delta.kernel.internal.util.ColumnMapping.getColumnId;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.columndefaults.ColumnDefaults;\nimport io.delta.kernel.internal.skipping.StatsSchemaHelper;\nimport io.delta.kernel.internal.types.TypeWideningChecker;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n * Utility methods for schema related operations such as validating the schema has no duplicate\n * columns and the names contain only valid characters.\n */\npublic class SchemaUtils {\n\n  private SchemaUtils() {}\n\n  /**\n   * Validate the schema. This method checks if the schema has no duplicate columns, the names\n   * contain only valid characters, the data types are supported, and the column metadata is valid.\n   *\n   * @param schema the schema to validate\n   * @param isColumnMappingEnabled whether column mapping is enabled. When column mapping is\n   *     enabled, the column names in the schema can contain special characters that are allowed as\n   *     column names in the Parquet file\n   * @param isColumnDefaultEnabled whether column defaults is enabled\n   * @throws IllegalArgumentException if the schema is invalid\n   */\n  public static void validateSchema(\n      StructType schema,\n      boolean isColumnMappingEnabled,\n      boolean isColumnDefaultEnabled,\n      boolean isIcebergCompatV3Enabled) {\n    checkArgument(schema.length() > 0, \"Schema should contain at least one column\");\n\n    List<String> flattenColNames =\n        new SchemaIterable(schema)\n            .stream()\n                // Paths to struct fields are sufficient to find duplicate columns, as arrays/maps\n                // always have the same names.\n                .filter(SchemaIterable.SchemaElement::isStructField)\n                .map(SchemaIterable.SchemaElement::getNamePath)\n                .collect(Collectors.toList());\n\n    // check there are no duplicate column names in the schema\n    Set<String> uniqueColNames =\n        flattenColNames.stream().map(String::toLowerCase).collect(Collectors.toSet());\n\n    if (uniqueColNames.size() != flattenColNames.size()) {\n      Set<String> uniqueCols = new HashSet<>();\n      List<String> duplicateColumns =\n          flattenColNames.stream()\n              .map(String::toLowerCase)\n              .filter(n -> !uniqueCols.add(n))\n              .sorted(String::compareTo)\n              .collect(Collectors.toList());\n      throw DeltaErrors.duplicateColumnsInSchema(schema, duplicateColumns);\n    }\n\n    // Check the column names are valid\n    if (!isColumnMappingEnabled) {\n      validParquetColumnNames(flattenColNames);\n    } else {\n      // when column mapping is enabled, just check the name contains no new line in it.\n      flattenColNames.forEach(\n          name -> {\n            if (name.contains(\"\\\\n\")) {\n              throw invalidColumnName(name, \"\\\\n\");\n            }\n          });\n    }\n\n    validateSupportedType(schema);\n    ColumnDefaults.validateSchema(schema, isColumnDefaultEnabled, isIcebergCompatV3Enabled);\n  }\n\n  /**\n   * Performs the following validations on an updated table schema using the current schema as a\n   * base for validation. ColumnMapping must be enabled to call this.\n   *\n   * <p>Returns an updated schema if metadata (i.e. TypeChanges needs to be copied over from\n   * currentSchema and new type changes need to be recorded. Kernel is expected to handle this work\n   * instead of clients).\n   *\n   * <p>The following checks are performed:\n   *\n   * <ul>\n   *   <li>No duplicate columns are allowed\n   *   <li>Column names contain only valid characters\n   *   <li>Data types are supported\n   *   <li>Physical column name consistency is preserved in the new schema\n   *   <li>If IcebergWriterCompatV1 is enabled, that map struct keys have not changed\n   *   <li>ToDo: Nested IDs for array/map types are preserved in the new schema for IcebergCompatV2\n   * </ul>\n   */\n  public static Optional<StructType> validateUpdatedSchemaAndGetUpdatedSchema(\n      Metadata currentMetadata,\n      Metadata newMetadata,\n      Protocol newProtocol,\n      Set<String> clusteringColumnPhysicalNames,\n      boolean allowNewRequiredFields) {\n    checkArgument(\n        isColumnMappingModeEnabled(\n            ColumnMapping.getColumnMappingMode(newMetadata.getConfiguration())),\n        \"Cannot validate updated schema when column mapping is disabled\");\n    validateSchema(\n        newMetadata.getSchema(),\n        true /*columnMappingEnabled*/,\n        newProtocol.supportsFeature(ALLOW_COLUMN_DEFAULTS_W_FEATURE),\n        TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(newMetadata));\n    validatePartitionColumns(\n        newMetadata.getSchema(), new ArrayList<>(newMetadata.getPartitionColNames()));\n    int currentMaxFieldId =\n        Integer.parseInt(\n            currentMetadata.getConfiguration().getOrDefault(COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"0\"));\n    return validateSchemaEvolution(\n        currentMetadata.getSchema(),\n        newMetadata.getSchema(),\n        ColumnMapping.getColumnMappingMode(newMetadata.getConfiguration()),\n        clusteringColumnPhysicalNames,\n        currentMaxFieldId,\n        allowNewRequiredFields,\n        TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.fromMetadata(newMetadata.getConfiguration()),\n        TableConfig.TYPE_WIDENING_ENABLED.fromMetadata(newMetadata.getConfiguration()));\n  }\n\n  /**\n   * Validates a given schema evolution by using field ID as the source of truth for identifying\n   * fields\n   *\n   * @param currentSchema the schema that is present the table schema _before_ the schema evolution\n   * @param newSchema the new schema that is present the table schema _after_ the schema evolution\n   * @param clusteringColumnPhysicalNames The clustering columns present in the table before the\n   *     schema update\n   * @param oldMaxFieldId the maximum field id in the table before the schema update\n   * @param allowNewRequiredFields If `false`, adding new required columns throws an error. If\n   *     `true`, new required columns are allowed\n   * @param icebergWriterCompatV1Enabled `true` if icebergCompatV1 is enabled on the table\n   * @return an updated schema if metadata (e.g. TypeChanges needs to be copied over from the old\n   *     schema\n   * @throws IllegalArgumentException if the schema evolution is invalid\n   */\n  // TODO: Consider renaming or refactoring to avoid returning the\n  // StructType here.\n  public static Optional<StructType> validateSchemaEvolutionById(\n      StructType currentSchema,\n      StructType newSchema,\n      Set<String> clusteringColumnPhysicalNames,\n      int oldMaxFieldId,\n      boolean allowNewRequiredFields,\n      boolean icebergWriterCompatV1Enabled,\n      boolean typeWideningEnabled) {\n    SchemaChanges schemaChanges = computeSchemaChangesById(currentSchema, newSchema);\n    validatePhysicalNameConsistency(schemaChanges.updatedFields());\n    // Validates that the updated schema does not contain breaking changes in terms of types and\n    // nullability\n    validateUpdatedSchemaCompatibility(\n        schemaChanges,\n        oldMaxFieldId,\n        allowNewRequiredFields,\n        icebergWriterCompatV1Enabled,\n        typeWideningEnabled);\n    validateClusteringColumnsNotDropped(\n        schemaChanges.removedFields(), clusteringColumnPhysicalNames);\n    return schemaChanges.updatedSchema();\n    // ToDo Potentially validate IcebergCompatV2 nested IDs\n  }\n\n  /**\n   * Verify the partition columns exists in the table schema and a supported data type for a\n   * partition column.\n   *\n   * @param schema\n   * @param partitionCols\n   */\n  public static void validatePartitionColumns(StructType schema, List<String> partitionCols) {\n    // partition columns are always the top-level columns\n    Map<String, DataType> columnNameToType =\n        schema.fields().stream()\n            .collect(\n                Collectors.toMap(\n                    field -> field.getName().toLowerCase(Locale.ROOT), StructField::getDataType));\n\n    partitionCols.stream()\n        .forEach(\n            partitionCol -> {\n              DataType dataType = columnNameToType.get(partitionCol.toLowerCase(Locale.ROOT));\n              checkArgument(\n                  dataType != null, \"Partition column %s not found in the schema\", partitionCol);\n\n              if (!(dataType instanceof BooleanType\n                  || dataType instanceof ByteType\n                  || dataType instanceof ShortType\n                  || dataType instanceof IntegerType\n                  || dataType instanceof LongType\n                  || dataType instanceof FloatType\n                  || dataType instanceof DoubleType\n                  || dataType instanceof DecimalType\n                  || dataType instanceof StringType\n                  || dataType instanceof BinaryType\n                  || dataType instanceof DateType\n                  || dataType instanceof TimestampType\n                  || dataType instanceof TimestampNTZType)) {\n                throw unsupportedPartitionDataType(partitionCol, dataType);\n              }\n            });\n  }\n\n  /**\n   * Delta expects partition column names to be same case preserving as the name in the schema. E.g:\n   * Schema: (a INT, B STRING) and partition columns: (b). In this case we store the schema as (a\n   * INT, B STRING) and partition columns as (B).\n   *\n   * <p>This method expects the inputs are already validated (i.e. schema contains all the partition\n   * columns).\n   */\n  public static List<String> casePreservingPartitionColNames(\n      StructType tableSchema, List<String> partitionColumns) {\n    Map<String, String> columnNameMap = new HashMap<>();\n    tableSchema\n        .fieldNames()\n        .forEach(colName -> columnNameMap.put(colName.toLowerCase(Locale.ROOT), colName));\n    return partitionColumns.stream()\n        .map(colName -> columnNameMap.get(colName.toLowerCase(Locale.ROOT)))\n        .collect(Collectors.toList());\n  }\n\n  /**\n   * Convert the partition column names in {@code partitionValues} map into the same case as the\n   * column in the table metadata. Delta expects the partition column names to preserve the case\n   * same as the table schema.\n   *\n   * @param partitionColNames List of partition columns in the table metadata. The names preserve\n   *     the case as given by the connector when the table is created.\n   * @param partitionValues Map of partition column name to partition value. Convert the partition\n   *     column name to be same case preserving name as its equivalent column in the {@code\n   *     partitionColName}. Column name comparison is case-insensitive.\n   * @return Rewritten {@code partitionValues} map with names case preserved.\n   */\n  public static Map<String, Literal> casePreservingPartitionColNames(\n      List<String> partitionColNames, Map<String, Literal> partitionValues) {\n    Map<String, String> partitionColNameMap = new HashMap<>();\n    partitionColNames.forEach(\n        colName -> partitionColNameMap.put(colName.toLowerCase(Locale.ROOT), colName));\n\n    return partitionValues.entrySet().stream()\n        .collect(\n            Collectors.toMap(\n                entry -> partitionColNameMap.get(entry.getKey().toLowerCase(Locale.ROOT)),\n                Map.Entry::getValue));\n  }\n\n  /**\n   * Verify the clustering columns exists in the table schema.\n   *\n   * @param schema The schema of the table\n   * @param clusteringCols List of clustering columns\n   */\n  public static List<Column> casePreservingEligibleClusterColumns(\n      StructType schema, List<Column> clusteringCols) {\n\n    List<Tuple2<Column, DataType>> physicalColumnsWithTypes =\n        clusteringCols.stream()\n            .map(col -> ColumnMapping.getPhysicalColumnNameAndDataType(schema, col))\n            .collect(Collectors.toList());\n\n    List<String> nonSkippingEligibleColumns =\n        physicalColumnsWithTypes.stream()\n            .filter(tuple -> !StatsSchemaHelper.isSkippingEligibleDataType(tuple._2))\n            .map(tuple -> tuple._1.toString() + \" : \" + tuple._2)\n            .collect(Collectors.toList());\n\n    if (!nonSkippingEligibleColumns.isEmpty()) {\n      throw new KernelException(\n          format(\n              \"Clustering is not supported because the following column(s): %s \"\n                  + \"don't support data skipping\",\n              nonSkippingEligibleColumns));\n    }\n\n    return physicalColumnsWithTypes.stream().map(tuple -> tuple._1).collect(Collectors.toList());\n  }\n\n  /**\n   * Search (case-insensitive) for the given {@code colName} in the {@code schema} and return its\n   * position in the {@code schema}.\n   *\n   * @param schema {@link StructType}\n   * @param colName Name of the column whose index is needed.\n   * @return Valid index or -1 if not found.\n   */\n  public static int findColIndex(StructType schema, String colName) {\n    for (int i = 0; i < schema.length(); i++) {\n      if (schema.at(i).getName().equalsIgnoreCase(colName)) {\n        return i;\n      }\n    }\n    return -1;\n  }\n\n  /**\n   * Collects all leaf columns from the given schema (including flattened columns only for\n   * StructTypes), up to maxColumns. NOTE: If maxColumns = -1, we collect ALL leaf columns in the\n   * schema.\n   */\n  public static List<Column> collectLeafColumns(\n      StructType schema, Set<String> excludedColumns, int maxColumns) {\n    List<Column> result = new ArrayList<>();\n    collectLeafColumnsInternal(schema, null, excludedColumns, result, maxColumns);\n    return result;\n  }\n\n  /** @return column name by concatenating the column path elements (think of nested) with dots */\n  public static String concatWithDot(List<String> columnPath) {\n    return columnPath.stream().map(SchemaUtils::escapeDots).collect(Collectors.joining(\".\"));\n  }\n\n  /** Helper method to create a copy of a column that is marked as an internal column. */\n  public static StructField asInternalColumn(StructField field) {\n    FieldMetadata metadata =\n        FieldMetadata.builder()\n            .fromMetadata(field.getMetadata())\n            .putBoolean(StructField.IS_INTERNAL_COLUMN_KEY, true)\n            .build();\n    return field.withNewMetadata(metadata);\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  /// Private methods                                                                           ///\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Compute the SchemaChanges using field IDs\n   *\n   * @throws KernelException if any existing fields have been illegally moved outside their parent\n   *     struct\n   */\n  static SchemaChanges computeSchemaChangesById(StructType currentSchema, StructType newSchema) {\n    SchemaChanges.Builder schemaDiff = SchemaChanges.builder();\n    findAndAddRemovedFields(currentSchema, newSchema, schemaDiff);\n\n    // Given a schema like struct<a (id=1) : map<int,struct<b (id=2) : Int >>\n    // This map would contain:\n    // {<\"\", 1> : StructField(\"a\", MapType),\n    // <\"key\", 1> : StructField(\"key\", IntegerType),\n    // <\"value\", 1> : StructField(\"value\", StructType),\n    // <\"\", 2>: StructField(\"b\", IntegerType)}\n    Map<SchemaElementId, StructField> currentFieldIdToField = fieldsByElementId(currentSchema);\n    // This map only contains struct field keys (does not include complex type elements).\n    // Using the earlier example, this map would contain:\n    // {1: Optional.empty(),\n    //  2: Optional.of(StructField(\"a\", MapType), \"value\"))}\n    Map<Integer, Optional<SchemaIterable.ParentStructFieldInfo>> currentFieldIdToParent =\n        mapStructFieldsToParent(currentSchema);\n    Set<Integer> addedFieldIds = new HashSet<>();\n    SchemaIterable newSchemaIterable = new SchemaIterable(newSchema);\n    Iterator<SchemaIterable.MutableSchemaElement> newSchemaIterator =\n        newSchemaIterable.newMutableIterator();\n\n    boolean newSchemaHasUpdates = false;\n    while (newSchemaIterator.hasNext()) {\n      SchemaIterable.MutableSchemaElement newElement = newSchemaIterator.next();\n      SchemaElementId id = getSchemaElementId(newElement);\n\n      // If the element is a struct field we need to validate that it has not been moved out of\n      // its parent struct. To do this, we check that in the old schema and the new schema\n      // its parent struct field (and the path to it) is unchanged.\n      if (newElement.isStructField()) {\n        int columnId = getColumnId(newElement.getField());\n        if (currentFieldIdToParent.containsKey(columnId)) { // If it's an existing field\n          // We need both the parent struct field and the path to it in case of nested arrays/maps\n          Optional<SchemaIterable.ParentStructFieldInfo> currentParent =\n              currentFieldIdToParent.get(columnId);\n          Optional<SchemaIterable.ParentStructFieldInfo> newParent =\n              newElement.getParentStructFieldAndPath();\n          Optional<SchemaElementId> currentParentId =\n              currentParent.map(SchemaUtils::getSchemaElementId);\n          Optional<SchemaElementId> newParentId = newParent.map(SchemaUtils::getSchemaElementId);\n          if (!Objects.equals(currentParentId, newParentId)) {\n            throw DeltaErrors.invalidFieldMove(columnId, currentParent, newParent);\n          }\n        }\n      }\n\n      if (addedFieldIds.contains(id.getId())) {\n        // Skip early if this is a descendant of an added field.\n        continue;\n      }\n\n      StructField existingField = currentFieldIdToField.get(id);\n      if (existingField == null) {\n        // If the field wasn't present in the schema before then it represents either\n        // a schema change or a newly added field. To check if it is a new struct field,\n        // we check the old schema for just the field ID (i.e. no nested path) to make\n        // a determination between these two cases.\n        // If new StructField where added to the schema example above (e.g\n        // <a (id=1) : map<int,struct<b (id=2) : Int, c (id=3) : Int>>> )\n        // This would eventually probe the currentFieldIdToField map for <\"\", 3> and\n        // find it is an addition.\n        SchemaElementId rootId = new SchemaElementId(/* nestedPath= */ \"\", id.getId());\n        if (!currentFieldIdToField.containsKey(rootId)) {\n          addedFieldIds.add(id.getId());\n          schemaDiff.withAddedField(newElement.getNearestStructFieldAncestor());\n        }\n        // Getting here implies a structural change in nested maps/arrays which will be caught at\n        // when comparing the higher level element (or we added a field in the if statement above).\n        // This can be somewhat subtle, if this is a type change.\n        // Consider the case of the changing a field from Map to an Array. This would imply that\n        // path would be \"element\" here and either \"key\" or \"value\" in the previous schema. Nothing\n        // is done for the new \"element\" path if it isn't a field addition there must be at least\n        // one ancestor node in common (at least the nearest struct field) which would get added as\n        // an update below. This logic is inductive. If the previous schema was a array<array<x>>\n        // and was not a new addition and the new schema was array<array<array<x>>> then this path\n        // would be reached on element.element.element but the type the code would move past this\n        // block for element.element which would have a type change detected from x to array<x>.\n        // concretely if the new schema was <a id=1 : array<struct<b (id=2) : Int>>> then\n        // <\"element, \"1\"> would be skipped here but the type change would be detected for <\"\", 1>\n        // from map to array.\n        continue;\n      }\n      StructField newField = newElement.getField();\n      List<TypeChange> existingTypeChanges = existingField.getTypeChanges();\n      if (!existingTypeChanges.isEmpty() && newField.getTypeChanges().isEmpty()) {\n        // Copy over type changes from existing field because new schemas\n        // might be constructed elsewhere and not have the persisted type\n        // change metadata.\n        newField = newField.withTypeChanges(existingTypeChanges);\n        newElement.updateField(newField);\n        newSchemaHasUpdates = true;\n      }\n\n      // Ensure the schemas are equal now, updating type changes from clients is not supported\n      // so they should be.\n      if (!existingTypeChanges.equals(newField.getTypeChanges())) {\n        throw new KernelException(\n            String.format(\n                \"Detected a modified type changes field at '%s' %s != %s\",\n                newElement.getNamePath(), existingTypeChanges, newField.getTypeChanges()));\n      }\n\n      if (!existingField.equals(newField)) {\n        if (!existingField.getDataType().isNested()\n            && !newField.getDataType().isNested()\n            && !existingField.getDataType().equivalent(newField.getDataType())) {\n          // Type changes only apply to non-nested types.  This loop evaluates both\n          // nested and non-nested, so we narrow down updates here. Actual type differences\n          // between nested types are validated against SchemaChanges returned by this\n          // function.\n          List<TypeChange> changes = new ArrayList<>(newField.getTypeChanges());\n          changes.add(new TypeChange(existingField.getDataType(), newField.getDataType()));\n          newElement.updateField(newField.withTypeChanges(changes));\n          newSchemaHasUpdates = true;\n        }\n        // Field changed name, nullability, metadata or type\n        schemaDiff.withUpdatedField(existingField, newField, newElement.getNamePath());\n      }\n    }\n    if (newSchemaHasUpdates) {\n      schemaDiff.withUpdatedSchema(newSchemaIterable.getSchema());\n    }\n    return schemaDiff.build();\n  }\n\n  private static Map<SchemaElementId, StructField> fieldsByElementId(StructType schema) {\n    Map<SchemaElementId, StructField> fieldIdToField = new HashMap<>();\n    for (SchemaIterable.SchemaElement element : new SchemaIterable(schema)) {\n      SchemaElementId id = getSchemaElementId(element);\n      checkArgument(\n          !fieldIdToField.containsKey(id),\n          \"Field %s with id %d already exists\",\n          element.getNamePath(),\n          id);\n      fieldIdToField.put(id, element.getField());\n    }\n    return fieldIdToField;\n  }\n\n  private static Map<Integer, Optional<SchemaIterable.ParentStructFieldInfo>>\n      mapStructFieldsToParent(StructType schema) {\n    Map<Integer, Optional<SchemaIterable.ParentStructFieldInfo>> fieldIdToParent = new HashMap<>();\n    for (SchemaIterable.SchemaElement element : new SchemaIterable(schema)) {\n      if (element.isStructField()) {\n        StructField elementField = element.getField();\n        int columnId = getColumnId(elementField);\n        Optional<SchemaIterable.ParentStructFieldInfo> parentInfo =\n            element.getParentStructFieldAndPath();\n        fieldIdToParent.put(columnId, parentInfo);\n      }\n    }\n    return fieldIdToParent;\n  }\n\n  private static SchemaElementId getSchemaElementId(SchemaIterable.SchemaElement element) {\n    int columnId = getColumnId(element.getNearestStructFieldAncestor());\n    return new SchemaElementId(element.getPathFromNearestStructFieldAncestor(\"\"), columnId);\n  }\n\n  private static SchemaElementId getSchemaElementId(\n      SchemaIterable.ParentStructFieldInfo parentInfo) {\n    int columnId = getColumnId(parentInfo.getParentField());\n    return new SchemaElementId(parentInfo.getPathFromParent(), columnId);\n  }\n\n  private static void findAndAddRemovedFields(\n      StructType currentSchema, StructType newSchema, SchemaChanges.Builder schemaDiff) {\n    // With schema: <a (id=1) : map<int,struct<b (id=2) : Int, c (id=3) : Int>>>\n    // contains {1: StructField(\"a\", MapType), 3: StructField(\"c\", IntegerType)}\n    Map<Integer, StructField> fieldIdToField = fieldsById(newSchema);\n    for (SchemaIterable.SchemaElement element : new SchemaIterable(currentSchema)) {\n      // Removed fields are always calculated at the Struct level.\n      // From the example above, we only \"c\" or \"a\" are removed, as all other StructFields\n      // returned by the iterator (e.g. a.key, a.value cannot be removed without a type\n      // change).\n      if (!element.isStructField()) {\n        continue;\n      }\n      StructField field = element.getField();\n      int columnId = getCheckedColumnId(field);\n      if (!fieldIdToField.containsKey(columnId)) {\n        schemaDiff.withRemovedField(element.getField());\n      }\n    }\n  }\n\n  private static void validatePhysicalNameConsistency(\n      List<SchemaChanges.SchemaUpdate> updatedFields) {\n    for (SchemaChanges.SchemaUpdate updatedField : updatedFields) {\n      StructField currentField = updatedField.before();\n      StructField newField = updatedField.after();\n      if (!getPhysicalName(currentField).equals(getPhysicalName(newField))) {\n        throw new IllegalArgumentException(\n            String.format(\n                \"Existing field with id %s in current schema has \"\n                    + \"physical name %s which is different from %s\",\n                getColumnId(currentField),\n                getPhysicalName(currentField),\n                getPhysicalName(newField)));\n      }\n    }\n  }\n\n  /*\n   * Validate if a given schema evolution is safe for a given column mapping mode\n   *\n   * <p>Returns an updated schema if metadata (i.e. TypeChanges needs to be copied\n   * over from currentSchema).\n   */\n  private static Optional<StructType> validateSchemaEvolution(\n      StructType currentSchema,\n      StructType newSchema,\n      ColumnMappingMode columnMappingMode,\n      Set<String> clusteringColumnPhysicalNames,\n      int currentMaxFieldId,\n      boolean allowNewRequiredFields,\n      boolean icebergWriterCompatV1Enabled,\n      boolean typeWideningEnabled) {\n    switch (columnMappingMode) {\n      case ID:\n      case NAME:\n        return validateSchemaEvolutionById(\n            currentSchema,\n            newSchema,\n            clusteringColumnPhysicalNames,\n            currentMaxFieldId,\n            allowNewRequiredFields,\n            icebergWriterCompatV1Enabled,\n            typeWideningEnabled);\n      case NONE:\n        throw new UnsupportedOperationException(\n            \"Schema evolution without column mapping is not supported\");\n      default:\n        throw new UnsupportedOperationException(\n            \"Unknown column mapping mode: \" + columnMappingMode);\n    }\n  }\n\n  private static void validateClusteringColumnsNotDropped(\n      List<StructField> droppedFields, Set<String> clusteringColumnPhysicalNames) {\n    for (StructField droppedField : droppedFields) {\n      // ToDo: At some point plumb through mapping of ID to full name, so we get better error\n      // messages\n      if (clusteringColumnPhysicalNames.contains(getPhysicalName(droppedField))) {\n        throw new KernelException(\n            String.format(\"Cannot drop clustering column %s\", droppedField.getName()));\n      }\n    }\n  }\n\n  /**\n   * Verifies that no non-nullable fields are added, no existing field nullability is tightened and\n   * no invalid type changes are performed\n   *\n   * <p>ToDo: Prevent moving fields outside of their containing struct\n   */\n  private static void validateUpdatedSchemaCompatibility(\n      SchemaChanges schemaChanges,\n      int oldMaxFieldId,\n      boolean allowNewRequiredFields,\n      boolean icebergWriterCompatV1Enabled,\n      boolean typeWideningEnabled) {\n    for (StructField addedField : schemaChanges.addedFields()) {\n      if (!allowNewRequiredFields && !addedField.isNullable()) {\n        throw new KernelException(\n            String.format(\"Cannot add non-nullable field %s\", addedField.getName()));\n      }\n      int colId = getColumnId(addedField);\n      if (colId <= oldMaxFieldId) {\n        throw new IllegalArgumentException(\n            String.format(\n                \"Cannot add a new column with a fieldId <= maxFieldId. Found field: %s with\"\n                    + \"fieldId=%s. Current maxFieldId in the table is: %s\",\n                addedField, colId, oldMaxFieldId));\n      }\n    }\n\n    for (SchemaChanges.SchemaUpdate updatedFields : schemaChanges.updatedFields()) {\n      validateFieldCompatibility(updatedFields, icebergWriterCompatV1Enabled, typeWideningEnabled);\n    }\n  }\n\n  /**\n   * Validate that there was no change in type from existing field from new field, excluding\n   * modified, dropped, or added fields to structs. Validates that a field's nullability is not\n   * tightened\n   */\n  private static void validateFieldCompatibility(\n      SchemaChanges.SchemaUpdate update,\n      boolean icebergWriterCompatV1Enabled,\n      boolean typeWideningEnabled) {\n    StructField existingField = update.before();\n    StructField newField = update.after();\n    if (existingField.isNullable() && !newField.isNullable()) {\n      throw new KernelException(\n          String.format(\n              \"Cannot tighten the nullability of existing field %s\", update.getPathToAfterField()));\n    }\n\n    if (existingField.getDataType() instanceof MapType\n        && newField.getDataType() instanceof MapType) {\n      MapType existingMapType = (MapType) existingField.getDataType();\n      MapType newMapType = (MapType) newField.getDataType();\n\n      if (icebergWriterCompatV1Enabled\n          && existingMapType.getKeyType() instanceof StructType\n          && newMapType.getKeyType() instanceof StructType) {\n        // Enforce that we don't change map struct keys. This is a requirement for\n        // IcebergWriterCompatV1\n        StructType currentKeyType = (StructType) existingMapType.getKeyType();\n        StructType newKeyType = (StructType) newMapType.getKeyType();\n        if (!currentKeyType.equals(newKeyType)) {\n          throw new KernelException(\n              String.format(\n                  \"Cannot change the type key of Map field %s from %s to %s\",\n                  newField.getName(), currentKeyType, newKeyType));\n        }\n      }\n    } else {\n      // Note because computeSchemaChangesById() adds all changed struct fields and any nested\n      // elements that have the same path but different types then the following scenarios are\n      // handled in this block.\n      // 1. This is non-leaf node (e.g. StructType, ArrayType) type, in which case it is sufficient\n      // to ensure that the types are of the same class. For a struct type there might be field\n      // additions or removals, which shouldn't be considered invalid schema transitions\n      // (and hence `equivalent(...)` is not used in this case). If the nested type changes\n      // (e.g. ArrayType to Primitive) then the classes would be different and the types by\n      // definition would not be equivalent.\n      //\n      // 2. This is a leaf node, in which case it sufficient to check that the types are equivalent\n      // (or once implemented the transition of types is valid).\n      //\n      // The subtle point here is for any non-leaf node change, the computed changes will include at\n      // least one ancestor change where the type change can be detected.\n      //\n      for (Class<?> clazz : new Class<?>[] {StructType.class, ArrayType.class}) {\n        if (existingField.getDataType().getClass() == clazz\n            && newField.getDataType().getClass() == clazz) {\n          return;\n        }\n      }\n      DataType existingType = existingField.getDataType();\n      DataType newType = newField.getDataType();\n      if (!existingType.equivalent(newType)) {\n\n        if (typeWideningEnabled) {\n          if ((icebergWriterCompatV1Enabled\n                  && TypeWideningChecker.isIcebergV2Compatible(existingType, newType))\n              || (!icebergWriterCompatV1Enabled\n                  && TypeWideningChecker.isWideningSupported(existingType, newType))) {\n            return;\n          }\n        }\n        throw new KernelException(\n            String.format(\n                \"Cannot change the type of existing field %s from %s to %s\",\n                existingField.getName(), existingField.getDataType(), newField.getDataType()));\n      }\n    }\n  }\n\n  /**\n   * A composite class that uniquely identifiers an element in a schema.\n   *\n   * <p>When column mapping is enabled, the every field in structs has a unique ID. For other nested\n   * types (maps and arrays), elements are identified from there path from the struct field.\n   */\n  private static class SchemaElementId {\n    private final String nestedPath;\n    private final int id;\n\n    SchemaElementId(String nestedPath, int id) {\n      this.nestedPath = nestedPath;\n      this.id = id;\n    }\n\n    public int getId() {\n      return id;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n      if (o == null || getClass() != o.getClass()) return false;\n      SchemaElementId that = (SchemaElementId) o;\n      return id == that.id && Objects.equals(nestedPath, that.nestedPath);\n    }\n\n    @Override\n    public int hashCode() {\n      return Objects.hash(nestedPath, id);\n    }\n\n    @Override\n    public String toString() {\n      return \"SchemaElementId{\" + \"getPath='\" + nestedPath + '\\'' + \", id=\" + id + '}';\n    }\n  }\n\n  private static Map<Integer, StructField> fieldsById(StructType schema) {\n    Map<Integer, StructField> columnIdToField = new HashMap<>();\n    for (SchemaIterable.SchemaElement element : new SchemaIterable(schema)) {\n      if (!element.isStructField()) {\n        continue;\n      }\n      StructField field = element.getField();\n      int columnId = getCheckedColumnId(field);\n      checkArgument(\n          !columnIdToField.containsKey(columnId),\n          \"Field %s with id %d already exists\",\n          field.getName(),\n          columnId);\n      columnIdToField.put(columnId, field);\n    }\n\n    return columnIdToField;\n  }\n\n  private static int getCheckedColumnId(StructField field) {\n    checkArgument(hasColumnId(field), \"Field %s is missing column id\", field.getName());\n    checkArgument(hasPhysicalName(field), \"Field %s is missing physical name\", field.getName());\n    return ColumnMapping.getColumnId(field);\n  }\n\n  private static String escapeDots(String name) {\n    return name.contains(\".\") ? \"`\" + name + \"`\" : name;\n  }\n\n  private static void collectLeafColumnsInternal(\n      StructType schema,\n      Column parentColumn,\n      Set<String> excludedColumns,\n      List<Column> result,\n      int maxColumns) {\n    boolean hasLimit = maxColumns != -1;\n    for (StructField field : schema.fields()) {\n      if (hasLimit && result.size() >= maxColumns) {\n        return;\n      }\n\n      Column currentColumn = null;\n      if (parentColumn == null) {\n        // Skip excluded top-level columns\n        if (excludedColumns.contains(field.getName())) {\n          continue;\n        }\n        currentColumn = new Column(field.getName());\n      } else {\n        currentColumn = parentColumn.appendNestedField(field.getName());\n      }\n\n      if (field.getDataType() instanceof StructType) {\n        collectLeafColumnsInternal(\n            (StructType) field.getDataType(), currentColumn, excludedColumns, result, maxColumns);\n      } else {\n        result.add(currentColumn);\n      }\n    }\n  }\n\n  protected static void validParquetColumnNames(List<String> columnNames) {\n    for (String name : columnNames) {\n      // ,;{}()\\n\\t= and space are special characters in Parquet schema\n      if (name.matches(\".*[ ,;{}()\\n\\t=].*\")) {\n        throw invalidColumnName(name, \"[ ,;{}()\\\\n\\\\t=]\");\n      }\n    }\n  }\n\n  /**\n   * Validate the supported data types. Once we start supporting additional types, take input the\n   * protocol features and validate the schema.\n   *\n   * @param dataType the data type to validate\n   */\n  protected static void validateSupportedType(DataType dataType) {\n    if (dataType instanceof BooleanType\n        || dataType instanceof ByteType\n        || dataType instanceof ShortType\n        || dataType instanceof IntegerType\n        || dataType instanceof LongType\n        || dataType instanceof FloatType\n        || dataType instanceof DoubleType\n        || dataType instanceof DecimalType\n        || dataType instanceof StringType\n        || dataType instanceof BinaryType\n        || dataType instanceof DateType\n        || dataType instanceof TimestampType\n        || dataType instanceof TimestampNTZType\n        || dataType instanceof VariantType) {\n      // supported types\n      return;\n    } else if (dataType instanceof StructType) {\n      ((StructType) dataType).fields().forEach(field -> validateSupportedType(field.getDataType()));\n    } else if (dataType instanceof ArrayType) {\n      validateSupportedType(((ArrayType) dataType).getElementType());\n    } else if (dataType instanceof MapType) {\n      validateSupportedType(((MapType) dataType).getKeyType());\n      validateSupportedType(((MapType) dataType).getValueType());\n    } else {\n      throw unsupportedDataType(dataType);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/TimestampUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.OffsetDateTime;\n\n/**\n * Utilities for timestamp conversions that avoid overflow issues.\n *\n * <p>Note: {@code ChronoUnit.MICROS.between()} internally computes {@code (seconds * 1_000_000_000)\n * / 1000}, where the intermediate nanoseconds value overflows for timestamps beyond ~292 years from\n * epoch. These methods compute {@code seconds * 1_000_000} directly to avoid overflow.\n */\npublic final class TimestampUtils {\n\n  private TimestampUtils() {}\n\n  /** Converts an Instant to microseconds since epoch. */\n  public static long toEpochMicros(Instant instant) {\n    long microsFromSeconds = Math.multiplyExact(instant.getEpochSecond(), 1_000_000L);\n    long microsFromNanos = instant.getNano() / 1000;\n    return Math.addExact(microsFromSeconds, microsFromNanos);\n  }\n\n  /** Converts an OffsetDateTime to microseconds since epoch. */\n  public static long toEpochMicros(OffsetDateTime dateTime) {\n    return toEpochMicros(dateTime.toInstant());\n  }\n\n  /** Converts a LocalDateTime (interpreted as UTC) to microseconds since epoch. */\n  public static long toEpochMicros(LocalDateTime dateTime) {\n    return toEpochMicros(dateTime.toInstant(java.time.ZoneOffset.UTC));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/Tuple2.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\n\n/**\n * Represents tuple of objects.\n *\n * @param <K> Type of the first element in the tuple\n * @param <V> Type of the second element in the tuple\n * @since 3.0.0\n */\n@Evolving\npublic class Tuple2<K, V> {\n\n  public final K _1;\n  public final V _2;\n\n  public Tuple2(K _1, V _2) {\n    this._1 = _1;\n    this._2 = _2;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    Tuple2<?, ?> tuple2 = (Tuple2<?, ?>) o;\n    return Objects.equals(_1, tuple2._1) && Objects.equals(_2, tuple2._2);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(_1, _2);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/Utils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util;\n\nimport static io.delta.kernel.internal.DeltaErrors.wrapEngineExceptionThrowsIO;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\nimport java.util.Optional;\n\n/**\n * Various utility methods to help the connectors work with data objects returned by Kernel\n *\n * @since 3.0.0\n */\n@Evolving\npublic class Utils {\n  /**\n   * Utility method to create a singleton {@link CloseableIterator}.\n   *\n   * @param elem Element to create iterator with.\n   * @param <T> Element type.\n   * @return A {@link CloseableIterator} with just one element.\n   */\n  public static <T> CloseableIterator<T> singletonCloseableIterator(T elem) {\n    return new CloseableIterator<T>() {\n      private boolean accessed;\n\n      @Override\n      public void close() throws IOException {\n        // nothing to close\n      }\n\n      @Override\n      public boolean hasNext() {\n        return !accessed;\n      }\n\n      @Override\n      public T next() {\n        accessed = true;\n        return elem;\n      }\n    };\n  }\n\n  /**\n   * Convert a {@link Iterator} to {@link CloseableIterator}. Useful when passing normal iterators\n   * for arguments that require {@link CloseableIterator} type.\n   *\n   * @param iter {@link Iterator} instance\n   * @param <T> Element type\n   * @return A {@link CloseableIterator} wrapping the given {@link Iterator}\n   */\n  public static <T> CloseableIterator<T> toCloseableIterator(Iterator<T> iter) {\n    return new CloseableIterator<T>() {\n      @Override\n      public void close() {}\n\n      @Override\n      public boolean hasNext() {\n        return iter.hasNext();\n      }\n\n      @Override\n      public T next() {\n        return iter.next();\n      }\n    };\n  }\n\n  /**\n   * Close the given one or more {@link AutoCloseable}s. {@link AutoCloseable#close()} will be\n   * called on all given non-null closeables. Will throw unchecked {@link RuntimeException} if an\n   * error occurs while closing. If multiple closeables causes exceptions in closing, the exceptions\n   * will be added as suppressed to the main exception that is thrown.\n   *\n   * @param closeables\n   */\n  public static void closeCloseables(AutoCloseable... closeables) {\n    RuntimeException exception = null;\n    for (AutoCloseable closeable : closeables) {\n      if (closeable == null) {\n        continue;\n      }\n      try {\n        closeable.close();\n      } catch (Exception ex) {\n        if (exception == null) {\n          exception = new RuntimeException(ex);\n        } else {\n          exception.addSuppressed(ex);\n        }\n      }\n    }\n    if (exception != null) {\n      throw exception;\n    }\n  }\n\n  /**\n   * Close the given list of {@link AutoCloseable} objects. Any exception thrown is silently\n   * ignored.\n   *\n   * @param closeables\n   */\n  public static void closeCloseablesSilently(AutoCloseable... closeables) {\n    try {\n      closeCloseables(closeables);\n    } catch (Throwable throwable) {\n      // ignore\n    }\n  }\n\n  // Utility class to support `intoRows` below\n  private static class FilteredBatchToRowIter implements CloseableIterator<Row> {\n    private final CloseableIterator<FilteredColumnarBatch> sourceBatches;\n    private CloseableIterator<Row> current;\n    private boolean isClosed = false;\n\n    FilteredBatchToRowIter(CloseableIterator<FilteredColumnarBatch> sourceBatches) {\n      this.sourceBatches = sourceBatches;\n    }\n\n    @Override\n    public boolean hasNext() {\n      if (isClosed) {\n        return false;\n      }\n      while ((current == null || !current.hasNext()) && sourceBatches.hasNext()) {\n        closeCloseables(current);\n        FilteredColumnarBatch next = sourceBatches.next();\n        current = next.getRows();\n      }\n      return current != null && current.hasNext();\n    }\n\n    @Override\n    public Row next() {\n      if (!hasNext()) {\n        throw new java.util.NoSuchElementException(\"No more rows available\");\n      }\n      return current.next();\n    }\n\n    @Override\n    public void close() throws IOException {\n      isClosed = true;\n      closeCloseables(current, sourceBatches);\n    }\n  }\n\n  /** Convert a ClosableIterator of FilteredColumnarBatch into a CloseableIterator of Row */\n  public static CloseableIterator<Row> intoRows(\n      CloseableIterator<FilteredColumnarBatch> sourceBatches) {\n    return new FilteredBatchToRowIter(sourceBatches);\n  }\n\n  /**\n   * Flattens a nested {@link CloseableIterator} structure into a single flat iterator. This method\n   * takes an iterator of iterators (nested structure) and flattens it into a single iterator that\n   * yields all elements from all inner iterators in sequence.\n   *\n   * <p><b>Important:</b> Callers must call {@link CloseableIterator#close()} on the returned\n   * iterator even if it is not fully consumed, to ensure all inner iterators are properly closed\n   * and resources are released.\n   *\n   * @param nestedIterator An iterator of iterators to flatten\n   * @param <T> The type of elements in the inner iterators\n   * @return A new {@link CloseableIterator} that flattens all nested iterators\n   */\n  public static <T> CloseableIterator<T> flatten(\n      CloseableIterator<CloseableIterator<T>> nestedIterator) {\n    return new CloseableIterator<>() {\n      private CloseableIterator<T> currentInnerIterator = null;\n\n      @Override\n      public boolean hasNext() {\n        while (true) {\n          if (currentInnerIterator != null && currentInnerIterator.hasNext()) {\n            return true;\n          }\n\n          if (currentInnerIterator != null) {\n            closeCloseables(currentInnerIterator);\n            currentInnerIterator = null;\n          }\n\n          if (!nestedIterator.hasNext()) {\n            return false;\n          }\n\n          try {\n            currentInnerIterator = nestedIterator.next();\n          } catch (Exception e) {\n            // Ensure cleanup on exception\n            closeCloseables(nestedIterator);\n            throw e;\n          }\n        }\n      }\n\n      @Override\n      public T next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        return currentInnerIterator.next();\n      }\n\n      @Override\n      public void close() {\n        // Close both the current inner iterator and the outer iterator\n        // closeCloseables works with null closeable.\n        closeCloseables(currentInnerIterator, nestedIterator);\n      }\n    };\n  }\n\n  /**\n   * Returns the last element from the given {@link CloseableIterator}, if present.\n   *\n   * <p>This method iterates through all elements of the iterator to find the last one. Once\n   * iteration is complete, the iterator is automatically closed to release any underlying\n   * resources.\n   *\n   * @param iterator The iterator to get the last element from\n   * @param <T> The type of elements in the iterator\n   * @return An {@link Optional} containing the last element, or {@link Optional#empty()} if the\n   *     iterator is empty.\n   * @throws UncheckedIOException If an {@link IOException} occurs while closing the iterator.\n   */\n  public static <T> Optional<T> iteratorLast(CloseableIterator<T> iterator) {\n    try (CloseableIterator<T> iter = iterator) {\n      T last = null;\n      while (iter.hasNext()) {\n        last = iter.next();\n      }\n      return Optional.ofNullable(last);\n    } catch (IOException e) {\n      throw new UncheckedIOException(\"Failed to close the CloseableIterator\", e);\n    }\n  }\n\n  public static String resolvePath(Engine engine, String path) {\n    try {\n      return wrapEngineExceptionThrowsIO(\n          () -> engine.getFileSystemClient().resolvePath(path), \"Resolving path %s\", path);\n    } catch (IOException io) {\n      throw new UncheckedIOException(io);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/internal/util/VectorUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.data.GenericColumnVector;\nimport io.delta.kernel.internal.data.StructRow;\nimport io.delta.kernel.types.*;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic final class VectorUtils {\n\n  private VectorUtils() {}\n\n  /**\n   * Converts an {@link ArrayValue} to a Java list. Any nested complex types are also converted to\n   * their Java type.\n   */\n  public static <T> List<T> toJavaList(ArrayValue arrayValue) {\n    final ColumnVector elementVector = arrayValue.getElements();\n    final DataType dataType = elementVector.getDataType();\n\n    List<T> elements = new ArrayList<>();\n    for (int i = 0; i < arrayValue.getSize(); i++) {\n      elements.add((T) getValueAsObject(elementVector, dataType, i));\n    }\n    return elements;\n  }\n\n  /**\n   * Converts a {@link MapValue} to a Java map. Any nested complex types are also converted to their\n   * Java type.\n   *\n   * <p>Please note not all key types override hashCode/equals. Be careful when using with keys of:\n   * - Struct type at any nesting level (i.e. ArrayType(StructType) does not) - Binary type\n   */\n  public static <K, V> Map<K, V> toJavaMap(MapValue mapValue) {\n    final ColumnVector keyVector = mapValue.getKeys();\n    final DataType keyDataType = keyVector.getDataType();\n    final ColumnVector valueVector = mapValue.getValues();\n    final DataType valueDataType = valueVector.getDataType();\n\n    Map<K, V> values = new HashMap<>();\n\n    for (int i = 0; i < mapValue.getSize(); i++) {\n      Object key = getValueAsObject(keyVector, keyDataType, i);\n      Object value = getValueAsObject(valueVector, valueDataType, i);\n      values.put((K) key, (V) value);\n    }\n    return values;\n  }\n\n  /**\n   * Creates a {@link MapValue} from map of string keys and string values. The type {@code\n   * map(string -> string)} is a common occurrence in Delta Log schema.\n   *\n   * @param keyValues\n   * @return\n   */\n  public static MapValue stringStringMapValue(Map<String, String> keyValues) {\n    List<String> keys = new ArrayList<>();\n    List<String> values = new ArrayList<>();\n    for (Map.Entry<String, String> entry : keyValues.entrySet()) {\n      keys.add(entry.getKey());\n      values.add(entry.getValue());\n    }\n    return new MapValue() {\n      @Override\n      public int getSize() {\n        return values.size();\n      }\n\n      @Override\n      public ColumnVector getKeys() {\n        return buildColumnVector(keys, StringType.STRING);\n      }\n\n      @Override\n      public ColumnVector getValues() {\n        return buildColumnVector(values, StringType.STRING);\n      }\n    };\n  }\n\n  /** Creates an {@link ArrayValue} from list of objects. */\n  public static ArrayValue buildArrayValue(List<?> values, DataType dataType) {\n    if (values == null) {\n      return null;\n    }\n    return new ArrayValue() {\n      @Override\n      public int getSize() {\n        return values.size();\n      }\n\n      @Override\n      public ColumnVector getElements() {\n        return buildColumnVector(values, dataType);\n      }\n    };\n  }\n\n  /**\n   * Utility method to create a {@link ColumnVector} for given list of object, the object should be\n   * primitive type or an Row instance.\n   *\n   * @param values list of strings\n   * @return a {@link ColumnVector} with the given values type.\n   */\n  public static ColumnVector buildColumnVector(List<?> values, DataType dataType) {\n    return new GenericColumnVector(values, dataType);\n  }\n\n  /**\n   * Gets the value at {@code rowId} from the column vector. The type of the Object returned depends\n   * on the data type of the column vector. For complex types array and map, returns the value as\n   * Java list or Java map. For struct type, returns an {@link Row}.\n   */\n  public static Object getValueAsObject(ColumnVector columnVector, DataType dataType, int rowId) {\n    if (columnVector.isNullAt(rowId)) {\n      return null;\n    } else if (dataType instanceof BooleanType) {\n      return columnVector.getBoolean(rowId);\n    } else if (dataType instanceof ByteType) {\n      return columnVector.getByte(rowId);\n    } else if (dataType instanceof ShortType) {\n      return columnVector.getShort(rowId);\n    } else if (dataType instanceof IntegerType || dataType instanceof DateType) {\n      // DateType data is stored internally as the number of days since 1970-01-01\n      return columnVector.getInt(rowId);\n    } else if (dataType instanceof LongType || dataType instanceof TimestampType) {\n      // TimestampType data is stored internally as the number of microseconds since the unix\n      // epoch\n      return columnVector.getLong(rowId);\n    } else if (dataType instanceof FloatType) {\n      return columnVector.getFloat(rowId);\n    } else if (dataType instanceof DoubleType) {\n      return columnVector.getDouble(rowId);\n    } else if (dataType instanceof StringType) {\n      return columnVector.getString(rowId);\n    } else if (dataType instanceof BinaryType) {\n      return columnVector.getBinary(rowId);\n    } else if (dataType instanceof StructType) {\n      // TODO are we okay with this usage of StructRow?\n      return StructRow.fromStructVector(columnVector, rowId);\n    } else if (dataType instanceof DecimalType) {\n      return columnVector.getDecimal(rowId);\n    } else if (dataType instanceof ArrayType) {\n      return toJavaList(columnVector.getArray(rowId));\n    } else if (dataType instanceof MapType) {\n      return toJavaMap(columnVector.getMap(rowId));\n    } else {\n      throw new UnsupportedOperationException(\"unsupported data type\");\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/DeltaOperationReport.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport java.util.Optional;\nimport java.util.UUID;\n\n/** Defines the common fields that are shared by reports for Delta operations */\npublic interface DeltaOperationReport extends MetricsReport {\n\n  /** @return the path of the table */\n  String getTablePath();\n\n  /** @return a string representation of the operation this report is for */\n  String getOperationType();\n\n  /** @return a unique ID for this report */\n  UUID getReportUUID();\n\n  /** @return the exception thrown if this report is for a failed operation, otherwise empty */\n  Optional<Exception> getException();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/FileSizeHistogramResult.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\n/** Stores the file size histogram information to track file size distribution and their counts. */\npublic interface FileSizeHistogramResult {\n\n  /**\n   * Sorted list of bin boundaries where each element represents the start of the bin (inclusive)\n   * and the next element represents the end of the bin (exclusive).\n   */\n  long[] getSortedBinBoundaries();\n\n  /**\n   * The total number of files in each bin of {@link\n   * FileSizeHistogramResult#getSortedBinBoundaries()}\n   */\n  long[] getFileCounts();\n\n  /**\n   * The total number of bytes in each bin of {@link\n   * FileSizeHistogramResult#getSortedBinBoundaries()}\n   */\n  long[] getTotalBytes();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/MetricsReport.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\n\n/**\n * Interface containing the metrics for a given operation.\n *\n * <p>Implementations of this interface capture performance metrics and diagnostic information for\n * various Delta table operations. These metrics can be used for monitoring, debugging, and\n * performance analysis.\n */\npublic interface MetricsReport {\n\n  /**\n   * Converts this metrics report to a JSON string representation.\n   *\n   * @return a JSON string representation of this metrics report\n   * @throws JsonProcessingException if the report cannot be serialized to JSON\n   */\n  String toJson() throws JsonProcessingException;\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/ScanMetricsResult.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\n\n/** Stores the metrics results for a {@link ScanReport} */\n@JsonPropertyOrder({\n  \"totalPlanningDurationNs\",\n  \"numAddFilesSeen\",\n  \"numAddFilesSeenFromDeltaFiles\",\n  \"numActiveAddFiles\",\n  \"numDuplicateAddFiles\",\n  \"numRemoveFilesSeenFromDeltaFiles\"\n})\npublic interface ScanMetricsResult {\n\n  /**\n   * Returns the total duration to find, filter, and consume the scan files. This begins at the\n   * request for the scan files and terminates once all the scan files have been consumed and the\n   * scan file iterator closed. It includes reading the _delta_log, log replay, filtering\n   * optimizations, and any work from the connector before closing the scan file iterator.\n   *\n   * @return the total duration to find, filter, and consume the scan files\n   */\n  long getTotalPlanningDurationNs();\n\n  /**\n   * @return the number of AddFile actions seen during log replay (from both checkpoint and delta\n   *     files). For a failed or incomplete scan this metric may be incomplete.\n   */\n  long getNumAddFilesSeen();\n\n  /**\n   * @return the number of AddFile actions seen during log replay from delta files only. For a\n   *     failed or incomplete scan this metric may be incomplete.\n   */\n  long getNumAddFilesSeenFromDeltaFiles();\n\n  /**\n   * @return the number of active AddFile actions that survived log replay (i.e. belong to the table\n   *     state). For a failed or incomplete scan this metric may be incomplete.\n   */\n  long getNumActiveAddFiles();\n\n  /**\n   * Returns the number of duplicate AddFile actions seen during log replay. The same AddFile (same\n   * path and DV) can be present in multiple commit files when stats collection is run on the table.\n   * In this case, the same AddFile will be added with stats without removing the original.\n   *\n   * @return the number of AddFile actions seen during log replay that are duplicates. For a failed\n   *     or incomplete scan this metric may be incomplete.\n   */\n  long getNumDuplicateAddFiles();\n\n  /**\n   * @return the number of RemoveFiles seen in log replay (only from delta files). For a failed or\n   *     incomplete scan this metric may be incomplete.\n   */\n  long getNumRemoveFilesSeenFromDeltaFiles();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/ScanReport.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\nimport java.util.UUID;\n\n/** Defines the metadata and metrics for a Scan {@link MetricsReport} */\n@JsonSerialize(as = ScanReport.class)\n@JsonPropertyOrder({\n  \"tablePath\",\n  \"operationType\",\n  \"reportUUID\",\n  \"exception\",\n  \"tableVersion\",\n  \"tableSchema\",\n  \"snapshotReportUUID\",\n  \"filter\",\n  \"readSchema\",\n  \"partitionPredicate\",\n  \"dataSkippingFilter\",\n  \"isFullyConsumed\",\n  \"scanMetrics\"\n})\npublic interface ScanReport extends DeltaOperationReport {\n\n  /** @return the version of the table in this scan */\n  long getTableVersion();\n\n  /** @return the schema of the table for this scan */\n  StructType getTableSchema();\n\n  /**\n   * @return the {@link SnapshotReport#getReportUUID} for the snapshot this scan was created from\n   */\n  UUID getSnapshotReportUUID();\n\n  /** @return the filter provided when building the scan */\n  Optional<Predicate> getFilter();\n\n  /** @return the read schema provided when building the scan */\n  StructType getReadSchema();\n\n  /** @return the part of {@link ScanReport#getFilter()} that was used for partition pruning */\n  Optional<Predicate> getPartitionPredicate();\n\n  /** @return the filter used for data skipping using the file statistics */\n  Optional<Predicate> getDataSkippingFilter();\n\n  /**\n   * Whether the scan file iterator had been fully consumed when it was closed. The iterator may be\n   * closed early (before being fully consumed) either due to an exception originating within\n   * connector code or intentionally (such as for a LIMIT query).\n   *\n   * @return whether the scan file iterator had been fully consumed when it was closed\n   */\n  boolean getIsFullyConsumed();\n\n  /** @return the metrics for this scan */\n  ScanMetricsResult getScanMetrics();\n\n  @Override\n  default String getOperationType() {\n    return \"Scan\";\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/SnapshotMetricsResult.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport java.util.Optional;\n\n/** Stores the metrics results for a {@link SnapshotReport} */\n@JsonPropertyOrder({\n  \"computeTimestampToVersionTotalDurationNs\",\n  \"loadSnapshotTotalDurationNs\",\n  \"loadProtocolMetadataTotalDurationNs\",\n  \"loadLogSegmentTotalDurationNs\",\n  \"loadCrcTotalDurationNs\"\n})\npublic interface SnapshotMetricsResult {\n\n  /**\n   * @return the duration (ns) to resolve the provided timestamp to a table version for timestamp\n   *     time-travel queries. Empty for time-travel by version or non-time-travel queries.\n   */\n  Optional<Long> getComputeTimestampToVersionTotalDurationNs();\n\n  /**\n   * @return the total duration (ns) to load the snapshot, including all steps such as resolving\n   *     timestamp to version, LISTing the _delta_log, building the log segment, and determining the\n   *     latest protocol and metadata.\n   */\n  long getLoadSnapshotTotalDurationNs();\n\n  /**\n   * @return the duration (ns) to load the initial delta actions for the snapshot (such as the table\n   *     protocol and metadata). 0 if snapshot construction fails before log replay.\n   */\n  long getLoadProtocolMetadataTotalDurationNs();\n\n  /**\n   * @return the duration (ns) to build the log segment for the specified version during snapshot\n   *     construction. 0 if snapshot construction fails before this step.\n   */\n  long getLoadLogSegmentTotalDurationNs();\n\n  /**\n   * @return the duration (ns) to get CRC information during snapshot construction. 0 if snapshot\n   *     construction fails before this step or if CRC is not read in loading snapshot.\n   */\n  long getLoadCrcTotalDurationNs();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/SnapshotReport.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport java.util.Optional;\n\n/** Defines the metadata and metrics for a snapshot construction {@link MetricsReport} */\n@JsonSerialize(as = SnapshotReport.class)\n@JsonPropertyOrder({\n  \"tablePath\",\n  \"operationType\",\n  \"reportUUID\",\n  \"exception\",\n  \"version\",\n  \"checkpointVersion\",\n  \"providedTimestamp\",\n  \"snapshotMetrics\"\n})\npublic interface SnapshotReport extends DeltaOperationReport {\n\n  /**\n   * For a time-travel by version query, this is the version provided. For a time-travel by\n   * timestamp query, this is the version resolved from the provided timestamp. For a latest\n   * snapshot, this is the version read from the delta log.\n   *\n   * <p>This is empty when this report is for a failed snapshot construction, and the error occurs\n   * before a version can be resolved.\n   *\n   * @return the version of the snapshot\n   */\n  Optional<Long> getVersion();\n\n  /**\n   * @return the timestamp provided for time-travel, empty if this is not a timestamp-based\n   *     time-travel query\n   */\n  Optional<Long> getProvidedTimestamp();\n\n  /**\n   * @return the version of the checkpoint used for this snapshot, empty if no checkpoint was used\n   *     or if this is a failed snapshot construction\n   */\n  Optional<Long> getCheckpointVersion();\n\n  /** @return the metrics for this snapshot construction */\n  SnapshotMetricsResult getSnapshotMetrics();\n\n  @Override\n  default String getOperationType() {\n    return \"Snapshot\";\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/TransactionMetricsResult.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport java.util.Optional;\n\n/** Stores the metrics results for a {@link TransactionReport} */\n@JsonPropertyOrder({\n  \"totalCommitDurationNs\",\n  \"numCommitAttempts\",\n  \"numAddFiles\",\n  \"numRemoveFiles\",\n  \"numTotalActions\",\n  \"totalAddFilesSizeInBytes\",\n  \"totalRemoveFilesSizeInBytes\"\n})\npublic interface TransactionMetricsResult {\n\n  /** @return the total duration (ns) this transaction spent committing or trying to commit */\n  long getTotalCommitDurationNs();\n\n  /** @return the total number of commit attempts this transaction made */\n  long getNumCommitAttempts();\n\n  /**\n   * @return the number of add files committed in this transaction. For a failed transaction this\n   *     metric may be incomplete.\n   */\n  long getNumAddFiles();\n\n  /**\n   * @return the number of remove files committed in this transaction. For a failed transaction this\n   *     metric may be incomplete.\n   */\n  long getNumRemoveFiles();\n\n  /**\n   * @return the total number of delta actions committed in this transaction. For a failed\n   *     transaction this metric may be incomplete.\n   */\n  long getNumTotalActions();\n\n  /**\n   * @return the sum of size of added files committed in this transaction. For a failed transaction\n   *     this metric may be incomplete.\n   */\n  long getTotalAddFilesSizeInBytes();\n\n  /**\n   * @return the sum of size of removed files committed in this transaction. For a failed\n   *     transaction this metric may be incomplete.\n   */\n  long getTotalRemoveFilesSizeInBytes();\n\n  /**\n   * @return the file size histogram information for the table version committed in this\n   *     transaction. For a failed transaction this metric may be incomplete.\n   */\n  @JsonIgnore\n  Optional<FileSizeHistogramResult> getTableFileSizeHistogram();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/metrics/TransactionReport.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.metrics;\n\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport io.delta.kernel.expressions.Column;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\n\n/** Defines the metadata and metrics for a transaction {@link MetricsReport} */\n@JsonSerialize(as = TransactionReport.class)\n@JsonPropertyOrder({\n  \"tablePath\",\n  \"operationType\",\n  \"reportUUID\",\n  \"exception\",\n  \"operation\",\n  \"engineInfo\",\n  \"baseSnapshotVersion\",\n  \"snapshotReportUUID\",\n  \"committedVersion\",\n  \"clusteringColumns\",\n  \"transactionMetrics\"\n})\npublic interface TransactionReport extends DeltaOperationReport {\n\n  /**\n   * @return The {@link io.delta.kernel.Operation} provided when the transaction was created using\n   *     {@link io.delta.kernel.Table#createTransactionBuilder}.\n   */\n  String getOperation();\n\n  /**\n   * @return The engineInfo provided when the transaction was created using {@link\n   *     io.delta.kernel.Table#createTransactionBuilder}.\n   */\n  String getEngineInfo();\n\n  /**\n   * The version of the table the transaction was created from. For example, if the latest table\n   * version is 4 when the transaction is created, the transaction is based off of the snapshot of\n   * the table at version 4. For a new table (e.g. a transaction that is creating a table) this is\n   * -1.\n   *\n   * @return the table version of the snapshot the transaction was started from\n   */\n  long getBaseSnapshotVersion();\n\n  /**\n   * Get the list of clustering columns the table data is expected to be clustered by. This is\n   * optional because clustering columns are not always defined for a table. Consumers of the\n   * transaction report trigger clustering operations based on this list.\n   *\n   * @return list of clustering columns for the table. The columns are physical names of how the\n   *     data is written in the data files. Each column can contain one or more elements\n   *     representing the hierarchy of the column names in case of nested columns.\n   */\n  List<Column> getClusteringColumns();\n\n  /**\n   * @return the {@link SnapshotReport#getReportUUID} of the SnapshotReport for the transaction's\n   *     snapshot construction. Empty for a new table transaction.\n   */\n  Optional<UUID> getSnapshotReportUUID();\n\n  /**\n   * @return the version committed to the table in this transaction. Empty for a failed transaction.\n   */\n  Optional<Long> getCommittedVersion();\n\n  /** @return the metrics for this transaction */\n  TransactionMetricsResult getTransactionMetrics();\n\n  @Override\n  default String getOperationType() {\n    return \"Transaction\";\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Delta Kernel interfaces for constructing table object representing a Delta Lake table, getting\n * snapshot from the table and building a scan object to scan a subset of the data in the table.\n */\npackage io.delta.kernel;\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/statistics/DataFileStatistics.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.statistics;\n\nimport static io.delta.kernel.internal.DeltaErrors.unsupportedStatsDataType;\nimport static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.DeltaErrors;\nimport io.delta.kernel.internal.skipping.StatsSchemaHelper;\nimport io.delta.kernel.internal.util.JsonUtils;\nimport io.delta.kernel.types.*;\nimport java.io.IOException;\nimport java.math.BigDecimal;\nimport java.nio.charset.StandardCharsets;\nimport java.time.*;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\n\n/**\n * Encapsulates statistics for a data file in a Delta Lake table and provides methods to serialize\n * those stats to JSON with basic physical-type validation. Note that connectors (e.g. Spark, Flink)\n * are responsible for ensuring the correctness of collected stats, including any necessary string\n * truncation, prior to constructing this class.\n */\npublic class DataFileStatistics {\n  public static final DateTimeFormatter TIMESTAMP_FORMATTER =\n      DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSSX\");\n  public static final OffsetDateTime EPOCH = Instant.ofEpochSecond(0).atOffset(ZoneOffset.UTC);\n\n  private final long numRecords;\n  private final Map<Column, Literal> minValues;\n  private final Map<Column, Literal> maxValues;\n  private final Map<Column, Long> nullCount;\n  private final Optional<Boolean> tightBounds;\n\n  /**\n   * Create a new instance of {@link DataFileStatistics}. The minValues, maxValues, and nullCount\n   * are required fields. The tightBounds field is optional - pass Optional.empty() if not\n   * specified, Optional.of(true) or Optional.of(false) for explicit values.\n   *\n   * @param numRecords Number of records in the data file.\n   * @param minValues Map of column to minimum value of it in the data file. If the data file has\n   *     all nulls for the column, the value will be null or not present in the map.\n   * @param maxValues Map of column to maximum value of it in the data file. If the data file has\n   *     all nulls for the column, the value will be null or not present in the map.\n   * @param nullCount Map of column to number of nulls in the data file.\n   * @param tightBounds Optional boolean indicating if bounds are tight (accurate). Pass\n   *     Optional.empty() if not specified.\n   */\n  public DataFileStatistics(\n      long numRecords,\n      Map<Column, Literal> minValues,\n      Map<Column, Literal> maxValues,\n      Map<Column, Long> nullCount,\n      Optional<Boolean> tightBounds) {\n    Objects.requireNonNull(minValues, \"minValues must not be null to serialize stats.\");\n    Objects.requireNonNull(maxValues, \"maxValues must not be null to serialize stats.\");\n    Objects.requireNonNull(nullCount, \"nullCount must not be null to serialize stats.\");\n\n    this.numRecords = numRecords;\n    this.minValues = Collections.unmodifiableMap(minValues);\n    this.maxValues = Collections.unmodifiableMap(maxValues);\n    this.nullCount = Collections.unmodifiableMap(nullCount);\n    this.tightBounds = tightBounds;\n  }\n\n  /**\n   * Utility method to extract only the numRecords field from a statistics JSON string.\n   *\n   * @param json Data statistics JSON string to deserialize.\n   * @return An {@link Optional} containing the numRecords value if present.\n   * @throws KernelException if JSON parsing fails\n   */\n  public static Optional<Long> getNumRecords(String json) {\n    // Delegate to the full deserialization method with null schema to only parse numRecords\n    Optional<DataFileStatistics> stats = deserializeFromJson(json, null);\n    return stats.map(DataFileStatistics::getNumRecords);\n  }\n\n  /**\n   * Utility method to deserialize statistics from a JSON string with full type information. This\n   * overloaded version uses the provided schema to correctly parse min/max values and null counts\n   * with their appropriate data types.\n   *\n   * @param json Data statistics JSON string to deserialize.\n   * @param physicalSchema The physical schema providing type information for columns. Must match\n   *     the schema used during serialization.\n   * @return An {@link Optional} containing the deserialized {@link DataFileStatistics} if present.\n   * @throws KernelException if JSON parsing fails or if values don't match expected types\n   */\n  public static Optional<DataFileStatistics> deserializeFromJson(\n      String json, StructType physicalSchema) {\n    JsonNode root;\n    try {\n      root = JsonUtils.mapper().readTree(json);\n    } catch (IOException e) {\n      throw new KernelException(String.format(\"Failed to parse JSON string: %s\", json), e);\n    }\n\n    // Parse numRecords\n    JsonNode numRecordsNode = root.get(StatsSchemaHelper.NUM_RECORDS);\n    if (numRecordsNode == null || !numRecordsNode.isNumber()) {\n      return Optional.empty();\n    }\n    long numRecords = numRecordsNode.asLong();\n    // Check if schema is null or empty\n    if (physicalSchema == null || physicalSchema.fields().isEmpty()) {\n      // Return statistics with only numRecords\n      return Optional.of(\n          new DataFileStatistics(\n              numRecords, new HashMap<>(), new HashMap<>(), new HashMap<>(), Optional.empty()));\n    }\n    // Parse minValues\n    Map<Column, Literal> minValues = new HashMap<>();\n    JsonNode minNode = root.get(StatsSchemaHelper.MIN);\n    if (minNode != null && minNode.isObject()) {\n      parseMinMaxValues(minNode, minValues, new Column(new String[0]), physicalSchema);\n    }\n\n    // Parse maxValues\n    Map<Column, Literal> maxValues = new HashMap<>();\n    JsonNode maxNode = root.get(StatsSchemaHelper.MAX);\n    if (maxNode != null && maxNode.isObject()) {\n      parseMinMaxValues(maxNode, maxValues, new Column(new String[0]), physicalSchema);\n    }\n\n    // Parse nullCount\n    Map<Column, Long> nullCount = new HashMap<>();\n    JsonNode nullCountNode = root.get(StatsSchemaHelper.NULL_COUNT);\n    if (nullCountNode != null && nullCountNode.isObject()) {\n      parseNullCounts(nullCountNode, nullCount, new Column(new String[0]), physicalSchema);\n    }\n\n    // Parse tightBounds\n    Optional<Boolean> tightBounds = Optional.empty();\n    JsonNode tightBoundsNode = root.get(StatsSchemaHelper.TIGHT_BOUNDS);\n    if (tightBoundsNode != null && tightBoundsNode.isBoolean()) {\n      tightBounds = Optional.of(tightBoundsNode.asBoolean());\n    }\n\n    return Optional.of(\n        new DataFileStatistics(numRecords, minValues, maxValues, nullCount, tightBounds));\n  }\n\n  /**\n   * Get the number of records in the data file.\n   *\n   * @return Number of records in the data file.\n   */\n  public long getNumRecords() {\n    return numRecords;\n  }\n\n  /**\n   * Get the minimum values of the columns in the data file. The map may contain statistics for only\n   * a subset of columns in the data file.\n   *\n   * @return Map of column to minimum value of it in the data file.\n   */\n  public Map<Column, Literal> getMinValues() {\n    return minValues;\n  }\n\n  /**\n   * Get the maximum values of the columns in the data file. The map may contain statistics for only\n   * a subset of columns in the data file.\n   *\n   * @return Map of column to minimum value of it in the data file.\n   */\n  public Map<Column, Literal> getMaxValues() {\n    return maxValues;\n  }\n\n  /**\n   * Get the number of nulls of columns in the data file. The map may contain statistics for only a\n   * subset of columns in the data file.\n   *\n   * @return Map of column to number of nulls in the data file.\n   */\n  public Map<Column, Long> getNullCount() {\n    return nullCount;\n  }\n\n  /**\n   * Get the tight bounds information for the data file. Tight bounds indicate whether the values\n   * are guaranteed to be accurate bounds for the data.\n   *\n   * @return The tight bounds boolean value.\n   */\n  public Optional<Boolean> getTightBounds() {\n    return tightBounds;\n  }\n\n  /**\n   * Returns a new DataFileStatistics instance with tightBounds set to false. This is useful when\n   * the statistics bounds are no longer guaranteed to be tight, such as after applying deletion\n   * vectors.\n   *\n   * @return A new DataFileStatistics with tightBounds set to false\n   */\n  public DataFileStatistics withoutTightBounds() {\n    return new DataFileStatistics(\n        this.numRecords, this.minValues, this.maxValues, this.nullCount, Optional.of(false));\n  }\n\n  /**\n   * Serializes the statistics as a JSON string.\n   *\n   * <p>Example: For nested column structures:\n   *\n   * <pre>\n   * Input:\n   *   minValues = {\n   *     new Column(new String[]{\"a\", \"b\", \"c\"}) mapped to Literal.ofInt(10),\n   *     new Column(\"d\") mapped to Literal.ofString(\"value\")\n   *   }\n   *\n   * Output JSON:\n   *   {\n   *     \"minValues\": {\n   *       \"a\": {\n   *         \"b\": {\n   *           \"c\": 10\n   *         }\n   *       },\n   *       \"d\": \"value\"\n   *     }\n   *   }\n   * </pre>\n   *\n   * @param physicalSchema the optional physical schema. If provided, all min/max values and null\n   *     counts will be included and validated against their physical types. If null, only\n   *     numRecords will be serialized without validation.\n   * @return a JSON representation of the statistics.\n   * @throws KernelException if dataSchema is provided and there's a type mismatch between the\n   *     Literal values and the expected types in the schema, or if an unsupported data type is\n   *     found.\n   */\n  public String serializeAsJson(StructType physicalSchema) {\n    return JsonUtils.generate(\n        gen -> {\n          gen.writeStartObject();\n          gen.writeNumberField(StatsSchemaHelper.NUM_RECORDS, numRecords);\n\n          if (physicalSchema != null) {\n            gen.writeObjectFieldStart(StatsSchemaHelper.MIN);\n            writeJsonValues(\n                gen,\n                physicalSchema,\n                minValues,\n                new Column(new String[0]),\n                (g, v) -> writeJsonValue(g, v));\n            gen.writeEndObject();\n\n            gen.writeObjectFieldStart(StatsSchemaHelper.MAX);\n            writeJsonValues(\n                gen,\n                physicalSchema,\n                maxValues,\n                new Column(new String[0]),\n                (g, v) -> writeJsonValue(g, v));\n            gen.writeEndObject();\n\n            gen.writeObjectFieldStart(StatsSchemaHelper.NULL_COUNT);\n            writeJsonValues(\n                gen,\n                physicalSchema,\n                nullCount,\n                new Column(new String[0]),\n                (g, v) -> g.writeNumber(v));\n            gen.writeEndObject();\n\n            if (tightBounds.isPresent()) {\n              gen.writeBooleanField(StatsSchemaHelper.TIGHT_BOUNDS, tightBounds.get());\n            }\n          }\n\n          gen.writeEndObject();\n        });\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (!(o instanceof DataFileStatistics)) {\n      return false;\n    }\n    DataFileStatistics that = (DataFileStatistics) o;\n    return numRecords == that.numRecords\n        && Objects.equals(minValues, that.minValues)\n        && Objects.equals(maxValues, that.maxValues)\n        && Objects.equals(nullCount, that.nullCount)\n        && Objects.equals(tightBounds, that.tightBounds);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = Long.hashCode(numRecords);\n    result = 31 * result + Objects.hash(minValues.keySet());\n    result = 31 * result + Objects.hash(maxValues.keySet());\n    result = 31 * result + Objects.hash(nullCount.keySet());\n    result = 31 * result + Objects.hash(tightBounds);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"DataFileStatistics(numRecords=%s, minValues=%s, maxValues=%s,\"\n            + \"nullCount=%s, tightBounds=%s)\",\n        numRecords,\n        minValues,\n        maxValues,\n        nullCount,\n        tightBounds.map(Object::toString).orElse(\"empty\"));\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////\n  /// Private methods                                                           ///\n  /////////////////////////////////////////////////////////////////////////////////\n\n  private <T> void writeJsonValues(\n      JsonGenerator generator,\n      StructType schema,\n      Map<Column, T> values,\n      Column parentCol,\n      JsonUtils.JsonValueWriter<T> writer)\n      throws IOException {\n    if (schema == null) {\n      return;\n    }\n    for (StructField field : schema.fields()) {\n      Column colPath = parentCol.appendNestedField(field.getName());\n      if (field.getDataType() instanceof StructType) {\n        generator.writeObjectFieldStart(field.getName());\n        writeJsonValues(generator, (StructType) field.getDataType(), values, colPath, writer);\n        generator.writeEndObject();\n      } else {\n        T value = values.get(colPath);\n        if (value != null) {\n          if (value instanceof Literal) {\n            validateLiteralType(field, (Literal) value);\n          }\n          generator.writeFieldName(field.getName());\n          writer.write(generator, value);\n        }\n      }\n    }\n  }\n\n  /**\n   * Validates that the literal's data type matches the expected field type.\n   *\n   * @param field The schema field with the expected data type\n   * @param literal The literal to validate\n   * @throws KernelException if the data types don't match\n   */\n  private void validateLiteralType(StructField field, Literal literal) {\n    // Variant stats in JSON are Z85 encoded strings, all other stats should match the field type\n    DataType expectedLiteralType =\n        field.getDataType() instanceof VariantType ? StringType.STRING : field.getDataType();\n    if (literal.getDataType() == null\n        || !expectedLiteralType.isWriteCompatible(literal.getDataType())) {\n      throw DeltaErrors.statsTypeMismatch(\n          field.getName(), expectedLiteralType, literal.getDataType());\n    }\n  }\n\n  private void writeJsonValue(JsonGenerator generator, Literal literal) throws IOException {\n    if (literal == null || literal.getValue() == null) {\n      generator.writeNull();\n      return;\n    }\n    DataType type = literal.getDataType();\n    Object value = literal.getValue();\n    if (type instanceof BooleanType) {\n      generator.writeBoolean((Boolean) value);\n    } else if (type instanceof ByteType) {\n      generator.writeNumber(((Number) value).byteValue());\n    } else if (type instanceof ShortType) {\n      generator.writeNumber(((Number) value).shortValue());\n    } else if (type instanceof IntegerType) {\n      generator.writeNumber(((Number) value).intValue());\n    } else if (type instanceof LongType) {\n      generator.writeNumber(((Number) value).longValue());\n    } else if (type instanceof FloatType) {\n      float f = ((Number) value).floatValue();\n      if (Float.isNaN(f) || Float.isInfinite(f)) {\n        generator.writeString(String.valueOf(f));\n      } else {\n        generator.writeNumber(f);\n      }\n    } else if (type instanceof DoubleType) {\n      double d = ((Number) value).doubleValue();\n      if (Double.isNaN(d) || Double.isInfinite(d)) {\n        generator.writeString(String.valueOf(d));\n      } else {\n        generator.writeNumber(d);\n      }\n    } else if (type instanceof StringType) {\n      generator.writeString((String) value);\n    } else if (type instanceof BinaryType) {\n      generator.writeString(new String((byte[]) value, StandardCharsets.UTF_8));\n    } else if (type instanceof DecimalType) {\n      generator.writeNumber((BigDecimal) value);\n    } else if (type instanceof DateType) {\n      generator.writeString(\n          LocalDate.ofEpochDay(((Number) value).longValue()).format(ISO_LOCAL_DATE));\n    } else if (type instanceof TimestampType) {\n      long epochMicros = (long) value;\n      LocalDateTime localDateTime = ChronoUnit.MICROS.addTo(EPOCH, epochMicros).toLocalDateTime();\n      LocalDateTime truncated = localDateTime.truncatedTo(ChronoUnit.MILLIS);\n      generator.writeString(TIMESTAMP_FORMATTER.format(truncated.atOffset(ZoneOffset.UTC)));\n    } else if (type instanceof TimestampNTZType) {\n      long epochMicros = (long) value;\n      LocalDateTime localDateTime = ChronoUnit.MICROS.addTo(EPOCH, epochMicros).toLocalDateTime();\n      LocalDateTime truncated = localDateTime.truncatedTo(ChronoUnit.MILLIS);\n      generator.writeString(truncated.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));\n    } else {\n      throw unsupportedStatsDataType(type);\n    }\n  }\n  /**\n   * Helper method to recursively parse nested JSON values back into Column->Literal maps for\n   * min/max values using the schema for type information. This is the inverse of the\n   * writeJsonValues method in DataFileStatistics.serializeAsJson.\n   *\n   * <p>Example JSON structure being parsed:\n   *\n   * <pre>\n   * {\n   *   \"simpleColumn\": 42,\n   *   \"nestedColumn\": {\n   *     \"field1\": \"value1\",\n   *     \"field2\": {\n   *       \"subfield\": 10.5\n   *     }\n   *   }\n   * }\n   * </pre>\n   *\n   * <p>This would create Column entries:\n   *\n   * <ul>\n   *   <li>Column([\"simpleColumn\"]) → Literal.ofInt(42)\n   *   <li>Column([\"nestedColumn\", \"field1\"]) → Literal.ofString(\"value1\")\n   *   <li>Column([\"nestedColumn\", \"field2\", \"subfield\"]) → Literal.ofDouble(10.5)\n   * </ul>\n   *\n   * @param node The JSON node to parse\n   * @param resultMap The map to populate with Column->Literal mappings\n   * @param currentColumn The current column path being built\n   * @param schema The schema for the current level\n   */\n  private static void parseMinMaxValues(\n      JsonNode node, Map<Column, Literal> resultMap, Column currentColumn, StructType schema) {\n    if (node == null || !node.isObject() || schema == null) {\n      return;\n    }\n\n    for (StructField field : schema.fields()) {\n      String fieldName = field.getName();\n      JsonNode valueNode = node.get(fieldName);\n\n      if (valueNode == null) {\n        // Field not present in JSON, skip\n        continue;\n      }\n\n      Column newColumn = currentColumn.appendNestedField(fieldName);\n      DataType fieldType = field.getDataType();\n\n      if (fieldType instanceof StructType) {\n        // This is a nested structure, recurse deeper\n        if (valueNode.isObject()) {\n          parseMinMaxValues(valueNode, resultMap, newColumn, (StructType) fieldType);\n        }\n      } else {\n        // This is a leaf value\n        Literal literal = JsonUtils.parseJsonValueToLiteral(valueNode, fieldType);\n        if (literal != null) {\n          resultMap.put(newColumn, literal);\n        }\n      }\n    }\n  }\n\n  /**\n   * Helper method to recursively parse nested JSON null count values back into Column->Long maps\n   * using the schema for type information.\n   *\n   * <p>Example JSON structure being parsed:\n   *\n   * <pre>\n   * {\n   *   \"simpleColumn\": 5,\n   *   \"nestedColumn\": {\n   *     \"field1\": 0,\n   *     \"field2\": {\n   *       \"subfield\": 10\n   *     }\n   *   }\n   * }\n   * </pre>\n   *\n   * <p>This would create Column entries:\n   *\n   * <ul>\n   *   <li>Column([\"simpleColumn\"]) → 5L\n   *   <li>Column([\"nestedColumn\", \"field1\"]) → 0L\n   *   <li>Column([\"nestedColumn\", \"field2\", \"subfield\"]) → 10L\n   * </ul>\n   *\n   * @param node The JSON node to parse\n   * @param resultMap The map to populate with Column->Long mappings\n   * @param currentColumn The current column path being built\n   * @param schema The schema for the current level\n   */\n  private static void parseNullCounts(\n      JsonNode node, Map<Column, Long> resultMap, Column currentColumn, StructType schema) {\n    if (node == null || !node.isObject() || schema == null) {\n      return;\n    }\n\n    for (StructField field : schema.fields()) {\n      String fieldName = field.getName();\n      JsonNode valueNode = node.get(fieldName);\n\n      if (valueNode == null) {\n        // Field not present in JSON, skip\n        continue;\n      }\n\n      Column newColumn = currentColumn.appendNestedField(fieldName);\n      DataType fieldType = field.getDataType();\n\n      if (fieldType instanceof StructType) {\n        // This is a nested structure, recurse deeper\n        if (valueNode.isObject()) {\n          parseNullCounts(valueNode, resultMap, newColumn, (StructType) fieldType);\n        }\n      } else {\n        // This is a leaf value - parse as long for null count\n        if (valueNode.isNumber()) {\n          resultMap.put(newColumn, valueNode.asLong());\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/statistics/SnapshotStatistics.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.statistics;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Optional;\n\n/** Provides statistics and metadata about a {@link Snapshot}. */\n@Evolving\npublic interface SnapshotStatistics {\n\n  /**\n   * Determines the appropriate mode for writing a checksum file for this Snapshot.\n   *\n   * <p>The returned value can be passed to {@link Snapshot#writeChecksum} to write the checksum\n   * file using the most efficient approach available:\n   *\n   * <ul>\n   *   <li>{@link Optional#empty()} - Checksum already exists, no write needed\n   *   <li>{@link Optional} of {@link Snapshot.ChecksumWriteMode#SIMPLE} - Fast write using\n   *       in-memory CRC info\n   *   <li>{@link Optional} of {@link Snapshot.ChecksumWriteMode#FULL} - Requires log replay to\n   *       compute CRC info\n   * </ul>\n   *\n   * @return the recommended checksum write mode, or empty if checksum already exists\n   */\n  Optional<Snapshot.ChecksumWriteMode> getChecksumWriteMode();\n\n  // TODO: getNumUnpublishedCatalogCommits\n  // TODO: getNumDeltasSinceLastCheckpoint\n  // TODO: getCheckpointInterval\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/transaction/CreateTableTransactionBuilder.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.transaction;\n\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.utils.CloseableIterable;\nimport java.util.Map;\n\n/**\n * Builder for creating a {@link Transaction} to create a new Delta table.\n *\n * @since 3.4.0\n */\n@Evolving\npublic interface CreateTableTransactionBuilder {\n\n  /**\n   * Set table properties for the new Delta table.\n   *\n   * <p>This method can be called multiple times to add additional properties. If a property key\n   * already exists from a previous call with a different value, an {@link IllegalArgumentException}\n   * will be thrown.\n   *\n   * <p>Note, user-properties (those without a '.delta' prefix) are case-sensitive. Delta-properties\n   * are case-insensitive and are normalized to their expected case before writing to the log.\n   *\n   * @param properties A map of table property names to their values.\n   * @throws IllegalArgumentException if any property key already exists from a previous call with a\n   *     different value.\n   */\n  CreateTableTransactionBuilder withTableProperties(Map<String, String> properties);\n\n  /**\n   * Set the data layout specification for the new Delta table.\n   *\n   * <p>The data layout specification determines how data files are organized within the table, such\n   * as partitioning and clustering strategies.\n   *\n   * <p>The default, if not specified in the builder, is unpartitioned.\n   *\n   * @param spec The data layout specification.\n   * @see DataLayoutSpec\n   */\n  CreateTableTransactionBuilder withDataLayoutSpec(DataLayoutSpec spec);\n\n  /**\n   * Set the maximum number of retries to retry the commit in the case of a retryable error.\n   *\n   * @param maxRetries The maximum number of retries. Must be at least 0. Default is 200.\n   */\n  CreateTableTransactionBuilder withMaxRetries(int maxRetries);\n\n  /**\n   * Provides a custom committer to use at transaction commit time.\n   *\n   * <p>Catalog implementations that wish to support the catalogManaged Delta table feature should\n   * provide to engines their own catalog-specific Committer implementation which may, for example,\n   * send a commit RPC to the catalog service to finalize the commit.\n   *\n   * <p>If no committer is provided, a default committer will be created that only supports writing\n   * into filesystem-managed Delta tables.\n   *\n   * @param committer the committer to use\n   * @return a new builder instance with the provided committer\n   * @see Committer\n   */\n  CreateTableTransactionBuilder withCommitter(Committer committer);\n\n  /**\n   * Build the transaction for creating the Delta table.\n   *\n   * <p>This validates all the configuration and creates a {@link Transaction} that can be used to\n   * create the new Delta table. The transaction must be committed using {@link\n   * Transaction#commit(Engine, CloseableIterable)} to actually create the table.\n   *\n   * @param engine The {@link Engine} instance to use.\n   * @return A configured {@link Transaction} for creating the table.\n   */\n  Transaction build(Engine engine);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/transaction/DataLayoutSpec.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.transaction;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.expressions.Column;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * Specification for the data layout of a Delta table, including partitioning and clustering\n * configurations.\n *\n * <p>This class supports different layout strategies:\n *\n * <ul>\n *   <li><b>No data layout spec:</b> No special data layout\n *   <li><b>Partitioned:</b> Data is partitioned by specified columns.\n *   <li><b>Clustered:</b> Data is clustered by specified columns.\n * </ul>\n *\n * @since 3.4.0\n */\n@Evolving\npublic class DataLayoutSpec {\n\n  /**\n   * Creates data layout spec with no special layout.\n   *\n   * @return A new {@link DataLayoutSpec} with no special layout.\n   */\n  public static DataLayoutSpec noDataLayout() {\n    return new DataLayoutSpec(null, null);\n  }\n\n  /**\n   * Creates a data layout specification for a partitioned table.\n   *\n   * @param partitionColumns The columns to partition by. Cannot be null or empty. Only top-level\n   *     columns are supported for partitioning.\n   * @return A new {@link DataLayoutSpec} for a partitioned table.\n   */\n  public static DataLayoutSpec partitioned(List<Column> partitionColumns) {\n    checkArgument(\n        partitionColumns != null && !partitionColumns.isEmpty(),\n        \"Partition columns cannot be null or empty\");\n    checkArgument(\n        partitionColumns.stream().allMatch(col -> col.getNames().length == 1),\n        \"Partition columns must be only top-level columns\");\n    return new DataLayoutSpec(partitionColumns, null);\n  }\n\n  /**\n   * Creates a data layout specification for a clustered table.\n   *\n   * @param clusteringColumns The columns to cluster by. Cannot be null, but can be empty to\n   *     indicate clustering is enabled without specific column definitions.\n   * @return A new {@link DataLayoutSpec} for a clustered table.\n   */\n  public static DataLayoutSpec clustered(List<Column> clusteringColumns) {\n    checkArgument(\n        clusteringColumns != null, \"Clustering columns cannot be null (but can be empty)\");\n    return new DataLayoutSpec(null, clusteringColumns);\n  }\n\n  private final List<Column> partitionColumns;\n  private final List<Column> clusteringColumns;\n\n  private DataLayoutSpec(List<Column> partitionColumns, List<Column> clusteringColumns) {\n    if (partitionColumns != null && clusteringColumns != null) {\n      throw new IllegalArgumentException(\"Cannot specify both partition and clustering columns\");\n    }\n    if (partitionColumns != null && partitionColumns.isEmpty()) {\n      throw new IllegalArgumentException(\"Partition columns cannot be empty\");\n    }\n    this.partitionColumns = partitionColumns;\n    this.clusteringColumns = clusteringColumns;\n  }\n\n  /**\n   * Returns true if this layout specification includes partitioning.\n   *\n   * <p>Partitioning requires non-empty partition columns. An empty list of partition columns is not\n   * considered valid partitioning.\n   */\n  public boolean hasPartitioning() {\n    return partitionColumns != null && !partitionColumns.isEmpty();\n  }\n\n  /**\n   * Returns true if this layout specification includes clustering.\n   *\n   * <p>Clustering can be enabled even with empty clustering columns, which indicates that\n   * clustering is enabled on the table but no specific columns are defined yet.\n   */\n  public boolean hasClustering() {\n    return clusteringColumns != null;\n  }\n\n  /**\n   * Returns true if this is a data layout spec with no special layout.\n   *\n   * <p>This means it has neither partitioning nor clustering enabled.\n   */\n  public boolean hasNoDataLayoutSpec() {\n    return !hasPartitioning() && !hasClustering();\n  }\n\n  /**\n   * Returns the partition columns for this layout.\n   *\n   * @throws IllegalStateException if partitioning is not enabled on this layout. Use {@link\n   *     #hasPartitioning()} to check first.\n   */\n  public List<Column> getPartitionColumns() {\n    if (!hasPartitioning()) {\n      throw new IllegalStateException(\n          \"Cannot get partition columns: partitioning is not enabled on this layout\");\n    }\n    return Collections.unmodifiableList(partitionColumns);\n  }\n\n  /**\n   * Returns the partition columns for this layout as strings.\n   *\n   * @throws IllegalStateException if partitioning is not enabled on this layout. Use {@link\n   *     #hasPartitioning()} to check first.\n   */\n  public List<String> getPartitionColumnsAsStrings() {\n    return getPartitionColumns().stream()\n        .map(col -> col.getNames()[0])\n        .collect(Collectors.toList());\n  }\n\n  /**\n   * Returns the clustering columns for this layout.\n   *\n   * <p>The returned list may be empty if clustering is enabled but no specific columns are defined.\n   *\n   * @throws IllegalStateException if clustering is not enabled on this layout. Use {@link\n   *     #hasClustering()} to check first.\n   */\n  public List<Column> getClusteringColumns() {\n    if (!hasClustering()) {\n      throw new IllegalStateException(\n          \"Cannot get clustering columns: clustering is not enabled on this layout\");\n    }\n    return Collections.unmodifiableList(clusteringColumns);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/transaction/ReplaceTableTransactionBuilder.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.transaction;\n\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\nimport java.util.Map;\n\n/**\n * Builds a {@link Transaction} to replace an existing Delta table.\n *\n * <p>Replace table creates a new table definition (schema, properties, layout) and atomically\n * replaces the existing table.\n *\n * @since 3.4.0\n */\n@Evolving\npublic interface ReplaceTableTransactionBuilder {\n\n  /**\n   * Set table properties for the new table definition.\n   *\n   * @param properties A map of table property names to their values.\n   */\n  ReplaceTableTransactionBuilder withTableProperties(Map<String, String> properties);\n\n  /**\n   * Set the data layout specification for the new table definition.\n   *\n   * @param spec The data layout specification.\n   * @see DataLayoutSpec\n   */\n  ReplaceTableTransactionBuilder withDataLayoutSpec(DataLayoutSpec spec);\n\n  /**\n   * Set the maximum number of retries to retry the commit in the case of a retryable error.\n   *\n   * @param maxRetries The maximum number of retries. Must be at least 0. Default is 200.\n   */\n  ReplaceTableTransactionBuilder withMaxRetries(int maxRetries);\n\n  /**\n   * Build the transaction for replacing the Delta table.\n   *\n   * <p>The transaction must be committed using {@link Transaction#commit(Engine,\n   * io.delta.kernel.utils.CloseableIterable)} to apply the changes.\n   *\n   * @param engine The {@link Engine} instance to use for the transaction. Cannot be null.\n   * @return A configured {@link Transaction} for replacing the table.\n   */\n  Transaction build(Engine engine);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/transaction/UpdateTableTransactionBuilder.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.transaction;\n\nimport io.delta.kernel.Transaction;\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.*;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Builds a {@link Transaction} to update an existing Delta table.\n *\n * @since 3.4.0\n */\n@Evolving\npublic interface UpdateTableTransactionBuilder {\n\n  /**\n   * Set a new schema for the table, enabling schema evolution.\n   *\n   * <p>Schema evolution allows you to modify the table's structure by adding, removing, renaming,\n   * or reordering columns. Column mapping must be enabled on the table for schema evolution to be\n   * supported.\n   *\n   * <p>The provided schema should preserve field metadata (such as field IDs and physical names)\n   * for existing columns. Columns without metadata will be considered new columns and be assigned\n   * new IDs and physical names automatically.\n   *\n   * <p>Supported schema evolution operations:\n   *\n   * <ul>\n   *   <li><b>Add columns:</b> New columns can be added at any position\n   *   <li><b>Rename columns:</b> Change the logical name while preserving the physical name\n   *   <li><b>Type widening:</b> Compatible type changes (e.g., int to long)\n   *   <li><b>Reorder columns:</b> Change the position of columns in the schema\n   *   <li><b>Drop columns:</b> Remove columns (with restrictions)\n   * </ul>\n   *\n   * @param schema The new schema for the table. Cannot be null. Must be compatible with the current\n   *     schema and follow schema evolution rules.\n   */\n  UpdateTableTransactionBuilder withUpdatedSchema(StructType schema);\n\n  /**\n   * Add or update table properties (configuration).\n   *\n   * <p>Properties specified here will be added to the table or override existing values. To remove\n   * properties, use {@link #withTablePropertiesRemoved(Set)}.\n   *\n   * @param properties A map of property names to their values. The properties will be validated and\n   *     normalized. Cannot be null.\n   */\n  UpdateTableTransactionBuilder withTablePropertiesAdded(Map<String, String> properties);\n\n  /**\n   * Remove table properties from the table configuration.\n   *\n   * <p>The specified property keys will be removed from the table's configuration. Attempting to\n   * remove a property that doesn't exist is not an error.\n   *\n   * <p>Currently only user-properties (in other words, ones that are not prefixed by 'delta.') can\n   * be removed using this API. Adding and removing the same key in the same transaction is not\n   * allowed.\n   *\n   * @param propertyKeys A set of property names to remove. Cannot be null.\n   * @throws IllegalArgumentException if attempting to remove a 'delta.' property\n   */\n  UpdateTableTransactionBuilder withTablePropertiesRemoved(Set<String> propertyKeys);\n\n  /**\n   * Update the clustering columns for the table and enable clustering if it is not already enabled.\n   * Note: clustering cannot be enabled for a partitioned table.\n   *\n   * @param clusteringColumns The columns to cluster by. Cannot be null.\n   * @throws IllegalArgumentException if the table is partitioned\n   */\n  UpdateTableTransactionBuilder withClusteringColumns(List<Column> clusteringColumns);\n\n  /**\n   * Set a transaction identifier for idempotent operations.\n   *\n   * <p>Transaction identifiers allow you to implement idempotent operations by ensuring that\n   * multiple attempts to perform the same logical operation don't result in duplicate effects. This\n   * is useful for:\n   *\n   * <ul>\n   *   <li>Retry logic in distributed systems\n   *   <li>Exactly-once processing guarantees\n   *   <li>Recovery from failures\n   * </ul>\n   *\n   * <p>If a transaction with the same application ID and version (or higher) has already been\n   * committed the transaction will fail.\n   *\n   * @param applicationId A unique identifier for the application or process. Cannot be null.\n   * @param transactionVersion A monotonically increasing version number for this application ID.\n   */\n  UpdateTableTransactionBuilder withTransactionId(String applicationId, long transactionVersion);\n\n  /**\n   * Set the maximum number of retries for handling concurrent write conflicts.\n   *\n   * <p>When multiple writers attempt to modify the same Delta table simultaneously, conflicts can\n   * occur. This setting controls how many times the operation will be retried with conflict\n   * resolution before giving up.\n   *\n   * @param maxRetries The maximum number of retries. Must be at least 0. Default is 200.\n   */\n  UpdateTableTransactionBuilder withMaxRetries(int maxRetries);\n\n  /**\n   * Set the log compaction interval for optimizing the transaction log.\n   *\n   * <p>Log compaction creates periodic checkpoint files that consolidate multiple transaction log\n   * entries, improving read performance and reducing the number of files that need to be processed\n   * when reading table metadata.\n   *\n   * <p>A value of 0 disables automatic log compaction for this transaction. Positive values specify\n   * how many commits should occur between compactions. Defaults to 0.\n   *\n   * @param logCompactionInterval The number of commits between checkpoints. Must be at least 0. A\n   *     value of 0 disables log compaction.\n   */\n  UpdateTableTransactionBuilder withLogCompactionInterval(int logCompactionInterval);\n\n  /**\n   * Build the transaction for updating the Delta table.\n   *\n   * <p>This validates all the configuration and creates a {@link Transaction} that can be used to\n   * update the existing Delta table. The transaction must be committed using {@link\n   * Transaction#commit(Engine, CloseableIterable)} to actually apply the changes.\n   *\n   * @param engine The {@link Engine} instance to use for the transaction. Cannot be null.\n   * @return A configured {@link Transaction} for updating the table.\n   * @throws ConcurrentTransactionException if the table already has a committed transaction with\n   *     the same given transaction identifier.\n   */\n  Transaction build(Engine engine);\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/ArrayType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\n\n/**\n * Represent {@code array} data type\n *\n * @since 3.0.0\n */\n@Evolving\npublic class ArrayType extends DataType {\n  private final StructField elementField;\n  public static final String ARRAY_ELEMENT_NAME = \"element\";\n\n  public ArrayType(DataType elementType, boolean containsNull) {\n    this.elementField = new StructField(ARRAY_ELEMENT_NAME, elementType, containsNull);\n  }\n\n  public ArrayType(StructField elementField) {\n    this.elementField = elementField;\n  }\n\n  public StructField getElementField() {\n    return elementField;\n  }\n\n  public DataType getElementType() {\n    return elementField.getDataType();\n  }\n\n  public boolean containsNull() {\n    return elementField.isNullable();\n  }\n\n  @Override\n  public boolean equivalent(DataType dataType) {\n    return dataType instanceof ArrayType\n        && ((ArrayType) dataType).getElementType().equivalent(getElementType());\n  }\n\n  /**\n   * Checks whether the given {@code dataType} is compatible with this type when writing data.\n   * Collation differences are ignored.\n   *\n   * <p>This method is intended to be used during the write path to validate that an input type\n   * matches the expected schema before data is written.\n   *\n   * <p>It should not be used in other cases, such as the read path.\n   *\n   * @param dataType the input data type being written\n   * @return {@code true} if the input type is compatible with this type.\n   */\n  @Override\n  public boolean isWriteCompatible(DataType dataType) {\n    if (this == dataType) {\n      return true;\n    }\n    if (dataType == null || getClass() != dataType.getClass()) {\n      return false;\n    }\n    ArrayType arrayType = (ArrayType) dataType;\n    return (elementField == null && arrayType.elementField == null)\n        || (elementField != null && elementField.isWriteCompatible(arrayType.elementField));\n  }\n\n  @Override\n  public boolean isNested() {\n    return true;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    ArrayType arrayType = (ArrayType) o;\n    return Objects.equals(elementField, arrayType.elementField);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(elementField);\n  }\n\n  @Override\n  public String toString() {\n    return \"array[\" + getElementType() + \"]\";\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/BasePrimitiveType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/** Base class for all primitive types {@link DataType}. */\npublic abstract class BasePrimitiveType extends DataType {\n  /**\n   * Create a primitive type {@link DataType}\n   *\n   * @param primitiveTypeName Primitive type name.\n   * @return {@link DataType} for given primitive type name\n   */\n  public static DataType createPrimitive(String primitiveTypeName) {\n    return Optional.ofNullable(nameToPrimitiveTypeMap.get().get(primitiveTypeName))\n        .orElseThrow(\n            () -> new IllegalArgumentException(\"Unknown primitive type \" + primitiveTypeName));\n  }\n\n  /** Is the given type name a primitive type? */\n  public static boolean isPrimitiveType(String typeName) {\n    return nameToPrimitiveTypeMap.get().containsKey(typeName);\n  }\n\n  /** For testing only */\n  public static List<DataType> getAllPrimitiveTypes() {\n    return nameToPrimitiveTypeMap.get().values().stream().collect(Collectors.toList());\n  }\n\n  private static final Supplier<Map<String, DataType>> nameToPrimitiveTypeMap =\n      () ->\n          Collections.unmodifiableMap(\n              new HashMap<String, DataType>() {\n                {\n                  put(\"boolean\", BooleanType.BOOLEAN);\n                  put(\"byte\", ByteType.BYTE);\n                  put(\"short\", ShortType.SHORT);\n                  put(\"integer\", IntegerType.INTEGER);\n                  put(\"long\", LongType.LONG);\n                  put(\"float\", FloatType.FLOAT);\n                  put(\"double\", DoubleType.DOUBLE);\n                  put(\"date\", DateType.DATE);\n                  put(\"timestamp\", TimestampType.TIMESTAMP);\n                  put(\"timestamp_ntz\", TimestampNTZType.TIMESTAMP_NTZ);\n                  put(\"binary\", BinaryType.BINARY);\n                  put(\"string\", StringType.STRING);\n                  put(\"variant\", VariantType.VARIANT);\n                }\n              });\n\n  private final String primitiveTypeName;\n\n  protected BasePrimitiveType(String primitiveTypeName) {\n    this.primitiveTypeName = primitiveTypeName;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    BasePrimitiveType that = (BasePrimitiveType) o;\n    return primitiveTypeName.equals(that.primitiveTypeName);\n  }\n\n  @Override\n  public boolean isNested() {\n    return false;\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(primitiveTypeName);\n  }\n\n  @Override\n  public String toString() {\n    return primitiveTypeName;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/BinaryType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code byte[]} values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class BinaryType extends BasePrimitiveType {\n  public static final BinaryType BINARY = new BinaryType();\n\n  private BinaryType() {\n    super(\"binary\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/BooleanType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Data type representing {@code boolean} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class BooleanType extends BasePrimitiveType {\n  public static final BooleanType BOOLEAN = new BooleanType();\n\n  private BooleanType() {\n    super(\"boolean\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/ByteType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code byte} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class ByteType extends BasePrimitiveType {\n  public static final ByteType BYTE = new ByteType();\n\n  private ByteType() {\n    super(\"byte\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/CollationIdentifier.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\nimport java.util.Optional;\n\n/**\n * Identifies collation for string type. <a\n * href=\"https://github.com/delta-io/delta/blob/master/protocol_rfcs/collated-string-type.md#collation-identifiers\">\n * Collation identifiers</a>\n *\n * @since 3.3.0\n */\n@Evolving\npublic class CollationIdentifier {\n  /** The default Spark UTF8_BINARY collation. */\n  public static final CollationIdentifier SPARK_UTF8_BINARY =\n      new CollationIdentifier(\"SPARK\", \"UTF8_BINARY\");\n\n  private final String provider;\n  private final String name;\n  private final Optional<String> version;\n\n  private CollationIdentifier(String provider, String collationName) {\n    this(provider, collationName, Optional.empty());\n  }\n\n  private CollationIdentifier(String provider, String collationName, Optional<String> version) {\n    Objects.requireNonNull(provider, \"Collation provider cannot be null.\");\n    Objects.requireNonNull(collationName, \"Collation name cannot be null.\");\n    Objects.requireNonNull(version, \"Collation version cannot be null.\");\n\n    this.provider = provider.toUpperCase();\n    this.name = collationName.toUpperCase();\n    this.version = version.map(String::toUpperCase);\n  }\n\n  /** @return collation provider. */\n  public String getProvider() {\n    return provider;\n  }\n\n  /** @return collation name. */\n  public String getName() {\n    return name;\n  }\n\n  /** @return collation version. */\n  public Optional<String> getVersion() {\n    return version;\n  }\n\n  /** @return if this collation is the default Spark UTF8_BINARY collation. */\n  public boolean isSparkUTF8BinaryCollation() {\n    return equals(SPARK_UTF8_BINARY);\n  }\n\n  /**\n   * @param identifier collation identifier in string form of <br>\n   *     {@code PROVIDER.COLLATION_NAME[.COLLATION_VERSION]}.\n   * @return appropriate collation identifier object\n   */\n  public static CollationIdentifier fromString(String identifier) {\n    long numDots = identifier.chars().filter(ch -> ch == '.').count();\n    checkArgument(numDots > 0, \"Invalid collation identifier: %s\", identifier);\n    if (numDots == 1) {\n      String[] parts = identifier.split(\"\\\\.\");\n      return new CollationIdentifier(parts[0], parts[1]);\n    } else {\n      String[] parts = identifier.split(\"\\\\.\", 3);\n      return new CollationIdentifier(parts[0], parts[1], Optional.of(parts[2]));\n    }\n  }\n\n  /** Collation identifiers are identical when the provider, name, and version are the same. */\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof CollationIdentifier)) {\n      return false;\n    }\n\n    CollationIdentifier other = (CollationIdentifier) o;\n    return this.provider.equals(other.provider)\n        && this.name.equals(other.name)\n        && this.version.equals(other.version);\n  }\n\n  /** @return collation identifier in form of {@code PROVIDER.COLLATION_NAME}. */\n  public String toStringWithoutVersion() {\n    return String.format(\"%s.%s\", provider, name);\n  }\n\n  /** @return collation identifier in form of {@code PROVIDER.COLLATION_NAME[.COLLATION_VERSION]} */\n  @Override\n  public String toString() {\n    if (version.isPresent()) {\n      return String.format(\"%s.%s.%s\", provider, name, version.get());\n    } else {\n      return String.format(\"%s.%s\", provider, name);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/DataType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * Base class for all data types.\n *\n * @since 3.0.0\n */\n@Evolving\npublic abstract class DataType {\n\n  /**\n   * Are the data types same? The metadata, collations or column names could be different.\n   *\n   * <p>Should be used for schema comparisons during schema evolution.\n   *\n   * @param dataType\n   * @return\n   */\n  public boolean equivalent(DataType dataType) {\n    return equals(dataType);\n  }\n\n  /**\n   * Checks whether the given {@code dataType} is compatible with this type when writing data.\n   * Collation differences are ignored.\n   *\n   * <p>This method is intended to be used during the write path to validate that an input type\n   * matches the expected schema before data is written.\n   *\n   * <p>It should not be used in other cases, such as the read path.\n   *\n   * @param dataType the input data type being written\n   * @return {@code true} if the input type is compatible with this type.\n   */\n  public boolean isWriteCompatible(DataType dataType) {\n    return equals(dataType);\n  }\n\n  /**\n   * Returns true iff this data is a nested data type (it logically parameterized by other types).\n   *\n   * <p>For example StructType, ArrayType, MapType are nested data types.\n   */\n  public abstract boolean isNested();\n\n  @Override\n  public abstract int hashCode();\n\n  @Override\n  public abstract boolean equals(Object obj);\n\n  @Override\n  public abstract String toString();\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/DateType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * A date type, supporting \"0001-01-01\" through \"9999-12-31\". Internally, this is represented as the\n * number of days from 1970-01-01.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class DateType extends BasePrimitiveType {\n  public static final DateType DATE = new DateType();\n\n  private DateType() {\n    super(\"date\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/DecimalType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\n\n/**\n * The data type representing {@code java.math.BigDecimal} values. A Decimal that must have fixed\n * precision (the maximum number of digits) and scale (the number of digits on right side of dot).\n *\n * <p>The precision can be up to 38, scale can also be up to 38 (less or equal to precision).\n *\n * <p>The default precision and scale is (10, 0).\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class DecimalType extends DataType {\n  public static final DecimalType USER_DEFAULT = new DecimalType(10, 0);\n\n  private final int precision;\n  private final int scale;\n\n  public DecimalType(int precision, int scale) {\n    if (precision < 0 || precision > 38 || scale < 0 || scale > 38 || scale > precision) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"Invalid precision and scale combo (%d, %d). They should be in the range [0, 38] \"\n                  + \"and scale can not be more than the precision.\",\n              precision, scale));\n    }\n    this.precision = precision;\n    this.scale = scale;\n  }\n\n  /** @return the maximum number of digits of the decimal */\n  public int getPrecision() {\n    return precision;\n  }\n\n  /** @return the number of digits on the right side of the decimal point (dot) */\n  public int getScale() {\n    return scale;\n  }\n\n  @Override\n  public boolean isNested() {\n    return false;\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"Decimal(%d, %d)\", precision, scale);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    DecimalType that = (DecimalType) o;\n    return precision == that.precision && scale == that.scale;\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(precision, scale);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/DoubleType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code double} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class DoubleType extends BasePrimitiveType {\n  public static final DoubleType DOUBLE = new DoubleType();\n\n  private DoubleType() {\n    super(\"double\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/FieldMetadata.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license below).\n * It contains modifications which are licensed as specified above.\n */\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.internal.util.Preconditions;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/** The metadata for a given {@link StructField}. The contents are immutable. */\npublic final class FieldMetadata {\n  private final Map<String, Object> metadata;\n\n  private FieldMetadata(Map<String, Object> metadata) {\n    this.metadata = Collections.unmodifiableMap(metadata);\n  }\n\n  /** @return list of the key-value pairs in this {@link FieldMetadata} */\n  public Map<String, Object> getEntries() {\n    return metadata;\n  }\n\n  /**\n   * @param key the key to check for\n   * @return True if {@code this} contains a mapping for the given key, False otherwise\n   */\n  public boolean contains(String key) {\n    return metadata.containsKey(key);\n  }\n\n  /**\n   * @param key the key to check for\n   * @return the value to which the specified key is mapped, or null if there is no mapping for the\n   *     given key\n   */\n  public Object get(String key) {\n    return metadata.get(key);\n  }\n\n  public Long getLong(String key) {\n    return get(key, Long.class);\n  }\n\n  public Double getDouble(String key) {\n    return get(key, Double.class);\n  }\n\n  public Boolean getBoolean(String key) {\n    return get(key, Boolean.class);\n  }\n\n  public String getString(String key) {\n    return get(key, String.class);\n  }\n\n  public MetadataColumnSpec getMetadataColumnSpec(String key) {\n    return get(key, MetadataColumnSpec.class);\n  }\n\n  public FieldMetadata getMetadata(String key) {\n    return get(key, FieldMetadata.class);\n  }\n\n  public Long[] getLongArray(String key) {\n    return get(key, Long[].class);\n  }\n\n  public Double[] getDoubleArray(String key) {\n    return get(key, Double[].class);\n  }\n\n  public Boolean[] getBooleanArray(String key) {\n    return get(key, Boolean[].class);\n  }\n\n  public String[] getStringArray(String key) {\n    return get(key, String[].class);\n  }\n\n  public FieldMetadata[] getMetadataArray(String key) {\n    return get(key, FieldMetadata[].class);\n  }\n\n  @Override\n  public String toString() {\n    return metadata.entrySet().stream()\n        .map(\n            entry -> {\n              String key = entry.getKey();\n              Object value = entry.getValue();\n              if (value == null) {\n                return key + \"=null\";\n              }\n              String valueStr =\n                  value.getClass().isArray() ? Arrays.toString((Object[]) value) : value.toString();\n\n              return key + \"=\" + valueStr;\n            })\n        .collect(Collectors.joining(\", \", \"{\", \"}\"));\n  }\n\n  /** Are the metadata same, ignoring the specified keys? */\n  public boolean equalsIgnoreKeys(FieldMetadata other, Set<String> keys) {\n    Preconditions.checkArgument(keys != null, \"keys must not be null\");\n    if (this == other) {\n      return true;\n    }\n    if (other == null) {\n      return false;\n    }\n\n    Map<String, Object> filteredMetadata = new HashMap<>();\n    for (Map.Entry<String, Object> entry : this.metadata.entrySet()) {\n      if (!keys.contains(entry.getKey())) {\n        filteredMetadata.put(entry.getKey(), entry.getValue());\n      }\n    }\n    Map<String, Object> otherFilteredMetadata = new HashMap<>();\n    for (Map.Entry<String, Object> entry : other.metadata.entrySet()) {\n      if (!keys.contains(entry.getKey())) {\n        otherFilteredMetadata.put(entry.getKey(), entry.getValue());\n      }\n    }\n\n    if (filteredMetadata.size() != otherFilteredMetadata.size()) {\n      return false;\n    }\n    return filteredMetadata.entrySet().stream()\n        .allMatch(\n            e -> {\n              Object value = e.getValue();\n              Object otherValue = otherFilteredMetadata.get(e.getKey());\n              return Objects.deepEquals(value, otherValue);\n            });\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (o == null || getClass() != o.getClass()) return false;\n    FieldMetadata other = (FieldMetadata) o;\n    if (this.metadata.size() != other.metadata.size()) return false;\n    return this.metadata.entrySet().stream()\n        .allMatch(\n            e -> {\n              Object value = e.getValue();\n              Object otherValue = other.metadata.get(e.getKey());\n              return Objects.deepEquals(value, otherValue);\n            });\n  }\n\n  @Override\n  public int hashCode() {\n    return metadata.entrySet().stream()\n        .mapToInt(\n            entry ->\n                (entry.getValue().getClass().isArray()\n                    ? (entry.getKey() == null ? 0 : entry.getKey().hashCode())\n                        ^ Arrays.hashCode((Object[]) entry.getValue())\n                    : entry.hashCode()))\n        .sum();\n  }\n\n  /** @return a new {@link FieldMetadata.Builder} */\n  public static Builder builder() {\n    return new Builder();\n  }\n\n  /** @return an empty {@link FieldMetadata} instance */\n  public static FieldMetadata empty() {\n    return builder().build();\n  }\n\n  /**\n   * @param key the key to check for\n   * @param type the type to cast the value to\n   * @return the value (casted to the given type) to which the specified key is mapped, or null if\n   *     there is no mapping for the given key\n   */\n  private <T> T get(String key, Class<T> type) {\n    Object value = get(key);\n    if (null == value) {\n      return (T) value;\n    }\n\n    Preconditions.checkArgument(\n        value.getClass().isAssignableFrom(type),\n        \"Expected '%s' to be of type '%s' but was '%s'\",\n        value,\n        type,\n        value.getClass());\n    return type.cast(value);\n  }\n\n  /** Builder class for {@link FieldMetadata}. */\n  public static class Builder {\n    private Map<String, Object> metadata = new HashMap<String, Object>();\n\n    public Builder putNull(String key) {\n      metadata.put(key, null);\n      return this;\n    }\n\n    public Builder putLong(String key, long value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putDouble(String key, double value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putBoolean(String key, boolean value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putString(String key, String value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putMetadataColumnSpec(String key, MetadataColumnSpec value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putFieldMetadata(String key, FieldMetadata value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putLongArray(String key, Long[] value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putDoubleArray(String key, Double[] value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putBooleanArray(String key, Boolean[] value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putStringArray(String key, String[] value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    public Builder putFieldMetadataArray(String key, FieldMetadata[] value) {\n      metadata.put(key, value);\n      return this;\n    }\n\n    /**\n     * Adds all metadata from {@code meta.metadata} to the builder's {@code metadata}. Entries in\n     * the builder's {@code metadata} are overwritten with the entries from {@code meta.metadata}.\n     *\n     * @param meta The {@link FieldMetadata} instance holding metadata\n     * @return this\n     */\n    public Builder fromMetadata(FieldMetadata meta) {\n      metadata.putAll(meta.metadata);\n      return this;\n    }\n\n    /** @return a new {@link FieldMetadata} with the mappings added to the builder */\n    public FieldMetadata build() {\n      return new FieldMetadata(this.metadata);\n    }\n\n    public FieldMetadata getMetadata(String key) {\n      Object value = metadata.get(key);\n      if (null == value) {\n        return null;\n      }\n      if (value instanceof FieldMetadata) {\n        return (FieldMetadata) value;\n      }\n      throw new io.delta.kernel.exceptions.KernelException(\n          String.format(\n              \"Expected '%s' to be of type 'FieldMetadata' but was '%s'\",\n              value, value.getClass().getName()));\n    }\n\n    public Builder remove(String s) {\n      metadata.remove(s);\n      return this;\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/FloatType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code float} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class FloatType extends BasePrimitiveType {\n  public static final FloatType FLOAT = new FloatType();\n\n  private FloatType() {\n    super(\"float\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/GeographyType.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * The data type representing geography values. A Geography must have a fixed Spatial Reference\n * System Identifier (SRID) that defines the coordinate system and an algorithm that determines how\n * geometric calculations are performed.\n *\n * <p>The SRID is specified as a string and the algorithm defines the calculation method. The engine\n * is responsible for validating and interpreting the SRID and algorithm values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class GeographyType extends DataType {\n  public static final String DEFAULT_SRID = \"OGC:CRS84\";\n  public static final String DEFAULT_ALGORITHM = \"spherical\";\n\n  public static final Set<String> VALID_ALGORITHMS =\n      Set.of(\"spherical\", \"vincenty\", \"thomas\", \"andoyer\", \"karney\");\n\n  private final String srid;\n  private final String algorithm;\n\n  /** Returns a GeographyType with the default SRID and algorithm. */\n  public static GeographyType ofDefault() {\n    return new GeographyType(DEFAULT_SRID, DEFAULT_ALGORITHM);\n  }\n\n  /**\n   * Returns a GeographyType with the specified SRID and default algorithm.\n   *\n   * @param srid the Spatial Reference System Identifier (any non-null, non-empty string)\n   */\n  public static GeographyType ofSRID(String srid) {\n    return new GeographyType(srid, DEFAULT_ALGORITHM);\n  }\n\n  /**\n   * Returns a GeographyType with the default SRID and the specified algorithm.\n   *\n   * @param algorithm one of: spherical, vincenty, thomas, andoyer, karney\n   */\n  public static GeographyType ofAlgorithm(String algorithm) {\n    return new GeographyType(DEFAULT_SRID, algorithm);\n  }\n\n  /**\n   * Create a GeographyType with the specified SRID and algorithm.\n   *\n   * @param srid the Spatial Reference System Identifier (any non-null, non-empty string)\n   * @param algorithm the algorithm for geometric calculations (any non-null, non-empty string)\n   * @throws IllegalArgumentException if the SRID or algorithm is null or empty or algorithm is\n   *     invalid\n   */\n  public GeographyType(String srid, String algorithm) {\n    if (srid == null || srid.isEmpty()) {\n      throw new IllegalArgumentException(\"SRID cannot be null or empty\");\n    }\n    if (algorithm == null || algorithm.isEmpty()) {\n      throw new IllegalArgumentException(\"Algorithm cannot be null or empty\");\n    }\n    if (!VALID_ALGORITHMS.contains(algorithm)) {\n      throw new IllegalArgumentException(\n          \"Algorithm must be one of: spherical, vincenty, thomas, andoyer, karney, got: \"\n              + algorithm);\n    }\n    this.srid = srid;\n    this.algorithm = algorithm;\n  }\n\n  /**\n   * Get the Spatial Reference System Identifier.\n   *\n   * @return the SRID string\n   */\n  public String getSRID() {\n    return srid;\n  }\n\n  /**\n   * Get the algorithm for geometric calculations.\n   *\n   * @return the algorithm string\n   */\n  public String getAlgorithm() {\n    return algorithm;\n  }\n\n  @Override\n  public boolean isNested() {\n    return false;\n  }\n\n  /**\n   * Serialize this GeographyType to its string representation with minimal info.\n   *\n   * @return the serialized string representation\n   */\n  public String simpleString() {\n    return String.format(\"geography(%s, %s)\", srid, algorithm);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"Geography(srid=%s, algorithm=%s)\", srid, algorithm);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    GeographyType that = (GeographyType) o;\n    return srid.equals(that.srid) && algorithm.equals(that.algorithm);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(srid, algorithm);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/GeometryType.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\n\n/**\n * The data type representing geometry values. A Geometry must have a fixed Spatial Reference System\n * Identifier (SRID) that defines the coordinate system.\n *\n * <p>The SRID is specified as a string The engine is responsible for validating and interpreting\n * the SRID value.\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class GeometryType extends DataType {\n\n  public static final String DEFAULT_SRID = \"OGC:CRS84\";\n\n  private final String srid;\n\n  /** Returns a GeometryType with the default SRID. */\n  public static GeometryType ofDefault() {\n    return new GeometryType(DEFAULT_SRID);\n  }\n\n  /**\n   * Returns a GeometryType with the specified SRID.\n   *\n   * @param srid the Spatial Reference System Identifier (any non-null, non-empty string)\n   */\n  public static GeometryType ofSRID(String srid) {\n    return new GeometryType(srid);\n  }\n\n  /**\n   * Create a GeometryType with the specified SRID.\n   *\n   * @param srid the Spatial Reference System Identifier (any non-null, non-empty string)\n   * @throws IllegalArgumentException if the SRID is null or empty\n   */\n  public GeometryType(String srid) {\n    if (srid == null || srid.isEmpty()) {\n      throw new IllegalArgumentException(\"SRID cannot be null or empty\");\n    }\n    this.srid = srid;\n  }\n\n  /**\n   * Get the Spatial Reference System Identifier.\n   *\n   * @return the SRID string\n   */\n  public String getSRID() {\n    return srid;\n  }\n\n  @Override\n  public boolean isNested() {\n    return false;\n  }\n\n  /**\n   * Serialize this GeometryType to its string representation with minimal info.\n   *\n   * @return the serialized string representation\n   */\n  public String simpleString() {\n    return String.format(\"geometry(%s)\", srid);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"Geometry(srid=%s)\", srid);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    GeometryType that = (GeometryType) o;\n    return srid.equals(that.srid);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(srid);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/IntegerType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code integer} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class IntegerType extends BasePrimitiveType {\n  public static final IntegerType INTEGER = new IntegerType();\n\n  private IntegerType() {\n    super(\"integer\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/LongType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code long} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class LongType extends BasePrimitiveType {\n  public static final LongType LONG = new LongType();\n\n  private LongType() {\n    super(\"long\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/MapType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\n\n/**\n * Data type representing a {@code map} type.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class MapType extends DataType {\n\n  private final StructField keyField;\n  private final StructField valueField;\n\n  public static final String MAP_KEY_NAME = \"key\";\n  public static final String MAP_VALUE_NAME = \"value\";\n\n  public MapType(DataType keyType, DataType valueType, boolean valueContainsNull) {\n    validateKeyType(keyType);\n    this.keyField = new StructField(MAP_KEY_NAME, keyType, false);\n    this.valueField = new StructField(MAP_VALUE_NAME, valueType, valueContainsNull);\n  }\n\n  public MapType(StructField keyField, StructField valueField) {\n    validateKeyType(keyField.getDataType());\n    this.keyField = keyField;\n    this.valueField = valueField;\n  }\n\n  /**\n   * The Delta protocol does not support collated string types as map keys. Only StringType with the\n   * default UTF8_BINARY collation is allowed.\n   *\n   * @see <a\n   *     href=\"https://github.com/delta-io/delta/blob/master/protocol_rfcs/collated-string-type.md\">\n   *     Collated String Type RFC</a>\n   */\n  private static void validateKeyType(DataType keyType) {\n    if (keyType instanceof StringType && !((StringType) keyType).isUTF8BinaryCollated()) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"MapType does not support collated string types as keys. \"\n                  + \"Found key type '%s', but only StringType with default UTF8_BINARY \"\n                  + \"collation is allowed.\",\n              keyType));\n    }\n  }\n\n  public StructField getKeyField() {\n    return keyField;\n  }\n\n  public StructField getValueField() {\n    return valueField;\n  }\n\n  public DataType getKeyType() {\n    return getKeyField().getDataType();\n  }\n\n  public DataType getValueType() {\n    return getValueField().getDataType();\n  }\n\n  public boolean isValueContainsNull() {\n    return valueField.isNullable();\n  }\n\n  @Override\n  public boolean equivalent(DataType dataType) {\n    return dataType instanceof MapType\n        && ((MapType) dataType).getKeyType().equivalent(getKeyType())\n        && ((MapType) dataType).getValueType().equivalent(getValueType())\n        && ((MapType) dataType).isValueContainsNull() == isValueContainsNull();\n  }\n\n  /**\n   * Checks whether the given {@code dataType} is compatible with this type when writing data.\n   * Collation differences are ignored.\n   *\n   * <p>This method is intended to be used during the write path to validate that an input type\n   * matches the expected schema before data is written.\n   *\n   * <p>It should not be used in other cases, such as the read path.\n   *\n   * @param dataType the input data type being written\n   * @return {@code true} if the input type is compatible with this type.\n   */\n  @Override\n  public boolean isWriteCompatible(DataType dataType) {\n    if (this == dataType) {\n      return true;\n    }\n    if (dataType == null || getClass() != dataType.getClass()) {\n      return false;\n    }\n    MapType mapType = (MapType) dataType;\n    return ((keyField == null && mapType.keyField == null)\n            || (keyField != null && keyField.isWriteCompatible(mapType.keyField)))\n        && ((valueField == null && mapType.valueField == null)\n            || (valueField != null && valueField.isWriteCompatible(mapType.valueField)));\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    MapType mapType = (MapType) o;\n    return Objects.equals(keyField, mapType.keyField)\n        && Objects.equals(valueField, mapType.valueField);\n  }\n\n  @Override\n  public boolean isNested() {\n    return true;\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(keyField, valueField);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"map[%s, %s]\", getKeyType(), getValueType());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/MetadataColumnSpec.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.types;\n\n/**\n * Enumeration of metadata columns recognized by Delta Kernel.\n *\n * <p>Metadata columns provide additional information about rows in a Delta table.\n */\npublic enum MetadataColumnSpec {\n  ROW_INDEX(\"row_index\", LongType.LONG, false),\n  ROW_ID(\"row_id\", LongType.LONG, false),\n  ROW_COMMIT_VERSION(\"row_commit_version\", LongType.LONG, false);\n\n  public final String textValue;\n  public final DataType dataType;\n  public final boolean nullable;\n\n  MetadataColumnSpec(String textValue, DataType dataType, boolean nullable) {\n    this.textValue = textValue;\n    this.dataType = dataType;\n    this.nullable = nullable;\n  }\n\n  public String toString() {\n    return textValue;\n  }\n\n  public static MetadataColumnSpec fromString(String text) {\n    for (MetadataColumnSpec type : MetadataColumnSpec.values()) {\n      if (type.textValue.equalsIgnoreCase(text)) {\n        return type;\n      }\n    }\n    throw new IllegalArgumentException(\"Unknown MetadataColumnType: \" + text);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/ShortType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code short} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class ShortType extends BasePrimitiveType {\n  public static final ShortType SHORT = new ShortType();\n\n  private ShortType() {\n    super(\"short\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/StringType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The data type representing {@code string} type values.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class StringType extends BasePrimitiveType {\n  public static final StringType STRING = new StringType(CollationIdentifier.SPARK_UTF8_BINARY);\n\n  private final CollationIdentifier collationIdentifier;\n\n  /**\n   * @param collationIdentifier An identifier representing the collation to be used for string\n   *     comparison and sorting. This determines how strings will be ordered and compared in query\n   *     operations.\n   */\n  public StringType(CollationIdentifier collationIdentifier) {\n    super(\"string\");\n    this.collationIdentifier = collationIdentifier;\n  }\n\n  /**\n   * @param collationName name of collation in which this StringType will be observed. In form of\n   *     {@code PROVIDER.COLLATION_NAME[.VERSION]}\n   */\n  public StringType(String collationName) {\n    super(\"string\");\n    this.collationIdentifier = CollationIdentifier.fromString(collationName);\n  }\n\n  /** @return StringType's collation identifier */\n  public CollationIdentifier getCollationIdentifier() {\n    return collationIdentifier;\n  }\n\n  /**\n   * Are the data types same? The metadata, collations or column names could be different.\n   *\n   * @param dataType\n   * @return\n   */\n  public boolean equivalent(DataType dataType) {\n    return dataType instanceof StringType;\n  }\n\n  /**\n   * Checks whether the given {@code dataType} is compatible with this type when writing data.\n   * Collation differences are ignored.\n   *\n   * <p>This method is intended to be used during the write path to validate that an input type\n   * matches the expected schema before data is written.\n   *\n   * <p>It should not be used in other cases, such as the read path.\n   *\n   * @param dataType the input data type being written\n   * @return {@code true} if the input type is compatible with this type.\n   */\n  @Override\n  public boolean isWriteCompatible(DataType dataType) {\n    return dataType instanceof StringType;\n  }\n\n  /** @return true if this StringType uses the default Spark UTF8_BINARY collation. */\n  public boolean isUTF8BinaryCollated() {\n    return collationIdentifier.isSparkUTF8BinaryCollation();\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (!(o instanceof StringType)) {\n      return false;\n    }\n\n    StringType that = (StringType) o;\n    return collationIdentifier.equals(that.collationIdentifier);\n  }\n\n  /**\n   * Override is needed because {@code toString()} may be used for schema serialization and similar\n   * contexts, so collation information must be included.\n   *\n   * @return string representation of the StringType.\n   */\n  @Override\n  public String toString() {\n    if (isUTF8BinaryCollated()) {\n      return super.toString();\n    } else {\n      return String.format(\"string collate %s\", collationIdentifier.getName());\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/StructField.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.types;\n\nimport static io.delta.kernel.types.MetadataColumnSpec.*;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\nimport io.delta.kernel.internal.util.SchemaIterable;\nimport java.util.*;\n\n/**\n * Represents a subfield of {@link StructType} with additional properties and metadata.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class StructField {\n  ////////////////////////////////////////////////////////////////////////////////\n  // Static Fields / Methods\n  ////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * The existence of this key indicates that a column is a metadata column and its values indicates\n   * what kind of {@link MetadataColumnSpec} it is.\n   */\n  public static final String METADATA_SPEC_KEY = \"delta.metadataSpec\";\n\n  /**\n   * Indicates that a column was requested for internal computations and should not be returned to\n   * the user.\n   */\n  public static final String IS_INTERNAL_COLUMN_KEY = \"delta.isInternalColumn\";\n\n  /** The name of the default row index metadata column. */\n  private static final String DEFAULT_ROW_INDEX_COLUMN_NAME = \"_metadata.row_index\";\n\n  /**\n   * The default row index metadata column used by Kernel. When present, this column must be\n   * populated with row index of each row when reading from Parquet.\n   */\n  public static StructField DEFAULT_ROW_INDEX_COLUMN =\n      createMetadataColumn(DEFAULT_ROW_INDEX_COLUMN_NAME, MetadataColumnSpec.ROW_INDEX);\n\n  public static final String COLLATIONS_METADATA_KEY = \"__COLLATIONS\";\n  public static final String FROM_TYPE_KEY = \"fromType\";\n  public static final String TO_TYPE_KEY = \"toType\";\n  public static final String FIELD_PATH_KEY = \"fieldPath\";\n  public static final String DELTA_TYPE_CHANGES_KEY = \"delta.typeChanges\";\n\n  /**\n   * Creates a metadata column of the given {@code colSpec} with the given {@code name}.\n   *\n   * @param name Name of the metadata column\n   * @param colSpec Type of the metadata column\n   * @return A StructField representing the metadata column\n   */\n  public static StructField createMetadataColumn(String name, MetadataColumnSpec colSpec) {\n    switch (colSpec) {\n      case ROW_INDEX:\n        return new StructField(\n            name,\n            colSpec.dataType,\n            colSpec.nullable,\n            new FieldMetadata.Builder()\n                .putMetadataColumnSpec(METADATA_SPEC_KEY, ROW_INDEX)\n                .build());\n      case ROW_ID:\n        return new StructField(\n            name,\n            colSpec.dataType,\n            colSpec.nullable,\n            new FieldMetadata.Builder().putMetadataColumnSpec(METADATA_SPEC_KEY, ROW_ID).build());\n      case ROW_COMMIT_VERSION:\n        return new StructField(\n            name,\n            colSpec.dataType,\n            colSpec.nullable,\n            new FieldMetadata.Builder()\n                .putMetadataColumnSpec(METADATA_SPEC_KEY, ROW_COMMIT_VERSION)\n                .build());\n      default:\n        throw new IllegalArgumentException(\"Unknown MetadataColumnType: \" + colSpec);\n    }\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////\n  // Instance Fields / Methods\n  ////////////////////////////////////////////////////////////////////////////////\n\n  private final String name;\n  private final DataType dataType;\n  private final boolean nullable;\n  private final FieldMetadata metadata;\n  private final List<TypeChange> typeChanges;\n\n  public StructField(String name, DataType dataType, boolean nullable) {\n    this(name, dataType, nullable, FieldMetadata.empty());\n  }\n\n  public StructField(String name, DataType dataType, boolean nullable, FieldMetadata metadata) {\n    this(name, dataType, nullable, metadata, Collections.emptyList());\n  }\n\n  /*\n   * N.B. Type changes should be entirely managed by the Delta Kernel, users are not expected to\n   * maintain this field, and therefore should not be using this constructor.\n   */\n  StructField(\n      String name,\n      DataType dataType,\n      boolean nullable,\n      FieldMetadata metadata,\n      List<TypeChange> typeChanges) {\n    this.name = name;\n    this.dataType = dataType;\n    this.nullable = nullable;\n    this.typeChanges = typeChanges == null ? Collections.emptyList() : typeChanges;\n\n    FieldMetadata nestedMetadata = collectNestedMapArrayTypeMetadata();\n    this.metadata =\n        new FieldMetadata.Builder().fromMetadata(metadata).fromMetadata(nestedMetadata).build();\n    if (!this.typeChanges.isEmpty()\n        && (dataType instanceof MapType\n            || dataType instanceof StructType\n            || dataType instanceof ArrayType)) {\n      throw new KernelException(\"Type changes are not supported on nested types.\");\n    }\n  }\n\n  /** @return the name of this field */\n  public String getName() {\n    return name;\n  }\n\n  /** @return the data type of this field */\n  public DataType getDataType() {\n    return dataType;\n  }\n\n  /** @return the metadata for this field */\n  public FieldMetadata getMetadata() {\n    return metadata;\n  }\n\n  /** @return whether this field allows to have a {@code null} value. */\n  public boolean isNullable() {\n    return nullable;\n  }\n\n  /**\n   * Returns the list of type changes for this field. A field can go through multiple type changes\n   * (e.g. {@code int->long->decimal}). Changes are ordered from least recent to most recent in the\n   * list (index 0 is the oldest change).\n   *\n   * <p>N.B. Type changes should be entirely managed by the Delta Kernel, users are not expected to\n   * maintain this field.\n   */\n  public List<TypeChange> getTypeChanges() {\n    return Collections.unmodifiableList(typeChanges);\n  }\n\n  public boolean isMetadataColumn() {\n    return metadata != null && metadata.contains(METADATA_SPEC_KEY);\n  }\n\n  public boolean isDataColumn() {\n    return !isMetadataColumn();\n  }\n\n  /** Returns the type of metadata column if this is a metadata column, otherwise returns null. */\n  public MetadataColumnSpec getMetadataColumnSpec() {\n    return metadata.getMetadataColumnSpec(METADATA_SPEC_KEY);\n  }\n\n  public boolean isInternalColumn() {\n    return Optional.ofNullable(metadata.getBoolean(IS_INTERNAL_COLUMN_KEY)).orElse(false);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"StructField(name=%s,type=%s,nullable=%s,metadata=%s,typeChanges=%s)\",\n        name, dataType, nullable, metadata, typeChanges);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    StructField that = (StructField) o;\n    return nullable == that.nullable\n        && name.equals(that.name)\n        && dataType.equals(that.dataType)\n        && metadata.equals(that.metadata)\n        && Objects.equals(typeChanges, that.typeChanges);\n  }\n\n  /**\n   * Checks whether the given {@code other} is compatible with this {@code StructField} when writing\n   * data. Collation differences are ignored.\n   */\n  public boolean isWriteCompatible(StructField other) {\n    if (this == other) {\n      return true;\n    }\n    if (other == null) {\n      return false;\n    }\n\n    return nullable == other.nullable\n        && name.equals(other.name)\n        && dataType.isWriteCompatible(other.dataType)\n        // Compare metadata while ignoring collation metadata differences\n        && metadata.equalsIgnoreKeys(other.metadata, Collections.singleton(COLLATIONS_METADATA_KEY))\n        && Objects.equals(typeChanges, other.typeChanges);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(name, dataType, nullable, metadata, typeChanges);\n  }\n\n  public StructField withNewMetadata(FieldMetadata metadata) {\n    return new StructField(name, dataType, nullable, metadata, typeChanges);\n  }\n\n  /**\n   * Creates a copy of this StructField with the specified type changes.\n   *\n   * <p>N.B. Type changes should be entirely managed by the Delta Kernel, users are not expected to\n   * maintain this field.\n   *\n   * @param typeChanges The list of type changes to set\n   * @return A new StructField with the same properties but with the specified type changes\n   */\n  public StructField withTypeChanges(List<TypeChange> typeChanges) {\n    return new StructField(name, dataType, nullable, metadata, typeChanges);\n  }\n\n  /**\n   * Creates a copy of this StructField with the specified data type.\n   *\n   * <p>TypeChanges are NOT updated as part of this call.\n   *\n   * @param newType The new type to use in the StructField.\n   * @return A new StructField with the same properties but with the specified data type.\n   */\n  public StructField withDataType(DataType newType) {\n    return new StructField(name, newType, nullable, metadata, typeChanges);\n  }\n\n  /** Fetches collation and type changes metadata from nested fields. */\n  private FieldMetadata collectNestedMapArrayTypeMetadata() {\n    FieldMetadata.Builder collationBuilder = FieldMetadata.builder();\n    List<FieldMetadata> typeChangesBuilder = new ArrayList<>();\n    // This is a little risky since this isn't fully initialized but should be fine since all fields\n    // we needed are initialized.\n    // StructTypes children would already have their own collation metadata, so skip them here.\n    SchemaIterable iterable =\n        SchemaIterable.newSchemaIterableWithIgnoredRecursion(\n            new StructType().add(this), new Class[] {StructType.class});\n    for (SchemaIterable.SchemaElement element : iterable) {\n      DataType type = element.getField().getDataType();\n      if (type instanceof StringType) {\n        StringType stringType = (StringType) type;\n        if (!stringType\n            .getCollationIdentifier()\n            .equals(CollationIdentifier.fromString(\"SPARK.UTF8_BINARY\"))) {\n          // TODO: Should this account for column mapping?\n          String path =\n              element.getPathFromNearestStructFieldAncestor(\n                  element.getNearestStructFieldAncestor().name);\n          collationBuilder.putString(\n              path, stringType.getCollationIdentifier().toStringWithoutVersion());\n        }\n      }\n      StructField field = element.getField();\n      if (!field.getTypeChanges().isEmpty()) {\n        for (TypeChange typeChange : field.getTypeChanges()) {\n          FieldMetadata.Builder typeChangeBuilder = FieldMetadata.builder();\n          typeChangeBuilder.putString(FROM_TYPE_KEY, typeAsString(typeChange.getFrom()));\n          typeChangeBuilder.putString(TO_TYPE_KEY, typeAsString(typeChange.getTo()));\n          if (!element.isStructField()) {\n            // For type changes the field name the field name is not a prefix.\n            typeChangeBuilder.putString(\n                FIELD_PATH_KEY, element.getPathFromNearestStructFieldAncestor(\"\"));\n          }\n          typeChangesBuilder.add(typeChangeBuilder.build());\n        }\n      }\n    }\n\n    FieldMetadata.Builder finalBuilder = FieldMetadata.builder();\n\n    FieldMetadata collationMetadata = collationBuilder.build();\n    if (!collationMetadata.getEntries().isEmpty()) {\n      finalBuilder.putFieldMetadata(COLLATIONS_METADATA_KEY, collationMetadata);\n    }\n    if (!typeChangesBuilder.isEmpty()) {\n      finalBuilder.putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY, typeChangesBuilder.toArray(new FieldMetadata[0]));\n    }\n    return finalBuilder.build();\n  }\n\n  private static String typeAsString(DataType dt) {\n    String jsonString = DataTypeJsonSerDe.serializeDataType(dt);\n    // Remove leading/trailing quotes.\n    return jsonString.substring(1, jsonString.length() - 1);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/StructType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\nimport io.delta.kernel.internal.util.Tuple2;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\n/**\n * Struct type which contains one or more columns.\n *\n * @since 3.0.0\n */\n@Evolving\npublic final class StructType extends DataType {\n\n  private final Map<String, Tuple2<StructField, Integer>> nameToFieldAndOrdinal;\n  private final List<StructField> fields;\n  private final List<String> fieldNames;\n\n  public StructType() {\n    this(new ArrayList<>());\n  }\n\n  public StructType(List<StructField> fields) {\n    // Extract all nested fields and ensure that they do not contain metadata columns\n    validateNoMetadataColumns(\n        fields.stream()\n            .filter(f -> !(f.getDataType() instanceof BasePrimitiveType))\n            .collect(Collectors.toList()));\n\n    // Ensure that there are no duplicate metadata columns at the top level\n    Set<MetadataColumnSpec> seenMetadataCols = new HashSet<>();\n    for (StructField field : fields) {\n      if (field.isMetadataColumn()) {\n        MetadataColumnSpec colType = field.getMetadataColumnSpec();\n        if (seenMetadataCols.contains(colType)) {\n          throw new IllegalArgumentException(\n              String.format(\"Duplicate metadata column %s found in struct type\", colType));\n        }\n        seenMetadataCols.add(colType);\n      }\n    }\n\n    this.fields = fields;\n    this.fieldNames = fields.stream().map(f -> f.getName()).collect(Collectors.toList());\n\n    this.nameToFieldAndOrdinal = new HashMap<>();\n    for (int i = 0; i < fields.size(); i++) {\n      nameToFieldAndOrdinal.put(fields.get(i).getName(), new Tuple2<>(fields.get(i), i));\n    }\n  }\n\n  public StructType add(StructField field) {\n    final List<StructField> fieldsCopy = new ArrayList<>(fields);\n    fieldsCopy.add(field);\n\n    return new StructType(fieldsCopy);\n  }\n\n  public StructType add(String name, DataType dataType) {\n    return add(new StructField(name, dataType, true /* nullable */));\n  }\n\n  public StructType add(String name, DataType dataType, boolean nullable) {\n    return add(new StructField(name, dataType, nullable));\n  }\n\n  public StructType add(String name, DataType dataType, FieldMetadata metadata) {\n    return add(new StructField(name, dataType, true /* nullable */, metadata));\n  }\n\n  public StructType add(String name, DataType dataType, boolean nullable, FieldMetadata metadata) {\n    return add(new StructField(name, dataType, nullable, metadata));\n  }\n\n  /** Add a predefined metadata column of {@link MetadataColumnSpec} to the struct type. */\n  public StructType addMetadataColumn(String name, MetadataColumnSpec colType) {\n    return add(StructField.createMetadataColumn(name, colType));\n  }\n\n  /** @return array of fields */\n  public List<StructField> fields() {\n    return Collections.unmodifiableList(fields);\n  }\n\n  /** @return array of field names */\n  public List<String> fieldNames() {\n    return fieldNames;\n  }\n\n  /** @return the number of fields */\n  public int length() {\n    return fields.size();\n  }\n\n  /** @return the index of the field with the given name, or -1 if not found */\n  public int indexOf(String fieldName) {\n    Tuple2<StructField, Integer> fieldAndOrdinal = nameToFieldAndOrdinal.get(fieldName);\n    return fieldAndOrdinal != null ? fieldAndOrdinal._2 : -1;\n  }\n\n  /** @return the index of the metadata column of the given spec, or -1 if not found */\n  public int indexOf(MetadataColumnSpec spec) {\n    // We only allow each metadata column type to appear at most once in the schema and only at top\n    // level (i.e., not nested).\n    for (int i = 0; i < fields.size(); i++) {\n      if (spec.equals(fields.get(i).getMetadataColumnSpec())) {\n        return i;\n      }\n    }\n    return -1; // Not found\n  }\n\n  /** @return true if the struct type contains a metadata column of the given spec */\n  public boolean contains(MetadataColumnSpec spec) {\n    return indexOf(spec) >= 0;\n  }\n\n  public StructField get(String fieldName) {\n    return nameToFieldAndOrdinal.get(fieldName)._1;\n  }\n\n  public StructField at(int index) {\n    return fields.get(index);\n  }\n\n  /**\n   * Creates a {@link Column} expression for the field at the given {@code ordinal}\n   *\n   * @param ordinal the ordinal of the {@link StructField} to create a column for\n   * @return a {@link Column} expression for the {@link StructField} with ordinal {@code ordinal}\n   */\n  public Column column(int ordinal) {\n    final StructField field = at(ordinal);\n    return new Column(field.getName());\n  }\n\n  /**\n   * Convert the struct type to Delta protocol specified serialization format.\n   *\n   * @return serialized in JSON format.\n   */\n  public String toJson() {\n    return DataTypeJsonSerDe.serializeStructType(this);\n  }\n\n  @Override\n  public boolean equivalent(DataType dataType) {\n    if (!(dataType instanceof StructType)) {\n      return false;\n    }\n\n    StructType otherType = ((StructType) dataType);\n    return otherType.length() == length()\n        && IntStream.range(0, length())\n            .mapToObj(i -> otherType.at(i).getDataType().equivalent(at(i).getDataType()))\n            .allMatch(result -> result);\n  }\n\n  /**\n   * Checks whether the given {@code dataType} is compatible with this type when writing data.\n   * Collation differences are ignored.\n   *\n   * <p>This method is intended to be used during the write path to validate that an input type\n   * matches the expected schema before data is written.\n   *\n   * <p>It should not be used in other cases, such as the read path.\n   *\n   * @param dataType the input data type being written\n   * @return {@code true} if the input type is compatible with this type.\n   */\n  @Override\n  public boolean isWriteCompatible(DataType dataType) {\n    if (this == dataType) {\n      return true;\n    }\n    if (dataType == null || getClass() != dataType.getClass()) {\n      return false;\n    }\n    StructType structType = (StructType) dataType;\n    return this.length() == structType.length()\n        && fieldNames.equals(structType.fieldNames)\n        && IntStream.range(0, this.length())\n            .mapToObj(\n                i -> {\n                  StructField thisField = this.at(i);\n                  StructField otherField = structType.at(i);\n                  return (thisField == null && otherField == null)\n                      || (thisField != null && thisField.isWriteCompatible(otherField));\n                })\n            .allMatch(result -> result);\n  }\n\n  @Override\n  public boolean isNested() {\n    return true;\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"struct(%s)\", fields.stream().map(StructField::toString).collect(Collectors.joining(\", \")));\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    StructType that = (StructType) o;\n    return nameToFieldAndOrdinal.equals(that.nameToFieldAndOrdinal)\n        && fields.equals(that.fields)\n        && fieldNames.equals(that.fieldNames);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(nameToFieldAndOrdinal, fields, fieldNames);\n  }\n\n  /**\n   * Validates that there are no metadata columns in a list of StructFields.\n   *\n   * @param fields The list of fields to validate\n   * @throws IllegalArgumentException if any nested metadata columns are found\n   */\n  private void validateNoMetadataColumns(List<StructField> fields) {\n    for (StructField field : fields) {\n      DataType dataType = field.getDataType();\n\n      if (dataType instanceof StructType) {\n        StructType structType = (StructType) dataType;\n        // We filter out nested StructTypes since they have already been validated at their creation\n        validateNoMetadataColumns(\n            structType.fields().stream()\n                .filter(f -> !(f.getDataType() instanceof StructType))\n                .collect(Collectors.toList()));\n      } else if (dataType instanceof MapType) {\n        MapType mapType = (MapType) dataType;\n        validateNoMetadataColumns(Arrays.asList(mapType.getKeyField(), mapType.getValueField()));\n      } else if (dataType instanceof ArrayType) {\n        ArrayType arrayType = (ArrayType) dataType;\n        validateNoMetadataColumns(Collections.singletonList(arrayType.getElementField()));\n      } else if (field.isMetadataColumn()) {\n        throw new IllegalArgumentException(\n            \"Metadata columns are only allowed at the top level of a schema.\");\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/TimestampNTZType.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * The timestamp without time zone type represents a local time in microsecond precision, which is\n * independent of time zone. Its valid range is [0001-01-01T00:00:00.000000,\n * 9999-12-31T23:59:59.999999]. To represent an absolute point in time, use {@link TimestampType}\n * instead.\n *\n * @since 3.2.0\n */\n@Evolving\npublic class TimestampNTZType extends BasePrimitiveType {\n  public static final TimestampNTZType TIMESTAMP_NTZ = new TimestampNTZType();\n\n  private TimestampNTZType() {\n    super(\"timestamp_ntz\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/TimestampType.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * A timestamp type, supporting [0001-01-01T00:00:00.000000Z, 9999-12-31T23:59:59.999999Z] where the\n * left/right-bound is a date and time of the proleptic Gregorian calendar in UTC+00:00. Internally,\n * this is represented as the number of microseconds since the Unix epoch, 1970-01-01 00:00:00 UTC.\n *\n * <p>Due to historical reasons timestamp partition columns do not store timezone information.\n * Kernel interprets all timestamp partition columns in UTC.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class TimestampType extends BasePrimitiveType {\n  public static final TimestampType TIMESTAMP = new TimestampType();\n\n  private TimestampType() {\n    super(\"timestamp\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/TypeChange.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport java.util.Objects;\n\n/**\n * Represents a type change for a field, containing the original and new primitive types.\n *\n * <p>Type changes are actually persisted in metadata attached to StructFields but the rules for\n * where the metadata is attached depend on if the change is for nested arrays/maps or primitive\n * types.\n */\npublic class TypeChange {\n  private final DataType from;\n  private final DataType to;\n\n  public TypeChange(DataType from, DataType to) {\n    this.from = Objects.requireNonNull(from, \"from type cannot be null\");\n    this.to = Objects.requireNonNull(to, \"to type cannot be null\");\n  }\n\n  public DataType getFrom() {\n    return from;\n  }\n\n  public DataType getTo() {\n    return to;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    TypeChange that = (TypeChange) o;\n    return Objects.equals(from, that.from) && Objects.equals(to, that.to);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(from, to);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"TypeChange(from=%s,to=%s)\", from, to);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/VariantType.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types;\n\nimport io.delta.kernel.annotation.Evolving;\n\n/**\n * A logical variant type.\n *\n * <p>The RFC for the variant data type can be found at\n * https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md.\n *\n * @since 3.3.0\n */\n@Evolving\npublic class VariantType extends BasePrimitiveType {\n  public static final VariantType VARIANT = new VariantType();\n\n  private VariantType() {\n    super(\"variant\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/types/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Data types defined by the Delta Kernel to exchange the type info between the Delta Kernel and the\n * connectors.\n */\npackage io.delta.kernel.types;\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/utils/CloseableIterable.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.utils;\n\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\n\nimport io.delta.kernel.exceptions.KernelException;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.function.Consumer;\n\n/**\n * Extend the Java {@link Iterable} interface to provide a way to close the iterator.\n *\n * @param <T> the type of elements returned by this iterator\n */\npublic interface CloseableIterable<T> extends Iterable<T>, Closeable {\n\n  /**\n   * Overrides the default iterator method to return a {@link CloseableIterator}.\n   *\n   * @return a {@link CloseableIterator} instance.\n   */\n  @Override\n  CloseableIterator<T> iterator();\n\n  @Override\n  default void forEach(Consumer<? super T> action) {\n    try (CloseableIterator<T> iterator = iterator()) {\n      iterator.forEachRemaining(action);\n    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  default Spliterator<T> spliterator() {\n    // We need a way to close the iterator and is not used in Kernel, so for now\n    // make the default implementation throw an exception.\n    throw new UnsupportedOperationException(\"spliterator is not supported\");\n  }\n\n  /**\n   * Return an {@link CloseableIterable} object that is backed by an in-memory collection of given\n   * {@link CloseableIterator}. Users should aware that the returned {@link CloseableIterable} will\n   * hold the data in memory.\n   *\n   * @param iterator the iterator to be converted to a {@link CloseableIterable}. It will be closed\n   *     by this callee.\n   * @param <T> the type of elements returned by the iterator\n   * @return a {@link CloseableIterable} instance.\n   */\n  static <T> CloseableIterable<T> inMemoryIterable(CloseableIterator<T> iterator) {\n    final ArrayList<T> elements = new ArrayList<>();\n    try (CloseableIterator<T> iter = iterator) {\n      while (iter.hasNext()) {\n        elements.add(iter.next());\n      }\n    } catch (Exception e) {\n      // TODO: we may need utility methods to throw the KernelException as is\n      // without wrapping in RuntimeException.\n      if (e instanceof KernelException) {\n        throw (KernelException) e;\n      } else {\n        throw new RuntimeException(e);\n      }\n    }\n    return new CloseableIterable<T>() {\n      @Override\n      public void close() throws IOException {\n        // nothing to close\n      }\n\n      @Override\n      public CloseableIterator<T> iterator() {\n        return toCloseableIterator(elements.iterator());\n      }\n    };\n  }\n\n  /**\n   * Return an {@link CloseableIterable} object for an empty collection.\n   *\n   * @return a {@link CloseableIterable} instance.\n   * @param <T> the type of elements returned by the iterator\n   */\n  static <T> CloseableIterable<T> emptyIterable() {\n    final CloseableIterator<T> EMPTY_ITERATOR =\n        toCloseableIterator(Collections.<T>emptyList().iterator());\n    return new CloseableIterable<T>() {\n      @Override\n      public void close() throws IOException {\n        // nothing to close\n      }\n\n      @Override\n      public CloseableIterator<T> iterator() {\n        return EMPTY_ITERATOR;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/utils/CloseableIterator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.utils;\n\nimport io.delta.kernel.annotation.Evolving;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.KernelEngineException;\nimport io.delta.kernel.exceptions.KernelException;\nimport io.delta.kernel.internal.util.Utils;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.NoSuchElementException;\nimport java.util.function.Function;\n\n/**\n * Closeable extension of {@link Iterator}\n *\n * @param <T> the type of elements returned by this iterator\n * @since 3.0.0\n */\n@Evolving\npublic interface CloseableIterator<T> extends Iterator<T>, Closeable {\n\n  /**\n   * Represents the result of applying the filter condition in the {@link\n   * #breakableFilter(Function)} method of a {@link CloseableIterator}. This enum determines how\n   * each element in the iterator should be handled.\n   */\n  enum BreakableFilterResult {\n    /**\n     * Indicates that the current element should be included in the resulting iterator produced by\n     * {@link #breakableFilter(Function)}.\n     */\n    INCLUDE,\n\n    /**\n     * Indicates that the current element should be excluded from the resulting iterator produced by\n     * {@link #breakableFilter(Function)}.\n     */\n    EXCLUDE,\n\n    /**\n     * Indicates that the iteration should stop immediately and that no further elements should be\n     * processed by {@link #breakableFilter(Function)}.\n     */\n    BREAK\n  }\n\n  /**\n   * Returns true if the iteration has more elements. (In other words, returns true if next would\n   * return an element rather than throwing an exception.)\n   *\n   * @return true if the iteration has more elements\n   * @throws KernelEngineException For any underlying exception occurs in {@link Engine} while\n   *     trying to execute the operation. The original exception is (if any) wrapped in this\n   *     exception as cause. E.g. {@link IOException} thrown while trying to read from a Delta log\n   *     file. It will be wrapped in this exception as cause.\n   * @throws KernelException When encountered an operation or state that is invalid or unsupported.\n   */\n  @Override\n  boolean hasNext();\n\n  /**\n   * Returns the next element in the iteration.\n   *\n   * @return the next element in the iteration\n   * @throws NoSuchElementException if the iteration has no more elements\n   * @throws KernelEngineException For any underlying exception occurs in {@link Engine} while\n   *     trying to execute the operation. The original exception is (if any) wrapped in this\n   *     exception as cause. E.g. {@link IOException} thrown while trying to read from a Delta log\n   *     file. It will be wrapped in this exception as cause.\n   * @throws KernelException When encountered an operation or state that is invalid or unsupported\n   *     in Kernel. For example, trying to read from a Delta table that has advanced features which\n   *     are not yet supported by Kernel.\n   */\n  @Override\n  T next();\n\n  default <U> CloseableIterator<U> map(Function<T, U> mapper) {\n    CloseableIterator<T> delegate = this;\n    return new CloseableIterator<U>() {\n      @Override\n      public void remove() {\n        delegate.remove();\n      }\n\n      @Override\n      public boolean hasNext() {\n        return delegate.hasNext();\n      }\n\n      @Override\n      public U next() {\n        return mapper.apply(delegate.next());\n      }\n\n      @Override\n      public void close() throws IOException {\n        delegate.close();\n      }\n    };\n  }\n\n  /**\n   * Returns a new {@link CloseableIterator} that applies a function to each element of this\n   * iterator, where each element is transformed into another iterator, and the results are\n   * flattened into a single iterator.\n   *\n   * <p>Example:\n   *\n   * <pre>{@code\n   * // [1, 2, 3].flatMap(x -> [x, x]) => [1, 1, 2, 2, 3, 3]\n   * iterator.flatMap(commit -> processCommit(commit))\n   * }</pre>\n   *\n   * @param mapper A function that transforms each element into a {@link CloseableIterator}\n   * @param <U> The type of elements in the resulting iterator\n   * @return A flattened {@link CloseableIterator} over all elements from all inner iterators\n   */\n  default <U> CloseableIterator<U> flatMap(Function<T, CloseableIterator<U>> mapper) {\n    CloseableIterator<T> delegate = this;\n    return new CloseableIterator<U>() {\n      private CloseableIterator<U> currentInner = null;\n\n      @Override\n      public boolean hasNext() {\n        while (true) {\n          if (currentInner != null && currentInner.hasNext()) {\n            return true;\n          }\n          if (currentInner != null) {\n            Utils.closeCloseables(currentInner);\n            currentInner = null;\n          }\n          if (!delegate.hasNext()) {\n            return false;\n          }\n          currentInner = mapper.apply(delegate.next());\n        }\n      }\n\n      @Override\n      public U next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        return currentInner.next();\n      }\n\n      @Override\n      public void close() throws IOException {\n        Utils.closeCloseables(currentInner, delegate);\n        currentInner = null;\n      }\n    };\n  }\n\n  /**\n   * Returns a new {@link CloseableIterator} that includes only the elements of this iterator for\n   * which the given {@code mapper} function returns {@code true}.\n   *\n   * @param mapper A function that determines whether an element should be included in the resulting\n   *     iterator.\n   * @return A {@link CloseableIterator} that includes only the filtered the elements of this\n   *     iterator.\n   */\n  default CloseableIterator<T> filter(Function<T, Boolean> mapper) {\n    return breakableFilter(\n        t -> {\n          if (mapper.apply(t)) {\n            return BreakableFilterResult.INCLUDE;\n          } else {\n            return BreakableFilterResult.EXCLUDE;\n          }\n        });\n  }\n\n  /**\n   * Returns a new {@link CloseableIterator} that includes elements from this iterator as long as\n   * the given {@code mapper} function returns {@code true}. Once the mapper function returns {@code\n   * false}, the iteration is terminated.\n   *\n   * @param mapper A function that determines whether to include an element in the resulting\n   *     iterator.\n   * @return A {@link CloseableIterator} that stops iteration when the condition is not met.\n   */\n  default CloseableIterator<T> takeWhile(Function<T, Boolean> mapper) {\n    return breakableFilter(\n        t -> {\n          if (mapper.apply(t)) {\n            return BreakableFilterResult.INCLUDE;\n          } else {\n            return BreakableFilterResult.BREAK;\n          }\n        });\n  }\n\n  /**\n   * Returns a new {@link CloseableIterator} that applies a {@link BreakableFilterResult}-based\n   * filtering function to determine whether elements of this iterator should be included or\n   * excluded, or whether the iteration should terminate.\n   *\n   * @param mapper A function that determines the filtering action for each element: include,\n   *     exclude, or break.\n   * @return A {@link CloseableIterator} that applies the specified {@link\n   *     BreakableFilterResult}-based logic.\n   */\n  default CloseableIterator<T> breakableFilter(Function<T, BreakableFilterResult> mapper) {\n    CloseableIterator<T> delegate = this;\n    return new CloseableIterator<T>() {\n      T next;\n      boolean hasLoadedNext;\n      boolean shouldBreak = false;\n\n      @Override\n      public boolean hasNext() {\n        if (shouldBreak) {\n          return false;\n        }\n        if (hasLoadedNext) {\n          return true;\n        }\n        while (delegate.hasNext()) {\n          final T potentialNext = delegate.next();\n          final BreakableFilterResult result = mapper.apply(potentialNext);\n          if (result == BreakableFilterResult.INCLUDE) {\n            next = potentialNext;\n            hasLoadedNext = true;\n            return true;\n          } else if (result == BreakableFilterResult.BREAK) {\n            shouldBreak = true;\n            return false;\n          }\n        }\n        return false;\n      }\n\n      @Override\n      public T next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        hasLoadedNext = false;\n        return next;\n      }\n\n      @Override\n      public void close() throws IOException {\n        delegate.close();\n      }\n    };\n  }\n\n  /**\n   * Combine the current iterator with another iterator. The resulting iterator will return all\n   * elements from the current iterator followed by all elements from the other iterator.\n   *\n   * @param other the other iterator to combine with\n   * @return a new iterator that combines the current iterator with the other iterator\n   */\n  default CloseableIterator<T> combine(CloseableIterator<T> other) {\n\n    CloseableIterator<T> delegate = this;\n    return new CloseableIterator<T>() {\n      @Override\n      public boolean hasNext() {\n        return delegate.hasNext() || other.hasNext();\n      }\n\n      @Override\n      public T next() {\n        if (delegate.hasNext()) {\n          return delegate.next();\n        } else {\n          return other.next();\n        }\n      }\n\n      @Override\n      public void close() throws IOException {\n        Utils.closeCloseables(delegate, other);\n      }\n    };\n  }\n\n  /**\n   * Collects all elements from this {@link CloseableIterator} into a {@link List}.\n   *\n   * <p>This method iterates through all elements of the iterator, storing them in an in-memory\n   * list. Once iteration is complete, the iterator is automatically closed to release any\n   * underlying resources.\n   *\n   * @return A {@link List} containing all elements from this iterator.\n   * @throws UncheckedIOException If an {@link IOException} occurs while closing the iterator.\n   */\n  default List<T> toInMemoryList() {\n    final List<T> result = new ArrayList<>();\n    try (CloseableIterator<T> iterator = this) {\n      while (iterator.hasNext()) {\n        result.add(iterator.next());\n      }\n    } catch (IOException e) {\n      throw new UncheckedIOException(\"Failed to close the CloseableIterator\", e);\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/utils/DataFileStatus.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.utils;\n\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport java.util.Optional;\n\n/**\n * Extends {@link FileStatus} to include additional details such as column level statistics of the\n * data file in the Delta Lake table.\n */\npublic class DataFileStatus extends FileStatus {\n\n  private final Optional<DataFileStatistics> statistics;\n\n  /**\n   * Create a new instance of {@link DataFileStatus}.\n   *\n   * @param path Fully qualified file path.\n   * @param size File size in bytes.\n   * @param modificationTime Last modification time of the file in epoch milliseconds.\n   * @param statistics Optional column and file level statistics in the data file.\n   */\n  public DataFileStatus(\n      String path, long size, long modificationTime, Optional<DataFileStatistics> statistics) {\n    super(path, size, modificationTime);\n    this.statistics = statistics;\n  }\n\n  /**\n   * Get the statistics of the data file encapsulated in this object.\n   *\n   * @return Statistics of the file.\n   */\n  public Optional<DataFileStatistics> getStatistics() {\n    return statistics;\n  }\n\n  @Override\n  public String toString() {\n    return \"DataFileStatus{\"\n        + \"path='\"\n        + getPath()\n        + '\\''\n        + \", size=\"\n        + getSize()\n        + \", modificationTime=\"\n        + getModificationTime()\n        + \", statistics=\"\n        + statistics\n        + '}';\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/utils/FileStatus.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.utils;\n\nimport io.delta.kernel.annotation.Evolving;\nimport java.util.Objects;\n\n/**\n * Class for encapsulating metadata about a file in Delta Lake table.\n *\n * @since 3.0.0\n */\n@Evolving\npublic class FileStatus {\n\n  //////////////////////////////////\n  // Static variables and methods //\n  //////////////////////////////////\n\n  /**\n   * Create a {@link FileStatus} with the given path, size and modification time.\n   *\n   * @param path Fully qualified file path.\n   * @param size File size in bytes\n   * @param modificationTime Modification time of the file in epoch millis\n   */\n  public static FileStatus of(String path, long size, long modificationTime) {\n    return new FileStatus(path, size, modificationTime);\n  }\n\n  //////////////////////////////////\n  // Member variables and methods //\n  //////////////////////////////////\n\n  private final String path;\n  private final long size;\n  private final long modificationTime;\n\n  // TODO add further documentation about the expected format for modificationTime?\n  protected FileStatus(String path, long size, long modificationTime) {\n    this.path = Objects.requireNonNull(path, \"path is null\");\n    this.size = size; // TODO: validation\n    this.modificationTime = modificationTime; // TODO: validation\n  }\n\n  /**\n   * Get the path to the file.\n   *\n   * @return Fully qualified file path\n   */\n  public String getPath() {\n    return path;\n  }\n\n  /**\n   * Get the size of the file in bytes.\n   *\n   * @return File size in bytes.\n   */\n  public long getSize() {\n    return size;\n  }\n\n  /**\n   * Get the modification time of the file in epoch millis.\n   *\n   * @return Modification time in epoch millis\n   */\n  public long getModificationTime() {\n    return modificationTime;\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"FileStatus{path='%s', size=%d, modificationTime=%d}\", path, size, modificationTime);\n  }\n\n  /**\n   * Create a {@link FileStatus} with the given path with size and modification time set to 0.\n   *\n   * @param path Fully qualified file path.\n   * @return {@link FileStatus} object\n   */\n  public static FileStatus of(String path) {\n    return new FileStatus(path, 0 /* size */, 0 /* modTime */);\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    FileStatus that = (FileStatus) o;\n    return Objects.equals(this.path, that.path)\n        && Objects.equals(this.size, that.size)\n        && Objects.equals(this.modificationTime, that.modificationTime);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(path, size, modificationTime);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/utils/PartitionUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.utils;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Scan;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Predicate;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class PartitionUtils {\n\n  private PartitionUtils() {}\n\n  /**\n   * Check if a partition exists (i.e. actually has data) in the given {@link Snapshot} based on the\n   * given {@link Predicate}.\n   *\n   * @param engine the {@link Engine} to use for scanning the partition.\n   * @param snapshot the {@link Snapshot} to scan.\n   * @param partitionPredicate the {@link Predicate} to use for filtering the partition.\n   * @return true if the partition exists, false otherwise.\n   * @throws IllegalArgumentException if the predicate does not reference any partition columns or\n   *     if it references any data columns\n   */\n  public static boolean partitionExists(\n      Engine engine, Snapshot snapshot, Predicate partitionPredicate) {\n    requireNonNull(engine, \"engine is null\");\n    requireNonNull(snapshot, \"snapshot is null\");\n    requireNonNull(partitionPredicate, \"partitionPredicate is null\");\n\n    final Set<String> snapshotPartColNames = new HashSet<>(snapshot.getPartitionColumnNames());\n\n    io.delta.kernel.internal.util.PartitionUtils.validatePredicateOnlyOnPartitionColumns(\n        partitionPredicate, snapshotPartColNames);\n\n    final Scan scan = snapshot.getScanBuilder().withFilter(partitionPredicate).build();\n\n    try (CloseableIterator<FilteredColumnarBatch> columnarBatchIter = scan.getScanFiles(engine)) {\n      while (columnarBatchIter.hasNext()) {\n        try (CloseableIterator<Row> selectedRowsIter = columnarBatchIter.next().getRows()) {\n          if (selectedRowsIter.hasNext()) {\n            return true;\n          }\n        }\n      }\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/main/java/io/delta/kernel/utils/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** Utilities. */\npackage io.delta.kernel.utils;\n"
  },
  {
    "path": "kernel/kernel-api/src/test/resources/log4j2.properties",
    "content": "#\n#  Copyright (2025) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n# Set everything to be logged to the file target/unit-tests.log\nrootLogger.level = warn\nrootLogger.appenderRef.file.ref = ${sys:test.appender:-File}\n\nappender.file.type = File\nappender.file.name = File\nappender.file.fileName = target/unit-tests.log\nappender.file.append = true\nappender.file.layout.type = PatternLayout\nappender.file.layout.pattern = %d{yy/MM/dd HH:mm:ss.SSS} %t %p %c{1}: %m%n\n\n# Tests that launch java subprocesses can set the \"test.appender\" system property to\n# \"console\" to avoid having the child process's logs overwrite the unit test's\n# log file.\nappender.console.type = Console\nappender.console.name = console\nappender.console.target = SYSTEM_ERR\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = %t: %m%n\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/CloseableIteratorSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel\n\nimport scala.collection.JavaConverters._\nimport scala.util.Using\n\nimport io.delta.kernel.internal.util.Utils\nimport io.delta.kernel.utils.CloseableIterator\nimport io.delta.kernel.utils.CloseableIterator.BreakableFilterResult\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CloseableIteratorSuite extends AnyFunSuite {\n\n  private def toCloseableIter[T](elems: Seq[T]): CloseableIterator[T] = {\n    Utils.toCloseableIterator(elems.iterator.asJava)\n  }\n\n  private def toList[T](iter: CloseableIterator[T]): List[T] = {\n    iter.toInMemoryList.asScala.toList\n  }\n\n  private def normalDataIter = toCloseableIter(Seq(1, 2, 3, 4, 5))\n\n  private def throwingDataIter = toCloseableIter(Seq(1, 2, 3, 4, 5)).map { x =>\n    if (x > 4) {\n      throw new RuntimeException(\"Underlying data evaluated at element > 4\")\n    }\n    x\n  }\n\n  /**\n   * A CloseableIterator wrapper that tracks whether close() was called.\n   * Used for testing resource cleanup.\n   */\n  private class TrackingCloseableIterator(\n      elems: Seq[Int],\n      onClose: () => Unit) extends CloseableIterator[Int] {\n    private val iter = elems.iterator\n    private var closed = false\n\n    override def hasNext(): Boolean = {\n      assert(!closed)\n      iter.hasNext\n    }\n    override def next(): Int = iter.next()\n    override def close(): Unit = {\n      if (!closed) {\n        onClose()\n        closed = true\n      }\n    }\n  }\n\n  test(\"CloseableIterator::filter -- returns filtered result\") {\n    val result = normalDataIter.filter(x => x <= 3 || x == 5)\n    assert(toList(result) === List(1, 2, 3, 5))\n  }\n\n  test(\"CloseableIterator::filter -- iterates over all elements\") {\n    intercept[RuntimeException] {\n      toList(throwingDataIter.filter(x => x <= 3))\n    }\n  }\n\n  test(\"CloseableIterator::takeWhile -- stops iteration at first false condition\") {\n    // we expect it to evaluate 1, 2, 3, 4; break when it sees x == 4; and only return 1, 2, 3\n    val result = throwingDataIter.takeWhile(x => x <= 3)\n    assert(toList(result) === List(1, 2, 3))\n  }\n\n  test(\"CloseableIterator::breakableFilter -- correctly filters and breaks iteration\") {\n    val result = throwingDataIter.breakableFilter { x =>\n      if (x <= 1 || x == 3) {\n        BreakableFilterResult.INCLUDE\n      } else if (x == 2) {\n        BreakableFilterResult.EXCLUDE\n      } else if (x == 4) {\n        BreakableFilterResult.BREAK\n      } else {\n        throw new RuntimeException(\"This should never be reached\")\n      }\n    }\n    // we except it to include 1; exclude 2; include 3; and break at 4, thus never seeing 5\n    assert(toList(result) === List(1, 3))\n  }\n\n  test(\"flatten -- flattens nested iterators\") {\n    // Create an iterator of iterators\n    val nestedIter = toCloseableIter(\n      Seq(\n        toCloseableIter(Seq(1, 2)),\n        toCloseableIter(Seq(3, 4, 5)),\n        toCloseableIter(Seq(6))))\n\n    val result = Utils.flatten(nestedIter)\n    assert(toList(result) === List(1, 2, 3, 4, 5, 6))\n  }\n\n  test(\"flatten -- handles empty inner iterators\") {\n    val nestedIter = toCloseableIter(\n      Seq(\n        toCloseableIter(Seq(1, 2)),\n        toCloseableIter(Seq[Int]()),\n        toCloseableIter(Seq(3, 4)),\n        toCloseableIter(Seq[Int]()),\n        toCloseableIter(Seq(5))))\n\n    val result = Utils.flatten(nestedIter)\n\n    assert(toList(result) === List(1, 2, 3, 4, 5))\n  }\n\n  test(\"flatten -- handles empty outer iterator\") {\n    val nestedIter = toCloseableIter(Seq[CloseableIterator[Int]]())\n\n    val result = Utils.flatten(nestedIter)\n\n    assert(toList(result) === List())\n  }\n\n  test(\"flatten -- properly closes inner iterators\") {\n    var innerClosedCount = 0\n    var outerClosed = false\n    val nestedIter = new CloseableIterator[CloseableIterator[Int]] {\n      private val iter =\n        Seq(\n          new TrackingCloseableIterator(Seq(1, 2), () => innerClosedCount += 1),\n          new TrackingCloseableIterator(Seq(3, 4), () => innerClosedCount += 1)).iterator\n      override def hasNext(): Boolean = iter.hasNext\n      override def next(): CloseableIterator[Int] = iter.next()\n      override def close(): Unit = {\n        outerClosed = true\n      }\n    }\n\n    val result = Utils.flatten(nestedIter)\n\n    // Consume the iterator fully\n    toList(result)\n\n    // All inner iterators should have been closed (2 inner iterators)\n    assert(innerClosedCount === 2)\n    // Outer iterator should also be closed\n    assert(outerClosed === true)\n  }\n\n  test(\"flatten -- closes iterators even when not fully consumed\") {\n    var innerClosedCount = 0\n    var outerClosed = false\n\n    val nestedIter = new CloseableIterator[CloseableIterator[Int]] {\n      private val iter = Seq(\n        new TrackingCloseableIterator(Seq(1, 2), () => innerClosedCount += 1),\n        new TrackingCloseableIterator(Seq(3, 4), () => innerClosedCount += 1),\n        new TrackingCloseableIterator(Seq(5, 6), () => innerClosedCount += 1)).iterator\n      override def hasNext(): Boolean = iter.hasNext\n      override def next(): CloseableIterator[Int] = iter.next()\n      override def close(): Unit = {\n        outerClosed = true\n      }\n    }\n\n    val result = Utils.flatten(nestedIter)\n\n    // Only consume first 3 elements (from first 2 inner iterators)\n    assert(result.hasNext === true)\n    assert(result.next() === 1)\n    assert(result.next() === 2)\n    assert(result.next() === 3)\n\n    // Explicitly close without consuming all\n    result.close()\n    // First two are closed.\n    assert(innerClosedCount === 2)\n    assert(outerClosed === true)\n  }\n\n  test(\"flatten -- handles exception during iteration and cleans up\") {\n    var innerClosedCount = 0\n    var outerClosed = false\n\n    val nestedIter = new CloseableIterator[CloseableIterator[Int]] {\n      private var count = 0\n      override def hasNext(): Boolean = count < 3\n      override def next(): CloseableIterator[Int] = {\n        count += 1\n        if (count == 2) {\n          throw new RuntimeException(\"Test exception during next()\")\n        }\n        new TrackingCloseableIterator(Seq(1, 2), () => innerClosedCount += 1)\n      }\n      override def close(): Unit = {\n        outerClosed = true\n      }\n    }\n\n    val result = Utils.flatten(nestedIter)\n\n    // Consume first inner iterator completely\n    assert(result.hasNext === true)\n    assert(result.next() === 1)\n    assert(result.next() === 2)\n\n    // This should trigger the exception when trying to get the next inner iterator\n    val exception = intercept[RuntimeException] {\n      result.hasNext\n    }\n    assert(exception.getMessage === \"Test exception during next()\")\n\n    // Verify that the outer iterator was closed due to exception\n    assert(outerClosed === true)\n  }\n\n  test(\"iteratorLast -- returns empty for empty iterator\") {\n    val result = Utils.iteratorLast(toCloseableIter(Seq[Int]()))\n    assert(!result.isPresent)\n  }\n\n  test(\"iteratorLast -- returns last element for single element iterator\") {\n    val result = Utils.iteratorLast(toCloseableIter(Seq(42)))\n    assert(result.isPresent)\n    assert(result.get() === 42)\n  }\n\n  test(\"iteratorLast -- returns last element for multiple element iterator\") {\n    val result = Utils.iteratorLast(toCloseableIter(Seq(1, 2, 3, 4, 5)))\n    assert(result.isPresent)\n    assert(result.get() === 5)\n  }\n\n  test(\"iteratorLast -- properly closes iterator after consumption\") {\n    var closed = false\n    val iter = new TrackingCloseableIterator(Seq(1, 2, 3), () => closed = true)\n    val result = Utils.iteratorLast(iter)\n    assert(result.isPresent)\n    assert(result.get() === 3)\n    assert(closed === true)\n  }\n\n  test(\"flatMap -- basic functionality\") {\n    val input = toCloseableIter(Seq(1, 2, 3))\n    val result = input.flatMap { x: Int =>\n      toCloseableIter(Seq(x, x * 10))\n    }\n    assert(toList(result) === List(1, 10, 2, 20, 3, 30))\n  }\n\n  test(\"flatMap -- properly closes all resources\") {\n    var outerClosed = false\n    var innerClosedCount = 0\n\n    val outer = new TrackingCloseableIterator(Seq(1, 2, 3), () => outerClosed = true)\n\n    val result = outer.flatMap { x: Int =>\n      new TrackingCloseableIterator(\n        Seq(x, x * 10),\n        () => innerClosedCount += 1): CloseableIterator[Int]\n    }\n\n    assert(toList(result) === List(1, 10, 2, 20, 3, 30))\n\n    // 3 inner iterators, one per outer element\n    assert(innerClosedCount === 3)\n    assert(outerClosed === true)\n  }\n\n  test(\"flatMap -- closes resources when mapper function throws\") {\n    var outerClosed = false\n    val outer = new TrackingCloseableIterator(Seq(1, 2, 3), () => outerClosed = true)\n\n    val result = outer.flatMap { x: Int =>\n      (throw new RuntimeException(\"Error in mapper\")): CloseableIterator[Int]\n    }\n\n    // Use scala's equivalent of java's try-with-resources\n    val exception = intercept[RuntimeException] {\n      Using.resource(result) { r =>\n        r.hasNext\n      }\n    }\n    assert(exception.getMessage === \"Error in mapper\")\n    assert(outerClosed === true)\n  }\n\n  test(\"flatMap -- closes resources when inner iterator throws during consumption\") {\n    var outerClosed = false\n    var innerClosedCount = 0\n\n    val outer = new TrackingCloseableIterator(Seq(1, 2, 3), () => outerClosed = true)\n\n    val result = outer.flatMap { x: Int =>\n      (new TrackingCloseableIterator(Seq(x, x * 10), () => innerClosedCount += 1) {\n        override def next(): Int = {\n          val value = super.next()\n          if (value == 20) {\n            throw new RuntimeException(\"Error reading value 20\")\n          }\n          value\n        }\n      }): CloseableIterator[Int]\n    }\n\n    // Use scala's equivalent of java's try-with-resources\n    val exception = intercept[RuntimeException] {\n      Using.resource(result) { r =>\n        // First inner iterator\n        assert(r.next() === 1)\n        assert(r.next() === 10)\n\n        // Second inner iterator\n        assert(r.next() === 2)\n\n        // Second inner iterator -- throws on next value (20)\n        r.next()\n      }\n    }\n    assert(exception.getMessage === \"Error reading value 20\")\n\n    assert(outerClosed === true)\n    assert(innerClosedCount === 2) // Both inner iterators that were created should be closed\n  }\n\n  test(\"flatMap -- mapper returns null\") {\n    var outerClosed = false\n    val outer = new TrackingCloseableIterator(Seq(1, 2, 3), () => outerClosed = true)\n\n    val result = outer.flatMap { x: Int =>\n      if (x % 2 == 0) {\n        null.asInstanceOf[CloseableIterator[Int]]\n      } else {\n        toCloseableIter(Seq(x, x * 10))\n      }\n    }\n\n    assert(toList(result) === List(1, 10, 3, 30))\n    assert(outerClosed === true)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/TransactionSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel\n\nimport java.lang.{Long => JLong}\nimport java.util\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.Transaction.{generateAppendActions, getWriteContext, transformLogicalData}\nimport io.delta.kernel.data._\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.{DataWriteContextImpl, TableConfig, TransactionImpl}\nimport io.delta.kernel.internal.TableConfig.{COLUMN_MAPPING_MODE, ICEBERG_COMPAT_V2_ENABLED, ICEBERG_COMPAT_V3_ENABLED}\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.data.TransactionStateRow\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.internal.util.VectorUtils.stringStringMapValue\nimport io.delta.kernel.statistics.DataFileStatistics\nimport io.delta.kernel.test.{MockEngineUtils, VectorTestUtils}\nimport io.delta.kernel.types.{DoubleType, FloatType, IntegerType, LongType, StringType, StructField, StructType, TimestampType, VariantType}\nimport io.delta.kernel.utils.{CloseableIterator, DataFileStatus}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TransactionSuite extends AnyFunSuite with VectorTestUtils with MockEngineUtils {\n\n  import io.delta.kernel.TransactionSuite._\n\n  def withIcebergCompatVersions(testNamePrefix: String)(\n      body: (Boolean, Boolean) => Unit): Unit = {\n    Seq((false, false), (true, false), (false, true)).foreach {\n      case (v2, v3) =>\n        test(s\"$testNamePrefix, icebergCompatV2Enabled=$v2 icebergCompatV3Enabled=$v3\") {\n          body(v2, v3)\n        }\n    }\n  }\n\n  withIcebergCompatVersions(\"transformLogicalData: un-partitioned table\") {\n    case (icebergCompatV2Enabled, icebergCompatV3Enabled) =>\n      val transformedDateIter = transformLogicalData(\n        mockEngine(),\n        testTxnState(\n          testSchema,\n          enableIcebergCompatV2 = icebergCompatV2Enabled,\n          enableIcebergCompatV3 = icebergCompatV3Enabled),\n        testData(includePartitionCols = false),\n        Map.empty[String, Literal].asJava /* partition values */ )\n      transformedDateIter.map(_.getData).forEachRemaining(batch => {\n        assert(batch.getSchema === testSchema)\n      })\n  }\n\n  withIcebergCompatVersions(\"transformLogicalData: partitioned table\") {\n    case (icebergCompatV2Enabled, icebergCompatV3Enabled) =>\n      val transformedDateIter = transformLogicalData(\n        mockEngine(),\n        testTxnState(\n          testSchemaWithPartitions,\n          testPartitionColNames,\n          enableIcebergCompatV2 = icebergCompatV2Enabled,\n          enableIcebergCompatV3 = icebergCompatV3Enabled),\n        testData(includePartitionCols = true),\n        /* partition values */\n        Map(\"state\" -> Literal.ofString(\"CA\"), \"country\" -> Literal.ofString(\"USA\")).asJava)\n\n      transformedDateIter.map(_.getData).forEachRemaining { batch =>\n        if (icebergCompatV2Enabled || icebergCompatV3Enabled) {\n          // when icebergCompatV2Enabled is true, the partition columns included in the output\n          assert(batch.getSchema === testSchemaWithPartitions)\n        } else {\n          assert(batch.getSchema === testSchema)\n        }\n      }\n  }\n\n  test(\"transformLogicalData: partitioned table with MaterializePartitionColumns feature\") {\n    val transformedDateIter = transformLogicalData(\n      mockEngine(),\n      testTxnState(\n        testSchemaWithPartitions,\n        testPartitionColNames,\n        enableMaterializePartitionColumns = true),\n      testData(includePartitionCols = true),\n      /* partition values */\n      Map(\"state\" -> Literal.ofString(\"CA\"), \"country\" -> Literal.ofString(\"USA\")).asJava)\n\n    transformedDateIter.map(_.getData).forEachRemaining { batch =>\n      // when MaterializePartitionColumns feature is enabled, partition columns are included\n      assert(batch.getSchema === testSchemaWithPartitions)\n    }\n  }\n\n  test(\"transformLogicalData: partitioned table without MaterializePartitionColumns feature\") {\n    val transformedDateIter = transformLogicalData(\n      mockEngine(),\n      testTxnState(\n        testSchemaWithPartitions,\n        testPartitionColNames,\n        enableMaterializePartitionColumns = false),\n      testData(includePartitionCols = true),\n      /* partition values */\n      Map(\"state\" -> Literal.ofString(\"CA\"), \"country\" -> Literal.ofString(\"USA\")).asJava)\n\n    transformedDateIter.map(_.getData).forEachRemaining { batch =>\n      // when MaterializePartitionColumns feature is disabled, partition columns are filtered out\n      assert(batch.getSchema === testSchema)\n    }\n  }\n\n  withIcebergCompatVersions(\"generateAppendActions: iceberg comaptibily checks\") {\n    case (icebergCompatV2Enabled, icebergCompatV3Enabled) =>\n      val txnState = testTxnState(\n        testSchema,\n        enableIcebergCompatV2 = icebergCompatV2Enabled,\n        enableIcebergCompatV3 = icebergCompatV3Enabled)\n      val engine = mockEngine()\n\n      Seq(\n        // missing stats\n        (\n          testDataFileStatuses(\n            \"file1\" -> testStats(Some(10)), // valid stats\n            \"file2\" -> None // missing stats\n          ),\n          \"compatibility requires 'numRecords' statistic.\" // expected error message\n        )).foreach { case (actionRows, expectedErrorMsg) =>\n        if (icebergCompatV2Enabled || icebergCompatV3Enabled) {\n          val ex = intercept[KernelException] {\n            generateAppendActions(engine, txnState, actionRows, testDataWriteContext())\n              .forEachRemaining(_ => ()) // consume the iterator\n          }\n          assert(ex.getMessage.contains(expectedErrorMsg))\n        } else {\n          // when icebergCompatV2Enabled is disabled, no exception should be thrown\n          generateAppendActions(engine, txnState, actionRows, testDataWriteContext())\n            .forEachRemaining(_ => ()) // consume the iterator\n        }\n      }\n\n      // valid stats\n      val dataFileStatuses = testDataFileStatuses(\n        \"file1\" -> testStats(Some(10)),\n        \"file2\" -> testStats(Some(20)))\n      var actStats: Seq[String] = Seq.empty\n      generateAppendActions(engine, txnState, dataFileStatuses, testDataWriteContext())\n        .forEachRemaining { addActionRow =>\n          val addOrdinal = addActionRow.getSchema.indexOf(\"add\")\n          val add = addActionRow.getStruct(addOrdinal)\n          val statsOrdinal = add.getSchema.indexOf(\"stats\")\n          actStats = actStats :+ add.getString(statsOrdinal)\n        }\n\n      assert(actStats === Seq(\n        \"{\\\"numRecords\\\":10,\\\"minValues\\\":{},\\\"maxValues\\\":{},\\\"nullCount\\\":{}}\",\n        \"{\\\"numRecords\\\":20,\\\"minValues\\\":{},\\\"maxValues\\\":{},\\\"nullCount\\\":{}}\"))\n  }\n\n  Seq(0, -1).foreach { numIndexedCols =>\n    test(s\"stats: validate DATA_SKIPPING_NUM_INDEXED_COLS limit\" +\n      s\" is respected when set to: $numIndexedCols\") {\n      // Create schema with simple and nested columns\n      val schema = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"name\", StringType.STRING)\n        .add(\n          \"metrics\",\n          new StructType()\n            .add(\"temperature\", DoubleType.DOUBLE)\n            .add(\"humidity\", FloatType.FLOAT))\n        .add(\"timestamp\", TimestampType.TIMESTAMP)\n\n      // Create transaction state with specified numIndexedCols\n      val configMap = Map(TableConfig\n        .DATA_SKIPPING_NUM_INDEXED_COLS.getKey -> numIndexedCols.toString)\n      val metadata = new Metadata(\n        \"id\",\n        Optional.empty(),\n        Optional.empty(),\n        new Format(),\n        DataTypeJsonSerDe.serializeDataType(schema),\n        schema,\n        VectorUtils.buildArrayValue(Seq.empty.asJava, StringType.STRING),\n        Optional.empty(),\n        stringStringMapValue(configMap.asJava))\n      val protocol = new Protocol(1, 1) // simple protocol for this test\n      val txnState = TransactionStateRow.of(\n        metadata,\n        protocol,\n        \"table path\",\n        200 /* maxRetries */ )\n\n      // Get statistics columns and define expected result\n      val statsColumns = TransactionImpl.getStatisticsColumns(txnState)\n      if (numIndexedCols == -1) {\n        // For -1, all leaf columns should be included\n        val expectedColumns = Set(\n          new Column(\"id\"),\n          new Column(\"name\"),\n          new Column(Array(\"metrics\", \"temperature\")),\n          new Column(Array(\"metrics\", \"humidity\")),\n          new Column(\"timestamp\"))\n\n        assert(\n          statsColumns.size == 5,\n          s\"With numIndexedCols=$numIndexedCols, expected 5 columns but got ${statsColumns.size}\")\n\n        // Verify the expected columns are present\n        val statsColumnsSet = statsColumns.asScala.toSet\n        assert(statsColumnsSet == expectedColumns, s\"Expected columns do not match actual columns\")\n      } else if (numIndexedCols == 0) {\n        // For 0, no columns should be included\n        assert(\n          statsColumns.isEmpty,\n          s\"With numIndexedCols=$numIndexedCols,\" +\n            s\" expected no columns but got ${statsColumns.size} columns\")\n      }\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { cmMode =>\n    test(s\"transformLogicalData: CM tables are blocked: cmMode=$cmMode\") {\n      val txnState = testTxnState(new StructType(), cmMode = cmMode)\n      val engine = mockEngine()\n\n      val ex = intercept[UnsupportedOperationException] {\n        transformLogicalData(\n          engine,\n          txnState,\n          testData(includePartitionCols = false),\n          Map.empty[String, Literal].asJava /* partition values */ )\n          .forEachRemaining(_ => ()) // consume the iterator\n      }\n      assert(ex.getMessage.contains(\n        \"Writing into column mapping enabled table is not supported yet.\"))\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { cmMode =>\n    test(s\"getWriteContext: CM tables are blocked: $cmMode\") {\n      val txnState = testTxnState(new StructType(), cmMode = cmMode)\n      val engine = mockEngine()\n\n      val ex = intercept[UnsupportedOperationException] {\n        getWriteContext(\n          engine,\n          txnState,\n          Map.empty[String, Literal].asJava /* partition values */ )\n      }\n      assert(ex.getMessage.contains(\n        \"Writing into column mapping enabled table is not supported yet.\"))\n    }\n  }\n\n  test(\"transformLogicalData: Writing to tables with variant is blocked\") {\n    val txnState = testTxnState(new StructType().add(\"variant\", VariantType.VARIANT))\n    val engine = mockEngine()\n\n    val ex = intercept[UnsupportedOperationException] {\n      transformLogicalData(\n        engine,\n        txnState,\n        testData(includePartitionCols = false),\n        Map.empty[String, Literal].asJava /* partition values */ )\n        .forEachRemaining(_ => ()) // consume the iterator\n    }\n    assert(ex.getMessage.contains(\n      \"Transforming logical data with variant data is currently unsupported\"))\n  }\n\n  test(\"transformLogicalData: Writing to tables with nested variant is blocked\") {\n    val txnState = testTxnState(new StructType().add(\n      \"nested\",\n      new StructType().add(\"nested_variant\", VariantType.VARIANT)))\n    val engine = mockEngine()\n\n    val ex = intercept[UnsupportedOperationException] {\n      transformLogicalData(\n        engine,\n        txnState,\n        testData(includePartitionCols = false),\n        Map.empty[String, Literal].asJava /* partition values */ )\n        .forEachRemaining(_ => ()) // consume the iterator\n    }\n    assert(ex.getMessage.contains(\n      \"Transforming logical data with variant data is currently unsupported\"))\n  }\n}\n\nobject TransactionSuite extends VectorTestUtils with MockEngineUtils {\n  def testData(includePartitionCols: Boolean): CloseableIterator[FilteredColumnarBatch] = {\n    toCloseableIterator(\n      Seq.range(0, 5).map(_ => testBatch(includePartitionCols)).asJava.iterator()).map(batch =>\n      new FilteredColumnarBatch(batch, Optional.empty()))\n  }\n\n  def testBatch(includePartitionCols: Boolean): ColumnarBatch = {\n    val testColumnVectors = Seq(\n      stringVector(Seq(\"Alice\", \"Bob\", \"Charlie\", \"David\", \"Eve\")), // name\n      longVector(Seq(20L, 30L, 40L, 50L, 60L)), // id\n      stringVector(Seq(\"Campbell\", \"Roanoke\", \"Dallas\", \"Monte Sereno\", \"Minneapolis\")) // city\n    ) ++ {\n      if (includePartitionCols) {\n        Seq(\n          stringVector(Seq(\"CA\", \"TX\", \"NC\", \"CA\", \"MN\")), // state\n          stringVector(Seq(\"USA\", \"USA\", \"USA\", \"USA\", \"USA\")) // country\n        )\n      } else Seq.empty\n    }\n\n    columnarBatch(\n      schema = if (includePartitionCols) testSchemaWithPartitions else testSchema,\n      testColumnVectors)\n  }\n\n  val testSchema: StructType = new StructType()\n    .add(\"name\", StringType.STRING)\n    .add(\"id\", LongType.LONG)\n    .add(\"city\", StringType.STRING)\n\n  val testSchemaWithPartitions: StructType = new StructType(testSchema.fields())\n    .add(\"state\", StringType.STRING) // partition column\n    .add(\"country\", StringType.STRING) // partition column\n\n  val testPartitionColNames = Seq(\"state\", \"country\")\n\n  def columnarBatch(schema: StructType, vectors: Seq[ColumnVector]): ColumnarBatch = {\n    new ColumnarBatch {\n      override def getSchema: StructType = schema\n\n      override def getColumnVector(ordinal: Int): ColumnVector = vectors(ordinal)\n\n      override def withDeletedColumnAt(ordinal: Int): ColumnarBatch = {\n        // Update the schema\n        val newStructFields = new util.ArrayList(schema.fields)\n        newStructFields.remove(ordinal)\n        val newSchema: StructType = new StructType(newStructFields)\n\n        // Update the vectors\n        val newColumnVectors = vectors.toBuffer\n        newColumnVectors.remove(ordinal)\n        columnarBatch(newSchema, newColumnVectors.toSeq)\n      }\n\n      override def withNewColumn(\n          ordinal: Int,\n          columnSchema: StructField,\n          columnVector: ColumnVector): ColumnarBatch = {\n        // Update the schema\n        val newStructFields = new util.ArrayList(schema.fields)\n        newStructFields.add(ordinal, columnSchema)\n        val newSchema: StructType = new StructType(newStructFields)\n\n        // Update the vectors\n        val newColumnVectors = vectors.toBuffer\n        newColumnVectors.insert(ordinal, columnVector)\n        columnarBatch(newSchema, newColumnVectors.toSeq)\n      }\n\n      override def getSize: Int = vectors.head.getSize\n    }\n  }\n\n  def testTxnState(\n      schema: StructType,\n      partitionCols: Seq[String] = Seq.empty,\n      cmMode: String = \"none\",\n      enableIcebergCompatV2: Boolean = false,\n      enableIcebergCompatV3: Boolean = false,\n      enableMaterializePartitionColumns: Boolean = false): Row = {\n    val configurationMap = Map(\n      ICEBERG_COMPAT_V2_ENABLED.getKey -> enableIcebergCompatV2.toString,\n      ICEBERG_COMPAT_V3_ENABLED.getKey -> enableIcebergCompatV3.toString,\n      COLUMN_MAPPING_MODE.getKey -> cmMode)\n    val metadata = new Metadata(\n      \"id\",\n      Optional.empty(), /* name */\n      Optional.empty(), /* description */\n      new Format(),\n      DataTypeJsonSerDe.serializeDataType(schema),\n      schema,\n      VectorUtils.buildArrayValue(partitionCols.asJava, StringType.STRING), // partitionColumns\n      Optional.empty(), // createdTime\n      stringStringMapValue(configurationMap.asJava) // configurationMap\n    )\n\n    // Create protocol with appropriate features\n    val writerFeatures: java.util.Set[String] = if (enableMaterializePartitionColumns) {\n      Set(TableFeatures.MATERIALIZE_PARTITION_COLUMNS_W_FEATURE.featureName()).asJava\n    } else {\n      java.util.Collections.emptySet[String]()\n    }\n    val protocol = new Protocol(\n      3, // minReaderVersion\n      7, // minWriterVersion to support table features\n      java.util.Collections.emptySet[String](), // readerFeatures\n      writerFeatures)\n\n    TransactionStateRow.of(metadata, protocol, \"table path\", 200 /* maxRetries */ )\n  }\n\n  def testStats(numRowsOpt: Option[Long]): Option[DataFileStatistics] = {\n    numRowsOpt.map(numRows => {\n      new DataFileStatistics(\n        numRows,\n        Map.empty[Column, Literal].asJava, // minValues - empty value as this is just for tests.\n        Map.empty[Column, Literal].asJava, // maxValues - empty value as this is just for tests.\n        Map.empty[Column, JLong].asJava, // nullCount - empty value as this is just for tests.\n        Optional.empty() // tightBounds is unspecified\n      )\n    })\n  }\n\n  def testDataFileStatuses(fileNameStatsPairs: (String, Option[DataFileStatistics])*)\n      : CloseableIterator[DataFileStatus] = {\n\n    toCloseableIterator(\n      fileNameStatsPairs.map { case (fileName, statsOpt) =>\n        new DataFileStatus(\n          fileName,\n          23L, // size - arbitrary value as this is just for tests.\n          23L, // modificationTime - arbitrary value as this is just for tests.\n          Optional.ofNullable(statsOpt.orNull))\n      }.asJava.iterator())\n  }\n\n  /** Test [[DataWriteContext]]. As of now we don't need any custom values in this suite. */\n  def testDataWriteContext(): DataWriteContext = {\n    new DataWriteContextImpl(\"targetDir\", Map.empty[String, Literal].asJava, Seq.empty.asJava)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/commit/CatalogCommitterUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.commit\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.internal.actions.Protocol\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CatalogCommitterUtilsSuite extends AnyFunSuite {\n\n  test(\"extractProtocolProperties - legacy protocol (1, 2)\") {\n    // ===== GIVEN =====\n    val protocol = new Protocol(1, 2)\n\n    // ===== WHEN =====\n    val properties = CatalogCommitterUtils.extractProtocolProperties(protocol).asScala\n\n    // ===== THEN =====\n    assert(properties.size === 2)\n    assert(properties(\"delta.minReaderVersion\") === \"1\")\n    assert(properties(\"delta.minWriterVersion\") === \"2\")\n  }\n\n  test(\"extractProtocolProperties - protocol with overlapping reader and writer features\") {\n    // ===== GIVEN =====\n    val readerFeatures = Set(\"columnMapping\", \"deletionVectors\")\n    val writerFeatures = Set(\"columnMapping\", \"appendOnly\") // Note: columnMapping overlaps\n    val protocol = new Protocol(3, 7, readerFeatures.asJava, writerFeatures.asJava)\n\n    // ===== WHEN =====\n    val properties = CatalogCommitterUtils.extractProtocolProperties(protocol).asScala\n\n    // ===== THEN =====\n    assert(properties.size === 2 + 3) // minReader + minWriter + 3 unique features\n    assert(properties(\"delta.minReaderVersion\") === \"3\")\n    assert(properties(\"delta.minWriterVersion\") === \"7\")\n    assert(properties(\"delta.feature.columnMapping\") === \"supported\")\n    assert(properties(\"delta.feature.deletionVectors\") === \"supported\")\n    assert(properties(\"delta.feature.appendOnly\") === \"supported\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/deletionvectors/Base85CodecSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.deletionvectors\n\nimport java.nio.charset.StandardCharsets.US_ASCII\nimport java.util.UUID\n\nimport scala.util.Random\n\nimport io.delta.kernel.internal.deletionvectors.Base85Codec\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass Base85CodecSuite extends AnyFunSuite {\n\n  // Z85 reference strings are generated by https://cryptii.com/pipes/z85-encoder\n  val testUuids = Seq[(UUID, String)](\n    new UUID(0L, 0L) -> \"00000000000000000000\",\n    new UUID(Long.MinValue, Long.MinValue) -> \"Fb/MH00000Fb/MH00000\",\n    new UUID(-1L, -1L) -> \"%nSc0%nSc0%nSc0%nSc0\",\n    new UUID(0L, Long.MinValue) -> \"0000000000Fb/MH00000\",\n    new UUID(0L, -1L) -> \"0000000000%nSc0%nSc0\",\n    new UUID(0L, Long.MaxValue) -> \"0000000000Fb/MG%nSc0\",\n    new UUID(Long.MinValue, 0L) -> \"Fb/MH000000000000000\",\n    new UUID(-1L, 0L) -> \"%nSc0%nSc00000000000\",\n    new UUID(Long.MaxValue, 0L) -> \"Fb/MG%nSc00000000000\",\n    new UUID(0L, 1L) -> \"00000000000000000001\",\n    // Just a few random ones, using literals for test determinism\n    new UUID(-4124158004264678669L, -6032951921472435211L) -> \"-(5oirYA.yTvx6v@H:L>\",\n    new UUID(6453181356142382984L, 8208554093199893996L) -> \"s=Mlx-0Pp@AQ6uw@k6=D\",\n    new UUID(6453181356142382984L, -8208554093199893996L) -> \"s=Mlx-0Pp@JUL=R13LuL\",\n    new UUID(-4124158004264678669L, 8208554093199893996L) -> \"-(5oirYA.yAQ6uw@k6=D\")\n\n  // From https://rfc.zeromq.org/spec/32/ - Test Case\n  test(\"Z85 spec reference value\") {\n    val inputBytes: Array[Byte] =\n      Array(0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B).map(_.toByte)\n    val expectedEncodedString = \"HelloWorld\"\n    val actualEncodedString = Base85Codec.encodeBytes(inputBytes)\n    assert(actualEncodedString === expectedEncodedString)\n    val outputBytes = Base85Codec.decodeAlignedBytes(expectedEncodedString)\n    assert(outputBytes sameElements inputBytes)\n  }\n\n  test(\"Z85 reference implementation values\") {\n    for ((id, expectedEncodedString) <- testUuids) {\n      val actualEncodedString = Base85Codec.encodeUUID(id)\n      assert(actualEncodedString === expectedEncodedString)\n    }\n  }\n\n  test(\"Z85 spec character map\") {\n    assert(Base85Codec.ENCODE_MAP.length === 85)\n    val referenceBytes = Seq(\n      0x00, 0x09, 0x98, 0x62, 0x0F, 0xC7, 0x99, 0x43, 0x1F, 0x85,\n      0x9A, 0x24, 0x2F, 0x43, 0x9B, 0x05, 0x3F, 0x01, 0x9B, 0xE6,\n      0x4E, 0xBF, 0x9C, 0xC7, 0x5E, 0x7D, 0x9D, 0xA8, 0x6E, 0x3B,\n      0x9E, 0x89, 0x7D, 0xF9, 0x9F, 0x6A, 0x8D, 0xB7, 0xA0, 0x4B,\n      0x9D, 0x75, 0xA1, 0x2C, 0xAD, 0x33, 0xA2, 0x0D, 0xBC, 0xF1,\n      0xA2, 0xEE, 0xCC, 0xAF, 0xA3, 0xCF, 0xDC, 0x6D, 0xA4, 0xB0,\n      0xEC, 0x2B, 0xA5, 0x91, 0xFB, 0xE9, 0xA6, 0x72)\n      .map(_.toByte).toArray\n    val referenceString = new String(Base85Codec.ENCODE_MAP, US_ASCII)\n    val encodedString = Base85Codec.encodeBytes(referenceBytes)\n    assert(encodedString === referenceString)\n    val decodedBytes = Base85Codec.decodeAlignedBytes(encodedString)\n    assert(decodedBytes sameElements referenceBytes)\n  }\n\n  test(\"Reject illegal Z85 input - unaligned string\") {\n    // Minimum string should 5 characters\n    val illegalEncodedString = \"abc\"\n    assertThrows[IllegalArgumentException] {\n      Base85Codec.decodeBytes(\n        illegalEncodedString,\n        // This value is irrelevant, any value should cause the failure.\n        3\n      ) // outputLength\n    }\n  }\n\n  // scalastyle:off nonascii\n  test(s\"Reject illegal Z85 input - illegal character\") {\n    for (char <- Seq[Char]('î', 'π', '\"', 0x7F)) {\n      val illegalEncodedString = String.valueOf(Array[Char]('a', 'b', char, 'd', 'e'))\n      val ex = intercept[IllegalArgumentException] {\n        Base85Codec.decodeAlignedBytes(illegalEncodedString)\n      }\n      assert(ex.getMessage.contains(\"Input is not valid Z85\"))\n    }\n  }\n  // scalastyle:on nonascii\n\n  test(\"base85 codec uuid roundtrips\") {\n    for ((id, _) <- testUuids) {\n      val encodedString = Base85Codec.encodeUUID(id)\n      // 16 bytes always get encoded into 20 bytes with Base85.\n      assert(encodedString.length === Base85Codec.ENCODED_UUID_LENGTH)\n      val decodedId = Base85Codec.decodeUUID(encodedString)\n      assert(id === decodedId, s\"encodedString = $encodedString\")\n    }\n  }\n\n  test(\"base85 codec empty byte array\") {\n    val empty = Array.empty[Byte]\n    val encodedString = Base85Codec.encodeBytes(empty)\n    assert(encodedString === \"\")\n    val decodedArray = Base85Codec.decodeAlignedBytes(encodedString)\n    assert(decodedArray.isEmpty)\n    val decodedArray2 = Base85Codec.decodeBytes(encodedString, 0)\n    assert(decodedArray2.isEmpty)\n  }\n\n  test(\"base85 codec byte array random roundtrips\") {\n    val rand = new Random(1L) // Fixed seed for determinism\n    val arrayLengths = (1 to 20) ++ Seq(32, 56, 64, 128, 1022, 11 * 1024 * 1024)\n\n    for (len <- arrayLengths) {\n      val inputArray: Array[Byte] = Array.ofDim(len)\n      rand.nextBytes(inputArray)\n      val encodedString = Base85Codec.encodeBytes(inputArray)\n      val decodedArray = Base85Codec.decodeBytes(encodedString, len)\n      assert(decodedArray === inputArray, s\"encodedString = $encodedString\")\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/deletionvectors/RoaringBitmapArraySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.deletionvectors\n\nimport io.delta.kernel.internal.deletionvectors.RoaringBitmapArray\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass RoaringBitmapArraySuite extends AnyFunSuite {\n\n  test(\"RoaringBitmapArray create empty map\") {\n    val bitmap = RoaringBitmapArray.create()\n    assert(bitmap.toArray.isEmpty)\n  }\n\n  test(\"RoaringBitmapArray create map with values only in first bitmap\") {\n    // Values <= max unsigned int (4,294,967,295) will be in the first bitmap\n    val bitmap = RoaringBitmapArray.create(1L, 100L)\n\n    assert(bitmap.contains(1L))\n    assert(bitmap.contains(100L))\n\n    assert(!bitmap.contains(2L))\n    assert(!bitmap.contains(99L))\n\n    assert(bitmap.toArray sameElements Array(1L, 100L))\n  }\n\n  test(\"RoaringBitmapArray create map with values only in second bitmap\") {\n    // Values between max unsigned int and 2*(max unsigned int) will be in the second bitmap\n    val bitmap = RoaringBitmapArray.create(5000000000L, 5000000100L)\n\n    assert(bitmap.contains(5000000000L))\n    assert(bitmap.contains(5000000100L))\n\n    assert(!bitmap.contains(5000000001L))\n    assert(!bitmap.contains(5000000099L))\n\n    assert(bitmap.toArray sameElements Array(5000000000L, 5000000100L))\n  }\n\n  test(\"RoaringBitmapArray create map with values in first and third bitmap\") {\n    // Values between 2*(max unsigned int) and 3*(max unsigned int) will be in the third bitmap\n    val bitmap = RoaringBitmapArray.create(100L, 10000000000L)\n\n    assert(bitmap.contains(100L))\n    assert(bitmap.contains(10000000000L))\n\n    assert(!bitmap.contains(101L))\n    assert(!bitmap.contains(10000000001L))\n\n    assert(bitmap.toArray sameElements Array(100L, 10000000000L))\n  }\n\n  // TODO need to implement serialize to copy over tests\n\n  /*\n  final val BITMAP2_NUMBER = Int.MaxValue.toLong * 3L\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(s\"serialization - $serializationFormat\") {\n      checkSerializeDeserialize(RoaringBitmapArray.create(), serializationFormat)\n      checkSerializeDeserialize(RoaringBitmapArray.create(1L), serializationFormat)\n      checkSerializeDeserialize(RoaringBitmapArray.create(BITMAP2_NUMBER), serializationFormat)\n      checkSerializeDeserialize(RoaringBitmapArray.create(1L, BITMAP2_NUMBER), serializationFormat)\n      // checkSerializeDeserialize(allContainerTypesBitmap, serializationFormat)\n    }\n  }\n\n  private def checkSerializeDeserialize(\n    input: RoaringBitmapArray,\n    format: RoaringBitmapArrayFormat.Value): Unit = {\n    val serializedSize = Ints.checkedCast(input.serializedSizeInBytes(format))\n    val buffer = ByteBuffer.allocate(serializedSize).order(ByteOrder.LITTLE_ENDIAN)\n    input.serialize(buffer, format)\n    val output = RoaringBitmapArray()\n    buffer.flip()\n    output.deserialize(buffer)\n    assert(input === output)\n  }\n   */\n\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/exceptions/ExceptionSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.exceptions\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.UnsupportedProtocolVersionException.ProtocolVersionType\nimport io.delta.kernel.internal.DeltaErrors\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Unit tests for Kernel exception types.\n */\nclass ExceptionSuite extends AnyFunSuite {\n\n  test(\"UnsupportedReaderFeatureException - basic functionality\") {\n    val tablePath = \"/path/to/table\"\n    val features = Set(\"feature1\", \"feature2\").asJava\n\n    val ex = DeltaErrors.unsupportedReaderFeatures(tablePath, features)\n\n    assert(ex.getTablePath == tablePath)\n    assert(ex.getUnsupportedFeatures.asScala == Set(\"feature1\", \"feature2\"))\n    assert(ex.getMessage.contains(\"reader table features\"))\n    assert(ex.getMessage.contains(\"feature1\"))\n    assert(ex.getMessage.contains(\"feature2\"))\n    assert(ex.isInstanceOf[UnsupportedTableFeatureException])\n  }\n\n  test(\"UnsupportedWriterFeatureException - basic functionality\") {\n    val tablePath = \"/path/to/table\"\n    val features = Set(\"writerFeature\").asJava\n\n    val ex = DeltaErrors.unsupportedWriterFeatures(tablePath, features)\n\n    assert(ex.getTablePath == tablePath)\n    assert(ex.getUnsupportedFeatures.asScala == Set(\"writerFeature\"))\n    assert(ex.getMessage.contains(\"writer table features\"))\n    assert(ex.getMessage.contains(\"writerFeature\"))\n    assert(ex.isInstanceOf[UnsupportedTableFeatureException])\n  }\n\n  test(\"UnsupportedProtocolVersionException - reader version\") {\n    val tablePath = \"/path/to/table\"\n    val version = 3\n\n    val ex = DeltaErrors.unsupportedReaderProtocol(tablePath, version)\n\n    assert(ex.getTablePath == tablePath)\n    assert(ex.getVersion == version)\n    assert(ex.getVersionType == ProtocolVersionType.READER)\n    assert(ex.getMessage.contains(\"reader\"))\n    assert(ex.getMessage.contains(\"version 3\"))\n  }\n\n  test(\"UnsupportedProtocolVersionException - writer version\") {\n    val tablePath = \"/path/to/table\"\n    val version = 7\n\n    val ex = DeltaErrors.unsupportedWriterProtocol(tablePath, version)\n\n    assert(ex.getTablePath == tablePath)\n    assert(ex.getVersion == version)\n    assert(ex.getVersionType == ProtocolVersionType.WRITER)\n    assert(ex.getMessage.contains(\"writer\"))\n    assert(ex.getMessage.contains(\"version 7\"))\n  }\n\n  test(\"CommitRangeNotFoundException - with start and end version\") {\n    val tablePath = \"/path/to/table\"\n    val startVersion = 5L\n    val endVersion = Optional.of(java.lang.Long.valueOf(10L))\n\n    val ex = DeltaErrors.noCommitFilesFoundForVersionRange(tablePath, startVersion, endVersion)\n\n    assert(ex.getTablePath == tablePath)\n    assert(ex.getStartVersion == startVersion)\n    assert(ex.getEndVersion == endVersion)\n    assert(ex.getMessage.contains(\"Requested table changes between [5, Optional[10]]\"))\n    assert(ex.getMessage.contains(\"no log files found\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/expressions/ExpressionsSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions\n\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ExpressionsSuite extends AnyFunSuite {\n  test(\"expressions: unsupported literal data types\") {\n    val ex1 = intercept[IllegalArgumentException] {\n      Literal.ofNull(new ArrayType(IntegerType.INTEGER, true))\n    }\n    assert(ex1.getMessage.contains(\"array[integer] is an invalid data type for Literal.\"))\n\n    val ex2 = intercept[IllegalArgumentException] {\n      Literal.ofNull(new MapType(IntegerType.INTEGER, IntegerType.INTEGER, true))\n    }\n    assert(ex2.getMessage.contains(\"map[integer, integer] is an invalid data type for Literal.\"))\n\n    val ex3 = intercept[IllegalArgumentException] {\n      Literal.ofNull(new StructType().add(\"s1\", BooleanType.BOOLEAN))\n    }\n    assert(ex3.getMessage.matches(\"struct.* is an invalid data type for Literal.\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/expressions/PredicateSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.expressions\n\nimport java.util.Locale\n\nimport io.delta.kernel.types.CollationIdentifier\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PredicateSuite extends AnyFunSuite {\n  test(\"Check invalid collation operations\") {\n    Seq(\n      \"anD\",\n      \"oR\",\n      \"ELEMENT_AT\",\n      \"SUBstring\").foreach {\n      operationName =>\n        val e = intercept[IllegalArgumentException] {\n          new Predicate(\n            operationName,\n            new Column(\"c1\"),\n            new Column(\"c2\"),\n            CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\"))\n        }\n        assert(e.getMessage.contains(s\"Collation is not supported for operator\" +\n          s\" ${operationName.toUpperCase(Locale.ENGLISH)}.\"))\n    }\n  }\n\n  test(\"Check toString with collation\") {\n    Seq(\n      (\n        new Predicate(\n          \"<\",\n          new Column(\"c1\"),\n          new Column(\"c2\"),\n          CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\")),\n        \"(column(`c1`) < column(`c2`) COLLATE SPARK.UTF8_LCASE)\"),\n      (\n        new Predicate(\n          \">=\",\n          Literal.ofString(\"a\"),\n          new Column(\"c1\"),\n          CollationIdentifier.fromString(\"ICU.sr_Cyrl_SRB.75.1\")),\n        \"(a >= column(`c1`) COLLATE ICU.SR_CYRL_SRB.75.1)\"),\n      (\n        new Predicate(\n          \"stARtS_wiTh\",\n          new Column(\"c1\"),\n          Literal.ofString(\"a\"),\n          CollationIdentifier.fromString(\"ICU.en_US\")),\n        \"(column(`c1`) STARTS_WITH a COLLATE ICU.EN_US)\")).foreach {\n      case (predicate, expectedToString) =>\n        assert(predicate.toString == expectedToString)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/CommitRangeBuilderSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{CommitRange, TableManager}\nimport io.delta.kernel.CommitRangeBuilder.CommitBoundary\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{InvalidTableException, KernelException}\nimport io.delta.kernel.internal.commitrange.{CommitRangeBuilderImpl, CommitRangeImpl}\nimport io.delta.kernel.internal.files.{ParsedCatalogCommitData, ParsedDeltaData, ParsedLogData}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.test.{MockFileSystemClientUtils, MockListFromFileSystemClient, MockReadICTFileJsonHandler, MockSnapshotUtils, VectorTestUtils}\nimport io.delta.kernel.test.MockSnapshotUtils.getMockSnapshot\nimport io.delta.kernel.utils.FileStatus\n\nimport junit.runner.Version\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CommitRangeBuilderSuite extends AnyFunSuite with MockFileSystemClientUtils\n    with VectorTestUtils {\n\n  private def checkQueryBoundaries(\n      commitRange: CommitRange,\n      startBoundary: RequiredBoundaryDef,\n      endBoundary: BoundaryDef): Unit = {\n    def assertBoundaryVersion(boundary: CommitBoundary, version: Long) = {\n      assert(boundary.isVersion && boundary.getVersion == version)\n    }\n    def assertBoundaryTimestamp(boundary: CommitBoundary, timestamp: Long) = {\n      assert(boundary.isTimestamp && boundary.getTimestamp == timestamp)\n    }\n    if (startBoundary.version.nonEmpty) {\n      assertBoundaryVersion(commitRange.getQueryStartBoundary, startBoundary.version.get)\n    } else if (startBoundary.timestamp.nonEmpty) {\n      assertBoundaryTimestamp(commitRange.getQueryStartBoundary, startBoundary.timestamp.get)\n    } else {\n      throw new IllegalStateException(\"RequiredBoundaryDef must have either timestamp or version\")\n    }\n    if (endBoundary.version.nonEmpty) {\n      assert(commitRange.getQueryEndBoundary.isPresent)\n      assertBoundaryVersion(commitRange.getQueryEndBoundary.get, endBoundary.version.get)\n    } else if (endBoundary.timestamp.nonEmpty) {\n      assert(commitRange.getQueryEndBoundary.isPresent)\n      assertBoundaryTimestamp(commitRange.getQueryEndBoundary.get, endBoundary.timestamp.get)\n    } else {\n      assert(!commitRange.getQueryEndBoundary.isPresent)\n    }\n  }\n\n  private def buildCommitRange(\n      engine: Engine,\n      fileList: Seq[FileStatus],\n      startBoundary: RequiredBoundaryDef,\n      endBoundary: BoundaryDef,\n      logData: Option[Seq[ParsedLogData]] = None,\n      ictEnablementInfo: Option[(Long, Long)] = None): CommitRange = {\n    def getVersionFromFS(fs: FileStatus): Long = FileNames.getFileVersion(new Path(fs.getPath))\n    val latestVersion = fileList.map(getVersionFromFS).max\n    // If we have a ratified commit file at the end version, we want to use this in the log segment\n    // for our mockLatestSnapshot, so we get the ICT from that file\n    val deltaFileAtEndVersion = fileList\n      .filter(fs => FileNames.isStagedDeltaFile(fs.getPath))\n      .find(getVersionFromFS(_) == latestVersion)\n\n    lazy val mockLatestSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion,\n      ictEnablementInfoOpt = ictEnablementInfo,\n      deltaFileAtEndVersion = deltaFileAtEndVersion)\n\n    // Determine the start boundary\n    val startBound = if (startBoundary.version.isDefined) {\n      CommitBoundary.atVersion(startBoundary.version.get)\n    } else if (startBoundary.timestamp.isDefined) {\n      CommitBoundary.atTimestamp(startBoundary.timestamp.get, mockLatestSnapshot)\n    } else {\n      throw new IllegalStateException(\"RequiredBoundaryDef must have either timestamp or version\")\n    }\n\n    var commitRangeBuilder = TableManager.loadCommitRange(dataPath.toString, startBound)\n    endBoundary.version.foreach { v =>\n      commitRangeBuilder = commitRangeBuilder.withEndBoundary(CommitBoundary.atVersion(v))\n    }\n    endBoundary.timestamp.foreach { v =>\n      commitRangeBuilder = commitRangeBuilder.withEndBoundary(\n        CommitBoundary.atTimestamp(v, mockLatestSnapshot))\n    }\n    logData.foreach { l =>\n      commitRangeBuilder = commitRangeBuilder.withLogData(l.asJava)\n    }\n    commitRangeBuilder.build(engine)\n  }\n\n  private def checkCommitRange(\n      fileList: Seq[FileStatus],\n      expectedStartVersion: Long,\n      expectedEndVersion: Long,\n      startBoundary: RequiredBoundaryDef,\n      endBoundary: BoundaryDef): Unit = {\n    val commitRange = buildCommitRange(\n      createMockFSListFromEngine(fileList),\n      fileList,\n      startBoundary,\n      endBoundary)\n    assert(commitRange.getStartVersion == expectedStartVersion)\n    assert(commitRange.getEndVersion == expectedEndVersion)\n    checkQueryBoundaries(commitRange, startBoundary, endBoundary)\n    val expectedFileList = fileList\n      .filter(fs => {\n        val version = FileNames.getFileVersion(new Path(fs.getPath))\n        version >= expectedStartVersion && version <= expectedEndVersion\n      }).filter(fs => FileNames.isCommitFile(fs.getPath))\n    assert(expectedFileList.toSet ==\n      commitRange.asInstanceOf[CommitRangeImpl].getDeltaFiles.asScala.toSet)\n  }\n\n  /**\n   * Base class for boundary definitions used in testing.\n   * @param expectedVersion the expected version this def will be resolved to\n   * @param expectError whether we expect this def to inherently fail for the corresponding listing\n   */\n  private abstract class BoundaryDef(\n      val expectedVersion: Long,\n      val expectError: Boolean = false) {\n\n    def version: Option[Long] = None\n    def timestamp: Option[Long] = None\n  }\n\n  /**\n   * Base class for boundary definitions that are NOT the default (i.e. are provided).\n   *\n   * At least one of `version` or `timestamp` must be defined in this case.\n   */\n  private abstract class RequiredBoundaryDef(\n      expectedVersion: Long,\n      expectError: Boolean = false) extends BoundaryDef(expectedVersion, expectError) {\n    assert(version.isDefined || timestamp.isDefined)\n  }\n\n  /**\n   * Version-based boundary definition.\n   * @param versionValue the version to use as boundary\n   * @param expectsError whether we expect this def to inherently fail\n   */\n  private case class VersionBoundaryDef(\n      versionValue: Long,\n      expectsError: Boolean = false) extends RequiredBoundaryDef(versionValue, expectsError) {\n\n    override def version: Option[Long] = Some(versionValue)\n  }\n\n  /**\n   * Timestamp-based boundary definition.\n   * @param timestampValue the timestamp to use as boundary\n   * @param resolvedVersion the expected version this timestamp will resolve to\n   * @param expectsError whether we expect this def to inherently fail\n   */\n  private case class TimestampBoundaryDef(\n      timestampValue: Long,\n      resolvedVersion: Long,\n      expectsError: Boolean = false) extends RequiredBoundaryDef(resolvedVersion, expectsError) {\n\n    override def timestamp: Option[Long] = Some(timestampValue)\n  }\n\n  /**\n   * Default boundary definition (no specific version or timestamp).\n   * @param resolvedVersion the expected version this will resolve to\n   * @param expectsError whether we expect this def to inherently fail\n   */\n  private case class DefaultBoundaryDef(\n      resolvedVersion: Long,\n      expectsError: Boolean = false) extends BoundaryDef(resolvedVersion, expectsError)\n\n  def getExpectedException(\n      startBoundary: RequiredBoundaryDef,\n      endBoundary: BoundaryDef,\n      fileStatuses: Seq[FileStatus]): Option[(Class[_ <: Throwable], String)] = {\n    // These two cases fail on CommitRangeBuilderImpl.validateInputOnBuild\n    if (startBoundary.version.isDefined && endBoundary.version.isDefined) {\n      if (startBoundary.version.get > endBoundary.version.get) {\n        return Some(classOf[IllegalArgumentException], \"startVersion must be <= endVersion\")\n      }\n    }\n    if (startBoundary.timestamp.isDefined && endBoundary.timestamp.isDefined) {\n      if (startBoundary.timestamp.get > endBoundary.timestamp.get) {\n        return Some(classOf[IllegalArgumentException], \"startTimestamp must be <= endTimestamp\")\n      }\n    }\n    if (endBoundary.version.isDefined) {\n      val stagedCommits = fileStatuses\n        .filter(fs => FileNames.isStagedDeltaFile(fs.getPath))\n      if (stagedCommits.nonEmpty) {\n        val tailStagedCommit = stagedCommits(stagedCommits.length - 1)\n        if (endBoundary.version.get > FileNames.deltaVersion(tailStagedCommit.getPath)) {\n          return Some(\n            classOf[IllegalArgumentException],\n            \"When endVersion is specified with logData, the last logData version\")\n        }\n      }\n    }\n\n    // We try to resolve any timestamps, first startVersion then endVersion (CommitRangeFactory)\n    if (startBoundary.expectError && startBoundary.timestamp.isDefined) {\n      return Some(classOf[KernelException], \"is after the latest available version\")\n    }\n    if (endBoundary.expectError && endBoundary.timestamp.isDefined) {\n      return Some(classOf[KernelException], \"is before the earliest available version\")\n    }\n    // Now we hit an exception if resolved startVersion > resolvedEndVersion (CommitRangeFactory)\n    // (endVersion is only resolved before listing if either TS or Version was provided)\n    if (\n      startBoundary.expectedVersion > endBoundary.expectedVersion &&\n      (endBoundary.timestamp.isDefined || endBoundary.version.isDefined)\n    ) {\n      return Some(\n        classOf[KernelException],\n        s\"startVersion=${startBoundary.expectedVersion} > \" +\n          s\"endVersion=${endBoundary.expectedVersion}\")\n    }\n    // Now we query the file list, this is where we fail if the provided versions do not exist\n    if (startBoundary.expectError) {\n      // These either hit DeltaErrors.noCommitFilesFoundForVersionRange or\n      // DeltaErrors.startVersionNotFound\n      return Some(classOf[KernelException], s\"no log file\")\n    }\n    if (endBoundary.expectError) {\n      // These either hit DeltaErrors.noCommitFilesFoundForVersionRange or\n      // DeltaErrors.endVersionNotFound\n      return Some(classOf[KernelException], s\"no log file\")\n    }\n    None\n  }\n\n  def testStartAndEndBoundaryCombinations(\n      description: String,\n      fileStatuses: Seq[FileStatus],\n      startBoundaries: Seq[RequiredBoundaryDef],\n      endBoundaries: Seq[BoundaryDef]): Unit = {\n    startBoundaries.foreach { startBound =>\n      endBoundaries.foreach { endBound =>\n        test(s\"$description: build CommitRange with startBound=$startBound endBound=$endBound\") {\n          val expectedException = getExpectedException(startBound, endBound, fileStatuses)\n          if (expectedException.isDefined) {\n            val e = intercept[Throwable] {\n              buildCommitRange(\n                createMockFSListFromEngine(fileStatuses),\n                fileList = fileStatuses,\n                startBoundary = startBound,\n                endBoundary = endBound)\n            }\n            assert(\n              expectedException.get._1.isInstance(e),\n              s\"Expected exception of ${expectedException.get._1} but found $e\")\n            assert(e.getMessage.contains(expectedException.get._2))\n          } else {\n            checkCommitRange(\n              fileList = fileStatuses,\n              expectedStartVersion = startBound.expectedVersion,\n              expectedEndVersion = endBound.expectedVersion,\n              startBoundary = startBound,\n              endBoundary = endBound)\n          }\n        }\n      }\n    }\n  }\n\n  /* --------------- Without catalog commits --------------- */\n\n  // Test with negative timestamps - manually create FileStatus with negative timestamps\n  testStartAndEndBoundaryCombinations(\n    description = \"deltaFiles=(0, 1) with negative timestamps\", // v0 -> -100, v1 -> -50\n    fileStatuses = Seq(\n      FileStatus.of(FileNames.deltaFile(logPath, 0L), 0L, -100L),\n      FileStatus.of(FileNames.deltaFile(logPath, 1L), 1L, -50L)),\n    startBoundaries = Seq(\n      VersionBoundaryDef(0L),\n      VersionBoundaryDef(1L),\n      TimestampBoundaryDef(-150, resolvedVersion = 0L), // before v0\n      TimestampBoundaryDef(-100, resolvedVersion = 0L), // at v0\n      TimestampBoundaryDef(-75, resolvedVersion = 1), // between v0, v1\n      TimestampBoundaryDef(-50, resolvedVersion = 1), // at v1\n      TimestampBoundaryDef(-40, resolvedVersion = -1, expectsError = true), // after v1\n      VersionBoundaryDef(2L, expectsError = true) // version DNE\n    ),\n    endBoundaries = Seq(\n      VersionBoundaryDef(0L),\n      VersionBoundaryDef(1L),\n      TimestampBoundaryDef(-150, resolvedVersion = -1, expectsError = true), // before v0\n      TimestampBoundaryDef(-100, resolvedVersion = 0L), // at v0\n      TimestampBoundaryDef(-75, resolvedVersion = 0), // between v0, v1\n      TimestampBoundaryDef(-50, resolvedVersion = 1), // at v1\n      TimestampBoundaryDef(-40, resolvedVersion = 1), // after v1\n      DefaultBoundaryDef(resolvedVersion = 1), // default to latest\n      VersionBoundaryDef(2L, expectsError = true) // version DNE\n    ))\n\n  // The below test cases mimic the cases in TableImplSuite for the timestamp-resolution\n  testStartAndEndBoundaryCombinations(\n    description = \"deltaFiles=(0, 1)\", // (version -> timestamp) = v0 -> 0, v1 -> 10\n    fileStatuses = deltaFileStatuses(Seq(0L, 1L)),\n    startBoundaries = Seq(\n      VersionBoundaryDef(0L),\n      VersionBoundaryDef(1L),\n      TimestampBoundaryDef(-5, resolvedVersion = 0L), // before v0, negative timestamp\n      TimestampBoundaryDef(0, resolvedVersion = 0L), // at v0\n      TimestampBoundaryDef(5, resolvedVersion = 1), // between v0, v1\n      TimestampBoundaryDef(10, resolvedVersion = 1), // at v1\n      TimestampBoundaryDef(11, resolvedVersion = -1, expectsError = true), // after v1\n      VersionBoundaryDef(2L, expectsError = true) // version DNE\n    ),\n    endBoundaries = Seq(\n      VersionBoundaryDef(0L),\n      VersionBoundaryDef(1L),\n      TimestampBoundaryDef(-5, resolvedVersion = -1, expectsError = true), // before v0, negative\n      TimestampBoundaryDef(0, resolvedVersion = 0L), // at v0\n      TimestampBoundaryDef(5, resolvedVersion = 0), // between v0, v1\n      TimestampBoundaryDef(10, resolvedVersion = 1), // at v1\n      TimestampBoundaryDef(11, resolvedVersion = 1), // after v1\n      DefaultBoundaryDef(resolvedVersion = 1), // default to latest\n      VersionBoundaryDef(2L, expectsError = true) // version DNE\n    ))\n\n  testStartAndEndBoundaryCombinations(\n    // (version -> timestamp) = v10 -> 100, v11 -> 110, v12 -> 120\n    description = \"deltaFiles=(10, 11, 12)\",\n    fileStatuses = deltaFileStatuses(Seq(10L, 11L, 12L)) ++\n      singularCheckpointFileStatuses(Seq(10L)),\n    startBoundaries = Seq(\n      VersionBoundaryDef(10L),\n      VersionBoundaryDef(11L),\n      VersionBoundaryDef(12L),\n      TimestampBoundaryDef(99, resolvedVersion = 10), // before v10\n      TimestampBoundaryDef(100, resolvedVersion = 10L), // at v10\n      TimestampBoundaryDef(105, resolvedVersion = 11L), // between v10, v11\n      TimestampBoundaryDef(110, resolvedVersion = 11L), // at v11\n      TimestampBoundaryDef(115, resolvedVersion = 12L), // between v11, v12\n      TimestampBoundaryDef(120, resolvedVersion = 12L), // at v12\n      TimestampBoundaryDef(125, resolvedVersion = -1, expectsError = true), // after v12\n      VersionBoundaryDef(9L, expectsError = true), // version DNE\n      VersionBoundaryDef(13L, expectsError = true) // version DNE\n    ),\n    endBoundaries = Seq(\n      VersionBoundaryDef(10L),\n      VersionBoundaryDef(11L),\n      VersionBoundaryDef(12L),\n      TimestampBoundaryDef(100, resolvedVersion = 10), // at v10\n      TimestampBoundaryDef(105, resolvedVersion = 10), // between v10, v11\n      TimestampBoundaryDef(110, resolvedVersion = 11), // at v11\n      TimestampBoundaryDef(115, resolvedVersion = 11), // between v11, v12\n      TimestampBoundaryDef(120, resolvedVersion = 12), // at v12\n      TimestampBoundaryDef(125, resolvedVersion = 12), // after v12\n      DefaultBoundaryDef(resolvedVersion = 12), // default to latest\n      TimestampBoundaryDef(99, resolvedVersion = -1, expectsError = true), // before V10\n      VersionBoundaryDef(9L, expectsError = true), // version DNE\n      VersionBoundaryDef(13L, expectsError = true) // version DNE\n    ))\n\n  // Check case with only 1 delta file\n  testStartAndEndBoundaryCombinations(\n    description = \"deltaFiles=(10)\", // (version -> timestamp) = v10 -> 100\n    fileStatuses = deltaFileStatuses(Seq(10L)) ++ singularCheckpointFileStatuses(Seq(10L)),\n    startBoundaries = Seq(\n      VersionBoundaryDef(10L),\n      TimestampBoundaryDef(99L, resolvedVersion = 10L), // before v10\n      TimestampBoundaryDef(100L, resolvedVersion = 10L), // at v10\n      TimestampBoundaryDef(101L, resolvedVersion = -1, expectsError = true), // after v10\n      VersionBoundaryDef(1L, expectsError = true) // version DNE\n    ),\n    endBoundaries = Seq(\n      VersionBoundaryDef(10L),\n      TimestampBoundaryDef(100L, resolvedVersion = 10L), // at v10\n      TimestampBoundaryDef(101L, resolvedVersion = 10L), // after v10\n      DefaultBoundaryDef(resolvedVersion = 10L), // default to latest\n      TimestampBoundaryDef(99L, resolvedVersion = -1, expectsError = true), // before v10\n      VersionBoundaryDef(1L, expectsError = true) // version DNE\n    ))\n\n  /* --------------- With catalog commits --------------- */\n\n  private def checkCommitRangeWithCatalogCommits(\n      fileList: Seq[FileStatus],\n      logData: Seq[ParsedLogData],\n      versionToICT: Map[Long, Long],\n      startBound: RequiredBoundaryDef,\n      endBound: BoundaryDef,\n      expectedFileList: Seq[FileStatus]): Unit = {\n    // Create mock engine with ICT reading support\n    val commitRange = buildCommitRange(\n      createMockFSAndJsonEngineForICT(fileList, versionToICT),\n      fileList,\n      startBoundary = startBound,\n      endBoundary = endBound,\n      Some(logData),\n      ictEnablementInfo = Some((0, 0)))\n    assert(commitRange.getStartVersion == startBound.expectedVersion)\n    assert(commitRange.getEndVersion == endBound.expectedVersion)\n    checkQueryBoundaries(\n      commitRange,\n      startBound,\n      endBound)\n    assert(expectedFileList.toSet ==\n      commitRange.asInstanceOf[CommitRangeImpl].getDeltaFiles.asScala.toSet)\n  }\n\n  /**\n   * @param expectedFileList takes in a tuple (startVersion, endVersion) of a range and returns the\n   *                         expected file list for that version range\n   */\n  private def testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      description: String,\n      fileStatuses: Seq[FileStatus],\n      expectedFileList: (Long, Long) => Seq[FileStatus],\n      logData: Seq[ParsedLogData],\n      versionToICT: Map[Long, Long],\n      startBoundaries: Seq[RequiredBoundaryDef],\n      endBoundaries: Seq[BoundaryDef]): Unit = {\n    startBoundaries.foreach { startBound =>\n      endBoundaries.foreach { endBound =>\n        test(s\"$description: build CommitRange with startBound=$startBound endBound=$endBound\") {\n          val expectedException = getExpectedException(startBound, endBound, fileStatuses)\n          if (expectedException.isDefined) {\n            val e = intercept[Throwable] {\n              buildCommitRange(\n                createMockFSAndJsonEngineForICT(fileStatuses, versionToICT),\n                fileStatuses,\n                startBoundary = startBound,\n                endBoundary = endBound,\n                Some(logData),\n                ictEnablementInfo = Some((0, 0)))\n            }\n            assert(\n              expectedException.get._1.isInstance(e),\n              s\"Expected exception of ${expectedException.get._1} but found $e\")\n            assert(e.getMessage.contains(expectedException.get._2))\n          } else {\n            checkCommitRangeWithCatalogCommits(\n              fileStatuses,\n              logData,\n              versionToICT,\n              startBound,\n              endBound,\n              expectedFileList(startBound.expectedVersion, endBound.expectedVersion))\n          }\n        }\n      }\n    }\n  }\n\n  // Basic case with no overlap: P0, P1, C2, C3, C4\n  {\n    val catalogCommitFiles = Seq(stagedCommitFile(2), stagedCommitFile(3), stagedCommitFile(4))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val fileList = Seq(deltaFileStatus(0), deltaFileStatus(1)) ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L, 2L -> 2050L, 3L -> 3050L, 4L -> 4050L)\n\n    val startBoundaries = Seq(\n      // V0 (first published commit)\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(5L, 0), // before V0\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      // V1 (last published commit)\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1000L, 1), // between V0 and V1\n      // V2 (first catalog commit)\n      VersionBoundaryDef(2),\n      TimestampBoundaryDef(1500, 2), // between V1 and V2\n      TimestampBoundaryDef(2050, 2), // exactly at V2\n      // V3 (middle catalog commit)\n      VersionBoundaryDef(3L),\n      // V4 (last catalog commit)\n      VersionBoundaryDef(4L),\n      TimestampBoundaryDef(3500L, 4L), // between V3 and V4\n      TimestampBoundaryDef(4050L, 4), // exactly at V4\n      // Some error cases\n      TimestampBoundaryDef(4500, 4L, expectsError = true), // after V4\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    val endBoundaries = Seq(\n      // V0 (first published commit)\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      TimestampBoundaryDef(500L, 0L), // between V0 and V1\n      // V1 (last published commit)\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1500L, 1), // between V1 and V2\n      // V2 (first catalog commit)\n      VersionBoundaryDef(2),\n      TimestampBoundaryDef(2500, 2), // between V2 and V3\n      TimestampBoundaryDef(2050, 2), // exactly at V2\n      // V3 (middle catalog commit)\n      VersionBoundaryDef(3L),\n      TimestampBoundaryDef(3500L, 3L), // between V3 and V4\n      // V4 (last catalog commit)\n      VersionBoundaryDef(4L),\n      TimestampBoundaryDef(4050L, 4), // exactly at V4\n      DefaultBoundaryDef(4L),\n      TimestampBoundaryDef(4500, 4L), // after V4\n      // Some error cases\n      TimestampBoundaryDef(5L, 0, expectsError = true), // before V0\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      \"catalog commits basic case no overlap\",\n      fileList,\n      (startV, endV) => fileList.slice(startV.toInt, endV.toInt + 1),\n      parsedLogData,\n      versionToICT,\n      startBoundaries,\n      endBoundaries)\n  }\n\n  // Basic case with overlap: P0, P1, P2, R1, R2, R3 (+ prioritize catalog commits)\n  {\n    val catalogCommitFiles = Seq(stagedCommitFile(1), stagedCommitFile(2), stagedCommitFile(3))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val publishedDeltaFiles = Seq(deltaFileStatus(0), deltaFileStatus(1), deltaFileStatus(2))\n    val fileList = publishedDeltaFiles ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L, 2L -> 2050L, 3L -> 3050L)\n    // We expect catalog commits to take precedence over published deltas\n    val expectedFileList = publishedDeltaFiles.slice(0, 1) ++ catalogCommitFiles\n\n    val startBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(5L, 0), // before V0\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // at V1\n      TimestampBoundaryDef(1000L, 1), // between V0 and V1\n      // V2\n      VersionBoundaryDef(2),\n      TimestampBoundaryDef(1500, 2), // between V1 and V2\n      TimestampBoundaryDef(2050, 2), // exactly at V2\n      // V3\n      VersionBoundaryDef(3L),\n      TimestampBoundaryDef(2500, 3), // between V2 and V3\n      TimestampBoundaryDef(3050, 3), // exactly at V3\n      // Some error cases\n      TimestampBoundaryDef(4500, 3L, expectsError = true), // after V4\n      VersionBoundaryDef(4, expectsError = true) // version DNE\n    )\n    val endBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      TimestampBoundaryDef(500L, 0L), // between V0 and V1\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1500L, 1), // between V1 and V2\n      // V2\n      VersionBoundaryDef(2),\n      TimestampBoundaryDef(2500, 2), // between V2 and V3\n      TimestampBoundaryDef(2050, 2), // exactly at V2\n      // V3\n      VersionBoundaryDef(3L),\n      TimestampBoundaryDef(3050L, 3), // exactly at V3\n      DefaultBoundaryDef(3L),\n      TimestampBoundaryDef(3500, 3L), // after V3\n      // Some error cases\n      TimestampBoundaryDef(5L, 0, expectsError = true), // before V0\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      \"catalog commits basic case with overlap\",\n      fileList,\n      (startV, endV) => expectedFileList.slice(startV.toInt, endV.toInt + 1),\n      parsedLogData,\n      versionToICT,\n      startBoundaries,\n      endBoundaries)\n  }\n\n  // Only catalog commits: C0, C1\n  {\n    val catalogCommitFiles = Seq(stagedCommitFile(0), stagedCommitFile(1))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L)\n\n    val startBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(5L, 0), // before V0\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // at V1\n      TimestampBoundaryDef(1000L, 1), // between V0 and V1\n      // Some error cases\n      TimestampBoundaryDef(4500, 1L, expectsError = true), // after V1\n      VersionBoundaryDef(4, expectsError = true) // version DNE\n    )\n    val endBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      TimestampBoundaryDef(500L, 0L), // between V0 and V1\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // exactly at V1\n      TimestampBoundaryDef(1500L, 1), // after V1\n      DefaultBoundaryDef(1),\n      // Some error cases\n      TimestampBoundaryDef(5L, 0, expectsError = true), // before V0\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      \"catalog commits no published deltas\",\n      catalogCommitFiles,\n      (startV, endV) => catalogCommitFiles.slice(startV.toInt, endV.toInt + 1),\n      parsedLogData,\n      versionToICT,\n      startBoundaries,\n      endBoundaries)\n  }\n\n  // Single published commit + single catalog commit: P0, C1\n  {\n    val catalogCommitFiles = Seq(stagedCommitFile(1))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val publishedDeltaFiles = Seq(deltaFileStatus(0))\n    val fileList = publishedDeltaFiles ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L)\n\n    val startBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(5L, 0), // before V0\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // at V1\n      TimestampBoundaryDef(1000L, 1), // between V0 and V1\n      // Some error cases\n      TimestampBoundaryDef(4500, 1L, expectsError = true), // after V1\n      VersionBoundaryDef(4, expectsError = true) // version DNE\n    )\n    val endBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      TimestampBoundaryDef(500L, 0L), // between V0 and V1\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // exactly at V1\n      TimestampBoundaryDef(1500L, 1), // after V1\n      DefaultBoundaryDef(1),\n      // Some error cases\n      TimestampBoundaryDef(5L, 0, expectsError = true), // before V0\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      \"catalog commits single catalog commit single published commit\",\n      fileList,\n      (startV, endV) => fileList.slice(startV.toInt, endV.toInt + 1),\n      parsedLogData,\n      versionToICT,\n      startBoundaries,\n      endBoundaries)\n  }\n\n  // Overlap by just 1: P0, P1, R1, R2\n  {\n    val catalogCommitFiles = Seq(stagedCommitFile(1), stagedCommitFile(2))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val publishedDeltaFiles = Seq(deltaFileStatus(0), deltaFileStatus(1))\n    val fileList = publishedDeltaFiles ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L, 2L -> 2050L)\n    // We expect catalog commits to take precedence over published deltas\n    val expectedFileList = publishedDeltaFiles.slice(0, 1) ++ catalogCommitFiles\n\n    val startBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(5L, 0), // before V0\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // at V1\n      TimestampBoundaryDef(1000L, 1), // between V0 and V1\n      // V2\n      VersionBoundaryDef(2L),\n      TimestampBoundaryDef(1500, 2), // between V1 and V2\n      TimestampBoundaryDef(2050, 2), // exactly at V2\n      // Some error cases\n      TimestampBoundaryDef(4500, 3L, expectsError = true), // after V2\n      VersionBoundaryDef(4, expectsError = true) // version DNE\n    )\n    val endBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      TimestampBoundaryDef(500L, 0L), // between V0 and V1\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1500L, 1), // between V1 and V2\n      // V2\n      VersionBoundaryDef(2L),\n      TimestampBoundaryDef(2050L, 2), // exactly at V2\n      DefaultBoundaryDef(2L),\n      TimestampBoundaryDef(2500, 2), // after V2\n      // Some error cases\n      TimestampBoundaryDef(5L, 0, expectsError = true), // before V0\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      \"catalog commits single commit overlap\",\n      fileList,\n      (startV, endV) => expectedFileList.slice(startV.toInt, endV.toInt + 1),\n      parsedLogData,\n      versionToICT,\n      startBoundaries,\n      endBoundaries)\n  }\n\n  // Full overlap: P0, P1, P2 + C0, C1, C2\n  {\n    val catalogCommitFiles = Seq(stagedCommitFile(0), stagedCommitFile(1), stagedCommitFile(2))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val publishedDeltaFiles = Seq(deltaFileStatus(0), deltaFileStatus(1), deltaFileStatus(2))\n    val fileList = publishedDeltaFiles ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L, 2L -> 2050L)\n\n    val startBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(5L, 0), // before V0\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1050L, 1), // at V1\n      TimestampBoundaryDef(1000L, 1), // between V0 and V1\n      // V2\n      VersionBoundaryDef(2L),\n      TimestampBoundaryDef(1500, 2), // between V1 and V2\n      TimestampBoundaryDef(2050, 2), // exactly at V2\n      // Some error cases\n      TimestampBoundaryDef(4500, 3L, expectsError = true), // after V2\n      VersionBoundaryDef(4, expectsError = true) // version DNE\n    )\n    val endBoundaries = Seq(\n      // V0\n      VersionBoundaryDef(0),\n      TimestampBoundaryDef(50L, 0L), // exactly at V0\n      TimestampBoundaryDef(500L, 0L), // between V0 and V1\n      // V1\n      VersionBoundaryDef(1),\n      TimestampBoundaryDef(1500L, 1), // between V1 and V2\n      // V2\n      VersionBoundaryDef(2L),\n      TimestampBoundaryDef(2050L, 2), // exactly at V2\n      DefaultBoundaryDef(2L),\n      TimestampBoundaryDef(2500, 2), // after V2\n      // Some error cases\n      TimestampBoundaryDef(5L, 0, expectsError = true), // before V0\n      VersionBoundaryDef(5, expectsError = true) // version DNE\n    )\n    testStartAndEndBoundaryCombinationsWithCatalogCommits(\n      \"catalog commits full overlap\",\n      fileList,\n      (startV, endV) => catalogCommitFiles.slice(startV.toInt, endV.toInt + 1),\n      parsedLogData,\n      versionToICT,\n      startBoundaries,\n      endBoundaries)\n  }\n\n  test(\"build CommitRange fails if catalog commits pre-curse published commits\") {\n    val catalogCommitFiles = Seq(stagedCommitFile(0), stagedCommitFile(1), stagedCommitFile(2))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val publishedDeltaFiles = Seq(deltaFileStatus(1), deltaFileStatus(2), deltaFileStatus(3))\n    val fileList = publishedDeltaFiles ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L, 2L -> 2050L)\n\n    val e = intercept[InvalidTableException] {\n      buildCommitRange(\n        createMockFSAndJsonEngineForICT(fileList, versionToICT),\n        fileList,\n        startBoundary = VersionBoundaryDef(0),\n        endBoundary = VersionBoundaryDef(2),\n        logData = Some(parsedLogData),\n        ictEnablementInfo = Some((0, 0)))\n    }\n    assert(e.getMessage.contains(\n      \"Missing delta file: found staged ratified commit for version 0 but no published \" +\n        \"delta file. Found published deltas for later versions: [1, 2]\"))\n  }\n\n  test(\"build CommitRange fails if published commits and catalog commits are not contiguous\") {\n    val catalogCommitFiles = Seq(stagedCommitFile(2))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val publishedDeltaFiles = Seq(deltaFileStatus(0))\n    val fileList = publishedDeltaFiles ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 50L, 1L -> 1050L, 2L -> 2050L)\n\n    val e = intercept[InvalidTableException] {\n      buildCommitRange(\n        createMockFSAndJsonEngineForICT(fileList, versionToICT),\n        fileList,\n        startBoundary = VersionBoundaryDef(0),\n        endBoundary = VersionBoundaryDef(2),\n        logData = Some(parsedLogData),\n        ictEnablementInfo = Some((0, 0)))\n    }\n    assert(e.getMessage.contains(\n      \"Missing delta files: found published delta files for versions [0] and staged \" +\n        \"ratified commits for versions [2]\"))\n  }\n\n  test(\"build CommitRange fails if published deltas are not contiguous\") {\n    val publishedDeltaFiles = Seq(deltaFileStatus(0), deltaFileStatus(2), deltaFileStatus(3))\n    val e = intercept[InvalidTableException] {\n      buildCommitRange(\n        createMockFSListFromEngine(publishedDeltaFiles),\n        publishedDeltaFiles,\n        startBoundary = VersionBoundaryDef(0),\n        endBoundary = VersionBoundaryDef(3))\n    }\n    assert(e.getMessage.contains(\n      \"Missing delta files: versions are not contiguous: ([0, 2, 3])\"))\n  }\n\n  Seq(\n    ParsedCatalogCommitData.forInlineData(1, emptyColumnarBatch),\n    ParsedLogData.forFileStatus(logCompactionStatus(0, 1))).foreach { parsedLogData =>\n    val suffix = s\"- type=${parsedLogData.getGroupByCategoryClass.toString}\"\n    test(s\"withLogData: non-staged-ratified-commit throws IllegalArgumentException $suffix\") {\n      val builder = TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withLogData(Collections.singletonList(parsedLogData))\n\n      val exMsg = intercept[IllegalArgumentException] {\n        builder.build(mockEngine())\n      }.getMessage\n\n      assert(exMsg.contains(\"Only staged ratified commits are supported\"))\n    }\n  }\n\n  test(\"withLogData: non-contiguous input throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withLogData(parsedRatifiedStagedCommits(Seq(0, 2)).toList.asJava)\n        .build(mockEngine())\n    }.getMessage\n\n    assert(exMsg.contains(\"Log data must be sorted and contiguous\"))\n  }\n\n  test(\"withLogData: non-sorted input throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withLogData(parsedRatifiedStagedCommits(Seq(2, 1, 0)).toList.asJava)\n        .build(mockEngine())\n    }.getMessage\n\n    assert(exMsg.contains(\"Log data must be sorted and contiguous\"))\n  }\n\n  //////////////////////////////////////////////\n  // withMaxCatalogVersion Tests\n  //////////////////////////////////////////////\n\n  test(\"withMaxCatalogVersion: negative version throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withMaxCatalogVersion(-1)\n    }.getMessage\n\n    assert(exMsg.contains(\"maxCatalogVersion must be >= 0\"))\n  }\n\n  test(\"withMaxCatalogVersion: zero is valid\") {\n    val fileList = deltaFileStatuses(Seq(0, 1, 2))\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(0)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 0)\n  }\n\n  test(\"withMaxCatalogVersion: positive version is valid\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(5)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 5)\n  }\n\n  test(\"withMaxCatalogVersion: start version must be <= maxCatalogVersion\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(6))\n        .withMaxCatalogVersion(5)\n        .build(createMockFSListFromEngine(fileList))\n    }.getMessage\n\n    assert(exMsg.contains(\"startVersion (6) must be <= maxCatalogVersion (5)\"))\n  }\n\n  test(\"withMaxCatalogVersion: start version equal to maxCatalogVersion is valid\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(5))\n      .withMaxCatalogVersion(5)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 5)\n    assert(commitRange.getEndVersion == 5)\n  }\n\n  test(\"withMaxCatalogVersion: end version must be <= maxCatalogVersion\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withMaxCatalogVersion(5)\n        .withEndBoundary(CommitBoundary.atVersion(6))\n        .build(createMockFSListFromEngine(fileList))\n    }.getMessage\n\n    assert(exMsg.contains(\"endVersion (6) must be <= maxCatalogVersion (5)\"))\n  }\n\n  test(\"withMaxCatalogVersion: end version equal to maxCatalogVersion is valid\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(5)\n      .withEndBoundary(CommitBoundary.atVersion(5))\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 5)\n  }\n\n  test(\n    \"withMaxCatalogVersion: start timestamp boundary requires snapshot version = \" +\n      \"maxCatalogVersion\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val mockLatestSnapshot = getMockSnapshot(dataPath, 10)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(\n          dataPath.toString,\n          CommitBoundary.atTimestamp(50, mockLatestSnapshot))\n        .withMaxCatalogVersion(5)\n        .build(createMockFSListFromEngine(fileList))\n    }.getMessage\n\n    assert(exMsg.contains(\"the provided snapshot version (10) must equal maxCatalogVersion (5)\"))\n  }\n\n  test(\"withMaxCatalogVersion: start timestamp boundary with matching snapshot version is valid\") {\n    val fileList = deltaFileStatuses(0L to 5L)\n    val mockLatestSnapshot = getMockSnapshot(dataPath, 5)\n\n    val builder = TableManager\n      .loadCommitRange(\n        dataPath.toString,\n        CommitBoundary.atTimestamp(30, mockLatestSnapshot))\n      .withMaxCatalogVersion(5)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 3) // timestamp 30 maps to version 3\n    assert(commitRange.getEndVersion == 5)\n  }\n\n  test(\n    \"withMaxCatalogVersion: end timestamp boundary requires snapshot version = maxCatalogVersion\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val mockLatestSnapshot = getMockSnapshot(dataPath, 10)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withMaxCatalogVersion(5)\n        .withEndBoundary(CommitBoundary.atTimestamp(50, mockLatestSnapshot))\n        .build(createMockFSListFromEngine(fileList))\n    }.getMessage\n\n    assert(exMsg.contains(\"the provided snapshot version (10) must equal maxCatalogVersion (5)\"))\n  }\n\n  test(\"withMaxCatalogVersion: end timestamp boundary with matching snapshot version is valid\") {\n    val fileList = deltaFileStatuses(0L to 5L)\n    val mockLatestSnapshot = getMockSnapshot(dataPath, 5)\n\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(5)\n      .withEndBoundary(CommitBoundary.atTimestamp(30, mockLatestSnapshot))\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 3) // timestamp 30 maps to version 3\n  }\n\n  test(\"withMaxCatalogVersion: without end boundary, logData must end with maxCatalogVersion\") {\n    val logData = parsedRatifiedStagedCommits(Seq(0, 1, 2, 3, 4))\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withMaxCatalogVersion(5)\n        .withLogData(logData.toList.asJava)\n        .build(mockEngine())\n    }.getMessage\n\n    assert(exMsg.contains(\"the last logData version (4) must equal maxCatalogVersion (5)\"))\n  }\n\n  test(\n    \"withMaxCatalogVersion: without end boundary, logData ending with maxCatalogVersion is valid\") {\n    val fileList = deltaFileStatuses(0L to 5L)\n    val logData = parsedRatifiedStagedCommits(Seq(0, 1, 2, 3, 4, 5))\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(5)\n      .withLogData(logData.toList.asJava)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 5)\n  }\n\n  test(\"withMaxCatalogVersion: empty logData with maxCatalogVersion is valid\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(5)\n      .withLogData(Collections.emptyList())\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 5)\n  }\n\n  test(\"withMaxCatalogVersion: with end boundary and logData is valid\") {\n    val fileList = deltaFileStatuses(0L to 10L)\n    val logData = parsedRatifiedStagedCommits(Seq(0, 1, 2, 3))\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withMaxCatalogVersion(10)\n      .withEndBoundary(CommitBoundary.atVersion(3))\n      .withLogData(logData.toList.asJava)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 3)\n  }\n\n  //////////////////////////////////////////////\n  // withLogData + endVersion validation tests\n  //////////////////////////////////////////////\n\n  test(\"withLogData: with endVersion, logData must cover the requested range\") {\n    val logData = parsedRatifiedStagedCommits(Seq(0, 1, 2))\n    val fileList = deltaFileStatuses(0L to 3L)\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager\n        .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n        .withEndBoundary(CommitBoundary.atVersion(3))\n        .withLogData(logData.toList.asJava)\n        .build(createMockFSListFromEngine(fileList))\n    }.getMessage\n\n    assert(exMsg.contains(\"the last logData version (2) must be >= endVersion (3)\"))\n  }\n\n  test(\"withLogData: with endVersion equal to last logData version is valid\") {\n    val logData = parsedRatifiedStagedCommits(Seq(0, 1, 2, 3))\n    val fileList = deltaFileStatuses(0L to 3L)\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withEndBoundary(CommitBoundary.atVersion(3))\n      .withLogData(logData.toList.asJava)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 3)\n  }\n\n  test(\"withLogData: with endVersion less than last logData version is valid\") {\n    val logData = parsedRatifiedStagedCommits(Seq(0, 1, 2, 3, 4, 5))\n    val fileList = deltaFileStatuses(0L to 5L)\n    val builder = TableManager\n      .loadCommitRange(dataPath.toString, CommitBoundary.atVersion(0))\n      .withEndBoundary(CommitBoundary.atVersion(3))\n      .withLogData(logData.toList.asJava)\n\n    val commitRange = builder.build(createMockFSListFromEngine(fileList))\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 3)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/DeltaHistoryManagerSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.io.FileNotFoundException\nimport java.util\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.TransactionSuite.testSchema\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.exceptions.TableNotFoundException\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter\nimport io.delta.kernel.internal.files.{ParsedCatalogCommitData, ParsedPublishedDeltaData}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.lang.Lazy\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext\nimport io.delta.kernel.internal.snapshot.LogSegment\nimport io.delta.kernel.internal.util.{FileNames, VectorUtils}\nimport io.delta.kernel.internal.util.InCommitTimestampUtils\nimport io.delta.kernel.internal.util.VectorUtils.{buildArrayValue, stringStringMapValue}\nimport io.delta.kernel.test.{MockFileSystemClientUtils, MockListFromFileSystemClient, MockReadICTFileJsonHandler}\nimport io.delta.kernel.test.MockSnapshotUtils.getMockSnapshot\nimport io.delta.kernel.types.StringType\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeltaHistoryManagerSuite extends AnyFunSuite with MockFileSystemClientUtils {\n\n  // Helper function for non-catalog-managed tables (no staged commits)\n  private def getActiveCommitAtTimestamp(\n      engine: Engine,\n      latestSnapshot: SnapshotImpl,\n      logPath: Path,\n      timestamp: Long,\n      mustBeRecreatable: Boolean = true,\n      canReturnLastCommit: Boolean = false,\n      canReturnEarliestCommit: Boolean = false): DeltaHistoryManager.Commit = {\n    DeltaHistoryManager.getActiveCommitAtTimestamp(\n      engine,\n      latestSnapshot,\n      logPath,\n      timestamp,\n      mustBeRecreatable,\n      canReturnLastCommit,\n      canReturnEarliestCommit,\n      Seq.empty.asJava /* parsedLogDelta */\n    )\n  }\n\n  def checkGetActiveCommitAtTimestamp(\n      fileList: Seq[FileStatus],\n      timestamp: Long,\n      expectedVersion: Long,\n      mustBeRecreatable: Boolean = true,\n      canReturnLastCommit: Boolean = false,\n      canReturnEarliestCommit: Boolean = false): Unit = {\n    val lastDelta = fileList.map(_.getPath).filter(FileNames.isCommitFile).last\n    val latestVersion = FileNames.getFileVersion(new Path(lastDelta))\n    val activeCommit = getActiveCommitAtTimestamp(\n      createMockFSListFromEngine(fileList),\n      getMockSnapshot(dataPath, latestVersion = latestVersion),\n      logPath,\n      timestamp,\n      mustBeRecreatable,\n      canReturnLastCommit,\n      canReturnEarliestCommit)\n    assert(\n      activeCommit.getVersion == expectedVersion,\n      s\"Expected version $expectedVersion but got $activeCommit for timestamp=$timestamp\")\n\n    if (mustBeRecreatable) {\n      // When mustBeRecreatable=true, we should have the same answer as mustBeRecreatable=false\n      // for valid queries that do not throw an error\n      val activeCommit = getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(fileList),\n        getMockSnapshot(dataPath, latestVersion),\n        logPath,\n        timestamp,\n        false, // mustBeRecreatable\n        canReturnLastCommit,\n        canReturnEarliestCommit)\n      assert(\n        activeCommit.getVersion == expectedVersion,\n        s\"Expected version $expectedVersion but got $activeCommit for timestamp=$timestamp\")\n    }\n  }\n\n  def checkGetActiveCommitAtTimestampError[T <: Throwable](\n      fileList: Seq[FileStatus],\n      latestVersion: Long,\n      timestamp: Long,\n      expectedErrorMessageContains: String,\n      mustBeRecreatable: Boolean = true,\n      canReturnLastCommit: Boolean = false,\n      canReturnEarliestCommit: Boolean = false)(implicit classTag: ClassTag[T]): Unit = {\n    val e = intercept[T] {\n      getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(fileList),\n        getMockSnapshot(dataPath, latestVersion = latestVersion),\n        logPath,\n        timestamp,\n        mustBeRecreatable,\n        canReturnLastCommit,\n        canReturnEarliestCommit)\n    }\n    assert(e.getMessage.contains(expectedErrorMessageContains))\n  }\n\n  test(\"getActiveCommitAtTimestamp: basic listing from 0 with no checkpoints\") {\n    val deltaFiles = deltaFileStatuses(Seq(0L, 1L, 2L))\n    // Valid queries\n    checkGetActiveCommitAtTimestamp(deltaFiles, 0, 0)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 1, 0)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 10, 1)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 11, 1)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 20, 2)\n    // Invalid queries\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 2L,\n      -1,\n      DeltaErrors.timestampBeforeFirstAvailableCommit(dataPath.toString, -1, 0, 0).getMessage)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 2L,\n      21,\n      DeltaErrors.timestampAfterLatestCommit(dataPath.toString, 21, 20, 2).getMessage)\n    // Valid queries with canReturnLastCommit=true and canReturnEarliestCommit=true\n    checkGetActiveCommitAtTimestamp(deltaFiles, -1, 0, canReturnEarliestCommit = true)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 21, 2, canReturnLastCommit = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp: basic listing from 0 with a checkpoint\") {\n    val deltaFiles = deltaFileStatuses(Seq(0L, 1L, 2L)) ++ singularCheckpointFileStatuses(Seq(2L))\n    // Valid queries\n    checkGetActiveCommitAtTimestamp(deltaFiles, 0, 0)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 1, 0)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 10, 1)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 11, 1)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 20, 2)\n    // Invalid queries\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 2L,\n      -1,\n      DeltaErrors.timestampBeforeFirstAvailableCommit(dataPath.toString, -1, 0, 0).getMessage)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 2L,\n      21,\n      DeltaErrors.timestampAfterLatestCommit(dataPath.toString, 21, 20, 2).getMessage)\n    // Valid queries with canReturnLastCommit=true and canReturnEarliestCommit=true\n    checkGetActiveCommitAtTimestamp(deltaFiles, -1, 0, canReturnEarliestCommit = true)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 21, 2, canReturnLastCommit = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp: truncated delta log\") {\n    val deltaFiles = deltaFileStatuses(Seq(2L, 3L)) ++ singularCheckpointFileStatuses(Seq(2L))\n    // Valid queries\n    checkGetActiveCommitAtTimestamp(deltaFiles, 20, 2)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 25, 2)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 30, 3)\n    // Invalid queries\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 3L,\n      8,\n      DeltaErrors.timestampBeforeFirstAvailableCommit(dataPath.toString, 8, 20, 2).getMessage)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 3L,\n      31,\n      DeltaErrors.timestampAfterLatestCommit(dataPath.toString, 31, 30, 3).getMessage)\n    // Valid queries with canReturnLastCommit=true and canReturnEarliestCommit=true\n    checkGetActiveCommitAtTimestamp(deltaFiles, 8, 2, canReturnEarliestCommit = true)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 31, 3, canReturnLastCommit = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp: truncated delta log only checkpoint version\") {\n    val deltaFiles = deltaFileStatuses(Seq(2L)) ++ singularCheckpointFileStatuses(Seq(2L))\n    // Valid queries\n    checkGetActiveCommitAtTimestamp(deltaFiles, 20, 2)\n    // Invalid queries\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 2L,\n      8,\n      DeltaErrors.timestampBeforeFirstAvailableCommit(dataPath.toString, 8, 20, 2).getMessage)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 2L,\n      21,\n      DeltaErrors.timestampAfterLatestCommit(dataPath.toString, 21, 20, 2).getMessage)\n    // Valid queries with canReturnLastCommit=true and canReturnEarliestCommit=true\n    checkGetActiveCommitAtTimestamp(deltaFiles, 8, 2, canReturnEarliestCommit = true)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 21, 2, canReturnLastCommit = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp: truncated delta log with multi-part checkpoint\") {\n    val deltaFiles = deltaFileStatuses(Seq(2L, 3L)) ++ multiCheckpointFileStatuses(Seq(2L), 2)\n    // Valid queries\n    checkGetActiveCommitAtTimestamp(deltaFiles, 20, 2)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 25, 2)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 30, 3)\n    // Invalid queries\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 3L,\n      8,\n      DeltaErrors.timestampBeforeFirstAvailableCommit(dataPath.toString, 8, 20, 2).getMessage)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFiles,\n      latestVersion = 3L,\n      31,\n      DeltaErrors.timestampAfterLatestCommit(dataPath.toString, 31, 30, 3).getMessage)\n    // Valid queries with canReturnLastCommit=true and canReturnEarliestCommit=true\n    checkGetActiveCommitAtTimestamp(deltaFiles, 8, 2, canReturnEarliestCommit = true)\n    checkGetActiveCommitAtTimestamp(deltaFiles, 31, 3, canReturnLastCommit = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp: throws table not found exception\") {\n    // Non-existent path\n    intercept[TableNotFoundException](\n      getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(p => throw new FileNotFoundException(p)),\n        getMockSnapshot(dataPath, latestVersion = 1L),\n        logPath,\n        timestamp = 0))\n    // Empty _delta_log directory\n    intercept[TableNotFoundException](\n      getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(p => Seq()),\n        getMockSnapshot(dataPath, latestVersion = 1L),\n        logPath,\n        timestamp = 0))\n  }\n\n  // TODO: corrects commit timestamps for increasing commits (monotonizeCommitTimestamps)?\n  //  (see test \"getCommits should monotonize timestamps\" in DeltaTimeTravelSuite)?\n\n  test(\"getActiveCommitAtTimestamp: corrupt listings\") {\n    // No checkpoint or 000.json present\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFileStatuses(Seq(1L, 2L, 3L)),\n      latestVersion = 3L,\n      25,\n      \"No recreatable commits found\")\n    // Must have corresponding delta file for a checkpoint\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      singularCheckpointFileStatuses(Seq(1L)) ++ deltaFileStatuses(Seq(2L, 3L)),\n      latestVersion = 3L,\n      25,\n      \"No recreatable commits found\")\n    // No commit files at all (only checkpoint files)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      singularCheckpointFileStatuses(Seq(1L)),\n      latestVersion = 1L,\n      25,\n      \"No commits found\")\n    // No delta files\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      Seq(\"foo\", \"notdelta.parquet\", \"foo.json\", \"001.checkpoint.00f.oo0.parquet\")\n        .map(new Path(logPath, _))\n        .map(path => FileStatus.of(path.toString, 10, 10)),\n      latestVersion = 1L,\n      25,\n      \"No delta files found in the directory\")\n    // No complete checkpoint\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      deltaFileStatuses(Seq(2L, 3L)) ++ multiCheckpointFileStatuses(Seq(2L), 3).take(2),\n      latestVersion = 3L,\n      25,\n      \"No recreatable commits found\")\n  }\n\n  test(\"getActiveCommitAtTimestamp: when mustBeRecreatable=false\") {\n    Seq(\n      deltaFileStatuses(Seq(1L, 2L, 3L)), // w/o checkpoint\n      singularCheckpointFileStatuses(Seq(2L)) ++ deltaFileStatuses(Seq(1L, 2L, 3L)) // w/checkpoint\n    ).foreach { deltaFiles =>\n      // Valid queries\n      checkGetActiveCommitAtTimestamp(deltaFiles, 10, 1, mustBeRecreatable = false)\n      checkGetActiveCommitAtTimestamp(deltaFiles, 11, 1, mustBeRecreatable = false)\n      checkGetActiveCommitAtTimestamp(deltaFiles, 20, 2, mustBeRecreatable = false)\n      checkGetActiveCommitAtTimestamp(deltaFiles, 21, 2, mustBeRecreatable = false)\n      checkGetActiveCommitAtTimestamp(deltaFiles, 30, 3, mustBeRecreatable = false)\n      // Invalid queries\n      checkGetActiveCommitAtTimestampError[RuntimeException](\n        deltaFiles,\n        latestVersion = 3L,\n        -1,\n        DeltaErrors.timestampBeforeFirstAvailableCommit(dataPath.toString, -1, 10, 1).getMessage,\n        mustBeRecreatable = false)\n      checkGetActiveCommitAtTimestampError[RuntimeException](\n        deltaFiles,\n        latestVersion = 3L,\n        31,\n        DeltaErrors.timestampAfterLatestCommit(dataPath.toString, 31, 30, 3).getMessage,\n        mustBeRecreatable = false)\n      // Valid queries with canReturnLastCommit=true and canReturnEarliestCommit=true\n      checkGetActiveCommitAtTimestamp(\n        deltaFiles,\n        0,\n        1,\n        mustBeRecreatable = false,\n        canReturnEarliestCommit = true)\n      checkGetActiveCommitAtTimestamp(\n        deltaFiles,\n        31,\n        3,\n        mustBeRecreatable = false,\n        canReturnLastCommit = true)\n    }\n  }\n\n  test(\"getActiveCommitAtTimestamp: mustBeRecreatable=false error cases\") {\n    /* ---------- TABLE NOT FOUND --------- */\n    // Non-existent path\n    intercept[TableNotFoundException](\n      getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(p => throw new FileNotFoundException(p)),\n        getMockSnapshot(dataPath, latestVersion = 1L),\n        logPath,\n        timestamp = 0,\n        mustBeRecreatable = false))\n    // Empty _delta_log directory\n    intercept[TableNotFoundException](\n      getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(p => Seq()),\n        getMockSnapshot(dataPath, latestVersion = 1L),\n        logPath,\n        timestamp = 0))\n    /* ---------- CORRUPT LISTINGS --------- */\n    // No commit files at all (only checkpoint files)\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      singularCheckpointFileStatuses(Seq(1L)),\n      latestVersion = 1L,\n      25,\n      \"No delta files found in the directory\",\n      mustBeRecreatable = false)\n    // No delta files\n    checkGetActiveCommitAtTimestampError[RuntimeException](\n      Seq(\"foo\", \"notdelta.parquet\", \"foo.json\", \"001.checkpoint.00f.oo0.parquet\")\n        .map(new Path(logPath, _))\n        .map(path => FileStatus.of(path.toString, 10, 10)),\n      latestVersion = 1L,\n      25,\n      \"No delta files found in the directory\",\n      mustBeRecreatable = false)\n  }\n\n  // ========== ICT TIME TRAVEL TESTS ==========\n\n  /**\n   * Common function to test getActiveCommitAtTimestamp with all combinations of boolean flags.\n   * This reduces duplication and ensures comprehensive testing of flag combinations.\n   */\n  def checkGetActiveCommitAtTimestampWithAllFlags(\n      fileList: Seq[FileStatus],\n      timestamp: Long,\n      expectedVersion: Long,\n      ictEnablementInfoOpt: Option[(Long, Long)] = None,\n      shouldSucceed: Boolean = true,\n      expectedErrorMessageContains: String = \"\"): Unit = {\n\n    val lastDelta = fileList.map(_.getPath).filter(FileNames.isCommitFile).last\n    val latestVersion = FileNames.getFileVersion(new Path(lastDelta))\n\n    // Test all combinations of boolean flags\n    val flagCombinations = for {\n      mustBeRecreatable <- Seq(true, false)\n      canReturnLastCommit <- Seq(true, false)\n      canReturnEarliestCommit <- Seq(true, false)\n    } yield (mustBeRecreatable, canReturnLastCommit, canReturnEarliestCommit)\n\n    flagCombinations.foreach {\n      case (mustBeRecreatable, canReturnLastCommit, canReturnEarliestCommit) =>\n        if (shouldSucceed) {\n          val activeCommit = getActiveCommitAtTimestamp(\n            createMockFSListFromEngine(fileList),\n            getMockSnapshot(dataPath, latestVersion = latestVersion, ictEnablementInfoOpt),\n            logPath,\n            timestamp,\n            mustBeRecreatable,\n            canReturnLastCommit,\n            canReturnEarliestCommit)\n          assert(\n            activeCommit.getVersion == expectedVersion,\n            s\"Expected version $expectedVersion but got ${activeCommit.getVersion}  \" +\n              s\"for timestamp=$timestamp with flags: \" +\n              s\"mustBeRecreatable=$mustBeRecreatable, \" +\n              s\"canReturnLastCommit=$canReturnLastCommit, \" +\n              s\"canReturnEarliestCommit=$canReturnEarliestCommit\")\n        } else {\n          val e = intercept[Exception] {\n            getActiveCommitAtTimestamp(\n              createMockFSListFromEngine(fileList),\n              getMockSnapshot(dataPath, latestVersion = latestVersion, ictEnablementInfoOpt),\n              logPath,\n              timestamp,\n              mustBeRecreatable,\n              canReturnLastCommit,\n              canReturnEarliestCommit)\n          }\n          assert(\n            e.getMessage.contains(expectedErrorMessageContains),\n            s\"Expected error message to contain \" +\n              s\"'$expectedErrorMessageContains' but got '${e.getMessage}' \" +\n              s\"with flags: \" +\n              s\"mustBeRecreatable=$mustBeRecreatable, \" +\n              s\"canReturnLastCommit=$canReturnLastCommit, \" +\n              s\"canReturnEarliestCommit=$canReturnEarliestCommit\")\n        }\n    }\n  }\n\n  /**\n   * Common function to test ICT time travel scenarios.\n   */\n  def testICTTimeTravelScenario(\n      icts: Seq[Long],\n      modTimes: Seq[Long],\n      ictEnablementVersion: Long,\n      testCases: Seq[(Long, Long, String)] // (searchTimestamp, expectedVersion, description)\n  ): Unit = {\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(\n        FileNames.deltaFile(logPath, v),\n        1, /* size */\n        ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = icts.size - 1,\n      Some((ictEnablementVersion, deltaToICTMap(ictEnablementVersion))))\n\n    testCases.foreach { case (timestamp, expectedVersion, description) =>\n      val activeCommit = getActiveCommitAtTimestamp(\n        engine,\n        mockSnapshot,\n        logPath,\n        timestamp)\n      assert(\n        activeCommit.getVersion == expectedVersion,\n        s\"$description: Expected version $expectedVersion \" +\n          s\"but got ${activeCommit.getVersion} for timestamp=$timestamp\")\n\n      // Verify timestamp is correct based on ICT enablement\n      val expectedTimestamp = if (expectedVersion >= ictEnablementVersion) {\n        icts(expectedVersion.toInt)\n      } else {\n        modTimes(expectedVersion.toInt)\n      }\n      assert(\n        activeCommit.getTimestamp == expectedTimestamp,\n        s\"$description: Expected timestamp $expectedTimestamp but got ${activeCommit.getTimestamp}\")\n    }\n  }\n\n  test(\"ICT time travel: comprehensive enablement scenarios\") {\n    val icts = Seq(1L, 11L, 21L, 31L, 50L, 60L)\n    val modTimes = Seq(4L, 14L, 24L, 34L, 54L, 64L)\n\n    // Test enablement at version 0 (entire history has ICT)\n    testICTTimeTravelScenario(\n      icts,\n      modTimes,\n      ictEnablementVersion = 0L,\n      Seq(\n        (1L, 0L, \"Exact match at first ICT\"),\n        (5L, 0L, \"Between first and second ICT\"),\n        (11L, 1L, \"Exact match at second ICT\"),\n        (25L, 2L, \"Between third and fourth ICT\"),\n        (60L, 5L, \"Exact match at last ICT\")))\n\n    // Test enablement at version 1 (mixed ICT/non-ICT)\n    testICTTimeTravelScenario(\n      icts,\n      modTimes,\n      ictEnablementVersion = 1L,\n      Seq(\n        (4L, 0L, \"Non-ICT commit using modification time\"),\n        (11L, 1L, \"First ICT commit\"),\n        (25L, 2L, \"Between ICT commits\"),\n        (31L, 3L, \"Exact ICT match\"),\n        (60L, 5L, \"Last ICT commit\")))\n\n    // Test enablement at version 3 (mixed ICT/non-ICT)\n    testICTTimeTravelScenario(\n      icts,\n      modTimes,\n      ictEnablementVersion = 3L,\n      Seq(\n        (4L, 0L, \"Non-ICT commit\"),\n        (14L, 1L, \"Non-ICT commit\"),\n        (24L, 2L, \"Non-ICT commit before enablement\"),\n        (31L, 3L, \"First ICT commit\"),\n        (50L, 4L, \"ICT commit\"),\n        (60L, 5L, \"Last ICT commit\")))\n\n    // Test enablement at last version\n    testICTTimeTravelScenario(\n      icts,\n      modTimes,\n      ictEnablementVersion = 5L,\n      Seq(\n        (4L, 0L, \"Non-ICT commit\"),\n        (54L, 4L, \"Non-ICT commit before enablement\"),\n        (60L, 5L, \"Only ICT commit\")))\n  }\n\n  test(\"ICT time travel: boundary conditions and edge cases\") {\n    val icts = Seq(10L, 20L, 30L, 40L, 50L)\n    val modTimes = Seq(5L, 15L, 25L, 35L, 45L)\n\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n\n    // Test with ICT enabled from version 0\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = icts.size - 1,\n      Some((0L, deltaToICTMap(0L))))\n\n    // Test timestamp exactly at ICT enablement\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 10L)\n    assert(activeCommit1.getVersion == 0L)\n    assert(activeCommit1.getTimestamp == 10L)\n\n    // Test timestamp just before first ICT\n    val activeCommit2 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 9L,\n      canReturnEarliestCommit = true)\n    assert(activeCommit2.getVersion == 0L) // Should return earliest commit\n    assert(activeCommit2.getTimestamp == 10L)\n\n    // Test timestamp just after last ICT\n    intercept[io.delta.kernel.exceptions.KernelException] {\n      getActiveCommitAtTimestamp(\n        engine,\n        mockSnapshot,\n        logPath,\n        timestamp = 51L)\n    }\n\n    // Test with canReturnLastCommit=true\n    val activeCommit3 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 51L,\n      canReturnLastCommit = true)\n    assert(activeCommit3.getVersion == 4L)\n    assert(activeCommit3.getTimestamp == 50L)\n  }\n\n  test(\"ICT time travel: latest snapshot timestamp optimization\") {\n    val icts = Seq(10L, 20L, 30L)\n    val modTimes = Seq(5L, 15L, 25L)\n\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = icts.size - 1,\n      Some((0L, deltaToICTMap(0L))))\n\n    // Test timestamp equal to latest snapshot timestamp\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 30L)\n    assert(activeCommit1.getVersion == 2L)\n    assert(activeCommit1.getTimestamp == 30L)\n\n    // Test timestamp greater than latest snapshot timestamp\n    intercept[io.delta.kernel.exceptions.KernelException] {\n      getActiveCommitAtTimestamp(\n        engine,\n        mockSnapshot,\n        logPath,\n        timestamp = 35L)\n    }\n\n    // Test with canReturnLastCommit=true\n    val activeCommit2 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 35L,\n      canReturnLastCommit = true)\n    assert(activeCommit2.getVersion == 2L)\n    assert(activeCommit2.getTimestamp == 30L)\n  }\n\n  test(\"ICT time travel: mixed ICT and non-ICT commits with truncated log\") {\n    val icts = Seq(100L, 200L, 300L, 400L) // ICT enabled from version 2\n    val modTimes = Seq(50L, 150L, 250L, 350L)\n\n    // Simulate truncated log starting from version 1\n    val deltasWithModTimes = modTimes.drop(1).zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v + 1), 1, ts)\n    }\n    // Add a checkpoint file at version 1 to make the table recreatable\n    val checkpointFile = FileStatus.of(\n      FileNames.checkpointFileSingular(logPath, 1L).toString,\n      1,\n      150L)\n    val allFiles = checkpointFile +: deltasWithModTimes\n\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(allFiles, deltaToICTMap)\n\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = 3L,\n      Some((2L, deltaToICTMap(2L)))\n    ) // ICT enabled at version 2\n\n    // Test timestamp before ICT enablement (should use modification time)\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 150L)\n    assert(activeCommit1.getVersion == 1L)\n    assert(activeCommit1.getTimestamp == 150L) // modification time\n\n    // Test timestamp after ICT enablement (should use ICT)\n    val activeCommit2 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 300L)\n    assert(activeCommit2.getVersion == 2L)\n    assert(activeCommit2.getTimestamp == 300L) // ICT\n\n    // Test timestamp between ICT commits\n    val activeCommit3 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 350L)\n    assert(activeCommit3.getVersion == 2L)\n    assert(activeCommit3.getTimestamp == 300L) // Should return previous ICT commit\n  }\n\n  test(\"ICT time travel: non-ICT commits missing scenario\") {\n    val icts = Seq(100L, 200L, 300L)\n    val modTimes = Seq(50L, 150L, 250L)\n\n    // Simulate scenario where ICT enablement version <= earliest available version\n    val deltasWithModTimes = modTimes.drop(2).zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v + 2), 1, ts)\n    }\n    // Add a checkpoint file at version 2 to make the table recreatable\n    val checkpointFile = FileStatus.of(\n      FileNames.checkpointFileSingular(logPath, 2L).toString,\n      1,\n      250L)\n    val allFiles = checkpointFile +: deltasWithModTimes\n\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(allFiles, deltaToICTMap)\n\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = 2L,\n      Some((1L, deltaToICTMap(1L)))\n    ) // ICT enabled at version 1, but earliest available is 2\n\n    // Test timestamp before ICT enablement but non-ICT commits are missing\n    // Should return earliest available commit with its ICT\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 50L,\n      canReturnEarliestCommit = true)\n    assert(activeCommit1.getVersion == 2L)\n    assert(activeCommit1.getTimestamp == 300L) // ICT of earliest available commit\n\n    // Test error case when canReturnEarliestCommit=false\n    intercept[io.delta.kernel.exceptions.KernelException] {\n      getActiveCommitAtTimestamp(\n        engine,\n        mockSnapshot,\n        logPath,\n        timestamp = 50L)\n    }\n  }\n\n  test(\"ICT time travel: binary search edge cases\") {\n    // Test with odd number of commits\n    val icts = Seq(1L, 11L, 21L, 31L, 50L, 60L, 70L)\n    val modTimes = Seq(4L, 14L, 24L, 34L, 54L, 64L, 74L)\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = icts.size - 1,\n      Some((0L, deltaToICTMap(0L))))\n\n    // Test searchTimestamp is the exact match with the middle commit\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 31L // Exact match with version 3\n    )\n    assert(activeCommit1.getVersion == 3L)\n\n    // Test searchTimestamp = start case\n    val activeCommit2 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 1L // First ICT\n    )\n    assert(activeCommit2.getVersion == 0L)\n\n    // Test searchTimestamp = end case\n    val activeCommit3 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 70L // Last ICT\n    )\n    assert(activeCommit3.getVersion == 6L)\n\n    // Test with even number of commits\n    val ictsEven = Seq(1L, 11L, 21L, 31L, 50L, 60L)\n    val modTimesEven = Seq(4L, 14L, 24L, 34L, 54L, 64L)\n    val deltasWithModTimesEven = modTimesEven.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMapEven = ictsEven.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engineEven = createMockFSAndJsonEngineForICT(deltasWithModTimesEven, deltaToICTMapEven)\n    val mockSnapshotEven = getMockSnapshot(\n      dataPath,\n      latestVersion = ictsEven.size - 1,\n      Some((0L, deltaToICTMapEven(0L))))\n\n    val activeCommit4 = getActiveCommitAtTimestamp(\n      engineEven,\n      mockSnapshotEven,\n      logPath,\n      timestamp = 25L // Between version 2 and 3\n    )\n    assert(activeCommit4.getVersion == 2L)\n    assert(activeCommit4.getTimestamp == 21L)\n  }\n\n  test(\"ICT time travel: single commit scenario\") {\n    val icts = Seq(100L)\n    val modTimes = Seq(50L)\n\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = 0L,\n      Some((0L, deltaToICTMap(0L))))\n\n    // Test exact match\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 100L)\n    assert(activeCommit1.getVersion == 0L)\n    assert(activeCommit1.getTimestamp == 100L)\n\n    // Test timestamp before single commit\n    val activeCommit2 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 50L,\n      canReturnEarliestCommit = true)\n    assert(activeCommit2.getVersion == 0L)\n    assert(activeCommit2.getTimestamp == 100L)\n\n    // Test timestamp after single commit\n    val activeCommit3 = getActiveCommitAtTimestamp(\n      engine,\n      mockSnapshot,\n      logPath,\n      timestamp = 150L,\n      canReturnLastCommit = true)\n    assert(activeCommit3.getVersion == 0L)\n    assert(activeCommit3.getTimestamp == 100L)\n  }\n\n  test(\"ICT time travel: modification times out of order\") {\n    val icts = Seq(10L, 20L, 30L, 40L)\n    val modTimes = Seq(40L, 30L, 20L, 10L) // Reversed modification times\n\n    testICTTimeTravelScenario(\n      icts,\n      modTimes,\n      ictEnablementVersion = 0L,\n      Seq(\n        (10L, 0L, \"First ICT despite reversed mod times\"),\n        (15L, 0L, \"Between first and second ICT\"),\n        (20L, 1L, \"Second ICT\"),\n        (40L, 3L, \"Last ICT\")))\n  }\n\n  test(\"ICT time travel: comprehensive boolean flag combinations\") {\n    val icts = Seq(10L, 20L, 30L)\n    val modTimes = Seq(5L, 15L, 25L)\n\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n\n    val mockSnapshot = getMockSnapshot(\n      dataPath,\n      latestVersion = 2L,\n      Some((0L, deltaToICTMap(0L))))\n\n    // Test all flag combinations for valid timestamp\n    val flagCombinations = for {\n      mustBeRecreatable <- Seq(true, false)\n      canReturnLastCommit <- Seq(true, false)\n      canReturnEarliestCommit <- Seq(true, false)\n    } yield (mustBeRecreatable, canReturnLastCommit, canReturnEarliestCommit)\n\n    flagCombinations.foreach {\n      case (mustBeRecreatable, canReturnLastCommit, canReturnEarliestCommit) =>\n        val activeCommit = getActiveCommitAtTimestamp(\n          engine,\n          mockSnapshot,\n          logPath,\n          timestamp = 20L,\n          mustBeRecreatable,\n          canReturnLastCommit,\n          canReturnEarliestCommit)\n        assert(activeCommit.getVersion == 1L)\n        assert(activeCommit.getTimestamp == 20L)\n    }\n\n    // Test edge cases with different flag combinations\n    // Timestamp before earliest commit\n    flagCombinations.foreach {\n      case (mustBeRecreatable, canReturnLastCommit, canReturnEarliestCommit) =>\n        if (canReturnEarliestCommit) {\n          val activeCommit = getActiveCommitAtTimestamp(\n            engine,\n            mockSnapshot,\n            logPath,\n            timestamp = 5L,\n            mustBeRecreatable,\n            canReturnLastCommit,\n            canReturnEarliestCommit)\n          assert(activeCommit.getVersion == 0L)\n        } else {\n          intercept[io.delta.kernel.exceptions.KernelException] {\n            getActiveCommitAtTimestamp(\n              engine,\n              mockSnapshot,\n              logPath,\n              timestamp = 5L,\n              mustBeRecreatable,\n              canReturnLastCommit,\n              canReturnEarliestCommit)\n          }\n        }\n    }\n\n    // Timestamp after latest commit\n    flagCombinations.foreach {\n      case (mustBeRecreatable, canReturnLastCommit, canReturnEarliestCommit) =>\n        if (canReturnLastCommit) {\n          val activeCommit = getActiveCommitAtTimestamp(\n            engine,\n            mockSnapshot,\n            logPath,\n            timestamp = 35L,\n            mustBeRecreatable,\n            canReturnLastCommit,\n            canReturnEarliestCommit)\n          assert(activeCommit.getVersion == 2L)\n        } else {\n          intercept[io.delta.kernel.exceptions.KernelException] {\n            getActiveCommitAtTimestamp(\n              engine,\n              mockSnapshot,\n              logPath,\n              timestamp = 35L,\n              mustBeRecreatable,\n              canReturnLastCommit,\n              canReturnEarliestCommit)\n          }\n        }\n    }\n  }\n\n  test(\"ICT time travel: error handling and edge cases\") {\n    val icts = Seq(10L, 20L, 30L)\n    val modTimes = Seq(5L, 15L, 25L)\n\n    val deltasWithModTimes = modTimes.zipWithIndex.map { case (ts, v) =>\n      FileStatus.of(FileNames.deltaFile(logPath, v), 1, ts)\n    }\n    val deltaToICTMap = icts.zipWithIndex.map { case (ts, v) => v.toLong -> ts }.toMap\n    val engine = createMockFSAndJsonEngineForICT(deltasWithModTimes, deltaToICTMap)\n\n    // Test with ICT not enabled\n    val nonICTSnapshot = getMockSnapshot(dataPath, latestVersion = 2L, None)\n    val activeCommit1 = getActiveCommitAtTimestamp(\n      createMockFSListFromEngine(deltasWithModTimes),\n      nonICTSnapshot,\n      logPath,\n      timestamp = 15L)\n    assert(activeCommit1.getVersion == 1L)\n    assert(activeCommit1.getTimestamp == 15L) // Should use modification time\n\n    // Test with malformed ICT enablement info (only version set)\n    val malformedConfig = Map(\n      TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\",\n      TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey -> \"1\"\n      // Missing enablement timestamp\n    )\n    val malformedMetadata = new Metadata(\n      \"id\",\n      Optional.empty(),\n      Optional.empty(),\n      new Format(),\n      testSchema.toJson,\n      testSchema,\n      buildArrayValue(java.util.Arrays.asList(\"c3\"), StringType.STRING),\n      Optional.of(123),\n      stringStringMapValue(malformedConfig.asJava))\n\n    val malformedSnapshot = new SnapshotImpl(\n      dataPath,\n      2L,\n      new Lazy(() =>\n        new LogSegment(\n          logPath,\n          2L,\n          Seq(deltaFileStatus(2L)).asJava,\n          Seq.empty.asJava,\n          Seq.empty.asJava,\n          deltaFileStatus(2L),\n          Optional.empty(), /* lastSeenChecksum */\n          Optional.empty() /* maxPublishedDeltaVersion */\n        )),\n      null, /* logReplay */\n      new Protocol(1, 2),\n      malformedMetadata,\n      DefaultFileSystemManagedTableOnlyCommitter.INSTANCE,\n      SnapshotQueryContext.forLatestSnapshot(dataPath.toString),\n      Optional.empty() /* inCommitTimestampOpt */ )\n\n    intercept[IllegalStateException] {\n      getActiveCommitAtTimestamp(\n        engine,\n        malformedSnapshot,\n        logPath,\n        timestamp = 15L)\n    }\n  }\n\n  test(\"greatestLowerBound: basic functionality\") {\n    // Test with a simple sequence of timestamps\n    val timestamps = Seq(1L, 3L, 5L, 7L, 9L)\n    val indexToValueMapper = new java.util.function.Function[java.lang.Long, java.lang.Long] {\n      override def apply(index: java.lang.Long): java.lang.Long = timestamps(index.toInt)\n    }\n\n    // Test exact match (should return index 2, value 5)\n    val result1 = InCommitTimestampUtils.greatestLowerBound(5L, 0L, 4L, indexToValueMapper)\n    assert(result1.isPresent)\n    assert(result1.get._1 == 2L)\n    assert(result1.get._2 == 5L)\n\n    // Test between values (should return index 1, value 3)\n    val result2 = InCommitTimestampUtils.greatestLowerBound(4L, 0L, 4L, indexToValueMapper)\n    assert(result2.isPresent)\n    assert(result2.get._1 == 1L)\n    assert(result2.get._2 == 3L)\n\n    // Test before first value (should not be present)\n    val result3 = InCommitTimestampUtils.greatestLowerBound(0L, 0L, 4L, indexToValueMapper)\n    assert(!result3.isPresent)\n\n    // Test after last value (should return last index/value)\n    val result4 = InCommitTimestampUtils.greatestLowerBound(10L, 0L, 4L, indexToValueMapper)\n    assert(result4.isPresent)\n    assert(result4.get._1 == 4L)\n    assert(result4.get._2 == 9L)\n  }\n\n  test(\"greatestLowerBound: single element in search range\") {\n    // Test with single element\n    val singleElement = Seq(5L)\n    val singleElementMapper = new java.util.function.Function[java.lang.Long, java.lang.Long] {\n      override def apply(index: java.lang.Long): java.lang.Long = singleElement(index.toInt)\n    }\n\n    // Target equals the element\n    val result1 = InCommitTimestampUtils.greatestLowerBound(5L, 0L, 0L, singleElementMapper)\n    assert(result1.isPresent)\n    assert(result1.get._1 == 0L)\n    assert(result1.get._2 == 5L)\n\n    // Target less than the element\n    val result2 = InCommitTimestampUtils.greatestLowerBound(4L, 0L, 0L, singleElementMapper)\n    assert(!result2.isPresent)\n\n    // Target greater than the element\n    val result3 = InCommitTimestampUtils.greatestLowerBound(6L, 0L, 0L, singleElementMapper)\n    assert(result3.isPresent)\n    assert(result3.get._1 == 0L)\n    assert(result3.get._2 == 5L)\n\n    // Test with empty range (should not be present)\n    val result4 = InCommitTimestampUtils.greatestLowerBound(5L, 1L, 0L, singleElementMapper)\n    assert(!result4.isPresent)\n  }\n\n  test(\"greatestLowerBound: binary search correctness\") {\n    // Test with a larger sequence to verify binary search correctness\n    val timestamps = (0L until 100L by 2).toSeq // 0, 2, 4, ..., 98\n    val indexToValueMapper = new java.util.function.Function[java.lang.Long, java.lang.Long] {\n      override def apply(index: java.lang.Long): java.lang.Long = timestamps(index.toInt)\n    }\n\n    // Test various positions in the sequence (exact matches)\n    for (i <- 0 until 50) {\n      val target = i * 2L\n      val result = InCommitTimestampUtils.greatestLowerBound(target, 0L, 49L, indexToValueMapper)\n      assert(result.isPresent)\n      assert(result.get._1 == i)\n      assert(result.get._2 == target)\n    }\n\n    // Test values between elements (should return the lower index/value)\n    for (i <- 1 until 50) {\n      val target = i * 2L - 1\n      val result = InCommitTimestampUtils.greatestLowerBound(target, 0L, 49L, indexToValueMapper)\n      assert(result.isPresent)\n      assert(result.get._1 == i - 1)\n      assert(result.get._2 == (i - 1) * 2L)\n    }\n\n    // Test value less than all (should not be present)\n    val resultLow = InCommitTimestampUtils.greatestLowerBound(-1L, 0L, 49L, indexToValueMapper)\n    assert(!resultLow.isPresent)\n\n    // Test value greater than all (should return last index/value)\n    val resultHigh = InCommitTimestampUtils.greatestLowerBound(100L, 0L, 49L, indexToValueMapper)\n    assert(resultHigh.isPresent)\n    assert(resultHigh.get._1 == 49L)\n    assert(resultHigh.get._2 == 98L)\n  }\n\n  // ============== Tests for Staged Commits Support ===============\n\n  private def checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList: Seq[FileStatus],\n      catalogCommits: Seq[ParsedCatalogCommitData],\n      versionToICT: Map[Long, Long],\n      timestampToQuery: Long,\n      expectedVersion: Long,\n      canReturnLastCommit: Boolean = false,\n      canReturnEarliestCommit: Boolean = false,\n      add10ToICTForStagedFiles: Boolean = false,\n      ictEnablementInfo: (Long, Long) = (0, 0)): Unit = {\n    // Create mock engine with ICT reading support\n    val mockJsonHandler = new MockReadICTFileJsonHandler(versionToICT, add10ToICTForStagedFiles)\n    val mockedEngine = mockEngine(\n      fileSystemClient = new MockListFromFileSystemClient(listFromProvider(fileList)),\n      jsonHandler = mockJsonHandler)\n\n    def getVersionFromFS(fs: FileStatus): Long = FileNames.getFileVersion(new Path(fs.getPath))\n    val latestVersion = fileList.map(getVersionFromFS(_)).max\n    // If we have a ratified commit file at the end version, we want to use this in the log segment\n    // for our mockLatestSnapshot, so we get the ICT from that file\n    val deltaFileAtEndVersion = fileList\n      .filter(fs => FileNames.isStagedDeltaFile(fs.getPath))\n      .find(getVersionFromFS(_) == latestVersion)\n\n    val mockLatestSnapshot =\n      getMockSnapshot(\n        dataPath,\n        latestVersion = latestVersion,\n        ictEnablementInfoOpt = Some(ictEnablementInfo),\n        deltaFileAtEndVersion = deltaFileAtEndVersion)\n\n    val activeCommit = DeltaHistoryManager.getActiveCommitAtTimestamp(\n      mockedEngine,\n      mockLatestSnapshot,\n      logPath,\n      timestampToQuery,\n      false,\n      canReturnLastCommit,\n      canReturnEarliestCommit,\n      catalogCommits.asJava)\n    assert(\n      activeCommit.getVersion == expectedVersion,\n      s\"Expected version $expectedVersion but got ${activeCommit.getVersion} \" +\n        s\"for timestamp=$timestampToQuery\")\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: empty log, 1 ratified commit\") {\n    // Published commits: _\n    // Ratified commits: V0\n    val catalogCommitFiles = Seq(stagedCommitFile(0L))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val versionToICT = Map(0L -> 180L)\n\n    // Query the exact timestamp\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      catalogCommitFiles,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = versionToICT(0),\n      expectedVersion = 0)\n\n    // Querying before without canReturnEarliestCommit results in error\n    intercept[KernelException] {\n      checkGetActiveCommitAtTimestampWithParsedLogData(\n        catalogCommitFiles,\n        parsedLogData,\n        versionToICT,\n        timestampToQuery = versionToICT(0) - 10,\n        expectedVersion = 0)\n    }\n\n    // Querying before with canReturnEarliestCommit passes\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      catalogCommitFiles,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = versionToICT(0) - 10,\n      expectedVersion = 0,\n      canReturnEarliestCommit = true)\n\n    // Querying after without canReturnLatestCommit results in error\n    intercept[KernelException] {\n      checkGetActiveCommitAtTimestampWithParsedLogData(\n        catalogCommitFiles,\n        parsedLogData,\n        versionToICT,\n        timestampToQuery = versionToICT(0) + 10,\n        expectedVersion = 0)\n    }\n\n    // Querying after with canReturnLatestCommit passes\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      catalogCommitFiles,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = versionToICT(0) + 10,\n      expectedVersion = 0,\n      canReturnLastCommit = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: empty log, 2 ratified commit\") {\n    // Published commits: _\n    // Ratified commits: V0, V1\n    val catalogCommits = Seq(stagedCommitFile(0L), stagedCommitFile(1L))\n    val parsedLogData = catalogCommits.map(ParsedCatalogCommitData.forFileStatus(_))\n    val versionToICT = Map(0L -> 180L, 1L -> 280L)\n\n    // Query the exact timestamp of V0\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      catalogCommits,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 180L,\n      expectedVersion = 0)\n\n    // Query in between V0 and V1\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      catalogCommits,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 200L,\n      expectedVersion = 0)\n\n    // Query the exact timestamp of V1\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      catalogCommits,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 280L,\n      expectedVersion = 1)\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: no overlap\") {\n    // Published commits: V0, V1\n    // Ratified commits: V2, V3\n    val catalogCommitFiles = Seq(stagedCommitFile(2L), stagedCommitFile(3L))\n    val fileList = Seq(\n      deltaFileStatus(0),\n      deltaFileStatus(1)) ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 180L, 1L -> 280L, 2L -> 380L, 3L -> 480L)\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n\n    // Query the exact timestamp of V1\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 280L,\n      expectedVersion = 1)\n\n    // Query in between V1 and V2\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 300L,\n      expectedVersion = 1)\n\n    // Query the exact timestamp of V2\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 380L,\n      expectedVersion = 2)\n\n    // Query in between V2 and V3\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 400L,\n      expectedVersion = 2)\n\n    // Query the exact timestamp of V3\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 480L,\n      expectedVersion = 3)\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: \" +\n    \"v0 published and ratified => prefer ratified\") {\n    // Published commits: V0\n    // Ratified commits: V0\n    val catalogCommitFiles = Seq(stagedCommitFile(0L))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val fileList = Seq(deltaFileStatus(0)) ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 200L)\n    // If we read from the published file, we should get ICT=200\n    // If we read from the ratified file, we should get ICT=210 (correct behavior!)\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 210L,\n      expectedVersion = 0,\n      add10ToICTForStagedFiles = true)\n\n    intercept[KernelException] {\n      checkGetActiveCommitAtTimestampWithParsedLogData(\n        fileList,\n        parsedLogData,\n        versionToICT,\n        timestampToQuery = 200L,\n        expectedVersion = 0,\n        add10ToICTForStagedFiles = true)\n    }\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: overlap => prefer ratified\") {\n    // Published commits: V10, V11\n    // Ratified commits: V11, V12\n    val catalogCommitFiles = Seq(stagedCommitFile(11), stagedCommitFile(12))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val fileList = Seq(\n      classicCheckpointFileStatus(10),\n      deltaFileStatus(10),\n      deltaFileStatus(11)) ++ catalogCommitFiles\n    val versionToICT = Map(10L -> 1000L, 11L -> 1100L, 12L -> 1200L)\n    // We have v10=1000, v11=1110 (if we use the ratified commit), v12=1210\n\n    // Read at v10\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1000L,\n      expectedVersion = 10,\n      add10ToICTForStagedFiles = true)\n\n    // Read between v10 and v11 (if we incorrectly use the published file this will fail!)\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1101L,\n      expectedVersion = 10,\n      add10ToICTForStagedFiles = true)\n\n    // Read at v11\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1110L,\n      expectedVersion = 11,\n      add10ToICTForStagedFiles = true)\n\n    // Read between v11 and v12\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1150,\n      expectedVersion = 11,\n      add10ToICTForStagedFiles = true)\n\n    // Read at v12\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1210,\n      expectedVersion = 12,\n      add10ToICTForStagedFiles = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: \" +\n    \"discontinuous catalog commits => prefer ratified\") {\n    // Published commits: V0, V1, V2\n    // Ratified commits: V0, V2\n    val catalogCommitFiles = Seq(stagedCommitFile(0), stagedCommitFile(2))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val fileList = Seq(\n      deltaFileStatus(0),\n      deltaFileStatus(1),\n      deltaFileStatus(2)) ++ catalogCommitFiles\n    val versionToICT = Map(0L -> 1000L, 1L -> 2000L, 2L -> 3000L)\n    // We have v0=1010, v1=2000, v2=3010 assuming we use the ratified commits > published commits\n\n    // Read at published file ICT for v0 should fail\n    intercept[KernelException] {\n      checkGetActiveCommitAtTimestampWithParsedLogData(\n        fileList,\n        parsedLogData,\n        versionToICT,\n        timestampToQuery = 1000L,\n        expectedVersion = 0,\n        add10ToICTForStagedFiles = true)\n    }\n\n    // Read at correct version for v0 if using staged commit\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1010L,\n      expectedVersion = 0L,\n      add10ToICTForStagedFiles = true)\n\n    // Read between v0 and v1\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 1500L,\n      expectedVersion = 0,\n      add10ToICTForStagedFiles = true)\n\n    // Read at v1\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 2000L,\n      expectedVersion = 1,\n      add10ToICTForStagedFiles = true)\n\n    // Read between v1 and v2 (this will fail if we don't use the ratified commit)\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 3000L,\n      expectedVersion = 1,\n      add10ToICTForStagedFiles = true)\n\n    // Read at v2\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 3010L,\n      expectedVersion = 2,\n      add10ToICTForStagedFiles = true)\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: ICT enabled after v0\") {\n    // Published commits: V0 (non-ICT), V1 (enables ICT)\n    // Ratified commits: V2\n    val catalogCommitFiles = Seq(stagedCommitFile(2))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val fileList = Seq(\n      deltaFileStatus(0),\n      deltaFileStatus(1)) ++ catalogCommitFiles\n    val versionToICT = Map(1L -> 2000L, 2L -> 3000L)\n    val ictEnablementInfo = (1L, 2000L) // (version, timestamp)\n\n    // Query exact timestamp of v0 (no ICT)\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 0L,\n      expectedVersion = 0,\n      ictEnablementInfo = ictEnablementInfo)\n\n    // Query between v0 and v1\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 8L,\n      expectedVersion = 0,\n      ictEnablementInfo = ictEnablementInfo)\n\n    // TODO: this fails due to an existing bug when querying a timestamp between\n    //  (ictEnablementVersionFsTs, ictEnablementTs) -- re-enable this once it's fixed\n    // Query between v0 and v1 - ICT based\n    /*\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      500L,\n      0,\n      ictEnablementInfo = ictEnablementInfo)\n     */\n\n    // Query exact timestamp of v1 (ICT)\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 2000L,\n      expectedVersion = 1,\n      ictEnablementInfo = ictEnablementInfo)\n\n    // Query between v1 and v2\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 2500L,\n      expectedVersion = 1,\n      ictEnablementInfo = ictEnablementInfo)\n\n    // Query exact v2\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 3000L,\n      expectedVersion = 2,\n      ictEnablementInfo = ictEnablementInfo)\n  }\n\n  test(\"getActiveCommitAtTimestamp with catalog commits: ICT enabled after v0 and \" +\n    \"only ICT commits available\") {\n    // This tests the scenario where we are searching for a pre-ICT time but all the non-ICT commits\n    // are missing. This throws an error based on `canReturnEarliestCommit`.\n    // Published commits: v10\n    // Ratified commits: V11\n    val catalogCommitFiles = Seq(stagedCommitFile(11))\n    val parsedLogData = catalogCommitFiles.map(ParsedCatalogCommitData.forFileStatus(_))\n    val fileList = Seq(\n      classicCheckpointFileStatus(10),\n      deltaFileStatus(10)) ++ catalogCommitFiles\n    val versionToICT = Map(10L -> 1000L, 11L -> 1100L)\n    val ictEnablementInfo = (5L, 500L) // (version, timestamp)\n\n    // If we have canReturnEarliestCommit=false should fail\n    // Querying after without canReturnLatestCommit results in error\n    val e = intercept[KernelException] {\n      checkGetActiveCommitAtTimestampWithParsedLogData(\n        fileList,\n        parsedLogData,\n        versionToICT,\n        timestampToQuery = 400,\n        expectedVersion = 0,\n        ictEnablementInfo = ictEnablementInfo)\n    }\n    assert(e.getMessage.contains(\"is before the earliest available version 10. Please use a \" +\n      \"timestamp greater than or equal to 1000 ms\"))\n\n    // Query with canReturnEarliestCommit=true should pass\n    checkGetActiveCommitAtTimestampWithParsedLogData(\n      fileList,\n      parsedLogData,\n      versionToICT,\n      timestampToQuery = 400,\n      expectedVersion = 10,\n      canReturnEarliestCommit = true,\n      ictEnablementInfo = ictEnablementInfo)\n  }\n\n  test(\"getActiveCommitAtTimestamp rejects non-ratified staged commits\") {\n    val fileList = Seq(\n      classicCheckpointFileStatus(0),\n      deltaFileStatus(0))\n\n    // Test 1: Inline commits (non-materialized) should be rejected\n    val mockColumnarBatch = new ColumnarBatch {\n      override def getSchema: StructType = null\n      override def getColumnVector(ordinal: Int): ColumnVector = null\n      override def getSize: Int = 1\n    }\n    val inlineCommit = ParsedCatalogCommitData.forInlineData(1L, mockColumnarBatch)\n    val inlineData = Seq(inlineCommit).asJava\n\n    assertThrows[IllegalArgumentException] {\n      // Args don't matter as validation should fail immediately\n      DeltaHistoryManager.getActiveCommitAtTimestamp(\n        createMockFSListFromEngine(fileList),\n        getMockSnapshot(dataPath, latestVersion = 0),\n        logPath,\n        10,\n        false,\n        false,\n        false,\n        inlineData)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/DeltaLogActionUtilsSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.io.FileNotFoundException\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.exceptions.{CommitRangeNotFoundException, InvalidTableException, KernelException, TableNotFoundException}\nimport io.delta.kernel.internal.DeltaLogActionUtils.{getCommitFilesForVersionRange, listDeltaLogFilesAsIter, verifyDeltaVersions}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.test.MockFileSystemClientUtils\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeltaLogActionUtilsSuite extends AnyFunSuite with MockFileSystemClientUtils {\n\n  ///////////////////////////////\n  // verifyDeltaVersions tests //\n  ///////////////////////////////\n\n  def getCommitFiles(versions: Seq[Long]): java.util.List[FileStatus] = {\n    versions\n      .map(v => FileStatus.of(FileNames.deltaFile(logPath, v), 0, 0))\n      .asJava\n  }\n\n  test(\"verifyDeltaVersions\") {\n    // Basic correct use case\n    verifyDeltaVersions(\n      getCommitFiles(Seq(1, 2, 3)),\n      1,\n      Optional.of(3),\n      dataPath)\n    // Only one version provided\n    verifyDeltaVersions(\n      getCommitFiles(Seq(1)),\n      1,\n      Optional.of(1),\n      dataPath)\n    // No end version provided\n    verifyDeltaVersions(\n      getCommitFiles(Seq(1, 2, 3, 4)),\n      1,\n      Optional.empty(),\n      dataPath)\n    // Non-contiguous versions\n    intercept[InvalidTableException] {\n      verifyDeltaVersions(\n        getCommitFiles(Seq(1, 3, 4)),\n        1,\n        Optional.of(4L),\n        dataPath)\n    }\n    // End-version or start-version not right\n    intercept[KernelException] {\n      verifyDeltaVersions(\n        getCommitFiles(Seq(1, 2, 3)),\n        0,\n        Optional.of(3L),\n        dataPath)\n    }\n    intercept[KernelException] {\n      verifyDeltaVersions(\n        getCommitFiles(Seq(1, 2, 3)),\n        1,\n        Optional.of(4L),\n        dataPath)\n    }\n    // Empty versions\n    intercept[KernelException] {\n      verifyDeltaVersions(\n        getCommitFiles(Seq()),\n        1,\n        Optional.of(4L),\n        dataPath)\n    }\n    // Unsorted or duplicates (shouldn't be possible)\n    intercept[InvalidTableException] {\n      verifyDeltaVersions(\n        getCommitFiles(Seq(1, 1, 2)),\n        1,\n        Optional.of(4L),\n        dataPath)\n    }\n    intercept[InvalidTableException] {\n      verifyDeltaVersions(\n        getCommitFiles(Seq(1, 4, 3, 2)),\n        1,\n        Optional.of(2L),\n        dataPath)\n    }\n  }\n\n  /////////////////////////////////////////\n  // getCommitFilesForVersionRange tests //\n  /////////////////////////////////////////\n\n  test(\"getCommitFilesForVersionRange: directory does not exist\") {\n    intercept[TableNotFoundException] {\n      getCommitFilesForVersionRange(\n        createMockFSListFromEngine(_ => throw new FileNotFoundException()),\n        dataPath,\n        0,\n        Optional.of(1L))\n    }\n  }\n\n  def testGetCommitFilesExpectedError[T <: Throwable](\n      testName: String,\n      files: Seq[FileStatus],\n      startVersion: Long = 1,\n      endVersion: Optional[java.lang.Long] = Optional.of(3L),\n      expectedErrorMessageContains: String)(implicit classTag: ClassTag[T]): Unit = {\n    test(\"getCommitFilesForVersionRange: \" + testName) {\n      val e = intercept[T] {\n        getCommitFilesForVersionRange(\n          createMockFSListFromEngine(files),\n          dataPath,\n          startVersion,\n          endVersion)\n      }\n      assert(e.getMessage.contains(expectedErrorMessageContains))\n    }\n  }\n\n  testGetCommitFilesExpectedError[CommitRangeNotFoundException](\n    testName = \"empty directory\",\n    files = Seq(),\n    expectedErrorMessageContains = \"no log files found in the requested version range\")\n\n  testGetCommitFilesExpectedError[CommitRangeNotFoundException](\n    testName = \"all versions less than startVersion\",\n    files = deltaFileStatuses(Seq(0)),\n    expectedErrorMessageContains = \"no log files found in the requested version range\")\n\n  testGetCommitFilesExpectedError[CommitRangeNotFoundException](\n    testName = \"all versions greater than endVersion\",\n    files = deltaFileStatuses(Seq(4, 5, 6)),\n    expectedErrorMessageContains = \"no log files found in the requested version range\")\n\n  testGetCommitFilesExpectedError[CommitRangeNotFoundException](\n    testName = \"all versions less than startVersion no endVersion\",\n    files = deltaFileStatuses(Seq(0)),\n    endVersion = Optional.empty(),\n    expectedErrorMessageContains = \"no log files found in the requested version range\")\n\n  testGetCommitFilesExpectedError[InvalidTableException](\n    testName = \"missing log files\",\n    files = deltaFileStatuses(Seq(1, 3)),\n    expectedErrorMessageContains = \"versions are not contiguous\")\n\n  testGetCommitFilesExpectedError[KernelException](\n    testName = \"start version not available\",\n    files = deltaFileStatuses(Seq(2, 3, 4, 5)),\n    expectedErrorMessageContains = \"no log file found for version 1\")\n\n  testGetCommitFilesExpectedError[KernelException](\n    testName = \"end version not available\",\n    files = deltaFileStatuses(Seq(0, 1, 2)),\n    expectedErrorMessageContains = \"no log file found for version 3\")\n\n  testGetCommitFilesExpectedError[KernelException](\n    testName = \"invalid start version\",\n    files = deltaFileStatuses(Seq(0, 1, 2)),\n    startVersion = -1,\n    expectedErrorMessageContains = \"Invalid version range\")\n\n  testGetCommitFilesExpectedError[KernelException](\n    testName = \"invalid end version\",\n    files = deltaFileStatuses(Seq(0, 1, 2)),\n    startVersion = 3,\n    endVersion = Optional.of(2L),\n    expectedErrorMessageContains = \"Invalid version range\")\n\n  def testGetCommitFiles(\n      testName: String,\n      files: Seq[FileStatus],\n      startVersion: Long = 1,\n      endVersion: Optional[java.lang.Long] = Optional.of(3L),\n      expectedCommitFiles: Seq[FileStatus]): Unit = {\n    test(\"getCommitFilesForVersionRange: \" + testName) {\n      assert(\n        getCommitFilesForVersionRange(\n          createMockFSListFromEngine(files),\n          dataPath,\n          startVersion,\n          endVersion).asScala sameElements expectedCommitFiles)\n    }\n  }\n\n  testGetCommitFiles(\n    testName = \"basic case\",\n    files = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5)),\n    expectedCommitFiles = deltaFileStatuses(Seq(1, 2, 3)))\n\n  testGetCommitFiles(\n    testName = \"basic case with checkpoint file\",\n    files = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5)) ++ singularCheckpointFileStatuses(Seq(2)),\n    expectedCommitFiles = deltaFileStatuses(Seq(1, 2, 3)))\n\n  testGetCommitFiles(\n    testName = \"basic case with non-log files\",\n    files = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5)) ++\n      deltaFileStatuses(Seq(2))\n        .map(fs => FileStatus.of(fs.getPath + \".crc\", fs.getSize, fs.getModificationTime)),\n    expectedCommitFiles = deltaFileStatuses(Seq(1, 2, 3)))\n\n  testGetCommitFiles(\n    testName = \"version range size 1\",\n    files = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5)),\n    startVersion = 0,\n    endVersion = Optional.of(0L),\n    expectedCommitFiles = deltaFileStatuses(Seq(0)))\n\n  testGetCommitFiles(\n    testName = \"no end version provided - should read to latest available\",\n    files = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5)),\n    endVersion = Optional.empty(),\n    expectedCommitFiles = deltaFileStatuses(Seq(1, 2, 3, 4, 5)))\n\n  testGetCommitFiles(\n    testName = \"no end version provided - single version from start\",\n    files = deltaFileStatuses(Seq(2)),\n    startVersion = 2,\n    endVersion = Optional.empty(),\n    expectedCommitFiles = deltaFileStatuses(Seq(2)))\n\n  /////////////////////////////\n  // listDeltaLogFiles tests //\n  /////////////////////////////\n\n  private val checkpointsAndDeltas = singularCheckpointFileStatuses(Seq(10)) ++\n    deltaFileStatuses(Seq(10, 11, 12, 13, 14)) ++\n    Seq(FileStatus.of(s\"$logPath/00000000000000000014.crc\", 0, 0)) ++\n    multiCheckpointFileStatuses(Seq(14), 2) ++\n    deltaFileStatuses(Seq(15, 16, 17)) ++\n    v2CheckpointFileStatuses(Seq((17, false, 2)), \"json\").map(_._1)\n\n  private def extractVersions(files: Seq[FileStatus]): Seq[Long] = {\n    files.map(fs => FileNames.getFileVersion(new Path(fs.getPath)))\n  }\n\n  test(\"listDeltaLogFiles: no fileTypes provided\") {\n    intercept[IllegalArgumentException] {\n      listDeltaLogFilesAsIter(\n        createMockFSListFromEngine(deltaFileStatuses(Seq(1, 2, 3))),\n        Collections.emptySet(), // No fileTypes provided!\n        dataPath,\n        1,\n        Optional.empty(),\n        false /* mustBeRecreatable */\n      ).toInMemoryList\n    }\n  }\n\n  test(\"listDeltaLogFiles: returns requested file type only\") {\n    val commitFiles = listDeltaLogFilesAsIter(\n      createMockFSListFromEngine(checkpointsAndDeltas),\n      Set(FileNames.DeltaLogFileType.COMMIT).asJava,\n      dataPath,\n      10,\n      Optional.empty(),\n      false /* mustBeRecreatable */\n    ).toInMemoryList.asScala\n\n    assert(commitFiles.forall(fs => FileNames.isCommitFile(fs.getPath)))\n    assert(extractVersions(commitFiles.toSeq) == Seq(10, 11, 12, 13, 14, 15, 16, 17))\n\n    val checkpointFiles = listDeltaLogFilesAsIter(\n      createMockFSListFromEngine(checkpointsAndDeltas),\n      Set(FileNames.DeltaLogFileType.CHECKPOINT).asJava,\n      dataPath,\n      10,\n      Optional.empty(),\n      false /* mustBeRecreatable */\n    ).toInMemoryList.asScala\n\n    assert(checkpointFiles.forall(fs => FileNames.isCheckpointFile(fs.getPath)))\n    assert(extractVersions(checkpointFiles.toSeq) == Seq(10, 14, 14, 17))\n  }\n\n  test(\"listDeltaLogFiles: mustBeRecreatable\") {\n    val exMsg = intercept[KernelException] {\n      listDeltaLogFilesAsIter(\n        createMockFSListFromEngine(checkpointsAndDeltas),\n        Set(FileNames.DeltaLogFileType.COMMIT, FileNames.DeltaLogFileType.CHECKPOINT).asJava,\n        dataPath,\n        0,\n        Optional.of(4),\n        true /* mustBeRecreatable */\n      ).toInMemoryList\n    }.getMessage\n    assert(exMsg.contains(\"Cannot load table version 4 as the transaction log has been \" +\n      \"truncated due to manual deletion or the log/checkpoint retention policy. The earliest \" +\n      \"available version is 10\"))\n  }\n\n  def testListWithCompactions(\n      testName: String,\n      files: Seq[FileStatus],\n      startVersion: Long,\n      endVersion: Optional[java.lang.Long],\n      expectedListedFiles: Seq[FileStatus]): Unit = {\n    test(\"testListWithCompactions: \" + testName) {\n      val listed = listDeltaLogFilesAsIter(\n        createMockFSListFromEngine(files),\n        Set(\n          FileNames.DeltaLogFileType.COMMIT,\n          FileNames.DeltaLogFileType.CHECKPOINT,\n          FileNames.DeltaLogFileType.LOG_COMPACTION).asJava,\n        dataPath,\n        startVersion,\n        endVersion,\n        false /* mustBeRecreatable */ ).toInMemoryList.asScala\n      assert(listed sameElements expectedListedFiles)\n    }\n  }\n\n  testListWithCompactions(\n    \"compaction at start, no endVersion\",\n    files = deltaFileStatuses(0L to 4L) ++ compactedFileStatuses(Seq((0, 4))),\n    startVersion = 0,\n    endVersion = Optional.empty(),\n    expectedListedFiles = compactedFileStatuses(Seq((0, 4))) ++ deltaFileStatuses(0L to 4L))\n\n  testListWithCompactions(\n    \"compaction at end, no endVersion\",\n    files = deltaFileStatuses(0L to 4L) ++ compactedFileStatuses(Seq((3, 4))),\n    startVersion = 0,\n    endVersion = Optional.empty(),\n    expectedListedFiles = deltaFileStatuses(0L to 2L) ++\n      compactedFileStatuses(Seq((3, 4))) ++\n      deltaFileStatuses(3L to 4L))\n\n  testListWithCompactions(\n    \"compaction at end, with endVersion\",\n    files = deltaFileStatuses(0L to 4L) ++ compactedFileStatuses(Seq((3, 4))),\n    startVersion = 0,\n    endVersion = Optional.of(4),\n    expectedListedFiles = deltaFileStatuses(0L to 2L) ++\n      compactedFileStatuses(Seq((3, 4))) ++\n      deltaFileStatuses(3L to 4L))\n\n  testListWithCompactions(\n    \"compaction in middle, no endVersion\",\n    files = deltaFileStatuses(0L to 4L) ++ compactedFileStatuses(Seq((2, 4))),\n    startVersion = 0,\n    endVersion = Optional.empty(),\n    expectedListedFiles = deltaFileStatuses(0L to 1L) ++\n      compactedFileStatuses(Seq((2, 4))) ++\n      deltaFileStatuses(2L to 4L))\n\n  testListWithCompactions(\n    \"compaction over end, with endVersion\",\n    files = deltaFileStatuses(0L to 6L) ++ compactedFileStatuses(Seq((3, 7))),\n    startVersion = 0,\n    endVersion = Optional.of(5),\n    expectedListedFiles = deltaFileStatuses(0L to 5L))\n\n  testListWithCompactions(\n    \"compaction before start, no endVersion\",\n    files = deltaFileStatuses(0L to 6L) ++ compactedFileStatuses(Seq((2, 4))),\n    startVersion = 3,\n    endVersion = Optional.empty(),\n    expectedListedFiles = deltaFileStatuses(3L to 6L))\n\n  testListWithCompactions(\n    \"compaction before start, with endVersion\",\n    files = deltaFileStatuses(0L to 6L) ++ compactedFileStatuses(Seq((2, 4))),\n    startVersion = 3,\n    endVersion = Optional.of(5),\n    expectedListedFiles = deltaFileStatuses(3L to 5L))\n\n  testListWithCompactions(\n    \"multiple compactions, no endVersion\",\n    files = deltaFileStatuses(0L to 7L) ++ compactedFileStatuses(Seq((2, 4), (5, 7))),\n    startVersion = 0,\n    endVersion = Optional.empty(),\n    expectedListedFiles = deltaFileStatuses(0L to 1L) ++\n      compactedFileStatuses(Seq((2, 4))) ++\n      deltaFileStatuses(2L to 4L) ++\n      compactedFileStatuses(Seq((5, 7))) ++\n      deltaFileStatuses(5L to 7L))\n\n  testListWithCompactions(\n    \"multiple compactions, with endVersion, don't return second compaction\",\n    files = deltaFileStatuses(0L to 7L) ++ compactedFileStatuses(Seq((2, 4), (5, 7))),\n    startVersion = 0,\n    endVersion = Optional.of(6),\n    expectedListedFiles = deltaFileStatuses(0L to 1L) ++\n      compactedFileStatuses(Seq((2, 4))) ++\n      deltaFileStatuses(2L to 4L) ++\n      deltaFileStatuses(5L to 6L))\n\n  testListWithCompactions(\n    \"multiple compactions, no endVersion, start after first compaction\",\n    files = deltaFileStatuses(0L to 7L) ++ compactedFileStatuses(Seq((2, 4), (5, 7))),\n    startVersion = 3,\n    endVersion = Optional.empty(),\n    expectedListedFiles = deltaFileStatuses(3L to 4L) ++\n      compactedFileStatuses(Seq((5, 7))) ++\n      deltaFileStatuses(5L to 7L))\n\n  testListWithCompactions(\n    \"multiple compactions, with endVersion, return no compactions\",\n    files = deltaFileStatuses(0L to 7L) ++ compactedFileStatuses(Seq((2, 4), (5, 7))),\n    startVersion = 3,\n    endVersion = Optional.of(6),\n    expectedListedFiles = deltaFileStatuses(3L to 6L))\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/FilteredColumnarBatchSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.util.Optional\n\nimport io.delta.kernel.TransactionSuite.columnarBatch\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.data.FilteredColumnarBatch\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types.{LongType, StructField, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.matchers.should.Matchers\n\nclass FilteredColumnarBatchSuite extends AnyFunSuite with VectorTestUtils with Matchers {\n\n  private val testSchema = new StructType().add(\"id\", LongType.LONG)\n\n  test(\"constructor should succeed when selectionVector is present and numSelectedRows is valid\") {\n    val data = columnarBatch(testSchema, Seq(longVector(Seq(0L, 1L, 2L, 3L, 4L))))\n    val selectionVector = Optional.of(booleanVector(Seq(true, false, true, false, true)))\n    val batch = new FilteredColumnarBatch(data, selectionVector, \"/test/path\", 3)\n\n    assert(batch.getFilePath == Optional.of(\"/test/path\"))\n    assert(batch.getPreComputedNumSelectedRows == Optional.of(3))\n    assert(batch.getData == data)\n    assert(batch.getSelectionVector == selectionVector)\n  }\n\n  test(\n    \"constructor should succeed when selectionVector is empty and numSelectedRows \" +\n      \"equals batch size\") {\n    val data = columnarBatch(testSchema, Seq(longVector(Seq(0L, 1L, 2L, 3L, 4L))))\n    val selectionVector = Optional.empty[ColumnVector]()\n    val batch = new FilteredColumnarBatch(data, selectionVector, \"/test/path\", 5)\n\n    assert(batch.getFilePath == Optional.of(\"/test/path\"))\n    assert(batch.getPreComputedNumSelectedRows == Optional.of(5))\n    assert(batch.getData == data)\n    assert(batch.getSelectionVector == selectionVector)\n  }\n\n  test(\"constructor should throw IllegalArgumentException \" +\n    \"when selectionVector is empty and numSelectedRows != batch size\") {\n    val data = columnarBatch(testSchema, Seq(longVector(Seq(0L, 1L, 2L, 3L, 4L))))\n    val selectionVector = Optional.empty[ColumnVector]()\n    val exMsg = intercept[IllegalArgumentException] {\n      new FilteredColumnarBatch(data, selectionVector, \"/test/path\", 3)\n    }.getMessage\n\n    assert(exMsg.contains(\"Invalid precomputedNumSelectedRows\"))\n    assert(exMsg.contains(\"must be equal to batch size when selectionVector is empty\"))\n  }\n\n  test(\"constructor should throw IllegalArgumentException \" +\n    \"when selectionVector is present and numSelectedRows > batch size\") {\n    val data = columnarBatch(testSchema, Seq(longVector(Seq(0L, 1L, 2L, 3L))))\n    val selectionVector = Optional.of(booleanVector(Seq(true, false, true, false)))\n\n    val exMsg = intercept[IllegalArgumentException] {\n      new FilteredColumnarBatch(data, selectionVector, \"/test/path\", 5)\n    }.getMessage\n\n    assert(exMsg.contains(\"Invalid precomputedNumSelectedRows\"))\n    assert(exMsg.contains(\"no larger than batch size\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/PageTokenSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.util\nimport java.util.{HashMap, Map}\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.internal.annotation.VisibleForTesting\nimport io.delta.kernel.internal.data.GenericRow\nimport io.delta.kernel.internal.replay.PageToken\nimport io.delta.kernel.test.MockFileSystemClientUtils\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PageTokenSuite extends AnyFunSuite with MockFileSystemClientUtils {\n\n  private val TEST_FILE_NAME = \"/path/to/table/test_file.json\"\n  private val TEST_ROW_INDEX = 42L\n  private val TEST_SIDECAR_INDEX = Optional.of(java.lang.Long.valueOf(5L))\n  private val TEST_KERNEL_VERSION = \"4.0.0\"\n  private val TEST_TABLE_PATH = \"/path/to/table\"\n  private val TEST_TABLE_VERSION = 5L\n  private val TEST_PREDICATE_HASH = 123\n  private val TEST_LOG_SEGMENT_HASH = 456\n\n  private val expectedPageToken = new PageToken(\n    TEST_FILE_NAME,\n    TEST_ROW_INDEX,\n    TEST_SIDECAR_INDEX,\n    TEST_KERNEL_VERSION,\n    TEST_TABLE_PATH,\n    TEST_TABLE_VERSION,\n    TEST_PREDICATE_HASH,\n    TEST_LOG_SEGMENT_HASH)\n\n  private val rowData: Map[Integer, Object] = new HashMap()\n  rowData.put(0, TEST_FILE_NAME)\n  rowData.put(1, TEST_ROW_INDEX.asInstanceOf[Object])\n  rowData.put(2, TEST_SIDECAR_INDEX.get())\n  rowData.put(3, TEST_KERNEL_VERSION)\n  rowData.put(4, TEST_TABLE_PATH)\n  rowData.put(5, TEST_TABLE_VERSION.asInstanceOf[Object])\n  rowData.put(6, TEST_PREDICATE_HASH.asInstanceOf[Object])\n  rowData.put(7, TEST_LOG_SEGMENT_HASH.asInstanceOf[Object])\n\n  val expectedRow = new GenericRow(PageToken.PAGE_TOKEN_SCHEMA, rowData)\n\n  test(\"PageToken.fromRow with valid data\") {\n    val pageToken = PageToken.fromRow(expectedRow)\n    assert(pageToken.equals(expectedPageToken))\n  }\n\n  test(\"PageToken.toRow with valid data\") {\n    val row = expectedPageToken.toRow\n    assert(row.getSchema.equals(PageToken.PAGE_TOKEN_SCHEMA))\n\n    assert(row.getString(0) == TEST_FILE_NAME)\n    assert(row.getLong(1) == TEST_ROW_INDEX)\n    assert(Optional.of(row.getLong(2)) == TEST_SIDECAR_INDEX)\n    assert(row.getString(3) == TEST_KERNEL_VERSION)\n    assert(row.getString(4) == TEST_TABLE_PATH)\n    assert(row.getLong(5) == TEST_TABLE_VERSION)\n    assert(row.getInt(6) == TEST_PREDICATE_HASH)\n    assert(row.getInt(7) == TEST_LOG_SEGMENT_HASH)\n  }\n\n  test(\"E2E: PageToken round-trip: toRow -> fromRow\") {\n    val row = expectedPageToken.toRow\n    val reconstructedPageToken = PageToken.fromRow(row)\n    assert(reconstructedPageToken.equals(expectedPageToken))\n  }\n\n  test(\"PageToken.fromRow throws exception when input row schema has invalid field name\") {\n    val invalidSchema = new StructType()\n      .add(\"wrongFieldName\", StringType.STRING)\n      .add(\"lastReturnedRowIndex\", LongType.LONG)\n      .add(\"lastReadSidecarFileIdx\", LongType.LONG)\n      .add(\"kernelVersion\", StringType.STRING)\n      .add(\"tablePath\", StringType.STRING)\n      .add(\"tableVersion\", LongType.LONG)\n      .add(\"predicateHash\", LongType.LONG)\n      .add(\"logSegmentHash\", LongType.LONG)\n\n    val invalidRowData: Map[Integer, Object] = new HashMap()\n    invalidRowData.put(0, TEST_FILE_NAME)\n    invalidRowData.put(1, TEST_ROW_INDEX.asInstanceOf[Object])\n    invalidRowData.put(2, TEST_SIDECAR_INDEX)\n    invalidRowData.put(3, TEST_KERNEL_VERSION)\n    invalidRowData.put(4, TEST_TABLE_PATH)\n    invalidRowData.put(5, TEST_TABLE_VERSION.asInstanceOf[Object])\n    invalidRowData.put(6, TEST_PREDICATE_HASH.asInstanceOf[Object])\n    invalidRowData.put(7, TEST_LOG_SEGMENT_HASH.asInstanceOf[Object])\n\n    val row = new GenericRow(invalidSchema, invalidRowData)\n    val exception = intercept[IllegalArgumentException] {\n      PageToken.fromRow(row)\n    }\n    assert(exception.getMessage.contains(\n      \"Invalid Page Token: input row schema does not match expected PageToken schema\"))\n  }\n\n  test(\"PageToken.fromRow throws exception when input row schema has wrong data type\") {\n    val invalidSchema = new StructType()\n      .add(\"lastReadLogFilePath\", StringType.STRING)\n      .add(\"lastReturnedRowIndex\", LongType.LONG)\n      .add(\"lastReadSidecarFileIdx\", StringType.STRING) // should be long type\n      .add(\"kernelVersion\", StringType.STRING)\n      .add(\"tablePath\", StringType.STRING)\n      .add(\"tableVersion\", LongType.LONG)\n      .add(\"predicateHash\", LongType.LONG)\n      .add(\"logSegmentHash\", LongType.LONG)\n\n    val invalidRowData: Map[Integer, Object] = new HashMap()\n    invalidRowData.put(0, TEST_FILE_NAME)\n    invalidRowData.put(1, TEST_ROW_INDEX.asInstanceOf[Object])\n    invalidRowData.put(2, TEST_SIDECAR_INDEX)\n    invalidRowData.put(3, TEST_KERNEL_VERSION)\n    invalidRowData.put(4, TEST_TABLE_PATH)\n    invalidRowData.put(5, TEST_TABLE_VERSION.asInstanceOf[Object])\n    invalidRowData.put(6, TEST_PREDICATE_HASH.asInstanceOf[Object])\n    invalidRowData.put(7, TEST_LOG_SEGMENT_HASH.asInstanceOf[Object])\n\n    val row = new GenericRow(invalidSchema, invalidRowData)\n    val exception = intercept[IllegalArgumentException] {\n      PageToken.fromRow(row)\n    }\n    assert(exception.getMessage.contains(\n      \"Invalid Page Token: input row schema does not match expected PageToken schema\"))\n  }\n\n  test(\"PageToken.fromRow accepts the case sidecar field is null\") {\n    val nullSidecarData: Map[Integer, Object] = new HashMap(rowData)\n    nullSidecarData.put(2, null)\n    val nullSidecarRow = new GenericRow(PageToken.PAGE_TOKEN_SCHEMA, nullSidecarData)\n    val pageToken = PageToken.fromRow(nullSidecarRow)\n    assert(pageToken.getLastReadSidecarFileIdx == Optional.empty())\n  }\n\n  test(\"PageToken.fromRow throws exception when required field is null\") {\n    val invalidData: Map[Integer, Object] = new HashMap(rowData)\n    invalidData.put(3, null)\n    val invalidRow = new GenericRow(PageToken.PAGE_TOKEN_SCHEMA, invalidData)\n    val exception = intercept[IllegalArgumentException] {\n      PageToken.fromRow(invalidRow)\n    }\n    assert(exception.getMessage.contains(\n      \"Invalid Page Token: required field\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/PaginationContextSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.util.Optional\n\nimport io.delta.kernel.Meta\nimport io.delta.kernel.internal.replay.{PageToken, PaginationContext}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PaginationContextSuite extends AnyFunSuite {\n\n  private val TEST_FILE_NAME = \"test_file.json\"\n  private val TEST_ROW_INDEX = 42L\n  private val TEST_SIDECAR_INDEX = Optional.of(java.lang.Long.valueOf(5L))\n  private val TEST_INVALID_KERNEL_VERSION = \"300.0.0\"\n  private val TEST_VALID_KERNEL_VERSION = Meta.KERNEL_VERSION\n  private val TEST_TABLE_PATH = \"/path/to/table\"\n  private val TEST_WRONG_TABLE_PATH = \"/wrong/path/to/table\"\n  private val TEST_TABLE_VERSION = 5L\n  private val TEST_WRONG_TABLE_VERSION = 5000L\n  private val TEST_PREDICATE_HASH = 123\n  private val TEST_WRONG_PREDICATE_HASH = 321\n  private val TEST_LOG_SEGMENT_HASH = 456\n  private val TEST_WRONG_LOG_SEGMENT_HASH = 654\n\n  private val validPageToken = new PageToken(\n    TEST_FILE_NAME,\n    TEST_ROW_INDEX,\n    TEST_SIDECAR_INDEX,\n    TEST_VALID_KERNEL_VERSION,\n    TEST_TABLE_PATH,\n    TEST_TABLE_VERSION,\n    TEST_PREDICATE_HASH,\n    TEST_LOG_SEGMENT_HASH)\n\n  private val invalidKernelVersionPageToken = new PageToken(\n    TEST_FILE_NAME,\n    TEST_ROW_INDEX,\n    TEST_SIDECAR_INDEX,\n    TEST_INVALID_KERNEL_VERSION,\n    TEST_TABLE_PATH,\n    TEST_TABLE_VERSION,\n    TEST_PREDICATE_HASH,\n    TEST_LOG_SEGMENT_HASH)\n\n  test(\"forFirstPage should create context with empty optionals and specified page size\") {\n    val pageSize = 100L\n    val context = PaginationContext.forFirstPage(\n      TEST_TABLE_PATH,\n      TEST_TABLE_VERSION,\n      TEST_LOG_SEGMENT_HASH,\n      TEST_PREDICATE_HASH,\n      pageSize)\n\n    assert(!context.getLastReadLogFilePath().isPresent)\n    assert(!context.getLastReturnedRowIndex().isPresent)\n    assert(!context.getLastReadSidecarFileIdx().isPresent)\n    assert(context.getPageSize() === pageSize)\n  }\n\n  test(\"forPageWithPageToken should create context with provided values\") {\n    val pageSize = 50L\n    val context = PaginationContext.forPageWithPageToken(\n      TEST_TABLE_PATH,\n      TEST_TABLE_VERSION,\n      TEST_LOG_SEGMENT_HASH,\n      TEST_PREDICATE_HASH,\n      pageSize,\n      validPageToken)\n\n    assert(context.getLastReadLogFilePath() === Optional.of(TEST_FILE_NAME))\n    assert(context.getLastReturnedRowIndex() === Optional.of(TEST_ROW_INDEX))\n    assert(context.getLastReadSidecarFileIdx() === TEST_SIDECAR_INDEX)\n    assert(context.getPageSize() === pageSize)\n  }\n\n  test(\"forPageWithPageToken should throw exception when page token is null\") {\n    val pageSize = 50L\n\n    val e = intercept[NullPointerException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        pageSize,\n        null /* page token */ )\n    }\n    assert(e.getMessage === \"page token is null\")\n  }\n\n  test(\"should throw exception for zero page size\") {\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forFirstPage(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        0L)\n    }\n    assert(e.getMessage === \"Page size must be greater than zero!\")\n  }\n\n  test(\"should throw exception for negative page size\") {\n    val negativePageSize = -10L\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forFirstPage(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        negativePageSize)\n    }\n    assert(e.getMessage === \"Page size must be greater than zero!\")\n  }\n\n  test(\"should throw exception for negative page size with page token\") {\n    val negativePageSize = -5L\n\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        negativePageSize,\n        validPageToken)\n    }\n    assert(e.getMessage === \"Page size must be greater than zero!\")\n  }\n\n  test(\"should throw exception when the requested kernel version doesn't \" +\n    \"match the value in page token\") {\n    val pageSize = 50L\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        pageSize,\n        invalidKernelVersionPageToken)\n    }\n    assert(e.getMessage.contains(\"Invalid page token: token kernel version\"))\n  }\n\n  test(\"should throw exception for when the requested table path doesn't \" +\n    \"match the value in page token\") {\n    val pageSize = 50L\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_WRONG_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        pageSize,\n        validPageToken)\n    }\n    assert(e.getMessage.contains(\"Invalid page token: token table path\"))\n  }\n\n  test(\"should throw exception for when the requested table version doesn't \" +\n    \"match the value in page token\") {\n    val pageSize = 50L\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_TABLE_PATH,\n        TEST_WRONG_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        pageSize,\n        validPageToken)\n    }\n    assert(e.getMessage.contains(\"Invalid page token: token table version\"))\n  }\n\n  test(\"should throw exception for when the requested predicate doesn't \" +\n    \"match the value in page token\") {\n    val pageSize = 50L\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_LOG_SEGMENT_HASH,\n        TEST_WRONG_PREDICATE_HASH,\n        pageSize,\n        validPageToken)\n    }\n    assert(e.getMessage.contains(\"Invalid page token: token predicate\"))\n  }\n\n  test(\"should throw exception for when the requested log segment doesn't \" +\n    \"match the value in page token\") {\n    val pageSize = 50L\n    val e = intercept[IllegalArgumentException] {\n      PaginationContext.forPageWithPageToken(\n        TEST_TABLE_PATH,\n        TEST_TABLE_VERSION,\n        TEST_WRONG_LOG_SEGMENT_HASH,\n        TEST_PREDICATE_HASH,\n        pageSize,\n        validPageToken)\n    }\n    assert(e.getMessage.contains(\"Invalid page token: token log segment\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/SnapshotManagerSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.lang.{Long => JLong}\nimport java.util.{Arrays, Collections, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.engine.FileReadResult\nimport io.delta.kernel.exceptions.{InvalidTableException, TableNotFoundException}\nimport io.delta.kernel.expressions.Predicate\nimport io.delta.kernel.internal.checkpoints.{CheckpointInstance, CheckpointMetaData, SidecarFile}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.snapshot.{LogSegment, SnapshotManager}\nimport io.delta.kernel.internal.util.{FileNames, Utils}\nimport io.delta.kernel.test.{BaseMockJsonHandler, BaseMockParquetHandler, MockFileSystemClientUtils, MockListFromFileSystemClient, VectorTestUtils}\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass SnapshotManagerSuite extends AnyFunSuite with MockFileSystemClientUtils {\n\n  test(\"verifyDeltaVersionsContiguous\") {\n    val path = new Path(\"/path/to/table\")\n    // empty array\n    SnapshotManager.verifyDeltaVersionsContiguous(Collections.emptyList(), path)\n    // array of size 1\n    SnapshotManager.verifyDeltaVersionsContiguous(Collections.singletonList(1), path)\n    // contiguous versions\n    SnapshotManager.verifyDeltaVersionsContiguous(Arrays.asList(1, 2, 3), path)\n    // non-contiguous versions\n    intercept[InvalidTableException] {\n      SnapshotManager.verifyDeltaVersionsContiguous(Arrays.asList(1, 3), path)\n    }\n    // duplicates in versions\n    intercept[InvalidTableException] {\n      SnapshotManager.verifyDeltaVersionsContiguous(Arrays.asList(1, 2, 2, 3), path)\n    }\n    // unsorted versions\n    intercept[InvalidTableException] {\n      SnapshotManager.verifyDeltaVersionsContiguous(Arrays.asList(3, 2, 1), path)\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // getLogSegmentForVersion tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  private val snapshotManager = new SnapshotManager(dataPath)\n\n  /* ------------------HELPER METHODS------------------ */\n\n  private def checkLogSegment(\n      logSegment: LogSegment,\n      expectedVersion: Long,\n      expectedDeltas: Seq[FileStatus],\n      expectedCompactions: Seq[FileStatus],\n      expectedCheckpoints: Seq[FileStatus],\n      expectedCheckpointVersion: Option[Long],\n      expectedLastCommitTimestamp: Long): Unit = {\n\n    assert(logSegment.getLogPath == logPath)\n    assert(logSegment.getVersion == expectedVersion)\n    assert(expectedDeltas.map(f => (f.getPath, f.getSize, f.getModificationTime)) sameElements\n      logSegment.getDeltas.asScala.map(f => (f.getPath, f.getSize, f.getModificationTime)))\n    assert(expectedCompactions.map(f => (f.getPath, f.getSize, f.getModificationTime)) sameElements\n      logSegment.getCompactions.asScala.map(f => (f.getPath, f.getSize, f.getModificationTime)))\n\n    val expectedCheckpointStatuses = expectedCheckpoints\n      .map(f => (f.getPath, f.getSize, f.getModificationTime)).sortBy(_._1)\n    val actualCheckpointStatuses = logSegment.getCheckpoints.asScala\n      .map(f => (f.getPath, f.getSize, f.getModificationTime)).sortBy(_._1)\n    assert(\n      expectedCheckpointStatuses sameElements actualCheckpointStatuses,\n      s\"expected:\\n$expectedCheckpointStatuses\\nactual:\\n$actualCheckpointStatuses\")\n\n    expectedCheckpointVersion match {\n      case Some(v) =>\n        assert(logSegment.getCheckpointVersionOpt.isPresent() &&\n          logSegment.getCheckpointVersionOpt.get == v)\n      case None => assert(!logSegment.getCheckpointVersionOpt.isPresent())\n    }\n    assert(expectedLastCommitTimestamp == logSegment.getDeltaFileAtEndVersion.getModificationTime)\n  }\n\n  /**\n   * Test `getLogSegmentForVersion` for a given set of delta versions, singular checkpoint versions,\n   * and multi-part checkpoint versions with a given _last_checkpoint starting checkpoint and\n   * a versionToLoad.\n   *\n   * @param deltaVersions versions of the delta JSON files in the delta log\n   * @param checkpointVersions version of the singular checkpoint parquet files in the delta log\n   * @param multiCheckpointVersions versions of the multi-part checkpoint files in the delta log\n   * @param numParts number of parts for the multi-part checkpoints if applicable\n   * @param startCheckpoint starting checkpoint to list from, in practice provided by the\n   *                        _last_checkpoint file; if not provided list from 0\n   * @param versionToLoad specific version to load; if not provided load the latest\n   * @param v2CheckpointSpec Versions of V2 checkpoints to be included along with the number of\n   *                         sidecars in each checkpoint and the naming scheme for each checkpoint.\n   */\n  def testWithCheckpoints(\n      deltaVersions: Seq[Long],\n      checkpointVersions: Seq[Long],\n      multiCheckpointVersions: Seq[Long],\n      numParts: Int = -1,\n      startCheckpoint: Optional[java.lang.Long] = Optional.empty(),\n      versionToLoad: Optional[java.lang.Long] = Optional.empty(),\n      v2CheckpointSpec: Seq[(Long, Boolean, Int)] = Seq.empty,\n      compactionVersions: Seq[(Long, Long)] = Seq.empty): Unit = {\n    val deltas = deltaFileStatuses(deltaVersions)\n    val singularCheckpoints = singularCheckpointFileStatuses(checkpointVersions)\n    val multiCheckpoints = multiCheckpointFileStatuses(multiCheckpointVersions, numParts)\n\n    // Only test both filetypes if we have to read the checkpoint top-level file.\n    val topLevelFileTypes = if (v2CheckpointSpec.nonEmpty) {\n      Seq(\"parquet\", \"json\")\n    } else {\n      Seq(\"parquet\")\n    }\n    topLevelFileTypes.foreach { topLevelFileType =>\n      val v2Checkpoints =\n        v2CheckpointFileStatuses(v2CheckpointSpec, topLevelFileType)\n\n      val checkpointFiles = v2Checkpoints.flatMap {\n        case (topLevelCheckpointFile, sidecars) =>\n          Seq(topLevelCheckpointFile) ++ sidecars\n      } ++ singularCheckpoints ++ multiCheckpoints\n\n      val expectedCheckpointVersion = (checkpointVersions ++ multiCheckpointVersions ++\n        v2CheckpointSpec.map(_._1))\n        .filter(_ <= versionToLoad.orElse(Long.MaxValue))\n        .sorted\n        .lastOption\n\n      val (expectedV2Checkpoint, expectedSidecars) = expectedCheckpointVersion.map { v =>\n        val matchingCheckpoints = v2Checkpoints.filter { case (topLevelFile, _) =>\n          FileNames.checkpointVersion(topLevelFile.getPath) == v\n        }\n        if (matchingCheckpoints.nonEmpty) {\n          matchingCheckpoints.maxBy(f => new CheckpointInstance(f._1.getPath)) match {\n            case (c, sidecars) => (Seq(c), sidecars)\n          }\n        } else {\n          (Seq.empty, Seq.empty)\n        }\n      }.getOrElse((Seq.empty, Seq.empty))\n\n      val compactions = compactedFileStatuses(compactionVersions)\n      val mockSidecarParquetHandler = if (expectedSidecars.nonEmpty) {\n        new MockSidecarParquetHandler(expectedSidecars, expectedV2Checkpoint.head.getPath)\n      } else {\n        new BaseMockParquetHandler {}\n      }\n      val logSegment = snapshotManager.getLogSegmentForVersion(\n        createMockFSListFromEngine(\n          deltas ++ compactions ++ checkpointFiles,\n          mockSidecarParquetHandler,\n          new MockSidecarJsonHandler(expectedSidecars)),\n        versionToLoad)\n\n      val expectedDeltas = deltaFileStatuses(\n        deltaVersions.filter { v =>\n          v > expectedCheckpointVersion.getOrElse(-1L) && v <= versionToLoad.orElse(Long.MaxValue)\n        })\n      val expectedCheckpoints = expectedCheckpointVersion.map { v =>\n        if (expectedV2Checkpoint.nonEmpty) {\n          expectedV2Checkpoint\n        } else if (checkpointVersions.toSet.contains(v)) {\n          singularCheckpointFileStatuses(Seq(v))\n        } else {\n          multiCheckpointFileStatuses(Seq(v), numParts)\n        }\n      }.getOrElse(Seq.empty)\n      val expectedCompactions = compactedFileStatuses(\n        compactionVersions.filter { case (s, e) =>\n          // we can only use a compaction if it starts after the checkpoint and ends at or before\n          // the version we're trying to load\n          s > expectedCheckpointVersion.getOrElse(-1L) && e <= versionToLoad.orElse(Long.MaxValue)\n        })\n\n      checkLogSegment(\n        logSegment,\n        expectedVersion = versionToLoad.orElse(deltaVersions.max),\n        expectedDeltas = expectedDeltas,\n        expectedCompactions = expectedCompactions,\n        expectedCheckpoints = expectedCheckpoints,\n        expectedCheckpointVersion = expectedCheckpointVersion,\n        expectedLastCommitTimestamp = versionToLoad.orElse(deltaVersions.max) * 10)\n    }\n  }\n\n  /** Simple test for a log with only JSON files and no checkpoints */\n  def testNoCheckpoint(\n      deltaVersions: Seq[Long],\n      versionToLoad: Optional[java.lang.Long] = Optional.empty()): Unit = {\n    testWithCheckpoints(\n      deltaVersions,\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = versionToLoad)\n  }\n\n  /** Simple test with only json and compactions */\n  def testWithCompactionsNoCheckpoint(\n      deltaVersions: Seq[Long],\n      compactionVersions: Seq[(Long, Long)],\n      versionToLoad: Optional[java.lang.Long] = Optional.empty()): Unit = {\n    testWithCheckpoints(\n      deltaVersions,\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = versionToLoad,\n      compactionVersions = compactionVersions)\n  }\n\n  /**\n   * Test `getLogSegmentForVersion` for a set of delta versions and checkpoint versions. Tests\n   * with (1) singular checkpoint (2) multi-part checkpoints with 5 parts\n   * (3) multi-part checkpoints with 1 part\n   */\n  def testWithSingularAndMultipartCheckpoint(\n      deltaVersions: Seq[Long],\n      checkpointVersions: Seq[Long],\n      startCheckpoint: Optional[java.lang.Long] = Optional.empty(),\n      versionToLoad: Optional[java.lang.Long] = Optional.empty()): Unit = {\n\n    // test with singular checkpoint\n    testWithCheckpoints(\n      deltaVersions = deltaVersions,\n      checkpointVersions = checkpointVersions,\n      multiCheckpointVersions = Seq.empty,\n      startCheckpoint = startCheckpoint,\n      versionToLoad = versionToLoad)\n\n    // test with multi-part checkpoint  numParts=5\n    testWithCheckpoints(\n      deltaVersions = deltaVersions,\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = checkpointVersions,\n      numParts = 5,\n      startCheckpoint = startCheckpoint,\n      versionToLoad = versionToLoad)\n\n    // test with multi-part checkpoint numParts=1\n    testWithCheckpoints(\n      deltaVersions = deltaVersions,\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = checkpointVersions,\n      numParts = 1,\n      startCheckpoint = startCheckpoint,\n      versionToLoad = versionToLoad)\n  }\n\n  /**\n   * For a given set of _delta_log files check for error.\n   */\n  def testExpectedError[T <: Throwable](\n      files: Seq[FileStatus],\n      lastCheckpointVersion: Optional[java.lang.Long] = Optional.empty(),\n      versionToLoad: Optional[java.lang.Long] = Optional.empty(),\n      expectedErrorMessageContains: String = \"\")(implicit classTag: ClassTag[T]): Unit = {\n    val e = intercept[T] {\n      snapshotManager.getLogSegmentForVersion(\n        createMockFSAndJsonEngineForLastCheckpoint(files, lastCheckpointVersion),\n        versionToLoad)\n    }\n    assert(e.getMessage.contains(expectedErrorMessageContains))\n  }\n\n  /* ------------------- VALID DELTA LOG FILE LISTINGS ----------------------- */\n\n  test(\"getLogSegmentForVersion: 000.json only\") {\n    testNoCheckpoint(Seq(0))\n    testNoCheckpoint(Seq(0), Optional.of(0))\n  }\n\n  test(\"getLogSegmentForVersion: 000.json .. 009.json\") {\n    testNoCheckpoint(0L until 10L)\n    testNoCheckpoint(0L until 10L, Optional.of(9))\n    testNoCheckpoint(0L until 10L, Optional.of(5))\n  }\n\n  test(\"getLogSegmentForVersion: 000.json..010.json + checkpoint(10)\") {\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 10L),\n      checkpointVersions = Seq(10))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 10L),\n      checkpointVersions = Seq(10),\n      startCheckpoint = Optional.of(10))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 10L),\n      checkpointVersions = Seq(10),\n      versionToLoad = Optional.of(10))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 10L),\n      checkpointVersions = Seq(10),\n      startCheckpoint = Optional.of(10),\n      versionToLoad = Optional.of(10))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 10L),\n      checkpointVersions = Seq(10),\n      versionToLoad = Optional.of(6))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 10L),\n      checkpointVersions = Seq(10),\n      startCheckpoint = Optional.of(10),\n      versionToLoad = Optional.of(6))\n  }\n\n  test(\"getLogSegmentForVersion: 000.json...20.json + checkpoint(10) + checkpoint(20)\") {\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 20L),\n      checkpointVersions = Seq(10, 20))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 20L),\n      checkpointVersions = Seq(10, 20),\n      startCheckpoint = Optional.of(20))\n    // _last_checkpoint hasn't been updated yet\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 20L),\n      checkpointVersions = Seq(10, 20),\n      startCheckpoint = Optional.of(10))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 20L),\n      checkpointVersions = Seq(10, 20),\n      versionToLoad = Optional.of(15))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 20L),\n      checkpointVersions = Seq(10, 20),\n      startCheckpoint = Optional.of(10),\n      versionToLoad = Optional.of(15))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (0L to 20L),\n      checkpointVersions = Seq(10, 20),\n      startCheckpoint = Optional.of(20),\n      versionToLoad = Optional.of(15))\n  }\n\n  test(\"getLogSegmentForVersion: outdated _last_checkpoint that does not exist\") {\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (20L until 25L),\n      checkpointVersions = Seq(20),\n      startCheckpoint = Optional.of(10))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (20L until 25L),\n      checkpointVersions = Seq(20),\n      startCheckpoint = Optional.of(10),\n      versionToLoad = Optional.of(20))\n  }\n\n  test(\"getLogSegmentForVersion: 20.json...25.json + checkpoint(20)\") {\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (20L to 25L),\n      checkpointVersions = Seq(20))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (20L to 25L),\n      checkpointVersions = Seq(20),\n      startCheckpoint = Optional.of(20))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = (20L to 25L),\n      checkpointVersions = Seq(20),\n      versionToLoad = Optional.of(23))\n  }\n\n  test(\"getLogSegmentForVersion: empty delta log\") {\n    val exMsg = intercept[TableNotFoundException] {\n      snapshotManager.getLogSegmentForVersion(\n        createMockFSListFromEngine(Seq.empty),\n        Optional.empty() /* versionToLoad */\n      )\n    }.getMessage\n\n    assert(exMsg.contains(\"No delta files found in the directory\"))\n  }\n\n  test(\"getLogSegmentForVersion: no delta files in the delta log\") {\n    // listDeltaAndCheckpointFiles = Optional.of(EmptyList)\n    val files = Seq(\"foo\", \"notdelta.parquet\", \"foo.json\", \"001.checkpoint.00f.oo0.parquet\")\n      .map(FileStatus.of(_, 10, 10))\n    testExpectedError[TableNotFoundException](\n      files,\n      expectedErrorMessageContains =\n        \"No delta files found in the directory: /fake/path/to/table/_delta_log\")\n    testExpectedError[TableNotFoundException](\n      files,\n      versionToLoad = Optional.of(5),\n      expectedErrorMessageContains =\n        \"No delta files found in the directory: /fake/path/to/table/_delta_log\")\n  }\n\n  test(\"getLogSegmentForVersion: versionToLoad higher than possible\") {\n    testExpectedError[RuntimeException](\n      files = deltaFileStatuses(Seq(0L)),\n      versionToLoad = Optional.of(15),\n      expectedErrorMessageContains =\n        \"Cannot load table version 15 as it does not exist. The latest available version is 0\")\n    testExpectedError[RuntimeException](\n      files = deltaFileStatuses((10L until 13L)) ++ singularCheckpointFileStatuses(Seq(10L)),\n      versionToLoad = Optional.of(15),\n      expectedErrorMessageContains =\n        \"Cannot load table version 15 as it does not exist. The latest available version is 12\")\n  }\n\n  test(\"getLogSegmentForVersion: start listing from _last_checkpoint when it is provided\") {\n    val deltas = deltaFileStatuses(0L to 24)\n    val checkpoints = singularCheckpointFileStatuses(Seq(10, 20))\n\n    for (lastCheckpointVersion <- Seq(10, 20)) {\n      val lastCheckpointFileStatus = FileStatus.of(s\"$logPath/_last_checkpoint\", 2, 2)\n      val files = deltas ++ checkpoints ++ Seq(lastCheckpointFileStatus)\n\n      def listFrom(filePath: String): Seq[FileStatus] = {\n        if (filePath < FileNames.listingPrefix(logPath, lastCheckpointVersion)) {\n          throw new RuntimeException(\n            s\"Listing from before the checkpoint version referenced by _last_checkpoint. \" +\n              s\"Last checkpoint version: $lastCheckpointVersion. Listing from: $filePath\")\n        }\n        listFromProvider(files)(filePath)\n      }\n\n      val logSegment = snapshotManager.getLogSegmentForVersion(\n        mockEngine(\n          jsonHandler = new MockReadLastCheckpointFileJsonHandler(\n            lastCheckpointFileStatus.getPath,\n            lastCheckpointVersion),\n          fileSystemClient = new MockListFromFileSystemClient(listFrom)),\n        Optional.empty() /* versionToLoad */\n      )\n\n      checkLogSegment(\n        logSegment,\n        expectedVersion = 24,\n        expectedDeltas = deltaFileStatuses(21L until 25L),\n        expectedCompactions = Seq.empty,\n        expectedCheckpoints = singularCheckpointFileStatuses(Seq(20L)),\n        expectedCheckpointVersion = Some(20),\n        expectedLastCommitTimestamp = 240L)\n    }\n  }\n\n  test(\"getLogSegmentForVersion: multi-part and single-part checkpoints in same log\") {\n    testWithCheckpoints(\n      (0L to 50L),\n      Seq(10, 30, 50),\n      Seq(20, 40),\n      numParts = 5)\n    testWithCheckpoints(\n      (0L to 50L),\n      Seq(10, 30, 50),\n      Seq(20, 40),\n      numParts = 5,\n      startCheckpoint = Optional.of(40))\n  }\n\n  test(\"getLogSegmentForVersion: versionToLoad not constructable from history\") {\n    testExpectedError[RuntimeException](\n      deltaFileStatuses(20L until 25L) ++ singularCheckpointFileStatuses(Seq(20L)),\n      versionToLoad = Optional.of(15),\n      expectedErrorMessageContains = \"Cannot load table version 15\")\n  }\n\n  /* ------------------- V2 CHECKPOINT TESTS ------------------ */\n  test(\"v2 checkpoint exists at version\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(5L),\n      v2CheckpointSpec = Seq((0L, true, 2), (5L, true, 2)))\n  }\n\n  test(\"multiple v2 checkpoint exist at version\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(5L),\n      v2CheckpointSpec = Seq((5L, true, 2), (5L, true, 2)))\n  }\n\n  test(\"v2 checkpoint exists before version\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 7L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(6L),\n      v2CheckpointSpec = Seq((0L, true, 2), (5L, true, 2)))\n  }\n\n  test(\"v1 and v2 checkpoints in table\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 12L),\n      checkpointVersions = Seq(0L, 10L),\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(8L),\n      v2CheckpointSpec = Seq((5L, true, 2)))\n    testWithCheckpoints(\n      (0L to 12L),\n      checkpointVersions = Seq(0L, 10L),\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(12L),\n      v2CheckpointSpec = Seq((5L, true, 2)))\n  }\n\n  test(\"multipart and v2 checkpoints in table\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 12L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq(0L, 10L),\n      numParts = 5,\n      versionToLoad = Optional.of(8L),\n      v2CheckpointSpec = Seq((5L, true, 2)))\n    testWithCheckpoints(\n      deltaVersions = (0L to 12L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq(0L, 10L),\n      numParts = 5,\n      versionToLoad = Optional.of(12L),\n      v2CheckpointSpec = Seq((5L, true, 2)))\n  }\n\n  test(\"no checkpoint prior to version\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(3L),\n      v2CheckpointSpec = Seq((5L, true, 2)))\n  }\n\n  test(\"read from compatibility checkpoint\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(5L),\n      v2CheckpointSpec = Seq((5L, false, 5)))\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(5L),\n      v2CheckpointSpec = Seq((0L, true, 5), (5L, false, 5)))\n  }\n\n  test(\"read from V2 checkpoint with compatibility checkpoint at same version\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(5L),\n      v2CheckpointSpec = Seq((5L, true, 5), (5L, false, 5)))\n  }\n\n  test(\"read from V2 checkpoint with compatibility checkpoint at previous version\") {\n    testWithCheckpoints(\n      deltaVersions = (0L to 5L),\n      checkpointVersions = Seq.empty,\n      multiCheckpointVersions = Seq.empty,\n      versionToLoad = Optional.of(5L),\n      v2CheckpointSpec = Seq((3L, false, 5), (5L, true, 5)))\n  }\n\n  /* ------------------- CORRUPT DELTA LOG FILE LISTINGS ------------------ */\n\n  test(\"getLogSegmentForVersion: corrupt listing with only checkpoint file\") {\n    Seq(Optional.empty(), Optional.of(10L)).foreach { lastCheckpointVersion =>\n      Seq(Optional.empty(), Optional.of(10L)).foreach { versionToLoad =>\n        testExpectedError[InvalidTableException](\n          files = singularCheckpointFileStatuses(Seq(10L)),\n          lastCheckpointVersion.map(Long.box),\n          versionToLoad.map(Long.box),\n          expectedErrorMessageContains = \"Missing delta file for version 10\")\n      }\n    }\n  }\n\n  test(\"getLogSegmentForVersion: corrupt listing with missing log files\") {\n    // checkpoint(10), 010.json, 011.json, 013.json\n    val fileList = deltaFileStatuses(Seq(10L, 11L)) ++ deltaFileStatuses(Seq(13L)) ++\n      singularCheckpointFileStatuses(Seq(10L))\n    Seq(Optional.empty(), Optional.of(10L)).foreach { lastCheckpointVersion =>\n      Seq(Optional.empty(), Optional.of(13L)).foreach { versionToLoad =>\n        testExpectedError[InvalidTableException](\n          fileList,\n          lastCheckpointVersion.map(Long.box),\n          versionToLoad.map(Long.box),\n          expectedErrorMessageContains = \"versions are not contiguous: ([11, 13])\")\n      }\n    }\n  }\n\n  test(\"getLogSegmentForVersion: corrupt listing 000.json...009.json + checkpoint(10)\") {\n    val fileList = deltaFileStatuses((0L until 10L)) ++ singularCheckpointFileStatuses(Seq(10L))\n    Seq(Optional.empty(), Optional.of(10L)).foreach { lastCheckpointVersion =>\n      Seq(Optional.empty(), Optional.of(15L)).foreach { versionToLoad =>\n        testExpectedError[InvalidTableException](\n          fileList,\n          lastCheckpointVersion.map(Long.box),\n          versionToLoad.map(Long.box),\n          expectedErrorMessageContains = \"Missing delta file for version 10\")\n      }\n    }\n  }\n\n  test(\"getLogSegmentForVersion: corrupt listing: checkpoint(10); 11 to 14.json; no 10.json\") {\n    val fileList = singularCheckpointFileStatuses(Seq(10L)) ++ deltaFileStatuses((11L until 15L))\n    Seq(Optional.empty(), Optional.of(10L)).foreach { lastCheckpointVersion =>\n      Seq(Optional.empty(), Optional.of(10L)).foreach { versionToLoad =>\n        testExpectedError[InvalidTableException](\n          fileList,\n          lastCheckpointVersion.map(Long.box),\n          versionToLoad.map(Long.box),\n          expectedErrorMessageContains = \"Missing delta file for version 10\")\n      }\n    }\n  }\n\n  test(\"getLogSegmentForVersion: corrupted log missing json files / no way to construct history\") {\n    testExpectedError[InvalidTableException](\n      deltaFileStatuses(1L until 10L),\n      expectedErrorMessageContains = \"Cannot compute snapshot. Missing delta file version 0.\")\n    testExpectedError[InvalidTableException](\n      deltaFileStatuses(15L until 25L) ++ singularCheckpointFileStatuses(Seq(20L)),\n      versionToLoad = Optional.of(17),\n      expectedErrorMessageContains = \"Cannot compute snapshot. Missing delta file version 0.\")\n    testExpectedError[InvalidTableException](\n      deltaFileStatuses((0L until 5L) ++ (6L until 9L)),\n      expectedErrorMessageContains = \"are not contiguous\")\n    // corrupt incomplete multi-part checkpoint\n    val corruptedCheckpointStatuses = FileNames.checkpointFileWithParts(logPath, 10, 5).asScala\n      .map(p => FileStatus.of(p.toString, 10, 10))\n      .take(4)\n    val deltas = deltaFileStatuses(10L to 13L)\n    testExpectedError[InvalidTableException](\n      corruptedCheckpointStatuses.toSeq ++ deltas,\n      expectedErrorMessageContains = \"Cannot compute snapshot. Missing delta file version 0.\")\n  }\n\n  test(\"getLogSegmentForVersion: corrupt log but reading outside corrupted range\") {\n    testNoCheckpoint(\n      deltaVersions = (0L until 5L) ++ (6L until 9L),\n      versionToLoad = Optional.of(4))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = 15L until 25L,\n      checkpointVersions = Seq(20),\n      versionToLoad = Optional.of(22))\n    testWithSingularAndMultipartCheckpoint(\n      deltaVersions = 15L until 25L,\n      checkpointVersions = Seq(20),\n      startCheckpoint = Optional.of(20),\n      versionToLoad = Optional.of(22))\n  }\n\n  test(\"getLogSegmentForVersion: corrupt _last_checkpoint (is after existing versions)\") {\n    // in the case of a corrupted _last_checkpoint we revert to listing from version 0\n    // (on first run newFiles.isEmpty() but since startingCheckpointOpt.isPresent() re-list from 0)\n    testWithSingularAndMultipartCheckpoint(\n      (0L until 25L),\n      Seq(10L, 20L),\n      startCheckpoint = Optional.of(30))\n  }\n\n  test(\"getLogSegmentForVersion: corrupt _last_checkpoint refers to in range version \" +\n    \"but no valid checkpoint\") {\n    // _last_checkpoint refers to a v1 checkpoint at version 20 that is missing\n    testExpectedError[RuntimeException](\n      deltaFileStatuses(0L until 25L) ++ singularCheckpointFileStatuses(Seq(10L)),\n      lastCheckpointVersion = Optional.of(20),\n      expectedErrorMessageContains = \"Missing checkpoint at version 20\")\n    // _last_checkpoint refers to incomplete multi-part checkpoint at version 20 that is missing\n    val corruptedCheckpointStatuses = FileNames.checkpointFileWithParts(logPath, 20, 5).asScala\n      .map(p => FileStatus.of(p.toString, 10, 10))\n      .take(4)\n    testExpectedError[RuntimeException](\n      files = corruptedCheckpointStatuses.toSeq ++ deltaFileStatuses(10L to 20L) ++\n        singularCheckpointFileStatuses(Seq(10L)),\n      lastCheckpointVersion = Optional.of(20),\n      expectedErrorMessageContains = \"Missing checkpoint at version 20\")\n  }\n\n  test(\"getLogSegmentForVersion: corrupted incomplete multi-part checkpoint with no\" +\n    \"_last_checkpoint or a valid _last_checkpoint provided\") {\n    val cases: Seq[(Long, Seq[Long], Seq[Long], Optional[java.lang.Long])] = Seq(\n      /* (corruptedCheckpointVersion, validCheckpointVersions, deltaVersions, lastCheckpointV) */\n      (20, Seq(10), (10L to 20L), Optional.empty()),\n      (20, Seq(10), (10L to 20L), Optional.of(10)),\n      (10, Seq.empty, (0L to 10L), Optional.empty()))\n    cases.foreach { case (corruptedVersion, validVersions, deltaVersions, lastCheckpointVersion) =>\n      val corruptedCheckpoint = FileNames.checkpointFileWithParts(logPath, corruptedVersion, 5)\n        .asScala\n        .map(p => FileStatus.of(p.toString, 10, 10))\n        .take(4)\n      val checkpoints = singularCheckpointFileStatuses(validVersions)\n      val deltas = deltaFileStatuses(deltaVersions)\n      val allFiles = deltas ++ corruptedCheckpoint ++ checkpoints\n      val logSegment = snapshotManager.getLogSegmentForVersion(\n        createMockFSAndJsonEngineForLastCheckpoint(allFiles, lastCheckpointVersion),\n        Optional.empty())\n      val checkpointVersion = validVersions.sorted.lastOption\n      checkLogSegment(\n        logSegment,\n        expectedVersion = deltaVersions.max,\n        expectedDeltas = deltaFileStatuses(\n          deltaVersions.filter(_ > checkpointVersion.getOrElse(-1L))),\n        expectedCompactions = Seq.empty,\n        expectedCheckpoints = checkpoints,\n        expectedCheckpointVersion = checkpointVersion,\n        expectedLastCommitTimestamp = deltaVersions.max * 10)\n    }\n  }\n\n  test(\"getLogSegmentForVersion: corrupt _last_checkpoint with empty delta log\") {\n    val exMsg = intercept[InvalidTableException] {\n      snapshotManager.getLogSegmentForVersion(\n        createMockFSAndJsonEngineForLastCheckpoint(Seq.empty, Optional.of(1)),\n        Optional.empty())\n    }.getMessage\n\n    assert(exMsg.contains(\"Missing checkpoint at version 1\"))\n  }\n\n  /* ------------------- CATALOG MANAGED TABLE TESTS ------------------ */\n\n  test(\"catalog managed: latest query, we load the maxCatalogVersion even if other deltas exist\") {\n    val deltas = deltaFileStatuses(0L to 20)\n    val checkpoints = singularCheckpointFileStatuses(Seq(10))\n\n    val logSegment = snapshotManager.getLogSegmentForVersion(\n      createMockFSListFromEngine(deltas ++ checkpoints),\n      Optional.empty(), // timeTravelVersionOpt\n      Collections.emptyList(), // parsedLogDatas\n      Optional.of(15L) // maxCatalogVersionOpt\n    )\n\n    checkLogSegment(\n      logSegment,\n      expectedVersion = 15,\n      expectedDeltas = deltaFileStatuses(11L to 15),\n      expectedCompactions = Seq.empty,\n      expectedCheckpoints = singularCheckpointFileStatuses(Seq(10)),\n      expectedCheckpointVersion = Some(10),\n      expectedLastCommitTimestamp = 150L)\n  }\n\n  test(\"catalog managed: latest query, _last_checkpoint does not exist\") {\n    val deltas = deltaFileStatuses(0L to 20)\n    val checkpoints = singularCheckpointFileStatuses(Seq(10))\n\n    val logSegment = snapshotManager.getLogSegmentForVersion(\n      createMockFSListFromEngine(deltas ++ checkpoints),\n      Optional.empty(), // timeTravelVersionOpt\n      Collections.emptyList(), // parsedLogDatas\n      Optional.of(20L) // maxCatalogVersionOpt\n    )\n\n    // Should find checkpoint at version 10 by searching backwards from version 20\n    checkLogSegment(\n      logSegment,\n      expectedVersion = 20,\n      expectedDeltas = deltaFileStatuses(11L to 20),\n      expectedCompactions = Seq.empty,\n      expectedCheckpoints = singularCheckpointFileStatuses(Seq(10)),\n      expectedCheckpointVersion = Some(10),\n      expectedLastCommitTimestamp = 200L)\n  }\n\n  test(\"catalog managed: latest query, when _last_checkpoint exists and \" +\n    \"is <= maxCatalogVersion we use it\") {\n    val deltas = deltaFileStatuses(0L to 30)\n    val checkpoints = singularCheckpointFileStatuses(Seq(10, 20, 25))\n    val lastCheckpointFileStatus = FileStatus.of(s\"$logPath/_last_checkpoint\", 2, 2)\n    val files = deltas ++ checkpoints ++ Seq(lastCheckpointFileStatus)\n\n    // Create mocked engine that fails if we try to list before the version stored in\n    // _last_checkpoint\n    def listFrom(filePath: String): Seq[FileStatus] = {\n      if (filePath < FileNames.listingPrefix(logPath, 25)) {\n        throw new RuntimeException(\n          s\"Listing from before the checkpoint version referenced by _last_checkpoint.\")\n      }\n      listFromProvider(files)(filePath)\n    }\n    val mockedEngine = mockEngine(\n      jsonHandler = new MockReadLastCheckpointFileJsonHandler(\n        lastCheckpointFileStatus.getPath,\n        25\n      ), // _last_checkpoint points to version 25\n      fileSystemClient = new MockListFromFileSystemClient(listFrom))\n\n    // Latest query with catalog managed table (maxCatalogVersion = 30)\n    val logSegment = snapshotManager.getLogSegmentForVersion(\n      mockedEngine,\n      Optional.empty(), // timeTravelVersionOpt\n      Collections.emptyList(), // parsedLogDatas\n      Optional.of(30L) // maxCatalogVersionOpt\n    )\n\n    checkLogSegment(\n      logSegment,\n      expectedVersion = 30,\n      expectedDeltas = deltaFileStatuses(26L to 30),\n      expectedCompactions = Seq.empty,\n      expectedCheckpoints = singularCheckpointFileStatuses(Seq(25)),\n      expectedCheckpointVersion = Some(25),\n      expectedLastCommitTimestamp = 300L)\n  }\n\n  test(\"catalog managed:\" +\n    \"latest query, ignore _last_checkpoint if it's newer than maxCatalogVersion\") {\n    val deltas = deltaFileStatuses(0L to 26)\n    val checkpoints = singularCheckpointFileStatuses(Seq(10, 20, 25))\n    val lastCheckpointFileStatus = FileStatus.of(s\"$logPath/_last_checkpoint\", 2, 2)\n    val files = deltas ++ checkpoints ++ Seq(lastCheckpointFileStatus)\n\n    // Latest query with catalog managed table where maxCatalogVersion < _last_checkpoint version\n    val logSegment = snapshotManager.getLogSegmentForVersion(\n      mockEngine(\n        jsonHandler = new MockReadLastCheckpointFileJsonHandler(\n          lastCheckpointFileStatus.getPath,\n          25\n        ), // _last_checkpoint points to version 25\n        fileSystemClient = new MockListFromFileSystemClient(listFromProvider(files))),\n      Optional.empty(), // timeTravelVersionOpt\n      Collections.emptyList(), // parsedLogDatas\n      Optional.of(24L) // maxCatalogVersionOpt is 24, which is < 25\n    )\n\n    // Should use checkpoint at version 20 not 25, and should load maxCatalogVersion\n    checkLogSegment(\n      logSegment,\n      expectedVersion = 24,\n      expectedDeltas = deltaFileStatuses(21L to 24),\n      expectedCompactions = Seq.empty,\n      expectedCheckpoints = singularCheckpointFileStatuses(Seq(20)),\n      expectedCheckpointVersion = Some(20),\n      expectedLastCommitTimestamp = 240L)\n  }\n\n  test(\"catalog managed: time travel query ignores _last_checkpoint\") {\n    val deltas = deltaFileStatuses(0L to 30)\n    val checkpoints = singularCheckpointFileStatuses(Seq(10, 20, 25))\n    val lastCheckpointFileStatus = FileStatus.of(s\"$logPath/_last_checkpoint\", 2, 2)\n    val files = deltas ++ checkpoints ++ Seq(lastCheckpointFileStatus)\n\n    val jsonHandler = new BaseMockJsonHandler {\n      override def readJsonFiles(\n          fileIter: CloseableIterator[FileStatus],\n          physicalSchema: StructType,\n          predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n        assert(fileIter.hasNext)\n        if (fileIter.next.getPath == lastCheckpointFileStatus.getPath) {\n          throw new RuntimeException(\n            \"We should not be reading the _last_checkpoint file for time-travel queries\")\n        } else {\n          throw new RuntimeException(\"We should not be reading JSON files besides \" +\n            \"_last_checkpoint during log segment construction\")\n        }\n      }\n    }\n\n    // Time travel query with catalog managed table\n    val logSegment = snapshotManager.getLogSegmentForVersion(\n      mockEngine(\n        jsonHandler = jsonHandler,\n        fileSystemClient = new MockListFromFileSystemClient(listFromProvider(files))),\n      Optional.of(15L), // timeTravelVersionOpt = 15\n      Collections.emptyList(), // parsedLogDatas\n      Optional.of(30L) // maxCatalogVersionOpt\n    )\n\n    // Should use checkpoint at version 10 for time travel to version 15\n    checkLogSegment(\n      logSegment,\n      expectedVersion = 15,\n      expectedDeltas = deltaFileStatuses(11L to 15),\n      expectedCompactions = Seq.empty,\n      expectedCheckpoints = singularCheckpointFileStatuses(Seq(10)),\n      expectedCheckpointVersion = Some(10),\n      expectedLastCommitTimestamp = 150L)\n  }\n\n  /* ------------------- Compaction tests ------------------ */\n\n  test(\"One compaction\") {\n    testWithCompactionsNoCheckpoint(\n      deltaVersions = 0L until 5L,\n      compactionVersions = Seq((0, 4)))\n\n    testWithCompactionsNoCheckpoint(\n      deltaVersions = 0L until 5L,\n      compactionVersions = Seq((0, 4)),\n      versionToLoad = Optional.of(4))\n  }\n\n  test(\"Compaction extends too far\") {\n    testWithCompactionsNoCheckpoint(\n      deltaVersions = 0L until 5L,\n      compactionVersions = Seq((3, 5)),\n      versionToLoad = Optional.of(4))\n  }\n\n  test(\"Compaction after checkpoint\") {\n    testWithCheckpoints(\n      deltaVersions = 0L until 6L,\n      checkpointVersions = Seq(2),\n      multiCheckpointVersions = Seq.empty,\n      compactionVersions = Seq((3, 5)))\n  }\n\n  test(\"Compaction starting before checkpoint\") {\n    testWithCheckpoints(\n      deltaVersions = 0L until 6L,\n      checkpointVersions = Seq(2),\n      multiCheckpointVersions = Seq.empty,\n      compactionVersions = Seq((1, 5)))\n  }\n\n  test(\"Compaction starting same as checkpoint\") {\n    testWithCheckpoints(\n      deltaVersions = 0L until 5L,\n      checkpointVersions = Seq(2),\n      multiCheckpointVersions = Seq.empty,\n      compactionVersions = Seq((2, 5)))\n  }\n}\n\ntrait SidecarIteratorProvider extends VectorTestUtils {\n\n  private def buildSidecarBatch(sidecars: Seq[FileStatus]): ColumnarBatch = new ColumnarBatch {\n    override def getSchema: StructType = SidecarFile.READ_SCHEMA\n\n    override def getColumnVector(ordinal: Int): ColumnVector = ordinal match {\n      case 0 => stringVector(sidecars.map(_.getPath)) // path\n      case 1 => longVector(sidecars.map(_.getSize).map(JLong.valueOf)) // size\n      case 2 =>\n        longVector(sidecars.map(_.getModificationTime).map(JLong.valueOf)) // modification time\n    }\n\n    override def getSize: Int = sidecars.length\n  }\n\n  def singletonSidecarParquetIterator(sidecars: Seq[FileStatus], v2CheckpointFileName: String)\n      : CloseableIterator[FileReadResult] = {\n    val batch = buildSidecarBatch(sidecars)\n    Utils.singletonCloseableIterator(new FileReadResult(batch, v2CheckpointFileName))\n  }\n\n  // TODO: [delta-io/delta#4849] extend FileReadResult for JSON read result\n  def singletonSidecarJsonIterator(sidecars: Seq[FileStatus]): CloseableIterator[ColumnarBatch] = {\n    val batch = buildSidecarBatch(sidecars)\n    Utils.singletonCloseableIterator(batch)\n  }\n}\n\nclass MockSidecarParquetHandler(sidecars: Seq[FileStatus], v2CheckpointFileName: String)\n    extends BaseMockParquetHandler with SidecarIteratorProvider {\n  override def readParquetFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[FileReadResult] =\n    singletonSidecarParquetIterator(sidecars, v2CheckpointFileName)\n}\n\nclass MockSidecarJsonHandler(sidecars: Seq[FileStatus])\n    extends BaseMockJsonHandler\n    with SidecarIteratorProvider {\n  override def readJsonFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] =\n    singletonSidecarJsonIterator(sidecars)\n}\n\nclass MockReadLastCheckpointFileJsonHandler(\n    lastCheckpointPath: String,\n    lastCheckpointVersion: Long)\n    extends BaseMockJsonHandler with VectorTestUtils {\n  override def readJsonFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n    assert(fileIter.hasNext)\n    assert(fileIter.next.getPath == lastCheckpointPath)\n\n    Utils.singletonCloseableIterator(\n      new ColumnarBatch {\n        override def getSchema: StructType = CheckpointMetaData.READ_SCHEMA\n\n        override def getColumnVector(ordinal: Int): ColumnVector = {\n          ordinal match {\n            case 0 => longVector(Seq(lastCheckpointVersion)) /* version */\n            case 1 => longVector(Seq(100)) /* size */\n            case 2 => longVector(Seq(1)) /* parts */\n            case 3 => mapTypeVector(Seq(Map.empty[String, String]))\n          }\n        }\n\n        override def getSize: Int = 1\n      })\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/TableConfigSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TableConfigSuite extends AnyFunSuite {\n\n  test(\"check TableConfig.editable is true\") {\n    TableConfig.validateAndNormalizeDeltaProperties(\n      Map(\n        TableConfig.TOMBSTONE_RETENTION.getKey -> \"interval 2 week\",\n        TableConfig.CHECKPOINT_INTERVAL.getKey -> \"20\",\n        TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\",\n        TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey -> \"1\",\n        TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey -> \"1\",\n        TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n        TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\",\n        TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"iceberg\").asJava)\n  }\n\n  test(\"check TableConfig.MAX_COLUMN_ID.editable is false\") {\n    val e = intercept[KernelException] {\n      TableConfig.validateAndNormalizeDeltaProperties(\n        Map(\n          TableConfig.TOMBSTONE_RETENTION.getKey -> \"interval 2 week\",\n          TableConfig.CHECKPOINT_INTERVAL.getKey -> \"20\",\n          TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\",\n          TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.getKey -> \"10\").asJava)\n    }\n\n    assert(e.isInstanceOf[KernelException])\n    assert(e.getMessage ===\n      s\"The Delta table property \" +\n      s\"'${TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.getKey}'\" +\n      s\" is an internal property and cannot be updated.\")\n  }\n\n  Seq(\n    Map[String, String](),\n    Map(TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"\")).foreach {\n    config =>\n      {\n        test(\n          s\"Parsing UNIVERSAL_ENABLED formats returns empty set when key is not present $config\") {\n          val formats = TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.fromMetadata(config.asJava)\n          assert(formats.isEmpty)\n        }\n      }\n  }\n\n  test(\"Parsing UNIVERSAL_ENABLED_FORMATS can parse spaces\") {\n    val FORMATS_KEY = TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey\n    val config = Map(FORMATS_KEY -> \"iceberg, hudi \").asJava\n    val formats = TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.fromMetadata(config)\n    assert(formats == Set(\"iceberg\", \"hudi\").asJava)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/TableImplSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport io.delta.kernel.Snapshot\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.{Clock, FileNames, ManualClock}\nimport io.delta.kernel.test.{MockFileSystemClientUtils, MockListFromResolvePathFileSystemClient}\nimport io.delta.kernel.test.MockSnapshotUtils.getMockSnapshot\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TableImplSuite extends AnyFunSuite with MockFileSystemClientUtils {\n\n  /**\n   * Both timestamp-based travel methods need to be able to construct the latest snapshot\n   * internally. This class overrides getLatestSnapshot to return a mocked snapshot.\n   */\n  class TableImplWithMockedLatestSnapshot(tablePath: String, clock: Clock, latestVersion: Long)\n      extends TableImpl(tablePath, clock) {\n    override def getLatestSnapshot(engine: Engine): SnapshotImpl = {\n      getMockSnapshot(\n        new Path(tablePath),\n        latestVersion)\n    }\n  }\n\n  def checkGetVersionBeforeOrAtTimestamp(\n      fileList: Seq[FileStatus],\n      timestamp: Long,\n      expectedVersion: Option[Long] = None,\n      expectedErrorMessageContains: Option[String] = None): Unit = {\n    // Check our inputs are as expected\n    assert(expectedVersion.isEmpty || expectedErrorMessageContains.isEmpty)\n    assert(expectedVersion.nonEmpty || expectedErrorMessageContains.nonEmpty)\n\n    val engine = mockEngine(fileSystemClient =\n      new MockListFromResolvePathFileSystemClient(listFromProvider(fileList)))\n    val latestVersion = fileList.map(fs => FileNames.getFileVersion(new Path(fs.getPath))).max\n    val table =\n      new TableImplWithMockedLatestSnapshot(dataPath.toString, new ManualClock(0), latestVersion)\n\n    expectedVersion.foreach { v =>\n      assert(table.asInstanceOf[TableImpl].getVersionBeforeOrAtTimestamp(engine, timestamp) == v)\n    }\n    expectedErrorMessageContains.foreach { s =>\n      assert(intercept[KernelException] {\n        table.asInstanceOf[TableImpl].getVersionBeforeOrAtTimestamp(engine, timestamp)\n      }.getMessage.contains(s))\n    }\n  }\n\n  def checkGetVersionAtOrAfterTimestamp(\n      fileList: Seq[FileStatus],\n      timestamp: Long,\n      expectedVersion: Option[Long] = None,\n      expectedErrorMessageContains: Option[String] = None): Unit = {\n    // Check our inputs are as expected\n    assert(expectedVersion.isEmpty || expectedErrorMessageContains.isEmpty)\n    assert(expectedVersion.nonEmpty || expectedErrorMessageContains.nonEmpty)\n\n    val engine = mockEngine(fileSystemClient =\n      new MockListFromResolvePathFileSystemClient(listFromProvider(fileList)))\n    val latestVersion = fileList.map(fs => FileNames.getFileVersion(new Path(fs.getPath))).max\n    val table =\n      new TableImplWithMockedLatestSnapshot(dataPath.toString, new ManualClock(0), latestVersion)\n\n    expectedVersion.foreach { v =>\n      assert(table.asInstanceOf[TableImpl].getVersionAtOrAfterTimestamp(engine, timestamp) == v)\n    }\n    expectedErrorMessageContains.foreach { s =>\n      assert(intercept[KernelException] {\n        table.asInstanceOf[TableImpl].getVersionAtOrAfterTimestamp(engine, timestamp)\n      }.getMessage.contains(s))\n    }\n  }\n\n  test(\"getVersionBeforeOrAtTimestamp: basic case from 0\") {\n    val deltaFiles = deltaFileStatuses(Seq(0L, 1L))\n    checkGetVersionBeforeOrAtTimestamp(\n      deltaFiles,\n      -1,\n      expectedErrorMessageContains = Some(\"is before the earliest available version 0\")\n    ) // before 0\n    checkGetVersionBeforeOrAtTimestamp(deltaFiles, 0, expectedVersion = Some(0)) // at 0\n    checkGetVersionBeforeOrAtTimestamp(deltaFiles, 5, expectedVersion = Some(0)) // btw 0, 1\n    checkGetVersionBeforeOrAtTimestamp(deltaFiles, 10, expectedVersion = Some(1)) // at 1\n    checkGetVersionBeforeOrAtTimestamp(deltaFiles, 11, expectedVersion = Some(1)) // after 1\n  }\n\n  test(\"getVersionAtOrAfterTimestamp: basic case from 0\") {\n    val deltaFiles = deltaFileStatuses(Seq(0L, 1L))\n    checkGetVersionAtOrAfterTimestamp(deltaFiles, -1, expectedVersion = Some(0)) // before 0\n    checkGetVersionAtOrAfterTimestamp(deltaFiles, 0, expectedVersion = Some(0)) // at 0\n    checkGetVersionAtOrAfterTimestamp(deltaFiles, 5, expectedVersion = Some(1)) // btw 0, 1\n    checkGetVersionAtOrAfterTimestamp(deltaFiles, 10, expectedVersion = Some(1)) // at 1\n    checkGetVersionAtOrAfterTimestamp(\n      deltaFiles,\n      11,\n      expectedErrorMessageContains = Some(\"is after the latest available version 1\")\n    ) // after 1\n  }\n\n  test(\"getVersionBeforeOrAtTimestamp: w/ checkpoint + w/o checkpoint\") {\n    Seq(\n      deltaFileStatuses(Seq(10L, 11L, 12L)) ++ singularCheckpointFileStatuses(Seq(10L)),\n      deltaFileStatuses(Seq(10L, 11L, 12L)) // checks that does not need to be recreatable\n    ).foreach { deltaFiles =>\n      checkGetVersionBeforeOrAtTimestamp(\n        deltaFiles,\n        99, // before 10\n        expectedErrorMessageContains = Some(\"is before the earliest available version 10\"))\n      checkGetVersionBeforeOrAtTimestamp(deltaFiles, 100, expectedVersion = Some(10)) // at 10\n      checkGetVersionBeforeOrAtTimestamp(deltaFiles, 105, expectedVersion = Some(10)) // btw 10, 11\n      checkGetVersionBeforeOrAtTimestamp(deltaFiles, 110, expectedVersion = Some(11)) // at 11\n      checkGetVersionBeforeOrAtTimestamp(deltaFiles, 115, expectedVersion = Some(11)) // btw 11, 12\n      checkGetVersionBeforeOrAtTimestamp(deltaFiles, 120, expectedVersion = Some(12)) // at 12\n      checkGetVersionBeforeOrAtTimestamp(deltaFiles, 125, expectedVersion = Some(12)) // after 12\n    }\n  }\n\n  test(\"getVersionAtOrAfterTimestamp: w/ checkpoint + w/o checkpoint\") {\n    Seq(\n      deltaFileStatuses(Seq(10L, 11L, 12L)) ++ singularCheckpointFileStatuses(Seq(10L)),\n      deltaFileStatuses(Seq(10L, 11L, 12L)) // checks that does not need to be recreatable\n    ).foreach { deltaFiles =>\n      checkGetVersionAtOrAfterTimestamp(deltaFiles, 99, expectedVersion = Some(10)) // before 10\n      checkGetVersionAtOrAfterTimestamp(deltaFiles, 100, expectedVersion = Some(10)) // at 10\n      checkGetVersionAtOrAfterTimestamp(deltaFiles, 105, expectedVersion = Some(11)) // btw 10, 11\n      checkGetVersionAtOrAfterTimestamp(deltaFiles, 110, expectedVersion = Some(11)) // at 11\n      checkGetVersionAtOrAfterTimestamp(deltaFiles, 115, expectedVersion = Some(12)) // btw 11, 12\n      checkGetVersionAtOrAfterTimestamp(deltaFiles, 120, expectedVersion = Some(12)) // at 12\n      checkGetVersionAtOrAfterTimestamp(\n        deltaFiles,\n        125,\n        expectedErrorMessageContains = Some(\"is after the latest available version 12\")\n      ) // after 12\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/TransactionBuilderImplSuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.Operation\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.checksum.CRCInfo\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.lang.Lazy\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext\nimport io.delta.kernel.internal.snapshot.LogSegment\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.internal.util.VectorUtils.{buildArrayValue, stringStringMapValue}\nimport io.delta.kernel.test.MockEngineUtils\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TransactionBuilderImplSuite extends AnyFunSuite with MockEngineUtils {\n\n  /**\n   * Creates a mock snapshot whose metadata includes a `delta.feature.<name>` property that\n   * TransactionMetadataFactory would attempt to process. This is used to verify the early-return\n   * path: if the code falls through to the factory, processing the unknown feature throws.\n   */\n  private def createMockSnapshot(\n      dataPath: Path,\n      version: Long,\n      extraConfig: Map[String, String] = Map.empty): SnapshotImpl = {\n    val schema = new StructType().add(\"id\", IntegerType.INTEGER)\n    val metadata = new Metadata(\n      \"id\",\n      Optional.empty(),\n      Optional.empty(),\n      new Format(),\n      schema.toJson,\n      schema,\n      buildArrayValue(java.util.Arrays.asList(), StringType.STRING),\n      Optional.of(123),\n      stringStringMapValue(extraConfig.asJava))\n    val logPath = new Path(dataPath, \"_delta_log\")\n    val fs = FileStatus.of(FileNames.deltaFile(logPath, version), 1, 1)\n    val logSegment = new LogSegment(\n      logPath,\n      version,\n      Seq(fs).asJava,\n      Seq.empty.asJava,\n      Seq.empty.asJava,\n      fs,\n      Optional.empty(),\n      Optional.empty())\n    val snapshotQueryContext = SnapshotQueryContext.forLatestSnapshot(dataPath.toString)\n    new SnapshotImpl(\n      dataPath,\n      logSegment.getVersion,\n      new Lazy(() => logSegment),\n      null, // logReplay - not needed; getCurrentCrcInfo is overridden below\n      new Protocol(1, 2),\n      metadata,\n      DefaultFileSystemManagedTableOnlyCommitter.INSTANCE,\n      snapshotQueryContext,\n      Optional.empty()) {\n      override def getCurrentCrcInfo: Optional[CRCInfo] = Optional.empty()\n    }\n  }\n\n  test(\"early return when no metadata or protocol update is needed\") {\n    // The snapshot metadata includes an unrecognized delta.feature.* property. If the code\n    // falls through to TransactionMetadataFactory (the bug), extractFeaturePropertyOverrides\n    // will try to resolve this feature and throw. The early-return path skips the factory\n    // entirely, so no exception is thrown.\n    val dataPath = new Path(\"/tmp/test-table\")\n    val tableImpl = new TableImpl(dataPath.toString, () => System.currentTimeMillis())\n    val snapshot = createMockSnapshot(\n      dataPath,\n      version = 0L,\n      extraConfig = Map(\"delta.feature.fakeFeatureForEarlyReturnTest\" -> \"supported\"))\n\n    val builder = new TransactionBuilderImpl(tableImpl, \"test-engine\", Operation.WRITE)\n\n    // With the fix this returns immediately; without the fix this would throw\n    val txn = builder.buildTransactionInternal(\n      mockEngine(),\n      false, // isCreateOrReplace\n      Optional.of(snapshot))\n\n    // Verify the returned transaction does not mark protocol/metadata for update\n    assert(txn.getSchema(mockEngine()) === snapshot.getMetadata().getSchema())\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/TransactionMetadataFactorySuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.test.MockSnapshotUtils\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TransactionMetadataFactorySuite extends AnyFunSuite {\n\n  // Test schema for metadata creation\n  val testSchema: StructType = new StructType()\n    .add(\"id\", IntegerType.INTEGER)\n    .add(\"name\", StringType.STRING)\n    .add(\"value\", IntegerType.INTEGER)\n\n  val testTablePath = \"/test/table/path\"\n\n  // =====================================================================\n  // buildCreateTableMetadata tests\n  // =====================================================================\n\n  test(\"buildCreateTableMetadata - basic schema without partitions or clustering\") {\n    val tableProperties = Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\").asJava\n    val output = TransactionMetadataFactory.buildCreateTableMetadata(\n      testTablePath,\n      testSchema,\n      tableProperties,\n      Optional.empty(), // no partition columns\n      Optional.empty(), // no clustering columns\n      Optional.empty() // no custom committer\n    )\n\n    // Verify both metadata and protocol are present for create table\n    assert(output.newMetadata.isPresent, \"New metadata should be present for create table\")\n    assert(output.newProtocol.isPresent, \"New protocol should be present for create table\")\n    assert(\n      !output.physicalNewClusteringColumns.isPresent,\n      \"No clustering columns should be present\")\n\n    val metadata = output.newMetadata.get()\n    assert(metadata.getSchema === testSchema)\n    assert(metadata.getConfiguration.asScala === tableProperties.asScala)\n  }\n\n  test(\"buildCreateTableMetadata - with partition columns\") {\n    val tableProperties = Map(\"delta.autoOptimize\" -> \"true\").asJava\n    val partitionCols = Optional.of(List(\"name\").asJava)\n\n    val output = TransactionMetadataFactory.buildCreateTableMetadata(\n      testTablePath,\n      testSchema,\n      tableProperties,\n      partitionCols,\n      Optional.empty(),\n      Optional.empty() /* committerOpt */\n    )\n\n    assert(output.newMetadata.isPresent)\n    assert(output.newProtocol.isPresent)\n    assert(!output.physicalNewClusteringColumns.isPresent)\n\n    val metadata = output.newMetadata.get()\n    assert(metadata.getSchema === testSchema)\n\n    // Verify partition columns are set correctly\n    val partitionColumns = metadata.getPartitionColumns\n    assert(partitionColumns.getSize === 1)\n  }\n\n  test(\"buildCreateTableMetadata - with clustering columns\") {\n    val tableProperties = Map(\"delta.feature.clustering\" -> \"supported\").asJava\n    val clusteringCols = Optional.of(List(new Column(\"name\")).asJava)\n\n    val output = TransactionMetadataFactory.buildCreateTableMetadata(\n      testTablePath,\n      testSchema,\n      tableProperties,\n      Optional.empty(), // no partition columns\n      clusteringCols,\n      Optional.empty() /* committerOpt */\n    )\n\n    assert(output.newMetadata.isPresent)\n    assert(output.newProtocol.isPresent)\n    assert(output.physicalNewClusteringColumns.isPresent &&\n      output.physicalNewClusteringColumns.get.size == 1)\n  }\n\n  test(\"buildCreateTableMetadata - should reject both partition and clustering columns\") {\n    val tableProperties = Map.empty[String, String].asJava\n    val partitionCols = Optional.of(List(\"name\").asJava)\n    val clusteringCols = Optional.of(List(new Column(\"value\")).asJava)\n\n    assertThrows[IllegalArgumentException] {\n      TransactionMetadataFactory.buildCreateTableMetadata(\n        testTablePath,\n        testSchema,\n        tableProperties,\n        partitionCols,\n        clusteringCols,\n        Optional.empty() /* committerOpt */\n      )\n    }\n  }\n\n  // =====================================================================\n  // buildReplaceTableMetadata tests\n  // =====================================================================\n\n  test(\"buildReplaceTableMetadata - basic replacement\") {\n    val newTableProperties = Map(\"newKey\" -> \"newValue\").asJava\n\n    // Create a mock snapshot for the existing table\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 1L)\n\n    val output = TransactionMetadataFactory.buildReplaceTableMetadata(\n      testTablePath,\n      mockSnapshot,\n      testSchema,\n      newTableProperties,\n      Optional.empty(), // no partition columns\n      Optional.empty() // no clustering columns\n    )\n\n    assert(output.newMetadata.isPresent, \"New metadata should be present for replace table\")\n    assert(output.newProtocol.isPresent, \"New protocol should be present for replace table\")\n    assert(!output.physicalNewClusteringColumns.isPresent)\n\n    val metadata = output.newMetadata.get()\n    assert(metadata.getSchema === testSchema)\n  }\n\n  test(\"buildReplaceTableMetadata - with partition columns\") {\n    val newTableProperties = Map(\"delta.autoOptimize\" -> \"false\").asJava\n    val partitionCols = Optional.of(List(\"id\").asJava)\n\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 2L)\n\n    val output = TransactionMetadataFactory.buildReplaceTableMetadata(\n      testTablePath,\n      mockSnapshot,\n      testSchema,\n      newTableProperties,\n      partitionCols,\n      Optional.empty())\n\n    assert(output.newMetadata.isPresent)\n    assert(output.newProtocol.isPresent)\n\n    val metadata = output.newMetadata.get()\n    assert(metadata.getSchema === testSchema)\n  }\n\n  test(\"buildReplaceTableMetadata - with clustering columns\") {\n    val newTableProperties = Map(\"delta.feature.clustering\" -> \"supported\").asJava\n    val clusteringCols = Optional.of(List(new Column(\"name\"), new Column(\"value\")).asJava)\n\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 3L)\n\n    val output = TransactionMetadataFactory.buildReplaceTableMetadata(\n      testTablePath,\n      mockSnapshot,\n      testSchema,\n      newTableProperties,\n      Optional.empty(), // no partition columns\n      clusteringCols)\n\n    assert(output.newMetadata.isPresent, \"New metadata should be present for replace table\")\n    assert(output.newProtocol.isPresent, \"New protocol should be present for replace table\")\n    assert(output.physicalNewClusteringColumns.isPresent, \"Clustering columns should be resolved\")\n\n    val metadata = output.newMetadata.get()\n    assert(metadata.getSchema === testSchema)\n\n    // Verify clustering columns are resolved\n    val clusteringColumns = output.physicalNewClusteringColumns.get()\n    assert(clusteringColumns.size() === 2, \"Should have 2 clustering columns\")\n  }\n\n  test(\"buildReplaceTableMetadata - should reject both partition and clustering columns\") {\n    val newTableProperties = Map.empty[String, String].asJava\n    val partitionCols = Optional.of(List(\"name\").asJava)\n    val clusteringCols = Optional.of(List(new Column(\"value\")).asJava)\n\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 1L)\n\n    assertThrows[IllegalArgumentException] {\n      TransactionMetadataFactory.buildReplaceTableMetadata(\n        testTablePath,\n        mockSnapshot,\n        testSchema,\n        newTableProperties,\n        partitionCols,\n        clusteringCols)\n    }\n  }\n\n  // =====================================================================\n  // buildUpdateTableMetadata tests\n  // =====================================================================\n\n  test(\"buildUpdateTableMetadata - no changes\") {\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 3L)\n\n    val output = TransactionMetadataFactory.buildUpdateTableMetadata(\n      testTablePath,\n      mockSnapshot,\n      Optional.empty(), // no properties added\n      Optional.empty(), // no properties removed\n      Optional.empty(), // no schema change\n      Optional.empty() // no clustering columns\n    )\n\n    // With no changes, no new metadata or protocol should be present\n    assert(!output.newMetadata.isPresent, \"No new metadata should be present when no changes\")\n    assert(!output.newProtocol.isPresent, \"No new protocol should be present when no changes\")\n    assert(!output.physicalNewClusteringColumns.isPresent)\n  }\n\n  test(\"buildUpdateTableMetadata - add table properties\") {\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 4L)\n\n    val newProperties = Map(\"newKey\" -> \"newValue\", \"anotherKey\" -> \"anotherValue\").asJava\n    val output = TransactionMetadataFactory.buildUpdateTableMetadata(\n      testTablePath,\n      mockSnapshot,\n      Optional.of(newProperties),\n      Optional.empty(),\n      Optional.empty(),\n      Optional.empty())\n\n    // Properties change should trigger new metadata\n    assert(output.newMetadata.isPresent, \"New metadata should be present when properties change\")\n\n    val metadata = output.newMetadata.get()\n    // Verify the new properties are merged\n    assert(metadata.getConfiguration.containsKey(\"newKey\"))\n    assert(metadata.getConfiguration.get(\"newKey\") === \"newValue\")\n  }\n\n  test(\"buildUpdateTableMetadata - with clustering columns\") {\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 9L)\n\n    val clusteringColumns = Optional.of(List(new Column(\"name\")).asJava)\n    val output = TransactionMetadataFactory.buildUpdateTableMetadata(\n      testTablePath,\n      mockSnapshot,\n      Optional.empty(),\n      Optional.empty(),\n      Optional.empty(),\n      clusteringColumns)\n\n    assert(output.physicalNewClusteringColumns.isPresent &&\n      output.physicalNewClusteringColumns.get.size == 1)\n  }\n\n  test(\"buildUpdateTableMetadata - overlapping set and unset properties should fail\") {\n    val mockSnapshot = MockSnapshotUtils.getMockSnapshot(\n      new Path(testTablePath),\n      latestVersion = 7L)\n\n    val propertiesToAdd = Map(\"conflictKey\" -> \"newValue\").asJava\n    val propertyKeysToRemove = Set(\"conflictKey\").asJava\n\n    assertThrows[Exception] {\n      TransactionMetadataFactory.buildUpdateTableMetadata(\n        testTablePath,\n        mockSnapshot,\n        Optional.of(propertiesToAdd),\n        Optional.of(propertyKeysToRemove),\n        Optional.empty(),\n        Optional.empty())\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/actions/AddFileSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions\n\nimport java.lang.{Boolean => JBoolean, Long => JLong}\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.internal.util.VectorUtils.stringStringMapValue\nimport io.delta.kernel.statistics.DataFileStatistics\n\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.matchers.must.Matchers\n\nclass AddFileSuite extends AnyFunSuite with Matchers {\n\n  /**\n   * Generate a Row representing an AddFile action with provided fields.\n   */\n  private def generateTestAddFileRow(\n      path: String = \"path\",\n      partitionValues: Map[String, String] = Map.empty,\n      size: Long = 10L,\n      modificationTime: Long = 20L,\n      dataChange: Boolean = true,\n      deletionVector: Option[DeletionVectorDescriptor] = Option.empty,\n      tags: Option[Map[String, String]] = Option.empty,\n      baseRowId: Option[Long] = Option.empty,\n      defaultRowCommitVersion: Option[Long] = Option.empty,\n      stats: Option[String] = Option.empty): Row = {\n    def toJavaOptional[T](option: Option[T]): Optional[T] = option match {\n      case Some(value) => Optional.of(value)\n      case None => Optional.empty()\n    }\n\n    AddFile.createAddFileRow(\n      null,\n      path,\n      stringStringMapValue(partitionValues.asJava),\n      size.asInstanceOf[JLong],\n      modificationTime.asInstanceOf[JLong],\n      dataChange.asInstanceOf[JBoolean],\n      toJavaOptional(deletionVector),\n      toJavaOptional(tags.map(_.asJava).map(stringStringMapValue)),\n      toJavaOptional(baseRowId.asInstanceOf[Option[JLong]]),\n      toJavaOptional(defaultRowCommitVersion.asInstanceOf[Option[JLong]]),\n      DataFileStatistics.deserializeFromJson(stats.getOrElse(\"\"), null))\n  }\n\n  test(\"getters can read AddFile's fields from the backing row\") {\n    val addFileRow = generateTestAddFileRow(\n      path = \"test/path\",\n      partitionValues = Map(\"a\" -> \"1\"),\n      size = 1L,\n      modificationTime = 10L,\n      dataChange = false,\n      deletionVector = Option.empty,\n      tags = Option(Map(\"tag1\" -> \"value1\")),\n      baseRowId = Option(30L),\n      defaultRowCommitVersion = Option(40L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    val addFile = new AddFile(addFileRow)\n    assert(addFile.getPath === \"test/path\")\n    assert(VectorUtils.toJavaMap(addFile.getPartitionValues).asScala.equals(Map(\"a\" -> \"1\")))\n    assert(addFile.getSize === 1L)\n    assert(addFile.getModificationTime === 10L)\n    assert(addFile.getDataChange === false)\n    assert(addFile.getDeletionVector === Optional.empty())\n    assert(VectorUtils.toJavaMap(addFile.getTags.get()).asScala.equals(Map(\"tag1\" -> \"value1\")))\n    assert(addFile.getBaseRowId === Optional.of(30L))\n    assert(addFile.getDefaultRowCommitVersion === Optional.of(40L))\n    // DataFileStatistics doesn't have an equals() override, so we need to compare the string\n    assert(addFile.getStats(null).get().serializeAsJson(null) === \"{\\\"numRecords\\\":100}\")\n    assert(addFile.getNumRecords === Optional.of(100L))\n  }\n\n  test(\"update a single field of an AddFile\") {\n    val addFileRow = generateTestAddFileRow(baseRowId = Option(1L))\n    val addFileAction = new AddFile(addFileRow)\n\n    val updatedAddFileAction = addFileAction.withNewBaseRowId(2L)\n    assert(updatedAddFileAction.getBaseRowId === Optional.of(2L))\n\n    val updatedAddFileRow = updatedAddFileAction.toRow\n    assert(new AddFile(updatedAddFileRow).getBaseRowId === Optional.of(2L))\n  }\n\n  test(\"update multiple fields of an AddFile multiple times\") {\n    val baseAddFileRow =\n      generateTestAddFileRow(\n        path = \"test/path\",\n        baseRowId = Option(0L),\n        defaultRowCommitVersion = Option(0L))\n    var addFileAction = new AddFile(baseAddFileRow)\n\n    (1L until 10L).foreach { i =>\n      addFileAction = addFileAction\n        .withNewBaseRowId(i)\n        .withNewDefaultRowCommitVersion(i * 10)\n\n      assert(addFileAction.getPath === \"test/path\")\n      assert(addFileAction.getBaseRowId === Optional.of(i))\n      assert(addFileAction.getDefaultRowCommitVersion === Optional.of(i * 10))\n    }\n  }\n\n  test(\"toString() prints all fields of AddFile\") {\n    Seq(true, false).foreach { dvPresent =>\n      val deletionVector = if (dvPresent) {\n        Some(new DeletionVectorDescriptor(\n          \"storage\",\n          \"s\",\n          Optional.of(1),\n          25,\n          35))\n      } else {\n        None\n      }\n\n      val addFileRow = generateTestAddFileRow(\n        path = \"test/path\",\n        partitionValues = Map(\"col1\" -> \"val1\"),\n        size = 100L,\n        modificationTime = 1234L,\n        dataChange = false,\n        tags = Option(Map(\"tag1\" -> \"value1\")),\n        baseRowId = Option(12345L),\n        defaultRowCommitVersion = Option(67890L),\n        stats = Option(\"{\\\"numRecords\\\":10000}\"),\n        deletionVector = deletionVector)\n\n      val addFile = new AddFile(addFileRow)\n\n      val deletionVectorString = if (dvPresent) {\n        \"Optional[DeletionVectorDescriptor(storageType=storage,\" +\n          \" pathOrInlineDv=s, offset=Optional[1], sizeInBytes=25, cardinality=35)]\"\n      } else {\n        \"Optional.empty\"\n      }\n\n      val expectedString = \"AddFile{\" +\n        \"path='test/path', \" +\n        \"partitionValues={col1=val1}, \" +\n        \"size=100, \" +\n        \"modificationTime=1234, \" +\n        \"dataChange=false, \" +\n        s\"deletionVector=$deletionVectorString, \" +\n        \"tags=Optional[{tag1=value1}], \" +\n        \"baseRowId=Optional[12345], \" +\n        \"defaultRowCommitVersion=Optional[67890], \" +\n        \"stats={\\\"numRecords\\\":10000}}\"\n\n      assert(addFile.toString == expectedString)\n    }\n  }\n\n  test(\"equals() compares AddFile instances correctly\") {\n    val addFileRow1 = generateTestAddFileRow(\n      path = \"test/path\",\n      size = 100L,\n      partitionValues = Map(\"a\" -> \"1\"),\n      baseRowId = Option(12345L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    // Create an identical AddFile\n    val addFileRow2 = generateTestAddFileRow(\n      path = \"test/path\",\n      size = 100L,\n      partitionValues = Map(\"a\" -> \"1\"),\n      baseRowId = Option(12345L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    // Create a AddFile with different path\n    val addFileRowDiffPath = generateTestAddFileRow(\n      path = \"different/path\",\n      size = 100L,\n      partitionValues = Map(\"a\" -> \"1\"),\n      baseRowId = Option(12345L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    // Create a AddFile with different partition values, which is handled specially in equals()\n    val addFileRowDiffPartition = generateTestAddFileRow(\n      path = \"test/path\",\n      size = 100L,\n      partitionValues = Map(\"x\" -> \"0\"),\n      baseRowId = Option(12345L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    // Create a AddFile with deletion vector value\n    val addFileRowDeletionVector = generateTestAddFileRow(\n      path = \"test/path\",\n      size = 100L,\n      partitionValues = Map(\"x\" -> \"0\"),\n      baseRowId = Option(12345L),\n      deletionVector = Some(\n        new DeletionVectorDescriptor(\n          \"storage\",\n          \"s\",\n          Optional.of(1),\n          25,\n          35)),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    val addFile1 = new AddFile(addFileRow1)\n    val addFile2 = new AddFile(addFileRow2)\n    val addFileDiffPath = new AddFile(addFileRowDiffPath)\n    val addFileDiffPartition = new AddFile(addFileRowDiffPartition)\n    val addFileDeletionVector = new AddFile(addFileRowDeletionVector)\n\n    // Test equality\n    assert(addFile1 === addFile2)\n    assert(addFile1 != addFileDiffPath)\n    assert(addFile1 != addFileDiffPartition)\n    assert(addFile2 != addFileDiffPath)\n    assert(addFile2 != addFileDiffPartition)\n    assert(addFileDiffPath != addFileDiffPartition)\n    assert(addFileDeletionVector != addFileDiffPartition)\n\n    // Test null and different type\n    assert(!addFile1.equals(null))\n    assert(!addFile1.equals(new DomainMetadata(\"domain\", \"config\", false)))\n  }\n\n  test(\"hashCode is consistent with equals\") {\n    val addFileRow1 = generateTestAddFileRow(\n      path = \"test/path\",\n      size = 100L,\n      partitionValues = Map(\"a\" -> \"1\"),\n      baseRowId = Option(12345L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    val addFileRow2 = generateTestAddFileRow(\n      path = \"test/path\",\n      size = 100L,\n      partitionValues = Map(\"a\" -> \"1\"),\n      baseRowId = Option(12345L),\n      stats = Option(\"{\\\"numRecords\\\":100}\"))\n\n    val addFile1 = new AddFile(addFileRow1)\n    val addFile2 = new AddFile(addFileRow2)\n\n    // Equal objects should have equal hash codes\n    assert(addFile1.hashCode === addFile2.hashCode)\n\n    // Hash code should be consistent across multiple calls\n    assert(addFile1.hashCode === addFile1.hashCode)\n  }\n\n  // Tests for toRemoveFileRow\n  test(\"toRemoveFileRow: handles AddFile with all required fields\") {\n    val addFile = new AddFile(generateTestAddFileRow(\n      path = \"/path/to/file\",\n      dataChange = false))\n\n    def verify(\n        result: RemoveFile,\n        expDataChange: Boolean,\n        expDeletionTimestamp: Option[Long]): Unit = {\n      assert(result.getPath === \"/path/to/file\")\n      if (expDeletionTimestamp.isDefined) {\n        assert(result.getDeletionTimestamp.get() === expDeletionTimestamp.get)\n      }\n      assert(result.getDataChange === expDataChange)\n      assert(result.getExtendedFileMetadata === Optional.of(true))\n      assert(VectorUtils.toJavaMap[String, String](result.getPartitionValues.get()).asScala ===\n        Map.empty[String, String])\n      assert(result.getSize === Optional.of(10L))\n      assert(result.getStatsJson === Optional.empty())\n      assert(result.getTags === Optional.empty())\n      assert(result.getBaseRowId === Optional.empty())\n      assert(result.getDefaultRowCommitVersion === Optional.empty())\n    }\n\n    val result1 = new RemoveFile(addFile.toRemoveFileRow(true, Optional.empty()))\n    verify(result1, expDataChange = true, expDeletionTimestamp = None)\n\n    val result2 = new RemoveFile(addFile.toRemoveFileRow(false, Optional.empty()))\n    verify(result2, expDataChange = false, expDeletionTimestamp = None)\n\n    val result3 = new RemoveFile(addFile.toRemoveFileRow(true, Optional.of(100L)))\n    verify(result3, expDataChange = true, expDeletionTimestamp = Some(100L))\n  }\n\n  test(\"toRemoveFileRow: handles AddFile with optional fields present\") {\n    val addFile = new AddFile(generateTestAddFileRow(\n      path = \"/path/to/file\",\n      partitionValues = Map(\"a\" -> \"1\"),\n      size = 100L,\n      modificationTime = 200L,\n      dataChange = true,\n      deletionVector = None,\n      tags = Some(Map(\"tag1\" -> \"value1\")),\n      baseRowId = Some(67890L),\n      defaultRowCommitVersion = Some(2823L),\n      stats = Some(\"{\\\"numRecords\\\":100}\")))\n\n    val result = new RemoveFile(addFile.toRemoveFileRow(false, Optional.of(200L)))\n\n    assert(result.getPath === \"/path/to/file\")\n    assert(VectorUtils.toJavaMap[String, String](result.getPartitionValues.get()).asScala ===\n      Map(\"a\" -> \"1\"))\n    assert(result.getSize === Optional.of(100L))\n    assert(result.getDeletionTimestamp === Optional.of(200L))\n    assert(result.getDataChange === false)\n    assert(result.getDeletionVector === Optional.empty())\n    assert(VectorUtils.toJavaMap[String, String](result.getTags.get()).asScala ===\n      Map[String, String](\"tag1\" -> \"value1\"))\n    assert(result.getBaseRowId === Optional.of(67890L))\n    assert(result.getDefaultRowCommitVersion === Optional.of(2823L))\n    assert(result.getStatsJson === Optional.of(\"{\\\"numRecords\\\":100}\"))\n  }\n\n  test(\"toRemoveFileRow: DV is converted properly\") {\n    val addFile = new AddFile(generateTestAddFileRow(\n      path = \"/path/to/file\",\n      partitionValues = Map(\"a\" -> \"1\"),\n      size = 100L,\n      modificationTime = 200L,\n      dataChange = true,\n      deletionVector = Some(\n        new DeletionVectorDescriptor(\n          \"storage\",\n          \"s\",\n          Optional.of(1),\n          25,\n          35)),\n      tags = Some(Map(\"tag1\" -> \"value1\")),\n      baseRowId = Some(67890L),\n      defaultRowCommitVersion = Some(2823L),\n      stats = Some(\"{\\\"numRecords\\\":100}\")))\n\n    val result = new RemoveFile(addFile.toRemoveFileRow(true, Optional.of(200L)))\n\n    assert(result.getPath === \"/path/to/file\")\n    assert(VectorUtils.toJavaMap[String, String](result.getPartitionValues.get()).asScala ===\n      Map[String, String](\"a\" -> \"1\"))\n    assert(result.getSize.get() === 100L)\n    assert(result.getDeletionTimestamp.get() === 200L)\n    assert(result.getDataChange === true)\n    assert(result.getDeletionVector === Optional.of(\n      new DeletionVectorDescriptor(\"storage\", \"s\", Optional.of(1), 25, 35)))\n    assert(VectorUtils.toJavaMap[String, String](result.getTags.get()).asScala ===\n      Map[String, String](\"tag1\" -> \"value1\"))\n    assert(result.getBaseRowId === Optional.of(67890L))\n    assert(result.getDefaultRowCommitVersion === Optional.of(2823L))\n    assert(result.getStatsJson === Optional.of(\"{\\\"numRecords\\\":100}\"))\n\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/actions/DeletionVectorDescriptorSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions\n\nimport java.io.{ByteArrayInputStream, DataInputStream}\nimport java.util.{Base64, Optional}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Tests for DeletionVectorDescriptor.\n */\nclass DeletionVectorDescriptorSuite extends AnyFunSuite {\n\n  // Test cases: (storageType, pathOrInlineDv, offset, sizeInBytes, cardinality)\n  private val testCases = Seq(\n    (\"u\", \"ab^-aqEH.-t@S}K{vb[*k^\", Some(4), 40, 2L),\n    (\"p\", \"path/to/dv.bin\", Some(100), 1024, 50L),\n    (\"i\", \"inline_data_here\", None, 16, 3L))\n\n  testCases.foreach { case (storageType, pathOrInlineDv, offset, sizeInBytes, cardinality) =>\n    test(s\"serializeToBase64 - $storageType storage type\") {\n      val dv = new DeletionVectorDescriptor(\n        storageType,\n        pathOrInlineDv,\n        offset.map(Integer.valueOf).map(Optional.of[Integer]).getOrElse(Optional.empty[Integer]()),\n        sizeInBytes,\n        cardinality)\n\n      val base64Result = dv.serializeToBase64()\n\n      // Decode and verify the serialization format\n      val bytes = Base64.getDecoder.decode(base64Result)\n      val dis = new DataInputStream(new ByteArrayInputStream(bytes))\n\n      assert(dis.readLong() === cardinality)\n      assert(dis.readInt() === sizeInBytes)\n      assert(dis.readByte().toChar.toString === storageType)\n\n      if (storageType != \"i\") {\n        assert(dis.readInt() === offset.get)\n      }\n\n      assert(dis.readUTF() === pathOrInlineDv)\n      dis.close()\n    }\n  }\n\n  // Regression test: isInline() must use .equals() not == for String comparison.\n  // Using `new String(...)` creates non-interned Strings that would fail with ==.\n  testCases.foreach { case (storageType, pathOrInlineDv, offset, sizeInBytes, cardinality) =>\n    test(s\"isInline with non-interned string - $storageType storage type\") {\n      val dv = new DeletionVectorDescriptor(\n        new String(storageType), // deliberately non-interned\n        pathOrInlineDv,\n        offset.map(Integer.valueOf).map(Optional.of[Integer]).getOrElse(Optional.empty[Integer]()),\n        sizeInBytes,\n        cardinality)\n\n      assert(dv.isInline() === (storageType == \"i\"))\n    }\n  }\n\n  test(\"serializeToBase64 throws for non-inline DV without offset\") {\n    val ex = intercept[IllegalArgumentException] {\n      val dv = new DeletionVectorDescriptor(\n        \"u\",\n        \"ab^-aqEH.-t@S}K{vb[*k^\",\n        Optional.empty[Integer](),\n        40,\n        2L)\n      dv.serializeToBase64()\n    }\n    assert(ex.getMessage.contains(\"Non-inline DV must have offset\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/actions/GenerateIcebergCompatActionUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.data.TransactionStateRow\nimport io.delta.kernel.internal.util.{ColumnMapping, VectorUtils}\nimport io.delta.kernel.statistics.DataFileStatistics\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\nimport io.delta.kernel.utils.DataFileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass GenerateIcebergCompatActionUtilsSuite extends AnyFunSuite {\n\n  import GenerateIcebergCompatActionUtilsSuite._\n\n  private def getTestTransactionStateRow(\n      tblProperties: Map[String, String],\n      maxRetries: Int = 0,\n      partitionColumns: Seq[String] = Seq.empty): Row = {\n\n    val metadata = new Metadata(\n      \"id\",\n      Optional.empty(), /* name */\n      Optional.empty(), /* description */\n      new Format(),\n      testSchema.toJson,\n      testSchema,\n      VectorUtils.buildArrayValue(partitionColumns.asJava, StringType.STRING),\n      Optional.empty(), /* createdTime */\n      VectorUtils.stringStringMapValue(tblProperties.asJava))\n    val protocol = new Protocol(\n      3, // minReaderVersion\n      7, // minWriterVersion to support table features\n      java.util.Collections.emptySet[String](), // readerFeatures\n      java.util.Collections.emptySet[String]() // writerFeatures\n    )\n\n    TransactionStateRow.of(\n      ColumnMapping.updateColumnMappingMetadataIfNeeded(metadata, true).orElse(metadata),\n      protocol,\n      testTablePath,\n      maxRetries)\n  }\n\n  /* ----- Error cases ----- */\n\n  private def testErrorAddAndRemove(\n      txnStateRow: Row,\n      dataFileStatus: DataFileStatus,\n      partitionValues: java.util.Map[String, Literal],\n      dataChange: Boolean,\n      expectedErrorMessageContains: String): Unit = {\n    assert(\n      intercept[UnsupportedOperationException] {\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1AddAction(\n          txnStateRow,\n          dataFileStatus,\n          partitionValues,\n          dataChange,\n          Optional.empty() /* Pre-parsed physicalSchema if present */ )\n      }.getMessage.contains(expectedErrorMessageContains))\n    assert(\n      intercept[UnsupportedOperationException] {\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1RemoveAction(\n          txnStateRow,\n          dataFileStatus,\n          partitionValues,\n          dataChange,\n          Optional.empty() /* Pre-parsed physicalSchema if present */ )\n      }.getMessage.contains(expectedErrorMessageContains))\n  }\n\n  test(\"GenerateIcebergCompatActionUtils requires maxRetries=0\") {\n    testErrorAddAndRemove(\n      getTestTransactionStateRow(compatibleTableProperties, maxRetries = 1),\n      testDataFileStatusWithStatistics,\n      partitionValues = Collections.emptyMap(),\n      dataChange = true,\n      \"GenerateIcebergCompatActionUtils requires maxRetries=0\")\n  }\n\n  test(\"GenerateIcebergCompatActionUtils requires icebergWriterCompatV1\") {\n    // Not set at all\n    testErrorAddAndRemove(\n      getTestTransactionStateRow(tblProperties = Map()),\n      testDataFileStatusWithStatistics,\n      partitionValues = Collections.emptyMap(),\n      dataChange = true,\n      \"only supported on tables with 'delta.enableIcebergWriterCompatV1' set to true\")\n    // Set to false\n    testErrorAddAndRemove(\n      getTestTransactionStateRow(tblProperties = Map(\n        TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"FALSE\",\n        TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\",\n        TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")),\n      testDataFileStatusWithStatistics,\n      partitionValues = Collections.emptyMap(),\n      dataChange = true,\n      \"only supported on tables with 'delta.enableIcebergWriterCompatV1' set to true\")\n  }\n\n  test(\"GenerateIcebergCompatActionUtils doesn't support partitioned tables\") {\n    testErrorAddAndRemove(\n      getTestTransactionStateRow(compatibleTableProperties, partitionColumns = Seq(\"id\")),\n      testDataFileStatusWithStatistics,\n      partitionValues = Map(\"id\" -> Literal.ofInt(1)).asJava,\n      dataChange = true,\n      \"GenerateIcebergCompatActionUtils is not supported for partitioned tables\")\n  }\n\n  test(\"GenerateIcebergCompatActionUtils requires statistics in add files\") {\n    assert(\n      intercept[KernelException] {\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1AddAction(\n          getTestTransactionStateRow(compatibleTableProperties),\n          testDataFileStatusWithoutStatistics,\n          Collections.emptyMap(), // partitionValues\n          true, // dataChange\n          Optional.empty() // Pre-parsed physical schema\n        )\n      }.getMessage.contains(\"icebergCompatV2 compatibility requires 'numRecords' statistic\"))\n  }\n\n  test(\"GenerateIcebergCompatActionUtils cannot create remove with dataChange=true \" +\n    \"for append-only table\") {\n    val txnStateRow = getTestTransactionStateRow(\n      compatibleTableProperties ++ Map(TableConfig.APPEND_ONLY_ENABLED.getKey -> \"true\"))\n    assert(\n      intercept[KernelException] {\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1RemoveAction(\n          txnStateRow,\n          testDataFileStatusWithStatistics,\n          Collections.emptyMap(), // partitionValues\n          true, // dataChange\n          Optional.of(TransactionStateRow.getPhysicalSchema(txnStateRow)))\n      }.getMessage.contains(\"Cannot modify append-only table\"))\n  }\n\n  /* ----- Valid cases ----- */\n\n  private def validateAddAction(\n      row: Row,\n      expectedPath: String,\n      // expectedPartitionValues - for now this is not supported as anything other than empty\n      expectedSize: Long,\n      expectedModificationTime: Long,\n      expectedDataChange: Boolean,\n      expectedStatsString: String): Unit = {\n    assert(row.getSchema == SingleAction.FULL_SCHEMA)\n    (0 until SingleAction.FULL_SCHEMA.length()).foreach { idx =>\n      if (idx == SingleAction.ADD_FILE_ORDINAL) {\n        assert(!row.isNullAt(idx))\n      } else {\n        assert(row.isNullAt(idx))\n      }\n    }\n    val addRow = row.getStruct(SingleAction.ADD_FILE_ORDINAL)\n    assert(addRow.getSchema == AddFile.FULL_SCHEMA)\n\n    val addFile = new AddFile(addRow)\n    assert(addFile.getPath == expectedPath)\n    assert(addFile.getPartitionValues.getSize == 0)\n    assert(addFile.getSize == expectedSize)\n    assert(addFile.getModificationTime == expectedModificationTime)\n    assert(addFile.getDataChange == expectedDataChange)\n    assert(!addFile.getTags.isPresent)\n    assert(!addFile.getBaseRowId.isPresent)\n    assert(!addFile.getDefaultRowCommitVersion.isPresent)\n    assert(!addFile.getDeletionVector.isPresent)\n    // We have to do our stats check differently since the AddFile::getStats API does not fully\n    // deserialize the statistics (only grabs the numRecords field)\n    assert(addRow.getString(AddFile.FULL_SCHEMA.indexOf(\"stats\")) == expectedStatsString)\n  }\n\n  private def validateRemoveAction(\n      row: Row,\n      expectedPath: String,\n      // expectedPartitionValues - for now this is not supported as anything other than empty\n      expectedSize: Long,\n      expectedDeletionTimestamp: Long,\n      expectedDataChange: Boolean,\n      expectedStatsString: Option[String]): Unit = {\n    assert(row.getSchema == SingleAction.FULL_SCHEMA)\n    (0 until SingleAction.FULL_SCHEMA.length()).foreach { idx =>\n      if (idx == SingleAction.REMOVE_FILE_ORDINAL) {\n        assert(!row.isNullAt(idx))\n      } else {\n        assert(row.isNullAt(idx))\n      }\n    }\n    val removeRow = row.getStruct(SingleAction.REMOVE_FILE_ORDINAL)\n    assert(removeRow.getSchema == RemoveFile.FULL_SCHEMA)\n\n    val removeFile = new RemoveFile(removeRow)\n    assert(removeFile.getPath == expectedPath)\n    assert(removeFile.getDeletionTimestamp.isPresent &&\n      removeFile.getDeletionTimestamp.get == expectedDeletionTimestamp)\n    assert(removeFile.getDataChange == expectedDataChange)\n    assert(removeFile.getExtendedFileMetadata.isPresent && removeFile.getExtendedFileMetadata.get)\n    assert(\n      removeFile.getPartitionValues.isPresent && removeFile.getPartitionValues.get.getSize == 0)\n    assert(removeFile.getSize.isPresent && removeFile.getSize.get == expectedSize)\n    if (expectedStatsString.nonEmpty) {\n      // We have to do our stats check differently since the RemoveFile::getStats API does not fully\n      // deserialize the statistics (only grabs the numRecords field)\n      assert(\n        removeRow.getString(RemoveFile.FULL_SCHEMA.indexOf(\"stats\")) == expectedStatsString.get)\n    } else {\n      assert(removeRow.isNullAt(RemoveFile.FULL_SCHEMA.indexOf(\"stats\")))\n    }\n    assert(!removeFile.getTags.isPresent)\n    assert(!removeFile.getDeletionVector.isPresent)\n    assert(!removeFile.getBaseRowId.isPresent)\n    assert(!removeFile.getDefaultRowCommitVersion.isPresent)\n  }\n\n  test(\"generateIcebergCompatWriterV1AddAction creates correct add row\") {\n    Seq(true, false).foreach { dataChange =>\n      val txnRow = getTestTransactionStateRow(compatibleTableProperties)\n      val statsString = testDataFileStatusWithStatistics.getStatistics.get\n        .serializeAsJson(TransactionStateRow.getPhysicalSchema(txnRow))\n      validateAddAction(\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1AddAction(\n          txnRow,\n          testDataFileStatusWithStatistics,\n          Collections.emptyMap(), // partitionValues\n          dataChange,\n          Optional.empty() /* Pre-parsed physicalSchema if present */ ),\n        expectedPath = \"file1.parquet\",\n        expectedSize = 1000,\n        expectedModificationTime = 10,\n        expectedDataChange = dataChange,\n        expectedStatsString = statsString)\n    }\n  }\n\n  test(\"generateIcebergCompatWriterV1AddAction creates correct remove row\") {\n    Seq(true, false).foreach { dataChange =>\n      // RemoveFiles are allowed to be missing statistics (where as AddFiles are not)\n      Seq(testDataFileStatusWithStatistics, testDataFileStatusWithoutStatistics).foreach {\n        fileStatus =>\n          val txnRow = getTestTransactionStateRow(compatibleTableProperties)\n          val statsString = if (fileStatus.getStatistics.isPresent) {\n            Some(fileStatus.getStatistics.get\n              .serializeAsJson(TransactionStateRow.getPhysicalSchema(txnRow)))\n          } else {\n            None\n          }\n          validateRemoveAction(\n            GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1RemoveAction(\n              txnRow,\n              fileStatus,\n              Collections.emptyMap(), // partitionValues\n              dataChange,\n              Optional.empty() /* Pre-parsed physicalSchema if present */ ),\n            expectedPath = \"file1.parquet\",\n            expectedSize = 1000,\n            expectedDeletionTimestamp = 10,\n            expectedDataChange = dataChange,\n            expectedStatsString = statsString)\n      }\n    }\n  }\n}\n\nobject GenerateIcebergCompatActionUtilsSuite {\n\n  private val testDataFileStatusWithStatistics = new DataFileStatus(\n    \"/test/table/path/file1.parquet\",\n    1000,\n    10,\n    Optional.of(\n      new DataFileStatistics(\n        100,\n        Map(new Column(\"id\") -> Literal.ofInt(0)).asJava,\n        Map(new Column(\"id\") -> Literal.ofInt(10)).asJava,\n        Map(new Column(\"id\") -> java.lang.Long.valueOf(0)).asJava,\n        Optional.empty())))\n\n  private val testDataFileStatusWithoutStatistics = new DataFileStatus(\n    \"/test/table/path/file1.parquet\",\n    1000,\n    10,\n    Optional.empty())\n\n  private val testSchema = new StructType()\n    .add(\"id\", IntegerType.INTEGER)\n    .add(\"comment\", StringType.STRING)\n\n  private val testTablePath = \"/test/table/path\"\n\n  private val compatibleTableProperties = Map(\n    TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\",\n    TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\",\n    TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/actions/MetadataSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ArrayValue, ColumnVector, MapValue}\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.data.GenericRow\nimport io.delta.kernel.internal.util.InternalUtils.singletonStringColumnVector\nimport io.delta.kernel.internal.util.VectorUtils.buildColumnVector\nimport io.delta.kernel.test.TestUtils\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass MetadataSuite extends AnyFunSuite with TestUtils {\n\n  test(\"withMergedConfig upserts values\") {\n    val metadata = testMetadata(Map(\"a\" -> \"b\", \"f\" -> \"g\"))\n\n    val newMetadata = metadata.withMergedConfiguration(Map(\"a\" -> \"c\", \"d\" -> \"f\").asJava)\n\n    assert(newMetadata.getConfiguration.equals(Map(\"a\" -> \"c\", \"d\" -> \"f\", \"f\" -> \"g\").asJava))\n  }\n\n  test(\"withReplacedConfiguration replaces values\") {\n    val metadata = testMetadata(Map(\"a\" -> \"b\", \"f\" -> \"g\"))\n\n    val newMetadata = metadata.withReplacedConfiguration(Map(\"a\" -> \"c\", \"d\" -> \"f\").asJava)\n\n    assert(newMetadata.getConfiguration.equals(Map(\"a\" -> \"c\", \"d\" -> \"f\").asJava))\n  }\n\n  private val defaultTestSchema = new StructType()\n    .add(\"c1\", IntegerType.INTEGER)\n    .add(\"c2\", StringType.STRING)\n\n  def testMetadata(\n      tblProps: Map[String, String] = Map.empty,\n      schemaString: String = defaultTestSchema.toJson): Metadata = {\n    val partitionCols = new ArrayValue() {\n      override def getSize = 1\n      override def getElements: ColumnVector = singletonStringColumnVector(\"c3\")\n    }\n    val conf = new MapValue() {\n      override def getSize = tblProps.size\n      override def getKeys: ColumnVector =\n        buildColumnVector(tblProps.toSeq.map(_._1).asJava, StringType.STRING)\n      override def getValues: ColumnVector =\n        buildColumnVector(tblProps.toSeq.map(_._2).asJava, StringType.STRING)\n    }\n    val values = new java.util.HashMap[Integer, Object]()\n    values.put(0, \"id\")\n    values.put(1, \"name\")\n    values.put(2, \"description\")\n    values.put(3, new Format(\"parquet\", Collections.emptyMap()).toRow)\n    values.put(4, schemaString)\n    values.put(5, partitionCols)\n    values.put(6, null) // createdTime\n    values.put(7, conf)\n    Metadata.fromRow(new GenericRow(Metadata.FULL_SCHEMA, values))\n  }\n\n  test(\"schema parsing is lazy - void type does not block non-schema access\") {\n    val voidSchemaJson = \"\"\"{\"type\":\"struct\",\"fields\":[\"\"\" +\n      \"\"\"{\"name\":\"x\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},\"\"\" +\n      \"\"\"{\"name\":\"y\",\"type\":\"void\",\"nullable\":true,\"metadata\":{}}]}\"\"\"\n\n    val metadata = testMetadata(schemaString = voidSchemaJson)\n\n    // Non-schema methods should work without triggering schema parsing\n    assert(metadata.getId === \"id\")\n    assert(metadata.getConfiguration.isEmpty)\n    assert(metadata.getSchemaString === voidSchemaJson)\n\n    // Accessing the schema should throw KernelException due to VOID type\n    val e = intercept[KernelException] {\n      metadata.getSchema\n    }\n    assert(e.getMessage.contains(\"VOID\"))\n  }\n\n  test(\"Metadata serialization round trip\") {\n    val source = testMetadata(Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\"))\n    val deserialized = roundTripSerialize(source)\n\n    // Verify all public methods return the same values\n    assert(deserialized.getId === source.getId)\n    assert(deserialized.getName === source.getName)\n    assert(deserialized.getDescription === source.getDescription)\n    assert(deserialized.getFormat === source.getFormat)\n    assert(deserialized.getSchemaString === source.getSchemaString)\n    assert(deserialized.getSchema === source.getSchema)\n    assert(deserialized.getCreatedTime === source.getCreatedTime)\n    assert(deserialized.getConfiguration === source.getConfiguration)\n    assert(deserialized.getPartitionColNames === source.getPartitionColNames)\n    assert(deserialized.getDataSchema === source.getDataSchema)\n    assert(deserialized.getPhysicalSchema === source.getPhysicalSchema)\n\n    // Verify equals and hashCode\n    assert(deserialized === source)\n    assert(deserialized.hashCode() === source.hashCode())\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/actions/ProtocolSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.internal.data.GenericRow\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.test.TestUtils\nimport io.delta.kernel.types.{ArrayType, IntegerType, StringType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ProtocolSuite extends AnyFunSuite with TestUtils {\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for TableFeature related methods on Protocol                                          //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Invalid protocol versions/features throw validation errors\n  Seq(\n    // Test format:\n    // minReaderVersion, minWriterVersion, readerFeatures, writerFeatures, expectedErrorMsg\n    (0, 1, Set(), Set(), \"minReaderVersion should be at least 1\"),\n    (1, 0, Set(), Set(), \"minWriterVersion should be at least 1\"),\n    (\n      // writer version doesn't support writer features\n      1,\n      2,\n      Set(\"columnMapping\"),\n      Set(),\n      \"Reader features are not supported for the reader version: 1\"),\n    (\n      // writer version doesn't support writer features\n      1,\n      2,\n      Set(),\n      Set(\"columnMapping\"),\n      \"Writer features are not supported for the writer version: 2\"),\n    // you can't have reader version with feature support, but not the writer version\n    (3, 5, Set(), Set(), \"writer version doesn't support writer features: 5\"),\n    // columnMapping feature is not supported for reader version 1\n    (1, 5, Set(), Set(), \"Reader version 1 does not support readerWriter feature columnMapping\"),\n    (\n      // readerWriter feature columnMapping is missing from the readerFeatures set\n      3,\n      7,\n      Set(),\n      Set(\"columnMapping\"),\n      \"ReaderWriter feature columnMapping is not present in readerFeatures\"),\n    // minReaderVersion doesn't support readerWriter feature columnMapping requirement\n    (\n      1,\n      7,\n      Set(),\n      Set(\"columnMapping\"),\n      \"Reader version 1 does not support readerWriter feature columnMapping\")).foreach {\n    case (\n          readerVersion,\n          writerVersion,\n          readerFeatures: Set[String],\n          writerFeatures: Set[String],\n          expectedError) =>\n      test(s\"Invalid protocol versions \" +\n        s\"($readerVersion, $writerVersion, $readerFeatures, $writerFeatures)\") {\n        val protocol =\n          new Protocol(readerVersion, writerVersion, readerFeatures.asJava, writerFeatures.asJava)\n        val e = intercept[IllegalArgumentException] {\n          protocol.validate()\n        }\n        assert(e.getMessage === expectedError)\n      }\n  }\n\n  // Tests for getImplicitlySupportedFeatures, getExplicitlySupportedFeatures and\n  // getImplicitlyAndExplicitlySupportedFeatures\n  Seq(\n    // Test format:\n    // (minReaderVersion, minWriterVersion, expected features)\n    (1, 1, Set()),\n    (1, 2, Set(\"appendOnly\", \"invariants\")),\n    (1, 3, Set(\"appendOnly\", \"invariants\", \"checkConstraints\")),\n    (\n      1,\n      4,\n      Set(\"appendOnly\", \"invariants\", \"checkConstraints\", \"changeDataFeed\", \"generatedColumns\")),\n    (\n      2,\n      5,\n      Set(\n        \"appendOnly\",\n        \"invariants\",\n        \"checkConstraints\",\n        \"changeDataFeed\",\n        \"generatedColumns\",\n        \"columnMapping\")),\n    (\n      2,\n      6,\n      Set(\n        \"appendOnly\",\n        \"invariants\",\n        \"checkConstraints\",\n        \"changeDataFeed\",\n        \"generatedColumns\",\n        \"columnMapping\",\n        \"identityColumns\"))).foreach {\n    case (minReaderVersion, minWriterVersion, expectedFeatures) =>\n      test(s\"getImplicitlySupportedFeatures with minReaderVersion $minReaderVersion and \" +\n        s\"minWriterVersion $minWriterVersion\") {\n        val protocol = new Protocol(minReaderVersion, minWriterVersion)\n        assert(\n          protocol.getImplicitlySupportedFeatures.asScala.map(_.featureName()) === expectedFeatures)\n\n        assert(\n          protocol.getImplicitlyAndExplicitlySupportedFeatures.asScala.map(_.featureName()) ===\n            expectedFeatures)\n\n        assert(\n          protocol.getExplicitlySupportedFeatures.asScala.map(_.featureName()) === Set())\n      }\n  }\n\n  Seq(\n    // Test format: readerFeatures, writerFeatures, expected set\n    (Set(), Set(), Set()),\n    (Set(), Set(\"rowTracking\"), Set(\"rowTracking\")),\n    (Set(), Set(\"checkConstraints\", \"rowTracking\"), Set(\"checkConstraints\", \"rowTracking\")),\n    (\n      Set(\"columnMapping\"),\n      Set(\"columnMapping\", \"domainMetadata\"),\n      Set(\"columnMapping\", \"domainMetadata\"))).foreach {\n    case (\n          readerFeatures: Set[String],\n          writerFeatures: Set[String],\n          expectedFeatureSet: Set[String]) =>\n      test(s\"getExplicitlySupportedFeatures $readerFeatures $writerFeatures\") {\n        val protocol = new Protocol(3, 7, readerFeatures.asJava, writerFeatures.asJava)\n        assert(\n          protocol.getExplicitlySupportedFeatures.asScala.map(_.featureName()) ===\n            expectedFeatureSet)\n\n        assert(\n          protocol.getImplicitlyAndExplicitlySupportedFeatures.asScala.map(_.featureName()) ===\n            expectedFeatureSet)\n\n        assert(protocol.getImplicitlySupportedFeatures.asScala.map(_.featureName()) === Set())\n      }\n  }\n\n  // Tests for `normalized\n  Seq(\n    // Test format: input, expected output out of the `normalized`\n\n    // If the protocol has no table features, then the normalized shouldn't change\n    (1, 1, Set[String](), Set[String]()) -> (1, 1, Set[String](), Set[String]()),\n    (1, 2, Set[String](), Set[String]()) -> (1, 2, Set[String](), Set[String]()),\n    (2, 5, Set[String](), Set[String]()) -> (2, 5, Set[String](), Set[String]()),\n\n    // If the protocol has table features, then the normalized may or\n    // may not have the table features\n    (3, 7, Set[String](), Set(\"appendOnly\", \"invariants\")) ->\n      (1, 2, Set[String](), Set[String]()),\n    (3, 7, Set[String](), Set(\"appendOnly\", \"invariants\", \"checkConstraints\")) ->\n      (1, 3, Set[String](), Set[String]()),\n    (\n      3,\n      7,\n      Set[String](),\n      Set(\"appendOnly\", \"invariants\", \"checkConstraints\", \"changeDataFeed\", \"generatedColumns\")) ->\n      (1, 4, Set[String](), Set[String]()),\n    (\n      3,\n      7,\n      Set(\"columnMapping\"),\n      Set(\n        \"appendOnly\",\n        \"invariants\",\n        \"checkConstraints\",\n        \"changeDataFeed\",\n        \"generatedColumns\",\n        \"columnMapping\")) ->\n      (2, 5, Set[String](), Set[String]()),\n\n    // reader version is downgraded\n    // can't downgrade the writer version, because version 2 (appendOnly) also has support for\n    // invariants which is not supported in the writer features in the input\n    (1, 7, Set[String](), Set(\"appendOnly\")) -> (1, 7, Set[String](), Set[String](\"appendOnly\")),\n    (3, 7, Set(\"columnMapping\"), Set(\"columnMapping\")) ->\n      (2, 7, Set[String](), Set(\"columnMapping\")),\n    (3, 7, Set(\"columnMapping\"), Set(\"columnMapping\", \"domainMetadata\")) ->\n      (2, 7, Set[String](), Set(\"columnMapping\", \"domainMetadata\"))).foreach {\n    case (\n          (readerVersion, writerVersion, readerFeatures, writerFeatures),\n          (\n            expReaderVersion,\n            expWriterVersion,\n            expReaderFeatures,\n            expWriterFeatures)) =>\n      test(s\"normalized $readerVersion $writerVersion $readerFeatures $writerFeatures\") {\n        val protocol =\n          new Protocol(readerVersion, writerVersion, readerFeatures.asJava, writerFeatures.asJava)\n        val normalized = protocol.normalized()\n        assert(normalized.getMinReaderVersion === expReaderVersion)\n        assert(normalized.getMinWriterVersion === expWriterVersion)\n        assert(normalized.getReaderFeatures.asScala === expReaderFeatures)\n        assert(normalized.getWriterFeatures.asScala === expWriterFeatures)\n      }\n  }\n\n  // Tests for `denormalized`\n  Seq(\n    // Test format: input, expected output out of the `denormalized`\n    (1, 1, Set[String](), Set[String]()) -> (1, 7, Set[String](), Set[String]()),\n    (1, 2, Set[String](), Set[String]()) -> (1, 7, Set[String](), Set(\"appendOnly\", \"invariants\")),\n    (2, 5, Set[String](), Set[String]()) -> (\n      2,\n      7,\n      Set[String](),\n      Set(\n        \"appendOnly\",\n        \"invariants\",\n        \"checkConstraints\",\n        \"changeDataFeed\",\n        \"generatedColumns\",\n        \"columnMapping\")),\n\n    // invalid protocol versions (2, 3)\n    (2, 3, Set[String](), Set[String]()) -> (\n      1,\n      7,\n      Set[String](),\n      Set(\"appendOnly\", \"invariants\", \"checkConstraints\")),\n\n    // shouldn't change the protocol already has the table feature set support\n    (3, 7, Set[String](), Set(\"appendOnly\", \"invariants\")) ->\n      (3, 7, Set[String](), Set(\"appendOnly\", \"invariants\")),\n    (3, 7, Set[String](), Set(\"appendOnly\", \"invariants\", \"checkConstraints\")) ->\n      (3, 7, Set[String](), Set(\"appendOnly\", \"invariants\", \"checkConstraints\"))).foreach {\n    case (\n          (readerVersion, writerVersion, readerFeatures, writerFeatures),\n          (\n            expReaderVersion,\n            expWriterVersion,\n            expReaderFeatures,\n            expWriterFeatures)) =>\n      test(s\"denormalized $readerVersion $writerVersion $readerFeatures $writerFeatures\") {\n        val protocol =\n          new Protocol(readerVersion, writerVersion, readerFeatures.asJava, writerFeatures.asJava)\n        val denormalized = protocol.denormalized()\n        assert(denormalized.getMinReaderVersion === expReaderVersion)\n        assert(denormalized.getMinWriterVersion === expWriterVersion)\n        assert(denormalized.getReaderFeatures.asScala === expReaderFeatures)\n        assert(denormalized.getWriterFeatures.asScala === expWriterFeatures)\n      }\n  }\n\n  // Tests for `withFeature` and `normalized`\n  Seq(\n    // can't downgrade the writer version, because version 2 (appendOnly) also has support for\n    // invariants which is not supported in the writer features in the input\n    Set(\"appendOnly\") -> (1, 7, Set[String](), Set(\"appendOnly\")),\n    Set(\"invariants\") -> (1, 7, Set[String](), Set[String](\"invariants\")),\n    Set(\"appendOnly\", \"invariants\") -> (1, 2, Set[String](), Set[String]()),\n    Set(\"checkConstraints\") -> (1, 7, Set[String](), Set(\"checkConstraints\")),\n    Set(\"changeDataFeed\") -> (1, 7, Set[String](), Set(\"changeDataFeed\")),\n    Set(\"appendOnly\", \"invariants\", \"checkConstraints\") -> (1, 3, Set[String](), Set[String]()),\n    Set(\"generatedColumns\") -> (1, 7, Set[String](), Set(\"generatedColumns\")),\n    Set(\"columnMapping\") -> (2, 7, Set(), Set(\"columnMapping\")),\n    Set(\"identityColumns\") -> (1, 7, Set[String](), Set[String](\"identityColumns\")),\n\n    // expect the dependency features also to be supported\n    Set(\"icebergCompatV2\") ->\n      (2, 7, Set[String](), Set[String](\"icebergCompatV2\", \"columnMapping\")),\n    Set(\"variantShredding-preview\") -> (\n      3,\n      7,\n      Set[String](\"variantType\", \"variantShredding-preview\"),\n      Set[String](\"variantType\", \"variantShredding-preview\")),\n    Set(\"variantShredding\") -> (\n      3,\n      7,\n      Set[String](\"variantType\", \"variantShredding\"),\n      Set[String](\"variantType\", \"variantShredding\")),\n    Set(\"rowTracking\") -> (\n      1,\n      7,\n      Set[String](),\n      Set[String](\"rowTracking\", \"domainMetadata\"))).foreach {\n    case (features, (expReaderVersion, expWriterVersion, expReaderFeatures, expWriterFeatures)) =>\n      test(s\"withFeature $features\") {\n        val protocol = new Protocol(3, 7)\n        val updated = protocol\n          .withFeatures(features.map(TableFeatures.getTableFeature).asJava)\n          .normalized()\n        assert(updated.getMinReaderVersion === expReaderVersion)\n        assert(updated.getMinWriterVersion === expWriterVersion)\n        assert(updated.getReaderFeatures.asScala === expReaderFeatures)\n        assert(updated.getWriterFeatures.asScala === expWriterFeatures)\n      }\n  }\n\n  test(\"withFeature - can't add a feature at the current version\") {\n    val protocol = new Protocol(1, 2)\n    val e = intercept[UnsupportedOperationException] {\n      protocol.withFeatures(Set(TableFeatures.getTableFeature(\"columnMapping\")).asJava)\n    }\n    assert(e.getMessage === \"TableFeature requires higher reader protocol version\")\n  }\n\n  // Tests for `merge` (also tests denormalized and normalized)\n  Seq(\n    // Test format: (protocol1, protocol2) -> expected merged protocol\n    (\n      (1, 1, Set[String](), Set[String]()),\n      (1, 2, Set[String](), Set[String]())) ->\n      (1, 2, Set[String](), Set[String]()),\n    ((1, 2, Set[String](), Set[String]()), (1, 3, Set[String](), Set[String]())) ->\n      (1, 3, Set[String](), Set[String]()),\n    ((1, 4, Set[String](), Set[String]()), (2, 5, Set[String](), Set[String]())) ->\n      (2, 5, Set[String](), Set[String]()),\n    ((1, 4, Set[String](), Set[String]()), (2, 6, Set[String](), Set[String]())) ->\n      (2, 6, Set[String](), Set[String]()),\n    ((1, 2, Set[String](), Set[String]()), (1, 7, Set[String](), Set(\"invariants\"))) ->\n      (1, 2, Set[String](), Set[String]()),\n    ((1, 2, Set[String](), Set[String]()), (3, 7, Set(\"columnMapping\"), Set(\"columnMapping\"))) ->\n      (2, 7, Set[String](), Set(\"columnMapping\", \"invariants\", \"appendOnly\")),\n    (\n      (1, 2, Set[String](), Set[String]()),\n      (3, 7, Set(\"columnMapping\"), Set(\"columnMapping\", \"domainMetadata\"))) ->\n      (2, 7, Set[String](), Set(\"domainMetadata\", \"columnMapping\", \"invariants\", \"appendOnly\")),\n    (\n      (2, 5, Set[String](), Set[String]()),\n      (3, 7, Set(\"v2Checkpoint\"), Set(\"v2Checkpoint\", \"domainMetadata\"))) ->\n      (\n        3,\n        7,\n        Set(\"columnMapping\", \"v2Checkpoint\"),\n        Set(\n          \"domainMetadata\",\n          \"columnMapping\",\n          \"v2Checkpoint\",\n          \"invariants\",\n          \"appendOnly\",\n          \"checkConstraints\",\n          \"changeDataFeed\",\n          \"generatedColumns\"))).foreach({\n    case (\n          (\n            (readerVersion1, writerVersion1, readerFeatures1, writerFeatures1),\n            (readerVersion2, writerVersion2, readerFeatures2, writerFeatures2)),\n          (expReaderVersion, expWriterVersion, expReaderFeatures, expWriterFeatures)) =>\n      test(s\"merge $readerVersion1 $writerVersion1 $readerFeatures1 $writerFeatures1 \" +\n        s\"$readerVersion2 $writerVersion2 $readerFeatures2 $writerFeatures2\") {\n        val protocol1 =\n          new Protocol(\n            readerVersion1,\n            writerVersion1,\n            readerFeatures1.asJava,\n            writerFeatures1.asJava)\n        val protocol2 = new Protocol(\n          readerVersion2,\n          writerVersion2,\n          readerFeatures2.asJava,\n          writerFeatures2.asJava)\n        val merged = protocol1.merge(protocol2)\n        assert(merged.getMinReaderVersion === expReaderVersion)\n        assert(merged.getMinWriterVersion === expWriterVersion)\n        assert(merged.getReaderFeatures.asScala === expReaderFeatures)\n        assert(merged.getWriterFeatures.asScala === expWriterFeatures)\n      }\n  })\n\n  test(\"extract protocol from the row representation\") {\n    val ordinalToValue: Map[Integer, Object] = Map(\n      Integer.valueOf(0) -> Integer.valueOf(42),\n      Integer.valueOf(1) -> Integer.valueOf(43),\n      Integer.valueOf(2) -> VectorUtils.buildArrayValue(\n        List(\"foo\").asJava,\n        StringType.STRING).asInstanceOf[Object],\n      Integer.valueOf(3) -> VectorUtils.buildArrayValue(\n        List(\"bar\").asJava,\n        StringType.STRING).asInstanceOf[Object])\n    val row = new GenericRow(\n      new StructType().add(\"minReaderVersion\", IntegerType.INTEGER)\n        .add(\"minWriterVersion\", IntegerType.INTEGER)\n        .add(\"readerFeatures\", new ArrayType(StringType.STRING, true))\n        .add(\"writerFeatures\", new ArrayType(StringType.STRING, true)),\n      ordinalToValue.asJava)\n\n    val expected = new Protocol(42, 43, Set(\"foo\").asJava, Set(\"bar\").asJava)\n    assert(Protocol.fromRow(row) === expected)\n  }\n\n  test(\"Protocol serialization round trip\") {\n    val source = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\", \"v2Checkpoint\").asJava,\n      Set(\"columnMapping\", \"domainMetadata\").asJava)\n    val deserialized = roundTripSerialize(source)\n\n    // Verify all public methods return the same values\n    assert(deserialized.getMinReaderVersion === source.getMinReaderVersion)\n    assert(deserialized.getMinWriterVersion === source.getMinWriterVersion)\n    assert(deserialized.getReaderFeatures === source.getReaderFeatures)\n    assert(deserialized.getWriterFeatures === source.getWriterFeatures)\n    assert(deserialized.getReaderAndWriterFeatures === source.getReaderAndWriterFeatures)\n    assert(deserialized.supportsReaderFeatures() === source.supportsReaderFeatures())\n    assert(deserialized.supportsWriterFeatures() === source.supportsWriterFeatures())\n    assert(deserialized.getImplicitlySupportedFeatures === source.getImplicitlySupportedFeatures)\n    assert(deserialized.getExplicitlySupportedFeatures === source.getExplicitlySupportedFeatures)\n    assert(deserialized.getImplicitlyAndExplicitlySupportedFeatures ===\n      source.getImplicitlyAndExplicitlySupportedFeatures)\n\n    // Verify equals and hashCode\n    assert(deserialized === source)\n    assert(deserialized.hashCode() === source.hashCode())\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for supportsFeature                                                                  //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  test(\"supportsFeature - legacy protocol with readerVersion=1, writerVersion=2\") {\n    // Protocol (1, 2) implicitly supports appendOnly and invariants\n    val protocol = new Protocol(1, 2)\n\n    // appendOnly is a writer-only feature with minWriterVersion = 2\n    assert(protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n    // invariants is a writer-only feature with minWriterVersion = 2\n    assert(protocol.supportsFeature(TableFeatures.INVARIANTS_W_FEATURE))\n    // checkConstraints is a writer-only feature with minWriterVersion = 3\n    assert(!protocol.supportsFeature(TableFeatures.CONSTRAINTS_W_FEATURE))\n    // columnMapping is a reader-writer feature with minReaderVersion = 2, minWriterVersion = 5\n    assert(!protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n  }\n\n  test(\"supportsFeature - legacy protocol with readerVersion=2, writerVersion=5\") {\n    // Protocol (2, 5) implicitly supports columnMapping (and other legacy writer features)\n    val protocol = new Protocol(2, 5)\n\n    // columnMapping is a reader-writer feature with minReaderVersion = 2, minWriterVersion = 5\n    assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n    // changeDataFeed is a writer feature with minWriterVersion = 4\n    assert(protocol.supportsFeature(TableFeatures.CHANGE_DATA_FEED_W_FEATURE))\n    // identityColumns is a writer-only feature with minWriterVersion = 6\n    assert(!protocol.supportsFeature(TableFeatures.IDENTITY_COLUMNS_W_FEATURE))\n  }\n\n  test(\"supportsFeature - protocol with table features support\") {\n    // Protocol (3, 7) with explicit features\n    val protocol = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\", \"v2Checkpoint\").asJava,\n      Set(\"columnMapping\", \"domainMetadata\", \"rowTracking\").asJava)\n\n    // Features explicitly listed\n    assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.CHECKPOINT_V2_RW_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.ROW_TRACKING_W_FEATURE))\n\n    // Features not listed\n    assert(!protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.DELETION_VECTORS_RW_FEATURE))\n  }\n\n  test(\"supportsFeature - protocol with only writer features (and legacy reader version)\") {\n    // Protocol (1, 7) with only writer features\n    val protocol = new Protocol(\n      1,\n      7,\n      Set().asJava,\n      Set(\"appendOnly\", \"invariants\", \"domainMetadata\").asJava)\n\n    // Writer features listed\n    assert(protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.INVARIANTS_W_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE))\n\n    // Writer features not listed\n    assert(!protocol.supportsFeature(TableFeatures.ROW_TRACKING_W_FEATURE))\n    // Reader-writer features not listed (reader version too low)\n    assert(!protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.DELETION_VECTORS_RW_FEATURE))\n  }\n\n  test(\"supportsFeature - doesn't throw on unknown writer feature when checking reader feature\") {\n    // Protocol with unknown writer features in the set\n    val protocol = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\").asJava,\n      Set(\"columnMapping\", \"unknownWriterFeature\").asJava)\n\n    // Check a reader-writer feature that is present - should not throw\n    assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n\n    // Check a reader-writer feature that is not present - should not throw, just return false\n    assert(!protocol.supportsFeature(TableFeatures.CHECKPOINT_V2_RW_FEATURE))\n  }\n\n  test(\"supportsFeature - doesn't throw on unknown features in reader or writer list\") {\n    // Protocol with unknown features in both reader and writer feature sets\n    val protocol = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\", \"unknownReaderWriterFeature\").asJava,\n      Set(\n        \"columnMapping\",\n        \"domainMetadata\",\n        \"unknownReaderWriterFeature\",\n        \"unknownWriterFeature\").asJava)\n\n    // Check reader-writer features - should not throw\n    assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.CHECKPOINT_V2_RW_FEATURE))\n\n    // Check writer features - should not throw\n    assert(protocol.supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.ROW_TRACKING_W_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n  }\n\n  test(\"supportsFeature - empty feature sets\") {\n    // Protocol (3, 7) with empty feature sets\n    val protocol = new Protocol(3, 7, Set().asJava, Set().asJava)\n\n    // No features should be supported\n    assert(!protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n    assert(!protocol.supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/actions/RemoveFileSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.actions\n\nimport java.lang.{Long => JLong}\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.internal.util.VectorUtils.stringStringMapValue\nimport io.delta.kernel.statistics.DataFileStatistics\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass RemoveFileSuite extends AnyFunSuite {\n  // For now we use GenerateIcebergCompatActionUtils::createRemoveFileRowWithExtendedFileMetadata\n  // because this is the only path we support creating RemoveFile rows currently. In the future when\n  // we implement broader support for RemoveFiles we should use the more generic methods to create\n  // the test rows\n  private def createTestRemoveFileRow(\n      path: String,\n      deletionTimestamp: Long,\n      dataChange: Boolean,\n      partitionValues: Map[String, String],\n      size: Long,\n      stats: Option[String],\n      baseRowId: Option[Long] = Option.empty,\n      defaultRowCommitVersion: Option[Long] = Option.empty,\n      deletionVectorDescriptor: Option[DeletionVectorDescriptor]): Row = {\n    def toJavaOptional[T](option: Option[T]): Optional[T] = option match {\n      case Some(value) => Optional.of(value)\n      case None => Optional.empty()\n    }\n    GenerateIcebergCompatActionUtils.createRemoveFileRowWithExtendedFileMetadata(\n      path,\n      deletionTimestamp,\n      dataChange,\n      stringStringMapValue(partitionValues.asJava),\n      size,\n      DataFileStatistics.deserializeFromJson(stats.getOrElse(\"\"), null),\n      null, // physicalSchema\n      toJavaOptional(baseRowId.asInstanceOf[Option[JLong]]),\n      toJavaOptional(defaultRowCommitVersion.asInstanceOf[Option[JLong]]),\n      deletionVectorDescriptor match {\n        case Some(dvd) => Optional.of(dvd)\n        case None => Optional.empty[DeletionVectorDescriptor]()\n      })\n  }\n\n  test(\"getters can read RemoveFile's fields from the backing row\") {\n    val deletionVectorDescriptor = new DeletionVectorDescriptor(\n      \"storage\",\n      \"s\",\n      Optional.of(1),\n      25,\n      35)\n    val removeFileRow = createTestRemoveFileRow(\n      path = \"test/path\",\n      deletionTimestamp = 1000L,\n      dataChange = true,\n      partitionValues = Map(\"a\" -> \"1\"),\n      size = 55555L,\n      stats = Option(\"{\\\"numRecords\\\":100}\"),\n      deletionVectorDescriptor = Some(deletionVectorDescriptor))\n    val removeFile = new RemoveFile(removeFileRow)\n    assert(removeFile.getPath === \"test/path\")\n    assert(removeFile.getDeletionTimestamp == Optional.of(1000L))\n    assert(removeFile.getDataChange)\n    assert(removeFile.getExtendedFileMetadata == Optional.of(true))\n    assert(removeFile.getPartitionValues.isPresent &&\n      VectorUtils.toJavaMap(removeFile.getPartitionValues.get).asScala.equals(Map(\"a\" -> \"1\")))\n    assert(removeFile.getSize == Optional.of(55555L))\n    assert(removeFile.getStats(null).isPresent &&\n      removeFile.getStats(null).get.serializeAsJson(null) == \"{\\\"numRecords\\\":100}\")\n    assert(!removeFile.getTags.isPresent)\n    assert(removeFile.getDeletionVector.isPresent)\n    assert(removeFile.getDeletionVector.get == deletionVectorDescriptor)\n    assert(!removeFile.getBaseRowId.isPresent)\n    assert(!removeFile.getDefaultRowCommitVersion.isPresent)\n  }\n\n  test(\"getters can read RemoveFile's fields from the backing row with row tracking\") {\n    val deletionVectorDescriptor = new DeletionVectorDescriptor(\n      \"storage\",\n      \"s\",\n      Optional.of(1),\n      25,\n      35)\n    val removeFileRow = createTestRemoveFileRow(\n      path = \"test/path\",\n      deletionTimestamp = 1000L,\n      dataChange = true,\n      partitionValues = Map(\"a\" -> \"1\"),\n      size = 55555L,\n      stats = Option(\"{\\\"numRecords\\\":100}\"),\n      baseRowId = Option(30L),\n      defaultRowCommitVersion = Option(40L),\n      deletionVectorDescriptor = Some(deletionVectorDescriptor))\n\n    val removeFile = new RemoveFile(removeFileRow)\n    assert(removeFile.getPath === \"test/path\")\n    assert(removeFile.getDeletionTimestamp == Optional.of(1000L))\n    assert(removeFile.getDataChange)\n    assert(removeFile.getExtendedFileMetadata == Optional.of(true))\n    assert(removeFile.getPartitionValues.isPresent &&\n      VectorUtils.toJavaMap(removeFile.getPartitionValues.get).asScala.equals(Map(\"a\" -> \"1\")))\n    assert(removeFile.getSize == Optional.of(55555L))\n    assert(removeFile.getStats(null).isPresent &&\n      removeFile.getStats(\n        null).get.serializeAsJson(null) == \"{\\\"numRecords\\\":100}\")\n    assert(removeFile.getBaseRowId === Optional.of(30L))\n    assert(removeFile.getDefaultRowCommitVersion === Optional.of(40L))\n    assert(removeFile.getDeletionVector.get == deletionVectorDescriptor)\n    assert(!removeFile.getTags.isPresent)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/catalogManaged/CatalogManagedLogSegmentSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.catalogManaged\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.TableManager\nimport io.delta.kernel.internal.actions.Protocol\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.test.{ActionUtils, MockFileSystemClientUtils}\nimport io.delta.kernel.types.{IntegerType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CatalogManagedLogSegmentSuite extends AnyFunSuite\n    with MockFileSystemClientUtils\n    with ActionUtils {\n\n  implicit class OptionOps[T](option: Option[T]) {\n    def asJava: Optional[T] = option match {\n      case Some(value) => Optional.of(value)\n      case None => Optional.empty()\n    }\n  }\n\n  private def testLogSegment(\n      testName: String,\n      versionToLoad: Long,\n      checkpointVersionOpt: Option[Long],\n      deltaVersions: Seq[Long],\n      ratifiedCommitVersions: Seq[Long],\n      crcVersions: Seq[Long] = Seq.empty,\n      expectedDeltaAndCommitVersionsOpt: Option[Seq[Long]] = None,\n      expectedExceptionClassOpt: Option[Class[_ <: Exception]] = None): Unit = {\n    // TODO: test with ratified=inline\n\n    test(testName + \" - ratified=materialized\") {\n      val checkpointFile = checkpointVersionOpt.map(v => classicCheckpointFileStatus(v)).toSeq\n      val deltaFiles = deltaFileStatuses(deltaVersions)\n      val crcFiles = crcVersions.map(checksumFileStatus)\n      val ratifiedCommitParsedLogDatas = parsedRatifiedStagedCommits(ratifiedCommitVersions)\n\n      val engine = createMockFSListFromEngine(checkpointFile ++ deltaFiles ++ crcFiles)\n\n      val testSchema = new StructType().add(\"c1\", IntegerType.INTEGER);\n\n      val builder = TableManager\n        .loadSnapshot(dataPath.toString)\n        .asInstanceOf[SnapshotBuilderImpl]\n        .atVersion(versionToLoad)\n        .withProtocolAndMetadata(new Protocol(1, 2), testMetadata(testSchema))\n        .withLogData(ratifiedCommitParsedLogDatas.toList.asJava)\n\n      if (expectedExceptionClassOpt.isDefined) {\n        val exception = intercept[Throwable] {\n          // Ensure we load the LogSegment to identify any gaps/issues\n          builder.build(engine).getLogSegment\n        }\n        assert(expectedExceptionClassOpt.get.isInstance(exception))\n      } else {\n        val snapshot = builder.build(engine)\n        val logSegment = snapshot.getLogSegment\n\n        val actualDeltaAndCommitFileStatuses = logSegment.getDeltas.asScala\n\n        // Check: we got the expected versions\n        val actualDeltaAndCommitVersions =\n          actualDeltaAndCommitFileStatuses.map(x => FileNames.deltaVersion(x.getPath))\n        assert(actualDeltaAndCommitVersions sameElements expectedDeltaAndCommitVersionsOpt.get)\n\n        // Check: ratified commits take priority over published deltas when versions overlap\n        val expectedRatifiedVersions =\n          ratifiedCommitVersions.toSet.intersect(actualDeltaAndCommitVersions.toSet)\n\n        actualDeltaAndCommitFileStatuses.map(_.getPath).foreach { path =>\n          val version = FileNames.deltaVersion(path)\n          if (expectedRatifiedVersions.contains(version)) {\n            assert(FileNames.isStagedDeltaFile(path))\n          } else {\n            assert(FileNames.isPublishedDeltaFile(path))\n          }\n        }\n\n        // Check: maxPublishedDeltaVersion\n        val expectedMaxPublishedDeltaVersion = deltaVersions\n          .filter(_ <= versionToLoad).reduceOption(_ max _).asJava\n\n        assert(logSegment.getMaxPublishedDeltaVersion === expectedMaxPublishedDeltaVersion)\n\n        // Check: lastSeenChecksum\n        val expectedLastSeenChecksumVersion = crcVersions\n          .filter(v => v <= versionToLoad && checkpointVersionOpt.forall(v >= _))\n          .lastOption\n\n        expectedLastSeenChecksumVersion match {\n          case Some(expectedVersion) =>\n            val checksumPath = logSegment.getLastSeenChecksum.get.getPath\n            val actualVersion = FileNames.checksumVersion(checksumPath)\n            assert(actualVersion === expectedVersion)\n          case None =>\n            assert(!logSegment.getLastSeenChecksum.isPresent)\n        }\n      }\n    }\n  }\n\n  /////////////////////////////////////////////////////\n  // LogSegment construction tests -- positive cases //\n  /////////////////////////////////////////////////////\n\n  // _delta_log: [                          10.checkpoint+json, 11.json, 12.json]\n  // catalog:    [8.uuid.json, 9.uuid.json                                      ]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that are before first checkpoint\",\n    versionToLoad = 12L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 8L to 9L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 12L))\n\n  // _delta_log: [          10.checkpoint+json, 11.json, 12.json, 13.json]\n  // catalog:    [9.uuid.json, 10.uuid.json, 11.uuid.json                ]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that overlap w first checkpoint + deltas\",\n    versionToLoad = 13L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 13L,\n    ratifiedCommitVersions = 9L to 11L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 13L))\n\n  // _delta_log: [10.checkpoint+json, 11.json, 12.json, 13.json, 14.json, 15.json]\n  // catalog:    [                  11.uuid.json, 12.uuid.json, 13.uuid.json     ]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that are contained within first checkpoint + deltas\",\n    versionToLoad = 15L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 15L,\n    ratifiedCommitVersions = 11L to 13L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 15L))\n\n  // _delta_log: [             10.checkpoint+json, 11.json, 12.json                      ]\n  // catalog:    [9.uuid.json, 10.uuid.json 11.uuid.json, 12.uuid.json, 13.uuid.json     ]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that supersets the first checkpoint + deltas\",\n    versionToLoad = 13L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 9L to 13L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 13L))\n\n  // _delta_log: [10.checkpoint+json, 11.json, 12.json                                 ]\n  // catalog:    [                             12.uuid.json, 13.uuid.json, 14.uuid.json]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that overlap with end of deltas\",\n    versionToLoad = 14L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 12L to 14L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 14L))\n\n  // _delta_log: [10.checkpoint+json, 11.json, 12.json                           ]\n  // catalog:    [                                     13.uuid.json, 14.uuid.json]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that are after (no gap) the deltas\",\n    versionToLoad = 14L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 13L to 14L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 14L))\n\n  // versionToLoad:     V\n  // _delta_log: [10.checkpoint+json, 11.json, 12.json                           ]\n  // catalog:    [                                     13.uuid.json, 14.uuid.json]\n  testLogSegment(\n    testName = \"Build RT with commit versions > versionToLoad - versionToLoad = checkpoint version\",\n    versionToLoad = 10L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 13L to 14L,\n    expectedDeltaAndCommitVersionsOpt = Some(Nil))\n\n  // versionToLoad:                              V\n  // _delta_log: [10.checkpoint+json, 11.json, 12.json                           ]\n  // catalog:    [                                     13.uuid.json, 14.uuid.json]\n  testLogSegment(\n    testName = \"Build RT with commit versions > versionToLoad - versionToLoad = delta version\",\n    versionToLoad = 12L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 13L to 14L,\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 12L))\n\n  // _delta_log: [0.json,                                      ]\n  // catalog:    [        1.uuid.json, 2.uuid.json, 3.uuid.json]\n  testLogSegment(\n    testName = \"Build RT with only deltas and ratified commits (no checkpoint)\",\n    versionToLoad = 3L,\n    checkpointVersionOpt = None,\n    deltaVersions = Seq(0L),\n    ratifiedCommitVersions = 1L to 3L,\n    expectedDeltaAndCommitVersionsOpt = Some(0L to 3L))\n\n  // _delta_log: [10.checkpoint+json,             ]\n  // catalog:    [                    11.uuid.json]\n  testLogSegment(\n    testName = \"Build RT when checkpoint version is the last version from the filesystem\",\n    versionToLoad = 11L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = Seq(10L),\n    ratifiedCommitVersions = Seq(11L),\n    expectedDeltaAndCommitVersionsOpt = Some(Seq(11L)))\n\n  // scalastyle:off line.size.limit\n  // _delta_log: [10.checkpoint+json, 11.json+crc, 12.json, 13.crc,                     15.crc]\n  // catalog:    [                                          13.uuid.json, 14.uuid.json, 15.uuid.json, 16.uuid.json]\n  // scalastyle:on line.size.limit\n  testLogSegment(\n    testName = \"Build LogSegment with CRC files for unpublished versions\",\n    versionToLoad = 16L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = Seq(10L, 11L, 12L),\n    ratifiedCommitVersions = 13L to 16L,\n    crcVersions = Seq(11L, 13L, 15L),\n    expectedDeltaAndCommitVersionsOpt = Some(11L to 16L))\n\n  // TODO: Support this case in a followup PR\n  // _delta_log: [                                                  ]\n  // catalog:    [0.uuid.json, 1.uuid.json, 2.uuid.json, 3.uuid.json]\n  /*\n  testLogSegment(\n    testName = \"Build RT with only ratified commits\",\n    versionToLoad = 3L,\n    checkpointVersionOpt = None,\n    deltaVersions = Seq(),\n    ratifiedCommitVersions = 0L to 3L,\n    expectedDeltaAndCommitVersionsOpt = Some(0L to 3L))\n   */\n\n  /////////////////////////////////////////////////////\n  // LogSegment construction tests -- negative cases //\n  /////////////////////////////////////////////////////\n\n  // _delta_log: [10.checkpoint+json, 11.json, 12.json                                ]\n  // catalog:    [                                          14.uuid.json, 15.uuid.json]\n  testLogSegment(\n    testName = \"Build RT with ratified commits that are after (with gap) the deltas => ERROR\",\n    versionToLoad = 15L,\n    checkpointVersionOpt = Some(10L),\n    deltaVersions = 10L to 12L,\n    ratifiedCommitVersions = 14L to 15L,\n    expectedExceptionClassOpt = Some(classOf[io.delta.kernel.exceptions.InvalidTableException]))\n\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/catalogManaged/SnapshotBuilderSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.catalogManaged\n\nimport java.util.Collections\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.TableManager\nimport io.delta.kernel.commit.{CommitMetadata, CommitResponse, Committer}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{KernelException, UnsupportedProtocolVersionException, UnsupportedTableFeatureException}\nimport io.delta.kernel.internal.actions.Protocol\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter\nimport io.delta.kernel.internal.files.{ParsedCatalogCommitData, ParsedLogData, ParsedPublishedDeltaData}\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.test.{ActionUtils, MockFileSystemClientUtils, MockSnapshotUtils, VectorTestUtils}\nimport io.delta.kernel.types.{IntegerType, StructType}\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass SnapshotBuilderSuite extends AnyFunSuite\n    with MockFileSystemClientUtils\n    with ActionUtils\n    with VectorTestUtils\n    with MockSnapshotUtils {\n\n  private val emptyMockEngine = createMockFSListFromEngine(Nil)\n  private val protocol = new Protocol(1, 2)\n  private val metadata = testMetadata(new StructType().add(\"c1\", IntegerType.INTEGER))\n  private val mockSnapshotAtTimestamp0 =\n    getMockSnapshot(dataPath, latestVersion = 0L, timestamp = 0L)\n\n  ///////////////////////////////////////\n  // Builder Validation Tests -- START //\n  ///////////////////////////////////////\n\n  test(\"loadTable: null path throws NullPointerException\") {\n    assertThrows[NullPointerException] {\n      TableManager.loadSnapshot(null)\n    }\n  }\n\n  // ===== Version Tests ===== //\n\n  test(\"atVersion: negative version throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString).atVersion(-1)\n    }.getMessage\n\n    assert(exMsg === \"version must be >= 0\")\n  }\n\n  // ===== Timestamp Tests ===== //\n\n  test(\"atTimestamp: null latestSnapshot throws NullPointerException\") {\n    assertThrows[NullPointerException] {\n      TableManager.loadSnapshot(dataPath.toString).atTimestamp(1000L, null)\n    }\n  }\n\n  test(\"atTimestamp: timestamp greater than latest snapshot throws IllegalArgumentException\") {\n    val builder =\n      TableManager.loadSnapshot(dataPath.toString).atTimestamp(99, mockSnapshotAtTimestamp0)\n\n    val exMsg = intercept[KernelException] {\n      builder.build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg.contains(\"The provided timestamp 99 ms\"))\n    assert(exMsg.contains(\"is after the latest available version\"))\n  }\n\n  test(\"atTimestamp: timestamp and version both provided throws IllegalArgumentException\") {\n    val builder = TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(1)\n      .atTimestamp(0L, mockSnapshotAtTimestamp0)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      builder.build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"timestamp and version cannot be provided together\")\n  }\n\n  test(\"atTimestamp: protocol and metadata with timestamp throws IllegalArgumentException\") {\n    val builder = TableManager.loadSnapshot(dataPath.toString)\n      .atTimestamp(0L, mockSnapshotAtTimestamp0)\n      .withProtocolAndMetadata(protocol, metadata)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      builder.build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"protocol and metadata can only be provided if a version is provided\")\n  }\n\n  // ===== Committer Tests ===== //\n\n  test(\"withCommitter: null committer throws NullPointerException\") {\n    assertThrows[NullPointerException] {\n      TableManager.loadSnapshot(dataPath.toString).withCommitter(null)\n    }\n  }\n\n  test(\"when no committer is provided, the default committer is created\") {\n    val committer = TableManager.loadSnapshot(dataPath.toString)\n      .asInstanceOf[SnapshotBuilderImpl]\n      .atVersion(1)\n      .withProtocolAndMetadata(protocol, metadata) // avoid trying to use engine to load log segment\n      .build(emptyMockEngine)\n      .getCommitter\n\n    assert(committer.isInstanceOf[DefaultFileSystemManagedTableOnlyCommitter])\n  }\n\n  test(\"custom committer is correctly propagated\") {\n    class CustomCommitter extends Committer {\n      override def commit(\n          engine: Engine,\n          finalizedActions: CloseableIterator[Row],\n          commitMetadata: CommitMetadata): CommitResponse = {\n        throw new UnsupportedOperationException(\"Not implemented\")\n      }\n    }\n\n    val committer = TableManager.loadSnapshot(dataPath.toString)\n      .asInstanceOf[SnapshotBuilderImpl]\n      .atVersion(1)\n      .withCommitter(new CustomCommitter())\n      .withProtocolAndMetadata(protocol, metadata) // avoid trying to use engine to load log segment\n      .build(emptyMockEngine)\n      .getCommitter\n\n    assert(committer.isInstanceOf[CustomCommitter])\n  }\n\n  // ===== Protocol and Metadata Tests ===== //\n\n  test(\"withProtocolAndMetadata: null protocol throws NullPointerException\") {\n    assertThrows[NullPointerException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .withProtocolAndMetadata(null, metadata)\n    }\n\n    assertThrows[NullPointerException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .withProtocolAndMetadata(protocol, null)\n    }\n  }\n\n  test(\"withProtocolAndMetadata: only if version is provided\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .withProtocolAndMetadata(protocol, metadata)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"protocol and metadata can only be provided if a version is provided\")\n  }\n\n  test(\"withProtocolAndMetadata: invalid readerVersion throws KernelException\") {\n    val ex = intercept[UnsupportedProtocolVersionException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(10)\n        .withProtocolAndMetadata(new Protocol(999, 2), metadata)\n        .build(emptyMockEngine)\n    }\n\n    assert(ex.getVersionType === UnsupportedProtocolVersionException.ProtocolVersionType.READER)\n    assert(ex.getMessage.contains(\"Unsupported Delta protocol reader version\"))\n  }\n\n  test(\"withProtocolAndMetadata: unknown reader feature throws KernelException\") {\n    val exMsg = intercept[UnsupportedTableFeatureException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(10)\n        .withProtocolAndMetadata(\n          new Protocol(3, 7, Set(\"unknownReaderFeature\").asJava, Collections.emptySet()),\n          metadata)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg.contains(\"Unsupported Delta table feature\"))\n  }\n\n  // ===== LogData Tests ===== //\n\n  test(\"withLogData: null input throws NullPointerException\") {\n    assertThrows[NullPointerException] {\n      TableManager.loadSnapshot(dataPath.toString).withLogData(null)\n    }\n  }\n\n  Seq(\n    ParsedCatalogCommitData.forInlineData(1, emptyColumnarBatch),\n    ParsedPublishedDeltaData.forFileStatus(deltaFileStatus(1, logPath)),\n    ParsedLogData.forFileStatus(logCompactionStatus(0, 1))).foreach { parsedLogData =>\n    val suffix = s\"- type=${parsedLogData.getClass.getSimpleName}\"\n    test(s\"withLogData: non-staged-ratified-commit throws IllegalArgumentException $suffix\") {\n      val builder = TableManager\n        .loadSnapshot(dataPath.toString)\n        .atVersion(1)\n        .withLogData(Collections.singletonList(parsedLogData))\n\n      val exMsg = intercept[IllegalArgumentException] {\n        builder.build(emptyMockEngine)\n      }.getMessage\n\n      assert(exMsg.contains(\"Only staged ratified commits are supported\"))\n    }\n  }\n\n  test(\"withLogData: non-contiguous input throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(2)\n        .withLogData(parsedRatifiedStagedCommits(Seq(0, 2)).toList.asJava)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg.contains(\"Log data must be sorted and contiguous\"))\n  }\n\n  test(\"withLogData: non-sorted input throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(2)\n        .withLogData(parsedRatifiedStagedCommits(Seq(2, 1, 0)).toList.asJava)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg.contains(\"Log data must be sorted and contiguous\"))\n  }\n\n  /////////////////////////////////////\n  // Builder Validation Tests -- END //\n  /////////////////////////////////////\n\n  test(\"if P & M are provided then LogSegment is not loaded\") {\n    val snapshot = TableManager\n      .loadSnapshot(dataPath.toString)\n      .asInstanceOf[SnapshotBuilderImpl]\n      .atVersion(13)\n      .withProtocolAndMetadata(protocol, metadata)\n      .withLogData(Collections.emptyList())\n      .build(emptyMockEngine)\n\n    assert(!snapshot.getLazyLogSegment.isPresent)\n  }\n\n  // ===== MaxCatalogVersion Tests ===== //\n\n  test(\"withMaxCatalogVersion: negative version throws IllegalArgumentException\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString).withMaxCatalogVersion(-1)\n    }.getMessage\n\n    assert(exMsg === \"A valid version must be >= 0\")\n  }\n\n  test(\"withMaxCatalogVersion: zero is valid\") {\n    // Should not throw\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(0)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withMaxCatalogVersion(0)\n      .build(emptyMockEngine)\n  }\n\n  test(\"withMaxCatalogVersion: positive version is valid\") {\n    // Should not throw\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(10)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withMaxCatalogVersion(10)\n      .build(emptyMockEngine)\n  }\n\n  test(\"withMaxCatalogVersion: version time-travel must be <= maxCatalogVersion\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(15)\n        .withMaxCatalogVersion(10)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"Cannot time-travel to version 15 after the max catalog version 10\")\n  }\n\n  test(\"withMaxCatalogVersion: version time-travel equal to maxCatalogVersion is valid\") {\n    // Should not throw\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(10)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withMaxCatalogVersion(10)\n      .build(emptyMockEngine)\n  }\n\n  test(\"withMaxCatalogVersion: version time-travel less than maxCatalogVersion is valid\") {\n    // Should not throw\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(5)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withMaxCatalogVersion(10)\n      .build(emptyMockEngine)\n  }\n\n  test(\"withMaxCatalogVersion: timestamp time-travel latestSnapshot must have version = \" +\n    \"maxCatalogVersion\") {\n    val mockSnapshotAtVersion5 =\n      getMockSnapshot(dataPath, latestVersion = 5L, timestamp = 1000L)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atTimestamp(0L, mockSnapshotAtVersion5)\n        .withMaxCatalogVersion(10)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"The latestSnapshot provided for timestamp-based time-travel queries must \" +\n      \"have version = maxCatalogVersion\")\n  }\n\n  test(\n    \"withMaxCatalogVersion: timestamp time-travel with matching latestSnapshot version is valid\") {\n    val mockSnapshotAtVersion10 =\n      getMockSnapshot(dataPath, latestVersion = 10L, timestamp = 1000L)\n\n    // Input validation should not throw (but will throw later when trying to construct log segment)\n    val exMsg = intercept[Exception] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atTimestamp(500L, mockSnapshotAtVersion10)\n        .withMaxCatalogVersion(10)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    // Should fail on log segment loading, not on validation\n    assert(!exMsg.contains(\"latestSnapshot provided for timestamp-based time-travel\"))\n  }\n\n  test(\"withMaxCatalogVersion: without version, logData must end with maxCatalogVersion\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .withLogData(parsedRatifiedStagedCommits(Seq(0, 1, 2)).toList.asJava)\n        .withMaxCatalogVersion(5)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"Provided catalog commits must end with max catalog version\")\n  }\n\n  test(\"withMaxCatalogVersion: without version, logData ending with maxCatalogVersion is valid\") {\n    // Input validation should not throw (but will throw later when trying to construct log segment)\n    val exMsg = intercept[Exception] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .withLogData(parsedRatifiedStagedCommits(Seq(0, 1, 2, 3, 4, 5)).toList.asJava)\n        .withMaxCatalogVersion(5)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    // Should fail on log segment loading, not on validation\n    assert(!exMsg.contains(\"Provided catalog commits must end with max catalog version\"))\n  }\n\n  test(\"withMaxCatalogVersion: empty logData with maxCatalogVersion is valid\") {\n    // Should not throw - empty logData is allowed\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(5)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withLogData(Collections.emptyList())\n      .withMaxCatalogVersion(5)\n      .build(emptyMockEngine)\n  }\n\n  test(\"withMaxCatalogVersion: version time-travel with logData not including version fails\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(5)\n        .withLogData(parsedRatifiedStagedCommits(Seq(0, 1, 2, 3)).toList.asJava)\n        .withMaxCatalogVersion(10)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"Provided catalog commits must include versionToLoad for time-travel queries\")\n  }\n\n  test(\"withMaxCatalogVersion: version time-travel with logData ending at version is valid\") {\n    // Should not throw - logData ends exactly at requested version\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(5)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withLogData(parsedRatifiedStagedCommits(Seq(0, 1, 2, 3, 4, 5)).toList.asJava)\n      .withMaxCatalogVersion(10)\n      .build(emptyMockEngine)\n  }\n\n  test(\"withMaxCatalogVersion: version time-travel with logData beyond version is valid\") {\n    // Should not throw - logData extends beyond requested version\n    TableManager.loadSnapshot(dataPath.toString)\n      .atVersion(5)\n      .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n      .withLogData(parsedRatifiedStagedCommits(Seq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)).toList.asJava)\n      .withMaxCatalogVersion(10)\n      .build(emptyMockEngine)\n  }\n\n  test(\"validateMaxCatalogVersionPresence: catalogManaged table requires maxCatalogVersion\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(1)\n        .withProtocolAndMetadata(protocolWithCatalogManagedSupport, metadata)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"Must provide maxCatalogVersion for catalogManaged tables\")\n  }\n\n  test(\n    \"validateMaxCatalogVersionPresence: non-catalogManaged table cannot have maxCatalogVersion\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      TableManager.loadSnapshot(dataPath.toString)\n        .atVersion(1)\n        .withProtocolAndMetadata(protocol, metadata) // protocol without catalogManaged\n        .withMaxCatalogVersion(1)\n        .build(emptyMockEngine)\n    }.getMessage\n\n    assert(exMsg === \"Should not provide maxCatalogVersion for file-system managed tables\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/checkpoints/CheckpointInstanceSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checkpoints\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.internal.fs.Path\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CheckpointInstanceSuite extends AnyFunSuite {\n\n  private val FAKE_DELTA_LOG_PATH = new Path(\"/path/to/delta/log\")\n\n  test(\"checkpoint instance comparisons\") {\n    val ci1_single_1 = new CheckpointInstance(1, Optional.empty())\n    val ci1_withparts_2 = new CheckpointInstance(1, Optional.of(2))\n    val ci1_v2_1 = new CheckpointInstance(\"01.checkpoint.abc.parquet\")\n\n    val ci2_single_1 = new CheckpointInstance(2, Optional.empty())\n    val ci2_withparts_4 = new CheckpointInstance(2, Optional.of(4))\n    val ci2_v2_1 = new CheckpointInstance(\"02.checkpoint.abc.parquet\")\n    val ci2_v2_2 = new CheckpointInstance(\"02.checkpoint.def.parquet\")\n\n    val ci3_single_1 = new CheckpointInstance(3, Optional.empty())\n    val ci3_withparts_2 = new CheckpointInstance(3, Optional.of(2))\n\n    // version takes priority\n    assert(ci1_single_1.compareTo(ci2_single_1) < 0)\n    assert(ci1_v2_1.compareTo(ci2_single_1) < 0)\n    // v2 takes priority over v1 and multipart\n    assert(ci2_single_1.compareTo(ci2_v2_1) < 0)\n    assert(ci2_withparts_4.compareTo(ci2_v2_1) < 0)\n    // parts takes priority when versions are same\n    assert(ci1_single_1.compareTo(ci1_withparts_2) < 0)\n    // version takes priority over parts or v2\n    assert(ci2_withparts_4.compareTo(ci3_withparts_2) < 0)\n    assert(ci2_single_1.compareTo(ci3_withparts_2) < 0)\n    // For v2, filepath is tiebreaker.\n    assert(ci2_v2_2.compareTo(ci2_v2_1) > 0)\n\n    // Everything is less than CheckpointInstance.MAX_VALUE\n    Seq(\n      ci1_single_1,\n      ci1_withparts_2,\n      ci2_single_1,\n      ci2_withparts_4,\n      ci3_single_1,\n      ci3_withparts_2,\n      ci1_v2_1,\n      ci2_v2_1).foreach(ci => assert(ci.compareTo(CheckpointInstance.MAX_VALUE) < 0))\n  }\n\n  test(\"checkpoint instance equality\") {\n    val single = new CheckpointInstance(\"01.checkpoint.parquet\")\n    val multipartPart1 = new CheckpointInstance(\"01.checkpoint.01.02.parquet\")\n    val multipartPart2 = new CheckpointInstance(\"01.checkpoint.02.02.parquet\")\n    val v2Checkpoint1 = new CheckpointInstance(\"01.checkpoint.abc-def.parquet\")\n    val v2Checkpoint2 = new CheckpointInstance(\"01.checkpoint.ghi-klm.parquet\")\n\n    // Single checkpoint is not equal to any other checkpoints at the same version.\n    Seq(multipartPart1, multipartPart2, v2Checkpoint1, v2Checkpoint2).foreach { ci =>\n      assert(!single.equals(ci))\n      assert(single.hashCode() != ci.hashCode())\n    }\n\n    // Multipart checkpoints at the same version are equal if they have the same number of parts.\n    Seq(single, v2Checkpoint1, v2Checkpoint2).foreach { ci =>\n      assert(!multipartPart1.equals(ci))\n      assert(multipartPart1.hashCode() != ci.hashCode())\n    }\n    assert(multipartPart1.equals(multipartPart2))\n    assert(multipartPart1.hashCode() == multipartPart2.hashCode())\n\n    // V2 checkpoints at the same version are equal only if they have the same UUID.\n    Seq(single, multipartPart1, multipartPart2, v2Checkpoint2).foreach { ci =>\n      assert(!v2Checkpoint1.equals(ci))\n      assert(v2Checkpoint1.hashCode() != ci.hashCode())\n    }\n  }\n\n  test(\"checkpoint instance instantiation\") {\n    // classic checkpoint\n    val classicCheckpoint = new CheckpointInstance(\n      new Path(FAKE_DELTA_LOG_PATH, \"00000000000000000010.checkpoint.parquet\").toString)\n    assert(classicCheckpoint.version == 10)\n    assert(!classicCheckpoint.numParts.isPresent())\n    assert(classicCheckpoint.format == CheckpointInstance.CheckpointFormat.CLASSIC)\n    assert(classicCheckpoint.format.usesSidecars())\n\n    // multi-part checkpoint\n    val multipartCheckpoint = new CheckpointInstance(\n      new Path(\n        FAKE_DELTA_LOG_PATH,\n        \"00000000000000000010.checkpoint.0000000002.0000000003.parquet\").toString)\n    assert(multipartCheckpoint.version == 10)\n    assert(multipartCheckpoint.numParts.isPresent() && multipartCheckpoint.numParts.get() == 3)\n    assert(multipartCheckpoint.format == CheckpointInstance.CheckpointFormat.MULTI_PART)\n    assert(!multipartCheckpoint.format.usesSidecars())\n\n    // V2 checkpoint\n    val v2Checkpoint = new CheckpointInstance(\n      new Path(\n        FAKE_DELTA_LOG_PATH,\n        \"00000000000000000010.checkpoint.abcda-bacbac.parquet\").toString)\n    assert(v2Checkpoint.version == 10)\n    assert(!v2Checkpoint.numParts.isPresent())\n    assert(v2Checkpoint.format == CheckpointInstance.CheckpointFormat.V2)\n    assert(v2Checkpoint.format.usesSidecars())\n\n    // invalid checkpoints\n    intercept[RuntimeException] {\n      new CheckpointInstance(\n        new Path(FAKE_DELTA_LOG_PATH, \"00000000000000000010.checkpoint.000000.a.parquet\").toString)\n    }\n    intercept[RuntimeException] {\n      new CheckpointInstance(\n        new Path(FAKE_DELTA_LOG_PATH, \"00000000000000000010.parquet\").toString)\n    }\n  }\n\n  test(\"checkpoint instance getCorrespondingFiles\") {\n    // classic checkpoint\n    val classicCheckpoint0 = new CheckpointInstance(0)\n    assert(classicCheckpoint0.getCorrespondingFiles(FAKE_DELTA_LOG_PATH).equals(\n      Seq(new Path(FAKE_DELTA_LOG_PATH, \"00000000000000000000.checkpoint.parquet\")).asJava))\n    val classicCheckpoint10 = new CheckpointInstance(10)\n    assert(classicCheckpoint10.getCorrespondingFiles(FAKE_DELTA_LOG_PATH).equals(\n      Seq(new Path(FAKE_DELTA_LOG_PATH, \"00000000000000000010.checkpoint.parquet\")).asJava))\n\n    // multi-part checkpoint\n    val multipartCheckpoint = new CheckpointInstance(10, Optional.of(3))\n    val expectedResult = Seq(\n      \"00000000000000000010.checkpoint.0000000001.0000000003.parquet\",\n      \"00000000000000000010.checkpoint.0000000002.0000000003.parquet\",\n      \"00000000000000000010.checkpoint.0000000003.0000000003.parquet\").map(new Path(\n      FAKE_DELTA_LOG_PATH,\n      _))\n    assert(multipartCheckpoint.getCorrespondingFiles(FAKE_DELTA_LOG_PATH).equals(\n      expectedResult.asJava))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/checkpoints/CheckpointerSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checkpoints\n\nimport java.io.{FileNotFoundException, IOException}\nimport java.util.Optional\n\nimport scala.util.control.NonFatal\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.exceptions.KernelEngineException\nimport io.delta.kernel.expressions.Predicate\nimport io.delta.kernel.internal.checkpoints.Checkpointer.findLastCompleteCheckpointBeforeHelper\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames.checkpointFileSingular\nimport io.delta.kernel.internal.util.Utils\nimport io.delta.kernel.test.{BaseMockJsonHandler, MockFileSystemClientUtils, VectorTestUtils}\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CheckpointerSuite extends AnyFunSuite with MockFileSystemClientUtils {\n  import CheckpointerSuite._\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // readLastCheckpointFile tests\n  //////////////////////////////////////////////////////////////////////////////////\n  test(\"load a valid last checkpoint metadata file\") {\n    val jsonHandler = new MockLastCheckpointMetadataFileReader(maxFailures = 0)\n    val lastCheckpoint = new Checkpointer(VALID_LAST_CHECKPOINT_FILE_TABLE)\n      .readLastCheckpointFile(mockEngine(jsonHandler = jsonHandler))\n    assertValidCheckpointMetadata(lastCheckpoint)\n    assert(jsonHandler.currentFailCount == 0)\n  }\n\n  test(\"load a zero-sized last checkpoint metadata file\") {\n    val jsonHandler = new MockLastCheckpointMetadataFileReader(maxFailures = 0)\n    val lastCheckpoint = new Checkpointer(ZERO_SIZED_LAST_CHECKPOINT_FILE_TABLE)\n      .readLastCheckpointFile(mockEngine(jsonHandler = jsonHandler))\n    assert(!lastCheckpoint.isPresent)\n    assert(jsonHandler.currentFailCount == 0)\n  }\n\n  test(\"load an invalid last checkpoint metadata file\") {\n    val jsonHandler = new MockLastCheckpointMetadataFileReader(maxFailures = 0)\n    val lastCheckpoint = new Checkpointer(INVALID_LAST_CHECKPOINT_FILE_TABLE)\n      .readLastCheckpointFile(mockEngine(jsonHandler = jsonHandler))\n    assert(!lastCheckpoint.isPresent)\n    assert(jsonHandler.currentFailCount == 0)\n  }\n\n  test(\"retry last checkpoint metadata loading - succeeds at third attempt\") {\n    val jsonHandler = new MockLastCheckpointMetadataFileReader(maxFailures = 2)\n    val lastCheckpoint = new Checkpointer(VALID_LAST_CHECKPOINT_FILE_TABLE)\n      .readLastCheckpointFile(mockEngine(jsonHandler = jsonHandler))\n    assertValidCheckpointMetadata(lastCheckpoint)\n    assert(jsonHandler.currentFailCount == 2)\n  }\n\n  test(\"retry last checkpoint metadata loading - exceeds max failures\") {\n    val jsonHandler = new MockLastCheckpointMetadataFileReader(maxFailures = 4)\n    val lastCheckpoint = new Checkpointer(VALID_LAST_CHECKPOINT_FILE_TABLE)\n      .readLastCheckpointFile(mockEngine(jsonHandler = jsonHandler))\n    assert(!lastCheckpoint.isPresent)\n    assert(jsonHandler.currentFailCount == 3) // 3 is the max retries\n  }\n\n  test(\"try to load last checkpoint metadata when the file is missing\") {\n    val jsonHandler = new MockLastCheckpointMetadataFileReader(maxFailures = 0)\n    val lastCheckpoint = new Checkpointer(LAST_CHECKPOINT_FILE_NOT_FOUND_TABLE)\n      .readLastCheckpointFile(mockEngine(jsonHandler = jsonHandler))\n    assert(!lastCheckpoint.isPresent)\n    assert(jsonHandler.currentFailCount == 0)\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // findLastCompleteCheckpointBefore tests\n  //////////////////////////////////////////////////////////////////////////////////\n  test(\"findLastCompleteCheckpointBefore - no checkpoints\") {\n    val files = deltaFileStatuses(Seq.range(0, 25))\n\n    Seq((0, 0), (10, 10), (20, 20), (27, 25 /* no delta log files after version 24 */ )).foreach {\n      case (beforeVersion, expNumFilesListed) =>\n        assertNoLastCheckpoint(files, beforeVersion, expNumFilesListed)\n    }\n  }\n\n  test(\"findLastCompleteCheckpointBefore - single checkpoint\") {\n    // 25 delta files and 1 checkpoint file = total 26 files.\n    val files = deltaFileStatuses(Seq.range(0, 25)) ++ singularCheckpointFileStatuses(Seq(10))\n\n    Seq((0, 0), (4, 4), (9, 9), (10, 10)).foreach {\n      case (beforeVersion, expNumFilesListed) =>\n        assertNoLastCheckpoint(files, beforeVersion, expNumFilesListed)\n    }\n\n    Seq((14, 10, 15), (25, 10, 26), (27, 10, 26)).foreach {\n      case (beforeVersion, expCheckpointVersion, expNumFilesListed) =>\n        assertLastCheckpoint(files, beforeVersion, expCheckpointVersion, expNumFilesListed)\n    }\n  }\n\n  test(\"findLastCompleteCheckpointBefore - multi-part checkpoint\") {\n    // 25 delta files and 20 checkpoint files = total 45 files.\n    val files = deltaFileStatuses(Seq.range(0, 25)) ++ multiCheckpointFileStatuses(Seq(10), 20)\n\n    Seq((0, 0), (4, 4), (9, 9), (10, 10)).foreach {\n      case (beforeVersion, expNumFilesListed) =>\n        assertNoLastCheckpoint(files, beforeVersion, expNumFilesListed)\n    }\n\n    Seq((14, 10, 14 + 20), (25, 10, 25 + 20), (27, 10, 25 + 20)).foreach {\n      case (beforeVersion, expCheckpointVersion, expNumFilesListed) =>\n        assertLastCheckpoint(files, beforeVersion, expCheckpointVersion, expNumFilesListed)\n    }\n  }\n\n  test(\"findLastCompleteCheckpointBefore - multi-part checkpoint per 10commits - 10K commits\") {\n    // 10K delta files and 1K checkpoints * 20 files for each checkpoint = total 30K files.\n    val files = deltaFileStatuses(Seq.range(0, 10000)) ++\n      multiCheckpointFileStatuses(Seq.range(10, 10000, 10), 20)\n\n    Seq(0, 4, 9, 10).foreach { beforeVersion =>\n      val expNumFilesListed = beforeVersion\n      assertNoLastCheckpoint(files, beforeVersion, expNumFilesListed)\n    }\n\n    Seq(789, 1005, 5787, 9999).foreach { beforeVersion =>\n      val expCheckpointVersion = (beforeVersion / 10) * 10\n      // Listing size is 1000 delta versions (i.e list _delta_log/0001000* to _delta_log/0001999*)\n      val versionsListed = Math.min(beforeVersion, 1000)\n      val expNumFilesListed =\n        versionsListed /* delta files */ +\n          (versionsListed / 10) * 20 /* checkpoints */\n      assertLastCheckpoint(files, beforeVersion, expCheckpointVersion, expNumFilesListed)\n    }\n  }\n\n  test(\"findLastCompleteCheckpointBefore - multi-part checkpoint per 2.5K commits - 10K commits\") {\n    // 10K delta files and 4 checkpoints * 50 files for each checkpoint = total 10,080 files.\n    val files = deltaFileStatuses(Seq.range(0, 10000)) ++\n      multiCheckpointFileStatuses(Seq.range(2500, 10000, 2500), 50)\n\n    Seq((0, 0), (889, 889), (1001, 1002), (2400, 2402)).foreach {\n      case (beforeVersion, expNumFilesListed) =>\n        assertNoLastCheckpoint(files, beforeVersion, expNumFilesListed)\n    }\n\n    Seq(2600, 5002, 7980, 9999).foreach { beforeVersion =>\n      val expCheckpointVersion = (beforeVersion / 2500) * 2500\n      // Listing size is 1000 delta versions (i.e list _delta_log/0001000* to _delta_log/0001999*)\n      // We list until the checkpoint is encounters in increments of 1000 versions at a time\n      val numListCalls = ((beforeVersion - expCheckpointVersion) / 1000) + 1\n      val versionsListed = 1000 * numListCalls\n      val expNumFilesListed =\n        numListCalls - 1 /* last file scanned that fails the search and stops */ +\n          versionsListed /* delta files */ +\n          50 /* one multi-part checkpoint */\n      assertLastCheckpoint(files, beforeVersion, expCheckpointVersion, expNumFilesListed)\n    }\n  }\n\n  test(\"findLastCompleteCheckpointBefore - two checkpoints (one is zero-sized - not valid)\") {\n    // 25 delta files and 2 checkpoint file = total 27 files.\n    val files = deltaFileStatuses(Seq.range(0, 25)) ++\n      singularCheckpointFileStatuses(Seq(10)) ++\n      Seq(FileStatus.of(\n        checkpointFileSingular(logPath, 20).toString,\n        0,\n        0\n      )) // zero-sized CP\n\n    Seq((0, 0), (4, 4), (9, 9), (10, 10)).foreach {\n      case (beforeVersion, expNumFilesListed) =>\n        assertNoLastCheckpoint(files, beforeVersion, expNumFilesListed)\n    }\n\n    Seq((14, 10, 15), (25, 10, 27), (27, 10, 27)).foreach {\n      case (beforeVersion, expCheckpointVersion, expNumFilesListed) =>\n        assertLastCheckpoint(files, beforeVersion, expCheckpointVersion, expNumFilesListed)\n    }\n  }\n\n  /** Assert that the checkpoint metadata is same as [[SAMPLE_LAST_CHECKPOINT_FILE_CONTENT]] */\n  def assertValidCheckpointMetadata(actual: Optional[CheckpointMetaData]): Unit = {\n    assert(actual.isPresent)\n    val metadata = actual.get()\n    assert(metadata.version == 40L)\n    assert(metadata.size == 44L)\n    assert(metadata.parts == Optional.of(20L))\n  }\n\n  def assertLastCheckpoint(\n      deltaLogFiles: Seq[FileStatus],\n      beforeVersion: Long,\n      expCheckpointVersion: Long,\n      expNumFilesListed: Long): Unit = {\n    val engine = createMockFSListFromEngine(deltaLogFiles)\n    val result = findLastCompleteCheckpointBeforeHelper(engine, logPath, beforeVersion)\n    assert(result._1.isPresent, s\"Checkpoint should be found for version=$beforeVersion\")\n    assert(\n      result._1.get().version === expCheckpointVersion,\n      s\"Incorrect checkpoint version before version=$beforeVersion\")\n    assert(result._2 === expNumFilesListed, s\"Invalid number of files listed: $beforeVersion\")\n  }\n\n  def assertNoLastCheckpoint(\n      deltaLogFiles: Seq[FileStatus],\n      beforeVersion: Long,\n      expNumFilesListed: Long): Unit = {\n    val engine = createMockFSListFromEngine(deltaLogFiles)\n    val result = findLastCompleteCheckpointBeforeHelper(engine, logPath, beforeVersion)\n    assert(!result._1.isPresent, s\"No checkpoint should be found for version=$beforeVersion\")\n    assert(result._2 == expNumFilesListed, s\"Invalid number of files listed: $beforeVersion\")\n  }\n}\n\nobject CheckpointerSuite extends VectorTestUtils {\n  val SAMPLE_LAST_CHECKPOINT_FILE_CONTENT: ColumnarBatch = new ColumnarBatch {\n    override def getSchema: StructType = CheckpointMetaData.READ_SCHEMA\n\n    override def getColumnVector(ordinal: Int): ColumnVector = {\n      ordinal match {\n        case 0 => longVector(Seq(40)) // version\n        case 1 => longVector(Seq(44)) // size\n        case 2 => longVector(Seq(20)); // parts\n        case 3 => mapTypeVector(Seq(Map.empty[String, String])) // tags\n      }\n    }\n\n    override def getSize: Int = 1\n  }\n\n  val ZERO_ENTRIES_COLUMNAR_BATCH: ColumnarBatch = new ColumnarBatch {\n    override def getSchema: StructType = CheckpointMetaData.READ_SCHEMA\n\n    // empty vector for all columns\n    override def getColumnVector(ordinal: Int): ColumnVector = longVector(Seq.empty)\n\n    override def getSize: Int = 0\n  }\n\n  val VALID_LAST_CHECKPOINT_FILE_TABLE = new Path(\"/valid\")\n  val ZERO_SIZED_LAST_CHECKPOINT_FILE_TABLE = new Path(\"/zero_sized\")\n  val INVALID_LAST_CHECKPOINT_FILE_TABLE = new Path(\"/invalid\")\n  val LAST_CHECKPOINT_FILE_NOT_FOUND_TABLE = new Path(\"/filenotfoundtable\")\n}\n\n/** `maxFailures` allows how many times to fail before returning the valid data */\nclass MockLastCheckpointMetadataFileReader(maxFailures: Int) extends BaseMockJsonHandler {\n  import CheckpointerSuite._\n  var currentFailCount = 0\n\n  override def readJsonFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n    val file = fileIter.next()\n    val path = new Path(file.getPath)\n\n    Utils.singletonCloseableIterator(\n      try {\n        if (currentFailCount < maxFailures) {\n          currentFailCount += 1\n          throw new IOException(\"Retryable exception\")\n        }\n\n        path.getParent match {\n          case VALID_LAST_CHECKPOINT_FILE_TABLE => SAMPLE_LAST_CHECKPOINT_FILE_CONTENT\n          case ZERO_SIZED_LAST_CHECKPOINT_FILE_TABLE => ZERO_ENTRIES_COLUMNAR_BATCH\n          case INVALID_LAST_CHECKPOINT_FILE_TABLE =>\n            throw new IOException(\"Invalid last checkpoint file\")\n          case LAST_CHECKPOINT_FILE_NOT_FOUND_TABLE =>\n            throw new FileNotFoundException(\"File not found\")\n          case _ => throw new IOException(\"Unknown table\")\n        }\n      } catch {\n        case NonFatal(e) => throw new KernelEngineException(\"Failed to read last checkpoint\", e);\n      })\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/checksum/CRCInfoReadCompatSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checksum\n\nimport java.util\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, Row}\nimport io.delta.kernel.internal.actions.{DomainMetadata, Format, Metadata, Protocol}\nimport io.delta.kernel.internal.checksum.CRCInfo.{CRC_FILE_READ_SCHEMA, CRC_FILE_SCHEMA}\nimport io.delta.kernel.internal.data.GenericColumnVector\nimport io.delta.kernel.internal.stats.FileSizeHistogram\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.internal.util.VectorUtils.{buildArrayValue, stringStringMapValue}\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types.{DataType, StringType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Tests that CRCInfo.fromColumnarBatch correctly reads the file size histogram from CRC files\n * written using the legacy field name \"histogramOpt\" or the spec-compliant \"fileSizeHistogram\".\n */\nclass CRCInfoReadCompatSuite extends AnyFunSuite with VectorTestUtils {\n\n  private val testProtocol =\n    new Protocol(1, 2, Collections.emptySet(), Collections.emptySet())\n\n  private val testMetadata = new Metadata(\n    \"id\",\n    Optional.of(\"name\"),\n    Optional.of(\"description\"),\n    new Format(\"parquet\", Collections.emptyMap()),\n    DataTypeJsonSerDe.serializeDataType(new StructType()),\n    new StructType(),\n    buildArrayValue(util.Arrays.asList(\"c3\"), StringType.STRING),\n    Optional.of(123),\n    stringStringMapValue(new util.HashMap[String, String]() {\n      put(\"delta.appendOnly\", \"true\")\n    }))\n\n  /** Creates a simple histogram with distinct values for identification in tests. */\n  private def createTestHistogram(fileCount: Long): FileSizeHistogram = {\n    val boundaries = Array(0L, 1024L)\n    val counts = Array(fileCount, 0L)\n    val bytes = Array(fileCount * 100, 0L)\n    new FileSizeHistogram(boundaries, counts, bytes)\n  }\n\n  /**\n   * Builds a ColumnVector for a FileSizeHistogram struct field. If histogram is None, the vector\n   * is null at row 0.\n   */\n  private def histogramColumnVector(\n      histogram: Option[FileSizeHistogram]): ColumnVector = {\n    val rowValue: Row = histogram.map(_.toRow()).orNull\n    new GenericColumnVector(\n      util.Arrays.asList(rowValue),\n      FileSizeHistogram.FULL_SCHEMA)\n  }\n\n  /**\n   * Build a ColumnarBatch with the given schema and histogram column vectors at the appropriate\n   * positions.\n   */\n  private def buildBatch(\n      schema: StructType,\n      fileSizeHistogram: Option[FileSizeHistogram],\n      histogramOpt: Option[FileSizeHistogram]): ColumnarBatch = {\n    val protocolColVector =\n      new GenericColumnVector(\n        util.Arrays.asList(testProtocol.toRow()),\n        Protocol.FULL_SCHEMA)\n    val metadataColVector =\n      new GenericColumnVector(\n        util.Arrays.asList(testMetadata.toRow()),\n        Metadata.FULL_SCHEMA)\n\n    new ColumnarBatch {\n      override def getSchema: StructType = schema\n      override def getSize: Int = 1\n\n      override def getColumnVector(ordinal: Int): ColumnVector = {\n        val fieldName = schema.at(ordinal).getName\n        fieldName match {\n          case \"tableSizeBytes\" => longVector(Seq(1000L))\n          case \"numFiles\" => longVector(Seq(10L))\n          case \"numMetadata\" => longVector(Seq(1L))\n          case \"numProtocol\" => longVector(Seq(1L))\n          case \"metadata\" => metadataColVector\n          case \"protocol\" => protocolColVector\n          case \"txnId\" => stringVector(Seq(null))\n          case \"domainMetadata\" => nullColumnVector(\n              CRC_FILE_SCHEMA.get(\"domainMetadata\").getDataType)\n          case \"fileSizeHistogram\" => histogramColumnVector(fileSizeHistogram)\n          case \"histogramOpt\" => histogramColumnVector(histogramOpt)\n          case _ =>\n            throw new IllegalArgumentException(s\"Unknown field: $fieldName\")\n        }\n      }\n    }\n  }\n\n  /** Creates a column vector that is null at every row. */\n  private def nullColumnVector(dataType: DataType): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = dataType\n      override def getSize: Int = 1\n      override def close(): Unit = {}\n      override def isNullAt(rowId: Int): Boolean = true\n    }\n  }\n\n  test(\"reads fileSizeHistogram when only spec-compliant field is present\") {\n    val histogram = createTestHistogram(fileCount = 42)\n    val batch = buildBatch(\n      CRC_FILE_READ_SCHEMA,\n      fileSizeHistogram = Some(histogram),\n      histogramOpt = None)\n\n    val crcInfo = CRCInfo.fromColumnarBatch(1L, batch, 0, \"test.crc\")\n    assert(crcInfo.isPresent)\n    assert(crcInfo.get().getFileSizeHistogram.isPresent)\n    assert(crcInfo.get().getFileSizeHistogram.get() === histogram)\n  }\n\n  test(\"reads histogramOpt when only legacy field is present\") {\n    val histogram = createTestHistogram(fileCount = 99)\n    val batch = buildBatch(\n      CRC_FILE_READ_SCHEMA,\n      fileSizeHistogram = None,\n      histogramOpt = Some(histogram))\n\n    val crcInfo = CRCInfo.fromColumnarBatch(1L, batch, 0, \"test.crc\")\n    assert(crcInfo.isPresent)\n    assert(crcInfo.get().getFileSizeHistogram.isPresent)\n    assert(crcInfo.get().getFileSizeHistogram.get() === histogram)\n  }\n\n  test(\"prefers fileSizeHistogram when both fields are present\") {\n    val specHistogram = createTestHistogram(fileCount = 10)\n    val legacyHistogram = createTestHistogram(fileCount = 20)\n    val batch = buildBatch(\n      CRC_FILE_READ_SCHEMA,\n      fileSizeHistogram = Some(specHistogram),\n      histogramOpt = Some(legacyHistogram))\n\n    val crcInfo = CRCInfo.fromColumnarBatch(1L, batch, 0, \"test.crc\")\n    assert(crcInfo.isPresent)\n    assert(crcInfo.get().getFileSizeHistogram.isPresent)\n    assert(crcInfo.get().getFileSizeHistogram.get() === specHistogram)\n  }\n\n  test(\"returns empty histogram when neither field is present\") {\n    val batch = buildBatch(\n      CRC_FILE_READ_SCHEMA,\n      fileSizeHistogram = None,\n      histogramOpt = None)\n\n    val crcInfo = CRCInfo.fromColumnarBatch(1L, batch, 0, \"test.crc\")\n    assert(crcInfo.isPresent)\n    assert(!crcInfo.get().getFileSizeHistogram.isPresent)\n  }\n\n  test(\"safely skips fallback when batch uses original CRC_FILE_SCHEMA\") {\n    // When fromColumnarBatch is called with a batch using the original schema\n    // (without histogramOpt), the fallback should be safely skipped.\n    val protocolColVector =\n      new GenericColumnVector(\n        util.Arrays.asList(testProtocol.toRow()),\n        Protocol.FULL_SCHEMA)\n    val metadataColVector =\n      new GenericColumnVector(\n        util.Arrays.asList(testMetadata.toRow()),\n        Metadata.FULL_SCHEMA)\n\n    val batch = new ColumnarBatch {\n      override def getSchema: StructType = CRC_FILE_SCHEMA\n      override def getSize: Int = 1\n      override def getColumnVector(ordinal: Int): ColumnVector = {\n        val fieldName = CRC_FILE_SCHEMA.at(ordinal).getName\n        fieldName match {\n          case \"tableSizeBytes\" => longVector(Seq(1000L))\n          case \"numFiles\" => longVector(Seq(10L))\n          case \"numMetadata\" => longVector(Seq(1L))\n          case \"numProtocol\" => longVector(Seq(1L))\n          case \"metadata\" => metadataColVector\n          case \"protocol\" => protocolColVector\n          case \"txnId\" => stringVector(Seq(null))\n          case \"domainMetadata\" => nullColumnVector(\n              CRC_FILE_SCHEMA.get(\"domainMetadata\").getDataType)\n          case \"fileSizeHistogram\" => histogramColumnVector(None)\n          case _ =>\n            throw new IllegalArgumentException(s\"Unknown field: $fieldName\")\n        }\n      }\n    }\n\n    val crcInfo = CRCInfo.fromColumnarBatch(1L, batch, 0, \"test.crc\")\n    assert(crcInfo.isPresent)\n    assert(!crcInfo.get().getFileSizeHistogram.isPresent)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/checksum/ChecksumWriterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.checksum\n\nimport java.util\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters.{asScalaBufferConverter, asScalaSetConverter, seqAsJavaListConverter, setAsJavaSetConverter}\n\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.exceptions.TableNotFoundException\nimport io.delta.kernel.internal.actions.{DomainMetadata, Format, Metadata, Protocol}\nimport io.delta.kernel.internal.checksum.CRCInfo.CRC_FILE_SCHEMA\nimport io.delta.kernel.internal.data.{GenericRow, StructRow}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.internal.util.VectorUtils.{buildArrayValue, buildColumnVector, stringStringMapValue}\nimport io.delta.kernel.test.{BaseMockJsonHandler, MockEngineUtils}\nimport io.delta.kernel.types.{StringType, StructType}\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for ChecksumWriter functionality.\n */\nclass ChecksumWriterSuite extends AnyFunSuite with MockEngineUtils {\n\n  private val FAKE_DELTA_LOG_PATH = new Path(\"/path/to/delta/log\")\n\n  // Schema field indices in crc file\n  private val TABLE_SIZE_BYTES_IDX = CRC_FILE_SCHEMA.indexOf(\"tableSizeBytes\")\n  private val NUM_FILES_IDX = CRC_FILE_SCHEMA.indexOf(\"numFiles\")\n  private val NUM_METADATA_IDX = CRC_FILE_SCHEMA.indexOf(\"numMetadata\")\n  private val NUM_PROTOCOL_IDX = CRC_FILE_SCHEMA.indexOf(\"numProtocol\")\n  private val TXN_ID_IDX = CRC_FILE_SCHEMA.indexOf(\"txnId\")\n  private val DOMAIN_METADATA_IDX = CRC_FILE_SCHEMA.indexOf(\"domainMetadata\")\n  private val METADATA_IDX = CRC_FILE_SCHEMA.indexOf(\"metadata\")\n  private val PROTOCOL_IDX = CRC_FILE_SCHEMA.indexOf(\"protocol\")\n  private val FILE_SIZE_HISTOGRAM_IDX = CRC_FILE_SCHEMA.indexOf(\"fileSizeHistogram\")\n\n  test(\"write checksum\") {\n    val jsonHandler = new MockCheckSumFileJsonWriter()\n    val checksumWriter = new ChecksumWriter(FAKE_DELTA_LOG_PATH)\n    val protocol = createTestProtocol()\n    val metadata = createTestMetadata()\n\n    def testChecksumWrite(\n        txn: Optional[String],\n        domainMetadata: Optional[util.Set[DomainMetadata]]): Unit = {\n      val version = 1L\n      val tableSizeBytes = 100L\n      val numFiles = 1L\n\n      // TODO when we support writing fileSizeHistogram as part of CRC update this to be non-empty\n      checksumWriter.writeCheckSum(\n        mockEngine(jsonHandler = jsonHandler),\n        new CRCInfo(\n          version,\n          metadata,\n          protocol,\n          tableSizeBytes,\n          numFiles,\n          txn,\n          domainMetadata,\n          Optional.empty()))\n\n      verifyChecksumFile(jsonHandler, version)\n      verifyChecksumContent(\n        jsonHandler.capturedCrcRow.get,\n        tableSizeBytes,\n        numFiles,\n        metadata,\n        protocol,\n        txn,\n        domainMetadata)\n    }\n\n    // Test with and without transaction ID, domain metadata\n    testChecksumWrite(Optional.of(\"txn\"), Optional.empty())\n    testChecksumWrite(Optional.empty(), Optional.empty())\n    testChecksumWrite(\n      Optional.empty(),\n      Optional.of(Seq(\n        new DomainMetadata(\"domain1\", \"\", false /* removed */ ),\n        new DomainMetadata(\"domain2\", \"\", false /* removed */ )).toSet.asJava))\n    // Per protocol, domain metadata list should exclude tombstone.\n    val exception = intercept[IllegalArgumentException] {\n      testChecksumWrite(\n        Optional.empty(),\n        Optional.of(Seq(\n          new DomainMetadata(\"domain1\", \"\", true /* removed */ ),\n          new DomainMetadata(\"domain2\", \"\", false /* removed */ )).toSet.asJava))\n    }\n    assert(exception.getMessage.contains(\"Domain metadata in CRC should exclude tombstones\"))\n  }\n\n  private def verifyChecksumFile(jsonHandler: MockCheckSumFileJsonWriter, version: Long): Unit = {\n    assert(jsonHandler.checksumFilePath == s\"$FAKE_DELTA_LOG_PATH/${\"%020d\".format(version)}.crc\")\n    assert(jsonHandler.capturedCrcRow.isDefined)\n    assert(jsonHandler.capturedCrcRow.get.getSchema == CRC_FILE_SCHEMA)\n  }\n\n  private def verifyChecksumContent(\n      actualCheckSumRow: Row,\n      expectedTableSizeBytes: Long,\n      expectedNumFiles: Long,\n      expectedMetadata: Metadata,\n      expectedProtocol: Protocol,\n      expectedTxnId: Optional[String],\n      expectedDomainMetadata: Optional[util.Set[DomainMetadata]]): Unit = {\n    assert(!actualCheckSumRow.isNullAt(TABLE_SIZE_BYTES_IDX) && actualCheckSumRow.getLong(\n      TABLE_SIZE_BYTES_IDX) == expectedTableSizeBytes)\n    assert(!actualCheckSumRow.isNullAt(\n      NUM_FILES_IDX) && actualCheckSumRow.getLong(NUM_FILES_IDX) == expectedNumFiles)\n    assert(!actualCheckSumRow.isNullAt(\n      NUM_METADATA_IDX) && actualCheckSumRow.getLong(NUM_METADATA_IDX) == 1L)\n    assert(!actualCheckSumRow.isNullAt(\n      NUM_PROTOCOL_IDX) && actualCheckSumRow.getLong(NUM_PROTOCOL_IDX) == 1L)\n    assert(expectedProtocol === Protocol.fromRow(actualCheckSumRow.getStruct(PROTOCOL_IDX)))\n    assert(expectedMetadata === Metadata.fromRow(actualCheckSumRow.getStruct(METADATA_IDX)))\n\n    if (expectedTxnId.isPresent) {\n      assert(actualCheckSumRow.getString(TXN_ID_IDX) == expectedTxnId.get())\n    } else {\n      assert(actualCheckSumRow.isNullAt(TXN_ID_IDX))\n    }\n\n    if (expectedDomainMetadata.isPresent) {\n      assert(VectorUtils.toJavaList[Row](actualCheckSumRow.getArray(DOMAIN_METADATA_IDX)).asScala\n        .map(DomainMetadata.fromRow).toSet\n        === expectedDomainMetadata.get().asScala)\n    } else {\n      assert(actualCheckSumRow.isNullAt(DOMAIN_METADATA_IDX))\n    }\n\n    // TODO once we support writing fileSizeHistogram as part of CRC check it here\n    assert(actualCheckSumRow.isNullAt(FILE_SIZE_HISTOGRAM_IDX))\n  }\n\n  private def createTestMetadata(): Metadata = {\n    new Metadata(\n      \"id\",\n      Optional.of(\"name\"),\n      Optional.of(\"description\"),\n      new Format(\"parquet\", Collections.emptyMap()),\n      DataTypeJsonSerDe.serializeDataType(new StructType()),\n      new StructType(),\n      buildArrayValue(util.Arrays.asList(\"c3\"), StringType.STRING),\n      Optional.of(123),\n      stringStringMapValue(new util.HashMap[String, String]() {\n        put(\"delta.appendOnly\", \"true\")\n      }))\n  }\n\n  private def createTestProtocol(): Protocol = {\n    new Protocol(\n      /* minReaderVersion= */ 1,\n      /* minWriterVersion= */ 2,\n      Collections.emptySet(),\n      Collections.emptySet())\n  }\n}\n\n/**\n * Mock implementation of JsonHandler for testing checksum file writing.\n */\nclass MockCheckSumFileJsonWriter extends BaseMockJsonHandler {\n  var capturedCrcRow: Option[Row] = None\n  var checksumFilePath: String = \"\"\n\n  override def writeJsonFileAtomically(\n      filePath: String,\n      data: CloseableIterator[Row],\n      overwrite: Boolean): Unit = {\n    checksumFilePath = filePath\n    assert(data.hasNext, \"Expected data iterator to contain exactly one row\")\n    capturedCrcRow = Some(data.next())\n    assert(!data.hasNext, \"Expected data iterator to contain exactly one row\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/clustering/ClusteringMetadataDomainSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.clustering\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.util.{ColumnMapping, ColumnMappingSuiteBase}\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ClusteringMetadataDomainSuite\n    extends AnyFunSuite\n    with ColumnMappingSuiteBase {\n\n  private def convertToPhysicalColumn(\n      logicalColumns: List[Column],\n      schema: StructType): List[Column] = {\n    logicalColumns.map { column =>\n      ColumnMapping.getPhysicalColumnNameAndDataType(schema, column)._1\n    }\n  }\n\n  test(\"ClusteringDomainMetadata can be serialized\") {\n    val clusteringColumns =\n      List(new Column(Array(\"col1\", \"`col2,col3`\", \"`col4.col5`,col6\")))\n    val clusteringMetadataDomain = ClusteringMetadataDomain.fromClusteringColumns(\n      clusteringColumns.asJava)\n    val serializedString = clusteringMetadataDomain.toDomainMetadata.toString\n    assert(serializedString ===\n      \"\"\"|DomainMetadata{domain='delta.clustering', configuration=\n         |'{\"clusteringColumns\":[[\"col1\",\"`col2,col3`\",\"`col4.col5`,col6\"]]}',\n         | removed='false'}\"\"\".stripMargin.replace(\"\\n\", \"\"))\n  }\n\n  test(\"ClusteringDomainMetadata can be deserialized\") {\n    val configJson = \"\"\"{\"clusteringColumns\":[[\"col1\",\"`col2,col3`\",\"`col4.col5`,col6\"]]}\"\"\"\n    val clusteringMD = ClusteringMetadataDomain.fromJsonConfiguration(configJson)\n\n    assert(clusteringMD.getClusteringColumns === List(new Column(Array(\n      \"col1\",\n      \"`col2,col3`\",\n      \"`col4.col5`,col6\"))).asJava)\n  }\n\n  test(\"Successfully get DomainMetadata for non-nested columns\") {\n    val schema = new StructType()\n      .add(\"id\", IntegerType.INTEGER, true)\n      .add(\"name\", IntegerType.INTEGER, true)\n      .add(\"age\", IntegerType.INTEGER, true)\n\n    val clusterColumns = List(new Column(\"name\"), new Column(\"age\"))\n    val physicalColumns = convertToPhysicalColumn(clusterColumns, schema)\n\n    val clusteringMetadataDomain =\n      ClusteringMetadataDomain.fromClusteringColumns(\n        physicalColumns.asJava)\n\n    val clusteringDomainMetadata = clusteringMetadataDomain.toDomainMetadata\n    assert(clusteringMetadataDomain.getClusteringColumns == clusterColumns.asJava)\n    assert(clusteringDomainMetadata.getDomain == \"delta.clustering\")\n    assert(clusteringDomainMetadata.getConfiguration ==\n      \"\"\"{\"clusteringColumns\":[[\"name\"],[\"age\"]]}\"\"\")\n  }\n\n  test(\"Successfully get DomainMetadata for nested columns\") {\n    val schema = new StructType()\n      .add(\"id\", IntegerType.INTEGER, true)\n      .add(\n        \"user\",\n        new StructType()\n          .add(\n            \"address\",\n            new StructType()\n              .add(\"city\", StringType.STRING, true)))\n\n    val clusterColumns = List(new Column(Array(\"user\", \"address\", \"city\")))\n    val physicalColumns = convertToPhysicalColumn(clusterColumns, schema)\n\n    val clusteringMetadataDomain = ClusteringMetadataDomain.fromClusteringColumns(\n      physicalColumns.asJava)\n\n    val clusteringDomainMetadata = clusteringMetadataDomain.toDomainMetadata\n    assert(clusteringMetadataDomain.getClusteringColumns ==\n      List(new Column(Array(\"user\", \"address\", \"city\"))).asJava)\n    assert(clusteringDomainMetadata.getDomain == \"delta.clustering\")\n    assert(clusteringDomainMetadata.getConfiguration ==\n      \"\"\"{\"clusteringColumns\":[[\"user\",\"address\",\"city\"]]}\"\"\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/columndefaults/ColumnDefaultsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.columndefaults\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.test.ActionUtils\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ColumnDefaultsSuite extends AnyFunSuite with ActionUtils {\n  val validProtocol = new Protocol(\n    TableFeatures.TABLE_FEATURES_MIN_READER_VERSION,\n    TableFeatures.TABLE_FEATURES_MIN_WRITER_VERSION,\n    Set.empty[String].asJava,\n    Set(\n      TableFeatures.ALLOW_COLUMN_DEFAULTS_W_FEATURE.featureName(),\n      TableFeatures.ICEBERG_COMPAT_V3_W_FEATURE.featureName()).asJava)\n\n  def metadataForDefault(value: String): FieldMetadata =\n    FieldMetadata.builder().putString(\"CURRENT_DEFAULT\", value).build()\n\n  test(\"validate schema with valid literals\") {\n    val correctSchema = new StructType()\n      .add(\"id\", IntegerType.INTEGER)\n      .add(\"int\", IntegerType.INTEGER, metadataForDefault(\"\\\"123\\\"\"))\n      .add(\"short\", ShortType.SHORT, metadataForDefault(\"123\"))\n      .add(\"long\", LongType.LONG, metadataForDefault(\"1231341\"))\n      .add(\"decimal\", new DecimalType(10, 5), metadataForDefault(\"1231.34155\"))\n      .add(\"float\", FloatType.FLOAT, metadataForDefault(\"123.1341\"))\n      .add(\"double\", DoubleType.DOUBLE, metadataForDefault(\"123.7774\"))\n      .add(\"double2\", DoubleType.DOUBLE, metadataForDefault(\"'123.7774'\"))\n      .add(\"name\", StringType.STRING, metadataForDefault(\"\\\"tom\\\"\"))\n      .add(\"name2\", StringType.STRING, metadataForDefault(\"'tom'\"))\n      .add(\"date\", DateType.DATE, metadataForDefault(\"'2025-01-01'\"))\n      .add(\"date2\", DateType.DATE, metadataForDefault(\"2025-01-01\"))\n      .add(\"ts\", TimestampType.TIMESTAMP, metadataForDefault(\"\\\"2025-01-01T00:00:00Z\\\"\"))\n      .add(\"ts2\", TimestampType.TIMESTAMP, metadataForDefault(\"\\\"2025-01-01T00:00:00+01:00\\\"\"))\n      .add(\"ts3\", TimestampType.TIMESTAMP, metadataForDefault(\"2025-01-01T00:00:00Z\"))\n      .add(\"tsntz\", TimestampNTZType.TIMESTAMP_NTZ, metadataForDefault(\"'2025-01-01T00:00:00'\"))\n      .add(\"tsntz2\", TimestampNTZType.TIMESTAMP_NTZ, metadataForDefault(\"2025-01-01T00:00:00\"))\n      .add(\n        \"childStruct\",\n        new StructType()\n          .add(\"childId\", IntegerType.INTEGER, metadataForDefault(\"100\"))\n          .add(\n            \"grandChildList\",\n            new ArrayType(\n              new StructType().add(\"nestedId\", IntegerType.INTEGER, metadataForDefault(\"120\")),\n              false)))\n      .add(\n        \"grandChildMap\",\n        new MapType(\n          new StructType().add(\"mapKeyId\", IntegerType.INTEGER, metadataForDefault(\"220\")),\n          new StructType().add(\"mapValueId\", IntegerType.INTEGER, metadataForDefault(\"330\")),\n          false))\n      .add(\n        \"childList\",\n        new ArrayType(\n          new StructType().add(\"clid\", IntegerType.INTEGER, metadataForDefault(\"300\")),\n          false))\n    ColumnDefaults.validateSchema(correctSchema, true, true)\n    var e = intercept[KernelException] {\n      ColumnDefaults.validateSchema(correctSchema, true, false)\n    }\n    assert(e.getMessage ==\n      \"In Delta Kernel, default values table feature requires IcebergCompatV3 to be enabled.\")\n    // Validate column default requires v3 even if schema has no defaults\n    e = intercept[KernelException] {\n      ColumnDefaults.validateSchema(new StructType().add(\"key\", IntegerType.INTEGER), true, false)\n    }\n    assert(e.getMessage ==\n      \"In Delta Kernel, default values table feature requires IcebergCompatV3 to be enabled.\")\n\n    e = intercept[KernelException] {\n      ColumnDefaults.validateSchema(correctSchema, false, true)\n    }\n    assert(e.getMessage == \"Found column defaults in the schema but the table does not support\" +\n      \" the columnDefaults table feature.\")\n  }\n\n  test(\"validate schema with unsupported types\") {\n    val unsupportedCases = Seq(\n      new StructType().add(\"sub\", IntegerType.INTEGER),\n      new ArrayType(IntegerType.INTEGER, false),\n      new MapType(IntegerType.INTEGER, IntegerType.INTEGER, false),\n      VariantType.VARIANT)\n    unsupportedCases.foreach { dataType =>\n      val schemaWithUnsupportedType = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"col1\", dataType, metadataForDefault(\"120\"))\n      val e = intercept[KernelException] {\n        ColumnDefaults.validateSchema(schemaWithUnsupportedType, true, true)\n      }\n      assert(e.getMessage.contains(\"Kernel does not support default value for data type\"))\n    }\n  }\n\n  test(\"validate schema with invalid literal values\") {\n    val badCases = Seq(\n      (StringType.STRING, \"string with no quotes\"),\n      (BinaryType.BINARY, \"string with no quotes\"),\n      (ShortType.SHORT, \"1248.995\"),\n      (IntegerType.INTEGER, \"1248.995\"),\n      (LongType.LONG, \"1248.995\"),\n      (FloatType.FLOAT, \"michael\"),\n      (DoubleType.DOUBLE, \"jordan\"),\n      (new DecimalType(10, 0), \"1248.995\"),\n      (new DecimalType(10, 5), \"12480031341.995\"),\n      (new DecimalType(10, 5), \"1248.995031\"),\n      (DateType.DATE, \"1248.995031\"),\n      (DateType.DATE, \"\\\"2025/09/01\\\"\"),\n      (DateType.DATE, \"09/01/2025\"),\n      (TimestampType.TIMESTAMP, \"2025-01-01\"),\n      (TimestampType.TIMESTAMP, \"2025-01-01\"),\n      (TimestampType.TIMESTAMP, \"2025-01-01 00:00:00\"),\n      (TimestampType.TIMESTAMP, \"'2025-01-01T00:00:00'\"),\n      (TimestampType.TIMESTAMP, \"2025-01-01T00:00:00+2:00\"),\n      (TimestampNTZType.TIMESTAMP_NTZ, \"2025-01-01\"),\n      (TimestampNTZType.TIMESTAMP_NTZ, \"'2025-01-01 00:00:00'\"))\n\n    badCases.foreach { case (dataType, defaultValue) =>\n      val badSchema1 = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"col\", dataType, metadataForDefault(defaultValue))\n\n      val badSchema2 = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\n          \"childStruct\",\n          new StructType()\n            .add(\"childId\", IntegerType.INTEGER, metadataForDefault(\"100\"))\n            .add(\"badcol\", dataType, metadataForDefault(defaultValue)))\n      val badSchema3 = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"name\", StringType.STRING, metadataForDefault(\"tom\"))\n        .add(\n          \"childList\",\n          new ArrayType(\n            new StructType()\n              .add(\"clid\", IntegerType.INTEGER, metadataForDefault(\"300\"))\n              .add(\"badcol\", dataType, metadataForDefault(defaultValue)),\n            false))\n      val badSchema4 = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"name\", StringType.STRING, metadataForDefault(\"tom\"))\n        .add(\n          \"grandChildMap\",\n          new MapType(\n            new StructType().add(\"mapKeyId\", IntegerType.INTEGER, metadataForDefault(\"220\")),\n            new StructType().add(\"mapValueId\", dataType, metadataForDefault(defaultValue)),\n            false))\n      Seq(badSchema1, badSchema2, badSchema3, badSchema4).foreach(schema => {\n        val e = intercept[KernelException] {\n          ColumnDefaults.validateSchema(schema, true, true)\n        }\n        assert(e.getMessage.contains(\n          \"currently only literal values are supported for default values in Kernel.\"))\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/commit/CommitMetadataSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.commit\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.commit.CommitMetadata.CommitType\nimport io.delta.kernel.internal.actions.{DomainMetadata, Metadata, Protocol}\nimport io.delta.kernel.internal.util.{Tuple2 => KernelTuple2}\nimport io.delta.kernel.test.{TestFixtures, VectorTestUtils}\nimport io.delta.kernel.types.{IntegerType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CommitMetadataSuite extends AnyFunSuite\n    with TestFixtures\n    with VectorTestUtils {\n\n  private val protocol12 = new Protocol(1, 2)\n  private val logPath = \"/fake/_delta_log\"\n  private val createVersion0 = 0\n  private val updateVersionNonZero = 1\n\n  test(\"constructor validates non-negative version\") {\n    val ex = intercept[IllegalArgumentException] {\n      createCommitMetadata(version = -1L)\n    }\n    assert(ex.getMessage.contains(\"version must be non-negative\"))\n  }\n\n  test(\"constructor validates null parameters\") {\n    intercept[NullPointerException] {\n      createCommitMetadata(\n        version = updateVersionNonZero,\n        logPath = null,\n        readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n    }\n\n    intercept[NullPointerException] {\n      createCommitMetadata(\n        version = updateVersionNonZero,\n        commitInfo = null,\n        readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n    }\n\n    intercept[NullPointerException] {\n      createCommitMetadata(\n        version = createVersion0,\n        commitDomainMetadatas = null,\n        newProtocolOpt = Optional.of(protocol12),\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n    }\n\n    intercept[NullPointerException] {\n      createCommitMetadata(\n        version = updateVersionNonZero,\n        readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n        committerProperties = null)\n    }\n\n    intercept[NullPointerException] {\n      createCommitMetadata(\n        version = updateVersionNonZero,\n        readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n        maxKnownPublishedDeltaVersion = null)\n    }\n  }\n\n  test(\"constructor validates readProtocol and readMetadata consistency\") {\n    // Both present is valid\n    createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n\n    // Both absent is valid if new ones are present\n    createCommitMetadata(\n      version = createVersion0,\n      newProtocolOpt = Optional.of(protocol12),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata))\n  }\n\n  test(\"constructor validates at least one protocol must be present\") {\n    intercept[IllegalArgumentException] {\n      createCommitMetadata(\n        version = createVersion0,\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n    }\n  }\n\n  test(\"constructor validates at least one metadata must be present\") {\n    intercept[IllegalArgumentException] {\n      createCommitMetadata(\n        version = createVersion0,\n        newProtocolOpt = Optional.of(protocol12))\n    }\n  }\n\n  test(\"constructor validates ICT present if catalogManaged enabled\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createCommitMetadata(\n        version = createVersion0,\n        commitInfo = testCommitInfo(ictEnabled = false),\n        newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n    }.getMessage\n\n    assert(exMsg.contains(\"InCommitTimestamp must be present for commits to catalogManaged tables\"))\n  }\n\n  test(\"getNewDomainMetadatas returns provided domain metadata\") {\n    val domainMetadata1 = new DomainMetadata(\"domain1\", \"\"\"{\"key\":\"value\"}\"\"\", false)\n    val domainMetadata2 = new DomainMetadata(\"domain2\", \"\", false)\n    val domainMetadatas = List(domainMetadata1, domainMetadata2)\n\n    val commitMetadata = createCommitMetadata(\n      version = createVersion0,\n      commitDomainMetadatas = domainMetadatas,\n      newProtocolOpt = Optional.of(protocol12),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata))\n\n    val returnedMetadatas = commitMetadata.getCommitDomainMetadatas\n    assert(returnedMetadatas.size() == 2)\n    assert(returnedMetadatas.contains(domainMetadata1))\n    assert(returnedMetadatas.contains(domainMetadata2))\n  }\n\n  test(\"getCommitterProperties returns provided supplier\") {\n    val props = Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\").asJava\n\n    val commitMetadata = createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      committerProperties = () => props)\n\n    assert(commitMetadata.getCommitterProperties.get() == props)\n  }\n\n  test(\"getEffectiveProtocol returns new protocol when present\") {\n    val newProtocol = new Protocol(2, 3)\n    val commitMetadata = createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      newProtocolOpt = Optional.of(newProtocol))\n\n    assert(commitMetadata.getEffectiveProtocol == newProtocol)\n  }\n\n  test(\"getEffectiveProtocol returns read protocol when new protocol absent\") {\n    val commitMetadata = createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n\n    assert(commitMetadata.getEffectiveProtocol == protocol12)\n  }\n\n  test(\"getEffectiveMetadata returns new metadata when present\") {\n    val newMetadata = testMetadata(new StructType().add(\"newCol\", IntegerType.INTEGER))\n    val commitMetadata = createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      newMetadataOpt = Optional.of(newMetadata))\n\n    assert(commitMetadata.getEffectiveMetadata == newMetadata)\n  }\n\n  test(\"getEffectiveMetadata returns read metadata when new metadata absent\") {\n    val commitMetadata = createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n\n    assert(commitMetadata.getEffectiveMetadata == basicPartitionedMetadata)\n  }\n\n  // ========== CommitType Tests START ==========\n\n  case class CommitTypeTestCase(\n      readPandMOpt: Optional[KernelTuple2[Protocol, Metadata]] = Optional.empty(),\n      newProtocolOpt: Optional[Protocol] = Optional.empty(),\n      newMetadataOpt: Optional[Metadata] = Optional.empty(),\n      expectedCommitType: CommitType)\n\n  private val commitTypeTestCases = Seq(\n    CommitTypeTestCase(\n      readPandMOpt = Optional.empty(), // No read state for table creation\n      newProtocolOpt = Optional.of(protocol12),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata),\n      expectedCommitType = CommitType.FILESYSTEM_CREATE),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.empty(), // No read state for table creation\n      newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata),\n      expectedCommitType = CommitType.CATALOG_CREATE),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      expectedCommitType = CommitType.FILESYSTEM_WRITE),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(\n        new KernelTuple2(protocolWithCatalogManagedSupport, basicPartitionedMetadata)),\n      expectedCommitType = CommitType.CATALOG_WRITE),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n      expectedCommitType = CommitType.FILESYSTEM_UPGRADE_TO_CATALOG),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(\n        new KernelTuple2(protocolWithCatalogManagedSupport, basicPartitionedMetadata)),\n      newProtocolOpt = Optional.of(protocol12),\n      expectedCommitType = CommitType.CATALOG_DOWNGRADE_TO_FILESYSTEM))\n\n  commitTypeTestCases.foreach { testCase =>\n    test(s\"getCommitType returns ${testCase.expectedCommitType}\") {\n      // version > 0 for writes, version 0 for create\n      val version = if (testCase.readPandMOpt.isPresent) 1L else 0L\n\n      val commitMetadata = createCommitMetadata(\n        version = version,\n        logPath = logPath,\n        readPandMOpt = testCase.readPandMOpt,\n        newProtocolOpt = testCase.newProtocolOpt,\n        newMetadataOpt = testCase.newMetadataOpt)\n\n      assert(commitMetadata.getCommitType == testCase.expectedCommitType)\n    }\n  }\n\n  // ========== CommitType Tests END ==========\n\n  test(\"checkReadStateAbsentIfAndOnlyIfVersion0 - version 0 with absent readState should pass\") {\n    // This should pass: version 0 (table creation) with absent readPandMOpt\n    createCommitMetadata(\n      version = createVersion0,\n      newProtocolOpt = Optional.of(protocol12),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata))\n  }\n\n  test(\"checkReadStateAbsentIfAndOnlyIfVersion0 - version 0 with present readState should fail\") {\n    // This should fail: version 0 (table creation) with present readPandMOpt\n    val exMsg = intercept[IllegalArgumentException] {\n      createCommitMetadata(\n        version = createVersion0,\n        readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n    }.getMessage\n    assert(exMsg.contains(\"Table creation (version 0) requires absent readPandMOpt\"))\n  }\n\n  test(\"checkReadStateAbsentIfAndOnlyIfVersion0 - version > 0 with present readState should pass\") {\n    // This should pass: version > 0 (existing table) with present readPandMOpt\n    createCommitMetadata(\n      version = updateVersionNonZero,\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n  }\n\n  test(\"checkReadStateAbsentIfAndOnlyIfVersion0 - version > 0 with absent readState should fail\") {\n    // This should fail: version > 0 (existing table) with absent readPandMOpt\n    val exMsg = intercept[IllegalArgumentException] {\n      createCommitMetadata(\n        version = updateVersionNonZero,\n        newProtocolOpt = Optional.of(protocol12),\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n    }.getMessage\n    assert(exMsg.contains(\"existing table writes (version > 0) require present readPandMOpt\"))\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/commit/DefaultCommitterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.commit\n\nimport java.io.IOException\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.Optional\n\nimport io.delta.kernel.TableManager\nimport io.delta.kernel.commit.{CommitFailedException, CommitMetadata}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.exceptions.KernelEngineException\nimport io.delta.kernel.internal.actions.Protocol\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.{FileNames, Tuple2 => KernelTuple2}\nimport io.delta.kernel.test.{ActionUtils, BaseMockFileSystemClient, BaseMockJsonHandler, MockFileSystemClientUtils, TestFixtures, VectorTestUtils}\nimport io.delta.kernel.types.{IntegerType, StructType}\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DefaultCommitterSuite extends AnyFunSuite\n    with MockFileSystemClientUtils\n    with TestFixtures\n    with VectorTestUtils {\n\n  private val protocol12 = new Protocol(1, 2)\n\n  private val basicFileSystemCommitMetadataNoPMChange = createCommitMetadata(\n    version = 1L,\n    commitInfo = testCommitInfo(ictEnabled = false),\n    readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)))\n\n  Seq(\n    (protocol12, protocolWithCatalogManagedSupport, \"Upgrade\"),\n    (protocolWithCatalogManagedSupport, protocol12, \"Downgrade\"),\n    (\n      protocolWithCatalogManagedSupport,\n      protocolWithCatalogManagedSupport,\n      \"CatalogManagedWrite\")).foreach { case (readProtocol, newProtocol, testCase) =>\n    test(s\"default committer does not support committing to catalog-managed tables -- $testCase\") {\n      val emptyMockEngine = createMockFSListFromEngine(Nil)\n      val schema = new StructType().add(\"col1\", IntegerType.INTEGER)\n      val metadata = testMetadata(schema, Seq[String]())\n      val committer = TableManager.loadSnapshot(dataPath.toString)\n        .asInstanceOf[SnapshotBuilderImpl]\n        .withProtocolAndMetadata(readProtocol, metadata)\n        .atVersion(1)\n        .withMaxCatalogVersionIfApplicable(\n          readProtocol.supportsFeature(TableFeatures.CATALOG_MANAGED_RW_FEATURE),\n          1).build(emptyMockEngine)\n        .getCommitter\n\n      assert(committer.isInstanceOf[DefaultFileSystemManagedTableOnlyCommitter])\n\n      val exMsg = intercept[UnsupportedOperationException] {\n        committer.commit(\n          emptyMockEngine,\n          emptyActionsIterator,\n          createCommitMetadata(\n            version = 3L,\n            readPandMOpt = Optional.of(new KernelTuple2(readProtocol, metadata)),\n            newProtocolOpt = Optional.of(newProtocol),\n            newMetadataOpt = Optional.of(metadata)))\n      }.getMessage\n\n      assert(exMsg.contains(\"No io.delta.kernel.commit.Committer has been provided to Kernel, so \" +\n        \"Kernel is using a default Committer that only supports committing to \" +\n        \"filesystem-managed Delta tables, not catalog-managed Delta tables. Since this table \" +\n        \"is catalog-managed, this commit operation is unsupported\"))\n    }\n  }\n\n  ////////////////////////////////////////////////////////\n  // DefaultCommitter exception handling tests -- START //\n  ////////////////////////////////////////////////////////\n\n  case class ExceptionTestCase(\n      description: String,\n      exceptionToThrow: Exception,\n      expectedRetryableOpt: Option[Boolean],\n      expectedConflictOpt: Option[Boolean],\n      expectedThrownType: Class[_],\n      expectedCauseType: Class[_])\n\n  private val exceptionTestCases = Seq(\n    ExceptionTestCase(\n      description = \"FileAlreadyExistsException -> CFE(true, true)\",\n      exceptionToThrow = new FileAlreadyExistsException(\"_delta_log/001.json\"),\n      expectedRetryableOpt = Some(true),\n      expectedConflictOpt = Some(true),\n      expectedThrownType = classOf[CommitFailedException],\n      expectedCauseType = classOf[FileAlreadyExistsException]),\n    ExceptionTestCase(\n      description = \"IOException -> CFE(true, false)\",\n      exceptionToThrow = new IOException(\"Network timeout writing to _delta_log/001.json\"),\n      expectedRetryableOpt = Some(true),\n      expectedConflictOpt = Some(false),\n      expectedThrownType = classOf[CommitFailedException],\n      expectedCauseType = classOf[IOException]),\n    ExceptionTestCase(\n      description = \"RuntimeException wrapped and thrown as KernelEngineException\",\n      exceptionToThrow = new RuntimeException(\"Some runtime error\"),\n      expectedRetryableOpt = None,\n      expectedConflictOpt = None,\n      expectedThrownType = classOf[KernelEngineException],\n      expectedCauseType = classOf[RuntimeException]))\n\n  exceptionTestCases.foreach { testCase =>\n    test(s\"default committer handles ${testCase.description} correctly\") {\n\n      val throwingEngine = mockEngine(jsonHandler = new BaseMockJsonHandler {\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          throw testCase.exceptionToThrow\n        }\n      })\n\n      val ex = intercept[Exception] {\n        DefaultFileSystemManagedTableOnlyCommitter.INSTANCE.commit(\n          throwingEngine,\n          emptyActionsIterator,\n          basicFileSystemCommitMetadataNoPMChange)\n      }\n\n      assert(ex.getClass == testCase.expectedThrownType)\n      assert(ex.getCause.getClass == testCase.expectedCauseType)\n\n      testCase.expectedRetryableOpt.foreach { expectedRetryable =>\n        assert(ex.isInstanceOf[CommitFailedException])\n        val commitEx = ex.asInstanceOf[CommitFailedException]\n        assert(commitEx.isRetryable == expectedRetryable)\n      }\n\n      testCase.expectedConflictOpt.foreach { expectedConflict =>\n        assert(ex.isInstanceOf[CommitFailedException])\n        val commitEx = ex.asInstanceOf[CommitFailedException]\n        assert(commitEx.isConflict == expectedConflict)\n      }\n    }\n  }\n\n  //////////////////////////////////////////////////////\n  // DefaultCommitter exception handling tests -- END //\n  //////////////////////////////////////////////////////\n\n  test(\"success commit returns ParsedLogData containing FileStatus for that commit file\") {\n    var writtenFileStatus = Option.empty[FileStatus]\n\n    val fakeWriteReadJsonEngine = mockEngine(\n      jsonHandler = new BaseMockJsonHandler {\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          writtenFileStatus = Some(FileStatus.of(filePath, 1234L, 4567L)) // (path, size, modTime)\n        }\n      },\n      fileSystemClient = new BaseMockFileSystemClient {\n        override def getFileStatus(path: String): FileStatus = writtenFileStatus.get\n      })\n\n    val commitResult = DefaultFileSystemManagedTableOnlyCommitter.INSTANCE.commit(\n      fakeWriteReadJsonEngine,\n      emptyActionsIterator,\n      basicFileSystemCommitMetadataNoPMChange)\n\n    val commit = commitResult.getCommitLogData\n\n    assert(commit.isFile)\n    assert(commit.getVersion === basicFileSystemCommitMetadataNoPMChange.getVersion)\n    assert(commit.getFileStatus === writtenFileStatus.get)\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/commit/PublishMetadataSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.commit\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.commit.PublishMetadata\nimport io.delta.kernel.test.TestFixtures\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PublishMetadataSuite extends AnyFunSuite with TestFixtures {\n\n  private val logPath = \"/fake/_delta_log\"\n\n  ////////////////////\n  // Negative Tests //\n  ////////////////////\n\n  test(\"constructor validates null logPath\") {\n    val commits = List(createStagedCatalogCommit(1)).asJava\n    intercept[NullPointerException] {\n      new PublishMetadata(1, null, commits)\n    }\n  }\n\n  test(\"constructor validates null ascendingCatalogCommits\") {\n    intercept[NullPointerException] {\n      new PublishMetadata(1, logPath, null)\n    }\n  }\n\n  test(\"constructor validates non-empty commits\") {\n    val ex = intercept[IllegalArgumentException] {\n      new PublishMetadata(1, logPath, List.empty.asJava)\n    }\n    assert(ex.getMessage.contains(\"ascendingCatalogCommits must be non-empty\"))\n  }\n\n  test(\"constructor validates contiguous commits - gap in sequence\") {\n    val commits = List(\n      createStagedCatalogCommit(1),\n      createStagedCatalogCommit(2),\n      createStagedCatalogCommit(4) // Gap: missing version 3\n    ).asJava\n\n    val ex = intercept[IllegalArgumentException] {\n      new PublishMetadata(4, logPath, commits)\n    }\n    assert(ex.getMessage.contains(\"must be sorted and contiguous\"))\n  }\n\n  test(\"constructor validates sorted commits - out of order\") {\n    val commits = List(\n      createStagedCatalogCommit(2),\n      createStagedCatalogCommit(1), // Out of order\n      createStagedCatalogCommit(3)).asJava\n\n    val ex = intercept[IllegalArgumentException] {\n      new PublishMetadata(3, logPath, commits)\n    }\n    assert(ex.getMessage.contains(\"must be sorted and contiguous\"))\n  }\n\n  test(\"constructor validates last commit matches snapshot version\") {\n    val commits = List(\n      createStagedCatalogCommit(1),\n      createStagedCatalogCommit(2),\n      createStagedCatalogCommit(3)).asJava\n\n    val ex = intercept[IllegalArgumentException] {\n      new PublishMetadata(5, logPath, commits) // Snapshot is 5, but last commit is 3\n    }\n    assert(ex.getMessage.contains(\"Last catalog commit version 3 must equal snapshot version 5\"))\n  }\n\n  ////////////////////\n  // Positive Tests //\n  ////////////////////\n\n  test(\"valid construction with single commit\") {\n    val commits = List(createStagedCatalogCommit(5)).asJava\n    val publishMetadata = new PublishMetadata(5, logPath, commits)\n\n    assert(publishMetadata.getSnapshotVersion == 5)\n    assert(publishMetadata.getLogPath == logPath)\n    assert(publishMetadata.getAscendingCatalogCommits == commits)\n  }\n\n  test(\"valid construction with multiple contiguous commits\") {\n    val commits = List(\n      createStagedCatalogCommit(3),\n      createStagedCatalogCommit(4),\n      createStagedCatalogCommit(5)).asJava\n    val publishMetadata = new PublishMetadata(5, logPath, commits)\n\n    assert(publishMetadata.getSnapshotVersion == 5)\n    assert(publishMetadata.getAscendingCatalogCommits.size() == 3)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/files/LogDataUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.test.{MockFileSystemClientUtils, VectorTestUtils}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LogDataUtilsSuite extends AnyFunSuite with MockFileSystemClientUtils with VectorTestUtils {\n\n  private val emptyInlineData = emptyColumnarBatch\n\n  //////////////////////////////////////////////////////\n  // validateLogDataContainsOnlyRatifiedStagedCommits //\n  //////////////////////////////////////////////////////\n\n  test(\"validateLogDataContainsOnlyRatifiedStagedCommits: empty list passes\") {\n    // Should not throw any exception\n    LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(Seq.empty.asJava)\n  }\n\n  test(\"validateLogDataContainsOnlyRatifiedStagedCommits: valid list passes\") {\n    val logDatas = Seq(\n      ParsedCatalogCommitData.forFileStatus(stagedCommitFile(1)),\n      ParsedCatalogCommitData.forFileStatus(stagedCommitFile(2)))\n    // Should not throw any exception\n    LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(logDatas.asJava)\n  }\n\n  test(\"validateLogDataContainsOnlyRatifiedStagedCommits: inline delta fails\") {\n    intercept[IllegalArgumentException] {\n      LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(\n        Seq(ParsedCatalogCommitData.forInlineData(3, emptyInlineData)).asJava)\n    }\n  }\n\n  test(\"validateLogDataContainsOnlyRatifiedStagedCommits: published delta fails\") {\n    intercept[IllegalArgumentException] {\n      LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(\n        Seq(ParsedPublishedDeltaData.forFileStatus(deltaFileStatus(1))).asJava)\n    }\n  }\n\n  test(\"validateLogDataContainsOnlyRatifiedStagedCommits: checkpoint data fails\") {\n    intercept[IllegalArgumentException] {\n      LogDataUtils.validateLogDataContainsOnlyRatifiedStagedCommits(\n        Seq(ParsedClassicCheckpointData.forFileStatus(classicCheckpointFileStatus(0))).asJava)\n    }\n  }\n\n  ///////////////////////////////////////\n  // validateLogDataIsSortedContiguous //\n  ///////////////////////////////////////\n\n  test(\"validateLogDataIsSortedContiguous: empty list should pass\") {\n    // Should not throw any exception\n    LogDataUtils.validateLogDataIsSortedContiguous(Seq.empty.asJava)\n  }\n\n  test(\"validateLogDataIsSortedContiguous: single element should pass\") {\n    val singleElement = Seq(ParsedDeltaData.forFileStatus(deltaFileStatus(1)))\n\n    // Should not throw any exception\n    LogDataUtils.validateLogDataIsSortedContiguous(singleElement.asJava)\n  }\n\n  test(\"validateLogDataIsSortedContiguous: contiguous versions should pass\") {\n    val contiguousData = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(3)))\n\n    // Should not throw any exception\n    LogDataUtils.validateLogDataIsSortedContiguous(contiguousData.asJava)\n  }\n\n  test(\"validateLogDataIsSortedContiguous: non-contiguous versions should fail\") {\n    val nonContiguousData = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(3)) // Missing version 2\n    )\n\n    intercept[IllegalArgumentException] {\n      LogDataUtils.validateLogDataIsSortedContiguous(nonContiguousData.asJava)\n    }\n  }\n\n  test(\"validateLogDataIsSortedContiguous: unsorted versions should fail\") {\n    val unsortedData = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)))\n\n    intercept[IllegalArgumentException] {\n      LogDataUtils.validateLogDataIsSortedContiguous(unsortedData.asJava)\n    }\n  }\n\n  test(\"validateLogDataIsSortedContiguous: duplicate versions should fail\") {\n    val duplicateData = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)))\n\n    intercept[IllegalArgumentException] {\n      LogDataUtils.validateLogDataIsSortedContiguous(duplicateData.asJava)\n    }\n  }\n\n  test(\"validateLogDataIsSortedContiguous: mixed log data types should work\") {\n    val mixedData = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedLogData.forFileStatus(checksumFileStatus(2)),\n      ParsedLogData.forFileStatus(classicCheckpointFileStatus(3)))\n\n    // Should not throw any exception\n    LogDataUtils.validateLogDataIsSortedContiguous(mixedData.asJava)\n  }\n\n  //////////////////////////////////////////////////////////\n  // combinePublishedAndRatifiedDeltasWithCatalogPriority //\n  //////////////////////////////////////////////////////////\n\n  test(\"combinePublishedAndRatifiedDeltasWithCatalogPriority: empty published, empty ratified\") {\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      Seq.empty.asJava,\n      Seq.empty.asJava)\n    assert(result.isEmpty)\n  }\n\n  test(\n    \"combinePublishedAndRatifiedDeltasWithCatalogPriority: empty published, non-empty ratified\") {\n    val ratifiedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(stagedCommitFile(1)),\n      ParsedDeltaData.forFileStatus(stagedCommitFile(2)))\n\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      Seq.empty.asJava,\n      ratifiedDeltas.asJava)\n\n    assert(result.asScala === ratifiedDeltas)\n  }\n\n  test(\n    \"combinePublishedAndRatifiedDeltasWithCatalogPriority: non-empty published, empty ratified\") {\n    val publishedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)))\n\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      publishedDeltas.asJava,\n      Seq.empty.asJava)\n\n    assert(result.asScala === publishedDeltas)\n  }\n\n  test(\"combinePublishedAndRatifiedDeltasWithCatalogPriority: non-overlapping ranges\") {\n    val publishedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)))\n    val ratifiedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(stagedCommitFile(3)),\n      ParsedDeltaData.forFileStatus(stagedCommitFile(4)))\n\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      publishedDeltas.asJava,\n      ratifiedDeltas.asJava)\n\n    assert(result.asScala === publishedDeltas ++ ratifiedDeltas)\n  }\n\n  test(\n    \"combinePublishedAndRatifiedDeltasWithCatalogPriority: \" +\n      \"overlapping ranges - ratified priority\") {\n    val publishedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)))\n    val ratifiedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(stagedCommitFile(2)),\n      ParsedDeltaData.forFileStatus(stagedCommitFile(3)))\n\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      publishedDeltas.asJava,\n      ratifiedDeltas.asJava)\n\n    val expected = Seq(publishedDeltas.head) ++ ratifiedDeltas\n    assert(result.asScala === expected)\n  }\n\n  test(\"combinePublishedAndRatifiedDeltasWithCatalogPriority: ratified in middle of published\") {\n    val publishedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(3)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(4)))\n    val ratifiedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(stagedCommitFile(2)),\n      ParsedDeltaData.forFileStatus(stagedCommitFile(3)))\n\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      publishedDeltas.asJava,\n      ratifiedDeltas.asJava)\n\n    val expected = Seq(publishedDeltas.head) ++ ratifiedDeltas ++ Seq(publishedDeltas(3))\n\n    assert(result.asScala === expected)\n  }\n\n  test(\"combinePublishedAndRatifiedDeltasWithCatalogPriority: single ratified version\") {\n    val publishedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(deltaFileStatus(1)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(2)),\n      ParsedDeltaData.forFileStatus(deltaFileStatus(3)))\n    val ratifiedDeltas = Seq(\n      ParsedDeltaData.forFileStatus(stagedCommitFile(3)))\n\n    val result = LogDataUtils.combinePublishedAndRatifiedDeltasWithCatalogPriority(\n      publishedDeltas.asJava,\n      ratifiedDeltas.asJava)\n\n    val expected = Seq(publishedDeltas(0), publishedDeltas(1)) ++ ratifiedDeltas\n\n    assert(result.asScala === expected)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/files/ParsedLogDataSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.files\n\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.test.{MockFileSystemClientUtils, VectorTestUtils}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.matchers.must.Matchers.be\nimport org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper\n\nclass ParsedLogDataSuite extends AnyFunSuite with MockFileSystemClientUtils with VectorTestUtils {\n\n  private val emptyInlineData = emptyColumnarBatch\n\n  /////////////\n  // General //\n  /////////////\n\n  test(\"ParsedLogData throws on unknown log file\") {\n    val fileStatus = FileStatus.of(\"unknown\", 0, 0)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedLogData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Unknown log file type\"))\n  }\n\n  test(\"ParsedLogData (super) throws on version < 0\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedCatalogCommitData.forInlineData(-1, emptyInlineData)\n    }.getMessage\n    assert(exMsg === \"version must be non-negative\")\n  }\n\n  test(\"ParsedLogData: different types are not equal\") {\n    val delta = ParsedLogData.forFileStatus(deltaFileStatus(5))\n    val checksum = ParsedLogData.forFileStatus(checksumFileStatus(5))\n    val checkpoint = ParsedLogData.forFileStatus(classicCheckpointFileStatus(5))\n    val logCompaction = ParsedLogData.forFileStatus(logCompactionStatus(5, 10))\n\n    assert(delta != checksum)\n    assert(delta != checkpoint)\n    assert(delta != logCompaction)\n    assert(checksum != checkpoint)\n    assert(checksum != logCompaction)\n    assert(checkpoint != logCompaction)\n  }\n\n  //////////////////////////////\n  // ParsedPublishedDeltaData //\n  //////////////////////////////\n\n  test(\"ParsedLogData.forFileStatus(publishedDelta) creates a ParsedPublishedDeltaData\") {\n    val fileStatus = deltaFileStatus(5)\n    val parsed = ParsedLogData.forFileStatus(fileStatus)\n    assert(parsed.isInstanceOf[ParsedPublishedDeltaData])\n  }\n\n  test(\"ParsedPublishedDeltaData: correctly parses published delta file\") {\n    val fileStatus = deltaFileStatus(5)\n    val parsed = ParsedPublishedDeltaData.forFileStatus(fileStatus)\n\n    assert(parsed.isInstanceOf[ParsedDeltaData])\n    assert(parsed.getVersion == 5)\n    assert(parsed.isFile)\n    assert(!parsed.isInline)\n    assert(parsed.getFileStatus == fileStatus)\n  }\n\n  test(\"ParsedPublishedDeltaData: throws on staged commit file\") {\n    val fileStatus = stagedCommitFile(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedPublishedDeltaData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a published Delta file but got\"))\n  }\n\n  test(\"ParsedDeltaData: equality\") {\n    val fileStatus1 = deltaFileStatus(5)\n    val fileStatus2 = deltaFileStatus(5)\n    val fileStatus3 = deltaFileStatus(6)\n\n    val delta1 = ParsedPublishedDeltaData.forFileStatus(fileStatus1)\n    val delta2 = ParsedPublishedDeltaData.forFileStatus(fileStatus2)\n    val delta3 = ParsedPublishedDeltaData.forFileStatus(fileStatus3)\n\n    assert(delta1 == delta1)\n    assert(delta1 == delta2)\n    assert(delta1 != delta3)\n  }\n\n  /////////////////////////////\n  // ParsedCatalogCommitData //\n  /////////////////////////////\n\n  test(\"ParsedLogData.forFileStatus(stagedCommit) creates a ParsedCatalogCommitData\") {\n    val fileStatus = stagedCommitFile(5)\n    val parsed = ParsedLogData.forFileStatus(fileStatus)\n    assert(parsed.isInstanceOf[ParsedCatalogCommitData])\n  }\n\n  test(\"ParsedCatalogCommitData: correctly parses staged commit file\") {\n    val fileStatus = stagedCommitFile(5)\n    val parsed = ParsedCatalogCommitData.forFileStatus(fileStatus)\n\n    assert(parsed.isInstanceOf[ParsedDeltaData])\n    assert(parsed.getVersion == 5)\n    assert(parsed.isFile)\n    assert(!parsed.isInline)\n    assert(parsed.getFileStatus == fileStatus)\n  }\n\n  test(\"ParsedCatalogCommitData: can construct inline data\") {\n    val parsed = ParsedCatalogCommitData.forInlineData(10, emptyInlineData)\n    assert(parsed.getVersion == 10)\n    assert(parsed.isInline)\n    assert(!parsed.isFile)\n    assert(parsed.getInlineData == emptyInlineData)\n  }\n\n  test(\"ParsedCatalogCommitData: throws on published delta file\") {\n    val fileStatus = deltaFileStatus(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedCatalogCommitData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a staged commit file but got\"))\n  }\n\n  test(\"ParsedCatalogCommitData: equality\") {\n    val fileStatus1 = stagedCommitFile(5)\n    val fileStatus3 = stagedCommitFile(6)\n\n    val catalogCommit1 = ParsedCatalogCommitData.forFileStatus(fileStatus1)\n    val catalogCommit2 = ParsedCatalogCommitData.forFileStatus(fileStatus1)\n    val catalogCommit3 = ParsedCatalogCommitData.forFileStatus(fileStatus3)\n\n    assert(catalogCommit1 == catalogCommit1)\n    assert(catalogCommit1 == catalogCommit2)\n    assert(catalogCommit1 != catalogCommit3)\n  }\n\n  //////////////////////////\n  // ParsedCheckpointData //\n  //////////////////////////\n\n  test(\"ParsedClassicCheckpointData: throws on non-classic checkpoint file\") {\n    val fileStatus = deltaFileStatus(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedClassicCheckpointData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a classic checkpoint file but got\"))\n  }\n\n  test(\"ParsedClassicCheckpointData: correctly parses classic checkpoint file\") {\n    val fileStatus = classicCheckpointFileStatus(10)\n    val parsed = ParsedLogData.forFileStatus(fileStatus)\n\n    assert(parsed.isInstanceOf[ParsedClassicCheckpointData])\n    assert(parsed.getVersion == 10)\n    assert(parsed.getGroupByCategoryClass == classOf[ParsedCheckpointData])\n    assert(parsed.isFile)\n    assert(!parsed.isInline)\n    assert(parsed.getFileStatus == fileStatus)\n  }\n\n  test(\"ParsedClassicCheckpointData: equality\") {\n    val cp1 = ParsedLogData.forFileStatus(classicCheckpointFileStatus(10))\n    val cp2 = ParsedLogData.forFileStatus(classicCheckpointFileStatus(10))\n    val cp3 = ParsedLogData.forFileStatus(classicCheckpointFileStatus(11))\n\n    assert(cp1 == cp1)\n    assert(cp1 == cp2)\n    assert(cp1 != cp3)\n  }\n\n  test(\"ParsedV2CheckpointData: throws on non-V2 checkpoint file\") {\n    val fileStatus = deltaFileStatus(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedV2CheckpointData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a V2 checkpoint file but got\"))\n  }\n\n  test(\"ParsedV2CheckpointData: correctly parses V2 checkpoint file\") {\n    val fileStatus = v2CheckpointFileStatus(20)\n    val parsed = ParsedLogData.forFileStatus(fileStatus)\n\n    assert(parsed.isInstanceOf[ParsedCheckpointData])\n    assert(parsed.getVersion == 20)\n    assert(parsed.getGroupByCategoryClass == classOf[ParsedCheckpointData])\n    assert(parsed.isFile)\n    assert(!parsed.isInline)\n    assert(parsed.getFileStatus == fileStatus)\n  }\n\n  test(\"ParsedV2CheckpointData: equality\") {\n    val parsed1 = ParsedLogData.forFileStatus(v2CheckpointFileStatus(20, useUUID = false))\n    val parsed2 = ParsedLogData.forFileStatus(v2CheckpointFileStatus(20, useUUID = false))\n    val parsed3 = ParsedLogData.forFileStatus(v2CheckpointFileStatus(21))\n\n    assert(parsed1 == parsed1)\n    assert(parsed1 == parsed2)\n    assert(parsed1 != parsed3)\n  }\n\n  ///////////////////////////////////\n  // ParsedMultiPartCheckpointData //\n  ///////////////////////////////////\n\n  test(\"ParsedMultiPartCheckpointData: throws on non-multi-part checkpoint file\") {\n    val fileStatus = deltaFileStatus(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedMultiPartCheckpointData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a multi-part checkpoint file but got\"))\n  }\n\n  test(\"ParsedMultiPartCheckpointData: correctly parses multi-part checkpoint file\") {\n    val chkpt_15_1_3 = multiPartCheckpointFileStatus(15, 1, 3)\n    val parsed = ParsedLogData.forFileStatus(chkpt_15_1_3)\n\n    assert(parsed.isInstanceOf[ParsedMultiPartCheckpointData])\n    assert(parsed.getVersion == 15)\n    assert(parsed.getGroupByCategoryClass == classOf[ParsedCheckpointData])\n    assert(parsed.isFile)\n    assert(!parsed.isInline)\n    assert(parsed.getFileStatus == chkpt_15_1_3)\n\n    val casted = parsed.asInstanceOf[ParsedMultiPartCheckpointData]\n    assert(casted.part == 1)\n    assert(casted.numParts == 3)\n  }\n\n  test(\"ParsedMultiPartCheckpointData: throws on part > numParts\") {\n    val path = FileNames.multiPartCheckpointFile(logPath, 10, 5, 3) // part = 5, numParts = 3\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedLogData.forFileStatus(FileStatus.of(path.toString))\n    }.getMessage\n    assert(exMsg === \"part must be between 1 and numParts\")\n  }\n\n  test(\"ParsedMultiPartCheckpointData: throws on numParts = 0\") {\n    val path = FileNames.multiPartCheckpointFile(logPath, 10, 0, 0) // part = 0, numParts = 0\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedLogData.forFileStatus(FileStatus.of(path.toString))\n    }.getMessage\n    assert(exMsg === \"numParts must be greater than 0\")\n  }\n\n  test(\"ParsedMultiPartCheckpointData: throws on part = 0\") {\n    val path = FileNames.multiPartCheckpointFile(logPath, 10, 0, 3) // part = 0, numParts = 3\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedLogData.forFileStatus(FileStatus.of(path.toString))\n    }.getMessage\n    assert(exMsg === \"part must be between 1 and numParts\")\n  }\n\n  test(\"ParsedMultiPartCheckpointData: equality\") {\n    val parsed_15_1_3_a = ParsedLogData.forFileStatus(multiPartCheckpointFileStatus(15, 1, 3))\n    val parsed_15_1_3_b = ParsedLogData.forFileStatus(multiPartCheckpointFileStatus(15, 1, 3))\n    val parsed_15_2_3 = ParsedLogData.forFileStatus(multiPartCheckpointFileStatus(15, 2, 3))\n    val parsed_15_1_4 = ParsedLogData.forFileStatus(multiPartCheckpointFileStatus(15, 1, 4))\n    val parsed_16_1_3 = ParsedLogData.forFileStatus(multiPartCheckpointFileStatus(16, 1, 3))\n\n    assert(parsed_15_1_3_a == parsed_15_1_3_a)\n    assert(parsed_15_1_3_a == parsed_15_1_3_b)\n    assert(parsed_15_1_3_a != parsed_15_2_3)\n    assert(parsed_15_1_3_a != parsed_15_1_4)\n    assert(parsed_15_1_3_a != parsed_16_1_3)\n  }\n\n  /////////////////////////\n  // Checkpoint ordering //\n  /////////////////////////\n\n  test(\"checkpoint ordering\") {\n    // _m means materialized, _i means inline\n\n    val classic_12_m: ParsedCheckpointData =\n      ParsedClassicCheckpointData.forFileStatus(classicCheckpointFileStatus(12))\n    val multi_11_3_m: ParsedCheckpointData =\n      ParsedMultiPartCheckpointData.forFileStatus(multiPartCheckpointFileStatus(11, 1, 3))\n    val v2_10_m: ParsedCheckpointData =\n      ParsedV2CheckpointData.forFileStatus(v2CheckpointFileStatus(10))\n    val multi_12_3_m: ParsedCheckpointData =\n      ParsedMultiPartCheckpointData.forFileStatus(multiPartCheckpointFileStatus(12, 1, 3))\n    val v2_12_m: ParsedCheckpointData =\n      ParsedV2CheckpointData.forFileStatus(v2CheckpointFileStatus(12))\n    val multi_12_4_m: ParsedCheckpointData =\n      ParsedMultiPartCheckpointData.forFileStatus(multiPartCheckpointFileStatus(12, 1, 4))\n    val v2_aaa: ParsedCheckpointData = ParsedV2CheckpointData.forFileStatus(\n      FileStatus.of(FileNames.topLevelV2CheckpointFile(logPath, 10, \"aaa\", \"json\").toString))\n    val v2_bbb: ParsedCheckpointData = ParsedV2CheckpointData.forFileStatus(\n      FileStatus.of(FileNames.topLevelV2CheckpointFile(logPath, 10, \"bbb\", \"json\").toString))\n\n    // Case 1: Version priority\n    classic_12_m should be > multi_11_3_m\n    classic_12_m should be > v2_10_m\n    multi_11_3_m should be > v2_10_m\n\n    // Case 2: Type priority, when version is tied\n    v2_12_m should be > classic_12_m\n    v2_12_m should be > multi_12_3_m\n\n    // Case 3: Inline priority, when version and type are tied (and parts are tied for multi)\n    // TODO: Test this when we allow creating checkpoints with inline data\n\n    // Case 4: Multi-part checkpoint with more parts has higher priority\n    multi_12_4_m should be > multi_12_3_m\n\n    // Case 5: For tied v2, filepath is tie-breaker\n    v2_bbb should be > v2_aaa\n  }\n\n  ////////////////////////////\n  // ParsedLogCompactionData //\n  ////////////////////////////\n\n  test(\"ParsedLogCompactionData: throws on non-log-compaction file\") {\n    val fileStatus = deltaFileStatus(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedLogCompactionData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a log compaction file but got\"))\n  }\n\n  test(\"ParsedLogCompactionData: correctly parses log compaction file\") {\n    val fileStatus = logCompactionStatus(25, 30)\n    val parsed = ParsedLogData.forFileStatus(fileStatus)\n\n    assert(parsed.isInstanceOf[ParsedLogCompactionData])\n    assert(parsed.getVersion == 30)\n    assert(parsed.getGroupByCategoryClass == classOf[ParsedLogCompactionData])\n    assert(parsed.isFile)\n    assert(!parsed.isInline)\n    assert(parsed.getFileStatus == fileStatus)\n\n    val casted = parsed.asInstanceOf[ParsedLogCompactionData]\n    assert(casted.startVersion == 25)\n    assert(casted.endVersion == 30)\n  }\n\n  test(\"ParsedLogCompactionData: throws on startVersion > endVersion\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      val invalidFilePath = \"00000000000000000003.00000000000000000001.compacted.json\"\n      ParsedLogCompactionData.forFileStatus(FileStatus.of(invalidFilePath))\n    }.getMessage\n    assert(exMsg === \"startVersion must be less than endVersion\")\n  }\n\n  test(\"ParsedLogCompactionData: equality\") {\n    val fileStatus1 = logCompactionStatus(25, 30)\n    val fileStatus2 = logCompactionStatus(25, 30)\n    val fileStatus3 = logCompactionStatus(31, 32)\n\n    val parsed1 = ParsedLogData.forFileStatus(fileStatus1)\n    val parsed2 = ParsedLogData.forFileStatus(fileStatus2)\n    val parsed3 = ParsedLogData.forFileStatus(fileStatus3)\n\n    assert(parsed1 == parsed1)\n    assert(parsed1 == parsed2)\n    assert(parsed1 != parsed3)\n  }\n\n  ////////////////////////\n  // ParsedChecksumData //\n  ////////////////////////\n\n  test(\"ParsedChecksumData: throws on non-checksum file\") {\n    val fileStatus = deltaFileStatus(5)\n    val exMsg = intercept[IllegalArgumentException] {\n      ParsedChecksumData.forFileStatus(fileStatus)\n    }.getMessage\n    assert(exMsg.contains(\"Expected a checksum file but got\"))\n  }\n\n  test(\"ParsedChecksumData: correctly parses checksum file\") {\n    val fileStatus = checksumFileStatus(5)\n    val parsed = ParsedLogData.forFileStatus(fileStatus)\n\n    assert(parsed.isInstanceOf[ParsedChecksumData])\n    assert(parsed.getVersion == 5)\n    assert(parsed.getGroupByCategoryClass == classOf[ParsedChecksumData])\n    assert(parsed.getFileStatus == fileStatus)\n  }\n\n  test(\"ParsedChecksumData: equality\") {\n    val fileStatus1 = checksumFileStatus(5)\n    val fileStatus2 = checksumFileStatus(5)\n    val fileStatus3 = checksumFileStatus(6)\n\n    val parsed1 = ParsedLogData.forFileStatus(fileStatus1)\n    val parsed2 = ParsedLogData.forFileStatus(fileStatus2)\n    val parsed3 = ParsedLogData.forFileStatus(fileStatus3)\n\n    assert(parsed1 == parsed1)\n    assert(parsed1 == parsed2)\n    assert(parsed1 != parsed3)\n  }\n\n  //////////////\n  // toString //\n  //////////////\n\n  // scalastyle:off line.size.limit\n\n  test(\"ParsedPublishedDeltaData: toString\") {\n    val parsed = ParsedPublishedDeltaData.forFileStatus(deltaFileStatus(5))\n    val expected =\n      \"ParsedPublishedDeltaData{version=5, source=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000005.json', size=5, modificationTime=50}}\"\n    assert(parsed.toString === expected)\n  }\n\n  test(\"ParsedCatalogCommitData: toString\") {\n    val parsed = ParsedCatalogCommitData.forFileStatus(stagedCommitFile(5))\n    val expectedPattern =\n      \"\"\"ParsedCatalogCommitData\\{version=5, source=FileStatus\\{path='/fake/path/to/table/_delta_log/_staged_commits/00000000000000000005\\.[^']+\\.json', size=5, modificationTime=50\\}\\}\"\"\".r\n    assert(expectedPattern.findFirstIn(parsed.toString).isDefined)\n  }\n\n  test(\"ParsedLogCompactionData: toString\") {\n    val fileStatus = logCompactionStatus(10, 20)\n    val parsed = ParsedLogCompactionData.forFileStatus(fileStatus)\n    val expected =\n      \"ParsedLogCompactionData{version=20, source=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000010.00000000000000000020.compacted.json', size=10, modificationTime=100}, startVersion=10}\"\n    assert(parsed.toString === expected)\n  }\n\n  test(\"ParsedChecksumData: toString\") {\n    val parsed = ParsedLogData.forFileStatus(checksumFileStatus(5))\n    val expected =\n      \"ParsedChecksumData{version=5, source=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000005.crc', size=10, modificationTime=10}}\"\n    assert(parsed.toString === expected)\n  }\n\n  test(\"ParsedClassicCheckpointData: toString\") {\n    val parsed = ParsedLogData.forFileStatus(classicCheckpointFileStatus(10))\n    val expected =\n      \"ParsedClassicCheckpointData{version=10, source=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000010.checkpoint.parquet', size=10, modificationTime=100}}\"\n    assert(parsed.toString === expected)\n  }\n\n  test(\"ParsedMultiPartCheckpointData: toString\") {\n    val fileStatus = multiPartCheckpointFileStatus(10, 1, 3)\n    val parsed = ParsedMultiPartCheckpointData.forFileStatus(fileStatus)\n    val expected =\n      \"ParsedMultiPartCheckpointData{version=10, source=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000010.checkpoint.0000000001.0000000003.parquet', size=10, modificationTime=100}, part=1, numParts=3}\"\n    assert(parsed.toString === expected)\n  }\n\n  test(\"ParsedV2CheckpointData: toString\") {\n    val parsed = ParsedLogData.forFileStatus(v2CheckpointFileStatus(20))\n    val expectedPattern =\n      \"\"\"ParsedV2CheckpointData\\{version=20, source=FileStatus\\{path='/fake/path/to/table/_delta_log/00000000000000000020\\.checkpoint\\.[a-f0-9-]+\\.json', size=20, modificationTime=200\\}\\}\"\"\".r\n    assert(expectedPattern.findFirstIn(parsed.toString).isDefined)\n  }\n\n  // scalastyle:on line.size.limit\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/fs/PathSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.fs\n\nimport java.net.URI\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PathSuite extends AnyFunSuite {\n\n  test(\"Path construction from String\") {\n    // Basic path construction\n    val path1 = new Path(\"/user/data\")\n    assert(path1.toString === \"/user/data\")\n\n    // Path with scheme\n    val path2 = new Path(\"file:/user/data\")\n    assert(path2.toString === \"file:/user/data\")\n\n    // Path with authority\n    val path3 = new Path(\"hdfs://localhost:9000/user/data\")\n    assert(path3.toString === \"hdfs://localhost:9000/user/data\")\n\n    // Relative path\n    val path4 = new Path(\"relative/path\")\n    assert(path4.toString === \"relative/path\")\n\n    // Empty path should throw exception\n    val exception1 = intercept[IllegalArgumentException] {\n      new Path(\"\")\n    }\n    assert(exception1.getMessage.contains(\"empty string\"))\n\n    // Null path should throw exception\n    val exception2 = intercept[IllegalArgumentException] {\n      new Path(null: String)\n    }\n    assert(exception2.getMessage.contains(\"null string\"))\n  }\n\n  test(\"Path construction from parent and child\") {\n    // String parent, String child\n    val path1 = new Path(\"/user\", \"data\")\n    assert(path1.toString === \"/user/data\")\n\n    // Path parent, String child\n    val path2 = new Path(new Path(\"/user\"), \"data\")\n    assert(path2.toString === \"/user/data\")\n\n    // String parent, Path child\n    val path3 = new Path(\"/user\", new Path(\"data\"))\n    assert(path3.toString === \"/user/data\")\n\n    // Path parent, Path child\n    val path4 = new Path(new Path(\"/user\"), new Path(\"data\"))\n    assert(path4.toString === \"/user/data\")\n\n    // Parent with trailing slash\n    val path5 = new Path(\"/user/\", \"data\")\n    assert(path5.toString === \"/user/data\")\n\n    // Parent is root\n    val path6 = new Path(\"/\", \"data\")\n    assert(path6.toString === \"/data\")\n\n    // Parent with scheme\n    val path7 = new Path(\"file:/user\", \"data\")\n    assert(path7.toString === \"file:/user/data\")\n  }\n\n  test(\"Path construction from URI\") {\n    val uri1 = new URI(\"file:/user/data\")\n    val path1 = new Path(uri1)\n    assert(path1.toString === \"file:/user/data\")\n\n    val uri2 = new URI(\"hdfs\", \"localhost:9000\", \"/user/data\", null, null)\n    val path2 = new Path(uri2)\n    assert(path2.toString === \"hdfs://localhost:9000/user/data\")\n  }\n\n  test(\"Path construction from scheme, authority, path\") {\n    val path1 = new Path(\"file\", null, \"/user/data\")\n    assert(path1.toString === \"file:/user/data\")\n\n    val path2 = new Path(\"hdfs\", \"localhost:9000\", \"/user/data\")\n    assert(path2.toString === \"hdfs://localhost:9000/user/data\")\n\n    val path3 = new Path(null, null, \"/user/data\")\n    assert(path3.toString === \"/user/data\")\n\n    // Skip the test for relative path with scheme as it's not supported\n  }\n\n  test(\"Path normalization\") {\n    // Remove duplicate slashes\n    val path1 = new Path(\"/user//data///file\")\n    assert(path1.toString === \"/user/data/file\")\n\n    // Remove trailing slash\n    val path2 = new Path(\"/user/data/\")\n    assert(path2.toString === \"/user/data\")\n    val path3 = new Path(\"/user/data//\")\n    assert(path3.toString === \"/user/data\")\n\n    // Don't remove trailing slash from root\n    val path4 = new Path(\"/\")\n    assert(path4.toString === \"/\")\n  }\n\n  test(\"Path.getName\") {\n    val path1 = new Path(\"/user/data/file.txt\")\n    assert(path1.getName === \"file.txt\")\n\n    val path2 = new Path(\"/user/data/\")\n    assert(path2.getName === \"data\")\n\n    val path3 = new Path(\"/\")\n    assert(path3.getName === \"\")\n\n    val path4 = new Path(\"file.txt\")\n    assert(path4.getName === \"file.txt\")\n  }\n\n  test(\"Path.getParent\") {\n    val path1 = new Path(\"/user/data/file.txt\")\n    assert(path1.getParent.toString === \"/user/data\")\n\n    val path2 = new Path(\"/user/data\")\n    assert(path2.getParent.toString === \"/user\")\n\n    val path3 = new Path(\"/user\")\n    assert(path3.getParent.toString === \"/\")\n\n    val path4 = new Path(\"/\")\n    assert(path4.getParent === null)\n\n    val path5 = new Path(\"file.txt\")\n    assert(path5.getParent.toString === \"\")\n\n    val path6 = new Path(\"dir/file.txt\")\n    assert(path6.getParent.toString === \"dir\")\n  }\n\n  test(\"Path.isAbsolute and Path.isUriPathAbsolute\") {\n    val path1 = new Path(\"/user/data\")\n    assert(path1.isAbsolute === true)\n    assert(path1.isUriPathAbsolute === true)\n\n    val path2 = new Path(\"user/data\")\n    assert(path2.isAbsolute === false)\n    assert(path2.isUriPathAbsolute === false)\n\n    // Skip the tests with scheme and relative paths as they cause exceptions\n  }\n\n  test(\"Path.isRoot\") {\n    val path1 = new Path(\"/\")\n    assert(path1.isRoot === true)\n\n    val path2 = new Path(\"/user\")\n    assert(path2.isRoot === false)\n\n    val path3 = new Path(\"file:/\")\n    assert(path3.isRoot === true)\n\n    val path4 = new Path(\"file:/user\")\n    assert(path4.isRoot === false)\n  }\n\n  test(\"Path.toUri\") {\n    val path1 = new Path(\"/user/data\")\n    val uri1 = path1.toUri\n    assert(uri1.getScheme === null)\n    assert(uri1.getAuthority === null)\n    assert(uri1.getPath === \"/user/data\")\n\n    val path2 = new Path(\"file:/user/data\")\n    val uri2 = path2.toUri\n    assert(uri2.getScheme === \"file\")\n    assert(uri2.getAuthority === null)\n    assert(uri2.getPath === \"/user/data\")\n\n    val path3 = new Path(\"hdfs://localhost:9000/user/data\")\n    val uri3 = path3.toUri\n    assert(uri3.getScheme === \"hdfs\")\n    assert(uri3.getAuthority === \"localhost:9000\")\n    assert(uri3.getPath === \"/user/data\")\n  }\n\n  test(\"Path equality and comparison\") {\n    val path1 = new Path(\"/user/data\")\n    val path2 = new Path(\"/user/data\")\n    val path3 = new Path(\"/user/other\")\n\n    // Test equals\n    assert(path1 === path2)\n    assert(path1 !== path3)\n\n    // Test hashCode\n    assert(path1.hashCode === path2.hashCode)\n\n    // Test compareTo\n    assert(path1.compareTo(path2) === 0)\n    assert(path1.compareTo(path3) < 0) // \"data\" comes before \"other\" alphabetically\n    assert(path3.compareTo(path1) > 0)\n  }\n\n  test(\"Path.getName static method\") {\n    assert(Path.getName(\"/user/data/file.txt\") === \"file.txt\")\n    assert(Path.getName(\"file.txt\") === \"file.txt\")\n    assert(Path.getName(\"/\") === \"\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/fs/benchmarks/PathNormalizationBenchmarks.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.fs.benchmarks;\n\nimport io.delta.kernel.internal.fs.Path;\nimport org.openjdk.jmh.annotations.*;\nimport org.openjdk.jmh.infra.Blackhole;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Benchmark to measure the performance of initializing/normalizing path objects.\n *\n * <ul>\n *   <li>\n *       <pre>{@code\n * build/sbt sbt:delta> project kernel\n * sbt:delta> set fork in run := true sbt:delta>\n * sbt:delta> test:runMain \\\n *   io.delta.kernel.internal.fs.benchmarks.PathNormalizationBenchmarks.\n *\n * }</pre>\n * </ul>\n *\n * }</pre>\n */\n@State(Scope.Benchmark)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@Warmup(iterations = 3)\n@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)\n@Fork(1)\npublic class PathNormalizationBenchmarks {\n    @Benchmark\n    @BenchmarkMode(Mode.AverageTime)\n    public void benchmarkNoNormalizationNeeded(Blackhole blackhole) throws Exception {\n        blackhole.consume(new Path(\"s3://bucket-name/table-path/metadata/data/some_file.parquet\"));\n    }\n\n    @Benchmark\n    @BenchmarkMode(Mode.AverageTime)\n    public void benchmarkNormalizationNeeded(Blackhole blackhole) throws Exception {\n        blackhole.consume(new Path(\"s3://bucket-name/table-path/metadata/data//some_file.parquet\"));\n    }\n\n    public static void main(String[] args) throws Exception {\n        org.openjdk.jmh.Main.main(args);\n    }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/icebergcompat/IcebergCompatMetadataValidatorAndUpdaterSuiteBase.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeature\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{COLUMN_MAPPING_RW_FEATURE, DELETION_VECTORS_RW_FEATURE, ICEBERG_COMPAT_V2_W_FEATURE, TYPE_WIDENING_RW_FEATURE, TYPE_WIDENING_RW_PREVIEW_FEATURE}\nimport io.delta.kernel.internal.util.ColumnMappingSuiteBase\nimport io.delta.kernel.test.{TestFixtures, VectorTestUtils}\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Base trait for testing Iceberg compatibility metadata validation and updates.\n * This trait provides common functionality and test cases\n * that can be used by both writer and compat test suites.\n */\ntrait IcebergCompatMetadataValidatorAndUpdaterSuiteBase\n    extends AnyFunSuite\n    with VectorTestUtils\n    with ColumnMappingSuiteBase\n    with TestFixtures {\n\n  /** The version of Iceberg compatibility being tested (e.g., \"V2\" or \"V3\") */\n  def icebergCompatVersion: String\n\n  /** When testing supported simple column types skip any types defined here */\n  def simpleTypesToSkip: Set[DataType]\n\n  /** Get a metadata with the given schema and partCols with the desired icebergCompat enabled */\n  def getCompatEnabledMetadata(\n      schema: StructType,\n      columnMappingMode: String = \"name\",\n      partCols: Seq[String] = Seq.empty): Metadata\n\n  /** Get a protocol with features needed for the desired icebergCompat plus the `tableFeatures` */\n  def getCompatEnabledProtocol(tableFeatures: TableFeature*): Protocol\n\n  /** Run the desired validate and update metadata method that triggers icebergCompat checks */\n  def validateAndUpdateIcebergCompatMetadata(\n      isNewTable: Boolean,\n      metadata: Metadata,\n      protocol: Protocol): Optional[Metadata]\n\n  /** Returns a [[Metadata]] instance with IcebergCompat feature and column mapping mode enabled */\n  def withIcebergCompatAndCMEnabled(\n      schema: StructType,\n      columnMappingMode: String,\n      partCols: Seq[String]): Metadata\n\n  /** Get the set of supported data column types */\n  def supportedDataColumnTypes: Set[DataType]\n\n  /** Get the set of unsupported data column types */\n  def unsupportedDataColumnTypes: Set[DataType]\n\n  /** Get the set of unsupported partition column types */\n  def unsupportedPartitionColumnTypes: Set[DataType]\n\n  /** Whether deletion vectors are supported */\n  def isDeletionVectorsSupported: Boolean\n\n  // Common test cases that apply to both writer and compat versions\n  supportedDataColumnTypes.diff(simpleTypesToSkip).foreach {\n    dataType: DataType =>\n      Seq(true, false).foreach { isNewTable =>\n        test(s\"allowed data column types: $dataType, new table = $isNewTable\") {\n          val schema = new StructType().add(\"col\", dataType)\n          val metadata = getCompatEnabledMetadata(schema)\n          val protocol = getCompatEnabledProtocol()\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        }\n      }\n  }\n\n  PRIMITIVE_TYPES.diff(simpleTypesToSkip).foreach {\n    dataType: DataType =>\n      Seq(true, false).foreach { isNewTable =>\n        test(s\"allowed partition column types: $dataType, new table = $isNewTable\") {\n          val schema = new StructType().add(\"col\", dataType)\n          val metadata = getCompatEnabledMetadata(schema, partCols = Seq(\"col\"))\n          val protocol = getCompatEnabledProtocol()\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        }\n      }\n  }\n\n  unsupportedDataColumnTypes.foreach {\n    dataType: DataType =>\n      Seq(true, false).foreach { isNewTable =>\n        test(s\"disallowed data column types: $dataType, new table = $isNewTable\") {\n          val schema = new StructType().add(\"col\", dataType)\n          val metadata = getCompatEnabledMetadata(schema)\n          val protocol = getCompatEnabledProtocol()\n          val e = intercept[KernelException] {\n            validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n          }\n          assert(e.getMessage.contains(\n            s\"icebergCompat$icebergCompatVersion does not support the data types: \"))\n        }\n      }\n  }\n\n  unsupportedPartitionColumnTypes.foreach {\n    dataType: DataType =>\n      Seq(true, false).foreach { isNewTable =>\n        test(s\"disallowed partition column types: $dataType, new table = $isNewTable\") {\n          val schema = new StructType().add(\"col\", dataType)\n          val metadata = getCompatEnabledMetadata(schema, partCols = Seq(\"col\"))\n          val protocol = getCompatEnabledProtocol()\n          val e = intercept[KernelException] {\n            validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n          }\n          assert(e.getMessage.matches(\n            s\"icebergCompat$icebergCompatVersion does not support\" +\n              s\" the data type .* for a partition column.\"))\n        }\n      }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"deletion vectors support behavior, isNewTable $isNewTable\") {\n      val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n      val metadata = getCompatEnabledMetadata(schema)\n      val protocol = getCompatEnabledProtocol(DELETION_VECTORS_RW_FEATURE)\n\n      if (isDeletionVectorsSupported) {\n        // Should not throw an exception\n        validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n      } else {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        }\n        assert(e.getMessage.contains(\n          s\"Table features [deletionVectors] are incompatible \" +\n            s\"with icebergCompat$icebergCompatVersion\"))\n      }\n    }\n  }\n\n  // Compat-specific test cases\n  test(\"compatible type widening is allowed\") {\n    val schema = new StructType()\n      .add(\n        new StructField(\n          \"intToLong\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.empty()).withTypeChanges(\n          Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava))\n      .add(\n        new StructField(\n          \"decimalToDecimal\",\n          new DecimalType(10, 2),\n          true,\n          FieldMetadata.empty()).withTypeChanges(\n          Seq(new TypeChange(new DecimalType(5, 2), new DecimalType(10, 2))).asJava))\n\n    val metadata = getCompatEnabledMetadata(schema)\n    val protocol = getCompatEnabledProtocol(TYPE_WIDENING_RW_FEATURE)\n\n    // This should not throw an exception\n    validateAndUpdateIcebergCompatMetadata(false, metadata, protocol)\n  }\n\n  test(\"incompatible type widening throws exception\") {\n    val schema = new StructType()\n      .add(\n        new StructField(\n          \"dateToTimestamp\",\n          TimestampNTZType.TIMESTAMP_NTZ,\n          true,\n          FieldMetadata.empty()).withTypeChanges(\n          Seq(new TypeChange(DateType.DATE, TimestampNTZType.TIMESTAMP_NTZ)).asJava))\n\n    val metadata = getCompatEnabledMetadata(schema)\n    val protocol = getCompatEnabledProtocol(TYPE_WIDENING_RW_FEATURE)\n\n    val e = intercept[KernelException] {\n      validateAndUpdateIcebergCompatMetadata(false, metadata, protocol)\n    }\n\n    assert(e.getMessage.contains(\n      s\"icebergCompat$icebergCompatVersion does not support type widening present in table\"))\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(\n      s\"can't enable icebergCompat$icebergCompatVersion on a table with icebergCompatV1 enabled, \" +\n        s\"isNewTable = $isNewTable\") {\n      val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n      val metadata = getCompatEnabledMetadata(schema)\n        .withMergedConfiguration(\n          Map(\"delta.enableIcebergCompatV1\" -> \"true\").asJava)\n      val protocol = getCompatEnabledProtocol()\n\n      val ex = intercept[KernelException] {\n        validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n      }\n      assert(ex.getMessage.contains(\n        s\"icebergCompat$icebergCompatVersion: Only one IcebergCompat version can be enabled. \" +\n          \"Incompatible version enabled: delta.enableIcebergCompatV1\"))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/icebergcompat/IcebergCompatV2MetadataValidatorAndUpdaterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatV2MetadataValidatorAndUpdater.validateAndUpdateIcebergCompatV2Metadata\nimport io.delta.kernel.internal.tablefeatures.TableFeature\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{COLUMN_MAPPING_RW_FEATURE, DELETION_VECTORS_RW_FEATURE, ICEBERG_COMPAT_V2_W_FEATURE}\nimport io.delta.kernel.test.TestFixtures\nimport io.delta.kernel.types._\n\ntrait IcebergCompatV2MetadataValidatorAndUpdaterSuiteBase\n    extends IcebergCompatMetadataValidatorAndUpdaterSuiteBase\n    with TestFixtures {\n\n  override def icebergCompatVersion: String = \"V2\"\n\n  override def supportedDataColumnTypes: Set[DataType] = ALL_TYPES\n\n  override def unsupportedDataColumnTypes: Set[DataType] = Set(VariantType.VARIANT)\n\n  override def unsupportedPartitionColumnTypes: Set[DataType] = NESTED_TYPES\n\n  override def isDeletionVectorsSupported: Boolean = false\n\n  override def withIcebergCompatAndCMEnabled(\n      schema: StructType,\n      columnMappingMode: String = \"name\",\n      partCols: Seq[String] = Seq.empty): Metadata = {\n    testMetadata(schema, partCols).withIcebergCompatV2AndCMEnabled(columnMappingMode)\n  }\n}\n\nclass IcebergCompatV2MetadataValidatorAndUpdaterSuite\n    extends IcebergCompatV2MetadataValidatorAndUpdaterSuiteBase {\n\n  override def simpleTypesToSkip: Set[DataType] = Set.empty\n\n  override def getCompatEnabledMetadata(\n      schema: StructType,\n      columnMappingMode: String = \"name\",\n      partCols: Seq[String] = Seq.empty): Metadata = {\n    testMetadata(schema, partCols)\n      .withIcebergCompatV2AndCMEnabled(columnMappingMode)\n  }\n\n  override def getCompatEnabledProtocol(tableFeatures: TableFeature*): Protocol = {\n    testProtocol(tableFeatures ++ Seq(ICEBERG_COMPAT_V2_W_FEATURE, COLUMN_MAPPING_RW_FEATURE): _*)\n  }\n\n  override def validateAndUpdateIcebergCompatMetadata(\n      isNewTable: Boolean,\n      metadata: Metadata,\n      protocol: Protocol): Optional[Metadata] = {\n    validateAndUpdateIcebergCompatV2Metadata(isNewTable, metadata, protocol, Optional.empty())\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"protocol is missing required column mapping feature, isNewTable $isNewTable\") {\n      val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n\n      val metadata = withIcebergCompatAndCMEnabled(schema, partCols = Seq.empty)\n      val protocol =\n        new Protocol(3, 7, Set.empty.asJava, Set(s\"icebergCompat$icebergCompatVersion\").asJava)\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n      }\n      assert(e.getMessage.contains(\n        s\"icebergCompat$icebergCompatVersion: requires the feature 'columnMapping' to be enabled.\"))\n    }\n  }\n\n  Seq(\"id\", \"name\").foreach { existingCMMode =>\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"existing column mapping mode `$existingCMMode` is preserved \" +\n        s\"when icebergCompat is enabled, isNewTable = $isNewTable\") {\n        val metadata = getCompatEnabledMetadata(cmTestSchema(), columnMappingMode = existingCMMode)\n        val protocol = getCompatEnabledProtocol()\n\n        assert(metadata.getConfiguration.get(\"delta.columnMapping.mode\") === existingCMMode)\n\n        val updatedMetadata =\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        // No metadata update is needed since already compatible column mapping mode\n        assert(!updatedMetadata.isPresent)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"column mapping mode `name` is auto enabled when icebergCompat is enabled, \" +\n      s\"isNewTable = $isNewTable\") {\n      val metadata = testMetadata(cmTestSchema())\n        .withMergedConfiguration(\n          Map(s\"delta.enableIcebergCompat$icebergCompatVersion\" -> \"true\").asJava)\n      val protocol = getCompatEnabledProtocol()\n\n      assert(!metadata.getConfiguration.containsKey(\"delta.columnMapping.mode\"))\n\n      if (isNewTable) {\n        val updatedMetadata =\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        assert(updatedMetadata.isPresent)\n        assert(updatedMetadata.get().getConfiguration.get(\"delta.columnMapping.mode\") == \"name\")\n      } else {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        }\n        assert(e.getMessage.contains(\n          \"The value 'none' for the property 'delta.columnMapping.mode' is\" +\n            s\" not compatible with icebergCompat$icebergCompatVersion requirements\"))\n      }\n    }\n  }\n\n  /* --- prevProtocol DV check (concurrent writer protection) --- */\n\n  test(\"DV check catches deletion vectors enabled in previous protocol\") {\n    val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n    val metadata = getCompatEnabledMetadata(schema)\n    // newProtocol does NOT support DVs\n    val newProtocol = getCompatEnabledProtocol()\n    // prevProtocol DOES support DVs (simulating a concurrent writer that enabled DVs)\n    val prevProtocol = Optional.of(getCompatEnabledProtocol(DELETION_VECTORS_RW_FEATURE))\n\n    val e = intercept[KernelException] {\n      validateAndUpdateIcebergCompatV2Metadata(\n        false, /* isCreatingNewTable */\n        metadata,\n        newProtocol,\n        prevProtocol)\n    }\n    assert(e.getMessage.contains(\n      \"Table features [deletionVectors] are incompatible with icebergCompatV2\"))\n  }\n\n  test(\"DV check passes when neither new nor previous protocol has DVs\") {\n    val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n    val metadata = getCompatEnabledMetadata(schema)\n    val newProtocol = getCompatEnabledProtocol()\n    val prevProtocol = Optional.of(getCompatEnabledProtocol())\n\n    // Should not throw\n    validateAndUpdateIcebergCompatV2Metadata(\n      false, /* isCreatingNewTable */\n      metadata,\n      newProtocol,\n      prevProtocol)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/icebergcompat/IcebergCompatV3MetadataValidatorAndUpdateSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.icebergcompat.IcebergCompatV3MetadataValidatorAndUpdater.validateAndUpdateIcebergCompatV3Metadata\nimport io.delta.kernel.internal.tablefeatures.TableFeature\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{ALLOW_COLUMN_DEFAULTS_W_FEATURE, COLUMN_MAPPING_RW_FEATURE, ICEBERG_COMPAT_V3_W_FEATURE, ROW_TRACKING_W_FEATURE, TYPE_WIDENING_RW_FEATURE}\nimport io.delta.kernel.test.TestFixtures\nimport io.delta.kernel.types._\n\nimport org.assertj.core.util.Maps\n\ntrait IcebergCompatV3MetadataValidatorAndUpdaterSuiteBase\n    extends IcebergCompatMetadataValidatorAndUpdaterSuiteBase\n    with TestFixtures {\n\n  override def icebergCompatVersion: String = \"V3\"\n\n  override def supportedDataColumnTypes: Set[DataType] = ALL_TYPES + VariantType.VARIANT\n\n  override def unsupportedDataColumnTypes: Set[DataType] = Set.empty\n\n  override def unsupportedPartitionColumnTypes: Set[DataType] = NESTED_TYPES\n\n  override def isDeletionVectorsSupported: Boolean = true\n\n  override def withIcebergCompatAndCMEnabled(\n      schema: StructType,\n      columnMappingMode: String = \"name\",\n      partCols: Seq[String] = Seq.empty): Metadata = {\n    testMetadata(\n      schema,\n      partCols).withIcebergCompatV3AndCMEnabled(columnMappingMode).withMergedConfiguration(\n      Maps.newHashMap(TableConfig.ROW_TRACKING_ENABLED.getKey, \"true\"))\n  }\n}\n\nclass IcebergCompatV3MetadataValidatorAndUpdaterSuite\n    extends IcebergCompatV3MetadataValidatorAndUpdaterSuiteBase {\n\n  override def simpleTypesToSkip: Set[DataType] = Set.empty\n\n  override def getCompatEnabledMetadata(\n      schema: StructType,\n      columnMappingMode: String = \"name\",\n      partCols: Seq[String] = Seq.empty): Metadata = {\n    testMetadata(schema, partCols)\n      .withIcebergCompatV3AndCMEnabled(columnMappingMode).withMergedConfiguration(\n        Maps.newHashMap(TableConfig.ROW_TRACKING_ENABLED.getKey, \"true\"))\n  }\n\n  override def getCompatEnabledProtocol(tableFeatures: TableFeature*): Protocol = {\n    testProtocol(tableFeatures ++ Seq(\n      ICEBERG_COMPAT_V3_W_FEATURE,\n      COLUMN_MAPPING_RW_FEATURE,\n      ROW_TRACKING_W_FEATURE): _*)\n  }\n\n  override def validateAndUpdateIcebergCompatMetadata(\n      isNewTable: Boolean,\n      metadata: Metadata,\n      protocol: Protocol): Optional[Metadata] = {\n    validateAndUpdateIcebergCompatV3Metadata(isNewTable, metadata, protocol, Optional.empty())\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"protocol is missing required column mapping feature, isNewTable $isNewTable\") {\n      val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n      val metadata = getCompatEnabledMetadata(schema)\n      val protocol =\n        new Protocol(3, 7, Set.empty.asJava, Set(\"icebergCompatV3\", \"rowTracking\").asJava)\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergCompatV3Metadata(isNewTable, metadata, protocol, Optional.empty())\n      }\n      assert(e.getMessage.contains(\n        \"icebergCompatV3: requires the feature 'columnMapping' to be enabled.\"))\n    }\n  }\n\n  Seq(\"id\", \"name\").foreach { existingCMMode =>\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"existing column mapping mode `$existingCMMode` is preserved \" +\n        s\"when icebergCompat is enabled, isNewTable = $isNewTable\") {\n        val metadata = getCompatEnabledMetadata(cmTestSchema(), columnMappingMode = existingCMMode)\n        val protocol = getCompatEnabledProtocol()\n\n        assert(metadata.getConfiguration.get(\"delta.columnMapping.mode\") === existingCMMode)\n\n        val updatedMetadata =\n          validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n        // No metadata update is needed since already compatible column mapping mode\n        assert(!updatedMetadata.isPresent)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"column mapping and row tracking are auto enabled when icebergCompatV3 is enabled, \" +\n      s\"isNewTable = $isNewTable\") {\n      val metadata = testMetadata(cmTestSchema()).withIcebergCompatV3Enabled\n      val protocol =\n        testProtocol(ICEBERG_COMPAT_V3_W_FEATURE, COLUMN_MAPPING_RW_FEATURE, ROW_TRACKING_W_FEATURE)\n\n      assert(!metadata.getConfiguration.containsKey(\"delta.columnMapping.mode\"))\n      assert(!metadata.getConfiguration.containsKey(\"delta.rowTracking.enabled\"))\n\n      if (isNewTable) {\n        val updatedMetadata =\n          validateAndUpdateIcebergCompatV3Metadata(isNewTable, metadata, protocol, Optional.empty())\n        assert(updatedMetadata.isPresent)\n        assert(updatedMetadata.get().getConfiguration.get(\"delta.columnMapping.mode\") == \"name\")\n        assert(TableConfig.ROW_TRACKING_ENABLED.fromMetadata(updatedMetadata.get()))\n      } else {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergCompatV3Metadata(isNewTable, metadata, protocol, Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          \"The value 'none' for the property 'delta.columnMapping.mode' is\" +\n            \" not compatible with icebergCompatV3 requirements\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(\n      s\"can't enable icebergCompatV3 on a table with icebergCompatV2 enabled, \" +\n        s\"isNewTable = $isNewTable\") {\n      val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n      val metadata = getCompatEnabledMetadata(schema)\n        .withMergedConfiguration(\n          Map(\"delta.enableIcebergCompatV2\" -> \"true\").asJava)\n      val protocol = getCompatEnabledProtocol()\n\n      val ex = intercept[KernelException] {\n        validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n      }\n      assert(ex.getMessage.contains(\n        s\"icebergCompat$icebergCompatVersion: Only one IcebergCompat version can be enabled. \" +\n          \"Incompatible version enabled: delta.enableIcebergCompatV2\"))\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(\n      s\"icebergCompatV3 requires column default to be literal, \" +\n        s\"isNewTable = $isNewTable\") {\n      val schema = new StructType().add(\n        \"col\",\n        IntegerType.INTEGER,\n        new FieldMetadata.Builder().putString(\"CURRENT_DEFAULT\", \"CURRENT_TIMESTAMP()\").build())\n      val metadata = getCompatEnabledMetadata(schema)\n      val protocol = getCompatEnabledProtocol(ALLOW_COLUMN_DEFAULTS_W_FEATURE)\n\n      val ex = intercept[KernelException] {\n        validateAndUpdateIcebergCompatMetadata(isNewTable, metadata, protocol)\n      }\n      assert(ex.getMessage.contains(\"icebergCompatV3 requires the default value to be literal \" +\n        \"with correct data types for a column. 'integer: CURRENT_TIMESTAMP()' is invalid.\"))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/icebergcompat/IcebergUniversalFormatMetadataValidatorAndUpdaterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat\n\nimport io.delta.kernel.exceptions.{InvalidConfigurationValueException, KernelException}\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.Metadata\nimport io.delta.kernel.internal.util.ColumnMappingSuiteBase\nimport io.delta.kernel.types.IntegerType\nimport io.delta.kernel.types.StructType\n\nimport org.scalatest.funsuite.AnyFunSuiteLike\n\nclass IcebergUniversalFormatMetadataValidatorAndUpdaterSuite extends AnyFunSuiteLike\n    with ColumnMappingSuiteBase {\n  test(\"validateAndUpdate shouldn't throw when when no config is set\") {\n    val metadata = createMetadata(Map(\"unrelated_key\" -> \"unrelated_value\"))\n    IcebergUniversalFormatMetadataValidatorAndUpdater.validate(\n      metadata)\n  }\n\n  test(\n    \"validate shouldn't throw with valid Hudi enabled.\") {\n    val metadata = createMetadata(Map(\n      TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"hudi\",\n      \"unrelated_key\" -> \"unrelated_value\"))\n    IcebergUniversalFormatMetadataValidatorAndUpdater.validate(metadata)\n  }\n\n  test(\n    \"validate can enable iceberg universal compat is enabled and icebergCompatV2 is enabled\") {\n    val metadata = createMetadata(Map(\n      TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"iceberg,hudi\",\n      TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\",\n      \"unrelated_key\" -> \"unrelated_value\"))\n    IcebergUniversalFormatMetadataValidatorAndUpdater.validate(metadata)\n  }\n\n  Seq(\n    Map(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"false\"),\n    Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"false\"),\n    Map[String, String]()).foreach { disableIcebergCompat =>\n    test(\n      \"validate should throw when iceberg universal format is enabled but  \"\n        + s\"no IcebergCompat version is enabled: $disableIcebergCompat\") {\n      val metadata = createMetadata(Map(\n        TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"iceberg\",\n        \"unrelated_key\" -> \"unrelated_value\") ++ disableIcebergCompat)\n      val exc = intercept[InvalidConfigurationValueException] {\n        IcebergUniversalFormatMetadataValidatorAndUpdater.validate(metadata)\n      }\n      assert(exc.getMessage == \"Invalid value for table property \" +\n        \"'delta.universalFormat.enabledFormats': 'iceberg'. \" +\n        \"One of delta.enableIcebergCompatV2 or delta.enableIcebergCompatV3 \" +\n        \"must be set to \\\"true\\\" to enable iceberg uniform format.\")\n    }\n  }\n\n  test(\"validate should throw when both IcebergCompatV2 and V3 are enabled\") {\n    val metadata = createMetadata(Map(\n      TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"iceberg\",\n      TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\",\n      TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\"))\n    val exc = intercept[InvalidConfigurationValueException] {\n      IcebergUniversalFormatMetadataValidatorAndUpdater.validate(metadata)\n    }\n    assert(exc.getMessage.contains(\n      \"'delta.enableIcebergCompatV2' and 'delta.enableIcebergCompatV3' \" +\n        \"cannot be enabled at the same time.\"))\n  }\n\n  def createMetadata(tblProps: Map[String, String] = Map.empty): Metadata = {\n    val schema = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n    testMetadata(schema, tblProps = tblProps)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/icebergcompat/IcebergWriterCompatV1MetadataValidatorAndUpdaterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.icebergcompat.IcebergWriterCompatV1MetadataValidatorAndUpdater.validateAndUpdateIcebergWriterCompatV1Metadata\nimport io.delta.kernel.internal.tablefeatures.TableFeature\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{COLUMN_MAPPING_RW_FEATURE, DELETION_VECTORS_RW_FEATURE, ICEBERG_COMPAT_V2_W_FEATURE, ICEBERG_WRITER_COMPAT_V1, TYPE_WIDENING_RW_FEATURE}\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.types.{BooleanType, ByteType, DataType, DecimalType, FieldMetadata, IntegerType, LongType, ShortType, StructField, StructType, TypeChange}\n\nclass IcebergWriterCompatV1MetadataValidatorAndUpdaterSuite\n    extends IcebergCompatV2MetadataValidatorAndUpdaterSuiteBase {\n\n  override def validateAndUpdateIcebergCompatMetadata(\n      isNewTable: Boolean,\n      metadata: Metadata,\n      protocol: Protocol): Optional[Metadata] = {\n    validateAndUpdateIcebergWriterCompatV1Metadata(isNewTable, metadata, protocol, Optional.empty())\n  }\n\n  val icebergWriterCompatV1EnabledProps = Map(\n    TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\")\n\n  val icebergCompatV2EnabledProps = Map(\n    TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")\n\n  val columnMappingIdModeProps = Map(\n    TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n\n  /* icebergWriterCompatV1 restricts additional types allowed by icebergCompatV2 */\n  override def simpleTypesToSkip: Set[DataType] = Set(ByteType.BYTE, ShortType.SHORT)\n\n  override def getCompatEnabledMetadata(\n      schema: StructType,\n      columnMappingMode: String = \"id\",\n      partCols: Seq[String] = Seq.empty): Metadata = {\n    testMetadata(schema, partCols)\n      .withMergedConfiguration((\n        icebergWriterCompatV1EnabledProps ++ icebergCompatV2EnabledProps\n          ++ columnMappingIdModeProps).asJava)\n  }\n\n  override def getCompatEnabledProtocol(tableFeatures: TableFeature*): Protocol = {\n    testProtocol(tableFeatures ++\n      Seq(ICEBERG_WRITER_COMPAT_V1, ICEBERG_COMPAT_V2_W_FEATURE, COLUMN_MAPPING_RW_FEATURE): _*)\n  }\n\n  test(\"checks are not enforced when table property is not enabled\") {\n    // Violate check by including BYTE type column\n    val schema = new StructType().add(\"col\", ByteType.BYTE)\n    val metadata = testMetadata(schema)\n    assert(!TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.fromMetadata(metadata))\n    validateAndUpdateIcebergWriterCompatV1Metadata(\n      true, /* isNewTable */\n      metadata,\n      getCompatEnabledProtocol(),\n      Optional.empty())\n  }\n\n  /* --- UNSUPPORTED_TYPES_CHECK tests --- */\n\n  Seq(ByteType.BYTE, ShortType.SHORT).foreach { unsupportedType =>\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"disallowed data types: $unsupportedType, new table = $isNewTable\") {\n        val schema = new StructType().add(\"col\", unsupportedType)\n        val metadata = getCompatEnabledMetadata(schema)\n        val protocol = getCompatEnabledProtocol()\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV1Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          s\"icebergWriterCompatV1 does not support the data types: \"))\n      }\n    }\n  }\n\n  /* --- CM_ID_MODE_ENABLED and PHYSICAL_NAMES_MATCH_FIELD_IDS_CHECK tests --- */\n\n  Seq(true, false).foreach { isNewTable =>\n    Seq(true, false).foreach { icebergCompatV2Enabled =>\n      test(s\"column mapping mode `id` is auto enabled when icebergWriterCompatV1 is enabled, \" +\n        s\"isNewTable = $isNewTable, icebergCompatV2Enabled = $icebergCompatV2Enabled\") {\n\n        val tblProperties = icebergWriterCompatV1EnabledProps ++\n          (if (icebergCompatV2Enabled) {\n             icebergCompatV2EnabledProps\n           } else {\n             Map()\n           })\n        val metadata = testMetadata(cmTestSchema(), tblProps = tblProperties)\n        val protocol = getCompatEnabledProtocol()\n\n        assert(!metadata.getConfiguration.containsKey(\"delta.columnMapping.mode\"))\n\n        if (isNewTable) {\n          val updatedMetadata =\n            validateAndUpdateIcebergWriterCompatV1Metadata(\n              isNewTable,\n              metadata,\n              protocol,\n              Optional.empty())\n          assert(updatedMetadata.isPresent)\n          assert(updatedMetadata.get().getConfiguration.get(\"delta.columnMapping.mode\") == \"id\")\n          // We correctly populate the column mapping metadata\n          verifyCMTestSchemaHasValidColumnMappingInfo(\n            updatedMetadata.get(),\n            isNewTable,\n            enableIcebergCompatV2 = true,\n            enableIcebergWriterCompatV1 = true)\n        } else {\n          val e = intercept[KernelException] {\n            validateAndUpdateIcebergWriterCompatV1Metadata(\n              isNewTable,\n              metadata,\n              protocol,\n              Optional.empty())\n          }\n          assert(e.getMessage.contains(\n            \"The value 'none' for the property 'delta.columnMapping.mode' is\" +\n              \" not compatible with icebergWriterCompatV1 requirements\"))\n        }\n      }\n    }\n  }\n\n  Seq(\"name\", \"none\").foreach { cmMode =>\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"cannot enable icebergWriterCompatV1 with incompatible column mapping mode \" +\n        s\"`$cmMode`, isNewTable = $isNewTable\") {\n        val tblProperties = icebergWriterCompatV1EnabledProps ++\n          Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> cmMode)\n\n        val metadata = testMetadata(cmTestSchema(), tblProps = tblProperties)\n        val protocol = getCompatEnabledProtocol()\n\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV1Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          s\"The value '$cmMode' for the property 'delta.columnMapping.mode' is\" +\n            \" not compatible with icebergWriterCompatV1 requirements\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"cannot set physicalName to anything other than col-{fieldId}, isNewTable=$isNewTable\") {\n      val schema = new StructType()\n        .add(\n          \"c1\",\n          IntegerType.INTEGER,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"c1\")\n            .build())\n\n      val metadata = getCompatEnabledMetadata(schema)\n      val protocol = getCompatEnabledProtocol()\n\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergWriterCompatV1Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      }\n      assert(e.getMessage.contains(\n        \"IcebergWriterCompatV1 requires column mapping field physical names be equal to \"\n          + \"'col-[fieldId]', but this is not true for the following fields \" +\n          \"[c1(physicalName='c1', columnId=1)]\"))\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"can provide correct physicalName=col-{fieldId}, isNewTable=$isNewTable\") {\n      val schema = new StructType()\n        .add(\n          \"c1\",\n          IntegerType.INTEGER,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n            .build())\n\n      val metadata = getCompatEnabledMetadata(schema)\n        .withMergedConfiguration(Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"1\").asJava)\n      val protocol = getCompatEnabledProtocol()\n\n      val updatedMetadata =\n        validateAndUpdateIcebergWriterCompatV1Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      // No metadata update happens\n      assert(!updatedMetadata.isPresent)\n    }\n  }\n\n  /* --- ICEBERG_COMPAT_V2_ENABLED tests --- */\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"icebergCompatV2 is auto enabled when icebergWriterCompatV1 is enabled, \" +\n      s\"isNewTable = $isNewTable\") {\n      val metadata = testMetadata(\n        cmTestSchema(),\n        tblProps = icebergWriterCompatV1EnabledProps ++ columnMappingIdModeProps)\n      val protocol = getCompatEnabledProtocol()\n      assert(!TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(metadata))\n\n      if (isNewTable) {\n        val updatedMetadata =\n          validateAndUpdateIcebergWriterCompatV1Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        assert(updatedMetadata.isPresent)\n        assert(TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(updatedMetadata.get))\n      } else {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV1Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          \"The value 'false' for the property 'delta.enableIcebergCompatV2' is\" +\n            \" not compatible with icebergWriterCompatV1 requirements\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"cannot enable icebergWriterCompatV1 with icebergCompatV2 explicitly disabled, \" +\n      s\"isNewTable = $isNewTable\") {\n      val tblProperties = icebergWriterCompatV1EnabledProps ++ columnMappingIdModeProps ++\n        Map(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"false\")\n\n      val metadata = testMetadata(cmTestSchema(), tblProps = tblProperties)\n      val protocol = getCompatEnabledProtocol()\n\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergWriterCompatV1Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      }\n      assert(e.getMessage.contains(\n        \"The value 'false' for the property 'delta.enableIcebergCompatV2' is\" +\n          \" not compatible with icebergWriterCompatV1 requirements\"))\n    }\n  }\n\n  /* --- UNSUPPORTED_FEATURES_CHECK tests --- */\n\n  test(\"all supported features are allowed\") {\n    val readerFeatures = Set(\"columnMapping\", \"timestampNtz\", \"v2Checkpoint\", \"vacuumProtocolCheck\")\n    // TODO add typeWidening and typeWidening-preview here once it's no longer blocked\n    //  icebergCompatV2\n    val writerFeatures = Set(\n      // Legacy incompatible features (allowed as long as they are inactive)\n      \"invariants\",\n      \"checkConstraints\",\n      \"changeDataFeed\",\n      \"identityColumns\",\n      \"generatedColumns\",\n      // Compatible table features\n      \"appendOnly\",\n      \"columnMapping\",\n      \"icebergCompatV2\",\n      \"icebergWriterCompatV1\",\n      \"domainMetadata\",\n      \"vacuumProtocolCheck\",\n      \"v2Checkpoint\",\n      \"checkpointProtection\",\n      \"inCommitTimestamp\",\n      \"clustering\",\n      \"typeWidening\",\n      \"typeWidening-preview\",\n      \"timestampNtz\",\n      \"catalogManaged\")\n    val protocol = new Protocol(3, 7, readerFeatures.asJava, writerFeatures.asJava)\n    val metadata = getCompatEnabledMetadata(cmTestSchema())\n    validateAndUpdateIcebergWriterCompatV1Metadata(true, metadata, protocol, Optional.empty())\n    validateAndUpdateIcebergWriterCompatV1Metadata(false, metadata, protocol, Optional.empty())\n  }\n\n  test(\"compatible type widening is allowed with icebergWriterCompatV1\") {\n    val schema = new StructType()\n      .add(\n        new StructField(\n          \"intToLong\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n            .build()).withTypeChanges(Seq(new TypeChange(\n          IntegerType.INTEGER,\n          LongType.LONG)).asJava))\n\n    val metadata = getCompatEnabledMetadata(schema)\n      .withMergedConfiguration(Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"1\").asJava)\n    val protocol = getCompatEnabledProtocol(TYPE_WIDENING_RW_FEATURE)\n\n    // This should not throw an exception\n    validateAndUpdateIcebergWriterCompatV1Metadata(false, metadata, protocol, Optional.empty())\n  }\n\n  test(\"incompatible type widening throws exception with icebergWriterCompatV1\") {\n    val schema = new StructType()\n      .add(\n        new StructField(\n          \"intToLong\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n            .build()).withTypeChanges(\n          Seq(new TypeChange(ByteType.BYTE, new DecimalType(10, 0))).asJava))\n\n    val metadata = getCompatEnabledMetadata(schema)\n      .withMergedConfiguration(Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"1\").asJava)\n    val protocol = getCompatEnabledProtocol(TYPE_WIDENING_RW_FEATURE)\n\n    val e = intercept[KernelException] {\n      validateAndUpdateIcebergWriterCompatV1Metadata(false, metadata, protocol, Optional.empty())\n    }\n\n    assert(e.getMessage.contains(\"icebergCompatV2 does not support type widening present in table\"))\n  }\n\n  private def checkUnsupportedOrIncompatibleFeature(\n      tableFeature: String,\n      expectedErrorMessageContains: String): Unit = {\n    val protocol = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\").asJava,\n      Set(\n        \"columnMapping\",\n        \"icebergCompatV2\",\n        \"icebergWriterCompatV1\",\n        tableFeature).asJava)\n    val metadata = getCompatEnabledMetadata(cmTestSchema())\n    Seq(true, false).foreach { isNewTable =>\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergWriterCompatV1Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      }\n      assert(e.getMessage.contains(expectedErrorMessageContains))\n    }\n  }\n\n  Seq(\n    // \"defaultColumns\", add this to this test once we support defaultColumns\n    // \"collations\", add this to this test once we support collations\n    \"variantType\").foreach { incompatibleFeature =>\n    test(s\"cannot enable with incompatible feature $incompatibleFeature\") {\n      checkUnsupportedOrIncompatibleFeature(\n        incompatibleFeature,\n        s\"Table features [$incompatibleFeature] are incompatible with \" +\n          s\"icebergWriterCompatV1\")\n    }\n  }\n\n  Seq(\"collations\", \"defaultColumns\").foreach { unsupportedIncompatibleFeature =>\n    test(s\"cannot enable with incompatible UNSUPPORTED feature $unsupportedIncompatibleFeature\") {\n      // We add this test here so that it will fail when we add Kernel support for these features\n      // When this happens -> add the feature to the test above\n      checkUnsupportedOrIncompatibleFeature(\n        unsupportedIncompatibleFeature,\n        \"Unsupported Delta table feature: table requires feature \" +\n          s\"\"\"\"$unsupportedIncompatibleFeature\" which is unsupported by this version of \"\"\" +\n          \"Delta Kernel\")\n    }\n  }\n\n  private def testIncompatibleActiveLegacyFeature(\n      activeFeatureMetadata: Metadata,\n      tableFeature: String): Unit = {\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"cannot enable with $tableFeature active, isNewTable = $isNewTable\") {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV1Metadata(\n            isNewTable,\n            activeFeatureMetadata,\n            getCompatEnabledProtocol(),\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          s\"Table features [$tableFeature] are incompatible with icebergWriterCompatV1\"))\n      }\n    }\n  }\n\n  /* --- INVARIANTS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.invariants\", \"{\\\"expression\\\": { \\\"expression\\\": \\\"x > 3\\\"} }\")\n          .build())),\n    \"invariants\")\n\n  /* --- CHANGE_DATA_FEED_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(cmTestSchema())\n      .withMergedConfiguration(Map(TableConfig.CHANGE_DATA_FEED_ENABLED.getKey -> \"true\").asJava),\n    \"changeDataFeed\")\n\n  /* --- CHECK_CONSTRAINTS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(cmTestSchema())\n      .withMergedConfiguration(Map(\"delta.constraints.a\" -> \"a = b\").asJava),\n    \"checkConstraints\")\n\n  /* --- ROW_TRACKING_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(cmTestSchema())\n      .withMergedConfiguration(Map(TableConfig.ROW_TRACKING_ENABLED.getKey -> \"true\").asJava),\n    \"rowTracking\")\n\n  /* --- IDENTITY_COLUMNS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putLong(\"delta.identity.start\", 1L)\n          .putLong(\"delta.identity.step\", 2L)\n          .putBoolean(\"delta.identity.allowExplicitInsert\", true)\n          .build())),\n    \"identityColumns\")\n\n  /* --- GENERATED_COLUMNS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.generationExpression\", \"{\\\"expression\\\": \\\"c1 + 1\\\"}\")\n          .build())),\n    \"generatedColumns\")\n\n  /* --- requiredDependencyTableFeatures tests --- */\n  Seq(\n    (\"columnMapping\", \"icebergCompatV2\"),\n    (\"icebergCompatV2\", \"columnMapping\")).foreach {\n    case (featureToIncludeStr, missingFeatureStr) =>\n      Seq(true, false).foreach { isNewTable =>\n        test(s\"protocol is missing required feature $missingFeatureStr, isNewTable = $isNewTable\") {\n          val metadata = getCompatEnabledMetadata(cmTestSchema())\n          val readerFeatures: Set[String] = if (\"columnMapping\" == featureToIncludeStr) {\n            Set(\"columnMapping\")\n          } else Set.empty\n          val writerFeatures = Set(\"icebergWriterCompatV1\", featureToIncludeStr)\n          val protocol = new Protocol(3, 7, readerFeatures.asJava, writerFeatures.asJava)\n          val e = intercept[KernelException] {\n            validateAndUpdateIcebergWriterCompatV1Metadata(\n              isNewTable,\n              metadata,\n              protocol,\n              Optional.empty())\n          }\n          // Since we run icebergCompatV2 validation as part of\n          // ICEBERG_COMPAT_V2_ENABLED.postProcess we actually hit the missing feature error in the\n          // icebergCompatV2 checks first\n          assert(e.getMessage.contains(\n            s\"icebergCompatV2: requires the feature '$missingFeatureStr' to be enabled\"))\n        }\n      }\n  }\n\n  /* --- prevProtocol DV check flows through WriterCompatV1 -> CompatV2 delegation --- */\n\n  test(\"prevProtocol DV check flows through WriterCompatV1 delegation to CompatV2\") {\n    val schema = new StructType().add(\"col\", BooleanType.BOOLEAN)\n    val metadata = getCompatEnabledMetadata(schema)\n    val newProtocol = getCompatEnabledProtocol()\n    // prevProtocol has DVs enabled, simulating a table that had DVs before this transaction\n    val prevProtocol = Optional.of(getCompatEnabledProtocol(DELETION_VECTORS_RW_FEATURE))\n\n    val e = intercept[KernelException] {\n      validateAndUpdateIcebergWriterCompatV1Metadata(\n        false,\n        metadata,\n        newProtocol,\n        prevProtocol)\n    }\n    // The error comes from icebergCompatV2 since WriterCompatV1 delegates DV checking to V2\n    assert(e.getMessage.contains(\n      \"Table features [deletionVectors] are incompatible with icebergCompatV2\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/icebergcompat/IcebergWriterCompatV3MetadataValidatorAndUpdaterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.icebergcompat\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.icebergcompat.IcebergWriterCompatV3MetadataValidatorAndUpdater.validateAndUpdateIcebergWriterCompatV3Metadata\nimport io.delta.kernel.internal.tablefeatures.TableFeature\nimport io.delta.kernel.internal.tablefeatures.TableFeatures._\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.types.{ByteType, DataType, DateType, FieldMetadata, IntegerType, LongType, ShortType, StructField, StructType, TimestampNTZType, TypeChange}\n\nclass IcebergWriterCompatV3MetadataValidatorAndUpdaterSuite\n    extends IcebergCompatV3MetadataValidatorAndUpdaterSuiteBase {\n  override def validateAndUpdateIcebergCompatMetadata(\n      isNewTable: Boolean,\n      metadata: Metadata,\n      protocol: Protocol): Optional[Metadata] = {\n    validateAndUpdateIcebergWriterCompatV3Metadata(isNewTable, metadata, protocol, Optional.empty())\n  }\n\n  val icebergWriterCompatV3EnabledProps = Map(\n    TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.getKey -> \"true\")\n\n  val icebergCompatV3EnabledProps = Map(\n    TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\")\n\n  val deletionVectorsEnabledProps = Map(\n    TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\")\n\n  val variantShreddingEnabledProps = Map(\n    TableConfig.VARIANT_SHREDDING_ENABLED.getKey -> \"true\")\n\n  val columnMappingIdModeProps = Map(\n    TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n\n  val rowTrackingEnabledProps = Map(\n    TableConfig.ROW_TRACKING_ENABLED.getKey -> \"true\")\n\n  override def getCompatEnabledMetadata(\n      schema: StructType,\n      columnMappingMode: String = \"id\",\n      partCols: Seq[String] = Seq.empty): Metadata = {\n    val result = testMetadata(schema, partCols)\n      .withMergedConfiguration((\n        icebergWriterCompatV3EnabledProps ++\n          icebergCompatV3EnabledProps ++\n          columnMappingIdModeProps ++\n          rowTrackingEnabledProps).asJava)\n\n    result\n  }\n\n  override def getCompatEnabledProtocol(tableFeatures: TableFeature*): Protocol = {\n    testProtocol(tableFeatures ++\n      Seq(\n        ICEBERG_WRITER_COMPAT_V3,\n        ICEBERG_COMPAT_V3_W_FEATURE,\n        DELETION_VECTORS_RW_FEATURE,\n        VARIANT_RW_FEATURE,\n        VARIANT_SHREDDING_RW_FEATURE,\n        VARIANT_SHREDDING_PREVIEW_RW_FEATURE,\n        VARIANT_RW_PREVIEW_FEATURE,\n        ROW_TRACKING_W_FEATURE,\n        COLUMN_MAPPING_RW_FEATURE): _*)\n  }\n\n  /* icebergWriterCompatV3 restricts additional types allowed by icebergCompatV3 */\n  override def simpleTypesToSkip: Set[DataType] = Set(ByteType.BYTE, ShortType.SHORT)\n\n  private def checkUnsupportedOrIncompatibleFeature(\n      tableFeature: String,\n      expectedErrorMessageContains: String): Unit = {\n    val protocol = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\", \"rowTracking\").asJava,\n      Set(\n        \"columnMapping\",\n        \"icebergCompatV3\",\n        \"icebergWriterCompatV3\",\n        \"deletionVectors\",\n        \"rowTracking\",\n        \"variantType\",\n        \"variantType-preview\",\n        \"variantShredding\",\n        \"variantShredding-preview\",\n        tableFeature).asJava)\n    val metadata = getCompatEnabledMetadata(cmTestSchema())\n    Seq(true, false).foreach { isNewTable =>\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergWriterCompatV3Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      }\n      assert(e.getMessage.contains(expectedErrorMessageContains))\n    }\n  }\n\n  private def testIncompatibleActiveLegacyFeature(\n      activeFeatureMetadata: Metadata,\n      tableFeature: String): Unit = {\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"cannot enable with $tableFeature active, isNewTable = $isNewTable\") {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV3Metadata(\n            isNewTable,\n            activeFeatureMetadata,\n            getCompatEnabledProtocol(),\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          s\"Table features [$tableFeature] are incompatible with icebergWriterCompatV3\"))\n      }\n    }\n  }\n\n  /* --- CM_ID_MODE_ENABLED and PHYSICAL_NAMES_MATCH_FIELD_IDS_CHECK tests --- */\n\n  Seq(true, false).foreach { isNewTable =>\n    Seq(true, false).foreach { icebergCompatV3Enabled =>\n      test(s\"column mapping mode `id` is auto enabled when icebergWriterCompatV3 is enabled, \" +\n        s\"isNewTable = $isNewTable, icebergCompatV3Enabled = $icebergCompatV3Enabled\") {\n\n        val tblProperties = icebergWriterCompatV3EnabledProps ++\n          (if (icebergCompatV3Enabled) {\n             icebergCompatV3EnabledProps\n           } else {\n             Map()\n           })\n        val metadata = testMetadata(cmTestSchema(), tblProps = tblProperties)\n        val protocol = getCompatEnabledProtocol()\n\n        assert(!metadata.getConfiguration.containsKey(\"delta.columnMapping.mode\"))\n\n        if (isNewTable) {\n          val updatedMetadata =\n            validateAndUpdateIcebergWriterCompatV3Metadata(\n              isNewTable,\n              metadata,\n              protocol,\n              Optional.empty())\n          assert(updatedMetadata.isPresent)\n          assert(updatedMetadata.get().getConfiguration.get(\"delta.columnMapping.mode\") == \"id\")\n          // We correctly populate the column mapping metadata\n          verifyCMTestSchemaHasValidColumnMappingInfo(\n            updatedMetadata.get(),\n            isNewTable,\n            true,\n            true)\n        } else {\n          val e = intercept[KernelException] {\n            validateAndUpdateIcebergWriterCompatV3Metadata(\n              isNewTable,\n              metadata,\n              protocol,\n              Optional.empty())\n          }\n          assert(e.getMessage.contains(\n            \"The value 'none' for the property 'delta.columnMapping.mode' is\" +\n              \" not compatible with icebergWriterCompatV3 requirements\"))\n        }\n      }\n    }\n  }\n\n  test(\"checks are not enforced when table property is not enabled\") {\n    // Violate check by including BYTE type column\n    val schema = new StructType().add(\"col\", ByteType.BYTE)\n    val metadata = testMetadata(schema)\n    assert(!TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.fromMetadata(metadata))\n    validateAndUpdateIcebergWriterCompatV3Metadata(\n      true, /* isNewTable */\n      metadata,\n      getCompatEnabledProtocol(),\n      Optional.empty())\n  }\n\n  Seq(\"name\", \"none\").foreach { cmMode =>\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"cannot enable icebergWriterCompatV3 with incompatible column mapping mode \" +\n        s\"`$cmMode`, isNewTable = $isNewTable\") {\n        val tblProperties = icebergWriterCompatV3EnabledProps ++\n          Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> cmMode)\n\n        val metadata = testMetadata(cmTestSchema(), tblProps = tblProperties)\n        val protocol = getCompatEnabledProtocol()\n\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV3Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          s\"The value '$cmMode' for the property 'delta.columnMapping.mode' is\" +\n            \" not compatible with icebergWriterCompatV3 requirements\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"cannot set physicalName to anything other than col-{fieldId}, isNewTable=$isNewTable\") {\n      val schema = new StructType()\n        .add(\n          \"c1\",\n          IntegerType.INTEGER,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"c1\")\n            .build())\n\n      val metadata = getCompatEnabledMetadata(schema)\n      val protocol = getCompatEnabledProtocol()\n\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergWriterCompatV3Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      }\n      assert(e.getMessage.contains(\n        \"requires column mapping field physical names be equal to \"\n          + \"'col-[fieldId]', but this is not true for the following fields \" +\n          \"[c1(physicalName='c1', columnId=1)]\"))\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"can provide correct physicalName=col-{fieldId}, isNewTable=$isNewTable\") {\n      val schema = new StructType()\n        .add(\n          \"c1\",\n          IntegerType.INTEGER,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n            .build())\n\n      val metadata = getCompatEnabledMetadata(schema)\n        .withMergedConfiguration(Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"1\").asJava)\n      val protocol = getCompatEnabledProtocol()\n\n      val updatedMetadata =\n        validateAndUpdateIcebergWriterCompatV3Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      // No metadata update happens\n      assert(!updatedMetadata.isPresent)\n    }\n  }\n\n  /* --- UNSUPPORTED_TYPES_CHECK tests --- */\n\n  Seq(ByteType.BYTE, ShortType.SHORT).foreach { unsupportedType =>\n    Seq(true, false).foreach { isNewTable =>\n      test(s\"disallowed data types: $unsupportedType, new table = $isNewTable\") {\n        val schema = new StructType().add(\"col\", unsupportedType)\n        val metadata = getCompatEnabledMetadata(schema)\n        val protocol = getCompatEnabledProtocol()\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV3Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          s\"icebergWriterCompatV3 does not support the data types: \"))\n      }\n    }\n  }\n\n  test(\"compatible type widening is allowed with icebergWriterCompatV3\") {\n    val schema = new StructType()\n      .add(\n        new StructField(\n          \"intToLong\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n            .build()).withTypeChanges(Seq(new TypeChange(\n          IntegerType.INTEGER,\n          LongType.LONG)).asJava))\n\n    val metadata = getCompatEnabledMetadata(schema)\n      .withMergedConfiguration(Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"1\").asJava)\n    val protocol = getCompatEnabledProtocol(TYPE_WIDENING_RW_FEATURE)\n\n    validateAndUpdateIcebergCompatMetadata(false, metadata, protocol)\n  }\n\n  test(\"incompatible type widening throws exception with icebergWriterCompatV3\") {\n    val schema = new StructType()\n      .add(\n        new StructField(\n          \"dateToTimestamp\",\n          TimestampNTZType.TIMESTAMP_NTZ,\n          true,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n            .build()).withTypeChanges(\n          Seq(new TypeChange(DateType.DATE, TimestampNTZType.TIMESTAMP_NTZ)).asJava))\n\n    val metadata = getCompatEnabledMetadata(schema)\n      .withMergedConfiguration(Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"1\").asJava)\n    val protocol = getCompatEnabledProtocol(TYPE_WIDENING_RW_FEATURE, ROW_TRACKING_W_FEATURE)\n\n    val e = intercept[KernelException] {\n      validateAndUpdateIcebergCompatMetadata(false, metadata, protocol)\n    }\n    assert(e.getMessage.contains(\"icebergCompatV3 does not support type widening present in table\"))\n  }\n\n  /* --- ICEBERG_COMPAT_V3_ENABLED tests --- */\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"icebergCompatV3 is auto enabled when icebergWriterCompatV3 is enabled, \" +\n      s\"isNewTable = $isNewTable\") {\n      val metadata = testMetadata(\n        cmTestSchema(),\n        tblProps =\n          icebergWriterCompatV3EnabledProps ++ columnMappingIdModeProps ++ rowTrackingEnabledProps)\n      val protocol = getCompatEnabledProtocol()\n      assert(!TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(metadata))\n\n      if (isNewTable) {\n        val updatedMetadata =\n          validateAndUpdateIcebergWriterCompatV3Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        assert(updatedMetadata.isPresent)\n        assert(TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(updatedMetadata.get))\n      } else {\n        val e = intercept[KernelException] {\n          validateAndUpdateIcebergWriterCompatV3Metadata(\n            isNewTable,\n            metadata,\n            protocol,\n            Optional.empty())\n        }\n        assert(e.getMessage.contains(\n          \"The value 'false' for the property 'delta.enableIcebergCompatV3' is\" +\n            \" not compatible with icebergWriterCompatV3 requirements\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"cannot enable icebergWriterCompatV3 with icebergCompatV3 explicitly disabled, \" +\n      s\"isNewTable = $isNewTable\") {\n      val tblProperties = icebergWriterCompatV3EnabledProps ++ columnMappingIdModeProps ++\n        rowTrackingEnabledProps ++\n        Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"false\")\n\n      val metadata = testMetadata(cmTestSchema(), tblProps = tblProperties)\n      val protocol = getCompatEnabledProtocol()\n\n      val e = intercept[KernelException] {\n        validateAndUpdateIcebergWriterCompatV3Metadata(\n          isNewTable,\n          metadata,\n          protocol,\n          Optional.empty())\n      }\n      assert(e.getMessage.contains(\n        \"The value 'false' for the property 'delta.enableIcebergCompatV3' is\" +\n          \" not compatible with icebergWriterCompatV3 requirements\"))\n    }\n  }\n  /* --- UNSUPPORTED_FEATURES_CHECK tests --- */\n\n  test(\"all supported features are allowed\") {\n    val readerFeatures =\n      Set(\"columnMapping\", \"timestampNtz\", \"v2Checkpoint\", \"vacuumProtocolCheck\", \"rowTracking\")\n    val writerFeatures = Set(\n      // Legacy incompatible features (allowed as long as they are inactive)\n      \"invariants\",\n      \"checkConstraints\",\n      \"changeDataFeed\",\n      \"identityColumns\",\n      \"generatedColumns\",\n      // Compatible table features\n      \"appendOnly\",\n      \"columnMapping\",\n      \"icebergCompatV3\",\n      \"icebergWriterCompatV3\",\n      \"domainMetadata\",\n      \"vacuumProtocolCheck\",\n      \"v2Checkpoint\",\n      \"checkpointProtection\",\n      \"inCommitTimestamp\",\n      \"clustering\",\n      \"typeWidening\",\n      \"typeWidening-preview\",\n      \"timestampNtz\",\n      \"deletionVectors\",\n      \"rowTracking\",\n      \"variantType\",\n      \"variantType-preview\",\n      \"variantShredding\",\n      \"variantShredding-preview\",\n      \"icebergCompatV2\",\n      \"icebergWriterCompatV1\",\n      \"catalogManaged\")\n    val protocol = new Protocol(3, 7, readerFeatures.asJava, writerFeatures.asJava)\n    val metadata = getCompatEnabledMetadata(cmTestSchema())\n    validateAndUpdateIcebergWriterCompatV3Metadata(true, metadata, protocol, Optional.empty())\n    validateAndUpdateIcebergWriterCompatV3Metadata(false, metadata, protocol, Optional.empty())\n  }\n\n  Seq(\"collations\", \"defaultColumns\").foreach { unsupportedIncompatibleFeature =>\n    test(s\"cannot enable with incompatible UNSUPPORTED feature $unsupportedIncompatibleFeature\") {\n      // We add this test here so that it will fail when we add Kernel support for these features\n      // When this happens -> add the feature to the test above\n      checkUnsupportedOrIncompatibleFeature(\n        unsupportedIncompatibleFeature,\n        \"Unsupported Delta table feature: table requires feature \" +\n          s\"\"\"\"$unsupportedIncompatibleFeature\" which is unsupported by this version of \"\"\" +\n          \"Delta Kernel\")\n    }\n  }\n\n  /* --- INVARIANTS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.invariants\", \"{\\\"expression\\\": { \\\"expression\\\": \\\"x > 3\\\"} }\")\n          .build())),\n    \"invariants\")\n\n  /* --- CHANGE_DATA_FEED_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(cmTestSchema())\n      .withMergedConfiguration(Map(TableConfig.CHANGE_DATA_FEED_ENABLED.getKey -> \"true\").asJava),\n    \"changeDataFeed\")\n\n  /* --- CHECK_CONSTRAINTS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(cmTestSchema())\n      .withMergedConfiguration(Map(\"delta.constraints.a\" -> \"a = b\").asJava),\n    \"checkConstraints\")\n\n  /* --- IDENTITY_COLUMNS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putLong(\"delta.identity.start\", 1L)\n          .putLong(\"delta.identity.step\", 2L)\n          .putBoolean(\"delta.identity.allowExplicitInsert\", true)\n          .build())),\n    \"identityColumns\")\n\n  /* --- GENERATED_COLUMNS_INACTIVE_CHECK tests --- */\n  testIncompatibleActiveLegacyFeature(\n    getCompatEnabledMetadata(new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.generationExpression\", \"{\\\"expression\\\": \\\"c1 + 1\\\"}\")\n          .build())),\n    \"generatedColumns\")\n\n  /* --- requiredDependencyTableFeatures tests --- */\n  Seq(\n    (\"columnMapping\", \"icebergCompatV3\"),\n    (\"icebergCompatV3\", \"columnMapping\"),\n    (\"rowTracking\", \"icebergCompatV3\"),\n    (\"deletionVectors\", \"icebergCompatV3\"),\n    (\"variantType\", \"icebergCompatV3\")).foreach {\n    case (featureToIncludeStr, missingFeatureStr) =>\n      Seq(true, false).foreach { isNewTable =>\n        test(\n          s\"protocol is missing required feature $missingFeatureStr when \" +\n            s\"only $featureToIncludeStr present, isNewTable = $isNewTable\") {\n          val metadata = getCompatEnabledMetadata(cmTestSchema())\n          val readerFeatures: Set[String] =\n            if (Set(\"columnMapping\", \"rowTracking\").contains(featureToIncludeStr)) {\n              Set(featureToIncludeStr)\n            } else Set.empty\n          val writerFeatures = Set(\"icebergWriterCompatV3\", featureToIncludeStr)\n          val protocol = new Protocol(3, 7, readerFeatures.asJava, writerFeatures.asJava)\n          val e = intercept[KernelException] {\n            validateAndUpdateIcebergWriterCompatV3Metadata(\n              isNewTable,\n              metadata,\n              protocol,\n              Optional.empty())\n          }\n          // Since we run icebergCompatV3 validation as part of\n          // ICEBERG_COMPAT_V3_ENABLED.postProcess we actually hit the missing feature error in the\n          // icebergCompatV3 checks first\n          assert(e.getMessage.contains(\n            s\"icebergCompatV3: requires the feature '$missingFeatureStr' to be enabled\"))\n        }\n      }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/metadatadomain/JsonMetadataDomainSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metadatadomain\n\nimport java.util.Optional\n\nimport io.delta.kernel.internal.rowtracking.RowTrackingMetadataDomain\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass JsonMetadataDomainSuite extends AnyFunSuite {\n\n  test(\"JsonMetadataDomain can be serialized/deserialized - TestJsonMetadataDomain\") {\n    val testMetadataDomain = new TestJsonMetadataDomain(Optional.of(\"value1\"), Optional.empty(), 10)\n\n    // Test the serialization, empty Optional fields should be omitted\n    val config = testMetadataDomain.toJsonConfiguration\n    assert(config === \"\"\"{\"field1\":\"value1\",\"field3\":10}\"\"\")\n\n    // Test the deserialization, missing Optional fields should be initialized as empty\n    val deserializedDomain = TestJsonMetadataDomain.fromJsonConfiguration(config)\n    assert(deserializedDomain.getField1.isPresent && deserializedDomain.getField1.get === \"value1\")\n    assert(!deserializedDomain.getField2.isPresent)\n    assert(deserializedDomain.getField3 === 10)\n    assert(deserializedDomain.equals(testMetadataDomain))\n  }\n\n  test(\"JsonMetadataDomain can be serialized/deserialized - RowTrackingMetadataDomain\") {\n    // RowTrackingMetadataDomain is an actual production class with one field 'rowIdHighWaterMark'\n    val rowTrackingMetadataDomain = new RowTrackingMetadataDomain(10)\n\n    // Test the serialization\n    val config = rowTrackingMetadataDomain.toJsonConfiguration\n    assert(config === \"\"\"{\"rowIdHighWaterMark\":10}\"\"\")\n\n    // Test the deserialization\n    val deserializedDomain = RowTrackingMetadataDomain.fromJsonConfiguration(config)\n    assert(deserializedDomain.getRowIdHighWaterMark === 10)\n    assert(deserializedDomain.equals(rowTrackingMetadataDomain))\n  }\n\n  test(\"JsonMetadataDomain deserialization can handle the extra 'domainName' field\") {\n    // Delta Spark has a bug where the serialized JSON includes an unintended 'domainName' field.\n    // Delta Kernel can gracefully handle this because 'domainName' is annotated with @JsonIgnore,\n    // so this field is ignored if encountered without throwing exception.\n\n    // This test explicitly verifies that the deserialization can handle input JSON both\n    // with and without the 'domainName' field.\n\n    // Test with TestJsonMetadataDomain\n    val testJson1 = \"\"\"{\"field3\":10}\"\"\"\n    val testJson2 = \"\"\"{\"domainName\":\"testDomain\",\"field3\":10}\"\"\"\n\n    val testMD1 = TestJsonMetadataDomain.fromJsonConfiguration(testJson1)\n    val testMD2 = TestJsonMetadataDomain.fromJsonConfiguration(testJson2)\n\n    assert(!testMD1.getField1.isPresent)\n    assert(!testMD1.getField2.isPresent)\n    assert(testMD1.getField3 === 10)\n    assert(testMD1 === testMD2)\n\n    // Also test with an actual production class - RowTrackingMetadataDomain\n    val rowTrackingJson1 = \"\"\"{\"rowIdHighWaterMark\":10}\"\"\"\n    val rowTrackingJson2 = \"\"\"{\"domainName\":\"delta.rowTracking\",\"rowIdHighWaterMark\":10}\"\"\"\n\n    val rowTrackingMD1 = RowTrackingMetadataDomain.fromJsonConfiguration(rowTrackingJson1)\n    val rowTrackingMD2 = RowTrackingMetadataDomain.fromJsonConfiguration(rowTrackingJson2)\n\n    assert(rowTrackingMD1.getRowIdHighWaterMark === 10)\n    assert(rowTrackingMD1 === rowTrackingMD2)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/metadatadomain/TestJsonMetadataDomain.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metadatadomain;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.Optional;\n\n/**\n * A test implementation of {@link JsonMetadataDomain} for testing purposes. It has two Optional\n * fields and one primitive field.\n */\npublic final class TestJsonMetadataDomain extends JsonMetadataDomain {\n  private final Optional<String> field1;\n  private final Optional<String> field2;\n  private final int field3;\n\n  @JsonCreator\n  public TestJsonMetadataDomain(\n      @JsonProperty(\"field1\") Optional<String> field1,\n      @JsonProperty(\"field2\") Optional<String> field2,\n      @JsonProperty(\"field3\") int field3) {\n    this.field1 = field1;\n    this.field2 = field2;\n    this.field3 = field3;\n  }\n\n  @Override\n  public String getDomainName() {\n    return \"testDomain\";\n  }\n\n  public Optional<String> getField1() {\n    return field1;\n  }\n\n  public Optional<String> getField2() {\n    return field2;\n  }\n\n  public int getField3() {\n    return field3;\n  }\n\n  public static TestJsonMetadataDomain fromJsonConfiguration(String json) {\n    return JsonMetadataDomain.fromJsonConfiguration(json, TestJsonMetadataDomain.class);\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (obj instanceof TestJsonMetadataDomain) {\n      TestJsonMetadataDomain other = (TestJsonMetadataDomain) obj;\n      return field1.equals(other.field1) && field2.equals(other.field2) && field3 == other.field3;\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/metrics/CounterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CounterSuite extends AnyFunSuite {\n\n  test(\"Counter class\") {\n    val counter = new Counter()\n    assert(counter.value == 0)\n    counter.increment(0)\n    assert(counter.value == 0)\n    counter.increment()\n    assert(counter.value == 1)\n    counter.increment()\n    assert(counter.value == 2)\n    counter.increment(10)\n    assert(counter.value == 12)\n    counter.reset()\n    assert(counter.value == 0)\n    counter.increment()\n    assert(counter.value == 1)\n  }\n\n  test(\"Counter toString representation\") {\n    val counter = new Counter()\n    counter.increment(42)\n\n    val stringRepresentation = counter.toString()\n    assert(stringRepresentation === \"Counter(42)\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/metrics/MetricsReportSerializerSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics\n\nimport java.util.{Collections, Optional, UUID}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions.{Column, Literal, Predicate}\nimport io.delta.kernel.metrics.{ScanReport, SnapshotReport, TransactionReport}\nimport io.delta.kernel.types.{FieldMetadata, IntegerType, StringType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass MetricsReportSerializerSuite extends AnyFunSuite {\n\n  private def optionToString[T](option: Optional[T]): String = {\n    if (option.isPresent) {\n      if (option.get().isInstanceOf[String]) {\n        s\"\"\"\"${option.get()}\"\"\"\" // For string objects wrap with quotes\n      } else {\n        option.get().toString\n      }\n    } else {\n      \"null\"\n    }\n  }\n\n  private def testSnapshotReport(snapshotReport: SnapshotReport): Unit = {\n    val computeTimestampToVersionTotalDuration = optionToString(\n      snapshotReport.getSnapshotMetrics().getComputeTimestampToVersionTotalDurationNs())\n    val loadSnapshotTotalDuration =\n      snapshotReport.getSnapshotMetrics().getLoadSnapshotTotalDurationNs()\n    val loadProtocolAndMetadataDuration =\n      snapshotReport.getSnapshotMetrics().getLoadProtocolMetadataTotalDurationNs()\n    val buildLogSegmentDuration =\n      snapshotReport.getSnapshotMetrics().getLoadLogSegmentTotalDurationNs()\n    val loadCrcTotalDuration =\n      snapshotReport.getSnapshotMetrics().getLoadCrcTotalDurationNs()\n    val exception: Optional[String] = snapshotReport.getException().map(_.toString)\n    val expectedJson =\n      s\"\"\"\n         |{\"tablePath\":\"${snapshotReport.getTablePath()}\",\n         |\"operationType\":\"Snapshot\",\n         |\"reportUUID\":\"${snapshotReport.getReportUUID()}\",\n         |\"exception\":${optionToString(exception)},\n         |\"version\":${optionToString(snapshotReport.getVersion())},\n         |\"checkpointVersion\":${optionToString(snapshotReport.getCheckpointVersion())},\n         |\"providedTimestamp\":${optionToString(snapshotReport.getProvidedTimestamp())},\n         |\"snapshotMetrics\":{\n         |\"computeTimestampToVersionTotalDurationNs\":${computeTimestampToVersionTotalDuration},\n         |\"loadSnapshotTotalDurationNs\":${loadSnapshotTotalDuration},\n         |\"loadProtocolMetadataTotalDurationNs\":${loadProtocolAndMetadataDuration},\n         |\"loadLogSegmentTotalDurationNs\":${buildLogSegmentDuration},\n         |\"loadCrcTotalDurationNs\":${loadCrcTotalDuration}\n         |}\n         |}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    assert(expectedJson == snapshotReport.toJson())\n  }\n\n  test(\"SnapshotReport serializer\") {\n    val snapshotContext1 = SnapshotQueryContext.forTimestampSnapshot(\"/table/path\", 0)\n    snapshotContext1.getSnapshotMetrics.computeTimestampToVersionTotalDurationTimer.record(10)\n    snapshotContext1.getSnapshotMetrics.loadSnapshotTotalTimer.record(2000)\n    snapshotContext1.getSnapshotMetrics.loadProtocolMetadataTotalDurationTimer.record(1000)\n    snapshotContext1.getSnapshotMetrics.loadLogSegmentTotalDurationTimer.record(500)\n    snapshotContext1.getSnapshotMetrics.loadCrcTotalDurationTimer.record(250)\n    snapshotContext1.setResolvedVersion(25)\n    snapshotContext1.setCheckpointVersion(Optional.of(20))\n    val exception = new RuntimeException(\"something something failed\")\n\n    val snapshotReport1 = SnapshotReportImpl.forError(\n      snapshotContext1,\n      exception)\n\n    // Manually check expected JSON\n    val expectedJson =\n      s\"\"\"\n        |{\"tablePath\":\"/table/path\",\n        |\"operationType\":\"Snapshot\",\n        |\"reportUUID\":\"${snapshotReport1.getReportUUID()}\",\n        |\"exception\":\"$exception\",\n        |\"version\":25,\n        |\"checkpointVersion\":20,\n        |\"providedTimestamp\":0,\n        |\"snapshotMetrics\":{\n        |\"computeTimestampToVersionTotalDurationNs\":10,\n        |\"loadSnapshotTotalDurationNs\":2000,\n        |\"loadProtocolMetadataTotalDurationNs\":1000,\n        |\"loadLogSegmentTotalDurationNs\":500,\n        |\"loadCrcTotalDurationNs\":250\n        |}\n        |}\n        |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    assert(expectedJson == snapshotReport1.toJson())\n\n    // Check with test function\n    testSnapshotReport(snapshotReport1)\n\n    // Empty options for all possible fields (version, providedTimestamp and exception)\n    val snapshotContext2 = SnapshotQueryContext.forLatestSnapshot(\"/table/path\")\n    val snapshotReport2 = SnapshotReportImpl.forSuccess(snapshotContext2)\n    testSnapshotReport(snapshotReport2)\n  }\n\n  private def testTransactionReport(transactionReport: TransactionReport): Unit = {\n    val exception: Optional[String] = transactionReport.getException().map(_.toString)\n    val snapshotReportUUID: Optional[String] =\n      transactionReport.getSnapshotReportUUID().map(_.toString)\n    val transactionMetrics = transactionReport.getTransactionMetrics\n\n    val clusterColString = transactionReport.getClusteringColumns\n      .asScala\n      .map(col =>\n        col.getNames.map(s => s\"\"\"\"$s\"\"\"\").mkString(\"[\", \",\", \"]\"))\n      .mkString(\"[\", \",\", \"]\")\n\n    val expectedJson =\n      s\"\"\"\n         |{\"tablePath\":\"${transactionReport.getTablePath()}\",\n         |\"operationType\":\"Transaction\",\n         |\"reportUUID\":\"${transactionReport.getReportUUID()}\",\n         |\"exception\":${optionToString(exception)},\n         |\"operation\":\"${transactionReport.getOperation()}\",\n         |\"engineInfo\":\"${transactionReport.getEngineInfo()}\",\n         |\"baseSnapshotVersion\":${transactionReport.getBaseSnapshotVersion()},\n         |\"snapshotReportUUID\":${optionToString(snapshotReportUUID)},\n         |\"committedVersion\":${optionToString(transactionReport.getCommittedVersion())},\n         |\"clusteringColumns\":$clusterColString,\n         |\"transactionMetrics\":{\n         |\"totalCommitDurationNs\":${transactionMetrics.getTotalCommitDurationNs},\n         |\"numCommitAttempts\":${transactionMetrics.getNumCommitAttempts},\n         |\"numAddFiles\":${transactionMetrics.getNumAddFiles},\n         |\"numRemoveFiles\":${transactionMetrics.getNumRemoveFiles},\n         |\"numTotalActions\":${transactionMetrics.getNumTotalActions},\n         |\"totalAddFilesSizeInBytes\":${transactionMetrics.getTotalAddFilesSizeInBytes},\n         |\"totalRemoveFilesSizeInBytes\":${transactionMetrics.getTotalRemoveFilesSizeInBytes}\n         |}\n         |}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    assert(expectedJson == transactionReport.toJson())\n  }\n\n  test(\"TransactionReport serializer\") {\n    val snapshotReport1 = SnapshotReportImpl.forSuccess(\n      SnapshotQueryContext.forVersionSnapshot(\"/table/path\", 1))\n    val exception = new RuntimeException(\"something something failed\")\n\n    // Initialize transaction metrics and record some values\n    val transactionMetrics1 = TransactionMetrics.forNewTable();\n    transactionMetrics1.totalCommitTimer.record(200)\n    transactionMetrics1.commitAttemptsCounter.increment(2)\n    transactionMetrics1.updateForAddFile(1000)\n    transactionMetrics1.updateForAddFile(100)\n    transactionMetrics1.updateForRemoveFile(1000)\n    transactionMetrics1.totalActionsCounter.increment(90)\n\n    val transactionReport1 = new TransactionReportImpl(\n      \"/table/path\",\n      \"test-operation\",\n      \"test-engine\",\n      Optional.of(2), /* committedVersion */\n      Optional.of(Collections.singletonList(\n        new Column(Array[String](\"test-clustering-col1\", \"nested\")))),\n      transactionMetrics1,\n      Optional.of(snapshotReport1),\n      Optional.of(exception))\n\n    // Manually check expected JSON\n    val expectedJson =\n      s\"\"\"\n         |{\"tablePath\":\"/table/path\",\n         |\"operationType\":\"Transaction\",\n         |\"reportUUID\":\"${transactionReport1.getReportUUID()}\",\n         |\"exception\":\"$exception\",\n         |\"operation\":\"test-operation\",\n         |\"engineInfo\":\"test-engine\",\n         |\"baseSnapshotVersion\":1,\n         |\"snapshotReportUUID\":\"${snapshotReport1.getReportUUID}\",\n         |\"committedVersion\":2,\n         |\"clusteringColumns\":[[\"test-clustering-col1\",\"nested\"]],\n         |\"transactionMetrics\":{\n         |\"totalCommitDurationNs\":200,\n         |\"numCommitAttempts\":2,\n         |\"numAddFiles\":2,\n         |\"numRemoveFiles\":1,\n         |\"numTotalActions\":90,\n         |\"totalAddFilesSizeInBytes\":1100,\n         |\"totalRemoveFilesSizeInBytes\":1000\n         |}\n         |}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    assert(expectedJson == transactionReport1.toJson())\n    // Check with test function\n    testTransactionReport(transactionReport1)\n\n    // Initialize snapshot report for the empty table case\n    val snapshotReport2 = SnapshotReportImpl.forSuccess(\n      SnapshotQueryContext.forVersionSnapshot(\"/table/path\", -1))\n    // Empty option for all possible fields (committedVersion, exception)\n    val transactionReport2 = new TransactionReportImpl(\n      \"/table/path\",\n      \"test-operation-2\",\n      \"test-engine-2\",\n      Optional.empty(), /* committedVersion */\n      Optional.of(Collections.singletonList(new Column(\"test-clustering-col1\"))),\n      // empty/un-incremented transaction metrics\n      TransactionMetrics.withExistingTableFileSizeHistogram(Optional.empty()),\n      Optional.of(snapshotReport2),\n      Optional.empty() /* exception */\n    )\n    testTransactionReport(transactionReport2)\n  }\n\n  private def testScanReport(scanReport: ScanReport): Unit = {\n    val exception: Optional[String] = scanReport.getException().map(_.toString)\n    val filter: Optional[String] = scanReport.getFilter.map(_.toString)\n    val partitionPredicate: Optional[String] = scanReport.getPartitionPredicate().map(_.toString)\n    val dataSkippingFilter: Optional[String] = scanReport.getDataSkippingFilter().map(_.toString)\n    val scanMetrics = scanReport.getScanMetrics\n\n    val expectedJson =\n      s\"\"\"\n         |{\"tablePath\":\"${scanReport.getTablePath()}\",\n         |\"operationType\":\"Scan\",\n         |\"reportUUID\":\"${scanReport.getReportUUID()}\",\n         |\"exception\":${optionToString(exception)},\n         |\"tableVersion\":${scanReport.getTableVersion()},\n         |\"tableSchema\":\"${scanReport.getTableSchema()}\",\n         |\"snapshotReportUUID\":\"${scanReport.getSnapshotReportUUID}\",\n         |\"filter\":${optionToString(filter)},\n         |\"readSchema\":\"${scanReport.getReadSchema}\",\n         |\"partitionPredicate\":${optionToString(partitionPredicate)},\n         |\"dataSkippingFilter\":${optionToString(dataSkippingFilter)},\n         |\"isFullyConsumed\":${scanReport.getIsFullyConsumed},\n         |\"scanMetrics\":{\n         |\"totalPlanningDurationNs\":${scanMetrics.getTotalPlanningDurationNs},\n         |\"numAddFilesSeen\":${scanMetrics.getNumAddFilesSeen},\n         |\"numAddFilesSeenFromDeltaFiles\":${scanMetrics.getNumAddFilesSeenFromDeltaFiles},\n         |\"numActiveAddFiles\":${scanMetrics.getNumActiveAddFiles},\n         |\"numDuplicateAddFiles\":${scanMetrics.getNumDuplicateAddFiles},\n         |\"numRemoveFilesSeenFromDeltaFiles\":${scanMetrics.getNumRemoveFilesSeenFromDeltaFiles}\n         |}\n         |}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    assert(expectedJson == scanReport.toJson())\n  }\n\n  test(\"ScanReport serializer\") {\n    val snapshotReportUUID = java.util.UUID.randomUUID()\n\n    // tableSchema now includes FieldMetadata with a null value.\n    val fmNull = FieldMetadata.builder().putString(\"kNull\", null).build()\n    val fmArray =\n      FieldMetadata.builder()\n        .putStringArray(\"arr\", Array[String](\"x\", null, \"z\"))\n        .build()\n    val tableSchema = new StructType()\n      .add(\"part\", IntegerType.INTEGER, fmNull)\n      .add(\"id\", IntegerType.INTEGER, fmArray)\n\n    val partitionPredicate = new Predicate(\">\", new Column(\"part\"), Literal.ofInt(1))\n    val exception = new RuntimeException(\"something something failed\")\n\n    // Initialize transaction metrics and record some values\n    val scanMetrics = new ScanMetrics()\n    scanMetrics.totalPlanningTimer.record(200)\n    scanMetrics.addFilesCounter.increment(100)\n    scanMetrics.addFilesFromDeltaFilesCounter.increment(90)\n    scanMetrics.activeAddFilesCounter.increment(10)\n    scanMetrics.removeFilesFromDeltaFilesCounter.increment(10)\n\n    val scanReport1 = new ScanReportImpl(\n      \"/table/path\",\n      1,\n      tableSchema,\n      snapshotReportUUID,\n      Optional.of(partitionPredicate),\n      new StructType().add(\"id\", IntegerType.INTEGER),\n      Optional.of(partitionPredicate),\n      Optional.empty(),\n      true,\n      scanMetrics,\n      Optional.of(exception))\n\n    // Manually check expected JSON including field metadata\n    val tableSchemaStr =\n      \"struct(StructField(name=part,type=integer,nullable=true,metadata={kNull=null},\" +\n        \"typeChanges=[]), \" +\n        \"StructField(name=id,type=integer,nullable=true,metadata={arr=[x, null, z]},\" +\n        \"typeChanges=[]))\"\n    val readSchemaStr =\n      \"struct(StructField(name=id,type=integer,nullable=true,metadata={},typeChanges=[]))\"\n\n    val expectedJson =\n      s\"\"\"\n         |{\"tablePath\":\"/table/path\",\n         |\"operationType\":\"Scan\",\n         |\"reportUUID\":\"${scanReport1.getReportUUID}\",\n         |\"exception\":\"$exception\",\n         |\"tableVersion\":1,\n         |\"tableSchema\":\"$tableSchemaStr\",\n         |\"snapshotReportUUID\":\"$snapshotReportUUID\",\n         |\"filter\":\"(column(`part`) > 1)\",\n         |\"readSchema\":\"$readSchemaStr\",\n         |\"partitionPredicate\":\"(column(`part`) > 1)\",\n         |\"dataSkippingFilter\":null,\n         |\"isFullyConsumed\":true,\n         |\"scanMetrics\":{\n         |\"totalPlanningDurationNs\":200,\n         |\"numAddFilesSeen\":100,\n         |\"numAddFilesSeenFromDeltaFiles\":90,\n         |\"numActiveAddFiles\":10,\n         |\"numDuplicateAddFiles\":0,\n         |\"numRemoveFilesSeenFromDeltaFiles\":10\n         |}\n         |}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    assert(expectedJson == scanReport1.toJson())\n\n    // Check with test function\n    testScanReport(scanReport1)\n\n    // Empty options for all possible fields (version, providedTimestamp and exception)\n    val scanReport2 = new ScanReportImpl(\n      \"/table/path\",\n      1,\n      tableSchema,\n      snapshotReportUUID,\n      Optional.empty(),\n      tableSchema,\n      Optional.empty(),\n      Optional.empty(),\n      false, // isFullyConsumed\n      new ScanMetrics(),\n      Optional.empty())\n    testScanReport(scanReport2)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/metrics/TimerSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.metrics\n\nimport java.util.concurrent.Callable\nimport java.util.function.Supplier\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TimerSuite extends AnyFunSuite {\n\n  val NANOSECONDS_PER_MILLISECOND = 1000000\n\n  def millisToNanos(millis: Long): Long = {\n    millis * NANOSECONDS_PER_MILLISECOND\n  }\n\n  /**\n   * @param incrementFx Function given (duration, timer) increments the timer by approximately\n   *                    duration ms\n   */\n  def testTimer(incrementFx: (Long, Timer) => Unit): Unit = {\n    val timer = new Timer()\n    // Verify initial values\n    assert(timer.count == 0)\n    assert(timer.totalDurationNs == 0)\n\n    def incrementAndCheck(amtMillis: Long): Unit = {\n      val initialCount = timer.count()\n      val initialDuration = timer.totalDurationNs() // in nanoseconds\n\n      val startTime = System.currentTimeMillis()\n      incrementFx(amtMillis, timer)\n      // upperLimitDuration is in milliseconds; we take the max of time elapsed vs the incrementAmt\n      val upperLimitDuration = Math.max(\n        // we pad by 1 due to rounding of nanoseconds to milliseconds for system time\n        System.currentTimeMillis() - startTime + 1,\n        amtMillis)\n\n      // check count\n      assert(timer.count == initialCount + 1)\n      // check lowerbound\n      assert(timer.totalDurationNs >= initialDuration + millisToNanos(amtMillis))\n      // check upperbound\n      assert(timer.totalDurationNs <= initialDuration + millisToNanos(upperLimitDuration))\n    }\n\n    incrementAndCheck(0)\n    incrementAndCheck(20)\n    incrementAndCheck(50)\n  }\n\n  test(\"Timer class\") {\n    // Using Timer.record()\n    testTimer((amount, timer) => timer.record(millisToNanos(amount)))\n\n    // Using Timer.start()\n    testTimer((amount, timer) => {\n      val timed = timer.start()\n      Thread.sleep(amount)\n      timed.stop()\n    })\n\n    // Using Timer.time(supplier)\n    def supplier(amount: Long): Supplier[Long] = {\n      () =>\n        {\n          Thread.sleep(amount)\n          amount\n        }\n    }\n    testTimer((amount, timer) => {\n      timer.time(supplier(amount))\n    })\n\n    // Using Timer.timeCallable\n    def callable(amount: Long): Callable[Long] = {\n      () =>\n        {\n          Thread.sleep(amount)\n          amount\n        }\n    }\n    testTimer((amount, timer) => {\n      timer.timeCallable(callable(amount))\n    })\n\n    // Using Timer.time(runnable)\n    def runnable(amount: Long): Runnable = {\n      () => Thread.sleep(amount)\n    }\n    testTimer((amount, timer) => {\n      timer.time(runnable(amount))\n    })\n  }\n\n  test(\"Timer class with exceptions\") {\n    // We catch the exception outside of the functional interfaces\n    def catchException(fx: () => Any): Unit = {\n      try {\n        fx.apply()\n      } catch {\n        case _: Exception =>\n      }\n    }\n\n    // Using Timer.time(supplier)\n    def supplier(amount: Long): Supplier[Long] = {\n      () =>\n        {\n          Thread.sleep(amount)\n          throw new RuntimeException()\n        }\n    }\n    testTimer((amount, timer) => {\n      catchException(() => timer.time(supplier(amount)))\n    })\n\n    // Using Timer.timeCallable\n    def callable(amount: Long): Callable[Long] = {\n      () =>\n        {\n          Thread.sleep(amount)\n          throw new RuntimeException()\n        }\n    }\n    testTimer((amount, timer) => {\n      catchException(() => timer.timeCallable(callable(amount)))\n    })\n\n    // Using Timer.time(runnable)\n    def runnable(amount: Long): Runnable = {\n      () =>\n        {\n          Thread.sleep(amount)\n          throw new RuntimeException()\n        }\n    }\n    testTimer((amount, timer) => {\n      catchException(() => timer.time(runnable(amount)))\n    })\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/replay/ActionsIteratorSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.replay\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, Row}\nimport io.delta.kernel.engine._\nimport io.delta.kernel.expressions.Predicate\nimport io.delta.kernel.test.BaseMockJsonHandler\nimport io.delta.kernel.test.MockEngineUtils\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ActionsIteratorSuite extends AnyFunSuite with MockEngineUtils {\n\n  /**\n   * Test for ActionsIterator resource leak fix validation\n   *\n   * This test validates that the fix applied in ActionsIterator.java prevents resource\n   * leaks by ensuring that CloseableIterators are properly closed when exceptions occur.\n   *\n   * The specific fix being tested: Utils.closeCloseablesSilently(dataIter) in the catch block of\n   * readCommitOrCompactionFile method.\n   */\n  test(\"ActionsIterator readCommitOrCompactionFile resource cleanup\") {\n    var iteratorClosed = false\n\n    val engine = mockEngine(jsonHandler = new BaseMockJsonHandler {\n      override def readJsonFiles(\n          fileIter: CloseableIterator[FileStatus],\n          physicalSchema: StructType,\n          predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n\n        // Return an empty iterator that tracks closure\n        new CloseableIterator[ColumnarBatch] {\n          override def hasNext(): Boolean =\n            throw new NoSuchElementException(\"This is a test exception\")\n          override def next(): ColumnarBatch =\n            throw new UnsupportedOperationException(\"Not needed for this test\")\n          override def close(): Unit = iteratorClosed = true\n        }\n      }\n    })\n\n    val testFile = FileStatus.of(\n      \"/path/to/00000000000000000000.json\",\n      100L,\n      System.currentTimeMillis())\n    val files = Collections.singletonList(testFile)\n    val schema = new StructType()\n\n    val actionsIterator =\n      new ActionsIterator(engine, files, schema, Optional.empty[Predicate]())\n\n    assertThrows[NoSuchElementException] {\n      actionsIterator.hasNext()\n    }\n\n    // Verify that resources were cleaned up\n    assert(iteratorClosed, \"Internal iterator should be closed after exception in ActionsIterator\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/skipping/DataSkippingUtilsSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.skipping\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions.{Column, Expression, Predicate}\nimport io.delta.kernel.internal.skipping.DataSkippingUtils.constructDataSkippingFilter\nimport io.delta.kernel.internal.skipping.StatsSchemaHelper.{MAX, MIN, STATS_WITH_COLLATION}\nimport io.delta.kernel.internal.util.ExpressionUtils.createPredicate\nimport io.delta.kernel.test.TestUtils\nimport io.delta.kernel.types._\nimport io.delta.kernel.types.IntegerType.INTEGER\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DataSkippingUtilsSuite extends AnyFunSuite with TestUtils {\n\n  def dataSkippingPredicate(\n      operator: String,\n      children: Seq[Expression],\n      referencedColumns: Set[Column]): DataSkippingPredicate = {\n    new DataSkippingPredicate(operator, children.asJava, referencedColumns.asJava)\n  }\n\n  def dataSkippingPredicate(\n      operator: String,\n      left: DataSkippingPredicate,\n      right: DataSkippingPredicate): DataSkippingPredicate = {\n    new DataSkippingPredicate(operator, left, right)\n  }\n\n  def dataSkippingPredicateWithCollation(\n      operator: String,\n      children: Seq[Expression],\n      collation: CollationIdentifier,\n      referencedColumns: Set[Column]): DataSkippingPredicate = {\n    new DataSkippingPredicate(operator, children.asJava, collation, referencedColumns.asJava)\n  }\n\n  /* For struct type checks for equality based on field names & data type only */\n  def compareDataTypeUnordered(type1: DataType, type2: DataType): Boolean = (type1, type2) match {\n    case (schema1: StructType, schema2: StructType) =>\n      val fields1 = schema1.fields().asScala.sortBy(_.getName)\n      val fields2 = schema2.fields().asScala.sortBy(_.getName)\n      if (fields1.length != fields2.length) {\n        false\n      } else {\n        fields1.zip(fields2).forall { case (field1: StructField, field2: StructField) =>\n          field1.getName == field2.getName &&\n          compareDataTypeUnordered(field1.getDataType, field2.getDataType)\n        }\n      }\n    case _ =>\n      type1 == type2\n  }\n\n  def checkPruneStatsSchema(\n      inputSchema: StructType,\n      referencedCols: Set[Column],\n      expectedSchema: StructType): Unit = {\n    val prunedSchema = DataSkippingUtils.pruneStatsSchema(inputSchema, referencedCols.asJava)\n    assert(\n      compareDataTypeUnordered(expectedSchema, prunedSchema),\n      s\"expected=$expectedSchema\\nfound=$prunedSchema\")\n  }\n\n  test(\"pruneStatsSchema - multiple basic cases one level of nesting\") {\n    val nestedField = new StructField(\n      \"nested\",\n      new StructType()\n        .add(\"col1\", INTEGER)\n        .add(\"col2\", INTEGER),\n      true)\n    val testSchema = new StructType()\n      .add(nestedField)\n      .add(\"top_level_col\", INTEGER)\n    // no columns pruned\n    checkPruneStatsSchema(\n      testSchema,\n      Set(col(\"top_level_col\"), nestedCol(\"nested.col1\"), nestedCol(\"nested.col2\")),\n      testSchema)\n    // top level column pruned\n    checkPruneStatsSchema(\n      testSchema,\n      Set(nestedCol(\"nested.col1\"), nestedCol(\"nested.col2\")),\n      new StructType().add(nestedField))\n    // nested column only one field pruned\n    checkPruneStatsSchema(\n      testSchema,\n      Set(nestedCol(\"top_level_col\"), nestedCol(\"nested.col1\")),\n      new StructType()\n        .add(\"nested\", new StructType().add(\"col1\", INTEGER))\n        .add(\"top_level_col\", INTEGER))\n    // nested column completely pruned\n    checkPruneStatsSchema(\n      testSchema,\n      Set(nestedCol(\"top_level_col\")),\n      new StructType().add(\"top_level_col\", INTEGER))\n    // prune all columns\n    checkPruneStatsSchema(\n      testSchema,\n      Set(),\n      new StructType())\n  }\n\n  test(\"pruneStatsSchema - 3 levels of nesting\") {\n    /*\n    |--level1: struct\n    |   |--level2: struct\n    |       |--level3: struct\n    |           |--level_4_col: int\n    |       |--level_3_col: int\n    |   |--level_2_col: int\n     */\n    val testSchema = new StructType()\n      .add(\n        \"level1\",\n        new StructType()\n          .add(\n            \"level2\",\n            new StructType()\n              .add(\n                \"level3\",\n                new StructType().add(\"level_4_col\", INTEGER))\n              .add(\"level_3_col\", INTEGER))\n          .add(\"level_2_col\", INTEGER))\n    // prune only 4th level col\n    checkPruneStatsSchema(\n      testSchema,\n      Set(nestedCol(\"level1.level2.level_3_col\"), nestedCol(\"level1.level_2_col\")),\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\"level2\", new StructType().add(\"level_3_col\", INTEGER))\n            .add(\"level_2_col\", INTEGER)))\n    // prune only 3rd level column\n    checkPruneStatsSchema(\n      testSchema,\n      Set(nestedCol(\"level1.level2.level3.level_4_col\"), nestedCol(\"level1.level_2_col\")),\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\n              \"level2\",\n              new StructType()\n                .add(\n                  \"level3\",\n                  new StructType().add(\"level_4_col\", INTEGER)))\n            .add(\"level_2_col\", INTEGER)))\n    // prune 4th and 3rd level column\n    checkPruneStatsSchema(\n      testSchema,\n      Set(nestedCol(\"level1.level_2_col\")),\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\"level_2_col\", INTEGER)))\n    // prune all columns\n    checkPruneStatsSchema(\n      testSchema,\n      Set(),\n      new StructType())\n  }\n\n  test(\"pruneStatsSchema - collated statistics\") {\n    val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE.75\")\n    val unicode = CollationIdentifier.fromString(\"ICU.UNICODE.74.1\")\n    val unicodeString = new StringType(unicode)\n\n    val ab = new StructType()\n      .add(\"a\", StringType.STRING)\n      .add(\"b\", unicodeString)\n\n    val statsSchema = new StructType()\n      .add(MIN, ab)\n      .add(MAX, ab)\n      .add(\n        STATS_WITH_COLLATION,\n        new StructType()\n          .add(\n            utf8Lcase.toString,\n            new StructType()\n              .add(MIN, ab)\n              .add(MAX, ab))\n          .add(\n            unicode.toString,\n            new StructType()\n              .add(MIN, ab)\n              .add(MAX, ab)))\n\n    val referenced = Set(\n      nestedCol(s\"$MAX.b\"),\n      collatedStatsCol(utf8Lcase, MIN, \"a\"),\n      collatedStatsCol(unicode, MAX, \"b\"))\n\n    val expected = new StructType()\n      .add(MAX, new StructType().add(\"b\", unicodeString))\n      .add(\n        STATS_WITH_COLLATION,\n        new StructType()\n          .add(\n            utf8Lcase.toString,\n            new StructType()\n              .add(MIN, new StructType().add(\"a\", StringType.STRING)))\n          .add(\n            unicode.toString,\n            new StructType()\n              .add(MAX, new StructType().add(\"b\", unicodeString))))\n\n    checkPruneStatsSchema(statsSchema, referenced, expected)\n  }\n\n  // TODO: add tests for remaining operators\n  test(\"check constructDataSkippingFilter\") {\n    val testCases = Seq(\n      // (schema, predicate, expectedDataSkippingPredicateOpt)\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\"<\", col(\"a\"), col(\"b\"), Optional.empty[CollationIdentifier]),\n        None),\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\"<\", col(\"a\"), literal(\"x\"), Optional.empty[CollationIdentifier]),\n        Some(dataSkippingPredicate(\n          \"<\",\n          Seq(nestedCol(s\"$MIN.a\"), literal(\"x\")),\n          Set(nestedCol(s\"$MIN.a\"))))),\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\"<\", literal(\"x\"), col(\"a\"), Optional.empty[CollationIdentifier]),\n        Some(dataSkippingPredicate(\n          \">\",\n          Seq(nestedCol(s\"$MAX.a\"), literal(\"x\")),\n          Set(nestedCol(s\"$MAX.a\"))))),\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\">\", col(\"a\"), literal(\"x\"), Optional.empty[CollationIdentifier]),\n        Some(dataSkippingPredicate(\n          \">\",\n          Seq(nestedCol(s\"$MAX.a\"), literal(\"x\")),\n          Set(nestedCol(s\"$MAX.a\"))))),\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER),\n        createPredicate(\"=\", col(\"a\"), literal(10), Optional.empty[CollationIdentifier]),\n        Some(dataSkippingPredicate(\n          \"AND\",\n          dataSkippingPredicate(\n            \"<=\",\n            Seq(nestedCol(s\"$MIN.a\"), literal(10)),\n            Set(nestedCol(s\"$MIN.a\"))),\n          dataSkippingPredicate(\n            \">=\",\n            Seq(nestedCol(s\"$MAX.a\"), literal(10)),\n            Set(nestedCol(s\"$MAX.a\")))))),\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\"<\", col(\"a\"), literal(10), Optional.empty[CollationIdentifier])),\n        Some(dataSkippingPredicate(\n          \">=\",\n          Seq(nestedCol(s\"$MAX.a\"), literal(10)),\n          Set(nestedCol(s\"$MAX.a\"))))),\n      // NOT over AND: NOT(a < 5 AND a > 10) => (max.a >= 5) OR (min.a <= 10)\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\n            \"AND\",\n            createPredicate(\"<\", col(\"a\"), literal(5), Optional.empty[CollationIdentifier]),\n            createPredicate(\">\", col(\"a\"), literal(10), Optional.empty[CollationIdentifier]),\n            Optional.empty[CollationIdentifier])),\n        Some(dataSkippingPredicate(\n          \"OR\",\n          dataSkippingPredicate(\n            \">=\",\n            Seq(nestedCol(s\"$MAX.a\"), literal(5)),\n            Set(nestedCol(s\"$MAX.a\"))),\n          dataSkippingPredicate(\n            \"<=\",\n            Seq(nestedCol(s\"$MIN.a\"), literal(10)),\n            Set(nestedCol(s\"$MIN.a\")))))),\n      // NOT over OR: NOT(a < 5 OR a > 10) => (max.a >= 5) AND (min.a <= 10)\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\n            \"OR\",\n            createPredicate(\"<\", col(\"a\"), literal(5), Optional.empty[CollationIdentifier]),\n            createPredicate(\">\", col(\"a\"), literal(10), Optional.empty[CollationIdentifier]),\n            Optional.empty[CollationIdentifier])),\n        Some(dataSkippingPredicate(\n          \"AND\",\n          dataSkippingPredicate(\n            \">=\",\n            Seq(nestedCol(s\"$MAX.a\"), literal(5)),\n            Set(nestedCol(s\"$MAX.a\"))),\n          dataSkippingPredicate(\n            \"<=\",\n            Seq(nestedCol(s\"$MIN.a\"), literal(10)),\n            Set(nestedCol(s\"$MIN.a\")))))),\n      // NOT over OR with one ineligible leg: NOT(a < b OR a < 5) => NOT(a < b) AND NOT(a < 5)\n      // The first leg is ineligible; AND with single leg should return that leg only\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\n            \"OR\",\n            createPredicate(\"<\", col(\"a\"), col(\"b\"), Optional.empty[CollationIdentifier]),\n            createPredicate(\"<\", col(\"a\"), literal(5), Optional.empty[CollationIdentifier]),\n            Optional.empty[CollationIdentifier])),\n        Some(dataSkippingPredicate(\n          \">=\",\n          Seq(nestedCol(s\"$MAX.a\"), literal(5)),\n          Set(nestedCol(s\"$MAX.a\"))))),\n      // NOT over AND with one ineligible leg: NOT(a < 5 AND a < b)\n      // => NOT(a < 5) OR NOT(a < b); since OR needs both legs, expect None\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\n            \"AND\",\n            createPredicate(\"<\", col(\"a\"), literal(5), Optional.empty[CollationIdentifier]),\n            createPredicate(\"<\", col(\"a\"), col(\"b\"), Optional.empty[CollationIdentifier]),\n            Optional.empty[CollationIdentifier])),\n        None),\n      // Double NOT elimination: NOT(NOT(a < 5)) => a < 5 => min.a < 5\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          new Predicate(\n            \"NOT\",\n            createPredicate(\"<\", col(\"a\"), literal(5), Optional.empty[CollationIdentifier]))),\n        Some(dataSkippingPredicate(\n          \"<\",\n          Seq(nestedCol(s\"$MIN.a\"), literal(5)),\n          Set(nestedCol(s\"$MIN.a\"))))),\n      // Cross-column case: NOT(a < 5 OR b > 7) => (max.a >= 5) AND (min.b <= 7)\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", IntegerType.INTEGER),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\n            \"OR\",\n            createPredicate(\"<\", col(\"a\"), literal(5), Optional.empty[CollationIdentifier]),\n            createPredicate(\">\", col(\"b\"), literal(7), Optional.empty[CollationIdentifier]),\n            Optional.empty[CollationIdentifier])),\n        Some(dataSkippingPredicate(\n          \"AND\",\n          dataSkippingPredicate(\n            \">=\",\n            Seq(nestedCol(s\"$MAX.a\"), literal(5)),\n            Set(nestedCol(s\"$MAX.a\"))),\n          dataSkippingPredicate(\n            \"<=\",\n            Seq(nestedCol(s\"$MIN.b\"), literal(7)),\n            Set(nestedCol(s\"$MIN.b\")))))))\n\n    testCases.foreach { case (schema, predicate, expectedDataSkippingPredicateOpt) =>\n      val dataSkippingPredicateOpt =\n        JavaOptionalOps(constructDataSkippingFilter(predicate, schema)).toScala\n      (dataSkippingPredicateOpt, expectedDataSkippingPredicateOpt) match {\n        case (Some(dataSkippingPredicate), Some(expectedDataSkippingPredicate)) =>\n          assert(dataSkippingPredicate == expectedDataSkippingPredicate)\n        case (None, None) => // pass\n        case _ =>\n          fail(s\"Expected $expectedDataSkippingPredicateOpt, found $dataSkippingPredicateOpt\")\n      }\n    }\n  }\n\n  test(\"check constructDataSkippingFilter with collations\") {\n    val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE.75\")\n    val unicode = CollationIdentifier.fromString(\"ICU.UNICODE.74.1\")\n\n    val testCases = Seq(\n      // (schema, predicate, expectedDataSkippingPredicateOpt)\n      // Ineligible: both sides are columns\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\"<\", col(\"a\"), col(\"b\"), Optional.of(utf8Lcase)),\n        None),\n      // Eligible: a < \"m\" with collation -> min(a, collation) < \"m\"\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\"<\", col(\"a\"), literal(\"m\"), Optional.of(utf8Lcase)), {\n          val minA = collatedStatsCol(utf8Lcase, MIN, \"a\")\n          Some(dataSkippingPredicateWithCollation(\n            \"<\",\n            Seq(minA, literal(\"m\")),\n            utf8Lcase,\n            Set(minA)))\n        }),\n      // Reversed comparator: \"m\" < a -> max(a, collation) > \"m\"\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING),\n        createPredicate(\"<\", literal(\"m\"), col(\"a\"), Optional.of(utf8Lcase)), {\n          val maxA = collatedStatsCol(utf8Lcase, MAX, \"a\")\n          Some(dataSkippingPredicateWithCollation(\n            \">\",\n            Seq(maxA, literal(\"m\")),\n            utf8Lcase,\n            Set(maxA)))\n        }),\n      // Direct \">\": a > \"m\" -> max(a, collation) > \"m\"\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING),\n        createPredicate(\">\", col(\"a\"), literal(\"m\"), Optional.of(utf8Lcase)), {\n          val maxA = collatedStatsCol(utf8Lcase, MAX, \"a\")\n          Some(dataSkippingPredicateWithCollation(\n            \">\",\n            Seq(maxA, literal(\"m\")),\n            utf8Lcase,\n            Set(maxA)))\n        }),\n      // Equality\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING),\n        createPredicate(\"=\", col(\"a\"), literal(\"abc\"), Optional.of(unicode)), {\n          val minA = collatedStatsCol(unicode, MIN, \"a\")\n          val maxA = collatedStatsCol(unicode, MAX, \"a\")\n          Some(dataSkippingPredicate(\n            \"AND\",\n            dataSkippingPredicateWithCollation(\"<=\", Seq(minA, literal(\"abc\")), unicode, Set(minA)),\n            dataSkippingPredicateWithCollation(\n              \">=\",\n              Seq(maxA, literal(\"abc\")),\n              unicode,\n              Set(maxA))))\n        }),\n      // NOT over comparator: NOT(a < \"m\") -> max(a, collation) >= \"m\"\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\"<\", col(\"a\"), literal(\"m\"), Optional.of(utf8Lcase))), {\n          val maxA = collatedStatsCol(utf8Lcase, MAX, \"a\")\n          Some(dataSkippingPredicateWithCollation(\n            \">=\",\n            Seq(maxA, literal(\"m\")),\n            utf8Lcase,\n            Set(maxA)))\n        }),\n      // NOT over AND\n      // NOT(a < \"m\" AND a > \"t\") => (max.a >= \"m\") OR (min.a <= \"t\")\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING),\n        new Predicate(\n          \"NOT\",\n          createPredicate(\n            \"AND\",\n            createPredicate(\"<\", col(\"a\"), literal(\"m\"), Optional.of(unicode)),\n            createPredicate(\">\", col(\"a\"), literal(\"t\"), Optional.of(utf8Lcase)),\n            Optional.empty[CollationIdentifier])), {\n          val unicodeMaxA = collatedStatsCol(unicode, MAX, \"a\")\n          val utf8LcaseMinA = collatedStatsCol(utf8Lcase, MIN, \"a\")\n          Some(dataSkippingPredicate(\n            \"OR\",\n            dataSkippingPredicateWithCollation(\n              \">=\",\n              Seq(unicodeMaxA, literal(\"m\")),\n              unicode,\n              Set(unicodeMaxA)),\n            dataSkippingPredicateWithCollation(\n              \"<=\",\n              Seq(utf8LcaseMinA, literal(\"t\")),\n              utf8Lcase,\n              Set(utf8LcaseMinA))))\n        }),\n      // AND(a < \"m\" COLLATE UTF8_LCASE, b < 1)\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", IntegerType.INTEGER),\n        createPredicate(\n          \"AND\",\n          createPredicate(\"<\", col(\"a\"), literal(\"m\"), Optional.of(utf8Lcase)),\n          createPredicate(\"<\", col(\"b\"), literal(1), Optional.empty[CollationIdentifier]),\n          Optional.empty[CollationIdentifier]), {\n          val minA = collatedStatsCol(utf8Lcase, MIN, \"a\")\n          val minB = nestedCol(s\"$MIN.b\")\n          Some(dataSkippingPredicate(\n            \"AND\",\n            dataSkippingPredicateWithCollation(\"<\", Seq(minA, literal(\"m\")), utf8Lcase, Set(minA)),\n            dataSkippingPredicate(\"<\", Seq(minB, literal(1)), Set(minB))))\n        }),\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER),\n        createPredicate(\"<\", col(\"a\"), literal(1), Optional.of(utf8Lcase)), {\n          val minA = collatedStatsCol(utf8Lcase, MIN, \"a\")\n          Some(dataSkippingPredicateWithCollation(\n            \"<\",\n            Seq(minA, literal(1)),\n            utf8Lcase,\n            Set(minA)))\n        }))\n\n    testCases.foreach { case (schema, predicate, expectedDataSkippingPredicateOpt) =>\n      val dataSkippingPredicateOpt =\n        JavaOptionalOps(constructDataSkippingFilter(predicate, schema)).toScala\n      (dataSkippingPredicateOpt, expectedDataSkippingPredicateOpt) match {\n        case (Some(dataSkippingPredicate), Some(expectedDataSkippingPredicate)) =>\n          assert(dataSkippingPredicate == expectedDataSkippingPredicate)\n        case (None, None) => // pass\n        case _ =>\n          fail(s\"Expected $expectedDataSkippingPredicateOpt, found $dataSkippingPredicateOpt\")\n      }\n    }\n  }\n\n  test(\"check constructDataSkippingFilter with collations (no version in collation)\") {\n    val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\")\n    val unicodeWithoutVersion = CollationIdentifier.fromString(\"ICU.UNICODE\")\n    val unicodeWithVersion = CollationIdentifier.fromString(\"ICU.UNICODE.74.1\")\n\n    val testCases = Seq(\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", StringType.STRING),\n        createPredicate(\"<\", col(\"a\"), literal(\"m\"), Optional.of(unicodeWithoutVersion)),\n        Optional.empty[DataSkippingPredicate]),\n      (\n        new StructType()\n          .add(\"a\", StringType.STRING),\n        createPredicate(\n          \"AND\",\n          Seq[Expression](\n            createPredicate(\n              \"<\",\n              col(\"a\"),\n              literal(\"m\"),\n              Optional.of(utf8Lcase)),\n            createPredicate(\n              \">\",\n              col(\"a\"),\n              literal(\"t\"),\n              Optional.of(unicodeWithVersion))).asJava,\n          Optional.empty[CollationIdentifier]), {\n          val minAUnicode = collatedStatsCol(unicodeWithVersion, MAX, \"a\")\n          Optional.of(dataSkippingPredicateWithCollation(\n            \">\",\n            Seq(minAUnicode, literal(\"t\")),\n            unicodeWithVersion,\n            Set(minAUnicode)))\n        }))\n\n    testCases.foreach { case (schema, predicate, expectedDataSkippingPredicate) =>\n      val dataSkippingPredicateOpt = constructDataSkippingFilter(predicate, schema)\n      assert(dataSkippingPredicateOpt == expectedDataSkippingPredicate)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/skipping/StatsSchemaHelperSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.skipping\n\nimport scala.collection.JavaConverters.setAsJavaSetConverter\n\nimport io.delta.kernel.types.{ArrayType, BinaryType, BooleanType, ByteType, CollationIdentifier, DateType, DecimalType, DoubleType, FloatType, IntegerType, LongType, MapType, ShortType, StringType, StructType, TimestampNTZType, TimestampType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass StatsSchemaHelperSuite extends AnyFunSuite {\n  val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE.74\")\n  val unicode = CollationIdentifier.fromString(\"ICU.UNICODE.75.1\")\n  val utf8LcaseString = new StringType(utf8Lcase)\n  val unicodeString = new StringType(unicode)\n\n  test(\"check getStatsSchema for supported data types\") {\n    val testCases = Seq(\n      (\n        new StructType().add(\"a\", IntegerType.INTEGER),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"a\", IntegerType.INTEGER, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"a\", IntegerType.INTEGER, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"a\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"b\", StringType.STRING),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"b\", StringType.STRING, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"b\", StringType.STRING, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"b\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"c\", ByteType.BYTE),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"c\", ByteType.BYTE, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"c\", ByteType.BYTE, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"c\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"d\", ShortType.SHORT),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"d\", ShortType.SHORT, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"d\", ShortType.SHORT, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"d\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"e\", LongType.LONG),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"e\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"e\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"e\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"f\", FloatType.FLOAT),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"f\", FloatType.FLOAT, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"f\", FloatType.FLOAT, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"f\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"g\", DoubleType.DOUBLE),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"g\", DoubleType.DOUBLE, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"g\", DoubleType.DOUBLE, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"g\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"h\", DateType.DATE),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"h\", DateType.DATE, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"h\", DateType.DATE, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"h\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"i\", TimestampType.TIMESTAMP),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(\n            StatsSchemaHelper.MIN,\n            new StructType().add(\"i\", TimestampType.TIMESTAMP, true),\n            true)\n          .add(\n            StatsSchemaHelper.MAX,\n            new StructType().add(\"i\", TimestampType.TIMESTAMP, true),\n            true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"i\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"j\", TimestampNTZType.TIMESTAMP_NTZ),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(\n            StatsSchemaHelper.MIN,\n            new StructType().add(\"j\", TimestampNTZType.TIMESTAMP_NTZ, true),\n            true)\n          .add(\n            StatsSchemaHelper.MAX,\n            new StructType().add(\"j\", TimestampNTZType.TIMESTAMP_NTZ, true),\n            true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"j\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"k\", new DecimalType(20, 5)),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"k\", new DecimalType(20, 5), true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"k\", new DecimalType(20, 5), true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"k\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType().add(\"l\", utf8LcaseString),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"l\", utf8LcaseString, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"l\", utf8LcaseString, true), true)\n          .add(StatsSchemaHelper.NULL_COUNT, new StructType().add(\"l\", LongType.LONG, true), true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)))\n\n    testCases.foreach { case (dataSchema, expectedStatsSchema) =>\n      val statsSchema = StatsSchemaHelper.getStatsSchema(\n        dataSchema,\n        Set.empty[CollationIdentifier].asJava)\n      assert(statsSchema == expectedStatsSchema)\n    }\n  }\n\n  test(\"check getStatsSchema with mix of supported and unsupported data types\") {\n    val testCases = Seq(\n      (\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER)\n          .add(\"b\", BinaryType.BINARY)\n          .add(\"c\", new ArrayType(LongType.LONG, true)),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(StatsSchemaHelper.MIN, new StructType().add(\"a\", IntegerType.INTEGER, true), true)\n          .add(StatsSchemaHelper.MAX, new StructType().add(\"a\", IntegerType.INTEGER, true), true)\n          .add(\n            StatsSchemaHelper.NULL_COUNT,\n            new StructType()\n              .add(\"a\", LongType.LONG, true)\n              .add(\"b\", LongType.LONG, true)\n              .add(\"c\", LongType.LONG, true),\n            true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      (\n        new StructType()\n          .add(\n            \"s\",\n            new StructType()\n              .add(\"s1\", StringType.STRING)\n              .add(\"s2\", BooleanType.BOOLEAN)),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(\n            StatsSchemaHelper.MIN,\n            new StructType()\n              .add(\"s\", new StructType().add(\"s1\", StringType.STRING, true), true),\n            true)\n          .add(\n            StatsSchemaHelper.MAX,\n            new StructType()\n              .add(\"s\", new StructType().add(\"s1\", StringType.STRING, true), true),\n            true)\n          .add(\n            StatsSchemaHelper.NULL_COUNT,\n            new StructType()\n              .add(\n                \"s\",\n                new StructType()\n                  .add(\"s1\", LongType.LONG, true)\n                  .add(\"s2\", LongType.LONG, true),\n                true),\n            true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      // Un-nested array/map alongside a supported type\n      (\n        new StructType()\n          .add(\"arr\", new ArrayType(IntegerType.INTEGER, true))\n          .add(\"mp\", new MapType(StringType.STRING, LongType.LONG, true))\n          .add(\"z\", DoubleType.DOUBLE),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(\n            StatsSchemaHelper.MIN,\n            new StructType().add(\"z\", DoubleType.DOUBLE, true),\n            true)\n          .add(\n            StatsSchemaHelper.MAX,\n            new StructType().add(\"z\", DoubleType.DOUBLE, true),\n            true)\n          .add(\n            StatsSchemaHelper.NULL_COUNT,\n            new StructType()\n              .add(\"arr\", LongType.LONG, true)\n              .add(\"mp\", LongType.LONG, true)\n              .add(\"z\", LongType.LONG, true),\n            true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)),\n      // Nested array/map inside a struct; empty struct preserved in min/max\n      (\n        new StructType()\n          .add(\n            \"s\",\n            new StructType()\n              .add(\"arr\", new ArrayType(StringType.STRING, true))\n              .add(\"mp\", new MapType(IntegerType.INTEGER, StringType.STRING, true)))\n          .add(\"k\", StringType.STRING),\n        new StructType()\n          .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n          .add(\n            StatsSchemaHelper.MIN,\n            new StructType()\n              .add(\"s\", new StructType(), true)\n              .add(\"k\", StringType.STRING, true),\n            true)\n          .add(\n            StatsSchemaHelper.MAX,\n            new StructType()\n              .add(\"s\", new StructType(), true)\n              .add(\"k\", StringType.STRING, true),\n            true)\n          .add(\n            StatsSchemaHelper.NULL_COUNT,\n            new StructType()\n              .add(\n                \"s\",\n                new StructType()\n                  .add(\"arr\", LongType.LONG, true)\n                  .add(\"mp\", LongType.LONG, true),\n                true)\n              .add(\"k\", LongType.LONG, true),\n            true)\n          .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)))\n\n    testCases.foreach { case (dataSchema, expectedStatsSchema) =>\n      val statsSchema = StatsSchemaHelper.getStatsSchema(\n        dataSchema,\n        Set.empty[CollationIdentifier].asJava)\n      assert(\n        statsSchema == expectedStatsSchema,\n        s\"Stats schema mismatch for data schema: $dataSchema\")\n    }\n  }\n\n  test(\"check getStatsSchema with collations - un-nested mix\") {\n    val dataSchema = new StructType()\n      .add(\"a\", StringType.STRING)\n      .add(\"b\", IntegerType.INTEGER)\n      .add(\"c\", BinaryType.BINARY)\n      .add(\"d\", unicodeString)\n\n    val skippableFields = new StructType()\n      .add(\"a\", StringType.STRING)\n      .add(\"b\", IntegerType.INTEGER)\n      .add(\"d\", unicodeString)\n\n    val collations = Set(utf8Lcase, unicode, CollationIdentifier.SPARK_UTF8_BINARY)\n\n    val expectedCollatedMinMax = new StructType()\n      .add(\"a\", StringType.STRING, true).add(\"d\", unicodeString, true)\n\n    val expectedStatsSchema = new StructType()\n      .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n      .add(\n        StatsSchemaHelper.MIN,\n        skippableFields,\n        true)\n      .add(\n        StatsSchemaHelper.MAX,\n        skippableFields,\n        true)\n      .add(\n        StatsSchemaHelper.NULL_COUNT,\n        new StructType()\n          .add(\"a\", LongType.LONG, true)\n          .add(\"b\", LongType.LONG, true)\n          .add(\"c\", LongType.LONG, true)\n          .add(\"d\", LongType.LONG, true),\n        true)\n      .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)\n      .add(\n        StatsSchemaHelper.STATS_WITH_COLLATION,\n        new StructType()\n          .add(\n            utf8Lcase.toString,\n            new StructType()\n              .add(StatsSchemaHelper.MIN, expectedCollatedMinMax, true)\n              .add(StatsSchemaHelper.MAX, expectedCollatedMinMax, true),\n            true)\n          .add(\n            unicode.toString,\n            new StructType()\n              .add(StatsSchemaHelper.MIN, expectedCollatedMinMax, true)\n              .add(StatsSchemaHelper.MAX, expectedCollatedMinMax, true),\n            true),\n        true)\n\n    val statsSchema = StatsSchemaHelper.getStatsSchema(dataSchema, collations.asJava)\n    assert(statsSchema == expectedStatsSchema)\n  }\n\n  test(\"check getStatsSchema with collations - nested mix and multiple collations\") {\n    val dataSchema = new StructType()\n      .add(\n        \"s\",\n        new StructType()\n          .add(\"x\", StringType.STRING)\n          .add(\"y\", IntegerType.INTEGER)\n          .add(\"z\", new StructType().add(\"p\", StringType.STRING).add(\"q\", DoubleType.DOUBLE)))\n      .add(\"arr\", new ArrayType(StringType.STRING, true))\n      .add(\"mp\", new MapType(StringType.STRING, StringType.STRING, true))\n\n    val skippableFields = new StructType()\n      .add(\n        \"s\",\n        new StructType()\n          .add(\"x\", StringType.STRING)\n          .add(\"y\", IntegerType.INTEGER)\n          .add(\"z\", new StructType().add(\"p\", StringType.STRING).add(\"q\", DoubleType.DOUBLE)))\n\n    val collations = Set(utf8Lcase, CollationIdentifier.SPARK_UTF8_BINARY)\n\n    val expectedCollatedNested = new StructType()\n      .add(\n        \"s\",\n        new StructType()\n          .add(\"x\", StringType.STRING, true)\n          .add(\"z\", new StructType().add(\"p\", StringType.STRING, true), true),\n        true)\n\n    val expectedStatsSchema = new StructType()\n      .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n      .add(\n        StatsSchemaHelper.MIN,\n        skippableFields,\n        true)\n      .add(\n        StatsSchemaHelper.MAX,\n        skippableFields,\n        true)\n      .add(\n        StatsSchemaHelper.NULL_COUNT,\n        new StructType()\n          .add(\n            \"s\",\n            new StructType()\n              .add(\"x\", LongType.LONG, true)\n              .add(\"y\", LongType.LONG, true)\n              .add(\n                \"z\",\n                new StructType().add(\"p\", LongType.LONG, true).add(\"q\", LongType.LONG, true),\n                true),\n            true)\n          .add(\"arr\", LongType.LONG, true)\n          .add(\"mp\", LongType.LONG, true),\n        true)\n      .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)\n      .add(\n        StatsSchemaHelper.STATS_WITH_COLLATION,\n        new StructType()\n          .add(\n            utf8Lcase.toString,\n            new StructType()\n              .add(StatsSchemaHelper.MIN, expectedCollatedNested, true)\n              .add(StatsSchemaHelper.MAX, expectedCollatedNested, true),\n            true),\n        true)\n\n    val statsSchema = StatsSchemaHelper.getStatsSchema(dataSchema, collations.asJava)\n    assert(statsSchema == expectedStatsSchema)\n  }\n\n  test(\"check getStatsSchema with collations - no eligible string columns\") {\n    val dataSchema = new StructType()\n      .add(\"a\", IntegerType.INTEGER)\n      .add(\"b\", new ArrayType(StringType.STRING, true))\n      .add(\"c\", new MapType(StringType.STRING, LongType.LONG, true))\n\n    val a = new StructType().add(\"a\", IntegerType.INTEGER, true)\n\n    val collations = Set(utf8Lcase, unicode, CollationIdentifier.SPARK_UTF8_BINARY)\n\n    val expectedStatsSchema = new StructType()\n      .add(StatsSchemaHelper.NUM_RECORDS, LongType.LONG, true)\n      .add(\n        StatsSchemaHelper.MIN,\n        a,\n        true)\n      .add(\n        StatsSchemaHelper.MAX,\n        a,\n        true)\n      .add(\n        StatsSchemaHelper.NULL_COUNT,\n        new StructType()\n          .add(\"a\", LongType.LONG, true)\n          .add(\"b\", LongType.LONG, true)\n          .add(\"c\", LongType.LONG, true),\n        true)\n      .add(StatsSchemaHelper.TIGHT_BOUNDS, BooleanType.BOOLEAN, true)\n\n    val statsSchema = StatsSchemaHelper.getStatsSchema(dataSchema, collations.asJava)\n    assert(statsSchema == expectedStatsSchema)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/snapshot/LogSegmentSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.snapshot\n\nimport java.lang.{Long => JLong}\nimport java.util.{Collections, List => JList, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.internal.files.{ParsedCatalogCommitData, ParsedDeltaData}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.test.{MockFileSystemClientUtils, VectorTestUtils}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LogSegmentSuite extends AnyFunSuite with MockFileSystemClientUtils with VectorTestUtils {\n  private val checkpointFs10List = singularCheckpointFileStatuses(Seq(10)).toList.asJava\n  private val checksumAtVersion10 = checksumFileStatus(10)\n  private val deltaFs11List = deltaFileStatuses(Seq(11)).toList.asJava\n  private val deltaFs12List = deltaFileStatuses(Seq(12)).toList.asJava\n  private val deltasFs11To12List = deltaFileStatuses(Seq(11, 12)).toList.asJava\n  private val parsedRatifiedCommits11To12List =\n    Seq(11, 12).map(v => ParsedDeltaData.forFileStatus(stagedCommitFile(v))).asJava\n  private val compactionFs11To12List = compactedFileStatuses(Seq((11, 12))).toList.asJava\n  private val badJsonsList = Collections.singletonList(\n    FileStatus.of(s\"${logPath.toString}/gibberish.json\", 1, 1))\n  private val badCheckpointsList = Collections.singletonList(\n    FileStatus.of(s\"${logPath.toString}/gibberish.checkpoint.parquet\", 1, 1))\n  private val logPath2 = new Path(\"/another/fake/path/to/table/\", \"_delta_log\")\n\n  private def createLogSegmentForTest(\n      logPath: Path = this.logPath,\n      version: Long,\n      deltas: JList[FileStatus] = Collections.emptyList(),\n      compactions: JList[FileStatus] = Collections.emptyList(),\n      checkpoints: JList[FileStatus] = Collections.emptyList(),\n      deltaAtEndVersion: Option[FileStatus] = None,\n      lastSeenChecksum: Optional[FileStatus] = Optional.empty(),\n      maxPublishedDeltaVersion: Optional[JLong] = Optional.empty()): LogSegment = {\n    val finalDeltaAtEndVersion = deltaAtEndVersion.getOrElse {\n      if (!deltas.isEmpty()) {\n        // If we have deltas, use the last delta\n        deltas.get(deltas.size() - 1)\n      } else if (!checkpoints.isEmpty()) {\n        // If we only have checkpoints, create a delta file for the checkpoint version\n        val checkpointVersion = io.delta.kernel.internal.util.FileNames.checkpointVersion(\n          new Path(checkpoints.get(0).getPath()))\n        deltaFileStatus(checkpointVersion)\n      } else {\n        // If neither deltas nor checkpoints are provided, create a delta for the target version\n        deltaFileStatus(version)\n      }\n    }\n\n    new LogSegment(\n      logPath,\n      version,\n      deltas,\n      compactions,\n      checkpoints,\n      finalDeltaAtEndVersion,\n      lastSeenChecksum,\n      maxPublishedDeltaVersion)\n  }\n\n  test(\"constructor -- valid case (non-empty)\") {\n    createLogSegmentForTest(\n      version = 12,\n      deltas = deltasFs11To12List,\n      compactions = compactionFs11To12List,\n      checkpoints = checkpointFs10List)\n  }\n\n  test(\"constructor -- null arguments => throw\") {\n    // logPath is null\n    intercept[NullPointerException] {\n      createLogSegmentForTest(\n        logPath = null,\n        version = 1)\n    }\n    // deltas is null\n    intercept[NullPointerException] {\n      createLogSegmentForTest(\n        version = 1,\n        deltas = null)\n    }\n    // compactions is null\n    intercept[NullPointerException] {\n      createLogSegmentForTest(\n        version = 1,\n        compactions = null)\n    }\n    // checkpoints is null\n    intercept[NullPointerException] {\n      createLogSegmentForTest(\n        version = 1,\n        checkpoints = null)\n    }\n    // deltaAtEndVersion is null\n    intercept[NullPointerException] {\n      createLogSegmentForTest(\n        version = 1,\n        deltas = Collections.singletonList(deltaFileStatus(1)),\n        deltaAtEndVersion = null)\n    }\n    // lastSeenChecksum is null\n    intercept[NullPointerException] {\n      createLogSegmentForTest(\n        version = 1,\n        deltas = Collections.singletonList(deltaFileStatus(1)),\n        lastSeenChecksum = null)\n    }\n  }\n\n  test(\"constructor -- version must be >= 0\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = -1,\n        deltaAtEndVersion = Some(deltaFileStatus(0))\n      ) // dummy value\n    }.getMessage\n    assert(exMsg === \"version must be >= 0\")\n  }\n\n  test(\"constructor -- all deltas must be actual delta files\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = badJsonsList,\n        checkpoints = checkpointFs10List)\n    }.getMessage\n    assert(exMsg.startsWith(\"deltas must all be actual delta (commit) files\"))\n  }\n\n  test(\"constructor -- all checkpoints must be actual checkpoint files\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltasFs11To12List,\n        checkpoints = badCheckpointsList)\n    }.getMessage\n    assert(exMsg.startsWith(\"checkpoints must all be actual checkpoint files\"))\n  }\n\n  test(\"constructor -- deltas and checkpoints cannot be empty\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(version = 12)\n    }.getMessage\n    assert(exMsg === \"No files to read\")\n  }\n\n  test(\"constructor -- checksum version must be <= LogSegment version\") {\n    val checksumAtVersion13 = checksumFileStatus(13)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12, // LogSegment version is 12\n        deltas = deltasFs11To12List,\n        checkpoints = checkpointFs10List,\n        lastSeenChecksum = Optional.of(checksumAtVersion13)\n      ) // Checksum version is 13\n    }.getMessage\n\n    assert(exMsg.contains(\n      \"checksum version (13) should be less than or equal to LogSegment version (12)\"))\n  }\n\n  test(\"constructor -- deltaAtEndVersion must match version (checkpoint only)\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 10,\n        checkpoints = checkpointFs10List,\n        deltaAtEndVersion = Some(deltaFileStatus(9)) // Wrong version - should be 10\n      )\n    }.getMessage\n    assert(exMsg.contains(\n      \"deltaAtEndVersion (9) must be equal to LogSegment version (10)\"))\n  }\n\n  test(\"constructor -- deltaAtEndVersion must match version (checkpoint + deltas)\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltasFs11To12List,\n        checkpoints = checkpointFs10List,\n        deltaAtEndVersion = Some(deltaFileStatus(11)) // Wrong version - should be 12\n      )\n    }.getMessage\n    assert(exMsg.contains(\n      \"deltaAtEndVersion (11) must be equal to LogSegment version (12)\"))\n  }\n\n  test(\"constructor -- checksum version must be >= checkpoint version\") {\n    val checksumAtVersion9 = checksumFileStatus(9)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltasFs11To12List,\n        checkpoints = checkpointFs10List, // Checkpoint version is 10\n        lastSeenChecksum = Optional.of(checksumAtVersion9)\n      ) // Checksum version is 9\n    }.getMessage\n\n    assert(exMsg.contains(\n      \"checksum version (9) should be greater than or equal to checkpoint version (10)\"))\n  }\n\n  test(\"constructor -- if deltas non-empty then first delta must equal checkpointVersion + 1\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltaFs12List,\n        checkpoints = checkpointFs10List)\n    }.getMessage\n    assert(exMsg.contains(\n      \"First delta file version (12) must equal checkpointVersion + 1 (11)\"))\n  }\n\n  test(\"constructor -- if deltas non-empty then last delta must equal version\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltaFs11List,\n        checkpoints = checkpointFs10List)\n    }.getMessage\n    assert(exMsg.contains(\n      \"Last delta file version (11) must equal LogSegment version (12)\"))\n  }\n\n  test(\"constructor -- if no deltas then checkpointVersion must equal version\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 11,\n        checkpoints = checkpointFs10List)\n    }.getMessage\n    assert(exMsg.contains(\n      \"If no deltas, then checkpointVersion (10) must equal LogSegment version (11)\"))\n  }\n\n  test(\"constructor -- deltas not contiguous\") {\n    val deltas = deltaFileStatuses(Seq(11, 13)).toList.asJava\n    val exMsg = intercept[IllegalArgumentException] {\n      createLogSegmentForTest(\n        version = 13,\n        deltas = deltas,\n        checkpoints = checkpointFs10List)\n    }.getMessage\n    assert(exMsg === \"Delta versions must be contiguous: [11, 13]\")\n  }\n\n  test(\"constructor -- delta commit files (JSON) outside of log path\") {\n    val deltasForDifferentTable =\n      deltaFileStatuses(Seq(11, 12), logPath2).toList.asJava\n    val ex = intercept[RuntimeException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltasForDifferentTable,\n        checkpoints = checkpointFs10List)\n    }\n    assert(ex.getMessage.contains(\"doesn't belong in the transaction log\"))\n  }\n\n  test(\"constructor -- compaction log files outside of log path\") {\n    val compactionsForDifferentTable =\n      compactedFileStatuses(Seq((11, 12)), logPath2).toList.asJava\n    val ex = intercept[RuntimeException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltasFs11To12List,\n        compactions = compactionsForDifferentTable,\n        checkpoints = checkpointFs10List)\n    }\n    assert(ex.getMessage.contains(\"doesn't belong in the transaction log\"))\n  }\n\n  test(\"constructor -- checkpoint files (parquet) outside of log path\") {\n    val checkpointsForDifferentTable =\n      singularCheckpointFileStatuses(Seq(10), logPath2).toList.asJava\n    val ex = intercept[RuntimeException] {\n      createLogSegmentForTest(\n        version = 12,\n        deltas = deltasFs11To12List,\n        checkpoints = checkpointsForDifferentTable)\n    }\n    assert(ex.getMessage.contains(\"doesn't belong in the transaction log\"))\n  }\n\n  test(\"toString\") {\n    val logSegment = createLogSegmentForTest(\n      version = 12,\n      deltas = deltasFs11To12List,\n      checkpoints = checkpointFs10List,\n      lastSeenChecksum = Optional.of(checksumAtVersion10),\n      maxPublishedDeltaVersion = Optional.of(12L))\n    // scalastyle:off line.size.limit\n    val expectedToString =\n      \"\"\"LogSegment {\n        |  logPath='/fake/path/to/table/_delta_log',\n        |  version=12,\n        |  deltas=[\n        |    FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000011.json', size=11, modificationTime=110},\n        |    FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000012.json', size=12, modificationTime=120}\n        |  ],\n        |  checkpoints=[\n        |    FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000010.checkpoint.parquet', size=10, modificationTime=100}\n        |  ],\n        |  deltaAtEndVersion=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000012.json', size=12, modificationTime=120},\n        |  lastSeenChecksum=FileStatus{path='/fake/path/to/table/_delta_log/00000000000000000010.crc', size=10, modificationTime=10},\n        |  checkpointVersion=10,\n        |  maxPublishedDeltaVersion=12\n        |}\"\"\".stripMargin\n    // scalastyle:on line.size.limit\n    assert(logSegment.toString === expectedToString)\n  }\n\n  private def parseExpectedString(expected: String): JList[FileStatus] = {\n    expected.split(\",\").map(_.trim).map { item =>\n      if (item.contains(\"-\")) {\n        // compaction file contains a -\n        val parts = item.split(\"-\").map(_.trim.toLong)\n        logCompactionStatus(parts(0), parts(1))\n      } else {\n        // delta file does not\n        deltaFileStatus(item.toLong)\n      }\n    }.toList.asJava\n  }\n\n  private def testCompactionCase(\n      deltas: Seq[Long],\n      compactions: Seq[(Long, Long)],\n      expected: String): Unit = {\n    val version = deltas.max\n    val deltas_list = deltaFileStatuses(deltas).toList.asJava\n    val compactions_list = compactedFileStatuses(compactions).toList.asJava\n    val segment = createLogSegmentForTest(\n      version = version,\n      deltas = deltas_list,\n      compactions = compactions_list)\n    val expectedFiles = parseExpectedString(expected)\n    assert(segment.allFilesWithCompactionsReversed() === expectedFiles)\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5 in middle\") {\n    testCompactionCase(\n      Seq.range(0, 7),\n      Seq((3, 5)),\n      \"6, 3-5, 2, 1, 0\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5 at start\") {\n    testCompactionCase(\n      Seq.range(3, 8),\n      Seq((3, 5)),\n      \"7, 6, 3-5\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5 at end\") {\n    testCompactionCase(\n      Seq.range(0, 6),\n      Seq((3, 5)),\n      \"3-5, 2, 1, 0\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5 at second to last\") {\n    testCompactionCase(\n      Seq.range(2, 7),\n      Seq((3, 5)),\n      \"6, 3-5, 2\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5, and 7 - 9\") {\n    testCompactionCase(\n      Seq.range(1, 11),\n      Seq((3, 5), (7, 9)),\n      \"10, 7-9, 6, 3-5, 2, 1\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5, and 4 - 8 (overlap)\") {\n    testCompactionCase(\n      Seq.range(2, 11),\n      Seq((3, 5), (4, 8)),\n      \"10, 9, 4-8, 3, 2\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- 3 - 5, whole range\") {\n    testCompactionCase(\n      Seq.range(3, 6),\n      Seq((3, 5)),\n      \"3-5\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- consecutive compactions\") {\n    testCompactionCase(\n      Seq.range(0, 13),\n      Seq((3, 5), (6, 8), (9, 11)),\n      \"12, 9-11, 6-8, 3-5, 2, 1, 0\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- contained range\") {\n    testCompactionCase(\n      Seq.range(1, 12),\n      Seq((2, 10), (4, 8)),\n      \"11, 2-10, 1\")\n  }\n\n  test(\"allFilesWithCompactionsReversed -- complex ranges\") {\n    testCompactionCase(\n      Seq.range(0, 21),\n      Seq((1, 3), (1, 5), (7, 10), (11, 14), (11, 12), (16, 20), (18, 20)),\n      \"16-20, 15, 11-14, 7-10, 6, 1-5, 0\")\n  }\n\n  test(\"assertLogFilesBelongToTable should pass for correct log paths\") {\n    val tablePath = new Path(\"s3://bucket/logPath\")\n    val logFiles = List(\n      FileStatus.of(\"s3://bucket/logPath/deltafile1\", 0L, 0L),\n      FileStatus.of(\"s3://bucket/logPath/deltafile2\", 0L, 0L),\n      FileStatus.of(\"s3://bucket/logPath/checkpointfile1\", 0L, 0L),\n      FileStatus.of(\"s3://bucket/logPath/checkpointfile2\", 0L, 0L)).asJava\n\n    LogSegment.assertLogFilesBelongToTable(tablePath, logFiles)\n  }\n\n  test(\"assertLogFilesBelongToTable should fail for incorrect log paths\") {\n    val tablePath = new Path(\"s3://bucket/logPath\")\n    val logFiles = List(\n      FileStatus.of(\"s3://bucket/logPath/deltafile1\", 0L, 0L),\n      FileStatus.of(\"s3://bucket/invalidLogPath/deltafile2\", 0L, 0L),\n      FileStatus.of(\"s3://bucket/logPath/checkpointfile1\", 0L, 0L),\n      FileStatus.of(\"s3://bucket/invalidLogPath/checkpointfile2\", 0L, 0L)).asJava\n\n    // Test that files with incorrect log paths trigger the assertion\n    val ex = intercept[RuntimeException] {\n      LogSegment.assertLogFilesBelongToTable(tablePath, logFiles)\n    }\n    assert(ex.getMessage.contains(\"File (s3://bucket/invalidLogPath/deltafile2) \" +\n      s\"doesn't belong in the transaction log at $tablePath\"))\n  }\n\n  ////////////////////////////////////\n  // copyWithAdditionalDeltas tests //\n  ////////////////////////////////\n\n  test(\"copyWithAdditionalDeltas: single additional delta\") {\n    val baseSegment = createLogSegmentForTest(\n      version = 10,\n      checkpoints = checkpointFs10List)\n\n    val updated = baseSegment.newWithAddedDeltas(parsedRatifiedCommits11To12List.subList(0, 1))\n\n    assert(updated.getVersion === 11)\n    assert(updated.getDeltas.size() === 1)\n  }\n\n  test(\"copyWithAdditionalDeltas: multiple additional deltas\") {\n    val baseSegment = createLogSegmentForTest(\n      version = 10,\n      checkpoints = checkpointFs10List)\n\n    val updated = baseSegment.newWithAddedDeltas(parsedRatifiedCommits11To12List)\n\n    assert(updated.getVersion === 12)\n    assert(updated.getDeltas.size() === 2)\n  }\n\n  test(\"copyWithAdditionalDeltas: empty list returns same segment\") {\n    val baseSegment = createLogSegmentForTest(\n      version = 10,\n      checkpoints = checkpointFs10List)\n\n    val updated = baseSegment.newWithAddedDeltas(Collections.emptyList())\n    assert(updated eq baseSegment)\n  }\n\n  test(\"copyWithAdditionalDeltas: first delta must be version + 1\") {\n    val baseSegment = createLogSegmentForTest(\n      version = 10,\n      checkpoints = checkpointFs10List)\n\n    val wrongVersionDeltas = List(ParsedDeltaData.forFileStatus(stagedCommitFile(12))).asJava\n    val exMsg = intercept[IllegalArgumentException] {\n      baseSegment.newWithAddedDeltas(wrongVersionDeltas)\n    }.getMessage\n    assert(exMsg.contains(\"Expected 11 but got 12\"))\n  }\n\n  test(\"copyWithAdditionalDeltas: deltas must be contiguous\") {\n    val baseSegment = createLogSegmentForTest(\n      version = 10,\n      checkpoints = checkpointFs10List)\n\n    val nonContiguousDeltas = List(\n      ParsedDeltaData.forFileStatus(stagedCommitFile(11)),\n      ParsedDeltaData.forFileStatus(stagedCommitFile(13))).asJava\n    val exMsg = intercept[IllegalArgumentException] {\n      baseSegment.newWithAddedDeltas(nonContiguousDeltas)\n    }.getMessage\n    assert(exMsg.contains(\"Delta versions must be contiguous. Expected 12 but got 13\"))\n  }\n\n  test(\"copyWithAdditionalDeltas: inline delta fails\") {\n    val baseSegment = createLogSegmentForTest(\n      version = 10,\n      checkpoints = checkpointFs10List)\n\n    val inlineDelta = ParsedCatalogCommitData.forInlineData(11, emptyColumnarBatch)\n    val inlineDeltas = List[ParsedDeltaData](inlineDelta).asJava\n\n    val exMsg = intercept[IllegalArgumentException] {\n      baseSegment.newWithAddedDeltas(inlineDeltas)\n    }.getMessage\n    assert(exMsg.contains(\"Currently, only file-based deltas are supported\"))\n  }\n\n  ///////////////////////////\n  // fromSingleDelta tests //\n  ///////////////////////////\n\n  test(\"fromSingleDelta -- creates valid LogSegment\") {\n    val deltaData = ParsedDeltaData.forFileStatus(deltaFileStatus(0))\n    val logSegment = LogSegment.createForNewTable(logPath, deltaData)\n\n    assert(logSegment.getVersion === 0)\n    assert(logSegment.getDeltas.size() === 1)\n    assert(logSegment.getCheckpoints.isEmpty)\n    assert(logSegment.getCompactions.isEmpty)\n    assert(logSegment.getLastSeenChecksum === Optional.empty())\n  }\n\n  test(\"fromSingleDelta -- non-zero version fails\") {\n    val deltaData = ParsedDeltaData.forFileStatus(deltaFileStatus(1))\n    val exMsg = intercept[IllegalArgumentException] {\n      LogSegment.createForNewTable(logPath, deltaData)\n    }.getMessage\n    assert(exMsg.contains(\"Version must be 0 for a LogSegment with only a single delta\"))\n  }\n\n  test(\"fromSingleDelta -- inline delta fails\") {\n    val inlineDelta = ParsedCatalogCommitData.forInlineData(0, emptyColumnarBatch)\n    val exMsg = intercept[IllegalArgumentException] {\n      LogSegment.createForNewTable(logPath, inlineDelta)\n    }.getMessage\n    assert(exMsg.contains(\"Currently, only file-based deltas are supported\"))\n  }\n\n  ////////////////////////////////\n  // newAsPublished tests       //\n  ////////////////////////////////\n\n  test(\"newAsPublished: list all files when there's no checkpoint\") {\n    val commits = (0 to 10).map(i => FileStatus.of(s\"$logPath/$i.json\")) ++\n      (11 to 20).map(i => FileStatus.of(s\"$logPath/$i.${java.util.UUID.randomUUID.toString}.json\"))\n    val baseSegment = createLogSegmentForTest(\n      version = 20,\n      deltas = commits.asJava,\n      deltaAtEndVersion = Some(commits.last),\n      maxPublishedDeltaVersion = Optional.of(10))\n\n    val updated = baseSegment.newAsPublished()\n\n    assert(updated.getVersion === 20)\n    assert(updated.getDeltas.size() === 21)\n    updated.getDeltas.asScala.zipWithIndex.foreach { case (fs, i) =>\n      assert(fs.getPath == FileNames.deltaFile(logPath, i))\n    }\n    assert(updated.getDeltaFileAtEndVersion.getPath == FileNames.deltaFile(logPath, 20))\n    assert(updated.getMaxPublishedDeltaVersion == Optional.of(20L))\n  }\n\n  test(\"newAsPublished: list all files starting from checkpoint\") {\n    val commits = (11 until 15).map(i => FileStatus.of(s\"$logPath/$i.json\")) ++\n      (15 to 20).map(i => FileStatus.of(s\"$logPath/$i.${java.util.UUID.randomUUID.toString}.json\"))\n    val baseSegment = createLogSegmentForTest(\n      version = 20,\n      deltas = commits.asJava,\n      deltaAtEndVersion = Some(commits.last),\n      checkpoints = checkpointFs10List,\n      maxPublishedDeltaVersion = Optional.of(15))\n\n    val updated = baseSegment.newAsPublished()\n\n    assert(updated.getVersion === 20)\n    assert(updated.getDeltas.size() === 10)\n    updated.getDeltas.asScala.zipWithIndex.foreach { case (fs, i) =>\n      assert(fs.getPath == FileNames.deltaFile(logPath, i + 11))\n    }\n    assert(updated.getDeltaFileAtEndVersion.getPath == FileNames.deltaFile(logPath, 20))\n    assert(updated.getMaxPublishedDeltaVersion == Optional.of(20L))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/snapshot/MetadataCleanupSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.snapshot\n\nimport io.delta.kernel.internal.snapshot.MetadataCleanup.cleanupExpiredLogs\nimport io.delta.kernel.internal.util.ManualClock\nimport io.delta.kernel.test.{MockFileSystemClientUtils, MockListFromDeleteFileSystemClient}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for the metadata cleanup logic in the Delta log directory. It mocks the\n * `FileSystemClient` to test the cleanup logic for various combinations of delta files and\n * checkpoint files. Utility methods in `MockFileSystemClientUtils` are used to generate the\n * log file statuses which usually have modification time as the `version * 10`.\n */\nclass MetadataCleanupSuite extends AnyFunSuite with MockFileSystemClientUtils {\n\n  import MetadataCleanupSuite._\n\n  /* ------------------- TESTS ------------------ */\n\n  // Simple case where the Delta log directory contains only delta files and no checkpoint files\n  Seq(\n    (\n      \"no files should be deleted even some of them are expired\",\n      DeletedFileList(), // expected deleted files - none of them should be deleted\n      70, // current time\n      30 // retention period\n    ),\n    (\n      \"no files should be deleted as none of them are expired\",\n      DeletedFileList(), // expected deleted files - none of them should be deleted\n      200, // current time\n      200 // retention period\n    ),\n    (\n      \"no files should be deleted as none of them are expired\",\n      DeletedFileList(), // expected deleted files - none of them should be deleted\n      200, // current time\n      0 // retention period\n    )).foreach {\n    case (testName, expectedDeletedFiles, currentTime, retentionPeriod) =>\n      // _deltalog directory contents - contains only delta files\n      val logFiles = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5, 6))\n      test(s\"metadataCleanup: $testName: $currentTime, $retentionPeriod\") {\n        cleanupAndVerify(logFiles, expectedDeletedFiles.fileList(), currentTime, retentionPeriod)\n      }\n  }\n\n  // with various checkpoint types\n  Seq(\"classic\", \"multi-part\", \"v2\", \"hybrid\").foreach { checkpointType =>\n    // _deltalog directory contains a combination of delta files and checkpoint files\n\n    val logFiles = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) ++\n      (checkpointType match {\n        case \"classic\" =>\n          singularCheckpointFileStatuses(Seq(3, 6, 9, 12))\n        case \"multi-part\" =>\n          multiCheckpointFileStatuses(Seq(3, 6, 9, 12), multiPartCheckpointPartsSize)\n        case \"v2\" =>\n          v2CPFileStatuses(Seq[Long](3, 6, 9, 12))\n        case \"hybrid\" =>\n          singularCheckpointFileStatuses(Seq(3)) ++\n            multiCheckpointFileStatuses(Seq(6), numParts = multiPartCheckpointPartsSize) ++\n            v2CPFileStatuses(Seq[Long](9)) ++\n            singularCheckpointFileStatuses(Seq(12))\n      })\n\n    // test cases\n    Seq(\n      (\n        \"delete expired delta files up to the checkpoint version, \" +\n          \"not all expired delta files are deleted\",\n        Seq(0L, 1L, 2L), // expDeletedDeltaVersions,\n        Seq(), // expDeletedCheckpointVersions,\n        130, // current time\n        80 // retention period\n      ),\n      (\n        \"expired delta files + expired checkpoint should be deleted\",\n        Seq(0L, 1L, 2L, 3L, 4L, 5L), // expDeletedDeltaVersions,\n        Seq(3L), // expDeletedCheckpointVersions,\n        130, // current time\n        60 // retention period\n      ),\n      (\n        \"expired delta files + expired checkpoints should be deleted\",\n        Seq(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), // expDeletedDeltaVersions,\n        Seq(3L, 6L), // expDeletedCheckpointVersions,\n        130, // current time\n        40 // retention period\n      ),\n      (\n        \"all delta/checkpoint files should be except the last checkpoint file\",\n        Seq(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L), // expDeletedDeltaVersions,\n        Seq(3L, 6L, 9L), // expDeletedCheckpointVersions,\n        130, // current time\n        0 // retention period\n      ),\n      (\n        \"no delta/checkpoint files should be deleted as none expired\",\n        Seq(), // expDeletedDeltaVersions\n        Seq(), // expDeletedDeltaVersions\n        200, // current time\n        200 // retention period\n      )).foreach {\n      case (\n            testName,\n            expDeletedDeltaVersions,\n            expDeletedCheckpointVersions,\n            currentTime,\n            retentionPeriod) =>\n\n        val expectedDeletedFiles = DeletedFileList(\n          deltaVersions = expDeletedDeltaVersions,\n          classicCheckpointVersions = checkpointType match {\n            case \"classic\" => expDeletedCheckpointVersions\n            case \"hybrid\" => expDeletedCheckpointVersions.filter(Seq(3, 12).contains(_))\n            case _ => Seq.empty\n          },\n          multipartCheckpointVersions = checkpointType match {\n            case \"multi-part\" => expDeletedCheckpointVersions\n            case \"hybrid\" => expDeletedCheckpointVersions.filter(_ == 6)\n            case _ => Seq.empty\n          },\n          v2CheckpointVersions = checkpointType match {\n            case \"v2\" => expDeletedCheckpointVersions\n            case \"hybrid\" => expDeletedCheckpointVersions.filter(_ == 9)\n            case _ => Seq.empty\n          })\n\n        test(s\"metadataCleanup: $checkpointType: $testName: $currentTime, $retentionPeriod\") {\n          cleanupAndVerify(logFiles, expectedDeletedFiles.fileList(), currentTime, retentionPeriod)\n        }\n    }\n  }\n\n  test(\"first log entry is a checkpoint\") {\n    val logFiles = multiCheckpointFileStatuses(Seq(25), multiPartCheckpointPartsSize) ++\n      singularCheckpointFileStatuses(Seq(29)) ++\n      deltaFileStatuses(Seq(25, 26, 27, 28, 29, 30, 31, 32))\n\n    Seq(\n      (\n        330, // current time\n        50, // retention period\n        DeletedFileList() // expected deleted files - none of them should be deleted\n      ),\n      (\n        330, // current time\n        30, // retention period\n        DeletedFileList(\n          deltaVersions = Seq(25, 26, 27, 28),\n          multipartCheckpointVersions = Seq(25))),\n      (\n        330, // current time\n        10, // retention period\n        DeletedFileList(\n          deltaVersions = Seq(25, 26, 27, 28),\n          multipartCheckpointVersions = Seq(25)))).foreach {\n      case (currentTime, retentionPeriod, expectedDeletedFiles) =>\n        cleanupAndVerify(logFiles, expectedDeletedFiles.fileList(), currentTime, retentionPeriod)\n    }\n  }\n\n  /* ------------------- NEGATIVE TESTS ------------------ */\n  test(\"metadataCleanup: invalid retention period\") {\n    val e = intercept[IllegalArgumentException] {\n      cleanupExpiredLogs(\n        mockEngine(mockFsClient(Seq.empty)),\n        new ManualClock(100),\n        logPath,\n        -1 /* retentionPeriod */\n      )\n    }\n\n    assert(e.getMessage.contains(\"Retention period must be non-negative\"))\n  }\n\n  test(\"incomplete checkpoints should not be considered\") {\n    val logFiles = deltaFileStatuses(Seq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) ++\n      multiCheckpointFileStatuses(Seq(3), multiPartCheckpointPartsSize)\n        // delete the third part of the checkpoint\n        .filterNot(_.getPath.contains(s\"%010d.%010d\".format(2, 4))) ++\n      multiCheckpointFileStatuses(Seq(6), multiPartCheckpointPartsSize) ++\n      v2CPFileStatuses(Seq(9))\n\n    // test cases\n    Seq(\n      (\n        Seq[Long](), // expDeletedDeltaVersions,\n        Seq[Long](), // expDeletedCheckpointVersions,\n        130, // current time\n        80 // retention period\n      ),\n      (\n        Seq(0L, 1L, 2L, 3L, 4L, 5L), // expDeletedDeltaVersions,\n        Seq(3L), // expDeletedCheckpointVersions,\n        130, // current time\n        60 // retention period\n      ),\n      (\n        Seq(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), // expDeletedDeltaVersions,\n        Seq(3L, 6L), // expDeletedCheckpointVersions,\n        130, // current time\n        20 // retention period\n      )).foreach {\n      case (expDeletedDeltaVersions, expDeletedCheckpointVersions, currentTime, retentionPeriod) =>\n\n        val expectedDeletedFiles = (deltaFileStatuses(expDeletedDeltaVersions) ++\n          expDeletedCheckpointVersions.flatMap {\n            case v @ 3 => multiCheckpointFileStatuses(Seq(v), multiPartCheckpointPartsSize)\n                .filterNot(_.getPath.contains(s\"%010d.%010d\".format(2, 4)))\n            case v @ 6 => multiCheckpointFileStatuses(Seq(v), multiPartCheckpointPartsSize)\n            case v @ 9 => v2CPFileStatuses(Seq(v))\n          }).map(_.getPath)\n\n        cleanupAndVerify(logFiles, expectedDeletedFiles, currentTime, retentionPeriod)\n    }\n  }\n\n  /* ------------------- HELPER UTILITIES/CONSTANTS ------------------ */\n  /**\n   * Cleanup the metadata log files and verify the expected deleted files.\n   *\n   * @param logFiles List of log files in the _delta_log directory\n   * @param expectedDeletedFiles List of expected deleted file paths\n   * @param currentTimeMillis Current time in millis\n   * @param retentionPeriodMillis Retention period in millis\n   */\n  def cleanupAndVerify(\n      logFiles: Seq[FileStatus],\n      expectedDeletedFiles: Seq[String],\n      currentTimeMillis: Long,\n      retentionPeriodMillis: Long): Unit = {\n    val fsClient = mockFsClient(logFiles)\n    val resultDeletedCount = cleanupExpiredLogs(\n      mockEngine(fsClient),\n      new ManualClock(currentTimeMillis),\n      logPath,\n      retentionPeriodMillis)\n\n    assert(resultDeletedCount === expectedDeletedFiles.size)\n    assert(fsClient.getDeleteCalls.toSet === expectedDeletedFiles.toSet)\n  }\n}\n\nobject MetadataCleanupSuite extends MockFileSystemClientUtils {\n  /* ------------------- HELPER UTILITIES/CONSTANTS ------------------ */\n  private val multiPartCheckpointPartsSize = 4\n\n  /** Case class containing the list of expected files in the deleted metadata log file list */\n  case class DeletedFileList(\n      deltaVersions: Seq[Long] = Seq.empty,\n      classicCheckpointVersions: Seq[Long] = Seq.empty,\n      multipartCheckpointVersions: Seq[Long] = Seq.empty,\n      v2CheckpointVersions: Seq[Long] = Seq.empty) {\n\n    def fileList(): Seq[String] = {\n      (deltaFileStatuses(deltaVersions) ++\n        singularCheckpointFileStatuses(classicCheckpointVersions) ++\n        multiCheckpointFileStatuses(multipartCheckpointVersions, multiPartCheckpointPartsSize) ++\n        v2CPFileStatuses(v2CheckpointVersions)).sortBy(_.getPath).map(_.getPath)\n    }\n  }\n\n  def mockFsClient(logFiles: Seq[FileStatus]): MockListFromDeleteFileSystemClient = {\n    new MockListFromDeleteFileSystemClient(logFiles)\n  }\n\n  def v2CPFileStatuses(versions: Seq[Long]): Seq[FileStatus] = {\n    // Replace the UUID with a standard UUID to make the test deterministic\n    val standardUUID = \"123e4567-e89b-12d3-a456-426614174000\"\n    val uuidPattern =\n      \"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\".r\n\n    v2CheckpointFileStatuses(\n      versions.map(v => (v, true, 20)), // to (version, useUUID, numSidecars)\n      \"parquet\").map(_._1)\n      .map(f =>\n        FileStatus.of(\n          uuidPattern.replaceAllIn(f.getPath, standardUUID),\n          f.getSize,\n          f.getModificationTime))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/stats/FileSizeHistogramSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.stats\n\nimport scala.collection.JavaConverters._\nimport scala.collection.Searching.search\n\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.internal.util.VectorUtils.toJavaList\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass FileSizeHistogramSuite extends AnyFunSuite {\n  implicit class HistogramOps(histogram: FileSizeHistogram) {\n    def getSortedBinBoundaries: List[Long] = {\n      toJavaList(histogram.toRow().getArray(\n        FileSizeHistogram.FULL_SCHEMA.indexOf(\"sortedBinBoundaries\"))).asScala.toList\n    }\n\n    def getFileCounts: List[Long] = {\n      toJavaList(histogram.toRow().getArray(\n        FileSizeHistogram.FULL_SCHEMA.indexOf(\"fileCounts\"))).asScala.toList\n    }\n\n    def getTotalBytes: List[Long] = {\n      toJavaList(histogram.toRow().getArray(\n        FileSizeHistogram.FULL_SCHEMA.indexOf(\"totalBytes\"))).asScala.toList\n    }\n  }\n\n  private val KB = 1024L\n  private val MB = KB * 1024\n  private val GB = MB * 1024\n\n  test(\"createDefaultHistogram should create histogram with zero counts and bytes\") {\n    val histogram = FileSizeHistogram.createDefaultHistogram()\n    assert(histogram.getFileCounts.forall(_ == 0L))\n    assert(histogram.getTotalBytes.forall(_ == 0L))\n    assert(histogram.getSortedBinBoundaries == List(\n      0L,\n      // Power of 2 till 4 MB\n      8 * KB,\n      16 * KB,\n      32 * KB,\n      64 * KB,\n      128 * KB,\n      256 * KB,\n      512 * KB,\n      1 * MB,\n      2 * MB,\n      4 * MB,\n      // 4 MB jumps till 40 MB\n      8 * MB,\n      12 * MB,\n      16 * MB,\n      20 * MB,\n      24 * MB,\n      28 * MB,\n      32 * MB,\n      36 * MB,\n      40 * MB,\n      // 8 MB jumps till 120 MB\n      48 * MB,\n      56 * MB,\n      64 * MB,\n      72 * MB,\n      80 * MB,\n      88 * MB,\n      96 * MB,\n      104 * MB,\n      112 * MB,\n      120 * MB,\n      // 4 MB jumps till 144 MB\n      124 * MB,\n      128 * MB,\n      132 * MB,\n      136 * MB,\n      140 * MB,\n      144 * MB,\n      // 16 MB jumps till 576 MB\n      160 * MB,\n      176 * MB,\n      192 * MB,\n      208 * MB,\n      224 * MB,\n      240 * MB,\n      256 * MB,\n      272 * MB,\n      288 * MB,\n      304 * MB,\n      320 * MB,\n      336 * MB,\n      352 * MB,\n      368 * MB,\n      384 * MB,\n      400 * MB,\n      416 * MB,\n      432 * MB,\n      448 * MB,\n      464 * MB,\n      480 * MB,\n      496 * MB,\n      512 * MB,\n      528 * MB,\n      544 * MB,\n      560 * MB,\n      576 * MB,\n      // 64 MB jumps till 1408 MB\n      640 * MB,\n      704 * MB,\n      768 * MB,\n      832 * MB,\n      896 * MB,\n      960 * MB,\n      1024 * MB,\n      1088 * MB,\n      1152 * MB,\n      1216 * MB,\n      1280 * MB,\n      1344 * MB,\n      1408 * MB,\n      // 128 MB jumps till 2 GB\n      1536 * MB,\n      1664 * MB,\n      1792 * MB,\n      1920 * MB,\n      2048 * MB,\n      // 256 MB jumps till 4 GB\n      2304 * MB,\n      2560 * MB,\n      2816 * MB,\n      3072 * MB,\n      3328 * MB,\n      3584 * MB,\n      3840 * MB,\n      4 * GB,\n      // power of 2 till 256 GB\n      8 * GB,\n      16 * GB,\n      32 * GB,\n      64 * GB,\n      128 * GB,\n      256 * GB))\n  }\n\n  test(\"basic insert\") {\n    val histogram = FileSizeHistogram.createDefaultHistogram()\n    val testSizes = List(\n      512L, // Small file\n      8L * KB, // 8KB\n      1L * MB, // 1MB\n      128L * MB, // 128MB\n      1L * GB, // 1GB\n      10L * GB // 10GB\n    )\n\n    // Test single insertions with different bins\n    testSizes.foreach { size =>\n      histogram.insert(size)\n      val index = getBinIndexForTesting(histogram.getSortedBinBoundaries, size)\n      assert(histogram.getFileCounts(index) == 1L)\n      assert(histogram.getTotalBytes(index) == size)\n    }\n\n    // Test multiple insertions in same bin\n    val sizeForMultiple = 1L * MB\n    val numFiles = 5\n    val index = getBinIndexForTesting(histogram.getSortedBinBoundaries, sizeForMultiple)\n    val initialCount = histogram.getFileCounts(index)\n    val initialBytes = histogram.getTotalBytes(index)\n\n    (1 to numFiles).foreach { i =>\n      histogram.insert(sizeForMultiple)\n      assert(histogram.getFileCounts(index) == initialCount + i)\n      assert(histogram.getTotalBytes(index) == initialBytes + (i * sizeForMultiple))\n    }\n\n    // Test negative file size\n    intercept[IllegalArgumentException] {\n      histogram.insert(-1)\n    }\n  }\n\n  test(\"insert the boundary\") {\n    val histogram = FileSizeHistogram.createDefaultHistogram()\n    val boundaries = histogram.getSortedBinBoundaries\n    boundaries.foreach { boundary =>\n      histogram.insert(boundary)\n      val index = getBinIndexForTesting(boundaries, boundary)\n      assert(histogram.getFileCounts(index) == 1)\n    }\n  }\n\n  test(\"insert very large file\") {\n    val histogram = FileSizeHistogram.createDefaultHistogram()\n    val veryLargeSize = 256L * GB\n    histogram.insert(veryLargeSize)\n    val lastIndex = histogram.getSortedBinBoundaries.size - 1\n\n    assert(histogram.getFileCounts(lastIndex) == 1)\n  }\n\n  test(\"remove should handle file sizes correctly\") {\n    val histogram = FileSizeHistogram.createDefaultHistogram()\n    val fileSize = 1L * MB\n    val index = getBinIndexForTesting(histogram.getSortedBinBoundaries, fileSize)\n\n    // Test multiple inserts and removes\n    val numFiles = 3\n    (1 to numFiles).foreach(_ => histogram.insert(fileSize))\n\n    (1 to numFiles).foreach { i =>\n      histogram.remove(fileSize)\n      val remainingFiles = numFiles - i\n      assert(histogram.getFileCounts(index) == remainingFiles)\n      assert(histogram.getTotalBytes(index) == fileSize * remainingFiles)\n    }\n\n    assert(histogram.getTotalBytes(\n      getBinIndexForTesting(histogram.getSortedBinBoundaries, fileSize)) === 0)\n    // Test error cases\n    intercept[IllegalArgumentException] {\n      histogram.remove(fileSize) // Try to remove from empty bin\n    }\n\n    histogram.insert(fileSize)\n    assert(\n      getBinIndexForTesting(histogram.getSortedBinBoundaries, fileSize)\n        === getBinIndexForTesting(histogram.getSortedBinBoundaries, fileSize + 1))\n    intercept[IllegalArgumentException] {\n      histogram.remove(fileSize + 1) // Try to remove more bytes than available\n    }\n\n    intercept[IllegalArgumentException] {\n      histogram.remove(-1) // Try to remove negative size\n    }\n  }\n\n  test(\"histogram should be identical after serialization round trip\") {\n    val histogram = FileSizeHistogram.createDefaultHistogram()\n    List(\n      (512L, 5), // 5 files of 512B\n      (1 * MB, 3), // 3 files of 1MB\n      (10 * MB, 2) // 2 files of 10MB\n    ).foreach { case (size, count) =>\n      (1 to count).foreach(_ => histogram.insert(size))\n    }\n\n    val reconstructedHistogram = FileSizeHistogram.fromColumnVector(\n      VectorUtils.buildColumnVector(\n        Seq(histogram.toRow).toList.asJava,\n        FileSizeHistogram.FULL_SCHEMA),\n      0)\n\n    assert(reconstructedHistogram.isPresent)\n    assert(histogram === reconstructedHistogram.get())\n  }\n\n  test(\"plus should combine histograms correctly\") {\n    // Create two histograms\n    val histogram1 = FileSizeHistogram.createDefaultHistogram()\n    val histogram2 = FileSizeHistogram.createDefaultHistogram()\n\n    // Add data to first histogram\n    histogram1.insert(1 * MB)\n    histogram1.insert(1 * MB)\n    histogram1.insert(10 * MB)\n\n    // Add data to second histogram\n    histogram2.insert(2 * MB)\n    histogram2.insert(10 * MB)\n    histogram2.insert(10 * MB)\n\n    // Combine histograms\n    val combined = histogram1.plus(histogram2)\n\n    // Check results\n    val mbBinIndex1 = getBinIndexForTesting(combined.getSortedBinBoundaries, 1 * MB)\n    val mbBinIndex2 = getBinIndexForTesting(combined.getSortedBinBoundaries, 2 * MB)\n    val mbBinIndex10 = getBinIndexForTesting(combined.getSortedBinBoundaries, 10 * MB)\n\n    assert(combined.getFileCounts(mbBinIndex1) == 2)\n    assert(combined.getTotalBytes(mbBinIndex1) == 2 * MB)\n\n    assert(combined.getFileCounts(mbBinIndex2) == 1)\n    assert(combined.getTotalBytes(mbBinIndex2) == 2 * MB)\n\n    assert(combined.getFileCounts(mbBinIndex10) == 3)\n    assert(combined.getTotalBytes(mbBinIndex10) == 30 * MB)\n    // check commutative.\n    assert(combined === histogram2.plus(histogram1))\n\n    // Test error case - different bin boundaries\n    val customBoundaries = Array(0L, 1L * KB, 1L * MB)\n    val customCounts = new Array[Long](customBoundaries.length)\n    val customBytes = new Array[Long](customBoundaries.length)\n    val customHistogram = new FileSizeHistogram(customBoundaries, customCounts, customBytes)\n\n    intercept[IllegalArgumentException] {\n      histogram1.plus(customHistogram)\n    }\n  }\n\n  test(\"minus should subtract histograms correctly\") {\n    // Create two histograms\n    val histogram1 = FileSizeHistogram.createDefaultHistogram()\n    val histogram2 = FileSizeHistogram.createDefaultHistogram()\n\n    // Add data to first histogram\n    histogram1.insert(1 * MB)\n    histogram1.insert(1 * MB)\n    histogram1.insert(1 * MB)\n    histogram1.insert(10 * MB)\n    histogram1.insert(10 * MB)\n\n    // Add subset of data to second histogram\n    histogram2.insert(1 * MB)\n    histogram2.insert(10 * MB)\n\n    // Check plus and minus are opposite operation.\n    assert(histogram1 === histogram1.plus(histogram2).minus(histogram2))\n\n    // Subtract histograms\n    val result = histogram1.minus(histogram2)\n\n    // Check results\n    val mbBinIndex1 = getBinIndexForTesting(result.getSortedBinBoundaries, 1 * MB)\n    val mbBinIndex10 = getBinIndexForTesting(result.getSortedBinBoundaries, 10 * MB)\n\n    assert(result.getFileCounts(mbBinIndex1) == 2)\n    assert(result.getTotalBytes(mbBinIndex1) == 2 * MB)\n\n    assert(result.getFileCounts(mbBinIndex10) == 1)\n    assert(result.getTotalBytes(mbBinIndex10) == 10 * MB)\n\n    // Test error cases\n    // different bin boundaries\n    val customBoundaries = Array(0L, 1L * KB, 1L * MB)\n    val customCounts = new Array[Long](customBoundaries.length)\n    val customBytes = new Array[Long](customBoundaries.length)\n    val customHistogram = new FileSizeHistogram(customBoundaries, customCounts, customBytes)\n\n    intercept[IllegalArgumentException] {\n      histogram1.minus(customHistogram)\n    }\n\n    // Negative result scenario\n    val largerHistogram = FileSizeHistogram.createDefaultHistogram()\n    largerHistogram.insert(1 * MB)\n    largerHistogram.insert(1 * MB)\n\n    intercept[IllegalArgumentException] {\n      // Try to subtract more than what exists\n      FileSizeHistogram.createDefaultHistogram().minus(largerHistogram)\n    }\n  }\n\n  /**\n   * Determines the bin index for a given file size using binary search.\n   * Returns the index of the largest bin boundary that is less than or equal to the file size.\n   */\n  private def getBinIndexForTesting(boundaries: List[Long], fileSize: Long): Int = {\n    import scala.collection.Searching.{Found, InsertionPoint}\n    boundaries.search(fileSize) match {\n      // Exact match found, return the matching index\n      case Found(index) => index\n      // Not found and got insert point.\n      // Return the index of the bin boundary just below the file size (insert point - 1)\n      case InsertionPoint(index) => index - 1\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/tablefeatures/TableFeaturesSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.tablefeatures\n\nimport java.util.{Collections, Optional}\nimport java.util.Collections.{emptySet, singleton}\nimport java.util.stream.Collectors.toList\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ArrayValue, ColumnVector, MapValue}\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{validateKernelCanReadTheTable, validateKernelCanWriteToTable, TABLE_FEATURES}\nimport io.delta.kernel.internal.util.InternalUtils.singletonStringColumnVector\nimport io.delta.kernel.internal.util.VectorUtils.buildColumnVector\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Suite that tests Kernel throws error when it receives a unsupported protocol and metadata\n */\nclass TableFeaturesSuite extends AnyFunSuite {\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for [[TableFeature]] implementations                                                  //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  val readerWriterFeatures = Seq(\n    \"catalogManaged\",\n    \"columnMapping\",\n    \"deletionVectors\",\n    \"timestampNtz\",\n    \"typeWidening\",\n    \"typeWidening-preview\",\n    \"v2Checkpoint\",\n    \"vacuumProtocolCheck\",\n    \"variantType\",\n    \"variantType-preview\",\n    \"variantShredding\",\n    \"variantShredding-preview\")\n\n  val writerOnlyFeatures = Seq(\n    \"allowColumnDefaults\",\n    \"appendOnly\",\n    \"checkpointProtection\",\n    \"invariants\",\n    \"checkConstraints\",\n    \"generatedColumns\",\n    \"changeDataFeed\",\n    \"identityColumns\",\n    \"rowTracking\",\n    \"domainMetadata\",\n    \"icebergCompatV2\",\n    \"icebergCompatV3\",\n    \"inCommitTimestamp\",\n    \"icebergWriterCompatV1\",\n    \"icebergWriterCompatV3\",\n    \"clustering\",\n    \"materializePartitionColumns\")\n\n  val legacyFeatures = Seq(\n    \"appendOnly\",\n    \"invariants\",\n    \"checkConstraints\",\n    \"generatedColumns\",\n    \"changeDataFeed\",\n    \"identityColumns\",\n    \"columnMapping\")\n\n  test(\"basic properties checks\") {\n\n    // Check that all features are correctly classified as reader-writer or writer-only\n    readerWriterFeatures.foreach { feature =>\n      assert(TableFeatures.getTableFeature(feature).isReaderWriterFeature)\n    }\n    writerOnlyFeatures.foreach { feature =>\n      assert(!TableFeatures.getTableFeature(feature).isReaderWriterFeature)\n    }\n\n    // Check that legacy features are correctly classified as legacy features\n    (readerWriterFeatures ++ writerOnlyFeatures) foreach { feature =>\n      if (legacyFeatures.contains(feature)) {\n        assert(TableFeatures.getTableFeature(feature).isLegacyFeature)\n      } else {\n        assert(!TableFeatures.getTableFeature(feature).isLegacyFeature)\n      }\n    }\n\n    // all expected features are present in list. Just make sure we don't miss any\n    // adding to the list. This is the list used to iterate over all features\n    assert(\n      TableFeatures.TABLE_FEATURES.size() == readerWriterFeatures.size + writerOnlyFeatures.size)\n  }\n\n  val testProtocol = new Protocol(1, 2, emptySet(), emptySet())\n  Seq(\n    // Test feature, metadata, expected result\n    (\"appendOnly\", testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"true\")), true),\n    (\"appendOnly\", testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"false\")), false),\n    (\"invariants\", testMetadata(includeInvariant = true), true),\n    (\"invariants\", testMetadata(includeInvariant = false), false),\n    (\"checkConstraints\", testMetadata(tblProps = Map(\"delta.constraints.a\" -> \"a = b\")), true),\n    (\"checkConstraints\", testMetadata(), false),\n    (\"generatedColumns\", testMetadata(includeGeneratedColumn = true), true),\n    (\"generatedColumns\", testMetadata(includeGeneratedColumn = false), false),\n    (\"changeDataFeed\", testMetadata(tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")), true),\n    (\n      \"changeDataFeed\",\n      testMetadata(tblProps = Map(\"delta.enableChangeDataFeed\" -> \"false\")),\n      false),\n    (\"identityColumns\", testMetadata(includeIdentityColumn = true), true),\n    (\"identityColumns\", testMetadata(includeIdentityColumn = false), false),\n    (\"columnMapping\", testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"id\")), true),\n    (\"columnMapping\", testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"none\")), false),\n    (\"typeWidening\", testMetadata(tblProps = Map(\"delta.enableTypeWidening\" -> \"true\")), true),\n    (\"typeWidening\", testMetadata(tblProps = Map(\"delta.enableTypeWidening\" -> \"false\")), false),\n    (\"variantType\", testMetadata(includeVariantTypeCol = true), true),\n    (\"variantType\", testMetadata(includeVariantTypeCol = false), false),\n    (\"rowTracking\", testMetadata(tblProps = Map(\"delta.enableRowTracking\" -> \"true\")), true),\n    (\"rowTracking\", testMetadata(tblProps = Map(\"delta.enableRowTracking\" -> \"false\")), false),\n    (\n      \"variantShredding\",\n      testMetadata(tblProps = Map(\"delta.enableVariantShredding\" -> \"true\")),\n      true),\n    (\n      \"variantShredding\",\n      testMetadata(tblProps = Map(\"delta.enableVariantShredding\" -> \"false\")),\n      false),\n    (\n      \"deletionVectors\",\n      testMetadata(tblProps = Map(\"delta.enableDeletionVectors\" -> \"true\")),\n      true),\n    (\n      \"deletionVectors\",\n      testMetadata(tblProps = Map(\"delta.enableDeletionVectors\" -> \"false\")),\n      false),\n    (\"timestampNtz\", testMetadata(includeTimestampNtzTypeCol = true), true),\n    (\"timestampNtz\", testMetadata(includeTimestampNtzTypeCol = false), false),\n    (\"v2Checkpoint\", testMetadata(tblProps = Map(\"delta.checkpointPolicy\" -> \"v2\")), true),\n    (\"v2Checkpoint\", testMetadata(tblProps = Map(\"delta.checkpointPolicy\" -> \"classic\")), false),\n    (\n      \"icebergCompatV2\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV2\" -> \"true\")),\n      true),\n    (\n      \"icebergCompatV2\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV2\" -> \"false\")),\n      false),\n    (\n      \"icebergCompatV3\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV3\" -> \"true\")),\n      true),\n    (\n      \"icebergCompatV3\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV3\" -> \"false\")),\n      false),\n    (\n      \"inCommitTimestamp\",\n      testMetadata(tblProps = Map(\"delta.enableInCommitTimestamps\" -> \"true\")),\n      true),\n    (\n      \"inCommitTimestamp\",\n      testMetadata(tblProps = Map(\"delta.enableInCommitTimestamps\" -> \"false\")),\n      false),\n    (\n      \"icebergWriterCompatV1\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV1\" -> \"true\")),\n      true),\n    (\n      \"icebergWriterCompatV1\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV1\" -> \"false\")),\n      false),\n    (\n      \"icebergWriterCompatV3\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV3\" -> \"true\")),\n      true),\n    (\n      \"icebergWriterCompatV3\",\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV3\" -> \"false\")),\n      false)).foreach({ case (feature, metadata, expected) =>\n    test(s\"metadataRequiresFeatureToBeEnabled - $feature - $metadata\") {\n      val tableFeature = TableFeatures.getTableFeature(feature)\n      assert(tableFeature.isInstanceOf[FeatureAutoEnabledByMetadata])\n      assert(tableFeature.asInstanceOf[FeatureAutoEnabledByMetadata]\n        .metadataRequiresFeatureToBeEnabled(testProtocol, metadata) == expected)\n    }\n  })\n\n  Seq(\n    \"checkpointProtection\",\n    \"domainMetadata\",\n    \"vacuumProtocolCheck\",\n    \"clustering\",\n    \"catalogManaged\",\n    \"allowColumnDefaults\",\n    \"variantShredding-preview\").foreach {\n    feature =>\n      test(s\"doesn't support auto enable by metadata: $feature\") {\n        val tableFeature = TableFeatures.getTableFeature(feature)\n        assert(!tableFeature.isInstanceOf[FeatureAutoEnabledByMetadata])\n      }\n  }\n\n  Seq(\n    (\"variantType\", testMetadata(includeVariantTypeCol = true)),\n    (\"typeWidening\", testMetadata(tblProps = Map(\"delta.enableTypeWidening\" -> \"true\"))),\n    (\n      \"variantShredding\",\n      testMetadata(tblProps = Map(\"delta.enableVariantShredding\" -> \"true\")))).foreach {\n    case (feature, metadataEnablingFeature) =>\n      test(\"special handling of tables containing preview features: \" + feature) {\n        val protocolWithPreviewFeature = new Protocol(3, 7)\n          .withFeature(TableFeatures.getTableFeature(s\"$feature-preview\"))\n\n        val enable = TableFeatures.getTableFeature(feature)\n          .asInstanceOf[FeatureAutoEnabledByMetadata]\n          .metadataRequiresFeatureToBeEnabled(\n            protocolWithPreviewFeature,\n            metadataEnablingFeature)\n        assert(!enable, \"shouldn't enable non-preview feature\")\n      }\n  }\n\n  test(\"hasKernelReadSupport expected to be true\") {\n    val results = TABLE_FEATURES.stream()\n      .filter(_.isReaderWriterFeature)\n      .filter(_.hasKernelReadSupport())\n      .collect(toList()).asScala\n\n    val expected = Seq(\n      \"catalogManaged\",\n      \"columnMapping\",\n      \"v2Checkpoint\",\n      \"variantType\",\n      \"variantType-preview\",\n      \"variantShredding\",\n      \"variantShredding-preview\",\n      \"typeWidening\",\n      \"typeWidening-preview\",\n      \"deletionVectors\",\n      \"timestampNtz\",\n      \"vacuumProtocolCheck\")\n\n    assert(results.map(_.featureName()).toSet == expected.toSet)\n  }\n\n  test(\"hasKernelWriteSupport expected to be true\") {\n    val results = TABLE_FEATURES.stream()\n      .filter(_.hasKernelWriteSupport(testMetadata()))\n      .collect(toList()).asScala\n\n    // checkConstraints, generatedColumns, identityColumns, invariants and changeDataFeed\n    // are writable because the metadata has not been set the info that\n    // these features are enabled\n    val expected = Seq(\n      \"catalogManaged\",\n      \"checkpointProtection\",\n      \"columnMapping\",\n      \"allowColumnDefaults\",\n      \"v2Checkpoint\",\n      \"deletionVectors\",\n      \"vacuumProtocolCheck\",\n      \"rowTracking\",\n      \"domainMetadata\",\n      \"icebergCompatV2\",\n      \"icebergCompatV3\",\n      \"inCommitTimestamp\",\n      \"appendOnly\",\n      \"invariants\",\n      \"checkConstraints\",\n      \"generatedColumns\",\n      \"changeDataFeed\",\n      \"timestampNtz\",\n      \"identityColumns\",\n      \"typeWidening-preview\",\n      \"typeWidening\",\n      \"icebergWriterCompatV1\",\n      \"icebergWriterCompatV3\",\n      \"clustering\",\n      \"variantType-preview\",\n      \"variantType\",\n      \"variantShredding\",\n      \"variantShredding-preview\",\n      \"materializePartitionColumns\")\n\n    assert(results.map(_.featureName()).toSet == expected.toSet)\n  }\n\n  Seq(\n    // Test format: feature, metadata, expected value\n    (\"invariants\", testMetadata(includeInvariant = true), false),\n    (\"checkConstraints\", testMetadata(tblProps = Map(\"delta.constraints.a\" -> \"a = b\")), false),\n    (\"generatedColumns\", testMetadata(includeGeneratedColumn = true), false),\n    (\"identityColumns\", testMetadata(includeIdentityColumn = true), false)).foreach({\n    case (feature, metadata, expected) =>\n      test(s\"hasKernelWriteSupport - $feature has metadata\") {\n        val tableFeature = TableFeatures.getTableFeature(feature)\n        assert(tableFeature.hasKernelWriteSupport(metadata) == expected)\n      }\n  })\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for validateKernelCanReadTheTable and validateKernelCanWriteToTable                   //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Reads: All legacy protocols should be readable by Kernel\n  Seq(\n    // Test format: protocol (minReaderVersion, minWriterVersion)\n    (1, 1),\n    (1, 2),\n    (1, 3),\n    (1, 4),\n    (2, 5),\n    (2, 6)).foreach {\n    case (minReaderVersion, minWriterVersion) =>\n      test(s\"validateKernelCanReadTheTable: protocol ($minReaderVersion, $minWriterVersion)\") {\n        val protocol = new Protocol(minReaderVersion, minWriterVersion)\n        validateKernelCanReadTheTable(protocol, \"/test/table\")\n      }\n  }\n\n  // Reads: Supported table features represented as readerFeatures in the protocol\n  Seq(\n    \"catalogManaged\",\n    \"variantType\",\n    \"variantType-preview\",\n    \"variantShredding\",\n    \"variantShredding-preview\",\n    \"deletionVectors\",\n    \"typeWidening\",\n    \"typeWidening-preview\",\n    \"timestampNtz\",\n    \"v2Checkpoint\",\n    \"vacuumProtocolCheck\",\n    \"allowColumnDefaults\",\n    \"columnMapping\").foreach { feature =>\n    test(s\"validateKernelCanReadTheTable: protocol 3 with $feature\") {\n      val protocol = new Protocol(3, 1, singleton(feature), Set().asJava)\n      validateKernelCanReadTheTable(protocol, \"/test/table\")\n    }\n  }\n\n  // Read is supported when all table readerWriter features are supported by the Kernel,\n  // but the table has writeOnly table feature unknown to Kernel\n  test(\"validateKernelCanReadTheTable: with writeOnly feature unknown to Kernel\") {\n\n    // legacy reader protocol version\n    val protocol1 = new Protocol(1, 7, emptySet(), singleton(\"unknownFeature\"))\n    validateKernelCanReadTheTable(protocol1, \"/test/table\")\n\n    // table feature supported reader version\n    val protocol2 = new Protocol(\n      3,\n      7,\n      Set(\"columnMapping\", \"timestampNtz\").asJava,\n      Set(\"columnMapping\", \"timestampNtz\", \"unknownFeature\").asJava)\n    validateKernelCanReadTheTable(protocol2, \"/test/table\")\n  }\n\n  test(\"validateKernelCanReadTheTable: unknown readerWriter feature to Kernel\") {\n    val protocol = new Protocol(3, 7, singleton(\"unknownFeature\"), singleton(\"unknownFeature\"))\n    val ex = intercept[KernelException] {\n      validateKernelCanReadTheTable(protocol, \"/test/table\")\n    }\n    assert(ex.getMessage.contains(\n      \"requires feature \\\"unknownFeature\\\" which is unsupported by this version of Delta Kernel\"))\n  }\n\n  test(\"validateKernelCanReadTheTable: reader version > 3\") {\n    val protocol = new Protocol(4, 7, emptySet(), singleton(\"unknownFeature\"))\n    val ex = intercept[KernelException] {\n      validateKernelCanReadTheTable(protocol, \"/test/table\")\n    }\n    assert(ex.getMessage.contains(\n      \"requires reader version 4 which is unsupported by this version of Delta Kernel\"))\n  }\n\n  // Writes\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with catalogManaged\",\n    new Protocol(3, 7, singleton(\"catalogManaged\"), singleton(\"catalogManaged\")),\n    testMetadata())\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 8\", // beyond the table feature writer version\n    new Protocol(3, 8))\n\n  checkWriteUnsupported(\n    // beyond the table feature reader/writer version\n    \"validateKernelCanWriteToTable: protocol 4, 8\",\n    new Protocol(4, 8))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 1\",\n    new Protocol(1, 1),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 2\",\n    new Protocol(1, 2),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 2 with appendOnly\",\n    new Protocol(1, 2),\n    testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"true\")))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 2 with invariants\",\n    new Protocol(1, 2),\n    testMetadata(includeInvariant = true))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 2, with appendOnly and invariants\",\n    new Protocol(1, 2),\n    testMetadata(includeInvariant = true, tblProps = Map(\"delta.appendOnly\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 3\",\n    new Protocol(1, 3))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 3 with checkConstraints\",\n    new Protocol(1, 3),\n    testMetadata(tblProps = Map(\"delta.constraints.a\" -> \"a = b\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 4\",\n    new Protocol(1, 4))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 4 with generatedColumns\",\n    new Protocol(1, 4),\n    testMetadata(includeGeneratedColumn = true))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 4 with changeDataFeed\",\n    new Protocol(1, 4),\n    testMetadata(tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 5 with columnMapping\",\n    new Protocol(2, 5),\n    testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"id\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 6\",\n    new Protocol(2, 6),\n    testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"none\")))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 6 with identityColumns\",\n    new Protocol(2, 6),\n    testMetadata(includeIdentityColumn = true))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with appendOnly supported\",\n    new Protocol(1, 7, Set().asJava, singleton(\"appendOnly\")),\n    testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with invariants, \" +\n      \"schema doesn't contain invariants\",\n    new Protocol(1, 7, Set().asJava, singleton(\"invariants\")),\n    testMetadata(includeInvariant = false))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with invariants, \" +\n      \"schema contains invariants\",\n    new Protocol(1, 7, Set().asJava, singleton(\"invariants\")),\n    testMetadata(includeInvariant = true))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with checkConstraints, \" +\n      \"metadata doesn't contains any constraints\",\n    new Protocol(1, 7, Set().asJava, singleton(\"checkConstraints\")),\n    testMetadata())\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with checkConstraints, \" +\n      \"metadata contains constraints\",\n    new Protocol(1, 7, Set().asJava, singleton(\"checkConstraints\")),\n    testMetadata(tblProps = Map(\"delta.constraints.a\" -> \"a = b\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with generatedColumns, \" +\n      \"metadata doesn't contains any generated columns\",\n    new Protocol(1, 7, Set().asJava, singleton(\"generatedColumns\")),\n    testMetadata())\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with generatedColumns, \" +\n      \"metadata contains generated columns\",\n    new Protocol(1, 7, Set().asJava, singleton(\"generatedColumns\")),\n    testMetadata(includeGeneratedColumn = true))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with changeDataFeed, \" +\n      \"metadata doesn't contains changeDataFeed\",\n    new Protocol(1, 7, Set().asJava, singleton(\"changeDataFeed\")),\n    testMetadata())\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with changeDataFeed, \" +\n      \"metadata contains changeDataFeed\",\n    new Protocol(1, 7, Set().asJava, singleton(\"changeDataFeed\")),\n    testMetadata(tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with columnMapping, \" +\n      \"metadata doesn't contains columnMapping\",\n    new Protocol(2, 7, Set().asJava, singleton(\"columnMapping\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with columnMapping, \" +\n      \"metadata contains columnMapping\",\n    new Protocol(2, 7, Set().asJava, singleton(\"columnMapping\")),\n    testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"id\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with allowColumnDefaults, \" +\n      \"metadata contains allowColumnDefaults\",\n    new Protocol(2, 7, Set().asJava, singleton(\"allowColumnDefaults\")),\n    testMetadata(tblProps = Map(\"delta.feature.allowColumnDefaults\" -> \"supported\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with identityColumns, \" +\n      \"schema doesn't contains identity columns\",\n    new Protocol(2, 7, Set().asJava, singleton(\"identityColumns\")),\n    testMetadata())\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with identityColumns, \" +\n      \"schema contains identity columns\",\n    new Protocol(2, 7, Set().asJava, singleton(\"identityColumns\")),\n    testMetadata(includeIdentityColumn = true))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with deletionVectors, \" +\n      \"metadata doesn't contains deletionVectors\",\n    new Protocol(2, 7, Set().asJava, singleton(\"deletionVectors\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with deletionVectors, \" +\n      \"metadata contains deletionVectors\",\n    new Protocol(2, 7, Set().asJava, singleton(\"deletionVectors\")),\n    testMetadata(tblProps = Map(\"delta.enableDeletionVectors\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with timestampNtz, \" +\n      \"schema doesn't contains timestampNtz\",\n    new Protocol(3, 7, singleton(\"timestampNtz\"), singleton(\"timestampNtz\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with timestampNtz, \" +\n      \"schema contains timestampNtz\",\n    new Protocol(3, 7, singleton(\"timestampNtz\"), singleton(\"timestampNtz\")),\n    testMetadata(includeTimestampNtzTypeCol = true))\n\n  Seq(\"typeWidening\", \"typeWidening-preview\").foreach { feature =>\n    checkWriteSupported(\n      s\"validateKernelCanWriteToTable: protocol 7 with $feature, \" +\n        s\"metadata doesn't contains $feature\",\n      new Protocol(3, 7, singleton(feature), singleton(feature)),\n      testMetadata())\n\n    checkWriteSupported(\n      s\"validateKernelCanWriteToTable: protocol 7 with $feature, \" +\n        s\"metadata contains $feature\",\n      new Protocol(3, 7, singleton(feature), singleton(feature)),\n      testMetadata(tblProps = Map(\"delta.enableTypeWidening\" -> \"true\")))\n  }\n\n  Seq(\"variantType\", \"variantType-preview\", \"variantShredding\", \"variantShredding-preview\")\n    .foreach { feature =>\n      checkWriteSupported(\n        s\"validateKernelCanWriteToTable: protocol 7 with $feature, \" +\n          s\"metadata doesn't contains $feature\",\n        new Protocol(3, 7, singleton(feature), singleton(feature)),\n        testMetadata())\n\n      checkWriteSupported(\n        s\"validateKernelCanWriteToTable: protocol 7 with $feature, \" +\n          s\"metadata contains $feature\",\n        new Protocol(3, 7, singleton(feature), singleton(feature)),\n        testMetadata(includeVariantTypeCol = true))\n    }\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with vacuumProtocolCheck, \" +\n      \"metadata doesn't contains vacuumProtocolCheck\",\n    new Protocol(3, 7, singleton(\"vacuumProtocolCheck\"), singleton(\"vacuumProtocolCheck\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with domainMetadata\",\n    new Protocol(3, 7, emptySet(), singleton(\"domainMetadata\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with rowTracking\",\n    new Protocol(3, 7, emptySet(), singleton(\"rowTracking\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with inCommitTimestamp\",\n    new Protocol(3, 7, emptySet(), singleton(\"inCommitTimestamp\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with icebergCompatV2\",\n    new Protocol(3, 7, emptySet(), singleton(\"icebergCompatV2\")),\n    testMetadata(tblProps = Map(\"delta.enableIcebergCompatV2\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with icebergCompatV3\",\n    new Protocol(3, 7, emptySet(), singleton(\"icebergCompatV3\")),\n    testMetadata(tblProps = Map(\"delta.enableIcebergCompatV3\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with v2Checkpoint, \" +\n      \"metadata enables v2Checkpoint\",\n    new Protocol(3, 7, singleton(\"v2Checkpoint\"), singleton(\"v2Checkpoint\")),\n    testMetadata(tblProps = Map(\"delta.checkpointPolicy\" -> \"v2\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with icebergWriterCompatV1\",\n    new Protocol(3, 7, emptySet(), singleton(\"icebergWriterCompatV1\")),\n    testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV1\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with icebergWriterCompatV3\",\n    new Protocol(3, 7, emptySet(), singleton(\"icebergWriterCompatV3\")),\n    testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV3\" -> \"true\")))\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with clustering\",\n    new Protocol(3, 7, emptySet(), singleton(\"clustering\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with materializePartitionColumns\",\n    new Protocol(3, 7, emptySet(), singleton(\"materializePartitionColumns\")),\n    testMetadata())\n\n  checkWriteSupported(\n    \"validateKernelCanWriteToTable: protocol 7 with multiple features supported\",\n    new Protocol(\n      3,\n      7,\n      Set(\"v2Checkpoint\", \"columnMapping\").asJava,\n      Set(\"v2Checkpoint\", \"columnMapping\", \"rowTracking\", \"domainMetadata\").asJava),\n    testMetadata(tblProps = Map(\n      \"delta.checkpointPolicy\" -> \"v2\",\n      \"delta.columnMapping.mode\" -> \"id\",\n      \"delta.enableRowTracking\" -> \"true\")))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with multiple features supported, \" +\n      \"with one of the features not supported by Kernel for writing\",\n    new Protocol(\n      3,\n      7,\n      Set(\"v2Checkpoint\", \"columnMapping\", \"invariants\").asJava,\n      Set(\"v2Checkpoint\", \"columnMapping\", \"invariants\").asJava),\n    testMetadata(\n      includeInvariant = true, // unsupported feature\n      tblProps = Map(\n        \"delta.checkpointPolicy\" -> \"v2\",\n        \"delta.enableRowTracking\" -> \"true\")))\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with unknown writerOnly feature\",\n    new Protocol(1, 7, emptySet(), singleton(\"unknownWriterOnlyFeature\")),\n    testMetadata())\n\n  checkWriteUnsupported(\n    \"validateKernelCanWriteToTable: protocol 7 with unknown readerWriter feature\",\n    new Protocol(\n      3,\n      7,\n      singleton(\"unknownWriterOnlyFeature\"),\n      singleton(\"unknownWriterOnlyFeature\")),\n    testMetadata())\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for autoUpgradeProtocolBasedOnMetadata                                                //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  Seq(\n    // Test format:\n    //  new metadata,\n    //  current protocol,\n    //  expected protocol,\n    //  expected new features added\n    (\n      testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"true\")),\n      new Protocol(1, 1),\n      new Protocol(1, 7, emptySet(), set(\"appendOnly\")),\n      set(\"appendOnly\")),\n    (\n      testMetadata(includeInvariant = true),\n      new Protocol(1, 1),\n      new Protocol(1, 7, emptySet(), set(\"invariants\")),\n      set(\"invariants\")),\n    (\n      testMetadata(includeInvariant = true, tblProps = Map(\"delta.appendOnly\" -> \"true\")),\n      new Protocol(1, 1),\n      new Protocol(1, 2), // (1, 2) covers both appendOnly and invariants\n      Set(\"invariants\", \"appendOnly\").asJava),\n    (\n      testMetadata(tblProps = Map(\"delta.constraints.a\" -> \"a = b\")),\n      new Protocol(1, 1),\n      new Protocol(1, 7, emptySet(), set(\"checkConstraints\")),\n      set(\"checkConstraints\")),\n    (\n      testMetadata(\n        includeInvariant = true,\n        tblProps = Map(\"delta.appendOnly\" -> \"true\", \"delta.constraints.a\" -> \"a = b\")),\n      new Protocol(1, 1),\n      new Protocol(1, 3),\n      set(\"appendOnly\", \"checkConstraints\", \"invariants\")),\n    (\n      testMetadata(tblProps = Map(\"delta.constraints.a\" -> \"a = b\")),\n      new Protocol(1, 2),\n      new Protocol(1, 3), // (1, 3) covers all: appendOnly, invariants, checkConstraints\n      set(\"checkConstraints\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")),\n      new Protocol(1, 1),\n      new Protocol(1, 7, emptySet(), set(\"changeDataFeed\")),\n      set(\"changeDataFeed\")),\n    (\n      testMetadata(includeGeneratedColumn = true),\n      new Protocol(1, 1),\n      new Protocol(1, 7, emptySet(), set(\"generatedColumns\")),\n      set(\"generatedColumns\")),\n    (\n      testMetadata(\n        includeGeneratedColumn = true,\n        tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")),\n      new Protocol(1, 1),\n      new Protocol(1, 7, emptySet(), set(\"generatedColumns\", \"changeDataFeed\")),\n      set(\"generatedColumns\", \"changeDataFeed\")),\n    (\n      testMetadata(\n        includeGeneratedColumn = true,\n        tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")),\n      new Protocol(1, 2),\n      new Protocol(\n        1,\n        7,\n        set(),\n        set(\"generatedColumns\", \"changeDataFeed\", \"appendOnly\", \"invariants\")),\n      set(\"generatedColumns\", \"changeDataFeed\")),\n    (\n      testMetadata(\n        includeGeneratedColumn = true,\n        tblProps = Map(\"delta.enableChangeDataFeed\" -> \"true\")),\n      new Protocol(1, 3),\n      new Protocol(1, 4), // 4 - implicitly supports all features\n      set(\"generatedColumns\", \"changeDataFeed\")),\n    (\n      testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"name\")),\n      new Protocol(1, 1),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\"columnMapping\")),\n      set(\"columnMapping\")),\n    (\n      testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"name\")),\n      new Protocol(1, 2),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\"columnMapping\", \"appendOnly\", \"invariants\")),\n      set(\"columnMapping\")),\n    (\n      testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"name\")),\n      new Protocol(1, 3),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\"columnMapping\", \"appendOnly\", \"invariants\", \"checkConstraints\")),\n      set(\"columnMapping\")),\n    (\n      testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"name\")),\n      new Protocol(1, 4),\n      new Protocol(2, 5), // implicitly supports all features\n      set(\"columnMapping\")),\n    (\n      testMetadata(includeIdentityColumn = true),\n      new Protocol(1, 1),\n      new Protocol(\n        1,\n        7,\n        set(),\n        set(\"identityColumns\")),\n      set(\"identityColumns\")),\n    (\n      testMetadata(includeIdentityColumn = true),\n      new Protocol(1, 2),\n      new Protocol(\n        1,\n        7,\n        set(),\n        set(\"identityColumns\", \"appendOnly\", \"invariants\")),\n      set(\"identityColumns\")),\n    (\n      testMetadata(includeIdentityColumn = true),\n      new Protocol(1, 3),\n      new Protocol(\n        1,\n        7,\n        set(),\n        set(\"identityColumns\", \"appendOnly\", \"invariants\", \"checkConstraints\")),\n      set(\"identityColumns\")),\n    (\n      testMetadata(includeIdentityColumn = true),\n      new Protocol(2, 5),\n      new Protocol(2, 6), // implicitly supports all features\n      set(\"identityColumns\")),\n    (\n      testMetadata(includeTimestampNtzTypeCol = true),\n      new Protocol(1, 1),\n      new Protocol(\n        3,\n        7,\n        set(\"timestampNtz\"),\n        set(\"timestampNtz\")),\n      set(\"timestampNtz\")),\n    (\n      testMetadata(includeTimestampNtzTypeCol = true),\n      new Protocol(1, 2),\n      new Protocol(\n        3,\n        7,\n        set(\"timestampNtz\"),\n        set(\"timestampNtz\", \"appendOnly\", \"invariants\")),\n      set(\"timestampNtz\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV2\" -> \"true\")),\n      new Protocol(1, 2),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\"columnMapping\", \"appendOnly\", \"invariants\", \"icebergCompatV2\")),\n      set(\"icebergCompatV2\", \"columnMapping\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV3\" -> \"true\")),\n      new Protocol(1, 2),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\n          \"columnMapping\",\n          \"appendOnly\",\n          \"invariants\",\n          \"icebergCompatV3\",\n          \"domainMetadata\",\n          \"rowTracking\")),\n      set(\"icebergCompatV3\", \"domainMetadata\", \"columnMapping\", \"rowTracking\")),\n    (\n      testMetadata(tblProps =\n        Map(\"delta.enableIcebergCompatV2\" -> \"true\", \"delta.enableDeletionVectors\" -> \"true\")),\n      new Protocol(2, 5),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\n          \"columnMapping\",\n          \"appendOnly\",\n          \"invariants\",\n          \"icebergCompatV2\",\n          \"checkConstraints\",\n          \"deletionVectors\",\n          \"generatedColumns\",\n          \"changeDataFeed\")),\n      set(\"icebergCompatV2\", \"deletionVectors\")),\n    (\n      testMetadata(tblProps =\n        Map(\"delta.enableIcebergCompatV2\" -> \"true\")),\n      new Protocol(3, 7, set(\"columnMapping\", \"deletionVectors\"), set(\"columnMapping\")),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\"columnMapping\", \"icebergCompatV2\", \"deletionVectors\")),\n      set(\"icebergCompatV2\")),\n    (\n      testMetadata(tblProps =\n        Map(\"delta.enableIcebergCompatV3\" -> \"true\")),\n      new Protocol(3, 7, set(\"columnMapping\", \"deletionVectors\"), set(\"columnMapping\")),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\n          \"columnMapping\",\n          \"icebergCompatV3\",\n          \"deletionVectors\",\n          \"domainMetadata\",\n          \"rowTracking\")),\n      set(\"icebergCompatV3\", \"domainMetadata\", \"rowTracking\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV1\" -> \"true\")),\n      new Protocol(1, 2),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\n          \"columnMapping\",\n          \"appendOnly\",\n          \"invariants\",\n          \"icebergCompatV2\",\n          \"icebergWriterCompatV1\")),\n      set(\"icebergCompatV2\", \"columnMapping\", \"icebergWriterCompatV1\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV3\" -> \"true\")),\n      new Protocol(1, 2),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\n          \"columnMapping\",\n          \"appendOnly\",\n          \"invariants\",\n          \"icebergCompatV3\",\n          \"icebergWriterCompatV3\",\n          \"domainMetadata\",\n          \"rowTracking\")),\n      set(\n        \"icebergCompatV3\",\n        \"columnMapping\",\n        \"icebergWriterCompatV3\",\n        \"domainMetadata\",\n        \"rowTracking\")),\n    (\n      testMetadata(tblProps = Map(\n        \"delta.enableIcebergWriterCompatV3\" -> \"true\",\n        \"delta.enableDeletionVectors\" -> \"true\")),\n      new Protocol(2, 5),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\n          \"columnMapping\",\n          \"appendOnly\",\n          \"deletionVectors\",\n          \"invariants\",\n          \"icebergCompatV3\",\n          \"icebergWriterCompatV3\",\n          \"checkConstraints\",\n          \"generatedColumns\",\n          \"changeDataFeed\",\n          \"domainMetadata\",\n          \"rowTracking\")),\n      set(\n        \"icebergCompatV3\",\n        \"icebergWriterCompatV3\",\n        \"deletionVectors\",\n        \"domainMetadata\",\n        \"rowTracking\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV3\" -> \"true\")),\n      new Protocol(1, 1), // Minimal starting protocol with no features\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\n          \"columnMapping\",\n          \"icebergCompatV3\", // Added as dependency\n          \"icebergWriterCompatV3\",\n          \"domainMetadata\",\n          \"rowTracking\")),\n      set(\n        \"icebergCompatV3\",\n        \"columnMapping\",\n        \"icebergWriterCompatV3\",\n        \"domainMetadata\",\n        \"rowTracking\")),\n    (\n      testMetadata(tblProps = Map(\n        \"delta.enableIcebergWriterCompatV1\" -> \"true\",\n        \"delta.enableDeletionVectors\" -> \"true\")),\n      new Protocol(2, 5),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\n          \"columnMapping\",\n          \"appendOnly\",\n          \"deletionVectors\",\n          \"invariants\",\n          \"icebergCompatV2\",\n          \"icebergWriterCompatV1\",\n          \"checkConstraints\",\n          \"generatedColumns\",\n          \"changeDataFeed\")),\n      set(\"icebergCompatV2\", \"icebergWriterCompatV1\", \"deletionVectors\")),\n    (\n      testMetadata(tblProps = Map(\"delta.enableIcebergWriterCompatV1\" -> \"true\")),\n      new Protocol(3, 7, set(\"columnMapping\", \"deletionVectors\"), set(\"columnMapping\")),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\"columnMapping\", \"icebergCompatV2\", \"deletionVectors\", \"icebergWriterCompatV1\")),\n      set(\"icebergCompatV2\", \"icebergWriterCompatV1\")),\n    (\n      testMetadata(\n        tblProps = Map(\"delta.enableVariantShredding\" -> \"true\"),\n        includeVariantTypeCol = true),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\"columnMapping\")),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\n          \"columnMapping\",\n          \"deletionVectors\",\n          \"variantShredding\",\n          \"variantType\")),\n      set(\"variantType\", \"variantShredding\"))).foreach {\n    case (newMetadata, currentProtocol, expectedProtocol, expectedNewFeatures) =>\n      test(s\"autoUpgradeProtocolBasedOnMetadata:\" +\n        s\"$currentProtocol -> $expectedProtocol, $expectedNewFeatures\") {\n\n        for (\n          (manualFeatures) <-\n            Seq(\n              Set[TableFeature](),\n              Set(TableFeatures.DOMAIN_METADATA_W_FEATURE),\n              Set(TableFeatures.CLUSTERING_W_FEATURE),\n              Set(TableFeatures.CLUSTERING_W_FEATURE, TableFeatures.DOMAIN_METADATA_W_FEATURE))\n        ) {\n          val newProtocolAndNewFeaturesEnabled = TableFeatures.autoUpgradeProtocolBasedOnMetadata(\n            newMetadata,\n            manualFeatures.asJava,\n            currentProtocol)\n\n          assert(newProtocolAndNewFeaturesEnabled.isPresent, \"expected protocol upgrade\")\n\n          val newProtocol = newProtocolAndNewFeaturesEnabled.get()._1\n          val newFeaturesEnabled = newProtocolAndNewFeaturesEnabled.get()._2\n\n          // Reader version should remain the same\n          assert(newProtocol.getMinReaderVersion == expectedProtocol.getMinReaderVersion)\n\n          // Writer version: upgrade to 7 if domain metadata or clustering feature is enabled\n          val expectedWriterVersion =\n            if (\n              !(manualFeatures & Set(\n                TableFeatures.CLUSTERING_W_FEATURE,\n                TableFeatures.DOMAIN_METADATA_W_FEATURE)).isEmpty\n            ) { 7 }\n            else expectedProtocol.getMinWriterVersion\n          assert(newProtocol.getMinWriterVersion == expectedWriterVersion)\n\n          // Expected enabled features\n          val expectedEnabledFeatures =\n            expectedNewFeatures.asScala ++ manualFeatures.map(_.featureName()).toSet ++ (\n              if (manualFeatures.contains(TableFeatures.CLUSTERING_W_FEATURE)) Set(\"domainMetadata\")\n              else Set.empty\n            )\n          assert(newFeaturesEnabled.asScala.map(_.featureName()).toSet == expectedEnabledFeatures)\n\n          // Expected supported features\n          val implicitAndExplicitFeatures =\n            expectedProtocol.getImplicitlyAndExplicitlySupportedFeatures.asScala\n          val expectedSupportedFeatures =\n            implicitAndExplicitFeatures ++ manualFeatures ++ (\n              if (manualFeatures.contains(TableFeatures.CLUSTERING_W_FEATURE)) {\n                Set(TableFeatures.DOMAIN_METADATA_W_FEATURE)\n              } else { Set.empty }\n            )\n          assert(newProtocol.getImplicitlyAndExplicitlySupportedFeatures.asScala\n            == expectedSupportedFeatures)\n        }\n      }\n  }\n\n  // No-op upgrade\n  Seq(\n    // Test format: new metadata, current protocol\n    (testMetadata(), new Protocol(1, 1)),\n    (\n      // try to enable the writer that is already supported on a protocol\n      // that is of legacy protocol\n      testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"true\")),\n      new Protocol(1, 7, emptySet(), set(\"appendOnly\"))),\n    (\n      // try to enable the writer that is already supported on a protocol\n      // that is of legacy protocol\n      testMetadata(tblProps = Map(\"delta.appendOnly\" -> \"true\", \"delta.constraints.a\" -> \"a = b\")),\n      new Protocol(1, 3)),\n    (\n      // try to enable the reader writer feature that is already supported on a protocol\n      // that is of legacy protocol\n      testMetadata(tblProps = Map(\"delta.columnMapping.mode\" -> \"name\")),\n      new Protocol(2, 5)),\n    (\n      // try to enable the feature that is already supported on a protocol\n      // that is of partial (writer only) table feature support\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV2\" -> \"true\")),\n      new Protocol(2, 7, set(), set(\"columnMapping\", \"icebergCompatV2\"))),\n    (\n      // try to enable the feature that is already supported on a protocol\n      // that is of partial (writer only) table feature support\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV3\" -> \"true\")),\n      new Protocol(\n        2,\n        7,\n        set(),\n        set(\"columnMapping\", \"icebergCompatV3\", \"domainMetadata\", \"rowTracking\"))),\n    (\n      // try to enable the feature that is already supported on a protocol\n      // that is of table feature support\n      testMetadata(tblProps = Map(\"delta.enableIcebergCompatV2\" -> \"true\")),\n      new Protocol(\n        3,\n        7,\n        set(\"columnMapping\", \"deletionVectors\"),\n        set(\"columnMapping\", \"deletionVectors\", \"icebergCompatV2\"))),\n    (\n      // Enable the variantShredding GA feature when the preview feature is already enabled.\n      // The GA feature should not be auto-enabled.\n      testMetadata(\n        tblProps = Map(\"delta.enableVariantShredding\" -> \"true\"),\n        includeVariantTypeCol = true),\n      new Protocol(\n        3,\n        7,\n        set(\"variantType\", \"variantShredding-preview\"),\n        set(\"variantType\", \"variantShredding-preview\")))).foreach {\n    case (newMetadata, currentProtocol) =>\n      test(s\"autoUpgradeProtocolBasedOnMetadata: no-op upgrade: $currentProtocol\") {\n        val newProtocolAndNewFeaturesEnabled =\n          TableFeatures.autoUpgradeProtocolBasedOnMetadata(\n            newMetadata,\n            Set.empty.asJava,\n            currentProtocol)\n        assert(!newProtocolAndNewFeaturesEnabled.isPresent, \"expected no-op upgrade\")\n      }\n  }\n\n  test(\n    \"extractFeaturePropertyOverrides returns feature options and removes from them from metadata\") {\n    val metadata = testMetadata(tblProps = Map(\n      \"delta.feature.deletionVectors\" -> \"supported\",\n      \"delta.feature.appendOnly\" -> \"supported\",\n      \"anotherkey\" -> \"some_value\",\n      \"delta.enableRowTracking\" -> \"true\"))\n\n    val tableFeaturesAndMetadata =\n      TableFeatures.extractFeaturePropertyOverrides(metadata)\n\n    val newFeatures = tableFeaturesAndMetadata._1\n    assert(tableFeaturesAndMetadata._2.isPresent)\n    val newMetadata = tableFeaturesAndMetadata._2.get\n    assert(\n      newFeatures.equals(Set(\n        TableFeatures.APPEND_ONLY_W_FEATURE,\n        TableFeatures.DELETION_VECTORS_RW_FEATURE).asJava),\n      s\"Explicit features: ${newFeatures}\")\n\n    val tableConfig = newMetadata.getConfiguration\n    val expectedMap = Map(\"anotherkey\" -> \"some_value\", \"delta.enableRowTracking\" -> \"true\")\n    assert(expectedMap.asJava.equals(tableConfig), s\"$tableConfig != $expectedMap\")\n  }\n\n  test(\n    \"extractFeaturePropertyOverrides returns empty metadata with no change\") {\n    val metadata = testMetadata(tblProps = Map(\n      \"anotherkey\" -> \"some_value\",\n      \"delta.enableRowTracking\" -> \"true\"))\n\n    val tableFeaturesAndMetadata =\n      TableFeatures.extractFeaturePropertyOverrides(metadata)\n\n    assert(tableFeaturesAndMetadata._1.isEmpty)\n    assert(!tableFeaturesAndMetadata._2.isPresent)\n  }\n\n  Seq(\n    Map(\"delta.feature.deletionVectors\" -> \"not_valid_value\"),\n    Map(\"delta.feature.invalidFeatureName\" -> \"supported\")).foreach {\n    properties =>\n      test(s\"extractFeaturePropertyOverrides throws: $properties\") {\n        intercept[KernelException] {\n          TableFeatures.extractFeaturePropertyOverrides(\n            testMetadata(tblProps = properties))\n        }\n      }\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Test utility methods.                                                                       //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  def checkWriteSupported(\n      testDesc: String,\n      protocol: Protocol,\n      metadata: Metadata = testMetadata()): Unit = {\n    test(testDesc) {\n      validateKernelCanWriteToTable(protocol, metadata, \"/test/table\")\n    }\n  }\n\n  def checkWriteUnsupported(\n      testDesc: String,\n      protocol: Protocol,\n      metadata: Metadata = testMetadata()): Unit = {\n    test(testDesc) {\n      intercept[KernelException] {\n        validateKernelCanWriteToTable(protocol, metadata, \"/test/table\")\n      }\n    }\n  }\n\n  def testMetadata(\n      includeInvariant: Boolean = false,\n      includeTimestampNtzTypeCol: Boolean = false,\n      includeVariantTypeCol: Boolean = false,\n      includeGeneratedColumn: Boolean = false,\n      includeIdentityColumn: Boolean = false,\n      tblProps: Map[String, String] = Map.empty): Metadata = {\n    val testSchema = createTestSchema(\n      includeInvariant,\n      includeTimestampNtzTypeCol,\n      includeVariantTypeCol,\n      includeGeneratedColumn,\n      includeIdentityColumn)\n    new Metadata(\n      \"id\",\n      Optional.of(\"name\"),\n      Optional.of(\"description\"),\n      new Format(\"parquet\", Collections.emptyMap()),\n      testSchema.toJson,\n      testSchema,\n      new ArrayValue() { // partitionColumns\n        override def getSize = 1\n\n        override def getElements: ColumnVector = singletonStringColumnVector(\"c3\")\n      },\n      Optional.empty(),\n      new MapValue() { // conf\n        override def getSize = tblProps.size\n        override def getKeys: ColumnVector =\n          buildColumnVector(tblProps.toSeq.map(_._1).asJava, StringType.STRING)\n        override def getValues: ColumnVector =\n          buildColumnVector(tblProps.toSeq.map(_._2).asJava, StringType.STRING)\n      })\n  }\n\n  def createTestSchema(\n      includeInvariant: Boolean = false,\n      includeTimestampNtzTypeCol: Boolean = false,\n      includeVariantTypeCol: Boolean = false,\n      includeGeneratedColumn: Boolean = false,\n      includeIdentityColumn: Boolean = false): StructType = {\n    var structType = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\"c2\", StringType.STRING)\n    if (includeInvariant) {\n      structType = structType.add(\n        \"c3\",\n        TimestampType.TIMESTAMP,\n        FieldMetadata.builder()\n          .putString(\"delta.invariants\", \"{\\\"expression\\\": { \\\"expression\\\": \\\"x > 3\\\"} }\")\n          .build())\n    }\n    if (includeTimestampNtzTypeCol) {\n      structType = structType.add(\"c4\", TimestampNTZType.TIMESTAMP_NTZ)\n    }\n    if (includeVariantTypeCol) {\n      structType = structType.add(\"c5\", VariantType.VARIANT)\n    }\n    if (includeGeneratedColumn) {\n      structType = structType.add(\n        \"c6\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.generationExpression\", \"{\\\"expression\\\": \\\"c1 + 1\\\"}\")\n          .build())\n    }\n    if (includeIdentityColumn) {\n      structType = structType.add(\n        \"c7\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putLong(\"delta.identity.start\", 1L)\n          .putLong(\"delta.identity.step\", 2L)\n          .putBoolean(\"delta.identity.allowExplicitInsert\", true)\n          .build())\n    }\n\n    structType\n  }\n\n  private def set(elements: String*): java.util.Set[String] = {\n    Set(elements: _*).asJava\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/types/DataTypeJsonSerDeSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.types\n\nimport java.util.HashMap\n\nimport scala.collection.JavaConverters._\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.types._\n\nimport StructField.COLLATIONS_METADATA_KEY\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DataTypeJsonSerDeSuite extends AnyFunSuite {\n\n  import DataTypeJsonSerDeSuite._\n\n  private val objectMapper = new ObjectMapper()\n\n  private def parse(json: String): DataType = {\n    DataTypeJsonSerDe.parseDataType(objectMapper.readTree(json))\n  }\n\n  private def serialize(dataType: DataType): String = {\n    DataTypeJsonSerDe.serializeDataType(dataType)\n  }\n\n  private def testRoundTrip(dataTypeJsonString: String, dataType: DataType): Unit = {\n    // test deserialization\n    assert(parse(dataTypeJsonString) === dataType)\n\n    // test serialization\n    val serializedJson = serialize(dataType)\n    assert(parse(serializedJson) === dataType)\n  }\n\n  private def checkError[T <: Throwable](json: String, expectedErrorContains: String)(implicit\n      classTag: ClassTag[T]): Unit = {\n    val e = intercept[T] {\n      parse(json)\n    }\n    assert(e.getMessage.contains(expectedErrorContains))\n  }\n\n  /* --------------- Primitive data types (stored as a string) ----------------- */\n\n  Seq(\n    (\"\\\"string\\\"\", StringType.STRING),\n    (\"\\\"long\\\"\", LongType.LONG),\n    (\"\\\"short\\\"\", ShortType.SHORT),\n    (\"\\\"integer\\\"\", IntegerType.INTEGER),\n    (\"\\\"boolean\\\"\", BooleanType.BOOLEAN),\n    (\"\\\"byte\\\"\", ByteType.BYTE),\n    (\"\\\"float\\\"\", FloatType.FLOAT),\n    (\"\\\"double\\\"\", DoubleType.DOUBLE),\n    (\"\\\"binary\\\"\", BinaryType.BINARY),\n    (\"\\\"date\\\"\", DateType.DATE),\n    (\"\\\"timestamp\\\"\", TimestampType.TIMESTAMP),\n    (\"\\\"decimal\\\"\", DecimalType.USER_DEFAULT),\n    (\"\\\"decimal(10, 5)\\\"\", new DecimalType(10, 5)),\n    (\"\\\"variant\\\"\", VariantType.VARIANT),\n    (\"\\\"geometry\\\"\", GeometryType.ofDefault()),\n    (\"\\\"geometry(EPSG:0)\\\"\", GeometryType.ofSRID(\"EPSG:0\")),\n    (\"\\\"geography\\\"\", GeographyType.ofDefault()),\n    (\"\\\"geography(EPSG:4326)\\\"\", new GeographyType(\"EPSG:4326\", \"spherical\")),\n    (\"\\\"geography(vincenty)\\\"\", new GeographyType(\"OGC:CRS84\", \"vincenty\")),\n    (\"\\\"geography(EPSG:3857, vincenty)\\\"\", new GeographyType(\"EPSG:3857\", \"vincenty\"))).foreach {\n    case (json, dataType) =>\n      test(\"serialize/deserialize: \" + dataType) {\n        testRoundTrip(json, dataType)\n      }\n  }\n\n  test(\"parseDataType: invalid primitive string data type\") {\n    checkError[IllegalArgumentException](\"\\\"foo\\\"\", \"foo is not a supported delta data type\")\n  }\n\n  test(\"parseDataType: mis-formatted decimal  data type\") {\n    checkError[IllegalArgumentException](\n      \"\\\"decimal(1)\\\"\",\n      \"decimal(1) is not a supported delta data type\")\n  }\n\n  test(\"deserialize: geometry with default SRID\") {\n    // Parsing \"geometry\" should use default SRID (OGC:CRS84)\n    assert(parse(\"\\\"geometry\\\"\") === GeometryType.ofDefault())\n    // But serialization always writes full form\n    assert(serialize(GeometryType.ofDefault()) === \"\\\"geometry(OGC:CRS84)\\\"\")\n  }\n\n  test(\"serialize/deserialize: geometry with various SRID formats\") {\n    // Kernel accepts any SRID format; engine validates compatibility\n    testRoundTrip(\"\\\"geometry(OGC:CRS84)\\\"\", GeometryType.ofSRID(\"OGC:CRS84\"))\n    testRoundTrip(\"\\\"geometry(EPSG:4326)\\\"\", GeometryType.ofSRID(\"EPSG:4326\"))\n    testRoundTrip(\"\\\"geometry(EPSG:0)\\\"\", GeometryType.ofSRID(\"EPSG:0\"))\n    testRoundTrip(\"\\\"geometry(epsg:4326)\\\"\", GeometryType.ofSRID(\"epsg:4326\"))\n  }\n\n  test(\"deserialize: geography with default SRID and algorithm\") {\n    // Parsing \"geography\" should use defaults (OGC:CRS84, spherical)\n    assert(parse(\"\\\"geography\\\"\") === GeographyType.ofDefault())\n    // But serialization always writes full form\n    assert(serialize(GeographyType.ofDefault()) === \"\\\"geography(OGC:CRS84, spherical)\\\"\")\n  }\n\n  test(\"deserialize: geography with SRID and default algorithm\") {\n    // Parsing \"geography(<srid>)\" should use default algorithm (spherical)\n    assert(parse(\"\\\"geography(EPSG:4326)\\\"\") === GeographyType.ofSRID(\"EPSG:4326\"))\n    assert(parse(\"\\\"geography(EPSG:3857)\\\"\") === GeographyType.ofSRID(\"EPSG:3857\"))\n    assert(parse(\"\\\"geography(OGC:CRS84)\\\"\") === GeographyType.ofSRID(\"OGC:CRS84\"))\n    // But serialization always writes full form\n    assert(serialize(GeographyType.ofSRID(\"EPSG:4326\")) === \"\\\"geography(EPSG:4326, spherical)\\\"\")\n  }\n\n  test(\"deserialize: geography with default SRID and specified algorithm\") {\n    // Parsing \"geography(<algorithm>)\" should use default SRID (OGC:CRS84)\n    assert(parse(\"\\\"geography(spherical)\\\"\") === GeographyType.ofAlgorithm(\"spherical\"))\n    assert(parse(\"\\\"geography(vincenty)\\\"\") === GeographyType.ofAlgorithm(\"vincenty\"))\n    assert(parse(\"\\\"geography(andoyer)\\\"\") === GeographyType.ofAlgorithm(\"andoyer\"))\n    // But serialization always writes full form\n    assert(\n      serialize(GeographyType.ofAlgorithm(\"vincenty\")) ===\n        \"\\\"geography(OGC:CRS84, vincenty)\\\"\")\n  }\n\n  test(\"serialize/deserialize: geography with both SRID and algorithm\") {\n    // Both SRID and algorithm specified - round trips correctly\n    testRoundTrip(\n      \"\\\"geography(EPSG:4326, spherical)\\\"\",\n      new GeographyType(\"EPSG:4326\", \"spherical\"))\n    testRoundTrip(\n      \"\\\"geography(EPSG:3857, vincenty)\\\"\",\n      new GeographyType(\"EPSG:3857\", \"vincenty\"))\n    testRoundTrip(\n      \"\\\"geography(OGC:CRS84, andoyer)\\\"\",\n      new GeographyType(\"OGC:CRS84\", \"andoyer\"))\n    assert(\n      serialize(new GeographyType(\"EPSG:4326\", \"vincenty\")) ===\n        \"\\\"geography(EPSG:4326, vincenty)\\\"\")\n  }\n\n  test(\"parseDataType: invalid geometry formats\") {\n    // Missing SRID parameter\n    checkError[IllegalArgumentException](\n      \"\\\"geometry()\\\"\",\n      \"geometry() is not a supported delta data type\")\n    // Invalid format with multiple parameters\n    checkError[IllegalArgumentException](\n      \"\\\"geometry(EPSG:4326, extra)\\\"\",\n      \"geometry(EPSG:4326, extra) is not a supported delta data type\")\n    checkError[IllegalArgumentException](\n      \"\\\"geometry(noCollon)\\\"\",\n      \"geometry(noCollon) is not a supported delta data type\")\n  }\n\n  test(\"parseDataType: invalid geography formats\") {\n    // Empty parameters\n    checkError[IllegalArgumentException](\n      \"\\\"geography()\\\"\",\n      \"geography() is not a supported delta data type\")\n    // Too many parameters\n    checkError[IllegalArgumentException](\n      \"\\\"geography(EPSG:4326, spherical, extra)\\\"\",\n      \"geography(EPSG:4326, spherical, extra) is not a supported delta data type\")\n    // Invalid format\n    checkError[IllegalArgumentException](\n      \"\\\"geography(EPSG:4326,)\\\"\",\n      \"geography(EPSG:4326,) is not a supported delta data type\")\n  }\n\n  test(\"parseDataType: invalid geography algorithm\") {\n    val expectedMsg = \"Algorithm must be one of: spherical, vincenty, thomas, andoyer, karney\"\n    checkError[IllegalArgumentException](\n      \"\\\"geography(EPSG:4326, planar)\\\"\",\n      expectedMsg)\n    checkError[IllegalArgumentException](\n      \"\\\"geography(haversine)\\\"\",\n      expectedMsg)\n  }\n\n  /* ---------------  Complex types ----------------- */\n\n  test(\"serialize/deserialize: array type\") {\n    for (containsNull <- Seq(true, false)) {\n      for ((elementJson, elementType) <- SAMPLE_JSON_TO_TYPES) {\n        // test deserialization\n        val json = arrayTypeJson(elementJson, containsNull)\n        val expectedType = new ArrayType(elementType, containsNull)\n\n        testRoundTrip(json, expectedType)\n      }\n    }\n  }\n\n  test(\"serialize/deserialize: map type\") {\n    for (valueContainsNull <- Seq(true, false)) {\n      for ((keyJson, keyType) <- SAMPLE_JSON_TO_TYPES) {\n        for ((valueJson, valueType) <- SAMPLE_JSON_TO_TYPES) {\n          val json = mapTypeJson(keyJson, valueJson, valueContainsNull)\n          val expectedType = new MapType(keyType, valueType, valueContainsNull)\n\n          testRoundTrip(json, expectedType)\n        }\n      }\n    }\n  }\n\n  test(\"serialize/deserialize: struct type\") {\n    for ((col1Json, col1Type) <- SAMPLE_JSON_TO_TYPES) {\n      for ((col2Json, col2Type) <- SAMPLE_JSON_TO_TYPES) {\n        val fieldsJson = Seq(\n          structFieldJson(\"col1\", col1Json, false),\n          structFieldJson(\"col2\", col2Json, true, Some(\"{ \\\"int\\\" : 0 }\")))\n\n        val json = structTypeJson(fieldsJson)\n        val expectedType = new StructType()\n          .add(\"col1\", col1Type, false)\n          .add(\"col2\", col2Type, true, FieldMetadata.builder().putLong(\"int\", 0).build())\n\n        testRoundTrip(json, expectedType)\n      }\n    }\n  }\n\n  test(\"serialize/deserialize: complex types\") {\n    SAMPLE_COMPLEX_JSON_TO_TYPES\n      .foreach {\n        case (json, dataType) =>\n          testRoundTrip(json, dataType)\n      }\n  }\n\n  test(\"serialize/deserialize: types with collated strings\") {\n    SAMPLE_JSON_TO_TYPES_WITH_COLLATION\n      .foreach {\n        case (json, dataType) =>\n          testRoundTrip(json, dataType)\n      }\n  }\n\n  test(\"deserialize: schema with collated map key throws IllegalArgumentException\") {\n    // A JSON schema encoding a MapType with a collated StringType key should fail\n    // deserialization because MapType rejects collated keys.\n    val json = structTypeJson(Seq(\n      structFieldJson(\n        \"m\",\n        mapTypeJson(\"\\\"string\\\"\", \"\\\"integer\\\"\", false),\n        true,\n        metadataJson = Some(\n          s\"\"\"{\"$COLLATIONS_METADATA_KEY\" : {\"m.key\" : \"ICU.UNICODE_CI\"}}\"\"\"))))\n\n    intercept[IllegalArgumentException] {\n      parse(json)\n    }\n  }\n\n  test(\"serialize/deserialize: parsed and original struct\" +\n    \" type differing just in StringType collation\") {\n    val json = structTypeJson(Seq(\n      structFieldJson(\n        \"a1\",\n        \"\\\"string\\\"\",\n        true,\n        metadataJson = Some(\n          s\"\"\"{\"$COLLATIONS_METADATA_KEY\" :\n             | {\"a1\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin))))\n    val dataType = new StructType()\n      .add(\"a1\", new StringType(\"ICU.UNICODE\"), true)\n\n    assert(!(parse(json) === dataType))\n  }\n\n  test(\"serialize/deserialize: round-trip type changes metadata\") {\n    // Test cases for various type changes based on Delta Protocol specification\n\n    // Case 1: Simple type widening (short -> integer -> long)\n    val simpleTypeChangeJson = structTypeJson(Seq(\n      structFieldJson(\n        \"e\",\n        \"\\\"long\\\"\",\n        true,\n        metadataJson = Some(\n          \"\"\"{\n            |\"delta.typeChanges\": [\n            |  {\n            |    \"fromType\": \"short\",\n            |    \"toType\": \"integer\"\n            |  },\n            |  {\n            |    \"fromType\": \"integer\",\n            |    \"toType\": \"long\"\n            |  }\n            |]\n            |}\"\"\".stripMargin))))\n\n    val simpleTypeChangeDataType = new StructType()\n      .add(new StructField(\"e\", LongType.LONG, true).withTypeChanges(\n        Seq(\n          new TypeChange(ShortType.SHORT, IntegerType.INTEGER),\n          new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava))\n\n    testRoundTrip(simpleTypeChangeJson, simpleTypeChangeDataType)\n\n    // Case 2: Map key type change (float -> double)\n    val mapKeyTypeChangeJson = structTypeJson(Seq(\n      structFieldJson(\n        \"e\",\n        mapTypeJson(\"\\\"double\\\"\", \"\\\"integer\\\"\", true),\n        true,\n        metadataJson = Some(\n          \"\"\"{\n            |\"delta.typeChanges\": [\n            |  {\n            |    \"fromType\": \"float\",\n            |    \"toType\": \"double\",\n            |    \"fieldPath\": \"key\"\n            |  }\n            |]\n            |}\"\"\".stripMargin))))\n\n    val mapKeyTypeChangeDataType = new StructType()\n      .add(new StructField(\n        \"e\",\n        new MapType(\n          new StructField(\"key\", DoubleType.DOUBLE, false)\n            .withTypeChanges(Seq(new TypeChange(FloatType.FLOAT, DoubleType.DOUBLE)).asJava),\n          new StructField(\"value\", IntegerType.INTEGER, true)),\n        true))\n\n    testRoundTrip(mapKeyTypeChangeJson, mapKeyTypeChangeDataType)\n\n    // Case 3: Nested map value in array type change (decimal scale change)\n    val nestedTypeChangeJson = structTypeJson(Seq(\n      structFieldJson(\n        \"e\",\n        arrayTypeJson(\n          mapTypeJson(\"\\\"string\\\"\", \"\\\"decimal(10,4)\\\"\", true),\n          true),\n        true,\n        metadataJson = Some(\n          \"\"\"{\n            |\"delta.typeChanges\": [\n            |  {\n            |    \"fromType\": \"decimal(6,2)\",\n            |    \"toType\": \"decimal(10,4)\",\n            |    \"fieldPath\": \"element.value\"\n            |  }\n            |]\n            |}\"\"\".stripMargin))))\n\n    val nestedTypeChangeDataType = new StructType()\n      .add(\n        \"e\",\n        new ArrayType(\n          new MapType(\n            new StructField(\"key\", StringType.STRING, false),\n            new StructField(\"value\", new DecimalType(10, 4), true)\n              .withTypeChanges(\n                Seq(new TypeChange(new DecimalType(6, 2), new DecimalType(10, 4))).asJava)),\n          true),\n        true)\n\n    testRoundTrip(nestedTypeChangeJson, nestedTypeChangeDataType)\n\n    // Case 4: Combined type changes and collations\n    val combinedJson = structTypeJson(Seq(\n      structFieldJson(\n        \"tags\",\n        mapTypeJson(\"\\\"string\\\"\", \"\\\"string\\\"\", false),\n        true,\n        metadataJson = Some(\n          s\"\"\"{\n             |\"$COLLATIONS_METADATA_KEY\": {\n             |  \"tags.value\": \"ICU.UNICODE\"\n             |},\n             |\"delta.typeChanges\": [\n             |  {\n             |    \"fromType\": \"binary\",\n             |    \"toType\": \"string\",\n             |    \"fieldPath\": \"value\"\n             |  }\n             |]\n             |}\"\"\".stripMargin))))\n\n    val combinedDataType = new StructType()\n      .add(\n        \"tags\",\n        new MapType(\n          new StructField(\"key\", StringType.STRING, false),\n          new StructField(\"value\", new StringType(\"ICU.UNICODE\"), false)\n            .withTypeChanges(Seq(new TypeChange(BinaryType.BINARY, StringType.STRING)).asJava)),\n        true)\n\n    testRoundTrip(combinedJson, combinedDataType)\n\n    // Case 5: Deeply nested maps\n    val deeplyNestedMapJson = structTypeJson(Seq(\n      structFieldJson(\n        \"tags\",\n        mapTypeJson(\n          /* key= */ mapTypeJson(\"\\\"integer\\\"\", \"\\\"integer\\\"\", false),\n          /* value= */ mapTypeJson(\"\\\"long\\\"\", \"\\\"long\\\"\", false),\n          false),\n        true,\n        metadataJson = Some(\n          s\"\"\"{\n             |\"delta.typeChanges\": [\n             |  {\n             |    \"fromType\": \"byte\",\n             |    \"toType\": \"integer\",\n             |    \"fieldPath\": \"key.key\"\n             |  },\n             |  {\n             |    \"fromType\": \"byte\",\n             |    \"toType\": \"short\",\n             |    \"fieldPath\": \"key.value\"\n             |  },\n             |  {\n             |    \"fromType\": \"short\",\n             |    \"toType\": \"integer\",\n             |    \"fieldPath\": \"key.value\"\n             |  },\n             |  {\n             |    \"fromType\": \"short\",\n             |    \"toType\": \"long\",\n             |    \"fieldPath\": \"value.key\"\n             |  },\n             |  {\n             |    \"fromType\": \"byte\",\n             |    \"toType\": \"long\",\n             |    \"fieldPath\": \"value.value\"\n             |  }]\n             |}\"\"\".stripMargin))))\n\n    val deeplyNestedMapType = new StructType()\n      .add(\n        \"tags\",\n        new MapType(\n          new StructField(\n            \"key\",\n            new MapType(\n              new StructField(\"key\", IntegerType.INTEGER, false)\n                .withTypeChanges(Seq(new TypeChange(ByteType.BYTE, IntegerType.INTEGER)).asJava),\n              new StructField(\"value\", IntegerType.INTEGER, false)\n                .withTypeChanges(\n                  Seq(\n                    new TypeChange(ByteType.BYTE, ShortType.SHORT),\n                    new TypeChange(ShortType.SHORT, IntegerType.INTEGER)).asJava)),\n            false),\n          new StructField(\n            \"value\",\n            new MapType(\n              new StructField(\"key\", LongType.LONG, false)\n                .withTypeChanges(Seq(new TypeChange(ShortType.SHORT, LongType.LONG)).asJava),\n              new StructField(\"value\", LongType.LONG, false)\n                .withTypeChanges(Seq(new TypeChange(ByteType.BYTE, LongType.LONG)).asJava)),\n            false)),\n        true);\n\n    testRoundTrip(deeplyNestedMapJson, deeplyNestedMapType)\n  }\n\n  test(\"serialize/deserialize: geometry and geography as nested types\") {\n    // Array of geometry\n    testRoundTrip(\n      arrayTypeJson(\"\\\"geometry(OGC:CRS84)\\\"\", false),\n      new ArrayType(GeometryType.ofDefault(), false))\n\n    // Array of geography with non-default SRID and algorithm\n    testRoundTrip(\n      arrayTypeJson(\"\\\"geography(EPSG:4326, vincenty)\\\"\", true),\n      new ArrayType(new GeographyType(\"EPSG:4326\", \"vincenty\"), true))\n\n    // Struct with geometry and geography fields\n    testRoundTrip(\n      structTypeJson(Seq(\n        structFieldJson(\"geom\", \"\\\"geometry(EPSG:4326)\\\"\", false),\n        structFieldJson(\"geog\", \"\\\"geography(EPSG:3857, spherical)\\\"\", true))),\n      new StructType()\n        .add(\"geom\", GeometryType.ofSRID(\"EPSG:4326\"), false)\n        .add(\"geog\", new GeographyType(\"EPSG:3857\", \"spherical\"), true))\n\n    // Struct containing arrays of geometry and geography\n    testRoundTrip(\n      structTypeJson(Seq(\n        structFieldJson(\"geoms\", arrayTypeJson(\"\\\"geometry(OGC:CRS84)\\\"\", false), true),\n        structFieldJson(\n          \"geogs\",\n          arrayTypeJson(\"\\\"geography(OGC:CRS84, vincenty)\\\"\", true),\n          false))),\n      new StructType()\n        .add(\"geoms\", new ArrayType(GeometryType.ofDefault(), false), true)\n        .add(\"geogs\", new ArrayType(new GeographyType(\"OGC:CRS84\", \"vincenty\"), true), false))\n  }\n\n  test(\"serialize/deserialize: special characters for column name\") {\n    val json = structTypeJson(Seq(\n      structFieldJson(\"@_! *c\", \"\\\"string\\\"\", true)))\n    val expectedType = new StructType()\n      .add(\"@_! *c\", StringType.STRING, true)\n\n    testRoundTrip(json, expectedType)\n  }\n\n  test(\"serialize/deserialize: empty struct type\") {\n    val str =\n      \"\"\"\n        |{\n        |  \"type\" : \"struct\",\n        |  \"fields\": []\n        |}\n        |\"\"\".stripMargin\n    testRoundTrip(str, new StructType())\n  }\n\n  test(\"serialize/deserialize: parsing FieldMetadata\") {\n    def testFieldMetadata(fieldMetadataJson: String, expectedFieldMetadata: FieldMetadata): Unit = {\n      val json = structTypeJson(Seq(\n        structFieldJson(\"testCol\", \"\\\"string\\\"\", true, Some(fieldMetadataJson))))\n\n      val dataType = new StructType().add(\"testCol\", StringType.STRING, true, expectedFieldMetadata)\n\n      testRoundTrip(json, dataType)\n    }\n\n    val fieldMetadataAllTypesJson =\n      \"\"\"\n        |{\n        |  \"null\" : null,\n        |  \"int\" : 10,\n        |  \"long-1\" : -16070400023423400,\n        |  \"long-2\" : 16070400023423400,\n        |  \"double\" : 2.22,\n        |  \"boolean\" : true,\n        |  \"string\" : \"10\\\"@\",\n        |  \"metadata\" : { \"nestedInt\" : 200 },\n        |  \"empty_arr\" : [],\n        |  \"int_arr\" : [1, 2, 0],\n        |  \"double_arr\" : [1.0, 2.0, 3.0],\n        |  \"boolean_arr\" : [true],\n        |  \"string_arr\" : [\"one\", \"two\"],\n        |  \"metadata_arr\" : [{ \"one\" : 1, \"two\" : true }, {}]\n        |}\n        |\"\"\".stripMargin\n    val expectedFieldMetadataAllTypes = FieldMetadata.builder()\n      .putNull(\"null\")\n      .putLong(\"int\", 10)\n      .putLong(\"long-1\", -16070400023423400L)\n      .putLong(\"long-2\", 16070400023423400L)\n      .putDouble(\"double\", 2.22)\n      .putBoolean(\"boolean\", true)\n      .putString(\"string\", \"10\\\"@\") // special characters\n      .putFieldMetadata(\"metadata\", FieldMetadata.builder().putLong(\"nestedInt\", 200).build())\n      .putLongArray(\"empty_arr\", Array())\n      .putLongArray(\"int_arr\", Array(1, 2, 0))\n      .putDoubleArray(\"double_arr\", Array(1.0, 2.0, 3.0))\n      .putBooleanArray(\"boolean_arr\", Array(true))\n      .putStringArray(\"string_arr\", Array(\"one\", \"two\"))\n      .putFieldMetadataArray(\n        \"metadata_arr\",\n        Array(\n          FieldMetadata.builder().putLong(\"one\", 1).putBoolean(\"two\", true).build(),\n          FieldMetadata.empty()))\n      .build()\n\n    testFieldMetadata(fieldMetadataAllTypesJson, expectedFieldMetadataAllTypes)\n    testFieldMetadata(\"{}\", FieldMetadata.empty())\n  }\n\n  test(\"parseDataType: invalid field for type\") {\n    checkError[IllegalArgumentException](\n      \"\"\"\n        |{\n        |  \"type\" : \"foo\",\n        |  \"two\" : \"val2\"\n        |}\n        |\"\"\".stripMargin,\n      \"Could not parse the following JSON as a valid Delta data type\")\n  }\n\n  test(\"parseDataType: not a valid JSON node (not a string or object)\") {\n    checkError[IllegalArgumentException](\n      \"0\",\n      \"Could not parse the following JSON as a valid Delta data type\")\n  }\n}\n\nobject DataTypeJsonSerDeSuite {\n\n  val SAMPLE_JSON_TO_TYPES = Seq(\n    (\"\\\"string\\\"\", StringType.STRING),\n    (\"\\\"integer\\\"\", IntegerType.INTEGER),\n    (\"\\\"variant\\\"\", VariantType.VARIANT),\n    (arrayTypeJson(\"\\\"string\\\"\", true), new ArrayType(StringType.STRING, true)),\n    (\n      mapTypeJson(\"\\\"integer\\\"\", \"\\\"string\\\"\", true),\n      new MapType(IntegerType.INTEGER, StringType.STRING, true)),\n    (\n      structTypeJson(Seq(\n        structFieldJson(\"col1\", \"\\\"string\\\"\", true),\n        structFieldJson(\"col2\", \"\\\"string\\\"\", false, Some(\"{ \\\"int\\\" : 0 }\")),\n        structFieldJson(\"col3\", \"\\\"variant\\\"\", false))),\n      new StructType()\n        .add(\"col1\", StringType.STRING, true)\n        .add(\"col2\", StringType.STRING, false, FieldMetadata.builder().putLong(\"int\", 0).build())\n        .add(\"col3\", VariantType.VARIANT, false)))\n\n  val SAMPLE_COMPLEX_JSON_TO_TYPES = Seq(\n    (\n      structTypeJson(Seq(\n        structFieldJson(\"c1\", \"\\\"binary\\\"\", true),\n        structFieldJson(\"c2\", \"\\\"boolean\\\"\", false),\n        structFieldJson(\"c3\", \"\\\"byte\\\"\", false),\n        structFieldJson(\"c4\", \"\\\"date\\\"\", true),\n        structFieldJson(\"c5\", \"\\\"decimal(10,0)\\\"\", false),\n        structFieldJson(\"c6\", \"\\\"double\\\"\", false),\n        structFieldJson(\"c7\", \"\\\"float\\\"\", false),\n        structFieldJson(\"c8\", \"\\\"integer\\\"\", true),\n        structFieldJson(\"c9\", \"\\\"long\\\"\", true),\n        structFieldJson(\"c10\", \"\\\"short\\\"\", true),\n        structFieldJson(\"c11\", \"\\\"string\\\"\", true),\n        structFieldJson(\"c12\", \"\\\"timestamp_ntz\\\"\", false),\n        structFieldJson(\"c13\", \"\\\"timestamp\\\"\", false),\n        structFieldJson(\"c14\", \"\\\"variant\\\"\", false))),\n      new StructType()\n        .add(\"c1\", BinaryType.BINARY, true)\n        .add(\"c2\", BooleanType.BOOLEAN, false)\n        .add(\"c3\", ByteType.BYTE, false)\n        .add(\"c4\", DateType.DATE, true)\n        .add(\"c5\", DecimalType.USER_DEFAULT, false)\n        .add(\"c6\", DoubleType.DOUBLE, false)\n        .add(\"c7\", FloatType.FLOAT, false)\n        .add(\"c8\", IntegerType.INTEGER, true)\n        .add(\"c9\", LongType.LONG, true)\n        .add(\"c10\", ShortType.SHORT, true)\n        .add(\"c11\", StringType.STRING, true)\n        .add(\"c12\", TimestampNTZType.TIMESTAMP_NTZ, false)\n        .add(\"c13\", TimestampType.TIMESTAMP, false)\n        .add(\"c14\", VariantType.VARIANT, false)),\n    (\n      structTypeJson(Seq(\n        structFieldJson(\"a1\", \"\\\"string\\\"\", true),\n        structFieldJson(\n          \"a2\",\n          structTypeJson(Seq(\n            structFieldJson(\n              \"b1\",\n              mapTypeJson(\n                arrayTypeJson(\n                  arrayTypeJson(\n                    \"\\\"string\\\"\",\n                    true),\n                  true),\n                structTypeJson(Seq(\n                  structFieldJson(\"c1\", \"\\\"string\\\"\", false),\n                  structFieldJson(\"c2\", \"\\\"string\\\"\", true))),\n                true),\n              true),\n            structFieldJson(\"b2\", \"\\\"long\\\"\", true))),\n          true),\n        structFieldJson(\n          \"a3\",\n          arrayTypeJson(\n            mapTypeJson(\n              \"\\\"string\\\"\",\n              structTypeJson(Seq(\n                structFieldJson(\"b1\", \"\\\"date\\\"\", false))),\n              false),\n            false),\n          true))),\n      new StructType()\n        .add(\"a1\", StringType.STRING, true)\n        .add(\n          \"a2\",\n          new StructType()\n            .add(\n              \"b1\",\n              new MapType(\n                new ArrayType(\n                  new ArrayType(StringType.STRING, true),\n                  true),\n                new StructType()\n                  .add(\"c1\", StringType.STRING, false)\n                  .add(\"c2\", StringType.STRING, true),\n                true))\n            .add(\"b2\", LongType.LONG),\n          true)\n        .add(\n          \"a3\",\n          new ArrayType(\n            new MapType(\n              StringType.STRING,\n              new StructType()\n                .add(\"b1\", DateType.DATE, false),\n              false),\n            false),\n          true)))\n\n  val SAMPLE_JSON_TO_TYPES_WITH_COLLATION = Seq(\n    (\n      structTypeJson(Seq(\n        structFieldJson(\n          \"a1\",\n          \"\\\"string\\\"\",\n          true,\n          metadataJson = Some(s\"\"\"{\"$COLLATIONS_METADATA_KEY\" : {\"a1\" : \"ICU.UNICODE\"}}\"\"\")),\n        structFieldJson(\"a2\", \"\\\"integer\\\"\", false),\n        structFieldJson(\n          \"a3\",\n          \"\\\"string\\\"\",\n          false,\n          metadataJson = Some(s\"\"\"{\"$COLLATIONS_METADATA_KEY\" : {\"a3\" : \"SPARK.UTF8_LCASE\"}}\"\"\")),\n        structFieldJson(\"a4\", \"\\\"string\\\"\", true))),\n      new StructType()\n        .add(\"a1\", new StringType(\"ICU.UNICODE\"), true)\n        .add(\"a2\", IntegerType.INTEGER, false)\n        .add(\"a3\", new StringType(\"SPARK.UTF8_LCASE\"), false)\n        .add(\"a4\", StringType.STRING, true)),\n    (\n      structTypeJson(Seq(\n        structFieldJson(\n          \"a1\",\n          structTypeJson(Seq(\n            structFieldJson(\n              \"b1\",\n              \"\\\"string\\\"\",\n              true,\n              metadataJson = Some(\n                s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                 | : {\"b1\" : \"ICU.UNICODE\"}}\"\"\".stripMargin)))),\n          true),\n        structFieldJson(\n          \"a2\",\n          structTypeJson(Seq(\n            structFieldJson(\n              \"b1\",\n              arrayTypeJson(\"\\\"string\\\"\", false),\n              true,\n              metadataJson = Some(\n                s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                 | : {\"b1.element\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)),\n            structFieldJson(\n              \"b2\",\n              mapTypeJson(\"\\\"string\\\"\", \"\\\"string\\\"\", true),\n              false,\n              metadataJson = Some(\n                s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                 | : {\"b2.value\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)),\n            structFieldJson(\"b3\", arrayTypeJson(\"\\\"string\\\"\", false), true),\n            structFieldJson(\"b4\", mapTypeJson(\"\\\"string\\\"\", \"\\\"string\\\"\", false), false))),\n          true),\n        structFieldJson(\n          \"a3\",\n          structTypeJson(Seq(\n            structFieldJson(\"b1\", \"\\\"string\\\"\", false),\n            structFieldJson(\"b2\", arrayTypeJson(\"\\\"integer\\\"\", false), true))),\n          false,\n          metadataJson = Some(\n            s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n               | : {\"b1\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)))),\n      new StructType()\n        .add(\n          \"a1\",\n          new StructType()\n            .add(\"b1\", new StringType(\"ICU.UNICODE\")),\n          true)\n        .add(\n          \"a2\",\n          new StructType()\n            .add(\"b1\", new ArrayType(new StringType(\"SPARK.UTF8_LCASE\"), false))\n            .add(\n              \"b2\",\n              new MapType(\n                StringType.STRING,\n                new StringType(\"SPARK.UTF8_LCASE\"),\n                true),\n              false)\n            .add(\"b3\", new ArrayType(StringType.STRING, false))\n            .add(\n              \"b4\",\n              new MapType(\n                StringType.STRING,\n                StringType.STRING,\n                false),\n              false),\n          true)\n        .add(\n          \"a3\",\n          new StructType()\n            .add(\"b1\", StringType.STRING, false)\n            .add(\"b2\", new ArrayType(IntegerType.INTEGER, false), true),\n          false)),\n    (\n      structTypeJson(Seq(\n        structFieldJson(\"a1\", \"\\\"string\\\"\", true),\n        structFieldJson(\n          \"a2\",\n          structTypeJson(Seq(\n            structFieldJson(\n              \"b1\",\n              mapTypeJson(\n                arrayTypeJson(arrayTypeJson(\"\\\"string\\\"\", true), true),\n                structTypeJson(Seq(\n                  structFieldJson(\n                    \"c1\",\n                    \"\\\"string\\\"\",\n                    false,\n                    metadataJson = Some(\n                      s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                     | : {\"c1\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)),\n                  structFieldJson(\n                    \"c2\",\n                    \"\\\"string\\\"\",\n                    true,\n                    metadataJson = Some(\n                      s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                     | : {\\\"c2\\\" : \\\"ICU.UNICODE\\\"}}\"\"\".stripMargin)),\n                  structFieldJson(\"c3\", \"\\\"string\\\"\", true))),\n                true),\n              true,\n              metadataJson = Some(\n                s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n               | : {\"b1.key.element.element\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)),\n            structFieldJson(\"b2\", \"\\\"long\\\"\", true))),\n          true),\n        structFieldJson(\n          \"a3\",\n          arrayTypeJson(\n            mapTypeJson(\n              \"\\\"string\\\"\",\n              structTypeJson(Seq(\n                structFieldJson(\n                  \"b1\",\n                  \"\\\"string\\\"\",\n                  false,\n                  metadataJson = Some(\n                    s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                     | : {\"b1\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)))),\n              false),\n            false),\n          true),\n        structFieldJson(\n          \"a4\",\n          arrayTypeJson(\n            structTypeJson(Seq(\n              structFieldJson(\n                \"b1\",\n                \"\\\"string\\\"\",\n                false,\n                metadataJson = Some(\n                  s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                   | : {\"b1\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)))),\n            false),\n          false),\n        structFieldJson(\n          \"a5\",\n          mapTypeJson(\n            structTypeJson(Seq(\n              structFieldJson(\n                \"b1\",\n                \"\\\"string\\\"\",\n                false,\n                metadataJson = Some(\n                  s\"\"\"{\"$COLLATIONS_METADATA_KEY\"\n                 | : {\"b1\" : \"SPARK.UTF8_LCASE\"}}\"\"\".stripMargin)))),\n            \"\\\"string\\\"\",\n            false),\n          false))),\n      new StructType()\n        .add(\"a1\", StringType.STRING, true)\n        .add(\n          \"a2\",\n          new StructType()\n            .add(\n              \"b1\",\n              new MapType(\n                new ArrayType(\n                  new ArrayType(\n                    new StringType(\"SPARK.UTF8_LCASE\"),\n                    true),\n                  true),\n                new StructType()\n                  .add(\"c1\", new StringType(\"SPARK.UTF8_LCASE\"), false)\n                  .add(\"c2\", new StringType(\"ICU.UNICODE\"), true)\n                  .add(\"c3\", StringType.STRING),\n                true))\n            .add(\"b2\", LongType.LONG),\n          true)\n        .add(\n          \"a3\",\n          new ArrayType(\n            new MapType(\n              StringType.STRING,\n              new StructType()\n                .add(\"b1\", new StringType(\"SPARK.UTF8_LCASE\"), false),\n              false),\n            false),\n          true)\n        .add(\n          \"a4\",\n          new ArrayType(\n            new StructType()\n              .add(\"b1\", new StringType(\"SPARK.UTF8_LCASE\"), false),\n            false),\n          false)\n        .add(\n          \"a5\",\n          new MapType(\n            new StructType()\n              .add(\"b1\", new StringType(\"SPARK.UTF8_LCASE\"), false),\n            StringType.STRING,\n            false),\n          false)))\n\n  def arrayTypeJson(elementJson: String, containsNull: Boolean): String = {\n    s\"\"\"\n       |{\n       |  \"type\" : \"array\",\n       |  \"elementType\" : $elementJson,\n       |  \"containsNull\" : $containsNull\n       |}\n       |\"\"\".stripMargin\n  }\n\n  def mapTypeJson(keyJson: String, valueJson: String, valueContainsNull: Boolean): String = {\n    s\"\"\"\n       |{\n       |  \"type\" : \"map\",\n       |  \"keyType\" : $keyJson,\n       |  \"valueType\" : $valueJson,\n       |  \"valueContainsNull\" : $valueContainsNull\n       |}\n       |\"\"\".stripMargin\n  }\n\n  def structFieldJson(\n      name: String,\n      typeJson: String,\n      nullable: Boolean,\n      metadataJson: Option[String] = None): String = {\n    metadataJson match {\n      case Some(metadata) =>\n        s\"\"\"\n           |{\n           |  \"name\" : \"$name\",\n           |  \"type\" : $typeJson,\n           |  \"nullable\" : $nullable,\n           |  \"metadata\" : $metadata\n           |}\n           |\"\"\".stripMargin\n      case None =>\n        s\"\"\"\n           |{\n           |  \"name\" : \"$name\",\n           |  \"type\" : $typeJson,\n           |  \"nullable\" : $nullable\n           |}\n           |\"\"\".stripMargin\n    }\n  }\n\n  def structTypeJson(fieldsJsons: Seq[String]): String = {\n    s\"\"\"\n       |{\n       |  \"type\" : \"struct\",\n       |  \"fields\": ${fieldsJsons.mkString(\"[\\n\", \",\\n\", \"]\\n\")}\n       |}\n       |\"\"\".stripMargin\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/types/TypeWideningCheckerSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.types\n\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Test suite for the TypeWideningChecker class. */\nclass TypeWideningCheckerSuite extends AnyFunSuite {\n\n  test(\"same type is allowed\") {\n    // Same types should not be considered widening\n    assert(TypeWideningChecker.isWideningSupported(IntegerType.INTEGER, IntegerType.INTEGER))\n    assert(TypeWideningChecker.isWideningSupported(StringType.STRING, StringType.STRING))\n    assert(TypeWideningChecker.isWideningSupported(\n      new DecimalType(10, 2),\n      new DecimalType(10, 2)))\n  }\n\n  test(\"integer widening is supported\") {\n    assert(TypeWideningChecker.isWideningSupported(ByteType.BYTE, ShortType.SHORT))\n    assert(TypeWideningChecker.isWideningSupported(ByteType.BYTE, IntegerType.INTEGER))\n    assert(TypeWideningChecker.isWideningSupported(ByteType.BYTE, LongType.LONG))\n    assert(TypeWideningChecker.isWideningSupported(ShortType.SHORT, IntegerType.INTEGER))\n    assert(TypeWideningChecker.isWideningSupported(ShortType.SHORT, LongType.LONG))\n    assert(TypeWideningChecker.isWideningSupported(IntegerType.INTEGER, LongType.LONG))\n  }\n\n  test(\"integer type narrowing is not supported\") {\n    assert(!TypeWideningChecker.isWideningSupported(LongType.LONG, IntegerType.INTEGER))\n    assert(!TypeWideningChecker.isWideningSupported(IntegerType.INTEGER, ShortType.SHORT))\n    assert(!TypeWideningChecker.isWideningSupported(ShortType.SHORT, ByteType.BYTE))\n  }\n\n  test(\"float to double widening\") {\n    assert(TypeWideningChecker.isWideningSupported(FloatType.FLOAT, DoubleType.DOUBLE))\n  }\n\n  test(\"double to float is not supported\") {\n    assert(!TypeWideningChecker.isWideningSupported(DoubleType.DOUBLE, FloatType.FLOAT))\n  }\n\n  test(\"integer to double widening supported\") {\n    Seq(ByteType.BYTE, ShortType.SHORT, IntegerType.INTEGER) foreach { t =>\n      assert(TypeWideningChecker.isWideningSupported(t, DoubleType.DOUBLE))\n    }\n  }\n\n  test(\"unsupported integral to floating point no supported\") {\n    // Test invalid integer to double widening\n    assert(!TypeWideningChecker.isWideningSupported(LongType.LONG, DoubleType.DOUBLE))\n    Seq(ByteType.BYTE, ShortType.SHORT, IntegerType.INTEGER) foreach { t =>\n      assert(!TypeWideningChecker.isWideningSupported(t, FloatType.FLOAT))\n    }\n  }\n\n  test(\"date to timestamp NTZ widening\") {\n    // Test Date -> TimestampNTZ widening\n    assert(TypeWideningChecker.isWideningSupported(DateType.DATE, TimestampNTZType.TIMESTAMP_NTZ))\n  }\n\n  test(\"decimal widening supported\") {\n    // Test Decimal(p, s) -> Decimal(p + k1, s + k2) where k1 >= k2 >= 0\n\n    // Same scale, increased precision\n    assert(TypeWideningChecker.isWideningSupported(new DecimalType(5, 2), new DecimalType(10, 2)))\n    // Increased scale and precision, with precision increase >= scale increase\n    assert(TypeWideningChecker.isWideningSupported(new DecimalType(5, 2), new DecimalType(10, 5)))\n    assert(TypeWideningChecker.isWideningSupported(new DecimalType(5, 2), new DecimalType(10, 5)))\n    assert(TypeWideningChecker.isWideningSupported(new DecimalType(5, 2), new DecimalType(8, 4)))\n  }\n\n  test(\"decimal widening unsupported\") {\n    // Invalid decimal widening\n    assert(!TypeWideningChecker.isWideningSupported(new DecimalType(10, 2), new DecimalType(5, 2)))\n    assert(!TypeWideningChecker.isWideningSupported(new DecimalType(10, 2), new DecimalType(10, 1)))\n    assert(!TypeWideningChecker.isWideningSupported(new DecimalType(10, 2), new DecimalType(9, 2)))\n    assert(!TypeWideningChecker.isWideningSupported(\n      new DecimalType(10, 5),\n      new DecimalType(12, 8)\n    )) // k1 < k2\n    assert(!TypeWideningChecker.isWideningSupported(\n      new DecimalType(10, 5),\n      new DecimalType(10, 3)\n    )) // scale decrease\n  }\n\n  test(\"integer to decimal supported widening\") {\n    // Test Byte, Short, Int -> Decimal(10 + k1, k2) where k1 >= k2 >= 0\n    Seq(ByteType.BYTE, ShortType.SHORT, IntegerType.INTEGER) foreach { t =>\n      assert(TypeWideningChecker.isWideningSupported(t, new DecimalType(10, 0)))\n      assert(TypeWideningChecker.isWideningSupported(t, new DecimalType(11, 0)))\n      assert(TypeWideningChecker.isWideningSupported(t, new DecimalType(12, 2)))\n      assert(TypeWideningChecker.isWideningSupported(t, new DecimalType(15, 3)))\n    }\n\n    // Test Long -> Decimal(20 + k1, k2) where k1 >= k2 >= 0\n    assert(TypeWideningChecker.isWideningSupported(LongType.LONG, new DecimalType(20, 0)))\n    assert(TypeWideningChecker.isWideningSupported(LongType.LONG, new DecimalType(25, 5)))\n  }\n\n  test(\"integer to Decimal unsupported widening\") {\n    Seq(ByteType.BYTE, ShortType.SHORT, IntegerType.INTEGER) foreach { t =>\n      assert(!TypeWideningChecker.isWideningSupported(\n        t,\n        new DecimalType(9, 0)\n      )) // precision < 10\n      assert(!TypeWideningChecker.isWideningSupported(\n        t,\n        new DecimalType(12, 3)\n      )) // k1 < k2\n    }\n    assert(!TypeWideningChecker.isWideningSupported(\n      LongType.LONG,\n      new DecimalType(19, 0)\n    )) // precision < 20\n  }\n\n  test(\"unsupported widening\") {\n    // Test unsupported widening operations\n    assert(!TypeWideningChecker.isWideningSupported(StringType.STRING, BinaryType.BINARY))\n    assert(!TypeWideningChecker.isWideningSupported(IntegerType.INTEGER, StringType.STRING))\n    assert(!TypeWideningChecker.isWideningSupported(DateType.DATE, StringType.STRING))\n    assert(!TypeWideningChecker.isWideningSupported(DoubleType.DOUBLE, new DecimalType(10, 2)))\n    // Test invalid date widening\n    assert(!TypeWideningChecker.isWideningSupported(DateType.DATE, TimestampType.TIMESTAMP))\n    assert(!TypeWideningChecker.isWideningSupported(TimestampNTZType.TIMESTAMP_NTZ, DateType.DATE))\n  }\n\n  test(\"Iceberg V2 compatible widening\") {\n    // Test Iceberg V2 compatible widening\n\n    // Integer widening\n    assert(TypeWideningChecker.isIcebergV2Compatible(ByteType.BYTE, ShortType.SHORT))\n    assert(TypeWideningChecker.isIcebergV2Compatible(ShortType.SHORT, IntegerType.INTEGER))\n    assert(TypeWideningChecker.isIcebergV2Compatible(IntegerType.INTEGER, LongType.LONG))\n\n    // Float -> Double widening\n    assert(TypeWideningChecker.isIcebergV2Compatible(FloatType.FLOAT, DoubleType.DOUBLE))\n\n    // Decimal precision increase (without scale increase)\n    assert(TypeWideningChecker.isIcebergV2Compatible(\n      new DecimalType(5, 2),\n      new DecimalType(10, 2)))\n\n  }\n\n  test(\"iceberg V2 unsupported type widening\") {\n    /////////////////////////////////////////////////////////////////////////////////////\n    // Test generally unsupported widening operations\n    /////////////////////////////////////////////////////////////////////////////////////\n    assert(!TypeWideningChecker.isIcebergV2Compatible(StringType.STRING, BinaryType.BINARY))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(IntegerType.INTEGER, StringType.STRING))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(DateType.DATE, StringType.STRING))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(DoubleType.DOUBLE, new DecimalType(10, 2)))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(DateType.DATE, TimestampType.TIMESTAMP))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(\n      TimestampNTZType.TIMESTAMP_NTZ,\n      DateType.DATE))\n\n    ////////////////////////////////////////////////////////////////////////////////////\n    // Test invalid widening that are generally supported by Delta but not by Iceberg V2\n    ////////////////////////////////////////////////////////////////////////////////////\n\n    // Integer to Double widening (not supported by Iceberg)\n    assert(!TypeWideningChecker.isIcebergV2Compatible(ByteType.BYTE, DoubleType.DOUBLE))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(ShortType.SHORT, DoubleType.DOUBLE))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(IntegerType.INTEGER, DoubleType.DOUBLE))\n\n    // Date to TimestampNTZ widening (not supported by Iceberg)\n    assert(!TypeWideningChecker.isIcebergV2Compatible(\n      DateType.DATE,\n      TimestampNTZType.TIMESTAMP_NTZ))\n\n    // Decimal scale increase (not supported by Iceberg)\n    assert(!TypeWideningChecker.isIcebergV2Compatible(\n      new DecimalType(5, 2),\n      new DecimalType(10, 5)))\n\n    // Integer to Decimal widening (not supported by Iceberg)\n    assert(!TypeWideningChecker.isIcebergV2Compatible(ByteType.BYTE, new DecimalType(10, 0)))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(ShortType.SHORT, new DecimalType(12, 2)))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(\n      IntegerType.INTEGER,\n      new DecimalType(15, 3)))\n    assert(!TypeWideningChecker.isIcebergV2Compatible(LongType.LONG, new DecimalType(20, 0)))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/ColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport java.util\n\nimport scala.collection.JavaConverters.mapAsJavaMapConverter\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.actions.Metadata\nimport io.delta.kernel.internal.util.ColumnMapping._\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode._\nimport io.delta.kernel.types._\n\nimport org.assertj.core.api.Assertions.{assertThat, assertThatNoException, assertThatThrownBy}\nimport org.assertj.core.util.Maps\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ColumnMappingSuite extends AnyFunSuite with ColumnMappingSuiteBase {\n  test(\"column mapping is only enabled on known mapping modes\") {\n    assertThat(ColumnMapping.isColumnMappingModeEnabled(null)).isFalse\n    assertThat(ColumnMapping.isColumnMappingModeEnabled(NONE)).isFalse\n    assertThat(ColumnMapping.isColumnMappingModeEnabled(NAME)).isTrue\n    assertThat(ColumnMapping.isColumnMappingModeEnabled(ID)).isTrue\n  }\n\n  test(\"column mapping change with empty config\") {\n    assertThatNoException.isThrownBy(() =>\n      ColumnMapping.verifyColumnMappingChange(\n        new util.HashMap(),\n        new util.HashMap()))\n  }\n\n  test(\"column mapping mode change not allowed on existing table\") {\n    assertThatThrownBy(() =>\n      ColumnMapping.verifyColumnMappingChange(\n        Maps.newHashMap(COLUMN_MAPPING_MODE_KEY, NAME.toString),\n        Maps.newHashMap(COLUMN_MAPPING_MODE_KEY, ID.toString)))\n      .isInstanceOf(classOf[IllegalArgumentException])\n      .hasMessage(\"Changing column mapping mode from 'name' to 'id' is not supported\")\n\n    assertThatThrownBy(() =>\n      ColumnMapping.verifyColumnMappingChange(\n        Maps.newHashMap(COLUMN_MAPPING_MODE_KEY, ID.toString),\n        Maps.newHashMap(COLUMN_MAPPING_MODE_KEY, NAME.toString)))\n      .isInstanceOf(classOf[IllegalArgumentException])\n      .hasMessage(\"Changing column mapping mode from 'id' to 'name' is not supported\")\n\n    assertThatThrownBy(() =>\n      ColumnMapping.verifyColumnMappingChange(\n        new util.HashMap(),\n        Maps.newHashMap(COLUMN_MAPPING_MODE_KEY, ID.toString)))\n      .isInstanceOf(classOf[IllegalArgumentException])\n      .hasMessage(\"Changing column mapping mode from 'none' to 'id' is not supported\")\n  }\n\n  test(\"finding max column id with different schemas\") {\n    assertThat(ColumnMapping.findMaxColumnId(new StructType)).isEqualTo(0)\n\n    assertThat(ColumnMapping.findMaxColumnId(\n      new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"b\", IntegerType.INTEGER, true)))\n      .isEqualTo(0)\n\n    assertThat(ColumnMapping.findMaxColumnId(\n      new StructType()\n        .add(\"a\", StringType.STRING, createMetadataWithFieldId(14))\n        .add(\"b\", IntegerType.INTEGER, createMetadataWithFieldId(17))\n        .add(\"c\", IntegerType.INTEGER, createMetadataWithFieldId(3))))\n      .isEqualTo(17)\n\n    // nested columns are currently not supported\n    assertThat(ColumnMapping.findMaxColumnId(\n      new StructType().add(\"a\", StringType.STRING, createMetadataWithFieldId(14))\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true)\n            .add(\"e\", IntegerType.INTEGER, true),\n          createMetadataWithFieldId(15))\n        .add(\"c\", IntegerType.INTEGER, createMetadataWithFieldId(7))))\n      .isEqualTo(15)\n  }\n\n  test(\"finding max column id with nested struct type\") {\n    val nestedStruct = new StructType()\n      .add(\"d\", IntegerType.INTEGER, createMetadataWithFieldId(3))\n      .add(\"e\", IntegerType.INTEGER, createMetadataWithFieldId(4))\n\n    val schema = new StructType()\n      .add(\"a\", StringType.STRING, createMetadataWithFieldId(1))\n      .add(\"b\", nestedStruct, createMetadataWithFieldId(2))\n      .add(\"c\", IntegerType.INTEGER, createMetadataWithFieldId(5))\n\n    assertThat(ColumnMapping.findMaxColumnId(schema)).isEqualTo(5)\n  }\n\n  test(\"finding max column id with nested struct type and random ids\") {\n    val nestedStruct = new StructType()\n      .add(\"d\", IntegerType.INTEGER, createMetadataWithFieldId(2))\n      .add(\"e\", IntegerType.INTEGER, createMetadataWithFieldId(1))\n\n    val schema = new StructType()\n      .add(\"a\", StringType.STRING, createMetadataWithFieldId(3))\n      .add(\"b\", nestedStruct, createMetadataWithFieldId(4))\n      .add(\"c\", IntegerType.INTEGER, createMetadataWithFieldId(5))\n\n    assertThat(ColumnMapping.findMaxColumnId(schema)).isEqualTo(5)\n  }\n\n  test(\"finding max column id with nested array type\") {\n    val nestedStruct = new StructType()\n      .add(\"e\", IntegerType.INTEGER, createMetadataWithFieldId(4))\n      .add(\"f\", IntegerType.INTEGER, createMetadataWithFieldId(5))\n\n    val nestedMeta = FieldMetadata.builder()\n      .putLong(COLUMN_MAPPING_ID_KEY, 2)\n      .putFieldMetadata(\n        COLUMN_MAPPING_NESTED_IDS_KEY,\n        FieldMetadata.builder().putLong(\"b.element\", 6).build())\n      .build()\n\n    val schema = new StructType()\n      .add(\"a\", StringType.STRING, createMetadataWithFieldId(1))\n      .add(\"b\", new ArrayType(new StructField(\"d\", nestedStruct, false)), nestedMeta)\n      .add(\"c\", IntegerType.INTEGER, createMetadataWithFieldId(3))\n\n    assertThat(ColumnMapping.findMaxColumnId(schema)).isEqualTo(6)\n  }\n\n  test(\"finding max column id with nested map type\") {\n    val nestedStruct = new StructType()\n      .add(\"e\", IntegerType.INTEGER, createMetadataWithFieldId(4))\n      .add(\"f\", IntegerType.INTEGER, createMetadataWithFieldId(5))\n\n    val nestedMeta = FieldMetadata.builder()\n      .putLong(COLUMN_MAPPING_ID_KEY, 2)\n      .putFieldMetadata(\n        COLUMN_MAPPING_NESTED_IDS_KEY,\n        FieldMetadata.builder()\n          .putLong(\"b.key\", 11)\n          .putLong(\"b.value\", 12).build())\n      .build()\n\n    val schema = new StructType()\n      .add(\"a\", StringType.STRING, createMetadataWithFieldId(1))\n      .add(\n        \"b\",\n        new MapType(\n          IntegerType.INTEGER,\n          new StructField(\"d\", nestedStruct, false).getDataType,\n          false),\n        nestedMeta)\n      .add(\"c\", IntegerType.INTEGER, createMetadataWithFieldId(3))\n\n    assertThat(ColumnMapping.findMaxColumnId(schema)).isEqualTo(12)\n  }\n\n  private val testingSchema = new StructType()\n    .add(\"a\", StringType.STRING)\n    .add(\n      \"b\",\n      new StructType()\n        .add(\"c\", DoubleType.DOUBLE)\n        .add(\"d\", DateType.DATE))\n    .add(\"e\", FloatType.FLOAT)\n    .add(\n      \"f\",\n      new StructType()\n        .add(\n          \"g\",\n          new StructType()\n            .add(\"h\", TimestampNTZType.TIMESTAMP_NTZ)))\n    .add(\"i\", new MapType(StringType.STRING, DoubleType.DOUBLE, false))\n    .add(\"j\", new ArrayType(StringType.STRING, false))\n\n  Seq(\n    (Array(\"a\"), StringType.STRING),\n    (Array(\"b\", \"c\"), DoubleType.DOUBLE),\n    (Array(\"b\", \"d\"), DateType.DATE),\n    (Array(\"e\"), FloatType.FLOAT),\n    (Array(\"f\", \"g\", \"h\"), TimestampNTZType.TIMESTAMP_NTZ),\n    (Array(\"i\"), new MapType(StringType.STRING, DoubleType.DOUBLE, false)),\n    (Array(\"j\"), new ArrayType(StringType.STRING, false))).foreach {\n    case (columnName, expectedType) =>\n      test(s\"get physical column name and dataType for ${columnName.mkString(\".\")}\") {\n        // case 1: column mapping disabled\n        val column = new Column(columnName)\n        val resultTuple =\n          ColumnMapping.getPhysicalColumnNameAndDataType(testingSchema, column)\n\n        val actualColumn = resultTuple._1\n        val actualType = resultTuple._2\n        assert(actualColumn == column)\n        assert(actualType == expectedType)\n\n        // case 2: column mapping enabled\n        val metadata: Metadata = updateColumnMappingMetadataIfNeeded(\n          testMetadata(testingSchema).withColumnMappingEnabled(\"id\"),\n          true).orElseGet(() => fail(\"Metadata should not be empty\"))\n\n        val physicalResultTuple = ColumnMapping.getPhysicalColumnNameAndDataType(\n          metadata.getSchema,\n          column)\n        val actualPhysicalColumn = physicalResultTuple._1\n        val actualPhysicalType = physicalResultTuple._2\n        assert(actualPhysicalColumn.getNames.length == columnName.length)\n        assert(actualPhysicalType == expectedType)\n\n        // case 3: round-trip test - physical back to logical\n        val logicalResultTuple = ColumnMapping.getLogicalColumnNameAndDataType(\n          metadata.getSchema,\n          actualPhysicalColumn)\n        assert(logicalResultTuple._1 == column)\n        assert(logicalResultTuple._2 == expectedType)\n      }\n  }\n\n  Seq(\n    (Array(\"A\"), Array(\"a\"), StringType.STRING),\n    (Array(\"B\", \"C\"), Array(\"b\", \"c\"), DoubleType.DOUBLE),\n    (Array(\"B\", \"D\"), Array(\"b\", \"d\"), DateType.DATE),\n    (Array(\"E\"), Array(\"e\"), FloatType.FLOAT),\n    (Array(\"F\", \"G\", \"H\"), Array(\"f\", \"g\", \"h\"), TimestampNTZType.TIMESTAMP_NTZ),\n    (Array(\"I\"), Array(\"i\"), new MapType(StringType.STRING, DoubleType.DOUBLE, false)),\n    (Array(\"J\"), Array(\"j\"), new ArrayType(StringType.STRING, false))).foreach {\n    case (inputColumnName, expectedColumnName, expectedType) =>\n      val inputColumnNameStr = inputColumnName.mkString(\".\")\n      test(s\"get physical column name should respect case of table schema, $inputColumnNameStr\") {\n\n        val column = new Column(inputColumnName)\n        val resultTuple =\n          ColumnMapping.getPhysicalColumnNameAndDataType(testingSchema, column)\n\n        val actualColumn = resultTuple._1\n        val actualType = resultTuple._2\n        assert(actualColumn == new Column(expectedColumnName))\n        assert(actualType == expectedType)\n      }\n  }\n\n  test(\"getPhysicalColumnNameAndDataType: exception expected when column does not exist\") {\n    val ex = intercept[KernelException] {\n      ColumnMapping.getPhysicalColumnNameAndDataType(\n        new StructType()\n          .add(\"A\", StringType.STRING)\n          .add(\"b\", IntegerType.INTEGER),\n        new Column(\"abc\"))\n    }\n    assert(ex.getMessage.contains(\"Column 'column(`abc`)' was not found in the table schema\"))\n\n    val ex1 = intercept[KernelException] {\n      ColumnMapping.getPhysicalColumnNameAndDataType(\n        new StructType().add(\"a\", StringType.STRING)\n          .add(\n            \"b\",\n            new StructType()\n              .add(\"D\", IntegerType.INTEGER)\n              .add(\"e\", IntegerType.INTEGER))\n          .add(\"c\", IntegerType.INTEGER),\n        new Column(Array(\"Bbb\", \"d\")))\n    }\n    assert(ex1.getMessage.contains(\"Column 'column(`Bbb`.`d`)' was not found in the table schema\"))\n  }\n\n  test(\"getLogicalColumnNameAndDataType: without column mapping\") {\n    val logicalTuple = ColumnMapping.getLogicalColumnNameAndDataType(\n      testingSchema,\n      new Column(Array(\"b\", \"c\")))\n\n    assert(logicalTuple._1 === new Column(Array(\"b\", \"c\")))\n    assert(logicalTuple._2 === DoubleType.DOUBLE)\n  }\n\n  test(\"getLogicalColumnNameAndDataType: with column mapping\") {\n    val metadata = updateColumnMappingMetadataIfNeeded(\n      testMetadata(testingSchema).withColumnMappingEnabled(\"id\"),\n      true).orElseGet(() => fail(\"Metadata should not be empty\"))\n\n    // Test simple column lookup\n    {\n      val physicalTuple = ColumnMapping.getPhysicalColumnNameAndDataType(\n        metadata.getSchema,\n        new Column(\"a\"))\n      val physicalColumn = physicalTuple._1\n\n      val logicalTuple = ColumnMapping.getLogicalColumnNameAndDataType(\n        metadata.getSchema,\n        physicalColumn)\n\n      assert(logicalTuple._1 === new Column(\"a\"))\n      assert(logicalTuple._2 === StringType.STRING)\n    }\n\n    // Test nested column lookup\n    {\n      val physicalTuple = ColumnMapping.getPhysicalColumnNameAndDataType(\n        metadata.getSchema,\n        new Column(Array(\"b\", \"c\")))\n      val physicalColumn = physicalTuple._1\n\n      val logicalTuple = ColumnMapping.getLogicalColumnNameAndDataType(\n        metadata.getSchema,\n        physicalColumn)\n\n      assert(logicalTuple._1 === new Column(Array(\"b\", \"c\")))\n      assert(logicalTuple._2 === DoubleType.DOUBLE)\n    }\n  }\n\n  test(\"getLogicalColumnNameAndDataType: with column mapping + explicit physical schema\") {\n    // Create a simple schema with explicit physical column names\n    val schemaWithPhysicalNames = new StructType()\n      .add(new StructField(\n        \"logicalCol\",\n        StringType.STRING,\n        true,\n        FieldMetadata.builder()\n          .putLong(COLUMN_MAPPING_ID_KEY, 1L)\n          .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-uuid-123\")\n          .build()))\n      .add(new StructField(\n        \"nestedStruct\",\n        new StructType()\n          .add(new StructField(\n            \"innerCol\",\n            IntegerType.INTEGER,\n            true,\n            FieldMetadata.builder()\n              .putLong(COLUMN_MAPPING_ID_KEY, 3L)\n              .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-uuid-456\")\n              .build())),\n        true,\n        FieldMetadata.builder()\n          .putLong(COLUMN_MAPPING_ID_KEY, 2L)\n          .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-uuid-789\")\n          .build()))\n\n    // Test simple column lookup\n    val simpleResult = ColumnMapping.getLogicalColumnNameAndDataType(\n      schemaWithPhysicalNames,\n      new Column(\"col-uuid-123\"))\n    assert(simpleResult._1 === new Column(\"logicalCol\"))\n    assert(simpleResult._2 === StringType.STRING)\n\n    // Test nested column lookup\n    val nestedResult = ColumnMapping.getLogicalColumnNameAndDataType(\n      schemaWithPhysicalNames,\n      new Column(Array(\"col-uuid-789\", \"col-uuid-456\")))\n    assert(nestedResult._1 === new Column(Array(\"nestedStruct\", \"innerCol\")))\n    assert(nestedResult._2 === IntegerType.INTEGER)\n  }\n\n  test(\"getLogicalColumnNameAndDataType: exception when physical column not found\") {\n    val ex = intercept[KernelException] {\n      ColumnMapping.getLogicalColumnNameAndDataType(\n        testingSchema,\n        new Column(\"foo\"))\n    }\n    assert(ex.getMessage.contains(\"Column 'column(`foo`)' was not found in the table schema\"))\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"assign id and physical name to new table: $isNewTable\") {\n      val schema: StructType = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"b\", StringType.STRING, true)\n\n      val metadata: Metadata = updateColumnMappingMetadataIfNeeded(\n        testMetadata(schema).withColumnMappingEnabled(\"id\"),\n        isNewTable).orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      assertColumnMapping(metadata.getSchema.get(\"a\"), 1L, if (isNewTable) \"UUID\" else \"a\")\n      assertColumnMapping(metadata.getSchema.get(\"b\"), 2L, if (isNewTable) \"UUID\" else \"b\")\n\n      assertThat(metadata.getConfiguration)\n        .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"2\")\n\n      // Requesting the same operation on the same schema shouldn't change anything\n      // as the schema already has the necessary column mapping info\n      assertNoOpOnUpdateColumnMappingMetadataRequest(\n        metadata.getSchema,\n        enableIcebergCompatV2 = false,\n        isNewTable)\n    }\n  }\n\n  test(\"none mapping mode returns original schema\") {\n    val schema = new StructType().add(\"a\", StringType.STRING, true)\n    assertThat(updateColumnMappingMetadataIfNeeded(testMetadata(schema), true)).isEmpty\n  }\n\n  test(\"assigning id and physical name preserves field metadata\") {\n    val schema = new StructType()\n      .add(\n        \"a\",\n        StringType.STRING,\n        FieldMetadata.builder.putString(\"key1\", \"val1\").putString(\"key2\", \"val2\").build)\n\n    val metadata = updateColumnMappingMetadataIfNeeded(\n      testMetadata(schema).withColumnMappingEnabled(),\n      true).orElseGet(() => fail(\"Metadata should not be empty\"))\n    val fieldMetadata = metadata.getSchema.get(\"a\").getMetadata.getEntries\n\n    assertThat(fieldMetadata)\n      .containsEntry(\"key1\", \"val1\")\n      .containsEntry(\"key2\", \"val2\")\n      .containsEntry(ColumnMapping.COLUMN_MAPPING_ID_KEY, (1L).asInstanceOf[AnyRef])\n      .hasEntrySatisfying(\n        ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY,\n        (k: AnyRef) => assertThat(k).asString.startsWith(\"col-\"))\n  }\n\n  runWithIcebergCompatComboForNewAndExistingTables(\n    \"assign id and physical name to schema with nested struct type\") {\n    (isNewTable, enableIcebergCompatV2, enableIcebergWriterCompatV1) =>\n      val schema: StructType =\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\n            \"b\",\n            new StructType()\n              .add(\"d\", IntegerType.INTEGER)\n              .add(\"e\", IntegerType.INTEGER))\n          .add(\"c\", IntegerType.INTEGER)\n\n      var inputMetadata = testMetadata(schema).withColumnMappingEnabled(\"id\")\n      if (enableIcebergCompatV2) {\n        inputMetadata = inputMetadata.withIcebergCompatV2Enabled\n      }\n      if (enableIcebergWriterCompatV1) {\n        inputMetadata = inputMetadata.withIcebergWriterCompatV1Enabled\n      }\n      val metadata = updateColumnMappingMetadataIfNeeded(inputMetadata, isNewTable)\n        .orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      assertColumnMapping(metadata.getSchema.get(\"a\"), 1L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(metadata.getSchema.get(\"b\"), 2L, isNewTable, enableIcebergWriterCompatV1)\n      val innerStruct = metadata.getSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n      assertColumnMapping(innerStruct.get(\"d\"), 3L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(innerStruct.get(\"e\"), 4L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(metadata.getSchema.get(\"c\"), 5L, isNewTable, enableIcebergWriterCompatV1)\n\n      assertThat(metadata.getConfiguration)\n        .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"5\")\n\n      // Requesting the same operation on the same schema shouldn't change anything\n      // as the schema already has the necessary column mapping info\n      assertNoOpOnUpdateColumnMappingMetadataRequest(\n        metadata.getSchema,\n        enableIcebergCompatV2,\n        isNewTable)\n  }\n\n  runWithIcebergCompatComboForNewAndExistingTables(\n    \"assign id and physical name to schema with array type\") {\n    (isNewTable, enableIcebergCompatV2, enableIcebergWriterCompatV1) =>\n      val schema: StructType =\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", new ArrayType(IntegerType.INTEGER, false))\n          .add(\"c\", IntegerType.INTEGER)\n\n      var inputMetadata = testMetadata(schema).withColumnMappingEnabled(\"id\")\n      if (enableIcebergCompatV2) {\n        inputMetadata = inputMetadata.withIcebergCompatV2Enabled\n      }\n      if (enableIcebergWriterCompatV1) {\n        inputMetadata = inputMetadata.withIcebergWriterCompatV1Enabled\n      }\n      val metadata = updateColumnMappingMetadataIfNeeded(inputMetadata, isNewTable)\n        .orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      assertColumnMapping(metadata.getSchema.get(\"a\"), 1L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(metadata.getSchema.get(\"b\"), 2L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(metadata.getSchema.get(\"c\"), 3L, isNewTable, enableIcebergWriterCompatV1)\n\n      if (enableIcebergCompatV2) {\n        val colPrefix = if (enableIcebergWriterCompatV1) {\n          \"col-2.\"\n        } else if (isNewTable) {\n          \"col-\"\n        } else {\n          \"b.\"\n        }\n        // verify nested ids\n        assertThat(metadata.getSchema.get(\"b\").getMetadata.getEntries\n          .get(COLUMN_MAPPING_NESTED_IDS_KEY).asInstanceOf[FieldMetadata].getEntries)\n          .hasSize(1)\n          .anySatisfy((k: AnyRef, v: AnyRef) => {\n            assertThat(k).asString.startsWith(colPrefix)\n            assertThat(k).asString.endsWith(\".element\")\n            assertThat(v).isEqualTo(4L)\n          })\n\n        assertThat(metadata.getConfiguration)\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"4\")\n      } else {\n        assertThat(metadata.getSchema.get(\"b\").getMetadata.getEntries)\n          .doesNotContainKey(COLUMN_MAPPING_NESTED_IDS_KEY)\n\n        assertThat(metadata.getConfiguration)\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"3\")\n      }\n\n      // Requesting the same operation on the same schema shouldn't change anything\n      // as the schema already has the necessary column mapping info\n      assertNoOpOnUpdateColumnMappingMetadataRequest(\n        metadata.getSchema,\n        enableIcebergCompatV2,\n        isNewTable)\n  }\n\n  runWithIcebergCompatComboForNewAndExistingTables(\n    \"assign id and physical name to schema with map type\") {\n    (isNewTable, enableIcebergCompatV2, enableIcebergWriterCompatV1) =>\n      val schema: StructType =\n        new StructType()\n          .add(\"a\", StringType.STRING)\n          .add(\"b\", new MapType(IntegerType.INTEGER, StringType.STRING, false))\n          .add(\"c\", IntegerType.INTEGER)\n\n      var inputMetadata = testMetadata(schema).withColumnMappingEnabled(\"id\")\n      if (enableIcebergCompatV2) {\n        inputMetadata = inputMetadata.withIcebergCompatV2Enabled\n      }\n      if (enableIcebergWriterCompatV1) {\n        inputMetadata = inputMetadata.withIcebergWriterCompatV1Enabled\n      }\n      val metadata = updateColumnMappingMetadataIfNeeded(inputMetadata, isNewTable)\n        .orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      assertColumnMapping(metadata.getSchema.get(\"a\"), 1L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(metadata.getSchema.get(\"b\"), 2L, isNewTable, enableIcebergWriterCompatV1)\n      assertColumnMapping(metadata.getSchema.get(\"c\"), 3L, isNewTable, enableIcebergWriterCompatV1)\n\n      if (enableIcebergCompatV2) {\n        val colPrefix = if (enableIcebergWriterCompatV1) {\n          \"col-2.\"\n        } else if (isNewTable) {\n          \"col-\"\n        } else {\n          \"b.\"\n        }\n        assert(\n          metadata.getSchema.get(\n            \"b\").getMetadata.getMetadata(COLUMN_MAPPING_NESTED_IDS_KEY) != null,\n          s\"${metadata.getSchema}\")\n        // verify nested ids\n        assertThat(metadata.getSchema.get(\"b\").getMetadata.getEntries\n          .get(COLUMN_MAPPING_NESTED_IDS_KEY).asInstanceOf[FieldMetadata].getEntries)\n          .hasSize(2)\n          .anySatisfy((k: AnyRef, v: AnyRef) => {\n            assertThat(k).asString.startsWith(colPrefix)\n            assertThat(k).asString.endsWith(\".key\")\n            assertThat(v).isEqualTo(4L)\n          })\n          .anySatisfy((k: AnyRef, v: AnyRef) => {\n            assertThat(k).asString.startsWith(colPrefix)\n            assertThat(k).asString.endsWith(\".value\")\n            assertThat(v).isEqualTo(5L)\n          })\n      } else {\n        assertThat(metadata.getSchema.get(\"b\").getMetadata.getEntries)\n          .doesNotContainKey(COLUMN_MAPPING_NESTED_IDS_KEY)\n      }\n\n      assertThat(metadata.getConfiguration).containsEntry(\n        ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY,\n        if (enableIcebergCompatV2) \"5\" else \"3\")\n\n      // Requesting the same operation on the same schema shouldn't change anything\n      // as the schema already has the necessary column mapping info\n      assertNoOpOnUpdateColumnMappingMetadataRequest(\n        metadata.getSchema,\n        enableIcebergCompatV2,\n        isNewTable)\n  }\n\n  Seq(false, true).foreach { isNewTable =>\n    val baseSchema: StructType =\n      new StructType()\n        .add(\n          \"l\",\n          new ArrayType(\n            new ArrayType(\n              new MapType(\n                new ArrayType(\n                  StringType.STRING,\n                  /* nullable= */ false),\n                new MapType(\n                  StringType.STRING,\n                  new StructType().add(\n                    \"leaf\",\n                    StringType.STRING,\n                    false,\n                    FieldMetadata.builder().putBoolean(\"k1\", true).build()),\n                  /* valuesContainNull= */ false),\n                /* valuesContainNull= */ false),\n              /* nullableElements= */ false),\n            /* nullableElements= */ false),\n          /* nullable= */ false,\n          FieldMetadata.builder().putBoolean(\"k2\", true).build())\n    Seq(\n      (baseSchema, 1, (md: Metadata) => md.getSchema.get(\"l\")),\n      (\n        new StructType().add(\n          \"p\",\n          baseSchema,\n          /* nullable= */ false),\n        2,\n        (md: Metadata) =>\n          md.getSchema.get(\"p\").getDataType.asInstanceOf[StructType].get(\"l\"))).foreach {\n      case (schemaToTest, base, getter) =>\n\n        test(s\"Deeply nested values don't assign more field IDs then necessary $isNewTable $base\") {\n\n          var inputMetadata = testMetadata(schemaToTest).withColumnMappingEnabled(\"id\")\n          inputMetadata = inputMetadata.withIcebergCompatV2Enabled\n          inputMetadata = inputMetadata.withIcebergWriterCompatV1Enabled\n          val metadata = updateColumnMappingMetadataIfNeeded(inputMetadata, isNewTable)\n            .orElseGet(() => fail(\"Metadata should not be empty\"))\n\n          assertThat(metadata.getConfiguration).containsEntry(\n            ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY,\n            (base + 8).toString)\n          val prefix = s\"col-$base\"\n          // Values are offset by base.  All Ids are assigned in depth first order to\n          // StructField's first and then intermediate nested fields are added after.\n          val nestedColumnMappingValues = FieldMetadata.builder()\n            .putLong(s\"$prefix.element\", base + 2)\n            .putLong(s\"$prefix.element.element\", base + 3)\n            .putLong(s\"$prefix.element.element.key\", base + 4)\n            .putLong(s\"$prefix.element.element.key.element\", base + 5)\n            .putLong(s\"$prefix.element.element.value\", base + 6)\n            .putLong(s\"$prefix.element.element.value.key\", base + 7)\n            .putLong(s\"$prefix.element.element.value.value\", base + 8).build()\n          val expectedMetadata = FieldMetadata.builder().putFieldMetadata(\n            COLUMN_MAPPING_NESTED_IDS_KEY,\n            nestedColumnMappingValues)\n            .putString(\"delta.columnMapping.physicalName\", prefix)\n            .putLong(\"delta.columnMapping.id\", base)\n            .putBoolean(\"k2\", true).build()\n          val firstColumnMetadata = getter(metadata).getMetadata\n          assertThat(firstColumnMetadata.getMetadata(\n            COLUMN_MAPPING_NESTED_IDS_KEY).getEntries).containsExactlyInAnyOrderEntriesOf(\n            nestedColumnMappingValues.getEntries)\n          assertThat(firstColumnMetadata.getEntries).containsExactlyInAnyOrderEntriesOf(\n            expectedMetadata.getEntries)\n\n          // TODO: It would be nice to have visitor pattern on schema so we can assert\n          // all metadata for nested fields\n          // are empty but this at least provides a sanity check.\n          assert(getter(metadata).getDataType.asInstanceOf[\n            ArrayType].getElementField.getMetadata == FieldMetadata.empty())\n\n          // Requesting the same operation on the same schema shouldn't change anything\n          // as the schema already has the necessary column mapping info. This includes both\n          // IDs and maxFieldId.\n          assertNoOpOnUpdateColumnMappingMetadataRequest(\n            metadata.getSchema,\n            /* enableIcebergCompatV2= */ true,\n            isNewTable)\n        }\n    }\n  }\n\n  runWithIcebergCompatComboForNewAndExistingTables(\n    \"assign id and physical name to schema with nested schema\") {\n    (isNewTable, enableIcebergCompatV2, enableIcebergWriterCompatV1) =>\n      val schema: StructType = cmTestSchema()\n\n      var inputMetadata = testMetadata(schema).withColumnMappingEnabled(\"id\")\n      if (enableIcebergCompatV2) {\n        inputMetadata = inputMetadata.withIcebergCompatV2Enabled\n      }\n      if (enableIcebergWriterCompatV1) {\n        inputMetadata = inputMetadata.withIcebergWriterCompatV1Enabled\n      }\n      val metadata = updateColumnMappingMetadataIfNeeded(inputMetadata, isNewTable)\n        .orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      verifyCMTestSchemaHasValidColumnMappingInfo(\n        metadata,\n        isNewTable,\n        enableIcebergCompatV2,\n        enableIcebergWriterCompatV1)\n\n      // Requesting the same operation on the same schema shouldn't change anything\n      // as the schema already has the necessary column mapping info\n      assertNoOpOnUpdateColumnMappingMetadataRequest(\n        metadata.getSchema,\n        enableIcebergCompatV2,\n        isNewTable)\n  }\n\n  Seq(true, false).foreach { icebergCompatV2Enabled =>\n    test(s\"assign id and physical name to only to the fields that don't have \" +\n      s\"with icebergCompatV2=$icebergCompatV2Enabled\") {\n      val schema: StructType =\n        new StructType().add(\"a\", StringType.STRING)\n\n      val inputMetadata = testMetadata(schema).withColumnMappingEnabled(\"id\")\n      val updatedMetadata = updateColumnMappingMetadataIfNeeded(\n        if (icebergCompatV2Enabled) inputMetadata.withIcebergCompatV2Enabled else inputMetadata,\n        true).orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      assertColumnMapping(updatedMetadata.getSchema.get(\"a\"), 1L)\n\n      // Now add few more fields to the same schema\n      val updateSchema = updatedMetadata.getSchema\n        .add(\"b\", StringType.STRING)\n        .add(\"c\", new ArrayType(IntegerType.INTEGER, false))\n        .add(\"d\", new MapType(IntegerType.INTEGER, StringType.STRING, false))\n        .add(\"e\", new StructType().add(\"h\", IntegerType.INTEGER))\n\n      val inputMetadata2 = testMetadata(updateSchema).withColumnMappingEnabled(\"id\")\n      val updatedMetadata2 = updateColumnMappingMetadataIfNeeded(\n        if (icebergCompatV2Enabled) inputMetadata2.withIcebergCompatV2Enabled else inputMetadata2,\n        false).orElseGet(() => fail(\"Metadata should not be empty\"))\n\n      var fieldId = 0L\n\n      def nextFieldId: Long = {\n        fieldId += 1\n        fieldId\n      }\n\n      assertColumnMapping(updatedMetadata2.getSchema.get(\"a\"), nextFieldId)\n      // newly added columns will have the physical names same as logical names\n      assertColumnMapping(updatedMetadata2.getSchema.get(\"b\"), nextFieldId, \"b\")\n      assertColumnMapping(updatedMetadata2.getSchema.get(\"c\"), nextFieldId, \"c\")\n      assertColumnMapping(updatedMetadata2.getSchema.get(\"d\"), nextFieldId, \"d\")\n      assertColumnMapping(updatedMetadata2.getSchema.get(\"e\"), nextFieldId, \"e\")\n      assertColumnMapping(\n        updatedMetadata2.getSchema.get(\"e\")\n          .getDataType.asInstanceOf[StructType].get(\"h\"),\n        nextFieldId,\n        \"h\")\n\n      if (icebergCompatV2Enabled) {\n        // verify nested ids\n        assertThat(updatedMetadata2.getSchema.get(\"c\").getMetadata.getEntries\n          .get(COLUMN_MAPPING_NESTED_IDS_KEY).asInstanceOf[FieldMetadata].getEntries)\n          .hasSize(1)\n          .anySatisfy((k: AnyRef, v: AnyRef) => {\n            assertThat(k).asString.startsWith(\"c.\")\n            assertThat(k).asString.endsWith(\".element\")\n            assertThat(v).isEqualTo(nextFieldId)\n          })\n\n        assertThat(updatedMetadata2.getSchema.get(\"d\").getMetadata.getEntries\n          .get(COLUMN_MAPPING_NESTED_IDS_KEY).asInstanceOf[FieldMetadata].getEntries)\n          .hasSize(2)\n          .anySatisfy((k: AnyRef, v: AnyRef) => {\n            assertThat(k).asString.startsWith(\"d.\")\n            assertThat(k).asString.endsWith(\".key\")\n            assertThat(v).isEqualTo(nextFieldId)\n          })\n          .anySatisfy((k: AnyRef, v: AnyRef) => {\n            assertThat(k).asString.startsWith(\"d.\")\n            assertThat(k).asString.endsWith(\".value\")\n            assertThat(v).isEqualTo(nextFieldId)\n          })\n      } else {\n        assertThat(updatedMetadata2.getSchema.get(\"c\").getMetadata.getEntries)\n          .doesNotContainKey(COLUMN_MAPPING_NESTED_IDS_KEY)\n        assertThat(updatedMetadata2.getSchema.get(\"d\").getMetadata.getEntries)\n          .doesNotContainKey(COLUMN_MAPPING_NESTED_IDS_KEY)\n      }\n\n      assertNoOpOnUpdateColumnMappingMetadataRequest(\n        updatedMetadata2.getSchema,\n        icebergCompatV2Enabled,\n        isNewTable = false)\n    }\n  }\n\n  test(\"both id and physical name must be provided if one is provided\") {\n    val schemaWithoutPhysicalName = new StructType()\n      .add(\n        new StructField(\n          \"col1\",\n          StringType.STRING,\n          true,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 0)\n            .build()))\n    val schemaWithoutId = new StructType()\n      .add(\n        new StructField(\n          \"col1\",\n          StringType.STRING,\n          true,\n          FieldMetadata.builder()\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"physical-name-col1\")\n            .build()))\n\n    Seq(schemaWithoutId, schemaWithoutPhysicalName).foreach { schema =>\n      val e = intercept[IllegalArgumentException] {\n        updateColumnMappingMetadataIfNeeded(testMetadata(schema).withColumnMappingEnabled(), true)\n      }\n      assert(e.getMessage.contains(\n        \"Both columnId and physicalName must be present if one is present\"))\n    }\n  }\n\n  /**\n   * A struct type with all necessary CM info won't cause metadata change by\n   * [[updateColumnMappingMetadataIfNeeded]]\n   */\n  def assertNoOpOnUpdateColumnMappingMetadataRequest(\n      schemaWithCMInfo: StructType,\n      enableIcebergCompatV2: Boolean,\n      isNewTable: Boolean): Unit = {\n\n    var metadata = testMetadata(schemaWithCMInfo).withColumnMappingEnabled(\"id\")\n    if (enableIcebergCompatV2) {\n      metadata = metadata.withIcebergCompatV2Enabled\n    }\n    if (!metadata.getConfiguration.containsKey(COLUMN_MAPPING_MAX_COLUMN_ID_KEY)) {\n      // A hack, if the metadata doesn't have max column ID in it,\n      // then new metadata is always returned.\n      metadata =\n        metadata.withMergedConfiguration(Map(COLUMN_MAPPING_MAX_COLUMN_ID_KEY -> \"100\").asJava)\n    }\n\n    assertThat(updateColumnMappingMetadataIfNeeded(metadata, isNewTable)).isEmpty\n  }\n\n  def runWithIcebergCompatComboForNewAndExistingTables(testName: String)(f: (\n      Boolean,\n      Boolean,\n      Boolean) => Unit): Unit = {\n    for {\n      isNewTable <- Seq(true, false)\n      enableIcebergCompatV2 <- Seq(true, false)\n    } {\n      // We only test icebergWriterCompatV1 when icebergCompatV2 is enabled\n      val icebergWriterCompatV1Modes = if (enableIcebergCompatV2) {\n        Seq(true, false)\n      } else {\n        Seq(false)\n      }\n      icebergWriterCompatV1Modes.foreach { enableIcebergWriterCompatV1 =>\n        test(s\"$testName, enableIcebergCompatV2=$enableIcebergCompatV2, \" +\n          s\"isNewTable=$isNewTable, enableIcebergWriterCompatV1=$enableIcebergWriterCompatV1\") {\n          f(isNewTable, enableIcebergCompatV2, enableIcebergWriterCompatV1)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/ColumnMappingSuiteBase.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeature\nimport io.delta.kernel.internal.util.ColumnMapping.{COLUMN_MAPPING_ID_KEY, COLUMN_MAPPING_NESTED_IDS_KEY}\nimport io.delta.kernel.test.ActionUtils\nimport io.delta.kernel.types.{ArrayType, FieldMetadata, IntegerType, MapType, StringType, StructField, StructType}\n\nimport org.assertj.core.api.Assertions.assertThat\nimport org.assertj.core.util.Maps\n\n/**\n * Common utilities for column mapping and iceberg compat v2 related nested column mapping\n * functionality\n */\ntrait ColumnMappingSuiteBase extends ActionUtils {\n\n  /* Asserts that the given field has the expected column mapping info */\n  def assertColumnMapping(\n      field: StructField,\n      expId: Long,\n      isNewTable: Boolean,\n      isIcebergWriterCompatV1: Boolean): Unit = {\n    val logicalName = field.getName\n    val expPhysicalName = if (isIcebergWriterCompatV1) {\n      s\"col-$expId\"\n    } else {\n      if (isNewTable) {\n        \"UUID\"\n      } else {\n        logicalName\n      }\n    }\n    assertColumnMapping(field, expId, expPhysicalName)\n  }\n\n  /* Asserts that the given field has the expected column mapping info */\n  def assertColumnMapping(\n      field: StructField,\n      expId: Long,\n      expPhysicalName: String = \"UUID\"): Unit = {\n    assertThat(field.getMetadata.getEntries)\n      .containsEntry(ColumnMapping.COLUMN_MAPPING_ID_KEY, expId.asInstanceOf[AnyRef])\n      .hasEntrySatisfying(\n        ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY,\n        (k: AnyRef) => {\n          if (expPhysicalName == \"UUID\") {\n            assertThat(k).asString.startsWith(\"col-\")\n          } else {\n            assertThat(k).asString.isEqualTo(expPhysicalName)\n          }\n        })\n  }\n\n  implicit class MetadataImplicits(metadata: Metadata) {\n    def withIcebergCompatV2Enabled: Metadata = {\n      metadata.withMergedConfiguration(\n        Maps.newHashMap(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey, \"true\"))\n    }\n\n    def withIcebergCompatV3Enabled: Metadata = {\n      metadata.withMergedConfiguration(\n        Maps.newHashMap(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey, \"true\"))\n    }\n\n    def withColumnMappingEnabled(mode: String = \"name\"): Metadata = {\n      metadata.withMergedConfiguration(\n        Maps.newHashMap(TableConfig.COLUMN_MAPPING_MODE.getKey, mode))\n    }\n\n    def withIcebergCompatV2AndCMEnabled(columnMappingMode: String = \"name\"): Metadata = {\n      metadata.withIcebergCompatV2Enabled.withColumnMappingEnabled(columnMappingMode)\n    }\n\n    def withIcebergCompatV3AndCMEnabled(columnMappingMode: String = \"name\"): Metadata = {\n      metadata.withIcebergCompatV3Enabled.withColumnMappingEnabled(columnMappingMode)\n    }\n\n    def withIcebergWriterCompatV1Enabled: Metadata = {\n      metadata.withMergedConfiguration(\n        Maps.newHashMap(TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey, \"true\"))\n    }\n  }\n\n  def createMetadataWithFieldId(fieldId: Int): FieldMetadata = {\n    FieldMetadata.builder.putLong(COLUMN_MAPPING_ID_KEY, fieldId).build()\n  }\n\n  /** Test schema containing various different types that test the nested column mapping info */\n  def cmTestSchema(): StructType = {\n    new StructType()\n      .add(\"a\", StringType.STRING)\n      .add(\n        \"b\",\n        new MapType(\n          IntegerType.INTEGER,\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER)\n            .add(\"e\", IntegerType.INTEGER)\n            .add(\n              \"f\",\n              new ArrayType(\n                new StructType()\n                  .add(\"g\", IntegerType.INTEGER)\n                  .add(\"h\", IntegerType.INTEGER),\n                false),\n              false),\n          false))\n      .add(\"c\", IntegerType.INTEGER)\n  }\n\n  /**\n   * Verify the schema returned by [[cmTestSchema()]] has correct column mapping (including nested)\n   * info assigned\n   */\n  def verifyCMTestSchemaHasValidColumnMappingInfo(\n      metadata: Metadata,\n      isNewTable: Boolean = true,\n      enableIcebergCompatV2: Boolean = true,\n      enableIcebergWriterCompatV1: Boolean = false,\n      initialFieldId: Long = 0L): Unit = {\n    var fieldId: Long = initialFieldId\n\n    def nextFieldId: Long = {\n      fieldId += 1\n      fieldId\n    }\n\n    assertColumnMapping(\n      metadata.getSchema.get(\"a\"),\n      nextFieldId,\n      isNewTable,\n      enableIcebergWriterCompatV1)\n    assertColumnMapping(\n      metadata.getSchema.get(\"b\"),\n      nextFieldId,\n      isNewTable,\n      enableIcebergWriterCompatV1)\n    val mapType = metadata.getSchema.get(\"b\").getDataType.asInstanceOf[MapType]\n    val innerStruct = mapType.getValueField.getDataType.asInstanceOf[StructType]\n    assertColumnMapping(innerStruct.get(\"d\"), nextFieldId, isNewTable, enableIcebergWriterCompatV1)\n    assertColumnMapping(innerStruct.get(\"e\"), nextFieldId, isNewTable, enableIcebergWriterCompatV1)\n    assertColumnMapping(innerStruct.get(\"f\"), nextFieldId, isNewTable, enableIcebergWriterCompatV1)\n    val innerArray = innerStruct.get(\"f\").getDataType.asInstanceOf[ArrayType]\n    val structInArray = innerArray.getElementField.getDataType.asInstanceOf[StructType]\n    assertColumnMapping(\n      structInArray.get(\"g\"),\n      nextFieldId,\n      isNewTable,\n      enableIcebergWriterCompatV1)\n    assertColumnMapping(\n      structInArray.get(\"h\"),\n      nextFieldId,\n      isNewTable,\n      enableIcebergWriterCompatV1)\n    assertColumnMapping(\n      metadata.getSchema.get(\"c\"),\n      nextFieldId,\n      isNewTable,\n      enableIcebergWriterCompatV1)\n\n    // verify nested ids\n    if (enableIcebergCompatV2) {\n      val colBPrefix = if (enableIcebergWriterCompatV1) {\n        \"col-2.\"\n      } else if (isNewTable) {\n        \"col-\"\n      } else {\n        \"b.\"\n      }\n      assertThat(metadata.getSchema.get(\"b\").getMetadata.getEntries\n        .get(COLUMN_MAPPING_NESTED_IDS_KEY).asInstanceOf[FieldMetadata].getEntries)\n        .hasSize(2)\n        .anySatisfy((k: AnyRef, v: AnyRef) => {\n          assertThat(k).asString.startsWith(colBPrefix)\n          assertThat(k).asString.endsWith(\".key\")\n          assert(k.asInstanceOf[String].count(_ == '.') == 1)\n          assertThat(v).isEqualTo(nextFieldId)\n        })\n        .anySatisfy((k: AnyRef, v: AnyRef) => {\n          assertThat(k).asString.startsWith(colBPrefix)\n          assertThat(k).asString.endsWith(\".value\")\n          assert(k.asInstanceOf[String].count(_ == '.') == 1)\n          assertThat(v).isEqualTo(nextFieldId)\n        })\n\n      assertThat(mapType.getKeyField.getMetadata.getEntries)\n        .doesNotContainKey(ColumnMapping.COLUMN_MAPPING_ID_KEY)\n        .doesNotContainKey(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n\n      assertThat(mapType.getValueField.getMetadata.getEntries)\n        .doesNotContainKey(ColumnMapping.COLUMN_MAPPING_ID_KEY)\n        .doesNotContainKey(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n\n      // verify nested ids\n      val colFPrefix = if (enableIcebergWriterCompatV1) {\n        \"col-5.\"\n      } else if (isNewTable) {\n        \"col-\"\n      } else {\n        \"f.\"\n      }\n      assert(\n        innerStruct.get(\"f\").getMetadata.getEntries\n          .get(COLUMN_MAPPING_NESTED_IDS_KEY) != null,\n        s\"${metadata.getSchema}\")\n      assertThat(innerStruct.get(\"f\").getMetadata.getEntries\n        .get(COLUMN_MAPPING_NESTED_IDS_KEY).asInstanceOf[FieldMetadata].getEntries)\n        .hasSize(1)\n        .anySatisfy((k: AnyRef, v: AnyRef) => {\n          assertThat(k).asString.startsWith(colFPrefix)\n          assertThat(k).asString.endsWith(\".element\")\n          assert(k.asInstanceOf[String].count(_ == '.') == 1)\n          assertThat(v).isEqualTo(nextFieldId)\n        })\n    }\n\n    assertThat(metadata.getConfiguration)\n      .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, fieldId.toString)\n  }\n\n  def testProtocol(tableFeatures: TableFeature*): Protocol = {\n    val protocol = new Protocol(3, 7)\n    protocol.withFeatures(tableFeatures.asJava).normalized()\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/DataFileStatisticsSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters.mapAsJavaMapConverter\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.statistics.DataFileStatistics\nimport io.delta.kernel.types._\n\nimport com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.matchers.must.Matchers\n\nclass DataFileStatisticsSuite extends AnyFunSuite with Matchers {\n\n  val objectMapper = new ObjectMapper()\n\n  def jsonToNode(json: String): JsonNode = {\n    objectMapper.readTree(json)\n  }\n\n  def areJsonNodesEqual(json1: String, json2: String): Boolean = {\n    val node1 = jsonToNode(json1)\n    val node2 = jsonToNode(json2)\n    node1 == node2\n  }\n\n  test(\"DataFileStatistics serialization with all types\") {\n    val nestedStructType = new StructType()\n      .add(\"aa\", StringType.STRING)\n      .add(\"ac\", new StructType().add(\"aca\", IntegerType.INTEGER))\n      .add(\"nested_variant\", VariantType.VARIANT)\n\n    val schema = new StructType()\n      .add(\"ByteType\", ByteType.BYTE)\n      .add(\"ShortType\", ShortType.SHORT)\n      .add(\"IntegerType\", IntegerType.INTEGER)\n      .add(\"LongType\", LongType.LONG)\n      .add(\"FloatType\", FloatType.FLOAT)\n      .add(\"DoubleType\", DoubleType.DOUBLE)\n      .add(\"DecimalType\", new DecimalType(10, 2))\n      .add(\"StringType\", StringType.STRING)\n      .add(\"DateType\", DateType.DATE)\n      .add(\"TimestampType\", TimestampType.TIMESTAMP)\n      .add(\"TimestampNTZType\", TimestampNTZType.TIMESTAMP_NTZ)\n      .add(\"BinaryType\", BinaryType.BINARY)\n      .add(\"NestedStruct\", nestedStructType)\n      .add(\"VariantType\", VariantType.VARIANT)\n\n    val minValues = Map(\n      new Column(\"ByteType\") -> Literal.ofByte(1.toByte),\n      new Column(\"ShortType\") -> Literal.ofShort(1.toShort),\n      new Column(\"IntegerType\") -> Literal.ofInt(1),\n      new Column(\"LongType\") -> Literal.ofLong(1L),\n      new Column(\"FloatType\") -> Literal.ofFloat(0.1f),\n      new Column(\"DoubleType\") -> Literal.ofDouble(0.1),\n      new Column(\"DecimalType\") -> Literal.ofDecimal(new java.math.BigDecimal(\"123.45\"), 10, 2),\n      new Column(\"StringType\") -> Literal.ofString(\"a\"),\n      new Column(\"DateType\") -> Literal.ofDate(1),\n      new Column(\"TimestampType\") -> Literal.ofTimestamp(1L),\n      new Column(\"TimestampNTZType\") -> Literal.ofTimestampNtz(1L),\n      new Column(\"BinaryType\") -> Literal.ofBinary(\"a\".getBytes),\n      new Column(Array(\"NestedStruct\", \"aa\")) -> Literal.ofString(\"a\"),\n      new Column(Array(\"NestedStruct\", \"ac\", \"aca\")) -> Literal.ofInt(1),\n      new Column(\"VariantType\") -> Literal.ofString(\n        \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\"),\n      new Column(Array(\"NestedStruct\", \"nested_variant\")) -> Literal.ofString(\n        \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\")).asJava\n\n    val maxValues = Map(\n      new Column(\"ByteType\") -> Literal.ofByte(10.toByte),\n      new Column(\"ShortType\") -> Literal.ofShort(10.toShort),\n      new Column(\"IntegerType\") -> Literal.ofInt(10),\n      new Column(\"LongType\") -> Literal.ofLong(10L),\n      new Column(\"FloatType\") -> Literal.ofFloat(10.1f),\n      new Column(\"DoubleType\") -> Literal.ofDouble(10.1),\n      new Column(\"DecimalType\") -> Literal.ofDecimal(new java.math.BigDecimal(\"456.78\"), 10, 2),\n      new Column(\"StringType\") -> Literal.ofString(\"z\"),\n      new Column(\"DateType\") -> Literal.ofDate(10),\n      new Column(\"TimestampType\") -> Literal.ofTimestamp(10L),\n      new Column(\"TimestampNTZType\") -> Literal.ofTimestampNtz(10L),\n      new Column(\"BinaryType\") -> Literal.ofBinary(\"z\".getBytes),\n      new Column(Array(\"NestedStruct\", \"aa\")) -> Literal.ofString(\"z\"),\n      new Column(Array(\"NestedStruct\", \"ac\", \"aca\")) -> Literal.ofInt(10),\n      new Column(\"VariantType\") -> Literal.ofString(\n        \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\"),\n      new Column(Array(\"NestedStruct\", \"nested_variant\")) -> Literal.ofString(\n        \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\")).asJava\n\n    val nullCount = Map(\n      new Column(\"ByteType\") -> 1L,\n      new Column(\"ShortType\") -> 1L,\n      new Column(\"IntegerType\") -> 1L,\n      new Column(\"LongType\") -> 1L,\n      new Column(\"FloatType\") -> 1L,\n      new Column(\"DoubleType\") -> 1L,\n      new Column(\"DecimalType\") -> 1L,\n      new Column(\"StringType\") -> 1L,\n      new Column(\"DateType\") -> 1L,\n      new Column(\"TimestampType\") -> 1L,\n      new Column(\"TimestampNTZType\") -> 1L,\n      new Column(\"BinaryType\") -> 1L,\n      new Column(Array(\"NestedStruct\", \"aa\")) -> 1L,\n      new Column(Array(\"NestedStruct\", \"ac\", \"aca\")) -> 1L,\n      new Column(\"VariantType\") -> 1L,\n      new Column(Array(\"NestedStruct\", \"nested_variant\")) -> 1L)\n\n    val tightBounds = false\n\n    val stats = new DataFileStatistics(\n      100,\n      minValues,\n      maxValues,\n      nullCount.map { case (k, v) => (k, java.lang.Long.valueOf(v)) }.asJava,\n      Optional.of(tightBounds))\n\n    val expectedJson =\n      \"\"\"{\n        |  \"numRecords\": 100,\n        |  \"minValues\": {\n        |    \"ByteType\": 1,\n        |    \"ShortType\": 1,\n        |    \"IntegerType\": 1,\n        |    \"LongType\": 1,\n        |    \"FloatType\": 0.1,\n        |    \"DoubleType\": 0.1,\n        |    \"DecimalType\": 123.45,\n        |    \"StringType\": \"a\",\n        |    \"DateType\": \"1970-01-02\",\n        |    \"TimestampType\": \"1970-01-01T00:00:00.000Z\",\n        |    \"TimestampNTZType\": \"1970-01-01T00:00:00\",\n        |    \"BinaryType\": \"a\",\n        |    \"NestedStruct\": {\n        |      \"aa\": \"a\",\n        |      \"ac\": {\n        |        \"aca\": 1\n        |      },\n        |      \"nested_variant\": \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\"\n        |    },\n        |    \"VariantType\": \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\"\n        |  },\n        |  \"maxValues\": {\n        |    \"ByteType\": 10,\n        |    \"ShortType\": 10,\n        |    \"IntegerType\": 10,\n        |    \"LongType\": 10,\n        |    \"FloatType\": 10.1,\n        |    \"DoubleType\": 10.1,\n        |    \"DecimalType\": 456.78,\n        |    \"StringType\": \"z\",\n        |    \"DateType\": \"1970-01-11\",\n        |    \"TimestampType\": \"1970-01-01T00:00:00.000Z\",\n        |    \"TimestampNTZType\": \"1970-01-01T00:00:00\",\n        |    \"BinaryType\": \"z\",\n        |    \"NestedStruct\": {\n        |      \"aa\": \"z\",\n        |      \"ac\": {\n        |        \"aca\": 10\n        |      },\n        |      \"nested_variant\": \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\"\n        |    },\n        |    \"VariantType\": \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\"\n        |  },\n        |  \"nullCount\": {\n        |    \"ByteType\": 1,\n        |    \"ShortType\": 1,\n        |    \"IntegerType\": 1,\n        |    \"LongType\": 1,\n        |    \"FloatType\": 1,\n        |    \"DoubleType\": 1,\n        |    \"DecimalType\": 1,\n        |    \"StringType\": 1,\n        |    \"DateType\": 1,\n        |    \"TimestampType\": 1,\n        |    \"TimestampNTZType\": 1,\n        |    \"BinaryType\": 1,\n        |    \"NestedStruct\": {\n        |      \"aa\": 1,\n        |      \"ac\": {\n        |        \"aca\": 1\n        |      },\n        |      \"nested_variant\": 1\n        |    },\n        |    \"VariantType\": 1\n        |},\n        |\"tightBounds\": false\n        |}\"\"\".stripMargin\n\n    val json = stats.serializeAsJson(schema)\n\n    assert(areJsonNodesEqual(json, expectedJson))\n  }\n\n  test(\"serializeAsJson handles NaN and Infinity correctly\") {\n    val schema = new StructType()\n      .add(\"FloatType\", FloatType.FLOAT)\n      .add(\"DoubleType\", DoubleType.DOUBLE)\n\n    val minValues = Map(\n      new Column(\"FloatType\") -> Literal.ofFloat(Float.NaN),\n      new Column(\"DoubleType\") -> Literal.ofDouble(Double.NegativeInfinity)).asJava\n\n    val maxValues = Map(\n      new Column(\"FloatType\") -> Literal.ofFloat(Float.PositiveInfinity),\n      new Column(\"DoubleType\") -> Literal.ofDouble(Double.NaN)).asJava\n\n    val stats = new DataFileStatistics(\n      1L,\n      minValues,\n      maxValues,\n      Collections.emptyMap[Column, java.lang.Long](),\n      Optional.empty())\n\n    val json = stats.serializeAsJson(schema)\n    val expectedJson =\n      \"\"\"{\n        |  \"numRecords\": 1,\n        |  \"minValues\": {\n        |    \"FloatType\": \"NaN\",\n        |    \"DoubleType\": \"-Infinity\"\n        |  },\n        |  \"maxValues\": {\n        |    \"FloatType\": \"Infinity\",\n        |    \"DoubleType\": \"NaN\"\n        |  },\n        |  \"nullCount\": {}\n        |}\"\"\".stripMargin\n\n    assert(areJsonNodesEqual(json, expectedJson))\n  }\n\n  test(\"serializeAsJson handles null values and null literals correctly\") {\n    val schema = new StructType()\n      .add(\"col1\", IntegerType.INTEGER)\n      .add(\"col2\", StringType.STRING)\n      .add(\"col3\", DoubleType.DOUBLE)\n      .add(\n        \"nested\",\n        new StructType()\n          .add(\"nestedCol1\", IntegerType.INTEGER)\n          .add(\"nestedCol2\", StringType.STRING))\n\n    val minValues = Map[Column, Literal](\n      new Column(\"col1\") -> Literal.ofInt(1),\n      new Column(\"col2\") -> null,\n      new Column(\"col3\") -> Literal.ofNull(DoubleType.DOUBLE),\n      new Column(Array(\"nested\", \"nestedCol1\")) -> Literal.ofInt(5),\n      new Column(Array(\"nested\", \"nestedCol2\")) -> Literal.ofNull(StringType.STRING)).asJava\n\n    val maxValues = Map[Column, Literal](\n      new Column(\"col2\") -> Literal.ofString(\"z\"),\n      new Column(\"col3\") -> null,\n      new Column(Array(\"nested\", \"nestedCol1\")) -> null,\n      new Column(Array(\"nested\", \"nestedCol2\")) -> Literal.ofString(\"zzz\")).asJava\n\n    val nullCount = Map(\n      new Column(\"col1\") -> 5L,\n      new Column(\"col2\") -> 0L,\n      new Column(Array(\"nested\", \"nestedCol1\")) -> 2L).map { case (k, v) =>\n      (k, java.lang.Long.valueOf(v))\n    }.asJava\n\n    val tightBounds = true\n\n    val stats = new DataFileStatistics(\n      100,\n      minValues,\n      maxValues,\n      nullCount,\n      Optional.of(tightBounds))\n\n    val expectedJson =\n      \"\"\"{\n        |  \"numRecords\": 100,\n        |  \"minValues\": {\n        |    \"col1\": 1,\n        |    \"col3\": null,\n        |    \"nested\": {\n        |      \"nestedCol1\": 5,\n        |      \"nestedCol2\": null\n        |    }\n        |  },\n        |  \"maxValues\": {\n        |    \"col2\": \"z\",\n        |    \"nested\": {\n        |      \"nestedCol2\": \"zzz\"\n        |    }\n        |  },\n        |  \"nullCount\": {\n        |    \"col1\": 5,\n        |    \"col2\": 0,\n        |    \"nested\": {\n        |      \"nestedCol1\": 2\n        |    }\n        |  },\n        |  \"tightBounds\": true\n        |}\"\"\".stripMargin\n\n    val json = stats.serializeAsJson(schema)\n    assert(areJsonNodesEqual(json, expectedJson))\n  }\n\n  test(\"serializeAsJson returns empty nested objects when nested map is empty\") {\n    val nestedSchema = new StructType()\n      .add(\"field1\", IntegerType.INTEGER)\n      .add(\"field2\", StringType.STRING)\n    val schema = new StructType().add(\"nested\", nestedSchema)\n    val stats = new DataFileStatistics(\n      50L,\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Optional.empty())\n    val expectedJson =\n      \"\"\"{\n        |  \"numRecords\": 50,\n        |  \"minValues\": {\"nested\": {}},\n        |  \"maxValues\": {\"nested\": {}},\n        |  \"nullCount\": {\"nested\": {}}\n        |}\"\"\".stripMargin\n    val json = stats.serializeAsJson(schema)\n    assert(areJsonNodesEqual(json, expectedJson))\n  }\n\n  test(\"serializeAsJson handles partially populated nested values\") {\n    val nestedSchema = new StructType()\n      .add(\"field1\", IntegerType.INTEGER)\n      .add(\"field2\", StringType.STRING)\n    val schema = new StructType().add(\"nested\", nestedSchema)\n    val minValues = Map(\n      new Column(Array(\"nested\", \"field1\")) -> Literal.ofInt(10)).asJava\n    val stats = new DataFileStatistics(\n      75L,\n      minValues,\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Optional.empty())\n    val expectedJson =\n      \"\"\"{\n        |  \"numRecords\": 75,\n        |  \"minValues\": {\n        |    \"nested\": {\n        |      \"field1\": 10\n        |    }\n        |  },\n        |  \"maxValues\": {\"nested\":{}},\n        |  \"nullCount\": {\"nested\":{}}\n        |}\"\"\".stripMargin\n    val json = stats.serializeAsJson(schema)\n    assert(areJsonNodesEqual(json, expectedJson))\n  }\n\n  test(\"deserialize invalid JSON structure throws KernelException\") {\n    val malformedJson =\n      \"\"\"{\n        |  \"numRecords\": \"invalid_value\",\n        |}\"\"\".stripMargin\n\n    val exception = intercept[KernelException] {\n      DataFileStatistics.deserializeFromJson(malformedJson, null)\n    }\n    assert(exception.getMessage.contains(\"Failed to parse JSON string\"))\n  }\n\n  test(\"serialization and deserialization of stats\") {\n    val numRecords = 123L\n    val dataSchema = new StructType().add(\"a\", IntegerType.INTEGER)\n    val stats = new DataFileStatistics(\n      numRecords,\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Optional.empty())\n\n    val json = stats.serializeAsJson(dataSchema)\n    val deserialized = DataFileStatistics.deserializeFromJson(json, null)\n\n    assert(deserialized.get().getNumRecords == stats.getNumRecords)\n  }\n\n  test(\"test equals and hashCode work correctly for DataFileStatistics\") {\n    // Setup common test data\n    val col1 = new Column(\"col1\")\n    val nestedField = new Column(Array(\"nested\", \"field\"))\n\n    // Create two identical stats objects\n    val commonMaps = () => {\n      val min = Map(col1 -> Literal.ofInt(10), nestedField -> Literal.ofString(\"value\")).asJava\n      val max = Map(col1 -> Literal.ofInt(100), nestedField -> Literal.ofString(\"zzzz\")).asJava\n      val nulls =\n        Map(col1 -> java.lang.Long.valueOf(5L), nestedField -> java.lang.Long.valueOf(2L)).asJava\n      (min, max, nulls)\n    }\n\n    val (min1, max1, nulls1) = commonMaps()\n    val (min2, max2, nulls2) = commonMaps()\n\n    val stats1 = new DataFileStatistics(100L, min1, max1, nulls1, Optional.empty())\n    val stats2 = new DataFileStatistics(100L, min2, max2, nulls2, Optional.empty())\n\n    // Stats with different value\n    val differentMin =\n      Map(col1 -> Literal.ofInt(20), nestedField -> Literal.ofString(\"value\")).asJava\n    val stats3 = new DataFileStatistics(100L, differentMin, max1, nulls1, Optional.empty())\n\n    // Stats with different structure\n    val differentCol = new Column(\"col2\")\n    val structureMaps =\n      Map(col1 -> Literal.ofInt(10), differentCol -> Literal.ofString(\"new\")).asJava\n    val stats4 =\n      new DataFileStatistics(100L, structureMaps, structureMaps, nulls1, Optional.empty())\n\n    // Empty stats\n    val emptyStats1 = new DataFileStatistics(\n      50L,\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Optional.empty())\n    val emptyStats2 = new DataFileStatistics(\n      50L,\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Optional.empty())\n    val emptyStats3 = new DataFileStatistics(\n      60L,\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Collections.emptyMap(),\n      Optional.empty())\n\n    // Equality tests\n    assert(\n      stats1 == stats2 && stats1.hashCode() == stats2.hashCode(),\n      \"Identical stats should be equal with same hash\")\n    assert(stats1 != stats3, \"Stats with different values should not be equal\")\n    assert(stats1 != stats4, \"Stats with different structure should not be equal\")\n    assert(stats1 != null && stats1 != \"string\", \"Stats should not equal null or different types\")\n\n    // Empty stats tests\n    assert(\n      emptyStats1 == emptyStats2 && emptyStats1.hashCode() == emptyStats2.hashCode(),\n      \"Empty stats with same records should be equal with same hash\")\n    assert(emptyStats1 != emptyStats3, \"Empty stats with different records should not be equal\")\n  }\n\n  test(\"serializeAsJson throws exception when literal type doesn't match schema data type\") {\n    val schema = new StructType()\n      .add(\"intCol\", IntegerType.INTEGER)\n      .add(\"doubleCol\", DoubleType.DOUBLE)\n      .add(\n        \"nested\",\n        new StructType()\n          .add(\"stringCol\", StringType.STRING))\n\n    val minValues = Map[Column, Literal](\n      new Column(\"intCol\") -> Literal.ofString(\"not an int\"),\n      new Column(\"doubleCol\") -> Literal.ofDouble(1.23),\n      new Column(Array(\"nested\", \"stringCol\")) -> Literal.ofInt(42)).asJava\n\n    val stats = new DataFileStatistics(\n      100,\n      minValues,\n      Collections.emptyMap[Column, Literal](),\n      Collections.emptyMap[Column, java.lang.Long](),\n      Optional.empty())\n\n    val exception = intercept[KernelException] {\n      stats.serializeAsJson(schema)\n    }\n\n    val expectedMessage = \"Type mismatch for field 'intCol' when writing statistics\" +\n      \": expected integer, but found string\"\n    assert(exception.getMessage === expectedMessage)\n  }\n\n  test(\"deserializeFromJson handles all data types correctly\") {\n    val schema = new StructType()\n      .add(\"ByteType\", ByteType.BYTE)\n      .add(\"ShortType\", ShortType.SHORT)\n      .add(\"IntegerType\", IntegerType.INTEGER)\n      .add(\"LongType\", LongType.LONG)\n      .add(\"FloatType\", FloatType.FLOAT)\n      .add(\"DoubleType\", DoubleType.DOUBLE)\n      .add(\"DecimalType\", new DecimalType(10, 2))\n      .add(\"StringType\", StringType.STRING)\n      .add(\"DateType\", DateType.DATE)\n      .add(\"TimestampType\", TimestampType.TIMESTAMP)\n      .add(\"TimestampNTZType\", TimestampNTZType.TIMESTAMP_NTZ)\n      .add(\"BinaryType\", BinaryType.BINARY)\n      .add(\"BooleanType\", BooleanType.BOOLEAN)\n      .add(\"VariantType\", VariantType.VARIANT)\n\n    val json =\n      \"\"\"{\n        |  \"numRecords\": 100,\n        |  \"minValues\": {\n        |    \"ByteType\": 1,\n        |    \"ShortType\": 1,\n        |    \"IntegerType\": 1,\n        |    \"LongType\": 1,\n        |    \"FloatType\": 0.1,\n        |    \"DoubleType\": 0.1,\n        |    \"DecimalType\": 123.45,\n        |    \"StringType\": \"a\",\n        |    \"DateType\": \"1970-01-02\",\n        |    \"TimestampType\": \"1970-01-01T00:00:00.001Z\",\n        |    \"TimestampNTZType\": \"1970-01-01T00:00:00.001\",\n        |    \"BinaryType\": \"a\",\n        |    \"BooleanType\": true,\n        |    \"VariantType\": \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\"\n        |  },\n        |  \"maxValues\": {\n        |    \"ByteType\": 10,\n        |    \"ShortType\": 10,\n        |    \"IntegerType\": 10,\n        |    \"LongType\": 10,\n        |    \"FloatType\": 10.1,\n        |    \"DoubleType\": 10.1,\n        |    \"DecimalType\": 456.78,\n        |    \"StringType\": \"z\",\n        |    \"DateType\": \"1970-01-11\",\n        |    \"TimestampType\": \"1970-01-01T00:00:00.010Z\",\n        |    \"TimestampNTZType\": \"1970-01-01T00:00:00.010\",\n        |    \"BinaryType\": \"z\",\n        |    \"BooleanType\": false,\n        |    \"VariantType\": \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\"\n        |  },\n        |  \"nullCount\": {\n        |    \"ByteType\": 1,\n        |    \"StringType\": 2,\n        |    \"DecimalType\": 0,\n        |    \"BooleanType\": 5\n        |  },\n        |  \"tightBounds\": true\n        |}\"\"\".stripMargin\n\n    val result = DataFileStatistics.deserializeFromJson(json, schema)\n    assert(result.isPresent)\n\n    val stats = result.get()\n    assert(stats.getNumRecords == 100)\n\n    val minValues = stats.getMinValues\n    assert(minValues.get(new Column(\"ByteType\")).getValue == 1.toByte)\n    assert(minValues.get(new Column(\"IntegerType\")).getValue == 1)\n    assert(minValues.get(new Column(\"FloatType\")).getValue == 0.1f)\n    assert(minValues.get(new Column(\"StringType\")).getValue == \"a\")\n    assert(minValues.get(new Column(\"BooleanType\")).getValue == true)\n    assert(minValues.get(new Column(\"VariantType\")).getValue ==\n      \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\")\n\n    val maxValues = stats.getMaxValues\n    assert(maxValues.get(new Column(\"LongType\")).getValue == 10L)\n    assert(maxValues.get(new Column(\"DoubleType\")).getValue == 10.1)\n    assert(maxValues.get(new Column(\"BooleanType\")).getValue == false)\n    assert(maxValues.get(new Column(\"VariantType\")).getValue ==\n      \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\")\n\n    val nullCount = stats.getNullCount\n    assert(nullCount.get(new Column(\"ByteType\")) == 1L)\n    assert(nullCount.get(new Column(\"StringType\")) == 2L)\n    assert(nullCount.get(new Column(\"DecimalType\")) == 0L)\n\n    assert(stats.getTightBounds.isPresent && stats.getTightBounds.get)\n  }\n\n  test(\"deserializeFromJson handles nested structures correctly\") {\n    val schema = new StructType()\n      .add(\"simple\", StringType.STRING)\n      .add(\n        \"nested\",\n        new StructType()\n          .add(\"field1\", IntegerType.INTEGER)\n          .add(\n            \"deep\",\n            new StructType()\n              .add(\"field2\", StringType.STRING)\n              .add(\n                \"deeper\",\n                new StructType()\n                  .add(\"field3\", IntegerType.INTEGER))))\n\n    val json =\n      \"\"\"{\n        |  \"numRecords\": 50,\n        |  \"minValues\": {\n        |    \"simple\": \"value1\",\n        |    \"nested\": {\n        |      \"field1\": 10,\n        |      \"deep\": {\n        |        \"field2\": \"nested_value\",\n        |        \"deeper\": {\n        |          \"field3\": 42\n        |        }\n        |      }\n        |    }\n        |  },\n        |  \"maxValues\": {\n        |    \"simple\": \"value2\",\n        |    \"nested\": {\n        |      \"field1\": 100,\n        |      \"deep\": {\n        |        \"field2\": \"zzz_value\"\n        |      }\n        |    }\n        |  },\n        |  \"nullCount\": {\n        |    \"simple\": 0,\n        |    \"nested\": {\n        |      \"field1\": 5,\n        |      \"deep\": {\n        |        \"field2\": 2,\n        |        \"deeper\": {\n        |          \"field3\": 1\n        |        }\n        |      }\n        |    }\n        |  },\n        |  \"tightBounds\": true\n        |}\"\"\".stripMargin\n\n    val result = DataFileStatistics.deserializeFromJson(json, schema)\n    assert(result.isPresent)\n\n    val stats = result.get()\n    assert(stats.getNumRecords == 50)\n\n    // Test simple column\n    val minValues = stats.getMinValues\n    assert(minValues.get(new Column(\"simple\")).getValue == \"value1\")\n\n    // Test nested columns with different path depths\n    assert(minValues.get(new Column(Array(\"nested\", \"field1\"))).getValue == 10)\n    assert(minValues.get(new Column(Array(\"nested\", \"deep\", \"field2\"))).getValue == \"nested_value\")\n    assert(minValues.get(new Column(Array(\"nested\", \"deep\", \"deeper\", \"field3\"))).getValue == 42)\n\n    // Test that max values work for nested too\n    val maxValues = stats.getMaxValues\n    assert(maxValues.get(new Column(Array(\"nested\", \"field1\"))).getValue == 100)\n    assert(maxValues.get(new Column(Array(\"nested\", \"deep\", \"field2\"))).getValue == \"zzz_value\")\n\n    // Test null counts for nested\n    val nullCount = stats.getNullCount\n    assert(nullCount.get(new Column(Array(\"nested\", \"field1\"))) == 5L)\n    assert(nullCount.get(new Column(Array(\"nested\", \"deep\", \"deeper\", \"field3\"))) == 1L)\n\n    // Test tight bounds for nested columns\n    assert(stats.getTightBounds.isPresent && stats.getTightBounds.get)\n  }\n\n  test(\"round-trip serialization and deserialization consistency\") {\n    val nestedStructType = new StructType()\n      .add(\"aa\", StringType.STRING)\n      .add(\"ac\", new StructType().add(\"aca\", IntegerType.INTEGER))\n\n    val schema = new StructType()\n      .add(\"IntegerType\", IntegerType.INTEGER)\n      .add(\"StringType\", StringType.STRING)\n      .add(\"DoubleType\", DoubleType.DOUBLE)\n      .add(\"NestedStruct\", nestedStructType)\n\n    val minValues = Map(\n      new Column(\"IntegerType\") -> Literal.ofInt(1),\n      new Column(\"StringType\") -> Literal.ofString(\"a\"),\n      new Column(\"DoubleType\") -> Literal.ofDouble(0.1),\n      new Column(Array(\"NestedStruct\", \"aa\")) -> Literal.ofString(\"nested_a\"),\n      new Column(Array(\"NestedStruct\", \"ac\", \"aca\")) -> Literal.ofInt(5)).asJava\n\n    val maxValues = Map(\n      new Column(\"IntegerType\") -> Literal.ofInt(100),\n      new Column(\"StringType\") -> Literal.ofString(\"z\"),\n      new Column(\"DoubleType\") -> Literal.ofDouble(99.9),\n      new Column(Array(\"NestedStruct\", \"aa\")) -> Literal.ofString(\"nested_z\"),\n      new Column(Array(\"NestedStruct\", \"ac\", \"aca\")) -> Literal.ofInt(50)).asJava\n\n    val nullCount = Map(\n      new Column(\"IntegerType\") -> 2L,\n      new Column(\"StringType\") -> 0L,\n      new Column(Array(\"NestedStruct\", \"aa\")) -> 1L).map { case (k, v) =>\n      (k, java.lang.Long.valueOf(v))\n    }.asJava\n\n    val tightBounds = false\n\n    val originalStats = new DataFileStatistics(\n      123L,\n      minValues,\n      maxValues,\n      nullCount,\n      Optional.of(tightBounds))\n\n    // Serialize then deserialize\n    val json = originalStats.serializeAsJson(schema)\n    val deserializedOpt = DataFileStatistics.deserializeFromJson(json, schema)\n\n    assert(deserializedOpt.isPresent)\n    val deserializedStats = deserializedOpt.get()\n\n    // Verify they are equal\n    assert(deserializedStats.getNumRecords == originalStats.getNumRecords)\n    assert(deserializedStats.getMinValues.size() == originalStats.getMinValues.size())\n    assert(deserializedStats.getMaxValues.size() == originalStats.getMaxValues.size())\n    assert(deserializedStats.getNullCount.size() == originalStats.getNullCount.size())\n\n    // Verify specific values match\n    assert(deserializedStats.getMinValues.get(new Column(\"IntegerType\")).getValue == 1)\n    assert(deserializedStats.getMaxValues.get(new Column(Array(\n      \"NestedStruct\",\n      \"ac\",\n      \"aca\"))).getValue == 50)\n    assert(deserializedStats.getNullCount.get(new Column(\"StringType\")) == 0L)\n\n    assert(deserializedStats.getTightBounds == originalStats.getTightBounds)\n\n  }\n\n  test(\"deserializeFromJson handles NaN and Infinity correctly\") {\n    val schema = new StructType()\n      .add(\"FloatType\", FloatType.FLOAT)\n      .add(\"DoubleType\", DoubleType.DOUBLE)\n\n    val json =\n      \"\"\"{\n        |  \"numRecords\": 10,\n        |  \"minValues\": {\n        |    \"FloatType\": \"NaN\",\n        |    \"DoubleType\": \"-Infinity\"\n        |  },\n        |  \"maxValues\": {\n        |    \"FloatType\": \"Infinity\",\n        |    \"DoubleType\": \"NaN\"\n        |  },\n        |  \"nullCount\": {\n        |    \"FloatType\": 1,\n        |    \"DoubleType\": 2\n        |  },\n        |  \"tightBounds\": true\n        |}\"\"\".stripMargin\n\n    val result = DataFileStatistics.deserializeFromJson(json, schema)\n    assert(result.isPresent)\n\n    val stats = result.get()\n    assert(stats.getNumRecords == 10)\n\n    val minValues = stats.getMinValues\n    val maxValues = stats.getMaxValues\n\n    // Test NaN and Infinity values - Note: Float values will be stored as Float, not Double\n    assert(\n      java.lang.Float.isNaN(minValues.get(new Column(\"FloatType\")).getValue.asInstanceOf[Float]))\n    assert(minValues.get(new Column(\"DoubleType\")).getValue == Double.NegativeInfinity)\n    assert(maxValues.get(new Column(\"FloatType\")).getValue == Float.PositiveInfinity)\n    assert(\n      java.lang.Double.isNaN(maxValues.get(new Column(\"DoubleType\")).getValue.asInstanceOf[Double]))\n\n    val nullCount = stats.getNullCount\n    assert(nullCount.get(new Column(\"FloatType\")) == 1L)\n    assert(nullCount.get(new Column(\"DoubleType\")) == 2L)\n\n    assert(stats.getTightBounds.isPresent && stats.getTightBounds.get)\n  }\n\n  test(\"deserializeFromJson handles empty stats correctly\") {\n    val schema = new StructType() // Empty schema for empty stats\n\n    val json =\n      \"\"\"{\n        |  \"numRecords\": 42,\n        |  \"minValues\": {},\n        |  \"maxValues\": {},\n        |  \"nullCount\": {}\n        |}\"\"\".stripMargin\n\n    val result = DataFileStatistics.deserializeFromJson(json, schema)\n    assert(result.isPresent)\n\n    val stats = result.get()\n    assert(stats.getNumRecords == 42)\n    assert(stats.getMinValues.isEmpty)\n    assert(stats.getMaxValues.isEmpty)\n    assert(stats.getNullCount.isEmpty)\n    assert(!stats.getTightBounds.isPresent)\n  }\n\n  test(\"deserializeFromJson handles partial nested objects correctly\") {\n    // Schema should include all possible fields that appear in the JSON\n    val schema = new StructType()\n      .add(\"simple\", StringType.STRING)\n      .add(\n        \"nested\",\n        new StructType()\n          .add(\"field1\", IntegerType.INTEGER)\n          .add(\"field2\", StringType.STRING))\n      .add(\"other\", IntegerType.INTEGER)\n\n    val json =\n      \"\"\"{\n        |  \"numRecords\": 25,\n        |  \"minValues\": {\n        |    \"simple\": \"value\",\n        |    \"nested\": {\n        |      \"field1\": 10\n        |    }\n        |  },\n        |  \"maxValues\": {\n        |    \"nested\": {\n        |      \"field2\": \"different_field\"\n        |    },\n        |    \"other\": 99\n        |  },\n        |  \"nullCount\": {\n        |    \"simple\": 1,\n        |    \"nested\": {\n        |      \"field1\": 0,\n        |      \"field2\": 5\n        |    }\n        |  },\n        |\"tightBounds\": true\n        |}\"\"\".stripMargin\n\n    val result = DataFileStatistics.deserializeFromJson(json, schema)\n    assert(result.isPresent)\n\n    val stats = result.get()\n    assert(stats.getNumRecords == 25)\n\n    val minValues = stats.getMinValues\n    val maxValues = stats.getMaxValues\n    val nullCount = stats.getNullCount\n\n    // minValues has simple + nested.field1\n    assert(minValues.get(new Column(\"simple\")).getValue == \"value\")\n    assert(minValues.get(new Column(Array(\"nested\", \"field1\"))).getValue == 10)\n    assert(minValues.get(new Column(Array(\"nested\", \"field2\"))) == null) // not present in minValues\n\n    // maxValues has nested.field2 + other (different structure)\n    assert(maxValues.get(new Column(\"simple\")) == null) // not present in maxValues\n    assert(maxValues.get(new Column(Array(\"nested\", \"field2\"))).getValue == \"different_field\")\n    assert(maxValues.get(new Column(\"other\")).getValue == 99)\n\n    // nullCount has both fields under nested\n    assert(nullCount.get(new Column(\"simple\")) == 1L)\n    assert(nullCount.get(new Column(Array(\"nested\", \"field1\"))) == 0L)\n    assert(nullCount.get(new Column(Array(\"nested\", \"field2\"))) == 5L)\n\n    // tightBounds has simple + nested.field2 + other\n    assert(stats.getTightBounds.isPresent && stats.getTightBounds.get)\n\n  }\n\n  test(\"withoutTightBounds removes tight bounds from DataFileStatistics\") {\n    val schema = new StructType()\n      .add(\"col1\", IntegerType.INTEGER)\n      .add(\"col2\", StringType.STRING)\n      .add(\n        \"nested\",\n        new StructType()\n          .add(\"field1\", IntegerType.INTEGER)\n          .add(\"field2\", StringType.STRING))\n\n    // stats with a mix of true and false tight bounds\n    val minValues = Map(\n      new Column(\"col1\") -> Literal.ofInt(1),\n      new Column(\"col2\") -> Literal.ofString(\"a\"),\n      new Column(Array(\"nested\", \"field1\")) -> Literal.ofInt(10),\n      new Column(Array(\"nested\", \"field2\")) -> Literal.ofString(\"nested_a\")).asJava\n\n    val maxValues = Map(\n      new Column(\"col1\") -> Literal.ofInt(100),\n      new Column(\"col2\") -> Literal.ofString(\"z\"),\n      new Column(Array(\"nested\", \"field1\")) -> Literal.ofInt(200),\n      new Column(Array(\"nested\", \"field2\")) -> Literal.ofString(\"nested_z\")).asJava\n\n    val nullCount = Map(\n      new Column(\"col1\") -> 5L,\n      new Column(\"col2\") -> 0L,\n      new Column(Array(\"nested\", \"field1\")) -> 2L,\n      new Column(Array(\"nested\", \"field2\")) -> 3L).map { case (k, v) =>\n      (k, java.lang.Long.valueOf(v))\n    }.asJava\n\n    val originalTightBounds = true\n\n    val originalStats = new DataFileStatistics(\n      100L,\n      minValues,\n      maxValues,\n      nullCount,\n      Optional.of(originalTightBounds))\n\n    // Test that original stats has tight bounds\n    assert(originalStats.getTightBounds.isPresent && originalStats.getTightBounds.get)\n\n    // Apply withoutTightBounds\n    val statsWithoutTightBounds = originalStats.withoutTightBounds()\n\n    // Verify all other fields remain unchanged\n    assert(statsWithoutTightBounds.getNumRecords == originalStats.getNumRecords)\n    assert(statsWithoutTightBounds.getMinValues == originalStats.getMinValues)\n    assert(statsWithoutTightBounds.getMaxValues == originalStats.getMaxValues)\n    assert(statsWithoutTightBounds.getNullCount == originalStats.getNullCount)\n\n    // Verify tight bounds is now false\n    assert(statsWithoutTightBounds.getTightBounds.isPresent\n      && !statsWithoutTightBounds.getTightBounds.get)\n\n    // Verify serialization reflects the change\n    val jsonAfter = statsWithoutTightBounds.serializeAsJson(schema)\n    val expectedJsonWithFalseTightBounds =\n      \"\"\"{\n        |  \"numRecords\": 100,\n        |  \"minValues\": {\n        |    \"col1\": 1,\n        |    \"col2\": \"a\",\n        |    \"nested\": {\n        |      \"field1\": 10,\n        |      \"field2\": \"nested_a\"\n        |    }\n        |  },\n        |  \"maxValues\": {\n        |    \"col1\": 100,\n        |    \"col2\": \"z\",\n        |    \"nested\": {\n        |      \"field1\": 200,\n        |      \"field2\": \"nested_z\"\n        |    }\n        |  },\n        |  \"nullCount\": {\n        |    \"col1\": 5,\n        |    \"col2\": 0,\n        |    \"nested\": {\n        |      \"field1\": 2,\n        |      \"field2\": 3\n        |    }\n        |  },\n        |  \"tightBounds\": false\n        |}\"\"\".stripMargin\n\n    assert(areJsonNodesEqual(jsonAfter, expectedJsonWithFalseTightBounds))\n\n    // Test edge case: stats with already false tight bounds\n\n    val statsAlreadyFalse = new DataFileStatistics(\n      50L,\n      Map(new Column(\"col1\") -> Literal.ofInt(1)).asJava,\n      Map(new Column(\"col1\") -> Literal.ofInt(10)).asJava,\n      Map(new Column(\"col1\") -> java.lang.Long.valueOf(0L)).asJava,\n      Optional.of(false))\n\n    val resultAlreadyFalse = statsAlreadyFalse.withoutTightBounds()\n    assert(resultAlreadyFalse.getTightBounds.isPresent &&\n      !resultAlreadyFalse.getTightBounds.get)\n\n    // Test edge case: stats with empty tight bounds\n    val emptyTightBoundsStats = new DataFileStatistics(\n      25L,\n      minValues,\n      maxValues,\n      nullCount,\n      Optional.empty())\n\n    val resultFromEmpty = emptyTightBoundsStats.withoutTightBounds()\n    assert(resultFromEmpty.getTightBounds.isPresent &&\n      !resultFromEmpty.getTightBounds.get)\n  }\n\n  test(\"deserializing invalid variant stats throws KernelException\") {\n    val schema = new StructType().add(\"VariantType\", VariantType.VARIANT);\n    val invalidVariantStats =\n      \"\"\"|{\n         |  \"numRecords\": 100,\n         |  \"minValues\": {\n         |    \"VariantType\": 1234\n         |  },\n         |  \"maxValues\": {\n         |    \"VariantType\": 5678\n         |  }\n         |}\"\"\".stripMargin\n\n    val exception = intercept[KernelException] {\n      DataFileStatistics.deserializeFromJson(invalidVariantStats, schema)\n    }\n\n    assert(exception.getMessage.contains(\"Expected variant as string value\"))\n  }\n\n  test(\"serializeAsJson throws exception when literal type for variant is not string\") {\n    val schema = new StructType()\n      .add(\"variant\", VariantType.VARIANT)\n\n    val minValues = Map[Column, Literal](\n      new Column(\"variant\") -> Literal.ofInt(1)).asJava\n\n    val stats = new DataFileStatistics(\n      100,\n      minValues,\n      Collections.emptyMap[Column, Literal](),\n      Collections.emptyMap[Column, java.lang.Long](),\n      Optional.empty())\n\n    val exception = intercept[KernelException] {\n      stats.serializeAsJson(schema)\n    }\n\n    val expectedMessage = \"Type mismatch for field 'variant' when writing statistics\" +\n      \": expected string, but found integer\"\n    assert(exception.getMessage === expectedMessage)\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/FileNamesSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames._\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass FileNamesSuite extends AnyFunSuite {\n\n  private val checkpointV1 = \"/a/123.checkpoint.parquet\"\n  private val checkpointMultiPart = \"/a/123.checkpoint.0000000001.0000000087.parquet\"\n  private val checkpointV2Json = \"/a/000000010.checkpoint.80a083e8-7026.json\"\n  private val checkpointV2Parquet = \"/a/000000010.checkpoint.80a083e8-7026.parquet\"\n  private val commitNormal = \"/a/0000000088.json\"\n  private val commitUUID = \"/a/00000022.dc0f9f58-a1a0.json\"\n\n  /////////////////////////////\n  // Version extractor tests //\n  /////////////////////////////\n\n  test(\"checkpointVersion\") {\n    assert(checkpointVersion(new Path(checkpointV1)) == 123)\n    assert(checkpointVersion(new Path(checkpointMultiPart)) == 123)\n    assert(checkpointVersion(new Path(checkpointV2Json)) == 10)\n    assert(checkpointVersion(new Path(checkpointV2Parquet)) == 10)\n  }\n\n  test(\"deltaVersion\") {\n    assert(deltaVersion(new Path(commitNormal)) == 88)\n    assert(deltaVersion(new Path(commitUUID)) == 22)\n  }\n\n  test(\"getFileVersion\") {\n    assert(getFileVersion(new Path(checkpointV1)) == 123)\n    assert(getFileVersion(new Path(checkpointMultiPart)) == 123)\n    assert(getFileVersion(new Path(checkpointV2Json)) == 10)\n    assert(getFileVersion(new Path(checkpointV2Parquet)) == 10)\n    assert(getFileVersion(new Path(commitNormal)) == 88)\n    assert(getFileVersion(new Path(commitUUID)) == 22)\n  }\n\n  /////////////////////////////////////////\n  // File path and prefix builders tests //\n  /////////////////////////////////////////\n\n  test(\"deltaFile\") {\n    assert(deltaFile(new Path(\"/a\"), 1234) == \"/a/00000000000000001234.json\")\n  }\n\n  test(\"sidecarFile\") {\n    assert(sidecarFile(new Path(\"/a\"), \"7d17ac10.parquet\") == \"/a/_sidecars/7d17ac10.parquet\")\n  }\n\n  test(\"listingPrefix\") {\n    assert(listingPrefix(new Path(\"/a\"), 1234) == \"/a/00000000000000001234.\")\n  }\n\n  test(\"checkpointFileSingular\") {\n    assert(\n      checkpointFileSingular(new Path(\"/a\"), 1234).toString ==\n        \"/a/00000000000000001234.checkpoint.parquet\")\n  }\n\n  test(\"topLevelV2CheckpointFile\") {\n    assert(\n      topLevelV2CheckpointFile(new Path(\"/a\"), 1234, \"7d17ac10\", \"json\").toString ==\n        \"/a/00000000000000001234.checkpoint.7d17ac10.json\")\n    assert(\n      topLevelV2CheckpointFile(new Path(\"/a\"), 1234, \"7d17ac10\", \"parquet\").toString ==\n        \"/a/00000000000000001234.checkpoint.7d17ac10.parquet\")\n  }\n\n  test(\"v2CheckpointSidecarFile\") {\n    assert(\n      v2CheckpointSidecarFile(new Path(\"/a\"), \"7d17ac10\").toString ==\n        \"/a/_sidecars/7d17ac10.parquet\")\n  }\n\n  test(\"checkpointFileWithParts\") {\n    assert(checkpointFileWithParts(new Path(\"/a\"), 1, 1).asScala == Seq(\n      new Path(\"/a/00000000000000000001.checkpoint.0000000001.0000000001.parquet\")))\n    assert(checkpointFileWithParts(new Path(\"/a\"), 1, 2).asScala == Seq(\n      new Path(\"/a/00000000000000000001.checkpoint.0000000001.0000000002.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000002.0000000002.parquet\")))\n    assert(checkpointFileWithParts(new Path(\"/a\"), 1, 5).asScala == Seq(\n      new Path(\"/a/00000000000000000001.checkpoint.0000000001.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000002.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000003.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000004.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000005.0000000005.parquet\")))\n  }\n\n  test(\"logCompactionPath\") {\n    assert(logCompactionPath(new Path(\"/a\"), 1, 3) ==\n      new Path(\"/a/00000000000000000001.00000000000000000003.compacted.json\"))\n    assert(logCompactionPath(new Path(\"/a/b\"), 11, 300) ==\n      new Path(\"/a/b/00000000000000000011.00000000000000000300.compacted.json\"))\n  }\n\n  ///////////////////////////////////\n  // Is <type> file checkers tests //\n  ///////////////////////////////////\n\n  test(\"is checkpoint file\") {\n    // ===== V1 checkpoint =====\n    // Positive cases\n    assert(isCheckpointFile(checkpointV1))\n    assert(isCheckpointFile(new Path(checkpointV1).getName))\n    assert(isClassicCheckpointFile(checkpointV1))\n    assert(isClassicCheckpointFile(new Path(checkpointV1).getName))\n    // Negative cases\n    assert(!isMultiPartCheckpointFile(checkpointV1))\n    assert(!isV2CheckpointFile(checkpointV1))\n    assert(!isCommitFile(checkpointV1))\n\n    // ===== Multipart checkpoint =====\n    // Positive cases\n    assert(isCheckpointFile(checkpointMultiPart))\n    assert(isCheckpointFile(new Path(checkpointMultiPart).getName))\n    assert(isMultiPartCheckpointFile(checkpointMultiPart))\n    assert(isMultiPartCheckpointFile(new Path(checkpointMultiPart).getName))\n    // Negative cases\n    assert(!isClassicCheckpointFile(checkpointMultiPart))\n    assert(!isV2CheckpointFile(checkpointMultiPart))\n    assert(!isCommitFile(checkpointMultiPart))\n\n    // ===== V2 checkpoint =====\n    // Positive cases\n    assert(isCheckpointFile(checkpointV2Json))\n    assert(isCheckpointFile(new Path(checkpointV2Json).getName))\n    assert(isV2CheckpointFile(checkpointV2Json))\n    assert(isV2CheckpointFile(new Path(checkpointV2Json).getName))\n    assert(isCheckpointFile(checkpointV2Parquet))\n    assert(isCheckpointFile(new Path(checkpointV2Parquet).getName))\n    assert(isV2CheckpointFile(checkpointV2Parquet))\n    assert(isV2CheckpointFile(new Path(checkpointV2Parquet).getName))\n    // Negative cases\n    assert(!isClassicCheckpointFile(checkpointV2Json))\n    assert(!isClassicCheckpointFile(checkpointV2Parquet))\n    assert(!isMultiPartCheckpointFile(checkpointV2Json))\n    assert(!isMultiPartCheckpointFile(checkpointV2Parquet))\n    assert(!isCommitFile(checkpointV2Json))\n    assert(!isCommitFile(checkpointV2Parquet))\n\n    // ===== Others =====\n    assert(!isCheckpointFile(\"/a/123.json\"))\n    assert(!isCommitFile(\"/a/123.checkpoint.3.json\"))\n  }\n\n  test(\"is commit file\") {\n    assert(isCommitFile(commitNormal))\n    assert(isCommitFile(commitUUID))\n  }\n\n  test(\"determineFileType correctly identifies delta log file types\") {\n    // Test commit file detection\n    val commitFile = FileStatus.of(\"/path/00000000000000000001.json\", 100, 1000)\n    assert(FileNames.determineFileType(commitFile) === DeltaLogFileType.COMMIT)\n\n    // Test checkpoint file detection\n    val checkpointFile = FileStatus.of(\"/path/00000000000000000002.checkpoint.parquet\", 100, 1000)\n    assert(FileNames.determineFileType(checkpointFile) === DeltaLogFileType.CHECKPOINT)\n\n    // Test checksum file detection\n    val checksumFile = FileStatus.of(\"/path/00000000000000000003.crc\", 100, 1000)\n    assert(FileNames.determineFileType(checksumFile) === DeltaLogFileType.CHECKSUM)\n\n    // Test exception for unknown file type\n    val unknownFile = FileStatus.of(\"/path/unknown_file.txt\", 100, 1000)\n    val exception = intercept[IllegalStateException] {\n      FileNames.determineFileType(unknownFile)\n    }\n    assert(exception.getMessage.contains(\"Unexpected file type\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/IntervalParserUtilsSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport io.delta.kernel.internal.util.DateTimeConstants._\nimport io.delta.kernel.internal.util.IntervalParserUtils.parseIntervalAsMicros\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Subset of tests from Apache Spark's `org/apache/spark/sql/catalyst/util/IntervalUtilsSuite.scala`\n */\nclass IntervalParserUtilsSuite extends AnyFunSuite {\n  test(\"string to interval: basic\") {\n    testSingleUnit(\"Week\", 3, 21, 0)\n    testSingleUnit(\"DAY\", 3, 3, 0)\n    testSingleUnit(\"HouR\", 3, 0, 3 * MICROS_PER_HOUR)\n    testSingleUnit(\"MiNuTe\", 3, 0, 3 * MICROS_PER_MINUTE)\n    testSingleUnit(\"Second\", 3, 0, 3 * MICROS_PER_SECOND)\n    testSingleUnit(\"MilliSecond\", 3, 0, 3 * MICROS_PER_MILLIS)\n    testSingleUnit(\"MicroSecond\", 3, 0, 3)\n\n    checkFromInvalidString(null, \"cannot be null\")\n\n    Seq(\n      \"\",\n      \"interval\",\n      \"foo\",\n      \"foo 1 day\",\n      \"month 3\",\n      \"year 3\").foreach { input =>\n      checkFromInvalidString(input, \"Error parsing\")\n    }\n  }\n\n  test(\"string to interval: interval with dangling parts should not results null\") {\n    checkFromInvalidString(\"+\", \"expect a number after '+' but hit EOL\")\n    checkFromInvalidString(\"-\", \"expect a number after '-' but hit EOL\")\n    checkFromInvalidString(\"+ 2\", \"expect a unit name after '2' but hit EOL\")\n    checkFromInvalidString(\"- 1\", \"expect a unit name after '1' but hit EOL\")\n    checkFromInvalidString(\"1\", \"expect a unit name after '1' but hit EOL\")\n    checkFromInvalidString(\"1.2\", \"expect a unit name after '1.2' but hit EOL\")\n    checkFromInvalidString(\"1 day 2\", \"expect a unit name after '2' but hit EOL\")\n    checkFromInvalidString(\"1 day 2.2\", \"expect a unit name after '2.2' but hit EOL\")\n    checkFromInvalidString(\"1 day -\", \"expect a number after '-' but hit EOL\")\n    checkFromInvalidString(\"-.\", \"expect a unit name after '-.' but hit EOL\")\n  }\n\n  test(\"string to interval: multiple units\") {\n    Seq(\n      \"interval -1 day +3 Microseconds\" -> micros(-1, 3),\n      \"interval -   1 day +     3 Microseconds\" -> micros(-1, 3),\n      \"  interval  123  weeks   -1 day \" +\n        \"23 hours -22 minutes 1 second  -123  millisecond    567 microseconds \" ->\n        micros(860, 81480877567L)).foreach { case (input, expected) =>\n      checkFromString(input, expected)\n    }\n  }\n\n  test(\"string to interval: special cases\") {\n    // Support any order of interval units\n    checkFromString(\"1 microsecond 1 day\", micros(1, 1))\n    // Allow duplicated units and summarize their values\n    checkFromString(\"1 day 10 day\", micros(11, 0))\n    // Only the seconds units can have the fractional part\n    checkFromInvalidString(\"1.5 days\", \"'days' cannot have fractional part\")\n    checkFromInvalidString(\"1. hour\", \"'hour' cannot have fractional part\")\n    checkFromInvalidString(\"1 hourX\", \"invalid unit 'hourx'\")\n    checkFromInvalidString(\"~1 hour\", \"unrecognized number '~1'\")\n    checkFromInvalidString(\"1 Mour\", \"invalid unit 'mour'\")\n    checkFromInvalidString(\"1 aour\", \"invalid unit 'aour'\")\n    checkFromInvalidString(\"1a1 hour\", \"invalid value '1a1'\")\n    checkFromInvalidString(\"1.1a1 seconds\", \"invalid value '1.1a1'\")\n    checkFromInvalidString(\"2234567890 days\", \"integer overflow\")\n    checkFromInvalidString(\". seconds\", \"invalid value '.'\")\n  }\n\n  test(\"string to interval: whitespaces\") {\n    checkFromInvalidString(\" \", \"Error parsing ' ' to interval\")\n    checkFromInvalidString(\"\\n\", \"Error parsing '\\n' to interval\")\n    checkFromInvalidString(\"\\t\", \"Error parsing '\\t' to interval\")\n    checkFromString(\"1 \\t day \\n 2 \\r hour\", micros(1, 2 * MICROS_PER_HOUR))\n    checkFromInvalidString(\"interval1 \\t day \\n 2 \\r hour\", \"invalid interval prefix interval1\")\n    checkFromString(\"interval\\r1\\tday\", micros(1, 0))\n    // scalastyle:off nonascii\n    checkFromInvalidString(\"中国 interval 1 day\", \"unrecognized number '中国'\")\n    checkFromInvalidString(\"interval浙江 1 day\", \"invalid interval prefix interval浙江\")\n    checkFromInvalidString(\"interval 1杭州 day\", \"invalid value '1杭州'\")\n    checkFromInvalidString(\"interval 1 滨江day\", \"invalid unit '滨江day'\")\n    checkFromInvalidString(\"interval 1 day长河\", \"invalid unit 'day长河'\")\n    checkFromInvalidString(\"interval 1 day 网商路\", \"unrecognized number '网商路'\")\n    // scalastyle:on nonascii\n  }\n\n  test(\"string to interval: seconds with fractional part\") {\n    checkFromString(\"0.1 seconds\", micros(0, 100000))\n    checkFromString(\"1. seconds\", micros(0, 1000000))\n    checkFromString(\"123.001 seconds\", micros(0, 123001000))\n    checkFromString(\"1.001001 seconds\", micros(0, 1001001))\n    checkFromString(\"1 minute 1.001001 seconds\", micros(0, 61001001))\n    checkFromString(\"-1.5 seconds\", micros(0, -1500000))\n    // truncate nanoseconds to microseconds\n    checkFromString(\"0.999999999 seconds\", micros(0, 999999))\n    checkFromString(\".999999999 seconds\", micros(0, 999999))\n    checkFromInvalidString(\"0.123456789123 seconds\", \"'0.123456789123' is out of range\")\n  }\n\n  private def testSingleUnit(unit: String, number: Int, days: Int, microseconds: Long): Unit = {\n    for (prefix <- Seq(\"interval \", \"\")) {\n      val input1 = prefix + number + \" \" + unit\n      val input2 = prefix + number + \" \" + unit + \"s\"\n      val result = micros(days, microseconds)\n      checkFromString(input1, result)\n      checkFromString(input2, result)\n    }\n  }\n\n  private def checkFromString(input: String, expected: Long): Unit = {\n    assert(parseIntervalAsMicros(input) === expected)\n  }\n\n  private def checkFromInvalidString(input: String, errorMsg: String): Unit = {\n    failFuncWithInvalidInput(input, errorMsg, s => parseIntervalAsMicros(s))\n  }\n\n  private def failFuncWithInvalidInput(\n      input: String,\n      errorMsg: String,\n      converter: String => Long): Unit = {\n    withClue(s\"Expected to throw an exception for the invalid input: $input\") {\n      val e = intercept[IllegalArgumentException](converter(input))\n      assert(e.getMessage.contains(errorMsg))\n    }\n  }\n\n  private def micros(days: Long, microseconds: Long): Long = {\n    days * MICROS_PER_DAY + microseconds\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/JsonUtilsSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass JsonUtilsSuite extends AnyFunSuite {\n  test(\"Parse Map[String, String] JSON - positive case\") {\n    val expMap = Map(\"key1\" -> \"string_value\", \"key2Int\" -> \"2\", \"key3ComplexStr\" -> \"\\\"hello\\\"\")\n    val input = \"\"\"{\"key1\": \"string_value\", \"key2Int\": \"2\", \"key3ComplexStr\": \"\\\"hello\\\"\"}\"\"\"\n    assert(JsonUtils.parseJSONKeyValueMap(input) === expMap.asJava)\n\n    assert(JsonUtils.parseJSONKeyValueMap(\"\").isEmpty)\n    assert(JsonUtils.parseJSONKeyValueMap(null).isEmpty)\n  }\n\n  test(\"Parse Map[String, String] JSON - negative case\") {\n    val e = intercept[KernelException] {\n      JsonUtils.parseJSONKeyValueMap(\"\"\"{\"key1\": \"string_value\", asdf\"}\"\"\")\n    }\n    assert(e.getMessage.contains(\"Failed to parse JSON string:\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/PartitionUtilsSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport java.util\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions._\nimport io.delta.kernel.expressions.Literal._\nimport io.delta.kernel.internal.util.PartitionUtils._\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PartitionUtilsSuite extends AnyFunSuite {\n  private val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\")\n  private val unicode = CollationIdentifier.fromString(\"ICU.UNICODE\")\n\n  // Table schema\n  // Data columns: data1: int, data2: string, date3: struct(data31: boolean, data32: long)\n  // Partition columns: part1: int, part2: date, part3: string\n  val tableSchema = new StructType()\n    .add(\"data1\", IntegerType.INTEGER)\n    .add(\"data2\", StringType.STRING)\n    .add(\n      \"data3\",\n      new StructType()\n        .add(\"data31\", BooleanType.BOOLEAN)\n        .add(\"data32\", LongType.LONG))\n    .add(\"part1\", IntegerType.INTEGER)\n    .add(\"part2\", DateType.DATE)\n    .add(\"part3\", StringType.STRING)\n\n  private val partitionColsMetadata = new util.HashMap[String, StructField]() {\n    {\n      put(\"part1\", tableSchema.get(\"part1\"))\n      put(\"part2\", tableSchema.get(\"part2\"))\n      put(\"part3\", tableSchema.get(\"part3\"))\n    }\n  }\n\n  private val partitionCols: java.util.Set[String] = partitionColsMetadata.keySet()\n\n  // Test cases for verifying partition of predicate into data and partition predicates\n  // Map entry format (predicate -> (partition predicate, data predicate)\n  val partitionTestCases = Map[Predicate, (String, String)](\n    // single predicate on a data column\n    predicate(\"=\", col(\"data1\"), ofInt(12)) ->\n      (\"ALWAYS_TRUE()\", \"(column(`data1`) = 12)\"),\n    // single predicate with default collation on a data column\n    predicate(\"=\", col(\"data2\"), ofString(\"12\"), CollationIdentifier.SPARK_UTF8_BINARY) ->\n      (\"ALWAYS_TRUE()\", \"(column(`data2`) = 12 COLLATE SPARK.UTF8_BINARY)\"),\n    // single predicate with non-default collation on a data column\n    predicate(\"=\", col(\"data2\"), ofString(\"12\"), utf8Lcase) ->\n      (\"ALWAYS_TRUE()\", \"(column(`data2`) = 12 COLLATE SPARK.UTF8_LCASE)\"),\n    predicate(\"=\", col(\"data2\"), ofString(\"12\"), unicode) ->\n      (\"ALWAYS_TRUE()\", \"(column(`data2`) = 12 COLLATE ICU.UNICODE)\"),\n    // multiple predicates on data columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data1\"), ofInt(12)),\n      predicate(\">=\", col(\"data2\"), ofString(\"sss\"))) ->\n      (\"ALWAYS_TRUE()\", \"((column(`data1`) = 12) AND (column(`data2`) >= sss))\"),\n    // multiple predicates with collation on data columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data2\"), ofString(\"12\")),\n      predicate(\">=\", col(\"data2\"), ofString(\"sss\"), utf8Lcase)) ->\n      (\n        \"ALWAYS_TRUE()\",\n        \"((column(`data2`) = 12) AND (column(`data2`) >= sss COLLATE SPARK.UTF8_LCASE))\"),\n    // multiple predicates with collation on data columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data2\"), ofString(\"12\"), utf8Lcase),\n      predicate(\">=\", col(\"data2\"), ofString(\"sss\"), unicode)) ->\n      (\n        \"ALWAYS_TRUE()\",\n        \"\"\"((column(`data2`) = 12 COLLATE SPARK.UTF8_LCASE) AND\n          |(column(`data2`) >= sss COLLATE ICU.UNICODE))\"\"\".stripMargin.replaceAll(\"\\n\", \" \")),\n    // multiple predicates on data columns joined with OR\n    predicate(\n      \"OR\",\n      predicate(\"<=\", col(\"data2\"), ofString(\"sss\")),\n      predicate(\"=\", col(\"data3\", \"data31\"), ofBoolean(true))) ->\n      (\"ALWAYS_TRUE()\", \"((column(`data2`) <= sss) OR (column(`data3`.`data31`) = true))\"),\n    predicate(\n      \"OR\",\n      predicate(\"<=\", col(\"data2\"), ofString(\"sss\"), utf8Lcase),\n      predicate(\"=\", col(\"data3\", \"data31\"), ofBoolean(true))) ->\n      (\n        \"ALWAYS_TRUE()\",\n        \"((column(`data2`) <= sss COLLATE SPARK.UTF8_LCASE) OR (column(`data3`.`data31`) = true))\"),\n    // single predicate on a partition column\n    predicate(\"=\", col(\"part1\"), ofInt(12)) ->\n      (\"(column(`part1`) = 12)\", \"ALWAYS_TRUE()\"),\n    // single predicate with default collation on partition column\n    predicate(\"=\", col(\"part3\"), ofString(\"12\"), CollationIdentifier.SPARK_UTF8_BINARY) ->\n      (\"(column(`part3`) = 12 COLLATE SPARK.UTF8_BINARY)\", \"ALWAYS_TRUE()\"),\n    // single predicate with non-default collation on partition column\n    predicate(\"=\", col(\"part3\"), ofString(\"12\"), utf8Lcase) ->\n      (\"(column(`part3`) = 12 COLLATE SPARK.UTF8_LCASE)\", \"ALWAYS_TRUE()\"),\n    predicate(\"=\", col(\"part3\"), ofString(\"12\"), unicode) ->\n      (\"(column(`part3`) = 12 COLLATE ICU.UNICODE)\", \"ALWAYS_TRUE()\"),\n    // multiple predicates on partition columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"part1\"), ofInt(12)),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"))) ->\n      (\"((column(`part1`) = 12) AND (column(`part3`) >= sss))\", \"ALWAYS_TRUE()\"),\n    // multiple predicates with collation on partition columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"part3\"), ofString(\"sss\"), utf8Lcase),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"), CollationIdentifier.SPARK_UTF8_BINARY)) ->\n      (\n        \"\"\"((column(`part3`) = sss COLLATE SPARK.UTF8_LCASE) AND (column(`part3`)\n          |>= sss COLLATE SPARK.UTF8_BINARY))\"\"\".stripMargin.replaceAll(\"\\n\", \" \"),\n        \"ALWAYS_TRUE()\"),\n    // multiple predicates on partition columns joined with OR\n    predicate(\n      \"OR\",\n      predicate(\"<=\", col(\"part3\"), ofString(\"sss\")),\n      predicate(\"=\", col(\"part1\"), ofInt(2781))) ->\n      (\"((column(`part3`) <= sss) OR (column(`part1`) = 2781))\", \"ALWAYS_TRUE()\"),\n\n    // predicates (each on data and partition column) joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data1\"), ofInt(12)),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"))) ->\n      (\"(column(`part3`) >= sss)\", \"(column(`data1`) = 12)\"),\n\n    // predicates with collation (each on data and partition column) joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data2\"), ofString(\"12\"), utf8Lcase),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"), unicode)) ->\n      (\n        \"(column(`part3`) >= sss COLLATE ICU.UNICODE)\",\n        \"(column(`data2`) = 12 COLLATE SPARK.UTF8_LCASE)\"),\n\n    // predicates (each on data and partition column) joined with OR\n    predicate(\n      \"OR\",\n      predicate(\"=\", col(\"data1\"), ofInt(12)),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"))) ->\n      (\"ALWAYS_TRUE()\", \"((column(`data1`) = 12) OR (column(`part3`) >= sss))\"),\n\n    // predicates with collation (each on data and partition column) joined with OR\n    predicate(\n      \"OR\",\n      predicate(\"=\", col(\"data2\"), ofString(\"12\"), unicode),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"), unicode)) ->\n      (\n        \"ALWAYS_TRUE()\",\n        \"\"\"((column(`data2`) = 12 COLLATE ICU.UNICODE) OR (column(`part3`)\n          |>= sss COLLATE ICU.UNICODE))\"\"\".stripMargin.replaceAll(\"\\n\", \" \")),\n\n    // predicates (multiple on data and partition columns) joined with AND\n    predicate(\n      \"AND\",\n      predicate(\n        \"AND\",\n        predicate(\"=\", col(\"data1\"), ofInt(12)),\n        predicate(\">=\", col(\"data2\"), ofString(\"sss\"))),\n      predicate(\n        \"AND\",\n        predicate(\"=\", col(\"part1\"), ofInt(12)),\n        predicate(\">=\", col(\"part3\"), ofString(\"sss\")))) ->\n      (\n        \"((column(`part1`) = 12) AND (column(`part3`) >= sss))\",\n        \"((column(`data1`) = 12) AND (column(`data2`) >= sss))\"),\n\n    // predicates (multiple on data and partition columns joined with OR) joined with AND\n    predicate(\n      \"AND\",\n      predicate(\n        \"OR\",\n        predicate(\"=\", col(\"data1\"), ofInt(12)),\n        predicate(\">=\", col(\"data2\"), ofString(\"sss\"))),\n      predicate(\n        \"OR\",\n        predicate(\"=\", col(\"part1\"), ofInt(12)),\n        predicate(\">=\", col(\"part3\"), ofString(\"sss\")))) ->\n      (\n        \"((column(`part1`) = 12) OR (column(`part3`) >= sss))\",\n        \"((column(`data1`) = 12) OR (column(`data2`) >= sss))\"),\n\n    // predicates (multiple on data and partition columns joined with OR) joined with OR\n    predicate(\n      \"OR\",\n      predicate(\n        \"OR\",\n        predicate(\"=\", col(\"data1\"), ofInt(12)),\n        predicate(\">=\", col(\"data2\"), ofString(\"sss\"))),\n      predicate(\n        \"OR\",\n        predicate(\"=\", col(\"part1\"), ofInt(12)),\n        predicate(\">=\", col(\"part3\"), ofString(\"sss\")))) ->\n      (\n        \"ALWAYS_TRUE()\",\n        \"(((column(`data1`) = 12) OR (column(`data2`) >= sss)) OR \" +\n          \"((column(`part1`) = 12) OR (column(`part3`) >= sss)))\"),\n\n    // predicates (data and partitions compared in the same expression)\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data1\"), col(\"part1\")),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"))) ->\n      (\n        \"(column(`part3`) >= sss)\",\n        \"(column(`data1`) = column(`part1`))\"),\n\n    // predicates with collation (data and partitions compared in the same expression)\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"data2\"), col(\"part3\"), utf8Lcase),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"), unicode)) ->\n      (\n        \"(column(`part3`) >= sss COLLATE ICU.UNICODE)\",\n        \"(column(`data2`) = column(`part3`) COLLATE SPARK.UTF8_LCASE)\"),\n\n    // predicate only on data column but reverse order of literal and column\n    predicate(\"=\", ofInt(12), col(\"data1\")) ->\n      (\"ALWAYS_TRUE()\", \"(12 = column(`data1`))\"),\n\n    // predicate with collation only on data column but reverse order of literal and column\n    predicate(\"=\", ofString(\"12\"), col(\"data2\"), utf8Lcase) ->\n      (\"ALWAYS_TRUE()\", \"(12 = column(`data2`) COLLATE SPARK.UTF8_LCASE)\"))\n\n  partitionTestCases.foreach {\n    case (predicate, (partitionPredicate, dataPredicate)) =>\n      test(s\"split predicate into data and partition predicates: $predicate\") {\n        val metadataAndDataPredicates = splitMetadataAndDataPredicates(predicate, partitionCols)\n        assert(metadataAndDataPredicates._1.toString === partitionPredicate)\n        assert(metadataAndDataPredicates._2.toString === dataPredicate)\n      }\n  }\n\n  // Map entry format: (given predicate -> \\\n  // (exp predicate for partition pruning, exp predicate for checkpoint reader pushdown))\n  val rewriteTestCases = Map(\n    // single predicate on a partition column\n    predicate(\"=\", col(\"part2\"), ofTimestamp(12)) ->\n      (\n        // exp predicate for partition pruning\n        \"(partition_value(ELEMENT_AT(column(`add`.`partitionValues`), part2), date) = 12)\",\n\n        // exp predicate for checkpoint reader pushdown\n        \"(column(`add`.`partitionValues_parsed`.`part2`) = 12)\"),\n    // single predicate with collation on a partition column\n    predicate(\"=\", col(\"part3\"), ofString(\"sss\"), utf8Lcase) ->\n      (\n        // exp predicate for partition pruning\n        \"(ELEMENT_AT(column(`add`.`partitionValues`), part3) = sss COLLATE SPARK.UTF8_LCASE)\",\n\n        // exp predicate for checkpoint reader pushdown\n        \"(column(`add`.`partitionValues_parsed`.`part3`) = sss COLLATE SPARK.UTF8_LCASE)\"),\n    // multiple predicates on partition columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"part1\"), ofInt(12)),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"))) ->\n      (\n        // exp predicate for partition pruning\n        \"\"\"((partition_value(ELEMENT_AT(column(`add`.`partitionValues`), part1), integer) = 12) AND\n          |(ELEMENT_AT(column(`add`.`partitionValues`), part3) >= sss))\"\"\"\n          .stripMargin.replaceAll(\"\\n\", \" \"),\n\n        // exp predicate for checkpoint reader pushdown\n        \"\"\"((column(`add`.`partitionValues_parsed`.`part1`) = 12) AND\n          |(column(`add`.`partitionValues_parsed`.`part3`) >= sss))\"\"\"\n          .stripMargin.replaceAll(\"\\n\", \" \")),\n    // multiple predicates with collation on partition columns joined with AND\n    predicate(\n      \"AND\",\n      predicate(\"=\", col(\"part3\"), ofString(\"sss\"), utf8Lcase),\n      predicate(\">=\", col(\"part3\"), ofString(\"sss\"), CollationIdentifier.SPARK_UTF8_BINARY)) ->\n      (\n        // exp predicate for partition pruning\n        \"\"\"((ELEMENT_AT(column(`add`.`partitionValues`), part3) = sss COLLATE SPARK.UTF8_LCASE) AND\n          |(ELEMENT_AT(column(`add`.`partitionValues`), part3) >= sss COLLATE SPARK.UTF8_BINARY))\"\"\"\n          .stripMargin.replaceAll(\"\\n\", \" \"),\n\n        // exp predicate for checkpoint reader pushdown\n        \"\"\"((column(`add`.`partitionValues_parsed`.`part3`) = sss COLLATE SPARK.UTF8_LCASE) AND\n          |(column(`add`.`partitionValues_parsed`.`part3`) >= sss COLLATE SPARK.UTF8_BINARY))\"\"\"\n          .stripMargin.replaceAll(\"\\n\", \" \")),\n    // multiple predicates on partition columns joined with OR\n    predicate(\n      \"OR\",\n      predicate(\"<=\", col(\"part3\"), ofString(\"sss\")),\n      predicate(\"=\", col(\"part1\"), ofInt(2781))) ->\n      (\n        // exp predicate for partition pruning\n        \"\"\"((ELEMENT_AT(column(`add`.`partitionValues`), part3) <= sss) OR\n          |(partition_value(ELEMENT_AT(column(`add`.`partitionValues`), part1), integer) = 2781))\"\"\"\n          .stripMargin.replaceAll(\"\\n\", \" \"),\n\n        // exp predicate for checkpoint reader pushdown\n        \"\"\"((column(`add`.`partitionValues_parsed`.`part3`) <= sss) OR\n          |(column(`add`.`partitionValues_parsed`.`part1`) = 2781))\"\"\"\n          .stripMargin.replaceAll(\"\\n\", \" \")))\n  rewriteTestCases.foreach {\n    case (predicate, (expPartitionPruningPredicate, expCheckpointReaderPushdownPredicate)) =>\n      test(s\"rewrite partition predicate on scan file schema: $predicate\") {\n        val actPartitionPruningPredicate =\n          rewritePartitionPredicateOnScanFileSchema(predicate, partitionColsMetadata)\n        assert(actPartitionPruningPredicate.toString === expPartitionPruningPredicate)\n\n        val actCheckpointReaderPushdownPredicate =\n          rewritePartitionPredicateOnCheckpointFileSchema(predicate, partitionColsMetadata)\n        assert(actCheckpointReaderPushdownPredicate.toString ===\n          expCheckpointReaderPushdownPredicate)\n      }\n  }\n\n  private val nullFileName = \"__HIVE_DEFAULT_PARTITION__\"\n  Seq(\n    ofBoolean(true) -> (\"true\", \"true\"),\n    ofBoolean(false) -> (\"false\", \"false\"),\n    ofNull(BooleanType.BOOLEAN) -> (null, nullFileName),\n    ofByte(24.toByte) -> (\"24\", \"24\"),\n    ofNull(ByteType.BYTE) -> (null, nullFileName),\n    ofShort(876.toShort) -> (\"876\", \"876\"),\n    ofNull(ShortType.SHORT) -> (null, nullFileName),\n    ofInt(2342342) -> (\"2342342\", \"2342342\"),\n    ofNull(IntegerType.INTEGER) -> (null, nullFileName),\n    ofLong(234234223L) -> (\"234234223\", \"234234223\"),\n    ofNull(LongType.LONG) -> (null, nullFileName),\n    ofFloat(23423.4223f) -> (\"23423.422\", \"23423.422\"),\n    ofNull(FloatType.FLOAT) -> (null, nullFileName),\n    ofDouble(23423.422233d) -> (\"23423.422233\", \"23423.422233\"),\n    ofNull(DoubleType.DOUBLE) -> (null, nullFileName),\n    ofString(\"string_val\") -> (\"string_val\", \"string_val\"),\n    ofString(\"string_\\nval\") -> (\"string_\\nval\", \"string_%0Aval\"),\n    ofString(\"str=ing_\\u0001val\") -> (\"str=ing_\\u0001val\", \"str%3Ding_%01val\"),\n    ofNull(StringType.STRING) -> (null, nullFileName),\n    ofDecimal(new java.math.BigDecimal(\"23423.234234\"), 15, 7) ->\n      (\"23423.2342340\", \"23423.2342340\"),\n    ofNull(new DecimalType(15, 7)) -> (null, nullFileName),\n    ofBinary(\"binary_val\".getBytes) -> (\"binary_val\", \"binary_val\"),\n    ofNull(BinaryType.BINARY) -> (null, nullFileName),\n    ofDate(4234) -> (\"1981-08-05\", \"1981-08-05\"),\n    ofNull(DateType.DATE) -> (null, nullFileName),\n    ofTimestamp(2342342342232L) ->\n      (\"1970-01-28 02:39:02.342232\", \"1970-01-28 02%3A39%3A02.342232\"),\n    ofNull(TimestampType.TIMESTAMP) -> (null, nullFileName),\n    ofTimestampNtz(-2342342342L) ->\n      (\"1969-12-31 23:20:58.657658\", \"1969-12-31 23%3A20%3A58.657658\"),\n    ofNull(TimestampNTZType.TIMESTAMP_NTZ) -> (null, nullFileName)).foreach {\n    case (literal, (expSerializedValue, expFileName)) =>\n      test(s\"serialize partition value literal as string: ${literal.getDataType}($literal)\") {\n        val result = serializePartitionValue(literal)\n        assert(result === expSerializedValue)\n      }\n\n      test(s\"construct partition data output directory: ${literal.getDataType}($literal)\") {\n        val result = getTargetDirectory(\n          \"/tmp/root\",\n          Seq(\"part1\").asJava,\n          Map(\"part1\" -> literal).asJava)\n        assert(result === s\"/tmp/root/part1=$expFileName\")\n      }\n  }\n\n  test(\"construct partition data output directory with multiple partition columns\") {\n    val result = getTargetDirectory(\n      \"/tmp/root\",\n      Seq(\"part1\", \"part2\", \"part3\").asJava,\n      Map(\n        \"part1\" -> ofInt(12),\n        \"part3\" -> ofTimestamp(234234234L),\n        \"part2\" -> ofString(\"sss\")).asJava)\n    assert(result === \"/tmp/root/part1=12/part2=sss/part3=1970-01-01 00%3A03%3A54.234234\")\n  }\n\n  // Test cases for verifying if timestamp can be parsed correctly.\n  test(\"parse valid standard timestamp\") {\n    val result1 = PartitionUtils.tryParseTimestamp(\"2024-01-01 10:00:00\")\n    assert(result1 == 1704103200000000L)\n    val result2 = PartitionUtils.tryParseTimestamp(\"2024-01-01 10:00:00.123456\")\n    assert(result2 == 1704103200123456L)\n  }\n\n  test(\"parse valid ISO8601 timestamp\") {\n    val result = PartitionUtils.tryParseTimestamp(\"2024-01-01T10:00:00Z\")\n    assert(result == 1704103200000000L)\n  }\n\n  test(\"parse valid ISO8601 timestamp with microsecond precision\") {\n    val result = PartitionUtils.tryParseTimestamp(\"2025-01-01T00:00:00.123456Z\")\n    assert(result == 1735689600123456L)\n  }\n\n  test(\"throw on invalid timestamp\") {\n    val thrown = intercept[IllegalStateException] {\n      PartitionUtils.tryParseTimestamp(\"not-a-timestamp\")\n    }\n    assert(thrown.getMessage.contains(\"Invalid timestamp format for value\"))\n  }\n\n  private def col(names: String*): Column = {\n    new Column(names.toArray)\n  }\n\n  private def predicate(name: String, children: Expression*): Predicate = {\n    new Predicate(name, children.asJava)\n  }\n\n  private def predicate(\n      name: String,\n      left: Expression,\n      right: Expression,\n      collationIdentifier: CollationIdentifier) = {\n    new Predicate(name, left, right, collationIdentifier)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/SchemaIterableSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass SchemaIterableSuite extends AnyFunSuite {\n  test(\"depth first traversal works with deeply nested types\") {\n    val schema: StructType = getDeeplyNestedSchema\n\n    val iterable = new SchemaIterable(schema)\n\n    def parentStructFieldAndPathToString(\n        parent: Optional[SchemaIterable.ParentStructFieldInfo]): String = {\n      if (parent.isPresent) {\n        val parentInfo = parent.get\n        val parentField = parentInfo.getParentField\n        val parentPath = parentInfo.getPathFromParent\n        s\"Some(${parentField.getName}, $parentPath)\"\n      } else {\n        \"None\"\n      }\n    }\n\n    // Track the path stack during traversal\n    val fieldInfo = iterable.asScala.map {\n      element =>\n        (\n          parentStructFieldAndPathToString(element.getParentStructFieldAndPath()),\n          element.getNamePath(),\n          element.getPathFromNearestStructFieldAncestor(\n            element.getNearestStructFieldAncestor.getName),\n          element.getPathFromNearestStructFieldAncestor(\"\"),\n          element.getField.getDataType.getClass.getSimpleName)\n    }\n      .toList\n\n    // The expected traversal order with field types, showing the complete depth-first traversal\n    val expectedOrder = List(\n      // First branch: nested_array\n      (\"Some(nested_array, element)\", \"nested_array.element.id\", \"id\", \"\", \"IntegerType\"),\n      (\n        \"Some(tags, )\",\n        \"nested_array.element.tags.element\",\n        \"tags.element\",\n        \"element\",\n        \"StringType\"),\n      (\"Some(nested_array, element)\", \"nested_array.element.tags\", \"tags\", \"\", \"ArrayType\"),\n      (\n        \"Some(nested_array, )\",\n        \"nested_array.element\",\n        \"nested_array.element\",\n        \"element\",\n        \"StructType\"),\n      (\"None\", \"nested_array\", \"nested_array\", \"\", \"ArrayType\"),\n\n      // Second branch: nested_map\n      (\n        \"Some(nested_map, key)\",\n        \"nested_map.key.element\",\n        \"nested_map.key.element\",\n        \"key.element\",\n        \"StringType\"),\n      (\"Some(nested_map, )\", \"nested_map.key\", \"nested_map.key\", \"key\", \"ArrayType\"),\n      (\n        \"Some(points, element)\",\n        \"nested_map.value.points.element.x\",\n        \"x\",\n        \"\",\n        \"DoubleType\"),\n      (\n        \"Some(points, element)\",\n        \"nested_map.value.points.element.y\",\n        \"y\",\n        \"\",\n        \"DoubleType\"),\n      (\n        \"Some(points, )\",\n        \"nested_map.value.points.element\",\n        \"points.element\",\n        \"element\",\n        \"StructType\"),\n      (\n        \"Some(nested_map, value)\",\n        \"nested_map.value.points\",\n        \"points\",\n        \"\",\n        \"ArrayType\"),\n      (\n        \"Some(metadata, )\",\n        \"nested_map.value.metadata.key\",\n        \"metadata.key\",\n        \"key\",\n        \"StringType\"),\n      (\n        \"Some(metadata, )\",\n        \"nested_map.value.metadata.value\",\n        \"metadata.value\",\n        \"value\",\n        \"IntegerType\"),\n      (\n        \"Some(nested_map, value)\",\n        \"nested_map.value.metadata\",\n        \"metadata\",\n        \"\",\n        \"MapType\"),\n      (\"Some(nested_map, )\", \"nested_map.value\", \"nested_map.value\", \"value\", \"StructType\"),\n      (\"None\", \"nested_map\", \"nested_map\", \"\", \"MapType\"),\n      // Third branch\n      (\n        \"Some(double_nested, element.element.key.key)\",\n        \"double_nested.element.element.key.key.element\",\n        \"double_nested.element.element.key.key.element\",\n        \"element.element.key.key.element\",\n        \"IntegerType\"),\n      (\n        \"Some(double_nested, element.element.key)\",\n        \"double_nested.element.element.key.key\",\n        \"double_nested.element.element.key.key\",\n        \"element.element.key.key\",\n        \"ArrayType\"),\n      (\n        \"Some(double_nested, element.element.key)\",\n        \"double_nested.element.element.key.value\",\n        \"double_nested.element.element.key.value\",\n        \"element.element.key.value\",\n        \"StringType\"),\n      (\n        \"Some(double_nested, element.element)\",\n        \"double_nested.element.element.key\",\n        \"double_nested.element.element.key\",\n        \"element.element.key\",\n        \"MapType\"),\n      (\n        \"Some(double_nested, element.element.value)\",\n        \"double_nested.element.element.value.key\",\n        \"double_nested.element.element.value.key\",\n        \"element.element.value.key\",\n        \"StringType\"),\n      (\n        \"Some(double_nested, element.element.value)\",\n        \"double_nested.element.element.value.value\",\n        \"double_nested.element.element.value.value\",\n        \"element.element.value.value\",\n        \"StringType\"),\n      (\n        \"Some(double_nested, element.element)\",\n        \"double_nested.element.element.value\",\n        \"double_nested.element.element.value\",\n        \"element.element.value\",\n        \"MapType\"),\n      (\n        \"Some(double_nested, element)\",\n        \"double_nested.element.element\",\n        \"double_nested.element.element\",\n        \"element.element\",\n        \"MapType\"),\n      (\n        \"Some(double_nested, )\",\n        \"double_nested.element\",\n        \"double_nested.element\",\n        \"element\",\n        \"ArrayType\"),\n      (\"None\", \"double_nested\", \"double_nested\", \"\", \"ArrayType\"),\n      // fourth branch\n      (\"None\", \"empty_struct\", \"empty_struct\", \"\", \"StructType\"),\n      // fifth branch\n      (\n        \"Some(empty_struct_array, )\",\n        \"empty_struct_array.element\",\n        \"empty_struct_array.element\",\n        \"element\",\n        \"StructType\"),\n      (\"None\", \"empty_struct_array\", \"empty_struct_array\", \"\", \"ArrayType\"),\n      // sixth branch\n      (\n        \"Some(empty_map_struct, )\",\n        \"empty_map_struct.key\",\n        \"empty_map_struct.key\",\n        \"key\",\n        \"StructType\"),\n      (\n        \"Some(empty_map_struct, )\",\n        \"empty_map_struct.value\",\n        \"empty_map_struct.value\",\n        \"value\",\n        \"StructType\"),\n      (\"None\", \"empty_map_struct\", \"empty_map_struct\", \"\", \"MapType\"))\n\n    fieldInfo.zip(expectedOrder).foreach {\n      case (actual, expected) => assert(actual == expected)\n    }\n    assert(fieldInfo == expectedOrder)\n\n  }\n\n  Seq(\n    (new StructType(), List()),\n    (new StructType().add(\"empty\", new StructType()), List(\"empty\")),\n    (\n      new StructType().add(\"f1\", new StructType().add(\"f2\", IntegerType.INTEGER)),\n      List(\"f1.f2\", \"f1\")),\n    (\n      new StructType().add(\"f1\", IntegerType.INTEGER).add(\"f2\", IntegerType.INTEGER),\n      List(\"f1\", \"f2\")),\n    (\n      new StructType()\n        .add(\"f1\", IntegerType.INTEGER)\n        .add(\n          \"s1\",\n          new StructType()\n            .add(\"f1\", IntegerType.INTEGER)\n            .add(\"f2\", IntegerType.INTEGER))\n        .add(\"s2\", new StructType())\n        .add(\"f2\", IntegerType.INTEGER),\n      List(\"f1\", \"s1.f1\", \"s1.f2\", \"s1\", \"s2\", \"f2\"))).foreach {\n    case (schema, expected) =>\n      test(s\"check basic iteration ${schema.toString}\") {\n        val iterable = new SchemaIterable(schema)\n        val fieldInfo = iterable.asScala.map {\n          field => (field.getNamePath)\n        }\n          .toList\n        assert(fieldInfo === expected)\n      }\n  }\n\n  test(\"test update schema\") {\n\n    val schema: StructType = getDeeplyNestedSchema\n\n    val iterable = new SchemaIterable(schema)\n\n    val fieldMetadata = FieldMetadata.builder()\n      .putString(\"k1\", \"v1\")\n      .build()\n    val newTypes = Map(\n      \"nested_array.element.tags.element\" -> IntegerType.INTEGER,\n      \"nested_map.value.metadata.value\" -> StringType.STRING,\n      \"nested_map.value.points.element\" -> new StructType().add(\n        \"x\",\n        DoubleType.DOUBLE,\n        false).add(\"y\", DoubleType.DOUBLE, false)\n        .add(\"z\", LongType.LONG, false))\n    val newMetadata = Map(\"nested_array\" -> fieldMetadata)\n\n    iterable.newMutableIterator().asScala.foreach {\n      element =>\n        newTypes.get(element.getNamePath).foreach {\n          t => element.updateField(element.getField.withDataType(t))\n        }\n        newMetadata.get(element.getNamePath).foreach {\n          fm => element.updateField(element.getField.withNewMetadata(fm))\n        }\n    }\n\n    val fieldInfo = iterable.asScala.map {\n      element => (element.getNamePath, element.getField.getDataType.getClass.getSimpleName)\n    }.toList\n\n    // The expected traversal order with field types, showing the complete depth-first traversal\n    val expectedOrder = List(\n      // First branch: nested_array\n      (\"nested_array.element.id\", \"IntegerType\"),\n      (\"nested_array.element.tags.element\", \"IntegerType\"),\n      (\"nested_array.element.tags\", \"ArrayType\"),\n      (\"nested_array.element\", \"StructType\"),\n      (\"nested_array\", \"ArrayType\"),\n\n      // Second branch: nested_map\n      (\"nested_map.key.element\", \"StringType\"),\n      (\"nested_map.key\", \"ArrayType\"),\n      (\"nested_map.value.points.element.x\", \"DoubleType\"),\n      (\"nested_map.value.points.element.y\", \"DoubleType\"),\n      (\"nested_map.value.points.element.z\", \"LongType\"),\n      (\"nested_map.value.points.element\", \"StructType\"),\n      (\"nested_map.value.points\", \"ArrayType\"),\n      (\"nested_map.value.metadata.key\", \"StringType\"),\n      (\"nested_map.value.metadata.value\", \"StringType\"),\n      (\"nested_map.value.metadata\", \"MapType\"),\n      (\"nested_map.value\", \"StructType\"),\n      (\"nested_map\", \"MapType\"),\n      // Third branch\n      (\"double_nested.element.element.key.key.element\", \"IntegerType\"),\n      (\"double_nested.element.element.key.key\", \"ArrayType\"),\n      (\"double_nested.element.element.key.value\", \"StringType\"),\n      (\"double_nested.element.element.key\", \"MapType\"),\n      (\"double_nested.element.element.value.key\", \"StringType\"),\n      (\"double_nested.element.element.value.value\", \"StringType\"),\n      (\"double_nested.element.element.value\", \"MapType\"),\n      (\"double_nested.element.element\", \"MapType\"),\n      (\"double_nested.element\", \"ArrayType\"),\n      (\"double_nested\", \"ArrayType\"),\n      // fourth branch\n      (\"empty_struct\", \"StructType\"),\n      // fifth branch\n      (\"empty_struct_array.element\", \"StructType\"),\n      (\"empty_struct_array\", \"ArrayType\"),\n      // sixth branch\n      (\"empty_map_struct.key\", \"StructType\"),\n      (\"empty_map_struct.value\", \"StructType\"),\n      (\"empty_map_struct\", \"MapType\"))\n\n    fieldInfo.zip(expectedOrder).foreach {\n      case (actual, expected) => assert(actual == expected)\n    }\n    assert(iterable.getSchema.get(\"nested_array\").getMetadata == fieldMetadata)\n\n  }\n\n  test(\"test set nearest ancestor field metadata\") {\n\n    val schema: StructType = getDeeplyNestedSchema\n\n    val iterable = new SchemaIterable(schema)\n\n    val newMetadata = Map(\n      \"nested_array.element.tags.element\" -> newFieldMetadata(\"v1\"),\n      \"nested_map.value.metadata\" -> newFieldMetadata(\"v2\"),\n      \"nested_map.value.metadata.value\" -> newFieldMetadata(\"v3\"),\n      \"nested_array\" -> newFieldMetadata(\"v4\"))\n\n    val expected = Map(\n      \"nested_array.element.tags\" -> newFieldMetadata(\"v1\"),\n      \"nested_array.element.tags.element\" -> FieldMetadata.empty(),\n      \"nested_map.value.metadata\" ->\n        FieldMetadata.builder\n          .fromMetadata(newFieldMetadata(\"v2\"))\n          .fromMetadata(newFieldMetadata(\"v3\")).build(),\n      \"nested_map.value.metadata.value\" -> FieldMetadata.empty(),\n      \"nested_array\" -> newFieldMetadata(\"v4\"))\n\n    val originalCount = iterable.asScala.count(_ => true)\n\n    iterable.newMutableIterator().asScala.foreach {\n      element =>\n        newMetadata.get(element.getNamePath).foreach { fm =>\n          val ancestorField = element.getNearestStructFieldAncestor\n          val metadataBuilder = FieldMetadata.builder()\n            .fromMetadata(ancestorField.getMetadata).fromMetadata(fm)\n          element.setMetadataOnNearestStructFieldAncestor(metadataBuilder.build())\n        }\n    }\n\n    iterable.asScala.foreach {\n      element =>\n        expected.get(element.getNamePath).foreach {\n          fm =>\n            assert(\n              fm ==\n                element.getField.getMetadata,\n              s\"Path: ${element.getNamePath}  ${iterable.getSchema} \")\n        }\n    }\n    val newCount = iterable.asScala.count(_ => true)\n    assert(newCount > 0)\n    assert(originalCount == newCount)\n\n  }\n\n  val testCases = Seq(\n    // Test case 1: Skip ArrayType\n    (\n      Seq(classOf[ArrayType]),\n      List(\n        \"nested_array\",\n        \"nested_map.key\",\n        \"nested_map.value.points\",\n        \"nested_map.value.metadata.key\",\n        \"nested_map.value.metadata.value\",\n        \"nested_map.value.metadata\",\n        \"nested_map.value\",\n        \"nested_map\",\n        \"double_nested\",\n        \"empty_struct\",\n        \"empty_struct_array\",\n        \"empty_map_struct.key\",\n        \"empty_map_struct.value\",\n        \"empty_map_struct\")),\n\n    // Test case 2: Skip MapType\n    (\n      Seq(classOf[MapType]),\n      List(\n        \"nested_array.element.id\",\n        \"nested_array.element.tags.element\",\n        \"nested_array.element.tags\",\n        \"nested_array.element\",\n        \"nested_array\",\n        \"nested_map\",\n        \"double_nested.element.element\",\n        \"double_nested.element\",\n        \"double_nested\",\n        \"empty_struct\",\n        \"empty_struct_array.element\",\n        \"empty_struct_array\",\n        \"empty_map_struct\")),\n\n    // Test case 3: Skip StructType\n    (\n      Seq(classOf[StructType]),\n      List(\n        \"nested_array.element\",\n        \"nested_array\",\n        \"nested_map.key.element\",\n        \"nested_map.key\",\n        \"nested_map.value\",\n        \"nested_map\",\n        \"double_nested.element.element.key.key.element\",\n        \"double_nested.element.element.key.key\",\n        \"double_nested.element.element.key.value\",\n        \"double_nested.element.element.key\",\n        \"double_nested.element.element.value.key\",\n        \"double_nested.element.element.value.value\",\n        \"double_nested.element.element.value\",\n        \"double_nested.element.element\",\n        \"double_nested.element\",\n        \"double_nested\",\n        \"empty_struct\",\n        \"empty_struct_array.element\",\n        \"empty_struct_array\",\n        \"empty_map_struct.key\",\n        \"empty_map_struct.value\",\n        \"empty_map_struct\")),\n    // Test case 4: Skip multiple types (ArrayType and MapType)\n    (\n      Seq(classOf[ArrayType], classOf[MapType]),\n      List(\n        \"nested_array\",\n        \"nested_map\",\n        \"double_nested\",\n        \"empty_struct\",\n        \"empty_struct_array\",\n        \"empty_map_struct\"))).foreach { case (typesToSkip, expectedFields) =>\n    test(s\"skip recursion for specified types $typesToSkip\") {\n      val schema: StructType = getDeeplyNestedSchema\n      // Define test cases as a sequence of (types to skip, expected output) pairs\n\n      val iterable =\n        SchemaIterable.newSchemaIterableWithIgnoredRecursion(schema, typesToSkip.toArray)\n      val visitedPaths = iterable.asScala.map(_.getNamePath).toList\n\n      // Assert the results match expected output\n      assert(\n        visitedPaths === expectedFields,\n        s\"Failed for types: ${typesToSkip.map(_.getSimpleName).mkString(\", \")}\")\n    }\n  }\n\n  test(\"update schema with type recursion skipping\") {\n    val schema: StructType = getDeeplyNestedSchema\n\n    val iterable =\n      SchemaIterable.newSchemaIterableWithIgnoredRecursion(schema, Array(classOf[ArrayType]))\n\n    // Create a modified schema by skipping recursion into ArrayType\n    // and modifying only the top-level fields\n    iterable.newMutableIterator.asScala.foreach { element =>\n      if (element.getNamePath.contains(\"nested_array\")) {\n        // Add metadata to the array field but don't recurse into it\n        val fieldMetadata = FieldMetadata.builder()\n          .putString(\"array_skipped\", \"true\")\n          .build()\n        element.updateField(element.getField.withNewMetadata(fieldMetadata))\n      }\n    }\n\n    // Verify the metadata was added to the array field\n    assert(iterable.getSchema.get(\"nested_array\").getMetadata\n      .getString(\"array_skipped\") == \"true\")\n\n    // Verify that the array elements were not modified (recursion was skipped)\n    val newIterable = new SchemaIterable(iterable.getSchema)\n    var visited_count = 0\n    newIterable.asScala.foreach(element => {\n      if (element.getNamePath.startsWith(\"nested_array.\")) {\n        visited_count += 1\n        assert(element.getField.getMetadata == FieldMetadata.empty())\n      }\n    })\n    assert(visited_count > 0)\n\n  }\n\n  private def newFieldMetadata(v: String) = FieldMetadata.builder().putString(v, v).build()\n\n  private def getDeeplyNestedSchema = {\n    val intType = IntegerType.INTEGER\n    val stringType = StringType.STRING\n    val doubleType = DoubleType.DOUBLE\n\n    // Create a deeply nested schema:\n    // struct<\n    //   nested_array: array<\n    //     struct<\n    //       id: int,\n    //       tags: array<string>\n    //     >\n    //   >,\n    //   nested_map: map<\n    //     array<string>,\n    //     struct<\n    //       points: array<\n    //         struct<x: double, y: double>\n    //       >,\n    //       metadata: map<string, int>\n    //     >\n    //   >\n    //   double_nested:\n    //     array<array<map<map<array<int>, string>, map<string, string>>>\n    //   empty_struct: struct<>\n    //   empty_struct_array:\n    //     array<struct<>>>\n    //   empty_map_struct:\n    //    map<struct<>, struct<>>\n    // >\n\n    // Define the point struct inside the array\n    val pointStruct = new StructType().add(\"x\", doubleType).add(\"y\", doubleType);\n\n    // Define the inner struct containing tags array\n    val innerStruct = new StructType().add(\"id\", intType).add(\n      \"tags\",\n      new ArrayType(stringType, true));\n\n    // Define the value struct for the nested map\n    val valueStruct = new StructType().add(\"points\", new ArrayType(pointStruct, false)).add(\n      \"metadata\",\n      new MapType(\n        stringType,\n        intType,\n        /* valuesContainsNull = */ false));\n\n    // Create the root schema\n    val schema = new StructType().add(\"nested_array\", new ArrayType(innerStruct, false))\n      .add(\"nested_map\", new MapType(new ArrayType(stringType, false), valueStruct, true))\n      .add(\n        \"double_nested\",\n        new ArrayType(\n          new ArrayType(\n            new MapType(\n              new MapType(new ArrayType(IntegerType.INTEGER, true), StringType.STRING, true),\n              new MapType(StringType.STRING, StringType.STRING, true),\n              true),\n            false),\n          false),\n        false)\n      .add(\"empty_struct\", new StructType(), false)\n      .add(\"empty_struct_array\", new ArrayType(new StructType(), true), false)\n      .add(\"empty_map_struct\", new MapType(new StructType(), new StructType(), true), false)\n    schema\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/SchemaUtilsSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.internal.util\n\nimport java.util\nimport java.util.{Locale, Optional}\nimport java.util.Collections.emptySet\n\nimport scala.collection.JavaConverters._\nimport scala.collection.JavaConverters.mapAsJavaMapConverter\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.{TableFeature, TableFeatures}\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe\nimport io.delta.kernel.internal.util.ColumnMapping.{COLUMN_MAPPING_ID_KEY, COLUMN_MAPPING_MODE_KEY, COLUMN_MAPPING_PHYSICAL_NAME_KEY}\nimport io.delta.kernel.internal.util.SchemaUtils.{computeSchemaChangesById, validateUpdatedSchemaAndGetUpdatedSchema}\nimport io.delta.kernel.internal.util.VectorUtils.stringStringMapValue\nimport io.delta.kernel.types.{ArrayType, ByteType, DataType, DoubleType, FieldMetadata, IntegerType, LongType, MapType, StringType, StructField, StructType, TypeChange, VariantType}\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.LongType.LONG\nimport io.delta.kernel.types.TimestampType.TIMESTAMP\n\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.prop.TableDrivenPropertyChecks.forAll\nimport org.scalatest.prop.TableFor2\nimport org.scalatest.prop.Tables.Table\n\nclass SchemaUtilsSuite extends AnyFunSuite {\n\n  val dummyProtocol = new Protocol(0, 0)\n\n  private def expectFailure(shouldContain: String*)(f: => Unit): Unit = {\n    val e = intercept[KernelException] {\n      f\n    }\n    val msg = e.getMessage.toLowerCase(Locale.ROOT)\n    assert(\n      shouldContain.map(_.toLowerCase(Locale.ROOT)).forall(msg.contains),\n      s\"Error message '$msg' didn't contain: $shouldContain\")\n  }\n\n  private def validateSchema(\n      schema: StructType,\n      isColumnMappingEnabled: Boolean = false,\n      isColumnDefaultEnabled: Boolean = false,\n      isIcebergCompatV3Enabled: Boolean = false): Unit =\n    SchemaUtils.validateSchema(\n      schema,\n      isColumnMappingEnabled,\n      isColumnDefaultEnabled,\n      isIcebergCompatV3Enabled)\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Duplicate Column Checks\n  ///////////////////////////////////////////////////////////////////////////\n\n  test(\"duplicate column name in top level\") {\n    val schema = new StructType()\n      .add(\"dupColName\", INTEGER)\n      .add(\"b\", INTEGER)\n      .add(\"dupColName\", StringType.STRING)\n    expectFailure(\"dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name in top level - case sensitivity\") {\n    val schema = new StructType()\n      .add(\"dupColName\", INTEGER)\n      .add(\"b\", INTEGER)\n      .add(\"dupCOLNAME\", StringType.STRING)\n    expectFailure(\"dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name for nested column + non-nested column\") {\n    val schema = new StructType()\n      .add(\n        \"dupColName\",\n        new StructType()\n          .add(\"a\", INTEGER)\n          .add(\"b\", INTEGER))\n      .add(\"dupColName\", INTEGER)\n    expectFailure(\"dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name for nested column + non-nested column - case sensitivity\") {\n    val schema = new StructType()\n      .add(\n        \"dupColName\",\n        new StructType()\n          .add(\"a\", INTEGER)\n          .add(\"b\", INTEGER))\n      .add(\"dupCOLNAME\", INTEGER)\n    expectFailure(\"dupCOLNAME\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name in nested level\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new StructType()\n          .add(\"dupColName\", INTEGER)\n          .add(\"b\", INTEGER)\n          .add(\"dupColName\", StringType.STRING))\n    expectFailure(\"top.dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name in nested level - case sensitivity\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new StructType()\n          .add(\"dupColName\", INTEGER)\n          .add(\"b\", INTEGER)\n          .add(\"dupCOLNAME\", StringType.STRING))\n    expectFailure(\"top.dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name in double nested level\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new StructType()\n          .add(\n            \"b\",\n            new StructType()\n              .add(\"dupColName\", StringType.STRING)\n              .add(\"c\", INTEGER)\n              .add(\"dupColName\", StringType.STRING))\n          .add(\"d\", INTEGER))\n    expectFailure(\"top.b.dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name in double nested array\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new StructType()\n          .add(\n            \"b\",\n            new ArrayType(\n              new ArrayType(\n                new StructType()\n                  .add(\"dupColName\", StringType.STRING)\n                  .add(\"c\", INTEGER)\n                  .add(\"dupColName\", StringType.STRING),\n                true),\n              true))\n          .add(\"d\", INTEGER))\n    expectFailure(\"top.b.element.element.dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"only duplicate columns are listed in the error message\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType().add(\"a\", INTEGER).add(\"b\", INTEGER).add(\"c\", INTEGER)).add(\n        \"top\",\n        new StructType().add(\"b\", INTEGER).add(\"c\", INTEGER).add(\"d\", INTEGER)).add(\n        \"bottom\",\n        new StructType().add(\"b\", INTEGER).add(\"c\", INTEGER).add(\"d\", INTEGER))\n\n    val e = intercept[KernelException] {\n      validateSchema(schema)\n    }\n    assert(e.getMessage.contains(\"Schema contains duplicate columns: top, top.b, top.c\"))\n  }\n\n  test(\"duplicate column name in double nested map\") {\n    val keyType = new StructType()\n      .add(\"dupColName\", INTEGER)\n      .add(\"d\", StringType.STRING)\n    expectFailure(\"top.b.key.dupColName\") {\n      val schema = new StructType()\n        .add(\n          \"top\",\n          new StructType()\n            .add(\"b\", new MapType(keyType.add(\"dupColName\", StringType.STRING), keyType, true)))\n      validateSchema(schema)\n    }\n    expectFailure(\"top.b.value.dupColName\") {\n      val schema = new StructType()\n        .add(\n          \"top\",\n          new StructType()\n            .add(\"b\", new MapType(keyType, keyType.add(\"dupColName\", StringType.STRING), true)))\n      validateSchema(schema)\n    }\n    // This is okay\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new StructType()\n          .add(\"b\", new MapType(keyType, keyType, true)))\n    validateSchema(schema)\n  }\n\n  test(\"duplicate column name in nested array\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new ArrayType(\n          new StructType()\n            .add(\"dupColName\", INTEGER)\n            .add(\"b\", INTEGER)\n            .add(\"dupColName\", StringType.STRING),\n          true))\n    expectFailure(\"top.element.dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column name in nested array - case sensitivity\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new ArrayType(\n          new StructType()\n            .add(\"dupColName\", INTEGER)\n            .add(\"b\", INTEGER)\n            .add(\"dupCOLNAME\", StringType.STRING),\n          true))\n    expectFailure(\"top.element.dupColName\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"non duplicate column because of back tick\") {\n    val schema = new StructType()\n      .add(\n        \"top\",\n        new StructType()\n          .add(\"a\", INTEGER)\n          .add(\"b\", INTEGER))\n      .add(\"top.a\", INTEGER)\n    validateSchema(schema)\n  }\n\n  test(\"non duplicate column because of back tick - nested\") {\n    val schema = new StructType()\n      .add(\n        \"first\",\n        new StructType()\n          .add(\n            \"top\",\n            new StructType()\n              .add(\"a\", INTEGER)\n              .add(\"b\", INTEGER))\n          .add(\"top.a\", INTEGER))\n    validateSchema(schema)\n  }\n\n  test(\"variant\") {\n    val schema = new StructType()\n      .add(\n        \"variant\",\n        VariantType.VARIANT)\n\n    validateSchema(schema)\n  }\n\n  test(\"variant - nested\") {\n    val schema = new StructType()\n      .add(\n        \"first\",\n        new StructType()\n          .add(\"variant\", VariantType.VARIANT))\n\n    validateSchema(schema)\n  }\n\n  test(\"duplicate column with back ticks - nested\") {\n    val schema = new StructType()\n      .add(\n        \"first\",\n        new StructType()\n          .add(\"top.a\", StringType.STRING)\n          .add(\"b\", INTEGER)\n          .add(\"top.a\", INTEGER))\n    expectFailure(\"first.`top.a`\") {\n      validateSchema(schema)\n    }\n  }\n\n  test(\"duplicate column with back ticks - nested and case sensitivity\") {\n    val schema = new StructType()\n      .add(\n        \"first\",\n        new StructType()\n          .add(\"TOP.a\", StringType.STRING)\n          .add(\"b\", INTEGER)\n          .add(\"top.a\", INTEGER))\n    expectFailure(\"first.`top.a`\") {\n      validateSchema(schema)\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // check default columns check is invoked\n  ///////////////////////////////////////////////////////////////////////////\n  test(\"check default columns checks are invoked\") {\n    val schema = new StructType()\n      .add(\n        \"first\",\n        new StructType()\n          .add(\"TOP.a\", StringType.STRING)\n          .add(\"b\", INTEGER)\n          .add(\"top.a\", INTEGER))\n    expectFailure(\"first.`top.a`\") {\n      validateSchema(\n        schema,\n        isColumnMappingEnabled = false,\n        isColumnDefaultEnabled = true,\n        isIcebergCompatV3Enabled = false)\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // checkFieldNames\n  ///////////////////////////////////////////////////////////////////////////\n  test(\"check non alphanumeric column characters\") {\n    val badCharacters = \" ,;{}()\\n\\t=\"\n    val goodCharacters = \"#.`!@$%^&*~_<>?/:\"\n\n    badCharacters.foreach { char =>\n      Seq(s\"a${char}b\", s\"${char}ab\", s\"ab${char}\", char.toString).foreach { name =>\n        val schema = new StructType().add(name, INTEGER)\n        val e = intercept[KernelException] {\n          validateSchema(schema)\n        }\n\n        if (char != '\\n') {\n          // with column mapping disabled this should be a valid name\n          validateSchema(\n            schema,\n            isColumnMappingEnabled = true,\n            isColumnDefaultEnabled = false,\n            isIcebergCompatV3Enabled = false)\n        }\n\n        assert(e.getMessage.contains(\"contains one of the unsupported\"))\n      }\n    }\n\n    goodCharacters.foreach { char =>\n      // no issues here\n      Seq(s\"a${char}b\", s\"${char}ab\", s\"ab${char}\", char.toString).foreach { name =>\n        val schema = new StructType().add(name, INTEGER);\n        validateSchema(schema)\n        validateSchema(\n          schema,\n          /* isColumnMappingEnabled= */ true,\n          /* isColumnDefaultEnabled= */ false,\n          /* isIcebergCompatV3Enabled= */ false)\n      }\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // computeSchemaChangesById\n  ///////////////////////////////////////////////////////////////////////////\n  test(\"Compute schema changes with added columns\") {\n    val fieldMappingBefore = newSchema((1, new StructField(\"id\", IntegerType.INTEGER, true)))\n\n    val fieldMappingAfter = newSchema(\n      (1, new StructField(\"id\", IntegerType.INTEGER, true)),\n      (2, new StructField(\"data\", StringType.STRING, true)))\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.removedFields().isEmpty)\n    assert(schemaChanges.updatedFields().isEmpty)\n    assert(schemaChanges.addedFields().size() == 1)\n    assert(schemaChanges.addedFields().get(0) == fieldMappingAfter.get(\"data\"))\n  }\n\n  test(\"Compute schema changes with renamed fields\") {\n    val fieldMappingBefore = newSchema(\n      (1, new StructField(\"id\", IntegerType.INTEGER, true)))\n\n    val fieldMappingAfter = newSchema(\n      (1, new StructField(\"renamed_id\", IntegerType.INTEGER, true)))\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.addedFields().isEmpty)\n    assert(schemaChanges.removedFields().isEmpty)\n    assert(schemaChanges.updatedFields().size() == 1)\n    val schemaUpdate = schemaChanges.updatedFields().get(0)\n    assert(schemaUpdate.before == fieldMappingBefore.get(\"id\"))\n    assert(schemaUpdate.after == fieldMappingAfter.get(\"renamed_id\"))\n  }\n\n  test(\"Compute schema changes with type changed columns\") {\n    val fieldMappingBefore = newSchema((1, new StructField(\"id\", IntegerType.INTEGER, true)))\n\n    val fieldMappingAfter =\n      newSchema((1, new StructField(\"promoted_to_long\", LongType.LONG, true)));\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.addedFields().isEmpty)\n    assert(schemaChanges.removedFields().isEmpty)\n    assert(schemaChanges.updatedFields().size() == 1)\n    val schemaUpdate = schemaChanges.updatedFields().get(0)\n    assert(schemaUpdate.before == fieldMappingBefore.get(\"id\"))\n    assert(schemaUpdate.after == fieldMappingAfter.get(\n      \"promoted_to_long\"))\n  }\n\n  test(\"Compute schema changes with dropped fields\") {\n    val fieldMappingBefore = newSchema(\n      (1, new StructField(\"id\", IntegerType.INTEGER, true)),\n      (2, new StructField(\"data\", StringType.STRING, true)))\n\n    val fieldMappingAfter = newSchema((\n      2 -> new StructField(\"data\", StringType.STRING, true)))\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.addedFields().isEmpty)\n    assert(schemaChanges.updatedFields().isEmpty)\n    assert(schemaChanges.removedFields().size() == 1)\n    assert(schemaChanges.removedFields().get(0) == fieldMappingBefore.get(\"id\"))\n  }\n\n  test(\"Compute schema changes with nullability change\") {\n    val fieldMappingBefore = newSchema(\n      (1, new StructField(\"id\", IntegerType.INTEGER, true)),\n      (2, new StructField(\"data\", StringType.STRING, true)))\n\n    val fieldMappingAfter = newSchema(\n      (1, new StructField(\"id\", IntegerType.INTEGER, true)),\n      (2, new StructField(\"required_data\", StringType.STRING, false)))\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.addedFields().isEmpty)\n    assert(schemaChanges.removedFields().isEmpty)\n    assert(schemaChanges.updatedFields().size() == 1)\n    val schemaUpdate = schemaChanges.updatedFields().get(0)\n    assert(schemaUpdate.before == fieldMappingBefore.get(\"data\"))\n    assert(\n      schemaUpdate.after == fieldMappingAfter.get(\"required_data\"))\n  }\n\n  test(\"Compute schema changes with moved fields\") {\n    val fieldMappingBefore = newSchema(\n      (\n        1,\n        new StructField(\n          \"struct\",\n          newSchema(\n            (4, new StructField(\"id\", IntegerType.INTEGER, true)),\n            (5, new StructField(\"data\", StringType.STRING, true))),\n          true)),\n      (2, new StructField(\"id\", IntegerType.INTEGER, true)),\n      (3, new StructField(\"data\", StringType.STRING, true)))\n\n    val fieldMappingAfter = newSchema(\n      (\n        1,\n        new StructField(\n          \"struct\",\n          newSchema(\n            (5, new StructField(\"data\", StringType.STRING, true)),\n            (4, new StructField(\"id\", IntegerType.INTEGER, true))),\n          true)),\n      (2, new StructField(\"id\", IntegerType.INTEGER, true)),\n      (3, new StructField(\"data\", StringType.STRING, true)))\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.addedFields().isEmpty)\n    assert(schemaChanges.removedFields().isEmpty)\n    assert(\n      schemaChanges.updatedFields().size() == 1,\n      s\"${schemaChanges.updatedFields.get(0).before}\")\n    val schemaUpdate = schemaChanges.updatedFields().get(0)\n    assert(schemaUpdate.before == fieldMappingBefore.get(\"struct\"))\n    assert(schemaUpdate.after == fieldMappingAfter.get(\"struct\"))\n  }\n\n  test(\"Compute schema changes with field metadata changes\") {\n    val fieldMappingBefore = newSchema(\n      (\n        1,\n        new StructField(\n          \"id\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder().putString(\n            \"metadata_col\",\n            \"metadata_val\").build())))\n\n    val fieldMappingAfter = newSchema(\n      (\n        1,\n        new StructField(\n          \"id\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder().putString(\n            \"metadata_col\",\n            \"updated_metadata_val\").build())))\n\n    val schemaChanges = computeSchemaChangesById(fieldMappingBefore, fieldMappingAfter)\n\n    assert(schemaChanges.addedFields().isEmpty)\n    assert(schemaChanges.removedFields().isEmpty)\n    assert(schemaChanges.updatedFields().size() == 1)\n    val schemaUpdate = schemaChanges.updatedFields().get(0)\n    assert(schemaUpdate.before == fieldMappingBefore.get(\"id\"))\n    assert(schemaUpdate.after == fieldMappingAfter.get(\"id\"))\n  }\n\n  test(\"Compute schema changes fails for invalid field moves - basic\") {\n    val map_field_type = new MapType(\n      new StructType()\n        .add(\"c2\", StringType.STRING, fieldMetadata(1))\n        .add(\n          \"c3\",\n          new StructType()\n            .add(\"c4\", IntegerType.INTEGER, fieldMetadata(3)),\n          fieldMetadata(2)),\n      new StructType()\n        .add(\"c5\", IntegerType.INTEGER, fieldMetadata(4)),\n      true /* valueContainsNull */\n    )\n    val array_field_type = new ArrayType(\n      new ArrayType(\n        new StructType()\n          .add(\"c6\", IntegerType.INTEGER, fieldMetadata(6))\n          .add(\"c7\", IntegerType.INTEGER, fieldMetadata(7)),\n        true /* containsNull */\n      ),\n      true /* containsNull */\n    )\n    val beforeSchema = new StructType()\n      .add(\"nested_map\", map_field_type, fieldMetadata(0))\n      .add(\"nested_array\", array_field_type, fieldMetadata(5))\n      .add(\"c8\", StringType.STRING, fieldMetadata(8))\n\n    Seq(\n      // New struct column with existing field under it\n      new StructType()\n        .add(\n          \"nested_struct\",\n          new StructType()\n            .add(\"c8\", StringType.STRING, fieldMetadata(8)),\n          fieldMetadata(100)),\n      // Un-nest a nested struct all within a Map key\n      new StructType()\n        .add(\n          \"nested_map\",\n          new MapType(\n            new StructType()\n              .add(\"c2\", StringType.STRING, fieldMetadata(1))\n              .add(\"c4\", IntegerType.INTEGER, fieldMetadata(3)),\n            map_field_type.getValueType,\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0)),\n      // Nested column within Map key moved to top-level\n      new StructType()\n        .add(\"c2\", StringType.STRING, fieldMetadata(1)),\n      // Move struct field from key to value within map type\n      new StructType()\n        .add(\n          \"nested_map\",\n          new MapType(\n            new StructType()\n              .add(\n                \"c3\",\n                new StructType()\n                  .add(\"c4\", IntegerType.INTEGER, fieldMetadata(3)),\n                fieldMetadata(2)),\n            new StructType()\n              .add(\"c5\", IntegerType.INTEGER, fieldMetadata(4))\n              .add(\"c2\", StringType.STRING, fieldMetadata(1)),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0)),\n      // Move existing field into double-nested array\n      new StructType()\n        .add(\n          \"nested_array\",\n          new ArrayType(\n            new ArrayType(\n              new StructType()\n                .add(\"c8\", StringType.STRING, fieldMetadata(8))\n                .add(\"c6\", IntegerType.INTEGER, fieldMetadata(6))\n                .add(\"c7\", IntegerType.INTEGER, fieldMetadata(7)),\n              true /* containsNull */\n            ),\n            true /* containsNull */\n          ),\n          fieldMetadata(5)),\n      // Move field out of double-nested array\n      new StructType()\n        .add(\n          \"nested_array\",\n          new ArrayType(\n            new ArrayType(\n              new StructType()\n                .add(\"c7\", IntegerType.INTEGER, fieldMetadata(7)),\n              true /* containsNull */\n            ),\n            true /* containsNull */\n          ),\n          fieldMetadata(5)).add(\"c6\", IntegerType.INTEGER, fieldMetadata(6))).foreach {\n      afterSchema =>\n        val e = intercept[KernelException] {\n          computeSchemaChangesById(beforeSchema, afterSchema)\n        }\n        assert(e.getMessage.contains(\"Cannot move fields between different levels of nesting\"))\n    }\n  }\n\n  test(\"Compute schema changes fails for invalid field moves - map value operations\") {\n    val map_field_type = new MapType(\n      new StructType()\n        .add(\"k1\", StringType.STRING, fieldMetadata(1)),\n      new StructType()\n        .add(\"v1\", IntegerType.INTEGER, fieldMetadata(2))\n        .add(\n          \"v2\",\n          new StructType()\n            .add(\"v3\", StringType.STRING, fieldMetadata(4)),\n          fieldMetadata(3)),\n      true /* valueContainsNull */\n    )\n    val beforeSchema = new StructType()\n      .add(\"mymap\", map_field_type, fieldMetadata(0))\n      .add(\"c1\", StringType.STRING, fieldMetadata(5))\n\n    Seq(\n      // Move field from Map value to top-level\n      new StructType()\n        .add(\n          \"mymap\",\n          new MapType(\n            map_field_type.getKeyType,\n            new StructType()\n              .add(\n                \"v2\",\n                new StructType()\n                  .add(\"v3\", StringType.STRING, fieldMetadata(4)),\n                fieldMetadata(3)),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0))\n        .add(\"v1\", IntegerType.INTEGER, fieldMetadata(2))\n        .add(\"c1\", StringType.STRING, fieldMetadata(5)),\n      // Move field from Map value to Map key\n      new StructType()\n        .add(\n          \"mymap\",\n          new MapType(\n            new StructType()\n              .add(\"k1\", StringType.STRING, fieldMetadata(1))\n              .add(\"v1\", IntegerType.INTEGER, fieldMetadata(2)),\n            new StructType()\n              .add(\n                \"v2\",\n                new StructType()\n                  .add(\"v3\", StringType.STRING, fieldMetadata(4)),\n                fieldMetadata(3)),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0))\n        .add(\"c1\", StringType.STRING, fieldMetadata(5)),\n      // Un-nest a nested struct within Map value\n      new StructType()\n        .add(\n          \"mymap\",\n          new MapType(\n            map_field_type.getKeyType,\n            new StructType()\n              .add(\"v1\", IntegerType.INTEGER, fieldMetadata(2))\n              .add(\"v3\", StringType.STRING, fieldMetadata(4)),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0))\n        .add(\"c1\", StringType.STRING, fieldMetadata(5)),\n      // Move deeply nested field from Map value to top-level\n      new StructType()\n        .add(\n          \"mymap\",\n          new MapType(\n            map_field_type.getKeyType,\n            new StructType()\n              .add(\"v1\", IntegerType.INTEGER, fieldMetadata(2))\n              .add(\n                \"v2\",\n                new StructType(),\n                fieldMetadata(3)),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0))\n        .add(\"c1\", StringType.STRING, fieldMetadata(5))\n        .add(\"v3\", StringType.STRING, fieldMetadata(4))).foreach { afterSchema =>\n      val e = intercept[KernelException] {\n        computeSchemaChangesById(beforeSchema, afterSchema)\n      }\n      assert(e.getMessage.contains(\"Cannot move fields between different levels of nesting\"))\n    }\n  }\n\n  test(\"Compute schema changes fails for invalid field moves - between sibling structs\") {\n    val beforeSchema = new StructType()\n      .add(\n        \"struct1\",\n        new StructType()\n          .add(\"a\", IntegerType.INTEGER, fieldMetadata(1))\n          .add(\"b\", StringType.STRING, fieldMetadata(2)),\n        fieldMetadata(0))\n      .add(\n        \"struct2\",\n        new StructType()\n          .add(\"c\", IntegerType.INTEGER, fieldMetadata(4))\n          .add(\"d\", StringType.STRING, fieldMetadata(5)),\n        fieldMetadata(3))\n      .add(\"e\", IntegerType.INTEGER, fieldMetadata(6))\n\n    Seq(\n      // Move field from struct1 to struct2\n      new StructType()\n        .add(\n          \"struct1\",\n          new StructType()\n            .add(\"b\", StringType.STRING, fieldMetadata(2)),\n          fieldMetadata(0))\n        .add(\n          \"struct2\",\n          new StructType()\n            .add(\"a\", IntegerType.INTEGER, fieldMetadata(1))\n            .add(\"c\", IntegerType.INTEGER, fieldMetadata(4))\n            .add(\"d\", StringType.STRING, fieldMetadata(5)),\n          fieldMetadata(3))\n        .add(\"e\", IntegerType.INTEGER, fieldMetadata(6)),\n      // Move top-level field into a struct\n      new StructType()\n        .add(\n          \"struct1\",\n          new StructType()\n            .add(\"a\", IntegerType.INTEGER, fieldMetadata(1))\n            .add(\"b\", StringType.STRING, fieldMetadata(2))\n            .add(\"e\", IntegerType.INTEGER, fieldMetadata(6)),\n          fieldMetadata(0))\n        .add(\n          \"struct2\",\n          new StructType()\n            .add(\"c\", IntegerType.INTEGER, fieldMetadata(4))\n            .add(\"d\", StringType.STRING, fieldMetadata(5)),\n          fieldMetadata(3))).foreach { afterSchema =>\n      val e = intercept[KernelException] {\n        computeSchemaChangesById(beforeSchema, afterSchema)\n      }\n      assert(e.getMessage.contains(\"Cannot move fields between different levels of nesting\"))\n    }\n  }\n\n  test(\"Compute schema changes fails for invalid field moves - deeply nested structures\") {\n    val beforeSchema = new StructType()\n      .add(\n        \"level1\",\n        new StructType()\n          .add(\n            \"level2\",\n            new StructType()\n              .add(\n                \"level3\",\n                new StructType()\n                  .add(\"deep_field\", StringType.STRING, fieldMetadata(3)),\n                fieldMetadata(2)),\n            fieldMetadata(1)),\n        fieldMetadata(0))\n      .add(\"top_field\", IntegerType.INTEGER, fieldMetadata(4))\n\n    Seq(\n      // Move deeply nested field to top-level\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\n              \"level2\",\n              new StructType()\n                .add(\n                  \"level3\",\n                  new StructType(),\n                  fieldMetadata(2)),\n              fieldMetadata(1)),\n          fieldMetadata(0))\n        .add(\"top_field\", IntegerType.INTEGER, fieldMetadata(4))\n        .add(\"deep_field\", StringType.STRING, fieldMetadata(3)),\n      // Move deeply nested field to level2\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\n              \"level2\",\n              new StructType()\n                .add(\n                  \"level3\",\n                  new StructType(),\n                  fieldMetadata(2))\n                .add(\"deep_field\", StringType.STRING, fieldMetadata(3)),\n              fieldMetadata(1)),\n          fieldMetadata(0))\n        .add(\"top_field\", IntegerType.INTEGER, fieldMetadata(4)),\n      // Move deeply nested field to level1\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\n              \"level2\",\n              new StructType()\n                .add(\n                  \"level3\",\n                  new StructType(),\n                  fieldMetadata(2)),\n              fieldMetadata(1))\n            .add(\"deep_field\", StringType.STRING, fieldMetadata(3)),\n          fieldMetadata(0))\n        .add(\"top_field\", IntegerType.INTEGER, fieldMetadata(4)),\n      // Move top-level field deep into structure\n      new StructType()\n        .add(\n          \"level1\",\n          new StructType()\n            .add(\n              \"level2\",\n              new StructType()\n                .add(\n                  \"level3\",\n                  new StructType()\n                    .add(\"deep_field\", StringType.STRING, fieldMetadata(3))\n                    .add(\"top_field\", IntegerType.INTEGER, fieldMetadata(4)),\n                  fieldMetadata(2)),\n              fieldMetadata(1)),\n          fieldMetadata(0))).foreach { afterSchema =>\n      val e = intercept[KernelException] {\n        computeSchemaChangesById(beforeSchema, afterSchema)\n      }\n      assert(e.getMessage.contains(\"Cannot move fields between different levels of nesting\"))\n    }\n  }\n\n  test(\"Compute schema changes fails for invalid field moves - array and struct combinations\") {\n    val beforeSchema = new StructType()\n      .add(\n        \"my_array\",\n        new ArrayType(\n          new StructType()\n            .add(\"arr_field1\", IntegerType.INTEGER, fieldMetadata(1))\n            .add(\"arr_field2\", StringType.STRING, fieldMetadata(2)),\n          true /* containsNull */\n        ),\n        fieldMetadata(0))\n      .add(\n        \"my_struct\",\n        new StructType()\n          .add(\"struct_field\", IntegerType.INTEGER, fieldMetadata(4)),\n        fieldMetadata(3))\n      .add(\"top_field\", StringType.STRING, fieldMetadata(5))\n\n    Seq(\n      // Move field from array element to regular struct\n      new StructType()\n        .add(\n          \"my_array\",\n          new ArrayType(\n            new StructType()\n              .add(\"arr_field2\", StringType.STRING, fieldMetadata(2)),\n            true /* containsNull */\n          ),\n          fieldMetadata(0))\n        .add(\n          \"my_struct\",\n          new StructType()\n            .add(\"struct_field\", IntegerType.INTEGER, fieldMetadata(4))\n            .add(\"arr_field1\", IntegerType.INTEGER, fieldMetadata(1)),\n          fieldMetadata(3))\n        .add(\"top_field\", StringType.STRING, fieldMetadata(5)),\n      // Move field from regular struct to array element\n      new StructType()\n        .add(\n          \"my_array\",\n          new ArrayType(\n            new StructType()\n              .add(\"arr_field1\", IntegerType.INTEGER, fieldMetadata(1))\n              .add(\"arr_field2\", StringType.STRING, fieldMetadata(2))\n              .add(\"struct_field\", IntegerType.INTEGER, fieldMetadata(4)),\n            true /* containsNull */\n          ),\n          fieldMetadata(0))\n        .add(\n          \"my_struct\",\n          new StructType(),\n          fieldMetadata(3))\n        .add(\"top_field\", StringType.STRING, fieldMetadata(5)),\n      // Move field from array element to top-level\n      new StructType()\n        .add(\n          \"my_array\",\n          new ArrayType(\n            new StructType()\n              .add(\"arr_field2\", StringType.STRING, fieldMetadata(2)),\n            true /* containsNull */\n          ),\n          fieldMetadata(0))\n        .add(\n          \"my_struct\",\n          new StructType()\n            .add(\"struct_field\", IntegerType.INTEGER, fieldMetadata(4)),\n          fieldMetadata(3))\n        .add(\"top_field\", StringType.STRING, fieldMetadata(5))\n        .add(\"arr_field1\", IntegerType.INTEGER, fieldMetadata(1))).foreach { afterSchema =>\n      val e = intercept[KernelException] {\n        computeSchemaChangesById(beforeSchema, afterSchema)\n      }\n      assert(e.getMessage.contains(\"Cannot move fields between different levels of nesting\"))\n    }\n  }\n\n  test(\"Compute schema changes fails for invalid field moves - complex nested maps\") {\n    val beforeSchema = new StructType()\n      .add(\n        \"outer_map\",\n        new MapType(\n          StringType.STRING,\n          new MapType(\n            StringType.STRING,\n            new StructType()\n              .add(\"inner_field\", IntegerType.INTEGER, fieldMetadata(2)),\n            true /* valueContainsNull */\n          ),\n          true /* valueContainsNull */\n        ),\n        fieldMetadata(0))\n      .add(\"other_field\", StringType.STRING, fieldMetadata(3))\n\n    Seq(\n      // Move field from nested map value to outer map value\n      new StructType()\n        .add(\n          \"outer_map\",\n          new MapType(\n            StringType.STRING,\n            new MapType(\n              StringType.STRING,\n              new StructType(),\n              true /* valueContainsNull */\n            ),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0))\n        .add(\"other_field\", StringType.STRING, fieldMetadata(3))\n        .add(\"inner_field\", IntegerType.INTEGER, fieldMetadata(2)),\n      // Move top-level field into nested map value\n      new StructType()\n        .add(\n          \"outer_map\",\n          new MapType(\n            StringType.STRING,\n            new MapType(\n              StringType.STRING,\n              new StructType()\n                .add(\"inner_field\", IntegerType.INTEGER, fieldMetadata(2))\n                .add(\"other_field\", StringType.STRING, fieldMetadata(3)),\n              true /* valueContainsNull */\n            ),\n            true /* valueContainsNull */\n          ),\n          fieldMetadata(0))).foreach { afterSchema =>\n      val e = intercept[KernelException] {\n        computeSchemaChangesById(beforeSchema, afterSchema)\n      }\n      assert(e.getMessage.contains(\"Cannot move fields between different levels of nesting\"))\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // validateUpdatedSchema\n  ///////////////////////////////////////////////////////////////////////////\n\n  test(\"validateUpdatedSchema fails when column mapping is disabled\") {\n    val current = new StructType().add(new StructField(\"id\", IntegerType.INTEGER, true))\n    val updated = current.add(new StructField(\"data\", StringType.STRING, true))\n\n    val e = intercept[IllegalArgumentException] {\n      val tblProperties = Map(COLUMN_MAPPING_MODE_KEY -> \"none\")\n      validateUpdatedSchemaAndGetUpdatedSchema(\n        metadata(current, properties = tblProperties),\n        metadata(updated, properties = tblProperties),\n        dummyProtocol,\n        emptySet(),\n        false // allowNewRequiredFields\n      )\n    }\n\n    assert(e.getMessage == \"Cannot validate updated schema when column mapping is disabled\")\n  }\n\n  private val primitiveSchema = new StructType()\n    .add(\n      \"id\",\n      IntegerType.INTEGER,\n      true,\n      fieldMetadata(id = 1, physicalName = \"id\"))\n\n  private val mapWithStructKey = new StructType()\n    .add(\n      \"map\",\n      new MapType(\n        new StructType()\n          .add(\"id\", IntegerType.INTEGER, true, fieldMetadata(id = 2, physicalName = \"id\")),\n        IntegerType.INTEGER,\n        false),\n      true,\n      fieldMetadata(id = 1, physicalName = \"map\"))\n\n  private val structWithArrayOfStructs = new StructType()\n    .add(\n      \"top_level_struct\",\n      new StructType().add(\n        \"array\",\n        new ArrayType(\n          new StructType().add(\"id\", IntegerType.INTEGER, true, fieldMetadata(4, \"id\")),\n          false),\n        false,\n        fieldMetadata(2, \"array_field\")),\n      fieldMetadata(1, \"top_level_struct\"))\n\n  private val updatedSchemasWithInconsistentPhysicalNames = Table(\n    (\"schemaBefore\", \"updatedSchemaWithInconsistentPhysicalNames\"),\n    // Top level primitive has inconsistent physical name\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          true,\n          fieldMetadata(id = 1, physicalName = \"inconsistent_name\"))),\n    // Map with struct key has inconsistent physical name\n    (\n      mapWithStructKey,\n      new StructType()\n        .add(\n          \"map\",\n          new MapType(\n            new StructType()\n              .add(\n                \"renamed_id\",\n                IntegerType.INTEGER,\n                false,\n                fieldMetadata(id = 2, physicalName = \"inconsistent_name\")),\n            IntegerType.INTEGER,\n            false),\n          true,\n          fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct with array of struct field where inner struct field has inconsistent physical name\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(arrayType = new ArrayType(\n        new StructType().add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          fieldMetadata(4, \"inconsistent_name\")),\n        false))))\n\n  test(\"validateUpdatedSchema fails when physical names are not consistent across ids\") {\n    assertSchemaEvolutionFailure[IllegalArgumentException](\n      updatedSchemasWithInconsistentPhysicalNames,\n      \"Existing field with id .* in current schema\" +\n        \" has physical name id which is different from inconsistent_name\")\n  }\n\n  private val updatedSchemasMissingId = Table(\n    (\"schemaBefore\", \"updatedSchemaWithMissingId\"),\n    // Top level primitive missing field ID\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder().putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"id\").build())),\n    // Map with struct key missing field ID\n    (\n      mapWithStructKey,\n      mapWithStructKey(mapType = new MapType(\n        new StructType()\n          .add(\n            \"renamed_id\",\n            IntegerType.INTEGER,\n            false,\n            FieldMetadata.builder().putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"id\").build()),\n        IntegerType.INTEGER,\n        false))),\n    // Struct with array of struct field where inner struct is missing ID\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(new ArrayType(\n        new StructType().add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          FieldMetadata.builder()\n            .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"id\").build()),\n        false))))\n\n  test(\"validateUpdatedSchema fails when field is missing ID\") {\n    assertSchemaEvolutionFailure[IllegalArgumentException](\n      updatedSchemasMissingId,\n      \"Field renamed_id is missing column id\")\n  }\n\n  private val updatedSchemasMissingPhysicalName = Table(\n    (\"schemaBefore\", \"updatedSchemaWithMissingPhysicalName\"),\n    // Top level primitive missing physical name\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder().putLong(COLUMN_MAPPING_ID_KEY, 1).build())),\n    // Map with struct key missing physical name\n    (\n      mapWithStructKey,\n      mapWithStructKey(mapType = new MapType(\n        new StructType()\n          .add(\n            \"renamed_id\",\n            IntegerType.INTEGER,\n            false,\n            FieldMetadata.builder().putLong(COLUMN_MAPPING_ID_KEY, 1).build()),\n        IntegerType.INTEGER,\n        false))),\n    // Struct with array of struct field where inner struct is missing physical name\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(arrayType = new ArrayType(\n        new StructType().add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          FieldMetadata.builder()\n            .putLong(COLUMN_MAPPING_ID_KEY, 4L).build()),\n        false))))\n\n  test(\"validateUpdatedSchema fails when field is missing physical name\") {\n    assertSchemaEvolutionFailure[IllegalArgumentException](\n      updatedSchemasMissingPhysicalName,\n      \"Field renamed_id is missing physical name\")\n  }\n\n  private val updatedSchemaHasDuplicateColumnId = Table(\n    (\"schemaBefore\", \"updatedSchemaWithMissingPhysicalName\"),\n    // Top level primitive has duplicate id\n    (\n      primitiveSchema,\n      primitiveSchema\n        .add(\n          \"duplicate_id\",\n          IntegerType.INTEGER,\n          true,\n          fieldMetadata(id = 1, physicalName = \"duplicate_id\"))),\n    // Map with struct key adds duplicate field\n    (\n      mapWithStructKey,\n      mapWithStructKey\n        .add(\n          \"duplicate_id\",\n          IntegerType.INTEGER,\n          fieldMetadata(id = 2, physicalName = \"duplicate_id\"))),\n    // Struct with array of struct field where field with duplicate ID is added\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs\n        .add(\n          \"duplicate_id\",\n          IntegerType.INTEGER,\n          fieldMetadata(4, \"duplicate_id\"))))\n\n  test(\"validateUpdatedSchema fails with schema with duplicate column ID\") {\n    forAll(updatedSchemaHasDuplicateColumnId) { (schemaBefore, schemaAfter) =>\n      val e = intercept[IllegalArgumentException] {\n        validateUpdatedSchemaAndGetUpdatedSchema(\n          metadata(schemaBefore),\n          metadata(schemaAfter),\n          dummyProtocol,\n          emptySet(),\n          false /* allowNewRequiredFields */ )\n      }\n\n      assert(e.getMessage.matches(\"Field duplicate_id with id .* already exists\"))\n    }\n  }\n\n  private val validUpdatedSchemas = Table(\n    (\"schemaBefore\", \"updatedSchemaWithRenamedColumns\"),\n    // Top level primitive missing physical name\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"renamed_id\",\n          IntegerType.INTEGER,\n          true,\n          fieldMetadata(id = 1, physicalName = \"id\"))),\n    // Map with struct key renamed\n    (\n      mapWithStructKey,\n      mapWithStructKey(\n        new MapType(\n          new StructType()\n            .add(\n              \"renamed_id\",\n              IntegerType.INTEGER,\n              true,\n              fieldMetadata(id = 2, physicalName = \"id\")),\n          IntegerType.INTEGER,\n          false),\n        fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct with array of struct field where inner struct field is renamed\n    (\n      structWithArrayOfStructs,\n      new StructType()\n        .add(\n          \"top_level_struct\",\n          new StructType().add(\n            \"array\",\n            new ArrayType(\n              new StructType().add(\"renamed_id\", IntegerType.INTEGER, fieldMetadata(4, \"id\")),\n              false),\n            false,\n            fieldMetadata(2, \"array_field\")),\n          fieldMetadata(1, \"top_level_struct\"))))\n\n  test(\"validateUpdatedSchema succeeds with valid ID and physical name\") {\n    forAll(validUpdatedSchemas) { (schemaBefore, schemaAfter) =>\n      validateUpdatedSchemaAndGetUpdatedSchema(\n        metadata(schemaBefore),\n        metadata(schemaAfter),\n        dummyProtocol,\n        emptySet(),\n        false /* allowNewRequiredFields */ )\n    }\n  }\n\n  private val nonNullableFieldAdded = Table(\n    (\"schemaBefore\", \"schemaWithNonNullableFieldAdded\"),\n    (\n      primitiveSchema,\n      primitiveSchema.add(\n        \"required_field\",\n        IntegerType.INTEGER,\n        false,\n        fieldMetadata(3, \"required_field\"))),\n    // Map with struct key where non-nullable field is added to struct\n    (\n      mapWithStructKey,\n      mapWithStructKey(\n        new MapType(\n          new StructType()\n            .add(\n              \"renamed_id\",\n              IntegerType.INTEGER,\n              true,\n              fieldMetadata(id = 2, physicalName = \"id\"))\n            .add(\n              \"required_field\",\n              IntegerType.INTEGER,\n              false,\n              fieldMetadata(id = 3, physicalName = \"required_field\")),\n          IntegerType.INTEGER,\n          false),\n        fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct of array of structs where non-nullable field is added to struct\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(\n        arrayType = new ArrayType(\n          new StructType().add(\n            \"renamed_id\",\n            IntegerType.INTEGER,\n            fieldMetadata(4, \"id\"))\n            .add(\"required_field\", IntegerType.INTEGER, false, fieldMetadata(5, \"required_field\")),\n          false),\n        arrayName = \"renamed_array\")))\n\n  test(\"validateUpdatedSchema fails when non-nullable field is added with \" +\n    \"allowNewRequiredFields=false\") {\n    assertSchemaEvolutionFailure[KernelException](\n      nonNullableFieldAdded,\n      \"Cannot add non-nullable field required_field\",\n      allowNewRequiredFields = false)\n  }\n\n  test(\"validateUpdatedSchema succeeds when non-nullable field is added with \" +\n    \"allowNewRequiredFields=true\") {\n    forAll(nonNullableFieldAdded) { (schemaBefore, schemaAfter) =>\n      validateUpdatedSchemaAndGetUpdatedSchema(\n        metadata(schemaBefore),\n        metadata(schemaAfter),\n        dummyProtocol,\n        emptySet(),\n        true /* allowNewRequiredFields */ )\n    }\n  }\n\n  private val existingFieldNullabilityTightened = Table(\n    (\"schemaBefore\", \"schemaWithFieldNullabilityTightened\"),\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"id\",\n          IntegerType.INTEGER,\n          false,\n          fieldMetadata(id = 1, physicalName = \"id\"))),\n    // Map with struct key where existing id field has nullability tightened\n    (\n      mapWithStructKey,\n      mapWithStructKey(\n        new MapType(\n          new StructType()\n            .add(\n              \"renamed_id\",\n              IntegerType.INTEGER,\n              false,\n              fieldMetadata(id = 2, physicalName = \"id\")),\n          IntegerType.INTEGER,\n          false),\n        fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct of array of structs where id field in inner struct has nullability tightened\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(\n        arrayType = new ArrayType(\n          new StructType().add(\n            \"renamed_id\",\n            IntegerType.INTEGER,\n            false,\n            fieldMetadata(4, \"id\")),\n          false),\n        arrayName = \"renamed_array\")))\n\n  test(\"validateUpdatedSchema fails when existing nullability is tightened with \" +\n    \"allowNewRequiredFields=false\") {\n    assertSchemaEvolutionFailure[KernelException](\n      existingFieldNullabilityTightened,\n      \"Cannot tighten the nullability of existing field .*id\")\n  }\n\n  test(\"validateUpdatedSchema fails when existing nullability is tightened with \" +\n    \"allowNewRequiredFields=true\") {\n    assertSchemaEvolutionFailure[KernelException](\n      existingFieldNullabilityTightened,\n      \"Cannot tighten the nullability of existing field .*id\",\n      allowNewRequiredFields = true)\n  }\n\n  private val invalidTypeChange = Table(\n    (\"schemaBefore\", \"schemaWithInvalidTypeChange\"),\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"id\",\n          StringType.STRING,\n          true,\n          fieldMetadata(id = 1, physicalName = \"id\"))),\n    // Map with struct key where id is changed to string\n    (\n      mapWithStructKey,\n      mapWithStructKey(\n        new MapType(\n          new StructType()\n            .add(\n              \"renamed_id_as_string\",\n              StringType.STRING,\n              true,\n              fieldMetadata(id = 2, physicalName = \"id\")),\n          IntegerType.INTEGER,\n          false),\n        fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct of array of struct where inner id field is changed to string\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(\n        arrayType = new ArrayType(\n          new StructType().add(\n            \"renamed_id_as_string\",\n            StringType.STRING,\n            true,\n            fieldMetadata(4, \"id\")),\n          false),\n        arrayName = \"renamed_array\")))\n\n  test(\"validateUpdatedSchema fails when invalid type change is performed\") {\n    assertSchemaEvolutionFailure[KernelException](\n      invalidTypeChange,\n      \"Cannot change the type of existing field id from integer to string\")\n  }\n\n  private val invalidTypeChangesNested = Table(\n    (\"schemaBefore\", \"schemaWithInvalidTypeChange\"),\n    // Array to Map\n    (\n      newSchema((\n        1,\n        new StructField(\n          \"array\",\n          new ArrayType(\n            IntegerType.INTEGER,\n            false),\n          true))),\n      newSchema((\n        1,\n        new StructField(\n          \"to_map\",\n          new MapType(\n            IntegerType.INTEGER,\n            IntegerType.INTEGER,\n            false),\n          true)))),\n\n    // Array to Map\n    (\n      newSchema((\n        1,\n        new StructField(\n          \"map\",\n          new MapType(\n            IntegerType.INTEGER,\n            IntegerType.INTEGER,\n            false),\n          true))),\n      newSchema((\n        1,\n        new StructField(\n          \"to_array\",\n          new ArrayType(\n            IntegerType.INTEGER,\n            false),\n          true)))),\n    // nested array change\n    (\n      newSchema((\n        1,\n        new StructField(\n          \"array\",\n          new ArrayType(\n            new ArrayType(IntegerType.INTEGER, false),\n            false),\n          true))),\n      newSchema((\n        1,\n        new StructField(\n          \"to_map\",\n          new ArrayType(\n            new MapType(IntegerType.INTEGER, IntegerType.INTEGER, false),\n            false),\n          true)))),\n    // nested map change\n    (\n      newSchema((\n        1,\n        new StructField(\n          \"map\",\n          new MapType(\n            IntegerType.INTEGER,\n            new ArrayType(IntegerType.INTEGER, false),\n            false),\n          true))),\n      newSchema((\n        1,\n        new StructField(\n          \"to_nested_array_to_primitive\",\n          new MapType(\n            IntegerType.INTEGER,\n            IntegerType.INTEGER,\n            false),\n          false)))))\n  test(\"validateUpdatedSchema fails when invalid type change is performed on nested fields\") {\n    assertSchemaEvolutionFailure[KernelException](\n      invalidTypeChangesNested,\n      \"Cannot change the type of existing field.*\")\n  }\n\n  private val validateAddedFields = Table(\n    (\"schemaBefore\", \"schemaWithAddedField\"),\n    (\n      primitiveSchema,\n      primitiveSchema\n        .add(\n          \"data\",\n          StringType.STRING,\n          true,\n          fieldMetadata(id = 2, physicalName = \"data\"))),\n    // Map with struct key where data field is added\n    (\n      mapWithStructKey,\n      mapWithStructKey(\n        new MapType(\n          new StructType()\n            .add(\n              \"renamed_id\",\n              IntegerType.INTEGER,\n              true,\n              fieldMetadata(id = 2, physicalName = \"id\"))\n            .add(\n              \"data\",\n              StringType.STRING,\n              true,\n              fieldMetadata(id = 3, physicalName = \"data\")),\n          IntegerType.INTEGER,\n          false),\n        fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct of array of struct where inner struct has added string data field\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(\n        arrayType = new ArrayType(\n          new StructType().add(\n            \"renamed_id\",\n            IntegerType.INTEGER,\n            true,\n            fieldMetadata(4, \"id\"))\n            .add(\n              \"data\",\n              StringType.STRING,\n              true,\n              fieldMetadata(5, \"data\")),\n          false),\n        arrayName = \"renamed_array\")))\n\n  test(\"validateUpdatedSchema succeeds when adding field\") {\n    forAll(validateAddedFields) { (schemaBefore, schemaAfter) =>\n      validateUpdatedSchemaAndGetUpdatedSchema(\n        metadata(schemaBefore),\n        metadata(schemaAfter),\n        dummyProtocol,\n        emptySet(),\n        false /* allowNewRequiredFields */ )\n    }\n  }\n\n  private val validateMetadataChange = Table(\n    (\"schemaBefore\", \"schemaWithAddedField\"),\n    // Adding column comment to id column\n    (\n      primitiveSchema,\n      new StructType()\n        .add(\n          \"id\",\n          IntegerType.INTEGER,\n          true,\n          FieldMetadata.builder().fromMetadata(fieldMetadata(\n            id = 1,\n            physicalName = \"id\")).putString(\"comment\", \"id comment\").build())),\n    // Struct of array of struct where inner struct has added column comment to metadata\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(\n        arrayType = new ArrayType(\n          new StructType().add(\n            \"id\",\n            IntegerType.INTEGER,\n            true,\n            FieldMetadata.builder().fromMetadata(fieldMetadata(4, \"id\")).putString(\n              \"comment\",\n              \"id comment\").build()),\n          false),\n        arrayName = \"renamed_array\")))\n\n  test(\"validateUpdatedSchema succeeds when updating field metadata\") {\n    forAll(validateMetadataChange) { (schemaBefore, schemaAfter) =>\n      validateUpdatedSchemaAndGetUpdatedSchema(\n        metadata(schemaBefore),\n        metadata(schemaAfter),\n        dummyProtocol,\n        emptySet(),\n        false /* allowNewRequiredFields */ )\n    }\n  }\n\n  private val underMaxColIdFieldAdded = Table(\n    (\"schemaBefore\", \"schemaWithUnderMaxFieldIdAdded\"),\n    (\n      primitiveSchema,\n      primitiveSchema.add(\n        \"too_low_field_id_field\",\n        IntegerType.INTEGER,\n        true,\n        fieldMetadata(0, \"too_low_field_id_field\"))),\n    // Map with struct key where under max col-id field is added\n    (\n      mapWithStructKey,\n      mapWithStructKey(\n        new MapType(\n          new StructType()\n            .add(\n              \"renamed_id\",\n              IntegerType.INTEGER,\n              true,\n              fieldMetadata(id = 2, physicalName = \"id\"))\n            .add(\n              \"too_low_field_id_field\",\n              IntegerType.INTEGER,\n              true,\n              fieldMetadata(id = 0, physicalName = \"too_low_field_id_field\")),\n          IntegerType.INTEGER,\n          true),\n        fieldMetadata(id = 1, physicalName = \"map\"))),\n    // Struct of array of structs where under max col-id field is added\n    (\n      structWithArrayOfStructs,\n      structWithArrayOfStructs(\n        arrayType = new ArrayType(\n          new StructType().add(\n            \"renamed_id\",\n            IntegerType.INTEGER,\n            fieldMetadata(4, \"id\"))\n            .add(\n              \"too_low_field_id_field\",\n              IntegerType.INTEGER,\n              true,\n              fieldMetadata(0, \"too_low_field_id_field\")),\n          true),\n        arrayName = \"renamed_array\")))\n\n  test(\"validateUpdatedSchema fails when a new field with a fieldId < maxColId is added\") {\n    assertSchemaEvolutionFailure[IllegalArgumentException](\n      underMaxColIdFieldAdded,\n      \"Cannot add a new column with a fieldId <= maxFieldId. Found field: .* with\"\n        + \"fieldId=0. Current maxFieldId in the table is: .*\")\n  }\n\n  private def mapWithStructKey(\n      mapType: DataType =\n        mapWithStructKey.get(\"map\").getDataType,\n      mapFieldMetadata: FieldMetadata =\n        mapWithStructKey.get(\"map\").getMetadata): StructType = {\n    new StructType()\n      .add(\n        \"map\",\n        mapType,\n        true,\n        mapFieldMetadata)\n  }\n\n  private def structWithArrayOfStructs(\n      arrayType: ArrayType =\n        structWithArrayOfStructs\n          .get(\"top_level_struct\")\n          .getDataType.asInstanceOf[StructType]\n          .get(\"array\").getDataType.asInstanceOf[ArrayType],\n      arrayMetadata: FieldMetadata =\n        structWithArrayOfStructs\n          .get(\"top_level_struct\")\n          .getDataType.asInstanceOf[StructType]\n          .get(\"array\").getMetadata,\n      arrayName: String = \"array\") = {\n    new StructType()\n      .add(\n        \"top_level_struct\",\n        new StructType().add(\n          arrayName,\n          arrayType,\n          false,\n          arrayMetadata),\n        fieldMetadata(1, \"top_level_struct\"))\n  }\n\n  private def assertSchemaEvolutionFailure[T <: Throwable](\n      evolutionCases: TableFor2[StructType, StructType],\n      expectedMessage: String,\n      tableProperties: Map[String, String] =\n        Map(ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\"),\n      allowNewRequiredFields: Boolean = false)(implicit classTag: ClassTag[T]) {\n    forAll(evolutionCases) { (schemaBefore, schemaAfter) =>\n      val e = intercept[T] {\n        validateUpdatedSchemaAndGetUpdatedSchema(\n          metadata(schemaBefore, tableProperties),\n          metadata(schemaAfter, tableProperties),\n          dummyProtocol,\n          emptySet(),\n          allowNewRequiredFields)\n      }\n\n      assert(e.getMessage.matches(expectedMessage), s\"${e.getMessage} ~= $expectedMessage\")\n    }\n  }\n\n  private def metadata(\n      schema: StructType,\n      properties: Map[String, String] =\n        Map(ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\")): Metadata = {\n    val tblProperties = properties ++\n      Map(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY ->\n        ColumnMapping.findMaxColumnId(schema).toString)\n    new Metadata(\n      \"id\",\n      Optional.empty(), /* name */\n      Optional.empty(), /* description */\n      new Format(),\n      DataTypeJsonSerDe.serializeDataType(schema),\n      schema,\n      VectorUtils.buildArrayValue(util.Arrays.asList(), StringType.STRING), // partitionColumns\n      Optional.empty(), // createdTime\n      stringStringMapValue(tblProperties.asJava) // configurationMap\n    )\n  }\n\n  private def fieldMetadata(id: Integer): FieldMetadata = {\n    fieldMetadata(id, s\"col-$id\")\n  }\n\n  private def fieldMetadata(id: Integer, physicalName: String): FieldMetadata = {\n    FieldMetadata.builder()\n      .putLong(COLUMN_MAPPING_ID_KEY, id.longValue())\n      .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, physicalName)\n      .build()\n  }\n\n  private def newSchema(tuple: (Int, StructField)*): StructType = {\n    val fields = tuple.map { case (id, field) =>\n      addFieldId(id, field)\n    }\n    val schemaWithIds = new StructType(fields.asJava)\n    schemaWithIds\n  }\n\n  private def addFieldId(id: Int, field: StructField) = {\n    val metadataWithFieldIds = FieldMetadata.builder().fromMetadata(field.getMetadata)\n      .putLong(\"delta.columnMapping.id\", id)\n      .putString(\"delta.columnMapping.physicalName\", s\"col-$id\")\n      .build()\n    field.withNewMetadata(metadataWithFieldIds)\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Type Changes Tests\n  ///////////////////////////////////////////////////////////////////////////\n\n  test(\"Compute schema changes adds schema change\") {\n    val currentSchema = newSchema((1, new StructField(\"id\", IntegerType.INTEGER, true)))\n    val updatedSchema = newSchema((1, new StructField(\"id\", LongType.LONG, true)))\n\n    val schemaChanges = computeSchemaChangesById(currentSchema, updatedSchema)\n\n    // Verify that we have one updated field\n    assert(schemaChanges.updatedFields().size() == 1)\n    val schemaUpdate = schemaChanges.updatedFields().get(0)\n\n    // Verify that the updated field has the expected before and after values\n    assert(schemaUpdate.before == currentSchema.get(\"id\"))\n    assert(schemaUpdate.after == updatedSchema.get(\"id\"))\n\n    // Verify that the updated schema has a TypeChange recorded\n    assert(schemaChanges.updatedSchema().isPresent)\n    val updatedField = schemaChanges.updatedSchema().get().get(\"id\")\n    val typeChanges = updatedField.getTypeChanges\n    assert(typeChanges.size() == 1)\n    val typeChange = typeChanges.get(0)\n    assert(typeChange.getFrom == IntegerType.INTEGER)\n    assert(typeChange.getTo == LongType.LONG)\n  }\n\n  test(\"Type changes are preserved across schema updates\") {\n    // Create a field with integer type and an existing type change from byte to int\n    val byteType = ByteType.BYTE\n    val intType = IntegerType.INTEGER\n\n    val existingTypeChange = new TypeChange(byteType, intType)\n    val fieldWithTypeChange = new StructField(\n      \"id\",\n      intType,\n      true,\n      fieldMetadata(1, \"id\")).withTypeChanges(List(existingTypeChange).asJava)\n\n    val currentSchema = new StructType(List(fieldWithTypeChange).asJava)\n    // Change the type to long without specifying the previous type change\n    val updatedSchema = newSchema((1, new StructField(\"id\", intType, true)))\n\n    val schemaChanges = computeSchemaChangesById(currentSchema, updatedSchema)\n\n    // Verify that the updated schema has the TypeChange preserved and a new one added\n    assert(schemaChanges.updatedSchema().isPresent)\n    val updatedField = schemaChanges.updatedSchema().get().get(\"id\")\n\n    // Check that both type changes are recorded (the original and the new one)\n    val typeChanges = updatedField.getTypeChanges\n    assert(typeChanges.size() == 1)\n    assert(typeChanges.get(0).getFrom == byteType)\n    assert(typeChanges.get(0).getTo == intType)\n  }\n\n  test(\"Type changes are preserved across schema updates and new ones are added\") {\n    // Create a field with integer type and an existing type change from byte to int\n    val byteType = ByteType.BYTE\n    val intType = IntegerType.INTEGER\n    val longType = LongType.LONG\n\n    val existingTypeChange = new TypeChange(byteType, intType)\n    val fieldWithTypeChange = new StructField(\n      \"id\",\n      intType,\n      true,\n      fieldMetadata(1, \"id\")).withTypeChanges(List(existingTypeChange).asJava)\n\n    val currentSchema = new StructType(List(fieldWithTypeChange).asJava)\n    // Change the type to long without specifying the previous type change\n    val updatedSchema = newSchema((1, new StructField(\"id\", longType, true)))\n\n    val schemaChanges = computeSchemaChangesById(currentSchema, updatedSchema)\n\n    // Verify that the updated schema has the TypeChange preserved and a new one added\n    assert(schemaChanges.updatedSchema().isPresent)\n    val updatedField = schemaChanges.updatedSchema().get().get(\"id\")\n\n    // Check that both type changes are recorded (the original and the new one)\n    val typeChanges = updatedField.getTypeChanges\n    assert(typeChanges.size() == 2)\n    val typeChange = typeChanges.get(0)\n    assert(typeChange.getFrom == byteType)\n    assert(typeChange.getTo == intType)\n    val newTypeChange = typeChanges.get(1)\n    assert(newTypeChange.getFrom == intType)\n    assert(newTypeChange.getTo == longType)\n  }\n\n  test(\"Throws exception when type changes are inconsistent\") {\n    // Create a field with integer type and an existing type change from byte to int\n    val byteType = ByteType.BYTE\n    val intType = IntegerType.INTEGER\n    val longType = LongType.LONG\n\n    val existingTypeChange = new TypeChange(byteType, intType)\n    val fieldWithTypeChange = new StructField(\n      \"id\",\n      intType,\n      true,\n      fieldMetadata(1, \"id\")).withTypeChanges(List(existingTypeChange).asJava)\n\n    val currentSchema = new StructType(List(fieldWithTypeChange).asJava)\n\n    // Create a new field with a different type change history\n    val differentTypeChange = new TypeChange(longType, intType)\n    val fieldWithDifferentTypeChange = new StructField(\n      \"id\",\n      intType,\n      true,\n      fieldMetadata(1, \"id\")).withTypeChanges(List(differentTypeChange).asJava)\n\n    val updatedSchema = new StructType(List(fieldWithDifferentTypeChange).asJava)\n\n    // This should throw an exception because the type changes are not equal\n    expectFailure(\n      \"detected a modified type changes field at 'id'\",\n      \"typechange(from=byte,to=integer)\",\n      \"typechange(from=long,to=integer)\") {\n      computeSchemaChangesById(currentSchema, updatedSchema)\n    }\n  }\n\n  private val validTypeWideningSchemas = Table(\n    (\n      \"schemaBefore\",\n      \"schemaAfter\",\n      \"typeWideningEnabled\",\n      \"icebergWriterCompatV1Enabled\",\n      \"shouldSucceed\"),\n    // Integer widening: Int -> Long (always allowed with type widening)\n    (\n      primitiveSchema,\n      new StructType().add(\n        \"id\",\n        LongType.LONG,\n        true,\n        fieldMetadata(id = 1, physicalName = \"id\")),\n      /* typeWideningEnabled= */ true,\n      /* icebergWriterCompatv1enabled= */ false,\n      /* shouldSucceed= */ true),\n    // Integer widening: Int -> Long (not allowed without type widening)\n    (\n      primitiveSchema,\n      new StructType().add(\n        \"id\",\n        LongType.LONG,\n        true,\n        fieldMetadata(id = 1, physicalName = \"id\")),\n      /* typeWideningEnabled= */ false,\n      /* icebergWriterCompatv1enabled= */ false,\n      /* shouldSucceed= */ false),\n    // Integer to Double widening (allowed with type widening but not with Iceberg compat)\n    (\n      primitiveSchema,\n      new StructType().add(\n        \"id\",\n        DoubleType.DOUBLE,\n        true,\n        fieldMetadata(id = 1, physicalName = \"id\")),\n      /* typeWideningEnabled= */ true,\n      /* icebergWriterCompatv1enabled= */ false,\n      /* shouldSucceed= */ true),\n    // Integer to Double widening (not allowed with Iceberg compat)\n    (\n      primitiveSchema,\n      new StructType().add(\n        \"id\",\n        DoubleType.DOUBLE,\n        true,\n        fieldMetadata(id = 1, physicalName = \"id\")),\n      /* typeWideningEnabled= */ true,\n      /* icebergwritercompatv1enabled= */ true,\n      /* shouldSucceed= */ false),\n    // Invalid type change: Int -> String (never allowed)\n    (\n      primitiveSchema,\n      new StructType().add(\n        \"id\",\n        StringType.STRING,\n        true,\n        fieldMetadata(id = 1, physicalName = \"id\")),\n      /* typeWideningEnabled= */ true,\n      /* typeWideningEnabled= */ false,\n      /* shouldSucceed= */ false))\n\n  forAll(validTypeWideningSchemas) {\n    (\n        schemaBefore,\n        schemaAfter,\n        typeWideningEnabled,\n        icebergWriterCompatV1Enabled,\n        shouldSucceed) =>\n      test(\"validateUpdatedSchema with type widening \" +\n        s\"$schemaBefore $schemaAfter $typeWideningEnabled $icebergWriterCompatV1Enabled\") {\n\n        val tblProperties = Map(\n          ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\",\n          TableConfig.TYPE_WIDENING_ENABLED.getKey -> typeWideningEnabled.toString,\n          TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey ->\n            icebergWriterCompatV1Enabled.toString)\n\n        if (shouldSucceed) {\n          // Should not throw an exception\n          validateUpdatedSchemaAndGetUpdatedSchema(\n            metadata(schemaBefore, tblProperties),\n            metadata(schemaAfter, tblProperties),\n            dummyProtocol,\n            emptySet(),\n            false /* allowNewRequiredFields */\n          )\n        } else {\n          // Should throw an exception\n          val e = intercept[KernelException] {\n            validateUpdatedSchemaAndGetUpdatedSchema(\n              metadata(schemaBefore, tblProperties),\n              metadata(schemaAfter, tblProperties),\n              dummyProtocol,\n              emptySet(),\n              false /* allowNewRequiredFields */\n            )\n          }\n          assert(e.getMessage.contains(\"Cannot change the type of existing field\"))\n        }\n      }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // validateNoMapStructKeyChanges\n  ///////////////////////////////////////////////////////////////////////////\n\n  private val updatedSchemasWithChangedMaps = Table(\n    (\"schemaBefore\", \"updatedSchemaWithChangedMapKey\"),\n    // add a col\n    (\n      mapWithStructKey,\n      new StructType()\n        .add(\n          \"map\",\n          new MapType(\n            new StructType()\n              .add(\"id\", IntegerType.INTEGER, true, fieldMetadata(id = 2, physicalName = \"id\"))\n              .add(\"id2\", IntegerType.INTEGER, true, fieldMetadata(id = 3, physicalName = \"id2\")),\n            IntegerType.INTEGER,\n            false),\n          true,\n          fieldMetadata(id = 1, physicalName = \"map\"))),\n    (\n      new StructType()\n        .add(\n          \"map\",\n          new MapType(\n            new StructType()\n              .add(\"id\", IntegerType.INTEGER, true, fieldMetadata(id = 2, physicalName = \"id\"))\n              .add(\"id2\", IntegerType.INTEGER, true, fieldMetadata(id = 3, physicalName = \"id2\")),\n            IntegerType.INTEGER,\n            false),\n          true,\n          fieldMetadata(id = 1, physicalName = \"map\")),\n      mapWithStructKey),\n    (\n      new StructType()\n        .add(\n          \"top_level_struct\",\n          new StructType().add(\n            \"map\",\n            new MapType(\n              new StructType()\n                .add(\"id\", IntegerType.INTEGER, true, fieldMetadata(id = 3, physicalName = \"id\")),\n              IntegerType.INTEGER,\n              false),\n            true,\n            fieldMetadata(2, \"map\")),\n          fieldMetadata(1, \"top_level_struct\")),\n      new StructType()\n        .add(\n          \"top_level_struct\",\n          new StructType().add(\n            \"map\",\n            new MapType(\n              new StructType()\n                .add(\"id\", IntegerType.INTEGER, true, fieldMetadata(id = 3, physicalName = \"id\"))\n                .add(\"id2\", IntegerType.INTEGER, true, fieldMetadata(id = 4, physicalName = \"id\")),\n              IntegerType.INTEGER,\n              false),\n            true,\n            fieldMetadata(2, \"map\")),\n          fieldMetadata(1, \"top_level_struct\"))))\n\n  test(\"validateNoMapStructKeyChanges fails when map struct changes\") {\n    val tblProperties = Map(\n      TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\",\n      ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\")\n    assertSchemaEvolutionFailure[KernelException](\n      updatedSchemasWithChangedMaps,\n      \"Cannot change the type key of Map field map from .*\",\n      tableProperties = tblProperties)\n  }\n\n  test(\"Validate succeeds when adding variant column\") {\n    val tableProperties = Map(ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\")\n    val before = new StructType().add(\n      \"id\",\n      IntegerType.INTEGER,\n      false,\n      fieldMetadata(id = 1, physicalName = \"id\"))\n\n    val schemaWithVariant = before.add(\n      \"variant\",\n      VariantType.VARIANT,\n      true,\n      fieldMetadata(id = 2, physicalName = \"variant\"))\n\n    validateUpdatedSchemaAndGetUpdatedSchema(\n      metadata(before, tableProperties),\n      metadata(schemaWithVariant, tableProperties),\n      dummyProtocol,\n      emptySet(),\n      false)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/TimestampUtilsSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util\n\nimport java.time.{Instant, LocalDateTime, OffsetDateTime, ZoneOffset}\nimport java.time.temporal.ChronoUnit\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TimestampUtilsSuite extends AnyFunSuite {\n  // Expected micros for 9999-12-31T23:59:59Z\n  private val FAR_FUTURE_MICROS = 253402300799000000L\n\n  test(\"toEpochMicros(Instant) - epoch\") {\n    assert(TimestampUtils.toEpochMicros(Instant.EPOCH) === 0L)\n  }\n\n  test(\"toEpochMicros(Instant) - far future timestamp does not overflow\") {\n    val instant = Instant.parse(\"9999-12-31T23:59:59Z\")\n    assert(TimestampUtils.toEpochMicros(instant) === FAR_FUTURE_MICROS)\n  }\n\n  test(\"toEpochMicros(Instant) - preserves microsecond precision\") {\n    // 1000 seconds + 123456 microseconds (123456000 nanoseconds)\n    val instant = Instant.ofEpochSecond(1000, 123456000)\n    assert(TimestampUtils.toEpochMicros(instant) === 1000123456L)\n  }\n\n  test(\"toEpochMicros(OffsetDateTime) - epoch\") {\n    val dt = OffsetDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)\n    assert(TimestampUtils.toEpochMicros(dt) === 0L)\n  }\n\n  test(\"toEpochMicros(OffsetDateTime) - far future timestamp does not overflow\") {\n    val dt = OffsetDateTime.parse(\"9999-12-31T23:59:59Z\")\n    assert(TimestampUtils.toEpochMicros(dt) === FAR_FUTURE_MICROS)\n  }\n\n  test(\"toEpochMicros(LocalDateTime) - epoch\") {\n    val dt = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)\n    assert(TimestampUtils.toEpochMicros(dt) === 0L)\n  }\n\n  test(\"toEpochMicros(LocalDateTime) - far future timestamp does not overflow\") {\n    val dt = LocalDateTime.parse(\"9999-12-31T23:59:59\")\n    assert(TimestampUtils.toEpochMicros(dt) === FAR_FUTURE_MICROS)\n  }\n\n  test(\"toEpochMicros(LocalDateTime) - preserves microsecond precision\") {\n    // 1000 seconds + 123456 microseconds (123456000 nanoseconds)\n    val dt = LocalDateTime.ofEpochSecond(1000, 123456000, ZoneOffset.UTC)\n    assert(TimestampUtils.toEpochMicros(dt) === 1000123456L)\n  }\n\n  test(\"ChronoUnit.MICROS.between() throws for far-future timestamps\") {\n    // This test documents why we need TimestampUtils instead of ChronoUnit.MICROS.between().\n    // ChronoUnit.MICROS.between() internally computes (seconds * 1_000_000_000) / 1000,\n    // where the intermediate nanoseconds value overflows for timestamps beyond ~292 years\n    // from epoch.\n    val farFutureInstant = Instant.parse(\"9999-12-31T23:59:59Z\")\n\n    // ChronoUnit throws ArithmeticException due to overflow\n    intercept[ArithmeticException] {\n      ChronoUnit.MICROS.between(Instant.EPOCH, farFutureInstant)\n    }\n\n    // TimestampUtils returns correct value\n    assert(TimestampUtils.toEpochMicros(farFutureInstant) === FAR_FUTURE_MICROS)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/internal/util/VectorUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.internal.util\n\nimport java.lang.{Boolean => BooleanJ, Byte => ByteJ, Double => DoubleJ, Float => FloatJ, Integer => IntegerJ, Long => LongJ, Short => ShortJ}\nimport java.math.BigDecimal\nimport java.sql.{Date, Timestamp}\nimport java.util\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ArrayValue, ColumnVector, MapValue, Row}\nimport io.delta.kernel.internal.data.GenericRow\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types.{ArrayType, BinaryType, BooleanType, ByteType, DateType, DecimalType, DoubleType, FloatType, IntegerType, LongType, MapType, ShortType, StringType, StructType, TimestampNTZType, TimestampType}\n\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.prop.Tables.Table\n\nclass VectorUtilsSuite extends AnyFunSuite with VectorTestUtils {\n\n  Table(\n    (\"values\", \"dataType\"),\n    (List[ByteJ](1.toByte, 2.toByte, 3.toByte, null), ByteType.BYTE),\n    (List[ShortJ](1.toShort, 2.toShort, 3.toShort, null), ShortType.SHORT),\n    (List[IntegerJ](1, 2, 3, null), IntegerType.INTEGER),\n    (List[LongJ](1L, 2L, 3L, null), LongType.LONG),\n    (List[FloatJ](1.0f, 2.0f, 3.0f, null), FloatType.FLOAT),\n    (List[DoubleJ](1.0, 2.0, 3.0, null), DoubleType.DOUBLE),\n    (List[Array[Byte]](\"one\".getBytes, \"two\".getBytes, \"three\".getBytes, null), BinaryType.BINARY),\n    (List[BooleanJ](true, false, false, null), BooleanType.BOOLEAN),\n    (\n      List[BigDecimal](new BigDecimal(\"1\"), new BigDecimal(\"2\"), new BigDecimal(\"3\"), null),\n      new DecimalType(10, 2)),\n    (List[String](\"one\", \"two\", \"three\", null), StringType.STRING),\n    (\n      List[IntegerJ](10, 20, 30, null),\n      DateType.DATE),\n    (\n      List[LongJ](\n        Timestamp.valueOf(\"2023-01-01 00:00:00\").getTime,\n        Timestamp.valueOf(\"2023-01-02 00:00:00\").getTime,\n        Timestamp.valueOf(\"2023-01-03 00:00:00\").getTime,\n        null),\n      TimestampType.TIMESTAMP),\n    (\n      List[LongJ](\n        Timestamp.valueOf(\"2023-01-01 00:00:00\").getTime,\n        Timestamp.valueOf(\"2023-01-02 00:00:00\").getTime,\n        Timestamp.valueOf(\"2023-01-03 00:00:00\").getTime,\n        null),\n      TimestampNTZType.TIMESTAMP_NTZ)).foreach(testCase =>\n    test(s\"handle ${testCase._2} array correctly\") {\n      val values = testCase._1\n      val dataType = testCase._2\n      val columnVector = VectorUtils.buildColumnVector(values.asJava, dataType)\n      assert(columnVector.getSize == 4)\n\n      dataType match {\n        case ByteType.BYTE =>\n          assert(columnVector.getByte(0) == 1.toByte)\n          assert(columnVector.getByte(1) == 2.toByte)\n          assert(columnVector.getByte(2) == 3.toByte)\n        case ShortType.SHORT =>\n          assert(columnVector.getShort(0) == 1.toShort)\n          assert(columnVector.getShort(1) == 2.toShort)\n          assert(columnVector.getShort(2) == 3.toShort)\n        case IntegerType.INTEGER =>\n          assert(columnVector.getInt(0) == 1)\n          assert(columnVector.getInt(1) == 2)\n          assert(columnVector.getInt(2) == 3)\n        case LongType.LONG =>\n          assert(columnVector.getLong(0) == 1L)\n          assert(columnVector.getLong(1) == 2L)\n          assert(columnVector.getLong(2) == 3L)\n        case FloatType.FLOAT =>\n          assert(columnVector.getFloat(0) == 1.0f)\n          assert(columnVector.getFloat(1) == 2.0f)\n          assert(columnVector.getFloat(2) == 3.0f)\n        case DoubleType.DOUBLE =>\n          assert(columnVector.getDouble(0) == 1.0)\n          assert(columnVector.getDouble(1) == 2.0)\n          assert(columnVector.getDouble(2) == 3.0)\n        case BooleanType.BOOLEAN =>\n          assert(columnVector.getBoolean(0))\n          assert(!columnVector.getBoolean(1))\n          assert(!columnVector.getBoolean(2))\n        case _: DecimalType =>\n          assert(columnVector.getDecimal(0) == new BigDecimal(\"1\"))\n          assert(columnVector.getDecimal(1) == new BigDecimal(\"2\"))\n          assert(columnVector.getDecimal(2) == new BigDecimal(\"3\"))\n        case BinaryType.BINARY =>\n          assert(columnVector.getBinary(0) sameElements \"one\".getBytes)\n          assert(columnVector.getBinary(1) sameElements \"two\".getBytes)\n          assert(columnVector.getBinary(2) sameElements \"three\".getBytes)\n        case StringType.STRING =>\n          assert(columnVector.getString(0) == \"one\")\n          assert(columnVector.getString(1) == \"two\")\n          assert(columnVector.getString(2) == \"three\")\n        case DateType.DATE =>\n          assert(columnVector.getInt(0) == 10)\n          assert(columnVector.getInt(1) == 20)\n          assert(columnVector.getInt(2) == 30)\n        case TimestampType.TIMESTAMP =>\n          assert(\n            columnVector.getLong(0) == Timestamp.valueOf(\"2023-01-01 00:00:00\").getTime)\n          assert(\n            columnVector.getLong(1) == Timestamp.valueOf(\"2023-01-02 00:00:00\").getTime)\n          assert(\n            columnVector.getLong(2) == Timestamp.valueOf(\"2023-01-03 00:00:00\").getTime)\n        case TimestampNTZType.TIMESTAMP_NTZ =>\n          assert(\n            columnVector.getLong(0) == Timestamp.valueOf(\"2023-01-01 00:00:00\").getTime)\n          assert(\n            columnVector.getLong(1) == Timestamp.valueOf(\"2023-01-02 00:00:00\").getTime)\n          assert(\n            columnVector.getLong(2) == Timestamp.valueOf(\"2023-01-03 00:00:00\").getTime)\n      }\n      assert(columnVector.isNullAt(3))\n    })\n\n  test(s\"handle array of struct correctly\") {\n    val structType =\n      new StructType().add(\"name\", StringType.STRING).add(\"value\", IntegerType.INTEGER)\n\n    val arrayType = new ArrayType(structType, true)\n\n    def row(name: String, value: Integer): Row = {\n      val map = new util.HashMap[Integer, AnyRef]\n      map.put(0, name)\n      map.put(1, value)\n      new GenericRow(structType, map)\n    }\n\n    val values = List[ArrayValue](\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[Row](\n            row(\"a1\", 1),\n            row(\"a2\", 2)).asJava,\n          structType)\n      },\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[Row](\n            row(\"b1\", 3),\n            row(\"b2\", 4)).asJava,\n          structType)\n      },\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[Row](\n            row(\"c1\", 5),\n            row(\"c2\", 6)).asJava,\n          structType)\n      },\n      null)\n\n    val columnVector = VectorUtils.buildColumnVector(values.asJava, arrayType)\n\n    // Test size\n    assert(columnVector.getSize == 4)\n\n    // Test first array\n    val array0 = columnVector.getArray(0)\n    val struct0 = array0.getElements\n    assert(struct0.getSize == 2)\n\n    val nameVector0 = struct0.getChild(0)\n    val valueVector0 = struct0.getChild(1)\n    assert(nameVector0.getString(0) == \"a1\")\n    assert(valueVector0.getInt(0) == 1)\n    assert(nameVector0.getString(1) == \"a2\")\n    assert(valueVector0.getInt(1) == 2)\n\n    // Test second array\n    val array1 = columnVector.getArray(1)\n    val struct1 = array1.getElements\n    assert(struct1.getSize == 2)\n\n    val nameVector1 = struct1.getChild(0)\n    val valueVector1 = struct1.getChild(1)\n    assert(nameVector1.getString(0) == \"b1\")\n    assert(valueVector1.getInt(0) == 3)\n    assert(nameVector1.getString(1) == \"b2\")\n    assert(valueVector1.getInt(1) == 4)\n\n    // Test third array\n    val array2 = columnVector.getArray(2)\n    val struct2 = array2.getElements\n    assert(struct2.getSize == 2)\n\n    val nameVector2 = struct2.getChild(0)\n    val valueVector2 = struct2.getChild(1)\n    assert(nameVector2.getString(0) == \"c1\")\n    assert(valueVector2.getInt(0) == 5)\n    assert(nameVector2.getString(1) == \"c2\")\n    assert(valueVector2.getInt(1) == 6)\n\n    // Test null value\n    assert(columnVector.isNullAt(3))\n  }\n\n  test(s\"handle array of map correctly\") {\n    val mapType = new MapType(StringType.STRING, IntegerType.INTEGER, true)\n    val arrayType = new ArrayType(mapType, true)\n\n    val values = List[ArrayValue](\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[MapValue](\n            new MapValue {\n              override def getSize: Int = 2\n              override def getKeys: ColumnVector =\n                VectorUtils.buildColumnVector(List(\"a1\", \"a2\").asJava, StringType.STRING)\n              override def getValues: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](1, 2).asJava, IntegerType.INTEGER)\n            },\n            new MapValue {\n              override def getSize: Int = 2\n              override def getKeys: ColumnVector =\n                VectorUtils.buildColumnVector(List(\"a3\", \"a4\").asJava, StringType.STRING)\n              override def getValues: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](3, 4).asJava, IntegerType.INTEGER)\n            }).asJava,\n          mapType)\n      },\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[MapValue](\n            new MapValue {\n              override def getSize: Int = 2\n              override def getKeys: ColumnVector =\n                VectorUtils.buildColumnVector(List(\"b1\", \"b2\").asJava, StringType.STRING)\n              override def getValues: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](5, 6).asJava, IntegerType.INTEGER)\n            },\n            new MapValue {\n              override def getSize: Int = 2\n              override def getKeys: ColumnVector =\n                VectorUtils.buildColumnVector(List(\"b3\", \"b4\").asJava, StringType.STRING)\n              override def getValues: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](7, 8).asJava, IntegerType.INTEGER)\n            }).asJava,\n          mapType)\n      },\n      null)\n\n    val columnVector = VectorUtils.buildColumnVector(values.asJava, arrayType)\n\n    // Test size\n    assert(columnVector.getSize == 3)\n\n    // Test first array\n    val firstArray = columnVector.getArray(0)\n    val firstArrayMaps = firstArray.getElements\n    assert(firstArrayMaps.getSize == 2)\n\n    val firstArrayFirstMap = firstArrayMaps.getMap(0)\n    assert(firstArrayFirstMap.getKeys.getString(0) == \"a1\")\n    assert(firstArrayFirstMap.getKeys.getString(1) == \"a2\")\n    assert(firstArrayFirstMap.getValues.getInt(0) == 1)\n    assert(firstArrayFirstMap.getValues.getInt(1) == 2)\n\n    val firstArraySecondMap = firstArrayMaps.getMap(1)\n    assert(firstArraySecondMap.getKeys.getString(0) == \"a3\")\n    assert(firstArraySecondMap.getKeys.getString(1) == \"a4\")\n    assert(firstArraySecondMap.getValues.getInt(0) == 3)\n    assert(firstArraySecondMap.getValues.getInt(1) == 4)\n\n    // Test second array\n    val secondArray = columnVector.getArray(1)\n    val secondArrayMaps = secondArray.getElements\n    assert(secondArrayMaps.getSize == 2)\n\n    val secondArrayFirstMap = secondArrayMaps.getMap(0)\n    assert(secondArrayFirstMap.getKeys.getString(0) == \"b1\")\n    assert(secondArrayFirstMap.getKeys.getString(1) == \"b2\")\n    assert(secondArrayFirstMap.getValues.getInt(0) == 5)\n    assert(secondArrayFirstMap.getValues.getInt(1) == 6)\n\n    val secondArraySecondMap = secondArrayMaps.getMap(1)\n    assert(secondArraySecondMap.getKeys.getString(0) == \"b3\")\n    assert(secondArraySecondMap.getKeys.getString(1) == \"b4\")\n    assert(secondArraySecondMap.getValues.getInt(0) == 7)\n    assert(secondArraySecondMap.getValues.getInt(1) == 8)\n\n    // Test null value\n    assert(columnVector.isNullAt(2))\n  }\n\n  test(s\"handle array of array correctly\") {\n    val innerArrayType = new ArrayType(IntegerType.INTEGER, true)\n    val outerArrayType = new ArrayType(innerArrayType, true)\n\n    val values = List[ArrayValue](\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[ArrayValue](\n            new ArrayValue {\n              override def getSize: Int = 2\n              override def getElements: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](1, 2).asJava, IntegerType.INTEGER)\n            },\n            new ArrayValue {\n              override def getSize: Int = 2\n              override def getElements: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](3, 4).asJava, IntegerType.INTEGER)\n            }).asJava,\n          innerArrayType)\n      },\n      new ArrayValue {\n        override def getSize: Int = 2\n        override def getElements: ColumnVector = VectorUtils.buildColumnVector(\n          List[ArrayValue](\n            new ArrayValue {\n              override def getSize: Int = 2\n              override def getElements: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](5, 6).asJava, IntegerType.INTEGER)\n            },\n            new ArrayValue {\n              override def getSize: Int = 2\n              override def getElements: ColumnVector =\n                VectorUtils.buildColumnVector(List[IntegerJ](7, 8).asJava, IntegerType.INTEGER)\n            }).asJava,\n          innerArrayType)\n      },\n      null)\n\n    val columnVector = VectorUtils.buildColumnVector(values.asJava, outerArrayType)\n\n    // Test size\n    assert(columnVector.getSize == 3)\n\n    // Test first outer array\n    val firstOuterArray = columnVector.getArray(0)\n    val firstOuterArrayElements = firstOuterArray.getElements\n    assert(firstOuterArrayElements.getSize == 2)\n\n    val firstOuterArrayFirstInner = firstOuterArrayElements.getArray(0)\n    val firstOuterArrayFirstInnerElements = firstOuterArrayFirstInner.getElements\n    assert(firstOuterArrayFirstInnerElements.getInt(0) == 1)\n    assert(firstOuterArrayFirstInnerElements.getInt(1) == 2)\n\n    val firstOuterArraySecondInner = firstOuterArrayElements.getArray(1)\n    val firstOuterArraySecondInnerElements = firstOuterArraySecondInner.getElements\n    assert(firstOuterArraySecondInnerElements.getInt(0) == 3)\n    assert(firstOuterArraySecondInnerElements.getInt(1) == 4)\n\n    // Test second outer array\n    val secondOuterArray = columnVector.getArray(1)\n    val secondOuterArrayElements = secondOuterArray.getElements\n    assert(secondOuterArrayElements.getSize == 2)\n\n    val secondOuterArrayFirstInner = secondOuterArrayElements.getArray(0)\n    val secondOuterArrayFirstInnerElements = secondOuterArrayFirstInner.getElements\n    assert(secondOuterArrayFirstInnerElements.getInt(0) == 5)\n    assert(secondOuterArrayFirstInnerElements.getInt(1) == 6)\n\n    val secondOuterArraySecondInner = secondOuterArrayElements.getArray(1)\n    val secondOuterArraySecondInnerElements = secondOuterArraySecondInner.getElements\n    assert(secondOuterArraySecondInnerElements.getInt(0) == 7)\n    assert(secondOuterArraySecondInnerElements.getInt(1) == 8)\n\n    // Test null value\n    assert(columnVector.isNullAt(2))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/ActionUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.test\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ArrayValue, ColumnVector, MapValue}\nimport io.delta.kernel.internal.actions.{CommitInfo, Format, Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.types.{IntegerType, StructType}\n\ntrait ActionUtils extends VectorTestUtils {\n  val protocolWithCatalogManagedSupport: Protocol =\n    new Protocol(\n      TableFeatures.TABLE_FEATURES_MIN_READER_VERSION,\n      TableFeatures.TABLE_FEATURES_MIN_WRITER_VERSION,\n      Set(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.featureName()).asJava,\n      Set(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.featureName(),\n        TableFeatures.IN_COMMIT_TIMESTAMP_W_FEATURE.featureName()).asJava)\n\n  val basicPartitionedMetadata = testMetadata(\n    schema = new StructType()\n      .add(\"part1\", IntegerType.INTEGER).add(\"col1\", IntegerType.INTEGER),\n    partitionCols = Seq(\"part1\"))\n\n  def testCommitInfo(ictEnabled: Boolean = true): CommitInfo = {\n    new CommitInfo(\n      if (ictEnabled) Optional.of(1L) else Optional.empty(), // ICT\n      1L, // timestamp\n      Optional.of(\"engineInfo\"),\n      Optional.of(\"operation\"),\n      Collections.emptyMap(), // operationParameters\n      Optional.of(false), // isBlindAppend\n      Optional.of(\"txnId\"),\n      Collections.emptyMap() // operationMetrics\n    )\n  }\n\n  def testMetadata(\n      schema: StructType,\n      partitionCols: Seq[String] = Seq.empty,\n      tblProps: Map[String, String] = Map.empty): Metadata = {\n    new Metadata(\n      \"id\",\n      Optional.of(\"name\"),\n      Optional.of(\"description\"),\n      new Format(\"parquet\", Collections.emptyMap()),\n      schema.toJson,\n      schema,\n      new ArrayValue() { // partitionColumns\n        override def getSize: Int = partitionCols.size\n        override def getElements: ColumnVector = stringVector(partitionCols)\n      },\n      Optional.empty(),\n      new MapValue() { // conf\n        override def getSize: Int = tblProps.size\n        override def getKeys: ColumnVector = stringVector(tblProps.toSeq.map(_._1))\n        override def getValues: ColumnVector = stringVector(tblProps.toSeq.map(_._2))\n      })\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/MockEngineUtils.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.test\n\nimport java.io.ByteArrayInputStream\nimport java.util\nimport java.util.Optional\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, FilteredColumnarBatch, Row}\nimport io.delta.kernel.engine._\nimport io.delta.kernel.expressions.{Column, Expression, ExpressionEvaluator, Predicate, PredicateEvaluator}\nimport io.delta.kernel.internal.actions.CommitInfo\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.{FileNames, Utils}\nimport io.delta.kernel.types.{DataType, StructType}\nimport io.delta.kernel.utils.{CloseableIterator, DataFileStatus, FileStatus}\n\n/**\n * Contains broiler plate code for mocking [[Engine]] and its sub-interfaces.\n *\n * A concrete class is created for each sub-interface (e.g. [[FileSystemClient]]) with\n * default implementation (unsupported). Test suites can override a specific API(s)\n * in the sub-interfaces to mock the behavior as desired.\n *\n * Example:\n * {{{\n *   val myMockFileSystemClient = new BaseMockFileSystemClient() {\n *     override def listFrom(filePath: String): CloseableIterator[FileStatus] = {\n *        .. my mock code to return specific values for given file path ...\n *     }\n *   }\n *\n *   val myMockEngine = mockEngine(fileSystemClient = myMockFileSystemClient)\n * }}}\n */\ntrait MockEngineUtils {\n\n  /**\n   * Create a mock Engine with the given components. If a component is not provided, it will\n   * throw an exception when accessed.\n   */\n  def mockEngine(\n      fileSystemClient: FileSystemClient = null,\n      jsonHandler: JsonHandler = null,\n      parquetHandler: ParquetHandler = null,\n      expressionHandler: ExpressionHandler = null): Engine = {\n    new Engine() {\n      override def getExpressionHandler: ExpressionHandler =\n        Option(expressionHandler).getOrElse(\n          throw new UnsupportedOperationException(\"not supported in this test suite\"))\n\n      override def getJsonHandler: JsonHandler =\n        Option(jsonHandler).getOrElse(\n          throw new UnsupportedOperationException(\"not supported in this test suite\"))\n\n      override def getFileSystemClient: FileSystemClient =\n        Option(fileSystemClient).getOrElse(\n          throw new UnsupportedOperationException(\"not supported in this test suite\"))\n\n      override def getParquetHandler: ParquetHandler =\n        Option(parquetHandler).getOrElse(\n          throw new UnsupportedOperationException(\"not supported in this test suite\"))\n    }\n  }\n}\n\n/**\n * Base class for mocking [[JsonHandler]]\n */\ntrait BaseMockJsonHandler extends JsonHandler {\n  override def parseJson(\n      jsonStringVector: ColumnVector,\n      outputSchema: StructType,\n      selectionVector: Optional[ColumnVector]): ColumnarBatch =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def readJsonFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def writeJsonFileAtomically(\n      filePath: String,\n      data: CloseableIterator[Row],\n      overwrite: Boolean): Unit =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n}\n\n/**\n * Base class for mocking [[ParquetHandler]]\n */\ntrait BaseMockParquetHandler extends ParquetHandler with MockEngineUtils {\n  override def readParquetFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[FileReadResult] =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def writeParquetFiles(\n      directoryPath: String,\n      dataIter: CloseableIterator[FilteredColumnarBatch],\n      statsColumns: util.List[Column]): CloseableIterator[DataFileStatus] =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def writeParquetFileAtomically(\n      filePath: String,\n      data: CloseableIterator[FilteredColumnarBatch]): Unit =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n}\n\n/**\n * Base class for mocking [[ExpressionHandler]]\n */\ntrait BaseMockExpressionHandler extends ExpressionHandler {\n  override def getPredicateEvaluator(\n      inputSchema: StructType,\n      predicate: Predicate): PredicateEvaluator =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def getEvaluator(\n      inputSchema: StructType,\n      expression: Expression,\n      outputType: DataType): ExpressionEvaluator =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def createSelectionVector(values: Array[Boolean], from: Int, to: Int): ColumnVector =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n}\n\n/**\n * Base class for [[FileSystemClient]]\n */\ntrait BaseMockFileSystemClient extends FileSystemClient {\n  override def listFrom(filePath: String): CloseableIterator[FileStatus] =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def resolvePath(path: String): String =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def readFiles(\n      readRequests: CloseableIterator[FileReadRequest]): CloseableIterator[ByteArrayInputStream] =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def mkdirs(path: String): Boolean =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def delete(path: String): Boolean =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def getFileStatus(path: String): FileStatus =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n\n  override def copyFileAtomically(srcPath: String, destPath: String, overwrite: Boolean): Unit =\n    throw new UnsupportedOperationException(\"not supported in this test suite\")\n}\n\n/**\n * A mock [[JsonHandler]] that reads a single file and returns a single [[ColumnarBatch]].\n * The columnar batch only contains the [[CommitInfo]] action with the `inCommitTimestamp`\n * column set to the value in the mapping.\n *\n * @param deltaVersionToICTMapping A mapping from delta version to inCommitTimestamp.\n */\nclass MockReadICTFileJsonHandler(\n    deltaVersionToICTMapping: Map[Long, Long],\n    add10ForStagedFiles: Boolean = false)\n    extends BaseMockJsonHandler with VectorTestUtils {\n  override def readJsonFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n    assert(fileIter.hasNext)\n    val filePathStr = fileIter.next.getPath\n    assert(FileNames.isCommitFile(filePathStr))\n    val deltaVersion = FileNames.getFileVersion(new Path(filePathStr))\n    assert(deltaVersionToICTMapping.contains(deltaVersion))\n\n    var ict = deltaVersionToICTMapping(deltaVersion)\n    // This enables us to have different ICT times for staged vs published delta files, which lets\n    // us test that we use the correct file when both exist for a specific version\n    if (add10ForStagedFiles && FileNames.isStagedDeltaFile(filePathStr)) {\n      ict += 10\n    }\n    val schema = new StructType().add(\"commitInfo\", CommitInfo.FULL_SCHEMA);\n    Utils.singletonCloseableIterator(\n      new ColumnarBatch {\n        override def getSchema: StructType = schema\n\n        override def getColumnVector(ordinal: Int): ColumnVector = {\n          val struct = Seq(\n            longVector(Seq(ict)), /* inCommitTimestamp */\n            longVector(Seq(-1L)), /* timestamp */\n            stringVector(Seq(\"engine\")), /* engineInfo */\n            stringVector(Seq(\"operation\")), /* operation */\n            mapTypeVector(Seq(Map(\"operationParameter\" -> \"\"))), /* operationParameters */\n            booleanVector(Seq(false)), /* isBlindAppend */\n            stringVector(Seq(\"txnId\")), /* txnId */\n            mapTypeVector(Seq(Map(\"operationMetrics\" -> \"\"))) /* operationMetrics */\n          )\n          ordinal match {\n            case 0 => new ColumnVector {\n                override def getDataType: DataType = schema\n\n                override def getSize: Int = struct.head.getSize\n\n                override def close(): Unit = {}\n\n                override def isNullAt(rowId: Int): Boolean = false\n\n                override def getChild(ordinal: Int): ColumnVector = struct(ordinal)\n              }\n          }\n        }\n        override def getSize: Int = 1\n      })\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/MockFileSystemClientUtils.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.test\n\nimport java.util.{Optional, UUID}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.engine._\nimport io.delta.kernel.internal.MockReadLastCheckpointFileJsonHandler\nimport io.delta.kernel.internal.files.ParsedLogData\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nobject MockFileSystemClientUtils extends MockFileSystemClientUtils\n\n/**\n * This is an extension to [[BaseMockFileSystemClient]] containing specific mock implementations\n * [[FileSystemClient]] which are shared across multiple test suite.\n *\n * [[MockListFromFileSystemClient]] - mocks the `listFrom` API within [[FileSystemClient]].\n */\ntrait MockFileSystemClientUtils extends MockEngineUtils {\n\n  val dataPath = new Path(\"/fake/path/to/table/\")\n  val logPath = new Path(dataPath, \"_delta_log\")\n\n  def parsedRatifiedStagedCommit(version: Long): ParsedLogData = {\n    ParsedLogData.forFileStatus(stagedCommitFile(version))\n  }\n\n  def parsedRatifiedStagedCommits(versions: Seq[Long]): Seq[ParsedLogData] = {\n    versions.map(parsedRatifiedStagedCommit)\n  }\n\n  /** Staged commit file status where the timestamp = 10*version */\n  def stagedCommitFile(v: Long): FileStatus =\n    FileStatus.of(FileNames.stagedCommitFile(logPath, v), v, v * 10)\n\n  /** Delta file status where the timestamp = 10*version */\n  def deltaFileStatus(v: Long, path: Path = logPath): FileStatus =\n    FileStatus.of(FileNames.deltaFile(path, v), v, v * 10)\n\n  /** Compaction file status where the timestamp = 10*startVersion */\n  def logCompactionStatus(s: Long, e: Long, path: Path = logPath): FileStatus =\n    FileStatus.of(FileNames.logCompactionPath(path, s, e).toString, s, s * 10)\n\n  /** Delta file statuses where the timestamp = 10*version */\n  def deltaFileStatuses(deltaVersions: Seq[Long], path: Path = logPath): Seq[FileStatus] = {\n    assert(deltaVersions.size == deltaVersions.toSet.size)\n    deltaVersions.map(v => deltaFileStatus(v, path))\n  }\n\n  /** Compaction file statuses where the timestamp = 10*startVersion */\n  def compactedFileStatuses(\n      compactedVersions: Seq[(Long, Long)],\n      path: Path = logPath): Seq[FileStatus] = {\n    compactedVersions.map { case (s, e) =>\n      logCompactionStatus(s, e, path)\n    }\n  }\n\n  /** Checksum file status for given a version */\n  def checksumFileStatus(deltaVersion: Long): FileStatus = {\n    FileStatus.of(FileNames.checksumFile(logPath, deltaVersion).toString, 10, 10)\n  }\n\n  /** Classic checkpoint file status where the timestamp = 10*version */\n  def classicCheckpointFileStatus(v: Long): FileStatus = {\n    FileStatus.of(FileNames.checkpointFileSingular(logPath, v).toString, v, v * 10)\n  }\n\n  /** Checkpoint file statuses where the timestamp = 10*version */\n  def singularCheckpointFileStatuses(\n      checkpointVersions: Seq[Long],\n      path: Path = logPath): Seq[FileStatus] = {\n    assert(checkpointVersions.size == checkpointVersions.toSet.size)\n    checkpointVersions.map(v =>\n      FileStatus.of(FileNames.checkpointFileSingular(path, v).toString, v, v * 10))\n  }\n\n  /** Multi-part checkpoint file status where the timestamp = 10*version */\n  def multiPartCheckpointFileStatus(version: Long, part: Integer, numParts: Integer): FileStatus = {\n    val path = FileNames.multiPartCheckpointFile(logPath, version, part, numParts)\n    FileStatus.of(path.toString, version, version * 10)\n  }\n\n  /** Checkpoint file statuses where the timestamp = 10*version */\n  def multiCheckpointFileStatuses(\n      checkpointVersions: Seq[Long],\n      numParts: Int): Seq[FileStatus] = {\n    assert(checkpointVersions.size == checkpointVersions.toSet.size)\n    checkpointVersions.flatMap(v =>\n      FileNames.checkpointFileWithParts(logPath, v, numParts).asScala\n        .map(p => FileStatus.of(p.toString, v, v * 10)))\n  }\n\n  /** Checkpoint file status for a top-level V2 checkpoint file. */\n  def v2CheckpointFileStatus(\n      version: Long,\n      useUUID: Boolean = true,\n      fileType: String = \"json\"): FileStatus = {\n    val path = if (useUUID) {\n      val uuid = UUID.randomUUID().toString\n      FileNames.topLevelV2CheckpointFile(logPath, version, uuid, fileType).toString\n    } else {\n      FileNames.checkpointFileSingular(logPath, version).toString\n    }\n    FileStatus.of(path, version, version * 10)\n  }\n\n  /**\n   * Checkpoint file status for a top-level V2 checkpoint file.\n   *\n   * @param checkpointVersions List of checkpoint versions, given as Seq(version, whether to use\n   *                           UUID naming scheme, number of sidecars).\n   * Returns top-level checkpoint file and sidecar files for each checkpoint version.\n   */\n  def v2CheckpointFileStatuses(\n      checkpointVersions: Seq[(Long, Boolean, Int)],\n      fileType: String): Seq[(FileStatus, Seq[FileStatus])] = {\n    checkpointVersions.map { case (v, useUUID, numSidecars) =>\n      val topLevelFile = v2CheckpointFileStatus(v, useUUID, fileType)\n      val sidecars = (0 until numSidecars).map { _ =>\n        FileStatus.of(\n          FileNames.v2CheckpointSidecarFile(logPath, UUID.randomUUID().toString).toString,\n          v,\n          v * 10)\n      }\n      (topLevelFile, sidecars)\n    }\n  }\n\n  /* Create input function for createMockEngine to implement listFrom from a list of\n   * file statuses.\n   */\n  def listFromProvider(files: Seq[FileStatus])(filePath: String): Seq[FileStatus] = {\n    val parentPath = new Path(filePath).getParent\n    files\n      // This currently excludes listing nested directories, we can fix this if needed\n      .filter(fs => new Path(fs.getPath).getParent == parentPath)\n      .filter(_.getPath.compareTo(filePath) >= 0)\n      .sortBy(_.getPath)\n  }\n\n  /**\n   * Create a mock [[Engine]] to mock the [[FileSystemClient.listFrom]] calls using\n   * the given contents. The contents are filtered depending upon the list from path prefix.\n   */\n  def createMockFSListFromEngine(\n      contents: Seq[FileStatus],\n      parquetHandler: ParquetHandler,\n      jsonHandler: JsonHandler): Engine = {\n    mockEngine(\n      fileSystemClient =\n        new MockListFromFileSystemClient(listFromProvider(contents)),\n      parquetHandler = parquetHandler,\n      jsonHandler = jsonHandler)\n  }\n\n  def createMockFSAndJsonEngineForLastCheckpoint(\n      contents: Seq[FileStatus],\n      lastCheckpointVersion: Optional[java.lang.Long]): Engine = {\n    mockEngine(\n      fileSystemClient = new MockListFromFileSystemClient(listFromProvider(contents)),\n      jsonHandler = if (lastCheckpointVersion.isPresent) {\n        new MockReadLastCheckpointFileJsonHandler(\n          s\"$logPath/_last_checkpoint\",\n          lastCheckpointVersion.get())\n      } else {\n        null\n      })\n  }\n\n  /**\n   * Create a mock [[Engine]] to mock the [[FileSystemClient.listFrom]] calls using\n   * the given list of delta file statuses. When read, each file status will return\n   * a single `commitInfo` action with the an inCommitTimestamp set as per\n   * `deltaToICTMap`.\n   */\n  def createMockFSAndJsonEngineForICT(\n      contents: Seq[FileStatus],\n      deltaToICTMap: Map[Long, Long]): Engine = {\n    mockEngine(\n      fileSystemClient = new MockListFromFileSystemClient(listFromProvider(contents)),\n      jsonHandler = new MockReadICTFileJsonHandler(deltaToICTMap))\n  }\n\n  /**\n   * Create a mock [[Engine]] to mock the [[FileSystemClient.listFrom]] calls using\n   * the given contents. The contents are filtered depending upon the list from path prefix.\n   */\n  def createMockFSListFromEngine(contents: Seq[FileStatus]): Engine = {\n    mockEngine(fileSystemClient =\n      new MockListFromFileSystemClient(listFromProvider(contents)))\n  }\n\n  /**\n   * Create a mock [[Engine]] to mock the [[FileSystemClient.listFrom]] calls using\n   * [[MockListFromFileSystemClient]].\n   */\n  def createMockFSListFromEngine(listFromProvider: String => Seq[FileStatus]): Engine = {\n    mockEngine(fileSystemClient = new MockListFromFileSystemClient(listFromProvider))\n  }\n}\n\n/**\n * A mock [[FileSystemClient]] that answers `listFrom` calls from a given content provider.\n *\n * It also maintains metrics on number of times `listFrom` is called and arguments for each call.\n */\nclass MockListFromFileSystemClient(listFromProvider: String => Seq[FileStatus])\n    extends BaseMockFileSystemClient {\n  private var listFromCalls: Seq[String] = Seq.empty\n\n  override def listFrom(filePath: String): CloseableIterator[FileStatus] = {\n    listFromCalls = listFromCalls :+ filePath\n    toCloseableIterator(listFromProvider(filePath).iterator.asJava)\n  }\n\n  override def resolvePath(path: String): String = path\n\n  def getListFromCalls: Seq[String] = listFromCalls\n}\n\n/**\n * A mock [[FileSystemClient]] that answers `listFrom` calls from a given content provider and\n * implements the identity function for `resolvePath` calls.\n *\n * It also maintains metrics on number of times `listFrom` is called and arguments for each call.\n */\nclass MockListFromResolvePathFileSystemClient(listFromProvider: String => Seq[FileStatus])\n    extends BaseMockFileSystemClient {\n  private var listFromCalls: Seq[String] = Seq.empty\n\n  override def listFrom(filePath: String): CloseableIterator[FileStatus] = {\n    listFromCalls = listFromCalls :+ filePath\n    toCloseableIterator(listFromProvider(filePath).iterator.asJava)\n  }\n\n  override def resolvePath(path: String): String = path\n\n  def getListFromCalls: Seq[String] = listFromCalls\n}\n\n/**\n * A mock [[FileSystemClient]] that answers `listFrom` call from the given list of file statuses\n * and tracks the delete calls.\n * @param listContents List of file statuses to be returned by `listFrom` call.\n */\nclass MockListFromDeleteFileSystemClient(listContents: Seq[FileStatus])\n    extends BaseMockFileSystemClient {\n  private val listOfFiles: Seq[String] = listContents.map(_.getPath).toSeq\n  private var isListFromAlreadyCalled = false\n  private var deleteCalls: Seq[String] = Seq.empty\n\n  override def listFrom(filePath: String): CloseableIterator[FileStatus] = {\n    assert(!isListFromAlreadyCalled, \"listFrom should be called only once\")\n    isListFromAlreadyCalled = true\n    toCloseableIterator(listContents.sortBy(_.getPath).asJava.iterator())\n  }\n\n  override def delete(path: String): Boolean = {\n    deleteCalls = deleteCalls :+ path\n    listOfFiles.contains(path)\n  }\n\n  def getDeleteCalls: Seq[String] = deleteCalls\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/MockSnapshotUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.test\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.TransactionSuite.testSchema\nimport io.delta.kernel.internal.{SnapshotImpl, TableConfig}\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.lang.Lazy\nimport io.delta.kernel.internal.metrics.SnapshotQueryContext\nimport io.delta.kernel.internal.snapshot.LogSegment\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.internal.util.VectorUtils.{buildArrayValue, stringStringMapValue}\nimport io.delta.kernel.types.StringType\nimport io.delta.kernel.utils.FileStatus\n\nobject MockSnapshotUtils extends MockSnapshotUtils\n\ntrait MockSnapshotUtils {\n\n  /**\n   * Creates a mock snapshot with valid metadata at the given version.\n   * @param ictEnablementInfoOpt Controls the enablement state of in-commit timestamps.\n   */\n  def getMockSnapshot(\n      dataPath: Path,\n      latestVersion: Long,\n      ictEnablementInfoOpt: Option[(Long, Long)] = None,\n      timestamp: Long = 0L,\n      deltaFileAtEndVersion: Option[FileStatus] = None): SnapshotImpl = {\n    val configuration = ictEnablementInfoOpt match {\n      case Some((version, _)) if version == 0L =>\n        Map(TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\")\n      case Some((version, ts)) =>\n        Map(\n          TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\",\n          TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey -> version.toString,\n          TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey -> ts.toString)\n      case None =>\n        Map[String, String]()\n    }\n    val metadata = new Metadata(\n      \"id\",\n      Optional.empty(), /* name */\n      Optional.empty(), /* description */\n      new Format(),\n      testSchema.toJson,\n      testSchema,\n      buildArrayValue(java.util.Arrays.asList(\"c3\"), StringType.STRING),\n      Optional.of(123),\n      stringStringMapValue(configuration.asJava));\n    val logPath = new Path(dataPath, \"_delta_log\")\n\n    val fs = deltaFileAtEndVersion.getOrElse(FileStatus.of(\n      FileNames.deltaFile(logPath, latestVersion),\n      1, /* size */\n      1 /* modificationTime */ ))\n    val logSegment = new LogSegment(\n      logPath, /* logPath */\n      latestVersion,\n      Seq(fs).asJava, /* deltas */\n      Seq.empty.asJava, /* compactions */\n      Seq.empty.asJava, /* checkpoints */\n      fs, /* deltaAtEndVersion */\n      Optional.empty(), /* lastSeenChecksum */\n      Optional.empty() /* maxPublishedDeltaVersion */\n    )\n    val snapshotQueryContext = SnapshotQueryContext.forLatestSnapshot(dataPath.toString)\n    new SnapshotImpl(\n      dataPath, /* dataPath */\n      logSegment.getVersion, /* version */\n      new Lazy(() => logSegment), /* logSegment */\n      null, /* logReplay */\n      new Protocol(1, 2), /* protocol */\n      metadata,\n      DefaultFileSystemManagedTableOnlyCommitter.INSTANCE,\n      snapshotQueryContext, /* snapshotContext */\n      Optional.empty() /* inCommitTimestampOpt */\n    )\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/TestFixtures.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.test\n\nimport java.lang.{Long => JLong}\nimport java.util.{Collections, Map => JMap, Optional}\nimport java.util.function.Supplier\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.SnapshotBuilder\nimport io.delta.kernel.commit.CommitMetadata\nimport io.delta.kernel.internal.actions.{CommitInfo, DomainMetadata, Metadata, Protocol}\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData\nimport io.delta.kernel.internal.util.{FileNames, Tuple2}\nimport io.delta.kernel.types.{ArrayType, BinaryType, BooleanType, ByteType, DataType, DateType, DecimalType, DoubleType, FloatType, IntegerType, LongType, MapType, ShortType, StringType, StructType, TimestampNTZType, TimestampType}\nimport io.delta.kernel.utils.FileStatus\n\n/**\n * Test fixtures including factory methods and constants for creating test objects with sensible\n * defaults.\n */\ntrait TestFixtures extends ActionUtils {\n\n  /** All simple data type used in parameterized tests where type is one of the test dimensions. */\n  val PRIMITIVE_TYPES = Set(\n    BooleanType.BOOLEAN,\n    ByteType.BYTE,\n    ShortType.SHORT,\n    IntegerType.INTEGER,\n    LongType.LONG,\n    FloatType.FLOAT,\n    DoubleType.DOUBLE,\n    DateType.DATE,\n    TimestampType.TIMESTAMP,\n    TimestampNTZType.TIMESTAMP_NTZ,\n    StringType.STRING,\n    BinaryType.BINARY,\n    new DecimalType(10, 5))\n\n  val NESTED_TYPES: Set[DataType] = Set(\n    new ArrayType(BooleanType.BOOLEAN, true),\n    new MapType(IntegerType.INTEGER, LongType.LONG, true),\n    new StructType().add(\"s1\", BooleanType.BOOLEAN).add(\"s2\", IntegerType.INTEGER))\n\n  /** All types. Used in parameterized tests where type is one of the test dimensions. */\n  val ALL_TYPES: Set[DataType] = PRIMITIVE_TYPES ++ NESTED_TYPES\n\n  def createCommitMetadata(\n      version: Long,\n      logPath: String = \"/fake/_delta_log\",\n      commitInfo: CommitInfo = testCommitInfo(),\n      commitDomainMetadatas: List[DomainMetadata] = List.empty,\n      committerProperties: Supplier[JMap[String, String]] = () => Collections.emptyMap(),\n      readPandMOpt: Optional[Tuple2[Protocol, Metadata]] = Optional.empty(),\n      newProtocolOpt: Optional[Protocol] = Optional.empty(),\n      newMetadataOpt: Optional[Metadata] = Optional.empty(),\n      maxKnownPublishedDeltaVersion: Optional[JLong] = Optional.empty()): CommitMetadata = {\n    new CommitMetadata(\n      version,\n      logPath,\n      commitInfo,\n      commitDomainMetadatas.asJava,\n      committerProperties,\n      readPandMOpt,\n      newProtocolOpt,\n      newMetadataOpt,\n      maxKnownPublishedDeltaVersion)\n  }\n\n  def createStagedCatalogCommit(\n      v: Long,\n      logPath: String = \"/fake/_delta_log\"): ParsedCatalogCommitData = {\n    val fileStatus = FileStatus.of(FileNames.stagedCommitFile(logPath, v), v, v * 10)\n    ParsedCatalogCommitData.forFileStatus(fileStatus)\n  }\n\n  implicit class SnapshotBuilderCatalogVersionOps[T <: SnapshotBuilder](snapshotBuilder: T) {\n    def withMaxCatalogVersionIfApplicable(\n        isCatalogManaged: Boolean,\n        maxCatalogVersion: Long): T = {\n      if (isCatalogManaged) {\n        snapshotBuilder\n          .withMaxCatalogVersion(maxCatalogVersion)\n          .asInstanceOf[T]\n      } else {\n        snapshotBuilder\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/TestUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.test\n\nimport java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}\nimport java.util.Optional\n\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.skipping.StatsSchemaHelper.STATS_WITH_COLLATION\nimport io.delta.kernel.types.CollationIdentifier\n\n/** Utility functions for tests. */\ntrait TestUtils {\n  def col(name: String): Column = new Column(name)\n\n  def nestedCol(name: String): Column = {\n    new Column(name.split(\"\\\\.\"))\n  }\n\n  def collatedStatsCol(\n      collation: CollationIdentifier,\n      statName: String,\n      fieldName: String): Column = {\n    val columnPath =\n      Array(STATS_WITH_COLLATION, collation.toString, statName) ++ fieldName.split('.')\n    new Column(columnPath)\n  }\n\n  def literal(value: Any): Literal = {\n    value match {\n      case v: String => Literal.ofString(v)\n      case v: Int => Literal.ofInt(v)\n      case v: Long => Literal.ofLong(v)\n      case v: Float => Literal.ofFloat(v)\n      case v: Double => Literal.ofDouble(v)\n      case v: Boolean => Literal.ofBoolean(v)\n      case _ => throw new IllegalArgumentException(s\"Unsupported literal type: ${value}\")\n    }\n  }\n\n  implicit class ScalaOptionOps[T](option: Option[T]) {\n    def toJava: Optional[T] = option match {\n      case Some(value) => Optional.of(value)\n      case None => Optional.empty()\n    }\n  }\n\n  implicit class JavaOptionalOps[T](optional: Optional[T]) {\n    def toScala: Option[T] =\n      if (optional.isPresent) Some(optional.get()) else None\n  }\n\n  /**\n   * Helper to test Java serialization by performing a round-trip serialize/deserialize.\n   *\n   * @param obj The object to serialize (must be Serializable)\n   * @return The deserialized object\n   */\n  def roundTripSerialize[T](obj: T): T = {\n    val baos = new ByteArrayOutputStream()\n    val oos = new ObjectOutputStream(baos)\n    try {\n      oos.writeObject(obj)\n      oos.flush()\n    } finally {\n      oos.close()\n    }\n\n    val bais = new ByteArrayInputStream(baos.toByteArray)\n    val ois = new ObjectInputStream(bais)\n    try {\n      ois.readObject().asInstanceOf[T]\n    } finally {\n      ois.close()\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/test/VectorTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.test\n\nimport java.lang.{Boolean => BooleanJ, Double => DoubleJ, Float => FloatJ, Integer => IntegerJ, Long => LongJ}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, MapValue, Row}\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.types._\nimport io.delta.kernel.utils.CloseableIterator\n\ntrait VectorTestUtils {\n\n  protected def emptyActionsIterator = new CloseableIterator[Row] {\n    override def hasNext: Boolean = false\n    override def next(): Row = throw new NoSuchElementException(\"No more elements\")\n    override def close(): Unit = {}\n  }\n\n  protected def emptyColumnarBatch = new ColumnarBatch {\n    override def getSchema: StructType = null\n    override def getColumnVector(ordinal: Int): ColumnVector = null\n    override def getSize: Int = 0\n  }\n\n  protected def booleanVector(values: Seq[BooleanJ]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = BooleanType.BOOLEAN\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getBoolean(rowId: Int): Boolean = values(rowId)\n    }\n  }\n\n  protected def timestampVector(values: Seq[LongJ]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = TimestampType.TIMESTAMP\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null || values(rowId) == -1\n\n      // Values are stored as Longs representing milliseconds since epoch\n      override def getLong(rowId: Int): Long = values(rowId)\n    }\n  }\n\n  protected def stringVector(values: Seq[String]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = StringType.STRING\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getString(rowId: Int): String = values(rowId)\n    }\n  }\n\n  protected def mapTypeVector(values: Seq[Map[String, String]]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = new MapType(StringType.STRING, StringType.STRING, true)\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getMap(rowId: Int): MapValue =\n        VectorUtils.stringStringMapValue(values(rowId).asJava)\n    }\n  }\n\n  protected def byteVector(values: Seq[java.lang.Byte]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = ByteType.BYTE\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getByte(rowId: Int): Byte = values(rowId)\n    }\n  }\n\n  protected def intVector(values: Seq[IntegerJ]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = IntegerType.INTEGER\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getInt(rowId: Int): Int = values(rowId)\n    }\n  }\n\n  protected def floatVector(values: Seq[FloatJ]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = FloatType.FLOAT\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getFloat(rowId: Int): Float = values(rowId)\n    }\n  }\n\n  protected def doubleVector(values: Seq[DoubleJ]): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = DoubleType.DOUBLE\n\n      override def getSize: Int = values.length\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n      override def getDouble(rowId: Int): Double = values(rowId)\n    }\n  }\n\n  def longVector(values: Seq[LongJ]): ColumnVector = new ColumnVector {\n    override def getDataType: DataType = LongType.LONG\n\n    override def getSize: Int = values.length\n\n    override def close(): Unit = {}\n\n    override def isNullAt(rowId: Int): Boolean = values(rowId) == null\n\n    override def getLong(rowId: Int): Long = values(rowId)\n  }\n\n  def selectSingleElement(size: Int, selectRowId: Int): ColumnVector = new ColumnVector {\n    override def getDataType: DataType = BooleanType.BOOLEAN\n\n    override def getSize: Int = size\n\n    override def close(): Unit = {}\n\n    override def isNullAt(rowId: Int): Boolean = false\n\n    override def getBoolean(rowId: Int): Boolean = rowId == selectRowId\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/transaction/DataLayoutSpecSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.transaction\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions.Column\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for [[DataLayoutSpec]].\n */\nclass DataLayoutSpecSuite extends AnyFunSuite {\n\n  // Helper methods for creating test columns\n  private def cols(names: String*): java.util.List[Column] =\n    names.map(new Column(_)).asJava\n\n  test(\"noDataLayout creates spec with no special layout\") {\n    val spec = DataLayoutSpec.noDataLayout()\n\n    assert(spec.hasNoDataLayoutSpec())\n    assert(!spec.hasPartitioning())\n    assert(!spec.hasClustering())\n  }\n\n  test(\"partitioned creates spec with partition columns\") {\n    val partitionCols = cols(\"year\", \"month\", \"day\")\n    val spec = DataLayoutSpec.partitioned(partitionCols)\n\n    assert(!spec.hasNoDataLayoutSpec())\n    assert(spec.hasPartitioning())\n    assert(!spec.hasClustering())\n    assert(spec.getPartitionColumns() == partitionCols)\n    assert(spec.getPartitionColumnsAsStrings().asScala == Seq(\"year\", \"month\", \"day\"))\n  }\n\n  test(\"partitioned throws exception for null columns\") {\n    val exception = intercept[IllegalArgumentException] {\n      DataLayoutSpec.partitioned(null)\n    }\n    assert(exception.getMessage.contains(\"Partition columns cannot be null or empty\"))\n  }\n\n  test(\"partitioned throws exception for empty columns\") {\n    val exception = intercept[IllegalArgumentException] {\n      DataLayoutSpec.partitioned(List.empty[Column].asJava)\n    }\n    assert(exception.getMessage.contains(\"Partition columns cannot be null or empty\"))\n  }\n\n  test(\"partitioned throws exception for nested columns\") {\n    val nestedCol = new Column(Array(\"struct_col\", \"nested_field\"))\n    val exception = intercept[IllegalArgumentException] {\n      DataLayoutSpec.partitioned(List(nestedCol).asJava)\n    }\n    assert(exception.getMessage.contains(\"Partition columns must be only top-level columns\"))\n  }\n\n  test(\"clustered creates spec with clustering columns\") {\n    val clusteringCols = cols(\"user_id\", \"timestamp\")\n    val spec = DataLayoutSpec.clustered(clusteringCols)\n\n    assert(!spec.hasNoDataLayoutSpec())\n    assert(!spec.hasPartitioning())\n    assert(spec.hasClustering())\n    assert(spec.getClusteringColumns() == clusteringCols)\n  }\n\n  test(\"clustered with empty columns list\") {\n    val spec = DataLayoutSpec.clustered(List.empty[Column].asJava)\n\n    assert(!spec.hasNoDataLayoutSpec())\n    assert(!spec.hasPartitioning())\n    assert(spec.hasClustering())\n    assert(spec.getClusteringColumns().isEmpty)\n  }\n\n  test(\"clustered throws exception for null columns\") {\n    val exception = intercept[IllegalArgumentException] {\n      DataLayoutSpec.clustered(null)\n    }\n    assert(exception.getMessage.contains(\"Clustering columns cannot be null (but can be empty)\"))\n  }\n\n  test(\"getPartitionColumns throws exception when partitioning not enabled\") {\n    val spec = DataLayoutSpec.noDataLayout()\n    val exception = intercept[IllegalStateException] {\n      spec.getPartitionColumns()\n    }\n    assert(exception.getMessage.contains(\n      \"Cannot get partition columns: partitioning is not enabled on this layout\"))\n  }\n\n  test(\"getPartitionColumnsAsStrings throws exception when partitioning not enabled\") {\n    val spec = DataLayoutSpec.noDataLayout()\n    val exception = intercept[IllegalStateException] {\n      spec.getPartitionColumnsAsStrings()\n    }\n    assert(exception.getMessage.contains(\n      \"Cannot get partition columns: partitioning is not enabled on this layout\"))\n  }\n\n  test(\"getClusteringColumns throws exception when clustering not enabled\") {\n    val spec = DataLayoutSpec.noDataLayout()\n    val exception = intercept[IllegalStateException] {\n      spec.getClusteringColumns()\n    }\n    assert(exception.getMessage.contains(\n      \"Cannot get clustering columns: clustering is not enabled on this layout\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/CollationIdentifierSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types\n\nimport java.util.Optional\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CollationIdentifierSuite extends AnyFunSuite {\n  val PROVIDER_SPARK = \"SPARK\"\n  val PROVIDER_ICU = \"ICU\"\n  val DEFAULT_COLLATION_NAME = \"UTF8_BINARY\"\n  val DEFAULT_COLLATION_IDENTIFIER = CollationIdentifier.fromString(\"SPARK.UTF8_BINARY\")\n\n  test(\"check fromString with valid string\") {\n    Seq(\n      (\n        s\"$PROVIDER_SPARK.$DEFAULT_COLLATION_NAME\",\n        DEFAULT_COLLATION_IDENTIFIER),\n      (\n        s\"$PROVIDER_ICU.sr_Cyrl_SRB\",\n        CollationIdentifier.fromString(s\"$PROVIDER_ICU.sr_Cyrl_SRB\")),\n      (\n        s\"$PROVIDER_ICU.sr_Cyrl_SRB.75.1\",\n        CollationIdentifier.fromString(s\"$PROVIDER_ICU.sr_Cyrl_SRB.75.1\"))).foreach {\n      case (stringIdentifier, collationIdentifier) =>\n        assert(CollationIdentifier.fromString(stringIdentifier).equals(collationIdentifier))\n    }\n  }\n\n  test(\"check fromString with invalid string\") {\n    Seq(\n      PROVIDER_SPARK,\n      s\"${PROVIDER_SPARK}_sr_Cyrl_SRB\").foreach {\n      stringIdentifier =>\n        val e = intercept[IllegalArgumentException] {\n          val collationIdentifier = CollationIdentifier.fromString(stringIdentifier)\n        }\n        assert(e.getMessage == String.format(\"Invalid collation identifier: %s\", stringIdentifier))\n    }\n  }\n\n  test(\"check toStringWithoutVersion\") {\n    Seq(\n      (\n        DEFAULT_COLLATION_IDENTIFIER,\n        s\"$PROVIDER_SPARK.$DEFAULT_COLLATION_NAME\"),\n      (\n        CollationIdentifier.fromString(s\"$PROVIDER_ICU.sr_Cyrl_SRB\"),\n        s\"$PROVIDER_ICU.SR_CYRL_SRB\"),\n      (\n        CollationIdentifier.fromString(s\"$PROVIDER_ICU.sr_Cyrl_SRB.75.1\"),\n        s\"$PROVIDER_ICU.SR_CYRL_SRB\")).foreach {\n      case (collationIdentifier, toStringWithoutVersion) =>\n        assert(collationIdentifier.toStringWithoutVersion == toStringWithoutVersion)\n    }\n  }\n\n  test(\"check toString\") {\n    Seq(\n      (\n        DEFAULT_COLLATION_IDENTIFIER,\n        s\"$PROVIDER_SPARK.$DEFAULT_COLLATION_NAME\"),\n      (\n        CollationIdentifier.fromString(s\"$PROVIDER_ICU.sr_Cyrl_SRB\"),\n        s\"$PROVIDER_ICU.SR_CYRL_SRB\"),\n      (\n        CollationIdentifier.fromString(s\"$PROVIDER_ICU.sr_Cyrl_SRB.75.1\"),\n        s\"$PROVIDER_ICU.SR_CYRL_SRB.75.1\")).foreach {\n      case (collationIdentifier, toString) =>\n        assert(collationIdentifier.toString == toString)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/DataTypeSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DataTypeSuite extends AnyFunSuite {\n  val utf8LcaseString = new StringType(\"SPARK.UTF8_LCASE\")\n  val unicodeString = new StringType(\"ICU.UNICODE\")\n\n  test(\"isWriteCompatible\") {\n    val testCases = Seq(\n      (StringType.STRING, StringType.STRING, true),\n      (StringType.STRING, utf8LcaseString, true),\n      (IntegerType.INTEGER, StringType.STRING, false),\n      (utf8LcaseString, unicodeString, true),\n      (\n        new ArrayType(StringType.STRING, true),\n        new ArrayType(utf8LcaseString, true),\n        true),\n      (\n        new ArrayType(unicodeString, false),\n        new ArrayType(StringType.STRING, false),\n        true),\n      (\n        new ArrayType(StringType.STRING, true),\n        new ArrayType(utf8LcaseString, false),\n        false),\n      (\n        new MapType(StringType.STRING, utf8LcaseString, false),\n        new MapType(StringType.STRING, unicodeString, false),\n        true),\n      (\n        new MapType(StringType.STRING, utf8LcaseString, false),\n        new MapType(StringType.STRING, StringType.STRING, false),\n        true),\n      (\n        new MapType(StringType.STRING, IntegerType.INTEGER, false),\n        new MapType(StringType.STRING, IntegerType.INTEGER, true),\n        false),\n      (\n        new StructType()\n          .add(\"name\", StringType.STRING)\n          .add(\"age\", IntegerType.INTEGER),\n        new StructType()\n          .add(\"name\", utf8LcaseString)\n          .add(\"age\", IntegerType.INTEGER),\n        true),\n      (\n        new StructType()\n          .add(\"name\", StringType.STRING)\n          .add(\"details\", new StructType().add(\"address\", StringType.STRING)),\n        new StructType()\n          .add(\"name\", unicodeString)\n          .add(\"details\", new StructType().add(\"address\", utf8LcaseString)),\n        true),\n      (\n        new StructType()\n          .add(\"c1\", new ArrayType(unicodeString, true))\n          .add(\"c2\", new MapType(StringType.STRING, utf8LcaseString, false)),\n        new StructType()\n          .add(\"c1\", new ArrayType(StringType.STRING, true))\n          .add(\"c2\", new MapType(StringType.STRING, unicodeString, false)),\n        true),\n      (\n        new StructType()\n          .add(\"c1\", new ArrayType(unicodeString, false))\n          .add(\"c2\", new MapType(StringType.STRING, utf8LcaseString, false)),\n        new StructType()\n          .add(\"c1\", new ArrayType(StringType.STRING, true))\n          .add(\"c2\", new MapType(StringType.STRING, unicodeString, false)),\n        false),\n      (\n        new StructType()\n          .add(\"c1\", new ArrayType(IntegerType.INTEGER, true))\n          .add(\"c2\", new MapType(StringType.STRING, utf8LcaseString, false)),\n        new StructType()\n          .add(\"c1\", new ArrayType(StringType.STRING, true))\n          .add(\"c2\", new MapType(StringType.STRING, unicodeString, false)),\n        false),\n      (\n        new ArrayType(\n          new StructType().add(\"c1\", new MapType(StringType.STRING, StringType.STRING, true), true),\n          true),\n        new ArrayType(\n          new StructType().add(\"c1\", new MapType(StringType.STRING, utf8LcaseString, true), true),\n          true),\n        true),\n      (\n        new ArrayType(\n          new StructType().add(\"c1\", new MapType(StringType.STRING, StringType.STRING, true), true),\n          true),\n        new ArrayType(\n          new StructType().add(\"c2\", new MapType(StringType.STRING, unicodeString, true), true),\n          true),\n        false),\n      (\n        new ArrayType(\n          new StructType().add(\n            \"c1\",\n            new MapType(StringType.STRING, StringType.STRING, true),\n            false),\n          true),\n        new ArrayType(\n          new StructType().add(\"c1\", new MapType(StringType.STRING, utf8LcaseString, true), true),\n          true),\n        false),\n      (\n        new MapType(\n          new StructType().add(\"c1\", StringType.STRING),\n          new ArrayType(utf8LcaseString, false),\n          true),\n        new MapType(\n          new StructType().add(\"c1\", StringType.STRING),\n          new ArrayType(utf8LcaseString, false),\n          true),\n        true),\n      (\n        new MapType(\n          new StructType().add(\"c1\", StringType.STRING),\n          new ArrayType(utf8LcaseString, false),\n          false),\n        new MapType(\n          new StructType().add(\"c1\", StringType.STRING),\n          new ArrayType(utf8LcaseString, false),\n          true),\n        false),\n      (\n        new MapType(new StructType().add(\"c1\", StringType.STRING), StringType.STRING, false),\n        new MapType(new StructType().add(\"c1\", StringType.STRING), utf8LcaseString, true),\n        false))\n\n    testCases.foreach { case (dt1, dt2, expected) =>\n      assert(dt1.isWriteCompatible(dt2) == expected)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/FieldMetadataSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.exceptions.KernelException\n\nimport org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass FieldMetadataSuite extends AnyFunSuite {\n\n  test(\"retrieving non-existing key returns null\") {\n    assertThat(FieldMetadata.builder().build().get(\"non-existing\")).isNull()\n    assertThat(FieldMetadata.builder().putBoolean(\"key\", false).build()\n      .getLong(\"non-existing\")).isNull()\n  }\n\n  test(\"retrieving key with null value should never throw\") {\n    val meta = FieldMetadata.builder().putNull(\"nullKey\").build()\n    assertThat(meta.getLong(\"nullKey\")).isNull()\n    assertThat(meta.getBoolean(\"nullKey\")).isNull()\n  }\n\n  test(\"retrieving key with wrong type throws exception\") {\n    val longs: Seq[java.lang.Long] = Seq(1L, 2L, 3L)\n    val doubles: Seq[java.lang.Double] = Seq(1.0, 2.0, 3.0)\n    val booleans: Seq[java.lang.Boolean] = Seq(true, false, true)\n    val strings: Seq[java.lang.String] = Seq(\"a\", \"b\", \"c\")\n    val innerMeta = FieldMetadata.builder().putBoolean(\"key\", true).build()\n    val meta = FieldMetadata.builder()\n      .putLong(\"longKey\", 23L)\n      .putDouble(\"doubleKey\", 23.0)\n      .putBoolean(\"booleanKey\", true)\n      .putString(\"stringKey\", \"random\")\n      .putFieldMetadata(\"fieldMetadataKey\", innerMeta)\n      .putLongArray(\"longArrayKey\", longs.toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Seq(innerMeta).toArray)\n      .build\n\n    assertThatThrownBy(() => meta.getLongArray(\"longKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getDoubleArray(\"doubleKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getBooleanArray(\"booleanKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getStringArray(\"stringKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getMetadataArray(\"fieldMetadataKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getLong(\"longArrayKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getDouble(\"doubleArrayKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getBoolean(\"booleanArrayKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getString(\"stringArrayKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n\n    assertThatThrownBy(() => meta.getMetadata(\"fieldMetadataArrayKey\"))\n      .isInstanceOf(classOf[IllegalArgumentException])\n  }\n\n  test(\"retrieving key with correct type returns value\") {\n    val longs: Seq[java.lang.Long] = Seq(1L, 2L, 3L)\n    val doubles: Seq[java.lang.Double] = Seq(1.0, 2.0, 3.0)\n    val booleans: Seq[java.lang.Boolean] = Seq(true, false, true)\n    val strings: Seq[java.lang.String] = Seq(\"a\", \"b\", \"c\")\n    val innerMeta = FieldMetadata.builder().putBoolean(\"key\", true).build()\n    val meta = FieldMetadata.builder()\n      .putLong(\"longKey\", 23L)\n      .putDouble(\"doubleKey\", 23.0)\n      .putBoolean(\"booleanKey\", true)\n      .putString(\"stringKey\", \"random\")\n      .putFieldMetadata(\"fieldMetadataKey\", innerMeta)\n      .putLongArray(\"longArrayKey\", longs.toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Seq(innerMeta).toArray)\n      .build\n\n    assertThat(meta.getLong(\"longKey\")).isEqualTo(23L)\n    assertThat(meta.getDouble(\"doubleKey\")).isEqualTo(23.0)\n    assertThat(meta.getBoolean(\"booleanKey\")).isTrue\n    assertThat(meta.getString(\"stringKey\")).isEqualTo(\"random\")\n    assertThat(meta.getMetadata(\"fieldMetadataKey\")).isEqualTo(innerMeta)\n    assertThat(meta.getLongArray(\"longArrayKey\")).isEqualTo(longs.toArray)\n    assertThat(meta.getDoubleArray(\"doubleArrayKey\")).isEqualTo(doubles.toArray)\n    assertThat(meta.getBooleanArray(\"booleanArrayKey\")).isEqualTo(booleans.toArray)\n    assertThat(meta.getStringArray(\"stringArrayKey\")).isEqualTo(strings.toArray)\n    assertThat(meta.getMetadataArray(\"fieldMetadataArrayKey\"))\n      .isEqualTo(Seq(innerMeta).toArray)\n  }\n\n  test(\"builder.getMetadata handles null correctly\") {\n    val builder = FieldMetadata.builder()\n    assertThat(builder.getMetadata(\"non-existing\")).isNull()\n  }\n\n  test(\"builder.getMetadata with wrong type throws KernelException\") {\n    val builder = FieldMetadata.builder()\n      .putLong(\"longKey\", 23L)\n\n    val err = intercept[KernelException] {\n      builder.getMetadata(\"longKey\")\n    }\n\n    assert(err.getMessage.contains(\"Expected '23' to be of type 'FieldMetadata'\"))\n  }\n\n  test(\"builder.getMetadata with correct type returns value\") {\n    val innerMeta = FieldMetadata.builder().putBoolean(\"key\", true).build()\n    val builder = FieldMetadata.builder()\n      .putFieldMetadata(\"fieldMetadataKey\", innerMeta)\n\n    assertThat(builder.getMetadata(\"fieldMetadataKey\")).isEqualTo(innerMeta)\n  }\n\n  test(\"toString handles empty metadata\") {\n    val fieldMetadata = FieldMetadata.builder().build()\n    val result = fieldMetadata.toString\n\n    assertThat(result).isEqualTo(\"{}\")\n  }\n\n  test(\"toString handles null values and arrays with null elements\") {\n    val fieldMetadata = FieldMetadata.builder()\n      .putString(\"nullValueKey\", null)\n      .putStringArray(\"arrayWithNulls\", Array(\"a\", null, \"b\"))\n      .putString(\"validValue\", \"test\")\n      .putStringArray(\"validArray\", Array(\"x\", \"y\", \"z\"))\n      .build()\n\n    val result = fieldMetadata.toString\n\n    assertThat(result).contains(\"nullValueKey=null\")\n    assertThat(result).contains(\"[a, null, b]\")\n    assertThat(result).contains(\"validValue=test\")\n    assertThat(result).contains(\"[x, y, z]\")\n  }\n\n  test(\"equalsIgnoreKeys ignores specified keys while validating others\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(\"collation\", \"UTF8_BINARY\")\n      .putString(\"otherKey\", \"same\")\n      .putLongArray(\"arr\", Seq[java.lang.Long](1L, 2L).toArray)\n      .build()\n\n    val meta2 = FieldMetadata.builder()\n      .putString(\"collation\", \"EN_CI\") // different but should be ignored\n      .putString(\"otherKey\", \"same\")\n      .putLongArray(\"arr\", Seq[java.lang.Long](1L, 2L).toArray)\n      .build()\n\n    val ignoreCollation: java.util.Set[String] = Set(\"collation\").asJava\n    val emptyIgnore: java.util.Set[String] = new java.util.HashSet[String]()\n\n    assertThat(meta1.equalsIgnoreKeys(meta2, ignoreCollation)).isTrue\n    assertThat(meta1.equalsIgnoreKeys(meta2, emptyIgnore)).isFalse\n  }\n\n  test(\"equalsIgnoreKeys deepEquals handles arrays properly\") {\n    val longs: Seq[java.lang.Long] = Seq(1L, 2L, 3L)\n    val doubles: Seq[java.lang.Double] = Seq(1.0, 2.0, 3.0)\n    val booleans: Seq[java.lang.Boolean] = Seq(true, false, true)\n    val strings: Seq[java.lang.String] = Seq(\"x\", \"y\", \"z\")\n    val stringsWithNulls: Seq[java.lang.String] = Seq(\"a\", null, \"b\")\n    val inner1 = FieldMetadata.builder().putBoolean(\"k\", true).build()\n    val inner2 = FieldMetadata.builder().putBoolean(\"k\", true).build()\n\n    val meta1 = FieldMetadata.builder()\n      .putLongArray(\"longArrayKey\", longs.toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putStringArray(\"stringArrayWithNulls\", stringsWithNulls.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Array(inner1))\n      .build()\n\n    val meta2 = FieldMetadata.builder()\n      .putLongArray(\"longArrayKey\", longs.toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putStringArray(\"stringArrayWithNulls\", stringsWithNulls.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Array(inner2))\n      .build()\n\n    val emptyIgnore: java.util.Set[String] = new java.util.HashSet[String]()\n    assertThat(meta1.equalsIgnoreKeys(meta2, emptyIgnore)).isTrue\n\n    // Change one array element to ensure inequality is detected\n    val meta3 = FieldMetadata.builder()\n      .putLongArray(\"longArrayKey\", Seq[java.lang.Long](1L, 2L, 99L).toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putStringArray(\"stringArrayWithNulls\", stringsWithNulls.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Array(inner2))\n      .build()\n\n    val ignoreLongArrayKey: java.util.Set[String] = Set(\"longArrayKey\").asJava\n\n    assertThat(meta1.equalsIgnoreKeys(meta3, emptyIgnore)).isFalse\n    assertThat(meta1.equalsIgnoreKeys(meta3, ignoreLongArrayKey)).isTrue\n  }\n\n  test(\"equalsIgnoreKeys handles case where only one side has the ignored key\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(\"collation\", \"EN_CI\")\n      .putString(\"common\", \"v\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(\"common\", \"v\")\n      .build()\n\n    val ignoreCollation: java.util.Set[String] = Set(\"collation\").asJava\n    val emptyIgnore: java.util.Set[String] = new java.util.HashSet[String]()\n\n    assertThat(meta1.equalsIgnoreKeys(meta2, ignoreCollation)).isTrue\n    assertThat(meta1.equalsIgnoreKeys(meta2, emptyIgnore)).isFalse\n  }\n\n  test(\"equalsIgnoreKeys handles entries with null value\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(\"nullableKey\", null)\n      .putString(\"same\", \"x\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(\"nullableKey\", null)\n      .putString(\"same\", \"x\")\n      .build()\n    val meta3 = FieldMetadata.builder()\n      .putString(\"nullableKey\", \"value\")\n      .putString(\"same\", \"x\")\n      .build()\n\n    val emptyIgnore: java.util.Set[String] = new java.util.HashSet[String]()\n    val ignoreNullable: java.util.Set[String] = Set(\"nullableKey\").asJava\n\n    assertThat(meta1.equalsIgnoreKeys(meta2, emptyIgnore)).isTrue\n    assertThat(meta1.equalsIgnoreKeys(meta2, ignoreNullable)).isTrue\n    assertThat(meta1.equalsIgnoreKeys(meta3, emptyIgnore)).isFalse\n    assertThat(meta1.equalsIgnoreKeys(meta3, ignoreNullable)).isTrue\n  }\n\n  test(\"equalsIgnoreKeys handles entries with null key\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(null, \"A\")\n      .putString(\"same\", \"x\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(null, \"A\")\n      .putString(\"same\", \"x\")\n      .build()\n    val meta3 = FieldMetadata.builder()\n      .putString(null, \"B\")\n      .putString(\"same\", \"x\")\n      .build()\n    val meta4 = FieldMetadata.builder()\n      .putString(\"same\", \"x\")\n      .build()\n\n    val emptyIgnore: java.util.Set[String] = new java.util.HashSet[String]()\n    val ignoreNullKey: java.util.Set[String] = Set[String](null).asJava\n\n    // same key/value pairs -> equal\n    assertThat(meta1.equalsIgnoreKeys(meta2, emptyIgnore)).isTrue\n    // different values under null key -> unequal unless null key is ignored\n    assertThat(meta1.equalsIgnoreKeys(meta3, emptyIgnore)).isFalse\n    assertThat(meta1.equalsIgnoreKeys(meta3, ignoreNullKey)).isTrue\n    // one side missing the null key -> unequal unless null key is ignored\n    assertThat(meta1.equalsIgnoreKeys(meta4, emptyIgnore)).isFalse\n    assertThat(meta1.equalsIgnoreKeys(meta4, ignoreNullKey)).isTrue\n  }\n\n  test(\"equalsIgnoreKeys handles entry with null key and null value\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(null, null)\n      .putString(\"same\", \"x\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(null, null)\n      .putString(\"same\", \"x\")\n      .build()\n\n    val emptyIgnore: java.util.Set[String] = new java.util.HashSet[String]()\n    val ignoreNullKey: java.util.Set[String] = Set[String](null).asJava\n\n    assertThat(meta1.equalsIgnoreKeys(meta2, emptyIgnore)).isTrue\n    assertThat(meta1.equalsIgnoreKeys(meta2, ignoreNullKey)).isTrue\n  }\n\n  test(\"equalsIgnoreKeys throws when keys is null\") {\n    val meta1 = FieldMetadata.builder().putString(\"k\", \"v\").build()\n    val meta2 = FieldMetadata.builder().putString(\"k\", \"v\").build()\n\n    val e = intercept[IllegalArgumentException] {\n      meta1.equalsIgnoreKeys(meta2, null.asInstanceOf[java.util.Set[String]])\n    }\n    assert(e.getMessage == \"keys must not be null\")\n  }\n\n  test(\"equals validates all keys and values\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(\"collation\", \"UTF8_BINARY\")\n      .putString(\"otherKey\", \"same\")\n      .putLongArray(\"arr\", Seq[java.lang.Long](1L, 2L).toArray)\n      .build()\n\n    val meta2 = FieldMetadata.builder()\n      .putString(\"collation\", \"UTF8_BINARY\")\n      .putString(\"otherKey\", \"same\")\n      .putLongArray(\"arr\", Seq[java.lang.Long](1L, 2L).toArray)\n      .build()\n\n    val meta3 = FieldMetadata.builder()\n      .putString(\"collation\", \"EN_CI\") // different -> should not be equal\n      .putString(\"otherKey\", \"same\")\n      .putLongArray(\"arr\", Seq[java.lang.Long](1L, 2L).toArray)\n      .build()\n\n    assertThat(meta1.equals(meta2)).isTrue\n    assertThat(meta1.equals(meta3)).isFalse\n  }\n\n  test(\"equals handles arrays properly\") {\n    val longs: Seq[java.lang.Long] = Seq(1L, 2L, 3L)\n    val doubles: Seq[java.lang.Double] = Seq(1.0, 2.0, 3.0)\n    val booleans: Seq[java.lang.Boolean] = Seq(true, false, true)\n    val strings: Seq[java.lang.String] = Seq(\"x\", \"y\", \"z\")\n    val stringsWithNulls: Seq[java.lang.String] = Seq(\"a\", null, \"b\")\n    val inner1 = FieldMetadata.builder().putBoolean(\"k\", true).build()\n    val inner2 = FieldMetadata.builder().putBoolean(\"k\", true).build()\n\n    val meta1 = FieldMetadata.builder()\n      .putLongArray(\"longArrayKey\", longs.toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putStringArray(\"stringArrayWithNulls\", stringsWithNulls.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Array(inner1))\n      .build()\n\n    val meta2 = FieldMetadata.builder()\n      .putLongArray(\"longArrayKey\", longs.toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putStringArray(\"stringArrayWithNulls\", stringsWithNulls.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Array(inner2))\n      .build()\n\n    assertThat(meta1.equals(meta2)).isTrue\n\n    // Change one array element to ensure inequality is detected\n    val meta3 = FieldMetadata.builder()\n      .putLongArray(\"longArrayKey\", Seq[java.lang.Long](1L, 2L, 99L).toArray)\n      .putDoubleArray(\"doubleArrayKey\", doubles.toArray)\n      .putBooleanArray(\"booleanArrayKey\", booleans.toArray)\n      .putStringArray(\"stringArrayKey\", strings.toArray)\n      .putStringArray(\"stringArrayWithNulls\", stringsWithNulls.toArray)\n      .putFieldMetadataArray(\"fieldMetadataArrayKey\", Array(inner2))\n      .build()\n\n    assertThat(meta1.equals(meta3)).isFalse\n  }\n\n  test(\"equals handles case where only one side has the key\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(\"collation\", \"EN_CI\")\n      .putString(\"common\", \"v\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(\"common\", \"v\")\n      .build()\n\n    assertThat(meta1.equals(meta2)).isFalse\n    assertThat(meta2.equals(meta1)).isFalse\n  }\n\n  test(\"equals handles entries with null value\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(\"nullableKey\", null)\n      .putString(\"same\", \"x\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(\"nullableKey\", null)\n      .putString(\"same\", \"x\")\n      .build()\n    val meta3 = FieldMetadata.builder()\n      .putString(\"nullableKey\", \"value\")\n      .putString(\"same\", \"x\")\n      .build()\n\n    assertThat(meta1.equals(meta2)).isTrue\n    assertThat(meta1.equals(meta3)).isFalse\n  }\n\n  test(\"equals handles entries with null key\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(null, \"A\")\n      .putString(\"same\", \"x\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(null, \"A\")\n      .putString(\"same\", \"x\")\n      .build()\n    val meta3 = FieldMetadata.builder()\n      .putString(null, \"B\")\n      .putString(\"same\", \"x\")\n      .build()\n    val meta4 = FieldMetadata.builder()\n      .putString(\"same\", \"x\")\n      .build()\n\n    // same key/value pairs -> equal\n    assertThat(meta1.equals(meta2)).isTrue\n    // different values under null key -> unequal\n    assertThat(meta1.equals(meta3)).isFalse\n    // one side missing the null key -> unequal\n    assertThat(meta1.equals(meta4)).isFalse\n  }\n\n  test(\"equals handles entry with null key and null value\") {\n    val meta1 = FieldMetadata.builder()\n      .putString(null, null)\n      .putString(\"same\", \"x\")\n      .build()\n    val meta2 = FieldMetadata.builder()\n      .putString(null, null)\n      .putString(\"same\", \"x\")\n      .build()\n\n    assertThat(meta1.equals(meta2)).isTrue\n  }\n\n  test(\"equals returns false for null or different class\") {\n    val meta = FieldMetadata.builder().putString(\"k\", \"v\").build()\n    assertThat(meta.equals(null)).isFalse\n    assertThat(meta.equals(\"not-metadata\")).isFalse\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/MetadataColumnSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types\n\nimport io.delta.kernel.utils.MetadataColumnTestUtils\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass MetadataColumnSuite extends AnyFunSuite with MetadataColumnTestUtils {\n  test(\"add metadata columns to schema\") {\n    val schema = new StructType()\n      .add(\"number\", IntegerType.INTEGER)\n      .add(\"name\", StringType.STRING)\n      .addMetadataColumn(\"_metadata.row_index\", MetadataColumnSpec.ROW_INDEX)\n\n    // We compare using addMetadataColumn() against manually adding the expected metadata columns\n    // as provided by MetadataColumnTestUtils\n    val expected = new StructType()\n      .add(\"number\", IntegerType.INTEGER)\n      .add(\"name\", StringType.STRING)\n      .add(ROW_INDEX)\n\n    assert(schema.equals(expected))\n  }\n\n  test(\"fail if metadata column already exists in schema\") {\n    val schema = new StructType()\n      .add(\"number\", IntegerType.INTEGER)\n      .add(\"name\", StringType.STRING)\n      .addMetadataColumn(\"_metadata.row_index\", MetadataColumnSpec.ROW_INDEX)\n\n    // Adding the same metadata column should fail\n    val e = intercept[IllegalArgumentException] {\n      schema.addMetadataColumn(\"some other name\", MetadataColumnSpec.ROW_INDEX)\n    }\n    assert(e.getMessage.contains(\"Duplicate metadata column row_index found in struct type\"))\n\n    // Adding a different metadata column should not fail\n    val updated = schema.addMetadataColumn(\"_metadata.row_id\", MetadataColumnSpec.ROW_ID)\n    val expected = new StructType()\n      .add(\"number\", IntegerType.INTEGER)\n      .add(\"name\", StringType.STRING)\n      .add(ROW_INDEX)\n      .add(ROW_ID)\n    assert(updated.equals(expected))\n  }\n\n  test(\"fail if metadata column is nested\") {\n    val schema = new StructType()\n\n    // Adding a nested metadata column should fail\n    val e1 = intercept[IllegalArgumentException] {\n      schema.add(new StructField(\n        \"struct\",\n        new StructType().addMetadataColumn(\"row_index\", MetadataColumnSpec.ROW_INDEX),\n        false))\n    }\n    assert(\n      e1.getMessage.contains(\"Metadata columns are only allowed at the top level of a schema.\"))\n\n    // Verify two-level nesting fails\n    val e2 = intercept[IllegalArgumentException] {\n      schema.add(new StructField(\n        \"struct\",\n        new StructType().add(new StructField(\n          \"inner_struct\",\n          new StructType().addMetadataColumn(\"row_index\", MetadataColumnSpec.ROW_INDEX),\n          false)),\n        false))\n    }\n    assert(\n      e2.getMessage.contains(\"Metadata columns are only allowed at the top level of a schema.\"))\n\n    // Verify metadata in map type fails\n    val e3 = intercept[IllegalArgumentException] {\n      schema.add(new StructField(\n        \"map\",\n        new MapType(\n          StringType.STRING,\n          new StructType().addMetadataColumn(\"row_index\", MetadataColumnSpec.ROW_INDEX),\n          false),\n        false))\n    }\n    assert(\n      e3.getMessage.contains(\"Metadata columns are only allowed at the top level of a schema.\"))\n\n    // Verify metadata in array type fails\n    val e4 = intercept[IllegalArgumentException] {\n      schema.add(new StructField(\n        \"array\",\n        new ArrayType(\n          new StructType().addMetadataColumn(\"row_index\", MetadataColumnSpec.ROW_INDEX),\n          false),\n        false))\n    }\n    assert(\n      e4.getMessage.contains(\"Metadata columns are only allowed at the top level of a schema.\"))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/StringTypeSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass StringTypeSuite extends AnyFunSuite {\n  test(\"check equals\") {\n    // Testcase: (instance1, instance2, expected value for `instance1 == instance2`)\n    Seq(\n      (\n        StringType.STRING,\n        StringType.STRING,\n        true),\n      (\n        StringType.STRING,\n        new StringType(\"sPark.UTF8_bINary\"),\n        true),\n      (\n        StringType.STRING,\n        new StringType(\"SPARK.UTF8_LCASE\"),\n        false),\n      (\n        new StringType(\"ICU.UNICODE\"),\n        new StringType(\"SPARK.UTF8_LCASE\"),\n        false),\n      (\n        new StringType(\"ICU.UNICODE\"),\n        new StringType(\"ICU.UNICODE_CI\"),\n        false),\n      (\n        new StringType(\"ICU.UNICODE_CI\"),\n        new StringType(\"icU.uniCODe_Ci\"),\n        true)).foreach {\n      case (st1, st2, expResult) =>\n        assert(st1.equals(st2) == expResult)\n    }\n  }\n\n  test(\"isUTF8BinaryCollated\") {\n    assert(StringType.STRING.isUTF8BinaryCollated)\n    assert(new StringType(\"sPark.UTF8_bINary\").isUTF8BinaryCollated)\n    assert(!new StringType(\"SPARK.UTF8_LCASE\").isUTF8BinaryCollated)\n    assert(!new StringType(\"ICU.UNICODE.72.2\").isUTF8BinaryCollated)\n    assert(!new StringType(\"ICU.UNICODE_CI\").isUTF8BinaryCollated)\n  }\n\n  test(\"toString\") {\n    assert(StringType.STRING.toString == \"string\")\n    assert(new StringType(\"sPark.UTF8_bINary\").toString == \"string\")\n    assert(new StringType(\"SPARK.UTF8_LCASE\").toString == \"string collate UTF8_LCASE\")\n    assert(new StringType(\"ICU.uNICoDE.72.2\").toString == \"string collate UNICODE\")\n    assert(new StringType(\"ICU.UNICODE_CI\").toString == \"string collate UNICODE_CI\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/StructFieldSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.types\n\nimport java.util.ArrayList\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.types.StructField.{COLLATIONS_METADATA_KEY, DELTA_TYPE_CHANGES_KEY, FIELD_PATH_KEY, FROM_TYPE_KEY, TO_TYPE_KEY}\n\nimport collection.JavaConverters._\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for [[StructField]] class.\n */\nclass StructFieldSuite extends AnyFunSuite {\n\n  // Test equality and hashcode\n  test(\"equality and hashcode\") {\n    val field1 = new StructField(\n      \"field\",\n      LongType.LONG,\n      true,\n      FieldMetadata.empty(),\n      Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava)\n    val field2 = new StructField(\n      \"field\",\n      LongType.LONG,\n      true,\n      FieldMetadata.empty(),\n      Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava)\n    val field3 = new StructField(\"differentField\", IntegerType.INTEGER, true)\n    val field4 = new StructField(\"field\", StringType.STRING, true)\n    val field5 = new StructField(\"field\", IntegerType.INTEGER, false)\n    val field6 = new StructField(\n      \"field\",\n      IntegerType.INTEGER,\n      true,\n      FieldMetadata.builder().putBoolean(\"a\", true).build(),\n      Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava)\n    val field7 = new StructField(\n      \"field\",\n      LongType.LONG,\n      true,\n      FieldMetadata.empty(),\n      Seq(new TypeChange(IntegerType.INTEGER, StringType.STRING)).asJava)\n\n    assert(field1 == field2)\n    assert(field1.hashCode() == field2.hashCode())\n\n    assert(field1 != field3)\n    assert(field1 != field4)\n    assert(field1 != field5)\n    assert(field1 != field6)\n    assert(field1 != field7)\n  }\n\n  Seq(\n    new StructType(),\n    new ArrayType(LongType.LONG, false),\n    new MapType(LongType.LONG, LongType.LONG, false)).foreach { dataType =>\n    test(s\"withType should throw exception with change types for nested types $dataType\") {\n      val field = new StructField(\n        \"field\",\n        dataType,\n        true)\n      assertThrows[KernelException] {\n        field.withTypeChanges(Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava)\n      }\n    }\n\n    test(s\"Constructor should throw exception with change types for nested types $dataType\") {\n\n      assertThrows[KernelException] {\n        new StructField(\n          \"field\",\n          dataType,\n          true,\n          FieldMetadata.empty(),\n          Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava)\n      }\n    }\n  }\n\n  // Test metadata column detection\n  test(\"metadata column detection\") {\n    val regularField = new StructField(\"regularField\", IntegerType.INTEGER, true)\n    assert(!regularField.isMetadataColumn)\n    assert(regularField.isDataColumn)\n\n    // Create a metadata field\n    val metadataFieldName = \"_metadata.custom\"\n    val metadataBuilder = FieldMetadata.builder()\n    metadataBuilder.putMetadataColumnSpec(\n      StructField.METADATA_SPEC_KEY,\n      MetadataColumnSpec.ROW_INDEX)\n    val metadataField =\n      new StructField(metadataFieldName, LongType.LONG, false, metadataBuilder.build())\n\n    assert(metadataField.isMetadataColumn)\n    assert(!metadataField.isDataColumn)\n  }\n\n  // Test withNewMetadata method\n  test(\"withNewMetadata\") {\n    val originalField = new StructField(\"field\", IntegerType.INTEGER, true)\n    assert(originalField.getMetadata() == FieldMetadata.empty())\n\n    val newMetadataBuilder = FieldMetadata.builder()\n    newMetadataBuilder.putString(\"key\", \"value\")\n    val newMetadata = newMetadataBuilder.build()\n\n    val updatedField = originalField.withNewMetadata(newMetadata)\n\n    assert(updatedField.getName == originalField.getName)\n    assert(updatedField.getDataType == originalField.getDataType)\n    assert(updatedField.isNullable == originalField.isNullable)\n    assert(updatedField.getMetadata == newMetadata)\n    assert(updatedField.getMetadata.getString(\"key\") == \"value\")\n  }\n\n  // Test type changes\n  test(\"type changes\") {\n    val originalField = new StructField(\n      \"field\",\n      IntegerType.INTEGER,\n      true,\n      FieldMetadata.builder().putString(\"a\", \"b\").build())\n    assert(originalField.getTypeChanges.isEmpty)\n\n    val typeChanges = new ArrayList[TypeChange]()\n    typeChanges.add(new TypeChange(IntegerType.INTEGER, LongType.LONG))\n\n    val updatedField = originalField.withTypeChanges(typeChanges)\n\n    assert(updatedField.getName == originalField.getName)\n    assert(updatedField.getDataType == originalField.getDataType)\n    assert(updatedField.isNullable == originalField.isNullable)\n    assert(updatedField.getMetadata == FieldMetadata.builder()\n      .putString(\"a\", \"b\")\n      .putFieldMetadataArray(\n        \"delta.typeChanges\",\n        Array(FieldMetadata.builder()\n          .putString(\"fromType\", \"integer\")\n          .putString(\"toType\", \"long\").build()))\n      .build())\n    assert(updatedField.getTypeChanges.size() == 1)\n\n    val typeChange = updatedField.getTypeChanges.get(0)\n    assert(typeChange.getFrom == IntegerType.INTEGER)\n    assert(typeChange.getTo == LongType.LONG)\n  }\n\n  // Test TypeChange class\n  test(\"TypeChange class\") {\n    val from = IntegerType.INTEGER\n    val to = LongType.LONG\n    val typeChange = new TypeChange(from, to)\n\n    assert(typeChange.getFrom == from)\n    assert(typeChange.getTo == to)\n\n    // Test equals and hashCode\n    val sameTypeChange = new TypeChange(IntegerType.INTEGER, LongType.LONG)\n    val differentTypeChange = new TypeChange(IntegerType.INTEGER, StringType.STRING)\n\n    assert(typeChange == sameTypeChange)\n    assert(typeChange.hashCode() == sameTypeChange.hashCode())\n    assert(typeChange != differentTypeChange)\n  }\n\n  // Sequence of tuples containing StructFields with type changes and their expected FieldMetadata\n  Seq(\n    // Simple primitive type change: Integer -> Long\n    (\n      new StructField(\n        \"intToLongField\",\n        LongType.LONG,\n        true,\n        FieldMetadata.empty(),\n        Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava),\n      FieldMetadata.builder()\n        .putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY,\n          Array(\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"integer\")\n              .putString(TO_TYPE_KEY, \"long\")\n              .build()))\n        .build()),\n\n    // Multiple type changes: Integer -> Long -> Decimal\n    (\n      new StructField(\n        \"multiTypeChangeField\",\n        new DecimalType(10, 2),\n        true,\n        FieldMetadata.empty(),\n        Seq(\n          new TypeChange(IntegerType.INTEGER, LongType.LONG),\n          new TypeChange(LongType.LONG, new DecimalType(10, 2))).asJava),\n      FieldMetadata.builder()\n        .putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY,\n          Array(\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"integer\")\n              .putString(TO_TYPE_KEY, \"long\")\n              .build(),\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"long\")\n              .putString(TO_TYPE_KEY, \"decimal(10,2)\")\n              .build()))\n        .build()),\n    // Float -> Double type change with additional metadata\n    (\n      new StructField(\n        \"floatToDoubleField\",\n        DoubleType.DOUBLE,\n        true,\n        FieldMetadata.builder().putString(\"description\", \"A field with type change\").build(),\n        Seq(new TypeChange(FloatType.FLOAT, DoubleType.DOUBLE)).asJava),\n      FieldMetadata.builder()\n        .putString(\"description\", \"A field with type change\")\n        .putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY,\n          Array(\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"float\")\n              .putString(TO_TYPE_KEY, \"double\")\n              .build()))\n        .build()),\n    // Type change in array element type\n    (\n      new StructField(\n        \"arrayField\",\n        new ArrayType(new StructField(\"element\", LongType.LONG, true)\n          .withTypeChanges(Seq(new TypeChange(ShortType.SHORT, LongType.LONG)).asJava)),\n        true),\n      FieldMetadata.builder()\n        .putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY,\n          Array(\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"short\")\n              .putString(TO_TYPE_KEY, \"long\")\n              .putString(FIELD_PATH_KEY, \"element\")\n              .build()))\n        .build()),\n    // Type change in map value type\n    (\n      new StructField(\n        \"mapField\",\n        new MapType(\n          new StructField(\"key\", StringType.STRING, false),\n          new StructField(\"value\", LongType.LONG, true).withTypeChanges(\n            Seq(\n              new TypeChange(ShortType.SHORT, IntegerType.INTEGER),\n              new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava)),\n        false),\n      FieldMetadata.builder()\n        .putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY,\n          Array(\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"short\")\n              .putString(TO_TYPE_KEY, \"integer\")\n              .putString(FIELD_PATH_KEY, \"value\")\n              .build(),\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"integer\")\n              .putString(TO_TYPE_KEY, \"long\")\n              .putString(FIELD_PATH_KEY, \"value\")\n              .build()))\n        .build()),\n    // Complex nested type with multiple type changes\n    (\n      new StructField(\n        \"complexField\",\n        new ArrayType(new StructField(\n          \"element\",\n          new MapType(\n            new StructField(\n              \"key\",\n              new ArrayType(new StructField(\"element\", LongType.LONG, false)\n                .withTypeChanges(Seq(new TypeChange(ShortType.SHORT, LongType.LONG)).asJava)),\n              false),\n            new StructField(\n              \"value\",\n              new MapType(\n                new StructField(\"key\", ShortType.SHORT, false),\n                new StructField(\"value\", LongType.LONG, true)\n                  .withTypeChanges(Seq(new TypeChange(ByteType.BYTE, LongType.LONG)).asJava)),\n              false)),\n          false)),\n        false),\n      FieldMetadata.builder()\n        .putFieldMetadataArray(\n          DELTA_TYPE_CHANGES_KEY,\n          Array(\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"short\")\n              .putString(TO_TYPE_KEY, \"long\")\n              .putString(FIELD_PATH_KEY, \"element.key.element\")\n              .build(),\n            FieldMetadata.builder()\n              .putString(FROM_TYPE_KEY, \"byte\")\n              .putString(TO_TYPE_KEY, \"long\")\n              .putString(FIELD_PATH_KEY, \"element.value.value\")\n              .build()))\n        .build())).foreach {\n\n    case (structField, expectedMetadata) =>\n\n      test(s\"$structField has expected metadata\") {\n        assert(structField.getMetadata == expectedMetadata)\n      }\n\n      test(s\"$structField does not leak field metadata if it is a child struct field.\") {\n        // Field metadata for type changes is stored at the nearest ancestor of the type\n        // sho it shouldn't leak up.\n        assert(new StructField(\"parent\", new StructType().add(structField), false).getMetadata ==\n          FieldMetadata.empty())\n        assert(new StructField(\n          \"parent\",\n          new ArrayType(new StructType().add(structField), false),\n          false).getMetadata ==\n          FieldMetadata.empty())\n        assert(new StructField(\n          \"parent\",\n          new MapType(new StructType().add(structField), new StructType().add(structField), false),\n          false).getMetadata ==\n          FieldMetadata.empty())\n\n      }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/types/TypesSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.types\n\nimport java.util.Arrays\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TypesSuite extends AnyFunSuite {\n  test(\"isNested - false\") {\n    // All primitive types should return false for isNested\n    val primitiveTypes = Seq(\n      BinaryType.BINARY,\n      BooleanType.BOOLEAN,\n      ByteType.BYTE,\n      DateType.DATE,\n      new DecimalType(10, 2),\n      DoubleType.DOUBLE,\n      FloatType.FLOAT,\n      IntegerType.INTEGER,\n      LongType.LONG,\n      ShortType.SHORT,\n      StringType.STRING,\n      TimestampType.TIMESTAMP,\n      TimestampNTZType.TIMESTAMP_NTZ,\n      VariantType.VARIANT)\n\n    primitiveTypes.foreach { dataType =>\n      assert(!dataType.isNested(), s\"Expected $dataType to not be nested\")\n    }\n  }\n\n  test(\"isNested - nested types\") {\n    // Create instances of nested types\n    val structFields = Arrays.asList(\n      new StructField(\"field1\", IntegerType.INTEGER, true),\n      new StructField(\"field2\", StringType.STRING, true))\n    val structType = new StructType(structFields)\n\n    val arrayType = new ArrayType(\n      new StructField(\"element\", IntegerType.INTEGER, true))\n\n    val mapType = new MapType(\n      new StructField(\"key\", StringType.STRING, false),\n      new StructField(\"value\", IntegerType.INTEGER, true))\n\n    // All nested types should return true for isNested\n    val nestedTypes = Seq(structType, arrayType, mapType)\n\n    nestedTypes.foreach { dataType =>\n      assert(dataType.isNested(), s\"Expected $dataType to be nested\")\n    }\n  }\n\n  test(\"MapType constructor throws IllegalArgumentException for collated StringType keys\") {\n    // Test multiple collation providers to ensure all non-default collations are rejected\n    Seq(\"SPARK.UTF8_LCASE\", \"ICU.UNICODE_CI\").foreach { collation =>\n      val collatedString = new StringType(collation)\n\n      // 3-arg constructor\n      val ex1 = intercept[IllegalArgumentException] {\n        new MapType(collatedString, IntegerType.INTEGER, false)\n      }\n      assert(ex1.getMessage.contains(\"does not support collated string types as keys\"))\n      assert(ex1.getMessage.contains(\"UTF8_BINARY\"))\n      assert(\n        ex1.getMessage.contains(collatedString.toString),\n        s\"Error message should include the found type but was: ${ex1.getMessage}\")\n\n      // 2-arg StructField constructor\n      val ex2 = intercept[IllegalArgumentException] {\n        new MapType(\n          new StructField(\"key\", collatedString, false),\n          new StructField(\"value\", IntegerType.INTEGER, true))\n      }\n      assert(ex2.getMessage.contains(\"does not support collated string types as keys\"))\n      assert(\n        ex2.getMessage.contains(collatedString.toString),\n        s\"Error message should include the found type but was: ${ex2.getMessage}\")\n    }\n  }\n\n  test(\"MapType allows default UTF8_BINARY StringType keys and non-string keys\") {\n    val map1 = new MapType(StringType.STRING, IntegerType.INTEGER, false)\n    assert(map1.getKeyType === StringType.STRING)\n\n    val utf8BinaryString = new StringType(\"SPARK.UTF8_BINARY\")\n    val map2 = new MapType(utf8BinaryString, IntegerType.INTEGER, false)\n    assert(map2.getKeyType === utf8BinaryString)\n\n    val map3 = new MapType(IntegerType.INTEGER, StringType.STRING, true)\n    assert(map3.getKeyType === IntegerType.INTEGER)\n  }\n\n  test(\"MapType allows collated StringType as values\") {\n    val collatedString = new StringType(\"SPARK.UTF8_LCASE\")\n    val map = new MapType(StringType.STRING, collatedString, true)\n    assert(map.getValueType === collatedString)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-api/src/test/scala/io/delta/kernel/utils/MetadataColumnTestUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.utils\n\nimport io.delta.kernel.types.{MetadataColumnSpec, StructField}\nimport io.delta.kernel.types.StructField.createMetadataColumn\n\ntrait MetadataColumnTestUtils {\n  val ROW_INDEX: StructField =\n    createMetadataColumn(\"_metadata.row_index\", MetadataColumnSpec.ROW_INDEX)\n  val ROW_ID: StructField = createMetadataColumn(\"_metadata.row_id\", MetadataColumnSpec.ROW_ID)\n  val ROW_COMMIT_VERSION: StructField =\n    createMetadataColumn(\"_metadata.row_commit_version\", MetadataColumnSpec.ROW_COMMIT_VERSION)\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/AbstractBenchmarkState.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks;\n\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport io.delta.kernel.benchmarks.workloadrunners.WorkloadRunner;\nimport io.delta.kernel.engine.*;\nimport org.openjdk.jmh.annotations.*;\n\n/**\n * Base state class for all benchmark state. This class is responsible for setting up the {@link\n * WorkloadRunner} based on the {@link WorkloadSpec} and engine parameters provided by JMH.\n *\n * <p>To add support for a new engine, extend this class and implement the {@link\n * #getEngine(String)} method to return an instance of the desired engine based on the provided\n * engine name.\n *\n * @see WorkloadRunner\n * @see WorkloadSpec\n */\n@State(Scope.Thread)\npublic abstract class AbstractBenchmarkState {\n\n  /**\n   * The json representation of the workload specification. Note: This parameter will be set\n   * dynamically by JMH. The value is set in the main method.\n   */\n  @Param({})\n  private String workloadSpecJson;\n\n  /**\n   * The engine to use for this benchmark. Note: This parameter will be set dynamically by JMH. The\n   * value is set in the main method.\n   */\n  @Param({})\n  private String engineName;\n\n  /** The workload runner initialized for this benchmark invocation. */\n  private WorkloadRunner runner;\n\n  /**\n   * Parses the workload specification from JSON and initializes the benchmarking engine. This also\n   * sets up the workload runner.\n   *\n   * @throws Exception If any error occurs during setup.\n   */\n  @Setup(Level.Trial)\n  public void setupTrial() throws Exception {\n    WorkloadSpec spec = WorkloadSpec.fromJsonString(workloadSpecJson);\n    Engine engine = KernelMetricsProfiler.BenchmarkingEngine.wrapEngine(getEngine(engineName));\n    runner = spec.getRunner(engine);\n  }\n\n  /**\n   * Setup method that runs before each benchmark invocation. This calls the {@link\n   * WorkloadRunner#setup()} to set up the workload runner.\n   *\n   * @throws Exception If any error occurs during setup.\n   */\n  @Setup(Level.Invocation)\n  public void setupInvocation() throws Exception {\n    runner.setup();\n  }\n\n  /**\n   * Teardown method that runs after each benchmark invocation. This calls the {@link\n   * WorkloadRunner#cleanup()} to clean up any state created during execution.\n   *\n   * @throws Exception If any error occurs during cleanup.\n   */\n  @TearDown(Level.Invocation)\n  public void teardownInvocation() throws Exception {\n    runner.cleanup();\n  }\n\n  /**\n   * Returns an instance of the desired engine based on the provided engine name.\n   *\n   * @param engineName The name of the engine to instantiate.\n   * @return An instance of the specified engine.\n   */\n  protected abstract Engine getEngine(String engineName);\n\n  /** @return The workload specification for this benchmark invocation. */\n  public WorkloadSpec getWorkloadSpecification() {\n    return getRunner().getWorkloadSpec();\n  }\n\n  /** @return The workload runner initialized for this benchmark invocation. */\n  public WorkloadRunner getRunner() {\n    return runner;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/BenchmarkParallelCheckpointReading.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks;\n\nimport static java.util.concurrent.Executors.newFixedThreadPool;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.defaults.engine.DefaultParquetHandler;\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO;\nimport io.delta.kernel.defaults.internal.parquet.ParquetFileReader;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.engine.FileReadResult;\nimport io.delta.kernel.engine.ParquetHandler;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport org.apache.hadoop.conf.Configuration;\nimport org.openjdk.jmh.annotations.*;\nimport org.openjdk.jmh.infra.Blackhole;\n\n/**\n * Benchmark to measure the performance of reading multi-part checkpoint files, using a custom\n * ParquetHandler that reads the files in parallel. To run this benchmark (from delta repo root):\n *\n * <ul>\n *   <li>Generate the test table by following the instructions at `testTablePath` member variable.\n *   <li>\n *       <pre>{@code\n * build/sbt sbt:delta> project kernelDefaults\n * sbt:delta> set fork in run := true sbt:delta>\n * sbt:delta> test:runMain \\\n *   io.delta.kernel.benchmarks.BenchmarkParallelCheckpointReading.\n *\n * }</pre>\n * </ul>\n *\n * <p>Sample benchmarks on a table with checkpoint (13) parts containing total of 1.3mil actions on\n * Macbook Pro M2 Max with table stored locally.\n *\n * <pre>{@code\n * Benchmark  (parallelReaderCount)  Mode  Cnt Score Error  Units\n * benchmark                      0  avgt    5  1565.520 ±  20.551  ms/op\n * benchmark                      1  avgt    5  1064.850 ±  19.699  ms/op\n * benchmark                      2  avgt    5   785.918 ± 176.285  ms/op\n * benchmark                      4  avgt    5   729.487 ±  51.470  ms/op\n * benchmark                     10  avgt    5   693.757 ±  41.252  ms/op\n * benchmark                     20  avgt    5   702.656 ±  19.145  ms/op\n * }</pre>\n */\n@State(Scope.Benchmark)\n@OutputTimeUnit(TimeUnit.MILLISECONDS)\n@Warmup(iterations = 3)\n@Fork(1)\npublic class BenchmarkParallelCheckpointReading {\n\n  /**\n   * Following are the steps to generate a simple large table with multi-part checkpoint files\n   *\n   * <pre>{@code\n   * bin/spark-shell --packages io.delta:delta-spark_2.12:3.1.0 \\\n   *   --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n   *   --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\" \\\n   *   --conf \"spark.databricks.delta.checkpoint.partSize=100000\"\n   *\n   * # Within the Spark shell, run the following commands\n   * scala> spark.range(0, 100000) .withColumn(\"pCol\", 'id % 100000) .repartition(10)\n   *   .write.format(\"delta\") .partitionBy(\"pCol\") .mode(\"append\")\n   *   .save(\"~/test-tables/large-table\")\n   *\n   * # Repeat the above steps for each of the next ranges # 100000 to 200000, 200000 to 300000 etc\n   * until enough log entries are reached.\n   *\n   * # Then create a checkpoint\n   * # This step create a multi-part checkpoint with each checkpoint containing 100K records.\n   * scala> import org.apache.spark.sql.delta.DeltaLog\n   * scala> DeltaLog.forTable(spark, \"~/test-tables/large-table\").checkpoint()\n   * }</pre>\n   */\n  public static final String testTablePath = \"<TODO> fill the path here\";\n\n  @State(Scope.Benchmark)\n  public static class BenchmarkData {\n    // Variations of number of threads to read the multi-part checkpoint files\n    // When thread count is 0, we read using the current default parquet handler implementation\n    // In all other cases we use the custom parallel parquet handler implementation defined\n    // in this benchmark\n    @Param({\"0\", \"1\", \"2\", \"4\", \"10\", \"20\"})\n    private int parallelReaderCount = 0;\n  }\n\n  @Benchmark\n  @BenchmarkMode(Mode.AverageTime)\n  public void benchmark(BenchmarkData benchmarkData, Blackhole blackhole) throws Exception {\n    Engine engine = createEngine(benchmarkData.parallelReaderCount);\n    Table table = Table.forPath(engine, testTablePath);\n\n    Snapshot snapshot = table.getLatestSnapshot(engine);\n    ScanBuilder scanBuilder = snapshot.getScanBuilder();\n    Scan scan = scanBuilder.build();\n\n    // Scan state is not used, but get it so that we simulate the real use case.\n    Row row = scan.getScanState(engine);\n    blackhole.consume(row); // To avoid dead code elimination by the JIT compiler\n    long fileSize = 0;\n    try (CloseableIterator<FilteredColumnarBatch> batchIter = scan.getScanFiles(engine)) {\n      while (batchIter.hasNext()) {\n        FilteredColumnarBatch batch = batchIter.next();\n        try (CloseableIterator<Row> rowIter = batch.getRows()) {\n          while (rowIter.hasNext()) {\n            Row r = rowIter.next();\n            long size = r.getStruct(0).getLong(2);\n            fileSize += size;\n          }\n        }\n      }\n    }\n\n    // Consume the result to avoid dead code elimination by the JIT compiler\n    blackhole.consume(fileSize);\n  }\n\n  public static void main(String[] args) throws Exception {\n    org.openjdk.jmh.Main.main(args);\n  }\n\n  private static Engine createEngine(int numberOfParallelThreads) {\n    FileIO fileIO = new HadoopFileIO(new Configuration());\n    if (numberOfParallelThreads <= 0) {\n      return DefaultEngine.create(fileIO);\n    }\n\n    return new DefaultEngine(fileIO) {\n      @Override\n      public ParquetHandler getParquetHandler() {\n        return new ParallelParquetHandler(fileIO, numberOfParallelThreads);\n      }\n    };\n  }\n\n  /**\n   * Custom implementation of {@link ParquetHandler} to read the Parquet files in parallel. Reason\n   * for this not being in the {@link DefaultParquetHandler} is that this implementation keeps the\n   * contents of the Parquet files in memory, which is not suitable for default implementation\n   * without a proper design that allows limits on the memory usage. If the parallel reading of\n   * checkpoint becomes a common in connectors, we can look at adding the functionality in the\n   * default implementation.\n   */\n  static class ParallelParquetHandler extends DefaultParquetHandler {\n    private final FileIO fileIO;\n    private final int numberOfParallelThreads;\n\n    ParallelParquetHandler(FileIO fileIO, int numberOfParallelThreads) {\n      super(fileIO);\n      this.fileIO = fileIO;\n      this.numberOfParallelThreads = numberOfParallelThreads;\n    }\n\n    @Override\n    public CloseableIterator<FileReadResult> readParquetFiles(\n        CloseableIterator<FileStatus> fileIter,\n        StructType physicalSchema,\n        Optional<Predicate> predicate)\n        throws IOException {\n      return new CloseableIterator<FileReadResult>() {\n        // Executor service will be closed as part of the returned `CloseableIterator`'s\n        // close method.\n        private final ExecutorService executorService = newFixedThreadPool(numberOfParallelThreads);\n        private Iterator<Future<List<FileReadResult>>> futuresIter;\n        private Iterator<FileReadResult> currentBatchIter;\n\n        @Override\n        public void close() throws IOException {\n          Utils.closeCloseables(fileIter, () -> executorService.shutdown());\n        }\n\n        @Override\n        public boolean hasNext() {\n          submitReadRequestsIfNotDone();\n          if (currentBatchIter != null && currentBatchIter.hasNext()) {\n            return true;\n          }\n\n          if (futuresIter.hasNext()) {\n            try {\n              currentBatchIter = futuresIter.next().get().iterator();\n              return hasNext();\n            } catch (InterruptedException | ExecutionException e) {\n              throw new RuntimeException(e);\n            }\n          }\n          return false;\n        }\n\n        @Override\n        public FileReadResult next() {\n          return currentBatchIter.next();\n        }\n\n        private void submitReadRequestsIfNotDone() {\n          if (futuresIter != null) {\n            return;\n          }\n          List<Future<List<FileReadResult>>> futures = new ArrayList<>();\n          while (fileIter.hasNext()) {\n            futures.add(\n                executorService.submit(() -> parquetFileReader(fileIter.next(), physicalSchema)));\n          }\n          futuresIter = futures.iterator();\n        }\n      };\n    }\n\n    List<FileReadResult> parquetFileReader(FileStatus fileStatus, StructType readSchema) {\n      ParquetFileReader reader = new ParquetFileReader(fileIO);\n      try (CloseableIterator<ColumnarBatch> batchIter =\n          reader.read(fileStatus, readSchema, Optional.empty())) {\n        List<FileReadResult> batches = new ArrayList<>();\n        while (batchIter.hasNext()) {\n          batches.add(new FileReadResult(batchIter.next(), fileStatus.getPath()));\n        }\n        return batches;\n      } catch (IOException e) {\n        throw new RuntimeException(e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/BenchmarkUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.benchmarks;\n\nimport io.delta.kernel.benchmarks.models.TableInfo;\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/** Useful utilities and values for benchmarks. */\npublic class BenchmarkUtils {\n\n  public static final Path RESOURCES_DIR = getResourcesDirectory();\n  public static final Path WORKLOAD_SPECS_DIR = RESOURCES_DIR.resolve(\"workload_specs\");\n\n  private static final String DELTA_DIR_NAME = \"delta\";\n  private static final String SPECS_DIR_NAME = \"specs\";\n  private static final String SPEC_FILE_NAME = \"spec.json\";\n  private static final String TABLE_INFO_FILE_NAME = \"table_info.json\";\n\n  /**\n   * Gets the resources directory, ensuring user.dir system property is set.\n   *\n   * @return the path to the test resources directory\n   * @throws IllegalStateException if user.dir system property is not set\n   */\n  private static Path getResourcesDirectory() {\n    String userDir = System.getProperty(\"user.dir\");\n    if (userDir == null || userDir.trim().isEmpty()) {\n      throw new IllegalStateException(\n          \"System property 'user.dir' is not set. This is required to locate test resources.\");\n    }\n    return Paths.get(userDir + \"/src/test/resources\");\n  }\n\n  /**\n   * Scans the workloads directory and loads all JSON workload specifications.\n   *\n   * <p>This method:\n   *\n   * <ol>\n   *   <li>Finds all table directories in the workload specs directory\n   *   <li>Loads table_info.json from each table directory\n   *   <li>Loads all spec.json files from specs/ subdirectories\n   *   <li>Enriches each spec with tableInfo and caseName\n   *   <li>Returns loaded specs (not yet expanded into variants)\n   * </ol>\n   *\n   * <p>Note: Variant generation happens later via {@link WorkloadSpec#getWorkloadVariants()}.\n   *\n   * @param specDirPath Path to the directory containing workload specifications\n   * @return List of loaded workload specifications (base specs, not variants)\n   * @throws WorkloadLoadException if workloads cannot be loaded\n   */\n  public static List<WorkloadSpec> loadAllWorkloads(Path specDirPath) {\n    validateWorkloadDirectory(specDirPath);\n\n    List<Path> tableDirectories = findTableDirectories(specDirPath);\n\n    return tableDirectories.stream()\n        .flatMap(tableDir -> loadSpecsFromTable(tableDir).stream())\n        .collect(Collectors.toList());\n  }\n\n  /** Validates that the workload directory exists and is accessible. */\n  private static void validateWorkloadDirectory(Path specDirPath) {\n    if (!Files.exists(specDirPath)) {\n      throw new WorkloadLoadException(\"Workload directory does not exist: \" + specDirPath);\n    }\n\n    if (!Files.isDirectory(specDirPath)) {\n      throw new WorkloadLoadException(\"Path is not a directory: \" + specDirPath);\n    }\n\n    if (!Files.isReadable(specDirPath)) {\n      throw new WorkloadLoadException(\"Cannot read workload directory: \" + specDirPath);\n    }\n  }\n\n  /** Finds all table directories within the workload specifications directory. */\n  private static List<Path> findTableDirectories(Path specDirPath) {\n    try (Stream<Path> files = Files.list(specDirPath)) {\n      List<Path> tableDirectories = files.filter(Files::isDirectory).collect(Collectors.toList());\n\n      if (tableDirectories.isEmpty()) {\n        throw new WorkloadLoadException(\"No table directories found in \" + specDirPath);\n      }\n\n      return tableDirectories;\n    } catch (IOException e) {\n      throw new WorkloadLoadException(\"Failed to list table directories in \" + specDirPath, e);\n    }\n  }\n\n  /** Loads all workload specifications from a single table directory. */\n  private static List<WorkloadSpec> loadSpecsFromTable(Path tableDir) {\n    validateTableStructure(tableDir);\n\n    Path tableInfoPath = tableDir.resolve(TABLE_INFO_FILE_NAME);\n    Path specsDir = tableDir.resolve(SPECS_DIR_NAME);\n\n    TableInfo tableInfo =\n        TableInfo.fromJsonPath(tableInfoPath.toString(), tableDir.toAbsolutePath().toString());\n\n    return findSpecDirectories(specsDir).stream()\n        .map(specDir -> loadSingleSpec(specDir, tableInfo))\n        .collect(Collectors.toList());\n  }\n\n  /** Validates that a table directory has the required structure. */\n  private static void validateTableStructure(Path tableDir) {\n    Path deltaDir = tableDir.resolve(DELTA_DIR_NAME);\n    Path specsDir = tableDir.resolve(SPECS_DIR_NAME);\n\n    if (!Files.exists(deltaDir) || !Files.isDirectory(deltaDir)) {\n      throw new WorkloadLoadException(\"Delta directory not found: \" + deltaDir);\n    }\n\n    if (!Files.exists(specsDir) || !Files.isDirectory(specsDir)) {\n      throw new WorkloadLoadException(\"Specs directory not found: \" + specsDir);\n    }\n  }\n\n  /** Finds all specification directories within the specs directory. */\n  private static List<Path> findSpecDirectories(Path specsDir) {\n    try (Stream<Path> specDirs = Files.list(specsDir)) {\n      List<Path> specDirectories = specDirs.filter(Files::isDirectory).collect(Collectors.toList());\n\n      if (specDirectories.isEmpty()) {\n        throw new WorkloadLoadException(\"No spec directories found in \" + specsDir);\n      }\n\n      return specDirectories;\n    } catch (IOException e) {\n      throw new WorkloadLoadException(\"Failed to list spec directories in \" + specsDir, e);\n    }\n  }\n\n  /** Loads a single workload specification from a spec directory. */\n  private static WorkloadSpec loadSingleSpec(Path specDir, TableInfo tableInfo) {\n    Path specFile = specDir.resolve(SPEC_FILE_NAME);\n\n    if (!Files.exists(specFile) || !Files.isRegularFile(specFile)) {\n      throw new WorkloadLoadException(\"Spec file not found: \" + specFile);\n    }\n\n    try {\n      String specName = specDir.getFileName().toString();\n      WorkloadSpec workloadSpec =\n          WorkloadSpec.fromJsonPath(specFile.toString(), specName, tableInfo);\n\n      return workloadSpec;\n\n    } catch (Exception e) {\n      throw new WorkloadLoadException(\"Failed to parse spec file: \" + specFile, e);\n    }\n  }\n\n  /** Custom exception for workload loading errors. */\n  public static class WorkloadLoadException extends RuntimeException {\n    public WorkloadLoadException(String message) {\n      super(message);\n    }\n\n    public WorkloadLoadException(String message, Throwable cause) {\n      super(message, cause);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/KernelMetricsProfiler.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks;\n\nimport io.delta.kernel.engine.*;\nimport io.delta.kernel.metrics.MetricsReport;\nimport io.delta.kernel.metrics.ScanMetricsResult;\nimport io.delta.kernel.metrics.ScanReport;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.openjdk.jmh.infra.BenchmarkParams;\nimport org.openjdk.jmh.infra.IterationParams;\nimport org.openjdk.jmh.profile.InternalProfiler;\nimport org.openjdk.jmh.results.*;\nimport org.openjdk.jmh.util.SampleBuffer;\n\n/**\n * JMH profiler that extracts and reports Delta Kernel metrics during benchmark execution.\n *\n * <p>This profiler collects metrics reports from the Delta Kernel during benchmark runs and\n * converts them into JMH secondary results for analysis. It works by wrapping the benchmark engine\n * with a {@link BenchmarkingEngine} that captures all metrics reports.\n *\n * <p>The profiler extracts various scan metrics including planning duration, file counts, and other\n * performance-related measurements that can be analyzed alongside the primary benchmark timing\n * results.\n */\npublic class KernelMetricsProfiler implements InternalProfiler {\n\n  /**\n   * Creates a new KernelMetricsProfiler instance.\n   *\n   * <p>This constructor is called by JMH when the profiler is registered via {@code\n   * addProfiler(KernelMetricsProfiler.class)}. No initialization is needed as the profiler uses\n   * static state to collect metrics across all benchmark iterations.\n   */\n  public KernelMetricsProfiler() {}\n\n  public static final List<MetricsReport> reports = new ArrayList<>();\n\n  /**\n   * Adds a metrics report to the collection for processing during benchmark execution.\n   *\n   * @param newReport the metrics report to add\n   */\n  public static void addReport(MetricsReport newReport) {\n    reports.add(newReport);\n  }\n\n  @Override\n  public String getDescription() {\n    return \"Extracts metrics from the Delta Kernel metrics reports.\";\n  }\n\n  @Override\n  public void beforeIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams) {\n    reports.clear();\n  }\n\n  /**\n   * Generates a secondary scalar result with average aggregation policy for count metrics.\n   *\n   * @param name the name of the metric\n   * @param value the metric value\n   * @return a ScalarResult configured as a secondary metric for count data with average aggregation\n   */\n  private static ScalarResult generateAvgScalarCount(String name, double value) {\n    return new ScalarResult(name, value, \"count\", AggregationPolicy.AVG);\n  }\n\n  /**\n   * Generates a secondary time sample result for timing metrics.\n   *\n   * @param name the name of the timing metric\n   * @param value the timing value\n   * @param unit the time unit for the value\n   * @return a SampleTimeResult configured as a secondary metric for timing data\n   */\n  private static SampleTimeResult generateTimeSample(String name, long value, TimeUnit unit) {\n    SampleBuffer buf = new SampleBuffer();\n    buf.add(value);\n    return new SampleTimeResult(ResultRole.SECONDARY, name, buf, unit);\n  }\n\n  /**\n   * Extracts scan metrics from a scan report and converts them to JMH secondary results.\n   *\n   * @param report the scan report containing metrics to extract\n   * @return a stream of JMH secondary Result objects representing the extracted scan metrics\n   */\n  private Stream<? extends Result> extractSecondaryScanMetrics(ScanReport report) {\n    ScanMetricsResult scanReport = report.getScanMetrics();\n    Stream<? extends Result> out =\n        Stream.of(\n            generateTimeSample(\n                \"scan.scan_metrics.total_planning_duration_ns\",\n                scanReport.getTotalPlanningDurationNs(),\n                TimeUnit.NANOSECONDS),\n            generateAvgScalarCount(\n                \"scan.scan_metrics.num_active_add_files\", scanReport.getNumActiveAddFiles()),\n            generateAvgScalarCount(\n                \"scan.scan_metrics.num_add_files_seen\", scanReport.getNumAddFilesSeen()),\n            generateAvgScalarCount(\n                \"scan.scan_metrics.num_add_files_seen_from_delta_files\",\n                scanReport.getNumAddFilesSeenFromDeltaFiles()),\n            generateAvgScalarCount(\n                \"scan.scan_metrics.num_duplicate_add_files\", scanReport.getNumDuplicateAddFiles()),\n            generateAvgScalarCount(\n                \"scan.scan_metrics.num_remove_files_seen_from_delta_files\",\n                scanReport.getNumRemoveFilesSeenFromDeltaFiles()));\n\n    return out;\n  }\n\n  @Override\n  public Collection<? extends Result> afterIteration(\n      BenchmarkParams benchmarkParams, IterationParams iterationParams, IterationResult result) {\n    Stream<? extends Result> out = Stream.empty();\n    for (MetricsReport report : reports) {\n      if (report instanceof ScanReport) {\n        out = Stream.concat(out, extractSecondaryScanMetrics((ScanReport) report));\n      }\n    }\n    return out.collect(Collectors.toList());\n  }\n\n  /**\n   * An {@link Engine} implementation that wraps an existing engine and delegates all engine tasks.\n   * The BenchmarkingEngine sends all metrics reports to the KernelMetricsProfiler for collection\n   * during benchmarks. The metrics reports can then be extracted and reported by the\n   * KernelMetricsProfiler.\n   */\n  public static class BenchmarkingEngine implements Engine {\n    private final Engine delegate;\n    static final BenchmarkMetricsReporter BENCHMARK_METRICS_REPORTER =\n        new BenchmarkMetricsReporter();\n\n    /**\n     * Creates a new BenchmarkingEngine that wraps the provided engine.\n     *\n     * @param delegate the engine to wrap for metrics collection\n     */\n    BenchmarkingEngine(Engine delegate) {\n      this.delegate = delegate;\n    }\n\n    @Override\n    public ExpressionHandler getExpressionHandler() {\n      return delegate.getExpressionHandler();\n    }\n\n    @Override\n    public JsonHandler getJsonHandler() {\n      return delegate.getJsonHandler();\n    }\n\n    @Override\n    public FileSystemClient getFileSystemClient() {\n      return delegate.getFileSystemClient();\n    }\n\n    @Override\n    public ParquetHandler getParquetHandler() {\n      return delegate.getParquetHandler();\n    }\n\n    @Override\n    public List<MetricsReporter> getMetricsReporters() {\n      return Collections.singletonList(BENCHMARK_METRICS_REPORTER);\n    }\n\n    /**\n     * Wraps an engine with benchmarking metrics collection capabilities.\n     *\n     * @param engine the engine to wrap\n     * @return a BenchmarkingEngine that collects metrics from the wrapped engine\n     */\n    public static BenchmarkingEngine wrapEngine(Engine engine) {\n      return new BenchmarkingEngine(engine);\n    }\n\n    /** Metrics reporter implementation that forwards all metrics reports to the profiler. */\n    private static final class BenchmarkMetricsReporter implements MetricsReporter {\n      @Override\n      public void report(MetricsReport report) {\n        addReport(report);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/WorkloadBenchmark.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks;\n\nimport static io.delta.kernel.benchmarks.BenchmarkUtils.*;\n\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport io.delta.kernel.benchmarks.workloadrunners.WorkloadRunner;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.*;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport org.apache.hadoop.conf.Configuration;\nimport org.openjdk.jmh.annotations.*;\nimport org.openjdk.jmh.infra.Blackhole;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport org.openjdk.jmh.runner.options.TimeValue;\n\n/**\n * Generic JMH benchmark for all workload types. Automatically loads and runs benchmarks based on\n * JSON workload specifications.\n */\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MILLISECONDS)\n@Fork(value = 1, warmups = 1)\n@Warmup(iterations = 3, time = 1)\n@Measurement(iterations = 5, time = 1)\npublic class WorkloadBenchmark<T> {\n\n  /** Default implementation of BenchmarkState that supports only the \"default\" engine. */\n  public static class DefaultBenchmarkState extends AbstractBenchmarkState {\n    @Override\n    protected Engine getEngine(String engineName) {\n      if (engineName.equals(\"default\")) {\n        return DefaultEngine.create(new Configuration());\n      } else {\n        throw new IllegalArgumentException(\"Unsupported engine: \" + engineName);\n      }\n    }\n  }\n\n  /**\n   * Benchmark method that executes the workload runner specified in the state as a benchmark.\n   *\n   * @param state The benchmark state containing the workload runner to execute.\n   * @param blackhole The Blackhole provided by JMH to consume results and prevent dead code\n   *     elimination.\n   * @throws Exception If any error occurs during workload execution.\n   */\n  @Benchmark\n  public void benchmarkWorkload(DefaultBenchmarkState state, Blackhole blackhole) throws Exception {\n    WorkloadRunner runner = state.getRunner();\n    runner.executeAsBenchmark(blackhole);\n  }\n\n  /**\n   * TODO: In the future, this can be extracted so that new benchmarks with custom BenchmarkStates\n   * can be easily constructed.\n   */\n  public static void main(String[] args) throws RunnerException, IOException {\n    // Get workload specs from the workloads directory\n    List<WorkloadSpec> workloadSpecs = BenchmarkUtils.loadAllWorkloads(WORKLOAD_SPECS_DIR);\n    if (workloadSpecs.isEmpty()) {\n      throw new RunnerException(\n          \"No workloads found. Please add workload specs to the workloads directory.\");\n    }\n\n    // Parse the Json specs from the json paths\n    List<WorkloadSpec> filteredSpecs = new ArrayList<>();\n    for (WorkloadSpec spec : workloadSpecs) {\n      // TODO(#5420): In the future, we can filter specific workloads using command line args here.\n      filteredSpecs.addAll(spec.getWorkloadVariants());\n    }\n\n    // Convert paths into a String array for JMH. JMH requires that parameters be of type String[].\n    String[] workloadSpecsArray =\n        filteredSpecs.stream().map(WorkloadSpec::toJsonString).toArray(String[]::new);\n\n    // Configure and run JMH benchmark with the loaded workload specs\n    Options opt =\n        new OptionsBuilder()\n            .include(WorkloadBenchmark.class.getSimpleName())\n            .shouldFailOnError(true)\n            .param(\"workloadSpecJson\", workloadSpecsArray)\n            // TODO: In the future, this can be extended to support multiple engines.\n            .param(\"engineName\", \"default\")\n            // TODO(#5420): Allow configuring forks, warmup, and measurement via command line args.\n            .forks(1)\n            .warmupIterations(3) // Proper warmup for production benchmarks\n            .measurementIterations(5) // Proper measurement iterations for production benchmarks\n            .warmupTime(TimeValue.seconds(1))\n            .measurementTime(TimeValue.seconds(1))\n            .addProfiler(KernelMetricsProfiler.class)\n            .build();\n\n    new Runner(opt, new WorkloadOutputFormat()).run();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/WorkloadOutputFormat.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport org.openjdk.jmh.infra.BenchmarkParams;\nimport org.openjdk.jmh.infra.IterationParams;\nimport org.openjdk.jmh.results.*;\nimport org.openjdk.jmh.results.BenchmarkResult;\nimport org.openjdk.jmh.results.IterationResult;\nimport org.openjdk.jmh.results.Result;\nimport org.openjdk.jmh.results.RunResult;\nimport org.openjdk.jmh.runner.format.OutputFormat;\nimport org.openjdk.jmh.runner.format.OutputFormatFactory;\nimport org.openjdk.jmh.runner.options.VerboseMode;\nimport org.openjdk.jmh.util.Statistics;\n\n/**\n * Custom JMH output format that generates structured JSON benchmark reports.\n *\n * <p>This output format captures benchmark results and generates a comprehensive JSON report\n * containing execution environment details, benchmark configuration, timing metrics, and secondary\n * metrics. The report includes detailed percentile analysis and is written to the working directory\n * as {@code benchmark_report.json}.\n *\n * <p>This format also delegates to JMH's standard text output format to print progress during\n * benchmark execution.\n *\n * <p>The generated report structure includes:\n *\n * <ul>\n *   <li>Report metadata (generation time, JMH version, etc.)\n *   <li>Execution environment (JVM, OS, hardware details)\n *   <li>Benchmark configuration and parameters\n *   <li>Benchmark details (spec, additional params, timing metrics, secondary metrics)\n * </ul>\n */\npublic class WorkloadOutputFormat implements OutputFormat {\n  private final OutputFormat delegate =\n      OutputFormatFactory.createFormatInstance(System.out, VerboseMode.NORMAL);\n  private final Path outputPath =\n      Paths.get(System.getProperty(\"user.dir\"), \"benchmark_report.json\");\n\n  private static final double[] PERCENTILES = {0.5, 0.9, 0.95, 0.99, 0.999, 0.9999, 1.0};\n\n  /** Metadata about the benchmark report itself. Json formatted */\n  private static class ReportMetadata {\n    @JsonProperty(\"generated_at\")\n    private final String generated_at;\n\n    @JsonProperty(\"jmh_version\")\n    private final String jmh_version;\n\n    @JsonProperty(\"report_version\")\n    private final String report_version;\n\n    @JsonProperty(\"benchmark_suite\")\n    private final String benchmark_suite;\n\n    ReportMetadata(\n        String generated_at, String jmh_version, String report_version, String benchmark_suite) {\n      this.generated_at = generated_at;\n      this.jmh_version = jmh_version;\n      this.report_version = report_version;\n      this.benchmark_suite = benchmark_suite;\n    }\n\n    public String toString() {\n      return String.format(\n          \"ReportMetadata{generated_at='%s', jmh_version='%s',\"\n              + \" report_version='%s', benchmark_suite='%s'}\",\n          generated_at, jmh_version, report_version, benchmark_suite);\n    }\n  }\n\n  public static class ExecutionEnvironment {\n    @JsonProperty(\"jvm\")\n    private final String jvm;\n\n    @JsonProperty(\"heap_size_mb\")\n    private final String heapSizeMB;\n\n    @JsonProperty(\"jdk_version\")\n    private final String jdk_version;\n\n    @JsonProperty(\"vm_name\")\n    private final String vm_name;\n\n    @JsonProperty(\"vm_version\")\n    private final String vm_version;\n\n    @JsonProperty(\"cpu_model\")\n    private final String cpuModel;\n\n    @JsonProperty(\"cpu_arch\")\n    private final String cpuArch;\n\n    @JsonProperty(\"cpu_cores\")\n    private final Long cpuCores;\n\n    @JsonProperty(\"os_name\")\n    private final String osName;\n\n    @JsonProperty(\"os_version\")\n    private final String osVersion;\n\n    @JsonProperty(\"max_memory_mb\")\n    private final Long maxMemoryMb;\n\n    public ExecutionEnvironment() {\n      this.jvm = System.getProperty(\"java.vm.name\");\n      this.heapSizeMB = Runtime.getRuntime().maxMemory() / (1024 * 1024) + \" MB\";\n      this.jdk_version = System.getProperty(\"java.version\");\n      this.vm_name = System.getProperty(\"java.vm.name\");\n      this.vm_version = System.getProperty(\"java.vm.version\");\n      this.cpuModel = System.getProperty(\"os.arch\");\n      this.cpuArch = System.getProperty(\"os.arch\");\n      this.cpuCores = (long) Runtime.getRuntime().availableProcessors();\n      this.osName = System.getProperty(\"os.name\");\n      this.osVersion = System.getProperty(\"os.version\");\n      this.maxMemoryMb = Runtime.getRuntime().maxMemory() / (1024 * 1024);\n    }\n\n    public String toString() {\n      return String.format(\n          \"ExecutionEnvironment{jvm='%s', heapSizeMB='%s', jdk_version='%s',\"\n              + \" vm_name='%s', vm_version='%s', cpuModel='%s', cpuArch='%s',\"\n              + \" cpuCores=%d, osName='%s', osVersion='%s'}\",\n          jvm,\n          heapSizeMB,\n          jdk_version,\n          vm_name,\n          vm_version,\n          cpuModel,\n          cpuArch,\n          cpuCores,\n          osName,\n          osVersion);\n    }\n  }\n\n  private static class BenchmarkDetails {\n    @JsonProperty(\"spec\")\n    private WorkloadSpec spec;\n\n    @JsonProperty(\"additional_params\")\n    private HashMap<String, String> additionalParams;\n\n    @JsonProperty(\"time\")\n    private TimingMetric time;\n\n    @JsonProperty(\"secondary_metrics\")\n    private HashMap<String, Object> secondary_metrics;\n\n    BenchmarkDetails(\n        WorkloadSpec spec,\n        HashMap<String, String> additionalParams,\n        TimingMetric time,\n        HashMap<String, Object> secondary_metrics) {\n      this.spec = spec;\n      this.additionalParams = additionalParams;\n      this.time = time;\n      this.secondary_metrics = secondary_metrics;\n    }\n\n    public String toString() {\n      return String.format(\n          \"BenchmarkDetails{spec=%s, additionalParams=%s, time=%s, secondary_metrics=%s}\",\n          spec, additionalParams.toString(), time.toString(), secondary_metrics.toString());\n    }\n  }\n\n  private static class BenchmarkReport {\n    @JsonProperty(\"report_metadata\")\n    private ReportMetadata reportMetadata;\n\n    @JsonProperty(\"execution_environment\")\n    private ExecutionEnvironment executionEnvironment;\n\n    @JsonProperty(\"benchmark_configuration\")\n    private HashMap<String, String> benchmarkConfiguration;\n\n    @JsonProperty(\"benchmarks\")\n    private HashMap<String, BenchmarkDetails> benchmarks;\n\n    BenchmarkReport(\n        ReportMetadata reportMetadata,\n        ExecutionEnvironment executionEnvironment,\n        HashMap<String, String> benchmarkConfiguration,\n        HashMap<String, BenchmarkDetails> benchmarks) {\n      this.reportMetadata = reportMetadata;\n      this.executionEnvironment = executionEnvironment;\n      this.benchmarkConfiguration = benchmarkConfiguration;\n      this.benchmarks = benchmarks;\n    }\n\n    public String toString() {\n      return String.format(\n          \"BenchmarkReport{reportMetadata=%s, executionEnvironment=%s,\"\n              + \" benchmarkConfiguration=%s, benchmarks=%s}\",\n          reportMetadata.toString(),\n          executionEnvironment.toString(),\n          benchmarkConfiguration.toString(),\n          benchmarks.toString());\n    }\n  }\n\n  private static class TimingMetric {\n    @JsonProperty(\"score\")\n    private final double score;\n\n    @JsonProperty(\"score_unit\")\n    private final String score_unit;\n\n    @JsonProperty(\"score_error\")\n    private final double score_error;\n\n    @JsonProperty(\"score_confidence\")\n    private final double[] score_confidence;\n\n    @JsonProperty(\"sample_count\")\n    private final long sample_count;\n\n    @JsonProperty(\"percentiles\")\n    private final HashMap<String, Double> percentiles;\n\n    TimingMetric(\n        double score,\n        String score_unit,\n        double score_error,\n        double[] score_confidence,\n        long sample_count,\n        HashMap<String, Double> percentiles) {\n      this.score = score;\n      this.score_unit = score_unit;\n      this.score_error = score_error;\n      this.score_confidence = score_confidence;\n      this.sample_count = sample_count;\n      this.percentiles = percentiles;\n    }\n\n    public static TimingMetric fromResult(Result result) {\n      HashMap<String, Double> percentiles = new HashMap<>();\n      Statistics stats = result.getStatistics();\n      for (double p : PERCENTILES) {\n        String key = String.format(\"p%.2f\", p);\n        percentiles.put(key, stats.getPercentile(p));\n      }\n      return new TimingMetric(\n          result.getScore(),\n          result.getScoreUnit(),\n          result.getScoreError(),\n          result.getScoreConfidence(),\n          result.getSampleCount(),\n          percentiles);\n    }\n\n    public String toString() {\n      return String.format(\n          \"TimingMetric{score=%f, score_unit='%s', score_error=%f,\"\n              + \" score_confidence=[%s], sample_count=%d, percentiles=%s}\",\n          score,\n          score_unit,\n          score_error,\n          String.join(\n              \", \",\n              new String[] {\n                String.valueOf(score_confidence[0]), String.valueOf(score_confidence[1])\n              }),\n          sample_count,\n          percentiles.toString());\n    }\n  }\n\n  @Override\n  public void iteration(BenchmarkParams benchParams, IterationParams params, int iteration) {\n    delegate.iteration(benchParams, params, iteration);\n  }\n\n  @Override\n  public void iterationResult(\n      BenchmarkParams benchParams, IterationParams params, int iteration, IterationResult data) {\n    delegate.iterationResult(benchParams, params, iteration, data);\n  }\n\n  @Override\n  public void startBenchmark(BenchmarkParams benchParams) {\n    delegate.startBenchmark(benchParams);\n  }\n\n  @Override\n  public void endBenchmark(BenchmarkResult result) {\n    delegate.endBenchmark(result);\n  }\n\n  @Override\n  public void startRun() {\n    delegate.startRun();\n  }\n\n  @Override\n  public void endRun(Collection<RunResult> result) {\n    println(\"\\n=== Generating JSON Benchmark Report ===\");\n    ReportMetadata metadata =\n        new ReportMetadata(\n            String.valueOf(System.currentTimeMillis()),\n            org.openjdk.jmh.Main.class.getPackage().getImplementationVersion(),\n            \"1.0\",\n            \"Delta Kernel Benchmarks\");\n    ExecutionEnvironment env = new ExecutionEnvironment();\n    HashMap<String, String> benchConfig = new HashMap<>();\n\n    HashMap<String, BenchmarkDetails> benchmarks = new HashMap<>();\n\n    for (RunResult res : result) {\n      BenchmarkResult br = res.getAggregatedResult();\n      try {\n        WorkloadSpec spec =\n            WorkloadSpec.fromJsonString(br.getParams().getParam(\"workloadSpecJson\"));\n        HashMap<String, String> additionalParams = new HashMap<>();\n        additionalParams.put(\"engine\", br.getParams().getParam(\"engineName\"));\n\n        HashMap<String, Object> secondaryMetrics = new HashMap<>();\n        for (String resultKey : br.getSecondaryResults().keySet()) {\n          Result r = br.getSecondaryResults().get(resultKey);\n          if (r instanceof org.openjdk.jmh.results.SampleTimeResult) {\n            secondaryMetrics.put(r.getLabel(), TimingMetric.fromResult(r));\n          } else if (r instanceof org.openjdk.jmh.results.ScalarResult) {\n            ScalarResult scalarResult = (ScalarResult) r;\n            if (scalarResult.getScoreUnit().equals(\"count\")) {\n              // Convert count metrics to long integers to avoid decimal representation in JSON\n              // output (e.g., report \"42\" instead of \"42.0\" for file counts)\n              secondaryMetrics.put(r.getLabel(), (long) r.getScore());\n            } else {\n              secondaryMetrics.put(r.getLabel(), r.getScore());\n            }\n          }\n        }\n\n        BenchmarkDetails details =\n            new BenchmarkDetails(\n                spec,\n                additionalParams,\n                TimingMetric.fromResult(br.getPrimaryResult()),\n                secondaryMetrics);\n        benchmarks.put(spec.getFullName(), details);\n      } catch (IOException e) {\n        throw new RuntimeException(e);\n      }\n    }\n\n    BenchmarkReport report = new BenchmarkReport(metadata, env, benchConfig, benchmarks);\n\n    // Write report to user.dir\n    ObjectMapper mapper = new ObjectMapper();\n    try {\n      String jsonReport = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(report);\n\n      println(\"Generated benchmark report:\\n\" + jsonReport);\n      println(\"Writing benchmark report to \" + outputPath);\n\n      Files.write(outputPath, jsonReport.getBytes());\n    } catch (IOException e) {\n      throw new RuntimeException(\"Failed to serialize benchmark report to JSON\", e);\n    }\n  }\n\n  @Override\n  public void print(String s) {\n    delegate.print(s);\n  }\n\n  @Override\n  public void println(String s) {\n    delegate.println(s);\n  }\n\n  @Override\n  public void flush() {\n    delegate.flush();\n  }\n\n  @Override\n  public void close() {\n    delegate.close();\n  }\n\n  @Override\n  public void verbosePrintln(String s) {\n    delegate.verbosePrintln(s);\n  }\n\n  @Override\n  public void write(int b) {\n    delegate.write(b);\n  }\n\n  @Override\n  public void write(byte[] b) throws IOException {\n    delegate.write(b);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/models/ReadSpec.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.models;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.delta.kernel.benchmarks.workloadrunners.ReadMetadataRunner;\nimport io.delta.kernel.benchmarks.workloadrunners.WorkloadRunner;\nimport io.delta.kernel.engine.Engine;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Workload specification for read benchmarks. Defines test cases for reading Delta tables at\n * specific versions or latest, with different operation types.\n *\n * <h2>Usage</h2>\n *\n * <p>To run this workload, use {@link WorkloadSpec#getRunner(Engine)} to get the appropriate {@link\n * ReadMetadataRunner} or ReadDataRunner based on the operation type.\n *\n * @see ReadMetadataRunner\n */\npublic class ReadSpec extends WorkloadSpec {\n\n  /** The snapshot version to read. If null, the latest version will be read. From spec file. */\n  @JsonProperty(\"version\")\n  private Long version;\n\n  /**\n   * The operation type for this variant (\"read_metadata\" or \"read_data\"). Set during variant\n   * generation via getWorkloadVariants(), not present in spec files.\n   */\n  @JsonProperty(\"operation_type\")\n  private String operationType;\n\n  // Default constructor for Jackson\n  public ReadSpec() {\n    super(\"read\");\n  }\n\n  // Copy constructor\n  public ReadSpec(TableInfo tableInfo, String caseName, Long version, String operationType) {\n    super(\"read\");\n    this.tableInfo = tableInfo;\n    this.version = version;\n    this.caseName = caseName;\n    this.operationType = operationType;\n  }\n\n  /** @return the snapshot version to read, or null if the latest version should be read. */\n  public Long getVersion() {\n    return version;\n  }\n\n  /**\n   * @return the full name of this workload, derived from table name, case name, and operation type.\n   */\n  @Override\n  public String getFullName() {\n    return this.tableInfo.name + \"/\" + caseName + \"/read/\" + operationType;\n  }\n\n  @Override\n  public WorkloadRunner getRunner(Engine engine) {\n    if (operationType.equals(\"read_metadata\")) {\n      return new ReadMetadataRunner(this, engine);\n    } else {\n      throw new IllegalArgumentException(\"Unsupported operation for ReadSpec: \" + operationType);\n    }\n  }\n\n  /**\n   * Generates workload variants from this test case specification.\n   *\n   * <p>A single ReadSpec test case can generate multiple workload variants, one for each operation\n   * type. Each variant is a complete ReadSpec with the operation type set.\n   *\n   * @return list of ReadSpec variants, each representing a separate workload execution\n   */\n  @Override\n  public List<WorkloadSpec> getWorkloadVariants() {\n    // TODO: In the future, we will support the read_data operation as well.\n    String[] operationTypes = {\"read_metadata\"};\n    List<WorkloadSpec> out = new ArrayList<>();\n    for (String opType : operationTypes) {\n      ReadSpec specVariant = new ReadSpec(tableInfo, caseName, version, opType);\n      out.add(specVariant);\n    }\n    return out;\n  }\n\n  /** @return the operation type (\"read_metadata\" or \"read_data\") */\n  public String getOperationType() {\n    return operationType;\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"Read{caseName='%s', operationType='%s', snapshotVersion=%s, tableInfo='%s'}\",\n        caseName, operationType, version, tableInfo);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/models/SnapshotConstructionSpec.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.models;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.delta.kernel.benchmarks.workloadrunners.SnapshotConstructionRunner;\nimport io.delta.kernel.benchmarks.workloadrunners.WorkloadRunner;\nimport io.delta.kernel.engine.Engine;\n\npublic class SnapshotConstructionSpec extends WorkloadSpec {\n\n  /** The snapshot version to read. If null, the latest version will be read. From spec file. */\n  @JsonProperty(\"version\")\n  private Long version;\n\n  // Default constructor for Jackson\n  public SnapshotConstructionSpec() {\n    super(\"snapshot_construction\");\n  }\n\n  /** @return the snapshot version to read, or null if the latest version should be read. */\n  public Long getVersion() {\n    return version;\n  }\n\n  @Override\n  public WorkloadRunner getRunner(Engine engine) {\n    return new SnapshotConstructionRunner(this, engine);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/models/TableInfo.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.models;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Paths;\nimport java.util.Optional;\n\n/**\n * Represents metadata about a Delta table used in benchmark workloads.\n *\n * <p>This class contains information about a Delta table that is used by benchmark workloads to\n * locate and access the table data. It includes the table name, description, the root path where\n * the table is stored, and optional engine information.\n *\n * <p>TableInfo instances are typically loaded from JSON files in the workload specifications\n * directory structure. Each table directory should contain a {@code table_info.json} file with the\n * table metadata and a {@code delta} subdirectory containing the actual table data. The table root\n * path is the absolute path to the root of the table and is provided separately in {@link\n * WorkloadSpec#fromJsonPath(String, String, TableInfo)}.\n *\n * <p>Example JSON structure:\n *\n * <pre>{@code\n * {\n *   \"name\": \"large-table\",\n *   \"description\": \"A large Delta table with multi-part checkpoints for performance testing\",\n *   \"engineInfo\": \"Apache-Spark/3.5.1 Delta-Lake/3.1.0\"\n * }\n * }</pre>\n */\npublic class TableInfo {\n  /** The name of the table, used for identification in benchmark reports. */\n  @JsonProperty(\"name\")\n  public String name;\n\n  /** A human-readable description of the table and its purpose. */\n  @JsonProperty(\"description\")\n  public String description;\n\n  /**\n   * Information about the engine used to create this table (e.g., \"Apache-Spark/3.5.1\n   * Delta-Lake/3.1.0\"). Optional field to track which engine/version created the table data.\n   */\n  @JsonProperty(\"engine_info\")\n  public String engineInfo;\n\n  /** The path to the table_info directory */\n  @JsonProperty(\"table_info_path\")\n  private String tableInfoPath;\n\n  /**\n   * Whether this table is a Unity Catalog managed table. If true, the UC Catalog info is loaded\n   * from a fixed path: catalog_managed_info.json in the same directory as table_info.json.\n   */\n  @JsonProperty(\"is_catalog_managed\")\n  private boolean isCatalogManaged;\n\n  /**\n   * Lazily loaded Unity Catalog information. This is populated when {@link #getUcCatalogInfo()} is\n   * called for the first time.\n   */\n  @JsonIgnore private Optional<UcCatalogInfo> ucCatalogInfo = Optional.empty();\n\n  /**\n   * Default constructor for Jackson deserialization.\n   *\n   * <p>This constructor is required for Jackson to deserialize JSON into TableInfo objects. All\n   * fields should be set via Jackson annotations or setter methods.\n   */\n  public TableInfo() {}\n\n  /** Resolves the table root path based on the table type and location configuration. */\n  @JsonIgnore\n  public String getResolvedTableRoot() {\n    return Paths.get(tableInfoPath, \"delta\").toAbsolutePath().toString();\n  }\n\n  public String getTableInfoPath() {\n    return tableInfoPath;\n  }\n\n  public void setTableInfoPath(String tableInfoDirectory) {\n    this.tableInfoPath = tableInfoDirectory;\n  }\n\n  /**\n   * Checks if this table is a Unity Catalog managed table.\n   *\n   * @return true if is_catalog_managed is true, false otherwise\n   */\n  @JsonIgnore\n  public boolean isCatalogManaged() {\n    return isCatalogManaged;\n  }\n\n  /**\n   * Gets the Unity Catalog information for this table. Lazily loads the UcCatalogInfo from\n   * catalog_managed_info.json in the same directory as table_info.json if not already loaded.\n   *\n   * @return the UcCatalogInfo for this table\n   * @throws IllegalStateException if this is not a Unity Catalog managed table\n   * @throws RuntimeException if there is an error loading the UcCatalogInfo\n   */\n  @JsonIgnore\n  public UcCatalogInfo getUcCatalogInfo() {\n    if (!isCatalogManaged()) {\n      throw new IllegalStateException(\n          \"This is not a Unity Catalog managed table. is_catalog_managed is not set to true in table_info.json\");\n    }\n\n    // If ucCatalogInfo is not cached, load it from catalog_managed_info.json\n    if (!ucCatalogInfo.isPresent()) {\n      String catalogManagedInfoFullPath =\n          Paths.get(tableInfoPath, \"catalog_managed_info.json\").toString();\n      try {\n        ucCatalogInfo = Optional.of(UcCatalogInfo.fromJsonPath(catalogManagedInfoFullPath));\n      } catch (java.io.IOException e) {\n        throw new RuntimeException(\n            \"Failed to load UcCatalogInfo from: \" + catalogManagedInfoFullPath, e);\n      }\n    }\n\n    return ucCatalogInfo.get();\n  }\n\n  /**\n   * Creates a TableInfo instance by reading from a JSON file at the specified path.\n   *\n   * <p>This method loads table metadata from a JSON file and sets the table root path. The JSON\n   * file should contain the table name and description, while the table root path is provided\n   * separately with the absolute path.\n   *\n   * @param jsonPath the path to the JSON file containing the TableInfo metadata\n   * @param tableInfoPath the directory containing the table_info.json file\n   * @return a TableInfo instance populated from the JSON file and table root path\n   * @throws RuntimeException if there is an error reading or parsing the JSON file\n   */\n  public static TableInfo fromJsonPath(String jsonPath, String tableInfoPath) {\n    ObjectMapper mapper = new ObjectMapper();\n    try {\n      TableInfo info = mapper.readValue(new File(jsonPath), TableInfo.class);\n      info.setTableInfoPath(tableInfoPath);\n      return info;\n    } catch (IOException e) {\n      throw new RuntimeException(\"Failed to read TableInfo from JSON file: \" + jsonPath, e);\n    }\n  }\n\n  /**\n   * Returns a string representation of this TableInfo.\n   *\n   * <p>The string includes the table name, description, and engine info, but excludes the table\n   * root path for security reasons (as it may contain sensitive path information).\n   *\n   * @return a string representation of this TableInfo\n   */\n  @Override\n  public String toString() {\n    return \"TableInfo{name='\"\n        + name\n        + \"', description='\"\n        + description\n        + \"', engineInfo='\"\n        + engineInfo\n        + \"'}\";\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/models/UcCatalogInfo.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.models;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.unitycatalog.InMemoryUCClient;\nimport io.delta.kernel.unitycatalog.UCCatalogManagedCommitter;\nimport io.delta.kernel.utils.FileStatus;\nimport io.delta.storage.commit.Commit;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport scala.collection.mutable.ArrayBuffer;\n\n/**\n * Configuration for Unity catalog managed tables used in benchmarks.\n *\n * <p>Contains information about staged commits in the `_delta_log/_staged_commits/` directory.\n *\n * <p>Example JSON structure:\n *\n * <pre>{@code\n * {\n *   \"uc_table_id\": \"12345678-1234-1234-1234-123456789abc\",\n *   \"max_ratified_version\": 2,\n *   \"log_tail\": [\n *     {\n *       \"staged_commit_file_name\": \"00000000000000000002.a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.json\"\n *     }\n *   ]\n * }\n * }</pre>\n */\npublic class UcCatalogInfo {\n\n  /**\n   * A single staged commit in the log tail.\n   *\n   * <p>Each staged commit is a JSON file in `_delta_log/_staged_commits/` containing Delta log\n   * actions.\n   */\n  public static class StagedCommit {\n    /**\n     * The filename of the staged commit, relative to `_delta_log/_staged_commits/`.\n     *\n     * <p>Example: \"00000000000000000002.a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.json\"\n     */\n    @JsonProperty(\"staged_commit_file_name\")\n    private String stagedCommitFileName;\n\n    /** Default constructor for Jackson deserialization. */\n    public StagedCommit() {}\n\n    /**\n     * Constructor for creating a StagedCommit.\n     *\n     * @param stagedCommitFileName the filename of the staged commit\n     */\n    public StagedCommit(String stagedCommitFileName) {\n      this.stagedCommitFileName = stagedCommitFileName;\n    }\n\n    /**\n     * Gets the version number from the filename.\n     *\n     * @return the version number\n     */\n    @JsonIgnore\n    public long getVersion() {\n      return FileNames.deltaVersion(stagedCommitFileName);\n    }\n\n    /** @return the filename of the staged commit */\n    public String getStagedCommitFileName() {\n      return stagedCommitFileName;\n    }\n\n    /**\n     * Gets the full path to the staged commit file.\n     *\n     * @param tableRoot the root path of the Delta table\n     * @return the full path\n     */\n    public String getFullPath(String tableRoot) {\n      Path logPath = new Path(tableRoot, \"_delta_log\");\n      Path stagedCommitsDir = new Path(logPath, FileNames.STAGED_COMMIT_DIRECTORY);\n      return new Path(stagedCommitsDir, stagedCommitFileName).toUri().toString();\n    }\n\n    @Override\n    public String toString() {\n      return String.format(\n          \"StagedCommit{version=%d, stagedCommitFileName='%s'}\",\n          getVersion(), stagedCommitFileName);\n    }\n\n    public Commit toCommit(Engine engine, String tableRoot) throws IOException {\n      String stagedCommitPath = getFullPath(tableRoot);\n      FileStatus fileStatus = engine.getFileSystemClient().getFileStatus(stagedCommitPath);\n\n      // While catalog managed tables expect to use inCommitTimestamp, we use\n      // modification time here for simplicity.\n      long commitTimestamp = fileStatus.getModificationTime();\n\n      org.apache.hadoop.fs.FileStatus hadoopFileStatus =\n          UCCatalogManagedCommitter.kernelFileStatusToHadoopFileStatus(fileStatus);\n      return new Commit(getVersion(), hadoopFileStatus, commitTimestamp);\n    }\n  }\n\n  /**\n   * The list of staged commits for this table.\n   *\n   * <p>These commits are not yet backfilled to the regular Delta log.\n   */\n  @JsonProperty(value = \"log_tail\", required = true)\n  private List<StagedCommit> logTail;\n\n  /** The Unity Catalog table ID. */\n  @JsonProperty(value = \"uc_table_id\", required = true)\n  private String ucTableId;\n\n  /** The maximum ratified version for this table in Unity Catalog. */\n  @JsonProperty(value = \"max_ratified_version\", required = true)\n  private long maxRatifiedVersion;\n\n  /** Default constructor for Jackson deserialization. */\n  public UcCatalogInfo() {}\n\n  /**\n   * Creates a UcCatalogInfo with the given staged commits.\n   *\n   * @param logTail the list of staged commits\n   */\n  public UcCatalogInfo(List<StagedCommit> logTail) {\n    this.logTail = logTail;\n  }\n\n  /** @return the list of staged commits, or an empty list if none */\n  public List<StagedCommit> getLogTail() {\n    return logTail;\n  }\n\n  /** @return the Unity Catalog table ID */\n  public String getUcTableId() {\n    return ucTableId;\n  }\n\n  /**\n   * Creates an InMemoryUCClient for this table with the staged commits pre-loaded.\n   *\n   * @param engine the Engine to use for reading staged commit files\n   * @param tableRoot the root path of the Delta table\n   * @return an initialized InMemoryUCClient\n   * @throws IOException if there's an error reading staged commit files\n   */\n  public InMemoryUCClient createUCClient(Engine engine, String tableRoot) throws IOException {\n    ArrayBuffer<Commit> commits = new ArrayBuffer<>();\n    if (!logTail.isEmpty()) {\n      // Commits must be added to TableData in order of version. We sort staged commits by version.\n      List<StagedCommit> sortedLogTail =\n          logTail.stream()\n              .sorted(Comparator.comparingLong(StagedCommit::getVersion))\n              .collect(Collectors.toList());\n\n      for (StagedCommit stagedCommit : sortedLogTail) {\n        commits.addOne(stagedCommit.toCommit(engine, tableRoot));\n      }\n    }\n\n    InMemoryUCClient ucClient = new InMemoryUCClient(\"benchmark-metastore\");\n    InMemoryUCClient.TableData tableData =\n        new InMemoryUCClient.TableData(maxRatifiedVersion, commits);\n    ucClient.insertTableData(ucTableId, tableData);\n\n    return ucClient;\n  }\n\n  /**\n   * Loads a UcCatalogInfo from a JSON file.\n   *\n   * @param jsonPath the path to the JSON file\n   * @return the parsed UcCatalogInfo\n   * @throws IOException if there is an error reading the file\n   */\n  public static UcCatalogInfo fromJsonPath(String jsonPath) throws IOException {\n    ObjectMapper mapper = new ObjectMapper();\n    return mapper.readValue(new File(jsonPath), UcCatalogInfo.class);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"UcCatalogInfo{logTail=%s}\", logTail);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/models/WorkloadSpec.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.models;\n\nimport com.fasterxml.jackson.annotation.*;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.kernel.benchmarks.workloadrunners.WorkloadRunner;\nimport io.delta.kernel.engine.Engine;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Base class for all workload specifications. Workload specifications define test cases and their\n * parameters that can be executed as benchmarks using the corresponding {@link WorkloadRunner}.\n *\n * <p>This class uses Jackson annotations to support polymorphic deserialization based on the \"type\"\n * field in the JSON.\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n@JsonSubTypes({\n  @JsonSubTypes.Type(value = ReadSpec.class, name = \"read\"),\n  @JsonSubTypes.Type(value = SnapshotConstructionSpec.class, name = \"snapshot_construction\"),\n  @JsonSubTypes.Type(value = WriteSpec.class, name = \"write\")\n})\npublic abstract class WorkloadSpec {\n  /**\n   * The type of workload (e.g., \"read\"). This is used by Jackson's polymorphic deserialization to\n   * automatically instantiate the correct subclass based on the \"type\" field in the JSON.\n   */\n  protected String type;\n\n  @JsonProperty(\"table_info\")\n  protected TableInfo tableInfo;\n\n  @JsonProperty(\"case_name\")\n  protected String caseName;\n\n  private static final ObjectMapper objectMapper = new ObjectMapper();\n\n  /** Default constructor for Jackson */\n  protected WorkloadSpec() {}\n\n  protected WorkloadSpec(String type) {\n    this.type = type;\n  }\n\n  /** @return the type of this workload. */\n  @JsonIgnore\n  public String getType() {\n    return type;\n  }\n\n  /** @return the case name of this workload. */\n  public String getCaseName() {\n    return caseName;\n  }\n\n  @JsonProperty(value = \"full_name\", access = JsonProperty.Access.READ_ONLY)\n  public String getFullName() {\n    return tableInfo.name + \"/\" + caseName + \"/\" + type;\n  }\n\n  public void setCaseName(String caseName) {\n    this.caseName = caseName;\n  }\n\n  public TableInfo getTableInfo() {\n    return tableInfo;\n  }\n\n  @JsonIgnore\n  public String getSpecDirectoryPath() {\n    return tableInfo.getTableInfoPath() + \"/specs/\" + caseName;\n  }\n\n  /**\n   * Sets the table information for this workload specification.\n   *\n   * @param tableInfo the table information containing name, description, and root path\n   */\n  public void setTableInfo(TableInfo tableInfo) {\n    this.tableInfo = tableInfo;\n  }\n\n  /**\n   * Creates a WorkloadRunner for this workload specification.\n   *\n   * @param engine The engine to use for executing the workload.\n   * @return the WorkloadRunner instance for this workload specification.\n   */\n  public abstract WorkloadRunner getRunner(Engine engine);\n\n  /**\n   * Loads a WorkloadSpec from the given JSON file path.\n   *\n   * @param workloadPath the path to the JSON file containing the workload specification.\n   * @param caseName the name of the test case for this workload\n   * @param tableInfo the table information to associate with this workload\n   * @return the WorkloadSpec instance parsed from the JSON file.\n   * @throws IOException if there is an error reading or parsing the file.\n   */\n  public static WorkloadSpec fromJsonPath(String workloadPath, String caseName, TableInfo tableInfo)\n      throws IOException {\n\n    WorkloadSpec spec = objectMapper.readValue(new File(workloadPath), WorkloadSpec.class);\n    spec.setTableInfo(tableInfo);\n    spec.setCaseName(caseName);\n    return spec;\n  }\n\n  /**\n   * Generates workload variants from this test case specification.\n   *\n   * <p>A single WorkloadSpec can generate multiple workload variants. For example, a read spec\n   * might generate both read_metadata and read_data variants. Each variant can be executed as a\n   * benchmark or test.\n   *\n   * <p>The default implementation returns a single variant (itself). Subclasses should override to\n   * generate multiple variants if needed.\n   *\n   * @return list of WorkloadSpec variants, each representing a separate workload execution\n   */\n  @JsonIgnore\n  public List<WorkloadSpec> getWorkloadVariants() {\n    return Collections.singletonList(this);\n  }\n\n  /**\n   * Loads a WorkloadSpec from the given JSON string.\n   *\n   * @param json the JSON string containing the workload specification.\n   * @return the WorkloadSpec instance parsed from the JSON string.\n   * @throws IOException if there is an error parsing the JSON.\n   */\n  public static WorkloadSpec fromJsonString(String json) throws IOException {\n    return objectMapper.readValue(json, WorkloadSpec.class);\n  }\n\n  /**\n   * Serializes this WorkloadSpec to a pretty-printed JSON string.\n   *\n   * @return the JSON string representation of this WorkloadSpec.\n   */\n  public String toJsonString() {\n    try {\n      return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(this);\n    } catch (IOException e) {\n      throw new RuntimeException(\"Failed to serialize WorkloadSpec to JSON\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/models/WriteSpec.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.models;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.kernel.benchmarks.workloadrunners.WorkloadRunner;\nimport io.delta.kernel.benchmarks.workloadrunners.WriteRunner;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.DataFileStatus;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n * Workload specification for write benchmarks. Defines test cases for writing to Delta tables with\n * one or more commits containing add/remove actions.\n *\n * <h2>Usage</h2>\n *\n * <p>To run this workload, use {@link WorkloadSpec#getRunner(Engine)} to get the appropriate {@link\n * WriteRunner}.\n *\n * @see WriteRunner\n */\npublic class WriteSpec extends WorkloadSpec {\n\n  /**\n   * Container for data file actions (adds) from a commit specification file.\n   *\n   * <p>This class is used to deserialize the JSON structure containing the list of files to add in\n   * a commit.\n   */\n  private static class DataFileOperations {\n    @JsonProperty(\"adds\")\n    private ArrayList<DataFilesStatusSerde> adds;\n\n    /** @return the list of added data files in this commit. */\n    @JsonIgnore\n    public ArrayList<DataFilesStatusSerde> getAdds() {\n      return adds;\n    }\n  }\n\n  /**\n   * Serialization/deserialization wrapper for {@link DataFileStatus}.\n   *\n   * <p>This class represents the JSON structure for data file metadata, including path, size,\n   * modification time, and optional statistics. It can be converted to a {@link DataFileStatus}\n   * instance for use in Delta Kernel APIs.\n   */\n  private static class DataFilesStatusSerde {\n    @JsonProperty(\"path\")\n    private String path;\n\n    @JsonProperty(\"size\")\n    private long size;\n\n    @JsonProperty(\"modification_time\")\n    private long modificationTime;\n\n    @JsonProperty(\"stats\")\n    private String stats;\n\n    /**\n     * Converts this serialization object to a {@link DataFileStatus} instance.\n     *\n     * @param schema the table schema used to parse statistics\n     * @return a DataFileStatus instance with the file metadata\n     */\n    public DataFileStatus toDataFileStatus(StructType schema) {\n      Optional<DataFileStatistics> parsedStats = Optional.empty();\n      if (stats != null) {\n        parsedStats = DataFileStatistics.deserializeFromJson(stats, schema);\n      }\n      return new DataFileStatus(path, size, modificationTime, parsedStats);\n    }\n  }\n\n  /**\n   * Container for a single commit's configuration.\n   *\n   * <p>Each commit references a file containing the Delta log JSON actions (add/remove files) to be\n   * committed.\n   */\n  public static class CommitSpec {\n    /**\n     * Path to the commit file containing Delta log JSON actions. The path is relative to the spec\n     * directory (where spec.json is located).\n     *\n     * <p>Example: \"commit_a.json\"\n     */\n    @JsonProperty(\"data_files_path\")\n    private String dataFilesPath;\n\n    /** Default constructor for Jackson. */\n    public CommitSpec() {}\n\n    public String getDataFilesPath() {\n      return dataFilesPath;\n    }\n\n    /**\n     * Parses the data_files file and returns the list of added and removed data files.\n     *\n     * @param specPath the base path where the commit file is located\n     * @throws IOException if there's an error reading or parsing the file\n     */\n    public List<DataFileStatus> readDataFiles(String specPath, StructType schema)\n        throws IOException {\n      ObjectMapper mapper = new ObjectMapper();\n      String commitFilePath = new Path(specPath, getDataFilesPath()).toString();\n\n      DataFileOperations dataFileOps =\n          mapper.readValue(new File(commitFilePath), DataFileOperations.class);\n      return dataFileOps.adds.stream()\n          .map(file -> file.toDataFileStatus(schema))\n          .collect(Collectors.toList());\n    }\n  }\n\n  /**\n   * List of commits to execute in sequence. Each commit contains a reference to a file with Delta\n   * log JSON actions. All commits are executed as part of the timed benchmark.\n   */\n  @JsonProperty(\"commits\")\n  private List<CommitSpec> commits;\n\n  // Default constructor for Jackson\n  public WriteSpec() {\n    super(\"write\");\n  }\n\n  /**\n   * Gets the list of commits to execute.\n   *\n   * @return list of commit specifications\n   */\n  public List<CommitSpec> getCommits() {\n    return commits != null ? commits : Collections.emptyList();\n  }\n\n  /** @return the full name of this workload, derived from table name, case name, and type. */\n  @Override\n  public String getFullName() {\n    return this.tableInfo.name + \"/\" + caseName + \"/write\";\n  }\n\n  @Override\n  public WorkloadRunner getRunner(Engine engine) {\n    return new WriteRunner(this, engine);\n  }\n\n  /**\n   * Generates workload variants from this test case specification.\n   *\n   * <p>Currently, WriteSpec generates a single variant (itself). In the future, this could be\n   * extended to generate variants for different write patterns or configurations.\n   *\n   * @return list of WriteSpec variants, each representing a separate workload execution\n   */\n  @Override\n  public List<WorkloadSpec> getWorkloadVariants() {\n    return Collections.singletonList(this);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"Write{caseName='%s', commits=%d, tableInfo='%s'}\",\n        caseName, getCommits().size(), tableInfo);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/workloadrunners/ReadMetadataRunner.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.workloadrunners;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.benchmarks.models.ReadSpec;\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.util.Optional;\nimport org.openjdk.jmh.infra.Blackhole;\n\n/**\n * A WorkloadRunner that can execute the read_metadata workload as a benchmark. This runner is\n * created from a {@link ReadSpec}. The workload performs a scan of the table's metadata, at the\n * specified snapshot version (or latest if not specified).\n *\n * <p>If run as a benchmark using {@link #executeAsBenchmark(Blackhole)}, this measures the time to\n * perform the metadata scan and consume all results. It does not include the time to load the\n * snapshot or set up the scan, which is done in {@link #setup()}.\n */\npublic class ReadMetadataRunner extends WorkloadRunner {\n  private Scan scan;\n  private final Engine engine;\n  private final ReadSpec workloadSpec;\n\n  /**\n   * Constructs the ReadMetadataRunner from the workload spec and engine.\n   *\n   * @param workloadSpec The read_metadata workload specification.\n   * @param engine The engine to use for executing the workload.\n   * @throws IllegalArgumentException if the operation type is not \"read_metadata\"\n   */\n  public ReadMetadataRunner(ReadSpec workloadSpec, Engine engine) {\n    // ensure the operation type is read_metadata\n    if (!workloadSpec.getOperationType().equals(\"read_metadata\")) {\n      throw new IllegalArgumentException(\n          \"ReadMetadataRunner can only be used for read_metadata workloads\");\n    }\n    this.workloadSpec = workloadSpec;\n    this.engine = engine;\n  }\n\n  @Override\n  public void setup() throws Exception {\n    Optional<Long> versionOpt = Optional.ofNullable(workloadSpec.getVersion());\n    Snapshot snapshot = loadSnapshot(engine, workloadSpec.getTableInfo(), versionOpt);\n    scan = snapshot.getScanBuilder().build();\n  }\n\n  /** @return the name of this workload derived from the workload specification. */\n  @Override\n  public String getName() {\n    return \"read_metadata\";\n  }\n\n  /** @return The workload specification used to create this runner. */\n  @Override\n  public WorkloadSpec getWorkloadSpec() {\n    return workloadSpec;\n  }\n\n  /**\n   * Executes the read_metadata workload as a benchmark, consuming results via the provided\n   * Blackhole.\n   *\n   * @param blackhole The Blackhole to consume results and avoid dead code elimination.\n   */\n  @Override\n  public void executeAsBenchmark(Blackhole blackhole) {\n    // Run the actual metadata reading workload\n    try (CloseableIterator<FilteredColumnarBatch> iterator = execute()) {\n      // Consume the iterator to measure the actual work\n      while (iterator.hasNext()) {\n        FilteredColumnarBatch batch = iterator.next();\n        blackhole.consume(batch);\n      }\n    } catch (Exception e) {\n      throw new RuntimeException(\"Error during benchmark execution\", e);\n    }\n  }\n\n  @Override\n  public void cleanup() throws Exception {\n    /* This is a read-only workload; no cleanup necessary. */\n  }\n\n  /**\n   * Executes the read_metadata workload, returning an iterator over the results. This must be fully\n   * consumed by the caller to ensure the workload is fully executed.\n   *\n   * @return Iterator of results from the read_metadata workload.\n   */\n  private CloseableIterator<FilteredColumnarBatch> execute() {\n    if (scan == null) {\n      throw new IllegalStateException(\n          \"ReadMetadataRunner not initialized. Call setup() before executing.\");\n    }\n    return scan.getScanFiles(engine);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/workloadrunners/SnapshotConstructionRunner.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.workloadrunners;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.SnapshotBuilder;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.benchmarks.models.SnapshotConstructionSpec;\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport io.delta.kernel.engine.Engine;\nimport org.openjdk.jmh.infra.Blackhole;\n\npublic class SnapshotConstructionRunner extends WorkloadRunner {\n\n  private final SnapshotConstructionSpec workloadSpec;\n  private final Engine engine;\n\n  /**\n   * Construct a SnapshotConstructionRunner from the workload spec and engine.\n   *\n   * @param workloadSpec The snapshot_construction workload specification.\n   * @param engine The engine to use for executing the workload.\n   */\n  public SnapshotConstructionRunner(SnapshotConstructionSpec workloadSpec, Engine engine) {\n    this.workloadSpec = workloadSpec;\n    this.engine = engine;\n  }\n\n  @Override\n  public String getName() {\n    return \"snapshot_construction\";\n  }\n\n  @Override\n  public WorkloadSpec getWorkloadSpec() {\n    return this.workloadSpec;\n  }\n\n  @Override\n  public void setup() throws Exception {\n    /* No setup needed for snapshot construction */\n  }\n\n  @Override\n  public void executeAsBenchmark(Blackhole blackhole) throws Exception {\n    blackhole.consume(this.execute());\n  }\n\n  @Override\n  public void cleanup() throws Exception {\n    /* No cleanup needed for snapshot construction */\n  }\n\n  private Snapshot execute() {\n    String workloadTableRoot = workloadSpec.getTableInfo().getResolvedTableRoot();\n    SnapshotBuilder builder = TableManager.loadSnapshot(workloadTableRoot);\n    if (workloadSpec.getVersion() != null) {\n      builder.atVersion(workloadSpec.getVersion());\n    }\n    return builder.build(engine);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/workloadrunners/WorkloadRunner.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.workloadrunners;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.SnapshotBuilder;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.benchmarks.models.TableInfo;\nimport io.delta.kernel.benchmarks.models.UcCatalogInfo;\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.unitycatalog.InMemoryUCClient;\nimport io.delta.kernel.unitycatalog.UCCatalogManagedClient;\nimport java.net.URI;\nimport java.nio.file.Paths;\nimport java.util.Optional;\nimport org.openjdk.jmh.infra.Blackhole;\n\n/**\n * A runner that can execute a specific workload as a benchmark or test. A WorkloadRunner is created\n * from a {@link WorkloadSpec} and is responsible for setting up any state necessary to execute the\n * workload using {@link WorkloadRunner#setup()}, as well as executing the workload itself.\n *\n * <h2>Execution Modes</h2>\n *\n * <ul>\n *   <li><b>Benchmark</b>: Execute via {@link #executeAsBenchmark(Blackhole)} for JMH performance\n *       measurements\n *   <li><b>Test</b>: Execute via executeAsTest() for correctness validation (future work)\n * </ul>\n *\n * <p>The {@link #setup()} method must be called before any execution method.\n */\npublic abstract class WorkloadRunner {\n\n  public WorkloadRunner() {}\n\n  /** @return the name of this workload derived from the contents of the workload specification. */\n  public abstract String getName();\n\n  /** @return The workload specification used to create this runner. */\n  public abstract WorkloadSpec getWorkloadSpec();\n\n  /**\n   * Sets up any state necessary to execute this workload. This method must be called before\n   * executing the workload as a benchmark or test.\n   *\n   * @throws Exception if any error occurs during setup.\n   */\n  public abstract void setup() throws Exception;\n\n  /**\n   * Executes the workload as a benchmark, consuming any output via the provided Blackhole to\n   * prevent dead code elimination by the JIT compiler. The {@link #setup()} method must be called\n   * before invoking this method.\n   *\n   * @param blackhole the Blackhole provided by JMH to consume output.\n   * @throws Exception if any error occurs during execution.\n   */\n  public abstract void executeAsBenchmark(Blackhole blackhole) throws Exception;\n\n  /**\n   * Cleans up any state created during benchmark execution. For write workloads, this removes added\n   * files and reverts table state. For read workloads, this is typically a no-op.\n   *\n   * <p>This method is called after each benchmark invocation to ensure a clean state for the next\n   * run.\n   *\n   * @throws Exception if any error occurs during cleanup.\n   */\n  public abstract void cleanup() throws Exception;\n\n  /**\n   * Loads a snapshot for the table.\n   *\n   * <p>For Unity Catalog managed tables, uses {@link UCCatalogManagedClient} to handle staged\n   * commits. For regular tables, uses {@link TableManager}.\n   *\n   * @param engine the engine to use\n   * @param tableInfo the table information\n   * @param versionOpt optional version to load (if empty, loads latest)\n   * @return a Snapshot for the table\n   * @throws Exception if there's an error loading the snapshot\n   */\n  protected Snapshot loadSnapshot(Engine engine, TableInfo tableInfo, Optional<Long> versionOpt)\n      throws Exception {\n    String tableRoot = tableInfo.getResolvedTableRoot();\n\n    if (tableInfo.isCatalogManaged()) {\n      UcCatalogInfo ucCatalogInfo = tableInfo.getUcCatalogInfo();\n      InMemoryUCClient ucClient = ucCatalogInfo.createUCClient(engine, tableRoot);\n      UCCatalogManagedClient ucCatalogManagedClient = new UCCatalogManagedClient(ucClient);\n\n      // Use Paths.get().toUri() to get properly formatted file:// URI\n      URI tableUri = Paths.get(tableRoot).toUri();\n      return ucCatalogManagedClient.loadSnapshot(\n          engine,\n          ucCatalogInfo.getUcTableId(),\n          tableUri.toString(),\n          versionOpt,\n          Optional.empty() /* timestampOpt */);\n    } else {\n      // Use direct TableManager for regular filesystem tables\n      SnapshotBuilder builder = TableManager.loadSnapshot(tableRoot);\n      if (versionOpt.isPresent()) {\n        builder = builder.atVersion(versionOpt.get());\n      }\n      return builder.build(engine);\n    }\n  }\n\n  // TODO: Add executeAsTest() method for correctness validation\n  // public abstract void executeAsTest() throws Exception;\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/java/io/delta/kernel/benchmarks/workloadrunners/WriteRunner.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.benchmarks.workloadrunners;\n\nimport static io.delta.kernel.internal.util.Utils.toCloseableIterator;\n\nimport io.delta.kernel.*;\nimport io.delta.kernel.benchmarks.models.WorkloadSpec;\nimport io.delta.kernel.benchmarks.models.WriteSpec;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.transaction.UpdateTableTransactionBuilder;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.DataFileStatus;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.util.*;\nimport org.openjdk.jmh.infra.Blackhole;\n\n/**\n * A WorkloadRunner that executes write workloads as benchmarks. This runner performs one or more\n * commits to a Delta table and measures the performance of those commits.\n *\n * <p>The runner executes commits specified in the {@link WriteSpec}, where each commit contains a\n * set of Delta log actions (add/remove files) defined in external JSON files.\n *\n * <p>If run as a benchmark using {@link #executeAsBenchmark(Blackhole)}, this measures the time to\n * execute all commits. Setup (loading commit files) and cleanup (reverting changes) are not\n * included in the benchmark timing.\n */\npublic class WriteRunner extends WorkloadRunner {\n  private final Engine engine;\n  private final WriteSpec workloadSpec;\n  private final List<List<DataFileStatus>> commitContents;\n  private Snapshot snapshot;\n  private Optional<Set<String>> initialDeltaLogFiles = Optional.empty();\n\n  /**\n   * Constructs the WriteRunner from the workload spec and engine.\n   *\n   * @param workloadSpec The write workload specification.\n   * @param engine The engine to use for executing the workload.\n   */\n  public WriteRunner(WriteSpec workloadSpec, Engine engine) {\n    this.workloadSpec = workloadSpec;\n    this.engine = engine;\n    this.commitContents = new ArrayList<>();\n  }\n\n  @Override\n  public void setup() throws Exception {\n    String tableRoot = workloadSpec.getTableInfo().getResolvedTableRoot();\n\n    // Capture initial listing of delta log files. This is used during cleanup to revert changes.\n    if (!initialDeltaLogFiles.isPresent()) {\n      initialDeltaLogFiles = Optional.of(captureFileListing());\n    }\n\n    // Load the initial snapshot of the table. This will be used as the starting point for commits\n    // and will be updated after each commit using the post-commit snapshot.\n    snapshot = loadSnapshot(engine, workloadSpec.getTableInfo(), Optional.empty());\n\n    if (commitContents.isEmpty()) {\n      for (WriteSpec.CommitSpec commitSpec : workloadSpec.getCommits()) {\n        commitContents.add(\n            commitSpec.readDataFiles(workloadSpec.getSpecDirectoryPath(), snapshot.getSchema()));\n      }\n    }\n  }\n\n  /** @return the name of this workload. */\n  @Override\n  public String getName() {\n    return \"write\";\n  }\n\n  /** @return The workload specification used to create this runner. */\n  @Override\n  public WorkloadSpec getWorkloadSpec() {\n    return workloadSpec;\n  }\n\n  /**\n   * Executes the write workload as a benchmark, consuming results via the provided Blackhole.\n   *\n   * <p>This method executes all commits specified in the workload spec in sequence. The timing\n   * includes only the commit execution, not the setup or cleanup. We reuse the post-commit snapshot\n   * from each transaction to avoid reloading from disk, which makes the benchmark more efficient\n   * and realistic.\n   *\n   * @param blackhole The Blackhole to consume results and avoid dead code elimination.\n   */\n  @Override\n  public void executeAsBenchmark(Blackhole blackhole) throws Exception {\n    // Execute all commits in sequence\n    for (List<DataFileStatus> actions : commitContents) {\n      UpdateTableTransactionBuilder txnBuilder =\n          snapshot.buildUpdateTableTransaction(\"Delta-Kernel-Benchmarks\", Operation.WRITE);\n\n      Transaction txn = txnBuilder.build(engine);\n      Row txnState = txn.getTransactionState(engine);\n      DataWriteContext writeContext =\n          Transaction.getWriteContext(engine, txnState, new HashMap<>() /* partitionValues */);\n      CloseableIterator<Row> add_actions =\n          Transaction.generateAppendActions(\n              engine, txnState, toCloseableIterator(actions.iterator()), writeContext);\n      TransactionCommitResult result =\n          txn.commit(engine, CloseableIterable.inMemoryIterable(add_actions));\n\n      long version = result.getVersion();\n      blackhole.consume(version);\n\n      // Use the post-commit snapshot for the next transaction\n      // Post-commit snapshot should always be present unless there was a conflict\n      snapshot =\n          result\n              .getPostCommitSnapshot()\n              .orElseThrow(\n                  () ->\n                      new IllegalStateException(\n                          \"Post-commit snapshot not available. This indicates a conflict \"\n                              + \"occurred during the benchmark, which should not happen. \"\n                              + \"Ensure no other processes are writing to the table: \"\n                              + workloadSpec.getTableInfo().getResolvedTableRoot()));\n    }\n  }\n\n  /** Cleans up the state created during benchmark execution by reverting all committed changes. */\n  @Override\n  public void cleanup() throws Exception {\n    if (!initialDeltaLogFiles.isPresent()) {\n      throw new RuntimeException(\"Cannot cleanup before setup is called.\");\n    }\n    // Delete any files that weren't present initially\n    Set<String> currentFiles = captureFileListing();\n    Set<String> initialFiles =\n        initialDeltaLogFiles.orElseThrow(\n            () -> new RuntimeException(\"Cannot cleanup before setup is called.\"));\n    for (String filePath : currentFiles) {\n      if (!initialFiles.contains(filePath)) {\n        engine.getFileSystemClient().delete(filePath);\n      }\n    }\n  }\n\n  /**\n   * @return all file paths in the `_delta_log/` directory (including `_staged_commits/` if Unity\n   *     Catalog managed and `_sidecars/` if it exists)\n   */\n  private Set<String> captureFileListing() throws IOException {\n    List<String> prefixes =\n        new ArrayList<>(\n            Arrays.asList(\"_delta_log\", \"_delta_log/_sidecars\", \"_delta_log/_staged_commits\"));\n\n    Set<String> files = new HashSet<>();\n    for (String prefix : prefixes) {\n      // Construct path prefix for all files in `_delta_log/`. The prefix is for file with name `0`\n      // because the filesystem client lists all _sibling_ files in the directory with a path\n      // greater than `0`.\n      String deltaLogPathPrefix =\n          new Path(workloadSpec.getTableInfo().getResolvedTableRoot(), new Path(prefix, \"0\"))\n              .toUri()\n              .getPath();\n\n      // List from the lowest version in the prefix\n      try (CloseableIterator<FileStatus> filesIter =\n          engine.getFileSystemClient().listFrom(deltaLogPathPrefix)) {\n        while (filesIter.hasNext()) {\n          files.add(filesIter.next().getPath());\n        }\n      } catch (FileNotFoundException e) {\n        // Ignore if the directory does not exist\n      }\n    }\n    return files;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/delta/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712091396253,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"3\",\"numOutputBytes\":\"996\"},\"engineInfo\":\"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\"txnId\":\"5df7dc20-b980-4207-a5fd-b69cb4541b2e\"}}\n{\"metaData\":{\"id\":\"2a1e618f-d92a-4c94-bb06-a2808f8b39f3\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"letter\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"number\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"a_float\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1712091393302}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-c9f44819-b06d-45dd-b33d-ae9aa1b96909-c000.snappy.parquet\",\"partitionValues\":{},\"size\":996,\"modificationTime\":1712091396057,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"letter\\\":\\\"a\\\",\\\"number\\\":1,\\\"a_float\\\":1.1},\\\"maxValues\\\":{\\\"letter\\\":\\\"c\\\",\\\"number\\\":3,\\\"a_float\\\":3.3},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/delta/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712091404556,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"984\"},\"engineInfo\":\"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\"txnId\":\"64562965-a4c4-48a7-84dd-e68ee934f467\"}}\n{\"add\":{\"path\":\"part-00000-a9daef62-5a40-43c5-ac63-3ad4a7d749ae-c000.snappy.parquet\",\"partitionValues\":{},\"size\":984,\"modificationTime\":1712091404545,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"letter\\\":\\\"d\\\",\\\"number\\\":4,\\\"a_float\\\":4.4},\\\"maxValues\\\":{\\\"letter\\\":\\\"e\\\",\\\"number\\\":5,\\\"a_float\\\":5.5},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/read_latest/spec.json",
    "content": "{\n  \"type\": \"read\"\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/read_v0/spec.json",
    "content": "{\n  \"type\": \"read\",\n  \"version\": 0\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/snapshot_latest/spec.json",
    "content": "{\n  \"type\": \"snapshot_construction\"\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/snapshot_v0/spec.json",
    "content": "{\n  \"type\": \"snapshot_construction\",\n  \"version\": 0\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/write_appends/commit_2_adds.json",
    "content": "{\n  \"adds\": [\n    {\n      \"path\": \"dummy_data_b.parquet\",\n      \"size\": 1024,\n      \"modification_time\": 1712091404545,\n      \"stats\": \"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"letter\\\":\\\"a\\\",\\\"number\\\":1,\\\"a_float\\\":1.1},\\\"maxValues\\\":{\\\"letter\\\":\\\"j\\\",\\\"number\\\":10,\\\"a_float\\\":10.10},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"\n    },\n    {\n      \"path\": \"dummy_data_c.parquet\",\n      \"size\": 927,\n      \"modification_time\": 1712091405000,\n      \"stats\": \"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"letter\\\":\\\"b\\\",\\\"number\\\":2,\\\"a_float\\\":1.3},\\\"maxValues\\\":{\\\"letter\\\":\\\"j\\\",\\\"number\\\":10,\\\"a_float\\\":10.10},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"\n    }\n  ]\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/write_appends/commit_add.json",
    "content": "{\n  \"adds\": [\n    {\n      \"path\": \"dummy_data_a.parquet\",\n      \"size\": 1024,\n      \"modification_time\": 1712091404545,\n      \"stats\": \"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"letter\\\":\\\"a\\\",\\\"number\\\":1,\\\"a_float\\\":1.1},\\\"maxValues\\\":{\\\"letter\\\":\\\"j\\\",\\\"number\\\":10,\\\"a_float\\\":10.10},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"\n    }\n  ]\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/specs/write_appends/spec.json",
    "content": "{\n  \"type\": \"write\",\n  \"commits\": [\n    {\"data_files_path\": \"commit_add.json\"},\n    {\"data_files_path\": \"commit_2_adds.json\"}\n  ]\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_append/table_info.json",
    "content": "{\n  \"name\": \"basic_append\",\n  \"description\": \"A basic table with two append writes.\",\n  \"engine_info\": \"Apache-Spark/3.5.1 Delta-Lake/3.1.0\"\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/catalog_managed_info.json",
    "content": "{\n  \"uc_table_id\": \"12345678-1234-1234-1234-123456789abc\",\n  \"max_ratified_version\": 3,\n  \"log_tail\": [\n    {\n      \"staged_commit_file_name\": \"00000000000000000002.a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.json\"\n    },\n    {\n      \"staged_commit_file_name\": \"00000000000000000003.f7e8d9c0-b1a2-4536-9748-5a6b7c8d9e0f.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/delta/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"inCommitTimestamp\":1712091396253,\"timestamp\":1712091396253,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"3\",\"numOutputBytes\":\"996\"},\"engineInfo\":\"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\"txnId\":\"5df7dc20-b980-4207-a5fd-b69cb4541b2e\"}}\n{\"metaData\":{\"id\":\"2a1e618f-d92a-4c94-bb06-a2808f8b39f3\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"letter\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"number\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"a_float\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableInCommitTimestamps\":\"true\",\"catalogManaged.unityCatalog.tableId\":\"12345678-1234-1234-1234-123456789abc\"},\"createdTime\":1712091393302}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"catalogManaged\"],\"writerFeatures\":[\"catalogManaged\",\"inCommitTimestamp\"]}}\n{\"add\":{\"path\":\"part-00000-c9f44819-b06d-45dd-b33d-ae9aa1b96909-c000.snappy.parquet\",\"partitionValues\":{},\"size\":996,\"modificationTime\":1712091396057,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"letter\\\":\\\"a\\\",\\\"number\\\":1,\\\"a_float\\\":1.1},\\\"maxValues\\\":{\\\"letter\\\":\\\"c\\\",\\\"number\\\":3,\\\"a_float\\\":3.3},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"}}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/delta/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1712091404556,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"984\"},\"engineInfo\":\"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\"txnId\":\"64562965-a4c4-48a7-84dd-e68ee934f467\"}}\n{\"add\":{\"path\":\"part-00000-a9daef62-5a40-43c5-ac63-3ad4a7d749ae-c000.snappy.parquet\",\"partitionValues\":{},\"size\":984,\"modificationTime\":1712091404545,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"letter\\\":\\\"d\\\",\\\"number\\\":4,\\\"a_float\\\":4.4},\\\"maxValues\\\":{\\\"letter\\\":\\\"e\\\",\\\"number\\\":5,\\\"a_float\\\":5.5},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"}}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/delta/_delta_log/_staged_commits/00000000000000000002.a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.json",
    "content": "{\"commitInfo\":{\"inCommitTimestamp\":1712091410000,\"timestamp\":1712091410000,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"984\"},\"engineInfo\":\"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\"txnId\":\"catalog-managed-test-txn-0002\"}}\n{\"remove\":{\"path\":\"part-00000-a9daef62-5a40-43c5-ac63-3ad4a7d749ae-c000.snappy.parquet\",\"partitionValues\":{},\"size\":984,\"modificationTime\":1712091404545,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"letter\\\":\\\"d\\\",\\\"number\\\":4,\\\"a_float\\\":4.4},\\\"maxValues\\\":{\\\"letter\\\":\\\"e\\\",\\\"number\\\":5,\\\"a_float\\\":5.5},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"}}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/delta/_delta_log/_staged_commits/00000000000000000003.f7e8d9c0-b1a2-4536-9748-5a6b7c8d9e0f.json",
    "content": "{\"commitInfo\":{\"inCommitTimestamp\":1712091415000,\"timestamp\":1712091415000,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"3\",\"numOutputBytes\":\"996\"},\"engineInfo\":\"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\"txnId\":\"catalog-managed-test-txn-0003\"}}\n{\"remove\":{\"path\":\"part-00000-c9f44819-b06d-45dd-b33d-ae9aa1b96909-c000.snappy.parquet\",\"partitionValues\":{},\"size\":996,\"modificationTime\":1712091396057,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"letter\\\":\\\"a\\\",\\\"number\\\":1,\\\"a_float\\\":1.1},\\\"maxValues\\\":{\\\"letter\\\":\\\"c\\\",\\\"number\\\":3,\\\"a_float\\\":3.3},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"}}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/specs/read_with_staged/spec.json",
    "content": "{\n  \"type\": \"read\",\n  \"operation_type\": \"read_metadata\"\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/specs/write_with_staged/commit_2.json",
    "content": "{\n  \"adds\": [\n    {\n      \"path\": \"part-00000-c9f44819-b06d-45dd-b33d-ae9aa1b96909-c000.snappy.parquet\",\n      \"size\": 996,\n      \"modification_time\": 1712091396057,\n      \"stats\": \"{\\\"numRecords\\\":3,\\\"minValues\\\":{\\\"letter\\\":\\\"a\\\",\\\"number\\\":1,\\\"a_float\\\":1.1},\\\"maxValues\\\":{\\\"letter\\\":\\\"c\\\",\\\"number\\\":3,\\\"a_float\\\":3.3},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"\n    },\n    {\n      \"path\": \"part-00000-a9daef62-5a40-43c5-ac63-3ad4a7d749ae-c000.snappy.parquet\",\n      \"size\": 984,\n      \"modification_time\": 1712091404545,\n      \"stats\": \"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"letter\\\":\\\"d\\\",\\\"number\\\":4,\\\"a_float\\\":4.4},\\\"maxValues\\\":{\\\"letter\\\":\\\"e\\\",\\\"number\\\":5,\\\"a_float\\\":5.5},\\\"nullCount\\\":{\\\"letter\\\":0,\\\"number\\\":0,\\\"a_float\\\":0}}\"\n    }\n  ]\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/specs/write_with_staged/spec.json",
    "content": "{\n  \"type\": \"write\",\n  \"commits\": [\n    {\"data_files_path\": \"commit_2.json\"}\n  ]\n}"
  },
  {
    "path": "kernel/kernel-benchmarks/src/test/resources/workload_specs/basic_catalog_managed/table_info.json",
    "content": "{\n  \"name\": \"basic_catalog_managed\",\n  \"description\": \"A basic Unity Catalog managed table with 2 backfilled commits (v0-1) and 2 staged commits (v2-3).\",\n  \"engine_info\": \"Apache-Spark/3.5.1 Delta-Lake/3.1.0\",\n  \"is_catalog_managed\": true\n}"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/DefaultEngine.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine;\n\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO;\nimport io.delta.kernel.engine.*;\nimport java.util.Collections;\nimport java.util.List;\nimport org.apache.hadoop.conf.Configuration;\n\n/** Default implementation of {@link Engine} based on Hadoop APIs. */\npublic class DefaultEngine implements Engine {\n  private final FileIO fileIO;\n\n  protected DefaultEngine(FileIO fileIO) {\n    this.fileIO = fileIO;\n  }\n\n  @Override\n  public ExpressionHandler getExpressionHandler() {\n    return new DefaultExpressionHandler();\n  }\n\n  @Override\n  public JsonHandler getJsonHandler() {\n    return new DefaultJsonHandler(fileIO);\n  }\n\n  @Override\n  public FileSystemClient getFileSystemClient() {\n    return new DefaultFileSystemClient(fileIO);\n  }\n\n  @Override\n  public ParquetHandler getParquetHandler() {\n    return new DefaultParquetHandler(fileIO);\n  }\n\n  @Override\n  public List<MetricsReporter> getMetricsReporters() {\n    return Collections.singletonList(new LoggingMetricsReporter());\n  };\n\n  /**\n   * Create an instance of {@link DefaultEngine}.\n   *\n   * @param hadoopConf Hadoop configuration to use.\n   * @return an instance of {@link DefaultEngine}.\n   */\n  public static DefaultEngine create(Configuration hadoopConf) {\n    return new DefaultEngine(new HadoopFileIO(hadoopConf));\n  }\n\n  /**\n   * Create an instance of {@link DefaultEngine}. It takes {@link FileIO} as an argument which is\n   * used for I/O related operations.\n   *\n   * @param fileIO File IO implementation to use for reading and writing files.\n   * @return an instance of {@link DefaultEngine}.\n   */\n  public static DefaultEngine create(FileIO fileIO) {\n    return new DefaultEngine(fileIO);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/DefaultExpressionHandler.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultBooleanVector;\nimport io.delta.kernel.defaults.internal.expressions.DefaultExpressionEvaluator;\nimport io.delta.kernel.defaults.internal.expressions.DefaultPredicateEvaluator;\nimport io.delta.kernel.engine.ExpressionHandler;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.ExpressionEvaluator;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.expressions.PredicateEvaluator;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructType;\nimport java.util.Arrays;\nimport java.util.Optional;\n\n/** Default implementation of {@link ExpressionHandler} */\npublic class DefaultExpressionHandler implements ExpressionHandler {\n\n  @Override\n  public ExpressionEvaluator getEvaluator(\n      StructType inputSchema, Expression expression, DataType outputType) {\n    return new DefaultExpressionEvaluator(inputSchema, expression, outputType);\n  }\n\n  @Override\n  public PredicateEvaluator getPredicateEvaluator(StructType inputSchema, Predicate predicate) {\n    return new DefaultPredicateEvaluator(inputSchema, predicate);\n  }\n\n  @Override\n  public ColumnVector createSelectionVector(boolean[] values, int from, int to) {\n    requireNonNull(values, \"values is null\");\n    int length = to - from;\n    checkArgument(\n        length >= 0 && values.length > from && values.length >= to,\n        \"invalid range from=%s, to=%s, values length=%s\",\n        from,\n        to,\n        values.length);\n\n    // Make a copy of the `values` array.\n    boolean[] valuesCopy = Arrays.copyOfRange(values, from, to);\n    return new DefaultBooleanVector(length, Optional.empty(), valuesCopy);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/DefaultFileSystemClient.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine;\n\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.fileio.InputFile;\nimport io.delta.kernel.defaults.engine.fileio.SeekableInputStream;\nimport io.delta.kernel.engine.FileReadRequest;\nimport io.delta.kernel.engine.FileSystemClient;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport io.delta.storage.LogStore;\nimport java.io.*;\nimport java.util.Objects;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileSystem;\n\n/**\n * Default implementation of {@link FileSystemClient} based on Hadoop APIs. It takes a Hadoop {@link\n * Configuration} object to interact with the file system. The following optional configurations can\n * be set to customize the behavior of the client:\n *\n * <ul>\n *   <li>{@code io.delta.kernel.logStore.<scheme>.impl} - The class name of the custom {@link\n *       LogStore} implementation to use for operations on storage systems with the specified {@code\n *       scheme}. For example, to use a custom {@link LogStore} for S3 storage objects:\n *       <pre>{@code\n * <property>\n *   <name>io.delta.kernel.logStore.s3.impl</name>\n *   <value>com.example.S3LogStore</value>\n * </property>\n *\n * }</pre>\n *       If not set, the default LogStore implementation for the scheme will be used.\n *   <li>{@code delta.enableFastS3AListFrom} - Set to {@code true} to enable fast listing\n *       functionality when using a {@link LogStore} created for S3 storage objects.\n * </ul>\n *\n * The above list of options is not exhaustive. For a complete list of options, refer to the\n * specific implementation of {@link FileSystem}.\n */\npublic class DefaultFileSystemClient implements FileSystemClient {\n  private final FileIO fileIO;\n\n  /**\n   * Create an instance of the default {@link FileSystemClient} implementation.\n   *\n   * @param fileIO The {@link FileIO} implementation to use for file operations.\n   */\n  public DefaultFileSystemClient(FileIO fileIO) {\n    this.fileIO = Objects.requireNonNull(fileIO, \"fileIO is null\");\n  }\n\n  @Override\n  public CloseableIterator<FileStatus> listFrom(String filePath) throws IOException {\n    return fileIO.listFrom(filePath);\n  }\n\n  @Override\n  public String resolvePath(String path) throws IOException {\n    return fileIO.resolvePath(path);\n  }\n\n  @Override\n  public CloseableIterator<ByteArrayInputStream> readFiles(\n      CloseableIterator<FileReadRequest> readRequests) throws IOException {\n    return readRequests.map(\n        elem -> getStream(elem.getPath(), elem.getStartOffset(), elem.getReadLength()));\n  }\n\n  @Override\n  public boolean mkdirs(String path) throws IOException {\n    return fileIO.mkdirs(path);\n  }\n\n  @Override\n  public boolean delete(String path) throws IOException {\n    return fileIO.delete(path);\n  }\n\n  @Override\n  public FileStatus getFileStatus(String path) throws IOException {\n    return fileIO.getFileStatus(path);\n  }\n\n  @Override\n  public void copyFileAtomically(String srcPath, String destPath, boolean overwrite)\n      throws IOException {\n    fileIO.copyFileAtomically(srcPath, destPath, overwrite);\n  }\n\n  private ByteArrayInputStream getStream(String filePath, int offset, int size) {\n    InputFile inputFile = this.fileIO.newInputFile(filePath, /* fileSize */ -1);\n    try (SeekableInputStream stream = inputFile.newStream()) {\n      stream.seek(offset);\n      byte[] buff = new byte[size];\n      stream.readFully(buff, 0, size);\n      return new ByteArrayInputStream(buff);\n    } catch (IOException ex) {\n      throw new UncheckedIOException(\n          String.format(\n              \"IOException reading from file %s at offset %s size %s\", filePath, offset, size),\n          ex);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/DefaultJsonHandler.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.fileio.SeekableInputStream;\nimport io.delta.kernel.defaults.internal.data.DefaultJsonRow;\nimport io.delta.kernel.defaults.internal.data.DefaultRowBasedColumnarBatch;\nimport io.delta.kernel.defaults.internal.json.JsonUtils;\nimport io.delta.kernel.engine.JsonHandler;\nimport io.delta.kernel.exceptions.KernelEngineException;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport io.delta.storage.LogStore;\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\n\n/** Default implementation of {@link JsonHandler} based on Hadoop APIs. */\npublic class DefaultJsonHandler implements JsonHandler {\n  private static final ObjectMapper mapper = new ObjectMapper();\n  // by default BigDecimals are truncated and read as floats\n  private static final ObjectReader objectReaderReadBigDecimals =\n      new ObjectMapper().reader(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);\n\n  private final FileIO fileIO;\n  private final int maxBatchSize;\n\n  public DefaultJsonHandler(FileIO fileIO) {\n    this.fileIO = fileIO;\n    this.maxBatchSize =\n        fileIO\n            .getConf(\"delta.kernel.default.json.reader.batch-size\")\n            .map(Integer::valueOf)\n            .orElse(1024);\n    checkArgument(maxBatchSize > 0, \"invalid JSON reader batch size: %d\", maxBatchSize);\n  }\n\n  @Override\n  public ColumnarBatch parseJson(\n      ColumnVector jsonStringVector,\n      StructType outputSchema,\n      Optional<ColumnVector> selectionVector) {\n    List<Row> rows = new ArrayList<>();\n    for (int i = 0; i < jsonStringVector.getSize(); i++) {\n      boolean isSelected =\n          !selectionVector.isPresent()\n              || (!selectionVector.get().isNullAt(i) && selectionVector.get().getBoolean(i));\n      if (isSelected && !jsonStringVector.isNullAt(i)) {\n        rows.add(parseJson(jsonStringVector.getString(i), outputSchema));\n      } else {\n        rows.add(null);\n      }\n    }\n    return new DefaultRowBasedColumnarBatch(outputSchema, rows);\n  }\n\n  @Override\n  public CloseableIterator<ColumnarBatch> readJsonFiles(\n      CloseableIterator<FileStatus> scanFileIter,\n      StructType physicalSchema,\n      Optional<Predicate> predicate)\n      throws IOException {\n    return new CloseableIterator<ColumnarBatch>() {\n      private FileStatus currentFile;\n      private BufferedReader currentFileReader;\n      private String nextLine;\n\n      @Override\n      public void close() throws IOException {\n        Utils.closeCloseables(currentFileReader, scanFileIter);\n      }\n\n      @Override\n      public boolean hasNext() {\n        if (nextLine != null) {\n          return true; // we have un-consumed last read line\n        }\n\n        // There is no file in reading or the current file being read has no more data\n        // initialize the next file reader or return false if there are no more files to\n        // read.\n        try {\n          if (currentFileReader == null || (nextLine = currentFileReader.readLine()) == null) {\n            // `nextLine` will initially be null because `currentFileReader` is guaranteed\n            // to be null\n            if (tryOpenNextFile()) {\n              return hasNext();\n            }\n          }\n          return nextLine != null;\n        } catch (IOException ex) {\n          throw new KernelEngineException(\n              format(\"Error reading JSON file: %s\", currentFile.getPath()), ex);\n        }\n      }\n\n      @Override\n      public ColumnarBatch next() {\n        if (nextLine == null) {\n          throw new NoSuchElementException();\n        }\n\n        List<Row> rows = new ArrayList<>();\n        int currentBatchSize = 0;\n        do {\n          // hasNext already reads the next one and keeps it in member variable `nextLine`\n          rows.add(parseJson(nextLine, physicalSchema));\n          nextLine = null;\n          currentBatchSize++;\n        } while (currentBatchSize < maxBatchSize && hasNext());\n\n        return new DefaultRowBasedColumnarBatch(physicalSchema, rows);\n      }\n\n      private boolean tryOpenNextFile() throws IOException {\n        Utils.closeCloseables(currentFileReader); // close the current opened file\n        currentFileReader = null;\n\n        if (scanFileIter.hasNext()) {\n          currentFile = scanFileIter.next();\n          SeekableInputStream stream = null;\n          try {\n            stream = fileIO.newInputFile(currentFile.getPath(), currentFile.getSize()).newStream();\n            currentFileReader =\n                new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));\n          } catch (Exception e) {\n            Utils.closeCloseablesSilently(stream); // close it avoid leaking resources\n            throw e;\n          }\n        }\n        return currentFileReader != null;\n      }\n    };\n  }\n\n  /**\n   * Makes use of {@link LogStore} implementations in `delta-storage` to atomically write the data\n   * to a file depending upon the destination filesystem.\n   *\n   * @param filePath Destination file path\n   * @param data Data to write as Json\n   * @throws IOException\n   */\n  @Override\n  public void writeJsonFileAtomically(\n      String filePath, CloseableIterator<Row> data, boolean overwrite) throws IOException {\n    fileIO.newOutputFile(filePath).writeAtomically(data.map(JsonUtils::rowToJson), overwrite);\n  }\n\n  private Row parseJson(String json, StructType readSchema) {\n    try {\n      final JsonNode jsonNode = objectReaderReadBigDecimals.readTree(json);\n      return new DefaultJsonRow((ObjectNode) jsonNode, readSchema);\n    } catch (JsonProcessingException ex) {\n      throw new KernelEngineException(format(\"Could not parse JSON: %s\", json), ex);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/DefaultParquetHandler.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.internal.parquet.ParquetFileReader;\nimport io.delta.kernel.defaults.internal.parquet.ParquetFileWriter;\nimport io.delta.kernel.engine.FileReadResult;\nimport io.delta.kernel.engine.ParquetHandler;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.*;\nimport io.delta.kernel.utils.FileStatus;\nimport io.delta.storage.LogStore;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\n\n/** Default implementation of {@link ParquetHandler} based on Hadoop APIs. */\npublic class DefaultParquetHandler implements ParquetHandler {\n  private final FileIO fileIO;\n\n  /**\n   * Create an instance of default {@link ParquetHandler} implementation.\n   *\n   * @param fileIO File IO implementation to use for reading and writing files.\n   */\n  public DefaultParquetHandler(FileIO fileIO) {\n    this.fileIO = Objects.requireNonNull(fileIO, \"fileIO is null\");\n  }\n\n  @Override\n  public CloseableIterator<FileReadResult> readParquetFiles(\n      CloseableIterator<FileStatus> fileIter,\n      StructType physicalSchema,\n      Optional<Predicate> predicate)\n      throws IOException {\n    return new CloseableIterator<FileReadResult>() {\n      private final ParquetFileReader batchReader = new ParquetFileReader(fileIO);\n      private CloseableIterator<ColumnarBatch> currentFileReader;\n      private String currentFilePath;\n\n      @Override\n      public void close() throws IOException {\n        Utils.closeCloseables(currentFileReader, fileIter);\n      }\n\n      @Override\n      public boolean hasNext() {\n        if (currentFileReader != null && currentFileReader.hasNext()) {\n          return true;\n        } else {\n          // There is no file in reading or the current file being read has no more data.\n          // Initialize the next file reader or return false if there are no more files to\n          // read.\n          Utils.closeCloseables(currentFileReader);\n          currentFileReader = null;\n          currentFilePath = null;\n          if (fileIter.hasNext()) {\n            FileStatus fileStatus = fileIter.next();\n            currentFileReader = batchReader.read(fileStatus, physicalSchema, predicate);\n            currentFilePath = fileStatus.getPath();\n            return hasNext(); // recurse since it's possible the loaded file is empty\n          } else {\n            return false;\n          }\n        }\n      }\n\n      @Override\n      public FileReadResult next() {\n        return new FileReadResult(currentFileReader.next(), currentFilePath);\n      }\n    };\n  }\n\n  @Override\n  public CloseableIterator<DataFileStatus> writeParquetFiles(\n      String directoryPath,\n      CloseableIterator<FilteredColumnarBatch> dataIter,\n      List<Column> statsColumns)\n      throws IOException {\n    ParquetFileWriter batchWriter =\n        ParquetFileWriter.multiFileWriter(fileIO, directoryPath, statsColumns);\n    return batchWriter.write(dataIter);\n  }\n\n  /**\n   * Makes use of {@link LogStore} implementations in `delta-storage` to atomically write the data\n   * to a file depending upon the destination filesystem.\n   *\n   * @param filePath Fully qualified destination file path\n   * @param data Iterator of {@link FilteredColumnarBatch}\n   * @throws IOException\n   */\n  @Override\n  public void writeParquetFileAtomically(\n      String filePath, CloseableIterator<FilteredColumnarBatch> data) throws IOException {\n\n    try {\n      ParquetFileWriter fileWriter =\n          ParquetFileWriter.singleFileWriter(\n              fileIO,\n              filePath,\n              /* atomicWrite= */ true,\n              /* statsColumns= */ Collections.emptyList());\n      fileWriter.write(data).next(); // TODO: fix this\n    } catch (UncheckedIOException e) {\n      throw e.getCause();\n    } finally {\n      Utils.closeCloseables(data);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/LoggingMetricsReporter.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine;\n\nimport io.delta.kernel.engine.MetricsReporter;\nimport io.delta.kernel.metrics.MetricsReport;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.core.JsonProcessingException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * An implementation of {@link MetricsReporter} that logs the reports (as JSON) to Log4J at the info\n * level.\n */\npublic class LoggingMetricsReporter implements MetricsReporter {\n\n  private static final Logger logger = LoggerFactory.getLogger(LoggingMetricsReporter.class);\n\n  @Override\n  public void report(MetricsReport report) {\n    try {\n      logger.info(\"{} = {}\", report.getClass().getName(), report.toJson());\n    } catch (JsonProcessingException e) {\n      logger.warn(\"Serialization issue while logging metrics report {}: {}\", report, e.toString());\n    } catch (Exception e) {\n      logger.warn(\"Unexpected error while logging metrics report {}:\", report, e);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/fileio/FileIO.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.fileio;\n\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.util.Optional;\n\n/**\n * Interface for file IO operations. Connectors can implement their own version of the {@link\n * FileIO} depending upon their environment. The {@link DefaultEngine} takes {@link FileIO} instance\n * as input and all I/O operations from the default engine are done using the passed in {@link\n * FileIO} instance.\n */\npublic interface FileIO {\n  /**\n   * List the paths in the same directory that are lexicographically greater or equal to (UTF-8\n   * sorting) the given `path`. The result should also be sorted by the file name.\n   *\n   * @param filePath Fully qualified path to a file\n   * @return Closeable iterator of files. It is the responsibility of the caller to close the\n   *     iterator.\n   * @throws FileNotFoundException if the file at the given path is not found\n   * @throws IOException for any other IO error.\n   */\n  CloseableIterator<FileStatus> listFrom(String filePath) throws IOException;\n\n  /**\n   * Get the metadata of the file at the given path.\n   *\n   * @param path Fully qualified path to the file.\n   * @return Metadata of the file.\n   * @throws IOException for any IO error.\n   */\n  FileStatus getFileStatus(String path) throws IOException;\n\n  /**\n   * Resolve the given path to a fully qualified path.\n   *\n   * @param path Input path\n   * @return Fully qualified path.\n   * @throws FileNotFoundException If the given path doesn't exist.\n   * @throws IOException for any other IO error.\n   */\n  String resolvePath(String path) throws IOException;\n\n  /**\n   * Create a directory at the given path including parent directories. This mimics the behavior of\n   * `mkdir -p` in Unix.\n   *\n   * @param path Full qualified path to create a directory at.\n   * @return true if the directory was created successfully, false otherwise.\n   * @throws IOException for any IO error.\n   */\n  boolean mkdirs(String path) throws IOException;\n\n  /**\n   * Get an {@link InputFile} for file at given path which can be used to read the file from any\n   * arbitrary position in the file.\n   *\n   * @param path Fully qualified path to the file.\n   * @param fileSize Size of the file in bytes.\n   * @return {@link InputFile} instance.\n   */\n  InputFile newInputFile(String path, long fileSize);\n\n  /**\n   * Create a {@link OutputFile} to write new file at the given path.\n   *\n   * @param path Fully qualified path to the file.\n   * @return {@link OutputFile} instance which can be used to write to the file.\n   */\n  OutputFile newOutputFile(String path);\n\n  /**\n   * Delete the file at given path.\n   *\n   * @param path the path to delete. If path is a directory throws an exception.\n   * @return true if delete is successful else false.\n   * @throws IOException for any IO error.\n   */\n  boolean delete(String path) throws IOException;\n\n  /**\n   * Get the configuration value for the given key.\n   *\n   * <p>TODO: should be in a separate interface? may be called ConfigurationProvider?\n   *\n   * @param confKey configuration key name\n   * @return If no such value is present, an {@link Optional#empty()} is returned.\n   */\n  Optional<String> getConf(String confKey);\n\n  /**\n   * Atomically copy a file from source path to destination path. The copy operation should be\n   * atomic to ensure that the destination file is either fully copied or not present at all.\n   *\n   * @param srcPath Fully qualified path to the source file to copy\n   * @param destPath Fully qualified path to the destination where the file will be copied\n   * @param overwrite If true, overwrite the destination file if it already exists. If false, throw\n   *     an exception if the destination exists.\n   * @throws java.nio.file.FileAlreadyExistsException if the destination file already exists and\n   *     {@code overwrite} is false.\n   * @throws FileNotFoundException if the source file does not exist\n   * @throws IOException for any other IO error\n   */\n  void copyFileAtomically(String srcPath, String destPath, boolean overwrite) throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/fileio/InputFile.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.fileio;\n\nimport java.io.IOException;\n\n/** Interface for reading a file and getting metadata about it. */\npublic interface InputFile {\n  /**\n   * Get the size of the file.\n   *\n   * @return the size of the file.\n   */\n  long length() throws IOException;\n\n  /**\n   * Get the path of the file.\n   *\n   * @return the path of the file.\n   */\n  String path();\n\n  /**\n   * Get the input stream to read the file.\n   *\n   * @return the input stream to read the file. It is the responsibility of the caller to close the\n   *     stream.\n   */\n  SeekableInputStream newStream() throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/fileio/OutputFile.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.fileio;\n\nimport io.delta.kernel.utils.CloseableIterator;\nimport java.io.IOException;\n\n/** Interface for writing to a file and getting metadata about it. */\npublic interface OutputFile {\n  /**\n   * Get the path of the file.\n   *\n   * @return the path of the file.\n   */\n  String path();\n\n  /**\n   * Get the output stream to write to the file.\n   *\n   * <ul>\n   *   <li>If the file already exists, (either at the time of creating the {@link\n   *       PositionOutputStream} or at the time of closing it), it will be overwritten.\n   *   <li>if {@code atomicWrite} is set, then the entire content is written or none, but won't\n   *       create a file with the partial contents.\n   * </ul>\n   *\n   * @return the output stream to write to the file. It is the responsibility of the caller to close\n   *     the stream.\n   * @throws IOException if an I/O error occurs.\n   */\n  PositionOutputStream create(boolean atomicWrite) throws IOException;\n\n  /**\n   * Atomically write (either write is completely or don't write all - i.e. don't leave file with\n   * partial content) the data to a file at the given path. If the file already exists do not\n   * replace it if {@code replace} is false. If {@code replace} is true, then replace the file with\n   * the new data.\n   *\n   * <p>TODO: the semantics are very loose here, see if there is a better API name. One of the\n   * reasons why the data is passed as an iterator is because of the existing LogStore interface\n   * which are used in the Hadoop implementation of the {@link FileIO}\n   *\n   * @param data the data to write. Each element in the iterator is a line in the file.\n   * @param overwrite if true, overwrite the file with the new data. If false, do not overwrite the\n   *     file.\n   * @throws java.nio.file.FileAlreadyExistsException if the file already exists and replace is\n   *     false.\n   * @throws IOException for any IO error.\n   */\n  void writeAtomically(CloseableIterator<String> data, boolean overwrite) throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/fileio/PositionOutputStream.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.fileio;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * Extends {@link OutputStream} to provide the current position in the stream. This stream is used\n * to write data into the file.\n */\npublic abstract class PositionOutputStream extends OutputStream {\n  /**\n   * Get the current position in the stream.\n   *\n   * @return the current position in bytes from the start of the stream\n   */\n  public abstract long getPos() throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/fileio/SeekableInputStream.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.fileio;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Extends {@link InputStream} to provide the current position in the stream and seek to a new\n * position. Also provides additional utility methods such as {@link #readFully(byte[], int, int)}.\n */\npublic abstract class SeekableInputStream extends InputStream {\n  /**\n   * Get the current position in the stream.\n   *\n   * @return the current position in bytes from the start of the stream\n   * @throws IOException if the underlying stream throws an IOException\n   */\n  public abstract long getPos() throws IOException;\n\n  /**\n   * Seek to a new position in the stream.\n   *\n   * @param newPos the new position to seek to\n   * @throws IOException if the underlying stream throws an IOException\n   */\n  public abstract void seek(long newPos) throws IOException;\n\n  /**\n   * Read fully len bytes into the buffer b.\n   *\n   * @param b byte array\n   * @param off offset in the byte array\n   * @param len number of bytes to read\n   * @throws java.io.EOFException – if this input stream reaches the end before reading all the\n   *     bytes.\n   * @throws IOException – the stream has been closed and the contained input stream does not\n   *     support reading after close, or another I/ O error occurs.\n   */\n  public abstract void readFully(byte[] b, int off, int len) throws IOException;\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/hadoopio/HadoopFileIO.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.hadoopio;\n\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.fileio.InputFile;\nimport io.delta.kernel.defaults.engine.fileio.OutputFile;\nimport io.delta.kernel.defaults.internal.logstore.LogStoreProvider;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport io.delta.storage.LogStore;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.Objects;\nimport java.util.Optional;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\n/** Implementation of {@link FileIO} based on Hadoop APIs. */\npublic class HadoopFileIO implements FileIO {\n  private final Configuration hadoopConf;\n\n  public HadoopFileIO(Configuration hadoopConf) {\n    this.hadoopConf = Objects.requireNonNull(hadoopConf, \"hadoopConf is null\");\n  }\n\n  @Override\n  public CloseableIterator<FileStatus> listFrom(String filePath) throws IOException {\n    Path path = new Path(filePath);\n    LogStore logStore = LogStoreProvider.getLogStore(hadoopConf, path.toUri().getScheme());\n\n    return Utils.toCloseableIterator(logStore.listFrom(path, hadoopConf))\n        .map(\n            hadoopFileStatus ->\n                FileStatus.of(\n                    hadoopFileStatus.getPath().toString(),\n                    hadoopFileStatus.getLen(),\n                    hadoopFileStatus.getModificationTime()));\n  }\n\n  @Override\n  public FileStatus getFileStatus(String path) throws IOException {\n    Path pathObject = new Path(path);\n    FileSystem fs = pathObject.getFileSystem(hadoopConf);\n    org.apache.hadoop.fs.FileStatus hadoopFileStatus = fs.getFileStatus(pathObject);\n    return FileStatus.of(\n        hadoopFileStatus.getPath().toString(),\n        hadoopFileStatus.getLen(),\n        hadoopFileStatus.getModificationTime());\n  }\n\n  @Override\n  public String resolvePath(String path) throws IOException {\n    Path pathObject = new Path(path);\n    FileSystem fs = pathObject.getFileSystem(hadoopConf);\n    return fs.makeQualified(pathObject).toString();\n  }\n\n  @Override\n  public boolean mkdirs(String path) throws IOException {\n    Path pathObject = new Path(path);\n    FileSystem fs = pathObject.getFileSystem(hadoopConf);\n    return fs.mkdirs(pathObject);\n  }\n\n  @Override\n  public InputFile newInputFile(String path, long fileSize) {\n    return new HadoopInputFile(getFs(path), new Path(path), fileSize);\n  }\n\n  @Override\n  public OutputFile newOutputFile(String path) {\n    return new HadoopOutputFile(hadoopConf, path);\n  }\n\n  @Override\n  public boolean delete(String path) throws IOException {\n    FileSystem fs = getFs(path);\n    return fs.delete(new Path(path), false);\n  }\n\n  @Override\n  public Optional<String> getConf(String confKey) {\n    return Optional.ofNullable(hadoopConf.get(confKey));\n  }\n\n  @Override\n  public void copyFileAtomically(String srcPath, String destPath, boolean overwrite)\n      throws IOException {\n    Path parsedSrcPath = new Path(srcPath);\n    Path parsedDestPath = new Path(destPath);\n    LogStore logStore = LogStoreProvider.getLogStore(hadoopConf, parsedSrcPath.toUri().getScheme());\n\n    try (io.delta.storage.CloseableIterator<String> srcContents =\n        logStore.read(parsedSrcPath, hadoopConf)) {\n      logStore.write(parsedDestPath, srcContents, overwrite, hadoopConf);\n    }\n  }\n\n  private FileSystem getFs(String path) {\n    try {\n      Path pathObject = new Path(path);\n      return pathObject.getFileSystem(hadoopConf);\n    } catch (IOException e) {\n      throw new UncheckedIOException(\"Could not resolve the FileSystem\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/hadoopio/HadoopInputFile.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.hadoopio;\n\nimport io.delta.kernel.defaults.engine.fileio.InputFile;\nimport io.delta.kernel.defaults.engine.fileio.SeekableInputStream;\nimport java.io.IOException;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\npublic class HadoopInputFile implements InputFile {\n  private final FileSystem fs;\n  private final Path path;\n  private final long fileSize;\n\n  public HadoopInputFile(FileSystem fs, Path path, long fileSize) {\n    this.fs = fs;\n    this.path = path;\n    this.fileSize = fileSize;\n  }\n\n  @Override\n  public long length() throws IOException {\n    return fileSize;\n  }\n\n  @Override\n  public String path() {\n    return path.toString();\n  }\n\n  @Override\n  public SeekableInputStream newStream() throws IOException {\n    return new HadoopSeekableInputStream(fs.open(path));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/hadoopio/HadoopOutputFile.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.hadoopio;\n\nimport static java.lang.String.format;\n\nimport io.delta.kernel.defaults.engine.fileio.OutputFile;\nimport io.delta.kernel.defaults.engine.fileio.PositionOutputStream;\nimport io.delta.kernel.defaults.internal.logstore.LogStoreProvider;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.storage.LogStore;\nimport java.io.IOException;\nimport java.util.Objects;\nimport java.util.UUID;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\npublic class HadoopOutputFile implements OutputFile {\n  private final Configuration hadoopConf;\n  private final String path;\n\n  public HadoopOutputFile(Configuration hadoopConf, String path) {\n    this.hadoopConf = Objects.requireNonNull(hadoopConf, \"fs is null\");\n    this.path = Objects.requireNonNull(path, \"path is null\");\n  }\n\n  @Override\n  public String path() {\n    return path;\n  }\n\n  @Override\n  public PositionOutputStream create(boolean putIfAbsent) throws IOException {\n    Path targetPath = new Path(path);\n    FileSystem fs = targetPath.getFileSystem(hadoopConf);\n    if (!putIfAbsent) {\n      return new HadoopPositionOutputStream(fs.create(targetPath));\n    }\n    LogStore logStore = LogStoreProvider.getLogStore(hadoopConf, targetPath.toUri().getScheme());\n\n    boolean useRename = logStore.isPartialWriteVisible(targetPath, hadoopConf);\n\n    final Path writePath;\n    if (useRename) {\n      // In order to atomically write the file, write to a temp file and rename\n      // to target path\n      String tempFileName = format(\".%s.%s.tmp\", targetPath.getName(), UUID.randomUUID());\n      writePath = new Path(targetPath.getParent(), tempFileName);\n    } else {\n      writePath = targetPath;\n    }\n\n    return new HadoopPositionOutputStream(fs.create(writePath)) {\n      @Override\n      public void close() throws IOException {\n        super.close();\n        if (useRename) {\n          boolean renameDone = false;\n          try {\n            renameDone = fs.rename(writePath, targetPath);\n            if (!renameDone) {\n              if (fs.exists(targetPath)) {\n                throw new java.nio.file.FileAlreadyExistsException(\n                    \"target file already exists: \" + targetPath);\n              }\n              throw new IOException(\"Failed to rename the file\");\n            }\n          } finally {\n            if (!renameDone) {\n              fs.delete(writePath, false /* recursive */);\n            }\n          }\n        }\n      }\n    };\n  }\n\n  @Override\n  public void writeAtomically(CloseableIterator<String> data, boolean overwrite)\n      throws IOException {\n    Path pathObj = new Path(path);\n    try {\n      LogStore logStore = LogStoreProvider.getLogStore(hadoopConf, pathObj.toUri().getScheme());\n      logStore.write(pathObj, data, overwrite, hadoopConf);\n    } finally {\n      Utils.closeCloseables(data);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/hadoopio/HadoopPositionOutputStream.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.hadoopio;\n\nimport io.delta.kernel.defaults.engine.fileio.PositionOutputStream;\nimport java.io.IOException;\nimport org.apache.hadoop.fs.FSDataOutputStream;\n\npublic class HadoopPositionOutputStream extends PositionOutputStream {\n  private final FSDataOutputStream delegateStream;\n\n  public HadoopPositionOutputStream(FSDataOutputStream delegateStream) {\n    this.delegateStream = delegateStream;\n  }\n\n  @Override\n  public void write(int b) throws IOException {\n    delegateStream.write(b);\n  }\n\n  @Override\n  public void write(byte[] b) throws IOException {\n    delegateStream.write(b);\n  }\n\n  @Override\n  public void write(byte[] b, int off, int len) throws IOException {\n    delegateStream.write(b, off, len);\n  }\n\n  @Override\n  public void flush() throws IOException {\n    delegateStream.flush();\n  }\n\n  @Override\n  public void close() throws IOException {\n    delegateStream.close();\n  }\n\n  @Override\n  public long getPos() throws IOException {\n    return delegateStream.getPos();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/hadoopio/HadoopSeekableInputStream.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine.hadoopio;\n\nimport io.delta.kernel.defaults.engine.fileio.SeekableInputStream;\nimport java.io.IOException;\nimport java.util.Objects;\nimport org.apache.hadoop.fs.FSDataInputStream;\n\npublic class HadoopSeekableInputStream extends SeekableInputStream {\n  private final FSDataInputStream delegateStream;\n\n  public HadoopSeekableInputStream(FSDataInputStream delegateStream) {\n    this.delegateStream = Objects.requireNonNull(delegateStream, \"delegateStream is null\");\n  }\n\n  @Override\n  public int read() throws java.io.IOException {\n    return delegateStream.read();\n  }\n\n  @Override\n  public int read(byte[] b) throws java.io.IOException {\n    return delegateStream.read(b);\n  }\n\n  @Override\n  public int read(byte[] b, int off, int len) throws java.io.IOException {\n    return delegateStream.read(b, off, len);\n  }\n\n  @Override\n  public long skip(long n) throws java.io.IOException {\n    return delegateStream.skip(n);\n  }\n\n  @Override\n  public int available() throws java.io.IOException {\n    return delegateStream.available();\n  }\n\n  @Override\n  public void close() throws java.io.IOException {\n    delegateStream.close();\n  }\n\n  @Override\n  public void seek(long pos) throws java.io.IOException {\n    delegateStream.seek(pos);\n  }\n\n  @Override\n  public long getPos() throws java.io.IOException {\n    return delegateStream.getPos();\n  }\n\n  @Override\n  public void readFully(byte[] b, int off, int len) throws IOException {\n    delegateStream.readFully(b, off, len);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/engine/package-info.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Default implementation of {@link io.delta.kernel.engine.Engine} interface and the sub-interfaces\n * exposed by the {@link io.delta.kernel.engine.Engine}.\n */\npackage io.delta.kernel.defaults.engine;\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/DefaultEngineErrors.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal;\n\nimport static java.lang.String.format;\n\nimport io.delta.kernel.expressions.Expression;\n\npublic class DefaultEngineErrors {\n\n  public static IllegalArgumentException canNotInstantiateLogStore(\n      String logStoreClassName, String context, Exception cause) {\n    String msg =\n        format(\"Can not instantiate `LogStore` class (%s): %s\", context, logStoreClassName);\n    return new IllegalArgumentException(msg, cause);\n  }\n\n  /**\n   * Exception for when the default expression evaluator cannot evaluate an expression.\n   *\n   * @param expression the unsupported expression\n   * @param reason reason for why the expression is not supported/cannot be evaluated\n   */\n  public static UnsupportedOperationException unsupportedExpressionException(\n      Expression expression, String reason) {\n    String message =\n        format(\n            \"Default expression evaluator cannot evaluate the expression: %s. Reason: %s\",\n            expression, reason);\n    return new UnsupportedOperationException(message);\n  }\n\n  /**\n   * Exception class for invalid escape sequence used in input for LIKE expressions\n   *\n   * @param pattern the invalid pattern\n   * @param index character index of occurrence of the offending escape in the pattern\n   */\n  public static IllegalArgumentException invalidEscapeSequence(String pattern, int index) {\n    return new IllegalArgumentException(\n        format(\"LIKE expression has invalid escape sequence '%s' at index %d\", pattern, index));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/DefaultKernelUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal;\n\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.util.DateTimeConstants;\nimport io.delta.kernel.internal.util.TimestampUtils;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructType;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeFormatterBuilder;\nimport java.time.temporal.ChronoField;\nimport java.util.concurrent.TimeUnit;\n\npublic class DefaultKernelUtils {\n  private static final DateTimeFormatter DEFAULT_JSON_TIMESTAMPNTZ_FORMATTER =\n      new DateTimeFormatterBuilder()\n          .appendPattern(\"yyyy-MM-dd'T'HH:mm:ss\")\n          .optionalStart()\n          .appendFraction(ChronoField.MICRO_OF_SECOND, 0, 6, true)\n          .optionalEnd()\n          .toFormatter();\n\n  private DefaultKernelUtils() {}\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Below utils are adapted from org.apache.spark.sql.catalyst.util.DateTimeUtils\n  //////////////////////////////////////////////////////////////////////////////////\n\n  // See http://stackoverflow.com/questions/466321/convert-unix-timestamp-to-julian\n  // It's 2440587.5, rounding up to be compatible with Hive.\n  static int JULIAN_DAY_OF_EPOCH = 2440588;\n\n  /** Returns the number of microseconds since epoch from Julian day and nanoseconds in a day. */\n  public static long fromJulianDay(int days, long nanos) {\n    // use Long to avoid rounding errors\n    return ((long) (days - JULIAN_DAY_OF_EPOCH)) * DateTimeConstants.MICROS_PER_DAY\n        + nanos / DateTimeConstants.NANOS_PER_MICROS;\n  }\n\n  /**\n   * Returns Julian day and remaining nanoseconds from the number of microseconds\n   *\n   * <p>Note: support timestamp since 4717 BC (without negative nanoseconds, compatible with Hive).\n   */\n  public static Tuple2<Integer, Long> toJulianDay(long micros) {\n    long julianUs = micros + JULIAN_DAY_OF_EPOCH * DateTimeConstants.MICROS_PER_DAY;\n    long days = julianUs / DateTimeConstants.MICROS_PER_DAY;\n    long us = julianUs % DateTimeConstants.MICROS_PER_DAY;\n    return new Tuple2<>((int) days, TimeUnit.MICROSECONDS.toNanos(us));\n  }\n\n  public static long millisToMicros(long millis) {\n    return Math.multiplyExact(millis, DateTimeConstants.MICROS_PER_MILLIS);\n  }\n\n  /**\n   * Converts a number of days since epoch (1970-01-01 00:00:00 UTC) to microseconds between epoch\n   * and start of the day in the given timezone.\n   */\n  public static long daysToMicros(int days, ZoneOffset timezone) {\n    long seconds = LocalDate.ofEpochDay(days).atStartOfDay(timezone).toEpochSecond();\n    return seconds * DateTimeConstants.MICROS_PER_SECOND;\n  }\n\n  /**\n   * Parses a TimestampNTZ string in UTC format, supporting milliseconds and microseconds, to\n   * microseconds since the Unix epoch.\n   *\n   * @param timestampString the timestamp string to parse.\n   * @return the number of microseconds since epoch.\n   */\n  public static long parseTimestampNTZ(String timestampString) {\n    LocalDateTime time = LocalDateTime.parse(timestampString, DEFAULT_JSON_TIMESTAMPNTZ_FORMATTER);\n    return TimestampUtils.toEpochMicros(time);\n  }\n\n  /**\n   * Search for the data type of the given column in the schema.\n   *\n   * @param schema the schema to search\n   * @param column the column whose data type is to be found\n   * @return the data type of the column\n   * @throws IllegalArgumentException if the column is not found in the schema\n   */\n  public static DataType getDataType(StructType schema, Column column) {\n    DataType dataType = schema;\n    for (String part : column.getNames()) {\n      if (!(dataType instanceof StructType)) {\n        throw new IllegalArgumentException(\n            String.format(\"Cannot resolve column (%s) in schema: %s\", column, schema));\n      }\n      StructType structType = (StructType) dataType;\n      if (structType.fieldNames().contains(part)) {\n        dataType = structType.get(part).getDataType();\n      } else {\n        throw new IllegalArgumentException(\n            String.format(\"Cannot resolve column (%s) in schema: %s\", column, schema));\n      }\n    }\n    return dataType;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/DefaultColumnarBatch.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class DefaultColumnarBatch implements ColumnarBatch {\n  private final int size;\n  private final StructType schema;\n  private final List<ColumnVector> columnVectors;\n\n  public DefaultColumnarBatch(int size, StructType schema, ColumnVector[] columnVectors) {\n    this.schema = schema;\n    this.size = size;\n    this.columnVectors = Collections.unmodifiableList(Arrays.asList(columnVectors));\n  }\n\n  @Override\n  public StructType getSchema() {\n    return schema;\n  }\n\n  @Override\n  public ColumnVector getColumnVector(int ordinal) {\n    checkColumnOrdinal(ordinal);\n    return columnVectors.get(ordinal);\n  }\n\n  @Override\n  public ColumnarBatch withNewColumn(\n      int ordinal, StructField structField, ColumnVector columnVector) {\n    if (ordinal < 0 || ordinal > columnVectors.size()) {\n      throw new IllegalArgumentException(\"Invalid ordinal: \" + ordinal);\n    }\n\n    if (columnVector == null || columnVector.getSize() != size) {\n      throw new IllegalArgumentException(\n          \"given vector size is not matching the current batch size\");\n    }\n\n    // Update the schema\n    ArrayList<StructField> newStructFields = new ArrayList<>(schema.fields());\n    newStructFields.ensureCapacity(schema.length() + 1);\n    newStructFields.add(ordinal, structField);\n    StructType newSchema = new StructType(newStructFields);\n\n    // Update the vectors\n    ArrayList<ColumnVector> newColumnVectors = new ArrayList<>(columnVectors);\n    newColumnVectors.ensureCapacity(columnVectors.size() + 1);\n    newColumnVectors.add(ordinal, columnVector);\n\n    return new DefaultColumnarBatch(size, newSchema, newColumnVectors.toArray(new ColumnVector[0]));\n  }\n\n  @Override\n  public ColumnarBatch withDeletedColumnAt(int ordinal) {\n    if (ordinal < 0 || ordinal >= columnVectors.size()) {\n      throw new IllegalArgumentException(\"Invalid ordinal: \" + ordinal);\n    }\n\n    // Update the schema\n    ArrayList<StructField> newStructFields = new ArrayList<>(schema.fields());\n    newStructFields.remove(ordinal);\n    StructType newSchema = new StructType(newStructFields);\n\n    // Update the vectors\n    ArrayList<ColumnVector> newColumnVectors = new ArrayList<>(columnVectors);\n    newColumnVectors.remove(ordinal);\n\n    return new DefaultColumnarBatch(size, newSchema, newColumnVectors.toArray(new ColumnVector[0]));\n  }\n\n  @Override\n  public ColumnarBatch withNewSchema(StructType newSchema) {\n    if (!schema.equivalent(newSchema)) {\n      throw new IllegalArgumentException(\n          \"Given new schema data type is not same as the existing schema\");\n    }\n\n    return new DefaultColumnarBatch(size, newSchema, columnVectors.toArray(new ColumnVector[0]));\n  }\n\n  @Override\n  public int getSize() {\n    return size;\n  }\n\n  private void checkColumnOrdinal(int ordinal) {\n    if (ordinal < 0 || ordinal >= columnVectors.size()) {\n      throw new IllegalArgumentException(\"invalid column ordinal: \" + ordinal);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/DefaultJsonRow.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.internal.DefaultKernelUtils;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultGenericVector;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport io.delta.kernel.internal.util.TimestampUtils;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.sql.Date;\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\npublic class DefaultJsonRow implements Row {\n  private final Object[] parsedValues;\n  private final StructType readSchema;\n\n  public DefaultJsonRow(ObjectNode rootNode, StructType readSchema) {\n    this.readSchema = readSchema;\n    this.parsedValues = new Object[readSchema.length()];\n\n    for (int i = 0; i < readSchema.length(); i++) {\n      final StructField field = readSchema.at(i);\n      final Object parsedValue = decodeField(rootNode, field);\n      parsedValues[i] = parsedValue;\n    }\n  }\n\n  @Override\n  public StructType getSchema() {\n    return readSchema;\n  }\n\n  @Override\n  public boolean isNullAt(int ordinal) {\n    return parsedValues[ordinal] == null;\n  }\n\n  @Override\n  public boolean getBoolean(int ordinal) {\n    return (boolean) parsedValues[ordinal];\n  }\n\n  @Override\n  public byte getByte(int ordinal) {\n    return (byte) parsedValues[ordinal];\n  }\n\n  @Override\n  public short getShort(int ordinal) {\n    return (short) parsedValues[ordinal];\n  }\n\n  @Override\n  public int getInt(int ordinal) {\n    return (int) parsedValues[ordinal];\n  }\n\n  @Override\n  public long getLong(int ordinal) {\n    return (long) parsedValues[ordinal];\n  }\n\n  @Override\n  public float getFloat(int ordinal) {\n    return (float) parsedValues[ordinal];\n  }\n\n  @Override\n  public double getDouble(int ordinal) {\n    return (double) parsedValues[ordinal];\n  }\n\n  @Override\n  public String getString(int ordinal) {\n    return (String) parsedValues[ordinal];\n  }\n\n  @Override\n  public BigDecimal getDecimal(int ordinal) {\n    return (BigDecimal) parsedValues[ordinal];\n  }\n\n  @Override\n  public byte[] getBinary(int ordinal) {\n    throw new UnsupportedOperationException(\"not yet implemented\");\n  }\n\n  @Override\n  public Row getStruct(int ordinal) {\n    return (DefaultJsonRow) parsedValues[ordinal];\n  }\n\n  @Override\n  public ArrayValue getArray(int ordinal) {\n    return (ArrayValue) parsedValues[ordinal];\n  }\n\n  @Override\n  public MapValue getMap(int ordinal) {\n    return (MapValue) parsedValues[ordinal];\n  }\n\n  private static void throwIfTypeMismatch(String expType, boolean hasExpType, JsonNode jsonNode) {\n    if (!hasExpType) {\n      throw new RuntimeException(\n          String.format(\"Couldn't decode %s, expected a %s\", jsonNode, expType));\n    }\n  }\n\n  private static Object decodeElement(JsonNode jsonValue, DataType dataType) {\n    if (jsonValue.isNull()) {\n      return null;\n    }\n\n    if (dataType instanceof BooleanType) {\n      throwIfTypeMismatch(\"boolean\", jsonValue.isBoolean(), jsonValue);\n      return jsonValue.booleanValue();\n    }\n\n    if (dataType instanceof ByteType) {\n      throwIfTypeMismatch(\n          \"byte\",\n          jsonValue.canConvertToExactIntegral()\n              && jsonValue.canConvertToInt()\n              && jsonValue.intValue() <= Byte.MAX_VALUE\n              && jsonValue.canConvertToInt()\n              && jsonValue.intValue() >= Byte.MIN_VALUE,\n          jsonValue);\n      return jsonValue.numberValue().byteValue();\n    }\n\n    if (dataType instanceof ShortType) {\n      throwIfTypeMismatch(\n          \"short\",\n          jsonValue.canConvertToExactIntegral()\n              && jsonValue.canConvertToInt()\n              && jsonValue.intValue() <= Short.MAX_VALUE\n              && jsonValue.canConvertToInt()\n              && jsonValue.intValue() >= Short.MIN_VALUE,\n          jsonValue);\n      return jsonValue.numberValue().shortValue();\n    }\n\n    if (dataType instanceof IntegerType) {\n      throwIfTypeMismatch(\n          \"integer\", jsonValue.isIntegralNumber() && jsonValue.canConvertToInt(), jsonValue);\n      return jsonValue.intValue();\n    }\n\n    if (dataType instanceof LongType) {\n      throwIfTypeMismatch(\n          \"long\", jsonValue.isIntegralNumber() && jsonValue.canConvertToLong(), jsonValue);\n      return jsonValue.numberValue().longValue();\n    }\n\n    if (dataType instanceof FloatType) {\n      switch (jsonValue.getNodeType()) {\n        case NUMBER:\n          throwIfTypeMismatch(\n              \"float\",\n              // floatValue() will be converted to +/-INF if it cannot be represented\n              // by a float\n              // Note it is still possible to lose precision in this conversion but\n              // checking for that requires converting to a float and back to BigDecimal\n              !Float.isInfinite(jsonValue.floatValue()),\n              jsonValue);\n          return jsonValue.floatValue();\n        case STRING:\n          switch (jsonValue.asText()) {\n            case \"NaN\":\n              return Float.NaN;\n            case \"+INF\":\n            case \"+Infinity\":\n            case \"Infinity\":\n              return Float.POSITIVE_INFINITY;\n            case \"-INF\":\n            case \"-Infinity\":\n              return Float.NEGATIVE_INFINITY;\n          }\n        default:\n          throwIfTypeMismatch(\"float\", false, jsonValue);\n      }\n    }\n\n    if (dataType instanceof DoubleType) {\n      switch (jsonValue.getNodeType()) {\n        case NUMBER:\n          throwIfTypeMismatch(\n              \"double\",\n              // doubleValue() will be converted to +/-INF if it cannot be represented by\n              // a double\n              // Note it is still possible to lose precision in this conversion but\n              // checking for that requires converting to a double and back to BigDecimal\n              !Double.isInfinite(jsonValue.doubleValue()),\n              jsonValue);\n          return jsonValue.doubleValue();\n        case STRING:\n          switch (jsonValue.asText()) {\n            case \"NaN\":\n              return Double.NaN;\n            case \"+INF\":\n            case \"+Infinity\":\n            case \"Infinity\":\n              return Double.POSITIVE_INFINITY;\n            case \"-INF\":\n            case \"-Infinity\":\n              return Double.NEGATIVE_INFINITY;\n          }\n        default:\n          throwIfTypeMismatch(\"double\", false, jsonValue);\n      }\n    }\n\n    if (dataType instanceof StringType) {\n      throwIfTypeMismatch(\"string\", jsonValue.isTextual(), jsonValue);\n      return jsonValue.asText();\n    }\n\n    if (dataType instanceof DecimalType) {\n      throwIfTypeMismatch(\"decimal\", jsonValue.isNumber(), jsonValue);\n      return jsonValue.decimalValue();\n    }\n\n    if (dataType instanceof DateType) {\n      throwIfTypeMismatch(\"date\", jsonValue.isTextual(), jsonValue);\n      return InternalUtils.daysSinceEpoch(Date.valueOf(jsonValue.textValue()));\n    }\n\n    if (dataType instanceof TimestampType) {\n      throwIfTypeMismatch(\"timestamp\", jsonValue.isTextual(), jsonValue);\n      Instant time = OffsetDateTime.parse(jsonValue.textValue()).toInstant();\n      return TimestampUtils.toEpochMicros(time);\n    }\n\n    if (dataType instanceof TimestampNTZType) {\n      throwIfTypeMismatch(\"timestamp_ntz\", jsonValue.isTextual(), jsonValue);\n      return DefaultKernelUtils.parseTimestampNTZ(jsonValue.textValue());\n    }\n\n    if (dataType instanceof StructType) {\n      throwIfTypeMismatch(\"object\", jsonValue.isObject(), jsonValue);\n      return new DefaultJsonRow((ObjectNode) jsonValue, (StructType) dataType);\n    }\n\n    if (dataType instanceof ArrayType) {\n      throwIfTypeMismatch(\"array\", jsonValue.isArray(), jsonValue);\n      final ArrayType arrayType = ((ArrayType) dataType);\n      final ArrayNode jsonArray = (ArrayNode) jsonValue;\n      final Object[] elements = new Object[jsonArray.size()];\n      for (int i = 0; i < jsonArray.size(); i++) {\n        final JsonNode element = jsonArray.get(i);\n        final Object parsedElement = decodeElement(element, arrayType.getElementType());\n        if (parsedElement == null && !arrayType.containsNull()) {\n          throw new RuntimeException(\n              \"Array type expects no nulls as elements, but \" + \"received `null` as array element\");\n        }\n        elements[i] = parsedElement;\n      }\n      return new ArrayValue() {\n        @Override\n        public int getSize() {\n          return elements.length;\n        }\n\n        @Override\n        public ColumnVector getElements() {\n          return DefaultGenericVector.fromArray(arrayType.getElementType(), elements);\n        }\n      };\n    }\n\n    if (dataType instanceof MapType) {\n      throwIfTypeMismatch(\"map\", jsonValue.isObject(), jsonValue);\n      final MapType mapType = (MapType) dataType;\n      if (!(mapType.getKeyType() instanceof StringType)) {\n        throw new RuntimeException(\n            \"MapType with a key type of `String` is supported, \"\n                + \"received a key type: \"\n                + mapType.getKeyType());\n      }\n      List<Object> keys = new ArrayList<>(jsonValue.size());\n      List<Object> values = new ArrayList<>(jsonValue.size());\n      final Iterator<Map.Entry<String, JsonNode>> iter = jsonValue.fields();\n\n      boolean isValueOfStringType = mapType.getValueType() instanceof StringType;\n      while (iter.hasNext()) {\n        Map.Entry<String, JsonNode> entry = iter.next();\n        String keyParsed = entry.getKey();\n\n        Object valueParsed = null;\n        if (isValueOfStringType) {\n          // Special handling for value which is of type string. Delta tables generated by\n          // Delta-Spark ended up having serializing values as their original type and not\n          // as string in the Delta commit files.\n          // Ex. {\"key\": true} instead of {\"key\": \"true\"}\n          if (!entry.getValue().isNull()) {\n            valueParsed = entry.getValue().asText();\n          }\n        } else {\n          valueParsed = decodeElement(entry.getValue(), mapType.getValueType());\n        }\n        if (valueParsed == null && !mapType.isValueContainsNull()) {\n          throw new RuntimeException(\n              \"Map type expects no nulls in values, but \" + \"received `null` as value\");\n        }\n        keys.add(keyParsed);\n        values.add(valueParsed);\n      }\n      return new MapValue() {\n        @Override\n        public int getSize() {\n          return jsonValue.size();\n        }\n\n        @Override\n        public ColumnVector getKeys() {\n          return DefaultGenericVector.fromList(mapType.getKeyType(), keys);\n        }\n\n        @Override\n        public ColumnVector getValues() {\n          return DefaultGenericVector.fromList(mapType.getValueType(), values);\n        }\n      };\n    }\n\n    throw new UnsupportedOperationException(\n        String.format(\"Unsupported DataType %s for RootNode %s\", dataType, jsonValue));\n  }\n\n  private static Object decodeField(ObjectNode rootNode, StructField field) {\n    if (rootNode.get(field.getName()) == null || rootNode.get(field.getName()).isNull()) {\n      if (field.isNullable()) {\n        return null;\n      }\n\n      throw new RuntimeException(\n          String.format(\n              \"Root node at key %s is null but field isn't nullable. Root node: %s\",\n              field.getName(), rootNode));\n    }\n\n    return decodeElement(rootNode.get(field.getName()), field.getDataType());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/DefaultRowBasedColumnarBatch.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultSubFieldVector;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * {@link ColumnarBatch} wrapper around list of {@link Row} objects. TODO: We should change the\n * {@link io.delta.kernel.defaults.engine.DefaultJsonHandler} to generate data in true columnar\n * format than wrapping a set of rows with a columnar batch interface.\n */\npublic class DefaultRowBasedColumnarBatch implements ColumnarBatch {\n  private final StructType schema;\n  private final List<Row> rows;\n\n  /**\n   * Holds the actual ColumnVectors, once the rows have been parsed for that column.\n   *\n   * <p>Uses lazy initialization, i.e. a value of Optional.empty() at an ordinal means we have not\n   * parsed the rows for that column yet.\n   */\n  private final List<Optional<ColumnVector>> columnVectors;\n\n  public DefaultRowBasedColumnarBatch(StructType schema, List<Row> rows) {\n    this.schema = schema;\n    this.rows = rows;\n    this.columnVectors = new ArrayList<>(schema.length());\n    for (int i = 0; i < schema.length(); i++) {\n      columnVectors.add(Optional.empty());\n    }\n  }\n\n  @Override\n  public StructType getSchema() {\n    return schema;\n  }\n\n  @Override\n  public int getSize() {\n    return rows.size();\n  }\n\n  @Override\n  public ColumnVector getColumnVector(int ordinal) {\n    if (ordinal < 0 || ordinal >= columnVectors.size()) {\n      throw new IllegalArgumentException(\"Invalid ordinal: \" + ordinal);\n    }\n\n    if (!columnVectors.get(ordinal).isPresent()) {\n      final StructField field = schema.at(ordinal);\n      final ColumnVector vector =\n          new DefaultSubFieldVector(\n              getSize(), field.getDataType(), ordinal, (rowId) -> rows.get(rowId));\n      columnVectors.set(ordinal, Optional.of(vector));\n    }\n\n    return columnVectors.get(ordinal).get();\n  }\n\n  @Override\n  public ColumnarBatch withNewColumn(\n      int ordinal, StructField columnSchema, ColumnVector columnVector) {\n    if (ordinal < 0 || ordinal >= columnVectors.size() + 1) {\n      throw new IllegalArgumentException(\"Invalid ordinal: \" + ordinal);\n    }\n\n    // Update the schema\n    final List<StructField> newStructFields = new ArrayList<>(schema.fields());\n    newStructFields.add(ordinal, columnSchema);\n    final StructType newSchema = new StructType(newStructFields);\n\n    for (int i = 0; i < columnVectors.size(); i++) {\n      getColumnVector(i);\n    }\n\n    // Add the vector at the target ordinal\n    final List<Optional<ColumnVector>> newColumnVectors = new ArrayList<>(columnVectors);\n    newColumnVectors.add(ordinal, Optional.of(columnVector));\n\n    // Fill the new array\n    ColumnVector[] newColumnVectorArr = new ColumnVector[newColumnVectors.size()];\n    for (int i = 0; i < newColumnVectorArr.length; i++) {\n      newColumnVectorArr[i] = newColumnVectors.get(i).get();\n    }\n\n    return new DefaultColumnarBatch(\n        getSize(), // # of rows hasn't changed\n        newSchema,\n        newColumnVectorArr);\n  }\n\n  /** TODO this implementation sucks */\n  @Override\n  public ColumnarBatch withDeletedColumnAt(int ordinal) {\n    if (ordinal < 0 || ordinal >= columnVectors.size()) {\n      throw new IllegalArgumentException(\"Invalid ordinal: \" + ordinal);\n    }\n\n    // Update the schema\n    final List<StructField> newStructFields = new ArrayList<>(schema.fields());\n    newStructFields.remove(ordinal);\n    final StructType newSchema = new StructType(newStructFields);\n\n    // Fill all the vectors, except the one being deleted\n    for (int i = 0; i < columnVectors.size(); i++) {\n      if (i == ordinal) {\n        continue;\n      }\n      getColumnVector(i);\n    }\n\n    // Delete the vector at the target ordinal\n    final List<Optional<ColumnVector>> newColumnVectors = new ArrayList<>(columnVectors);\n    newColumnVectors.remove(ordinal);\n\n    // Fill the new array\n    ColumnVector[] newColumnVectorArr = new ColumnVector[newColumnVectors.size()];\n    for (int i = 0; i < newColumnVectorArr.length; i++) {\n      newColumnVectorArr[i] = newColumnVectors.get(i).get();\n    }\n\n    return new DefaultColumnarBatch(\n        getSize(), // # of rows hasn't changed\n        newSchema,\n        newColumnVectorArr);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/AbstractColumnVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.types.DataType;\nimport java.math.BigDecimal;\nimport java.util.Optional;\n\n/**\n * Abstract implementation of {@link ColumnVector} that provides the default functionality common to\n * most of the specific data type {@link ColumnVector} implementations.\n */\npublic abstract class AbstractColumnVector implements ColumnVector {\n  private final int size;\n  private final DataType dataType;\n  private final Optional<boolean[]> nullability;\n\n  protected AbstractColumnVector(int size, DataType dataType, Optional<boolean[]> nullability) {\n    checkArgument(size >= 0, \"invalid size: %s\", size);\n    nullability.ifPresent(\n        array ->\n            checkArgument(\n                array.length >= size,\n                \"invalid number of values (%s) for given size (%s)\",\n                array.length,\n                size));\n    this.size = size;\n    this.dataType = requireNonNull(dataType);\n    this.nullability = requireNonNull(nullability);\n  }\n\n  @Override\n  public DataType getDataType() {\n    return dataType;\n  }\n\n  @Override\n  public int getSize() {\n    return size;\n  }\n\n  @Override\n  public void close() {\n    // By default, nothing to close, if the implementation has any resources to release\n    // it can override it\n  }\n\n  /**\n   * Is the value at given {@code rowId} index is null?\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public boolean isNullAt(int rowId) {\n    checkValidRowId(rowId);\n    if (!nullability.isPresent()) {\n      return false; // if there is no-nullability vector, every value is a non-null value\n    }\n    return nullability.get()[rowId];\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    throw unsupportedDataAccessException(\"boolean\");\n  }\n\n  @Override\n  public byte getByte(int rowId) {\n    throw unsupportedDataAccessException(\"byte\");\n  }\n\n  @Override\n  public short getShort(int rowId) {\n    throw unsupportedDataAccessException(\"short\");\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    throw unsupportedDataAccessException(\"int\");\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    throw unsupportedDataAccessException(\"long\");\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    throw unsupportedDataAccessException(\"float\");\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    throw unsupportedDataAccessException(\"double\");\n  }\n\n  @Override\n  public byte[] getBinary(int rowId) {\n    throw unsupportedDataAccessException(\"binary\");\n  }\n\n  @Override\n  public String getString(int rowId) {\n    throw unsupportedDataAccessException(\"string\");\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    throw unsupportedDataAccessException(\"decimal\");\n  }\n\n  @Override\n  public MapValue getMap(int rowId) {\n    throw unsupportedDataAccessException(\"map\");\n  }\n\n  @Override\n  public ArrayValue getArray(int rowId) {\n    throw unsupportedDataAccessException(\"array\");\n  }\n\n  // TODO no need to override these here; update default implementations in `ColumnVector`\n  //   to have a more informative exception message\n  protected UnsupportedOperationException unsupportedDataAccessException(String accessType) {\n    String msg =\n        String.format(\n            \"Trying to access a `%s` value from vector of type `%s`\", accessType, getDataType());\n    throw new UnsupportedOperationException(msg);\n  }\n\n  /**\n   * Helper method that make sure the given {@code rowId} position is valid in this vector\n   *\n   * @param rowId\n   */\n  protected void checkValidRowId(int rowId) {\n    if (rowId < 0 || rowId >= size) {\n      throw new IllegalArgumentException(\"invalid row access: \" + rowId);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultArrayVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.types.DataType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for array type data. */\npublic class DefaultArrayVector extends AbstractColumnVector {\n  private final int[] offsets;\n  private final ColumnVector elementVector;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for array type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param offsets Offsets into element vector on where the index of particular row values start\n   *     and end.\n   * @param elementVector Vector containing the array elements.\n   */\n  public DefaultArrayVector(\n      int size,\n      DataType type,\n      Optional<boolean[]> nullability,\n      int[] offsets,\n      ColumnVector elementVector) {\n    super(size, type, nullability);\n    checkArgument(offsets.length >= size + 1, \"invalid offset array size\");\n    this.offsets = requireNonNull(offsets, \"offsets is null\");\n    this.elementVector = requireNonNull(elementVector, \"elementVector is null\");\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public ArrayValue getArray(int rowId) {\n    checkValidRowId(rowId);\n    if (isNullAt(rowId)) {\n      return null;\n    }\n    // use the offsets array to find the starting and ending index in the underlying vector\n    // for this rowId\n    int start = offsets[rowId];\n    int end = offsets[rowId + 1];\n    return new ArrayValue() {\n\n      // create a view over the elements for this rowId\n      private final ColumnVector elements = new DefaultViewVector(elementVector, start, end);\n\n      @Override\n      public int getSize() {\n        return elements.getSize();\n      }\n\n      @Override\n      public ColumnVector getElements() {\n        return elements;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultBinaryVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.BinaryType;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StringType;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for binary type data. */\npublic class DefaultBinaryVector extends AbstractColumnVector {\n  private final byte[][] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for binary type.\n   *\n   * @param size number of elements in the vector.\n   * @param values column vector values.\n   */\n  public DefaultBinaryVector(DataType dataType, int size, byte[][] values) {\n    super(size, dataType, Optional.empty());\n    checkArgument(\n        dataType instanceof StringType || dataType instanceof BinaryType,\n        \"invalid type for binary vector: %s\",\n        dataType);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId] == null;\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null. The error check on {@code rowId} explicitly skipped for\n   * performance reasons.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public String getString(int rowId) {\n    if (!(getDataType() instanceof StringType)) {\n      throw unsupportedDataAccessException(\"string\");\n    }\n    checkValidRowId(rowId);\n    byte[] value = values[rowId];\n    if (value == null) {\n      return null;\n    }\n    return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)).toString();\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null. The error check on {@code rowId} explicitly skipped for\n   * performance reasons.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public byte[] getBinary(int rowId) {\n    if (!(getDataType() instanceof BinaryType)) {\n      throw unsupportedDataAccessException(\"binary\");\n    }\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultBooleanVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.BooleanType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for boolean type data. */\npublic class DefaultBooleanVector extends AbstractColumnVector {\n  private final boolean[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for boolean type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultBooleanVector(int size, Optional<boolean[]> nullability, boolean[] values) {\n    super(size, BooleanType.BOOLEAN, nullability);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public boolean getBoolean(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultByteVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.ByteType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for byte type data. */\npublic class DefaultByteVector extends AbstractColumnVector {\n  private final byte[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for byte type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultByteVector(int size, Optional<boolean[]> nullability, byte[] values) {\n    super(size, ByteType.BYTE, nullability);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public byte getByte(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultConstantVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport io.delta.kernel.types.DataType;\n\npublic class DefaultConstantVector extends DefaultGenericVector {\n\n  public DefaultConstantVector(DataType dataType, int numRows, Object value) {\n    // TODO: Validate datatype and value object type\n    super(numRows, dataType, (rowId) -> value);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultDecimalVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.DecimalType;\nimport java.math.BigDecimal;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for decimal type data. */\npublic class DefaultDecimalVector extends AbstractColumnVector {\n\n  private final BigDecimal[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for decimal type.\n   *\n   * @param size number of elements in the vector.\n   * @param values column vector values.\n   */\n  public DefaultDecimalVector(DataType dataType, int size, BigDecimal[] values) {\n\n    super(size, dataType, Optional.empty());\n    checkArgument(dataType instanceof DecimalType, \"invalid type for decimal vector: %s\", dataType);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId] == null;\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultDoubleVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.DoubleType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for double type data. */\npublic class DefaultDoubleVector extends AbstractColumnVector {\n  private final double[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for float type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultDoubleVector(int size, Optional<boolean[]> nullability, double[] values) {\n    super(size, DoubleType.DOUBLE, nullability);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public double getDouble(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultFloatVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.FloatType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for float type data. */\npublic class DefaultFloatVector extends AbstractColumnVector {\n  private final float[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for float type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultFloatVector(int size, Optional<boolean[]> nullability, float[] values) {\n    super(size, FloatType.FLOAT, nullability);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public float getFloat(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultGenericVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.function.Function;\n\n/** Generic column vector implementation to expose an array of objects as a column vector. */\npublic class DefaultGenericVector implements ColumnVector {\n\n  public static DefaultGenericVector fromArray(DataType dataType, Object[] elements) {\n    return new DefaultGenericVector(elements.length, dataType, rowId -> elements[rowId]);\n  }\n\n  public static DefaultGenericVector fromList(DataType dataType, List<Object> elements) {\n    return new DefaultGenericVector(elements.size(), dataType, rowId -> elements.get(rowId));\n  }\n\n  private final int size;\n  private final DataType dataType;\n  private final Function<Integer, Object> rowIdToValueAccessor;\n\n  protected DefaultGenericVector(\n      int size, DataType dataType, Function<Integer, Object> rowIdToValueAccessor) {\n    this.size = size;\n    this.dataType = dataType;\n    this.rowIdToValueAccessor = rowIdToValueAccessor;\n  }\n\n  @Override\n  public DataType getDataType() {\n    return dataType;\n  }\n\n  @Override\n  public int getSize() {\n    return size;\n  }\n\n  @Override\n  public void close() {}\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToValueAccessor.apply(rowId) == null;\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(BooleanType.class, \"boolean\");\n    return (boolean) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public byte getByte(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(ByteType.class, \"byte\");\n    return (byte) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public short getShort(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(ShortType.class, \"short\");\n    return (short) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(IntegerType.class, DateType.class, dataType.toString());\n    return (int) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(\n        LongType.class, TimestampType.class, TimestampNTZType.class, dataType.toString());\n    return (long) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(FloatType.class, \"float\");\n    return (float) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(DoubleType.class, \"double\");\n    return (double) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public String getString(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(StringType.class, \"string\");\n    return (String) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(DecimalType.class, \"decimal\");\n    return (BigDecimal) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public byte[] getBinary(int rowId) {\n    assertValidRowId(rowId);\n    throwIfUnsafeAccess(BinaryType.class, \"binary\");\n    return (byte[]) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public ArrayValue getArray(int rowId) {\n    assertValidRowId(rowId);\n    // TODO: not sufficient check, also need to check the element type\n    throwIfUnsafeAccess(ArrayType.class, \"array\");\n    return (ArrayValue) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public MapValue getMap(int rowId) {\n    assertValidRowId(rowId);\n    // TODO: not sufficient check, also need to check the element types\n    throwIfUnsafeAccess(MapType.class, \"map\");\n    return (MapValue) rowIdToValueAccessor.apply(rowId);\n  }\n\n  @Override\n  public ColumnVector getChild(int ordinal) {\n    throwIfUnsafeAccess(StructType.class, \"struct\");\n    StructType structType = (StructType) dataType;\n    return new DefaultSubFieldVector(\n        getSize(),\n        structType.at(ordinal).getDataType(),\n        ordinal,\n        (rowId) -> (Row) rowIdToValueAccessor.apply(rowId));\n  }\n\n  private void throwIfUnsafeAccess(Class<? extends DataType> expDataType, String accessType) {\n    if (!expDataType.isAssignableFrom(dataType.getClass())) {\n      String msg =\n          String.format(\n              \"Trying to access a `%s` value from vector of type `%s`\", accessType, dataType);\n      throw new UnsupportedOperationException(msg);\n    }\n  }\n\n  private void throwIfUnsafeAccess(\n      Class<? extends DataType> expDataType1,\n      Class<? extends DataType> expDataType2,\n      String accessType) {\n    if (!(expDataType1.isAssignableFrom(dataType.getClass())\n        || expDataType2.isAssignableFrom(dataType.getClass()))) {\n      String msg =\n          String.format(\n              \"Trying to access a `%s` value from vector of type `%s`\", accessType, dataType);\n      throw new UnsupportedOperationException(msg);\n    }\n  }\n\n  private void throwIfUnsafeAccess(\n      Class<? extends DataType> expDataType1,\n      Class<? extends DataType> expDataType2,\n      Class<? extends DataType> expDataType3,\n      String accessType) {\n    if (!(expDataType1.isAssignableFrom(dataType.getClass())\n        || expDataType2.isAssignableFrom(dataType.getClass())\n        || expDataType3.isAssignableFrom(dataType.getClass()))) {\n      String msg =\n          String.format(\n              \"Trying to access a `%s` value from vector of type `%s`\", accessType, dataType);\n      throw new UnsupportedOperationException(msg);\n    }\n  }\n\n  private void assertValidRowId(int rowId) {\n    checkArgument(rowId < size, \"Invalid rowId: %s, max allowed rowId is: %s\", rowId, (size - 1));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultIntVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.DateType;\nimport io.delta.kernel.types.IntegerType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for integer type data. */\npublic class DefaultIntVector extends AbstractColumnVector {\n  private final int[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for integer type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultIntVector(\n      DataType dataType, int size, Optional<boolean[]> nullability, int[] values) {\n    super(size, dataType, nullability);\n    checkArgument(dataType instanceof IntegerType || dataType instanceof DateType);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public int getInt(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultLongVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.types.*;\nimport java.util.Optional;\n\n/** {@link ColumnVector} implementation for long, timestamp or timestamp_ntz type data. */\npublic class DefaultLongVector extends AbstractColumnVector {\n  private final long[] values;\n\n  /**\n   * Create an instance of {@link ColumnVector} for long type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultLongVector(\n      DataType dataType, int size, Optional<boolean[]> nullability, long[] values) {\n    super(size, dataType, nullability);\n    checkArgument(\n        dataType instanceof LongType\n            || dataType instanceof TimestampType\n            || dataType instanceof TimestampNTZType);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public long getLong(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultMapVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.types.DataType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for map type data. */\npublic class DefaultMapVector extends AbstractColumnVector {\n  private final int[] offsets;\n  private final ColumnVector keyVector;\n  private final ColumnVector valueVector;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for map type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param offsets Offsets into key and value column vectors on where the index of particular row\n   *     values start and end.\n   * @param keyVector Vector containing the `key` values from the kv map.\n   * @param valueVector Vector containing the `value` values from the kv map.\n   */\n  public DefaultMapVector(\n      int size,\n      DataType type,\n      Optional<boolean[]> nullability,\n      int[] offsets,\n      ColumnVector keyVector,\n      ColumnVector valueVector) {\n    super(size, type, nullability);\n    checkArgument(offsets.length >= size + 1, \"invalid offset array size\");\n    this.offsets = requireNonNull(offsets, \"offsets is null\");\n    this.keyVector = requireNonNull(keyVector, \"keyVector is null\");\n    this.valueVector = requireNonNull(valueVector, \"valueVector is null\");\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public MapValue getMap(int rowId) {\n    checkValidRowId(rowId);\n    if (isNullAt(rowId)) {\n      return null;\n    }\n    // use the offsets array to find the starting and ending index in the underlying vectors\n    // for this rowId\n    int start = offsets[rowId];\n    int end = offsets[rowId + 1];\n    return new MapValue() {\n\n      // create a view over the keys and values for this rowId\n      private final ColumnVector keys = new DefaultViewVector(keyVector, start, end);\n      private final ColumnVector values = new DefaultViewVector(valueVector, start, end);\n\n      @Override\n      public int getSize() {\n        return keys.getSize();\n      }\n\n      @Override\n      public ColumnVector getKeys() {\n        return keys;\n      }\n\n      @Override\n      public ColumnVector getValues() {\n        return values;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultShortVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.ShortType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for short type data. */\npublic class DefaultShortVector extends AbstractColumnVector {\n  private final short[] values;\n\n  /**\n   * Create an instance of {@link io.delta.kernel.data.ColumnVector} for short type.\n   *\n   * @param size number of elements in the vector.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param values column vector values.\n   */\n  public DefaultShortVector(int size, Optional<boolean[]> nullability, short[] values) {\n    super(size, ShortType.SHORT, nullability);\n    this.values = requireNonNull(values, \"values is null\");\n    checkArgument(\n        values.length >= size,\n        \"invalid number of values (%s) for given size (%s)\",\n        values.length,\n        size);\n  }\n\n  /**\n   * Get the value at given {@code rowId}. The return value is undefined and can be anything, if the\n   * slot for {@code rowId} is null.\n   *\n   * @param rowId\n   * @return\n   */\n  @Override\n  public short getShort(int rowId) {\n    checkValidRowId(rowId);\n    return values[rowId];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultStructVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\n\n/** {@link io.delta.kernel.data.ColumnVector} implementation for struct type data. */\npublic class DefaultStructVector extends AbstractColumnVector {\n  private final ColumnVector[] memberVectors;\n\n  /**\n   * Create an instance of {@link ColumnVector} for {@code struct} type.\n   *\n   * @param size number of elements in the vector.\n   * @param dataType {@code struct} datatype definition.\n   * @param nullability Optional array of nullability value for each element in the vector. All\n   *     values in the vector are considered non-null when parameter is empty.\n   * @param memberVectors column vectors for each member of the struct.\n   */\n  public DefaultStructVector(\n      int size, DataType dataType, Optional<boolean[]> nullability, ColumnVector[] memberVectors) {\n    super(size, dataType, nullability);\n    checkArgument(dataType instanceof StructType, \"not a struct type\");\n    StructType structType = (StructType) dataType;\n    checkArgument(\n        structType.length() == memberVectors.length,\n        \"expected a one column vector for each member\");\n    this.memberVectors = memberVectors;\n  }\n\n  @Override\n  public ColumnVector getChild(int ordinal) {\n    checkArgument(ordinal >= 0 && ordinal < memberVectors.length, \"Invalid ordinal %s\", ordinal);\n    return memberVectors[ordinal];\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultSubFieldVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport java.math.BigDecimal;\nimport java.util.function.Function;\n\n/**\n * {@link ColumnVector} wrapper on top of {@link Row} objects. This wrapper allows referencing any\n * nested level column vector from a set of rows.\n */\npublic class DefaultSubFieldVector implements ColumnVector {\n  private final int size;\n  private final DataType dataType;\n  private final int columnOrdinal;\n  private final Function<Integer, Row> rowIdToRowAccessor;\n\n  /**\n   * Create an instance of {@link DefaultSubFieldVector}\n   *\n   * @param size Number of elements in the vector\n   * @param dataType Datatype of the vector\n   * @param columnOrdinal Ordinal of the column represented by this vector in the rows returned by\n   *     {@link #rowIdToRowAccessor}\n   * @param rowIdToRowAccessor {@link Function} that returns a {@link Row} object for given rowId\n   */\n  public DefaultSubFieldVector(\n      int size, DataType dataType, int columnOrdinal, Function<Integer, Row> rowIdToRowAccessor) {\n    checkArgument(size >= 0, \"invalid size: %s\", size);\n    this.size = size;\n    checkArgument(columnOrdinal >= 0, \"invalid column ordinal: %s\", columnOrdinal);\n    this.columnOrdinal = columnOrdinal;\n    this.rowIdToRowAccessor = requireNonNull(rowIdToRowAccessor, \"rowIdToRowAccessor is null\");\n    this.dataType = requireNonNull(dataType, \"dataType is null\");\n  }\n\n  @Override\n  public DataType getDataType() {\n    return dataType;\n  }\n\n  @Override\n  public int getSize() {\n    return size;\n  }\n\n  @Override\n  public void close() {\n    /* nothing to close */\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    assertValidRowId(rowId);\n    Row row = rowIdToRowAccessor.apply(rowId);\n    return row == null || row.isNullAt(columnOrdinal);\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getBoolean(columnOrdinal);\n  }\n\n  @Override\n  public byte getByte(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getByte(columnOrdinal);\n  }\n\n  @Override\n  public short getShort(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getShort(columnOrdinal);\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getInt(columnOrdinal);\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getLong(columnOrdinal);\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getFloat(columnOrdinal);\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getDouble(columnOrdinal);\n  }\n\n  @Override\n  public byte[] getBinary(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getBinary(columnOrdinal);\n  }\n\n  @Override\n  public String getString(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getString(columnOrdinal);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getDecimal(columnOrdinal);\n  }\n\n  @Override\n  public MapValue getMap(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getMap(columnOrdinal);\n  }\n\n  @Override\n  public ArrayValue getArray(int rowId) {\n    assertValidRowId(rowId);\n    return rowIdToRowAccessor.apply(rowId).getArray(columnOrdinal);\n  }\n\n  @Override\n  public ColumnVector getChild(int childOrdinal) {\n    StructType structType = (StructType) dataType;\n    StructField childField = structType.at(childOrdinal);\n    return new DefaultSubFieldVector(\n        size,\n        childField.getDataType(),\n        childOrdinal,\n        (rowId) -> {\n          if (isNullAt(rowId)) {\n            return null;\n          } else {\n            return rowIdToRowAccessor.apply(rowId).getStruct(columnOrdinal);\n          }\n        });\n  }\n\n  private void assertValidRowId(int rowId) {\n    checkArgument(rowId < size, \"Invalid rowId: %s, max allowed rowId is: %s\", rowId, (size - 1));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/data/vector/DefaultViewVector.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.data.vector;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.types.DataType;\nimport java.math.BigDecimal;\n\n/** Provides a restricted view on an underlying column vector. */\npublic class DefaultViewVector implements ColumnVector {\n\n  private final ColumnVector underlyingVector;\n  private final int offset;\n  private final int size;\n\n  /**\n   * @param underlyingVector the underlying column vector to read\n   * @param start the row index of the underlyingVector where we want this vector to start\n   * @param end the row index of the underlyingVector where we want this vector to end (exclusive)\n   */\n  public DefaultViewVector(ColumnVector underlyingVector, int start, int end) {\n    this.underlyingVector = underlyingVector;\n    this.offset = start;\n    this.size = end - start;\n  }\n\n  @Override\n  public DataType getDataType() {\n    return underlyingVector.getDataType();\n  }\n\n  @Override\n  public int getSize() {\n    return size;\n  }\n\n  @Override\n  public void close() {\n    // Don't close the underlying vector as it may still be used\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.isNullAt(offset + rowId);\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getBoolean(offset + rowId);\n  }\n\n  @Override\n  public byte getByte(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getByte(offset + rowId);\n  }\n\n  @Override\n  public short getShort(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getShort(offset + rowId);\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getInt(offset + rowId);\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getLong(offset + rowId);\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getFloat(offset + rowId);\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getDouble(offset + rowId);\n  }\n\n  @Override\n  public byte[] getBinary(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getBinary(offset + rowId);\n  }\n\n  @Override\n  public String getString(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getString(offset + rowId);\n  }\n\n  @Override\n  public BigDecimal getDecimal(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getDecimal(offset + rowId);\n  }\n\n  @Override\n  public MapValue getMap(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getMap(offset + rowId);\n  }\n\n  @Override\n  public ArrayValue getArray(int rowId) {\n    checkValidRowId(rowId);\n    return underlyingVector.getArray(offset + rowId);\n  }\n\n  @Override\n  public ColumnVector getChild(int ordinal) {\n    return new DefaultViewVector(underlyingVector.getChild(ordinal), offset, offset + size);\n  }\n\n  private void checkValidRowId(int rowId) {\n    checkArgument(rowId >= 0 && rowId < size, \"Invalid rowId=%s for size=%s\", rowId, size);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException;\nimport static io.delta.kernel.defaults.internal.expressions.DefaultExpressionUtils.*;\nimport static io.delta.kernel.defaults.internal.expressions.ImplicitCastExpression.canCastTo;\nimport static io.delta.kernel.internal.util.ExpressionUtils.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\nimport static java.util.Objects.requireNonNull;\nimport static java.util.stream.Collectors.toList;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultBooleanVector;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultConstantVector;\nimport io.delta.kernel.engine.ExpressionHandler;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n * Implementation of {@link ExpressionEvaluator} for default {@link ExpressionHandler}. It takes\n * care of validating, adding necessary implicit casts and evaluating the {@link Expression} on\n * given {@link ColumnarBatch}.\n */\npublic class DefaultExpressionEvaluator implements ExpressionEvaluator {\n  private final Expression expression;\n\n  /**\n   * Create a {@link DefaultExpressionEvaluator} instance bound to the given expression and\n   * <i>inputSchem</i>.\n   *\n   * @param inputSchema Input data schema\n   * @param expression Expression to evaluate.\n   * @param outputType Expected result data type.\n   */\n  public DefaultExpressionEvaluator(\n      StructType inputSchema, Expression expression, DataType outputType) {\n    ExpressionTransformResult transformResult =\n        new ExpressionTransformer(inputSchema).visit(expression);\n    if (!transformResult.outputType.equivalent(outputType)) {\n      String reason =\n          String.format(\n              \"Expression %s does not match expected output type %s\", expression, outputType);\n      throw unsupportedExpressionException(expression, reason);\n    }\n    this.expression = transformResult.expression;\n  }\n\n  @Override\n  public ColumnVector eval(ColumnarBatch input) {\n    return new ExpressionEvalVisitor(input).visit(expression);\n  }\n\n  @Override\n  public void close() {\n    /* nothing to close */\n  }\n\n  /** Encapsulates the result of {@link ExpressionTransformer} */\n  private static class ExpressionTransformResult {\n    public final Expression expression; // transformed expression\n    public final DataType outputType; // output type of the expression\n\n    ExpressionTransformResult(Expression expression, DataType outputType) {\n      this.expression = expression;\n      this.outputType = outputType;\n    }\n  }\n\n  /**\n   * Implementation of {@link ExpressionVisitor} to validate the given expression as follows.\n   *\n   * <ul>\n   *   <li>given input column is part of the input data schema\n   *   <li>expression inputs are of supported types. Insert cast according to the rules in {@link\n   *       ImplicitCastExpression} to make the types compatible for evaluation by {@link\n   *       ExpressionEvalVisitor}\n   * </ul>\n   *\n   * <p>Return type of each expression visit is a tuple of new rewritten expression and its result\n   * data type.\n   */\n  private static class ExpressionTransformer extends ExpressionVisitor<ExpressionTransformResult> {\n    private StructType inputDataSchema;\n\n    ExpressionTransformer(StructType inputDataSchema) {\n      this.inputDataSchema = requireNonNull(inputDataSchema, \"inputDataSchema is null\");\n    }\n\n    @Override\n    ExpressionTransformResult visitAnd(And and) {\n      Predicate left = validateIsPredicate(and, visit(and.getLeft()));\n      Predicate right = validateIsPredicate(and, visit(and.getRight()));\n      return new ExpressionTransformResult(new And(left, right), BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitOr(Or or) {\n      Predicate left = validateIsPredicate(or, visit(or.getLeft()));\n      Predicate right = validateIsPredicate(or, visit(or.getRight()));\n      return new ExpressionTransformResult(new Or(left, right), BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitAlwaysTrue(AlwaysTrue alwaysTrue) {\n      // nothing to validate or rewrite.\n      return new ExpressionTransformResult(alwaysTrue, BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitAlwaysFalse(AlwaysFalse alwaysFalse) {\n      // nothing to validate or rewrite.\n      return new ExpressionTransformResult(alwaysFalse, BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitComparator(Predicate predicate) {\n      switch (predicate.getName()) {\n        case \"=\":\n        case \">\":\n        case \">=\":\n        case \"<\":\n        case \"<=\":\n        case \"IS NOT DISTINCT FROM\":\n          return new ExpressionTransformResult(\n              transformBinaryComparator(predicate), BooleanType.BOOLEAN);\n        default:\n          // We should never reach this based on the ExpressionVisitor\n          throw new IllegalStateException(\n              String.format(\"%s is not a recognized comparator\", predicate.getName()));\n      }\n    }\n\n    @Override\n    ExpressionTransformResult visitLiteral(Literal literal) {\n      // nothing to validate or rewrite\n      return new ExpressionTransformResult(literal, literal.getDataType());\n    }\n\n    @Override\n    ExpressionTransformResult visitColumn(Column column) {\n      String[] names = column.getNames();\n      DataType currentType = inputDataSchema;\n      for (int level = 0; level < names.length; level++) {\n        assertColumnExists(currentType instanceof StructType, inputDataSchema, column);\n        StructType structSchema = ((StructType) currentType);\n        int ordinal = structSchema.indexOf(names[level]);\n        assertColumnExists(ordinal != -1, inputDataSchema, column);\n        currentType = structSchema.at(ordinal).getDataType();\n      }\n      assertColumnExists(currentType != null, inputDataSchema, column);\n      return new ExpressionTransformResult(column, currentType);\n    }\n\n    @Override\n    ExpressionTransformResult visitCast(ImplicitCastExpression cast) {\n      throw new UnsupportedOperationException(\"CAST expression is not expected.\");\n    }\n\n    @Override\n    ExpressionTransformResult visitPartitionValue(PartitionValueExpression partitionValue) {\n      ExpressionTransformResult serializedPartValueInput = visit(partitionValue.getInput());\n      checkArgument(\n          serializedPartValueInput.outputType instanceof StringType,\n          \"%s: expected string input, but got %s\",\n          partitionValue,\n          serializedPartValueInput.outputType);\n      DataType partitionColType = partitionValue.getDataType();\n      if (partitionColType instanceof StructType\n          || partitionColType instanceof ArrayType\n          || partitionColType instanceof MapType) {\n        throw unsupportedExpressionException(\n            partitionValue, \"unsupported partition data type: \" + partitionColType);\n      }\n      return new ExpressionTransformResult(\n          new PartitionValueExpression(serializedPartValueInput.expression, partitionColType),\n          partitionColType);\n    }\n\n    @Override\n    ExpressionTransformResult visitElementAt(ScalarExpression elementAt) {\n      ExpressionTransformResult transformedMapInput = visit(childAt(elementAt, 0));\n      ExpressionTransformResult transformedLookupKey = visit(childAt(elementAt, 1));\n\n      ScalarExpression transformedExpression =\n          ElementAtEvaluator.validateAndTransform(\n              elementAt,\n              transformedMapInput.expression,\n              transformedMapInput.outputType,\n              transformedLookupKey.expression,\n              transformedLookupKey.outputType);\n\n      return new ExpressionTransformResult(\n          transformedExpression, ((MapType) transformedMapInput.outputType).getValueType());\n    }\n\n    @Override\n    ExpressionTransformResult visitNot(Predicate predicate) {\n      Predicate child = validateIsPredicate(predicate, visit(predicate.getChildren().get(0)));\n      return new ExpressionTransformResult(\n          new Predicate(predicate.getName(), child), BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitIsNotNull(Predicate predicate) {\n      Expression child = visit(predicate.getChildren().get(0)).expression;\n      return new ExpressionTransformResult(\n          new Predicate(predicate.getName(), child), BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitIsNull(Predicate predicate) {\n      Expression child = visit(getUnaryChild(predicate)).expression;\n      return new ExpressionTransformResult(\n          new Predicate(predicate.getName(), child), BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitCoalesce(ScalarExpression coalesce) {\n      List<ExpressionTransformResult> children =\n          coalesce.getChildren().stream().map(this::visit).collect(Collectors.toList());\n      if (children.isEmpty()) {\n        throw unsupportedExpressionException(coalesce, \"Coalesce requires at least one expression\");\n      }\n      // TODO support least-common-type resolution\n      long numDistinctTypes = children.stream().map(e -> e.outputType).distinct().count();\n      if (numDistinctTypes > 1) {\n        throw unsupportedExpressionException(\n            coalesce, \"Coalesce is only supported for arguments of the same type\");\n      }\n      return new ExpressionTransformResult(\n          new ScalarExpression(\n              \"COALESCE\", children.stream().map(e -> e.expression).collect(Collectors.toList())),\n          children.get(0).outputType);\n    }\n\n    @Override\n    ExpressionTransformResult visitAdd(ScalarExpression add) {\n      List<ExpressionTransformResult> children =\n          add.getChildren().stream().map(this::visit).collect(Collectors.toList());\n      if (children.size() != 2) {\n        throw unsupportedExpressionException(\n            add, \"ADD requires exactly two arguments: left and right operands\");\n      }\n      if (!children.get(0).outputType.equivalent(children.get(1).outputType)) {\n        throw unsupportedExpressionException(\n            add, \"ADD is only supported for arguments of the same type\");\n      }\n      if (!(children.get(0).outputType instanceof ByteType\n          || children.get(0).outputType instanceof ShortType\n          || children.get(0).outputType instanceof IntegerType\n          || children.get(0).outputType instanceof LongType\n          || children.get(0).outputType instanceof FloatType\n          || children.get(0).outputType instanceof DoubleType)) {\n        throw unsupportedExpressionException(\n            add, \"ADD is only supported for numeric types: byte, short, int, long, float, double\");\n      }\n\n      return new ExpressionTransformResult(\n          new ScalarExpression(\n              \"ADD\", Arrays.asList(children.get(0).expression, children.get(1).expression)),\n          children.get(0).outputType);\n    }\n\n    @Override\n    ExpressionTransformResult visitTimeAdd(ScalarExpression timeAdd) {\n      List<ExpressionTransformResult> children =\n          timeAdd.getChildren().stream().map(this::visit).collect(Collectors.toList());\n\n      if (children.size() != 2) {\n        throw unsupportedExpressionException(\n            timeAdd, \"TIMEADD requires exactly two arguments: timestamp column and milliseconds\");\n      }\n\n      Expression timestampColumn = children.get(0).expression;\n      Expression durationMilliseconds = children.get(1).expression;\n      DataType timestampColumnType = children.get(0).outputType;\n      DataType literalColumnType = children.get(1).outputType;\n\n      // Ensure the first child is either a TimestampType or a TimestampNTZType,\n      // and the second is a LongType.\n      if (!((timestampColumnType instanceof TimestampType\n              || timestampColumnType instanceof TimestampNTZType)\n          && (literalColumnType instanceof LongType))) {\n        throw new IllegalArgumentException(\n            \"TIMEADD requires a timestamp and a Long (milliseconds) to add to it\");\n      }\n\n      return new ExpressionTransformResult(\n          new ScalarExpression(\"TIMEADD\", Arrays.asList(timestampColumn, durationMilliseconds)),\n          timestampColumnType // Result is also a timestamp\n          );\n    }\n\n    @Override\n    ExpressionTransformResult visitSubstring(ScalarExpression substring) {\n      List<ExpressionTransformResult> children =\n          substring.getChildren().stream().map(this::visit).collect(toList());\n      ScalarExpression transformedExpression =\n          SubstringEvaluator.validateAndTransform(\n              substring,\n              children.stream().map(e -> e.expression).collect(toList()),\n              children.stream().map(e -> e.outputType).collect(toList()));\n      return new ExpressionTransformResult(transformedExpression, StringType.STRING);\n    }\n\n    @Override\n    ExpressionTransformResult visitLike(final Predicate like) {\n      List<ExpressionTransformResult> children =\n          like.getChildren().stream().map(this::visit).collect(toList());\n      Predicate transformedExpression =\n          LikeExpressionEvaluator.validateAndTransform(\n              like,\n              children.stream().map(e -> e.expression).collect(toList()),\n              children.stream().map(e -> e.outputType).collect(toList()));\n\n      return new ExpressionTransformResult(transformedExpression, BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitStartsWith(Predicate startsWith) {\n      List<ExpressionTransformResult> children =\n          startsWith.getChildren().stream().map(this::visit).collect(toList());\n      Predicate transformedExpression =\n          StartsWithExpressionEvaluator.validateAndTransform(\n              startsWith,\n              children.stream().map(e -> e.expression).collect(toList()),\n              children.stream().map(e -> e.outputType).collect(toList()));\n      return new ExpressionTransformResult(transformedExpression, BooleanType.BOOLEAN);\n    }\n\n    @Override\n    ExpressionTransformResult visitIn(In in) {\n      ExpressionTransformResult visitedValue = visit(in.getValueExpression());\n      List<ExpressionTransformResult> visitedInList =\n          in.getInListElements().stream().map(this::visit).collect(toList());\n      In transformedExpression =\n          InExpressionEvaluator.validateAndTransform(\n              in,\n              visitedValue.expression,\n              visitedValue.outputType,\n              visitedInList.stream().map(e -> e.expression).collect(toList()),\n              visitedInList.stream().map(e -> e.outputType).collect(toList()));\n      return new ExpressionTransformResult(transformedExpression, BooleanType.BOOLEAN);\n    }\n\n    private Predicate validateIsPredicate(\n        Expression baseExpression, ExpressionTransformResult result) {\n      checkArgument(\n          result.outputType instanceof BooleanType && result.expression instanceof Predicate,\n          \"%s: expected a predicate expression but got %s with output type %s.\",\n          baseExpression,\n          result.expression,\n          result.outputType);\n      return (Predicate) result.expression;\n    }\n\n    private Expression transformBinaryComparator(Predicate predicate) {\n      ExpressionTransformResult leftResult = visit(getLeft(predicate));\n      ExpressionTransformResult rightResult = visit(getRight(predicate));\n      Expression left = leftResult.expression;\n      Expression right = rightResult.expression;\n\n      if (predicate.getCollationIdentifier().isPresent()) {\n        CollationIdentifier collationIdentifier = predicate.getCollationIdentifier().get();\n        checkIsUTF8BinaryCollation(predicate, collationIdentifier);\n\n        for (DataType dataType : Arrays.asList(leftResult.outputType, rightResult.outputType)) {\n          checkIsStringType(\n              dataType,\n              predicate,\n              format(\"Predicate %s expects STRING type inputs\", predicate.getName()));\n        }\n        return new Predicate(predicate.getName(), left, right, collationIdentifier);\n      }\n\n      if (!leftResult.outputType.equivalent(rightResult.outputType)) {\n        if (canCastTo(leftResult.outputType, rightResult.outputType)) {\n          left = new ImplicitCastExpression(left, rightResult.outputType);\n        } else if (canCastTo(rightResult.outputType, leftResult.outputType)) {\n          right = new ImplicitCastExpression(right, leftResult.outputType);\n        } else {\n          String msg =\n              format(\n                  \"operands are of different types which are not \"\n                      + \"comparable: left type=%s, right type=%s\",\n                  leftResult.outputType, rightResult.outputType);\n          throw unsupportedExpressionException(predicate, msg);\n        }\n      }\n      return new Predicate(predicate.getName(), left, right);\n    }\n  }\n\n  /**\n   * Implementation of {@link ExpressionVisitor} to evaluate expression on a {@link ColumnarBatch}.\n   */\n  private static class ExpressionEvalVisitor extends ExpressionVisitor<ColumnVector> {\n    private final ColumnarBatch input;\n\n    ExpressionEvalVisitor(ColumnarBatch input) {\n      this.input = input;\n    }\n\n    /*\n    | Operand 1 | Operand 2 | `AND`      | `OR`       |\n    |-----------|-----------|------------|------------|\n    | True      | True      | True       | True       |\n    | True      | False     | False      | True       |\n    | True      | NULL      | NULL       | True       |\n    | False     | True      | False      | True       |\n    | False     | False     | False      | False      |\n    | False     | NULL      | False      | NULL       |\n    | NULL      | True      | NULL       | True       |\n    | NULL      | False     | False      | NULL       |\n    | NULL      | NULL      | NULL       | NULL       |\n     */\n    @Override\n    ColumnVector visitAnd(And and) {\n      PredicateChildrenEvalResult argResults = evalBinaryExpressionChildren(and);\n      ColumnVector left = argResults.leftResult;\n      ColumnVector right = argResults.rightResult;\n      int numRows = argResults.rowCount;\n      boolean[] result = new boolean[numRows];\n      boolean[] nullability = new boolean[numRows];\n      for (int rowId = 0; rowId < numRows; rowId++) {\n        boolean leftIsTrue = !left.isNullAt(rowId) && left.getBoolean(rowId);\n        boolean rightIsTrue = !right.isNullAt(rowId) && right.getBoolean(rowId);\n        boolean leftIsFalse = !left.isNullAt(rowId) && !left.getBoolean(rowId);\n        boolean rightIsFalse = !right.isNullAt(rowId) && !right.getBoolean(rowId);\n\n        if (leftIsFalse || rightIsFalse) {\n          nullability[rowId] = false;\n          result[rowId] = false;\n        } else if (leftIsTrue && rightIsTrue) {\n          nullability[rowId] = false;\n          result[rowId] = true;\n        } else {\n          nullability[rowId] = true;\n          // result[rowId] is undefined when nullability[rowId] = true\n        }\n      }\n      return new DefaultBooleanVector(numRows, Optional.of(nullability), result);\n    }\n\n    @Override\n    ColumnVector visitOr(Or or) {\n      PredicateChildrenEvalResult argResults = evalBinaryExpressionChildren(or);\n      ColumnVector left = argResults.leftResult;\n      ColumnVector right = argResults.rightResult;\n      int numRows = argResults.rowCount;\n      boolean[] result = new boolean[numRows];\n      boolean[] nullability = new boolean[numRows];\n      for (int rowId = 0; rowId < numRows; rowId++) {\n        boolean leftIsTrue = !left.isNullAt(rowId) && left.getBoolean(rowId);\n        boolean rightIsTrue = !right.isNullAt(rowId) && right.getBoolean(rowId);\n        boolean leftIsFalse = !left.isNullAt(rowId) && !left.getBoolean(rowId);\n        boolean rightIsFalse = !right.isNullAt(rowId) && !right.getBoolean(rowId);\n\n        if (leftIsTrue || rightIsTrue) {\n          nullability[rowId] = false;\n          result[rowId] = true;\n        } else if (leftIsFalse && rightIsFalse) {\n          nullability[rowId] = false;\n          result[rowId] = false;\n        } else {\n          nullability[rowId] = true;\n          // result[rowId] is undefined when nullability[rowId] = true\n        }\n      }\n      return new DefaultBooleanVector(numRows, Optional.of(nullability), result);\n    }\n\n    @Override\n    ColumnVector visitAlwaysTrue(AlwaysTrue alwaysTrue) {\n      return new DefaultConstantVector(BooleanType.BOOLEAN, input.getSize(), true);\n    }\n\n    @Override\n    ColumnVector visitAlwaysFalse(AlwaysFalse alwaysFalse) {\n      return new DefaultConstantVector(BooleanType.BOOLEAN, input.getSize(), false);\n    }\n\n    @Override\n    ColumnVector visitComparator(Predicate predicate) {\n      PredicateChildrenEvalResult argResults = evalBinaryExpressionChildren(predicate);\n      switch (predicate.getName()) {\n        case \"=\":\n          return comparatorVector(\n              argResults.leftResult,\n              argResults.rightResult,\n              (compareResult) -> (compareResult == 0));\n        case \">\":\n          return comparatorVector(\n              argResults.leftResult,\n              argResults.rightResult,\n              (compareResult) -> (compareResult > 0));\n        case \">=\":\n          return comparatorVector(\n              argResults.leftResult,\n              argResults.rightResult,\n              (compareResult) -> (compareResult >= 0));\n        case \"<\":\n          return comparatorVector(\n              argResults.leftResult,\n              argResults.rightResult,\n              (compareResult) -> (compareResult < 0));\n        case \"<=\":\n          return comparatorVector(\n              argResults.leftResult,\n              argResults.rightResult,\n              (compareResult) -> (compareResult <= 0));\n        case \"IS NOT DISTINCT FROM\":\n          return nullSafeComparatorVector(\n              argResults.leftResult,\n              argResults.rightResult,\n              (compareResult) -> (compareResult == 0));\n        default:\n          // We should never reach this based on the ExpressionVisitor\n          throw new IllegalStateException(\n              String.format(\"%s is not a recognized comparator\", predicate.getName()));\n      }\n    }\n\n    @Override\n    ColumnVector visitLiteral(Literal literal) {\n      DataType dataType = literal.getDataType();\n      if (dataType instanceof BooleanType\n          || dataType instanceof ByteType\n          || dataType instanceof ShortType\n          || dataType instanceof IntegerType\n          || dataType instanceof LongType\n          || dataType instanceof FloatType\n          || dataType instanceof DoubleType\n          || dataType instanceof StringType\n          || dataType instanceof BinaryType\n          || dataType instanceof DecimalType\n          || dataType instanceof DateType\n          || dataType instanceof TimestampType\n          || dataType instanceof TimestampNTZType) {\n        return new DefaultConstantVector(dataType, input.getSize(), literal.getValue());\n      }\n\n      throw new UnsupportedOperationException(\"unsupported expression encountered: \" + literal);\n    }\n\n    @Override\n    ColumnVector visitColumn(Column column) {\n      String[] names = column.getNames();\n      DataType currentType = input.getSchema();\n      ColumnVector columnVector = null;\n      for (int level = 0; level < names.length; level++) {\n        assertColumnExists(currentType instanceof StructType, input.getSchema(), column);\n        StructType structSchema = ((StructType) currentType);\n        int ordinal = structSchema.indexOf(names[level]);\n        assertColumnExists(ordinal != -1, input.getSchema(), column);\n        currentType = structSchema.at(ordinal).getDataType();\n\n        if (level == 0) {\n          columnVector = input.getColumnVector(ordinal);\n        } else {\n          columnVector = columnVector.getChild(ordinal);\n        }\n      }\n      assertColumnExists(columnVector != null, input.getSchema(), column);\n      return columnVector;\n    }\n\n    @Override\n    ColumnVector visitCast(ImplicitCastExpression cast) {\n      ColumnVector inputResult = visit(cast.getInput());\n      return cast.eval(inputResult);\n    }\n\n    @Override\n    ColumnVector visitPartitionValue(PartitionValueExpression partitionValue) {\n      ColumnVector input = visit(partitionValue.getInput());\n      return PartitionValueEvaluator.eval(input, partitionValue.getDataType());\n    }\n\n    @Override\n    ColumnVector visitElementAt(ScalarExpression elementAt) {\n      ColumnVector map = visit(childAt(elementAt, 0));\n      ColumnVector lookupKey = visit(childAt(elementAt, 1));\n      return ElementAtEvaluator.eval(map, lookupKey);\n    }\n\n    @Override\n    ColumnVector visitNot(Predicate predicate) {\n      ColumnVector childResult = visit(childAt(predicate, 0));\n      return booleanWrapperVector(\n          childResult,\n          rowId -> !childResult.getBoolean(rowId),\n          rowId -> childResult.isNullAt(rowId));\n    }\n\n    @Override\n    ColumnVector visitIsNotNull(Predicate predicate) {\n      ColumnVector childResult = visit(childAt(predicate, 0));\n      return booleanWrapperVector(\n          childResult, rowId -> !childResult.isNullAt(rowId), rowId -> false);\n    }\n\n    @Override\n    ColumnVector visitIsNull(Predicate predicate) {\n      ColumnVector childResult = visit(getUnaryChild(predicate));\n      return booleanWrapperVector(\n          childResult, rowId -> childResult.isNullAt(rowId), rowId -> false);\n    }\n\n    @Override\n    ColumnVector visitCoalesce(ScalarExpression coalesce) {\n      List<ColumnVector> childResults =\n          coalesce.getChildren().stream().map(this::visit).collect(Collectors.toList());\n      return DefaultExpressionUtils.combinationVector(\n          childResults,\n          rowId -> {\n            for (int idx = 0; idx < childResults.size(); idx++) {\n              if (!childResults.get(idx).isNullAt(rowId)) {\n                return idx;\n              }\n            }\n            return 0; // If all are null then any idx suffices\n          });\n    }\n\n    @Override\n    ColumnVector visitAdd(ScalarExpression add) {\n      List<ColumnVector> childResults =\n          add.getChildren().stream().map(this::visit).collect(toList());\n\n      // NOTE: The current implementation only supports operands of the same type, and it does not\n      // check for overflows (i.e., values will wrap around when overflowing).\n      return DefaultExpressionUtils.arithmeticVector(\n          childResults.get(0),\n          childResults.get(1),\n          new ArithmeticOperator() {\n            @Override\n            public byte apply(byte a, byte b) {\n              return (byte) (a + b);\n            }\n\n            @Override\n            public short apply(short a, short b) {\n              return (short) (a + b);\n            }\n\n            @Override\n            public int apply(int a, int b) {\n              return a + b;\n            }\n\n            @Override\n            public long apply(long a, long b) {\n              return a + b;\n            }\n\n            @Override\n            public float apply(float a, float b) {\n              return a + b;\n            }\n\n            @Override\n            public double apply(double a, double b) {\n              return a + b;\n            }\n          });\n    }\n\n    @Override\n    ColumnVector visitTimeAdd(ScalarExpression timeAdd) {\n      ColumnVector timestampColumn = visit(timeAdd.getChildren().get(0));\n      ColumnVector durationVector = visit(timeAdd.getChildren().get(1));\n\n      return new ColumnVector() {\n        @Override\n        public DataType getDataType() {\n          return timestampColumn.getDataType();\n        }\n\n        @Override\n        public int getSize() {\n          return timestampColumn.getSize();\n        }\n\n        @Override\n        public void close() {\n          timestampColumn.close();\n          durationVector.close();\n        }\n\n        @Override\n        public boolean isNullAt(int rowId) {\n          return timestampColumn.isNullAt(rowId) || durationVector.isNullAt(rowId);\n        }\n\n        @Override\n        public long getLong(int rowId) {\n          if (isNullAt(rowId)) {\n            return 0;\n          }\n          long durationMicros = durationVector.getLong(rowId) * 1000L;\n          return timestampColumn.getLong(rowId) + durationMicros;\n        }\n      };\n    }\n\n    @Override\n    ColumnVector visitSubstring(ScalarExpression subString) {\n      return SubstringEvaluator.eval(\n          subString.getChildren().stream().map(this::visit).collect(toList()));\n    }\n\n    @Override\n    ColumnVector visitLike(final Predicate like) {\n      List<Expression> children = like.getChildren();\n      return LikeExpressionEvaluator.eval(\n          children, children.stream().map(this::visit).collect(toList()));\n    }\n\n    @Override\n    ColumnVector visitStartsWith(Predicate startsWith) {\n      return StartsWithExpressionEvaluator.eval(\n          startsWith.getChildren().stream().map(this::visit).collect(toList()));\n    }\n\n    @Override\n    ColumnVector visitIn(In in) {\n      return InExpressionEvaluator.eval(\n          in.getChildren().stream().map(this::visit).collect(toList()));\n    }\n\n    /**\n     * Utility method to evaluate inputs to the binary input expression. Also validates the\n     * evaluated expression result {@link ColumnVector}s are of the same size.\n     *\n     * @param predicate\n     * @return Triplet of (result vector size, left operand result, left operand result)\n     */\n    private PredicateChildrenEvalResult evalBinaryExpressionChildren(Predicate predicate) {\n      ColumnVector left = visit(getLeft(predicate));\n      ColumnVector right = visit(getRight(predicate));\n      checkArgument(\n          left.getSize() == right.getSize(),\n          \"Left and right operand returned different results: left=%d, right=d\",\n          left.getSize(),\n          right.getSize());\n      return new PredicateChildrenEvalResult(left.getSize(), left, right);\n    }\n  }\n\n  /** Encapsulates children expression result of binary input predicate */\n  private static class PredicateChildrenEvalResult {\n    public final int rowCount;\n    public final ColumnVector leftResult;\n    public final ColumnVector rightResult;\n\n    PredicateChildrenEvalResult(int rowCount, ColumnVector leftResult, ColumnVector rightResult) {\n      this.rowCount = rowCount;\n      this.leftResult = leftResult;\n      this.rightResult = rightResult;\n    }\n  }\n\n  private static void assertColumnExists(boolean condition, StructType schema, Column column) {\n    if (!condition) {\n      throw new IllegalArgumentException(\n          format(\"%s doesn't exist in input data schema: %s\", column, schema));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultExpressionUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.function.IntPredicate;\nimport java.util.stream.Collectors;\n\n/** Utility methods used by the default expression evaluator. */\nclass DefaultExpressionUtils {\n\n  static final Comparator<BigDecimal> BIGDECIMAL_COMPARATOR = Comparator.naturalOrder();\n  static final Comparator<byte[]> BINARY_COMPARTOR =\n      (leftOp, rightOp) -> {\n        int i = 0;\n        while (i < leftOp.length && i < rightOp.length) {\n          if (leftOp[i] != rightOp[i]) {\n            return Byte.toUnsignedInt(leftOp[i]) - Byte.toUnsignedInt(rightOp[i]);\n          }\n          i++;\n        }\n        return Integer.compare(leftOp.length, rightOp.length);\n      };\n  static final Comparator<String> STRING_COMPARATOR =\n      (leftOp, rightOp) -> {\n        byte[] leftBytes = leftOp.getBytes(StandardCharsets.UTF_8);\n        byte[] rightBytes = rightOp.getBytes(StandardCharsets.UTF_8);\n        return BINARY_COMPARTOR.compare(leftBytes, rightBytes);\n      };\n\n  private DefaultExpressionUtils() {}\n\n  /**\n   * Utility method that calculates the nullability result from given two vectors. Result is null if\n   * at least one side is a null.\n   */\n  static boolean[] evalNullability(ColumnVector left, ColumnVector right) {\n    int numRows = left.getSize();\n    boolean[] nullability = new boolean[numRows];\n    for (int rowId = 0; rowId < numRows; rowId++) {\n      nullability[rowId] = left.isNullAt(rowId) || right.isNullAt(rowId);\n    }\n    return nullability;\n  }\n\n  /**\n   * Wraps a child vector as a boolean {@link ColumnVector} with the given value and nullability\n   * accessors.\n   */\n  static ColumnVector booleanWrapperVector(\n      ColumnVector childVector,\n      Function<Integer, Boolean> valueAccessor,\n      Function<Integer, Boolean> nullabilityAccessor) {\n\n    return new ColumnVector() {\n\n      @Override\n      public DataType getDataType() {\n        return BooleanType.BOOLEAN;\n      }\n\n      @Override\n      public int getSize() {\n        return childVector.getSize();\n      }\n\n      @Override\n      public void close() {\n        childVector.close();\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return nullabilityAccessor.apply(rowId);\n      }\n\n      @Override\n      public boolean getBoolean(int rowId) {\n        return valueAccessor.apply(rowId);\n      }\n    };\n  }\n\n  /**\n   * Utility method for getting value comparator\n   *\n   * @param left\n   * @param right\n   * @param booleanComparator\n   * @return\n   */\n  static IntPredicate getComparator(\n      ColumnVector left, ColumnVector right, IntPredicate booleanComparator) {\n    checkArgument(\n        left.getSize() == right.getSize(), \"Left and right operand have different vector sizes.\");\n\n    DataType dataType = left.getDataType();\n    IntPredicate vectorValueComparator;\n    if (dataType instanceof BooleanType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(\n                  Boolean.compare(left.getBoolean(rowId), right.getBoolean(rowId)));\n    } else if (dataType instanceof ByteType) {\n      vectorValueComparator =\n          rowId -> booleanComparator.test(Byte.compare(left.getByte(rowId), right.getByte(rowId)));\n    } else if (dataType instanceof ShortType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(Short.compare(left.getShort(rowId), right.getShort(rowId)));\n    } else if (dataType instanceof IntegerType || dataType instanceof DateType) {\n      vectorValueComparator =\n          rowId -> booleanComparator.test(Integer.compare(left.getInt(rowId), right.getInt(rowId)));\n    } else if (dataType instanceof LongType\n        || dataType instanceof TimestampType\n        || dataType instanceof TimestampNTZType) {\n      vectorValueComparator =\n          rowId -> booleanComparator.test(Long.compare(left.getLong(rowId), right.getLong(rowId)));\n    } else if (dataType instanceof FloatType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(Float.compare(left.getFloat(rowId), right.getFloat(rowId)));\n    } else if (dataType instanceof DoubleType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(Double.compare(left.getDouble(rowId), right.getDouble(rowId)));\n    } else if (dataType instanceof DecimalType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(\n                  BIGDECIMAL_COMPARATOR.compare(left.getDecimal(rowId), right.getDecimal(rowId)));\n    } else if (dataType instanceof StringType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(\n                  STRING_COMPARATOR.compare(left.getString(rowId), right.getString(rowId)));\n    } else if (dataType instanceof BinaryType) {\n      vectorValueComparator =\n          rowId ->\n              booleanComparator.test(\n                  BINARY_COMPARTOR.compare(left.getBinary(rowId), right.getBinary(rowId)));\n    } else {\n      throw new UnsupportedOperationException(dataType + \" can not be compared.\");\n    }\n\n    return vectorValueComparator;\n  }\n\n  /**\n   * Utility method to create a column vector that lazily evaluate the comparator ex. (ie. ==, >=,\n   * <=......) for left and right column vector according to the natural ordering of numbers\n   *\n   * <p>Only primitive data types are supported.\n   */\n  static ColumnVector comparatorVector(\n      ColumnVector left, ColumnVector right, IntPredicate booleanComparator) {\n    IntPredicate vectorValueComparator = getComparator(left, right, booleanComparator);\n\n    return new ColumnVector() {\n\n      @Override\n      public DataType getDataType() {\n        return BooleanType.BOOLEAN;\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(left, right);\n      }\n\n      @Override\n      public int getSize() {\n        return left.getSize();\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return left.isNullAt(rowId) || right.isNullAt(rowId);\n      }\n\n      @Override\n      public boolean getBoolean(int rowId) {\n        if (isNullAt(rowId)) {\n          return false;\n        }\n        return vectorValueComparator.test(rowId);\n      }\n    };\n  }\n\n  /**\n   * Utility method to create a null safe column vector that lazily evaluate the comparator ex. (ie.\n   * <=>) for left and right column vector according to the natural ordering of numbers\n   *\n   * <p>Only primitive data types are supported.\n   */\n  static ColumnVector nullSafeComparatorVector(\n      ColumnVector left, ColumnVector right, IntPredicate booleanComparator) {\n    IntPredicate vectorValueComparator = getComparator(left, right, booleanComparator);\n    return new ColumnVector() {\n      @Override\n      public DataType getDataType() {\n        return BooleanType.BOOLEAN;\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(left, right);\n      }\n\n      @Override\n      public int getSize() {\n        return left.getSize();\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        // Nullsafe comparator can never return null\n        return false;\n      }\n\n      /**\n       * Null safe comparator follows the truth table in Comparison Operators part of following link\n       * https://spark.apache.org/docs/latest/sql-ref-null-semantics.html\n       *\n       * <p>If either left or right is null, return false If both left and right is null, return\n       * true else compare the non null value of left and right\n       *\n       * @param rowId\n       * @return\n       */\n      @Override\n      public boolean getBoolean(int rowId) {\n        if (left.isNullAt(rowId) && right.isNullAt(rowId)) {\n          return true;\n        } else if (left.isNullAt(rowId) || right.isNullAt(rowId)) {\n          return false;\n        }\n        return vectorValueComparator.test(rowId);\n      }\n    };\n  }\n\n  static Expression childAt(Expression expression, int index) {\n    return expression.getChildren().get(index);\n  }\n\n  /**\n   * Combines a list of column vectors into one column vector based on the resolution of idxToReturn\n   *\n   * @param vectors List of ColumnVectors of the same data type with length >= 1\n   * @param idxToReturn Function that takes in a rowId and returns the index of the column vector to\n   *     use as the return value\n   */\n  static ColumnVector combinationVector(\n      List<ColumnVector> vectors, Function<Integer, Integer> idxToReturn) {\n    return new ColumnVector() {\n      // Store the last lookup value to avoid multiple looks up for same rowId.\n      // The general pattern is call `isNullAt(rowId)` followed by `getBoolean(rowId)` or\n      // some other value accessor. So the cache of one value is enough.\n      private int lastLookupRowId = -1;\n      private ColumnVector lastLookupVector = null;\n\n      @Override\n      public DataType getDataType() {\n        return vectors.get(0).getDataType();\n      }\n\n      @Override\n      public int getSize() {\n        return vectors.get(0).getSize();\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(vectors.toArray(new ColumnVector[0]));\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return getVector(rowId).isNullAt(rowId);\n      }\n\n      @Override\n      public boolean getBoolean(int rowId) {\n        return getVector(rowId).getBoolean(rowId);\n      }\n\n      @Override\n      public byte getByte(int rowId) {\n        return getVector(rowId).getByte(rowId);\n      }\n\n      @Override\n      public short getShort(int rowId) {\n        return getVector(rowId).getShort(rowId);\n      }\n\n      @Override\n      public int getInt(int rowId) {\n        return getVector(rowId).getInt(rowId);\n      }\n\n      @Override\n      public long getLong(int rowId) {\n        return getVector(rowId).getLong(rowId);\n      }\n\n      @Override\n      public float getFloat(int rowId) {\n        return getVector(rowId).getFloat(rowId);\n      }\n\n      @Override\n      public double getDouble(int rowId) {\n        return getVector(rowId).getDouble(rowId);\n      }\n\n      @Override\n      public byte[] getBinary(int rowId) {\n        return getVector(rowId).getBinary(rowId);\n      }\n\n      @Override\n      public String getString(int rowId) {\n        return getVector(rowId).getString(rowId);\n      }\n\n      @Override\n      public BigDecimal getDecimal(int rowId) {\n        return getVector(rowId).getDecimal(rowId);\n      }\n\n      @Override\n      public MapValue getMap(int rowId) {\n        return getVector(rowId).getMap(rowId);\n      }\n\n      @Override\n      public ArrayValue getArray(int rowId) {\n        return getVector(rowId).getArray(rowId);\n      }\n\n      @Override\n      public ColumnVector getChild(int ordinal) {\n        return combinationVector(\n            vectors.stream().map(v -> v.getChild(ordinal)).collect(Collectors.toList()),\n            idxToReturn);\n      }\n\n      private ColumnVector getVector(int rowId) {\n        if (rowId == lastLookupRowId) {\n          return lastLookupVector;\n        }\n        lastLookupRowId = rowId;\n        lastLookupVector = vectors.get(idxToReturn.apply(rowId));\n        return lastLookupVector;\n      }\n    };\n  }\n\n  /** Represents an arithmetic operator that can be applied to two numeric values. */\n  public interface ArithmeticOperator {\n    byte apply(byte a, byte b);\n\n    short apply(short a, short b);\n\n    int apply(int a, int b);\n\n    long apply(long a, long b);\n\n    float apply(float a, float b);\n\n    double apply(double a, double b);\n  }\n\n  /**\n   * Creates a column vector that lazily evaluates an arithmetic operation between two column\n   * vectors.\n   *\n   * <p>Only numeric data types are supported.\n   *\n   * @param left the left operand column vector\n   * @param right the right operand column vector\n   * @param operator the arithmetic operator to apply\n   * @return a new column vector representing the result of the arithmetic operation\n   */\n  static ColumnVector arithmeticVector(\n      ColumnVector left, ColumnVector right, ArithmeticOperator operator) {\n    checkArgument(\n        left.getSize() == right.getSize(), \"Left and right operand have different vector sizes.\");\n    checkArgument(\n        left.getDataType().equals(right.getDataType()),\n        \"Left and right operand have different data types.\");\n    return new ColumnVector() {\n      @Override\n      public DataType getDataType() {\n        return left.getDataType();\n      }\n\n      @Override\n      public int getSize() {\n        return left.getSize();\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(left, right);\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return left.isNullAt(rowId) || right.isNullAt(rowId);\n      }\n\n      @Override\n      public byte getByte(int rowId) {\n        return operator.apply(left.getByte(rowId), right.getByte(rowId));\n      }\n\n      @Override\n      public short getShort(int rowId) {\n        return operator.apply(left.getShort(rowId), right.getShort(rowId));\n      }\n\n      @Override\n      public int getInt(int rowId) {\n        return operator.apply(left.getInt(rowId), right.getInt(rowId));\n      }\n\n      @Override\n      public long getLong(int rowId) {\n        return operator.apply(left.getLong(rowId), right.getLong(rowId));\n      }\n\n      @Override\n      public float getFloat(int rowId) {\n        return operator.apply(left.getFloat(rowId), right.getFloat(rowId));\n      }\n\n      @Override\n      public double getDouble(int rowId) {\n        return operator.apply(left.getDouble(rowId), right.getDouble(rowId));\n      }\n    };\n  }\n\n  /**\n   * Checks if the specific expression is an integer literal, throws {@link\n   * UnsupportedOperationException} if not.\n   *\n   * @param expr, expression to be checked.\n   * @param context string describing the context, used for constructing error message.\n   * @param baseExpression expression whose evaluation triggers this check. Used for constructing\n   *     error message.\n   */\n  static void checkIntegerLiteral(Expression expr, String context, Expression baseExpression) {\n    if (!(expr instanceof Literal) || !IntegerType.INTEGER.equals(((Literal) expr).getDataType())) {\n      throw unsupportedExpressionException(\n          baseExpression, String.format(\"%s, expects an integral numeric\", context));\n    }\n  }\n\n  /**\n   * Checks the argument count of an expression. throws {@code unsupportedExpressionException} if\n   * argument count mismatched.\n   */\n  static void checkArgsCount(Expression expr, int expectedCount, String exprName, String context) {\n    if (expr.getChildren().size() != expectedCount) {\n      throw unsupportedExpressionException(\n          expr, String.format(\"Invalid number of inputs of %s expression, %s\", exprName, context));\n    }\n  }\n\n  static void checkIsStringType(DataType dataType, Expression parentExpr, String errorMessage) {\n    if (dataType instanceof StringType) {\n      return;\n    }\n    throw unsupportedExpressionException(parentExpr, errorMessage);\n  }\n\n  static void checkIsLiteral(Expression expr, Expression parentExpr, String errorMessage) {\n    if (!(expr instanceof Literal)) {\n      throw unsupportedExpressionException(parentExpr, errorMessage);\n    }\n  }\n\n  /**\n   * Checks if the collation is `UTF8_BINARY`, since this is the only collation the default engine\n   * can evaluate.\n   */\n  static void checkIsUTF8BinaryCollation(\n      Predicate predicate, CollationIdentifier collationIdentifier) {\n    if (!collationIdentifier.isSparkUTF8BinaryCollation()) {\n      String msg =\n          format(\n              \"Unsupported collation: \\\"%s\\\". Default Engine supports just\" + \" \\\"%s\\\" collation.\",\n              collationIdentifier, CollationIdentifier.SPARK_UTF8_BINARY);\n      throw unsupportedExpressionException(predicate, msg);\n    }\n  }\n\n  /**\n   * Checks if the given expression is a null literal.\n   *\n   * @param expression The expression to check\n   * @return true if the expression is a Literal with null value, false otherwise\n   */\n  static boolean isNullLiteral(Expression expression) {\n    return expression instanceof Literal && ((Literal) expression).getValue() == null;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultPredicateEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultConstantVector;\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport java.util.Optional;\n\n/**\n * Default implementation of {@link PredicateEvaluator}. It makes use of the {@link\n * DefaultExpressionEvaluator} with some modification of the given predicate. Refer to {@link\n * #DefaultPredicateEvaluator(StructType, Predicate)} and {@link #eval(ColumnarBatch, Optional)} for\n * details.\n */\npublic class DefaultPredicateEvaluator implements PredicateEvaluator {\n  private static final String EXISTING_SEL_VECTOR_COL_NAME =\n      \"____existing_selection_vector_value____\";\n  private static final StructField EXISTING_SEL_VECTOR_FIELD =\n      new StructField(EXISTING_SEL_VECTOR_COL_NAME, BooleanType.BOOLEAN, false);\n\n  private final ExpressionEvaluator expressionEvaluator;\n\n  public DefaultPredicateEvaluator(StructType inputSchema, Predicate predicate) {\n    // Create a predicate that takes into account of the selection value in existing selection\n    // vector in addition to the given predicate. This is needed to make a row remain\n    // unselected in the final vector when it is unselected in existing selection vector.\n    Predicate rewrittenPredicate =\n        new And(\n            new Predicate(\"=\", new Column(EXISTING_SEL_VECTOR_COL_NAME), Literal.ofBoolean(true)),\n            predicate);\n    StructType rewrittenInputSchema = inputSchema.add(EXISTING_SEL_VECTOR_FIELD);\n    this.expressionEvaluator =\n        new DefaultExpressionEvaluator(\n            rewrittenInputSchema, rewrittenPredicate, BooleanType.BOOLEAN);\n  }\n\n  @Override\n  public ColumnVector eval(\n      ColumnarBatch inputData, Optional<ColumnVector> existingSelectionVector) {\n    try {\n      ColumnVector newVector =\n          existingSelectionVector.orElse(\n              new DefaultConstantVector(BooleanType.BOOLEAN, inputData.getSize(), true));\n      ColumnarBatch withExistingSelVector =\n          inputData.withNewColumn(\n              inputData.getSchema().length(), EXISTING_SEL_VECTOR_FIELD, newVector);\n\n      return expressionEvaluator.eval(withExistingSelVector);\n    } finally {\n      // release the existing selection vector.\n      Utils.closeCloseables(existingSelectionVector.orElse(null));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ElementAtEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException;\nimport static io.delta.kernel.defaults.internal.expressions.ImplicitCastExpression.canCastTo;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.ScalarExpression;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.MapType;\nimport io.delta.kernel.types.StringType;\nimport java.util.Arrays;\n\n/** Utility methods to evaluate {@code element_at} expression. */\nclass ElementAtEvaluator {\n  private ElementAtEvaluator() {}\n\n  /**\n   * Validate and transform the {@code element_at} expression with given validated and transformed\n   * inputs.\n   */\n  static ScalarExpression validateAndTransform(\n      ScalarExpression elementAt,\n      Expression mapInput,\n      DataType mapInputType,\n      Expression lookupKey,\n      DataType lookupKeyType) {\n\n    MapType asMapType = validateSupportedMapType(elementAt, mapInputType);\n    DataType keyTypeFromMapInput = asMapType.getKeyType();\n\n    if (!keyTypeFromMapInput.equivalent(lookupKeyType)) {\n      if (canCastTo(lookupKeyType, keyTypeFromMapInput)) {\n        lookupKey = new ImplicitCastExpression(lookupKey, keyTypeFromMapInput);\n      } else {\n        String reason =\n            format(\n                \"lookup key type (%s) is different from the map key type (%s)\",\n                lookupKeyType, asMapType.getKeyType());\n        throw unsupportedExpressionException(elementAt, reason);\n      }\n    }\n    return new ScalarExpression(elementAt.getName(), Arrays.asList(mapInput, lookupKey));\n  }\n\n  /**\n   * Utility method to evaluate the {@code element_at} on given map and key vectors.\n   *\n   * @param map {@link ColumnVector} of {@code map(string, string)} type.\n   * @param lookupKey {@link ColumnVector} of {@code string} type.\n   * @return result {@link ColumnVector} containing the lookup values.\n   */\n  static ColumnVector eval(ColumnVector map, ColumnVector lookupKey) {\n    return new ColumnVector() {\n      // Store the last lookup value to avoid multiple looks up for same row id.\n      // The general pattern is call `isNullAt(rowId)` followed by `getString`.\n      // So the cache of one value is enough.\n      private int lastLookupRowId = -1;\n      private String lastLookupValue = null;\n\n      @Override\n      public DataType getDataType() {\n        return ((MapType) map.getDataType()).getValueType();\n      }\n\n      @Override\n      public int getSize() {\n        return map.getSize();\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(map, lookupKey);\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        if (rowId == lastLookupRowId) {\n          return lastLookupValue == null;\n        }\n        return map.isNullAt(rowId) || lookupValue(rowId) == null;\n      }\n\n      @Override\n      public String getString(int rowId) {\n        lookupValue(rowId);\n        return lastLookupValue == null ? null : lastLookupValue;\n      }\n\n      private Object lookupValue(int rowId) {\n        if (rowId == lastLookupRowId) {\n          return lastLookupValue;\n        }\n        lastLookupRowId = rowId;\n        String keyValue = lookupKey.getString(rowId);\n        lastLookupValue = findValueForKey(map.getMap(rowId), keyValue);\n        return lastLookupValue;\n      }\n\n      /**\n       * Given a {@link MapValue} and string {@code key} find the corresponding value. Returns null\n       * if the key is not in the map.\n       *\n       * @param mapValue String->String map to search\n       * @param key the key to look up the value for; may be null\n       */\n      private String findValueForKey(MapValue mapValue, String key) {\n        ColumnVector keyVector = mapValue.getKeys();\n        for (int i = 0; i < mapValue.getSize(); i++) {\n          if ((keyVector.isNullAt(i) && key == null)\n              || (!keyVector.isNullAt(i) && keyVector.getString(i).equals(key))) {\n            return mapValue.getValues().isNullAt(i) ? null : mapValue.getValues().getString(i);\n          }\n        }\n        // If the key is not in the map return null\n        return null;\n      }\n    };\n  }\n\n  private static MapType validateSupportedMapType(Expression elementAt, DataType mapInputType) {\n    checkArgument(\n        mapInputType instanceof MapType,\n        \"expected a map type input as first argument: %s\",\n        elementAt);\n    MapType asMapType = (MapType) mapInputType;\n    // For now we only need to support lookup in columns of type `map(string -> string)`.\n    // Additional type support may be added later\n    if (asMapType.getKeyType().equivalent(StringType.STRING)\n        && asMapType.getValueType().equivalent(StringType.STRING)) {\n      return asMapType;\n    }\n    throw new UnsupportedOperationException(\n        format(\"%s: Supported only on type map(string, string) input data\", elementAt));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ExpressionVisitor.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.expressions.AlwaysFalse.ALWAYS_FALSE;\nimport static io.delta.kernel.expressions.AlwaysTrue.ALWAYS_TRUE;\nimport static io.delta.kernel.internal.util.ExpressionUtils.createPredicate;\nimport static java.util.stream.Collectors.joining;\n\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.types.CollationIdentifier;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Optional;\n\n/**\n * Interface to allow visiting an expression tree and implementing handling for each specific\n * expression type.\n *\n * @param <R> Return type of result of visit expression methods.\n */\nabstract class ExpressionVisitor<R> {\n\n  abstract R visitAnd(And and);\n\n  abstract R visitOr(Or or);\n\n  abstract R visitAlwaysTrue(AlwaysTrue alwaysTrue);\n\n  abstract R visitAlwaysFalse(AlwaysFalse alwaysFalse);\n\n  abstract R visitComparator(Predicate predicate);\n\n  abstract R visitLiteral(Literal literal);\n\n  abstract R visitColumn(Column column);\n\n  abstract R visitCast(ImplicitCastExpression cast);\n\n  abstract R visitPartitionValue(PartitionValueExpression partitionValue);\n\n  abstract R visitElementAt(ScalarExpression elementAt);\n\n  abstract R visitNot(Predicate predicate);\n\n  abstract R visitIsNotNull(Predicate predicate);\n\n  abstract R visitIsNull(Predicate predicate);\n\n  abstract R visitCoalesce(ScalarExpression ifNull);\n\n  abstract R visitTimeAdd(ScalarExpression timeAdd);\n\n  abstract R visitSubstring(ScalarExpression subString);\n\n  abstract R visitAdd(ScalarExpression add);\n\n  abstract R visitLike(Predicate predicate);\n\n  abstract R visitStartsWith(Predicate predicate);\n\n  abstract R visitIn(In in);\n\n  final R visit(Expression expression) {\n    if (expression instanceof PartitionValueExpression) {\n      return visitPartitionValue((PartitionValueExpression) expression);\n    } else if (expression instanceof ScalarExpression) {\n      return visitScalarExpression((ScalarExpression) expression);\n    } else if (expression instanceof Literal) {\n      return visitLiteral((Literal) expression);\n    } else if (expression instanceof Column) {\n      return visitColumn((Column) expression);\n    } else if (expression instanceof ImplicitCastExpression) {\n      return visitCast((ImplicitCastExpression) expression);\n    }\n\n    throw new UnsupportedOperationException(\n        String.format(\"Expression %s is not supported.\", expression));\n  }\n\n  private R visitScalarExpression(ScalarExpression expression) {\n    List<Expression> children = expression.getChildren();\n    String name = expression.getName().toUpperCase(Locale.ENGLISH);\n    Optional<CollationIdentifier> collationIdentifier = Optional.empty();\n    if (expression instanceof Predicate) {\n      collationIdentifier = ((Predicate) expression).getCollationIdentifier();\n    }\n    switch (name) {\n      case \"ALWAYS_TRUE\":\n        return visitAlwaysTrue(ALWAYS_TRUE);\n      case \"ALWAYS_FALSE\":\n        return visitAlwaysFalse(ALWAYS_FALSE);\n      case \"AND\":\n        return visitAnd(new And(elemAsPredicate(children, 0), elemAsPredicate(children, 1)));\n      case \"OR\":\n        return visitOr(new Or(elemAsPredicate(children, 0), elemAsPredicate(children, 1)));\n      case \"=\":\n      case \"<\":\n      case \"<=\":\n      case \">\":\n      case \">=\":\n      case \"IS NOT DISTINCT FROM\":\n        return visitComparator(createPredicate(name, children, collationIdentifier));\n      case \"ELEMENT_AT\":\n        return visitElementAt(expression);\n      case \"NOT\":\n        return visitNot(createPredicate(name, children, collationIdentifier));\n      case \"IS_NOT_NULL\":\n        return visitIsNotNull(createPredicate(name, children, collationIdentifier));\n      case \"IS_NULL\":\n        return visitIsNull(createPredicate(name, children, collationIdentifier));\n      case \"COALESCE\":\n        return visitCoalesce(expression);\n      case \"ADD\":\n        return visitAdd(expression);\n      case \"TIMEADD\":\n        return visitTimeAdd(expression);\n      case \"SUBSTRING\":\n        return visitSubstring(expression);\n      case \"LIKE\":\n        return visitLike(createPredicate(name, children, collationIdentifier));\n      case \"STARTS_WITH\":\n        return visitStartsWith(createPredicate(name, children, collationIdentifier));\n      case \"IN\":\n        if (collationIdentifier.isPresent()) {\n          return visitIn(\n              new In(\n                  children.get(0),\n                  children.subList(1, children.size()),\n                  collationIdentifier.get()));\n        } else {\n          return visitIn(new In(children.get(0), children.subList(1, children.size())));\n        }\n      default:\n        throw new UnsupportedOperationException(\n            String.format(\"Scalar expression `%s` is not supported.\", name));\n    }\n  }\n\n  private static Predicate elemAsPredicate(List<Expression> expressions, int index) {\n    if (expressions.size() <= index) {\n      throw new RuntimeException(\n          String.format(\n              \"Trying to access invalid entry (%d) in list %s\",\n              index, expressions.stream().map(Object::toString).collect(joining(\",\"))));\n    }\n\n    Expression elemExpression = expressions.get(index);\n    if (!(elemExpression instanceof Predicate)) {\n      throw new RuntimeException(\"Expected a predicate, but got \" + elemExpression);\n    }\n    return (Predicate) expressions.get(index);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ImplicitCastExpression.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static java.lang.String.format;\nimport static java.util.Collections.unmodifiableMap;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.engine.DefaultExpressionHandler;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.types.DataType;\nimport java.util.*;\n\n/**\n * An implicit cast expression to convert the input type to another given type. Here is the valid\n * list of casts\n *\n * <p>\n *\n * <ul>\n *   <li>{@code byte} to {@code short, int, long, float, double}\n *   <li>{@code short} to {@code int, long, float, double}\n *   <li>{@code int} to {@code long, float, double}\n *   <li>{@code long} to {@code float, double}\n *   <li>{@code float} to {@code double}\n * </ul>\n *\n * <p>The above list is not exhaustive. Based on the need, we can add more casts.\n *\n * <p>In {@link DefaultExpressionHandler} this is used when the operands of an expression are not of\n * the same type, but the evaluator expects same type inputs. There could be more use cases, but for\n * now this is the only use case.\n */\nfinal class ImplicitCastExpression implements Expression {\n  private final Expression input;\n  private final DataType outputType;\n\n  /**\n   * Create a cast around the given input expression to specified output data type. It is the\n   * responsibility of the caller to validate the input expression can be cast to the new type using\n   * {@link #canCastTo(DataType, DataType)}\n   */\n  ImplicitCastExpression(Expression input, DataType outputType) {\n    this.input = requireNonNull(input, \"input is null\");\n    this.outputType = requireNonNull(outputType, \"outputType is null\");\n  }\n\n  public Expression getInput() {\n    return input;\n  }\n\n  public DataType getOutputType() {\n    return outputType;\n  }\n\n  @Override\n  public List<Expression> getChildren() {\n    return Collections.singletonList(input);\n  }\n\n  /**\n   * Evaluate the given column expression on the input {@link ColumnVector}.\n   *\n   * @param input {@link ColumnVector} data of the input to the cast expression.\n   * @return {@link ColumnVector} result applying target type casting on every element in the input\n   *     {@link ColumnVector}.\n   */\n  ColumnVector eval(ColumnVector input) {\n    String fromTypeStr = input.getDataType().toString();\n    switch (fromTypeStr) {\n      case \"byte\":\n        return new ByteUpConverter(outputType, input);\n      case \"short\":\n        return new ShortUpConverter(outputType, input);\n      case \"integer\":\n        return new IntUpConverter(outputType, input);\n      case \"long\":\n        return new LongUpConverter(outputType, input);\n      case \"float\":\n        return new FloatUpConverter(outputType, input);\n      default:\n        throw new UnsupportedOperationException(\n            format(\"Cast from %s is not supported\", fromTypeStr));\n    }\n  }\n\n  /** Map containing for each type what are the target cast types can be. */\n  private static final Map<String, List<String>> UP_CASTABLE_TYPE_TABLE =\n      unmodifiableMap(\n          new HashMap<String, List<String>>() {\n            {\n              this.put(\"byte\", Arrays.asList(\"short\", \"integer\", \"long\", \"float\", \"double\"));\n              this.put(\"short\", Arrays.asList(\"integer\", \"long\", \"float\", \"double\"));\n              this.put(\"integer\", Arrays.asList(\"long\", \"float\", \"double\"));\n              this.put(\"long\", Arrays.asList(\"float\", \"double\"));\n              this.put(\"float\", Arrays.asList(\"double\"));\n            }\n          });\n\n  /**\n   * Utility method which returns whether the given {@code from} type can be cast to {@code to}\n   * type.\n   */\n  static boolean canCastTo(DataType from, DataType to) {\n    // TODO: The type name should be a first class method on `DataType` instead of getting it\n    // using the `toString`.\n    String fromStr = from.toString();\n    String toStr = to.toString();\n    return UP_CASTABLE_TYPE_TABLE.containsKey(fromStr)\n        && UP_CASTABLE_TYPE_TABLE.get(fromStr).contains(toStr);\n  }\n\n  /** Base class for up casting {@link ColumnVector} data. */\n  private abstract static class UpConverter implements ColumnVector {\n    protected final DataType targetType;\n    protected final ColumnVector inputVector;\n\n    UpConverter(DataType targetType, ColumnVector inputVector) {\n      this.targetType = targetType;\n      this.inputVector = inputVector;\n    }\n\n    @Override\n    public DataType getDataType() {\n      return targetType;\n    }\n\n    @Override\n    public boolean isNullAt(int rowId) {\n      return inputVector.isNullAt(rowId);\n    }\n\n    @Override\n    public int getSize() {\n      return inputVector.getSize();\n    }\n\n    @Override\n    public void close() {\n      inputVector.close();\n    }\n  }\n\n  private static class ByteUpConverter extends UpConverter {\n    ByteUpConverter(DataType targetType, ColumnVector inputVector) {\n      super(targetType, inputVector);\n    }\n\n    @Override\n    public short getShort(int rowId) {\n      return inputVector.getByte(rowId);\n    }\n\n    @Override\n    public int getInt(int rowId) {\n      return inputVector.getByte(rowId);\n    }\n\n    @Override\n    public long getLong(int rowId) {\n      return inputVector.getByte(rowId);\n    }\n\n    @Override\n    public float getFloat(int rowId) {\n      return inputVector.getByte(rowId);\n    }\n\n    @Override\n    public double getDouble(int rowId) {\n      return inputVector.getByte(rowId);\n    }\n  }\n\n  private static class ShortUpConverter extends UpConverter {\n    ShortUpConverter(DataType targetType, ColumnVector inputVector) {\n      super(targetType, inputVector);\n    }\n\n    @Override\n    public int getInt(int rowId) {\n      return inputVector.getShort(rowId);\n    }\n\n    @Override\n    public long getLong(int rowId) {\n      return inputVector.getShort(rowId);\n    }\n\n    @Override\n    public float getFloat(int rowId) {\n      return inputVector.getShort(rowId);\n    }\n\n    @Override\n    public double getDouble(int rowId) {\n      return inputVector.getShort(rowId);\n    }\n  }\n\n  private static class IntUpConverter extends UpConverter {\n    IntUpConverter(DataType targetType, ColumnVector inputVector) {\n      super(targetType, inputVector);\n    }\n\n    @Override\n    public long getLong(int rowId) {\n      return inputVector.getInt(rowId);\n    }\n\n    @Override\n    public float getFloat(int rowId) {\n      return inputVector.getInt(rowId);\n    }\n\n    @Override\n    public double getDouble(int rowId) {\n      return inputVector.getInt(rowId);\n    }\n  }\n\n  private static class LongUpConverter extends UpConverter {\n    LongUpConverter(DataType targetType, ColumnVector inputVector) {\n      super(targetType, inputVector);\n    }\n\n    @Override\n    public float getFloat(int rowId) {\n      return inputVector.getLong(rowId);\n    }\n\n    @Override\n    public double getDouble(int rowId) {\n      return inputVector.getLong(rowId);\n    }\n  }\n\n  private static class FloatUpConverter extends UpConverter {\n    FloatUpConverter(DataType targetType, ColumnVector inputVector) {\n      super(targetType, inputVector);\n    }\n\n    @Override\n    public double getDouble(int rowId) {\n      return inputVector.getFloat(rowId);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/InExpressionEvaluator.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException;\nimport static io.delta.kernel.defaults.internal.expressions.DefaultExpressionUtils.*;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.In;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.internal.util.Preconditions;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.types.*;\nimport io.delta.kernel.types.CollationIdentifier;\nimport java.math.BigDecimal;\nimport java.util.*;\nimport java.util.function.BiFunction;\n\n/** Utility methods to evaluate {@code IN} expression. */\npublic class InExpressionEvaluator {\n\n  private static final Map<Class<? extends DataType>, BiFunction<Object, Object, Integer>>\n      COMPARATORS = createComparatorMap();\n\n  private static Map<Class<? extends DataType>, BiFunction<Object, Object, Integer>>\n      createComparatorMap() {\n    Map<Class<? extends DataType>, BiFunction<Object, Object, Integer>> map = new HashMap<>();\n    map.put(BooleanType.class, (v1, v2) -> Boolean.compare((Boolean) v1, (Boolean) v2));\n    map.put(\n        ByteType.class,\n        (v1, v2) -> Byte.compare(((Number) v1).byteValue(), ((Number) v2).byteValue()));\n    map.put(\n        ShortType.class,\n        (v1, v2) -> Short.compare(((Number) v1).shortValue(), ((Number) v2).shortValue()));\n    map.put(\n        IntegerType.class,\n        (v1, v2) -> Integer.compare(((Number) v1).intValue(), ((Number) v2).intValue()));\n    map.put(\n        DateType.class,\n        (v1, v2) -> Integer.compare(((Number) v1).intValue(), ((Number) v2).intValue()));\n    map.put(\n        LongType.class,\n        (v1, v2) -> Long.compare(((Number) v1).longValue(), ((Number) v2).longValue()));\n    map.put(\n        TimestampType.class,\n        (v1, v2) -> Long.compare(((Number) v1).longValue(), ((Number) v2).longValue()));\n    map.put(\n        TimestampNTZType.class,\n        (v1, v2) -> Long.compare(((Number) v1).longValue(), ((Number) v2).longValue()));\n    map.put(\n        FloatType.class,\n        (v1, v2) -> Float.compare(((Number) v1).floatValue(), ((Number) v2).floatValue()));\n    map.put(\n        DoubleType.class,\n        (v1, v2) -> Double.compare(((Number) v1).doubleValue(), ((Number) v2).doubleValue()));\n    map.put(\n        DecimalType.class,\n        (v1, v2) -> BIGDECIMAL_COMPARATOR.compare((BigDecimal) v1, (BigDecimal) v2));\n    map.put(StringType.class, (v1, v2) -> STRING_COMPARATOR.compare((String) v1, (String) v2));\n    map.put(BinaryType.class, (v1, v2) -> BINARY_COMPARTOR.compare((byte[]) v1, (byte[]) v2));\n    return Collections.unmodifiableMap(map);\n  }\n\n  /** Validates and transforms the {@code IN} expression. */\n  static In validateAndTransform(\n      In in,\n      Expression valueExpression,\n      DataType valueDataType,\n      List<Expression> inListExpressions,\n      List<DataType> inListDataTypes) {\n    // TODO: [delta-io/delta#5227] Try to reuse Implicit cast and simplify comparison logic\n    validateArgumentCount(in, inListExpressions);\n    validateInListElementsAreLiterals(in, inListExpressions);\n    validateTypeCompatibility(in, valueDataType, inListDataTypes);\n    validateCollation(in, valueDataType, inListExpressions, inListDataTypes);\n    if (in.getCollationIdentifier().isPresent()) {\n      return new In(valueExpression, inListExpressions, in.getCollationIdentifier().get());\n    } else {\n      return new In(valueExpression, inListExpressions);\n    }\n  }\n\n  /** Evaluates the IN expression on the given column vectors. */\n  static ColumnVector eval(List<ColumnVector> childrenVectors) {\n    return new InColumnVector(childrenVectors);\n  }\n\n  ////////////////////\n  // Private Helper //\n  ////////////////////\n\n  private static void validateArgumentCount(In in, List<Expression> inListExpressions) {\n    Objects.requireNonNull(inListExpressions);\n    if (inListExpressions.isEmpty()) {\n      throw unsupportedExpressionException(\n          in,\n          \"IN expression requires at least 1 element in the IN list. \"\n              + \"Example usage: column IN (value1, value2, ...)\");\n    }\n  }\n\n  private static void validateInListElementsAreLiterals(In in, List<Expression> inListExpressions) {\n    for (int i = 0; i < inListExpressions.size(); i++) {\n      Expression child = inListExpressions.get(i);\n      if (!(child instanceof Literal)) {\n        throw unsupportedExpressionException(\n            in,\n            String.format(\n                \"IN expression requires all list elements to be literals. \"\n                    + \"Non-literal expression found at position %d: %s. \"\n                    + \"Only constant values are currently supported in IN lists.\",\n                i + 1, child.getClass().getSimpleName()));\n      }\n    }\n  }\n\n  private static void validateTypeCompatibility(\n      In in, DataType valueDataType, List<DataType> inListDataTypes) {\n    // Check for nested types which are not supported\n    if (valueDataType.isNested()) {\n      throw unsupportedExpressionException(\n          in, String.format(\"IN expression does not support nested types.\", valueDataType));\n    }\n\n    for (int i = 0; i < inListDataTypes.size(); i++) {\n      DataType listElementType = inListDataTypes.get(i);\n      if (!valueDataType.equivalent(listElementType)) {\n        throw unsupportedExpressionException(\n            in,\n            String.format(\n                \"IN expression requires all list elements to match the value type. \"\n                    + \"Value type: %s, but found incompatible element type at position %d: %s. \"\n                    + \"Consider casting the incompatible element to the value type.\",\n                valueDataType, i + 1, listElementType));\n      }\n    }\n  }\n\n  /** Validates that collation is only used with string types and the collation is UTF8Binary. */\n  private static void validateCollation(\n      In in,\n      DataType valueDataType,\n      List<Expression> inListExpressions,\n      List<DataType> inListDataTypes) {\n    in.getCollationIdentifier()\n        .ifPresent(\n            collationIdentifier -> {\n              checkIsUTF8BinaryCollation(in, collationIdentifier);\n              validateStringTypesForCollation(\n                  in, valueDataType, inListExpressions, inListDataTypes);\n            });\n  }\n\n  private static void validateStringTypesForCollation(\n      In in,\n      DataType valueDataType,\n      List<Expression> inListExpressions,\n      List<DataType> inListDataTypes) {\n    checkIsStringType(\n        valueDataType, in, \"'IN' with collation expects STRING type for the value expression\");\n    for (int i = 0; i < inListDataTypes.size(); i++) {\n      if (!isNullLiteral(inListExpressions.get(i))) {\n        checkIsStringType(\n            inListDataTypes.get(i),\n            in,\n            \"'IN' with collation expects STRING type for all list elements\");\n      }\n    }\n  }\n\n  private static void checkIsUTF8BinaryCollation(In in, CollationIdentifier collationIdentifier) {\n    if (!\"SPARK.UTF8_BINARY\".equals(collationIdentifier.toString())) {\n      throw unsupportedExpressionException(\n          in,\n          String.format(\n              \"Unsupported collation: \\\"%s\\\". \"\n                  + \"Default Engine supports just \\\"SPARK.UTF8_BINARY\\\" collation.\",\n              collationIdentifier));\n    }\n  }\n\n  private static void checkIsStringType(DataType dataType, In in, String message) {\n    if (!(dataType instanceof StringType)) {\n      throw unsupportedExpressionException(in, message);\n    }\n  }\n\n  private static boolean compareValues(Object value1, Object value2, DataType valueType) {\n    Preconditions.checkArgument(value1 != null || value2 != null);\n    if (value1 == null || value2 == null) {\n      return false;\n    }\n    return getComparator(valueType).apply(value1, value2) == 0;\n  }\n\n  private static BiFunction<Object, Object, Integer> getComparator(DataType dataType) {\n    BiFunction<Object, Object, Integer> comparator = COMPARATORS.get(dataType.getClass());\n    if (comparator == null) {\n      throw new UnsupportedOperationException(\n          \"No comparator available for data type: \" + dataType.getClass().getSimpleName());\n    }\n    return comparator;\n  }\n\n  /** Column vector implementation for IN expression evaluation. */\n  private static class InColumnVector implements ColumnVector {\n    private final ColumnVector valueVector;\n    private final List<ColumnVector> inListVectors;\n\n    InColumnVector(List<ColumnVector> childrenVectors) {\n      this.valueVector = childrenVectors.get(0);\n      this.inListVectors = childrenVectors.subList(1, childrenVectors.size());\n\n      // Validate type compatibility once during construction rather than for each row\n      DataType valueType = valueVector.getDataType();\n      for (ColumnVector inListVector : inListVectors) {\n        Preconditions.checkArgument(\n            valueType.equivalent(inListVector.getDataType()),\n            String.format(\n                \"Type mismatch in IN expression: value type %s is not equivalent to \"\n                    + \"list element type %s\",\n                valueType, inListVector.getDataType()));\n      }\n    }\n\n    @Override\n    public DataType getDataType() {\n      return BooleanType.BOOLEAN;\n    }\n\n    @Override\n    public int getSize() {\n      return valueVector.getSize();\n    }\n\n    @Override\n    public void close() {\n      Utils.closeCloseables(valueVector);\n      inListVectors.forEach(Utils::closeCloseables);\n    }\n\n    @Override\n    public boolean getBoolean(int rowId) {\n      Optional<Boolean> result = evaluateInLogic(rowId);\n      Preconditions.checkArgument(\n          result.isPresent(), \"This method is expected to be called only when isNullAt is false\");\n      return result.get();\n    }\n\n    @Override\n    public boolean isNullAt(int rowId) {\n      return !evaluateInLogic(rowId).isPresent();\n    }\n\n    private Optional<Boolean> evaluateInLogic(int rowId) {\n      if (valueVector.isNullAt(rowId)) {\n        return Optional.empty();\n      }\n\n      Object valueToFind =\n          VectorUtils.getValueAsObject(valueVector, valueVector.getDataType(), rowId);\n\n      // Track if we encounter any null values in the IN list\n      // SQL semantics:\n      // - If value matches any element, return true (e.g., 5 IN {0, 4, null, 5} = true)\n      // - If value is null OR (no matches found AND any null in list), return null\n      //   (e.g., null IN {1, 2} = null, 3 IN {1, null, 2} = null)\n      // - If value is not null AND no matches found AND no nulls in list, return false\n      //   (e.g., 3 IN {1, 2} = false)\n      boolean foundNull = false;\n\n      for (ColumnVector inListVector : inListVectors) {\n        if (inListVector.isNullAt(rowId)) {\n          foundNull = true;\n        } else {\n          Object inListValue =\n              VectorUtils.getValueAsObject(inListVector, inListVector.getDataType(), rowId);\n          if (compareValues(valueToFind, inListValue, valueVector.getDataType())) {\n            return Optional.of(true);\n          }\n        }\n      }\n      return foundNull ? Optional.empty() : Optional.of(false);\n    }\n  }\n\n  private InExpressionEvaluator() {}\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/LikeExpressionEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.invalidEscapeSequence;\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.StringType;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.regex.Pattern;\n\n/** Utility methods to evaluate {@code like} expression. */\npublic class LikeExpressionEvaluator {\n  private LikeExpressionEvaluator() {}\n\n  static Predicate validateAndTransform(\n      Predicate like, List<Expression> childrenExpressions, List<DataType> childrenOutputTypes) {\n    int size = childrenExpressions.size();\n    if (size < 2 || size > 3) {\n      throw unsupportedExpressionException(\n          like,\n          \"Invalid number of inputs to LIKE expression. \"\n              + \"Example usage: LIKE(column, 'test%'), LIKE(column, 'test\\\\[%', '\\\\')\");\n    }\n\n    Expression left = childrenExpressions.get(0);\n    DataType leftOutputType = childrenOutputTypes.get(0);\n    Expression right = childrenExpressions.get(1);\n    DataType rightOutputType = childrenOutputTypes.get(1);\n    Expression escapeCharExpr = size == 3 ? childrenExpressions.get(2) : null;\n    DataType escapeCharOutputType = size == 3 ? childrenOutputTypes.get(2) : null;\n\n    if (!(StringType.STRING.equivalent(leftOutputType)\n        && StringType.STRING.equivalent(rightOutputType))) {\n      throw unsupportedExpressionException(\n          like, \"LIKE is only supported for string type expressions\");\n    }\n\n    if (escapeCharExpr != null\n        && (!(escapeCharExpr instanceof Literal\n            && StringType.STRING.equivalent(escapeCharOutputType)))) {\n      throw unsupportedExpressionException(\n          like, \"LIKE expects escape token expression to be a literal of String type\");\n    }\n\n    Literal literal = (Literal) escapeCharExpr;\n    if (literal != null && literal.getValue().toString().length() != 1) {\n      throw unsupportedExpressionException(\n          like, \"LIKE expects escape token to be a single character\");\n    }\n\n    List<Expression> children = new ArrayList<>(Arrays.asList(left, right));\n    if (Objects.nonNull(escapeCharExpr)) {\n      children.add(escapeCharExpr);\n    }\n    return new Predicate(like.getName(), children);\n  }\n\n  static ColumnVector eval(\n      List<Expression> childrenExpressions, List<ColumnVector> childrenVectors) {\n    final char DEFAULT_ESCAPE_CHAR = '\\\\';\n    final boolean isPatternLiteralType = childrenExpressions.get(1) instanceof Literal;\n\n    return new ColumnVector() {\n      final ColumnVector escapeCharVector =\n          childrenVectors.size() == 3 ? childrenVectors.get(2) : null;\n      final ColumnVector left = childrenVectors.get(0);\n      final ColumnVector right = childrenVectors.get(1);\n\n      Character escapeChar = null;\n      String regexCache = null;\n\n      public void initEscapeCharIfRequired() {\n        if (escapeChar == null) {\n          escapeChar =\n              escapeCharVector != null && !escapeCharVector.getString(0).isEmpty()\n                  ? escapeCharVector.getString(0).charAt(0)\n                  : DEFAULT_ESCAPE_CHAR;\n        }\n      }\n\n      @Override\n      public DataType getDataType() {\n        return BooleanType.BOOLEAN;\n      }\n\n      @Override\n      public int getSize() {\n        return left.getSize();\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(left, right);\n      }\n\n      @Override\n      public boolean getBoolean(int rowId) {\n        initEscapeCharIfRequired();\n        return isLike(left.getString(rowId), right.getString(rowId), escapeChar);\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return left.isNullAt(rowId) || right.isNullAt(rowId);\n      }\n\n      public boolean isLike(String input, String pattern, char escape) {\n        if (!Objects.isNull(input) && !Objects.isNull(pattern)) {\n          String regex = getRegexFromCacheOrEval(pattern, escape);\n          return input.matches(regex);\n        }\n        return false;\n      }\n\n      public String getRegexFromCacheOrEval(String pattern, char escape) {\n        if (regexCache != null) {\n          return regexCache;\n        }\n        String regex = escapeLikeRegex(pattern, escape);\n        if (isPatternLiteralType) { // set cache only for literals to avoid re-computation\n          regexCache = regex;\n        }\n        return regex;\n      }\n    };\n  }\n\n  /**\n   * utility method to convert a predicate pattern to a java regex\n   *\n   * @param pattern the pattern used in the expression\n   * @param escape escape character to use\n   * @return java regex\n   */\n  private static String escapeLikeRegex(String pattern, char escape) {\n    final int len = pattern.length();\n    final StringBuilder javaPattern = new StringBuilder(len + len);\n    for (int i = 0; i < len; i++) {\n      char c = pattern.charAt(i);\n\n      if (c == escape) {\n        if (i == (pattern.length() - 1)) {\n          throw invalidEscapeSequence(pattern, i);\n        }\n        char nextChar = pattern.charAt(i + 1);\n        if ((nextChar == '_') || (nextChar == '%') || (nextChar == escape)) {\n          javaPattern.append(Pattern.quote(Character.toString(nextChar)));\n          i++;\n        } else {\n          throw invalidEscapeSequence(pattern, i);\n        }\n      } else if (c == '_') {\n        javaPattern.append('.');\n      } else if (c == '%') {\n        javaPattern.append(\".*\");\n      } else {\n        javaPattern.append(Pattern.quote(Character.toString(c)));\n      }\n    }\n    return \"(?s)\" + javaPattern;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/PartitionValueEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport io.delta.kernel.internal.util.PartitionUtils;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.sql.Date;\nimport java.sql.Timestamp;\n\n/** Utility methods to evaluate {@code partition_value} expression */\nclass PartitionValueEvaluator {\n  /**\n   * Evaluate the {@code partition_value} expression for given input column vector and generate a\n   * column vector with decoded values according to the given partition type.\n   */\n  static ColumnVector eval(ColumnVector input, DataType partitionType) {\n    return new ColumnVector() {\n      @Override\n      public DataType getDataType() {\n        return partitionType;\n      }\n\n      @Override\n      public int getSize() {\n        return input.getSize();\n      }\n\n      @Override\n      public void close() {\n        input.close();\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return input.isNullAt(rowId);\n      }\n\n      @Override\n      public boolean getBoolean(int rowId) {\n        return Boolean.parseBoolean(input.getString(rowId));\n      }\n\n      @Override\n      public byte getByte(int rowId) {\n        return Byte.parseByte(input.getString(rowId));\n      }\n\n      @Override\n      public short getShort(int rowId) {\n        return Short.parseShort(input.getString(rowId));\n      }\n\n      @Override\n      public int getInt(int rowId) {\n        if (partitionType.equivalent(IntegerType.INTEGER)) {\n          return Integer.parseInt(input.getString(rowId));\n        } else if (partitionType.equivalent(DateType.DATE)) {\n          return InternalUtils.daysSinceEpoch(Date.valueOf(input.getString(rowId)));\n        }\n        throw new UnsupportedOperationException(\"Invalid value request for data type\");\n      }\n\n      @Override\n      public long getLong(int rowId) {\n        if (partitionType.equivalent(LongType.LONG)) {\n          return Long.parseLong(input.getString(rowId));\n        } else if (partitionType.equivalent(TimestampType.TIMESTAMP)) {\n          // For TIMESTAMP type the format could be standard format or ISO8601\n          return PartitionUtils.tryParseTimestamp(input.getString(rowId));\n        } else if (partitionType.equivalent(TimestampNTZType.TIMESTAMP_NTZ)) {\n          // For TIMESTAMP_NTZ the format should never have timezone info\n          return InternalUtils.microsSinceEpoch(Timestamp.valueOf(input.getString(rowId)));\n        }\n        throw new UnsupportedOperationException(\"Invalid value request for data type\");\n      }\n\n      @Override\n      public float getFloat(int rowId) {\n        return Float.parseFloat(input.getString(rowId));\n      }\n\n      @Override\n      public double getDouble(int rowId) {\n        return Double.parseDouble(input.getString(rowId));\n      }\n\n      @Override\n      public byte[] getBinary(int rowId) {\n        return input.isNullAt(rowId) ? null : input.getString(rowId).getBytes();\n      }\n\n      @Override\n      public String getString(int rowId) {\n        return input.getString(rowId);\n      }\n\n      @Override\n      public BigDecimal getDecimal(int rowId) {\n        return input.isNullAt(rowId) ? null : new BigDecimal(input.getString(rowId));\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/StartsWithExpressionEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.expressions.DefaultExpressionUtils.*;\nimport static io.delta.kernel.internal.util.ExpressionUtils.createPredicate;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.CollationIdentifier;\nimport io.delta.kernel.types.DataType;\nimport java.util.List;\n\npublic class StartsWithExpressionEvaluator {\n\n  /** Validates and transforms the {@code starts_with} expression. */\n  static Predicate validateAndTransform(\n      Predicate startsWith,\n      List<Expression> childrenExpressions,\n      List<DataType> childrenOutputTypes) {\n    checkArgsCount(\n        startsWith,\n        /* expectedCount= */ 2,\n        startsWith.getName(),\n        \"Example usage: STARTS_WITH(column, 'test')\");\n    for (DataType dataType : childrenOutputTypes) {\n      checkIsStringType(dataType, startsWith, \"'STARTS_WITH' expects STRING type inputs\");\n    }\n\n    // TODO: support non literal as the second input of starts with.\n    checkIsLiteral(\n        childrenExpressions.get(1),\n        startsWith,\n        \"'STARTS_WITH' expects literal as the second input\");\n\n    if (startsWith.getCollationIdentifier().isPresent()) {\n      CollationIdentifier collationIdentifier = startsWith.getCollationIdentifier().get();\n      checkIsUTF8BinaryCollation(startsWith, collationIdentifier);\n    }\n    return createPredicate(\n        startsWith.getName(), startsWith.getChildren(), startsWith.getCollationIdentifier());\n  }\n\n  static ColumnVector eval(List<ColumnVector> childrenVectors) {\n    return new ColumnVector() {\n      final ColumnVector left = childrenVectors.get(0);\n      final ColumnVector right = childrenVectors.get(1);\n\n      @Override\n      public DataType getDataType() {\n        return BooleanType.BOOLEAN;\n      }\n\n      @Override\n      public int getSize() {\n        return left.getSize();\n      }\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(left, right);\n      }\n\n      @Override\n      public boolean getBoolean(int rowId) {\n        if (isNullAt(rowId)) {\n          // The return value is undefined and can be anything, if the slot for rowId is null.\n          return false;\n        }\n        return left.getString(rowId).startsWith(right.getString(rowId));\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        return left.isNullAt(rowId) || right.isNullAt(rowId);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/SubstringEvaluator.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.expressions.Expression;\nimport io.delta.kernel.expressions.ScalarExpression;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.*;\nimport java.util.*;\n\n/** Utility methods to evaluate {@code substring} expression. */\npublic class SubstringEvaluator {\n\n  private SubstringEvaluator() {}\n\n  /** Validates and transforms the {@code substring} expression. */\n  static ScalarExpression validateAndTransform(\n      ScalarExpression substring,\n      List<Expression> childrenExpressions,\n      List<DataType> childrenOutputTypes) {\n    int childrenSize = substring.getChildren().size();\n    if (childrenSize < 2 || childrenSize > 3) {\n      throw unsupportedExpressionException(\n          substring,\n          \"Invalid number of inputs to SUBSTRING expression. \"\n              + \"Example usage: SUBSTRING(column, pos), SUBSTRING(column, pos, len)\");\n    }\n\n    // TODO: support binary type.\n    if (!StringType.STRING.equals(childrenOutputTypes.get(0))) {\n      throw unsupportedExpressionException(\n          substring, \"Invalid type of first input of SUBSTRING: expects STRING\");\n    }\n\n    Expression posExpression = childrenExpressions.get(1);\n    DefaultExpressionUtils.checkIntegerLiteral(\n        posExpression, /* context= */ \"Invalid `pos` argument type for SUBSTRING\", substring);\n    if (childrenSize == 3) {\n      Expression lengthExpression = childrenExpressions.get(2);\n      DefaultExpressionUtils.checkIntegerLiteral(\n          lengthExpression, /* context= */ \"Invalid `len` argument type for SUBSTRING\", substring);\n    }\n    return new ScalarExpression(substring.getName(), childrenExpressions);\n  }\n\n  /**\n   * Evaluates the {@code substring} expression for given input column vector, builds a column\n   * vector with substring applied to each row.\n   */\n  static ColumnVector eval(List<ColumnVector> childrenVectors) {\n    return new ColumnVector() {\n      final ColumnVector input = childrenVectors.get(0);\n      final ColumnVector positionVector = childrenVectors.get(1);\n      final Optional<ColumnVector> lengthVector =\n          childrenVectors.size() > 2 ? Optional.of(childrenVectors.get(2)) : Optional.empty();\n\n      @Override\n      public DataType getDataType() {\n        return StringType.STRING;\n      }\n\n      @Override\n      public int getSize() {\n        return input.getSize();\n      }\n\n      @Override\n      public void close() {\n        // Utils.closeCloseables method will ignore the null element.\n        Utils.closeCloseables(input, positionVector, lengthVector.orElse(null));\n      }\n\n      @Override\n      public boolean isNullAt(int rowId) {\n        if (rowId < 0 || rowId >= getSize()) {\n          throw new IllegalArgumentException(\n              String.format(\n                  \"Unexpected rowId %d, expected between 0 and the size of the column vector\",\n                  rowId));\n        }\n        return input.isNullAt(rowId);\n      }\n\n      @Override\n      public String getString(int rowId) {\n        if (isNullAt(rowId)) {\n          return null;\n        }\n\n        String inputString = input.getString(rowId);\n        int position = positionVector.getInt(rowId);\n        Optional<Integer> length = lengthVector.map(columnVector -> columnVector.getInt(rowId));\n        if (position > getStringLength(inputString) || (length.isPresent() && length.get() < 1)) {\n          return \"\";\n        }\n        int startPosition = buildStartPosition(inputString, position);\n        int startIndex = Math.max(startPosition, 0);\n        return length\n            .map(\n                len -> {\n                  // endIndex should be less than the length of input string, but positive.\n                  // e.g. Substring(\"aaa\", -100, 95), should be read as Substring(\"aaa\", 0, 0)\n                  int endIndex =\n                      Math.min(getStringLength(inputString), Math.max(startPosition + len, 0));\n                  return getSubstring(inputString, startIndex, Optional.of(endIndex));\n                })\n            .orElse(getSubstring(inputString, startIndex, Optional.empty()));\n      }\n    };\n  }\n\n  /**\n   * Computes the start position following Hive and SQL's one-based indexing for substring.\n   *\n   * @param pos, pos can be positive, in which case the startIndex is computed from the left end of\n   *     the string. Otherwise, the startIndex is computed from the right end of the string.\n   * @return the position of inputString to compute the substring. The returned value can fall\n   *     beyond the input index valid range. For example, pos could be larger than the inputString's\n   *     length or inputString.length() + pos could be negative.\n   */\n  private static int buildStartPosition(String inputString, int pos) {\n    // Handles the negative position (substring(\"abc\", -2, 1), the start position should be 1(\"b\"))\n    if (pos < 0) {\n      return getStringLength(inputString) + pos;\n    }\n    // Pos is 1 based and pos = 0 is treated as 1.\n    return Math.max(pos - 1, 0);\n  }\n\n  /** Returns code point based string length for handling surrogate pairs. */\n  private static int getStringLength(String s) {\n    return s.codePointCount(/* beginIndex = */ 0, s.length());\n  }\n\n  /** Returns code point based substring for handling surrogate pairs. */\n  private static String getSubstring(String s, int start, Optional<Integer> end) {\n    int startIndex = s.offsetByCodePoints(/* beginIndex = */ 0, start);\n    return end.map(e -> s.substring(startIndex, s.offsetByCodePoints(0, e)))\n        .orElse(s.substring(startIndex));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/json/JsonUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.internal.json;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.defaults.internal.data.DefaultJsonRow;\nimport io.delta.kernel.types.*;\nimport java.io.IOException;\n\n/**\n * Utilities method to serialize and deserialize {@link Row} objects with a limited set of data type\n * values.\n *\n * <p>Following are the supported data types: {@code boolean}, {@code byte}, {@code short}, {@code\n * int}, {@code long}, {@code float}, {@code double}, {@code string}, {@code StructType} (containing\n * any of the supported subtypes), {@code ArrayType}, {@code MapType} (only a map with string keys\n * is supported).\n *\n * <p>At a high-level, the JSON serialization is similar to that of Jackson's {@link ObjectMapper}.\n */\npublic class JsonUtils {\n  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n\n  static {\n    OBJECT_MAPPER.registerModule(new SimpleModule().addSerializer(Row.class, new RowSerializer()));\n  }\n\n  private JsonUtils() {}\n\n  /**\n   * Converts a {@link Row} to a single line JSON string. This is currently used just in tests. Wll\n   * be used as part of the refactoring planned in <a\n   * href=\"https://github.com/delta-io/delta/issues/2929\">#2929</a>\n   *\n   * @param row the row to convert\n   * @return JSON string\n   */\n  public static String rowToJson(Row row) {\n    try {\n      return OBJECT_MAPPER.writeValueAsString(row);\n    } catch (JsonProcessingException ex) {\n      throw new RuntimeException(\"Could not serialize row object to JSON\", ex);\n    }\n  }\n\n  /**\n   * Converts a JSON string to a {@link Row}.\n   *\n   * @param json JSON string\n   * @param schema to read the JSON according the schema\n   * @return {@link Row} instance with given schema.\n   */\n  public static Row rowFromJson(String json, StructType schema) {\n    try {\n      final JsonNode jsonNode = OBJECT_MAPPER.readTree(json);\n      return new DefaultJsonRow((ObjectNode) jsonNode, schema);\n    } catch (JsonProcessingException ex) {\n      throw new RuntimeException(String.format(\"Could not parse JSON: %s\", json), ex);\n    }\n  }\n\n  public static class RowSerializer extends StdSerializer<Row> {\n    public RowSerializer() {\n      super(Row.class);\n    }\n\n    @Override\n    public void serialize(Row row, JsonGenerator gen, SerializerProvider provider)\n        throws IOException {\n      writeRow(gen, row, row.getSchema());\n    }\n\n    private void writeRow(JsonGenerator gen, Row row, StructType schema) throws IOException {\n      gen.writeStartObject();\n      for (int columnOrdinal = 0; columnOrdinal < schema.length(); columnOrdinal++) {\n        StructField field = schema.at(columnOrdinal);\n        if (!row.isNullAt(columnOrdinal)) {\n          gen.writeFieldName(field.getName());\n          writeValue(gen, row, columnOrdinal, field.getDataType());\n        }\n      }\n      gen.writeEndObject();\n    }\n\n    private void writeStruct(JsonGenerator gen, ColumnVector vector, StructType type, int rowId)\n        throws IOException {\n      gen.writeStartObject();\n      for (int columnOrdinal = 0; columnOrdinal < type.length(); columnOrdinal++) {\n        StructField field = type.at(columnOrdinal);\n        ColumnVector childVector = vector.getChild(columnOrdinal);\n        if (!childVector.isNullAt(rowId)) {\n          gen.writeFieldName(field.getName());\n          writeValue(gen, childVector, rowId, field.getDataType());\n        }\n      }\n      gen.writeEndObject();\n    }\n\n    private void writeArrayValue(JsonGenerator gen, ArrayValue arrayValue, ArrayType arrayType)\n        throws IOException {\n      gen.writeStartArray();\n      ColumnVector arrayElems = arrayValue.getElements();\n      for (int i = 0; i < arrayValue.getSize(); i++) {\n        if (arrayElems.isNullAt(i)) {\n          // Jackson serializes the null values in the array, but not in the map\n          gen.writeNull();\n        } else {\n          writeValue(gen, arrayValue.getElements(), i, arrayType.getElementType());\n        }\n      }\n      gen.writeEndArray();\n    }\n\n    private void writeMapValue(JsonGenerator gen, MapValue mapValue, MapType mapType)\n        throws IOException {\n      assertSupportedMapType(mapType);\n      gen.writeStartObject();\n      ColumnVector keys = mapValue.getKeys();\n      ColumnVector values = mapValue.getValues();\n      for (int i = 0; i < mapValue.getSize(); i++) {\n        gen.writeFieldName(keys.getString(i));\n        if (!values.isNullAt(i)) {\n          writeValue(gen, values, i, mapType.getValueType());\n        } else {\n          gen.writeNull();\n        }\n      }\n      gen.writeEndObject();\n    }\n\n    private void writeValue(JsonGenerator gen, Row row, int columnOrdinal, DataType type)\n        throws IOException {\n      checkArgument(!row.isNullAt(columnOrdinal), \"value should not be null\");\n      if (type instanceof BooleanType) {\n        gen.writeBoolean(row.getBoolean(columnOrdinal));\n      } else if (type instanceof ByteType) {\n        gen.writeNumber(row.getByte(columnOrdinal));\n      } else if (type instanceof ShortType) {\n        gen.writeNumber(row.getShort(columnOrdinal));\n      } else if (type instanceof IntegerType) {\n        gen.writeNumber(row.getInt(columnOrdinal));\n      } else if (type instanceof LongType) {\n        gen.writeNumber(row.getLong(columnOrdinal));\n      } else if (type instanceof FloatType) {\n        gen.writeNumber(row.getFloat(columnOrdinal));\n      } else if (type instanceof DoubleType) {\n        gen.writeNumber(row.getDouble(columnOrdinal));\n      } else if (type instanceof StringType) {\n        gen.writeString(row.getString(columnOrdinal));\n      } else if (type instanceof StructType) {\n        writeRow(gen, row.getStruct(columnOrdinal), (StructType) type);\n      } else if (type instanceof ArrayType) {\n        writeArrayValue(gen, row.getArray(columnOrdinal), (ArrayType) type);\n      } else if (type instanceof MapType) {\n        writeMapValue(gen, row.getMap(columnOrdinal), (MapType) type);\n      } else {\n        // `binary` type is not supported according the Delta Protocol\n        throw new UnsupportedOperationException(\"unsupported data type: \" + type);\n      }\n    }\n\n    private void writeValue(JsonGenerator gen, ColumnVector vector, int rowId, DataType type)\n        throws IOException {\n      checkArgument(!vector.isNullAt(rowId), \"value should not be null\");\n      if (type instanceof BooleanType) {\n        gen.writeBoolean(vector.getBoolean(rowId));\n      } else if (type instanceof ByteType) {\n        gen.writeNumber(vector.getByte(rowId));\n      } else if (type instanceof ShortType) {\n        gen.writeNumber(vector.getShort(rowId));\n      } else if (type instanceof IntegerType) {\n        gen.writeNumber(vector.getInt(rowId));\n      } else if (type instanceof LongType) {\n        gen.writeNumber(vector.getLong(rowId));\n      } else if (type instanceof FloatType) {\n        gen.writeNumber(vector.getFloat(rowId));\n      } else if (type instanceof DoubleType) {\n        gen.writeNumber(vector.getDouble(rowId));\n      } else if (type instanceof StringType) {\n        gen.writeString(vector.getString(rowId));\n      } else if (type instanceof StructType) {\n        writeStruct(gen, vector, (StructType) type, rowId);\n      } else if (type instanceof ArrayType) {\n        writeArrayValue(gen, vector.getArray(rowId), (ArrayType) type);\n      } else if (type instanceof MapType) {\n        writeMapValue(gen, vector.getMap(rowId), (MapType) type);\n      } else {\n        throw new UnsupportedOperationException(\"unsupported data type: \" + type);\n      }\n    }\n  }\n\n  private static void assertSupportedMapType(MapType keyType) {\n    checkArgument(\n        keyType.getKeyType() instanceof StringType,\n        \"Only STRING type keys are supported in MAP type in JSON serialization\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/logstore/LogStoreProvider.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.logstore;\n\nimport static io.delta.kernel.defaults.internal.DefaultEngineErrors.canNotInstantiateLogStore;\n\nimport io.delta.storage.*;\nimport java.util.*;\nimport org.apache.hadoop.conf.Configuration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Utility class to provide the correct {@link LogStore} based on the scheme of the path. */\npublic class LogStoreProvider {\n  private static final Logger logger = LoggerFactory.getLogger(LogStoreProvider.class);\n\n  // Supported schemes per storage system.\n  private static final Set<String> S3_SCHEMES = unmodifiableSet(\"s3\", \"s3a\", \"s3n\");\n  private static final Set<String> AZURE_SCHEMES =\n      unmodifiableSet(\"abfs\", \"abfss\", \"adl\", \"wasb\", \"wasbs\");\n  private static final Set<String> GCS_SCHEMES = unmodifiableSet(\"gs\");\n\n  /**\n   * Get the {@link LogStore} instance for the given scheme and configuration. Callers can set\n   * {@code io.delta.kernel.logStore.<scheme>.impl} to specify the {@link LogStore} implementation\n   * to use for {@code scheme}.\n   *\n   * <p>If not set, the default {@link LogStore} implementation (given below) for the scheme will be\n   * used.\n   *\n   * <ul>\n   *   <li>{@code s3, s3a, s3n}: {@link S3SingleDriverLogStore}\n   *   <li>{@code abfs, abfss, adl, wasb, wasbs}: {@link AzureLogStore}\n   *   <li>{@code gs}: {@link GCSLogStore}\n   *   <li>{@code hdfs, file}: {@link HDFSLogStore}\n   *   <li>remaining: {@link HDFSLogStore}\n   * </ul>\n   *\n   * @param hadoopConf {@link Configuration} to use for creating the LogStore.\n   * @param scheme Scheme of the path.\n   * @return {@link LogStore} instance.\n   * @throws IllegalArgumentException if the LogStore implementation is not found or can not be\n   *     instantiated.\n   */\n  public static LogStore getLogStore(Configuration hadoopConf, String scheme) {\n    String schemeLower = Optional.ofNullable(scheme).map(String::toLowerCase).orElse(null);\n\n    // Check if the LogStore implementation is set in the configuration.\n    String classNameFromConfig = hadoopConf.get(getLogStoreSchemeConfKey(schemeLower));\n    if (classNameFromConfig != null) {\n      return createLogStore(classNameFromConfig, hadoopConf, \"from config\");\n    }\n\n    // Create default LogStore based on the scheme.\n    String defaultClassName = HDFSLogStore.class.getName();\n    if (S3_SCHEMES.contains(schemeLower)) {\n      defaultClassName = S3SingleDriverLogStore.class.getName();\n    } else if (AZURE_SCHEMES.contains(schemeLower)) {\n      defaultClassName = AzureLogStore.class.getName();\n    } else if (GCS_SCHEMES.contains(schemeLower)) {\n      defaultClassName = GCSLogStore.class.getName();\n    }\n\n    return createLogStore(defaultClassName, hadoopConf, \"(default for file scheme)\");\n  }\n\n  /**\n   * Configuration key for setting the LogStore implementation for a scheme. ex:\n   * `io.delta.kernel.logStore.s3.impl` -> `io.delta.storage.S3SingleDriverLogStore`\n   */\n  static String getLogStoreSchemeConfKey(String scheme) {\n    return \"io.delta.kernel.logStore.\" + scheme + \".impl\";\n  }\n\n  /** Utility method to get the LogStore class from the class name. */\n  private static Class<? extends LogStore> getLogStoreClass(String logStoreClassName)\n      throws ClassNotFoundException {\n    return Class.forName(logStoreClassName).asSubclass(LogStore.class);\n  }\n\n  private static LogStore createLogStore(\n      String className, Configuration hadoopConf, String context) {\n    try {\n      return getLogStoreClass(className)\n          .getConstructor(Configuration.class)\n          .newInstance(hadoopConf);\n    } catch (Exception e) {\n      String msgTemplate = \"Failed to instantiate LogStore class ({}): {}\";\n      logger.error(msgTemplate, context, className, e);\n      throw canNotInstantiateLogStore(className, context, e);\n    }\n  }\n\n  /** Remove this method once we start supporting JDK9+ */\n  private static Set<String> unmodifiableSet(String... elements) {\n    return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(elements)));\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ArrayColumnReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetColumnReaders.createConverter;\nimport static io.delta.kernel.defaults.internal.parquet.ParquetSchemaUtils.validateAndGetThreeLevelParquetArrayElementType;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultArrayVector;\nimport io.delta.kernel.types.ArrayType;\nimport java.util.Optional;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.schema.GroupType;\nimport org.apache.parquet.schema.Type;\n\n/**\n * Array column reader for materializing the column values from Parquet files into Kernels {@link\n * ColumnVector}.\n */\nclass ArrayColumnReader extends RepeatedValueConverter {\n  private final ArrayType typeFromClient;\n\n  ArrayColumnReader(int initialBatchSize, ArrayType typeFromClient, GroupType typeFromFile) {\n    super(initialBatchSize, createElementConverter(initialBatchSize, typeFromClient, typeFromFile));\n    this.typeFromClient = typeFromClient;\n  }\n\n  @Override\n  public ColumnVector getDataColumnVector(int batchSize) {\n    ColumnVector arrayVector =\n        new DefaultArrayVector(\n            batchSize,\n            typeFromClient,\n            Optional.of(getNullability()),\n            getOffsets(),\n            getElementDataVectors()[0]);\n    resetWorkingState();\n    return arrayVector;\n  }\n\n  /**\n   * Currently, support for 3-level nested arrays only.\n   *\n   * <p>optional group readerFeatures (LIST) { repeated group list { optional binary element\n   * (STRING); } }\n   *\n   * <p>optional group readerFeatures (LIST) { repeated group bag { optional binary array (STRING);\n   * } }\n   *\n   * <p>TODO: Add support for 2-level nested arrays.\n   */\n  private static Converter createElementConverter(\n      int initialBatchSize, ArrayType typeFromClient, GroupType typeFromFile) {\n\n    Type elementType = validateAndGetThreeLevelParquetArrayElementType(typeFromFile);\n\n    return createConverter(initialBatchSize, typeFromClient.getElementType(), elementType);\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/DecimalColumnReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.BINARY;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT64;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultDecimalVector;\nimport io.delta.kernel.defaults.internal.parquet.ParquetColumnReaders.BasePrimitiveColumnReader;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.DecimalType;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.math.RoundingMode;\nimport java.util.Arrays;\nimport org.apache.parquet.column.Dictionary;\nimport org.apache.parquet.io.api.Binary;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.schema.LogicalTypeAnnotation;\nimport org.apache.parquet.schema.PrimitiveType;\nimport org.apache.parquet.schema.Type;\n\n/**\n * Decimal column readers for materializing the column values from Parquet files into Kernels {@link\n * ColumnVector}.\n */\npublic class DecimalColumnReader {\n\n  public static Converter createDecimalConverter(\n      int initialBatchSize, DecimalType typeFromClient, Type typeFromFile) {\n\n    PrimitiveType primType = typeFromFile.asPrimitiveType();\n    LogicalTypeAnnotation typeAnnotation = primType.getLogicalTypeAnnotation();\n\n    if (primType.getPrimitiveTypeName() == INT32) {\n      // For INT32 backed decimals\n      if (typeAnnotation instanceof LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) {\n        LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalType =\n            (LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) typeAnnotation;\n        return new IntDictionaryAwareDecimalColumnReader(\n            typeFromClient, decimalType.getPrecision(), decimalType.getScale(), initialBatchSize);\n      } else {\n        // If the column is a plain INT32, we should pick the precision that can host\n        // the largest INT32 value.\n        return new IntDictionaryAwareDecimalColumnReader(typeFromClient, 10, 0, initialBatchSize);\n      }\n    } else if (primType.getPrimitiveTypeName() == INT64) {\n      // For INT64 backed decimals\n      if (typeAnnotation instanceof LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) {\n        LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalType =\n            (LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) typeAnnotation;\n        return new LongDictionaryAwareDecimalColumnReader(\n            typeFromClient, decimalType.getPrecision(), decimalType.getScale(), initialBatchSize);\n      } else {\n        // If the column is a plain INT64, we should pick the precision that can host\n        // the largest INT64 value.\n        return new LongDictionaryAwareDecimalColumnReader(typeFromClient, 20, 0, initialBatchSize);\n      }\n    } else if (primType.getPrimitiveTypeName() == FIXED_LEN_BYTE_ARRAY\n        || primType.getPrimitiveTypeName() == BINARY) {\n      // For BINARY and FIXED_LEN_BYTE_ARRAY backed decimals\n      if (typeAnnotation instanceof LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) {\n        LogicalTypeAnnotation.DecimalLogicalTypeAnnotation decimalType =\n            (LogicalTypeAnnotation.DecimalLogicalTypeAnnotation) typeAnnotation;\n        return new BinaryDictionaryAwareDecimalColumnReader(\n            typeFromClient, decimalType.getPrecision(), decimalType.getScale(), initialBatchSize);\n      } else {\n        throw new RuntimeException(\n            String.format(\n                \"Unable to create Parquet converter for DecimalType whose parquet \"\n                    + \"type is %s without decimal metadata.\",\n                typeFromFile));\n      }\n    } else {\n      throw new RuntimeException(\n          String.format(\n              \"Unable to create Parquet converter for DecimalType whose Parquet type \"\n                  + \"is %s. Parquet DECIMAL type can only be backed by INT32, INT64, \"\n                  + \"FIXED_LEN_BYTE_ARRAY, or BINARY\",\n              typeFromFile));\n    }\n  }\n\n  public abstract static class BaseDecimalColumnReader extends BasePrimitiveColumnReader {\n    // working state\n    private BigDecimal[] values;\n\n    private final DecimalType decimalType;\n\n    private final int scale;\n    protected BigDecimal[] expandedDictionary;\n\n    BaseDecimalColumnReader(DataType dataType, int precision, int scale, int initialBatchSize) {\n      super(initialBatchSize);\n      DecimalType decimalType = (DecimalType) dataType;\n      int scaleIncrease = decimalType.getScale() - scale;\n      int precisionIncrease = decimalType.getPrecision() - precision;\n      checkArgument(\n          scaleIncrease >= 0 && precisionIncrease >= scaleIncrease,\n          \"Found Delta type %s but Parquet type has precision=%s and scale=%s\",\n          decimalType,\n          precision,\n          scale);\n      this.scale = scale;\n      this.decimalType = decimalType;\n      this.values = new BigDecimal[initialBatchSize];\n    }\n\n    /**\n     * Dictionary support is an optional optimization to reduce BigDecimal instantiation and binary\n     * decoding (for Binary backed decimals).\n     */\n    @Override\n    public boolean hasDictionarySupport() {\n      return true;\n    }\n\n    protected void addDecimal(BigDecimal value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      if (decimalType.getScale() != scale) {\n        value = value.setScale(decimalType.getScale(), RoundingMode.UNNECESSARY);\n      }\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public void addValueFromDictionary(int dictionaryId) {\n      addDecimal(expandedDictionary[dictionaryId]);\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultDecimalVector(decimalType, batchSize, values);\n      // re-initialize the working space\n      this.nullability = ParquetColumnReaders.initNullabilityVector(nullability.length);\n      this.values = new BigDecimal[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        ParquetColumnReaders.setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n\n    protected BigDecimal decimalFromLong(long value) {\n      return BigDecimal.valueOf(value, scale);\n    }\n\n    protected BigDecimal decimalFromBinary(Binary value) {\n      return new BigDecimal(new BigInteger(value.getBytes()), scale);\n    }\n  }\n\n  public static class IntDictionaryAwareDecimalColumnReader extends BaseDecimalColumnReader {\n    IntDictionaryAwareDecimalColumnReader(\n        DataType dataType, int precision, int scale, int initialBatchSize) {\n      super(dataType, precision, scale, initialBatchSize);\n    }\n\n    @Override\n    public void setDictionary(Dictionary dictionary) {\n      this.expandedDictionary = new BigDecimal[dictionary.getMaxId() + 1];\n      for (int id = 0; id < dictionary.getMaxId() + 1; id++) {\n        this.expandedDictionary[id] = decimalFromLong(dictionary.decodeToInt(id));\n      }\n    }\n\n    @Override\n    // Converts decimals stored as INT32\n    public void addInt(int value) {\n      addDecimal(decimalFromLong(value));\n    }\n  }\n\n  public static class LongDictionaryAwareDecimalColumnReader extends BaseDecimalColumnReader {\n    LongDictionaryAwareDecimalColumnReader(\n        DataType dataType, int precision, int scale, int initialBatchSize) {\n      super(dataType, precision, scale, initialBatchSize);\n    }\n\n    @Override\n    public void setDictionary(Dictionary dictionary) {\n      this.expandedDictionary = new BigDecimal[dictionary.getMaxId() + 1];\n      for (int id = 0; id < dictionary.getMaxId() + 1; id++) {\n        this.expandedDictionary[id] = decimalFromLong(dictionary.decodeToLong(id));\n      }\n    }\n\n    @Override\n    // Converts decimals stored as INT64\n    public void addLong(long value) {\n      addDecimal(decimalFromLong(value));\n    }\n  }\n\n  public static class BinaryDictionaryAwareDecimalColumnReader extends BaseDecimalColumnReader {\n    BinaryDictionaryAwareDecimalColumnReader(\n        DataType dataType, int precision, int scale, int initialBatchSize) {\n      super(dataType, precision, scale, initialBatchSize);\n    }\n\n    @Override\n    public void setDictionary(Dictionary dictionary) {\n      this.expandedDictionary = new BigDecimal[dictionary.getMaxId() + 1];\n      for (int id = 0; id < dictionary.getMaxId() + 1; id++) {\n        this.expandedDictionary[id] = decimalFromBinary(dictionary.decodeToBinary(id));\n      }\n    }\n\n    @Override\n    // Converts decimals stored as either FIXED_LENGTH_BYTE_ARRAY or BINARY\n    public void addBinary(Binary value) {\n      addDecimal(decimalFromBinary(value));\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/MapColumnReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetColumnReaders.createConverter;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultMapVector;\nimport io.delta.kernel.types.MapType;\nimport java.util.Optional;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.schema.GroupType;\n\n/**\n * Map column readers for materializing the column values from Parquet files into Kernels {@link\n * ColumnVector}.\n */\nclass MapColumnReader extends RepeatedValueConverter {\n  private final MapType typeFromClient;\n\n  MapColumnReader(int initialBatchSize, MapType typeFromClient, GroupType typeFromFile) {\n    super(\n        initialBatchSize, createElementConverters(initialBatchSize, typeFromClient, typeFromFile));\n    this.typeFromClient = typeFromClient;\n  }\n\n  @Override\n  public ColumnVector getDataColumnVector(int batchSize) {\n    ColumnVector[] elementVectors = getElementDataVectors();\n    ColumnVector mapVector =\n        new DefaultMapVector(\n            batchSize,\n            typeFromClient,\n            Optional.of(getNullability()),\n            getOffsets(),\n            elementVectors[0],\n            elementVectors[1]);\n    resetWorkingState();\n    return mapVector;\n  }\n\n  private static Converter[] createElementConverters(\n      int initialBatchSize, MapType typeFromClient, GroupType typeFromFile) {\n    // Repeated element can be any name. Latest Parquet versions use \"key_value\" as the name,\n    // but legacy versions can use any arbitrary name for the repeated group.\n    // See https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#maps for details\n    checkArgument(\n        typeFromFile.getFieldCount() == 1,\n        \"Expected exactly one repeated field in the map type, but got: %s\",\n        typeFromFile);\n\n    GroupType innerMapType = typeFromFile.getType(0).asGroupType();\n    Converter[] elemConverters = new Converter[2];\n    elemConverters[0] =\n        createConverter(initialBatchSize, typeFromClient.getKeyType(), innerMapType.getType(\"key\"));\n    elemConverters[1] =\n        createConverter(\n            initialBatchSize, typeFromClient.getValueType(), innerMapType.getType(\"value\"));\n\n    return elemConverters;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetColumnReaders.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.TimestampConverters.createTimestampConverter;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.data.vector.*;\nimport io.delta.kernel.types.*;\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.Optional;\nimport org.apache.parquet.io.api.Binary;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.io.api.PrimitiveConverter;\nimport org.apache.parquet.schema.GroupType;\nimport org.apache.parquet.schema.Type;\n\n/**\n * Parquet column readers for materializing the column values from Parquet files into Kernels {@link\n * ColumnVector}.\n */\nclass ParquetColumnReaders {\n  public static Converter createConverter(\n      int initialBatchSize, DataType typeFromClient, Type typeFromFile) {\n    if (typeFromClient instanceof StructType) {\n      checkArgument(typeFromFile instanceof GroupType, \"cannot be cast to GroupType\");\n      return new RowColumnReader(\n          initialBatchSize, (StructType) typeFromClient, (GroupType) typeFromFile);\n    } else if (typeFromClient instanceof ArrayType) {\n      checkArgument(typeFromFile instanceof GroupType, \"cannot be cast to GroupType\");\n      return new ArrayColumnReader(\n          initialBatchSize, (ArrayType) typeFromClient, (GroupType) typeFromFile);\n    } else if (typeFromClient instanceof MapType) {\n      checkArgument(typeFromFile instanceof GroupType, \"cannot be cast to GroupType\");\n      return new MapColumnReader(\n          initialBatchSize, (MapType) typeFromClient, (GroupType) typeFromFile);\n    } else if (typeFromClient instanceof StringType || typeFromClient instanceof BinaryType) {\n      return new BinaryColumnReader(typeFromClient, initialBatchSize);\n    } else if (typeFromClient instanceof BooleanType) {\n      return new BooleanColumnReader(initialBatchSize);\n    } else if (typeFromClient instanceof IntegerType || typeFromClient instanceof DateType) {\n      return new IntColumnReader(typeFromClient, initialBatchSize);\n    } else if (typeFromClient instanceof ByteType) {\n      return new ByteColumnReader(initialBatchSize);\n    } else if (typeFromClient instanceof ShortType) {\n      return new ShortColumnReader(initialBatchSize);\n    } else if (typeFromClient instanceof LongType) {\n      return new LongColumnReader(typeFromClient, initialBatchSize);\n    } else if (typeFromClient instanceof FloatType) {\n      return new FloatColumnReader(initialBatchSize);\n    } else if (typeFromClient instanceof DoubleType) {\n      return new DoubleColumnReader(initialBatchSize);\n    } else if (typeFromClient instanceof DecimalType) {\n      return DecimalColumnReader.createDecimalConverter(\n          initialBatchSize, (DecimalType) typeFromClient, typeFromFile);\n    } else if (typeFromClient instanceof TimestampType) {\n      return createTimestampConverter(initialBatchSize, typeFromFile, TimestampType.TIMESTAMP);\n    } else if (typeFromClient instanceof TimestampNTZType) {\n      return createTimestampConverter(\n          initialBatchSize, typeFromFile, TimestampNTZType.TIMESTAMP_NTZ);\n    }\n\n    throw new UnsupportedOperationException(typeFromClient + \" is not supported\");\n  }\n\n  static boolean[] initNullabilityVector(int size) {\n    boolean[] nullability = new boolean[size];\n    // Initialize all values as null. As Parquet calls this converter only for non-null\n    // values, make the corresponding value to false.\n    Arrays.fill(nullability, true);\n\n    return nullability;\n  }\n\n  static void setNullabilityToTrue(boolean[] nullability, int start, int end) {\n    // Initialize all values as null. As Parquet calls this converter only for non-null\n    // values, make the corresponding value to false.\n    Arrays.fill(nullability, start, end, true);\n  }\n\n  /**\n   * Base column reader for all implementations of Parquet {@link Converter} to return data in\n   * columnar batch. General operation flow is: - each reader implementation allocates state to\n   * receive a fixed number of column values - before accepting a new value the state is resized if\n   * it is not of sufficient size - after each row, {@link #finalizeCurrentRow(long)} is called to\n   * finalize the state of the last read row column value.\n   */\n  public interface BaseColumnReader {\n    ColumnVector getDataColumnVector(int batchSize);\n\n    /**\n     * Finalize the current row: - close the state of the row that was read most recently. -\n     * reallocate the state to be of sufficient size for the current batch size. Generally the state\n     * value arrays are resized as part of setting the value, but method doesn't get called for null\n     * values which results in the state value arrays are not sufficient size for the current batch\n     * size.\n     *\n     * @param currentRowIndex Row index of the current row in the Parquet file.\n     */\n    void finalizeCurrentRow(long currentRowIndex);\n\n    default void resizeIfNeeded() {}\n\n    default void resetWorkingState() {}\n  }\n\n  public static class NonExistentColumnReader extends PrimitiveConverter\n      implements BaseColumnReader {\n    private final DataType dataType;\n\n    NonExistentColumnReader(DataType dataType) {\n      this.dataType = Objects.requireNonNull(dataType, \"dataType is null\");\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      return new DefaultConstantVector(dataType, batchSize, null);\n    }\n\n    @Override\n    public void finalizeCurrentRow(long currentRowIndex) {}\n  }\n\n  public abstract static class BasePrimitiveColumnReader extends PrimitiveConverter\n      implements BaseColumnReader {\n    // working state\n    protected int currentRowIndex;\n    protected boolean[] nullability;\n\n    BasePrimitiveColumnReader(int initialBatchSize) {\n      checkArgument(initialBatchSize > 0, \"invalid initialBatchSize: %s\", initialBatchSize);\n      // Initialize the working state\n      this.nullability = initNullabilityVector(initialBatchSize);\n    }\n\n    @Override\n    public void finalizeCurrentRow(long currentRowIndex) {\n      resizeIfNeeded();\n      this.currentRowIndex++;\n    }\n  }\n\n  public static class BooleanColumnReader extends BasePrimitiveColumnReader {\n    // working state\n    private boolean[] values;\n\n    BooleanColumnReader(int initialBatchSize) {\n      super(initialBatchSize);\n      this.values = new boolean[initialBatchSize];\n    }\n\n    @Override\n    public void addBoolean(boolean value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultBooleanVector(batchSize, Optional.of(nullability), values);\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new boolean[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class ByteColumnReader extends BasePrimitiveColumnReader {\n    // working state\n    private byte[] values;\n\n    ByteColumnReader(int initialBatchSize) {\n      super(initialBatchSize);\n      this.values = new byte[initialBatchSize];\n    }\n\n    @Override\n    public void addInt(int value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = (byte) value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultByteVector(batchSize, Optional.of(nullability), values);\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new byte[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class ShortColumnReader extends BasePrimitiveColumnReader {\n    // working state\n    private short[] values;\n\n    ShortColumnReader(int initialBatchSize) {\n      super(initialBatchSize);\n      this.values = new short[initialBatchSize];\n    }\n\n    @Override\n    public void addInt(int value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = (short) value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultShortVector(batchSize, Optional.of(nullability), values);\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new short[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class IntColumnReader extends BasePrimitiveColumnReader {\n    private final DataType dataType;\n    // working state\n    private int[] values;\n\n    IntColumnReader(DataType dataType, int initialBatchSize) {\n      super(initialBatchSize);\n      checkArgument(dataType instanceof IntegerType || dataType instanceof DateType);\n      this.dataType = dataType;\n      this.values = new int[initialBatchSize];\n    }\n\n    @Override\n    public void addInt(int value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector =\n          new DefaultIntVector(dataType, batchSize, Optional.of(nullability), values);\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new int[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class LongColumnReader extends BasePrimitiveColumnReader {\n    private final DataType dataType;\n    // working state\n    private long[] values;\n\n    LongColumnReader(DataType dataType, int initialBatchSize) {\n      super(initialBatchSize);\n      checkArgument(\n          dataType instanceof LongType\n              || dataType instanceof TimestampType\n              || dataType instanceof TimestampNTZType);\n      this.dataType = dataType;\n      this.values = new long[initialBatchSize];\n    }\n\n    @Override\n    public void addInt(int value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public void addLong(long value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector =\n          new DefaultLongVector(dataType, batchSize, Optional.of(nullability), values);\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new long[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class FloatColumnReader extends BasePrimitiveColumnReader {\n    // working state\n    private float[] values;\n\n    FloatColumnReader(int initialBatchSize) {\n      super(initialBatchSize);\n      this.values = new float[initialBatchSize];\n    }\n\n    @Override\n    public void addFloat(float value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultFloatVector(batchSize, Optional.of(nullability), values);\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new float[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class DoubleColumnReader extends BasePrimitiveColumnReader {\n    // working state\n    private double[] values;\n\n    DoubleColumnReader(int initialBatchSize) {\n      super(initialBatchSize);\n      this.values = new double[initialBatchSize];\n    }\n\n    @Override\n    public void addInt(int value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public void addFloat(float value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public void addDouble(double value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value;\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultDoubleVector(batchSize, Optional.of(nullability), values);\n      // re-initialize the working space\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new double[values.length];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class BinaryColumnReader extends BasePrimitiveColumnReader {\n    private final DataType dataType;\n\n    // working state\n    private byte[][] values;\n\n    BinaryColumnReader(DataType dataType, int initialBatchSize) {\n      super(initialBatchSize);\n      this.dataType = dataType;\n      this.values = new byte[initialBatchSize][];\n    }\n\n    @Override\n    public void addBinary(Binary value) {\n      resizeIfNeeded();\n      this.nullability[currentRowIndex] = false;\n      this.values[currentRowIndex] = value.getBytes();\n    }\n\n    @Override\n    public ColumnVector getDataColumnVector(int batchSize) {\n      ColumnVector vector = new DefaultBinaryVector(dataType, batchSize, values);\n      // re-initialize the working space\n      this.nullability = initNullabilityVector(nullability.length);\n      this.values = new byte[values.length][];\n      this.currentRowIndex = 0;\n      return vector;\n    }\n\n    @Override\n    public void resizeIfNeeded() {\n      if (values.length == currentRowIndex) {\n        int newSize = values.length * 2;\n        this.values = Arrays.copyOf(this.values, newSize);\n        this.nullability = Arrays.copyOf(this.nullability, newSize);\n        setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n      }\n    }\n  }\n\n  public static class FileRowIndexColumnReader extends LongColumnReader {\n    FileRowIndexColumnReader(int initialBatchSize) {\n      super(LongType.LONG, initialBatchSize);\n    }\n\n    @Override\n    public void addLong(long value) {\n      throw new UnsupportedOperationException(\"cannot add long to metadata column\");\n    }\n\n    @Override\n    public void finalizeCurrentRow(long currentRowIndex) {\n      // Set the previous row index value as the value\n      super.addLong(currentRowIndex);\n      super.finalizeCurrentRow(currentRowIndex);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetColumnWriters.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetSchemaUtils.MAX_BYTES_PER_PRECISION;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport org.apache.parquet.io.api.Binary;\nimport org.apache.parquet.io.api.RecordConsumer;\n\n/**\n * Parquet column writers for writing columnar vectors to Parquet files using the {@link\n * RecordConsumer} interface.\n */\nclass ParquetColumnWriters {\n  private ParquetColumnWriters() {}\n\n  /**\n   * Create column vector writers for the given columnar batch.\n   *\n   * @param batch the columnar batch\n   * @return an array of column vector writers\n   */\n  static ColumnWriter[] createColumnVectorWriters(ColumnarBatch batch) {\n    requireNonNull(batch, \"batch is null\");\n\n    StructType schema = batch.getSchema();\n    ColumnVector[] columnVectors = new ColumnVector[schema.length()];\n    for (int fieldIndex = 0; fieldIndex < schema.length(); fieldIndex++) {\n      columnVectors[fieldIndex] = batch.getColumnVector(fieldIndex);\n    }\n\n    return createColumnVectorWritersHelper(schema, columnVectors);\n  }\n\n  /**\n   * Create column vector writers for the given struct column vector. TODO: Having the ColumnarBatch\n   * as separate interface complicates the code. ColumnarBatch is also a ColumnVector of type\n   * STRUCT.\n   *\n   * @param structColumnVector the column vector\n   * @return an array of column vector writers\n   */\n  static ColumnWriter[] createColumnVectorWriters(ColumnVector structColumnVector) {\n    requireNonNull(structColumnVector, \"batch is null\");\n    checkArgument(\n        structColumnVector.getDataType() instanceof StructType,\n        \"ColumnVector is not a struct type\");\n\n    StructType schema = (StructType) structColumnVector.getDataType();\n    ColumnVector[] columnVectors = new ColumnVector[schema.length()];\n    for (int fieldIndex = 0; fieldIndex < schema.length(); fieldIndex++) {\n      columnVectors[fieldIndex] = structColumnVector.getChild(fieldIndex);\n    }\n\n    return createColumnVectorWritersHelper(schema, columnVectors);\n  }\n\n  private static ColumnWriter[] createColumnVectorWritersHelper(\n      StructType schema, ColumnVector[] columnVectors) {\n    int numCols = schema.length();\n    checkArgument(\n        numCols == columnVectors.length,\n        \"Number of columns in schema does not match number of column vectors\");\n\n    ColumnWriter[] columnWriters = new ColumnWriter[numCols];\n    for (int fieldIndex = 0; fieldIndex < numCols; fieldIndex++) {\n      String colName = schema.at(fieldIndex).getName();\n      ColumnVector columnVector = columnVectors[fieldIndex];\n      columnWriters[fieldIndex] = createColumnWriter(colName, fieldIndex, columnVector);\n    }\n    return columnWriters;\n  }\n\n  private static ColumnWriter createColumnWriter(\n      String colName, int fieldIndex, ColumnVector columnVector) {\n    DataType dataType = columnVector.getDataType();\n\n    if (dataType instanceof BooleanType) {\n      return new BooleanWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof ByteType) {\n      return new ByteWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof ShortType) {\n      return new ShortWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof IntegerType) {\n      return new IntWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof LongType) {\n      return new LongWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof FloatType) {\n      return new FloatWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof DoubleType) {\n      return new DoubleWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof StringType) {\n      return new StringWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof BinaryType) {\n      return new BinaryWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof DecimalType) {\n      int precision = ((DecimalType) dataType).getPrecision();\n      if (precision <= ParquetSchemaUtils.DECIMAL_MAX_DIGITS_IN_INT) {\n        return new DecimalIntWriter(colName, fieldIndex, columnVector);\n      } else if (precision <= ParquetSchemaUtils.DECIMAL_MAX_DIGITS_IN_LONG) {\n        return new DecimalLongWriter(colName, fieldIndex, columnVector);\n      }\n      // TODO: Need to support legacy mode where all decimals are written as binary\n      return new DecimalFixedBinaryWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof DateType) {\n      return new DateWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof TimestampType || dataType instanceof TimestampNTZType) {\n      // for both get the input as long type from column vector and write to file as INT64\n      return new TimestampWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof ArrayType) {\n      return new ArrayWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof MapType) {\n      return new MapWriter(colName, fieldIndex, columnVector);\n    } else if (dataType instanceof StructType) {\n      return new StructWriter(colName, fieldIndex, columnVector);\n    }\n\n    throw new IllegalArgumentException(\"Unsupported column vector type: \" + dataType);\n  }\n\n  /**\n   * Base class for column writers. Handles the common stuff such as null check, start/stop of field\n   * and delegating the actual writing of non-null values to the subclass.\n   */\n  abstract static class ColumnWriter {\n    protected final String colName;\n    protected final int fieldIndex;\n    protected final ColumnVector columnVector;\n\n    ColumnWriter(String colName, int fieldIndex, ColumnVector columnVector) {\n      this.colName = colName;\n      this.fieldIndex = fieldIndex;\n      this.columnVector = columnVector;\n    }\n\n    void writeRowValue(RecordConsumer recordConsumer, int rowId) {\n      if (!columnVector.isNullAt(rowId)) {\n        recordConsumer.startField(colName, fieldIndex);\n        writeNonNullRowValue(recordConsumer, rowId);\n        recordConsumer.endField(colName, fieldIndex);\n      }\n    }\n\n    /**\n     * Each specific column writer for data type, will implement to call appropriate methods on the\n     * {@link RecordConsumer} to write the non-null value.\n     */\n    abstract void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId);\n  }\n\n  static class BooleanWriter extends ColumnWriter {\n    BooleanWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addBoolean(columnVector.getBoolean(rowId));\n    }\n  }\n\n  static class ByteWriter extends ColumnWriter {\n    ByteWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addInteger(columnVector.getByte(rowId));\n    }\n  }\n\n  static class ShortWriter extends ColumnWriter {\n    ShortWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addInteger(columnVector.getShort(rowId));\n    }\n  }\n\n  static class IntWriter extends ColumnWriter {\n    IntWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addInteger(columnVector.getInt(rowId));\n    }\n  }\n\n  static class LongWriter extends ColumnWriter {\n    LongWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addLong(columnVector.getLong(rowId));\n    }\n  }\n\n  static class FloatWriter extends ColumnWriter {\n    FloatWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addFloat(columnVector.getFloat(rowId));\n    }\n  }\n\n  static class DoubleWriter extends ColumnWriter {\n    DoubleWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.addDouble(columnVector.getDouble(rowId));\n    }\n  }\n\n  static class DecimalIntWriter extends ColumnWriter {\n    private final int scale;\n\n    DecimalIntWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n      this.scale = ((DecimalType) columnVector.getDataType()).getScale();\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      BigDecimal decimal = columnVector.getDecimal(rowId).movePointRight(scale);\n      recordConsumer.addInteger(decimal.intValue());\n    }\n  }\n\n  static class DecimalLongWriter extends ColumnWriter {\n    private final int scale;\n\n    DecimalLongWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n      this.scale = ((DecimalType) columnVector.getDataType()).getScale();\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      BigDecimal decimal = columnVector.getDecimal(rowId).movePointRight(scale);\n      recordConsumer.addLong(decimal.longValue());\n    }\n  }\n\n  static class DecimalFixedBinaryWriter extends ColumnWriter {\n    private final int precision;\n    private final int scale;\n    private final int numBytes;\n    private final byte[] reusedBuffer;\n\n    DecimalFixedBinaryWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n      DecimalType decimalType = (DecimalType) columnVector.getDataType();\n      this.precision = decimalType.getPrecision();\n      this.scale = decimalType.getScale();\n      this.numBytes = MAX_BYTES_PER_PRECISION.get(precision);\n      this.reusedBuffer = new byte[numBytes];\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      byte[] bytes = columnVector.getDecimal(rowId).unscaledValue().toByteArray();\n\n      Binary binary;\n      if (bytes.length == numBytes) {\n        // If the length of the underlying byte array of the unscaled `BigInteger`\n        // happens to be `numBytes`, just reuse it, so that we don't bother\n        // copying it to `reusedBuffer`.\n        binary = Binary.fromReusedByteArray(bytes);\n      } else {\n        // Otherwise, the length must be less than `numBytes`.  In this case we copy\n        // contents of the underlying bytes with padding sign bytes to `decimalBuffer`\n        // to form the result fixed-length byte array.\n        byte signByte = (bytes[0] < 0) ? (byte) -1 : (byte) 0;\n        Arrays.fill(reusedBuffer, 0, numBytes - bytes.length, signByte);\n        System.arraycopy(bytes, 0, reusedBuffer, numBytes - bytes.length, bytes.length);\n        binary = Binary.fromReusedByteArray(reusedBuffer);\n      }\n\n      recordConsumer.addBinary(binary);\n    }\n  }\n\n  static class DateWriter extends ColumnWriter {\n    DateWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      // TODO: Spark has various handling mode for DateType, need to check if it is needed\n      // for Delta Kernel.\n      recordConsumer.addInteger(columnVector.getInt(rowId)); // dates are stores as epoch days\n    }\n  }\n\n  /** Writer for both timestamp and timestamp with time zone. */\n  static class TimestampWriter extends ColumnWriter {\n    TimestampWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      long microsSinceEpochUTC = columnVector.getLong(rowId);\n      recordConsumer.addLong(microsSinceEpochUTC);\n    }\n  }\n\n  static class StringWriter extends ColumnWriter {\n    StringWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      Binary binary =\n          Binary.fromConstantByteArray(\n              columnVector.getString(rowId).getBytes(StandardCharsets.UTF_8));\n      recordConsumer.addBinary(binary);\n    }\n  }\n\n  static class BinaryWriter extends ColumnWriter {\n    BinaryWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      Binary binary = Binary.fromConstantByteArray(columnVector.getBinary(rowId));\n      recordConsumer.addBinary(binary);\n    }\n  }\n\n  static class ArrayWriter extends ColumnWriter {\n    ArrayWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      // Write as 3-level representation. Later on, depending upon the config,\n      // we can write either as 2-level or 3-level representation.\n      recordConsumer.startGroup();\n      ArrayValue arrayValue = columnVector.getArray(rowId);\n      if (arrayValue.getSize() > 0) {\n        // Use the fieldIndex as zero. Once we support Uniform compatible Parquet files,\n        // the field index will come from the Delta schema.\n        recordConsumer.startField(\"list\", 0 /* fieldIndex */);\n        ColumnVector elementVector = arrayValue.getElements();\n        ColumnWriter elementWriter =\n            createColumnWriter(\"element\", 0 /* fieldIndex */, elementVector);\n        for (int i = 0; i < arrayValue.getSize(); i++) {\n          recordConsumer.startGroup();\n          if (!elementVector.isNullAt(i)) {\n            elementWriter.writeRowValue(recordConsumer, i);\n          }\n          recordConsumer.endGroup();\n        }\n        recordConsumer.endField(\"list\", 0 /* fieldIndex */);\n      }\n      recordConsumer.endGroup();\n    }\n  }\n\n  static class MapWriter extends ColumnWriter {\n    MapWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      // Write as 3-level representation. Later on, depending upon the config,\n      // we can write either as 2-level or 3-level representation.\n      recordConsumer.startGroup();\n\n      MapValue mapValue = columnVector.getMap(rowId);\n      if (mapValue.getSize() > 0) {\n        recordConsumer.startField(\"key_value\", 0 /* fieldIndex */);\n\n        // Use the fieldIndex as zero. Once we support Uniform compatible Parquet files,\n        // the field index will come from the Delta schema.\n        ColumnVector keyVector = mapValue.getKeys();\n        ColumnWriter keyWriter = createColumnWriter(\"key\", 0 /* fieldIndex */, keyVector);\n        ColumnVector valueVector = mapValue.getValues();\n        ColumnWriter valueWriter = createColumnWriter(\"value\", 1 /* fieldIndex */, valueVector);\n\n        for (int i = 0; i < mapValue.getSize(); i++) {\n          recordConsumer.startGroup();\n          keyWriter.writeRowValue(recordConsumer, i);\n          if (!valueVector.isNullAt(i)) {\n            valueWriter.writeRowValue(recordConsumer, i);\n          }\n          recordConsumer.endGroup();\n        }\n\n        recordConsumer.endField(\"key_value\", 0 /* fieldIndex */);\n      }\n      recordConsumer.endGroup();\n    }\n  }\n\n  static class StructWriter extends ColumnWriter {\n    private final ColumnWriter[] fieldWriters;\n\n    StructWriter(String name, int fieldId, ColumnVector columnVector) {\n      super(name, fieldId, columnVector);\n      fieldWriters = createColumnVectorWriters(columnVector);\n    }\n\n    @Override\n    void writeNonNullRowValue(RecordConsumer recordConsumer, int rowId) {\n      recordConsumer.startGroup();\n      for (ColumnWriter fieldWriter : fieldWriters) {\n        fieldWriter.writeRowValue(recordConsumer, rowId);\n      }\n      recordConsumer.endGroup();\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetFileReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetFilterUtils.toParquetFilter;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.fileio.InputFile;\nimport io.delta.kernel.exceptions.KernelEngineException;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.types.MetadataColumnSpec;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport java.io.IOException;\nimport java.util.*;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.parquet.filter2.compat.FilterCompat;\nimport org.apache.parquet.filter2.predicate.FilterPredicate;\nimport org.apache.parquet.format.converter.ParquetMetadataConverter;\nimport org.apache.parquet.hadoop.ParquetReader;\nimport org.apache.parquet.hadoop.api.InitContext;\nimport org.apache.parquet.hadoop.api.ReadSupport;\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata;\nimport org.apache.parquet.io.api.GroupConverter;\nimport org.apache.parquet.io.api.RecordMaterializer;\nimport org.apache.parquet.schema.MessageType;\n\npublic class ParquetFileReader {\n  private final FileIO fileIO;\n  private final int maxBatchSize;\n\n  public ParquetFileReader(FileIO fileIO) {\n    this.fileIO = requireNonNull(fileIO, \"fileIO is null\");\n    this.maxBatchSize =\n        fileIO\n            .getConf(\"delta.kernel.default.parquet.reader.batch-size\")\n            .map(Integer::valueOf)\n            .orElse(1024);\n    checkArgument(maxBatchSize > 0, \"invalid Parquet reader batch size: %s\", maxBatchSize);\n  }\n\n  public CloseableIterator<ColumnarBatch> read(\n      FileStatus fileStatus, StructType schema, Optional<Predicate> predicate) {\n\n    if (schema.fields().stream()\n        .filter(StructField::isMetadataColumn)\n        .anyMatch(col -> col.getMetadataColumnSpec() != MetadataColumnSpec.ROW_INDEX)) {\n      throw new IllegalArgumentException(\n          \"The parquet reader does not support metadata columns other than ROW_INDEX\");\n    }\n    final boolean hasRowIndexCol = schema.contains(MetadataColumnSpec.ROW_INDEX);\n\n    return new CloseableIterator<ColumnarBatch>() {\n      private final BatchReadSupport readSupport = new BatchReadSupport(maxBatchSize, schema);\n      private ParquetReader<Object> reader;\n      private boolean hasNotConsumedNextElement;\n\n      @Override\n      public void close() throws IOException {\n        Utils.closeCloseables(reader);\n      }\n\n      @Override\n      public boolean hasNext() {\n        initParquetReaderIfRequired();\n        try {\n          if (hasNotConsumedNextElement) {\n            return true;\n          }\n\n          Object next = reader.read();\n          hasNotConsumedNextElement = next != null;\n          return hasNotConsumedNextElement;\n        } catch (IOException ex) {\n          throw new KernelEngineException(\n              \"Error reading Parquet file: \" + fileStatus.getPath(), ex);\n        }\n      }\n\n      @Override\n      public ColumnarBatch next() {\n        if (!hasNotConsumedNextElement) {\n          throw new NoSuchElementException();\n        }\n        int batchSize = 0;\n        do {\n          hasNotConsumedNextElement = false;\n          // hasNext reads to row to confirm there is a next element.\n          // get the row index only if required by the read schema\n          long rowIndex = hasRowIndexCol ? reader.getCurrentRowIndex() : -1;\n          readSupport.finalizeCurrentRow(rowIndex);\n          batchSize++;\n        } while (batchSize < maxBatchSize && hasNext());\n\n        return readSupport.getDataAsColumnarBatch(batchSize);\n      }\n\n      private void initParquetReaderIfRequired() {\n        if (reader == null) {\n          org.apache.parquet.hadoop.ParquetFileReader fileReader = null;\n          try {\n            InputFile inputFile = fileIO.newInputFile(fileStatus.getPath(), fileStatus.getSize());\n\n            // We need physical schema in order to construct a filter that can be\n            // pushed into the `parquet-mr` reader. For that reason read the footer\n            // in advance.\n            org.apache.parquet.io.InputFile parquetInputFile =\n                ParquetIOUtils.createParquetInputFile(inputFile);\n\n            ParquetMetadata footer =\n                org.apache.parquet.hadoop.ParquetFileReader.readFooter(\n                    parquetInputFile, ParquetMetadataConverter.NO_FILTER);\n\n            MessageType parquetSchema = footer.getFileMetaData().getSchema();\n            Optional<FilterPredicate> parquetPredicate =\n                predicate.flatMap(predicate -> toParquetFilter(parquetSchema, predicate));\n\n            // TODO: We can avoid reading the footer again if we can pass the footer, but there is\n            // no API to do that in the current version of parquet-mr which takes InputFile\n            // as input.\n            reader =\n                new ParquetReader.Builder<Object>(parquetInputFile) {\n                  @Override\n                  protected ReadSupport<Object> getReadSupport() {\n                    return readSupport;\n                  }\n                }.withFilter(parquetPredicate.map(FilterCompat::get).orElse(FilterCompat.NOOP))\n                    // Disable the record level filtering as the `parquet-mr` evaluates\n                    // the filter once the entire record has been materialized. Instead,\n                    // we use the predicate to prune the row groups which is more efficient.\n                    // In the future, we can consider using the record level filtering if a\n                    // native Parquet reader is implemented in Kernel default module.\n                    .useRecordFilter(false)\n                    .useStatsFilter(true) // only enable the row group level filtering\n                    .useBloomFilter(false)\n                    .useDictionaryFilter(false)\n                    .useColumnIndexFilter(false)\n                    .build();\n\n          } catch (IOException e) {\n            Utils.closeCloseablesSilently(fileReader, reader);\n            throw new KernelEngineException(\n                \"Error reading Parquet file: \" + fileStatus.getPath(), e);\n          }\n        }\n      }\n    };\n  }\n\n  /**\n   * Implement a {@link ReadSupport} that will collect the data for each row and return as a {@link\n   * ColumnarBatch}.\n   */\n  public static class BatchReadSupport extends ReadSupport<Object> {\n    private final int maxBatchSize;\n    private final StructType readSchema;\n    private RowRecordCollector rowRecordCollector;\n\n    public BatchReadSupport(int maxBatchSize, StructType readSchema) {\n      this.maxBatchSize = maxBatchSize;\n      this.readSchema = requireNonNull(readSchema, \"readSchema is not null\");\n    }\n\n    @Override\n    public ReadContext init(InitContext context) {\n      return new ReadContext(ParquetSchemaUtils.pruneSchema(context.getFileSchema(), readSchema));\n    }\n\n    @Override\n    public RecordMaterializer<Object> prepareForRead(\n        Configuration configuration,\n        Map<String, String> keyValueMetaData,\n        MessageType fileSchema,\n        ReadContext readContext) {\n      rowRecordCollector = new RowRecordCollector(maxBatchSize, readSchema, fileSchema);\n      return rowRecordCollector;\n    }\n\n    public ColumnarBatch getDataAsColumnarBatch(int batchSize) {\n      return rowRecordCollector.getDataAsColumnarBatch(batchSize);\n    }\n\n    /** @param fileRowIndex the file row index of the row just processed. */\n    public void finalizeCurrentRow(long fileRowIndex) {\n      rowRecordCollector.finalizeCurrentRow(fileRowIndex);\n    }\n  }\n\n  /**\n   * Collects the records given by the Parquet reader as columnar data. Parquet reader allows\n   * reading data row by row, but {@link ParquetFileReader} wants to expose the data as a columnar\n   * batch. Parquet reader takes an implementation of {@link RecordMaterializer} to which it gives\n   * data for each column one row at a time. This {@link RecordMaterializer} implementation collects\n   * the column values for multiple rows and returns a {@link ColumnarBatch} at the end.\n   */\n  public static class RowRecordCollector extends RecordMaterializer<Object> {\n    private static final Object FAKE_ROW_RECORD = new Object();\n    private final RowColumnReader rowRecordGroupConverter;\n\n    public RowRecordCollector(int maxBatchSize, StructType readSchema, MessageType fileSchema) {\n      this.rowRecordGroupConverter = new RowColumnReader(maxBatchSize, readSchema, fileSchema);\n    }\n\n    @Override\n    public void skipCurrentRecord() {\n      super.skipCurrentRecord();\n    }\n\n    /**\n     * Return a fake object. This is not used by {@link ParquetFileReader}, instead {@link\n     * #getDataAsColumnarBatch}} once a sufficient number of rows are collected.\n     */\n    @Override\n    public Object getCurrentRecord() {\n      return FAKE_ROW_RECORD;\n    }\n\n    @Override\n    public GroupConverter getRootConverter() {\n      return rowRecordGroupConverter;\n    }\n\n    /** Return the data collected so far as a {@link ColumnarBatch}. */\n    public ColumnarBatch getDataAsColumnarBatch(int batchSize) {\n      return rowRecordGroupConverter.getDataAsColumnarBatch(batchSize);\n    }\n\n    /**\n     * Finalize the current row.\n     *\n     * @param fileRowIndex the file row index of the row just processed\n     */\n    public void finalizeCurrentRow(long fileRowIndex) {\n      rowRecordGroupConverter.finalizeCurrentRow(fileRowIndex);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetFileWriter.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetIOUtils.createParquetOutputFile;\nimport static io.delta.kernel.defaults.internal.parquet.ParquetStatsReader.readDataFileStatistics;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Collections.emptyMap;\nimport static java.util.Objects.requireNonNull;\nimport static org.apache.parquet.hadoop.ParquetOutputFormat.*;\n\nimport io.delta.kernel.Meta;\nimport io.delta.kernel.data.*;\nimport io.delta.kernel.defaults.engine.fileio.FileIO;\nimport io.delta.kernel.defaults.engine.fileio.OutputFile;\nimport io.delta.kernel.defaults.internal.parquet.ParquetColumnWriters.ColumnWriter;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.fs.Path;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.utils.*;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.parquet.column.ParquetProperties.WriterVersion;\nimport org.apache.parquet.hadoop.ParquetOutputFormat;\nimport org.apache.parquet.hadoop.ParquetWriter;\nimport org.apache.parquet.hadoop.api.WriteSupport;\nimport org.apache.parquet.hadoop.metadata.CompressionCodecName;\nimport org.apache.parquet.io.api.RecordConsumer;\nimport org.apache.parquet.schema.MessageType;\n\n/**\n * Implements writing data given as {@link FilteredColumnarBatch} to Parquet files.\n *\n * <p>It makes use of the `parquet-mr` library to write the data in Parquet format. The main class\n * used is {@link ParquetWriter} which is used to write the data row by row to the Parquet file.\n * Supporting interface for this writer is {@link WriteSupport} (in this writer implementation, it\n * is {@link BatchWriteSupport}). {@link BatchWriteSupport}, on call back from {@link\n * ParquetWriter}, reads the contents of {@link ColumnarBatch} and passes the contents to {@link\n * ParquetWriter} through {@link RecordConsumer}.\n */\npublic class ParquetFileWriter {\n  public static final String TARGET_FILE_SIZE_CONF =\n      \"delta.kernel.default.parquet.writer.targetMaxFileSize\";\n  public static final long DEFAULT_TARGET_FILE_SIZE = 128 * 1024 * 1024; // 128MB\n\n  private final FileIO fileIO;\n  private final boolean writeAsSingleFile;\n  private final String location;\n  private final boolean atomicWrite;\n  private final long targetMaxFileSize;\n  private final List<Column> statsColumns;\n\n  private long currentFileNumber; // used to generate the unique file names.\n\n  /**\n   * Create writer to write data into one or more files depending upon the {@code\n   * delta.kernel.default.parquet.writer.targetMaxFileSize} value and the given data.\n   *\n   * @param fileIO File IO implementation to use for reading and writing files.\n   * @param location Location to write the data. Should be a directory.\n   * @param statsColumns List of columns to collect statistics for. The statistics collection is\n   *     optional.\n   */\n  public static ParquetFileWriter multiFileWriter(\n      FileIO fileIO, String location, List<Column> statsColumns) {\n    return new ParquetFileWriter(\n        fileIO, location, /* writeAsSingleFile = */ false, /* atomicWrite = */ false, statsColumns);\n  }\n\n  /**\n   * Create writer to write the data exactly into one file.\n   *\n   * @param fileIO File IO implementation to use for reading and writing files.\n   * @param location Location to write the data. Shouldn't be a directory.\n   * @param atomicWrite If true, write the file is written atomically (i.e. either the entire\n   *     content is written or none, but won't create a file with the partial contents).\n   * @param statsColumns List of columns to collect statistics for. The statistics collection is\n   *     optional.\n   */\n  public static ParquetFileWriter singleFileWriter(\n      FileIO fileIO, String location, boolean atomicWrite, List<Column> statsColumns) {\n    return new ParquetFileWriter(\n        fileIO, location, /* writeAsSingleFile = */ true, atomicWrite, statsColumns);\n  }\n\n  /**\n   * Private constructor to create the writer. Use {@link #multiFileWriter} or {@link\n   * #singleFileWriter} to create the writer.\n   */\n  private ParquetFileWriter(\n      FileIO fileIO,\n      String location,\n      boolean writeAsSingleFile,\n      boolean atomicWrite,\n      List<Column> statsColumns) {\n    this.fileIO = requireNonNull(fileIO, \"fileIO is null\");\n    this.writeAsSingleFile = writeAsSingleFile;\n    this.location = requireNonNull(location, \"location is null\");\n    this.atomicWrite = atomicWrite;\n    this.statsColumns = requireNonNull(statsColumns, \"statsColumns is null\");\n    this.targetMaxFileSize =\n        fileIO.getConf(TARGET_FILE_SIZE_CONF).map(Long::valueOf).orElse(DEFAULT_TARGET_FILE_SIZE);\n    checkArgument(targetMaxFileSize > 0, \"Invalid target Parquet file size: %s\", targetMaxFileSize);\n  }\n\n  /**\n   * Write the given data to Parquet files.\n   *\n   * @param dataIter Iterator of data to write.\n   * @return an iterator of {@link DataFileStatus} where each entry contains the metadata of the\n   *     data file written. It is the responsibility of the caller to close the iterator.\n   */\n  public CloseableIterator<DataFileStatus> write(\n      CloseableIterator<FilteredColumnarBatch> dataIter) {\n    return new CloseableIterator<DataFileStatus>() {\n      // Last written file output.\n      private Optional<DataFileStatus> lastWrittenFileOutput = Optional.empty();\n\n      // Current batch of data that is being written, updated in {@link #hasNextRow()}.\n      private FilteredColumnarBatch currentBatch = null;\n\n      // Which record in the `currentBatch` is being written,\n      // initialized in {@link #hasNextRow()} and updated in {@link #consumeNextRow}.\n      private int currentBatchCursor = 0;\n\n      // BatchWriteSupport is initialized when the first batch is read and reused for\n      // subsequent batches with the same schema. `ParquetWriter` can use this write support\n      // to consume data from `ColumnarBatch` and write it to Parquet files.\n      private BatchWriteSupport batchWriteSupport = null;\n\n      private StructType dataSchema = null;\n\n      @Override\n      public void close() {\n        Utils.closeCloseables(dataIter);\n      }\n\n      @Override\n      public boolean hasNext() {\n        if (lastWrittenFileOutput.isPresent()) {\n          return true;\n        }\n        lastWrittenFileOutput = writeNextFile();\n        return lastWrittenFileOutput.isPresent();\n      }\n\n      @Override\n      public DataFileStatus next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        DataFileStatus toReturn = lastWrittenFileOutput.get();\n        lastWrittenFileOutput = Optional.empty();\n        return toReturn;\n      }\n\n      private Optional<DataFileStatus> writeNextFile() {\n        if (!hasNextRow()) {\n          return Optional.empty();\n        }\n\n        org.apache.parquet.io.OutputFile parquetOutputFile =\n            createParquetOutputFile(generateNextOutputFile(), atomicWrite);\n        assert batchWriteSupport != null : \"batchWriteSupport is not initialized\";\n        long currentFileRowCount = 0; // tracks the number of rows written to the current file\n        try (ParquetWriter<Integer> writer = createWriter(parquetOutputFile, batchWriteSupport)) {\n          boolean maxFileSizeReached;\n          do {\n            if (consumeNextRow(writer)) {\n              // If the row was written, increment the row count\n              currentFileRowCount++;\n            }\n            // If we are writing a single file, then don't need to check for the current\n            // file size. Otherwise see if the current file size reached the target file\n            // size.\n            maxFileSizeReached = !writeAsSingleFile && writer.getDataSize() >= targetMaxFileSize;\n            // Keep writing until max file is reached or no more data to write\n          } while (!maxFileSizeReached && hasNextRow());\n        } catch (IOException e) {\n          throw new UncheckedIOException(\n              \"Failed to write the Parquet file: \" + parquetOutputFile.getPath(), e);\n        }\n\n        return Optional.of(\n            constructDataFileStatus(parquetOutputFile.getPath(), dataSchema, currentFileRowCount));\n      }\n\n      /**\n       * Returns true if there is data to write.\n       *\n       * <p>Internally it traverses the rows in one batch after the other. Whenever a batch is fully\n       * consumed, moves to the next input batch and updates the column writers in\n       * `batchWriteSupport`.\n       */\n      boolean hasNextRow() {\n        boolean hasNextRowInCurrentBatch =\n            currentBatch != null\n                &&\n                // Is current batch is fully read?\n                currentBatchCursor < currentBatch.getData().getSize();\n\n        if (hasNextRowInCurrentBatch) {\n          return true;\n        }\n\n        // loop until we find a non-empty batch or there are no more batches\n        do {\n          if (!dataIter.hasNext()) {\n            return false;\n          }\n          currentBatch = dataIter.next();\n          currentBatchCursor = 0;\n        } while (currentBatch.getData().getSize() == 0); // skip empty batches\n\n        // Initialize the batch support and create writers for each column\n        ColumnarBatch inputBatch = currentBatch.getData();\n        dataSchema = inputBatch.getSchema();\n        BatchWriteSupport writeSupport = createOrGetWriteSupport(dataSchema);\n\n        ColumnWriter[] columnWriters = ParquetColumnWriters.createColumnVectorWriters(inputBatch);\n\n        writeSupport.setColumnVectorWriters(columnWriters);\n\n        return true;\n      }\n\n      /**\n       * Consume the next row of data to write. If the row is selected, write it. Otherwise, skip\n       * it. At the end move the cursor to the next row.\n       *\n       * @return true if the row was written, false if it was skipped\n       */\n      boolean consumeNextRow(ParquetWriter<Integer> writer) throws IOException {\n        Optional<ColumnVector> selectionVector = currentBatch.getSelectionVector();\n        boolean isRowSelected =\n            !selectionVector.isPresent()\n                || (!selectionVector.get().isNullAt(currentBatchCursor)\n                    && selectionVector.get().getBoolean(currentBatchCursor));\n\n        if (isRowSelected) {\n          writer.write(currentBatchCursor);\n        }\n        currentBatchCursor++;\n        return isRowSelected;\n      }\n\n      /**\n       * Create a {@link BatchWriteSupport} if it does not exist or return the existing one for\n       * given schema.\n       */\n      BatchWriteSupport createOrGetWriteSupport(StructType inputSchema) {\n        if (batchWriteSupport == null) {\n          MessageType parquetSchema = ParquetSchemaUtils.toParquetSchema(inputSchema);\n          batchWriteSupport = new BatchWriteSupport(inputSchema, parquetSchema);\n          return batchWriteSupport;\n        }\n        // Ensure the new input schema matches the one used to create the write support\n        if (!batchWriteSupport.inputSchema.equals(inputSchema)) {\n          throw new IllegalArgumentException(\n              \"Input data has columnar batches with \"\n                  + \"different schemas:\\n schema 1: \"\n                  + batchWriteSupport.inputSchema\n                  + \"\\n schema 2: \"\n                  + inputSchema);\n        }\n        return batchWriteSupport;\n      }\n    };\n  }\n\n  /**\n   * Implementation of {@link WriteSupport} to write the {@link ColumnarBatch} to Parquet files.\n   * {@link ParquetWriter} makes use of this interface to consume the data row by row and write to\n   * the Parquet file. Call backs from the {@link ParquetWriter} includes:\n   *\n   * <ul>\n   *   <li>{@link #init(Configuration)}: Called once to init and get {@link WriteContext} which\n   *       includes the schema and extra properties.\n   *   <li>{@link #prepareForWrite(RecordConsumer)}: Called once to prepare for writing the data.\n   *       {@link RecordConsumer} is a way for this batch support to write data for each column in\n   *       the current row.\n   *   <li>{@link #write(Integer)}: Called for each row to write the data. In this method, column\n   *       values are passed to the {@link RecordConsumer} through series of calls.\n   * </ul>\n   */\n  private static class BatchWriteSupport extends WriteSupport<Integer> {\n    final StructType inputSchema;\n    final MessageType parquetSchema;\n\n    private ColumnWriter[] columnWriters;\n    private RecordConsumer recordConsumer;\n\n    BatchWriteSupport(\n        StructType inputSchema, // WriteSupport created for this specific schema\n        MessageType parquetSchema) { // Parquet equivalent schema\n      this.inputSchema = requireNonNull(inputSchema, \"inputSchema is null\");\n      this.parquetSchema = requireNonNull(parquetSchema, \"parquetSchema is null\");\n    }\n\n    void setColumnVectorWriters(ColumnWriter[] columnWriters) {\n      this.columnWriters = requireNonNull(columnWriters, \"columnVectorWriters is null\");\n    }\n\n    @Override\n    public String getName() {\n      return \"delta-kernel-default-parquet-writer\";\n    }\n\n    @Override\n    public WriteContext init(Configuration configuration) {\n      Map<String, String> extraProps =\n          Collections.singletonMap(\n              \"io.delta.kernel.default-parquet-writer\", \"Kernel-Defaults-\" + Meta.KERNEL_VERSION);\n      return new WriteContext(parquetSchema, extraProps);\n    }\n\n    @Override\n    public void prepareForWrite(RecordConsumer recordConsumer) {\n      this.recordConsumer = recordConsumer;\n    }\n\n    @Override\n    public void write(Integer rowId) {\n      // Use java asserts which are disabled in prod to reduce the overhead\n      // and enabled in tests with `-ea` argument.\n      assert (recordConsumer != null) : \"Parquet record consumer is null\";\n      assert (columnWriters != null) : \"Column writers are not set\";\n      recordConsumer.startMessage();\n      for (int i = 0; i < columnWriters.length; i++) {\n        columnWriters[i].writeRowValue(recordConsumer, rowId);\n      }\n      recordConsumer.endMessage();\n    }\n  }\n\n  /** Generate the next file path to write the data. */\n  private OutputFile generateNextOutputFile() {\n    if (writeAsSingleFile) {\n      checkArgument(currentFileNumber++ == 0, \"expected to write just one file\");\n      return fileIO.newOutputFile(location);\n    }\n    String fileName = String.format(\"%s-%03d.parquet\", UUID.randomUUID(), currentFileNumber++);\n    String filePath = new Path(location, fileName).toString();\n    return fileIO.newOutputFile(filePath);\n  }\n\n  /**\n   * Helper method to create {@link ParquetWriter} for given file path and write support. It makes\n   * use of configuration options in `configuration` to configure the writer. Different available\n   * configuration options are defined in {@link ParquetOutputFormat}.\n   */\n  private ParquetWriter<Integer> createWriter(\n      org.apache.parquet.io.OutputFile outputFile, WriteSupport<Integer> writeSupport)\n      throws IOException {\n    ParquetRowDataBuilder rowDataBuilder = new ParquetRowDataBuilder(outputFile, writeSupport);\n\n    fileIO\n        .getConf(COMPRESSION)\n        .ifPresent(\n            compression ->\n                rowDataBuilder.withCompressionCodec(CompressionCodecName.fromConf(compression)));\n\n    fileIO.getConf(BLOCK_SIZE).map(Long::parseLong).ifPresent(rowDataBuilder::withRowGroupSize);\n\n    fileIO.getConf(PAGE_SIZE).map(Integer::parseInt).ifPresent(rowDataBuilder::withPageSize);\n\n    fileIO\n        .getConf(DICTIONARY_PAGE_SIZE)\n        .map(Integer::parseInt)\n        .ifPresent(rowDataBuilder::withDictionaryPageSize);\n\n    fileIO\n        .getConf(MAX_PADDING_BYTES)\n        .map(Integer::parseInt)\n        .ifPresent(rowDataBuilder::withMaxPaddingSize);\n\n    fileIO\n        .getConf(ENABLE_DICTIONARY)\n        .map(Boolean::parseBoolean)\n        .ifPresent(rowDataBuilder::withDictionaryEncoding);\n\n    fileIO.getConf(VALIDATION).map(Boolean::parseBoolean).ifPresent(rowDataBuilder::withValidation);\n\n    fileIO\n        .getConf(WRITER_VERSION)\n        .map(WriterVersion::fromString)\n        .ifPresent(rowDataBuilder::withWriterVersion);\n\n    return rowDataBuilder.build();\n  }\n\n  private static class ParquetRowDataBuilder\n      extends ParquetWriter.Builder<Integer, ParquetRowDataBuilder> {\n    private final WriteSupport<Integer> writeSupport;\n\n    protected ParquetRowDataBuilder(\n        org.apache.parquet.io.OutputFile outputFile, WriteSupport<Integer> writeSupport) {\n      super(outputFile);\n      this.writeSupport = requireNonNull(writeSupport, \"writeSupport is null\");\n    }\n\n    @Override\n    protected ParquetRowDataBuilder self() {\n      return this;\n    }\n\n    @Override\n    protected WriteSupport<Integer> getWriteSupport(Configuration conf) {\n      return writeSupport;\n    }\n  }\n\n  /**\n   * Construct the {@link DataFileStatus} for the given file path. It reads the file status and\n   * Parquet footer to compute the statistics for the file.\n   *\n   * <p>Potential improvement in future to directly compute the statistics while writing the file if\n   * this becomes a sufficiently large part of the write operation time.\n   *\n   * @param path the path of the file\n   * @param dataSchema the schema of the data in the file\n   * @param numRows the number of rows in the file. If no column stats are required, this is used to\n   *     construct the {@link DataFileStatistics}. Otherwise, the stats are read from the file.\n   * @return the {@link DataFileStatus} for the file\n   */\n  private DataFileStatus constructDataFileStatus(String path, StructType dataSchema, long numRows) {\n    try {\n      // Get the FileStatus to figure out the file size and modification time\n      FileStatus fileStatus = fileIO.getFileStatus(path);\n      String resolvedPath = fileIO.resolvePath(path);\n\n      DataFileStatistics stats;\n      if (statsColumns.isEmpty()) {\n        stats =\n            new DataFileStatistics(\n                numRows,\n                emptyMap() /* minValues */,\n                emptyMap() /* maxValues */,\n                emptyMap() /* nullCount */,\n                Optional.empty() /* tightBounds */);\n      } else {\n        stats =\n            readDataFileStatistics(\n                fileIO.newInputFile(resolvedPath, fileStatus.getSize()), dataSchema, statsColumns);\n      }\n\n      return new DataFileStatus(\n          resolvedPath,\n          fileStatus.getSize(),\n          fileStatus.getModificationTime(),\n          Optional.ofNullable(stats));\n    } catch (IOException ioe) {\n      throw new UncheckedIOException(\"Failed to read the stats for: \" + path, ioe);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetFilterUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.internal.util.ExpressionUtils.*;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static org.apache.parquet.filter2.predicate.FilterApi.*;\n\nimport io.delta.kernel.expressions.*;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport org.apache.parquet.column.ColumnDescriptor;\nimport org.apache.parquet.filter2.compat.FilterCompat.Filter;\nimport org.apache.parquet.filter2.predicate.*;\nimport org.apache.parquet.filter2.predicate.Operators.*;\nimport org.apache.parquet.hadoop.metadata.ColumnPath;\nimport org.apache.parquet.io.api.Binary;\nimport org.apache.parquet.schema.*;\nimport org.apache.parquet.schema.LogicalTypeAnnotation.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** Utilities to convert the Kernel {@link Predicate} into `parquet-mr` {@link FilterPredicate}. */\npublic class ParquetFilterUtils {\n  private static final Logger logger = LoggerFactory.getLogger(ParquetFilterUtils.class);\n\n  private ParquetFilterUtils() {}\n\n  /**\n   * Convert the given Kernel predicate {@code kernelPredicate} into `parquet-mr` predicate.\n   *\n   * @param parquetFileSchema Schema of the Parquet file. We need it to find what columns exists in\n   *     the Parquet file in order to remove predicates on columns that do not exist in the file.\n   *     There is no clear way to handle the predicate on columns that don't exist in the Parquet\n   *     file.\n   * @param kernelPredicate Kernel predicate to convert.\n   * @return instance of {@link Filter} (`parquet-mr` filter)\n   */\n  public static Optional<FilterPredicate> toParquetFilter(\n      MessageType parquetFileSchema, Predicate kernelPredicate) {\n    // Construct a map of field names to field metadata objects\n    Map<Column, ParquetField> parquetFieldMap = extractParquetFields(parquetFileSchema);\n    return convertToParquetFilter(parquetFieldMap, kernelPredicate);\n  }\n\n  private static class ParquetField {\n    final LogicalTypeAnnotation logicalType;\n    final PrimitiveType primitiveType;\n\n    private ParquetField(LogicalTypeAnnotation logicalType, PrimitiveType primitiveType) {\n      this.logicalType = logicalType;\n      this.primitiveType = primitiveType;\n    }\n\n    static ParquetField of(LogicalTypeAnnotation logicalType, PrimitiveType primitiveType) {\n      return new ParquetField(logicalType, primitiveType);\n    }\n  }\n\n  /**\n   * Create a mapping of column to ParquetField for each non-repeated leaf-level column in the given\n   * parquet schema.\n   *\n   * @param parquetSchema Schema of the Parquet file\n   * @return Mapping of column to ParquetField\n   */\n  private static Map<Column, ParquetField> extractParquetFields(MessageType parquetSchema) {\n    Map<Column, ParquetField> parquetFields = new HashMap<>();\n    for (ColumnDescriptor columnDescriptor : parquetSchema.getColumns()) {\n      String[] columnPath = columnDescriptor.getPath();\n      Type type = parquetSchema.getType(columnPath);\n      if (type.getRepetition() == Type.Repetition.REPEATED) {\n        // `parquet-mr` doesn't support applying filter on a repeated column\n        continue;\n      }\n      assert type.isPrimitive() : \"Only primitive types are expected from .getColumns()\";\n      PrimitiveType primitiveType = type.asPrimitiveType();\n      parquetFields.put(\n          new Column(columnPath), ParquetField.of(type.getLogicalTypeAnnotation(), primitiveType));\n    }\n    return parquetFields;\n  }\n\n  private static boolean canUseLiteral(Literal literal, PrimitiveType parquetType) {\n    DataType litType = literal.getDataType();\n    LogicalTypeAnnotation logicalType = parquetType.getLogicalTypeAnnotation();\n    switch (parquetType.getPrimitiveTypeName()) {\n      case BOOLEAN:\n        return litType instanceof BooleanType;\n      case INT32:\n        if (!isInteger(literal)) {\n          return false;\n        }\n        return logicalType == null\n            || // no logical type when the type is int32 or int64\n            (logicalType instanceof IntLogicalTypeAnnotation\n                && ((IntLogicalTypeAnnotation) logicalType).getBitWidth() <= 32)\n            || logicalType instanceof DateLogicalTypeAnnotation;\n      case INT64:\n        if (!isLong(literal)) {\n          return false;\n        }\n        return logicalType == null\n            || // no logical type when the type is int32 or int64\n            (logicalType instanceof IntLogicalTypeAnnotation\n                && ((IntLogicalTypeAnnotation) logicalType).getBitWidth() <= 64);\n      case FLOAT:\n        return isFloat(literal);\n      case DOUBLE:\n        return isDouble(literal);\n      case BINARY:\n        {\n          return isBinary(literal)\n              &&\n              // logical type should be binary (null) or string\n              (logicalType == null || logicalType instanceof StringLogicalTypeAnnotation);\n        }\n      default:\n        return false;\n    }\n  }\n\n  private static Optional<FilterPredicate> convertToParquetFilter(\n      Map<Column, ParquetField> parquetFieldMap, Predicate deltaPredicate) {\n    String name = deltaPredicate.getName().toLowerCase(Locale.ROOT);\n    switch (name) {\n      case \"=\":\n      case \"<\":\n      case \"<=\":\n      case \">\":\n      case \">=\":\n        return convertComparatorToParquetFilter(parquetFieldMap, deltaPredicate);\n      case \"not\":\n        return convertNotToParquetFilter(parquetFieldMap, deltaPredicate);\n      case \"and\":\n        return convertAndToParquetFilter(parquetFieldMap, deltaPredicate);\n      case \"or\":\n        return convertOrToParquetFilter(parquetFieldMap, deltaPredicate);\n      case \"is_null\":\n        return convertIsNullIsNotNull(parquetFieldMap, deltaPredicate, false /* isNotNull */);\n      case \"is_not_null\":\n        return convertIsNullIsNotNull(parquetFieldMap, deltaPredicate, true /* isNotNull */);\n      default:\n        return visitUnsupported(deltaPredicate, name + \" is not a supported predicate.\");\n    }\n  }\n\n  private static Optional<FilterPredicate> convertComparatorToParquetFilter(\n      Map<Column, ParquetField> parquetFieldMap, Predicate deltaPredicate) {\n    Expression child0 = getLeft(deltaPredicate);\n    Expression child1 = getRight(deltaPredicate);\n\n    if (child0 instanceof Literal && child1 instanceof Column) {\n      Expression temp = child0;\n      child0 = child1;\n      child1 = temp;\n    }\n\n    if (!(child0 instanceof Column) || !(child1 instanceof Literal)) {\n      return visitUnsupported(\n          deltaPredicate, \"Comparison predicate must have a column and a literal.\");\n    }\n\n    Column column = (Column) child0;\n    Literal literal = (Literal) child1;\n\n    ParquetField parquetField = parquetFieldMap.get(column);\n    if (parquetField == null) {\n      return visitUnsupported(\n          deltaPredicate, \"Column used in predicate does not exist in the parquet file.\");\n    }\n\n    if (literal.getValue() == null) {\n      return visitUnsupported(\n          deltaPredicate,\n          \"Literal value is null for a comparator operator. Comparator is not \"\n              + \"supported for null values as the Parquet comparator is not null safe\");\n    }\n\n    if (!canUseLiteral(literal, parquetField.primitiveType)) {\n      return visitUnsupported(\n          deltaPredicate,\n          \"Literal type is not compatible with the column type: \" + literal.getDataType());\n    }\n\n    PrimitiveType parquetType = parquetField.primitiveType;\n    String columnPath = ColumnPath.get(column.getNames()).toDotString();\n    String comparator = deltaPredicate.getName();\n\n    switch (parquetType.getPrimitiveTypeName()) {\n      case BOOLEAN:\n        BooleanColumn booleanColumn = booleanColumn(columnPath);\n        if (\"=\".equals(comparator)) { // Only = is supported for boolean\n          return Optional.of(FilterApi.eq(booleanColumn, getBoolean(literal)));\n        }\n        break;\n      case INT32:\n        IntColumn intColumn = intColumn(columnPath);\n        switch (comparator) {\n          case \"=\":\n            return Optional.of(FilterApi.eq(intColumn, getInt(literal)));\n          case \"<\":\n            return Optional.of(FilterApi.lt(intColumn, getInt(literal)));\n          case \"<=\":\n            return Optional.of(FilterApi.ltEq(intColumn, getInt(literal)));\n          case \">\":\n            return Optional.of(FilterApi.gt(intColumn, getInt(literal)));\n          case \">=\":\n            return Optional.of(FilterApi.gtEq(intColumn, getInt(literal)));\n        }\n        break;\n      case INT64:\n        LongColumn longColumn = longColumn(columnPath);\n        switch (comparator) {\n          case \"=\":\n            return Optional.of(FilterApi.eq(longColumn, getLong(literal)));\n          case \"<\":\n            return Optional.of(FilterApi.lt(longColumn, getLong(literal)));\n          case \"<=\":\n            return Optional.of(FilterApi.ltEq(longColumn, getLong(literal)));\n          case \">\":\n            return Optional.of(FilterApi.gt(longColumn, getLong(literal)));\n          case \">=\":\n            return Optional.of(FilterApi.gtEq(longColumn, getLong(literal)));\n        }\n        break;\n      case FLOAT:\n        FloatColumn floatColumn = floatColumn(columnPath);\n        switch (comparator) {\n          case \"=\":\n            return Optional.of(FilterApi.eq(floatColumn, getFloat(literal)));\n          case \"<\":\n            return Optional.of(FilterApi.lt(floatColumn, getFloat(literal)));\n          case \"<=\":\n            return Optional.of(FilterApi.ltEq(floatColumn, getFloat(literal)));\n          case \">\":\n            return Optional.of(FilterApi.gt(floatColumn, getFloat(literal)));\n          case \">=\":\n            return Optional.of(FilterApi.gtEq(floatColumn, getFloat(literal)));\n        }\n        break;\n      case DOUBLE:\n        DoubleColumn doubleColumn = doubleColumn(columnPath);\n        switch (comparator) {\n          case \"=\":\n            return Optional.of(FilterApi.eq(doubleColumn, getDouble(literal)));\n          case \"<\":\n            return Optional.of(FilterApi.lt(doubleColumn, getDouble(literal)));\n          case \"<=\":\n            return Optional.of(FilterApi.ltEq(doubleColumn, getDouble(literal)));\n          case \">\":\n            return Optional.of(FilterApi.gt(doubleColumn, getDouble(literal)));\n          case \">=\":\n            return Optional.of(FilterApi.gtEq(doubleColumn, getDouble(literal)));\n        }\n        break;\n      case BINARY:\n        BinaryColumn binaryColumn = binaryColumn(columnPath);\n        Binary binary = getBinary(literal);\n        switch (comparator) {\n          case \"=\":\n            return Optional.of(FilterApi.eq(binaryColumn, binary));\n          case \"<\":\n            return Optional.of(FilterApi.lt(binaryColumn, binary));\n          case \"<=\":\n            return Optional.of(FilterApi.ltEq(binaryColumn, binary));\n          case \">\":\n            return Optional.of(FilterApi.gt(binaryColumn, binary));\n          case \">=\":\n            return Optional.of(FilterApi.gtEq(binaryColumn, binary));\n        }\n        break;\n    }\n    return visitUnsupported(\n        deltaPredicate,\n        String.format(\n            \"Unsupported column type (%s) with comparator (%s): \", parquetType, comparator));\n  }\n\n  private static Optional<FilterPredicate> convertNotToParquetFilter(\n      Map<Column, ParquetField> parquetFieldMap, Predicate deltaPredicate) {\n    Optional<FilterPredicate> childFilter =\n        convertToParquetFilter(parquetFieldMap, (Predicate) getUnaryChild(deltaPredicate));\n\n    return childFilter.map(FilterApi::not);\n  }\n\n  private static Optional<FilterPredicate> convertOrToParquetFilter(\n      Map<Column, ParquetField> parquetFieldMap, Predicate deltaPredicate) {\n    Optional<FilterPredicate> leftFilter =\n        convertToParquetFilter(parquetFieldMap, asPredicate(getLeft(deltaPredicate)));\n    Optional<FilterPredicate> rightFilter =\n        convertToParquetFilter(parquetFieldMap, asPredicate(getRight(deltaPredicate)));\n\n    if (leftFilter.isPresent() && rightFilter.isPresent()) {\n      return Optional.of(FilterApi.or(leftFilter.get(), rightFilter.get()));\n    }\n    return Optional.empty();\n  }\n\n  private static Optional<FilterPredicate> convertAndToParquetFilter(\n      Map<Column, ParquetField> parquetFieldMap, Predicate deltaPredicate) {\n    Optional<FilterPredicate> leftFilter =\n        convertToParquetFilter(parquetFieldMap, asPredicate(getLeft(deltaPredicate)));\n    Optional<FilterPredicate> rightFilter =\n        convertToParquetFilter(parquetFieldMap, asPredicate(getRight(deltaPredicate)));\n\n    if (leftFilter.isPresent() && rightFilter.isPresent()) {\n      return Optional.of(FilterApi.and(leftFilter.get(), rightFilter.get()));\n    }\n    if (leftFilter.isPresent()) {\n      return leftFilter;\n    }\n    return rightFilter;\n  }\n\n  private static Optional<FilterPredicate> convertIsNullIsNotNull(\n      Map<Column, ParquetField> parquetFieldMap, Predicate deltaPredicate, boolean isNotNull) {\n    Expression child = getUnaryChild(deltaPredicate);\n    if (!(child instanceof Column)) {\n      return visitUnsupported(deltaPredicate, \"IS NULL predicate must have a column input.\");\n    }\n\n    Column column = (Column) child;\n    ParquetField parquetField = parquetFieldMap.get(column);\n    if (parquetField == null) {\n      return visitUnsupported(\n          deltaPredicate, \"Column used in predicate does not exist in the parquet file.\");\n    }\n\n    String columnPath = ColumnPath.get(column.getNames()).toDotString();\n    // Parquet filter keeps records if their value is equal to the provided value.\n    // Nulls are treated the same way the java programming language does.\n    // For example: eq(column, null) will keep all records whose value is null. eq(column, 7)\n    // will keep all records whose value is 7, and will drop records whose value is null\n    // NOTE: this is different from how some query languages handle null.\n    switch (parquetField.primitiveType.getPrimitiveTypeName()) {\n      case BOOLEAN:\n        return createIsNullOrIsNotNullPredicate(booleanColumn(columnPath), isNotNull);\n      case INT32:\n        return createIsNullOrIsNotNullPredicate(intColumn(columnPath), isNotNull);\n      case INT64:\n        return createIsNullOrIsNotNullPredicate(longColumn(columnPath), isNotNull);\n      case FLOAT:\n        return createIsNullOrIsNotNullPredicate(floatColumn(columnPath), isNotNull);\n      case DOUBLE:\n        return createIsNullOrIsNotNullPredicate(doubleColumn(columnPath), isNotNull);\n      case BINARY:\n        return createIsNullOrIsNotNullPredicate(binaryColumn(columnPath), isNotNull);\n      default:\n        return visitUnsupported(\n            deltaPredicate, \"Unsupported column type: \" + parquetField.primitiveType);\n    }\n  }\n\n  private static <T extends Comparable<T>, C extends Operators.Column<T> & SupportsEqNotEq>\n      Optional<FilterPredicate> createIsNullOrIsNotNullPredicate(C column, boolean isNotNull) {\n    return Optional.of(isNotNull ? FilterApi.notEq(column, null) : FilterApi.eq(column, null));\n  }\n\n  private static Optional<FilterPredicate> visitUnsupported(Predicate predicate, String message) {\n    logger.info(\"Unsupported predicate: {}. Reason: {}\", predicate, message);\n    // Filtering is a best effort. If an unsupported predicate expression is received,\n    // do not consider it for filtering.\n    return Optional.empty();\n  }\n\n  private static boolean isBoolean(Literal literal) {\n    return literal.getDataType() instanceof BooleanType;\n  }\n\n  private static boolean getBoolean(Literal literal) {\n    checkArgument(isBoolean(literal), \"Literal is not a boolean: %s\", literal);\n    return (boolean) literal.getValue();\n  }\n\n  private static boolean isInteger(Literal literal) {\n    DataType dataType = literal.getDataType();\n    if (dataType instanceof LongType) {\n      // Check if the long value can be represented as an integer\n      return ((Long) literal.getValue()).intValue() == (Long) literal.getValue();\n    }\n\n    return dataType instanceof ByteType\n        || dataType instanceof ShortType\n        || dataType instanceof IntegerType\n        || dataType instanceof DateType;\n  }\n\n  private static int getInt(Literal literal) {\n    checkArgument(isInteger(literal), \"Literal is not an integer: %s\", literal);\n    DataType dataType = literal.getDataType();\n    if (dataType instanceof LongType) {\n      return ((Long) literal.getValue()).intValue();\n    }\n\n    return ((Number) literal.getValue()).intValue();\n  }\n\n  private static boolean isLong(Literal literal) {\n    DataType dataType = literal.getDataType();\n    return dataType instanceof LongType\n        || dataType instanceof ByteType\n        || dataType instanceof ShortType\n        || dataType instanceof IntegerType\n        || dataType instanceof DateType;\n  }\n\n  private static long getLong(Literal literal) {\n    checkArgument(isLong(literal), \"Literal is not a long: %s\", literal);\n    DataType dataType = literal.getDataType();\n    if (dataType instanceof LongType) {\n      return (long) literal.getValue();\n    }\n\n    return ((Number) literal.getValue()).longValue();\n  }\n\n  private static boolean isFloat(Literal literal) {\n    return literal.getDataType() instanceof FloatType;\n  }\n\n  private static float getFloat(Literal literal) {\n    checkArgument(isFloat(literal), \"Literal is not a float: %s\", literal);\n    return ((Number) literal.getValue()).floatValue();\n  }\n\n  private static boolean isDouble(Literal literal) {\n    return literal.getDataType() instanceof DoubleType;\n  }\n\n  private static double getDouble(Literal literal) {\n    checkArgument(isDouble(literal), \"Literal is not a double: %s\", literal);\n    return ((Number) literal.getValue()).doubleValue();\n  }\n\n  private static boolean isBinary(Literal literal) {\n    DataType type = literal.getDataType();\n    return type instanceof BinaryType || type instanceof StringType;\n  }\n\n  private static Binary getBinary(Literal literal) {\n    checkArgument(isBinary(literal), \"Literal is not a binary: %s\", literal);\n    DataType type = literal.getDataType();\n    if (type instanceof BinaryType) {\n      return Binary.fromConstantByteArray((byte[]) literal.getValue());\n    }\n    return Binary.fromString((String) literal.getValue());\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetIOUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport io.delta.kernel.defaults.engine.fileio.InputFile;\nimport io.delta.kernel.defaults.engine.fileio.OutputFile;\nimport io.delta.kernel.defaults.engine.fileio.PositionOutputStream;\nimport io.delta.kernel.defaults.engine.fileio.SeekableInputStream;\nimport java.io.IOException;\nimport org.apache.parquet.io.DelegatingPositionOutputStream;\nimport org.apache.parquet.io.DelegatingSeekableInputStream;\n\n/**\n * Utilities related to Parquet I/O. These utilities bridge the gap between Kernel's {@link\n * io.delta.kernel.defaults.engine.fileio.FileIO} and the Parquet I/O classes.\n */\npublic class ParquetIOUtils {\n  private ParquetIOUtils() {}\n\n  /** Create a Parquet {@link org.apache.parquet.io.InputFile} from a Kernel's {@link InputFile}. */\n  static org.apache.parquet.io.InputFile createParquetInputFile(InputFile inputFile) {\n    return new org.apache.parquet.io.InputFile() {\n      @Override\n      public long getLength() throws IOException {\n        return inputFile.length();\n      }\n\n      @Override\n      public org.apache.parquet.io.SeekableInputStream newStream() throws IOException {\n        SeekableInputStream seekableStream = inputFile.newStream();\n        return new DelegatingSeekableInputStream(seekableStream) {\n          @Override\n          public void seek(long newPos) throws IOException {\n            seekableStream.seek(newPos);\n          }\n\n          @Override\n          public long getPos() throws IOException {\n            return seekableStream.getPos();\n          }\n        };\n      }\n    };\n  }\n\n  /**\n   * Create a Parquet {@link org.apache.parquet.io.OutputFile} from a Kernel's {@link OutputFile}.\n   */\n  static org.apache.parquet.io.OutputFile createParquetOutputFile(\n      OutputFile kernelOutputFile, boolean atomicWrite) {\n    return new org.apache.parquet.io.OutputFile() {\n      @Override\n      public org.apache.parquet.io.PositionOutputStream create(long blockSizeHint)\n          throws IOException {\n        // blockSizeHint is hint used in HDFS compliant file systems. In cloud storage systems\n        // it is irrelevant. So, we ignore it.\n        PositionOutputStream posOutputStream = kernelOutputFile.create(atomicWrite);\n        return new DelegatingPositionOutputStream(posOutputStream) {\n          @Override\n          public long getPos() throws IOException {\n            return posOutputStream.getPos();\n          }\n        };\n      }\n\n      @Override\n      public org.apache.parquet.io.PositionOutputStream createOrOverwrite(long blockSizeHint)\n          throws IOException {\n        // In Kernel we never overwrite files, so this method is not used.\n        throw new UnsupportedOperationException(\"createOrOverwrite is not supported in Kernel\");\n      }\n\n      @Override\n      public boolean supportsBlockSize() {\n        return false;\n      }\n\n      @Override\n      public long defaultBlockSize() {\n        // blockSizeHint is hint used in HDFS compliant file systems. In cloud storage systems\n        // it is irrelevant. So, return some default value.\n        return 128 * 1024 * 1024; // 128MB\n      }\n\n      @Override\n      public String getPath() {\n        return kernelOutputFile.path();\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetSchemaUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.internal.util.ColumnMapping.PARQUET_FIELD_NESTED_IDS_METADATA_KEY;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.lang.String.format;\nimport static org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit.MICROS;\nimport static org.apache.parquet.schema.LogicalTypeAnnotation.decimalType;\nimport static org.apache.parquet.schema.LogicalTypeAnnotation.timestampType;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.BINARY;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.BOOLEAN;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.DOUBLE;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.FLOAT;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT64;\nimport static org.apache.parquet.schema.Type.Repetition.OPTIONAL;\nimport static org.apache.parquet.schema.Type.Repetition.REQUIRED;\nimport static org.apache.parquet.schema.Types.primitive;\n\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.BiFunction;\nimport java.util.stream.Collectors;\nimport org.apache.parquet.schema.*;\nimport org.apache.parquet.schema.LogicalTypeAnnotation.DecimalLogicalTypeAnnotation;\nimport org.apache.parquet.schema.Type.Repetition;\n\n/** Utility methods for Delta schema to Parquet schema conversion. */\nclass ParquetSchemaUtils {\n\n  /**\n   * Constants that help if a Decimal type can be stored as INT32 or INT64 based on the precision.\n   * The maximum precision that can be stored in INT32 is 9 and in INT64 is 18. If the precision\n   * exceeds these values, then the DecimalType is stored as FIXED_LEN_BYTE_ARRAY.\n   */\n  public static final int DECIMAL_MAX_DIGITS_IN_INT = 9;\n\n  public static final int DECIMAL_MAX_DIGITS_IN_LONG = 18;\n\n  /**\n   * Maximum number of bytes required to store a decimal of a given precision as\n   * FIXED_LEN_BYTE_ARRAY in Parquet.\n   */\n  public static final List<Integer> MAX_BYTES_PER_PRECISION;\n\n  static {\n    List<Integer> maxBytesPerPrecision = new ArrayList<>();\n    for (int i = 0; i <= 38; i++) {\n      int numBytes = 1;\n      while (Math.pow(2.0, 8 * numBytes - 1) < Math.pow(10.0, i)) {\n        numBytes += 1;\n      }\n      maxBytesPerPrecision.add(numBytes);\n    }\n    MAX_BYTES_PER_PRECISION = Collections.unmodifiableList(maxBytesPerPrecision);\n  }\n\n  private ParquetSchemaUtils() {}\n\n  /**\n   * Given the file schema in Parquet file and selected columns by Delta, return a subschema of the\n   * file schema.\n   *\n   * @param fileSchema\n   * @param deltaType\n   * @return\n   */\n  static MessageType pruneSchema(\n      GroupType fileSchema /* parquet */, StructType deltaType /* delta-kernel */) {\n    return new MessageType(\"fileSchema\", pruneFields(fileSchema, deltaType));\n  }\n\n  /**\n   * Search for the Parquet type in {@code groupType} of subfield which is equivalent to given\n   * {@code field}.\n   *\n   * @param groupType Parquet group type coming from the file schema.\n   * @param field Sub field given as Delta Kernel's {@link StructField}\n   * @return {@link Type} of the Parquet field. Returns {@code null}, if not found.\n   */\n  static Type findSubFieldType(\n      GroupType groupType, StructField field, Map<Integer, Type> parquetFieldIdToTypeMap) {\n\n    // First search by the field id. If not found, search by case-sensitive name. Finally\n    // by the case-insensitive name.\n    // For Delta readers, no need to use the nested field ids added as part of icebergCompatV2\n    Optional<Integer> fieldId = getFieldId(field.getMetadata());\n    if (fieldId.isPresent()) {\n      Type subType = parquetFieldIdToTypeMap.get(fieldId.get());\n      if (subType != null) {\n        return subType;\n      }\n    }\n\n    final String columnName = field.getName();\n    if (groupType.containsField(columnName)) {\n      return groupType.getType(columnName);\n    }\n    // Parquet is case-sensitive, but the engine that generated the parquet file may not be.\n    // Check for direct match above but if no match found, try case-insensitive match.\n    for (Type type : groupType.getFields()) {\n      if (type.getName().equalsIgnoreCase(columnName)) {\n        return type;\n      }\n    }\n\n    return null;\n  }\n\n  /** Returns a map from field id to Parquet type for fields that have the field id set. */\n  static Map<Integer, Type> getParquetFieldToTypeMap(GroupType parquetGroupType) {\n    // Generate the field id to Parquet type map only if the read schema has field ids.\n    return parquetGroupType.getFields().stream()\n        .filter(subFieldType -> subFieldType.getId() != null)\n        .collect(\n            Collectors.toMap(\n                subFieldType -> subFieldType.getId().intValue(),\n                subFieldType -> subFieldType,\n                (u, v) -> {\n                  throw new IllegalStateException(\n                      format(\n                          \"Parquet file contains multiple columns \"\n                              + \"(%s, %s) with the same field id\",\n                          u, v));\n                }));\n  }\n\n  /**\n   * Convert the given Kernel schema to Parquet's schema\n   *\n   * @param structType Kernel schema object\n   * @return {@link MessageType} representing the schema in Parquet format.\n   */\n  static MessageType toParquetSchema(StructType structType) {\n    BiFunction<Optional<Integer>, Boolean, Optional<Integer>> fieldIdValidator =\n        createFieldIdValidator(structType);\n    List<Type> types = new ArrayList<>();\n    for (StructField structField : structType.fields()) {\n      Optional<Integer> fieldId =\n          fieldIdValidator.apply(\n              getFieldId(structField.getMetadata()), false /* isNestedFieldId */);\n      types.add(\n          toParquetType(\n              structField /* nearestAncestor with struct field */,\n              structField.getName() /* relativePath to nearestAncestor */,\n              structField.getDataType(),\n              structField.getName(),\n              structField.isNullable() ? OPTIONAL : REQUIRED,\n              fieldId,\n              fieldIdValidator));\n    }\n    return new MessageType(\"DefaultKernelSchema\", types);\n  }\n\n  private static List<Type> pruneFields(GroupType type, StructType deltaDataType) {\n    // prune fields including nested pruning like in pruneSchema\n    final Map<Integer, Type> parquetFieldIdToTypeMap = getParquetFieldToTypeMap(type);\n\n    return deltaDataType.fields().stream()\n        .map(\n            column -> {\n              Type subType = findSubFieldType(type, column, parquetFieldIdToTypeMap);\n              if (subType != null) {\n                return prunedType(subType, column.getDataType());\n              } else {\n                return null;\n              }\n            })\n        .filter(Objects::nonNull)\n        .collect(Collectors.toList());\n  }\n\n  private static Type prunedType(Type type, DataType deltaType) {\n    if (type instanceof GroupType && deltaType instanceof StructType) {\n      GroupType groupType = (GroupType) type;\n      StructType structType = (StructType) deltaType;\n      return groupType.withNewFields(pruneFields(groupType, structType));\n    } else if (type instanceof GroupType && deltaType instanceof ArrayType) {\n      GroupType arrayGroupType = (GroupType) type;\n      ArrayType deltaArrayType = (ArrayType) deltaType;\n      // For standard 3-level arrays, extract the element type and recursively prune it\n      Type elementType = validateAndGetThreeLevelParquetArrayElementType(arrayGroupType);\n      Type listField = arrayGroupType.getType(0);\n      GroupType listGroup = (GroupType) listField;\n\n      GroupType newListGroup =\n          listGroup.withNewFields(\n              Collections.singletonList(prunedType(elementType, deltaArrayType.getElementType())));\n      return arrayGroupType.withNewFields(Collections.singletonList(newListGroup));\n      // TODO: check if we need to fix map type.\n    } else {\n      return type;\n    }\n  }\n\n  /**\n   * Validates and extracts the element type from a 3-level Parquet array type.\n   *\n   * <p>According to Parquet specification, all arrays (including primitive arrays) should use the\n   * standard 3-level structure:\n   *\n   * <pre>\n   * optional group array_field (LIST) {\n   *   repeated group list {\n   *     optional <element-type> element;\n   *   }\n   * }\n   * </pre>\n   *\n   * Examples: - For array of int:\n   *\n   * <pre>\n   * optional group array_field (LIST) {\n   *   repeated group list {\n   *     optional int32 element;\n   *   }\n   * }\n   * </pre>\n   *\n   * - For array of struct{x:int, y:string};:\n   *\n   * <pre>\n   * optional group array_field (LIST) {\n   *   repeated group list {\n   *     optional group element {\n   *       optional int32 x;\n   *       optional binary y (STRING);\n   *     }\n   *   }\n   * }\n   * </pre>\n   *\n   * @param arrayGroupType The group type of the array, which must be a LIST.\n   * @return The Parquet type of the array element.\n   * @throws IllegalArgumentException if the structure doesn't follow 3-level format.\n   */\n  public static Type validateAndGetThreeLevelParquetArrayElementType(GroupType arrayGroupType) {\n    checkArgument(\n        arrayGroupType.getFieldCount() == 1,\n        \"In Parquet's 3-level structure, group type must only contain one sub field: %s\",\n        arrayGroupType);\n\n    Type listField = arrayGroupType.getType(0);\n    checkArgument(\n        listField.isRepetition(Type.Repetition.REPEATED),\n        \"Array list field must be repeated: %s\",\n        listField);\n\n    // Ensure this is a proper 3-level structure\n    checkArgument(\n        listField instanceof GroupType,\n        \"Expected 3-level array structure with repeated group, but got: %s\",\n        listField);\n\n    // array_field.list\n    GroupType listGroup = (GroupType) listField;\n    checkArgument(\n        listGroup.getFieldCount() == 1,\n        \"Array list group must have exactly one element: %s\",\n        listGroup);\n\n    // array_field.list.element\n    return listGroup.getType(0);\n  }\n\n  /**\n   * Converts a Delta type {@code dataType} to a Parquet type.\n   *\n   * @param nearestAncestor The nearest ancestor with a {@link StructField}. This ancestor\n   *     represents the current node or nearest node on the path to the current node from root of\n   *     the schema.\n   * @param relativePath The relative path to this element from {@code nearestAncestor}. For\n   *     example, consider a column type {@code col1 STRUCT(a INT, b STRUCT(c INT, d ARRAY(INT)))}.\n   *     The absolute path to the nested {@code element} field of the list is col1.b.d.element,\n   *     while the relative path is d.element, i.e., relative to the nearest ancestor with a struct\n   *     field.\n   * @param dataType The Delta type to be converted to a Parquet type.\n   * @param name The name of the field.\n   * @param repetition The {@link Repetition} of the field.\n   * @param fieldId The field ID of the field. If present, the field ID is added to the Parquet\n   *     type.\n   * @return The Parquet type representing the given Delta type.\n   */\n  private static Type toParquetType(\n      StructField nearestAncestor,\n      String relativePath,\n      DataType dataType,\n      String name,\n      Repetition repetition,\n      Optional<Integer> fieldId,\n      BiFunction<Optional<Integer>, Boolean, Optional<Integer>> fieldIdValidator) {\n    Type type;\n    if (dataType instanceof BooleanType) {\n      type = primitive(BOOLEAN, repetition).named(name);\n    } else if (dataType instanceof ByteType\n        || dataType instanceof ShortType\n        || dataType instanceof IntegerType) {\n      type = primitive(INT32, repetition).named(name);\n    } else if (dataType instanceof LongType) {\n      type = primitive(INT64, repetition).named(name);\n    } else if (dataType instanceof FloatType) {\n      type = primitive(FLOAT, repetition).named(name);\n    } else if (dataType instanceof DoubleType) {\n      type = primitive(DOUBLE, repetition).named(name);\n    } else if (dataType instanceof DecimalType) {\n      DecimalType decimalType = (DecimalType) dataType;\n      int precision = decimalType.getPrecision();\n      int scale = decimalType.getScale();\n      // DecimalType constructor already has checks to make sure the precision and scale are\n      // within the valid range. No need to check them again.\n\n      DecimalLogicalTypeAnnotation decimalAnnotation = decimalType(scale, precision);\n      if (precision <= DECIMAL_MAX_DIGITS_IN_INT) {\n        type = primitive(INT32, repetition).as(decimalAnnotation).named(name);\n      } else if (precision <= DECIMAL_MAX_DIGITS_IN_LONG) {\n        type = primitive(INT64, repetition).as(decimalAnnotation).named(name);\n      } else {\n        type =\n            primitive(FIXED_LEN_BYTE_ARRAY, repetition)\n                .as(decimalAnnotation)\n                .length(MAX_BYTES_PER_PRECISION.get(precision))\n                .named(name);\n      }\n    } else if (dataType instanceof StringType) {\n      type = primitive(BINARY, repetition).as(LogicalTypeAnnotation.stringType()).named(name);\n    } else if (dataType instanceof BinaryType) {\n      type = primitive(BINARY, repetition).named(name);\n    } else if (dataType instanceof DateType) {\n      type = primitive(INT32, repetition).as(LogicalTypeAnnotation.dateType()).named(name);\n    } else if (dataType instanceof TimestampType) {\n      // Kernel is by default going to write as INT64 with isAdjustedToUTC set to true\n      // Delta-Spark writes as INT96 for legacy reasons (maintaining compatibility with\n      // unknown consumers with very, very old versions of Parquet reader). Kernel is a new\n      // project, and we are ok if it breaks readers (we use this opportunity to find such\n      // readers and ask them to upgrade).\n      type =\n          primitive(INT64, repetition)\n              .as(timestampType(true /* isAdjustedToUTC */, MICROS))\n              .named(name);\n    } else if (dataType instanceof TimestampNTZType) {\n      // Write as INT64 with isAdjustedToUTC set to false\n      type =\n          primitive(INT64, repetition)\n              .as(timestampType(false /* isAdjustedToUTC */, MICROS))\n              .named(name);\n    } else if (dataType instanceof ArrayType) {\n      type =\n          toParquetArrayType(\n              nearestAncestor,\n              relativePath,\n              (ArrayType) dataType,\n              name,\n              repetition,\n              fieldIdValidator);\n    } else if (dataType instanceof MapType) {\n      type =\n          toParquetMapType(\n              nearestAncestor,\n              relativePath,\n              (MapType) dataType,\n              name,\n              repetition,\n              fieldIdValidator);\n    } else if (dataType instanceof StructType) {\n      type = toParquetStructType((StructType) dataType, name, repetition, fieldIdValidator);\n    } else {\n      throw new UnsupportedOperationException(\n          \"Writing given type data to Parquet is not supported: \" + dataType);\n    }\n\n    if (fieldId.isPresent()) {\n      // Add field id to the type.\n      type = type.withId(fieldId.get());\n    }\n    return type;\n  }\n\n  private static Type toParquetArrayType(\n      StructField nearestAncestor,\n      String relativePath,\n      ArrayType arrayType,\n      String name,\n      Repetition rep,\n      BiFunction<Optional<Integer>, Boolean, Optional<Integer>> fieldIdValidator) {\n    // We will be supporting the 3-level array structure only. 2-level array structures are\n    // a very old legacy versions of Parquet which Kernel doesn't support writing as.\n\n    String elementRelativePath = relativePath + \".element\";\n    Optional<Integer> fieldId =\n        fieldIdValidator.apply(\n            getNestedFieldId(nearestAncestor, elementRelativePath), true /* isNestedFieldId */);\n    return Types.buildGroup(rep)\n        .as(LogicalTypeAnnotation.listType())\n        .addField(\n            Types.repeatedGroup()\n                .addField(\n                    toParquetType(\n                        nearestAncestor,\n                        elementRelativePath,\n                        arrayType.getElementType(),\n                        \"element\", /* name */\n                        arrayType.containsNull() ? OPTIONAL : REQUIRED,\n                        fieldId,\n                        fieldIdValidator))\n                .named(\"list\"))\n        .named(name);\n  }\n\n  private static Type toParquetMapType(\n      StructField nearestAncestor,\n      String relativePath,\n      MapType mapType,\n      String name,\n      Repetition repetition,\n      BiFunction<Optional<Integer>, Boolean, Optional<Integer>> fieldIdValidator) {\n    // We will be supporting the 3-level map structure only. 2-level map structures are\n    // a very old legacy versions of Parquet which Kernel doesn't support writing as.\n\n    String keyRelativePath = relativePath + \".key\";\n    String valueRelativePath = relativePath + \".value\";\n    Optional<Integer> keyFieldId =\n        fieldIdValidator.apply(\n            getNestedFieldId(nearestAncestor, keyRelativePath), true /* isNestedFieldId */);\n    Optional<Integer> valueFieldId =\n        fieldIdValidator.apply(\n            getNestedFieldId(nearestAncestor, valueRelativePath), true /* isNestedFieldId */);\n\n    return Types.buildGroup(repetition)\n        .as(LogicalTypeAnnotation.mapType())\n        .addField(\n            Types.repeatedGroup()\n                .addField(\n                    toParquetType(\n                        nearestAncestor,\n                        keyRelativePath,\n                        mapType.getKeyType(),\n                        \"key\", /* name */\n                        REQUIRED, /* repetition */\n                        keyFieldId,\n                        fieldIdValidator))\n                .addField(\n                    toParquetType(\n                        nearestAncestor,\n                        valueRelativePath,\n                        mapType.getValueType(),\n                        \"value\", /* name */\n                        mapType.isValueContainsNull() ? OPTIONAL : REQUIRED,\n                        valueFieldId,\n                        fieldIdValidator))\n                .named(\"key_value\"))\n        .named(name);\n  }\n\n  private static Type toParquetStructType(\n      StructType structType,\n      String name,\n      Repetition repetition,\n      BiFunction<Optional<Integer>, Boolean, Optional<Integer>> fieldIdValidator) {\n    List<Type> fields = new ArrayList<>();\n    for (StructField field : structType.fields()) {\n      Optional<Integer> fieldId =\n          fieldIdValidator.apply(getFieldId(field.getMetadata()), false /* isNestedFieldId */);\n      fields.add(\n          toParquetType(\n              field, /* nearestAncestor with struct field */\n              field.getName(), /* relativePath to nearestAncestor */\n              field.getDataType(),\n              field.getName(),\n              field.isNullable() ? OPTIONAL : REQUIRED,\n              fieldId,\n              fieldIdValidator));\n    }\n    return new GroupType(repetition, name, fields);\n  }\n\n  private static Optional<Integer> getFieldId(FieldMetadata fieldMetadata) {\n    return getFieldId(fieldMetadata, ColumnMapping.PARQUET_FIELD_ID_KEY);\n  }\n\n  private static Optional<Integer> getNestedFieldId(\n      StructField field, String nestedFieldRelativePath) {\n    FieldMetadata nestedFieldIDMetadata =\n        field.getMetadata().getMetadata(PARQUET_FIELD_NESTED_IDS_METADATA_KEY);\n    if (nestedFieldIDMetadata != null) {\n      return getFieldId(nestedFieldIDMetadata, nestedFieldRelativePath);\n    }\n    return Optional.empty();\n  }\n\n  private static Optional<Integer> getFieldId(FieldMetadata fieldMetadata, String fieldIdKey) {\n    // Field id delta schema metadata is deserialized as long, but the range should always\n    // be within integer range.\n    return Optional.ofNullable(fieldMetadata.getLong(fieldIdKey)).map(Math::toIntExact);\n  }\n\n  /**\n   * Validator for checking that the field ids in the schema are valid. Any visitor that recursively\n   * visits the fields in the schema {@link StructType} can call this validator to check:\n   *\n   * <ul>\n   *   <li>Field ids should be unique within the schema.\n   *   <li>Field ids should be non-negative.\n   *   <li>All {@link StructField} should have a field id or none should have it\n   *   <li>All nested elements of {@link ArrayType} and {@link MapType} should have the nested field\n   *       id or none of them should have.\n   * </ul>\n   *\n   * @param structType The schema to validate. Used for error messages.\n   * @return A lambda that can be used to validate the field ids. It takes the field id and a\n   *     boolean (true for nested field ids). Since the lamda is stateful, it should be used only\n   *     once for each unique schema and traversal.\n   */\n  private static BiFunction<Optional<Integer>, Boolean, Optional<Integer>> createFieldIdValidator(\n      StructType structType) {\n    Set<Integer> fieldIds = new HashSet<>(); // include nested field ids too.\n    AtomicBoolean seenFieldWithNoId = new AtomicBoolean();\n    AtomicBoolean seenFieldWithId = new AtomicBoolean();\n    AtomicBoolean seenNestedFieldWithId = new AtomicBoolean();\n    AtomicBoolean seemNestedWithNoId = new AtomicBoolean();\n\n    return (fieldIdOpt, isNestedFieldId) -> {\n      if (fieldIdOpt.isPresent()) {\n        checkArgument(fieldIdOpt.get() >= 0, \"Field id should be non-negative.\\n%s\", structType);\n        checkArgument(fieldIds.add(fieldIdOpt.get()), \"Field id should be unique.\\n%s\", structType);\n\n        if (isNestedFieldId) {\n          seenNestedFieldWithId.set(true);\n          checkArgument(\n              !seemNestedWithNoId.get() && !seenFieldWithNoId.get(),\n              \"Some of the fields are missing field ids.\\n%s\",\n              structType);\n        } else {\n          seenFieldWithId.set(true);\n          checkArgument(\n              !seenFieldWithNoId.get(),\n              \"Some of the fields are missing field ids.\\n%s\",\n              structType);\n        }\n      } else {\n        if (isNestedFieldId) {\n          seemNestedWithNoId.set(true);\n          checkArgument(\n              !seenNestedFieldWithId.get() && !seenFieldWithId.get(),\n              \"Some of the fields are missing field ids.\\n%s\",\n              structType);\n        } else {\n          seenFieldWithNoId.set(true);\n          checkArgument(\n              !seenFieldWithId.get(), \"Some of the fields are missing field ids.\\n%s\", structType);\n        }\n      }\n      return fieldIdOpt;\n    };\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/ParquetStatsReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.DefaultKernelUtils.getDataType;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.function.UnaryOperator.identity;\nimport static org.apache.hadoop.shaded.com.google.common.collect.ImmutableMap.toImmutableMap;\n\nimport io.delta.kernel.defaults.engine.fileio.InputFile;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.statistics.DataFileStatistics;\nimport io.delta.kernel.types.*;\nimport java.io.IOException;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.util.*;\nimport org.apache.hadoop.shaded.com.google.common.collect.ImmutableMultimap;\nimport org.apache.hadoop.shaded.com.google.common.collect.Multimap;\nimport org.apache.parquet.column.statistics.*;\nimport org.apache.parquet.format.converter.ParquetMetadataConverter;\nimport org.apache.parquet.hadoop.ParquetFileReader;\nimport org.apache.parquet.hadoop.metadata.BlockMetaData;\nimport org.apache.parquet.hadoop.metadata.ColumnChunkMetaData;\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata;\nimport org.apache.parquet.schema.LogicalTypeAnnotation;\nimport org.apache.parquet.schema.LogicalTypeAnnotation.DecimalLogicalTypeAnnotation;\n\n/** Helper class to read statistics from Parquet files. */\npublic class ParquetStatsReader {\n  /**\n   * Read the statistics for the given Parquet file.\n   *\n   * @param kernelInputFile {@link InputFile} representing the Parquet file.\n   * @param dataSchema The schema of the Parquet file. Type info is used to decode statistics.\n   * @param statsColumns The columns for which statistics should be collected and returned.\n   * @return File/column level statistics as {@link DataFileStatistics} instance.\n   */\n  public static DataFileStatistics readDataFileStatistics(\n      InputFile kernelInputFile, StructType dataSchema, List<Column> statsColumns)\n      throws IOException {\n    // Read the Parquet footer to compute the statistics\n    org.apache.parquet.io.InputFile parquetFile =\n        ParquetIOUtils.createParquetInputFile(kernelInputFile);\n    ParquetMetadata footer =\n        ParquetFileReader.readFooter(parquetFile, ParquetMetadataConverter.NO_FILTER);\n    ImmutableMultimap.Builder<Column, ColumnChunkMetaData> metadataForColumn =\n        ImmutableMultimap.builder();\n\n    long rowCount = 0;\n    for (BlockMetaData blockMetaData : footer.getBlocks()) {\n      rowCount += blockMetaData.getRowCount();\n      for (ColumnChunkMetaData columnChunkMetaData : blockMetaData.getColumns()) {\n        Column column = new Column(columnChunkMetaData.getPath().toArray());\n        metadataForColumn.put(column, columnChunkMetaData);\n      }\n    }\n\n    return constructFileStats(metadataForColumn.build(), dataSchema, statsColumns, rowCount);\n  }\n\n  /**\n   * Merge statistics from multiple rowgroups into a single set of statistics for each column.\n   *\n   * @return Stats for each column in the file as {@link DataFileStatistics}.\n   */\n  private static DataFileStatistics constructFileStats(\n      Multimap<Column, ColumnChunkMetaData> metadataForColumn,\n      StructType dataSchema,\n      List<Column> statsColumns,\n      long rowCount) {\n    Map<Column, Optional<Statistics<?>>> statsForColumn =\n        metadataForColumn.keySet().stream()\n            .collect(\n                toImmutableMap(identity(), key -> mergeMetadataList(metadataForColumn.get(key))));\n\n    Map<Column, Literal> minValues = new HashMap<>();\n    Map<Column, Literal> maxValues = new HashMap<>();\n    Map<Column, Long> nullCounts = new HashMap<>();\n    for (Column statsColumn : statsColumns) {\n      Optional<Statistics<?>> stats = statsForColumn.get(statsColumn);\n      DataType columnType = getDataType(dataSchema, statsColumn);\n      if (stats == null || !stats.isPresent() || !isStatsSupportedDataType(columnType)) {\n        continue;\n      }\n      Statistics<?> statistics = stats.get();\n\n      Long numNulls = statistics.isNumNullsSet() ? statistics.getNumNulls() : null;\n      nullCounts.put(statsColumn, numNulls);\n\n      if (numNulls != null && rowCount == numNulls) {\n        // If all values are null, then min and max are also null\n        minValues.put(statsColumn, Literal.ofNull(columnType));\n        maxValues.put(statsColumn, Literal.ofNull(columnType));\n        continue;\n      }\n\n      Literal minValue = decodeMinMaxStat(columnType, statistics, true /* decodeMin */);\n      minValues.put(statsColumn, minValue);\n\n      Literal maxValue = decodeMinMaxStat(columnType, statistics, false /* decodeMin */);\n      maxValues.put(statsColumn, maxValue);\n    }\n\n    return new DataFileStatistics(rowCount, minValues, maxValues, nullCounts, Optional.empty());\n  }\n\n  private static Literal decodeMinMaxStat(\n      DataType dataType, Statistics<?> statistics, boolean decodeMin) {\n    Object statValue = decodeMin ? statistics.genericGetMin() : statistics.genericGetMax();\n    if (statValue == null) {\n      return null;\n    }\n\n    if (dataType instanceof BooleanType) {\n      return Literal.ofBoolean((Boolean) statValue);\n    } else if (dataType instanceof ByteType) {\n      return Literal.ofByte(((Number) statValue).byteValue());\n    } else if (dataType instanceof ShortType) {\n      return Literal.ofShort(((Number) statValue).shortValue());\n    } else if (dataType instanceof IntegerType) {\n      return Literal.ofInt(((Number) statValue).intValue());\n    } else if (dataType instanceof LongType) {\n      return Literal.ofLong(((Number) statValue).longValue());\n    } else if (dataType instanceof FloatType) {\n      return Literal.ofFloat(((Number) statValue).floatValue());\n    } else if (dataType instanceof DoubleType) {\n      return Literal.ofDouble(((Number) statValue).doubleValue());\n    } else if (dataType instanceof DecimalType) {\n      LogicalTypeAnnotation logicalType = statistics.type().getLogicalTypeAnnotation();\n      checkArgument(\n          logicalType instanceof DecimalLogicalTypeAnnotation,\n          \"Physical decimal column has invalid Parquet Logical Type: %s\",\n          logicalType);\n      int scale = ((DecimalLogicalTypeAnnotation) logicalType).getScale();\n\n      DecimalType decimalType = (DecimalType) dataType;\n\n      // Check the scale is same in both the Delta data type and the Parquet Logical Type\n      checkArgument(\n          scale == decimalType.getScale(),\n          \"Physical decimal type has different scale than the logical type: %s\",\n          scale);\n\n      // Decimal is stored either as int, long or binary. Decode the stats accordingly.\n      BigDecimal decimalStatValue;\n      if (statistics instanceof IntStatistics) {\n        decimalStatValue = BigDecimal.valueOf((Integer) statValue).movePointLeft(scale);\n      } else if (statistics instanceof LongStatistics) {\n        decimalStatValue = BigDecimal.valueOf((Long) statValue).movePointLeft(scale);\n      } else if (statistics instanceof BinaryStatistics) {\n        BigInteger base = new BigInteger(getBinaryStat(statistics, decodeMin));\n        decimalStatValue = new BigDecimal(base, scale);\n      } else {\n        throw new UnsupportedOperationException(\n            \"Unsupported stats type for Decimal: \" + statistics.getClass());\n      }\n      return Literal.ofDecimal(\n          decimalStatValue, decimalType.getPrecision(), decimalType.getScale());\n    } else if (dataType instanceof DateType) {\n      checkArgument(\n          statistics instanceof IntStatistics,\n          \"Column with DATE type contained invalid statistics: %s\",\n          statistics);\n      return Literal.ofDate((Integer) statValue); // stats are stored as epoch days in Parquet\n    } else if (dataType instanceof TimestampType) {\n      // Kernel Parquet writer always writes timestamps in INT64 format\n      checkArgument(\n          statistics instanceof LongStatistics,\n          \"Column with TIMESTAMP type contained invalid statistics: %s\",\n          statistics);\n      return Literal.ofTimestamp((Long) statValue);\n    } else if (dataType instanceof TimestampNTZType) {\n      checkArgument(\n          statistics instanceof LongStatistics,\n          \"Column with TIMESTAMP_NTZ type contained invalid statistics: %s\",\n          statistics);\n      return Literal.ofTimestampNtz((Long) statValue);\n    } else if (dataType instanceof StringType) {\n      byte[] binaryStat = getBinaryStat(statistics, decodeMin);\n      return Literal.ofString(new String(binaryStat, UTF_8));\n    } else if (dataType instanceof BinaryType) {\n      return Literal.ofBinary(getBinaryStat(statistics, decodeMin));\n    }\n\n    throw new IllegalArgumentException(\"Unsupported stats data type: \" + statValue);\n  }\n\n  private static Optional<Statistics<?>> mergeMetadataList(\n      Collection<ColumnChunkMetaData> metadataList) {\n    if (hasInvalidStatistics(metadataList)) {\n      return Optional.empty();\n    }\n\n    return metadataList.stream()\n        .<Statistics<?>>map(ColumnChunkMetaData::getStatistics)\n        .reduce(\n            (statsA, statsB) -> {\n              statsA.mergeStatistics(statsB);\n              return statsA;\n            });\n  }\n\n  private static boolean hasInvalidStatistics(Collection<ColumnChunkMetaData> metadataList) {\n    // If any row group does not have stats collected, stats for the file will not be valid\n    return metadataList.stream()\n        .anyMatch(\n            metadata -> {\n              Statistics<?> stats = metadata.getStatistics();\n              if (stats == null || stats.isEmpty() || !stats.isNumNullsSet()) {\n                return true;\n              }\n\n              // Columns with NaN values are marked by `hasNonNullValue` = false by the Parquet\n              // reader\n              // See issue: https://issues.apache.org/jira/browse/PARQUET-1246\n              return !stats.hasNonNullValue() && stats.getNumNulls() != metadata.getValueCount();\n            });\n  }\n\n  private static boolean isStatsSupportedDataType(DataType dataType) {\n    return dataType instanceof BooleanType\n        || dataType instanceof ByteType\n        || dataType instanceof ShortType\n        || dataType instanceof IntegerType\n        || dataType instanceof LongType\n        || dataType instanceof FloatType\n        || dataType instanceof DoubleType\n        || dataType instanceof DecimalType\n        || dataType instanceof DateType\n        || dataType instanceof TimestampType\n        || dataType instanceof TimestampNTZType\n        || dataType instanceof StringType\n        || dataType instanceof BinaryType;\n  }\n\n  private static byte[] getBinaryStat(Statistics<?> statistics, boolean decodeMin) {\n    return decodeMin ? statistics.getMinBytes() : statistics.getMaxBytes();\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/RepeatedValueConverter.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetColumnReaders.initNullabilityVector;\nimport static io.delta.kernel.defaults.internal.parquet.ParquetColumnReaders.setNullabilityToTrue;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.parquet.ParquetColumnReaders.BaseColumnReader;\nimport java.util.Arrays;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.io.api.GroupConverter;\n\n/**\n * Abstract implementation of Parquet converters for capturing the repeated types such as list or\n * map.\n */\nabstract class RepeatedValueConverter extends GroupConverter implements BaseColumnReader {\n  private final Collector collector;\n\n  // working state\n  private int currentRowIndex;\n  private boolean[] nullability;\n  private int[] offsets;\n  // If the repeated value is null, start/end never get called which is a signal for null\n  // Set the initial state to true and when start() is called set it to false.\n  private boolean isCurrentValueNull = true;\n\n  /**\n   * Create instance.\n   *\n   * @param initialBatchSize Starting batch output size.\n   * @param elementConverters List of converters that are part of the repeated type.\n   */\n  RepeatedValueConverter(int initialBatchSize, Converter... elementConverters) {\n    checkArgument(initialBatchSize > 0, \"invalid initialBatchSize: %s\", initialBatchSize);\n    this.collector = new Collector(elementConverters);\n    // initialize working state\n    this.nullability = initNullabilityVector(initialBatchSize);\n    this.offsets = new int[initialBatchSize + 1];\n  }\n\n  @Override\n  public Converter getConverter(int fieldIndex) {\n    if (fieldIndex != 0) {\n      throw new IllegalArgumentException(\"Invalid field index: \" + fieldIndex);\n    }\n    return collector;\n  }\n\n  @Override\n  public void start() {\n    this.isCurrentValueNull = false;\n  }\n\n  @Override\n  public void end() {}\n\n  @Override\n  public void finalizeCurrentRow(long currentRowIndex) {\n    resizeIfNeeded();\n    this.offsets[this.currentRowIndex + 1] = collector.currentEntryIndex;\n    this.nullability[this.currentRowIndex] = isCurrentValueNull;\n    this.isCurrentValueNull = true;\n\n    this.currentRowIndex++;\n  }\n\n  @Override\n  public void resizeIfNeeded() {\n    if (nullability.length == currentRowIndex) {\n      int newSize = nullability.length * 2;\n      this.nullability = Arrays.copyOf(this.nullability, newSize);\n      setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n\n      this.offsets = Arrays.copyOf(this.offsets, newSize + 1);\n    }\n  }\n\n  @Override\n  public void resetWorkingState() {\n    this.currentRowIndex = 0;\n    this.isCurrentValueNull = true;\n    this.nullability = initNullabilityVector(nullability.length);\n    this.offsets = new int[offsets.length];\n  }\n\n  protected boolean[] getNullability() {\n    return nullability;\n  }\n\n  protected int[] getOffsets() {\n    return offsets;\n  }\n\n  /**\n   * @return the {@link ColumnVector}s from the underlying element vectors. Once retrieved the\n   *     converters are reset and requires {@link #resetWorkingState()} before using this repeated\n   *     converter again.\n   */\n  protected ColumnVector[] getElementDataVectors() {\n    return collector.getDataVectors();\n  }\n\n  /**\n   * GroupConverter to collect repeated elements. For each repeated element value set, the call\n   * pattern from the Parquet reader: start(), followed by value read for each element converter and\n   * end().\n   */\n  private static class Collector extends GroupConverter {\n    private final Converter[] elementConverters;\n\n    // working state\n    private int currentEntryIndex;\n\n    Collector(Converter... elementConverters) {\n      this.elementConverters = elementConverters;\n    }\n\n    @Override\n    public Converter getConverter(int fieldIndex) {\n      if (fieldIndex < 0 || fieldIndex >= elementConverters.length) {\n        throw new IllegalArgumentException(\"Invalid field index: \" + fieldIndex);\n      }\n      return elementConverters[fieldIndex];\n    }\n\n    @Override\n    public void start() {}\n\n    @Override\n    public void end() {\n      for (Converter converter : elementConverters) {\n        long prevRowIndex = -1; // Row indexes are not needed for nested columns\n        ((BaseColumnReader) converter).finalizeCurrentRow(prevRowIndex);\n      }\n      currentEntryIndex++;\n    }\n\n    ColumnVector[] getDataVectors() {\n      ColumnVector[] dataVectors = new ColumnVector[elementConverters.length];\n      for (int i = 0; i < elementConverters.length; i++) {\n        dataVectors[i] =\n            ((BaseColumnReader) elementConverters[i]).getDataColumnVector(currentEntryIndex);\n      }\n      currentEntryIndex = 0;\n      return dataVectors;\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/RowColumnReader.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.defaults.internal.parquet.ParquetSchemaUtils.findSubFieldType;\nimport static io.delta.kernel.defaults.internal.parquet.ParquetSchemaUtils.getParquetFieldToTypeMap;\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch;\nimport io.delta.kernel.defaults.internal.data.vector.DefaultStructVector;\nimport io.delta.kernel.types.*;\nimport java.util.*;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.io.api.GroupConverter;\nimport org.apache.parquet.schema.GroupType;\nimport org.apache.parquet.schema.Type;\n\n/**\n * Row column readers for materializing the column values from Parquet files into Kernels {@link\n * ColumnVector}.\n */\nclass RowColumnReader extends GroupConverter implements ParquetColumnReaders.BaseColumnReader {\n  private final StructType readSchema;\n  private final Converter[] converters;\n  // The delta may request columns that don't exists in Parquet\n  // This map is to track the ordinal known to Parquet reader to the converter array ordinal.\n  // If a column is missing, a dummy converter added to the `converters` array and which\n  // generates all null vector at the end.\n  private final Map<Integer, Integer> parquetOrdinalToConverterOrdinal;\n\n  // Working state\n  private boolean isCurrentValueNull = true;\n  private int currentRowIndex;\n  private boolean[] nullability;\n\n  /**\n   * Create converter for {@link StructType} column.\n   *\n   * @param initialBatchSize Estimate of initial row batch size. Used in memory allocations.\n   * @param readSchema Schem of the columns to read from the file.\n   * @param fileSchema Schema of the pruned columns from the file schema We have some necessary\n   *     requirements here: (1) the fields in fileSchema are a subset of readSchema (parquet schema\n   *     has been pruned). (2) the fields in fileSchema are in the same order as the corresponding\n   *     fields in readSchema.\n   */\n  RowColumnReader(int initialBatchSize, StructType readSchema, GroupType fileSchema) {\n    checkArgument(initialBatchSize > 0, \"invalid initialBatchSize: %s\", initialBatchSize);\n    this.readSchema = requireNonNull(readSchema, \"readSchema is not null\");\n    List<StructField> fields = readSchema.fields();\n    this.converters = new Converter[fields.size()];\n    this.parquetOrdinalToConverterOrdinal = new HashMap<>();\n\n    // Initialize the working state\n    this.nullability = ParquetColumnReaders.initNullabilityVector(initialBatchSize);\n\n    int parquetOrdinal = 0;\n    for (int i = 0; i < converters.length; i++) {\n      final StructField field = fields.get(i);\n      final DataType typeFromClient = field.getDataType();\n      final Map<Integer, Type> parquetFieldIdToTypeMap = getParquetFieldToTypeMap(fileSchema);\n      final Type typeFromFile =\n          field.isDataColumn()\n              ? findSubFieldType(fileSchema, field, parquetFieldIdToTypeMap)\n              : null;\n      if (typeFromFile == null) {\n        if (MetadataColumnSpec.ROW_INDEX.equals(field.getMetadataColumnSpec())) {\n          checkArgument(\n              field.getDataType() instanceof LongType,\n              \"row index metadata column must be type long\");\n          converters[i] = new ParquetColumnReaders.FileRowIndexColumnReader(initialBatchSize);\n        } else {\n          converters[i] = new ParquetColumnReaders.NonExistentColumnReader(typeFromClient);\n        }\n      } else {\n        converters[i] =\n            ParquetColumnReaders.createConverter(initialBatchSize, typeFromClient, typeFromFile);\n        parquetOrdinalToConverterOrdinal.put(parquetOrdinal, i);\n        parquetOrdinal++;\n      }\n    }\n  }\n\n  @Override\n  public Converter getConverter(int fieldIndex) {\n    return converters[parquetOrdinalToConverterOrdinal.get(fieldIndex)];\n  }\n\n  @Override\n  public void start() {\n    isCurrentValueNull = false;\n  }\n\n  @Override\n  public void end() {}\n\n  public ColumnarBatch getDataAsColumnarBatch(int batchSize) {\n    ColumnVector[] memberVectors = collectMemberVectors(batchSize);\n    ColumnarBatch batch = new DefaultColumnarBatch(batchSize, readSchema, memberVectors);\n    resetWorkingState();\n    return batch;\n  }\n\n  @Override\n  public void finalizeCurrentRow(long currentRowIndex) {\n    resizeIfNeeded();\n    finalizeLastRowInConverters(currentRowIndex);\n    nullability[this.currentRowIndex] = isCurrentValueNull;\n    isCurrentValueNull = true;\n\n    this.currentRowIndex++;\n  }\n\n  public ColumnVector getDataColumnVector(int batchSize) {\n    ColumnVector[] memberVectors = collectMemberVectors(batchSize);\n    ColumnVector vector =\n        new DefaultStructVector(batchSize, readSchema, Optional.of(nullability), memberVectors);\n    resetWorkingState();\n    return vector;\n  }\n\n  @Override\n  public void resizeIfNeeded() {\n    if (nullability.length == currentRowIndex) {\n      int newSize = nullability.length * 2;\n      this.nullability = Arrays.copyOf(this.nullability, newSize);\n      ParquetColumnReaders.setNullabilityToTrue(this.nullability, newSize / 2, newSize);\n    }\n  }\n\n  @Override\n  public void resetWorkingState() {\n    this.currentRowIndex = 0;\n    this.isCurrentValueNull = true;\n    this.nullability = ParquetColumnReaders.initNullabilityVector(this.nullability.length);\n  }\n\n  private void finalizeLastRowInConverters(long prevRowIndex) {\n    for (int i = 0; i < converters.length; i++) {\n      ((ParquetColumnReaders.BaseColumnReader) converters[i]).finalizeCurrentRow(prevRowIndex);\n    }\n  }\n\n  private ColumnVector[] collectMemberVectors(int batchSize) {\n    final ColumnVector[] output = new ColumnVector[converters.length];\n\n    for (int i = 0; i < converters.length; i++) {\n      output[i] =\n          ((ParquetColumnReaders.BaseColumnReader) converters[i]).getDataColumnVector(batchSize);\n    }\n\n    return output;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/parquet/TimestampConverters.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT64;\nimport static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT96;\n\nimport io.delta.kernel.defaults.internal.DefaultKernelUtils;\nimport io.delta.kernel.types.*;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.time.ZoneOffset;\nimport org.apache.parquet.io.api.Binary;\nimport org.apache.parquet.io.api.Converter;\nimport org.apache.parquet.schema.*;\nimport org.apache.parquet.schema.LogicalTypeAnnotation.TimestampLogicalTypeAnnotation;\n\n/**\n * Column readers for columns of types {@code timestmap, timestamp_ntz}. These both data types share\n * the same encoding methods except the logical type.\n */\npublic class TimestampConverters {\n\n  /**\n   * Create a {@code timestamp} column type reader\n   *\n   * @param initialBatchSize Initial batch size of the generated column vector\n   * @param typeFromFile Column type metadata from Parquet file\n   * @param typeFromClient Column type from client\n   * @return instance of {@link Converter}\n   */\n  public static Converter createTimestampConverter(\n      int initialBatchSize, Type typeFromFile, DataType typeFromClient) {\n    PrimitiveType primType = typeFromFile.asPrimitiveType();\n    LogicalTypeAnnotation typeAnnotation = primType.getLogicalTypeAnnotation();\n    boolean isTimestampTz = (typeFromClient instanceof TimestampType);\n\n    if (primType.getPrimitiveTypeName() == INT96) {\n      // INT96 does not have a logical type in both TIMESTAMP and TIMESTAMP_NTZ\n      // Also, TimestampNTZ type does not require rebasing\n      // due to its lack of time zone context.\n      return new TimestampBinaryConverter(typeFromClient, initialBatchSize);\n    } else if (primType.getPrimitiveTypeName() == INT64\n        && typeAnnotation instanceof TimestampLogicalTypeAnnotation) {\n      TimestampLogicalTypeAnnotation timestamp = (TimestampLogicalTypeAnnotation) typeAnnotation;\n\n      boolean isAdjustedUtc = timestamp.isAdjustedToUTC();\n      if (!((isTimestampTz && isAdjustedUtc) || (!isTimestampTz && !isAdjustedUtc))) {\n        throw new RuntimeException(\n            String.format(\n                \"Incompatible Utc adjustment for timestamp column. \"\n                    + \"Client type: %s, File type: %s, isAdjustedUtc: %s\",\n                typeFromClient, typeFromFile, isAdjustedUtc));\n      }\n\n      switch (timestamp.getUnit()) {\n        case MICROS:\n          return new ParquetColumnReaders.LongColumnReader(typeFromClient, initialBatchSize);\n        case MILLIS:\n          return new TimestampMillisConverter(typeFromClient, initialBatchSize);\n        default:\n          throw new UnsupportedOperationException(\n              String.format(\"Unsupported Parquet TimeType unit=%s\", timestamp.getUnit()));\n      }\n    } else if (typeFromClient == TimestampNTZType.TIMESTAMP_NTZ\n        && primType.getPrimitiveTypeName() == INT32\n        && typeAnnotation instanceof LogicalTypeAnnotation.DateLogicalTypeAnnotation) {\n      return new DateToTimestampNTZConverter(typeFromClient, initialBatchSize);\n    } else {\n      throw new RuntimeException(\n          String.format(\"Unsupported timestamp column with Parquet type %s.\", typeFromFile));\n    }\n  }\n\n  public static class TimestampMillisConverter extends ParquetColumnReaders.LongColumnReader {\n\n    TimestampMillisConverter(DataType dataType, int initialBatchSize) {\n      super(validTimestampType(dataType), initialBatchSize);\n    }\n\n    @Override\n    public void addLong(long value) {\n      super.addLong(DefaultKernelUtils.millisToMicros(value));\n    }\n  }\n\n  public static class TimestampBinaryConverter extends ParquetColumnReaders.LongColumnReader {\n\n    TimestampBinaryConverter(DataType dataType, int initialBatchSize) {\n      super(validTimestampType(dataType), initialBatchSize);\n    }\n\n    private long binaryToSQLTimestamp(Binary binary) {\n      checkArgument(\n          binary.length() == 12,\n          \"Timestamps (with nanoseconds) are expected to be stored in 12-byte long \"\n              + \"binaries. Found a %s-byte binary instead.\",\n          binary.length());\n      ByteBuffer buffer = binary.toByteBuffer().order(ByteOrder.LITTLE_ENDIAN);\n      long timeOfDayNanos = buffer.getLong();\n      int julianDay = buffer.getInt();\n      return DefaultKernelUtils.fromJulianDay(julianDay, timeOfDayNanos);\n    }\n\n    @Override\n    public void addBinary(Binary value) {\n      long julianMicros = binaryToSQLTimestamp(value);\n      // we do not rebase timestamps\n      long gregorianMicros = julianMicros;\n      super.addLong(gregorianMicros);\n    }\n\n    @Override\n    public void addLong(long value) {\n      throw new UnsupportedOperationException(getClass().getName());\n    }\n  }\n\n  public static class DateToTimestampNTZConverter extends ParquetColumnReaders.LongColumnReader {\n\n    DateToTimestampNTZConverter(DataType dataType, int initialBatchSize) {\n      super(validTimestampType(dataType), initialBatchSize);\n    }\n\n    @Override\n    public void addInt(int value) {\n      super.addLong(DefaultKernelUtils.daysToMicros(value, ZoneOffset.UTC));\n    }\n  }\n\n  private static DataType validTimestampType(DataType dataType) {\n    checkArgument(dataType instanceof TimestampType || dataType instanceof TimestampNTZType);\n    return dataType;\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/java/io/delta/kernel/defaults/integration/DataBuilderUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.integration;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.internal.data.DefaultRowBasedColumnarBatch;\nimport io.delta.kernel.types.StructType;\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.IntStream;\n\npublic class DataBuilderUtils {\n  public static TestColumnBatchBuilder builder(StructType schema) {\n    return new TestColumnBatchBuilder(schema);\n  }\n\n  public static Row row(StructType structType, Object... values) {\n    return new TestRow(structType, values);\n  }\n\n  public static Row row(StructType structType) {\n    return new TestRow(structType);\n  }\n\n  public static class TestColumnBatchBuilder {\n    private StructType schema;\n    private List<Row> rows = new ArrayList<>();\n\n    private TestColumnBatchBuilder(StructType schema) {\n      this.schema = schema;\n    }\n\n    public TestColumnBatchBuilder addRow(Object... values) {\n      checkArgument(values.length == schema.length(), \"Invalid columns length\");\n      rows.add(row(schema, values));\n\n      return this;\n    }\n\n    public TestColumnBatchBuilder addAllNullsRow() {\n      rows.add(row(schema));\n      return this;\n    }\n\n    public ColumnarBatch build() {\n      return new DefaultRowBasedColumnarBatch(schema, rows);\n    }\n  }\n\n  private static class TestRow implements Row {\n    private final StructType schema;\n    private final Map<Integer, Object> values;\n\n    private TestRow(StructType schema, Object... values) {\n      this.schema = schema;\n      this.values = new HashMap<>();\n      for (int i = 0; i < values.length; i++) {\n        // lamdas + streams don't work well with null values\n        this.values.put(i, values[i]);\n      }\n    }\n\n    private TestRow(StructType schema) {\n      Map<Integer, Object> values = new HashMap<>();\n      IntStream.range(0, schema.length()).forEach(idx -> values.put(idx, null));\n      this.schema = schema;\n      this.values = values;\n    }\n\n    @Override\n    public StructType getSchema() {\n      return schema;\n    }\n\n    @Override\n    public boolean isNullAt(int ordinal) {\n      return values.get(ordinal) == null;\n    }\n\n    @Override\n    public boolean getBoolean(int ordinal) {\n      return (boolean) values.get(ordinal);\n    }\n\n    @Override\n    public byte getByte(int ordinal) {\n      return (byte) values.get(ordinal);\n    }\n\n    @Override\n    public short getShort(int ordinal) {\n      return (short) values.get(ordinal);\n    }\n\n    @Override\n    public int getInt(int ordinal) {\n      return (int) values.get(ordinal);\n    }\n\n    @Override\n    public long getLong(int ordinal) {\n      return (long) values.get(ordinal);\n    }\n\n    @Override\n    public float getFloat(int ordinal) {\n      return (float) values.get(ordinal);\n    }\n\n    @Override\n    public double getDouble(int ordinal) {\n      return (double) values.get(ordinal);\n    }\n\n    @Override\n    public String getString(int ordinal) {\n      return (String) values.get(ordinal);\n    }\n\n    @Override\n    public BigDecimal getDecimal(int ordinal) {\n      return (BigDecimal) values.get(ordinal);\n    }\n\n    @Override\n    public byte[] getBinary(int ordinal) {\n      return (byte[]) values.get(ordinal);\n    }\n\n    @Override\n    public Row getStruct(int ordinal) {\n      return (Row) values.get(ordinal);\n    }\n\n    @Override\n    public ArrayValue getArray(int ordinal) {\n      throw new UnsupportedOperationException(\n          \"array type unsupported for TestColumnBatchBuilder; use scala test utilities\");\n    }\n\n    @Override\n    public MapValue getMap(int ordinal) {\n      throw new UnsupportedOperationException(\n          \"map type unsupported for TestColumnBatchBuilder; use scala test utilities\");\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/java/io/delta/kernel/defaults/utils/DefaultKernelTestUtils.java",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.utils;\n\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.types.*;\n\npublic class DefaultKernelTestUtils {\n  private DefaultKernelTestUtils() {}\n\n  /** Returns a URI encoded path of the resource. */\n  public static String getTestResourceFilePath(String resourcePath) {\n    return DefaultKernelTestUtils.class.getClassLoader().getResource(resourcePath).getFile();\n  }\n\n  // This will no longer be needed once all tests have been moved to Scala\n  public static Object getValueAsObject(Row row, int columnOrdinal) {\n    final DataType dataType = row.getSchema().at(columnOrdinal).getDataType();\n\n    if (row.isNullAt(columnOrdinal)) {\n      return null;\n    }\n\n    if (dataType instanceof BooleanType) {\n      return row.getBoolean(columnOrdinal);\n    } else if (dataType instanceof ByteType) {\n      return row.getByte(columnOrdinal);\n    } else if (dataType instanceof ShortType) {\n      return row.getShort(columnOrdinal);\n    } else if (dataType instanceof IntegerType || dataType instanceof DateType) {\n      return row.getInt(columnOrdinal);\n    } else if (dataType instanceof LongType || dataType instanceof TimestampType) {\n      return row.getLong(columnOrdinal);\n    } else if (dataType instanceof FloatType) {\n      return row.getFloat(columnOrdinal);\n    } else if (dataType instanceof DoubleType) {\n      return row.getDouble(columnOrdinal);\n    } else if (dataType instanceof StringType) {\n      return row.getString(columnOrdinal);\n    } else if (dataType instanceof BinaryType) {\n      return row.getBinary(columnOrdinal);\n    } else if (dataType instanceof StructType) {\n      return row.getStruct(columnOrdinal);\n    }\n\n    throw new UnsupportedOperationException(dataType + \" is not supported yet\");\n  }\n\n  /**\n   * Get the value at given {@code rowId} from the column vector. The type of the value object\n   * depends on the data type of the {@code vector}.\n   */\n  public static Object getValueAsObject(ColumnVector vector, int rowId) {\n    final DataType dataType = vector.getDataType();\n\n    if (vector.isNullAt(rowId)) {\n      return null;\n    }\n\n    if (dataType instanceof BooleanType) {\n      return vector.getBoolean(rowId);\n    } else if (dataType instanceof ByteType) {\n      return vector.getByte(rowId);\n    } else if (dataType instanceof ShortType) {\n      return vector.getShort(rowId);\n    } else if (dataType instanceof IntegerType || dataType instanceof DateType) {\n      return vector.getInt(rowId);\n    } else if (dataType instanceof LongType\n        || dataType instanceof TimestampType\n        || dataType instanceof TimestampNTZType) {\n      return vector.getLong(rowId);\n    } else if (dataType instanceof FloatType) {\n      return vector.getFloat(rowId);\n    } else if (dataType instanceof DoubleType) {\n      return vector.getDouble(rowId);\n    } else if (dataType instanceof StringType) {\n      return vector.getString(rowId);\n    } else if (dataType instanceof BinaryType) {\n      return vector.getBinary(rowId);\n    } else if (dataType instanceof DecimalType) {\n      return vector.getDecimal(rowId);\n    }\n\n    throw new UnsupportedOperationException(dataType + \" is not supported yet\");\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-no-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1686191546018,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"90476688-fac4-4af7-9ea1-debb0c965333\"}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1686191541734}}\n{\"add\":{\"path\":\"part-00000-a489737f-d477-4d9a-8b4a-bd6a6536df5b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1686191545000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part-00001-1c9b5e60-ab86-4017-9ec9-a6fe4150cdd5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1686191545000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-no-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1686191563139,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#378L < 2)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"6958\",\"numDeletedRows\":\"2\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"c4ab9bb3-c0af-4e68-8eac-4b6c3d141492\"}}\n{\"add\":{\"path\":\"part-00000-a489737f-d477-4d9a-8b4a-bd6a6536df5b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1686191545000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"IjB3V2d3#qUP%s94R0WF\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part-00000-a489737f-d477-4d9a-8b4a-bd6a6536df5b-c000.snappy.parquet\",\"deletionTimestamp\":1686191562047,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":500}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493352168,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"500\",\"numOutputBytes\":\"3714\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"39f72778-9262-4a53-9179-a3386be311cb\"}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1687493346028}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493376134,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#378L = 0)\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"8909\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"5e6e12bc-1b7f-4776-b953-c76f358f85bd\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"2FtLtJDE!.VZ.+udLGa0\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493374011,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493383677,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#1630L = 11)\\\"]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"5074\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"b6abb0cc-20ed-402b-aaa4-da99395282f7\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"u+2?H{A@KdHac(G*R3bY\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493382707,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"2FtLtJDE!.VZ.+udLGa0\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493388280,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#2728L = 22)\\\"]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2789\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"6f872525-30c3-4f2e-b159-00e5dc79d883\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Z8-vW<AKq&NtwnJyQFIm\",\"offset\":1,\"sizeInBytes\":38,\"cardinality\":3}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493387617,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"u+2?H{A@KdHac(G*R3bY\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493392053,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#3824L = 33)\\\"]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2427\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"eae7a4ed-15c7-463a-b4d7-a0447f65d872\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"9xb3::}NfrO=GogoI/(4\",\"offset\":1,\"sizeInBytes\":40,\"cardinality\":4}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493391385,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Z8-vW<AKq&NtwnJyQFIm\",\"offset\":1,\"sizeInBytes\":38,\"cardinality\":3}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493395110,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#4920L = 44)\\\"]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1881\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"87b8a365-9e21-4039-87c5-68f3b37b40dc\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"mrTPS!:VovJ%q5%rJ64G\",\"offset\":1,\"sizeInBytes\":42,\"cardinality\":5}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493394641,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"9xb3::}NfrO=GogoI/(4\",\"offset\":1,\"sizeInBytes\":40,\"cardinality\":4}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493398768,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#6016L = 55)\\\"]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2690\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"4211f6f6-f673-4a5c-b27b-a2f6976e6305\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"od*38mmoPTNufN%zxe@H\",\"offset\":1,\"sizeInBytes\":44,\"cardinality\":6}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493398262,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"mrTPS!:VovJ%q5%rJ64G\",\"offset\":1,\"sizeInBytes\":42,\"cardinality\":5}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493402193,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#7112L = 66)\\\"]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2403\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"f451b18d-e57b-4d67-ae6d-8038ef0d0c57\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"dTnVvWLQ^#NuWWk/:&<p\",\"offset\":1,\"sizeInBytes\":46,\"cardinality\":7}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493401750,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"od*38mmoPTNufN%zxe@H\",\"offset\":1,\"sizeInBytes\":44,\"cardinality\":6}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493405006,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#8208L = 77)\\\"]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1650\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"390d2e1d-7aa1-44b1-8d8f-1280c4702478\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"C>lG.o0}xSJ&Dj^:+rNR\",\"offset\":1,\"sizeInBytes\":48,\"cardinality\":8}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493404691,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"dTnVvWLQ^#NuWWk/:&<p\",\"offset\":1,\"sizeInBytes\":46,\"cardinality\":7}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493407900,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#9304L = 88)\\\"]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1430\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"4e959201-5ca2-4634-bbd8-b2e29b699d60\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"F}lqo<A17wFX#F7@dNP/\",\"offset\":1,\"sizeInBytes\":50,\"cardinality\":9}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493407580,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"C>lG.o0}xSJ&Dj^:+rNR\",\"offset\":1,\"sizeInBytes\":48,\"cardinality\":8}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493411379,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#10400L = 99)\\\"]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2186\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"1d3a5961-02f7-43df-9e94-f685bd8aa4a9\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"(w{TGxkf!oV>34U26WTB\",\"offset\":1,\"sizeInBytes\":52,\"cardinality\":10}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493410867,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"F}lqo<A17wFX#F7@dNP/\",\"offset\":1,\"sizeInBytes\":50,\"cardinality\":9}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493415880,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#11585L = 110)\\\"]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2198\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"a1a543c0-9165-45af-bcbf-fb4a5403ec74\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"YK?8=aIQnrWC%eK+:syC\",\"offset\":1,\"sizeInBytes\":54,\"cardinality\":11}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493415646,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"(w{TGxkf!oV>34U26WTB\",\"offset\":1,\"sizeInBytes\":52,\"cardinality\":10}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000012.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493419030,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#12986L = 121)\\\"]\"},\"readVersion\":11,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1553\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"448849f9-e20c-4012-8aa2-e52ed4d85c82\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"7>Z9H2%z4LIuqwRlf+)>\",\"offset\":1,\"sizeInBytes\":56,\"cardinality\":12}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493418827,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"YK?8=aIQnrWC%eK+:syC\",\"offset\":1,\"sizeInBytes\":54,\"cardinality\":11}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000013.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493421483,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#14107L = 132)\\\"]\"},\"readVersion\":12,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1378\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"43e8665e-173a-415a-ab30-20ce4308c8b0\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"2(ocO7^QM/Msat&JfpAR\",\"offset\":1,\"sizeInBytes\":58,\"cardinality\":13}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493421320,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"7>Z9H2%z4LIuqwRlf+)>\",\"offset\":1,\"sizeInBytes\":56,\"cardinality\":12}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000014.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493423908,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#15228L = 143)\\\"]\"},\"readVersion\":13,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1144\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"2319e0d7-eea8-4beb-820e-12b7f28ef060\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Tl?MJB%Y[oJiQ>B+@/*:\",\"offset\":1,\"sizeInBytes\":60,\"cardinality\":14}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493423676,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"2(ocO7^QM/Msat&JfpAR\",\"offset\":1,\"sizeInBytes\":58,\"cardinality\":13}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000015.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493426612,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#16349L = 154)\\\"]\"},\"readVersion\":14,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1295\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"2881078a-fc97-42fc-9b6c-295aee92f3eb\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"V./QH]:sH4Q]00QmNsI4\",\"offset\":1,\"sizeInBytes\":62,\"cardinality\":15}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493426448,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Tl?MJB%Y[oJiQ>B+@/*:\",\"offset\":1,\"sizeInBytes\":60,\"cardinality\":14}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000016.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493429640,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#17470L = 165)\\\"]\"},\"readVersion\":15,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2021\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"b3e8c5e4-5230-43ab-a1a3-44dc928d7431\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"egQo!fD<28G{F(Ci=@L$\",\"offset\":1,\"sizeInBytes\":64,\"cardinality\":16}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493429383,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"V./QH]:sH4Q]00QmNsI4\",\"offset\":1,\"sizeInBytes\":62,\"cardinality\":15}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000017.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493432150,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#18591L = 176)\\\"]\"},\"readVersion\":16,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1523\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"0517b48f-02d2-4ea1-81c0-58cc248a881d\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"ez<lZtr+[-FjEr]9gKyA\",\"offset\":1,\"sizeInBytes\":66,\"cardinality\":17}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493431959,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"egQo!fD<28G{F(Ci=@L$\",\"offset\":1,\"sizeInBytes\":64,\"cardinality\":16}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000018.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493435189,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#19712L = 187)\\\"]\"},\"readVersion\":17,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1954\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"920af52b-30f3-4869-adb3-db582c48ff9c\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"R-[Nbl&x/sV}b$!bHf$e\",\"offset\":1,\"sizeInBytes\":68,\"cardinality\":18}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493434984,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"ez<lZtr+[-FjEr]9gKyA\",\"offset\":1,\"sizeInBytes\":66,\"cardinality\":17}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000019.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493438291,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#20833L = 198)\\\"]\"},\"readVersion\":18,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2122\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"1872fc6e-5db1-4479-8947-159455020645\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"v48Y/zMMo)HBie2P<u<:\",\"offset\":1,\"sizeInBytes\":70,\"cardinality\":19}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493438101,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"R-[Nbl&x/sV}b$!bHf$e\",\"offset\":1,\"sizeInBytes\":68,\"cardinality\":18}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000020.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493441433,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#21954L = 209)\\\"]\"},\"readVersion\":19,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1619\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"9f47105e-b2f5-4891-acc3-23fbb7fe8927\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"mq2er:%G^HPl{6N]dCWk\",\"offset\":1,\"sizeInBytes\":72,\"cardinality\":20}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493441161,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"v48Y/zMMo)HBie2P<u<:\",\"offset\":1,\"sizeInBytes\":70,\"cardinality\":19}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000021.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493445832,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#23164L = 220)\\\"]\"},\"readVersion\":20,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1964\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"34affc99-e372-4d82-8f7e-45dcdf2e5335\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"AQ)$?-+W@XPL]2Mi0[Il\",\"offset\":1,\"sizeInBytes\":74,\"cardinality\":21}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493445668,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"mq2er:%G^HPl{6N]dCWk\",\"offset\":1,\"sizeInBytes\":72,\"cardinality\":20}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000022.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493448082,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#24565L = 231)\\\"]\"},\"readVersion\":21,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1410\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"e17bb88d-0c7f-4a08-9df8-967f8a2a0398\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"AK-GIw=$EuTQ)<F92#%H\",\"offset\":1,\"sizeInBytes\":76,\"cardinality\":22}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493447869,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"AQ)$?-+W@XPL]2Mi0[Il\",\"offset\":1,\"sizeInBytes\":74,\"cardinality\":21}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000023.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493450963,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#25686L = 242)\\\"]\"},\"readVersion\":22,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1630\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"f49193c8-69ce-4e57-a6c0-1a945d90072d\"}}\n{\"add\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":2219,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":249},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"rJ(BUnm+lwX)CTfMh^Q:\",\"offset\":1,\"sizeInBytes\":78,\"cardinality\":23}}}\n{\"remove\":{\"path\":\"part-00000-87eec267-9acd-4e9a-a216-ec596132203d-c000.snappy.parquet\",\"deletionTimestamp\":1687493450768,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":2219,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"AK-GIw=$EuTQ)<F92#%H\",\"offset\":1,\"sizeInBytes\":76,\"cardinality\":22}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000024.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493453771,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#26807L = 253)\\\"]\"},\"readVersion\":23,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1411\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"4abb4a60-b822-4277-89db-46050ab4dde0\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"<jm>0pRYlnGp#D/X2&/I\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493453617,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000025.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493456787,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#28067L = 264)\\\"]\"},\"readVersion\":24,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1987\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"c4ae3482-f3d5-4e8c-972f-5dfe6c6d16e9\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"/PV+vOr=NRJZUZc>4izq\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493456593,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"<jm>0pRYlnGp#D/X2&/I\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000026.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493459673,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#29188L = 275)\\\"]\"},\"readVersion\":25,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1523\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"7387b8d6-320c-48a1-b274-e08bdb09f365\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"[OrxMF<&j1KIzaYw>L1$\",\"offset\":1,\"sizeInBytes\":38,\"cardinality\":3}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493459510,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"/PV+vOr=NRJZUZc>4izq\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000027.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493462246,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#30309L = 286)\\\"]\"},\"readVersion\":26,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1628\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"5bd58d9b-61a8-43e1-9f54-adb944e5bb51\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"JX$#0D<-MnJ9i[$BYwYD\",\"offset\":1,\"sizeInBytes\":40,\"cardinality\":4}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493461861,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"[OrxMF<&j1KIzaYw>L1$\",\"offset\":1,\"sizeInBytes\":38,\"cardinality\":3}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000028.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493465572,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#31430L = 297)\\\"]\"},\"readVersion\":27,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2070\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"67246fbb-8e42-471e-8a6e-911ee02289eb\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"?i@EYwa-J5RbtEIPJ.Gl\",\"offset\":1,\"sizeInBytes\":42,\"cardinality\":5}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493465351,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"JX$#0D<-MnJ9i[$BYwYD\",\"offset\":1,\"sizeInBytes\":40,\"cardinality\":4}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000029.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493467801,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#32551L = 308)\\\"]\"},\"readVersion\":28,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1410\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"51e5335a-cd2c-467c-93b3-44b1731e03a6\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"l$m[rf}7BENh@zObPYMw\",\"offset\":1,\"sizeInBytes\":44,\"cardinality\":6}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493467650,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"?i@EYwa-J5RbtEIPJ.Gl\",\"offset\":1,\"sizeInBytes\":42,\"cardinality\":5}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000030.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493470112,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#33672L = 319)\\\"]\"},\"readVersion\":29,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1399\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"852d93d5-998e-4210-871c-6f86b6fa9d9b\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\">i1<2fv@(-V::TBC{X^$\",\"offset\":1,\"sizeInBytes\":46,\"cardinality\":7}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493469924,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"l$m[rf}7BENh@zObPYMw\",\"offset\":1,\"sizeInBytes\":44,\"cardinality\":6}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000031.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493476087,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#34882L = 330)\\\"]\"},\"readVersion\":30,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"3310\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"982792fe-4bd7-404a-a7bd-19113e7ba7da\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\")y]bjYT7n0T&3ZI7fsnR\",\"offset\":1,\"sizeInBytes\":48,\"cardinality\":8}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493475923,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\">i1<2fv@(-V::TBC{X^$\",\"offset\":1,\"sizeInBytes\":46,\"cardinality\":7}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000032.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493478659,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#36283L = 341)\\\"]\"},\"readVersion\":31,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1489\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"19b580df-8671-46f3-bedf-f0ba70b45a64\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"B(bfMgm}tEXY:Biw>Sff\",\"offset\":1,\"sizeInBytes\":50,\"cardinality\":9}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493478493,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\")y]bjYT7n0T&3ZI7fsnR\",\"offset\":1,\"sizeInBytes\":48,\"cardinality\":8}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000033.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493481125,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#37404L = 352)\\\"]\"},\"readVersion\":32,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1406\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"88f23969-ecda-4300-ad0a-0522b3c50277\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"o0UC@Ov}pLUd!8XlnsU/\",\"offset\":1,\"sizeInBytes\":52,\"cardinality\":10}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493480901,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"B(bfMgm}tEXY:Biw>Sff\",\"offset\":1,\"sizeInBytes\":50,\"cardinality\":9}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000034.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493483736,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#38525L = 363)\\\"]\"},\"readVersion\":33,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1555\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"c4c917d0-94d2-4eab-9e3b-c38a00d8d30f\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"[W5]y}RT#DZ1e^jIu+&k\",\"offset\":1,\"sizeInBytes\":54,\"cardinality\":11}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493483543,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"o0UC@Ov}pLUd!8XlnsU/\",\"offset\":1,\"sizeInBytes\":52,\"cardinality\":10}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000035.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493486709,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#39646L = 374)\\\"]\"},\"readVersion\":34,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"2087\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"ae9e26f7-5c42-4a11-ae83-28913ac79aaf\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"}Vi)6cLx4bSSkUi37-Sb\",\"offset\":1,\"sizeInBytes\":56,\"cardinality\":12}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493486280,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"[W5]y}RT#DZ1e^jIu+&k\",\"offset\":1,\"sizeInBytes\":54,\"cardinality\":11}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000036.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493488949,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#40767L = 385)\\\"]\"},\"readVersion\":35,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1387\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"f8cfc704-8c61-470b-a00b-675db4744a4d\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"QV![s>Cg)KJ<W>c8r+c2\",\"offset\":1,\"sizeInBytes\":58,\"cardinality\":13}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493488645,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"}Vi)6cLx4bSSkUi37-Sb\",\"offset\":1,\"sizeInBytes\":56,\"cardinality\":12}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000037.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493491174,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#41888L = 396)\\\"]\"},\"readVersion\":36,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1432\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"60dd6745-b035-4fd4-9c74-cae1ad56c998\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"=pfvKB2V<JX?E8ofGVkz\",\"offset\":1,\"sizeInBytes\":60,\"cardinality\":14}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493490905,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"QV![s>Cg)KJ<W>c8r+c2\",\"offset\":1,\"sizeInBytes\":58,\"cardinality\":13}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000038.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493493743,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#43009L = 407)\\\"]\"},\"readVersion\":37,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1747\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"68f77edd-106f-4a66-a2fd-2cc53a1e8e2c\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"188rU5d7$uQnz8Cw*&JR\",\"offset\":1,\"sizeInBytes\":62,\"cardinality\":15}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493493431,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"=pfvKB2V<JX?E8ofGVkz\",\"offset\":1,\"sizeInBytes\":60,\"cardinality\":14}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000039.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493495961,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#44130L = 418)\\\"]\"},\"readVersion\":38,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"998\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"1dded16c-3554-4d8a-84fb-34badee131c6\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"5:sIhnruLmRI?57YA4?-\",\"offset\":1,\"sizeInBytes\":64,\"cardinality\":16}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493495806,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"188rU5d7$uQnz8Cw*&JR\",\"offset\":1,\"sizeInBytes\":62,\"cardinality\":15}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000040.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493498797,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#45251L = 429)\\\"]\"},\"readVersion\":39,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1837\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"d0c6a7c7-cc54-455e-8c28-a25c98543d12\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"^wgAJ=4X/TIzz&%qDElT\",\"offset\":1,\"sizeInBytes\":66,\"cardinality\":17}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493498625,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"5:sIhnruLmRI?57YA4?-\",\"offset\":1,\"sizeInBytes\":64,\"cardinality\":16}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000041.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493502795,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#46461L = 440)\\\"]\"},\"readVersion\":40,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1990\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"d4d539ae-e6f1-4a5c-9e49-839d5e24c54e\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"5TLR.gkXvkTLoklq>ckU\",\"offset\":1,\"sizeInBytes\":68,\"cardinality\":18}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493502653,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"^wgAJ=4X/TIzz&%qDElT\",\"offset\":1,\"sizeInBytes\":66,\"cardinality\":17}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000042.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493505010,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#47862L = 451)\\\"]\"},\"readVersion\":41,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1328\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"c9eadb2b-ac9c-4b31-9b38-923978074394\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"1ENM$^ul]}VH<&ub7J5y\",\"offset\":1,\"sizeInBytes\":70,\"cardinality\":19}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493504829,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"5TLR.gkXvkTLoklq>ckU\",\"offset\":1,\"sizeInBytes\":68,\"cardinality\":18}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000043.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493507835,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#48983L = 462)\\\"]\"},\"readVersion\":42,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1977\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"81b2bee3-440a-4051-92ca-c7c9df8fcd02\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"1lI0]tO]*HV].E0P<8T2\",\"offset\":1,\"sizeInBytes\":72,\"cardinality\":20}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493507663,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"1ENM$^ul]}VH<&ub7J5y\",\"offset\":1,\"sizeInBytes\":70,\"cardinality\":19}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000044.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493510227,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#50104L = 473)\\\"]\"},\"readVersion\":43,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1453\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"5096139c-7d93-40aa-a573-37088d859c0c\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"8RfaCZ!EkOFD(^Syd?cC\",\"offset\":1,\"sizeInBytes\":74,\"cardinality\":21}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493510052,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"1lI0]tO]*HV].E0P<8T2\",\"offset\":1,\"sizeInBytes\":72,\"cardinality\":20}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000045.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493513120,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#51225L = 484)\\\"]\"},\"readVersion\":44,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1945\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"5efa6fb6-29e3-46c9-8a4a-2d82df02da68\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Yq}%xtpq&YTHdjxg?Osr\",\"offset\":1,\"sizeInBytes\":76,\"cardinality\":22}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493512712,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"8RfaCZ!EkOFD(^Syd?cC\",\"offset\":1,\"sizeInBytes\":74,\"cardinality\":21}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/00000000000000000046.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687493515976,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(id#52346L = 495)\\\"]\"},\"readVersion\":45,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numCopiedRows\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1419\",\"numDeletedRows\":\"1\",\"scanTimeMs\":\"0\",\"numAddedFiles\":\"0\",\"numAddedBytes\":\"0\",\"rewriteTimeMs\":\"0\"},\"engineInfo\":\"Apache-Spark/3.4.0 Delta-Lake/2.4.0\",\"txnId\":\"deeed50a-1264-4c7e-9e26-b3217e7779dd\"}}\n{\"add\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1495,\"modificationTime\":1687493351000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":250,\\\"minValues\\\":{\\\"id\\\":250},\\\"maxValues\\\":{\\\"id\\\":499},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"+Ry*Wio9zqLW9MIvM6Su\",\"offset\":1,\"sizeInBytes\":78,\"cardinality\":23}}}\n{\"remove\":{\"path\":\"part-00001-c94c50bd-c7bb-4c0d-b6cb-958707d77d01-c000.snappy.parquet\",\"deletionTimestamp\":1687493515806,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1495,\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Yq}%xtpq&YTHdjxg?Osr\",\"offset\":1,\"sizeInBytes\":76,\"cardinality\":22}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-dv-with-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":40,\"size\":44,\"sizeInBytes\":17200,\"numOfAddFiles\":2,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"1db7d2ee97496873124d8c27e4d28019\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943471996,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"fa272a31-18c1-4c57-ae5c-6b52fbe83e92\"}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1679943471575}}\n{\"add\":{\"path\":\"part-00000-a65ab59f-72fd-44c9-a73e-e2d09459f836-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1679943471000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-39e6196f-2259-4ba4-b1d6-005712cd7784-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943471000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943473096,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"f9689ad3-1179-4682-bd00-4a635b48cba8\"}}\n{\"add\":{\"path\":\"part-00000-5c99dc53-38c0-420f-a91b-6df7a4c27a2b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943473000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":14},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-8be0e82d-ce51-43a6-92eb-df71a9088173-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943473000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":15},\\\"maxValues\\\":{\\\"id\\\":19},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943474078,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"33a8226f-dfcc-4ec1-82a7-fb4f82014006\"}}\n{\"add\":{\"path\":\"part-00000-3317387d-183d-4db7-ac3e-596901d90de0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943474000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":20},\\\"maxValues\\\":{\\\"id\\\":24},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-a65a81c6-292a-49f2-8eea-82c0299cdfb3-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943474000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":25},\\\"maxValues\\\":{\\\"id\\\":29},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943475370,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"1fc575df-b4bf-452f-bf30-6715cb63ae21\"}}\n{\"add\":{\"path\":\"part-00000-14b8a37a-107b-455f-ab94-8f55e44c004b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943475000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":30},\\\"maxValues\\\":{\\\"id\\\":34},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-fdaa71cc-84b2-43b1-b049-7cd36dbaa0de-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943475000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":35},\\\"maxValues\\\":{\\\"id\\\":39},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943476409,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"896a19db-a5c0-45e1-9265-7d6745b0860e\"}}\n{\"add\":{\"path\":\"part-00000-100e4547-5ff3-4735-9550-7757ca23c61d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943476000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":40},\\\"maxValues\\\":{\\\"id\\\":44},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-4e30e0a7-63d2-4d2f-a028-b92058c3c8cf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943476000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":45},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943477339,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"a9a4c094-2e27-40f2-bdef-6eead53d535c\"}}\n{\"add\":{\"path\":\"part-00000-eb1dae3f-8c89-46c3-818f-491cc673936a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943477000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":54},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-a9a33a7f-26fa-447d-8b34-863b5f695f06-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943477000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":55},\\\"maxValues\\\":{\\\"id\\\":59},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943478349,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"e60661af-beaf-444d-9b7e-df34d0ef119e\"}}\n{\"add\":{\"path\":\"part-00000-1bbb3853-04b4-4539-a112-be7140314153-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943478000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":60},\\\"maxValues\\\":{\\\"id\\\":64},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-30432f6b-710f-440c-8145-adbaed187c63-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943478000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":65},\\\"maxValues\\\":{\\\"id\\\":69},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943479247,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"1545252b-0e40-4ac8-9fb9-2a165a524c61\"}}\n{\"add\":{\"path\":\"part-00000-e51a2d2a-d1a3-437e-a428-f5afe93d4619-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943479000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":70},\\\"maxValues\\\":{\\\"id\\\":74},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-ed427b16-f597-432a-a49e-135b126d38a8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943479000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":75},\\\"maxValues\\\":{\\\"id\\\":79},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943480075,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"43e883c7-8afd-4ef4-9d4d-0be79a3a6b9e\"}}\n{\"add\":{\"path\":\"part-00000-5e9186c7-c7b0-4c4d-9f22-1c0cd403142c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943480000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":80},\\\"maxValues\\\":{\\\"id\\\":84},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-3cd0b397-0ac3-48ac-88ab-3cc21542e303-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943480000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":85},\\\"maxValues\\\":{\\\"id\\\":89},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943480946,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"c63d10cf-f665-4b99-a620-abbf9518d520\"}}\n{\"add\":{\"path\":\"part-00000-8d5f08ff-261b-4315-99cb-d289a3191368-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943480000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":90},\\\"maxValues\\\":{\\\"id\\\":94},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-51925029-c591-4366-b3e9-aeea97594037-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943480000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":95},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943481745,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1005\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"cdda48e4-68d6-4416-9eca-a23f5c3a59cd\"}}\n{\"add\":{\"path\":\"part-00000-2a210d80-a4e6-4a1c-8716-ee0b542aee08-c000.snappy.parquet\",\"partitionValues\":{},\"size\":502,\"modificationTime\":1679943481000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":100},\\\"maxValues\\\":{\\\"id\\\":104},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-43e387db-3e56-44f3-8965-9187a80fce9a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943481000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":105},\\\"maxValues\\\":{\\\"id\\\":109},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943484145,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"8cdfb467-8be9-4fed-b3e4-d1c2ad6f8b46\"}}\n{\"add\":{\"path\":\"part-00000-326010e2-01f4-4dfb-90a7-98cbc04a60d1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943484000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":110},\\\"maxValues\\\":{\\\"id\\\":114},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-ba1ceb1e-6a37-4e2e-8e97-a17b9b1bb33d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943484000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":115},\\\"maxValues\\\":{\\\"id\\\":119},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000012.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943485143,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":11,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"d19bd23c-dc4a-430f-8882-657f3bb9aa20\"}}\n{\"add\":{\"path\":\"part-00000-6e367682-7cd1-48e6-bc2f-bc94aa94d1a3-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943485000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":120},\\\"maxValues\\\":{\\\"id\\\":124},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-63b224e2-ba72-4b95-af02-5af2367d4130-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943485000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":125},\\\"maxValues\\\":{\\\"id\\\":129},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000013.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943486048,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":12,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1005\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"8388e2cc-f11a-4b4b-98f1-a1ad5e45d9ee\"}}\n{\"add\":{\"path\":\"part-00000-0d9c05f4-8afc-4325-b1e0-ea32e4eff918-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943486000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":130},\\\"maxValues\\\":{\\\"id\\\":134},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-d64b05c7-d80d-4eff-8c58-d209003ee4c0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":502,\"modificationTime\":1679943486000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":135},\\\"maxValues\\\":{\\\"id\\\":139},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/00000000000000000014.json",
    "content": "{\"commitInfo\":{\"timestamp\":1679943486941,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":13,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1005\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.2.0\",\"txnId\":\"9687712e-b9dd-4a48-bede-acb00101133f\"}}\n{\"add\":{\"path\":\"part-00000-ce6aed75-3e85-4d7c-90de-9d465e9acc04-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1679943486000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":140},\\\"maxValues\\\":{\\\"id\\\":144},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-4707caa4-d293-4b4a-aeef-fa4d5815e732-c000.snappy.parquet\",\"partitionValues\":{},\"size\":502,\"modificationTime\":1679943486000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":145},\\\"maxValues\\\":{\\\"id\\\":149},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/basic-with-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":24,\"sizeInBytes\":11658,\"numOfAddFiles\":22,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"d4dd43c87695abaede4556e73b008658\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/catalog-owned-preview/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"inCommitTimestamp\":1749830855993,\"timestamp\":1749830855992,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[\\\"part1\\\"]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/4.0.0 Delta-Lake/4.0.0\",\"txnId\":\"d108f896-9662-4eda-b4de-444a99850aa8\"}}\n{\"metaData\":{\"id\":\"64dcd182-b3b4-4ee0-88e0-63c159a4121c\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"part1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"part1\"],\"configuration\":{\"delta.enableInCommitTimestamps\":\"true\"},\"createdTime\":1749830855646}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"catalogManaged\"],\"writerFeatures\":[\"catalogManaged\",\"inCommitTimestamp\",\"invariants\",\"appendOnly\"]}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/catalog-owned-preview/_delta_log/_staged_commits/00000000000000000001.4cb9708e-b478-44de-b203-53f9ba9b2876.json",
    "content": "{\"commitInfo\":{\"inCommitTimestamp\":1749830871085,\"timestamp\":1749830871084,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"889\"},\"engineInfo\":\"Apache-Spark/4.0.0 Delta-Lake/4.0.0\",\"txnId\":\"4cb9708e-b478-44de-b203-53f9ba9b2876\"}}\n{\"add\":{\"path\":\"part1=0/part-00000-13fefaba-8ec2-4762-b17e-aeda657451c5.c000.snappy.parquet\",\"partitionValues\":{\"part1\":\"0\"},\"size\":889,\"modificationTime\":1749830870833,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"col1\\\":0},\\\"maxValues\\\":{\\\"col1\\\":99},\\\"nullCount\\\":{\\\"col1\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/catalog-owned-preview/_delta_log/_staged_commits/00000000000000000002.5b9bba4a-0085-430d-a65e-b0d38c1afbe9.json",
    "content": "{\"commitInfo\":{\"inCommitTimestamp\":1749830881799,\"timestamp\":1749830881798,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"891\"},\"engineInfo\":\"Apache-Spark/4.0.0 Delta-Lake/4.0.0\",\"txnId\":\"5b9bba4a-0085-430d-a65e-b0d38c1afbe9\"}}\n{\"add\":{\"path\":\"part1=1/part-00000-8afb1c56-2018-4af2-aa4f-4336c1b39efd.c000.snappy.parquet\",\"partitionValues\":{\"part1\":\"1\"},\"size\":891,\"modificationTime\":1749830881779,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"col1\\\":100},\\\"maxValues\\\":{\\\"col1\\\":199},\\\"nullCount\\\":{\\\"col1\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/catalog-owned-preview/info.txt",
    "content": "# Below are the commands and instructions to create the `catalog-owned-preview` table.\n# Note that delta-spark:4.0.0 does not yet support *creating* catalogManaged tables.\n# So, for now, we create a normal table with ICT enabled and then\n# (a) manually add the `catalogManaged`\n# (b) manually move and rename the published delta files into the _staged_commits directory.\n\npyspark --packages io.delta:delta-spark_2.13:4.0.0 \\\n    --conf \"spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\" \\\n    --conf \"spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\"\n\ntable_path = <table_path>\n\n# Commit 0: Create the table\n\nspark.sql(f\"\"\"\nCREATE TABLE delta.`{table_path}` (\n    part1 INT,\n    col1 INT\n)\nUSING DELTA\nPARTITIONED BY (part1)\n\"\"\")\n\n# Commit 1: Insert 100 rows into part1=0\n\nspark.sql(f\"\"\"\nINSERT INTO delta.`{table_path}`\nSELECT\n    col1 DIV 100 as part1,\n    col1\nFROM (\n    SELECT explode(sequence(0, 99)) as col1\n)\n\"\"\")\n\n# Commit 2: Insert 100 rows into part1=1\n\nspark.sql(f\"\"\"\nINSERT INTO delta.`{table_path}`\nSELECT\n    col1 DIV 100 as part1,\n    col1\nFROM (\n    SELECT explode(sequence(100, 199)) as col1\n)\n\"\"\")\n\n# Then, add `\"readerFeatures\":[\"catalogManaged\"]` to the _delta_log/001.json protocol\n# Then, for commits version $v in [1, 2] move _delta_log/$v.json into\n# _delta_log/_staged_commits/$v.$uuid.json, where $uuid is taken from the commitInfo.txnId in\n# $v.json"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/column-mapping-id/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1681169404146,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"20\",\"numOutputBytes\":\"1470\"},\"engineInfo\":\"Apache-Spark/3.3.2 Delta-Lake/2.3.0-SNAPSHOT\",\"txnId\":\"21c53b30-edd8-481b-9e07-7d37b04fd514\"}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"id\",\"delta.columnMapping.maxColumnId\":\"1\"},\"createdTime\":1681169403930}}\n{\"add\":{\"path\":\"part-00000-7f7d554f-a8f2-459f-aaca-9a3b7e8af2dc-c000.snappy.parquet\",\"partitionValues\":{},\"size\":737,\"modificationTime\":1681169404000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\":1},\\\"maxValues\\\":{\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\":18},\\\"nullCount\\\":{\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-85082a62-baeb-46c5-8970-c9c6c23dc33c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":733,\"modificationTime\":1681169404000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\":0},\\\"maxValues\\\":{\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\":19},\\\"nullCount\\\":{\\\"col-f6363b34-a18d-4117-a4f5-cd443e9d4fda\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/data-reader-partition-values-column-mapping-name/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687761154342,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"as_int\\\",\\\"as_long\\\",\\\"as_byte\\\",\\\"as_short\\\",\\\"as_boolean\\\",\\\"as_float\\\",\\\"as_double\\\",\\\"as_string\\\",\\\"as_date\\\",\\\"as_timestamp\\\",\\\"as_big_decimal\\\"]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"3\",\"numOutputRows\":\"3\",\"numOutputBytes\":\"1896\"},\"engineInfo\":\"Apache-Spark/3.3.1 Delta-Lake/2.3.0\",\"txnId\":\"8ddbc378-38bd-4992-b394-c73162a776ec\"}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"metaData\":{\"id\":\"85380a11-828f-4831-b58f-219ffc825181\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed\\\"}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa\\\"}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08\\\"}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.physicalName\\\":\\\"col-29f826c0-7fff-4e5f-bc11-44a6975c7708\\\"}},{\\\"name\\\":\\\"as_boolean\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.physicalName\\\":\\\"col-7781d665-6951-4244-b9bc-a28e477e2d57\\\"}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.physicalName\\\":\\\"col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae\\\"}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.physicalName\\\":\\\"col-3463c48b-4b94-4500-b14f-4a554284b94f\\\"}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.physicalName\\\":\\\"col-05f332c4-ebdb-4437-9e80-e23f92bee4a2\\\"}},{\\\"name\\\":\\\"as_date\\\",\\\"type\\\":\\\"date\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.physicalName\\\":\\\"col-c025b8f8-481c-4db2-8932-f37129146ceb\\\"}},{\\\"name\\\":\\\"as_timestamp\\\",\\\"type\\\":\\\"timestamp\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.physicalName\\\":\\\"col-bd963d5f-2199-4700-b5d6-0759bd7a9d90\\\"}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":11,\\\"delta.columnMapping.physicalName\\\":\\\"col-01ec4063-ed54-41db-805e-ebfd9b9a6e67\\\"}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":12,\\\"delta.columnMapping.physicalName\\\":\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\"}}]}\",\"partitionColumns\":[\"as_int\",\"as_long\",\"as_byte\",\"as_short\",\"as_boolean\",\"as_float\",\"as_double\",\"as_string\",\"as_date\",\"as_timestamp\",\"as_big_decimal\"],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.columnMapping.maxColumnId\":\"12\"},\"createdTime\":1687761153419}}\n{\"add\":{\"path\":\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed=1/col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa=1/col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08=1/col-29f826c0-7fff-4e5f-bc11-44a6975c7708=1/col-7781d665-6951-4244-b9bc-a28e477e2d57=false/col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae=1.0/col-3463c48b-4b94-4500-b14f-4a554284b94f=1.0/col-05f332c4-ebdb-4437-9e80-e23f92bee4a2=1/col-c025b8f8-481c-4db2-8932-f37129146ceb=2021-09-08/col-bd963d5f-2199-4700-b5d6-0759bd7a9d90=2021-09-08%2011%253A11%253A11/col-01ec4063-ed54-41db-805e-ebfd9b9a6e67=1/part-00000-c9d9ab23-0f5c-4a12-837f-b709c5037905.c000.snappy.parquet\",\"partitionValues\":{\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed\":\"1\",\"col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa\":\"1\",\"col-29f826c0-7fff-4e5f-bc11-44a6975c7708\":\"1\",\"col-01ec4063-ed54-41db-805e-ebfd9b9a6e67\":\"1\",\"col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08\":\"1\",\"col-3463c48b-4b94-4500-b14f-4a554284b94f\":\"1.0\",\"col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae\":\"1.0\",\"col-7781d665-6951-4244-b9bc-a28e477e2d57\":\"false\",\"col-05f332c4-ebdb-4437-9e80-e23f92bee4a2\":\"1\",\"col-c025b8f8-481c-4db2-8932-f37129146ceb\":\"2021-09-08\",\"col-bd963d5f-2199-4700-b5d6-0759bd7a9d90\":\"2021-09-08 11:11:11\"},\"size\":632,\"modificationTime\":1687761154332,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":\\\"1\\\"},\\\"maxValues\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":\\\"1\\\"},\\\"nullCount\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":0}}\"}}\n{\"add\":{\"path\":\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed=__HIVE_DEFAULT_PARTITION__/col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa=__HIVE_DEFAULT_PARTITION__/col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08=__HIVE_DEFAULT_PARTITION__/col-29f826c0-7fff-4e5f-bc11-44a6975c7708=__HIVE_DEFAULT_PARTITION__/col-7781d665-6951-4244-b9bc-a28e477e2d57=__HIVE_DEFAULT_PARTITION__/col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae=__HIVE_DEFAULT_PARTITION__/col-3463c48b-4b94-4500-b14f-4a554284b94f=__HIVE_DEFAULT_PARTITION__/col-05f332c4-ebdb-4437-9e80-e23f92bee4a2=__HIVE_DEFAULT_PARTITION__/col-c025b8f8-481c-4db2-8932-f37129146ceb=__HIVE_DEFAULT_PARTITION__/col-bd963d5f-2199-4700-b5d6-0759bd7a9d90=__HIVE_DEFAULT_PARTITION__/col-01ec4063-ed54-41db-805e-ebfd9b9a6e67=__HIVE_DEFAULT_PARTITION__/part-00001-dac9e981-94cc-4dc5-8d01-2cbae8ef69c6.c000.snappy.parquet\",\"partitionValues\":{\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed\":null,\"col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa\":null,\"col-29f826c0-7fff-4e5f-bc11-44a6975c7708\":null,\"col-01ec4063-ed54-41db-805e-ebfd9b9a6e67\":null,\"col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08\":null,\"col-3463c48b-4b94-4500-b14f-4a554284b94f\":null,\"col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae\":null,\"col-7781d665-6951-4244-b9bc-a28e477e2d57\":null,\"col-05f332c4-ebdb-4437-9e80-e23f92bee4a2\":null,\"col-c025b8f8-481c-4db2-8932-f37129146ceb\":null,\"col-bd963d5f-2199-4700-b5d6-0759bd7a9d90\":null},\"size\":632,\"modificationTime\":1687761154335,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":\\\"2\\\"},\\\"maxValues\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":\\\"2\\\"},\\\"nullCount\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":0}}\"}}\n{\"add\":{\"path\":\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed=0/col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa=0/col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08=0/col-29f826c0-7fff-4e5f-bc11-44a6975c7708=0/col-7781d665-6951-4244-b9bc-a28e477e2d57=true/col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae=0.0/col-3463c48b-4b94-4500-b14f-4a554284b94f=0.0/col-05f332c4-ebdb-4437-9e80-e23f92bee4a2=0/col-c025b8f8-481c-4db2-8932-f37129146ceb=2021-09-08/col-bd963d5f-2199-4700-b5d6-0759bd7a9d90=2021-09-08%2011%253A11%253A11/col-01ec4063-ed54-41db-805e-ebfd9b9a6e67=0/part-00002-e0842c02-93d2-4c38-b041-fc88b581688b.c000.snappy.parquet\",\"partitionValues\":{\"col-25948c99-9f51-4e05-9f9e-b4f7042f75ed\":\"0\",\"col-6e87b90d-f5df-4dcc-91a1-7a43fa3173fa\":\"0\",\"col-29f826c0-7fff-4e5f-bc11-44a6975c7708\":\"0\",\"col-01ec4063-ed54-41db-805e-ebfd9b9a6e67\":\"0\",\"col-e3e3dce4-fbd4-4a52-a0bd-5d54af7a7a08\":\"0\",\"col-3463c48b-4b94-4500-b14f-4a554284b94f\":\"0.0\",\"col-b72a5284-7c06-47f9-9e37-c88c3b54c6ae\":\"0.0\",\"col-7781d665-6951-4244-b9bc-a28e477e2d57\":\"true\",\"col-05f332c4-ebdb-4437-9e80-e23f92bee4a2\":\"0\",\"col-c025b8f8-481c-4db2-8932-f37129146ceb\":\"2021-09-08\",\"col-bd963d5f-2199-4700-b5d6-0759bd7a9d90\":\"2021-09-08 11:11:11\"},\"size\":632,\"modificationTime\":1687761154336,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":\\\"0\\\"},\\\"maxValues\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":\\\"0\\\"},\\\"nullCount\\\":{\\\"col-9045d285-9170-4b1b-acc4-1ce4ee176916\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/data-reader-primitives-column-mapping-name/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1687757789720,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"11\",\"numOutputBytes\":\"8996\"},\"engineInfo\":\"Apache-Spark/3.3.1 Delta-Lake/2.3.0\",\"txnId\":\"95e57353-d8fc-4e4e-a7ee-0c56559054d9\"}}\n{\"protocol\":{\"minReaderVersion\":2,\"minWriterVersion\":5}}\n{\"metaData\":{\"id\":\"02a552b7-5f9f-4fef-a992-ddc436e735cf\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"as_int\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\"}},{\\\"name\\\":\\\"as_long\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":2,\\\"delta.columnMapping.physicalName\\\":\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\"}},{\\\"name\\\":\\\"as_byte\\\",\\\"type\\\":\\\"byte\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":3,\\\"delta.columnMapping.physicalName\\\":\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\"}},{\\\"name\\\":\\\"as_short\\\",\\\"type\\\":\\\"short\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":4,\\\"delta.columnMapping.physicalName\\\":\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\"}},{\\\"name\\\":\\\"as_boolean\\\",\\\"type\\\":\\\"boolean\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":5,\\\"delta.columnMapping.physicalName\\\":\\\"col-eded3bff-704e-4046-97e6-1395b1e38f2a\\\"}},{\\\"name\\\":\\\"as_float\\\",\\\"type\\\":\\\"float\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":6,\\\"delta.columnMapping.physicalName\\\":\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\"}},{\\\"name\\\":\\\"as_double\\\",\\\"type\\\":\\\"double\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":7,\\\"delta.columnMapping.physicalName\\\":\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\"}},{\\\"name\\\":\\\"as_string\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":8,\\\"delta.columnMapping.physicalName\\\":\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\"}},{\\\"name\\\":\\\"as_binary\\\",\\\"type\\\":\\\"binary\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":9,\\\"delta.columnMapping.physicalName\\\":\\\"col-7441db29-eefe-4fed-b11c-a0886325267e\\\"}},{\\\"name\\\":\\\"as_big_decimal\\\",\\\"type\\\":\\\"decimal(1,0)\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":10,\\\"delta.columnMapping.physicalName\\\":\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.columnMapping.maxColumnId\":\"10\"},\"createdTime\":1687757788998}}\n{\"add\":{\"path\":\"part-00000-dedd3195-6cd1-451d-83b8-fe0028f9b2b6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4542,\"modificationTime\":1687757789686,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":6,\\\"minValues\\\":{\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\":4,\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\":4,\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\":4,\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\":4,\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\":4.0,\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\":4.0,\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\":\\\"4\\\",\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\":4},\\\"maxValues\\\":{\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\":9,\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\":9,\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\":9,\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\":9,\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\":9.0,\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\":9.0,\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\":\\\"9\\\",\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\":9},\\\"nullCount\\\":{\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\":0,\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\":0,\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\":0,\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\":0,\\\"col-eded3bff-704e-4046-97e6-1395b1e38f2a\\\":0,\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\":0,\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\":0,\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\":0,\\\"col-7441db29-eefe-4fed-b11c-a0886325267e\\\":0,\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-d8bdfc55-29fe-40bc-bfe4-f7732d559aa9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":4454,\"modificationTime\":1687757789686,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\":0,\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\":0,\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\":0,\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\":0,\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\":0.0,\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\":0.0,\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\":\\\"0\\\",\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\":0},\\\"maxValues\\\":{\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\":3,\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\":3,\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\":3,\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\":3,\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\":3.0,\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\":3.0,\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\":\\\"3\\\",\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\":3},\\\"nullCount\\\":{\\\"col-41726333-d452-47f3-b9ed-0adff6ffeabf\\\":1,\\\"col-668f7731-ddc9-4523-83cd-9c33d739c7d4\\\":1,\\\"col-6fbaa34c-ff0e-45c3-ab66-efdf765dd606\\\":1,\\\"col-bbfb738d-7298-4c00-a465-44b80ca3c97a\\\":1,\\\"col-eded3bff-704e-4046-97e6-1395b1e38f2a\\\":1,\\\"col-97d3b9ba-c7e6-4f97-8e64-b4ffd3c836ca\\\":1,\\\"col-438778fc-0408-4a26-9aca-9ce1d7d3b63d\\\":1,\\\"col-ce49aefa-a5a0-4f8f-80c1-ae3ae9918ffa\\\":1,\\\"col-7441db29-eefe-4fed-b11c-a0886325267e\\\":1,\\\"col-1dbc7e88-d12e-4e0c-b35d-1d504f71207a\\\":1}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files/1.json",
    "content": "{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"partitionValues\":{},\"size\":348,\"modificationTime\":1603723974000,\"dataChange\":true}\n{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":687,\"modificationTime\":1603723972000,\"dataChange\":true}\n{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":705,\"modificationTime\":1603723972000,\"dataChange\":true}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files/2.json",
    "content": "{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}\n{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files/3.json",
    "content": "{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}\n{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-all-empty/1.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-all-empty/2.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-all-empty/3.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/1.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/2.json",
    "content": "{\"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\"partitionValues\":{},\"size\":348,\"modificationTime\":1603723974000,\"dataChange\":true}\n{\"path\":\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":687,\"modificationTime\":1603723972000,\"dataChange\":true}\n{\"path\":\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":705,\"modificationTime\":1603723972000,\"dataChange\":true}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/3.json",
    "content": "{\"path\":\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}\n{\"path\":\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":650,\"modificationTime\":1603723967000,\"dataChange\":true}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/4.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/5.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/6.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/7.json",
    "content": "{\"path\":\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}\n{\"path\":\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":649,\"modificationTime\":1603723970000,\"dataChange\":true}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/json-files-with-empty/8.json",
    "content": ""
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-all-jsons/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013250800,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"50\",\"numOutputBytes\":\"2636\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"42c3eb4a-9e6e-4c19-9893-96106670b540\"}}\n{\"metaData\":{\"id\":\"b1624866-6060-42ab-9bbc-831735240dd3\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1752013249002}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-5aead402-bb3a-4c76-9d78-67c09cfcfa8a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":528,\"modificationTime\":1752013250522,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-f7ad6879-5e8e-4ac2-817f-2dc95477d7d7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013250614,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":19},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-021db285-9b02-4973-a282-afcfd8afd007-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013250650,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":20},\\\"maxValues\\\":{\\\"id\\\":29},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-a2307510-6cac-4772-8ad1-94e874534491-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013250686,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":30},\\\"maxValues\\\":{\\\"id\\\":39},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00004-39753157-19bf-4a6d-b65d-04505516762d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013250722,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":40},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-all-jsons/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013252241,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"50\",\"numOutputBytes\":\"2636\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"f460664f-8dbf-4775-8a06-0ed95a058c6e\"}}\n{\"add\":{\"path\":\"part-00000-520b63c8-7004-4fb3-9a7d-6b1ee913d1ac-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013252130,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":59},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-5236d99c-44d0-4eb3-b9a6-c84066e73742-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013252154,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":60},\\\"maxValues\\\":{\\\"id\\\":69},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-113a09fa-8d92-4285-a192-75d9ef68ccdf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013252182,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":70},\\\"maxValues\\\":{\\\"id\\\":79},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-d6736222-7d56-4b90-ad5e-abea2a47353d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":528,\"modificationTime\":1752013252206,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":80},\\\"maxValues\\\":{\\\"id\\\":89},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00004-b0a130bc-96cd-4b89-8d24-253f9fea21cb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013252230,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":90},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-all-jsons/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013252497,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"5\",\"numOutputRows\":\"50\",\"numOutputBytes\":\"2640\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"b8cd7f95-9f09-467c-a151-73ca0bbfa40f\"}}\n{\"add\":{\"path\":\"part-00000-8dc5e78e-a25c-47ed-8025-e171c85ace7a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":529,\"modificationTime\":1752013252394,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":100},\\\"maxValues\\\":{\\\"id\\\":109},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-5109896f-2a5e-4e79-b445-8966c6c82ef0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":529,\"modificationTime\":1752013252418,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":110},\\\"maxValues\\\":{\\\"id\\\":119},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-38299dc0-eb52-4e16-a1df-f3c6bb37eaf4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":526,\"modificationTime\":1752013252442,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":120},\\\"maxValues\\\":{\\\"id\\\":129},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-90bfacb7-34aa-4dab-bc3d-9367d9045103-c000.snappy.parquet\",\"partitionValues\":{},\"size\":527,\"modificationTime\":1752013252462,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":130},\\\"maxValues\\\":{\\\"id\\\":139},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00004-cdc9fd11-6e89-45d0-8941-a74a104fde75-c000.snappy.parquet\",\"partitionValues\":{},\"size\":529,\"modificationTime\":1752013252486,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":140},\\\"maxValues\\\":{\\\"id\\\":149},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-all-jsons/info.txt",
    "content": "# Below are scala codes used to create the `kernel-pagination-all-jsons` table.\n\n// First commit: files 0-4 (5 files)\nspark.range(0, 50, 1, 5).write.format(\"delta\").save(tablePath)\n\n// Second commit: files 5-9 (5 more files)\nspark.range(50, 100, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n// Third commit: files 10-14 (5 more files)\nspark.range(100, 150, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-multi-part-checkpoints/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752557234741,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"18\",\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numOutputRows\":\"1800\",\"numOutputBytes\":\"16351\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"c49abce0-3b91-4c04-8a24-bb38cf9bb311\"}}\n{\"metaData\":{\"id\":\"3a943196-646c-42a6-b7ac-e8a27c0f231a\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1752557231235}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-78e9541a-f565-4548-a474-a675a4aab37f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":906,\"modificationTime\":1752557233141,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":1785},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-54a44075-ed30-4dc5-b4a2-59a91f28fd37-c000.snappy.parquet\",\"partitionValues\":{},\"size\":911,\"modificationTime\":1752557233225,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":20},\\\"maxValues\\\":{\\\"id\\\":1786},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-9f4f85c0-5694-4b0c-9cfb-3b616460786c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":911,\"modificationTime\":1752557233265,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":16},\\\"maxValues\\\":{\\\"id\\\":1789},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-e33eff7b-6300-4cc5-8612-27f4959d10eb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":908,\"modificationTime\":1752557233305,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":12},\\\"maxValues\\\":{\\\"id\\\":1769},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00004-a5f332bd-bf7f-4552-a0ae-011cc6888787-c000.snappy.parquet\",\"partitionValues\":{},\"size\":912,\"modificationTime\":1752557233337,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":1759},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00005-4d98fede-302c-4db9-817e-ab226e024e63-c000.snappy.parquet\",\"partitionValues\":{},\"size\":902,\"modificationTime\":1752557233369,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":59},\\\"maxValues\\\":{\\\"id\\\":1771},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00006-06a29b13-6f35-42fa-a937-1403c16c5d9f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":912,\"modificationTime\":1752557233405,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":48},\\\"maxValues\\\":{\\\"id\\\":1798},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00007-6c15fb5f-ece5-4067-bff3-fe2b4481066e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":909,\"modificationTime\":1752557233441,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1797},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00008-2e216ef2-8209-4c29-a775-d1ff656485a1-c000.snappy.parquet\",\"partitionValues\":{},\"size\":913,\"modificationTime\":1752557233473,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":1799},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00009-ba5132c6-d400-4260-9b1e-59d575d34e45-c000.snappy.parquet\",\"partitionValues\":{},\"size\":900,\"modificationTime\":1752557233505,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":1753},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00010-1b506159-7065-4f32-886b-b7ac88b9f25a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":905,\"modificationTime\":1752557233533,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":13},\\\"maxValues\\\":{\\\"id\\\":1777},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00011-679b09b3-3872-4ead-98a3-97ad55c83560-c000.snappy.parquet\",\"partitionValues\":{},\"size\":914,\"modificationTime\":1752557233565,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":27},\\\"maxValues\\\":{\\\"id\\\":1792},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00012-7773a168-219f-46fe-a977-807219b9facb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":916,\"modificationTime\":1752557233593,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":19},\\\"maxValues\\\":{\\\"id\\\":1779},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00013-340640ec-f6d0-4ca8-a015-8b6be2045627-c000.snappy.parquet\",\"partitionValues\":{},\"size\":905,\"modificationTime\":1752557233617,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":6},\\\"maxValues\\\":{\\\"id\\\":1766},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00014-61a01309-3035-43dd-ab57-722dc7a1d6c7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":913,\"modificationTime\":1752557233649,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":3},\\\"maxValues\\\":{\\\"id\\\":1796},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00015-c4272dab-2228-43fc-9a33-f778a584e0f8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":905,\"modificationTime\":1752557233677,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":1788},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00016-e5846569-bace-41ec-9652-81e1a5b01b31-c000.snappy.parquet\",\"partitionValues\":{},\"size\":900,\"modificationTime\":1752557233701,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":28},\\\"maxValues\\\":{\\\"id\\\":1743},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00017-2057bacd-70e6-43b9-be0b-eb6787dfb990-c000.snappy.parquet\",\"partitionValues\":{},\"size\":909,\"modificationTime\":1752557233729,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":11},\\\"maxValues\\\":{\\\"id\\\":1795},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-multi-part-checkpoints/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752557240968,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"898\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"b94a28fc-9c29-4f85-b27d-a3b6127dab7c\"}}\n{\"add\":{\"path\":\"part-00000-99790b5f-5683-4194-8d08-b3c87c854589-c000.snappy.parquet\",\"partitionValues\":{},\"size\":898,\"modificationTime\":1752557240957,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":1000},\\\"maxValues\\\":{\\\"id\\\":1099},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-multi-part-checkpoints/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752557241128,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"896\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"4be32135-c242-4d3e-b0fd-9d0ef5e088ea\"}}\n{\"add\":{\"path\":\"part-00000-3ae77526-4e2f-4408-b0fd-769dfed78395-c000.snappy.parquet\",\"partitionValues\":{},\"size\":896,\"modificationTime\":1752557241121,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":2000},\\\"maxValues\\\":{\\\"id\\\":2099},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-multi-part-checkpoints/_delta_log/_last_checkpoint",
    "content": "{\"version\":0,\"size\":20,\"parts\":3,\"sizeInBytes\":46186,\"numOfAddFiles\":18,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"clusteringProvider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"30afa70634ba9cf813dc0270c5c5ad03\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-multi-part-checkpoints/info.txt",
    "content": "# Below are scala codes used to create the `kernel-pagination-multi-part-checkpoints` table.\n\n// Create one commit with 10 files (10 AddFile actions)\nspark.range(0, 1800)\n     .repartition(18) // 10 files = 10 AddFile actions\n     .write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n\n// Force multi-part checkpoint creation with small part size\nwithSQLConf(\n    \"spark.databricks.delta.checkpoint.partSize\" -> \"6\" // 10 AddFiles → 3 checkpoint parts\n    ) {\n    val deltaLog = DeltaLog.forTable(spark, tablePath)\n    deltaLog.checkpoint() // multi-part checkpoint at version 0\n    }\n\n// Commits 1 and 2: Add 1 file each\nfor (i <- 1 to 2) {\n    spark.range(i * 1000, i * 1000 + 100) // small data\n    .coalesce(1) // 1 file\n    .write.format(\"delta\").mode(\"append\").save(tablePath)\n}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013810389,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1003\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"36c63f66-f7e7-44cb-a4cc-2a51a2bdd83d\"}}\n{\"metaData\":{\"id\":\"a0079aa7-9f6d-435b-979c-39075ebef610\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1752013807695}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"add\":{\"path\":\"part-00000-677492ad-40aa-40c6-a1f0-bf9c7dd641e2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":500,\"modificationTime\":1752013809237,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-c8077b5a-da61-4f74-a958-fa885076c35b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013809337,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013811028,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"f77d1887-7e69-45a1-8093-08081b90f025\"}}\n{\"add\":{\"path\":\"part-00000-c5b4b509-af7c-42b7-bbf5-8797cdb5eeaa-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013810989,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":10},\\\"maxValues\\\":{\\\"id\\\":14},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-7c0e1634-1491-4f2d-9eec-15950d9d900d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811017,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":15},\\\"maxValues\\\":{\\\"id\\\":19},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013811224,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"c0447f48-f289-4bf6-a914-0cb5fc080e02\"}}\n{\"add\":{\"path\":\"part-00000-852814f7-34c8-43da-9c22-8e5f8bd0d571-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811189,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":20},\\\"maxValues\\\":{\\\"id\\\":24},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-8410fa8f-df52-445b-82ea-6d03366945d0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811217,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":25},\\\"maxValues\\\":{\\\"id\\\":29},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013811405,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"71adef37-5fe3-4b27-9fe7-ce19b28db68c\"}}\n{\"add\":{\"path\":\"part-00000-ed129b33-0211-4af7-bdce-432cf823c5b7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811373,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":30},\\\"maxValues\\\":{\\\"id\\\":34},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-7d8b44fb-445c-4e16-a1ff-75da579203db-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811397,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":35},\\\"maxValues\\\":{\\\"id\\\":39},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013811603,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"39db7b90-7aa6-4500-a0bb-367a0853ca29\"}}\n{\"add\":{\"path\":\"part-00000-f54d5bbb-fe06-4c91-a1ed-500abce87546-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811569,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":40},\\\"maxValues\\\":{\\\"id\\\":44},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-eec9d516-0606-439e-9067-4edd54ce97c6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811597,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":45},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013811776,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":4,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"09a1fec1-8c16-44e2-8f4d-7ce2e8fa256d\"}}\n{\"add\":{\"path\":\"part-00000-0c720b4a-672d-4bcc-a267-d79c3c6dbc8f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811745,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":54},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-827b6732-c283-437a-a9e0-95458ded5340-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811769,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":55},\\\"maxValues\\\":{\\\"id\\\":59},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000006.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013811948,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":5,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"6a44dc2d-88f3-4c0e-9eef-e7211ff4aa1f\"}}\n{\"add\":{\"path\":\"part-00000-3e44afef-2600-426f-9f47-771f7ace1868-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811917,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":60},\\\"maxValues\\\":{\\\"id\\\":64},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-af89282e-3e0f-4589-9862-274a3e343245-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013811941,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":65},\\\"maxValues\\\":{\\\"id\\\":69},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000007.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013812128,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":6,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"885285be-5392-414f-a196-44ca35943841\"}}\n{\"add\":{\"path\":\"part-00000-b29b2c0c-a58e-43d8-880b-9a113c27035e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013812097,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":70},\\\"maxValues\\\":{\\\"id\\\":74},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-0cea8033-06b0-4cbf-b32b-f14cc03fae77-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013812121,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":75},\\\"maxValues\\\":{\\\"id\\\":79},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000008.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013812293,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":7,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1007\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"52f0e2e3-71bb-4d3a-816a-40fcbccf5326\"}}\n{\"add\":{\"path\":\"part-00000-24d7f48b-c0c4-4363-a208-2a6417e19022-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013812265,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":80},\\\"maxValues\\\":{\\\"id\\\":84},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-4014c89a-e2c8-44ea-8421-86d80922ed3d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":504,\"modificationTime\":1752013812285,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":85},\\\"maxValues\\\":{\\\"id\\\":89},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000009.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013812464,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":8,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"b814cdd8-7a8a-4463-b581-0de0176c023d\"}}\n{\"add\":{\"path\":\"part-00000-016be37f-0c8d-470d-b7b1-160515f14dc2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013812433,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":90},\\\"maxValues\\\":{\\\"id\\\":94},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-81d82b44-4bce-405c-b483-ff741b45d3ef-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013812457,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":95},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000010.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013817910,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":9,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1005\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"e8bef162-9e06-44e2-8ba5-cc8b1358061a\"}}\n{\"add\":{\"path\":\"part-00000-70b4a723-351c-4495-b4ea-088d5637d901-c000.snappy.parquet\",\"partitionValues\":{},\"size\":502,\"modificationTime\":1752013817881,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":100},\\\"maxValues\\\":{\\\"id\\\":104},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-d723c7f5-e27d-4a89-b4a9-586a6c960721-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013817901,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":105},\\\"maxValues\\\":{\\\"id\\\":109},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000011.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013821726,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":10,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1007\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"8feb199d-27a1-4859-bf04-cd3b69c1db17\"}}\n{\"add\":{\"path\":\"part-00000-2ae23645-7b78-491f-aa29-01ba885bba82-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013821701,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":110},\\\"maxValues\\\":{\\\"id\\\":114},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-2605f1b1-7042-411e-850a-a969e55275ae-c000.snappy.parquet\",\"partitionValues\":{},\"size\":504,\"modificationTime\":1752013821721,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":115},\\\"maxValues\\\":{\\\"id\\\":119},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/00000000000000000012.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752013821874,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":11,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1006\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"bcabca67-f668-4e33-a51f-59f8d73f63ae\"}}\n{\"add\":{\"path\":\"part-00000-39319b4a-4a46-4698-a3f0-0b9f42ff9fad-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013821849,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":120},\\\"maxValues\\\":{\\\"id\\\":124},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-d25a786d-5ecc-4d53-98e3-b82dae5627da-c000.snappy.parquet\",\"partitionValues\":{},\"size\":503,\"modificationTime\":1752013821869,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"id\\\":125},\\\"maxValues\\\":{\\\"id\\\":129},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":10,\"size\":24,\"sizeInBytes\":16991,\"numOfAddFiles\":22,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"clusteringProvider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"581fbce1d7a7e20ce1931e12ff67de8a\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-single-checkpoint/info.txt",
    "content": "# Below are scala codes used to create the `kernel-pagination-single-checkpoint` table.\n\n// First, create 10 commits\nfor (i <- 0 until 10) {\n    val mode = if (i == 0) \"overwrite\" else \"append\"\n    spark.range(i * 10, (i + 1) * 10, 1, 2)\n         .write.format(\"delta\").mode(mode).save(tablePath)\n}\n// Force checkpoint creation\nval deltaLog = DeltaLog.forTable(spark, tablePath)\ndeltaLog.checkpoint()\n\n// Add a few more commits after checkpoint to create additional JSON files\nfor (i <- 10 until 13) {\n    spark.range(i * 10, (i + 1) * 10, 1, 2)\n         .write.format(\"delta\").mode(\"append\").save(tablePath)\n}"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496114594,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"f6282e54-afc6-4669-939b-0f8ba73062a0\"}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1714496114564}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496114748,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.checkpointPolicy\\\":\\\"v2\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"fddb3112-ca9b-48af-bf19-be23f1c36c22\"}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000002.checkpoint.6374b053-df23-479b-b2cf-c9c550132b49.json",
    "content": "{\"checkpointMetadata\":{\"version\":2}}\n{\"sidecar\":{\"path\":\"00000000000000000002.checkpoint.0000000001.0000000002.bd1885fd-6ec0-4370-b0f5-43b5162fd4de.parquet\",\"sizeInBytes\":9367,\"modificationTime\":1714496115780}}\n{\"sidecar\":{\"path\":\"00000000000000000002.checkpoint.0000000002.0000000002.0a8d73ee-aa83-49d0-9583-c99db75b89b2.parquet\",\"sizeInBytes\":9296,\"modificationTime\":1714496115788}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496115090,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1952\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"a76e8fca-8bab-42cc-9618-77f8c536968c\"}}\n{\"add\":{\"path\":\"part-00000-240b5dd6-323b-4f74-b6bc-ab9fdcacc630-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496115046,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":8},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-534ea355-2edd-4046-8d49-d932469170c7-c000.snappy.parquet\",\"partitionValues\":{},\"size\":496,\"modificationTime\":1714496115048,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-4438bc9d-9c60-4dd2-9343-574743ea4ca8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":486,\"modificationTime\":1714496115087,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":5},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-ae431d66-23d5-4dc7-b961-136ce33e63da-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496115087,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":6},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752616665239,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"898\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"2507ea29-2720-4e92-98f9-4a251850513c\"}}\n{\"add\":{\"path\":\"part-00000-813a3813-84c9-4251-bbe6-f6502a32b833-c000.snappy.parquet\",\"partitionValues\":{},\"size\":898,\"modificationTime\":1752616665166,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":1000},\\\"maxValues\\\":{\\\"id\\\":1099},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000004.checkpoint.a2670232-dd52-4e21-8ba7-1f70fe762bce.json",
    "content": "{\"checkpointMetadata\":{\"version\":4}}\n{\"sidecar\":{\"path\":\"00000000000000000004.checkpoint.0000000001.0000000001.019924f2-3318-4cca-a460-b7d0b75f0d0f.parquet\",\"sizeInBytes\":9511,\"modificationTime\":1752616673806}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752616670215,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"896\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"124835df-5eac-4b42-8ce6-969f94e839e8\"}}\n{\"add\":{\"path\":\"part-00000-9a247ca4-22bf-4173-bd69-66401dad2178-c000.snappy.parquet\",\"partitionValues\":{},\"size\":896,\"modificationTime\":1752616670206,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":2000},\\\"maxValues\\\":{\\\"id\\\":2099},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-json/_delta_log/_last_checkpoint",
    "content": "{\"version\":4,\"size\":10,\"sizeInBytes\":10228,\"numOfAddFiles\":6,\"v2Checkpoint\":{\"path\":\"00000000000000000004.checkpoint.a2670232-dd52-4e21-8ba7-1f70fe762bce.json\",\"sizeInBytes\":717,\"modificationTime\":1752616673818,\"nonFileActions\":[{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}},{\"metaData\":{\"id\":\"8a390218-e4ee-4341-b6de-4920e27d3f78\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496114564}},{\"checkpointMetadata\":{\"version\":4}}],\"sidecarFiles\":[{\"path\":\"00000000000000000004.checkpoint.0000000001.0000000001.019924f2-3318-4cca-a460-b7d0b75f0d0f.parquet\",\"sizeInBytes\":9511,\"modificationTime\":1752616673806}]},\"checksum\":\"e0e16b97d85501a7f67b00c24aaa07f2\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496109365,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"7517176e-cff7-46ac-b133-3cf096e2620d\"}}\n{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1714496109258}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496110834,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.checkpointPolicy\\\":\\\"v2\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"12ea26b9-c620-4104-95f6-654bcaabdda6\"}}\n{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496109258}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1714496112086,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"4\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"1952\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.2.0-SNAPSHOT\",\"txnId\":\"c9f86c17-1b30-44e7-873d-1e2102f54b0f\"}}\n{\"add\":{\"path\":\"part-00000-485b0fff-1c7b-4f14-92e9-a72300fcdf88-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496111974,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":8},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-f7a80035-0622-431e-832e-a756c65cb2a5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":496,\"modificationTime\":1714496111974,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-5754df9c-5a25-43a6-947b-f27840fddb1a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":486,\"modificationTime\":1714496112068,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":5},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00003-6ab7bbbb-e14d-4fa3-8767-06b509e0a666-c000.snappy.parquet\",\"partitionValues\":{},\"size\":485,\"modificationTime\":1714496112071,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":6},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752616904697,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"898\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"afeb48b0-dc63-4af9-b6b9-45796b6043b9\"}}\n{\"add\":{\"path\":\"part-00000-4f78beda-ea4d-4ab3-95fc-ab68e40b3fce-c000.snappy.parquet\",\"partitionValues\":{},\"size\":898,\"modificationTime\":1752616904619,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":1000},\\\"maxValues\\\":{\\\"id\\\":1099},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/00000000000000000004.checkpoint.1391a262-4df6-494d-8166-dcd139a6ba46.json",
    "content": "{\"checkpointMetadata\":{\"version\":4}}\n{\"sidecar\":{\"path\":\"00000000000000000004.checkpoint.0000000001.0000000001.87b3aafd-6627-401d-b9aa-83f6b2450f0a.parquet\",\"sizeInBytes\":9518,\"modificationTime\":1752616913834}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}}\n{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496109258}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1752616909723,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"896\"},\"engineInfo\":\"Apache-Spark/3.5.3 Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"b487a2c8-bf55-4773-be11-506b2d150450\"}}\n{\"add\":{\"path\":\"part-00000-38f3b7ca-0e92-449a-a2ff-0d4b7c7908f3-c000.snappy.parquet\",\"partitionValues\":{},\"size\":896,\"modificationTime\":1752616909714,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"id\\\":2000},\\\"maxValues\\\":{\\\"id\\\":2099},\\\"nullCount\\\":{\\\"id\\\":0}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/kernel-pagination-v2-checkpoint-parquet/_delta_log/_last_checkpoint",
    "content": "{\"version\":4,\"size\":10,\"sizeInBytes\":10235,\"numOfAddFiles\":6,\"v2Checkpoint\":{\"path\":\"00000000000000000004.checkpoint.1391a262-4df6-494d-8166-dcd139a6ba46.json\",\"sizeInBytes\":717,\"modificationTime\":1752616913846,\"nonFileActions\":[{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"v2Checkpoint\"],\"writerFeatures\":[\"v2Checkpoint\",\"appendOnly\",\"invariants\"]}},{\"metaData\":{\"id\":\"7e2a1106-198b-4653-a612-2aa44685cb27\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\",\"delta.checkpointPolicy\":\"v2\"},\"createdTime\":1714496109258}},{\"checkpointMetadata\":{\"version\":4}}],\"sidecarFiles\":[{\"path\":\"00000000000000000004.checkpoint.0000000001.0000000001.87b3aafd-6627-401d-b9aa-83f6b2450f0a.parquet\",\"sizeInBytes\":9518,\"modificationTime\":1752616913834}]},\"checksum\":\"88938ad4c49a232427f870052fb92743\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/log4j2.properties",
    "content": "#\n#  Copyright (2025) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n# Set everything to be logged to the file target/unit-tests.log\nrootLogger.level = warn\nrootLogger.appenderRef.file.ref = ${sys:test.appender:-File}\n\nappender.file.type = File\nappender.file.name = File\nappender.file.fileName = target/unit-tests.log\nappender.file.append = true\nappender.file.layout.type = PatternLayout\nappender.file.layout.pattern = %d{yy/MM/dd HH:mm:ss.SSS} %t %p %c{1}: %m%n\n\n# Tests that launch java subprocesses can set the \"test.appender\" system property to\n# \"console\" to avoid having the child process's logs overwrite the unit test's\n# log file.\nappender.console.type = Console\nappender.console.name = console\nappender.console.target = SYSTEM_ERR\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = %t: %m%n\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/00000000000000000000.crc",
    "content": "{\"txnId\":\"e7ea145a-a509-48d3-b233-b1333e7ddb17\",\"tableSizeBytes\":14741,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"757b2255-cffd-4165-90b4-b491beb21ba1\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableVariantShredding\":\"true\",\"delta.checkpointInterval\":\"2\"},\"createdTime\":1747170231449},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantShredding-preview\",\"variantType\"],\"writerFeatures\":[\"variantShredding-preview\",\"variantType\",\"appendOnly\",\"invariants\"]},\"allFiles\":[{\"path\":\"test%25file%25prefix-part-00001-0ffdbb7b-af76-4d0b-9cbe-08bb2091c1c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7311,\"modificationTime\":1747170233554,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"},{\"path\":\"test%25file%25prefix-part-00000-24104cdb-691b-4410-a2a1-afc84fe2ea18-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7430,\"modificationTime\":1747170233554,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}]}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1747170234128,\"operation\":\"CREATE OR REPLACE TABLE AS SELECT\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.enableVariantShredding\\\":\\\"true\\\",\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numRemovedFiles\":\"0\",\"numRemovedBytes\":\"0\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"14741\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"e7ea145a-a509-48d3-b233-b1333e7ddb17\"}}\n{\"metaData\":{\"id\":\"757b2255-cffd-4165-90b4-b491beb21ba1\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableVariantShredding\":\"true\",\"delta.checkpointInterval\":\"2\"},\"createdTime\":1747170231449}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantShredding-preview\",\"variantType\"],\"writerFeatures\":[\"variantShredding-preview\",\"variantType\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00000-24104cdb-691b-4410-a2a1-afc84fe2ea18-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7430,\"modificationTime\":1747170233554,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00001-0ffdbb7b-af76-4d0b-9cbe-08bb2091c1c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7311,\"modificationTime\":1747170233554,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/00000000000000000001.crc",
    "content": "{\"txnId\":\"913a574c-1aeb-4834-82ba-2dc334dfb584\",\"tableSizeBytes\":19801,\"numFiles\":3,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"757b2255-cffd-4165-90b4-b491beb21ba1\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableVariantShredding\":\"true\",\"delta.checkpointInterval\":\"2\"},\"createdTime\":1747170231449},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantShredding-preview\",\"variantType\"],\"writerFeatures\":[\"variantShredding-preview\",\"variantType\",\"appendOnly\",\"invariants\"]},\"allFiles\":[{\"path\":\"test%25file%25prefix-part-00000-5ed80cd3-35e4-419e-bf56-e685f8634cbf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5060,\"modificationTime\":1747170236539,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"},{\"path\":\"test%25file%25prefix-part-00001-0ffdbb7b-af76-4d0b-9cbe-08bb2091c1c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7311,\"modificationTime\":1747170233554,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"},{\"path\":\"test%25file%25prefix-part-00000-24104cdb-691b-4410-a2a1-afc84fe2ea18-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7430,\"modificationTime\":1747170233554,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}]}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1747170236545,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"5060\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"913a574c-1aeb-4834-82ba-2dc334dfb584\"}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00000-5ed80cd3-35e4-419e-bf56-e685f8634cbf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5060,\"modificationTime\":1747170236539,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/00000000000000000002.crc",
    "content": "{\"txnId\":\"b08424c1-854d-443b-b615-b356164f37c5\",\"tableSizeBytes\":24861,\"numFiles\":4,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"757b2255-cffd-4165-90b4-b491beb21ba1\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableVariantShredding\":\"true\",\"delta.checkpointInterval\":\"2\"},\"createdTime\":1747170231449},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantShredding-preview\",\"variantType\"],\"writerFeatures\":[\"variantShredding-preview\",\"variantType\",\"appendOnly\",\"invariants\"]},\"allFiles\":[{\"path\":\"test%25file%25prefix-part-00000-bda6fee1-d8d4-4a8b-a1fb-eb171758ef40-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5060,\"modificationTime\":1747170237486,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"},{\"path\":\"test%25file%25prefix-part-00000-5ed80cd3-35e4-419e-bf56-e685f8634cbf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5060,\"modificationTime\":1747170236539,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"},{\"path\":\"test%25file%25prefix-part-00001-0ffdbb7b-af76-4d0b-9cbe-08bb2091c1c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7311,\"modificationTime\":1747170233554,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"},{\"path\":\"test%25file%25prefix-part-00000-24104cdb-691b-4410-a2a1-afc84fe2ea18-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7430,\"modificationTime\":1747170233554,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}]}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1747170237490,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"5060\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.4.0-SNAPSHOT\",\"txnId\":\"b08424c1-854d-443b-b615-b356164f37c5\"}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00000-bda6fee1-d8d4-4a8b-a1fb-eb171758ef40-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5060,\"modificationTime\":1747170237486,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":6,\"sizeInBytes\":21895,\"numOfAddFiles\":4,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"clusteringProvider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"49753c4b48895f36efac7342b5db921b\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-shredded-variant-preview-delta/info.txt",
    "content": "This file contains the code used to generate this golden table \"spark-variant-checkpoint\"\n\nUsing delta-spark 4.0, run the following scala script:\n\nval tableName = \"<REPLACE WITH THE TABLE NAME OR PATH>\"\nval query = \"\"\"\n  with jsonStrings as (\n    select\n      id,\n      format_string('{\"key\": %s}', id) as jsonString\n    from\n      range(0, 100)\n  )\n  select\n    id,\n    parse_json(jsonString) as v,\n    array(\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString)\n    ) as array_of_variants,\n    named_struct('v', parse_json(jsonString)) as struct_of_variants,\n    map(\n      cast(id as string),\n      parse_json(jsonString),\n      'nullKey',\n      null\n    ) as map_of_variants,\n    array(\n      named_struct('v', parse_json(jsonString)),\n      named_struct('v', null),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      ),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      )\n    ) as array_of_struct_of_variants,\n    named_struct(\n      'v',\n      array(\n        null,\n        parse_json(jsonString)\n      )\n    ) as struct_of_array_of_variants\n  from\n    jsonStrings\n\"\"\"\n\nval writeToTableSql = s\"\"\"\n  create or replace table $tableName\n  USING DELTA TBLPROPERTIES\n  (delta.checkpointInterval = 2,\n  '${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'true')\n\"\"\"\n\nspark.sql(s\"${writeToTableSql}\\n${query}\")\n// Write two additional rows to create a checkpoint.\n(0 until 2).foreach { v =>\n  spark\n    .sql(query)\n    .where(s\"id = $v\")\n    .write\n    .format(\"delta\")\n    .mode(\"append\")\n    .insertInto(tableName)\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723768497710,\"operation\":\"CREATE OR REPLACE TABLE AS SELECT\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"100\",\"numOutputBytes\":\"14767\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"2cc10429-f586-4c74-805c-8d19fd180c87\"}}\n{\"metaData\":{\"id\":\"d7eb0848-b002-4e0b-9d8d-dd335c90946f\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1723768495302}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType-preview\"],\"writerFeatures\":[\"variantType-preview\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"part-00000-16c852df-ba66-4080-be25-530a05922422-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7443,\"modificationTime\":1723768496908,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":49},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n{\"add\":{\"path\":\"part-00001-664313d3-14b4-4dbf-8110-77001b877182-c000.snappy.parquet\",\"partitionValues\":{},\"size\":7324,\"modificationTime\":1723768496908,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":50,\\\"minValues\\\":{\\\"id\\\":50},\\\"maxValues\\\":{\\\"id\\\":99},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723768498557,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"5072\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"78417efa-a13f-45df-add0-f96aa113fd68\"}}\n{\"add\":{\"path\":\"part-00000-9a9c570c-ee32-4322-ad2f-8c837a77d398-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5072,\"modificationTime\":1723768498551,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":0},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-checkpoint/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1723768498990,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"5072\"},\"engineInfo\":\"Apache-Spark/4.0.0-SNAPSHOT Delta-Lake/3.3.0-SNAPSHOT\",\"txnId\":\"d90393d5-9cdd-40f1-8861-121f2169808b\"}}\n{\"add\":{\"path\":\"part-00000-1e14ba22-3114-46d1-96fb-48b4912507ce-c000.snappy.parquet\",\"partitionValues\":{},\"size\":5072,\"modificationTime\":1723768498986,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\"}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-checkpoint/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":6,\"sizeInBytes\":21929,\"numOfAddFiles\":4,\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"txn\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"appId\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"version\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"lastUpdated\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"add\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"modificationTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"clusteringProvider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"stats\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"remove\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"path\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionTimestamp\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"dataChange\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"extendedFileMetadata\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionValues\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"size\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"deletionVector\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"storageType\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"pathOrInlineDv\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"offset\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"sizeInBytes\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"cardinality\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"maxRowIndex\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"baseRowId\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"defaultRowCommitVersion\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"metaData\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"description\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"format\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"provider\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"options\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"schemaString\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"partitionColumns\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":{\"type\":\"map\",\"keyType\":\"string\",\"valueType\":\"string\",\"valueContainsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"createdTime\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"protocol\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"minReaderVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"minWriterVersion\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"readerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}},{\"name\":\"writerFeatures\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"domainMetadata\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"domain\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"configuration\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"removed\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"checksum\":\"a8d400a03ead8a86dbb412f2a693e26e\"}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-checkpoint/info.txt",
    "content": "This file contains the code used to generate this golden table \"spark-variant-checkpoint\"\n\nUsing delta-spark 4.0, run the following scala script:\n\nval tableName = \"<REPLACE WITH THE TABLE NAME OR PATH>\"\nval query = \"\"\"\n  with jsonStrings as (\n    select\n      id,\n      format_string('{\"key\": %s}', id) as jsonString\n    from\n      range(0, 100)\n  )\n  select\n    id,\n    parse_json(jsonString) as v,\n    array(\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString)\n    ) as array_of_variants,\n    named_struct('v', parse_json(jsonString)) as struct_of_variants,\n    map(\n      cast(id as string),\n      parse_json(jsonString),\n      'nullKey',\n      null\n    ) as map_of_variants,\n    array(\n      named_struct('v', parse_json(jsonString)),\n      named_struct('v', null),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      ),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      )\n    ) as array_of_struct_of_variants,\n    named_struct(\n      'v',\n      array(\n        null,\n        parse_json(jsonString)\n      )\n    ) as struct_of_array_of_variants\n  from\n    jsonStrings\n\"\"\"\n\nval writeToTableSql = s\"\"\"\n  create or replace table $tableName\n  USING DELTA TBLPROPERTIES (delta.checkpointInterval = 2)\n\"\"\"\n\nspark.sql(s\"${writeToTableSql}\\n${query}\")\n// Write two additional rows to create a checkpoint.\n(0 until 2).foreach { v =>\n  spark\n    .sql(query)\n    .where(s\"id = $v\")\n    .write\n    .format(\"delta\")\n    .mode(\"append\")\n    .insertInto(tableName)\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-stable-feature-checkpoint/_delta_log/00000000000000000000.crc",
    "content": "{\"txnId\":\"7dddb463-9062-4c74-a5e6-2b0866c16b00\",\"tableSizeBytes\":333867,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"f1448d1b-cd82-48a2-ba5e-cffcb9fa9239\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1734924468826},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\"],\"writerFeatures\":[\"variantType\",\"appendOnly\",\"invariants\"]},\"histogramOpt\":{\"sortedBinBoundaries\":[0,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,12582912,16777216,20971520,25165824,29360128,33554432,37748736,41943040,50331648,58720256,67108864,75497472,83886080,92274688,100663296,109051904,117440512,125829120,130023424,134217728,138412032,142606336,146800640,150994944,167772160,184549376,201326592,218103808,234881024,251658240,268435456,285212672,301989888,318767104,335544320,352321536,369098752,385875968,402653184,419430400,436207616,452984832,469762048,486539264,503316480,520093696,536870912,553648128,570425344,587202560,603979776,671088640,738197504,805306368,872415232,939524096,1006632960,1073741824,1140850688,1207959552,1275068416,1342177280,1409286144,1476395008,1610612736,1744830464,1879048192,2013265920,2147483648,2415919104,2684354560,2952790016,3221225472,3489660928,3758096384,4026531840,4294967296,8589934592,17179869184,34359738368,68719476736,137438953472,274877906944],\"fileCounts\":[0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"totalBytes\":[0,0,0,0,0,333867,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},\"allFiles\":[{\"path\":\"test%25file%25prefix-part-00001-c7ee7ba3-625c-495b-95df-06f44ffb72c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166740,\"modificationTime\":1734924470884,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":5000},\\\"maxValues\\\":{\\\"id\\\":9999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924470884001\",\"MIN_INSERTION_TIME\":\"1734924470884001\",\"MAX_INSERTION_TIME\":\"1734924470884001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}},{\"path\":\"test%25file%25prefix-part-00000-5f6f82ed-28c5-4f4e-b358-93904826c84d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":167127,\"modificationTime\":1734924470884,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924470884000\",\"MIN_INSERTION_TIME\":\"1734924470884000\",\"MAX_INSERTION_TIME\":\"1734924470884000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}]}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-stable-feature-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1734924472549,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"statsOnLoad\":false,\"partitionBy\":\"[]\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10000\",\"numOutputBytes\":\"333867\"},\"tags\":{\"noRowsCopied\":\"true\",\"restoresDeletedRows\":\"false\"},\"engineInfo\":\"Databricks-Runtime/<unknown>\",\"txnId\":\"7dddb463-9062-4c74-a5e6-2b0866c16b00\"}}\n{\"metaData\":{\"id\":\"f1448d1b-cd82-48a2-ba5e-cffcb9fa9239\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1734924468826}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\"],\"writerFeatures\":[\"variantType\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00000-5f6f82ed-28c5-4f4e-b358-93904826c84d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":167127,\"modificationTime\":1734924470884,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924470884000\",\"MIN_INSERTION_TIME\":\"1734924470884000\",\"MAX_INSERTION_TIME\":\"1734924470884000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00001-c7ee7ba3-625c-495b-95df-06f44ffb72c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166740,\"modificationTime\":1734924470884,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":5000},\\\"maxValues\\\":{\\\"id\\\":9999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924470884001\",\"MIN_INSERTION_TIME\":\"1734924470884001\",\"MAX_INSERTION_TIME\":\"1734924470884001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-stable-feature-checkpoint/_delta_log/00000000000000000001.crc",
    "content": "{\"txnId\":\"4004e1eb-034f-411d-9d98-742f1553ade2\",\"tableSizeBytes\":667559,\"numFiles\":4,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"f1448d1b-cd82-48a2-ba5e-cffcb9fa9239\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"map_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"map\\\",\\\"keyType\\\":\\\"string\\\",\\\"valueType\\\":\\\"variant\\\",\\\"valueContainsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"array_of_struct_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"struct_of_array_of_variants\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":{\\\"type\\\":\\\"array\\\",\\\"elementType\\\":\\\"variant\\\",\\\"containsNull\\\":true},\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1734924468826},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\"],\"writerFeatures\":[\"variantType\",\"appendOnly\",\"invariants\"]},\"histogramOpt\":{\"sortedBinBoundaries\":[0,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,12582912,16777216,20971520,25165824,29360128,33554432,37748736,41943040,50331648,58720256,67108864,75497472,83886080,92274688,100663296,109051904,117440512,125829120,130023424,134217728,138412032,142606336,146800640,150994944,167772160,184549376,201326592,218103808,234881024,251658240,268435456,285212672,301989888,318767104,335544320,352321536,369098752,385875968,402653184,419430400,436207616,452984832,469762048,486539264,503316480,520093696,536870912,553648128,570425344,587202560,603979776,671088640,738197504,805306368,872415232,939524096,1006632960,1073741824,1140850688,1207959552,1275068416,1342177280,1409286144,1476395008,1610612736,1744830464,1879048192,2013265920,2147483648,2415919104,2684354560,2952790016,3221225472,3489660928,3758096384,4026531840,4294967296,8589934592,17179869184,34359738368,68719476736,137438953472,274877906944],\"fileCounts\":[0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"totalBytes\":[0,0,0,0,0,667559,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},\"allFiles\":[{\"path\":\"test%25file%25prefix-part-00001-95062e44-13fa-4917-b169-d289cd21c717-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166770,\"modificationTime\":1734924475576,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":15000},\\\"maxValues\\\":{\\\"id\\\":19999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924475576001\",\"MIN_INSERTION_TIME\":\"1734924475576001\",\"MAX_INSERTION_TIME\":\"1734924475576001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}},{\"path\":\"test%25file%25prefix-part-00000-5f6f82ed-28c5-4f4e-b358-93904826c84d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":167127,\"modificationTime\":1734924470884,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":4999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924470884000\",\"MIN_INSERTION_TIME\":\"1734924470884000\",\"MAX_INSERTION_TIME\":\"1734924470884000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}},{\"path\":\"test%25file%25prefix-part-00001-c7ee7ba3-625c-495b-95df-06f44ffb72c9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166740,\"modificationTime\":1734924470884,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":5000},\\\"maxValues\\\":{\\\"id\\\":9999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924470884001\",\"MIN_INSERTION_TIME\":\"1734924470884001\",\"MAX_INSERTION_TIME\":\"1734924470884001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}},{\"path\":\"test%25file%25prefix-part-00000-c98a0433-2bfc-4903-9b2e-0fb34243f552-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166922,\"modificationTime\":1734924475588,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":10000},\\\"maxValues\\\":{\\\"id\\\":14999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924475576000\",\"MIN_INSERTION_TIME\":\"1734924475576000\",\"MAX_INSERTION_TIME\":\"1734924475576000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}]}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-stable-feature-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1734924475736,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"statsOnLoad\":false,\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"10000\",\"numOutputBytes\":\"333692\"},\"tags\":{\"noRowsCopied\":\"true\",\"restoresDeletedRows\":\"false\"},\"engineInfo\":\"Databricks-Runtime/<unknown>\",\"txnId\":\"4004e1eb-034f-411d-9d98-742f1553ade2\"}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00000-c98a0433-2bfc-4903-9b2e-0fb34243f552-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166922,\"modificationTime\":1734924475588,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":10000},\\\"maxValues\\\":{\\\"id\\\":14999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924475576000\",\"MIN_INSERTION_TIME\":\"1734924475576000\",\"MAX_INSERTION_TIME\":\"1734924475576000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"test%25file%25prefix-part-00001-95062e44-13fa-4917-b169-d289cd21c717-c000.snappy.parquet\",\"partitionValues\":{},\"size\":166770,\"modificationTime\":1734924475576,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5000,\\\"minValues\\\":{\\\"id\\\":15000},\\\"maxValues\\\":{\\\"id\\\":19999},\\\"nullCount\\\":{\\\"id\\\":0,\\\"v\\\":0,\\\"array_of_variants\\\":0,\\\"struct_of_variants\\\":{\\\"v\\\":0},\\\"map_of_variants\\\":0,\\\"array_of_struct_of_variants\\\":0,\\\"struct_of_array_of_variants\\\":{\\\"v\\\":0}}}\",\"tags\":{\"INSERTION_TIME\":\"1734924475576001\",\"MIN_INSERTION_TIME\":\"1734924475576001\",\"MAX_INSERTION_TIME\":\"1734924475576001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/resources/spark-variant-stable-feature-checkpoint/_delta_log/info.txt",
    "content": "This file contains the code used to generate this golden table \"spark-variant-stable-feature-checkpoint\"\n\nUsing delta-spark 4.0, run the following scala script:\n\nval tableName = \"<REPLACE WITH THE TABLE NAME OR PATH>\"\nval query = \"\"\"\n  with jsonStrings as (\n    select\n      id,\n      format_string('{\"key\": %s}', id) as jsonString\n    from\n      range(0, 100)\n  )\n  select\n    id,\n    parse_json(jsonString) as v,\n    array(\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString),\n      null,\n      parse_json(jsonString)\n    ) as array_of_variants,\n    named_struct('v', parse_json(jsonString)) as struct_of_variants,\n    map(\n      cast(id as string),\n      parse_json(jsonString),\n      'nullKey',\n      null\n    ) as map_of_variants,\n    array(\n      named_struct('v', parse_json(jsonString)),\n      named_struct('v', null),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      ),\n      null,\n      named_struct(\n        'v',\n        parse_json(jsonString)\n      )\n    ) as array_of_struct_of_variants,\n    named_struct(\n      'v',\n      array(\n        null,\n        parse_json(jsonString)\n      )\n    ) as struct_of_array_of_variants\n  from\n    jsonStrings\n\"\"\"\n\nval writeToTableSql = s\"\"\"\n  create or replace table $tableName\n  USING DELTA TBLPROPERTIES (delta.checkpointInterval = 2)\n\"\"\"\n\nspark.sql(s\"${writeToTableSql}\\n${query}\")\n// Write two additional rows to create a checkpoint.\n(0 until 2).foreach { v =>\n  spark\n    .sql(query)\n    .where(s\"id = $v\")\n    .write\n    .format(\"delta\")\n    .mode(\"append\")\n    .insertInto(tableName)\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/CheckpointV2ReadSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, ExpressionTestUtils, TestRow, TestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.{InternalScanFileUtils, SnapshotImpl}\nimport io.delta.kernel.internal.checkpoints.CheckpointInstance\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.delta.{DeltaLog, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.FileNames\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.types.{BooleanType, IntegerType, LongType, MapType, StringType, StructType}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LegacyCheckpointV2ReadSuite extends AbstractCheckpointV2ReadSuite\n    with TestUtilsWithLegacyKernelAPIs {\n  override lazy val defaultEngine = defaultEngineBatchSize2\n}\n\nclass CheckpointV2ReadSuite extends AbstractCheckpointV2ReadSuite\n    with TestUtilsWithTableManagerAPIs {\n  override lazy val defaultEngine = defaultEngineBatchSize2\n}\n\ntrait AbstractCheckpointV2ReadSuite extends AnyFunSuite with ExpressionTestUtils {\n  self: AbstractTestUtils =>\n\n  private final val supportedFileFormats = Seq(\"json\", \"parquet\")\n\n  def createSourceTable(\n      tbl: String,\n      path: String,\n      partitionOrClusteringSpec: String): Unit = {\n    spark.sql(s\"CREATE TABLE $tbl (a INT, b STRING) USING delta \" +\n      s\"$partitionOrClusteringSpec BY (a) LOCATION '$path' \" +\n      s\"TBLPROPERTIES ('delta.checkpointInterval' = '2', 'delta.checkpointPolicy'='v2')\")\n    spark.sql(s\"INSERT INTO $tbl VALUES (1, 'a'), (2, 'b')\")\n    spark.sql(s\"INSERT INTO $tbl VALUES (3, 'c'), (4, 'd')\")\n    spark.sql(s\"INSERT INTO $tbl VALUES (5, 'e'), (6, 'f')\")\n  }\n\n  def validateSnapshot(\n      path: String,\n      snapshotFromSpark: Snapshot,\n      strictFileValidation: Boolean = true,\n      ckptVersionExpected: Option[Int] = None,\n      expectV2CheckpointFormat: Boolean = true): Unit = {\n    // Create a snapshot from Spark connector and from kernel.\n    val snapshotImpl = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, path)\n\n    // Validate metadata/protocol loaded correctly from top-level v2 checkpoint file.\n    val expectedMetadataId =\n      DeltaTable.forPath(path).detail().select(\"id\").collect().head.getString(0)\n    assert(snapshotImpl.getMetadata.getId == expectedMetadataId)\n    assert(snapshotImpl.getProtocol.getMinReaderVersion ==\n      snapshotFromSpark.protocol.minReaderVersion)\n    assert(snapshotImpl.getProtocol.getMinWriterVersion ==\n      snapshotFromSpark.protocol.minWriterVersion)\n    assert(snapshotImpl.getProtocol.getReaderFeatures.asScala.toSet ==\n      snapshotFromSpark.protocol.readerFeatureNames)\n    assert(snapshotImpl.getProtocol.getWriterFeatures.asScala.toSet ==\n      snapshotFromSpark.protocol.writerFeatureNames)\n    assert(snapshotImpl.getVersion() == snapshotFromSpark.version)\n\n    // Validate that snapshot read from most recent checkpoint. For most cases, given a checkpoint\n    // interval of 2, this will be the most recent even version.\n    val expectedV2CkptToRead =\n      ckptVersionExpected.getOrElse(snapshotFromSpark.version - (snapshotFromSpark.version % 2))\n    assert(snapshotImpl.getLogSegment.getCheckpoints.asScala.map(f =>\n      FileNames.checkpointVersion(new Path(f.getPath)))\n      .contains(expectedV2CkptToRead))\n    assert(snapshotImpl.getLogSegment.getCheckpoints.asScala.map(f =>\n      new CheckpointInstance(f.getPath).format == CheckpointInstance.CheckpointFormat.V2)\n      .contains(expectV2CheckpointFormat))\n\n    // Validate AddFiles from sidecars found against Spark connector.\n    val scan = snapshotImpl.getScanBuilder().build()\n    val foundFiles =\n      collectScanFileRows(scan).map(InternalScanFileUtils.getAddFileStatus).map(\n        _.getPath.split('/').last).toSet\n    val expectedFiles = snapshotFromSpark.allFiles.collect().map(_.toPath.toString).toSet\n    if (strictFileValidation) {\n      assert(foundFiles == expectedFiles)\n    } else {\n      assert(foundFiles.subsetOf(expectedFiles))\n    }\n  }\n\n  test(\"v2 checkpoint support\") {\n    supportedFileFormats.foreach { format =>\n      withTempDir { path =>\n        withTempTable { tbl =>\n          // Create table.\n          withSQLConf(\n            DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> format,\n            \"spark.databricks.delta.clusteredTable.enableClusteringTablePreview\" -> \"true\") {\n            createSourceTable(tbl, path.toString, \"CLUSTER\")\n\n            // Insert more data to ensure multiple ColumnarBatches created.\n            spark.createDataFrame(\n              spark.sparkContext.parallelize(10 to 110).map(i => Row(i, i.toString)),\n              new StructType().add(\"a\", IntegerType).add(\"b\", StringType))\n              .repartition(10)\n              .write.format(\"delta\").mode(\"append\").saveAsTable(tbl)\n          }\n\n          // Validate snapshot and data.\n          validateSnapshot(path.toString, DeltaLog.forTable(spark, path.toString).update())\n          checkTable(\n            path = path.toString,\n            expectedAnswer = spark.sql(s\"SELECT * FROM $tbl\").collect().map(TestRow(_)))\n\n          // Remove some files from the table, then add a new one.\n          spark.sql(s\"DELETE FROM $tbl WHERE a=1 OR a=2\")\n          spark.sql(s\"INSERT INTO $tbl VALUES (7, 'g'), (8, 'h')\")\n\n          // Validate snapshot and data.\n          validateSnapshot(path.toString, DeltaLog.forTable(spark, path.toString).update())\n          checkTable(\n            path = path.toString,\n            expectedAnswer = spark.sql(s\"SELECT * FROM $tbl\").collect().map(TestRow(_)))\n        }\n      }\n    }\n  }\n\n  test(\"v2 checkpoint support with multiple sidecars\") {\n    supportedFileFormats.foreach { format =>\n      withTempDir { path =>\n        withTempTable { tbl =>\n          // Create table.\n          withSQLConf(\n            DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> format,\n            DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"1\", // Ensure 1 action per checkpoint.\n            \"spark.databricks.delta.clusteredTable.enableClusteringTablePreview\" -> \"true\") {\n            createSourceTable(tbl, path.toString, \"CLUSTER\")\n          }\n\n          // Validate snapshot and data.\n          validateSnapshot(path.toString, DeltaLog.forTable(spark, path.toString).update())\n          checkTable(\n            path = path.toString,\n            expectedAnswer = (1 to 6).map(i => TestRow(i, (i - 1 + 'a').toChar.toString)))\n\n          // Remove some files from the table, then add a new one.\n          spark.sql(s\"DELETE FROM $tbl WHERE a=1 OR a=2\")\n          spark.sql(s\"INSERT INTO $tbl VALUES (7, 'g'), (8, 'h')\")\n\n          // Validate snapshot and data.\n          validateSnapshot(path.toString, DeltaLog.forTable(spark, path.toString).update())\n          checkTable(\n            path = path.toString,\n            expectedAnswer = (3 to 8).map(i => TestRow(i, (i - 1 + 'a').toChar.toString)))\n        }\n      }\n    }\n  }\n\n  test(\"UUID named checkpoint with actions\") {\n    withTempDir { path =>\n      // Create Delta log and a checkpoint file with actions in it.\n      val log = DeltaLog.forTable(spark, new Path(path.toString))\n      new File(log.logPath.toUri).mkdirs()\n\n      val metadata = Metadata(\n        \"testId\",\n        schemaString = \"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[\" +\n          \"{\\\"name\\\":\\\"a\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\")\n      val supportedFeatures = Set(\"v2Checkpoint\", \"appendOnly\", \"invariants\")\n      val protocol = Protocol(3, 7, Some(Set(\"v2Checkpoint\")), Some(supportedFeatures))\n      val add = AddFile(new Path(\"addfile\").toUri.toString, Map.empty, 100L, 10L, dataChange = true)\n\n      log.startTransaction().commitManuallyWithValidation(metadata, add)\n      log.upgradeProtocol(None, log.update(), protocol)\n      log.checkpoint(log.update())\n\n      // Spark snapshot and files must be evaluated before renaming the checkpoint file.\n      // This is because this checkpoint file (technically) becomes invalid, as there is no\n      // CheckpointManifest action in it. However, because the Spark connector will place all\n      // Add and Remove actions in the sidecar files, we must use this hack to test this\n      // scenario.\n      val snapshotFromSpark = DeltaLog.forTable(spark, path.toString).update()\n      snapshotFromSpark.allFiles.collect()\n\n      // Rename to UUID.\n      val ckptPath = new Path(new File(log.logPath.toUri).listFiles().filter(f =>\n        FileNames.isCheckpointFile(new Path(f.getPath))).head.toURI)\n      new File(ckptPath.toUri).renameTo(new File(new Path(\n        ckptPath.getParent,\n        ckptPath.getName\n          .replace(\"checkpoint.parquet\", \"checkpoint.abc-def.parquet\")).toUri))\n\n      // Validate snapshot.\n      validateSnapshot(path.toString, snapshotFromSpark, ckptVersionExpected = Some(1))\n    }\n  }\n\n  test(\"compatibility checkpoint with sidecar files\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        // Create checkpoint with sidecars.\n        withSQLConf(\n          DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> \"parquet\",\n          \"spark.databricks.delta.clusteredTable.enableClusteringTablePreview\" -> \"true\") {\n          spark.conf.set(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key, \"parquet\")\n          createSourceTable(tbl, path.toString, \"CLUSTER\")\n        }\n\n        // Spark snapshot and files must be evaluated before renaming the checkpoint file.\n        val snapshotFromSpark = DeltaLog.forTable(spark, path.toString).update()\n        snapshotFromSpark.allFiles.collect()\n\n        // Rename from UUID.\n        val ckptPath = new Path(\n          new File(DeltaLog.forTable(spark, path.toString).logPath.toUri).listFiles()\n            .filter(f => FileNames.isCheckpointFile(new Path(f.getPath))).head.toURI)\n        new File(ckptPath.toUri).renameTo(new File(\n          FileNames.checkpointFileSingular(ckptPath.getParent, 2).toUri))\n\n        // Validate snapshot and data.\n        validateSnapshot(path.toString, snapshotFromSpark, expectV2CheckpointFormat = false)\n        checkTable(\n          path = path.toString,\n          expectedAnswer = (1 to 6).map(i => TestRow(i, (i - 1 + 'a').toChar.toString)))\n      }\n    }\n  }\n\n  test(\"read from table with partition predicates\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        // Create source table with schema (a INT, b STRING) partitioned by a.\n        createSourceTable(tbl, path.toString, \"PARTITIONED\")\n\n        // Read from the source table with a partition predicate and validate the results.\n        val result = readSnapshot(\n          latestSnapshot(path.toString),\n          filter = greaterThan(col(\"a\"), Literal.ofInt(3)))\n        checkAnswer(result, Seq(TestRow(4, \"d\"), TestRow(5, \"e\"), TestRow(6, \"f\")))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/ChecksumLogReplayMetricsTestBase.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\n\n/** Suite to test engine metrics when loading Protocol and Metadata through checksum files. */\nclass PandMCheckSumLogReplayMetricsSuite extends ChecksumLogReplayMetricsTestBase\n    with TestUtilsWithTableManagerAPIs\n\n/** Suite to test engine metrics when loading Protocol and Metadata through checksum files. */\nclass LegacyPandMCheckSumLogReplayMetricsSuite extends ChecksumLogReplayMetricsTestBase\n    with TestUtilsWithLegacyKernelAPIs\n\n/**\n * Base trait for testing log replay optimizations when reading tables with checksum files.\n * This trait contains common test setup and test cases but allows specific metadata types\n * to customize how they load and verify data.\n *\n * Test subclasses implement specialized behavior for loading different items:\n * - PandMCheckSumLogReplayMetricsSuite - tests Protocol and Metadata loading\n * - DomainMetadataCheckSumReplayMetricsSuite - tests Domain Metadata loading\n */\ntrait ChecksumLogReplayMetricsTestBase extends LogReplayBaseSuite { self: AbstractTestUtils =>\n\n  /////////////////////////\n  // Test Helper Methods //\n  /////////////////////////\n\n  // Method to adjust list of versions of checkpoint file read.\n  // For example, if crc is missing and P&M is loaded from checkpoint.\n  // Domain metadata will load from checkpoint as well.\n  protected def getExpectedCheckpointReadSize(size: Seq[Long]): Seq[Long] = size\n\n  // When loading from a CRC at an earlier version, domain metadata and P&M use different\n  // replay strategies. P&M replay checks the CRC eagerly at (crcVersion + 1) and returns\n  // without opening files at or below the CRC version. Domain metadata replay instead scans\n  // all files in reverse and breaks when version < minLogVersion, which requires reading one\n  // batch from the file just below minLogVersion to discover its version.\n  protected def isDomainMetadataReplay: Boolean = false\n\n  ///////////\n  // Tests //\n  ///////////\n\n  Seq(-1L, 0L, 3L, 4L).foreach { readVersion => // -1 means latest version\n    test(\n      s\"checksum found at the read version: ${if (readVersion == -1) \"latest\" else readVersion}\") {\n      withTableWithCrc { (tablePath, engine) =>\n        loadPandMCheckMetrics(\n          tablePath,\n          engine,\n          // shouldn't need to read commit or checkpoint files as P&M/DM are found through checksum\n          expJsonVersionsRead = Nil,\n          expParquetVersionsRead = Nil,\n          expParquetReadSetSizes = Nil,\n          expChecksumReadSet = Seq(if (readVersion == -1) 11 else readVersion),\n          readVersion)\n      }\n    }\n  }\n\n  test(\n    \"checksum not found at read version and checkpoint exists at read version => use checkpoint\") {\n    withTableWithCrc { (tablePath, engine) =>\n      val checkpointVersion = 10\n      deleteChecksumFileForTable(tablePath, Seq(checkpointVersion))\n\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        // 10.crc missing, 10.checkpoint.parquet exists.\n        // Attempt to read 10.crc fails and read 10.checkpoint.parquet succeeds.\n        expJsonVersionsRead = Nil,\n        expParquetVersionsRead = Seq(10),\n        expParquetReadSetSizes = getExpectedCheckpointReadSize(Seq(1)),\n        expChecksumReadSet = Nil,\n        version = 10)\n    }\n  }\n\n  test(\n    \"checksum not found at read version but before and after version => use previous version\") {\n    withTableWithCrc { (tablePath, engine) =>\n      deleteChecksumFileForTable(tablePath, Seq(8))\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        expJsonVersionsRead =\n          if (isDomainMetadataReplay) Seq(8, 7) else Seq(8),\n        expParquetVersionsRead = Nil,\n        expParquetReadSetSizes = Nil,\n        expChecksumReadSet = Seq(7),\n        version = 8)\n    }\n  }\n\n  test(\n    \"checksum missing read version & the previous version, \" +\n      \"checkpoint exists the read version and the previous version => use checkpoint\") {\n    withTableWithCrc { (tablePath, engine) =>\n      val checkpointVersion = 10\n      deleteChecksumFileForTable(tablePath, Seq(checkpointVersion, checkpointVersion + 1))\n\n      // 11.crc, 10.crc missing, 10.checkpoint.parquet exists.\n      // Attempt to read 11.crc fails and read 10.checkpoint.parquet and 11.json succeeds.\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        expJsonVersionsRead = Seq(11),\n        expParquetVersionsRead = Seq(10),\n        expParquetReadSetSizes = getExpectedCheckpointReadSize(Seq(1)),\n        expChecksumReadSet = Nil,\n        version = 11)\n    }\n  }\n\n  test(\"crc found at read version and checkpoint at read version => use checksum\") {\n    withTableWithCrc { (tablePath, engine) =>\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        expJsonVersionsRead = Nil,\n        expParquetVersionsRead = Nil,\n        expParquetReadSetSizes = Nil,\n        expChecksumReadSet = Seq(10),\n        version = 10)\n    }\n  }\n\n  test(\"checksum not found at the read version, but found at a previous version\") {\n    withTableWithCrc { (tablePath, engine) =>\n      deleteChecksumFileForTable(tablePath, Seq(10, 11, 5, 6))\n\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        expJsonVersionsRead = Seq(11),\n        expParquetVersionsRead = Seq(10),\n        expParquetReadSetSizes = getExpectedCheckpointReadSize(Seq(1)),\n        expChecksumReadSet = Nil)\n\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        // We find the checksum from crc at version 4, but still read commit files 5 and 6\n        // to find the P&M which could have been updated in version 5 and 6.\n        expJsonVersionsRead =\n          if (isDomainMetadataReplay) Seq(6, 5, 4) else Seq(6, 5),\n        expParquetVersionsRead = Nil,\n        expParquetReadSetSizes = Nil,\n        expChecksumReadSet = Seq(4),\n        version = 6)\n\n      // now try to load version 3 and it should get P&M from checksum files only\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        // We find the checksum from crc at version 3, so shouldn't read anything else\n        expJsonVersionsRead = Nil,\n        expParquetVersionsRead = Nil,\n        expParquetReadSetSizes = Nil,\n        expChecksumReadSet = Seq(3),\n        version = 3)\n    }\n  }\n\n  test(\n    \"checksum missing read version, \" +\n      \"both checksum and checkpoint exist the read version the previous version => use checksum\") {\n    withTableWithCrc { (tablePath, engine) =>\n      val checkpointVersion = 10\n      val readVersion = checkpointVersion + 1\n      deleteChecksumFileForTable(tablePath, Seq(checkpointVersion + 1))\n\n      // 11.crc missing, 10.crc and 10.checkpoint.parquet exist.\n      // read 10.crc and 11.json.\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        expJsonVersionsRead = Seq(readVersion),\n        expParquetVersionsRead =\n          if (isDomainMetadataReplay) Seq(checkpointVersion.toLong) else Nil,\n        expParquetReadSetSizes =\n          if (isDomainMetadataReplay) Seq(1L) else Nil,\n        expChecksumReadSet = Seq(checkpointVersion),\n        version = readVersion)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/ChecksumSimpleComparisonSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.nio.file.Files\nimport java.util\nimport java.util.Optional\n\nimport scala.collection.immutable.Seq\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.{Operation, Table}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{TestUtils, WriteUtils}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.hook.PostCommitHook.PostCommitHookType\nimport io.delta.kernel.internal.DeltaLogActionUtils.DeltaAction\nimport io.delta.kernel.internal.TableImpl\nimport io.delta.kernel.internal.actions.{AddFile, Metadata, SingleAction}\nimport io.delta.kernel.internal.checksum.{ChecksumReader, CRCInfo}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames.checksumFile\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.types.LongType.LONG\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.apache.spark.sql.functions.col\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite to verify checksum file correctness by comparing\n * Delta Spark and Delta Kernel generated checksum files.\n * This suite ensures that both implementations generate consistent checksums\n * for various table operations.\n */\ntrait ChecksumComparisonSuiteBase extends AnyFunSuite with WriteUtils with TestUtils {\n\n  private val PARTITION_COLUMN = \"part\"\n\n  protected def getPostCommitHookType: PostCommitHookType\n\n  test(\"create table, insert data and verify checksum\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val sparkTablePath = tablePath + \"spark\"\n      val kernelTablePath = tablePath + \"kernel\"\n\n      getCreateTxn(\n        engine,\n        kernelTablePath,\n        schema = new StructType().add(\"id\", LONG),\n        partCols = Seq.empty).commit(engine, emptyIterable())\n        .getPostCommitHooks\n        .forEach(hook => hook.threadSafeInvoke(engine))\n      spark.sql(s\"CREATE OR REPLACE TABLE delta.`${sparkTablePath}` (id LONG) USING DELTA\")\n      assertChecksumEquals(engine, sparkTablePath, kernelTablePath, 0)\n\n      (1 to 10).foreach { version =>\n        spark.range(0, version).write.format(\"delta\").mode(\"append\").save(sparkTablePath)\n        commitSparkChangeToKernel(kernelTablePath, engine, sparkTablePath, version)\n        assertChecksumEquals(engine, sparkTablePath, kernelTablePath, version)\n      }\n    }\n  }\n\n  test(\"create partitioned table, insert and verify checksum\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val sparkTablePath = tablePath + \"spark\"\n      val kernelTablePath = tablePath + \"kernel\"\n\n      getCreateTxn(\n        engine,\n        kernelTablePath,\n        schema = new StructType().add(\"id\", LONG).add(PARTITION_COLUMN, LONG),\n        partCols = Seq(PARTITION_COLUMN)).commit(engine, emptyIterable())\n        .getPostCommitHooks\n        .forEach(hook => hook.threadSafeInvoke(engine))\n      spark.sql(\n        s\"CREATE OR REPLACE TABLE delta.`${sparkTablePath}` \" +\n          s\"(id LONG, part LONG) USING DELTA PARTITIONED BY (part)\")\n      assertChecksumEquals(engine, sparkTablePath, kernelTablePath, 0)\n\n      (1 to 10).foreach { version =>\n        spark.range(0, version).withColumn(PARTITION_COLUMN, col(\"id\") % 2)\n          .write.format(\"delta\").mode(\"append\").save(sparkTablePath)\n        commitSparkChangeToKernel(kernelTablePath, engine, sparkTablePath, version)\n        assertChecksumEquals(engine, sparkTablePath, kernelTablePath, version)\n      }\n    }\n  }\n\n  implicit class MetadataOpt(private val metadata: Metadata) {\n    def withDeterministicIdAndCreateTime: Metadata = {\n      new Metadata(\n        \"id\",\n        metadata.getName,\n        metadata.getDescription,\n        metadata.getFormat,\n        metadata.getSchemaString,\n        metadata.getSchema,\n        metadata.getPartitionColumns,\n        Optional.empty(),\n        metadata.getConfigurationMapValue)\n    }\n  }\n\n  implicit class CrcInfoOpt(private val crcInfo: CRCInfo) {\n    def withoutTransactionId: CRCInfo = {\n      new CRCInfo(\n        crcInfo.getVersion,\n        crcInfo.getMetadata.withDeterministicIdAndCreateTime,\n        crcInfo.getProtocol,\n        crcInfo.getTableSizeBytes,\n        crcInfo.getNumFiles,\n        Optional.empty(),\n        // TODO: check domain metadata.\n        Optional.empty(),\n        // TODO: check file size histogram once https://github.com/delta-io/delta/pull/3907 merged.\n        Optional.empty())\n    }\n  }\n\n  private def assertChecksumEquals(\n      engine: Engine,\n      sparkTablePath: String,\n      kernelTablePath: String,\n      version: Long): Unit = {\n    val sparkCrcPath = buildCrcPath(sparkTablePath, version)\n    val kernelCrcPath = buildCrcPath(kernelTablePath, version)\n\n    assert(\n      Files.exists(sparkCrcPath) && Files.exists(kernelCrcPath),\n      s\"CRC files not found for version $version\")\n\n    val sparkCrc = readCrcInfo(engine, sparkTablePath, version)\n    val kernelCrc = readCrcInfo(engine, kernelTablePath, version)\n    // Remove the randomly generated TxnId\n    assert(sparkCrc.withoutTransactionId === kernelCrc.withoutTransactionId)\n  }\n\n  private def readCrcInfo(engine: Engine, path: String, version: Long): CRCInfo = {\n    ChecksumReader\n      .tryReadChecksumFile(\n        engine,\n        FileStatus.of(checksumFile(new Path(f\"$path/_delta_log/\"), version).toString))\n      .orElseThrow(() => new IllegalStateException(s\"CRC info not found for version $version\"))\n  }\n\n  // Extracts the changes from spark table and commit the exactly same change to kernel table\n  protected def commitSparkChangeToKernel(\n      path: String,\n      engine: Engine,\n      sparkTablePath: String,\n      versionToConvert: Long): Unit = {\n\n    val txn = getUpdateTxn(engine, path, logCompactionInterval = 0) // disable compaction\n\n    val tableChange = Table.forPath(engine, sparkTablePath).asInstanceOf[TableImpl].getChanges(\n      engine,\n      versionToConvert,\n      versionToConvert,\n      // TODO include REMOVE action as well once we support it\n      Set(DeltaAction.ADD).asJava)\n\n    val addFilesRows = new util.ArrayList[Row]()\n    tableChange.forEach(batch =>\n      batch.getRows.forEach(row => {\n        val addIndex = row.getSchema.indexOf(\"add\")\n        if (!row.isNullAt(addIndex)) {\n          addFilesRows.add(\n            SingleAction.createAddFileSingleAction(new AddFile(row.getStruct(addIndex)).toRow))\n        }\n      }))\n\n    txn\n      .commit(engine, inMemoryIterable(toCloseableIterator(addFilesRows.iterator())))\n      .getPostCommitHooks\n      .stream().filter(_.getType == getPostCommitHookType)\n      .forEach(_.threadSafeInvoke(engine))\n  }\n}\n\nclass ChecksumSimpleComparisonSuite extends ChecksumComparisonSuiteBase {\n\n  override def getPostCommitHookType\n      : PostCommitHookType =\n    PostCommitHookType.CHECKSUM_SIMPLE\n}\n\nclass ChecksumFullComparisonSuite extends ChecksumComparisonSuiteBase {\n\n  override def getPostCommitHookType\n      : PostCommitHookType =\n    PostCommitHookType.CHECKSUM_FULL\n\n  override def commitSparkChangeToKernel(\n      kernelTablePath: String,\n      engine: Engine,\n      sparkTablePath: String,\n      versionToConvert: Long): Unit = {\n\n    // Delete previous version's checksum to force CHECKSUM_FULL for next commit\n    if (versionToConvert > 0) {\n      deleteChecksumFileForTable(kernelTablePath, Seq((versionToConvert - 1).toInt))\n    }\n\n    super.commitSparkChangeToKernel(kernelTablePath, engine, sparkTablePath, versionToConvert)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/ChecksumStatsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util.{Collections, Optional}\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.{Table, Transaction, TransactionCommitResult}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.WriteUtils\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.hook.PostCommitHook.PostCommitHookType\nimport io.delta.kernel.internal.{InternalScanFileUtils, SnapshotImpl, TableConfig, TableImpl}\nimport io.delta.kernel.internal.actions.{AddFile, GenerateIcebergCompatActionUtils, RemoveFile}\nimport io.delta.kernel.internal.checksum.ChecksumReader\nimport io.delta.kernel.internal.data.TransactionStateRow\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.stats.FileSizeHistogram\nimport io.delta.kernel.internal.util.FileNames.checksumFile\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.utils.{CloseableIterable, DataFileStatus, FileStatus}\nimport io.delta.kernel.utils.CloseableIterable.inMemoryIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Functional e2e test suite for verifying file stats collection in CRC are correct.\n */\ntrait ChecksumStatsSuiteBase extends AnyFunSuite with WriteUtils {\n\n  test(\"Check stats in checksum are correct\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Currently only table with IcebergWriterCompatV1 could easily\n      // support both add/remove files.\n      val tableProperties = Map(\n        TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\")\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = tableProperties)\n      val expectedFileSizeHistogram = FileSizeHistogram.createDefaultHistogram()\n\n      val dataFiles = Map(\"file1.parquet\" -> 100L, \"file2.parquet\" -> 100802L)\n      addFiles(engine, tablePath, dataFiles, expectedFileSizeHistogram)\n      checkCrcCorrect(\n        engine,\n        tablePath,\n        version = 1,\n        expectedFileCount = 2,\n        expectedTableSize = 100902,\n        expectedFileSizeHistogram = expectedFileSizeHistogram)\n\n      removeFiles(\n        engine,\n        tablePath,\n        Map(\"file1.parquet\" -> 100),\n        expectedFileSizeHistogram)\n      checkCrcCorrect(\n        engine,\n        tablePath,\n        version = 2,\n        expectedFileCount = 1,\n        expectedTableSize = 100902 - 100,\n        expectedFileSizeHistogram = expectedFileSizeHistogram)\n    }\n  }\n\n  /**\n   * Verifies that the CRC information at the given version matches expectations.\n   *\n   * @param engine The Delta Kernel engine\n   * @param tablePath Path to the Delta table\n   * @param version The table version to check\n   * @param expectedFileCount Expected number of files in the table\n   * @param expectedTableSize Expected total size of all files in bytes\n   * @param expectedFileSizeHistogram Expected file size histogram\n   */\n  def checkCrcCorrect(\n      engine: Engine,\n      tablePath: String,\n      version: Long,\n      expectedFileCount: Long,\n      expectedTableSize: Long,\n      expectedFileSizeHistogram: FileSizeHistogram): Unit = {\n    def verifyCrcExistsAndCorrect(): Unit = {\n      val crcInfo = ChecksumReader.tryReadChecksumFile(\n        engine,\n        FileStatus.of(checksumFile(\n          new Path(tablePath + \"/_delta_log\"),\n          version).toString))\n        .orElseThrow(() => new AssertionError(\"CRC information should be present\"))\n      assert(crcInfo.getNumFiles === expectedFileCount)\n      assert(crcInfo.getTableSizeBytes === expectedTableSize)\n      assert(crcInfo.getFileSizeHistogram === Optional.of(expectedFileSizeHistogram))\n    }\n    verifyCrcExistsAndCorrect()\n    // Delete existing CRC to regenerate a new one from state construction.\n    engine.getFileSystemClient.delete(buildCrcPath(tablePath, version).toString)\n    Table.forPath(engine, tablePath).checksum(engine, version)\n    verifyCrcExistsAndCorrect()\n  }\n\n  /**\n   * Adds files to the table and updates the expected histogram.\n   *\n   * @param engine The Delta Kernel engine\n   * @param tablePath Path to the Delta table\n   * @param filesToAdd Map of file paths to their sizes\n   * @param histogram The histogram to update with new file sizes\n   */\n  protected def addFiles(\n      engine: Engine,\n      tablePath: String,\n      filesToAdd: Map[String, Long],\n      histogram: FileSizeHistogram): Unit = {\n\n    val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n\n    val actionsToCommit = filesToAdd.map { case (path, size) =>\n      histogram.insert(size)\n      GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1AddAction(\n        txn.getTransactionState(engine),\n        generateDataFileStatus(tablePath, path, fileSize = size),\n        Collections.emptyMap(),\n        true, /* dataChange */\n        Optional.empty())\n    }.toSeq\n\n    commitTransaction(\n      txn,\n      engine,\n      inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n  }\n\n  /**\n   * Removes files from the table and updates the expected histogram.\n   *\n   * @param engine The Delta Kernel engine\n   * @param tablePath Path to the Delta table\n   * @param filesToRemove Map of file paths to their sizes\n   * @param histogram The histogram to update by removing file sizes\n   */\n  protected def removeFiles(\n      engine: Engine,\n      tablePath: String,\n      filesToRemove: Map[String, Long],\n      histogram: FileSizeHistogram): Unit = {\n\n    val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n\n    val actionsToCommit = filesToRemove.map { case (path, size) =>\n      histogram.remove(size)\n      GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1RemoveAction(\n        txn.getTransactionState(engine),\n        generateDataFileStatus(tablePath, path, fileSize = size),\n        Collections.emptyMap(),\n        true, /* dataChange */\n        Optional.of(TransactionStateRow.getPhysicalSchema(txn.getTransactionState(engine))))\n    }.toSeq\n\n    commitTransaction(\n      txn,\n      engine,\n      inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n  }\n\n  override def commitTransaction(\n      txn: Transaction,\n      engine: Engine,\n      dataActions: CloseableIterable[Row]): TransactionCommitResult = {\n    val result = txn.commit(engine, dataActions)\n\n    // Verify that we don't have both checksum hook types\n    val simpleHooks = result.getPostCommitHooks.stream()\n      .filter(hook => hook.getType == PostCommitHookType.CHECKSUM_SIMPLE)\n      .count()\n    val fullHooks = result.getPostCommitHooks.stream()\n      .filter(hook => hook.getType == PostCommitHookType.CHECKSUM_FULL)\n      .count()\n    assert(\n      simpleHooks == 0 || fullHooks == 0,\n      \"Both CHECKSUM_SIMPLE and CHECKSUM_FULL hooks should not be present\")\n\n    val checksumHook = result.getPostCommitHooks.stream().filter(hook =>\n      hook.getType == getPostCommitHookType).findFirst()\n    if (getPostCommitHookType == PostCommitHookType.CHECKSUM_SIMPLE) {\n      assert(checksumHook.isPresent, \"CHECKSUM_SIMPLE hook should be present\")\n      // When result.getVersion is 0, there will only no CHECKSUM_FULL.\n    } else if (result.getVersion > 0) {\n      assert(checksumHook.isPresent, \"CHECKSUM_FULL hook should be present for version > 0\")\n    }\n    checksumHook.ifPresent(_.threadSafeInvoke(engine))\n    result\n  }\n\n  protected def getPostCommitHookType: PostCommitHookType\n}\n\nclass ChecksumSimpleStatsSuite extends ChecksumStatsSuiteBase {\n  override def getPostCommitHookType: PostCommitHookType = PostCommitHookType.CHECKSUM_SIMPLE\n}\n\nclass ChecksumFullStatsSuite extends ChecksumStatsSuiteBase {\n  override def getPostCommitHookType: PostCommitHookType = PostCommitHookType.CHECKSUM_FULL\n\n  // Delete the checksum, so that the subsequent commit will generate CHECKSUM_FULL hook.\n  override def addFiles(\n      engine: Engine,\n      tablePath: String,\n      filesToAdd: Map[String, Long],\n      histogram: FileSizeHistogram): Unit = {\n    val previousVersion = Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n      .getLatestSnapshot(engine).getVersion\n    deleteChecksumFileForTable(tablePath.stripPrefix(\"file:\"), Seq(previousVersion.toInt))\n    super.addFiles(engine, tablePath, filesToAdd, histogram)\n  }\n\n  override def removeFiles(\n      engine: Engine,\n      tablePath: String,\n      filesToRemove: Map[String, Long],\n      histogram: FileSizeHistogram): Unit = {\n    val previousVersion =\n      Table.forPath(engine, tablePath).asInstanceOf[TableImpl].getLatestSnapshot(engine).getVersion\n    deleteChecksumFileForTable(tablePath.stripPrefix(\"file:\"), Seq(previousVersion.toInt))\n    super.removeFiles(engine, tablePath, filesToRemove, histogram)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/ChecksumUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\nimport scala.collection.immutable.Seq\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.WriteUtils\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.{SnapshotImpl, TableImpl}\nimport io.delta.kernel.internal.checksum.ChecksumUtils\nimport io.delta.kernel.internal.util.ManualClock\nimport io.delta.kernel.types.{StringType, StructType}\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.CommitInfo\n\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for io.delta.kernel.internal.checksum.ChecksumUtils\n */\nclass ChecksumUtilsSuite extends AnyFunSuite with WriteUtils with LogReplayBaseSuite {\n\n  private def initialTestTable(tablePath: String, engine: Engine): Unit = {\n    createEmptyTable(engine, tablePath, testSchema, clock = new ManualClock(0))\n    appendData(\n      engine,\n      tablePath,\n      isNewTable = false,\n      data = Seq(Map.empty[String, Literal] -> dataBatches1))\n  }\n\n  test(\"Create checksum for different version\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      initialTestTable(tablePath, engine)\n\n      val snapshot0 = Table.forPath(\n        engine,\n        tablePath).getSnapshotAsOfVersion(engine, 0).asInstanceOf[SnapshotImpl]\n      ChecksumUtils.computeStateAndWriteChecksum(engine, snapshot0.getLogSegment)\n      verifyChecksumForSnapshot(snapshot0)\n\n      val snapshot1 = Table.forPath(\n        engine,\n        tablePath).getSnapshotAsOfVersion(engine, 1).asInstanceOf[SnapshotImpl]\n      ChecksumUtils.computeStateAndWriteChecksum(engine, snapshot1.getLogSegment)\n      verifyChecksumForSnapshot(snapshot1)\n    }\n  }\n\n  test(\"Create checksum is idempotent\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      initialTestTable(tablePath, engine)\n\n      val snapshot = Table.forPath(\n        engine,\n        tablePath).getSnapshotAsOfVersion(engine, 0).asInstanceOf[SnapshotImpl]\n\n      // First call should create the checksum file\n      ChecksumUtils.computeStateAndWriteChecksum(engine, snapshot.getLogSegment)\n      verifyChecksumForSnapshot(snapshot)\n\n      // Second call should be a no-op (no exception thrown)\n      ChecksumUtils.computeStateAndWriteChecksum(engine, snapshot.getLogSegment)\n      verifyChecksumForSnapshot(snapshot)\n    }\n  }\n\n  test(\"test checksum -- no checksum, with checkpoint\") {\n    withTableWithCrc { (table, _, engine) =>\n      // Need to use HadoopFs to delete file to avoid fs throwing checksum mismatch on read.\n      deleteChecksumFileForTableUsingHadoopFs(table.getPath(engine).stripPrefix(\"file:\"), (0 to 11))\n      engine.resetMetrics()\n      table.checksum(engine, 11)\n      assertMetrics(\n        engine,\n        Seq(11),\n        Seq(10),\n        Seq(1),\n        expChecksumReadSet = Nil)\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 11))\n    }\n  }\n\n  test(\"test checksum -- stale checksum without file size histogram\" +\n    \", no checkpoint => fall back to full state construction\") {\n    withTableWithCrc { (table, _, engine) =>\n      deleteChecksumFileForTableUsingHadoopFs(table.getPath(engine), (5 to 8))\n      engine.resetMetrics()\n      table.checksum(engine, 8)\n      assertMetrics(\n        engine,\n        Seq(8, 7, 6, 5, 4, 3, 2, 1, 0),\n        Nil,\n        Nil,\n        expChecksumReadSet = Seq(4))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 8))\n    }\n  }\n\n  test(\"test checksum -- stale checksum, no checkpoint => incrementally load from checksum\") {\n    withTableWithCrc { (table, _, engine) =>\n      deleteChecksumFileForTableUsingHadoopFs(table.getPath(engine).stripPrefix(\"file:\"), (5 to 8))\n      // Spark generated CRC from Spark doesn't include file size histogram, regenerate it.\n      table.checksum(engine, 5)\n      engine.resetMetrics()\n      table.checksum(engine, 8)\n      assertMetrics(\n        engine,\n        Seq(8, 7, 6),\n        Nil,\n        Nil,\n        expChecksumReadSet = Seq(5))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 8))\n    }\n  }\n\n  test(\"test checksum -- stale checksum, checkpoint after checksum \" +\n    \"=> checkpoint with log replay\") {\n    withTableWithCrc { (table, _, engine) =>\n      deleteChecksumFileForTableUsingHadoopFs(table.getPath(engine), (5 to 11))\n      engine.resetMetrics()\n      table.checksum(engine, 11)\n      assertMetrics(\n        engine,\n        Seq(11),\n        Seq(10),\n        Seq(1),\n        expChecksumReadSet = Nil)\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 11))\n    }\n  }\n\n  test(\"test checksum -- not allowlisted operation => fallback with full state construction\") {\n    withTableWithCrc { (table, path, engine) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(path))\n      deltaLog\n        .startTransaction()\n        .commitManuallyWithValidation(\n          CommitInfo(\n            time = 12345,\n            operation = \"MANUAL UPDATE\",\n            inCommitTimestamp = Some(12345),\n            operationParameters = Map.empty,\n            commandContext = Map.empty,\n            readVersion = Some(11),\n            isolationLevel = None,\n            isBlindAppend = None,\n            operationMetrics = None,\n            userMetadata = None,\n            tags = None,\n            txnId = None),\n          deltaLog.getSnapshotAt(11).allFiles.head().copy(dataChange = false).wrap.unwrap)\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11, 12))\n      table.checksum(engine, 11)\n      engine.resetMetrics()\n      table.checksum(engine, 12)\n      assertMetrics(\n        engine,\n        Seq(12, 11),\n        Seq(10),\n        Seq(1),\n        // Tries to incrementally load CRC but fall back with unable to handle\n        // Add file without data change(compute stats).\n        expChecksumReadSet = Seq(11))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 12))\n    }\n  }\n\n  test(\"test checksum -- removeFile without Stats => fallback with full state construction\") {\n    withTableWithCrc { (table, path, engine) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(path))\n      deltaLog\n        .startTransaction()\n        .commitManuallyWithValidation(\n          CommitInfo(\n            time = 12345,\n            operation = \"REPLACE TABLE\",\n            inCommitTimestamp = Some(12345),\n            operationParameters = Map.empty,\n            commandContext = Map.empty,\n            readVersion = Some(11),\n            isolationLevel = None,\n            isBlindAppend = None,\n            operationMetrics = None,\n            userMetadata = None,\n            tags = None,\n            txnId = None),\n          deltaLog.getSnapshotAt(11).allFiles.head().remove.copy(size = None).wrap.unwrap)\n      // Spark generated CRC from Spark doesn't include file size histogram\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11, 12))\n      table.checksum(engine, 11)\n      engine.resetMetrics()\n      table.checksum(engine, 12)\n      assertMetrics(\n        engine,\n        Seq(12, 11),\n        Seq(10),\n        Seq(1),\n        // Tries to incrementally load CRC but fall back with unable to handle\n        // Remove file without stats.\n        expChecksumReadSet = Seq(11))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 12))\n    }\n  }\n\n  test(\"test checksum -- Optimize => incrementally build from CRC\") {\n    withTableWithCrc { (table, path, engine) =>\n      spark.sql(s\"OPTIMIZE delta.`$path`\")\n      // Spark generated CRC from Spark doesn't include file size histogram\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11, 12))\n      table.checksum(engine, 11)\n      engine.resetMetrics()\n      table.checksum(engine, 12)\n      assertMetrics(\n        engine,\n        Seq(12),\n        Nil,\n        Nil,\n        expChecksumReadSet = Seq(11))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 12))\n    }\n  }\n\n  test(\"test checksum -- replace table updating p&m, domain metadata is picked up\") {\n    withTableWithCrc { (table, path, engine) =>\n      // Spark generated CRC from Spark doesn't include file size histogram\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11))\n      table.checksum(engine, 11)\n      getReplaceTxn(\n        engine,\n        path,\n        new StructType().add(\n          \"a\",\n          StringType.STRING),\n        clusteringColsOpt = Some(Seq(new Column(\"a\"))),\n        withDomainMetadataSupported = true).commit(engine, emptyIterable[Row])\n      engine.resetMetrics()\n      table.checksum(engine, 12)\n      assertMetrics(\n        engine,\n        Seq(12),\n        Nil,\n        Nil,\n        expChecksumReadSet = Seq(11))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 12))\n    }\n  }\n\n  test(\"test checksum -- commit info not in the first action => \" +\n    \"fallback with full state construction\") {\n    withTableWithCrc { (table, path, engine) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(path))\n      deltaLog\n        .startTransaction()\n        .commitManuallyWithValidation(\n          deltaLog.getSnapshotAt(11).allFiles.head().remove.wrap.unwrap,\n          CommitInfo(\n            time = 12345,\n            operation = \"REPLACE TABLE\",\n            inCommitTimestamp = Some(12345),\n            operationParameters = Map.empty,\n            commandContext = Map.empty,\n            readVersion = Some(11),\n            isolationLevel = None,\n            isBlindAppend = None,\n            operationMetrics = None,\n            userMetadata = None,\n            tags = None,\n            txnId = None).wrap.unwrap)\n      // Spark generated CRC from Spark doesn't include file size histogram\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11, 12))\n      table.checksum(engine, 11)\n      engine.resetMetrics()\n      table.checksum(engine, 12)\n      assertMetrics(\n        engine,\n        Seq(12, 11),\n        Seq(10),\n        Seq(1),\n        expChecksumReadSet = Seq(11))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 12))\n    }\n  }\n\n  test(\"test checksum -- commit info missing => fallback with full state construction\") {\n    withTableWithCrc { (table, path, engine) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(path))\n      deltaLog\n        .startTransaction()\n        .commitManuallyWithValidation(\n          deltaLog.getSnapshotAt(11).allFiles.head().remove.wrap.unwrap)\n      // Spark generated CRC from Spark doesn't include file size histogram\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11, 12))\n      table.checksum(engine, 11)\n      engine.resetMetrics()\n      table.checksum(engine, 12)\n      assertMetrics(\n        engine,\n        Seq(12, 11),\n        Seq(10),\n        Seq(1),\n        expChecksumReadSet = Seq(11))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 12))\n    }\n  }\n\n  test(\"test checksum -- crc missing domain metadata => fallback with full state construction\") {\n    withTableWithCrc { (table, path, engine) =>\n      // Spark generated CRC from Spark doesn't include file size histogram\n      deleteChecksumFileForTableUsingHadoopFs(\n        table.getPath(engine).stripPrefix(\"file:\"),\n        Seq(11))\n      rewriteChecksumFileToExcludeDomainMetadata(engine, path, 10)\n      engine.resetMetrics()\n      table.checksum(engine, 11)\n      assertMetrics(\n        engine,\n        Seq(11),\n        Seq(10),\n        Seq(1),\n        expChecksumReadSet = Seq(10))\n      verifyChecksumForSnapshot(table.getSnapshotAsOfVersion(engine, 11))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/ColumnDefaultsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.TransactionCommitResult\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.WriteUtils\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.util.Clock\nimport io.delta.kernel.types._\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ColumnDefaultsSuite extends AnyFunSuite with WriteUtils {\n  private val schemasWithDefaults = Seq(\n    (\n      \"plain\",\n      new StructType()\n        .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n        .add(\"b\", IntegerType.INTEGER, true, fieldMeta(2, \"127\"))),\n    (\n      \"nested\",\n      new StructType()\n        .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n        .add(\n          \"nested\",\n          new StructType()\n            .add(\"nested_a\", IntegerType.INTEGER, true, fieldMeta(2, \"100\")),\n          fieldMeta(3, null))))\n\n  private def fieldMeta(fieldId: Int, defaultVal: String) = {\n    var builder = FieldMetadata.builder()\n    if (fieldId != -1) {\n      builder = builder\n        .putLong(\"delta.columnMapping.id\", fieldId)\n        .putString(\"delta.columnMapping.physicalName\", s\"col-$fieldId\")\n    }\n    if (defaultVal != null) {\n      builder = builder.putString(\"CURRENT_DEFAULT\", defaultVal)\n    }\n    builder.build()\n  }\n\n  val goodTblProperties = Map(\n    \"delta.feature.allowColumnDefaults\" -> \"supported\",\n    \"delta.enableIcebergCompatV3\" -> \"true\",\n    \"delta.columnMapping.mode\" -> \"id\")\n\n  schemasWithDefaults.foreach { case (schemaName, schemaWithDefault) =>\n    test(s\"allow default value in schema when the table feature is enabled - $schemaName\") {\n      val goodTblProperties2 = Map(\n        \"delta.feature.allowColumnDefaults\" -> \"supported\",\n        \"delta.enableIcebergCompatV3\" -> \"true\")\n      for (tableProperties <- Seq(goodTblProperties, goodTblProperties2)) {\n        // Create table\n        withTempDirAndEngine { (tablePath, engine) =>\n          createEmptyTable(\n            engine,\n            tablePath,\n            schemaWithDefault,\n            tableProperties = tableProperties)\n        }\n      }\n    }\n\n    test(s\"block default value in schema when table features are not properly set - $schemaName\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val e = intercept[KernelException] {\n          createEmptyTable(\n            engine,\n            tablePath,\n            schemaWithDefault,\n            tableProperties = Map(\n              \"delta.feature.allowColumnDefaults\" -> \"supported\"))\n        }\n        assert(e.getMessage ==\n          \"In Delta Kernel, default values table feature requires IcebergCompatV3 to be enabled.\")\n      }\n      withTempDirAndEngine { (tablePath, engine) =>\n        val e = intercept[KernelException] {\n          createEmptyTable(engine, tablePath, schemaWithDefault)\n        }\n        assert(e.getMessage ==\n          \"Found column defaults in the schema but the table does not support the \" +\n          \"columnDefaults table feature.\")\n      }\n    }\n\n    test(s\"block writing to tables with default values - $schemaName\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        createEmptyTable(\n          engine,\n          tablePath,\n          schemaWithDefault,\n          tableProperties = goodTblProperties)\n        val e = intercept[UnsupportedOperationException] {\n          appendData(engine, tablePath, data = Seq(Map.empty[String, Literal] -> dataBatches1))\n        }\n        assert(e.getMessage == \"Writing into column mapping enabled table is not supported yet.\")\n      }\n    }\n\n    Seq(\n      (StringType.STRING, \"CURRENT_TIMESTAMP()\"),\n      (IntegerType.INTEGER, \"313.55\"),\n      (DoubleType.DOUBLE, \"Good boy\"),\n      (new DecimalType(10, 5), \"234243243243243234.234\"),\n      (DateType.DATE, \"2022/01/05\"),\n      (TimestampType.TIMESTAMP, \"2025-01-01\"),\n      (TimestampNTZType.TIMESTAMP_NTZ, \"2025-01-01T00:00:00+02:00\")).foreach {\n      case (dataType, value) =>\n        test(s\"block invalid default values - ($schemaName, $value)\") {\n          // Create tables\n          val schema = new StructType().add(\"col1\", dataType, true, fieldMeta(100, value))\n          withTempDirAndEngine { (tablePath, engine) =>\n            intercept[KernelException] {\n              createEmptyTable(\n                engine,\n                tablePath,\n                schema,\n                tableProperties = goodTblProperties)\n            }\n          }\n          // Schema Evolutions -> change type\n          withTempDirAndEngine { (tablePath, engine) =>\n            createEmptyTable(\n              engine,\n              tablePath,\n              schemaWithDefault,\n              tableProperties = goodTblProperties)\n            intercept[KernelException] {\n              val schema = new StructType()\n                .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n                .add(\"b\", IntegerType.INTEGER, true, fieldMeta(2, null))\n                .add(\"col1\", dataType, true, fieldMeta(100, value))\n              updateTableMetadata(engine, tablePath, schema)\n            }\n            intercept[KernelException] {\n              val schema = new StructType().add(\"col1\", dataType, true, fieldMeta(3, value))\n              updateTableMetadata(engine, tablePath, schema)\n            }\n          }\n        }\n    }\n  }\n\n  Seq(\n    (\"remove col\", new StructType().add(\"a\", StringType.STRING, true, fieldMeta(1, null))),\n    (\n      \"add col\",\n      new StructType()\n        .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n        .add(\"add1\", StringType.STRING, true, fieldMeta(7, \"'Tom'\"))\n        .add(\"add2\", DateType.DATE, true, fieldMeta(4, \"2025-01-01\"))\n        .add(\"add3\", DoubleType.DOUBLE, true, fieldMeta(5, \"'3.21'\"))\n        .add(\"b\", IntegerType.INTEGER, true, fieldMeta(6, \"127\"))),\n    (\n      \"update value\",\n      new StructType()\n        .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n        .add(\"b\", IntegerType.INTEGER, true, fieldMeta(2, \"350\"))),\n    (\n      \"rename column\",\n      new StructType()\n        .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n        .add(\"xxx\", IntegerType.INTEGER, true, fieldMeta(2, \"350\"))),\n    (\n      \"add renamed column\",\n      new StructType()\n        .add(\"a\", StringType.STRING, true, fieldMeta(1, null))\n        .add(\"b\", LongType.LONG, true, fieldMeta(220, \"350\")))).foreach { case (name, schema) =>\n    test(s\"allow valid default values - for new table & $name schema evolve\") {\n      // Create tables\n      // TODO: reconsider removing this part, not sure what exactly the point of this test is...\n      //  (schema evolve?)\n      withTempDirAndEngine { (tablePath, engine) =>\n        createEmptyTable(\n          engine,\n          tablePath,\n          schema,\n          tableProperties = goodTblProperties)\n      }\n      // Schema Evolutions\n      withTempDirAndEngine { (tablePath, engine) =>\n        createEmptyTable(\n          engine,\n          tablePath,\n          // Test this with just the plain unnested schema\n          // TODO: in the future reconsider the point of running all the above tests with both these\n          //  schemas\n          schemasWithDefaults(0)._2,\n          tableProperties = goodTblProperties)\n        updateTableMetadata(engine, tablePath, schema)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/CommitIcebergActionSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util.{Collections, Optional}\nimport java.util.Collections.emptyMap\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Table, Transaction}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtilsWithV1Builders, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.{TableConfig, TableImpl}\nimport io.delta.kernel.internal.DeltaLogActionUtils.DeltaAction\nimport io.delta.kernel.internal.actions.{AddFile, DeletionVectorDescriptor, GenerateIcebergCompatActionUtils, RemoveFile}\nimport io.delta.kernel.internal.data.TransactionStateRow\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.statistics.DataFileStatistics\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\nimport io.delta.kernel.utils.DataFileStatus\n\nimport org.apache.spark.sql.delta.DeltaLog\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CommitIcebergActionTransactionBuilderV1Suite extends AbstractCommitIcebergActionSuite\n    with WriteUtilsWithV1Builders {}\n\nclass CommitIcebergActionTransactionBuilderV2Suite extends AbstractCommitIcebergActionSuite\n    with WriteUtilsWithV2Builders {}\n\ntrait AbstractCommitIcebergActionSuite extends AnyFunSuite { self: AbstractWriteUtils =>\n\n  private val tblPropertiesIcebergWriterCompatV1Enabled = Map(\n    TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\")\n\n  private val tblPropertiesIcebergWriterCompatV3Enabled = Map(\n    TableConfig.ICEBERG_WRITER_COMPAT_V3_ENABLED.getKey -> \"true\")\n\n  /** Helper to create a transaction, generate a single action, and commit it. */\n  private def commitSingleAction(\n      engine: Engine,\n      tablePath: String,\n      actionFn: Transaction => Row): Unit = {\n    val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n    val action = actionFn(txn)\n    commitTransaction(\n      txn,\n      engine,\n      inMemoryIterable(toCloseableIterator(Seq(action).asJava.iterator())))\n  }\n\n  private def createIcebergCompatAction(\n      actionType: String, // \"ADD\" or \"REMOVE\"\n      version: String,\n      txn: Transaction,\n      engine: Engine,\n      fileStatus: DataFileStatus,\n      dataChange: Boolean,\n      tags: Map[String, String] = Map.empty, // ignored for REMOVE\n      deletionVector: Optional[DeletionVectorDescriptor] = Optional.empty() // ignored for V1\n  ): Row = {\n\n    (actionType, version) match {\n      case (\"ADD\", \"V1\") =>\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1AddAction(\n          txn.getTransactionState(engine),\n          fileStatus,\n          Collections.emptyMap(),\n          dataChange,\n          tags.asJava,\n          Optional.of(TransactionStateRow.getPhysicalSchema(txn.getTransactionState(engine))))\n      case (\"ADD\", \"V3\") =>\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV3AddAction(\n          txn.getTransactionState(engine),\n          fileStatus,\n          Collections.emptyMap(),\n          dataChange,\n          tags.asJava,\n          Optional.empty[java.lang.Long](),\n          Optional.empty[java.lang.Long](),\n          deletionVector,\n          Optional.of(TransactionStateRow.getPhysicalSchema(txn.getTransactionState(engine))))\n      case (\"REMOVE\", \"V1\") =>\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV1RemoveAction(\n          txn.getTransactionState(engine),\n          fileStatus,\n          Collections.emptyMap(),\n          dataChange,\n          Optional.of(TransactionStateRow.getPhysicalSchema(txn.getTransactionState(engine))))\n      case (\"REMOVE\", \"V3\") =>\n        GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV3RemoveAction(\n          txn.getTransactionState(engine),\n          fileStatus,\n          Collections.emptyMap(),\n          dataChange,\n          Optional.empty[java.lang.Long](),\n          Optional.empty[java.lang.Long](),\n          deletionVector,\n          Optional.of(TransactionStateRow.getPhysicalSchema(txn.getTransactionState(engine))))\n      case _ => throw new IllegalArgumentException(\n          s\"Unsupported actionType: $actionType or version: $version\")\n    }\n  }\n\n  /* ----- Error cases ----- */\n\n  Seq(\"V1\", \"V3\").foreach { version =>\n    test(s\"$version: requires that maxRetries = 0\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        val txn = getCreateTxn(engine, tablePath, testSchema, tableProperties = properties)\n        intercept[UnsupportedOperationException] {\n          createIcebergCompatAction(\n            \"ADD\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            dataChange = true)\n        }\n        intercept[UnsupportedOperationException] {\n          createIcebergCompatAction(\n            \"REMOVE\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            dataChange = true)\n        }\n      }\n    }\n\n    test(s\"$version: requires that icebergWriterCompat${version} enabled\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val txn = getCreateTxn(engine, tablePath, testSchema, maxRetries = 0)\n        intercept[UnsupportedOperationException] {\n          createIcebergCompatAction(\n            \"ADD\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            dataChange = true)\n        }\n        intercept[UnsupportedOperationException] {\n          createIcebergCompatAction(\n            \"REMOVE\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            dataChange = true)\n        }\n      }\n    }\n\n    test(s\"$version: partitioned tables unsupported\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        val txn = getCreateTxn(\n          engine,\n          tablePath,\n          testPartitionSchema,\n          testPartitionColumns,\n          properties,\n          maxRetries = 0)\n        intercept[UnsupportedOperationException] {\n          createIcebergCompatAction(\n            \"ADD\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            dataChange = true)\n        }\n        intercept[UnsupportedOperationException] {\n          createIcebergCompatAction(\n            \"REMOVE\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            dataChange = true)\n        }\n      }\n    }\n\n    test(s\"$version: cannot create add without stats present\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        val txn =\n          getCreateTxn(engine, tablePath, testSchema, tableProperties = properties, maxRetries = 0)\n        intercept[KernelException] {\n          createIcebergCompatAction(\n            \"ADD\",\n            version,\n            txn,\n            engine,\n            generateDataFileStatus(tablePath, \"file1.parquet\", includeStats = false),\n            dataChange = true)\n        }\n      }\n    }\n  }\n\n  /* ----- E2E commit tests + read back with Spark ----- */\n  // Note - since we don't fully support column mapping writes (i.e. transformLogicalData doesn't\n  // support id mode) we cannot really test these APIs with actual tables with data since\n  // we cannot write column-mapping-id-based data\n  // For now - we write the actions and check that they are correct in Spark\n  // After we have full column mapping support we will add E2E tests with data that can be read\n\n  trait ExpectedFileAction\n\n  case class ExpectedAdd(path: String, size: Long, modificationTime: Long, dataChange: Boolean)\n      extends ExpectedFileAction\n\n  case class ExpectedRemove(path: String, size: Long, deletionTimestamp: Long, dataChange: Boolean)\n      extends ExpectedFileAction\n\n  private def checkActionsWrittenInJson(\n      engine: Engine,\n      tablePath: String,\n      version: Long,\n      expectedFileActions: Set[ExpectedFileAction],\n      icebergCompatWriterVersion: String = \"V1\"): Unit = {\n    val rows = Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n      .getChanges(engine, version, version, Set(DeltaAction.ADD, DeltaAction.REMOVE).asJava)\n      .toSeq\n      .flatMap(_.getRows.toSeq)\n    val fileActions = rows.flatMap { row =>\n      if (!row.isNullAt(row.getSchema.indexOf(\"add\"))) {\n        val addFile = new AddFile(row.getStruct(row.getSchema.indexOf(\"add\")))\n        assert(addFile.getPartitionValues.getSize == 0)\n        assert(!addFile.getTags.isPresent)\n\n        if (icebergCompatWriterVersion == \"V1\") {\n          assert(!addFile.getBaseRowId.isPresent)\n          assert(!addFile.getDefaultRowCommitVersion.isPresent)\n        } else { // V3\n          // In V3, baseRowId and defaultRowCommitVersion are required for row lineage\n          assert(addFile.getBaseRowId.isPresent)\n          assert(addFile.getDefaultRowCommitVersion.isPresent)\n        }\n\n        assert(!addFile.getDeletionVector.isPresent)\n        assert(addFile.getStats(null).isPresent)\n        Some(ExpectedAdd(\n          addFile.getPath,\n          addFile.getSize,\n          addFile.getModificationTime,\n          addFile.getDataChange))\n      } else if (!row.isNullAt(row.getSchema.indexOf(\"remove\"))) {\n        val removeFile = new RemoveFile(row.getStruct(row.getSchema.indexOf(\"remove\")))\n        assert(removeFile.getDeletionTimestamp.isPresent)\n        assert(removeFile.getExtendedFileMetadata.toScala.contains(true))\n        assert(removeFile.getPartitionValues.toScala.exists(_.getSize == 0))\n        assert(removeFile.getSize.isPresent)\n        assert(removeFile.getStats(null).isPresent)\n        assert(!removeFile.getTags.isPresent)\n        assert(!removeFile.getDeletionVector.isPresent)\n\n        assert(!removeFile.getBaseRowId.isPresent)\n        assert(!removeFile.getDefaultRowCommitVersion.isPresent)\n\n        Some(ExpectedRemove(\n          removeFile.getPath,\n          removeFile.getSize.get,\n          removeFile.getDeletionTimestamp.get,\n          removeFile.getDataChange))\n      } else {\n        None\n      }\n    }\n    assert(fileActions.size == expectedFileActions.size)\n    assert(fileActions.toSet == expectedFileActions)\n  }\n\n  private def checkSparkLogReplay(\n      tablePath: String,\n      version: Long,\n      expectedAdds: Set[ExpectedAdd],\n      icebergCompatWriterVersion: String = \"V1\"): Unit = {\n    val snapshot = DeltaLog.forTable(spark, tablePath).getSnapshotAt(version)\n    assert(snapshot.allFiles.count() == expectedAdds.size)\n    val addsFoundSet = snapshot.allFiles.collect().map { add =>\n      assert(add.partitionValues.isEmpty)\n      assert(!add.dataChange)\n\n      // Row lineage fields - different behavior for V1 vs V3\n      if (icebergCompatWriterVersion == \"V1\") {\n        assert(add.baseRowId.isEmpty)\n        assert(add.defaultRowCommitVersion.isEmpty)\n      } else { // V3\n        assert(add.baseRowId.nonEmpty)\n        assert(add.defaultRowCommitVersion.nonEmpty)\n      }\n\n      assert(add.deletionVector == null)\n      assert(add.stats != null)\n      assert(add.clusteringProvider.isEmpty)\n      ExpectedAdd(\n        add.path,\n        add.size,\n        add.modificationTime,\n        // Always false because Delta Spark copies add with dataChange=false during log replay\n        add.dataChange)\n    }.toSet\n    // We must \"hack\" all the expectedAdds to have dataChange=false since Delta Spark does this\n    // in log replay\n    assert(addsFoundSet == expectedAdds.map(_.copy(dataChange = false)))\n  }\n\n  Seq(\"V1\", \"V3\").foreach { version =>\n    test(s\"$version: Correctly commits adds to table and compat with Spark\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        // Create table\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = properties)\n\n        // Append 1 add with dataChange = true\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = true))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Append 1 add with dataChange = false (in theory this could involve updating stats but\n        // once we support remove add a case that looks like optimize/compaction)\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = false))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Verify we wrote the adds we expected into the JSON files using Kernel's getChanges\n        checkActionsWrittenInJson(engine, tablePath, 0, Set(), version)\n        checkActionsWrittenInJson(\n          engine,\n          tablePath,\n          1,\n          Set(ExpectedAdd(\"file1.parquet\", 1000, 10, true)),\n          version)\n        checkActionsWrittenInJson(\n          engine,\n          tablePath,\n          2,\n          Set(ExpectedAdd(\"file1.parquet\", 1000, 10, false)),\n          version)\n\n        // Verify that Spark can read the actions written via log replay\n        checkSparkLogReplay(tablePath, 0, Set(), version)\n        checkSparkLogReplay(\n          tablePath,\n          1,\n          Set(ExpectedAdd(\"file1.parquet\", 1000, 10, true)),\n          version)\n        // We added the same path twice so only the second remains after log replay\n        checkSparkLogReplay(\n          tablePath,\n          2,\n          Set(ExpectedAdd(\"file1.parquet\", 1000, 10, false)),\n          version)\n      }\n    }\n\n    test(s\"$version: Correctly commits adds and removes to table and compat with Spark\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        // Create table\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = properties)\n\n        // Append 1 add with dataChange = true\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = true))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Re-arrange data by removing that Add and adding a new Add\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"REMOVE\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = false),\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file2.parquet\"),\n              dataChange = false))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Remove that add so that the table is empty\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"REMOVE\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file2.parquet\"),\n              dataChange = true))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Verify we wrote the adds we expected into the JSON files using Kernel's getChanges\n        checkActionsWrittenInJson(engine, tablePath, 0, Set(), version)\n        checkActionsWrittenInJson(\n          engine,\n          tablePath,\n          1,\n          Set(ExpectedAdd(\"file1.parquet\", 1000, 10, true)),\n          version)\n        checkActionsWrittenInJson(\n          engine,\n          tablePath,\n          2,\n          Set(\n            ExpectedAdd(\"file2.parquet\", 1000, 10, false),\n            ExpectedRemove(\"file1.parquet\", 1000, 10, false)),\n          version)\n        checkActionsWrittenInJson(\n          engine,\n          tablePath,\n          3,\n          Set(ExpectedRemove(\"file2.parquet\", 1000, 10, true)),\n          version)\n\n        // Verify that Spark can read the actions written via log replay\n        checkSparkLogReplay(tablePath, 0, Set(), version)\n        checkSparkLogReplay(\n          tablePath,\n          1,\n          Set(ExpectedAdd(\"file1.parquet\", 1000, 10, true)),\n          version)\n        checkSparkLogReplay(\n          tablePath,\n          2,\n          Set(ExpectedAdd(\"file2.parquet\", 1000, 10, false)),\n          version)\n        checkSparkLogReplay(tablePath, 3, Set(), version)\n      }\n    }\n\n    test(s\"$version: append-only configuration is observed when committing removes\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        // Create table\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = properties ++ Map(\n            TableConfig.APPEND_ONLY_ENABLED.getKey -> \"true\"))\n\n        // Append 1 add with dataChange = true\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = true))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Re-arrange data by removing that Add and adding a new Add\n        // (can commit remove with dataChange=false)\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"REMOVE\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = false),\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file2.parquet\"),\n              dataChange = true))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Cannot create remove with dataChange=true\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          intercept[KernelException] {\n            createIcebergCompatAction(\n              \"REMOVE\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = true)\n          }\n        }\n      }\n    }\n\n    test(s\"$version: Tags can be successfully passed for generating addFile\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val properties = if (version == \"V1\") tblPropertiesIcebergWriterCompatV1Enabled\n        else tblPropertiesIcebergWriterCompatV3Enabled\n\n        // Create table\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = properties)\n\n        // Commit one add file with tags\n        val tags = Map(\"tag1\" -> \"abc\", \"tag2\" -> \"def\")\n\n        {\n          val txn = getUpdateTxn(engine, tablePath, maxRetries = 0)\n          val actionsToCommit = Seq(\n            createIcebergCompatAction(\n              \"ADD\",\n              version,\n              txn,\n              engine,\n              generateDataFileStatus(tablePath, \"file1.parquet\"),\n              dataChange = true,\n              tags = tags))\n          commitTransaction(\n            txn,\n            engine,\n            inMemoryIterable(toCloseableIterator(actionsToCommit.asJava.iterator())))\n        }\n\n        // Read back committed ADD actions\n        val tableVersion = 1\n        val rows = Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n          .getChanges(engine, tableVersion, tableVersion, Set(DeltaAction.ADD).asJava)\n          .toSeq\n          .flatMap(_.getRows.toSeq)\n          .filterNot(row => row.isNullAt(row.getSchema.indexOf(\"add\")))\n\n        assert(rows.size == 1)\n\n        val addFile = new AddFile(rows.head.getStruct(rows.head.getSchema.indexOf(\"add\")))\n        assert(addFile.getTags.isPresent)\n        assert(VectorUtils.toJavaMap(addFile.getTags.get()).asScala.equals(tags))\n      }\n    }\n  }\n\n  test(\"V3: baseRowId and defaultRowCommitVersion are forwarded in remove actions\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = tblPropertiesIcebergWriterCompatV3Enabled)\n\n      // Append 1 add so we have something to remove\n      commitSingleAction(\n        engine,\n        tablePath,\n        createIcebergCompatAction(\n          \"ADD\",\n          \"V3\",\n          _,\n          engine,\n          generateDataFileStatus(tablePath, \"file1.parquet\"),\n          true))\n\n      // Remove with explicit baseRowId and defaultRowCommitVersion\n      val expectedBaseRowId = 42L\n      val expectedDefaultRowCommitVersion = 7L\n      commitSingleAction(\n        engine,\n        tablePath,\n        txn =>\n          GenerateIcebergCompatActionUtils.generateIcebergCompatWriterV3RemoveAction(\n            txn.getTransactionState(engine),\n            generateDataFileStatus(tablePath, \"file1.parquet\"),\n            Collections.emptyMap(),\n            true, // dataChange\n            Optional.of[java.lang.Long](expectedBaseRowId),\n            Optional.of[java.lang.Long](expectedDefaultRowCommitVersion),\n            Optional.empty(), // deletionVectorDescriptor\n            Optional.of(\n              TransactionStateRow.getPhysicalSchema(txn.getTransactionState(engine)))))\n\n      // Read back the remove action and verify the values are preserved\n      val rows = Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n        .getChanges(engine, 2, 2, Set(DeltaAction.REMOVE).asJava)\n        .toSeq\n        .flatMap(_.getRows.toSeq)\n        .filterNot(row => row.isNullAt(row.getSchema.indexOf(\"remove\")))\n\n      assert(rows.size == 1)\n      val removeFile = new RemoveFile(rows.head.getStruct(rows.head.getSchema.indexOf(\"remove\")))\n      assert(\n        removeFile.getBaseRowId.isPresent,\n        \"baseRowId should be present when caller provides it\")\n      assert(removeFile.getBaseRowId.get == expectedBaseRowId)\n      assert(\n        removeFile.getDefaultRowCommitVersion.isPresent,\n        \"defaultRowCommitVersion should be present when caller provides it\")\n      assert(removeFile.getDefaultRowCommitVersion.get == expectedDefaultRowCommitVersion)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/CommitMetadataE2ESuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ListBuffer\n\nimport io.delta.kernel._\nimport io.delta.kernel.commit.{CommitMetadata, CommitResponse, Committer}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.internal.parquet.ParquetSuiteBase\nimport io.delta.kernel.defaults.utils.{TestCommitterUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CommitMetadataE2ESuite extends AnyFunSuite\n    with WriteUtilsWithV2Builders\n    with ParquetSuiteBase\n    with TestCommitterUtils {\n\n  private class CapturingCommitter extends Committer {\n    var latestCommitMetadata: Option[CommitMetadata] = None\n\n    override def commit(\n        engine: Engine,\n        finalizedActions: CloseableIterator[Row],\n        commitMetadata: CommitMetadata): CommitResponse = {\n      latestCommitMetadata = Some(commitMetadata)\n\n      committerUsingPutIfAbsent.commit(engine, finalizedActions, commitMetadata)\n    }\n  }\n\n  test(\"transaction passes added and removed (and not existing) domain metadatas to committer\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== TEST HELPER SETUP =====\n      val capturingCommitter = new CapturingCommitter()\n\n      def createTxnAtLatest(): Transaction =\n        TableManager\n          .loadSnapshot(tablePath)\n          .withCommitter(capturingCommitter)\n          .build(engine)\n          .buildUpdateTableTransaction(\"engineInfo\", Operation.WRITE)\n          .build(engine)\n\n      // ===== GIVEN =====\n      val txn0 = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withTableProperties(Map(\"delta.feature.domainMetadata\" -> \"supported\").asJava)\n        .build(engine)\n      txn0.addDomainMetadata(\"foo\", \"bar\")\n      commitTransaction(txn0, engine, emptyIterable())\n\n      // ===== WHEN (Case 1: No domain metadata change on table with existing domain metadata) =====\n      val txn1 = createTxnAtLatest()\n      commitTransaction(txn1, engine, emptyIterable())\n\n      // ===== THEN (Case 1) =====\n      {\n        val commitMetadata = capturingCommitter.latestCommitMetadata.get\n        assert(commitMetadata.getVersion === 1)\n        assert(commitMetadata.getCommitDomainMetadatas.isEmpty)\n      }\n\n      // ===== WHEN (Case 2: Add domain metadata) =====\n      val txn2 = createTxnAtLatest()\n      txn2.addDomainMetadata(\"zip\", \"zap\")\n      commitTransaction(txn2, engine, emptyIterable())\n\n      // ===== THEN (Case 2) =====\n      {\n        val commitMetadata = capturingCommitter.latestCommitMetadata.get\n        assert(commitMetadata.getVersion === 2)\n\n        val commitDomainMetadatas = commitMetadata.getCommitDomainMetadatas\n        assert(commitDomainMetadatas.size() === 1)\n\n        val dm = commitDomainMetadatas.asScala.head\n        assert(dm.getDomain === \"zip\")\n        assert(dm.getConfiguration === \"zap\")\n        assert(!dm.isRemoved)\n      }\n\n      // ===== WHEN (Case 3: Remove domain metadata) =====\n      val txn3 = createTxnAtLatest()\n      txn3.removeDomainMetadata(\"zip\")\n      commitTransaction(txn3, engine, emptyIterable())\n\n      // ===== THEN (Case 3: Remove domain metadata) =====\n      {\n        val commitMetadata = capturingCommitter.latestCommitMetadata.get\n        assert(commitMetadata.getVersion === 3)\n\n        val commitDomainMetadatas = commitMetadata.getCommitDomainMetadatas\n        assert(commitDomainMetadatas.size() === 1)\n\n        val dm = commitDomainMetadatas.get(0)\n        assert(dm.getDomain === \"zip\")\n        assert(dm.isRemoved)\n      }\n\n    }\n  }\n\n  test(\"maxKnownPublishedDeltaVersion is -1 for CREATE operation\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val capturingCommitter = new CapturingCommitter()\n\n      // Create the table\n      TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withCommitter(capturingCommitter)\n        .build(engine)\n        .commit(engine, emptyIterable())\n\n      // Verify\n      val commitMetadata = capturingCommitter.latestCommitMetadata.get\n      assert(commitMetadata.getVersion === 0)\n      assert(commitMetadata.getMaxKnownPublishedDeltaVersion.get === -1L)\n    }\n  }\n\n  test(\"maxKnownPublishedDeltaVersion is correctly passed for UPDATE operation\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val capturingCommitter = new CapturingCommitter()\n\n      // Create the table\n      TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .build(engine)\n        .commit(engine, emptyIterable())\n\n      // Update the table\n      TableManager\n        .loadSnapshot(tablePath)\n        .withCommitter(capturingCommitter)\n        .build(engine)\n        .buildUpdateTableTransaction(\"engineInfo\", Operation.WRITE)\n        .build(engine)\n        .commit(engine, emptyIterable())\n\n      // Verify\n      val commitMetadata = capturingCommitter.latestCommitMetadata.get\n      assert(commitMetadata.getVersion === 1)\n      assert(commitMetadata.getMaxKnownPublishedDeltaVersion.get === 0L)\n    }\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/CreateCheckpointSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.{Table, TableManager}\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.defaults.utils.{TestRow, TestUtils, WriteUtils}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{CheckpointAlreadyExistsException, TableNotFoundException}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.SnapshotImpl\n\nimport org.apache.spark.sql.delta.{DeltaLog, VersionNotFoundException}\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, RemoveFile}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.sql.types.{IntegerType, StructType}\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for `io.delta.kernel.Table.checkpoint(engine, version)`\n */\nclass CreateCheckpointSuite extends CheckpointBase {\n\n  ///////////\n  // Tests //\n  ///////////\n\n  /**\n   * Helper for tests.\n   *\n   * Creates a new table at version 0, then appends {@code commits} additional commits.\n   * Returns the `_delta_log` directory.\n   */\n  private def setupTestTable(\n      engine: Engine,\n      tablePath: String,\n      tableProperties: Map[String, String],\n      commits: Int): File = {\n    val data = Seq(Map.empty[String, Literal] ->\n      generateData(testSchema, Seq.empty, Map.empty, batchSize = 1, numBatches = 1))\n\n    // Create table (version 0) and add commits (version 1..commits)\n    appendData(\n      engine,\n      tablePath,\n      isNewTable = true,\n      schema = testSchema,\n      data = data,\n      tableProperties = tableProperties)\n\n    for (_ <- 1 to commits) { appendData(engine, tablePath, data = data) }\n\n    val deltaLogDir = new File(tablePath, \"_delta_log\")\n    assert(deltaLogDir.listFiles().count(_.getName.endsWith(\".json\")) === commits + 1)\n    deltaLogDir\n  }\n\n  Seq(true, false).foreach { includeRemoves =>\n    val testMsgUpdate = if (includeRemoves) \" and removes\" else \"\"\n    test(s\"commits containing adds$testMsgUpdate, and no previous checkpoint\") {\n      withTempDirAndEngine { (tablePath, tc) =>\n        addData(tablePath, alternateBetweenAddsAndRemoves = includeRemoves, numberIter = 10)\n\n        // before creating checkpoint, read and save the expected results using Spark\n        val expResults = readUsingSpark(tablePath)\n        assert(expResults.size === (if (includeRemoves) 45 else 100))\n\n        val checkpointVersion = 9\n        kernelCheckpoint(tc, tablePath, checkpointVersion)\n\n        verifyResults(tablePath, expResults, checkpointVersion)\n        verifyLastCheckpointMetadata(\n          tablePath,\n          checkpointVersion,\n          expSize = if (includeRemoves) 5 else 10)\n\n        // add few more commits and verify the read still works\n        appendCommit(tablePath)\n        val newExpResults = expResults ++ Seq.range(0, 10).map(_.longValue()).map(TestRow(_))\n        verifyResults(tablePath, newExpResults, checkpointVersion)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { includeRemoves =>\n    Seq(\n      // Create a checkpoint using Spark (either classic or multi-part checkpoint)\n      1000000, // use large number of actions per file to make Spark create a classic checkpoint\n      3 // use small number of actions per file to make Spark create a multi-part checkpoint\n    ).foreach { sparkCheckpointActionPerFile =>\n      val testMsgUpdate = if (includeRemoves) \" and removes\" else \"\"\n\n      test(s\"commits containing adds$testMsgUpdate, and a previous checkpoint \" +\n        s\"created using Spark (actions/perfile): $sparkCheckpointActionPerFile\") {\n        withTempDirAndEngine { (tablePath, tc) =>\n          addData(tablePath, includeRemoves, numberIter = 6)\n\n          // checkpoint using Spark\n          sparkCheckpoint(tablePath, actionsPerFile = sparkCheckpointActionPerFile)\n\n          addData(tablePath, includeRemoves, numberIter = 6) // add some more data\n\n          // before creating checkpoint, read and save the expected results using Spark\n          val expResults = readUsingSpark(tablePath)\n          assert(expResults.size === (if (includeRemoves) 54 else 120))\n\n          val checkpointVersion = 11\n\n          kernelCheckpoint(tc, tablePath, checkpointVersion)\n          verifyResults(tablePath, expResults, checkpointVersion)\n          verifyLastCheckpointMetadata(\n            tablePath,\n            checkpointVersion,\n            expSize = if (includeRemoves) 6 else 12)\n\n          // add few more commits and verify the read still works\n          appendCommit(tablePath)\n          val newExpResults = expResults ++ Seq.range(0, 10).map(_.longValue()).map(TestRow(_))\n          verifyResults(tablePath, newExpResults, checkpointVersion)\n        }\n      }\n    }\n  }\n\n  test(\"commits with metadata updates\") {\n    withTempDirAndEngine { (tablePath, tc) =>\n      addData(path = tablePath, alternateBetweenAddsAndRemoves = true, numberIter = 16)\n\n      // makes the latest table version 16\n      spark.sql(\n        s\"\"\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES ('delta.appendOnly' = 'true')\"\"\")\n\n      // before creating checkpoint, read and save the expected results using Spark\n      val expResults = readUsingSpark(tablePath)\n      assert(expResults.size === 72)\n\n      val checkpointVersion = 16\n      kernelCheckpoint(tc, tablePath, checkpointVersion)\n      verifyResults(tablePath, expResults, checkpointVersion)\n      verifyLastCheckpointMetadata(tablePath, checkpointVersion, expSize = 8)\n\n      // verify there is only one metadata entry in the checkpoint and it has the\n      // configuration with `delta.appendOnly` = `true`\n      val result = spark.read.format(\"parquet\")\n        .load(checkpointFilePath(tablePath, checkpointVersion))\n        .filter(\"metaData is not null\")\n        .select(\"metaData.configuration\")\n        .collect().toSeq.map(TestRow(_))\n\n      val expected = Seq(TestRow(Map(\"delta.appendOnly\" -> \"true\")))\n\n      checkAnswer(result, expected)\n    }\n  }\n\n  test(\"commits with protocol updates\") {\n    withTempDirAndEngine { (tablePath, tc) =>\n      addData(path = tablePath, alternateBetweenAddsAndRemoves = true, numberIter = 16)\n\n      spark.sql(\n        s\"\"\"\n           |ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES (\n           |  'delta.minReaderVersion' = '1',\n           |  'delta.minWriterVersion' = '2'\n           |)\n           |\"\"\".stripMargin\n      ) // makes the latest table version 16\n\n      // before creating checkpoint, read and save the expected results using Spark\n      val expResults = readUsingSpark(tablePath)\n      assert(expResults.size === 72)\n\n      val checkpointVersion = 16\n      kernelCheckpoint(tc, tablePath, checkpointVersion)\n      verifyResults(tablePath, expResults, checkpointVersion)\n\n      // verify there is only one protocol entry in the checkpoint and it has the\n      // expected minReaderVersion and minWriterVersion\n      val result = spark.read.format(\"parquet\")\n        .load(checkpointFilePath(tablePath, checkpointVersion))\n        .filter(\"protocol is not null\")\n        .select(\"protocol.minReaderVersion\", \"protocol.minWriterVersion\")\n        .collect().toSeq.map(TestRow(_))\n\n      val expected = Seq(TestRow(1, 2))\n\n      checkAnswer(result, expected)\n    }\n  }\n\n  test(\"commits with set transactions\") {\n    withTempDirAndEngine { (tablePath, tc) =>\n      def idempotentAppend(appId: String, version: Int): Unit = {\n        spark.range(end = 10).repartition(2).write.format(\"delta\")\n          .option(\"txnAppId\", appId)\n          .option(\"txnVersion\", version)\n          .mode(\"append\").save(tablePath)\n      }\n\n      idempotentAppend(\"appId1\", 0) // version 0\n      idempotentAppend(\"appId1\", 2) // version 1\n      idempotentAppend(\"appId1\", 3) // version 2\n      deleteCommit(tablePath) // version 3\n      idempotentAppend(\"appId2\", 7) // version 4\n      idempotentAppend(\"appId2\", 25) // version 5\n      idempotentAppend(\"appId3\", 7908) // version 6\n      appendCommit(tablePath) // version 7, no txn identifiers\n      idempotentAppend(\"appId4\", 12312312) // version 8\n\n      // before creating checkpoint, read and save the expected results using Spark\n      val expResults = readUsingSpark(tablePath)\n      assert(expResults.size === 77)\n\n      val checkpointVersion = 8\n      kernelCheckpoint(tc, tablePath, checkpointVersion);\n      verifyResults(tablePath, expResults, checkpointVersion)\n\n      // Load the checkpoint and verify that only the last txn identifier for each appId is stored\n      def verifyTxnIdInCheckpoint(appId: String, expVersion: Long): Unit = {\n        val result = spark.read.format(\"parquet\")\n          .load(checkpointFilePath(tablePath, checkpointVersion))\n          .filter(s\"txn is not null and txn.appId='$appId'\")\n          .select(\"txn.appId\", \"txn.version\")\n          .collect().toSeq.map(TestRow(_))\n        checkAnswer(result, Seq(TestRow(appId, expVersion)))\n      }\n\n      verifyTxnIdInCheckpoint(\"appId1\", 3)\n      verifyTxnIdInCheckpoint(\"appId2\", 25)\n      verifyTxnIdInCheckpoint(\"appId3\", 7908)\n      verifyTxnIdInCheckpoint(\"appId4\", 12312312)\n    }\n  }\n\n  Seq(None, Some(\"2 days\"), Some(\"0 days\")).foreach { retentionInterval =>\n    test(s\"checkpoint contains all not expired tombstones: $retentionInterval\") {\n      withTempDirAndEngine { (tablePath, tc) =>\n        def addFile(path: String): AddFile = AddFile(\n          path = path,\n          partitionValues = Map.empty,\n          size = 0,\n          modificationTime = 0L,\n          dataChange = true)\n\n        def removeFile(path: String, deletionTimestamp: Long): Unit = {\n          val remove = RemoveFile(path = path, deletionTimestamp = Some(deletionTimestamp))\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          val txn = deltaLog.startTransaction()\n          txn.commit(Seq(remove), ManualUpdate)\n        }\n\n        def addFiles(addFiles: String*): Unit = {\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          val txn = deltaLog.startTransaction()\n          val configuration = retentionInterval.map(interval =>\n            Map(\"delta.deletedFileRetentionDuration\" -> interval)).getOrElse(Map.empty)\n          txn.updateMetadata(Metadata(\n            schemaString = new StructType().add(\"c1\", IntegerType).json,\n            configuration = configuration))\n          txn.commit(addFiles.map(addFile(_)), ManualUpdate)\n        }\n\n        def millisPerDays(days: Int): Long = days * 24 * 60 * 60 * 1000\n\n        // version 0\n        addFiles(\n          \"file1\",\n          \"file2\",\n          \"file3\",\n          \"file4\",\n          \"file5\",\n          \"file6\",\n          \"file7\",\n          \"file8\",\n          \"file9\")\n\n        val now = System.currentTimeMillis()\n        removeFile(\"file8\", deletionTimestamp = 1) // set delete time very old\n        removeFile(\"file7\", deletionTimestamp = now - millisPerDays(8))\n        removeFile(\"file6\", deletionTimestamp = now - millisPerDays(3))\n        removeFile(\"file5\", deletionTimestamp = now - 1000) // set delete time 1 second ago\n        // end version 4\n\n        // add few more files - version 5\n        addFiles(\n          \"file10\",\n          \"file11\",\n          \"file12\",\n          \"file13\",\n          \"file14\",\n          \"file15\",\n          \"file16\",\n          \"file17\",\n          \"file18\")\n\n        // delete some files again\n        removeFile(\"file3\", deletionTimestamp = now - millisPerDays(9))\n        removeFile(\"file2\", deletionTimestamp = now - millisPerDays(1))\n        // end version 7\n\n        val expected = if (retentionInterval.isEmpty) {\n          // Given the default retention interval is 1 week, the tombstones file8, file 7 and file 3\n          // should be expired and not included in the checkpoint\n          Seq(\"file6\", \"file5\", \"file2\").map(TestRow(_))\n        } else if (retentionInterval.get.equals(\"2 days\")) {\n          // Given the retention interval is 2 days, the tombstones file8, file 7, file 6, file 3\n          // should be expired and not included in the checkpoint\n          Seq(\"file5\", \"file2\").map(TestRow(_))\n        } else {\n          // All tombstones should be excluded in the checkpoint\n          Seq.empty\n        }\n\n        val checkpointVersion = 7\n        kernelCheckpoint(tc, tablePath, checkpointVersion)\n\n        val result = spark.read.format(\"parquet\")\n          .load(checkpointFilePath(tablePath, checkpointVersion))\n          .filter(\"remove is not null\")\n          .select(\"remove.path\")\n          .collect().toSeq.map(TestRow(_))\n\n        checkAnswer(result, expected)\n      }\n    }\n  }\n\n  test(\"try creating checkpoint on a non-existent table\") {\n    withTempDirAndEngine { (path, tc) =>\n      Seq(0, 1, 2).foreach { checkpointVersion =>\n        val ex = intercept[TableNotFoundException] {\n          kernelCheckpoint(tc, path, checkpointVersion)\n        }\n        assert(ex.getMessage.contains(\"not found\"))\n      }\n    }\n  }\n\n  test(\"try creating checkpoint at version that already has a \" +\n    \"checkpoint or a version that doesn't exist\") {\n    withTempDirAndEngine { (path, tc) =>\n      for (_ <- 0 to 3) {\n        appendCommit(path)\n      }\n\n      val table = Table.forPath(tc, path)\n      table.checkpoint(tc, 3)\n      val ex = intercept[CheckpointAlreadyExistsException] {\n        kernelCheckpoint(tc, path, 3)\n      }\n      assert(ex.getMessage.contains(\"Checkpoint for given version 3 already exists in the table\"))\n\n      val ex2 = intercept[Exception] {\n        kernelCheckpoint(tc, path, checkpointVersion = 5)\n      }\n      assert(ex2.getMessage.contains(\"Cannot load table version 5 as it does not exist\"))\n    }\n  }\n\n  test(\"create a checkpoint on a existing table\") {\n    withTempDirAndEngine { (tablePath, tc) =>\n      copyTable(\"time-travel-start-start20-start40\", tablePath)\n\n      // before creating checkpoint, read and save the expected results using Spark\n      val expResults = readUsingSpark(tablePath)\n      assert(expResults.size === 30)\n\n      val checkpointVersion = 2\n      kernelCheckpoint(tc, tablePath, checkpointVersion)\n      verifyResults(tablePath, expResults, checkpointVersion)\n    }\n  }\n\n  test(\"try create a checkpoint on a unsupported table feature table\") {\n    withTempDirAndEngine { (tablePath, tc) =>\n      spark.sql(s\"CREATE TABLE delta.`$tablePath` (name STRING, age INT) USING delta \" +\n        \"TBLPROPERTIES ('delta.constraints.checks' = 'name IS NOT NULL')\")\n\n      for (_ <- 0 to 3) {\n        spark.sql(s\"INSERT INTO delta.`$tablePath` VALUES ('John Doe', 30), ('Bob Johnson', 35)\")\n      }\n\n      val ex2 = intercept[Exception] {\n        kernelCheckpoint(tc, tablePath, checkpointVersion = 4)\n      }\n      assert(ex2.getMessage.contains(\"requires writer table features [checkConstraints] \" +\n        \"which is unsupported by this version of Delta Kernel\"))\n    }\n  }\n\n  test(\"create a checkpoint on a table with deletion vectors\") {\n    withTempDirAndEngine { (tablePath, tc) =>\n      copyTable(\"dv-with-columnmapping\", tablePath)\n\n      // before creating checkpoint, read and save the expected results using Spark\n      val expResults = readUsingSpark(tablePath)\n      assert(expResults.size === 35)\n\n      val checkpointVersion = 15\n      kernelCheckpoint(tc, tablePath, checkpointVersion)\n      verifyResults(tablePath, expResults, checkpointVersion)\n    }\n  }\n\n  test(\"log cleanup: non-latest snapshot can NOT trigger log cleanup\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val commits = 3\n      val tableProperties = Map(\n        \"delta.logRetentionDuration\" -> \"interval 0 seconds\",\n        \"delta.enableExpiredLogCleanup\" -> \"true\")\n      val deltaLogDir = setupTestTable(engine, tablePath, tableProperties, commits)\n\n      // Checkpoint at version 2 using SnapshotBuilder.atVersion() - wasBuiltAsLatest=false\n      val snapshot = TableManager.loadSnapshot(tablePath)\n        .atVersion(2)\n        .build(engine)\n        .asInstanceOf[SnapshotImpl]\n      snapshot.writeCheckpoint(engine)\n\n      // Verify no log cleanup happened\n      assert(\n        deltaLogDir.listFiles().count(_.getName.endsWith(\".json\")) === commits + 1,\n        \"Checkpoint on snapshot built with specific version should NOT trigger log cleanup\")\n    }\n  }\n\n  test(\"log cleanup: latest snapshot can trigger log cleanup\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val commits = 3\n      val tableProperties = Map(\n        \"delta.logRetentionDuration\" -> \"interval 0 seconds\",\n        \"delta.enableExpiredLogCleanup\" -> \"true\")\n      val deltaLogDir = setupTestTable(engine, tablePath, tableProperties, commits)\n\n      // Get latest snapshot (version == commits) using builder without atVersion() -\n      // wasBuiltAsLatest=true\n      val latestSnapshot = TableManager.loadSnapshot(tablePath)\n        .build(engine)\n        .asInstanceOf[SnapshotImpl]\n      assert(latestSnapshot.wasBuiltAsLatest())\n\n      latestSnapshot.writeCheckpoint(engine)\n\n      // Verify log cleanup happened\n      assert(\n        deltaLogDir.listFiles().count(_.getName.endsWith(\".json\")) < commits + 1,\n        \"Checkpoint on snapshot built without specific version should trigger log cleanup\")\n    }\n  }\n\n  test(\n    \"log cleanup: checkpointProtection enabled prevents log cleanup, \" +\n      \"even snapshot is built as latest\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val commits = 3\n      val tableProperties = Map(\n        \"delta.logRetentionDuration\" -> \"interval 0 seconds\",\n        \"delta.enableExpiredLogCleanup\" -> \"true\",\n        \"delta.feature.checkpointProtection\" -> \"supported\")\n      val deltaLogDir = setupTestTable(engine, tablePath, tableProperties, commits)\n\n      // Get latest snapshot using builder without atVersion() - wasBuiltAsLatest=true\n      val latestSnapshot = TableManager.loadSnapshot(tablePath)\n        .build(engine)\n        .asInstanceOf[SnapshotImpl]\n      assert(latestSnapshot.wasBuiltAsLatest())\n      assert(latestSnapshot.getProtocol.getWriterFeatures.contains(\"checkpointProtection\"))\n\n      latestSnapshot.writeCheckpoint(engine)\n\n      // Verify no log cleanup (checkpointProtection prevents it)\n      assert(\n        deltaLogDir.listFiles().count(_.getName.endsWith(\".json\")) === commits + 1,\n        \"Log cleanup should NOT happen with checkpointProtection enabled\")\n    }\n  }\n}\n\n/**\n *  Helper methods for suites that do checkpoint operations\n */\ntrait CheckpointBase extends AnyFunSuite with WriteUtils {\n  def addData(path: String, alternateBetweenAddsAndRemoves: Boolean, numberIter: Int): Unit = {\n    Seq.range(0, numberIter).foreach { version =>\n      if (version % 2 == 1 && alternateBetweenAddsAndRemoves) {\n        deleteCommit(path) // removes one file and adds a new one\n      } else {\n        appendCommit(path) // add one new file\n      }\n    }\n  }\n\n  def appendCommit(path: String): Unit =\n    spark.range(end = 10).write.format(\"delta\").mode(\"append\").save(path)\n\n  def deleteCommit(path: String): Unit = {\n    spark.sql(s\"DELETE FROM delta.`${path}` WHERE id = 5\")\n  }\n\n  def sparkCheckpoint(path: String, actionsPerFile: Int = 10000000): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> actionsPerFile.toString) {\n      DeltaLog.forTable(spark, path).checkpoint()\n    }\n  }\n\n  def kernelCheckpoint(tc: Engine, tablePath: String, checkpointVersion: Long): Unit = {\n    Table.forPath(tc, tablePath).checkpoint(tc, checkpointVersion)\n  }\n\n  def readUsingSpark(tablePath: String): Seq[TestRow] = {\n    spark.read.format(\"delta\").load(tablePath).collect().map(TestRow(_))\n  }\n\n  def verifyResults(\n      tablePath: String,\n      expResults: Seq[TestRow],\n      checkpointVersion: Long): Unit = {\n    // before verifying delete the delta commits before the checkpoint to make sure\n    // the state is constructed using the table path\n    deleteDeltaFilesBefore(tablePath, checkpointVersion)\n\n    // verify using Spark reader\n    checkAnswer(readUsingSpark(tablePath), expResults)\n\n    // verify using Kernel reader\n    checkTable(tablePath, expResults)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DataSkippingDeltaTestsUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.immutable.Seq\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.AddFile\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.{Expression, PredicateHelper}\n\n/**\n * Encapsulates a few Delta-Spark DataSkipping Utils.\n * E.g A helper to get files in a deltaScan after applying a predicate.\n */\ntrait DataSkippingDeltaTestsUtils extends PredicateHelper {\n\n  /** Parses a predicate string into Spark expressions by analyzing the optimized query plan. */\n  def parse(spark: SparkSession, deltaLog: DeltaLog, predicate: String): Seq[Expression] = {\n    if (predicate == \"True\") {\n      Seq(org.apache.spark.sql.catalyst.expressions.Literal.TrueLiteral)\n    } else {\n      val filtered = spark.read.format(\"delta\").load(deltaLog.dataPath.toString).where(predicate)\n      filtered\n        .queryExecution\n        .optimizedPlan\n        .expressions\n        .flatMap(splitConjunctivePredicates)\n        .toList\n    }\n  }\n\n  /**\n   * Returns the number of files that would be read when applying\n   * the given predicate (for data skipping validation).\n   */\n  def filesReadCount(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      predicate: String): Int = {\n    getFilesRead(spark, deltaLog, predicate, checkEmptyUnusedFilters = false).size\n  }\n\n  /**\n   * Returns the files that should be included in a scan after applying the given predicate on\n   * a snapshot of the Delta log.\n   *\n   * @param deltaLog                Delta log for a table.\n   * @param predicate               Predicate to run on the Delta table.\n   * @param checkEmptyUnusedFilters If true, check if there were no unused filters, meaning\n   *                                the given predicate was used as data or partition filters.\n   * @return The files that should be included in a scan after applying the predicate.\n   */\n  def getFilesRead(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      predicate: String,\n      checkEmptyUnusedFilters: Boolean): Seq[AddFile] = {\n    val parsed: Seq[Expression] = parse(spark, deltaLog, predicate)\n    val res = deltaLog.snapshot.filesForScan(parsed)\n    assert(res.total.files.get == deltaLog.snapshot.numOfFiles)\n    assert(res.total.bytesCompressed.get == deltaLog.snapshot.sizeInBytes)\n    assert(res.scanned.files.get == res.files.size)\n    assert(res.scanned.bytesCompressed.get == res.files.map(_.size).sum)\n    assert(!checkEmptyUnusedFilters || res.unusedFilters.isEmpty)\n    res.files.toList\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeletionVectorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.defaults.utils.{TestRow, TestUtils}\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeletionVectorSuite extends AnyFunSuite with TestUtils {\n\n  test(\"end-to-end usage: reading a table with dv\") {\n    checkTable(\n      path = getTestResourceFilePath(\"basic-dv-no-checkpoint\"),\n      expectedAnswer = (2L until 10L).map(TestRow(_)))\n  }\n\n  test(\"end-to-end usage: reading a table with dv with space in the root path\") {\n    withTempDir { tempDir =>\n      val target = tempDir.getCanonicalPath + \"spark test\"\n      spark.sql(s\"\"\"CREATE TABLE tbl (\n          id int\n        ) USING delta LOCATION '$target'\n        TBLPROPERTIES ('delta.enableDeletionVectors' = true) \"\"\")\n      spark.sql(\"INSERT INTO tbl VALUES (1),(2),(3),(4),(5)\")\n      spark.sql(\"DELETE FROM tbl WHERE id = 1\")\n      checkTable(\n        path = target,\n        expectedAnswer = Seq(TestRow(2), TestRow(3), TestRow(4), TestRow(5)))\n    }\n  }\n\n  test(\"end-to-end usage: reading a table with dv with checkpoint\") {\n    checkTable(\n      path = getTestResourceFilePath(\"basic-dv-with-checkpoint\"),\n      expectedAnswer = (0L until 500L).filter(_ % 11 != 0).map(TestRow(_)))\n  }\n\n  test(\"end-to-end usage: reading partitioned dv table with checkpoint\") {\n    val conf = new Configuration()\n    // Set the batch size small enough so there will be multiple batches\n    conf.setInt(\"delta.kernel.default.parquet.reader.batch-size\", 2)\n\n    val expectedResult = (0 until 50).map(x => (x % 10, x, s\"foo${x % 5}\"))\n      .filter { case (_, col1, _) =>\n        !(col1 % 2 == 0 && col1 < 30)\n      }\n\n    checkTable(\n      path = goldenTablePath(\"dv-partitioned-with-checkpoint\"),\n      expectedAnswer = expectedResult.map(TestRow.fromTuple(_)),\n      engine = defaultEngine)\n  }\n\n  test(\n    \"end-to-end usage: reading partitioned dv table with checkpoint with columnMappingMode=name\") {\n    val expectedResult = (0 until 50).map(x => (x % 10, x, s\"foo${x % 5}\"))\n      .filter { case (_, col1, _) =>\n        !(col1 % 2 == 0 && col1 < 30)\n      }\n    checkTable(\n      path = goldenTablePath(\"dv-with-columnmapping\"),\n      expectedAnswer = expectedResult.map(TestRow.fromTuple(_)))\n  }\n\n  // TODO detect corrupted DV checksum\n  // TODO detect corrupted dv size\n  // TODO multiple dvs in one file\n}\n\nobject DeletionVectorsSuite {\n  // TODO: test using this once we support reading by version\n  val table1Path = \"src/test/resources/delta/table-with-dv-large\"\n  // Table at version 0: contains [0, 2000)\n  val expectedTable1DataV0 = Seq.range(0, 2000)\n  // Table at version 1: removes rows with id = 0, 180, 300, 700, 1800\n  val v1Removed = Set(0, 180, 300, 700, 1800)\n  val expectedTable1DataV1 = expectedTable1DataV0.filterNot(e => v1Removed.contains(e))\n  // Table at version 2: inserts rows with id = 300, 700\n  val v2Added = Set(300, 700)\n  val expectedTable1DataV2 = expectedTable1DataV1 ++ v2Added\n  // Table at version 3: removes rows with id = 300, 250, 350, 900, 1353, 1567, 1800\n  val v3Removed = Set(300, 250, 350, 900, 1353, 1567, 1800)\n  val expectedTable1DataV3 = expectedTable1DataV2.filterNot(e => v3Removed.contains(e))\n  // Table at version 4: inserts rows with id = 900, 1567\n  val v4Added = Set(900, 1567)\n  val expectedTable1DataV4 = expectedTable1DataV3 ++ v4Added\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.exceptions.InvalidConfigurationValueException\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.util.{ColumnMapping, ColumnMappingSuiteBase}\nimport io.delta.kernel.types.{FieldMetadata, IntegerType, StringType, StructField, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeltaColumnMappingTransactionBuilderV1Suite extends DeltaColumnMappingSuiteBase\n    with WriteUtils {}\n\nclass DeltaColumnMappingTransactionBuilderV2Suite extends DeltaColumnMappingSuiteBase\n    with WriteUtilsWithV2Builders {}\n\ntrait DeltaColumnMappingSuiteBase extends AnyFunSuite with AbstractWriteUtils\n    with ColumnMappingSuiteBase {\n\n  val simpleTestSchema = new StructType()\n    .add(\"a\", StringType.STRING, true)\n    .add(\"b\", IntegerType.INTEGER, true)\n\n  test(\"create table with unsupported column mapping mode\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val ex = intercept[InvalidConfigurationValueException] {\n        val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"invalid\")\n        createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n      }\n      assert(ex.getMessage.contains(\"Invalid value for table property \" +\n        \"'delta.columnMapping.mode': 'invalid'. Needs to be one of: [none, id, name].\"))\n    }\n  }\n\n  test(\"create table with column mapping mode = none\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"none\")\n      createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n\n      assert(getMetadata(engine, tablePath).getSchema.equals(simpleTestSchema))\n    }\n  }\n\n  test(\"cannot update table with unsupported column mapping mode\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, simpleTestSchema)\n\n      val ex = intercept[InvalidConfigurationValueException] {\n        val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"invalid\")\n        updateTableMetadata(engine, tablePath, tableProperties = props)\n      }\n      assert(ex.getMessage.contains(\"Invalid value for table property \" +\n        \"'delta.columnMapping.mode': 'invalid'. Needs to be one of: [none, id, name].\"))\n    }\n  }\n\n  test(\"new table with column mapping mode = name\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n      createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n\n      val structType = getMetadata(engine, tablePath).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"b\"), 2)\n\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.getMinReaderVersion == 2 && protocol.getMinWriterVersion == 7)\n    }\n  }\n\n  test(\"new table with column mapping mode = id\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n      createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n\n      val structType = getMetadata(engine, tablePath).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"b\"), 2)\n\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 2)\n\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.getMinReaderVersion == 2 && protocol.getMinWriterVersion == 7)\n    }\n  }\n\n  test(\"new table with existing column mappings in schema writes COLUMN_MAPPING_MAX_COLUMN_ID\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n      val fieldMetadata = FieldMetadata.builder()\n        .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n        .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-0\").build()\n      val structField = new StructField(\"col_name\", IntegerType.INTEGER, false, fieldMetadata)\n      val schema = new StructType(Seq(structField).asJava)\n      createEmptyTable(engine, tablePath, schema, tableProperties = props)\n\n      val structtype = getMetadata(engine, tablePath).getSchema\n      assertColumnMapping(structtype.get(\"col_name\"), 1)\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 1)\n    }\n  }\n\n  test(\"can update existing table to column mapping mode = name\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, simpleTestSchema)\n      val structType = getMetadata(engine, tablePath).getSchema\n      assert(structType.equals(simpleTestSchema))\n\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n      updateTableMetadata(engine, tablePath, tableProperties = props)\n\n      val updatedSchema = getMetadata(engine, tablePath).getSchema\n      assertColumnMapping(updatedSchema.get(\"a\"), 1, \"a\")\n      assertColumnMapping(updatedSchema.get(\"b\"), 2, \"b\")\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { startingCMMode =>\n    test(s\"cannot update table with unsupported column mapping mode change: $startingCMMode\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> startingCMMode)\n        createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n\n        val structType = getMetadata(engine, tablePath).getSchema\n        assertColumnMapping(structType.get(\"a\"), 1)\n        assertColumnMapping(structType.get(\"b\"), 2)\n\n        val ex = intercept[IllegalArgumentException] {\n          val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"none\")\n          updateTableMetadata(engine, tablePath, tableProperties = props)\n        }\n        assert(ex.getMessage.contains(s\"Changing column mapping mode \" +\n          s\"from '$startingCMMode' to 'none' is not supported\"))\n      }\n    }\n  }\n\n  test(\"cannot update column mapping mode from name to id on existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n      createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n\n      val structType = getMetadata(engine, tablePath).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"b\"), 2)\n\n      val ex = intercept[IllegalArgumentException] {\n        val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n        updateTableMetadata(engine, tablePath, tableProperties = props)\n      }\n      assert(ex.getMessage.contains(\"Changing column mapping mode \" +\n        \"from 'name' to 'id' is not supported\"))\n    }\n  }\n\n  test(\"cannot update column mapping mode from none to id on existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, simpleTestSchema)\n\n      val structType = getMetadata(engine, tablePath).getSchema\n      assert(structType.equals(simpleTestSchema))\n\n      val ex = intercept[IllegalArgumentException] {\n        val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n        updateTableMetadata(engine, tablePath, tableProperties = props)\n      }\n      assert(ex.getMessage.contains(\"Changing column mapping mode \" +\n        \"from 'none' to 'id' is not supported\"))\n    }\n  }\n\n  test(\"update table properties on a column mapping enabled table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n      createEmptyTable(engine, tablePath, simpleTestSchema, tableProperties = props)\n\n      val metadata = getMetadata(engine, tablePath)\n      assertColumnMapping(metadata.getSchema.get(\"a\"), 1)\n      assertColumnMapping(metadata.getSchema.get(\"b\"), 2)\n\n      val newProps = Map(\"key\" -> \"value\")\n      updateTableMetadata(engine, tablePath, tableProperties = newProps)\n\n      assert(getMetadata(engine, tablePath).getConfiguration.get(\"key\") == \"value\")\n    }\n  }\n\n  Seq(true, false).foreach { withIcebergCompatV2 =>\n    test(s\"new table with column mapping mode = name and nested schema, \" +\n      s\"enableIcebergCompatV2 = $withIcebergCompatV2\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val props = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> withIcebergCompatV2.toString)\n\n        createEmptyTable(engine, tablePath, cmTestSchema(), tableProperties = props)\n\n        verifyCMTestSchemaHasValidColumnMappingInfo(\n          getMetadata(engine, tablePath),\n          isNewTable = true,\n          enableIcebergCompatV2 = withIcebergCompatV2)\n      }\n    }\n  }\n\n  test(\"subsequent updates don't update the metadata again when there is no change\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val props = Map(\n        TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n        TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")\n\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = props)\n\n      appendData(engine, tablePath, data = Seq.empty) // version 1\n      appendData(engine, tablePath, data = Seq.empty) // version 2\n\n      val table = Table.forPath(engine, tablePath)\n      assert(getMetadataActionFromCommit(engine, table, version = 0).isDefined)\n      assert(getMetadataActionFromCommit(engine, table, version = 1).isEmpty)\n      assert(getMetadataActionFromCommit(engine, table, version = 2).isEmpty)\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { cmMode =>\n    test(s\"test writing data into a column mapping enabled table is blocked: $cmMode\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val props = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> cmMode)\n        createEmptyTable(engine, tablePath, testSchema, tableProperties = props)\n\n        val ex = intercept[UnsupportedOperationException] {\n          appendData(engine, tablePath, data = Seq(Map.empty[String, Literal] -> dataBatches1))\n        }\n        assert(ex.getMessage.contains(\n          \"Writing into column mapping enabled table is not supported yet.\"))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaIcebergCompatBaseSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.defaults.utils.AbstractWriteUtils\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.tablefeatures.{TableFeature, TableFeatures}\nimport io.delta.kernel.internal.util.ColumnMappingSuiteBase\nimport io.delta.kernel.types.{DataType, DateType, IntegerType, LongType, StructField, StructType, TimestampNTZType, TypeChange}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Base suite containing common test cases for Delta Iceberg compatibility features.\n * This includes tests that apply to both V2 and V3 compatibility modes.\n */\ntrait DeltaIcebergCompatBaseSuite extends AnyFunSuite with AbstractWriteUtils\n    with ColumnMappingSuiteBase {\n\n  /** The name of the iceberg compatibility version for display in test names */\n  def icebergCompatVersion: String\n\n  /** The table property key for enabling the specific iceberg compatibility version */\n  def icebergCompatEnabledKey: String\n\n  /** The table feature that should be enabled for this compatibility version */\n  def expectedTableFeatures: Seq[TableFeature]\n\n  def supportedDataColumnTypes: Seq[DataType]\n\n  def supportedPartitionColumnTypes: Seq[DataType]\n\n  supportedDataColumnTypes.foreach {\n    dataType: DataType =>\n      test(s\"allowed data column types: $dataType on creating table\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          val schema = new StructType().add(\"col\", dataType)\n          val tblProps = Map(icebergCompatEnabledKey -> \"true\")\n          createEmptyTable(engine, tablePath, schema, tableProperties = tblProps)\n        }\n      }\n  }\n\n  supportedPartitionColumnTypes.foreach {\n    dataType: DataType =>\n      test(s\"allowed partition column types: $dataType on creating table\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          val schema = new StructType().add(\"col\", dataType)\n          val partitionCols = Seq(\"col\")\n          val tblProps = Map(icebergCompatEnabledKey -> \"true\")\n          createEmptyTable(engine, tablePath, schema, partitionCols, tableProperties = tblProps)\n        }\n      }\n  }\n\n  test(s\"enable $icebergCompatVersion on creating table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(icebergCompatEnabledKey -> \"true\")\n      createEmptyTable(engine, tablePath, cmTestSchema(), tableProperties = tblProps)\n\n      val protocol = getProtocol(engine, tablePath)\n      expectedTableFeatures.foreach { feature =>\n        assert(protocol.supportsFeature(feature))\n      }\n\n      val metadata = getMetadata(engine, tablePath)\n      val actualCMMode = metadata.getConfiguration.get(TableConfig.COLUMN_MAPPING_MODE.getKey)\n      assert(actualCMMode === \"name\")\n      verifyCMTestSchemaHasValidColumnMappingInfo(metadata)\n    }\n  }\n\n  test(s\"compatible type widening is allowed with $icebergCompatVersion\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a table with icebergCompat and type widening enabled\n      val schema = new StructType()\n        .add(new StructField(\n          \"intToLong\",\n          LongType.LONG,\n          false).withTypeChanges(Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava))\n\n      val tblProps = Map(\n        icebergCompatEnabledKey -> \"true\",\n        TableConfig.TYPE_WIDENING_ENABLED.getKey -> \"true\")\n\n      // This should not throw an exception\n      createEmptyTable(engine, tablePath, schema, tableProperties = tblProps)\n      appendData(engine, tablePath, data = Seq.empty)\n\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n      val metadata = getMetadata(engine, tablePath)\n      assert(metadata.getSchema.get(\"intToLong\").getTypeChanges.asScala == schema.get(\n        \"intToLong\").getTypeChanges.asScala)\n    }\n  }\n\n  test(s\"incompatible type widening throws exception with $icebergCompatVersion\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Try to create a table with icebergCompat and incompatible type widening\n      val schema = new StructType()\n        .add(\n          new StructField(\n            \"dateToTimestamp\",\n            TimestampNTZType.TIMESTAMP_NTZ,\n            false).withTypeChanges(Seq(\n            new TypeChange(DateType.DATE, TimestampNTZType.TIMESTAMP_NTZ)).asJava))\n\n      val tblProps = Map(\n        icebergCompatEnabledKey -> \"true\",\n        TableConfig.TYPE_WIDENING_ENABLED.getKey -> \"true\")\n\n      val e = intercept[KernelException] {\n        createEmptyTable(engine, tablePath, schema, tableProperties = tblProps)\n      }\n\n      assert(\n        e.getMessage.contains(\n          s\"$icebergCompatVersion does not support type widening present in table\"))\n    }\n  }\n\n  /**\n   * Utility that checks after executing given fn gets the given exception and error message.\n   * [[ClassTag]] is used to preserve the type information during the runtime.\n   */\n  def checkError[T <: Throwable: ClassTag](expectedMessage: String)(fn: => Unit): Unit = {\n    val e = intercept[T] {\n      fn\n    }\n    assert(e.getMessage.contains(expectedMessage))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaIcebergCompatV2Suite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.tablefeatures.{TableFeature, TableFeatures}\nimport io.delta.kernel.internal.util.{ColumnMapping, VectorUtils}\nimport io.delta.kernel.types.{DataType, DateType, FieldMetadata, StructField, StructType, TimestampNTZType, TypeChange}\n\nclass DeltaIcebergCompatV2TransactionBuilderV1Suite extends DeltaIcebergCompatV2SuiteBase\n    with WriteUtils {}\n\nclass DeltaIcebergCompatV2TransactionBuilderV2Suite extends DeltaIcebergCompatV2SuiteBase\n    with WriteUtilsWithV2Builders {}\n\n/** This suite tests reading or writing into Delta table that have `icebergCompatV2` enabled. */\ntrait DeltaIcebergCompatV2SuiteBase extends DeltaIcebergCompatBaseSuite {\n\n  override def icebergCompatVersion: String = \"icebergCompatV2\"\n\n  override def icebergCompatEnabledKey: String = TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey\n\n  override def expectedTableFeatures: Seq[TableFeature] = Seq(\n    TableFeatures.ICEBERG_COMPAT_V2_W_FEATURE,\n    TableFeatures.COLUMN_MAPPING_RW_FEATURE)\n\n  override def supportedDataColumnTypes: Seq[DataType] = ALL_TYPES.toList\n\n  override def supportedPartitionColumnTypes: Seq[DataType] = PRIMITIVE_TYPES.toList\n\n  ignore(\"can't enable icebergCompatV2 on a table with icebergCompatv1 enabled\") {\n    // We can't test this as Kernel throws error when enabling icebergCompatV1\n    // as there is no support it in the current version.\n    // This is covered in unittests in [[IcebergCompatV2MetadataValidatorAndUpdaterSuite]]\n  }\n\n  ignore(\"test unsupported data types\") {\n    // Can't test this now as the only unsupported data type in Iceberg is VariantType,\n    // and it also has no write support in Kernel.\n    // Unit test for this is covered in the respective MetadataValidatorAndUpdaterSuite\n  }\n\n  Seq(\"id\", \"name\").foreach { existingCMMode =>\n    // also tests enabling icebergCompat on an existing table\n    test(s\"existing column mapping mode `$existingCMMode` is \" +\n      s\"preserved after $icebergCompatVersion is enabled\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val tblProps = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> existingCMMode)\n        createEmptyTable(engine, tablePath, cmTestSchema(), tableProperties = tblProps)\n\n        val newTblProps =\n          Map(icebergCompatEnabledKey -> \"true\")\n        updateTableMetadata(engine, tablePath, tableProperties = newTblProps)\n\n        val protocol = getProtocol(engine, tablePath)\n        assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n        val metadata = getMetadata(engine, tablePath)\n        val actualCMMode = metadata.getConfiguration.get(TableConfig.COLUMN_MAPPING_MODE.getKey)\n        assert(actualCMMode === existingCMMode)\n        verifyCMTestSchemaHasValidColumnMappingInfo(metadata)\n      }\n    }\n  }\n\n  Seq(\"id\", \"name\").foreach { existingCMMode =>\n    test(s\"existing column mapping mode `$existingCMMode` is \" +\n      s\"preserved after $icebergCompatVersion is enabled for new table\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val tblProps = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> existingCMMode,\n          icebergCompatEnabledKey -> \"true\")\n        createEmptyTable(engine, tablePath, cmTestSchema(), tableProperties = tblProps)\n\n        val protocol = getProtocol(engine, tablePath)\n        assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n        val metadata = getMetadata(engine, tablePath)\n        val actualCMMode = metadata.getConfiguration.get(TableConfig.COLUMN_MAPPING_MODE.getKey)\n        assert(actualCMMode === existingCMMode)\n        verifyCMTestSchemaHasValidColumnMappingInfo(metadata)\n      }\n    }\n  }\n\n  test(s\"when column mapping mode is set to 'none`, should fail enabling $icebergCompatVersion\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"none\")\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = tblProps)\n\n      checkError[KernelException](\n        s\"The value 'none' for the property 'delta.columnMapping.mode' is not \" +\n          s\"compatible with $icebergCompatVersion requirements\") {\n        val newTblProps =\n          Map(icebergCompatEnabledKey -> \"true\")\n        updateTableMetadata(engine, tablePath, tableProperties = newTblProps)\n      }\n    }\n  }\n\n  test(s\"can't enable $icebergCompatVersion on an existing table with no column mapping enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n\n      checkError[KernelException](\n        s\"The value 'none' for the property 'delta.columnMapping.mode' is not \" +\n          s\"compatible with $icebergCompatVersion requirements\") {\n        val tblProps = Map(icebergCompatEnabledKey -> \"true\")\n        updateTableMetadata(engine, tablePath, testSchema, tableProperties = tblProps)\n      }\n    }\n  }\n\n  test(\"subsequent writes to icebergCompatV2 enabled tables doesn't update metadata\") {\n    // we want to make sure the [[IcebergCompatV2MetadataValidatorAndUpdater]] doesn't\n    // make unneeded metadata updates\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = tblProps)\n\n      val metadata = getMetadata(engine, tablePath)\n      val actualCMMode = metadata.getConfiguration.get(TableConfig.COLUMN_MAPPING_MODE.getKey)\n      assert(actualCMMode === \"name\")\n\n      appendData(engine, tablePath, data = Seq.empty) // version 1\n      appendData(engine, tablePath, data = Seq.empty) // version 2\n\n      val table = Table.forPath(engine, tablePath)\n      assert(getMetadataActionFromCommit(engine, table, version = 0).isDefined)\n      assert(getMetadataActionFromCommit(engine, table, version = 1).isEmpty)\n      assert(getMetadataActionFromCommit(engine, table, version = 2).isEmpty)\n\n      // make a metadata update and see it is reflected in the table\n      val newProps = Map(\"key\" -> \"value\")\n      updateTableMetadata(engine, tablePath, tableProperties = newProps) // version 3\n      val ver3Metadata: Row = getMetadataActionFromCommit(engine, table, version = 3)\n        .getOrElse(fail(\"Metadata action not found in version 3\"))\n\n      // TODO: ugly, find a better utilities\n      val result = VectorUtils.toJavaMap[String, String](\n        ver3Metadata.getMap(ver3Metadata.getSchema.indexOf(\"configuration\")))\n        .get(\"key\")\n      assert(result === \"value\")\n    }\n  }\n\n  test(s\"can't be enabled on a new table with deletion vectors supported\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      checkError[KernelException](\n        s\"Table features [deletionVectors] are \" +\n          s\"incompatible with $icebergCompatVersion\") {\n        val tblProps = Map(\n          TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\",\n          icebergCompatEnabledKey -> \"true\")\n        createEmptyTable(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n      }\n    }\n  }\n\n  test(s\"can't update an existing table with DVs supported to have $icebergCompatVersion\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(\n        TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\",\n        // without CM on existing table, you can't update to icebergCompat\n        TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n      createEmptyTable(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n\n      checkError[KernelException](\n        s\"Table features [deletionVectors] are \" +\n          s\"incompatible with $icebergCompatVersion\") {\n        val newTblProps =\n          Map(icebergCompatEnabledKey -> \"true\")\n        updateTableMetadata(engine, tablePath, tableProperties = newTblProps)\n      }\n    }\n  }\n\n  test(s\"can't enable deletion vectors on a table with $icebergCompatVersion enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(icebergCompatEnabledKey -> \"true\")\n      createEmptyTable(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n\n      checkError[KernelException](\n        s\"Table features [deletionVectors] are incompatible with $icebergCompatVersion\") {\n        val tblProps = Map(TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\")\n        updateTableMetadata(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n      }\n    }\n  }\n\n  test(\n    s\"incompatible type widening throws exception with\" +\n      s\" $icebergCompatVersion enabled on existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val schema = new StructType()\n        .add(new StructField(\n          \"dateToTimestamp\",\n          TimestampNTZType.TIMESTAMP_NTZ,\n          false,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(\n              ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY,\n              \"col-1\").build()).withTypeChanges(\n          Seq(new TypeChange(DateType.DATE, TimestampNTZType.TIMESTAMP_NTZ)).asJava))\n\n      val tblProps = Map(TableConfig.TYPE_WIDENING_ENABLED.getKey -> \"true\")\n      createEmptyTable(engine, tablePath, schema, tableProperties = tblProps)\n\n      val e = intercept[KernelException] {\n        updateTableMetadata(\n          engine,\n          tablePath,\n          tableProperties = Map(\n            icebergCompatEnabledKey -> \"true\",\n            TableConfig.COLUMN_MAPPING_MODE.getKey -> \"ID\"))\n      }\n\n      assert(\n        e.getMessage.contains(\n          s\"$icebergCompatVersion does not support type widening present in table\"))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaIcebergCompatV3Suite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.tablefeatures.{TableFeature, TableFeatures}\nimport io.delta.kernel.internal.util.{ColumnMapping, VectorUtils}\nimport io.delta.kernel.types.{DataType, DateType, FieldMetadata, IntegerType, LongType, StructField, StructType, TimestampNTZType, TypeChange, VariantType}\n\nclass DeltaIcebergCompatV3TransactionBuilderV1Suite extends DeltaIcebergCompatV3SuiteBase\n    with WriteUtils {}\n\nclass DeltaIcebergCompatV3TransactionBuilderV2Suite extends DeltaIcebergCompatV3SuiteBase\n    with WriteUtilsWithV2Builders {}\n\n/** This suite tests reading or writing into Delta table that have `icebergCompatV3` enabled. */\ntrait DeltaIcebergCompatV3SuiteBase extends DeltaIcebergCompatBaseSuite {\n\n  override def icebergCompatVersion: String = \"icebergCompatV3\"\n\n  override def icebergCompatEnabledKey: String = TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey\n\n  override def expectedTableFeatures: Seq[TableFeature] = Seq(\n    TableFeatures.ICEBERG_COMPAT_V3_W_FEATURE,\n    TableFeatures.COLUMN_MAPPING_RW_FEATURE,\n    TableFeatures.ROW_TRACKING_W_FEATURE)\n\n  override def supportedDataColumnTypes: Seq[DataType] =\n    // TODO add VARIANT_TYPE once it is supported\n    (PRIMITIVE_TYPES.toList ++ NESTED_TYPES.toList) // ++ Seq(VariantType.VARIANT))\n\n  override def supportedPartitionColumnTypes: Seq[DataType] = PRIMITIVE_TYPES.toList\n\n  test(s\"enable $icebergCompatVersion on a new table - verify row tracking is enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(icebergCompatEnabledKey -> \"true\")\n      createEmptyTable(engine, tablePath, cmTestSchema(), tableProperties = tblProps)\n\n      val protocol = getProtocol(engine, tablePath)\n      expectedTableFeatures.foreach { feature =>\n        assert(protocol.supportsFeature(feature))\n      }\n\n      val metadata = getMetadata(engine, tablePath)\n      assert(metadata.getConfiguration.get(TableConfig.ROW_TRACKING_ENABLED.getKey) === \"true\")\n      assert(\n        metadata.getConfiguration.get(TableConfig.MATERIALIZED_ROW_ID_COLUMN_NAME.getKey).nonEmpty)\n      assert(metadata.getConfiguration.get(\n        TableConfig.MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME.getKey).nonEmpty)\n\n    }\n  }\n\n  test(s\"enable $icebergCompatVersion on a new table with deletion vectors\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(\n        TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\",\n        icebergCompatEnabledKey -> \"true\")\n      createEmptyTable(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n    }\n  }\n\n  test(\"can't enable icebergCompatV3 on a existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n      createEmptyTable(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n\n      checkError[KernelException](\n        \"Cannot enable delta.enableIcebergCompatV3 on an existing table\") {\n        val newTblProps = Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\")\n        updateTableMetadata(engine, tablePath, tableProperties = newTblProps)\n      }\n    }\n  }\n\n  test(\"can't disable icebergCompatV3 on a existing icebergCompatV3 enabled table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\")\n      createEmptyTable(engine, tablePath, schema = testSchema, tableProperties = tblProps)\n\n      checkError[KernelException](\n        \"Disabling delta.enableIcebergCompatV3 on an existing table is not allowed\") {\n        val newTblProps = Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"false\")\n        updateTableMetadata(engine, tablePath, tableProperties = newTblProps)\n      }\n    }\n  }\n\n  test(\"subsequent writes to icebergCompatV3 enabled tables doesn't update metadata\") {\n    // we want to make sure the [[IcebergCompatV3MetadataValidatorAndUpdater]] doesn't\n    // make unneeded metadata updates\n    withTempDirAndEngine { (tablePath, engine) =>\n      val tblProps = Map(\n        TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\")\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = tblProps)\n\n      val metadata = getMetadata(engine, tablePath)\n      val actualCMMode = metadata.getConfiguration.get(TableConfig.COLUMN_MAPPING_MODE.getKey)\n      assert(actualCMMode === \"name\")\n\n      appendData(engine, tablePath, data = Seq.empty) // version 1\n      appendData(engine, tablePath, data = Seq.empty) // version 2\n\n      val table = Table.forPath(engine, tablePath)\n      assert(getMetadataActionFromCommit(engine, table, version = 0).isDefined)\n      assert(getMetadataActionFromCommit(engine, table, version = 1).isEmpty)\n      assert(getMetadataActionFromCommit(engine, table, version = 2).isEmpty)\n\n      // make a metadata update and see it is reflected in the table\n      val newProps = Map(\"key\" -> \"value\")\n      updateTableMetadata(engine, tablePath, tableProperties = newProps) // version 3\n      val ver3Metadata: Row = getMetadataActionFromCommit(engine, table, version = 3)\n        .getOrElse(fail(\"Metadata action not found in version 3\"))\n\n      // TODO: ugly, find a better utilities\n      val result = VectorUtils.toJavaMap[String, String](\n        ver3Metadata.getMap(ver3Metadata.getSchema.indexOf(\"configuration\")))\n        .get(\"key\")\n      assert(result === \"value\")\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaLogActionUtilsE2ESuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.defaults.utils.TestUtils\nimport io.delta.kernel.exceptions.TableNotFoundException\nimport io.delta.kernel.internal.DeltaLogActionUtils.listDeltaLogFilesAsIter\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Test suite for end-to-end cases. See also the mocked unit tests in DeltaLogActionUtilsSuite. */\nclass DeltaLogActionUtilsE2ESuite extends AnyFunSuite with TestUtils {\n  test(\"listDeltaLogFiles: throws TableNotFoundException if _delta_log does not exist\") {\n    withTempDir { tableDir =>\n      intercept[TableNotFoundException] {\n        listDeltaLogFilesAsIter(\n          defaultEngine,\n          Set(FileNames.DeltaLogFileType.COMMIT, FileNames.DeltaLogFileType.CHECKPOINT).asJava,\n          new Path(tableDir.getAbsolutePath),\n          0,\n          Optional.empty(),\n          true /* mustBeRecreatable */\n        ).toInMemoryList\n      }\n    }\n  }\n\n  test(\"listDeltaLogFiles: returns empty list if _delta_log is empty\") {\n    withTempDir { tableDir =>\n      val logDir = new File(tableDir, \"_delta_log\")\n      assert(logDir.mkdirs() && logDir.isDirectory && logDir.listFiles().isEmpty)\n\n      val result = listDeltaLogFilesAsIter(\n        defaultEngine,\n        Set(FileNames.DeltaLogFileType.COMMIT, FileNames.DeltaLogFileType.CHECKPOINT).asJava,\n        new Path(tableDir.getAbsolutePath),\n        0,\n        Optional.empty(),\n        true /* mustBeRecreatable */\n      ).toInMemoryList\n\n      assert(result.isEmpty)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaReplaceTableColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.immutable.Seq\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.defaults.utils.{WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.util.{ColumnMapping, ColumnMappingSuiteBase}\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode\nimport io.delta.kernel.types.{ArrayType, DataType, FieldMetadata, IntegerType, LongType, MapType, StringType, StructField, StructType}\n\nclass DeltaReplaceTableColumnMappingNameModeTransactionBuilderV1Suite\n    extends DeltaReplaceTableColumnMappingNameModeSuite with WriteUtils\n\nclass DeltaReplaceTableColumnMappingNameModeTransactionBuilderV2Suite\n    extends DeltaReplaceTableColumnMappingNameModeSuite with WriteUtilsWithV2Builders\n\nclass DeltaReplaceTableColumnMappingIdModeTransactionBuilderV1Suite\n    extends DeltaReplaceTableColumnMappingIdModeSuite with WriteUtils\n\nclass DeltaReplaceTableColumnMappingIdModeTransactionBuilderV2Suite\n    extends DeltaReplaceTableColumnMappingIdModeSuite with WriteUtilsWithV2Builders\n\nabstract class DeltaReplaceTableColumnMappingNameModeSuite\n    extends DeltaReplaceTableColumnMappingSuiteBase {\n\n  override def tblPropertiesCmEnabled: Map[String, String] =\n    Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\")\n}\n\nabstract class DeltaReplaceTableColumnMappingIdModeSuite\n    extends DeltaReplaceTableColumnMappingSuiteBase {\n\n  override def tblPropertiesCmEnabled: Map[String, String] =\n    Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n\n  // We only need to run the below tests once since they check combos of id and name mode, put them\n  // in this suite for this reason\n  ColumnMapping.ColumnMappingMode.values().foreach { initialCmMode =>\n    ColumnMapping.ColumnMappingMode.values().foreach { replaceCmMode =>\n      if (initialCmMode != replaceCmMode) {\n        test(s\"Cannot change CM mode from $initialCmMode to $replaceCmMode\") {\n          withTempDirAndEngine { (tablePath, engine) =>\n            createInitialTable(\n              engine,\n              tablePath,\n              tableProperties = cmModeTblProperties(initialCmMode),\n              includeData = false)\n            assert(intercept[UnsupportedOperationException] {\n              commitReplaceTable(\n                engine,\n                tablePath,\n                tableProperties = cmModeTblProperties(replaceCmMode))\n            }.getMessage.contains(\n              s\"Changing column mapping mode from $initialCmMode to $replaceCmMode is not \" +\n                s\"currently supported in Kernel during REPLACE TABLE\"))\n          }\n        }\n      } else if (initialCmMode != ColumnMappingMode.NONE) {\n        test(s\"Replace with entirely new schema for cmMode=$initialCmMode assigns CM info\") {\n          withTempDirAndEngine { (tablePath, engine) =>\n            createInitialTable(\n              engine,\n              tablePath,\n              schema = new StructType().add(\"col1\", StringType.STRING),\n              tableProperties = cmModeTblProperties(initialCmMode),\n              includeData = false)\n            commitReplaceTable(\n              engine,\n              tablePath,\n              cmTestSchema(),\n              tableProperties = cmModeTblProperties(replaceCmMode))\n            verifyCMTestSchemaHasValidColumnMappingInfo(\n              getMetadata(engine, tablePath),\n              enableIcebergCompatV2 = false,\n              initialFieldId = 1)\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait DeltaReplaceTableColumnMappingSuiteBase extends DeltaReplaceTableSuiteBase\n    with ColumnMappingSuiteBase {\n\n  // Child suites override this to run tests with either id or name based column mapping\n  def tblPropertiesCmEnabled: Map[String, String]\n\n  /* ------ Test helpers ------- */\n\n  def cmModeTblProperties(mode: ColumnMappingMode): Map[String, String] = {\n    Map(\n      TableConfig.COLUMN_MAPPING_MODE.getKey -> mode.value)\n  }\n\n  implicit class StructFieldOps(field: StructField) {\n    def withCMMetadata(physicalName: String, fieldId: Long): StructField = {\n      field.withNewMetadata(\n        FieldMetadata.builder()\n          .fromMetadata(field.getMetadata)\n          .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, fieldId)\n          .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, physicalName)\n          .build())\n    }\n  }\n\n  def singletonSchema(colName: String, dataType: DataType): StructType = {\n    val topLevelCol = new StructField(colName, dataType, true)\n      .withCMMetadata(colName + \"-physicalName\", 4)\n    new StructType().add(topLevelCol)\n  }\n\n  def nestedStructSchema(nestedField: StructField): StructType = {\n    singletonSchema(\"top-struct\", new StructType().add(nestedField))\n  }\n\n  def nestedArraySchema(nestedField: StructField): StructType = {\n    singletonSchema(\n      \"array-col\",\n      new ArrayType(new StructType().add(nestedField), true))\n  }\n\n  def nestedMapKeySchema(nestedField: StructField): StructType = {\n    singletonSchema(\n      \"map-col\",\n      new MapType(new StructType().add(nestedField), StringType.STRING, true))\n  }\n\n  def nestedMapValueSchema(nestedField: StructField): StructType = {\n    singletonSchema(\n      \"map-col\",\n      new MapType(StringType.STRING, new StructType().add(nestedField), true))\n  }\n\n  implicit val exceptionType = ClassTag(classOf[KernelException])\n\n  def checkReplaceThrowsException[T <: Throwable](\n      initialSchema: StructType,\n      replaceSchema: StructType,\n      expectedErrorMessageContains: String)(implicit exceptionType: ClassTag[T]): Unit = {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        schema = initialSchema,\n        includeData = false,\n        tableProperties = tblPropertiesCmEnabled)\n      val e = intercept[T] {\n        commitReplaceTable(\n          engine,\n          tablePath,\n          schema = replaceSchema,\n          tableProperties = tblPropertiesCmEnabled)\n      }\n      assert(e.getMessage.contains(expectedErrorMessageContains))\n    }\n  }\n\n  def checkReplaceSucceeds(\n      initialSchema: StructType,\n      replaceSchema: StructType): Unit = {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        schema = initialSchema,\n        includeData = false,\n        tableProperties = tblPropertiesCmEnabled)\n      commitReplaceTable(\n        engine,\n        tablePath,\n        schema = replaceSchema,\n        tableProperties = tblPropertiesCmEnabled)\n      assert(getMetadata(engine, tablePath).getSchema == replaceSchema)\n    }\n  }\n\n  def testCompatibleFieldIdReuseDifferentNestings(\n      testDescription: String,\n      initialField: StructField,\n      replaceField: StructField): Unit = {\n    val initialFieldComplete = initialField.withCMMetadata(\"col-1\", 1)\n    val replaceFieldComplete = replaceField.withCMMetadata(\"col-1\", 1)\n\n    test(s\"$testDescription - top-level field\") {\n      val initialSchema = new StructType().add(initialFieldComplete)\n      val replaceSchema = new StructType().add(replaceFieldComplete)\n      checkReplaceSucceeds(initialSchema, replaceSchema)\n    }\n\n    test(s\"$testDescription - nested within a struct, with struct fieldIdReuse\") {\n      val initialSchema = nestedStructSchema(initialFieldComplete)\n      val replaceSchema = nestedStructSchema(replaceFieldComplete)\n      checkReplaceSucceeds(initialSchema, replaceSchema)\n    }\n\n    test(s\"$testDescription - nested within a struct in an array, with array fieldIdReuse\") {\n      val initialSchema = nestedArraySchema(initialFieldComplete)\n      val replaceSchema = nestedArraySchema(replaceFieldComplete)\n      checkReplaceSucceeds(initialSchema, replaceSchema)\n    }\n\n    test(s\"$testDescription - nested within a struct in an map (key), with array fieldIdReuse\") {\n      val initialSchema = nestedMapKeySchema(initialFieldComplete)\n      val replaceSchema = nestedMapKeySchema(replaceFieldComplete)\n      checkReplaceSucceeds(initialSchema, replaceSchema)\n    }\n\n    test(s\"$testDescription - nested within a struct in an map (value), with array fieldIdReuse\") {\n      val initialSchema = nestedMapValueSchema(initialFieldComplete)\n      val replaceSchema = nestedMapValueSchema(replaceFieldComplete)\n      checkReplaceSucceeds(initialSchema, replaceSchema)\n    }\n  }\n\n  def testIncompatibleFieldIdReuseDifferentNestings[T <: Throwable](\n      testDescription: String,\n      initialField: StructField,\n      replaceField: StructField,\n      expectedErrorMessageContains: String,\n      initialPhysicalName: String = \"col-1\",\n      replacePhysicalName: String = \"col-1\")(implicit exceptionType: ClassTag[T]): Unit = {\n    val initialFieldComplete = initialField.withCMMetadata(initialPhysicalName, 1)\n    val replaceFieldComplete = replaceField.withCMMetadata(replacePhysicalName, 1)\n\n    test(s\"$testDescription - top-level field\") {\n      val initialSchema = new StructType().add(initialFieldComplete)\n      val replaceSchema = new StructType().add(replaceFieldComplete)\n      checkReplaceThrowsException[T](\n        initialSchema,\n        replaceSchema,\n        expectedErrorMessageContains)\n    }\n\n    test(s\"$testDescription - nested within a struct, with struct fieldIdReuse\") {\n      val initialSchema = nestedStructSchema(initialFieldComplete)\n      val replaceSchema = nestedStructSchema(replaceFieldComplete)\n      checkReplaceThrowsException[T](\n        initialSchema,\n        replaceSchema,\n        expectedErrorMessageContains)\n    }\n\n    test(s\"$testDescription - nested within a struct in an array, with array fieldIdReuse\") {\n      val initialSchema = nestedArraySchema(initialFieldComplete)\n      val replaceSchema = nestedArraySchema(replaceFieldComplete)\n      checkReplaceThrowsException[T](\n        initialSchema,\n        replaceSchema,\n        expectedErrorMessageContains)\n    }\n\n    test(s\"$testDescription - nested within a struct in a map (key), with array fieldIdReuse\") {\n      val initialSchema = nestedMapKeySchema(initialFieldComplete)\n      val replaceSchema = nestedMapKeySchema(replaceFieldComplete)\n      checkReplaceThrowsException[T](\n        initialSchema,\n        replaceSchema,\n        expectedErrorMessageContains)\n    }\n\n    test(s\"$testDescription - nested within a struct in a map (value), with array fieldIdReuse\") {\n      val initialSchema = nestedMapKeySchema(initialFieldComplete)\n      val replaceSchema = nestedMapKeySchema(replaceFieldComplete)\n      checkReplaceThrowsException[T](\n        initialSchema,\n        replaceSchema,\n        expectedErrorMessageContains)\n    }\n  }\n\n  /* ---------------- TEST CASES ---------------- */\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible primitive type\",\n    new StructField(\"col1\", StringType.STRING, true),\n    new StructField(\"col1\", IntegerType.INTEGER, true),\n    \"Cannot change the type of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible primitive type w/logical name change\",\n    new StructField(\"col1\", StringType.STRING, true),\n    new StructField(\"col2\", IntegerType.INTEGER, true),\n    \"Cannot change the type of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible nullability\",\n    new StructField(\"col1\", StringType.STRING, true),\n    new StructField(\"col1\", StringType.STRING, false),\n    \"Cannot tighten the nullability of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible nullability for array-type field\",\n    new StructField(\"col1\", new ArrayType(StringType.STRING, true), true),\n    new StructField(\"col1\", new ArrayType(StringType.STRING, false), true),\n    \"Cannot tighten the nullability of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible nullability for map-type field\",\n    new StructField(\"col1\", new MapType(StringType.STRING, StringType.STRING, true), true),\n    new StructField(\"col1\", new MapType(StringType.STRING, StringType.STRING, false), true),\n    \"Cannot tighten the nullability of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible nullability for struct of struct\",\n    new StructField(\n      \"col1\",\n      new StructType()\n        .add(\"col2\", StringType.STRING, true),\n      true),\n    new StructField(\n      \"col1\",\n      new StructType()\n        .add(\"col2\", StringType.STRING, true),\n      false),\n    \"Cannot tighten the nullability of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible type for array-type field\",\n    new StructField(\"col1\", new ArrayType(StringType.STRING, true), true),\n    new StructField(\"col1\", new ArrayType(IntegerType.INTEGER, true), true),\n    \"Cannot change the type of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible key-type for map-type field\",\n    new StructField(\"col1\", new MapType(StringType.STRING, StringType.STRING, true), true),\n    new StructField(\"col1\", new MapType(IntegerType.INTEGER, StringType.STRING, true), true),\n    \"Cannot change the type of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible value-type for map-type field\",\n    new StructField(\"col1\", new MapType(StringType.STRING, StringType.STRING, true), true),\n    new StructField(\"col1\", new MapType(StringType.STRING, IntegerType.INTEGER, true), true),\n    \"Cannot change the type of existing field\")\n\n  testIncompatibleFieldIdReuseDifferentNestings(\n    \"Cannot reuse fieldId incompatible primitive type, type-widening supported change\",\n    new StructField(\"col1\", IntegerType.INTEGER, true),\n    new StructField(\"col1\", LongType.LONG, true),\n    \"Cannot change the type of existing field\")\n\n  test(\"Cannot add a new field with a fieldId <= maxColId\") {\n    val initialSchema = new StructType()\n      .add(new StructField(\"col1\", StringType.STRING, true).withCMMetadata(\"col-200\", 200))\n    val replaceSchema = new StructType()\n      .add(new StructField(\"col1\", StringType.STRING, true).withCMMetadata(\"col-200\", 200))\n      .add(new StructField(\"col2\", StringType.STRING, true).withCMMetadata(\"col-1\", 1))\n    checkReplaceThrowsException[IllegalArgumentException](\n      initialSchema,\n      replaceSchema,\n      \"Cannot add a new column with a fieldId <= maxFieldId\")\n  }\n\n  testIncompatibleFieldIdReuseDifferentNestings[IllegalArgumentException](\n    \"Cannot reuse fieldId with change in physical name\",\n    new StructField(\"col1\", StringType.STRING, true),\n    new StructField(\"col1\", StringType.STRING, true),\n    \"Existing field with id 1 in current schema has physical name \" +\n      \"col1-physical-name which is different\",\n    initialPhysicalName = \"col1-physical-name\",\n    replacePhysicalName = \"0001111023383922\")\n\n  val validNullabilityChanges = Seq(\n    (true, true),\n    (false, false),\n    (false, true))\n\n  validNullabilityChanges.foreach { case (initialNullable, replaceNullable) =>\n    testCompatibleFieldIdReuseDifferentNestings(\n      s\"Valid nullability + type change: initialNullable=$initialNullable to \" +\n        s\"replaceNullable$replaceNullable - primitive type\",\n      new StructField(\"col1\", StringType.STRING, initialNullable),\n      new StructField(\"col1\", StringType.STRING, replaceNullable))\n\n    testCompatibleFieldIdReuseDifferentNestings(\n      s\"Valid nullability + type change: initialNullable=$initialNullable to \" +\n        s\"replaceNullable$replaceNullable - array type\",\n      new StructField(\"col1\", new ArrayType(StringType.STRING, initialNullable), true),\n      new StructField(\"col1\", new ArrayType(StringType.STRING, replaceNullable), true))\n\n    testCompatibleFieldIdReuseDifferentNestings(\n      s\"Valid nullability + type change: initialNullable=$initialNullable to \" +\n        s\"replaceNullable$replaceNullable - map type\",\n      new StructField(\n        \"col1\",\n        new MapType(StringType.STRING, StringType.STRING, initialNullable),\n        true),\n      new StructField(\n        \"col1\",\n        new MapType(StringType.STRING, StringType.STRING, replaceNullable),\n        true))\n\n    testCompatibleFieldIdReuseDifferentNestings(\n      s\"Valid nullability + type change: initialNullable=$initialNullable to \" +\n        s\"replaceNullable$replaceNullable - struct of struct\",\n      new StructField(\n        \"col1\",\n        new StructType()\n          .add(new StructField(\"col2\", StringType.STRING, true)\n            // we must set the CM metadata for nested field so it doesn't update for replace\n            .withCMMetadata(\"col-200\", 200)),\n        initialNullable),\n      new StructField(\n        \"col1\",\n        new StructType()\n          .add(new StructField(\"col2\", StringType.STRING, true)\n            .withCMMetadata(\"col-200\", 200)),\n        replaceNullable))\n  }\n\n  testCompatibleFieldIdReuseDifferentNestings(\n    \"No type change with logical name change - primitive type\",\n    new StructField(\"col1\", StringType.STRING, true),\n    new StructField(\"col2\", StringType.STRING, true))\n\n  test(\"Can add a new non-nullable column - top level primitive\") {\n    val initialSchema = new StructType()\n      .add(new StructField(\"col1\", StringType.STRING, true)\n        .withCMMetadata(\"col-1\", 1))\n    val replaceSchema = new StructType()\n      .add(new StructField(\"col2\", StringType.STRING, false)\n        .withCMMetadata(\"col-2\", 2))\n    checkReplaceSucceeds(initialSchema, replaceSchema)\n  }\n\n  testCompatibleFieldIdReuseDifferentNestings(\n    \"Can add a new non-nullable column - nested within a struct\",\n    new StructField(\n      \"struct\",\n      new StructType()\n        .add(new StructField(\"col1\", StringType.STRING, true)\n          .withCMMetadata(\"col-100\", 100)),\n      true),\n    new StructField(\n      \"struct\",\n      new StructType()\n        .add(new StructField(\"col1\", StringType.STRING, true)\n          .withCMMetadata(\"col-100\", 100))\n        .add(new StructField(\"col2\", StringType.STRING, false)\n          .withCMMetadata(\"col-200\", 200)),\n      true))\n\n  test(\"Cannot provide just a colId without physicalName\") {\n    val initialSchema = new StructType()\n      .add(new StructField(\"col1\", StringType.STRING, true).withCMMetadata(\"col-0\", 0))\n    val replaceSchema = new StructType()\n      .add(\n        new StructField(\n          \"col1\",\n          StringType.STRING,\n          true,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 0)\n            .build()))\n    checkReplaceThrowsException[IllegalArgumentException](\n      initialSchema,\n      replaceSchema,\n      \"Both columnId and physicalName must be present if one is present\")\n  }\n\n  test(\"Assigns colId to new fields correctly based on previous maxFieldId with partial\" +\n    \" fieldId reuse\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val baseSchema = new StructType()\n        .add(new StructField(\n          \"col1\",\n          StringType.STRING,\n          true).withCMMetadata(\"col1-physical-name\", 0))\n      val initialSchema = baseSchema\n        .add(\"col2\", StringType.STRING, true)\n        .add(\"col3\", StringType.STRING, true)\n      createInitialTable(\n        engine,\n        tablePath,\n        schema = initialSchema,\n        tableProperties = tblPropertiesCmEnabled,\n        includeData = false)\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 2)\n      // Update the schema such that the only present fieldId is 0, but the max should still be 2\n      updateTableMetadata(\n        engine,\n        tablePath,\n        baseSchema)\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 2)\n      // Replace the table with a schema with some fieldId re-use, but also some new columns without\n      // a fieldId\n      val replaceSchema = baseSchema\n        .add(\"col2\", StringType.STRING, true)\n        .add(\"col4\", StringType.STRING, true)\n      commitReplaceTable(\n        engine,\n        tablePath,\n        schema = replaceSchema,\n        tableProperties = tblPropertiesCmEnabled)\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 4)\n      val resultSchema = getMetadata(engine, tablePath).getSchema\n      assert(ColumnMapping.getColumnId(resultSchema.get(\"col1\")) == 0)\n      assert(ColumnMapping.getColumnId(resultSchema.get(\"col2\")) == 3)\n      assert(ColumnMapping.getColumnId(resultSchema.get(\"col4\")) == 4)\n      assert(ColumnMapping.getPhysicalName(resultSchema.get(\"col1\")) == \"col1-physical-name\")\n      // These should have UUID physical names which start with \"col-\"\n      assert(ColumnMapping.getPhysicalName(resultSchema.get(\"col2\")).startsWith(\"col-\"))\n      assert(ColumnMapping.getPhysicalName(resultSchema.get(\"col4\")).startsWith(\"col-\"))\n    }\n  }\n\n  test(\"Replace correctly updates the maxFieldId when providing fieldId in the replace schema\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val initialSchema = new StructType()\n        .add(new StructField(\n          \"col1\",\n          StringType.STRING,\n          true).withCMMetadata(\"col1-physical-name\", 0))\n      createInitialTable(\n        engine,\n        tablePath,\n        schema = initialSchema,\n        tableProperties = tblPropertiesCmEnabled,\n        includeData = false)\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 0)\n      // Replace the table with a schema with new column with provided fieldId\n      val replaceSchema = initialSchema\n        .add(new StructField(\n          \"new-col\",\n          StringType.STRING,\n          true).withCMMetadata(\"col-200\", 200))\n      commitReplaceTable(\n        engine,\n        tablePath,\n        schema = replaceSchema,\n        tableProperties = tblPropertiesCmEnabled)\n      assert(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.fromMetadata(getMetadata(\n        engine,\n        tablePath)) == 200)\n    }\n  }\n\n  // E2E tests that we disallow fieldId reuse when fields are moved out of their prior parent.\n  // This validation is thoroughly unit tested in SchemaUtilsSuite.\n\n  test(\"Cannot reuse fieldId when moving field out of parent struct to top-level\") {\n    val initialSchema = new StructType()\n      .add(new StructField(\n        \"parent-struct\",\n        new StructType()\n          .add(new StructField(\"nested-col\", StringType.STRING, true)\n            .withCMMetadata(\"nested-col-physical\", 100)),\n        true).withCMMetadata(\"parent-struct-physical\", 1))\n    val replaceSchema = new StructType()\n      .add(new StructField(\"nested-col\", StringType.STRING, true)\n        .withCMMetadata(\"nested-col-physical\", 100))\n    checkReplaceThrowsException[KernelException](\n      initialSchema,\n      replaceSchema,\n      \"Cannot move fields between different levels of nesting\")\n  }\n\n  test(\"Cannot reuse fieldId when moving field from one parent struct to another\") {\n    // Initial: nested_struct (fieldId=0) with col1 (fieldId=1) inside\n    val initialSchema = new StructType()\n      .add(new StructField(\n        \"nested_struct\",\n        new StructType()\n          .add(new StructField(\"col1\", StringType.STRING, true)\n            .withCMMetadata(\"col1-physical\", 1)),\n        true).withCMMetadata(\"nested_struct-physical\", 0))\n    // Replace: nested_struct_new (fieldId=2) with col1 (fieldId=1) inside\n    val replaceSchema = new StructType()\n      .add(new StructField(\n        \"nested_struct_new\",\n        new StructType()\n          .add(new StructField(\"col1\", StringType.STRING, true)\n            .withCMMetadata(\"col1-physical\", 1)),\n        true).withCMMetadata(\"nested_struct_new-physical\", 2))\n    checkReplaceThrowsException[KernelException](\n      initialSchema,\n      replaceSchema,\n      \"Cannot move fields between different levels of nesting\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaReplaceTableSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Operation, Table, TableManager}\nimport io.delta.kernel.commit.{CommitMetadata, CommitResponse, Committer}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{TestRow, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{KernelException, MaxCommitRetryLimitReachedException, TableNotFoundException}\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.{SnapshotImpl, TableConfig, TableImpl}\nimport io.delta.kernel.internal.TableConfig._\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\nimport io.delta.kernel.utils.CloseableIterator\n\nclass DeltaReplaceTableTransactionBuilderV1Suite extends DeltaReplaceTableSuite with WriteUtils\n\nclass DeltaReplaceTableTransactionBuilderV2Suite extends DeltaReplaceTableSuite\n    with WriteUtilsWithV2Builders {\n\n  test(\"ReplaceTableTransactionBuilder uses the committer provided during snapshot building\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      class FakeCommitter extends Committer {\n        override def commit(\n            engine: Engine,\n            finalizedActions: CloseableIterator[Row],\n            commitMetadata: CommitMetadata): CommitResponse = {\n          throw new RuntimeException(\"This is a fake committer\")\n        }\n      }\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema)\n\n      // Build snapshot with committer and start txn\n      val txn = TableManager.loadSnapshot(tablePath)\n        .withCommitter(new FakeCommitter())\n        .build(engine).asInstanceOf[SnapshotImpl]\n        .buildReplaceTableTransaction(testSchema, \"test-engine\")\n        .build(engine)\n\n      // Check the txn returns the correct committer\n      assert(txn.getCommitter.isInstanceOf[FakeCommitter])\n      // Check that the txn invokes the provided committer upon commit\n      val e = intercept[RuntimeException] {\n        txn.commit(engine, emptyIterable())\n      }\n      assert(e.getMessage.contains(\"This is a fake committer\"))\n    }\n  }\n}\n\nabstract class DeltaReplaceTableSuite extends DeltaReplaceTableSuiteBase {\n\n  /* ----- ERROR CASES ------ */\n\n  test(\"Conflict resolution is disabled for replace table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      // Start replace transaction - use default maxRetries\n      val txn1 = getReplaceTxn(engine, tablePath, testSchema)\n      // Start replace transaction - explicitly set maxRetries > 0\n      val txn2 = getReplaceTxn(engine, tablePath, testSchema, maxRetries = 100)\n      // Commit a simple blind append as a conflicting txn\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> (dataBatches2)))\n      // Try to commit replace table and intercept conflicting txn (no conflict resolution)\n      intercept[MaxCommitRetryLimitReachedException] {\n        commitTransaction(txn1, engine, emptyIterable())\n      }\n      intercept[MaxCommitRetryLimitReachedException] {\n        commitTransaction(txn2, engine, emptyIterable())\n      }\n    }\n  }\n\n  test(\"Table::createTransactionBuilder does not allow REPLACE TABLE\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(intercept[UnsupportedOperationException] {\n        Table.forPath(engine, tablePath)\n          .createTransactionBuilder(engine, testEngineInfo, Operation.REPLACE_TABLE)\n          .build(engine)\n      }.getMessage.contains(\"REPLACE TABLE is not yet supported\"))\n    }\n  }\n\n  test(\"Cannot replace a table that does not exist\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      assert(\n        intercept[TableNotFoundException] {\n          // This is not possible on an API level for V2 builders since building is from a Snapshot\n          Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n            .createReplaceTableTransactionBuilder(engine, \"test-engine\")\n            .withSchema(engine, testSchema)\n            .build(engine)\n        }.getMessage.contains(\"Trying to replace a table that does not exist\"))\n    }\n  }\n\n  test(\"Cannot enable a feature that Kernel does not support\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(\n        intercept[KernelException] {\n          commitReplaceTable(\n            engine,\n            tablePath,\n            tableProperties = Map(TableConfig.CHANGE_DATA_FEED_ENABLED.getKey -> \"true\"))\n        }.getMessage.contains(\"Unsupported Delta writer feature\"))\n    }\n  }\n\n  test(\"Cannot replace a table with a protocol Kernel does not support\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      spark.sql(\n        s\"\"\"\n          |CREATE TABLE delta.`$tablePath` (id INT) USING DELTA\n          |TBLPROPERTIES('delta.enableChangeDataFeed' = 'true')\n          |\"\"\".stripMargin)\n      assert(\n        intercept[KernelException] {\n          commitReplaceTable(\n            engine,\n            tablePath)\n        }.getMessage.contains(\"Unsupported Delta writer feature\"))\n    }\n  }\n\n  test(\"Must provide a schema for replace table transaction\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(intercept[KernelException] {\n        Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n          .createReplaceTableTransactionBuilder(engine, \"test-engine\")\n          .build(engine)\n      }.getMessage.contains(\"Must provide a new schema for REPLACE TABLE\"))\n    }\n  }\n\n  test(\"Cannot define both partition and clustering columns at the same time\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(intercept[IllegalArgumentException] {\n        // Setting both is not possible on an API level for v2 builders\n        Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n          .createReplaceTableTransactionBuilder(engine, \"test-engine\")\n          .withSchema(engine, testPartitionSchema)\n          .withPartitionColumns(engine, testPartitionColumns.asJava)\n          .withClusteringColumns(engine, testClusteringColumns.asJava)\n          .build(engine)\n      }.getMessage.contains(\n        \"Partition Columns and Clustering Columns cannot be set at the same time\"))\n    }\n  }\n\n  test(\"Schema provided must be valid\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(intercept[KernelException] {\n        getReplaceTxn(\n          engine,\n          tablePath,\n          schema = new StructType().add(\"col\", IntegerType.INTEGER).add(\"col\", IntegerType.INTEGER))\n      }.getMessage.contains(\n        \"Schema contains duplicate columns\"))\n    }\n  }\n\n  test(\"Partition columns provided must be valid\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(intercept[IllegalArgumentException] {\n        getReplaceTxn(\n          engine,\n          tablePath,\n          schema = testSchema,\n          partCols = Seq(\"foo\"))\n      }.getMessage.contains(\n        \"Partition column foo not found in the schema\"))\n    }\n  }\n\n  test(\"Clustering columns provided must be valid\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(intercept[KernelException] {\n        getReplaceTxn(\n          engine,\n          tablePath,\n          schema = testSchema,\n          clusteringColsOpt = Some(Seq(new Column(\"foo\"))))\n      }.getMessage.contains(\n        \"Column 'column(`foo`)' was not found in the table schema\"))\n    }\n  }\n\n  test(\"icebergWriterCompatV1 checks are enforced\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(\n        intercept[KernelException] {\n          commitReplaceTable(\n            engine,\n            tablePath,\n            tableProperties = Map(\n              TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\",\n              TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\"))\n        }.getMessage.contains(\"The value 'name' for the property 'delta.columnMapping.mode' is \" +\n          \"not compatible with icebergWriterCompatV1 requirements\"))\n    }\n  }\n\n  test(\"icebergCompatV2 checks are enforced\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      assert(\n        intercept[KernelException] {\n          commitReplaceTable(\n            engine,\n            tablePath,\n            tableProperties = Map(\n              TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\",\n              TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\"))\n        }.getMessage.contains(\n          \"Table features [deletionVectors] are incompatible with icebergCompatV2\"))\n    }\n  }\n\n  test(\"REPLACE is not supported on existing table with icebergCompatV3 feature\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        tableProperties = Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\"),\n        includeData = false // To avoid writing data with correct CM schema\n      )\n      assert(\n        intercept[UnsupportedOperationException] {\n          commitReplaceTable(\n            engine,\n            tablePath,\n            tableProperties = Map(TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\"))\n        }.getMessage.contains(\"REPLACE TABLE is not yet supported on IcebergCompatV3 tables\"))\n    }\n  }\n\n  test(\"REPLACE is not supported when enabling icebergCompatV3 feature\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\"),\n        includeData = false // To avoid writing data with correct CM schema\n      )\n      assert(\n        intercept[UnsupportedOperationException] {\n          commitReplaceTable(\n            engine,\n            tablePath,\n            tableProperties = Map(\n              TableConfig.ICEBERG_COMPAT_V3_ENABLED.getKey -> \"true\",\n              TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\"))\n        }.getMessage.contains(\"REPLACE TABLE is not yet supported on IcebergCompatV3 tables\"))\n    }\n  }\n\n  /* ----------------- POSITIVE CASES ----------------- */\n\n  // TODO can we refactor other suites to run with both create + replace?\n\n  Seq(Seq(), Seq(Map.empty[String, Literal] -> (dataBatches1))).foreach { replaceData =>\n    test(s\"Basic case with no metadata changes, insertData=${replaceData.nonEmpty}\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        createInitialTable(engine, tablePath)\n        checkReplaceTable(engine, tablePath, data = replaceData)\n      }\n    }\n\n    test(s\"Basic case with initial empty table, insertData=${replaceData.nonEmpty}\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        createInitialTable(engine, tablePath)\n        checkReplaceTable(engine, tablePath, data = replaceData)\n      }\n    }\n  }\n\n  // Note, these tests cover things like transitioning between unpartitioned, partitioned, and\n  // clustered tables. This means it includes removing existing clustering domains when the initial\n  // table was clustered.\n  validSchemaDefs.foreach { case (initialSchemaDef, initialData) =>\n    validSchemaDefs.foreach { case (replaceSchemaDef, replaceData) =>\n      Seq(true, false).foreach { initialTableEmpty =>\n        Seq(true, false).foreach { insertDataInReplace =>\n          test(s\"Schema change from $initialSchemaDef to $replaceSchemaDef; \" +\n            s\"initialTableEmpty=$initialTableEmpty, insertDataInReplace=$insertDataInReplace\") {\n            withTempDirAndEngine { (tablePath, engine) =>\n              createInitialTable(\n                engine,\n                tablePath,\n                schema = initialSchemaDef.schema,\n                partitionColumns = initialSchemaDef.partitionColumns,\n                clusteringColumns = initialSchemaDef.clusteringColumns,\n                includeData = !initialTableEmpty,\n                data = initialData)\n              checkReplaceTable(\n                engine,\n                tablePath,\n                schema = replaceSchemaDef.schema,\n                partitionColumns = replaceSchemaDef.partitionColumns,\n                clusteringColumns = replaceSchemaDef.clusteringColumns,\n                data = if (insertDataInReplace) replaceData else Seq.empty)\n            }\n          }\n\n        }\n\n      }\n\n      test(s\"Schema change from $initialSchemaDef to $replaceSchemaDef\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          createInitialTable(\n            engine,\n            tablePath,\n            schema = initialSchemaDef.schema,\n            partitionColumns = initialSchemaDef.partitionColumns,\n            clusteringColumns = initialSchemaDef.clusteringColumns,\n            includeData = false)\n          checkReplaceTable(\n            engine,\n            tablePath,\n            schema = replaceSchemaDef.schema,\n            partitionColumns = replaceSchemaDef.partitionColumns,\n            clusteringColumns = replaceSchemaDef.clusteringColumns)\n        }\n      }\n    }\n  }\n\n  test(\"Case with DVs in the initial table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE delta.`$tablePath` (id INT) USING DELTA\n           |TBLPROPERTIES('delta.enableDeletionVectors' = 'true')\n           |\"\"\".stripMargin)\n      spark.sql(\n        s\"\"\"\n           |INSERT INTO delta.`$tablePath` VALUES (0), (1), (2), (3)\n           |\"\"\".stripMargin)\n      spark.sql(\n        s\"\"\"\n           |DELETE FROM delta.`$tablePath` WHERE id > 1\n           |\"\"\".stripMargin)\n      checkTable(tablePath, Seq(TestRow(0), TestRow(1)))\n      checkReplaceTable(engine, tablePath) // check it is empty after (also DVs no longer enabled)\n    }\n  }\n\n  test(\"Existing table properties are removed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        tableProperties = Map(\n          TableConfig.APPEND_ONLY_ENABLED.getKey -> \"true\",\n          \"user.facing.prop\" -> \"existing_prop\"))\n      checkReplaceTable(engine, tablePath)\n    }\n  }\n\n  test(\"New table features are correctly enabled\") {\n    // This also validates that withDomainMetadataSupported works\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      checkReplaceTable(\n        engine,\n        tablePath,\n        tableProperties = Map(\n          TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\"),\n        domainsToAdd = Seq((\"domain-name\", \"some-config\")),\n        expectedTableFeaturesSupported =\n          Seq(TableFeatures.DELETION_VECTORS_RW_FEATURE, TableFeatures.DOMAIN_METADATA_W_FEATURE))\n    }\n  }\n\n  test(\"Domain metadata are reset (user-facing)\") {\n    // (1) checks that we correctly override an existing domain with the new config if set in the\n    //     replace txn\n    // (2) checks we remove stale ones that are not set in the replace txn\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create initial table with 2 domains\n      val txn = getCreateTxn(engine, tablePath, testSchema, withDomainMetadataSupported = true)\n      txn.addDomainMetadata(\"domainToOverride\", \"check1\")\n      txn.addDomainMetadata(\"domainToRemove\", \"check2\")\n      commitTransaction(txn, engine, emptyIterable())\n\n      // Validate the 2 domains are present\n      val snapshot = Table.forPath(engine, tablePath).getLatestSnapshot(engine)\n      assert(snapshot.getDomainMetadata(\"domainToOverride\").toScala.contains(\"check1\"))\n      assert(snapshot.getDomainMetadata(\"domainToRemove\").toScala.contains(\"check2\"))\n\n      // Replace table and override 1/2 of the domains\n      checkReplaceTable(\n        engine,\n        tablePath,\n        domainsToAdd = Seq((\"domainToOverride\", \"overridden-config\")))\n    }\n  }\n\n  test(\"Column mapping maxFieldId is preserved during REPLACE TABLE \" +\n    \"- turning off column mapping mode\") {\n    // Note: DeltaReplaceTableColumnMappingSuite already tests that we preserve it correctly for the\n    // column mapping case\n    // TODO: once we support Id -> None mode during replace update this test\n    // We should preserve maxFieldId regardless of column mapping mode (if a future replace\n    // operation re-enables id mode we should not start our fieldIds from 0)\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\"),\n        includeData = false // To avoid writing data with correct CM schema\n      )\n      intercept[UnsupportedOperationException] {\n        checkReplaceTable(\n          engine,\n          tablePath,\n          expectedTableProperties =\n            Some(Map(TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID.getKey -> \"1\")))\n      }\n    }\n  }\n\n  test(\"icebergCompatV2 checks are executed and properties updated/auto-enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      // TODO once we support column mapping update this test\n      intercept[UnsupportedOperationException] {\n        checkReplaceTable(\n          engine,\n          tablePath,\n          tableProperties = Map(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"),\n          expectedTableProperties = Some(Map(\n            TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n            TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")),\n          expectedTableFeaturesSupported = Seq(\n            TableFeatures.ICEBERG_COMPAT_V2_W_FEATURE,\n            TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n      }\n    }\n  }\n\n  // TODO - can we reuse the tests in IcebergWriterCompatV1Suite to run with both create table and\n  //  replace table?\n  test(\"icebergWriterCompatV1 checks are executed and properties updated/auto-enabled\") {\n    // This also validates you can enable icebergWriterCompatV1 on an existing table during replace\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(engine, tablePath)\n      // TODO once we support column mapping update this test\n      intercept[UnsupportedOperationException] {\n        checkReplaceTable(\n          engine,\n          tablePath,\n          tableProperties = Map(TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\"),\n          expectedTableProperties = Some(Map(\n            TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\",\n            TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n            TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")),\n          expectedTableFeaturesSupported = Seq(\n            TableFeatures.ICEBERG_COMPAT_V2_W_FEATURE,\n            TableFeatures.ICEBERG_WRITER_COMPAT_V1,\n            TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n      }\n    }\n  }\n\n  test(\"When cmMode=None it is possible to have column with same name different type\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createInitialTable(\n        engine,\n        tablePath,\n        schema = new StructType()\n          .add(\"col1\", StringType.STRING),\n        includeData = false)\n      checkReplaceTable(\n        engine,\n        tablePath,\n        schema = new StructType()\n          .add(\"col1\", IntegerType.INTEGER))\n    }\n  }\n\n  test(\"REPLACE TABLE preserves ICT enablement tracking properties\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val snapshotV1 = createTableThenEnableIctAndVerify(engine, tablePath)\n      val ictEnablementTimestamp = snapshotV1.getTimestamp(engine)\n\n      checkReplaceTable(\n        engine,\n        tablePath,\n        expectedTableProperties = Some(Map(\n          IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\",\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey -> ictEnablementTimestamp.toString,\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey -> \"1\")))\n    }\n  }\n\n  test(\"REPLACE TABLE removes ICT enablement tracking properties when explicitly disabling ICT\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableThenEnableIctAndVerify(engine, tablePath)\n\n      checkReplaceTable(\n        engine,\n        tablePath,\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"false\"),\n        expectedTableProperties = Some(Map(\n          IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"false\")))\n    }\n  }\n\n  test(\"REPLACE TABLE can enable ICT\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n\n      checkReplaceTable(\n        engine,\n        tablePath,\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"),\n        expectedTableProperties = Some(Map(\n          IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\",\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey -> \"__check_exists__\",\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey -> \"1\")))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaReplaceTableSuiteBase.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.TransactionCommitResult\nimport io.delta.kernel.data.FilteredColumnarBatch\nimport io.delta.kernel.defaults.utils.AbstractWriteUtils\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.expressions.Literal.{ofInt, ofString}\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.clustering.ClusteringMetadataDomain\nimport io.delta.kernel.internal.tablefeatures.{TableFeature, TableFeatures}\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\ntrait DeltaReplaceTableSuiteBase extends AnyFunSuite with AbstractWriteUtils {\n\n  /* -------- Test values to use -------- */\n\n  case class SchemaDef(\n      schema: StructType,\n      partitionColumns: Seq[String] = null,\n      clusteringColumns: Option[List[Column]] = None) {\n    override def toString: String = {\n      s\"Schema=$schema, partCols=$partitionColumns, \" +\n        s\"clusteringColumns=${clusteringColumns.map(_.toString).getOrElse(List.empty)}\"\n    }\n  }\n\n  val schemaA = new StructType()\n    .add(\"col1\", IntegerType.INTEGER)\n    .add(\"col2\", IntegerType.INTEGER)\n\n  val schemaB = new StructType()\n    .add(\"col4\", StringType.STRING)\n    .add(\"col5\", StringType.STRING)\n\n  val unpartitionedSchemaDefA = SchemaDef(schemaA)\n  val unpartitionedSchemaDefB = SchemaDef(schemaB)\n  val unpartitionedSchemaDefA_dataBatches = generateData(schemaA, Seq.empty, Map.empty, 200, 3)\n  val unpartitionedSchemaDefB_dataBatches = generateData(schemaB, Seq.empty, Map.empty, 200, 3)\n\n  val partitionedSchemaDefA_1 = SchemaDef(schemaA, partitionColumns = Seq(\"col1\"))\n  val partitionedSchemaDefA_2 = SchemaDef(schemaA, partitionColumns = Seq(\"col2\"))\n  val partitionedSchemaDefA_1_dataBatches = generateData(\n    schemaA,\n    partitionedSchemaDefA_1.partitionColumns,\n    Map(\"col1\" -> ofInt(1)),\n    batchSize = 237,\n    numBatches = 3)\n  val partitionedSchemaDefA_2_dataBatches = generateData(\n    schemaA,\n    partitionedSchemaDefA_2.partitionColumns,\n    Map(\"col2\" -> ofInt(5)),\n    batchSize = 400,\n    numBatches = 1)\n\n  val partitionedSchemaDefB = SchemaDef(schemaB, partitionColumns = Seq(\"col4\"))\n  val partitionedSchemaDefB_dataBatches = generateData(\n    schemaB,\n    partitionedSchemaDefB.partitionColumns,\n    Map(\"col4\" -> ofString(\"foo\")),\n    batchSize = 100,\n    numBatches = 1)\n\n  val clusteredSchemaDefA_1 = SchemaDef(\n    schemaA,\n    clusteringColumns = Some(List(new Column(\"col1\"))))\n  val clusteredSchemaDefA_2 = SchemaDef(\n    schemaA,\n    clusteringColumns = Some(List(new Column(\"col2\"))))\n  val clusteredSchemaDefA_1_dataBatches = generateData(\n    schemaA,\n    partitionCols = Seq.empty,\n    partitionValues = Map.empty,\n    batchSize = 237,\n    numBatches = 3)\n  val clusteredSchemaDefA_2_dataBatches = generateData(\n    schemaA,\n    partitionCols = Seq.empty,\n    partitionValues = Map.empty,\n    batchSize = 100,\n    numBatches = 3)\n\n  val clusteredSchemaDefB = SchemaDef(\n    schemaB,\n    clusteringColumns = Some(List(new Column(\"col4\"))))\n  val clusteredSchemaDefB_dataBatches = generateData(\n    schemaB,\n    partitionCols = Seq.empty,\n    partitionValues = Map.empty,\n    batchSize = 2,\n    numBatches = 1)\n\n  val validSchemaDefs = Map(\n    unpartitionedSchemaDefA ->\n      Seq(Map.empty[String, Literal] -> unpartitionedSchemaDefA_dataBatches),\n    unpartitionedSchemaDefB ->\n      Seq(Map.empty[String, Literal] -> unpartitionedSchemaDefB_dataBatches),\n    partitionedSchemaDefA_1 ->\n      Seq(Map(\"col1\" -> ofInt(1)) -> partitionedSchemaDefA_1_dataBatches),\n    partitionedSchemaDefA_2 ->\n      Seq(Map(\"col2\" -> ofInt(5)) -> partitionedSchemaDefA_2_dataBatches),\n    partitionedSchemaDefB ->\n      Seq(Map(\"col4\" -> ofString(\"foo\")) -> partitionedSchemaDefB_dataBatches),\n    clusteredSchemaDefA_1 ->\n      Seq(Map.empty[String, Literal] -> clusteredSchemaDefA_1_dataBatches),\n    clusteredSchemaDefA_2 ->\n      Seq(Map.empty[String, Literal] -> clusteredSchemaDefA_2_dataBatches),\n    clusteredSchemaDefB ->\n      Seq(Map.empty[String, Literal] -> clusteredSchemaDefB_dataBatches))\n\n  /* -------- Test methods -------- */\n\n  protected def createInitialTable(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = testSchema,\n      partitionColumns: Seq[String] = null,\n      clusteringColumns: Option[List[Column]] = None,\n      tableProperties: Map[String, String] = null,\n      includeData: Boolean = true,\n      data: Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])] =\n        Seq(Map.empty[String, Literal] -> (dataBatches1))): Unit = {\n    val dataToWrite = if (includeData) {\n      data\n    } else {\n      Seq.empty\n    }\n\n    appendData(\n      engine,\n      tablePath,\n      isNewTable = true,\n      schema,\n      partCols = partitionColumns,\n      clusteringColsOpt = clusteringColumns,\n      tableProperties = tableProperties,\n      data = dataToWrite)\n    checkTable(tablePath, dataToWrite.flatMap(_._2).flatMap(_.toTestRows))\n  }\n\n  protected def commitReplaceTable(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = testSchema,\n      partitionColumns: Seq[String] = null,\n      clusteringColumns: Option[Seq[Column]] = None,\n      tableProperties: Map[String, String] = Map.empty,\n      domainsToAdd: Seq[(String, String)] = Seq.empty,\n      data: Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])] = Seq.empty)\n      : TransactionCommitResult = {\n\n    val txn = getReplaceTxn(\n      engine,\n      tablePath,\n      schema,\n      partitionColumns,\n      clusteringColumns,\n      tableProperties,\n      domainsToAdd.nonEmpty)\n    domainsToAdd.foreach { case (domainName, config) =>\n      txn.addDomainMetadata(domainName, config)\n    }\n\n    commitTransaction(txn, engine, getAppendActions(txn, data))\n  }\n\n  // scalastyle:off argcount\n  protected def checkReplaceTable(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = testSchema,\n      partitionColumns: Seq[String] = null,\n      clusteringColumns: Option[Seq[Column]] = None,\n      tableProperties: Map[String, String] = Map.empty,\n      domainsToAdd: Seq[(String, String)] = Seq.empty,\n      data: Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])] = Seq.empty,\n      expectedTableProperties: Option[Map[String, String]] = None,\n      expectedTableFeaturesSupported: Seq[TableFeature] = Seq.empty): Unit = {\n    // scalastyle:on argcount\n    val oldProtocol = getProtocol(engine, tablePath)\n    val wasClusteredTable = oldProtocol.supportsFeature(TableFeatures.CLUSTERING_W_FEATURE)\n\n    val commitResult = commitReplaceTable(\n      engine,\n      tablePath,\n      schema,\n      partitionColumns,\n      clusteringColumns,\n      tableProperties,\n      domainsToAdd,\n      data)\n    assertCommitResultHasClusteringCols(commitResult, clusteringColumns.getOrElse(Seq.empty))\n\n    verifyWrittenContent(\n      tablePath,\n      schema,\n      data.flatMap(_._2).flatMap(_.toTestRows))\n\n    val snapshot = latestSnapshot(tablePath).asInstanceOf[SnapshotImpl]\n\n    // Check partition columns\n    val expectedPartitionColumns = if (partitionColumns == null) Seq() else partitionColumns\n    assert(snapshot.getPartitionColumnNames.asScala == expectedPartitionColumns)\n\n    // Check clustering columns\n    clusteringColumns match {\n      case Some(clusteringCols) =>\n        // Check clustering table feature is supported\n        assertHasWriterFeature(snapshot, \"clustering\")\n        assertHasWriterFeature(snapshot, \"domainMetadata\")\n        // Validate clustering columns are correct\n        // TODO when we support column mapping we will need to convert to physical-name here\n        assert(snapshot.getPhysicalClusteringColumns.toScala\n          .exists(_.asScala == clusteringCols))\n      case None =>\n        if (wasClusteredTable) {\n          // If the table was previously clustered we expect the table feature to remain and for\n          // there to be a clustering domain metadata with clusteringColumns=[]\n          assertHasWriterFeature(snapshot, \"clustering\")\n          assert(snapshot.getPhysicalClusteringColumns.toScala\n            .exists(_.isEmpty))\n        } else {\n          // Otherwise there should be no table feature and no clustering domain metadata\n          assertHasNoWriterFeature(snapshot, \"clustering\")\n          assert(!ClusteringMetadataDomain.fromSnapshot(snapshot).isPresent)\n        }\n    }\n\n    // Check table properties\n    val actualProperties = snapshot.getMetadata.getConfiguration.asScala\n    val expectedProperties = expectedTableProperties.getOrElse(tableProperties)\n    expectedProperties.foreach { case (key, expectedValue) =>\n      if (expectedValue == \"__check_exists__\") {\n        assert(actualProperties.contains(key), s\"Expected property $key to exist\")\n      } else {\n        assert(\n          actualProperties.get(key).contains(expectedValue),\n          s\"Property $key: expected $expectedValue, got ${actualProperties.get(key)}\")\n      }\n    }\n\n    // Check other domain metadata\n    val nonClusteringActiveDomains = snapshot.getActiveDomainMetadataMap.asScala\n      .filter { case (domainName, _) =>\n        domainName != ClusteringMetadataDomain.DOMAIN_NAME\n      }.map { case (domainName, domainMetadata) => (domainName, domainMetadata.getConfiguration) }\n    assert(nonClusteringActiveDomains.toSet == domainsToAdd.toSet)\n\n    // Check protocol. In particular, check that we never downgrade the protocol\n    val newProtocol = getProtocol(engine, tablePath)\n    assert(oldProtocol.canUpgradeTo(newProtocol))\n    assert(expectedTableFeaturesSupported.forall(newProtocol.supportsFeature))\n\n    // Check CommitInfo.operation\n    val row = spark.sql(s\"DESCRIBE HISTORY delta.`$tablePath`\")\n      .filter(s\"version = ${snapshot.getVersion}\")\n      .select(\"operation\")\n      .collect().last\n    assert(row.getAs[String](\"operation\") == \"REPLACE TABLE\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableClusteringSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Table, Transaction, TransactionCommitResult}\nimport io.delta.kernel.Operation.{CREATE_TABLE, WRITE}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{KernelException, TableAlreadyExistsException}\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.expressions.Literal.ofInt\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.actions.DomainMetadata\nimport io.delta.kernel.internal.clustering.ClusteringMetadataDomain\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.internal.util.ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY\nimport io.delta.kernel.types.{MapType, StructType}\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.utils.CloseableIterable\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.clustering.{ClusteringMetadataDomain => SparkClusteringMetadataDomain}\n\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeltaTableClusteringTransactionBuilderV1Suite extends DeltaTableClusteringSuiteBase\n    with WriteUtils {\n\n  // It is not possible on an API level to set both clustering and partition columns in V2 builders\n  test(\"build table txn: \" +\n    \"clustering column and partition column cannot be set at same time\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val ex = intercept[IllegalArgumentException] {\n        getCreateTxn(\n          engine,\n          tablePath,\n          testPartitionSchema,\n          partCols = Seq(\"part1\"),\n          clusteringColsOpt = Some(List(new Column(\"PART1\"), new Column(\"part2\"))))\n      }\n      assert(\n        ex.getMessage\n          .contains(\"Partition Columns and Clustering Columns cannot be set at the same time\"))\n    }\n  }\n}\n\nclass DeltaTableClusteringTransactionBuilderV2Suite extends DeltaTableClusteringSuiteBase\n    with WriteUtilsWithV2Builders {}\n\ntrait DeltaTableClusteringSuiteBase extends AnyFunSuite with AbstractWriteUtils {\n\n  private val testingDomainMetadata = new DomainMetadata(\n    \"delta.clustering\",\n    \"\"\"{\"clusteringColumns\":[[\"part1\"],[\"part2\"]]}\"\"\",\n    false)\n\n  override def commitTransaction(\n      txn: Transaction,\n      engine: Engine,\n      dataActions: CloseableIterable[Row]): TransactionCommitResult = {\n    executeCrcSimple(txn.commit(engine, dataActions), engine)\n  }\n\n  private def verifyClusteringDMAndCRC(\n      snapshot: SnapshotImpl,\n      expectedDomainMetadata: DomainMetadata): Unit = {\n    verifyClusteringDomainMetadata(snapshot, expectedDomainMetadata)\n    // verifyChecksum will check the domain metadata in CRC against the latest snapshot.\n    verifyChecksum(snapshot.getDataPath.toString)\n  }\n\n  test(\"build table txn: clustering column should be part of the schema\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val ex = intercept[KernelException] {\n        getCreateTxn(\n          engine,\n          tablePath,\n          testPartitionSchema,\n          clusteringColsOpt = Some(List(new Column(\"PART1\"), new Column(\"part3\"))))\n      }\n      assert(ex.getMessage.contains(\"Column 'column(`part3`)' was not found in the table schema\"))\n    }\n  }\n\n  test(\"build table txn: clustering column should be data skipping supported data type\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val testPartitionSchema = new StructType()\n        .add(\"id\", INTEGER)\n        .add(\"part1\", INTEGER) // partition column\n        .add(\"mapType\", new MapType(INTEGER, INTEGER, false));\n      val ex = intercept[KernelException] {\n        getCreateTxn(\n          engine,\n          tablePath,\n          testPartitionSchema,\n          clusteringColsOpt = Some(List(new Column(\"mapType\"))))\n      }\n      assert(ex.getMessage.contains(\"Clustering is not supported because the following column(s)\"))\n    }\n  }\n\n  test(\"create a clustered table should succeed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val commitResult = createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns))\n\n      assertCommitResultHasClusteringCols(\n        commitResult,\n        expectedClusteringCols = testClusteringColumns)\n\n      val table = Table.forPath(engine, tablePath)\n      // Verify the clustering feature is included in the protocol\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertHasWriterFeature(snapshot, \"clustering\")\n\n      // Verify the clustering domain metadata is written\n      verifyClusteringDMAndCRC(snapshot, testingDomainMetadata)\n\n      // Use Spark to read the table's clustering metadata domain and verify the result\n      val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n      val clusteringMetadataDomainRead =\n        SparkClusteringMetadataDomain.fromSnapshot(deltaLog.snapshot)\n      assert(clusteringMetadataDomainRead.exists(_.clusteringColumns === Seq(\n        Seq(\"part1\"),\n        Seq(\"part2\"))))\n    }\n  }\n\n  test(\"clustering column should store as physical name with column mapping\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val commitResult = createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns),\n        tableProperties = Map(ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\"))\n\n      val table = Table.forPath(engine, tablePath)\n      // Verify the clustering feature is included in the protocol\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertHasWriterFeature(snapshot, \"clustering\")\n\n      // Verify the clustering domain metadata is written\n      val schema = table.getLatestSnapshot(engine).getSchema\n      val col1 = schema.get(\"part1\").getMetadata.getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n      val col2 = schema.get(\"part2\").getMetadata.getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n      val expectedDomainMetadata = new DomainMetadata(\n        \"delta.clustering\",\n        s\"\"\"{\"clusteringColumns\":[[\"$col1\"],[\"$col2\"]]}\"\"\",\n        false)\n      verifyClusteringDMAndCRC(snapshot, expectedDomainMetadata)\n\n      assertCommitResultHasClusteringCols(\n        commitResult,\n        expectedClusteringCols = Seq(new Column(col1), new Column(col2)))\n    }\n  }\n\n  test(\"create a clustered table should succeed with column case matches schema\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(List(new Column(\"pArT1\"), new Column(\"PaRt2\"))))\n\n      val table = Table.forPath(engine, tablePath)\n      // Verify the clustering feature is included in the protocol\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertHasWriterFeature(snapshot, \"clustering\")\n\n      // Verify the clustering domain metadata is written\n      verifyClusteringDMAndCRC(snapshot, testingDomainMetadata)\n    }\n  }\n\n  test(\"update a non-clustered table with clustering columns should succeed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testPartitionSchema)\n      val table = Table.forPath(engine, tablePath)\n      updateTableMetadata(engine, tablePath, clusteringColsOpt = Some(testClusteringColumns))\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertHasWriterFeature(snapshot, \"clustering\")\n      verifyClusteringDMAndCRC(snapshot, testingDomainMetadata)\n    }\n  }\n\n  test(\"update a clustered table with subset of previous clustering columns should succeed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns))\n      val table = Table.forPath(engine, tablePath)\n      updateTableMetadata(engine, tablePath, clusteringColsOpt = Some(List(new Column(\"part1\"))))\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertHasWriterFeature(snapshot, \"clustering\")\n      val expectedDomainMetadata = new DomainMetadata(\n        \"delta.clustering\",\n        \"\"\"{\"clusteringColumns\":[[\"part1\"]]}\"\"\",\n        false)\n      verifyClusteringDMAndCRC(snapshot, expectedDomainMetadata)\n    }\n  }\n\n  test(\"update a clustered table with a overlap clustering columns should succeed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns)\n      ) // Seq(\"part1\", \"part2\")\n      val table = Table.forPath(engine, tablePath)\n      updateTableMetadata(\n        engine,\n        tablePath,\n        clusteringColsOpt = Some(List(new Column(\"part2\"), new Column(\"id\"))))\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertHasWriterFeature(snapshot, \"clustering\")\n      val expectedDomainMetadata = new DomainMetadata(\n        \"delta.clustering\",\n        \"\"\"{\"clusteringColumns\":[[\"part2\"],[\"id\"]]}\"\"\",\n        false)\n      verifyClusteringDMAndCRC(snapshot, expectedDomainMetadata)\n    }\n  }\n\n  test(\"update a clustered table with a non-overlap clustering columns should succeed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(List(new Column(\"part1\"))))\n      val table = Table.forPath(engine, tablePath)\n      updateTableMetadata(engine, tablePath, clusteringColsOpt = Some(List(new Column(\"part2\"))))\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      val expectedDomainMetadata = new DomainMetadata(\n        \"delta.clustering\",\n        \"\"\"{\"clusteringColumns\":[[\"part2\"]]}\"\"\",\n        false)\n      assertHasWriterFeature(snapshot, \"clustering\")\n      verifyClusteringDMAndCRC(snapshot, expectedDomainMetadata)\n    }\n  }\n\n  test(\"update a clustered table with empty clustering columns should succeed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val commitResult0 = createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns))\n      assertCommitResultHasClusteringCols(\n        commitResult0,\n        expectedClusteringCols = testClusteringColumns)\n\n      val table = Table.forPath(engine, tablePath)\n      val commitResult1 = updateTableMetadata(engine, tablePath, clusteringColsOpt = Some(List()))\n      assertCommitResultHasClusteringCols(commitResult1, expectedClusteringCols = Seq.empty)\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      val expectedDomainMetadata = new DomainMetadata(\n        \"delta.clustering\",\n        \"\"\"{\"clusteringColumns\":[]}\"\"\",\n        false)\n      assertHasWriterFeature(snapshot, \"clustering\")\n      verifyClusteringDMAndCRC(snapshot, expectedDomainMetadata)\n    }\n  }\n\n  test(\"update a table with clustering columns doesn't exist in the table should fail\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testPartitionSchema)\n      val ex = intercept[KernelException] {\n        updateTableMetadata(\n          engine,\n          tablePath,\n          clusteringColsOpt = Some(List(new Column(\"non-exist\"))))\n      }\n      assert(\n        ex.getMessage.contains(\"Column 'column(`non-exist`)' was not found in the table schema\"))\n    }\n  }\n\n  test(\"update a partitioned table with clustering columns should fail\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testPartitionSchema, partCols = testPartitionColumns)\n      // test case 1: update with non-empty clustering columns\n      val ex1 = intercept[KernelException] {\n        updateTableMetadata(\n          engine,\n          tablePath,\n          clusteringColsOpt = Some(List(new Column(\"non-exist\"))))\n      }\n      assert(\n        ex1.getMessage.contains(\"Cannot enable clustering on a partitioned table\"))\n\n      // test case 2: update with empty clustering columns,\n      // this would still be regarded as enabling clustering\n      val ex2 = intercept[KernelException] {\n        updateTableMetadata(\n          engine,\n          tablePath,\n          clusteringColsOpt = Some(List()))\n      }\n      assert(\n        ex2.getMessage.contains(\"Cannot enable clustering on a partitioned table\"))\n    }\n  }\n\n  test(\"insert into clustered table - table create from scratch\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val testData = Seq(Map.empty[String, Literal] -> dataClusteringBatches1)\n\n      val commitResult = appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns),\n        data = testData)\n\n      verifyCommitResult(commitResult, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tablePath, version = 0)\n      verifyWrittenContent(\n        tablePath,\n        testPartitionSchema,\n        dataClusteringBatches1.flatMap(_.toTestRows))\n\n      val table = Table.forPath(engine, tablePath)\n      verifyClusteringDMAndCRC(\n        table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl],\n        testingDomainMetadata)\n    }\n  }\n\n  test(\"insert into clustered table - already existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n\n      {\n        val commitResult0 = appendData(\n          engine,\n          tablePath,\n          isNewTable = true,\n          testPartitionSchema,\n          clusteringColsOpt = Some(testClusteringColumns),\n          data = Seq(Map.empty[String, Literal] -> dataClusteringBatches1))\n        assertCommitResultHasClusteringCols(\n          commitResult0,\n          expectedClusteringCols = testClusteringColumns)\n\n        val expData = dataClusteringBatches1.flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tablePath, version = 0)\n        verifyWrittenContent(tablePath, testPartitionSchema, expData)\n        verifyClusteringDMAndCRC(\n          table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl],\n          testingDomainMetadata)\n      }\n      {\n        val commitResult1 = appendData(\n          engine,\n          tablePath,\n          data = Seq(Map.empty[String, Literal] -> dataClusteringBatches2))\n        assertCommitResultHasClusteringCols(\n          commitResult1,\n          expectedClusteringCols = testClusteringColumns)\n\n        val expData = dataClusteringBatches1.flatMap(_.toTestRows) ++\n          dataClusteringBatches2.flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tablePath, version = 1, partitionCols = null)\n        verifyWrittenContent(tablePath, testPartitionSchema, expData)\n        verifyClusteringDMAndCRC(\n          table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl],\n          testingDomainMetadata)\n      }\n    }\n  }\n\n  test(\"insert into clustered table after update clusteringColumns should still work\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val expectedDomainMetadataAfterUpdate = new DomainMetadata(\n        \"delta.clustering\",\n        \"\"\"{\"clusteringColumns\":[[\"id\"],[\"part1\"]]}\"\"\",\n        false)\n\n      val newClusteringCols = List(new Column(\"id\"), new Column(\"part1\")) // will be updated in v1\n\n      {\n        val commitResult0 = appendData(\n          engine,\n          tablePath,\n          isNewTable = true,\n          testPartitionSchema,\n          clusteringColsOpt = Some(testClusteringColumns),\n          data = Seq(Map.empty[String, Literal] -> dataClusteringBatches1))\n        assertCommitResultHasClusteringCols(\n          commitResult0,\n          expectedClusteringCols = testClusteringColumns)\n\n        val expData = dataClusteringBatches1.flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tablePath, version = 0)\n        verifyWrittenContent(tablePath, testPartitionSchema, expData)\n        verifyClusteringDMAndCRC(\n          table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl],\n          testingDomainMetadata)\n      }\n      {\n        val commitResult1 = updateTableMetadata(\n          engine,\n          tablePath,\n          clusteringColsOpt = Some(newClusteringCols))\n        assertCommitResultHasClusteringCols(\n          commitResult1,\n          expectedClusteringCols = newClusteringCols)\n\n        verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n        verifyClusteringDMAndCRC(\n          table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl],\n          expectedDomainMetadataAfterUpdate)\n      }\n      {\n        val commitResult2 = appendData(\n          engine,\n          tablePath,\n          data = Seq(Map.empty[String, Literal] -> dataClusteringBatches2))\n        assertCommitResultHasClusteringCols(\n          commitResult2,\n          expectedClusteringCols = newClusteringCols)\n\n        val expData = dataClusteringBatches1.flatMap(_.toTestRows) ++\n          dataClusteringBatches2.flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult2, expVersion = 2, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tablePath, version = 2, partitionCols = null)\n        verifyWrittenContent(tablePath, testPartitionSchema, expData)\n        verifyClusteringDMAndCRC(\n          table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl],\n          expectedDomainMetadataAfterUpdate)\n      }\n    }\n  }\n\n  test(\"can convert physical clustering columns to logical on column-mapping-enabled table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      val tableProperties = Map(ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\")\n      val clusteringColumns = List(new Column(\"part1\"), new Column(\"part2\"))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        tableProperties = tableProperties,\n        clusteringColsOpt = Some(clusteringColumns))\n\n      // ===== WHEN =====\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      val physicalClusteringColumns = snapshot.getPhysicalClusteringColumns.get().asScala\n\n      // ===== THEN =====\n      assert(physicalClusteringColumns.size == 2)\n      physicalClusteringColumns.foreach { c => assert(c.getNames()(0).startsWith(\"col-\")) }\n\n      val schema = snapshot.getSchema\n      physicalClusteringColumns.zipWithIndex.foreach { case (physicalColumn, idx) =>\n        val logicalColumn = ColumnMapping.getLogicalColumnNameAndDataType(schema, physicalColumn)._1\n        val expectedLogicalName = if (idx == 0) \"part1\" else \"part2\"\n\n        assert(logicalColumn.getNames.length == 1)\n        assert(logicalColumn.getNames()(0) == expectedLogicalName)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableFeaturesSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.nio.file.{Files, Paths}\nimport java.util.Collections\n\nimport scala.collection.immutable.Seq\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.{Operation, Table}\nimport io.delta.kernel.Operation.CREATE_TABLE\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{InvalidConfigurationValueException, KernelException}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.{SnapshotImpl, TableConfig}\nimport io.delta.kernel.internal.TableConfig.UniversalFormats\nimport io.delta.kernel.internal.actions.{Protocol => KernelProtocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.types.{StructType, TimestampNTZType}\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.apache.spark.sql.delta.{DeltaLog, DeltaTableFeatureException}\nimport org.apache.spark.sql.delta.actions.Protocol\n\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.hadoop.ParquetFileReader\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeltaTableFeaturesTransactionBuilderV1Suite extends DeltaTableFeaturesSuiteBase\n    with WriteUtils {}\n\nclass DeltaTableFeaturesTransactionBuilderV2Suite extends DeltaTableFeaturesSuiteBase\n    with WriteUtilsWithV2Builders {}\n\n/**\n * Integration test suite for Delta table features.\n */\ntrait DeltaTableFeaturesSuiteBase extends AnyFunSuite with AbstractWriteUtils {\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Tests for deletionVector, v2Checkpoint table features\n  ///////////////////////////////////////////////////////////////////////////\n  Seq(\n    // Test format: feature (readerWriter type), table property to enable the feature\n    // For each feature, we test the following scenarios:\n    // 1. able to write to an existing Delta table with the feature supported\n    // 2. create a table with the feature supported and append data\n    // 3. update an existing table with the feature supported\n    (\"deletionVectors\", \"delta.enableDeletionVectors\", \"true\"),\n    (\"v2Checkpoint\", \"delta.checkpointPolicy\", \"v2\")).foreach {\n    case (feature, tblProp, propValue) =>\n      test(s\"able to write to an existing Delta table with $feature supported\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          // Create a table with the feature supported\n          spark.sql(s\"CREATE TABLE delta.`$tablePath` (id INTEGER) USING delta \" +\n            s\"TBLPROPERTIES ('$tblProp' = '$propValue')\")\n\n          checkReaderWriterFeaturesSupported(tablePath, feature)\n\n          // Write data to the table using Kernel\n          val testData = Seq(Map.empty[String, Literal] -> dataBatches1)\n          appendData(\n            engine,\n            tablePath,\n            data = testData)\n\n          // Check the data using Kernel and Delta-Spark readers\n          verifyWrittenContent(tablePath, testSchema, dataBatches1.flatMap(_.toTestRows))\n        }\n      }\n\n      test(s\"create a table with $feature supported\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          val testData = Seq(Map.empty[String, Literal] -> dataBatches1)\n\n          // create a table with the feature supported and append testData\n          appendData(\n            engine,\n            tablePath,\n            isNewTable = true,\n            testSchema,\n            data = testData,\n            tableProperties = Map(tblProp -> propValue))\n\n          checkReaderWriterFeaturesSupported(tablePath, feature)\n\n          // insert more data\n          appendData(\n            engine,\n            tablePath,\n            data = testData)\n\n          // Check the data using Kernel and Delta-Spark readers\n          verifyWrittenContent(\n            tablePath,\n            testSchema,\n            dataBatches1.flatMap(_.toTestRows) ++ dataBatches1.flatMap(_.toTestRows))\n        }\n      }\n\n      test(s\"update an existing table with $feature support\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          val testData = Seq(Map.empty[String, Literal] -> dataBatches1)\n\n          // create a table without the table feature supported\n          appendData(\n            engine,\n            tablePath,\n            isNewTable = true,\n            testSchema,\n            data = testData)\n\n          checkNoReaderWriterFeaturesSupported(tablePath, feature)\n\n          // insert more data and enable the feature\n          appendData(\n            engine,\n            tablePath,\n            data = testData,\n            tableProperties = Map(tblProp -> propValue))\n\n          checkReaderWriterFeaturesSupported(tablePath, feature)\n\n          // Check the data using Kernel and Delta-Spark readers\n          verifyWrittenContent(\n            tablePath,\n            testSchema,\n            dataBatches1.flatMap(_.toTestRows) ++ dataBatches1.flatMap(_.toTestRows))\n        }\n      }\n  }\n\n  // Test format: isTimestampNtzEnabled, expected protocol.\n  Seq(\n    (true, new KernelProtocol(3, 7, Set(\"timestampNtz\").asJava, Set(\"timestampNtz\").asJava)),\n    (false, new KernelProtocol(1, 2, Collections.emptySet(), Collections.emptySet())))\n    .foreach({\n      case (isTimestampNtzEnabled, expectedProtocol) =>\n        test(s\"Create table with timestampNtz enabled: $isTimestampNtzEnabled\") {\n          withTempDirAndEngine { (tablePath, engine) =>\n            val schema = if (isTimestampNtzEnabled) {\n              new StructType().add(\"tz\", TimestampNTZType.TIMESTAMP_NTZ)\n            } else {\n              new StructType().add(\"id\", INTEGER)\n            }\n            val txn = getCreateTxn(engine, tablePath, schema)\n\n            assert(txn.getSchema(engine) === schema)\n            assert(txn.getPartitionColumns(engine).isEmpty)\n            val txnResult = commitTransaction(txn, engine, emptyIterable())\n\n            assert(txnResult.getVersion === 0)\n            val protocolRow = getProtocolActionFromCommit(engine, tablePath, 0)\n            assert(protocolRow.isDefined)\n            val protocol = KernelProtocol.fromRow(protocolRow.get)\n            assert(protocol.getMinReaderVersion === expectedProtocol.getMinReaderVersion)\n            assert(protocol.getMinWriterVersion === expectedProtocol.getMinWriterVersion)\n            assert(protocol.getReaderFeatures.containsAll(expectedProtocol.getReaderFeatures))\n            assert(protocol.getWriterFeatures.containsAll(expectedProtocol.getWriterFeatures))\n          }\n        }\n    })\n\n  test(\"schema evolution from Spark to add TIMESTAMP_NTZ type on a table created with kernel\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n      val txnResult = commitTransaction(txn, engine, emptyIterable())\n\n      assert(txnResult.getVersion === 0)\n      assertThrows[DeltaTableFeatureException] {\n        spark.sql(\"ALTER TABLE delta.`\" + tablePath + \"` ADD COLUMN newCol TIMESTAMP_NTZ\")\n      }\n      spark.sql(\"ALTER TABLE delta.`\" + tablePath +\n        \"` SET TBLPROPERTIES ('delta.feature.timestampNtz' = 'supported')\")\n      spark.sql(\"ALTER TABLE delta.`\" + tablePath + \"` ADD COLUMN newCol TIMESTAMP_NTZ\")\n    }\n  }\n\n  test(\"feature can be enabled via delta.feature prefix\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val domainMetadataKey = (\n        TableFeatures.SET_TABLE_FEATURE_SUPPORTED_PREFIX\n          + TableFeatures.DOMAIN_METADATA_W_FEATURE.featureName)\n      val properties = Map(\n        \"delta.feature.vacuumProtocolCheck\" -> \"supported\",\n        domainMetadataKey -> \"supported\")\n\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = properties)\n\n      val table = Table.forPath(engine, tablePath)\n      val writtenSnapshot = latestSnapshot(table, engine)\n      assert(writtenSnapshot.getMetadata.getConfiguration.isEmpty)\n      assert(writtenSnapshot.getProtocol.getExplicitlySupportedFeatures.containsAll(Set(\n        TableFeatures.VACUUM_PROTOCOL_CHECK_RW_FEATURE,\n        TableFeatures.DOMAIN_METADATA_W_FEATURE).asJava))\n    }\n  }\n\n  test(\"withDomainMetadata adds corresponding feature option\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn = getCreateTxn(engine, tablePath, testSchema, withDomainMetadataSupported = true)\n      commitTransaction(txn, engine, emptyIterable())\n      assert(latestSnapshot(table, engine).getProtocol.getExplicitlySupportedFeatures.contains(\n        TableFeatures.DOMAIN_METADATA_W_FEATURE))\n    }\n  }\n\n  test(\"delta.feature prefixed keys are removed even if property is already present on protocol\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val properties = Map(\"delta.feature.vacuumProtocolCheck\" -> \"supported\")\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = properties)\n      val table = Table.forPath(engine, tablePath)\n      assert(latestSnapshot(table, engine).getMetadata.getConfiguration.isEmpty)\n\n      // Update table with the same feature override set.\n      val updateTxn = getUpdateTxn(engine, tablePath, tableProperties = properties)\n\n      commitTransaction(updateTxn, engine, emptyIterable())\n\n      assert(latestSnapshot(table, engine).getMetadata.getConfiguration.isEmpty)\n    }\n  }\n\n  test(\"delta.feature override populate dependent features\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val properties = Map(\"delta.feature.clustering\" -> \"supported\")\n\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = properties)\n\n      val table = Table.forPath(engine, tablePath)\n      val writtenSnapshot = latestSnapshot(table, engine)\n      assert(\n        writtenSnapshot.getProtocol.getExplicitlySupportedFeatures.containsAll(Set(\n          TableFeatures.CLUSTERING_W_FEATURE,\n          TableFeatures.DOMAIN_METADATA_W_FEATURE).asJava),\n        s\"${writtenSnapshot.getProtocol.getExplicitlySupportedFeatures}\")\n    }\n  }\n\n  test(\"delta.feature override and TableConfig populate necessary features\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val properties =\n        Map(\"delta.feature.clustering\" -> \"supported\", \"delta.enableDeletionVectors\" -> \"true\")\n\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = properties)\n\n      val table = Table.forPath(engine, tablePath)\n      val writtenSnapshot = latestSnapshot(table, engine)\n      assert(\n        writtenSnapshot.getProtocol.getExplicitlySupportedFeatures.containsAll(Set(\n          TableFeatures.CLUSTERING_W_FEATURE,\n          TableFeatures.DOMAIN_METADATA_W_FEATURE,\n          TableFeatures.DELETION_VECTORS_RW_FEATURE).asJava),\n        s\"${writtenSnapshot.getProtocol.getExplicitlySupportedFeatures}\")\n      assert(writtenSnapshot.getMetadata.getConfiguration == Map(\n        \"delta.enableDeletionVectors\" -> \"true\").asJava)\n    }\n  }\n\n  test(\"UNIVERSAL_FORMAT feature can be populated\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val properties =\n        Map(\n          TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"iceberg\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")\n      createEmptyTable(engine, tablePath, testSchema, tableProperties = properties)\n\n      val table = Table.forPath(engine, tablePath)\n      val writtenSnapshot = latestSnapshot(table, engine)\n      assert(TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.fromMetadata(\n        writtenSnapshot.getMetadata).contains(UniversalFormats.FORMAT_ICEBERG))\n    }\n  }\n\n  test(\"UNIVERSAL_FORMAT feature will throw if icebergCompatV2 was not enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n\n      intercept[InvalidConfigurationValueException] {\n        getUpdateTxn(\n          engine,\n          tablePath,\n          tableProperties = Map(TableConfig.UNIVERSAL_FORMAT_ENABLED_FORMATS.getKey -> \"iceberg\"))\n      }\n    }\n  }\n\n  test(\"read throws if the table contains unsupported table feature\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = false,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      checkTable(tablePath, expectedAnswer = dataBatches1.flatMap(_.toTestRows))\n\n      // If test is running in intelliJ, set DELTA_TESTING=1 in env variables.\n      // This will enable the testReaderWriter feature in delta-spark. In CI jobs,\n      // build.sbt already has set and effective.\n      spark.sql(\"ALTER TABLE delta.`\" + tablePath +\n        \"` SET TBLPROPERTIES ('delta.feature.testReaderWriter' = 'supported')\")\n\n      // try to read the table\n      val ex = intercept[KernelException] {\n        checkTable(\n          tablePath,\n          expectedAnswer = Seq.empty /* it doesn't matter as expect failure in reading */ )\n      }\n      assert(ex.getMessage.contains(\n        \"feature \\\"testReaderWriter\\\" which is unsupported by this version of Delta Kernel\"))\n    }\n  }\n\n  test(\"read succeeds with unrecognized writer-only feature\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n      // Add an unknown writer feature to the protocol\n      // When DELTA_TESTING=1 (set in build.sbt) this test writer feature is allowed\n      spark.sql(\n        \"ALTER TABLE delta.`\" + tablePath +\n          \"` SET TBLPROPERTIES ('delta.feature.testWriter' = 'supported')\")\n\n      // Read should succeed - writer-only features don't affect readers\n      getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assert(getProtocol(engine, tablePath).getWriterFeatures().contains(\"testWriter\"))\n    }\n  }\n\n  /* ---- Start: type widening tests ---- */\n  test(\"only typeWidening feature is enabled when metadata supports it: new table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath = tablePath,\n        schema = testSchema,\n        tableProperties = Map(\"delta.enableTypeWidening\" -> \"true\"))\n\n      val protocolV0 = getProtocol(engine, tablePath)\n      assert(!protocolV0.supportsFeature(TableFeatures.TYPE_WIDENING_RW_PREVIEW_FEATURE))\n      assert(protocolV0.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n\n      // try enabling type widening again and expect no change in protocol\n      updateTableMetadata(\n        engine = engine,\n        tablePath = tablePath,\n        tableProperties = Map(\"delta.enableTypeWidening\" -> \"true\"))\n      val protocolV1 = getProtocol(engine, tablePath)\n      assert(protocolV1 === protocolV0)\n    }\n  }\n\n  test(\"only typeWidening feature is enabled when new metadata supports it: existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath = tablePath, schema = testSchema)\n      val protocolV0 = getProtocol(engine, tablePath)\n      assert(!protocolV0.supportsFeature(TableFeatures.TYPE_WIDENING_RW_PREVIEW_FEATURE))\n      assert(!protocolV0.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n\n      // try enabling type widening  and expect change in protocol\n      updateTableMetadata(\n        engine = engine,\n        tablePath = tablePath,\n        tableProperties = Map(\"delta.enableTypeWidening\" -> \"true\"))\n      val protocolV1 = getProtocol(engine, tablePath)\n      assert(!protocolV1.supportsFeature(TableFeatures.TYPE_WIDENING_RW_PREVIEW_FEATURE))\n      assert(protocolV1.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n    }\n  }\n\n  test(\"typeWidening-preview in existing table is respected\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      spark.sql(s\"CREATE TABLE delta.`$tablePath`(id INT) USING delta \" +\n        s\"TBLPROPERTIES ('delta.feature.typeWidening-preview' = 'supported')\")\n\n      val protocolV0 = getProtocol(engine, tablePath)\n      require(protocolV0.supportsFeature(TableFeatures.TYPE_WIDENING_RW_PREVIEW_FEATURE))\n      require(!protocolV0.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n\n      // now through Kernel type enabling the type widening through table property\n      updateTableMetadata(\n        engine = engine,\n        tablePath = tablePath,\n        tableProperties = Map(\"delta.enableTypeWidening\" -> \"true\"))\n      val protocolV1 = getProtocol(engine, tablePath)\n      assert(protocolV1.supportsFeature(TableFeatures.TYPE_WIDENING_RW_PREVIEW_FEATURE))\n      assert(!protocolV1.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n    }\n  }\n  /* ---- End: type widening tests ---- */\n\n  /* ---- Start: variant shredding tests ---- */\n  test(\"only variantShredding feature is enabled when metadata supports it: new table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath = tablePath,\n        schema = testSchema,\n        tableProperties = Map(\"delta.enableVariantShredding\" -> \"true\"))\n\n      val protocolV0 = getProtocol(engine, tablePath)\n      assert(!protocolV0.supportsFeature(TableFeatures.VARIANT_SHREDDING_PREVIEW_RW_FEATURE))\n      assert(protocolV0.supportsFeature(TableFeatures.VARIANT_SHREDDING_RW_FEATURE))\n\n      updateTableMetadata(\n        engine = engine,\n        tablePath = tablePath,\n        tableProperties = Map(\"delta.enableVariantShredding\" -> \"true\"))\n      val protocolV1 = getProtocol(engine, tablePath)\n      assert(protocolV1 === protocolV0)\n    }\n  }\n\n  test(\"only variantShredding feature is enabled when new metadata supports it: existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath = tablePath, schema = testSchema)\n      val protocolV0 = getProtocol(engine, tablePath)\n      assert(!protocolV0.supportsFeature(TableFeatures.VARIANT_SHREDDING_PREVIEW_RW_FEATURE))\n      assert(!protocolV0.supportsFeature(TableFeatures.VARIANT_SHREDDING_RW_FEATURE))\n\n      updateTableMetadata(\n        engine = engine,\n        tablePath = tablePath,\n        tableProperties = Map(\"delta.enableVariantShredding\" -> \"true\"))\n      val protocolV1 = getProtocol(engine, tablePath)\n      assert(!protocolV1.supportsFeature(TableFeatures.VARIANT_SHREDDING_PREVIEW_RW_FEATURE))\n      assert(protocolV1.supportsFeature(TableFeatures.VARIANT_SHREDDING_RW_FEATURE))\n    }\n  }\n\n  test(\"variantShredding-preview in existing table is respected\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      spark.sql(s\"CREATE TABLE delta.`$tablePath`(id INT) USING delta \" +\n        s\"TBLPROPERTIES ('delta.feature.variantShredding-preview' = 'supported')\")\n\n      val protocolV0 = getProtocol(engine, tablePath)\n      require(protocolV0.supportsFeature(TableFeatures.VARIANT_SHREDDING_PREVIEW_RW_FEATURE))\n      require(!protocolV0.supportsFeature(TableFeatures.VARIANT_SHREDDING_RW_FEATURE))\n\n      updateTableMetadata(\n        engine = engine,\n        tablePath = tablePath,\n        tableProperties = Map(\"delta.enableVariantShredding\" -> \"true\"))\n      val protocolV1 = getProtocol(engine, tablePath)\n      assert(protocolV1.supportsFeature(TableFeatures.VARIANT_SHREDDING_PREVIEW_RW_FEATURE))\n      assert(!protocolV1.supportsFeature(TableFeatures.VARIANT_SHREDDING_RW_FEATURE))\n    }\n  }\n  test(\"both variantShredding and variantShredding-preview can coexist\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath = tablePath,\n        schema = testSchema,\n        tableProperties = Map(\n          \"delta.enableVariantShredding\" -> \"true\",\n          \"delta.feature.variantShredding-preview\" -> \"supported\"))\n\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.VARIANT_SHREDDING_RW_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.VARIANT_SHREDDING_PREVIEW_RW_FEATURE))\n    }\n  }\n  /* ---- End: variant shredding tests ---- */\n\n  /* ---- Start: materialize partition columns tests ---- */\n  test(\"materialize partition columns in parquet schema when feature is enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a partitioned table with materialize partition columns feature enabled\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testPartitionSchema,\n        partCols = testPartitionColumns,\n        data = Seq(Map(\"part1\" -> Literal.ofInt(1), \"part2\" -> Literal.ofInt(2)) ->\n          dataPartitionBatches1),\n        tableProperties = Map(\"delta.feature.materializePartitionColumns\" -> \"supported\"))\n\n      // Verify the feature is enabled in protocol\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.MATERIALIZE_PARTITION_COLUMNS_W_FEATURE))\n\n      // Find data parquet files (excluding checkpoints)\n      val dataFiles = Files.walk(Paths.get(tablePath)).iterator().asScala\n        .filter(path =>\n          path.toString.endsWith(\".parquet\") &&\n            !path.toString.contains(\"_delta_log\"))\n        .toSeq\n\n      assert(dataFiles.nonEmpty, \"Expected at least one data file to be written\")\n\n      // Read parquet schema from the first data file\n      val parquetMetadata = ParquetFileReader.readFooter(\n        configuration,\n        new Path(dataFiles.head.toString))\n      val parquetSchema = parquetMetadata.getFileMetaData.getSchema\n\n      // Verify that partition columns are present in the parquet schema\n      val fieldNames: Set[String] = (0 until parquetSchema.getFieldCount).map(i =>\n        parquetSchema.getType(i).getName).toSet\n\n      assert(\n        fieldNames.contains(\"part1\"),\n        s\"Partition column 'part1' should be present in parquet schema. Found fields: $fieldNames\")\n      assert(\n        fieldNames.contains(\"part2\"),\n        s\"Partition column 'part2' should be present in parquet schema. Found fields: $fieldNames\")\n      assert(\n        fieldNames.contains(\"id\"),\n        s\"Data column 'id' should be present in parquet schema. Found fields: $fieldNames\")\n\n      // Verify data using Kernel and Spark readers\n      verifyWrittenContent(\n        tablePath,\n        testPartitionSchema,\n        dataPartitionBatches1.flatMap(_.toTestRows))\n    }\n  }\n\n  test(\"partition columns NOT in parquet schema when materializePartitionColumns is disabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a partitioned table without the feature (default behavior)\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testPartitionSchema,\n        partCols = testPartitionColumns,\n        data = Seq(Map(\"part1\" -> Literal.ofInt(1), \"part2\" -> Literal.ofInt(2)) ->\n          dataPartitionBatches1))\n\n      // Verify the feature is NOT enabled in protocol\n      val protocol = getProtocol(engine, tablePath)\n      assert(!protocol.supportsFeature(TableFeatures.MATERIALIZE_PARTITION_COLUMNS_W_FEATURE))\n\n      // Find data parquet files (excluding checkpoints)\n      val dataFiles = Files.walk(Paths.get(tablePath)).iterator().asScala\n        .filter(path =>\n          path.toString.endsWith(\".parquet\") &&\n            !path.toString.contains(\"_delta_log\"))\n        .toSeq\n\n      assert(dataFiles.nonEmpty, \"Expected at least one data file to be written\")\n\n      // Read parquet schema from the first data file.\n      val parquetMetadata = ParquetFileReader.readFooter(\n        configuration,\n        new Path(dataFiles.head.toString))\n      val parquetSchema = parquetMetadata.getFileMetaData.getSchema\n\n      // Verify that partition columns are NOT present in the parquet schema.\n      val fieldNames: Set[String] = (0 until parquetSchema.getFieldCount).map(i =>\n        parquetSchema.getType(i).getName).toSet\n\n      assert(\n        !fieldNames.contains(\"part1\"),\n        s\"Partition column 'part1' should NOT be present in parquet schema. Found fields: \" +\n          fieldNames.toString)\n      assert(\n        !fieldNames.contains(\"part2\"),\n        s\"Partition column 'part2' should NOT be present in parquet schema. Found fields: \" +\n          fieldNames.toString)\n      assert(\n        fieldNames.contains(\"id\"),\n        s\"Data column 'id' should be present in parquet schema. Found fields: $fieldNames\")\n\n      // Verify data using Kernel and Spark readers.\n      verifyWrittenContent(\n        tablePath,\n        testPartitionSchema,\n        dataPartitionBatches1.flatMap(_.toTestRows))\n    }\n  }\n  /* ---- End: materialize partition columns tests ---- */\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Helper methods\n  ///////////////////////////////////////////////////////////////////////////\n  def latestSnapshot(table: Table, engine: Engine): SnapshotImpl = {\n    table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n  }\n\n  def checkWriterFeaturesSupported(\n      tblPath: String,\n      expWriterOnlyFeatures: String*): Unit = {\n    val protocol = getLatestProtocol(tblPath)\n    val missingFeatures =\n      expWriterOnlyFeatures.toSet -- protocol.writerFeatures.getOrElse(Set.empty)\n\n    assert(\n      missingFeatures.isEmpty,\n      s\"The following expected writer features are not supported: \" +\n        s\"${missingFeatures.mkString(\", \")}\")\n  }\n\n  def checkNoWriterFeaturesSupported(tblPath: String, notExpWriterOnlyFeatures: String*): Unit = {\n    val protocol = getLatestProtocol(tblPath)\n    assert(protocol.writerFeatures.getOrElse(Set.empty)\n      .intersect(notExpWriterOnlyFeatures.toSet).isEmpty)\n  }\n\n  def checkReaderWriterFeaturesSupported(\n      tblPath: String,\n      expectedReaderWriterFeatures: String*): Unit = {\n\n    val protocol = getLatestProtocol(tblPath)\n\n    val missingInWriterSet =\n      expectedReaderWriterFeatures.toSet -- protocol.writerFeatures.getOrElse(Set.empty)\n    assert(\n      missingInWriterSet.isEmpty,\n      s\"The following expected readerWriter features are not supported in writerFeatures set: \" +\n        s\"${missingInWriterSet.mkString(\", \")}\")\n\n    val missingInReaderSet =\n      expectedReaderWriterFeatures.toSet -- protocol.readerFeatures.getOrElse(Set.empty)\n    assert(\n      missingInReaderSet.isEmpty,\n      s\"The following expected readerWriter features are not supported in readerFeatures set: \" +\n        s\"${missingInReaderSet.mkString(\", \")}\")\n  }\n\n  def checkNoReaderWriterFeaturesSupported(\n      tblPath: String,\n      notExpReaderWriterFeatures: String*): Unit = {\n    val protocol = getLatestProtocol(tblPath)\n    assert(protocol.readerFeatures.getOrElse(Set.empty)\n      .intersect(notExpReaderWriterFeatures.toSet).isEmpty)\n    assert(protocol.writerFeatures.getOrElse(Set.empty)\n      .intersect(notExpReaderWriterFeatures.toSet).isEmpty)\n  }\n\n  def getLatestProtocol(tblPath: String): Protocol = {\n    val deltaLog = DeltaLog.forTable(spark, tblPath)\n    deltaLog.update()\n    deltaLog.snapshot.protocol\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableReadsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.math.BigDecimal\nimport java.sql.Date\nimport java.time.Instant\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.Table\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestRow, TestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\nimport io.delta.kernel.exceptions.{InvalidTableException, KernelException, TableNotFoundException, UnsupportedProtocolVersionException}\nimport io.delta.kernel.expressions.{Column, Literal, Predicate}\nimport io.delta.kernel.internal.TableImpl\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.{DateTimeConstants, FileNames}\nimport io.delta.kernel.internal.util.InternalUtils.daysSinceEpoch\nimport io.delta.kernel.types.{LongType, StructType}\n\nimport org.apache.spark.sql.delta.{DeltaLog, DeltaOperations}\nimport org.apache.spark.sql.delta.actions.AddFile\n\nimport org.apache.hadoop.shaded.org.apache.commons.io.FileUtils\nimport org.apache.spark.sql.functions.col\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LegacyDeltaTableReadsSuite\n    extends AbstractDeltaTableReadsSuite\n    with TestUtilsWithLegacyKernelAPIs {\n\n  // Loading a `Table` at a path and then loading a `Snapshot` is only applicable to the Legacy API.\n  test(\"table deleted after the `Table` creation\") {\n    withTempDir { temp =>\n      val source = new File(goldenTablePath(\"data-reader-primitives\"))\n      val target = new File(temp.getCanonicalPath)\n      FileUtils.copyDirectory(source, target)\n\n      val table = Table.forPath(defaultEngine, target.getCanonicalPath)\n      // delete the table and try to get the snapshot. Expect a failure.\n      FileUtils.deleteDirectory(target)\n      val ex = intercept[TableNotFoundException] {\n        table.getLatestSnapshot(defaultEngine)\n      }\n      assert(ex.getMessage.contains(\n        s\"Delta table at path `file:${target.getCanonicalPath}` is not found\"))\n    }\n  }\n}\n\nclass DeltaTableReadsSuite\n    extends AbstractDeltaTableReadsSuite\n    with TestUtilsWithTableManagerAPIs\n\ntrait AbstractDeltaTableReadsSuite extends AnyFunSuite { self: AbstractTestUtils =>\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Timestamp type tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  // Below table is written in either UTC or PDT for the golden tables\n  // Kernel always interprets partition timestamp columns in UTC\n  /*\n  id: int  | Part (TZ agnostic): timestamp     | time : timestamp\n  ------------------------------------------------------------------------\n  0        | 2020-01-01 08:09:10.001           | 2020-02-01 08:09:10\n  1        | 2021-10-01 08:09:20               | 1999-01-01 09:00:00\n  2        | 2021-10-01 08:09:20               | 2000-01-01 09:00:00\n  3        | 1969-01-01 00:00:00               | 1969-01-01 00:00:00\n  4        | null                              | null\n   */\n\n  def row0: TestRow = TestRow(\n    0,\n    1577866150001000L, // 2020-01-01 08:09:10.001 UTC to micros since the epoch\n    1580544550000000L // 2020-02-01 08:09:10 UTC to micros since the epoch\n  )\n\n  def row1: TestRow = TestRow(\n    1,\n    1633075760000000L, // 2021-10-01 08:09:20 UTC to micros since the epoch\n    915181200000000L // 1999-01-01 09:00:00 UTC to micros since the epoch\n  )\n\n  def row2: TestRow = TestRow(\n    2,\n    1633075760000000L, // 2021-10-01 08:09:20 UTC to micros since the epoch\n    946717200000000L // 2000-01-01 09:00:00 UTC to micros since the epoch\n  )\n\n  def row3: TestRow = TestRow(\n    3,\n    -31536000000000L, // 1969-01-01 00:00:00  UTC to micros since the epoch\n    -31536000000000L // 1969-01-01 00:00:00 UTC to micros since the epoch\n  )\n\n  def row4: TestRow = TestRow(\n    4,\n    null,\n    null)\n\n  def utcTableExpectedResult: Seq[TestRow] = Seq(row0, row1, row2, row3, row4)\n\n  def testTimestampTable(\n      goldenTableName: String,\n      timeZone: String,\n      expectedResult: Seq[TestRow]): Unit = {\n    withTimeZone(timeZone) {\n      checkTable(\n        path = goldenTablePath(goldenTableName),\n        expectedAnswer = expectedResult)\n    }\n  }\n\n  for (timestampType <- Seq(\"INT96\", \"TIMESTAMP_MICROS\", \"TIMESTAMP_MILLIS\")) {\n    for (timeZone <- Seq(\"UTC\", \"Iceland\", \"PST\", \"America/Los_Angeles\")) {\n      test(\n        s\"end-to-end usage: timestamp table parquet timestamp format $timestampType tz $timeZone\") {\n        testTimestampTable(\"kernel-timestamp-\" + timestampType, timeZone, utcTableExpectedResult)\n      }\n    }\n  }\n\n  // PST table - all the \"time\" col timestamps are + 8 hours\n  def pstTableExpectedResult: Seq[TestRow] = utcTableExpectedResult.map { testRow =>\n    val values = testRow.toSeq\n    TestRow(\n      values(0),\n      // Partition columns are written as the local date time without timezone information and then\n      // interpreted by Kernel in UTC --> so the written partition value (& the read value) is the\n      // same as the UTC table\n      values(1),\n      if (values(2) == null) {\n        null\n      } else {\n        values(2).asInstanceOf[Long] + DateTimeConstants.MICROS_PER_HOUR * 8\n      })\n  }\n\n  for (timeZone <- Seq(\"UTC\", \"Iceland\", \"PST\", \"America/Los_Angeles\")) {\n    test(s\"end-to-end usage: timestamp in written in PST read in $timeZone\") {\n      testTimestampTable(\"kernel-timestamp-PST\", timeZone, pstTableExpectedResult)\n    }\n  }\n\n  test(s\"end-to-end usage: table with partition column in ISO8601 timestamp format\") {\n    /*\n    str: string         | ts: timestamp (partition col)\n    ------------------------------------------------------------------------\n    2024-01-01 10:00:00 | 2024-01-01T10:00:00.000000Z\n    2024-01-02 12:30:00 | 2024-01-02T12:30:00.000000Z\n     */\n    def row00: TestRow = TestRow(\n      \"2024-01-01 10:00:00\",\n      1704103200000000L // 2024-01-01 10:00:00 UTC to micros since the epoch\n    )\n\n    def row11: TestRow = TestRow(\n      \"2024-01-02 12:30:00\",\n      1704198600000000L // 2024-01-02 12:30:00 UTC to micros since the epoch\n    )\n    def ISO8601PartitionColTableExpectedResult: Seq[TestRow] =\n      Seq(row00, row11)\n    checkTable(\n      goldenTablePath(\"kernel-timestamp-partition-col-ISO8601\"),\n      ISO8601PartitionColTableExpectedResult)\n  }\n\n  test(s\"end-to-end usage: table with partition column in ISO8601 timestamp format with \" +\n    s\"partition pruning\") {\n    /*\n    str: string         | ts: timestamp (partition col)\n    ------------------------------------------------------------------------\n    2024-01-01 10:00:00 | 2024-01-01T10:00:00.000000Z\n    2024-01-02 12:30:00 | 2024-01-02T12:30:00.000000Z\n     */\n    def row00: TestRow = TestRow(\n      \"2024-01-01 10:00:00\",\n      1704103200000000L // 2024-01-01 10:00:00 UTC to micros since the epoch\n    )\n    val filter = new Predicate(\"=\", new Column(\"ts\"), Literal.ofTimestamp(1704103200000000L))\n    def ISO8601PartitionColTableExpectedResult: Seq[TestRow] =\n      Seq(row00)\n    checkTable(\n      goldenTablePath(\"kernel-timestamp-partition-col-ISO8601\"),\n      ISO8601PartitionColTableExpectedResult,\n      filter = filter)\n  }\n\n  test(s\"end-to-end usage: spark-created table with partition column in ISO8601 timestamp \" +\n    s\"format with microsecond precision and partition pruning\") {\n    // Set timezone to UTC so timestamps are interpreted consistently\n    withTimeZone(\"UTC\") {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getCanonicalPath\n\n        // Create table with Spark - timestamp partition with microsecond precision\n        // Using spark.databricks.delta.write.utcTimestampPartitionValues=true\n        /*\n        str: string         | ts: timestamp (partition col)\n        ------------------------------------------------------------------------\n        2024-01-01 10:00:00.123456 | 2024-01-01T10:00:00.123456Z\n        2024-01-02 12:30:00.654321 | 2024-01-02T12:30:00.654321Z\n         */\n        withSQLConf(\"spark.databricks.delta.write.utcTimestampPartitionValues\" -> \"true\") {\n          spark.sql(s\"\"\"CREATE TABLE delta.`$tablePath` (\n              str string,\n              ts timestamp\n            ) USING delta PARTITIONED BY (ts)\"\"\")\n\n          // Insert data with microsecond precision\n          spark.sql(s\"\"\"INSERT INTO delta.`$tablePath` VALUES\n              ('2024-01-01 10:00:00.123456', TIMESTAMP '2024-01-01 10:00:00.123456'),\n              ('2024-01-02 12:30:00.654321', TIMESTAMP '2024-01-02 12:30:00.654321')\"\"\")\n        }\n\n        // Verify partition format is ISO8601 by checking the Delta log's partitionValues\n        // (not the physical directory names, which may use a different format)\n        val deltaLog = DeltaLog.forTable(spark, tablePath)\n        val snapshot = deltaLog.update()\n        val addFiles = snapshot.allFiles.collect()\n\n        assert(addFiles.length == 2, s\"Expected 2 AddFile entries, but found ${addFiles.length}\")\n\n        // Check that partitionValues in the Delta log use ISO8601 format\n        val partitionValues = addFiles.map(_.partitionValues(\"ts\")).toSeq\n        assert(\n          partitionValues.forall(_.contains(\"T\")),\n          s\"Expected ISO8601 format with 'T' separator in partitionValues, but found: \" +\n            s\"${partitionValues.mkString(\", \")}\")\n\n        // Now verify reading with Kernel\n        def row00: TestRow = TestRow(\n          \"2024-01-01 10:00:00.123456\",\n          1704103200123456L // 2024-01-01 10:00:00.123456 UTC to micros since the epoch\n        )\n\n        def row11: TestRow = TestRow(\n          \"2024-01-02 12:30:00.654321\",\n          1704198600654321L // 2024-01-02 12:30:00.654321 UTC to micros since the epoch\n        )\n\n        // Test reading all data\n        checkTable(tablePath, Seq(row00, row11))\n\n        // Test partition pruning\n        val filter = new Predicate(\n          \"=\",\n          new Column(\"ts\"),\n          Literal.ofTimestamp(1704103200123456L)\n        ) // Only read row00\n        checkTable(\n          tablePath,\n          Seq(row00),\n          filter = filter)\n      }\n    }\n  }\n\n  test(\"read table with far-future timestamp in stats\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      spark.sql(s\"CREATE TABLE delta.`$path` (ts TIMESTAMP) USING DELTA\")\n      spark.sql(s\"INSERT INTO delta.`$path` VALUES (TIMESTAMP'2020-01-01 00:00:00')\")\n      spark.sql(s\"INSERT INTO delta.`$path` VALUES (TIMESTAMP'9999-12-31 23:59:59')\")\n      val filter = new Predicate(\"<\", new Column(\"ts\"), Literal.ofTimestamp(253402300799000000L))\n      checkTable(\n        path,\n        Seq(TestRow(1577836800000000L)),\n        filter = filter,\n        expectedRemainingFilter = filter)\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Timestamp_NTZ tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  // Below is the golden table used in test\n  // (INTEGER id, TIMESTAMP_NTZ tsNtz, TIMESTAMP_NTZ tsNtzPartition)\n  // (0, '2021-11-18 02:30:00.123456','2021-11-18 02:30:00.123456'),\n  // (1, '2013-07-05 17:01:00.123456','2021-11-18 02:30:00.123456'),\n  // (2, NULL,                         '2021-11-18 02:30:00.123456'),\n  // (3, '2021-11-18 02:30:00.123456','2013-07-05 17:01:00.123456'),\n  // (4, '2013-07-05 17:01:00.123456','2013-07-05 17:01:00.123456'),\n  // (5, NULL,                        '2013-07-05 17:01:00.123456'),\n  // (6, '2021-11-18 02:30:00.123456', NULL),\n  // (7, '2013-07-05 17:01:00.123456', NULL),\n  // (8, NULL,                         NULL)\n  val expectedTimestampNtzTestRows = Seq(\n    TestRow(0, 1637202600123456L, 1637202600123456L),\n    TestRow(1, 1373043660123456L, 1637202600123456L),\n    TestRow(2, null, 1637202600123456L),\n    TestRow(3, 1637202600123456L, 1373043660123456L),\n    TestRow(4, 1373043660123456L, 1373043660123456L),\n    TestRow(5, null, 1373043660123456L),\n    TestRow(6, 1637202600123456L, null),\n    TestRow(7, 1373043660123456L, null),\n    TestRow(8, null, null))\n\n  Seq(\"\", \"-name-mode\", \"-id-mode\").foreach { cmMode =>\n    test(s\"end-to-end: read table with timestamp_ntz columns (including partition): $cmMode\") {\n      checkTable(\n        path = goldenTablePath(s\"data-reader-timestamp_ntz$cmMode\"),\n        expectedAnswer = expectedTimestampNtzTestRows)\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Decimal type tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  for (tablePath <- Seq(\"basic-decimal-table\", \"basic-decimal-table-legacy\")) {\n    test(s\"end to end: reading $tablePath\") {\n      val expectedResult = Seq(\n        (\"234.00000\", \"1.00\", \"2.00000\", \"3.0000000000\"),\n        (\"2342222.23454\", \"111.11\", \"22222.22222\", \"3333333333.3333333333\"),\n        (\"0.00004\", \"0.00\", \"0.00000\", \"0E-10\"),\n        (\"-2342342.23423\", \"-999.99\", \"-99999.99999\", \"-9999999999.9999999999\")).map { tup =>\n        (\n          new BigDecimal(tup._1),\n          new BigDecimal(tup._2),\n          new BigDecimal(tup._3),\n          new BigDecimal(tup._4))\n      }\n\n      checkTable(\n        path = goldenTablePath(tablePath),\n        expectedAnswer = expectedResult.map(TestRow.fromTuple(_)))\n    }\n  }\n\n  test(s\"end to end: reading decimal-various-scale-precision\") {\n    val tablePath = goldenTablePath(\"decimal-various-scale-precision\")\n    val expResults = spark.sql(s\"SELECT * FROM delta.`$tablePath`\")\n      .collect()\n      .map(TestRow(_))\n\n    checkTable(\n      path = goldenTablePath(\"decimal-various-scale-precision\"),\n      expectedAnswer = expResults)\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Table/Snapshot tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  test(\"invalid path\") {\n    val invalidPath = \"/path/to/non-existent-directory\"\n    val tableManager = getTableManagerAdapter\n\n    def expectTableNotFoundException(fn: () => Unit): Unit = {\n      val ex = intercept[TableNotFoundException] {\n        fn()\n      }\n      assert(ex.getMessage().contains(s\"Delta table at path `file:$invalidPath` is not found\"))\n    }\n\n    expectTableNotFoundException { () =>\n      tableManager.getSnapshotAtLatest(defaultEngine, invalidPath)\n    }\n    expectTableNotFoundException { () =>\n      tableManager.getSnapshotAtVersion(defaultEngine, invalidPath, 1)\n    }\n    expectTableNotFoundException { () =>\n      tableManager.getSnapshotAtTimestamp(defaultEngine, invalidPath, 1)\n    }\n  }\n\n  // TODO for the below, when should we throw an exception? #2253\n  //   - on Table creation?\n  //   - on Snapshot creation?\n\n  test(\"empty _delta_log folder\") {\n    withTempDir { dir =>\n      new File(dir, \"_delta_log\").mkdirs()\n      intercept[TableNotFoundException] {\n        latestSnapshot(dir.getAbsolutePath)\n      }\n    }\n  }\n\n  test(\"empty folder with no _delta_log dir\") {\n    withTempDir { dir =>\n      intercept[TableNotFoundException] {\n        latestSnapshot(dir.getAbsolutePath)\n      }\n    }\n  }\n\n  test(\"non-empty folder not a delta table\") {\n    intercept[TableNotFoundException] {\n      latestSnapshot(goldenTablePath(\"no-delta-log-folder\"))\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Misc tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  test(\"end to end: multi-part checkpoint\") {\n    checkTable(\n      path = goldenTablePath(\"multi-part-checkpoint\"),\n      expectedAnswer = (Seq(0L) ++ (0L until 30L)).map(TestRow(_)))\n  }\n\n  test(\"read partitioned table\") {\n    val path = \"file:\" + goldenTablePath(\"data-reader-partition-values\")\n\n    // for now we don't support timestamp type partition columns so remove from read columns\n    val readCols = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, path)\n      .getSchema()\n      .withoutField(\"as_timestamp\")\n      .fields()\n      .asScala\n      .map(_.getName)\n      .toSeq\n\n    val expectedAnswer = Seq(0, 1).map { i =>\n      TestRow(\n        i,\n        i.toLong,\n        i.toByte,\n        i.toShort,\n        i % 2 == 0,\n        i.toFloat,\n        i.toDouble,\n        i.toString,\n        \"null\",\n        daysSinceEpoch(Date.valueOf(\"2021-09-08\")),\n        new BigDecimal(i),\n        Seq(TestRow(i), TestRow(i), TestRow(i)),\n        TestRow(i.toString, i.toString, TestRow(i, i.toLong)),\n        i.toString)\n    } ++ (TestRow(\n      null,\n      null,\n      null,\n      null,\n      null,\n      null,\n      null,\n      null,\n      null,\n      null,\n      null,\n      Seq(TestRow(2), TestRow(2), TestRow(2)),\n      TestRow(\"2\", \"2\", TestRow(2, 2L)),\n      \"2\") :: Nil)\n\n    checkTable(\n      path = path,\n      expectedAnswer = expectedAnswer,\n      readCols = readCols)\n  }\n\n  test(\"table with complex array types\") {\n    val path = \"file:\" + goldenTablePath(\"data-reader-array-complex-objects\")\n\n    val expectedAnswer = (0 until 10).map { i =>\n      TestRow(\n        i,\n        Seq(Seq(Seq(i, i, i), Seq(i, i, i)), Seq(Seq(i, i, i), Seq(i, i, i))),\n        Seq(\n          Seq(Seq(Seq(i, i, i), Seq(i, i, i)), Seq(Seq(i, i, i), Seq(i, i, i))),\n          Seq(Seq(Seq(i, i, i), Seq(i, i, i)), Seq(Seq(i, i, i), Seq(i, i, i)))),\n        Seq(\n          Map[String, Long](i.toString -> i.toLong),\n          Map[String, Long](i.toString -> i.toLong)),\n        Seq(TestRow(i), TestRow(i), TestRow(i)))\n    }\n\n    checkTable(\n      path = path,\n      expectedAnswer = expectedAnswer)\n  }\n\n  Seq(\"name\", \"id\").foreach { columnMappingMode =>\n    test(s\"table with `$columnMappingMode` column mapping mode\") {\n      val path = goldenTablePath(s\"table-with-columnmapping-mode-$columnMappingMode\")\n\n      val expectedAnswer = (0 until 5).map { i =>\n        TestRow(\n          i.byteValue(),\n          i.shortValue(),\n          i,\n          i.longValue(),\n          i.floatValue(),\n          i.doubleValue(),\n          new java.math.BigDecimal(i),\n          i % 2 == 0,\n          i.toString,\n          i.toString.getBytes,\n          daysSinceEpoch(Date.valueOf(\"2021-11-18\")), // date in days\n          (i * 1000).longValue(), // timestamp in micros\n          TestRow(i.toString, TestRow(i)), // nested_struct\n          Seq(i, i + 1), // array_of_prims\n          Seq(Seq(i, i + 1), Seq(i + 2, i + 3)), // array_of_arrays\n          Seq(Map(i -> Seq(2, 3), i + 1 -> Seq(4, 5))), // array_of_map_of_arrays\n          Seq(TestRow(i), TestRow(i)), // array_of_structs\n          TestRow( // struct_of_arrays_maps_of_structs\n            Seq(i, i + 1),\n            Map(Seq(i, i + 1) -> TestRow(i + 2))),\n          Map(\n            i -> (i + 1).longValue(),\n            (i + 2) -> (i + 3).longValue()\n          ), // map_of_prims\n          Map(i + 1 -> TestRow((i * 20).longValue())), // map_of_rows\n          {\n            val val1 = Seq(i, null, i + 1)\n            val val2 = Seq[Integer]()\n            Map(\n              i.longValue() -> val1,\n              (i + 1).longValue() -> val2\n            ) // map_of_arrays\n          },\n          Map( // map_of_maps\n            i.toLong -> Map(i -> i),\n            (i + 1).toLong -> Map(i + 2 -> i)))\n      } ++ Seq(TestRow(Seq.fill(22)(null): _*)) // all nulls row, 22 columns\n\n      checkTable(\n        path = path,\n        expectedAnswer = expectedAnswer)\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { columnMappingMode =>\n    test(s\"table with `$columnMappingMode` column mapping mode - read subset of columns\") {\n      val path = goldenTablePath(s\"table-with-columnmapping-mode-$columnMappingMode\")\n\n      val expectedAnswer = (0 until 5).map { i =>\n        TestRow(\n          i.byteValue(),\n          new java.math.BigDecimal(i),\n          TestRow(i.toString, TestRow(i)), // nested_struct\n          Seq(i, i + 1), // array_of_prims\n          Map(\n            i -> (i + 1).longValue(),\n            (i + 2) -> (i + 3).longValue()\n          ) // map_of_prims\n        )\n      } ++ (TestRow(\n        null,\n        null,\n        null,\n        null,\n        null\n      ) :: Nil)\n\n      checkTable(\n        path = path,\n        expectedAnswer = expectedAnswer,\n        readCols = Seq(\"ByteType\", \"decimal\", \"nested_struct\", \"array_of_prims\", \"map_of_prims\"))\n    }\n  }\n\n  test(\"read subfield of array of struct\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        spark.sql(s\"\"\"CREATE TABLE $tbl (\n          id int,\n          array_of_struct array<struct<x: int, y: int>>\n        ) USING delta LOCATION '$path' \"\"\")\n        spark.sql(s\"\"\"INSERT INTO $tbl VALUES\n        (1, array(struct(2 as x, 3 as y))),\n        (6, array(struct(7 as x, 8 as y))),\n        (11, array(struct(null as x, 8 as y))),\n        (12, array()),\n        (13, null)\"\"\")\n\n        // Test reading with pruned schema - only x field from struct\n        val prunedSchema = new io.delta.kernel.types.StructType()\n          .add(\n            \"array_of_struct\",\n            new io.delta.kernel.types.ArrayType(\n              new io.delta.kernel.types.StructType()\n                .add(\"x\", io.delta.kernel.types.IntegerType.INTEGER),\n              true))\n\n        val result = readSnapshot(\n          latestSnapshot(path.toString),\n          readSchema = prunedSchema)\n\n        val expectedAnswer = Seq(\n          TestRow(Seq(TestRow(2))),\n          TestRow(Seq(TestRow(7))),\n          TestRow(Seq(TestRow(null: Any))),\n          TestRow(Seq()),\n          TestRow(null: Any))\n        checkAnswer(result, expectedAnswer)\n      }\n    }\n  }\n\n  test(\"read subfield of array of array of struct\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        spark.sql(s\"\"\"CREATE TABLE $tbl (\n          id int,\n          array_of_array_of_struct array<array<struct<x: int, y: int>>>\n        ) USING delta LOCATION '$path' \"\"\")\n        spark.sql(s\"\"\"INSERT INTO $tbl VALUES\n        (1, array(array(struct(4 as x, 5 as y)))),\n        (6, array(array(struct(9 as x, 10 as y)))),\n        (11, array(array(struct(null as x, 10 as y)))),\n        (12, array(array())),\n        (13, array(null))\"\"\")\n\n        val prunedSchema = new io.delta.kernel.types.StructType()\n          .add(\n            \"array_of_array_of_struct\",\n            new io.delta.kernel.types.ArrayType(\n              new io.delta.kernel.types.ArrayType(\n                new io.delta.kernel.types.StructType()\n                  .add(\"x\", io.delta.kernel.types.IntegerType.INTEGER),\n                true),\n              true))\n\n        val result = readSnapshot(\n          latestSnapshot(path.toString),\n          readSchema = prunedSchema)\n\n        val expectedAnswer = Seq(\n          TestRow(Seq(Seq(TestRow(4)))),\n          TestRow(Seq(Seq(TestRow(9)))),\n          TestRow(Seq(Seq(TestRow(null: Any)))),\n          TestRow(Seq(Seq())),\n          TestRow(Seq(null)))\n        checkAnswer(result, expectedAnswer)\n      }\n    }\n  }\n\n  test(\"read array of array of int\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        spark.sql(s\"\"\"CREATE TABLE $tbl (\n          id int,\n          array_of_array_of_int array<array<int>>\n        ) USING delta LOCATION '$path' \"\"\")\n        spark.sql(s\"\"\"INSERT INTO $tbl VALUES\n        (1, array(array(100, 101))),\n        (6, array(array(102, 103))),\n        (11, array(array(null, 104))),\n        (12, array(array())),\n        (13, array(null))\"\"\")\n\n        checkTable(\n          path = path.toString,\n          expectedAnswer = Seq(\n            TestRow(Seq(Seq(100, 101))),\n            TestRow(Seq(Seq(102, 103))),\n            TestRow(Seq(Seq(null, 104))),\n            TestRow(Seq(Seq())),\n            TestRow(Seq(null))),\n          readCols = Seq(\"array_of_array_of_int\"))\n      }\n    }\n  }\n\n  test(\"read array of int\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        spark.sql(s\"\"\"CREATE TABLE $tbl (\n          id int,\n          array_of_int array<int>\n        ) USING delta LOCATION '$path' \"\"\")\n        spark.sql(s\"\"\"INSERT INTO $tbl VALUES\n        (1, array(200, 201)),\n        (6, array(202, 203)),\n        (11, array(null, 204)),\n        (12, array()),\n        (13, null)\"\"\")\n\n        checkTable(\n          path = path.toString,\n          expectedAnswer = Seq(\n            TestRow(Seq(200, 201)),\n            TestRow(Seq(202, 203)),\n            TestRow(Seq(null, 204)),\n            TestRow(Seq()),\n            TestRow(null: Any)),\n          readCols = Seq(\"array_of_int\"))\n      }\n    }\n  }\n\n  test(\"table with type widening on basic types\") {\n    val path = goldenTablePath(\"type-widening\")\n\n    def timestampToMicros(timestamp: String): Long = {\n      val instant = Instant.parse(timestamp)\n      instant.getEpochSecond() * DateTimeConstants.MICROS_PER_SECOND + instant.getNano() / 1000\n    }\n\n    val expectedAnswer = Seq(\n      TestRow(\n        1L,\n        2L,\n        3.4.toFloat.toDouble,\n        5.0,\n        6.0,\n        7.0,\n        timestampToMicros(\"2024-09-09T00:00:00Z\")),\n      TestRow(\n        Long.MaxValue,\n        Long.MaxValue,\n        1.234567890123,\n        1.234567890123,\n        1.234567890123,\n        1.234567890123,\n        timestampToMicros(\"2024-09-09T12:34:56.123456Z\")))\n    checkTable(\n      path = path,\n      expectedAnswer = expectedAnswer,\n      readCols = Seq(\n        \"byte_long\",\n        \"int_long\",\n        \"float_double\",\n        \"byte_double\",\n        \"short_double\",\n        \"int_double\",\n        \"date_timestamp_ntz\"))\n  }\n\n  test(\"table with type widening to decimal types\") {\n    val path = goldenTablePath(\"type-widening\")\n    val expectedAnswer = Seq(\n      TestRow(\n        BigDecimal.valueOf(12345L, 2),\n        BigDecimal.valueOf(6789000L, 5),\n        BigDecimal.valueOf(10L, 1),\n        BigDecimal.valueOf(20L, 1),\n        BigDecimal.valueOf(30L, 1),\n        BigDecimal.valueOf(40L, 1)),\n      TestRow(\n        BigDecimal.valueOf(1234567890123456L, 2),\n        BigDecimal.valueOf(1234567890123456L, 5),\n        BigDecimal.valueOf(1234L, 1),\n        BigDecimal.valueOf(123456L, 1),\n        BigDecimal.valueOf(12345678901L, 1),\n        BigDecimal.valueOf(1234567890123456789L, 1)))\n    checkTable(\n      path = path,\n      expectedAnswer = expectedAnswer,\n      readCols = Seq(\n        \"decimal_decimal_same_scale\",\n        \"decimal_decimal_greater_scale\",\n        \"byte_decimal\",\n        \"short_decimal\",\n        \"int_decimal\",\n        \"long_decimal\"))\n  }\n\n  test(\"table with type widening to nested types\") {\n    val path = goldenTablePath(\"type-widening-nested\")\n    val expectedAnswer = Seq(\n      TestRow(TestRow(1L), Map(2L -> 3L), Seq(4L, 5L)),\n      TestRow(\n        TestRow(Long.MaxValue),\n        Map(Long.MaxValue -> Long.MaxValue),\n        Seq(Long.MaxValue, Long.MinValue)))\n    checkTable(\n      path = path,\n      expectedAnswer = expectedAnswer,\n      readCols = Seq(\"struct\", \"map\", \"array\"))\n  }\n\n  test(\"table with complex map types\") {\n    val path = \"file:\" + goldenTablePath(\"data-reader-map\")\n\n    val expectedAnswer = (0 until 10).map { i =>\n      TestRow(\n        i,\n        Map(i -> i),\n        Map(i.toLong -> i.toByte),\n        Map(i.toShort -> (i % 2 == 0)),\n        Map(i.toFloat -> i.toDouble),\n        Map(i.toString -> new BigDecimal(i)),\n        Map(i -> Seq(TestRow(i), TestRow(i), TestRow(i))))\n    }\n\n    checkTable(\n      path = path,\n      expectedAnswer = expectedAnswer)\n  }\n\n  test(\"table with array of primitives\") {\n    val expectedAnswer = (0 until 10).map { i =>\n      TestRow(\n        Seq(i),\n        Seq(i.toLong),\n        Seq(i.toByte),\n        Seq(i.toShort),\n        Seq(i % 2 == 0),\n        Seq(i.toFloat),\n        Seq(i.toDouble),\n        Seq(i.toString),\n        Seq(Array(i.toByte, i.toByte)),\n        Seq(new BigDecimal(i)))\n    }\n    checkTable(\n      path = goldenTablePath(\"data-reader-array-primitives\"),\n      expectedAnswer = expectedAnswer)\n  }\n\n  test(\"table primitives\") {\n    val expectedAnswer = (0 to 10).map {\n      case 10 => TestRow(null, null, null, null, null, null, null, null, null, null)\n      case i => TestRow(\n          i,\n          i.toLong,\n          i.toByte,\n          i.toShort,\n          i % 2 == 0,\n          i.toFloat,\n          i.toDouble,\n          i.toString,\n          Array[Byte](i.toByte, i.toByte),\n          new BigDecimal(i))\n    }\n\n    checkTable(\n      path = goldenTablePath(\"data-reader-primitives\"),\n      expectedAnswer = expectedAnswer)\n  }\n\n  test(\"table with checkpoint\") {\n    checkTable(\n      path = getTestResourceFilePath(\"basic-with-checkpoint\"),\n      expectedAnswer = (0 until 150).map(i => TestRow(i.toLong)))\n  }\n\n  test(s\"table with spaces in the table path\") {\n    withTempDir { tempDir =>\n      val target = tempDir.getCanonicalPath + s\"/table- -path\"\n      spark.sql(s\"CREATE TABLE delta.`$target` USING DELTA \" +\n        s\"SELECT * FROM delta.`${getTestResourceFilePath(\"basic-with-checkpoint\")}`\")\n      checkTable(\n        path = target,\n        expectedAnswer = (0 until 150).map(i => TestRow(i.toLong)))\n    }\n  }\n\n  test(\"table with name column mapping mode\") {\n    val expectedAnswer = (0 to 10).map {\n      case 10 => TestRow(null, null, null, null, null, null, null, null, null, null)\n      case i => TestRow(\n          i,\n          i.toLong,\n          i.toByte,\n          i.toShort,\n          i % 2 == 0,\n          i.toFloat,\n          i.toDouble,\n          i.toString,\n          Array[Byte](i.toByte, i.toByte),\n          new BigDecimal(i))\n    }\n\n    checkTable(\n      path = getTestResourceFilePath(\"data-reader-primitives-column-mapping-name\"),\n      expectedAnswer = expectedAnswer)\n  }\n\n  test(\"partitioned table with column mapping\") {\n    val expectedAnswer = (0 to 2).map {\n      case 2 => TestRow(null, null, \"2\")\n      case i => TestRow(i, i.toDouble, i.toString)\n    }\n    val readCols = Seq(\n      // partition fields\n      \"as_int\",\n      \"as_double\",\n      // data fields\n      \"value\")\n\n    checkTable(\n      path = getTestResourceFilePath(\"data-reader-partition-values-column-mapping-name\"),\n      readCols = readCols,\n      expectedAnswer = expectedAnswer)\n  }\n\n  test(\"simple end to end with vacuum protocol check feature\") {\n    val expectedValues = (0 until 100).map(x => (x, s\"val=$x\"))\n    checkTable(\n      path = goldenTablePath(\"basic-with-vacuum-protocol-check-feature\"),\n      expectedAnswer = expectedValues.map(TestRow.fromTuple))\n  }\n\n  test(\"table with nested struct\") {\n    val expectedAnswer = (0 until 10).map { i =>\n      TestRow(TestRow(i.toString, i.toString, TestRow(i, i.toLong)), i)\n    }\n    checkTable(\n      path = goldenTablePath(\"data-reader-nested-struct\"),\n      expectedAnswer = expectedAnswer)\n  }\n\n  test(\"table with empty parquet files\") {\n    checkTable(\n      path = goldenTablePath(\"125-iterator-bug\"),\n      expectedAnswer = (1 to 5).map(TestRow(_)))\n  }\n\n  test(\"handle corrupted '_last_checkpoint' file\") {\n    checkTable(\n      path = goldenTablePath(\"corrupted-last-checkpoint-kernel\"),\n      expectedAnswer = (0L until 100L).map(TestRow(_)))\n  }\n\n  test(\"error - version not contiguous\") {\n    val e = intercept[InvalidTableException] {\n      latestSnapshot(goldenTablePath(\"versions-not-contiguous\"))\n    }\n    assert(e.getMessage.contains(\"versions are not contiguous: ([0, 2])\"))\n  }\n\n  test(\"table protocol version greater than reader protocol version\") {\n    val e = intercept[UnsupportedProtocolVersionException] {\n      latestSnapshot(goldenTablePath(\"deltalog-invalid-protocol-version\"))\n        .getScanBuilder()\n        .build()\n    }\n    assert(e.getMessage.contains(\"Unsupported Delta protocol reader version\"))\n  }\n\n  test(\"table with void type - schema parsing is lazy\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      spark.sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}`(x INTEGER, y VOID) USING DELTA\")\n      // Snapshot loading should succeed since schema parsing is now lazy\n      val snapshot = latestSnapshot(path)\n      assert(snapshot.getVersion >= 0)\n      // Accessing the schema should still throw KernelException due to VOID type\n      val e = intercept[KernelException] {\n        snapshot.getSchema\n      }\n      assert(e.getMessage.contains(\n        \"Failed to parse the schema. Encountered unsupported Delta data type: VOID\"))\n    }\n  }\n\n  test(\"read a shallow cloned table\") {\n    withTempDir { tempDir =>\n      val target = tempDir.getCanonicalPath\n      val source = goldenTablePath(\"data-reader-partition-values\")\n      spark.sql(s\"CREATE TABLE delta.`$target` SHALLOW CLONE delta.`$source`\")\n\n      withSparkTimeZone(\"UTC\") {\n        val expAnswer = spark.read.format(\"delta\").load(source).collect().map(TestRow(_)).toSeq\n        assert(expAnswer.size == 3)\n        checkTable(target, expAnswer)\n      }\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // getSnapshotAtVersion end-to-end tests (log segment tests in SnapshotManagerSuite)\n  //////////////////////////////////////////////////////////////////////////////////\n\n  test(\"getSnapshotAtVersion: basic end-to-end read\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      (0 to 10).foreach { i =>\n        spark.range(i * 10, i * 10 + 10).write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(path)\n      }\n      // Read a checkpoint version\n      checkTable(\n        path = path,\n        expectedAnswer = (0L to 99L).map(TestRow(_)),\n        version = Some(9),\n        expectedVersion = Some(9))\n      // Read a JSON version\n      checkTable(\n        path = path,\n        expectedAnswer = (0L to 89L).map(TestRow(_)),\n        version = Some(8),\n        expectedVersion = Some(8))\n      // Read the current version\n      checkTable(\n        path = path,\n        expectedAnswer = (0L to 109L).map(TestRow(_)),\n        version = Some(10),\n        expectedVersion = Some(10))\n      // Cannot read a version that does not exist\n      val e = intercept[RuntimeException] {\n        getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, path, 11)\n      }\n      assert(e.getMessage.contains(\n        \"Cannot load table version 11 as it does not exist. The latest available version is 10\"))\n    }\n  }\n\n  test(\"getSnapshotAtVersion: end-to-end test with truncated delta log\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      // Write versions [0, 10] (inclusive) including a checkpoint\n      (0 to 10).foreach { i =>\n        spark.range(i * 10, i * 10 + 10).write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tablePath)\n      }\n      val log = org.apache.spark.sql.delta.DeltaLog.forTable(\n        spark,\n        new org.apache.hadoop.fs.Path(tablePath))\n      val deltaCommitFileProvider = org.apache.spark.sql.delta.util.DeltaCommitFileProvider(\n        log.unsafeVolatileSnapshot)\n      // Delete the log files for versions 0-9, truncating the table history to version 10\n      (0 to 9).foreach { i =>\n        val jsonFile = deltaCommitFileProvider.deltaFile(i)\n        new File(new org.apache.hadoop.fs.Path(log.logPath, jsonFile).toUri).delete()\n      }\n      // Create version 11 that overwrites the whole table\n      spark.range(50).write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tablePath)\n\n      // Cannot read a version that has been truncated\n      val e = intercept[RuntimeException] {\n        getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tablePath, 9)\n      }\n      assert(e.getMessage.contains(\"Cannot load table version 9\"))\n      // Can read version 10\n      checkTable(\n        path = tablePath,\n        expectedAnswer = (0L to 109L).map(TestRow(_)),\n        version = Some(10),\n        expectedVersion = Some(10))\n      // Can read version 11\n      checkTable(\n        path = tablePath,\n        expectedAnswer = (0L until 50L).map(TestRow(_)),\n        version = Some(11),\n        expectedVersion = Some(11))\n    }\n  }\n\n  test(\"time travel with schema change\") {\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.getCanonicalPath)\n      spark.range(10, 20).withColumn(\"part\", col(\"id\"))\n        .write.format(\"delta\").mode(\"append\").option(\"mergeSchema\", true)\n        .save(tempDir.getCanonicalPath)\n      checkTable(\n        path = tempDir.getCanonicalPath,\n        expectedAnswer = (0L until 10L).map(TestRow(_)),\n        expectedSchema = new StructType().add(\"id\", LongType.LONG),\n        version = Some(0),\n        expectedVersion = Some(0))\n    }\n  }\n\n  test(\"time travel with partition change\") {\n    withTempDir { tempDir =>\n      spark.range(10).withColumn(\"part5\", col(\"id\") % 5)\n        .write.format(\"delta\").partitionBy(\"part5\").mode(\"append\")\n        .save(tempDir.getCanonicalPath)\n      spark.range(10, 20).withColumn(\"part2\", col(\"id\") % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part2\")\n        .mode(\"overwrite\")\n        .option(\"overwriteSchema\", true)\n        .save(tempDir.getCanonicalPath)\n      checkTable(\n        path = tempDir.getCanonicalPath,\n        expectedAnswer = (0L until 10L).map(v => TestRow(v, v % 5)),\n        expectedSchema = new StructType()\n          .add(\"id\", LongType.LONG)\n          .add(\"part5\", LongType.LONG),\n        version = Some(0),\n        expectedVersion = Some(0))\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // getSnapshotAtTimestamp end-to-end tests (more tests in DeltaHistoryManagerSuite)\n  //////////////////////////////////////////////////////////////////////////////////\n\n  private def generateCommits(path: String, commits: Long*): Unit = {\n    commits.zipWithIndex.foreach { case (ts, i) =>\n      spark.range(i * 10, i * 10 + 10).write.format(\"delta\").mode(\"append\").save(path)\n      val file = new File(FileNames.deltaFile(new Path(path, \"_delta_log\"), i))\n      file.setLastModified(ts)\n    }\n  }\n\n  test(\"getSnapshotAtTimestamp: basic end-to-end read\") {\n    withTempDir { tempDir =>\n      val start = 1540415658000L\n      val minuteInMilliseconds = 60000L\n      generateCommits(\n        tempDir.getCanonicalPath,\n        start,\n        start + 20 * minuteInMilliseconds,\n        start + 40 * minuteInMilliseconds)\n      // Exact timestamp for version 0\n      checkTable(\n        path = tempDir.getCanonicalPath,\n        expectedAnswer = (0L until 10L).map(TestRow(_)),\n        timestamp = Some(start),\n        expectedVersion = Some(0))\n      // Timestamp between version 0 and 1 should load version 0\n      checkTable(\n        path = tempDir.getCanonicalPath,\n        expectedAnswer = (0L until 10L).map(TestRow(_)),\n        timestamp = Some(start + 10 * minuteInMilliseconds),\n        expectedVersion = Some(0))\n      // Exact timestamp for version 1\n      checkTable(\n        path = tempDir.getCanonicalPath,\n        expectedAnswer = (0L until 20L).map(TestRow(_)),\n        timestamp = Some(start + 20 * minuteInMilliseconds),\n        expectedVersion = Some(1))\n      // Exact timestamp for the last version\n      checkTable(\n        path = tempDir.getCanonicalPath,\n        expectedAnswer = (0L until 30L).map(TestRow(_)),\n        timestamp = Some(start + 40 * minuteInMilliseconds),\n        expectedVersion = Some(2))\n      // Timestamp after last commit fails\n      val e1 = intercept[RuntimeException] {\n        checkTable(\n          path = tempDir.getCanonicalPath,\n          expectedAnswer = Seq(),\n          timestamp = Some(start + 50 * minuteInMilliseconds))\n      }\n      assert(e1.getMessage.contains(\n        s\"The provided timestamp ${start + 50 * minuteInMilliseconds} ms \" +\n          s\"(2018-10-24T22:04:18Z) is after the latest available version\"))\n      // Timestamp before the first commit fails\n      val e2 = intercept[RuntimeException] {\n        checkTable(\n          path = tempDir.getCanonicalPath,\n          expectedAnswer = Seq(),\n          timestamp = Some(start - 1L))\n      }\n      assert(e2.getMessage.contains(\n        s\"The provided timestamp ${start - 1L} ms (2018-10-24T21:14:17.999Z) is before \" +\n          s\"the earliest available version\"))\n    }\n  }\n\n  test(\"getSnapshotAtTimestamp: empty _delta_log folder\") {\n    withTempDir { dir =>\n      new File(dir, \"_delta_log\").mkdirs()\n      intercept[TableNotFoundException] {\n        getTableManagerAdapter\n          .getSnapshotAtTimestamp(defaultEngine, dir.getCanonicalPath, 0L)\n      }\n    }\n  }\n\n  test(\"getSnapshotAtTimestamp: empty folder no _delta_log dir\") {\n    withTempDir { dir =>\n      intercept[TableNotFoundException] {\n        getTableManagerAdapter\n          .getSnapshotAtTimestamp(defaultEngine, dir.getCanonicalPath, 0L)\n      }\n    }\n  }\n\n  test(\"getSnapshotAtTimestamp: non-empty folder not a delta table\") {\n    withTempDir { dir =>\n      spark.range(20).write.format(\"parquet\").mode(\"overwrite\").save(dir.getCanonicalPath)\n      intercept[TableNotFoundException] {\n        getTableManagerAdapter\n          .getSnapshotAtTimestamp(defaultEngine, dir.getCanonicalPath, 0L)\n      }\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////////////////////////\n  // getVersionBeforeOrAtTimestamp + getVersionAtOrAfterTimestamp tests\n  // (more in TableImplSuite and DeltaHistoryManagerSuite)\n  //////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Copied from Standalone DeltaLogSuite\n  test(\"getVersionBeforeOrAtTimestamp and getVersionAtOrAfterTimestamp\") {\n    // TODO: [delta-io/delta#4770] Implement the `getVersionBeforeOrAtTimestamp` and\n    //       `getVersionAtOrAfterTimestamp` APIs for CatalogManaged tables.\n    assume(getTableManagerAdapter.supportsTimestampResolution, \"Timestamp queries not supported\")\n\n    // Note:\n    // - all Xa test cases will test getVersionBeforeOrAtTimestamp\n    // - all Xb test cases will test getVersionAtOrAfterTimestamp\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val tableImpl = Table.forPath(defaultEngine, dir.getCanonicalPath).asInstanceOf[TableImpl]\n\n      // ========== case 0: delta table does not exist ==========\n      intercept[TableNotFoundException] {\n        tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, System.currentTimeMillis())\n      }\n      intercept[TableNotFoundException] {\n        tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, System.currentTimeMillis())\n      }\n\n      // Setup part 1 of 2: create log files\n      spark.range(10).write.format(\"delta\").mode(\"overwrite\").save(dir.getCanonicalPath)\n      (1 to 2).foreach { i =>\n        val files = AddFile(i.toString, Map.empty, 1, 1, true) :: Nil\n        log.startTransaction().commit(files, DeltaOperations.ManualUpdate)\n      }\n\n      // Setup part 2 of 2: edit lastModified times\n      val logPath = new Path(dir.getCanonicalPath, \"_delta_log\")\n\n      val delta0 = new File(FileNames.deltaFile(logPath, 0))\n      val delta1 = new File(FileNames.deltaFile(logPath, 1))\n      val delta2 = new File(FileNames.deltaFile(logPath, 2))\n      delta0.setLastModified(1000)\n      delta1.setLastModified(2000)\n      delta2.setLastModified(3000)\n\n      // ========== case 1: before first commit ==========\n      // case 1a\n      val e1 = intercept[KernelException] {\n        tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, 500)\n      }.getMessage\n      assert(e1.contains(\"is before the earliest available version 0\"))\n      // case 1b\n      assert(tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, 500) == 0)\n\n      // ========== case 2: at first commit ==========\n      // case 2a\n      assert(tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, 1000) == 0)\n      // case 2b\n      assert(tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, 1000) == 0)\n\n      // ========== case 3: between two normal commits ==========\n      // case 3a\n      assert(tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, 1500) == 0) // round down to v0\n      // case 3b\n      assert(tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, 1500) == 1) // round up to v1\n\n      // ========== case 4: at last commit ==========\n      // case 4a\n      assert(tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, 3000) == 2)\n      // case 4b\n      assert(tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, 3000) == 2)\n\n      // ========== case 5: after last commit ==========\n      // case 5a\n      assert(tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, 4000) == 2)\n      // case 5b\n      val e2 = intercept[KernelException] {\n        tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, 4000)\n      }.getMessage\n      assert(e2.contains(\"is after the latest available version 2\"))\n    }\n  }\n\n  // Copied from Standalone DeltaLogSuite\n  test(\"getVersionBeforeOrAtTimestamp and getVersionAtOrAfterTimestamp - recoverability\") {\n    // TODO: [delta-io/delta#4770] Implement the `getVersionBeforeOrAtTimestamp` and\n    //       `getVersionAtOrAfterTimestamp` APIs for CatalogManaged tables.\n    assume(getTableManagerAdapter.supportsTimestampResolution, \"Timestamp queries not supported\")\n\n    withTempDir { dir =>\n      // local file system truncates to seconds\n      val nowEpochMs = System.currentTimeMillis() / 1000 * 1000\n\n      val logPath = new Path(dir.getCanonicalPath, \"_delta_log\")\n\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val tableImpl = Table.forPath(defaultEngine, dir.getCanonicalPath).asInstanceOf[TableImpl]\n\n      spark.range(10).write.format(\"delta\").mode(\"overwrite\").save(dir.getCanonicalPath)\n      (1 to 35).foreach { i =>\n        val files = AddFile(i.toString, Map.empty, 1, 1, true) :: Nil\n        log.startTransaction().commit(files, DeltaOperations.ManualUpdate)\n      }\n\n      (0 to 35).foreach { i =>\n        val delta = new File(FileNames.deltaFile(logPath, i))\n        if (i >= 25) {\n          delta.setLastModified(nowEpochMs + i * 1000)\n        } else {\n          assert(delta.delete())\n        }\n      }\n\n      // A checkpoint exists at version 30, so all versions [30, 35] are recoverable.\n      // Nonetheless, getVersionBeforeOrAtTimestamp and getVersionAtOrAfterTimestamp do not\n      // require that the version is recoverable, so we should still be able to get back versions\n      // [25-29]\n\n      (25 to 34).foreach { i =>\n        if (i == 25) {\n          assertThrows[KernelException] {\n            tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, nowEpochMs + i * 1000 - 1)\n          }\n        } else {\n          assert(tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, nowEpochMs + i * 1000 - 1)\n            == i - 1)\n        }\n\n        assert(\n          tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, nowEpochMs + i * 1000 - 1) == i)\n\n        assert(tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, nowEpochMs + i * 1000) == i)\n        assert(tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, nowEpochMs + i * 1000) == i)\n\n        assert(\n          tableImpl.getVersionBeforeOrAtTimestamp(defaultEngine, nowEpochMs + i * 1000 + 1) == i)\n\n        if (i == 35) {\n          tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, nowEpochMs + i * 1000 + 1)\n        } else {\n          assert(tableImpl.getVersionAtOrAfterTimestamp(defaultEngine, nowEpochMs + i * 1000 + 1)\n            == i + 1)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableSchemaEvolutionSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util.Collections.emptySet\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Operation, Table, Transaction, TransactionCommitResult}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.{SnapshotImpl, TableConfig}\nimport io.delta.kernel.internal.actions.DomainMetadata\nimport io.delta.kernel.internal.clustering.ClusteringMetadataDomain\nimport io.delta.kernel.internal.util.{ColumnMapping, ColumnMappingSuiteBase}\nimport io.delta.kernel.types.{ArrayType, CollationIdentifier, DecimalType, FieldMetadata, IntegerType, LongType, MapType, StringType, StructType, TypeChange}\nimport io.delta.kernel.utils.CloseableIterable\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.prop.TableDrivenPropertyChecks.forAll\nimport org.scalatest.prop.Tables\n\nclass DeltaTableSchemaEvolutionTransactionBuilderV1Suite extends DeltaTableSchemaEvolutionSuiteBase\n    with WriteUtils {}\n\nclass DeltaTableSchemaEvolutionTransactionBuilderV2Suite extends DeltaTableSchemaEvolutionSuiteBase\n    with WriteUtilsWithV2Builders {}\n\n/**\n * ToDo: Clean this up by moving some common schemas to fixtures and abstracting\n * the setup/run schema evolution/assert loop\n */\ntrait DeltaTableSchemaEvolutionSuiteBase extends AnyFunSuite with AbstractWriteUtils\n    with ColumnMappingSuiteBase {\n\n  val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\")\n\n  test(\"Add nullable column succeeds and correctly updates maxFieldId\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true, fieldMetadataForColumn(4, \"d\"))\n            .add(\"e\", IntegerType.INTEGER, true, fieldMetadataForColumn(5, \"e\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n        .add(\"f\", new StringType(utf8Lcase), true, fieldMetadataForColumn(6, \"f\"))\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"f\"), 6, \"f\")\n      val updatedFType = structType.get(\"f\").getDataType.asInstanceOf[StringType]\n      assert(updatedFType.getCollationIdentifier == utf8Lcase)\n\n      val innerStruct = structType.get(\"b\").getDataType.asInstanceOf[StructType]\n      assertColumnMapping(innerStruct.get(\"d\"), 4, \"d\")\n      assertColumnMapping(innerStruct.get(\"e\"), 5, \"e\")\n      assertColumnMapping(structType.get(\"c\"), 2)\n      assert(getMaxFieldId(engine, tablePath) == 6)\n    }\n  }\n\n  test(\"Change collation of existing STRING field succeeds\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", new StringType(utf8Lcase), true, currentSchema.get(\"a\").getMetadata)\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n      val updatedAType = updatedSchema.get(\"a\").getDataType.asInstanceOf[StringType]\n      assert(updatedAType.getCollationIdentifier == utf8Lcase)\n      // Ensure no type changes recorded for a pure collation change\n      assert(updatedSchema.get(\"a\").getTypeChanges.isEmpty)\n    }\n  }\n\n  test(\"Add new STRING column with non-default collation succeeds\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\"b\", new StringType(utf8Lcase), true, fieldMetadataForColumn(3, \"b\"))\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(updatedSchema.get(\"a\"), 1)\n      assertColumnMapping(updatedSchema.get(\"b\"), 3, \"b\")\n      val updatedBType = updatedSchema.get(\"b\").getDataType.asInstanceOf[StringType]\n      assert(updatedBType.getCollationIdentifier == utf8Lcase)\n    }\n  }\n\n  test(\"Change nested STRING collation inside struct succeeds\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", StringType.STRING, true, fieldMetadataForColumn(3, \"d\")),\n          true,\n          fieldMetadataForColumn(2, \"b\"))\n        .add(\"a\", StringType.STRING, true, fieldMetadataForColumn(1, \"a\"))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val innerStruct = currentSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", new StringType(utf8Lcase), true, innerStruct.get(\"d\").getMetadata),\n          true,\n          currentSchema.get(\"b\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n      val updatedInnerStruct = updatedSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n      val updatedDType = updatedInnerStruct.get(\"d\").getDataType.asInstanceOf[StringType]\n      assert(updatedDType.getCollationIdentifier == utf8Lcase)\n      // Ensure IDs/physical names preserved\n      assertColumnMapping(updatedInnerStruct.get(\"d\"), 3, \"d\")\n    }\n  }\n\n  test(\"Change collation of STRING partition column succeeds\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"partition1\", StringType.STRING, true)\n        .add(\"data\", StringType.STRING, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"partition1\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\n          \"partition1\",\n          new StringType(utf8Lcase),\n          true,\n          currentSchema.get(\"partition1\").getMetadata)\n        .add(\"data\", StringType.STRING, true, currentSchema.get(\"data\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n      val updatedPartitionType =\n        updatedSchema.get(\"partition1\").getDataType.asInstanceOf[StringType]\n      assert(updatedPartitionType.getCollationIdentifier == utf8Lcase)\n      // Verify ordering preserved and no unintended changes\n      val topLevelFields = updatedSchema.fieldNames().asScala\n      assert(topLevelFields == Array(\"partition1\", \"data\").toSeq)\n    }\n  }\n\n  test(\"Drop column succeeds\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n        .add(\"d\", new StringType(utf8Lcase), true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n      assertColumnMapping(table.getLatestSnapshot(engine).getSchema.get(\"c\"), 2)\n      assertColumnMapping(table.getLatestSnapshot(engine).getSchema.get(\"d\"), 3)\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assert(getMaxFieldId(engine, tablePath) == 3)\n    }\n  }\n\n  test(\"Rename fields\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true)\n            .add(\"e\", IntegerType.INTEGER, true),\n          true)\n        .add(\"c\", IntegerType.INTEGER, true)\n        .add(\"s\", new StringType(utf8Lcase), true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n\n      val innerStruct = currentSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"renamed-d\", IntegerType.INTEGER, true, innerStruct.get(\"d\").getMetadata)\n            .add(\"e\", IntegerType.INTEGER, true, innerStruct.get(\"e\").getMetadata),\n          true,\n          currentSchema.get(\"b\").getMetadata)\n        .add(\"renamed-c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n        .add(\"renamed-s\", new StringType(utf8Lcase), true, currentSchema.get(\"s\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(updatedSchema.get(\"a\"), 1)\n\n      val updatedInnerStruct = updatedSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n      assertColumnMapping(updatedInnerStruct.get(\"renamed-d\"), 3)\n      assertColumnMapping(updatedInnerStruct.get(\"e\"), 4)\n      assertColumnMapping(updatedSchema.get(\"renamed-c\"), 5)\n      assertColumnMapping(updatedSchema.get(\"renamed-s\"), 6)\n      val renamedSType = updatedSchema.get(\"renamed-s\").getDataType.asInstanceOf[StringType]\n      assert(renamedSType.getCollationIdentifier == utf8Lcase)\n    }\n  }\n\n  test(\"Move fields\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true)\n            .add(\"e\", IntegerType.INTEGER, true)\n            .add(\"s\", new StringType(utf8Lcase), true),\n          true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n\n      val innerStruct = currentSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"s\", new StringType(utf8Lcase), true, innerStruct.get(\"s\").getMetadata)\n            .add(\"e\", IntegerType.INTEGER, true, innerStruct.get(\"e\").getMetadata)\n            .add(\"d\", IntegerType.INTEGER, true, innerStruct.get(\"d\").getMetadata),\n          true,\n          currentSchema.get(\"b\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(updatedSchema.get(\"a\"), 1)\n\n      val updatedInnerStruct = updatedSchema.get(\"b\").getDataType.asInstanceOf[StructType]\n      assertColumnMapping(updatedInnerStruct.get(\"d\"), 3)\n      assertColumnMapping(updatedInnerStruct.get(\"e\"), 4)\n      assertColumnMapping(updatedInnerStruct.get(\"s\"), 5)\n      val nestedSType = updatedInnerStruct.get(\"s\").getDataType.asInstanceOf[StringType]\n      assert(nestedSType.getCollationIdentifier == utf8Lcase)\n      assertColumnMapping(updatedSchema.get(\"c\"), 6)\n\n      // Verify the top level and nested field reordering is maintained\n      val topLevelFields = updatedSchema.fieldNames().asScala\n      assert(topLevelFields == Array(\"a\", \"c\", \"b\").toSeq)\n      val innerFields = updatedInnerStruct.fieldNames().asScala\n      assert(innerFields == Array(\"s\", \"e\", \"d\").toSeq)\n    }\n  }\n\n  test(\"Updating schema with adding an array and map type\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"arr\",\n          new ArrayType(StringType.STRING, false),\n          true,\n          fieldMetadataForArrayColumn(2, \"arr\", \"arr\", 3))\n        .add(\n          \"map\",\n          new MapType(StringType.STRING, StringType.STRING, false),\n          true,\n          fieldMetadataForMapColumn(4, \"map\", \"map\", 5, 6))\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"arr\"), 2, \"arr\")\n      assertColumnMapping(structType.get(\"map\"), 4, \"map\")\n      assert(structType.get(\"arr\").getMetadata.get(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        == FieldMetadata.builder().putLong(\"arr.element\", 3).build())\n      assert(structType.get(\"map\").getMetadata.get(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        == FieldMetadata.builder().putLong(\"map.key\", 5).putLong(\"map.value\", 6).build())\n    }\n  }\n\n  test(\"Updating schema with adding an array and map type with collated strings\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"arr\",\n          new ArrayType(new StringType(utf8Lcase), false),\n          true,\n          fieldMetadataForArrayColumn(2, \"arr\", \"arr\", 3))\n        .add(\n          \"map\",\n          new MapType(StringType.STRING, new StringType(utf8Lcase), false),\n          true,\n          fieldMetadataForMapColumn(4, \"map\", \"map\", 5, 6))\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"arr\"), 2, \"arr\")\n      assertColumnMapping(structType.get(\"map\"), 4, \"map\")\n      assert(structType.get(\"arr\").getMetadata.get(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        == FieldMetadata.builder().putLong(\"arr.element\", 3).build())\n      assert(structType.get(\"map\").getMetadata.get(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        == FieldMetadata.builder().putLong(\"map.key\", 5).putLong(\"map.value\", 6).build())\n    }\n  }\n\n  test(\"Add map whose values are array of struct\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType().add(\n                \"nested_map_value\",\n                IntegerType.INTEGER,\n                fieldMetadataForColumn(3, \"some-physical-column\")),\n              true),\n            false),\n          true,\n          fieldMetadataForMapColumn(4, \"map\", \"map\", 5, 6))\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val latestSnapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      val structType = latestSnapshot.getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 4, \"map\")\n      assert(structType.get(\"map\").getMetadata.get(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        == FieldMetadata.builder()\n          .putLong(\"map.key\", 5)\n          .putLong(\"map.value\", 6)\n          .putLong(\"map.value.element\", 7)\n          .build())\n      val configuration = latestSnapshot.getMetadata.getConfiguration\n      assert(configuration.get(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY) == \"7\")\n    }\n  }\n\n  test(\"Drop nested struct field in map<int, array<struct>>\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType().add(\"field\", IntegerType.INTEGER)\n                .add(\"field_to_drop\", IntegerType.INTEGER),\n              true),\n            false),\n          true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val arrayValue = mapSchema.getValueType.asInstanceOf[ArrayType]\n      val innerStruct = arrayValue.getElementType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType()\n                .add(\"field\", IntegerType.INTEGER, innerStruct.get(\"field\").getMetadata),\n              true),\n            false),\n          true,\n          currentSchema.get(\"map\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 2)\n    }\n  }\n\n  test(\"Add nested struct field to map<int, array<struct>>\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType().add(\"field\", IntegerType.INTEGER),\n              true),\n            false),\n          true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val arrayValue = mapSchema.getValueType.asInstanceOf[ArrayType]\n      val innerStruct = arrayValue.getElementType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType()\n                .add(\"field\", IntegerType.INTEGER, innerStruct.get(\"field\").getMetadata)\n                .add(\n                  \"field_to_add\",\n                  IntegerType.INTEGER,\n                  fieldMetadataForColumn(7, \"field_to_add\")),\n              true),\n            false),\n          true,\n          currentSchema.get(\"map\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 2)\n\n      val mapType = structType.get(\"map\").getDataType.asInstanceOf[MapType]\n      val updatedArrayValue = mapType.getValueField.getDataType.asInstanceOf[ArrayType]\n      val updatedInnerStruct = updatedArrayValue.getElementType.asInstanceOf[StructType]\n\n      assertColumnMapping(updatedInnerStruct.get(\"field_to_add\"), 7, \"field_to_add\")\n\n    }\n  }\n\n  test(\"Renaming clustering columns\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"clustering-col\", StringType.STRING, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        clusteringColsOpt = Some(List(new Column(\"clustering-col\"))),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val expectedSchema = new StructType()\n        .add(\n          \"renamed-clustering-col\",\n          StringType.STRING,\n          true,\n          currentSchema.get(\"clustering-col\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, expectedSchema)\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      val actualSchema = snapshot.getSchema\n\n      assert(expectedSchema == actualSchema)\n    }\n  }\n\n  test(\"Renaming collated clustering columns\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"clustering-col\", new StringType(utf8Lcase), true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        clusteringColsOpt = Some(List(new Column(\"clustering-col\"))),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val expectedSchema = new StructType()\n        .add(\n          \"renamed-clustering-col\",\n          new StringType(utf8Lcase),\n          true,\n          currentSchema.get(\"clustering-col\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, expectedSchema)\n\n      val snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      val actualSchema = snapshot.getSchema\n\n      assert(expectedSchema == actualSchema)\n\n      val renamedType =\n        actualSchema.get(\"renamed-clustering-col\").getDataType.asInstanceOf[StringType]\n      assert(renamedType.getCollationIdentifier == utf8Lcase)\n    }\n  }\n\n  test(\"Add nested array field to map<int, struct> with already assigned IDs\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType().add(\"field\", IntegerType.INTEGER),\n            false),\n          true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val innerStruct = mapSchema.getValueType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType()\n              .add(\"field\", IntegerType.INTEGER, innerStruct.get(\"field\").getMetadata)\n              .add(\n                \"array_field_to_add\",\n                new ArrayType(IntegerType.INTEGER, true),\n                FieldMetadata.builder()\n                  .putFieldMetadata(\n                    ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY,\n                    FieldMetadata.builder().putLong(\"array_field_to_add\", 7).build())\n                  .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 6)\n                  .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"array_field_to_add\")\n                  .build()),\n            false),\n          true,\n          currentSchema.get(\"map\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 2)\n      val mapType = structType.get(\"map\").getDataType.asInstanceOf[MapType]\n      val updatedInnerStruct = mapType.getValueType.asInstanceOf[StructType]\n\n      assertColumnMapping(updatedInnerStruct.get(\"array_field_to_add\"), 6, \"array_field_to_add\")\n    }\n  }\n\n  test(\"Add nested array field to map<int, struct> with fresh IDs\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType().add(\"field\", IntegerType.INTEGER),\n            false),\n          true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val innerStruct = mapSchema.getValueType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType()\n              .add(\n                \"array_field_to_add\",\n                new ArrayType(IntegerType.INTEGER, true))\n              .add(\"field\", IntegerType.INTEGER, innerStruct.get(\"field\").getMetadata),\n            false),\n          true,\n          currentSchema.get(\"map\").getMetadata)\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 2)\n      val mapType = structType.get(\"map\").getDataType.asInstanceOf[MapType]\n      val updatedInnerStruct = mapType.getValueType.asInstanceOf[StructType]\n\n      assertColumnMapping(updatedInnerStruct.get(\"array_field_to_add\"), 6, \"array_field_to_add\")\n    }\n  }\n\n  test(\"Drop nested array field in map<int, struct>\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType().add(\"field\", IntegerType.INTEGER)\n              .add(\"array_field_to_drop\", new ArrayType(IntegerType.INTEGER, true)),\n            false),\n          true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val innerStruct = mapSchema.getValueType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType()\n              .add(\"field\", IntegerType.INTEGER, innerStruct.get(\"field\").getMetadata),\n            false),\n          true,\n          currentSchema.get(\"map\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 2)\n      val mapType = structType.get(\"map\").getDataType.asInstanceOf[MapType]\n      val updatedInnerStruct = mapType.getValueType.asInstanceOf[StructType]\n\n      assert(updatedInnerStruct == innerStruct.withoutField(\"array_field_to_drop\"))\n    }\n  }\n\n  test(\"Rename nested array field in map<int, struct>\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType().add(\"field\", IntegerType.INTEGER)\n              .add(\"array_field_to_rename\", new ArrayType(IntegerType.INTEGER, true)),\n            false),\n          true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val innerStruct = mapSchema.getValueType.asInstanceOf[StructType]\n\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new StructType()\n              .add(\"field\", IntegerType.INTEGER, innerStruct.get(\"field\").getMetadata)\n              .add(\n                \"renamed_array_field\",\n                new ArrayType(IntegerType.INTEGER, true),\n                innerStruct.get(\"array_field_to_rename\").getMetadata),\n            false),\n          true,\n          currentSchema.get(\"map\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n      assertColumnMapping(structType.get(\"map\"), 2)\n      val mapType = structType.get(\"map\").getDataType.asInstanceOf[MapType]\n      val updatedInnerStruct = mapType.getValueType.asInstanceOf[StructType]\n\n      assert(updatedInnerStruct.get(\"renamed_array_field\").getDataType\n        == innerStruct.get(\"array_field_to_rename\").getDataType)\n      assert(updatedInnerStruct.get(\"renamed_array_field\").getMetadata\n        == innerStruct.get(\"array_field_to_rename\").getMetadata)\n    }\n  }\n\n  test(\"Adding struct of structs\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\n              \"d\",\n              new StructType().add(\"e\", IntegerType.INTEGER, fieldMetadataForColumn(5, \"e\")),\n              true,\n              fieldMetadataForColumn(4, \"d\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val structType = table.getLatestSnapshot(engine).getSchema\n      assertColumnMapping(structType.get(\"a\"), 1)\n\n      val firstInnerStruct = structType.get(\"b\").getDataType.asInstanceOf[StructType]\n      assertColumnMapping(firstInnerStruct.get(\"d\"), 4, \"d\")\n\n      val secondInnerStruct = firstInnerStruct.get(\"d\").getDataType.asInstanceOf[StructType]\n      assertColumnMapping(secondInnerStruct.get(\"e\"), 5, \"e\")\n    }\n  }\n\n  test(\"Add array of arrays\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"array_of_arrays\",\n          new ArrayType(new ArrayType(IntegerType.INTEGER, true), true),\n          true,\n          FieldMetadata.builder()\n            .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"array_of_arrays\")\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 4L)\n            .putFieldMetadata(\n              ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY,\n              FieldMetadata.builder().putLong(\"array_of_arrays.element\", 2L)\n                .putLong(\"array_of_arrays.element.element\", 3L).build()).build())\n\n      updateTableMetadata(engine, tablePath, newSchema)\n\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema()\n\n      assertColumnMapping(updatedSchema.get(\"a\"), 1)\n      assertColumnMapping(updatedSchema.get(\"array_of_arrays\"), 4L, \"array_of_arrays\")\n\n      val arrayMetadata = updatedSchema.get(\"array_of_arrays\").getMetadata\n      assert(arrayMetadata.getMetadata(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        .getLong(\"array_of_arrays.element\") == 2L)\n      assert(arrayMetadata.getMetadata(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n        .getLong(\"array_of_arrays.element.element\") == 3L)\n    }\n  }\n\n  test(\"Changing column mapping on table and evolve schema at same time fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      // Create a table initially without column mapping\n      createEmptyTable(engine, tablePath, initialSchema)\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema()\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true, fieldMetadataForColumn(4, \"d\"))\n            .add(\"e\", IntegerType.INTEGER, true, fieldMetadataForColumn(5, \"e\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n        // Add a new collated STRING field\n        .add(\"s\", new StringType(utf8Lcase), true, fieldMetadataForColumn(6, \"s\"))\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot update mapping mode and perform schema evolution\",\n        Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n    }\n  }\n\n  test(\"Updating schema on table when column mapping disabled fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n        .add(\"s\", new StringType(utf8Lcase), true)\n\n      createEmptyTable(engine, tablePath, initialSchema, tableProperties = Map.empty)\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true, fieldMetadataForColumn(4, \"d\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot update schema for table when column mapping is disabled\")\n    }\n  }\n\n  test(\"Move partition columns\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"partition1\", StringType.STRING, true)\n        .add(\"partition2\", IntegerType.INTEGER, true)\n        .add(\"partition3\", new StringType(utf8Lcase), true)\n        .add(\"data\", StringType.STRING, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"partition1\", \"partition2\", \"partition3\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"partition2\", IntegerType.INTEGER, true, currentSchema.get(\"partition2\").getMetadata)\n        .add(\n          \"partition3\",\n          new StringType(utf8Lcase),\n          true,\n          currentSchema.get(\"partition3\").getMetadata)\n        .add(\"partition1\", StringType.STRING, true, currentSchema.get(\"partition1\").getMetadata)\n        .add(\"data\", StringType.STRING, true, currentSchema.get(\"data\").getMetadata)\n\n      updateTableMetadata(engine, tablePath, newSchema)\n      val updatedSchema = table.getLatestSnapshot(engine).getSchema\n\n      // Verify the ordering is expected\n      val topLevelFields = updatedSchema.fieldNames().asScala\n      assert(topLevelFields == Array(\"partition2\", \"partition3\", \"partition1\", \"data\").toSeq)\n      val p3Type = updatedSchema.get(\"partition3\").getDataType.asInstanceOf[StringType]\n      assert(p3Type.getCollationIdentifier == utf8Lcase)\n    }\n  }\n\n  test(\"Updating schema with duplicate field IDs fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"duplicate_field_id\", IntegerType.INTEGER, true, fieldMetadataForColumn(1, \"d\"))\n            .add(\"e\", IntegerType.INTEGER, true, fieldMetadataForColumn(5, \"e\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Field duplicate_field_id with id 1 already exists\")\n    }\n  }\n\n  test(\"Adding non-nullable field fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"non_nullable_field\", IntegerType.INTEGER, false, fieldMetadataForColumn(4, \"d\"))\n            .add(\"e\", IntegerType.INTEGER, true, fieldMetadataForColumn(5, \"e\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n        .add(\"s\", new StringType(utf8Lcase), true, fieldMetadataForColumn(6, \"s\"))\n        .add(\"c\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot add non-nullable field non_nullable_field\")\n    }\n  }\n\n  test(\"Adding non-nullable field to map value which is a struct fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType().add(\"nested_map_value\", IntegerType.INTEGER),\n              true),\n            false))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val mapSchema = currentSchema.get(\"map\").getDataType.asInstanceOf[MapType]\n      val arrayValue = mapSchema.getValueType.asInstanceOf[ArrayType]\n      val innerStruct = arrayValue.getElementType.asInstanceOf[StructType]\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"map\",\n          new MapType(\n            StringType.STRING,\n            new ArrayType(\n              new StructType().add(\n                \"nested_map_value\",\n                IntegerType.INTEGER,\n                innerStruct.get(\"nested_map_value\").getMetadata)\n                .add(\n                  \"new_required_field\",\n                  IntegerType.INTEGER,\n                  false,\n                  fieldMetadataForColumn(7, \"7\")),\n              true),\n            false),\n          true,\n          fieldMetadataForMapColumn(\n            2,\n            ColumnMapping.getPhysicalName(currentSchema.get(\"map\")),\n            \"map\",\n            4,\n            5))\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot add non-nullable field new_required_field\")\n    }\n  }\n\n  test(\"Cannot drop a partition column\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"c\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true, fieldMetadataForColumn(4, \"d\"))\n            .add(\"e\", IntegerType.INTEGER, true, fieldMetadataForColumn(5, \"e\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Partition column c not found in the schema\")\n    }\n  }\n\n  test(\"Cannot drop a collated partition column\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"p\", new StringType(utf8Lcase), true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"p\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"b\",\n          new StructType()\n            .add(\"d\", IntegerType.INTEGER, true, fieldMetadataForColumn(4, \"d\"))\n            .add(\"e\", IntegerType.INTEGER, true, fieldMetadataForColumn(5, \"e\")),\n          true,\n          fieldMetadataForColumn(3, \"b\"))\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Partition column p not found in the schema\")\n    }\n  }\n\n  test(\"Cannot rename a partition column\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"c\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\"e\", IntegerType.INTEGER, true, currentSchema.get(\"c\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Partition column c not found in the schema\")\n    }\n  }\n\n  test(\"Cannot rename a collated partition column\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"p\", new StringType(utf8Lcase), true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"p\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\n          \"a\",\n          StringType.STRING,\n          true,\n          currentSchema.get(\"a\").getMetadata\n        ) // currentSchema.get(\"p\").getMetadata\n        .add(\"q\", new StringType(utf8Lcase), true, currentSchema.get(\"p\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Partition column p not found in the schema\")\n    }\n  }\n\n  test(\"Cannot change types\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n        .add(\"c\", LongType.LONG, true, currentSchema.get(\"c\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot change the type of existing field c from integer to long\")\n    }\n  }\n\n  test(\"Cannot change clustering column type\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"clustering_col\", StringType.STRING, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        clusteringColsOpt = Some(List(new Column(\"clustering_col\"))),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"clustering_col\", LongType.LONG, true, currentSchema.get(\"clustering_col\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot change the type of existing field clustering_col from string to long\")\n    }\n  }\n\n  test(\"Updating schema if physical columns are not preserved fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\n          \"a\",\n          StringType.STRING,\n          true,\n          fieldMetadataForColumn(1, \"not-preserving-physical-column\"))\n        .add(\"c\", LongType.LONG, true, currentSchema.get(\"c\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Existing field with id 1 in current schema has physical name\")\n    }\n  }\n\n  test(\"Updating schema and tightening nullability on existing field fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"renamed_a\", IntegerType.INTEGER, false, currentSchema.get(\"a\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot tighten the nullability of existing field renamed_a\")\n    }\n  }\n\n  test(\"Cannot tighten nullability on renamed array element\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true)\n        .add(\n          \"arr\",\n          new ArrayType(StringType.STRING, true))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"a\", IntegerType.INTEGER, true, currentSchema.get(\"a\").getMetadata)\n        .add(\n          \"some_renamed_array\",\n          new ArrayType(StringType.STRING, false),\n          currentSchema.get(\"arr\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot tighten the nullability of existing field\")\n    }\n  }\n\n  test(\"Cannot change a partition column type\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        partCols = Seq(\"c\"),\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val currentSchema = table.getLatestSnapshot(engine).getSchema\n      val newSchema = new StructType()\n        .add(\"c\", StringType.STRING, true, currentSchema.get(\"c\").getMetadata)\n        .add(\"a\", StringType.STRING, true, currentSchema.get(\"a\").getMetadata)\n\n      assertSchemaEvolutionFails[IllegalArgumentException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot change the type of existing field c from integer to string\")\n    }\n  }\n\n  val primitiveSchemaWithClusteringColumn = new StructType()\n    .add(\n      \"clustering_col\",\n      IntegerType.INTEGER,\n      fieldMetadataForColumn(1, \"clustering_col_physical\"))\n    .add(\"data\", IntegerType.INTEGER, fieldMetadataForColumn(2, \"data_physical\"))\n\n  val nestedSchemaWithClusteringColumn = new StructType()\n    .add(\n      \"struct\",\n      new StructType()\n        .add(\n          \"clustering_col\",\n          IntegerType.INTEGER,\n          fieldMetadataForColumn(1, \"clustering_col_physical\"))\n        .add(\"data\", IntegerType.INTEGER, fieldMetadataForColumn(2, \"data_physical\")),\n      true,\n      fieldMetadataForColumn(3, \"struct_physical\"))\n\n  private val updatedSchemaWithDroppedClusteringColumn = Tables.Table(\n    (\"schemaBefore\", \"updatedSchemaWithDroppedClusteringColumn\", \"clusteringColumn\"),\n    (\n      primitiveSchemaWithClusteringColumn,\n      new StructType()\n        .add(\n          \"data\",\n          IntegerType.INTEGER,\n          true,\n          primitiveSchemaWithClusteringColumn.get(\"data\").getMetadata),\n      new Column(\"clustering_col\")),\n    (\n      nestedSchemaWithClusteringColumn,\n      new StructType()\n        .add(\n          \"struct\",\n          new StructType()\n            .add(\n              \"data\",\n              IntegerType.INTEGER,\n              nestedSchemaWithClusteringColumn.get(\"struct\").getDataType\n                .asInstanceOf[StructType].get(\"data\").getMetadata),\n          true,\n          nestedSchemaWithClusteringColumn.get(\"struct\").getMetadata),\n      new Column(Array(\"struct\", \"clustering_col\"))),\n    (\n      nestedSchemaWithClusteringColumn,\n      new StructType().add(\"id\", IntegerType.INTEGER, fieldMetadataForColumn(4, \"id\")),\n      new Column(Array(\"struct\", \"clustering_col\"))))\n\n  test(\"Cannot drop clustering column\") {\n    forAll(updatedSchemaWithDroppedClusteringColumn) {\n      (schemaBefore, schemaAfter, clusteringColumn) =>\n        withTempDirAndEngine { (tablePath, engine) =>\n          val table = Table.forPath(engine, tablePath)\n          createEmptyTable(\n            engine,\n            tablePath,\n            schemaBefore,\n            clusteringColsOpt = Some(List(clusteringColumn)),\n            tableProperties = Map(\n              TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n              TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n          assertSchemaEvolutionFails[KernelException](\n            table,\n            engine,\n            schemaAfter,\n            \"Cannot drop clustering column clustering_col\")\n        }\n    }\n  }\n\n  test(\"Updating schema should use the new clustering columns if passed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val initialSchema = new StructType()\n        .add(\"a\", StringType.STRING, true)\n        .add(\"b\", StringType.STRING, true)\n        .add(\"c\", IntegerType.INTEGER, true)\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"),\n        clusteringColsOpt = Some(List(new Column(\"b\"))))\n      assertColumnMapping(table.getLatestSnapshot(engine).getSchema.get(\"c\"), 3)\n\n      val newSchema = new StructType()\n        .add(\"d\", StringType.STRING, true)\n\n      updateTableMetadata(\n        engine,\n        tablePath,\n        newSchema,\n        clusteringColsOpt = Some(List(new Column(\"d\"))))\n\n      val snapshot = table.getLatestSnapshot(engine)\n      val structType = snapshot.getSchema\n      assertColumnMapping(structType.get(\"d\"), 4, \"d\")\n      assert(getMaxFieldId(engine, tablePath) == 4)\n\n      val physicalName =\n        structType.get(\"d\").getMetadata.get(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n      val expectedDomainMetadata = new DomainMetadata(\n        \"delta.clustering\",\n        s\"\"\"{\"clusteringColumns\":[[\"$physicalName\"]]}\"\"\",\n        false)\n\n      verifyClusteringDomainMetadata(snapshot.asInstanceOf[SnapshotImpl], expectedDomainMetadata)\n    }\n  }\n\n  test(\"Cannot move field from nested struct to top-level\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val nestedSchema = new StructType()\n        .add(\"nestedCol1\", StringType.STRING, fieldMetadataForColumn(1, \"col-1\"))\n        .add(\"nestedCol2\", StringType.STRING, fieldMetadataForColumn(2, \"col-2\"))\n      val initialSchema = new StructType()\n        .add(\"topCol1\", nestedSchema, fieldMetadataForColumn(3, \"col-3\"))\n        .add(\"topCol2\", IntegerType.INTEGER, fieldMetadataForColumn(4, \"col-4\"))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val newNestedSchema = new StructType()\n        .add(\"nestedCol1\", StringType.STRING, fieldMetadataForColumn(1, \"col-1\"))\n      val newSchema = new StructType()\n        .add(\"topCol1\", newNestedSchema, fieldMetadataForColumn(3, \"col-3\"))\n        .add(\"topCol2\", IntegerType.INTEGER, fieldMetadataForColumn(4, \"col-4\"))\n        .add(\"nestedCol2\", StringType.STRING, fieldMetadataForColumn(2, \"col-2\"))\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot move fields between different levels of nesting\")\n    }\n  }\n\n  test(\"Cannot move field between sibling structs\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val struct1 = new StructType()\n        .add(\"field1\", StringType.STRING, fieldMetadataForColumn(1, \"col-1\"))\n        .add(\"field2\", IntegerType.INTEGER, fieldMetadataForColumn(2, \"col-2\"))\n      val struct2 = new StructType()\n        .add(\"field3\", StringType.STRING, fieldMetadataForColumn(3, \"col-3\"))\n      val initialSchema = new StructType()\n        .add(\"struct1\", struct1, fieldMetadataForColumn(4, \"col-4\"))\n        .add(\"struct2\", struct2, fieldMetadataForColumn(5, \"col-5\"))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val newStruct1 = new StructType()\n        .add(\"field2\", IntegerType.INTEGER, fieldMetadataForColumn(2, \"col-2\"))\n      val newStruct2 = new StructType()\n        .add(\"field1\", StringType.STRING, fieldMetadataForColumn(1, \"col-1\"))\n        .add(\"field3\", StringType.STRING, fieldMetadataForColumn(3, \"col-3\"))\n      val newSchema = new StructType()\n        .add(\"struct1\", newStruct1, fieldMetadataForColumn(4, \"col-4\"))\n        .add(\"struct2\", newStruct2, fieldMetadataForColumn(5, \"col-5\"))\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot move fields between different levels of nesting\")\n    }\n  }\n\n  test(\"Cannot move field from array element to top-level\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val arrayElementStruct = new StructType()\n        .add(\"elemField1\", IntegerType.INTEGER, fieldMetadataForColumn(1, \"col-1\"))\n        .add(\"elemField2\", StringType.STRING, fieldMetadataForColumn(2, \"col-2\"))\n      val initialSchema = new StructType()\n        .add(\n          \"arrayCol\",\n          new ArrayType(arrayElementStruct, true),\n          fieldMetadataForColumn(3, \"col-3\"))\n        .add(\"topField\", IntegerType.INTEGER, fieldMetadataForColumn(4, \"col-4\"))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val newArrayElementStruct = new StructType()\n        .add(\"elemField2\", StringType.STRING, fieldMetadataForColumn(2, \"col-2\"))\n      val newSchema = new StructType()\n        .add(\n          \"arrayCol\",\n          new ArrayType(newArrayElementStruct, true),\n          fieldMetadataForColumn(3, \"col-3\"))\n        .add(\"topField\", IntegerType.INTEGER, fieldMetadataForColumn(4, \"col-4\"))\n        .add(\"elemField1\", IntegerType.INTEGER, fieldMetadataForColumn(1, \"col-1\"))\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot move fields between different levels of nesting\")\n    }\n  }\n\n  test(\"Cannot move field from map value to map key\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val mapKeyStruct = new StructType()\n        .add(\"keyField1\", StringType.STRING, fieldMetadataForColumn(1, \"col-1\"))\n      val mapValueStruct = new StructType()\n        .add(\"valueField1\", IntegerType.INTEGER, fieldMetadataForColumn(2, \"col-2\"))\n        .add(\"valueField2\", StringType.STRING, fieldMetadataForColumn(3, \"col-3\"))\n      val initialSchema = new StructType()\n        .add(\n          \"mapCol\",\n          new MapType(mapKeyStruct, mapValueStruct, true),\n          fieldMetadataForColumn(4, \"col-4\"))\n\n      createEmptyTable(\n        engine,\n        tablePath,\n        initialSchema,\n        tableProperties = Map(\n          TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n          TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n      val newMapKeyStruct = new StructType()\n        .add(\"keyField1\", StringType.STRING, fieldMetadataForColumn(1, \"col-1\"))\n        .add(\"valueField1\", IntegerType.INTEGER, fieldMetadataForColumn(2, \"col-2\"))\n      val newMapValueStruct = new StructType()\n        .add(\"valueField2\", StringType.STRING, fieldMetadataForColumn(3, \"col-3\"))\n      val newSchema = new StructType()\n        .add(\n          \"mapCol\",\n          new MapType(newMapKeyStruct, newMapValueStruct, true),\n          fieldMetadataForColumn(4, \"col-4\"))\n\n      assertSchemaEvolutionFails[KernelException](\n        table,\n        engine,\n        newSchema,\n        \"Cannot move fields between different levels of nesting\")\n    }\n  }\n\n  private val typeWideningTestCases = Tables.Table(\n    (\n      \"testName\",\n      \"initialType\",\n      \"newType\",\n      \"typeWideningEnabled\",\n      \"icebergV1Enabled\",\n      \"shouldSucceed\",\n      \"errorMessageFragment\"),\n    (\n      \"Integer widening (Int -> Long) with type widening enabled\",\n      IntegerType.INTEGER,\n      LongType.LONG,\n      /* typeWideningEnabled= */ true,\n      /* icebergV1Enabled= */ false,\n      /* shouldSucceed= */ true,\n      \"\"),\n    (\n      \"Integer widening (Int -> Long) with type widening disabled\",\n      IntegerType.INTEGER,\n      LongType.LONG,\n      /* typeWideningEnabled= */ false,\n      /* icebergV1Enabled= */ false,\n      /* shouldSucceed= */ false,\n      \"Cannot change the type of existing field id from integer to long\"),\n    (\n      \"Decimal precision and scale increase\",\n      new DecimalType(10, 2),\n      new DecimalType(15, 5),\n      /* typeWideningEnabled= */ true,\n      /* icebergV1Enabled= */ false,\n      /* shouldSucceed= */ true,\n      \"\"),\n    (\n      \"Decimal precision and scale increase with Iceberg V1 compatibility\",\n      new DecimalType(10, 2),\n      new DecimalType(15, 5),\n      /* typeWideningEnabled= */ true,\n      /* icebergV1Enabled= */ true,\n      /* shouldSucceed= */ false,\n      \"Cannot change the type of existing field id\"))\n\n  forAll(typeWideningTestCases) {\n    (\n        testName,\n        initialType,\n        newType,\n        typeWideningEnabled,\n        icebergV1Enabled,\n        shouldSucceed,\n        errorMessageFragment) =>\n      test(s\"Type widening scenarios $testName\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          val table = Table.forPath(engine, tablePath)\n          val initialSchema = new StructType()\n            .add(\"id\", initialType, true)\n            .add(\"data\", StringType.STRING, true)\n\n          createEmptyTable(\n            engine,\n            tablePath,\n            initialSchema,\n            tableProperties = Map(\n              TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\",\n              TableConfig.TYPE_WIDENING_ENABLED.getKey -> typeWideningEnabled.toString,\n              TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> icebergV1Enabled.toString,\n              TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\"))\n\n          val currentSchema = table.getLatestSnapshot(engine).getSchema()\n          val newSchema = new StructType()\n            .add(\"id\", newType, true, currentSchema.get(\"id\").getMetadata)\n            .add(\"data\", StringType.STRING, true, currentSchema.get(\"data\").getMetadata)\n\n          if (shouldSucceed) {\n            // This should succeed because conditions allow type widening\n            updateTableMetadata(engine, tablePath, newSchema)\n\n            val updatedSchema = table.getLatestSnapshot(engine).getSchema\n            assert(updatedSchema.get(\"id\").getDataType == newType)\n            assert(updatedSchema.get(\"id\").getTypeChanges.asScala ==\n              List(new TypeChange(initialType, newType)))\n\n            // Do an unrelated schema change. And ensure type change and type changes\n            // are still present.\n            updateTableMetadata(\n              engine,\n              tablePath,\n              newSchema.add(\"newField\", StringType.STRING, true))\n\n            val lastSchema = table.getLatestSnapshot(engine).getSchema\n            assert(lastSchema.get(\"id\").getDataType == newType)\n            assert(lastSchema.get(\"id\").getTypeChanges.asScala ==\n              List(new TypeChange(initialType, newType)))\n          } else {\n            // This should fail because conditions don't allow type widening\n            assertSchemaEvolutionFails[KernelException](\n              table,\n              engine,\n              newSchema,\n              errorMessageFragment)\n          }\n        }\n      }\n  }\n\n  def fieldMetadataForColumn(\n      columnId: Long,\n      physicalColumnId: String): FieldMetadata = {\n    FieldMetadata.builder()\n      .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, columnId)\n      .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, physicalColumnId)\n      .build()\n  }\n\n  def fieldMetadataForArrayColumn(\n      columnId: Long,\n      physicalColumnId: String,\n      arrayFieldName: String,\n      nestedElementId: Long): FieldMetadata = {\n    FieldMetadata.builder()\n      .fromMetadata(fieldMetadataForColumn(columnId, physicalColumnId))\n      .putFieldMetadata(\n        ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY,\n        FieldMetadata.builder().putLong(s\"$arrayFieldName.element\", nestedElementId).build())\n      .build()\n  }\n\n  def fieldMetadataForMapColumn(\n      columnId: Long,\n      physicalColumnId: String,\n      mapFieldName: String,\n      keyId: Long,\n      valueId: Long): FieldMetadata = {\n    FieldMetadata.builder()\n      .fromMetadata(fieldMetadataForColumn(columnId, physicalColumnId))\n      .putFieldMetadata(\n        ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY,\n        FieldMetadata.builder().putLong(s\"$mapFieldName.key\", keyId)\n          .putLong(s\"$mapFieldName.value\", valueId).build())\n      .build()\n  }\n\n  private def assertSchemaEvolutionFails[T <: Throwable](\n      table: Table,\n      engine: Engine,\n      newSchema: StructType,\n      expectedMessageContained: String,\n      tableProperties: Map[String, String] = Map.empty): Unit = {\n    val e = intercept[Exception] {\n      updateTableMetadata(\n        engine,\n        table.getPath(engine),\n        newSchema,\n        tableProperties = tableProperties)\n    }\n\n    assert(e.isInstanceOf[T])\n    assert(e.getMessage.contains(expectedMessageContained))\n  }\n\n  private def getMaxFieldId(engine: Engine, tablePath: String): Long = {\n    TableConfig.COLUMN_MAPPING_MAX_COLUMN_ID\n      .fromMetadata(getMetadata(engine, tablePath))\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableWriteWithCrcSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.immutable.Seq\nimport scala.language.implicitConversions\n\nimport io.delta.kernel.{Transaction, TransactionCommitResult}\nimport io.delta.kernel.Snapshot.ChecksumWriteMode\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{TestRow, WriteUtils}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.CloseableIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Trait to mixin into a test suite that extends [[WriteUtils]] to run all the tests\n * with CRC file written after each commit and verify the written CRC files are valid.\n * Note, this requires the test suite uses [[commitTransaction]] and [[verifyWrittenContent]].\n */\ntrait WriteUtilsWithCrc extends AnyFunSuite with WriteUtils {\n  override def commitTransaction(\n      txn: Transaction,\n      engine: Engine,\n      dataActions: CloseableIterable[Row]): TransactionCommitResult = {\n    executeCrcSimple(txn.commit(engine, dataActions), engine)\n  }\n\n  override def verifyWrittenContent(\n      path: String,\n      expSchema: StructType,\n      expData: Seq[TestRow]): Unit = {\n    super.verifyWrittenContent(path, expSchema, expData)\n    verifyChecksum(path, expectEmptyTable = expData.isEmpty)\n  }\n}\n\n/**\n * Trait to mixin into a test suite that extends [[WriteUtils]] to use post-commit snapshots for\n * writing CRC files using the simple CRC write method. This ensures that the checksum write mode is\n * SIMPLE and uses the post-commit snapshot's writeChecksumSimple method. Note, this requires the\n * test suite uses [[commitTransaction]] and [[verifyWrittenContent]].\n */\ntrait WriteUtilsWithPostCommitSnapshotCrcSimpleWrite extends AnyFunSuite with WriteUtils {\n\n  override def commitTransaction(\n      txn: Transaction,\n      engine: Engine,\n      dataActions: CloseableIterable[Row]): TransactionCommitResult = {\n    val txnResult = txn.commit(engine, dataActions)\n\n    val postCommitSnapshot = txnResult\n      .getPostCommitSnapshot\n      .orElseThrow(() => new IllegalStateException(\"Required post-commit snapshot is missing\"))\n\n    postCommitSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n\n    txnResult\n  }\n\n  override def verifyWrittenContent(\n      path: String,\n      expSchema: StructType,\n      expData: Seq[TestRow]): Unit = {\n    super.verifyWrittenContent(path, expSchema, expData)\n    verifyChecksum(path, expectEmptyTable = expData.isEmpty)\n  }\n}\n\nclass DeltaTableWriteWithCrcSuite extends DeltaTableWritesSuite\n    with WriteUtilsWithCrc {}\n\nclass DeltaReplaceTableWithCrcSuite extends DeltaReplaceTableSuite\n    with WriteUtilsWithCrc {}\n\nclass DeltaTableWriteWithPostCommitSnapshotCrcSimpleSuite extends DeltaTableWritesSuite\n    with WriteUtilsWithPostCommitSnapshotCrcSimpleWrite {\n\n  // Tests to skip due to known limitation: post-commit snapshots are not yet built after conflicts,\n  // so we cannot write CRC files in those cases. See TransactionImpl.buildPostCommitSnapshotOpt.\n  // We use `lazy` due to ScalaTest's initialization order.\n  lazy val testsToSkip = Set(\n    \"create table and configure properties with retries\",\n    \"insert into table - idempotent writes\",\n    \"conflicts - concurrent data append (1) after the losing txn has started\",\n    \"conflicts - concurrent data append (5) after the losing txn has started\",\n    \"conflicts - concurrent data append (12) after the losing txn has started\")\n\n  override protected def test(\n      testName: String,\n      testTags: org.scalatest.Tag*)(\n      testFun: => Any)(implicit pos: org.scalactic.source.Position): Unit = {\n    if (testsToSkip.contains(testName)) {\n      ignore(testName, testTags: _*)(testFun)(pos)\n    } else {\n      super.test(testName, testTags: _*)(testFun)(pos)\n    }\n  }\n}\n\nclass DeltaReplaceTableWithPostCommitSnapshotCrcSimpleSuite extends DeltaReplaceTableSuite\n    with WriteUtilsWithPostCommitSnapshotCrcSimpleWrite {}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableWritesSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.nio.file.Files\nimport java.util.{Locale, Optional}\n\nimport scala.collection.immutable.Seq\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel._\nimport io.delta.kernel.Operation.{CREATE_TABLE, MANUAL_UPDATE, WRITE}\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, FilteredColumnarBatch, Row}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.defaults.internal.data.vector.DefaultGenericVector\nimport io.delta.kernel.defaults.internal.data.vector.DefaultStructVector\nimport io.delta.kernel.defaults.internal.parquet.ParquetSuiteBase\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, TestRow, WriteUtils}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions._\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.expressions.Literal._\nimport io.delta.kernel.internal.{ScanImpl, SnapshotImpl, TableConfig}\nimport io.delta.kernel.internal.checkpoints.CheckpointerSuite.selectSingleElement\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe\nimport io.delta.kernel.internal.util.{Clock, JsonUtils}\nimport io.delta.kernel.internal.util.SchemaUtils.casePreservingPartitionColNames\nimport io.delta.kernel.shaded.com.fasterxml.jackson.databind.node.ObjectNode\nimport io.delta.kernel.transaction.DataLayoutSpec\nimport io.delta.kernel.types._\nimport io.delta.kernel.types.ByteType.BYTE\nimport io.delta.kernel.types.DateType.DATE\nimport io.delta.kernel.types.DecimalType\nimport io.delta.kernel.types.DoubleType.DOUBLE\nimport io.delta.kernel.types.FloatType.FLOAT\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.LongType.LONG\nimport io.delta.kernel.types.ShortType.SHORT\nimport io.delta.kernel.types.StringType.STRING\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.types.TimestampType.TIMESTAMP\nimport io.delta.kernel.utils.CloseableIterable\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\nimport io.delta.tables.DeltaTable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DeltaTableWritesSuite extends AbstractDeltaTableWritesSuite with WriteUtils\n\n/** Transaction commit in this suite IS REQUIRED TO use commitTransaction than .commit */\nabstract class AbstractDeltaTableWritesSuite extends AnyFunSuite with AbstractWriteUtils\n    with ParquetSuiteBase {\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Create table tests\n  ///////////////////////////////////////////////////////////////////////////\n\n  test(\"create table - provide no schema - expect failure\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txnBuilder = table.createTransactionBuilder(engine, testEngineInfo, CREATE_TABLE)\n\n      val ex = intercept[TableNotFoundException] {\n        txnBuilder.build(engine)\n      }\n      assert(ex.getMessage.contains(\"Must provide a new schema to write to a new table\"))\n    }\n  }\n\n  test(\"create table - provide partition columns but no schema - expect failure\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txnBuilder = table\n        .createTransactionBuilder(engine, testEngineInfo, CREATE_TABLE)\n        .withPartitionColumns(engine, Seq(\"part1\", \"part2\").asJava)\n\n      val ex = intercept[TableNotFoundException] {\n        txnBuilder.build(engine)\n      }\n      assert(ex.getMessage.contains(\"Must provide a new schema to write to a new table\"))\n    }\n  }\n\n  test(\"create table - table already exists at the location\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(txn, engine, emptyIterable())\n\n      {\n        intercept[TableAlreadyExistsException] {\n          table.createTransactionBuilder(engine, testEngineInfo, CREATE_TABLE)\n            .build(engine)\n        }\n      }\n\n      // Provide schema\n      {\n        intercept[TableAlreadyExistsException] {\n          table.createTransactionBuilder(engine, testEngineInfo, CREATE_TABLE)\n            .withSchema(engine, testSchema)\n            .build(engine)\n        }\n      }\n\n      // Provide partition columns\n      {\n        intercept[TableAlreadyExistsException] {\n          table.createTransactionBuilder(engine, testEngineInfo, CREATE_TABLE)\n            .withSchema(engine, testPartitionSchema)\n            .withPartitionColumns(engine, testPartitionColumns.asJava)\n            .build(engine)\n        }\n      }\n    }\n  }\n\n  test(\"create table - table is concurrently created before txn commits\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn1 = getCreateTxn(engine, tablePath, testSchema)\n\n      val txn2 = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(txn2, engine, emptyIterable())\n\n      intercept[ConcurrentWriteException] {\n        commitTransaction(txn1, engine, emptyIterable())\n      }\n    }\n  }\n\n  test(\"cannot provide partition columns for existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(txn, engine, emptyIterable())\n\n      val ex = intercept[TableAlreadyExistsException] {\n        // Use operation != CREATE_TABLE since this fails earlier if the table already exists\n        table.createTransactionBuilder(engine, testEngineInfo, WRITE)\n          .withSchema(engine, testPartitionSchema)\n          .withPartitionColumns(engine, testPartitionColumns.asJava)\n          .build(engine)\n      }\n      assert(ex.getMessage.contains(\"Table already exists, but provided new partition columns.\" +\n        \" Partition columns can only be set on a new table.\"))\n    }\n  }\n\n  test(\"create table with metadata columns in the schema - expect failure\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val schemaWithMetadataCol =\n        testSchema.addMetadataColumn(\"_metadata.row_index\", MetadataColumnSpec.ROW_INDEX)\n\n      val ex = intercept[IllegalArgumentException] {\n        getCreateTxn(engine, tablePath, schemaWithMetadataCol)\n      }\n      assert(ex.getMessage.contains(\"Table schema cannot contain metadata columns\"))\n    }\n  }\n\n  test(\"create un-partitioned table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n\n      assert(txn.getSchema(engine) === testSchema)\n      assert(txn.getPartitionColumns(engine) === Seq.empty.asJava)\n      assert(txn.getReadTableVersion == -1)\n      val txnResult = commitTransaction(txn, engine, emptyIterable())\n\n      assert(txnResult.getVersion === 0)\n      assertCheckpointReadiness(txnResult, isReadyForCheckpoint = false)\n\n      verifyCommitInfo(tablePath = tablePath, version = 0)\n      verifyWrittenContent(tablePath, testSchema, Seq.empty)\n    }\n  }\n\n  test(\"create table and set properties\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      val txn1 = getCreateTxn(engine, tablePath, testSchema)\n\n      commitTransaction(txn1, engine, emptyIterable())\n\n      val ver0Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver0Snapshot, TableConfig.CHECKPOINT_INTERVAL, 10)\n\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        isNewTable = false,\n        key = TableConfig.CHECKPOINT_INTERVAL,\n        value = \"2\",\n        expectedValue = 2)\n    }\n  }\n\n  test(\"create table with properties and they should be retained\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        key = TableConfig.CHECKPOINT_INTERVAL,\n        value = \"2\",\n        expectedValue = 2)\n\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n      val ver1Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver1Snapshot, TableConfig.CHECKPOINT_INTERVAL, 2)\n    }\n  }\n\n  test(\"create table and configure properties with retries\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create table\n      val table = Table.forPath(engine, tablePath)\n      val txn0 = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(txn0, engine, emptyIterable())\n\n      // Create txn1 with config changes\n      val txn1 = getUpdateTxn(\n        engine,\n        tablePath,\n        tableProperties = Map(TableConfig.CHECKPOINT_INTERVAL.getKey -> \"2\"))\n      // Create and commit txn2\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      val ver1Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver1Snapshot, TableConfig.CHECKPOINT_INTERVAL, 10)\n\n      // Try to commit txn1\n      txn1.commit(engine, emptyIterable())\n\n      val ver2Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver2Snapshot, TableConfig.CHECKPOINT_INTERVAL, 2)\n    }\n  }\n\n  test(\"Setting retries to 0 disables retries\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create table\n      val table = Table.forPath(engine, tablePath)\n      val txn0 = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(txn0, engine, emptyIterable())\n\n      // Create txn1 with config changes\n      val txn1 = getUpdateTxn(\n        engine,\n        tablePath,\n        tableProperties = Map(TableConfig.CHECKPOINT_INTERVAL.getKey -> \"2\"),\n        maxRetries = 0)\n\n      // Create and commit txn2\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      val ver1Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver1Snapshot, TableConfig.CHECKPOINT_INTERVAL, 10)\n\n      // Try to commit txn1 but expect failure\n      intercept[MaxCommitRetryLimitReachedException] {\n        txn1.commit(engine, emptyIterable())\n      }\n\n      // check that we're still set to 10\n      val ver2Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver2Snapshot, TableConfig.CHECKPOINT_INTERVAL, 10)\n    }\n  }\n\n  test(\"create table and configure the same properties\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        key = TableConfig.CHECKPOINT_INTERVAL,\n        value = \"2\",\n        expectedValue = 2)\n      assert(getMetadataActionFromCommit(engine, table, 0).isDefined)\n\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1),\n        tableProperties =\n          Map(TableConfig.CHECKPOINT_INTERVAL.getKey.toLowerCase(Locale.ROOT) -> \"2\"))\n      val ver1Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver1Snapshot, TableConfig.CHECKPOINT_INTERVAL, 2)\n      assert(getMetadataActionFromCommit(engine, table, 1).isEmpty)\n    }\n  }\n\n  test(\"create table and configure verifying that the case of the property is same as the one in\" +\n    \"TableConfig and not the one passed by the user.\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val table = Table.forPath(engine, tablePath)\n\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testSchema,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1),\n        tableProperties =\n          Map(TableConfig.CHECKPOINT_INTERVAL.getKey.toLowerCase(Locale.ROOT) -> \"2\"))\n\n      val ver0Snapshot = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(ver0Snapshot, TableConfig.CHECKPOINT_INTERVAL, 2)\n\n      val configurations = ver0Snapshot.getMetadata.getConfiguration\n      assert(configurations.containsKey(TableConfig.CHECKPOINT_INTERVAL.getKey))\n      assert(\n        !configurations.containsKey(\n          TableConfig.CHECKPOINT_INTERVAL.getKey.toLowerCase(Locale.ROOT)))\n    }\n  }\n\n  test(\"create partitioned table - partition column is not part of the schema\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val ex = intercept[IllegalArgumentException] {\n        getCreateTxn(\n          engine,\n          tablePath,\n          schema = testPartitionSchema,\n          partCols = Seq(\"PART1\", \"part3\"))\n      }\n      assert(ex.getMessage.contains(\"Partition column part3 not found in the schema\"))\n    }\n  }\n\n  test(\"create partitioned table - partition column type is not supported\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val schema = new StructType()\n        .add(\"p1\", new ArrayType(INTEGER, true))\n        .add(\"c1\", DATE)\n        .add(\"c2\", new DecimalType(14, 2))\n\n      val ex = intercept[KernelException] {\n        getCreateTxn(engine, tablePath, schema = schema, partCols = Seq(\"p1\", \"c1\"))\n      }\n      assert(ex.getMessage.contains(\n        \"Kernel doesn't support writing data with partition column (p1) of type: array[integer]\"))\n    }\n  }\n\n  test(\"create a partitioned table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val schema = new StructType()\n        .add(\"id\", INTEGER)\n        .add(\"Part1\", INTEGER) // partition column\n        .add(\"part2\", INTEGER) // partition column\n\n      val txn = getCreateTxn(\n        engine,\n        tablePath,\n        schema = schema,\n        partCols = Seq(\"part1\", \"PART2\"))\n\n      assert(txn.getSchema(engine) === schema)\n      // Expect the partition column name is exactly same as the one in the schema\n      assert(txn.getPartitionColumns(engine) === Seq(\"Part1\", \"part2\").asJava)\n      val txnResult = commitTransaction(txn, engine, emptyIterable())\n\n      assert(txnResult.getVersion === 0)\n      assertCheckpointReadiness(txnResult, isReadyForCheckpoint = false)\n\n      verifyCommitInfo(tablePath, version = 0, Seq(\"Part1\", \"part2\"))\n      verifyWrittenContent(tablePath, schema, Seq.empty)\n    }\n  }\n\n  Seq(true, false).foreach { includeTimestampNtz =>\n    test(s\"create table with all supported types - timestamp_ntz included=$includeTimestampNtz\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val parquetAllTypes = goldenTablePath(\"parquet-all-types\")\n        val goldenTableSchema = tableSchema(parquetAllTypes)\n        val schema = if (includeTimestampNtz) goldenTableSchema\n        else removeTimestampNtzTypeColumns(goldenTableSchema)\n\n        val txn = getCreateTxn(engine, tablePath, schema = schema)\n        val txnResult = commitTransaction(txn, engine, emptyIterable())\n\n        assert(txnResult.getVersion === 0)\n        assertCheckpointReadiness(txnResult, isReadyForCheckpoint = false)\n\n        verifyCommitInfo(tablePath, version = 0)\n        verifyWrittenContent(tablePath, schema, Seq.empty)\n      }\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Collation write tests\n  ///////////////////////////////////////////////////////////////////////////\n\n  test(\"insert into table - simple collated string column\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n      val unicode = new StringType(\"ICU.UNICODE\")\n      val serbianWithVersion = new StringType(\"ICU.SR_CYRL_SRB.75.1\")\n      val serbianWithoutVersion = new StringType(\"ICU.SR_CYRL_SRB\")\n\n      val commonSchema = new StructType()\n        .add(\"c1\", IntegerType.INTEGER)\n        .add(\"c2\", StringType.STRING)\n        .add(\"c3\", STRING)\n        .add(\"c4\", utf8Lcase)\n        .add(\"c5\", unicode)\n      val schemaWithVersion = commonSchema.add(\"c6\", serbianWithVersion)\n      val schemaWithoutVersion = commonSchema.add(\"c6\", serbianWithoutVersion)\n\n      // First append\n      val data1 =\n        generateData(schemaWithVersion, Seq.empty, Map.empty, batchSize = 10, numBatches = 1)\n\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        schemaWithVersion,\n        data = Seq(Map.empty[String, Literal] -> data1))\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0)\n      // we use schemaWithoutVersion to verify since the version info is not stored in the\n      // schema serialization\n      verifyWrittenContent(tblPath, schemaWithoutVersion, data1.flatMap(_.toTestRows))\n\n      // Second append\n      val data2 =\n        generateData(schemaWithVersion, Seq.empty, Map.empty, batchSize = 5, numBatches = 1)\n\n      val commitResult1 = appendData(\n        engine,\n        tblPath,\n        data = Seq(Map.empty[String, Literal] -> data2))\n      verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 1, partitionCols = null)\n      verifyWrittenContent(\n        tblPath,\n        schemaWithoutVersion,\n        (data1 ++ data2).flatMap(_.toTestRows))\n\n      val metadata = getMetadata(engine, tblPath)\n      val parsed = DataTypeJsonSerDe.deserializeStructType(metadata.getSchemaString())\n      assert(parsed === schemaWithoutVersion)\n    }\n  }\n\n  test(\"insert into table - collated string column with nulls\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val unicode = new StringType(\"ICU.UNICODE.74.1\")\n      val schema = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"name\", unicode)\n\n      val batchSize = 4\n      val idValues = Array[java.lang.Integer](1, 2, 3, 4).asInstanceOf[Array[AnyRef]]\n      val nameValues = Array[AnyRef](\"Alice\", null, \"Bob\", null)\n\n      val idVector = DefaultGenericVector.fromArray(IntegerType.INTEGER, idValues)\n      val nameVector = DefaultGenericVector.fromArray(unicode, nameValues)\n      val batch = new DefaultColumnarBatch(\n        batchSize,\n        schema,\n        Array[ColumnVector](idVector, nameVector))\n      val fcb = new FilteredColumnarBatch(batch, Optional.empty())\n\n      val commit = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        schema,\n        data = Seq(Map.empty[String, Literal] -> Seq(fcb)))\n      verifyCommitResult(commit, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0)\n\n      verifyWrittenContent(tblPath, schema, fcb.toTestRows)\n    }\n  }\n\n  test(\"insert into table - complex types with collated strings in nested/array/map\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n      val unicode = new StringType(\"ICU.UNICODE\")\n      val unicodeWithVersion = new StringType(\"ICU.UNICODE.74\")\n\n      val commonNested = new StructType()\n        .add(\"s1\", utf8Lcase)\n        .add(\"n\", INTEGER)\n\n      val nestedWithVersion = commonNested.add(\"s2\", unicodeWithVersion)\n      val nestedWithoutVersion = commonNested.add(\"s2\", unicode)\n\n      val schemaWithVersion = new StructType()\n        .add(\"nested\", nestedWithVersion)\n        .add(\"arr\", new ArrayType(utf8Lcase, true))\n        .add(\"map\", new MapType(STRING, unicode, true))\n      val schemaWithoutVersion = new StructType()\n        .add(\"nested\", nestedWithoutVersion)\n        .add(\"arr\", new ArrayType(utf8Lcase, true))\n        .add(\"map\", new MapType(STRING, unicode, true))\n\n      val batchSize = 4\n\n      def buildBatch(seed: String): FilteredColumnarBatch = {\n        val nestedVectors = Array[ColumnVector](\n          testColumnVector(batchSize, utf8Lcase),\n          testColumnVector(batchSize, INTEGER),\n          testColumnVector(batchSize, unicode))\n        val nestedVector = new DefaultStructVector(\n          batchSize,\n          nestedWithVersion,\n          Optional.empty(),\n          nestedVectors)\n\n        val arrValues: Seq[Seq[AnyRef]] = (0 until batchSize).map { i =>\n          Seq(s\"${seed}t$i\", s\"${seed}x$i\").map(_.asInstanceOf[AnyRef])\n        }\n        val arrVector = buildArrayVector(arrValues, utf8Lcase, containsNull = true)\n\n        val mapType = new MapType(STRING, unicode, true)\n        val mapValues: Seq[Map[AnyRef, AnyRef]] = (0 until batchSize).map { i =>\n          Map[AnyRef, AnyRef](s\"${seed}k$i\" -> s\"${seed}v$i\")\n        }\n        val mapVector = buildMapVector(mapValues, mapType)\n\n        val vectors = Array[ColumnVector](nestedVector, arrVector, mapVector)\n        val batch = new DefaultColumnarBatch(batchSize, schemaWithVersion, vectors)\n        new FilteredColumnarBatch(batch, Optional.empty())\n      }\n\n      val fcb1 = buildBatch(\"a-\")\n      val fcb2 = buildBatch(\"b-\")\n\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        schemaWithVersion,\n        data = Seq(Map.empty[String, Literal] -> Seq(fcb1)))\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0)\n\n      val commitResult1 = appendData(\n        engine,\n        tblPath,\n        data = Seq(Map.empty[String, Literal] -> Seq(fcb2)))\n\n      verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 1, partitionCols = null)\n\n      val expectedRows = Seq(fcb1, fcb2).flatMap(_.toTestRows)\n      verifyWrittenContent(tblPath, schemaWithVersion, expectedRows)\n\n      val metadata = getMetadata(engine, tblPath)\n      val parsed = DataTypeJsonSerDe.deserializeStructType(metadata.getSchemaString())\n      assert(parsed === schemaWithoutVersion)\n    }\n  }\n\n  test(\"insert into table - nested struct with collated string field\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n      val unicode = new StringType(\"ICU.UNICODE\")\n      val nested = new StructType()\n        .add(\"c21\", utf8Lcase)\n        .add(\"c22\", IntegerType.INTEGER)\n        .add(\"c23\", unicode)\n        .add(\"c24\", STRING)\n      val schema = new StructType()\n        .add(\"c1\", LongType.LONG)\n        .add(\"c2\", nested)\n\n      val data = generateData(schema, Seq.empty, Map.empty, batchSize = 8, numBatches = 2)\n\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        schema,\n        data = Seq(Map.empty[String, Literal] -> data))\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0)\n      verifyWrittenContent(tblPath, schema, data.flatMap(_.toTestRows))\n\n      val metadata = getMetadata(engine, tblPath)\n      val parsed = DataTypeJsonSerDe.deserializeStructType(metadata.getSchemaString())\n      assert(parsed === schema)\n    }\n  }\n\n  test(\"insert into table - complex types with collated strings in nested fields\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n      val unicode = new StringType(\"ICU.UNICODE\")\n\n      val nested = new StructType()\n        .add(\"c1\", unicode)\n        .add(\"c2\", IntegerType.INTEGER)\n        .add(\"c3\", STRING)\n\n      val schema = new StructType()\n        .add(\"c1\", IntegerType.INTEGER)\n        .add(\"c2\", nested)\n        .add(\"c3\", new ArrayType(utf8Lcase, true))\n        .add(\"c4\", new MapType(STRING, unicode, true))\n\n      // Build vectors\n      val batchSize = 5\n      val c1Vector = testColumnVector(batchSize, IntegerType.INTEGER)\n\n      val nestedVectors = Array[ColumnVector](\n        testColumnVector(batchSize, unicode),\n        testColumnVector(batchSize, IntegerType.INTEGER),\n        testColumnVector(batchSize, STRING))\n      val c2Vector = new DefaultStructVector(batchSize, nested, Optional.empty(), nestedVectors)\n\n      val c3Values: Seq[Seq[AnyRef]] = (0 until batchSize).map { i =>\n        Seq(s\"t$i\", s\"x$i\").map(_.asInstanceOf[AnyRef])\n      }\n      val c3Vector = buildArrayVector(c3Values, utf8Lcase, containsNull = true)\n\n      val c4Type = new MapType(STRING, unicode, true)\n      val c4Values: Seq[Map[AnyRef, AnyRef]] = (0 until batchSize).map { i =>\n        Map[AnyRef, AnyRef](s\"k$i\" -> s\"v$i\")\n      }\n      val c4Vector = buildMapVector(c4Values, c4Type)\n\n      val vectors = Array[ColumnVector](c1Vector, c2Vector, c3Vector, c4Vector)\n      val batch = new DefaultColumnarBatch(batchSize, schema, vectors)\n      val fcb = new FilteredColumnarBatch(batch, Optional.empty())\n\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        schema,\n        data = Seq(Map.empty[String, Literal] -> Seq(fcb)))\n\n      val metadata = getMetadata(engine, tblPath)\n      val parsed = DataTypeJsonSerDe.deserializeStructType(metadata.getSchemaString)\n      assert(parsed === schema)\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0)\n      val expectedRows = Seq(fcb).flatMap(_.toTestRows)\n      verifyWrittenContent(tblPath, schema, expectedRows)\n    }\n  }\n\n  test(\"insert into partitioned table - collated string partition columns\") {\n    val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n    val unicode = new StringType(\"ICU.UNICODE.75.1\")\n    val serbian = new StringType(\"ICU.SR_CYRL_SRB\")\n    Seq(\n      // (p1BatchType, p2BatchType, vBatchType)\n      (utf8Lcase, unicode, serbian),\n      (serbian, serbian, utf8Lcase),\n      (utf8Lcase, serbian, unicode),\n      (unicode, serbian, STRING),\n      (STRING, serbian, STRING),\n      (STRING, STRING, STRING),\n      (utf8Lcase, STRING, utf8Lcase)).foreach { case (p1BatchType, p2BatchType, vBatchType) =>\n      withTempDirAndEngine { (tblPath, engine) =>\n        val schema = new StructType()\n          .add(\"id\", INTEGER)\n          .add(\"p1\", utf8Lcase) // partition column\n          .add(\"p2\", unicode) // partition column\n          .add(\"v\", serbian)\n\n        val schemaWithoutVersion = new StructType()\n          .add(\"id\", INTEGER)\n          .add(\"p1\", utf8Lcase)\n          .add(\"p2\", new StringType(\"ICU.UNICODE\"))\n          .add(\"v\", serbian)\n\n        val dataSchema = new StructType()\n          .add(\"id\", INTEGER)\n          .add(\"p1\", p1BatchType)\n          .add(\"p2\", p2BatchType)\n          .add(\"v\", vBatchType)\n\n        val vCollation = vBatchType.getCollationIdentifier\n\n        val partCols = Seq(\"p1\", \"p2\")\n\n        val v0Part = Map(\"p1\" -> ofString(\"a\"), \"p2\" -> ofString(\"alpha\", vCollation))\n        val v0Data = generateData(dataSchema, partCols, v0Part, batchSize = 8, numBatches = 1)\n\n        val v1Part = Map(\"p1\" -> ofString(\"B\", vCollation), \"p2\" -> ofString(\"beta\"))\n        val v1Data = generateData(dataSchema, partCols, v1Part, batchSize = 5, numBatches = 1)\n\n        val commitResult0 = appendData(\n          engine,\n          tblPath,\n          isNewTable = true,\n          schema,\n          partCols,\n          data = Seq(v0Part -> v0Data, v1Part -> v1Data))\n\n        verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n        // Expect partition columns in the same case as in the schema\n        verifyCommitInfo(tblPath, version = 0, partitionCols = partCols)\n\n        val expectedRows0 = v0Data.flatMap(_.toTestRows) ++ v1Data.flatMap(_.toTestRows)\n        verifyWrittenContent(tblPath, schema, expectedRows0)\n\n        val v2Part = Map(\"p1\" -> ofString(\"c\"), \"p2\" -> ofString(\"gamma\"))\n        val v2Data = generateData(dataSchema, partCols, v2Part, batchSize = 4, numBatches = 3)\n\n        val commitResult1 = appendData(\n          engine,\n          tblPath,\n          data = Seq(v2Part -> v2Data))\n\n        verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n        // For subsequent commits, partitionBy is not recorded in commit info\n        verifyCommitInfo(tblPath, version = 1, partitionCols = null)\n\n        val expectedRows1 = expectedRows0 ++ v2Data.flatMap(_.toTestRows)\n        verifyWrittenContent(tblPath, schema, expectedRows1)\n\n        val metadata = getMetadata(engine, tblPath)\n        val parsed = DataTypeJsonSerDe.deserializeStructType(metadata.getSchemaString)\n        assert(parsed === schemaWithoutVersion)\n      }\n    }\n  }\n\n  test(\"stats: default engine writes binary stats for collated string columns\") {\n    val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n    val unicode = new StringType(\"ICU.UNICODE\")\n    val serbian = new StringType(\"ICU.SR_CYRL_SRB.74\")\n    Seq(\n      (STRING, utf8Lcase, unicode),\n      (serbian, serbian, serbian),\n      (STRING, serbian, unicode),\n      (STRING, STRING, STRING)).foreach { case (c1DataType, c2DataType, c3DataType) =>\n      withTempDirAndEngine { (tblPath, engine) =>\n        val schema = new StructType()\n          .add(\"c1\", STRING)\n          .add(\"c2\", utf8Lcase)\n          .add(\"c3\", unicode)\n\n        val txn = getCreateTxn(engine, tblPath, schema)\n        commitTransaction(txn, engine, emptyIterable())\n\n        val batchSize = 4\n        val values = Array(\"b\", \"A\", \"B\", \"a\").map(_.asInstanceOf[AnyRef])\n        val c1 = DefaultGenericVector.fromArray(c1DataType, values)\n        val c2 = DefaultGenericVector.fromArray(c2DataType, values)\n        val c3 = DefaultGenericVector.fromArray(c3DataType, values)\n        val batch = new DefaultColumnarBatch(batchSize, schema, Array[ColumnVector](c1, c2, c3))\n        val fcb = new FilteredColumnarBatch(batch, Optional.empty())\n\n        val commit = appendData(engine, tblPath, data = Seq(Map.empty[String, Literal] -> Seq(fcb)))\n        verifyCommitResult(commit, expVersion = 1, expIsReadyForCheckpoint = false)\n\n        // Read stats JSON\n        val snapshot = Table.forPath(engine, tblPath).getLatestSnapshot(engine)\n        val scan = snapshot.getScanBuilder().build()\n        val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(engine, true).toSeq\n          .flatMap(_.getRows.toSeq)\n        val statsJson = scanFiles.headOption.flatMap { row =>\n          val add = row.getStruct(row.getSchema.indexOf(\"add\"))\n          val idx = add.getSchema.indexOf(\"stats\")\n          if (idx >= 0 && !add.isNullAt(idx)) Some(add.getString(idx)) else None\n        }.getOrElse(fail(\"Stats JSON not found\"))\n\n        // Default engine computes just non-collated stats; verify min/max values\n        val mapper = JsonUtils.mapper()\n        val statsNode = mapper.readTree(statsJson)\n        val minValues = statsNode.get(\"minValues\")\n        val maxValues = statsNode.get(\"maxValues\")\n\n        // All columns: [b, A, B, a] -> min \"A\", max \"b\"\n        assert(minValues.get(\"c1\").asText() == \"A\")\n        assert(maxValues.get(\"c1\").asText() == \"b\")\n\n        assert(minValues.get(\"c2\").asText() == \"A\")\n        assert(maxValues.get(\"c2\").asText() == \"b\")\n\n        assert(minValues.get(\"c3\").asText() == \"A\")\n        assert(maxValues.get(\"c3\").asText() == \"b\")\n      }\n    }\n  }\n\n  test(\"stats: collated non-partition column in partitioned table\") {\n    val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n    val unicode = new StringType(\"ICU.UNICODE\")\n    val serbian = new StringType(\"ICU.SR_CYRL_SRB.74\")\n    Seq(\n      (utf8Lcase, unicode),\n      (serbian, serbian),\n      (utf8Lcase, serbian),\n      (STRING, STRING),\n      (STRING, utf8Lcase),\n      (unicode, STRING)).foreach { case (pBatchType, dBatchType) =>\n      withTempDirAndEngine { (tblPath, engine) =>\n        val schema = new StructType()\n          .add(\"p\", utf8Lcase) // partition column\n          .add(\"c\", serbian) // non-partition, collated\n\n        val dCollation = dBatchType.getCollationIdentifier\n\n        val txn = getCreateTxn(engine, tblPath, schema, partCols = Seq(\"p\"))\n        commitTransaction(txn, engine, emptyIterable())\n\n        // Commit 1: p = \"north\", c values [b, A, B, a]\n        val batchSize1 = 4\n        val cValues1 = Array(\"b\", \"A\", \"B\", \"a\").map(_.asInstanceOf[AnyRef])\n        val pValues1 = Array.fill[AnyRef](batchSize1)(\"north\")\n        val pVec1 = DefaultGenericVector.fromArray(pBatchType, pValues1)\n        val cVec1 = DefaultGenericVector.fromArray(dBatchType, cValues1)\n        val batch1 = new DefaultColumnarBatch(batchSize1, schema, Array[ColumnVector](pVec1, cVec1))\n        val fcb1 = new FilteredColumnarBatch(batch1, Optional.empty())\n\n        val commit1 =\n          appendData(\n            engine,\n            tblPath,\n            data = Seq(Map(\"p\" -> ofString(\"north\", dCollation)) -> Seq(fcb1)))\n        verifyCommitResult(commit1, expVersion = 1, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tblPath, version = 1, partitionCols = null)\n\n        // Commit 2: p = \"south\", c values [d, C]\n        val batchSize2 = 2\n        val cValues2 = Array(\"d\", \"C\", \"a\").map(_.asInstanceOf[AnyRef])\n        val pValues2 = Array.fill[AnyRef](batchSize2)(\"south\")\n        val pVec2 = DefaultGenericVector.fromArray(pBatchType, pValues2)\n        val cVec2 = DefaultGenericVector.fromArray(dBatchType, cValues2)\n        val batch2 = new DefaultColumnarBatch(batchSize2, schema, Array[ColumnVector](pVec2, cVec2))\n        val fcb2 = new FilteredColumnarBatch(batch2, Optional.empty())\n\n        val commit2 =\n          appendData(engine, tblPath, data = Seq(Map(\"p\" -> ofString(\"south\")) -> Seq(fcb2)))\n        verifyCommitResult(commit2, expVersion = 2, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tblPath, version = 2, partitionCols = null)\n\n        // Read stats JSON\n        val snapshot = Table.forPath(engine, tblPath).getLatestSnapshot(engine)\n        val scan = snapshot.getScanBuilder.build()\n        val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(engine, true).toSeq\n          .flatMap(_.getRows.toSeq)\n\n        val mapper = JsonUtils.mapper()\n        assert(scanFiles.nonEmpty)\n        scanFiles.foreach { row =>\n          val add = row.getStruct(row.getSchema.indexOf(\"add\"))\n          val path = add.getString(add.getSchema.indexOf(\"path\"))\n          val statsIdx = add.getSchema.indexOf(\"stats\")\n          assert(statsIdx >= 0 && !add.isNullAt(statsIdx))\n          val statsJson = add.getString(statsIdx)\n          val statsNode = mapper.readTree(statsJson)\n          val minValues = statsNode.get(\"minValues\")\n          val maxValues = statsNode.get(\"maxValues\")\n\n          val minC = minValues.get(\"c\").asText()\n          val maxC = maxValues.get(\"c\").asText()\n\n          if (path.contains(\"p=north\")) {\n            // For [b, A, B, a] -> min \"A\", max \"b\"\n            assert(minC == \"A\")\n            assert(maxC == \"b\")\n          } else if (path.contains(\"p=south\")) {\n            // For [d, C] -> min \"C\", max \"d\"\n            assert(minC == \"C\")\n            assert(maxC == \"d\")\n          } else {\n            fail(s\"Unexpected partition: $path\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"stats: collect min/max for collated nested struct fields\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n      val nested = new StructType()\n        .add(\"s1\", utf8Lcase)\n        .add(\"i1\", INTEGER)\n      val schema = new StructType()\n        .add(\"nested\", nested)\n\n      val txn = getCreateTxn(engine, tblPath, schema)\n      commitTransaction(txn, engine, emptyIterable())\n\n      val batchSize = 4\n      val s1Values = Array(\"b\", \"A\", \"B\", \"a\").map(_.asInstanceOf[AnyRef])\n      val i1Values = Array[java.lang.Integer](3, -1, 10, 5)\n      val s1 = DefaultGenericVector.fromArray(utf8Lcase, s1Values)\n      val i1 = DefaultGenericVector.fromArray(INTEGER, i1Values.asInstanceOf[Array[AnyRef]])\n      val nestedVector = new DefaultStructVector(\n        batchSize,\n        nested,\n        Optional.empty(),\n        Array[ColumnVector](s1, i1))\n      val batch = new DefaultColumnarBatch(\n        batchSize,\n        schema,\n        Array[ColumnVector](nestedVector))\n      val fcb = new FilteredColumnarBatch(batch, Optional.empty())\n\n      val commit = appendData(engine, tblPath, data = Seq(Map.empty[String, Literal] -> Seq(fcb)))\n      verifyCommitResult(commit, expVersion = 1, expIsReadyForCheckpoint = false)\n\n      // Read stats JSON\n      val snapshot = Table.forPath(engine, tblPath).getLatestSnapshot(engine)\n      val scan = snapshot.getScanBuilder().build()\n      val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(engine, true).toSeq\n        .flatMap(_.getRows.toSeq)\n      val statsJson = scanFiles.headOption.flatMap { row =>\n        val add = row.getStruct(row.getSchema.indexOf(\"add\"))\n        val idx = add.getSchema.indexOf(\"stats\")\n        if (idx >= 0 && !add.isNullAt(idx)) Some(add.getString(idx)) else None\n      }.getOrElse(fail(\"Stats JSON not found\"))\n\n      val mapper = JsonUtils.mapper()\n      val statsNode = mapper.readTree(statsJson)\n      val minValues = statsNode.get(\"minValues\")\n      val maxValues = statsNode.get(\"maxValues\")\n\n      val minNested = minValues.get(\"nested\")\n      val maxNested = maxValues.get(\"nested\")\n\n      // For s1: [b, A, B, a] -> min \"A\", max \"b\"\n      assert(minNested.get(\"s1\").asText() == \"A\")\n      assert(maxNested.get(\"s1\").asText() == \"b\")\n\n      // For i1: [3, -1, 10, 5] -> min -1, max 10\n      assert(minNested.get(\"i1\").asInt() == -1)\n      assert(maxNested.get(\"i1\").asInt() == 10)\n    }\n  }\n\n  test(\"stats: arrays and maps produce no stats; collated string field stats present\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val unicode = new StringType(\"ICU.UNICODE\")\n      val utf8Lcase = new StringType(\"SPARK.UTF8_LCASE\")\n      val schema = new StructType()\n        .add(\"name\", unicode)\n        .add(\"arr\", new ArrayType(utf8Lcase, true))\n        .add(\"map\", new MapType(STRING, utf8Lcase, true))\n\n      val txn = getCreateTxn(engine, tblPath, schema)\n      commitTransaction(txn, engine, emptyIterable())\n\n      val batchSize = 4\n      val nameValues = Array(\"b\", \"A\", \"B\", \"a\").map(_.asInstanceOf[AnyRef])\n      val nameVec = DefaultGenericVector.fromArray(unicode, nameValues)\n\n      val arrValues: Seq[Seq[AnyRef]] = (0 until batchSize).map { i =>\n        Seq(s\"x$i\").map(_.asInstanceOf[AnyRef])\n      }\n      val arrVec = buildArrayVector(arrValues, utf8Lcase, containsNull = true)\n\n      val mapType = new MapType(STRING, utf8Lcase, true)\n      val mapValues: Seq[Map[AnyRef, AnyRef]] = (0 until batchSize).map { i =>\n        Map[AnyRef, AnyRef](s\"k$i\" -> s\"v$i\")\n      }\n      val mapVec = buildMapVector(mapValues, mapType)\n\n      val batch = new DefaultColumnarBatch(\n        batchSize,\n        schema,\n        Array[ColumnVector](nameVec, arrVec, mapVec))\n      val fcb = new FilteredColumnarBatch(batch, Optional.empty())\n\n      val commit = appendData(engine, tblPath, data = Seq(Map.empty[String, Literal] -> Seq(fcb)))\n      verifyCommitResult(commit, expVersion = 1, expIsReadyForCheckpoint = false)\n\n      // Read stats JSON\n      val snapshot = Table.forPath(engine, tblPath).getLatestSnapshot(engine)\n      val scan = snapshot.getScanBuilder().build()\n      val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(engine, true).toSeq\n        .flatMap(_.getRows.toSeq)\n      val statsJson = scanFiles.headOption.flatMap { row =>\n        val add = row.getStruct(row.getSchema.indexOf(\"add\"))\n        val idx = add.getSchema.indexOf(\"stats\")\n        if (idx >= 0 && !add.isNullAt(idx)) Some(add.getString(idx)) else None\n      }.getOrElse(fail(\"Stats JSON not found\"))\n\n      val mapper = JsonUtils.mapper()\n      val statsNode = mapper.readTree(statsJson)\n      val minValues = statsNode.get(\"minValues\")\n      val maxValues = statsNode.get(\"maxValues\")\n\n      // String column stats are present\n      assert(minValues.get(\"name\").asText() == \"A\")\n      assert(maxValues.get(\"name\").asText() == \"b\")\n\n      // Array/Map columns should not have stats\n      assert(!minValues.has(\"arr\"))\n      assert(!maxValues.has(\"arr\"))\n      assert(!minValues.has(\"map\"))\n      assert(!maxValues.has(\"map\"))\n    }\n  }\n\n  ///////////////////////////////////////////////////////////////////////////\n  // Create table and insert data tests (CTAS & INSERT)\n  ///////////////////////////////////////////////////////////////////////////\n\n  test(\"cannot write to a table with an unsupported writer feature\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema)\n\n      // Use your new commitUnsafe API to write an unsupported writer feature\n      import org.apache.spark.sql.delta.{DeltaLog, OptimisticTransaction}\n      import org.apache.spark.sql.delta.actions.Protocol\n\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      val txn = deltaLog.startTransaction()\n\n      // Create Protocol action with unsupported writer feature\n      val protocolAction = Protocol(\n        minReaderVersion = 3,\n        minWriterVersion = 7,\n        readerFeatures = Some(Set.empty),\n        writerFeatures = Some(Set(\"testUnsupportedWriter\")))\n\n      // Use your elegant API to commit directly to version 1\n      txn.commitUnsafe(tablePath, 1L, protocolAction)\n\n      val e = intercept[KernelException] {\n        getUpdateTxn(engine, tablePath)\n      }\n      assert(e.getMessage.contains(\"Unsupported Delta table feature\"))\n    }\n  }\n\n  test(\"insert into table - table created from scratch\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        testSchema,\n        data = Seq(Map.empty[String, Literal] -> (dataBatches1 ++ dataBatches2)))\n\n      val expectedAnswer = dataBatches1.flatMap(_.toTestRows) ++ dataBatches2.flatMap(_.toTestRows)\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0)\n      verifyWrittenContent(tblPath, testSchema, expectedAnswer)\n    }\n  }\n\n  test(\"insert into table - already existing table\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        testSchema,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0, partitionCols = Seq.empty)\n      verifyWrittenContent(tblPath, testSchema, dataBatches1.flatMap(_.toTestRows))\n\n      val txn = getUpdateTxn(engine, tblPath)\n      assert(txn.getReadTableVersion == 0)\n      val commitResult1 =\n        commitAppendData(engine, txn, data = Seq(Map.empty[String, Literal] -> dataBatches2))\n\n      val expAnswer = dataBatches1.flatMap(_.toTestRows) ++ dataBatches2.flatMap(_.toTestRows)\n\n      verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 1, partitionCols = null)\n      verifyWrittenContent(tblPath, testSchema, expAnswer)\n    }\n  }\n\n  test(\"insert into table - fails when committing the same txn twice\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val table = Table.forPath(engine, tblPath)\n      val txn = getCreateTxn(engine, tblPath, schema = testSchema)\n\n      val txnState = txn.getTransactionState(engine)\n      val stagedFiles = stageData(txnState, Map.empty, dataBatches1)\n\n      val stagedActionsIterable = inMemoryIterable(stagedFiles)\n      val commitResult = commitTransaction(txn, engine, stagedActionsIterable)\n      assert(commitResult.getVersion == 0)\n\n      // try to commit the same transaction and expect failure\n      val ex = intercept[IllegalStateException] {\n        commitTransaction(txn, engine, stagedActionsIterable)\n      }\n      assert(ex.getMessage.contains(\n        \"Transaction is already attempted to commit. Create a new transaction.\"))\n    }\n  }\n\n  test(\"insert into partitioned table - table created from scratch\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        testPartitionSchema,\n        testPartitionColumns,\n        Seq(\n          Map(\"part1\" -> ofInt(1), \"part2\" -> ofInt(2)) -> dataPartitionBatches1,\n          Map(\"part1\" -> ofInt(4), \"part2\" -> ofInt(5)) -> dataPartitionBatches2))\n\n      val expData = dataPartitionBatches1.flatMap(_.toTestRows) ++\n        dataPartitionBatches2.flatMap(_.toTestRows)\n\n      verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tblPath, version = 0, testPartitionColumns)\n      verifyWrittenContent(tblPath, testPartitionSchema, expData)\n    }\n  }\n\n  test(\"insert into partitioned table - already existing table\") {\n    withTempDirAndEngine { (tempTblPath, engine) =>\n      val tblPath = tempTblPath + \"/table+ with special chars\"\n      val partitionCols = Seq(\"part1\", \"part2\")\n\n      {\n        val commitResult0 = appendData(\n          engine,\n          tblPath,\n          isNewTable = true,\n          testPartitionSchema,\n          testPartitionColumns,\n          data = Seq(Map(\"part1\" -> ofInt(1), \"part2\" -> ofInt(2)) -> dataPartitionBatches1))\n\n        val expData = dataPartitionBatches1.flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult0, expVersion = 0, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tblPath, version = 0, partitionCols)\n        verifyWrittenContent(tblPath, testPartitionSchema, expData)\n      }\n      {\n        val commitResult1 = appendData(\n          engine,\n          tblPath,\n          data = Seq(Map(\"part1\" -> ofInt(4), \"part2\" -> ofInt(5)) -> dataPartitionBatches2))\n\n        val expData = dataPartitionBatches1.flatMap(_.toTestRows) ++\n          dataPartitionBatches2.flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult1, expVersion = 1, expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tblPath, version = 1, partitionCols = null)\n        verifyWrittenContent(tblPath, testPartitionSchema, expData)\n      }\n    }\n  }\n\n  test(\"insert into partitioned table - handling case sensitivity of partition columns\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val schema = new StructType()\n        .add(\"id\", INTEGER)\n        .add(\"Name\", STRING)\n        .add(\"Part1\", DOUBLE) // partition column\n        .add(\"parT2\", TIMESTAMP) // partition column\n\n      val partCols = Seq(\"part1\", \"Part2\") // given as input to the txn builder\n\n      // expected partition cols in the commit info or elsewhere in the Delta log.\n      // it is expected to contain the partition columns in the same case as the schema\n      val expPartCols = Seq(\"Part1\", \"parT2\")\n\n      val v0Part0Values = Map(\n        \"PART1\" -> ofDouble(1.0),\n        \"pART2\" -> ofTimestamp(1231212L))\n      val v0Part0Data =\n        generateData(schema, expPartCols, v0Part0Values, batchSize = 200, numBatches = 3)\n\n      val v0Part1Values = Map(\n        \"Part1\" -> ofDouble(7),\n        \"PARt2\" -> ofTimestamp(123112L))\n      val v0Part1Data =\n        generateData(schema, expPartCols, v0Part1Values, batchSize = 100, numBatches = 7)\n\n      val v1Part0Values = Map(\n        \"PART1\" -> ofNull(DOUBLE),\n        \"pART2\" -> ofTimestamp(1231212L))\n      val v1Part0Data =\n        generateData(schema, expPartCols, v1Part0Values, batchSize = 200, numBatches = 3)\n\n      val v1Part1Values = Map(\n        \"Part1\" -> ofDouble(7),\n        \"PARt2\" -> ofNull(TIMESTAMP))\n      val v1Part1Data =\n        generateData(schema, expPartCols, v1Part1Values, batchSize = 100, numBatches = 7)\n\n      val dataPerVersion = Map(\n        0 -> Seq(v0Part0Values -> v0Part0Data, v0Part1Values -> v0Part1Data),\n        1 -> Seq(v1Part0Values -> v1Part0Data, v1Part1Values -> v1Part1Data))\n\n      val expV0Data = v0Part0Data.flatMap(_.toTestRows) ++ v0Part1Data.flatMap(_.toTestRows)\n      val expV1Data = v1Part0Data.flatMap(_.toTestRows) ++ v1Part1Data.flatMap(_.toTestRows)\n\n      for (i <- 0 until 2) {\n        val commitResult = appendData(\n          engine,\n          tblPath,\n          isNewTable = i == 0,\n          if (i == 0) schema else null,\n          partCols,\n          dataPerVersion(i))\n\n        verifyCommitResult(commitResult, expVersion = i, expIsReadyForCheckpoint = false)\n        // partition cols are not written in the commit info for inserts\n        val partitionBy = if (i == 0) expPartCols else null\n        val expectedOperation = if (i == 0) CREATE_TABLE else WRITE\n        verifyCommitInfo(tblPath, version = i, partitionBy)\n        verifyWrittenContent(\n          tblPath,\n          schema,\n          if (i == 0) expV0Data else expV0Data ++ expV1Data)\n      }\n    }\n  }\n\n  Seq(10, 2).foreach { checkpointInterval =>\n    test(s\"insert into partitioned table - isReadyForCheckpoint(interval=$checkpointInterval)\") {\n      withTempDirAndEngine { (tblPath, engine) =>\n        val schema = new StructType()\n          .add(\"id\", INTEGER)\n          .add(\"Name\", STRING)\n          .add(\"Part1\", DOUBLE) // partition column\n          .add(\"parT2\", TIMESTAMP) // partition column\n\n        val partCols = Seq(\"Part1\", \"parT2\")\n        val partValues = Map(\"PART1\" -> ofDouble(1.0), \"pART2\" -> ofTimestamp(1231212L))\n        val data = Seq(\n          partValues -> generateData(schema, partCols, partValues, batchSize = 200, numBatches = 3))\n\n        // create a table first\n        appendData(engine, tblPath, isNewTable = true, schema, partCols, data) // version 0\n        var expData = data.map(_._2).flatMap(_.flatMap(_.toTestRows))\n        var currentTableVersion = 0\n\n        if (checkpointInterval != 10) {\n          // If it is not the default interval alter the table using Spark to set a\n          // custom checkpoint interval\n          setCheckpointInterval(tblPath, interval = checkpointInterval) // version 1\n          currentTableVersion = 1\n        }\n\n        def isCheckpointExpected(version: Long): Boolean = {\n          version != 0 && version % checkpointInterval == 0\n        }\n\n        for (i <- currentTableVersion + 1 until 31) {\n          val commitResult = appendData(engine, tblPath, data = data)\n\n          val parquetFileCount = dataFileCount(tblPath)\n          assert(parquetFileCount > 0)\n          checkpointIfReady(engine, tblPath, commitResult, expSize = parquetFileCount)\n\n          verifyCommitResult(commitResult, expVersion = i, isCheckpointExpected(i))\n\n          expData = expData ++ data.map(_._2).flatMap(_.flatMap(_.toTestRows))\n        }\n\n        // expect the checkpoints created at expected versions\n        Seq.range(0, 31).filter(isCheckpointExpected(_)).foreach { version =>\n          assertCheckpointExists(tblPath, atVersion = version)\n        }\n\n        // delete all commit files before version 30 in both cases and expect the read to pass as\n        // there is a checkpoint at version 30 and should be used for state reconstruction.\n        deleteDeltaFilesBefore(tblPath, beforeVersion = 30)\n        verifyWrittenContent(tblPath, schema, expData)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { includeTimestampNtz =>\n    test(s\"insert into table - all supported types data - \" +\n      s\"timestamp_ntz included = $includeTimestampNtz\") {\n      withTempDirAndEngine { (tblPath, engine) =>\n        val parquetAllTypes = goldenTablePath(\"parquet-all-types\")\n        val goldenTableSchema = tableSchema(parquetAllTypes)\n        val schema = if (includeTimestampNtz) goldenTableSchema\n        else removeTimestampNtzTypeColumns(goldenTableSchema)\n\n        val data = readTableUsingKernel(engine, parquetAllTypes, schema).toSeq\n        val dataWithPartInfo = Seq(Map.empty[String, Literal] -> data)\n\n        appendData(engine, tblPath, isNewTable = true, schema, data = dataWithPartInfo)\n        var expData = dataWithPartInfo.flatMap(_._2).flatMap(_.toTestRows)\n\n        val checkpointInterval = 4\n        setCheckpointInterval(tblPath, checkpointInterval)\n\n        for (i <- 2 until 5) {\n          // insert until a checkpoint is required\n          val commitResult = appendData(engine, tblPath, data = dataWithPartInfo)\n\n          expData = expData ++ dataWithPartInfo.flatMap(_._2).flatMap(_.toTestRows)\n          checkpointIfReady(engine, tblPath, commitResult, expSize = i /* one file per version */ )\n\n          verifyCommitResult(commitResult, expVersion = i, i % checkpointInterval == 0)\n          verifyCommitInfo(tblPath, version = i, null)\n          verifyWrittenContent(tblPath, schema, expData)\n        }\n        assertCheckpointExists(tblPath, atVersion = checkpointInterval)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { includeTimestampNtz =>\n    test(s\"insert into partitioned table - all supported partition column types data - \" +\n      s\"timestamp_ntz included = $includeTimestampNtz\") {\n      withTempDirAndEngine { (tblPath, engine) =>\n        val parquetAllTypes = goldenTablePath(\"parquet-all-types\")\n        val goldenTableSchema = tableSchema(parquetAllTypes)\n        val schema = if (includeTimestampNtz) goldenTableSchema\n        else removeTimestampNtzTypeColumns(goldenTableSchema)\n\n        val partCols = Seq(\n          \"byteType\",\n          \"shortType\",\n          \"integerType\",\n          \"longType\",\n          \"floatType\",\n          \"doubleType\",\n          \"decimal\",\n          \"booleanType\",\n          \"stringType\",\n          \"binaryType\",\n          \"dateType\",\n          \"timestampType\") ++ (if (includeTimestampNtz) Seq(\"timestampNtzType\") else Seq.empty)\n        val casePreservingPartCols =\n          casePreservingPartitionColNames(schema, partCols.asJava).asScala.toSeq\n\n        // get the partition values from the data batch at the given rowId\n        def getPartitionValues(batch: ColumnarBatch, rowId: Int): Map[String, Literal] = {\n          casePreservingPartCols.map { partCol =>\n            val colIndex = schema.indexOf(partCol)\n            val vector = batch.getColumnVector(colIndex)\n\n            val literal = if (vector.isNullAt(rowId)) {\n              Literal.ofNull(vector.getDataType)\n            } else {\n              vector.getDataType match {\n                case _: ByteType => Literal.ofByte(vector.getByte(rowId))\n                case _: ShortType => Literal.ofShort(vector.getShort(rowId))\n                case _: IntegerType => Literal.ofInt(vector.getInt(rowId))\n                case _: LongType => Literal.ofLong(vector.getLong(rowId))\n                case _: FloatType => Literal.ofFloat(vector.getFloat(rowId))\n                case _: DoubleType => Literal.ofDouble(vector.getDouble(rowId))\n                case dt: DecimalType =>\n                  Literal.ofDecimal(vector.getDecimal(rowId), dt.getPrecision, dt.getScale)\n                case _: BooleanType => Literal.ofBoolean(vector.getBoolean(rowId))\n                case _: StringType => Literal.ofString(vector.getString(rowId))\n                case _: BinaryType => Literal.ofBinary(vector.getBinary(rowId))\n                case _: DateType => Literal.ofDate(vector.getInt(rowId))\n                case _: TimestampType => Literal.ofTimestamp(vector.getLong(rowId))\n                case _: TimestampNTZType => Literal.ofTimestampNtz(vector.getLong(rowId))\n                case _ =>\n                  throw new IllegalArgumentException(s\"Unsupported type: ${vector.getDataType}\")\n              }\n            }\n            (partCol, literal)\n          }.toMap\n        }\n\n        val data = readTableUsingKernel(engine, parquetAllTypes, schema).toSeq\n\n        // From the above table read data, convert each row as a new batch with partition info\n        // Take the values of the partitionCols from the data and create a new batch with the\n        // selection vector to just select a single row.\n        var dataWithPartInfo = Seq.empty[(Map[String, Literal], Seq[FilteredColumnarBatch])]\n\n        data.foreach { filteredBatch =>\n          val batch = filteredBatch.getData\n          Seq.range(0, batch.getSize).foreach { rowId =>\n            val partValues = getPartitionValues(batch, rowId)\n            val filteredBatch = new FilteredColumnarBatch(\n              batch,\n              Optional.of(selectSingleElement(batch.getSize, rowId)))\n            dataWithPartInfo = dataWithPartInfo :+ (partValues, Seq(filteredBatch))\n          }\n        }\n\n        appendData(engine, tblPath, isNewTable = true, schema, partCols, dataWithPartInfo)\n        verifyCommitInfo(tblPath, version = 0, casePreservingPartCols)\n\n        var expData = dataWithPartInfo.flatMap(_._2).flatMap(_.toTestRows)\n\n        val checkpointInterval = 2\n        setCheckpointInterval(tblPath, checkpointInterval) // version 1\n\n        for (i <- 2 until 4) {\n          // insert until a checkpoint is required\n          val commitResult = appendData(engine, tblPath, data = dataWithPartInfo)\n\n          expData = expData ++ dataWithPartInfo.flatMap(_._2).flatMap(_.toTestRows)\n\n          val fileCount = dataFileCount(tblPath)\n          checkpointIfReady(engine, tblPath, commitResult, expSize = fileCount)\n\n          verifyCommitResult(commitResult, expVersion = i, i % checkpointInterval == 0)\n          verifyCommitInfo(tblPath, version = i, partitionCols = null)\n          verifyWrittenContent(tblPath, schema, expData)\n        }\n\n        assertCheckpointExists(tblPath, atVersion = checkpointInterval)\n      }\n    }\n  }\n\n  test(\"insert into table - given data schema mismatch\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val ex = intercept[KernelException] {\n        val data = Seq(Map.empty[String, Literal] -> dataPartitionBatches1) // data schema mismatch\n        appendData(engine, tblPath, isNewTable = true, testSchema, data = data)\n      }\n      assert(ex.getMessage.contains(\"The schema of the data to be written to \" +\n        \"the table doesn't match the table schema\"))\n    }\n  }\n\n  test(\"insert into table - missing partition column info\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val ex = intercept[IllegalArgumentException] {\n        appendData(\n          engine,\n          tblPath,\n          isNewTable = true,\n          testPartitionSchema,\n          testPartitionColumns,\n          data = Seq(Map(\"part1\" -> ofInt(1)) -> dataPartitionBatches1) // missing part2\n        )\n      }\n      assert(ex.getMessage.contains(\n        \"Partition values provided are not matching the partition columns.\"))\n    }\n  }\n\n  test(\"insert into partitioned table - invalid type of partition value\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val ex = intercept[IllegalArgumentException] {\n        // part2 type should be int, be giving a string value\n        val data = Seq(Map(\"part1\" -> ofInt(1), \"part2\" -> ofString(\"sdsd\"))\n          -> dataPartitionBatches1)\n        appendData(\n          engine,\n          tblPath,\n          isNewTable = true,\n          testPartitionSchema,\n          testPartitionColumns,\n          data)\n      }\n      assert(ex.getMessage.contains(\n        \"Partition column part2 is of type integer but the value provided is of type string\"))\n    }\n  }\n\n  test(\"insert into table - idempotent writes\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val data = Seq(Map(\"part1\" -> ofInt(1), \"part2\" -> ofInt(2)) -> dataPartitionBatches1)\n      var expData = Seq.empty[TestRow] // as the data in inserted, update this.\n\n      def prepTxnAndActions(newTbl: Boolean, appId: String, txnVer: Long)\n          : (Transaction, CloseableIterable[Row]) = {\n\n        val txn = if (newTbl) {\n          getCreateTxn(\n            engine,\n            tblPath,\n            schema = testPartitionSchema,\n            partCols = testPartitionColumns)\n        } else {\n          getUpdateTxn(\n            engine,\n            tblPath,\n            txnId = if (appId != null) Some((appId, txnVer)) else None)\n        }\n\n        val combinedActions = inMemoryIterable(\n          data.map { case (partValues, partData) =>\n            stageData(txn.getTransactionState(engine), partValues, partData)\n          }.reduceLeft(_ combine _))\n\n        (txn, combinedActions)\n      }\n\n      def commitAndVerify(\n          newTbl: Boolean,\n          txn: Transaction,\n          actions: CloseableIterable[Row],\n          expTblVer: Long): Unit = {\n        val commitResult = commitTransaction(txn, engine, actions)\n\n        expData = expData ++ data.flatMap(_._2).flatMap(_.toTestRows)\n\n        verifyCommitResult(commitResult, expVersion = expTblVer, expIsReadyForCheckpoint = false)\n        val expPartCols = if (newTbl) testPartitionColumns else null\n        val expOperation = if (newTbl) CREATE_TABLE else WRITE\n        verifyCommitInfo(tblPath, version = expTblVer, expPartCols)\n        verifyWrittenContent(tblPath, testPartitionSchema, expData)\n      }\n\n      def addDataWithTxnId(newTbl: Boolean, appId: String, txnVer: Long, expTblVer: Long): Unit = {\n        val (txn, combinedActions) = prepTxnAndActions(newTbl, appId, txnVer)\n        commitAndVerify(newTbl, txn, combinedActions, expTblVer)\n      }\n\n      def expFailure(appId: String, txnVer: Long, latestTxnVer: Long)(fn: => Any): Unit = {\n        val ex = intercept[ConcurrentTransactionException] {\n          fn\n        }\n        assert(ex.getMessage.contains(s\"This error occurs when multiple updates are using the \" +\n          s\"same transaction identifier to write into this table.\\nApplication ID: $appId, \" +\n          s\"Attempted version: $txnVer, Latest version in table: $latestTxnVer\"))\n      }\n\n      // Create a transaction with id (txnAppId1, 0) and commit it\n      addDataWithTxnId(newTbl = true, appId = \"txnAppId1\", txnVer = 0, expTblVer = 0)\n\n      // Try to create a transaction with id (txnAppId1, 1) and commit it - should be valid\n      addDataWithTxnId(newTbl = false, appId = \"txnAppId1\", txnVer = 1, expTblVer = 1)\n\n      // Try to create a transaction with id (txnAppId1, 1) and try to commit it\n      // Should fail the it is already committed above.\n      expFailure(\"txnAppId1\", txnVer = 1, latestTxnVer = 1) {\n        addDataWithTxnId(newTbl = false, \"txnAppId1\", txnVer = 1, expTblVer = 2)\n      }\n\n      // append with no txn id\n      addDataWithTxnId(newTbl = false, appId = null, txnVer = 0, expTblVer = 2)\n\n      // Try to create a transaction with id (txnAppId2, 1) and commit it\n      // Should be successful as the transaction app id is different\n      addDataWithTxnId(newTbl = false, \"txnAppId2\", txnVer = 1, expTblVer = 3)\n\n      // Try to create a transaction with id (txnAppId2, 0) and commit it\n      // Should fail as the transaction app id is same but the version is less than the committed\n      expFailure(\"txnAppId2\", txnVer = 0, latestTxnVer = 1) {\n        addDataWithTxnId(newTbl = false, \"txnAppId2\", txnVer = 0, expTblVer = 4)\n      }\n\n      // Start a transaction (txnAppId2, 2), but don't commit it yet\n      val (txn, combinedActions) = prepTxnAndActions(newTbl = false, \"txnAppId2\", txnVer = 2)\n      // Now start a new transaction with the same id (txnAppId2, 2) and commit it\n      addDataWithTxnId(newTbl = false, \"txnAppId2\", txnVer = 2, expTblVer = 4)\n      // Now try to commit the previous transaction (txnAppId2, 2) - should fail\n      expFailure(\"txnAppId2\", txnVer = 2, latestTxnVer = 2) {\n        commitAndVerify(newTbl = false, txn, combinedActions, expTblVer = 5)\n      }\n\n      // Start a transaction (txnAppId2, 3), but don't commit it yet\n      val (txn2, combinedActions2) = prepTxnAndActions(newTbl = false, \"txnAppId2\", txnVer = 3)\n      // Now start a new transaction with the different id (txnAppId1, 10) and commit it\n      addDataWithTxnId(newTbl = false, \"txnAppId1\", txnVer = 10, expTblVer = 5)\n      // Now try to commit the previous transaction (txnAppId2, 3) - should pass\n      commitAndVerify(newTbl = false, txn2, combinedActions2, expTblVer = 6)\n    }\n  }\n\n  test(\"insert into table - write stats and validate they can be read by Spark \") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      // Configure the table property for stats collection via TableConfig.\n      val numIndexedCols = 5\n      val tableProperties = Map(TableConfig.\n      DATA_SKIPPING_NUM_INDEXED_COLS.getKey -> numIndexedCols.toString)\n\n      // Schema of the table with some nested types\n      val schema = new StructType()\n        .add(\"id\", INTEGER)\n        .add(\"name\", STRING)\n        .add(\"height\", DOUBLE)\n        .add(\"timestamp\", TIMESTAMP)\n        .add(\n          \"metrics\",\n          new StructType()\n            .add(\"temperature\", DoubleType.DOUBLE)\n            .add(\"humidity\", FloatType.FLOAT))\n\n      // Create the table with the given schema and table properties.\n      val txn =\n        getCreateTxn(engine, tblPath, schema, tableProperties = tableProperties)\n      commitTransaction(txn, engine, emptyIterable())\n\n      val dataBatches1 = generateData(schema, Seq.empty, Map.empty, batchSize = 10, numBatches = 1)\n      val dataBatches2 = generateData(schema, Seq.empty, Map.empty, batchSize = 20, numBatches = 1)\n\n      // Write initial data via Kernel.\n      val commitResult0 = appendData(\n        engine,\n        tblPath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n      verifyCommitResult(commitResult0, expVersion = 1, expIsReadyForCheckpoint = false)\n      verifyWrittenContent(tblPath, schema, dataBatches1.flatMap(_.getRows().toSeq).map(TestRow(_)))\n\n      // Append additional data.\n      val commitResult1 = appendData(\n        engine,\n        tblPath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2))\n      val expectedRows = dataBatches1.flatMap(_.getRows().toSeq) ++\n        dataBatches2.flatMap(_.getRows().toSeq)\n      verifyCommitResult(commitResult1, expVersion = 2, expIsReadyForCheckpoint = false)\n      verifyWrittenContent(tblPath, schema, expectedRows.map(TestRow(_)))\n    }\n  }\n\n  test(\"insert - validate DATA_SKIPPING_NUM_INDEXED_COLS is respected when collecting stats\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      val numIndexedCols = 2\n      val tableProps = Map(TableConfig.DATA_SKIPPING_NUM_INDEXED_COLS\n        .getKey -> numIndexedCols.toString)\n      val schema = new StructType()\n        .add(\"id\", INTEGER)\n        .add(\n          \"name\",\n          new StructType()\n            .add(\"height\", DoubleType.DOUBLE)\n            .add(\"timestamp\", TimestampType.TIMESTAMP))\n\n      // Create table with stats collection enabled.\n      val txn = getCreateTxn(engine, tblPath, schema, tableProperties = tableProps)\n      commitTransaction(txn, engine, emptyIterable())\n\n      // Write one batch of data.\n      val dataBatches = generateData(schema, Seq.empty, Map.empty, batchSize = 10, numBatches = 1)\n      val commitResult =\n        appendData(engine, tblPath, data = Seq(Map.empty[String, Literal] -> dataBatches))\n      verifyCommitResult(commitResult, expVersion = 1, expIsReadyForCheckpoint = false)\n\n      // Retrieve the stats JSON from the file.\n      val snapshot = Table.forPath(engine, tblPath).getLatestSnapshot(engine)\n      val scan = snapshot.getScanBuilder().build()\n      val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(engine, true)\n        .toSeq.flatMap(_.getRows.toSeq)\n      val statsJson = scanFiles.headOption.flatMap { row =>\n        val addFile = row.getStruct(row.getSchema.indexOf(\"add\"))\n        val statsIdx = addFile.getSchema.indexOf(\"stats\")\n        if (statsIdx >= 0 && !addFile.isNullAt(statsIdx)) {\n          Some(addFile.getString(statsIdx))\n        } else {\n          None\n        }\n      }.getOrElse(fail(\"Stats JSON not found\"))\n\n      // With numIndexedCols = 2, we expect stats for id and name.height, but not for name.timestamp\n      assert(statsJson.contains(\"\\\"id\\\"\"), \"Stats should contain 'id' field\")\n      assert(statsJson.contains(\"\\\"height\\\"\"), \"Stats should contain 'height' field\")\n      assert(\n        !statsJson.contains(\"\\\"timestamp\\\"\"),\n        \"Stats should not contain 'timestamp' field, as it exceeds numIndexedCols\")\n    }\n  }\n\n  test(\"conflicts - creating new table - table created by other txn after current txn start\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val losingTx = getCreateTxn(engine, tablePath, schema = testSchema)\n\n      // don't commit losingTxn, instead create a new txn and commit it\n      val winningTx = getCreateTxn(engine, tablePath, schema = testSchema)\n      val winningTxResult = commitTransaction(winningTx, engine, emptyIterable())\n\n      // now attempt to commit the losingTxn\n      val ex = intercept[ProtocolChangedException] {\n        commitTransaction(losingTx, engine, emptyIterable())\n      }\n      assert(ex.getMessage.contains(\n        \"Transaction has encountered a conflict and can not be committed.\"))\n      // helpful message for table creation conflict\n      assert(ex.getMessage.contains(\"This happens when multiple writers are \" +\n        \"writing to an empty directory. Creating the table ahead of time will avoid \" +\n        \"this conflict.\"))\n\n      verifyCommitResult(winningTxResult, expVersion = 0, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tablePath = tablePath, version = 0)\n      verifyWrittenContent(tablePath, testSchema, Seq.empty)\n    }\n  }\n\n  test(\"insert into table - validate serialized json stats equal Spark written stats\") {\n    withTempDirAndEngine { (dir, engine) =>\n      // Test with all Skipping eligible types.\n      // TODO(Issue: 4284): Validate TIMESTAMP and TIMESTAMP_NTZ serialization\n      // format.\n      val schema = new StructType()\n        .add(\"byteCol\", BYTE)\n        .add(\"shortCol\", SHORT)\n        .add(\"intCol\", INTEGER)\n        .add(\"longCol\", LONG)\n        .add(\"floatCol\", FLOAT)\n        .add(\"doubleCol\", DOUBLE)\n        .add(\"stringCol\", STRING)\n        .add(\"dateCol\", DATE)\n        .add(\n          \"structCol\",\n          new StructType()\n            .add(\"nestedDecimal\", DecimalType.USER_DEFAULT)\n            .add(\"nestedDoubleCol\", DOUBLE))\n\n      // Create \"kernel\" and \"spark-copy\" directories\n      val kernelPath = new File(dir, \"kernel\").getAbsolutePath\n      val sparkPath = new File(dir, \"spark-copy\").getAbsolutePath\n\n      // Write a batch of data using the Kernel\n      val batch =\n        generateData(schema, Seq.empty, Map.empty, batchSize = 10, numBatches = 1)\n      appendData(\n        engine,\n        kernelPath,\n        isNewTable = true,\n        schema,\n        data = Seq(Map.empty[String, Literal] -> batch))\n\n      spark.read.format(\"delta\").load(kernelPath)\n        .write.format(\"delta\").mode(\"overwrite\").save(sparkPath)\n\n      val mapper = JsonUtils.mapper()\n      val kernelStats = collectStatsFromAddFiles(engine, kernelPath).map(mapper.readTree)\n      val sparkStats = collectStatsFromAddFiles(engine, sparkPath).map(mapper.readTree)\n\n      require(\n        kernelStats.nonEmpty && sparkStats.nonEmpty,\n        \"stats collected from AddFiles should be non-empty\")\n      // Since Spark doesn't write tightBounds but Kernel now does,\n      // we need to compare stats after removing the tightBounds field from Kernel stats\n      val kernelStatsWithoutTightBounds = kernelStats.map { node =>\n        val objectNode =\n          node.deepCopy().asInstanceOf[ObjectNode]\n        objectNode.remove(\"tightBounds\")\n        objectNode\n      }\n\n      assert(\n        kernelStatsWithoutTightBounds.toSet == sparkStats.toSet,\n        s\"\\nKernel stats (without tightBounds):\" +\n          s\"\\n${kernelStatsWithoutTightBounds.mkString(\"\\n\")}\\n\" +\n          s\"Spark stats:\\n${sparkStats.mkString(\"\\n\")}\")\n    }\n  }\n\n  test(\"conflicts - table metadata has changed after the losing txn has started\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val testData = Seq(Map.empty[String, Literal] -> dataBatches1)\n\n      // create a new table and commit it\n      appendData(engine, tablePath, isNewTable = true, testSchema, data = testData)\n\n      // start the losing transaction\n      val losingTx = getUpdateTxn(engine, tablePath)\n\n      // don't commit losingTxn, instead create a new txn (that changes metadata) and commit it\n      spark.sql(\"ALTER TABLE delta.`\" + tablePath + \"` ADD COLUMN newCol INT\")\n\n      // now attempt to commit the losingTxn\n      val ex = intercept[MetadataChangedException] {\n        commitTransaction(losingTx, engine, emptyIterable())\n      }\n      assert(ex.getMessage.contains(\"The metadata of the Delta table has been changed \" +\n        \"by a concurrent update. Please try the operation again.\"))\n    }\n  }\n\n  // Different scenarios that have multiple winning txns and with a checkpoint in between.\n  Seq(1, 5, 12).foreach { numWinningTxs =>\n    test(s\"conflicts - concurrent data append ($numWinningTxs) after the losing txn has started\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val testData = Seq(Map.empty[String, Literal] -> dataBatches1)\n        var expData = Seq.empty[TestRow]\n\n        // create a new table and commit it\n        appendData(engine, tablePath, isNewTable = true, testSchema, data = testData)\n        expData ++= testData.flatMap(_._2).flatMap(_.toTestRows)\n\n        // start the losing transaction\n        val txn1 = getUpdateTxn(engine, tablePath)\n\n        // don't commit txn1 yet, instead commit nex txns (that appends data) and commit it\n        Seq.range(0, numWinningTxs).foreach { i =>\n          appendData(engine, tablePath, data = Seq(Map.empty[String, Literal] -> dataBatches2))\n          expData ++= dataBatches2.flatMap(_.toTestRows)\n        }\n\n        // add data using the txn1\n        val txn1State = txn1.getTransactionState(engine)\n        val actions = inMemoryIterable(stageData(txn1State, Map.empty, dataBatches2))\n        expData ++= dataBatches2.flatMap(_.toTestRows)\n\n        val txn1Result = commitTransaction(txn1, engine, actions)\n\n        verifyCommitResult(\n          txn1Result,\n          expVersion = numWinningTxs + 1,\n          expIsReadyForCheckpoint = false)\n        verifyCommitInfo(tablePath = tablePath, version = 0)\n        verifyWrittenContent(tablePath, testSchema, expData)\n      }\n    }\n  }\n\n  def removeTimestampNtzTypeColumns(structType: StructType): StructType = {\n    def process(dataType: DataType): Option[DataType] = dataType match {\n      case a: ArrayType =>\n        val newElementType = process(a.getElementType)\n        newElementType.map(new ArrayType(_, a.containsNull()))\n      case m: MapType =>\n        val newKeyType = process(m.getKeyType)\n        val newValueType = process(m.getValueType)\n        (newKeyType, newValueType) match {\n          case (Some(newKeyType), Some(newValueType)) =>\n            Some(new MapType(newKeyType, newValueType, m.isValueContainsNull))\n          case _ => None\n        }\n      case _: TimestampNTZType => None // ignore\n      case s: StructType =>\n        val newType = removeTimestampNtzTypeColumns(s);\n        if (newType.length() > 0) {\n          Some(newType)\n        } else {\n          None\n        }\n      case _ => Some(dataType)\n    }\n\n    var newStructType = new StructType();\n    structType.fields().forEach { field =>\n      val newDataType = process(field.getDataType)\n      if (newDataType.isDefined) {\n        newStructType = newStructType.add(field.getName(), newDataType.get)\n      }\n    }\n    newStructType\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DeltaTableWritesTransactionBuilderV2Suite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Operation, TableManager}\nimport io.delta.kernel.commit.{CommitMetadata, CommitResponse, Committer}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.engine.{DefaultEngine, DefaultJsonHandler}\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.WriteUtilsWithV2Builders\nimport io.delta.kernel.engine.{Engine, JsonHandler}\nimport io.delta.kernel.exceptions.{KernelException, MaxCommitRetryLimitReachedException, TableAlreadyExistsException}\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.commit.DefaultFileSystemManagedTableOnlyCommitter\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.types.IntegerType\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.clustering.{ClusteringMetadataDomain => SparkClusteringMetadataDomain}\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\n/**\n * Tests for the V2 transaction builders [[CreateTableTransactionBuilder]] and\n * [[UpdateTableTransactionBuilder]]. We don't cover the full scope of tests we have for\n * TransactionBuilderV1 (to do so, we would have to duplicate many, many of the existing suites).\n * Instead, we selectively run everything in [[DeltaTableWritesSuite]] as well as some additional\n * white-box-tests for the logic specific to the new builders. The main metadata validation +\n * update logic is shared by both V1 + V2 builders in\n * [[io.delta.kernel.internal.TransactionMetadataFactory]] and thus is covered by all the existing\n * tests we have for the V1 builder (ideally we would have unit tests for just\n * TransactionMetadataFactory in the future).\n *\n * <p>In the future, we should consider duplicating additional test suites with these builders\n * (requires sharding our Kernel CI tests first to avoid increasing CI runtime too much).\n */\nclass DeltaTableWritesTransactionBuilderV2Suite extends DeltaTableWritesSuite\n    with WriteUtilsWithV2Builders {\n\n  ///////////////////////////////////////////////////\n  // Tests for code logic within the builder impls //\n  ///////////////////////////////////////////////////\n\n  // TablePropertiesTransactionBuilderV2Suite tests table property validation, normalization and\n  // unset/set overlap for Create + Update\n\n  // Tested in DeltaTableWritesSuite: setTxnOpt (covered by idempotent writes test)\n  // Tested in DeltaTableWritesSuite: validateKernelCanWriteToTable (covered by unsupported\n  // writer feature test)\n\n  test(\"Cannot add clustering columns to a partitioned table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        partCols = testPartitionColumns)\n      val e = intercept[KernelException] {\n        TableManager.loadSnapshot(tablePath)\n          .asInstanceOf[SnapshotBuilderImpl].build(engine)\n          .buildUpdateTableTransaction(testEngineInfo, Operation.WRITE)\n          .withClusteringColumns(List(new Column(\"part1\")).asJava)\n      }\n      assert(e.getMessage.contains(\"Cannot enable clustering on a partitioned table\"))\n\n    }\n  }\n\n  test(\"Cannot use UpdateTableTransactionBuilder with incompatible operations\") {\n    Seq(Operation.CREATE_TABLE, Operation.REPLACE_TABLE).foreach { op =>\n      withTempDirAndEngine { (tablePath, engine) =>\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema)\n        val e = intercept[IllegalArgumentException] {\n          TableManager.loadSnapshot(tablePath)\n            .build(engine)\n            .buildUpdateTableTransaction(testEngineInfo, op)\n        }\n        assert(e.getMessage.contains(\n          s\"Operation $op is not compatible with Snapshot::buildUpdateTableTransaction\"))\n      }\n    }\n  }\n\n  test(\"UpdateTableTransactionBuilder uses the committer provided during snapshot building\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      class FakeCommitter extends Committer {\n        override def commit(\n            engine: Engine,\n            finalizedActions: CloseableIterator[Row],\n            commitMetadata: CommitMetadata): CommitResponse = {\n          throw new RuntimeException(\"This is a fake committer\")\n        }\n      }\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema)\n\n      // Build snapshot with committer and start txn\n      val txn = TableManager.loadSnapshot(tablePath)\n        .withCommitter(new FakeCommitter())\n        .build(engine)\n        .buildUpdateTableTransaction(testEngineInfo, Operation.WRITE)\n        .build(engine)\n\n      // Check the txn returns the correct committer\n      assert(txn.getCommitter.isInstanceOf[FakeCommitter])\n      // Check that the txn invokes the provided committer upon commit\n      val e = intercept[RuntimeException] {\n        txn.commit(engine, emptyIterable())\n      }\n      assert(e.getMessage.contains(\"This is a fake committer\"))\n    }\n  }\n\n  test(\"create table fails when the table already exists (non-catalog-managed)\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n      intercept[TableAlreadyExistsException] {\n        TableManager.buildCreateTableTransaction(\n          tablePath,\n          testSchema,\n          testEngineInfo).build(engine)\n      }\n    }\n  }\n\n  test(\"CreateTableTransactionBuilderImpl::build does NOT check if the table already exists \" +\n    \"when creating a catalogManaged table\") {\n    // The catalog is responsible for providing a valid, empty table location when creating\n    // catalogManaged tables. CreateTableTransactionBuilderImpl::build does NOT check if the table\n    // exists when creating a catalogManaged table.\n    //\n    // This test validates that the above logic is correct, by first creating a table at a given\n    // path P, and then trying to create a catalogManaged table at the same path P. We expect that\n    // CreateTableTransactionBuilderImpl::build should NOT throw.\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create the table\n      createEmptyTable(engine, tablePath, testSchema)\n\n      // Now create it again but with catalogManaged supported. This should NOT throw.\n      TableManager.buildCreateTableTransaction(tablePath, testSchema, testEngineInfo)\n        .withTableProperties(\n          Map(\n            TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey\n              -> \"supported\").asJava)\n        .build(engine)\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests that builder impls correctly propagate inputs + outputs for TransactionMetadataFactory //\n  //////////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Table props are checked in TablePropertiesTransactionBuilderV2Suite + DeltaTableWritesSuite\n  // Transaction Id is checked in DeltaTableWritesSuite\n  // Max retries is checked DeltaTableWritesSuite for Update\n  // Creating partitioned table is covered in DeltaTableWritesSuite\n\n  test(\"Creating a table with clustering columns and updating the clustering columns\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      def checkClusteringColsWithSpark(expectedCols: Seq[Seq[String]]): Unit = {\n        val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n        val clusteringMetadataDomainRead =\n          SparkClusteringMetadataDomain.fromSnapshot(deltaLog.update())\n        assert(clusteringMetadataDomainRead.exists(_.clusteringColumns === expectedCols))\n      }\n      createEmptyTable(\n        engine,\n        tablePath,\n        testPartitionSchema,\n        clusteringColsOpt = Some(testClusteringColumns))\n      // Validate with Spark that the clustering columns are set\n      checkClusteringColsWithSpark(Seq(Seq(\"part1\"), Seq(\"part2\")))\n      // Update clustering columns and check that they are updated\n      updateTableMetadata(engine, tablePath, clusteringColsOpt = Some(List(new Column(\"part1\"))))\n      checkClusteringColsWithSpark(Seq(Seq(\"part1\")))\n    }\n  }\n\n  test(\"Can evolve schema using withUpdatedSchema\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = Map(ColumnMapping.COLUMN_MAPPING_MODE_KEY -> \"id\"))\n      val currentSchema = getMetadata(engine, tablePath).getSchema\n      assert(currentSchema.indexOf(\"newCol\") == -1)\n      val newSchema = currentSchema.add(\"newCol\", IntegerType.INTEGER)\n      updateTableMetadata(engine, tablePath, schema = newSchema)\n      // Validate that the new column exits\n      assert(getMetadata(engine, tablePath).getSchema.indexOf(\"newCol\") >= 0)\n    }\n  }\n\n  test(\"maxRetries is obeyed for Create table (error not a conflict)\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val fileIO = new HadoopFileIO(new Configuration())\n      class CustomJsonHandler extends DefaultJsonHandler(fileIO) {\n        var attemptCount = 0\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          attemptCount += 1\n          if (attemptCount == 1) {\n            // The default committer will turn this into a CFE(isRetryable=true, isConflict=false)\n            throw new java.io.IOException(\"Transient network error\")\n          }\n          super.writeJsonFileAtomically(filePath, data, overwrite)\n        }\n      }\n\n      class CustomEngine extends DefaultEngine(fileIO) {\n        val jsonHandler = new CustomJsonHandler()\n        override def getJsonHandler: JsonHandler = jsonHandler\n      }\n\n      // Commit fails when maxRetries = 0\n      {\n        val transientErrorEngine = new CustomEngine()\n        intercept[MaxCommitRetryLimitReachedException] {\n          getCreateTxn(\n            transientErrorEngine,\n            tablePath,\n            schema = testSchema,\n            maxRetries = 0).commit(transientErrorEngine, emptyIterable())\n        }\n      }\n\n      // Commit succeeds when maxRetries > 1\n      {\n        val transientErrorEngine = new CustomEngine()\n        getCreateTxn(\n          transientErrorEngine,\n          tablePath,\n          schema = testSchema,\n          maxRetries = 10).commit(transientErrorEngine, emptyIterable())\n      }\n    }\n  }\n\n  test(\"CreateTableTransactionBuilder uses the committer when provided\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      class FakeCommitter extends Committer {\n        override def commit(\n            engine: Engine,\n            finalizedActions: CloseableIterator[Row],\n            commitMetadata: CommitMetadata): CommitResponse = {\n          throw new RuntimeException(\"This is a fake committer\")\n        }\n      }\n      val txn = TableManager.buildCreateTableTransaction(tablePath, testSchema, testEngineInfo)\n        .withCommitter(new FakeCommitter())\n        .build(engine)\n\n      // Check the txn returns the correct committer\n      assert(txn.getCommitter.isInstanceOf[FakeCommitter])\n\n      // Check that the txn invokes the provided committer upon commit\n      val e = intercept[RuntimeException] {\n        txn.commit(engine, emptyIterable())\n      }\n      assert(e.getMessage.contains(\"This is a fake committer\"))\n    }\n  }\n\n  test(\"CreateTableTransactionBuilder uses the default file system committer if none is provided\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn = TableManager.buildCreateTableTransaction(tablePath, testSchema, testEngineInfo)\n        .build(engine)\n      // Check the txn returns the correct committer\n      assert(txn.getCommitter == DefaultFileSystemManagedTableOnlyCommitter.INSTANCE)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DirectoryCreationSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{Operation, TableManager}\nimport io.delta.kernel.defaults.utils.WriteUtils\nimport io.delta.kernel.internal.actions.Protocol\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.DirectoryCreationUtils.createAllDeltaDirectoriesAsNeeded\nimport io.delta.kernel.test.ActionUtils\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DirectoryCreationSuite extends AnyFunSuite with WriteUtils with ActionUtils {\n\n  ///////////////\n  // E2E Tests //\n  ///////////////\n\n  test(\"create table with catalogManaged & v2Checkpoints supported: _delta_log, _staged_commits, \" +\n    \"_sidecars directories are created\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val logDir = new File(tablePath, \"_delta_log\")\n      val stagedCommitsDir = new File(logDir.toString, \"_staged_commits\")\n      val sidecarDir = new File(logDir.toString, \"_sidecars\")\n\n      TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withTableProperties(Map(\n          TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey -> \"supported\",\n          \"delta.checkpointPolicy\" -> \"v2\").asJava)\n        .withCommitter(committerUsingPutIfAbsent)\n        .build(engine)\n        .commit(engine, emptyIterable())\n\n      assert(logDir.exists() && logDir.isDirectory())\n      assert(stagedCommitsDir.exists() && stagedCommitsDir.isDirectory())\n      assert(sidecarDir.exists() && sidecarDir.isDirectory())\n    }\n  }\n\n  test(\"enable catalogManaged & v2Checkpoints on existing table: _staged_commits and _sidecars \" +\n    \"directories are created\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val logDir = new File(tablePath, \"_delta_log\")\n      val stagedCommitsDir = new File(logDir.toString, \"_staged_commits\")\n      val sidecarDir = new File(logDir.toString, \"_sidecars\")\n\n      getCreateTxn(engine, tablePath, testSchema).commit(engine, emptyIterable())\n\n      assert(logDir.exists() && logDir.isDirectory())\n      assert(!sidecarDir.exists())\n      assert(!stagedCommitsDir.exists())\n\n      TableManager\n        .loadSnapshot(tablePath)\n        .withCommitter(committerUsingPutIfAbsent)\n        .build(engine)\n        .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n        .withTablePropertiesAdded(Map(\n          TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey -> \"supported\",\n          \"delta.checkpointPolicy\" -> \"v2\").asJava)\n        .build(engine)\n        .commit(engine, emptyIterable())\n\n      assert(stagedCommitsDir.exists() && stagedCommitsDir.isDirectory())\n      assert(sidecarDir.exists() && sidecarDir.isDirectory())\n    }\n  }\n\n  ////////////////\n  // Unit tests //\n  ////////////////\n\n  val basicProtocol = new Protocol(1, 2)\n\n  val protocolWithCatalogManagedAndV2CheckpointSupport =\n    new Protocol(\n      TableFeatures.TABLE_FEATURES_MIN_READER_VERSION,\n      TableFeatures.TABLE_FEATURES_MIN_WRITER_VERSION,\n      Set(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.featureName(),\n        TableFeatures.CHECKPOINT_V2_RW_FEATURE.featureName()).asJava,\n      Set(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.featureName(),\n        TableFeatures.CHECKPOINT_V2_RW_FEATURE.featureName(),\n        TableFeatures.IN_COMMIT_TIMESTAMP_W_FEATURE.featureName()).asJava)\n\n  test(\"_delta_log -- Case: CREATE\") {\n    withTempDirAndEngine { (tempDir, engine) =>\n      val logPath = new Path(tempDir, \"_delta_log\")\n\n      createAllDeltaDirectoriesAsNeeded(\n        engine,\n        logPath,\n        0,\n        Optional.empty[Protocol](),\n        basicProtocol)\n\n      val logDir = new File(logPath.toString)\n      assert(logDir.exists() && logDir.isDirectory())\n    }\n  }\n\n  test(\"_sidecars && _staged_commits -- Case: CREATE\") {\n    withTempDirAndEngine { (tempDir, engine) =>\n      val logPath = new Path(tempDir, \"_delta_log\")\n\n      createAllDeltaDirectoriesAsNeeded(\n        engine,\n        logPath,\n        0,\n        Optional.empty[Protocol](),\n        protocolWithCatalogManagedAndV2CheckpointSupport)\n\n      // Check delta log directory\n      val logDir = new File(logPath.toString)\n      assert(logDir.exists() && logDir.isDirectory())\n\n      // Check staged commits directory\n      val stagedCommitDir = new File(logPath.toString, \"_staged_commits\")\n      assert(stagedCommitDir.exists() && stagedCommitDir.isDirectory())\n\n      // Check sidecar directory\n      val sidecarDir = new File(logPath.toString, \"_sidecars\")\n      assert(sidecarDir.exists() && sidecarDir.isDirectory())\n    }\n  }\n\n  test(\"_sidecars && _staged_commits -- Case: UPGRADE\") {\n    withTempDirAndEngine { (tempDir, engine) =>\n      val logPath = new Path(tempDir, \"_delta_log\")\n\n      createAllDeltaDirectoriesAsNeeded(\n        engine,\n        logPath,\n        0,\n        Optional.of(basicProtocol),\n        protocolWithCatalogManagedAndV2CheckpointSupport)\n\n      // All directories should be created\n      assert(new File(logPath.toString).exists())\n      assert(new File(logPath.toString, \"_staged_commits\").exists())\n      assert(new File(logPath.toString, \"_sidecars\").exists())\n    }\n  }\n\n  test(\"handles existing directories gracefully\") {\n    withTempDirAndEngine { (tempDir, engine) =>\n      val logPath = new Path(tempDir, \"_delta_log\")\n\n      // Pre-create all directories\n      new File(logPath.toString).mkdirs()\n      new File(logPath.toString, \"_staged_commits\").mkdirs()\n      new File(logPath.toString, \"_sidecars\").mkdirs()\n\n      // Should not throw exception when directories already exist\n      createAllDeltaDirectoriesAsNeeded(\n        engine,\n        logPath,\n        0,\n        Optional.empty[Protocol](),\n        protocolWithCatalogManagedAndV2CheckpointSupport)\n\n      // Directories should still exist\n      assert(new File(logPath.toString).exists())\n      assert(new File(logPath.toString, \"_staged_commits\").exists())\n      assert(new File(logPath.toString, \"_sidecars\").exists())\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DomainMetadataCheckSumReplayMetricsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\nimport io.delta.kernel.Table\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\n\nclass LegacyDomainMetadataCheckSumReplayMetricsSuite\n    extends AbstractDomainMetadataCheckSumReplayMetricsSuite\n    with TestUtilsWithLegacyKernelAPIs {\n\n  // SnapshotHint tests only apply for the legacy APIs since in the new APIs there is no persistent\n  // Table instance\n  test(\"read domain metadata fro checksum even if snapshot hint exists\") {\n    withTableWithCrc { (tablePath, engine) =>\n      val readVersion = 11\n      val table = Table.forPath(engine, tablePath)\n      // Get snapshot to produce a snapshot hit at version 11.\n      table.getLatestSnapshot(engine)\n\n      engine.resetMetrics()\n      table.getLatestSnapshot(engine).getDomainMetadata(\"foo\")\n\n      assertMetrics(\n        engine,\n        expJsonVersionsRead = Nil,\n        expParquetVersionsRead = Nil,\n        expParquetReadSetSizes = Seq(),\n        expChecksumReadSet = Seq(readVersion))\n    }\n  }\n\n}\n\nclass DomainMetadataCheckSumReplayMetricsSuite\n    extends AbstractDomainMetadataCheckSumReplayMetricsSuite\n    with TestUtilsWithTableManagerAPIs\n\n/**\n * Suite to test the engine metrics when loading Domain Metadata through checksum files.\n */\ntrait AbstractDomainMetadataCheckSumReplayMetricsSuite extends ChecksumLogReplayMetricsTestBase {\n  self: AbstractTestUtils =>\n\n  override protected def loadPandMCheckMetrics(\n      tablePath: String,\n      engine: MetricsEngine,\n      expJsonVersionsRead: Seq[Long],\n      expParquetVersionsRead: Seq[Long],\n      expParquetReadSetSizes: Seq[Long],\n      expChecksumReadSet: Seq[Long],\n      readVersion: Long = -1): Unit = {\n\n    engine.resetMetrics()\n\n    readVersion match {\n      case -1 =>\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).getDomainMetadata(\"foo\")\n      case ver => getTableManagerAdapter.getSnapshotAtVersion(\n          engine,\n          tablePath,\n          ver).getDomainMetadata(\"foo\")\n    }\n\n    assertMetrics(\n      engine,\n      expJsonVersionsRead,\n      expParquetVersionsRead,\n      expParquetReadSetSizes,\n      expChecksumReadSet = expChecksumReadSet)\n  }\n\n  override protected def isDomainMetadataReplay: Boolean = true\n\n  // Domain metadata requires reading checkpoint files twice:\n  // 1. First read happens during loading Protocol & Metadata in snapshot construction.\n  // 2. Second read happens specifically for domain metadata loading.\n  override protected def getExpectedCheckpointReadSize(sizes: Seq[Long]): Seq[Long] = {\n    // we read each checkpoint file twice: once for P&M and once for domain metadata\n    sizes.flatMap(size => Seq(size, size))\n  }\n\n  test(\"checksum doesn't contain domain metadata => read from logs\") {\n    withTableWithCrc { (tablePath, engine) =>\n      val readVersion = 5L\n      rewriteChecksumFileToExcludeDomainMetadata(engine, tablePath, readVersion)\n\n      loadPandMCheckMetrics(\n        tablePath,\n        engine,\n        expJsonVersionsRead = Seq.range(readVersion, -1, -1),\n        expParquetVersionsRead = Nil,\n        expParquetReadSetSizes = Nil,\n        expChecksumReadSet = Seq(readVersion),\n        readVersion = readVersion)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/DomainMetadataSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.immutable.Seq\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel._\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.internal.parquet.ParquetSuiteBase\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV1Builders, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions._\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.{SnapshotImpl, TableImpl, TransactionImpl}\nimport io.delta.kernel.internal.actions.DomainMetadata\nimport io.delta.kernel.internal.checksum.ChecksumReader\nimport io.delta.kernel.internal.rowtracking.RowTrackingMetadataDomain\nimport io.delta.kernel.utils.CloseableIterable\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.RowId.{RowTrackingMetadataDomain => SparkRowTrackingMetadataDomain}\nimport org.apache.spark.sql.delta.actions.{DomainMetadata => SparkDomainMetadata}\n\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Runs domain metadata tests using the TableManager snapshot APIs and V2 transaction builders */\nclass DomainMetadataSuite extends AbstractDomainMetadataSuite with WriteUtilsWithV2Builders\n\n/** Runs domain metadata tests using the legacy Table snapshot APIs and V1 transaction builders */\nclass LegacyDomainMetadataSuite extends AbstractDomainMetadataSuite with WriteUtilsWithV1Builders\n\ntrait AbstractDomainMetadataSuite extends AnyFunSuite with AbstractWriteUtils\n    with ParquetSuiteBase {\n\n  private def assertDomainMetadata(\n      snapshot: SnapshotImpl,\n      expectedValue: Map[String, DomainMetadata]): Unit = {\n    // Check using internal API\n    assert(expectedValue === snapshot.getActiveDomainMetadataMap.asScala)\n    // Verify public API\n    expectedValue.foreach { case (key, domainMetadata) =>\n      snapshot.getDomainMetadata(key).toScala match {\n        case Some(config) =>\n          assert(!domainMetadata.isRemoved && config == domainMetadata.getConfiguration)\n        case None =>\n          assert(domainMetadata.isRemoved)\n      }\n    }\n  }\n\n  private def assertDomainMetadata(\n      tablePath: String,\n      engine: Engine,\n      expectedValue: Map[String, DomainMetadata]): Unit = {\n    // Get the table and latest snapshot\n    val snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n    assertDomainMetadata(snapshot, expectedValue)\n    // verifyChecksum will check the domain metadata in CRC against the lastest snapshot.\n    verifyChecksum(tablePath)\n    // Delete CRC and reload snapshot from log.\n    deleteChecksumFileForTable(\n      tablePath.stripPrefix(\"file:\"),\n      versions = Seq(snapshot.getVersion.toInt))\n    // Rebuild table to avoid loading domain metadata from cached crc info.\n    assertDomainMetadata(\n      getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath),\n      expectedValue)\n    // Write CRC back so that subsequence operation could generate CRC incrementally.\n    Table.forPath(engine, tablePath).checksum(engine, snapshot.getVersion)\n  }\n\n  private def commitDomainMetadataAndVerify(\n      engine: Engine,\n      tablePath: String,\n      domainMetadatas: Seq[DomainMetadata],\n      expectedValue: Map[String, DomainMetadata],\n      useInternalApi: Boolean = false): Unit = {\n    // Create the transaction with domain metadata and commit\n\n    val txn = createTxnWithDomainMetadatas(\n      engine,\n      tablePath,\n      domainMetadatas,\n      useInternalApi)\n    commitTransaction(txn, engine, emptyIterable())\n\n    // Verify the final state includes the expected domain metadata\n    assertDomainMetadata(tablePath, engine, expectedValue)\n  }\n\n  private def createTableWithDomainMetadataSupported(engine: Engine, tablePath: String): Unit = {\n    // Create an empty table\n    commitTransaction(\n      getCreateTxn(\n        engine,\n        tablePath,\n        testSchema,\n        withDomainMetadataSupported = true),\n      engine,\n      emptyIterable())\n  }\n\n  private def validateDomainMetadataConflictResolution(\n      engine: Engine,\n      tablePath: String,\n      currentTxn1DomainMetadatas: Seq[DomainMetadata],\n      winningTxn2DomainMetadatas: Seq[DomainMetadata],\n      winningTxn3DomainMetadatas: Seq[DomainMetadata],\n      expectedConflict: Boolean): Unit = {\n    // Create table with domain metadata support\n    createTableWithDomainMetadataSupported(engine, tablePath)\n\n    /**\n     * Txn1: i.e. the current transaction that comes later than winning transactions.\n     * Txn2: i.e. the winning transaction that was committed first.\n     * Txn3: i.e. the winning transaction that was committed secondly.\n     *\n     * Note tx is the timestamp.\n     *\n     * t1 ------------------------ Txn1 starts.\n     * t2 ------- Txn2 starts.\n     * t3 ------- Txn2 commits.\n     * t4 ------- Txn3 starts.\n     * t5 ------- Txn3 commits.\n     * t6 ------------------------ Txn1 commits (SUCCESS or FAIL).\n     */\n    // For these txns, set enableDomainMetadata = false since it's already been enabled in the\n    // initial table, and for V2 builders, re-enabling it will commit a new Metadata change (which\n    // will always trigger a conflict!)\n    val txn1 = createTxnWithDomainMetadatas(\n      engine,\n      tablePath,\n      currentTxn1DomainMetadatas,\n      enableDomainMetadata = false)\n\n    val txn2 = createTxnWithDomainMetadatas(\n      engine,\n      tablePath,\n      winningTxn2DomainMetadatas,\n      enableDomainMetadata = false)\n    commitTransaction(txn2, engine, emptyIterable())\n\n    val txn3 = createTxnWithDomainMetadatas(\n      engine,\n      tablePath,\n      winningTxn3DomainMetadatas,\n      enableDomainMetadata = false)\n    commitTransaction(txn3, engine, emptyIterable())\n\n    if (expectedConflict) {\n      // We expect the commit of txn1 to fail because of the conflicting DM actions\n      val ex = intercept[KernelException] {\n        commitTransaction(txn1, engine, emptyIterable())\n      }\n      assert(\n        ex.getMessage.contains(\n          \"A concurrent writer added a domainMetadata action for the same domain\"))\n    } else {\n      // We expect the commit of txn1 to succeed\n      commitTransaction(txn1, engine, emptyIterable())\n      // Verify the final state includes merged domain metadata\n      val expectedMetadata =\n        (winningTxn2DomainMetadatas ++ winningTxn3DomainMetadatas ++ currentTxn1DomainMetadatas)\n          .groupBy(_.getDomain)\n          .view.mapValues(_.last).toMap\n      assertDomainMetadata(tablePath, engine, expectedMetadata)\n    }\n  }\n\n  override def commitTransaction(\n      txn: Transaction,\n      engine: Engine,\n      dataActions: CloseableIterable[Row]): TransactionCommitResult = {\n    executeCrcSimple(txn.commit(engine, dataActions), engine)\n  }\n\n  test(\"create table w/o domain metadata\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create an empty table\n      commitTransaction(\n        getCreateTxn(engine, tablePath, testSchema),\n        engine,\n        emptyIterable())\n\n      // Verify that the table doesn't have any domain metadata\n      assertDomainMetadata(tablePath, engine, Map.empty)\n    }\n  }\n\n  test(\"table w/o domain metadata support fails domain metadata commits\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create an empty table\n      // Its minWriterVersion is 2 and doesn't have 'domainMetadata' in its writerFeatures\n      commitTransaction(\n        getCreateTxn(\n          engine,\n          tablePath,\n          testSchema),\n        engine,\n        emptyIterable())\n\n      val dm1 = new DomainMetadata(\"domain1\", \"\", false)\n      // We use the internal API because our public API will automatically upgrade the protocol\n      val txn1 = createTxnWithDomainMetadatas(engine, tablePath, List(dm1), useInternalApi = true)\n\n      // We expect the commit to fail because the table doesn't support domain metadata\n      val e = intercept[KernelException] {\n        commitTransaction(txn1, engine, emptyIterable())\n      }\n      assert(\n        e.getMessage\n          .contains(\n            \"Cannot commit DomainMetadata action(s) because the feature 'domainMetadata' \"\n              + \"is not supported on this table.\"))\n\n      // Commit domain metadata again and expect success\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        domainMetadatas = Seq(dm1),\n        expectedValue = Map(\"domain1\" -> dm1))\n    }\n  }\n\n  test(\"latest domain metadata overwriting existing ones\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      val dm1 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"1\"}, {\"key2\":\"2\"}\"\"\", false)\n      val dm2 = new DomainMetadata(\"domain2\", \"\", false)\n      val dm3 = new DomainMetadata(\"domain3\", \"\"\"{\"key3\":\"3\"}\"\"\", false)\n\n      val dm1_2 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"10\"}\"\"\", false)\n      val dm3_2 = new DomainMetadata(\"domain3\", \"\"\"{\"key3\":\"30\"}\"\"\", false)\n\n      Seq(\n        (Seq(dm1), Map(\"domain1\" -> dm1)),\n        (Seq(dm2, dm3, dm1_2), Map(\"domain1\" -> dm1_2, \"domain2\" -> dm2, \"domain3\" -> dm3)),\n        (Seq(dm3_2), Map(\"domain1\" -> dm1_2, \"domain2\" -> dm2, \"domain3\" -> dm3_2))).foreach {\n        case (domainMetadatas, expectedValue) =>\n          commitDomainMetadataAndVerify(engine, tablePath, domainMetadatas, expectedValue)\n      }\n    }\n  }\n\n  test(\"domain metadata persistence across log replay\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      val dm1 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"1\"}, {\"key2\":\"2\"}\"\"\", false)\n      val dm2 = new DomainMetadata(\"domain2\", \"\", false)\n\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        domainMetadatas = Seq(dm1, dm2),\n        expectedValue = Map(\"domain1\" -> dm1, \"domain2\" -> dm2))\n\n      // Restart the table and verify the domain metadata\n      assertDomainMetadata(tablePath, engine, Map(\"domain1\" -> dm1, \"domain2\" -> dm2))\n    }\n  }\n\n  test(\"only the latest domain metadata per domain is stored in checkpoints\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      val dm1 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"1\"}, {\"key2\":\"2\"}\"\"\", false)\n      val dm2 = new DomainMetadata(\"domain2\", \"\", false)\n      val dm3 = new DomainMetadata(\"domain3\", \"\"\"{\"key3\":\"3\"}\"\"\", false)\n      val dm1_2 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"10\"}\"\"\", false)\n      val dm3_2 = new DomainMetadata(\"domain3\", \"\"\"{\"key3\":\"3\"}\"\"\", true)\n\n      Seq(\n        (Seq(dm1), Map(\"domain1\" -> dm1)),\n        (Seq(dm2), Map(\"domain1\" -> dm1, \"domain2\" -> dm2)),\n        (Seq(dm3), Map(\"domain1\" -> dm1, \"domain2\" -> dm2, \"domain3\" -> dm3)),\n        (\n          Seq(dm1_2, dm3_2),\n          Map(\"domain1\" -> dm1_2, \"domain2\" -> dm2))).foreach {\n        case (domainMetadatas, expectedValue) =>\n          commitDomainMetadataAndVerify(engine, tablePath, domainMetadatas, expectedValue)\n      }\n\n      // Checkpoint the table\n      val latestVersion = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).getVersion()\n      Table.forPath(engine, tablePath).checkpoint(engine, latestVersion)\n\n      // Verify that only the latest domain metadata is persisted in the checkpoint\n      assertDomainMetadata(\n        tablePath,\n        engine,\n        Map(\"domain1\" -> dm1_2, \"domain2\" -> dm2))\n    }\n  }\n\n  test(\"Conflict resolution - one of three concurrent txns has DomainMetadata\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      /**\n       * Txn1: include DomainMetadata action.\n       * Txn2: does NOT include DomainMetadata action.\n       * Txn3: does NOT include DomainMetadata action.\n       *\n       * t1 ------------------------ Txn1 starts.\n       * t2 ------- Txn2 starts.\n       * t3 ------- Txn2 commits.\n       * t4 ------- Txn3 starts.\n       * t5 ------- Txn3 commits.\n       * t6 ------------------------ Txn1 commits (SUCCESS).\n       */\n      val dm1 = new DomainMetadata(\"domain1\", \"\", false)\n\n      validateDomainMetadataConflictResolution(\n        engine,\n        tablePath,\n        currentTxn1DomainMetadatas = Seq(dm1),\n        winningTxn2DomainMetadatas = Seq.empty,\n        winningTxn3DomainMetadatas = Seq.empty,\n        expectedConflict = false)\n    }\n  }\n\n  test(\n    \"Conflict resolution - three concurrent txns have DomainMetadata w/o conflicting domains\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      /**\n       * Txn1: include DomainMetadata action for \"domain1\".\n       * Txn2: include DomainMetadata action for \"domain2\".\n       * Txn3: include DomainMetadata action for \"domain3\".\n       *\n       * t1 ------------------------ Txn1 starts.\n       * t2 ------- Txn2 starts.\n       * t3 ------- Txn2 commits.\n       * t4 ------- Txn3 starts.\n       * t5 ------- Txn3 commits.\n       * t6 ------------------------ Txn1 commits (SUCCESS).\n       */\n      val dm1 = new DomainMetadata(\"domain1\", \"\", false)\n      val dm2 = new DomainMetadata(\"domain2\", \"\", false)\n      val dm3 = new DomainMetadata(\"domain3\", \"\", false)\n\n      validateDomainMetadataConflictResolution(\n        engine,\n        tablePath,\n        currentTxn1DomainMetadatas = Seq(dm1),\n        winningTxn2DomainMetadatas = Seq(dm2),\n        winningTxn3DomainMetadatas = Seq(dm3),\n        expectedConflict = false)\n    }\n  }\n\n  test(\n    \"Conflict resolution - three concurrent txns have DomainMetadata w/ conflicting domains\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      /**\n       * Txn1: include DomainMetadata action for \"domain1\".\n       * Txn2: include DomainMetadata action for \"domain2\".\n       * Txn3: include DomainMetadata action for \"domain1\".\n       *\n       * t1 ------------------------ Txn1 starts.\n       * t2 ------- Txn2 starts.\n       * t3 ------- Txn2 commits.\n       * t4 ------- Txn3 starts.\n       * t5 ------- Txn3 commits.\n       * t6 ------------------------ Txn1 commits (FAIL).\n       */\n      val dm1 = new DomainMetadata(\"domain1\", \"\", false)\n      val dm2 = new DomainMetadata(\"domain2\", \"\", false)\n      val dm3 = new DomainMetadata(\"domain1\", \"\", false)\n\n      validateDomainMetadataConflictResolution(\n        engine,\n        tablePath,\n        currentTxn1DomainMetadatas = Seq(dm1),\n        winningTxn2DomainMetadatas = Seq(dm2),\n        winningTxn3DomainMetadatas = Seq(dm3),\n        expectedConflict = true)\n    }\n  }\n\n  test(\n    \"Conflict resolution - three concurrent txns have DomainMetadata w/ conflict domains - 2\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      /**\n       * Txn1: include DomainMetadata action for \"domain1\".\n       * Txn2: include DomainMetadata action for \"domain1\".\n       * Txn3: include DomainMetadata action for \"domain2\".\n       *\n       * t1 ------------------------ Txn1 starts.\n       * t2 ------- Txn2 starts.\n       * t3 ------- Txn2 commits.\n       * t4 ------- Txn3 starts.\n       * t5 ------- Txn3 commits.\n       * t6 ------------------------ Txn1 commits (FAIL).\n       */\n      val dm1 = new DomainMetadata(\"domain1\", \"\", false)\n      val dm2 = new DomainMetadata(\"domain1\", \"\", false)\n      val dm3 = new DomainMetadata(\"domain2\", \"\", false)\n\n      validateDomainMetadataConflictResolution(\n        engine,\n        tablePath,\n        currentTxn1DomainMetadatas = Seq(dm1),\n        winningTxn2DomainMetadatas = Seq(dm2),\n        winningTxn3DomainMetadatas = Seq(dm3),\n        expectedConflict = true)\n    }\n  }\n\n  test(\"Integration test - create a table with Spark and read its domain metadata using Kernel\") {\n    withTempDir(dir => {\n      withTempTable { tbl =>\n        val tablePath = dir.getCanonicalPath\n        // Create table with domain metadata enabled\n        spark.sql(s\"CREATE TABLE $tbl (id LONG) USING delta LOCATION '$tablePath'\")\n        spark.sql(\n          s\"ALTER TABLE $tbl SET TBLPROPERTIES(\" +\n            s\"'delta.feature.domainMetadata' = 'enabled',\" +\n            s\"'delta.checkpointInterval' = '3')\")\n\n        // Manually commit domain metadata actions. This will create 02.json\n        val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n        deltaLog\n          .startTransaction()\n          .commitManuallyWithValidation(\n            SparkDomainMetadata(\"testDomain1\", \"{\\\"key1\\\":\\\"1\\\"}\", removed = false),\n            SparkDomainMetadata(\"testDomain2\", \"\", removed = false),\n            SparkDomainMetadata(\"testDomain3\", \"\", removed = false))\n\n        // This will create 03.json and 03.checkpoint\n        spark.range(0, 2).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n        // Manually commit domain metadata actions. This will create 04.json\n        deltaLog\n          .startTransaction()\n          .commitManuallyWithValidation(\n            SparkDomainMetadata(\"testDomain1\", \"{\\\"key1\\\":\\\"10\\\"}\", removed = false),\n            SparkDomainMetadata(\"testDomain2\", \"\", removed = true))\n\n        // Use Delta Kernel to read the table's domain metadata and verify the result.\n        // We will need to read 1 checkpoint file and 1 log file to replay the table.\n        // The state of the domain metadata should be:\n        // testDomain1: \"{\\\"key1\\\":\\\"10\\\"}\", removed = false  (from 03.checkpoint)\n        // testDomain2: \"\", removed = true                    (from 03.checkpoint)\n        // testDomain3: \"\", removed = false                   (from 04.json)\n\n        val dm1 = new DomainMetadata(\"testDomain1\", \"\"\"{\"key1\":\"10\"}\"\"\", false)\n        val dm3 = new DomainMetadata(\"testDomain3\", \"\", false)\n\n        assertDomainMetadata(\n          tablePath,\n          defaultEngine,\n          Map(\"testDomain1\" -> dm1, \"testDomain3\" -> dm3))\n      }\n    })\n  }\n\n  test(\"Integration test - create a table using Kernel and read its domain metadata using Spark\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      withTempTable { tbl =>\n        // Create table with domain metadata enabled\n        createTableWithDomainMetadataSupported(engine, tablePath)\n\n        // Manually commit three domain metadata actions\n        val dm1 = new DomainMetadata(\"testDomain1\", \"\"\"{\"key1\":\"1\"}\"\"\", false)\n        val dm2 = new DomainMetadata(\"testDomain2\", \"\", false)\n        val dm3 = new DomainMetadata(\"testDomain3\", \"\", false)\n        commitDomainMetadataAndVerify(\n          engine,\n          tablePath,\n          domainMetadatas = Seq(dm1, dm2, dm3),\n          expectedValue = Map(\"testDomain1\" -> dm1, \"testDomain2\" -> dm2, \"testDomain3\" -> dm3))\n\n        appendData(\n          engine,\n          tablePath,\n          data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n        // Checkpoint the table so domain metadata is distributed to both checkpoint and log files\n        val latestVersion =\n          getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).getVersion()\n        Table.forPath(engine, tablePath).checkpoint(engine, latestVersion)\n\n        // Manually commit two domain metadata actions\n        val dm1_2 = new DomainMetadata(\"testDomain1\", \"\"\"{\"key1\":\"10\"}\"\"\", false)\n        val dm2_2 = new DomainMetadata(\"testDomain2\", \"\", true)\n        commitDomainMetadataAndVerify(\n          engine,\n          tablePath,\n          domainMetadatas = Seq(dm1_2, dm2_2),\n          expectedValue = Map(\"testDomain1\" -> dm1_2, \"testDomain3\" -> dm3))\n\n        // Use Spark to read the table's domain metadata and verify the result\n        val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n        val domainMetadata = deltaLog.snapshot.domainMetadata.groupBy(_.domain).map {\n          case (name, domains) =>\n            assert(domains.size == 1)\n            name -> domains.head\n        }\n        // Note that in Delta-Spark, the deltaLog.snapshot.domainMetadata does not include\n        // domain metadata that are removed.\n        assert(\n          domainMetadata === Map(\n            \"testDomain1\" -> SparkDomainMetadata(\n              \"testDomain1\",\n              \"\"\"{\"key1\":\"10\"}\"\"\",\n              removed = false),\n            \"testDomain3\" -> SparkDomainMetadata(\"testDomain3\", \"\", removed = false)))\n      }\n    }\n  }\n\n  test(\"RowTrackingMetadataDomain can be committed and read\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      val rowTrackingMetadataDomain = new RowTrackingMetadataDomain(10)\n      val dmAction = rowTrackingMetadataDomain.toDomainMetadata\n\n      // The configuration string should be a JSON serialization of the rowTrackingMetadataDomain\n      assert(dmAction.getDomain === rowTrackingMetadataDomain.getDomainName)\n      assert(dmAction.getConfiguration === \"\"\"{\"rowIdHighWaterMark\":10}\"\"\")\n\n      // Commit the DomainMetadata action and verify\n      createTableWithDomainMetadataSupported(engine, tablePath)\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        domainMetadatas = Seq(dmAction),\n        expectedValue = Map(rowTrackingMetadataDomain.getDomainName -> dmAction),\n        useInternalApi = true // cannot commit system-controlled domains through public API\n      )\n\n      // Read the RowTrackingMetadataDomain from the table and verify\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      val rowTrackingMetadataDomainFromSnapshot =\n        RowTrackingMetadataDomain.fromSnapshot(snapshot)\n\n      assert(rowTrackingMetadataDomainFromSnapshot.isPresent)\n      assert(rowTrackingMetadataDomain === rowTrackingMetadataDomainFromSnapshot.get)\n    })\n  }\n\n  test(\"RowTrackingMetadataDomain Integration test - Write with Spark and read with Kernel\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      withTempTable { tbl =>\n        // Create table with domain metadata enabled using Spark\n        spark.sql(s\"CREATE TABLE $tbl (id LONG) USING delta LOCATION '$tablePath'\")\n        spark.sql(\n          s\"ALTER TABLE $tbl SET TBLPROPERTIES(\" +\n            s\"'delta.feature.domainMetadata' = 'enabled',\" +\n            s\"'delta.feature.rowTracking' = 'supported')\")\n\n        // Append 100 rows to the table, with fresh row IDs from 0 to 99\n        // The `delta.rowTracking.rowIdHighWaterMark` should be 99\n        spark.range(0, 20).write.format(\"delta\").mode(\"append\").save(tablePath)\n        spark.range(20, 100).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n        // Read the RowTrackingMetadataDomain from the table using Kernel\n        val snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n        val rowTrackingMetadataDomainRead = RowTrackingMetadataDomain.fromSnapshot(snapshot)\n\n        assert(rowTrackingMetadataDomainRead.isPresent)\n        assert(rowTrackingMetadataDomainRead.get.getRowIdHighWaterMark === 99)\n      }\n    })\n  }\n\n  test(\"RowTrackingMetadataDomain Integration test - Write with Kernel and read with Spark\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      withTempTable { tbl =>\n        // Create table and manually make changes to the row tracking metadata domain using Kernel\n        createTableWithDomainMetadataSupported(engine, tablePath)\n        val dmAction = new RowTrackingMetadataDomain(10).toDomainMetadata\n        commitDomainMetadataAndVerify(\n          engine,\n          tablePath,\n          domainMetadatas = Seq(dmAction),\n          expectedValue = Map(dmAction.getDomain -> dmAction),\n          useInternalApi = true // cannot commit system-controlled domains through public API\n        )\n\n        // Use Spark to read the table's row tracking metadata domain and verify the result\n        val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n        val rowTrackingMetadataDomainRead =\n          SparkRowTrackingMetadataDomain.fromSnapshot(deltaLog.snapshot)\n        assert(rowTrackingMetadataDomainRead.exists(_.rowIdHighWaterMark === 10))\n      }\n    }\n  }\n\n  test(\"basic txn.addDomainMetadata API tests\") {\n    // addDomainMetadata is tested thoroughly elsewhere in this suite, here we just test API\n    // specific behaviors\n\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      // Cannot set system-controlled domain metadata\n      Seq(\"delta.foo\", \"DELTA.foo\").foreach { domain =>\n        val e = intercept[IllegalArgumentException] {\n          val txn = getUpdateTxn(engine, tablePath)\n          txn.addDomainMetadata(domain, \"misc config\")\n        }\n        assert(\n          e.getMessage.contains(\"Setting a non-supported system-controlled domain is not allowed\"))\n      }\n    }\n\n    // Setting the same domain more than once uses the latest pair\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      val dm1_1 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"1\"}\"\"\", false)\n      val dm1_2 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"10\"}\"\"\"\", false)\n\n      commitDomainMetadataAndVerify(engine, tablePath, List(dm1_1, dm1_2), Map(\"domain1\" -> dm1_2))\n    }\n  }\n\n  test(\"updating domain metadata fails after transaction committed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(txn, engine, emptyIterable())\n\n      intercept[IllegalStateException] {\n        txn.addDomainMetadata(\"domain\", \"config\")\n      }\n      intercept[IllegalStateException] {\n        txn.removeDomainMetadata(\"domain\")\n      }\n    }\n  }\n\n  test(\"basic txn.removeDomainMetadata API tests\") {\n    // removeDomainMetadata is tested thoroughly elsewhere in this suite, here we just test API\n    // specific behaviors\n\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      // Cannot remove system-controlled domain metadata\n      Seq(\"delta.foo\", \"DELTA.foo\").foreach { domain =>\n        val e = intercept[IllegalArgumentException] {\n          val txn = getUpdateTxn(defaultEngine, tablePath)\n          txn.removeDomainMetadata(domain)\n        }\n\n        assert(e.getMessage.contains(\"Removing a system-controlled domain is not allowed\"))\n      }\n    }\n\n    // Can remove same domain more than once in same txn\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      // Set up table with domain \"domain1\n      val dm1 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"1\"}\"\"\", false)\n      commitDomainMetadataAndVerify(engine, tablePath, List(dm1), Map(\"domain1\" -> dm1))\n\n      val dm1_removed = dm1.removed()\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        List(dm1_removed, dm1_removed, dm1_removed),\n        Map())\n    }\n  }\n\n  test(\"txn.removeDomainMetadata removing a non-existent domain\") {\n    // Remove domain that does not exist and has never existed\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      intercept[DomainDoesNotExistException] {\n        val txn = getUpdateTxn(defaultEngine, tablePath)\n        txn.removeDomainMetadata(\"foo\")\n        commitTransaction(txn, defaultEngine, emptyIterable());\n      }\n    }\n\n    // Remove domain that exists as a tombstone\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      // Set up table with domain \"domain1\"\n      val dm1 = new DomainMetadata(\"domain1\", \"\"\"{\"key1\":\"1\"}\"\"\", false)\n      commitDomainMetadataAndVerify(engine, tablePath, List(dm1), Map(\"domain1\" -> dm1))\n\n      // Remove domain1 so it exists as a tombstone\n      val dm1_removed = dm1.removed()\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        List(dm1_removed),\n        Map())\n\n      // Removing it again should fail since it doesn't exist\n      intercept[DomainDoesNotExistException] {\n        commitDomainMetadataAndVerify(\n          engine,\n          tablePath,\n          List(dm1_removed),\n          Map())\n      }\n    }\n  }\n\n  test(\"Using add and remove with the same domain in the same txn\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n      // We forbid adding + removing a domain with the same identifier in a transaction to avoid\n      // any ambiguous behavior\n      // For example, is the expected behavior\n      // a) we don't write any domain metadata, and it's a no-op (remove cancels out the add)\n      // b) we remove the previous domain from the read snapshot, and add the new one as the current\n      //    domain metadata\n\n      {\n        val txn = getUpdateTxn(defaultEngine, tablePath)\n        txn.addDomainMetadata(\"foo\", \"fake config\")\n        val e = intercept[IllegalArgumentException] {\n          txn.removeDomainMetadata(\"foo\")\n        }\n        assert(e.getMessage.contains(\"Cannot remove a domain that is added in this transaction\"))\n      }\n      {\n        val txn = getUpdateTxn(defaultEngine, tablePath)\n        txn.removeDomainMetadata(\"foo\")\n        val e = intercept[IllegalArgumentException] {\n          txn.addDomainMetadata(\"foo\", \"fake config\")\n        }\n        assert(e.getMessage.contains(\"Cannot add a domain that is removed in this transaction\"))\n      }\n    }\n  }\n\n  test(\"basic snapshot.getDomainMetadataConfiguration API tests\") {\n    // getDomainMetadataConfiguration is tested thoroughly elsewhere in this suite, here we just\n    // test the API directly to be safe\n\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      // Non-existent domain is not returned\n      assert(!latestSnapshot(tablePath).getDomainMetadata(\"foo\").isPresent)\n\n      // Commit domain foo\n      val fooDm = new DomainMetadata(\"foo\", \"foo!\", false)\n      commitDomainMetadataAndVerify(engine, tablePath, List(fooDm), Map(\"foo\" -> fooDm))\n      assert( // Check here even though already verified in commitDomainMetadataAndVerify\n        latestSnapshot(tablePath).getDomainMetadata(\"foo\").toScala.contains(\"foo!\"))\n\n      // Remove domain foo (so tombstone exists but should not be returned)\n      val fooDm_removed = fooDm.removed()\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        List(fooDm_removed),\n        Map())\n      // Already checked in commitDomainMetadataAndVerify but check again\n      assert(!latestSnapshot(tablePath).getDomainMetadata(\"foo\").isPresent)\n    }\n  }\n\n  test(\"removing a domain on a table without DomainMetadata support\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create table with legacy protocol\n      commitTransaction(\n        getCreateTxn(\n          engine,\n          tablePath = tablePath,\n          schema = testSchema),\n        engine,\n        emptyIterable())\n      intercept[IllegalStateException] {\n        val txn = getUpdateTxn(engine, tablePath)\n        txn.removeDomainMetadata(\"foo\")\n      }\n    }\n  }\n\n  test(\"all domain metadata batches are read when CRC exists at earlier version\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create table with domain metadata support\n      createTableWithDomainMetadataSupported(engine, tablePath)\n\n      // Commit initial domain metadata so that the CRC has domain metadata entries\n      val initDm = new DomainMetadata(\"init\", \"\"\"{\"initial\":\"true\"}\"\"\", false)\n      commitDomainMetadataAndVerify(\n        engine,\n        tablePath,\n        domainMetadatas = Seq(initDm),\n        expectedValue = Map(\"init\" -> initDm))\n\n      // Commit 3 domain metadata actions in a single transaction.\n      // The commit JSON has 4 rows: commitInfo + 3 DomainMetadata actions.\n      // With a JSON reader batch size of 2, this produces 2 batches:\n      //   Batch 1: [commitInfo, d1]\n      //   Batch 2: [d2, d3]\n      val dm1 = new DomainMetadata(\"d1\", \"\"\"{\"key\":\"v1\"}\"\"\", false)\n      val dm2 = new DomainMetadata(\"d2\", \"\"\"{\"key\":\"v2\"}\"\"\", false)\n      val dm3 = new DomainMetadata(\"d3\", \"\"\"{\"key\":\"v3\"}\"\"\", false)\n      val dmTxn = createTxnWithDomainMetadatas(\n        engine,\n        tablePath,\n        Seq(dm1, dm2, dm3),\n        enableDomainMetadata = false)\n      val dmVersion = commitTransaction(dmTxn, engine, emptyIterable()).getVersion\n\n      // Delete CRC for the new version to force Case 3 in loadDomainMetadataMap:\n      // CRC at the earlier version (with \"init\" DM) exists, CRC at dmVersion is missing.\n      // This triggers incremental log replay from dmVersion with minLogVersion = dmVersion.\n      deleteChecksumFileForTable(tablePath, Seq(dmVersion.toInt))\n\n      // Load snapshot using an engine with batch size 2. Before the fix, the break\n      // condition (minLogVersion == version) would exit after the first batch of\n      // minLogVersion, missing d2 and d3 in the second batch.\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(\n        defaultEngineBatchSize2,\n        tablePath)\n\n      assertDomainMetadata(\n        snapshot,\n        Map(\"init\" -> initDm, \"d1\" -> dm1, \"d2\" -> dm2, \"d3\" -> dm3))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/IcebergWriterCompatV1Suite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Operation, Table, TableManager}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{KernelException, UnsupportedTableFeatureException}\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.{ColumnMapping, ColumnMappingSuiteBase}\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode\nimport io.delta.kernel.test.TestFixtures\nimport io.delta.kernel.types.{ByteType, DataType, DateType, FieldMetadata, IntegerType, LongType, ShortType, StructField, StructType, TimestampNTZType, TypeChange, VariantType}\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.assertj.core.api.Assertions.assertThat\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass IcebergWriterCompatV1TransactionBuilderV1Suite extends IcebergWriterCompatV1SuiteBase\n    with WriteUtils {}\n\nclass IcebergWriterCompatV1TransactionBuilderV2Suite extends IcebergWriterCompatV1SuiteBase\n    with WriteUtilsWithV2Builders {}\n\nclass CatalogManagedWithIcebergWriterCompatV1Suite\n    extends AnyFunSuite\n    with WriteUtilsWithV2Builders\n    with IcebergWriterCompatV1TestUtils {\n\n  test(\"can create a catalogManaged table with icebergWriterCompatV1\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      val createTxn = TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withCommitter(committerUsingPutIfAbsent)\n        .withTableProperties(\n          Map(\n            TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey -> \"supported\",\n            TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\").asJava)\n        .build(engine)\n\n      // ===== WHEN =====\n      createTxn.commit(engine, emptyIterable[Row])\n\n      // ===== THEN =====\n      val snapshotImpl = TableManager\n        .loadSnapshot(tablePath)\n        .asInstanceOf[SnapshotBuilderImpl]\n        .withMaxCatalogVersion(0)\n        .build(engine)\n      verifyIcebergWriterCompatV1Enabled(snapshotImpl.getProtocol, snapshotImpl.getMetadata)\n      assert(\n        snapshotImpl.getProtocol.supportsFeature(TableFeatures.CATALOG_MANAGED_RW_FEATURE))\n    }\n  }\n\n}\n\ntrait IcebergWriterCompatV1TestUtils { self: AbstractWriteUtils =>\n  def verifyIcebergWriterCompatV1Enabled(tablePath: String, engine: Engine): Unit = {\n    val protocol = getProtocol(engine, tablePath)\n    val metadata = getMetadata(engine, tablePath)\n    verifyIcebergWriterCompatV1Enabled(protocol, metadata)\n  }\n\n  def verifyIcebergWriterCompatV1Enabled(protocol: Protocol, metadata: Metadata): Unit = {\n    // Check expected protocol features are enabled\n    assert(protocol.supportsFeature(TableFeatures.ICEBERG_COMPAT_V2_W_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.COLUMN_MAPPING_RW_FEATURE))\n    assert(protocol.supportsFeature(TableFeatures.ICEBERG_WRITER_COMPAT_V1))\n\n    // Check expected confs are present\n    assert(TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.fromMetadata(metadata))\n    assert(TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(metadata))\n    assert(TableConfig.COLUMN_MAPPING_MODE.fromMetadata(metadata) == ColumnMappingMode.ID)\n  }\n}\n\ntrait IcebergWriterCompatV1SuiteBase\n    extends AnyFunSuite\n    with AbstractWriteUtils\n    with TestFixtures\n    with IcebergWriterCompatV1TestUtils\n    with ColumnMappingSuiteBase {\n\n  private val tblPropertiesIcebergWriterCompatV1Enabled = Map(\n    TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\")\n\n  private val tblPropertiesIcebergCompatV2Enabled = Map(\n    TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"true\")\n\n  private val tblPropertiesColumnMappingModeId = Map(\n    TableConfig.COLUMN_MAPPING_MODE.getKey -> \"id\")\n\n  Seq(\n    (Map(), \"no other properties\"),\n    (tblPropertiesIcebergCompatV2Enabled, \"icebergCompatV2 enabled\"),\n    (tblPropertiesColumnMappingModeId, \"column mapping mode set to id\"),\n    (\n      tblPropertiesIcebergCompatV2Enabled ++ tblPropertiesColumnMappingModeId,\n      \"icebergCompatV2 enabled and column mapping mode set to id\")).foreach {\n    case (tblProperties, description) =>\n      test(s\"Basic enablement on new table with $description\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          createEmptyTable(\n            engine,\n            tablePath,\n            cmTestSchema(),\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled ++ tblProperties)\n          verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n          verifyCMTestSchemaHasValidColumnMappingInfo(\n            getMetadata(engine, tablePath),\n            enableIcebergWriterCompatV1 = true)\n        }\n      }\n  }\n\n  test(\"Cannot enable on an existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = tblPropertiesColumnMappingModeId ++ tblPropertiesIcebergCompatV2Enabled)\n      val e = intercept[KernelException] {\n        updateTableMetadata(\n          engine,\n          tablePath,\n          tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      }\n      assert(e.getMessage.contains(\n        \"Cannot enable delta.enableIcebergWriterCompatV1 on an existing table\"))\n    }\n  }\n\n  test(\"Can enable on an existing table if already enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n      updateTableMetadata(\n        engine,\n        tablePath,\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n    }\n  }\n\n  test(\"Cannot disable icebergWriterCompatV1 conf on existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create an empty table with icebergWriterCompatV1 enabled\n      createEmptyTable(\n        engine,\n        tablePath,\n        cmTestSchema(),\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n\n      val e = intercept[KernelException] {\n        // Disable icebergWriterCompatV1 in the table properties\n        updateTableMetadata(\n          engine,\n          tablePath,\n          tableProperties = Map(TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"false\"))\n      }\n      assert(e.getMessage.contains(\n        \"Disabling delta.enableIcebergWriterCompatV1 on an existing table is not allowed\"))\n    }\n  }\n\n  test(\"Cannot enable when column mapping mode explicitly set to name/none\") {\n    Seq(\"name\", \"none\").foreach { cmMode =>\n      withTempDirAndEngine { (tablePath, engine) =>\n        val e = intercept[KernelException] {\n          createEmptyTable(\n            engine,\n            tablePath,\n            testSchema,\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled ++\n              Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> cmMode))\n        }\n        assert(e.getMessage.contains(s\"The value '$cmMode' for the property \" +\n          s\"'delta.columnMapping.mode' is not compatible with icebergWriterCompatV1\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { cmInfoPopulated =>\n    test(\n      s\"Column mapping metadata set correctly when cmInfoPrePopulated=$cmInfoPopulated\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        // Create new table and verify column mapping info set correctly\n        val initialSchema = if (cmInfoPopulated) {\n          new StructType()\n            .add(\n              \"c1\",\n              IntegerType.INTEGER,\n              FieldMetadata.builder()\n                .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n                .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n                .build())\n        } else {\n          new StructType()\n            .add(\"c1\", IntegerType.INTEGER)\n        }\n        createEmptyTable(\n          engine,\n          tablePath,\n          initialSchema,\n          tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n        val initialMetadata = getMetadata(engine, tablePath)\n        assertThat(initialMetadata.getConfiguration)\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"1\")\n        assertThat(initialMetadata.getSchema.get(\"c1\").getMetadata.getEntries)\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1L.asInstanceOf[AnyRef])\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-1\")\n\n        // Add a new column and verify column mapping info set correctly\n        val updatedSchema = if (cmInfoPopulated) {\n          initialMetadata.getSchema\n            .add(\n              \"c2\",\n              IntegerType.INTEGER,\n              FieldMetadata.builder()\n                .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 2)\n                .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-2\")\n                .build())\n        } else {\n          initialMetadata.getSchema\n            .add(\"c2\", IntegerType.INTEGER)\n        }\n        updateTableMetadata(engine, tablePath, schema = updatedSchema)\n        val updatedMetadata = getMetadata(engine, tablePath)\n        assertThat(updatedMetadata.getConfiguration)\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_MAX_COLUMN_ID_KEY, \"2\")\n        assertThat(updatedMetadata.getSchema.get(\"c2\").getMetadata.getEntries)\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_ID_KEY, 2L.asInstanceOf[AnyRef])\n          .containsEntry(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col-2\")\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isNewTable =>\n    test(s\"Cannot set physicalName to something other than col-{fieldId}, isNewTable=$isNewTable\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        if (!isNewTable) {\n          createEmptyTable(\n            engine,\n            tablePath,\n            new StructType().add(\"c1\", IntegerType.INTEGER),\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n        }\n        val schemaToCommit = if (isNewTable) {\n          new StructType()\n            .add(\n              \"c2\",\n              IntegerType.INTEGER,\n              FieldMetadata.builder()\n                .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n                .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"c2\")\n                .build())\n        } else {\n          getMetadata(engine, tablePath)\n            .getSchema\n            .add(\n              \"c2\",\n              IntegerType.INTEGER,\n              FieldMetadata.builder()\n                .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 2)\n                .putString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"c2\")\n                .build())\n        }\n        val e = intercept[KernelException] {\n          appendData(\n            engine,\n            tablePath,\n            isNewTable = isNewTable,\n            schema = schemaToCommit,\n            data = Seq.empty,\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n        }\n        val expectedInvalidColumnId = if (isNewTable) 1 else 2\n        assert(e.getMessage.contains(\n          \"IcebergWriterCompatV1 requires column mapping field physical names be equal to \"\n            + \"'col-[fieldId]', but this is not true for the following fields \" +\n            s\"[c2(physicalName='c2', columnId=$expectedInvalidColumnId)]\"))\n      }\n    }\n  }\n\n  test(\"Cannot enable when icebergCompatV2 explicitly disabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val e = intercept[KernelException] {\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = tblPropertiesIcebergWriterCompatV1Enabled ++\n            Map(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"false\"))\n      }\n      assert(e.getMessage.contains(\"'false' for the property 'delta.enableIcebergCompatV2' is \" +\n        \"not compatible with icebergWriterCompatV1\"))\n    }\n  }\n\n  test(\"Cannot disable icebergCompatV2 on an existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create an empty table with icebergWriterCompatV1 enabled\n      createEmptyTable(\n        engine,\n        tablePath,\n        cmTestSchema(),\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n\n      val e = intercept[KernelException] {\n        // Disable icebergCompatV2\n        updateTableMetadata(\n          engine,\n          tablePath,\n          tableProperties = Map(TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey -> \"false\"))\n      }\n      assert(e.getMessage.contains(\"'false' for the property 'delta.enableIcebergCompatV2' is \" +\n        \"not compatible with icebergWriterCompatV1\"))\n    }\n  }\n\n  // TODO once we support schema evolution test adding columns of these types\n  Seq(ByteType.BYTE, ShortType.SHORT).foreach { dataType =>\n    test(s\"Cannot enable IcebergWriterCompatV2 on a table with datatype $dataType\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val e = intercept[KernelException] {\n          createEmptyTable(\n            engine,\n            tablePath,\n            new StructType().add(\"col\", dataType),\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n        }\n        assert(e.getMessage.contains(\n          s\"icebergWriterCompatV1 does not support the data types: [${dataType.toString}]\"))\n      }\n    }\n  }\n\n  test(\"subsequent writes to icebergWriterCompatV1 enabled tables doesn't update metadata\") {\n    // we want to make sure the [[IcebergWriterCompatV1MetadataValidatorAndUpdater]] doesn't\n    // make unneeded metadata updates\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n\n      appendData(\n        engine,\n        tablePath,\n        data = Seq.empty,\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled ++\n          tblPropertiesIcebergCompatV2Enabled ++ tblPropertiesColumnMappingModeId\n      ) // version 1\n      appendData(engine, tablePath, data = Seq.empty) // version 2\n\n      val table = Table.forPath(engine, tablePath)\n      assert(getMetadataActionFromCommit(engine, table, version = 0).isDefined)\n      assert(getMetadataActionFromCommit(engine, table, version = 1).isEmpty)\n      assert(getMetadataActionFromCommit(engine, table, version = 2).isEmpty)\n\n      // make a metadata update and see it is reflected in the table\n      val newProps = Map(\"key\" -> \"value\")\n      updateTableMetadata(engine, tablePath, tableProperties = newProps) // version 3\n      assert(getMetadataActionFromCommit(engine, table, version = 3).isDefined)\n      val ver3Metadata = getMetadata(engine, tablePath)\n      assert(ver3Metadata.getConfiguration().get(\"key\") == \"value\")\n    }\n  }\n\n  /* -------------------- Tests for blocked table features -------------------- */\n\n  def testIncompatibleTableFeature(\n      featureName: String,\n      tablePropertiesToEnable: Map[String, String] = Map.empty,\n      schemaToEnable: StructType = testSchema,\n      expectedErrorMessage: String,\n      testOnExistingTable: Boolean = true // some features cannot be enabled for existing tables\n  ): Unit = {\n    if (testOnExistingTable) {\n      test(s\"Cannot enable feature $featureName on an existing table with \" +\n        s\"icebergWriterCompatV1 enabled\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          // Create existing table with icebergWriterCompatV1 enabled\n          createEmptyTable(\n            engine,\n            tablePath,\n            testSchema,\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n          verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n          val e = intercept[KernelException] {\n            // Update the table such that we enable the incompatible feature\n            updateTableMetadata(\n              engine,\n              tablePath,\n              schema = schemaToEnable,\n              tableProperties = tablePropertiesToEnable)\n          }\n          assert(e.getMessage.contains(expectedErrorMessage))\n        }\n      }\n    }\n    test(s\"Cannot enable feature $featureName and icebergWriterCompatV1 on a new table\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        // Create table with IcebergCompatWriterV1 and the incompatible feature enabled\n        val e = intercept[KernelException] {\n          createEmptyTable(\n            engine,\n            tablePath,\n            schema = schemaToEnable,\n            tableProperties =\n              tblPropertiesIcebergWriterCompatV1Enabled ++ tablePropertiesToEnable)\n        }\n        assert(e.getMessage.contains(expectedErrorMessage))\n      }\n    }\n    // Since we don't support enabling icebergWriterCompatV1 on an existing table we cannot test\n    // the case of enabling icebergWriterCompatV1 on an existing table with the incompatible\n    // feature enabled\n  }\n\n  // Features that don't have write support currently (once we add write support convert these\n  // tests and update error intercepted)\n  def testIncompatibleUnsupportedTableFeature(\n      featureName: String,\n      tablePropertiesToEnable: Map[String, String] = Map.empty,\n      schemaToEnable: StructType = testSchema,\n      expectedErrorMessage: String = \"Unsupported Delta writer feature\",\n      testOnExistingTable: Boolean = true // some features cannot be enabled for existing tables\n  ): Unit = {\n    testIncompatibleTableFeature(\n      featureName,\n      tablePropertiesToEnable,\n      schemaToEnable,\n      expectedErrorMessage,\n      testOnExistingTable)\n  }\n\n  /* ----- Incompatible features not supported when ACTIVE in the table ----- */\n\n  testIncompatibleUnsupportedTableFeature(\n    \"changeDataFeed\",\n    tablePropertiesToEnable = Map(TableConfig.CHANGE_DATA_FEED_ENABLED.getKey -> \"true\"))\n\n  testIncompatibleUnsupportedTableFeature(\n    \"invariants\",\n    schemaToEnable = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.invariants\", \"{\\\"expression\\\": { \\\"expression\\\": \\\"x > 3\\\"} }\")\n          .build()),\n    testOnExistingTable = false // we don't currently support schema updates\n  )\n\n  testIncompatibleUnsupportedTableFeature(\n    \"checkConstraints\",\n    tablePropertiesToEnable = Map(\"delta.constraints.a\" -> \"a = b\"),\n    expectedErrorMessage = \"Unknown configuration was specified: delta.constraints.a\")\n\n  testIncompatibleUnsupportedTableFeature(\n    \"generatedColumns\",\n    schemaToEnable = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putString(\"delta.generationExpression\", \"{\\\"expression\\\": \\\"c1 + 1\\\"}\")\n          .build()),\n    testOnExistingTable = false // we don't currently support schema updates\n  )\n\n  testIncompatibleUnsupportedTableFeature(\n    \"identityColumns\",\n    schemaToEnable = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\n        \"c2\",\n        IntegerType.INTEGER,\n        FieldMetadata.builder()\n          .putLong(\"delta.identity.start\", 1L)\n          .putLong(\"delta.identity.step\", 2L)\n          .putBoolean(\"delta.identity.allowExplicitInsert\", true)\n          .build()),\n    testOnExistingTable = false // we don't currently support schema updates\n  )\n\n  testIncompatibleUnsupportedTableFeature(\n    \"variantType\",\n    schemaToEnable = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\"c2\", VariantType.VARIANT),\n    testOnExistingTable = false, // we don't currently support schema updates\n    // We throw an error earlier for variant for some reason\n    expectedErrorMessage = \"icebergCompatV2 does not support the data types: [variant]\")\n\n  testIncompatibleTableFeature(\n    \"rowTracking\",\n    tablePropertiesToEnable = Map(\"delta.enableRowTracking\" -> \"true\"),\n    expectedErrorMessage =\n      \"Table features [rowTracking] are incompatible with icebergWriterCompatV1\")\n\n  // deletionVectors is blocked by both icebergCompatV2 and icebergWriterCompatV1; since the\n  // icebergCompatV2 checks are executed first as part of ICEBERG_COMPAT_V2_ENABLED.postProcess we\n  // hit that error message first\n  testIncompatibleTableFeature(\n    \"deletionVectors\",\n    tablePropertiesToEnable = Map(TableConfig.DELETION_VECTORS_CREATION_ENABLED.getKey -> \"true\"),\n    expectedErrorMessage =\n      \"Table features [deletionVectors] are incompatible with icebergCompatV2\")\n\n  /* ----- Non-legacy incompatible features not allowed even when inactive  ----- */\n\n  testIncompatibleUnsupportedTableFeature(\n    \"variantType inactive\",\n    tablePropertiesToEnable = Map(\"delta.feature.variantType\" -> \"supported\"),\n    expectedErrorMessage = \"Table features [variantType] are \" +\n      \"incompatible with icebergWriterCompatV1\")\n\n  // deletionVectors is blocked by both icebergCompatV2 and icebergWriterCompatV1; since the\n  // icebergCompatV2 checks are executed first as part of ICEBERG_COMPAT_V2_ENABLED.postProcess we\n  // hit that error message first\n  testIncompatibleTableFeature(\n    \"deletionVectors inactive\",\n    tablePropertiesToEnable = Map(\"delta.feature.deletionVectors\" -> \"supported\"),\n    expectedErrorMessage =\n      \"Table features [deletionVectors] are incompatible with icebergCompatV2\")\n\n  // defaultColumns is not added to Kernel yet --> throws an error on feature lookup\n  testIncompatibleUnsupportedTableFeature(\n    \"defaultColumns inactive\",\n    tablePropertiesToEnable = Map(\"delta.feature.defaultColumns\" -> \"supported\"),\n    expectedErrorMessage = \"Unsupported Delta table feature\")\n\n  // collations is not added to Kernel yet --> throws an error on feature lookup\n  testIncompatibleUnsupportedTableFeature(\n    \"collations inactive\",\n    tablePropertiesToEnable = Map(\"delta.feature.collations\" -> \"supported\"),\n    expectedErrorMessage = \"Unsupported Delta table feature\")\n\n  /* ----- Legacy incompatible features allowed if they are inactive  ----- */\n\n  test(\"legacy table features allowed with icebergWriterCompatV1 if inactive\") {\n    val tblProperties =\n      Seq(\n        \"invariants\",\n        \"changeDataFeed\",\n        \"checkConstraints\",\n        \"identityColumns\",\n        \"generatedColumns\",\n        \"typeWidening\",\n        \"typeWidening-preview\",\n        \"rowTracking\")\n        .map(tableFeature => s\"delta.feature.$tableFeature\" -> \"supported\")\n        .toMap\n\n    // New table with these features + icebergWriterCompatV1\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        cmTestSchema(),\n        tableProperties = tblProperties ++ tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n      // Check all the features are supported\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.GENERATED_COLUMNS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.IDENTITY_COLUMNS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.CONSTRAINTS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.CHANGE_DATA_FEED_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.INVARIANTS_W_FEATURE))\n    }\n\n    // Existing table with icebergWriterCompatV1 - enable these features\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        cmTestSchema(),\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n\n      updateTableMetadata(\n        engine,\n        tablePath,\n        tableProperties = tblProperties)\n      // Check all the features are supported\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.GENERATED_COLUMNS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.IDENTITY_COLUMNS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.CONSTRAINTS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.CHANGE_DATA_FEED_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.INVARIANTS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n    }\n  }\n\n  /* ----- Compatible features allowed when active  ----- */\n\n  test(\"All expected compatible features can be active with icebergWriterCompatV1\") {\n\n    val tblProperties = Map(\n      TableConfig.APPEND_ONLY_ENABLED.getKey -> \"true\", // appendOnly\n      TableConfig.CHECKPOINT_POLICY.getKey -> \"v2\", // checkpointV2\n      TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\", // inCommitTimestamp\n      TableConfig.ICEBERG_WRITER_COMPAT_V1_ENABLED.getKey -> \"true\",\n      TableConfig.TYPE_WIDENING_ENABLED.getKey -> \"true\")\n    val schema = new StructType()\n      .add(\"c1\", IntegerType.INTEGER)\n      .add(\"c2\", TimestampNTZType.TIMESTAMP_NTZ) // timestampNtz\n\n    // New table with these features + icebergWriterCompatV1\n    withTempDirAndEngine { (tablePath, engine) =>\n      getCreateTxn(\n        engine,\n        tablePath,\n        schema,\n        tableProperties = tblProperties,\n        withDomainMetadataSupported = true)\n        .commit(engine, emptyIterable[Row])\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n      // Check all the features are supported\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.CHECKPOINT_V2_RW_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.IN_COMMIT_TIMESTAMP_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.TIMESTAMP_NTZ_RW_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.INVARIANTS_W_FEATURE))\n      // TODO in the future add typeWidening and clustering once they are supported\n    }\n\n    // Existing table with icebergWriterCompatV1 - enable these features\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        cmTestSchema(),\n        tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n      verifyIcebergWriterCompatV1Enabled(tablePath, engine)\n\n      getUpdateTxn(\n        engine,\n        tablePath,\n        tableProperties = tblProperties,\n        withDomainMetadataSupported = true)\n        .commit(engine, emptyIterable[Row])\n      // Check all the features are supported\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.APPEND_ONLY_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.CHECKPOINT_V2_RW_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.IN_COMMIT_TIMESTAMP_W_FEATURE))\n      // assert(protocol.supportsFeature(TableFeatures.TIMESTAMP_NTZ_RW_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.DOMAIN_METADATA_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.INVARIANTS_W_FEATURE))\n      assert(protocol.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n      // TODO in the future add clustering once they are supported\n    }\n  }\n\n  test(\"compatible type widening is allowed with icebergWriterCompatV1\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a table with icebergWriterCompatV1 and type widening enabled\n      val schema = new StructType()\n        .add(new StructField(\n          \"intToLong\",\n          LongType.LONG,\n          false,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(\n              ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY,\n              \"col-1\").build()).withTypeChanges(\n          Seq(new TypeChange(IntegerType.INTEGER, LongType.LONG)).asJava))\n\n      val tblProps = tblPropertiesIcebergWriterCompatV1Enabled ++\n        Map(TableConfig.TYPE_WIDENING_ENABLED.getKey -> \"true\")\n\n      // This should not throw an exception\n      createEmptyTable(engine, tablePath, schema, tableProperties = tblProps)\n\n      val protocol = getProtocol(engine, tablePath)\n      assert(protocol.supportsFeature(TableFeatures.TYPE_WIDENING_RW_FEATURE))\n    }\n  }\n\n  test(\"incompatible type widening throws exception with icebergWriterCompatV1 on new Table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Try to create a table with icebergWriterCompatV1 and incompatible type widening\n      val schema = new StructType()\n        .add(new StructField(\n          \"dateToTimestamp\",\n          TimestampNTZType.TIMESTAMP_NTZ,\n          false,\n          FieldMetadata.builder()\n            .putLong(ColumnMapping.COLUMN_MAPPING_ID_KEY, 1)\n            .putString(\n              ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY,\n              \"col-1\").build()).withTypeChanges(\n          Seq(new TypeChange(DateType.DATE, TimestampNTZType.TIMESTAMP_NTZ)).asJava))\n\n      val tblProps = tblPropertiesIcebergWriterCompatV1Enabled ++\n        Map(TableConfig.TYPE_WIDENING_ENABLED.getKey -> \"true\")\n\n      val e = intercept[KernelException] {\n        createEmptyTable(engine, tablePath, schema, tableProperties = tblProps)\n      }\n\n      assert(\n        e.getMessage.contains(\"icebergCompatV2 does not support type widening present in table\"))\n    }\n  }\n\n  /* -------------------- Enforcements blocked by icebergCompatV2 -------------------- */\n  // We test the deletionVector checks above as part of blocked table feature tests\n\n  // We cannot test enabling icebergCompatV1 since it is not a table feature in Kernel; This is\n  // tested in the unit tests in IcebergWriterCompatV1MetadataValidatorAndUpdaterSuite\n\n  (PRIMITIVE_TYPES ++ NESTED_TYPES)\n    // filter out the types unsupported by icebergWriterCompatV1\n    .filter(dataType => dataType != ByteType.BYTE && dataType != ShortType.SHORT)\n    .foreach { dataType: DataType =>\n      test(s\"allowed data column types: $dataType on a new table\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          val schema = new StructType().add(\"col\", dataType)\n          createEmptyTable(\n            engine,\n            tablePath,\n            schema,\n            tableProperties = tblPropertiesIcebergWriterCompatV1Enabled)\n        }\n      }\n    }\n\n  ignore(\"test unsupported data types\") {\n    // Can't test this now as the only unsupported data type in Iceberg is VariantType,\n    // and it also has no write support in Kernel.\n    // Unit test for this is covered in [[IcebergWriterCompatV1MetadataValidatorAndUpdaterSuite]]\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/InCommitTimestampSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util.{Locale, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.{ListMap, Seq}\nimport scala.collection.mutable\n\nimport io.delta.kernel._\nimport io.delta.kernel.Operation.{CREATE_TABLE, WRITE}\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtilsWithV1Builders, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{InvalidTableException, ProtocolChangedException}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.{DeltaHistoryManager, SnapshotImpl, TableImpl}\nimport io.delta.kernel.internal.TableConfig._\nimport io.delta.kernel.internal.actions.{CommitInfo, SingleAction}\nimport io.delta.kernel.internal.actions.SingleAction.createCommitInfoSingleAction\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.{FileNames, VectorUtils}\nimport io.delta.kernel.internal.util.ManualClock\nimport io.delta.kernel.internal.util.Utils.singletonCloseableIterator\nimport io.delta.kernel.types._\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Runs in-commit timestamp tests using the TableManager snapshot APIs and V2 transaction builders\n */\nclass InCommitTimestampSuite extends AbstractInCommitTimestampSuite with WriteUtilsWithV2Builders\n\n/**\n * Runs in-commit timestamp tests using the legacy Table snapshot APIs and V1 transaction builders\n */\nclass LegacyInCommitTimestampSuite extends AbstractInCommitTimestampSuite\n    with WriteUtilsWithV1Builders\n\ntrait AbstractInCommitTimestampSuite extends AnyFunSuite {\n  self: AbstractWriteUtils =>\n\n  private def getLogPath(engine: Engine, tablePath: String): Path = {\n    val resolvedTablePath = engine.getFileSystemClient.resolvePath(tablePath)\n    new Path(resolvedTablePath, \"_delta_log\")\n  }\n\n  private def removeCommitInfoFromCommit(engine: Engine, version: Long, logPath: Path): Unit = {\n    val file = FileStatus.of(FileNames.deltaFile(logPath, version), 0, 0)\n    val columnarBatches =\n      engine.getJsonHandler.readJsonFiles(\n        singletonCloseableIterator(file),\n        SingleAction.FULL_SCHEMA,\n        Optional.empty())\n    assert(columnarBatches.hasNext)\n    val rows = columnarBatches.next().getRows\n    val rowsWithoutCommitInfo =\n      rows.filter(row => row.isNullAt(row.getSchema.indexOf(\"commitInfo\")))\n    engine\n      .getJsonHandler\n      .writeJsonFileAtomically(\n        FileNames.deltaFile(logPath, version),\n        rowsWithoutCommitInfo,\n        true /* overwrite */ )\n  }\n\n  test(\"Enable ICT on commit 0\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val beforeCommitAttemptStartTime = System.currentTimeMillis\n      val clock = new ManualClock(beforeCommitAttemptStartTime + 1)\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"true\",\n        expectedValue = true,\n        clock = clock)\n\n      val ver0Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n\n      assert(ver0Snapshot.getTimestamp(engine) === beforeCommitAttemptStartTime + 1)\n      assert(\n        getInCommitTimestamp(\n          engine,\n          tablePath,\n          version = 0).get === ver0Snapshot.getTimestamp(engine))\n      assertHasWriterFeature(ver0Snapshot, \"inCommitTimestamp\")\n      // Time travel should work\n      val searchedSnapshot = getTableManagerAdapter.getSnapshotAtTimestamp(\n        engine,\n        tablePath,\n        beforeCommitAttemptStartTime + 1)\n      assert(searchedSnapshot.getVersion == 0)\n    }\n  }\n\n  test(\"Create a non-inCommitTimestamp table and then enable ICT\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn1 = getCreateTxn(engine, tablePath, testSchema)\n\n      txn1.commit(engine, emptyIterable())\n\n      val ver0Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertMetadataProp(ver0Snapshot, IN_COMMIT_TIMESTAMPS_ENABLED, false)\n      assertHasNoWriterFeature(ver0Snapshot, \"inCommitTimestamp\")\n      assert(getInCommitTimestamp(engine, tablePath, version = 0).isEmpty)\n\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        isNewTable = false,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"true\",\n        expectedValue = true)\n\n      val ver1Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertHasWriterFeature(ver1Snapshot, \"inCommitTimestamp\")\n      assert(ver1Snapshot.getTimestamp(engine) > ver0Snapshot.getTimestamp(engine))\n      assert(\n        getInCommitTimestamp(\n          engine,\n          tablePath,\n          version = 1).get === ver1Snapshot.getTimestamp(engine))\n\n      // Time travel should work\n      // Search timestamp = ICT enablement time - 1\n      val searchedSnapshot1 = getTableManagerAdapter.getSnapshotAtTimestamp(\n        engine,\n        tablePath,\n        ver1Snapshot.getTimestamp(engine) - 1)\n      assert(searchedSnapshot1.getVersion == 0)\n      // Search timestamp = ICT enablement time\n      val searchedSnapshot2 =\n        getTableManagerAdapter.getSnapshotAtTimestamp(\n          engine,\n          tablePath,\n          ver1Snapshot.getTimestamp(engine))\n      assert(searchedSnapshot2.getVersion == 1)\n    }\n  }\n\n  test(\"InCommitTimestamps are monotonic even when the clock is skewed\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testSchema,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1),\n        clock = clock,\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"))\n\n      val ver1Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      val ver1Timestamp = ver1Snapshot.getTimestamp(engine)\n      assert(IN_COMMIT_TIMESTAMPS_ENABLED.fromMetadata(ver1Snapshot.getMetadata))\n\n      clock.setTime(startTime - 10000)\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2),\n        clock = clock)\n\n      val ver2Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      val ver2Timestamp = ver2Snapshot.getTimestamp(engine)\n      assert(ver2Timestamp === ver1Timestamp + 1)\n    }\n  }\n\n  test(\"Missing CommitInfo should result in a DELTA_MISSING_COMMIT_INFO exception\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"true\",\n        expectedValue = true)\n      // Remove CommitInfo from the commit.\n      removeCommitInfoFromCommit(engine, 0, getLogPath(engine, tablePath))\n\n      val ex = intercept[InvalidTableException] {\n        getTableManagerAdapter.getSnapshotAtLatest(\n          engine,\n          tablePath).getTimestamp(engine)\n      }\n      assert(ex.getMessage.contains(String.format(\n        \"This table has the feature %s enabled which requires the presence of the \" +\n          \"CommitInfo action in every commit. However, the CommitInfo action is \" +\n          \"missing from commit version %s.\",\n        \"inCommitTimestamp\",\n        \"0\")))\n    }\n  }\n\n  test(\"Missing CommitInfo.inCommitTimestamp should result in a \" +\n    \"DELTA_MISSING_COMMIT_TIMESTAMP exception\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      setTablePropAndVerify(\n        engine,\n        tablePath,\n        isNewTable = true,\n        IN_COMMIT_TIMESTAMPS_ENABLED,\n        \"true\",\n        true)\n      // Remove CommitInfo.inCommitTimestamp from the commit.\n      val logPath = getLogPath(engine, tablePath)\n      val file = FileStatus.of(FileNames.deltaFile(logPath, 0), 0, 0)\n      val columnarBatches =\n        engine.getJsonHandler.readJsonFiles(\n          singletonCloseableIterator(file),\n          SingleAction.FULL_SCHEMA,\n          Optional.empty())\n      assert(columnarBatches.hasNext)\n      val rows = columnarBatches.next().getRows\n      val commitInfoOpt =\n        CommitInfo.unsafeTryReadCommitInfoFromPublishedDeltaFile(engine, logPath, 0)\n      assert(commitInfoOpt.isPresent)\n      val commitInfo = commitInfoOpt.get\n      commitInfo.setInCommitTimestamp(Optional.empty())\n      val rowsWithoutCommitInfoInCommitTimestamp =\n        rows.map(row => {\n          val commitInfoOrd = row.getSchema.indexOf(\"commitInfo\")\n          if (row.isNullAt(commitInfoOrd)) {\n            row\n          } else {\n            createCommitInfoSingleAction(commitInfo.toRow)\n          }\n        })\n      engine\n        .getJsonHandler\n        .writeJsonFileAtomically(\n          FileNames.deltaFile(logPath, 0),\n          rowsWithoutCommitInfoInCommitTimestamp,\n          true /* overwrite */ )\n\n      val ex = intercept[InvalidTableException] {\n        getTableManagerAdapter.getSnapshotAtLatest(\n          engine,\n          tablePath).getTimestamp(engine)\n      }\n      assert(ex.getMessage.contains(String.format(\n        \"This table has the feature %s enabled which requires the presence of \" +\n          \"inCommitTimestamp in the CommitInfo action. However, this field has not \" +\n          \"been set in commit version %s.\",\n        \"inCommitTimestamp\",\n        \"0\")))\n    }\n  }\n\n  test(\"Enablement tracking properties should not be added if ICT is enabled on commit 0\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"true\",\n        expectedValue = true)\n\n      val ver0Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertHasNoMetadataProp(ver0Snapshot, IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP)\n      assertHasNoMetadataProp(ver0Snapshot, IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION)\n    }\n  }\n\n  test(\"Enablement tracking works when ICT is enabled post commit 0\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n\n      txn.commit(engine, emptyIterable())\n\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1),\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"))\n\n      val ver1Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertMetadataProp(ver1Snapshot, IN_COMMIT_TIMESTAMPS_ENABLED, true)\n      assertMetadataProp(\n        ver1Snapshot,\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP,\n        Optional.of(ver1Snapshot.getTimestamp(engine)))\n      assertMetadataProp(\n        ver1Snapshot,\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n        Optional.of(1L))\n\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2))\n\n      val ver2Snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertMetadataProp(ver2Snapshot, IN_COMMIT_TIMESTAMPS_ENABLED, true)\n      assertMetadataProp(\n        ver2Snapshot,\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP,\n        Optional.of(ver1Snapshot.getTimestamp(engine)))\n      assertMetadataProp(\n        ver2Snapshot,\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n        Optional.of(1L))\n    }\n  }\n\n  test(\"Update the protocol only if required\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"true\",\n        expectedValue = true)\n      val protocol = getProtocolActionFromCommit(engine, tablePath, 0)\n      assert(protocol.isDefined)\n      assert(VectorUtils.toJavaList(protocol.get.getArray(3)).contains(\"inCommitTimestamp\"))\n\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        isNewTable = false,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"false\",\n        expectedValue = false)\n      assert(getProtocolActionFromCommit(engine, tablePath, 1).isEmpty)\n\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        isNewTable = false,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"true\",\n        expectedValue = true)\n      assert(getProtocolActionFromCommit(engine, tablePath, 2).isEmpty)\n    }\n  }\n\n  test(\"Metadata toString should work with ICT enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n\n      txn.commit(engine, emptyIterable())\n\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1),\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"))\n\n      val metadata = getTableManagerAdapter.getSnapshotAtLatest(\n        engine,\n        tablePath).getMetadata\n      val inCommitTimestamp = getInCommitTimestamp(engine, tablePath, version = 1).get\n      assert(metadata.toString == String.format(\n        \"Metadata{id='%s', name=Optional.empty, description=Optional.empty, \" +\n          \"format=Format{provider='parquet', options={}}, \" +\n          \"schemaString='{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\" +\n          \"\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\" +\n          \"\\\"metadata\\\":{}}]}', \" +\n          \"partitionColumns=List(), createdTime=Optional[%s], \" +\n          \"configuration={delta.inCommitTimestampEnablementTimestamp=%s, \" +\n          \"delta.enableInCommitTimestamps=true, \" +\n          \"delta.inCommitTimestampEnablementVersion=1}}\",\n        metadata.getId,\n        metadata.getCreatedTime.get,\n        inCommitTimestamp.toString))\n    }\n  }\n\n  test(\"Table with ICT enabled is readable\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n\n      txn.commit(engine, emptyIterable())\n\n      val commitResult = appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1),\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"))\n\n      val ver1Snapshot =\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertMetadataProp(ver1Snapshot, IN_COMMIT_TIMESTAMPS_ENABLED, true)\n      assertMetadataProp(\n        ver1Snapshot,\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP,\n        Optional.of(getInCommitTimestamp(engine, tablePath, version = 1).get))\n      assertMetadataProp(\n        ver1Snapshot,\n        IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n        Optional.of(1L))\n\n      val expData = dataBatches1.flatMap(_.toTestRows)\n\n      verifyCommitResult(commitResult, expVersion = 1, expIsReadyForCheckpoint = false)\n      verifyCommitInfo(tablePath, version = 1, partitionCols = null)\n      verifyWrittenContent(tablePath, testSchema, expData)\n      verifyTableProperties(\n        tablePath,\n        ListMap(\n          // appendOnly, invariants implicitly supported as the protocol is upgraded from 2 to 7\n          // These properties are not set in the table properties, but are generated by the\n          // Spark describe\n          IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> true,\n          \"delta.feature.appendOnly\" -> \"supported\",\n          \"delta.feature.inCommitTimestamp\" -> \"supported\",\n          \"delta.feature.invariants\" -> \"supported\",\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.getKey\n            -> getInCommitTimestamp(engine, tablePath, version = 1).get,\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.getKey -> 1L),\n        1,\n        7)\n    }\n  }\n\n  /**\n   *  Helper method to read the inCommitTimestamp from the commit file of the given version if it\n   *  is not null, otherwise return null.\n   */\n  private def getInCommitTimestamp(\n      engine: Engine,\n      tablePath: String,\n      version: Long): Option[Long] = {\n    val commitInfoOpt =\n      CommitInfo.unsafeTryReadCommitInfoFromPublishedDeltaFile(\n        engine,\n        getLogPath(engine, tablePath),\n        version)\n    if (commitInfoOpt.isPresent && commitInfoOpt.get.getInCommitTimestamp.isPresent) {\n      Some(commitInfoOpt.get.getInCommitTimestamp.get)\n    } else {\n      Option.empty\n    }\n  }\n\n  test(\"Conflict resolution of timestamps\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      setTablePropAndVerify(\n        engine,\n        tablePath,\n        isNewTable = true,\n        IN_COMMIT_TIMESTAMPS_ENABLED,\n        \"true\",\n        true)\n\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val txn1 = getUpdateTxn(\n        engine,\n        tablePath,\n        clock = clock)\n      clock.setTime(startTime)\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2),\n        clock = clock)\n      clock.setTime(startTime - 1000)\n      commitAppendData(engine, txn1, Seq(Map.empty[String, Literal] -> dataBatches1))\n      assert(\n        getInCommitTimestamp(engine, tablePath, version = 2).get ===\n          getInCommitTimestamp(engine, tablePath, version = 1).get + 1)\n    }\n  }\n\n  Seq(10, 2).foreach { winningCommitCount =>\n    test(s\"Conflict resolution of enablement version(Winning Commit Count=$winningCommitCount)\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        val txn = getCreateTxn(engine, tablePath, testSchema)\n\n        txn.commit(engine, emptyIterable())\n\n        val startTime = System.currentTimeMillis() // we need to fix this now!\n        val txn1 = getUpdateTxn(\n          engine,\n          tablePath,\n          tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"),\n          clock = () => startTime)\n\n        // Sleep for 1 second to ensure that due to file-system truncation, the timestamp for these\n        // non-ict commits is not less than the fixed time for the txn above\n        // If this happens, nothing incorrect happens, but the\n        // ictEnablementTimestamp != prevVersion.timestamp + 1 since we will use the greater ts\n        Thread.sleep(1000)\n        for (_ <- 0 until winningCommitCount) {\n          appendData(\n            engine,\n            tablePath,\n            data = Seq(Map.empty[String, Literal] -> dataBatches2))\n        }\n\n        commitAppendData(engine, txn1, Seq(Map.empty[String, Literal] -> dataBatches1))\n\n        val lastSnapshot = getTableManagerAdapter.getSnapshotAtVersion(\n          engine,\n          tablePath,\n          winningCommitCount)\n        val curSnapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n        val observedEnablementTimestamp =\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetadata(curSnapshot.getMetadata)\n        val observedEnablementVersion =\n          IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetadata(curSnapshot.getMetadata)\n        assert(observedEnablementTimestamp.get === lastSnapshot.getTimestamp(engine) + 1)\n        assert(\n          observedEnablementTimestamp.get ===\n            getInCommitTimestamp(engine, tablePath, version = winningCommitCount + 1).get)\n        assert(observedEnablementVersion.get === winningCommitCount + 1)\n      }\n    }\n  }\n\n  test(\"Missing CommitInfo in last winning commit in conflict resolution should result in a \" +\n    \"DELTA_MISSING_COMMIT_INFO exception\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      setTablePropAndVerify(\n        engine,\n        tablePath,\n        isNewTable = true,\n        IN_COMMIT_TIMESTAMPS_ENABLED,\n        \"true\",\n        true)\n\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val txn1 = getUpdateTxn(\n        engine,\n        tablePath,\n        clock = clock)\n      clock.setTime(startTime)\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2),\n        clock = clock)\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2),\n        clock = clock)\n\n      // Remove CommitInfo from the commit.\n      removeCommitInfoFromCommit(engine, 2, getLogPath(engine, tablePath))\n\n      clock.setTime(startTime - 1000)\n      val ex = intercept[InvalidTableException] {\n        commitAppendData(engine, txn1, Seq(Map.empty[String, Literal] -> dataBatches1))\n      }\n      assert(ex.getMessage.contains(String.format(\n        \"This table has the feature %s enabled which requires the presence of the \" +\n          \"CommitInfo action in every commit. However, the CommitInfo action is \" +\n          \"missing from commit version %s.\",\n        \"inCommitTimestamp\",\n        \"2\")))\n    }\n  }\n\n  test(\"Throw an error where the winning txn enables the ICT and losing txn prepares txn with \" +\n    \"ICT enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val txn = getCreateTxn(engine, tablePath, testSchema)\n\n      txn.commit(engine, emptyIterable())\n\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val txn1 = getUpdateTxn(\n        engine,\n        tablePath,\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"),\n        clock = clock)\n      clock.setTime(startTime)\n      appendData(\n        engine,\n        tablePath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2),\n        tableProperties = Map(IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"),\n        clock = clock)\n      clock.setTime(startTime - 1000)\n      val ex = intercept[ProtocolChangedException] {\n        commitAppendData(engine, txn1, Seq(Map.empty[String, Literal] -> dataBatches1))\n      }\n      assert(ex.getMessage.contains(String.format(\"Transaction has encountered a conflict and \" +\n        \"can not be committed. Query needs to be re-executed using the latest version of the \" +\n        \"table.\")))\n    }\n  }\n\n  test(\"Disabling ICT removes enablement tracking properties\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      createTableThenEnableIctAndVerify(engine, tablePath)\n\n      // ===== WHEN =====\n      // Disable ICT. This should remove enablement tracking properties.\n      setTablePropAndVerify(\n        engine = engine,\n        tablePath = tablePath,\n        isNewTable = false,\n        key = IN_COMMIT_TIMESTAMPS_ENABLED,\n        value = \"false\",\n        expectedValue = false)\n\n      // ===== THEN =====\n      val snapshotV2 = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      assertMetadataProp(snapshotV2, IN_COMMIT_TIMESTAMPS_ENABLED, false)\n      assertHasNoMetadataProp(snapshotV2, IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP)\n      assertHasNoMetadataProp(snapshotV2, IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/LogCompactionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\n// import scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestRow, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs, WriteUtils}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.hook.LogCompactionHook\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LogCompactionSuite extends AbstractLogCompactionSuite with TestUtilsWithTableManagerAPIs\n\nclass LegacyLogCompactionSuite extends AbstractLogCompactionSuite with TestUtilsWithLegacyKernelAPIs\n\ntrait AbstractLogCompactionSuite extends AnyFunSuite with WriteUtils {\n  self: AbstractTestUtils =>\n\n  test(\"Compaction containing different action types\") {\n    withTempDirAndEngine { (tblPath, engine) =>\n      // commit 0 - add data\n      appendData(\n        engine,\n        tblPath,\n        isNewTable = true,\n        testSchema,\n        data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      // commit 1 - set a metadata prop\n      val newTblProps = Map(TableConfig.CHECKPOINT_POLICY.getKey -> \"v2\")\n      updateTableMetadata(engine, tblPath, tableProperties = newTblProps)\n\n      // commit 2 - add domain metadata\n      val dmTxn = getUpdateTxn(\n        engine,\n        tblPath,\n        withDomainMetadataSupported = true)\n      dmTxn.addDomainMetadata(\"testDomain\", \"testConfig\")\n      commitAppendData(engine, dmTxn, Seq.empty)\n\n      // commit 3 - add more data\n      appendData(\n        engine,\n        tblPath,\n        data = Seq(Map.empty[String, Literal] -> dataBatches2))\n\n      // create the compaction file(s)\n      val dataPath = new Path(s\"file:${tblPath}\")\n      val logPath = new Path(s\"file:${tblPath}\", \"_delta_log\")\n      val hook = new LogCompactionHook(dataPath, logPath, 0, 3, 0)\n      hook.threadSafeInvoke(engine)\n\n      val hadoopFileIO = new HadoopFileIO(new Configuration())\n      val metricEngine = new MetricsEngine(hadoopFileIO)\n\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(metricEngine, tblPath)\n      val checkpointProp =\n        snapshot.getMetadata().getConfiguration.get(TableConfig.CHECKPOINT_POLICY.getKey)\n      assert(checkpointProp == \"v2\")\n\n      // this is the read that the snapshot did\n      val propCompactionsRead = metricEngine.getJsonHandler.getCompactionsRead\n      assert(propCompactionsRead.toSet === Set((0, 3)))\n\n      metricEngine.resetMetrics()\n      val domainMetadata = snapshot.getDomainMetadata(\"testDomain\")\n      assert(domainMetadata.isPresent())\n      assert(domainMetadata.get == \"testConfig\")\n\n      // getting domain metadata requires another log-reply, so check that this one also used the\n      // compaction\n      val dmCompactionsRead = metricEngine.getJsonHandler.getCompactionsRead\n      assert(dmCompactionsRead.toSet === Set((0, 3)))\n\n      // ensure the data is all there\n      metricEngine.resetMetrics()\n      val expectedAnswer = dataBatches1.flatMap(_.toTestRows) ++ dataBatches2.flatMap(_.toTestRows)\n      checkTable(tblPath, expectedAnswer, engine = metricEngine)\n\n      val readCompactionsRead = metricEngine.getJsonHandler.getCompactionsRead\n      assert(readCompactionsRead.toSet === Set((0, 3)))\n    }\n  }\n\n  def testWithCompactions(\n      versionsToWrite: Seq[Int], // highest version MUST be last!\n      versionToRead: Option[Long],\n      doRemoves: Boolean,\n      compactions: Seq[(Int, Int)],\n      expectedDeltasToBeRead: Set[Int],\n      expectedCompactionsToBeRead: Set[(Int, Int)]) {\n    withTempDir { tmpDir =>\n      val tablePath = tmpDir.getCanonicalPath\n      val hadoopFileIO = new HadoopFileIO(new Configuration() {\n        {\n          // Set the batch sizes to small so that we get to test the multiple batch scenarios.\n          set(\"delta.kernel.default.parquet.reader.batch-size\", \"2\");\n          set(\"delta.kernel.default.json.reader.batch-size\", \"2\");\n        }\n      })\n      val engine = new MetricsEngine(hadoopFileIO)\n      var expectedRows: Set[Long] = Set()\n      versionsToWrite.foreach { i =>\n        // if we're removing, then on odd commits, remove the lower 10 of the previous 20 rows added\n        if (doRemoves && i % 2 == 1) {\n          val prev = i - 1;\n          val low = prev * 10\n          val high = prev * 10 + 10\n          val deleteQuery = \"DELETE FROM delta.`%s` WHERE id >= %d AND id < %d\".format(\n            tablePath,\n            low,\n            high)\n          spark.sql(deleteQuery)\n          if (versionToRead.isEmpty || versionToRead.get >= i) {\n            expectedRows --= (low until high).map(i => i.toLong)\n          }\n          // if (i == compactions(0).1) {\n          //   // ensure we put a DM in a compaction\n\n          // }\n        } else {\n          val low = i * 10\n          // if we're removing, add 20 rows as the first 10 will be removed by the next version,\n          // otherwise add 10 rows\n          val high = if (doRemoves) low + 20 else low + 10\n          spark.range(low, high).write\n            .format(\"delta\")\n            .mode(\"append\")\n            .save(tablePath)\n\n          if (versionToRead.isEmpty || versionToRead.get >= i) {\n            expectedRows ++= (low until high).map(i => i.toLong)\n          }\n        }\n      }\n\n      val dataPath = new Path(s\"file:${tablePath}\")\n      val logPath = new Path(s\"file:${tablePath}\", \"_delta_log\")\n      // create the compaction file(s)\n      compactions.foreach { compaction =>\n        val hook = new LogCompactionHook(\n          dataPath,\n          logPath,\n          compaction._1,\n          compaction._2,\n          0)\n        hook.threadSafeInvoke(engine)\n      }\n      engine.resetMetrics()\n\n      checkTable(\n        path = tablePath,\n        expectedAnswer = expectedRows.toSeq.map(i => TestRow(i)),\n        engine = engine,\n        version = versionToRead)\n\n      val actualJsonVersionsRead = engine.getJsonHandler.getVersionsRead\n      val actualCompactionsRead = engine.getJsonHandler.getCompactionsRead\n      assert(actualJsonVersionsRead.toSet == expectedDeltasToBeRead)\n      assert(actualCompactionsRead.toSet == expectedCompactionsToBeRead)\n    }\n  }\n\n  Seq(Seq((0, 3)), Seq((3, 5)), Seq((5, 9)), Seq((0, 3), (5, 8))).foreach {\n    compactions =>\n      Seq(true, false).foreach { doRemoves =>\n        val compactionStr = compactions.mkString(\", \")\n        test(s\"Compaction(s) at $compactionStr (no checkpoint, removes: $doRemoves)\") {\n          // for these tests, write 0 - 9 (inclusive)\n          val versionsToWrite = (0 to 9)\n          var expectedDeltasToBeRead = versionsToWrite.toSet\n          compactions.foreach { compaction =>\n            // subtract out the compaction versions from the full set\n            expectedDeltasToBeRead &~= (compaction._1 to compaction._2).toSet\n          }\n          testWithCompactions(\n            versionsToWrite,\n            versionToRead = None,\n            doRemoves,\n            compactions,\n            expectedDeltasToBeRead,\n            compactions.toSet)\n        }\n      }\n  }\n\n  Seq(Seq((3, 5)), Seq((8, 11)), Seq((8, 12), (11, 15)), Seq((11, 13), (15, 17))).foreach {\n    compactions =>\n      Seq(true, false).foreach { doRemoves =>\n        val compactionStr = compactions.mkString(\", \")\n        test(s\"Compaction(s) at $compactionStr (with checkpoint, removes: $doRemoves)\") {\n          // for these tests, write 0 - 19 (inclusive), will checkpoint at 10\n          val versionsToWrite = (0 to 19)\n          val versionsAfterCheckpoint = (11 to 19)\n          var expectedDeltasToBeRead = versionsAfterCheckpoint.toSet\n          var expectedCompactionsToBeRead = Set[(Int, Int)]()\n          compactions.foreach { compaction =>\n            if (compaction._1 > 10) { // only use if after checkpoint\n              // subtract out the compaction versions from the full set\n              expectedDeltasToBeRead &~= (compaction._1 to compaction._2).toSet\n              // add to expected compactions\n              expectedCompactionsToBeRead += compaction\n            }\n          }\n          testWithCompactions(\n            versionsToWrite,\n            versionToRead = None,\n            doRemoves,\n            compactions,\n            expectedDeltasToBeRead,\n            expectedCompactionsToBeRead)\n        }\n      }\n  }\n\n  test(\"Compaction with overlap\") {\n    testWithCompactions(\n      versionsToWrite = (0 to 9),\n      versionToRead = None,\n      doRemoves = true,\n      compactions = Seq((0, 3), (2, 4)),\n      expectedDeltasToBeRead = Set(0, 1, 5, 6, 7, 8, 9),\n      expectedCompactionsToBeRead = Set((2, 4)))\n  }\n\n  test(\"Compaction is whole range\") {\n    testWithCompactions(\n      versionsToWrite = (0 to 5),\n      versionToRead = None,\n      doRemoves = true,\n      compactions = Seq((0, 5)),\n      expectedDeltasToBeRead = Set(),\n      expectedCompactionsToBeRead = Set((0, 5)))\n  }\n\n  test(\"Compaction out of range\") {\n    testWithCompactions(\n      versionsToWrite = (0 to 9),\n      versionToRead = Some(6),\n      doRemoves = true,\n      compactions = Seq((1, 3), (5, 8)),\n      expectedDeltasToBeRead = Set(0, 4, 5, 6),\n      expectedCompactionsToBeRead = Set((1, 3)))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/LogCompactionWriterSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.TestRow\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.hook.PostCommitHook\nimport io.delta.kernel.internal.{DeltaLogActionUtils, SnapshotImpl}\nimport io.delta.kernel.internal.TableConfig.TOMBSTONE_RETENTION\nimport io.delta.kernel.internal.actions._\nimport io.delta.kernel.internal.compaction.LogCompactionWriter\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.hook.LogCompactionHook\nimport io.delta.kernel.internal.replay.ActionsIterator\nimport io.delta.kernel.internal.util.FileNames.DeltaLogFileType\nimport io.delta.kernel.internal.util.ManualClock\nimport io.delta.kernel.internal.util.Utils.singletonCloseableIterator\nimport io.delta.kernel.types.{IntegerType, StructType}\nimport io.delta.kernel.utils.FileStatus\n\nimport org.apache.spark.sql.delta.{DeltaLog, DomainMetadataTableFeature}\nimport org.apache.spark.sql.delta.DeltaOperations.Truncate\nimport org.apache.spark.sql.delta.actions.{DomainMetadata => DeltaSparkDomainMetadata, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\n/**\n * Test suite for io.delta.kernel.internal.compaction.LogCompactionWriter\n */\nclass LogCompactionWriterSuite extends CheckpointBase {\n  val COMPACTED_SCHEMA =\n    new StructType()\n      .add(\"txn\", SetTransaction.FULL_SCHEMA)\n      .add(\"add\", AddFile.FULL_SCHEMA)\n      .add(\"remove\", RemoveFile.FULL_SCHEMA)\n      .add(\"metaData\", Metadata.FULL_SCHEMA)\n      .add(\"protocol\", Protocol.FULL_SCHEMA)\n      // .add(\"cdc\", new StructType())\n      .add(\"domainMetadata\", DomainMetadata.FULL_SCHEMA);\n\n  val ADD_INDEX = 1\n  val REMOVE_INDEX = 2\n  val METADATA_INDEX = 3\n  val PROTOCOL_INDEX = 4\n  val DM_INDEX = 5\n\n  val ADD_REM_PATH_INDEX = 0\n  val DM_NAME_INDEX = 0\n\n  // check if a row is all null\n  def rowIsNull(row: Row): Boolean = {\n    val schema = row.getSchema()\n    for (ordinal <- 0 until schema.length()) {\n      if (!row.isNullAt(ordinal)) {\n        return false\n      }\n    }\n    true\n  }\n\n  // Get the expected actions from the log.  We filter down to just the actions that end up in a\n  // compacted log file, and also filter out adds that have been removed as well as duplicate\n  // metadata/protocol actions\n  def getActionsFromLog(\n      tablePath: Path,\n      engine: Engine,\n      startVersion: Long,\n      endVersion: Long): Seq[TestRow] = {\n    val files = DeltaLogActionUtils.listDeltaLogFilesAsIter(\n      engine,\n      Collections.singleton(DeltaLogFileType.COMMIT),\n      tablePath,\n      startVersion,\n      Optional.of(endVersion),\n      false /* mustBeRecreatable */ )\n      .toInMemoryList()\n    Collections.reverse(files) // we want things in reverse order\n    val actions =\n      new ActionsIterator(engine, files, COMPACTED_SCHEMA, Optional.empty())\n    val removed = scala.collection.mutable.HashSet.empty[String]\n    val seenDomains = scala.collection.mutable.HashSet.empty[String]\n    var seenMetadata = false\n    var seenProtocol = false\n    actions.toSeq.flatMap { wrapper =>\n      wrapper.getColumnarBatch().getRows.toSeq.flatMap { row =>\n        if (!row.isNullAt(REMOVE_INDEX)) {\n          val removeRow = row.getStruct(REMOVE_INDEX)\n          val path = removeRow.getString(ADD_REM_PATH_INDEX)\n          removed += path\n        }\n\n        if (!row.isNullAt(ADD_INDEX)) {\n          val addRow = row.getStruct(ADD_INDEX)\n          val path = addRow.getString(ADD_REM_PATH_INDEX)\n          if (!removed.contains(path)) {\n            Some(TestRow(row))\n          } else {\n            None\n          }\n        } else if (!row.isNullAt(METADATA_INDEX)) {\n          if (!seenMetadata) {\n            seenMetadata = true\n            Some(TestRow(row))\n          } else {\n            None\n          }\n        } else if (!row.isNullAt(PROTOCOL_INDEX)) {\n          if (!seenProtocol) {\n            seenProtocol = true\n            Some(TestRow(row))\n          } else {\n            None\n          }\n        } else if (!row.isNullAt(DM_INDEX)) {\n          val dm = row.getStruct(DM_INDEX)\n          val domain = dm.getString(DM_NAME_INDEX)\n          if (!seenDomains.contains(domain)) {\n            seenDomains += domain\n            Some(TestRow(row))\n          } else {\n            None\n          }\n        } else if (!rowIsNull(row)) {\n          Some(TestRow(row))\n        } else {\n          None\n        }\n      }\n    }\n  }\n\n  def getActionsFromCompacted(\n      compactedPath: String,\n      engine: Engine): Seq[Row] = {\n    val fileStatus = FileStatus.of(compactedPath, 0, 0)\n    val batches = engine\n      .getJsonHandler()\n      .readJsonFiles(\n        singletonCloseableIterator(fileStatus),\n        COMPACTED_SCHEMA,\n        Optional.empty())\n    batches.toSeq.flatMap(_.getRows().toSeq)\n  }\n\n  def addDomainMetadata(path: String, d1Val: String, d2Val: String): Unit = {\n    spark.sql(\n      s\"\"\"\n         |ALTER TABLE delta.`$path`\n         |SET TBLPROPERTIES\n         |('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n         |\"\"\".stripMargin)\n\n    val deltaLog = DeltaLog.forTable(spark, path)\n    val domainMetadata = DeltaSparkDomainMetadata(\"testDomain1\", d1Val, false) ::\n      DeltaSparkDomainMetadata(\"testDomain2\", d2Val, false) :: Nil\n    deltaLog.startTransaction().commit(domainMetadata, Truncate())\n  }\n\n  Seq(false, true).foreach { includeRemoves =>\n    Seq(false, true).foreach { includeDM =>\n      val removesMsg = if (includeRemoves) \" and removes\" else \"\"\n      val dmMsg = if (includeDM) \", include multiple PandM and DomainMetadata\" else \"\"\n      test(s\"commits containing adds${removesMsg}${dmMsg}\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          addData(tablePath, alternateBetweenAddsAndRemoves = includeRemoves, numberIter = 8)\n          if (includeDM) {\n            addDomainMetadata(tablePath, \"\", \"{\\\"key1\\\":\\\"value1\\\"}\")\n            addDomainMetadata(tablePath, \"here\", \"{\\\"key2\\\":\\\"value2\\\"}\")\n          }\n\n          val expectedLastCommit = if (includeDM) {\n            11 // 0-7 for add/removes + 2 for enable+set DomainMetatdata\n          } else {\n            7 // 0-7 for add/removes\n          }\n\n          val actionsFromCommits =\n            getActionsFromLog(new Path(tablePath), engine, 0, expectedLastCommit)\n\n          val dataPath = new Path(s\"file:${tablePath}\")\n          val logPath = new Path(s\"file:${tablePath}\", \"_delta_log\")\n\n          val hook = new LogCompactionHook(\n            dataPath,\n            logPath,\n            0,\n            expectedLastCommit,\n            0)\n          hook.threadSafeInvoke(engine)\n          val endCommitStr = f\"$expectedLastCommit%020d\"\n          val compactedPath =\n            tablePath + s\"/_delta_log/00000000000000000000.${endCommitStr}.compacted.json\"\n          val actionsFromCompacted = getActionsFromCompacted(compactedPath, engine)\n\n          checkAnswer(actionsFromCompacted, actionsFromCommits)\n        }\n      }\n    }\n  }\n\n  Seq(false, true).foreach { includeRemoves =>\n    val testMsgUpdate = if (includeRemoves) \" and removes\" else \"\"\n    test(s\"Read table with adds$testMsgUpdate\") {\n      withTempDirAndEngine { (tablePath, engine) =>\n        addData(tablePath, alternateBetweenAddsAndRemoves = includeRemoves, numberIter = 10)\n\n        spark.conf.set(DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS.key, \"false\")\n        val withoutCompactionData = readUsingSpark(tablePath)\n\n        val dataPath = new Path(s\"file:${tablePath}\")\n        val logPath = new Path(s\"file:${tablePath}\", \"_delta_log\")\n        val hook = new LogCompactionHook(dataPath, logPath, 0, 9, 0)\n        hook.threadSafeInvoke(engine)\n\n        spark.conf.set(DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS.key, \"true\")\n        val withCompactionData = readUsingSpark(tablePath)\n\n        checkAnswer(withCompactionData, withoutCompactionData)\n      }\n    }\n  }\n\n  test(s\"Error if not enough commits\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      addData(tablePath, alternateBetweenAddsAndRemoves = false, numberIter = 2)\n      val dataPath = new Path(s\"file:${tablePath}\")\n      val logPath = new Path(s\"file:${tablePath}\", \"_delta_log\")\n      val hook = new LogCompactionHook(dataPath, logPath, 0, 5, 0)\n      val ex = intercept[IllegalArgumentException] {\n        hook.threadSafeInvoke(engine)\n      }\n      assert(ex.getMessage.contains(\n        \"Asked to compact between versions 0 and 5, but found 2 delta files\"))\n    }\n  }\n\n  test(\"Hook is generated correctly and when expected\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val clock = new ManualClock(0)\n      val schema = new StructType().add(\"col\", IntegerType.INTEGER)\n      val dataPath = new Path(s\"file:${tablePath}\")\n      val logPath = new Path(s\"file:${tablePath}\", \"_delta_log\")\n      createEmptyTable(engine, tablePath, schema, clock = clock)\n      val table = Table.forPath(engine, tablePath)\n      val metadata = table.getLatestSnapshot(engine).asInstanceOf[SnapshotImpl].getMetadata()\n      val tombstoneRetention = TOMBSTONE_RETENTION.fromMetadata(metadata)\n      clock.setTime(tombstoneRetention) // set to the retention time so (time - retention) == 0\n      val compactionInterval = 3\n      var hooksFound = 0\n      // start at 1 since the create of the table is 0\n      for (commitNum <- 1 to 5) {\n        val txn =\n          getUpdateTxn(engine, tablePath, clock = clock, logCompactionInterval = compactionInterval)\n        val data = generateData(\n          schema,\n          Seq.empty,\n          Map.empty[String, Literal],\n          batchSize = 1,\n          numBatches = 1)\n        val commitResult =\n          commitAppendData(engine, txn, data = Seq(Map.empty[String, Literal] -> data))\n        // expect every compactionInterval\n        val expectHook = ((commitNum + 1) % compactionInterval == 0)\n        assert(LogCompactionWriter.shouldCompact(commitNum, compactionInterval) == expectHook)\n        var foundHook = false\n        for (hook <- commitResult.getPostCommitHooks().asScala) {\n          if (hook.getType() == PostCommitHook.PostCommitHookType.LOG_COMPACTION) {\n            assert(!foundHook) // there should never be more than one\n            foundHook = true\n            hooksFound += 1\n            val logCompactionHook = hook.asInstanceOf[LogCompactionHook]\n            assert(logCompactionHook.getDataPath() == dataPath)\n            assert(logCompactionHook.getLogPath() == logPath)\n            assert(logCompactionHook.getStartVersion() == commitNum + 1 - compactionInterval)\n            assert(logCompactionHook.getCommitVersion() == commitNum)\n            assert(logCompactionHook.getMinFileRetentionTimestampMillis() == 0)\n          }\n          hook.threadSafeInvoke(engine)\n        }\n        assert(foundHook == expectHook)\n      }\n      assert(hooksFound == 2) // expect 0<->2 and 3<->5\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/LogReplayBaseSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\nimport java.util.Optional\n\nimport scala.collection.convert.ImplicitConversions._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.ColumnarBatch\nimport io.delta.kernel.defaults.engine.{DefaultEngine, DefaultJsonHandler, DefaultParquetHandler}\nimport io.delta.kernel.defaults.engine.fileio.FileIO\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.AbstractTestUtils\nimport io.delta.kernel.engine.{Engine, ExpressionHandler, FileSystemClient}\nimport io.delta.kernel.engine.FileReadResult\nimport io.delta.kernel.expressions.Predicate\nimport io.delta.kernel.internal.checkpoints.Checkpointer\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Base trait containing shared code for log replay metric testing.\n *\n * This trait provides common infrastructure for testing how Delta log files are read\n * during table operations, with utilities for metrics collection and verification.\n */\ntrait LogReplayBaseSuite extends AnyFunSuite { self: AbstractTestUtils =>\n\n  protected def withTempDirAndMetricsEngine(f: (String, MetricsEngine) => Unit): Unit = {\n    val hadoopFileIO = new HadoopFileIO(new Configuration() {\n      {\n        // Set the batch sizes to small so that we get to test the multiple batch scenarios.\n        set(\"delta.kernel.default.parquet.reader.batch-size\", \"2\");\n        set(\"delta.kernel.default.json.reader.batch-size\", \"2\");\n      }\n    })\n\n    val engine = new MetricsEngine(hadoopFileIO)\n\n    withTempDir { dir =>\n      f(dir.getAbsolutePath, engine)\n    }\n  }\n\n  protected def loadPandMCheckMetrics(\n      tablePath: String,\n      engine: MetricsEngine,\n      expJsonVersionsRead: Seq[Long],\n      expParquetVersionsRead: Seq[Long],\n      expParquetReadSetSizes: Seq[Long] = null,\n      expChecksumReadSet: Seq[Long] = null,\n      version: Long = -1): Unit = {\n    engine.resetMetrics()\n\n    version match {\n      case -1 => getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      case ver => getTableManagerAdapter.getSnapshotAtVersion(engine, tablePath, version)\n    }\n\n    assertMetrics(\n      engine,\n      expJsonVersionsRead,\n      expParquetVersionsRead,\n      expParquetReadSetSizes,\n      expChecksumReadSet = expChecksumReadSet)\n  }\n\n  protected def assertMetrics(\n      engine: MetricsEngine,\n      expJsonVersionsRead: Seq[Long],\n      expParquetVersionsRead: Seq[Long],\n      expParquetReadSetSizes: Seq[Long] = null,\n      expLastCheckpointReadCalls: Option[Int] = None,\n      expChecksumReadSet: Seq[Long] = null): Unit = {\n    val actualJsonVersionsRead = engine.getJsonHandler.getVersionsRead\n    val actualParquetVersionsRead = engine.getParquetHandler.getVersionsRead\n\n    assert(\n      actualJsonVersionsRead === expJsonVersionsRead,\n      s\"Expected to read json versions \" +\n        s\"$expJsonVersionsRead but read $actualJsonVersionsRead\")\n    assert(\n      actualParquetVersionsRead === expParquetVersionsRead,\n      s\"Expected to read parquet \" +\n        s\"versions $expParquetVersionsRead but read $actualParquetVersionsRead\")\n\n    if (expParquetReadSetSizes != null) {\n      val actualParquetReadSetSizes = engine.getParquetHandler.checkpointReadRequestSizes\n      assert(\n        actualParquetReadSetSizes === expParquetReadSetSizes,\n        s\"Expected parquet read set sizes \" +\n          s\"$expParquetReadSetSizes but read $actualParquetReadSetSizes\")\n    }\n\n    expLastCheckpointReadCalls.foreach { expCalls =>\n      val actualCalls = engine.getJsonHandler.getLastCheckpointMetadataReadCalls\n      assert(\n        actualCalls === expCalls,\n        s\"Expected to read last checkpoint metadata $expCalls times but read $actualCalls times\")\n    }\n\n    if (expChecksumReadSet != null) {\n      val actualChecksumReadSet = engine.getJsonHandler.checksumsRead\n      assert(\n        actualChecksumReadSet === expChecksumReadSet,\n        s\"Expected checksum read set \" +\n          s\"$expChecksumReadSet but read $actualChecksumReadSet\")\n    }\n  }\n\n  protected def appendCommit(path: String): Unit =\n    spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n  /**\n   * Creates a temporary directory with a test engine and builds a table with CRC files.\n   * Returns the created table path and engine for testing.\n   *\n   * @param f code to run with the table and engine\n   */\n  protected def withTableWithCrc(f: (String, MetricsEngine) => Any): Unit = {\n    withTempDirAndMetricsEngine { (path, engine) =>\n      // Produce a test table with 0 to 11 .json, 0 to 11.crc, 10.checkpoint.parquet\n      withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"true\") {\n        spark.sql(\n          s\"CREATE TABLE delta.`$path` USING DELTA AS \" +\n            s\"SELECT 0L as id\")\n        for (_ <- 0 to 10) { appendCommit(path) }\n        assert(checkpointFileExistsForTable(path, 10))\n      }\n      f(path, engine)\n    }\n  }\n\n  /**\n   * Creates a temporary directory with a test engine and builds a table with CRC files.\n   * Returns the created table and engine for testing.\n   *\n   * @param f code to run with the table and engine\n   */\n  protected def withTableWithCrc(f: (Table, String, MetricsEngine) => Any): Unit = {\n    withTableWithCrc((path, engine) => {\n      val table = Table.forPath(engine, path)\n      f(table, path, engine)\n    })\n  }\n}\n\n////////////////////\n// Helper Classes //\n////////////////////\n\n/** An engine that records the Delta commit (.json) and checkpoint (.parquet) files read */\nclass MetricsEngine(fileIO: FileIO) extends Engine {\n  private val impl = DefaultEngine.create(fileIO)\n  private val jsonHandler = new MetricsJsonHandler(fileIO)\n  private val parquetHandler = new MetricsParquetHandler(fileIO)\n\n  def resetMetrics(): Unit = {\n    jsonHandler.resetMetrics()\n    parquetHandler.resetMetrics()\n  }\n\n  override def getExpressionHandler: ExpressionHandler = impl.getExpressionHandler\n\n  override def getJsonHandler: MetricsJsonHandler = jsonHandler\n\n  override def getFileSystemClient: FileSystemClient = impl.getFileSystemClient\n\n  override def getParquetHandler: MetricsParquetHandler = parquetHandler\n}\n\n/**\n * Helper trait which wraps an underlying json/parquet read and collects the versions (e.g. 10.json,\n * 10.checkpoint.parquet) read\n */\ntrait FileReadMetrics { self: Object =>\n  // number of times read is requested on `_last_checkpoint`\n  private var lastCheckpointMetadataReadCalls = 0\n\n  val checksumsRead = new ArrayBuffer[Long]() // versions of checksum files read\n\n  private val versionsRead = ArrayBuffer[Long]()\n\n  private val compactionVersionsRead = ArrayBuffer[(Long, Long)]()\n\n  // Number of checkpoint files requested read in each readParquetFiles call\n  val checkpointReadRequestSizes = new ArrayBuffer[Long]()\n\n  private def updateVersionsRead(fileStatus: FileStatus): Unit = {\n    val path = new Path(fileStatus.getPath)\n    if (FileNames.isCommitFile(path.getName) || FileNames.isCheckpointFile(path.getName)) {\n      val version = FileNames.getFileVersion(path)\n\n      // We may split json/parquet reads, so don't record the same file multiple times\n      if (!versionsRead.contains(version)) {\n        versionsRead += version\n      }\n    } else if (Checkpointer.LAST_CHECKPOINT_FILE_NAME.equals(path.getName)) {\n      lastCheckpointMetadataReadCalls += 1\n    } else if (FileNames.isChecksumFile(path.getName)) {\n      checksumsRead += FileNames.getFileVersion(path)\n    } else if (FileNames.isLogCompactionFile(path.getName)) {\n      val versions = FileNames.logCompactionVersions(path)\n      compactionVersionsRead += ((versions._1, versions._2))\n    }\n  }\n\n  def getVersionsRead: Seq[Long] = versionsRead.toSeq\n\n  def getCompactionsRead: Seq[(Long, Long)] = compactionVersionsRead.toSeq\n\n  def getLastCheckpointMetadataReadCalls: Int = lastCheckpointMetadataReadCalls\n\n  def resetMetrics(): Unit = {\n    lastCheckpointMetadataReadCalls = 0\n    versionsRead.clear()\n    compactionVersionsRead.clear()\n    checkpointReadRequestSizes.clear()\n    checksumsRead.clear()\n  }\n\n  def collectReadFiles(fileIter: CloseableIterator[FileStatus]): CloseableIterator[FileStatus] = {\n    fileIter.map(file => {\n      updateVersionsRead(file)\n      file\n    })\n  }\n}\n\n/** A JsonHandler that collects metrics on the Delta commit (.json) files read */\nclass MetricsJsonHandler(fileIO: FileIO)\n    extends DefaultJsonHandler(fileIO)\n    with FileReadMetrics {\n\n  override def readJsonFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n    super.readJsonFiles(collectReadFiles(fileIter), physicalSchema, predicate)\n  }\n}\n\n/** A ParquetHandler that collects metrics on the Delta checkpoint (.parquet) files read */\nclass MetricsParquetHandler(fileIO: FileIO)\n    extends DefaultParquetHandler(fileIO)\n    with FileReadMetrics {\n\n  override def readParquetFiles(\n      fileIter: CloseableIterator[FileStatus],\n      physicalSchema: StructType,\n      predicate: Optional[Predicate]): CloseableIterator[FileReadResult] = {\n    val fileReadSet = fileIter.toSeq\n    checkpointReadRequestSizes += fileReadSet.size\n    super.readParquetFiles(\n      collectReadFiles(toCloseableIterator(fileReadSet.iterator)),\n      physicalSchema,\n      predicate)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/LogReplayEngineMetricsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.nio.file.Files\n\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.scalatest.BeforeAndAfterAll\n\nclass LegacyLogReplayEngineMetricsSuite extends AbstractLogReplayEngineMetricsSuite\n    with TestUtilsWithLegacyKernelAPIs\n\nclass LogReplayEngineMetricsSuite extends AbstractLogReplayEngineMetricsSuite\n    with TestUtilsWithTableManagerAPIs\n\n/**\n * Suite to test the engine metrics while replaying logs for getting the table protocol and\n * metadata (P&M) and scanning files. The metrics include how many files delta files, checkpoint\n * files read, size of checkpoint read set, and how many times `_last_checkpoint` is read etc.\n *\n * The goal is to test the behavior of calls to `readJsonFiles` and `readParquetFiles` that\n * Kernel makes. This calls determine the performance.\n */\ntrait AbstractLogReplayEngineMetricsSuite extends LogReplayBaseSuite with BeforeAndAfterAll {\n  self: AbstractTestUtils =>\n\n  // Disable writing checksums for this test suite\n  // This test suite checks the files read when loading the P&M, however, with the crc optimization\n  // if crc are available, crc will be the only files read.\n  // We want to test the P&M loading when CRC are not available in the tests.\n  // Tests for tables with available CRC are included using resource test tables (and thus are\n  // unaffected by changing our confs for writes).\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key, false)\n  }\n\n  override def afterAll(): Unit = {\n    try {\n      spark.conf.set(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key, true)\n    } finally {\n      super.afterAll()\n    }\n  }\n\n  /////////////////////////\n  // Test Helper Methods //\n  /////////////////////////\n\n  def loadScanFilesCheckMetrics(\n      engine: MetricsEngine,\n      tablePath: String,\n      expJsonVersionsRead: Seq[Long],\n      expParquetVersionsRead: Seq[Long],\n      expParquetReadSetSizes: Seq[Long],\n      expLastCheckpointReadCalls: Option[Int] = None): Unit = {\n    engine.resetMetrics()\n    val scan = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n      .getScanBuilder().build()\n    // get all scan files and iterate through them to trigger the metrics collection\n    val scanFiles = scan.getScanFiles(engine)\n    while (scanFiles.hasNext) scanFiles.next()\n\n    assertMetrics(\n      engine,\n      expJsonVersionsRead,\n      expParquetVersionsRead,\n      expParquetReadSetSizes,\n      expLastCheckpointReadCalls)\n  }\n\n  def checkpoint(path: String, actionsPerFile: Int): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> actionsPerFile.toString) {\n      DeltaLog.forTable(spark, path).checkpoint()\n    }\n  }\n\n  ///////////\n  // Tests //\n  ///////////\n\n  test(\"no hint, no checkpoint, reads all files\") {\n    withTempDirAndMetricsEngine { (path, engine) =>\n      for (_ <- 0 to 9) { appendCommit(path) }\n\n      loadPandMCheckMetrics(\n        path,\n        engine,\n        expJsonVersionsRead = 9L to 0L by -1L,\n        expParquetVersionsRead = Nil)\n    }\n  }\n\n  test(\"no hint, existing checkpoint, reads all files up to that checkpoint\") {\n    withTempDirAndMetricsEngine { (path, engine) =>\n      for (_ <- 0 to 14) { appendCommit(path) }\n\n      loadPandMCheckMetrics(\n        path,\n        engine,\n        expJsonVersionsRead = 14L to 11L by -1L,\n        expParquetVersionsRead = Seq(10),\n        expParquetReadSetSizes = Seq(1))\n    }\n  }\n\n  test(\"no hint, existing checkpoint, newer P & M update, reads up to P & M commit\") {\n    withTempDirAndMetricsEngine { (path, engine) =>\n      for (_ <- 0 to 12) { appendCommit(path) }\n\n      // v13 changes the protocol (which also updates the metadata)\n      spark.sql(s\"\"\"\n          |ALTER TABLE delta.`$path` SET TBLPROPERTIES (\n          |  'delta.minReaderVersion' = '2',\n          |  'delta.minWriterVersion' = '5',\n          |  'delta.columnMapping.mode' = 'name'\n          |)\n          |\"\"\".stripMargin)\n\n      for (_ <- 14 to 16) { appendCommit(path) }\n\n      loadPandMCheckMetrics(\n        path,\n        engine,\n        expJsonVersionsRead = 16L to 13L by -1L,\n        expParquetVersionsRead = Nil)\n    }\n  }\n\n  test(\"read a table with multi-part checkpoint\") {\n    withTempDirAndMetricsEngine { (path, engine) =>\n      for (_ <- 0 to 14) { appendCommit(path) }\n\n      // there should be one checkpoint file at version 10\n      loadScanFilesCheckMetrics(\n        engine,\n        path,\n        expJsonVersionsRead = 14L to 11L by -1L,\n        expParquetVersionsRead = Seq(10),\n        // we read the checkpoint twice: once for the P &M and once for the scan files\n        expParquetReadSetSizes = Seq(1, 1))\n\n      // create a multi-part checkpoint\n      checkpoint(path, actionsPerFile = 2)\n\n      // Reset metrics.\n      engine.resetMetrics()\n\n      // expect the Parquet read set to contain one request with size of 15\n      loadScanFilesCheckMetrics(\n        engine,\n        path,\n        expJsonVersionsRead = Nil,\n        expParquetVersionsRead = Seq(14),\n        // we read the checkpoint twice: once for the P &M and once for the scan files\n        expParquetReadSetSizes = Seq(8, 8))\n    }\n  }\n\n  Seq(true, false).foreach { deleteLastCheckpointMetadataFile =>\n    test(\"ensure `_last_checkpoint` is tried to read only once when \" +\n      s\"\"\"${if (deleteLastCheckpointMetadataFile) \"not exists\" else \"valid file exists\"}\"\"\") {\n      withTempDirAndMetricsEngine { (path, engine) =>\n        for (_ <- 0 to 14) { appendCommit(path) }\n\n        if (deleteLastCheckpointMetadataFile) {\n          assert(Files.deleteIfExists(new File(path, \"_delta_log/_last_checkpoint\").toPath))\n        }\n\n        // there should be one checkpoint file at version 10\n        loadScanFilesCheckMetrics(\n          engine,\n          path,\n          expJsonVersionsRead = 14L to 11L by -1L,\n          expParquetVersionsRead = Seq(10),\n          // we read the checkpoint twice: once for the P &M and once for the scan files\n          expParquetReadSetSizes = Seq(1, 1),\n          // We try to read `_last_checkpoint` once. If it doesn't exist, we don't try reading\n          // again. If it exists, we succeed reading in the first time\n          expLastCheckpointReadCalls = Some(1))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/LogReplaySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.nio.file.Files\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.Table\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestRow, TestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\nimport io.delta.kernel.internal.{InternalScanFileUtils, SnapshotImpl}\nimport io.delta.kernel.internal.data.ScanStateRow\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.types.{LongType, StructType}\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LogReplaySuite extends AbstractLogReplaySuite with TestUtilsWithTableManagerAPIs {\n  override lazy val defaultEngine = defaultEngineBatchSize2\n}\n\nclass LegacyLogReplaySuite extends AbstractLogReplaySuite with TestUtilsWithLegacyKernelAPIs {\n  override lazy val defaultEngine = defaultEngineBatchSize2\n}\n\ntrait AbstractLogReplaySuite extends AnyFunSuite {\n  self: AbstractTestUtils =>\n\n  test(\"simple end to end with inserts and deletes and checkpoint\") {\n    val expectedValues = (0L until 5L) ++ (10L until 15L) ++ (20L until 25L) ++\n      (30L until 35L) ++ (40L until 45L) ++ (50L to 65L)\n    checkTable(\n      path = goldenTablePath(\"basic-with-inserts-deletes-checkpoint\"),\n      expectedAnswer = expectedValues.map(TestRow(_)),\n      expectedSchema = new StructType().add(\"id\", LongType.LONG),\n      expectedVersion = Some(13L))\n  }\n\n  test(\"simple end to end with inserts and updates\") {\n    val expectedValues = (0 until 50).map((_, \"N/A\")) ++\n      (50 until 100).map(x => (x, s\"val=$x\"))\n    checkTable(\n      path = goldenTablePath(\"basic-with-inserts-updates\"),\n      expectedAnswer = expectedValues.map(TestRow.fromTuple))\n  }\n\n  test(\"simple end to end with inserts and merge\") {\n    val expectedValues = (10 until 50).map(x => (x, s\"val=$x\")) ++\n      (50 until 100).map((_, \"N/A\")) ++ (100 until 150).map((_, \"EXT\"))\n    checkTable(\n      path = goldenTablePath(\"basic-with-inserts-merge\"),\n      expectedAnswer = expectedValues.map(TestRow.fromTuple))\n  }\n\n  test(\"simple end to end with restore\") {\n    checkTable(\n      path = goldenTablePath(\"basic-with-inserts-overwrite-restore\"),\n      expectedAnswer = (0L until 200L).map(TestRow(_)),\n      expectedVersion = Some(3))\n  }\n\n  test(\"end to end only checkpoint files\") {\n    val expectedValues = (5L until 10L) ++ (0L until 20L)\n    checkTable(\n      path = goldenTablePath(\"only-checkpoint-files\"),\n      expectedAnswer = expectedValues.map(TestRow(_)))\n  }\n\n  Seq(\"protocol\", \"metadata\").foreach { action =>\n    test(s\"missing $action should fail\") {\n      val path = goldenTablePath(s\"deltalog-state-reconstruction-without-$action\")\n      val e = intercept[IllegalStateException] {\n        latestSnapshot(path).getSchema()\n      }\n      assert(e.getMessage.contains(s\"No $action found\"))\n    }\n  }\n\n  // TODO missing protocol should fail when missing from checkpoint\n  //   GoldenTable(\"deltalog-state-reconstruction-from-checkpoint-missing-protocol\")\n  //   generation is broken and cannot be regenerated with a non-null schemaString until fixed\n  Seq(\"metadata\" /* , \"protocol\" */ ).foreach { action =>\n    test(s\"missing $action should fail missing from checkpoint\") {\n      val path = goldenTablePath(s\"deltalog-state-reconstruction-from-checkpoint-missing-$action\")\n      val e = intercept[IllegalStateException] {\n        latestSnapshot(path).getSchema()\n      }\n      assert(e.getMessage.contains(s\"No $action found\"))\n    }\n  }\n\n  test(\"fetches the latest protocol and metadata\") {\n    val path = goldenTablePath(\"log-replay-latest-metadata-protocol\")\n    val snapshot = latestSnapshot(path)\n    val scanStateRow = snapshot.getScanBuilder().build()\n      .getScanState(defaultEngine)\n\n    // schema is updated\n    assert(ScanStateRow.getLogicalSchema(scanStateRow)\n      .fieldNames().asScala.toSet == Set(\"col1\", \"col2\"))\n\n    // check protocol version is upgraded\n    val readerVersionOrd = scanStateRow.getSchema().indexOf(\"minReaderVersion\")\n    val writerVersionOrd = scanStateRow.getSchema().indexOf(\"minWriterVersion\")\n    assert(scanStateRow.getInt(readerVersionOrd) == 3 && scanStateRow.getInt(writerVersionOrd) == 7)\n  }\n\n  test(\"standalone DeltaLogSuite: 'checkpoint'\") {\n    val path = goldenTablePath(\"checkpoint\")\n    val snapshot = latestSnapshot(path)\n    assert(snapshot.getVersion() == 14)\n    val scan = snapshot.getScanBuilder().build()\n    assert(collectScanFileRows(scan).length == 1)\n  }\n\n  test(\"standalone DeltaLogSuite: 'snapshot'\") {\n    def getDirDataFiles(tablePath: String): Array[File] = {\n      val correctTablePath =\n        if (tablePath.startsWith(\"file:\")) tablePath.stripPrefix(\"file:\") else tablePath\n      val dir = new File(correctTablePath)\n      dir.listFiles().filter(_.isFile).filter(_.getName.endsWith(\"snappy.parquet\"))\n    }\n\n    def verifySnapshotScanFiles(\n        tablePath: String,\n        expectedFiles: Array[File],\n        expectedVersion: Int): Unit = {\n      val snapshot = latestSnapshot(tablePath)\n      assert(snapshot.getVersion() == expectedVersion)\n      val scanFileRows = collectScanFileRows(\n        snapshot.getScanBuilder().build())\n      assert(scanFileRows.length == expectedFiles.length)\n      val scanFilePaths = scanFileRows\n        .map(InternalScanFileUtils.getAddFileStatus)\n        .map(_.getPath)\n        .map(new File(_).getName) // get the relative path to compare\n      assert(scanFilePaths.toSet == expectedFiles.map(_.getName).toSet)\n    }\n\n    // Append data0\n    var data0_files: Array[File] = Array.empty\n    withGoldenTable(\"snapshot-data0\") { tablePath =>\n      data0_files = getDirDataFiles(tablePath) // data0 files\n      verifySnapshotScanFiles(tablePath, data0_files, 0)\n    }\n\n    // Append data1\n    var data0_data1_files: Array[File] = Array.empty\n    withGoldenTable(\"snapshot-data1\") { tablePath =>\n      data0_data1_files = getDirDataFiles(tablePath) // data0 & data1 files\n      verifySnapshotScanFiles(tablePath, data0_data1_files, 1)\n    }\n\n    // Overwrite with data2\n    var data2_files: Array[File] = Array.empty\n    withGoldenTable(\"snapshot-data2\") { tablePath =>\n      // we have overwritten files for data0 & data1; only data2 files should remain\n      data2_files = getDirDataFiles(tablePath)\n        .filterNot(f => data0_data1_files.exists(_.getName == f.getName))\n      verifySnapshotScanFiles(tablePath, data2_files, 2)\n    }\n\n    // Append data3\n    withGoldenTable(\"snapshot-data3\") { tablePath =>\n      // we have overwritten files for data0 & data1; only data2 & data3 files should remain\n      val data2_data3_files = getDirDataFiles(tablePath)\n        .filterNot(f => data0_data1_files.exists(_.getName == f.getName))\n      verifySnapshotScanFiles(tablePath, data2_data3_files, 3)\n    }\n\n    // Delete data2 files\n    withGoldenTable(\"snapshot-data2-deleted\") { tablePath =>\n      // we have overwritten files for data0 & data1, and deleted data2 files; only data3 files\n      // should remain\n      val data3_files = getDirDataFiles(tablePath)\n        .filterNot(f => data0_data1_files.exists(_.getName == f.getName))\n        .filterNot(f => data2_files.exists(_.getName == f.getName))\n      verifySnapshotScanFiles(tablePath, data3_files, 4)\n    }\n\n    // Repartition into 2 files\n    withGoldenTable(\"snapshot-repartitioned\") { tablePath =>\n      val snapshot = latestSnapshot(tablePath)\n      assert(snapshot.getVersion() == 5)\n      val scanFileRows = collectScanFileRows(\n        snapshot.getScanBuilder().build())\n      assert(scanFileRows.length == 2)\n    }\n\n    // Vacuum\n    withGoldenTable(\"snapshot-vacuumed\") { tablePath =>\n      // all remaining dir data files should be needed for current snapshot version\n      // vacuum doesn't change the snapshot version\n      verifySnapshotScanFiles(tablePath, getDirDataFiles(tablePath), 5)\n    }\n  }\n\n  test(\"DV cases with same path different DV keys\") {\n    val snapshot = latestSnapshot(goldenTablePath(\"log-replay-dv-key-cases\"))\n    val scanFileRows = collectScanFileRows(\n      snapshot.getScanBuilder().build())\n    assert(scanFileRows.length == 1) // there should only be 1 add file\n    val dv = InternalScanFileUtils.getDeletionVectorDescriptorFromRow(scanFileRows.head)\n    assert(dv.getCardinality == 3) // dv cardinality should be 3\n  }\n\n  test(\"special characters in path\") {\n    withGoldenTable(\"log-replay-special-characters-a\") { path =>\n      val snapshot = latestSnapshot(path)\n      val scanFileRows = collectScanFileRows(\n        snapshot.getScanBuilder().build())\n      assert(scanFileRows.isEmpty)\n    }\n    withGoldenTable(\"log-replay-special-characters-b\") { path =>\n      val snapshot = latestSnapshot(path)\n      val scanFileRows = collectScanFileRows(\n        snapshot.getScanBuilder().build())\n      assert(scanFileRows.length == 1)\n      val addFileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRows.head)\n      // get the relative path to compare\n      assert(new File(addFileStatus.getPath).getName == \"special p@#h\")\n    }\n  }\n\n  // TODO we need to canonicalize path during log replay see issue #2213\n  ignore(\"path should be canonicalized - normal characters\") {\n    Seq(\"canonicalized-paths-normal-a\", \"canonicalized-paths-normal-b\").foreach { path =>\n      val snapshot = latestSnapshot(goldenTablePath(path))\n      assert(snapshot.getVersion() == 1)\n      val scanFileRows = collectScanFileRows(snapshot.getScanBuilder().build())\n      assert(scanFileRows.isEmpty)\n    }\n  }\n\n  ignore(\"path should be canonicalized - special characters\") {\n    Seq(\"canonicalized-paths-special-a\", \"canonicalized-paths-special-b\").foreach { path =>\n      val snapshot = latestSnapshot(goldenTablePath(path))\n      assert(snapshot.getVersion() == 1)\n      val scanFileRows = collectScanFileRows(snapshot.getScanBuilder().build())\n      assert(scanFileRows.isEmpty)\n    }\n  }\n\n  // from DeltaDataReaderSuite in standalone\n  test(\"escaped chars sequences in path\") {\n    checkTable(\n      path = goldenTablePath(\"data-reader-escaped-chars\"),\n      expectedAnswer = TestRow(\"foo1\", \"bar+%21\") :: TestRow(\"foo2\", \"bar+%22\") ::\n        TestRow(\"foo3\", \"bar+%23\") :: Nil)\n  }\n\n  test(\"delete and re-add same file in different transactions\") {\n    val path = goldenTablePath(\"delete-re-add-same-file-different-transactions\")\n    val snapshot = latestSnapshot(path)\n    val scan = snapshot.getScanBuilder().build()\n\n    val foundFiles = collectScanFileRows(scan).map(InternalScanFileUtils.getAddFileStatus)\n\n    assert(foundFiles.length == 2)\n    assert(foundFiles.map(_.getPath.split('/').last).toSet == Set(\"foo\", \"bar\"))\n\n    // We added two add files with the same path `foo`. The first should have been removed.\n    // The second should remain, and should have a hard-coded modification time of 1700000000000L\n    assert(\n      foundFiles.find(_.getPath.endsWith(\"foo\")).exists(_.getModificationTime == 1700000000000L))\n  }\n\n  test(\"get the last transaction version for appID\") {\n    val unresolvedPath = goldenTablePath(\"deltalog-getChanges\")\n    val snapshot = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, unresolvedPath)\n    assert(snapshot.isInstanceOf[SnapshotImpl])\n    assert(snapshot.getLatestTransactionVersion(defaultEngine, \"fakeAppId\") === Optional.of(3L))\n    assert(!snapshot.getLatestTransactionVersion(defaultEngine, \"nonExistentAppId\").isPresent)\n  }\n\n  test(\"current checksum read => snapshot provides crc info\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      spark.sql(\n        s\"CREATE TABLE delta.`$tablePath` USING DELTA AS \" +\n          s\"SELECT 0L as id\")\n      spark.sql(\n        s\"INSERT INTO delta.`$tablePath` SELECT 1L as id\")\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, tablePath)\n      assert(snapshot.getCurrentCrcInfo.isPresent)\n      val crcInfo = snapshot.getCurrentCrcInfo.get()\n      assert(crcInfo.getVersion == 1)\n      assert(crcInfo.getProtocol == snapshot.getProtocol)\n      assert(crcInfo.getMetadata == snapshot.getMetadata)\n    }\n  }\n\n  test(\"stale checksum read => snapshot doesn't provides crc info\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      spark.sql(\n        s\"CREATE TABLE delta.`$tablePath` USING DELTA AS \" +\n          s\"SELECT 0L as id\")\n      spark.sql(\n        s\"INSERT INTO delta.`$tablePath` SELECT 1L as id\")\n      deleteChecksumFileForTable(tablePath, versions = Seq(1))\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, tablePath)\n      assert(!snapshot.getCurrentCrcInfo.isPresent)\n    }\n  }\n\n  test(\"no checksum read => snapshot doesn't provides crc info\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      spark.sql(\n        s\"CREATE TABLE delta.`$tablePath` USING DELTA AS \" +\n          s\"SELECT 0L as id\")\n      spark.sql(\n        s\"INSERT INTO delta.`$tablePath` SELECT 1L as id\")\n      deleteChecksumFileForTable(tablePath, versions = Seq(0, 1))\n      val snapshot = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, tablePath)\n      assert(!snapshot.getCurrentCrcInfo.isPresent)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/PaginatedScanSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util.Optional\n\nimport io.delta.kernel.PaginatedScan\nimport io.delta.kernel.PaginatedScanFilesIterator\nimport io.delta.kernel.ScanBuilder\nimport io.delta.kernel.data.FilteredColumnarBatch\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.engine.{DefaultEngine, DefaultJsonHandler, DefaultParquetHandler}\nimport io.delta.kernel.defaults.test.AbstractTableManagerAdapter\nimport io.delta.kernel.defaults.utils.{ExpressionTestUtils, TestUtils, TestUtilsWithTableManagerAPIs, WriteUtils}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.hook.LogCompactionHook\nimport io.delta.kernel.internal.replay.{PageToken, PaginatedScanFilesIteratorImpl}\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.apache.spark.sql.delta.DeltaLog\n\nimport org.apache.spark.sql.catalyst.plans.SQLHelper\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.slf4j.{Logger, LoggerFactory}\n\nclass PaginatedScanSuite extends AnyFunSuite with TestUtilsWithTableManagerAPIs\n    with ExpressionTestUtils with SQLHelper with WriteUtils {\n\n  private val logger = LoggerFactory.getLogger(classOf[PaginatedScanSuite])\n  val tableManager: AbstractTableManagerAdapter = getTableManagerAdapter\n\n  /**\n   * Custom engine with customized batch size. This engine will be used by\n   *  all test cases. This number should not change, and it affects every single test.\n   */\n  private val customEngine: DefaultEngine = {\n    val hadoopConf = new org.apache.hadoop.conf.Configuration()\n    hadoopConf.set(\"delta.kernel.default.json.reader.batch-size\", \"5\")\n    hadoopConf.set(\"delta.kernel.default.parquet.reader.batch-size\", \"5\")\n    DefaultEngine.create(hadoopConf)\n  }\n\n  // TODO: this can be a testUtil?\n  private def getScanBuilder(tablePath: String, tableVersionOpt: Optional[Long]): ScanBuilder = {\n    val snapshot = {\n      if (tableVersionOpt.isPresent) {\n        tableManager.getSnapshotAtVersion(\n          customEngine,\n          tablePath,\n          tableVersionOpt.get())\n      } else {\n        tableManager.getSnapshotAtLatest(customEngine, tablePath)\n      }\n    }\n    snapshot.getScanBuilder()\n  }\n\n  private def createPaginatedScan(\n      tablePath: String,\n      tableVersionOpt: Optional[Long],\n      pageSize: Long,\n      pageTokenOpt: Optional[Row] = Optional.empty()): PaginatedScan = {\n    getScanBuilder(tablePath, tableVersionOpt).buildPaginated(pageSize, pageTokenOpt)\n  }\n\n  case class FirstPageRequestTestContext(\n      pageSize: Int,\n      expScanFilesCnt: Int,\n      expBatchCnt: Int,\n      expLastReadLogFile: String,\n      expLastReadRowIdx: Int)\n\n  private def validateFirstPageResults(\n      batches: Seq[FilteredColumnarBatch],\n      expectedFileCount: Int,\n      expectedBatchCount: Int): Unit = {\n    assert(batches.nonEmpty)\n    val fileCounts: Seq[Long] = batches.map(_.getPreComputedNumSelectedRows.get().toLong)\n    val totalFileCountsReturned = fileCounts.sum\n\n    assert(fileCounts.length == expectedBatchCount)\n    assert(totalFileCountsReturned == expectedFileCount)\n\n    logger.info(s\"Total num batches returned in page one = ${fileCounts.length}\")\n    logger.info(s\"Total num Parquet Files fetched in page one = \" +\n      s\"$totalFileCountsReturned\")\n  }\n\n  private def validateFirstPageToken(\n      pageTokenRow: Row,\n      expectedLogFileName: String,\n      expectedRowIndex: Long): Unit = {\n    val lastReadLogFilePath = PageToken.fromRow(pageTokenRow).getLastReadLogFilePath\n    val lastReturnedRowIndex = PageToken.fromRow(pageTokenRow).getLastReturnedRowIndex\n\n    assert(lastReadLogFilePath.endsWith(expectedLogFileName))\n    assert(lastReturnedRowIndex == expectedRowIndex)\n\n    logger.info(s\"New PageToken: lastReadLogFileName = $lastReadLogFilePath\")\n    logger.info(s\"New PageToken: lastReadRowIndex = $lastReturnedRowIndex\")\n  }\n\n  /**\n   * Executes a single paginated scan request.\n   *\n   * 1. Constructs a paginated scan using the provided page size and page token (optional).\n   * 2. Collects scan results for the current page.\n   * 3. Returns the results along with the next page token.\n   */\n  private def doSinglePageRequest(\n      tablePath: String,\n      tableVersionOpt: Optional[Long],\n      pageTokenOpt: Optional[Row] = Optional.empty(),\n      pageSize: Long): (Optional[Row], Seq[FilteredColumnarBatch]) = {\n    val paginatedScan = createPaginatedScan(\n      tablePath = tablePath,\n      tableVersionOpt = tableVersionOpt,\n      pageSize = pageSize,\n      pageTokenOpt = pageTokenOpt)\n    val paginatedIter = paginatedScan.getScanFiles(customEngine)\n    val returnedBatchesInPage = paginatedIter.toSeq\n    val nextPageToken = paginatedIter.getCurrentPageToken\n\n    assert(returnedBatchesInPage.nonEmpty)\n\n    val fileCounts: Seq[Long] = returnedBatchesInPage.map(_.getPreComputedNumSelectedRows\n      .get().toLong)\n    val totalFileCountsReturned = fileCounts.sum\n\n    logger.info(s\"number of batches = ${returnedBatchesInPage.length}\")\n    logger.info(s\"number of AddFiles = ${totalFileCountsReturned}\")\n\n    if (nextPageToken.isPresent) {\n      val lastReadLogFilePath = PageToken.fromRow(nextPageToken.get).getLastReadLogFilePath\n      val lastReturnedRowIndex = PageToken.fromRow(nextPageToken.get).getLastReturnedRowIndex\n\n      logger.info(s\"New PageToken: lastReadLogFileName = $lastReadLogFilePath\")\n      logger.info(s\"New PageToken: lastReadRowIndex = $lastReturnedRowIndex\")\n    }\n\n    (nextPageToken, returnedBatchesInPage)\n  }\n\n  /**\n   * Simulates the client's behavior of reading a full scan in a paginated manner\n   * with a given page size.\n   *\n   *  The client:\n   * 1. Starts by requesting the first page (no page token).\n   * 2. Receives a page of results along with a page token.\n   * 3. Uses the page token to request the next page.\n   * 4. Repeats until the returned page token is empty, indicating that all data has been consumed.\n   */\n  private def runCompletePaginationTest(\n      testCase: FirstPageRequestTestContext,\n      tablePath: String,\n      tableVersionOpt: Optional[Long] = Optional.empty()): Unit = {\n\n    // ============ Request the first page ==============\n    var (pageTokenOpt, returnedBatchesInPage) = doSinglePageRequest(\n      tablePath = tablePath,\n      tableVersionOpt = tableVersionOpt,\n      pageSize = testCase.pageSize)\n\n    validateFirstPageResults(\n      returnedBatchesInPage,\n      testCase.expScanFilesCnt,\n      testCase.expBatchCnt)\n\n    // When the scan is exhausted, returned page token should be empty.\n    if (pageTokenOpt.isPresent) {\n      validateFirstPageToken(\n        pageTokenOpt.get,\n        testCase.expLastReadLogFile,\n        testCase.expLastReadRowIdx)\n    }\n\n    // ============ Request following pages ==============\n    var allBatchesPaginationScan = returnedBatchesInPage\n    while (pageTokenOpt.isPresent) {\n      val (newPageTokenOpt, newReturnedBatchesInPage) = doSinglePageRequest(\n        tablePath = tablePath,\n        tableVersionOpt = tableVersionOpt,\n        pageTokenOpt = pageTokenOpt,\n        pageSize = testCase.pageSize)\n      pageTokenOpt = newPageTokenOpt\n      allBatchesPaginationScan ++= newReturnedBatchesInPage\n    }\n\n    val normalScan =\n      getScanBuilder(tablePath = tablePath, tableVersionOpt = tableVersionOpt).build()\n\n    val iter = normalScan.getScanFiles(customEngine)\n    val allBatchesNormalScan = iter.toSeq\n\n    // check no duplicate or missing batches in paginated scan\n    assert(allBatchesNormalScan.size == allBatchesPaginationScan.size)\n    for (i <- allBatchesNormalScan.indices) {\n      val normalBatch = allBatchesNormalScan(i)\n      val paginatedBatch = allBatchesPaginationScan(i)\n      assert(normalBatch.getFilePath.equals(paginatedBatch.getFilePath))\n      assert(normalBatch.getData.getSize == paginatedBatch.getData.getSize)\n    }\n  }\n\n  // ==== Test Paginated Iterator Behaviors ======\n  // TODO: test call hasNext() twice\n  test(\"Calling getCurrentPageToken() without calling next() should throw Exception\") {\n    // Request first page\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      // First commit: files 0-4 (5 files)\n      spark.range(0, 50, 1, 5).write.format(\"delta\").save(tablePath)\n\n      val firstPageSize = 2L\n      val firstPaginatedScan = createPaginatedScan(\n        tablePath = tablePath,\n        tableVersionOpt = Optional.empty(),\n        pageSize = firstPageSize)\n      val firstPaginatedIter = firstPaginatedScan.getScanFiles(customEngine)\n\n      // throw exception\n      var e = intercept[IllegalStateException] {\n        firstPaginatedIter.getCurrentPageToken.get\n      }\n      assert(e.getMessage.contains(\"Can't call getCurrentPageToken()\"))\n\n      // throw exception\n      e = intercept[IllegalStateException] {\n        firstPaginatedIter.hasNext\n        firstPaginatedIter.getCurrentPageToken.get\n      }\n      assert(e.getMessage.contains(\"Can't call getCurrentPageToken()\"))\n\n      firstPaginatedIter.close()\n    }\n  }\n\n  test(\"getCurrentPageToken() is impacted only by next() calls, not hasNext() calls\") {\n    // Request first page\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      // First commit: files 0-4 (5 files)\n      spark.range(0, 50, 1, 5).write.format(\"delta\").save(tablePath)\n\n      // Second commit: files 5-9 (5 more files)\n      spark.range(50, 100, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n      // Third commit: files 10-14 (5 more files)\n      spark.range(100, 150, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n      val firstPageSize = 2L\n      val firstPaginatedScan = createPaginatedScan(\n        tablePath = tablePath,\n        tableVersionOpt = Optional.empty(),\n        pageSize = firstPageSize)\n      val firstPaginatedIter = firstPaginatedScan.getScanFiles(customEngine)\n      if (firstPaginatedIter.hasNext) firstPaginatedIter.next()\n      val expectedPageToken = firstPaginatedIter.getCurrentPageToken.get\n\n      firstPaginatedIter.hasNext // call hsaNext() again, should not affect page token\n      val pageToken = firstPaginatedIter.getCurrentPageToken.get\n\n      assert(PageToken.fromRow(pageToken).equals(PageToken.fromRow(expectedPageToken)))\n      firstPaginatedIter.close()\n    }\n  }\n\n  // ===== Data Integrity test cases=====\n  // TODO: test predicate changes\n  /**\n   * Test case to verify pagination behavior when log segment changes between page requests.\n   */\n  test(\"Throw exception when log segment changes between page requests\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      // First commit: files 0-4 (5 files)\n      spark.range(0, 50, 1, 5).write.format(\"delta\").save(tablePath)\n\n      // Second commit: files 5-9 (5 more files)\n      spark.range(50, 100, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n      // Third commit: files 10-14 (5 more files)\n      spark.range(100, 150, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n      // Fourth commit: files 15-19 (5 more files)\n      spark.range(150, 200, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n      // Fifth commit: files 20-24 (5 more files)\n      spark.range(200, 250, 1, 5).write.format(\"delta\").mode(\"append\").save(tablePath)\n\n      // Request first page\n      val firstPageSize = 2L\n      val firstPaginatedScan = createPaginatedScan(\n        tablePath = tablePath,\n        tableVersionOpt = Optional.empty(),\n        pageSize = firstPageSize)\n      val firstPaginatedIter = firstPaginatedScan.getScanFiles(customEngine)\n      if (firstPaginatedIter.hasNext) firstPaginatedIter.next() // call next() once\n      val firstPageToken = firstPaginatedIter.getCurrentPageToken.get\n      firstPaginatedIter.close()\n\n      // Perform log compaction for versions 0-2; log segment should change\n      val dataPath = new Path(s\"file:${tablePath}\")\n      val logPath = new Path(s\"file:${tablePath}\", \"_delta_log\")\n      val compactionHook = new LogCompactionHook(dataPath, logPath, 0, 2, 0)\n      compactionHook.threadSafeInvoke(customEngine)\n      logger.info(\"Log compaction completed for versions 0-2\")\n\n      // Request second page\n      val secondPageSize = 5L\n      val e = intercept[IllegalArgumentException] {\n        createPaginatedScan(\n          tablePath = tablePath,\n          tableVersionOpt = Optional.empty(),\n          pageSize = secondPageSize,\n          pageTokenOpt = Optional.of(firstPageToken))\n      }\n      assert(e.getMessage.contains(\"Invalid page token: token log segment\"))\n    }\n  }\n\n  // ==== Log File Name Variables ======\n  private val JSON_FILE_0 = \"00000000000000000000.json\"\n  private val JSON_FILE_1 = \"00000000000000000001.json\"\n  private val JSON_FILE_2 = \"00000000000000000002.json\"\n  private val JSON_FILE_11 = \"00000000000000000011.json\"\n  private val JSON_FILE_12 = \"00000000000000000012.json\"\n  private val CHECKPOINT_FILE_10 = \"00000000000000000010.checkpoint.parquet\"\n\n  // ===== Single JSON file test cases =====\n  /**\n   *  Log Segment List:\n   *  00000000000000000000.json contains 2 batches, 5 active AddFiles in total\n   *\n   *  Note: batch size is set to 5\n   *  Batch 1: 5 rows, 2 selected AddFiles\n   *  Batch 2: 3 rows, 3 selected AddFiles\n   */\n  Seq(\n    // Kernel is asked to read the 1st page of size 1. Kernel reads the 1st\n    // full batch, so returns 2 AddFiles and ends at the 5th row (index 4).\n    // Note: Kernel should always return full batches, so return full batch one.\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 2,\n      expBatchCnt = 1,\n      expLastReadLogFile = JSON_FILE_0,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 2. Kernel reads the 1st\n    // full batch, so returns 2 AddFiles and ends at the 5th row (index 4)\n    FirstPageRequestTestContext(\n      pageSize = 2,\n      expScanFilesCnt = 2,\n      expBatchCnt = 1,\n      expLastReadLogFile = JSON_FILE_0,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 4. Kernel reads batch 1 and\n    // batch 2 in JSON_FILE_0, so returns 5 AddFiles and ends at the 8th row (index 7)\n    // Note: Kernel should always return full batches, so return full 2 batches.\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 5,\n      expBatchCnt = 2,\n      expLastReadLogFile = JSON_FILE_0,\n      expLastReadRowIdx = 7),\n    // Kernel is asked to read the 1st page of size 5. Kernel reads batch 1 and\n    // batch 2 in JSON_FILE_0, so returns 5 AddFiles and ends at the 8th row (index 7)\n    FirstPageRequestTestContext(\n      pageSize = 5,\n      expScanFilesCnt = 5,\n      expBatchCnt = 2,\n      expLastReadLogFile = JSON_FILE_0,\n      expLastReadRowIdx = 7),\n    // Kernel is asked to read the 1st page of size 20. Kernel reads batch 1 and\n    // batch 2 in JSON_FILE_0, so returns 5 AddFiles and ends at the 8th row (index 7)\n    // Note: page size won't be reached because there is only 5 data files in total.\n    FirstPageRequestTestContext(\n      pageSize = 20,\n      expScanFilesCnt = 5,\n      expBatchCnt = 2,\n      expLastReadLogFile = JSON_FILE_0,\n      expLastReadRowIdx = 7)).foreach { testCase =>\n    test(s\"Single JSON file - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tablePath = getTestResourceFilePath(\"kernel-pagination-all-jsons\"),\n        tableVersionOpt = Optional.of(0L))\n    }\n  }\n\n  // ===== Multiple JSON files test cases =====\n  /**\n   * Log Segment List:\n   * 00000000000000000000.json : 8 rows (5 AddFile row + 3 non-AddFile rows)\n   * 00000000000000000001.json : 6 rows (5 AddFile row + 1 non-AddFile row)\n   * 00000000000000000002.json : 6 rows (5 AddFile row + 1 non-AddFile row)\n   *\n   * Note: batch size is set to 5\n   * 00000000000000000002.json contains 2 batches, 5 active AddFiles in total\n   * Batch 1: 5 rows, 4 selected AddFiles\n   * Batch 2: 1 rows, 1 selected AddFiles\n   *\n   * 00000000000000000001.json contains 2 batches, 5 active AddFiles in total\n   * Batch 1: 5 rows, 4 selected AddFiles\n   * Batch 2: 1 rows, 1 selected AddFiles\n   *\n   * 00000000000000000000.json contains 2 batches, 5 active AddFiles in total\n   * Batch 1: 5 rows, 2 selected AddFiles\n   * Batch 2: 3 rows, 3 selected AddFiles\n   */\n\n  Seq(\n    // Kernel is asked to read the 1st page of size 1. Kernel reads batch 1 in JSON_FILE_2,\n    // so returns 4 AddFiles and ends at the 5th row (index 4) in JSON_FILE_2.\n    // Note: Kernel should return full batches, so return full one batch (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 4,\n      expBatchCnt = 1,\n      expLastReadLogFile = JSON_FILE_2,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 4. Kernel reads batch 1 in JSON_FILE_2,\n    // so returns 4 AddFiles and ends at the 5th row (index 4) in JSON_FILE_2.\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 4,\n      expBatchCnt = 1,\n      expLastReadLogFile = JSON_FILE_2,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 5. Kernel reads batch 1 and 2 in JSON_FILE_2,\n    // so returns 5 AddFiles and ends at the 6th row (index 5) in JSON_FILE_2.\n    FirstPageRequestTestContext(\n      pageSize = 5,\n      expScanFilesCnt = 5,\n      expBatchCnt = 2,\n      expLastReadLogFile = JSON_FILE_2,\n      expLastReadRowIdx = 5),\n    // Kernel is asked to read the 1st page of size 7. Kernel reads all batches in JSON_FILE_2,\n    // batch 1 in JSON_FILE_1, so returns 9 AddFiles and ends at the 5th row (index 4)\n    // in JSON_FILE_1.\n    // Note: Kernel should return full batches, so return 3 full batches (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 7,\n      expScanFilesCnt = 9,\n      expBatchCnt = 3,\n      expLastReadLogFile = JSON_FILE_1,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 9. Kernel reads all batches in JSON_FILE_2,\n    // batch 1 in JSON_FILE_1, so returns 9 AddFiles and ends at the 5th row (index 4)\n    // in JSON_FILE_1.\n    FirstPageRequestTestContext(\n      pageSize = 9,\n      expScanFilesCnt = 9,\n      expBatchCnt = 3,\n      expLastReadLogFile = JSON_FILE_1,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 18. Kernel reads all batches in JSON_FILE_2,\n    // JSON_FILE_1 and SON_FILE_0, so returns 15 AddFiles and ends at the last row (index 7)\n    // in JSON_FILE_0.\n    // Note: page size won't be reached because there are 15 data files in total.\n    FirstPageRequestTestContext(\n      pageSize = 18,\n      expScanFilesCnt = 15,\n      expBatchCnt = 6,\n      expLastReadLogFile = JSON_FILE_0,\n      expLastReadRowIdx = 7)).foreach { testCase =>\n    test(s\"Multiple JSON files - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tablePath = getTestResourceFilePath(\"kernel-pagination-all-jsons\"))\n    }\n  }\n\n  // ===== Single checkpoint file test cases =====\n  /**\n   * Log Segment List:\n   * 00000000000000000010.checkpoint.parquet contains 5 batches, 22 active AddFiles, 24 rows\n   *\n   * Note: batch size is set to 5\n   *\n   * Batch 1: 5 rows, 5 selected AddFiles\n   * Batch 2: 5 rows, 5 selected AddFiles\n   * Batch 3: 5 rows, 5 selected AddFiles\n   * Batch 4: 5 rows, 3 selected AddFiles\n   * Batch 5: 4 rows, 4 selected AddFiles\n   */\n  Seq(\n    // Kernel is asked to read the 1st page of size 1. Kernel reads batch 1 in 10.checkpoint,\n    // so returns 5 AddFiles and ends at the 5th row (index 4) in 10.checkpoint.\n    // Note: Kernel should return full batches, so return one full batch (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 5,\n      expBatchCnt = 1,\n      expLastReadLogFile = CHECKPOINT_FILE_10,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 10. Kernel reads 2 batches in 10.checkpoint,\n    // so returns 10 AddFiles and ends at the 10th row (index 9) in 10.checkpoint.\n    FirstPageRequestTestContext(\n      pageSize = 10,\n      expScanFilesCnt = 10,\n      expBatchCnt = 2,\n      expLastReadLogFile = CHECKPOINT_FILE_10,\n      expLastReadRowIdx = 9),\n    // Kernel is asked to read the 1st page of size 12. Kernel reads 3 batches in 10.checkpoint,\n    // so returns 15 AddFiles and ends at the 15th row (index 14) in 10.checkpoint.\n    // Note: Kernel should return full batches, so return 3 full batches (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 12,\n      expScanFilesCnt = 15,\n      expBatchCnt = 3,\n      expLastReadLogFile = CHECKPOINT_FILE_10,\n      expLastReadRowIdx = 14),\n    // Kernel is asked to read the 1st page of size 100. Kernel reads all 5 batches\n    // in 10.checkpoint, so returns 22 AddFiles and ends at the 24th row (index 23)\n    // in 10.checkpoint. Note: page size won't be reached in this test case.\n    FirstPageRequestTestContext(\n      pageSize = 100,\n      expScanFilesCnt = 22,\n      expBatchCnt = 5,\n      expLastReadLogFile = CHECKPOINT_FILE_10,\n      expLastReadRowIdx = 23)).foreach { testCase =>\n    test(s\"Single checkpoint file - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tableVersionOpt = Optional.of(10L),\n        tablePath = getTestResourceFilePath(\"kernel-pagination-single-checkpoint\"))\n    }\n  }\n\n  // ===== Single checkpoint file and multiple JSON files test cases =====\n  /**\n   * Log segment list:\n   * 00000000000000000010.checkpoint.parquet\n   * 00000000000000000011.json\n   * 00000000000000000012.json\n   *\n   * Note: batch size is set to 5\n   *\n   * 00000000000000000012.json contains 1 batch, 2 active AddFiles in total\n   * Batch 1: 3 rows, 2 selected AddFiles\n   *\n   * 00000000000000000011.json contains 1 batch, 2 active AddFiles in total\n   * Batch 1: 3 rows, 2 selected AddFiles\n   *\n   * 00000000000000000010.checkpoint.parquet contains 5 batches, 22 active AddFiles, 24 rows\n   * Batch 1: 5 rows, 5 selected AddFiles\n   * Batch 2: 5 rows, 5 selected AddFiles\n   * Batch 3: 5 rows, 5 selected AddFiles\n   * Batch 4: 5 rows, 3 selected AddFiles\n   * Batch 5: 4 rows, 4 selected AddFiles\n   */\n  Seq(\n    // Kernel is asked to read the 1st page of size 1. Kernel reads 1 batches in 12.json,\n    // so returns 2 AddFiles and ends at the 3rd row (index 2) in 10.checkpoint.\n    // Note: Kernel should return full batches, so return one full batch (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 2,\n      expBatchCnt = 1,\n      expLastReadLogFile = JSON_FILE_12,\n      expLastReadRowIdx = 2),\n    // Kernel is asked to read the 1st page of size 2. Kernel reads 1 batches in 12.json,\n    // so returns 2 AddFiles and ends at the 3rd row (index 2) in 10.checkpoint.\n    FirstPageRequestTestContext(\n      pageSize = 2,\n      expScanFilesCnt = 2,\n      expBatchCnt = 1,\n      expLastReadLogFile = JSON_FILE_12,\n      expLastReadRowIdx = 2),\n    // Kernel is asked to read the 1st page of size 1. Kernel reads one batch in 12.json,\n    // and one batch in 11.json, so returns 4 AddFiles and ends at the 3rd row (index 2) in 11.json.\n    // Note: Kernel should return full batches, so return 2 full batches (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 3,\n      expScanFilesCnt = 4,\n      expBatchCnt = 2,\n      expLastReadLogFile = JSON_FILE_11,\n      expLastReadRowIdx = 2),\n    // Kernel is asked to read the 1st page of size 4. Kernel reads one batch in 12.json,\n    // and one batch in 11.json, so returns 4 AddFiles and ends at the 3rd row (index 2) in 11.json.\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 4,\n      expBatchCnt = 2,\n      expLastReadLogFile = JSON_FILE_11,\n      expLastReadRowIdx = 2),\n    // Kernel is asked to read the 1st page of size 8. Kernel reads one batch in 12.json,\n    // one batch in 11.json, and one batch in 10.checkpoint, so returns 9 AddFiles and\n    // ends at the 5th row (index 4) in 10.checkpoint.\n    // Note: Kernel should return full batches, so return 3 full batches (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 8,\n      expScanFilesCnt = 9,\n      expBatchCnt = 3,\n      expLastReadLogFile = CHECKPOINT_FILE_10,\n      expLastReadRowIdx = 4),\n    // Kernel is asked to read the 1st page of size 18. Kernel reads one batch in 12.json,\n    // one batch in 11.json, and 3 batches in 10.checkpoint, so returns 19 AddFiles and\n    // ends at the 15th row (index 14) in 10.checkpoint.\n    // Note: Kernel should return full batches, so return 5 full batches (and go over page limit).\n    FirstPageRequestTestContext(\n      pageSize = 18,\n      expScanFilesCnt = 19,\n      expBatchCnt = 5,\n      expLastReadLogFile = CHECKPOINT_FILE_10,\n      expLastReadRowIdx = 14)).foreach { testCase =>\n    test(s\"Single checkpoint and JSON files - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tablePath = getTestResourceFilePath(\"kernel-pagination-single-checkpoint\"))\n    }\n  }\n\n  // ===== Multi-part checkpoint files test cases =====\n  /**\n   * Log Segment List:\n   * 00000000000000000000.checkpoint.0000000003.0000000003.parquet (6 AddFile)\n   * Batch A: 5 AddFile, 5 rows\n   * Batch B: 1 AddFile, 1 row\n   * 00000000000000000000.checkpoint.0000000002.0000000003.parquet (7 AddFile)\n   * Batch C: 5 AddFile, 5 row\n   * Batch D: 2 AddFile, 2 row\n   * 00000000000000000000.checkpoint.0000000001.0000000003.parquet (5 AddFile)\n   * Batch E: 3 AddFile, 5 row\n   * Batch F: 2 AddFile, 2 row\n   */\n  val MULTI_CHECKPOINT_FILE_0_1 = \"00000000000000000000.checkpoint.0000000001.0000000003.parquet\"\n  val MULTI_CHECKPOINT_FILE_0_2 = \"00000000000000000000.checkpoint.0000000002.0000000003.parquet\"\n  val MULTI_CHECKPOINT_FILE_0_3 = \"00000000000000000000.checkpoint.0000000003.0000000003.parquet\"\n\n  // Note: Kernel should return full batches (and may go over page limit).\n  Seq(\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 5, /* 5 AddFiles in Batch A */\n      expBatchCnt = 1, /* return only batch A */\n      expLastReadLogFile = MULTI_CHECKPOINT_FILE_0_3, /* Batch A from checkpoint file 3 */\n      expLastReadRowIdx = 4 /* Last Row index in Batch A is 4 in checkpoint file 3 */ ),\n    FirstPageRequestTestContext(\n      pageSize = 7,\n      expScanFilesCnt = 11, /* 5 (batch A) + 1 (batch B) + 5 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = MULTI_CHECKPOINT_FILE_0_2, /* Batch C from checkpoint file 2 */\n      expLastReadRowIdx = 4 /* Last Row index in Batch C is 4 in checkpoint file 2 */\n    ),\n    FirstPageRequestTestContext(\n      pageSize = 13,\n      expScanFilesCnt = 13, /* 5 (batch A) + 1 (batch B) + 5 (batch C) + 2 (batch D) */\n      expBatchCnt = 4, /* return batch A, B, C, D */\n      expLastReadLogFile = MULTI_CHECKPOINT_FILE_0_2, /* Batch D from checkpoint file 2 */\n      expLastReadRowIdx = 6 /* Last Row index in Batch D is 6 in checkpoint file 3 */\n    ),\n    FirstPageRequestTestContext(\n      pageSize = 100,\n      expScanFilesCnt = 18, /* 5 (batch A) + 1 (batch B) + 5 (C) + 2 (D) + 3 (E) + 2 F) */\n      expBatchCnt = 6, /* return batch A, B, C, D, E, F */\n      expLastReadLogFile = MULTI_CHECKPOINT_FILE_0_1,\n      expLastReadRowIdx = 1)).foreach { testCase =>\n    test(s\"Multi-part checkpoints - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tableVersionOpt = Optional.of(0L),\n        tablePath = getTestResourceFilePath(\"kernel-pagination-multi-part-checkpoints\"))\n    }\n  }\n\n  // ===== Multi-part checkpoint files with JSON files test cases =====\n  /**\n   * Log Segment List:\n   * 00000000000000000002.json (1 AddFile)\n   * Batch A: 1 AddFile, 2 rows\n   * 00000000000000000001.json (1 AddFile)\n   * Batch B: 1 AddFile, 2 rows\n   * 00000000000000000000.checkpoint.0000000003.0000000003.parquet (6 AddFile)\n   * Batch C: 5 AddFile, 5 rows\n   * Batch D: 1 AddFile, 1 row\n   * 00000000000000000000.checkpoint.0000000002.0000000003.parquet (7 AddFile)\n   * Batch E: 5 AddFile, 5 row\n   * Batch F: 2 AddFile, 2 row\n   * 00000000000000000000.checkpoint.0000000001.0000000003.parquet (5 AddFile)\n   * Batch G: 3 AddFile, 5 row\n   * Batch H: 2 AddFile, 2 row\n   */\n  Seq(\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 1, /* 1 AddFile in Batch A */\n      expBatchCnt = 1, /* return only batch A */\n      expLastReadLogFile = JSON_FILE_2, /* Batch A from JSON file 2 */\n      expLastReadRowIdx = 1\n    ), /* Last Row index in Batch A is 1 in JSON file 2 */\n    FirstPageRequestTestContext(\n      pageSize = 2,\n      expScanFilesCnt = 2, /* 1 (batch A) + 1 (batch B) */\n      expBatchCnt = 2, /* return batch A, B */\n      expLastReadLogFile = JSON_FILE_1, /* Batch B from JSON file 1 */\n      expLastReadRowIdx = 1\n    ), /* Last Row index in Batch B is 1 in JSON file 1 */\n    FirstPageRequestTestContext(\n      pageSize = 6,\n      expScanFilesCnt = 7, /* 1 (batch A) + 1 (batch B) + 5 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = MULTI_CHECKPOINT_FILE_0_3, /* Batch C from checkpoint file 3 */\n      expLastReadRowIdx = 4\n    ), /* Last Row index in Batch C is 4 in checkpoint file 3 */\n    FirstPageRequestTestContext(\n      pageSize = 100,\n      expScanFilesCnt =\n        20, /* 1 (batch A) + 1 (batch B) + 6 (batch C+D) + 7 (batch E+F) + 5 (batch G+H) */\n      expBatchCnt = 8, /* return all batches A through H */\n      expLastReadLogFile = MULTI_CHECKPOINT_FILE_0_1, /* Batch H from checkpoint file 1 */\n      expLastReadRowIdx = 1\n    ) /* Last Row index in Batch H is 1 in checkpoint file 1 */\n  ).foreach { testCase =>\n    test(s\"Multi-part checkpoints with jsons - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tablePath = getTestResourceFilePath(\"kernel-pagination-multi-part-checkpoints\"))\n    }\n  }\n\n  // ===== V2 parquet checkpoint with sidecar files test cases =====\n  /**\n   * 2.checkpoint.uuid.parquet:\n   * - Batch A: 5 rows, 0 selected AddFiles\n   * sidecar 1:\n   * - Batch B: 3 rows, 3 selected AddFiles\n   * sidecar 2:\n   * - Batch C: 1 row, 1 selected AddFiles\n   */\n\n  val PARQUET_MANIFEST_SIDECAR_1 =\n    \"00000000000000000002.checkpoint.0000000001.0000000002.\" +\n      \"055454d8-329c-4e0e-864d-7f867075af33.parquet\"\n  val PARQUET_MANIFEST_SIDECAR_2 =\n    \"00000000000000000002.checkpoint.0000000002.0000000002.\" +\n      \"33321cc1-9c55-4d1f-8511-fafe6d2e1133.parquet\"\n\n  Seq(\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 3, /* 0 (batch A) + 3 (batch B) */\n      expBatchCnt = 2, /* return batch A, B */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_1, /* Batch B from sidecar 1 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch B is 2 in sidecar 1 */\n    FirstPageRequestTestContext(\n      pageSize = 3,\n      expScanFilesCnt = 3, /* 0 (batch A) + 3 (batch B) */\n      expBatchCnt = 2, /* return batch A, B */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_1, /* Batch B from sidecar 1 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch B is 2 in sidecar 1 */\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 4, /* 0 (batch A) + 3 (batch B) + 1 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_2, /* Batch C from sidecar 2 */\n      expLastReadRowIdx = 0\n    ), /* Last Row index in Batch C is 0 in sidecar 2 */\n    FirstPageRequestTestContext(\n      pageSize = 10000,\n      expScanFilesCnt = 4, /* 0 (batch A) + 3 (batch B) + 1 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_2, /* Batch C from sidecar 2 */\n      expLastReadRowIdx = 0\n    ) /* Last Row index in Batch C is 0 in sidecar 2 */\n  ).foreach { testCase =>\n    test(s\"V2 parquet checkpoints (and sidecars) - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tableVersionOpt = Optional.of(2L),\n        tablePath = getTestResourceFilePath(\"kernel-pagination-v2-checkpoint-parquet\"))\n    }\n  }\n\n  // ===== V2 parquet checkpoint with sidecar files with jsons test cases =====\n  /**\n   * 00000000000000000003.json\n   *  - Batch A: 2 rows, 1 selected AddFiles\n   * 2.checkpoint.uuid.parquet:\n   *  - Batch B: 5 rows, 0 selected AddFiles\n   * sidecar 1:\n   *  - Batch C: 3 rows, 3 selected AddFiles\n   * sidecar 2:\n   *  - Batch D: 1 row, 1 selected AddFiles\n   */\n  val JSON_FILE_3 = \"00000000000000000003.json\"\n  Seq(\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 1, /* 1 AddFile in Batch A */\n      expBatchCnt = 1, /* return only batch A */\n      expLastReadLogFile = JSON_FILE_3, /* Batch A from JSON file 3 */\n      expLastReadRowIdx = 1\n    ), /* Last Row index in Batch A is 1 in JSON file 3 */\n    FirstPageRequestTestContext(\n      pageSize = 2,\n      expScanFilesCnt = 4, /* 1 (batch A) + 0 (batch B) + 3 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_1, /* Batch C from sidecar 1 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch C is 2 in sidecar 1 */\n    FirstPageRequestTestContext(\n      pageSize = 3,\n      expScanFilesCnt = 4, /* 1 (batch A) + 0 (batch B) + 3 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_1, /* Batch C from sidecar 1 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch C is 2 in sidecar 1 */\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 4, /* 1 (batch A) + 0 (batch B) + 3 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_1, /* Batch C from sidecar 1 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch C is 2 in sidecar 1 */\n    FirstPageRequestTestContext(\n      pageSize = 10000,\n      expScanFilesCnt = 5, /* 1 (batch A) + 0 (batch B) + 3 (batch C) + 1 (batch D) */\n      expBatchCnt = 4, /* return batch A, B, C, D */\n      expLastReadLogFile = PARQUET_MANIFEST_SIDECAR_2, /* Batch D from sidecar 2 */\n      expLastReadRowIdx = 0\n    ) /* Last Row index in Batch D is 0 in sidecar 2 */\n  ).foreach { testCase =>\n    test(\n      s\"v2 parquet checkpoints (and sidecars) \" +\n        s\"with json delta commit files - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tableVersionOpt = Optional.of(3L),\n        tablePath = getTestResourceFilePath(\"kernel-pagination-v2-checkpoint-parquet\"))\n    }\n  }\n\n  // ===== V2 json checkpoint with sidecar files test cases =====\n  /**\n   * 2.checkpoint.uuid.json:\n   *  - Batch A: 5 rows, 0 selected AddFiles\n   * sidecar 1:\n   *  - Batch B: 1 rows, 1 selected AddFiles\n   * sidecar 2:\n   *  - Batch C: 3 row, 3 selected AddFiles\n   */\n  val JSON_MANIFEST = \"00000000000000000002.checkpoint.\" +\n    \"6374b053-df23-479b-b2cf-c9c550132b49.json\"\n  val JSON_MANIFEST_SIDECAR_1 =\n    \"00000000000000000002.checkpoint.0000000001.0000000002.\" +\n      \"bd1885fd-6ec0-4370-b0f5-43b5162fd4de.parquet\"\n  val JSON_MANIFEST_SIDECAR_2 =\n    \"00000000000000000002.checkpoint.0000000002.0000000002.\" +\n      \"0a8d73ee-aa83-49d0-9583-c99db75b89b2.parquet\"\n\n  Seq(\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 1, /* 0 (batch A) + 1 (batch B) */\n      expBatchCnt = 2, /* return batch A, B */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_1, /* Batch B from sidecar 1 */\n      expLastReadRowIdx = 0\n    ), /* Last Row index in Batch B is 0 in sidecar 1 */\n    FirstPageRequestTestContext(\n      pageSize = 2,\n      expScanFilesCnt = 4, /* 0 (batch A) + 1 (batch B) + 3 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_2, /* Batch C from sidecar 2 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch C is 2 in sidecar 2 */\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 4, /* 0 (batch A) + 1 (batch B) + 3 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_2, /* Batch C from sidecar 2 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch C is 2 in sidecar 2 */\n    FirstPageRequestTestContext(\n      pageSize = 10000,\n      expScanFilesCnt = 4, /* 0 (batch A) + 1 (batch B) + 3 (batch C) */\n      expBatchCnt = 3, /* return batch A, B, C */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_2, /* Batch C from sidecar 2 */\n      expLastReadRowIdx = 0\n    ) /* Last Row index in Batch C is 0 in sidecar 2 */\n  ).foreach { testCase =>\n    test(s\"v2 json checkpoint (and sidecars) - page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tableVersionOpt = Optional.of(2L),\n        tablePath = getTestResourceFilePath(\"kernel-pagination-v2-checkpoint-json\"))\n    }\n  }\n\n  // ===== V2 json checkpoint with sidecar files with jsons test cases =====\n  /**\n   * 00000000000000000003.json\n   *  - Batch A: 2 rows, 1 selected AddFiles\n   * 2.checkpoint.uuid.json:\n   *  - Batch B: 5 rows, 0 selected AddFiles\n   * sidecar 1:\n   *  - Batch C: 1 rows, 1 selected AddFiles\n   * sidecar 2:\n   *  - Batch D: 3 row, 3 selected AddFiles\n   */\n  Seq(\n    FirstPageRequestTestContext(\n      pageSize = 1,\n      expScanFilesCnt = 1, /* 1 AddFile in Batch A */\n      expBatchCnt = 1, /* return only batch A */\n      expLastReadLogFile = JSON_FILE_3, /* Batch A from JSON file 3 */\n      expLastReadRowIdx = 1\n    ), /* Last Row index in Batch A is 1 in JSON file 3 */\n    FirstPageRequestTestContext(\n      pageSize = 3,\n      expScanFilesCnt = 5, /* 1 (batch A) + 0 (batch B) + 1 (batch C) + 3 (batch D) */\n      expBatchCnt = 4, /* return batch A, B, C, D */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_2, /* Batch D from sidecar 2 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch D is 2 in sidecar 2 */\n    FirstPageRequestTestContext(\n      pageSize = 4,\n      expScanFilesCnt = 5, /* 1 (batch A) + 0 (batch B) + 1 (batch C) + 3 (batch D) */\n      expBatchCnt = 4, /* return batch A, B, C, D */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_2, /* Batch D from sidecar 2 */\n      expLastReadRowIdx = 2\n    ), /* Last Row index in Batch D is 2 in sidecar 2 */\n    FirstPageRequestTestContext(\n      pageSize = 10000,\n      expScanFilesCnt = 5, /* 1 (batch A) + 0 (batch B) + 1 (batch C) + 3 (batch D) */\n      expBatchCnt = 4, /* return batch A, B, C, D */\n      expLastReadLogFile = JSON_MANIFEST_SIDECAR_2, /* Batch D from sidecar 2 */\n      expLastReadRowIdx = 2\n    ) /* Last Row index in Batch D is 2 in sidecar 2 */\n  ).foreach { testCase =>\n    test(s\"v2 json checkpoint files (and sidecars) with json delta commit files - \" +\n      s\"page size ${testCase.pageSize}\") {\n      runCompletePaginationTest(\n        testCase = testCase,\n        tableVersionOpt = Optional.of(3L),\n        tablePath = getTestResourceFilePath(\"kernel-pagination-v2-checkpoint-json\"))\n    }\n  }\n  // TODO: tests for log compaction files\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/PartitionPruningSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.math.{BigDecimal => BigDecimalJ}\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.defaults.utils.{ExpressionTestUtils, TestRow, TestUtils}\nimport io.delta.kernel.expressions.{Column, Literal, Predicate}\nimport io.delta.kernel.expressions.Literal._\nimport io.delta.kernel.types._\nimport io.delta.kernel.types.TimestampNTZType.TIMESTAMP_NTZ\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PartitionPruningSuite extends AnyFunSuite with TestUtils with ExpressionTestUtils {\n\n  // scalastyle:off sparkimplicits\n  // scalastyle:on sparkimplicits\n\n  // Test golden table containing partition columns of all simple types\n  val allTypesPartitionTable = goldenTablePath(\"data-reader-partition-values\")\n\n  // Test case to verify pruning on each partition column type works.\n  // format: partition column reference -> (nonNullPartitionValues, nullPartitionValue)\n  val testCasesAllTypes = Map(\n    col(\"as_boolean\") -> (ofBoolean(false), ofNull(BooleanType.BOOLEAN)),\n    col(\"as_byte\") -> (ofByte(1), ofNull(ByteType.BYTE)),\n    col(\"as_short\") -> (ofShort(1), ofNull(ShortType.SHORT)),\n    col(\"as_int\") -> (ofInt(1), ofNull(IntegerType.INTEGER)),\n    col(\"as_long\") -> (ofLong(1), ofNull(LongType.LONG)),\n    col(\"as_float\") -> (ofFloat(1), ofNull(FloatType.FLOAT)),\n    col(\"as_double\") -> (ofDouble(1), ofNull(DoubleType.DOUBLE)),\n    // 2021-09-08 in days since epoch 18878\n    col(\"as_date\") -> (ofDate(18878 /* daysSinceEpochUTC */ ), ofNull(DateType.DATE)),\n    col(\"as_string\") -> (ofString(\"1\"), ofNull(StringType.STRING)),\n    // 2021-09-08 11:11:11 in micros since epoch UTC\n    col(\"as_timestamp\") -> (ofTimestamp(1631099471000000L), ofNull(TimestampType.TIMESTAMP)),\n    col(\"as_big_decimal\") -> (\n      ofDecimal(new BigDecimalJ(1), 1, 0),\n      ofNull(new DecimalType(1, 0))))\n\n  // Test for each partition column data type with partition value equal to non-null and null each\n  // Try with or without selecting the partition column that has the predicate\n  testCasesAllTypes.foreach {\n    case (partitionCol, (nonNullLiteral, nullLiteral)) =>\n      Seq(nonNullLiteral, nullLiteral).foreach { literal =>\n        Seq(true, false).foreach { selectPredicatePartitionCol =>\n          test(s\"partition pruning: simple filter `$partitionCol = $literal`, \" +\n            s\"select partition predicate column = $selectPredicatePartitionCol\") {\n\n            val isPartitionColDateOrTimestampType = literal.getDataType.isInstanceOf[DateType] ||\n              literal.getDataType.isInstanceOf[TimestampType]\n\n            val filter = predicate(\"=\", partitionCol, literal)\n            val expectedResult = if (literal.getValue == null) {\n              Seq.empty // part1 == null should always return false - that means no results\n            } else {\n              if (selectPredicatePartitionCol) {\n                if (isPartitionColDateOrTimestampType) {\n                  // Date and timestamp type has two partitions with the same value in golden table\n                  Seq((literal.getValue, 0L, \"0\"), (literal.getValue, 1L, \"1\"))\n                } else {\n                  Seq((literal.getValue, 1L, \"1\"))\n                }\n              } else {\n                if (isPartitionColDateOrTimestampType) {\n                  // Date and timestamp type has two partitions with the same value in golden table\n                  Seq((0L, \"0\"), (1L, \"1\"))\n                } else {\n                  Seq((1L, \"1\"))\n                }\n              }\n            }\n\n            // \"value\" is a non-partition column\n            val selectedColumns = if (selectPredicatePartitionCol) {\n              Seq(partColName(partitionCol), \"as_long\", \"value\")\n            } else {\n              Seq(\"as_long\", \"value\")\n            }\n\n            checkTable(\n              path = allTypesPartitionTable,\n              expectedAnswer = expectedResult.map(TestRow.fromTuple(_)),\n              readCols = selectedColumns,\n              filter = filter,\n              expectedRemainingFilter = null)\n          }\n        }\n      }\n  }\n\n  // Various combinations of predicate mix on partition and/or data columns mixes with AND or OR\n  // test case format: (test_name, predicate) -> (remainingPredicate, expectedResults)\n  // expected results is for query selecting `as_date` (partition column) and `value` (data column)\n  val combinationTestCases = Map(\n    (\n      \"partition pruning: with predicate on two different partition col combined with AND\",\n      and(\n        predicate(\">=\", col(\"as_float\"), ofFloat(-200)),\n        predicate(\"=\", col(\"as_date\"), ofDate(18878 /* daysSinceEpochUTC */ )))) -> (\n      null,\n      Seq((18878, \"0\"), (18878, \"1\"))),\n    (\n      \"partition pruning: with predicate on two different partition col combined with OR\",\n      or(\n        predicate(\"=\", col(\"as_float\"), ofFloat(0)),\n        predicate(\"=\", col(\"as_int\"), ofInt(1)))) -> (null, Seq((18878, \"0\"), (18878, \"1\"))),\n    (\n      \"partition pruning: with predicate on data and partition column mix with AND\",\n      and(\n        predicate(\"=\", col(\"as_value\"), ofString(\"1\")), // data col filter\n        predicate(\"=\", col(\"as_float\"), ofFloat(0)) // partition col filter\n      )) -> (\n      predicate(\"=\", col(\"as_value\"), ofString(\"1\")),\n      Seq((18878, \"0\"))),\n    (\n      \"partition pruning: with predicate on data and partition column mix with OR\",\n      or(\n        predicate(\"=\", col(\"as_value\"), ofString(\"1\")), // data col filter\n        predicate(\"=\", col(\"as_float\"), ofFloat(0)) // partition col filter\n      )) -> (\n      or(\n        predicate(\"=\", col(\"as_value\"), ofString(\"1\")), // data col filter\n        predicate(\"=\", col(\"as_float\"), ofFloat(0)) // partition col filter\n      ),\n      Seq((18878, \"0\"), (18878, \"1\"), (null, \"2\"))),\n    (\n      \"partition pruning: partition predicate prunes everything\",\n      and(\n        predicate(\"=\", col(\"as_value\"), ofString(\"200\")), // data col filter\n        predicate(\"=\", col(\"as_float\"), ofFloat(234)) // partition col filter\n      )) -> (\n      predicate(\"=\", col(\"as_value\"), ofString(\"200\")),\n      Seq()))\n\n  combinationTestCases.foreach {\n    case ((testTag, predicate), (expRemainingFilter, expResults)) =>\n      test(testTag) {\n        checkTable(\n          path = allTypesPartitionTable,\n          expectedAnswer = expResults.map(TestRow.fromTuple(_)),\n          readCols = Seq(\"as_date\", \"value\"),\n          filter = predicate,\n          expectedRemainingFilter = expRemainingFilter)\n      }\n  }\n\n  Seq(\"name\", \"id\").foreach { mode =>\n    test(s\"partition pruning on a column mapping enabled table: mode = $mode\") {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getCanonicalPath\n        spark.sql(\n          s\"\"\"CREATE TABLE delta.`$tablePath`(c1 long, c2 STRING, p1 STRING, p2 LONG)\n             | USING delta PARTITIONED BY (p1, p2)\n             | TBLPROPERTIES(\n             |'delta.columnMapping.mode' = '$mode',\n             |'delta.minReaderVersion' = '2',\n             |'delta.minWriterVersion' = '5')\n             |\"\"\".stripMargin)\n        Seq.range(0, 5).foreach { i =>\n          spark.sql(s\"insert into delta.`$tablePath` values ($i, '$i', '$i', $i)\")\n        }\n\n        checkTable(\n          tablePath,\n          expectedAnswer = Seq((3L, \"3\"), (4L, \"4\")).map(TestRow.fromTuple(_)),\n          readCols = Seq(\"p2\", \"c2\"),\n          filter = predicate(\">=\", col(\"p2\"), ofLong(3)),\n          expectedRemainingFilter = null)\n      }\n    }\n  }\n\n  Seq(\"\", \"-name-mode\", \"-id-mode\").foreach { cmMode =>\n    // Below is the golden table used in test\n    // (INTEGER id, TIMESTAMP_NTZ tsNtz, TIMESTAMP_NTZ tsNtzPartition)\n    // (0, '2021-11-18 02:30:00.123456','2021-11-18 02:30:00.123456'),\n    // (1, '2013-07-05 17:01:00.123456','2021-11-18 02:30:00.123456'),\n    // (2, NULL,                         '2021-11-18 02:30:00.123456'),\n    // (3, '2021-11-18 02:30:00.123456','2013-07-05 17:01:00.123456'),\n    // (4, '2013-07-05 17:01:00.123456','2013-07-05 17:01:00.123456'),\n    // (5, NULL,                        '2013-07-05 17:01:00.123456'),\n    // (6, '2021-11-18 02:30:00.123456', NULL),\n    // (7, '2013-07-05 17:01:00.123456', NULL),\n    // (8, NULL,                         NULL)\n\n    // test case (kernel predicate object, equivalent spark predicate as string,\n    //              expected row count, expected remaining filter)\n    Seq(\n      (\n        // 1637202600123456L in epoch micros for '2021-11-18 02:30:00.123456'\n        predicate(\"=\", col(\"tsNtzPartition\"), ofTimestampNtz(1637202600123456L)),\n        \"tsNtzPartition = '2021-11-18 02:30:00.123456'\",\n        3, // expected row count\n        null.asInstanceOf[Predicate] // expected remaining filter\n      ),\n      (\n        predicate(\"=\", col(\"tsNtzPartition\"), Literal ofNull (TIMESTAMP_NTZ)),\n        \"tsNtzPartition = null\",\n        0, // expected row count\n        null.asInstanceOf[Predicate] // expected remaining filter\n      ),\n      (\n        // 1373043660123456L in epoch micros for '2013-07-05 17:01:00.123456'\n        predicate(\">=\", col(\"tsNtzPartition\"), ofTimestampNtz(1373043660123456L)),\n        \"tsNtzPartition >= '2013-07-05 17:01:00'\",\n        6, // expected row count\n        null.asInstanceOf[Predicate] // expected remaining filter\n      ),\n      (\n        predicate(\"IS_NULL\", col(\"tsNtzPartition\")),\n        \"tsNtzPartition IS NULL\",\n        3, // expected row count\n        null.asInstanceOf[Predicate] // expected remaining filter\n      ),\n      (\n        // Filter on just the data column\n        // 1637202600123456L in epoch micros for '2021-11-18 02:30:00.123456'\n        predicate(\n          \"OR\",\n          predicate(\"=\", col(\"tsNtz\"), ofTimestampNtz(1637202600123456L)),\n          predicate(\"=\", col(\"tsNtz\"), ofTimestampNtz(1373043660123456L))),\n        \"\",\n        9, // expected row count\n        // expected remaining filter\n        predicate(\n          \"OR\",\n          predicate(\"=\", col(\"tsNtz\"), ofTimestampNtz(1637202600123456L)),\n          predicate(\"=\", col(\"tsNtz\"), ofTimestampNtz(1373043660123456L))))).foreach {\n      case (kernelPredicate, sparkPredicate, expectedRowCount, expRemainingFilter) =>\n        test(s\"partition pruning on timestamp_ntz columns: $cmMode ($kernelPredicate)\") {\n          val tablePath = goldenTablePath(s\"data-reader-timestamp_ntz$cmMode\")\n          val expectedResult = readUsingSpark(tablePath, sparkPredicate)\n          assert(expectedResult.size === expectedRowCount)\n          checkTable(\n            expectedAnswer = expectedResult,\n            path = tablePath,\n            expectedRemainingFilter = expRemainingFilter,\n            filter = kernelPredicate)\n        }\n    }\n  }\n\n  test(\"partition pruning from checkpoint\") {\n    withTempDir { path =>\n      withTempTable { tbl =>\n        // Create partitioned table and insert some data, ensuring that a checkpoint is created\n        // after the last insertion.\n        spark.sql(s\"CREATE TABLE $tbl (a INT, b STRING) USING delta \" +\n          s\"PARTITIONED BY (a) LOCATION '$path' \" +\n          s\"TBLPROPERTIES ('delta.checkpointInterval' = '2')\")\n        spark.sql(s\"INSERT INTO $tbl VALUES (1, 'a'), (2, 'b')\")\n        spark.sql(s\"INSERT INTO $tbl VALUES (3, 'c'), (4, 'd')\")\n        spark.sql(s\"INSERT INTO $tbl VALUES (5, 'e'), (6, 'f')\")\n\n        // Read from the source table with a partition predicate and validate the results.\n        val result = readSnapshot(\n          latestSnapshot(path.toString),\n          filter = greaterThan(col(\"a\"), Literal.ofInt(3)))\n        checkAnswer(result, Seq(TestRow(4, \"d\"), TestRow(5, \"e\"), TestRow(6, \"f\")))\n      }\n    }\n  }\n\n  private def readUsingSpark(tablePath: String, predicate: String): Seq[TestRow] = {\n    val where = if (predicate.isEmpty) \"\" else s\"WHERE $predicate\"\n    spark.sql(s\"SELECT * FROM delta.`$tablePath` $where\")\n      .collect()\n      .map(TestRow(_))\n  }\n\n  private def partColName(column: Column): String = {\n    assert(column.getNames.length == 1)\n    column.getNames()(0)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/PartitionUtilsSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.defaults.utils.{ExpressionTestUtils, TestRow, TestUtils}\nimport io.delta.kernel.expressions.Literal.ofInt\nimport io.delta.kernel.utils.PartitionUtils\n\nimport org.apache.spark.sql.functions.{col => sparkCol}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PartitionUtilsSuite extends AnyFunSuite with TestUtils with ExpressionTestUtils {\n\n  private def createTableWithPartCols(path: String): Unit = {\n    spark.range(100)\n      .withColumn(\"part1\", sparkCol(\"id\") % 2)\n      .withColumn(\"part2\", sparkCol(\"id\") % 5)\n      .withColumn(\"col1\", sparkCol(\"id\"))\n      .withColumn(\"col2\", sparkCol(\"id\"))\n      .drop(\"id\")\n      .write\n      .format(\"delta\")\n      .partitionBy(\"part1\", \"part2\")\n      .save(path)\n  }\n\n  Seq(\"name\", \"id\").foreach { mode =>\n    test(s\"withPartitionColumns - read subset of partition cols with column mapping mode = $mode\") {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getCanonicalPath\n        spark.sql(\n          s\"\"\"CREATE TABLE delta.`$tablePath`(c1 long, c2 STRING, p1 STRING, p2 LONG)\n             | USING delta PARTITIONED BY (p1, p2)\n             | TBLPROPERTIES(\n             |'delta.columnMapping.mode' = '$mode')\n             |\"\"\".stripMargin)\n        Seq.range(0, 5).foreach { i =>\n          spark.sql(s\"insert into delta.`$tablePath` values ($i, '$i', '$i', $i)\")\n        }\n\n        checkTable(\n          tablePath,\n          expectedAnswer =\n            Seq((0L, \"0\"), (1L, \"1\"), (2L, \"2\"), (3L, \"3\"), (4L, \"4\")).map(TestRow.fromTuple(_)),\n          readCols = Seq(\"p2\", \"c2\"))\n      }\n    }\n  }\n\n  test(\"partitionExists - input validation\") {\n    withTempDirAndEngine { (path, engine) =>\n      createTableWithPartCols(path)\n\n      val snapshot = Table.forPath(engine, path).getLatestSnapshot(engine)\n\n      {\n        val badPredicate = and(\n          predicate(\"=\", col(\"part1\"), ofInt(0)),\n          predicate(\"=\", col(\"col1\"), ofInt(0)))\n        val exMsg = intercept[IllegalArgumentException] {\n          PartitionUtils.partitionExists(engine, snapshot, badPredicate)\n        }.getMessage\n        assert(exMsg.contains(\"Partition predicate must contain only partition columns\"))\n      }\n\n      {\n        val badPredicate = predicate(\"=\", col(\"col1\"), ofInt(0))\n        val exMsg = intercept[IllegalArgumentException] {\n          PartitionUtils.partitionExists(engine, snapshot, badPredicate)\n        }.getMessage\n        assert(exMsg.contains(\"Partition predicate must contain at least one partition column\"))\n      }\n    }\n  }\n\n  test(\"partitionExists - simple case using latest table snapshot\") {\n    withTempDirAndEngine { (path, engine) =>\n      createTableWithPartCols(path)\n\n      val snapshot = Table.forPath(engine, path).getLatestSnapshot(engine)\n\n      // ===== Simple Cases =====\n      {\n        val partPredicate = predicate(\"=\", col(\"part1\"), ofInt(1)) // Yes\n        assert(PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n      {\n        val partPredicate = predicate(\">=\", col(\"part1\"), ofInt(500)) // No\n        assert(!PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n      {\n        val partPredicate = predicate(\"<\", col(\"part1\"), ofInt(100)) // Yes\n        assert(PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n\n      // ===== Conjunction and Disjunction Cases =====\n      {\n        val partPredicate = and(\n          predicate(\"=\", col(\"part1\"), ofInt(0)), // Yes\n          predicate(\"=\", col(\"part2\"), ofInt(4)) // Yes\n        )\n        assert(PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n      {\n        val partPredicate = and(\n          predicate(\"=\", col(\"part1\"), ofInt(0)), // Yes\n          predicate(\"=\", col(\"part2\"), ofInt(500)) // No\n        )\n        assert(!PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n      {\n        val partPredicate = or(\n          predicate(\"=\", col(\"part1\"), ofInt(500)), // No\n          predicate(\"=\", col(\"part2\"), ofInt(3)) // N/A\n        )\n        assert(PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n    }\n  }\n\n  test(\"partitionExists - some or all data in partition was removed\") {\n    withTempDirAndEngine { (path, engine) =>\n      createTableWithPartCols(path)\n      spark.sql(s\"DELETE FROM delta.`$path` WHERE part1 = 0\")\n      spark.sql(s\"DELETE FROM delta.`$path` WHERE part1 = 1 AND part2 = 0\")\n      spark.sql(s\"DELETE FROM delta.`$path` WHERE part1 = 1 AND part2 = 1 AND col1 < 50\")\n\n      val snapshot = Table.forPath(engine, path).getLatestSnapshot(engine)\n\n      {\n        val partPredicate = predicate(\"=\", col(\"part1\"), ofInt(0)) // No\n        assert(!PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n      {\n        val partPredicate = and(\n          predicate(\"=\", col(\"part1\"), ofInt(1)), // Yes\n          predicate(\"=\", col(\"part2\"), ofInt(0)) // No\n        )\n        assert(!PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n      {\n        val partPredicate = and(\n          predicate(\"=\", col(\"part1\"), ofInt(1)), // Yes\n          predicate(\"=\", col(\"part2\"), ofInt(1)) // Has some data\n        )\n        assert(PartitionUtils.partitionExists(engine, snapshot, partPredicate))\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/PostCommitSnapshotSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.{Operation, Snapshot, TransactionCommitResult}\nimport io.delta.kernel.Snapshot.ChecksumWriteMode\nimport io.delta.kernel.defaults.utils.{TestRow, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.{InternalScanFileUtils, SnapshotImpl, TableConfig}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.test.MockEngineUtils\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.utils.CloseableIterable.inMemoryIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for validating the behavior of postCommitSnapshot in various scenarios.\n *\n * Note that we use \"PCS\" in our test names for brevity.\n */\nclass PostCommitSnapshotSuite\n    extends AnyFunSuite\n    with WriteUtilsWithV2Builders\n    with MockEngineUtils {\n\n  //////////////////\n  // Test Helpers //\n  //////////////////\n\n  private def assertAddFilesMatch(\n      engine: Engine,\n      actual: SnapshotImpl,\n      expected: SnapshotImpl): Unit = {\n    val actualFiles = collectScanFileRows(actual.getScanBuilder.build(), engine)\n      .map(x => InternalScanFileUtils.getAddFileStatus(x).getPath)\n    val expectedFiles = collectScanFileRows(expected.getScanBuilder.build(), engine)\n      .map(x => InternalScanFileUtils.getAddFileStatus(x).getPath)\n\n    assert(actualFiles === expectedFiles)\n  }\n\n  private def checkPostCommitSnapshot(\n      engine: Engine,\n      postCommitSnapshot: Snapshot,\n      expectCrc: Boolean = false): Unit = {\n    val actual = postCommitSnapshot.asInstanceOf[SnapshotImpl]\n    val expected = latestSnapshot(actual.getPath, engine)\n\n    if (expectCrc) {\n      assert(actual.getCurrentCrcInfo.isPresent)\n    }\n\n    // TODO: We need better visibility into when the below information is loaded from the log,\n    //       loaded from CRC, or already stored in memory (i.e. injected during post-commit snapshot\n    //       creation)\n\n    assert(actual.getVersion === expected.getVersion)\n    assert(actual.getSchema === expected.getSchema)\n    assert(actual.getProtocol === expected.getProtocol)\n    assert(actual.getMetadata === expected.getMetadata)\n    assert(actual.getPartitionColumnNames === expected.getPartitionColumnNames)\n    assert(actual.getPhysicalClusteringColumns === expected.getPhysicalClusteringColumns)\n    assert(actual.getTimestamp(engine) === expected.getTimestamp(engine))\n    assert(actual.getActiveDomainMetadataMap === expected.getActiveDomainMetadataMap)\n\n    assertAddFilesMatch(engine, actual, expected)\n  }\n\n  ////////////////////////////\n  // Create new table tests //\n  ////////////////////////////\n\n  test(\"creating a new empty table => yields a PCS\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val result = createEmptyTable(engine, tablePath, testSchema)\n\n      checkPostCommitSnapshot(engine, result.getPostCommitSnapshot.get(), expectCrc = true)\n    }\n  }\n\n  test(\"creating a new table with data => yields a PCS\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val result = appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        schema = testSchema,\n        data = seqOfUnpartitionedDataBatch1)\n\n      checkPostCommitSnapshot(engine, result.getPostCommitSnapshot.get(), expectCrc = true)\n    }\n  }\n\n  /////////////////////////\n  // CRC existence tests //\n  /////////////////////////\n\n  test(\"commit at readVersion + 1 (*with* CRC at readVersion) => yields a PCS with CRC\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val result0 = createEmptyTable(engine, tablePath, testSchema)\n      result0.getPostCommitSnapshot.get().writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n      assert(latestSnapshot(tablePath, engine).getCurrentCrcInfo.isPresent)\n\n      val result1 = appendData(engine, tablePath, data = seqOfUnpartitionedDataBatch1)\n\n      checkPostCommitSnapshot(engine, result1.getPostCommitSnapshot.get(), expectCrc = true)\n    }\n  }\n\n  test(\"commit at readVersion + 1 (*without* CRC at readVersion) => yields a PCS without CRC\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n\n      val result1 = appendData(engine, tablePath, data = seqOfUnpartitionedDataBatch1)\n\n      checkPostCommitSnapshot(engine, result1.getPostCommitSnapshot.get(), expectCrc = false)\n    }\n  }\n\n  test(\"commit at readVersion + 2 => does NOT yield a PCS\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      createEmptyTable(engine, tablePath, testSchema) // V0\n\n      val txn = getUpdateTxn(engine, tablePath) // Create a transaction that reads at V0\n      assert(txn.getReadTableVersion == 0)\n\n      // Create winning commits at V1 and V2\n      appendData(engine, tablePath, data = seqOfUnpartitionedDataBatch1) // V1\n      appendData(engine, tablePath, data = seqOfUnpartitionedDataBatch2) // V2\n      assert(latestSnapshot(tablePath, engine).getVersion == 2)\n\n      // ===== WHEN =====\n      // Now commit the original txn that read at v0. This will commit at v3\n      val txnState = txn.getTransactionState(engine)\n      val actions = inMemoryIterable(stageData(txnState, Map.empty[String, Literal], dataBatches1))\n      val result = commitTransaction(txn, engine, actions) // V3\n\n      // ===== THEN =====\n      assert(result.getVersion == 3)\n      assert(!result.getPostCommitSnapshot.isPresent)\n    }\n  }\n\n  ////////////////////////////////////////////////////////////\n  // PostCommitSnapshot has certain fields pre-loaded tests //\n  ////////////////////////////////////////////////////////////\n\n  test(\"PCS has ICT pre-loaded\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = Map(TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED.getKey -> \"true\"))\n\n      val result = appendData(engine, tablePath, data = seqOfUnpartitionedDataBatch1)\n      val postCommitSnapshot = result.getPostCommitSnapshot.get().asInstanceOf[SnapshotImpl]\n\n      val failingEngine = mockEngine()\n\n      // should *not* use the engine to try and read ICT from delta file\n      postCommitSnapshot.getTimestamp(failingEngine)\n    }\n  }\n\n  // TODO: Test CRC is also pre-loaded. Requires\n  //       (1) SnapshotImpl::getCurrentCrcInfo to take in an engine param\n  //       (2) LogReplay to *not* be injected into SnapshotImpl constructor\n  //       (3) CRC to be injected into SnapshotImpl constructor\n\n  // TODO: Test clusteringColumns are pre-loaded (when txn sets new clustering columns)\n\n  ///////////////////////////\n  // Metadata change tests //\n  ///////////////////////////\n\n  case class MetadataChangeTestCase(\n      changeType: String,\n      initTableProperties: Map[String, String] = Map.empty,\n      updateFn: (Engine, String) => TransactionCommitResult)\n\n  Seq(\n    MetadataChangeTestCase(\n      changeType = \"schema\",\n      initTableProperties = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> \"name\"),\n      updateFn = (engine, tablePath) => {\n        val newSchema = latestSnapshot(tablePath, engine).getSchema.add(\"newCol\", INTEGER)\n        updateTableMetadata(engine, tablePath, schema = newSchema)\n      }),\n    MetadataChangeTestCase(\n      changeType = \"tbl property\",\n      updateFn = (engine, tablePath) =>\n        updateTableMetadata(engine, tablePath, tableProperties = Map(\"foo\" -> \"bar\"))),\n    MetadataChangeTestCase(\n      changeType = \"protocol\",\n      updateFn = (engine, tablePath) => {\n        val snapshot = latestSnapshot(tablePath, engine)\n        assert(!snapshot.getProtocol.getWriterFeatures.contains(\"deletionVectors\"))\n\n        updateTableMetadata(\n          engine,\n          tablePath,\n          tableProperties = Map(\"delta.enableDeletionVectors\" -> \"true\"))\n      }),\n    MetadataChangeTestCase(\n      changeType = \"clustering columns\",\n      updateFn = (engine, tablePath) =>\n        updateTableMetadata(\n          engine,\n          tablePath,\n          clusteringColsOpt = Some(testClusteringColumns)))).foreach {\n    case MetadataChangeTestCase(changeType, initTableProperties, updateFn) =>\n      test(\n        s\"commit $changeType change at readVersion + 1 => yields a PCS with updated $changeType\") {\n        withTempDirAndEngine { (tablePath, engine) =>\n          createEmptyTable(\n            engine,\n            tablePath,\n            schema = testPartitionSchema,\n            tableProperties = initTableProperties)\n\n          val result = updateFn(engine, tablePath)\n\n          checkPostCommitSnapshot(engine, result.getPostCommitSnapshot.get())\n        }\n      }\n  }\n\n  ////////////////////////////////////////////////////////////////\n  // Using PostCommitSnapshot to read data and write data tests //\n  ////////////////////////////////////////////////////////////////\n\n  test(\"postCommitSnapshot can be used to read data\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val result = appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        schema = testSchema,\n        data = seqOfUnpartitionedDataBatch1)\n\n      val postCommitSnapshot = result.getPostCommitSnapshot.get()\n      val expectedData = dataBatches1.flatMap(_.toTestRows)\n      val dataFromPostCommit = readSnapshot(postCommitSnapshot, engine = engine).map(TestRow(_))\n      checkAnswer(dataFromPostCommit, expectedData)\n    }\n  }\n\n  test(\"postCommitSnapshot can be used to start and commit a new transaction\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      val result0 = appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        schema = testSchema,\n        data = seqOfUnpartitionedDataBatch1)\n      val postCommitSnapshot0 = result0.getPostCommitSnapshot.get()\n      assert(postCommitSnapshot0.getVersion == 0)\n\n      // ===== WHEN =====\n      // Use the postCommitSnapshot to start a new transaction and commit v1\n      val txn = postCommitSnapshot0\n        .buildUpdateTableTransaction(testEngineInfo, Operation.WRITE)\n        .build(engine)\n      val txnState = txn.getTransactionState(engine)\n      val actions = inMemoryIterable(stageData(txnState, Map.empty[String, Literal], dataBatches2))\n      val result1 = commitTransaction(txn, engine, actions)\n\n      // ===== THEN =====\n      checkPostCommitSnapshot(engine, result1.getPostCommitSnapshot.get())\n    }\n  }\n\n  ////////////////\n  // Publishing //\n  ////////////////\n\n  test(\"publishing on a postCommitSnapshot for a filesystem-based table is a no-op\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val result = appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        schema = testSchema,\n        data = seqOfUnpartitionedDataBatch1)\n\n      val postCommitSnapshot = result.getPostCommitSnapshot.get().asInstanceOf[SnapshotImpl]\n\n      assert(!TableFeatures.isCatalogManagedSupported(postCommitSnapshot.getProtocol))\n\n      postCommitSnapshot.publish(engine) // Should not throw -- there are no catalog commits!\n    }\n  }\n\n  test(\"publishing on a postCommitSnapshot will return a snapshot\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      var result = appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        schema = testSchema,\n        data = seqOfUnpartitionedDataBatch1)\n\n      var postCommitSnapshot = result.getPostCommitSnapshot.get().asInstanceOf[SnapshotImpl]\n      var postPublishSnapshot = postCommitSnapshot.publish(engine)\n      assert(postPublishSnapshot == postCommitSnapshot)\n\n      result = appendData(\n        engine,\n        tablePath,\n        isNewTable = false,\n        data = seqOfUnpartitionedDataBatch1)\n\n      postCommitSnapshot = result.getPostCommitSnapshot.get().asInstanceOf[SnapshotImpl]\n      postPublishSnapshot = postCommitSnapshot.publish(engine)\n      assert(postPublishSnapshot == postCommitSnapshot)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/RowTrackingSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.util\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.{FilteredColumnarBatch, Row}\nimport io.delta.kernel.defaults.internal.parquet.ParquetSuiteBase\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, TestRow, WriteUtilsWithV1Builders, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{ConcurrentWriteException, InvalidTableException, KernelException, MaxCommitRetryLimitReachedException}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.{InternalScanFileUtils, SnapshotImpl, TableConfig, TableImpl}\nimport io.delta.kernel.internal.actions.{AddFile, SingleAction}\nimport io.delta.kernel.internal.rowtracking.{RowTracking, RowTrackingMetadataDomain}\nimport io.delta.kernel.internal.rowtracking.MaterializedRowTrackingColumn.{MATERIALIZED_ROW_COMMIT_VERSION, MATERIALIZED_ROW_ID}\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.types._\nimport io.delta.kernel.utils.{CloseableIterable, MetadataColumnTestUtils}\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\n\nimport org.apache.spark.sql.delta.DeltaLog\n\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Runs row tracking tests using the TableManager snapshot APIs and V2 transaction builders */\nclass RowTrackingSuite extends AbstractRowTrackingSuite with WriteUtilsWithV2Builders\n\n/** Runs row tracking tests using the legacy Table snapshot APIs and V1 transaction builders */\nclass LegacyRowTrackingSuite extends AbstractRowTrackingSuite with WriteUtilsWithV1Builders\n\ntrait AbstractRowTrackingSuite extends AnyFunSuite with ParquetSuiteBase\n    with MetadataColumnTestUtils { self: AbstractWriteUtils =>\n  private def prepareActionsForCommit(actions: Row*): CloseableIterable[Row] = {\n    inMemoryIterable(toCloseableIterator(actions.asJava.iterator()))\n  }\n\n  private def createTableWithRowTracking(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = testSchema,\n      extraProps: Map[String, String] = Map.empty): Unit = {\n    val tableProps = Map(TableConfig.ROW_TRACKING_ENABLED.getKey -> \"true\") ++ extraProps\n    createEmptyTable(engine, tablePath, schema = schema, tableProperties = tableProps)\n  }\n\n  /**\n   * Creates a table with row tracking enabled and inserts initial data, then performs a merge\n   * operation to update some records and insert new ones. We use Spark SQL for this test table to\n   * ensure that the result table has both materialized and not materialized row tracking columns.\n   *\n   * @param tablePath The path to the Delta table.\n   * @param extraProps Additional table properties to set.\n   */\n  private def createRowTrackingTableWithSpark(\n      tablePath: String,\n      extraProps: Map[String, String] = Map.empty): Unit = {\n    val tblPropsStr = (extraProps + (\"delta.enableRowTracking\" -> \"true\"))\n      .map { case (k, v) => s\"'$k' = '$v'\" }.mkString(\", \")\n    spark.sql(\n      s\"\"\"\n         |CREATE TABLE delta.`$tablePath` (\n         |  id INT,\n         |  value STRING\n         |) USING DELTA\n         |TBLPROPERTIES ($tblPropsStr)\n         |\"\"\".stripMargin)\n\n    // Insert 5 records\n    val initialData = Seq(\n      (1, \"A\"),\n      (2, \"B\"),\n      (3, \"C\"),\n      (4, \"D\"),\n      (5, \"E\"))\n    spark.createDataFrame(initialData).toDF(\n      \"id\",\n      \"value\").repartition(1).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n\n    // Prepare source for merge\n    val sourceData = Seq(\n      (3, \"C_updated\"), // will update id=3\n      (6, \"F\") // will insert new id=6\n    )\n    spark.createDataFrame(sourceData).toDF(\"id\", \"value\").createOrReplaceTempView(\"merge_source\")\n\n    // Merge: update id=3, insert id=6\n    spark.sql(\n      s\"\"\"\n         |MERGE INTO delta.`$tablePath` t\n         |USING merge_source s\n         |ON t.id = s.id\n         |WHEN MATCHED THEN UPDATE SET t.value = s.value\n         |WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value)\n         |\"\"\".stripMargin)\n  }\n\n  private def verifyBaseRowIDs(\n      engine: Engine,\n      tablePath: String,\n      expectedValue: Seq[Long]): Unit = {\n    val snapshot =\n      getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n\n    val scanFileRows = collectScanFileRows(snapshot.getScanBuilder().build())\n    val sortedBaseRowIds = scanFileRows\n      .map(InternalScanFileUtils.getBaseRowId)\n      .map(_.orElse(-1))\n      .sorted\n\n    assert(sortedBaseRowIds === expectedValue)\n  }\n\n  private def verifyDefaultRowCommitVersion(\n      engine: Engine,\n      tablePath: String,\n      expectedValue: Seq[Long]) = {\n    val snapshot =\n      getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n\n    val scanFileRows = collectScanFileRows(snapshot.getScanBuilder().build())\n    val sortedAddFileDefaultRowCommitVersions = scanFileRows\n      .map(InternalScanFileUtils.getDefaultRowCommitVersion)\n      .map(_.orElse(-1))\n      .sorted\n\n    assert(sortedAddFileDefaultRowCommitVersions === expectedValue)\n  }\n\n  private def verifyHighWatermark(engine: Engine, tablePath: String, expectedValue: Long): Unit = {\n    val snapshot =\n      getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n    val rowTrackingMetadataDomain = RowTrackingMetadataDomain.fromSnapshot(snapshot)\n\n    assert(rowTrackingMetadataDomain.isPresent)\n    assert(rowTrackingMetadataDomain.get().getRowIdHighWaterMark === expectedValue)\n  }\n\n  private def prepareDataForCommit(data: Seq[FilteredColumnarBatch]*)\n      : Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])] = {\n    data.map(Map.empty[String, Literal] -> _).toIndexedSeq\n  }\n\n  test(\"RowTracking.isEnabled - returns false when row tracking is not enabled in metadata\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create table with row tracking supported but not enabled\n      createEmptyTable(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = Map.empty)\n\n      val snapshot =\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n      val protocol = snapshot.getProtocol\n      val metadata = snapshot.getMetadata\n\n      assert(!RowTracking.isEnabled(protocol, metadata))\n    }\n  }\n\n  test(\"RowTracking.isEnabled - returns true when row tracking is supported and enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create table with row tracking enabled\n      createTableWithRowTracking(engine, tablePath)\n\n      val snapshot =\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n      val protocol = snapshot.getProtocol\n      val metadata = snapshot.getMetadata\n\n      assert(RowTracking.isEnabled(protocol, metadata))\n    }\n  }\n\n  test(\n    \"RowTracking.isEnabled - throws exception when enabled in metadata but not \" +\n      \"supported by protocol\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a table without row tracking support\n      createEmptyTable(engine, tablePath, testSchema)\n\n      // Get the current metadata and protocol\n      val snapshot =\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n      val protocol = snapshot.getProtocol\n      val originalMetadata = snapshot.getMetadata\n\n      // Manually create metadata with row tracking enabled (bypassing validation)\n      val configWithRowTracking = originalMetadata.getConfiguration.asScala.toMap +\n        (TableConfig.ROW_TRACKING_ENABLED.getKey -> \"true\")\n      val problematicMetadata =\n        originalMetadata.withReplacedConfiguration(configWithRowTracking.asJava)\n\n      // Verify that calling isEnabled throws IllegalStateException\n      val e = intercept[IllegalStateException] {\n        RowTracking.isEnabled(protocol, problematicMetadata)\n      }\n      assert(e.getMessage.contains(\n        \"Table property 'delta.enableRowTracking' is set on the table but this table version \" +\n          \"doesn't support table feature 'delta.feature.rowTracking'\"))\n    }\n  }\n\n  test(\"Base row IDs/default row commit versions are assigned to AddFile actions\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      val dataBatch1 = generateData(testSchema, Seq.empty, Map.empty, 100, 1) // 100 rows\n      val dataBatch2 = generateData(testSchema, Seq.empty, Map.empty, 200, 1) // 200 rows\n      val dataBatch3 = generateData(testSchema, Seq.empty, Map.empty, 400, 1) // 400 rows\n\n      // Commit three files in one transaction\n      val commitVersion = appendData(\n        engine,\n        tablePath,\n        data = prepareDataForCommit(dataBatch1, dataBatch2, dataBatch3)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0, 100, 300))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq.fill(3)(commitVersion))\n      verifyHighWatermark(engine, tablePath, 699)\n    }\n  }\n\n  test(\"Previous Row ID high watermark can be picked up to assign base row IDs\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      val dataBatch1 = generateData(testSchema, Seq.empty, Map.empty, 100, 1)\n      val commitVersion1 = appendData(\n        engine,\n        tablePath,\n        data = Seq(dataBatch1).map(Map.empty[String, Literal] -> _)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq(commitVersion1))\n      verifyHighWatermark(engine, tablePath, 99)\n\n      val dataBatch2 = generateData(testSchema, Seq.empty, Map.empty, 200, 1)\n      val commitVersion2 = appendData(\n        engine,\n        tablePath,\n        data = prepareDataForCommit(dataBatch2)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0, 100))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq(commitVersion1, commitVersion2))\n      verifyHighWatermark(engine, tablePath, 299)\n    }\n  }\n\n  test(\"Base row IDs/default row commit versions are preserved in checkpoint\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      val dataBatch1 = generateData(testSchema, Seq.empty, Map.empty, 100, 1)\n      val dataBatch2 = generateData(testSchema, Seq.empty, Map.empty, 200, 1)\n      val dataBatch3 = generateData(testSchema, Seq.empty, Map.empty, 400, 1)\n\n      val commitVersion1 = appendData(\n        engine,\n        tablePath,\n        data = prepareDataForCommit(dataBatch1)).getVersion\n\n      val commitVersion2 = appendData(\n        engine,\n        tablePath,\n        data = prepareDataForCommit(dataBatch2)).getVersion\n\n      // Checkpoint the table\n      val latestVersion = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).getVersion()\n      TableImpl.forPath(engine, tablePath).checkpoint(engine, latestVersion)\n\n      val commitVersion3 = appendData(\n        engine,\n        tablePath,\n        data = prepareDataForCommit(dataBatch3)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0, 100, 300))\n      verifyDefaultRowCommitVersion(\n        engine,\n        tablePath,\n        Seq(commitVersion1, commitVersion2, commitVersion3))\n      verifyHighWatermark(engine, tablePath, 699)\n    }\n  }\n\n  test(\"Provided Row ID high watermark should be set in the txn\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      val dataBatch1 = generateData(testSchema, Seq.empty, Map.empty, 100, 1)\n      val commitVersion1 = appendData(\n        engine,\n        tablePath,\n        data = Seq(dataBatch1).map(Map.empty[String, Literal] -> _)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq(commitVersion1))\n      verifyHighWatermark(engine, tablePath, 99)\n\n      val dataBatch2 = generateData(testSchema, Seq.empty, Map.empty, 200, 1)\n      val rowTrackingMetadataDomain = new RowTrackingMetadataDomain(400)\n\n      val txn2 = createTxnWithDomainMetadatas(\n        engine,\n        tablePath,\n        List(rowTrackingMetadataDomain.toDomainMetadata))\n      val commitVersion2 =\n        commitAppendData(engine, txn2, prepareDataForCommit(dataBatch2)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0, 100))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq(commitVersion1, commitVersion2))\n      verifyHighWatermark(engine, tablePath, 400)\n    }\n  }\n\n  test(\"Fail if provided Row ID high watermark is smaller than the calculated high watermark\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      val dataBatch1 = generateData(testSchema, Seq.empty, Map.empty, 100, 1)\n      val commitVersion1 = appendData(\n        engine,\n        tablePath,\n        data = Seq(dataBatch1).map(Map.empty[String, Literal] -> _)).getVersion\n\n      verifyBaseRowIDs(engine, tablePath, Seq(0))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq(commitVersion1))\n      verifyHighWatermark(engine, tablePath, 99)\n\n      val dataBatch2 = generateData(testSchema, Seq.empty, Map.empty, 200, 1)\n\n      // Set a higher value than the calculated high watermark = 299\n      val rowTrackingMetadataDomain = new RowTrackingMetadataDomain(120)\n      val txn2 = createTxnWithDomainMetadatas(\n        engine,\n        tablePath,\n        List(rowTrackingMetadataDomain.toDomainMetadata))\n      val e = intercept[RuntimeException] {\n        commitAppendData(engine, txn2, prepareDataForCommit(dataBatch2)).getVersion\n      }\n\n      assert(\n        e.getMessage.contains(\n          \"The provided row ID high watermark (120) must be greater than \" +\n            \"or equal to the calculated row ID high watermark (299) based \" +\n            \"on the transaction's data actions.\"))\n    }\n  }\n\n  test(\"Fail if row tracking is supported but AddFile actions are missing stats\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      val addFileRow = AddFile.createAddFileRow(\n        null,\n        \"fakePath\",\n        VectorUtils.stringStringMapValue(new util.HashMap[String, String]()),\n        0L,\n        0L,\n        false,\n        Optional.empty(),\n        Optional.empty(),\n        Optional.empty(),\n        Optional.empty(),\n        Optional.empty() // No stats\n      )\n      val action = SingleAction.createAddFileSingleAction(addFileRow)\n      val txn = getUpdateTxn(engine, tablePath)\n\n      // KernelException thrown inside a lambda is wrapped in a RuntimeException\n      val e = intercept[RuntimeException] {\n        txn.commit(engine, prepareActionsForCommit(action))\n      }\n      assert(\n        e.getMessage.contains(\n          \"Cannot write to a rowTracking-supported table without 'numRecords' statistics. \"\n            + \"Connectors are expected to populate the number of records statistics when \"\n            + \"writing to a Delta table with 'rowTracking' table feature supported.\"))\n    }\n  }\n\n  test(\"Fail if row tracking is not supported but client call withHighWatermark in txn\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createEmptyTable(engine, tablePath, testSchema)\n\n      val rowTrackingMetadataDomain = new RowTrackingMetadataDomain(30)\n\n      val e = intercept[RuntimeException] {\n        createTxnWithDomainMetadatas(\n          engine,\n          tablePath,\n          List(rowTrackingMetadataDomain.toDomainMetadata))\n      }\n\n      assert(\n        e.getMessage.contains(\n          \"Cannot assign a row id high water mark\"))\n    }\n  }\n\n  test(\"Integration test - Write table with Kernel then write with Spark\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      withTempTable { tbl =>\n        val schema = new StructType().add(\"id\", LongType.LONG)\n        createTableWithRowTracking(engine, tablePath, schema)\n\n        // Write table using Kernel\n        val dataBatch1 = generateData(schema, Seq.empty, Map.empty, 100, 1) // 100 rows\n        val dataBatch2 = generateData(schema, Seq.empty, Map.empty, 200, 1) // 200 rows\n        val dataBatch3 = generateData(schema, Seq.empty, Map.empty, 400, 1) // 400 rows\n        appendData(\n          engine,\n          tablePath,\n          data = prepareDataForCommit(dataBatch1, dataBatch2, dataBatch3)\n        ).getVersion // version 1\n\n        // Verify the table state\n        verifyBaseRowIDs(engine, tablePath, Seq(0, 100, 300))\n        verifyDefaultRowCommitVersion(engine, tablePath, Seq(1, 1, 1))\n        verifyHighWatermark(engine, tablePath, 699)\n\n        // Write 20, 80 rows to the table using Spark\n        spark.range(0, 20).write.format(\"delta\").mode(\"append\").save(tablePath) // version 2\n        spark.range(20, 100).write.format(\"delta\").mode(\"append\").save(tablePath) // version 3\n\n        // Verify the table state\n        verifyBaseRowIDs(engine, tablePath, Seq(0, 100, 300, 700, 720))\n        verifyDefaultRowCommitVersion(engine, tablePath, Seq(1, 1, 1, 2, 3))\n        verifyHighWatermark(engine, tablePath, 799)\n      }\n    })\n  }\n\n  test(\"Integration test - Write table with Spark then write with Kernel\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      withTempTable { tbl =>\n        spark.sql(\n          s\"\"\"CREATE TABLE $tbl (id LONG) USING delta\n             |LOCATION '$tablePath'\n             |TBLPROPERTIES (\n             |  'delta.feature.domainMetadata' = 'enabled',\n             |  'delta.feature.rowTracking' = 'supported'\n             |)\n             |\"\"\".stripMargin)\n\n        // Write to the table using delta-spark\n        spark.range(0, 20).write.format(\"delta\").mode(\"append\").save(tablePath) // version 1\n        spark.range(20, 100).write.format(\"delta\").mode(\"append\").save(tablePath) // version 2\n\n        // Verify the table state\n        verifyBaseRowIDs(engine, tablePath, Seq(0, 20))\n        verifyDefaultRowCommitVersion(engine, tablePath, Seq(1, 2))\n        verifyHighWatermark(engine, tablePath, 99)\n\n        // Write to the table using Kernel\n        val schema = new StructType().add(\"id\", LongType.LONG)\n        val dataBatch1 = generateData(schema, Seq.empty, Map.empty, 100, 1) // 100 rows\n        val dataBatch2 = generateData(schema, Seq.empty, Map.empty, 200, 1) // 200 rows\n        val dataBatch3 = generateData(schema, Seq.empty, Map.empty, 400, 1) // 400 rows\n        appendData(\n          engine,\n          tablePath,\n          data = prepareDataForCommit(dataBatch1, dataBatch2, dataBatch3)\n        ) // version 3\n\n        // Verify the table state\n        verifyBaseRowIDs(engine, tablePath, Seq(0, 20, 100, 200, 400))\n        verifyDefaultRowCommitVersion(engine, tablePath, Seq(1, 2, 3, 3, 3))\n        verifyHighWatermark(engine, tablePath, 799)\n      }\n    })\n  }\n\n  /* -------- Test reading from tables with row tracking -------- */\n  test(\"Error when reading row tracking columns from a non-row-tracking table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a new table without row tracking\n      val wrongSchema =\n        new StructType().add(\"id\", IntegerType.INTEGER).add(\"_metadata.row_id\", LongType.LONG)\n      createEmptyTable(engine, tablePath, wrongSchema)\n\n      // Try to read row tracking columns\n      val e = intercept[KernelException] {\n        checkTable(\n          tablePath,\n          expectedAnswer = Seq(),\n          readCols = Seq(\"id\", \"_metadata.row_id\"),\n          metadataCols =\n            Seq(ROW_ID, ROW_COMMIT_VERSION),\n          engine = engine)\n      }\n      assert(e.getMessage.contains(\"Row tracking is not enabled, but row tracking column\"))\n    }\n  }\n\n  Seq(\"none\", \"name\", \"id\").foreach(mode => {\n    test(s\"Read row tracking columns from delta-spark table with column mapping = $mode\") {\n      withTempDirAndEngine { (tablePath, _) =>\n        createRowTrackingTableWithSpark(\n          tablePath,\n          extraProps = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> mode))\n\n        val expectedAnswer = Seq(\n          TestRow(1, \"A\", 0L, 1L),\n          TestRow(2, \"B\", 1L, 1L),\n          TestRow(3, \"C_updated\", 2L, 2L),\n          TestRow(4, \"D\", 3L, 1L),\n          TestRow(5, \"E\", 4L, 1L),\n          TestRow(6, \"F\", 10L, 2L))\n\n        // We only check whether the delta-spark table schema is inferred correctly if column\n        // mapping is disabled\n        val expectedSchema = if (mode == \"none\") {\n          new StructType()\n            .add(new StructField(\"id\", IntegerType.INTEGER, true))\n            .add(new StructField(\"value\", StringType.STRING, true))\n        } else { null }\n\n        checkTable(\n          path = tablePath,\n          expectedAnswer,\n          metadataCols =\n            Seq(ROW_ID, ROW_COMMIT_VERSION),\n          expectedSchema = expectedSchema)\n      }\n    }\n  })\n\n  test(\"Read subset of row tracking columns from delta-spark table\") {\n    withTempDirAndEngine { (tablePath, _) =>\n      createRowTrackingTableWithSpark(tablePath)\n\n      val expectedAnswer = Seq(\n        TestRow(\"A\", 0L),\n        TestRow(\"B\", 1L),\n        TestRow(\"C_updated\", 2L),\n        TestRow(\"D\", 3L),\n        TestRow(\"E\", 4L),\n        TestRow(\"F\", 10L))\n\n      checkTable(\n        path = tablePath,\n        expectedAnswer,\n        readCols = Seq(\"value\"),\n        metadataCols = Seq(ROW_ID))\n    }\n  }\n\n  test(\"Only read row tracking columns from delta-spark table\") {\n    withTempDirAndEngine { (tablePath, _) =>\n      createRowTrackingTableWithSpark(tablePath)\n\n      val expectedAnswer = Seq(\n        TestRow(1L, 0L),\n        TestRow(1L, 1L),\n        TestRow(2L, 2L),\n        TestRow(1L, 3L),\n        TestRow(1L, 4L),\n        TestRow(2L, 10L))\n\n      // This test also checks a different ordering of the metadata columns\n      checkTable(\n        path = tablePath,\n        expectedAnswer,\n        readCols = Seq(),\n        metadataCols =\n          Seq(ROW_COMMIT_VERSION, ROW_ID))\n    }\n  }\n\n  test(\"Metadata columns are not read by default from delta-spark table\") {\n    withTempDirAndEngine { (tablePath, _) =>\n      createRowTrackingTableWithSpark(tablePath)\n\n      val expectedAnswer = Seq(\n        TestRow(1, \"A\"),\n        TestRow(2, \"B\"),\n        TestRow(3, \"C_updated\"),\n        TestRow(4, \"D\"),\n        TestRow(5, \"E\"),\n        TestRow(6, \"F\"))\n\n      checkTable(\n        path = tablePath,\n        expectedAnswer,\n        expectedSchema = new StructType()\n          .add(new StructField(\"id\", IntegerType.INTEGER, true))\n          .add(new StructField(\"value\", StringType.STRING, true)))\n    }\n  }\n\n  /* -------- Conflict resolution tests -------- */\n  private def validateConflictResolution(\n      engine: Engine,\n      tablePath: String,\n      dataSizeTxn1: Int,\n      dataSizeTxn2: Int,\n      useSparkTxn2: Boolean = false,\n      dataSizeTxn3: Int,\n      useSparkTxn3: Boolean = false): Unit = {\n\n    /**\n     * Txn1: the current transaction that commits later than winning transactions.\n     * Txn2: the winning transaction that was committed first.\n     * Txn3: the winning transaction that was committed second.\n     *\n     * Note tx is the timestamp.\n     *\n     * t1 ------------------------ Txn1 starts.\n     * t2 ------- Txn2 starts.\n     * t3 ------- Txn2 commits.\n     * t4 ------- Txn3 starts.\n     * t5 ------- Txn3 commits.\n     * t6 ------------------------ Txn1 commits.\n     */\n    val schema = new StructType().add(\"id\", LongType.LONG)\n\n    // Create a row-tracking-supported table and bump the row ID high watermark to the initial value\n    createTableWithRowTracking(engine, tablePath, schema)\n    val initDataSize = 100L\n    val dataBatch = generateData(schema, Seq.empty, Map.empty, initDataSize.toInt, 1)\n    val v0 = appendData(engine, tablePath, data = prepareDataForCommit(dataBatch)).getVersion\n\n    var expectedBaseRowIDs = Seq(0L)\n    var expectedDefaultRowCommitVersion = Seq(v0)\n    var expectedHighWatermark = initDataSize - 1\n\n    def verifyRowTrackingStates(): Unit = {\n      verifyBaseRowIDs(engine, tablePath, expectedBaseRowIDs)\n      verifyDefaultRowCommitVersion(engine, tablePath, expectedDefaultRowCommitVersion)\n      verifyHighWatermark(engine, tablePath, expectedHighWatermark)\n    }\n\n    verifyRowTrackingStates()\n\n    // Create txn1 but don't commit it yet\n    val txn1 = getUpdateTxn(engine, tablePath)\n\n    // Create and commit txn2\n    if (dataSizeTxn2 > 0) {\n      val v = if (useSparkTxn2) {\n        spark.range(0, dataSizeTxn2).write.format(\"delta\").mode(\"append\").save(tablePath)\n        DeltaLog.forTable(spark, new Path(tablePath)).snapshot.version\n      } else {\n        val dataBatchTxn2 = generateData(schema, Seq.empty, Map.empty, dataSizeTxn2, 1)\n        appendData(engine, tablePath, data = prepareDataForCommit(dataBatchTxn2)).getVersion\n      }\n      expectedBaseRowIDs = expectedBaseRowIDs ++ Seq(initDataSize)\n      expectedDefaultRowCommitVersion = expectedDefaultRowCommitVersion ++ Seq(v)\n      expectedHighWatermark = initDataSize + dataSizeTxn2 - 1\n    } else {\n      getUpdateTxn(engine, tablePath).commit(engine, emptyIterable())\n    }\n    verifyRowTrackingStates()\n\n    // Create and commit txn3\n    if (dataSizeTxn3 > 0) {\n      val v = if (useSparkTxn3) {\n        spark.range(0, dataSizeTxn3).write.format(\"delta\").mode(\"append\").save(tablePath)\n        DeltaLog.forTable(spark, new Path(tablePath)).snapshot.version\n      } else {\n        val dataBatchTxn3 = generateData(schema, Seq.empty, Map.empty, dataSizeTxn3, 1)\n        appendData(engine, tablePath, data = prepareDataForCommit(dataBatchTxn3)).getVersion\n      }\n      expectedBaseRowIDs = expectedBaseRowIDs ++ Seq(initDataSize + dataSizeTxn2)\n      expectedDefaultRowCommitVersion = expectedDefaultRowCommitVersion ++ Seq(v)\n      expectedHighWatermark = initDataSize + dataSizeTxn2 + dataSizeTxn3 - 1\n    } else {\n      getUpdateTxn(engine, tablePath).commit(engine, emptyIterable())\n    }\n    verifyRowTrackingStates()\n\n    // Commit txn1\n    if (dataSizeTxn1 > 0) {\n      val dataBatchTxn1 = generateData(schema, Seq.empty, Map.empty, dataSizeTxn1, 1)\n      val v = commitAppendData(engine, txn1, prepareDataForCommit(dataBatchTxn1)).getVersion\n      expectedBaseRowIDs = expectedBaseRowIDs ++ Seq(initDataSize + dataSizeTxn2 + dataSizeTxn3)\n      expectedDefaultRowCommitVersion = expectedDefaultRowCommitVersion ++ Seq(v)\n      expectedHighWatermark = initDataSize + dataSizeTxn2 + dataSizeTxn3 + dataSizeTxn1 - 1\n    } else {\n      txn1.commit(engine, emptyIterable())\n    }\n    verifyRowTrackingStates()\n  }\n\n  test(\"Conflict resolution - two concurrent txns both added new files\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      validateConflictResolution(\n        engine,\n        tablePath,\n        dataSizeTxn1 = 200,\n        dataSizeTxn2 = 300,\n        dataSizeTxn3 = 400)\n    })\n  }\n\n  test(\"Conflict resolution - only one of the two concurrent txns added new files\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      validateConflictResolution(\n        engine,\n        tablePath,\n        dataSizeTxn1 = 200,\n        dataSizeTxn2 = 300,\n        dataSizeTxn3 = 0)\n    })\n    withTempDirAndEngine((tablePath, engine) => {\n      validateConflictResolution(\n        engine,\n        tablePath,\n        dataSizeTxn1 = 200,\n        dataSizeTxn2 = 0,\n        dataSizeTxn3 = 300)\n    })\n  }\n\n  test(\"Conflict resolution - none of the two concurrent txns added new files\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      validateConflictResolution(\n        engine,\n        tablePath,\n        dataSizeTxn1 = 200,\n        dataSizeTxn2 = 0,\n        dataSizeTxn3 = 0)\n    })\n  }\n\n  test(\"Conflict resolution - the current txn didn't add new files\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      validateConflictResolution(\n        engine,\n        tablePath,\n        dataSizeTxn1 = 0,\n        dataSizeTxn2 = 200,\n        dataSizeTxn3 = 300)\n    })\n  }\n\n  test(\n    \"Conflict resolution - two concurrent txns were commited by delta-spark \" +\n      \"and both added new files\") {\n    withTempDirAndEngine((tablePath, engine) => {\n      validateConflictResolution(\n        engine,\n        tablePath,\n        dataSizeTxn1 = 200,\n        dataSizeTxn2 = 300,\n        useSparkTxn2 = true,\n        dataSizeTxn3 = 400,\n        useSparkTxn3 = true)\n    })\n  }\n\n  test(\"Conflict resolution - \" +\n    \"conflict resolution is not supported when providedRowIdHighWatermark is set\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      // Create txn1 but don't commit it yet\n      val rowTrackingMetadataDomainTxn1 = new RowTrackingMetadataDomain(400)\n      val txn1 = createTxnWithDomainMetadatas(\n        engine,\n        tablePath,\n        List(rowTrackingMetadataDomainTxn1.toDomainMetadata))\n\n      // Create and commit txn2\n      val dataBatch2 = generateData(testSchema, Seq.empty, Map.empty, 100, 1)\n      val commitVersion2 = appendData(\n        engine,\n        tablePath,\n        data = Seq(dataBatch2).map(Map.empty[String, Literal] -> _)).getVersion\n      verifyBaseRowIDs(engine, tablePath, Seq(0L))\n      verifyDefaultRowCommitVersion(engine, tablePath, Seq(commitVersion2))\n      verifyHighWatermark(engine, tablePath, 99)\n\n      // Commit txn1 with a provided row ID high watermark would fail\n      intercept[MaxCommitRetryLimitReachedException] {\n        txn1.commit(engine, emptyIterable())\n      }\n    }\n  }\n\n  private val ROW_TRACKING_ENABLED_PROP = Map(TableConfig.ROW_TRACKING_ENABLED.getKey -> \"true\")\n  private val ROW_TRACKING_DISABLED_PROP = Map(TableConfig.ROW_TRACKING_ENABLED.getKey -> \"false\")\n\n  test(\"row tracking can be enabled/disabled on new table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      getCreateTxn(\n        engine,\n        tablePath,\n        schema = testSchema,\n        tableProperties = ROW_TRACKING_ENABLED_PROP).commit(engine, emptyIterable())\n      val snapshot =\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(snapshot, TableConfig.ROW_TRACKING_ENABLED, true)\n    }\n\n    withTempDirAndEngine { (tablePath, engine) =>\n      getCreateTxn(\n        engine,\n        tablePath,\n        schema = testSchema,\n        tableProperties = ROW_TRACKING_DISABLED_PROP).commit(engine, emptyIterable())\n      val snapshot =\n        getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n      assertMetadataProp(snapshot, TableConfig.ROW_TRACKING_ENABLED, false)\n    }\n  }\n\n  test(\"row tracking cannot be enabled on existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a new table with row tracking disabled (it is disabled by default)\n      getCreateTxn(engine, tablePath, testSchema, tableProperties = Map.empty)\n        .commit(engine, emptyIterable())\n\n      // Fail if try to enable row tracking on an existing table\n      val e = intercept[KernelException] {\n        getUpdateTxn(engine, tablePath, tableProperties = ROW_TRACKING_ENABLED_PROP)\n          .commit(engine, emptyIterable())\n      }\n      assert(\n        e.getMessage.contains(\"Row tracking support cannot be changed once the table is created\"))\n\n      // It's okay to continue setting it disabled on an existing table; it will be a no-op\n      getUpdateTxn(engine, tablePath, tableProperties = ROW_TRACKING_DISABLED_PROP)\n        .commit(engine, emptyIterable())\n    }\n  }\n\n  test(\"row tracking cannot be disabled on existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a new table with row tracking enabled\n      createTableWithRowTracking(engine, tablePath)\n\n      // Fail if try to disable row tracking on an existing table\n      val e = intercept[KernelException] {\n        getUpdateTxn(engine, tablePath, tableProperties = ROW_TRACKING_DISABLED_PROP)\n          .commit(engine, emptyIterable())\n      }\n      assert(\n        e.getMessage.contains(\"Row tracking support cannot be changed once the table is created\"))\n\n      // It's okay to continue setting it enabled on an existing table; it will be a no-op\n      getUpdateTxn(engine, tablePath, tableProperties = ROW_TRACKING_ENABLED_PROP)\n        .commit(engine, emptyIterable())\n    }\n  }\n\n  test(\"materialized row tracking column names are assigned when the feature is enabled\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n      val config = getMetadata(engine, tablePath).getConfiguration\n\n      Seq(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION).foreach { rowTrackingColumn =>\n        assert(config.containsKey(rowTrackingColumn.getMaterializedColumnNameProperty))\n        assert(\n          config\n            .get(rowTrackingColumn.getMaterializedColumnNameProperty)\n            .startsWith(rowTrackingColumn.getMaterializedColumnNamePrefix))\n      }\n    }\n  }\n\n  Seq(\"none\", \"name\", \"id\").foreach(mode => {\n    test(\n      s\"throw if materialized row tracking column name conflicts with schema, \" +\n        s\"with column mapping = $mode\") {\n      withTempDirAndEngine {\n        (tablePath, engine) =>\n          // Create a new table with row tracking and specified column mapping mode\n          val columnMappingProp = Map(TableConfig.COLUMN_MAPPING_MODE.getKey -> mode)\n          createTableWithRowTracking(engine, tablePath, extraProps = columnMappingProp)\n          val config = getMetadata(engine, tablePath).getConfiguration\n\n          Seq(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION).foreach {\n            rowTrackingColumn =>\n              val colName =\n                config.get(rowTrackingColumn.getMaterializedColumnNameProperty)\n\n              val newSchema = testSchema.add(colName, LongType.LONG)\n              val e = intercept[KernelException] {\n                updateTableMetadata(engine, tablePath, schema = newSchema)\n              }\n\n              if (mode == \"none\") {\n                assert(\n                  e.getMessage\n                    .contains(s\"Cannot update schema for table when column mapping is disabled\"))\n              } else {\n                assert(\n                  e.getMessage.contains(\n                    s\"Cannot use column name '$colName' because it is reserved for internal use\"))\n              }\n          }\n      }\n    }\n  })\n\n  test(\"manually setting materialized row tracking column names is not allowed - new table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      Seq(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION).foreach { rowTrackingColumn =>\n        val propName = rowTrackingColumn.getMaterializedColumnNameProperty\n        val customTableProps = Map(propName -> \"custom_name\")\n        val e = intercept[KernelException] {\n          createTableWithRowTracking(engine, tablePath, extraProps = customTableProps)\n        }\n        assert(e.getMessage.contains(\n          s\"The Delta table property '$propName' is an internal property and cannot be updated\"))\n      }\n    }\n  }\n\n  test(\"manually setting materialized row tracking column names is not allowed - existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      createTableWithRowTracking(engine, tablePath)\n\n      Seq(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION).foreach { rowTrackingColumn =>\n        val propName = rowTrackingColumn.getMaterializedColumnNameProperty\n        val customTableProps = Map(propName -> \"custom_name\")\n        val e = intercept[KernelException] {\n          getUpdateTxn(engine, tablePath, tableProperties = customTableProps)\n            .commit(engine, emptyIterable())\n        }\n        assert(e.getMessage.contains(\n          s\"The Delta table property '$propName' is an internal property and cannot be updated\"))\n      }\n    }\n  }\n\n  test(\"throw if materialized row tracking column configs are missing on an existing table\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a normal table with row tracking enabled first\n      createTableWithRowTracking(engine, tablePath)\n\n      // Get the current metadata and manually remove row tracking materialized column configs\n      val originalMetadata = getMetadata(engine, tablePath)\n      val configWithoutMaterializedCols = originalMetadata.getConfiguration.asScala.toMap\n        .filterNot {\n          case (key, _) =>\n            key == MATERIALIZED_ROW_ID.getMaterializedColumnNameProperty ||\n            key == MATERIALIZED_ROW_COMMIT_VERSION.getMaterializedColumnNameProperty\n        }\n\n      // Create new metadata with row tracking enabled but configs missing\n      val newMetadata =\n        originalMetadata.withReplacedConfiguration(configWithoutMaterializedCols.asJava)\n\n      // Manually commit this problematic metadata\n      val txn = getUpdateTxn(engine, tablePath)\n      val metadataAction = SingleAction.createMetadataSingleAction(newMetadata.toRow)\n      commitTransaction(\n        txn,\n        engine,\n        inMemoryIterable(toCloseableIterator(Seq(metadataAction).asJava.iterator())))\n\n      // Verify that row tracking is enabled but configs are missing\n      val metadata = getMetadata(engine, tablePath)\n      assert(TableConfig.ROW_TRACKING_ENABLED.fromMetadata(metadata) == true)\n      assert(!metadata.getConfiguration.containsKey(\n        MATERIALIZED_ROW_ID.getMaterializedColumnNameProperty))\n      assert(\n        !metadata.getConfiguration\n          .containsKey(MATERIALIZED_ROW_COMMIT_VERSION.getMaterializedColumnNameProperty))\n\n      // Now try to perform an append operation on this existing table with missing configs\n      // This should trigger the validation and throw the expected exception\n      val e = intercept[InvalidTableException] {\n        val dataBatch = generateData(testSchema, Seq.empty, Map.empty, 10, 1)\n        appendData(engine, tablePath, data = prepareDataForCommit(dataBatch))\n      }\n\n      assert(\n        e.getMessage.contains(\n          s\"Row tracking is enabled but the materialized column name \" +\n            s\"`${MATERIALIZED_ROW_ID.getMaterializedColumnNameProperty}` is missing.\"))\n    }\n  }\n\n  /* -------- Test row tracking with replace table -------- */\n  val someData = Seq(Map.empty[String, Literal] -> dataBatches1)\n  val otherData = Seq(Map.empty[String, Literal] -> dataBatches2)\n  // Each tuple represents: (enableBefore, enableAfter, initialData, replaceData)\n  val replaceTableTestCases = Seq(\n    // Row tracking turned on\n    (false, true, Seq(), Seq()),\n    (false, true, Seq(), someData),\n    (false, true, someData, Seq()),\n    (false, true, someData, otherData),\n    // Row tracking turned off\n    (true, false, someData, otherData),\n    (true, false, someData, Seq()),\n    // Row tracking remains unchanged\n    (true, true, someData, Seq()),\n    (true, true, Seq(), someData),\n    (true, true, someData, otherData),\n    (true, true, Seq(), Seq()))\n\n  for ((enableBefore, enableAfter, initialData, replaceData) <- replaceTableTestCases) {\n    val testName = s\"\"\"Replace table with row tracking:\n                      |enableBefore=$enableBefore,\n                      |enableAfter=$enableAfter,\n                      |initialData=${initialData.nonEmpty},\n                      |replaceData=${replaceData.nonEmpty}\"\"\"\n      .stripMargin\n      .replace(\"\\n\", \" \")\n\n    test(testName) {\n      withTempDirAndEngine { (tablePath, engine) =>\n        // Create an empty table with row tracking enabled or disabled\n        createEmptyTable(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = Map(TableConfig.ROW_TRACKING_ENABLED.getKey -> enableBefore.toString))\n\n        // Optionally fill the table with initial data if provided\n        if (initialData.nonEmpty) {\n          appendData(engine, tablePath, data = initialData)\n        }\n\n        val beforeSnapshot =\n          getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n\n        // Create a REPLACE transaction and commit\n        val replaceTableProps =\n          Map(TableConfig.ROW_TRACKING_ENABLED.getKey -> enableAfter.toString)\n        val txn = getReplaceTxn(\n          engine,\n          tablePath,\n          testSchema,\n          tableProperties = replaceTableProps)\n\n        commitTransaction(txn, engine, getAppendActions(txn, replaceData))\n\n        // Get the latest snapshot of the table after the replace operation\n        val afterSnapshot =\n          getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).asInstanceOf[SnapshotImpl]\n\n        // Assert that row tracking is enabled/disabled as expected\n        assertMetadataProp(afterSnapshot, TableConfig.ROW_TRACKING_ENABLED, enableAfter)\n\n        // Assert that the high watermark is preserved or incremented based on the operations\n        // This only applies if row tracking is enabled before and after the replace operation\n        // and if there is initial data present\n        if (enableBefore && enableAfter && initialData.nonEmpty) {\n          val beforeHighWaterMark: Optional[Long] =\n            RowTrackingMetadataDomain.fromSnapshot(beforeSnapshot).map(_.getRowIdHighWaterMark)\n          val afterHighWaterMark: Optional[Long] =\n            RowTrackingMetadataDomain.fromSnapshot(afterSnapshot).map(_.getRowIdHighWaterMark)\n          val numInitialRows = initialData.head._2.map(_.getData.getSize).sum\n\n          assert(beforeHighWaterMark.get() == numInitialRows - 1)\n          if (replaceData.nonEmpty) {\n            // If replace data is provided, the high watermark should be incremented\n            val numReplaceRows = replaceData.head._2.map(_.getData.getSize).sum\n            assert(afterHighWaterMark.get() == numInitialRows + numReplaceRows - 1)\n          } else {\n            // If no replace data, the high watermark should remain the same\n            assert(beforeHighWaterMark.get() == afterHighWaterMark.get())\n          }\n        }\n\n        // Assert that metadata configurations are different before and after\n        // Since REPLACE assigns new materialized column names, the configs should never match\n        val beforeConfig = beforeSnapshot.getMetadata.getConfiguration\n        val afterConfig = afterSnapshot.getMetadata.getConfiguration\n        assert(!beforeConfig.equals(afterConfig))\n\n        // Assert that materialized row tracking columns are present when row tracking is enabled\n        if (enableAfter) {\n          Seq(MATERIALIZED_ROW_ID, MATERIALIZED_ROW_COMMIT_VERSION).foreach { rowTrackingColumn =>\n            assert(afterConfig.containsKey(rowTrackingColumn.getMaterializedColumnNameProperty))\n            assert(\n              afterConfig\n                .get(rowTrackingColumn.getMaterializedColumnNameProperty)\n                .startsWith(rowTrackingColumn.getMaterializedColumnNamePrefix))\n          }\n        }\n\n        // Check that AddFile actions in the new table have a base row ID and default commit version\n        if (replaceData.nonEmpty) {\n          // Base row IDs do not start from 0 if we had initial data and row tracking was enabled\n          val baseRowIds = if (initialData.nonEmpty && enableBefore) {\n            initialData.head._2.map(_.getData.getSize).sum\n          } else {\n            0L\n          }\n          // There was one more previous commit if initialData was present\n          val defaultCommitVersion = if (initialData.nonEmpty) 2 else 1\n\n          verifyBaseRowIDs(engine, tablePath, Seq(baseRowIds))\n          verifyDefaultRowCommitVersion(engine, tablePath, Seq(defaultCommitVersion))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/ScanSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.math.{BigDecimal => JBigDecimal}\nimport java.sql.Date\nimport java.time.{Instant, OffsetDateTime}\nimport java.time.temporal.ChronoUnit\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.{Scan, Snapshot, Table}\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, FilteredColumnarBatch, Row}\nimport io.delta.kernel.defaults.engine.{DefaultEngine, DefaultJsonHandler, DefaultParquetHandler}\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.defaults.internal.data.vector.{DefaultGenericVector, DefaultStructVector}\nimport io.delta.kernel.defaults.utils.{ExpressionTestUtils, TestUtils, WriteUtils}\nimport io.delta.kernel.engine.{Engine, JsonHandler, ParquetHandler}\nimport io.delta.kernel.engine.FileReadResult\nimport io.delta.kernel.exceptions.KernelEngineException\nimport io.delta.kernel.expressions._\nimport io.delta.kernel.expressions.Literal._\nimport io.delta.kernel.internal.{InternalScanFileUtils, ScanImpl, TableConfig}\nimport io.delta.kernel.internal.util.InternalUtils\nimport io.delta.kernel.types._\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.StringType.STRING\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog}\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.spark.sql.{Row => SparkRow}\nimport org.apache.spark.sql.catalyst.plans.SQLHelper\nimport org.apache.spark.sql.types.{IntegerType => SparkIntegerType, StructField => SparkStructField, StructType => SparkStructType}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ScanSuite extends AnyFunSuite with TestUtils\n    with ExpressionTestUtils with SQLHelper with WriteUtils {\n\n  import io.delta.kernel.defaults.ScanSuite._\n\n  // scalastyle:off sparkimplicits\n  import spark.implicits._\n  // scalastyle:on sparkimplicits\n\n  private def getDataSkippingConfs(\n      indexedCols: Option[Int],\n      deltaStatsColNamesOpt: Option[String]): Seq[(String, String)] = {\n    val numIndexedColsConfOpt = indexedCols\n      .map(DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.defaultTablePropertyKey -> _.toString)\n    val indexedColNamesConfOpt = deltaStatsColNamesOpt\n      .map(DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.defaultTablePropertyKey -> _)\n    (numIndexedColsConfOpt ++ indexedColNamesConfOpt).toSeq\n  }\n\n  def writeDataSkippingTable(\n      tablePath: String,\n      data: String,\n      schema: SparkStructType,\n      indexedCols: Option[Int],\n      deltaStatsColNamesOpt: Option[String]): Unit = {\n    withSQLConf(getDataSkippingConfs(indexedCols, deltaStatsColNamesOpt): _*) {\n      val jsonRecords = data.split(\"\\n\").toSeq\n      val reader = spark.read\n      if (schema != null) { reader.schema(schema) }\n      val df = reader.json(jsonRecords.toDS())\n\n      val r = DeltaLog.forTable(spark, tablePath)\n      df.coalesce(1).write.format(\"delta\").save(r.dataPath.toString)\n    }\n  }\n\n  private def getScanFileStats(scanFiles: Seq[Row]): Seq[String] = {\n    scanFiles.map { scanFile =>\n      val addFile = scanFile.getStruct(scanFile.getSchema.indexOf(\"add\"))\n      if (scanFile.getSchema.indexOf(\"stats\") >= 0) {\n        addFile.getString(scanFile.getSchema.indexOf(\"stats\"))\n      } else {\n        \"[No stats read]\"\n      }\n    }\n  }\n\n  /**\n   * @param tablePath the table to scan\n   * @param hits query filters that should yield at least one scan file\n   * @param misses query filters that should yield no scan files\n   */\n  def checkSkipping(tablePath: String, hits: Seq[Predicate], misses: Seq[Predicate]): Unit = {\n    val snapshot = latestSnapshot(tablePath)\n    hits.foreach { predicate =>\n      val scanFiles = collectScanFileRows(\n        snapshot.getScanBuilder().withFilter(predicate).build())\n      assert(scanFiles.nonEmpty, s\"Expected hit but got miss for $predicate\")\n    }\n    misses.foreach { predicate =>\n      val scanFiles = collectScanFileRows(\n        snapshot.getScanBuilder()\n          .withFilter(predicate)\n          .build())\n      assert(\n        scanFiles.isEmpty,\n        s\"Expected miss but got hit for $predicate\\n\" +\n          s\"Returned scan files have stats: ${getScanFileStats(scanFiles)}\")\n    }\n  }\n\n  /**\n   * @param tablePath the table to scan\n   * @param filterToNumExpFiles map of {predicate -> number of expected scan files}\n   */\n  def checkSkipping(tablePath: String, filterToNumExpFiles: Map[Predicate, Int]): Unit = {\n    val snapshot = latestSnapshot(tablePath)\n    filterToNumExpFiles.foreach { case (filter, numExpFiles) =>\n      val scanFiles = collectScanFileRows(\n        snapshot.getScanBuilder().withFilter(filter).build())\n      assert(\n        scanFiles.length == numExpFiles,\n        s\"Expected $numExpFiles but found ${scanFiles.length} for $filter\")\n    }\n  }\n\n  def testSkipping(\n      testName: String,\n      data: String,\n      schema: SparkStructType = null,\n      hits: Seq[Predicate],\n      misses: Seq[Predicate],\n      indexedCols: Option[Int] = None,\n      deltaStatsColNamesOpt: Option[String] = None): Unit = {\n    test(testName) {\n      withTempDir { tempDir =>\n        writeDataSkippingTable(\n          tempDir.getCanonicalPath,\n          data,\n          schema,\n          indexedCols,\n          deltaStatsColNamesOpt)\n        checkSkipping(\n          tempDir.getCanonicalPath,\n          hits,\n          misses)\n      }\n    }\n  }\n\n  /* Where timestampStr is in the format of \"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\" */\n  def getTimestampPredicate(\n      expr: String,\n      col: Column,\n      timestampStr: String,\n      timeStampType: String): Predicate = {\n    val time = OffsetDateTime.parse(timestampStr)\n    new Predicate(\n      expr,\n      col,\n      if (timeStampType.equalsIgnoreCase(\"timestamp\")) {\n        ofTimestamp(ChronoUnit.MICROS.between(Instant.EPOCH, time))\n      } else {\n        ofTimestampNtz(ChronoUnit.MICROS.between(Instant.EPOCH, time))\n      })\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Skipping tests from Spark's DataSkippingDeltaTests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  testSkipping(\n    \"data skipping - top level, single 1\",\n    \"\"\"{\"a\": 1}\"\"\",\n    hits = Seq(\n      AlwaysTrue.ALWAYS_TRUE, // trivial base case\n      equals(col(\"a\"), ofInt(1)), // a = 1\n      equals(ofInt(1), col(\"a\")), // 1 = a\n      greaterThanOrEqual(col(\"a\"), ofInt(1)), // a >= 1\n      lessThanOrEqual(col(\"a\"), ofInt(1)), // a <= 1\n      lessThanOrEqual(col(\"a\"), ofInt(2)), // a <= 2\n      greaterThanOrEqual(col(\"a\"), ofInt(0)), // a >= 0\n      lessThanOrEqual(ofInt(1), col(\"a\")), // 1 <= a\n      greaterThanOrEqual(ofInt(1), col(\"a\")), // 1 >= a\n      greaterThanOrEqual(ofInt(2), col(\"a\")), // 2 >= a\n      lessThanOrEqual(ofInt(0), col(\"a\")), // 0 <= a\n      // note <=> is not supported yet but these should still be hits once supported\n      nullSafeEquals(col(\"a\"), ofInt(1)), // a <=> 1\n      nullSafeEquals(ofInt(1), col(\"a\")), // 1 <=> a\n      not(nullSafeEquals(col(\"a\"), ofInt(2))), // NOT a <=> 2\n      // MOVE BELOW EXPRESSIONS TO MISSES ONCE SUPPORTED BY DATA SKIPPING\n      notEquals(col(\"a\"), ofInt(1)), // a != 1\n      notEquals(ofInt(1), col(\"a\")) // 1 != a\n    ),\n    misses = Seq(\n      equals(col(\"a\"), ofInt(2)), // a = 2\n      equals(ofInt(2), col(\"a\")), // 2 = a\n      greaterThan(col(\"a\"), ofInt(1)), // a > 1\n      lessThan(col(\"a\"), ofInt(1)), // a  < 1\n      greaterThanOrEqual(col(\"a\"), ofInt(2)), // a >= 2\n      lessThanOrEqual(col(\"a\"), ofInt(0)), // a <= 0\n      lessThan(ofInt(1), col(\"a\")), // 1 < a\n      greaterThan(ofInt(1), col(\"a\")), // 1 > a\n      lessThanOrEqual(ofInt(2), col(\"a\")), // 2 <= a\n      greaterThanOrEqual(ofInt(0), col(\"a\")), // 0 >= a\n      not(equals(col(\"a\"), ofInt(1))), // NOT a = 1\n      not(equals(ofInt(1), col(\"a\"))), // NOT 1 = a\n      not(nullSafeEquals(col(\"a\"), ofInt(1))), // NOT a <=> 1\n      not(nullSafeEquals(ofInt(1), col(\"a\"))), // NOT 1 <=> a\n      nullSafeEquals(ofInt(2), col(\"a\")), // 2 <=> a\n      nullSafeEquals(col(\"a\"), ofInt(2)) // a <=> 2\n    ))\n\n  testSkipping(\n    \"data skipping - nested, single 1\",\n    \"\"\"{\"a\": {\"b\": 1}}\"\"\",\n    hits = Seq(\n      equals(nestedCol(\"a.b\"), ofInt(1)), // a.b = 1\n      greaterThanOrEqual(nestedCol(\"a.b\"), ofInt(1)), // a.b >= 1\n      lessThanOrEqual(nestedCol(\"a.b\"), ofInt(1)), // a.b <= 1\n      lessThanOrEqual(nestedCol(\"a.b\"), ofInt(2)), // a.b <= 2\n      greaterThanOrEqual(nestedCol(\"a.b\"), ofInt(0)) // a.b >= 0\n    ),\n    misses = Seq(\n      equals(nestedCol(\"a.b\"), ofInt(2)), // a.b = 2\n      greaterThan(nestedCol(\"a.b\"), ofInt(1)), // a.b > 1\n      lessThan(nestedCol(\"a.b\"), ofInt(1)) // a.b < 1\n    ))\n\n  testSkipping(\n    \"data skipping - double nested, single 1\",\n    \"\"\"{\"a\": {\"b\": {\"c\": 1}}}\"\"\",\n    hits = Seq(\n      equals(nestedCol(\"a.b.c\"), ofInt(1)), // a.b.c = 1\n      greaterThanOrEqual(nestedCol(\"a.b.c\"), ofInt(1)), // a.b.c >= 1\n      lessThanOrEqual(nestedCol(\"a.b.c\"), ofInt(1)), // a.b.c <= 1\n      lessThanOrEqual(nestedCol(\"a.b.c\"), ofInt(2)), // a.b.c <= 2\n      greaterThanOrEqual(nestedCol(\"a.b.c\"), ofInt(0)) // a.b.c >= 0\n    ),\n    misses = Seq(\n      equals(nestedCol(\"a.b.c\"), ofInt(2)), // a.b.c = 2\n      greaterThan(nestedCol(\"a.b.c\"), ofInt(1)), // a.b.c > 1\n      lessThan(nestedCol(\"a.b.c\"), ofInt(1)) // a.b.c < 1\n    ))\n\n  private def longString(str: String) = str * 1000\n\n  testSkipping(\n    \"data skipping - long strings - long min\",\n    s\"\"\"\n       {\"a\": '${longString(\"A\")}'}\n       {\"a\": 'B'}\n       {\"a\": 'C'}\n     \"\"\",\n    hits = Seq(\n      equals(col(\"a\"), ofString(longString(\"A\"))),\n      greaterThan(col(\"a\"), ofString(\"BA\")),\n      lessThan(col(\"a\"), ofString(\"AB\")),\n      // note startsWith is not supported yet but these should still be hits once supported\n      startsWith(col(\"a\"), ofString(\"A\")) // a like 'A%'\n    ),\n    misses = Seq(\n      lessThan(col(\"a\"), ofString(\"AA\")),\n      greaterThan(col(\"a\"), ofString(\"CD\"))))\n\n  testSkipping(\n    \"data skipping - long strings - long max\",\n    s\"\"\"\n       {\"a\": 'A'}\n       {\"a\": 'B'}\n       {\"a\": '${longString(\"C\")}'}\n     \"\"\",\n    hits = Seq(\n      equals(col(\"a\"), ofString(longString(\"C\"))),\n      greaterThan(col(\"a\"), ofString(\"BA\")),\n      lessThan(col(\"a\"), ofString(\"AB\")),\n      greaterThan(col(\"a\"), ofString(\"CC\")),\n      // note startsWith is not supported yet but these should still be hits once supported\n      startsWith(col(\"a\"), ofString(\"A\")), // a like 'A%'\n      startsWith(col(\"a\"), ofString(\"C\")) // a like 'C%'\n    ),\n    misses = Seq(\n      greaterThanOrEqual(col(\"a\"), ofString(\"D\")),\n      greaterThan(col(\"a\"), ofString(\"CD\"))))\n\n  // Test:'starts with'  Expression: like\n  // Test:'starts with, nested'  Expression: like\n\n  testSkipping(\n    \"data skipping - and statements - simple\",\n    \"\"\"\n      {\"a\": 1}\n      {\"a\": 2}\n    \"\"\",\n    hits = Seq(\n      new And(\n        greaterThan(col(\"a\"), ofInt(0)),\n        lessThan(col(\"a\"), ofInt(3))),\n      new And(\n        lessThanOrEqual(col(\"a\"), ofInt(1)),\n        greaterThan(col(\"a\"), ofInt(-1)))),\n    misses = Seq(\n      new And(\n        lessThan(col(\"a\"), ofInt(0)),\n        greaterThan(col(\"a\"), ofInt(-2)))))\n\n  testSkipping(\n    \"data skipping - and statements - two fields\",\n    \"\"\"\n      {\"a\": 1, \"b\": \"2017-09-01\"}\n      {\"a\": 2, \"b\": \"2017-08-31\"}\n    \"\"\",\n    hits = Seq(\n      new And(\n        greaterThan(col(\"a\"), ofInt(0)),\n        equals(col(\"b\"), ofString(\"2017-09-01\"))),\n      new And(\n        equals(col(\"a\"), ofInt(2)),\n        greaterThanOrEqual(col(\"b\"), ofString(\"2017-08-30\"))),\n      // note startsWith is not supported yet but these should still be hits once supported\n      new And( //  a >= 2 AND b like '2017-08-%'\n        greaterThanOrEqual(col(\"a\"), ofInt(2)),\n        startsWith(col(\"b\"), ofString(\"2017-08-\"))),\n      // MOVE BELOW EXPRESSION TO MISSES ONCE SUPPORTED BY DATA SKIPPING\n      new And( // a > 0 AND b like '2016-%'\n        greaterThan(col(\"a\"), ofInt(0)),\n        startsWith(col(\"b\"), ofString(\"2016-\")))),\n    misses = Seq())\n\n  private val aRem100 = new ScalarExpression(\"%\", Seq(col(\"a\"), ofInt(100)).asJava)\n  private val bRem100 = new ScalarExpression(\"%\", Seq(col(\"b\"), ofInt(100)).asJava)\n\n  testSkipping(\n    \"data skipping - and statements - one side unsupported\",\n    \"\"\"\n      {\"a\": 10, \"b\": 10}\n      {\"a\": 20: \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      // a % 100 < 10 AND b % 100 > 20\n      new And(lessThan(aRem100, ofInt(10)), greaterThan(bRem100, ofInt(20)))),\n    misses = Seq(\n      // a < 10 AND b % 100 > 20\n      new And(lessThan(col(\"a\"), ofInt(10)), greaterThan(bRem100, ofInt(20))),\n      // a % 100 < 10 AND b > 20\n      new And(lessThan(aRem100, ofInt(10)), greaterThan(col(\"b\"), ofInt(20)))))\n\n  testSkipping(\n    \"data skipping - or statements - simple\",\n    \"\"\"\n      {\"a\": 1}\n      {\"a\": 2}\n    \"\"\",\n    hits = Seq(\n      // a > 0 or a < -3\n      new Or(greaterThan(col(\"a\"), ofInt(0)), lessThan(col(\"a\"), ofInt(-3))),\n      // a >= 2 or a < -1\n      new Or(greaterThanOrEqual(col(\"a\"), ofInt(2)), lessThan(col(\"a\"), ofInt(-1)))),\n    misses = Seq(\n      // a > 5 or a < -2\n      new Or(greaterThan(col(\"a\"), ofInt(5)), lessThan(col(\"a\"), ofInt(-2)))))\n\n  testSkipping(\n    \"data skipping - or statements - two fields\",\n    \"\"\"\n      {\"a\": 1, \"b\": \"2017-09-01\"}\n      {\"a\": 2, \"b\": \"2017-08-31\"}\n    \"\"\",\n    hits = Seq(\n      new Or(\n        lessThan(col(\"a\"), ofInt(0)),\n        equals(col(\"b\"), ofString(\"2017-09-01\"))),\n      new Or(\n        equals(col(\"a\"), ofInt(2)),\n        lessThan(col(\"b\"), ofString(\"2017-08-30\"))),\n      // note startsWith is not supported yet but these should still be hits once supported\n      new Or( //  a < 2 or b like '2017-08-%'\n        lessThan(col(\"a\"), ofInt(2)),\n        startsWith(col(\"b\"), ofString(\"2017-08-\"))),\n      new Or( //  a >= 2 or b like '2016-08-%'\n        greaterThanOrEqual(col(\"a\"), ofInt(2)),\n        startsWith(col(\"b\"), ofString(\"2016-08-\"))),\n      // MOVE BELOW EXPRESSION TO MISSES ONCE SUPPORTED BY DATA SKIPPING\n      new Or( // a < 0 or b like '2016-%'\n        lessThan(col(\"a\"), ofInt(0)),\n        startsWith(col(\"b\"), ofString(\"2016-\")))),\n    misses = Seq())\n\n  // One side of OR by itself isn't powerful enough to prune any files.\n  testSkipping(\n    \"data skipping - or statements - one side unsupported\",\n    \"\"\"\n      {\"a\": 10, \"b\": 10}\n      {\"a\": 20: \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      // a % 100 < 10 OR b > 20\n      new Or(lessThan(aRem100, ofInt(10)), greaterThan(col(\"b\"), ofInt(20))),\n      // a < 10 OR b % 100 > 20\n      new Or(lessThan(col(\"a\"), ofInt(10)), greaterThan(bRem100, ofInt(20)))),\n    misses = Seq(\n      // a < 10 OR b > 20\n      new Or(lessThan(col(\"a\"), ofInt(10)), greaterThan(col(\"b\"), ofInt(20)))))\n\n  testSkipping(\n    \"data skipping - not statements - simple\",\n    \"\"\"\n      {\"a\": 1}\n      {\"a\": 2}\n    \"\"\",\n    hits = Seq(\n      not(lessThan(col(\"a\"), ofInt(0)))),\n    misses = Seq(\n      not(greaterThan(col(\"a\"), ofInt(0))),\n      not(lessThan(col(\"a\"), ofInt(3))),\n      not(greaterThanOrEqual(col(\"a\"), ofInt(1))),\n      not(lessThanOrEqual(col(\"a\"), ofInt(2))),\n      not(not(lessThan(col(\"a\"), ofInt(0)))),\n      not(not(equals(col(\"a\"), ofInt(3))))))\n\n  // NOT(AND(a, b)) === OR(NOT(a), NOT(b)) ==> One side by itself cannot prune.\n  testSkipping(\n    \"data skipping - not statements - and\",\n    \"\"\"\n      {\"a\": 10, \"b\": 10}\n      {\"a\": 20: \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      not(\n        new And(\n          greaterThanOrEqual(aRem100, ofInt(10)),\n          lessThanOrEqual(bRem100, ofInt(20)))),\n      not(\n        new And(\n          greaterThanOrEqual(col(\"a\"), ofInt(10)),\n          lessThanOrEqual(bRem100, ofInt(20)))),\n      not(\n        new And(\n          greaterThanOrEqual(aRem100, ofInt(10)),\n          lessThanOrEqual(col(\"b\"), ofInt(20))))),\n    misses = Seq(\n      not(\n        new And(\n          greaterThanOrEqual(col(\"a\"), ofInt(10)),\n          lessThanOrEqual(col(\"b\"), ofInt(20))))))\n\n  // NOT(OR(a, b)) === AND(NOT(a), NOT(b)) => One side by itself is enough to prune.\n  testSkipping(\n    \"data skipping - not statements - or\",\n    \"\"\"\n      {\"a\": 1, \"b\": 10}\n      {\"a\": 2, \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      // NOT(a < 1 OR b > 20),\n      not(new Or(lessThan(col(\"a\"), ofInt(1)), greaterThan(col(\"b\"), ofInt(20)))),\n      // NOT(a % 100 >= 1 OR b % 100 <= 20)\n      not(new Or(greaterThanOrEqual(aRem100, ofInt(1)), lessThanOrEqual(bRem100, ofInt(20))))),\n    misses = Seq(\n      // NOT(a >= 1 OR b <= 20)\n      not(\n        new Or(greaterThanOrEqual(col(\"a\"), ofInt(1)), lessThanOrEqual(col(\"b\"), ofInt(20)))),\n      // NOT(a % 100 >= 1 OR b <= 20),\n      not(\n        new Or(greaterThanOrEqual(aRem100, ofInt(1)), lessThanOrEqual(col(\"b\"), ofInt(20)))),\n      // NOT(a >= 1 OR b % 100 <= 20)\n      not(\n        new Or(greaterThanOrEqual(col(\"a\"), ofInt(1)), lessThanOrEqual(bRem100, ofInt(20))))))\n\n  // If a column does not have stats, it does not participate in data skipping, which disqualifies\n  // that leg of whatever conjunct it was part of.\n  testSkipping(\n    \"data skipping - missing stats columns\",\n    \"\"\"\n      {\"a\": 1, \"b\": 10}\n      {\"a\": 2, \"b\": 20}\n    \"\"\",\n    indexedCols = Some(1),\n    hits = Seq(\n      lessThan(col(\"b\"), ofInt(10)), // b < 10: disqualified\n      // note OR is not supported yet but these should still be hits once supported\n      new Or( // a < 1 OR b < 10: a disqualified by b (same conjunct)\n        lessThan(col(\"a\"), ofInt(1)),\n        lessThan(col(\"b\"), ofInt(10))),\n      new Or( // a < 1 OR (a >= 1 AND b < 10): ==> a < 1 OR a >=1 ==> TRUE\n        lessThan(col(\"a\"), ofInt(1)),\n        new And(greaterThanOrEqual(col(\"a\"), ofInt(1)), lessThan(col(\"b\"), ofInt(10))))),\n    misses = Seq(\n      new And( // a < 1 AND b < 10: ==> a < 1 ==> FALSE\n        lessThan(col(\"a\"), ofInt(1)),\n        lessThan(col(\"b\"), ofInt(10))),\n      new Or( // a < 1 OR (a > 10 AND b < 10): ==> a < 1 OR a > 10 ==> FALSE\n        lessThan(col(\"a\"), ofInt(1)),\n        new And(greaterThan(col(\"a\"), ofInt(10)), lessThan(col(\"b\"), ofInt(10))))))\n\n  private def generateJsonData(numCols: Int): String = {\n    val fields = (0 until numCols).map(i => s\"\"\"\"col${\"%02d\".format(i)}\":$i\"\"\".stripMargin)\n\n    \"{\" + fields.mkString(\",\") + \"}\"\n  }\n\n  testSkipping(\n    \"data-skipping - more columns than indexed\",\n    generateJsonData(33), // defaultNumIndexedCols + 1\n    hits = Seq(\n      equals(col(\"col00\"), ofInt(0)),\n      equals(col(\"col32\"), ofInt(32)),\n      equals(col(\"col32\"), ofInt(-1))),\n    misses = Seq(\n      equals(col(\"col00\"), ofInt(1))))\n\n  testSkipping(\n    \"data skipping - nested schema - # indexed column = 3\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    indexedCols = Some(3),\n    hits = Seq(\n      equals(col(\"a\"), ofInt(1)), // a = 1\n      equals(nestedCol(\"b.c.d\"), ofInt(2)), // b.c.d = 2\n      equals(nestedCol(\"b.c.e\"), ofInt(3)), // b.c.e = 3\n      // below matches due to missing stats\n      lessThan(nestedCol(\"b.c.f.g\"), ofInt(0)), // b.c.f.g < 0\n      lessThan(nestedCol(\"b.c.f.i\"), ofInt(0)), // b.c.f.i < 0\n      lessThan(nestedCol(\"b.l\"), ofInt(0)) // b.l < 0\n    ),\n    misses = Seq(\n      lessThan(col(\"a\"), ofInt(0)), // a < 0\n      lessThan(nestedCol(\"b.c.d\"), ofInt(0)), // b.c.d < 0\n      lessThan(nestedCol(\"b.c.e\"), ofInt(0)) // b.c.e < 0\n    ))\n\n  testSkipping(\n    \"data skipping - nested schema - # indexed column = 0\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    indexedCols = Some(0),\n    hits = Seq(\n      // all included due to missing stats\n      lessThan(col(\"a\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.d\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.i\"), ofInt(0)),\n      lessThan(nestedCol(\"b.l\"), ofInt(0)),\n      lessThan(col(\"m\"), ofInt(0))),\n    misses = Seq())\n\n  testSkipping(\n    \"data skipping - indexed column names - \" +\n      \"naming a nested column indexes all leaf fields of that column\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    indexedCols = Some(3),\n    deltaStatsColNamesOpt = Some(\"b.c\"),\n    hits = Seq(\n      // these all have missing stats\n      lessThan(col(\"a\"), ofInt(0)),\n      lessThan(nestedCol(\"b.l\"), ofInt(0)),\n      lessThan(col(\"m\"), ofInt(0))),\n    misses = Seq(\n      lessThan(nestedCol(\"b.c.d\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.e\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.g\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.h\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.i\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.j\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.k\"), ofInt(0))))\n\n  testSkipping(\n    \"data skipping - indexed column names - index only a subset of leaf columns\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    indexedCols = Some(3),\n    deltaStatsColNamesOpt = Some(\"b.c.e, b.c.f.h, b.c.k, b.l\"),\n    hits = Seq(\n      // these all have missing stats\n      lessThan(col(\"a\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.d\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.g\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.i\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.j\"), ofInt(0)),\n      lessThan(col(\"m\"), ofInt(0))),\n    misses = Seq(\n      lessThan(nestedCol(\"b.c.e\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.f.h\"), ofInt(0)),\n      lessThan(nestedCol(\"b.c.k\"), ofInt(0)),\n      lessThan(nestedCol(\"b.l\"), ofInt(0))))\n\n  testSkipping(\n    \"data skipping - boolean comparisons\",\n    \"\"\"{\"a\": false}\"\"\",\n    hits = Seq(\n      equals(col(\"a\"), ofBoolean(false)),\n      greaterThan(col(\"a\"), ofBoolean(true)),\n      lessThanOrEqual(col(\"a\"), ofBoolean(false)),\n      equals(ofBoolean(true), col(\"a\")),\n      lessThan(ofBoolean(true), col(\"a\")),\n      not(equals(col(\"a\"), ofBoolean(false)))),\n    misses = Seq())\n\n  // Data skipping by stats should still work even when the only data in file is null, in spite of\n  // the NULL min/max stats that result -- this is different to having no stats at all.\n  testSkipping(\n    \"data skipping - nulls - only null in file\",\n    \"\"\"\n      {\"a\": null }\n    \"\"\",\n    schema = new SparkStructType().add(new SparkStructField(\"a\", SparkIntegerType)),\n    hits = Seq(\n      AlwaysTrue.ALWAYS_TRUE,\n      // Ideally this should not hit as it is always FALSE, but its correct to not skip\n      equals(col(\"a\"), ofNull(INTEGER)),\n      not(equals(col(\"a\"), ofNull(INTEGER))), // Same as previous case\n      isNull(col(\"a\")),\n      // This is optimized to `IsNull(a)` by NullPropagation in Spark\n      nullSafeEquals(col(\"a\"), ofNull(INTEGER)),\n      not(nullSafeEquals(col(\"a\"), ofInt(1))),\n      // In delta-spark we use verifyStatsForFilter to deal with missing stats instead of\n      // converting all nulls ==> true (keep). For comparisons with null statistics we end up with\n      // filter: dataFilter || !(verifyStatsForFilter) = null || false = null\n      // When filtering on a DF nulls are counted as false and eliminated. Thus these are misses\n      // in Delta-Spark.\n      // Including them is not incorrect. To skip these filters for Kernel we could use\n      // verifyStatsForFilter or some other solution like inserting a && isNotNull(a) expression.\n      equals(col(\"a\"), ofInt(1)),\n      lessThan(col(\"a\"), ofInt(1)),\n      greaterThan(col(\"a\"), ofInt(1)),\n      not(equals(col(\"a\"), ofInt(1))),\n      notEquals(col(\"a\"), ofInt(1))),\n    misses = Seq(\n      AlwaysFalse.ALWAYS_FALSE,\n      nullSafeEquals(col(\"a\"), ofInt(1)),\n      not(nullSafeEquals(col(\"a\"), ofNull(INTEGER))),\n      isNotNull(col(\"a\"))))\n\n  testSkipping(\n    \"data skipping - nulls - null + not-null in same file\",\n    \"\"\"\n      {\"a\": null }\n      {\"a\": 1 }\n    \"\"\",\n    schema = new SparkStructType().add(new SparkStructField(\"a\", SparkIntegerType)),\n    hits = Seq(\n      // Ideally this should not hit as it is always FALSE, but its correct to not skip\n      equals(col(\"a\"), ofNull(INTEGER)),\n      equals(col(\"a\"), ofInt(1)),\n      AlwaysTrue.ALWAYS_TRUE,\n      isNotNull(col(\"a\")),\n\n      // Note these expressions either aren't supported or aren't added to skipping yet\n      // but should still be hits once supported\n      isNull(col(\"a\")),\n      not(equals(col(\"a\"), ofNull(INTEGER))),\n      // This is optimized to `IsNull(a)` by NullPropagation in Spark\n      nullSafeEquals(col(\"a\"), ofNull(INTEGER)),\n      // This is optimized to `IsNotNull(a)` by NullPropagation in Spark\n      not(nullSafeEquals(col(\"a\"), ofNull(INTEGER))),\n      nullSafeEquals(col(\"a\"), ofInt(1)),\n      not(nullSafeEquals(col(\"a\"), ofInt(1))),\n\n      // MOVE BELOW EXPRESSIONS TO MISSES ONCE SUPPORTED BY DATA SKIPPING\n      notEquals(col(\"a\"), ofInt(1))),\n    misses = Seq(\n      AlwaysFalse.ALWAYS_FALSE,\n      lessThan(col(\"a\"), ofInt(1)),\n      greaterThan(col(\"a\"), ofInt(1)),\n      not(equals(col(\"a\"), ofInt(1)))))\n\n  Seq(\"TIMESTAMP\", \"TIMESTAMP_NTZ\").foreach { dataType =>\n    test(s\"data skipping - on $dataType type\") {\n      withTempDir { tempDir =>\n        withSparkTimeZone(\"UTC\") {\n          val data = \"2019-09-09 01:02:03.456789\"\n          val df = Seq(data).toDF(\"strTs\")\n            .selectExpr(\n              s\"CAST(strTs AS $dataType) AS ts\",\n              s\"STRUCT(CAST(strTs AS $dataType) AS ts) AS nested\")\n\n          val r = DeltaLog.forTable(spark, tempDir.getCanonicalPath)\n          df.coalesce(1).write.format(\"delta\").save(r.dataPath.toString)\n        }\n\n        checkSkipping(\n          tempDir.getCanonicalPath,\n          hits = Seq(\n            getTimestampPredicate(\"=\", col(\"ts\"), \"2019-09-09T01:02:03.456789Z\", dataType),\n            getTimestampPredicate(\">=\", col(\"ts\"), \"2019-09-09T01:02:03.456789Z\", dataType),\n            getTimestampPredicate(\"<=\", col(\"ts\"), \"2019-09-09T01:02:03.456789Z\", dataType),\n            getTimestampPredicate(\n              \">=\",\n              nestedCol(\"nested.ts\"),\n              \"2019-09-09T01:02:03.456789Z\",\n              dataType),\n            getTimestampPredicate(\n              \"<=\",\n              nestedCol(\"nested.ts\"),\n              \"2019-09-09T01:02:03.456789Z\",\n              dataType)),\n          misses = Seq(\n            getTimestampPredicate(\"=\", col(\"ts\"), \"2019-09-09T01:02:03.457001Z\", dataType),\n            getTimestampPredicate(\">=\", col(\"ts\"), \"2019-09-09T01:02:03.457001Z\", dataType),\n            getTimestampPredicate(\"<=\", col(\"ts\"), \"2019-09-09T01:02:03.455999Z\", dataType),\n            getTimestampPredicate(\n              \">=\",\n              nestedCol(\"nested.ts\"),\n              \"2019-09-09T01:02:03.457001Z\",\n              dataType),\n            getTimestampPredicate(\n              \"<=\",\n              nestedCol(\"nested.ts\"),\n              \"2019-09-09T01:02:03.455999Z\",\n              dataType)))\n      }\n    }\n  }\n\n  test(\"data skipping - Basic: Data skipping with delta statistic column\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      val tableProperty = \"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c9')\"\n      spark.sql(\n        s\"\"\"CREATE TABLE delta.`$tablePath`(\n           |c1 long, c2 STRING, c3 FLOAT, c4 DOUBLE, c5 TIMESTAMP, c6 DATE,\n           |c7 BINARY, c8 BOOLEAN, c9 DECIMAL(3, 2)\n           |) USING delta $tableProperty\"\"\".stripMargin)\n      spark.sql(\n        s\"\"\"insert into delta.`$tablePath` values\n           |(1, '1', 1.0, 1.0, TIMESTAMP'2001-01-01 01:00', DATE'2001-01-01', '1111', true, 1.0),\n           |(2, '2', 2.0, 2.0, TIMESTAMP'2002-02-02 02:00', DATE'2002-02-02', '2222', false, 2.0)\n           |\"\"\".stripMargin)\n      checkSkipping(\n        tablePath,\n        hits = Seq(\n          equals(col(\"c1\"), ofInt(1)),\n          equals(col(\"c2\"), ofString(\"2\")),\n          lessThan(col(\"c3\"), ofFloat(1.5f)),\n          greaterThan(col(\"c4\"), ofFloat(1.0f)),\n          equals(col(\"c6\"), ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2002-02-02\")))),\n          // Binary Column doesn't support delta statistics.\n          equals(col(\"c7\"), ofBinary(\"1111\".getBytes)),\n          equals(col(\"c7\"), ofBinary(\"3333\".getBytes)),\n          equals(col(\"c8\"), ofBoolean(true)),\n          equals(col(\"c8\"), ofBoolean(false)),\n          greaterThan(col(\"c9\"), ofDecimal(JBigDecimal.valueOf(1.5), 3, 2)),\n          getTimestampPredicate(\">=\", col(\"c5\"), \"2001-01-01T01:00:00-07:00\", \"TIMESTAMP\")),\n        misses = Seq(\n          equals(col(\"c1\"), ofInt(10)),\n          equals(col(\"c2\"), ofString(\"4\")),\n          lessThan(col(\"c3\"), ofFloat(0.5f)),\n          greaterThan(col(\"c4\"), ofFloat(5.0f)),\n          equals(col(\"c6\"), ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2003-02-02\")))),\n          greaterThan(col(\"c9\"), ofDecimal(JBigDecimal.valueOf(2.5), 3, 2)),\n          getTimestampPredicate(\">=\", col(\"c5\"), \"2003-01-01T01:00:00-07:00\", \"TIMESTAMP\")))\n    }\n  }\n\n  test(\"data skipping - Data skipping with delta statistic column rename column\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      spark.sql(\n        s\"\"\"CREATE TABLE delta.`$tablePath`(\n           |c1 long, c2 STRING, c3 FLOAT, c4 DOUBLE, c5 TIMESTAMP, c6 DATE,\n           |c7 BINARY, c8 BOOLEAN, c9 DECIMAL(3, 2)\n           |) USING delta\n           |TBLPROPERTIES(\n           |'delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c9',\n           |'delta.columnMapping.mode' = 'name',\n           |'delta.minReaderVersion' = '2',\n           |'delta.minWriterVersion' = '5'\n           |)\n           |\"\"\".stripMargin)\n      (1 to 9).foreach { i =>\n        spark.sql(s\"alter table delta.`$tablePath` RENAME COLUMN c$i to cc$i\")\n      }\n      spark.sql(\n        s\"\"\"insert into delta.`$tablePath` values\n           |(1, '1', 1.0, 1.0, TIMESTAMP'2001-01-01 01:00', DATE'2001-01-01', '1111', true, 1.0),\n           |(2, '2', 2.0, 2.0, TIMESTAMP'2002-02-02 02:00', DATE'2002-02-02', '2222', false, 2.0)\n           |\"\"\".stripMargin)\n\n      checkSkipping(\n        tablePath,\n        hits = Seq(\n          equals(col(\"cc1\"), ofInt(1)),\n          equals(col(\"cc2\"), ofString(\"2\")),\n          lessThan(col(\"cc3\"), ofFloat(1.5f)),\n          greaterThan(col(\"cc4\"), ofFloat(1.0f)),\n          equals(col(\"cc6\"), ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2002-02-02\")))),\n          // Binary Column doesn't support delta statistics.\n          equals(col(\"cc7\"), ofBinary(\"1111\".getBytes)),\n          equals(col(\"cc7\"), ofBinary(\"3333\".getBytes)),\n          equals(col(\"cc8\"), ofBoolean(true)),\n          equals(col(\"cc8\"), ofBoolean(false)),\n          greaterThan(col(\"cc9\"), ofDecimal(JBigDecimal.valueOf(1.5), 3, 2)),\n          getTimestampPredicate(\">=\", col(\"cc5\"), \"2001-01-01T01:00:00-07:00\", \"TIMESTAMP\")),\n        misses = Seq(\n          equals(col(\"cc1\"), ofInt(10)),\n          equals(col(\"cc2\"), ofString(\"4\")),\n          lessThan(col(\"cc3\"), ofFloat(0.5f)),\n          greaterThan(col(\"cc4\"), ofFloat(5.0f)),\n          equals(col(\"cc6\"), ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2003-02-02\")))),\n          getTimestampPredicate(\">=\", col(\"cc5\"), \"2003-01-01T01:00:00-07:00\", \"TIMESTAMP\"),\n          greaterThan(col(\"cc9\"), ofDecimal(JBigDecimal.valueOf(2.5), 3, 2))))\n    }\n  }\n\n  test(\"data skipping - Data skipping with delta statistic column drop column\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getCanonicalPath\n      spark.sql(\n        s\"\"\"CREATE TABLE delta.`$tablePath`(\n           |c1 long, c2 STRING, c3 FLOAT, c4 DOUBLE, c5 TIMESTAMP, c6 DATE,\n           |c7 BINARY, c8 BOOLEAN, c9 DECIMAL(3, 2), c10 TIMESTAMP_NTZ\n           |) USING delta\n           |TBLPROPERTIES(\n           |'delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c9,c10',\n           |'delta.columnMapping.mode' = 'name'\n           |)\n           |\"\"\".stripMargin)\n      spark.sql(s\"alter table delta.`$tablePath` drop COLUMN c2\")\n      spark.sql(s\"alter table delta.`$tablePath` drop COLUMN c7\")\n      spark.sql(s\"alter table delta.`$tablePath` drop COLUMN c8\")\n      spark.sql(\n        s\"\"\"insert into delta.`$tablePath` values\n           |(1, 1.0, 1.0, TIMESTAMP'2001-01-01 01:00', DATE'2001-01-01',\n           |1.0, TIMESTAMP_NTZ'2001-01-01 01:00'),\n           |(2, 2.0, 2.0, TIMESTAMP'2002-02-02 02:00', DATE'2002-02-02',\n           |2.0, TIMESTAMP_NTZ'2002-02-02 02:00')\n           |\"\"\".stripMargin)\n      checkSkipping(\n        tablePath,\n        hits = Seq(\n          equals(col(\"c1\"), ofInt(1)),\n          lessThan(col(\"c3\"), ofFloat(1.5f)),\n          greaterThan(col(\"c4\"), ofFloat(1.0f)),\n          equals(col(\"c6\"), ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2002-02-02\")))),\n          greaterThan(col(\"c9\"), ofDecimal(JBigDecimal.valueOf(1.5), 3, 2)),\n          getTimestampPredicate(\">=\", col(\"c5\"), \"2001-01-01T01:00:00-07:00\", \"TIMESTAMP\"),\n          getTimestampPredicate(\">=\", col(\"c10\"), \"2001-01-01T01:00:00-07:00\", \"TIMESTAMP_NTZ\")),\n        misses = Seq(\n          equals(col(\"c1\"), ofInt(10)),\n          lessThan(col(\"c3\"), ofFloat(0.5f)),\n          greaterThan(col(\"c4\"), ofFloat(5.0f)),\n          equals(col(\"c6\"), ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2003-02-02\")))),\n          greaterThan(col(\"c9\"), ofDecimal(JBigDecimal.valueOf(2.5), 3, 2)),\n          getTimestampPredicate(\">=\", col(\"c5\"), \"2003-01-01T01:00:00-07:00\", \"TIMESTAMP\"),\n          getTimestampPredicate(\">=\", col(\"c10\"), \"2003-01-01T01:00:00-07:00\", \"TIMESTAMP_NTZ\")))\n    }\n  }\n\n  test(\"data skipping by partition and data values - nulls\") {\n    withTempDir { tableDir =>\n      val dataSeqs =\n        Seq( // each sequence produce a single file\n          Seq((null, null)),\n          Seq((null, \"a\")),\n          Seq((null, \"b\")),\n          Seq((\"a\", \"a\"), (\"a\", null)),\n          Seq((\"b\", null)))\n      dataSeqs.foreach { seq =>\n        seq.toDF(\"key\", \"value\").coalesce(1)\n          .write.format(\"delta\").partitionBy(\"key\").mode(\"append\").save(tableDir.getCanonicalPath)\n      }\n      def checkResults(\n          predicate: Predicate,\n          expNumPartitions: Int,\n          expNumFiles: Long): Unit = {\n        val snapshot = latestSnapshot(tableDir.getCanonicalPath)\n        val scanFiles = collectScanFileRows(\n          snapshot.getScanBuilder().withFilter(predicate).build())\n        assert(\n          scanFiles.length == expNumFiles,\n          s\"Expected $expNumFiles but found ${scanFiles.length} for $predicate\")\n\n        val partitionValues = scanFiles.map { row =>\n          InternalScanFileUtils.getPartitionValues(row)\n        }.distinct\n        assert(\n          partitionValues.length == expNumPartitions,\n          s\"Expected $expNumPartitions partitions but found ${partitionValues.length}\")\n      }\n\n      // Trivial base case\n      checkResults(\n        predicate = AlwaysTrue.ALWAYS_TRUE,\n        expNumPartitions = 3,\n        expNumFiles = 5)\n\n      // Conditions on partition key\n      checkResults(\n        predicate = isNotNull(col(\"key\")),\n        expNumPartitions = 2,\n        expNumFiles = 2\n      ) // 2 files with key = 'a', and 1 file with key = 'b'\n\n      checkResults(\n        predicate = equals(col(\"key\"), ofString(\"a\")),\n        expNumPartitions = 1,\n        expNumFiles = 1\n      ) // 1 files with key = 'a'\n\n      checkResults(\n        predicate = equals(col(\"key\"), ofString(\"b\")),\n        expNumPartitions = 1,\n        expNumFiles = 1\n      ) // 1 files with key = 'b'\n\n      // TODO shouldn't partition filters on unsupported expressions just not prune instead of fail?\n      checkResults(\n        predicate = isNull(col(\"key\")),\n        expNumPartitions = 1,\n        expNumFiles = 3\n      ) // 3 files with key = null\n\n      checkResults(\n        predicate = nullSafeEquals(col(\"key\"), ofNull(STRING)),\n        expNumPartitions = 1,\n        expNumFiles = 3\n      ) // 3 files with key = null\n\n      checkResults(\n        predicate = nullSafeEquals(col(\"key\"), ofString(\"a\")),\n        expNumPartitions = 1,\n        expNumFiles = 1\n      ) // 1 files with key <=> 'a'\n\n      checkResults(\n        predicate = nullSafeEquals(col(\"key\"), ofString(\"b\")),\n        expNumPartitions = 1,\n        expNumFiles = 1\n      ) // 1 files with key <=> 'b'\n\n      // Conditions on partitions keys and values\n      checkResults(\n        predicate = isNull(col(\"value\")),\n        expNumPartitions = 3,\n        expNumFiles = 3)\n\n      checkResults(\n        predicate = isNotNull(col(\"value\")),\n        expNumPartitions = 2, // one of the partitions has no files left after data skipping\n        expNumFiles = 3\n      ) // files with all NULL values get skipped\n\n      checkResults(\n        predicate = nullSafeEquals(col(\"value\"), ofNull(STRING)),\n        expNumPartitions = 3,\n        expNumFiles = 3)\n\n      checkResults(\n        predicate = nullSafeEquals(ofNull(STRING), col(\"value\")),\n        expNumPartitions = 3,\n        expNumFiles = 3)\n\n      checkResults(\n        predicate = equals(col(\"value\"), ofString(\"a\")),\n        expNumPartitions = 3, // should be 2 if we can correctly skip \"value = 'a'\" for nulls\n        expNumFiles = 4\n      ) // should be 2 if we can correctly skip \"value = 'a'\" for nulls\n\n      checkResults(\n        predicate = nullSafeEquals(col(\"value\"), ofString(\"a\")),\n        expNumPartitions = 2,\n        expNumFiles = 2)\n\n      checkResults(\n        predicate = nullSafeEquals(ofString(\"a\"), col(\"value\")),\n        expNumPartitions = 2,\n        expNumFiles = 2)\n\n      checkResults(\n        predicate = notEquals(col(\"value\"), ofString(\"a\")),\n        expNumPartitions = 3, // should be 1 once <> is supported\n        expNumFiles = 5\n      ) // should be 1 once <> is supported\n\n      checkResults(\n        predicate = equals(col(\"value\"), ofString(\"b\")),\n        expNumPartitions = 2, // should be 1 if we can correctly skip \"value = 'b'\" for nulls\n        expNumFiles = 3\n      ) // should be 1 if we can correctly skip \"value = 'a'\" for nulls\n\n      checkResults(\n        predicate = nullSafeEquals(col(\"value\"), ofString(\"b\")),\n        expNumPartitions = 1,\n        expNumFiles = 1)\n\n      // Conditions on both, partition keys and values\n      /*\n      NOT YET SUPPORTED EXPRESSIONS\n      checkResults(\n        predicate = new And(isNull(col(\"key\")), equals(col(\"value\"), ofString(\"a\"))),\n        expNumPartitions = 2,\n        expNumFiles = 1) // only one file in the partition has (*, \"a\")\n\n      checkResults(\n        predicate = new And(nullSafeEquals(col(\"key\"), ofNull(STRING)), nullSafeEquals(col(\"value\"),\n        ofNull(STRING))),\n        expNumPartitions = 1,\n        expNumFiles = 1) // 3 files with key = null, but only 1 with val = null.\n       */\n\n      checkResults(\n        predicate = new And(isNotNull(col(\"key\")), isNotNull(col(\"value\"))),\n        expNumPartitions = 1,\n        expNumFiles = 1\n      ) // 1 file with (*, a)\n\n      checkResults(\n        predicate = new Or(\n          nullSafeEquals(col(\"key\"), ofNull(STRING)),\n          nullSafeEquals(col(\"value\"), ofNull(STRING))),\n        expNumPartitions = 3,\n        expNumFiles = 5\n      ) // all 5 files\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Kernel data skipping tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  test(\"basic data skipping for all types - all CM modes + checkpoint\") {\n    // Map of column name to (value_in_table, smaller_value, bigger_value)\n    val colToLits = Map(\n      \"as_int\" -> (ofInt(0), ofInt(-1), ofInt(1)),\n      \"as_long\" -> (ofLong(0), ofLong(-1), ofLong(1)),\n      \"as_byte\" -> (ofByte(0), ofByte(-1), ofByte(1)),\n      \"as_short\" -> (ofShort(0), ofShort(-1), ofShort(1)),\n      \"as_float\" -> (ofFloat(0), ofFloat(-1), ofFloat(1)),\n      \"as_double\" -> (ofDouble(0), ofDouble(-1), ofDouble(1)),\n      \"as_string\" -> (ofString(\"0\"), ofString(\"!\"), ofString(\"1\")),\n      \"as_date\" -> (\n        ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2000-01-01\"))),\n        ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"1999-01-01\"))),\n        ofDate(InternalUtils.daysSinceEpoch(Date.valueOf(\"2000-01-02\")))),\n      // TODO (delta-io/delta#2462) add Timestamp once we support skipping for TimestampType\n      \"as_big_decimal\" -> (\n        ofDecimal(JBigDecimal.valueOf(0), 1, 0),\n        ofDecimal(JBigDecimal.valueOf(-1), 1, 0),\n        ofDecimal(JBigDecimal.valueOf(1), 1, 0)))\n    val misses = colToLits.flatMap { case (colName, (value, small, big)) =>\n      Seq(\n        equals(col(colName), small),\n        greaterThan(col(colName), value),\n        greaterThanOrEqual(col(colName), big),\n        lessThan(col(colName), value),\n        lessThanOrEqual(col(colName), small))\n    }.toSeq\n    val hits = colToLits.flatMap { case (colName, (value, small, big)) =>\n      Seq(\n        equals(col(colName), value),\n        greaterThan(col(colName), small),\n        greaterThanOrEqual(col(colName), value),\n        lessThan(col(colName), big),\n        lessThanOrEqual(col(colName), value))\n    }.toSeq\n    Seq(\n      \"data-skipping-basic-stats-all-types\",\n      \"data-skipping-basic-stats-all-types-columnmapping-name\",\n      \"data-skipping-basic-stats-all-types-columnmapping-id\",\n      \"data-skipping-basic-stats-all-types-checkpoint\").foreach { goldenTable =>\n      checkSkipping(\n        goldenTablePath(goldenTable),\n        hits,\n        misses)\n    }\n  }\n\n  test(\"data skipping - implicit casting works\") {\n    checkSkipping(\n      goldenTablePath(\"data-skipping-basic-stats-all-types\"),\n      hits = Seq(\n        equals(col(\"as_short\"), ofFloat(0f)),\n        equals(col(\"as_float\"), ofShort(0))),\n      misses = Seq(\n        equals(col(\"as_short\"), ofFloat(1f)),\n        equals(col(\"as_float\"), ofShort(1))))\n  }\n\n  test(\"data skipping - incompatible schema change doesn't break\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getPath\n      // initially write with integer column value\n      Seq(0, 1, 2).toDF.repartition(1).write.format(\"delta\").save(tablePath)\n      // overwrite with string column value\n      Seq(\"0\", \"1\", \"2\").toDF.repartition(1).write\n        .format(\"delta\").mode(\"overwrite\").option(\"overwriteSchema\", true).save(tablePath)\n\n      checkSkipping(\n        tablePath,\n        hits = Seq(\n          equals(col(\"value\"), ofString(\"1\"))),\n        misses = Seq(\n          equals(col(\"value\"), ofString(\"3\"))))\n    }\n  }\n\n  test(\"data skipping - filter on non-existent column\") {\n    checkSkipping(\n      goldenTablePath(\"data-skipping-basic-stats-all-types\"),\n      hits = Seq(equals(col(\"foo\"), ofInt(1))),\n      misses = Seq())\n  }\n\n  // todo add a test with dvs where tightBounds=false\n\n  test(\"data skipping - filter on partition AND data column\") {\n    checkSkipping(\n      goldenTablePath(\"data-skipping-basic-stats-all-types\"),\n      filterToNumExpFiles = Map(\n        new And(\n          greaterThan(col(\"part\"), ofInt(0)),\n          greaterThan(col(\"id\"), ofInt(0))\n        ) -> 1 // should prune 3 files from partition + data filter\n      ))\n  }\n\n  test(\"data skipping - stats collected changing across versions\") {\n    checkSkipping(\n      goldenTablePath(\"data-skipping-change-stats-collected-across-versions\"),\n      filterToNumExpFiles = Map(\n        equals(col(\"col1\"), ofInt(1)) -> 1, // should prune 2 files\n        equals(col(\"col2\"), ofInt(1)) -> 2, // should prune 1 file\n        new And(\n          equals(col(\"col1\"), ofInt(1)),\n          equals(col(\"col2\"), ofInt(1))\n        ) -> 1 // should prune 2 files\n      ))\n  }\n\n  test(\"data skipping - range of ints\") {\n    withTempDir { tempDir =>\n      spark.range(10).repartition(1).write.format(\"delta\").save(tempDir.getCanonicalPath)\n      // to test where MIN != MAX\n      checkSkipping(\n        tempDir.getCanonicalPath,\n        hits = Seq(\n          equals(col(\"id\"), ofInt(5)),\n          lessThan(col(\"id\"), ofInt(7)),\n          lessThan(col(\"id\"), ofInt(15)),\n          lessThanOrEqual(col(\"id\"), ofInt(9)),\n          greaterThan(col(\"id\"), ofInt(3)),\n          greaterThan(col(\"id\"), ofInt(-1)),\n          greaterThanOrEqual(col(\"id\"), ofInt(0))),\n        misses = Seq(\n          equals(col(\"id\"), ofInt(10)),\n          lessThan(col(\"id\"), ofInt(0)),\n          lessThan(col(\"id\"), ofInt(-1)),\n          lessThanOrEqual(col(\"id\"), ofInt(-1)),\n          greaterThan(col(\"id\"), ofInt(10)),\n          greaterThan(col(\"id\"), ofInt(11)),\n          greaterThanOrEqual(col(\"id\"), ofInt(11))))\n    }\n  }\n\n  test(\"data skipping - non-eligible min/max data skipping types\") {\n    withTempDir { tempDir =>\n      val schema = SparkStructType.fromDDL(\"`id` INT, `arr_col` ARRAY<INT>, \" +\n        \"`map_col` MAP<STRING, INT>, `struct_col` STRUCT<`field1`: INT>\")\n      val data = SparkRow(0, Array(1, 2), Map(\"foo\" -> 1), SparkRow(5)) :: Nil\n      spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n        .write.format(\"delta\").save(tempDir.getCanonicalPath)\n      checkSkipping(\n        tempDir.getCanonicalPath,\n        hits = Seq(\n          equals(col(\"id\"), ofInt(0)), // filter on the one eligible column\n          isNotNull(col(\"id\")),\n          isNotNull(col(\"arr_col\")),\n          isNotNull(col(\"map_col\")),\n          isNotNull(col(\"struct_col\")),\n          isNotNull(nestedCol(\"struct_col.field1\")),\n          not(isNotNull(col(\"struct_col\"))), // we don't skip on non-leaf columns\n\n          not(isNull(col(\"id\"))),\n          not(isNull(col(\"arr_col\"))),\n          not(isNull(col(\"map_col\"))),\n          not(isNull(col(\"struct_col\"))),\n          not(isNull(nestedCol(\"struct_col.field1\"))),\n          isNull(col(\"struct_col\"))),\n        misses = Seq(\n          equals(col(\"id\"), ofInt(1)),\n          not(isNotNull(col(\"id\"))),\n          not(isNotNull(col(\"arr_col\"))),\n          not(isNotNull(col(\"map_col\"))),\n          not(isNotNull(nestedCol(\"struct_col.field1\"))),\n          isNull(col(\"id\")),\n          isNull(col(\"arr_col\")),\n          isNull(col(\"map_col\")),\n          isNull(nestedCol(\"struct_col.field1\"))))\n    }\n  }\n\n  test(\"data skipping - non-eligible min/max data skipping types all nulls in file\") {\n    withTempDir { tempDir =>\n      val schema = SparkStructType.fromDDL(\"`id` INT, `arr_col` ARRAY<INT>, \" +\n        \"`map_col` MAP<STRING, INT>, `struct_col` STRUCT<`field1`: INT>\")\n      val data = SparkRow(null, null, null, null) :: Nil\n      spark.createDataFrame(spark.sparkContext.parallelize(data), schema).coalesce(1)\n        .write.format(\"delta\").save(tempDir.getCanonicalPath)\n      checkSkipping(\n        tempDir.getCanonicalPath,\n        hits = Seq(\n          // [not(is_not_null) is converted to is_null]\n          not(isNotNull(col(\"id\"))),\n          not(isNotNull(col(\"arr_col\"))),\n          not(isNotNull(col(\"map_col\"))),\n          not(isNotNull(col(\"struct_col\"))),\n          not(isNotNull(nestedCol(\"struct_col.field1\"))),\n          isNotNull(col(\"struct_col\")) // we don't skip on non-leaf columns\n        ),\n        misses = Seq(\n          isNotNull(col(\"id\")),\n          isNotNull(col(\"arr_col\")),\n          isNotNull(col(\"map_col\")),\n          isNotNull(nestedCol(\"struct_col.field1\"))))\n    }\n  }\n\n  test(\"data skipping - non-eligible min/max data skipping types null +\" +\n    \"non-null in same file\") {\n    withTempDir { tempDir =>\n      val schema = SparkStructType.fromDDL(\"`id` INT, `arr_col` ARRAY<INT>, \" +\n        \"`map_col` MAP<STRING, INT>, `struct_col` STRUCT<`field1`: INT>\")\n      val data = SparkRow(0, Array(1, 2), Map(\"foo\" -> 1), SparkRow(5)) ::\n        SparkRow(null, null, null, null) :: Nil\n      spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n        .write.format(\"delta\").save(tempDir.getCanonicalPath)\n      checkSkipping(\n        tempDir.getCanonicalPath,\n        hits = Seq(\n          // [not(is_not_null) is converted to is_null]\n          not(isNotNull(col(\"id\"))),\n          not(isNotNull(col(\"arr_col\"))),\n          not(isNotNull(col(\"map_col\"))),\n          not(isNotNull(col(\"struct_col\"))),\n          not(isNotNull(nestedCol(\"struct_col.field1\"))),\n          isNotNull(col(\"id\")),\n          isNotNull(col(\"arr_col\")),\n          isNotNull(col(\"map_col\")),\n          isNotNull(col(\"struct_col\")),\n          isNotNull(nestedCol(\"struct_col.field1\"))),\n        misses = Seq())\n    }\n  }\n\n  test(\"data skipping - is not null with DVs in file with non-nulls\") {\n    withSQLConf((\"spark.databricks.delta.properties.defaults.enableDeletionVectors\", \"true\")) {\n      withTempDir { tempDir =>\n        def overwriteTableAndPerformDelete(deleteCondition: String): Unit = {\n          val data = SparkRow(0, 0) :: SparkRow(1, 1) :: SparkRow(2, null) ::\n            SparkRow(3, null) :: Nil\n          val schema = new SparkStructType()\n            .add(\"col1\", SparkIntegerType)\n            .add(\"col2\", SparkIntegerType)\n\n          spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n            .repartition(1)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .save(tempDir.getCanonicalPath)\n          spark.sql(s\"DELETE FROM delta.`${tempDir.getCanonicalPath}` WHERE $deleteCondition\")\n        }\n        def checkNoSkipping(): Unit = {\n          checkSkipping(\n            tempDir.getCanonicalPath,\n            hits = Seq(\n              isNotNull(col(\"col2\")),\n              isNotNull(col(\"col1\"))),\n            misses = Seq())\n        }\n\n        // remove no rows\n        overwriteTableAndPerformDelete(\"false\")\n        checkNoSkipping()\n        // remove all null rows\n        overwriteTableAndPerformDelete(\"col2 IS NULL\")\n        checkNoSkipping()\n        // remove all non-null rows\n        overwriteTableAndPerformDelete(\"col2 IS NOT NULL\")\n        checkNoSkipping()\n        // remove one null row\n        overwriteTableAndPerformDelete(\"col1 = 2\")\n        checkNoSkipping()\n      }\n    }\n  }\n\n  test(\"data skipping - is not null with DVs in file with all nulls\") {\n    withSQLConf((\"spark.databricks.delta.properties.defaults.enableDeletionVectors\", \"true\")) {\n      withTempDir { tempDir =>\n        def checkDoesSkipping(): Unit = {\n          checkSkipping(\n            tempDir.getCanonicalPath,\n            hits = Seq(\n              isNotNull(col(\"col1\"))),\n            misses = Seq(\n              isNotNull(col(\"col2\"))))\n        }\n        // write initial table with all nulls for col2\n        val data = SparkRow(0, null) :: SparkRow(1, null) :: Nil\n        val schema = new SparkStructType()\n          .add(\"col1\", SparkIntegerType)\n          .add(\"col2\", SparkIntegerType)\n        spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n          .repartition(1)\n          .write\n          .format(\"delta\")\n          .save(tempDir.getCanonicalPath)\n        checkDoesSkipping()\n        // delete one of the nulls\n        spark.sql(s\"DELETE FROM delta.`${tempDir.getCanonicalPath}` WHERE col1 = 0\")\n        checkDoesSkipping()\n      }\n    }\n  }\n\n  test(\"don't read stats column when there is no usable data skipping filter\") {\n    val path = goldenTablePath(\"data-skipping-basic-stats-all-types\")\n    val engine = engineDisallowedStatsReads\n\n    def snapshot(engine: Engine): Snapshot = {\n      Table.forPath(engine, path).getLatestSnapshot(engine)\n    }\n\n    def verifyNoStatsColumn(scanFiles: CloseableIterator[FilteredColumnarBatch]): Unit = {\n      scanFiles.forEach { batch =>\n        val addSchema = batch.getData.getSchema.get(\"add\").getDataType.asInstanceOf[StructType]\n        assert(addSchema.indexOf(\"stats\") < 0)\n      }\n    }\n\n    // no filter --> don't read stats\n    verifyNoStatsColumn(\n      snapshot(engineDisallowedStatsReads).getScanBuilder().build().getScanFiles(engine))\n\n    // partition filter only --> don't read stats\n    val partFilter = equals(new Column(\"part\"), ofInt(1))\n    verifyNoStatsColumn(\n      snapshot(engineDisallowedStatsReads)\n        .getScanBuilder().withFilter(partFilter).build()\n        .getScanFiles(engine))\n\n    // no eligible data skipping filter --> don't read stats\n    val nonEligibleFilter = lessThan(\n      new ScalarExpression(\"%\", Seq(col(\"as_int\"), ofInt(10)).asJava),\n      ofInt(1))\n    verifyNoStatsColumn(\n      snapshot(engineDisallowedStatsReads)\n        .getScanBuilder().withFilter(nonEligibleFilter).build()\n        .getScanFiles(engine))\n  }\n\n  test(\"data skipping - prune schema correctly for various predicates\") {\n    def structTypeToLeafColumns(\n        schema: StructType,\n        parentPath: Seq[String] = Seq()): Set[Column] = {\n      schema.fields().asScala.flatMap { field =>\n        field.getDataType() match {\n          case nestedSchema: StructType =>\n            assert(\n              nestedSchema.fields().size() > 0,\n              \"Schema should not have field of type StructType with no child fields\")\n            structTypeToLeafColumns(nestedSchema, parentPath ++ Seq(field.getName()))\n          case _ =>\n            Seq(new Column(parentPath.toArray :+ field.getName()))\n        }\n      }.toSet\n    }\n    def verifySchema(expectedReadCols: Set[Column]): StructType => Unit = { readSchema =>\n      assert(structTypeToLeafColumns(readSchema) == expectedReadCols)\n    }\n    val path = goldenTablePath(\"data-skipping-basic-stats-all-types\")\n    // Map of expression -> expected read columns\n    Map(\n      equals(col(\"as_int\"), ofInt(0)) ->\n        Set(nestedCol(\"minValues.as_int\"), nestedCol(\"maxValues.as_int\")),\n      lessThan(col(\"as_int\"), ofInt(0)) -> Set(nestedCol(\"minValues.as_int\")),\n      greaterThan(col(\"as_int\"), ofInt(0)) -> Set(nestedCol(\"maxValues.as_int\")),\n      greaterThanOrEqual(col(\"as_int\"), ofInt(0)) -> Set(nestedCol(\"maxValues.as_int\")),\n      lessThanOrEqual(col(\"as_int\"), ofInt(0)) -> Set(nestedCol(\"minValues.as_int\")),\n      new And(\n        lessThan(col(\"as_int\"), ofInt(0)),\n        greaterThan(col(\"as_long\"), ofInt(0))) -> Set(\n        nestedCol(\"minValues.as_int\"),\n        nestedCol(\"maxValues.as_long\"))).foreach { case (predicate, expectedCols) =>\n      val engine = engineVerifyJsonParseSchema(verifySchema(expectedCols))\n      collectScanFileRows(\n        Table.forPath(engine, path).getLatestSnapshot(engine)\n          .getScanBuilder().withFilter(predicate).build(),\n        engine = engine)\n    }\n  }\n\n  test(\"data skipping - validate stats written by kernel can be read and used\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val schema = new StructType()\n        .add(\n          \"nested\",\n          new StructType()\n            .add(\"byteCol\", ByteType.BYTE)\n            .add(\"intCol\", IntegerType.INTEGER)\n            .add(\"floatCol\", FloatType.FLOAT)\n            .add(\"normalDouble\", DoubleType.DOUBLE) // used for filtering\n            .add(\"weirdDouble\", DoubleType.DOUBLE) // may contain NaN/Infinity\n            .add(\"decimalCol\", new DecimalType(10, 2)))\n\n      val tableProps = Map(TableConfig.DATA_SKIPPING_NUM_INDEXED_COLS.getKey -> \"10\")\n      val txn = getCreateTxn(engine, tablePath, schema, List.empty, tableProps)\n      txn.commit(engine, emptyIterable())\n\n      // Build some rows with corner-case values\n      val testRows = Seq(\n        (\n          -128.toByte,\n          Int.MinValue,\n          Float.NaN,\n          1500.0,\n          Double.PositiveInfinity,\n          new java.math.BigDecimal(\"98765.43\")),\n        (0.toByte, 0, 1.23f, 10.0, -42.99, new java.math.BigDecimal(\"0.00\")),\n        (\n          127.toByte,\n          Int.MaxValue,\n          Float.NegativeInfinity,\n          200.0,\n          Double.NaN,\n          new java.math.BigDecimal(\"9999999.99\")))\n\n      testRows.zipWithIndex.foreach { case ((b, i, f, normalD, weirdD, dec), idx) =>\n        val singleRowBatch =\n          buildSingleStructColumnRowBatch(schema, Array[Any](b, i, f, normalD, weirdD, dec))\n        val commitResult = appendData(\n          engine,\n          tablePath,\n          data = List(Map.empty[String, Literal] -> List(singleRowBatch)))\n        verifyCommitResult(commitResult, expVersion = idx + 1, expIsReadyForCheckpoint = false)\n      }\n\n      // Filter: select rows where nested.normalDouble > 50.0.\n      // Expected: Row 0 (1500.0) and Row 2 (200.0) pass, Row 1 (10.0) is pruned.\n      val skipFilter = greaterThan(nestedCol(\"nested.normalDouble\"), ofDouble(50.0))\n      val snapshot = Table.forPath(engine, tablePath).getLatestSnapshot(engine)\n      val scan = snapshot.getScanBuilder().withFilter(skipFilter).build()\n      val scanFiles = collectScanFileRows(scan, engine)\n\n      // Assert that exactly 2 files (rows) match the filter.\n      assert(\n        scanFiles.size == 2,\n        s\"Expected exactly 2 matching files (rows with normalDouble > 50.0: row 0 & 2).\" +\n          s\" Found ${scanFiles.size}.\")\n    }\n  }\n\n  /**\n   * Creates a single-row FilteredColumnarBatch assuming the schema has one top-level StructType\n   * and `rowValues` align with its subfields.\n   */\n  def buildSingleStructColumnRowBatch(\n      schema: StructType,\n      rowValues: Array[Any]): FilteredColumnarBatch = {\n    require(schema.length() == 1, s\"Expected 1 field, found ${schema.length()}\")\n    val nestedType = schema.get(\"nested\").getDataType.asInstanceOf[StructType]\n    require(\n      nestedType.length() == rowValues.length,\n      s\"${nestedType.length()} vs ${rowValues.length}\")\n\n    // We zip each field with an index so we can pick rowValues(i)\n    val childVectors: Array[ColumnVector] =\n      nestedType.fields().asScala.zipWithIndex.map { case (field: StructField, i: Int) =>\n        DefaultGenericVector.fromArray(field.getDataType, Array(rowValues(i).asInstanceOf[AnyRef]))\n      }.toArray\n\n    val structVector =\n      new DefaultStructVector(1, nestedType, java.util.Optional.empty(), childVectors)\n    val batch = new DefaultColumnarBatch(1, schema, Array(structVector))\n    new FilteredColumnarBatch(batch, java.util.Optional.empty())\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // Check the includeStats parameter on ScanImpl.getScanFiles(engine, includeStats)\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  test(\"check ScanImpl.getScanFiles for includeStats=true\") {\n    // When includeStats=true the JSON statistic should always be returned in the scan files\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.getCanonicalPath)\n      def checkStatsPresent(scan: Scan): Unit = {\n        val scanFileBatches = scan.asInstanceOf[ScanImpl].getScanFiles(defaultEngine, true)\n        scanFileBatches.forEach { batch =>\n          assert(batch.getData().getSchema() == InternalScanFileUtils.SCAN_FILE_SCHEMA_WITH_STATS)\n        }\n      }\n      // No query filter\n      checkStatsPresent(\n        latestSnapshot(tempDir.getCanonicalPath).getScanBuilder().build())\n      // Query filter but no valid data skipping filter\n      checkStatsPresent(\n        latestSnapshot(tempDir.getCanonicalPath)\n          .getScanBuilder()\n          .withFilter(\n            greaterThan(\n              new ScalarExpression(\"+\", Seq(col(\"id\"), ofInt(10)).asJava),\n              ofInt(100))).build())\n      // With valid data skipping filter present\n      checkStatsPresent(\n        latestSnapshot(tempDir.getCanonicalPath)\n          .getScanBuilder()\n          .withFilter(greaterThan(col(\"id\"), ofInt(0)))\n          .build())\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for collation data skipping / partition pruning\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Generic helpers for building batches with fixed column names c1, c2, c3\n  private def buildBatch(schema: StructType, v1: AnyRef, v2: AnyRef): FilteredColumnarBatch = {\n    val c1Type = schema.get(\"c1\").getDataType\n    val c2Type = schema.get(\"c2\").getDataType\n    val c1Vec = DefaultGenericVector.fromArray(c1Type, Array(v1))\n    val c2Vec = DefaultGenericVector.fromArray(c2Type, Array(v2))\n    val batch = new DefaultColumnarBatch(1, schema, Array(c1Vec, c2Vec))\n    new FilteredColumnarBatch(batch, java.util.Optional.empty())\n  }\n\n  private def buildBatch(\n      schema: StructType,\n      v1: AnyRef,\n      v2: AnyRef,\n      v3: AnyRef): FilteredColumnarBatch = {\n    val c1Type = schema.get(\"c1\").getDataType\n    val c2Type = schema.get(\"c2\").getDataType\n    val c3Type = schema.get(\"c3\").getDataType\n    val c1Vec = DefaultGenericVector.fromArray(c1Type, Array(v1))\n    val c2Vec = DefaultGenericVector.fromArray(c2Type, Array(v2))\n    val c3Vec = DefaultGenericVector.fromArray(c3Type, Array(v3))\n    val batch = new DefaultColumnarBatch(1, schema, Array(c1Vec, c2Vec, c3Vec))\n    new FilteredColumnarBatch(batch, java.util.Optional.empty())\n  }\n\n  private def buildNestedBatch(\n      schema: StructType,\n      v1: AnyRef,\n      v2: AnyRef): FilteredColumnarBatch = {\n    val sType = schema.get(\"s\").getDataType.asInstanceOf[StructType]\n    val c1Type = sType.get(\"c1\").getDataType\n    val c2Type = sType.get(\"c2\").getDataType\n    val c1Vec = DefaultGenericVector.fromArray(c1Type, Array(v1))\n    val c2Vec = DefaultGenericVector.fromArray(c2Type, Array(v2))\n    val structVec =\n      new DefaultStructVector(1, sType, java.util.Optional.empty(), Array(c1Vec, c2Vec))\n    val batch = new DefaultColumnarBatch(1, schema, Array(structVec))\n    new FilteredColumnarBatch(batch, java.util.Optional.empty())\n  }\n\n  val utf8Lcase = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE.74\")\n  val utf8LcaseString = new StringType(utf8Lcase)\n  val unicode = CollationIdentifier.fromString(\"ICU.UNICODE.75.1\")\n  val unicodeString = new StringType(unicode)\n\n  test(\"partition pruning - predicates with SPARK.UTF8_BINARY on partition column\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n\n          val c2Collation = c2Type.getCollationIdentifier\n\n          getCreateTxn(defaultEngine, tablePath, schema, List(\"c1\")).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map(\"c1\" -> ofString(\"a\")) -> List(buildBatch(schema, \"a\", \"b\")),\n              Map(\"c1\" -> ofString(\"c\", c2Collation)) -> List(buildBatch(schema, \"c\", \"d\")),\n              Map(\"c1\" -> ofString(\"e\")) -> List(buildBatch(schema, \"e\", \"f\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            // Create a checkpoint for the table\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(defaultEngine, tempDir.getCanonicalPath)\n              .checkpoint(defaultEngine, version)\n          }\n\n          val filterToFileNumber = Map(\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\")) -> 0,\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation)) -> 0,\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"=\",\n              ofString(\"d\"),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"=\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new Predicate(\n              \"=\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new Predicate(\n              \"=\",\n              col(\"c1\"),\n              ofString(\"a\")) -> 1,\n            new Predicate(\n              \"=\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation)) -> 1,\n            new Predicate(\n              \">=\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new Predicate(\n              \">=\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new Predicate(\n              \">\",\n              col(\"c1\"),\n              ofString(\"e\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"b\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c1\"),\n                ofString(\"e\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new Or(\n              new Predicate(\n                \"=\",\n                col(\"c1\"),\n                ofString(\"x\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                col(\"c1\"),\n                ofString(\"d\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c1\"),\n                ofString(\"z\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> totalFiles,\n            new Predicate(\n              \"STARTS_WITH\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new In(\n              col(\"c1\"),\n              java.util.Arrays.asList(ofString(\"a\"), ofString(\"x\")),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new In(\n              col(\"c1\"),\n              java.util.Arrays.asList(ofString(\"x\"), ofString(\"y\")),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0)\n          checkSkipping(tempDir.getCanonicalPath, filterToFileNumber)\n        }\n      }\n    }\n  }\n\n  test(\"partition pruning - predicates with non default collation on partition column\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n\n          val c2Collation = c2Type.getCollationIdentifier\n\n          getCreateTxn(defaultEngine, tablePath, schema, List(\"c1\")).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map(\"c1\" -> ofString(\"a\")) -> List(buildBatch(schema, \"a\", \"b\")),\n              Map(\"c1\" -> ofString(\"c\")) -> List(buildBatch(schema, \"c\", \"d\")),\n              Map(\"c1\" -> ofString(\"e\", c2Collation)) -> List(buildBatch(schema, \"e\", \"f\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            // Create a checkpoint for the table\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          // Non-default collations are not supported by the default engine for predicate\n          // evaluation. Assert that attempting to evaluate such predicates during partition\n          // pruning throws.\n          val failingPredicates = Seq(\n            new Predicate(\"<\", col(\"c1\"), ofString(\"a\"), utf8Lcase),\n            new Predicate(\"<\", col(\"c1\"), ofString(\"a\", c2Collation), utf8Lcase),\n            new Predicate(\"=\", ofString(\"d\"), col(\"c1\"), unicode),\n            new And(\n              new Predicate(\">=\", col(\"c1\"), ofString(\"b\"), utf8Lcase),\n              new Predicate(\"<=\", col(\"c1\"), ofString(\"e\"), unicode)),\n            new Or(\n              new Predicate(\"<\", col(\"c1\"), ofString(\"b\"), utf8Lcase),\n              new Predicate(\">\", col(\"c1\"), ofString(\"a\"), CollationIdentifier.SPARK_UTF8_BINARY)),\n            new Predicate(\"STARTS_WITH\", col(\"c1\"), ofString(\"a\"), utf8Lcase),\n            new In(\n              col(\"c1\"),\n              java.util.Arrays.asList(ofString(\"a\"), ofString(\"c\")),\n              utf8Lcase),\n            new In(\n              col(\"c1\"),\n              java.util.Arrays.asList(ofString(\"a\", c2Collation), ofString(\"c\")),\n              utf8Lcase),\n            new Or(\n              new In(col(\"c1\"), java.util.Arrays.asList(ofString(\"x\"), ofString(\"y\")), unicode),\n              new Predicate(\"=\", col(\"c1\"), ofString(\"z\"))))\n\n          failingPredicates.foreach { predicate =>\n            val ex = intercept[KernelEngineException] {\n              collectScanFileRows(snapshot.getScanBuilder().withFilter(predicate).build())\n            }\n            assert(ex.getMessage.contains(\"Unsupported collation\"))\n            assert(ex.getMessage.contains(CollationIdentifier.SPARK_UTF8_BINARY.toString))\n            assert(ex.getCause.isInstanceOf[UnsupportedOperationException])\n          }\n        }\n      }\n    }\n  }\n\n  test(\"data skipping - predicates with SPARK.UTF8_BINARY on data column\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          // Create three files with values on non-partitioned STRING columns (c1, c2)\n          // Files: (\"a\",\"x\"), (\"c\",\"y\"), (\"e\",\"z\")\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n\n          val c2Collation = c2Type.getCollationIdentifier\n\n          getCreateTxn(defaultEngine, tablePath, schema, List.empty).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"a\", \"x\")),\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"c\", \"y\")),\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"e\", \"z\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder.build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            // Create a checkpoint for the table\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val filterToFileNumber = Map(\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\")) -> 0,\n            new Predicate(\n              \"<\",\n              col(\"c1\"),\n              ofString(\"a\", c2Collation)) -> 0,\n            new Predicate(\n              \"=\",\n              ofString(\"d\"),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"=\",\n              ofString(\"a\"),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new Predicate(\n              \"=\",\n              ofString(\"a\", c2Collation),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new Predicate(\n              \"=\",\n              ofString(\"a\"),\n              col(\"c1\")) -> 1,\n            new Predicate(\n              \"=\",\n              ofString(\"a\", c2Collation),\n              col(\"c1\")) -> 1,\n            new Predicate(\n              \"<=\",\n              ofString(\"a\"),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new Predicate(\n              \"<=\",\n              ofString(\"a\", c2Collation),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new Predicate(\n              \"<=\",\n              ofString(\"a\"),\n              col(\"c1\")) -> totalFiles,\n            new Predicate(\n              \"<=\",\n              ofString(\"a\", c2Collation),\n              col(\"c1\")) -> totalFiles,\n            new Predicate(\n              \"<\",\n              ofString(\"e\"),\n              col(\"c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"b\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c1\"),\n                ofString(\"e\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new Or(\n              new Predicate(\n                \"=\",\n                col(\"c1\"),\n                ofString(\"x\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                col(\"c1\"),\n                ofString(\"d\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c1\"),\n                ofString(\"z\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> totalFiles,\n            new And(\n              new Predicate(\n                \"<=\",\n                ofString(\"b\"),\n                col(\"c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">=\",\n                ofString(\"y\"),\n                col(\"c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new And(\n              new Predicate(\n                \">=\",\n                ofString(\"c\"),\n                col(\"c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                ofString(\"y\"),\n                col(\"c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new Or(\n              new Predicate(\n                \"=\",\n                ofString(\"a\"),\n                col(\"c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"=\",\n                ofString(\"z\"),\n                col(\"c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new Or(\n              new Predicate(\n                \"<\",\n                ofString(\"d\"),\n                col(\"c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                ofString(\"y\"),\n                col(\"c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new And(\n              new Predicate(\n                \"<\",\n                ofString(\"e\"),\n                col(\"c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                ofString(\"y\"),\n                col(\"c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 0,\n            new And(\n              new Predicate(\n                \"<\",\n                ofString(\"e\"),\n                col(\"c1\")),\n              new Predicate(\n                \">\",\n                ofString(\"y\"),\n                col(\"c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 0,\n            new And(\n              new Predicate(\n                \"<\",\n                ofString(\"e\"),\n                col(\"c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                ofString(\"y\"),\n                col(\"c2\"))) -> 0)\n          checkSkipping(tempDir.getCanonicalPath, filterToFileNumber)\n        }\n      }\n    }\n  }\n\n  test(\"data skipping - predicate with collation without version on data column\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n\n          getCreateTxn(defaultEngine, tablePath, schema, List(\"c1\")).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map(\"c1\" -> ofString(\"a\")) -> List(buildBatch(schema, \"a\", \"b\")),\n              Map(\"c1\" -> ofString(\"c\")) -> List(buildBatch(schema, \"c\", \"d\")),\n              Map(\"c1\" -> ofString(\"e\")) -> List(buildBatch(schema, \"e\", \"f\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            // Create a checkpoint for the table\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val filterToFileNumber = Map(\n            new Predicate(\n              \"<\",\n              col(\"c2\"),\n              ofString(\"a\"),\n              CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\")) -> totalFiles)\n\n          checkSkipping(tablePath, filterToFileNumber)\n        }\n      }\n    }\n  }\n\n  test(\"data skipping - predicates with SPARK.UTF8_BINARY on nested data column\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          // Create three files with values on non-partitioned nested STRING columns (s.c1, s.c2)\n          // Files: (\"a\",\"x\"), (\"c\",\"y\"), (\"e\",\"z\")\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"s\", new StructType().add(\"c1\", c1Type, true).add(\"c2\", c2Type, true), true)\n\n          val c2Collation = c2Type.getCollationIdentifier\n\n          getCreateTxn(defaultEngine, tablePath, schema, List.empty).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map.empty[String, Literal] -> List(buildNestedBatch(schema, \"a\", \"x\")),\n              Map.empty[String, Literal] -> List(buildNestedBatch(schema, \"c\", \"y\")),\n              Map.empty[String, Literal] -> List(buildNestedBatch(schema, \"e\", \"z\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder.build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            // Create a checkpoint for the table\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val filterToFileNumber = Map(\n            new Predicate(\n              \"<\",\n              nestedCol(\"s.c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"=\",\n              ofString(\"d\"),\n              nestedCol(\"s.c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new Predicate(\n              \"=\",\n              ofString(\"a\"),\n              nestedCol(\"s.c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new Predicate(\n              \"=\",\n              ofString(\"a\", c2Collation),\n              nestedCol(\"s.c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 1,\n            new Predicate(\n              \"=\",\n              ofString(\"a\"),\n              nestedCol(\"s.c1\")) -> 1,\n            new Predicate(\n              \"=\",\n              ofString(\"a\", c2Collation),\n              nestedCol(\"s.c1\")) -> 1,\n            new Predicate(\n              \"<=\",\n              ofString(\"a\"),\n              nestedCol(\"s.c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new Predicate(\n              \"<\",\n              ofString(\"e\"),\n              nestedCol(\"s.c1\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> 0,\n            new And(\n              new Predicate(\n                \">=\",\n                nestedCol(\"s.c1\"),\n                ofString(\"b\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                nestedCol(\"s.c1\"),\n                ofString(\"e\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new Or(\n              new Predicate(\n                \"=\",\n                nestedCol(\"s.c1\"),\n                ofString(\"x\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                nestedCol(\"s.c1\"),\n                ofString(\"d\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new And(\n              new Predicate(\n                \">=\",\n                nestedCol(\"s.c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                nestedCol(\"s.c1\"),\n                ofString(\"z\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> totalFiles,\n            new And(\n              new Predicate(\n                \"<=\",\n                ofString(\"b\"),\n                nestedCol(\"s.c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">=\",\n                ofString(\"y\"),\n                nestedCol(\"s.c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new And(\n              new Predicate(\n                \">=\",\n                ofString(\"c\"),\n                nestedCol(\"s.c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                ofString(\"y\"),\n                nestedCol(\"s.c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new Or(\n              new Predicate(\n                \"=\",\n                ofString(\"a\"),\n                nestedCol(\"s.c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"=\",\n                ofString(\"z\"),\n                nestedCol(\"s.c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new Or(\n              new Predicate(\n                \"<\",\n                ofString(\"d\"),\n                nestedCol(\"s.c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                ofString(\"y\"),\n                nestedCol(\"s.c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new And(\n              new Predicate(\n                \"<\",\n                ofString(\"e\"),\n                nestedCol(\"s.c1\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">\",\n                ofString(\"y\"),\n                nestedCol(\"s.c2\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 0)\n          checkSkipping(tablePath, filterToFileNumber)\n        }\n      }\n    }\n  }\n\n  test(\"data skipping - collated predicates not or partially convertible to skipping filter\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n\n          getCreateTxn(defaultEngine, tablePath, schema, List.empty).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"a\", \"x\")),\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"c\", \"y\")),\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"e\", \"z\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val filterToFileNumber = Map(\n            new Predicate(\n              \"STARTS_WITH\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new Predicate(\n              \"STARTS_WITH\",\n              col(\"c1\"),\n              ofString(\"a\"),\n              utf8Lcase) -> totalFiles,\n            new Predicate(\n              \"STARTS_WITH\",\n              col(\"c1\"),\n              ofString(\"z\"),\n              unicode) -> totalFiles,\n            new In(\n              col(\"c1\"),\n              java.util.Arrays.asList(ofString(\"a\"), ofString(\"z\")),\n              CollationIdentifier.SPARK_UTF8_BINARY) -> totalFiles,\n            new In(\n              col(\"c2\"),\n              java.util.Arrays.asList(ofString(\"x\"), ofString(\"zz\")),\n              utf8Lcase) -> totalFiles,\n            new And(\n              new Predicate(\n                \"<\",\n                col(\"c1\"),\n                ofString(\"d\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new In(\n                col(\"c2\"),\n                java.util.Arrays.asList(ofString(\"x\"), ofString(\"zz\")),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new Or(\n              new Predicate(\"STARTS_WITH\", col(\"c1\"), ofString(\"a\"), utf8Lcase),\n              new In(\n                col(\"c2\"),\n                java.util.Arrays.asList(ofString(\"x\"), ofString(\"y\")),\n                unicode)) -> totalFiles,\n            new And(\n              new Predicate(\n                \"STARTS_WITH\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\"STARTS_WITH\", col(\"c2\"), ofString(\"x\"), unicode)) -> totalFiles)\n          checkSkipping(tempDir.getCanonicalPath, filterToFileNumber)\n        }\n      }\n    }\n  }\n\n  test(\"data skipping - evaluation fails with non default collation on data column\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING),\n        (utf8LcaseString, utf8LcaseString),\n        (unicodeString, unicodeString),\n        (utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString),\n        (unicodeString, STRING)).foreach { case (c1Type, c2Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n\n          val c2Collation = c2Type.getCollationIdentifier\n\n          getCreateTxn(defaultEngine, tablePath, schema, List.empty).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"a\", \"x\")),\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"c\", \"y\")),\n              Map.empty[String, Literal] -> List(buildBatch(schema, \"e\", \"z\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val failingPredicates = Seq(\n            new Predicate(\"<\", col(\"c1\"), ofString(\"a\"), utf8Lcase),\n            new Predicate(\"<\", col(\"c1\"), ofString(\"a\", c2Collation), utf8Lcase),\n            new Predicate(\"=\", ofString(\"d\"), col(\"c1\"), unicode),\n            new And(\n              new Predicate(\">=\", col(\"c1\"), ofString(\"b\"), utf8Lcase),\n              new Predicate(\"<=\", col(\"c1\"), ofString(\"e\"), unicode)),\n            new And(\n              new Predicate(\">=\", col(\"c1\"), ofString(\"b\"), utf8Lcase),\n              new Predicate(\"<=\", col(\"c1\"), ofString(\"e\"))),\n            new And(\n              new Predicate(\">=\", col(\"c1\"), ofString(\"b\")),\n              new Predicate(\"<=\", col(\"c1\"), ofString(\"e\"), unicode)),\n            new Or(\n              new Predicate(\"<\", col(\"c1\"), ofString(\"b\"), utf8Lcase),\n              new Predicate(\">\", col(\"c1\"), ofString(\"a\"), CollationIdentifier.SPARK_UTF8_BINARY)),\n            new Or(\n              new Predicate(\"<\", col(\"c1\"), ofString(\"b\", c2Collation), utf8Lcase),\n              new Predicate(\n                \">\",\n                col(\"c1\"),\n                ofString(\"a\", c2Collation),\n                CollationIdentifier.SPARK_UTF8_BINARY)),\n            new And(\n              new Predicate(\">=\", col(\"c1\"), ofString(\"a\"), utf8Lcase),\n              new Predicate(\"<=\", col(\"c1\"), ofString(\"z\"), unicode)),\n            new Predicate(\"=\", col(\"c1\"), ofString(\"a\"), utf8Lcase))\n\n          failingPredicates.foreach { predicate =>\n            val ex = intercept[KernelEngineException] {\n              collectScanFileRows(snapshot.getScanBuilder.withFilter(predicate).build())\n            }\n            assert(ex.getMessage.contains(\"Unsupported collation\"))\n            assert(ex.getMessage.contains(CollationIdentifier.SPARK_UTF8_BINARY.toString))\n            assert(ex.getCause.isInstanceOf[UnsupportedOperationException])\n          }\n        }\n      }\n    }\n  }\n\n  test(\"partition and data skipping - combined pruning on collated partition and data columns\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING, STRING),\n        (utf8LcaseString, utf8LcaseString, utf8LcaseString),\n        (utf8LcaseString, unicodeString, unicodeString),\n        (STRING, utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString, STRING),\n        (unicodeString, STRING, utf8LcaseString)).foreach { case (c1Type, c2Type, c3Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n            .add(\"c3\", c3Type, true)\n\n          getCreateTxn(defaultEngine, tablePath, schema, List(\"c1\")).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map(\"c1\" -> ofString(\"a\")) -> List(buildBatch(\n                schema,\n                \"a\",\n                \"x\",\n                \"u\")),\n              Map(\"c1\" -> ofString(\"c\")) -> List(buildBatch(\n                schema,\n                \"c\",\n                \"y\",\n                \"v\")),\n              Map(\"c1\" -> ofString(\"e\")) -> List(buildBatch(\n                schema,\n                \"e\",\n                \"z\",\n                \"w\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val filterToFileNumber: Map[Predicate, Int] = Map(\n            new And(\n              new Predicate(\n                \"<=\",\n                col(\"c2\"),\n                ofString(\"y\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"b\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 1,\n            new And(\n              new Predicate(\n                \"<=\",\n                col(\"c1\"),\n                ofString(\"c\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c2\"),\n                ofString(\"z\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 2,\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<\",\n                col(\"c2\"),\n                ofString(\"d\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> 0,\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c2\"),\n                ofString(\"z\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)) -> totalFiles)\n\n          checkSkipping(tempDir.getCanonicalPath, filterToFileNumber)\n        }\n      }\n    }\n  }\n\n  test(\"partition and data skipping - evaluation fails with non default collation on \" +\n    \"combined filter\") {\n    Seq(true, false).foreach { createCheckpoint =>\n      Seq(\n        (STRING, STRING, STRING),\n        (utf8LcaseString, utf8LcaseString, utf8LcaseString),\n        (utf8LcaseString, unicodeString, unicodeString),\n        (STRING, utf8LcaseString, unicodeString),\n        (STRING, utf8LcaseString, STRING),\n        (unicodeString, STRING, utf8LcaseString)).foreach { case (c1Type, c2Type, c3Type) =>\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getCanonicalPath\n          val schema = new StructType()\n            .add(\"c1\", c1Type, true)\n            .add(\"c2\", c2Type, true)\n            .add(\"c3\", c3Type, true)\n\n          getCreateTxn(defaultEngine, tablePath, schema, List(\"c1\")).commit(\n            defaultEngine,\n            emptyIterable())\n\n          appendData(\n            defaultEngine,\n            tablePath,\n            data = List(\n              Map(\"c1\" -> ofString(\"a\")) -> List(buildBatch(\n                schema,\n                \"a\",\n                \"x\",\n                \"u\")),\n              Map(\"c1\" -> ofString(\"c\")) -> List(buildBatch(\n                schema,\n                \"c\",\n                \"y\",\n                \"v\")),\n              Map(\"c1\" -> ofString(\"e\")) -> List(buildBatch(\n                schema,\n                \"e\",\n                \"z\",\n                \"w\"))))\n\n          val snapshot = latestSnapshot(tablePath)\n          val totalFiles = collectScanFileRows(snapshot.getScanBuilder().build()).length\n\n          assert(totalFiles == 3)\n\n          if (createCheckpoint) {\n            val version = latestSnapshot(tempDir.getCanonicalPath).getVersion\n            Table.forPath(\n              defaultEngine,\n              tempDir.getCanonicalPath).checkpoint(defaultEngine, version)\n          }\n\n          val failingPredicates = Seq(\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                CollationIdentifier.SPARK_UTF8_BINARY),\n              new Predicate(\n                \"<=\",\n                col(\"c2\"),\n                ofString(\"z\"),\n                utf8Lcase)),\n            new And(\n              new Predicate(\n                \">=\",\n                col(\"c1\"),\n                ofString(\"a\"),\n                unicode),\n              new Predicate(\n                \"<=\",\n                col(\"c2\"),\n                ofString(\"z\"),\n                CollationIdentifier.SPARK_UTF8_BINARY)),\n            new And(\n              new Predicate(\n                \"<\",\n                col(\"c1\"),\n                ofString(\"z\"),\n                utf8Lcase),\n              new Predicate(\n                \">\",\n                col(\"c2\"),\n                ofString(\"a\"),\n                unicode)))\n\n          failingPredicates.foreach { predicate =>\n            val ex = intercept[KernelEngineException] {\n              collectScanFileRows(snapshot.getScanBuilder.withFilter(predicate).build())\n            }\n            assert(ex.getMessage.contains(\"Unsupported collation\"))\n            assert(ex.getMessage.contains(CollationIdentifier.SPARK_UTF8_BINARY.toString))\n            assert(ex.getCause.isInstanceOf[UnsupportedOperationException])\n          }\n        }\n      }\n    }\n  }\n\n  Seq(\n    \"spark-variant-checkpoint\",\n    \"spark-variant-stable-feature-checkpoint\",\n    \"spark-shredded-variant-preview-delta\").foreach { tableName =>\n    Seq(\n      (\"version 0 no predicate\", None, Some(0), 2),\n      (\"latest version (has checkpoint) no predicate\", None, None, 4),\n      (\"version 0 with predicate\", Some(equals(col(\"id\"), ofLong(10))), Some(0), 1)).foreach {\n      case (nameSuffix, predicate, snapshotVersion, expectedNumFiles) =>\n        test(s\"read scan files with variant - $nameSuffix - $tableName\") {\n          val path = getTestResourceFilePath(tableName)\n          val table = Table.forPath(defaultEngine, path)\n          val snapshot = snapshotVersion match {\n            case Some(version) => table.getSnapshotAsOfVersion(defaultEngine, version)\n            case None => table.getLatestSnapshot(defaultEngine)\n          }\n          val snapshotSchema = snapshot.getSchema()\n\n          val expectedSchema = new StructType()\n            .add(\"id\", LongType.LONG, true)\n            .add(\"v\", VariantType.VARIANT, true)\n            .add(\"array_of_variants\", new ArrayType(VariantType.VARIANT, true), true)\n            .add(\"struct_of_variants\", new StructType().add(\"v\", VariantType.VARIANT, true))\n            .add(\"map_of_variants\", new MapType(StringType.STRING, VariantType.VARIANT, true), true)\n            .add(\n              \"array_of_struct_of_variants\",\n              new ArrayType(new StructType().add(\"v\", VariantType.VARIANT, true), true),\n              true)\n            .add(\n              \"struct_of_array_of_variants\",\n              new StructType().add(\"v\", new ArrayType(VariantType.VARIANT, true), true),\n              true)\n\n          assert(snapshotSchema == expectedSchema)\n\n          val scanBuilder = snapshot.getScanBuilder()\n          val scan = predicate match {\n            case Some(pred) => scanBuilder.withFilter(pred).build()\n            case None => scanBuilder.build()\n          }\n          val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(defaultEngine, true)\n          var numFiles: Int = 0\n          scanFiles.forEach { s =>\n            numFiles += s.getRows().toSeq.length\n          }\n          assert(numFiles == expectedNumFiles)\n        }\n    }\n  }\n}\n\nobject ScanSuite {\n\n  private def throwErrorIfAddStatsInSchema(readSchema: StructType): Unit = {\n    if (readSchema.indexOf(\"add\") >= 0) {\n      val addSchema = readSchema.get(\"add\").getDataType.asInstanceOf[StructType]\n      assert(addSchema.indexOf(\"stats\") < 0, \"reading column add.stats is not allowed\");\n    }\n  }\n\n  /**\n   * Returns a custom engine implementation that doesn't allow \"add.stats\" in the read schema\n   * for parquet or json handlers.\n   */\n  def engineDisallowedStatsReads: Engine = {\n    val fileIO = new HadoopFileIO(new Configuration())\n    new DefaultEngine(fileIO) {\n\n      override def getParquetHandler: ParquetHandler = {\n        new DefaultParquetHandler(fileIO) {\n          override def readParquetFiles(\n              fileIter: CloseableIterator[FileStatus],\n              physicalSchema: StructType,\n              predicate: Optional[Predicate]): CloseableIterator[FileReadResult] = {\n            throwErrorIfAddStatsInSchema(physicalSchema)\n            super.readParquetFiles(fileIter, physicalSchema, predicate)\n          }\n        }\n      }\n\n      override def getJsonHandler: JsonHandler = {\n        new DefaultJsonHandler(fileIO) {\n          override def readJsonFiles(\n              fileIter: CloseableIterator[FileStatus],\n              physicalSchema: StructType,\n              predicate: Optional[Predicate]): CloseableIterator[ColumnarBatch] = {\n            throwErrorIfAddStatsInSchema(physicalSchema)\n            super.readJsonFiles(fileIter, physicalSchema, predicate)\n          }\n        }\n      }\n    }\n  }\n\n  def engineVerifyJsonParseSchema(verifyFx: StructType => Unit): Engine = {\n    val fileIO = new HadoopFileIO(new Configuration())\n    new DefaultEngine(fileIO) {\n      override def getJsonHandler: JsonHandler = {\n        new DefaultJsonHandler(fileIO) {\n          override def parseJson(\n              stringVector: ColumnVector,\n              schema: StructType,\n              selectionVector: Optional[ColumnVector]): ColumnarBatch = {\n            verifyFx(schema)\n            super.parseJson(stringVector, schema, selectionVector)\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/SnapshotChecksumStatisticsAndWriteSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport io.delta.kernel.{Operation, TableManager}\nimport io.delta.kernel.Snapshot.ChecksumWriteMode\nimport io.delta.kernel.defaults.utils.TestUtils\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass SnapshotChecksumStatisticsAndWriteSuite extends AnyFunSuite with TestUtils {\n\n  val testSchema = new StructType().add(\"id\", INTEGER)\n\n  private def assertCrcExistsAtLatest(engine: Engine, tablePath: String): Unit = {\n    val latestSnapshot = TableManager.loadSnapshot(tablePath).build(engine)\n    assert(latestSnapshot.getStatistics.getChecksumWriteMode.isEmpty)\n  }\n\n  test(\"getChecksumWriteMode: CRC already exists => empty (trivial case)\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // GIVEN\n      val txn0 = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"x\").build(engine)\n      val result0 = txn0.commit(engine, emptyIterable())\n      val snapshot0 = result0.getPostCommitSnapshot.get()\n      snapshot0.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n\n      // WHEN/THEN\n      assertCrcExistsAtLatest(engine, tablePath) // this is what we are really testing. trivial.\n    }\n  }\n\n  test(\"getChecksumWriteMode: created new table => SIMPLE\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== WHEN =====\n      val txn0 = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      val result0 = txn0.commit(engine, emptyIterable())\n\n      // ===== THEN =====\n      val snapshot0 = result0.getPostCommitSnapshot.get()\n      assert(snapshot0.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      snapshot0.writeChecksum(engine, ChecksumWriteMode.SIMPLE) // we can write it!\n      assertCrcExistsAtLatest(engine, tablePath) // it exists now\n    }\n  }\n\n  test(\"getChecksumWriteMode: CRC exists at N-1 => SIMPLE\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      val txn0 = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      val result0 = txn0.commit(engine, emptyIterable())\n      val snapshot0 = result0.getPostCommitSnapshot.get()\n      snapshot0.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n      assertCrcExistsAtLatest(engine, tablePath)\n\n      // ===== WHEN =====\n      val txn1 = snapshot0.buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n      val result1 = txn1.commit(engine, emptyIterable())\n\n      // ===== THEN =====\n      val snapshot1 = result1.getPostCommitSnapshot.get()\n      assert(snapshot1.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      snapshot1.writeChecksum(engine, ChecksumWriteMode.SIMPLE) // we can write it!\n      assertCrcExistsAtLatest(engine, tablePath) // it exists now\n    }\n  }\n\n  test(\"getChecksumWriteMode: CRC gap exists (no CRC at N-1) with fresh Snapshot => FULL\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      val txn0 = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      txn0.commit(engine, emptyIterable()) // We do NOT write 00.crc\n\n      // ===== WHEN =====\n      // We explicitly load a fresh Snapshot. If we used the post-commit Snapshot,the mode would be\n      // SIMPLE! See the test below.\n      val txn1 = TableManager\n        .loadSnapshot(tablePath)\n        .build(engine)\n        .buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n      val result1 = txn1.commit(engine, emptyIterable())\n      val snapshot1 = result1.getPostCommitSnapshot.get()\n\n      // ===== THEN =====\n      assert(snapshot1.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.FULL)\n      snapshot1.writeChecksum(engine, ChecksumWriteMode.FULL) // we can write it!\n      assertCrcExistsAtLatest(engine, tablePath) // it exists now\n    }\n  }\n\n  // Some additional context: This tests that even if there is no physical CRC file, a post-commit\n  // snapshot, and even the 20th post-commit snapshot in a continuous sequence of writes, will still\n  // have the CRC info loaded in memory, and thus the mode is SIMPLE.\n  test(\"getChecksumWriteMode: PostCommitSnapshot (starting from CREATE) => always SIMPLE\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      // Create the table and do NOT write 00.crc.\n      var txn = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      var postCommitSnapshot = txn.commit(engine, emptyIterable()).getPostCommitSnapshot.get()\n      assert(postCommitSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n\n      // ===== WHEN ====\n      for (_ <- 1 to 20) {\n        // NOTE: We do NOT write N.crc either!\n        txn = postCommitSnapshot.buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n        postCommitSnapshot = txn.commit(engine, emptyIterable()).getPostCommitSnapshot.get()\n\n        // Nonetheless, our post-commit snapshot (starting from CREATE) should have the CRC info\n        // loaded into memory ==> SIMPLE\n        assert(\n          postCommitSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      }\n\n      // ===== THEN =====\n      // We can now write 20.crc via the SIMPLE mode, even though 0 to 19.crc do not exist!\n      postCommitSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n      assertCrcExistsAtLatest(engine, tablePath)\n    }\n  }\n\n  // Some additional context: This tests that when starting from a fresh snapshot with an existing\n  // CRC file (at version 10), all subsequent post-commit snapshots in a continuous sequence will\n  // inherit and maintain the CRC info in memory, making the mode SIMPLE even without intermediate\n  // CRC files being written.\n  test(\"getChecksumWriteMode: PostCommitSnapshot (starting from N>0 with CRC) => always SIMPLE\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      // Create the table and do NOT write 00.crc.\n      var txn = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      var postCommitSnapshot = txn.commit(engine, emptyIterable()).getPostCommitSnapshot.get()\n      assert(postCommitSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n\n      for (_ <- 1 to 10) {\n        // Commit versions 1-10 without writing CRC files\n        txn = postCommitSnapshot.buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n        postCommitSnapshot = txn.commit(engine, emptyIterable()).getPostCommitSnapshot.get()\n      }\n\n      // Versions 0 to 9 do NOT have CRCs. Now we write 10.crc.\n      assert(postCommitSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      postCommitSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n\n      // Now, we restart our txn write loop, but using a FRESH Snapshot loaded from version 10.\n      // It will see the 10.crc file.\n      var postCommitSnapshot2 = TableManager.loadSnapshot(tablePath).build(engine)\n\n      // ===== WHEN =====\n      for (_ <- 11 to 20) {\n        // NOTE: We do NOT write N.crc either!\n        txn = postCommitSnapshot2.buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n        postCommitSnapshot2 = txn.commit(engine, emptyIterable()).getPostCommitSnapshot.get()\n\n        // Nonetheless, our post-commit snapshot (starting from a FRESH Snapshot at version 10)\n        // should have the CRC info loaded into memory ==> SIMPLE\n        assert(\n          postCommitSnapshot2.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      }\n\n      // ===== THEN =====\n      // We can now write 20.crc via the SIMPLE mode, even though 11 to 19.crc do not exist!\n      postCommitSnapshot2.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n      assertCrcExistsAtLatest(engine, tablePath)\n    }\n  }\n\n  test(\"invoking writeChecksum with SIMPLE mode when actual mode is FULL => throws\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .build(engine)\n        .commit(engine, emptyIterable())\n\n      val latestSnapshot = TableManager.loadSnapshot(tablePath).build(engine)\n\n      assert(latestSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.FULL)\n\n      intercept[IllegalStateException] {\n        latestSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n      }\n    }\n  }\n\n  test(\"invoking writeChecksum when checksum already exists => no-op\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val snapshot = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .build(engine)\n        .commit(engine, emptyIterable())\n        .getPostCommitSnapshot.get()\n      snapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n\n      val latestSnapshot = TableManager.loadSnapshot(tablePath).build(engine)\n      assert(latestSnapshot.getStatistics.getChecksumWriteMode.isEmpty)\n\n      // Both SIMPLE and FULL should be no-op when checksum already exists\n      latestSnapshot.writeChecksum(engine, ChecksumWriteMode.FULL) // no-op, should not throw\n      latestSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE) // no-op, should not throw\n    }\n  }\n\n  test(\"invoking writeChecksum with FULL mode when actual mode is SIMPLE => succeeds\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val snapshot = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"x\")\n        .build(engine).commit(engine, emptyIterable()).getPostCommitSnapshot.get()\n\n      assert(snapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      snapshot.writeChecksum(engine, ChecksumWriteMode.FULL)\n      assertCrcExistsAtLatest(engine, tablePath)\n    }\n  }\n\n  // Note that we can only use SIMPLE when starting from a post-commit snapshot whose transaction\n  // started with a CRC file. Even if there's a CRC file at historical version N-1, we still need to\n  // do a FULL replay to load the CRC file at version N to write it.\n  test(\"write checksum at historical version => FULL mode\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create version 0 without writing its CRC\n      val txn0 = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      txn0.commit(engine, emptyIterable())\n\n      // Create version 1 without writing its CRC\n      val snapshot0 = TableManager.loadSnapshot(tablePath).build(engine)\n      val txn1 = snapshot0.buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n      txn1.commit(engine, emptyIterable())\n\n      // Create version 2 without writing its CRC\n      val snapshot1 = TableManager.loadSnapshot(tablePath).build(engine)\n      val txn2 = snapshot1.buildUpdateTableTransaction(\"xx\", Operation.WRITE).build(engine)\n      txn2.commit(engine, emptyIterable())\n\n      // Now load the historical snapshot at version 1 and check its mode\n      val historicalSnapshot = TableManager.loadSnapshot(tablePath).atVersion(1).build(engine)\n      assert(historicalSnapshot.getVersion == 1)\n      assert(historicalSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.FULL)\n\n      // Write checksum for the historical version 1\n      historicalSnapshot.writeChecksum(engine, ChecksumWriteMode.FULL)\n\n      // Verify CRC file exists for version 1\n      val snapshot1Again = TableManager.loadSnapshot(tablePath).atVersion(1).build(engine)\n      assert(snapshot1Again.getStatistics.getChecksumWriteMode.isEmpty)\n    }\n  }\n\n  test(\"concurrent checksum write => second write still returns successfully without error\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // ===== GIVEN =====\n      // Step 1: Create a table (v0.json) and get the post-commit snapshot\n      val txn = TableManager.buildCreateTableTransaction(tablePath, testSchema, \"xx\").build(engine)\n      val result = txn.commit(engine, emptyIterable())\n      val postCommitSnapshot = result.getPostCommitSnapshot.get()\n\n      // Step 2: Load a new snapshot to latest (v0)\n      val freshSnapshot = TableManager.loadSnapshot(tablePath).build(engine)\n      assert(freshSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.FULL)\n\n      // ===== WHEN =====\n      // Step 3: Use the fresh snapshot to write the checksum\n      freshSnapshot.writeChecksum(engine, ChecksumWriteMode.FULL)\n      assertCrcExistsAtLatest(engine, tablePath)\n\n      // ===== THEN =====\n      // Step 4: Use the first (post-commit) snapshot to write the checksum -- should NOT fail\n      // This simulates a concurrent write scenario where another writer already wrote the CRC\n      assert(postCommitSnapshot.getStatistics.getChecksumWriteMode.get == ChecksumWriteMode.SIMPLE)\n      postCommitSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE) // should be a no-op\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/SnapshotSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{Operation, Table}\nimport io.delta.kernel.defaults.utils.{AbstractTestUtils, TestUtilsWithLegacyKernelAPIs, TestUtilsWithTableManagerAPIs}\nimport io.delta.kernel.types.{IntegerType, StructField, StructType}\nimport io.delta.kernel.utils.CloseableIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass SnapshotSuite extends AbstractSnapshotSuite with TestUtilsWithTableManagerAPIs\n\nclass LegacySnapshotSuite extends AbstractSnapshotSuite with TestUtilsWithLegacyKernelAPIs\n\ntrait AbstractSnapshotSuite extends AnyFunSuite {\n  self: AbstractTestUtils =>\n\n  Seq(\n    Seq(\"part1\"), // simple case\n    Seq(\"part1\", \"part2\", \"part3\"), // multiple partition columns\n    Seq(), // non-partitioned\n    Seq(\"PART1\", \"part2\") // case-sensitive\n  ).foreach { partCols =>\n    test(s\"Snapshot getPartitionColumnNames - partCols=$partCols\") {\n      withTempDir { dir =>\n        // Step 1: Create a table with the given partition columns\n        val table = Table.forPath(defaultEngine, dir.getCanonicalPath)\n\n        val columns = (partCols ++ Seq(\"col1\", \"col2\")).map { colName =>\n          new StructField(colName, IntegerType.INTEGER, true /* nullable */ )\n        }\n\n        val schema = new StructType(columns.asJava)\n\n        var txnBuilder = table\n          .createTransactionBuilder(defaultEngine, \"engineInfo\", Operation.CREATE_TABLE)\n          .withSchema(defaultEngine, schema)\n\n        if (partCols.nonEmpty) {\n          txnBuilder = txnBuilder.withPartitionColumns(defaultEngine, partCols.asJava)\n        }\n\n        txnBuilder.build(defaultEngine).commit(defaultEngine, CloseableIterable.emptyIterable())\n\n        // Step 2: Check the partition columns\n        val tablePartCols =\n          getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, dir.getCanonicalPath)\n            .getPartitionColumnNames()\n\n        assert(partCols.asJava === tablePartCols)\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/TableChangesSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.{Table, TableManager}\nimport io.delta.kernel.CommitRangeBuilder.CommitBoundary\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{TestUtils, WriteUtils}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.{CommitRangeNotFoundException, KernelException, TableNotFoundException, UnsupportedProtocolVersionException}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.DeltaLogActionUtils.DeltaAction\nimport io.delta.kernel.internal.TableImpl\nimport io.delta.kernel.internal.actions.{AddCDCFile, AddFile, CommitInfo, Metadata, Protocol, RemoveFile, SetTransaction}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.util.{FileNames, ManualClock, VectorUtils}\nimport io.delta.kernel.types.{DataType, LongType, StructField}\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.{Action => SparkAction, AddCDCFile => SparkAddCDCFile, AddFile => SparkAddFile, CommitInfo => SparkCommitInfo, Metadata => SparkMetadata, Protocol => SparkProtocol, RemoveFile => SparkRemoveFile, SetTransaction => SparkSetTransaction}\n\nimport org.apache.hadoop.fs.{Path => HadoopPath}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.{IntegerType, StructType}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LegacyTableChangesSuite extends TableChangesSuite {\n\n  override def getChanges(\n      tablePath: String,\n      startVersion: Long,\n      endVersion: Long,\n      actionSet: Set[DeltaAction]): Seq[ColumnarBatch] = {\n    Table.forPath(defaultEngine, tablePath)\n      .asInstanceOf[TableImpl]\n      .getChanges(defaultEngine, startVersion, endVersion, actionSet.asJava)\n      .toSeq\n  }\n}\n\nclass CommitRangeTableChangesSuite extends TableChangesSuite {\n\n  override def getChanges(\n      tablePath: String,\n      startVersion: Long,\n      endVersion: Long,\n      actionSet: Set[DeltaAction]): Seq[ColumnarBatch] = {\n    val commitRange =\n      TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(startVersion))\n        .withEndBoundary(CommitBoundary.atVersion(endVersion))\n        .build(defaultEngine)\n    commitRange.getActions(\n      defaultEngine,\n      getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tablePath, startVersion),\n      actionSet.asJava).toSeq\n  }\n\n  test(\"Must provide startSnapshot with the correct version\") {\n    withTempDir { tempDir =>\n      (0 to 4).foreach { _ =>\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(tempDir.getCanonicalPath)\n      }\n      val commitRange =\n        TableManager.loadCommitRange(tempDir.getCanonicalPath, CommitBoundary.atVersion(0))\n          .withEndBoundary(CommitBoundary.atVersion(4))\n          .build(defaultEngine)\n      val e = intercept[IllegalArgumentException] {\n        commitRange.getActions(\n          defaultEngine,\n          getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tempDir.getCanonicalPath, 2),\n          FULL_ACTION_SET.asJava).toSeq\n      }\n      assert(e.getMessage.contains(\"startSnapshot must have version = startVersion\"))\n    }\n  }\n\n  test(\"No end boundary provided defaults to latest\") {\n    withTempDir { tempDir =>\n      (0 to 4).foreach { _ =>\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(tempDir.getCanonicalPath)\n      }\n      val commitRange =\n        TableManager.loadCommitRange(tempDir.getCanonicalPath, CommitBoundary.atVersion(0))\n          .build(defaultEngine)\n      assert(commitRange.getStartVersion == 0 && commitRange.getEndVersion == 4)\n      // Just double check the changes are correct\n      testGetChangesVsSpark(tempDir.getCanonicalPath, 0, 4, FULL_ACTION_SET)\n    }\n  }\n\n  test(\"Basic timestamp resolution\") {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n      // Setup part 1 of 2: create log files\n      (0 to 2).foreach { i =>\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n      }\n\n      // Setup part 2 of 2: edit lastModified times\n      val logPath = new Path(dir.getCanonicalPath, \"_delta_log\")\n\n      val delta0 = new File(FileNames.deltaFile(logPath, 0))\n      val delta1 = new File(FileNames.deltaFile(logPath, 1))\n      val delta2 = new File(FileNames.deltaFile(logPath, 2))\n      delta0.setLastModified(1000)\n      delta1.setLastModified(2000)\n      delta2.setLastModified(3000)\n\n      val latestSnapshot = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, tablePath)\n      def checkStartBoundary(timestamp: Long, expectedVersion: Long): Unit = {\n        assert(TableManager.loadCommitRange(\n          tablePath,\n          CommitBoundary.atTimestamp(timestamp, latestSnapshot))\n          .build(defaultEngine)\n          .getStartVersion == expectedVersion)\n      }\n      def checkEndBoundary(timestamp: Long, expectedVersion: Long): Unit = {\n        assert(TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n          .withEndBoundary(CommitBoundary.atTimestamp(timestamp, latestSnapshot))\n          .build(defaultEngine)\n          .getEndVersion == expectedVersion)\n      }\n\n      // startTimestamp is before the earliest available version\n      checkStartBoundary(500, 0)\n      // endTimestamp is before the earliest available version\n      intercept[KernelException] {\n        checkEndBoundary(500, -1)\n      }\n\n      // startTimestamp is at first commit\n      checkStartBoundary(1000, 0)\n      // endTimestamp is at first commit\n      checkEndBoundary(1000, 0)\n\n      // startTimestamp is between two normal commits\n      checkStartBoundary(1500, 1)\n      // endTimestamp is between two normal commits\n      checkEndBoundary(1500, 0)\n\n      // startTimestamp is at last commit\n      checkStartBoundary(3000, 2)\n\n      // endTimestamp is at last commit\n      checkEndBoundary(3000, 2)\n\n      // startTimestamp is after the last commit\n      intercept[KernelException] {\n        checkStartBoundary(4000, -1)\n      }\n      // endTimestamp is after the last commit\n      checkEndBoundary(4000, 2)\n    }\n  }\n\n  test(\"ICT timestamp resolution\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a table with ICT enabled from the start and commits with specific custom-set ICTs\n      val startTime = 1000L\n      val clock = new ManualClock(startTime)\n\n      // Version 0 has ICT=1000L, but modificationTime=approx current time (1757368326512+)\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testSchema,\n        data = immutable.Seq(Map.empty[String, Literal] -> dataBatches1),\n        clock = clock,\n        tableProperties = Map(\"delta.enableInCommitTimestamps\" -> \"true\"))\n\n      // Version 1 has ICT=2000L\n      clock.setTime(startTime + 1000)\n      appendData(\n        engine,\n        tablePath,\n        data = immutable.Seq(Map.empty[String, Literal] -> (dataBatches1 ++ dataBatches2)),\n        clock = clock)\n\n      // Version 2 has ICT=3000L\n      clock.setTime(startTime + 2000)\n      appendData(\n        engine,\n        tablePath,\n        data = immutable.Seq(Map.empty[String, Literal] -> dataBatches1),\n        clock = clock)\n\n      val latestSnapshot = getTableManagerAdapter.getSnapshotAtLatest(defaultEngine, tablePath)\n\n      def checkStartBoundary(timestamp: Long, expectedVersion: Long): Unit = {\n        assert(TableManager.loadCommitRange(\n          tablePath,\n          CommitBoundary.atTimestamp(timestamp, latestSnapshot))\n          .build(defaultEngine)\n          .getStartVersion == expectedVersion)\n      }\n      def checkEndBoundary(timestamp: Long, expectedVersion: Long): Unit = {\n        assert(TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n          .withEndBoundary(CommitBoundary.atTimestamp(timestamp, latestSnapshot))\n          .build(defaultEngine)\n          .getEndVersion == expectedVersion)\n      }\n\n      // Test that timestamp resolution is done using ICT. Since the file modification times for\n      // this table should all be approx the current time, if we are able to correctly resolve\n      // TS-to-version for the timestamp-range we custom set (~1000L-3000L), we know we are\n      // correctly resolving with ICT.\n\n      // startTimestamp is before the earliest available version\n      checkStartBoundary(startTime - 500, 0)\n      // endTimestamp is before the earliest available version\n      intercept[KernelException] {\n        checkEndBoundary(startTime - 500, -1)\n      }\n\n      // startTimestamp is at first commit (ICT enabled)\n      checkStartBoundary(startTime, 0)\n      // endTimestamp is at first commit\n      checkEndBoundary(startTime, 0)\n\n      // startTimestamp is between first and second commit\n      checkStartBoundary(startTime + 500, 1)\n      // endTimestamp is between first and second commit\n      checkEndBoundary(startTime + 500, 0)\n\n      // startTimestamp is at second commit\n      checkStartBoundary(startTime + 1000, 1)\n      // endTimestamp is at second commit\n      checkEndBoundary(startTime + 1000, 1)\n\n      // startTimestamp is between second and third commit\n      checkStartBoundary(startTime + 1500, 2)\n      // endTimestamp is between second and third commit\n      checkEndBoundary(startTime + 1500, 1)\n\n      // startTimestamp is at third commit\n      checkStartBoundary(startTime + 2000, 2)\n      // endTimestamp is at third commit\n      checkEndBoundary(startTime + 2000, 2)\n\n      // startTimestamp is after the last commit\n      intercept[KernelException] {\n        checkStartBoundary(startTime + 3000, -1)\n      }\n      // endTimestamp is after the last commit\n      checkEndBoundary(startTime + 3000, 2)\n\n      // Verify that the changes are correctly retrieved using ICT timestamps\n      testGetChangesVsSpark(tablePath, 0, 2, FULL_ACTION_SET)\n    }\n  }\n\n  test(\"getCommitActions returns CommitActions with correct version and timestamp\") {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n\n      // Create commits with known modification times\n      (0 to 2).foreach { i =>\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n      }\n\n      // Set custom modification times on delta files\n      val logPath = new Path(dir.getCanonicalPath, \"_delta_log\")\n      val delta0 = new File(FileNames.deltaFile(logPath, 0))\n      val delta1 = new File(FileNames.deltaFile(logPath, 1))\n      val delta2 = new File(FileNames.deltaFile(logPath, 2))\n      delta0.setLastModified(1000)\n      delta1.setLastModified(2000)\n      delta2.setLastModified(3000)\n\n      val commitRange = TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n        .withEndBoundary(CommitBoundary.atVersion(2))\n        .build(defaultEngine)\n\n      val actionSet = Set(\n        DeltaAction.ADD,\n        DeltaAction.REMOVE,\n        DeltaAction.METADATA,\n        DeltaAction.PROTOCOL,\n        DeltaAction.CDC)\n\n      val commitsIter = commitRange.getCommitActions(\n        defaultEngine,\n        getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tablePath, 0),\n        actionSet.asJava)\n\n      val commits = commitsIter.toSeq\n      assert(commits.size == 3)\n\n      // Verify versions\n      assert(commits(0).getVersion == 0)\n      assert(commits(1).getVersion == 1)\n      assert(commits(2).getVersion == 2)\n\n      // Verify timestamps match file modification times (no ICT in this table)\n      assert(commits(0).getTimestamp == 1000)\n      assert(commits(1).getTimestamp == 2000)\n      assert(commits(2).getTimestamp == 3000)\n\n      // Get Spark's results for comparison\n      val sparkChanges = DeltaLog.forTable(spark, tablePath)\n        .getChanges(0)\n\n      // Compare actions with Spark using the new compareCommitActions method\n      compareCommitActions(commits, pruneSparkActionsByActionSet(sparkChanges, actionSet))\n\n      commitsIter.close()\n    }\n  }\n\n  test(\"getCommitActions with ICT returns timestamp from inCommitTimestamp\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val startTime = 5000L\n      val clock = new ManualClock(startTime)\n\n      // Version 0 with ICT=5000L\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testSchema,\n        data = immutable.Seq(Map.empty[String, Literal] -> dataBatches1),\n        clock = clock,\n        tableProperties = Map(\"delta.enableInCommitTimestamps\" -> \"true\"))\n\n      // Version 1 with ICT=6000L\n      clock.setTime(startTime + 1000)\n      appendData(\n        engine,\n        tablePath,\n        data = immutable.Seq(Map.empty[String, Literal] -> (dataBatches1 ++ dataBatches2)),\n        clock = clock)\n\n      // Version 2 with ICT=7000L\n      clock.setTime(startTime + 2000)\n      appendData(\n        engine,\n        tablePath,\n        data = immutable.Seq(Map.empty[String, Literal] -> dataBatches1),\n        clock = clock)\n\n      val commitRange = TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n        .withEndBoundary(CommitBoundary.atVersion(2))\n        .build(defaultEngine)\n\n      val actionSet = Set(\n        DeltaAction.ADD,\n        DeltaAction.REMOVE,\n        DeltaAction.METADATA,\n        DeltaAction.PROTOCOL,\n        DeltaAction.CDC)\n\n      val commitsIter = commitRange.getCommitActions(\n        engine,\n        getTableManagerAdapter.getSnapshotAtVersion(engine, tablePath, 0),\n        actionSet.asJava)\n\n      val commits = commitsIter.toSeq\n      assert(commits.size == 3)\n\n      // Verify versions\n      assert(commits(0).getVersion == 0)\n      assert(commits(1).getVersion == 1)\n      assert(commits(2).getVersion == 2)\n\n      // Verify timestamps come from ICT, not file modification times\n      // The file modification times would be much larger (current epoch time)\n      // but our ICT values are in the 5000-7000 range\n      assert(commits(0).getTimestamp == 5000)\n      assert(commits(1).getTimestamp == 6000)\n      assert(commits(2).getTimestamp == 7000)\n\n      // Get Spark's results for comparison\n      val sparkChanges = DeltaLog.forTable(spark, tablePath)\n        .getChanges(0)\n      compareCommitActions(commits, pruneSparkActionsByActionSet(sparkChanges, actionSet))\n      commitsIter.close()\n    }\n  }\n\n  test(\"getCommitActions can be called multiple times and returns same results\") {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n\n      // Create some commits\n      (0 to 2).foreach { i =>\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n      }\n\n      val commitRange = TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n        .withEndBoundary(CommitBoundary.atVersion(2))\n        .build(defaultEngine)\n\n      val actionSet = Set(\n        DeltaAction.ADD,\n        DeltaAction.REMOVE,\n        DeltaAction.METADATA,\n        DeltaAction.PROTOCOL,\n        DeltaAction.CDC)\n\n      // Call getCommitActions multiple times\n      val commits1 = commitRange.getCommitActions(\n        defaultEngine,\n        getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tablePath, 0),\n        actionSet.asJava).toSeq\n\n      val commits2 = commitRange.getCommitActions(\n        defaultEngine,\n        getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tablePath, 0),\n        actionSet.asJava).toSeq\n\n      val commits3 = commitRange.getCommitActions(\n        defaultEngine,\n        getTableManagerAdapter.getSnapshotAtVersion(defaultEngine, tablePath, 0),\n        actionSet.asJava).toSeq\n\n      // Verify all calls return the same number of commits\n      assert(commits1.size == 3)\n      assert(commits2.size == 3)\n      assert(commits3.size == 3)\n\n      // Verify each call returns the same actions by comparing with Spark\n      val sparkChanges = DeltaLog.forTable(spark, tablePath)\n        .getChanges(0)\n        .filter(_._1 <= 2)\n      compareCommitActions(commits1, pruneSparkActionsByActionSet(sparkChanges, actionSet))\n\n      // For commits2 and commits3, we need fresh Spark iterators\n      val sparkChanges2 = DeltaLog.forTable(spark, tablePath)\n        .getChanges(0)\n        .filter(_._1 <= 2)\n      compareCommitActions(commits2, pruneSparkActionsByActionSet(sparkChanges2, actionSet))\n\n      val sparkChanges3 = DeltaLog.forTable(spark, tablePath)\n        .getChanges(0)\n        .filter(_._1 <= 2)\n      compareCommitActions(commits3, pruneSparkActionsByActionSet(sparkChanges3, actionSet))\n\n      // Also verify that calling getActions on each CommitActions multiple times works\n      commits1.foreach { commit =>\n        val actions1 = commit.getActions.toSeq\n        val actions2 = commit.getActions.toSeq\n        assert(actions1.size == actions2.size)\n        // Verify the schemas are the same\n        actions1.zip(actions2).foreach { case (batch1, batch2) =>\n          assert(batch1.getSchema.equals(batch2.getSchema))\n          assert(batch1.getSize == batch2.getSize)\n        }\n      }\n    }\n  }\n\n  test(\"getCommitActions with empty commit file\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a table with an initial commit\n      appendData(\n        engine,\n        tablePath,\n        isNewTable = true,\n        testSchema,\n        data = immutable.Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      // Create an empty commit file at version 1 using commitUnsafe with no actions\n      import org.apache.spark.sql.delta.DeltaLog\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      val txn = deltaLog.startTransaction()\n      txn.commitUnsafe(tablePath, 1)\n\n      val commitRange = TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n        .withEndBoundary(CommitBoundary.atVersion(1))\n        .build(defaultEngine)\n\n      val actionSet = Set(\n        DeltaAction.ADD,\n        DeltaAction.REMOVE,\n        DeltaAction.METADATA,\n        DeltaAction.PROTOCOL,\n        DeltaAction.CDC)\n\n      val commitsIter = commitRange.getCommitActions(\n        engine,\n        getTableManagerAdapter.getSnapshotAtVersion(engine, tablePath, 0),\n        actionSet.asJava)\n\n      val commits = commitsIter.toSeq\n      assert(commits.size == 2) // Version 0 and version 1\n\n      // Version 0 should have actions (normal commit)\n      val v0Actions = commits(0).getActions.toSeq\n      assert(v0Actions.nonEmpty)\n\n      // Version 1 (empty commit) should return empty actions\n      val v1Actions = commits(1).getActions.toSeq\n      val totalRows = v1Actions.map(_.getSize).sum\n      assert(totalRows == 0, s\"Empty commit file should have no actions, but got $totalRows rows\")\n\n      // Can call getActions multiple times on empty commit\n      val v1ActionsTwice = commits(1).getActions.toSeq\n      assert(v1ActionsTwice.map(_.getSize).sum == 0)\n    }\n  }\n}\n\nabstract class TableChangesSuite extends AnyFunSuite with TestUtils with WriteUtils {\n\n  /* actionSet including all currently supported actions */\n  val FULL_ACTION_SET: Set[DeltaAction] = DeltaAction.values().toSet\n\n  def getChanges(\n      tablePath: String,\n      startVersion: Long,\n      endVersion: Long,\n      actionSet: Set[DeltaAction]): Seq[ColumnarBatch]\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // TableImpl.getChangesByVersion tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * For the given parameters, read the table changes from Kernel using\n   * TableImpl.getChangesByVersion and compare results with Spark\n   */\n  def testGetChangesVsSpark(\n      tablePath: String,\n      startVersion: Long,\n      endVersion: Long,\n      actionSet: Set[DeltaAction]): Unit = {\n\n    val sparkChanges = DeltaLog.forTable(spark, tablePath)\n      .getChanges(startVersion)\n      .filter(_._1 <= endVersion) // Spark API does not have endVersion\n\n    val kernelChanges = getChanges(tablePath, startVersion, endVersion, actionSet)\n\n    // Check schema is as expected (version + timestamp column + the actions requested)\n    kernelChanges.foreach { batch =>\n      batch.getSchema.fields().asScala sameElements\n        (Seq(\"version\", \"timestamp\") ++ actionSet.map(_.colName))\n    }\n\n    compareActions(kernelChanges, pruneSparkActionsByActionSet(sparkChanges, actionSet))\n  }\n\n  Seq(true, false).foreach { ictEnabled =>\n    test(s\"getChanges should return the same results as Spark [ictEnabled: $ictEnabled]\") {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getCanonicalPath()\n        // The code to create this table is copied from GoldenTables.scala.\n        // The part that enables ICT is a new addition.\n        val log = DeltaLog.forTable(spark, new HadoopPath(tablePath))\n\n        val schema = new StructType()\n          .add(\"part\", IntegerType)\n          .add(\"id\", IntegerType)\n        val configuration = if (ictEnabled) {\n          Map(\"delta.enableInCommitTimestamps\" -> \"true\")\n        } else {\n          Map.empty[String, String]\n        }\n        val metadata = SparkMetadata(schemaString = schema.json, configuration = configuration)\n\n        val add1 = SparkAddFile(\"fake/path/1\", Map.empty, 1, 1, dataChange = true)\n        val txn1 = log.startTransaction()\n        txn1.commitManuallyWithValidation(metadata, add1)\n\n        val addCDC2 = SparkAddCDCFile(\n          \"fake/path/2\",\n          Map(\"partition_foo\" -> \"partition_bar\"),\n          1,\n          Map(\"tag_foo\" -> \"tag_bar\"))\n        val remove2 = SparkRemoveFile(\"fake/path/1\", Some(100), dataChange = true)\n        val txn2 = log.startTransaction()\n        txn2.commitManuallyWithValidation(addCDC2, remove2)\n\n        val setTransaction3 = SparkSetTransaction(\"fakeAppId\", 3L, Some(200))\n        val txn3 = log.startTransaction()\n        val latestTableProtocol = log.snapshot.protocol\n        txn3.commitManuallyWithValidation(latestTableProtocol, setTransaction3)\n\n        // request subset of actions\n        testGetChangesVsSpark(\n          tablePath,\n          0,\n          2,\n          Set(DeltaAction.REMOVE))\n        testGetChangesVsSpark(\n          tablePath,\n          0,\n          2,\n          Set(DeltaAction.ADD))\n        testGetChangesVsSpark(\n          tablePath,\n          0,\n          2,\n          Set(DeltaAction.ADD, DeltaAction.REMOVE, DeltaAction.METADATA, DeltaAction.PROTOCOL))\n        // request full actions, various versions\n        testGetChangesVsSpark(\n          tablePath,\n          0,\n          2,\n          FULL_ACTION_SET)\n        testGetChangesVsSpark(\n          tablePath,\n          1,\n          2,\n          FULL_ACTION_SET)\n        testGetChangesVsSpark(\n          tablePath,\n          0,\n          0,\n          FULL_ACTION_SET)\n      }\n    }\n  }\n\n  Seq(Some(0), Some(1), None).foreach { ictEnablementVersion =>\n    test(\"getChanges - returns correct timestamps \" +\n      s\"[ictEnablementVersion = ${ictEnablementVersion.getOrElse(\"None\")}]\") {\n      withTempDirAndEngine { (tempDir, engine) =>\n        def generateCommits(tablePath: String, commits: Long*): Unit = {\n          commits.zipWithIndex.foreach { case (ts, i) =>\n            val tableProperties = if (ictEnablementVersion.contains(i)) {\n              Map(\"delta.enableInCommitTimestamps\" -> \"true\")\n            } else {\n              Map.empty[String, String]\n            }\n            val clock = new ManualClock(ts)\n            appendData(\n              engine,\n              tablePath,\n              isNewTable = i == 0,\n              schema = if (i == 0) testSchema else null,\n              data = immutable.Seq(Map.empty[String, Literal] -> dataBatches2),\n              clock = clock,\n              tableProperties = tableProperties)\n            // Only set the file modification time if ICT has not been enabled yet.\n            if (!ictEnablementVersion.exists(_ <= i)) {\n              val file = new File(FileNames.deltaFile(new Path(tablePath, \"_delta_log\"), i))\n              file.setLastModified(ts)\n            }\n          }\n        }\n\n        val start = 1540415658000L\n        val minuteInMilliseconds = 60000L\n        generateCommits(\n          tempDir,\n          start,\n          start + 20 * minuteInMilliseconds,\n          start + 40 * minuteInMilliseconds)\n        val versionToTimestamp: Map[Long, Long] = Map(\n          0L -> start,\n          1L -> (start + 20 * minuteInMilliseconds),\n          2L -> (start + 40 * minuteInMilliseconds))\n\n        // Check the timestamps are returned correctly\n        getChanges(tempDir, 0, 2, Set(DeltaAction.ADD))\n          .flatMap(_.getRows.toSeq)\n          .foreach { row =>\n            val version = row.getLong(0)\n            val timestamp = row.getLong(1)\n            assert(\n              timestamp == versionToTimestamp(version),\n              f\"Expected timestamp ${versionToTimestamp(version)} for version $version but\" +\n                f\"Kernel returned timestamp $timestamp\")\n          }\n\n        // Check contents as well\n        testGetChangesVsSpark(\n          tempDir,\n          0,\n          2,\n          FULL_ACTION_SET)\n      }\n    }\n  }\n\n  test(\"getChanges - empty _delta_log folder\") {\n    withTempDir { tempDir =>\n      new File(tempDir, \"delta_log\").mkdirs()\n      intercept[TableNotFoundException] {\n        getChanges(tempDir.getCanonicalPath, 0, 2, FULL_ACTION_SET)\n      }\n    }\n  }\n\n  test(\"getChanges - empty folder no _delta_log dir\") {\n    withTempDir { tempDir =>\n      intercept[TableNotFoundException] {\n        getChanges(tempDir.getCanonicalPath, 0, 2, FULL_ACTION_SET)\n      }\n    }\n  }\n\n  test(\"getChanges - non-empty folder not a delta table\") {\n    withTempDir { tempDir =>\n      spark.range(20).write.format(\"parquet\").mode(\"overwrite\").save(tempDir.getCanonicalPath)\n      intercept[TableNotFoundException] {\n        getChanges(tempDir.getCanonicalPath, 0, 2, FULL_ACTION_SET)\n      }\n    }\n  }\n\n  test(\"getChanges - directory does not exist\") {\n    intercept[TableNotFoundException] {\n      getChanges(\"/fake/table/path\", 0, 2, FULL_ACTION_SET)\n    }\n  }\n\n  test(\"getChanges - golden table deltalog-getChanges invalid queries\") {\n    withGoldenTable(\"deltalog-getChanges\") { tablePath =>\n      def getChangesByVersion(\n          startVersion: Long,\n          endVersion: Long): Seq[ColumnarBatch] = {\n        getChanges(tablePath, startVersion, endVersion, FULL_ACTION_SET)\n      }\n\n      // startVersion after latest available version\n      assert(intercept[CommitRangeNotFoundException] {\n        getChangesByVersion(3, 8)\n      }.getMessage.contains(\"no log files found in the requested version range\"))\n\n      // endVersion larger than latest available version\n      assert(intercept[KernelException] {\n        getChangesByVersion(0, 8)\n      }.getMessage.contains(\"no log file found for version 8\"))\n\n      // invalid start version\n      assert(intercept[IllegalArgumentException] {\n        getChangesByVersion(-1, 2)\n      }.getMessage.contains(\"must be >= 0\"))\n\n      // invalid end version\n      assert(intercept[IllegalArgumentException] {\n        getChangesByVersion(2, 1)\n      }.getMessage.contains(\"startVersion must be <= endVersion\"))\n    }\n  }\n\n  test(\"getChanges - with truncated log\") {\n    withTempDir { tempDir =>\n      // PREPARE TEST TABLE\n      val tablePath = tempDir.getCanonicalPath\n      // Write versions [0, 10] (inclusive) including a checkpoint\n      (0 to 10).foreach { i =>\n        spark.range(i * 10, i * 10 + 10).write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tablePath)\n      }\n      val log = org.apache.spark.sql.delta.DeltaLog.forTable(\n        spark,\n        new org.apache.hadoop.fs.Path(tablePath))\n      val deltaCommitFileProvider = org.apache.spark.sql.delta.util.DeltaCommitFileProvider(\n        log.unsafeVolatileSnapshot)\n      // Delete the log files for versions 0-9, truncating the table history to version 10\n      (0 to 9).foreach { i =>\n        val jsonFile = deltaCommitFileProvider.deltaFile(i)\n        new File(new org.apache.hadoop.fs.Path(log.logPath, jsonFile).toUri).delete()\n      }\n      // Create version 11 that overwrites the whole table\n      spark.range(50).write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tablePath)\n      // Create version 12 that appends new data\n      spark.range(10).write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tablePath)\n\n      // TEST ERRORS\n      // endVersion before earliest available version\n      assert(intercept[CommitRangeNotFoundException] {\n        getChanges(tablePath, 0, 9, FULL_ACTION_SET)\n      }.getMessage.contains(\"no log files found in the requested version range\"))\n\n      // startVersion less than the earliest available version\n      assert(intercept[KernelException] {\n        getChanges(tablePath, 5, 11, FULL_ACTION_SET)\n      }.getMessage.contains(\"no log file found for version 5\"))\n\n      // TEST VALID CASES\n      testGetChangesVsSpark(\n        tablePath,\n        10,\n        12,\n        FULL_ACTION_SET)\n      testGetChangesVsSpark(\n        tablePath,\n        11,\n        12,\n        FULL_ACTION_SET)\n    }\n  }\n\n  test(\"getChanges - table with a lot of changes\") {\n    withTempDir { tempDir =>\n      spark.sql(\n        f\"\"\"\n          |CREATE TABLE delta.`${tempDir.getCanonicalPath}` (id LONG, month LONG)\n          |USING DELTA\n          |PARTITIONED BY (month)\n          |TBLPROPERTIES (delta.enableChangeDataFeed = true)\n          |\"\"\".stripMargin)\n      spark.range(100).withColumn(\"month\", col(\"id\") % 12 + 1)\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tempDir.getCanonicalPath)\n      spark.sql( // cdc actions\n        f\"\"\"\n           |UPDATE delta.`${tempDir.getCanonicalPath}` SET month = 1 WHERE id < 10\n           |\"\"\".stripMargin)\n      spark.sql(\n        f\"\"\"\n           |DELETE FROM delta.`${tempDir.getCanonicalPath}` WHERE month = 12\n           |\"\"\".stripMargin)\n      spark.sql(\n        f\"\"\"\n           |DELETE FROM delta.`${tempDir.getCanonicalPath}` WHERE id = 52\n           |\"\"\".stripMargin)\n      spark.range(100, 150).withColumn(\"month\", col(\"id\") % 12)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tempDir.getCanonicalPath)\n      spark.sql( // change metadata\n        f\"\"\"\n           |ALTER TABLE delta.`${tempDir.getCanonicalPath}`\n           |ADD CONSTRAINT validMonth CHECK (month <= 12)\n           |\"\"\".stripMargin)\n\n      // Check all actions are correctly retrieved\n      testGetChangesVsSpark(\n        tempDir.getCanonicalPath,\n        0,\n        6,\n        FULL_ACTION_SET)\n      // Check some subset of actions\n      testGetChangesVsSpark(\n        tempDir.getCanonicalPath,\n        0,\n        6,\n        Set(DeltaAction.ADD))\n    }\n  }\n\n  test(\"getChanges - fails when protocol is not readable by Kernel\") {\n    // Existing tests suffice to check if the protocol column is present/dropped correctly\n    // We test our protocol checks for table features in TableFeaturesSuite\n    // Min reader version is too high\n    assert(intercept[UnsupportedProtocolVersionException] {\n      // Use toSeq because we need to consume the iterator to force the exception\n      getChanges(goldenTablePath(\"deltalog-invalid-protocol-version\"), 0, 0, FULL_ACTION_SET)\n    }.getMessage.contains(\"Unsupported Delta protocol reader version\"))\n    // We still get an error if we don't request the protocol file action\n    assert(intercept[UnsupportedProtocolVersionException] {\n      getChanges(goldenTablePath(\"deltalog-invalid-protocol-version\"), 0, 0, Set(DeltaAction.ADD))\n    }.getMessage.contains(\"Unsupported Delta protocol reader version\"))\n  }\n\n  withGoldenTable(\"commit-info-containing-arbitrary-operationParams-types\") { tablePath =>\n    test(\"getChanges - commit info with arbitrary operationParams types\") {\n      // Check all actions are correctly retrieved\n      testGetChangesVsSpark(\n        tablePath,\n        0,\n        2,\n        FULL_ACTION_SET)\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Helpers to compare actions returned between Kernel and Spark\n  //////////////////////////////////////////////////////////////////////////////////\n\n  // Standardize actions with case classes, keeping just a few fields to compare\n  trait StandardAction\n\n  case class StandardRemove(\n      path: String,\n      dataChange: Boolean,\n      partitionValues: Map[String, String]) extends StandardAction\n\n  case class StandardAdd(\n      path: String,\n      partitionValues: Map[String, String],\n      size: Long,\n      modificationTime: Long,\n      dataChange: Boolean) extends StandardAction\n\n  case class StandardMetadata(\n      id: String,\n      schemaString: String,\n      partitionColumns: Seq[String],\n      configuration: Map[String, String]) extends StandardAction\n\n  case class StandardProtocol(\n      minReaderVersion: Int,\n      minWriterVersion: Int,\n      readerFeatures: Set[String],\n      writerFeatures: Set[String]) extends StandardAction\n\n  case class StandardCommitInfo(\n      operation: String,\n      operationMetrics: Map[String, String]) extends StandardAction\n\n  case class StandardCdc(\n      path: String,\n      partitionValues: Map[String, String],\n      size: Long,\n      tags: Map[String, String]) extends StandardAction\n\n  case class StandardTxn(\n      appId: String,\n      version: Long,\n      lastUpdated: Option[Long]) extends StandardAction\n\n  def standardizeKernelAction(row: Row, startIdx: Int = 2): Option[StandardAction] = {\n    val actionIdx = (startIdx until row.getSchema.length()).find(!row.isNullAt(_)).getOrElse(\n      return None)\n\n    row.getSchema.at(actionIdx).getName match {\n      case DeltaAction.REMOVE.colName =>\n        val removeRow = row.getStruct(actionIdx)\n        val partitionValues: Map[String, String] = { // partitionValues is nullable for removes\n          if (removeRow.isNullAt(RemoveFile.FULL_SCHEMA.indexOf(\"partitionValues\"))) {\n            null\n          } else {\n            VectorUtils.toJavaMap[String, String](\n              removeRow.getMap(RemoveFile.FULL_SCHEMA.indexOf(\"partitionValues\"))).asScala.toMap\n          }\n        }\n        Some(StandardRemove(\n          removeRow.getString(RemoveFile.FULL_SCHEMA.indexOf(\"path\")),\n          removeRow.getBoolean(RemoveFile.FULL_SCHEMA.indexOf(\"dataChange\")),\n          partitionValues))\n\n      case DeltaAction.ADD.colName =>\n        val addRow = row.getStruct(actionIdx)\n        Some(StandardAdd(\n          addRow.getString(AddFile.FULL_SCHEMA.indexOf(\"path\")),\n          VectorUtils.toJavaMap[String, String](\n            addRow.getMap(AddFile.FULL_SCHEMA.indexOf(\"partitionValues\"))).asScala.toMap,\n          addRow.getLong(AddFile.FULL_SCHEMA.indexOf(\"size\")),\n          addRow.getLong(AddFile.FULL_SCHEMA.indexOf(\"modificationTime\")),\n          addRow.getBoolean(AddFile.FULL_SCHEMA.indexOf(\"dataChange\"))))\n\n      case DeltaAction.METADATA.colName =>\n        val metadataRow = row.getStruct(actionIdx)\n        Some(StandardMetadata(\n          metadataRow.getString(Metadata.FULL_SCHEMA.indexOf(\"id\")),\n          metadataRow.getString(Metadata.FULL_SCHEMA.indexOf(\"schemaString\")),\n          VectorUtils.toJavaList(\n            metadataRow.getArray(Metadata.FULL_SCHEMA.indexOf(\"partitionColumns\"))).asScala.toSeq,\n          VectorUtils.toJavaMap[String, String](\n            metadataRow.getMap(Metadata.FULL_SCHEMA.indexOf(\"configuration\"))).asScala.toMap))\n\n      case DeltaAction.PROTOCOL.colName =>\n        val protocolRow = row.getStruct(actionIdx)\n        val readerFeatures =\n          if (protocolRow.isNullAt(Protocol.FULL_SCHEMA.indexOf(\"readerFeatures\"))) {\n            Seq()\n          } else {\n            VectorUtils.toJavaList(\n              protocolRow.getArray(Protocol.FULL_SCHEMA.indexOf(\"readerFeatures\"))).asScala\n          }\n        val writerFeatures =\n          if (protocolRow.isNullAt(Protocol.FULL_SCHEMA.indexOf(\"writerFeatures\"))) {\n            Seq()\n          } else {\n            VectorUtils.toJavaList(\n              protocolRow.getArray(Protocol.FULL_SCHEMA.indexOf(\"writerFeatures\"))).asScala\n          }\n\n        Some(StandardProtocol(\n          protocolRow.getInt(Protocol.FULL_SCHEMA.indexOf(\"minReaderVersion\")),\n          protocolRow.getInt(Protocol.FULL_SCHEMA.indexOf(\"minWriterVersion\")),\n          readerFeatures.toSet,\n          writerFeatures.toSet))\n\n      case DeltaAction.COMMITINFO.colName =>\n        val commitInfoRow = row.getStruct(actionIdx)\n        val operationIdx = CommitInfo.FULL_SCHEMA.indexOf(\"operation\")\n        val operationMetricsIdx = CommitInfo.FULL_SCHEMA.indexOf(\"operationMetrics\")\n\n        Some(StandardCommitInfo(\n          if (commitInfoRow.isNullAt(operationIdx)) null else commitInfoRow.getString(operationIdx),\n          if (commitInfoRow.isNullAt(operationMetricsIdx)) {\n            Map.empty\n          } else {\n            VectorUtils.toJavaMap[String, String](\n              commitInfoRow.getMap(operationMetricsIdx)).asScala.toMap\n          }))\n\n      case DeltaAction.CDC.colName =>\n        val cdcRow = row.getStruct(actionIdx)\n        val tags: Map[String, String] = {\n          if (cdcRow.isNullAt(AddCDCFile.FULL_SCHEMA.indexOf(\"tags\"))) {\n            null\n          } else {\n            VectorUtils.toJavaMap[String, String](\n              cdcRow.getMap(AddCDCFile.FULL_SCHEMA.indexOf(\"tags\"))).asScala.toMap\n          }\n        }\n        Some(StandardCdc(\n          cdcRow.getString(AddCDCFile.FULL_SCHEMA.indexOf(\"path\")),\n          VectorUtils.toJavaMap[String, String](\n            cdcRow.getMap(AddCDCFile.FULL_SCHEMA.indexOf(\"partitionValues\"))).asScala.toMap,\n          cdcRow.getLong(AddCDCFile.FULL_SCHEMA.indexOf(\"size\")),\n          tags))\n      case DeltaAction.TXN.colName =>\n        val txnRow = row.getStruct(actionIdx)\n        val lastUpdated = if (txnRow.isNullAt(SetTransaction.FULL_SCHEMA.indexOf(\"lastUpdated\"))) {\n          None\n        } else {\n          Some(txnRow.getLong(SetTransaction.FULL_SCHEMA.indexOf(\"lastUpdated\")))\n        }\n        Some(StandardTxn(\n          txnRow.getString(SetTransaction.FULL_SCHEMA.indexOf(\"appId\")),\n          txnRow.getLong(SetTransaction.FULL_SCHEMA.indexOf(\"version\")),\n          lastUpdated))\n      case _ =>\n        throw new RuntimeException(\"Encountered an action that hasn't been added as an option yet\")\n    }\n  }\n\n  def standardizeSparkAction(action: SparkAction): Option[StandardAction] = action match {\n    case remove: SparkRemoveFile =>\n      Some(StandardRemove(remove.path, remove.dataChange, remove.partitionValues))\n    case add: SparkAddFile =>\n      Some(StandardAdd(\n        add.path,\n        add.partitionValues,\n        add.size,\n        add.modificationTime,\n        add.dataChange))\n    case metadata: SparkMetadata =>\n      Some(StandardMetadata(\n        metadata.id,\n        metadata.schemaString,\n        metadata.partitionColumns,\n        metadata.configuration))\n    case protocol: SparkProtocol =>\n      Some(StandardProtocol(\n        protocol.minReaderVersion,\n        protocol.minWriterVersion,\n        protocol.readerFeatures.getOrElse(Set.empty),\n        protocol.writerFeatures.getOrElse(Set.empty)))\n    case commitInfo: SparkCommitInfo =>\n      Some(StandardCommitInfo(\n        commitInfo.operation,\n        commitInfo.operationMetrics.getOrElse(Map.empty)))\n    case cdc: SparkAddCDCFile =>\n      Some(StandardCdc(cdc.path, cdc.partitionValues, cdc.size, cdc.tags))\n    case txn: SparkSetTransaction =>\n      Some(StandardTxn(txn.appId, txn.version, txn.lastUpdated))\n    case _ => None\n  }\n\n  /**\n   * When we query the Spark actions using DeltaLog::getChanges ALL action types are returned. Since\n   * Kernel only returns actions in the provided `actionSet` this FX prunes the Spark actions to\n   * match `actionSet`.\n   */\n  def pruneSparkActionsByActionSet(\n      sparkActions: Iterator[(Long, Seq[SparkAction])],\n      actionSet: Set[DeltaAction]): Iterator[(Long, Seq[SparkAction])] = {\n    sparkActions.map { case (version, actions) =>\n      (\n        version,\n        actions.filter {\n          case _: SparkRemoveFile => actionSet.contains(DeltaAction.REMOVE)\n          case _: SparkAddFile => actionSet.contains(DeltaAction.ADD)\n          case _: SparkMetadata => actionSet.contains(DeltaAction.METADATA)\n          case _: SparkProtocol => actionSet.contains(DeltaAction.PROTOCOL)\n          case _: SparkCommitInfo => actionSet.contains(DeltaAction.COMMITINFO)\n          case _: SparkAddCDCFile => actionSet.contains(DeltaAction.CDC)\n          case _: SparkSetTransaction => actionSet.contains(DeltaAction.TXN)\n          case _ => false\n        })\n    }\n  }\n\n  /**\n   * Compare actions from CommitActions objects directly with Spark actions.\n   * This automatically extracts version from each commit and standardizes the batches.\n   */\n  def compareCommitActions(\n      commits: Seq[io.delta.kernel.CommitActions],\n      sparkActions: Iterator[(Long, Seq[SparkAction])]): Unit = {\n    // Directly convert CommitActions to StandardActions without adding columns\n    val standardKernelActions: Seq[(Long, StandardAction)] = commits.flatMap { commit =>\n      val version = commit.getVersion\n      commit.getActions.toSeq.flatMap { batch =>\n        batch.getRows.toSeq\n          .map(row => (version, standardizeKernelAction(row, startIdx = 0)))\n          .filter(_._2.nonEmpty)\n          .map(t => (t._1, t._2.get))\n      }\n    }\n\n    val standardSparkActions: Seq[(Long, StandardAction)] =\n      sparkActions.flatMap { case (version, actions) =>\n        actions.map(standardizeSparkAction(_)).flatten.map((version, _))\n      }.toSeq\n\n    assert(\n      standardKernelActions.sameElements(standardSparkActions),\n      s\"Kernel actions did not match Spark actions.\\n\" +\n        s\"Kernel actions: ${standardKernelActions.take(5)}\\n\" +\n        s\"Spark actions: ${standardSparkActions.take(5)}\")\n  }\n\n  def compareActions(\n      kernelActions: Seq[ColumnarBatch],\n      sparkActions: Iterator[(Long, Seq[SparkAction])]): Unit = {\n\n    val standardKernelActions: Seq[(Long, StandardAction)] = {\n      kernelActions.flatMap(_.getRows.toSeq)\n        .map(row => (row.getLong(0), standardizeKernelAction(row)))\n        .filter(_._2.nonEmpty)\n        .map(t => (t._1, t._2.get))\n    }\n\n    val standardSparkActions: Seq[(Long, StandardAction)] =\n      sparkActions.flatMap { case (version, actions) =>\n        actions.map(standardizeSparkAction(_)).flatten.map((version, _))\n      }.toSeq\n\n    assert(\n      standardKernelActions sameElements standardSparkActions,\n      f\"Kernel actions did not match Spark actions.\\n\" +\n        f\"Kernel actions: $standardKernelActions\\n\" +\n        f\"Spark actions: $standardSparkActions\")\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/TablePropertiesSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{Table, TableManager}\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtils, WriteUtilsWithV2Builders}\nimport io.delta.kernel.exceptions.{KernelException, UnknownConfigurationException}\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TablePropertiesTransactionBuilderV1Suite extends TablePropertiesSuiteBase with WriteUtils {}\n\nclass TablePropertiesTransactionBuilderV2Suite extends TablePropertiesSuiteBase\n    with WriteUtilsWithV2Builders {\n  test(\"create table (V2 only) - withTableProperties can be called multiple times\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n\n      TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withTableProperties(Map(\"key1\" -> \"value1\").asJava)\n        .withTableProperties(Map(\"key2\" -> \"value2\").asJava)\n        .build(defaultEngine)\n        .commit(defaultEngine, emptyIterable())\n\n      assertHasProp(tablePath, Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\"))\n    }\n  }\n\n  test(\"create table (V2 only) - withTableProperties throws on same key with different value\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n\n      val createBuilder = TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withTableProperties(Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\").asJava)\n\n      val ex = intercept[IllegalArgumentException] {\n        createBuilder.withTableProperties(Map(\"key2\" -> \"different_value\").asJava)\n      }\n      assert(ex.getMessage.contains(\"Table property 'key2' has already been set\"))\n    }\n  }\n\n  test(\"create table (V2 only) - withTableProperties allows setting same key with same value\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n\n      TableManager\n        .buildCreateTableTransaction(tablePath, testSchema, \"engineInfo\")\n        .withTableProperties(Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\").asJava)\n        .withTableProperties(Map(\"key2\" -> \"value2\").asJava) // Same value, should not throw\n        .build(defaultEngine)\n        .commit(defaultEngine, emptyIterable())\n\n      assertHasProp(tablePath, Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\"))\n    }\n  }\n}\n\n/**\n * Suite to set or get table properties.\n */\ntrait TablePropertiesSuiteBase extends AnyFunSuite with AbstractWriteUtils {\n  test(\"create/update/replace table - allow arbitrary properties\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n\n      // create table with arbitrary properties and check if they are set\n      createUpdateTableWithProps(\n        tablePath,\n        createTable = true,\n        propsAdded = Map(\"my key\" -> \"10\", \"my key2\" -> \"20\"))\n      assertHasProp(tablePath, expProps = Map(\"my key\" -> \"10\", \"my key2\" -> \"20\"))\n\n      // update table by modifying the arbitrary properties and check if they are updated\n      createUpdateTableWithProps(tablePath, propsAdded = Map(\"my key\" -> \"30\"))\n      assertHasProp(tablePath, expProps = Map(\"my key\" -> \"30\", \"my key2\" -> \"20\"))\n\n      // update table without any new properties and check if the existing properties are retained\n      createUpdateTableWithProps(tablePath)\n      assertHasProp(tablePath, expProps = Map(\"my key\" -> \"30\", \"my key2\" -> \"20\"))\n\n      // update table by adding new arbitrary properties and check if they are set\n      createUpdateTableWithProps(tablePath, propsAdded = Map(\"new key3\" -> \"str\"))\n      assertHasProp(\n        tablePath,\n        expProps = Map(\"my key\" -> \"30\", \"my key2\" -> \"20\", \"new key3\" -> \"str\"))\n\n      // replace table and set new arbitrary properties and check if they are set (and old ones are\n      // removed)\n      getReplaceTxn(\n        defaultEngine,\n        tablePath,\n        testSchema,\n        tableProperties = Map(\"my key\" -> \"40\", \"my replace key\" -> \"0\"))\n        .commit(defaultEngine, emptyIterable())\n      assertHasProp(\n        tablePath,\n        expProps = Map(\"my key\" -> \"40\", \"my replace key\" -> \"0\"))\n      assertPropsDNE(tablePath, Set(\"my key2\", \"my key3\"))\n    }\n  }\n\n  test(\"create/update/replace table - disallow unknown delta.* properties to Kernel\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      val ex1 = intercept[UnknownConfigurationException] {\n        createUpdateTableWithProps(tablePath, createTable = true, Map(\"delta.unknown\" -> \"str\"))\n      }\n      assert(ex1.getMessage.contains(\"Unknown configuration was specified: delta.unknown\"))\n\n      // Try updating in an existing table\n      createUpdateTableWithProps(tablePath, createTable = true)\n      val ex2 = intercept[UnknownConfigurationException] {\n        createUpdateTableWithProps(tablePath, propsAdded = Map(\"Delta.unknown\" -> \"str\"))\n      }\n      assert(ex2.getMessage.contains(\"Unknown configuration was specified: Delta.unknown\"))\n\n      // Try replacing an existing table\n      val ex3 = intercept[UnknownConfigurationException] {\n        getReplaceTxn(\n          defaultEngine,\n          tablePath,\n          testSchema,\n          tableProperties = Map(\"Delta.unknown\" -> \"str\"))\n      }\n      assert(ex3.getMessage.contains(\"Unknown configuration was specified: Delta.unknown\"))\n    }\n  }\n\n  test(\"create/update/replace table - delta configs are stored with same case as \" +\n    \"defined in TableConfig\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      createUpdateTableWithProps(\n        tablePath,\n        createTable = true,\n        Map(\"delta.CHECKPOINTINTERVAL\" -> \"20\"))\n      assertHasProp(tablePath, expProps = Map(\"delta.checkpointInterval\" -> \"20\"))\n\n      // Try updating in an existing table\n      createUpdateTableWithProps(\n        tablePath,\n        propsAdded = Map(\"DELTA.CHECKPOINTINTERVAL\" -> \"30\"))\n      assertHasProp(tablePath, expProps = Map(\"delta.checkpointInterval\" -> \"30\"))\n\n      // Try replacing an existing table\n      getReplaceTxn(\n        defaultEngine,\n        tablePath,\n        testSchema,\n        tableProperties = Map(\"DELTA.CHECKPOINTINTERVAL\" -> \"30\"))\n        .commit(defaultEngine, emptyIterable())\n      assertHasProp(tablePath, expProps = Map(\"delta.checkpointInterval\" -> \"30\"))\n    }\n  }\n\n  test(\"Case is preserved for user properties and is case sensitive\") {\n    // This aligns with Spark's behavior\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      createUpdateTableWithProps(\n        tablePath,\n        createTable = true,\n        Map(\"user.facing.PROP\" -> \"20\"))\n      assertHasProp(tablePath, expProps = Map(\"user.facing.PROP\" -> \"20\"))\n\n      // Try updating in an existing table\n      createUpdateTableWithProps(\n        tablePath,\n        propsAdded = Map(\"user.facing.prop\" -> \"30\"))\n      assertHasProp(\n        tablePath,\n        expProps = Map(\"user.facing.PROP\" -> \"20\", \"user.facing.prop\" -> \"30\"))\n\n      // Try replacing an existing table\n      getReplaceTxn(\n        defaultEngine,\n        tablePath,\n        testSchema,\n        tableProperties = Map(\"user.facing.prop\" -> \"30\", \"user.facing.PROP\" -> \"20\"))\n        .commit(defaultEngine, emptyIterable())\n      assertHasProp(\n        tablePath,\n        expProps = Map(\"user.facing.PROP\" -> \"20\", \"user.facing.prop\" -> \"30\"))\n    }\n  }\n\n  test(\"Cannot unset delta table properties\") {\n    withTempDir { tablePath =>\n      // Create empty table with delta props\n      createUpdateTableWithProps(\n        tablePath.getAbsolutePath,\n        createTable = true,\n        propsAdded = Map(\"delta.checkpointInterval\" -> \"10\"))\n      Seq(\"delta.checkpointInterval\", \"DELTA.checkpointInterval\").foreach { key =>\n        val e = intercept[IllegalArgumentException] {\n          createUpdateTableWithProps(\n            tablePath.getAbsolutePath,\n            propsRemoved = Set(key))\n        }\n        assert(\n          e.getMessage.contains(\"Unsetting 'delta.' table properties is currently unsupported\"))\n      }\n    }\n  }\n\n  test(\"Cannot set and unset the same table property in same txn - new property\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      createEmptyTable(tablePath = tablePath, schema = testSchema)\n\n      val e = intercept[KernelException] {\n        createUpdateTableWithProps(\n          tablePath,\n          propsAdded = Map(\"foo.key\" -> \"value\"),\n          propsRemoved = Set(\"foo.key\"))\n      }\n      assert(e.getMessage.contains(\n        \"Cannot set and unset the same table property in the same transaction. \"\n          + \"Properties set and unset: [foo.key]\"))\n    }\n  }\n\n  test(\"Cannot set and unset the same table property in same txn - existing property\") {\n    // i.e. we don't only check against the new properties\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      // Create initial table with the property\n      createEmptyTable(\n        tablePath = tablePath,\n        schema = testSchema,\n        tableProperties = Map(\"foo.key\" -> \"value\"))\n\n      // Try to set and unset the existing property\n      val e = intercept[KernelException] {\n        createUpdateTableWithProps(\n          tablePath,\n          propsAdded = Map(\"foo.key\" -> \"value\"),\n          propsRemoved = Set(\"foo.key\"))\n      }\n      assert(e.getMessage.contains(\n        \"Cannot set and unset the same table property in the same transaction. \"\n          + \"Properties set and unset: [foo.key]\"))\n    }\n  }\n\n  test(\"Unset valid cases - properties are removed from the table\") {\n    withTempDir { tempFile =>\n      val tablePath = tempFile.getAbsolutePath\n      // Create initial table with properties\n      // This test also validates the operation is case-sensitive\n      createEmptyTable(\n        tablePath = tablePath,\n        schema = testSchema,\n        tableProperties = Map(\"foo.key\" -> \"value\", \"FOO.KEY\" -> \"VALUE\"))\n      assertHasProp(tablePath, Map(\"foo.key\" -> \"value\", \"FOO.KEY\" -> \"VALUE\"))\n\n      // Remove 1 of the properties set\n      createUpdateTableWithProps(\n        tablePath,\n        propsRemoved = Set(\"foo.key\"))\n      assertPropsDNE(tablePath, Set(\"foo.key\"))\n      // Check that the other property is not touched\n      assertHasProp(tablePath, Map(\"FOO.KEY\" -> \"VALUE\"))\n\n      // Can unset a property that DNE\n      createUpdateTableWithProps(\n        tablePath,\n        propsRemoved = Set(\"not.a.key\"))\n      assertPropsDNE(tablePath, Set(\"not.a.key\"))\n      // Check that the other property is not touched\n      assertHasProp(tablePath, Map(\"FOO.KEY\" -> \"VALUE\"))\n\n      // Can be simultaneous with setTblProps as long as no overlap\n      createUpdateTableWithProps(\n        tablePath,\n        propsAdded = Map(\"foo.key\" -> \"value-new\"),\n        propsRemoved = Set(\"FOO.KEY\"))\n      assertPropsDNE(tablePath, Set(\"FOO.KEY\"))\n      // Check that the other property is added successfully\n      assertHasProp(tablePath, Map(\"foo.key\" -> \"value-new\"))\n    }\n  }\n\n  def createUpdateTableWithProps(\n      tablePath: String,\n      createTable: Boolean = false,\n      propsAdded: Map[String, String] = null,\n      propsRemoved: Set[String] = null): Unit = {\n    val txn = if (createTable) {\n      getCreateTxn(\n        defaultEngine,\n        tablePath,\n        testSchema,\n        tableProperties = propsAdded)\n    } else {\n      getUpdateTxn(\n        defaultEngine,\n        tablePath,\n        tableProperties = propsAdded,\n        tablePropertiesRemoved = propsRemoved)\n    }\n    txn.commit(defaultEngine, emptyIterable())\n  }\n\n  def assertHasProp(tablePath: String, expProps: Map[String, String]): Unit = {\n    val snapshot = Table.forPath(defaultEngine, tablePath)\n      .getLatestSnapshot(defaultEngine)\n    expProps.foreach { case (key, value) =>\n      assert(snapshot.getTableProperties.get(key) === value, key)\n    }\n  }\n\n  def assertPropsDNE(tablePath: String, keys: Set[String]): Unit = {\n    val metadata = getMetadata(defaultEngine, tablePath)\n    assert(keys.forall(!metadata.getConfiguration.containsKey(_)))\n  }\n\n  val recognizedButUnimplementedProps = Seq(\n    (\"delta.dataSkippingStatsColumns\", \"col1,col2,nested.field\"))\n\n  recognizedButUnimplementedProps.foreach { case (propKey, value) =>\n    test(s\"$propKey is allowed (but not implemented) - create table\") {\n      withTempDir { tempFile =>\n        val tablePath = tempFile.getAbsolutePath\n        createUpdateTableWithProps(\n          tablePath,\n          createTable = true,\n          propsAdded = Map(propKey -> value))\n        assertHasProp(tablePath, Map(propKey -> value))\n      }\n    }\n\n    test(s\"$propKey is allowed (but not implemented) - update table\") {\n      withTempDir { tempFile =>\n        val tablePath = tempFile.getAbsolutePath\n        createUpdateTableWithProps(tablePath, createTable = true)\n\n        val updatedValue = s\"${value}_updated\"\n        createUpdateTableWithProps(\n          tablePath,\n          propsAdded = Map(propKey -> updatedValue))\n        assertHasProp(tablePath, Map(propKey -> updatedValue))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/TimestampStatsAndDataSkippingSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults\n\nimport java.io.File\nimport java.time.{LocalDateTime, ZoneOffset}\n\nimport scala.collection.immutable.Seq\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.defaults.internal.data.vector.DefaultGenericVector\nimport io.delta.kernel.defaults.internal.parquet.ParquetSuiteBase\nimport io.delta.kernel.defaults.utils.WriteUtils\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.util.JsonUtils\nimport io.delta.kernel.types.{StructType, TimestampNTZType, TimestampType}\n\nimport org.apache.spark.sql.delta.DeltaLog\n\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Tests timestamp statistics serialization and data skipping behavior\n * for TIMESTAMP and TIMESTAMP_NTZ types.\n */\nclass TimestampStatsAndDataSkippingSuite extends AnyFunSuite with WriteUtils\n    with DataSkippingDeltaTestsUtils\n    with ParquetSuiteBase {\n\n  test(\"verify on-disk TIMESTAMP stats format is equal when writing through spark and kernel\") {\n    withTempDirAndEngine { (dir, engine) =>\n      // Test with TIMESTAMP and TIMESTAMP_NTZ to verify serialization format\n      val schema = new StructType()\n        .add(\"timestampCol\", TimestampType.TIMESTAMP)\n        .add(\"timestampNtzCol\", TimestampNTZType.TIMESTAMP_NTZ)\n\n      // Create different batches with different timestamp ranges to test multiple boundaries\n      val timestampRanges = Seq(\n        (\"2019-06-09 01:02:04.123456\", \"2019-09-09 01:02:04.123999\"),\n        (\"2019-06-09 01:02:04.123456\", \"2019-09-09 01:02:05.123999\"),\n        // Microsecond boundary\n        (\"2019-09-09 01:02:03.456789\", \"2019-09-09 01:02:03.456999\"),\n        // End-of-millisecond boundary\n        (\"2019-09-09 01:02:03.999000\", \"2019-09-09 01:02:03.999999\"),\n        (\"2019-09-09 01:02:04.123456\", \"2019-09-09 01:02:04.123999\"))\n\n      // Create \"kernel\" and \"spark-copy\" directories\n      val kernelPath = new File(dir, \"kernel\").getAbsolutePath\n      val sparkTablePath = new File(dir, \"spark-copy\").getAbsolutePath\n\n      // Write through Kernel\n      timestampRanges.zipWithIndex.foreach { case ((minTs, maxTs), fileIndex) =>\n        val batch = createTimestampBatch(schema, minTs, maxTs, rowsPerFile = 10)\n        appendData(\n          engine,\n          kernelPath,\n          isNewTable = fileIndex == 0,\n          schema = if (fileIndex == 0) schema else null,\n          partCols = Seq.empty,\n          data = Seq(Map.empty[String, Literal] -> Seq(batch.toFiltered(Option.empty))))\n      }\n\n      val kernelDf = spark.read.format(\"delta\").load(kernelPath)\n      val kernelFiles = kernelDf.inputFiles\n\n      log.info(s\"Found ${kernelFiles.length} files from Kernel\")\n\n      withSparkTimeZone(\"UTC\") {\n        kernelFiles.zipWithIndex.foreach { case (filePath, fileIndex) =>\n          val singleFileDf = spark.read.parquet(filePath)\n          if (fileIndex == 0) {\n            singleFileDf.write.format(\"delta\").mode(\"overwrite\").save(sparkTablePath)\n          } else {\n            singleFileDf.write.format(\"delta\").mode(\"append\").save(sparkTablePath)\n          }\n        }\n      }\n\n      val mapper = JsonUtils.mapper()\n      val kernelStats = collectStatsFromAddFiles(engine, kernelPath).map(mapper.readTree)\n      val sparkStats = collectStatsFromAddFiles(engine, sparkTablePath).map(mapper.readTree)\n\n      require(\n        kernelStats.nonEmpty && sparkStats.nonEmpty,\n        \"stats collected from AddFiles should be non-empty\")\n      assert(\n        kernelStats.toSet == sparkStats.toSet,\n        s\"\\nKernel stats:\\n${kernelStats.mkString(\"\\n\")}\\n\" +\n          s\"Spark  stats:\\n${sparkStats.mkString(\"\\n\")}\")\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Timestamp Data Skipping Tests that mirror those of Delta-Spark\n  //////////////////////////////////////////////////////////////////////////////////\n  for (timestampType <- Seq(TimestampType.TIMESTAMP, TimestampNTZType.TIMESTAMP_NTZ)) {\n    test(s\"validate basic delta-spark data-skipping on ${timestampType.getClass.getSimpleName}\") {\n      withTempDirAndEngine { (kernelPath, engine) =>\n        val schema = new StructType().add(\"ts\", timestampType)\n\n        // Generate multiple files with different timestamp ranges.\n        val timestampRanges = Seq(\n          (\"2019-01-01 12:00:00.123456\", \"2019-01-01 18:00:00.999999\"),\n          (\"2019-09-09 01:02:03.456789\", \"2019-09-09 01:02:03.456789\"),\n          (\"2019-12-31 20:00:00.100000\", \"2019-12-31 23:59:59.999999\"),\n          (\"2020-06-15 10:30:45.555555\", \"2020-06-15 15:45:30.888888\"),\n          (\"2021-03-20 08:15:22.777777\", \"2021-03-20 16:42:18.333333\"))\n\n        timestampRanges.zipWithIndex.foreach { case ((minTs, maxTs), fileIndex) =>\n          val batch = createTimestampBatch(schema, minTs, maxTs, rowsPerFile = 10)\n\n          appendData(\n            engine,\n            kernelPath,\n            isNewTable = fileIndex == 0,\n            schema = if (fileIndex == 0) schema else null,\n            partCols = Seq.empty,\n            data = Seq(Map.empty[String, Literal] -> Seq(batch.toFiltered(Option.empty))))\n        }\n\n        // Query with all predicates in UTC for this test.\n        // We mainly want to validate that the scan results are correct.\n        withSparkTimeZone(\"UTC\") {\n          val deltaLogPath = DeltaLog.forTable(spark, new Path(kernelPath))\n\n          val exactHits = Seq(\n            // Files 1,2,3,4\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789\")}\", 4),\n            // Files 1,2,3,4 (millisecond expansion)\n            (s\"ts <= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789\")}\", 2),\n            // Files 1,2,3,4 (tests millisecond expansion in MAX due to truncation)\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456790\")}\", 4),\n            // File 1 only\n            (s\"ts = ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789\")}\", 1))\n\n          exactHits.foreach { case (predicate, expectedFiles) =>\n            val filesHit = filesReadCount(spark, deltaLogPath, predicate)\n            assert(\n              filesHit == expectedFiles,\n              s\"Expected exactly $expectedFiles files for: $predicate, but got $filesHit files\")\n          }\n\n          // Test range queries (should hit multiple files)\n          val rangeHits = Seq(\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2019-01-01 00:00:00\")}\", 5),\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2020-01-01 00:00:00\")}\", 3),\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2019-06-01 00:00:00\")}\", 4),\n            (s\"ts <= ${createTimestampLiteral(timestampType, \"2019-06-01 00:00:00\")}\", 1),\n            (\n              s\"ts BETWEEN ${createTimestampLiteral(timestampType, \"2019-09-01 00:00:00\")} \" +\n                s\"AND ${createTimestampLiteral(timestampType, \"2019-09-30 23:59:59\")}\",\n              1),\n            (\n              s\"ts BETWEEN ${createTimestampLiteral(timestampType, \"2019-01-01 00:00:00\")} \" +\n                s\"AND ${createTimestampLiteral(timestampType, \"2019-12-31 23:59:59\")}\",\n              3))\n\n          rangeHits.foreach { case (predicate, expectedFiles) =>\n            val filesHit = filesReadCount(spark, deltaLogPath, predicate)\n            assert(\n              filesHit == expectedFiles,\n              s\"Expected exactly $expectedFiles files for: $predicate, but got $filesHit files\")\n          }\n\n          // Test precise misses (outside the millisecond range due to truncation )\n          val preciseCases = Seq(\n            // Files 1, 2, 3. Next millisecond\n            // ${createTimestampLiteral(timestampType, \"2019-01-01 00:00:00\")}\n            (s\"ts > ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.457000\")}\", 3),\n            (s\"ts < ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456000\")}\", 1),\n            // Year boundary\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2020-01-01 00:00:00.000001\")}\", 2),\n            // True misses (0 files)\n            (s\"ts >= ${createTimestampLiteral(timestampType, \"2022-01-01 00:00:00\")}\", 0),\n            (s\"ts <= ${createTimestampLiteral(timestampType, \"2018-01-01 00:00:00\")}\", 0))\n\n          preciseCases.foreach { case (predicate, expectedFiles) =>\n            val filesHit = filesReadCount(spark, deltaLogPath, predicate)\n            assert(\n              filesHit == expectedFiles,\n              s\"Expected exactly $expectedFiles files for: $predicate, but got $filesHit files\")\n          }\n\n          // Test that we have the expected total number of files\n          val totalFiles = filesReadCount(spark, deltaLogPath, \"TRUE\")\n          assert(totalFiles == 5, s\"Expected 5 total files, but got $totalFiles\")\n        }\n      }\n    }\n  }\n\n  // Test timezone boundary behavior with a single timestamp across multiple timezones\n  test(s\"kernel data skipping timezone boundary behavior for TIMESTAMP type\") {\n    withTempDirAndEngine { (kernelPath, engine) =>\n      val timestampType = TimestampType.TIMESTAMP\n      val schema = new StructType().add(\"ts\", timestampType)\n\n      val testTimestamp = \"2019-09-09 01:02:03.456789\"\n      val batch = createTimestampBatch(schema, testTimestamp, testTimestamp, rowsPerFile = 1)\n\n      appendData(\n        engine,\n        kernelPath,\n        isNewTable = true,\n        schema,\n        partCols = Seq.empty,\n        data = Seq(Map.empty[String, Literal] -> Seq(batch.toFiltered(Option.empty))))\n\n      val sparkLog = DeltaLog.forTable(spark, new Path(kernelPath))\n\n      // Test UTC timezone-aware queries\n      val utcHits = Seq(\n        s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789+00:00\")}\",\n        s\"ts <= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789+00:00\")}\",\n        s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789 UTC\")}\",\n        s\"TS >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.456789+00:00\")}\")\n\n      val utcMisses = Seq(\n        s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.457001+00:00\")}\",\n        s\"ts <= ${createTimestampLiteral(timestampType, \"2019-09-04 01:02:03.455999+00:00\")}\",\n        s\"TS >= ${createTimestampLiteral(timestampType, \"2019-09-09 01:02:03.457001 UTC\")}\")\n\n      // Test PST timezone-aware queries\n      val pstHits = Seq(\n        s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-08 17:02:03.456789-08:00\")}\",\n        s\"ts <= ${createTimestampLiteral(timestampType, \"2019-09-08 17:02:03.456789-08:00\")}\",\n        s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-08 17:02:03.456789 PST\")}\")\n\n      val pstMisses = Seq(\n        s\"ts >= ${createTimestampLiteral(timestampType, \"2019-09-08 17:02:03.457001-08:00\")}\",\n        s\"ts <= ${createTimestampLiteral(timestampType, \"2019-09-08 17:02:03.455999-08:00\")}\")\n\n      utcHits.foreach { predicate =>\n        val filesHit = filesReadCount(spark, sparkLog, predicate)\n        assert(filesHit == 1, s\"Expected UTC hit but got miss for $predicate\")\n      }\n\n      utcMisses.foreach { predicate =>\n        val filesHit = filesReadCount(spark, sparkLog, predicate)\n        assert(filesHit == 0, s\"Expected UTC miss but got hit for $predicate\")\n      }\n\n      pstHits.foreach { predicate =>\n        val filesHit = filesReadCount(spark, sparkLog, predicate)\n        assert(filesHit == 1, s\"Expected PST hit but got miss for $predicate\")\n      }\n\n      pstMisses.foreach { predicate =>\n        val filesHit = filesReadCount(spark, sparkLog, predicate)\n        assert(filesHit == 0, s\"Expected PST miss but got hit for $predicate\")\n      }\n    }\n  }\n\n  private def createTimestampBatch(\n      schema: StructType,\n      minTimestampStr: String,\n      maxTimestampStr: String,\n      rowsPerFile: Int): ColumnarBatch = {\n    val minMicros = parseTimestampToMicros(minTimestampStr)\n    val maxMicros = parseTimestampToMicros(maxTimestampStr)\n\n    val timestampValues = (0 until rowsPerFile).map { rowIndex =>\n      if (rowIndex != 0 && rowIndex % 4 == 0) {\n        null\n      } else {\n        val fraction = if (rowsPerFile == 1) 0.0 else rowIndex.toDouble / (rowsPerFile - 1)\n        val interpolatedMicros = minMicros + ((maxMicros - minMicros) * fraction).toLong\n        interpolatedMicros\n      }\n    }.toArray.asInstanceOf[Array[AnyRef]]\n\n    val vectors = schema.fields().asScala.toSeq.map { field =>\n      DefaultGenericVector.fromArray(field.getDataType, timestampValues.toSeq.toArray)\n    }.toArray.asInstanceOf[Array[ColumnVector]]\n\n    new DefaultColumnarBatch(rowsPerFile, schema, vectors)\n  }\n\n  /**\n   * Parse timestamp string to microseconds since epoch\n   */\n  private def parseTimestampToMicros(timestampStr: String): Long = {\n    // Parse \"2019-09-09 01:02:03.456789\" format\n    val localDateTime = LocalDateTime.parse(timestampStr.replace(\" \", \"T\"))\n    val instant = localDateTime.toInstant(ZoneOffset.UTC)\n    instant.getEpochSecond * 1000000L + instant.getNano / 1000L\n  }\n\n  // Create type-appropriate literal function\n  def createTimestampLiteral(\n      timestampType: io.delta.kernel.types.DataType,\n      timestamp: String): String = {\n    timestampType match {\n      case _: TimestampType => s\"TIMESTAMP'$timestamp'\"\n      case _: TimestampNTZType => s\"TIMESTAMP_NTZ'$timestamp'\"\n      case _ => throw new IllegalArgumentException(s\"Unsupported type: $timestampType\")\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/TransactionCommitLoopSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults\n\nimport java.nio.file.FileAlreadyExistsException\n\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel.Table\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.engine.{DefaultEngine, DefaultJsonHandler}\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtilsWithV1Builders, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine.JsonHandler\nimport io.delta.kernel.exceptions.{CommitStateUnknownException, MaxCommitRetryLimitReachedException}\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TransactionCommitLoopTransactionBuilderV1Suite extends AbstractTransactionCommitLoopSuite\n    with WriteUtilsWithV1Builders {}\n\nclass TransactionCommitLoopTransactionBuilderV2Suite extends AbstractTransactionCommitLoopSuite\n    with WriteUtilsWithV2Builders {}\n\ntrait AbstractTransactionCommitLoopSuite extends AnyFunSuite { self: AbstractWriteUtils =>\n\n  private val fileIO = new HadoopFileIO(new Configuration())\n\n  test(\"Txn attempts to commit *next* version on CFE(isRetryable=true, isConflict=true)\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val initialTxn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(initialTxn, engine, emptyIterable()) // 000.json\n\n      val kernelTxn = getUpdateTxn(engine, tablePath, maxRetries = 5)\n\n      // Create 001.json. This will make the engine throw a FileAlreadyExistsException when trying\n      // to write 001.json. The default committer will turn this into a\n      // CFE(isRetryable=true, isConflict=true).\n      appendData(engine, tablePath, data = Seq(Map.empty[String, Literal] -> dataBatches1))\n\n      val result = commitTransaction(kernelTxn, engine, emptyIterable())\n\n      assert(result.getVersion == 2)\n      assert(result.getTransactionReport.getTransactionMetrics.getNumCommitAttempts == 2)\n    }\n  }\n\n  test(\"Txn attempts to commit *same* version on CFE(isRetryable=true, isConflict=false)\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val initialTxn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(initialTxn, engine, emptyIterable()) // 000.json\n\n      var attemptCount = 0 // Will be incremented when actual writeJson attempt occurs\n      val attemptNumberToSucceedAt = 5\n      val attemptedFilePaths = scala.collection.mutable.Set[String]()\n\n      class CustomJsonHandler extends DefaultJsonHandler(fileIO) {\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          attemptCount += 1\n          attemptedFilePaths += filePath\n          if (attemptCount < attemptNumberToSucceedAt) {\n            // The default committer will turn this into a CFE(isRetryable=true, isConflict=false)\n            throw new java.io.IOException(\"Transient network error\")\n          }\n          super.writeJsonFileAtomically(filePath, data, overwrite)\n        }\n      }\n\n      class CustomEngine extends DefaultEngine(fileIO) {\n        val jsonHandler = new CustomJsonHandler()\n        override def getJsonHandler: JsonHandler = jsonHandler\n      }\n\n      val transientErrorEngine = new CustomEngine()\n      val txn = getUpdateTxn(transientErrorEngine, tablePath)\n      val result = commitTransaction(txn, transientErrorEngine, emptyIterable())\n\n      assert(result.getVersion == 1)\n      assert(attemptCount == attemptNumberToSucceedAt)\n      assert(attemptedFilePaths.size == 1) // we should only be attempting to write 001.json\n      assert(result.getTransactionReport.getTransactionMetrics.getNumCommitAttempts ==\n        attemptNumberToSucceedAt)\n    }\n  }\n\n  test(\"Txn throws MaxCommitRetryLimitReachedException on too many retries\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val initialTxn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(initialTxn, engine, emptyIterable()) // 000.json\n\n      class CustomJsonHandler extends DefaultJsonHandler(fileIO) {\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          // The default committer will turn this into a CFE(isRetryable=true, isConflict=false)\n          throw new java.io.IOException(\"Transient network error\")\n        }\n      }\n\n      class AlwaysFailingEngine extends DefaultEngine(fileIO) {\n        val jsonHandler = new CustomJsonHandler()\n        override def getJsonHandler: JsonHandler = jsonHandler\n      }\n\n      val alwaysFailingEngine = new AlwaysFailingEngine()\n      val txn = getUpdateTxn(alwaysFailingEngine, tablePath, maxRetries = 10)\n\n      val exMsg = intercept[MaxCommitRetryLimitReachedException] {\n        commitTransaction(txn, alwaysFailingEngine, emptyIterable())\n      }.getMessage\n\n      assert(exMsg.contains(\"Commit attempt for version 1 failed with a retryable exception but \" +\n        \"will not be retried because the maximum number of retries (10) has been reached.\"))\n    }\n  }\n\n  test(\"Txn throws CommitStateUnknownException if it sees CFE(true,false) then CFE(true,true)\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val initialTxn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(initialTxn, engine, emptyIterable()) // 000.json\n\n      // This tests the case of:\n      // - first commit attempt: We succeed at writing 001.json, BUT a transient network error\n      //   occurs, so Kernel txn sees a failure.\n      // - second commit attempt: We try again to write 001.json, but we see that it already exists!\n      //   For now, we just throw, but in the future we could try detecting if that 001.json was\n      //   written by us on the previous attempt, or written by another writer.\n\n      class CustomJsonHandler extends DefaultJsonHandler(fileIO) {\n        var attemptCount = 0 // Will be incremented when actual writeJson attempt occurs\n\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          attemptCount += 1\n\n          if (attemptCount == 1) {\n            // The default committer will turn this into a CFE(isRetryable=true, isConflict=false)\n            throw new java.io.IOException(\"Transient network error\")\n          } else {\n            // The default committer will turn this into a CFE(isRetryable=true, isConflict=true)\n            throw new FileAlreadyExistsException(\"001.json already exists\")\n          }\n        }\n      }\n\n      class CustomEngine extends DefaultEngine(fileIO) {\n        private val jsonHandler = new CustomJsonHandler()\n        override def getJsonHandler: JsonHandler = jsonHandler\n      }\n\n      val transientErrorEngine = new CustomEngine()\n      val txn = getUpdateTxn(transientErrorEngine, tablePath)\n\n      val exMsg = intercept[CommitStateUnknownException] {\n        commitTransaction(txn, transientErrorEngine, emptyIterable())\n      }.getMessage\n      assert(exMsg.contains(\"Commit attempt 2 for version 1 failed due to a concurrent write \" +\n        \"conflict after a previous retry.\"))\n    }\n  }\n\n  // TODO: Transaction will fail on CFE(isRetryable=false, isConflict=true/false). The default\n  //       committer doesn't throw this error type. We could test this with a custom committer, but\n  //       currently our API to create transactions just use Table::getLatestSnapshot(), and is not\n  //       yet properly connected to the SnapshotBuilder.withCommitter code.\n\n  test(\"Txn will *not* retry on non-IOException RuntimeException\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val initialTxn = getCreateTxn(engine, tablePath, testSchema)\n      commitTransaction(initialTxn, engine, emptyIterable()) // 000.json\n\n      class CustomJsonHandler extends DefaultJsonHandler(fileIO) {\n        override def writeJsonFileAtomically(\n            filePath: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit = {\n          // The default committer doesn't explicitly turn this into a CFE\n          throw new RuntimeException(\"Non-retryable error\")\n        }\n      }\n\n      class CustomEngine extends DefaultEngine(fileIO) {\n        val jsonHandler = new CustomJsonHandler()\n        override def getJsonHandler: JsonHandler = jsonHandler\n      }\n\n      val alwaysFailingEngine = new CustomEngine()\n\n      val txn = getUpdateTxn(alwaysFailingEngine, tablePath)\n\n      val ex = intercept[RuntimeException] {\n        commitTransaction(txn, alwaysFailingEngine, emptyIterable())\n      }\n      assert(ex.getMessage.contains(\"Non-retryable error\"))\n    }\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/catalogManaged/CatalogManagedE2EReadSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.catalogManaged\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{SnapshotBuilder, TableManager}\nimport io.delta.kernel.CommitRangeBuilder.CommitBoundary\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.{TestRow, TestUtilsWithTableManagerAPIs, WriteUtilsWithV2Builders}\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.DeltaHistoryManager\nimport io.delta.kernel.internal.commitrange.CommitRangeImpl\nimport io.delta.kernel.internal.files.{ParsedCatalogCommitData, ParsedLogData}\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{isCatalogManagedSupported, CATALOG_MANAGED_RW_FEATURE, IN_COMMIT_TIMESTAMP_W_FEATURE, TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.utils.FileStatus\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test suite for end-to-end reads of catalog-managed Delta tables.\n *\n * The goal of this suite is to simulate how a real \"Catalog-Managed-Client\" would read a\n * catalog-managed Delta table, without introducing a full, or even partial (e.g. in-memory)\n * catalog client implementation.\n *\n * The catalog boundary is simulated by tests manually providing [[ParsedLogData]]. For example,\n * there can be X commits in the _staged_commits directory, and a given test can decide that Y\n * commits (subset of X) are in fact \"ratified\". The test can then turn those commits into\n * [[ParsedLogData]] and inject them into the [[SnapshotBuilder]]. This is,\n * in essence, doing exactly what we would expect a \"Catalog-Managed-Client\" to do.\n */\nclass CatalogManagedE2EReadSuite extends AnyFunSuite\n    with TestUtilsWithTableManagerAPIs with WriteUtilsWithV2Builders {\n\n  def withCatalogOwnedPreviewTestTable(testFx: (String, List[ParsedLogData]) => Unit): Unit = {\n    val tablePath = getTestResourceFilePath(\"catalog-owned-preview\")\n\n    // Note: We need to *resolve* each test resource file path, because the table root file path\n    //       will itself be resolved when we create the Snapshot. If we resolved some paths but\n    //       not others, we would get an error like `File <commit-file> doesn't belong in the\n    //       transaction log at <log-path>`.\n\n    val parsedLogData = Seq(\n      // scalastyle:off line.size.limit\n      getTestResourceFilePath(\"catalog-owned-preview/_delta_log/_staged_commits/00000000000000000001.4cb9708e-b478-44de-b203-53f9ba9b2876.json\"),\n      getTestResourceFilePath(\"catalog-owned-preview/_delta_log/_staged_commits/00000000000000000002.5b9bba4a-0085-430d-a65e-b0d38c1afbe9.json\"))\n      // scalastyle:on line.size.limit\n      .map { path => defaultEngine.getFileSystemClient.resolvePath(path) }\n      .map { p => FileStatus.of(p) }\n      .map { fs => ParsedLogData.forFileStatus(fs) }\n      .toList\n    testFx(tablePath, parsedLogData)\n  }\n\n  test(\"simple e2e read of catalogManaged table with staged ratified commits\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, parsedLogData) =>\n      // ===== WHEN =====\n      val snapshot = TableManager\n        .loadSnapshot(tablePath)\n        .asInstanceOf[SnapshotBuilderImpl]\n        .atVersion(2)\n        .withLogData(parsedLogData.asJava)\n        .withMaxCatalogVersion(2)\n        .build(defaultEngine)\n\n      // ===== THEN =====\n      assert(snapshot.getVersion === 2)\n      assert(snapshot.getLogSegment.getDeltas.size() === 3)\n      assert(snapshot.getTimestamp(defaultEngine) === 1749830881799L)\n\n      val protocol = snapshot.getProtocol\n      assert(protocol.getMinReaderVersion == TABLE_FEATURES_MIN_READER_VERSION)\n      assert(protocol.getMinWriterVersion == TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(protocol.getReaderFeatures.contains(CATALOG_MANAGED_RW_FEATURE.featureName()))\n      assert(protocol.getWriterFeatures.contains(CATALOG_MANAGED_RW_FEATURE.featureName()))\n      assert(protocol.getWriterFeatures.contains(IN_COMMIT_TIMESTAMP_W_FEATURE.featureName()))\n\n      val actualResult = readSnapshot(snapshot)\n      val expectedResult = (0 to 199).map { x => TestRow(x / 100, x) }\n      checkAnswer(actualResult, expectedResult)\n    }\n  }\n\n  test(\"e2e DeltaHistoryManager.getActiveCommitAtTimestamp with catalogManaged table \" +\n    \"with staged ratified commits\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, parsedLogData) =>\n      val logPath = new Path(tablePath, \"_delta_log\")\n\n      val parsedRatifiedCatalogCommits = parsedLogData\n        .filter(_.isInstanceOf[ParsedCatalogCommitData])\n        .map(_.asInstanceOf[ParsedCatalogCommitData])\n\n      val latestSnapshot = TableManager\n        .loadSnapshot(tablePath)\n        .asInstanceOf[SnapshotBuilderImpl]\n        .withLogData(parsedLogData.asJava)\n        .withMaxCatalogVersion(2)\n        .build(defaultEngine)\n\n      def checkGetActiveCommitAtTimestamp(\n          timestamp: Long,\n          expectedVersion: Long,\n          canReturnLastCommit: Boolean = false,\n          canReturnEarliestCommit: Boolean = false): Unit = {\n        val activeCommit = DeltaHistoryManager.getActiveCommitAtTimestamp(\n          defaultEngine,\n          latestSnapshot,\n          logPath,\n          timestamp,\n          true, /* mustBeRecreatable */\n          canReturnLastCommit,\n          canReturnEarliestCommit,\n          parsedRatifiedCatalogCommits.asJava)\n        assert(activeCommit.getVersion == expectedVersion)\n      }\n\n      val v0Ts = 1749830855993L // published commit\n      val v1Ts = 1749830871085L // staged commit\n      val v2Ts = 1749830881799L // staged commit\n\n      // Query a timestamp before V0 should fail if canReturnEarliestCommit = false\n      val e1 = intercept[KernelException] {\n        checkGetActiveCommitAtTimestamp(v0Ts - 1, 0)\n      }\n      assert(e1.getMessage.contains(\"before the earliest available version\"))\n\n      // Query a timestamp before V0 with canReturnEarliestCommit = true\n      checkGetActiveCommitAtTimestamp(v0Ts - 1, 0, canReturnEarliestCommit = true)\n\n      // Query @ V0\n      checkGetActiveCommitAtTimestamp(v0Ts, 0)\n\n      // Query between V0 and V1\n      checkGetActiveCommitAtTimestamp(v0Ts + 1, 0)\n\n      // Query at V1\n      checkGetActiveCommitAtTimestamp(v1Ts, 1)\n\n      // Query between V1 and V2\n      checkGetActiveCommitAtTimestamp(v1Ts + 1, 1)\n\n      // Query at V2\n      checkGetActiveCommitAtTimestamp(v2Ts, 2)\n\n      // Query a timestamp after V2 should fail with canReturnLastCommit = false\n      val e2 = intercept[KernelException] {\n        checkGetActiveCommitAtTimestamp(v2Ts + 1, 2)\n      }\n      assert(e2.getMessage.contains(\"is after the latest available version\"))\n\n      // Query a timestamp after V2 with canReturnLastCommit = true\n      checkGetActiveCommitAtTimestamp(v2Ts + 1, 2, canReturnLastCommit = true)\n\n    }\n  }\n\n  test(\"time-travel by ts read of catalogManaged table with ratified commits\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, parsedLogData) =>\n      val v0Ts = 1749830855993L // published commit\n      val v1Ts = 1749830871085L // ratified staged commit\n      val v2Ts = 1749830881799L // ratified staged commit\n\n      val latestSnapshot = TableManager\n        .loadSnapshot(tablePath)\n        .asInstanceOf[SnapshotBuilderImpl]\n        .withLogData(parsedLogData.asJava)\n        .withMaxCatalogVersion(2)\n        .build(defaultEngine)\n\n      def checkTimeTravelByTimestamp(\n          timestamp: Long,\n          expectedVersion: Long,\n          expectedSnapshotTimestamp: Long): Unit = {\n        val snapshot = TableManager\n          .loadSnapshot(tablePath)\n          .atTimestamp(timestamp, latestSnapshot)\n          .withMaxCatalogVersion(2)\n          .withLogData(parsedLogData.asJava)\n          .build(defaultEngine)\n        assert(snapshot.getVersion == expectedVersion)\n        assert(snapshot.getTimestamp(defaultEngine) == expectedSnapshotTimestamp)\n      }\n\n      // Between v0 and v1 should return v0 (between published & ratified)\n      checkTimeTravelByTimestamp(v0Ts + 1, 0, v0Ts)\n\n      // Exactly v1 should return v1\n      checkTimeTravelByTimestamp(v1Ts, 1, v1Ts)\n\n      // Between v1 and v2 should return v1 (between 2 ratified commits)\n      checkTimeTravelByTimestamp(v1Ts + 1, 1, v1Ts)\n\n      // Exactly v2 should return v2\n      checkTimeTravelByTimestamp(v2Ts, 2, v2Ts)\n\n      // After v2 should fail\n      val e = intercept[KernelException] {\n        checkTimeTravelByTimestamp(v2Ts + 1, 2, v2Ts)\n      }\n      assert(e.getMessage.contains(\"is after the latest available version\"))\n    }\n  }\n\n  test(\"e2e CommitRange test with catalogManaged table with staged ratified commits\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, parsedLogData) =>\n      val v0Ts = 1749830855993L // published commit\n      val v1Ts = 1749830871085L // staged commit\n      val v2Ts = 1749830881799L // staged commit\n\n      val latestSnapshot = TableManager\n        .loadSnapshot(tablePath)\n        .withLogData(parsedLogData.asJava)\n        .withMaxCatalogVersion(2)\n        .build(defaultEngine)\n\n      def checkStartBoundary(timestamp: Long, expectedVersion: Long): Unit = {\n        assert(TableManager.loadCommitRange(\n          tablePath,\n          CommitBoundary.atTimestamp(timestamp, latestSnapshot))\n          .withLogData(parsedLogData.asJava)\n          .withMaxCatalogVersion(2)\n          .build(defaultEngine)\n          .getStartVersion == expectedVersion)\n      }\n      def checkEndBoundary(timestamp: Long, expectedVersion: Long): Unit = {\n        assert(TableManager.loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n          .withLogData(parsedLogData.asJava)\n          .withMaxCatalogVersion(2)\n          .withEndBoundary(CommitBoundary.atTimestamp(timestamp, latestSnapshot))\n          .build(defaultEngine)\n          .getEndVersion == expectedVersion)\n      }\n\n      // startTimestamp is before V0\n      checkStartBoundary(v0Ts - 1, 0)\n      // endTimestamp is before V0\n      intercept[KernelException] {\n        checkEndBoundary(v0Ts - 1, -1)\n      }\n\n      // startTimestamp is at V0\n      checkStartBoundary(v0Ts, 0)\n      // endTimestamp is at V0\n      checkEndBoundary(v0Ts, 0)\n\n      // startTimestamp is between V0 and V1\n      checkStartBoundary(v0Ts + 100L, 1)\n      // endTimestamp is between V0 and V1\n      checkEndBoundary(v0Ts + 100L, 0)\n\n      // startTimestamp is at V1\n      checkStartBoundary(v1Ts, 1)\n      // endTimestamp is at V1\n      checkEndBoundary(v1Ts, 1)\n\n      // startTimestamp is between V1 and V2\n      checkStartBoundary(v1Ts + 100L, 2)\n      // endTimestamp is between V1 and V2\n      checkEndBoundary(v1Ts + 100L, 1)\n\n      // startTimestamp is at V2\n      checkStartBoundary(v2Ts, 2)\n      // endTimestamp is at V2\n      checkEndBoundary(v2Ts, 2)\n\n      // startTimestamp is after V2\n      intercept[KernelException] {\n        checkStartBoundary(v2Ts + 10, -1)\n      }\n      // endTimestamp is after V2\n      checkEndBoundary(v2Ts + 10, 2)\n\n      // Verify the fileList in the CommitRange\n      val commitRange = TableManager\n        .loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n        .withLogData(parsedLogData.asJava)\n        .withMaxCatalogVersion(2)\n        .build(defaultEngine)\n\n      val expectedFileList = Seq(\n        // scalastyle:off line.size.limit\n        getTestResourceFilePath(\"catalog-owned-preview/_delta_log/00000000000000000000.json\"),\n        getTestResourceFilePath(\"catalog-owned-preview/_delta_log/_staged_commits/00000000000000000001.4cb9708e-b478-44de-b203-53f9ba9b2876.json\"),\n        getTestResourceFilePath(\"catalog-owned-preview/_delta_log/_staged_commits/00000000000000000002.5b9bba4a-0085-430d-a65e-b0d38c1afbe9.json\")\n        // scalastyle:on line.size.limit\n      ).map(path => defaultEngine.getFileSystemClient.resolvePath(path))\n\n      assert(commitRange.asInstanceOf[CommitRangeImpl].getDeltaFiles().asScala.map(_.getPath) ==\n        expectedFileList)\n    }\n  }\n\n  // We test this in the unit tests as well, but since those use the withProtocolAndMetadata API\n  // we also test it here with a real table where we load the P&M from the log\n  test(\"reading a catalogManaged table without providing maxCatalogVersion fails\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, parsedLogData) =>\n      // With logData\n      intercept[IllegalArgumentException] {\n        TableManager\n          .loadSnapshot(tablePath)\n          .withLogData(parsedLogData.asJava)\n          .build(defaultEngine)\n      }\n      // Without logData (and with time-travel-version)\n      intercept[IllegalArgumentException] {\n        TableManager\n          .loadSnapshot(tablePath)\n          .atVersion(0)\n          .build(defaultEngine)\n      }\n    }\n  }\n\n  test(\"reading a file-system managed table and providing maxCatalogVersion fails\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      // Create a basic file-system managed table\n      createEmptyTable(tablePath = tablePath, schema = testSchema)\n      // Try to read it and provide the maxCatalogVersion\n      intercept[IllegalArgumentException] {\n        TableManager\n          .loadSnapshot(tablePath)\n          .withMaxCatalogVersion(0)\n          .build(engine)\n      }\n    }\n  }\n\n  test(\"for latest queries we do not load past the maxRatifiedVersion even if \" +\n    \"later versions exist on the file-system\") {\n    withTempDir { tempDir =>\n      withCatalogOwnedPreviewTestTable { (resourceTablePath, resourceLogData) =>\n        // Copy the catalog-owned-preview test resource table to the temp directory\n        org.apache.commons.io.FileUtils.copyDirectory(\n          new java.io.File(resourceTablePath),\n          tempDir)\n\n        // \"Publish\" v1 and v2 (we do both to maintain ordered backfill)\n        val deltaLogPath = new Path(tempDir.getPath, \"_delta_log\")\n        val stagedCommitPath = new Path(deltaLogPath, \"_staged_commits\")\n        resourceLogData.foreach { stagedCommit =>\n          val stagedCommitFile = new java.io.File(\n            stagedCommitPath.toString,\n            new Path(stagedCommit.getFileStatus.getPath).getName)\n          val publishedCommitFile = new java.io.File(\n            FileNames.deltaFile(deltaLogPath.toString, stagedCommit.getVersion))\n          org.apache.commons.io.FileUtils.copyFile(stagedCommitFile, publishedCommitFile)\n        }\n\n        def convertResourceLogData(logData: ParsedLogData): ParsedLogData = {\n          val path = new Path(stagedCommitPath, new Path(logData.getFileStatus.getPath).getName)\n          ParsedLogData.forFileStatus(FileStatus.of(\n            defaultEngine.getFileSystemClient.resolvePath(path.toString)))\n        }\n\n        Seq(0, 1, 2).foreach { maxCatalogVersion =>\n          {\n            // Try to read the table with no parsedLogData\n            val snapshot = TableManager\n              .loadSnapshot(tempDir.getPath)\n              .withMaxCatalogVersion(maxCatalogVersion)\n              .build(defaultEngine)\n            assert(snapshot.getVersion == maxCatalogVersion)\n\n          }\n          {\n            // Try to read the table with parsedLogData\n            val parsedLogData = resourceLogData\n              .filter(_.getVersion <= maxCatalogVersion)\n              .map(convertResourceLogData)\n            val snapshot = TableManager\n              .loadSnapshot(tempDir.getPath)\n              .withMaxCatalogVersion(maxCatalogVersion)\n              .withLogData(parsedLogData.asJava)\n              .build(defaultEngine)\n            assert(snapshot.getVersion == maxCatalogVersion)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"for latest queries if we cannot load the maxRatifiedVersion we fail\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, _) =>\n      // We can only test this when no logData is provided. Otherwise we require logData to end\n      // with maxRatifiedVersion ==> it should be able to be read.\n      val e = intercept[KernelException] {\n        TableManager\n          .loadSnapshot(tablePath)\n          .withMaxCatalogVersion(2)\n          .build(defaultEngine)\n      }\n      assert(e.getMessage.contains(\"Cannot load table version 2\"))\n    }\n  }\n\n  test(\"for latest queries we read the _last_checkpoint file\") {\n    withCatalogOwnedPreviewTestTable { (resourceTablePath, resourceLogData) =>\n      // It doesn't matter if the checkpoint actually exists; we just want to check that during\n      // log segment building we try to read _last_checkpoint\n      import io.delta.kernel.defaults.MetricsEngine\n      val engine = new MetricsEngine(new HadoopFileIO(new Configuration()))\n      val snapshot = TableManager\n        .loadSnapshot(resourceTablePath)\n        .withMaxCatalogVersion(2)\n        .withLogData(resourceLogData.asJava)\n        .build(engine)\n      assert(snapshot.getVersion == 2)\n      assert(engine.getJsonHandler.getLastCheckpointMetadataReadCalls == 1)\n    }\n  }\n\n  test(\"for commitRange queries with no end boundary we do not load past the maxRatifiedVersion \" +\n    \"even if later versions exist on the file-system\") {\n    withTempDir { tempDir =>\n      withCatalogOwnedPreviewTestTable { (resourceTablePath, resourceLogData) =>\n        // Copy the catalog-owned-preview test resource table to the temp directory\n        org.apache.commons.io.FileUtils.copyDirectory(\n          new java.io.File(resourceTablePath),\n          tempDir)\n\n        // \"Publish\" v1 and v2 (we do both to maintain ordered backfill)\n        val deltaLogPath = new Path(tempDir.getPath, \"_delta_log\")\n        val stagedCommitPath = new Path(deltaLogPath, \"_staged_commits\")\n        resourceLogData.foreach { stagedCommit =>\n          val stagedCommitFile = new java.io.File(\n            stagedCommitPath.toString,\n            new Path(stagedCommit.getFileStatus.getPath).getName)\n          val publishedCommitFile = new java.io.File(\n            FileNames.deltaFile(deltaLogPath.toString, stagedCommit.getVersion))\n          org.apache.commons.io.FileUtils.copyFile(stagedCommitFile, publishedCommitFile)\n        }\n\n        def convertResourceLogData(logData: ParsedLogData): ParsedLogData = {\n          val path = new Path(stagedCommitPath, new Path(logData.getFileStatus.getPath).getName)\n          ParsedLogData.forFileStatus(FileStatus.of(\n            defaultEngine.getFileSystemClient.resolvePath(path.toString)))\n        }\n\n        Seq(0, 1, 2).foreach { maxCatalogVersion =>\n          {\n            // Try to read the table with no parsedLogData\n            val commitRange = TableManager\n              .loadCommitRange(tempDir.getPath, CommitBoundary.atVersion(0))\n              .withMaxCatalogVersion(maxCatalogVersion)\n              .build(defaultEngine)\n            assert(commitRange.getEndVersion == maxCatalogVersion)\n          }\n          {\n            // Try to read the table with parsedLogData\n            val parsedLogData = resourceLogData\n              .filter(_.getVersion <= maxCatalogVersion)\n              .map(convertResourceLogData)\n            val commitRange = TableManager\n              .loadCommitRange(tempDir.getPath, CommitBoundary.atVersion(0))\n              .withMaxCatalogVersion(maxCatalogVersion)\n              .withLogData(parsedLogData.asJava)\n              .build(defaultEngine)\n            assert(commitRange.getEndVersion == maxCatalogVersion)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"for commitRange queries with no end boundary if we cannot load the maxRatifiedVersion we \" +\n    \"fail\") {\n    withCatalogOwnedPreviewTestTable { (tablePath, _) =>\n      // We can only test this when no logData is provided. Otherwise we require logData to end\n      // with maxRatifiedVersion ==> it should be able to be read.\n      val e = intercept[KernelException] {\n        TableManager\n          .loadCommitRange(tablePath, CommitBoundary.atVersion(0))\n          .withMaxCatalogVersion(2)\n          .build(defaultEngine)\n      }\n      assert(e.getMessage.contains(\n        \"Requested table changes ending with endVersion=2 but no log file found for version 2\"))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/catalogManaged/CatalogManagedPropertyValidationSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.catalogManaged\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{Operation, TableManager, Transaction}\nimport io.delta.kernel.commit.Committer\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.TestUtils\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.types.{IntegerType, StructType}\nimport io.delta.kernel.utils.CloseableIterable.emptyIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass CatalogManagedPropertyValidationSuite extends AnyFunSuite with TestUtils {\n\n  val catalogManagedFeaturePropMap = Map(\n    TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey -> \"supported\")\n  val validRequiredCatalogPropMap = Map(\n    customCatalogCommitter.REQUIRED_PROPERTY_KEY -> customCatalogCommitter.REQUIRED_PROPERTY_VALUE)\n  val invalidRequiredCatalogPropMap = Map(\n    customCatalogCommitter.REQUIRED_PROPERTY_KEY -> \"invalid\")\n\n  case class CatalogManagedTestCase(\n      testName: String,\n      /** \"CREATE\", \"UPDATE\", or \"REPLACE\" */\n      operationType: String,\n      initialTableProperties: Map[String, String] = Map.empty,\n      transactionProperties: Map[String, String],\n      /** only applicable to UPDATE */\n      removedPropertyKeys: Set[String] = Set.empty,\n      /** create table for UPDATE/REPLACE */\n      createInitialTableCommitter: Committer = customCatalogCommitter,\n      expectedSuccess: Boolean = true,\n      expectedExceptionMessage: Option[String] = None,\n      /** only applicable if SUCCESS */\n      expectedIctEnabled: Boolean = true,\n      /** only applicable if SUCCESS */\n      expectedCatalogManagedSupported: Boolean = true)\n\n  val catalogManagedTestCases = Seq(\n    // ===== CREATE cases =====\n    CatalogManagedTestCase(\n      testName = \"CREATE: set catalogManaged=supported => enables catalogManaged and ICT\",\n      operationType = \"CREATE\",\n      transactionProperties = catalogManagedFeaturePropMap),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL CREATE: set catalogManaged=supported and explicitly disable ICT => THROW\",\n      operationType = \"CREATE\",\n      transactionProperties = Map(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey -> \"supported\",\n        \"delta.enableInCommitTimestamps\" -> \"false\"),\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Cannot disable inCommitTimestamp when enabling catalogManaged\")),\n\n    // ===== UPDATE cases =====\n    CatalogManagedTestCase(\n      testName = \"UPDATE: set catalogManaged=supported => enables catalogManaged and ICT\",\n      operationType = \"UPDATE\",\n      initialTableProperties = Map.empty, // Start with basic table\n      transactionProperties = catalogManagedFeaturePropMap),\n    CatalogManagedTestCase(\n      testName = \"UPDATE: set catalogManaged=supported => enables ICT if previously disabled\",\n      operationType = \"UPDATE\",\n      initialTableProperties = Map(\"delta.enableInCommitTimestamps\" -> \"false\"),\n      transactionProperties = catalogManagedFeaturePropMap),\n    CatalogManagedTestCase(\n      testName = \"UPDATE: set catalogManaged=supported and ICT already enabled => Okay\",\n      operationType = \"UPDATE\",\n      initialTableProperties = Map(\"delta.enableInCommitTimestamps\" -> \"true\"),\n      transactionProperties = catalogManagedFeaturePropMap),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL UPDATE: set catalogManaged=supported and disable ICT => THROW\",\n      operationType = \"UPDATE\",\n      initialTableProperties = Map.empty,\n      transactionProperties = Map(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey -> \"supported\",\n        \"delta.enableInCommitTimestamps\" -> \"false\"),\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Cannot disable inCommitTimestamp when enabling catalogManaged\")),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL UPDATE: catalogManaged already supported, then disable ICT => THROW\",\n      operationType = \"UPDATE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = Map(\"delta.enableInCommitTimestamps\" -> \"false\"),\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Cannot disable inCommitTimestamp on a catalogManaged table\")),\n    CatalogManagedTestCase(\n      testName = \"NO-OP UPDATE: catalogManaged not being enabled should not affect ICT\",\n      operationType = \"UPDATE\",\n      initialTableProperties = Map.empty,\n      transactionProperties = Map(),\n      expectedIctEnabled = false,\n      expectedCatalogManagedSupported = false),\n\n    // ===== REPLACE cases =====\n    CatalogManagedTestCase(\n      testName = \"REPLACE: normal replace should succeed on a catalogManaged table\",\n      operationType = \"REPLACE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = Map()),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL REPLACE: set catalogManaged=supported => THROW\",\n      operationType = \"REPLACE\",\n      initialTableProperties = Map.empty,\n      transactionProperties = catalogManagedFeaturePropMap,\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Cannot enable the catalogManaged feature during a REPLACE command.\")),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL REPLACE: catalogManaged already supported, then disable ICT => THROW\",\n      operationType = \"REPLACE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = Map(\"delta.enableInCommitTimestamps\" -> \"false\"),\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Cannot disable inCommitTimestamp on a catalogManaged table\")),\n\n    // ===== Required catalog table property cases: Txn allowed to not explicitly set value =====\n    CatalogManagedTestCase(\n      testName = \"CREATE: User does not explicitly set catalog property => auto-set\",\n      operationType = \"CREATE\",\n      transactionProperties = catalogManagedFeaturePropMap\n    ), // <-- Missing, will be auto-set\n    CatalogManagedTestCase(\n      testName = \"REPLACE: User does not explicitly set catalog property => auto-set\",\n      operationType = \"REPLACE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = Map.empty\n    ), // <-- Missing, will be auto-set\n    CatalogManagedTestCase(\n      testName = \"UPDATE: Normal updates succeed\",\n      operationType = \"UPDATE\",\n      initialTableProperties = catalogManagedFeaturePropMap ++ validRequiredCatalogPropMap,\n      transactionProperties = Map(\"zip\" -> \"zap\")\n    ), // <-- Just testing that normal updates succee\n\n    // ===== Required catalog table property cases: User can input correct value =====\n    CatalogManagedTestCase(\n      testName = \"CREATE: Can set required catalog property to correct value\",\n      operationType = \"CREATE\",\n      transactionProperties =\n        catalogManagedFeaturePropMap ++ validRequiredCatalogPropMap\n    ), // <-- Set to valid\n    CatalogManagedTestCase(\n      testName = \"REPLACE: Can set required catalog property to correct value\",\n      operationType = \"REPLACE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = validRequiredCatalogPropMap\n    ), // <-- Set to valid\n    CatalogManagedTestCase(\n      testName = \"UPDATE: Can set required catalog property to correct value\",\n      operationType = \"UPDATE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = validRequiredCatalogPropMap\n    ), // <-- Set to valid\n\n    // ===== Required catalog table property case: User cannot remove or input incorrect value =====\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL CREATE: Set required catalog property to incorrect value => THROW\",\n      operationType = \"CREATE\",\n      transactionProperties =\n        catalogManagedFeaturePropMap ++ invalidRequiredCatalogPropMap, // <-- Set to invalid\n      expectedSuccess = false),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL REPLACE: Set required catalog property to incorrect value => THROW\",\n      operationType = \"REPLACE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = invalidRequiredCatalogPropMap, // <-- Set to invalid\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Metadata is missing or has incorrect values for required catalog properties\")),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL UPDATE: Set required catalog property to incorrect value => THROW\",\n      operationType = \"UPDATE\",\n      initialTableProperties = catalogManagedFeaturePropMap,\n      transactionProperties = invalidRequiredCatalogPropMap, // <-- Set to invalid\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Metadata is missing or has incorrect values for required catalog properties\")),\n    CatalogManagedTestCase(\n      testName = \"ILLEGAL UPDATE: Remove required catalog property => THROW\",\n      operationType = \"UPDATE\",\n      initialTableProperties = catalogManagedFeaturePropMap ++ validRequiredCatalogPropMap,\n      transactionProperties = Map.empty,\n      removedPropertyKeys = Set(customCatalogCommitter.REQUIRED_PROPERTY_KEY), // <-- Removed!\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Metadata is missing or has incorrect values for required catalog properties\")),\n\n    // ===== Required catalog table property case: Existing table invalid =====\n    CatalogManagedTestCase(\n      testName = \"REPLACE: On existing table with incorrect required catalog property => sets it\",\n      operationType = \"REPLACE\",\n      initialTableProperties =\n        catalogManagedFeaturePropMap ++ invalidRequiredCatalogPropMap, // <-- Set to invalid\n      createInitialTableCommitter = committerUsingPutIfAbsent, // allow creating the invalid table\n      transactionProperties = Map.empty),\n    CatalogManagedTestCase(\n      testName = \"UPDATE: On existing table with incorrect required catalog property => throws\",\n      operationType = \"UPDATE\",\n      initialTableProperties =\n        catalogManagedFeaturePropMap ++ invalidRequiredCatalogPropMap, // <-- Set to invalid\n      createInitialTableCommitter = committerUsingPutIfAbsent, // allow creating the invalid table\n      transactionProperties = Map.empty,\n      expectedSuccess = false,\n      expectedExceptionMessage =\n        Some(\"Metadata is missing or has incorrect values for required catalog properties\")))\n\n  catalogManagedTestCases.foreach { testCase =>\n    test(testCase.testName) {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        val schema = new StructType().add(\"id\", IntegerType.INTEGER)\n\n        // Setup initial table if this is an UPDATE operation\n        if (testCase.operationType == \"UPDATE\" || testCase.operationType == \"REPLACE\") {\n          TableManager\n            .buildCreateTableTransaction(tablePath, schema, \"engineInfo\")\n            .withTableProperties(testCase.initialTableProperties.asJava)\n            .withCommitter(testCase.createInitialTableCommitter)\n            .build(defaultEngine)\n            .commit(defaultEngine, emptyIterable[Row])\n        }\n\n        // CREATE, UPDATE, and REPLACE txnBuilders don't share a common parent interface. So, we\n        // treat the `txnBuilder` as a trait that has a `build(engine)` method. Scalastyle doesn't\n        // like this, but it's valid.\n        //\n        // scalastyle:off\n        val txnBuilder: { def build(engine: Engine): Transaction } = testCase.operationType match {\n          case \"CREATE\" =>\n            TableManager\n              .buildCreateTableTransaction(tablePath, schema, \"engineInfo\")\n              .withTableProperties(testCase.transactionProperties.asJava)\n              .withCommitter(customCatalogCommitter)\n\n          case \"UPDATE\" =>\n            val updateBuilder = TableManager\n              .loadSnapshot(tablePath)\n              .withCommitter(customCatalogCommitter)\n              .withMaxCatalogVersionIfApplicable(\n                isCatalogManaged = TableFeatures.isPropertiesManuallySupportingTableFeature(\n                  testCase.initialTableProperties.asJava,\n                  TableFeatures.CATALOG_MANAGED_RW_FEATURE),\n                maxCatalogVersion = 0)\n              .build(defaultEngine)\n              .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n              .withTablePropertiesAdded(testCase.transactionProperties.asJava)\n\n            if (testCase.removedPropertyKeys.nonEmpty) {\n              updateBuilder.withTablePropertiesRemoved(testCase.removedPropertyKeys.asJava)\n            } else {\n              updateBuilder\n            }\n\n          case \"REPLACE\" =>\n            val replaceSchema = schema.add(\"col2\", IntegerType.INTEGER)\n\n            TableManager\n              .loadSnapshot(tablePath)\n              .withCommitter(customCatalogCommitter)\n              .withMaxCatalogVersionIfApplicable(\n                isCatalogManaged = TableFeatures.isPropertiesManuallySupportingTableFeature(\n                  testCase.initialTableProperties.asJava,\n                  TableFeatures.CATALOG_MANAGED_RW_FEATURE),\n                maxCatalogVersion = 0)\n              .build(defaultEngine)\n              .asInstanceOf[SnapshotImpl]\n              .buildReplaceTableTransaction(replaceSchema, \"engineInfo\")\n              .withTableProperties(testCase.transactionProperties.asJava)\n        }\n        // scalastyle:on\n\n        if (testCase.expectedSuccess) {\n          // Transaction building should succeed\n          val result = txnBuilder.build(defaultEngine).commit(defaultEngine, emptyIterable[Row])\n\n          val postCommitSnapshot = result\n            .getPostCommitSnapshot\n            .orElseThrow(() =>\n              new RuntimeException(\"Expected post-commit snapshot when no concurrent writes\"))\n            .asInstanceOf[SnapshotImpl]\n\n          // Verify the results\n          val protocol = postCommitSnapshot.getProtocol\n\n          // Check if catalogManaged feature is supported\n          val catalogManagedSupported = protocol\n            .supportsFeature(TableFeatures.CATALOG_MANAGED_RW_FEATURE)\n          assert(catalogManagedSupported == testCase.expectedCatalogManagedSupported)\n\n          // Check if ICT is enabled in metadata\n          val ictEnabled = postCommitSnapshot.getMetadata.getConfiguration.asScala\n            .get(\"delta.enableInCommitTimestamps\")\n            .contains(\"true\")\n          assert(ictEnabled == testCase.expectedIctEnabled)\n\n          // If catalogManaged is supported, ICT feature should also be supported\n          if (testCase.expectedCatalogManagedSupported) {\n            assert(protocol.supportsFeature(TableFeatures.IN_COMMIT_TIMESTAMP_W_FEATURE))\n\n            assert(\n              customCatalogCommitter\n                .getRequiredTableProperties\n                .asScala.toSet.subsetOf(postCommitSnapshot.getTableProperties.asScala.toSet))\n          }\n        } else {\n          // Transaction building should fail\n          val exception = intercept[Exception] {\n            txnBuilder.build(defaultEngine)\n          }\n\n          testCase.expectedExceptionMessage.foreach { expectedMsg =>\n            assert(exception.getMessage.contains(expectedMsg))\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/engine/DefaultExpressionHandlerSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine\n\nimport io.delta.kernel.defaults.utils.ExpressionTestUtils\nimport io.delta.kernel.types.BooleanType.BOOLEAN\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.LongType.LONG\nimport io.delta.kernel.types.StringType.STRING\nimport io.delta.kernel.types.StructType\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DefaultExpressionHandlerSuite extends AnyFunSuite with ExpressionTestUtils {\n\n  test(\"create selection vector: single value\") {\n    Seq(true, false).foreach { testValue =>\n      val outputVector = selectionVector(Seq(testValue).toArray, 0, 1)\n      assert(outputVector.getDataType === BOOLEAN)\n      assert(outputVector.getSize == 1)\n      assert(outputVector.isNullAt(0) == false)\n      assert(outputVector.getBoolean(0) == testValue)\n    }\n  }\n\n  test(\"create selection vector: multiple values array, partial array\") {\n    Seq((0, testValues.length), (0, 3), (2, 2), (2, 4), (3, testValues.length)).foreach { pair =>\n      val (from, to) = (pair._1, pair._2)\n      val outputVector = selectionVector(testValues, from, to)\n      assert(outputVector.getDataType === BOOLEAN)\n      assert(outputVector.getSize == (to - from))\n      Seq.range(from, to).foreach { rowId =>\n        assert(outputVector.isNullAt(rowId - from) == false)\n        assert(outputVector.getBoolean(rowId - from) == testValues(rowId))\n      }\n    }\n  }\n\n  test(\"create selection vector: update values array and expect no changes in output\") {\n    val outputVector = selectionVector(testValues, 0, testValues.length)\n    // update the input values array and assert the value is not changed in the returned vector\n    val oldValue = testValues(2)\n    assert(oldValue == false)\n    testValues(2) = true\n    assert(outputVector.isNullAt(2) == false)\n    assert(outputVector.getBoolean(2) == oldValue)\n  }\n\n  test(\"create selection vector: invalid to and/or from offset\") {\n    Seq((3, 2), (2, testValues.length + 1), (testValues.length + 1, 100)).foreach { pair =>\n      val (from, to) = (pair._1, pair._2)\n      val ex = intercept[IllegalArgumentException] {\n        selectionVector(testValues, from, to)\n      }\n      assert(ex.getMessage.contains(\n        s\"invalid range from=$from, to=$to, values length=${testValues.length}\"))\n    }\n  }\n\n  test(\"create selection vector: null values array\") {\n    val ex = intercept[NullPointerException] {\n      selectionVector(null, 0, 25)\n    }\n    assert(ex.getMessage.contains(\"values is null\"))\n  }\n\n  private def selectionVector(values: Array[Boolean], from: Int, to: Int) = {\n    new DefaultExpressionHandler().createSelectionVector(values, from, to)\n  }\n\n  private val testValues = Seq(false, true, false, false, true, true).toArray\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/engine/DefaultFileSystemClientSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine\n\nimport java.io.FileNotFoundException\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.defaults.utils.TestUtils\n\nimport org.apache.hadoop.fs.{FileSystem, Path}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DefaultFileSystemClientSuite extends AnyFunSuite with TestUtils {\n\n  val fsClient = defaultEngine.getFileSystemClient\n  val fs = FileSystem.get(configuration)\n\n  private def writeFile(path: String, content: String): Unit = {\n    val out = fs.create(new Path(path))\n    try {\n      out.write(content.getBytes(\"UTF-8\"))\n    } finally {\n      out.close()\n    }\n  }\n\n  private def readFile(path: String): String = {\n    val fileStatus = fs.getFileStatus(new Path(path))\n    val buffer = new Array[Byte](fileStatus.getLen.toInt)\n    val in = fs.open(new Path(path))\n    try {\n      in.readFully(buffer)\n      new String(buffer, \"UTF-8\")\n    } finally {\n      in.close()\n    }\n  }\n\n  private def withTempSrcAndDestFiles(f: (String, String) => Unit): Unit = {\n    withTempDir { tempDir =>\n      val src = tempDir + \"/source.txt\"\n      val dest = tempDir + \"/dest.txt\"\n      f(src, dest)\n    }\n  }\n\n  test(\"list from file\") {\n    val basePath = fsClient.resolvePath(getTestResourceFilePath(\"json-files\"))\n    val listFrom = fsClient.resolvePath(getTestResourceFilePath(\"json-files/2.json\"))\n\n    val actListOutput = new ArrayBuffer[String]()\n    val files = fsClient.listFrom(listFrom)\n    try {\n      fsClient.listFrom(listFrom).forEach(f => actListOutput += f.getPath)\n    } finally if (files != null) {\n        files.close()\n      }\n\n    val expListOutput = Seq(basePath + \"/2.json\", basePath + \"/3.json\")\n\n    assert(expListOutput === actListOutput)\n  }\n\n  test(\"list from non-existent file\") {\n    intercept[FileNotFoundException] {\n      fsClient.listFrom(\"file:/non-existentfileTable/01.json\")\n    }\n  }\n\n  test(\"resolve path\") {\n    val inputPath = getTestResourceFilePath(\"json-files\")\n    val resolvedPath = fsClient.resolvePath(inputPath)\n\n    assert(\"file:\" + inputPath === resolvedPath)\n  }\n\n  test(\"resolve path on non-existent file\") {\n    val inputPath = \"/non-existentfileTable/01.json\"\n    val resolvedPath = fsClient.resolvePath(inputPath)\n    assert(\"file:\" + inputPath === resolvedPath)\n  }\n\n  test(\"mkdirs\") {\n    withTempDir { tempdir =>\n      val dir1 = tempdir + \"/test\"\n      assert(fsClient.mkdirs(dir1))\n      assert(fs.exists(new Path(dir1)))\n\n      val dir2 = tempdir + \"/test1/test2\" // nested\n      assert(fsClient.mkdirs(dir2))\n      assert(fs.exists(new Path(dir2)))\n\n      val dir3 = \"/non-existentfileTable/sfdsd\"\n      assert(!fsClient.mkdirs(dir3))\n      assert(!fs.exists(new Path(dir3)))\n    }\n  }\n\n  test(\"getFileStatus\") {\n    val filePath = getTestResourceFilePath(\"json-files/1.json\")\n    val fileStatus = fsClient.getFileStatus(filePath)\n\n    assert(fileStatus.getPath == fsClient.resolvePath(filePath))\n    assert(fileStatus.getSize > 0)\n    assert(fileStatus.getModificationTime > 0)\n  }\n\n  test(\"getFileStatus on non-existent file\") {\n    intercept[FileNotFoundException] {\n      fsClient.getFileStatus(\"/non-existent-file.json\")\n    }\n  }\n\n  test(\"copyFileAtomically - overwrite=false, dest does not exist\") {\n    withTempSrcAndDestFiles { (src, dest) =>\n      writeFile(src, \"test content\")\n      fsClient.copyFileAtomically(src, dest, false /* overwrite */ )\n\n      assert(fs.exists(new Path(dest)))\n      assert(readFile(dest).trim == \"test content\")\n    }\n  }\n\n  test(\"copyFileAtomically - overwrite=false, dest exists\") {\n    withTempSrcAndDestFiles { (src, dest) =>\n      writeFile(src, \"source content\")\n      writeFile(dest, \"existing content\")\n\n      intercept[java.nio.file.FileAlreadyExistsException] {\n        fsClient.copyFileAtomically(src, dest, false /* overwrite */ )\n      }\n    }\n  }\n\n  test(\"copyFileAtomically - overwrite=true\") {\n    withTempSrcAndDestFiles { (src, dest) =>\n      writeFile(src, \"new content\")\n      writeFile(dest, \"old content\")\n\n      fsClient.copyFileAtomically(src, dest, true /* overwrite */ )\n      assert(readFile(dest).trim == \"new content\")\n    }\n  }\n\n  test(\"copyFileAtomically with non-existent source\") {\n    withTempSrcAndDestFiles { (src, dest) =>\n      intercept[FileNotFoundException] {\n        fsClient.copyFileAtomically(src, dest, false /* overwrite */ )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/engine/DefaultJsonHandlerSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine\n\nimport java.math.{BigDecimal => JBigDecimal}\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.{Collections, Optional}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.ColumnVector\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.{DefaultVectorTestUtils, TestRow, TestUtils}\nimport io.delta.kernel.internal.actions.CommitInfo\nimport io.delta.kernel.internal.util.InternalUtils.singletonStringColumnVector\nimport io.delta.kernel.types._\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DefaultJsonHandlerSuite extends AnyFunSuite with TestUtils with DefaultVectorTestUtils {\n\n  val jsonHandler = new DefaultJsonHandler(\n    new HadoopFileIO(\n      new Configuration {\n        set(\"delta.kernel.default.json.reader.batch-size\", \"1\")\n      }))\n  val fsClient = defaultEngine.getFileSystemClient\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for parseJson for statistics eligible types (additional in TestDefaultJsonHandler.java)\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  def testJsonParserWithSchema(\n      jsonString: String,\n      schema: StructType,\n      expectedRow: TestRow): Unit = {\n    val batchRows = jsonHandler.parseJson(\n      singletonStringColumnVector(jsonString),\n      schema,\n      Optional.empty()).getRows.toSeq\n    checkAnswer(batchRows, Seq(expectedRow))\n  }\n\n  def testJsonParserForSingleType(\n      jsonString: String,\n      dataType: DataType,\n      numColumns: Int,\n      expectedRow: TestRow): Unit = {\n    val schema = new StructType(\n      (1 to numColumns).map(i => new StructField(s\"col$i\", dataType, true)).asJava)\n    testJsonParserWithSchema(jsonString, schema, expectedRow)\n  }\n\n  def testOutOfRangeValue(stringValue: String, dataType: DataType): Unit = {\n    val e = intercept[RuntimeException] {\n      testJsonParserForSingleType(\n        jsonString = s\"\"\"{\"col1\":$stringValue}\"\"\",\n        dataType = dataType,\n        numColumns = 1,\n        expectedRow = TestRow())\n    }\n    assert(e.getMessage.contains(s\"Couldn't decode $stringValue\"))\n  }\n\n  test(\"parse byte type\") {\n    testJsonParserForSingleType(\n      jsonString = \"\"\"{\"col1\":0,\"col2\":-127,\"col3\":127, \"col4\":null}\"\"\",\n      dataType = ByteType.BYTE,\n      4,\n      TestRow(0.toByte, -127.toByte, 127.toByte, null))\n    testOutOfRangeValue(\"128\", ByteType.BYTE)\n    testOutOfRangeValue(\"-129\", ByteType.BYTE)\n    testOutOfRangeValue(\"2147483648\", ByteType.BYTE)\n  }\n\n  test(\"parse short type\") {\n    testJsonParserForSingleType(\n      jsonString = \"\"\"{\"col1\":-32767,\"col2\":8,\"col3\":32767, \"col4\":null}\"\"\",\n      dataType = ShortType.SHORT,\n      4,\n      TestRow(-32767.toShort, 8.toShort, 32767.toShort, null))\n    testOutOfRangeValue(\"32768\", ShortType.SHORT)\n    testOutOfRangeValue(\"-32769\", ShortType.SHORT)\n    testOutOfRangeValue(\"2147483648\", ShortType.SHORT)\n  }\n\n  test(\"parse integer type\") {\n    testJsonParserForSingleType(\n      jsonString = \"\"\"{\"col1\":-2147483648,\"col2\":8,\"col3\":2147483647, \"col4\":null}\"\"\",\n      dataType = IntegerType.INTEGER,\n      4,\n      TestRow(-2147483648, 8, 2147483647, null))\n    testOutOfRangeValue(\"2147483648\", IntegerType.INTEGER)\n    testOutOfRangeValue(\"-2147483649\", IntegerType.INTEGER)\n  }\n\n  test(\"parse long type\") {\n    testJsonParserForSingleType(\n      jsonString =\n        \"\"\"{\"col1\":-9223372036854775808,\"col2\":8,\"col3\":9223372036854775807, \"col4\":null}\"\"\",\n      dataType = LongType.LONG,\n      4,\n      TestRow(-9223372036854775808L, 8L, 9223372036854775807L, null))\n    testOutOfRangeValue(\"9223372036854775808\", LongType.LONG)\n    testOutOfRangeValue(\"-9223372036854775809\", LongType.LONG)\n  }\n\n  test(\"parse float type\") {\n    testJsonParserForSingleType(\n      jsonString =\n        \"\"\"\n          |{\"col1\":-9223.33,\"col2\":0.4,\"col3\":1.2E8,\n          |\"col4\":1.23E-7,\"col5\":0.004444444, \"col6\":null}\"\"\".stripMargin,\n      dataType = FloatType.FLOAT,\n      6,\n      TestRow(-9223.33f, 0.4f, 120000000.0f, 0.000000123f, 0.004444444f, null))\n    testOutOfRangeValue(\"3.4028235E+39\", FloatType.FLOAT)\n  }\n\n  test(\"parse double type\") {\n    testJsonParserForSingleType(\n      jsonString =\n        \"\"\"\n          |{\"col1\":-9.2233333333E8,\"col2\":0.4,\"col3\":1.2E8,\n          |\"col4\":1.234444444E-7,\"col5\":0.0444444444, \"col6\":null}\"\"\".stripMargin,\n      dataType = DoubleType.DOUBLE,\n      6,\n      TestRow(-922333333.33d, 0.4d, 120000000.0d, 0.0000001234444444d, 0.0444444444d, null))\n    // For some reason out-of-range doubles are parsed initially as Double.INFINITY instead of\n    // a BigDecimal\n    val e = intercept[RuntimeException] {\n      testJsonParserForSingleType(\n        jsonString = s\"\"\"{\"col1\":1.7976931348623157E+309}\"\"\",\n        dataType = DoubleType.DOUBLE,\n        numColumns = 1,\n        expectedRow = TestRow())\n    }\n    assert(e.getMessage.contains(s\"Couldn't decode\"))\n  }\n\n  test(\"parse string type\") {\n    testJsonParserForSingleType(\n      jsonString = \"\"\"{\"col1\": \"foo\", \"col2\": \"\", \"col3\": null}\"\"\",\n      dataType = StringType.STRING,\n      3,\n      TestRow(\"foo\", \"\", null))\n  }\n\n  test(\"parse decimal type\") {\n    testJsonParserWithSchema(\n      jsonString = \"\"\"\n      |{\n      |  \"col1\":0,\n      |  \"col2\":0.01234567891234567891234567891234567890,\n      |  \"col3\":123456789123456789123456789123456789,\n      |  \"col4\":1234567891234567891234567891.2345678900,\n      |  \"col5\":1.23,\n      |  \"col6\":null\n      |}\n      |\"\"\".stripMargin,\n      schema = new StructType()\n        .add(\"col1\", DecimalType.USER_DEFAULT)\n        .add(\"col2\", new DecimalType(38, 38))\n        .add(\"col3\", new DecimalType(38, 0))\n        .add(\"col4\", new DecimalType(38, 10))\n        .add(\"col5\", new DecimalType(5, 2))\n        .add(\"col6\", new DecimalType(5, 2)),\n      TestRow(\n        new JBigDecimal(0),\n        new JBigDecimal(\"0.01234567891234567891234567891234567890\"),\n        new JBigDecimal(\"123456789123456789123456789123456789\"),\n        new JBigDecimal(\"1234567891234567891234567891.2345678900\"),\n        new JBigDecimal(\"1.23\"),\n        null))\n  }\n\n  test(\"parse date type\") {\n    testJsonParserForSingleType(\n      jsonString = \"\"\"{\"col1\":\"2020-12-31\", \"col2\":\"1965-01-31\", \"col3\": null}\"\"\",\n      dataType = DateType.DATE,\n      3,\n      TestRow(18627, -1796, null))\n  }\n\n  test(\"parse timestamp type\") {\n    testJsonParserForSingleType(\n      jsonString =\n        \"\"\"\n          |{\n          | \"col1\":\"2050-01-01T00:00:00.000-08:00\",\n          | \"col2\":\"1970-01-01T06:30:23.523Z\",\n          | \"col3\":\"1960-01-01T10:00:00.000Z\",\n          | \"col4\":null\n          | }\n          | \"\"\".stripMargin,\n      dataType = TimestampType.TIMESTAMP,\n      numColumns = 4,\n      TestRow(2524636800000000L, 23423523000L, -315583200000000L, null))\n  }\n\n  test(\"parse timestamp type with large values\") {\n    // Timestamps far in the future should not cause overflow.\n    // ChronoUnit.MICROS.between() internally computes nanoseconds first, which overflows\n    // for timestamps more than ~292 years from epoch.\n    testJsonParserForSingleType(\n      jsonString = \"\"\"{\"col1\":\"9999-12-31T23:59:59.000+00:00\"}\"\"\",\n      dataType = TimestampType.TIMESTAMP,\n      numColumns = 1,\n      TestRow(253402300799000000L))\n  }\n\n  test(\"parse null input\") {\n    val schema = new StructType()\n      .add(\"nested_struct\", new StructType().add(\"foo\", IntegerType.INTEGER))\n\n    val batch = jsonHandler.parseJson(\n      singletonStringColumnVector(null),\n      schema,\n      Optional.empty())\n    assert(batch.getColumnVector(0).getChild(0).isNullAt(0))\n  }\n\n  test(\"parse NaN and INF for float and double\") {\n    def testSpecifiedString(json: String, output: TestRow): Unit = {\n      testJsonParserWithSchema(\n        jsonString = json,\n        schema = new StructType()\n          .add(\"col1\", FloatType.FLOAT)\n          .add(\"col2\", DoubleType.DOUBLE),\n        output)\n    }\n    testSpecifiedString(\"\"\"{\"col1\":\"NaN\",\"col2\":\"NaN\"}\"\"\", TestRow(Float.NaN, Double.NaN))\n    testSpecifiedString(\n      \"\"\"{\"col1\":\"+INF\",\"col2\":\"+INF\"}\"\"\",\n      TestRow(Float.PositiveInfinity, Double.PositiveInfinity))\n    testSpecifiedString(\n      \"\"\"{\"col1\":\"+Infinity\",\"col2\":\"+Infinity\"}\"\"\",\n      TestRow(Float.PositiveInfinity, Double.PositiveInfinity))\n    testSpecifiedString(\n      \"\"\"{\"col1\":\"Infinity\",\"col2\":\"Infinity\"}\"\"\",\n      TestRow(Float.PositiveInfinity, Double.PositiveInfinity))\n    testSpecifiedString(\n      \"\"\"{\"col1\":\"-INF\",\"col2\":\"-INF\"}\"\"\",\n      TestRow(Float.NegativeInfinity, Double.NegativeInfinity))\n    testSpecifiedString(\n      \"\"\"{\"col1\":\"-Infinity\",\"col2\":\"-Infinity\"}\"\"\",\n      TestRow(Float.NegativeInfinity, Double.NegativeInfinity))\n  }\n\n  test(\"don't parse unselected rows\") {\n    val selectionVector = booleanVector(Seq(true, false, false))\n    val jsonVector = stringVector(\n      Seq(\"\"\"{\"col1\":1}\"\"\", \"\"\"{\"col1\":\"foo\"}\"\"\", \"\"\"{\"col1\":\"foo\"}\"\"\"))\n    val batchRows = jsonHandler.parseJson(\n      jsonVector,\n      new StructType()\n        .add(\"col1\", IntegerType.INTEGER),\n      Optional.of(selectionVector)).getRows.toSeq\n    assert(!batchRows(0).isNullAt(0) && batchRows(0).getInt(0) == 1)\n    assert(batchRows(1).isNullAt(0) && batchRows(2).isNullAt(0))\n  }\n\n  test(\"read json files\") {\n    val expResults = Seq(\n      TestRow(\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\", 348L, true),\n      TestRow(\"part-00000-cb078bc1-0aeb-46ed-9cf8-74a843b32c8c-c000.snappy.parquet\", 687L, true),\n      TestRow(\"part-00001-9bf4b8f8-1b95-411b-bf10-28dc03aa9d2f-c000.snappy.parquet\", 705L, true),\n      TestRow(\"part-00000-0441e99a-c421-400e-83a1-212aa6c84c73-c000.snappy.parquet\", 650L, true),\n      TestRow(\"part-00001-34c8c673-3f44-4fa7-b94e-07357ec28a7d-c000.snappy.parquet\", 650L, true),\n      TestRow(\"part-00000-842017c2-3e02-44b5-a3d6-5b9ae1745045-c000.snappy.parquet\", 649L, true),\n      TestRow(\"part-00001-e62ca5a1-923c-4ee6-998b-c61d1cfb0b1c-c000.snappy.parquet\", 649L, true))\n    Seq(\n      (\n        fsClient.listFrom(getTestResourceFilePath(\"json-files/1.json\")),\n        expResults),\n      (\n        fsClient.listFrom(getTestResourceFilePath(\"json-files-with-empty/1.json\")),\n        expResults),\n      (\n        fsClient.listFrom(getTestResourceFilePath(\"json-files-with-empty/5.json\")),\n        expResults.takeRight(2)),\n      (\n        fsClient.listFrom(getTestResourceFilePath(\"json-files-all-empty/1.json\")),\n        Seq())).foreach {\n      case (testFiles, expResults) =>\n        val actResult = jsonHandler.readJsonFiles(\n          testFiles,\n          new StructType()\n            .add(\"path\", StringType.STRING)\n            .add(\"size\", LongType.LONG)\n            .add(\"dataChange\", BooleanType.BOOLEAN),\n          Optional.empty()).toSeq.map(batch => TestRow(batch.getRows.next))\n\n        checkAnswer(actResult, expResults)\n    }\n  }\n\n  test(\"parse json content\") {\n    val input = \"\"\"\n      |{\n      |  \"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\n      |  \"partitionValues\":{\"p1\" : \"0\", \"p2\" : \"str\"},\n      |  \"size\":348,\n      |  \"modificationTime\":1603723974000,\n      |  \"dataChange\":true\n      |}\n      |\"\"\".stripMargin\n    val readSchema = new StructType()\n      .add(\"path\", StringType.STRING)\n      .add(\"partitionValues\", new MapType(StringType.STRING, StringType.STRING, false))\n      .add(\"size\", LongType.LONG)\n      .add(\"dataChange\", BooleanType.BOOLEAN)\n\n    val batch = jsonHandler.parseJson(\n      singletonStringColumnVector(input),\n      readSchema,\n      Optional.empty[ColumnVector]())\n    assert(batch.getSize == 1)\n\n    val actResult = Seq(TestRow(batch.getRows.next))\n    val expResult = Seq(TestRow(\n      \"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\n      Map(\"p1\" -> \"0\", \"p2\" -> \"str\"),\n      348L,\n      true))\n\n    checkAnswer(actResult, expResult)\n  }\n\n  test(\"parse nested complex types\") {\n    val json = \"\"\"\n      |{\n      |  \"array\": [0, 1, null],\n      |  \"nested_array\": [[\"a\", \"b\"], [\"c\"], []],\n      |  \"map\": {\"a\":  true, \"b\":  false},\n      |  \"nested_map\": {\"a\":  {\"one\":  [], \"two\":  [1, 2, 3]}, \"b\":  {}},\n      |  \"array_of_struct\": [{\"field1\": \"foo\", \"field2\": 3}, {\"field1\": null}]\n      |}\n      |\"\"\".stripMargin\n\n    val schema = new StructType()\n      .add(\"array\", new ArrayType(IntegerType.INTEGER, true))\n      .add(\"nested_array\", new ArrayType(new ArrayType(StringType.STRING, true), true))\n      .add(\"map\", new MapType(StringType.STRING, BooleanType.BOOLEAN, true))\n      .add(\n        \"nested_map\",\n        new MapType(\n          StringType.STRING,\n          new MapType(StringType.STRING, new ArrayType(IntegerType.INTEGER, true), true),\n          true))\n      .add(\n        \"array_of_struct\",\n        new ArrayType(\n          new StructType()\n            .add(\"field1\", StringType.STRING, true)\n            .add(\"field2\", IntegerType.INTEGER, true),\n          true))\n    val batch = jsonHandler.parseJson(\n      singletonStringColumnVector(json),\n      schema,\n      Optional.empty[ColumnVector]())\n\n    val actResult = Seq(TestRow(batch.getRows.next))\n    val expResult = Seq(TestRow(\n      Vector(0, 1, null),\n      Vector(Vector(\"a\", \"b\"), Vector(\"c\"), Vector()),\n      Map(\"a\" -> true, \"b\" -> false),\n      Map(\n        \"a\" -> Map(\n          \"one\" -> Vector(),\n          \"two\" -> Vector(1, 2, 3)),\n        \"b\" -> Map()),\n      Vector(TestRow.fromSeq(Seq(\"foo\", 3)), TestRow.fromSeq(Seq(null, null)))))\n\n    checkAnswer(actResult, expResult)\n  }\n\n  test(\"write rows as json\") {\n    withTempDir { tempDir =>\n      val input = Seq(\n        \"\"\"{\n          | \"add\":\n          |  {\n          |    \"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\n          |    \"partitionValues\":{\"p1\" : \"0\", \"p2\" : \"str\"},\n          |    \"size\":348,\n          |    \"dataChange\":true\n          |  }\n          |}\n          |\"\"\".stripMargin.linesIterator.mkString,\n        \"\"\"{\n          | \"remove\":\n          |  {\n          |    \"path\":\"part-00000-d83dafd8-c344-49f0-ab1c-acd944e32493-c000.snappy.parquet\",\n          |    \"partitionValues\":{\"p1\" : \"0\", \"p2\" : \"str\"},\n          |    \"size\":348,\n          |    \"dataChange\":true\n          |  }\n          |}\n          |\"\"\".stripMargin.linesIterator.mkString)\n\n      val addRemoveSchema = new StructType()\n        .add(\"path\", StringType.STRING)\n        .add(\"partitionValues\", new MapType(StringType.STRING, StringType.STRING, false))\n        .add(\"size\", LongType.LONG)\n        .add(\"dataChange\", BooleanType.BOOLEAN)\n\n      val readSchema = new StructType()\n        .add(\"add\", addRemoveSchema)\n        .add(\"remove\", addRemoveSchema)\n\n      val batch = jsonHandler.parseJson(stringVector(input), readSchema, Optional.empty())\n      assert(batch.getSize == 2)\n\n      val filePath = tempDir + \"/1.json\"\n      def writeAndVerify(overwrite: Boolean): Unit = {\n        jsonHandler.writeJsonFileAtomically(filePath, batch.getRows, overwrite)\n\n        // read it back and verify the contents are correct\n        val source = scala.io.Source.fromFile(filePath)\n        val result =\n          try source.getLines().mkString(\",\")\n          finally source.close()\n\n        // remove the whitespaces from the input to compare\n        assert(input.map(_.replaceAll(\" \", \"\")).mkString(\",\") === result)\n      }\n\n      writeAndVerify(overwrite = false)\n\n      // Try to write as same file with overwrite as false and expect an error\n      intercept[FileAlreadyExistsException] {\n        jsonHandler.writeJsonFileAtomically(filePath, batch.getRows, false /* overwrite */ )\n      }\n\n      // Try to write as file with overwrite set to true\n      writeAndVerify(overwrite = true)\n    }\n  }\n\n  test(\"parse diverse type values in a map[string, string]\") {\n    val input =\n      \"\"\"\n        |{\n        |   \"inCommitTimestamp\":1740009523401,\n        |   \"timestamp\":1740009523401,\n        |   \"engineInfo\":\"myengine.com\",\n        |   \"operation\":\"WRITE\",\n        |   \"operationParameters\":\n        |     {\"mode\":\"Append\",\"statsOnLoad\":false,\"partitionBy\":\"[]\"},\n        |   \"isBlindAppend\":true,\n        |   \"txnId\":\"cb009f42-5da1-4e7e-b4fa-09de3332f52a\",\n        |   \"operationMetrics\": {\n        |       \"numFiles\":\"1\",\n        |       \"serializedAsNumber\":2,\n        |       \"serializedAsBoolean\":true\n        |   }\n        |}\n        |\"\"\".stripMargin\n\n    val output = jsonHandler.parseJson(\n      stringVector(Seq(input)),\n      CommitInfo.FULL_SCHEMA,\n      Optional.empty())\n    assert(output.getSize == 1)\n    val actResult = TestRow(output.getRows.next)\n    val expResult = TestRow(\n      1740009523401L,\n      1740009523401L,\n      \"myengine.com\",\n      \"WRITE\",\n      Map(\"mode\" -> \"Append\", \"statsOnLoad\" -> \"false\", \"partitionBy\" -> \"[]\"),\n      true,\n      \"cb009f42-5da1-4e7e-b4fa-09de3332f52a\",\n      Map(\"numFiles\" -> \"1\", \"serializedAsNumber\" -> \"2\", \"serializedAsBoolean\" -> \"true\"))\n\n    checkAnswer(Seq(actResult), Seq(expResult))\n  }\n\n  test(\"parse CommitInfo JSON with missing isBlindAppend field\") {\n    val input =\n      \"\"\"\n        |{\n        |   \"inCommitTimestamp\":1740009523401,\n        |   \"timestamp\":1740009523401,\n        |   \"engineInfo\":\"myengine.com\",\n        |   \"operation\":\"WRITE\",\n        |   \"operationParameters\":\n        |     {\"mode\":\"Append\",\"partitionBy\":\"[]\"},\n        |   \"txnId\":\"cb009f42-5da1-4e7e-b4fa-09de3332f52a\",\n        |   \"operationMetrics\": {\n        |       \"numFiles\":\"1\"\n        |   }\n        |}\n        |\"\"\".stripMargin\n\n    val output = jsonHandler.parseJson(\n      stringVector(Seq(input)),\n      CommitInfo.FULL_SCHEMA,\n      Optional.empty())\n    assert(output.getSize == 1)\n    val actResult = TestRow(output.getRows.next)\n    val expResult = TestRow(\n      1740009523401L,\n      1740009523401L,\n      \"myengine.com\",\n      \"WRITE\",\n      Map(\"mode\" -> \"Append\", \"partitionBy\" -> \"[]\"),\n      null, // isBlindAppend is missing from JSON, should be null\n      \"cb009f42-5da1-4e7e-b4fa-09de3332f52a\",\n      Map(\"numFiles\" -> \"1\"))\n\n    checkAnswer(Seq(actResult), Seq(expResult))\n  }\n\n  test(\"fromColumnVector handles null isBlindAppend from parsed JSON without NPE\") {\n    val input =\n      \"\"\"\n        |{\n        |   \"timestamp\":1740009523401,\n        |   \"engineInfo\":\"myengine.com\",\n        |   \"operation\":\"WRITE\",\n        |   \"operationParameters\":{},\n        |   \"txnId\":\"test-txn-id\",\n        |   \"operationMetrics\":{}\n        |}\n        |\"\"\".stripMargin\n\n    val readSchema = new StructType().add(\"commitInfo\", CommitInfo.FULL_SCHEMA)\n    val output = jsonHandler.parseJson(\n      stringVector(Seq(s\"\"\"{\"commitInfo\":${input.trim}}\"\"\")),\n      readSchema,\n      Optional.empty())\n    assert(output.getSize == 1)\n    val commitInfoVector = output.getColumnVector(0)\n    val commitInfo = CommitInfo.fromColumnVector(commitInfoVector, 0)\n\n    assert(commitInfo != null)\n    assert(commitInfo.getIsBlindAppend === Optional.empty())\n    assert(commitInfo.getInCommitTimestamp === Optional.empty())\n    assert(commitInfo.getTimestamp === 1740009523401L)\n    assert(commitInfo.getEngineInfo === Optional.of(\"myengine.com\"))\n    assert(commitInfo.getOperation === Optional.of(\"WRITE\"))\n    assert(commitInfo.getTxnId === Optional.of(\"test-txn-id\"))\n  }\n\n  test(\"fromColumnVector handles null engineInfo, operation, and txnId without NPE\") {\n    // Simulates a commit written by an external engine that omits these optional fields\n    val input =\n      \"\"\"\n        |{\n        |   \"timestamp\":1740009523401,\n        |   \"operationParameters\":{},\n        |   \"operationMetrics\":{}\n        |}\n        |\"\"\".stripMargin\n\n    val readSchema = new StructType().add(\"commitInfo\", CommitInfo.FULL_SCHEMA)\n    val output = jsonHandler.parseJson(\n      stringVector(Seq(s\"\"\"{\"commitInfo\":${input.trim}}\"\"\")),\n      readSchema,\n      Optional.empty())\n    assert(output.getSize == 1)\n    val commitInfoVector = output.getColumnVector(0)\n    val commitInfo = CommitInfo.fromColumnVector(commitInfoVector, 0)\n\n    assert(commitInfo != null)\n    assert(commitInfo.getEngineInfo === Optional.empty())\n    assert(commitInfo.getOperation === Optional.empty())\n    assert(commitInfo.getTxnId === Optional.empty())\n    assert(commitInfo.getIsBlindAppend === Optional.empty())\n    assert(commitInfo.getInCommitTimestamp === Optional.empty())\n    assert(commitInfo.getTimestamp === 1740009523401L)\n    assert(commitInfo.getOperationParameters.isEmpty)\n    assert(commitInfo.getOperationMetrics.isEmpty)\n  }\n\n  test(\"fromColumnVector with only timestamp field does not NPE\") {\n    // Minimal commit info - only the required timestamp field\n    val input =\n      \"\"\"\n        |{\n        |   \"timestamp\":1000\n        |}\n        |\"\"\".stripMargin\n\n    val readSchema = new StructType().add(\"commitInfo\", CommitInfo.FULL_SCHEMA)\n    val output = jsonHandler.parseJson(\n      stringVector(Seq(s\"\"\"{\"commitInfo\":${input.trim}}\"\"\")),\n      readSchema,\n      Optional.empty())\n    assert(output.getSize == 1)\n    val commitInfoVector = output.getColumnVector(0)\n    val commitInfo = CommitInfo.fromColumnVector(commitInfoVector, 0)\n\n    assert(commitInfo != null)\n    assert(commitInfo.getTimestamp === 1000L)\n    assert(commitInfo.getEngineInfo === Optional.empty())\n    assert(commitInfo.getOperation === Optional.empty())\n    assert(commitInfo.getTxnId === Optional.empty())\n    assert(commitInfo.getIsBlindAppend === Optional.empty())\n    assert(commitInfo.getInCommitTimestamp === Optional.empty())\n    assert(commitInfo.getOperationParameters.isEmpty)\n    assert(commitInfo.getOperationMetrics.isEmpty)\n  }\n\n  test(\"CommitInfo round-trips through toRow with nullable fields\") {\n    val commitInfo = new CommitInfo(\n      Optional.of(100L),\n      200L,\n      Optional.empty(), // engineInfo\n      Optional.empty(), // operation\n      Collections.emptyMap(),\n      Optional.empty(), // isBlindAppend\n      Optional.empty(), // txnId\n      Collections.emptyMap())\n\n    val row = commitInfo.toRow()\n    assert(row.isNullAt(CommitInfo.FULL_SCHEMA.indexOf(\"engineInfo\")))\n    assert(row.isNullAt(CommitInfo.FULL_SCHEMA.indexOf(\"operation\")))\n    assert(row.isNullAt(CommitInfo.FULL_SCHEMA.indexOf(\"txnId\")))\n    assert(row.isNullAt(CommitInfo.FULL_SCHEMA.indexOf(\"isBlindAppend\")))\n    assert(row.getLong(CommitInfo.FULL_SCHEMA.indexOf(\"inCommitTimestamp\")) === 100L)\n    assert(row.getLong(CommitInfo.FULL_SCHEMA.indexOf(\"timestamp\")) === 200L)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/engine/DefaultParquetHandlerSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.engine\n\nimport java.io.IOException\nimport java.nio.file.FileAlreadyExistsException\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.golden.GoldenTableUtils.goldenTableFile\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.internal.parquet.ParquetSuiteBase\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DefaultParquetHandlerSuite extends AnyFunSuite with ParquetSuiteBase {\n\n  val parquetHandler = new DefaultParquetHandler(\n    new HadoopFileIO(\n      new Configuration {\n        set(\"delta.kernel.default.parquet.reader.batch-size\", \"10\")\n      }))\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests for `writeParquetFileAtomically`. Test for `writeParquetFiles` are covered in\n  // `ParquetFileWriterSuite` as this API implementation by itself doesn't have any special logic.\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  test(\"atomic write of a single Parquet file\") {\n    withTempDir { tempDir =>\n      val inputLocation = goldenTableFile(\"parquet-all-types\").toString\n\n      val dataToWrite =\n        readParquetUsingKernelAsColumnarBatches(inputLocation, tableSchema(inputLocation))\n          .map(_.toFiltered)\n      assert(dataToWrite.size === 1)\n      assert(dataToWrite.head.getData.getSize === 200)\n\n      val filePath = tempDir + \"/1.parquet\"\n\n      def writeAndVerify(): Unit = {\n        parquetHandler.writeParquetFileAtomically(\n          filePath,\n          toCloseableIterator(dataToWrite.asJava.iterator()))\n\n        // Uses both Spark and Kernel to read and verify the content is same as the one written.\n        verifyContent(tempDir.getAbsolutePath, dataToWrite)\n      }\n\n      writeAndVerify()\n\n      // Try to write as same file and expect an error\n      val e = intercept[IOException] {\n        parquetHandler.writeParquetFileAtomically(\n          filePath,\n          toCloseableIterator(dataToWrite.asJava.iterator()))\n      }\n      assert(e.getCause.isInstanceOf[FileAlreadyExistsException])\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluatorSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions\n\nimport java.lang.{Boolean => BooleanJ, Double => DoubleJ, Float => FloatJ, Integer => IntegerJ, Long => LongJ}\nimport java.math.{BigDecimal => BigDecimalJ}\nimport java.sql.{Date, Timestamp}\nimport java.util\nimport java.util.Optional\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.defaults.internal.data.vector.{DefaultIntVector, DefaultStructVector}\nimport io.delta.kernel.defaults.utils.DefaultKernelTestUtils.getValueAsObject\nimport io.delta.kernel.expressions._\nimport io.delta.kernel.expressions.AlwaysFalse.ALWAYS_FALSE\nimport io.delta.kernel.expressions.AlwaysTrue.ALWAYS_TRUE\nimport io.delta.kernel.expressions.Literal._\nimport io.delta.kernel.internal.util.InternalUtils\nimport io.delta.kernel.types._\nimport io.delta.kernel.types.CollationIdentifier.SPARK_UTF8_BINARY\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass DefaultExpressionEvaluatorSuite extends AnyFunSuite with ExpressionSuiteBase {\n  test(\"evaluate expression: literal\") {\n    val testLiterals = Seq(\n      Literal.ofBoolean(true),\n      Literal.ofBoolean(false),\n      Literal.ofNull(BooleanType.BOOLEAN),\n      ofByte(24.toByte),\n      Literal.ofNull(ByteType.BYTE),\n      Literal.ofShort(876.toShort),\n      Literal.ofNull(ShortType.SHORT),\n      Literal.ofInt(2342342),\n      Literal.ofNull(IntegerType.INTEGER),\n      Literal.ofLong(234234223L),\n      Literal.ofNull(LongType.LONG),\n      Literal.ofFloat(23423.4223f),\n      Literal.ofNull(FloatType.FLOAT),\n      Literal.ofDouble(23423.422233d),\n      Literal.ofNull(DoubleType.DOUBLE),\n      Literal.ofString(\"string_val\"),\n      Literal.ofNull(StringType.STRING),\n      Literal.ofBinary(\"binary_val\".getBytes),\n      Literal.ofNull(BinaryType.BINARY),\n      Literal.ofDate(4234),\n      Literal.ofNull(DateType.DATE),\n      Literal.ofTimestamp(2342342342232L),\n      Literal.ofNull(TimestampType.TIMESTAMP),\n      Literal.ofTimestampNtz(2342342342L),\n      Literal.ofNull(TimestampNTZType.TIMESTAMP_NTZ))\n\n    val inputBatches: Seq[ColumnarBatch] = Seq[ColumnarBatch](\n      zeroColumnBatch(rowCount = 0),\n      zeroColumnBatch(rowCount = 25),\n      zeroColumnBatch(rowCount = 128))\n\n    for (literal <- testLiterals) {\n      val outputDataType = literal.getDataType\n      for (inputBatch <- inputBatches) {\n        val outputVector: ColumnVector =\n          evaluator(inputBatch.getSchema, literal, literal.getDataType)\n            .eval(inputBatch)\n\n        assert(inputBatch.getSize === outputVector.getSize)\n        assert(outputDataType === outputVector.getDataType)\n\n        for (rowId <- 0 until outputVector.getSize) {\n          if (literal.getValue == null) {\n            assert(\n              outputVector.isNullAt(rowId),\n              s\"expected a null at $rowId for $literal expression\")\n          } else {\n            assert(\n              literal.getValue === getValueAsObject(outputVector, rowId),\n              s\"invalid value at $rowId for $literal expression\")\n          }\n        }\n      }\n    }\n  }\n\n  PRIMITIVE_TYPES.foreach { dataType =>\n    test(s\"evaluate expression: column of type $dataType\") {\n      val batchSize = 78;\n      val batchSchema = new StructType().add(\"col1\", dataType)\n      val batch = new DefaultColumnarBatch(\n        batchSize,\n        batchSchema,\n        Array[ColumnVector](testColumnVector(batchSize, dataType)))\n\n      val outputVector = evaluator(batchSchema, new Column(\"col1\"), dataType)\n        .eval(batch)\n\n      assert(batchSize === outputVector.getSize)\n      assert(dataType === outputVector.getDataType)\n      Seq.range(0, outputVector.getSize).foreach { rowId =>\n        assert(\n          testIsNullValue(dataType, rowId) === outputVector.isNullAt(rowId),\n          s\"unexpected nullability at $rowId for $dataType type vector\")\n        if (!outputVector.isNullAt(rowId)) {\n          assert(\n            testColumnValue(dataType, rowId) === getValueAsObject(outputVector, rowId),\n            s\"unexpected value at $rowId for $dataType type vector\")\n        }\n      }\n    }\n  }\n\n  test(\"evaluate expression: nested column reference\") {\n    val col3Type = IntegerType.INTEGER\n    val col2Type = new StructType().add(\"col3\", col3Type)\n    val col1Type = new StructType().add(\"col2\", col2Type)\n    val batchSchema = new StructType().add(\"col1\", col1Type)\n\n    val numRows = 5\n    val col3Nullability = Seq(false, true, false, true, false).toArray\n    val col3Values = Seq(27, 24, 29, 100, 125).toArray\n    val col3Vector =\n      new DefaultIntVector(col3Type, numRows, Optional.of(col3Nullability), col3Values)\n\n    val col2Nullability = Seq(false, true, true, true, false).toArray\n    val col2Vector =\n      new DefaultStructVector(numRows, col2Type, Optional.of(col2Nullability), Array(col3Vector))\n\n    val col1Nullability = Seq(false, false, false, true, false).toArray\n    val col1Vector =\n      new DefaultStructVector(numRows, col1Type, Optional.of(col1Nullability), Array(col2Vector))\n\n    val batch = new DefaultColumnarBatch(numRows, batchSchema, Array(col1Vector))\n\n    def assertTypeAndNullability(\n        actVector: ColumnVector,\n        expType: DataType,\n        expNullability: Array[Boolean]): Unit = {\n      assert(actVector.getDataType === expType)\n      assert(actVector.getSize === numRows)\n      Seq.range(0, numRows).foreach { rowId =>\n        assert(actVector.isNullAt(rowId) === expNullability(rowId))\n      }\n    }\n\n    val col3Ref = new Column(Array(\"col1\", \"col2\", \"col3\"))\n    val col3RefResult = evaluator(batchSchema, col3Ref, col3Type).eval(batch)\n    assertTypeAndNullability(col3RefResult, col3Type, col3Nullability);\n    Seq.range(0, numRows).foreach { rowId =>\n      assert(col3RefResult.getInt(rowId) === col3Values(rowId))\n    }\n\n    val col2Ref = new Column(Array(\"col1\", \"col2\"))\n    val col2RefResult = evaluator(batchSchema, col2Ref, col2Type).eval(batch)\n    assertTypeAndNullability(col2RefResult, col2Type, col2Nullability)\n\n    val col1Ref = new Column(Array(\"col1\"))\n    val col1RefResult = evaluator(batchSchema, col1Ref, col1Type).eval(batch)\n    assertTypeAndNullability(col1RefResult, col1Type, col1Nullability)\n\n    // try to reference non-existent nested column\n    val colNotValid = new Column(Array(\"col1\", \"colX`X\"))\n    val ex = intercept[IllegalArgumentException] {\n      evaluator(batchSchema, colNotValid, col1Type).eval(batch)\n    }\n    assert(ex.getMessage.contains(\"column(`col1`.`colX``X`) doesn't exist in input data schema\"))\n  }\n\n  test(\"evaluate expression: always true, always false\") {\n    Seq(ALWAYS_TRUE, ALWAYS_FALSE).foreach { expr =>\n      val batch = zeroColumnBatch(rowCount = 87)\n      val outputVector = evaluator(batch.getSchema, expr, BooleanType.BOOLEAN).eval(batch)\n      assert(outputVector.getSize === 87)\n      assert(outputVector.getDataType === BooleanType.BOOLEAN)\n      Seq.range(0, 87).foreach { rowId =>\n        assert(!outputVector.isNullAt(rowId))\n        assert(outputVector.getBoolean(rowId) == (expr == ALWAYS_TRUE))\n      }\n    }\n  }\n\n  test(\"evaluate expression: and, or\") {\n    val leftColumn = booleanVector(\n      Seq[BooleanJ](true, true, false, false, null, true, null, false, null))\n    val rightColumn = booleanVector(\n      Seq[BooleanJ](true, false, false, true, true, null, false, null, null))\n    val expAndOutputVector = booleanVector(\n      Seq[BooleanJ](true, false, false, false, null, null, false, false, null))\n    val expOrOutputVector = booleanVector(\n      Seq[BooleanJ](true, true, false, true, true, true, null, null, null))\n\n    val schema = new StructType()\n      .add(\"left\", BooleanType.BOOLEAN)\n      .add(\"right\", BooleanType.BOOLEAN)\n    val batch = new DefaultColumnarBatch(leftColumn.getSize, schema, Array(leftColumn, rightColumn))\n\n    val left = comparator(\"=\", new Column(\"left\"), Literal.ofBoolean(true))\n    val right = comparator(\"=\", new Column(\"right\"), Literal.ofBoolean(true))\n\n    // And\n    val andExpression = and(left, right)\n    val actAndOutputVector = evaluator(schema, andExpression, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actAndOutputVector, expAndOutputVector)\n\n    // Or\n    val orExpression = or(left, right)\n    val actOrOutputVector = evaluator(schema, orExpression, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actOrOutputVector, expOrOutputVector)\n  }\n\n  test(\"evaluate expression: not\") {\n    val childColumn = booleanVector(Seq[BooleanJ](true, false, null))\n\n    val schema = new StructType().add(\"child\", BooleanType.BOOLEAN)\n    val batch = new DefaultColumnarBatch(childColumn.getSize, schema, Array(childColumn))\n\n    val notExpression = new Predicate(\n      \"NOT\",\n      comparator(\"=\", new Column(\"child\"), Literal.ofBoolean(true)))\n    val expOutputVector = booleanVector(Seq[BooleanJ](false, true, null))\n    val actOutputVector = evaluator(schema, notExpression, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actOutputVector, expOutputVector)\n  }\n\n  test(\"evaluate expression: is not null\") {\n    val childColumn = booleanVector(Seq[BooleanJ](true, false, null))\n\n    val schema = new StructType().add(\"child\", BooleanType.BOOLEAN)\n    val batch = new DefaultColumnarBatch(childColumn.getSize, schema, Array(childColumn))\n\n    val isNotNullExpression = new Predicate(\"IS_NOT_NULL\", new Column(\"child\"))\n    val expOutputVector = booleanVector(Seq[BooleanJ](true, true, false))\n    val actOutputVector = evaluator(schema, isNotNullExpression, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actOutputVector, expOutputVector)\n  }\n\n  test(\"evaluate expression: is null\") {\n    val childColumn = booleanVector(Seq[BooleanJ](true, false, null))\n\n    val schema = new StructType().add(\"child\", BooleanType.BOOLEAN)\n    val batch = new DefaultColumnarBatch(childColumn.getSize, schema, Array(childColumn))\n\n    val isNullExpression = new Predicate(\"IS_NULL\", new Column(\"child\"))\n    val expOutputVector = booleanVector(Seq[BooleanJ](false, false, true))\n    val actOutputVector = evaluator(schema, isNullExpression, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actOutputVector, expOutputVector)\n  }\n\n  test(\"evaluate expression: coalesce (boolean columns)\") {\n    val col1 = booleanVector(Seq[BooleanJ](true, null, null, null))\n    val col2 = booleanVector(Seq[BooleanJ](false, false, null, null))\n    val col3 = booleanVector(Seq[BooleanJ](true, true, true, null))\n\n    val schema = new StructType()\n      .add(\"col1\", BooleanType.BOOLEAN)\n      .add(\"col2\", BooleanType.BOOLEAN)\n      .add(\"col3\", BooleanType.BOOLEAN)\n\n    val batch = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2, col3))\n\n    val coalesceEpxr1 = new ScalarExpression(\n      \"COALESCE\",\n      util.Arrays.asList(new Column(\"col1\")))\n    val expOutputVector1 = booleanVector(Seq[BooleanJ](true, null, null, null))\n    val actOutputVector1 = evaluator(schema, coalesceEpxr1, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actOutputVector1, expOutputVector1)\n\n    val coalesceEpxr3 = new ScalarExpression(\n      \"COALESCE\",\n      util.Arrays.asList(\n        new Column(\"col1\"),\n        new Column(\"col2\"),\n        new Column(\"col3\")))\n    val expOutputVector3 = booleanVector(Seq[BooleanJ](true, false, true, null))\n    val actOutputVector3 = evaluator(schema, coalesceEpxr3, BooleanType.BOOLEAN).eval(batch)\n    checkBooleanVectors(actOutputVector3, expOutputVector3)\n  }\n\n  test(\"evaluate expression: coalesce (long columns)\") {\n    val longCol1 = longVector(Seq(1L, null, null, 4L))\n    val longCol2 = longVector(Seq(null, 2L, null, 5L))\n    val longCol3 = longVector(Seq(100L, null, 3L, null))\n    val longSchema = new StructType()\n      .add(\"longCol1\", LongType.LONG)\n      .add(\"longCol2\", LongType.LONG)\n      .add(\"longCol3\", LongType.LONG)\n    val longBatch =\n      new DefaultColumnarBatch(longCol1.getSize, longSchema, Array(longCol1, longCol2, longCol3))\n    val longCoalesceExpr = new ScalarExpression(\n      \"COALESCE\",\n      util.Arrays.asList(new Column(\"longCol1\"), new Column(\"longCol2\"), new Column(\"longCol3\")))\n    val expLongOutput = longVector(Seq(1L, 2L, 3L, 4L))\n    val actLongOutput = evaluator(longSchema, longCoalesceExpr, LongType.LONG).eval(longBatch)\n    checkLongVectors(actLongOutput, expLongOutput)\n  }\n\n  test(\"evaluate expression: coalesce (string columns)\") {\n    val strCol1 = stringVector(Seq(\"a\", null, null, \"d\"))\n    val strCol2 = stringVector(Seq(\"null\", \"b\", null, null))\n    val strCol3 = stringVector(Seq(null, null, \"c\", \"abc\"))\n    val strSchema = new StructType()\n      .add(\"strCol1\", StringType.STRING)\n      .add(\"strCol2\", StringType.STRING)\n      .add(\"strCol3\", StringType.STRING)\n    val strBatch =\n      new DefaultColumnarBatch(strCol1.getSize, strSchema, Array(strCol1, strCol2, strCol3))\n    val strCoalesceExpr = new ScalarExpression(\n      \"COALESCE\",\n      util.Arrays.asList(new Column(\"strCol1\"), new Column(\"strCol2\"), new Column(\"strCol3\")))\n    val expStrOutput = stringVector(Seq(\"a\", \"b\", \"c\", \"d\"))\n    val actStrOutput = evaluator(strSchema, strCoalesceExpr, StringType.STRING).eval(strBatch)\n    checkStringVectors(actStrOutput, expStrOutput)\n  }\n\n  test(\"evaluate expression: coalesce (timestamp columns)\") {\n    val tsCol1 = timestampVector(Seq(1000L, null, null, 4000L))\n    val tsCol2 = timestampVector(Seq(null, 2000L, null, 5000L))\n    val tsCol3 = timestampVector(Seq(10000L, null, 3000L, null))\n    val tsSchema = new StructType()\n      .add(\"tsCol1\", TimestampType.TIMESTAMP)\n      .add(\"tsCol2\", TimestampType.TIMESTAMP)\n      .add(\"tsCol3\", TimestampType.TIMESTAMP)\n    val tsBatch =\n      new DefaultColumnarBatch(tsCol1.getSize, tsSchema, Array(tsCol1, tsCol2, tsCol3))\n    val tsCoalesceExpr = new ScalarExpression(\n      \"COALESCE\",\n      util.Arrays.asList(new Column(\"tsCol1\"), new Column(\"tsCol2\"), new Column(\"tsCol3\")))\n    val expTsOutput = timestampVector(Seq(1000L, 2000L, 3000L, 4000L))\n    val actTsOutput = evaluator(tsSchema, tsCoalesceExpr, TimestampType.TIMESTAMP).eval(tsBatch)\n    checkLongVectors(actTsOutput, expTsOutput)\n  }\n\n  test(\"evaluate expression: coalesce (unequal column types)\") {\n    def checkUnsupportedTypes(\n        col1Type: DataType,\n        col2Type: DataType,\n        messageContains: String): Unit = {\n      val schema = new StructType()\n        .add(\"col1\", col1Type)\n        .add(\"col2\", col2Type)\n      val batch = new DefaultColumnarBatch(\n        5,\n        schema,\n        Array(testColumnVector(5, col1Type), testColumnVector(5, col2Type)))\n      val e = intercept[UnsupportedOperationException] {\n        evaluator(\n          schema,\n          new ScalarExpression(\n            \"COALESCE\",\n            util.Arrays.asList(new Column(\"col1\"), new Column(\"col2\"))),\n          col1Type).eval(batch)\n      }\n      assert(e.getMessage.contains(messageContains))\n    }\n    // TODO support least-common-type resolution\n    checkUnsupportedTypes(\n      LongType.LONG,\n      IntegerType.INTEGER,\n      \"Coalesce is only supported for arguments of the same type\")\n  }\n\n  test(\"evaluate expression: ADD (column and literal)\") {\n    val col1 = longVector(Seq(1, 2, 3, 4, null))\n    val schema = new StructType().add(\"col1\", LongType.LONG)\n    val batch = new DefaultColumnarBatch(col1.getSize, schema, Array(col1))\n\n    // ADD with literal\n    val addExpr = new ScalarExpression(\n      \"ADD\",\n      util.Arrays.asList(new Column(\"col1\"), Literal.ofLong(10L)))\n    val expOutputVector = longVector(Seq(11, 12, 13, 14, null))\n    val actOutputVector = evaluator(schema, addExpr, LongType.LONG).eval(batch)\n    checkLongVectors(actOutputVector, expOutputVector)\n  }\n\n  test(\"evaluate expression: ADD (column and column)\") {\n    val col1 = longVector(Seq(1, 2, null, 4, null))\n    val col2 = longVector(Seq(null, 20, 30, 40, null))\n    val schema = new StructType()\n      .add(\"col1\", LongType.LONG)\n      .add(\"col2\", LongType.LONG)\n    val batch = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2))\n\n    // ADD with two columns\n    val addExpr = new ScalarExpression(\n      \"ADD\",\n      util.Arrays.asList(new Column(\"col1\"), new Column(\"col2\")))\n    val expOutputVector = longVector(Seq(null, 22, null, 44, null))\n    val actOutputVector = evaluator(schema, addExpr, LongType.LONG).eval(batch)\n    checkLongVectors(actOutputVector, expOutputVector)\n  }\n\n  test(\"evaluate expression: ADD (more than two operands)\") {\n    val col1 = longVector(Seq(1, 2, null, 4, null))\n    val col2 = longVector(Seq(null, 20, 30, 40, null))\n    val col3 = longVector(Seq(5, null, 15, null, 25))\n    val schema = new StructType()\n      .add(\"col1\", LongType.LONG)\n      .add(\"col2\", LongType.LONG)\n      .add(\"col3\", LongType.LONG)\n    val batch = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2, col3))\n\n    // ADD with three columns\n    val addExpr = new ScalarExpression(\n      \"ADD\",\n      util.Arrays.asList(new Column(\"col1\"), new Column(\"col2\"), new Column(\"col3\")))\n    val e =\n      intercept[UnsupportedOperationException] {\n        evaluator(schema, addExpr, LongType.LONG).eval(batch)\n      }\n    assert(e.getMessage.contains(\"ADD requires exactly two arguments: left and right operands\"))\n  }\n\n  test(\"evaluate expression: ADD (unequal operand types\") {\n    val col1 = longVector(Seq(1, 2, null, 4, null))\n    val col2 = floatVector(Seq(1.0f, 2.0f, 3.0f, 4.0f, null))\n    val schema = new StructType()\n      .add(\"col1\", LongType.LONG)\n      .add(\"col2\", FloatType.FLOAT)\n    val batch = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2))\n\n    // ADD with two columns of different types\n    val addExpr = new ScalarExpression(\n      \"ADD\",\n      util.Arrays.asList(new Column(\"col1\"), new Column(\"col2\")))\n    val e =\n      intercept[UnsupportedOperationException] {\n        evaluator(schema, addExpr, LongType.LONG).eval(batch)\n      }\n    assert(e.getMessage.contains(\"ADD is only supported for arguments of the same type\"))\n  }\n\n  test(\"evaluate expression: ADD (unsupported types)\") {\n    val col1 = stringVector(Seq(\"a\", \"b\", null, \"d\", null))\n    val col2 = stringVector(Seq(\"x\", \"y\", \"z\", \"w\", null))\n    val schema = new StructType()\n      .add(\"col1\", StringType.STRING)\n      .add(\"col2\", StringType.STRING)\n    val batch = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2))\n\n    // ADD with two columns of unsupported types\n    val addExpr = new ScalarExpression(\n      \"ADD\",\n      util.Arrays.asList(new Column(\"col1\"), new Column(\"col2\")))\n    val e =\n      intercept[UnsupportedOperationException] {\n        evaluator(schema, addExpr, StringType.STRING).eval(batch)\n      }\n    assert(e.getMessage.contains(\n      \"ADD is only supported for numeric types: byte, short, int, long, float, double\"))\n  }\n\n  test(\"evaluate expression: TIMEADD with TIMESTAMP columns\") {\n    val timestampColumn = timestampVector(Seq(\n      1577836800000000L, // 2020-01-01 00:00:00.000\n      1577836800123456L, // 2020-01-01 00:00:00.123456\n      -1 // Representing null\n    ))\n\n    val durationColumn = longVector(Seq(\n      1000, // 1 second in milliseconds\n      100, // 0.1 second in milliseconds\n      -1))\n\n    val schema = new StructType()\n      .add(\"timestamp\", TimestampType.TIMESTAMP)\n      .add(\"duration\", LongType.LONG)\n\n    val batch = new DefaultColumnarBatch(\n      timestampColumn.getSize,\n      schema,\n      Array(timestampColumn, durationColumn))\n\n    // TimeAdd expression adds milliseconds to timestamps\n    val timeAddExpr = new ScalarExpression(\n      \"TIMEADD\",\n      util.Arrays.asList(new Column(\"timestamp\"), new Column(\"duration\")))\n\n    val expOutputVector = timestampVector(Seq(\n      1577836801000000L, // 2020-01-01 00:00:01.000\n      1577836800123456L + 100000, // 2020-01-01 00:00:00.123556\n      -1 // Null should propagate\n    ))\n    val actOutputVector = evaluator(schema, timeAddExpr, TimestampType.TIMESTAMP).eval(batch)\n\n    checkTimestampVectors(actOutputVector, expOutputVector)\n  }\n\n  def checkUnsupportedTimeAddTypes(\n      col1Type: DataType,\n      col2Type: DataType): Unit = {\n    val schema = new StructType()\n      .add(\"timestamp\", col1Type)\n      .add(\"duration\", col2Type)\n    val batch = new DefaultColumnarBatch(\n      5,\n      schema,\n      Array(testColumnVector(5, col1Type), testColumnVector(5, col2Type)))\n\n    val timeAddExpr = new ScalarExpression(\n      \"TIMEADD\",\n      util.Arrays.asList(new Column(\"timestamp\"), new Column(\"duration\")))\n\n    val e = intercept[IllegalArgumentException] {\n      val evaluator = new DefaultExpressionEvaluator(schema, timeAddExpr, col1Type)\n      evaluator.eval(batch)\n    }\n    assert(e.getMessage.contains(\"TIMEADD requires a timestamp and a Long\"))\n  }\n\n  // Test to ensure TIMEADD requires the first argument to be a TimestampType\n  // and the second to be a LongType\n  test(\"TIMEADD with unsupported types\") {\n    // Check invalid timestamp column type\n    checkUnsupportedTimeAddTypes(\n      IntegerType.INTEGER,\n      IntegerType.INTEGER)\n\n    // Check invalid duration column type\n    checkUnsupportedTimeAddTypes(\n      TimestampType.TIMESTAMP,\n      StringType.STRING)\n\n    // Check valid type but with unsupported operations\n    checkUnsupportedTimeAddTypes(\n      TimestampType.TIMESTAMP,\n      FloatType.FLOAT)\n  }\n\n  test(\"evaluate expression: like\") {\n    val col1 = stringVector(Seq[String](\n      null,\n      \"one\",\n      \"two\",\n      \"three\",\n      \"four\",\n      null,\n      null,\n      \"seven\",\n      \"eight\"))\n    val col2 = stringVector(Seq[String](\n      null,\n      \"one\",\n      \"Two\",\n      \"thr%\",\n      \"four%\",\n      \"f\",\n      null,\n      null,\n      \"%ght\"))\n    val schema = new StructType()\n      .add(\"col1\", StringType.STRING)\n      .add(\"col2\", StringType.STRING)\n    val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2))\n\n    def checkLike(\n        input: DefaultColumnarBatch,\n        likeExpression: Predicate,\n        expOutputSeq: Seq[BooleanJ]): Unit = {\n      val actOutputVector =\n        new DefaultExpressionEvaluator(\n          schema,\n          likeExpression,\n          BooleanType.BOOLEAN).eval(input)\n      val expOutputVector = booleanVector(expOutputSeq);\n      checkBooleanVectors(actOutputVector, expOutputVector)\n    }\n\n    // check column expressions on both sides\n    checkLike(\n      input,\n      like(new Column(\"col1\"), new Column(\"col2\")),\n      Seq[BooleanJ](null, true, false, true, true, null, null, null, true))\n\n    // check column expression against literal\n    checkLike(\n      input,\n      like(new Column(\"col1\"), Literal.ofString(\"t%\")),\n      Seq[BooleanJ](null, false, true, true, false, null, null, false, false))\n\n    // ends with checks\n    checkLike(\n      input,\n      like(new Column(\"col1\"), Literal.ofString(\"%t\")),\n      Seq[BooleanJ](null, false, false, false, false, null, null, false, true))\n\n    // contains checks\n    checkLike(\n      input,\n      like(new Column(\"col1\"), Literal.ofString(\"%t%\")),\n      Seq[BooleanJ](null, false, true, true, false, null, null, false, true))\n\n    val dummyInput = new DefaultColumnarBatch(\n      1,\n      new StructType().add(\"dummy\", StringType.STRING),\n      Array(stringVector(Seq[String](\"\"))))\n\n    def checkLikeLiteral(\n        left: String,\n        right: String,\n        escape: Character = null,\n        expOutput: BooleanJ): Unit = {\n      val expression = like(Literal.ofString(left), Literal.ofString(right), Option(escape))\n      checkLike(dummyInput, expression, Seq[BooleanJ](expOutput))\n    }\n\n    // null/empty\n    checkLikeLiteral(null, \"a\", null, null)\n    checkLikeLiteral(\"a\", null, null, null)\n    checkLikeLiteral(null, null, null, null)\n    checkLikeLiteral(\"\", \"\", null, true)\n    checkLikeLiteral(\"a\", \"\", null, false)\n    checkLikeLiteral(\"\", \"a\", null, false)\n\n    Seq('!', '@', '#').foreach {\n      escape =>\n        {\n          // simple patterns\n          checkLikeLiteral(\"abc\", \"abc\", escape, true)\n          checkLikeLiteral(\"a_%b\", s\"a${escape}__b\", escape, true)\n          checkLikeLiteral(\"abbc\", \"a_%c\", escape, true)\n          checkLikeLiteral(\"abbc\", s\"a${escape}__c\", escape, false)\n          checkLikeLiteral(\"abbc\", s\"a%${escape}%c\", escape, false)\n          checkLikeLiteral(\"a_%b\", s\"a%${escape}%b\", escape, true)\n          checkLikeLiteral(\"abbc\", \"a%\", escape, true)\n          checkLikeLiteral(\"abbc\", \"**\", escape, false)\n          checkLikeLiteral(\"abc\", \"a%\", escape, true)\n          checkLikeLiteral(\"abc\", \"b%\", escape, false)\n          checkLikeLiteral(\"abc\", \"bc%\", escape, false)\n          checkLikeLiteral(\"a\\nb\", \"a_b\", escape, true)\n          checkLikeLiteral(\"ab\", \"a%b\", escape, true)\n          checkLikeLiteral(\"a\\nb\", \"a%b\", escape, true)\n          checkLikeLiteral(\"a\\nb\", \"ab\", escape, false)\n          checkLikeLiteral(\"a\\nb\", \"a\\nb\", escape, true)\n          checkLikeLiteral(\"a\\n\\nb\", \"a\\nb\", escape, false)\n          checkLikeLiteral(\"a\\n\\nb\", \"a\\n_b\", escape, true)\n\n          // case\n          checkLikeLiteral(\"A\", \"a%\", escape, false)\n          checkLikeLiteral(\"a\", \"a%\", escape, true)\n          checkLikeLiteral(\"a\", \"A%\", escape, false)\n          checkLikeLiteral(s\"aAa\", s\"aA_\", escape, true)\n\n          // regex\n          checkLikeLiteral(\"a([a-b]{2,4})a\", \"_([a-b]{2,4})%\", null, true)\n          checkLikeLiteral(\"a([a-b]{2,4})a\", \"_([a-c]{2,6})_\", null, false)\n\n          // %/_\n          checkLikeLiteral(\"a%a\", s\"%${escape}%%\", escape, true)\n          checkLikeLiteral(\"a%\", s\"%${escape}%%\", escape, true)\n          checkLikeLiteral(\"a%a\", s\"_${escape}%_\", escape, true)\n          checkLikeLiteral(\"a_a\", s\"%${escape}_%\", escape, true)\n          checkLikeLiteral(\"a_\", s\"%${escape}_%\", escape, true)\n          checkLikeLiteral(\"a_a\", s\"_${escape}__\", escape, true)\n\n          // double-escaping\n          checkLikeLiteral(\n            s\"$escape$escape$escape$escape\",\n            s\"%${escape}${escape}%\",\n            escape,\n            true)\n          checkLikeLiteral(\"%%\", \"%%\", escape, true)\n          checkLikeLiteral(s\"${escape}__\", s\"${escape}${escape}${escape}__\", escape, true)\n          checkLikeLiteral(s\"${escape}__\", s\"%${escape}${escape}%${escape}%\", escape, false)\n          checkLikeLiteral(s\"_${escape}${escape}${escape}%\", s\"%${escape}${escape}\", escape, false)\n        }\n    }\n\n    // check '_' for escape char\n    checkLikeLiteral(\"abc\", \"abc\", '_', true)\n    checkLikeLiteral(\"a_%b\", s\"a__%%b\", '_', true)\n    checkLikeLiteral(\"abbc\", \"a__c\", '_', false)\n    checkLikeLiteral(\"abbc\", \"a%%c\", '_', true)\n    checkLikeLiteral(\"abbc\", s\"a___%c\", '_', false)\n    checkLikeLiteral(\"abbc\", s\"a%_%c\", '_', false)\n\n    // check '%' for escape char\n    checkLikeLiteral(\"abc\", \"abc\", '%', true)\n    checkLikeLiteral(\"a_%b\", s\"a__%%b\", '%', false)\n    checkLikeLiteral(\"a_%b\", s\"a_%%b\", '%', true)\n    checkLikeLiteral(\"abbc\", \"a__c\", '%', true)\n    checkLikeLiteral(\"abbc\", \"a%%c\", '%', false)\n    checkLikeLiteral(\"abbc\", s\"a%__c\", '%', false)\n    checkLikeLiteral(\"abbc\", s\"a%_%_c\", '%', false)\n\n    def checkUnsupportedTypes(\n        col1Type: DataType,\n        col2Type: DataType): Unit = {\n      val schema = new StructType()\n        .add(\"col1\", col1Type)\n        .add(\"col2\", col2Type)\n      val expr = like(new Column(\"col1\"), new Column(\"col2\"), Option(null))\n      val input = new DefaultColumnarBatch(\n        5,\n        schema,\n        Array(testColumnVector(5, col1Type), testColumnVector(5, col2Type)))\n\n      val e = intercept[UnsupportedOperationException] {\n        new DefaultExpressionEvaluator(\n          schema,\n          expr,\n          BooleanType.BOOLEAN).eval(input)\n      }\n      assert(e.getMessage.contains(\"LIKE is only supported for string type expressions\"))\n    }\n    checkUnsupportedTypes(BooleanType.BOOLEAN, BooleanType.BOOLEAN)\n    checkUnsupportedTypes(LongType.LONG, LongType.LONG)\n    checkUnsupportedTypes(IntegerType.INTEGER, IntegerType.INTEGER)\n    checkUnsupportedTypes(StringType.STRING, BooleanType.BOOLEAN)\n    checkUnsupportedTypes(StringType.STRING, IntegerType.INTEGER)\n    checkUnsupportedTypes(StringType.STRING, LongType.LONG)\n    checkUnsupportedTypes(BooleanType.BOOLEAN, BooleanType.BOOLEAN)\n\n    // input count checks\n    val inputCountCheckUserMessage =\n      \"Invalid number of inputs to LIKE expression. Example usage:\"\n    val inputCountError1 = intercept[UnsupportedOperationException] {\n      val expression = like(List(Literal.ofString(\"a\")))\n      checkLike(dummyInput, expression, Seq[BooleanJ](null))\n    }\n    assert(inputCountError1.getMessage.contains(inputCountCheckUserMessage))\n\n    val inputCountError2 = intercept[UnsupportedOperationException] {\n      val expression = like(List(\n        Literal.ofString(\"a\"),\n        Literal.ofString(\"b\"),\n        Literal.ofString(\"c\"),\n        Literal.ofString(\"d\")))\n      checkLike(dummyInput, expression, Seq[BooleanJ](null))\n    }\n    assert(inputCountError2.getMessage.contains(inputCountCheckUserMessage))\n\n    // additional escape token checks\n    val escapeCharError1 = intercept[UnsupportedOperationException] {\n      val expression =\n        like(List(Literal.ofString(\"a\"), Literal.ofString(\"b\"), Literal.ofString(\"~~\")))\n      checkLike(dummyInput, expression, Seq[BooleanJ](null))\n    }\n    assert(escapeCharError1.getMessage.contains(\n      \"LIKE expects escape token to be a single character\"))\n\n    val escapeCharError2 = intercept[UnsupportedOperationException] {\n      val expression = like(List(Literal.ofString(\"a\"), Literal.ofString(\"b\"), Literal.ofInt(1)))\n      checkLike(dummyInput, expression, Seq[BooleanJ](null))\n    }\n    assert(escapeCharError2.getMessage.contains(\n      \"LIKE expects escape token expression to be a literal of String type\"))\n\n    // empty input checks\n    val emptyInput = new DefaultColumnarBatch(\n      0,\n      new StructType().add(\"dummy\", StringType.STRING),\n      Array(stringVector(Seq[String](\"\"))))\n    checkLike(\n      emptyInput,\n      like(Literal.ofString(\"abc\"), Literal.ofString(\"abc\"), Some('_')),\n      Seq[BooleanJ]())\n\n    // invalid pattern check\n    val invalidPatternError = intercept[IllegalArgumentException] {\n      checkLikeLiteral(\"abbc\", \"a%%%c\", '%', false)\n    }\n    assert(invalidPatternError.getMessage.contains(\n      \"LIKE expression has invalid escape sequence\"))\n  }\n\n  private val SPARK_UTF8_LCASE = CollationIdentifier.fromString(\"SPARK.UTF8_LCASE\")\n  test(\"evaluate expression: starts with\") {\n    Seq(\n      // collation\n      None,\n      Some(SPARK_UTF8_BINARY)).foreach {\n      collationIdentifier =>\n        val col1 = stringVector(Seq[String](\"one\", \"two\", \"t%hree\", \"four\", null, null, \"%\"))\n        val col2 = stringVector(Seq[String](\"o\", \"t\", \"T\", \"4\", \"f\", null, null))\n        val schema = new StructType()\n          .add(\"col1\", StringType.STRING)\n          .add(\"col2\", new StringType(SPARK_UTF8_LCASE))\n        val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2))\n\n        val startsWithExpressionLiteral =\n          startsWith(new Column(\"col1\"), Literal.ofString(\"t%\"), collationIdentifier)\n        val expOutputVectorLiteral =\n          booleanVector(Seq[BooleanJ](false, false, true, false, null, null, false))\n        checkBooleanVectors(\n          new DefaultExpressionEvaluator(\n            schema,\n            startsWithExpressionLiteral,\n            BooleanType.BOOLEAN).eval(input),\n          expOutputVectorLiteral)\n\n        val startsWithExpressionNullLiteral =\n          startsWith(new Column(\"col1\"), Literal.ofString(null), collationIdentifier)\n        val allNullVector =\n          booleanVector(Seq[BooleanJ](null, null, null, null, null, null, null))\n        checkBooleanVectors(\n          new DefaultExpressionEvaluator(\n            schema,\n            startsWithExpressionNullLiteral,\n            BooleanType.BOOLEAN).eval(input),\n          allNullVector)\n\n        // Two literal expressions on both sides\n        val startsWithExpressionAlwaysTrue =\n          startsWith(Literal.ofString(\"ABC\"), Literal.ofString(\"A\"), collationIdentifier)\n        val allTrueVector = booleanVector(Seq[BooleanJ](true, true, true, true, true, true, true))\n        checkBooleanVectors(\n          new DefaultExpressionEvaluator(\n            schema,\n            startsWithExpressionAlwaysTrue,\n            BooleanType.BOOLEAN).eval(input),\n          allTrueVector)\n\n        val startsWithExpressionAlwaysFalse =\n          startsWith(Literal.ofString(\"ABC\"), Literal.ofString(\"_B%\"), collationIdentifier)\n        val allFalseVector =\n          booleanVector(Seq[BooleanJ](false, false, false, false, false, false, false))\n        checkBooleanVectors(\n          new DefaultExpressionEvaluator(\n            schema,\n            startsWithExpressionAlwaysFalse,\n            BooleanType.BOOLEAN).eval(input),\n          allFalseVector)\n\n        // scalastyle:off nonascii\n        val colUnicode = stringVector(Seq[String](\"中文\", \"中\", \"文\"))\n        val schemaUnicode = new StructType().add(\"col\", StringType.STRING)\n        val inputUnicode =\n          new DefaultColumnarBatch(colUnicode.getSize, schemaUnicode, Array(colUnicode))\n        val startsWithExpressionUnicode =\n          startsWith(new Column(\"col\"), Literal.ofString(\"中\"), collationIdentifier)\n        val expOutputVectorLiteralUnicode = booleanVector(Seq[BooleanJ](true, true, false))\n        checkBooleanVectors(\n          new DefaultExpressionEvaluator(\n            schemaUnicode,\n            startsWithExpressionUnicode,\n            BooleanType.BOOLEAN).eval(inputUnicode),\n          expOutputVectorLiteralUnicode)\n\n        // scalastyle:off nonascii\n        val colSurrogatePair = stringVector(Seq[String](\"💕😉💕\", \"😉💕\", \"💕\"))\n        val schemaSurrogatePair = new StructType().add(\"col\", StringType.STRING)\n        val inputSurrogatePair =\n          new DefaultColumnarBatch(colSurrogatePair.getSize, schemaUnicode, Array(colSurrogatePair))\n        val startsWithExpressionSurrogatePair =\n          startsWith(new Column(\"col\"), Literal.ofString(\"💕\"), collationIdentifier)\n        val expOutputVectorLiteralSurrogatePair = booleanVector(Seq[BooleanJ](true, false, true))\n        checkBooleanVectors(\n          new DefaultExpressionEvaluator(\n            schemaSurrogatePair,\n            startsWithExpressionSurrogatePair,\n            BooleanType.BOOLEAN).eval(inputSurrogatePair),\n          expOutputVectorLiteralSurrogatePair)\n\n        val startsWithExpressionExpression =\n          startsWith(new Column(\"col1\"), new Column(\"col2\"), collationIdentifier)\n        val e = intercept[UnsupportedOperationException] {\n          new DefaultExpressionEvaluator(\n            schema,\n            startsWithExpressionExpression,\n            BooleanType.BOOLEAN).eval(input)\n        }\n        assert(e.getMessage.contains(\"'STARTS_WITH' expects literal as the second input\"))\n\n        def checkUnsupportedTypes(colType: DataType, literalType: DataType): Unit = {\n          val schema = new StructType()\n            .add(\"col\", colType)\n          val expr = startsWith(new Column(\"col\"), Literal.ofNull(literalType), collationIdentifier)\n          val input = new DefaultColumnarBatch(5, schema, Array(testColumnVector(5, colType)))\n\n          val e = intercept[UnsupportedOperationException] {\n            new DefaultExpressionEvaluator(\n              schema,\n              expr,\n              BooleanType.BOOLEAN).eval(input)\n          }\n          assert(e.getMessage.contains(\"'STARTS_WITH' expects STRING type inputs\"))\n        }\n\n        checkUnsupportedTypes(BooleanType.BOOLEAN, BooleanType.BOOLEAN)\n        checkUnsupportedTypes(LongType.LONG, LongType.LONG)\n        checkUnsupportedTypes(IntegerType.INTEGER, IntegerType.INTEGER)\n        checkUnsupportedTypes(StringType.STRING, BooleanType.BOOLEAN)\n        checkUnsupportedTypes(StringType.STRING, IntegerType.INTEGER)\n        checkUnsupportedTypes(StringType.STRING, LongType.LONG)\n    }\n  }\n\n  test(\"evaluate expression: starts with (unsupported collations)\") {\n    Seq(\n      Some(SPARK_UTF8_LCASE),\n      Some(CollationIdentifier.fromString(\"ICU.sr_Cyrl_SRB\")),\n      Some(CollationIdentifier.fromString(\"ICU.sr_Cyrl_SRB.75.1\"))).foreach {\n      collationIdentifier =>\n        val col1 = stringVector(Seq[String](\"one\", \"two\", \"t%hree\", \"four\", null, null, \"%\"))\n        val col2 = stringVector(Seq[String](\"o\", \"t\", \"T\", \"4\", \"f\", null, null))\n        val schema = new StructType()\n          .add(\"col1\", new StringType(SPARK_UTF8_LCASE))\n          .add(\"col2\", StringType.STRING)\n        val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2))\n\n        val startsWithExpressionLiteral =\n          startsWith(new Column(\"col1\"), Literal.ofString(\"t%\"), collationIdentifier)\n        checkUnsupportedCollation(\n          schema,\n          startsWithExpressionLiteral,\n          input,\n          collationIdentifier.get)\n\n        val startsWithExpressionNullLiteral =\n          startsWith(new Column(\"col1\"), Literal.ofString(null), collationIdentifier)\n        checkUnsupportedCollation(\n          schema,\n          startsWithExpressionNullLiteral,\n          input,\n          collationIdentifier.get)\n\n        // Two literal expressions on both sides\n        val startsWithExpressionAlwaysTrue =\n          startsWith(Literal.ofString(\"ABC\"), Literal.ofString(\"A\"), collationIdentifier)\n        checkUnsupportedCollation(\n          schema,\n          startsWithExpressionAlwaysTrue,\n          input,\n          collationIdentifier.get)\n\n        val startsWithExpressionAlwaysFalse =\n          startsWith(Literal.ofString(\"ABC\"), Literal.ofString(\"_B%\"), collationIdentifier)\n        checkUnsupportedCollation(\n          schema,\n          startsWithExpressionAlwaysFalse,\n          input,\n          collationIdentifier.get)\n\n        // scalastyle:off nonascii\n        val colUnicode = stringVector(Seq[String](\"中文\", \"中\", \"文\"))\n        val schemaUnicode = new StructType().add(\"col\", StringType.STRING)\n        val inputUnicode =\n          new DefaultColumnarBatch(colUnicode.getSize, schemaUnicode, Array(colUnicode))\n        val startsWithExpressionUnicode =\n          startsWith(new Column(\"col\"), Literal.ofString(\"中\"), collationIdentifier)\n        checkUnsupportedCollation(\n          schemaUnicode,\n          startsWithExpressionUnicode,\n          inputUnicode,\n          collationIdentifier.get)\n\n        // scalastyle:off nonascii\n        val colSurrogatePair = stringVector(Seq[String](\"💕😉💕\", \"😉💕\", \"💕\"))\n        val schemaSurrogatePair = new StructType().add(\"col\", StringType.STRING)\n        val inputSurrogatePair =\n          new DefaultColumnarBatch(\n            colSurrogatePair.getSize,\n            schemaSurrogatePair,\n            Array(colSurrogatePair))\n        val startsWithExpressionSurrogatePair =\n          startsWith(new Column(\"col\"), Literal.ofString(\"💕\"), collationIdentifier)\n        checkUnsupportedCollation(\n          schemaSurrogatePair,\n          startsWithExpressionSurrogatePair,\n          inputSurrogatePair,\n          collationIdentifier.get)\n    }\n  }\n\n  test(\"evaluate expression: basic case for in expression\") {\n    // Test with string values\n    val col1 = stringVector(Seq[String](\"one\", \"two\", \"three\", \"four\", null, \"five\"))\n    val schema = new StructType().add(\"col1\", StringType.STRING)\n    val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1))\n\n    // Basic case for string: col1 IN (\"one\", \"three\", \"five\")\n    val inExpressionBasic = in(\n      new Column(\"col1\"),\n      Literal.ofString(\"one\"),\n      Literal.ofString(\"three\"),\n      Literal.ofString(\"five\"))\n    val expOutputBasic = booleanVector(Seq[BooleanJ](true, false, true, false, null, true))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        schema,\n        inExpressionBasic,\n        BooleanType.BOOLEAN).eval(input),\n      expOutputBasic)\n\n    // IN test with no matches: col1 IN (\"six\", \"seven\")\n    val inExpressionNoMatch = in(\n      new Column(\"col1\"),\n      Literal.ofString(\"six\"),\n      Literal.ofString(\"seven\"))\n    val expOutputNoMatch = booleanVector(Seq[BooleanJ](false, false, false, false, null, false))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        schema,\n        inExpressionNoMatch,\n        BooleanType.BOOLEAN).eval(input),\n      expOutputNoMatch)\n\n    // IN test with NULL in list: col1 IN (\"one\", NULL, \"three\"), returns null if no matches.\n    val inExpressionWithNull = in(\n      new Column(\"col1\"),\n      Literal.ofString(\"one\"),\n      Literal.ofString(null),\n      Literal.ofString(\"three\"))\n    val expOutputWithNull = booleanVector(Seq[BooleanJ](true, null, true, null, null, null))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        schema,\n        inExpressionWithNull,\n        BooleanType.BOOLEAN).eval(input),\n      expOutputWithNull)\n\n    // Test with float values: col IN (1.5f, 2.5f, 3.5f)\n    val floatCol = floatVector(Seq[FloatJ](1.5f, 2.5f, 3.5f, 4.5f, null))\n    val floatSchema = new StructType().add(\"floatCol\", FloatType.FLOAT)\n    val floatInput = new DefaultColumnarBatch(floatCol.getSize, floatSchema, Array(floatCol))\n\n    val inExpressionFloat = in(\n      new Column(\"floatCol\"),\n      Literal.ofFloat(1.5f),\n      Literal.ofFloat(2.5f),\n      Literal.ofFloat(3.5f))\n    val expOutputFloat = booleanVector(Seq[BooleanJ](true, true, true, false, null))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        floatSchema,\n        inExpressionFloat,\n        BooleanType.BOOLEAN).eval(floatInput),\n      expOutputFloat)\n\n    // Test with double values: col IN (1.1, 2.2, null)\n    val doubleCol = doubleVector(Seq[DoubleJ](1.1, 2.2, 3.3, 4.4, null))\n    val doubleSchema = new StructType().add(\"doubleCol\", DoubleType.DOUBLE)\n    val doubleInput = new DefaultColumnarBatch(doubleCol.getSize, doubleSchema, Array(doubleCol))\n\n    val inExpressionDouble = in(\n      new Column(\"doubleCol\"),\n      Literal.ofDouble(1.1),\n      Literal.ofDouble(2.2),\n      Literal.ofNull(DoubleType.DOUBLE))\n    val expOutputDouble = booleanVector(Seq[BooleanJ](true, true, null, null, null))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        doubleSchema,\n        inExpressionDouble,\n        BooleanType.BOOLEAN).eval(doubleInput),\n      expOutputDouble)\n\n    // Test with byte values: col IN (0, 1)\n    val byteCol = byteVector(Seq[java.lang.Byte](null, 0.toByte, 0.toByte, 1.toByte, 2.toByte))\n    val byteSchema = new StructType().add(\"byteCol\", ByteType.BYTE)\n    val byteInput = new DefaultColumnarBatch(byteCol.getSize, byteSchema, Array(byteCol))\n\n    val inExpressionByte = in(\n      new Column(\"byteCol\"),\n      Literal.ofByte(0.toByte),\n      Literal.ofByte(1.toByte))\n    // Expected: [null, true, true, true, false] for values [null, 0, 0, 1, 2]\n    val expOutputByte = booleanVector(Seq[BooleanJ](null, true, true, true, false))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        byteSchema,\n        inExpressionByte,\n        BooleanType.BOOLEAN).eval(byteInput),\n      expOutputByte)\n  }\n\n  test(\"evaluate expression: in with incompatible types\") {\n    // Test error cases - incompatible types\n    def checkIncompatibleTypes(valueType: DataType, listElementType: DataType): Unit = {\n      val valueSchema = new StructType().add(\"col\", valueType)\n      val valueVector = testColumnVector(3, valueType)\n      val valueInput = new DefaultColumnarBatch(3, valueSchema, Array(valueVector))\n\n      val incompatibleInExpr = in(\n        new Column(\"col\"),\n        Literal.ofNull(listElementType))\n\n      val e = intercept[UnsupportedOperationException] {\n        new DefaultExpressionEvaluator(\n          valueSchema,\n          incompatibleInExpr,\n          BooleanType.BOOLEAN).eval(valueInput)\n      }\n      assert(\n        e.getMessage.contains(\"IN expression requires all list elements to match the value type\"))\n    }\n\n    // Test incompatible type combinations\n    checkIncompatibleTypes(StringType.STRING, IntegerType.INTEGER)\n    checkIncompatibleTypes(IntegerType.INTEGER, StringType.STRING)\n    checkIncompatibleTypes(BooleanType.BOOLEAN, StringType.STRING)\n  }\n\n  test(\"evaluate expression: in with collation\") {\n    Seq(\n      None, // no collation\n      Some(SPARK_UTF8_BINARY) // UTF8_BINARY collation\n    ).foreach { collationIdentifier =>\n      val col1 = stringVector(Seq[String](\"Test\", \"test\", \"TEST\", null))\n      val schema = new StructType().add(\"col1\", StringType.STRING)\n      val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1))\n\n      // Test with basic case\n      val inExpressionBasic = in(\n        new Column(\"col1\"),\n        collationIdentifier,\n        Literal.ofString(\"test\"),\n        Literal.ofString(\"other\"))\n      val expectedOutput = booleanVector(Seq[BooleanJ](false, true, false, null))\n      checkBooleanVectors(\n        new DefaultExpressionEvaluator(\n          schema,\n          inExpressionBasic,\n          BooleanType.BOOLEAN).eval(input),\n        expectedOutput)\n\n      // Test with NULL in list\n      val inExpressionWithNull = in(\n        new Column(\"col1\"),\n        collationIdentifier,\n        Literal.ofString(\"Test\"),\n        Literal.ofString(null))\n      val expOutputWithNull = booleanVector(Seq[BooleanJ](true, null, null, null))\n      checkBooleanVectors(\n        new DefaultExpressionEvaluator(\n          schema,\n          inExpressionWithNull,\n          BooleanType.BOOLEAN).eval(input),\n        expOutputWithNull)\n    }\n  }\n\n  test(\"evaluate expression: in with unsupported collations\") {\n    val col1 = stringVector(Seq[String](\"Test\", \"test\"))\n    val schema = new StructType().add(\"col1\", StringType.STRING)\n    val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1))\n\n    val inExpressionUnsupported = in(\n      new Column(\"col1\"),\n      Some(SPARK_UTF8_LCASE),\n      Literal.ofString(\"test\"))\n\n    checkUnsupportedCollation(\n      schema,\n      inExpressionUnsupported,\n      input,\n      SPARK_UTF8_LCASE)\n  }\n\n  test(\"evaluate expression: in with non-literal list elements\") {\n    val schema = new StructType().add(\"col1\", IntegerType.INTEGER).add(\"col2\", IntegerType.INTEGER)\n    val input = new DefaultColumnarBatch(\n      2,\n      schema,\n      Array(\n        testColumnVector(2, IntegerType.INTEGER),\n        testColumnVector(2, IntegerType.INTEGER)))\n\n    // Try to create IN with non-literal (Column) in the list\n    val nonLiteralInExpr = new Predicate(\n      \"IN\",\n      List[Expression](\n        new Column(\"col1\"),\n        new Column(\"col2\"), // This should cause an error\n        Literal.ofInt(1)).asJava)\n\n    val e = intercept[UnsupportedOperationException] {\n      new DefaultExpressionEvaluator(schema, nonLiteralInExpr, BooleanType.BOOLEAN).eval(input)\n    }\n    assert(e.getMessage.contains(\"IN expression requires all list elements to be literals\"))\n  }\n\n  test(\"evaluate expression: in expression handling null\") {\n    val col1 = testColumnVector(6, StringType.STRING) // [null, \"1\", null, \"3\", null, \"5\"]\n    val schema = new StructType().add(\"col1\", StringType.STRING)\n    val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1))\n\n    // Test all null semantics scenarios:\n    // 1. NULL value with non-null list -> NULL\n    // 2. Non-null value matches -> TRUE\n    // 3. Non-null value no match, no nulls in list -> FALSE\n    // 4. Non-null value no match, but nulls in list -> NULL\n\n    // Case: value IN (match, null) -> [null, true, null, null, null, null]\n    val inExprMatchWithNull = in(new Column(\"col1\"), Literal.ofString(\"1\"), Literal.ofString(null))\n    val expectedMatchWithNull = booleanVector(Seq[BooleanJ](null, true, null, null, null, null))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(schema, inExprMatchWithNull, BooleanType.BOOLEAN).eval(input),\n      expectedMatchWithNull)\n\n    // Case: value IN (no_match1, no_match2) -> [null, false, null, false, null, false]\n    val inExprNoMatch = in(new Column(\"col1\"), Literal.ofString(\"x\"), Literal.ofString(\"y\"))\n    val expectedNoMatch = booleanVector(Seq[BooleanJ](null, false, null, false, null, false))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(schema, inExprNoMatch, BooleanType.BOOLEAN).eval(input),\n      expectedNoMatch)\n\n    // Case: value IN (no_match, null) -> [null, null, null, null, null, null]\n    val inExprNoMatchWithNull =\n      in(new Column(\"col1\"), Literal.ofString(\"x\"), Literal.ofString(null))\n    val expectedNoMatchWithNull = booleanVector(Seq[BooleanJ](null, null, null, null, null, null))\n    checkBooleanVectors(\n      new DefaultExpressionEvaluator(\n        schema,\n        inExprNoMatchWithNull,\n        BooleanType.BOOLEAN).eval(input),\n      expectedNoMatchWithNull)\n  }\n\n  test(\"evaluate expression: comparators (=, <, <=, >, >=, 'IS NOT DISTINCT FROM')\") {\n    val ASCII_MAX_CHARACTER = '\\u007F'\n    val UTF8_MAX_CHARACTER = new String(Character.toChars(Character.MAX_CODE_POINT))\n\n    // Literals for each data type from the data type value range, used as inputs to comparator\n    // (small, big, small, null)\n    val literals = Seq(\n      (ofByte(1.toByte), ofByte(2.toByte), ofByte(1.toByte), ofNull(ByteType.BYTE)),\n      (ofShort(1.toShort), ofShort(2.toShort), ofShort(1.toShort), ofNull(ShortType.SHORT)),\n      (ofInt(1), ofInt(2), ofInt(1), ofNull(IntegerType.INTEGER)),\n      (ofLong(1L), ofLong(2L), ofLong(1L), ofNull(LongType.LONG)),\n      (ofFloat(1.0f), ofFloat(2.0f), ofFloat(1.0f), ofNull(FloatType.FLOAT)),\n      (ofDouble(1.0), ofDouble(2.0), ofDouble(1.0), ofNull(DoubleType.DOUBLE)),\n      (ofBoolean(false), ofBoolean(true), ofBoolean(false), ofNull(BooleanType.BOOLEAN)),\n      (\n        ofTimestamp(343L),\n        ofTimestamp(123212312L),\n        ofTimestamp(343L),\n        ofNull(TimestampType.TIMESTAMP)),\n      (\n        ofTimestampNtz(323423L),\n        ofTimestampNtz(1232123423312L),\n        ofTimestampNtz(323423L),\n        ofNull(TimestampNTZType.TIMESTAMP_NTZ)),\n      (ofDate(-12123), ofDate(123123), ofDate(-12123), ofNull(DateType.DATE)),\n      (ofString(\"apples\"), ofString(\"oranges\"), ofString(\"apples\"), ofNull(StringType.STRING)),\n      (ofString(\"\"), ofString(\"a\"), ofString(\"\"), ofNull(StringType.STRING)),\n      (ofString(\"abc\"), ofString(\"abc0\"), ofString(\"abc\"), ofNull(StringType.STRING)),\n      (ofString(\"abc\"), ofString(\"abcd\"), ofString(\"abc\"), ofNull(StringType.STRING)),\n      (ofString(\"abc\"), ofString(\"abd\"), ofString(\"abc\"), ofNull(StringType.STRING)),\n      (\n        ofString(\"Abcabcabc\"),\n        ofString(\"aBcabcabc\"),\n        ofString(\"Abcabcabc\"),\n        ofNull(StringType.STRING)),\n      (\n        ofString(\"abcabcabC\"),\n        ofString(\"abcabcabc\"),\n        ofString(\"abcabcabC\"),\n        ofNull(StringType.STRING)),\n      // scalastyle:off nonascii\n      (ofString(\"abc\"), ofString(\"世界\"), ofString(\"abc\"), ofNull(StringType.STRING)),\n      (ofString(\"世界\"), ofString(\"你好\"), ofString(\"世界\"), ofNull(StringType.STRING)),\n      (ofString(\"你好122\"), ofString(\"你好123\"), ofString(\"你好122\"), ofNull(StringType.STRING)),\n      (ofString(\"A\"), ofString(\"Ā\"), ofString(\"A\"), ofNull(StringType.STRING)),\n      (ofString(\"»\"), ofString(\"î\"), ofString(\"»\"), ofNull(StringType.STRING)),\n      (ofString(\"�\"), ofString(\"🌼\"), ofString(\"�\"), ofNull(StringType.STRING)),\n      (\n        ofString(\"abcdef🚀\"),\n        ofString(s\"abcdef$UTF8_MAX_CHARACTER\"),\n        ofString(\"abcdef🚀\"),\n        ofNull(StringType.STRING)),\n      (\n        ofString(\"abcde�abcdef�abcdef�abcdef\"),\n        ofString(s\"abcde�$ASCII_MAX_CHARACTER\"),\n        ofString(\"abcde�abcdef�abcdef�abcdef\"),\n        ofNull(StringType.STRING)),\n      (\n        ofString(\"abcde�abcdef�abcdef�abcdef\"),\n        ofString(s\"abcde�$ASCII_MAX_CHARACTER\"),\n        ofString(\"abcde�abcdef�abcdef�abcdef\"),\n        ofNull(StringType.STRING)),\n      (\n        ofString(\"����\"),\n        ofString(s\"��$UTF8_MAX_CHARACTER\"),\n        ofString(\"����\"),\n        ofNull(StringType.STRING)),\n      (\n        ofString(s\"a${UTF8_MAX_CHARACTER}d\"),\n        ofString(s\"a$UTF8_MAX_CHARACTER$ASCII_MAX_CHARACTER\"),\n        ofString(s\"a${UTF8_MAX_CHARACTER}d\"),\n        ofNull(StringType.STRING)),\n      (\n        ofString(\"abcdefghijklm💞😉💕\\n🥀🌹💐🌺🌷🌼🌻🌷🥀\"),\n        ofString(s\"abcdefghijklm💞😉💕\\n🥀🌹💐🌺🌷🌼$UTF8_MAX_CHARACTER\"),\n        ofString(\"abcdefghijklm💞😉💕\\n🥀🌹💐🌺🌷🌼🌻🌷🥀\"),\n        ofNull(StringType.STRING)),\n      // scalastyle:on nonascii\n      (\n        ofBinary(\"apples\".getBytes()),\n        ofBinary(\"oranges\".getBytes()),\n        ofBinary(\"apples\".getBytes()),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte]()),\n        ofBinary(Array[Byte](5.toByte)),\n        ofBinary(Array[Byte]()),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](0.toByte)), // 00000000\n        ofBinary(Array[Byte](-1.toByte)), // 11111111\n        ofBinary(Array[Byte](0.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](127.toByte)), // 01111111\n        ofBinary(Array[Byte](-1.toByte)), // 11111111\n        ofBinary(Array[Byte](127.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](5.toByte, 10.toByte)),\n        ofBinary(Array[Byte](6.toByte)),\n        ofBinary(Array[Byte](5.toByte, 10.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](5.toByte, 10.toByte)),\n        ofBinary(Array[Byte](5.toByte, 100.toByte)),\n        ofBinary(Array[Byte](5.toByte, 10.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](5.toByte, 10.toByte, 5.toByte)), // 00000101 00001010 00000101\n        ofBinary(Array[Byte](5.toByte, -3.toByte)), // 00000101 11111101\n        ofBinary(Array[Byte](5.toByte, 10.toByte, 5.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](5.toByte, -25.toByte, 5.toByte)), // 00000101 11100111 00000101\n        ofBinary(Array[Byte](5.toByte, -9.toByte)), // 00000101 11110111\n        ofBinary(Array[Byte](5.toByte, -25.toByte, 5.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofBinary(Array[Byte](5.toByte, 10.toByte)),\n        ofBinary(Array[Byte](5.toByte, 10.toByte, 0.toByte)),\n        ofBinary(Array[Byte](5.toByte, 10.toByte)),\n        ofNull(BinaryType.BINARY)),\n      (\n        ofDecimal(BigDecimalJ.valueOf(1.12), 7, 3),\n        ofDecimal(BigDecimalJ.valueOf(5233.232), 7, 3),\n        ofDecimal(BigDecimalJ.valueOf(1.12), 7, 3),\n        ofNull(new DecimalType(7, 3))))\n\n    // Mapping of comparator to expected results for:\n    // comparator(small, big)\n    // comparator(big, small)\n    // comparator(small, small)\n    // comparator(small, null)\n    // comparator(big, null)\n    // comparator(null, null)\n    val comparatorToExpResults = Map[String, Seq[BooleanJ]](\n      \"<\" -> Seq(true, false, false, null, null, null),\n      \"<=\" -> Seq(true, false, true, null, null, null),\n      \">\" -> Seq(false, true, false, null, null, null),\n      \">=\" -> Seq(false, true, true, null, null, null),\n      \"=\" -> Seq(false, false, true, null, null, null),\n      \"IS NOT DISTINCT FROM\" -> Seq(false, false, true, false, false, true))\n\n    literals.foreach {\n      case (small1, big, small2, nullLit) =>\n        comparatorToExpResults.foreach {\n          case (comparator, expectedResults) =>\n            val testCases = Seq(\n              (small1, big),\n              (big, small1),\n              (small1, small2),\n              (small1, nullLit),\n              (nullLit, big),\n              (nullLit, nullLit))\n            testCases.zip(expectedResults).foreach { case ((left, right), expected) =>\n              testComparator(comparator, left, right, expected)\n            }\n\n            // Predicate with collation is supported only for comparisons between StringTypes\n            val allStringTypes = Seq(small1, big, small2, nullLit).forall {\n              literal => literal.getDataType.isInstanceOf[StringType]\n            }\n            if (allStringTypes) {\n              Seq(\n                SPARK_UTF8_BINARY,\n                SPARK_UTF8_LCASE,\n                CollationIdentifier.fromString(\"ICU.sr_Cyrl_SRB\"),\n                CollationIdentifier.fromString(\"ICU.sr_Cyrl_SRB.75.1\")).foreach {\n                collationIdentifier =>\n                  testCases.zip(expectedResults).foreach { case ((left, right), expected) =>\n                    testCollatedComparator(comparator, left, right, expected, collationIdentifier)\n                  }\n              }\n            }\n        }\n    }\n  }\n\n  test(\"check Predicate with collation comparing invalid types\") {\n    Seq(\n      // predicateName\n      \"=\",\n      \"<\",\n      \"<=\",\n      \">\",\n      \">=\",\n      \"IS NOT DISTINCT FROM\",\n      \"STARTS_WITH\").foreach {\n      predicateName =>\n        Seq(\n          // (expr1, expr2, schema)\n          (\n            Literal.ofString(\"apple\"),\n            Literal.ofInt(1),\n            new StructType()),\n          (\n            Literal.ofString(\"apple\"),\n            Literal.ofLong(1L),\n            new StructType()),\n          (\n            Literal.ofFloat(2.3f),\n            Literal.ofString(\"apple\"),\n            new StructType()),\n          (\n            Literal.ofDouble(2.3),\n            Literal.ofBoolean(false),\n            new StructType()),\n          (\n            new Column(Array(\"col1\", \"col11\")),\n            Literal.ofString(\"apple\"),\n            new StructType()\n              .add(\n                \"col1\",\n                new StructType()\n                  .add(\"col11\", IntegerType.INTEGER))),\n          (\n            new Column(Array(\"col1\", \"col11\")),\n            Literal.ofBoolean(false),\n            new StructType()\n              .add(\n                \"col1\",\n                new StructType()\n                  .add(\"col11\", StringType.STRING))),\n          (\n            new Column(Array(\"col1\", \"col11\")),\n            Literal.ofBoolean(false),\n            new StructType()\n              .add(\n                \"col1\",\n                new StructType()\n                  .add(\"col11\", new StringType(SPARK_UTF8_LCASE)))),\n          (\n            new Column(Array(\"col1\", \"col11\")),\n            new Column(Array(\"col1\", \"col12\")),\n            new StructType()\n              .add(\n                \"col1\",\n                new StructType()\n                  .add(\"col11\", DoubleType.DOUBLE)\n                  .add(\"col12\", FloatType.FLOAT)))).foreach {\n          case (expr1: Expression, expr2: Expression, schema: StructType) =>\n            val expr = comparator(\n              predicateName,\n              expr1,\n              expr2,\n              Some(SPARK_UTF8_BINARY))\n            val input = zeroColumnBatch(rowCount = 1)\n\n            val e = intercept[UnsupportedOperationException] {\n              new DefaultExpressionEvaluator(\n                schema,\n                expr,\n                BooleanType.BOOLEAN).eval(input)\n            }\n            assert(e.getMessage.contains(\"expects STRING type inputs\"))\n        }\n    }\n  }\n\n  // Literals for each data type from the data type value range, used as inputs to comparator\n  // (byte, short, int, float, double)\n  val literals = Seq(\n    ofByte(1.toByte),\n    ofShort(223),\n    ofInt(-234),\n    ofLong(223L),\n    ofFloat(-2423423.9f),\n    ofNull(DoubleType.DOUBLE))\n\n  test(\"evaluate expression: substring\") {\n    // scalastyle:off nonascii\n    val data = Seq[String](\n      null,\n      \"one\",\n      \"two\",\n      \"three\",\n      \"four\",\n      null,\n      null,\n      \"seven\",\n      \"eight\",\n      \"😉\",\n      \"ë\")\n    val col = stringVector(data)\n    val col_name = \"str_col\"\n    val schema = new StructType().add(col_name, StringType.STRING)\n    val input = new DefaultColumnarBatch(col.getSize, schema, Array(col))\n\n    def checkSubString(\n        input: DefaultColumnarBatch,\n        substringExpression: ScalarExpression,\n        expOutputSeq: Seq[String]): Unit = {\n      val actOutputVector =\n        new DefaultExpressionEvaluator(\n          schema,\n          substringExpression,\n          StringType.STRING).eval(input)\n      val expOutputVector = stringVector(expOutputSeq);\n      checkStringVectors(actOutputVector, expOutputVector)\n    }\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 0),\n      // scalastyle:off nonascii\n      Seq[String](null, \"one\", \"two\", \"three\", \"four\", null, null, \"seven\", \"eight\", \"😉\", \"ë\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 1),\n      // scalastyle:off nonascii\n      Seq[String](null, \"one\", \"two\", \"three\", \"four\", null, null, \"seven\", \"eight\", \"😉\", \"ë\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2),\n      Seq[String](null, \"ne\", \"wo\", \"hree\", \"our\", null, null, \"even\", \"ight\", \"\", \"̈\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -1),\n      // scalastyle:off nonascii\n      Seq[String](null, \"e\", \"o\", \"e\", \"r\", null, null, \"n\", \"t\", \"😉\", \"̈\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -1000),\n      // scalastyle:off nonascii\n      Seq[String](null, \"one\", \"two\", \"three\", \"four\", null, null, \"seven\", \"eight\", \"😉\", \"ë\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 0, Option(4)),\n      // scalastyle:off nonascii\n      Seq[String](null, \"one\", \"two\", \"thre\", \"four\", null, null, \"seve\", \"eigh\", \"😉\", \"ë\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2, Option(0)),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 1, Option(1)),\n      // scalastyle:off nonascii\n      Seq[String](null, \"o\", \"t\", \"t\", \"f\", null, null, \"s\", \"e\", \"😉\", \"e\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2, Option(1)),\n      Seq[String](null, \"n\", \"w\", \"h\", \"o\", null, null, \"e\", \"i\", \"\", \"̈\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2, Option(10000)),\n      Seq[String](null, \"ne\", \"wo\", \"hree\", \"our\", null, null, \"even\", \"ight\", \"\", \"̈\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 1000),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 1000, Option(10000)),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2, Option(-10)),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -2, Option(1)),\n      Seq[String](null, \"n\", \"w\", \"e\", \"u\", null, null, \"e\", \"h\", \"\", \"e\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -2, Option(2)),\n      // scalastyle:off nonascii\n      Seq[String](null, \"ne\", \"wo\", \"ee\", \"ur\", null, null, \"en\", \"ht\", \"😉\", \"ë\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -4, Option(3)),\n      Seq[String](null, \"on\", \"tw\", \"hre\", \"fou\", null, null, \"eve\", \"igh\", \"\", \"e\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -100, Option(95)),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -100, Option(98)),\n      Seq[String](null, \"o\", \"t\", \"thr\", \"fo\", null, null, \"sev\", \"eig\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -100, Option(108)),\n      // scalastyle:off nonascii\n      Seq[String](null, \"one\", \"two\", \"three\", \"four\", null, null, \"seven\", \"eight\", \"😉\", \"ë\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2147483647, Option(10000)),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), 2147483647),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -2147483648, Option(10000)),\n      Seq[String](null, \"\", \"\", \"\", \"\", null, null, \"\", \"\", \"\", \"\"))\n\n    checkSubString(\n      input,\n      substring(new Column(col_name), -2147483648),\n      // scalastyle:off nonascii\n      Seq[String](null, \"one\", \"two\", \"three\", \"four\", null, null, \"seven\", \"eight\", \"😉\", \"ë\"))\n\n    val outputVectorForEmptyInput = evaluator(\n      schema,\n      new ScalarExpression(\n        \"SUBSTRING\",\n        util.Arrays.asList(\n          new Column(col_name),\n          Literal.ofInt(1),\n          Literal.ofInt(1))),\n      StringType.STRING).eval(new DefaultColumnarBatch(\n      /* size= */ 0,\n      schema,\n      Array(\n        testColumnVector( /* size= */ 0, StringType.STRING),\n        testColumnVector( /* size= */ 0, BinaryType.BINARY))))\n    checkStringVectors(outputVectorForEmptyInput, stringVector(Seq[String]()))\n\n    def checkUnsupportedColumnTypes(colType: DataType): Unit = {\n      val schema = new StructType()\n        .add(col_name, colType)\n      val batch = new DefaultColumnarBatch(5, schema, Array(testColumnVector(5, colType)))\n      val e = intercept[UnsupportedOperationException] {\n        evaluator(\n          schema,\n          new ScalarExpression(\n            \"SUBSTRING\",\n            util.Arrays.asList(new Column(col_name), Literal.ofInt(1))),\n          StringType.STRING).eval(batch)\n      }\n      assert(\n        e.getMessage.contains(\"Invalid type of first input of SUBSTRING: expects STRING\"))\n    }\n\n    checkUnsupportedColumnTypes(IntegerType.INTEGER)\n    checkUnsupportedColumnTypes(ByteType.BYTE)\n    checkUnsupportedColumnTypes(BooleanType.BOOLEAN)\n    checkUnsupportedColumnTypes(BinaryType.BINARY)\n\n    val badLiteralSize = intercept[UnsupportedOperationException] {\n      evaluator(\n        schema,\n        new ScalarExpression(\n          \"SUBSTRING\",\n          util.Arrays.asList(\n            new Column(col_name),\n            Literal.ofInt(1),\n            Literal.ofInt(1),\n            Literal.ofInt(1))),\n        StringType.STRING).eval(new DefaultColumnarBatch(\n        /* size= */ 5,\n        schema,\n        Array(testColumnVector( /* size= */ 5, StringType.STRING))))\n    }\n    assert(\n      badLiteralSize.getMessage.contains(\n        \"Invalid number of inputs to SUBSTRING expression.\"))\n\n    val badPosType = intercept[UnsupportedOperationException] {\n      evaluator(\n        schema,\n        new ScalarExpression(\n          \"SUBSTRING\",\n          util.Arrays.asList(\n            new Column(\"str_col\"),\n            Literal.ofBoolean(true))),\n        StringType.STRING).eval(new DefaultColumnarBatch(\n        /* size= */ 5,\n        schema,\n        Array(testColumnVector( /* size= */ 5, StringType.STRING))))\n    }\n    assert(badPosType.getMessage.contains(\"Invalid `pos` argument type for SUBSTRING\"))\n\n    val badLenType = intercept[UnsupportedOperationException] {\n      evaluator(\n        schema,\n        new ScalarExpression(\n          \"SUBSTRING\",\n          util.Arrays.asList(\n            new Column(col_name),\n            Literal.ofInt(1),\n            Literal.ofBoolean(true))),\n        StringType.STRING).eval(new DefaultColumnarBatch(\n        /* size= */ 5,\n        schema,\n        Array(testColumnVector( /* size= */ 5, StringType.STRING))))\n    }\n    assert(badLenType.getMessage.contains(\"Invalid `len` argument type for SUBSTRING\"))\n  }\n\n  test(\"evaluate expression: comparators `byte` with other implicit types\") {\n    // Mapping of comparator to expected results for:\n    // (byte, short), (byte, int), (byte, long), (byte, float), (byte, double)\n    val comparatorToExpResults = Map[String, Seq[BooleanJ]](\n      \"<\" -> Seq(true, false, true, false, null),\n      \"<=\" -> Seq(true, false, true, false, null),\n      \">\" -> Seq(false, true, false, true, null),\n      \">=\" -> Seq(false, true, false, true, null),\n      \"=\" -> Seq(false, false, false, false, null))\n\n    // Left operand is first literal in [[literal]] which a byte type\n    // Right operands are the remaining literals to the left side of it in [[literal]]\n    val right = literals(0)\n    Seq.range(1, literals.length).foreach { idx =>\n      comparatorToExpResults.foreach {\n        case (comparator, expectedResults) =>\n          testComparator(comparator, right, literals(idx), expectedResults(idx - 1))\n      }\n    }\n  }\n\n  test(\"evaluate expression: comparators `short` with other implicit types\") {\n    // Mapping of comparator to expected results for:\n    // (short, int), (short, long), (short, float), (short, double)\n    val comparatorToExpResults = Map[String, Seq[BooleanJ]](\n      \"<\" -> Seq(false, false, false, null),\n      \"<=\" -> Seq(false, true, false, null),\n      \">\" -> Seq(true, false, true, null),\n      \">=\" -> Seq(true, true, true, null),\n      \"=\" -> Seq(false, true, false, null))\n\n    // Left operand is first literal in [[literal]] which a short type\n    // Right operands are the remaining literals to the left side of it in [[literal]]\n    val right = literals(1)\n    Seq.range(2, literals.length).foreach { idx =>\n      comparatorToExpResults.foreach {\n        case (comparator, expectedResults) =>\n          testComparator(comparator, right, literals(idx), expectedResults(idx - 2))\n      }\n    }\n  }\n\n  test(\"evaluate expression: comparators `int` with other implicit types\") {\n    // Mapping of comparator to expected results for: (int, long), (int, float), (int, double)\n    val comparatorToExpResults = Map[String, Seq[BooleanJ]](\n      \"<\" -> Seq(true, false, null),\n      \"<=\" -> Seq(true, false, null),\n      \">\" -> Seq(false, true, null),\n      \">=\" -> Seq(false, true, null),\n      \"=\" -> Seq(false, false, null))\n\n    // Left operand is first literal in [[literal]] which a int type\n    // Right operands are the remaining literals to the left side of it in [[literal]]\n    val right = literals(2)\n    Seq.range(3, literals.length).foreach { idx =>\n      comparatorToExpResults.foreach {\n        case (comparator, expectedResults) =>\n          testComparator(comparator, right, literals(idx), expectedResults(idx - 3))\n      }\n    }\n  }\n\n  test(\"evaluate expression: comparators `long` with other implicit types\") {\n    // Mapping of comparator to expected results for: (long, float), (long, double)\n    val comparatorToExpResults = Map[String, Seq[BooleanJ]](\n      \"<\" -> Seq(false, null),\n      \"<=\" -> Seq(false, null),\n      \">\" -> Seq(true, null),\n      \">=\" -> Seq(true, null),\n      \"=\" -> Seq(false, null))\n\n    // Left operand is fourth literal in [[literal]] which a long type\n    // Right operands are the remaining literals to the left side of it in [[literal]]\n    val right = literals(3)\n    Seq.range(4, literals.length).foreach { idx =>\n      comparatorToExpResults.foreach {\n        case (comparator, expectedResults) =>\n          testComparator(comparator, right, literals(idx), expectedResults(idx - 4))\n      }\n    }\n  }\n\n  test(\"evaluate expression: unsupported implicit casts\") {\n    intercept[UnsupportedOperationException] {\n      testComparator(\"<\", ofInt(21), ofDate(123), null)\n    }\n  }\n\n  test(\"evaluate expression: comparators `float` with other implicit types\") {\n    // Comparator results for: (float, double) is always null as one of the operands is null\n    val comparatorToExpResults = Seq(\"<\", \"<=\", \">\", \">=\", \"=\")\n\n    // Left operand is fifth literal in [[literal]] which is a float type\n    // Right operands are the remaining literals to the left side of it in [[literal]]\n    val right = literals(4)\n    Seq.range(5, literals.length).foreach { idx =>\n      comparatorToExpResults.foreach { comparator =>\n        testComparator(comparator, right, literals(idx), null)\n      }\n    }\n  }\n\n  test(\"evaluate expression: element_at\") {\n    val nullStr = null.asInstanceOf[String]\n    val testMapValues: Seq[Map[AnyRef, AnyRef]] = Seq(\n      Map(\"k0\" -> \"v00\", \"k1\" -> \"v01\", \"k3\" -> nullStr, nullStr -> \"v04\"),\n      Map(\"k0\" -> \"v10\", \"k1\" -> nullStr, \"k3\" -> \"v13\", nullStr -> \"v14\"),\n      Map(\"k0\" -> nullStr, \"k1\" -> \"v21\", \"k3\" -> \"v23\", nullStr -> \"v24\"),\n      null)\n    val testMapVector = buildMapVector(\n      testMapValues,\n      new MapType(StringType.STRING, StringType.STRING, true))\n\n    val inputBatch = new DefaultColumnarBatch(\n      testMapVector.getSize,\n      new StructType().add(\"partitionValues\", testMapVector.getDataType),\n      Seq(testMapVector).toArray)\n    Seq(\"k0\", \"k1\", \"k2\", null).foreach { lookupKey =>\n      val expOutput = testMapValues.map(map => {\n        if (map == null) null\n        else map.getOrElse(lookupKey, null)\n      })\n\n      val lookupKeyExpr = if (lookupKey == null) {\n        Literal.ofNull(StringType.STRING)\n      } else {\n        Literal.ofString(lookupKey)\n      }\n      val elementAtExpr = new ScalarExpression(\n        \"element_at\",\n        util.Arrays.asList(new Column(\"partitionValues\"), lookupKeyExpr))\n\n      val outputVector = evaluator(inputBatch.getSchema, elementAtExpr, StringType.STRING)\n        .eval(inputBatch)\n      assert(outputVector.getSize === testMapValues.size)\n      assert(outputVector.getDataType === StringType.STRING)\n      Seq.range(0, testMapValues.size).foreach { rowId =>\n        val expNull = expOutput(rowId) == null\n        assert(outputVector.isNullAt(rowId) == expNull)\n        if (!expNull) {\n          assert(outputVector.getString(rowId) === expOutput(rowId))\n        }\n      }\n    }\n  }\n\n  test(\"evaluate expression: element_at - unsupported map type input\") {\n    val inputSchema = new StructType()\n      .add(\"as_map\", new MapType(IntegerType.INTEGER, BooleanType.BOOLEAN, true))\n    val elementAtExpr = new ScalarExpression(\n      \"element_at\",\n      util.Arrays.asList(new Column(\"as_map\"), Literal.ofString(\"empty\")))\n\n    val ex = intercept[UnsupportedOperationException] {\n      evaluator(inputSchema, elementAtExpr, StringType.STRING)\n    }\n    assert(ex.getMessage.contains(\n      \"ELEMENT_AT(column(`as_map`), empty): Supported only on type map(string, string) input data\"))\n  }\n\n  test(\"evaluate expression: element_at - unsupported lookup type input\") {\n    val inputSchema = new StructType()\n      .add(\"as_map\", new MapType(StringType.STRING, StringType.STRING, true))\n    val elementAtExpr = new ScalarExpression(\n      \"element_at\",\n      util.Arrays.asList(new Column(\"as_map\"), Literal.ofShort(24)))\n\n    val ex = intercept[UnsupportedOperationException] {\n      evaluator(inputSchema, elementAtExpr, StringType.STRING)\n    }\n    assert(ex.getMessage.contains(\n      \"lookup key type (short) is different from the map key type (string)\"))\n  }\n\n  test(\"evaluate expression: partition_value\") {\n    // (serialized partition value, partition col type, expected deserialized partition value)\n    val testCases = Seq(\n      (\"true\", BooleanType.BOOLEAN, true),\n      (\"false\", BooleanType.BOOLEAN, false),\n      (null, BooleanType.BOOLEAN, null),\n      (\"24\", ByteType.BYTE, 24.toByte),\n      (\"null\", ByteType.BYTE, null),\n      (\"876\", ShortType.SHORT, 876.toShort),\n      (\"null\", ShortType.SHORT, null),\n      (\"2342342\", IntegerType.INTEGER, 2342342),\n      (\"null\", IntegerType.INTEGER, null),\n      (\"234234223\", LongType.LONG, 234234223L),\n      (\"null\", LongType.LONG, null),\n      (\"23423.4223\", FloatType.FLOAT, 23423.4223f),\n      (\"null\", FloatType.FLOAT, null),\n      (\"23423.422233\", DoubleType.DOUBLE, 23423.422233d),\n      (\"null\", DoubleType.DOUBLE, null),\n      (\"234.422233\", new DecimalType(10, 6), new BigDecimalJ(\"234.422233\")),\n      (\"null\", DoubleType.DOUBLE, null),\n      (\"string_val\", StringType.STRING, \"string_val\"),\n      (\"null\", StringType.STRING, null),\n      (\"binary_val\", BinaryType.BINARY, \"binary_val\".getBytes()),\n      (\"null\", BinaryType.BINARY, null),\n      (\"2021-11-18\", DateType.DATE, InternalUtils.daysSinceEpoch(Date.valueOf(\"2021-11-18\"))),\n      (\"null\", DateType.DATE, null),\n      (\n        \"2020-02-18 22:00:10\",\n        TimestampType.TIMESTAMP,\n        InternalUtils.microsSinceEpoch(Timestamp.valueOf(\"2020-02-18 22:00:10\"))),\n      (\n        \"2020-02-18 00:00:10.023\",\n        TimestampType.TIMESTAMP,\n        InternalUtils.microsSinceEpoch(Timestamp.valueOf(\"2020-02-18 00:00:10.023\"))),\n      (\"null\", TimestampType.TIMESTAMP, null),\n      ( // ISO8601 format\n        \"2024-01-02T12:30:00.000000Z\",\n        TimestampType.TIMESTAMP,\n        InternalUtils.microsSinceEpoch(Timestamp.valueOf(\"2024-01-02 12:30:00\"))),\n      ( // Test with microsecond precision (no seconds)\n        \"1970-01-01T00:00:00.123456Z\",\n        TimestampType.TIMESTAMP,\n        InternalUtils.microsSinceEpoch(Timestamp.valueOf(\"1970-01-01 00:00:00.123456\"))),\n      ( // Test with microsecond precision (with seconds, current date)\n        \"2025-01-01T00:00:00.123456Z\",\n        TimestampType.TIMESTAMP,\n        InternalUtils.microsSinceEpoch(Timestamp.valueOf(\"2025-01-01 00:00:00.123456\"))))\n\n    val inputBatch = zeroColumnBatch(rowCount = 1)\n    testCases.foreach { testCase =>\n      val (serializedPartVal, partType, deserializedPartVal) = testCase\n      val literalSerializedPartVal = if (serializedPartVal == \"null\") {\n        Literal.ofNull(StringType.STRING)\n      } else {\n        Literal.ofString(serializedPartVal)\n      }\n      val expr = new PartitionValueExpression(literalSerializedPartVal, partType)\n      val outputVector = evaluator(inputBatch.getSchema, expr, partType).eval(inputBatch)\n      assert(outputVector.getSize === 1)\n      assert(outputVector.getDataType === partType)\n      assert(outputVector.isNullAt(0) === (deserializedPartVal == null))\n      if (deserializedPartVal != null) {\n        assert(getValueAsObject(outputVector, 0) === deserializedPartVal)\n      }\n    }\n  }\n\n  test(\"evaluate expression: partition_value - invalid serialize value\") {\n    val inputBatch = zeroColumnBatch(rowCount = 1)\n    val (serializedPartVal, partType) = (\"23423sdfsdf\", IntegerType.INTEGER)\n    val expr = new PartitionValueExpression(Literal.ofString(serializedPartVal), partType)\n    val ex = intercept[IllegalArgumentException] {\n      val outputVector = evaluator(inputBatch.getSchema, expr, partType).eval(inputBatch)\n      outputVector.getInt(0)\n    }\n    assert(ex.getMessage.contains(serializedPartVal))\n  }\n\n  private def evaluator(inputSchema: StructType, expression: Expression, outputType: DataType)\n      : DefaultExpressionEvaluator = {\n    new DefaultExpressionEvaluator(inputSchema, expression, outputType)\n  }\n\n  private def checkUnsupportedCollation(\n      schema: StructType,\n      expression: Expression,\n      input: ColumnarBatch,\n      collationIdentifier: CollationIdentifier): Unit = {\n    val e = intercept[UnsupportedOperationException] {\n      evaluator(schema, expression, BooleanType.BOOLEAN).eval(input)\n    }\n    assert(e.getMessage.contains(\n      s\"\"\"Unsupported collation: \"$collationIdentifier\".\n         | Default Engine supports just \"$SPARK_UTF8_BINARY\"\n         | collation.\"\"\".stripMargin.replace(\"\\n\", \"\")))\n\n  }\n\n  private def testCollatedComparator(\n      comparatorName: String,\n      left: Expression,\n      right: Expression,\n      expResult: BooleanJ,\n      collationIdentifier: CollationIdentifier): Unit = {\n    val collatedPredicate = comparator(comparatorName, left, right, Some(collationIdentifier))\n    val batch = zeroColumnBatch(rowCount = 1)\n\n    if (collationIdentifier.isSparkUTF8BinaryCollation) {\n      val outputVector = evaluator(\n        batch.getSchema,\n        collatedPredicate,\n        BooleanType.BOOLEAN).eval(batch)\n\n      assert(outputVector.getSize === 1)\n      assert(outputVector.getDataType === BooleanType.BOOLEAN)\n      assert(\n        outputVector.isNullAt(0) === (expResult == null),\n        s\"Unexpected null value for Predicate with collation: $collatedPredicate\")\n      if (expResult != null) {\n        assert(\n          outputVector.getBoolean(0) === expResult,\n          s\"\"\"Unexpected value for Predicate with collation: $collatedPredicate,\n             | expected: $expResult, actual: ${outputVector.getBoolean(0)}\"\"\".stripMargin)\n      }\n    } else {\n      checkUnsupportedCollation(batch.getSchema, collatedPredicate, batch, collationIdentifier)\n    }\n  }\n\n  private def testComparator(\n      comparator: String,\n      left: Expression,\n      right: Expression,\n      expResult: BooleanJ): Unit = {\n    val expression = new Predicate(comparator, left, right)\n    val batch = zeroColumnBatch(rowCount = 1)\n    val outputVector = evaluator(batch.getSchema, expression, BooleanType.BOOLEAN).eval(batch)\n\n    assert(outputVector.getSize === 1)\n    assert(outputVector.getDataType === BooleanType.BOOLEAN)\n    assert(\n      outputVector.isNullAt(0) === (expResult == null),\n      s\"Unexpected null value: $comparator($left, $right)\")\n    if (expResult != null) {\n      assert(\n        outputVector.getBoolean(0) === expResult,\n        s\"Unexpected value: $comparator($left, $right)\")\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/DefaultPredicateEvaluatorSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions\n\nimport java.lang.{Boolean => BooleanJ}\nimport java.util.Optional\nimport java.util.Optional.empty\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.types.{BooleanType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * [[DefaultPredicateEvaluator]] internally uses [[DefaultExpressionEvaluator]]. In this suite\n * test the code specific to the [[DefaultPredicateEvaluator]] such as taking into consideration of\n * existing selection vector.\n */\nclass DefaultPredicateEvaluatorSuite extends AnyFunSuite with ExpressionSuiteBase {\n  private val testLeftCol = booleanVector(\n    Seq[BooleanJ](true, true, false, true, true, false, null, true, null, false, null))\n  private val testRightCol = booleanVector(\n    Seq[BooleanJ](true, false, false, true, false, false, true, null, false, null, null))\n\n  private val testSchema = new StructType()\n    .add(\"left\", BooleanType.BOOLEAN)\n    .add(\"right\", BooleanType.BOOLEAN)\n\n  private val batch = new DefaultColumnarBatch(\n    testLeftCol.getSize,\n    testSchema,\n    Array(testLeftCol, testRightCol))\n\n  private val left = comparator(\"=\", new Column(\"left\"), Literal.ofBoolean(true))\n  private val right = comparator(\"=\", new Column(\"right\"), Literal.ofBoolean(true))\n\n  private val orPredicate = or(left, right)\n\n  private val expOrOutput = booleanVector(\n    Seq[BooleanJ](true, true, false, true, true, false, true, true, null, null, null))\n\n  test(\"evaluate predicate: with no starting selection vector\") {\n    val batch = new DefaultColumnarBatch(\n      testLeftCol.getSize,\n      testSchema,\n      Array(testLeftCol, testRightCol))\n\n    val actOutputVector = evalOr(batch)\n    checkBooleanVectors(actOutputVector, expOrOutput)\n  }\n\n  test(\"evaluate predicate: with existing selection vector\") {\n    val existingSelVector = booleanVector(\n      Seq[BooleanJ](false, true, true, true, false, false, null, null, null, null, null))\n    val outputWithSelVector = booleanVector(\n      Seq[BooleanJ](false, true, false, true, false, false, null, null, null, null, null))\n\n    val actOutputVector = evalOr(batch, Optional.of(existingSelVector))\n    checkBooleanVectors(actOutputVector, outputWithSelVector)\n  }\n\n  test(\"evaluate predicate: multiple rounds with selection vectors\") {\n    val output0 = evalOr(batch)\n    checkBooleanVectors(output0, expOrOutput)\n\n    val selVec1 = booleanVector(\n      Seq[BooleanJ](false, true, false, true, false, false, null, null, null, null, null))\n    val expOutputWithSelVec1 = booleanVector(\n      Seq[BooleanJ](false, true, false, true, false, false, null, null, null, null, null))\n    checkBooleanVectors(evalOr(batch, Optional.of(selVec1)), expOutputWithSelVec1)\n\n    val selVec2 = booleanVector(\n      Seq[BooleanJ](false, false, false, true, false, false, null, null, null, null, null))\n    val expOutputWithSelVec2 = booleanVector(\n      Seq[BooleanJ](false, false, false, true, false, false, null, null, null, null, null))\n    checkBooleanVectors(evalOr(batch, Optional.of(selVec2)), expOutputWithSelVec2)\n  }\n\n  def evalOr(\n      batch: ColumnarBatch,\n      existingSelVector: Optional[ColumnVector] = empty()): ColumnVector = {\n    val evaluator = new DefaultPredicateEvaluator(batch.getSchema, orPredicate)\n    evaluator.eval(batch, existingSelVector)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/ExpressionSuiteBase.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.defaults.utils.{DefaultVectorTestUtils, TestUtils}\nimport io.delta.kernel.defaults.utils.DefaultKernelTestUtils.getValueAsObject\nimport io.delta.kernel.expressions._\nimport io.delta.kernel.internal.util.ExpressionUtils.createPredicate\nimport io.delta.kernel.types._\n\ntrait ExpressionSuiteBase extends TestUtils with DefaultVectorTestUtils {\n\n  /** create a columnar batch of given `size` with zero columns in it. */\n  protected def zeroColumnBatch(rowCount: Int): ColumnarBatch = {\n    new DefaultColumnarBatch(rowCount, new StructType(), new Array[ColumnVector](0))\n  }\n\n  protected def and(left: Predicate, right: Predicate): And = {\n    new And(left, right)\n  }\n\n  protected def or(left: Predicate, right: Predicate): Or = {\n    new Or(left, right)\n  }\n\n  protected def substring(expr: Expression, pos: Int, len: Option[Int] = None): ScalarExpression = {\n    var children = List(expr, Literal.ofInt(pos))\n    if (len.isDefined) {\n      children = children :+ Literal.ofInt(len.get)\n    }\n    new ScalarExpression(\"substring\", children.asJava)\n  }\n\n  protected def like(\n      left: Expression,\n      right: Expression,\n      escape: Option[Character] = None): Predicate = {\n    if (escape.isDefined && escape.get != null) {\n      like(List(left, right, Literal.ofString(escape.get.toString)))\n    } else like(List(left, right))\n  }\n\n  protected def like(children: List[Expression]): Predicate = {\n    new Predicate(\"like\", children.asJava)\n  }\n\n  protected def startsWith(\n      left: Expression,\n      right: Expression,\n      collationIdentifier: Option[CollationIdentifier] = None): Predicate = {\n    createPredicate(\n      \"starts_with\",\n      List(left, right).asJava,\n      optionToJava(collationIdentifier))\n  }\n\n  protected def comparator(\n      symbol: String,\n      left: Expression,\n      right: Expression,\n      collationIdentifier: Option[CollationIdentifier] = None): Predicate = {\n    createPredicate(symbol, List(left, right).asJava, optionToJava(collationIdentifier))\n  }\n\n  protected def in(value: Expression, inList: Expression*): In = {\n    new In(value, inList.toList.asJava)\n  }\n\n  protected def in(\n      value: Expression,\n      collationIdentifier: Option[CollationIdentifier],\n      inList: Expression*): In = {\n    if (collationIdentifier.isDefined) {\n      new In(value, inList.toList.asJava, collationIdentifier.get)\n    } else {\n      new In(value, inList.toList.asJava)\n    }\n  }\n\n  protected def checkBooleanVectors(actual: ColumnVector, expected: ColumnVector): Unit = {\n    assert(actual.getDataType === expected.getDataType)\n    assert(actual.getSize === expected.getSize)\n    Seq.range(0, actual.getSize).foreach { rowId =>\n      assert(actual.isNullAt(rowId) === expected.isNullAt(rowId))\n      if (!actual.isNullAt(rowId)) {\n        assert(\n          actual.getBoolean(rowId) === expected.getBoolean(rowId),\n          s\"unexpected value at $rowId\")\n      }\n    }\n  }\n\n  protected def checkLongVectors(actual: ColumnVector, expected: ColumnVector): Unit = {\n    assert(actual.getDataType === expected.getDataType)\n    assert(actual.getSize === expected.getSize)\n    Seq.range(0, actual.getSize).foreach { rowId =>\n      if (expected.isNullAt(rowId)) {\n        assert(actual.isNullAt(rowId), s\"Expected null at row $rowId\")\n      } else {\n        assert(actual.getLong(rowId) === expected.getLong(rowId), s\"Unexpected value at row $rowId\")\n      }\n    }\n  }\n\n  protected def checkTimestampVectors(actual: ColumnVector, expected: ColumnVector): Unit = {\n    assert(actual.getSize === expected.getSize)\n    for (rowId <- 0 until actual.getSize) {\n      if (expected.isNullAt(rowId)) {\n        assert(actual.isNullAt(rowId), s\"Expected null at row $rowId\")\n      } else {\n        val expectedValue = getValueAsObject(expected, rowId).asInstanceOf[Long]\n        val actualValue = getValueAsObject(actual, rowId).asInstanceOf[Long]\n        assert(actualValue === expectedValue, s\"Unexpected value at row $rowId\")\n      }\n    }\n  }\n\n  protected def checkStringVectors(actual: ColumnVector, expected: ColumnVector): Unit = {\n    assert(actual.getDataType === StringType.STRING)\n    assert(actual.getDataType === expected.getDataType)\n    assert(actual.getSize === expected.getSize)\n    Seq.range(0, actual.getSize).foreach { rowId =>\n      assert(actual.isNullAt(rowId) === expected.isNullAt(rowId))\n      if (!actual.isNullAt(rowId)) {\n        assert(\n          actual.getString(rowId) === expected.getString(rowId),\n          s\"unexpected value at $rowId: \" +\n            s\"expected: ${expected.getString(rowId)} \" +\n            s\"actual: ${actual.getString(rowId)} \")\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/ImplicitCastExpressionSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.expressions\n\nimport io.delta.kernel.data.ColumnVector\nimport io.delta.kernel.defaults.internal.expressions.ImplicitCastExpression.canCastTo\nimport io.delta.kernel.defaults.utils.DefaultKernelTestUtils.getValueAsObject\nimport io.delta.kernel.defaults.utils.TestUtils\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ImplicitCastExpressionSuite extends AnyFunSuite with TestUtils {\n  private val allowedCasts: Set[(DataType, DataType)] = Set(\n    (ByteType.BYTE, ShortType.SHORT),\n    (ByteType.BYTE, IntegerType.INTEGER),\n    (ByteType.BYTE, LongType.LONG),\n    (ByteType.BYTE, FloatType.FLOAT),\n    (ByteType.BYTE, DoubleType.DOUBLE),\n    (ShortType.SHORT, IntegerType.INTEGER),\n    (ShortType.SHORT, LongType.LONG),\n    (ShortType.SHORT, FloatType.FLOAT),\n    (ShortType.SHORT, DoubleType.DOUBLE),\n    (IntegerType.INTEGER, LongType.LONG),\n    (IntegerType.INTEGER, FloatType.FLOAT),\n    (IntegerType.INTEGER, DoubleType.DOUBLE),\n    (LongType.LONG, FloatType.FLOAT),\n    (LongType.LONG, DoubleType.DOUBLE),\n    (FloatType.FLOAT, DoubleType.DOUBLE))\n\n  test(\"can cast to\") {\n    ALL_TYPES.foreach { fromType =>\n      ALL_TYPES.foreach { toType =>\n        assert(canCastTo(fromType, toType) ===\n          allowedCasts.contains((fromType, toType)))\n      }\n    }\n  }\n\n  allowedCasts.foreach { castPair =>\n    test(s\"eval cast expression: ${castPair._1} -> ${castPair._2}\") {\n      val fromType = castPair._1\n      val toType = castPair._2\n      val inputVector = testData(87, fromType, (rowId) => rowId % 7 == 0)\n      val outputVector = new ImplicitCastExpression(new Column(\"id\"), toType)\n        .eval(inputVector)\n      checkCastOutput(inputVector, toType, outputVector)\n    }\n  }\n\n  def testData(size: Int, dataType: DataType, nullability: (Int) => Boolean): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = dataType\n      override def getSize: Int = size\n      override def close(): Unit = {}\n      override def isNullAt(rowId: Int): Boolean = nullability(rowId)\n\n      override def getByte(rowId: Int): Byte = {\n        assert(dataType === ByteType.BYTE)\n        generateValue(rowId).toByte\n      }\n\n      override def getShort(rowId: Int): Short = {\n        assert(dataType === ShortType.SHORT)\n        generateValue(rowId).toShort\n      }\n\n      override def getInt(rowId: Int): Int = {\n        assert(dataType === IntegerType.INTEGER)\n        generateValue(rowId).toInt\n      }\n\n      override def getLong(rowId: Int): Long = {\n        assert(dataType === LongType.LONG)\n        generateValue(rowId).toLong\n      }\n\n      override def getFloat(rowId: Int): Float = {\n        assert(dataType === FloatType.FLOAT)\n        generateValue(rowId).toFloat\n      }\n\n      override def getDouble(rowId: Int): Double = {\n        assert(dataType === DoubleType.DOUBLE)\n        generateValue(rowId)\n      }\n    }\n  }\n\n  // Utility method to generate a value based on the rowId. Returned value is a double\n  // which the callers can cast to appropriate numerical type.\n  private def generateValue(rowId: Int): Double = rowId * 2.76 + 7623\n\n  private def checkCastOutput(input: ColumnVector, toType: DataType, output: ColumnVector): Unit = {\n    assert(input.getSize === output.getSize)\n    assert(toType === output.getDataType)\n    Seq.range(0, input.getSize).foreach { rowId =>\n      assert(input.isNullAt(rowId) === output.isNullAt(rowId))\n      assert(getValueAsObject(input, rowId) === getValueAsObject(output, rowId))\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/json/JsonUtilsSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.json\n\nimport scala.Double.NegativeInfinity\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.defaults.utils.{TestRow, TestUtils}\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass JsonUtilsSuite extends AnyFunSuite with TestUtils with VectorTestUtils {\n\n  // Tests for round trip of each data type\n  Seq(\n    (\n      BooleanType.BOOLEAN,\n      s\"\"\"{\"c0\":false,\"c1\":true,\"c2\":null,\"c3\":false}\"\"\", // test JSON\n      TestRow(false, true, null, false), // expected decoded row\n      // expected row serialized as JSON, null values won't be in output\n      s\"\"\"{\"c0\":false,\"c1\":true,\"c3\":false}\"\"\"),\n    (\n      ByteType.BYTE,\n      s\"\"\"{\"c0\":${Byte.MinValue},\"c1\":${Byte.MaxValue},\"c2\":null,\"c3\":4}\"\"\",\n      TestRow(Byte.MinValue, Byte.MaxValue, null, 4.toByte),\n      s\"\"\"{\"c0\":${Byte.MinValue},\"c1\":${Byte.MaxValue},\"c3\":4}\"\"\"),\n    (\n      ShortType.SHORT,\n      s\"\"\"{\"c0\":${Short.MinValue},\"c1\":${Short.MaxValue},\"c2\":null,\"c3\":44}\"\"\",\n      TestRow(Short.MinValue, Short.MaxValue, null, 44.toShort),\n      s\"\"\"{\"c0\":${Short.MinValue},\"c1\":${Short.MaxValue},\"c3\":44}\"\"\"),\n    (\n      IntegerType.INTEGER,\n      s\"\"\"{\"c0\":${Integer.MIN_VALUE},\"c1\":${Integer.MAX_VALUE},\"c2\":null,\"c3\":423423}\"\"\",\n      TestRow(Integer.MIN_VALUE, Integer.MAX_VALUE, null, 423423),\n      s\"\"\"{\"c0\":${Integer.MIN_VALUE},\"c1\":${Integer.MAX_VALUE},\"c3\":423423}\"\"\"),\n    (\n      LongType.LONG,\n      s\"\"\"{\"c0\":${Long.MinValue},\"c1\":${Long.MaxValue},\"c2\":null,\"c3\":423423}\"\"\",\n      TestRow(Long.MinValue, Long.MaxValue, null, 423423.toLong),\n      s\"\"\"{\"c0\":${Long.MinValue},\"c1\":${Long.MaxValue},\"c3\":423423}\"\"\"),\n    (\n      FloatType.FLOAT,\n      s\"\"\"{\"c0\":${Float.MinValue},\"c1\":${Float.MaxValue},\"c2\":null,\"c3\":\"${Float.NaN}\"}\"\"\",\n      TestRow(Float.MinValue, Float.MaxValue, null, Float.NaN),\n      s\"\"\"{\"c0\":${Float.MinValue},\"c1\":${Float.MaxValue},\"c3\":\"NaN\"}\"\"\"),\n    (\n      DoubleType.DOUBLE,\n      s\"\"\"{\"c0\":${Double.MinValue},\"c1\":${Double.MaxValue},\"c2\":null,\"c3\":\"${NegativeInfinity}\"}\"\"\",\n      TestRow(Double.MinValue, Double.MaxValue, null, NegativeInfinity),\n      s\"\"\"{\"c0\":${Double.MinValue},\"c1\":${Double.MaxValue},\"c3\":\"-Infinity\"}\"\"\"),\n    (\n      StringType.STRING,\n      s\"\"\"{\"c0\":\"\",\"c1\":\"ssdfsdf\",\"c2\":null,\"c3\":\"123sdsd\"}\"\"\",\n      TestRow(\"\", \"ssdfsdf\", null, \"123sdsd\"),\n      s\"\"\"{\"c0\":\"\",\"c1\":\"ssdfsdf\",\"c3\":\"123sdsd\"}\"\"\"),\n    (\n      new ArrayType(IntegerType.INTEGER, true /* containsNull */ ),\n      \"\"\"{\"c0\":[23,23],\"c1\":[1212,null,2332],\"c2\":null,\"c3\":[]}\"\"\",\n      TestRow(Seq(23, 23), Seq(1212, null, 2332), null, Seq()),\n      \"\"\"{\"c0\":[23,23],\"c1\":[1212,null,2332],\"c3\":[]}\"\"\"),\n    (\n      // array with complex element types\n      new ArrayType(\n        new StructType()\n          .add(\"cn0\", IntegerType.INTEGER)\n          .add(\"cn1\", new ArrayType(LongType.LONG, true /* containsNull */ )),\n        true /* containsNull */ ),\n      \"\"\"{\n        |\"c0\":[{\"cn0\":24,\"cn1\":[23,232]},{\"cn0\":25,\"cn1\":[24,237]}],\n        |\"c1\":[{\"cn0\":32,\"cn1\":[37,null,2323]},{\"cn0\":29,\"cn1\":[200,111237]}],\n        |\"c2\":null,\n        |\"c3\":[]}\"\"\".stripMargin,\n      TestRow(\n        Seq(TestRow(24, Seq(23L, 232L)), TestRow(25, Seq(24L, 237L))),\n        Seq(TestRow(32, Seq(37L, null, 2323L)), TestRow(29, Seq(200L, 111237L))),\n        null,\n        Seq()),\n      \"\"\"{\n        |\"c0\":[{\"cn0\":24,\"cn1\":[23,232]},{\"cn0\":25,\"cn1\":[24,237]}],\n        |\"c1\":[{\"cn0\":32,\"cn1\":[37,null,2323]},{\"cn0\":29,\"cn1\":[200,111237]}],\n        |\"c3\":[]}\"\"\".stripMargin),\n    (\n      new MapType(StringType.STRING, IntegerType.INTEGER, true /* valueContainsNull */ ),\n      \"\"\"{\n        |\"c0\":{\"24\":200,\"25\":201},\n        |\"c1\":{\"27\":null,\"25\":203},\n        |\"c2\":null,\n        |\"c3\":{}\n        |}\"\"\".stripMargin,\n      TestRow(\n        Map(\"24\" -> 200, \"25\" -> 201),\n        Map(\"27\" -> null, \"25\" -> 203),\n        null,\n        Map()),\n      \"\"\"{\n        |\"c0\":{\"24\":200,\"25\":201},\n        |\"c1\":{\"27\":null,\"25\":203},\n        |\"c3\":{}\n        |}\"\"\".stripMargin),\n    (\n      new StructType()\n        .add(\"cn0\", IntegerType.INTEGER)\n        .add(\"cn1\", new ArrayType(LongType.LONG, true /* containsNull */ )),\n      \"\"\"{\n        |\"c0\":{\"cn0\":24,\"cn1\":[23,232]},\n        |\"c1\":{\"cn0\":29,\"cn1\":[200,null,111237]},\n        |\"c2\":null,\n        |\"c3\":{}\n        |}\"\"\".stripMargin,\n      TestRow(\n        TestRow(24, Seq(23L, 232L)),\n        TestRow(29, Seq(200L, null, 111237L)),\n        null,\n        TestRow(null, null)),\n      \"\"\"{\n        |\"c0\":{\"cn0\":24,\"cn1\":[23,232]},\n        |\"c1\":{\"cn0\":29,\"cn1\":[200,null,111237]},\n        |\"c3\":{}\n        |}\"\"\".stripMargin)).foreach { case (dataType, testJson, expRow, expJson) =>\n    test(s\"JsonUtils.RowSerializer: $dataType\") {\n      val schema = new StructType(Seq.range(0, 4).map(colOrdinal =>\n        new StructField(s\"c$colOrdinal\", dataType, true)).asJava)\n\n      val actRow = JsonUtils.rowFromJson(testJson, schema)\n      checkAnswer(Seq(actRow), Seq(expRow))\n      assert(JsonUtils.rowToJson(actRow) === expJson.linesIterator.mkString)\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/logstore/LogStoreProviderSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.logstore\n\nimport io.delta.storage._\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LogStoreProviderSuite extends AnyFunSuite {\n\n  private val customLogStoreClassName = classOf[UserDefinedLogStore].getName\n\n  val hadoopConf = new Configuration()\n  Seq(\n    \"s3\" -> classOf[S3SingleDriverLogStore].getName,\n    \"s3a\" -> classOf[S3SingleDriverLogStore].getName,\n    \"s3n\" -> classOf[S3SingleDriverLogStore].getName,\n    \"hdfs\" -> classOf[HDFSLogStore].getName,\n    \"file\" -> classOf[HDFSLogStore].getName,\n    \"gs\" -> classOf[GCSLogStore].getName,\n    \"abfss\" -> classOf[AzureLogStore].getName,\n    \"abfs\" -> classOf[AzureLogStore].getName,\n    \"adl\" -> classOf[AzureLogStore].getName,\n    \"wasb\" -> classOf[AzureLogStore].getName,\n    \"wasbs\" -> classOf[AzureLogStore].getName).foreach { case (scheme, logStoreClass) =>\n    test(s\"get the default LogStore for scheme $scheme\") {\n      val logStore = LogStoreProvider.getLogStore(hadoopConf, scheme)\n      assert(logStore.getClass.getName === logStoreClass)\n    }\n  }\n\n  test(\"override the default LogStore for a schema\") {\n    val hadoopConf = new Configuration()\n    hadoopConf.set(LogStoreProvider.getLogStoreSchemeConfKey(\"s3\"), customLogStoreClassName)\n    val logStore = LogStoreProvider.getLogStore(hadoopConf, \"s3\")\n    assert(logStore.getClass.getName === customLogStoreClassName)\n  }\n\n  test(\"set LogStore config for a custom scheme\") {\n    val hadoopConf = new Configuration()\n    hadoopConf.set(LogStoreProvider.getLogStoreSchemeConfKey(\"fake\"), customLogStoreClassName)\n    val logStore = LogStoreProvider.getLogStore(hadoopConf, \"fake\")\n    assert(logStore.getClass.getName === customLogStoreClassName)\n  }\n\n  test(\"set LogStore config to a class that doesn't extend LogStore\") {\n    val hadoopConf = new Configuration()\n    hadoopConf.set(LogStoreProvider.getLogStoreSchemeConfKey(\"fake\"), \"java.lang.String\")\n    val e = intercept[IllegalArgumentException](\n      LogStoreProvider.getLogStore(hadoopConf, \"fake\"))\n    assert(e.getMessage.contains(\n      \"Can not instantiate `LogStore` class (from config): %s\".format(\"java.lang.String\")))\n  }\n}\n\n/**\n * Sample user-defined log store implementing [[LogStore]].\n */\nclass UserDefinedLogStore(override val initHadoopConf: Configuration)\n    extends LogStore(initHadoopConf) {\n\n  private val logStoreInternal = new HDFSLogStore(initHadoopConf)\n\n  override def read(path: Path, hadoopConf: Configuration): CloseableIterator[String] = {\n    logStoreInternal.read(path, hadoopConf)\n  }\n\n  override def write(\n      path: Path,\n      actions: java.util.Iterator[String],\n      overwrite: java.lang.Boolean,\n      hadoopConf: Configuration): Unit = {\n    logStoreInternal.write(path, actions, overwrite, hadoopConf)\n  }\n\n  override def listFrom(path: Path, hadoopConf: Configuration): java.util.Iterator[FileStatus] = {\n    logStoreInternal.listFrom(path, hadoopConf)\n  }\n\n  override def resolvePathOnPhysicalStorage(path: Path, hadoopConf: Configuration): Path = {\n    logStoreInternal.resolvePathOnPhysicalStorage(path, hadoopConf)\n  }\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): java.lang.Boolean = {\n    false\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/parquet/ParquetFileReaderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet\n\nimport java.math.BigDecimal\nimport java.util.TimeZone\n\nimport io.delta.golden.GoldenTableUtils.{goldenTableFile, goldenTablePath}\nimport io.delta.kernel.defaults.utils.{ExpressionTestUtils, TestRow}\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types._\nimport io.delta.kernel.utils.MetadataColumnTestUtils\n\nimport org.apache.spark.sql.internal.SQLConf\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.slf4j.LoggerFactory\n\nclass ParquetFileReaderSuite extends AnyFunSuite\n    with ParquetSuiteBase with VectorTestUtils with ExpressionTestUtils\n    with MetadataColumnTestUtils {\n  private val logger = LoggerFactory.getLogger(classOf[ParquetFileReaderSuite])\n\n  test(\"decimals encoded using dictionary encoding \") {\n    // Below golden tables contains three decimal columns\n    // each stored in a different physical format: int32, int64 and fixed binary\n    val decimalDictFileV1 = goldenTableFile(\"parquet-decimal-dictionaries-v1\").getAbsolutePath\n    val decimalDictFileV2 = goldenTableFile(\"parquet-decimal-dictionaries-v2\").getAbsolutePath\n\n    val expResult = (0 until 1000000).map { i =>\n      TestRow(i, BigDecimal.valueOf(i % 5), BigDecimal.valueOf(i % 6), BigDecimal.valueOf(i % 2))\n    }\n\n    val readSchema = tableSchema(decimalDictFileV1)\n\n    for (file <- Seq(decimalDictFileV1, decimalDictFileV2)) {\n      val actResult = readParquetFilesUsingKernel(file, readSchema)\n\n      checkAnswer(actResult, expResult)\n    }\n  }\n\n  test(\"large scale decimal type file\") {\n    val largeScaleDecimalTypesFile = goldenTableFile(\"parquet-decimal-type\").getAbsolutePath\n\n    def expand(n: BigDecimal): BigDecimal = {\n      n.scaleByPowerOfTen(5).add(n)\n    }\n\n    val expResult = (0 until 99998).map { i =>\n      if (i % 85 == 0) {\n        val n = BigDecimal.valueOf(i)\n        TestRow(i, n.movePointLeft(1).setScale(1), n.setScale(5), n.setScale(5))\n      } else {\n        val negation = if (i % 33 == 0) -1 else 1\n        val n = BigDecimal.valueOf(i * negation)\n        TestRow(\n          i,\n          n.movePointLeft(1),\n          expand(n).movePointLeft(5),\n          expand(expand(expand(n))).movePointLeft(5))\n      }\n    }\n\n    val readSchema = tableSchema(largeScaleDecimalTypesFile)\n\n    val actResult = readParquetFilesUsingKernel(largeScaleDecimalTypesFile, readSchema)\n\n    checkAnswer(actResult, expResult)\n  }\n\n  Seq(\n    \"parquet-all-types\",\n    \"parquet-all-types-legacy-format\").foreach { allTypesTableName =>\n    test(s\"read all types of data - $allTypesTableName\") {\n      val allTypesFile = goldenTableFile(allTypesTableName).getAbsolutePath\n      val readSchema = tableSchema(allTypesFile)\n\n      checkAnswer(\n        readParquetFilesUsingKernel(allTypesFile, readSchema), /* actual */\n        readParquetFilesUsingSpark(allTypesFile, readSchema) /* expected */ )\n    }\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Tests covering reading parquet values into a wider column type                              //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  /**\n   * Test case for reading a column using a given type.\n   * @param columnName Column to read from the file\n   * @param toType Read type to use. May be different from the actually Parquet type.\n   * @param expectedExpr Expression returning the expected value for each row in the file.\n   */\n  case class TestCase(columnName: String, toType: DataType, expectedExpr: Int => Any)\n\n  private val supportedConversions: Seq[TestCase] = Seq(\n    // 'ByteType' column was generated with overflowing values, we need to call i.toByte to also\n    // wrap around here and generate the correct expected values.\n    TestCase(\"ByteType\", ShortType.SHORT, i => if (i % 72 != 0) i.toByte.toShort else null),\n    TestCase(\"ByteType\", IntegerType.INTEGER, i => if (i % 72 != 0) i.toByte.toInt else null),\n    TestCase(\"ByteType\", LongType.LONG, i => if (i % 72 != 0) i.toByte.toLong else null),\n    TestCase(\"ByteType\", DoubleType.DOUBLE, i => if (i % 72 != 0) i.toByte.toDouble else null),\n    TestCase(\"ShortType\", IntegerType.INTEGER, i => if (i % 56 != 0) i else null),\n    TestCase(\"ShortType\", LongType.LONG, i => if (i % 56 != 0) i.toLong else null),\n    TestCase(\"ShortType\", DoubleType.DOUBLE, i => if (i % 56 != 0) i.toDouble else null),\n    TestCase(\"IntegerType\", LongType.LONG, i => if (i % 23 != 0) i.toLong else null),\n    TestCase(\"IntegerType\", DoubleType.DOUBLE, i => if (i % 23 != 0) i.toDouble else null),\n    TestCase(\n      \"FloatType\",\n      DoubleType.DOUBLE,\n      i => if (i % 28 != 0) (i * 0.234).toFloat.toDouble else null),\n    TestCase(\n      \"decimal\",\n      new DecimalType(12, 2),\n      i => if (i % 67 != 0) java.math.BigDecimal.valueOf(i * 12352, 2) else null),\n    TestCase(\n      \"decimal\",\n      new DecimalType(12, 4),\n      i => if (i % 67 != 0) java.math.BigDecimal.valueOf(i * 1235200, 4) else null),\n    TestCase(\n      \"decimal\",\n      new DecimalType(26, 10),\n      i =>\n        if (i % 67 != 0) java.math.BigDecimal.valueOf(i * 12352, 2).setScale(10)\n        else null),\n    TestCase(\n      \"IntegerType\",\n      new DecimalType(10, 0),\n      i => if (i % 23 != 0) new java.math.BigDecimal(i) else null),\n    TestCase(\n      \"IntegerType\",\n      new DecimalType(16, 4),\n      i => if (i % 23 != 0) new java.math.BigDecimal(i).setScale(4) else null),\n    TestCase(\n      \"LongType\",\n      new DecimalType(20, 0),\n      i => if (i % 25 != 0) new java.math.BigDecimal(i + 1) else null),\n    TestCase(\n      \"LongType\",\n      new DecimalType(28, 6),\n      i => if (i % 25 != 0) new java.math.BigDecimal(i + 1).setScale(6) else null),\n    TestCase(\"BinaryType\", StringType.STRING, i => if (i % 59 != 0) i.toString else null))\n\n  // The following conversions are supported by Kernel but not by Spark with parquet-mr.\n  // TODO: We should properly reject these conversions, a lot of them produce wrong results.\n  // Collecting them here to document the current behavior.\n  private val kernelOnlyConversions: Seq[TestCase] = Seq(\n    // This conversions will silently overflow.\n    TestCase(\"ShortType\", ByteType.BYTE, i => if (i % 56 != 0) i.toByte else null),\n    TestCase(\"IntegerType\", ByteType.BYTE, i => if (i % 23 != 0) i.toByte else null),\n    TestCase(\"IntegerType\", ShortType.SHORT, i => if (i % 23 != 0) i.toShort else null),\n\n    // This is reading the unscaled decimal value as long which is wrong.\n    TestCase(\"decimal\", LongType.LONG, i => if (i % 67 != 0) i.toLong * 12352 else null),\n\n    // The following conversions seem legit, although Spark rejects them.\n    TestCase(\"ByteType\", DateType.DATE, i => if (i % 72 != 0) i.toByte.toInt else null),\n    TestCase(\"ShortType\", DateType.DATE, i => if (i % 56 != 0) i else null),\n    TestCase(\"IntegerType\", DateType.DATE, i => if (i % 23 != 0) i else null),\n    TestCase(\"StringType\", BinaryType.BINARY, i => if (i % 57 != 0) i.toString.getBytes else null))\n\n  for (testCase <- supportedConversions ++ kernelOnlyConversions)\n    test(s\"parquet supported conversion - ${testCase.columnName} -> ${testCase.toType.toString}\") {\n      val inputLocation = goldenTablePath(\"parquet-all-types\")\n      val readSchema = new StructType().add(testCase.columnName, testCase.toType)\n      val result = readParquetFilesUsingKernel(inputLocation, readSchema)\n      val expected = (0 until 200)\n        .map { i => TestRow(testCase.expectedExpr(i)) }\n      checkAnswer(result, expected)\n\n      if (!kernelOnlyConversions.contains(testCase)) {\n        withSQLConf(SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> \"false\") {\n          val sparkResult = readParquetFilesUsingSpark(inputLocation, readSchema)\n          checkAnswer(result, sparkResult)\n        }\n      }\n    }\n\n  test(s\"parquet supported conversion - date -> timestamp_ntz\") {\n    val timezones =\n      Seq(\"UTC\", \"Iceland\", \"PST\", \"America/Los_Angeles\", \"Etc/GMT+9\", \"Asia/Beirut\", \"JST\")\n    for (fromTimezone <- timezones; toTimezone <- timezones) {\n      val inputLocation = goldenTablePath(s\"data-reader-date-types-$fromTimezone\")\n      TimeZone.setDefault(TimeZone.getTimeZone(toTimezone))\n\n      val readSchema = new StructType().add(\"date\", TimestampNTZType.TIMESTAMP_NTZ)\n      val result = readParquetFilesUsingKernel(inputLocation, readSchema)\n      // 1577836800000000L -> 2020-01-01 00:00:00 UTC\n      checkAnswer(result, Seq(TestRow(1577836800000000L)))\n    }\n  }\n\n  def checkParquetReadError(inputLocation: String, readSchema: StructType): Unit = {\n    val ex = intercept[Throwable] {\n      readParquetFilesUsingKernel(inputLocation, readSchema)\n    }\n\n    // We don't properly reject conversions and the error we get vary a lot, this checks various\n    // error message we may get as result.\n    // TODO(delta-io/delta#4493): Uniformize rejecting unsupported conversions.\n    assert(\n      ex.getMessage.contains(\"Can not read value\") ||\n        ex.getMessage.contains(\"column with Parquet type\") ||\n        ex.getMessage.contains(\"Unable to create Parquet converter for\") ||\n        ex.getMessage.contains(\"Found Delta type Decimal\") ||\n        ex.getMessage.contains(\"cannot be cast to\"))\n  }\n\n  for (\n    column <- Seq(\n      \"BooleanType\",\n      \"ByteType\",\n      \"ShortType\",\n      \"IntegerType\",\n      \"LongType\",\n      \"FloatType\",\n      \"DoubleType\",\n      \"StringType\",\n      \"BinaryType\")\n  ) {\n    test(s\"parquet unsupported conversion from $column\") {\n      val inputLocation = goldenTablePath(\"parquet-all-types\")\n      val supportedTypes = (supportedConversions ++ kernelOnlyConversions)\n        .filter(_.columnName == column)\n        .map(_.toType)\n      val unsupportedTypes = ALL_TYPES\n        .filterNot(supportedTypes.contains)\n        .filterNot(_.getClass.getSimpleName == column)\n\n      for (toType <- unsupportedTypes) {\n        val readSchema = new StructType().add(column, toType)\n        withClue(s\"Converting $column to $toType\") {\n          checkParquetReadError(inputLocation, readSchema)\n        }\n      }\n    }\n  }\n\n  test(s\"parquet unsupported conversion from decimal\") {\n    val inputLocation = goldenTablePath(\"parquet-all-types\")\n    // 'decimal' column is Decimal(10, 2) which fits into a long.\n    for (toType <- ALL_TYPES.filterNot(_ == LongType.LONG)) {\n      val readSchema = new StructType().add(\"decimal\", toType)\n      withClue(s\"Converting decimal to $toType\") {\n        checkParquetReadError(inputLocation, readSchema)\n      }\n    }\n  }\n\n  test(\"read subset of columns\") {\n    val tablePath = goldenTableFile(\"parquet-all-types\").getAbsolutePath\n    val readSchema = new StructType()\n      .add(\"byteType\", ByteType.BYTE)\n      .add(\"booleanType\", BooleanType.BOOLEAN)\n      .add(\"stringType\", StringType.STRING)\n      .add(\"dateType\", DateType.DATE)\n      .add(\n        \"nested_struct\",\n        new StructType()\n          .add(\"aa\", StringType.STRING)\n          .add(\"ac\", new StructType().add(\"aca\", IntegerType.INTEGER)))\n      .add(\"array_of_prims\", new ArrayType(IntegerType.INTEGER, true))\n\n    checkAnswer(\n      readParquetFilesUsingKernel(tablePath, readSchema), /* actual */\n      readParquetFilesUsingSpark(tablePath, readSchema) /* expected */ )\n  }\n\n  test(\"read subset of columns with missing columns in file\") {\n    val tablePath = goldenTableFile(\"parquet-all-types\").getAbsolutePath\n    val readSchema = new StructType()\n      .add(\"booleanType\", BooleanType.BOOLEAN)\n      .add(\"integerType\", IntegerType.INTEGER)\n      .add(\"missing_column_struct\", new StructType().add(\"ab\", IntegerType.INTEGER))\n      .add(\"longType\", LongType.LONG)\n      .add(\"missing_column_primitive\", DateType.DATE)\n      .add(\n        \"nested_struct\",\n        new StructType()\n          .add(\"aa\", StringType.STRING)\n          .add(\"ac\", new StructType().add(\"aca\", IntegerType.INTEGER)))\n\n    checkAnswer(\n      readParquetFilesUsingKernel(tablePath, readSchema), /* actual */\n      readParquetFilesUsingSpark(tablePath, readSchema) /* expected */ )\n  }\n\n  test(\"read columns with int96 timestamp_ntz\") {\n    // Spark doesn't support writing timestamp_NTZ as INT96 (although reads are)\n    // So we're reusing a pre-written file directly.\n    val filePath = getTestResourceFilePath(\"parquet/parquet-timestamp_ntz_int96.parquet\")\n    val readSchema = new StructType()\n      .add(\"id\", IntegerType.INTEGER)\n      .add(\"time\", TimestampNTZType.TIMESTAMP_NTZ)\n    checkAnswer(\n      readParquetFilesUsingKernel(filePath, readSchema), /* actual */\n      Seq(TestRow(1, 915181200000000L) /* expected */ ))\n  }\n\n  test(\"request row indices\") {\n    val readSchema = new StructType()\n      .add(\"id\", LongType.LONG)\n      .add(ROW_INDEX)\n\n    val path = getTestResourceFilePath(\"parquet-basic-row-indexes\")\n    val actResult1 = readParquetFilesUsingKernel(path, readSchema)\n    val expResult1 = (0L until 30L)\n      .map(i => TestRow(i, if (i < 10) i else if (i < 20) i - 10L else i - 20L))\n\n    checkAnswer(actResult1, expResult1)\n\n    // File with multiple row-groups [0, 20000) where rowIndex = id\n    val filePath = getTestResourceFilePath(\"parquet/row_index_multiple_row_groups.parquet\")\n    val actResult2 = readParquetFilesUsingKernel(filePath, readSchema)\n    val expResult2 = (0L until 20000L).map(i => TestRow(i, i))\n\n    checkAnswer(actResult2, expResult2)\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n  // Test compatibility with Parquet legacy format files                                         //\n  /////////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Test and the test file are copied from Spark's `ParquetThriftCompatibilitySuite`\n  test(\"read parquet file generated by parquet-thrift\") {\n    val parquetFilePath = getTestResourceFilePath(\"parquet/parquet-thrift-compat.snappy.parquet\")\n\n    val readSchema = new StructType()\n      .add(\"boolColumn\", BooleanType.BOOLEAN)\n      .add(\"byteColumn\", ByteType.BYTE)\n      .add(\"shortColumn\", ShortType.SHORT)\n      .add(\"intColumn\", IntegerType.INTEGER)\n      .add(\"longColumn\", LongType.LONG)\n      .add(\"doubleColumn\", DoubleType.DOUBLE)\n      // Thrift `BINARY` values are actually unencoded `STRING` values, and thus are always\n      // treated as `BINARY (UTF8)` in parquet-thrift, since parquet-thrift always assume\n      // Thrift `STRING`s are encoded using UTF-8.\n      .add(\"binaryColumn\", StringType.STRING)\n      .add(\"stringColumn\", StringType.STRING)\n      .add(\"enumColumn\", StringType.STRING)\n      // maybe indicates nullable columns, above ones are non-nullable\n      .add(\"maybeBoolColumn\", BooleanType.BOOLEAN)\n      .add(\"maybeByteColumn\", ByteType.BYTE)\n      .add(\"maybeShortColumn\", ShortType.SHORT)\n      .add(\"maybeIntColumn\", IntegerType.INTEGER)\n      .add(\"maybeLongColumn\", LongType.LONG)\n      .add(\"maybeDoubleColumn\", DoubleType.DOUBLE)\n      // Thrift `BINARY` values are actually unencoded `STRING` values, and thus are always\n      // treated as `BINARY (UTF8)` in parquet-thrift, since parquet-thrift always assume\n      // Thrift `STRING`s are encoded using UTF-8.\n      .add(\"maybeBinaryColumn\", StringType.STRING)\n      .add(\"maybeStringColumn\", StringType.STRING)\n      .add(\"maybeEnumColumn\", StringType.STRING)\n      // TODO: not working - separate PR to handle 2-level legacy lists\n      // .add(\"stringsColumn\", new ArrayType(StringType.STRING, true /* containsNull */))\n      // .add(\"intSetColumn\", new ArrayType(IntegerType.INTEGER, true /* containsNull */))\n      .add(\n        \"intToStringColumn\",\n        new MapType(IntegerType.INTEGER, StringType.STRING, true /* valueContainsNull */ ))\n    // TODO: not working - separate PR to handle 2-level legacy lists\n    // .add(\"complexColumn\", new MapType(\n    //  IntegerType.INTEGER,\n    //  new ArrayType(\n    //    new StructType()\n    //      .add(\"nestedIntsColumn\", new ArrayType(IntegerType.INTEGER, true /* containsNull */))\n    //      .add(\"nestedStringColumn\", StringType.STRING)\n    //      .add(\"stringColumn\", StringType.STRING),\n    //    true /* containsNull */),\n    //  true /* valueContainsNull */))\n\n    assert(parquetFileRowCount(parquetFilePath) === 10)\n    checkAnswer(\n      readParquetFilesUsingKernel(parquetFilePath, readSchema), /* actual */\n      readParquetFilesUsingSpark(parquetFilePath, readSchema) /* expected */ )\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/parquet/ParquetFileWriterSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet\n\nimport java.lang.{Double => DoubleJ, Float => FloatJ}\n\nimport io.delta.golden.GoldenTableUtils.{goldenTableFile, goldenTablePath}\nimport io.delta.kernel.data.{ColumnarBatch, FilteredColumnarBatch}\nimport io.delta.kernel.defaults.internal.DefaultKernelUtils\nimport io.delta.kernel.defaults.utils.{DefaultVectorTestUtils, ExpressionTestUtils, TestRow}\nimport io.delta.kernel.expressions.{Column, Literal, Predicate}\nimport io.delta.kernel.internal.TableConfig\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.internal.util.ColumnMapping.{convertToPhysicalSchema, ColumnMappingMode}\nimport io.delta.kernel.types._\nimport io.delta.kernel.utils.DataFileStatus\n\nimport org.apache.spark.sql.{functions => sparkfn}\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Test strategy for [[ParquetFileWriter]]\n * <p>\n * Golden tables already have Parquet files containing various supported\n * data types and variations (null, non-nulls, decimal types, nested nested types etc.).\n * We will use these files to simplify the tests for ParquetFileWriter. Alternative is to\n * generate the test data in the tests and try to write as Parquet files, but that would be a lot\n * of test code to cover all the combinations.\n * <p>\n * Using the golden Parquet files in combination with Kernel Parquet reader and Spark Parquet\n * reader we will reduce the test code and also test the inter-working of the Parquet writer with\n * the Parquet readers.\n * <p>\n * High level steps in the test:\n * 1) read data using the Kernel Parquet reader to generate the data in [[ColumnarBatch]]es\n * 2) Optional: filter the data from (1) and generate [[FilteredColumnarBatch]]es\n * 3) write the data back to new Parquet file(s) using the ParquetFileWriter that we are\n * testing. We will test the following variations:\n * 3.1) change target file size and stats collection columns etc.\n * 4) verification\n * 4.1) read the new Parquet file(s) using the Kernel Parquet reader and compare with (2)\n * 4.2) read the new Parquet file(s) using the Spark Parquet reader and compare with (2)\n * 4.3) verify the stats returned in (3) are correct using the Spark Parquet reader\n */\nclass ParquetFileWriterSuite extends AnyFunSuite\n    with ParquetSuiteBase with DefaultVectorTestUtils with ExpressionTestUtils {\n\n  Seq(\n    // Test cases reading and writing all types of data with or without stats collection\n    Seq((200, 67), (1024, 16), (1048576, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write all types (no stats)\", // test name\n          \"parquet-all-types\", // input table where the data is read and written\n          targetFileSize,\n          expParquetFileCount,\n          200, /* expected number of rows written to Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          Seq.empty[Column], // list of columns to collect stats as part of the Parquet file write\n          0 // how many columns have the stats collected from given list above\n        )\n    },\n    // Test cases reading and writing decimal types data with different precisions\n    // They trigger different paths in the Parquet writer as how decimal types are stored in Parquet\n    // based on the precision and scale.\n    Seq((1048576, 3), (2048576, 2)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write decimal all types (with stats)\", // test name\n          \"parquet-decimal-type\",\n          targetFileSize,\n          expParquetFileCount,\n          99998, /* expected number of rows written to Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          leafLevelPrimitiveColumns(\n            Seq.empty,\n            tableSchema(goldenTablePath(\"parquet-decimal-type\"))),\n          4 // how many columns have the stats collected from given list above\n        )\n    },\n    // Test cases reading and writing data with field ids. This is for column mapping mode ID.\n    Seq((1024, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write data with field ids (no stats)\", // test name\n          \"table-with-columnmapping-mode-id\",\n          targetFileSize,\n          expParquetFileCount,\n          6, /* expected number of rows written to Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          Seq.empty[Column], // list of columns to collect stats as part of the Parquet file write\n          0 // how many columns have the stats collected from given list above\n        )\n    },\n    // Test cases reading and writing only a subset of data passing a predicate.\n    Seq((200, 26), (1024, 6), (1048576, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write filtered all types (no stats)\", // test name\n          \"parquet-all-types\", // input table where the data is read and written\n          targetFileSize,\n          expParquetFileCount,\n          77, /* expected number of rows written to Parquet files */\n          // predicate for filtering what input rows to write to parquet files\n          Some(greaterThanOrEqual(col(\"ByteType\"), Literal.ofInt(50))),\n          Seq.empty[Column], // list of columns to collect stats as part of the Parquet file write\n          0 // how many columns have the stats collected from given list above\n        )\n    },\n    // Test cases reading and writing all types of data WITH stats collection\n    Seq((200, 67), (1024, 16), (1048576, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write all types (with stats for all leaf-level columns)\", // test name\n          \"parquet-all-types\", // input table where the data is read and written\n          targetFileSize,\n          expParquetFileCount,\n          200, /* expected number of rows written to Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          leafLevelPrimitiveColumns(Seq.empty, tableSchema(goldenTablePath(\"parquet-all-types\"))),\n          15 // how many columns have the stats collected from given list above\n        )\n    },\n    // Test cases reading and writing all types of data with a partial column set stats collection\n    Seq((200, 67), (1024, 16), (1048576, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write all types (with stats for a subset of leaf-level columns)\", // test name\n          \"parquet-all-types\", // input table where the data is read and written\n          targetFileSize,\n          expParquetFileCount,\n          200, /* expected number of rows written to Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          Seq(\n            new Column(\"ByteType\"),\n            new Column(\"DateType\"),\n            new Column(Array(\"nested_struct\", \"aa\")),\n            new Column(Array(\"nested_struct\", \"ac\", \"aca\")),\n            new Column(Array(\"nested_struct\", \"ac\")), // stats are not collected for struct types\n            new Column(\"nested_struct\"), // stats are not collected for struct types\n            new Column(\"array_of_prims\"), // stats are not collected for array types\n            new Column(\"map_of_prims\") // stats are not collected for map types\n          ),\n          4 // how many columns have the stats collected from given list above\n        )\n    },\n    // Decimal types with various precision and scales\n    Seq((10000, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write decimal various scales and precision (with stats)\", // test name\n          \"decimal-various-scale-precision\",\n          targetFileSize,\n          expParquetFileCount,\n          3, /* expected number of rows written to Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          leafLevelPrimitiveColumns(\n            Seq.empty,\n            tableSchema(goldenTablePath(\"decimal-various-scale-precision\"))),\n          29 // how many columns have the stats collected from given list above\n        )\n    },\n    // Read a iceberg compat v2 data with field ids and nested ids, and write it back\n    Seq((200, 1)).map {\n      case (targetFileSize, expParquetFileCount) =>\n        (\n          \"write iceberg compat v2 data with field ids (no stats)\", // test name\n          \"table-with-columnmapping-mode-id\", // input table where the data is read\n          targetFileSize,\n          expParquetFileCount,\n          6, /* input table has 6 rows, exp these in output Parquet files */\n          Option.empty[Predicate], // predicate for filtering what rows to write to parquet files\n          Seq.empty, // list of columns to collect statistics on\n          0 // how many columns have the stats collected from given list above\n        )\n    }).flatten.foreach {\n    case (name, input, fileSize, expFileCount, expRowCount, predicate, statsCols, expStatsColCnt) =>\n      test(s\"$name: targetFileSize=$fileSize, predicate=$predicate\") {\n        withTempDir { tempPath =>\n          val targetDir = tempPath.getAbsolutePath\n\n          val inputLocation = goldenTablePath(input)\n          val schema = tableSchema(inputLocation)\n\n          val hasColumnMappingId =\n            hasTableProperty(inputLocation, TableConfig.COLUMN_MAPPING_MODE.getKey, \"id\")\n          val hasIcebergCompatV2 =\n            hasTableProperty(inputLocation, TableConfig.ICEBERG_COMPAT_V2_ENABLED.getKey, \"true\")\n\n          val physicalSchema = if (hasColumnMappingId || hasIcebergCompatV2) {\n            convertToPhysicalSchema(schema, schema, ColumnMappingMode.ID)\n          } else {\n            schema\n          }\n\n          val dataToWrite =\n            readParquetUsingKernelAsColumnarBatches(inputLocation, physicalSchema) // read data\n              // Convert the schema of the data to the physical schema with field ids\n              .map(_.withNewSchema(physicalSchema))\n              // convert the data to filtered columnar batches\n              .map(_.toFiltered(predicate))\n\n          val writeOutput =\n            writeToParquetUsingKernel(dataToWrite, targetDir, fileSize, statsCols)\n\n          assert(parquetFileCount(targetDir) === expFileCount)\n          assert(parquetFileRowCount(targetDir) == expRowCount)\n\n          verifyContent(targetDir, dataToWrite)\n          if (hasIcebergCompatV2 || hasColumnMappingId) {\n            verifyFieldIds(targetDir, physicalSchema, hasIcebergCompatV2)\n          }\n          verifyStatsUsingSpark(targetDir, writeOutput, schema, statsCols, expStatsColCnt)\n        }\n      }\n  }\n\n  test(\"columnar batches containing different schema\") {\n    withTempDir { tempPath =>\n      val targetDir = tempPath.getAbsolutePath\n\n      // First batch with one column\n      val batch1 = columnarBatch(testColumnVector(10, IntegerType.INTEGER))\n\n      // Batch with two columns\n      val batch2 = columnarBatch(\n        testColumnVector(10, IntegerType.INTEGER),\n        testColumnVector(10, LongType.LONG))\n\n      // Batch with one column as first batch but different data type\n      val batch3 = columnarBatch(testColumnVector(10, LongType.LONG))\n\n      Seq(Seq(batch1, batch2), Seq(batch1, batch3)).foreach { dataToWrite =>\n        val e = intercept[IllegalArgumentException] {\n          writeToParquetUsingKernel(dataToWrite.map(_.toFiltered), targetDir)\n        }\n        assert(e.getMessage.contains(\"Input data has columnar batches with different schemas:\"))\n      }\n    }\n  }\n\n  /**\n   * Tests to cover floating point comparison special cases in Parquet.\n   * - https://issues.apache.org/jira/browse/PARQUET-1222\n   * - Parquet doesn't collect stats if NaN is present in the column values\n   * - Min is written as -0.0 instead of 0.0 and max is written as 0.0 instead of -0.0\n   */\n  test(\"float/double type column stats collection\") {\n    // Try writing different set of floating point values and verify the stats are correct\n    // (float values, double values, exp rowCount in files, exp stats (min, max, nullCount)\n    Seq(\n      ( // no stats collection as NaN is present\n        Seq(\n          Float.NegativeInfinity,\n          Float.MinValue,\n          -1.0f,\n          -0.0f,\n          0.0f,\n          1.0f,\n          null,\n          Float.MaxValue,\n          Float.PositiveInfinity,\n          Float.NaN),\n        Seq(\n          Double.NegativeInfinity,\n          Double.MinValue,\n          -1.0d,\n          -0.0d,\n          0.0d,\n          1.0d,\n          null,\n          Double.MaxValue,\n          Double.PositiveInfinity,\n          Double.NaN),\n        10,\n        (null, null, null),\n        (null, null, null)),\n      ( // Min and max are infinities\n        Seq(\n          Float.NegativeInfinity,\n          Float.MinValue,\n          -1.0f,\n          -0.0f,\n          0.0f,\n          1.0f,\n          null,\n          Float.MaxValue,\n          Float.PositiveInfinity),\n        Seq(\n          Double.NegativeInfinity,\n          Double.MinValue,\n          -1.0d,\n          -0.0d,\n          0.0d,\n          1.0d,\n          null,\n          Double.MaxValue,\n          Double.PositiveInfinity),\n        9,\n        (Float.NegativeInfinity, Float.PositiveInfinity, 1L),\n        (Double.NegativeInfinity, Double.PositiveInfinity, 1L)),\n      ( // no infinities or NaN - expect stats collected\n        Seq(Float.MinValue, -1.0f, -0.0f, 0.0f, 1.0f, null, Float.MaxValue),\n        Seq(Double.MinValue, -1.0d, -0.0d, 0.0d, 1.0d, null, Double.MaxValue),\n        7,\n        (Float.MinValue, Float.MaxValue, 1L),\n        (Double.MinValue, Double.MaxValue, 1L)),\n      ( // Only negative numbers. Max is 0.0 instead of -0.0 to avoid PARQUET-1222\n        Seq(Float.NegativeInfinity, Float.MinValue, -1.0f, -0.0f, null),\n        Seq(Double.NegativeInfinity, Double.MinValue, -1.0d, -0.0d, null),\n        5,\n        (Float.NegativeInfinity, 0.0f, 1L),\n        (Double.NegativeInfinity, 0.0d, 1L)),\n      ( // Only positive numbers. Min is  -0.0 instead of 0.0 to avoid PARQUET-1222\n        Seq(0.0f, 1.0f, null, Float.MaxValue, Float.PositiveInfinity),\n        Seq(0.0d, 1.0d, null, Double.MaxValue, Double.PositiveInfinity),\n        5,\n        (-0.0f, Float.PositiveInfinity, 1L),\n        (-0.0d, Double.PositiveInfinity, 1L))).foreach {\n      case (floats: Seq[FloatJ], doubles: Seq[DoubleJ], expRowCount, expFltStats, expDblStats) =>\n        withTempDir { tempPath =>\n          val targetDir = tempPath.getAbsolutePath\n          val testBatch = columnarBatch(floatVector(floats), doubleVector(doubles))\n          val dataToWrite = Seq(testBatch.toFiltered)\n\n          val writeOutput =\n            writeToParquetUsingKernel(\n              dataToWrite,\n              targetDir,\n              statsColumns = Seq(col(\"col_0\"), col(\"col_1\")))\n\n          assert(parquetFileRowCount(targetDir) == expRowCount)\n          verifyContent(targetDir, dataToWrite)\n\n          val stats = writeOutput.head.getStatistics.get()\n\n          def getStats(column: String): (Object, Object, Object) =\n            (\n              Option(stats.getMinValues.get(col(column))).map(_.getValue).orNull,\n              Option(stats.getMaxValues.get(col(column))).map(_.getValue).orNull,\n              Option(stats.getNullCount.get(col(column))).orNull)\n\n          assert(getStats(\"col_0\") === expFltStats)\n          assert(getStats(\"col_1\") === expDblStats)\n        }\n    }\n  }\n\n  test(s\"invalid target file size\") {\n    withTempDir { tempPath =>\n      val targetDir = tempPath.getAbsolutePath\n      val inputLocation = goldenTableFile(\"parquet-all-types\").toString\n      val schema = tableSchema(inputLocation)\n\n      val dataToWrite =\n        readParquetUsingKernelAsColumnarBatches(inputLocation, schema)\n          .map(_.toFiltered)\n\n      Seq(-1, 0).foreach { targetFileSize =>\n        val e = intercept[IllegalArgumentException] {\n          writeToParquetUsingKernel(dataToWrite, targetDir, targetFileSize)\n        }\n        assert(e.getMessage.contains(\"Invalid target Parquet file size: \" + targetFileSize))\n      }\n    }\n  }\n\n  def verifyStatsUsingSpark(\n      actualFileDir: String,\n      actualFileStatuses: Seq[DataFileStatus],\n      fileDataSchema: StructType,\n      statsColumns: Seq[Column],\n      expStatsColCount: Int): Unit = {\n\n    val actualStatsOutput = actualFileStatuses\n      .map { fileStatus =>\n        // validate there are no more the expected number of stats columns\n        assert(fileStatus.getStatistics.isPresent)\n        assert(fileStatus.getStatistics.get().getMinValues.size() === expStatsColCount)\n        assert(fileStatus.getStatistics.get().getMaxValues.size() === expStatsColCount)\n        assert(fileStatus.getStatistics.get().getNullCount.size() === expStatsColCount)\n\n        // Convert to TestRow for comparison with the actual values computing using Spark.\n        fileStatus.toTestRow(statsColumns)\n      }\n\n    // Use spark to fetch the stats from the parquet files use them as the expected statistics\n    // Compare them with the actual stats returned by the Kernel's Parquet writer.\n    val df = spark.read\n      .format(\"parquet\")\n      .parquet(actualFileDir)\n      .to(fileDataSchema.toSpark)\n      .select(\n        sparkfn.col(\"*\"), // select all columns from the parquet files\n        sparkfn.col(\"_metadata.file_path\").as(\"path\"), // select file path\n        sparkfn.col(\"_metadata.file_size\").as(\"size\"), // select file size\n        // select mod time and convert to millis\n        sparkfn.unix_timestamp(\n          sparkfn.col(\"_metadata.file_modification_time\")).as(\"modificationTime\"))\n      .groupBy(\"path\", \"size\", \"modificationTime\")\n\n    val nullStats = Seq(sparkfn.lit(null), sparkfn.lit(null), sparkfn.lit(null))\n\n    // Add the row count aggregation\n    val aggs = Seq(sparkfn.count(sparkfn.col(\"*\")).as(\"rowCount\")) ++\n      // add agg for each stats column to get min, max and null count\n      statsColumns\n        .flatMap { statColumn =>\n          val dataType = DefaultKernelUtils.getDataType(fileDataSchema, statColumn)\n          dataType match {\n            case _: StructType => nullStats // no concept of stats for struct types\n            case _: ArrayType => nullStats // no concept of stats for array types\n            case _: MapType => nullStats // no concept of stats for map types\n            case _ => // for all other types\n              val colName = statColumn.toPath\n              Seq(\n                sparkfn.min(colName).as(\"min_\" + colName),\n                sparkfn.max(colName).as(\"max_\" + colName),\n                sparkfn.sum(sparkfn.when(\n                  sparkfn.col(colName).isNull,\n                  1).otherwise(0)).as(\"nullCount_\" + colName))\n          }\n        }\n\n    val expectedStatsOutput = df.agg(aggs.head, aggs.tail: _*).collect().map(TestRow(_))\n\n    checkAnswer(actualStatsOutput, expectedStatsOutput)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/parquet/ParquetReaderPredicatePushdownSuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet\n\nimport java.nio.file.Files\nimport java.sql.Date\nimport java.util.Optional\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel.defaults.utils.{ExpressionTestUtils, TestRow}\nimport io.delta.kernel.expressions._\nimport io.delta.kernel.expressions.Literal.{ofBinary, ofBoolean, ofDate, ofDouble, ofFloat, ofInt, ofLong, ofNull, ofString}\nimport io.delta.kernel.internal.util.InternalUtils.daysSinceEpoch\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types.{IntegerType, StructType}\n\nimport org.apache.spark.sql.{types => sparktypes, Row}\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ParquetReaderPredicatePushdownSuite extends AnyFunSuite\n    with BeforeAndAfterAll with ParquetSuiteBase with VectorTestUtils with ExpressionTestUtils {\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // Test data generation and helper methods\n  //////////////////////////////////////////////////////////////////////////////////\n\n  var testParquetTable: String = \"\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    testParquetTable = Files.createTempDirectory(\"tempDir\").toString\n\n    // Generate a test Parquet file with 20 row groups. Each row group has 100 rows.\n    // Parquet-mr checks whether the current row group has reached the limit or for every 100 rows.\n    // We set the `parquet.block.size` to very low, so for every 100 rows, it will create a\n    // new row group.\n    val rows = Seq.range(0, 20).flatMap(i => generateRowsGroup(i))\n\n    val df = spark.createDataFrame(spark.sparkContext.parallelize(rows), testTableSchema)\n    withSQLConf(\"parquet.block.size\" -> 1.toString) {\n      df.repartition(1)\n        .orderBy(\"rowId\")\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(testParquetTable)\n    }\n  }\n\n  // test table schema\n  val testTableSchema: sparktypes.StructType = {\n    // These are the only supported column types in Parquet filter push down\n    def allTypesSchema(): Array[sparktypes.StructField] = {\n      Seq(\n        sparktypes.StructField(\"byteCol\", sparktypes.ByteType),\n        sparktypes.StructField(\"shortCol\", sparktypes.ShortType),\n        sparktypes.StructField(\"intCol\", sparktypes.IntegerType),\n        sparktypes.StructField(\"longCol\", sparktypes.LongType),\n        sparktypes.StructField(\"floatCol\", sparktypes.FloatType),\n        sparktypes.StructField(\"doubleCol\", sparktypes.DoubleType),\n        sparktypes.StructField(\"stringCol\", sparktypes.StringType),\n        // column with values that are truncated in stats\n        sparktypes.StructField(\"truncatedStringCol\", sparktypes.StringType),\n        sparktypes.StructField(\"binaryCol\", sparktypes.BinaryType),\n        sparktypes.StructField(\"truncatedBinaryCol\", sparktypes.BinaryType),\n        sparktypes.StructField(\"booleanCol\", sparktypes.BooleanType),\n        sparktypes.StructField(\"dateCol\", sparktypes.DateType)).toArray\n    }\n\n    // supported data type columns as top level columns\n    new sparktypes.StructType(allTypesSchema())\n      // supported data type columns as nested columns\n      .add(\"nested\", sparktypes.StructType(allTypesSchema()))\n      // row id to help with the test results verification\n      .add(\"rowId\", sparktypes.IntegerType)\n  }\n\n  private def generateRowsGroup(rowGroupIdx: Int): Seq[Row] = {\n    def values(rowId: Int): Seq[Any] = {\n      // One of the columns in each row group is all nulls or all non-nulls depending on\n      // the [[rowGroupIdx]]. This helps to verify the test results for `is null` and\n      // `is not null` pushdown\n      Seq(\n        // byteCol\n        if (rowGroupIdx == 0) null /* all nulls */\n        else if (rowGroupIdx == 11) rowId.byteValue() /* all non-nulls */\n        else (if (rowId % 72 != 0) rowId.byteValue() else null), /* mix of nulls and non-nulls */\n\n        // shortCol\n        if (rowGroupIdx == 1) null\n        else if (rowGroupIdx == 10) rowId.shortValue()\n        else (if (rowId % 56 != 0) rowId.shortValue() else null),\n\n        // intCol\n        if (rowGroupIdx == 2) null\n        else if (rowGroupIdx == 9) rowId\n        else (if (rowId % 23 != 0) rowId else null),\n\n        // longCol\n        if (rowGroupIdx == 3) null\n        else if (rowGroupIdx == 8) (rowId + 1).longValue()\n        else (if (rowId % 25 != 0) (rowId + 1).longValue() else null),\n\n        // floatCol\n        if (rowGroupIdx == 4) null\n        else if (rowGroupIdx == 7) (rowId + 0.125).floatValue()\n        else (if (rowId % 28 != 0) (rowId + 0.125).floatValue() else null),\n\n        // doubleCol\n        if (rowGroupIdx == 5) null\n        else if (rowGroupIdx == 6) (rowId + 0.000001).doubleValue()\n        else (if (rowId % 54 != 0) (rowId + 0.000001).doubleValue() else null),\n\n        // stringCol\n        if (rowGroupIdx == 6) null\n        else if (rowGroupIdx == 5) \"%05d\".format(rowId)\n        else (if (rowId % 57 != 0) \"%05d\".format(rowId) else null),\n\n        // truncatedStringCol - stats will be truncated as the value is too long\n        if (rowGroupIdx == 7) null\n        else if (rowGroupIdx == 4) \"%050d\".format(rowId)\n        else (if (rowId % 57 != 0) \"%050d\".format(rowId) else null),\n\n        // binaryCol\n        if (rowGroupIdx == 8) null\n        else if (rowGroupIdx == 3) \"%06d\".format(rowId).getBytes\n        else (if (rowId % 59 != 0) \"%06d\".format(rowId).getBytes else null),\n\n        // truncatedBinaryCol - stats will be truncated as the value is too long\n        if (rowGroupIdx == 9) null\n        else if (rowGroupIdx == 2) \"%060d\".format(rowId).getBytes\n        else (if (rowId % 59 != 0) \"%060d\".format(rowId).getBytes else null),\n\n        // booleanCol\n        if (rowGroupIdx == 10) null\n        else if (rowGroupIdx == 1) rowId % 2 == 0\n        // alternative between true and false for each row group\n        else (if (rowId % 29 != 0) rowGroupIdx % 2 == 0 else null),\n\n        // dateCol\n        if (rowGroupIdx == 11) null\n        else if (rowGroupIdx == 0) new Date(rowId * 86400000L)\n        else (if (rowId % 61 != 0) new Date(rowId * 86400000L) else null))\n    }\n\n    Seq.range(rowGroupIdx * 100, (rowGroupIdx + 1) * 100).map { rowId =>\n      Row.fromSeq(\n        values(rowId) ++ // top-level column values\n          Seq(\n            Row.fromSeq(values(rowId)), // nested column values\n            rowId // row id to help with the test results verification\n          ))\n    }\n  }\n\n  def generateExpData(rowGroupIndexes: Seq[Int]): Seq[TestRow] = {\n    spark.createDataFrame(\n      spark.sparkContext.parallelize(rowGroupIndexes.flatMap(i => generateRowsGroup(i))),\n      testTableSchema)\n      .collect\n      .map(TestRow(_))\n  }\n\n  private def readUsingKernel(tablePath: String, predicate: Predicate): Seq[TestRow] = {\n    val readSchema: StructType = tableSchema(testParquetTable)\n    readParquetFilesUsingKernel(tablePath, readSchema, Optional.of(predicate))\n  }\n\n  private def assertConvertedFilterIsEmpty(predicate: Predicate, tablePath: String): Unit = {\n    val parquetFileSchema =\n      parquetFiles(tablePath).map(_.getPath).map(footer(_)).head.getFileMetaData.getSchema\n\n    assert(\n      !ParquetFilterUtils.toParquetFilter(parquetFileSchema, predicate).isPresent,\n      \"Predicate should not be converted to Parquet filter\")\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////\n  // End-2-end tests\n  //////////////////////////////////////////////////////////////////////////////////\n\n  Seq(\n    // filter on int type column\n    (\n      eq(col(\"intCol\"), ofInt(20)), // top-level column\n      eq(col(\"nested\", \"intCol\"), ofInt(20)), // nested column\n      Seq(0) // expected row groups\n    ),\n    // filter on long type column\n    (\n      gt(col(\"longCol\"), ofLong(1600)),\n      gt(col(\"nested\", \"longCol\"), ofLong(1600)),\n      Seq(16, 17, 18, 19) // expected row groups\n    ),\n    // filter on float type column\n    (\n      lt(col(\"floatCol\"), ofFloat(1000.0f)),\n      lt(col(\"nested\", \"floatCol\"), ofFloat(1000.0f)),\n      Seq(0, 1, 2, 3, 5, 6, 7, 8, 9) // expected row groups - row group 4 has all nulls\n    ),\n    // filter on double type column\n    (\n      gt(col(\"doubleCol\"), ofDouble(1000.0)),\n      gt(col(\"nested\", \"doubleCol\"), ofDouble(1000.0)),\n      Seq(10, 11, 12, 13, 14, 15, 16, 17, 18, 19) // expected row groups\n    ),\n    // filter on boolean type column\n    (\n      eq(col(\"booleanCol\"), ofBoolean(true)),\n      eq(col(\"nested\", \"booleanCol\"), ofBoolean(true)),\n      // expected row groups\n      // 1 has mix of true/false (included), 10 has all nulls (not included)\n      Seq(0, 1, 2, 4, 6, 8, 12, 14, 16, 18)),\n    // filter on date type column\n    (\n      lte(\n        col(\"dateCol\"),\n        ofDate(\n          daysSinceEpoch(new Date(500 * 86400000L /* millis in a day */ )))),\n      lte(\n        col(\"nested\", \"dateCol\"),\n        ofDate(\n          daysSinceEpoch(new Date(500 * 86400000L /* millis in a day */ )))),\n      Seq(0, 1, 2, 3, 4, 5) // expected row groups\n    ),\n    // filter on string type column\n    (\n      eq(col(\"stringCol\"), ofString(\"%05d\".format(300))),\n      eq(col(\"nested\", \"stringCol\"), ofString(\"%05d\".format(300))),\n      Seq(3) // expected row groups\n    ),\n    // filter on binary type column\n    (\n      gte(col(\"binaryCol\"), ofBinary(\"%06d\".format(1700).getBytes)),\n      gte(col(\"nested\", \"binaryCol\"), ofBinary(\"%06d\".format(1700).getBytes)),\n      Seq(17, 18, 19) // expected row groups\n    ),\n    // filter on truncated stats string type column\n    (\n      gte(col(\"truncatedStringCol\"), ofString(\"%050d\".format(300))),\n      gte(col(\"nested\", \"truncatedStringCol\"), ofString(\"%050d\".format(300))),\n      // expected row groups\n      Seq(3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) // 7 has all nulls\n    ),\n    // filter on truncated stats binary type column\n    (\n      lte(col(\"truncatedBinaryCol\"), ofBinary(\"%060d\".format(600).getBytes)),\n      lte(col(\"nested\", \"truncatedBinaryCol\"), ofBinary(\"%060d\".format(600).getBytes)),\n      Seq(0, 1, 2, 3, 4, 5, 6) // expected row groups\n    )).foreach {\n    // boolean, int32, data, int64, float, double, binary, string\n    // Test table has 20 row groups, each with 100 rows.\n    case (predicateTopLevelCol, predicateNestedCol, expRowGroups) =>\n      Seq(predicateTopLevelCol, predicateNestedCol).foreach { predicate =>\n        test(s\"filter pushdown: $predicate\") {\n          val actualData = readUsingKernel(testParquetTable, predicate)\n          val expOutputRowCount = expRowGroups.length * 100 // 100 rows per row group\n          assert(actualData.size === expOutputRowCount, s\"predicate: $predicate\")\n          checkAnswer(actualData, generateExpData(expRowGroups))\n        }\n      }\n  }\n\n  // IS NULL and IS NOT NULL tests\n  Seq(\n    // (columnName, row groups with all nulls, row groups with all non-nulls)\n    (\"byteCol\", Seq(0), Seq(11)), // int type column\n    (\"shortCol\", Seq(1), Seq(10)), // short type column\n    (\"intCol\", Seq(2), Seq(9)), // int type column\n    (\"longCol\", Seq(3), Seq(8)), // long type column\n    (\"floatCol\", Seq(4), Seq(7)), // float type column\n    (\"doubleCol\", Seq(5), Seq(6)), // double type column\n    (\"stringCol\", Seq(6), Seq(5)), // string type column\n    (\"truncatedStringCol\", Seq(7), Seq(4)), // truncatedStringCol type column\n    (\"binaryCol\", Seq(8), Seq(3)), // binary type column\n    (\"truncatedBinaryCol\", Seq(9), Seq(2)), // truncatedBinaryCol type column\n    (\"booleanCol\", Seq(10), Seq(1)), // boolean type column\n    (\"dateCol\", Seq(11), Seq(0)) // date type column\n  ).foreach {\n    // Test table has 20 row groups, each with 100 rows.\n    case (colName, allNullsRowGroups, allNonNullsRowGroups) =>\n      // Test predicate on both top-level and nested columns\n      Seq(col(colName), col(\"nested\", colName)).foreach { column =>\n        val isNullFilter = isNull(column)\n        test(s\"filter pushdown: $isNullFilter\") {\n          val actualData = readUsingKernel(testParquetTable, isNullFilter)\n          val expOutputRowCount = 100 * (20 - 1) // 100 rows per row group\n\n          // we get everything expect the rowgroup that has all non-nulls\n          val expRowGroups = (0 until 20).filter(!allNonNullsRowGroups.contains(_))\n          assert(actualData.size === expOutputRowCount, s\"predicate: $isNullFilter\")\n          checkAnswer(actualData, generateExpData(expRowGroups))\n\n          // not (col is null) should return all row groups exception the one with all nulls\n          assertNot(isNullFilter, (0 until 20).filter(!allNullsRowGroups.contains(_)))\n        }\n\n        val isNotNullFilter = isNotNull(column)\n        test(s\"filter pushdown: $isNotNullFilter\") {\n          val actualData = readUsingKernel(testParquetTable, isNotNullFilter)\n          val expOutputRowCount = 100 * (20 - 1) // 100 rows per row group\n\n          // we get everything expect the rowgroup that has all nulls\n          val expRowGroups = (0 until 20).filter(!allNullsRowGroups.contains(_))\n          assert(actualData.size === expOutputRowCount, s\"predicate: $isNotNullFilter\")\n          checkAnswer(actualData, generateExpData(expRowGroups))\n\n          // not (col is not null) should return all row groups exception the one with all non-nulls\n          assertNot(isNotNullFilter, (0 until 20).filter(!allNonNullsRowGroups.contains(_)))\n        }\n      }\n  }\n\n  test(\"for a column that doesn't exist in the table\") {\n    val testPredicate = predicate(\"=\", col(\"nonExistentCol\"), ofInt(20))\n    assertConvertedFilterIsEmpty(testPredicate, testParquetTable)\n\n    val actData = readUsingKernel(testParquetTable, testPredicate)\n    // contains all the data in the table as the predicate is not pushed down\n    checkAnswer(actData, generateExpData(Seq.range(0, 20)))\n  }\n\n  test(\"literal and column are swapped\") {\n    val testPredicate = predicate(\"=\", ofInt(20), col(\"intCol\"))\n    val actData = readUsingKernel(testParquetTable, testPredicate)\n    checkAnswer(actData, generateExpData(Seq(0)))\n  }\n\n  test(\"comparator literal value is null\") {\n    val testPredicate = predicate(\"=\", col(\"intCol\"), ofNull(IntegerType.INTEGER))\n    assertConvertedFilterIsEmpty(testPredicate, testParquetTable)\n\n    val actData = readUsingKernel(testParquetTable, testPredicate)\n    // contains all the data in the table as the predicate is not pushed down\n    checkAnswer(actData, generateExpData(Seq.range(0, 20)))\n  }\n\n  test(\"comparator that compare column and column\") {\n    val testPredicate = predicate(\"=\", col(\"intCol\"), col(\"longCol\"))\n    assertConvertedFilterIsEmpty(testPredicate, testParquetTable)\n\n    val actData = readUsingKernel(testParquetTable, testPredicate)\n    // contains all the data in the table as the predicate is not pushed down\n    checkAnswer(actData, generateExpData(Seq.range(0, 20)))\n  }\n\n  test(\"comparator that compare literal and literal\") {\n    val testPredicate = predicate(\"=\", ofInt(20), ofInt(20))\n    assertConvertedFilterIsEmpty(testPredicate, testParquetTable)\n\n    val actData = readUsingKernel(testParquetTable, testPredicate)\n    // contains all the data in the table as the predicate is not pushed down\n    checkAnswer(actData, generateExpData(Seq.range(0, 20)))\n  }\n\n  test(\"OR support\") {\n    val predicate = or(\n      eq(col(\"intCol\"), ofInt(20)),\n      eq(col(\"longCol\"), ofLong(1600)))\n    val actData = readUsingKernel(testParquetTable, predicate)\n    checkAnswer(actData, generateExpData(Seq(0, 15)))\n  }\n\n  test(\"one end of the OR is not convertible\") {\n    val predicate = or(\n      eq(col(\"intCol\"), ofInt(1599)),\n      eq(col(\"nonExistentCol\"), ofInt(1600)))\n    assertConvertedFilterIsEmpty(predicate, testParquetTable)\n\n    val actData = readUsingKernel(testParquetTable, predicate)\n    // contains all the data in the table as the predicate is not pushed down\n    checkAnswer(actData, generateExpData(Seq.range(0, 20)))\n  }\n\n  test(\"AND support\") {\n    val predicate = and(\n      eq(col(\"intCol\"), ofInt(1599)),\n      eq(col(\"longCol\"), ofLong(1600)))\n    val actData = readUsingKernel(testParquetTable, predicate)\n    checkAnswer(actData, generateExpData(Seq(15)))\n  }\n\n  test(\"one end of the AND is not convertible\") {\n    val predicate = and(\n      eq(col(\"intCol\"), ofInt(1599)),\n      eq(col(\"nonExistentCol\"), ofInt(1600)))\n    val actData = readUsingKernel(testParquetTable, predicate)\n    checkAnswer(actData, generateExpData(Seq(15)))\n  }\n\n  test(\"not support on gt\") {\n    val predicate = not(gt(col(\"intCol\"), ofInt(950)))\n    val actData = readUsingKernel(testParquetTable, predicate)\n\n    // rowgroups until 9 could have values <= 950\n    // rowgroup 2 has all nulls, so it won't be included in the result\n    val expRowGroups = Seq(0, 1, 3, 4, 5, 6, 7, 8, 9)\n    val expOutputRowCount = expRowGroups.length * 100 // 100 rows per row group\n    assert(actData.size === expOutputRowCount, s\"predicate: $predicate\")\n\n    checkAnswer(actData, generateExpData(expRowGroups))\n  }\n\n  test(\"not support on equality\") {\n    val predicate = not(eq(col(\"longCol\"), ofLong(768)))\n    val actData = readUsingKernel(testParquetTable, predicate)\n    // rowgroup 3 has all nulls, so it will be included in the results as\n    // Parquet equality filter is not null safe\n    // every other group has value that is not 768\n    checkAnswer(actData, generateExpData(Seq.range(0, 20)))\n  }\n\n  test(\"doesn't work on the repeated columns\") {\n    val testTable = goldenTablePath(\"parquet-all-types\")\n    val readSchema = tableSchema(testTable)\n\n    val predicate = eq(col(\"array_of_prims\"), ofInt(20))\n    assertConvertedFilterIsEmpty(predicate, testTable)\n\n    val actResult = readParquetFilesUsingKernel(testTable, readSchema, Optional.of(predicate))\n    val expResult = readParquetFilesUsingSpark(testTable, readSchema)\n\n    checkAnswer(actResult, expResult)\n  }\n\n  /** Test the `not(predicate)` returns expected rowgroups */\n  private def assertNot(predicate: Predicate, expRowGroups: Seq[Int]): Unit = {\n    val notPredicate = not(predicate)\n    val actualData = readUsingKernel(testParquetTable, notPredicate)\n    val expOutputRowCount = expRowGroups.length * 100 // 100 rows per row group\n    assert(actualData.size === expOutputRowCount, s\"predicate: $notPredicate\")\n    checkAnswer(actualData, generateExpData(expRowGroups))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/parquet/ParquetSchemaUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet\n\nimport io.delta.kernel.defaults.internal.parquet.ParquetSchemaUtils.pruneSchema\nimport io.delta.kernel.defaults.utils.TestUtils\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.internal.util.ColumnMapping.PARQUET_FIELD_NESTED_IDS_METADATA_KEY\nimport io.delta.kernel.types.{ArrayType, DoubleType, FieldMetadata, MapType, StructType}\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.LongType.LONG\n\nimport org.apache.parquet.schema.MessageTypeParser\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ParquetSchemaUtilsSuite extends AnyFunSuite with TestUtils {\n  // Test parquet schema type containing different types of columns with field ids\n  private val testParquetFileSchema = MessageTypeParser.parseMessageType(\n    \"\"\"message fileSchema {\n      |  required group f0 = 1 {\n      |    optional int32 f00 = 2;\n      |    optional int64 f01 = 3;\n      |  }\n      |  optional group f1 = 4 {\n      |    repeated group list = 5 {\n      |      optional int32 element = 6;\n      |    }\n      |  }\n      |  required group f2 (MAP) = 7 {\n      |    repeated group key_value = 8 {\n      |      required group key = 9 {\n      |        required int32 key_f0 = 10;\n      |        required int64 key_f1 = 11;\n      |      }\n      |      required int32 value = 12;\n      |    }\n      |  }\n      |  optional double f3 = 13;\n      |}\n      \"\"\".stripMargin)\n\n  // Delta schema corresponding to the above test [[parquetSchema]]\n  private val testParquetFileDeltaSchema = new StructType()\n    .add(\n      \"f0\",\n      new StructType()\n        .add(\"f00\", INTEGER, fieldMetadata(2))\n        .add(\"f01\", LONG, fieldMetadata(3)),\n      fieldMetadata(1))\n    .add(\"f1\", new ArrayType(INTEGER, false), fieldMetadata(4))\n    .add(\n      \"f2\",\n      new MapType(\n        new StructType()\n          .add(\"key_f0\", INTEGER, fieldMetadata(10))\n          .add(\"key_f1\", INTEGER, fieldMetadata(11)),\n        INTEGER,\n        false),\n      fieldMetadata(7))\n    .add(\"f3\", DoubleType.DOUBLE, fieldMetadata(13))\n\n  test(\"id mapping mode - delta reads all columns in the parquet file\") {\n    val prunedParquetSchema = pruneSchema(testParquetFileSchema, testParquetFileDeltaSchema)\n    assert(prunedParquetSchema === testParquetFileSchema)\n  }\n\n  test(\"id mapping mode - delta selects a subset of columns in the parquet file\") {\n    val readDeltaSchema = new StructType()\n      .add(testParquetFileDeltaSchema.get(\"f1\"))\n      .add( // nested column pruning\n        \"f0\",\n        new StructType()\n          .add(\"f00\", INTEGER, fieldMetadata(2)),\n        fieldMetadata(1))\n\n    val expectedParquetSchema = MessageTypeParser.parseMessageType(\n      \"\"\"message fileSchema {\n        |  optional group f1 = 4 {\n        |    repeated group list = 5 {\n        |      optional int32 element = 6;\n        |    }\n        |  }\n        |  required group f0 = 1 {\n        |    optional int32 f00 = 2;\n        |  }\n        |}\n        \"\"\".stripMargin)\n\n    val prunedParquetSchema = pruneSchema(testParquetFileSchema, readDeltaSchema)\n    assert(prunedParquetSchema === expectedParquetSchema)\n  }\n\n  test(\"id mapping mode - delta tries to read a column not present in the parquet file\") {\n    val readDeltaSchema = new StructType()\n      .add(testParquetFileDeltaSchema.get(\"f1\"))\n      .add( // nested column has extra column that is not present in the file\n        \"f0\",\n        new StructType()\n          .add(\"f00\", INTEGER, fieldMetadata(2))\n          .add(\"f02\", INTEGER, fieldMetadata(15)),\n        fieldMetadata(1))\n      .add(\"f4\", INTEGER, fieldMetadata(14))\n\n    // pruned parquet file schema shouldn't have the column \"f4\"\n    val expectedParquetSchema = MessageTypeParser.parseMessageType(\n      \"\"\"message fileSchema {\n        |  optional group f1 = 4 {\n        |    repeated group list = 5 {\n        |      optional int32 element = 6;\n        |    }\n        |  }\n        |  required group f0 = 1 {\n        |    optional int32 f00 = 2;\n        |  }\n        |}\n        \"\"\".stripMargin)\n\n    val prunedParquetSchema = pruneSchema(testParquetFileSchema, readDeltaSchema)\n    assert(prunedParquetSchema === expectedParquetSchema)\n  }\n\n  test(\"id mapping mode - combination of columns with and w/o field ids in delta read schema\") {\n    val readDeltaSchema = new StructType()\n      .add(testParquetFileDeltaSchema.get(\"f1\")) // with field id\n      .add( // nested column has extra column that is not present in the file\n        \"f0\",\n        new StructType()\n          .add(\"F00\", INTEGER) // no field id and with case-insensitive column name\n          .add(\"f01\", INTEGER, fieldMetadata(3))\n        // no field id for struct f0\n      )\n\n    val expectedParquetSchema = MessageTypeParser.parseMessageType(\n      \"\"\"message fileSchema {\n        |  optional group f1 = 4 {\n        |    repeated group list = 5 {\n        |      optional int32 element = 6;\n        |    }\n        |  }\n        |  required group f0 = 1 {\n        |    optional int32 f00 = 2;\n        |    optional int64 f01 = 3;\n        |  }\n        |}\n        \"\"\".stripMargin)\n\n    val prunedParquetSchema = pruneSchema(testParquetFileSchema, readDeltaSchema)\n    assert(prunedParquetSchema === expectedParquetSchema)\n  }\n\n  test(\"id mapping mode - field id matches but not the column name\") {\n    val readDeltaSchema = new StructType()\n      // physical name in the file is f3, but the same field id\n      .add(\"f3_new\", DoubleType.DOUBLE, fieldMetadata(13))\n      .add(\n        \"f0\",\n        new StructType()\n          // physical name in the file is f00, but the same field id\n          .add(\"f00_new\", INTEGER, fieldMetadata(2)),\n        fieldMetadata(1))\n\n    val expectedParquetSchema = MessageTypeParser.parseMessageType(\n      \"\"\"message fileSchema {\n        |  optional double f3 = 13;\n        |  required group f0 = 1 {\n        |    optional int32 f00 = 2;\n        |  }\n        |}\n        \"\"\".stripMargin)\n\n    val prunedParquetSchema = pruneSchema(testParquetFileSchema, readDeltaSchema)\n    assert(prunedParquetSchema === expectedParquetSchema)\n  }\n\n  test(\"id mapping mode - duplicate id in file at the same level throws error\") {\n    val readDeltaSchema = new StructType()\n      .add(\"f3\", DoubleType.DOUBLE, fieldMetadata(13))\n\n    val testParquetFileSchema = MessageTypeParser.parseMessageType(\n      \"\"\"message fileSchema {\n        |  optional double f3 = 13;\n        |  optional double f4 = 13;\n        |}\n        \"\"\".stripMargin)\n\n    val ex = intercept[Exception] {\n      pruneSchema(testParquetFileSchema, readDeltaSchema)\n    }\n    assert(ex.getMessage.contains(\n      \"Parquet file contains multiple columns (optional double f3 = 13, \" +\n        \"optional double f4 = 13) with the same field id\"))\n  }\n\n  test(\"id mapping mode - duplicate id in file at the same nested level throws error\") {\n    val readDeltaSchema = new StructType()\n      .add(\n        \"f0\",\n        new StructType()\n          .add(\"f00\", INTEGER, fieldMetadata(2)),\n        fieldMetadata(1))\n\n    val testParquetFileSchema = MessageTypeParser.parseMessageType(\n      \"\"\"message fileSchema {\n        |  required group f0 = 1 {\n        |    optional int32 f00 = 2;\n        |    optional int64 f01 = 3;\n        |    optional int64 f02 = 2;\n        |  }\n        |}\n        \"\"\".stripMargin)\n\n    val ex = intercept[Exception] {\n      pruneSchema(testParquetFileSchema, readDeltaSchema)\n    }\n    assert(ex.getMessage.contains(\n      \"Parquet file contains multiple columns (optional int32 f00 = 2, \" +\n        \"optional int64 f02 = 2) with the same field id\"))\n  }\n\n  // icebergCompatV2 tests - nested field ids are converted correctly to parquet schema\n  Seq(\n    (\n      \"struct with array and map\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(2, (\"f00.element\", 3)))\n            .add(\n              \"f01\",\n              new MapType(INTEGER, INTEGER, true),\n              fieldMetadata(4, (\"f01.key\", 5), (\"f01.value\", 6))),\n          fieldMetadata(1)),\n      // Expected parquet schema\n      MessageTypeParser.parseMessageType(\n        \"\"\"message DefaultKernelSchema {\n          |  optional group f0 = 1 {\n          |    optional group f00 (LIST) = 2 {\n          |      repeated group list {\n          |        required int64 element = 3;\n          |      }\n          |    }\n          |    optional group f01 (MAP) = 4 {\n          |      repeated group key_value {\n          |        required int32 key = 5;\n          |        optional int32 value = 6;\n          |      }\n          |    }\n          |  }\n          |}\"\"\".stripMargin)),\n    (\n      \"top-level array and map columns\",\n      // Delta schema - input\n      new StructType()\n        .add(\"f1\", new ArrayType(INTEGER, true), fieldMetadata(1, (\"f1.element\", 2)))\n        .add(\n          \"f2\",\n          new MapType(\n            new StructType()\n              .add(\"key_f0\", INTEGER, fieldMetadata(6))\n              .add(\"key_f1\", INTEGER, fieldMetadata(7)),\n            INTEGER,\n            true),\n          fieldMetadata(3, (\"f2.key\", 4), (\"f2.value\", 5))),\n      // Expected parquet schema\n      MessageTypeParser.parseMessageType(\"\"\"message DefaultKernelSchema {\n        |  optional group f1 (LIST) = 1 {\n        |    repeated group list {\n        |      optional int32 element = 2;\n        |    }\n        |  }\n        |  optional group f2 (MAP) = 3 {\n        |    repeated group key_value {\n        |      required group key = 4 {\n        |        optional int32 key_f0 = 6;\n        |        optional int32 key_f1 = 7;\n        |      }\n        |      optional int32 value = 5;\n        |    }\n        |  }\n        |}\"\"\".stripMargin)),\n    (\n      \"array/map inside array/map\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f3\",\n          new ArrayType(new ArrayType(INTEGER, false), false),\n          fieldMetadata(0, (\"f3.element\", 1), (\"f3.element.element\", 2)))\n        .add(\n          \"f4\",\n          new MapType(\n            new MapType(\n              new StructType()\n                .add(\"key_f0\", INTEGER, fieldMetadata(3))\n                .add(\"key_f1\", INTEGER, fieldMetadata(4)),\n              INTEGER,\n              false),\n            INTEGER,\n            false),\n          fieldMetadata(5, (\"f4.key\", 6), (\"f4.value\", 7), (\"f4.key.key\", 8), (\"f4.key.value\", 9))),\n      // Expected parquet schema\n      MessageTypeParser.parseMessageType(\"\"\"message DefaultKernelSchema {\n        |  optional group f3 (LIST) = 0 {\n        |    repeated group list {\n        |      required group element (LIST) = 1 {\n        |        repeated group list {\n        |          required int32 element = 2;\n        |        }\n        |      }\n        |    }\n        |  }\n        |  optional group f4 (MAP) = 5 {\n        |    repeated group key_value {\n        |      required group key (MAP) = 6 {\n        |        repeated group key_value {\n        |          required group key = 8 {\n        |            optional int32 key_f0 = 3;\n        |            optional int32 key_f1 = 4;\n        |          }\n        |          required int32 value = 9;\n        |        }\n        |      }\n        |      required int32 value = 7;\n        |    }\n        |  }\n        |}\"\"\".stripMargin))).foreach { case (testName, deltaSchema, expectedParquetSchema) =>\n    test(s\"icebergCompatV2 - nested fields are converted to parquet schema - $testName\") {\n      val actParquetSchema = ParquetSchemaUtils.toParquetSchema(deltaSchema)\n      assert(actParquetSchema === expectedParquetSchema)\n    }\n  }\n\n  Seq(\n    (\n      \"field id validation: no negative field id\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(-1))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(4)),\n          fieldMetadata(1)),\n      // Expected error message\n      \"Field id should be non-negative.\"),\n    (\n      \"field id validation: no negative nested field id\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(1, (\"f00.element\", -1)))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(4)),\n          fieldMetadata(0)),\n      // Expected error message\n      \"Field id should be non-negative.\"),\n    (\n      \"field id validation: no duplicate field id\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(1, (\"f00.element\", 1)))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(4)),\n          fieldMetadata(1)),\n      // Expected error message\n      \"Field id should be unique.\"),\n    (\n      \"field id validation: no duplicate nested field id\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(1, (\"f00.element\", 2)))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(2)),\n          fieldMetadata(1)),\n      // Expected error message\n      \"Field id should be unique.\"),\n    (\n      \"field id validation: missing field ids\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(4)),\n          fieldMetadata(1)),\n      // Expected error message\n      \"Some of the fields are missing field ids.\"),\n    (\n      \"field id validation: missing nested field ids\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(1, (\"f00.element\", 2)))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(4)), // missing nested id\n          fieldMetadata(0)),\n      // Expected error message\n      \"Some of the fields are missing field ids.\"),\n    (\n      \"field id validation: missing field ids but have nested fields\",\n      // Delta schema - input\n      new StructType()\n        .add(\n          \"f0\",\n          new StructType()\n            .add(\"f00\", new ArrayType(LONG, false), fieldMetadata(1, (\"f00.element\", 2)))\n            .add(\"f01\", new MapType(INTEGER, INTEGER, true), fieldMetadata(4, (\"f01.key\", 5)))\n        ), // missing field id for f0\n      // Expected error message\n      \"Some of the fields are missing field ids.\")).foreach {\n    case (testName, deltaSchema, expectedErrorMsg) =>\n      test(testName) {\n        val ex = intercept[IllegalArgumentException] {\n          ParquetSchemaUtils.toParquetSchema(deltaSchema)\n        }\n        assert(ex.getMessage.contains(expectedErrorMsg))\n      }\n  }\n\n  private def fieldMetadata(id: Int, nestedFieldIds: (String, Int)*): FieldMetadata = {\n    val builder = FieldMetadata.builder().putLong(ColumnMapping.PARQUET_FIELD_ID_KEY, id)\n\n    val nestedFiledMetadata = FieldMetadata.builder()\n    nestedFieldIds.foreach { case (nestedColPath, nestedId) =>\n      nestedFiledMetadata.putLong(nestedColPath, nestedId)\n    }\n    builder\n      .putFieldMetadata(PARQUET_FIELD_NESTED_IDS_METADATA_KEY, nestedFiledMetadata.build())\n      .build()\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/parquet/ParquetSuiteBase.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.internal.parquet\n\nimport java.nio.file.{Files, Paths}\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport io.delta.kernel.data.{ColumnarBatch, FilteredColumnarBatch}\nimport io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO\nimport io.delta.kernel.defaults.utils.{TestRow, TestUtils}\nimport io.delta.kernel.expressions.{Column, Predicate}\nimport io.delta.kernel.internal.util.ColumnMapping\nimport io.delta.kernel.internal.util.Utils.toCloseableIterator\nimport io.delta.kernel.types.{ArrayType, DataType, MapType, StructField, StructType}\nimport io.delta.kernel.utils.{DataFileStatus, FileStatus}\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.hadoop.metadata.{ColumnPath, ParquetMetadata}\n\ntrait ParquetSuiteBase extends TestUtils {\n\n  implicit class DataFileStatusOps(dataFileStatus: DataFileStatus) {\n\n    /**\n     * Convert the [[DataFileStatus]] to a [[TestRow]].\n     * (path, size, modification time, numRecords,\n     * min_col1, max_col1, nullCount_col1 (..repeated for every stats column)\n     * )\n     */\n    def toTestRow(statsColumns: Seq[Column]): TestRow = {\n      val statsOpt = dataFileStatus.getStatistics\n      val record: Seq[Any] = {\n        dataFileStatus.getPath +:\n          dataFileStatus.getSize +:\n          // convert to seconds, Spark returns in seconds and we can compare at second level\n          (dataFileStatus.getModificationTime / 1000) +:\n          // Add the row count to the stats literals\n          (if (statsOpt.isPresent) statsOpt.get().getNumRecords else null) +:\n          statsColumns.flatMap { column =>\n            if (statsOpt.isPresent) {\n              val stats = statsOpt.get()\n              Seq(\n                Option(stats.getMinValues.get(column)).map(_.getValue).orNull,\n                Option(stats.getMaxValues.get(column)).map(_.getValue).orNull,\n                Option(stats.getNullCount.get(column)).orNull)\n            } else {\n              Seq(null, null, null)\n            }\n          }\n      }\n      TestRow(record: _*)\n    }\n  }\n\n  /**\n   * Verify the contents of the Parquet files located in `actualFileDir` matches the\n   * `expected` data. Does two types of verifications.\n   * 1) Verify the data using the Kernel Parquet reader\n   * 2) Verify the data using the Spark Parquet reader\n   */\n  def verifyContent(actualFileDir: String, expected: Seq[FilteredColumnarBatch]): Unit = {\n    verifyFileMetadata(actualFileDir)\n    verifyContentUsingKernelReader(actualFileDir, expected)\n    verifyContentUsingSparkReader(actualFileDir, expected)\n  }\n\n  /**\n   * Verify the metadata of the Parquet files in `targetDir` matches says it is written by Kernel.\n   */\n  def verifyFileMetadata(targetDir: String): Unit = {\n    parquetFiles(targetDir).foreach { file =>\n      footer(file.getPath).getFileMetaData\n        .getKeyValueMetaData.containsKey(\"io.delta.kernel.default-parquet-writer\")\n    }\n  }\n\n  /**\n   * Verify the data in the Parquet files located in `actualFileDir` matches the expected data.\n   * Use Kernel Parquet reader to read the data from the Parquet files.\n   */\n  def verifyContentUsingKernelReader(\n      actualFileDir: String,\n      expected: Seq[FilteredColumnarBatch]): Unit = {\n\n    val dataSchema = expected.head.getData.getSchema\n\n    val expectedTestRows = expected\n      .map(fb => fb.getRows)\n      .flatMap(_.toSeq)\n      .map(TestRow(_))\n\n    val actualTestRows = readParquetFilesUsingKernel(actualFileDir, dataSchema)\n\n    checkAnswer(actualTestRows, expectedTestRows)\n  }\n\n  /**\n   * Verify the data in the Parquet files located in `actualFileDir` matches the expected data.\n   * Use Spark Parquet reader to read the data from the Parquet files.\n   */\n  def verifyContentUsingSparkReader(\n      actualFileDir: String,\n      expected: Seq[FilteredColumnarBatch]): Unit = {\n\n    val dataSchema = expected.head.getData.getSchema;\n\n    val expectedTestRows = expected\n      .map(fb => fb.getRows)\n      .flatMap(_.toSeq)\n      .map(TestRow(_))\n\n    val actualTestRows = readParquetFilesUsingSpark(actualFileDir, dataSchema)\n\n    checkAnswer(actualTestRows, expectedTestRows)\n  }\n\n  /**\n   * Verify the field ids in Parquet files match the corresponding field ids in the Delta schema.\n   * If [[expectListMapEntryIds]] is true, verifies the array and map elements also have field ids\n   * the match the fields in nearest ancestor struct field (i.e array or map)\n   */\n  def verifyFieldIds(\n      targetDir: String,\n      deltaSchema: StructType,\n      expectNestedFiledIds: Boolean): Unit = {\n    parquetFiles(targetDir).map(_.getPath).map(footer(_)).foreach {\n      footer =>\n        val parquetSchema = footer.getFileMetaData.getSchema\n\n        def verifyFieldId(deltaFieldId: Long, parquetColumnPath: Array[String]): Unit = {\n          val parquetFieldId = parquetSchema.getType(parquetColumnPath: _*).getId\n          assert(parquetFieldId != null)\n          assert(deltaFieldId === parquetFieldId.intValue())\n        }\n\n        def verifyNestedFieldId(\n            nearestAncestorStructField: StructField,\n            relativeNestedFieldPath: Array[String], // relative to the nearest ancestor struct field\n            columnPath: Array[String]): Unit = {\n          val deltaFieldId = nearestAncestorStructField.getMetadata\n            .getMetadata(ColumnMapping.COLUMN_MAPPING_NESTED_IDS_KEY)\n            .getLong(relativeNestedFieldPath.mkString(\".\"))\n            .toInt\n          val parquetFieldId = parquetSchema.getType(columnPath: _*).getId\n          assert(parquetFieldId != null)\n          assert(deltaFieldId === parquetFieldId.intValue())\n        }\n\n        def visitDeltaType(\n            basePathInParquet: Array[String],\n            nearestAncestorStructField: StructField,\n            baseRelativePathToAncestor: Array[String],\n            deltaType: DataType): Unit = {\n          deltaType match {\n            case struct: StructType =>\n              visitStructType(basePathInParquet, struct)\n            case array: ArrayType =>\n              // Arrays are stored as three-level structure in Parquet. There are two elements\n              // between the array element and array itself. So in order to\n              // search for  the array element field id, we need to append \"list, element\"\n              // to the path.\n              // optional group col-b89fd303-7352-4044-842e-87f428ee80be (LIST) = 19 {\n              //  repeated group list {\n              //   optional group element {\n              //    optional int64 col-e983d1fc-d588-46a7-a0ad-2f63a6834ea6 = 20;\n              //   }\n              //  }\n              // }\n              val elemPathInParquet = basePathInParquet :+ \"list\" :+ \"element\"\n              val relativePathToNearestAncestor = baseRelativePathToAncestor :+ \"element\"\n              if (expectNestedFiledIds) {\n                verifyNestedFieldId(\n                  nearestAncestorStructField,\n                  relativePathToNearestAncestor,\n                  elemPathInParquet)\n              }\n              visitDeltaType(\n                elemPathInParquet,\n                nearestAncestorStructField,\n                relativePathToNearestAncestor,\n                array.getElementType)\n            case map: MapType =>\n              // reason for appending the \"key_value\" is same as the array type (see above)\n              val keyPathInParquet = basePathInParquet :+ \"key_value\" :+ \"key\"\n              val valuePathInParquet = basePathInParquet :+ \"key_value\" :+ \"value\"\n              val keyRelativePathToNearestAncestor = baseRelativePathToAncestor :+ \"key\"\n              val valueRelativePathToNearestAncestor = baseRelativePathToAncestor :+ \"value\"\n\n              if (expectNestedFiledIds) {\n                verifyNestedFieldId(\n                  nearestAncestorStructField,\n                  keyRelativePathToNearestAncestor,\n                  keyPathInParquet)\n                verifyNestedFieldId(\n                  nearestAncestorStructField,\n                  valueRelativePathToNearestAncestor,\n                  valuePathInParquet)\n              }\n\n              visitDeltaType(\n                keyPathInParquet,\n                nearestAncestorStructField,\n                keyRelativePathToNearestAncestor,\n                map.getKeyType)\n\n              visitDeltaType(\n                valuePathInParquet,\n                nearestAncestorStructField,\n                valueRelativePathToNearestAncestor,\n                map.getValueType)\n            case _ => // Primitive type - continue\n          }\n        }\n\n        def visitStructType(basePathInParquet: Array[String], structType: StructType): Unit = {\n          structType.fields.forEach { field =>\n            val deltaFieldId = field.getMetadata\n              .getLong(ColumnMapping.COLUMN_MAPPING_ID_KEY)\n            val physicalName = field.getMetadata\n              .getString(ColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n\n            verifyFieldId(deltaFieldId, basePathInParquet :+ physicalName)\n            visitDeltaType(\n              basePathInParquet :+ physicalName,\n              nearestAncestorStructField = field,\n              baseRelativePathToAncestor = Array(physicalName),\n              field.getDataType)\n          }\n        }\n\n        visitStructType(Array.empty, deltaSchema)\n    }\n  }\n\n  /**\n   * Write the [[FilteredColumnarBatch]]es to Parquet files using the ParquetFileWriter and\n   * verify the data using the Kernel Parquet reader and Spark Parquet reader.\n   */\n  def writeToParquetUsingKernel(\n      filteredData: Seq[FilteredColumnarBatch],\n      location: String,\n      targetFileSize: Long = 1024 * 1024,\n      statsColumns: Seq[Column] = Seq.empty): Seq[DataFileStatus] = {\n    val conf = new Configuration(configuration);\n    conf.setLong(ParquetFileWriter.TARGET_FILE_SIZE_CONF, targetFileSize)\n    val fileIO = new HadoopFileIO(conf)\n    val parquetWriter = ParquetFileWriter.multiFileWriter(\n      fileIO,\n      location,\n      statsColumns.asJava)\n\n    parquetWriter.write(toCloseableIterator(filteredData.asJava.iterator())).toSeq\n  }\n\n  def readParquetFilesUsingKernel(\n      actualFileDir: String,\n      readSchema: StructType,\n      predicate: Optional[Predicate] = Optional.empty()): Seq[TestRow] = {\n    val columnarBatches =\n      readParquetUsingKernelAsColumnarBatches(actualFileDir, readSchema, predicate)\n    columnarBatches.map(_.getRows).flatMap(_.toSeq).map(TestRow(_))\n  }\n\n  def readParquetUsingKernelAsColumnarBatches(\n      inputFileOrDir: String,\n      readSchema: StructType,\n      predicate: Optional[Predicate] = Optional.empty()): Seq[ColumnarBatch] = {\n    val parquetFileList = parquetFiles(inputFileOrDir)\n\n    val data = defaultEngine.getParquetHandler.readParquetFiles(\n      toCloseableIterator(parquetFileList.asJava.iterator()),\n      readSchema,\n      predicate)\n\n    data.asScala.toSeq.map(_.getData)\n  }\n\n  def parquetFileCount(fileOrDir: String): Long = parquetFiles(fileOrDir).size\n\n  def parquetFileRowCount(fileOrDir: String): Long = {\n    val files = parquetFiles(fileOrDir)\n\n    var rowCount = 0L\n    files.foreach { file =>\n      // read parquet file using spark and count.\n      rowCount = rowCount + spark.read.parquet(file.getPath).count()\n    }\n\n    rowCount\n  }\n\n  def parquetFiles(fileOrDir: String): Seq[FileStatus] = {\n    val fileOrDirPath = new Path(fileOrDir)\n    val hadoopFs = new Path(fileOrDir).getFileSystem(configuration)\n    hadoopFs.listStatus(fileOrDirPath)\n      .iterator\n      .filter(_.getPath.toString.endsWith(\".parquet\"))\n      .map(status =>\n        FileStatus.of(status.getPath.toString, status.getLen, status.getModificationTime))\n      .toSeq\n  }\n\n  def footer(path: String): ParquetMetadata = {\n    try {\n      org.apache.parquet.hadoop.ParquetFileReader.readFooter(configuration, new Path(path))\n    } catch {\n      case NonFatal(e) => fail(s\"Failed to read footer for file: $path\", e)\n    }\n  }\n\n  // Read the parquet files in actionFileDir using Spark Parquet reader\n  def readParquetFilesUsingSpark(\n      actualFileDir: String,\n      readSchema: StructType): Seq[TestRow] = {\n    spark.read\n      .format(\"parquet\")\n      .parquet(actualFileDir)\n      .to(readSchema.toSpark)\n      .collect()\n      .map(TestRow(_))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/metrics/LoggingMetricsReporterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.metrics\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.defaults.engine.LoggingMetricsReporter\nimport io.delta.kernel.metrics.MetricsReport\nimport io.delta.kernel.shaded.com.fasterxml.jackson.databind.exc.MismatchedInputException\nimport io.delta.kernel.types.{FieldMetadata, StringType, StructType}\n\nimport org.apache.logging.log4j.{Level, LogManager}\nimport org.apache.logging.log4j.core.{LogEvent, Logger => Log4jLogger}\nimport org.apache.logging.log4j.core.appender.AbstractAppender\nimport org.apache.logging.log4j.core.config.Property\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LoggingMetricsReporterSuite extends AnyFunSuite {\n\n  /** Captures logging events * */\n  private class BufferingAppender(name: String)\n      extends AbstractAppender(name, null, null, true, Property.EMPTY_ARRAY) {\n    val events: ArrayBuffer[LogEvent] = ArrayBuffer.empty[LogEvent]\n    override def append(event: LogEvent): Unit = events += event.toImmutable\n  }\n\n  private def withCapturedReporterLogger[T](f: BufferingAppender => T): T = {\n    val log = LogManager.getLogger(\"io.delta.kernel.defaults.engine.LoggingMetricsReporter\")\n      .asInstanceOf[Log4jLogger]\n    val app = new BufferingAppender(\"test-appender\")\n    app.start()\n    val oldLevel = log.getLevel\n    try {\n      log.addAppender(app)\n      log.setLevel(Level.ALL)\n      f(app)\n    } finally {\n      log.removeAppender(app)\n      log.setLevel(oldLevel)\n      app.stop()\n    }\n  }\n\n  test(\"LoggingMetricsReporter successfully logs a metrics report\") {\n    val fmNull = FieldMetadata.builder().putString(\"kNull\", null).build()\n    val fmArray =\n      FieldMetadata.builder().putStringArray(\"arr\", Array[String](\"x\", null, \"z\")).build()\n    val schema = new StructType()\n      .add(\"c1\", StringType.STRING, fmNull)\n      .add(\"c2\", StringType.STRING, fmArray)\n    val schemaStr = schema.toString\n\n    val report = new MetricsReport {\n      override def toJson: String = {\n        val s = schemaStr.replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\")\n        s\"\"\"{\"tableSchema\":\"$s\"}\"\"\"\n      }\n    }\n\n    withCapturedReporterLogger { app =>\n      new LoggingMetricsReporter().report(report)\n      val msgs = app.events.map(e => (e.getLevel, e.getMessage.getFormattedMessage))\n      assert(\n        msgs.exists { case (lvl, msg) =>\n          lvl == Level.INFO && msg.contains(\"tableSchema\")\n        },\n        s\"Expected INFO log with 'tableSchema' but got: ${msgs.mkString(\"; \")}\")\n      assert(\n        msgs.exists { case (lvl, msg) =>\n          lvl == Level.INFO && msg.contains(\"kNull=null\") && msg.contains(\"arr=[x, null, z]\")\n        },\n        s\"Expected schema with null values and arrays to be serialized correctly\")\n    }\n  }\n\n  test(\"LoggingMetricsReporter catches and logs kernel-api shaded JsonProcessingException\") {\n    val shadedThrow = new MetricsReport {\n      override def toJson: String = {\n        val ex = MismatchedInputException.from(\n          null.asInstanceOf[io.delta.kernel.shaded.com.fasterxml.jackson.core.JsonParser],\n          classOf[Object],\n          \"test exception\")\n        throw ex\n      }\n    }\n\n    withCapturedReporterLogger { app =>\n      val reporter = new LoggingMetricsReporter()\n      reporter.report(shadedThrow)\n      val msgs = app.events.map(e => (e.getLevel, e.getMessage.getFormattedMessage))\n      assert(\n        msgs.exists { case (lvl, msg) =>\n          lvl == Level.WARN && msg.contains(\"Serialization issue\")\n        },\n        s\"Expected WARN with 'Serialization issue' but got: ${msgs.mkString(\"; \")}\")\n    }\n  }\n\n  test(\"LoggingMetricsReporter logs generic Exception at WARN level\") {\n    val genericThrow = new MetricsReport {\n      override def toJson: String = throw new RuntimeException(\"generic boom\")\n    }\n\n    withCapturedReporterLogger { app =>\n      val reporter = new LoggingMetricsReporter()\n      reporter.report(genericThrow)\n      val msgs = app.events.map(e => (e.getLevel, e.getMessage.getFormattedMessage))\n      assert(\n        msgs.exists { case (lvl, msg) =>\n          lvl == Level.WARN && msg.contains(\"Unexpected error\")\n        },\n        s\"Expected WARN with 'Unexpected error' but got: ${msgs.mkString(\"; \")}\")\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/metrics/MetricsReportTestUtils.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.metrics\n\nimport java.util\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.defaults.utils.TestUtils\nimport io.delta.kernel.engine._\nimport io.delta.kernel.metrics.MetricsReport\n\nimport org.apache.hadoop.conf.Configuration\nimport org.slf4j.LoggerFactory\n\n/**\n * Test utilities for testing the Kernel-API created [[MetricsReports]]s.\n *\n * We test [[MetricsReport]]s in the defaults package so we can use real tables and avoid having\n * to mock both file listings AND file contents.\n */\ntrait MetricsReportTestUtils extends TestUtils {\n\n  private val logger = LoggerFactory.getLogger(classOf[MetricsReportTestUtils])\n\n  override lazy val defaultEngine = DefaultEngine.create(new Configuration() {\n    {\n      // Set the batch sizes to small so that we get to test the multiple batch scenarios.\n      set(\"delta.kernel.default.parquet.reader.batch-size\", \"2\");\n      set(\"delta.kernel.default.json.reader.batch-size\", \"2\");\n    }\n  })\n\n  // For now this just uses the default engine since we have no need to override it, if we would\n  // like to use a specific engine in the future for other tests we can simply add another arg here\n  /**\n   * Executes [[f]] using a special engine implementation to collect and return metrics reports.\n   * If [[expectException]], catches any exception thrown by [[f]] and returns it with the reports.\n   */\n  def collectMetricsReports(\n      f: Engine => Unit,\n      expectException: Boolean): (Seq[MetricsReport], Option[Exception]) = {\n    // Initialize a buffer for any metric reports and wrap the engine so that they are recorded\n    val reports = ArrayBuffer.empty[MetricsReport]\n    if (expectException) {\n      val e = intercept[Exception] {\n        f(new EngineWithInMemoryMetricsReporter(reports, defaultEngine))\n      }\n      logger.warn(\"Caught exception:\", e)\n      (reports.toSeq, Some(e))\n    } else {\n      f(new EngineWithInMemoryMetricsReporter(reports, defaultEngine))\n      (reports.toSeq, Option.empty)\n    }\n  }\n\n  /**\n   * Wraps an {@link Engine} to implement the metrics reporter such that it appends any reports\n   * to the provided in memory buffer.\n   */\n  class EngineWithInMemoryMetricsReporter(buf: ArrayBuffer[MetricsReport], baseEngine: Engine)\n      extends Engine {\n\n    private val inMemoryMetricsReporter = new MetricsReporter {\n      override def report(report: MetricsReport): Unit = buf.append(report)\n    }\n\n    private val metricsReporters = new util.ArrayList[MetricsReporter]() {\n      {\n        addAll(baseEngine.getMetricsReporters)\n        add(inMemoryMetricsReporter)\n      }\n    }\n\n    override def getExpressionHandler: ExpressionHandler = baseEngine.getExpressionHandler\n\n    override def getJsonHandler: JsonHandler = baseEngine.getJsonHandler\n\n    override def getFileSystemClient: FileSystemClient = baseEngine.getFileSystemClient\n\n    override def getParquetHandler: ParquetHandler = baseEngine.getParquetHandler\n\n    override def getMetricsReporters(): java.util.List[MetricsReporter] = {\n      metricsReporters\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/metrics/ScanReportSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.metrics\n\nimport java.util.Collections\n\nimport io.delta.kernel._\nimport io.delta.kernel.data.FilteredColumnarBatch\nimport io.delta.kernel.engine._\nimport io.delta.kernel.expressions.{Column, Literal, Predicate}\nimport io.delta.kernel.internal.data.GenericRow\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.metrics.Timer\nimport io.delta.kernel.internal.util.{FileNames, Utils}\nimport io.delta.kernel.metrics.{ScanReport, SnapshotReport}\nimport io.delta.kernel.types.{IntegerType, LongType, StructType}\nimport io.delta.kernel.utils.CloseableIterator\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\n\nimport org.apache.spark.sql.functions.col\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ScanReportSuite extends AnyFunSuite with MetricsReportTestUtils {\n\n  /**\n   * Creates a [[Scan]] using `getScan` and then requests and consumes the scan files. Uses a custom\n   * engine to collect emitted metrics reports (exactly 1 [[ScanReport]] and 1 [[SnapshotReport]] is\n   * expected). Also times and returns the duration it takes to consume the scan files.\n   *\n   * @param getScan function to generate a [[Scan]] given an engine\n   * @param expectException whether we expect consuming the scan files to throw an exception, which\n   *                        if so, is caught and returned with the other results\n   * @return (ScanReport, durationToConsumeScanFiles, SnapshotReport, ExceptionIfThrown)\n   */\n  def getScanAndSnapshotReport(\n      getScan: Engine => Scan,\n      expectException: Boolean,\n      consumeScanFiles: CloseableIterator[FilteredColumnarBatch] => Unit)\n      : (ScanReport, Long, SnapshotReport, Option[Exception]) = {\n    val timer = new Timer()\n\n    val (metricsReports, exception) = collectMetricsReports(\n      engine => {\n        val scan = getScan(engine)\n        // Time the actual operation\n        timer.timeCallable(() => consumeScanFiles(scan.getScanFiles(engine)))\n      },\n      expectException)\n\n    val scanReports = metricsReports.filter(_.isInstanceOf[ScanReport])\n    assert(scanReports.length == 1, \"Expected exactly 1 ScanReport\")\n    val snapshotReports = metricsReports.filter(_.isInstanceOf[SnapshotReport])\n    assert(snapshotReports.length == 1, \"Expected exactly 1 SnapshotReport\")\n    (\n      scanReports.head.asInstanceOf[ScanReport],\n      timer.totalDurationNs(),\n      snapshotReports.head.asInstanceOf[SnapshotReport],\n      exception)\n  }\n\n  /**\n   * Given a table path, constructs the latest snapshot, and uses it to generate a Scan with the\n   * provided filter and readSchema (if provided). Consumes the scan files from the scan and\n   * collects the emitted [[ScanReport]] and checks that the report is as expected.\n   *\n   * @param path table path to query\n   * @param expectException whether we expect consuming the scan files to throw an exception\n   * @param expectedNumAddFiles expected number of add files seen\n   * @param expectedNumAddFilesFromDeltaFiles expected number of add files seen from delta files\n   * @param expectedNumActiveAddFiles expected number of active add files\n   * @param expectedNumDuplicateAddFiles expected number of duplicate add files seen\n   * @param expectedNumRemoveFilesSeenFromDeltaFiles expected number of remove files seen\n   * @param expectedPartitionPredicate expected partition predicate\n   * @param expectedDataSkippingFilter expected data skipping filter\n   * @param filter filter to build the scan with\n   * @param readSchema read schema to build the scan with\n   * @param consumeScanFiles function to consume scan file iterator\n   */\n  // scalastyle:off\n  def checkScanReport(\n      path: String,\n      expectException: Boolean,\n      expectedNumAddFiles: Long,\n      expectedNumAddFilesFromDeltaFiles: Long,\n      expectedNumActiveAddFiles: Long,\n      expectedNumDuplicateAddFiles: Long = 0,\n      expectedNumRemoveFilesSeenFromDeltaFiles: Long = 0,\n      expectedPartitionPredicate: Option[Predicate] = None,\n      expectedDataSkippingFilter: Option[Predicate] = None,\n      expectedIsFullyConsumed: Boolean = true,\n      filter: Option[Predicate] = None,\n      readSchema: Option[StructType] = None,\n      // toSeq triggers log replay, consumes the actions and closes the iterator\n      consumeScanFiles: CloseableIterator[FilteredColumnarBatch] => Unit =\n        iter => iter.toSeq): Unit = {\n    // scalastyle:on\n    // We need to save the snapshotSchema to check against the generated scan report\n    // In order to use the utils to collect the reports, we need to generate the snapshot in a anon\n    // fx, thus we save the snapshotSchema as a side-effect\n    var snapshotSchema: StructType = null\n\n    val (scanReport, durationNs, snapshotReport, exceptionOpt) = getScanAndSnapshotReport(\n      engine => {\n        val snapshot = Table.forPath(engine, path).getLatestSnapshot(engine)\n        snapshotSchema = snapshot.getSchema()\n        var scanBuilder = snapshot.getScanBuilder()\n        if (filter.nonEmpty) {\n          scanBuilder = scanBuilder.withFilter(filter.get)\n        }\n        if (readSchema.nonEmpty) {\n          scanBuilder = scanBuilder.withReadSchema(readSchema.get)\n        }\n        scanBuilder.build()\n      },\n      expectException,\n      consumeScanFiles)\n\n    // Verify contents\n    assert(scanReport.getTablePath == defaultEngine.getFileSystemClient.resolvePath(path))\n    assert(scanReport.getOperationType == \"Scan\")\n    exceptionOpt match {\n      case Some(e) =>\n        assert(scanReport.getException().isPresent)\n        assert(scanReport.getException().get().getClass == e.getClass)\n        assert(scanReport.getException().get().getMessage == e.getMessage)\n      case None => assert(!scanReport.getException().isPresent)\n    }\n    assert(scanReport.getReportUUID != null)\n\n    assert(\n      snapshotReport.getVersion.isPresent,\n      \"Version should be present for success SnapshotReport\")\n    assert(scanReport.getTableVersion() == snapshotReport.getVersion.get())\n    assert(scanReport.getTableSchema() == snapshotSchema)\n    assert(scanReport.getSnapshotReportUUID == snapshotReport.getReportUUID)\n    assert(scanReport.getFilter.toScala == filter)\n    assert(scanReport.getReadSchema == readSchema.getOrElse(snapshotSchema))\n    assert(scanReport.getPartitionPredicate.toScala == expectedPartitionPredicate)\n    assert(scanReport.getIsFullyConsumed == expectedIsFullyConsumed)\n\n    (scanReport.getDataSkippingFilter.toScala, expectedDataSkippingFilter) match {\n      case (Some(found), Some(expected)) =>\n        assert(found.getName == expected.getName && found.getChildren == expected.getChildren)\n      case (found, expected) => assert(found == expected)\n    }\n\n    // Since we cannot know the actual duration of the scan we sanity check that they are > 0 and\n    // less than the total operation duration\n    assert(scanReport.getScanMetrics.getTotalPlanningDurationNs > 0)\n    assert(scanReport.getScanMetrics.getTotalPlanningDurationNs < durationNs)\n\n    assert(scanReport.getScanMetrics.getNumAddFilesSeen == expectedNumAddFiles)\n    assert(scanReport.getScanMetrics.getNumAddFilesSeenFromDeltaFiles ==\n      expectedNumAddFilesFromDeltaFiles)\n    assert(scanReport.getScanMetrics.getNumActiveAddFiles == expectedNumActiveAddFiles)\n    assert(scanReport.getScanMetrics.getNumDuplicateAddFiles == expectedNumDuplicateAddFiles)\n    assert(scanReport.getScanMetrics.getNumRemoveFilesSeenFromDeltaFiles ==\n      expectedNumRemoveFilesSeenFromDeltaFiles)\n  }\n\n  test(\"ScanReport: basic case with no extra parameters\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Set up delta table with 1 add file\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = 1,\n        expectedNumAddFilesFromDeltaFiles = 1,\n        expectedNumActiveAddFiles = 1)\n    }\n  }\n\n  test(\"ScanReport: basic case with read schema\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Set up delta table with 1 add file\n      spark.range(10).withColumn(\"c2\", col(\"id\") % 2)\n        .write.format(\"delta\").mode(\"append\").save(path)\n\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = 1,\n        expectedNumAddFilesFromDeltaFiles = 1,\n        expectedNumActiveAddFiles = 1,\n        readSchema = Some(new StructType().add(\"id\", LongType.LONG)))\n    }\n  }\n\n  test(\"ScanReport: different filter scenarios\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Set up partitioned table\n      spark.range(10).withColumn(\"part\", col(\"id\") % 2)\n        .write.format(\"delta\").partitionBy(\"part\").save(path)\n\n      val partFilter = new Predicate(\"=\", new Column(\"part\"), Literal.ofLong(0))\n      val dataFilter = new Predicate(\"<=\", new Column(\"id\"), Literal.ofLong(0))\n      val expectedSkippingFilter = new Predicate(\n        \"<=\",\n        new Column(Array(\"minValues\", \"id\")),\n        Literal.ofLong(0))\n\n      // The below metrics are incremented during log replay before any filtering happens and thus\n      // should be the same for all of the following test cases\n      val expectedNumAddFiles = 2\n      val expectedNumAddFilesFromDeltaFiles = 2\n      val expectedNumActiveAddFiles = 2\n\n      // No filter - 2 add files one for each partition\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = expectedNumAddFiles,\n        expectedNumAddFilesFromDeltaFiles = expectedNumAddFilesFromDeltaFiles,\n        expectedNumActiveAddFiles = expectedNumActiveAddFiles)\n\n      // With partition filter\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = expectedNumAddFiles,\n        expectedNumAddFilesFromDeltaFiles = expectedNumAddFilesFromDeltaFiles,\n        expectedNumActiveAddFiles = expectedNumActiveAddFiles,\n        filter = Some(partFilter),\n        expectedPartitionPredicate = Some(partFilter))\n\n      // With data filter\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = expectedNumAddFiles,\n        expectedNumAddFilesFromDeltaFiles = expectedNumAddFilesFromDeltaFiles,\n        expectedNumActiveAddFiles = expectedNumActiveAddFiles,\n        filter = Some(dataFilter),\n        expectedDataSkippingFilter = Some(expectedSkippingFilter))\n\n      // With data and partition filter\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = expectedNumAddFiles,\n        expectedNumAddFilesFromDeltaFiles = expectedNumAddFilesFromDeltaFiles,\n        expectedNumActiveAddFiles = expectedNumActiveAddFiles,\n        filter = Some(new Predicate(\"AND\", partFilter, dataFilter)),\n        expectedDataSkippingFilter = Some(expectedSkippingFilter),\n        expectedPartitionPredicate = Some(partFilter))\n    }\n  }\n\n  test(\"ScanReport: close scan file iterator early\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Set up delta table with 2 add files\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = 1,\n        expectedNumAddFilesFromDeltaFiles = 1,\n        expectedNumActiveAddFiles = 1,\n        expectedIsFullyConsumed = false,\n        consumeScanFiles = iter => iter.close() // Close iterator before consuming any scan files\n      )\n    }\n  }\n\n  //////////////////\n  // Error cases ///\n  //////////////////\n\n  test(\"ScanReport error case - unrecognized partition filter\") {\n    // Thrown during partition pruning when the expression handler cannot evaluate the filter\n    // Because partition pruning happens within a `map` on the iterator, this is caught and reported\n    // within `map`\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Set up partitioned table\n      spark.range(10).withColumn(\"part\", col(\"id\") % 2)\n        .write.format(\"delta\").partitionBy(\"part\").save(path)\n\n      val partFilter = new Predicate(\"foo\", new Column(\"part\"), Literal.ofLong(0))\n\n      checkScanReport(\n        path,\n        expectException = true,\n        expectedNumAddFiles = 0,\n        expectedNumAddFilesFromDeltaFiles = 0,\n        expectedNumActiveAddFiles = 0,\n        expectedIsFullyConsumed = false,\n        filter = Some(partFilter),\n        expectedPartitionPredicate = Some(partFilter))\n    }\n  }\n\n  test(\"ScanReport error case - error reading the log files\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // We set up a table with a giberish json file at version 0 and a valid json file at version 1\n      // that contains the P&M\n      // This is so the snapshot loading will happen successful, and we will only fail when trying\n      // to load up the scan files\n      // This exception is thrown from within the `hasNext` method on the iterator since that is\n      // when we load the actions from the log files. This exception is caught and reported within\n      // `hasNext`\n      spark.range(10).write.format(\"delta\").save(path)\n      // Update protocol and metadata (so that version 1 has both P&M present)\n      spark.sql(\n        s\"ALTER TABLE delta.`$path` SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n      // Overwrite json file with giberish (this will have a schema mismatch issue for `add`)\n      val giberishRow = new GenericRow(\n        new StructType().add(\"add\", IntegerType.INTEGER),\n        Collections.singletonMap(0, Integer.valueOf(0)))\n      defaultEngine.getJsonHandler.writeJsonFileAtomically(\n        FileNames.deltaFile(new Path(tempDir.toString, \"_delta_log\"), 0),\n        Utils.singletonCloseableIterator(giberishRow),\n        true)\n\n      checkScanReport(\n        path,\n        expectException = true,\n        expectedNumAddFiles = 0,\n        expectedNumAddFilesFromDeltaFiles = 0,\n        expectedNumActiveAddFiles = 0,\n        expectedIsFullyConsumed = false)\n    }\n  }\n\n  ///////////////////////////////\n  // Log replay metrics tests ///\n  ///////////////////////////////\n\n  test(\"active add files log replay metrics: only delta files\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      for (_ <- 0 to 9) {\n        appendCommit(path)\n      }\n\n      checkScanReport(\n        path,\n        expectException = false,\n        expectedNumAddFiles = 20, // each commit creates 2 files\n        expectedNumAddFilesFromDeltaFiles = 20,\n        expectedNumActiveAddFiles = 20)\n    }\n  }\n\n  Seq(true, false).foreach { multipartCheckpoint =>\n    val checkpointStr = if (multipartCheckpoint) \"multipart \" else \"\"\n    test(s\"active add files log replay metrics: ${checkpointStr}checkpoint + delta files\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n        for (_ <- 0 to 3) {\n          appendCommit(path)\n        }\n        checkpoint(path, actionsPerFile = if (multipartCheckpoint) 2 else 1000000)\n        for (_ <- 4 to 9) {\n          appendCommit(path)\n        }\n\n        checkScanReport(\n          path,\n          expectException = false,\n          expectedNumAddFiles = 20, // each commit creates 2 files\n          expectedNumAddFilesFromDeltaFiles = 12, // checkpoint is created at version 3\n          expectedNumActiveAddFiles = 20)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { multipartCheckpoint =>\n    val checkpointStr = if (multipartCheckpoint) \"multipart \" else \"\"\n    test(s\"active add files log replay metrics: ${checkpointStr}checkpoint + \" +\n      s\"delta files + tombstones\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n        for (_ <- 0 to 3) {\n          appendCommit(path)\n        } // has 8 add files\n        deleteCommit(path) // version 4 - deletes 4 files and adds 1 file\n        checkpoint(path, actionsPerFile = if (multipartCheckpoint) 2 else 1000000) // version 4\n        appendCommit(path) // version 5 - adds 2 files\n        deleteCommit(path) // version 6 - deletes 1 file and adds 1 file\n        appendCommit(path) // version 7 - adds 2 files\n        appendCommit(path) // version 8 - adds 2 files\n        deleteCommit(path) // version 9 - deletes 2 files and adds 1 file\n\n        checkScanReport(\n          path,\n          expectException = false,\n          expectedNumAddFiles = 5 /* checkpoint */ + 8, /* delta */\n          expectedNumAddFilesFromDeltaFiles = 8,\n          expectedNumActiveAddFiles = 10,\n          expectedNumRemoveFilesSeenFromDeltaFiles = 3)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { multipartCheckpoint =>\n    val checkpointStr = if (multipartCheckpoint) \"multipart \" else \"\"\n    test(s\"active add files log replay metrics: ${checkpointStr}checkpoint + delta files +\" +\n      s\" tombstones + duplicate adds\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n        for (_ <- 0 to 1) {\n          appendCommit(path)\n        } // activeAdds = 4\n        deleteCommit(path) // ver 2 - deletes 2 files and adds 1 file, activeAdds = 3\n        checkpoint(path, actionsPerFile = if (multipartCheckpoint) 2 else 1000000) // version 2\n        appendCommit(path) // ver 3 - adds 2 files, activeAdds = 5\n        recomputeStats(path) // ver 4 - adds the same 5 add files again, activeAdds = 5, dupes = 5\n        deleteCommit(path) // ver 5 - removes 1 file and adds 1 file, activeAdds = 5, dupes = 5\n        appendCommit(path) // ver 6 - adds 2 files, activeAdds = 7, dupes = 4\n        recomputeStats(path) // ver 7 - adds the same 7 add files again, activeAdds = 7, dupes = 12\n        deleteCommit(path) // ver 8 - removes 1 file and adds 1 files, activeAdds = 7, dupes = 12\n\n        checkScanReport(\n          path,\n          expectException = false,\n          expectedNumAddFiles = 3 /* checkpoint */ + 18, /* delta */\n          expectedNumAddFilesFromDeltaFiles = 18,\n          expectedNumActiveAddFiles = 7,\n          expectedNumDuplicateAddFiles = 12,\n          expectedNumRemoveFilesSeenFromDeltaFiles = 2)\n      }\n    }\n  }\n\n  /////////////////////////////////////////////\n  // Helpers for testing log replay metrics ///\n  /////////////////////////////////////////////\n\n  def appendCommit(path: String): Unit =\n    spark.range(10).repartition(2).write.format(\"delta\").mode(\"append\").save(path)\n\n  def deleteCommit(path: String): Unit = {\n    spark.sql(\"DELETE FROM delta.`%s` WHERE id = 5\".format(path))\n  }\n\n  def recomputeStats(path: String): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, new org.apache.hadoop.fs.Path(path))\n    StatisticsCollection.recompute(spark, deltaLog, catalogTable = None)\n  }\n\n  def checkpoint(path: String, actionsPerFile: Int): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> actionsPerFile.toString) {\n      DeltaLog.forTable(spark, path).checkpoint()\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/metrics/SnapshotReportSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.metrics\n\nimport java.io.File\nimport java.util.{Objects, Optional}\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel._\nimport io.delta.kernel.defaults.test.{AbstractTableManagerAdapter, LegacyTableManagerAdapter}\nimport io.delta.kernel.defaults.utils.WriteUtils\nimport io.delta.kernel.engine._\nimport io.delta.kernel.expressions.Literal\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.metrics.Timer\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.metrics.SnapshotReport\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.funsuite.AnyFunSuite\n\n/**\n * Concrete implementation for legacy [[Table.forPath]] API. This is being replaced by the\n * [[TableManager.loadSnapshot]] API.\n */\nclass LegacySnapshotReportSuite extends AbstractSnapshotReportSuite {\n  import io.delta.kernel.defaults.test.LegacyTableManagerAdapter\n  override def tableManager: AbstractTableManagerAdapter = new LegacyTableManagerAdapter()\n\n  // This test is only applicable to the legacy API because the\n  // SnapshotBuilder::atTimestamp(latestSnapshot, timestamp) API takes in the latest snapshot\n  // directly (as opposed to generating it internally). This lets the builder validate eagerly if\n  // the provided timestamp is after the latest snapshot, which happens before any metrics are\n  // recorded.\n  test(\"Snapshot report - invalid timestamp (timestamp too late)\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      appendData(tablePath = path, isNewTable = true, schema = testSchema, data = Nil)\n\n      // Test getSnapshotAsOfTimestamp with timestamp=currentTime (does not exist)\n      // This fails during timestamp -> version resolution\n      val currentTimeMillis = System.currentTimeMillis\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, currentTimeMillis),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 2, // latestSnapshot + timeTravelToTimestampSnapshot\n          expectException = true,\n          expectedVersion = Optional.empty(),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.of(currentTimeMillis),\n          expectNonEmptyTimestampToVersionResolutionDuration = true,\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroBuildLogSegmentDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n    }\n  }\n}\n\n/** Concrete implementation for [[TableManager.loadSnapshot]] API. */\nclass TableManagerSnapshotReportSuite extends AbstractSnapshotReportSuite {\n  import io.delta.kernel.defaults.test.TableManagerAdapter\n  override def tableManager: AbstractTableManagerAdapter = new TableManagerAdapter()\n}\n\nabstract class AbstractSnapshotReportSuite\n    extends AnyFunSuite\n    with MetricsReportTestUtils\n    with WriteUtils\n    with BeforeAndAfterAll {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key, true)\n  }\n\n  def tableManager: AbstractTableManagerAdapter\n\n  case class SnapshotReportExpectations(\n      expectedReportCount: Int,\n      expectException: Boolean,\n      expectedVersion: Optional[Long],\n      expectedCheckpointVersion: Optional[Long],\n      expectedProvidedTimestamp: Optional[Long],\n      expectNonEmptyTimestampToVersionResolutionDuration: Boolean = false,\n      expectNonZeroLoadProtocolAndMetadataDuration: Boolean = true,\n      expectNonZeroBuildLogSegmentDuration: Boolean = true,\n      expectNonZeroDurationToGetCrcInfo: Boolean = true)\n\n  /**\n   * Given a function [[f]] that generates a snapshot from an engine and path, runs [[f]] and looks\n   * for a generated [[SnapshotReport]]. Times and returns the duration it takes to run [[f]].\n   * Uses a custom engine to collect emitted metrics reports. If more than one report is\n   * generated (e.g. during timestamp-based time travel), only the last one is returned.\n   *\n   * @param f function to generate a snapshot from an engine and path\n   * @param path path of the table to query\n   * @param expectedReportCount the expected number of [[SnapshotReport]]s to be generated. This\n   *                            can be greater than 1 for timestamp-based time travel queries.\n   * @param expectException whether we expect [[f]] to throw an exception, which if so, is caught\n   *                        and returned with the other results\n   * @returns (SnapshotReport, durationToRunF, ExceptionIfThrown)\n   */\n  def getSnapshotReport(\n      f: (Engine, String) => Snapshot,\n      path: String,\n      expectedReportCount: Int,\n      expectException: Boolean): (SnapshotReport, Long, Option[Exception]) = {\n    val timer = new Timer()\n\n    val (metricsReports, exception) = collectMetricsReports(\n      engine => {\n        timer.time(() => f(engine, path)) // Time the actual operation\n      },\n      expectException)\n\n    val snapshotReports = metricsReports.filter(_.isInstanceOf[SnapshotReport])\n    assert(\n      snapshotReports.length == expectedReportCount,\n      s\"Expected exactly $expectedReportCount SnapshotReport\")\n    (snapshotReports.last.asInstanceOf[SnapshotReport], timer.totalDurationNs(), exception)\n  }\n\n  /**\n   * Given a table path and a function [[f]] to generate a snapshot, runs [[f]] and collects the\n   * generated [[SnapshotReport]]. Checks that the report is as expected.\n   *\n   * @param f function to generate a snapshot from an engine and path\n   * @param path table path to query from\n   * @param expectations encapsulates all the expected values and behaviors for the snapshot report.\n   *                     See [[SnapshotReportExpectations]] for detailed parameter descriptions.\n   */\n  def checkSnapshotReport(\n      f: (Engine, String) => Snapshot,\n      path: String,\n      expectations: SnapshotReportExpectations): Unit = {\n\n    val (snapshotReport, duration, exception) =\n      getSnapshotReport(f, path, expectations.expectedReportCount, expectations.expectException)\n\n    // Verify contents\n    assert(snapshotReport.getTablePath == defaultEngine.getFileSystemClient.resolvePath(path))\n    assert(snapshotReport.getOperationType == \"Snapshot\")\n    exception match {\n      case Some(e) =>\n        assert(snapshotReport.getException().isPresent &&\n          Objects.equals(snapshotReport.getException().get(), e))\n      case None => assert(!snapshotReport.getException().isPresent)\n    }\n    assert(snapshotReport.getReportUUID != null)\n    assert(\n      Objects.equals(snapshotReport.getVersion, expectations.expectedVersion),\n      s\"Expected version ${expectations.expectedVersion} found ${snapshotReport.getVersion}\")\n    assert(\n      Objects.equals(\n        snapshotReport.getCheckpointVersion,\n        expectations.expectedCheckpointVersion),\n      s\"Expected checkpoint version ${expectations.expectedCheckpointVersion}, found \" +\n        s\"${snapshotReport.getCheckpointVersion}\")\n    assert(Objects.equals(\n      snapshotReport.getProvidedTimestamp,\n      expectations.expectedProvidedTimestamp))\n\n    // Since we cannot know the actual durations of these we sanity check that they are > 0 and\n    // less than the total operation duration whenever they are expected to be non-zero/non-empty\n\n    val metrics = snapshotReport.getSnapshotMetrics\n\n    // ===== Metric: getLoadSnapshotTotalDurationNs =====\n    if (!expectations.expectException) {\n      assert(metrics.getLoadSnapshotTotalDurationNs > 0)\n      assert(metrics.getLoadSnapshotTotalDurationNs <= duration)\n    } else {\n      assert(metrics.getLoadSnapshotTotalDurationNs >= 0)\n    }\n\n    // ===== Metric: getComputeTimestampToVersionTotalDurationNs =====\n    if (expectations.expectNonEmptyTimestampToVersionResolutionDuration) {\n      assert(metrics.getComputeTimestampToVersionTotalDurationNs.isPresent)\n      assert(metrics.getComputeTimestampToVersionTotalDurationNs.get > 0)\n      assert(metrics.getComputeTimestampToVersionTotalDurationNs.get < duration)\n      assert(metrics.getComputeTimestampToVersionTotalDurationNs.get <=\n        metrics.getLoadSnapshotTotalDurationNs)\n    } else {\n      assert(!metrics.getComputeTimestampToVersionTotalDurationNs.isPresent)\n    }\n\n    // ===== Metric: getLoadProtocolMetadataTotalDurationNs  =====\n    if (expectations.expectNonZeroLoadProtocolAndMetadataDuration) {\n      assert(metrics.getLoadProtocolMetadataTotalDurationNs > 0)\n      assert(metrics.getLoadProtocolMetadataTotalDurationNs < duration)\n      assert(\n        metrics.getLoadProtocolMetadataTotalDurationNs <= metrics.getLoadSnapshotTotalDurationNs)\n    } else {\n      assert(metrics.getLoadProtocolMetadataTotalDurationNs == 0)\n    }\n\n    // ===== Metric: getLoadLogSegmentTotalDurationNs =====\n    if (expectations.expectNonZeroBuildLogSegmentDuration) {\n      assert(metrics.getLoadLogSegmentTotalDurationNs > 0)\n      assert(metrics.getLoadLogSegmentTotalDurationNs < duration)\n      assert(metrics.getLoadLogSegmentTotalDurationNs <= metrics.getLoadSnapshotTotalDurationNs)\n    } else {\n      assert(metrics.getLoadLogSegmentTotalDurationNs == 0)\n    }\n\n    // ===== Metric: getLoadCrcTotalDurationNs =====\n    if (expectations.expectNonZeroDurationToGetCrcInfo) {\n      assert(metrics.getLoadCrcTotalDurationNs > 0)\n      assert(metrics.getLoadCrcTotalDurationNs < duration)\n      assert(metrics.getLoadCrcTotalDurationNs <= metrics.getLoadSnapshotTotalDurationNs)\n    } else {\n      assert(metrics.getLoadCrcTotalDurationNs == 0)\n    }\n  }\n\n  /**\n   * Wait for the CRC file to exist for the given version. This helps ensure that Delta-Spark's\n   * [[ChecksumHook]] has finished writing the checksum file before running tests.\n   */\n  private def waitForCrcFileToExistElseThrow(tablePath: String, version: Long): Unit = {\n    val logPath = new Path(tablePath, \"_delta_log\")\n    val maxWaitMs = 1000 // Wait up to 1 second\n    val startTime = System.currentTimeMillis()\n    val crcFile = new java.io.File(FileNames.checksumFile(logPath, version).toString)\n\n    while (!crcFile.exists() && (System.currentTimeMillis() - startTime) < maxWaitMs) {\n      Thread.sleep(100)\n    }\n\n    def getDeltaLogContents: String = {\n      new java.io.File(tablePath, \"_delta_log\")\n        .listFiles().map(_.getName).sorted.mkString(\"\\n- \", \"\\n- \", \"\")\n    }\n\n    assert(crcFile.exists(), s\"CRC file $crcFile does not exist. Delta Log:$getDeltaLogContents\")\n  }\n\n  test(\"SnapshotReport valid queries - no checkpoint\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      // Set up delta table with version 0, 1\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val version0timestamp = System.currentTimeMillis\n      // Since filesystem modification time might be truncated to the second, we sleep to make\n      // sure the next commit is after this timestamp\n      Thread.sleep(1000)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      waitForCrcFileToExistElseThrow(path, 0L)\n      waitForCrcFileToExistElseThrow(path, 1L)\n\n      // Test getLatestSnapshot\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtLatest(engine, path),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = false,\n          expectedVersion = Optional.of(1),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty() // No time travel by timestamp\n        ))\n\n      // Test getSnapshotAsOfVersion\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtVersion(engine, path, 0),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = false,\n          expectedVersion = Optional.of(0),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty() // No time travel by timestamp\n        ))\n\n      // Test getSnapshotAsOfTimestamp\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, version0timestamp),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 2, // latestSnapshot + timeTravelToTimestampSnapshot\n          expectException = false,\n          expectedVersion = Optional.of(0),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.of(version0timestamp),\n          expectNonEmptyTimestampToVersionResolutionDuration = true))\n    }\n  }\n\n  test(\"SnapshotReport valid queries - with checkpoint\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Set up delta table with version 0 to 11 with checkpoint at version 10\n      (0 until 11).foreach(_ =>\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(path))\n\n      val version11timestamp = System.currentTimeMillis\n      // Since filesystem modification time might be truncated to the second, we sleep to make\n      // sure the next commit is after this timestamp\n      Thread.sleep(1000)\n      // create version 11\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      waitForCrcFileToExistElseThrow(path, 10L)\n      waitForCrcFileToExistElseThrow(path, 11L)\n\n      // Test getLatestSnapshot\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtLatest(engine, path),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = false,\n          expectedVersion = Optional.of(11),\n          expectedCheckpointVersion = Optional.of(10),\n          expectedProvidedTimestamp = Optional.empty() // No time travel by timestamp\n        ))\n\n      // Test getSnapshotAsOfVersion\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtVersion(engine, path, 11),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = false,\n          expectedVersion = Optional.of(11),\n          expectedCheckpointVersion = Optional.of(10),\n          expectedProvidedTimestamp = Optional.empty() // No time travel by timestamp\n        ))\n\n      // Test getSnapshotAsOfTimestamp\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, version11timestamp),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 2, // latestSnapshot + timeTravelToTimestampSnapshot\n          expectException = false,\n          expectedVersion = Optional.of(10),\n          expectedCheckpointVersion = Optional.of(10),\n          expectedProvidedTimestamp = Optional.of(version11timestamp),\n          expectNonEmptyTimestampToVersionResolutionDuration = true))\n    }\n  }\n\n  test(\"Snapshot report - invalid version (version does not exist)\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      appendData(tablePath = path, isNewTable = true, schema = testSchema, data = Nil)\n\n      // Test getSnapshotAsOfVersion with version 1 (does not exist)\n      // This fails during log segment building\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtVersion(engine, path, 1),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          expectedVersion = Optional.of(1),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n    }\n  }\n\n  test(\"Snapshot report - invalid timestamp (timestamp too early)\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      appendData(tablePath = path, isNewTable = true, schema = testSchema, data = Nil)\n\n      // Test getSnapshotAsOfTimestamp with timestamp=0 (does not exist)\n      // This fails during timestamp -> version resolution\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, 0),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 2, // latestSnapshot + timeTravelToTimestampSnapshot\n          expectException = true,\n          expectedVersion = Optional.empty(),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.of(0),\n          expectNonEmptyTimestampToVersionResolutionDuration = true,\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroBuildLogSegmentDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n    }\n  }\n\n  test(\"Snapshot report - table does not exist\") {\n    withTempDir { tempDir =>\n      // This fails during either log segment building or timestamp -> version resolution\n      val path = tempDir.getCanonicalPath\n\n      // Test getLatestSnapshot\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtLatest(engine, path),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          expectedVersion = Optional.empty(),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n\n      // Test getSnapshotAsOfVersion\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtVersion(engine, path, 0),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          expectedVersion = Optional.of(0),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n\n      // Test getSnapshotAsOfTimestamp\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, 1000),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          expectedVersion = Optional.empty(),\n          expectedCheckpointVersion = Optional.empty(),\n          // Query will fail before timestamp -> version resolution. The failure\n          // will happen when `getLatestSnapshot` is called.\n          expectedProvidedTimestamp = Optional.empty(),\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          // It will first build a lastest snapshot, and a logSegment is built there.\n          expectNonZeroBuildLogSegmentDuration = true,\n          expectNonZeroDurationToGetCrcInfo = false))\n    }\n  }\n\n  test(\"Snapshot report - log is corrupted\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      // Set up table with non-contiguous version (0, 2) which will fail during log segment building\n      // for all the following queries\n      (0 until 3).foreach(_ =>\n        spark.range(3).write.format(\"delta\").mode(\"append\").save(path))\n      assert(\n        new File(FileNames.deltaFile(new Path(tempDir.getCanonicalPath, \"_delta_log\"), 1)).delete())\n\n      // Test getLatestSnapshot\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtLatest(engine, path),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          expectedVersion = Optional.empty(),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n\n      // Test getSnapshotAsOfVersion\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtVersion(engine, path, 2),\n        path,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          expectedVersion = Optional.of(2),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n\n      // Test getSnapshotAsOfTimestamp\n      val version2Timestamp = new File(\n        FileNames.deltaFile(new Path(tempDir.getCanonicalPath, \"_delta_log\"), 2)).lastModified()\n      checkSnapshotReport(\n        (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, version2Timestamp),\n        tempDir.getCanonicalPath,\n        SnapshotReportExpectations(\n          expectedReportCount = 1,\n          expectException = true,\n          // Query will fail before timestamp -> version resolution. The failure\n          // will happen when `getLatestSnapshot` is called.\n          expectedVersion = Optional.empty(),\n          expectedCheckpointVersion = Optional.empty(),\n          expectedProvidedTimestamp = Optional.empty(),\n          expectNonZeroLoadProtocolAndMetadataDuration = false,\n          expectNonZeroDurationToGetCrcInfo = false))\n    }\n  }\n\n  test(\"Snapshot report - missing metadata\") {\n    // This fails during P&M loading for all of the following queries\n    val path = goldenTablePath(\"deltalog-state-reconstruction-without-metadata\")\n\n    // Test getLatestSnapshot\n    checkSnapshotReport(\n      (engine, path) => tableManager.getSnapshotAtLatest(engine, path),\n      path,\n      SnapshotReportExpectations(\n        expectedReportCount = 1,\n        expectException = true,\n        expectedVersion = Optional.of(0),\n        expectedCheckpointVersion = Optional.empty(),\n        expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n        // No CRC for golden table\n        expectNonZeroDurationToGetCrcInfo = false))\n\n    // Test getSnapshotAsOfVersion\n    checkSnapshotReport(\n      (engine, path) => tableManager.getSnapshotAtVersion(engine, path, 0),\n      path,\n      SnapshotReportExpectations(\n        expectedReportCount = 1,\n        expectException = true,\n        expectedVersion = Optional.of(0),\n        expectedCheckpointVersion = Optional.empty(),\n        expectedProvidedTimestamp = Optional.empty(), // No time travel by timestamp\n        // No CRC for golden table\n        expectNonZeroDurationToGetCrcInfo = false))\n\n    // Test getSnapshotAsOfTimestamp\n    // We use the timestamp of version 0\n    val version0Timestamp = new File(FileNames.deltaFile(new Path(path, \"_delta_log\"), 0))\n      .lastModified()\n    checkSnapshotReport(\n      (engine, path) => tableManager.getSnapshotAtTimestamp(engine, path, version0Timestamp),\n      path,\n      SnapshotReportExpectations(\n        expectedReportCount = 1,\n        expectException = true,\n        // Query will fail before timestamp -> version resolution. The failure\n        // will happen when `getLatestSnapshot` is called.\n        expectedVersion = Optional.of(0),\n        expectedCheckpointVersion = Optional.empty(),\n        expectedProvidedTimestamp = Optional.empty(),\n        // This is due to the `getLatestSnapshot` call\n        expectNonZeroLoadProtocolAndMetadataDuration = true,\n        // No CRC for golden table\n        expectNonZeroDurationToGetCrcInfo = false))\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/metrics/TransactionReportSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.metrics\n\nimport java.util.{Collections, Objects, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\n\nimport io.delta.kernel._\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.utils.{AbstractWriteUtils, WriteUtilsWithV1Builders, WriteUtilsWithV2Builders}\nimport io.delta.kernel.engine._\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.internal.{TableConfig, TableImpl}\nimport io.delta.kernel.internal.actions.{GenerateIcebergCompatActionUtils, SingleAction}\nimport io.delta.kernel.internal.data.TransactionStateRow\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.metrics.Timer\nimport io.delta.kernel.internal.stats.FileSizeHistogram\nimport io.delta.kernel.internal.util.Utils\nimport io.delta.kernel.metrics.{FileSizeHistogramResult, SnapshotReport, TransactionMetricsResult, TransactionReport}\nimport io.delta.kernel.types.{IntegerType, StructType}\nimport io.delta.kernel.utils.{CloseableIterable, CloseableIterator, DataFileStatus}\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TransactionReportTransactionBuilderV1Suite extends AbstractTransactionReportSuite\n    with WriteUtilsWithV1Builders\n\nclass TransactionReportTransactionBuilderV2Suite extends AbstractTransactionReportSuite\n    with WriteUtilsWithV2Builders\n\ntrait AbstractTransactionReportSuite extends AnyFunSuite with MetricsReportTestUtils {\n  self: AbstractWriteUtils =>\n\n  /**\n   * Creates a [[Transaction]] using `getTransaction`, requests actions to commit using\n   * `generateCommitActions`, and commits them to the transaction. Uses a custom engine for all\n   * of these operations that collects any emitted metrics reports. Exactly 1 [[TransactionReport]]\n   * is expected to be emitted, and at most one [[SnapshotReport]]. Also times and returns the\n   * duration it takes for [[Transaction#commit]] to finish.\n   *\n   * @param createTransaction given an engine return a started [[Transaction]]\n   * @param generateCommitActions given a [[Transaction]] and engine generates actions to commit\n   * @param expectException whether we expect committing to throw an exception, which if so, is\n   *                        caught and returned with the other results\n   * @return (TransactionReport, durationToCommit, SnapshotReportIfPresent, ExceptionIfThrown)\n   */\n  def getTransactionAndSnapshotReport(\n      createTransaction: Engine => Transaction,\n      generateCommitActions: (Transaction, Engine) => CloseableIterable[Row],\n      expectException: Boolean,\n      validateTransactionMetrics: (TransactionMetricsResult, Long) => Unit)\n      : (TransactionReport, Long, Option[SnapshotReport], Option[Exception]) = {\n    val timer = new Timer()\n\n    val (metricsReports, exception) = collectMetricsReports(\n      engine => {\n        val transaction = createTransaction(engine)\n        val actionsToCommit = generateCommitActions(transaction, engine)\n        val txnCommitResult = timer.time(() =>\n          transaction.commit(engine, actionsToCommit)) // Time the actual operation\n        // Validate the txn metrics returned in txnCommitResult\n        validateTransactionMetrics(\n          txnCommitResult.getTransactionReport.getTransactionMetrics,\n          timer.totalDurationNs())\n      },\n      expectException)\n\n    val transactionReports = metricsReports.filter(_.isInstanceOf[TransactionReport])\n    assert(transactionReports.length == 1, \"Expected exactly 1 TransactionReport\")\n    val snapshotReports = metricsReports.filter(_.isInstanceOf[SnapshotReport])\n    assert(snapshotReports.length <= 1, \"Expected at most 1 SnapshotReport\")\n    (\n      transactionReports.head.asInstanceOf[TransactionReport],\n      timer.totalDurationNs(),\n      snapshotReports.headOption.map(_.asInstanceOf[SnapshotReport]),\n      exception)\n  }\n\n  /**\n   * Builds a transaction using `getTransaction` for the table at the provided path. Commits\n   * to the transaction the actions generated by `generateCommitActions` and collects any emitted\n   * [[TransactionReport]]. Checks that the report is as expected\n   *\n   * @param generateCommitActions function to generate commit actions from a transaction and engine\n   * @param path table path to commit to\n   * @param expectException whether we expect committing to throw an exception\n   * @param expectedBaseSnapshotVersion expected snapshot version for the transaction\n   * @param expectedClusteringColumns expected clustering columns for the transaction\n   * @param expectedNumAddFiles expected number of add files recorded in the metrics\n   * @param expectedNumRemoveFiles expected number of remove files recorded in the metrics\n   * @param expectedNumTotalActions expected number of total actions recorded in the metrics\n   * @param expectedCommitVersion expected commit version if not `expectException`\n   * @param expectedNumAttempts expected number of commit attempts\n   * @param getTransaction function to build a transaction from a transaction builder\n   * @param engineInfo engine info to create the transaction with\n   * @param operation operation to create the transaction with\n   */\n  // scalastyle:off\n  def checkTransactionReport(\n      generateCommitActions: (Transaction, Engine) => CloseableIterable[Row],\n      path: String,\n      expectException: Boolean,\n      expectedBaseSnapshotVersion: Long,\n      getTransaction: (Engine) => Transaction,\n      expectedClusteringColumns: Seq[Column] = Seq.empty,\n      expectedNumAddFiles: Long = 0,\n      expectedNumRemoveFiles: Long = 0,\n      expectedNumTotalActions: Long = 0,\n      expectedCommitVersion: Option[Long] = None,\n      expectedNumAttempts: Long = 1,\n      expectedTotalAddFilesSizeInBytes: Long = 0,\n      expectedTotalRemoveFilesSizeInBytes: Long = 0,\n      expectedFileSizeHistogramResult: Option[FileSizeHistogramResult] = None,\n      operation: Operation = Operation.WRITE): Unit = {\n    // scalastyle:on\n    assert(expectException == expectedCommitVersion.isEmpty)\n    def validateTransactionMetrics(txnMetrics: TransactionMetricsResult, duration: Long): Unit = {\n      // Since we cannot know the actual duration of commit we sanity check that they are > 0 and\n      // less than the total operation duration\n      assert(txnMetrics.getTotalCommitDurationNs > 0)\n      assert(txnMetrics.getTotalCommitDurationNs < duration)\n\n      assert(txnMetrics.getNumCommitAttempts == expectedNumAttempts)\n      assert(txnMetrics.getNumAddFiles == expectedNumAddFiles)\n      assert(txnMetrics.getTotalAddFilesSizeInBytes == expectedTotalAddFilesSizeInBytes)\n      assert(txnMetrics.getNumRemoveFiles == expectedNumRemoveFiles)\n      assert(txnMetrics.getNumTotalActions == expectedNumTotalActions)\n      assert(txnMetrics.getTotalRemoveFilesSizeInBytes == expectedTotalRemoveFilesSizeInBytes)\n\n      // For now since we don't support writing fileSizeHistogram yet we only expect this to be\n      // present on the first write to a table. We will update these tests when we add write\n      // support.\n      expectedFileSizeHistogramResult match {\n        case Some(expectedHistogram) =>\n          assert(txnMetrics.getTableFileSizeHistogram.isPresent)\n          txnMetrics.getTableFileSizeHistogram.toScala.foreach { foundHistogram =>\n            assert(expectedHistogram.getSortedBinBoundaries sameElements\n              foundHistogram.getSortedBinBoundaries)\n            assert(expectedHistogram.getFileCounts sameElements foundHistogram.getFileCounts)\n            assert(expectedHistogram.getTotalBytes sameElements foundHistogram.getTotalBytes)\n          }\n        case None => assert(!txnMetrics.getTableFileSizeHistogram.isPresent)\n      }\n    }\n\n    val (transactionReport, duration, snapshotReportOpt, exception) =\n      getTransactionAndSnapshotReport(\n        getTransaction,\n        generateCommitActions,\n        expectException,\n        validateTransactionMetrics)\n\n    // Verify contents\n    assert(transactionReport.getTablePath == defaultEngine.getFileSystemClient.resolvePath(path))\n    assert(transactionReport.getOperationType == \"Transaction\")\n    exception match {\n      case Some(e) =>\n        assert(transactionReport.getException().isPresent &&\n          Objects.equals(transactionReport.getException().get(), e))\n      case None => assert(!transactionReport.getException().isPresent)\n    }\n    assert(transactionReport.getReportUUID != null)\n    assert(transactionReport.getOperation == operation.toString)\n    assert(transactionReport.getEngineInfo == \"test-engine\")\n\n    assert(transactionReport.getBaseSnapshotVersion == expectedBaseSnapshotVersion)\n    if (expectedBaseSnapshotVersion < 0) {\n      // This was for a new table, there is no corresponding SnapshotReport\n      assert(!transactionReport.getSnapshotReportUUID.isPresent)\n    } else {\n      assert(snapshotReportOpt.exists { snapshotReport =>\n        snapshotReport.getVersion.toScala.contains(expectedBaseSnapshotVersion) &&\n        transactionReport.getSnapshotReportUUID.toScala.contains(snapshotReport.getReportUUID)\n      })\n    }\n    assert(transactionReport.getClusteringColumns.asScala == expectedClusteringColumns)\n    assert(transactionReport.getCommittedVersion.toScala == expectedCommitVersion)\n    validateTransactionMetrics(transactionReport.getTransactionMetrics, duration)\n  }\n\n  def generateAppendActions(fileStatusIter: CloseableIterator[DataFileStatus])(\n      trans: Transaction,\n      engine: Engine): CloseableIterable[Row] = {\n    val transState = trans.getTransactionState(engine)\n    CloseableIterable.inMemoryIterable(\n      Transaction.generateAppendActions(\n        engine,\n        transState,\n        fileStatusIter,\n        Transaction.getWriteContext(engine, transState, Collections.emptyMap())))\n  }\n\n  def generateRemoveActions(fileStatusIter: CloseableIterator[DataFileStatus])(\n      trans: Transaction,\n      engine: Engine): CloseableIterable[Row] = {\n    // For now we use GenerateIcebergCompatActionUtils to generate the remove rows since this is the\n    // only current API support in Kernel for generating removes; in the future when we support a\n    // more general API for removes we should use that here\n    inMemoryIterable(fileStatusIter.map { fileStatus =>\n      SingleAction.createRemoveFileSingleAction(\n        GenerateIcebergCompatActionUtils.convertRemoveDataFileStatus(\n          TransactionStateRow.getPhysicalSchema(trans.getTransactionState(engine)),\n          new Path(TransactionStateRow.getTablePath(trans.getTransactionState(engine))).toUri,\n          fileStatus,\n          Collections.emptyMap(), // partitionValues\n          true, // dataChange,\n          Optional.empty(), // baseRowId\n          Optional.empty(), // defaultRowCommitVersion\n          Optional.empty() // deletionVectorDescriptor\n        ))\n    })\n  }\n\n  def incrementFileSizeHistogram(\n      histogram: FileSizeHistogram,\n      fileStatusIter: CloseableIterator[DataFileStatus]): FileSizeHistogram = {\n    fileStatusIter.forEach(fs => histogram.insert(fs.getSize))\n    histogram\n  }\n\n  test(\"TransactionReport: Basic append to existing table + update metadata\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      // Set up delta table with version 0\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      // Commit 1 AddFiles\n      checkTransactionReport(\n        generateCommitActions = generateAppendActions(fileStatusIter1),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 0,\n        getTransaction = (e) => getUpdateTxn(e, path),\n        expectedNumAddFiles = 1,\n        expectedNumTotalActions = 2, // commitInfo + addFile\n        expectedCommitVersion = Some(1),\n        expectedTotalAddFilesSizeInBytes = 100)\n\n      // Commit 2 AddFiles\n      checkTransactionReport(\n        generateCommitActions = generateAppendActions(fileStatusIter2),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 1,\n        getTransaction = (e) => getUpdateTxn(e, path),\n        expectedNumAddFiles = 2,\n        expectedNumTotalActions = 3, // commitInfo + addFile\n        expectedCommitVersion = Some(2),\n        expectedTotalAddFilesSizeInBytes = 200)\n\n      // Update metadata only\n      checkTransactionReport(\n        generateCommitActions = (_, _) => CloseableIterable.emptyIterable(),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 2,\n        getTransaction = engine =>\n          getUpdateTxn(\n            engine,\n            path,\n            tableProperties = Map(TableConfig.CHECKPOINT_INTERVAL.getKey -> \"2\")),\n        expectedNumTotalActions = 2, // metadata, commitInfo\n        expectedCommitVersion = Some(3))\n    }\n  }\n\n  test(\"TransactionReport: Create new empty table and then append\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      checkTransactionReport(\n        generateCommitActions = (_, _) => CloseableIterable.emptyIterable(),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = -1,\n        getTransaction =\n          (engine) => getCreateTxn(engine, path, new StructType().add(\"id\", IntegerType.INTEGER)),\n        expectedNumTotalActions = 3, // protocol, metadata, commitInfo\n        expectedCommitVersion = Some(0),\n        expectedFileSizeHistogramResult = Some(\n          FileSizeHistogram.createDefaultHistogram().captureFileSizeHistogramResult()),\n        operation = Operation.CREATE_TABLE)\n\n      // Commit 2 AddFiles\n      checkTransactionReport(\n        generateCommitActions = generateAppendActions(fileStatusIter2),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 0,\n        getTransaction = (e) => getUpdateTxn(e, path),\n        expectedNumAddFiles = 2,\n        expectedNumTotalActions = 3, // commitInfo + addFile\n        expectedCommitVersion = Some(1),\n        expectedTotalAddFilesSizeInBytes = 200)\n    }\n  }\n\n  test(\"TransactionReport: Create new non-empty table with insert\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      checkTransactionReport(\n        generateCommitActions = generateAppendActions(fileStatusIter1),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = -1,\n        getTransaction =\n          (engine) => getCreateTxn(engine, path, new StructType().add(\"id\", IntegerType.INTEGER)),\n        expectedNumAddFiles = 1,\n        expectedNumTotalActions = 4, // protocol, metadata, commitInfo\n        expectedCommitVersion = Some(0),\n        expectedTotalAddFilesSizeInBytes = 100,\n        expectedFileSizeHistogramResult = Some(\n          incrementFileSizeHistogram(\n            FileSizeHistogram.createDefaultHistogram(),\n            fileStatusIter1).captureFileSizeHistogramResult()),\n        operation = Operation.CREATE_TABLE)\n    }\n  }\n\n  test(\"TransactionReport: remove files from a table\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      // Create a table and insert 1 file into it\n      checkTransactionReport(\n        generateCommitActions = generateAppendActions(fileStatusIter1),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = -1,\n        getTransaction =\n          (engine) => getCreateTxn(engine, path, new StructType().add(\"id\", IntegerType.INTEGER)),\n        expectedNumAddFiles = 1,\n        expectedNumTotalActions = 4, // protocol, metadata, commitInfo, addFile\n        expectedCommitVersion = Some(0),\n        expectedTotalAddFilesSizeInBytes = 100,\n        expectedFileSizeHistogramResult = Some(\n          incrementFileSizeHistogram(\n            FileSizeHistogram.createDefaultHistogram(),\n            fileStatusIter1).captureFileSizeHistogramResult()),\n        operation = Operation.CREATE_TABLE)\n\n      // Remove the 1 file and insert 2 new ones\n      checkTransactionReport(\n        generateCommitActions = (txn, engine) =>\n          inMemoryIterable(generateAppendActions(fileStatusIter2)(txn, engine).iterator().combine(\n            generateRemoveActions(fileStatusIter1)(txn, engine).iterator())),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 0,\n        expectedNumAddFiles = 2,\n        expectedNumRemoveFiles = 1,\n        expectedNumTotalActions = 4, // commitInfo, removeFile, 2 addFile\n        getTransaction = (e) => getUpdateTxn(e, path),\n        expectedCommitVersion = Some(1),\n        expectedTotalAddFilesSizeInBytes = 200,\n        expectedTotalRemoveFilesSizeInBytes = 100)\n\n      // Remove the two files inserted\n      checkTransactionReport(\n        generateCommitActions = generateRemoveActions(fileStatusIter2),\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 1,\n        getTransaction = (e) => getUpdateTxn(e, path),\n        expectedNumRemoveFiles = 2,\n        expectedNumTotalActions = 3, // commitInfo, 2 removeFile\n        expectedCommitVersion = Some(2),\n        expectedTotalRemoveFilesSizeInBytes = 200)\n    }\n  }\n\n  test(\"TransactionReport: retry with a concurrent append\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      // Set up delta table with version 0\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      checkTransactionReport(\n        generateCommitActions = (trans, engine) => {\n          spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n          generateAppendActions(fileStatusIter1)(trans, engine)\n        },\n        path,\n        expectException = false,\n        expectedBaseSnapshotVersion = 0,\n        expectedNumAddFiles = 1,\n        expectedNumTotalActions = 2, // commitInfo + removeFile\n        getTransaction = (e) => getUpdateTxn(e, path),\n        expectedCommitVersion = Some(2),\n        expectedNumAttempts = 2,\n        expectedTotalAddFilesSizeInBytes = 100,\n        // This should always be empty on retries until we support updating based on concurrent txn\n        expectedFileSizeHistogramResult = None)\n    }\n  }\n\n  test(\"TransactionReport: fail due to conflicting write\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      // Set up delta table with version 0\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      checkTransactionReport(\n        generateCommitActions = (trans, engine) => {\n          spark.sql(\"ALTER TABLE delta.`\" + path + \"` ADD COLUMN newCol INT\")\n          generateAppendActions(fileStatusIter1)(trans, engine)\n        },\n        path,\n        expectException = true,\n        expectedBaseSnapshotVersion = 0,\n        getTransaction = (e) => getUpdateTxn(e, path))\n    }\n  }\n\n  test(\"TransactionReport: fail due to too many tries\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      // Set up delta table with version 0\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      // This writes a concurrent append everytime the iterable is asked for an iterator. This means\n      // there should be a conflicting transaction committed everytime Kernel tries to commit\n      def actionsIterableWithConcurrentAppend(\n          trans: Transaction,\n          engine: Engine): CloseableIterable[Row] = {\n        val transState = trans.getTransactionState(engine)\n        val writeContext = Transaction.getWriteContext(engine, transState, Collections.emptyMap())\n\n        new CloseableIterable[Row] {\n\n          override def iterator(): CloseableIterator[Row] = {\n            spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n            Transaction.generateAppendActions(engine, transState, fileStatusIter1, writeContext)\n          }\n\n          override def close(): Unit = ()\n        }\n      }\n\n      checkTransactionReport(\n        generateCommitActions = actionsIterableWithConcurrentAppend,\n        path,\n        expectException = true,\n        expectedBaseSnapshotVersion = 0,\n        getTransaction = (engine) => getUpdateTxn(engine, path, maxRetries = 5),\n        expectedNumAttempts = 6\n      ) // 1 first try + 6 retries\n    }\n  }\n\n  Seq(true, false).foreach { includeData =>\n    test(s\"TransactionReport: REPLACE a non-empty table, includeData=$includeData\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n        // Set up a non-empty table at version 0 with add file size that we know\n        val txn = getCreateTxn(\n          defaultEngine,\n          path,\n          new StructType().add(\"col1\", IntegerType.INTEGER),\n          withDomainMetadataSupported = true)\n        txn.addDomainMetadata(\"user-domain\", \"some config\")\n        val result = txn.commit(\n          defaultEngine,\n          generateAppendActions(fileStatusIter1)(txn, defaultEngine))\n        // Write out the CRC so that we will have fileSizeHistogram in the next commit\n        result.getPostCommitHooks.asScala.foreach(_.threadSafeInvoke(defaultEngine))\n\n        def generateCommitActions: (Transaction, Engine) => CloseableIterable[Row] =\n          if (!includeData) {\n            case (_, _) => emptyIterable()\n          }\n          else {\n            generateAppendActions(fileStatusIter1)\n          }\n        val numAddFiles = if (includeData) 1 else 0\n        val expectedFileSizeHistogram = if (includeData) {\n          incrementFileSizeHistogram(\n            FileSizeHistogram.createDefaultHistogram(),\n            fileStatusIter1).captureFileSizeHistogramResult()\n        } else {\n          FileSizeHistogram.createDefaultHistogram().captureFileSizeHistogramResult()\n        }\n\n        // Check TransactionReport for REPLACE operation\n        checkTransactionReport(\n          generateCommitActions,\n          path,\n          expectException = false,\n          expectedBaseSnapshotVersion = 0,\n          expectedNumAddFiles = numAddFiles,\n          expectedNumRemoveFiles = 1,\n          // protocol, metadata, commitInfo, domainMetadata (tombstone)\n          expectedNumTotalActions = numAddFiles + 5,\n          getTransaction = (engine) =>\n            getReplaceTxn(engine, path, new StructType().add(\"id\", IntegerType.INTEGER)),\n          expectedCommitVersion = Some(1),\n          expectedTotalRemoveFilesSizeInBytes = 100,\n          expectedTotalAddFilesSizeInBytes = 100 * numAddFiles,\n          expectedFileSizeHistogramResult = Some(expectedFileSizeHistogram),\n          operation = Operation.REPLACE_TABLE)\n      }\n    }\n  }\n\n  test(\"TransactionReport: clustering columns are in transaction report\") {\n    withTempDirAndEngine { (tablePath, engine) =>\n      val testSchema = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\"name\", IntegerType.INTEGER)\n        .add(\n          \"nested\",\n          new StructType()\n            .add(\"nestedId\", IntegerType.INTEGER)\n            .add(\"nestedName\", IntegerType.INTEGER))\n\n      // create table\n      checkTransactionReport(\n        generateCommitActions = (_, _) => CloseableIterable.emptyIterable(),\n        tablePath,\n        expectException = false,\n        expectedBaseSnapshotVersion = -1,\n        expectedCommitVersion = Some(0),\n        expectedNumTotalActions = 4, // protocol, metadata, commitInfo, domainMetadata\n        getTransaction = (engine) =>\n          getCreateTxn(\n            engine,\n            tablePath,\n            testSchema,\n            clusteringColsOpt = Some(List(\n              new Column(\"id\"),\n              new Column(Array[String](\"nested\", \"nestedId\"))))),\n        expectedClusteringColumns = Seq(\n          new Column(\"id\"),\n          new Column(Array[String](\"nested\", \"nestedId\"))),\n        expectedFileSizeHistogramResult = Some(\n          FileSizeHistogram.createDefaultHistogram().captureFileSizeHistogramResult()),\n        operation = Operation.CREATE_TABLE)\n\n      // update table (no clustering column change)\n      checkTransactionReport(\n        generateCommitActions = generateAppendActions(fileStatusIter1),\n        tablePath,\n        expectException = false,\n        expectedBaseSnapshotVersion = 0,\n        getTransaction = (engine) => getUpdateTxn(engine, tablePath),\n        expectedCommitVersion = Some(1),\n        expectedNumTotalActions = 2, // commitInfo, one add file\n        expectedNumAddFiles = 1,\n        expectedTotalAddFilesSizeInBytes = 100,\n        expectedClusteringColumns = Seq(\n          new Column(\"id\"),\n          new Column(Array[String](\"nested\", \"nestedId\"))))\n\n      // update clustering columns\n      checkTransactionReport(\n        generateCommitActions = (_, _) => CloseableIterable.emptyIterable(),\n        tablePath,\n        expectException = false,\n        expectedBaseSnapshotVersion = 1,\n        expectedCommitVersion = Some(2),\n        getTransaction = (engine) =>\n          getUpdateTxn(\n            engine,\n            tablePath,\n            clusteringColsOpt = Some(List(\n              new Column(\"name\"),\n              new Column(Array[String](\"nested\", \"nestedName\"))))),\n        expectedNumTotalActions = 2, // commitInfo, domainMetadata\n        expectedClusteringColumns = Seq(\n          new Column(\"name\"),\n          new Column(Array[String](\"nested\", \"nestedName\"))))\n\n      // replace table (with no new clustering columns)\n      checkTransactionReport(\n        generateCommitActions = (_, _) => CloseableIterable.emptyIterable(),\n        tablePath,\n        expectException = false,\n        expectedBaseSnapshotVersion = 2,\n        expectedNumAddFiles = 0,\n        expectedNumRemoveFiles = 1,\n        getTransaction =\n          (engine) => getReplaceTxn(engine, tablePath, testSchema.add(\"id2\", IntegerType.INTEGER)),\n        // protocol, metadata, commitInfo, remove file, domainMetadata (tombstone)\n        expectedNumTotalActions = 5,\n        expectedCommitVersion = Some(3),\n        expectedTotalRemoveFilesSizeInBytes = 100,\n        expectedTotalAddFilesSizeInBytes = 0,\n        operation = Operation.REPLACE_TABLE)\n\n      // replace the table with new clustering columns\n      checkTransactionReport(\n        generateCommitActions = (_, _) => CloseableIterable.emptyIterable(),\n        tablePath,\n        expectException = false,\n        expectedBaseSnapshotVersion = 3,\n        expectedClusteringColumns = Seq(\n          new Column(\"id3\"),\n          new Column(Array[String](\"nested\", \"nestedName\"))),\n        expectedNumAddFiles = 0,\n        expectedNumRemoveFiles = 0,\n        getTransaction = (engine) =>\n          getReplaceTxn(\n            engine,\n            tablePath,\n            testSchema.add(\"id3\", IntegerType.INTEGER),\n            clusteringColsOpt = Some(Seq(\n              new Column(\"id3\"),\n              new Column(Array[String](\"nested\", \"nestedName\"))))),\n        // protocol, metadata, commitInfo, domainMetadata (new one for clustering cols)\n        expectedNumTotalActions = 4,\n        expectedCommitVersion = Some(4),\n        expectedTotalRemoveFilesSizeInBytes = 0,\n        expectedTotalAddFilesSizeInBytes = 0,\n        operation = Operation.REPLACE_TABLE)\n    }\n  }\n\n  /////////////////////\n  // Test Constants //\n  ////////////////////\n\n  private def fileStatusIter1 = Utils.toCloseableIterator(\n    Seq(new DataFileStatus(\"/path/to/file\", 100, 100, Optional.empty())).iterator.asJava)\n\n  private def fileStatusIter2 = Utils.toCloseableIterator(\n    Seq(\n      new DataFileStatus(\"/path/to/file1\", 100, 100, Optional.empty()),\n      new DataFileStatus(\"/path/to/file2\", 100, 100, Optional.empty())).iterator.asJava)\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/test/AbstractTableManagerAdapter.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.test\n\nimport io.delta.kernel.{Table, TableManager}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.internal.{SnapshotImpl, TableImpl}\nimport io.delta.kernel.internal.table.SnapshotBuilderImpl\n\n/**\n * Test framework adapter that provides a unified interface for **loading** Delta tables.\n *\n * This trait enables test suites to be parameterized over different Kernel APIs via the\n * [[LegacyTableManagerAdapter]] and [[TableManagerAdapter]] child classes.\n *\n * By using this adapter pattern, the same test suite can verify both APIs work correctly, without\n * duplicating test logic.\n *\n * Tests can switch between implementations by mixing in either\n * [[io.delta.kernel.defaults.utils.TestUtilsWithLegacyKernelAPIs]] or\n * [[io.delta.kernel.defaults.utils.TestUtilsWithTableManagerAPIs]].\n */\ntrait AbstractTableManagerAdapter {\n\n  /**\n   * Does this adapter support resolving a timestamp to a version?\n   *\n   * e.g. getVersionBeforeOrAtTimestamp and getVersionAtOrAfterTimestamp\n   *\n   * This is different from loading a snapshot at a specific timestamp, which is supported by all\n   * adapter implementations.\n   */\n  def supportsTimestampResolution: Boolean\n\n  def getSnapshotAtLatest(engine: Engine, path: String): SnapshotImpl\n\n  def getSnapshotAtVersion(engine: Engine, path: String, version: Long): SnapshotImpl\n\n  def getSnapshotAtTimestamp(engine: Engine, path: String, timestamp: Long): SnapshotImpl\n}\n\n/**\n * Legacy implementation using the [[Table.forPath]] API.\n */\nclass LegacyTableManagerAdapter extends AbstractTableManagerAdapter {\n  override def supportsTimestampResolution: Boolean = true\n\n  override def getSnapshotAtLatest(\n      engine: Engine,\n      path: String): SnapshotImpl = {\n    Table.forPath(engine, path).asInstanceOf[TableImpl].getLatestSnapshot(engine)\n  }\n\n  override def getSnapshotAtVersion(\n      engine: Engine,\n      path: String,\n      version: Long): SnapshotImpl = {\n    Table.forPath(engine, path).asInstanceOf[TableImpl].getSnapshotAsOfVersion(engine, version)\n  }\n\n  override def getSnapshotAtTimestamp(\n      engine: Engine,\n      path: String,\n      timestamp: Long): SnapshotImpl = {\n    Table.forPath(engine, path).asInstanceOf[TableImpl].getSnapshotAsOfTimestamp(engine, timestamp)\n  }\n}\n\n/**\n * New implementation using the [[TableManager.loadSnapshot]] API.\n */\nclass TableManagerAdapter extends AbstractTableManagerAdapter {\n  override def supportsTimestampResolution: Boolean = false\n\n  override def getSnapshotAtLatest(\n      engine: Engine,\n      path: String): SnapshotImpl = {\n    TableManager.loadSnapshot(path).asInstanceOf[SnapshotBuilderImpl].build(engine)\n  }\n\n  override def getSnapshotAtVersion(\n      engine: Engine,\n      path: String,\n      version: Long): SnapshotImpl = {\n    TableManager\n      .loadSnapshot(path).asInstanceOf[SnapshotBuilderImpl].atVersion(version).build(engine)\n  }\n\n  override def getSnapshotAtTimestamp(\n      engine: Engine,\n      path: String,\n      timestamp: Long): SnapshotImpl = {\n    TableManager\n      .loadSnapshot(path)\n      .asInstanceOf[SnapshotBuilderImpl]\n      .atTimestamp(timestamp, getSnapshotAtLatest(engine, path))\n      .build(engine)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/DefaultVectorTestUtils.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.utils\n\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.test.VectorTestUtils\nimport io.delta.kernel.types._\n\ntrait DefaultVectorTestUtils extends VectorTestUtils {\n\n  /**\n   * Returns a [[ColumnarBatch]] with each given vector is a top-level column col_i where i is\n   * the index of the vector in the input list.\n   */\n  protected def columnarBatch(vectors: ColumnVector*): ColumnarBatch = {\n    val numRows = vectors.head.getSize\n    vectors.tail.foreach(v =>\n      require(v.getSize == numRows, \"All vectors should have the same size\"))\n\n    val schema = (0 until vectors.length)\n      .foldLeft(new StructType())((s, i) => s.add(s\"col_$i\", vectors(i).getDataType))\n\n    new DefaultColumnarBatch(numRows, schema, vectors.toArray)\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/ExpressionTestUtils.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.utils\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.expressions._\n\n/** Useful helper functions for creating expressions in tests */\ntrait ExpressionTestUtils {\n\n  def eq(left: Expression, right: Expression): Predicate = predicate(\"=\", left, right)\n  def equals(e1: Expression, e2: Expression): Predicate = eq(e1, e2)\n\n  def lt(e1: Expression, e2: Expression): Predicate = new Predicate(\"<\", e1, e2)\n  def lessThan(e1: Expression, e2: Expression): Predicate = lt(e1, e2)\n\n  def gt(e1: Expression, e2: Expression): Predicate = new Predicate(\">\", e1, e2)\n  def greaterThan(e1: Expression, e2: Expression): Predicate = gt(e1, e2)\n\n  def gte(e1: Expression, e2: Expression): Predicate = predicate(\">=\", e1, e2)\n  def greaterThanOrEqual(e1: Expression, e2: Expression): Predicate = gte(e1, e2)\n\n  def lessThanOrEqual(e1: Expression, e2: Expression): Predicate = new Predicate(\"<=\", e1, e2)\n  def lte(column: Column, literal: Literal): Predicate = predicate(\"<=\", column, literal)\n\n  def not(pred: Predicate): Predicate = new Predicate(\"NOT\", pred)\n\n  def isNotNull(e1: Expression): Predicate = new Predicate(\"IS_NOT_NULL\", e1)\n\n  def col(names: String*): Column = new Column(names.toArray)\n\n  def nestedCol(name: String): Column = {\n    new Column(name.split(\"\\\\.\"))\n  }\n\n  def predicate(name: String, children: Expression*): Predicate = {\n    new Predicate(name, children.asJava)\n  }\n\n  def and(left: Predicate, right: Predicate): Predicate = predicate(\"AND\", left, right)\n\n  def or(left: Predicate, right: Predicate): Predicate = predicate(\"OR\", left, right)\n\n  def int(value: Int): Literal = Literal.ofInt(value)\n\n  def str(value: String): Literal = Literal.ofString(value)\n\n  def nullSafeEquals(e1: Expression, e2: Expression): Predicate = {\n    new Predicate(\"IS NOT DISTINCT FROM\", e1, e2)\n  }\n\n  def unsupported(colName: String): Predicate = predicate(\"UNSUPPORTED\", col(colName));\n\n  /* ---------- NOT-YET SUPPORTED EXPRESSIONS ----------- */\n\n  /*\n  These expressions are used in ScanSuite to test data skipping. For unsupported expressions\n  no skipping filter will be generated and they should just be returned as part of the remaining\n  predicate to evaluate. As we add support for these expressions we'll adjust the tests that use\n  them to expect skipped files. If they are ever actually evaluated they will throw an exception.\n   */\n\n  def notEquals(e1: Expression, e2: Expression): Predicate = new Predicate(\"<>\", e1, e2)\n\n  def startsWith(e1: Expression, e2: Expression): Predicate = new Predicate(\"STARTS_WITH\", e1, e2)\n\n  def isNull(e1: Expression): Predicate = new Predicate(\"IS_NULL\", e1)\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/TestCommitterUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.defaults.utils\n\nimport java.util.Collections\n\nimport io.delta.kernel.commit.{CatalogCommitter, CommitMetadata, CommitResponse, Committer, PublishMetadata}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.internal.files.ParsedPublishedDeltaData\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\ntrait TestCommitterUtils {\n  val committerUsingPutIfAbsent = new Committer {\n    override def commit(\n        engine: Engine,\n        finalizedActions: CloseableIterator[Row],\n        commitMetadata: CommitMetadata): CommitResponse = {\n      val filePath =\n        FileNames.deltaFile(commitMetadata.getDeltaLogDirPath, commitMetadata.getVersion)\n      engine\n        .getJsonHandler\n        .writeJsonFileAtomically(filePath, finalizedActions, false)\n      new CommitResponse(ParsedPublishedDeltaData.forFileStatus(FileStatus.of(filePath)))\n    }\n  }\n\n  val customCatalogCommitter = new CatalogCommitter {\n\n    val REQUIRED_PROPERTY_KEY = \"test.committer.required.foo\"\n    val REQUIRED_PROPERTY_VALUE = \"bar\"\n\n    override def commit(\n        engine: Engine,\n        finalizedActions: CloseableIterator[Row],\n        commitMetadata: CommitMetadata): CommitResponse = {\n      committerUsingPutIfAbsent.commit(engine, finalizedActions, commitMetadata)\n    }\n\n    override def getRequiredTableProperties: java.util.Map[String, String] = {\n      Collections.singletonMap(REQUIRED_PROPERTY_KEY, REQUIRED_PROPERTY_VALUE)\n    }\n\n    override def publish(engine: Engine, publishMetadata: PublishMetadata): Unit = {\n      // No-op\n    }\n\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/TestRow.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.utils\n\nimport java.sql.Timestamp\nimport java.time.{Instant, LocalDate, LocalDateTime, ZoneOffset}\nimport java.time.ZoneOffset.UTC\nimport java.time.temporal.ChronoUnit\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.data.{ArrayValue, ColumnVector, MapValue, Row}\nimport io.delta.kernel.types._\n\nimport org.apache.spark.sql.{Row => SparkRow}\nimport org.apache.spark.sql.{types => sparktypes}\n\n/**\n * Corresponding Scala class for each Kernel data type:\n * - BooleanType --> boolean\n * - ByteType --> byte\n * - ShortType --> short\n * - IntegerType --> int\n * - LongType --> long\n * - FloatType --> float\n * - DoubleType --> double\n * - StringType --> String\n * - DateType --> int (number of days since the epoch)\n * - TimestampType --> long (number of microseconds since the unix epoch)\n * - TimestampNTZType --> long (number of microseconds in local time with no timezone)\n * - DecimalType --> java.math.BigDecimal\n * - BinaryType --> Array[Byte]\n * - ArrayType --> Seq[Any]\n * - MapType --> Map[Any, Any]\n * - StructType --> TestRow\n *\n * For complex types array and map, the inner elements types should align with this mapping.\n */\nclass TestRow(val values: Array[Any]) {\n\n  def length: Int = values.length\n\n  def get(i: Int): Any = values(i)\n\n  def toSeq: Seq[Any] = values.clone()\n\n  def mkString(start: String, sep: String, end: String): String = {\n    val n = length\n    val builder = new StringBuilder\n    builder.append(start)\n    if (n > 0) {\n      builder.append(get(0))\n      var i = 1\n      while (i < n) {\n        builder.append(sep)\n        builder.append(get(i))\n        i += 1\n      }\n    }\n    builder.append(end)\n    builder.toString()\n  }\n\n  override def toString: String = this.mkString(\"[\", \",\", \"]\")\n}\n\nobject TestRow {\n\n  /**\n   * Construct a [[TestRow]] with the given values. See the docs for [[TestRow]] for\n   * the scala type corresponding to each Kernel data type.\n   */\n  def apply(values: Any*): TestRow = {\n    new TestRow(values.toArray)\n  }\n\n  /**\n   * Construct a [[TestRow]] with the same values as a Kernel [[Row]].\n   */\n  def apply(row: Row): TestRow = {\n    TestRow.fromSeq(row.getSchema.fields().asScala.zipWithIndex.map { case (field, i) =>\n      field.getDataType match {\n        case _ if row.isNullAt(i) => null\n        case _: BooleanType => row.getBoolean(i)\n        case _: ByteType => row.getByte(i)\n        case _: IntegerType => row.getInt(i)\n        case _: LongType => row.getLong(i)\n        case _: ShortType => row.getShort(i)\n        case _: DateType => row.getInt(i)\n        case _: TimestampType => row.getLong(i)\n        case _: TimestampNTZType => row.getLong(i)\n        case _: FloatType => row.getFloat(i)\n        case _: DoubleType => row.getDouble(i)\n        case _: StringType => row.getString(i)\n        case _: BinaryType => row.getBinary(i)\n        case _: DecimalType => row.getDecimal(i)\n        case _: ArrayType => arrayValueToScalaSeq(row.getArray(i))\n        case _: MapType => mapValueToScalaMap(row.getMap(i))\n        case _: StructType => TestRow(row.getStruct(i))\n        case _ => throw new UnsupportedOperationException(\"unrecognized data type\")\n      }\n    }.toSeq)\n  }\n\n  def apply(row: SparkRow): TestRow = {\n    def decodeCellValue(dataType: sparktypes.DataType, obj: Any): Any = {\n      dataType match {\n        case _ if obj == null => null\n        case _: sparktypes.BooleanType => obj.asInstanceOf[Boolean]\n        case _: sparktypes.ByteType => obj.asInstanceOf[Byte]\n        case _: sparktypes.IntegerType => obj.asInstanceOf[Int]\n        case _: sparktypes.LongType => obj.asInstanceOf[Long]\n        case _: sparktypes.ShortType => obj.asInstanceOf[Short]\n        case _: sparktypes.DateType => LocalDate.ofEpochDay(obj.asInstanceOf[Int])\n        case _: sparktypes.TimestampType =>\n          ChronoUnit.MICROS.between(Instant.EPOCH, obj.asInstanceOf[Timestamp].toInstant)\n        case _: sparktypes.TimestampNTZType =>\n          ChronoUnit.MICROS.between(Instant.EPOCH, obj.asInstanceOf[LocalDateTime].toInstant(UTC))\n        case _: sparktypes.FloatType => obj.asInstanceOf[Float]\n        case _: sparktypes.DoubleType => obj.asInstanceOf[Double]\n        case _: sparktypes.StringType => obj.asInstanceOf[String]\n        case _: sparktypes.BinaryType => obj.asInstanceOf[Array[Byte]]\n        case _: sparktypes.DecimalType => obj.asInstanceOf[java.math.BigDecimal]\n        case arrayType: sparktypes.ArrayType =>\n          obj.asInstanceOf[Seq[Any]]\n            .map(decodeCellValue(arrayType.elementType, _))\n        case mapType: sparktypes.MapType => obj.asInstanceOf[Map[Any, Any]].map {\n            case (k, v) =>\n              decodeCellValue(mapType.keyType, k) -> decodeCellValue(mapType.valueType, v)\n          }\n        case _: sparktypes.StructType => TestRow(obj.asInstanceOf[SparkRow])\n        case _ => throw new UnsupportedOperationException(\"unrecognized data type\")\n      }\n    }\n\n    TestRow.fromSeq(row.schema.fields.zipWithIndex.map { case (field, i) =>\n      field.dataType match {\n        case _ if row.isNullAt(i) => null\n        case _: sparktypes.BooleanType => row.getBoolean(i)\n        case _: sparktypes.ByteType => row.getByte(i)\n        case _: sparktypes.IntegerType => row.getInt(i)\n        case _: sparktypes.LongType => row.getLong(i)\n        case _: sparktypes.ShortType => row.getShort(i)\n        case _: sparktypes.DateType => row.getDate(i).toLocalDate.toEpochDay.toInt\n        case _: sparktypes.TimestampType =>\n          ChronoUnit.MICROS.between(Instant.EPOCH, row.getTimestamp(i).toInstant)\n        case _: sparktypes.TimestampNTZType =>\n          ChronoUnit.MICROS.between(Instant.EPOCH, row.getAs[LocalDateTime](i).toInstant(UTC))\n        case _: sparktypes.FloatType => row.getFloat(i)\n        case _: sparktypes.DoubleType => row.getDouble(i)\n        case _: sparktypes.StringType => row.getString(i)\n        case _: sparktypes.BinaryType => row(i) // return as byte[], there is no getBinary method\n        case _: sparktypes.DecimalType => row.getDecimal(i)\n        case arrayType: sparktypes.ArrayType =>\n          val arrayValue = row.getSeq[Any](i)\n          arrayValue.indices.map { i =>\n            decodeCellValue(arrayType.elementType, arrayValue(i));\n          }\n        case mapType: sparktypes.MapType =>\n          val mapValue = row.getMap[Any, Any](i)\n          mapValue.map { case (k, v) =>\n            decodeCellValue(mapType.keyType, k) -> decodeCellValue(mapType.valueType, v)\n          }\n        case _: sparktypes.StructType => TestRow(row.getStruct(i))\n        case _ => throw new UnsupportedOperationException(\"unrecognized data type\")\n      }\n    })\n  }\n\n  /**\n   * Retrieves the value at `rowId` in the column vector as it's corresponding scala type.\n   * See the [[TestRow]] docs for details.\n   */\n  private def getAsTestObject(vector: ColumnVector, rowId: Int): Any = {\n    vector.getDataType match {\n      case _ if vector.isNullAt(rowId) => null\n      case _: BooleanType => vector.getBoolean(rowId)\n      case _: ByteType => vector.getByte(rowId)\n      case _: IntegerType => vector.getInt(rowId)\n      case _: LongType => vector.getLong(rowId)\n      case _: ShortType => vector.getShort(rowId)\n      case _: DateType => vector.getInt(rowId)\n      case _: TimestampType => vector.getLong(rowId)\n      case _: TimestampNTZType => vector.getLong(rowId)\n      case _: FloatType => vector.getFloat(rowId)\n      case _: DoubleType => vector.getDouble(rowId)\n      case _: StringType => vector.getString(rowId)\n      case _: BinaryType => vector.getBinary(rowId)\n      case _: DecimalType => vector.getDecimal(rowId)\n      case _: ArrayType => arrayValueToScalaSeq(vector.getArray(rowId))\n      case _: MapType => mapValueToScalaMap(vector.getMap(rowId))\n      case dataType: StructType =>\n        TestRow.fromSeq(Seq.range(0, dataType.length()).map { ordinal =>\n          getAsTestObject(vector.getChild(ordinal), rowId)\n        })\n      case _ => throw new UnsupportedOperationException(\"unrecognized data type\")\n    }\n  }\n\n  private def arrayValueToScalaSeq(arrayValue: ArrayValue): Seq[Any] = {\n    val elemVector = arrayValue.getElements\n    (0 until arrayValue.getSize).map { i =>\n      getAsTestObject(elemVector, i)\n    }\n  }\n\n  private def mapValueToScalaMap(mapValue: MapValue): Map[Any, Any] = {\n    val keyVector = mapValue.getKeys()\n    val valueVector = mapValue.getValues()\n    (0 until mapValue.getSize).map { i =>\n      getAsTestObject(keyVector, i) -> getAsTestObject(valueVector, i)\n    }.toMap\n  }\n\n  /**\n   * Construct a [[TestRow]] from the given seq of values. See the docs for [[TestRow]] for\n   * the scala type corresponding to each Kernel data type.\n   */\n  def fromSeq(values: Seq[Any]): TestRow = {\n    new TestRow(values.toArray)\n  }\n\n  /**\n   * Construct a [[TestRow]] with the elements of the given tuple. See the docs for\n   * [[TestRow]] for the scala type corresponding to each Kernel data type.\n   */\n  def fromTuple(tuple: Product): TestRow = fromSeq(tuple.productIterator.toSeq)\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/TestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.utils\n\nimport java.io.{File, FileNotFoundException}\nimport java.math.{BigDecimal => BigDecimalJ}\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.nio.file.{Files, Paths}\nimport java.util.{Optional, TimeZone, UUID}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.golden.GoldenTableUtils\nimport io.delta.kernel.{Scan, Snapshot, Table, TransactionCommitResult}\nimport io.delta.kernel.data._\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.defaults.internal.data.vector.{DefaultGenericVector, DefaultStructVector}\nimport io.delta.kernel.defaults.test.{AbstractTableManagerAdapter, LegacyTableManagerAdapter, TableManagerAdapter}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.{Column, Predicate}\nimport io.delta.kernel.hook.PostCommitHook.PostCommitHookType\nimport io.delta.kernel.internal.{InternalScanFileUtils, SnapshotImpl}\nimport io.delta.kernel.internal.actions.DomainMetadata\nimport io.delta.kernel.internal.checksum.{ChecksumReader, ChecksumWriter, CRCInfo}\nimport io.delta.kernel.internal.clustering.ClusteringMetadataDomain\nimport io.delta.kernel.internal.data.ScanStateRow\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.internal.stats.FileSizeHistogram\nimport io.delta.kernel.internal.util.{FileNames, Utils}\nimport io.delta.kernel.internal.util.FileNames.checksumFile\nimport io.delta.kernel.internal.util.Utils.singletonCloseableIterator\nimport io.delta.kernel.test.TestFixtures\nimport io.delta.kernel.types._\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\n\nimport org.apache.spark.sql.delta.{sources, OptimisticTransaction}\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.Action\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.shaded.org.apache.commons.io.FileUtils\nimport org.apache.spark.sql.{types => sparktypes, SparkSession}\nimport org.apache.spark.sql.catalyst.plans.SQLHelper\nimport org.scalatest.Assertions\n\ntrait TestUtils extends AbstractTestUtils {\n  override def getTableManagerAdapter: AbstractTableManagerAdapter = new LegacyTableManagerAdapter()\n}\n\n/**\n * DO NOT MODIFY this trait -- this is just syntactic sugar to clearly indicate we are extending the\n * \"default\" TestUtils which happens to use the legacy Kernel APIs\n */\ntrait TestUtilsWithLegacyKernelAPIs extends TestUtils\n\ntrait TestUtilsWithTableManagerAPIs extends AbstractTestUtils {\n  override def getTableManagerAdapter: AbstractTableManagerAdapter = new TableManagerAdapter()\n}\n\nobject TestUtilsWithTableManagerAPIs extends TestUtilsWithTableManagerAPIs\n\ntrait AbstractTestUtils\n    extends Assertions\n    with SQLHelper\n    with TestCommitterUtils\n    with TestFixtures {\n\n  def getTableManagerAdapter: AbstractTableManagerAdapter\n\n  lazy val configuration = new Configuration()\n  lazy val defaultEngine = DefaultEngine.create(configuration)\n\n  // Used in child suites to override defaultEngine\n  lazy val defaultEngineBatchSize2 = DefaultEngine.create(new Configuration() {\n    {\n      // Set the batch sizes to small so that we get to test the multiple batch scenarios.\n      set(\"delta.kernel.default.parquet.reader.batch-size\", \"2\");\n      set(\"delta.kernel.default.json.reader.batch-size\", \"2\");\n    }\n  })\n\n  lazy val spark = SparkSession\n    .builder()\n    .appName(\"Spark Test Writer for Delta Kernel\")\n    .config(\"spark.master\", \"local\")\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n    // Set this conf to empty string so that the golden tables generated\n    // using with the test-prefix (i.e. there is no DELTA_TESTING set) can still work\n    .config(DeltaSQLConf.TEST_DV_NAME_PREFIX.key, \"\")\n    .getOrCreate()\n\n  implicit class CloseableIteratorOps[T](private val iter: CloseableIterator[T]) {\n    def forEach(f: T => Unit): Unit = {\n      try {\n        while (iter.hasNext) {\n          f(iter.next())\n        }\n      } finally {\n        iter.close()\n      }\n    }\n\n    def toSeq: Seq[T] = {\n      try {\n        val result = new ArrayBuffer[T]\n        while (iter.hasNext) {\n          result.append(iter.next())\n        }\n        result.toSeq\n      } finally {\n        iter.close()\n      }\n    }\n  }\n\n  implicit class StructTypeOps(schema: StructType) {\n    def withoutField(name: String): StructType = {\n      val newFields = schema.fields().asScala\n        .filter(_.getName != name).asJava\n      new StructType(newFields)\n    }\n\n    def toSpark: sparktypes.StructType = {\n      toSparkSchema(schema)\n    }\n  }\n\n  implicit class ColumnarBatchOps(batch: ColumnarBatch) {\n    def toFiltered: FilteredColumnarBatch = {\n      new FilteredColumnarBatch(batch, Optional.empty())\n    }\n\n    def toFiltered(predicate: Option[Predicate]): FilteredColumnarBatch = {\n      if (predicate.isEmpty) {\n        new FilteredColumnarBatch(batch, Optional.empty())\n      } else {\n        val predicateEvaluator = defaultEngine.getExpressionHandler\n          .getPredicateEvaluator(batch.getSchema, predicate.get)\n        val selVector = predicateEvaluator.eval(batch, Optional.empty())\n        new FilteredColumnarBatch(batch, Optional.of(selVector))\n      }\n    }\n  }\n\n  implicit class FilteredColumnarBatchOps(batch: FilteredColumnarBatch) {\n    def toTestRows: Seq[TestRow] = {\n      batch.getRows.toSeq.map(TestRow(_))\n    }\n  }\n\n  implicit class ColumnOps(column: Column) {\n    def toPath: String = column.getNames.mkString(\".\")\n  }\n\n  implicit class JavaOptionalOps[T](optional: Optional[T]) {\n    def toScala: Option[T] = if (optional.isPresent) Some(optional.get()) else None\n  }\n\n  /**\n   * Provides test-only apis to internal Delta Spark APIs.\n   */\n  implicit class OptimisticTxnTestHelper(txn: OptimisticTransaction) {\n\n    /**\n     * Test only method to commit arbitrary actions to delta table.\n     */\n    def commitManuallyWithValidation(actions: Action*): Unit = {\n      txn.commit(actions.toSeq, ManualUpdate)\n    }\n\n    /**\n     * Test only method to unsafe commit - writes actions directly to transaction log.\n     * Note: This bypasses Delta Spark transaction logic.\n     *\n     * @param tablePath The path to the Delta table\n     * @param version The commit version number\n     * @param actions Sequence of Action objects to write\n     */\n    def commitUnsafe(tablePath: String, version: Long, actions: Action*): Unit = {\n      val logPath = new org.apache.hadoop.fs.Path(tablePath, \"_delta_log\")\n      val commitFile = org.apache.spark.sql.delta.util.FileNames.unsafeDeltaFile(logPath, version)\n      val commitContent = actions.map(_.json + \"\\n\").mkString.getBytes(UTF_8)\n      Files.write(Paths.get(commitFile.toString), commitContent)\n      // Generate crc file for this commit version.\n      Table.forPath(defaultEngine, tablePath).checksum(defaultEngine, version)\n    }\n  }\n\n  implicit object ResourceLoader {\n    lazy val classLoader: ClassLoader = ResourceLoader.getClass.getClassLoader\n  }\n\n  def withTempDirAndEngine(\n      f: (String, Engine) => Unit,\n      hadoopConf: Map[String, String] = Map.empty): Unit = {\n    val engine = DefaultEngine.create(new Configuration() {\n      {\n        for ((key, value) <- hadoopConf) {\n          set(key, value)\n        }\n        // Set the batch sizes to small so that we get to test the multiple batch/file scenarios.\n        set(\"delta.kernel.default.parquet.reader.batch-size\", \"20\");\n        set(\"delta.kernel.default.json.reader.batch-size\", \"20\");\n        set(\"delta.kernel.default.parquet.writer.targetMaxFileSize\", \"20\");\n      }\n    })\n    withTempDir { dir => f(dir.getAbsolutePath, engine) }\n  }\n\n  def withGoldenTable(tableName: String)(testFunc: String => Unit): Unit = {\n    val tablePath = GoldenTableUtils.goldenTablePath(tableName)\n    testFunc(tablePath)\n  }\n\n  def latestSnapshot(path: String, engine: Engine = defaultEngine): SnapshotImpl = {\n    getTableManagerAdapter.getSnapshotAtLatest(engine, path)\n  }\n\n  def tableSchema(path: String): StructType = {\n    latestSnapshot(path).getSchema()\n  }\n\n  def hasTableProperty(tablePath: String, propertyKey: String, expValue: String): Boolean = {\n    val schema = tableSchema(tablePath)\n    schema.fields().asScala.exists { field =>\n      field.getMetadata.getString(propertyKey) == expValue\n    }\n  }\n\n  /** Get the list of all leaf-level primitive column references in the given `structType` */\n  def leafLevelPrimitiveColumns(basePath: Seq[String], structType: StructType): Seq[Column] = {\n    structType.fields.asScala.flatMap {\n      case field if field.getDataType.isInstanceOf[StructType] =>\n        leafLevelPrimitiveColumns(\n          basePath :+ field.getName,\n          field.getDataType.asInstanceOf[StructType])\n      case field\n          if !field.getDataType.isInstanceOf[ArrayType] &&\n            !field.getDataType.isInstanceOf[MapType] =>\n        // for all primitive types\n        Seq(new Column((basePath :+ field.getName).asJava.toArray(new Array[String](0))));\n      case _ => Seq.empty\n    }.toSeq\n  }\n\n  def collectScanFileRows(scan: Scan, engine: Engine = defaultEngine): Seq[Row] = {\n    scan.getScanFiles(engine).toSeq\n      .flatMap(_.getRows.toSeq)\n  }\n\n  def readSnapshot(\n      snapshot: Snapshot,\n      readSchema: StructType = null,\n      filter: Predicate = null,\n      expectedRemainingFilter: Predicate = null,\n      engine: Engine = defaultEngine): Seq[Row] = {\n\n    val result = ArrayBuffer[Row]()\n\n    var scanBuilder = snapshot.getScanBuilder()\n\n    if (readSchema != null) {\n      scanBuilder = scanBuilder.withReadSchema(readSchema)\n    }\n\n    if (filter != null) {\n      scanBuilder = scanBuilder.withFilter(filter)\n    }\n\n    val scan = scanBuilder.build()\n\n    if (filter != null) {\n      val actRemainingPredicate = scan.getRemainingFilter()\n      assert(\n        actRemainingPredicate.toString === Optional.ofNullable(expectedRemainingFilter).toString)\n    }\n\n    val scanState = scan.getScanState(engine);\n    val fileIter = scan.getScanFiles(engine)\n\n    val physicalDataReadSchema = ScanStateRow.getPhysicalDataReadSchema(scanState)\n    fileIter.forEach { fileColumnarBatch =>\n      fileColumnarBatch.getRows().forEach { scanFileRow =>\n        val fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow)\n        val physicalDataIter = engine.getParquetHandler().readParquetFiles(\n          singletonCloseableIterator(fileStatus),\n          physicalDataReadSchema,\n          Optional.empty()).map(_.getData)\n        var dataBatches: CloseableIterator[FilteredColumnarBatch] = null\n        try {\n          dataBatches = Scan.transformPhysicalData(\n            engine,\n            scanState,\n            scanFileRow,\n            physicalDataIter)\n\n          dataBatches.forEach { batch =>\n            val selectionVector = batch.getSelectionVector()\n            val data = batch.getData()\n\n            var i = 0\n            val rowIter = data.getRows()\n            try {\n              while (rowIter.hasNext) {\n                val row = rowIter.next()\n                if (!selectionVector.isPresent || selectionVector.get.getBoolean(i)) {\n                  // row is valid\n                  result.append(row)\n                }\n                i += 1\n              }\n            } finally {\n              rowIter.close()\n            }\n          }\n        } finally {\n          dataBatches.close()\n        }\n      }\n    }\n    result.toSeq\n  }\n\n  def readTableUsingKernel(\n      engine: Engine,\n      tablePath: String,\n      readSchema: StructType): Seq[FilteredColumnarBatch] = {\n    val scan = latestSnapshot(tablePath, engine)\n      .getScanBuilder()\n      .withReadSchema(readSchema)\n      .build()\n    val scanState = scan.getScanState(engine)\n\n    val physicalDataReadSchema = ScanStateRow.getPhysicalDataReadSchema(scanState)\n    var result: Seq[FilteredColumnarBatch] = Nil\n    scan.getScanFiles(engine).forEach { fileColumnarBatch =>\n      fileColumnarBatch.getRows.forEach { scanFile =>\n        val fileStatus = InternalScanFileUtils.getAddFileStatus(scanFile)\n        val physicalDataIter = engine.getParquetHandler.readParquetFiles(\n          singletonCloseableIterator(fileStatus),\n          physicalDataReadSchema,\n          Optional.empty())\n        var dataBatches: CloseableIterator[FilteredColumnarBatch] = null\n        try {\n          dataBatches =\n            Scan.transformPhysicalData(engine, scanState, scanFile, physicalDataIter.map(_.getData))\n          dataBatches.forEach { dataBatch => result = result :+ dataBatch }\n        } finally {\n          Utils.closeCloseables(dataBatches)\n        }\n      }\n    }\n    result\n  }\n\n  /**\n   * Execute {@code f} with {@code TimeZone.getDefault()} set to the time zone provided.\n   *\n   * @param zoneId the ID for a TimeZone, either an abbreviation such as \"PST\", a full name such as\n   *               \"America/Los_Angeles\", or a custom ID such as \"GMT-8:00\".\n   */\n  def withTimeZone(zoneId: String)(f: => Unit): Unit = {\n    val currentDefault = TimeZone.getDefault\n    try {\n      TimeZone.setDefault(TimeZone.getTimeZone(zoneId))\n      f\n    } finally {\n      TimeZone.setDefault(currentDefault)\n    }\n  }\n\n  /**\n   * Compares the rows in the tables latest snapshot with the expected answer and fails if they\n   * do not match. The comparison is order independent. If expectedSchema is provided, checks\n   * that the latest snapshot's schema is equivalent.\n   *\n   * @param path fully qualified path of the table to check\n   * @param expectedAnswer expected rows\n   * @param readCols subset of columns to read; if null then all columns will be read\n   * @param metadataCols set of metadata columns to read; if null then no metadata columns will\n   *                     be read\n   * @param engine engine to use to read the table\n   * @param expectedSchema expected schema to check for (ignoring metadata columns);\n   *                       if null then no check is performed\n   * @param filter Filter to select a subset of rows form the table\n   * @param expectedRemainingFilter Remaining predicate out of the `filter` that is not enforced\n   *                                by Kernel.\n   * @param expectedVersion expected version of the latest snapshot for the table\n   */\n  // scalastyle:off argcount\n  def checkTable(\n      path: String,\n      expectedAnswer: Seq[TestRow],\n      readCols: Seq[String] = null,\n      metadataCols: Seq[StructField] = null,\n      engine: Engine = defaultEngine,\n      expectedSchema: StructType = null,\n      filter: Predicate = null,\n      version: Option[Long] = None,\n      timestamp: Option[Long] = None,\n      expectedRemainingFilter: Predicate = null,\n      expectedVersion: Option[Long] = None): Unit = {\n    assert(version.isEmpty || timestamp.isEmpty, \"Cannot provide both a version and timestamp\")\n\n    val snapshot = if (version.isDefined) {\n      getTableManagerAdapter.getSnapshotAtVersion(engine, path, version.get)\n    } else if (timestamp.isDefined) {\n      getTableManagerAdapter.getSnapshotAtTimestamp(engine, path, timestamp.get)\n    } else {\n      getTableManagerAdapter.getSnapshotAtLatest(engine, path)\n    }\n\n    val readSchema =\n      if (readCols == null && metadataCols == null) null\n      else {\n        val schema = snapshot.getSchema()\n        val readFields = Option(readCols).map(_.map(schema.get)).getOrElse(schema.fields().asScala)\n        val metadataFields = Option(metadataCols).getOrElse(Seq())\n        new StructType((readFields ++ metadataFields).asJava)\n      }\n\n    if (expectedSchema != null) {\n      // We ignore metadata columns in this check because metadata columns are not part of the\n      // public table schema.\n      assert(\n        expectedSchema == snapshot.getSchema(),\n        s\"\"\"\n           |Expected schema does not match actual schema:\n           |Expected schema: $expectedSchema\n           |Actual schema: ${snapshot.getSchema()}\n           |\"\"\".stripMargin)\n    }\n\n    val actualVersion = snapshot.getVersion()\n\n    expectedVersion.foreach { version =>\n      assert(\n        version == actualVersion,\n        s\"Expected version $version does not match actual version $actualVersion}\")\n    }\n\n    val result =\n      readSnapshot(\n        snapshot,\n        readSchema,\n        filter,\n        expectedRemainingFilter,\n        engine)\n    checkAnswer(result, expectedAnswer)\n  }\n  // scalastyle:on argcount\n\n  def checkAnswer(result: => Seq[Row], expectedAnswer: Seq[TestRow]): Unit = {\n    checkAnswer(result.map(TestRow(_)), expectedAnswer)\n  }\n\n  def checkAnswer(result: Seq[TestRow], expectedAnswer: Seq[TestRow]): Unit = {\n    if (!compare(prepareAnswer(result), prepareAnswer(expectedAnswer))) {\n      fail(genErrorMessage(expectedAnswer, result))\n    }\n  }\n\n  private def prepareAnswer(answer: Seq[TestRow]): Seq[TestRow] = {\n    // Converts data to types that we can do equality comparison using Scala collections.\n    // For BigDecimal type, the Scala type has a better definition of equality test (similar to\n    // Java's java.math.BigDecimal.compareTo).\n    // For binary arrays, we convert it to Seq to avoid of calling java.util.Arrays.equals for\n    // equality test.\n    val converted = answer.map(prepareRow)\n    converted.sortBy(_.toString())\n  }\n\n  // We need to call prepareRow recursively to handle schemas with struct types.\n  private def prepareRow(row: TestRow): TestRow = {\n    TestRow.fromSeq(row.toSeq.map {\n      case null => null\n      case bd: java.math.BigDecimal => BigDecimal(bd)\n      // Equality of WrappedArray differs for AnyVal and AnyRef in Scala 2.12.2+\n      case seq: Seq[_] => seq.map {\n          case b: java.lang.Byte => b.byteValue\n          case s: java.lang.Short => s.shortValue\n          case i: java.lang.Integer => i.intValue\n          case l: java.lang.Long => l.longValue\n          case f: java.lang.Float => f.floatValue\n          case d: java.lang.Double => d.doubleValue\n          case x => x\n        }\n      // Convert array to Seq for easy equality check.\n      case b: Array[_] => b.toSeq\n      case r: TestRow => prepareRow(r)\n      case o => o\n    })\n  }\n\n  private def compare(obj1: Any, obj2: Any): Boolean = (obj1, obj2) match {\n    case (null, null) => true\n    case (null, _) => false\n    case (_, null) => false\n    case (a: Array[_], b: Array[_]) =>\n      a.length == b.length && a.zip(b).forall { case (l, r) => compare(l, r) }\n    case (a: Map[_, _], b: Map[_, _]) =>\n      a.size == b.size && a.keys.forall { aKey =>\n        b.keys.find(bKey => compare(aKey, bKey)).exists(bKey => compare(a(aKey), b(bKey)))\n      }\n    case (a: Iterable[_], b: Iterable[_]) =>\n      a.size == b.size && a.zip(b).forall { case (l, r) => compare(l, r) }\n    case (a: Product, b: Product) =>\n      compare(a.productIterator.toSeq, b.productIterator.toSeq)\n    case (a: TestRow, b: TestRow) =>\n      compare(a.toSeq, b.toSeq)\n    // 0.0 == -0.0, turn float/double to bits before comparison, to distinguish 0.0 and -0.0.\n    case (a: Double, b: Double) =>\n      java.lang.Double.doubleToRawLongBits(a) == java.lang.Double.doubleToRawLongBits(b)\n    case (a: Float, b: Float) =>\n      java.lang.Float.floatToRawIntBits(a) == java.lang.Float.floatToRawIntBits(b)\n    case (a, b) =>\n      if (!a.equals(b)) {\n        val sds = 200;\n      }\n      a.equals(b)\n    // In scala == does not call equals for boxed numeric classes?\n  }\n\n  private def genErrorMessage(expectedAnswer: Seq[TestRow], result: Seq[TestRow]): String = {\n    // TODO: improve to include schema or Java type information to help debugging\n    s\"\"\"\n       |== Results ==\n       |\n       |== Expected Answer - ${expectedAnswer.size} ==\n       |${prepareAnswer(expectedAnswer).map(_.toString()).mkString(\"(\", \",\", \")\")}\n       |\n       |== Result - ${result.size} ==\n       |${prepareAnswer(result).map(_.toString()).mkString(\"(\", \",\", \")\")}\n       |\n       |\"\"\".stripMargin\n  }\n\n  /**\n   * Creates a temporary directory, which is then passed to `f` and will be deleted after `f`\n   * returns.\n   */\n  protected def withTempDir(f: File => Unit): Unit = {\n    val tempDir = Files.createTempDirectory(UUID.randomUUID().toString).toFile\n    try f(tempDir)\n    finally {\n      FileUtils.deleteDirectory(tempDir)\n    }\n  }\n\n  /**\n   * Creates a temporary directory with Delta log structure (_delta_log, _staged_commits,\n   * _sidecars), passes (tablePath, logPath) to `f`, and deletes the directory after `f` returns.\n   */\n  protected def withTempDirAndAllDeltaSubDirs(f: (String, String) => Unit): Unit = {\n    val tempDir = Files.createTempDirectory(UUID.randomUUID().toString).toFile\n    val deltaLogDir = new File(tempDir, \"_delta_log\")\n    deltaLogDir.mkdirs()\n    new File(deltaLogDir, FileNames.STAGED_COMMIT_DIRECTORY).mkdirs()\n    new File(deltaLogDir, FileNames.SIDECAR_DIRECTORY).mkdirs()\n    try f(tempDir.getAbsolutePath, deltaLogDir.getAbsolutePath)\n    finally {\n      FileUtils.deleteDirectory(tempDir)\n    }\n  }\n\n  /**\n   * Create a unique table name and drops it after completing `f`\n   */\n  protected def withTempTable[T](f: String => T): T = {\n    val tableName = s\"temp_table_${UUID.randomUUID().toString.replace(\"-\", \"_\")}\"\n    try {\n      f(tableName)\n    } finally {\n      spark.sql(s\"DROP TABLE IF EXISTS $tableName\")\n    }\n  }\n\n  def withSparkTimeZone(timeZone: String)(fn: => Unit): Unit = {\n    val prevTimeZone = spark.conf.get(\"spark.sql.session.timeZone\")\n    try {\n      spark.conf.set(\"spark.sql.session.timeZone\", timeZone)\n      fn\n    } finally {\n      spark.conf.set(\"spark.sql.session.timeZone\", prevTimeZone)\n    }\n  }\n\n  /**\n   * Builds a MapType ColumnVector from a sequence of maps.\n   */\n  def buildMapVector(mapValues: Seq[Map[AnyRef, AnyRef]], dataType: MapType): ColumnVector = {\n    val keyType = dataType.getKeyType\n    val valueType = dataType.getValueType\n\n    def getMapValue(map: Map[AnyRef, AnyRef]): MapValue = {\n      if (map == null) {\n        null\n      } else {\n        val (keys, values) = map.unzip\n        new MapValue() {\n          override def getSize: Int = map.size\n\n          override def getKeys = DefaultGenericVector.fromArray(keyType, keys.toArray)\n\n          override def getValues = DefaultGenericVector.fromArray(valueType, values.toArray)\n        }\n      }\n    }\n\n    DefaultGenericVector.fromArray(dataType, mapValues.map(getMapValue).toArray)\n  }\n\n  /**\n   * Builds an ArrayType ColumnVector from a sequence of per-row element sequences.\n   */\n  def buildArrayVector(\n      valuesPerRow: Seq[Seq[AnyRef]],\n      elementType: DataType,\n      containsNull: Boolean): ColumnVector = {\n    val arrayType = new ArrayType(elementType, containsNull)\n    val arrayValues: Array[ArrayValue] = valuesPerRow.map { elems =>\n      if (elems == null) null\n      else new ArrayValue {\n        override def getSize: Int = elems.size\n        override def getElements: ColumnVector =\n          DefaultGenericVector.fromArray(elementType, elems.toArray)\n      }\n    }.toArray\n    DefaultGenericVector.fromArray(arrayType, arrayValues.asInstanceOf[Array[AnyRef]])\n  }\n\n  /**\n   * Utility method to generate a [[dataType]] column vector of given size.\n   * The nullability of rows is determined by the [[testIsNullValue(dataType, rowId)]].\n   * The row values are determined by [[testColumnValue(dataType, rowId)]].\n   */\n  def testColumnVector(size: Int, dataType: DataType): ColumnVector = {\n    dataType match {\n      // Build a DefaultStructVector and recursively\n      // build child vectors for each field.\n      case structType: StructType =>\n        val memberVectors: Array[ColumnVector] =\n          structType.fields().asScala.map { field =>\n            testColumnVector(size, field.getDataType)\n          }.toArray\n\n        new DefaultStructVector(\n          size,\n          structType,\n          Optional.empty(),\n          memberVectors)\n\n      case _ =>\n        new ColumnVector {\n          override def getDataType: DataType = dataType\n\n          override def getSize: Int = size\n\n          override def close(): Unit = {}\n\n          override def isNullAt(rowId: Int): Boolean = testIsNullValue(dataType, rowId)\n\n          override def getBoolean(rowId: Int): Boolean =\n            testColumnValue(dataType, rowId).asInstanceOf[Boolean]\n\n          override def getByte(rowId: Int): Byte =\n            testColumnValue(dataType, rowId).asInstanceOf[Byte]\n\n          override def getShort(rowId: Int): Short =\n            testColumnValue(dataType, rowId).asInstanceOf[Short]\n\n          override def getInt(rowId: Int): Int = testColumnValue(dataType, rowId).asInstanceOf[Int]\n\n          override def getLong(rowId: Int): Long =\n            testColumnValue(dataType, rowId).asInstanceOf[Long]\n\n          override def getFloat(rowId: Int): Float =\n            testColumnValue(dataType, rowId).asInstanceOf[Float]\n\n          override def getDouble(rowId: Int): Double =\n            testColumnValue(dataType, rowId).asInstanceOf[Double]\n\n          override def getBinary(rowId: Int): Array[Byte] =\n            testColumnValue(dataType, rowId).asInstanceOf[Array[Byte]]\n\n          override def getString(rowId: Int): String =\n            testColumnValue(dataType, rowId).asInstanceOf[String]\n\n          override def getDecimal(rowId: Int): BigDecimalJ =\n            testColumnValue(dataType, rowId).asInstanceOf[BigDecimalJ]\n        }\n    }\n  }\n\n  /** Utility method to generate a consistent `isNull` value for given column type and row id */\n  def testIsNullValue(dataType: DataType, rowId: Int): Boolean = {\n    dataType match {\n      case BooleanType.BOOLEAN => rowId % 4 == 0\n      case ByteType.BYTE => rowId % 8 == 0\n      case ShortType.SHORT => rowId % 12 == 0\n      case IntegerType.INTEGER => rowId % 20 == 0\n      case LongType.LONG => rowId % 25 == 0\n      case FloatType.FLOAT => rowId % 5 == 0\n      case DoubleType.DOUBLE => rowId % 10 == 0\n      case _: StringType => rowId % 2 == 0\n      case BinaryType.BINARY => rowId % 3 == 0\n      case DateType.DATE => rowId % 5 == 0\n      case TimestampType.TIMESTAMP => rowId % 3 == 0\n      case TimestampNTZType.TIMESTAMP_NTZ => rowId % 2 == 0\n      case _ =>\n        if (dataType.isInstanceOf[DecimalType]) rowId % 6 == 0\n        else throw new UnsupportedOperationException(s\"$dataType is not supported\")\n    }\n  }\n\n  /** Utility method to generate a consistent column value for given column type and row id */\n  def testColumnValue(dataType: DataType, rowId: Int): Any = {\n    dataType match {\n      case BooleanType.BOOLEAN => rowId % 7 == 0\n      case ByteType.BYTE => (rowId * 7 / 17).toByte\n      case ShortType.SHORT => (rowId * 9 / 87).toShort\n      case IntegerType.INTEGER => rowId * 2876 / 176\n      case LongType.LONG => rowId * 287623L / 91\n      case FloatType.FLOAT => rowId * 7651.2323f / 91\n      case DoubleType.DOUBLE => rowId * 23423.23d / 17\n      case _: StringType => (rowId % 19).toString\n      case BinaryType.BINARY => Array[Byte]((rowId % 21).toByte, (rowId % 7 - 1).toByte)\n      case DateType.DATE => (rowId * 28234) % 2876\n      case TimestampType.TIMESTAMP => (rowId * 2342342L) % 23\n      case TimestampNTZType.TIMESTAMP_NTZ => (rowId * 523423L) % 29\n      case _ =>\n        if (dataType.isInstanceOf[DecimalType]) new BigDecimalJ(rowId * 22342.23)\n        else throw new UnsupportedOperationException(s\"$dataType is not supported\")\n    }\n  }\n\n  /**\n   * Utility method to replicate the behavior of individual values when they are converted from\n   * Row to TestRow.\n   */\n  def testColumnNullableValue(dataType: DataType, rowId: Int): Any = {\n    if (testIsNullValue(dataType, rowId)) {\n      null\n    } else {\n      testColumnValue(dataType, rowId)\n    }\n  }\n\n  def testSingleValueVector(dataType: DataType, size: Int, value: Any): ColumnVector = {\n    new ColumnVector {\n      override def getDataType: DataType = dataType\n\n      override def getSize: Int = size\n\n      override def close(): Unit = {}\n\n      override def isNullAt(rowId: Int): Boolean = value == null\n\n      override def getBoolean(rowId: Int): Boolean =\n        value.asInstanceOf[Boolean]\n\n      override def getByte(rowId: Int): Byte = value.asInstanceOf[Byte]\n\n      override def getShort(rowId: Int): Short =\n        value.asInstanceOf[Short]\n\n      override def getInt(rowId: Int): Int = value.asInstanceOf[Int]\n\n      override def getLong(rowId: Int): Long = value.asInstanceOf[Long]\n\n      override def getFloat(rowId: Int): Float =\n        value.asInstanceOf[Float]\n\n      override def getDouble(rowId: Int): Double =\n        value.asInstanceOf[Double]\n\n      override def getBinary(rowId: Int): Array[Byte] =\n        value.asInstanceOf[Array[Byte]]\n\n      override def getString(rowId: Int): String =\n        value.asInstanceOf[String]\n\n      override def getDecimal(rowId: Int): BigDecimalJ =\n        value.asInstanceOf[BigDecimalJ]\n    }\n  }\n\n  /**\n   * Converts a Delta Schema to a Spark Schema.\n   */\n  private def toSparkSchema(deltaSchema: StructType): sparktypes.StructType = {\n    toSparkType(deltaSchema).asInstanceOf[sparktypes.StructType]\n  }\n\n  /**\n   * Converts a Delta DataType to a Spark DataType.\n   */\n  private def toSparkType(deltaType: DataType): sparktypes.DataType = {\n    deltaType match {\n      case BooleanType.BOOLEAN => sparktypes.DataTypes.BooleanType\n      case ByteType.BYTE => sparktypes.DataTypes.ByteType\n      case ShortType.SHORT => sparktypes.DataTypes.ShortType\n      case IntegerType.INTEGER => sparktypes.DataTypes.IntegerType\n      case LongType.LONG => sparktypes.DataTypes.LongType\n      case FloatType.FLOAT => sparktypes.DataTypes.FloatType\n      case DoubleType.DOUBLE => sparktypes.DataTypes.DoubleType\n      case _: StringType => sparktypes.DataTypes.StringType\n      case BinaryType.BINARY => sparktypes.DataTypes.BinaryType\n      case DateType.DATE => sparktypes.DataTypes.DateType\n      case TimestampType.TIMESTAMP => sparktypes.DataTypes.TimestampType\n      case TimestampNTZType.TIMESTAMP_NTZ => sparktypes.DataTypes.TimestampNTZType\n      case dt: DecimalType =>\n        sparktypes.DecimalType(dt.getPrecision, dt.getScale)\n      case at: ArrayType =>\n        sparktypes.ArrayType(toSparkType(at.getElementType), at.containsNull())\n      case mt: MapType =>\n        sparktypes.MapType(\n          toSparkType(mt.getKeyType),\n          toSparkType(mt.getValueType),\n          mt.isValueContainsNull)\n      case st: StructType =>\n        sparktypes.StructType(st.fields().asScala.map { field =>\n          sparktypes.StructField(\n            field.getName,\n            toSparkType(field.getDataType),\n            field.isNullable)\n        }.toSeq)\n    }\n  }\n\n  /**\n   * Returns a URI encoded path of the resource.\n   */\n  def getTestResourceFilePath(resourcePath: String): String = {\n    val resource = ResourceLoader.classLoader.getResource(resourcePath)\n    if (resource == null) {\n      throw new FileNotFoundException(\"resource not found\")\n    }\n    resource.getFile\n  }\n\n  def checkpointFileExistsForTable(tablePath: String, versions: Int): Boolean =\n    Files.exists(\n      new File(FileNames.checkpointFileSingular(\n        new Path(s\"$tablePath/_delta_log\"),\n        versions).toString).toPath)\n\n  def deleteChecksumFileForTable(tablePath: String, versions: Seq[Int]): Unit =\n    versions.foreach(v =>\n      Files.deleteIfExists(\n        new File(FileNames.checksumFile(new Path(s\"$tablePath/_delta_log\"), v).toString).toPath))\n\n  def deleteChecksumFileForTableUsingHadoopFs(tablePath: String, versions: Seq[Int]): Unit =\n    versions.foreach(v =>\n      defaultEngine.getFileSystemClient.delete(FileNames.checksumFile(\n        new Path(s\"$tablePath/_delta_log\"),\n        v).toString))\n\n  def rewriteChecksumFileToExcludeDomainMetadata(\n      engine: Engine,\n      tablePath: String,\n      version: Long): Unit = {\n    val logPath = new Path(s\"$tablePath/_delta_log\");\n    val crcInfo = ChecksumReader.tryReadChecksumFile(\n      engine,\n      FileStatus.of(checksumFile(\n        logPath,\n        version).toString)).get()\n    // Delete it in hdfs.\n    engine.getFileSystemClient.delete(FileNames.checksumFile(\n      new Path(s\"$tablePath/_delta_log\"),\n      version).toString)\n    val crcWriter = new ChecksumWriter(logPath)\n    crcWriter.writeCheckSum(\n      engine,\n      new CRCInfo(\n        crcInfo.getVersion,\n        crcInfo.getMetadata,\n        crcInfo.getProtocol,\n        crcInfo.getTableSizeBytes,\n        crcInfo.getNumFiles,\n        crcInfo.getTxnId,\n        /* domainMetadata */ Optional.empty(),\n        crcInfo.getFileSizeHistogram))\n  }\n\n  def executeCrcSimple(result: TransactionCommitResult, engine: Engine): TransactionCommitResult = {\n    val crcSimpleHook = result\n      .getPostCommitHooks\n      .asScala\n      .find(hook => hook.getType == PostCommitHookType.CHECKSUM_SIMPLE)\n      .getOrElse(throw new IllegalStateException(\"CRC simple hook not found\"))\n\n    crcSimpleHook.threadSafeInvoke(engine)\n\n    result\n  }\n\n  def verifyClusteringDomainMetadata(\n      snapshot: SnapshotImpl,\n      expectedDomainMetadata: DomainMetadata): Unit = {\n    assert(snapshot.getActiveDomainMetadataMap.get(ClusteringMetadataDomain.DOMAIN_NAME)\n      == expectedDomainMetadata)\n  }\n\n  /**\n   * Verify checksum data matches the expected values in the snapshot.\n   * @param snapshot Snapshot to verify the checksum against\n   */\n  protected def verifyChecksumForSnapshot(\n      snapshot: Snapshot,\n      expectEmptyTable: Boolean = false): Unit = {\n    val logPath = snapshot.asInstanceOf[SnapshotImpl].getLogPath\n    val crcInfoOpt = ChecksumReader.tryReadChecksumFile(\n      defaultEngine,\n      FileStatus.of(checksumFile(\n        logPath,\n        snapshot.getVersion).toString))\n    assert(\n      crcInfoOpt.isPresent,\n      s\"CRC information should be present for version ${snapshot.getVersion}\")\n    crcInfoOpt.toScala.foreach { crcInfo =>\n      // TODO: check file size.\n      assert(crcInfo.getProtocol === snapshot.asInstanceOf[SnapshotImpl].getProtocol)\n      assert(crcInfo.getMetadata.getSchema === snapshot.getSchema)\n      assert(\n        crcInfo.getNumFiles === collectScanFileRows(snapshot.getScanBuilder.build()).size,\n        \"Number of files in checksum should match snapshot\")\n      if (expectEmptyTable) {\n        assert(crcInfo.getTableSizeBytes == 0)\n        crcInfo.getFileSizeHistogram.toScala.foreach { fileSizeHistogram =>\n          assert(fileSizeHistogram == FileSizeHistogram.createDefaultHistogram)\n        }\n      }\n      assert(\n        crcInfo.getDomainMetadata === Optional.of(\n          snapshot.asInstanceOf[SnapshotImpl].getActiveDomainMetadataMap.values().asScala\n            .toSet\n            .asJava),\n        \"Domain metadata in checksum should match snapshot\")\n    }\n  }\n\n  /**\n   * Ensure checksum is readable by CRC reader, matches snapshot data, and can be regenerated.\n   * This test verifies:\n   * 1. The initial checksum exists and is correct\n   * 2. After deleting the checksum file, it can be regenerated with the same content\n   */\n  def verifyChecksum(tablePath: String, expectEmptyTable: Boolean = false): Unit = {\n    val currentSnapshot = latestSnapshot(tablePath, defaultEngine)\n    val checksumVersion = currentSnapshot.getVersion\n\n    // Step 1: Verify initial checksum\n    verifyChecksumForSnapshot(currentSnapshot)\n\n    // Step 2: Delete and regenerate the checksum\n    defaultEngine.getFileSystemClient.delete(buildCrcPath(tablePath, checksumVersion).toString)\n    Table.forPath(defaultEngine, tablePath).checksum(defaultEngine, checksumVersion)\n\n    // Step 3: Verify regenerated checksum\n    verifyChecksumForSnapshot(currentSnapshot)\n  }\n\n  protected def buildCrcPath(basePath: String, version: Long): java.nio.file.Path = {\n    new File(FileNames.checksumFile(new Path(f\"$basePath/_delta_log\"), version).toString).toPath\n  }\n\n  protected def optionToJava[T](option: Option[T]): Optional[T] = {\n    option match {\n      case Some(value) => Optional.of(value)\n      case None => Optional.empty()\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/TransactionBuilderSupport.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.utils\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.{Operation, Table, TableManager, Transaction}\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.{CreateTableTransactionBuilderImpl, UpdateTableTransactionBuilderImpl}\nimport io.delta.kernel.internal.{SnapshotImpl, TableImpl}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.Clock\nimport io.delta.kernel.transaction.DataLayoutSpec\nimport io.delta.kernel.types.StructType\n\n/**\n * Test helper contract for constructing and configuring Delta Kernel transactions.\n */\ntrait TransactionBuilderSupport {\n\n  // scalastyle:off argcount\n  def getCreateTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      tableProperties: Map[String, String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1,\n      clusteringColsOpt: Option[List[Column]] = None): Transaction\n\n  def getUpdateTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = null,\n      tableProperties: Map[String, String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1,\n      clusteringColsOpt: Option[List[Column]] = None,\n      logCompactionInterval: Int = 10,\n      txnId: Option[(String, Long)] = None,\n      tablePropertiesRemoved: Set[String] = null): Transaction\n  // scalastyle:on argcount\n\n  def getReplaceTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      clusteringColsOpt: Option[Seq[Column]] = None,\n      tableProperties: Map[String, String] = null,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1): Transaction\n}\n\n/** An implementation of [[TransactionBuilderSupport]] that uses the V1 transaction builder. */\ntrait TransactionBuilderV1Support extends TransactionBuilderSupport with TestUtils {\n\n  // scalastyle:off argcount\n  override def getCreateTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      tableProperties: Map[String, String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1,\n      clusteringColsOpt: Option[List[Column]] = None): Transaction = {\n    // scalastyle:on argcount\n    var txnBuilder = TableImpl.forPath(engine, tablePath, clock)\n      .createTransactionBuilder(engine, \"test-engine\", Operation.CREATE_TABLE)\n      .withSchema(engine, schema)\n    if (partCols != null) {\n      txnBuilder = txnBuilder.withPartitionColumns(engine, partCols.asJava)\n    }\n    if (tableProperties != null) {\n      txnBuilder = txnBuilder.withTableProperties(engine, tableProperties.asJava)\n    }\n    if (withDomainMetadataSupported) {\n      txnBuilder = txnBuilder.withDomainMetadataSupported()\n    }\n    if (maxRetries >= 0) {\n      txnBuilder = txnBuilder.withMaxRetries(maxRetries)\n    }\n    if (clusteringColsOpt.isDefined) {\n      txnBuilder = txnBuilder.withClusteringColumns(engine, clusteringColsOpt.get.asJava)\n    }\n    txnBuilder.build(engine)\n  }\n\n  // scalastyle:off argcount\n  override def getUpdateTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = null,\n      tableProperties: Map[String, String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1,\n      clusteringColsOpt: Option[List[Column]] = None,\n      logCompactionInterval: Int = 10,\n      txnId: Option[(String, Long)] = None,\n      tablePropertiesRemoved: Set[String] = null): Transaction = {\n    // scalastyle:on argcount\n    var txnBuilder = TableImpl.forPath(engine, tablePath, clock)\n      .createTransactionBuilder(engine, \"test-engine\", Operation.WRITE)\n    if (schema != null) {\n      txnBuilder = txnBuilder.withSchema(engine, schema)\n    }\n    if (tableProperties != null) {\n      txnBuilder = txnBuilder.withTableProperties(engine, tableProperties.asJava)\n    }\n    if (withDomainMetadataSupported) {\n      txnBuilder = txnBuilder.withDomainMetadataSupported()\n    }\n    if (maxRetries >= 0) {\n      txnBuilder = txnBuilder.withMaxRetries(maxRetries)\n    }\n    if (clusteringColsOpt.isDefined) {\n      txnBuilder = txnBuilder.withClusteringColumns(engine, clusteringColsOpt.get.asJava)\n    }\n    txnBuilder = txnBuilder.withLogCompactionInverval(logCompactionInterval)\n    txnId.foreach { case (appId, txnVer) =>\n      txnBuilder = txnBuilder.withTransactionId(engine, appId, txnVer)\n    }\n    if (tablePropertiesRemoved != null) {\n      txnBuilder = txnBuilder.withTablePropertiesRemoved(tablePropertiesRemoved.asJava)\n    }\n    txnBuilder.build(engine)\n  }\n\n  override def getReplaceTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      clusteringColsOpt: Option[Seq[Column]] = None,\n      tableProperties: Map[String, String] = null,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1): Transaction = {\n    var txnBuilder = Table.forPath(engine, tablePath).asInstanceOf[TableImpl]\n      .createReplaceTableTransactionBuilder(engine, \"test-engine\")\n      .withSchema(engine, schema)\n    if (partCols != null) {\n      txnBuilder = txnBuilder.withPartitionColumns(engine, partCols.asJava)\n    }\n    if (tableProperties != null) {\n      txnBuilder = txnBuilder.withTableProperties(engine, tableProperties.asJava)\n    }\n    if (withDomainMetadataSupported) {\n      txnBuilder = txnBuilder.withDomainMetadataSupported()\n    }\n    clusteringColsOpt.foreach { cols =>\n      txnBuilder = txnBuilder.withClusteringColumns(engine, cols.asJava)\n    }\n    if (maxRetries >= 0) {\n      txnBuilder = txnBuilder.withMaxRetries(maxRetries)\n    }\n    txnBuilder.build(engine)\n  }\n}\n\n/** An implementation of [[TransactionBuilderSupport]] that uses the V2 transaction builder. */\ntrait TransactionBuilderV2Support extends TransactionBuilderSupport with TestUtils {\n\n  // scalastyle:off argcount\n  override def getCreateTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      tableProperties: Map[String, String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1,\n      clusteringColsOpt: Option[List[Column]] = None): Transaction = {\n    // scalastyle:on argcount\n    var txnBuilder = TableManager.buildCreateTableTransaction(\n      tablePath,\n      schema,\n      \"test-engine\")\n      .asInstanceOf[CreateTableTransactionBuilderImpl]\n      .withClock(clock)\n    if (partCols != null) {\n      txnBuilder = txnBuilder.withDataLayoutSpec(\n        DataLayoutSpec.partitioned(partCols.map(new Column(_)).asJava))\n    }\n    val completeTblProps =\n      tblPropertiesWithDomainMetadata(tableProperties, withDomainMetadataSupported)\n    if (completeTblProps != null) {\n      txnBuilder = txnBuilder.withTableProperties(completeTblProps.asJava)\n    }\n    if (clusteringColsOpt.nonEmpty) {\n      txnBuilder = txnBuilder.withDataLayoutSpec(\n        DataLayoutSpec.clustered(clusteringColsOpt.get.asJava))\n    }\n    if (maxRetries >= 0) {\n      txnBuilder = txnBuilder.withMaxRetries(maxRetries)\n    }\n    txnBuilder.build(engine)\n  }\n\n  // scalastyle:off argcount\n  override def getUpdateTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType = null,\n      tableProperties: Map[String, String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1,\n      clusteringColsOpt: Option[List[Column]] = None,\n      logCompactionInterval: Int = 10,\n      txnId: Option[(String, Long)] = None,\n      tablePropertiesRemoved: Set[String] = null): Transaction = {\n    // scalastyle:on argcount\n    var txnBuilder = TableManager.loadSnapshot(tablePath)\n      .build(engine)\n      .buildUpdateTableTransaction(\"test-engine\", Operation.WRITE)\n      .asInstanceOf[UpdateTableTransactionBuilderImpl]\n      .withClock(clock)\n    if (schema != null) {\n      txnBuilder = txnBuilder.withUpdatedSchema(schema)\n    }\n    clusteringColsOpt.foreach { clusteringCols =>\n      txnBuilder = txnBuilder.withClusteringColumns(clusteringCols.asJava)\n    }\n    val completeTblProps =\n      tblPropertiesWithDomainMetadata(tableProperties, withDomainMetadataSupported)\n    if (completeTblProps != null) {\n      txnBuilder = txnBuilder.withTablePropertiesAdded(completeTblProps.asJava)\n    }\n    if (maxRetries >= 0) {\n      txnBuilder = txnBuilder.withMaxRetries(maxRetries)\n    }\n    txnBuilder = txnBuilder.withLogCompactionInterval(logCompactionInterval)\n    txnId.foreach { case (appId, txnVer) =>\n      txnBuilder = txnBuilder.withTransactionId(appId, txnVer)\n    }\n    if (tablePropertiesRemoved != null) {\n      txnBuilder = txnBuilder.withTablePropertiesRemoved(tablePropertiesRemoved.asJava)\n    }\n    txnBuilder.build(engine)\n  }\n\n  override def getReplaceTxn(\n      engine: Engine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      clusteringColsOpt: Option[Seq[Column]] = None,\n      tableProperties: Map[String, String] = null,\n      withDomainMetadataSupported: Boolean = false,\n      maxRetries: Int = -1): Transaction = {\n    var txnBuilder = TableManager.loadSnapshot(tablePath)\n      .build(engine).asInstanceOf[SnapshotImpl]\n      .buildReplaceTableTransaction(schema, \"test-engine\")\n    if (partCols != null) {\n      txnBuilder = txnBuilder.withDataLayoutSpec(\n        DataLayoutSpec.partitioned(partCols.map(new Column(_)).asJava))\n    }\n    val completeTblProps =\n      tblPropertiesWithDomainMetadata(tableProperties, withDomainMetadataSupported)\n    if (completeTblProps != null) {\n      txnBuilder = txnBuilder.withTableProperties(completeTblProps.asJava)\n    }\n    if (clusteringColsOpt.nonEmpty) {\n      txnBuilder = txnBuilder.withDataLayoutSpec(\n        DataLayoutSpec.clustered(clusteringColsOpt.get.asJava))\n    }\n    if (maxRetries >= 0) {\n      txnBuilder = txnBuilder.withMaxRetries(maxRetries)\n    }\n    txnBuilder.build(engine)\n  }\n\n  private def tblPropertiesWithDomainMetadata(\n      tableProperties: Map[String, String],\n      withDomainMetadataSupported: Boolean): Map[String, String] = {\n    if (tableProperties == null && !withDomainMetadataSupported) {\n      null\n    } else {\n      val origTblProps = if (tableProperties != null) tableProperties else Map()\n      val dmTblProps = if (withDomainMetadataSupported) {\n        Map(TableFeatures.SET_TABLE_FEATURE_SUPPORTED_PREFIX + \"domainMetadata\" -> \"supported\")\n      } else {\n        Map()\n      }\n      (origTblProps ++ dmTblProps).toMap\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/utils/WriteUtils.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.defaults.utils\n\nimport java.io.File\nimport java.nio.file.{Files, Paths}\nimport java.util.Collections.emptyMap\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.{ListMap, Seq}\n\nimport io.delta.golden.GoldenTableUtils.goldenTablePath\nimport io.delta.kernel._\nimport io.delta.kernel.data.{ColumnarBatch, ColumnVector, FilteredColumnarBatch, Row}\nimport io.delta.kernel.defaults.internal.data.DefaultColumnarBatch\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.expressions.{Column, Literal}\nimport io.delta.kernel.expressions.Literal.ofInt\nimport io.delta.kernel.hook.PostCommitHook.PostCommitHookType\nimport io.delta.kernel.internal._\nimport io.delta.kernel.internal.actions.{DomainMetadata, Metadata, Protocol, SingleAction}\nimport io.delta.kernel.internal.fs.{Path => DeltaPath}\nimport io.delta.kernel.internal.util.{Clock, FileNames}\nimport io.delta.kernel.internal.util.SchemaUtils.casePreservingPartitionColNames\nimport io.delta.kernel.internal.util.Utils.{singletonCloseableIterator, toCloseableIterator}\nimport io.delta.kernel.statistics.DataFileStatistics\nimport io.delta.kernel.types.IntegerType.INTEGER\nimport io.delta.kernel.types.StructType\nimport io.delta.kernel.utils.{CloseableIterable, CloseableIterator, DataFileStatus, FileStatus}\nimport io.delta.kernel.utils.CloseableIterable.{emptyIterable, inMemoryIterable}\n\nimport org.apache.spark.sql.delta.VersionNotFoundException\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\n/** Default write utilities that use the V1 transaction builders and legacy Table Snapshot APIs */\ntrait WriteUtils extends AbstractWriteUtils with TransactionBuilderV1Support\n    with TestUtilsWithLegacyKernelAPIs\n\n/**\n * DO NOT MODIFY this trait -- this is just syntactic sugar to clearly indicate we are extending the\n * \"default\" WriteUtils which happens to use the legacy Kernel APIs\n */\ntrait WriteUtilsWithV1Builders extends WriteUtils\n\n/**\n * Write utilities that use the V2 transaction builders to create transactions and TableManager\n * snapshot APIs\n */\ntrait WriteUtilsWithV2Builders extends AbstractWriteUtils with TransactionBuilderV2Support\n    with TestUtilsWithTableManagerAPIs\n\n/**\n * Common utility methods for write test suites. For now, this includes mostly concrete\n * implementations for the utilities. As we improve our test structure, we should move concrete\n * implementations out of this class (like we have done for [[TransactionBuilderSupport]]). For\n * example, `commitTransaction` could go into a [[TransactionCommitSupport]] trait since it is\n * overridden in child suites.\n */\ntrait AbstractWriteUtils extends TestUtils with TransactionBuilderSupport {\n  val OBJ_MAPPER = new ObjectMapper()\n  val testEngineInfo = \"test-engine\"\n\n  /** Test table schemas and test */\n  lazy val testSchema = new StructType().add(\"id\", INTEGER)\n  lazy val dataBatches1 = generateData(testSchema, Seq.empty, Map.empty, 200, 3)\n  lazy val dataBatches2 = generateData(testSchema, Seq.empty, Map.empty, 400, 5)\n  lazy val seqOfUnpartitionedDataBatch1 = Seq(Map.empty[String, Literal] -> dataBatches1)\n  lazy val seqOfUnpartitionedDataBatch2 = Seq(Map.empty[String, Literal] -> dataBatches2)\n\n  val testPartitionColumns = Seq(\"part1\", \"part2\")\n  val testPartitionSchema = new StructType()\n    .add(\"id\", INTEGER)\n    .add(\"part1\", INTEGER) // partition column\n    .add(\"part2\", INTEGER) // partition column\n\n  val dataPartitionBatches1 = generateData(\n    testPartitionSchema,\n    testPartitionColumns,\n    Map(\"part1\" -> ofInt(1), \"part2\" -> ofInt(2)),\n    batchSize = 237,\n    numBatches = 3)\n\n  val dataPartitionBatches2 = generateData(\n    testPartitionSchema,\n    testPartitionColumns,\n    Map(\"part1\" -> ofInt(4), \"part2\" -> ofInt(5)),\n    batchSize = 876,\n    numBatches = 7)\n\n  val testClusteringColumns = List(new Column(\"part1\"), new Column(\"part2\"))\n  val dataClusteringBatches1 = generateData(\n    testPartitionSchema,\n    partitionCols = Seq.empty,\n    partitionValues = Map.empty,\n    batchSize = 200,\n    numBatches = 3)\n\n  val dataClusteringBatches2 = generateData(\n    testPartitionSchema,\n    partitionCols = Seq.empty,\n    partitionValues = Map.empty,\n    batchSize = 456,\n    numBatches = 5)\n\n  def verifyLastCheckpointMetadata(tablePath: String, checkpointAt: Long, expSize: Long): Unit = {\n    val filePath = f\"$tablePath/_delta_log/_last_checkpoint\"\n\n    val source = scala.io.Source.fromFile(filePath)\n    val result =\n      try source.getLines().mkString(\",\")\n      finally source.close()\n\n    assert(result === s\"\"\"{\"version\":$checkpointAt,\"size\":$expSize}\"\"\")\n  }\n\n  /**\n   * Helper method to remove the delta files before the given version, to make sure the read is\n   * using a checkpoint as base for state reconstruction.\n   */\n  def deleteDeltaFilesBefore(tablePath: String, beforeVersion: Long): Unit = {\n    Seq.range(0, beforeVersion).foreach { version =>\n      val filePath = new Path(f\"$tablePath/_delta_log/$version%020d.json\")\n      new Path(tablePath).getFileSystem(new Configuration()).delete(\n        filePath,\n        false /* recursive */ )\n    }\n\n    // try to query a version < beforeVersion\n    val ex = intercept[VersionNotFoundException] {\n      spark.read.format(\"delta\").option(\"versionAsOf\", beforeVersion - 1).load(tablePath)\n    }\n    assert(ex.getMessage().contains(\n      s\"Cannot time travel Delta table to version ${beforeVersion - 1}\"))\n  }\n\n  def setCheckpointInterval(tablePath: String, interval: Int): Unit = {\n    spark.sql(s\"ALTER TABLE delta.`$tablePath` \" +\n      s\"SET TBLPROPERTIES ('delta.checkpointInterval' = '$interval')\")\n  }\n\n  def dataFileCount(tablePath: String): Int = {\n    Files.walk(Paths.get(tablePath)).iterator().asScala\n      .count(path => path.toString.endsWith(\".parquet\") && !path.toString.contains(\"_delta_log\"))\n  }\n\n  def checkpointFilePath(tablePath: String, checkpointVersion: Long): String = {\n    f\"$tablePath/_delta_log/$checkpointVersion%020d.checkpoint.parquet\"\n  }\n\n  def assertCheckpointExists(tablePath: String, atVersion: Long): Unit = {\n    val cpPath = checkpointFilePath(tablePath, checkpointVersion = atVersion)\n    assert(new File(cpPath).exists())\n  }\n\n  def copyTable(goldenTableName: String, targetLocation: String): Unit = {\n    val source = new File(goldenTablePath(goldenTableName))\n    val target = new File(targetLocation)\n    FileUtils.copyDirectory(source, target)\n  }\n\n  def checkpointIfReady(\n      engine: Engine,\n      tablePath: String,\n      result: TransactionCommitResult,\n      expSize: Long): Unit = {\n    result.getPostCommitHooks.forEach(hook => {\n      if (hook.getType == PostCommitHookType.CHECKPOINT) {\n        hook.threadSafeInvoke(engine)\n        verifyLastCheckpointMetadata(tablePath, checkpointAt = result.getVersion, expSize)\n      }\n    })\n  }\n\n  /**\n   * Helper method to read the commit file of the given version and return the value at the given\n   * ordinal if it is not null and the consumer returns a value, otherwise return null.\n   */\n  def readCommitFile(\n      engine: Engine,\n      tablePath: String,\n      version: Long,\n      consumer: Row => Option[Any]): Option[Any] = {\n    val table = Table.forPath(engine, tablePath)\n    val logPath = new DeltaPath(table.getPath(engine), \"_delta_log\")\n    val file = FileStatus.of(FileNames.deltaFile(logPath, version), 0, 0)\n    val columnarBatches = engine.getJsonHandler.readJsonFiles(\n      singletonCloseableIterator(file),\n      SingleAction.FULL_SCHEMA,\n      Optional.empty())\n    while (columnarBatches.hasNext) {\n      val batch = columnarBatches.next\n      val rows = batch.getRows\n      while (rows.hasNext) {\n        val row = rows.next\n        val ret = consumer(row)\n        if (ret.isDefined) {\n          return ret\n        }\n      }\n    }\n    Option.empty\n  }\n\n  def getMetadata(engine: Engine, tablePath: String): Metadata = {\n    getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).getMetadata\n  }\n\n  def getProtocol(engine: Engine, tablePath: String): Protocol = {\n    getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath).getProtocol\n  }\n\n  /**\n   *  Helper method to read the Metadata from the commit file of the given version if it is not\n   *  null, otherwise return null.\n   *  TODO: get rid of this and use getMetadata instead\n   */\n  def getMetadataActionFromCommit(\n      engine: Engine,\n      table: Table,\n      version: Long): Option[Row] = {\n    readCommitFile(\n      engine,\n      table.getPath(engine),\n      version,\n      (row) => {\n        val ord = row.getSchema.indexOf(\"metaData\")\n        if (!row.isNullAt(ord)) {\n          Option(row.getStruct(ord))\n        } else {\n          Option.empty\n        }\n      }).map { case metadata: Row => Some(metadata) }.getOrElse(Option.empty)\n  }\n\n  /**\n   *  Helper method to read the Protocol from the commit file of the given version if it is not\n   *  null, otherwise return null.\n   *  TODO: get rid of this and use getProtocol instead\n   */\n  def getProtocolActionFromCommit(engine: Engine, tablePath: String, version: Long): Option[Row] = {\n    readCommitFile(\n      engine,\n      tablePath,\n      version,\n      (row) => {\n        val ord = row.getSchema.indexOf(\"protocol\")\n        if (!row.isNullAt(ord)) {\n          Some(row.getStruct(ord))\n        } else {\n          Option.empty\n        }\n      }).map { case protocol: Row => Some(protocol) }.getOrElse(Option.empty)\n  }\n\n  def generateData(\n      schema: StructType,\n      partitionCols: Seq[String],\n      partitionValues: Map[String, Literal],\n      batchSize: Int,\n      numBatches: Int): Seq[FilteredColumnarBatch] = {\n    val partitionValuesSchemaCase =\n      casePreservingPartitionColNames(partitionCols.asJava, partitionValues.asJava)\n\n    var batches = Seq.empty[ColumnarBatch]\n    for (_ <- 0 until numBatches) {\n      var vectors = Seq.empty[ColumnVector]\n      schema.fields().forEach { field =>\n        val colType = field.getDataType\n        val partValue = partitionValuesSchemaCase.get(field.getName)\n        if (partValue != null) {\n          // handle the partition column by inserting a vector with single value\n          val vector = testSingleValueVector(colType, batchSize, partValue.getValue)\n          vectors = vectors :+ vector\n        } else {\n          // handle the regular columns\n          val vector = testColumnVector(batchSize, colType)\n          vectors = vectors :+ vector\n        }\n      }\n      batches = batches :+ new DefaultColumnarBatch(batchSize, schema, vectors.toArray)\n    }\n    batches.map(batch => new FilteredColumnarBatch(batch, Optional.empty()))\n  }\n\n  def stageData(\n      state: Row,\n      partitionValues: Map[String, Literal],\n      data: Seq[FilteredColumnarBatch])\n      : CloseableIterator[Row] = {\n    val physicalDataIter = Transaction.transformLogicalData(\n      defaultEngine,\n      state,\n      toCloseableIterator(data.toIterator.asJava),\n      partitionValues.asJava)\n\n    val writeContext = Transaction.getWriteContext(defaultEngine, state, partitionValues.asJava)\n\n    val writeResultIter = defaultEngine\n      .getParquetHandler\n      .writeParquetFiles(\n        writeContext.getTargetDirectory,\n        physicalDataIter,\n        writeContext.getStatisticsColumns)\n\n    Transaction.generateAppendActions(defaultEngine, state, writeResultIter, writeContext)\n  }\n\n  def createTxnWithDomainMetadatas(\n      engine: Engine,\n      tablePath: String,\n      domainMetadatas: Seq[DomainMetadata],\n      useInternalApi: Boolean = false,\n      enableDomainMetadata: Boolean = true): Transaction = {\n\n    val txn = if (domainMetadatas.nonEmpty && !useInternalApi) {\n      getUpdateTxn(engine, tablePath, withDomainMetadataSupported = enableDomainMetadata)\n        .asInstanceOf[TransactionImpl]\n    } else {\n      getUpdateTxn(engine, tablePath).asInstanceOf[TransactionImpl]\n    }\n\n    domainMetadatas.foreach { dm =>\n      if (dm.isRemoved) {\n        if (useInternalApi) {\n          txn.removeDomainMetadataInternal(dm.getDomain)\n        } else {\n          txn.removeDomainMetadata(dm.getDomain)\n        }\n      } else {\n        if (useInternalApi) {\n          txn.addDomainMetadataInternal(dm.getDomain, dm.getConfiguration)\n        } else {\n          txn.addDomainMetadata(dm.getDomain, dm.getConfiguration)\n        }\n      }\n    }\n    txn\n  }\n\n  def getAppendActions(\n      txn: Transaction,\n      data: Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])]): CloseableIterable[Row] = {\n\n    val txnState = txn.getTransactionState(defaultEngine)\n\n    val actions = data.map { case (partValues, partData) =>\n      stageData(txnState, partValues, partData)\n    }\n\n    actions.reduceLeftOption(_ combine _) match {\n      case Some(combinedActions) =>\n        inMemoryIterable(combinedActions)\n      case None =>\n        emptyIterable[Row]\n    }\n  }\n\n  def commitAppendData(\n      engine: Engine = defaultEngine,\n      txn: Transaction,\n      data: Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])]): TransactionCommitResult = {\n    commitTransaction(txn, engine, getAppendActions(txn, data))\n  }\n\n  /** Utility to create table, with no data */\n  def createEmptyTable(\n      engine: Engine = defaultEngine,\n      tablePath: String,\n      schema: StructType,\n      partCols: Seq[String] = null,\n      clock: Clock = () => System.currentTimeMillis,\n      tableProperties: Map[String, String] = null,\n      clusteringColsOpt: Option[List[Column]] = None): TransactionCommitResult = {\n\n    appendData(\n      engine,\n      tablePath,\n      isNewTable = true,\n      schema,\n      partCols,\n      data = Seq.empty,\n      clock,\n      tableProperties,\n      clusteringColsOpt)\n  }\n\n  /** Update an existing table - metadata only changes (no data changes) */\n  def updateTableMetadata(\n      engine: Engine = defaultEngine,\n      tablePath: String,\n      schema: StructType = null, // non-null schema means schema change\n      clock: Clock = () => System.currentTimeMillis,\n      tableProperties: Map[String, String] = null,\n      clusteringColsOpt: Option[List[Column]] = None): TransactionCommitResult = {\n    appendData(\n      engine,\n      tablePath,\n      isNewTable = false,\n      schema,\n      Seq.empty,\n      data = Seq.empty,\n      clock,\n      tableProperties,\n      clusteringColsOpt)\n  }\n\n  def appendData(\n      engine: Engine = defaultEngine,\n      tablePath: String,\n      isNewTable: Boolean = false,\n      schema: StructType = null,\n      partCols: Seq[String] = null,\n      data: Seq[(Map[String, Literal], Seq[FilteredColumnarBatch])],\n      clock: Clock = () => System.currentTimeMillis,\n      tableProperties: Map[String, String] = null,\n      clusteringColsOpt: Option[List[Column]] = None): TransactionCommitResult = {\n\n    val txn = if (isNewTable) {\n      getCreateTxn(\n        engine,\n        tablePath,\n        schema,\n        partCols,\n        tableProperties,\n        clock,\n        clusteringColsOpt = clusteringColsOpt)\n    } else {\n      getUpdateTxn(\n        engine,\n        tablePath,\n        schema,\n        tableProperties,\n        clock,\n        clusteringColsOpt = clusteringColsOpt)\n    }\n    commitAppendData(engine, txn, data)\n  }\n\n  def assertMetadataProp(\n      snapshot: SnapshotImpl,\n      key: TableConfig[_ <: Any],\n      expectedValue: Any): Unit = {\n    assert(key.fromMetadata(snapshot.getMetadata) == expectedValue)\n  }\n\n  def assertHasNoMetadataProp(snapshot: SnapshotImpl, key: TableConfig[_ <: Any]): Unit = {\n    assertMetadataProp(snapshot, key, Optional.empty())\n  }\n\n  def assertHasWriterFeature(snapshot: SnapshotImpl, writerFeature: String): Unit = {\n    assert(snapshot.getProtocol.getWriterFeatures.contains(writerFeature))\n  }\n\n  def assertHasNoWriterFeature(snapshot: SnapshotImpl, writerFeature: String): Unit = {\n    assert(!snapshot.getProtocol.getWriterFeatures.contains(writerFeature))\n  }\n\n  def setTablePropAndVerify(\n      engine: Engine,\n      tablePath: String,\n      isNewTable: Boolean = true,\n      key: TableConfig[_ <: Any],\n      value: String,\n      expectedValue: Any,\n      clock: Clock = () => System.currentTimeMillis): Unit = {\n\n    val txn = if (isNewTable) {\n      getCreateTxn(\n        engine,\n        tablePath,\n        testSchema,\n        tableProperties = Map(key.getKey -> value),\n        clock = clock)\n    } else {\n      getUpdateTxn(\n        engine,\n        tablePath,\n        schema = if (isNewTable) testSchema else null,\n        tableProperties = Map(key.getKey -> value),\n        clock = clock)\n    }\n    commitTransaction(txn, engine, emptyIterable())\n\n    val snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n    assertMetadataProp(snapshot, key, expectedValue)\n  }\n\n  protected def verifyWrittenContent(\n      path: String,\n      expSchema: StructType,\n      expData: Seq[TestRow]): Unit = {\n    val actSchema = tableSchema(path)\n    assert(expSchema.isWriteCompatible(actSchema))\n\n    // verify data using Kernel reader\n    checkTable(path, expData)\n\n    // verify data using Spark reader.\n    // Spark reads the timestamp partition columns in local timezone vs. Kernel reads in UTC. We\n    // need to set the timezone to UTC before reading the data using Spark to make the tests pass\n    withSparkTimeZone(\"UTC\") {\n      val resultSpark = spark.sql(s\"SELECT * FROM delta.`$path`\").collect().map(TestRow(_))\n      checkAnswer(resultSpark, expData)\n    }\n  }\n\n  def verifyCommitInfo(\n      tablePath: String,\n      version: Long,\n      partitionCols: Seq[String] = Seq.empty): Unit = {\n    val expectedOperation = if (version == 0) Operation.CREATE_TABLE else Operation.WRITE\n    val row = spark.sql(s\"DESCRIBE HISTORY delta.`$tablePath`\")\n      .filter(s\"version = $version\")\n      .select(\n        \"version\",\n        \"operationParameters.partitionBy\",\n        \"isBlindAppend\",\n        \"engineInfo\",\n        \"operation\")\n      .collect().last\n\n    assert(row.getAs[Long](\"version\") === version)\n    assert(row.getAs[Long](\"partitionBy\") ===\n      (if (partitionCols == null) null else OBJ_MAPPER.writeValueAsString(partitionCols.asJava)))\n    // For now we've hardcoded isBlindAppend=false, once we support more precise setting of this\n    // field we should update this check\n    assert(!row.getAs[Boolean](\"isBlindAppend\"))\n    assert(row.getAs[Seq[String]](\"engineInfo\") ===\n      \"Kernel-\" + Meta.KERNEL_VERSION + \"/\" + testEngineInfo)\n    assert(row.getAs[String](\"operation\") === expectedOperation.getDescription)\n  }\n\n  def verifyCommitResult(\n      result: TransactionCommitResult,\n      expVersion: Long,\n      expIsReadyForCheckpoint: Boolean): Unit = {\n    assert(result.getVersion === expVersion)\n    assertCheckpointReadiness(result, expIsReadyForCheckpoint)\n  }\n\n  // TODO: Change this to use the table metadata and protocol and\n  // not rely on DESCRIBE which adds some properties based on the protocol.\n  def verifyTableProperties(\n      tablePath: String,\n      expProperties: ListMap[String, Any],\n      minReaderVersion: Int,\n      minWriterVersion: Int): Unit = {\n    val resultProperties = spark.sql(s\"DESCRIBE EXTENDED delta.`$tablePath`\")\n      .filter(\"col_name = 'Table Properties'\")\n      .select(\"data_type\")\n      .collect().map(TestRow(_))\n\n    val builder = new StringBuilder(\"[\")\n\n    expProperties.foreach { case (key, value) =>\n      builder.append(s\"$key=$value,\")\n    }\n\n    builder.append(s\"delta.minReaderVersion=$minReaderVersion,\")\n    builder.append(s\"delta.minWriterVersion=$minWriterVersion\")\n    builder.append(\"]\")\n    checkAnswer(resultProperties, Seq(builder.toString()).map(TestRow(_)))\n  }\n\n  def assertCheckpointReadiness(\n      txnResult: TransactionCommitResult,\n      isReadyForCheckpoint: Boolean): Unit = {\n    assert(\n      txnResult.getPostCommitHooks\n        .stream()\n        .anyMatch(hook => hook.getType == PostCommitHookType.CHECKPOINT) === isReadyForCheckpoint)\n  }\n\n  def collectStatsFromAddFiles(engine: Engine, path: String): Seq[String] = {\n    val snapshot = getTableManagerAdapter.getSnapshotAtLatest(engine, path)\n    val scan = snapshot.getScanBuilder.build()\n    val scanFiles = scan.asInstanceOf[ScanImpl].getScanFiles(engine, true)\n\n    scanFiles.asScala.toList.flatMap { scanFile =>\n      scanFile.getRows.asScala.toList.flatMap { row =>\n        val add = row.getStruct(row.getSchema.indexOf(\"add\"))\n        val idx = add.getSchema.indexOf(\"stats\")\n        if (idx >= 0 && !add.isNullAt(idx)) List(add.getString(idx)) else Nil\n      }\n    }\n  }\n\n  /**\n   * Commit transaction, all child suites should use this instead of txn.commit\n   * directly and could override it for specific test cases (e.g. commit and write CRC).\n   */\n  protected def commitTransaction(\n      txn: Transaction,\n      engine: Engine,\n      dataActions: CloseableIterable[Row]): TransactionCommitResult = {\n    txn.commit(engine, dataActions)\n  }\n\n  protected def generateDataFileStatus(\n      tablePath: String,\n      fileName: String,\n      fileSize: Long = 1000,\n      includeStats: Boolean = true): DataFileStatus = {\n    val filePath = defaultEngine.getFileSystemClient.resolvePath(tablePath + \"/\" + fileName)\n    new DataFileStatus(\n      filePath,\n      fileSize,\n      10,\n      if (includeStats) {\n        Optional.of(new DataFileStatistics(\n          100,\n          emptyMap(),\n          emptyMap(),\n          emptyMap(),\n          Optional.empty()))\n      } else Optional.empty())\n  }\n\n  protected def assertCommitResultHasClusteringCols(\n      commitResult: TransactionCommitResult,\n      expectedClusteringCols: Seq[Column]): Unit = {\n    val actualClusteringCols = commitResult.getTransactionReport.getClusteringColumns.asScala\n\n    assert(\n      actualClusteringCols === expectedClusteringCols,\n      s\"Expected clustering columns: $expectedClusteringCols, but got: $actualClusteringCols\")\n  }\n\n  /**\n   * A very particular utility that is used in both InCommitTimestampSuite and\n   * DeltaReplaceTableSuite.\n   */\n  protected def createTableThenEnableIctAndVerify(\n      engine: Engine,\n      tablePath: String): SnapshotImpl = {\n    // Create table without ICT. Note that this does not add ICT enablement tracking properties.\n    val txn1 = getCreateTxn(engine, tablePath, testSchema)\n    commitTransaction(txn1, engine, emptyIterable())\n\n    // Enable ICT. This should add enablement tracking properties.\n    setTablePropAndVerify(\n      engine = engine,\n      tablePath = tablePath,\n      isNewTable = false,\n      key = TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED,\n      value = \"true\",\n      expectedValue = true)\n\n    val snapshotV1 = getTableManagerAdapter.getSnapshotAtLatest(engine, tablePath)\n\n    // Verify enablement properties are present\n    assertMetadataProp(snapshotV1, TableConfig.IN_COMMIT_TIMESTAMPS_ENABLED, true)\n    assertMetadataProp(\n      snapshotV1,\n      TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP,\n      Optional.of(snapshotV1.getTimestamp(engine)))\n    assertMetadataProp(\n      snapshotV1,\n      TableConfig.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n      Optional.of(1L))\n\n    snapshotV1\n  }\n}\n"
  },
  {
    "path": "kernel/project/plugins.sbt",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-release\" % \"1.1.0\")\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-pgp\" % \"2.1.2\")\n\naddSbtPlugin(\"org.scalastyle\" %% \"scalastyle-sbt-plugin\" % \"1.0.0\")\n\naddSbtPlugin(\"com.eed3si9n\" % \"sbt-unidoc\" % \"0.4.3\")\n\naddSbtPlugin(\"com.typesafe\" % \"sbt-mima-plugin\" % \"1.0.1\")\n\naddSbtPlugin(\"org.xerial.sbt\" % \"sbt-sonatype\" % \"3.9.15\")\n\naddSbtPlugin(\"com.etsy\" % \"sbt-checkstyle-plugin\" % \"3.1.1\")\n\n// By default, sbt-checkstyle-plugin uses checkstyle version 6.15, but we should set it to use the\n// same version as Spark\ndependencyOverrides += \"com.puppycrawl.tools\" % \"checkstyle\" % \"8.43\"\n"
  },
  {
    "path": "kernel/scalastyle-config.xml",
    "content": "<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~    http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<!--\n  ~ This file contains code from the Apache Spark project (original license above).\n  ~It contains modifications, which are licensed as follows:\n  -->\n<!--\n  ~ Copyright (2021) The Delta Lake Project Authors.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<!--\n\nIf you wish to turn off checking for a section of code, you can put a comment in the source\nbefore and after the section, with the following syntax:\n\n  // scalastyle:off\n  ...  // stuff that breaks the styles\n  // scalastyle:on\n\nYou can also disable only one rule, by specifying its rule id, as specified in:\n  http://www.scalastyle.org/rules-0.7.0.html\n\n  // scalastyle:off no.finalize\n  override def finalize(): Unit = ...\n  // scalastyle:on no.finalize\n\nThis file is divided into 3 sections:\n (1) rules that we enforce.\n (2) rules that we would like to enforce, but haven't cleaned up the codebase to turn on yet\n     (or we need to make the scalastyle rule more configurable).\n (3) rules that we don't want to enforce.\n-->\n\n<scalastyle>\n  <name>Scalastyle standard configuration</name>\n\n  <!-- ================================================================================ -->\n  <!--                               rules we enforce                                   -->\n  <!-- ================================================================================ -->\n\n  <check level=\"error\" class=\"org.scalastyle.file.FileTabChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.HeaderMatchesChecker\" enabled=\"true\">\n    <parameters>\n       <parameter name=\"regex\">true</parameter>\n       <parameter name=\"header\"><![CDATA[(?:\\Q/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n\\E)?\\Q/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\\E]]></parameter>\n    </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SpacesAfterPlusChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SpacesBeforePlusChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.WhitespaceEndOfLineChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.FileLineLengthChecker\" enabled=\"true\">\n    <parameters>\n      <parameter name=\"maxLineLength\"><![CDATA[100]]></parameter>\n      <parameter name=\"tabSize\"><![CDATA[2]]></parameter>\n      <parameter name=\"ignoreImports\">true</parameter>\n    </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.ClassNamesChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\"><![CDATA[[A-Z][A-Za-z]*]]></parameter></parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.ObjectNamesChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\"><![CDATA[(config|[A-Z][A-Za-z]*)]]></parameter></parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.PackageObjectNamesChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\"><![CDATA[^[a-z][A-Za-z]*$]]></parameter></parameters>\n  </check>\n\n  <check customId=\"argcount\" level=\"error\" class=\"org.scalastyle.scalariform.ParameterNumberChecker\" enabled=\"true\">\n    <parameters><parameter name=\"maxParameters\"><![CDATA[10]]></parameter></parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoFinalizeChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.CovariantEqualsChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.StructuralTypeChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.UppercaseLChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.IfBraceChecker\" enabled=\"true\">\n    <parameters>\n      <parameter name=\"singleLineAllowed\"><![CDATA[true]]></parameter>\n      <parameter name=\"doubleLineAllowed\"><![CDATA[true]]></parameter>\n    </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.PublicMethodsHaveTypeChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.NewLineAtEofChecker\" enabled=\"true\"></check>\n\n  <check customId=\"nonascii\" level=\"error\" class=\"org.scalastyle.scalariform.NonASCIICharacterChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SpaceAfterCommentStartChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.EnsureSingleSpaceBeforeTokenChecker\" enabled=\"true\">\n   <parameters>\n     <parameter name=\"tokens\">ARROW, EQUALS, ELSE, TRY, CATCH, FINALLY, LARROW, RARROW</parameter>\n   </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.EnsureSingleSpaceAfterTokenChecker\" enabled=\"true\">\n    <parameters>\n     <parameter name=\"tokens\">ARROW, EQUALS, COMMA, COLON, IF, ELSE, DO, WHILE, FOR, MATCH, TRY, CATCH, FINALLY, LARROW, RARROW</parameter>\n    </parameters>\n  </check>\n\n  <!-- ??? usually shouldn't be checked into the code base. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NotImplementedErrorUsage\" enabled=\"true\"></check>\n\n  <!-- As of SPARK-7558, all tests in Spark should extend o.a.s.SparkFunSuite instead of FunSuite directly -->\n  <check customId=\"funsuite\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">^FunSuite[A-Za-z]*$</parameter></parameters>\n    <customMessage>Tests must extend org.apache.spark.SparkFunSuite instead.</customMessage>\n  </check>\n\n  <!-- As of SPARK-7977 all printlns need to be wrapped in '// scalastyle:off/on println' -->\n  <check customId=\"println\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">^println$</parameter></parameters>\n    <customMessage><![CDATA[Are you sure you want to println? If yes, wrap the code block with\n      // scalastyle:off println\n      println(...)\n      // scalastyle:on println]]></customMessage>\n  </check>\n\n  <check customId=\"hadoopconfiguration\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">spark(.sqlContext)?.sparkContext.hadoopConfiguration</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use sparkContext.hadoopConfiguration? In most cases, you should use\n      spark.sessionState.newHadoopConf() instead, so that the hadoop configurations specified in Spark session\n      configuration will come into effect.\n      If you must use sparkContext.hadoopConfiguration, wrap the code block with\n      // scalastyle:off hadoopconfiguration\n      spark.sparkContext.hadoopConfiguration...\n      // scalastyle:on hadoopconfiguration\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"deltahadoopconfiguration\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">sessionState.newHadoopConf</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use sessionState.newHadoopConf? In most cases, you should use\n      deltaLog.newDeltaHadoopConf() instead, so that the hadoop file system configurations specified\n      in DataFrame options will come into effect.\n      If you must use sessionState.newHadoopConf, wrap the code block with\n      // scalastyle:off deltahadoopconfiguration\n      spark.sessionState.newHadoopConf...\n      // scalastyle:on deltahadoopconfiguration\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"visiblefortesting\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">@VisibleForTesting</parameter></parameters>\n    <customMessage><![CDATA[\n      @VisibleForTesting causes classpath issues. Please note this in the java doc instead (SPARK-11615).\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"runtimeaddshutdownhook\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Runtime\\.getRuntime\\.addShutdownHook</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Runtime.getRuntime.addShutdownHook? In most cases, you should use\n      ShutdownHookManager.addShutdownHook instead.\n      If you must use Runtime.getRuntime.addShutdownHook, wrap the code block with\n      // scalastyle:off runtimeaddshutdownhook\n      Runtime.getRuntime.addShutdownHook(...)\n      // scalastyle:on runtimeaddshutdownhook\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"mutablesynchronizedbuffer\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">mutable\\.SynchronizedBuffer</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use mutable.SynchronizedBuffer? In most cases, you should use\n      java.util.concurrent.ConcurrentLinkedQueue instead.\n      If you must use mutable.SynchronizedBuffer, wrap the code block with\n      // scalastyle:off mutablesynchronizedbuffer\n      mutable.SynchronizedBuffer[...]\n      // scalastyle:on mutablesynchronizedbuffer\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"classforname\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Class\\.forName</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Class.forName? In most cases, you should use Utils.classForName instead.\n      If you must use Class.forName, wrap the code block with\n      // scalastyle:off classforname\n      Class.forName(...)\n      // scalastyle:on classforname\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"awaitresult\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Await\\.result</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Await.result? In most cases, you should use ThreadUtils.awaitResult instead.\n      If you must use Await.result, wrap the code block with\n      // scalastyle:off awaitresult\n      Await.result(...)\n      // scalastyle:on awaitresult\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"awaitready\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Await\\.ready</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Await.ready? In most cases, you should use ThreadUtils.awaitReady instead.\n      If you must use Await.ready, wrap the code block with\n      // scalastyle:off awaitready\n      Await.ready(...)\n      // scalastyle:on awaitready\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"caselocale\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">(\\.toUpperCase|\\.toLowerCase)(?!(\\(|\\(Locale.ROOT\\)))</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use toUpperCase or toLowerCase without the root locale? In most cases, you\n      should use toUpperCase(Locale.ROOT) or toLowerCase(Locale.ROOT) instead.\n      If you must use toUpperCase or toLowerCase without the root locale, wrap the code block with\n      // scalastyle:off caselocale\n      .toUpperCase\n      .toLowerCase\n      // scalastyle:on caselocale\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"typedlit\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">typed[lL]it</parameter></parameters>\n    <customMessage><![CDATA[\n      'typedlit' or `typedLit` uses ScalaReflection to resolve the data type which is inefficient in concurrent\n      queries. In most cases, you can use `lit` or `new Column(Literal(...))` instead.\n      If you must use it, wrap the code block with\n      // scalastyle:off typedlit\n      typedLit(\"foo\")\n      // scalastyle:on typedlit\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"sparkimplicits\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">spark(Session)?.implicits._</parameter></parameters>\n    <customMessage><![CDATA[\n      When importing `spark.implicits._`, it's easy to create an `Encoder` unintentionally which can hurt\n      the performance a lot in concurrent queries. You can use `import com.databricks.sql.transaction.tahoe.implicits._`\n      instead to use `Encoder`s we cache for reusing. If this doesn't work, you can define new `Encoder`s\n      in `DeltaEncoders` to cache and reuse them.\n      If you must use it, wrap the code block with\n      // scalastyle:off sparkimplicits\n      import spark.implicits._\n      // scalastyle:on sparkimplicits\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"throwerror\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">throw new \\w+Error\\(</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to throw Error? In most cases, you should use appropriate Exception instead.\n      If you must throw Error, wrap the code block with\n      // scalastyle:off throwerror\n      throw new XXXError(...)\n      // scalastyle:on throwerror\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"countstring\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">count\\(\"</parameter></parameters>\n    <customMessage><![CDATA[\n      'count(String)' returns 'TypedColumn' and touches ScalaReflection which is inefficient in concurrent queries.\n      In most cases, you don't need a 'TypedColumn' and you should use 'count(new Column(\"...\"))' instead.\n      If you must use it, wrap the code block with\n      // scalastyle:off countstring\n      count(\"foo\")\n      // scalastyle:on countstring\n    ]]></customMessage>\n  </check>\n\n  <!-- As of SPARK-9613 JavaConversions should be replaced with JavaConverters -->\n  <check customId=\"javaconversions\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">JavaConversions</parameter></parameters>\n    <customMessage>Instead of importing implicits in scala.collection.JavaConversions._, import\n    scala.collection.JavaConverters._ and use .asScala / .asJava methods</customMessage>\n  </check>\n\n  <check customId=\"commonslang2\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">org\\.apache\\.commons\\.lang\\.</parameter></parameters>\n    <customMessage>Use Commons Lang 3 classes (package org.apache.commons.lang3.*) instead\n    of Commons Lang 2 (package org.apache.commons.lang.*)</customMessage>\n  </check>\n\n  <check customId=\"extractopt\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">extractOpt</parameter></parameters>\n    <customMessage>Use jsonOption(x).map(.extract[T]) instead of .extractOpt[T], as the latter\n    is slower.  </customMessage>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.DisallowSpaceBeforeTokenChecker\" enabled=\"true\">\n    <parameters>\n      <parameter name=\"tokens\">COMMA</parameter>\n    </parameters>\n  </check>\n\n  <!-- SPARK-3854: Single Space between ')' and '{' -->\n  <check customId=\"SingleSpaceBetweenRParenAndLCurlyBrace\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">\\)\\{</parameter></parameters>\n    <customMessage><![CDATA[\n      Single Space between ')' and `{`.\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"NoScalaDoc\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">(?m)^(\\s*)/[*][*].*$(\\r|)\\n^\\1  [*]</parameter></parameters>\n    <customMessage>Use Javadoc style indentation for multiline comments</customMessage>\n  </check>\n\n  <check customId=\"OmitBracesInCase\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">case[^\\n>]*=>\\s*\\{</parameter></parameters>\n    <customMessage>Omit braces in case clauses.</customMessage>\n  </check>\n\n  <!-- SPARK-16877: Avoid Java annotations -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.OverrideJavaChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.DeprecatedJavaChecker\" enabled=\"true\"></check>\n\n  <!-- ================================================================================ -->\n  <!--       rules we'd like to enforce, but haven't cleaned up the codebase yet        -->\n  <!-- ================================================================================ -->\n\n  <!-- We cannot turn the following two on, because it'd fail a lot of string interpolation use cases. -->\n  <!-- Ideally the following two rules should be configurable to rule out string interpolation. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoWhitespaceBeforeLeftBracketChecker\" enabled=\"false\"></check>\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoWhitespaceAfterLeftBracketChecker\" enabled=\"false\"></check>\n\n  <!-- This breaks symbolic method names so we don't turn it on. -->\n  <!-- Maybe we should update it to allow basic symbolic names, and then we are good to go. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.MethodNamesChecker\" enabled=\"false\">\n    <parameters>\n    <parameter name=\"regex\"><![CDATA[^[a-z][A-Za-z0-9]*$]]></parameter>\n    </parameters>\n  </check>\n\n  <!-- Should turn this on, but we have a few places that need to be fixed first -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.EqualsHashCodeChecker\" enabled=\"true\"></check>\n\n  <!-- ================================================================================ -->\n  <!--                               rules we don't want                                -->\n  <!-- ================================================================================ -->\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.IllegalImportsChecker\" enabled=\"false\">\n    <parameters><parameter name=\"illegalImports\"><![CDATA[sun._,java.awt._]]></parameter></parameters>\n  </check>\n\n  <!-- We want the opposite of this: NewLineAtEofChecker -->\n  <check level=\"error\" class=\"org.scalastyle.file.NoNewLineAtEofChecker\" enabled=\"false\"></check>\n\n  <!-- This one complains about all kinds of random things. Disable. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SimplifyBooleanExpressionChecker\" enabled=\"false\"></check>\n\n  <!-- We use return quite a bit for control flows and guards -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.ReturnChecker\" enabled=\"false\"></check>\n\n  <!-- We use null a lot in low level code and to interface with 3rd party code -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NullChecker\" enabled=\"false\"></check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoCloneChecker\" enabled=\"false\"></check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.file.FileLengthChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxFileLength\">800></parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NumberOfTypesChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxTypes\">30</parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.CyclomaticComplexityChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maximum\">10</parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.MethodLengthChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxLength\">50</parameter></parameters>\n  </check>\n\n  <!-- Not exactly feasible to enforce this right now. -->\n  <!-- It is also infrequent that somebody introduces a new class with a lot of methods. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NumberOfMethodsInTypeChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxMethods\"><![CDATA[30]]></parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here, and we have a lot of magic numbers ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.MagicNumberChecker\" enabled=\"false\">\n    <parameters><parameter name=\"ignore\">-1,0,1,2,3</parameter></parameters>\n  </check>\n\n</scalastyle>\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/UCCatalogManagedClient.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.unitycatalog.utils.OperationTimer.timeUncheckedOperation;\n\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.CommitRangeBuilder;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.SnapshotBuilder;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.annotation.Experimental;\nimport io.delta.kernel.commit.Committer;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.files.ParsedLogData;\nimport io.delta.kernel.internal.lang.Lazy;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.transaction.CreateTableTransactionBuilder;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.unitycatalog.metrics.UcLoadSnapshotTelemetry;\nimport io.delta.storage.commit.Commit;\nimport io.delta.storage.commit.GetCommitsResponse;\nimport io.delta.storage.commit.uccommitcoordinator.UCClient;\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorException;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.*;\nimport java.util.function.BiConsumer;\nimport java.util.stream.Collectors;\nimport org.apache.hadoop.fs.Path;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Client for interacting with Unity Catalog (UC) catalog-managed Delta tables.\n *\n * @see UCClient\n * @see Snapshot\n */\n@Experimental\npublic class UCCatalogManagedClient {\n  private static final Logger logger = LoggerFactory.getLogger(UCCatalogManagedClient.class);\n\n  public static final String UC_PROPERTY_NAMESPACE_PREFIX = \"io.unitycatalog.\";\n\n  /** Key for identifying Unity Catalog table ID. */\n  public static final String UC_TABLE_ID_KEY = UC_PROPERTY_NAMESPACE_PREFIX + \"tableId\";\n\n  protected final UCClient ucClient;\n\n  public UCCatalogManagedClient(UCClient ucClient) {\n    this.ucClient = Objects.requireNonNull(ucClient, \"ucClient is null\");\n  }\n\n  // TODO: [delta-io/delta#4817] loadSnapshot API that takes in a UC TableInfo object\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  /**\n   * Loads a Kernel {@link Snapshot}. If no version is specified, the latest version of the table is\n   * loaded.\n   *\n   * @param engine The Delta Kernel {@link Engine} to use for loading the table.\n   * @param ucTableId The Unity Catalog table ID, which is a unique identifier for the table in UC.\n   * @param tablePath The path to the Delta table in the underlying storage system.\n   * @param versionOpt The optional version to time-travel to when loading the table. This must be\n   *     mutually exclusive with timestampOpt.\n   * @param timestampOpt The optional timestamp to time-travel to when loading the table. This must\n   *     be mutually exclusive with versionOpt.\n   * @throws IllegalArgumentException if a negative version or timestamp is provided\n   * @throws IllegalArgumentException if both versionOpt and timestampOpt are defined\n   */\n  public Snapshot loadSnapshot(\n      Engine engine,\n      String ucTableId,\n      String tablePath,\n      Optional<Long> versionOpt,\n      Optional<Long> timestampOpt) {\n    Objects.requireNonNull(engine, \"engine is null\");\n    Objects.requireNonNull(ucTableId, \"ucTableId is null\");\n    Objects.requireNonNull(tablePath, \"tablePath is null\");\n    Objects.requireNonNull(versionOpt, \"versionOpt is null\");\n    Objects.requireNonNull(timestampOpt, \"timestampOpt is null\");\n    versionOpt.ifPresent(version -> checkArgument(version >= 0, \"version must be non-negative\"));\n    checkArgument(\n        !timestampOpt.isPresent() || !versionOpt.isPresent(),\n        \"cannot provide both timestamp and version\");\n\n    logger.info(\n        \"[{}] Loading Snapshot at {}\",\n        ucTableId,\n        getVersionOrTimestampString(versionOpt, timestampOpt));\n\n    final UcLoadSnapshotTelemetry telemetry =\n        new UcLoadSnapshotTelemetry(ucTableId, tablePath, versionOpt, timestampOpt);\n\n    final UcLoadSnapshotTelemetry.MetricsCollector metricsCollector =\n        telemetry.getMetricsCollector();\n\n    try {\n      final Snapshot result =\n          metricsCollector.totalSnapshotLoadTimer.timeChecked(\n              () -> {\n                final GetCommitsResponse response =\n                    metricsCollector.getCommitsTimer.timeChecked(\n                        () -> getRatifiedCommitsFromUC(ucTableId, tablePath, versionOpt));\n\n                metricsCollector.setNumCatalogCommits(response.getCommits().size());\n\n                final long maxUcTableVersion = response.getLatestTableVersion();\n\n                versionOpt.ifPresent(\n                    version ->\n                        validateTimeTravelVersionNotPastMax(ucTableId, version, maxUcTableVersion));\n\n                final List<ParsedLogData> logData =\n                    getSortedKernelParsedDeltaDataFromRatifiedCommits(\n                        ucTableId, response.getCommits());\n\n                return metricsCollector.kernelSnapshotBuildTimer.timeChecked(\n                    () -> {\n                      SnapshotBuilder snapshotBuilder = TableManager.loadSnapshot(tablePath);\n\n                      if (versionOpt.isPresent()) {\n                        snapshotBuilder = snapshotBuilder.atVersion(versionOpt.get());\n                      }\n\n                      if (timestampOpt.isPresent()) {\n                        // If timestampOpt is present, we know versionOpt is not present. This means\n                        // logData was not requested with an endVersion and thus it can be re-used\n                        // to load the latest snapshot\n                        Snapshot latestSnapshot =\n                            metricsCollector.loadLatestSnapshotForTimestampTimeTravelTimer\n                                .timeChecked(\n                                    () ->\n                                        loadLatestSnapshotForTimestampResolution(\n                                            engine,\n                                            ucTableId,\n                                            tablePath,\n                                            logData,\n                                            maxUcTableVersion));\n                        snapshotBuilder =\n                            snapshotBuilder.atTimestamp(timestampOpt.get(), latestSnapshot);\n                      }\n\n                      Snapshot snapshot =\n                          snapshotBuilder\n                              .withCommitter(createUCCommitter(ucClient, ucTableId, tablePath))\n                              .withLogData(logData)\n                              .withMaxCatalogVersion(maxUcTableVersion)\n                              .build(engine);\n                      metricsCollector.setResolvedSnapshotVersion(snapshot.getVersion());\n                      return snapshot;\n                    });\n              });\n\n      final UcLoadSnapshotTelemetry.Report successReport = telemetry.createSuccessReport();\n      engine.getMetricsReporters().forEach(r -> r.report(successReport));\n      return result;\n    } catch (Exception e) {\n      final UcLoadSnapshotTelemetry.Report failureReport = telemetry.createFailureReport(e);\n      engine.getMetricsReporters().forEach(r -> r.report(failureReport));\n      throw e;\n    }\n  }\n\n  /**\n   * Builds a create table transaction for a Unity Catalog managed Delta table.\n   *\n   * <p>Configures the transaction with a {@link UCCatalogManagedCommitter} and required table\n   * properties for catalog-managed table enablement.\n   *\n   * <p>This assumes the table is being created in a staging location as per UC semantics. Once this\n   * transaction is built and committed, creating 000.json, you must call {@code\n   * TablesApi::createTable} to inform Unity Catalog of the successful table creation.\n   *\n   * @param ucTableId The Unity Catalog table ID.\n   * @param tablePath The staging path to the Delta table.\n   * @param schema The table schema.\n   * @param engineInfo Information about the creating engine.\n   * @return A {@link CreateTableTransactionBuilder} configured for UC managed tables.\n   */\n  public CreateTableTransactionBuilder buildCreateTableTransaction(\n      String ucTableId, String tablePath, StructType schema, String engineInfo) {\n    Objects.requireNonNull(ucTableId, \"ucTableId is null\");\n    Objects.requireNonNull(tablePath, \"tablePath is null\");\n    Objects.requireNonNull(schema, \"schema is null\");\n    Objects.requireNonNull(engineInfo, \"engineInfo is null\");\n\n    return TableManager.buildCreateTableTransaction(tablePath, schema, engineInfo)\n        .withCommitter(createUCCommitter(ucClient, ucTableId, tablePath))\n        .withTableProperties(getRequiredTablePropertiesForCreate(ucTableId));\n  }\n\n  /**\n   * Loads a Kernel {@link CommitRange} for the provided boundaries. If no end boundary is provided,\n   * defaults to the latest version.\n   *\n   * <p>A start boundary is required and must be specified using either {@code startVersionOpt} or\n   * {@code startTimestampOpt}. These parameters are mutually exclusive and at least one must be\n   * provided.\n   *\n   * @param engine The Delta Kernel {@link Engine} to use for loading the table.\n   * @param ucTableId The Unity Catalog table ID, which is a unique identifier for the table in UC.\n   * @param tablePath The path to the Delta table in the underlying storage system.\n   * @param startVersionOpt The optional start version boundary. This must be mutually exclusive\n   *     with startTimestampOpt. Either this or startTimestampOpt must be provided.\n   * @param startTimestampOpt The optional start timestamp boundary. This must be mutually exclusive\n   *     with startVersionOpt. Either this or startVersionOpt must be provided.\n   * @param endVersionOpt The optional end version boundary. This must be mutually exclusive with\n   *     endTimestampOpt.\n   * @param endTimestampOpt The optional end timestamp boundary. This must be mutually exclusive\n   *     with endVersionOpt.\n   * @throws IllegalArgumentException if neither startVersionOpt nor startTimestampOpt is provided\n   * @throws IllegalArgumentException if both startVersionOpt and startTimestampOpt are defined\n   * @throws IllegalArgumentException if both endVersionOpt and endTimestampOpt are defined\n   * @throws IllegalArgumentException if either startVersionOpt or endVersionOpt is provided and is\n   *     greater than the latest ratified version from UC\n   */\n  public CommitRange loadCommitRange(\n      Engine engine,\n      String ucTableId,\n      String tablePath,\n      Optional<Long> startVersionOpt,\n      Optional<Long> startTimestampOpt,\n      Optional<Long> endVersionOpt,\n      Optional<Long> endTimestampOpt) {\n    Objects.requireNonNull(engine, \"engine is null\");\n    Objects.requireNonNull(ucTableId, \"ucTableId is null\");\n    Objects.requireNonNull(tablePath, \"tablePath is null\");\n    Objects.requireNonNull(startVersionOpt, \"startVersionOpt is null\");\n    Objects.requireNonNull(startTimestampOpt, \"startTimestampOpt is null\");\n    Objects.requireNonNull(endVersionOpt, \"endVersionOpt is null\");\n    Objects.requireNonNull(endTimestampOpt, \"endTimestampOpt is null\");\n    checkArgument(\n        !startVersionOpt.isPresent() || !startTimestampOpt.isPresent(),\n        \"Cannot provide both a start timestamp and start version\");\n    checkArgument(\n        !endVersionOpt.isPresent() || !endTimestampOpt.isPresent(),\n        \"Cannot provide both an end timestamp and start version\");\n    checkArgument(\n        startVersionOpt.isPresent() || startTimestampOpt.isPresent(),\n        \"Must provide either a start timestamp or start version\");\n    if (startVersionOpt.isPresent() && endVersionOpt.isPresent()) {\n      checkArgument(\n          startVersionOpt.get() <= endVersionOpt.get(),\n          \"Cannot provide a start version greater than the end version\");\n    }\n    if (startTimestampOpt.isPresent() && endTimestampOpt.isPresent()) {\n      checkArgument(\n          startTimestampOpt.get() <= endTimestampOpt.get(),\n          \"Cannot provide a start timestamp greater than the end timestamp\");\n    }\n\n    logger.info(\n        \"[{}] Loading CommitRange for {}\",\n        ucTableId,\n        getCommitRangeBoundariesString(\n            startVersionOpt, startTimestampOpt, endVersionOpt, endTimestampOpt));\n    // If we have a timestamp-based boundary we need to build the latest snapshot, don't provide\n    // an endVersion\n    Optional<Long> endVersionOptForCommitQuery =\n        endVersionOpt.filter(v -> !startTimestampOpt.isPresent());\n    final GetCommitsResponse response =\n        getRatifiedCommitsFromUC(ucTableId, tablePath, endVersionOptForCommitQuery);\n    final long ucTableVersion = response.getLatestTableVersion();\n    validateVersionBoundariesExist(ucTableId, startVersionOpt, endVersionOpt, ucTableVersion);\n    final List<ParsedLogData> logData =\n        getSortedKernelParsedDeltaDataFromRatifiedCommits(ucTableId, response.getCommits());\n    final Lazy<Snapshot> latestSnapshot =\n        new Lazy<>(\n            () ->\n                loadLatestSnapshotForTimestampResolution(\n                    engine, ucTableId, tablePath, logData, ucTableVersion));\n\n    return timeUncheckedOperation(\n        logger,\n        \"TableManager.loadCommitRange\",\n        ucTableId,\n        () -> {\n          // Determine the start boundary (required - validated above)\n          CommitRangeBuilder.CommitBoundary startBoundary;\n          if (startVersionOpt.isPresent()) {\n            startBoundary = CommitRangeBuilder.CommitBoundary.atVersion(startVersionOpt.get());\n          } else {\n            // startTimestampOpt must be present due to validation above\n            startBoundary =\n                CommitRangeBuilder.CommitBoundary.atTimestamp(\n                    startTimestampOpt.get(), latestSnapshot.get());\n          }\n\n          CommitRangeBuilder commitRangeBuilder =\n              TableManager.loadCommitRange(tablePath, startBoundary)\n                  .withMaxCatalogVersion(ucTableVersion);\n\n          if (endVersionOpt.isPresent()) {\n            commitRangeBuilder =\n                commitRangeBuilder.withEndBoundary(\n                    CommitRangeBuilder.CommitBoundary.atVersion(endVersionOpt.get()));\n          }\n          if (endTimestampOpt.isPresent()) {\n            commitRangeBuilder =\n                commitRangeBuilder.withEndBoundary(\n                    CommitRangeBuilder.CommitBoundary.atTimestamp(\n                        endTimestampOpt.get(), latestSnapshot.get()));\n          }\n\n          return commitRangeBuilder.withLogData(logData).build(engine);\n        });\n  }\n\n  /////////////////////////////////////////\n  // Protected Methods for Extensibility //\n  /////////////////////////////////////////\n\n  /**\n   * Creates a UC committer instance for the specified table.\n   *\n   * <p>This method allows subclasses to provide custom committer implementations for specialized\n   * use cases.\n   */\n  protected Committer createUCCommitter(UCClient ucClient, String ucTableId, String tablePath) {\n    return new UCCatalogManagedCommitter(ucClient, ucTableId, tablePath);\n  }\n\n  ////////////////////\n  // Helper Methods //\n  ////////////////////\n\n  private String getVersionString(Optional<Long> versionOpt) {\n    return versionOpt.map(String::valueOf).orElse(\"latest\");\n  }\n\n  private String getVersionOrTimestampString(\n      Optional<Long> versionOpt, Optional<Long> timestampOpt) {\n    if (versionOpt.isPresent()) {\n      return \"version=\" + versionOpt.get();\n    } else if (timestampOpt.isPresent()) {\n      return \"timestamp=\" + timestampOpt.get();\n    } else {\n      return \"latest\";\n    }\n  }\n\n  private String getCommitRangeBoundariesString(\n      Optional<Long> startVersionOpt,\n      Optional<Long> startTimestampOpt,\n      Optional<Long> endVersionOpt,\n      Optional<Long> endTimestampOpt) {\n    String startBound;\n    if (startVersionOpt.isPresent()) {\n      startBound = startVersionOpt.get() + \"(version)\";\n    } else if (startTimestampOpt.isPresent()) {\n      startBound = startTimestampOpt.get() + \"(timestamp)\";\n    } else {\n      startBound = \"0(default)\";\n    }\n    String endBound;\n    if (endVersionOpt.isPresent()) {\n      endBound = endVersionOpt.get() + \"(version)\";\n    } else if (endTimestampOpt.isPresent()) {\n      endBound = endTimestampOpt.get() + \"(timestamp)\";\n    } else {\n      endBound = \"latestVersion(default)\";\n    }\n    return String.format(\"startBoundary=%s and endBoundary=%s\", startBound, endBound);\n  }\n\n  private GetCommitsResponse getRatifiedCommitsFromUC(\n      String ucTableId, String tablePath, Optional<Long> versionOpt) {\n    logger.info(\n        \"[{}] Invoking the UCClient to get ratified commits at version {}\",\n        ucTableId,\n        getVersionString(versionOpt));\n\n    // TODO: We can remove timeUncheckedOperation when the commitRange code integrates with metrics\n    final GetCommitsResponse response =\n        timeUncheckedOperation(\n            logger,\n            \"UCClient.getCommits\",\n            ucTableId,\n            () -> {\n              try {\n                return ucClient.getCommits(\n                    ucTableId,\n                    new Path(tablePath).toUri(),\n                    Optional.empty() /* startVersion */,\n                    versionOpt /* endVersion */);\n              } catch (IOException ex) {\n                throw new UncheckedIOException(ex);\n              } catch (UCCommitCoordinatorException ex) {\n                throw new RuntimeException(ex);\n              }\n            });\n\n    logger.info(\n        \"[{}] Number of ratified commits: {}, Max ratified version in UC: {}\",\n        ucTableId,\n        response.getCommits().size(),\n        response.getLatestTableVersion());\n\n    return response;\n  }\n\n  private void validateTimeTravelVersionNotPastMax(\n      String ucTableId, long tableVersionToLoad, long maxRatifiedVersion) {\n    if (tableVersionToLoad > maxRatifiedVersion) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"[%s] Cannot load table version %s as the latest version ratified by UC is %s\",\n              ucTableId, tableVersionToLoad, maxRatifiedVersion));\n    }\n  }\n\n  private void validateVersionBoundariesExist(\n      String ucTableId,\n      Optional<Long> startVersion,\n      Optional<Long> endVersion,\n      long maxRatifiedVersion) {\n    BiConsumer<Long, String> validateVersion =\n        (version, type) -> {\n          if (version > maxRatifiedVersion) {\n            throw new IllegalArgumentException(\n                String.format(\n                    \"[%s] Cannot load commit range with %s version %d as the latest version \"\n                        + \"ratified by UC is %d\",\n                    ucTableId, type, version, maxRatifiedVersion));\n          }\n        };\n    startVersion.ifPresent(v -> validateVersion.accept(v, \"start\"));\n    endVersion.ifPresent(v -> validateVersion.accept(v, \"end\"));\n  }\n\n  private Map<String, String> getRequiredTablePropertiesForCreate(String ucTableId) {\n    final Map<String, String> requiredProperties = new HashMap<>();\n\n    requiredProperties.put(\n        TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey(),\n        TableFeatures.SET_TABLE_FEATURE_SUPPORTED_VALUE);\n    requiredProperties.put(\n        TableFeatures.VACUUM_PROTOCOL_CHECK_RW_FEATURE.getTableFeatureSupportKey(),\n        TableFeatures.SET_TABLE_FEATURE_SUPPORTED_VALUE);\n    requiredProperties.put(UC_TABLE_ID_KEY, ucTableId);\n\n    return requiredProperties;\n  }\n\n  /**\n   * Converts a list of ratified commits into a sorted list of {@link ParsedLogData} for use in\n   * loading a Delta table.\n   */\n  @VisibleForTesting\n  static List<ParsedLogData> getSortedKernelParsedDeltaDataFromRatifiedCommits(\n      String ucTableId, List<Commit> commits) {\n    final List<ParsedLogData> result =\n        timeUncheckedOperation(\n            logger,\n            \"Sort and convert UC ratified commits into Kernel ParsedLogData\",\n            ucTableId,\n            () ->\n                commits.stream()\n                    .sorted(Comparator.comparingLong(Commit::getVersion))\n                    .map(\n                        commit ->\n                            ParsedCatalogCommitData.forFileStatus(\n                                hadoopFileStatusToKernelFileStatus(commit.getFileStatus())))\n                    .collect(Collectors.toList()));\n\n    logger.debug(\"[{}] Created ParsedLogData from ratified commits: {}\", ucTableId, result);\n\n    return result;\n  }\n\n  private static io.delta.kernel.utils.FileStatus hadoopFileStatusToKernelFileStatus(\n      org.apache.hadoop.fs.FileStatus hadoopFS) {\n    return io.delta.kernel.utils.FileStatus.of(\n        hadoopFS.getPath().toString(), hadoopFS.getLen(), hadoopFS.getModificationTime());\n  }\n\n  /**\n   * Helper method to load the latest snapshot and time the operation. This is used to load the\n   * latest snapshot for timestamp resolution queries. Reuses existing logData that has already been\n   * queried from the catalog (it is required that this includes the latest commits from the catalog\n   * and were not queried with an endVersion).\n   */\n  private Snapshot loadLatestSnapshotForTimestampResolution(\n      Engine engine,\n      String ucTableId,\n      String tablePath,\n      List<ParsedLogData> logData,\n      long ucTableVersion) {\n    // TODO: We can remove timeUncheckedOperation when the commitRange code integrates with metrics\n    return timeUncheckedOperation(\n        logger,\n        \"TableManager.loadSnapshot at latest for time-travel query\",\n        ucTableId,\n        () ->\n            TableManager.loadSnapshot(tablePath)\n                .withCommitter(new UCCatalogManagedCommitter(ucClient, ucTableId, tablePath))\n                .withLogData(logData)\n                .withMaxCatalogVersion(ucTableVersion)\n                .build(engine));\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/UCCatalogManagedCommitter.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\nimport static io.delta.kernel.unitycatalog.UCCatalogManagedClient.UC_TABLE_ID_KEY;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.commit.*;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.annotation.VisibleForTesting;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.internal.files.ParsedPublishedDeltaData;\nimport io.delta.kernel.internal.util.FileNames;\nimport io.delta.kernel.unitycatalog.adapters.MetadataAdapter;\nimport io.delta.kernel.unitycatalog.adapters.ProtocolAdapter;\nimport io.delta.kernel.unitycatalog.adapters.UniformAdapter;\nimport io.delta.kernel.unitycatalog.metrics.UcCommitTelemetry;\nimport io.delta.kernel.unitycatalog.metrics.UcPublishTelemetry;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.kernel.utils.FileStatus;\nimport io.delta.storage.commit.Commit;\nimport io.delta.storage.commit.uccommitcoordinator.UCClient;\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorException;\nimport io.delta.storage.commit.uniform.UniformMetadata;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.hadoop.fs.Path;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * An implementation of {@link Committer} that handles commits to Delta tables managed by Unity\n * Catalog. That is, these Delta tables must have the catalogManaged table feature supported.\n */\npublic class UCCatalogManagedCommitter implements Committer, CatalogCommitter {\n  private static final Logger logger = LoggerFactory.getLogger(UCCatalogManagedCommitter.class);\n\n  protected final UCClient ucClient;\n  protected final String ucTableId;\n  protected final Path tablePath;\n\n  /**\n   * Creates a new UCCatalogManagedCommitter for the specified Unity Catalog-managed Delta table.\n   *\n   * @param ucClient the Unity Catalog client to use for commit operations\n   * @param ucTableId the unique Unity Catalog table identifier\n   * @param tablePath the path to the Delta table in the underlying storage system\n   */\n  public UCCatalogManagedCommitter(UCClient ucClient, String ucTableId, String tablePath) {\n    this.ucClient = requireNonNull(ucClient, \"ucClient is null\");\n    this.ucTableId = requireNonNull(ucTableId, \"ucTableId is null\");\n    this.tablePath = new Path(requireNonNull(tablePath, \"tablePath is null\"));\n  }\n\n  /////////////////\n  // Public APIs //\n  /////////////////\n\n  @Override\n  public CommitResponse commit(\n      Engine engine, CloseableIterator<Row> finalizedActions, CommitMetadata commitMetadata)\n      throws CommitFailedException {\n    requireNonNull(engine, \"engine is null\");\n    requireNonNull(finalizedActions, \"finalizedActions is null\");\n    requireNonNull(commitMetadata, \"commitMetadata is null\");\n    validateLogPathBelongsToThisUcTable(commitMetadata);\n\n    final UcCommitTelemetry telemetry =\n        new UcCommitTelemetry(ucTableId, tablePath.toString(), commitMetadata);\n    final UcCommitTelemetry.MetricsCollector metricsCollector = telemetry.getMetricsCollector();\n\n    try {\n      final CommitResponse response =\n          metricsCollector.totalCommitTimer.timeChecked(\n              () -> {\n                final CommitMetadata.CommitType commitType = commitMetadata.getCommitType();\n\n                if (commitType == CommitMetadata.CommitType.CATALOG_CREATE) {\n                  return createImpl(engine, finalizedActions, commitMetadata, metricsCollector);\n                }\n                if (commitType == CommitMetadata.CommitType.CATALOG_WRITE) {\n                  return writeImpl(engine, finalizedActions, commitMetadata, metricsCollector);\n                }\n\n                throw new UnsupportedOperationException(\"Unsupported commit type: \" + commitType);\n              });\n\n      final UcCommitTelemetry.Report successfulReport = telemetry.createSuccessReport();\n      engine.getMetricsReporters().forEach(r -> r.report(successfulReport));\n      return response;\n    } catch (CommitFailedException | RuntimeException e) {\n      final UcCommitTelemetry.Report failureReport = telemetry.createFailureReport(e);\n      engine.getMetricsReporters().forEach(r -> r.report(failureReport));\n      throw e;\n    }\n  }\n\n  @Override\n  public void publish(Engine engine, PublishMetadata publishMetadata)\n      throws PublishFailedException {\n    requireNonNull(engine, \"engine is null\");\n    requireNonNull(publishMetadata, \"publishMetadata is null\");\n\n    final List<ParsedCatalogCommitData> catalogCommits =\n        publishMetadata.getAscendingCatalogCommits();\n\n    if (catalogCommits.isEmpty()) {\n      return;\n    }\n\n    final String logPath = publishMetadata.getLogPath();\n    final long snapshotVersion = publishMetadata.getSnapshotVersion();\n\n    logger.info(\n        \"[{}] Publishing {} catalog commits up to version {}\",\n        ucTableId,\n        catalogCommits.size(),\n        snapshotVersion);\n\n    final UcPublishTelemetry telemetry =\n        new UcPublishTelemetry(\n            ucTableId, tablePath.toString(), snapshotVersion, catalogCommits.size());\n    final UcPublishTelemetry.MetricsCollector metricsCollector = telemetry.getMetricsCollector();\n\n    try {\n      metricsCollector.totalPublishTimer.time(\n          () -> {\n            for (ParsedCatalogCommitData catalogCommit : catalogCommits) {\n              publishSingleCommit(engine, catalogCommit, logPath, metricsCollector);\n            }\n            return null;\n          });\n\n      logger.info(\n          \"[{}] Successfully published all catalog commits up to version {}. {} were published by \"\n              + \"this process, {} were already published by another process.\",\n          ucTableId,\n          snapshotVersion,\n          metricsCollector.getCommitsPublished(),\n          metricsCollector.getCommitsAlreadyPublished());\n\n      final UcPublishTelemetry.Report successfulReport = telemetry.createSuccessReport();\n      engine.getMetricsReporters().forEach(r -> r.report(successfulReport));\n    } catch (RuntimeException e) {\n      final UcPublishTelemetry.Report failureReport = telemetry.createFailureReport(e);\n      engine.getMetricsReporters().forEach(r -> r.report(failureReport));\n      throw e;\n    }\n  }\n\n  @Override\n  public Map<String, String> getRequiredTableProperties() {\n    return Collections.singletonMap(UC_TABLE_ID_KEY, ucTableId);\n  }\n\n  ///////////////////////////\n  // Commit helper methods //\n  ///////////////////////////\n\n  /**\n   * Handles CATALOG_CREATE by writing the published delta file for version 0.\n   *\n   * <p>Note that this assumes that the table is being created within a staging location, and that\n   * the Connector will post-commit inform UC of this 000.json file.\n   */\n  // TODO: [delta-io/delta#5118] If UC changes CREATE semantics, update logic here.\n  private CommitResponse createImpl(\n      Engine engine,\n      CloseableIterator<Row> finalizedActions,\n      CommitMetadata commitMetadata,\n      UcCommitTelemetry.MetricsCollector metricsCollector)\n      throws CommitFailedException {\n    checkArgument(\n        commitMetadata.getVersion() == 0,\n        \"Expected version 0, but got %s\",\n        commitMetadata.getVersion());\n\n    final FileStatus kernelPublishedDeltaFileStatus =\n        writeDeltaFile(\n            engine, finalizedActions, commitMetadata.getPublishedDeltaFilePath(), metricsCollector);\n\n    return new CommitResponse(\n        ParsedPublishedDeltaData.forFileStatus(kernelPublishedDeltaFileStatus));\n  }\n\n  /**\n   * Handles CATALOG_WRITE by writing the staged commit file and then committing (e.g. REST or RPC\n   * call) to UC server.\n   */\n  private CommitResponse writeImpl(\n      Engine engine,\n      CloseableIterator<Row> finalizedActions,\n      CommitMetadata commitMetadata,\n      UcCommitTelemetry.MetricsCollector commitMetricsCollector)\n      throws CommitFailedException {\n    checkArgument(\n        commitMetadata.getVersion() > 0, \"Can only write staged commit files for versions > 0\");\n\n    final FileStatus kernelStagedCommitFileStatus =\n        writeDeltaFile(\n            engine,\n            finalizedActions,\n            commitMetadata.generateNewStagedCommitFilePath(),\n            commitMetricsCollector);\n\n    commitToUC(commitMetadata, kernelStagedCommitFileStatus, commitMetricsCollector);\n\n    return new CommitResponse(ParsedCatalogCommitData.forFileStatus(kernelStagedCommitFileStatus));\n  }\n\n  ////////////////////////////\n  // Publish helper methods //\n  ////////////////////////////\n\n  private void publishSingleCommit(\n      Engine engine,\n      ParsedCatalogCommitData catalogCommit,\n      String logPath,\n      UcPublishTelemetry.MetricsCollector publishMetricsCollector)\n      throws PublishFailedException {\n    final long commitVersion = catalogCommit.getVersion();\n\n    if (catalogCommit.isInline()) {\n      throw new UnsupportedOperationException(\n          \"Publishing inline catalog commits is not yet supported\");\n    }\n\n    final String sourcePath = catalogCommit.getFileStatus().getPath();\n    final String targetPath = FileNames.deltaFile(logPath, commitVersion);\n\n    try {\n      logger.info(\"[{}] Publishing catalog commit: {} -> {}\", ucTableId, sourcePath, targetPath);\n\n      // Copy the staged commit file to the published delta file location. We use overwrite=false to\n      // ensure PUT-if-absent semantics, since UC catalogManaged tables expect immutability of\n      // published delta files (e.g. never want the e-tag to change).\n      engine\n          .getFileSystemClient()\n          .copyFileAtomically(sourcePath, targetPath, false /* overwrite */);\n\n      logger.info(\"[{}] Successfully published version {}\", ucTableId, commitVersion);\n      publishMetricsCollector.incrementCommitsPublished();\n    } catch (java.nio.file.FileAlreadyExistsException e) {\n      // File already exists - this is okay, it means this version was already published\n      logger.info(\"[{}] Version {} already published\", ucTableId, commitVersion);\n      publishMetricsCollector.incrementCommitsAlreadyPublished();\n    } catch (Exception ex) {\n      throw new PublishFailedException(\n          String.format(\n              \"Failed to publish version %d from %s to %s: %s\",\n              commitVersion, sourcePath, targetPath, ex.getMessage()),\n          ex);\n    }\n  }\n\n  /////////////////////////////////////////\n  // Protected Methods for Extensibility //\n  /////////////////////////////////////////\n\n  /**\n   * Generates the metadata payload for UC commit operations.\n   *\n   * <p>This method allows subclasses to customize or enhance metadata before sending to Unity\n   * Catalog.\n   */\n  protected Optional<Metadata> generateMetadataPayloadOpt(CommitMetadata commitMetadata) {\n    return commitMetadata.getNewMetadataOpt();\n  }\n\n  ////////////////////\n  // Helper methods //\n  ////////////////////\n\n  private String normalize(Path path) {\n    return path.toUri().normalize().toString();\n  }\n\n  private void validateLogPathBelongsToThisUcTable(CommitMetadata cm) {\n    final String expectedDeltaLogPathNormalized = normalize(new Path(tablePath, \"_delta_log\"));\n    final String providedDeltaLogPathNormalized = normalize(new Path(cm.getDeltaLogDirPath()));\n    checkArgument(\n        expectedDeltaLogPathNormalized.equals(providedDeltaLogPathNormalized),\n        \"Delta log path '%s' does not match expected '%s'\",\n        expectedDeltaLogPathNormalized,\n        providedDeltaLogPathNormalized);\n  }\n\n  /**\n   * Writes either a published delta file (for CREATE) or a staged commit file (for WRITE).\n   *\n   * <p>For both cases, writes using {@code overwrite=true} since:\n   *\n   * <ul>\n   *   <li>For CREATE, we can assume we are the only writer writing to the staging location\n   *   <li>For WRITE, we are writing to a UUID commit file\n   * </ul>\n   */\n  private FileStatus writeDeltaFile(\n      Engine engine,\n      CloseableIterator<Row> finalizedActions,\n      String filePath,\n      UcCommitTelemetry.MetricsCollector metricsCollector)\n      throws CommitFailedException {\n    return metricsCollector.writeCommitFileTimer.timeChecked(\n        () -> {\n          try {\n            logger.info(\"[{}] Writing file: {}\", ucTableId, filePath);\n\n            // Note: the engine is responsible for closing the actions iterator once it has been\n            //       fully consumed.\n            engine\n                .getJsonHandler()\n                .writeJsonFileAtomically(filePath, finalizedActions, true /* overwrite */);\n\n            return engine.getFileSystemClient().getFileStatus(filePath);\n          } catch (IOException ex) {\n            // Note that as per the JsonHandler::writeJsonFileAtomically API contract with\n            // overwrite=true, FileAlreadyExistsException should not be possible here.\n\n            throw new CommitFailedException(\n                true /* retryable */,\n                false /* conflict */,\n                \"Failed to write delta file due to: \" + ex.getMessage(),\n                ex);\n          }\n        });\n  }\n\n  private void commitToUC(\n      CommitMetadata commitMetadata,\n      FileStatus kernelStagedCommitFileStatus,\n      UcCommitTelemetry.MetricsCollector metricsCollector)\n      throws CommitFailedException {\n    metricsCollector.commitToUcServerTimer.timeChecked(\n        () -> {\n          logger.info(\n              \"[{}] Committing staged commit file to UC: {}\",\n              ucTableId,\n              kernelStagedCommitFileStatus.getPath());\n\n          final CommitMetadata.CommitType commitType = commitMetadata.getCommitType();\n\n          // commitToUc is only for normal catalog WRITES, not for CREATE, or UPGRADE, or\n          // DOWNGRADE, or anything filesystem related.\n          checkState(\n              commitType == CommitMetadata.CommitType.CATALOG_WRITE,\n              \"Only supported commit type is CATALOG_WRITE, but got: \" + commitType);\n\n          // Extract and validate Uniform metadata if present\n          Optional<UniformMetadata> uniformMetadataOpt =\n              UniformAdapter.fromCommitterProperties(commitMetadata.getCommitterProperties().get());\n\n          // Validate that convertedDeltaVersion matches the current commit version\n          uniformMetadataOpt.ifPresent(\n              uniformMetadata -> {\n                uniformMetadata\n                    .getIcebergMetadata()\n                    .ifPresent(\n                        icebergMetadata -> {\n                          long convertedVersion = icebergMetadata.getConvertedDeltaVersion();\n                          long commitVersion = commitMetadata.getVersion();\n                          checkState(\n                              convertedVersion == commitVersion,\n                              String.format(\n                                  \"Uniform convertedDeltaVersion (%d) must match \"\n                                      + \"commit version (%d)\",\n                                  convertedVersion, commitVersion));\n                        });\n              });\n\n          try {\n            ucClient.commit(\n                ucTableId,\n                tablePath.toUri(),\n                Optional.of(getUcCommitPayload(commitMetadata, kernelStagedCommitFileStatus)),\n                commitMetadata.getMaxKnownPublishedDeltaVersion(),\n                false /* isDisown */,\n                generateMetadataPayloadOpt(commitMetadata).map(MetadataAdapter::new),\n                commitMetadata.getNewProtocolOpt().map(ProtocolAdapter::new),\n                uniformMetadataOpt);\n            return null;\n          } catch (io.delta.storage.commit.CommitFailedException cfe) {\n            throw storageCFEtoKernelCFE(cfe);\n          } catch (IOException ex) {\n            throw new CommitFailedException(\n                true /* retryable */, false /* conflict */, ex.getMessage(), ex);\n          } catch (UCCommitCoordinatorException ucce) {\n            // For now, this catches all UC exceptions such as:\n            // - CommitLimitReachedException -> TODO: publish in this case\n            // - InvalidTargetTableException\n            // - UpgradeNotAllowedException\n            // We can add specific catch statements for these exceptions if needed in the future.\n            throw new CommitFailedException(\n                false /* retryable */, false /* conflict */, ucce.getMessage(), ucce);\n          }\n        });\n  }\n\n  private Commit getUcCommitPayload(\n      CommitMetadata commitMetadata, FileStatus kernelStagedCommitFileStatus) {\n    return new Commit(\n        commitMetadata.getVersion(),\n        kernelFileStatusToHadoopFileStatus(kernelStagedCommitFileStatus),\n        // commitMetadata validates that the ICT is present if writing to a catalogManaged table\n        commitMetadata.getCommitInfo().getInCommitTimestamp().get());\n  }\n\n  @VisibleForTesting\n  public static org.apache.hadoop.fs.FileStatus kernelFileStatusToHadoopFileStatus(\n      io.delta.kernel.utils.FileStatus kernelFileStatus) {\n    return new org.apache.hadoop.fs.FileStatus(\n        kernelFileStatus.getSize() /* length */,\n        false /* isDirectory */,\n        1 /* blockReplication */,\n        128 * 1024 * 1024 /* blockSize (128MB) */,\n        kernelFileStatus.getModificationTime() /* modificationTime */,\n        kernelFileStatus.getModificationTime() /* accessTime */,\n        org.apache.hadoop.fs.permission.FsPermission.getFileDefault() /* permission */,\n        \"unknown\" /* owner */,\n        \"unknown\" /* group */,\n        new org.apache.hadoop.fs.Path(kernelFileStatus.getPath()) /* path */);\n  }\n\n  private static CommitFailedException storageCFEtoKernelCFE(\n      io.delta.storage.commit.CommitFailedException storageCFE) {\n    return new CommitFailedException(\n        storageCFE.getRetryable(),\n        storageCFE.getConflict(),\n        storageCFE.getMessage(),\n        storageCFE.getCause());\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/UnityCatalogUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog;\n\nimport static io.delta.kernel.commit.CatalogCommitterUtils.*;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.Tuple2;\nimport io.delta.kernel.types.DataType;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class UnityCatalogUtils {\n  private UnityCatalogUtils() {}\n\n  private static final String UC_PROP_CLUSTERING_COLUMNS = \"clusteringColumns\";\n  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n\n  /**\n   * Extract all properties that should be sent to Unity Catalog when creating a table (version 0).\n   *\n   * <p>This method extracts:\n   *\n   * <ul>\n   *   <li>All table properties from the metadata configuration\n   *   <li>Protocol-derived properties (e.g., delta.minReaderVersion=3, delta.feature.XXX=supported)\n   *   <li>UC-specific properties (delta.lastUpdateVersion, delta.lastCommitTimestamp)\n   *   <li>Clustering properties if a clustering domain metadata is present\n   * </ul>\n   *\n   * @param engine the engine to use for I/O operations (to retrieve the commit timestamp)\n   * @param postCreateSnapshot the snapshot after version 0 has been written\n   * @return a map of properties to send to Unity Catalog\n   * @throws IllegalArgumentException if the snapshot is not version 0\n   */\n  public static Map<String, String> getPropertiesForCreate(\n      Engine engine, SnapshotImpl postCreateSnapshot) {\n    if (postCreateSnapshot.getVersion() != 0) {\n      throw new IllegalArgumentException(\n          String.format(\n              \"Expected a snapshot at version 0, but got a snapshot at version %d\",\n              postCreateSnapshot.getVersion()));\n    }\n\n    final Map<String, String> properties = new HashMap<>();\n\n    // Case 1: All table properties from metadata.configuration\n    properties.putAll(postCreateSnapshot.getTableProperties());\n\n    // Case 2: Protocol-derived properties\n    properties.putAll(extractProtocolProperties(postCreateSnapshot.getProtocol()));\n\n    // Case 3: UC-specific properties\n    properties.put(METASTORE_LAST_UPDATE_VERSION, String.valueOf(postCreateSnapshot.getVersion()));\n    properties.put(\n        METASTORE_LAST_COMMIT_TIMESTAMP, String.valueOf(postCreateSnapshot.getTimestamp(engine)));\n\n    // Case 4: Clustering properties if present\n    properties.putAll(extractClusteringProperties(postCreateSnapshot));\n\n    return properties;\n  }\n\n  /**\n   * Extract clustering properties from the snapshot.\n   *\n   * <p>Converts physical clustering columns to logical column names and serializes them as a JSON\n   * array of arrays for the \"clusteringColumns\" property.\n   *\n   * <p>Examples:\n   *\n   * <ul>\n   *   <li>Not clustered: returns empty map (no \"clusteringColumns\" property)\n   *   <li>Clustered with empty list: returns {\"clusteringColumns\": \"[]\"}\n   *   <li>Clustered with columns: physical column \"col-abcd-1234\" maps to nested logical column\n   *       \"address.city\" and is serialized as {\"clusteringColumns\": \"[[\"address\", \"city\"]]\"}\n   * </ul>\n   *\n   * @return clustering properties if present, otherwise empty map\n   */\n  private static Map<String, String> extractClusteringProperties(SnapshotImpl snapshot) {\n    return snapshot\n        .getPhysicalClusteringColumns()\n        .map(\n            physicalClusteringCols -> {\n              // Convert physical to logical column names\n              final List<List<String>> logicalClusteringCols =\n                  physicalClusteringCols.stream()\n                      .map(\n                          physicalCol -> {\n                            final Tuple2<Column, DataType> logicalColumnAndType =\n                                ColumnMapping.getLogicalColumnNameAndDataType(\n                                    snapshot.getSchema(), physicalCol);\n                            final Column logicalColumn = logicalColumnAndType._1;\n                            return Arrays.asList(logicalColumn.getNames());\n                          })\n                      .collect(Collectors.toList());\n\n              // Serialize to JSON\n              try {\n                final String clusteringColumnsJson =\n                    OBJECT_MAPPER.writeValueAsString(logicalClusteringCols);\n                return Map.of(UC_PROP_CLUSTERING_COLUMNS, clusteringColumnsJson);\n              } catch (JsonProcessingException ex) {\n                throw new RuntimeException(\"Failed to serialize clustering columns to JSON\", ex);\n              }\n            })\n        .orElse(Collections.emptyMap());\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/adapters/MetadataAdapter.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.adapters;\n\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport java.util.*;\n\n/**\n * Adapter from {@link io.delta.kernel.internal.actions.Metadata} to {@link\n * io.delta.storage.commit.actions.AbstractMetadata}.\n */\npublic class MetadataAdapter implements AbstractMetadata {\n\n  private final Metadata kernelMetadata;\n\n  public MetadataAdapter(Metadata kernelMetadata) {\n    this.kernelMetadata = Objects.requireNonNull(kernelMetadata, \"kernelMetadata is null\");\n  }\n\n  @Override\n  public String getId() {\n    return kernelMetadata.getId();\n  }\n\n  @Override\n  public String getName() {\n    return kernelMetadata.getName().orElse(null);\n  }\n\n  @Override\n  public String getDescription() {\n    return kernelMetadata.getDescription().orElse(null);\n  }\n\n  @Override\n  public String getProvider() {\n    return kernelMetadata.getFormat().getProvider();\n  }\n\n  @Override\n  public Map<String, String> getFormatOptions() {\n    return Collections.unmodifiableMap(kernelMetadata.getFormat().getOptions());\n  }\n\n  @Override\n  public String getSchemaString() {\n    return kernelMetadata.getSchemaString();\n  }\n\n  @Override\n  public List<String> getPartitionColumns() {\n    return Collections.unmodifiableList(\n        VectorUtils.toJavaList(kernelMetadata.getPartitionColumns()));\n  }\n\n  @Override\n  public Map<String, String> getConfiguration() {\n    return Collections.unmodifiableMap(kernelMetadata.getConfiguration());\n  }\n\n  @Override\n  public Long getCreatedTime() {\n    return kernelMetadata.getCreatedTime().orElse(null);\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/adapters/ProtocolAdapter.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.adapters;\n\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport java.util.Collections;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * Adapter from {@link io.delta.kernel.internal.actions.Protocol} to {@link\n * io.delta.storage.commit.actions.AbstractProtocol}.\n */\npublic class ProtocolAdapter implements AbstractProtocol {\n\n  private final Protocol kernelProtocol;\n\n  public ProtocolAdapter(Protocol kernelProtocol) {\n    this.kernelProtocol = Objects.requireNonNull(kernelProtocol, \"kernelProtocol is null\");\n  }\n\n  @Override\n  public int getMinReaderVersion() {\n    return kernelProtocol.getMinReaderVersion();\n  }\n\n  @Override\n  public int getMinWriterVersion() {\n    return kernelProtocol.getMinWriterVersion();\n  }\n\n  @Override\n  public Set<String> getReaderFeatures() {\n    return Collections.unmodifiableSet(kernelProtocol.getReaderFeatures());\n  }\n\n  @Override\n  public Set<String> getWriterFeatures() {\n    return Collections.unmodifiableSet(kernelProtocol.getWriterFeatures());\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/adapters/UniformAdapter.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.adapters;\n\nimport io.delta.storage.commit.uniform.IcebergMetadata;\nimport io.delta.storage.commit.uniform.UniformMetadata;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Adapter for Delta Uniform metadata to {@link UniformMetadata}.\n *\n * <p>This adapter extracts Uniform metadata (e.g., Iceberg) from committer properties and provides\n * it in the format expected by Unity Catalog.\n *\n * <p>The committer properties are provided by the connector which is responsible for computing the\n * Uniform metadata during write operations. The connector injects these properties into Kernel via\n * the CommitMetadata.withCommitterProperties, and Kernel propagates them to the\n * UCCatalogManagedCommitter, which then forwards them to Unity Catalog.\n */\npublic class UniformAdapter {\n  private static final Logger logger = LoggerFactory.getLogger(UniformAdapter.class);\n\n  // Keys for extracting Iceberg metadata from committer properties\n  public static final String ICEBERG_METADATA_LOCATION_KEY =\n      \"delta.uniform.iceberg.metadataLocation\";\n  public static final String ICEBERG_CONVERTED_DELTA_VERSION_KEY =\n      \"delta.uniform.iceberg.convertedDeltaVersion\";\n  public static final String ICEBERG_CONVERTED_DELTA_TIMESTAMP_KEY =\n      \"delta.uniform.iceberg.convertedDeltaTimestamp\";\n\n  private UniformAdapter() {\n    // Private constructor to prevent instantiation\n  }\n\n  /**\n   * Extracts Uniform metadata from committer properties.\n   *\n   * @param properties the committer properties map\n   * @return an Optional containing the UniformMetadata if all required fields are present,\n   *     Optional.empty() otherwise\n   */\n  public static Optional<UniformMetadata> fromCommitterProperties(Map<String, String> properties) {\n    if (properties == null || properties.isEmpty()) {\n      return Optional.empty();\n    }\n\n    String metadataLocation = properties.get(ICEBERG_METADATA_LOCATION_KEY);\n    String convertedVersionStr = properties.get(ICEBERG_CONVERTED_DELTA_VERSION_KEY);\n    String convertedTimestamp = properties.get(ICEBERG_CONVERTED_DELTA_TIMESTAMP_KEY);\n\n    // All three fields must be present\n    if (metadataLocation == null || convertedVersionStr == null || convertedTimestamp == null) {\n      return Optional.empty();\n    }\n\n    try {\n      long convertedVersion = Long.parseLong(convertedVersionStr);\n      IcebergMetadata icebergMetadata =\n          new IcebergMetadata(metadataLocation, convertedVersion, convertedTimestamp);\n      return Optional.of(new UniformMetadata(icebergMetadata));\n    } catch (NumberFormatException e) {\n      logger.warn(\n          \"Invalid converted delta version in committer properties: {}\", convertedVersionStr, e);\n      return Optional.empty();\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/metrics/UcCommitTelemetry.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.metrics;\n\nimport io.delta.kernel.commit.CommitMetadata;\nimport io.delta.kernel.internal.metrics.MetricsReportSerializer;\nimport io.delta.kernel.internal.metrics.Timer;\nimport io.delta.kernel.metrics.MetricsReport;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.core.JsonProcessingException;\nimport java.util.Optional;\n\n/**\n * Telemetry framework for Unity Catalog commit operations.\n *\n * <p>Collects timing metrics for commit operations and generates reports for successful and failed\n * commits.\n */\npublic class UcCommitTelemetry {\n\n  private final String ucTableId;\n  private final String ucTablePath;\n  private final CommitMetadata commitMetadata;\n  private final MetricsCollector metricsCollector;\n\n  public UcCommitTelemetry(String ucTableId, String ucTablePath, CommitMetadata commitMetadata) {\n    this.ucTableId = ucTableId;\n    this.ucTablePath = ucTablePath;\n    this.commitMetadata = commitMetadata;\n    this.metricsCollector = new MetricsCollector();\n  }\n\n  public MetricsCollector getMetricsCollector() {\n    return metricsCollector;\n  }\n\n  public Report createSuccessReport() {\n    return new Report(metricsCollector.capture(), Optional.empty());\n  }\n\n  public Report createFailureReport(Exception error) {\n    return new Report(metricsCollector.capture(), Optional.of(error));\n  }\n\n  /** Mutable collector for gathering metrics during commit. */\n  public static class MetricsCollector {\n    public final Timer totalCommitTimer = new Timer();\n    public final Timer writeCommitFileTimer = new Timer();\n    public final Timer commitToUcServerTimer = new Timer();\n\n    public MetricsResult capture() {\n      return new MetricsResult(this);\n    }\n  }\n\n  /** Immutable snapshot of collected metric results. */\n  @JsonPropertyOrder({\n    \"totalCommitDurationNs\",\n    \"writeCommitFileDurationNs\",\n    \"commitToUcServerDurationNs\"\n  })\n  public static class MetricsResult {\n    public final long totalCommitDurationNs;\n    public final long writeCommitFileDurationNs;\n    public final long commitToUcServerDurationNs;\n\n    MetricsResult(MetricsCollector collector) {\n      this.totalCommitDurationNs = collector.totalCommitTimer.totalDurationNs();\n      this.writeCommitFileDurationNs = collector.writeCommitFileTimer.totalDurationNs();\n      this.commitToUcServerDurationNs = collector.commitToUcServerTimer.totalDurationNs();\n    }\n  }\n\n  /** Complete UC commit report with metadata and metrics. */\n  @JsonPropertyOrder({\n    \"operationType\",\n    \"reportUUID\",\n    \"ucTableId\",\n    \"ucTablePath\",\n    \"commitVersion\",\n    \"commitType\",\n    \"metrics\",\n    \"exception\"\n  })\n  public class Report implements MetricsReport {\n    public final String operationType = \"UcCommit\";\n    public final String reportUUID = java.util.UUID.randomUUID().toString();\n    public final String ucTableId = UcCommitTelemetry.this.ucTableId;\n    public final String ucTablePath = UcCommitTelemetry.this.ucTablePath;\n    public final long commitVersion = commitMetadata.getVersion();\n    public final CommitMetadata.CommitType commitType = commitMetadata.getCommitType();\n    public final MetricsResult metrics;\n    public final Optional<Exception> exception;\n\n    public Report(MetricsResult metrics, Optional<Exception> exception) {\n      this.metrics = metrics;\n      this.exception = exception;\n    }\n\n    @Override\n    public String toJson() throws JsonProcessingException {\n      return MetricsReportSerializer.OBJECT_MAPPER.writeValueAsString(this);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/metrics/UcLoadSnapshotTelemetry.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.metrics;\n\nimport io.delta.kernel.internal.metrics.MetricsReportSerializer;\nimport io.delta.kernel.internal.metrics.Timer;\nimport io.delta.kernel.metrics.MetricsReport;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.core.JsonProcessingException;\nimport java.util.Optional;\n\n/**\n * Telemetry framework for Unity Catalog snapshot loading operations.\n *\n * <p>Collects timing metrics for snapshot loading and generates reports for successful and failed\n * loads.\n */\npublic class UcLoadSnapshotTelemetry {\n\n  private final String ucTableId;\n  private final String ucTablePath;\n  private final Optional<Long> versionOpt;\n  private final Optional<Long> timestampOpt;\n  private final MetricsCollector metricsCollector;\n\n  public UcLoadSnapshotTelemetry(\n      String ucTableId,\n      String ucTablePath,\n      Optional<Long> versionOpt,\n      Optional<Long> timestampOpt) {\n    this.ucTableId = ucTableId;\n    this.ucTablePath = ucTablePath;\n    this.versionOpt = versionOpt;\n    this.timestampOpt = timestampOpt;\n    this.metricsCollector = new MetricsCollector();\n  }\n\n  public MetricsCollector getMetricsCollector() {\n    return metricsCollector;\n  }\n\n  public Report createSuccessReport() {\n    return new Report(metricsCollector.capture(), Optional.empty());\n  }\n\n  public Report createFailureReport(Exception error) {\n    return new Report(metricsCollector.capture(), Optional.of(error));\n  }\n\n  /** Mutable collector for gathering metrics during snapshot loading. */\n  public static class MetricsCollector {\n    public final Timer totalSnapshotLoadTimer = new Timer();\n    public final Timer getCommitsTimer = new Timer();\n    public final Timer kernelSnapshotBuildTimer = new Timer();\n    public final Timer loadLatestSnapshotForTimestampTimeTravelTimer = new Timer();\n    private int numCatalogCommits = -1;\n    private long resolvedSnapshotVersion = -1;\n\n    public void setNumCatalogCommits(int count) {\n      this.numCatalogCommits = count;\n    }\n\n    public void setResolvedSnapshotVersion(long version) {\n      this.resolvedSnapshotVersion = version;\n    }\n\n    public MetricsResult capture() {\n      return new MetricsResult(this);\n    }\n  }\n\n  /** Immutable snapshot of collected metric results. */\n  @JsonPropertyOrder({\n    \"totalLoadSnapshotDurationNs\",\n    \"getCommitsDurationNs\",\n    \"numCatalogCommits\",\n    \"kernelSnapshotBuildDurationNs\",\n    \"loadLatestSnapshotForTimestampTimeTravelDurationNs\",\n    \"resolvedSnapshotVersion\"\n  })\n  public static class MetricsResult {\n    public final long totalLoadSnapshotDurationNs;\n    public final long getCommitsDurationNs;\n    public final int numCatalogCommits;\n    public final long kernelSnapshotBuildDurationNs;\n    public final long loadLatestSnapshotForTimestampTimeTravelDurationNs;\n    public final long resolvedSnapshotVersion;\n\n    MetricsResult(MetricsCollector collector) {\n      this.totalLoadSnapshotDurationNs = collector.totalSnapshotLoadTimer.totalDurationNs();\n      this.getCommitsDurationNs = collector.getCommitsTimer.totalDurationNs();\n      this.numCatalogCommits = collector.numCatalogCommits;\n      this.kernelSnapshotBuildDurationNs = collector.kernelSnapshotBuildTimer.totalDurationNs();\n      this.loadLatestSnapshotForTimestampTimeTravelDurationNs =\n          collector.loadLatestSnapshotForTimestampTimeTravelTimer.totalDurationNs();\n      this.resolvedSnapshotVersion = collector.resolvedSnapshotVersion;\n    }\n  }\n\n  /** Complete UC snapshot loading report with metadata and metrics. */\n  @JsonPropertyOrder({\n    \"operationType\",\n    \"reportUUID\",\n    \"ucTableId\",\n    \"ucTablePath\",\n    \"versionOpt\",\n    \"timestampOpt\",\n    \"metrics\",\n    \"exception\"\n  })\n  public class Report implements MetricsReport {\n    public final String operationType = \"UcLoadSnapshot\";\n    public final String reportUUID = java.util.UUID.randomUUID().toString();\n    public final String ucTableId = UcLoadSnapshotTelemetry.this.ucTableId;\n    public final String ucTablePath = UcLoadSnapshotTelemetry.this.ucTablePath;\n    public final Optional<Long> versionOpt = UcLoadSnapshotTelemetry.this.versionOpt;\n    public final Optional<Long> timestampOpt = UcLoadSnapshotTelemetry.this.timestampOpt;\n    public final MetricsResult metrics;\n    public final Optional<Exception> exception;\n\n    public Report(MetricsResult metrics, Optional<Exception> exception) {\n      this.metrics = metrics;\n      this.exception = exception;\n    }\n\n    @Override\n    public String toJson() throws JsonProcessingException {\n      return MetricsReportSerializer.OBJECT_MAPPER.writeValueAsString(this);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/metrics/UcPublishTelemetry.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.metrics;\n\nimport io.delta.kernel.internal.metrics.MetricsReportSerializer;\nimport io.delta.kernel.internal.metrics.Timer;\nimport io.delta.kernel.metrics.MetricsReport;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport io.delta.kernel.shaded.com.fasterxml.jackson.core.JsonProcessingException;\nimport java.util.Optional;\n\n/**\n * Telemetry framework for Unity Catalog publish operations.\n *\n * <p>Collects timing and counter metrics for publish operations and generates reports for\n * successful and failed publishes.\n */\npublic class UcPublishTelemetry {\n\n  private final String ucTableId;\n  private final String ucTablePath;\n  private final long snapshotVersion;\n  private final int numCommitsToPublish;\n  private final MetricsCollector metricsCollector;\n\n  public UcPublishTelemetry(\n      String ucTableId, String ucTablePath, long snapshotVersion, int numCommitsToPublish) {\n    this.ucTableId = ucTableId;\n    this.ucTablePath = ucTablePath;\n    this.snapshotVersion = snapshotVersion;\n    this.numCommitsToPublish = numCommitsToPublish;\n    this.metricsCollector = new MetricsCollector();\n  }\n\n  public MetricsCollector getMetricsCollector() {\n    return metricsCollector;\n  }\n\n  public Report createSuccessReport() {\n    return new Report(metricsCollector.capture(), Optional.empty());\n  }\n\n  public Report createFailureReport(Exception error) {\n    return new Report(metricsCollector.capture(), Optional.of(error));\n  }\n\n  /** Mutable collector for gathering metrics during publish. */\n  public static class MetricsCollector {\n    public final Timer totalPublishTimer = new Timer();\n    private int commitsPublished = 0;\n    private int commitsAlreadyPublished = 0;\n\n    public void incrementCommitsPublished() {\n      commitsPublished++;\n    }\n\n    /**\n     * Increments the counter for commits already published by another process. Called when\n     * FileAlreadyExistsException indicates the commit was previously published.\n     */\n    public void incrementCommitsAlreadyPublished() {\n      commitsAlreadyPublished++;\n    }\n\n    /** @return number of commits published */\n    public int getCommitsPublished() {\n      return commitsPublished;\n    }\n\n    /** @return number of commits already published by another process */\n    public int getCommitsAlreadyPublished() {\n      return commitsAlreadyPublished;\n    }\n\n    public MetricsResult capture() {\n      return new MetricsResult(this);\n    }\n  }\n\n  /** Immutable snapshot of collected metric results. */\n  @JsonPropertyOrder({\n    \"totalPublishDurationNs\",\n    \"numCommitsPublished\",\n    \"numCommitsAlreadyPublished\"\n  })\n  public static class MetricsResult {\n    public final long totalPublishDurationNs;\n    public final int numCommitsPublished;\n    public final int numCommitsAlreadyPublished;\n\n    MetricsResult(MetricsCollector collector) {\n      this.totalPublishDurationNs = collector.totalPublishTimer.totalDurationNs();\n      this.numCommitsPublished = collector.commitsPublished;\n      this.numCommitsAlreadyPublished = collector.commitsAlreadyPublished;\n    }\n  }\n\n  /** Complete UC publish report with metadata and metrics. */\n  @JsonPropertyOrder({\n    \"operationType\",\n    \"reportUUID\",\n    \"ucTableId\",\n    \"ucTablePath\",\n    \"snapshotVersion\",\n    \"numCommitsToPublish\",\n    \"metrics\",\n    \"exception\"\n  })\n  public class Report implements MetricsReport {\n    public final String operationType = \"UcPublish\";\n    public final String reportUUID = java.util.UUID.randomUUID().toString();\n    public final String ucTableId = UcPublishTelemetry.this.ucTableId;\n    public final String ucTablePath = UcPublishTelemetry.this.ucTablePath;\n    public final long snapshotVersion = UcPublishTelemetry.this.snapshotVersion;\n    public final int numCommitsToPublish = UcPublishTelemetry.this.numCommitsToPublish;\n    public final MetricsResult metrics;\n    public final Optional<Exception> exception;\n\n    public Report(MetricsResult metrics, Optional<Exception> exception) {\n      this.metrics = metrics;\n      this.exception = exception;\n    }\n\n    @Override\n    public String toJson() throws JsonProcessingException {\n      return MetricsReportSerializer.OBJECT_MAPPER.writeValueAsString(this);\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/main/java/io/delta/kernel/unitycatalog/utils/OperationTimer.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.utils;\n\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\n\n/** Utility class for timing operations and logging their execution duration. */\npublic class OperationTimer {\n  private OperationTimer() {}\n\n  @FunctionalInterface\n  public interface ThrowingSupplier<T, E extends Exception> {\n    T get() throws E;\n  }\n\n  /** Times an operation that throws a checked exception of type E and logs the duration. */\n  @SuppressWarnings(\"unchecked\")\n  public static <T, E extends Exception> T timeCheckedOperation(\n      Logger logger, String operationName, String ucTableId, ThrowingSupplier<T, E> operation)\n      throws E {\n    final long startTime = System.nanoTime();\n    try {\n      final T result = operation.get();\n      final long durationMs = nanoToMs(System.nanoTime() - startTime);\n      logger.info(\"[{}] {} completed in {} ms\", ucTableId, operationName, durationMs);\n      return result;\n    } catch (Exception e) {\n      final long durationMs = nanoToMs(System.nanoTime() - startTime);\n      logger.warn(\n          \"[{}] {} failed after {} ms: {}\", ucTableId, operationName, durationMs, e.getMessage());\n      throw (E) e; // Safe cast since operation can only throw E\n    }\n  }\n\n  /** Times an operation and logs the duration. */\n  public static <T> T timeUncheckedOperation(\n      Logger logger, String operationName, String ucTableId, Supplier<T> operation) {\n    return timeCheckedOperation(logger, operationName, ucTableId, operation::get);\n  }\n\n  private static long nanoToMs(long nanoTime) {\n    return nanoTime / 1_000_000;\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/resources/log4j2.properties",
    "content": "#\n#  Copyright (2025) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\n# Set everything to be logged to the file target/unit-tests.log\nrootLogger.level = warn\nrootLogger.appenderRef.file.ref = ${sys:test.appender:-File}\n\nappender.file.type = File\nappender.file.name = File\nappender.file.fileName = target/unit-tests.log\nappender.file.append = true\nappender.file.layout.type = PatternLayout\nappender.file.layout.pattern = %d{yy/MM/dd HH:mm:ss.SSS} %t %p %c{1}: %m%n\n\n# Tests that launch java subprocesses can set the \"test.appender\" system property to\n# \"console\" to avoid having the child process's logs overwrite the unit test's\n# log file.\nappender.console.type = Console\nappender.console.name = console\nappender.console.target = SYSTEM_ERR\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = %t: %m%n\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/InMemoryUCClient.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.lang.{Long => JLong}\nimport java.net.URI\nimport java.util.Optional\nimport java.util.concurrent.ConcurrentHashMap\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.storage.commit.{Commit, CommitFailedException, GetCommitsResponse}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport io.delta.storage.commit.uccommitcoordinator.{InvalidTargetTableException, UCClient}\nimport io.delta.storage.commit.uniform.{IcebergMetadata, UniformMetadata}\n\nobject InMemoryUCClient {\n\n  /**\n   * Internal data structure to track table state including commits and version information.\n   *\n   * Thread Safety: All public methods are synchronized to ensure thread-safe access to the\n   * internal mutable state. This class is designed to be safely accessed by multiple threads\n   * concurrently.\n   */\n  class TableData(\n      private var maxRatifiedVersion: Long,\n      private val commits: ArrayBuffer[Commit]) {\n\n    // For test only, since UC doesn't store these as top-level entities.\n    private var currentProtocolOpt: Option[AbstractProtocol] = None\n    private var currentMetadataOpt: Option[AbstractMetadata] = None\n    private var currentIcebergOpt: Option[IcebergMetadata] = None\n\n    /** @return the maximum ratified version. */\n    def getMaxRatifiedVersion: Long = synchronized { maxRatifiedVersion }\n\n    /** @return An immutable list of all commits. */\n    def getCommits: List[Commit] = synchronized { commits.toList }\n\n    /** @return commits filtered by version range. */\n    def getCommitsInRange(\n        startVersion: Optional[JLong],\n        endVersion: Optional[JLong]): List[Commit] = synchronized {\n      commits\n        .filter { commit =>\n          startVersion.orElse(0L) <= commit.getVersion &&\n          commit.getVersion <= endVersion.orElse(Long.MaxValue)\n        }\n        .toList\n    }\n\n    /** @return the current protocol. For test only. */\n    def getCurrentProtocolOpt: Option[AbstractProtocol] = synchronized { currentProtocolOpt }\n\n    /** @return the current metadata. For test only. */\n    def getCurrentMetadataOpt: Option[AbstractMetadata] = synchronized { currentMetadataOpt }\n\n    /** @return the current Iceberg metadata. For test only. */\n    def getCurrentIcebergOpt: Option[IcebergMetadata] = synchronized {\n      currentIcebergOpt\n    }\n\n    /** Updates the Iceberg metadata. */\n    def updateIcebergMetadata(icebergMetadata: IcebergMetadata): Unit = synchronized {\n      currentIcebergOpt = Some(icebergMetadata)\n    }\n\n    /** Appends a new commit to this table and atomically updates protocol/metadata. */\n    def appendCommit(\n        commit: Commit,\n        newProtocol: Optional[AbstractProtocol] = Optional.empty(),\n        newMetadata: Optional[AbstractMetadata] = Optional.empty()): Unit = synchronized {\n      val expectedCommitVersion = maxRatifiedVersion + 1\n\n      if (commit.getVersion != expectedCommitVersion) {\n        throw new CommitFailedException(\n          false, /* retryable */\n          false, /* conflict */\n          s\"Expected commit version $expectedCommitVersion but got ${commit.getVersion}\")\n      }\n\n      // Atomically update everything\n      commits += commit\n      maxRatifiedVersion = commit.getVersion\n      if (newProtocol.isPresent) currentProtocolOpt = Some(newProtocol.get())\n      if (newMetadata.isPresent) currentMetadataOpt = Some(newMetadata.get())\n    }\n\n    def forceRemoveCommitsUpToVersion(version: Long): Unit = synchronized {\n      if (version < 0) {\n        throw new IllegalArgumentException(s\"Version must be non-negative, but got: $version\")\n      }\n\n      val indexToRemove = commits.lastIndexWhere(_.getVersion <= version)\n      if (indexToRemove >= 0) {\n        commits.remove(0, indexToRemove + 1)\n      }\n    }\n  }\n\n  object TableData {\n    def afterCreate(): TableData = new TableData(0, ArrayBuffer.empty[Commit])\n  }\n}\n\n/**\n * In-memory Unity Catalog client implementation for testing.\n *\n * Provides a mock implementation of UCClient that stores all table data in memory. This is useful\n * for unit tests that need to simulate Unity Catalog operations without connecting to an actual UC\n * service.\n *\n * Thread Safety: This implementation is thread-safe for concurrent access. Multiple threads can\n * safely perform operations on different tables simultaneously. Operations on the same table are\n * internally synchronized by the [[TableData]] class.\n */\nclass InMemoryUCClient(ucMetastoreId: String) extends UCClient {\n\n  import InMemoryUCClient._\n\n  /** Map from UC_TABLE_ID to TABLE_DATA */\n  private val tables = new ConcurrentHashMap[String, TableData]()\n\n  override def getMetastoreId: String = ucMetastoreId\n\n  /** Convenience method for tests to commit with default parameters. */\n  def commitWithDefaults(\n      tableId: String,\n      tableUri: URI,\n      commit: Optional[Commit],\n      lastKnownBackfilledVersion: Optional[JLong] = Optional.empty(),\n      disown: Boolean = false,\n      newMetadata: Optional[AbstractMetadata] = Optional.empty(),\n      newProtocol: Optional[AbstractProtocol] = Optional.empty()): Unit = {\n    this.commit(\n      tableId,\n      tableUri,\n      commit,\n      lastKnownBackfilledVersion,\n      disown,\n      newMetadata,\n      newProtocol,\n      Optional.empty() /* uniform */ )\n  }\n\n  override def commit(\n      tableId: String,\n      tableUri: URI,\n      commitOpt: Optional[Commit] = Optional.empty(),\n      lastKnownBackfilledVersionOpt: Optional[JLong],\n      disown: Boolean,\n      newMetadata: Optional[AbstractMetadata],\n      newProtocol: Optional[AbstractProtocol],\n      uniform: Optional[UniformMetadata]): Unit = {\n    forceThrowInCommitMethod()\n\n    if (disown) {\n      throw new UnsupportedOperationException(\"disown not yet supported in InMemoryUCClient\")\n    }\n\n    val tableData = getOrCreateTableIfNotExists(tableId)\n\n    tableData.synchronized {\n      commitOpt.ifPresent { commit =>\n        tableData.appendCommit(commit, newProtocol, newMetadata)\n      }\n\n      lastKnownBackfilledVersionOpt.ifPresent { lastKnownBackfilledVersion =>\n        tableData.forceRemoveCommitsUpToVersion(lastKnownBackfilledVersion)\n      }\n\n      // Update Iceberg metadata if provided in uniform\n      uniform.ifPresent { u =>\n        u.getIcebergMetadata.ifPresent { iceberg =>\n          tableData.updateIcebergMetadata(iceberg)\n        }\n      }\n    }\n  }\n\n  override def getCommits(\n      tableId: String,\n      tableUri: URI,\n      startVersion: Optional[JLong],\n      endVersion: Optional[JLong]): GetCommitsResponse = {\n    val tableData = getTableDataElseThrow(tableId)\n    val filteredCommits = tableData.getCommitsInRange(startVersion, endVersion)\n    new GetCommitsResponse(filteredCommits.asJava, tableData.getMaxRatifiedVersion)\n  }\n\n  override def close(): Unit = {}\n\n  /** Visible for testing. Can be overridden to force an exception in commit method. */\n  protected def forceThrowInCommitMethod(): Unit = {}\n\n  private[unitycatalog] def insertTableDataAfterCreate(ucTableId: String): Unit = {\n    Option(tables.putIfAbsent(ucTableId, TableData.afterCreate()))\n      .foreach(_ => throw new IllegalArgumentException(s\"Table $ucTableId already exists\"))\n  }\n\n  private[unitycatalog] def insertTableData(ucTableId: String, tableData: TableData): Unit = {\n    Option(tables.putIfAbsent(ucTableId, tableData))\n      .foreach(_ => throw new IllegalArgumentException(s\"Table $ucTableId already exists\"))\n  }\n\n  private[unitycatalog] def getTablesCopy: Map[String, TableData] = {\n    tables.asScala.toMap\n  }\n\n  /** Retrieves table data for the given table ID or throws an exception if not found. */\n  private[unitycatalog] def getTableDataElseThrow(tableId: String): TableData = {\n    Option(tables.get(tableId))\n      .getOrElse(throw new InvalidTargetTableException(s\"Table not found: $tableId\"))\n  }\n\n  /** Retrieves the table data for the given table ID, creating it if it does not exist. */\n  private def getOrCreateTableIfNotExists(tableId: String): TableData = {\n    tables.computeIfAbsent(tableId, _ => TableData.afterCreate())\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/InMemoryUCClientSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.lang.{Long => JLong}\nimport java.net.URI\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.storage.commit.{Commit, CommitFailedException}\nimport io.delta.storage.commit.uccommitcoordinator.InvalidTargetTableException\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Unit tests for [[InMemoryUCClient]]. */\nclass InMemoryUCClientSuite extends AnyFunSuite with UCCatalogManagedTestUtils {\n\n  private def testGetCommitsFiltering(\n      allVersions: Seq[Long],\n      startVersionOpt: Optional[JLong],\n      endVersionOpt: Optional[JLong],\n      expectedVersions: Seq[Long]): Unit = {\n    val client = getInMemoryUCClientWithCommitsForTableId(\"tableId\", allVersions)\n    val response = client.getCommits(\"tableId\", fakeURI, startVersionOpt, endVersionOpt)\n    val actualVersions = response.getCommits.asScala.map(_.getVersion)\n\n    assert(actualVersions == expectedVersions)\n  }\n\n  test(\"TableData::appendCommit handles commit version 1 (since CREATE does not go through UC)\") {\n    val tableData = InMemoryUCClient.TableData.afterCreate()\n    assert(tableData.getMaxRatifiedVersion == 0L)\n\n    tableData.appendCommit(createCommit(1L))\n\n    assert(tableData.getMaxRatifiedVersion == 1L)\n    assert(tableData.getCommits.size == 1)\n    assert(tableData.getCommits.head.getVersion == 1L)\n  }\n\n  test(\"TableData::appendCommit throws if commit version is not maxRatifiedVersion + 1\") {\n    val tableData = InMemoryUCClient.TableData.afterCreate()\n    tableData.appendCommit(createCommit(1L))\n\n    val exMsg = intercept[CommitFailedException] {\n      tableData.appendCommit(createCommit(99L))\n    }.getMessage\n\n    assert(exMsg.contains(\"Expected commit version 2 but got 99\"))\n  }\n\n  test(\"TableData::appendCommit appends the commit and updates the maxRatifiedVersion\") {\n    val tableData = InMemoryUCClient.TableData.afterCreate()\n    tableData.appendCommit(createCommit(1L))\n\n    assert(tableData.getMaxRatifiedVersion == 1L)\n    assert(tableData.getCommits.size == 1)\n    assert(tableData.getCommits.head.getVersion == 1L)\n\n    tableData.appendCommit(createCommit(2L))\n    assert(tableData.getMaxRatifiedVersion == 2L)\n    assert(tableData.getCommits.size == 2)\n    assert(tableData.getCommits.last.getVersion == 2L)\n  }\n\n  test(\"getCommits throws InvalidTargetTableException for non-existent table\") {\n    val client = new InMemoryUCClient(\"ucMetastoreId\")\n    val exception = intercept[InvalidTargetTableException] {\n      client.getCommits(\"abcd\", new URI(\"s3://bucket/table\"), Optional.empty(), Optional.empty())\n    }\n    assert(exception.getMessage.contains(s\"Table not found: abcd\"))\n  }\n\n  test(\"getCommits returns all commits if no startVersion or endVersion filter\") {\n    testGetCommitsFiltering(\n      allVersions = 1L to 5L,\n      startVersionOpt = Optional.empty(),\n      endVersionOpt = Optional.empty(),\n      expectedVersions = 1L to 5L)\n  }\n\n  test(\"getCommits filters by startVersion\") {\n    testGetCommitsFiltering(\n      allVersions = 1L to 5L,\n      startVersionOpt = Optional.of(2L),\n      endVersionOpt = Optional.empty(),\n      expectedVersions = 2L to 5L)\n  }\n\n  test(\"getCommits filters by endVersion\") {\n    testGetCommitsFiltering(\n      allVersions = 1L to 5L,\n      startVersionOpt = Optional.empty(),\n      endVersionOpt = Optional.of(3L),\n      expectedVersions = 1L to 3L)\n  }\n\n  test(\"getCommits filters by startVersion and endVersion\") {\n    testGetCommitsFiltering(\n      allVersions = 1L to 5L,\n      startVersionOpt = Optional.of(2L),\n      endVersionOpt = Optional.of(4L),\n      expectedVersions = 2L to 4L)\n  }\n\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UCCatalogManagedClientCommitRangeSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.kernel.unitycatalog\n\nimport java.util.Optional\n\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.storage.commit.uccommitcoordinator.{InvalidTargetTableException, UCClient}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UCCatalogManagedClientCommitRangeSuite extends AnyFunSuite with UCCatalogManagedTestUtils {\n\n  /** Helper method with reasonable defaults */\n  private def loadCommitRange(\n      ucCatalogManagedClient: UCCatalogManagedClient,\n      engine: Engine = defaultEngine,\n      ucTableId: String = \"testUcTableId\",\n      tablePath: String = \"testUcTablePath\",\n      startVersionOpt: Optional[java.lang.Long] = emptyLongOpt,\n      startTimestampOpt: Optional[java.lang.Long] = emptyLongOpt,\n      endVersionOpt: Optional[java.lang.Long] = emptyLongOpt,\n      endTimestampOpt: Optional[java.lang.Long] = emptyLongOpt) = {\n    ucCatalogManagedClient.loadCommitRange(\n      engine,\n      ucTableId,\n      tablePath,\n      startVersionOpt,\n      startTimestampOpt,\n      endVersionOpt,\n      endTimestampOpt)\n  }\n\n  private def testLoadCommitRange(\n      expectedStartVersion: Long,\n      expectedEndVersion: Long,\n      startVersionOpt: Optional[java.lang.Long] = emptyLongOpt,\n      startTimestampOpt: Optional[java.lang.Long] = emptyLongOpt,\n      endVersionOpt: Optional[java.lang.Long] = emptyLongOpt,\n      endTimestampOpt: Optional[java.lang.Long] = emptyLongOpt): Unit = {\n    withUCClientAndTestTable { (ucClient, tablePath, _) =>\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n      val commitRange = loadCommitRange(\n        ucCatalogManagedClient,\n        tablePath = tablePath,\n        startVersionOpt = startVersionOpt,\n        startTimestampOpt = startTimestampOpt,\n        endVersionOpt = endVersionOpt,\n        endTimestampOpt = endTimestampOpt)\n\n      assert(commitRange.getStartVersion == expectedStartVersion)\n      assert(commitRange.getEndVersion == expectedEndVersion)\n      assert(ucClient.getNumGetCommitCalls == 1)\n    }\n  }\n\n  test(\"loadCommitRange throws on null input\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    assertThrows[NullPointerException] {\n      // engine is null\n      loadCommitRange(ucCatalogManagedClient, engine = null)\n    }\n    assertThrows[NullPointerException] {\n      // ucTableId is null\n      loadCommitRange(ucCatalogManagedClient, ucTableId = null)\n    }\n    assertThrows[NullPointerException] {\n      // tablePath is null\n      loadCommitRange(ucCatalogManagedClient, tablePath = null)\n    }\n    assertThrows[NullPointerException] {\n      // startVersionOpt is null\n      loadCommitRange(ucCatalogManagedClient, startVersionOpt = null)\n    }\n    assertThrows[NullPointerException] {\n      // startTimestampOpt is null\n      loadCommitRange(ucCatalogManagedClient, startTimestampOpt = null)\n    }\n    assertThrows[NullPointerException] {\n      // endVersionOpt is null\n      loadCommitRange(ucCatalogManagedClient, endVersionOpt = null)\n    }\n    assertThrows[NullPointerException] {\n      // endTimestampOpt is null\n      loadCommitRange(ucCatalogManagedClient, endTimestampOpt = null)\n    }\n  }\n\n  test(\"loadCommitRange throws on invalid input - conflicting start boundaries\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      loadCommitRange(\n        ucCatalogManagedClient,\n        startVersionOpt = Optional.of(1L),\n        startTimestampOpt = Optional.of(100L))\n    }\n    assert(ex.getMessage.contains(\"Cannot provide both a start timestamp and start version\"))\n  }\n\n  test(\"loadCommitRange throws on invalid input - conflicting end boundaries\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      loadCommitRange(\n        ucCatalogManagedClient,\n        startVersionOpt = Optional.of(0L),\n        endVersionOpt = Optional.of(2L),\n        endTimestampOpt = Optional.of(200L))\n    }\n    assert(ex.getMessage.contains(\"Cannot provide both an end timestamp and start version\"))\n  }\n\n  test(\"loadCommitRange throws on invalid input - start version > end version\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      loadCommitRange(\n        ucCatalogManagedClient,\n        startVersionOpt = Optional.of(5L),\n        endVersionOpt = Optional.of(2L))\n    }\n    assert(ex.getMessage.contains(\"Cannot provide a start version greater than the end version\"))\n  }\n\n  test(\"loadCommitRange throws on invalid input - start timestamp > end timestamp\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      loadCommitRange(\n        ucCatalogManagedClient,\n        startTimestampOpt = Optional.of(500L),\n        endTimestampOpt = Optional.of(200L))\n    }\n    assert(ex.getMessage.contains(\n      \"Cannot provide a start timestamp greater than the end timestamp\"))\n  }\n\n  test(\"loadCommitRange throws if startVersion is greater than max ratified version\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      testLoadCommitRange(\n        expectedStartVersion = 0,\n        expectedEndVersion = 2,\n        startVersionOpt = Optional.of(9L))\n    }\n    assert(ex.getMessage.contains(\n      \"Cannot load commit range with start version 9 as the latest version ratified by UC is 2\"))\n  }\n\n  test(\"loadCommitRange throws if endVersion is greater than max ratified version\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      testLoadCommitRange(\n        expectedStartVersion = 0,\n        expectedEndVersion = 2,\n        startVersionOpt = Optional.of(0L),\n        endVersionOpt = Optional.of(9L))\n    }\n    assert(ex.getMessage.contains(\n      \"Cannot load commit range with end version 9 as the latest version ratified by UC is 2\"))\n  }\n\n  test(\"loadCommitRange throws when no start boundary is provided\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[IllegalArgumentException] {\n      loadCommitRange(ucCatalogManagedClient)\n    }\n    assert(ex.getMessage.contains(\"Must provide either a start timestamp or start version\"))\n  }\n\n  test(\"loadCommitRange loads with default end boundary -> latest\") {\n    testLoadCommitRange(\n      expectedStartVersion = 0,\n      expectedEndVersion = 2,\n      startVersionOpt = Optional.of(0L))\n  }\n\n  test(\"loadCommitRange loads with version boundaries\") {\n    testLoadCommitRange(\n      expectedStartVersion = 1,\n      expectedEndVersion = 2,\n      startVersionOpt = Optional.of(1L),\n      endVersionOpt = Optional.of(2L))\n  }\n\n  test(\"loadCommitRange loads with timestamp boundaries\") {\n    testLoadCommitRange(\n      expectedStartVersion = 0L,\n      expectedEndVersion = 1L,\n      startTimestampOpt = Optional.of(v0Ts),\n      endTimestampOpt = Optional.of(v1Ts + 10))\n  }\n\n  test(\"loadCommitRange loads with mixed start timestamp and end version\") {\n    testLoadCommitRange(\n      expectedStartVersion = 0L,\n      expectedEndVersion = 2L,\n      startTimestampOpt = Optional.of(v0Ts),\n      endVersionOpt = Optional.of(2L))\n  }\n\n  test(\"loadCommitRange loads with mixed start version and end timestamp\") {\n    testLoadCommitRange(\n      expectedStartVersion = 1L,\n      expectedEndVersion = 2L,\n      startVersionOpt = Optional.of(1L),\n      endTimestampOpt = Optional.of(v2Ts))\n  }\n\n  test(\"loadCommitRange loads single version range\") {\n    testLoadCommitRange(\n      expectedStartVersion = 1L,\n      expectedEndVersion = 1L,\n      startVersionOpt = Optional.of(1L),\n      endVersionOpt = Optional.of(1L))\n  }\n\n  test(\"loadCommitRange loads single version range by timestamps\") {\n    testLoadCommitRange(\n      expectedStartVersion = 1L,\n      expectedEndVersion = 1L,\n      startTimestampOpt = Optional.of(v1Ts - 50),\n      endTimestampOpt = Optional.of(v1Ts + 50))\n  }\n\n  test(\"loadCommitRange invalid timestamp bound\") {\n    intercept[KernelException] {\n      testLoadCommitRange(\n        expectedStartVersion = 1L,\n        expectedEndVersion = 1L,\n        startTimestampOpt = Optional.of(v2Ts + 10))\n    }\n    intercept[KernelException] {\n      testLoadCommitRange(\n        expectedStartVersion = 1L,\n        expectedEndVersion = 1L,\n        startVersionOpt = Optional.of(0),\n        endTimestampOpt = Optional.of(v0Ts - 10))\n    }\n  }\n\n  test(\"loadCommitRange throws when the table doesn't exist in catalog\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    val ex = intercept[RuntimeException] {\n      loadCommitRange(\n        ucCatalogManagedClient,\n        ucTableId = \"nonExistentTableId\",\n        startVersionOpt = Optional.of(0L))\n    }\n    assert(ex.getCause.isInstanceOf[InvalidTargetTableException])\n  }\n\n  test(\"loadCommitRange for new table when UC maxRatifiedVersion is 0\") {\n    val tablePath = getTestResourceFilePath(\"catalog-owned-preview\")\n    val ucCatalogManagedClient = createUCCatalogManagedClientForTableAfterCreate()\n    val commitRange = loadCommitRange(\n      ucCatalogManagedClient,\n      tablePath = tablePath,\n      startVersionOpt = Optional.of(0L))\n\n    assert(commitRange.getStartVersion == 0)\n    assert(commitRange.getEndVersion == 0)\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UCCatalogManagedClientSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.internal.CreateTableTransactionBuilderImpl\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.tablefeatures.TableFeatures.{CATALOG_MANAGED_RW_FEATURE, TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport io.delta.storage.commit.uccommitcoordinator.InvalidTargetTableException\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Unit tests for [[UCCatalogManagedClient]]. */\nclass UCCatalogManagedClientSuite extends AnyFunSuite with UCCatalogManagedTestUtils {\n\n  import UCCatalogManagedClientSuite._\n\n  private val testUcTableId = \"testUcTableId\"\n\n  /**\n   * If present, loads the given `versionToLoad`, else loads the maxRatifiedVersion of 2.\n   *\n   * Also asserts that the desired `versionToLoad` is, in fact, loaded.\n   */\n  private def testCatalogManagedTable(\n      versionToLoad: Optional[java.lang.Long] = emptyLongOpt,\n      timestampToLoad: Optional[java.lang.Long] = emptyLongOpt,\n      expectedVersion: Option[Long] = None): Unit = {\n    require(!versionToLoad.isPresent || !timestampToLoad.isPresent)\n    // If timestamp time-travel, must provide expected version\n    require(!timestampToLoad.isPresent || expectedVersion.isDefined)\n\n    withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n      val snapshot = loadSnapshot(\n        ucCatalogManagedClient,\n        tablePath = tablePath,\n        versionToLoad = versionToLoad,\n        timestampToLoad = timestampToLoad)\n\n      val version = expectedVersion.getOrElse(versionToLoad.orElse(maxRatifiedVersion))\n      val protocol = snapshot.getProtocol\n      assert(snapshot.getVersion == version)\n      assert(protocol.getMinReaderVersion == TABLE_FEATURES_MIN_READER_VERSION)\n      assert(protocol.getMinWriterVersion == TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(protocol.getReaderFeatures.contains(CATALOG_MANAGED_RW_FEATURE.featureName()))\n      assert(protocol.getWriterFeatures.contains(CATALOG_MANAGED_RW_FEATURE.featureName()))\n      assert(ucClient.getNumGetCommitCalls == 1)\n    }\n  }\n\n  test(\"constructor throws on invalid input\") {\n    assertThrows[NullPointerException] {\n      new UCCatalogManagedClient(null)\n    }\n  }\n\n  test(\"loadTable throws on invalid input\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    assertThrows[NullPointerException] {\n      // engine is null\n      loadSnapshot(ucCatalogManagedClient, engine = null)\n    }\n    assertThrows[NullPointerException] {\n      // ucTableId is null\n      loadSnapshot(ucCatalogManagedClient, ucTableId = null)\n    }\n    assertThrows[NullPointerException] {\n      // tablePath is null\n      loadSnapshot(ucCatalogManagedClient, tablePath = null)\n    }\n    assertThrows[NullPointerException] {\n      // versionToLoad is null\n      loadSnapshot(ucCatalogManagedClient, versionToLoad = null)\n    }\n    assertThrows[NullPointerException] {\n      // timestampToLoad is null\n      loadSnapshot(ucCatalogManagedClient, timestampToLoad = null)\n    }\n    assertThrows[IllegalArgumentException] {\n      // version < 0\n      loadSnapshot(ucCatalogManagedClient, versionToLoad = Optional.of(-1L))\n    }\n    assertThrows[IllegalArgumentException] {\n      // cannot provide both timestamp and version\n      loadSnapshot(\n        ucCatalogManagedClient,\n        versionToLoad = Optional.of(10L),\n        timestampToLoad = Optional.of(10L))\n    }\n  }\n\n  Seq(\n    (emptyLongOpt, emptyLongOpt, \"latest (implicitly)\"),\n    (javaLongOpt(0L), emptyLongOpt, \"v0 (explicitly by version)\"),\n    (emptyLongOpt, javaLongOpt(1749830855993L), \"v0 (explicitly by timestamp\")).foreach {\n    case (versionToLoad, timestampToLoad, description) =>\n      test(s\"loadTable throws when table doesn't exist in catalog -- $description\") {\n        val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n        val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n        val ex = intercept[RuntimeException] {\n          loadSnapshot(\n            ucCatalogManagedClient,\n            ucTableId = \"nonExistentTableId\",\n            versionToLoad = versionToLoad,\n            timestampToLoad = timestampToLoad)\n        }\n        assert(ex.getCause.isInstanceOf[InvalidTargetTableException])\n      }\n  }\n\n  Seq(\n    (emptyLongOpt, emptyLongOpt, \"latest (implicitly)\"),\n    (javaLongOpt(0L), emptyLongOpt, \"v0 (explicitly by version)\"),\n    (emptyLongOpt, javaLongOpt(1749830855993L), \"v0 (explicitly by timestamp\")).foreach {\n    case (versionToLoad, timestampToLoad, description) =>\n      test(s\"table version 0 is loaded when UC maxRatifiedVersion is 0 -- $description\") {\n        val tablePath = getTestResourceFilePath(\"catalog-owned-preview\")\n        val ucCatalogManagedClient = createUCCatalogManagedClientForTableAfterCreate()\n        val snapshot = loadSnapshot(\n          ucCatalogManagedClient,\n          tablePath = tablePath,\n          versionToLoad = versionToLoad,\n          timestampToLoad = timestampToLoad)\n\n        assert(snapshot.getVersion == 0L)\n      }\n  }\n\n  test(\"loadTable correctly loads a UC table -- versionToLoad is empty => load latest\") {\n    // Since versionToLoad is empty, it asserts that the latest version (2) is loaded\n    testCatalogManagedTable()\n  }\n\n  /* ---- Time-travel-by-version tests --- */\n  test(\"loadTable correctly loads a UC table -- versionToLoad is a ratified commit (the max)\") {\n    testCatalogManagedTable(versionToLoad = Optional.of(2L))\n  }\n\n  test(\"loadTable correctly loads a UC table -- versionToLoad is a ratified commit (not the max)\") {\n    testCatalogManagedTable(versionToLoad = Optional.of(1L))\n  }\n\n  test(\"loadTable correctly loads a UC table -- versionToLoad is a published commit\") {\n    testCatalogManagedTable(versionToLoad = Optional.of(0L))\n  }\n\n  test(\"loadTable throws if version to load is greater than max ratified version\") {\n    val exMsg = intercept[IllegalArgumentException] {\n      testCatalogManagedTable(versionToLoad = Optional.of(9L))\n    }.getMessage\n\n    assert(exMsg.contains(\"Cannot load table version 9 as the latest version ratified by UC is 2\"))\n  }\n\n  /* ---- Time-travel-by-timestamp tests --- */\n\n  test(\"loadTable correctly loads a UC table -- \" +\n    \"timestampToLoad is exactly a ratified commit (the max)\") {\n    testCatalogManagedTable(timestampToLoad = Optional.of(v2Ts), expectedVersion = Some(2L))\n  }\n\n  test(\"loadTable correctly loads a UC table -- timestampToLoad is between ratified commits\") {\n    testCatalogManagedTable(timestampToLoad = Optional.of(v2Ts - 50L), expectedVersion = Some(1L))\n  }\n\n  test(\"loadTable correctly loads a UC table -- \" +\n    \"timestampToLoad is exactly a ratified commit (not the max)\") {\n    testCatalogManagedTable(timestampToLoad = Optional.of(v1Ts), expectedVersion = Some(1L))\n  }\n\n  test(\"loadTable correctly loads a UC table -- \" +\n    \"timestampToLoad is between ratified and published commits\") {\n    testCatalogManagedTable(timestampToLoad = Optional.of(v1Ts - 50L), expectedVersion = Some(0L))\n  }\n\n  test(\"loadTable correctly loads a UC table -- timestampToLoad is exactly a published commit\") {\n    testCatalogManagedTable(timestampToLoad = Optional.of(v0Ts), expectedVersion = Some(0L))\n  }\n\n  test(\"loadTable throws if timestampToLoad is before the earliest commit\") {\n    val exMsg = intercept[KernelException] {\n      testCatalogManagedTable(timestampToLoad = Optional.of(v0Ts - 1), expectedVersion = Some(0))\n    }.getMessage\n\n    assert(exMsg.contains(\"The provided timestamp 1749830855992 ms (2025-06-13T16:07:35.992Z) is \" +\n      \"before the earliest available version 0\"))\n  }\n\n  test(\"loadTable throws if timestampToLoad is after the latest commit\") {\n    val exMsg = intercept[KernelException] {\n      testCatalogManagedTable(timestampToLoad = Optional.of(v2Ts + 1), expectedVersion = Some(2))\n    }.getMessage\n\n    assert(exMsg.contains(\"The provided timestamp 1749830881800 ms (2025-06-13T16:08:01.800Z) is \" +\n      \"after the latest available version 2\"))\n  }\n\n  test(\"loadTable does not throw on negative timestamp in validation\") {\n    // This specifically tests that the validation logic in UCCatalogManagedClient.loadTable\n    // does not reject negative timestamps\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    // Should not throw IllegalArgumentException for negative timestamp\n    // (it will fail later when trying to find the table, but that's expected)\n    val ex = intercept[RuntimeException] {\n      loadSnapshot(ucCatalogManagedClient, timestampToLoad = Optional.of(-1L))\n    }\n    // Verify it fails because the table doesn't exist, NOT because of timestamp validation\n    assert(ex.getCause.isInstanceOf[InvalidTargetTableException])\n  }\n\n  /* ---- end time-travel-by-timestamp tests ---- */\n\n  test(\"converts UC Commit into Kernel ParsedLogData.RATIFIED_STAGED_COMMIT\") {\n    val ucCommit = createCommit(1)\n    val hadoopFS = ucCommit.getFileStatus\n\n    val kernelParsedDeltaData = UCCatalogManagedClient\n      .getSortedKernelParsedDeltaDataFromRatifiedCommits(testUcTableId, Seq(ucCommit).asJava)\n      .get(0)\n    val kernelFS = kernelParsedDeltaData.getFileStatus\n\n    assert(kernelParsedDeltaData.isFile)\n    assert(kernelFS.getPath == hadoopFS.getPath.toString)\n    assert(kernelFS.getSize == hadoopFS.getLen)\n    assert(kernelFS.getModificationTime == hadoopFS.getModificationTime)\n  }\n\n  test(\"sorts UC commits by version\") {\n    val ucCommitsUnsorted = Seq(createCommit(1), createCommit(2), createCommit(3)).asJava\n\n    val kernelParsedLogData = UCCatalogManagedClient\n      .getSortedKernelParsedDeltaDataFromRatifiedCommits(testUcTableId, ucCommitsUnsorted)\n\n    assert(kernelParsedLogData.size() == 3)\n    assert(kernelParsedLogData.get(0).getVersion == 1)\n    assert(kernelParsedLogData.get(1).getVersion == 2)\n    assert(kernelParsedLogData.get(2).getVersion == 3)\n  }\n\n  test(\"creates snapshot with UCCatalogManagedCommitter\") {\n    val tablePath = getTestResourceFilePath(\"catalog-owned-preview\")\n    val ucCatalogManagedClient = createUCCatalogManagedClientForTableAfterCreate()\n    val snapshot =\n      loadSnapshot(ucCatalogManagedClient, tablePath = tablePath, versionToLoad = Optional.of(0L))\n    assert(snapshot.getCommitter.isInstanceOf[UCCatalogManagedCommitter])\n  }\n\n  test(\"buildCreateTableTransaction sets required properties and uses UC committer\") {\n    // ===== GIVEN =====\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n    // ===== WHEN =====\n    val createTableTxnBuilder = ucCatalogManagedClient\n      .buildCreateTableTransaction(testUcTableId, baseTestTablePath, testSchema, \"test-engine\")\n      .withTableProperties(Map(\"foo\" -> \"bar\").asJava)\n      .asInstanceOf[CreateTableTransactionBuilderImpl]\n\n    // ===== THEN =====\n    val builderTableProperties = createTableTxnBuilder.getTablePropertiesOpt.get()\n    assert(builderTableProperties\n      .get(TableFeatures.CATALOG_MANAGED_RW_FEATURE.getTableFeatureSupportKey) == \"supported\")\n    assert(builderTableProperties\n      .get(TableFeatures.VACUUM_PROTOCOL_CHECK_RW_FEATURE.getTableFeatureSupportKey) == \"supported\")\n    assert(builderTableProperties.get(\"io.unitycatalog.tableId\") == testUcTableId)\n    assert(builderTableProperties.get(\"foo\") == \"bar\")\n\n    val committerOpt = createTableTxnBuilder.getCommitterOpt\n    assert(committerOpt.get().isInstanceOf[UCCatalogManagedCommitter])\n  }\n}\n\nobject UCCatalogManagedClientSuite {\n\n  private def javaLongOpt(value: Long): Optional[java.lang.Long] = {\n    Optional.of(value)\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UCCatalogManagedCommitterSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.io.IOException\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.commit.{CommitFailedException, CommitMetadata}\nimport io.delta.kernel.commit.CommitMetadata.CommitType\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.kernel.internal.util.{Tuple2 => KernelTuple2}\nimport io.delta.kernel.test.{BaseMockJsonHandler, MockFileSystemClientUtils, TestFixtures, VectorTestUtils}\nimport io.delta.kernel.unitycatalog.adapters.UniformAdapter\nimport io.delta.kernel.utils.{CloseableIterator, FileStatus}\nimport io.delta.storage.commit.Commit\nimport io.delta.storage.commit.uccommitcoordinator.InvalidTargetTableException\n\nimport InMemoryUCClient.TableData\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UCCatalogManagedCommitterSuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils\n    with TestFixtures\n    with VectorTestUtils\n    with MockFileSystemClientUtils {\n\n  private val testUcTableId = \"testUcTableId\"\n\n  // ============================================================\n  // ===================== Misc. Unit Tests =====================\n  // ============================================================\n\n  test(\"constructor throws on null inputs\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n\n    assertThrows[NullPointerException] {\n      new UCCatalogManagedCommitter(null, testUcTableId, baseTestTablePath)\n    }\n    assertThrows[NullPointerException] {\n      new UCCatalogManagedCommitter(ucClient, null, baseTestTablePath)\n    }\n    assertThrows[NullPointerException] {\n      new UCCatalogManagedCommitter(ucClient, testUcTableId, null)\n    }\n  }\n\n  test(\"commit throws on null inputs\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, baseTestTablePath)\n\n    // Null engine\n    assertThrows[NullPointerException] {\n      committer.commit(null, emptyActionsIterator, catalogManagedWriteCommitMetadata(version = 1))\n    }\n\n    // Null finalizedActions\n    assertThrows[NullPointerException] {\n      committer.commit(defaultEngine, null, catalogManagedWriteCommitMetadata(version = 1))\n    }\n\n    // Null commitMetadata\n    assertThrows[NullPointerException] {\n      committer.commit(defaultEngine, emptyActionsIterator, null)\n    }\n  }\n\n  test(\"commit throws if CommitMetadata is for a different table\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, baseTestTablePath)\n    val badCommitMetadata = catalogManagedWriteCommitMetadata(\n      version = 1,\n      \"/path/to/different/table/_delta_log\")\n\n    val exMsg = intercept[IllegalArgumentException] {\n      committer.commit(defaultEngine, emptyActionsIterator, badCommitMetadata)\n    }.getMessage\n\n    assert(exMsg.contains(\"Delta log path '/path/to/table/_delta_log' does not match expected \" +\n      \"'/path/to/different/table/_delta_log'\"))\n  }\n\n  // ========== CommitType Tests START ==========\n\n  case class CommitTypeTestCase(\n      readPandMOpt: Optional[KernelTuple2[Protocol, Metadata]] = Optional.empty(),\n      newProtocolOpt: Optional[Protocol] = Optional.empty(),\n      newMetadataOpt: Optional[Metadata] = Optional.empty(),\n      expectedCommitType: CommitType)\n\n  private val protocol12 = new Protocol(1, 2)\n\n  private val unsupportedCommitTypesTestCases = Seq(\n    CommitTypeTestCase(\n      readPandMOpt = Optional.empty(),\n      newProtocolOpt = Optional.of(protocol12),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata),\n      expectedCommitType = CommitType.FILESYSTEM_CREATE),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      expectedCommitType = CommitType.FILESYSTEM_WRITE),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(new KernelTuple2(protocol12, basicPartitionedMetadata)),\n      newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n      expectedCommitType = CommitType.FILESYSTEM_UPGRADE_TO_CATALOG),\n    CommitTypeTestCase(\n      readPandMOpt = Optional.of(\n        new KernelTuple2(protocolWithCatalogManagedSupport, basicPartitionedMetadata)),\n      newProtocolOpt = Optional.of(protocol12),\n      expectedCommitType = CommitType.CATALOG_DOWNGRADE_TO_FILESYSTEM))\n\n  unsupportedCommitTypesTestCases.foreach { testCase =>\n    test(s\"commit throws UnsupportedOperationException for ${testCase.expectedCommitType}\") {\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, baseTestTablePath)\n\n      // version > 0 for updates, version = 0 for creates\n      val version = if (testCase.readPandMOpt.isPresent) 1L else 0L\n\n      val commitMetadata = createCommitMetadata(\n        version = version,\n        logPath = baseTestLogPath,\n        readPandMOpt = testCase.readPandMOpt,\n        newProtocolOpt = testCase.newProtocolOpt,\n        newMetadataOpt = testCase.newMetadataOpt)\n\n      assert(commitMetadata.getCommitType == testCase.expectedCommitType)\n\n      val exception = intercept[UnsupportedOperationException] {\n        committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n      }\n      assert(exception.getMessage == s\"Unsupported commit type: ${testCase.expectedCommitType}\")\n    }\n  }\n\n  // ========== CommitType Tests END ==========\n\n  test(\"kernelFileStatusToHadoopFileStatus converts kernel FileStatus to Hadoop FileStatus\") {\n    // ===== GIVEN =====\n    val kernelFileStatus = FileStatus.of(\"/path/to/file.json\", 1024L, 1234567890L)\n\n    // ===== WHEN =====\n    val hadoopFileStatus =\n      UCCatalogManagedCommitter.kernelFileStatusToHadoopFileStatus(kernelFileStatus)\n\n    // ===== THEN =====\n    // These are the fields that we care about, taken from the Kernel FileStatus\n    assert(hadoopFileStatus.getPath.toString == \"/path/to/file.json\")\n    assert(hadoopFileStatus.getLen == 1024L)\n    assert(hadoopFileStatus.getModificationTime == 1234567890L)\n\n    // These are defaults that we set\n    assert(hadoopFileStatus.getAccessTime == 1234567890L) // same as modification time\n    assert(!hadoopFileStatus.isDirectory)\n    assert(hadoopFileStatus.getReplication == 1)\n    assert(hadoopFileStatus.getBlockSize == 128 * 1024 * 1024) // 128MB\n    assert(hadoopFileStatus.getOwner == \"unknown\")\n    assert(hadoopFileStatus.getGroup == \"unknown\")\n    assert(hadoopFileStatus.getPermission ==\n      org.apache.hadoop.fs.permission.FsPermission.getFileDefault)\n  }\n\n  test(\"writeDeltaFile returns real FileStatus\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n      val testValue = \"TEST_FILE_STATUS_DATA\"\n      val actionsIterator = getSingleElementRowIter(testValue)\n\n      val commitMetadata = createCommitMetadata(\n        version = 0,\n        logPath = logPath,\n        newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n\n      // ===== WHEN =====\n      val response = committer.commit(defaultEngine, actionsIterator, commitMetadata)\n\n      // ===== THEN =====\n      val fileStatus = response.getCommitLogData.getFileStatus\n      assert(fileStatus.getSize > 0)\n      assert(fileStatus.getModificationTime > 0)\n    }\n  }\n\n  // ===============================================================\n  // ===================== CATALOG_WRITE Tests =====================\n  // ===============================================================\n\n  test(\"CATALOG_WRITE: protocol and metadata changes are passed to UC client\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n\n      // ===== WHEN =====\n      val protocolUpgrade = protocolWithCatalogManagedSupport\n        .withFeature(TableFeatures.DELETION_VECTORS_RW_FEATURE)\n      val metadataUpgrade = basicPartitionedMetadata\n        .withMergedConfiguration(Map(\"foo\" -> \"bar\").asJava)\n\n      val commitMetadata = createCommitMetadata(\n        version = 1,\n        logPath = logPath,\n        readPandMOpt = Optional.of(\n          new KernelTuple2[Protocol, Metadata](\n            protocolWithCatalogManagedSupport,\n            basicPartitionedMetadata)),\n        newProtocolOpt = Optional.of(protocolUpgrade),\n        newMetadataOpt = Optional.of(metadataUpgrade))\n      committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n\n      // ===== THEN =====\n      val updatedTableData = ucClient.getTablesCopy.get(testUcTableId).get\n      val latestProtocol = updatedTableData.getCurrentProtocolOpt.get\n      val latestMetadata = updatedTableData.getCurrentMetadataOpt.get\n      assert(latestProtocol.getReaderFeatures === protocolUpgrade.getReaderFeatures)\n      assert(latestProtocol.getWriterFeatures === protocolUpgrade.getWriterFeatures)\n      assert(latestMetadata.getConfiguration === metadataUpgrade.getConfiguration)\n    }\n  }\n\n  test(\"CATALOG_WRITE: Iceberg metadata is extracted and passed to UC client\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n\n      // ===== WHEN =====\n      val icebergProperties = Map(\n        UniformAdapter.ICEBERG_METADATA_LOCATION_KEY -> \"s3://bucket/table/metadata/v1.json\",\n        UniformAdapter.ICEBERG_CONVERTED_DELTA_VERSION_KEY -> \"1\",\n        UniformAdapter.ICEBERG_CONVERTED_DELTA_TIMESTAMP_KEY -> \"2025-01-04T03:13:11.423\").asJava\n\n      val commitMetadata = createCommitMetadata(\n        version = 1,\n        logPath = logPath,\n        committerProperties = () => icebergProperties,\n        readPandMOpt = Optional.of(\n          new KernelTuple2[Protocol, Metadata](\n            protocolWithCatalogManagedSupport,\n            basicPartitionedMetadata)))\n      committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n\n      // ===== THEN =====\n      val updatedTableData = ucClient.getTablesCopy.get(testUcTableId).get\n      val icebergOpt = updatedTableData.getCurrentIcebergOpt\n      assert(icebergOpt.isDefined)\n      val iceberg = icebergOpt.get\n      assert(iceberg.getMetadataLocation === \"s3://bucket/table/metadata/v1.json\")\n      assert(iceberg.getConvertedDeltaVersion === 1L)\n      assert(iceberg.getConvertedDeltaTimestamp === \"2025-01-04T03:13:11.423\")\n    }\n  }\n\n  test(\"CATALOG_WRITE: empty committer properties result in no Iceberg metadata\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n\n      // ===== WHEN =====\n      val emptyProperties = Map.empty[String, String].asJava\n      val commitMetadata = createCommitMetadata(\n        version = 1,\n        logPath = logPath,\n        committerProperties = () => emptyProperties,\n        readPandMOpt = Optional.of(\n          new KernelTuple2[Protocol, Metadata](\n            protocolWithCatalogManagedSupport,\n            basicPartitionedMetadata)))\n      committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n\n      // ===== THEN =====\n      val updatedTableData = ucClient.getTablesCopy.get(testUcTableId).get\n      val icebergOpt = updatedTableData.getCurrentIcebergOpt\n      assert(icebergOpt.isEmpty)\n    }\n  }\n\n  test(\"CATALOG_WRITE: throws exception when convertedDeltaVersion \" +\n    \"does not match commit version\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n\n      // ===== WHEN =====\n      // Commit version is 1, but convertedDeltaVersion is 2 (mismatch)\n      val icebergProperties = Map(\n        UniformAdapter.ICEBERG_METADATA_LOCATION_KEY -> \"s3://bucket/table/metadata/v2.json\",\n        UniformAdapter.ICEBERG_CONVERTED_DELTA_VERSION_KEY -> \"2\",\n        UniformAdapter.ICEBERG_CONVERTED_DELTA_TIMESTAMP_KEY -> \"2025-01-04T03:13:11.423\").asJava\n\n      val commitMetadata = createCommitMetadata(\n        version = 1,\n        logPath = logPath,\n        committerProperties = () => icebergProperties,\n        readPandMOpt = Optional.of(\n          new KernelTuple2[Protocol, Metadata](\n            protocolWithCatalogManagedSupport,\n            basicPartitionedMetadata)))\n\n      // ===== THEN =====\n      val exception = intercept[IllegalStateException] {\n        committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n      }\n      assert(exception.getMessage.contains(\n        \"Uniform convertedDeltaVersion (2) must match commit version (1)\"))\n    }\n  }\n\n  test(\"CATALOG_WRITE: writes staged commit file and invokes UC client commit API (no P&M change\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n\n      val testValue = \"TEST_COMMIT_DATA_12345\"\n      val actionsIterator = getSingleElementRowIter(testValue)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n      val commitMetadata = catalogManagedWriteCommitMetadata(version = 1, logPath = logPath)\n\n      // ===== WHEN =====\n      val response = committer.commit(defaultEngine, actionsIterator, commitMetadata)\n\n      // ===== THEN =====\n      val stagedCommitFilePath = response.getCommitLogData.getFileStatus.getPath\n\n      // Verify the staged commit file actually exists on disk\n      val file = new java.io.File(new java.net.URI(stagedCommitFilePath))\n      assert(file.exists())\n      assert(file.isFile())\n\n      // Read the file content and verify our test value was written\n      val fileContent = scala.io.Source.fromFile(file).getLines().mkString(\"\\n\")\n      assert(fileContent.contains(testValue))\n\n      // Verify the file is in the correct location\n      val expectedPattern =\n        s\"^file:$tablePath/_delta_log/_staged_commits/00000000000000000001\\\\.[^.]+\\\\.json$$\"\n      assert(stagedCommitFilePath.matches(expectedPattern))\n\n      // Verify UC client was invoked and table was updated.\n      val updatedTable = ucClient.getTablesCopy.get(testUcTableId).get\n      assert(updatedTable.getMaxRatifiedVersion == 1)\n      assert(updatedTable.getCommits.size == 1)\n\n      // Assert that no P&M change in this txn => No P&M change sent to UC\n      assert(updatedTable.getCurrentProtocolOpt.isEmpty)\n      assert(updatedTable.getCurrentMetadataOpt.isEmpty)\n\n      // Verify the new commit in UC has correct version\n      val lastCommit = updatedTable.getCommits.last\n      assert(lastCommit.getVersion == 1)\n      assert(lastCommit.getFileStatus.getPath.toString == stagedCommitFilePath)\n    }\n  }\n\n  test(\"CATALOG_WRITE: IOException writing staged commit => CFE(retryable=true, conflict=false)\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val throwingEngine = mockEngine(jsonHandler = new BaseMockJsonHandler {\n        override def writeJsonFileAtomically(\n            path: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit =\n          throw new IOException(\"Network error\")\n      })\n\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val tableData = new TableData(maxRatifiedVersion = 1, commits = ArrayBuffer.empty[Commit])\n      ucClient.insertTableData(testUcTableId, tableData)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n      val commitMetadata = catalogManagedWriteCommitMetadata(2, logPath = logPath)\n\n      // ===== WHEN =====\n      val ex = intercept[CommitFailedException] {\n        committer.commit(throwingEngine, emptyActionsIterator, commitMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.isRetryable && !ex.isConflict)\n      assert(ex.getMessage.contains(\"Failed to write delta file due to: Network error\"))\n    }\n  }\n\n  test(\"CATALOG_WRITE: i.d.s.c.CommitFailedException during UC commit => kernel CFE\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\") {\n        override def forceThrowInCommitMethod(): Unit =\n          throw new io.delta.storage.commit.CommitFailedException(\n            true, // retryable\n            true, // conflict\n            \"Storage conflict\",\n            null)\n      }\n      val tableData = new TableData(maxRatifiedVersion = 1, commits = ArrayBuffer.empty[Commit])\n      ucClient.insertTableData(testUcTableId, tableData)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n      val commitMetadata = catalogManagedWriteCommitMetadata(2, logPath = logPath)\n      // ===== WHEN =====\n      val ex = intercept[CommitFailedException] {\n        committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.isRetryable && ex.isConflict)\n      assert(ex.getMessage.contains(\"Storage conflict\"))\n    }\n  }\n\n  test(\"CATALOG_WRITE: IOException during UC commit => CFE(retryable=true, conflict=false)\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\") {\n        override def forceThrowInCommitMethod(): Unit = throw new IOException(\"UC network error\")\n      }\n      val tableData = new TableData(maxRatifiedVersion = 1, commits = ArrayBuffer.empty[Commit])\n      ucClient.insertTableData(testUcTableId, tableData)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n      val commitMetadata = catalogManagedWriteCommitMetadata(2, logPath = logPath)\n\n      // ===== WHEN =====\n      val ex = intercept[CommitFailedException] {\n        committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.isRetryable && !ex.isConflict)\n      assert(ex.getMessage.contains(\"UC network error\"))\n    }\n  }\n\n  test(\"CATALOG_WRITE: i.d.s.c.u.UCCCE during UC commit => CFE(retryable=false, conflict=false)\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\") {\n        override def forceThrowInCommitMethod(): Unit = {\n          // A child type of UCCommitCoordinatorException\n          throw new InvalidTargetTableException(\"Target table does not exist\")\n        }\n      }\n      val tableData = new TableData(maxRatifiedVersion = 1, commits = ArrayBuffer.empty[Commit])\n      ucClient.insertTableData(testUcTableId, tableData)\n      val committer = new UCCatalogManagedCommitter(ucClient, \"unknownTableId\", tablePath)\n      val commitMetadata = catalogManagedWriteCommitMetadata(2, logPath = logPath)\n\n      // ===== WHEN =====\n      val ex = intercept[CommitFailedException] {\n        committer.commit(defaultEngine, emptyActionsIterator, commitMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.getCause.isInstanceOf[InvalidTargetTableException])\n      assert(!ex.isRetryable && !ex.isConflict)\n      assert(ex.getMessage.contains(\"Target table does not exist\"))\n    }\n  }\n\n  // ================================================================\n  // ===================== CATALOG_CREATE Tests =====================\n  // ================================================================\n\n  test(\"CATALOG_CREATE: writes published delta file for version 0\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val testValue = \"CREATE_TABLE_DATA_12345\"\n      val actionsIterator = getSingleElementRowIter(testValue)\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n\n      val commitMetadata = createCommitMetadata(\n        version = 0,\n        logPath = logPath,\n        newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n\n      // ===== WHEN =====\n      val response = committer.commit(defaultEngine, actionsIterator, commitMetadata)\n\n      // ===== THEN =====\n      val publishedDeltaFilePath = response.getCommitLogData.getFileStatus.getPath\n\n      // Verify the published delta file exists and is version 0\n      val expectedFilePath = s\"file:$logPath/00000000000000000000.json\"\n      assert(publishedDeltaFilePath == expectedFilePath)\n\n      val file = new java.io.File(new java.net.URI(publishedDeltaFilePath))\n      assert(file.exists())\n      assert(file.isFile())\n\n      // Read the file content and verify our test value was written\n      val fileContent = scala.io.Source.fromFile(file).getLines().mkString(\"\\n\")\n      assert(fileContent.contains(testValue))\n\n      // Validate that UC was not updated for v0\n      // TODO: [delta-io/delta#5118] If UC changes CREATE semantics, update logic here.\n      assert(!ucClient.getTablesCopy.contains(testUcTableId))\n    }\n  }\n\n  test(\"CATALOG_CREATE: IOException during write throws CFE(retryable=true, conflict=false)\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val throwingEngine = mockEngine(jsonHandler = new BaseMockJsonHandler {\n        override def writeJsonFileAtomically(\n            path: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit =\n          throw new IOException(\"Network hiccup\")\n      })\n      val committer = new UCCatalogManagedCommitter(ucClient, testUcTableId, tablePath)\n\n      val commitMetadata = createCommitMetadata(\n        version = 0,\n        logPath = logPath,\n        newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n        newMetadataOpt = Optional.of(basicPartitionedMetadata))\n\n      // ===== WHEN =====\n      val ex = intercept[CommitFailedException] {\n        committer.commit(throwingEngine, emptyActionsIterator, commitMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.isRetryable && !ex.isConflict)\n      assert(ex.getMessage.contains(\"Failed to write delta file due to: Network hiccup\"))\n    }\n  }\n\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UCCatalogManagedTestUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.lang.{Long => JLong}\nimport java.net.URI\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\nimport scala.reflect.ClassTag\n\nimport io.delta.kernel.commit.{CommitMetadata, PublishMetadata}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.defaults.utils.{TestUtils, WriteUtils}\nimport io.delta.kernel.engine.{Engine, MetricsReporter}\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.actions.{Metadata, Protocol}\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData\nimport io.delta.kernel.internal.util.{Tuple2 => KernelTuple2}\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.internal.util.Utils.singletonCloseableIterator\nimport io.delta.kernel.metrics.MetricsReport\nimport io.delta.kernel.test.{ActionUtils, TestFixtures}\nimport io.delta.kernel.utils.CloseableIterator\nimport io.delta.storage.commit.{Commit, GetCommitsResponse}\n\nimport InMemoryUCClient.TableData\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus => HadoopFileStatus, FileSystem, Path}\n\ntrait UCCatalogManagedTestUtils\n    extends TestUtils\n    with ActionUtils\n    with TestFixtures\n    with WriteUtils {\n\n  val fakeURI = new URI(\"s3://bucket/table\")\n  val baseTestTablePath = \"/path/to/table\"\n  val baseTestLogPath = \"/path/to/table/_delta_log\"\n  val emptyLongOpt = Optional.empty[java.lang.Long]()\n\n  /**\n   * Generic MetricsReporter that captures specific types of MetricsReport instances.\n   * This can be used for both UcCommitTelemetry.Report and UcPublishTelemetry.Report.\n   *\n   * @tparam T the type of MetricsReport to capture\n   */\n  class CapturingMetricsReporter[T <: MetricsReport: ClassTag] extends MetricsReporter {\n    val reports = ArrayBuffer[T]()\n\n    override def report(report: MetricsReport): Unit = {\n      report match {\n        case r: T => reports.append(r)\n        case _ => // Ignore other report types\n      }\n    }\n  }\n\n  /** Creates an Engine with a custom MetricsReporter for testing telemetry */\n  def createEngineWithMetricsCapture(reporter: MetricsReporter): Engine = {\n    val hadoopConf = new Configuration()\n    new DefaultEngine(\n      new io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO(hadoopConf)) {\n      override def getMetricsReporters: java.util.List[MetricsReporter] = {\n        val reporters = new java.util.ArrayList[MetricsReporter]()\n        reporters.add(reporter)\n        reporters\n      }\n    }\n  }\n\n  /** Helper method with reasonable defaults */\n  def loadSnapshot(\n      ucCatalogManagedClient: UCCatalogManagedClient,\n      engine: Engine = defaultEngine,\n      ucTableId: String = \"testUcTableId\",\n      tablePath: String = \"testUcTablePath\",\n      versionToLoad: Optional[java.lang.Long] = emptyLongOpt,\n      timestampToLoad: Optional[java.lang.Long] = emptyLongOpt): SnapshotImpl = {\n    ucCatalogManagedClient.loadSnapshot(\n      engine,\n      ucTableId,\n      tablePath,\n      versionToLoad,\n      timestampToLoad).asInstanceOf[SnapshotImpl]\n  }\n\n  def hadoopCommitFileStatus(version: Long): HadoopFileStatus = {\n    val filePath = FileNames.stagedCommitFile(baseTestLogPath, version)\n\n    new HadoopFileStatus(\n      version, /* length */\n      false, /* isDir */\n      version.toInt, /* blockReplication */\n      version, /* blockSize */\n      version, /* modificationTime */\n      new Path(filePath))\n  }\n\n  def createCommit(version: Long): Commit = {\n    new Commit(version, hadoopCommitFileStatus(version), version) // version, fileStatus, timestamp\n  }\n\n  /** Creates an InMemoryUCClient with the given tableId and commits for the specified versions. */\n  def getInMemoryUCClientWithCommitsForTableId(\n      tableId: String,\n      versions: Seq[Long]): InMemoryUCClient = {\n    val client = new InMemoryUCClient(\"ucMetastoreId\")\n    versions.foreach { v =>\n      client.commitWithDefaults(tableId, fakeURI, Optional.of(createCommit(v)))\n    }\n    client\n  }\n\n  def createPublishMetadata(\n      snapshotVersion: Long,\n      logPath: String,\n      catalogCommits: List[ParsedCatalogCommitData]): PublishMetadata = {\n    new PublishMetadata(snapshotVersion, logPath, catalogCommits.asJava)\n  }\n\n  def getSingleElementRowIter(elem: String): CloseableIterator[Row] = {\n    import io.delta.kernel.defaults.integration.DataBuilderUtils\n    import io.delta.kernel.types.{StringType, StructField, StructType}\n\n    val schema = new StructType().add(new StructField(\"testColumn\", StringType.STRING, true))\n    val simpleRow = DataBuilderUtils.row(schema, elem)\n    singletonCloseableIterator(simpleRow)\n  }\n\n  /** Creates a UCCatalogManagedClient with an InMemoryUCClient for testing */\n  def createUCClientAndCatalogManagedClient(\n      metastoreId: String = \"ucMetastoreId\"): (InMemoryUCClient, UCCatalogManagedClient) = {\n    val ucClient = new InMemoryUCClient(metastoreId)\n    val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n    (ucClient, ucCatalogManagedClient)\n  }\n\n  /** Version TS for the test table used in [[withUCClientAndTestTable]] */\n  val v0Ts = 1749830855993L // published commit\n  val v1Ts = 1749830871085L // ratified staged commit\n  val v2Ts = 1749830881799L // ratified staged commit\n\n  /**\n   * @param textFx test function to run that takes input (ucClient, tablePath, maxRatifiedVersion)\n   */\n  def withUCClientAndTestTable(\n      textFx: (InMemoryUCClientWithMetrics, String, Long) => Unit): Unit = {\n    val maxRatifiedVersion = 2L\n    val tablePath = getTestResourceFilePath(\"catalog-owned-preview\")\n    val ucClient = new InMemoryUCClientWithMetrics(\"ucMetastoreId\")\n    val fs = FileSystem.get(new Configuration())\n    val catalogCommits = Seq(\n      // scalastyle:off line.size.limit\n      getTestResourceFilePath(\"catalog-owned-preview/_delta_log/_staged_commits/00000000000000000001.4cb9708e-b478-44de-b203-53f9ba9b2876.json\"),\n      getTestResourceFilePath(\"catalog-owned-preview/_delta_log/_staged_commits/00000000000000000002.5b9bba4a-0085-430d-a65e-b0d38c1afbe9.json\"))\n      // scalastyle:on line.size.limit\n      .map { path => fs.getFileStatus(new Path(path)) }\n      .map { fileStatus =>\n        new Commit(\n          FileNames.deltaVersion(fileStatus.getPath.toString),\n          fileStatus,\n          fileStatus.getModificationTime)\n      }\n    val tableData = new TableData(maxRatifiedVersion, ArrayBuffer(catalogCommits: _*))\n    ucClient.insertTableData(\"testUcTableId\", tableData)\n    textFx(ucClient, tablePath, maxRatifiedVersion)\n  }\n\n  def createUCCatalogManagedClientForTableAfterCreate(\n      ucTableId: String = \"testUcTableId\"): UCCatalogManagedClient = {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    ucClient.insertTableDataAfterCreate(ucTableId)\n    new UCCatalogManagedClient(ucClient)\n  }\n\n  /** This should be used for WRITE operations (version >= 1), not for CREATE. */\n  def catalogManagedWriteCommitMetadata(\n      version: Long,\n      logPath: String = baseTestLogPath): CommitMetadata = createCommitMetadata(\n    version = version,\n    logPath = logPath,\n    readPandMOpt = Optional.of(\n      new KernelTuple2[Protocol, Metadata](\n        protocolWithCatalogManagedSupport,\n        basicPartitionedMetadata)))\n\n  /** Wrapper class around InMemoryUCClient that tracks number of getCommit calls made */\n  class InMemoryUCClientWithMetrics(ucMetastoreId: String) extends InMemoryUCClient(ucMetastoreId) {\n    private var numGetCommitsCalls: Long = 0\n\n    override def getCommits(\n        tableId: String,\n        tableUri: URI,\n        startVersion: Optional[JLong],\n        endVersion: Optional[JLong]): GetCommitsResponse = {\n      numGetCommitsCalls += 1\n      super.getCommits(tableId, tableUri, startVersion, endVersion)\n    }\n\n    def getNumGetCommitCalls: Long = numGetCommitsCalls\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UCE2ESuite.scala",
    "content": "/*\n * Copyright (2023) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.{CommitRange, Operation}\nimport io.delta.kernel.Snapshot\nimport io.delta.kernel.Snapshot.ChecksumWriteMode\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.unitycatalog.UCCatalogManagedCommitter\nimport io.delta.kernel.utils.CloseableIterable\nimport io.delta.storage.commit.{Commit, GetCommitsResponse}\n\nimport InMemoryUCClient.TableData\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UCE2ESuite extends AnyFunSuite with UCCatalogManagedTestUtils {\n\n  import UCE2ESuite._\n\n  private val testUcTableId = \"testUcTableId\"\n\n  /** Commits some data. Verifies UC is updated as expected. Returns the post-commit snapshot. */\n  private def writeDataAndVerify(\n      engine: Engine,\n      snapshot: Snapshot,\n      ucClient: InMemoryUCClient,\n      expCommitVersion: Long,\n      expNumCatalogCommits: Long): Snapshot = {\n    val txn = snapshot\n      .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n      .build(engine)\n    val result = commitAppendData(engine, txn, seqOfUnpartitionedDataBatch1)\n    val tableData = ucClient.getTableDataElseThrow(testUcTableId)\n    assert(tableData.getMaxRatifiedVersion === expCommitVersion)\n    assert(tableData.getCommits.size === expNumCatalogCommits)\n    result.getPostCommitSnapshot.get()\n  }\n\n  test(\"simple case: create, write, publish, load\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n      // Step 1: CREATE -- v0.json\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable() /* dataActions */ )\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n      result0.getPostCommitSnapshot.get().publish(engine) // Should be no-op!\n\n      // Step 2: WRITE -- v1.uuid.json\n      val postCommitSnapshot1 = writeDataAndVerify(\n        engine,\n        result0.getPostCommitSnapshot.get(),\n        ucClient,\n        expCommitVersion = 1,\n        expNumCatalogCommits = 1)\n\n      // Step 3: WRITE -- v2.uuid.json\n      val postCommitSnapshot2 = writeDataAndVerify(\n        engine,\n        postCommitSnapshot1,\n        ucClient,\n        expCommitVersion = 2,\n        expNumCatalogCommits = 2)\n\n      // Step 4a: PUBLISH v1.json and v2.json -- Note that this does NOT update UC\n      postCommitSnapshot2.publish(engine)\n\n      // Step 4b: VERIFY UC is unchanged by the publish operation\n      val tableData2 = ucClient.getTableDataElseThrow(testUcTableId)\n      assert(tableData2.getMaxRatifiedVersion === 2)\n      assert(tableData2.getCommits.size === 2)\n      postCommitSnapshot2.publish(engine) // idempotent! shouldn't throw\n\n      // Step 5: WRITE -- v3.uuid.json\n      // Even though v1.json and v2.json are published, snapshotV2 will still have v1.uuid.json and\n      // v2.uuid.json in its LogSegment (since catalog commits take priority). Nonetheless, it will\n      // see that v2 is the maxKnownPublishedDeltaVersion. It will include this information in its\n      // next commit, and UC will then clean up catalog commits v1.uuid.json and v2.uuid.json.\n      val snapshotV2 = loadSnapshot(ucCatalogManagedClient, engine, testUcTableId, tablePath)\n      val logSegmentV2 = snapshotV2.getLogSegment\n      assert(logSegmentV2.getAllCatalogCommits.asScala.map(x => x.getVersion) === Seq(1, 2))\n      assert(logSegmentV2.getMaxPublishedDeltaVersion.get() === 2)\n      writeDataAndVerify(\n        engine,\n        snapshotV2,\n        ucClient,\n        expCommitVersion = 3,\n        expNumCatalogCommits = 1 // just v3.uuid.json, since v1 and v2 are cleaned up\n      )\n\n      // Step 6: LOAD -- should read v0.json, v1.json, v2.json, and v3.uuid.json\n      val snapshotV3 = loadSnapshot(ucCatalogManagedClient, engine, testUcTableId, tablePath)\n      val logSegmentV3 = snapshotV3.getLogSegment\n      assert(snapshotV3.getVersion === 3)\n      assert(logSegmentV3.getAllCatalogCommits.asScala.map(x => x.getVersion) === Seq(3))\n      assert(logSegmentV3.getMaxPublishedDeltaVersion.get() === 2)\n    }\n  }\n\n  test(\"post-publish snapshot is similar to the actual snapshot\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n      // Step 1: CREATE -- v0.json\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable() /* dataActions */ )\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n      result0.getPostCommitSnapshot.get().publish(engine) // Should be no-op!\n\n      // Step 2: WRITE -- v1.uuid.json\n      val postCommitSnapshot1 = writeDataAndVerify(\n        engine,\n        result0.getPostCommitSnapshot.get(),\n        ucClient,\n        expCommitVersion = 1,\n        expNumCatalogCommits = 1)\n\n      // Step 3: WRITE -- v2.uuid.json\n      val postCommitSnapshot2 = writeDataAndVerify(\n        engine,\n        postCommitSnapshot1,\n        ucClient,\n        expCommitVersion = 2,\n        expNumCatalogCommits = 2)\n\n      // Step 4a: PUBLISH v1.json and v2.json -- Note that this does NOT update UC\n      val postPublishSnapshot = postCommitSnapshot2.publish(engine).asInstanceOf[SnapshotImpl]\n      assert(postCommitSnapshot2.getVersion == 2)\n      assert(postCommitSnapshot2.asInstanceOf[SnapshotImpl]\n        .getLogSegment.getMaxPublishedDeltaVersion == Optional.of(0L))\n\n      // All versions will be published in the post publish snapshot\n      assert(postPublishSnapshot.getVersion == 2)\n      assert(postPublishSnapshot.getLogSegment.getMaxPublishedDeltaVersion == Optional.of(2L))\n\n      // Step 5: Read the latest snapshot from disk. Post-publish snapshot should be similar to it\n      val snapshotV2 = loadSnapshot(ucCatalogManagedClient, engine, testUcTableId, tablePath)\n\n      assert(postPublishSnapshot.getVersion == snapshotV2.getVersion)\n      assert(postPublishSnapshot.getPath == snapshotV2.getPath)\n      assert(postPublishSnapshot.getLogPath == snapshotV2.getLogPath)\n      assert(postPublishSnapshot.getTimestamp(engine) == snapshotV2.getTimestamp(engine))\n      assert(postPublishSnapshot.getCommitter.isInstanceOf[UCCatalogManagedCommitter])\n      assert(postPublishSnapshot.getActiveDomainMetadataMap ==\n        snapshotV2.getActiveDomainMetadataMap)\n      assert(postPublishSnapshot.getSchema.equivalent(snapshotV2.getSchema))\n      assert(postPublishSnapshot.getMetadata == snapshotV2.getMetadata)\n      assert(postPublishSnapshot.getProtocol == snapshotV2.getProtocol)\n      assert(postPublishSnapshot.getPartitionColumnNames == snapshotV2.getPartitionColumnNames)\n\n      val postPublishLogSegment = postPublishSnapshot.getLogSegment\n      val logSegmentV2 = snapshotV2.getLogSegment\n      assert(logSegmentV2.getAllCatalogCommits.asScala.map(x => x.getVersion) === Seq(1, 2))\n      assert(logSegmentV2.getMaxPublishedDeltaVersion.get() === 2)\n\n      // Step 5: Use postPublish snapshot to write -- v3.uuid.json\n      writeDataAndVerify(\n        engine,\n        postPublishSnapshot,\n        ucClient,\n        expCommitVersion = 3,\n        expNumCatalogCommits = 1)\n\n      // Step 6: LOAD -- should read v0.json, v1.json, v2.json, and v3.uuid.json\n      val snapshotV3 = loadSnapshot(ucCatalogManagedClient, engine, testUcTableId, tablePath)\n      val logSegmentV3 = snapshotV3.getLogSegment\n      assert(snapshotV3.getVersion === 3)\n      assert(logSegmentV3.getAllCatalogCommits.asScala.map(x => x.getVersion) === Seq(3))\n      assert(logSegmentV3.getMaxPublishedDeltaVersion.get() === 2)\n    }\n  }\n\n  test(\"can load snapshot for table with CRC files for unpublished versions\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      // ===== GIVEN =====\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n      // CREATE -- v0.json\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n\n      var currentSnapshot = result0.getPostCommitSnapshot.get()\n\n      // INSERT -- Empty commits with CRC generation\n      for (_ <- 1 to 3) {\n        val txn = currentSnapshot\n          .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n          .build(engine)\n        val result = txn.commit(engine, CloseableIterable.emptyIterable())\n        currentSnapshot = result.getPostCommitSnapshot.get()\n        currentSnapshot.writeChecksum(engine, ChecksumWriteMode.SIMPLE)\n      }\n\n      // ===== WHEN =====\n      val freshSnapshot = loadSnapshot(ucCatalogManagedClient, engine, testUcTableId, tablePath)\n\n      // ===== THEN =====\n      val logSegment = freshSnapshot.getLogSegment\n\n      assert(freshSnapshot.getVersion === 3)\n      assert(logSegment.getAllCatalogCommits.asScala.map(_.getVersion) === Seq(1, 2, 3))\n      assert(logSegment.getMaxPublishedDeltaVersion.get() === 0)\n\n      val checksumVersion = FileNames.checksumVersion(logSegment.getLastSeenChecksum.get.getPath)\n      assert(checksumVersion === 3)\n    }\n  }\n\n  test(\"don't read versions past maxCatalogVersion even if they exist on filesystem\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n\n      val ucClient = new ConfigurableMaxVersionUCClient()\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n      // Step 1: CREATE -- v0.json\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n\n      // Step 2: WRITE and commit data up to version 2\n      val postCommitSnapshot1 = writeDataAndVerify(\n        engine,\n        result0.getPostCommitSnapshot.get(),\n        ucClient,\n        expCommitVersion = 1,\n        expNumCatalogCommits = 1)\n\n      val postCommitSnapshot2 = writeDataAndVerify(\n        engine,\n        postCommitSnapshot1,\n        ucClient,\n        expCommitVersion = 2,\n        expNumCatalogCommits = 2)\n\n      // Step 3: PUBLISH v1.json and v2.json to the filesystem\n      postCommitSnapshot2.publish(engine)\n\n      // Step 4: Configure the UC client to limit maxRatifiedVersion to 1\n      ucClient.setMaxVersionLimit(1)\n\n      // Step 5: Load snapshot with UC client that limits maxRatifiedVersion to 1\n      val snapshot = loadSnapshot(ucCatalogManagedClient, engine, testUcTableId, tablePath)\n\n      // Step 6: Verify that snapshot is at version 1, not version 2\n      assert(\n        snapshot.getVersion === 1,\n        \"Snapshot should be at version 1, not reading beyond maxCatalogVersion\")\n\n      // Verify the log segment only contains commits up to version 1\n      assert(\n        snapshot.getLogSegment.getMaxPublishedDeltaVersion.get() === 1,\n        \"Should recognize published version 1 but not go beyond it\")\n    }\n  }\n\n  test(\"CommitRange respects maxCatalogVersion\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val ucClient = new ConfigurableMaxVersionUCClient()\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n\n      // Step 1: CREATE -- v0.json\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n      ucClient.insertTableDataAfterCreate(testUcTableId)\n\n      // Step 2: WRITE multiple versions\n      val postCommitSnapshot1 = writeDataAndVerify(\n        engine,\n        result0.getPostCommitSnapshot.get(),\n        ucClient,\n        expCommitVersion = 1,\n        expNumCatalogCommits = 1)\n\n      val postCommitSnapshot2 = writeDataAndVerify(\n        engine,\n        postCommitSnapshot1,\n        ucClient,\n        expCommitVersion = 2,\n        expNumCatalogCommits = 2)\n\n      val postCommitSnapshot3 = writeDataAndVerify(\n        engine,\n        postCommitSnapshot2,\n        ucClient,\n        expCommitVersion = 3,\n        expNumCatalogCommits = 3)\n\n      // Step 3: Publish all versions\n      postCommitSnapshot3.publish(engine)\n\n      // Step 4: Load CommitRange with end boundary (should go from 0 to 3)\n      val commitRange1: CommitRange = ucCatalogManagedClient.loadCommitRange(\n        engine,\n        testUcTableId,\n        tablePath,\n        Optional.of(0),\n        emptyLongOpt,\n        emptyLongOpt,\n        emptyLongOpt)\n\n      assert(commitRange1.getStartVersion === 0)\n      assert(commitRange1.getEndVersion === 3, \"Should respect maxCatalogVersion of 3\")\n\n      // Step 5: Configure UC client to limit maxRatifiedVersion to 2\n      ucClient.setMaxVersionLimit(2)\n\n      // Step 6: Load CommitRange again (should now be limited to version 2)\n      val commitRange2: CommitRange = ucCatalogManagedClient.loadCommitRange(\n        engine,\n        testUcTableId,\n        tablePath,\n        Optional.of(0),\n        emptyLongOpt,\n        emptyLongOpt,\n        emptyLongOpt)\n\n      assert(commitRange2.getStartVersion === 0)\n      assert(commitRange2.getEndVersion === 2, \"Should respect maxCatalogVersion of 2\")\n\n      // Step 8: Load CommitRange with start version at maxCatalogVersion (should work)\n      val commitRange3: CommitRange = ucCatalogManagedClient.loadCommitRange(\n        engine,\n        testUcTableId,\n        tablePath,\n        Optional.of(2),\n        emptyLongOpt,\n        emptyLongOpt,\n        emptyLongOpt)\n\n      assert(commitRange3.getStartVersion === 2)\n      assert(commitRange3.getEndVersion === 2)\n    }\n  }\n}\n\nobject UCE2ESuite {\n  // Custom UCClient that can configure maxRatifiedVersion for testing withMaxCatalogVersion\n  class ConfigurableMaxVersionUCClient extends InMemoryUCClient(\"ucMetastoreId\") {\n    @volatile private var maxVersionLimit: Option[Long] = None\n\n    def setMaxVersionLimit(limit: Long): Unit = {\n      maxVersionLimit = Some(limit)\n    }\n\n    override def getCommits(\n        tableId: String,\n        tableUri: java.net.URI,\n        startVersion: Optional[java.lang.Long],\n        endVersion: Optional[java.lang.Long]): GetCommitsResponse = {\n      val response = super.getCommits(tableId, tableUri, startVersion, endVersion)\n      maxVersionLimit match {\n        case Some(limit) =>\n          // Filter commits and limit maxRatifiedVersion\n          val filteredCommits = response.getCommits.asScala.filter(_.getVersion <= limit)\n          new GetCommitsResponse(filteredCommits.asJava, limit)\n        case None =>\n          response\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UCPublishingSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.io.IOException\n\nimport io.delta.kernel.commit.PublishFailedException\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData\nimport io.delta.kernel.internal.util.FileNames\nimport io.delta.kernel.test.{BaseMockFileSystemClient, MockFileSystemClientUtils, TestFixtures, VectorTestUtils}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UCPublishingSuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils\n    with TestFixtures\n    with VectorTestUtils\n    with MockFileSystemClientUtils {\n\n  private def createCommitter(tablePath: String): UCCatalogManagedCommitter = {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    new UCCatalogManagedCommitter(ucClient, \"testUcTableId\", tablePath)\n  }\n\n  private def toFile(path: String): java.io.File = {\n    if (path.startsWith(\"file:\")) {\n      new java.io.File(new java.net.URI(path))\n    } else {\n      new java.io.File(path)\n    }\n  }\n\n  private def readFile(path: String): String = {\n    scala.io.Source.fromFile(toFile(path)).getLines().mkString(\"\\n\")\n  }\n\n  private def assertFileExists(path: String): Unit = {\n    assert(toFile(path).exists(), s\"File should exist: $path\")\n  }\n\n  /**\n   * Helper to create a staged commit file and return its ParsedCatalogCommitData.\n   * Just writes the file directly without using the committer.\n   */\n  private def writeStagedCatalogCommit(\n      logPath: String,\n      version: Long,\n      content: String = \"\"): ParsedCatalogCommitData = {\n    val stagedPath = FileNames.stagedCommitFile(logPath, version)\n    defaultEngine.getJsonHandler.writeJsonFileAtomically(\n      stagedPath,\n      getSingleElementRowIter(content),\n      true /* overwrite */ )\n    val fileStatus = defaultEngine.getFileSystemClient.getFileStatus(stagedPath)\n    ParsedCatalogCommitData.forFileStatus(fileStatus)\n  }\n\n  test(\"publish: throws on null inputs\") {\n    val committer = createCommitter(baseTestTablePath)\n    val publishMetadata = createPublishMetadata(\n      snapshotVersion = 1,\n      logPath = baseTestLogPath,\n      catalogCommits = List(createStagedCatalogCommit(1, baseTestLogPath)))\n\n    assertThrows[NullPointerException] {\n      committer.publish(null, publishMetadata)\n    }\n\n    assertThrows[NullPointerException] {\n      committer.publish(defaultEngine, null)\n    }\n  }\n\n  test(\"publish: throws UnsupportedOperationException for inline commits\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val inlineCatalogCommit = ParsedCatalogCommitData.forInlineData(\n        1L,\n        emptyColumnarBatch)\n      val committer = createCommitter(tablePath)\n      val publishMetadata = createPublishMetadata(\n        snapshotVersion = 1,\n        logPath = logPath,\n        catalogCommits = List(inlineCatalogCommit))\n\n      // ===== WHEN =====\n      val ex = intercept[UnsupportedOperationException] {\n        committer.publish(defaultEngine, publishMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.getMessage.contains(\"Publishing inline catalog commits is not yet supported\"))\n    }\n  }\n\n  test(\"publish: multiple catalog commits successfully\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val catalogCommits = List(\n        writeStagedCatalogCommit(logPath, 1, \"COMMIT_V1\"),\n        writeStagedCatalogCommit(logPath, 2, \"COMMIT_V2\"),\n        writeStagedCatalogCommit(logPath, 3, \"COMMIT_V3\"))\n      val committer = createCommitter(tablePath)\n      val publishMetadata = createPublishMetadata(\n        snapshotVersion = 3,\n        logPath = logPath,\n        catalogCommits = catalogCommits)\n\n      // ===== WHEN =====\n      committer.publish(defaultEngine, publishMetadata)\n\n      // ===== THEN =====\n      assert(readFile(FileNames.deltaFile(logPath, 1)).contains(\"COMMIT_V1\"))\n      assert(readFile(FileNames.deltaFile(logPath, 2)).contains(\"COMMIT_V2\"))\n      assert(readFile(FileNames.deltaFile(logPath, 3)).contains(\"COMMIT_V3\"))\n    }\n  }\n\n  test(\"publish: return a published snapshot\") {}\n\n  test(\"publish: does not overwrite existing published files\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val catalogCommit = writeStagedCatalogCommit(logPath, 1, \"TEST_IDEMPOTENT_PUBLISH\")\n      val committer = createCommitter(tablePath)\n      val publishMetadata = createPublishMetadata(\n        snapshotVersion = 1,\n        logPath = logPath,\n        catalogCommits = List(catalogCommit))\n\n      // ===== WHEN =====\n      // Publish once\n      committer.publish(defaultEngine, publishMetadata)\n      val publishedTimestamp1 = defaultEngine\n        .getFileSystemClient.getFileStatus(FileNames.deltaFile(logPath, 1)).getModificationTime\n\n      // Publish again - should succeed but not overwrite existing file\n      committer.publish(defaultEngine, publishMetadata)\n      val publishedTimestamp2 = defaultEngine\n        .getFileSystemClient.getFileStatus(FileNames.deltaFile(logPath, 1)).getModificationTime\n\n      // ===== THEN =====\n      assert(publishedTimestamp1 === publishedTimestamp2)\n    }\n  }\n\n  test(\"publish: creates published file at correct location with identical content\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val catalogCommit = writeStagedCatalogCommit(logPath, 1, \"VERSION_1\")\n      val committer = createCommitter(tablePath)\n      val publishMetadata = createPublishMetadata(\n        snapshotVersion = 1,\n        logPath = logPath,\n        catalogCommits = List(catalogCommit))\n\n      // ===== WHEN =====\n      committer.publish(defaultEngine, publishMetadata)\n\n      // ===== THEN =====\n      val stagedPath = catalogCommit.getFileStatus.getPath\n      val publishedPath = FileNames.deltaFile(logPath, 1)\n\n      // Verify staged file still exists (publish doesn't delete source)\n      assertFileExists(stagedPath)\n\n      // Verify published file exists at correct location\n      assertFileExists(publishedPath)\n      assert(FileNames.isPublishedDeltaFile(publishedPath))\n\n      // Verify content was copied correctly\n      assert(readFile(stagedPath) === readFile(publishedPath))\n    }\n  }\n\n  test(\"publish: throws PublishFailedException on IOException\") {\n    withTempDirAndAllDeltaSubDirs { case (tablePath, logPath) =>\n      // ===== GIVEN =====\n      val catalogCommit = writeStagedCatalogCommit(logPath, 1, \"TEST_EXCEPTION\")\n      val throwingEngine = mockEngine(fileSystemClient = new BaseMockFileSystemClient {\n        override def copyFileAtomically(\n            srcPath: String,\n            destPath: String,\n            overwrite: Boolean): Unit = {\n          throw new IOException(\"Network failure during copy\")\n        }\n      })\n      val committer = createCommitter(tablePath)\n      val publishMetadata = createPublishMetadata(\n        snapshotVersion = 1,\n        logPath = logPath,\n        catalogCommits = List(catalogCommit))\n\n      // ===== WHEN =====\n      val ex = intercept[PublishFailedException] {\n        committer.publish(throwingEngine, publishMetadata)\n      }\n\n      // ===== THEN =====\n      assert(ex.getMessage.contains(\"Failed to publish version 1\"))\n      assert(ex.getMessage.contains(\"Network failure during copy\"))\n      assert(ex.getCause.isInstanceOf[IOException])\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UcCommitTelemetrySuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.util.Optional\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.Operation\nimport io.delta.kernel.commit.{CommitFailedException, CommitMetadata}\nimport io.delta.kernel.data.Row\nimport io.delta.kernel.defaults.engine.DefaultEngine\nimport io.delta.kernel.exceptions.MaxCommitRetryLimitReachedException\nimport io.delta.kernel.test.{BaseMockJsonHandler, MockFileSystemClientUtils}\nimport io.delta.kernel.unitycatalog.InMemoryUCClient.TableData\nimport io.delta.kernel.unitycatalog.metrics.UcCommitTelemetry\nimport io.delta.kernel.utils.{CloseableIterable, CloseableIterator}\nimport io.delta.storage.commit.Commit\n\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UcCommitTelemetrySuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils\n    with MockFileSystemClientUtils {\n\n  test(\"commit metrics for CREATE and WRITE operations\") {\n    withTempDirAndEngine { case (tablePathUnresolved, _) =>\n      val reporter = new CapturingMetricsReporter[UcCommitTelemetry#Report]\n      val engine = createEngineWithMetricsCapture(reporter)\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val (ucClient, ucCatalogManagedClient) = createUCClientAndCatalogManagedClient()\n\n      // CREATE -- v0.json\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(\"testUcTableId\", tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable() /* dataActions */ )\n      ucClient.insertTableDataAfterCreate(\"testUcTableId\")\n\n      // Verify CREATE metrics\n      assert(reporter.reports.size === 1)\n      val createReport = reporter.reports.head\n      assert(createReport.operationType === \"UcCommit\")\n      assert(createReport.ucTableId === \"testUcTableId\")\n      assert(createReport.ucTablePath === tablePath)\n      assert(createReport.commitVersion === 0)\n      assert(createReport.commitType === CommitMetadata.CommitType.CATALOG_CREATE)\n      assert(createReport.exception.isEmpty)\n\n      val createMetrics = createReport.metrics\n      assert(createMetrics.totalCommitDurationNs > 0)\n      assert(createMetrics.writeCommitFileDurationNs > 0)\n      assert(createMetrics.commitToUcServerDurationNs === 0)\n\n      reporter.reports.clear()\n\n      // WRITE -- v1.uuid.json\n      result0\n        .getPostCommitSnapshot\n        .get()\n        .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n\n      // Verify WRITE metrics\n      assert(reporter.reports.size === 1)\n      val writeReport = reporter.reports.head\n      assert(writeReport.operationType === \"UcCommit\")\n      assert(writeReport.ucTableId === \"testUcTableId\")\n      assert(writeReport.ucTablePath === tablePath)\n      assert(writeReport.commitVersion === 1)\n      assert(writeReport.commitType === CommitMetadata.CommitType.CATALOG_WRITE)\n      assert(writeReport.exception.isEmpty)\n\n      val writeMetrics = writeReport.metrics\n      assert(writeMetrics.totalCommitDurationNs > 0)\n      assert(writeMetrics.writeCommitFileDurationNs > 0)\n      assert(writeMetrics.commitToUcServerDurationNs > 0)\n      assert(\n        writeMetrics.totalCommitDurationNs >=\n          writeMetrics.writeCommitFileDurationNs + writeMetrics.commitToUcServerDurationNs)\n      assert(writeReport.reportUUID != createReport.reportUUID)\n    }\n  }\n\n  test(\"telemetry captures exceptions during commit\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      // ===== GIVEN =====\n      val reporter = new CapturingMetricsReporter[UcCommitTelemetry#Report]\n      val throwingJsonHandler = new BaseMockJsonHandler {\n        override def writeJsonFileAtomically(\n            path: String,\n            data: CloseableIterator[Row],\n            overwrite: Boolean): Unit =\n          throw new java.io.IOException(\"Simulated network failure\")\n      }\n      val throwingEngineWithReporter = new DefaultEngine(\n        new io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO(new Configuration())) {\n        override def getJsonHandler = throwingJsonHandler\n        override def getMetricsReporters = java.util.Arrays.asList(reporter)\n      }\n\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val (_, ucCatalogManagedClient) = createUCClientAndCatalogManagedClient()\n\n      // ===== WHEN =====\n      intercept[MaxCommitRetryLimitReachedException] {\n        ucCatalogManagedClient\n          .buildCreateTableTransaction(\"testUcTableId\", tablePath, testSchema, \"test-engine\")\n          .withMaxRetries(0)\n          .build(throwingEngineWithReporter)\n          .commit(throwingEngineWithReporter, CloseableIterable.emptyIterable())\n      }\n\n      // ===== THEN =====\n      assert(reporter.reports.size === 1)\n      val report = reporter.reports.head\n      assert(report.operationType === \"UcCommit\")\n      assert(report.ucTableId === \"testUcTableId\")\n      assert(report.commitVersion === 0)\n      assert(report.commitType === CommitMetadata.CommitType.CATALOG_CREATE)\n      assert(report.exception.isPresent)\n      val exceptionString = report.exception.get().toString\n      assert(exceptionString.contains(\"CommitFailedException\"))\n      assert(exceptionString.contains(\"Simulated network failure\"))\n    }\n  }\n\n  test(\"JSON serialization: success + create (version == 0)\") {\n    val commitMetadata = createCommitMetadata(\n      version = 0,\n      logPath = baseTestLogPath,\n      readPandMOpt = Optional.empty(),\n      newProtocolOpt = Optional.of(protocolWithCatalogManagedSupport),\n      newMetadataOpt = Optional.of(basicPartitionedMetadata))\n\n    val telemetry = new UcCommitTelemetry(\"testUcTableId\", \"ucTablePath\", commitMetadata)\n    telemetry.getMetricsCollector.totalCommitTimer.record(200)\n    telemetry.getMetricsCollector.writeCommitFileTimer.record(200)\n    // Note: commitToUcServerTimer is not invoked for CREATE operations\n\n    val report = telemetry.createSuccessReport()\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcCommit\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"commitVersion\":0,\n         |\"commitType\":\"CATALOG_CREATE\",\n         |\"metrics\":{\"totalCommitDurationNs\":200,\"writeCommitFileDurationNs\":200,\"commitToUcServerDurationNs\":0},\n         |\"exception\":null}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n\n  test(\"JSON serialization: success + update (version >= 1)\") {\n    val commitMetadata = catalogManagedWriteCommitMetadata(version = 5)\n\n    val telemetry = new UcCommitTelemetry(\"testUcTableId\", \"ucTablePath\", commitMetadata)\n    telemetry.getMetricsCollector.totalCommitTimer.record(300)\n    telemetry.getMetricsCollector.writeCommitFileTimer.record(200)\n    telemetry.getMetricsCollector.commitToUcServerTimer.record(100)\n\n    val report = telemetry.createSuccessReport()\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcCommit\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"commitVersion\":5,\n         |\"commitType\":\"CATALOG_WRITE\",\n         |\"metrics\":{\"totalCommitDurationNs\":300,\"writeCommitFileDurationNs\":200,\"commitToUcServerDurationNs\":100},\n         |\"exception\":null}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n\n  test(\"JSON serialization: fail + update\") {\n    val commitMetadata = catalogManagedWriteCommitMetadata(version = 3)\n\n    val telemetry = new UcCommitTelemetry(\"testUcTableId\", \"ucTablePath\", commitMetadata)\n    telemetry.getMetricsCollector.totalCommitTimer.record(300)\n    telemetry.getMetricsCollector.writeCommitFileTimer.record(200)\n    telemetry.getMetricsCollector.commitToUcServerTimer.record(100)\n\n    val exception = new CommitFailedException(false, false, \"errMsg\") // notRetryable, notConflict\n    val report = telemetry.createFailureReport(exception)\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcCommit\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"commitVersion\":3,\n         |\"commitType\":\"CATALOG_WRITE\",\n         |\"metrics\":{\"totalCommitDurationNs\":300,\"writeCommitFileDurationNs\":200,\"commitToUcServerDurationNs\":100},\n         |\"exception\":\"io.delta.kernel.commit.CommitFailedException: retryable=false, conflict=false, msg=errMsg\"}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UcLoadSnapshotTelemetrySuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport java.util.Optional\n\nimport io.delta.kernel.engine.Engine\nimport io.delta.kernel.test.MockFileSystemClientUtils\nimport io.delta.kernel.unitycatalog.metrics.UcLoadSnapshotTelemetry\nimport io.delta.kernel.utils.CloseableIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UcLoadSnapshotTelemetrySuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils\n    with MockFileSystemClientUtils {\n\n  /**\n   * Helper to set up a table with v0 published and v1, v2 as staged commits.\n   *\n   * @return timestamp between v1 and v2 creation\n   */\n  private def setupTableWithCommits(\n      engine: Engine,\n      tablePath: String,\n      ucClient: InMemoryUCClient,\n      ucCatalogManagedClient: UCCatalogManagedClient): Long = {\n    val result0 = ucCatalogManagedClient\n      .buildCreateTableTransaction(\"testUcTableId\", tablePath, testSchema, \"test-engine\")\n      .build(engine)\n      .commit(engine, CloseableIterable.emptyIterable())\n\n    ucClient.insertTableDataAfterCreate(\"testUcTableId\")\n\n    val result1 = result0.getPostCommitSnapshot.get()\n      .buildUpdateTableTransaction(\"engineInfo\", io.delta.kernel.Operation.MANUAL_UPDATE)\n      .build(engine)\n      .commit(engine, CloseableIterable.emptyIterable())\n\n    val timestampBetweenV1AndV2 = System.currentTimeMillis()\n\n    Thread.sleep(100) // Ensure v2 timestamp is sufficiently after our captured timestamp\n\n    val result2 = result1.getPostCommitSnapshot.get()\n      .buildUpdateTableTransaction(\"engineInfo\", io.delta.kernel.Operation.MANUAL_UPDATE)\n      .build(engine)\n      .commit(engine, CloseableIterable.emptyIterable())\n\n    timestampBetweenV1AndV2\n  }\n\n  test(\"snapshot loading metrics for latest version\") {\n    withTempDirAndEngine { case (tablePathUnresolved, _) =>\n      // ===== GIVEN =====\n      val reporter = new CapturingMetricsReporter[UcLoadSnapshotTelemetry#Report]\n      val engine = createEngineWithMetricsCapture(reporter)\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val (ucClient, ucCatalogManagedClient) = createUCClientAndCatalogManagedClient()\n      setupTableWithCommits(engine, tablePath, ucClient, ucCatalogManagedClient)\n      reporter.reports.clear()\n\n      // ===== WHEN =====\n      ucCatalogManagedClient.loadSnapshot(\n        engine,\n        \"testUcTableId\",\n        tablePath,\n        Optional.empty(),\n        Optional.empty())\n\n      // ===== THEN =====\n      assert(reporter.reports.size === 1)\n      val report = reporter.reports.head\n      assert(report.operationType === \"UcLoadSnapshot\")\n      assert(report.ucTableId === \"testUcTableId\")\n      assert(report.ucTablePath === tablePath)\n      assert(report.versionOpt.isEmpty)\n      assert(report.timestampOpt.isEmpty)\n      assert(!report.exception.isPresent)\n\n      val metrics = report.metrics\n      assert(metrics.totalLoadSnapshotDurationNs > 0)\n      assert(metrics.getCommitsDurationNs > 0)\n      assert(metrics.numCatalogCommits === 2) // v1.uuid.json and v2.uuid.json\n      assert(metrics.kernelSnapshotBuildDurationNs > 0)\n      assert(metrics.loadLatestSnapshotForTimestampTimeTravelDurationNs === 0)\n      assert(metrics.resolvedSnapshotVersion === 2) // v2 is the latest\n      assert(\n        metrics.totalLoadSnapshotDurationNs >=\n          metrics.getCommitsDurationNs + metrics.kernelSnapshotBuildDurationNs)\n    }\n  }\n\n  test(\"snapshot loading metrics with timestamp\") {\n    withTempDirAndEngine { case (tablePathUnresolved, _) =>\n      // ===== GIVEN =====\n      val reporter = new CapturingMetricsReporter[UcLoadSnapshotTelemetry#Report]\n      val engine = createEngineWithMetricsCapture(reporter)\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val (ucClient, ucCatalogManagedClient) = createUCClientAndCatalogManagedClient()\n\n      val timestampBetweenV1AndV2 =\n        setupTableWithCommits(engine, tablePath, ucClient, ucCatalogManagedClient)\n      reporter.reports.clear()\n\n      // Time travel to timestamp between v1 and v2 - should resolve to v1\n      ucCatalogManagedClient.loadSnapshot(\n        engine,\n        \"testUcTableId\",\n        tablePath,\n        Optional.empty(),\n        Optional.of(timestampBetweenV1AndV2))\n\n      // Verify snapshot loading metrics\n      assert(reporter.reports.size === 1)\n      val report = reporter.reports.head\n      assert(report.operationType === \"UcLoadSnapshot\")\n      assert(report.ucTableId === \"testUcTableId\")\n      assert(report.ucTablePath === tablePath)\n      assert(report.versionOpt.isEmpty)\n      assert(report.timestampOpt.isPresent)\n      assert(report.timestampOpt.get() === timestampBetweenV1AndV2)\n      assert(!report.exception.isPresent)\n\n      val metrics = report.metrics\n      assert(metrics.totalLoadSnapshotDurationNs > 0)\n      assert(metrics.getCommitsDurationNs > 0)\n      assert(metrics.numCatalogCommits === 2) // v1.uuid.json and v2.uuid.json from loading latest\n      assert(metrics.kernelSnapshotBuildDurationNs > 0)\n      assert(metrics.loadLatestSnapshotForTimestampTimeTravelDurationNs > 0)\n      assert(metrics.resolvedSnapshotVersion === 1) // v1, since timestamp is between v1 and v2\n    }\n  }\n\n  test(\"telemetry captures exceptions during snapshot loading\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val reporter = new CapturingMetricsReporter[UcLoadSnapshotTelemetry#Report]\n      val engineWithReporter = new io.delta.kernel.defaults.engine.DefaultEngine(\n        new io.delta.kernel.defaults.engine.hadoopio.HadoopFileIO(\n          new org.apache.hadoop.conf.Configuration())) {\n        override def getMetricsReporters = java.util.Arrays.asList(reporter)\n      }\n\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val (_, ucCatalogManagedClient) = createUCClientAndCatalogManagedClient()\n\n      intercept[RuntimeException] {\n        // Try to load snapshot from non-existent table\n        ucCatalogManagedClient.loadSnapshot(\n          engineWithReporter,\n          \"nonExistentTableId\",\n          tablePath,\n          Optional.empty(),\n          Optional.empty())\n      }\n\n      assert(reporter.reports.size === 1)\n      val report = reporter.reports.head\n      assert(report.operationType === \"UcLoadSnapshot\")\n      assert(report.ucTableId === \"nonExistentTableId\")\n      assert(report.exception.isPresent)\n      assert(report.metrics.numCatalogCommits === -1) // Not set due to failure\n      assert(report.metrics.resolvedSnapshotVersion === -1) // Not set due to failure\n    }\n  }\n\n  test(\"JSON serialization: success report for latest version\") {\n    val telemetry = new UcLoadSnapshotTelemetry(\n      \"testUcTableId\",\n      \"ucTablePath\",\n      Optional.empty(), // versionOpt\n      Optional.empty() // timestampOpt\n    )\n\n    telemetry.getMetricsCollector.totalSnapshotLoadTimer.record(500)\n    telemetry.getMetricsCollector.getCommitsTimer.record(200)\n    telemetry.getMetricsCollector.kernelSnapshotBuildTimer.record(250)\n    telemetry.getMetricsCollector.setNumCatalogCommits(5)\n    telemetry.getMetricsCollector.setResolvedSnapshotVersion(3)\n\n    val report = telemetry.createSuccessReport()\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcLoadSnapshot\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"versionOpt\":null,\n         |\"timestampOpt\":null,\n         |\"metrics\":{\"totalLoadSnapshotDurationNs\":500,\"getCommitsDurationNs\":200,\"numCatalogCommits\":5,\"kernelSnapshotBuildDurationNs\":250,\"loadLatestSnapshotForTimestampTimeTravelDurationNs\":0,\"resolvedSnapshotVersion\":3},\n         |\"exception\":null}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n\n  test(\"JSON serialization: failure report\") {\n    val telemetry = new UcLoadSnapshotTelemetry(\n      \"testUcTableId\",\n      \"ucTablePath\",\n      Optional.empty(), // versionOpt\n      Optional.of(123456789L) // timestampOpt\n    )\n\n    telemetry.getMetricsCollector.totalSnapshotLoadTimer.record(100)\n    telemetry.getMetricsCollector.getCommitsTimer.record(100)\n\n    val exception = new RuntimeException(\"Failed to load snapshot\")\n    val report = telemetry.createFailureReport(exception)\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcLoadSnapshot\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"versionOpt\":null,\n         |\"timestampOpt\":123456789,\n         |\"metrics\":{\"totalLoadSnapshotDurationNs\":100,\"getCommitsDurationNs\":100,\"numCatalogCommits\":-1,\"kernelSnapshotBuildDurationNs\":0,\"loadLatestSnapshotForTimestampTimeTravelDurationNs\":0,\"resolvedSnapshotVersion\":-1},\n         |\"exception\":\"java.lang.RuntimeException: Failed to load snapshot\"}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UcPublishTelemetrySuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport io.delta.kernel.Operation\nimport io.delta.kernel.commit.PublishFailedException\nimport io.delta.kernel.test.MockFileSystemClientUtils\nimport io.delta.kernel.unitycatalog.InMemoryUCClient.TableData\nimport io.delta.kernel.unitycatalog.metrics.UcPublishTelemetry\nimport io.delta.kernel.utils.CloseableIterable\nimport io.delta.storage.commit.Commit\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UcPublishTelemetrySuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils\n    with MockFileSystemClientUtils {\n\n  test(\"publish metrics for successful publish operations\") {\n    withTempDirAndEngine { case (tablePathUnresolved, _) =>\n      // ===== GIVEN =====\n      val reporter = new CapturingMetricsReporter[UcPublishTelemetry#Report]\n      val engine = createEngineWithMetricsCapture(reporter)\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n      val (ucClient, ucCatalogManagedClient) = createUCClientAndCatalogManagedClient()\n\n      val result0 = ucCatalogManagedClient\n        .buildCreateTableTransaction(\"testUcTableId\", tablePath, testSchema, \"test-engine\")\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n\n      ucClient.insertTableDataAfterCreate(\"testUcTableId\")\n\n      val resultV1 = result0.getPostCommitSnapshot.get()\n        .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n\n      val resultV2 = resultV1.getPostCommitSnapshot.get()\n        .buildUpdateTableTransaction(\"engineInfo\", Operation.MANUAL_UPDATE)\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n\n      reporter.reports.clear()\n\n      // ===== WHEN =====\n      resultV1.getPostCommitSnapshot.get().publish(engine) // publishes 01.uuid.json -> 01.json\n      resultV2.getPostCommitSnapshot.get().publish(engine) // publishes 02.uuid.json -> 02.json\n\n      // ===== THEN =====\n      assert(reporter.reports.size === 2)\n\n      val firstPublish = reporter.reports(0)\n      assert(firstPublish.operationType === \"UcPublish\")\n      assert(firstPublish.ucTableId === \"testUcTableId\")\n      assert(firstPublish.snapshotVersion === 1)\n      assert(firstPublish.numCommitsToPublish === 1)\n      assert(firstPublish.metrics.numCommitsPublished === 1)\n      assert(firstPublish.metrics.numCommitsAlreadyPublished === 0)\n\n      val secondPublish = reporter.reports(1)\n      assert(secondPublish.operationType === \"UcPublish\")\n      assert(secondPublish.ucTableId === \"testUcTableId\")\n      assert(secondPublish.snapshotVersion === 2)\n      assert(secondPublish.numCommitsToPublish === 2) // Both 01.uuid.json and 02.uuid.json\n      assert(secondPublish.metrics.numCommitsPublished === 1) // Only 02.uuid.json\n      assert(secondPublish.metrics.numCommitsAlreadyPublished === 1) // 01.uuid.json was already!\n    }\n  }\n\n  test(\"JSON serialization: success report\") {\n    val telemetry = new UcPublishTelemetry(\"testUcTableId\", \"ucTablePath\", 5, 3)\n    val collector = telemetry.getMetricsCollector\n    collector.totalPublishTimer.record(500)\n    collector.incrementCommitsPublished()\n    collector.incrementCommitsPublished()\n    collector.incrementCommitsAlreadyPublished()\n\n    val report = telemetry.createSuccessReport()\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcPublish\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"snapshotVersion\":5,\n         |\"numCommitsToPublish\":3,\n         |\"metrics\":{\"totalPublishDurationNs\":500,\"numCommitsPublished\":2,\"numCommitsAlreadyPublished\":1},\n         |\"exception\":null}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n\n  test(\"JSON serialization: failure report\") {\n    val telemetry = new UcPublishTelemetry(\"testUcTableId\", \"ucTablePath\", 3, 2)\n    val collector = telemetry.getMetricsCollector\n    collector.totalPublishTimer.record(300)\n    collector.incrementCommitsPublished()\n\n    val exception = new PublishFailedException(\"Failed to publish\")\n    val report = telemetry.createFailureReport(exception)\n\n    // scalastyle:off line.size.limit\n    val expectedJson =\n      s\"\"\"\n         |{\"operationType\":\"UcPublish\",\n         |\"reportUUID\":\"${report.reportUUID}\",\n         |\"ucTableId\":\"testUcTableId\",\n         |\"ucTablePath\":\"ucTablePath\",\n         |\"snapshotVersion\":3,\n         |\"numCommitsToPublish\":2,\n         |\"metrics\":{\"totalPublishDurationNs\":300,\"numCommitsPublished\":1,\"numCommitsAlreadyPublished\":0},\n         |\"exception\":\"io.delta.kernel.commit.PublishFailedException: Failed to publish\"}\n         |\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n    // scalastyle:on line.size.limit\n\n    assert(report.toJson() === expectedJson)\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/UnityCatalogUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.expressions.Column\nimport io.delta.kernel.internal.SnapshotImpl\nimport io.delta.kernel.internal.fs.Path\nimport io.delta.kernel.test.MockSnapshotUtils\nimport io.delta.kernel.transaction.DataLayoutSpec\nimport io.delta.kernel.types.{IntegerType, StringType, StructType}\nimport io.delta.kernel.utils.CloseableIterable\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UnityCatalogUtilsSuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils\n    with MockSnapshotUtils {\n\n  private val testUcTableId = \"testUcTableId\"\n\n  test(\"getPropertiesForCreate: throws when snapshot is not version 0\") {\n    val mockSnapshotV1 = getMockSnapshot(new Path(\"/fake/table/path\"), latestVersion = 1)\n\n    val exMsg = intercept[IllegalArgumentException] {\n      UnityCatalogUtils.getPropertiesForCreate(defaultEngine, mockSnapshotV1)\n    }.getMessage\n\n    assert(exMsg.contains(\"Expected a snapshot at version 0, but got a snapshot at version 1\"))\n  }\n\n  test(\"getPropertiesForCreate: handles all cases together\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n\n      val testSchema = new StructType()\n        .add(\"id\", IntegerType.INTEGER)\n        .add(\n          \"address\",\n          new StructType()\n            .add(\"city\", StringType.STRING)\n            .add(\"state\", StringType.STRING))\n        .add(\"data\", StringType.STRING)\n\n      val clusteringColumns = List(new Column(\"id\"), new Column(Array(\"address\", \"city\")))\n\n      val snapshot = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .withTableProperties(\n          Map(\n            \"foo\" -> \"bar\",\n            \"delta.enableRowTracking\" -> \"true\",\n            \"delta.columnMapping.mode\" -> \"name\").asJava)\n        .withDataLayoutSpec(DataLayoutSpec.clustered(clusteringColumns.asJava))\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n        .getPostCommitSnapshot\n        .get()\n        .asInstanceOf[SnapshotImpl]\n\n      val snapshotTimestamp = snapshot.getTimestamp(engine)\n\n      val actualProps = UnityCatalogUtils.getPropertiesForCreate(engine, snapshot).asScala\n\n      val expectedProps = Map(\n        // Case 0: Properties we expect to be injected by the UC-CatalogManaged-Client (and are\n        //         stored in the metadata.configuration)\n        \"io.unitycatalog.tableId\" -> testUcTableId,\n\n        // Case 1: Table properties from metadata.configuration\n        \"foo\" -> \"bar\",\n        \"delta.enableRowTracking\" -> \"true\",\n        \"delta.columnMapping.mode\" -> \"name\",\n\n        // Case 2: Protocol-derived properties\n        \"delta.minReaderVersion\" -> \"3\",\n        \"delta.minWriterVersion\" -> \"7\",\n        \"delta.feature.catalogManaged\" -> \"supported\",\n        \"delta.feature.rowTracking\" -> \"supported\",\n        \"delta.feature.columnMapping\" -> \"supported\",\n        \"delta.feature.inCommitTimestamp\" -> \"supported\",\n\n        // Case 3: UC metastore properties\n        \"delta.lastUpdateVersion\" -> \"0\",\n        \"delta.lastCommitTimestamp\" -> s\"$snapshotTimestamp\",\n\n        // Case 4: Clustering properties - these should be the LOGICAL names not the PHYSICAL names\n        \"clusteringColumns\" -> \"\"\"[[\"id\"],[\"address\",\"city\"]]\"\"\")\n\n      val failures = expectedProps.collect {\n        case (k, v) if !actualProps.contains(k) => s\"$k: MISSING (expected: $v)\"\n        case (k, v) if actualProps(k) != v => s\"$k: expected '$v', got '${actualProps(k)}'\"\n      }\n\n      assert(failures.isEmpty, failures.mkString(\"Property mismatches:\\n\", \"\\n\", \"\"))\n    }\n  }\n\n  test(\"getPropertiesForCreate: clustered table with empty clustering columns\") {\n    withTempDirAndEngine { case (tablePathUnresolved, engine) =>\n      val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n      val ucCatalogManagedClient = new UCCatalogManagedClient(ucClient)\n      val tablePath = engine.getFileSystemClient.resolvePath(tablePathUnresolved)\n\n      val snapshot = ucCatalogManagedClient\n        .buildCreateTableTransaction(testUcTableId, tablePath, testSchema, \"test-engine\")\n        .withDataLayoutSpec(DataLayoutSpec.clustered(List.empty.asJava))\n        .build(engine)\n        .commit(engine, CloseableIterable.emptyIterable())\n        .getPostCommitSnapshot\n        .get()\n        .asInstanceOf[SnapshotImpl]\n\n      val props = UnityCatalogUtils.getPropertiesForCreate(engine, snapshot).asScala\n      assert(props(\"clusteringColumns\") == \"[]\")\n    }\n  }\n}\n"
  },
  {
    "path": "kernel/unitycatalog/src/test/scala/io/delta/kernel/unitycatalog/adapters/ActionAdaptersSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.kernel.unitycatalog.adapters\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.data.{ArrayValue, ColumnVector}\nimport io.delta.kernel.internal.actions.{Format, Metadata => KernelMetadata, Protocol => KernelProtocol}\nimport io.delta.kernel.internal.util.InternalUtils.singletonStringColumnVector\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.kernel.types.{IntegerType, StructType}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ActionAdaptersSuite extends AnyFunSuite {\n\n  test(\"ProtocolAdapter\") {\n    // ===== GIVEN =====\n    val readerFeatures = Set(\"v2Checkpoint\").asJava\n    val writerFeatures = Set(\"v2Checkpoint\", \"rowTracking\").asJava\n    val kernelProtocol = new KernelProtocol(3, 7, readerFeatures, writerFeatures)\n\n    // ===== WHEN =====\n    val adapterProtocol = new ProtocolAdapter(kernelProtocol)\n\n    // ===== THEN =====\n    assert(adapterProtocol.getMinReaderVersion === 3)\n    assert(adapterProtocol.getMinWriterVersion === 7)\n    assert(adapterProtocol.getReaderFeatures.asScala == Set(\"v2Checkpoint\"))\n    assert(adapterProtocol.getWriterFeatures.asScala == Set(\"v2Checkpoint\", \"rowTracking\"))\n  }\n\n  test(\"MetadataAdapter\") {\n    // ===== GIVEN =====\n    val partCols = new ArrayValue() {\n      override def getSize = 1\n      override def getElements: ColumnVector = singletonStringColumnVector(\"part1\")\n    }\n    val formatOptions = Map(\"foo\" -> \"bar\").asJava\n    val format = new Format(\"parquet\", formatOptions)\n    val configuration = Map(\"zip\" -> \"zap\").asJava\n\n    val kernelMetadata = new KernelMetadata(\n      \"id\",\n      java.util.Optional.of(\"name\"),\n      java.util.Optional.of(\"description\"),\n      format,\n      \"schemaStringJson\",\n      new StructType().add(\"part1\", IntegerType.INTEGER).add(\"col1\", IntegerType.INTEGER),\n      partCols,\n      java.util.Optional.of(42L), // createdTime\n      VectorUtils.stringStringMapValue(configuration))\n\n    // ===== WHEN =====\n    val adapter = new MetadataAdapter(kernelMetadata)\n\n    // ===== THEN =====\n    assert(adapter.getId === \"id\")\n    assert(adapter.getName === \"name\")\n    assert(adapter.getDescription === \"description\")\n    assert(adapter.getProvider === \"parquet\")\n    assert(adapter.getFormatOptions.asScala == Map(\"foo\" -> \"bar\"))\n    assert(adapter.getSchemaString === \"schemaStringJson\")\n    assert(adapter.getPartitionColumns.asScala == Seq(\"part1\"))\n    assert(adapter.getConfiguration.asScala == Map(\"zip\" -> \"zap\"))\n    assert(adapter.getCreatedTime === 42L)\n  }\n}\n"
  },
  {
    "path": "kernel/version.sbt",
    "content": "ThisBuild / version := \"0.1.0-SNAPSHOT\"\n"
  },
  {
    "path": "project/Checkstyle.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport com.etsy.sbt.checkstyle.CheckstylePlugin.autoImport._\nimport org.scalastyle.sbt.ScalastylePlugin.autoImport._\nimport sbt._\nimport sbt.Keys._\n\nobject Checkstyle {\n\n  /*\n   *****************************\n   * Scala checkstyle settings *\n   *****************************\n   */\n\n  ThisBuild / scalastyleConfig := baseDirectory.value / \"scalastyle-config.xml\"\n\n  private lazy val compileScalastyle = taskKey[Unit](\"compileScalastyle\")\n  private lazy val testScalastyle = taskKey[Unit](\"testScalastyle\")\n\n  lazy val scalaStyleSettings = Seq(\n    compileScalastyle := (Compile / scalastyle).toTask(\"\").value,\n\n    Compile / compile := ((Compile / compile) dependsOn compileScalastyle).value,\n\n    testScalastyle := (Test / scalastyle).toTask(\"\").value,\n\n    Test / test := ((Test / test) dependsOn testScalastyle).value\n  )\n\n  /*\n   ****************************\n   * Java checkstyle settings *\n   ****************************\n   */\n\n  private lazy val compileJavastyle = taskKey[Unit](\"compileJavastyle\")\n  private lazy val testJavastyle = taskKey[Unit](\"testJavastyle\")\n\n  def javaCheckstyleSettings(checkstyleFile: String): Def.SettingsDefinition = {\n    // Can be run explicitly via: build/sbt $module/checkstyle\n    // Will automatically be run during compilation (e.g. build/sbt compile)\n    // and during tests (e.g. build/sbt test)\n    Seq(\n      checkstyleConfigLocation := CheckstyleConfigLocation.File(checkstyleFile),\n      // if we keep the Error severity, `build/sbt` will throw an error and immediately stop at\n      // the `checkstyle` phase (if error) -> never execute the `check-report` phase of\n      // `checkstyle-report.xml` and `checkstyle-test-report.xml`. We need to ignore and throw\n      // error if exists when checking *report.xml.\n      checkstyleSeverityLevel := CheckstyleSeverityLevel.Ignore,\n\n      compileJavastyle := {\n        (Compile / checkstyle).value\n        javaCheckstyle(streams.value.log, checkstyleOutputFile.value)\n      },\n      (Compile / compile) := ((Compile / compile) dependsOn compileJavastyle).value,\n\n      testJavastyle := {\n        (Test / checkstyle).value\n        javaCheckstyle(streams.value.log, (Compile / target).value / \"checkstyle-test-report.xml\")\n      },\n      (Test / test) := ((Test / test) dependsOn (Test / testJavastyle)).value\n    )\n  }\n\n  private def javaCheckstyle(log: Logger, reportFile: File): Unit = {\n    val report = scala.xml.XML.loadFile(reportFile)\n\n    val errors = (report \\\\ \"file\").flatMap { fileNode =>\n      val file = fileNode.attribute(\"name\").get.head.text\n      (fileNode \\ \"error\").map { error =>\n        val line = error.attribute(\"line\").get.head.text\n        val message = error.attribute(\"message\").get.head.text\n        (file, line, message)\n      }\n    }\n\n    if (errors.nonEmpty) {\n      var errorMsg = \"Found checkstyle errors\"\n      errors.foreach { case (file, line, message) =>\n        val lineError = s\"File: $file, Line: $line, Message: $message\"\n        log.error(lineError)\n        errorMsg += (\"\\n\" + lineError)\n      }\n      sys.error(errorMsg + \"\\n\")\n    }\n  }\n\n}\n"
  },
  {
    "path": "project/CrossSparkVersions.scala",
    "content": "import sbt._\nimport sbt.Keys._\nimport sbt.complete.DefaultParsers._\nimport com.simplytyped.Antlr4Plugin\nimport com.simplytyped.Antlr4Plugin.autoImport._\nimport sbtrelease.ReleasePlugin.autoImport.ReleaseStep\nimport Unidoc._\n\n/** \n * ========================================================\n * Cross-Spark Build and Publish System\n * ========================================================\n * \n * This SBT plugin enables Delta Lake to be built and published for multiple Spark versions.\n * It provides version-specific configurations, artifact naming, and publishing workflows.\n *\n * ========================================================\n * Spark Version Definitions\n * ========================================================\n * \n * The Spark versions used for Delta is defined in the SparkVersionSpec object, and controlled by the sparkVersion property.\n * There are 2 keys labels assigned to the Spark versions: DEFAULT and MASTER.\n * - DEFAULT VERSION: This is the default when no sparkVersion property is specified.\n *\n * - MASTER VERSION: The Spark master/development branch version\n *   This is optional and typically \n *   - set in the Delta master branch to a Spark released or snapshot version .\n *   - not set in the Delta release branches as we want to avoid building against Spark unreleased version.\n *   If MASTER is defined, then it can be selected by setting the sparkVersion property to \"master\".\n *   Spark-dependent artifacts for this version HAVE a Spark version suffix in their artifact names (e.g., delta-spark_4.0_2.13 if MASTER is defined as Spark 4.0 branch).\n *\n * - OTHER VERSIONS: Any non-default Spark version specified in ALL_SPECS.\n *   Spark-dependent artifacts of all non-default versions get a Spark version suffix in their artifact names (e.g., delta-spark_4.1_2.13 if one of the other versions is defined as Spark 4.1 branch).\n *\n * To configure versions, update the SparkVersionSpec values (e.g., spark35, spark40, etc.) below.\n *\n * ========================================================\n * The sparkVersion Property\n * ========================================================\n * \n * The sparkVersion system property controls which Spark version to build against.\n * It accepts the following formats:\n *\n * 1. Full version string (e.g., \"3.5.7\", \"4.0.2-SNAPSHOT\")\n * 2. Short version string (e.g., \"3.5\", \"4.0\")\n * 3. Aliases:\n *    - \"default\" -> maps to DEFAULT version (e.g., spark35)\n *    - \"master\" -> maps to MASTER version (e.g., spark40), if configured\n *\n * If not specified, it defaults to the DEFAULT version.\n *\n * Examples:\n *   build/sbt                                    # Uses default version\n *   build/sbt -DsparkVersion=4.0                 # Uses Spark 4.0.x\n *   build/sbt -DsparkVersion=4.0.1               # Uses Spark 4.0.1 only if this version is defined in ALL_SPECS\n *   build/sbt -DsparkVersion=4.1                 # Uses Spark 4.1.x whatever it is defined in ALL_SPECS\n *   build/sbt -DsparkVersion=default             # Uses default version\n *   build/sbt -DsparkVersion=master              # Uses master version (if defined)\n *\n * ========================================================\n * Cross-Building for Development and Testing\n * ========================================================\n * \n * To build/test against a specific Spark version:\n *   build/sbt -DsparkVersion=<version> compile\n *   build/sbt -DsparkVersion=<version> test\n *   build/sbt -DsparkVersion=master compile test\n *\n * To publish to local Maven for testing:\n *   # Publish all modules for default Spark version\n *   build/sbt publishM2\n *\n *   # Publish only Spark-dependent modules for other versions\n *   build/sbt -DsparkVersion=master \"runOnlyForReleasableSparkModules publishM2\"\n *\n * ========================================================\n * Module Types\n * ========================================================\n * \n * Modules are automatically classified based on their settings:\n *\n * 1. Spark-Dependent Published Modules:\n *    - Use CrossSparkVersions.sparkDependentSettings(sparkVersion)\n *    - Include releaseSettings (publishable)\n *    - Examples: delta-spark, delta-connect-*, delta-sharing-spark, delta-iceberg, delta-hudi, delta-contribs\n *    - These modules get version-specific artifact names for non-default Spark versions\n *    - Automatically included in cross-Spark publishing\n *\n * 2. Spark-Dependent Internal Modules:\n *    - Use CrossSparkVersions.sparkDependentSettings(sparkVersion)\n *    - Include skipReleaseSettings (not published)\n *    - Examples: sparkV1, sparkV2\n *    - These modules are built for each Spark version but not published\n *    - Automatically excluded from cross-Spark publishing\n *\n * 3. Spark-Independent Modules:\n *    - Do not use CrossSparkVersions settings\n *    - Examples: delta-storage, delta-kernel-*, delta-standalone\n *    - These modules are built once and work with all Spark versions\n *\n * ========================================================\n * Artifact Naming Convention of Spark-dependent modules\n * ========================================================\n * \n * By default, Spark-dependent modules ALWAYS include the Spark version suffix:\n *   io.delta:delta-spark_4.0_2.13:4.1.0\n *   io.delta:delta-spark_4.1_2.13:4.1.0\n *   io.delta:delta-connect-server_4.0_2.13:4.1.0\n *   io.delta:delta-storage:4.1.0  (Spark-independent, no suffix)\n *\n * During release, backward-compatible artifacts are ALSO published (without suffix):\n *   io.delta:delta-spark_2.13:4.1.0       (backward compatibility)\n *   io.delta:delta-connect-server_2.13:4.1.0\n *\n * This means during release, Spark-dependent modules are published TWICE:\n *   - With suffix (e.g., delta-spark_4.1_2.13) - the default/normal name\n *   - Without suffix (e.g., delta-spark_2.13) - for backward compatibility\n *\n * ========================================================\n * Cross-Release Workflow\n * ========================================================\n * \n * The cross-release workflow publishes artifacts for all Spark versions:\n *\n * Step 1: Publish ALL modules WITHOUT Spark suffix (backward compatibility)\n *   build/sbt -DskipSparkSuffix=true publishSigned\n *   # Publishes: delta-spark_2.13, delta-storage, delta-kernel-api, etc.\n *\n * Step 2: Publish Spark-dependent modules WITH suffix for each non-master Spark version\n *   build/sbt -DsparkVersion=4.0 \"runOnlyForReleasableSparkModules publishSigned\"\n *   build/sbt -DsparkVersion=4.1 \"runOnlyForReleasableSparkModules publishSigned\"\n *   # Publishes: delta-spark_4.0_2.13, delta-spark_4.1_2.13, etc.\n *\n * This workflow is automated via crossSparkReleaseSteps() in the release process.\n * See releaseProcess in build.sbt for integration.\n *\n * Why this approach?\n * - Default behavior always includes Spark suffix for clarity\n * - Release also publishes without suffix for backward compatibility\n * - Spark-independent modules (kernel, storage) are built once\n * - Spark-dependent modules are built for each Spark version\n *\n * For manual testing during development:\n *   build/sbt publishM2  # Publishes delta-spark_4.0_2.13 (default, with suffix)\n *\n * For manual release testing:\n *   build/sbt -DskipSparkSuffix=true publishM2  # Without suffix (backward compat)\n *   build/sbt -DsparkVersion=4.0 \"runOnlyForReleasableSparkModules publishM2\"\n *   build/sbt -DsparkVersion=4.1 \"runOnlyForReleasableSparkModules publishM2\"\n *   # Verify JARs in ~/.m2/repository/io/delta/\n *\n * ========================================================\n * Commands Provided\n * ========================================================\n * \n * runOnlyForReleasableSparkModules <task>\n *   Runs the specified task only on publishable Spark-dependent modules.\n *   Automatically detects modules that:\n *   1. Have the sparkVersion setting (use Spark-aware configuration)\n *   2. Are publishable (publish/skip is not true)\n *\n *   Used for publishing Spark-dependent modules for non-default Spark versions.\n *\n *   Example:\n *     build/sbt -DsparkVersion=4.0 \"runOnlyForReleasableSparkModules publishM2\"\n *\n * showSparkVersions\n *   Lists all configured Spark versions (for testing/debugging).\n *\n *   Example:\n *     build/sbt showSparkVersions\n *\n * exportSparkVersionsJson\n *   Exports Spark version information to target/spark-versions.json.\n *   This is the SINGLE SOURCE OF TRUTH for Spark versions used by:\n *   - GitHub Actions workflows (for dynamic matrix generation)\n *   - CI/CD scripts (for version-specific configuration)\n *\n *   The JSON is an array where each element contains:\n *   - fullVersion: Full version string (e.g., \"4.0.1\", \"4.1.0\")\n *   - shortVersion: Short version string (e.g., \"4.0\", \"4.1\")\n *   - isMaster: Whether this is the master/snapshot version\n *   - isDefault: Whether this is the default Spark version\n *   - targetJvm: Target JVM version (e.g., \"17\")\n *   - packageSuffix: Maven artifact suffix for this version (e.g., \"_4.0\", \"_4.1\")\n *\n *   Example:\n *     build/sbt exportSparkVersionsJson\n *     # Generates: target/spark-versions.json\n *     # Output: [{\"fullVersion\": \"4.0.1\", \"shortVersion\": \"4.0\", \"isMaster\": false, \"isDefault\": true, \"targetJvm\": \"17\", \"packageSuffix\": \"_4.0\"}, ...]\n *\n *   Use with Python utilities to extract specific fields:\n *     python3 project/scripts/get_spark_version_info.py --all-spark-versions\n *     # Output: [\"4.0\", \"4.1\"] or [\"master\", \"4.0\"] if master is present\n *     python3 project/scripts/get_spark_version_info.py --get-field \"4.0\" targetJvm\n *     python3 project/scripts/get_spark_version_info.py --get-field \"master\" targetJvm\n *\n *   This ensures GitHub Actions always uses the versions defined here,\n *   eliminating manual synchronization across multiple files.\n *\n * ========================================================\n */\n\n\n/**\n * Specification for a Spark version with all its build configuration.\n *\n * @param fullVersion The full Spark version (e.g., \"3.5.7\", \"4.0.2-SNAPSHOT\")\n * @param targetJvm Target JVM version (e.g., \"11\", \"17\")\n * @param additionalSourceDir Optional version-specific source directory suffix (e.g., \"scala-spark-3.5\")\n * @param antlr4Version ANTLR version to use (e.g., \"4.9.3\", \"4.13.1\")\n * @param additionalJavaOptions Additional JVM options for tests (e.g., Java 17 --add-opens flags)\n */\ncase class SparkVersionSpec(\n  fullVersion: String,\n  targetJvm: String,\n  additionalSourceDir: Option[String] = None,\n  supportIceberg: Boolean,\n  supportHudi: Boolean = true,\n  antlr4Version: String,\n  additionalJavaOptions: Seq[String] = Seq.empty,\n  jacksonVersion: String = \"2.15.2\",\n  additionalResolvers: Seq[Resolver] = Seq.empty\n) {\n  /** Returns the Spark short version (e.g., \"3.5\", \"4.0\") */\n  def shortVersion: String = {\n    Mima.getMajorMinorPatch(fullVersion) match {\n      case (maj, min, _) => s\"$maj.$min\"\n    }\n  }\n\n  /** Whether this is the default Spark version */\n  def isDefault: Boolean = this == SparkVersionSpec.DEFAULT\n\n  /** Whether this is the master Spark version */\n  def isMaster: Boolean = SparkVersionSpec.MASTER.contains(this)\n\n  /** Returns log4j config file */\n  def log4jConfig: String = \"log4j2.properties\"\n\n  /** Whether to export JARs instead of class directories (needed for Spark Connect on master) */\n  def exportJars: Boolean = additionalSourceDir.exists(_.contains(\"master\"))\n\n  /** Whether to generate Javadoc/Scaladoc for this version */\n  def generateDocs: Boolean = isDefault\n}\n\nobject SparkVersionSpec {\n\n  private val java17TestSettings = Seq(\n    // Copied from SparkBuild.scala to support Java 17 for unit tests (see apache/spark#34153)\n    \"--add-opens=java.base/java.lang=ALL-UNNAMED\",\n    \"--add-opens=java.base/java.lang.invoke=ALL-UNNAMED\",\n    \"--add-opens=java.base/java.io=ALL-UNNAMED\",\n    \"--add-opens=java.base/java.net=ALL-UNNAMED\",\n    \"--add-opens=java.base/java.nio=ALL-UNNAMED\",\n    \"--add-opens=java.base/java.util=ALL-UNNAMED\",\n    \"--add-opens=java.base/java.util.concurrent=ALL-UNNAMED\",\n    \"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED\",\n    \"--add-opens=java.base/sun.nio.cs=ALL-UNNAMED\",\n    \"--add-opens=java.base/sun.security.action=ALL-UNNAMED\",\n    \"--add-opens=java.base/sun.util.calendar=ALL-UNNAMED\"\n  )\n\n  private val spark40 = SparkVersionSpec(\n    fullVersion = \"4.0.1\",\n    targetJvm = \"17\",\n    additionalSourceDir = Some(\"scala-shims/spark-4.0\"),\n    supportIceberg = true,\n    antlr4Version = \"4.13.1\",\n    additionalJavaOptions = java17TestSettings,\n    jacksonVersion = \"2.18.2\"\n  )\n\n  private val spark41 = SparkVersionSpec(\n    fullVersion = \"4.1.0\",\n    targetJvm = \"17\",\n    additionalSourceDir = Some(\"scala-shims/spark-4.1\"),\n    supportIceberg = false,\n    supportHudi = false,\n    antlr4Version = \"4.13.1\",\n    additionalJavaOptions = java17TestSettings,\n    jacksonVersion = \"2.18.2\"\n  )\n\n  private val spark42Snapshot = SparkVersionSpec(\n    fullVersion = \"4.2.0-SNAPSHOT\",\n    targetJvm = \"17\",\n    additionalSourceDir = Some(\"scala-shims/spark-4.2\"),\n    supportIceberg = false,\n    supportHudi = false,\n    antlr4Version = \"4.13.1\",\n    additionalJavaOptions = java17TestSettings,\n    jacksonVersion = \"2.18.2\",\n    // Artifact updates in maven central for roaringbitmap stopped after 1.3.0.\n    // Spark master uses 1.5.3. Relevant Spark PR here https://github.com/apache/spark/pull/52892\n    additionalResolvers = Seq(\"jitpack\" at \"https://jitpack.io\")\n  )\n\n  /** Default Spark version */\n  val DEFAULT = spark41\n\n  /** Spark master branch version (optional). Release branches should not build against master */\n  val MASTER: Option[SparkVersionSpec] = None\n\n  /** All supported Spark versions - internal use only */\n  val ALL_SPECS = Seq(spark40, spark41)\n}\n\n/** See docs on top of this file */\nobject CrossSparkVersions extends AutoPlugin {\n\n  override def trigger = allRequirements\n\n  /**\n   * Returns the current configured Spark version spec based on the `sparkVersion` property.\n   */\n  def getSparkVersionSpec(): SparkVersionSpec = {\n    val input = sys.props.getOrElse(\"sparkVersion\", SparkVersionSpec.DEFAULT.fullVersion)\n\n    // Resolve aliases first\n    val resolvedInput = input match {\n      case \"default\" => SparkVersionSpec.DEFAULT.fullVersion\n      case \"master\" => SparkVersionSpec.MASTER match {\n        case Some(masterSpec) => masterSpec.fullVersion\n        case None => throw new IllegalArgumentException(\n          \"No master Spark version is configured. Available versions: \" +\n          SparkVersionSpec.ALL_SPECS.map(_.fullVersion).mkString(\", \")\n        )\n      }\n      case other => other\n    }\n\n    // Find spec by full version or short version\n    SparkVersionSpec.ALL_SPECS.find { spec =>\n      spec.fullVersion == resolvedInput || spec.shortVersion == resolvedInput\n    }.getOrElse {\n      val aliases = Seq(\"default\") ++ SparkVersionSpec.MASTER.map(_ => \"master\").toSeq\n      val validInputs = SparkVersionSpec.ALL_SPECS.flatMap { spec =>\n        Seq(spec.fullVersion, spec.shortVersion)\n      } ++ aliases\n      throw new IllegalArgumentException(\n        s\"Invalid sparkVersion: $input. Valid values: ${validInputs.mkString(\", \")}\"\n      )\n    }\n  }\n\n  /**\n   * Returns the current configured Spark version based on the `sparkVersion` property.\n   */\n  def getSparkVersion(): String = getSparkVersionSpec().fullVersion\n\n  /**\n   * Returns module name with Spark version suffix.\n   * \n   * By default, ALL Spark-dependent modules include the Spark version suffix:\n   *   delta-spark_4.0_2.13, delta-spark_4.1_2.13, etc.\n   *\n   * During release, the `skipSparkSuffix=true` property is used to also publish\n   * backward-compatible artifacts without the suffix (e.g., delta-spark_2.13).\n   */\n  private def moduleName(baseName: String, sparkVer: String): String = {\n    val spec = SparkVersionSpec.ALL_SPECS.find(_.fullVersion == sparkVer)\n      .getOrElse(throw new IllegalArgumentException(s\"Unknown Spark version: $sparkVer\"))\n\n    // skipSparkSuffix removes the suffix (used during release for backward compatibility)\n    val skipSparkSuffix = sys.props.getOrElse(\"skipSparkSuffix\", \"false\").toBoolean\n\n    if (skipSparkSuffix) {\n      baseName\n    } else {\n      s\"${baseName}_${spec.shortVersion}\"\n    }\n  }\n\n  // Scala version constant (Scala 2.12 support was dropped)\n  private val scala213 = \"2.13.17\"\n\n  /**\n   * Common Spark version-specific settings used by all Spark-aware modules.\n   * Returns Scala version, source directories, ANTLR version, JVM options, etc.\n   */\n  private def sparkVersionAwareSettings(sparkVersionKey: SettingKey[String]): Seq[Setting[_]] = {\n    val spec = getSparkVersionSpec()\n\n    val baseSettings = Seq(\n      scalaVersion := scala213,\n      crossScalaVersions := Seq(scala213),\n      resolvers ++= spec.additionalResolvers,\n      Antlr4 / antlr4Version := spec.antlr4Version,\n      Test / javaOptions ++= (Seq(s\"-Dlog4j.configurationFile=${spec.log4jConfig}\") ++ spec.additionalJavaOptions)\n    )\n\n    val additionalSourceDirSettings = spec.additionalSourceDir.map { dir =>\n      // Add both scala-shims and java-shims directories\n      val javaShimsDir = dir.replace(\"scala-shims\", \"java-shims\")\n      Seq(\n        Compile / unmanagedSourceDirectories += (Compile / baseDirectory).value / \"src\" / \"main\" / dir,\n        Compile / unmanagedSourceDirectories += (Compile / baseDirectory).value / \"src\" / \"main\" / javaShimsDir,\n        Test / unmanagedSourceDirectories += (Test / baseDirectory).value / \"src\" / \"test\" / dir\n      )\n    }.getOrElse(Seq.empty)\n\n    val conditionalSettings = Seq(\n      if (spec.exportJars) Seq(exportJars := true) else Nil,\n      if (spec.generateDocs)\n        Seq(unidocSourceFilePatterns := Seq(SourceFilePattern(\"io/delta/tables/\", \"io/delta/exceptions/\")))\n      else Nil\n    ).flatten\n\n    // Jackson dependency overrides to match Spark version and avoid conflicts\n    val jacksonOverrides = Seq(\n      dependencyOverrides ++= {\n        val sparkVer = sparkVersionKey.value\n        val jacksonVer = SparkVersionSpec.ALL_SPECS.find(_.fullVersion == sparkVer)\n          .getOrElse(throw new IllegalArgumentException(s\"Unknown Spark version: $sparkVer\"))\n          .jacksonVersion\n        Seq(\n          \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVer,\n          \"com.fasterxml.jackson.core\" % \"jackson-core\" % jacksonVer,\n          \"com.fasterxml.jackson.core\" % \"jackson-annotations\" % jacksonVer,\n          \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % jacksonVer,\n          \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVer\n        )\n      }\n    )\n\n    baseSettings ++ additionalSourceDirSettings ++ conditionalSettings ++ jacksonOverrides\n  }\n\n  /**\n   * Just the module name setting for Spark-dependent modules that don't need full Spark integration.\n   * Use this for modules that need versioned artifacts but use default Scala settings.\n   *\n   * @param sparkVersionKey The sparkVersion setting key for this project\n   */\n  def sparkDependentModuleName(sparkVersionKey: SettingKey[String]): Seq[Setting[_]] = {\n    Seq(\n      sparkVersionKey := getSparkVersion(),\n      // Dynamically modify moduleName to add Spark version suffix\n      Keys.moduleName := moduleName(Keys.name.value, sparkVersionKey.value)\n    )\n  }\n\n  /**\n   * Unified settings for Spark-dependent modules.\n   * Use this for modules that need to be built for multiple Spark versions.\n   * Works for both published modules and internal modules.\n   *\n   * @param sparkVersionKey The sparkVersion setting key for this project\n   */\n  def sparkDependentSettings(sparkVersionKey: SettingKey[String]): Seq[Setting[_]] = {\n    sparkDependentModuleName(sparkVersionKey) ++ sparkVersionAwareSettings(sparkVersionKey)\n  }\n\n  /**\n   * Generates release steps for cross-Spark publishing.\n   *\n   * Returns a sequence of release steps that:\n   * 1. Publishes all modules WITHOUT Spark suffix (backward compatibility)\n   * 2. Publishes Spark-dependent modules WITH Spark suffix for each non-master version\n   *\n   * For example, with Spark versions 4.0 (default) and 4.1:\n   * - Step 1 publishes: delta-spark_2.13, delta-storage, delta-kernel-api, etc. (no suffix)\n   * - Step 2 publishes: delta-spark_4.0_2.13, delta-spark_4.1_2.13, etc. (with suffix)\n   *\n   * Each step runs as a separate SBT subprocess so the build reloads with\n   * the correct sparkVersion/skipSparkSuffix settings (SBT settings like\n   * moduleName are evaluated once at build load time and can't be changed\n   * at runtime).\n   *\n   * Usage in build.sbt:\n   *   releaseProcess := Seq[ReleaseStep](\n   *     ...,\n   *   ) ++ CrossSparkVersions.crossSparkReleaseSteps(\"publishSigned\") ++ Seq(\n   *     ...\n   *   )\n   */\n  def crossSparkReleaseSteps(task: String): Seq[ReleaseStep] = {\n    // SBT settings (like moduleName) are evaluated once at build load time.\n    // To publish with different Spark versions or suffix modes, we must run\n    // separate SBT processes so the build reloads with the correct settings.\n    // The release version is already committed to version.sbt by prior steps,\n    // so subprocess SBT instances will pick up the correct version.\n\n    def runSbtSubprocess(state: State, sbtArgs: Seq[String], description: String): State = {\n      val extracted = Project.extract(state)\n      val baseDir = extracted.get(ThisBuild / Keys.baseDirectory)\n      val cmd = Seq(s\"${baseDir.getAbsolutePath}/build/sbt\") ++ sbtArgs\n      println(s\"[info] ========================================\")\n      println(s\"[info] $description\")\n      println(s\"[info] Running: ${cmd.mkString(\" \")}\")\n      println(s\"[info] ========================================\")\n      val exitCode = scala.sys.process.Process(cmd, baseDir).!\n      if (exitCode != 0) {\n        sys.error(s\"$description failed with exit code $exitCode\")\n      }\n      state\n    }\n\n    // Step 1: Publish ALL modules WITHOUT Spark suffix (backward compatibility)\n    // Uses skipSparkSuffix=true to get artifact names like delta-spark_2.13\n    val backwardCompatStep: ReleaseStep = { (state: State) =>\n      runSbtSubprocess(\n        state,\n        Seq(\"-DskipSparkSuffix=true\", task),\n        \"Publishing all modules without Spark suffix (backward compat)\"\n      )\n    }\n\n    // Step 2+: Publish Spark-dependent modules WITH suffix for each non-master Spark version\n    // This gives users versioned artifacts like delta-spark_4.0_2.13, delta-spark_4.1_2.13\n    val suffixedSparkSteps: Seq[ReleaseStep] = SparkVersionSpec.ALL_SPECS\n      .filterNot(_.isMaster) // Exclude master/snapshot versions\n      .map { spec =>\n        { (state: State) =>\n          runSbtSubprocess(\n            state,\n            Seq(s\"-DsparkVersion=${spec.fullVersion}\",\n                s\"runOnlyForReleasableSparkModules $task\"),\n            s\"Publishing Spark-dependent modules with suffix for Spark ${spec.fullVersion}\"\n          )\n        }: ReleaseStep\n      }\n\n    backwardCompatStep +: suffixedSparkSteps\n  }\n\n  override lazy val projectSettings = Seq(\n    commands += Command.args(\"runOnlyForReleasableSparkModules\", \"<task>\") { (state, args) =>\n      // Used for cross-Spark publishing of Spark-dependent modules only.\n      // Runs the specified task only on publishable Spark-dependent projects.\n      if (args.isEmpty) {\n        sys.error(\"Usage: runOnlyForReleasableSparkModules <task>\\nExample: build/sbt -DsparkVersion=<version> \\\"runOnlyForReleasableSparkModules publishM2\\\"\")\n      }\n\n      val task = args.mkString(\" \")\n\n      // Discover Spark-dependent projects dynamically\n      // A project is Spark-dependent if:\n      // 1. It has the sparkVersion setting (uses Spark-aware configuration)\n      // 2. It is publishable (publishArtifact is not false)\n      val extracted = sbt.Project.extract(state)\n      val sparkVersionKey = SettingKey[String](\"sparkVersion\")\n      val publishArtifactKey = SettingKey[Boolean](\"publishArtifact\")\n      val sparkDependentProjects = extracted.structure.allProjectRefs.filter { projRef =>\n        val hasSparkVersion = (projRef / sparkVersionKey).get(extracted.structure.data).isDefined\n        val isPublishable = (projRef / publishArtifactKey).get(extracted.structure.data).getOrElse(true)\n        hasSparkVersion && isPublishable\n      }\n\n      if (sparkDependentProjects.isEmpty) {\n        println(s\"[warn] No publishable projects with sparkVersion setting found\")\n        state\n      } else {\n        val projectNames = sparkDependentProjects.map(_.project).mkString(\", \")\n        val sparkVer = getSparkVersion()\n        println(s\"[info] Running '$task' for Spark-dependent modules with Spark $sparkVer\")\n        println(s\"[info] Spark-dependent projects: $projectNames\")\n        println(s\"[info] ========================================\")\n\n        // Build scoped task for each Spark-dependent project sequentially\n        sparkDependentProjects.foldLeft(state) { (currentState, projRef) =>\n          // Handle SBT cross-build prefix: \"+publishSigned\" must become\n          // \"+project/publishSigned\", not \"project/+publishSigned\"\n          val scopedTask = if (task.startsWith(\"+\")) {\n            s\"+${projRef.project}/${task.stripPrefix(\"+\")}\"\n          } else {\n            s\"${projRef.project}/$task\"\n          }\n          Command.process(scopedTask, currentState)\n        }\n      }\n    },\n    commands += Command.command(\"showSparkVersions\") { state =>\n      // Used for testing the cross-Spark publish workflow\n      SparkVersionSpec.ALL_SPECS.foreach { spec =>\n        println(spec.fullVersion)\n      }\n      state\n    },\n    commands += Command.command(\"exportSparkVersionsJson\") { state =>\n      // Export Spark version information as JSON for use by CI/CD and other tools\n      import java.io.{File, PrintWriter}\n\n      val outputFile = new File(\"target/spark-versions.json\")\n      outputFile.getParentFile.mkdirs()\n      \n      val writer = new PrintWriter(outputFile)\n      // scalastyle:off\n      try {\n        writer.println(\"[\")\n        SparkVersionSpec.ALL_SPECS.zipWithIndex.foreach { case (spec, idx) =>\n          val comma = if (idx < SparkVersionSpec.ALL_SPECS.size - 1) \",\" else \"\"\n          val isMaster = SparkVersionSpec.MASTER.contains(spec)\n          val isDefault = spec == SparkVersionSpec.DEFAULT\n          // Package suffix always includes Spark version (e.g., \"_4.0\", \"_4.1\")\n          val packageSuffix = s\"_${spec.shortVersion}\"\n          writer.println(s\"\"\"  {\"\"\")\n          writer.println(s\"\"\"    \"fullVersion\": \"${spec.fullVersion}\",\"\"\")\n          writer.println(s\"\"\"    \"shortVersion\": \"${spec.shortVersion}\",\"\"\")\n          writer.println(s\"\"\"    \"isMaster\": $isMaster,\"\"\")\n          writer.println(s\"\"\"    \"isDefault\": $isDefault,\"\"\")\n          writer.println(s\"\"\"    \"targetJvm\": \"${spec.targetJvm}\",\"\"\")\n          writer.println(s\"\"\"    \"packageSuffix\": \"$packageSuffix\",\"\"\")\n          writer.println(s\"\"\"    \"supportIceberg\": \"${spec.supportIceberg}\",\"\"\")\n          writer.println(s\"\"\"    \"supportHudi\": \"${spec.supportHudi}\"\"\"\")\n          writer.println(s\"\"\"  }$comma\"\"\")\n        }\n        writer.println(\"]\")\n        \n        println(s\"[info] Spark version information exported to: ${outputFile.getAbsolutePath}\")\n      } finally {\n        writer.close()\n      }\n      // scalastyle:on\n      \n      state\n    }\n  )\n}\n"
  },
  {
    "path": "project/FlinkMimaExcludes.scala",
    "content": "/*\n * Copyright (2020-present) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport com.typesafe.tools.mima.core._\n\n/**\n * The list of Mima errors to exclude in the Flink project.\n */\nobject FlinkMimaExcludes {\n  // scalastyle:off line.size.limit\n\n  val ignoredABIProblems = Seq(\n    // We can ignore internal changes\n    ProblemFilters.exclude[Problem](\"io.delta.standalone.internal.*\")\n  )\n}\n"
  },
  {
    "path": "project/Mima.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport com.typesafe.tools.mima.plugin.MimaPlugin.autoImport.{mimaBinaryIssueFilters, mimaPreviousArtifacts, mimaReportBinaryIssues}\nimport sbt._\nimport sbt.Keys._\n\n/**\n * Mima settings\n */\nobject Mima {\n\n  /**\n   * @return tuple of (major, minor, patch) versions extracted from a version string.\n   *         e.g. \"1.2.3\" would return (1, 2, 3)\n   */\n  def getMajorMinorPatch(versionStr: String): (Int, Int, Int) = {\n    implicit def extractInt(str: String): Int = {\n      \"\"\"\\d+\"\"\".r.findFirstIn(str).map(java.lang.Integer.parseInt).getOrElse {\n        throw new Exception(s\"Could not extract version number from $str in $version\")\n      }\n    }\n\n    versionStr.split(\"\\\\.\").toList match {\n      case majorStr :: minorStr :: patchStr :: _ =>\n        (majorStr, minorStr, patchStr)\n      case _ => throw new Exception(s\"Could not parse version for $version.\")\n    }\n  }\n\n  def getPrevSparkName(currentVersion: String): String = {\n    val (major, minor, patch) = getMajorMinorPatch(currentVersion)\n    // name change in version 3.0.0, so versions > 3.0.0 should have delta-spark are prev version.\n    if (major < 3 || (major == 3 && minor == 0 && patch == 0)) {\n      \"delta-core\"\n    } else {\n      \"delta-spark\"\n    }\n  }\n\n  def getPrevSparkVersion(currentVersion: String): String = {\n    val (major, minor, patch) = getMajorMinorPatch(currentVersion)\n\n    val lastVersionInMajorVersion = Map(\n      0 -> \"0.8.0\",\n      1 -> \"1.2.1\",\n      2 -> \"2.4.0\",\n      3 -> \"3.3.1\"\n    )\n    if (minor == 0) {  // 1.0.0 or 2.0.0 or 3.0.0 or 4.0.0\n      lastVersionInMajorVersion.getOrElse(major - 1, {\n        throw new Exception(s\"Last version of ${major - 1}.x.x not configured.\")\n      })\n    } else if (patch == 0) {\n      s\"$major.${minor - 1}.0\"      // 1.1.0 -> 1.0.0\n    } else {\n      s\"$major.$minor.${patch - 1}\" // 1.1.1 -> 1.1.0\n    }\n  }\n\n  def getPrevConnectorVersion(currentVersion: String): String = {\n    val (major, minor, patch) = getMajorMinorPatch(currentVersion)\n\n    val majorToLastMinorVersions: Map[Int, String] = Map(\n      // We skip from 0.6.0 to 3.0.0 when migrating connectors to the main delta repo\n      0 -> \"0.6.0\",\n      1 -> \"0.6.0\",\n      2 -> \"0.6.0\",\n      3 -> \"3.3.1\"\n    )\n    if (minor == 0) {  // 1.0.0\n      majorToLastMinorVersions.getOrElse(major - 1, {\n        throw new Exception(s\"Last minor version of ${major - 1}.x.x not configured.\")\n      })\n    } else if (patch == 0) {\n      s\"$major.${minor - 1}.0\"      // 1.1.0 -> 1.0.0\n    } else {\n      s\"$major.$minor.${patch - 1}\" // 1.1.1 -> 1.1.0\n    }\n  }\n\n  lazy val sparkMimaSettings = Seq(\n    Test / test := ((Test / test) dependsOn mimaReportBinaryIssues).value,\n    mimaPreviousArtifacts :=\n      Set(\"io.delta\" %% getPrevSparkName(version.value) %  getPrevSparkVersion(version.value)),\n    mimaBinaryIssueFilters ++= SparkMimaExcludes.ignoredABIProblems\n  )\n\n  lazy val standaloneMimaSettings = Seq(\n    Test / test := ((Test / test) dependsOn mimaReportBinaryIssues).value,\n    mimaPreviousArtifacts := {\n      Set(\"io.delta\" %% \"delta-standalone\" % getPrevConnectorVersion(version.value))\n    },\n    mimaBinaryIssueFilters ++= StandaloneMimaExcludes.ignoredABIProblems\n  )\n\n  lazy val flinkMimaSettings = Seq(\n    Test / test := ((Test / test) dependsOn mimaReportBinaryIssues).value,\n    mimaPreviousArtifacts := {\n      Set(\"io.delta\" % \"delta-flink\" % getPrevConnectorVersion(version.value))\n    },\n    mimaBinaryIssueFilters ++= FlinkMimaExcludes.ignoredABIProblems\n  )\n}\n"
  },
  {
    "path": "project/MultiShardMultiJVMTestParallelization.scala",
    "content": "import scala.util.hashing.MurmurHash3\nimport sbt.Keys._\nimport sbt._\n\n// scalastyle:off println\n/** Provides SBT test settings for sharding and parallelizing multi-JVM tests. */\nobject MultiShardMultiJVMTestParallelization {\n\n  /**\n   * Total number of shards (machines) to split tests across.\n   * E.g., NUM_SHARDS=4 means tests will be split across 4 machines.\n   * Each test is assigned to exactly one shard based on hash(testName) % NUM_SHARDS.\n   */\n  lazy val numShardsOpt = sys.env.get(\"NUM_SHARDS\").map(_.toInt)\n\n  /**\n   * The ID of the current shard (0-indexed).\n   * E.g., SHARD_ID=0 means this is shard 0 out of NUM_SHARDS total shards.\n   * This shard will only run tests where hash(testName) % NUM_SHARDS == SHARD_ID.\n   */\n  lazy val shardIdOpt = sys.env.get(\"SHARD_ID\").map(_.toInt)\n\n  /**\n   * Number of parallel JVMs to use within this shard.\n   * E.g., TEST_PARALLELISM_COUNT=2 means tests in this shard will run across 2 JVMs in parallel.\n   * Tests are distributed across JVMs using hash(testName + \"group\") % TEST_PARALLELISM_COUNT.\n   */\n  lazy val testParallelismOpt = sys.env.get(\"TEST_PARALLELISM_COUNT\").map(_.toInt)\n\n  lazy val settings = {\n    println(s\"numShardsOpt: $numShardsOpt\")\n    println(s\"shardIdOpt: $shardIdOpt\")\n    println(s\"testParallelismOpt: $testParallelismOpt\")\n\n    (numShardsOpt, shardIdOpt, testParallelismOpt) match {\n      case (Some(numShards), Some(shardId), Some(testParallelism))\n        if numShards >= 1 && shardId >= 0 && testParallelism >= 1 =>\n        println(\"Test parallelization enabled.\")\n\n        Seq(\n          Test / testGrouping := {\n            val tests = (Test / definedTests).value\n\n            // Create default fork options that inherit all the project's settings\n            val defaultForkOptions = ForkOptions(\n              javaHome = javaHome.value,\n              outputStrategy = outputStrategy.value,\n              bootJars = Vector.empty,\n              workingDirectory = Some(baseDirectory.value),\n              runJVMOptions = (Test / javaOptions).value.toVector,\n              connectInput = connectInput.value,\n              envVars = (Test / envVars).value\n            )\n\n            // Filter tests for this shard\n            val testsForThisShard = tests.filter { testDef =>\n              math.abs(MurmurHash3.stringHash(testDef.name) % numShards) == shardId\n            }\n\n            println(s\"[Shard $shardId] # tests: ${testsForThisShard.size}\")\n\n            // Distribute tests across groups (JVMs) within this shard\n            (0 until testParallelism).map { groupId =>\n              val testsForThisGroup = testsForThisShard.filter { testDef =>\n                // Add \"group\" suffix to create a different hash than shard assignment,\n                // ensuring even distribution across groups independent of shard assignment\n                val groupHash = MurmurHash3.stringHash(testDef.name + \"group\")\n                math.abs(groupHash % testParallelism) == groupId\n              }\n\n              println(s\"[Group $groupId] # tests: ${testsForThisGroup.size}\")\n\n              Tests.Group(\n                name = s\"Shard $shardId - Group $groupId\",\n                tests = testsForThisGroup,\n                runPolicy = Tests.SubProcess(defaultForkOptions)\n              )\n            }\n          },\n          Test / parallelExecution := true,\n          Global / concurrentRestrictions := Seq(\n            Tags.limit(Tags.ForkedTestGroup, testParallelism)\n          )\n        )\n\n      case _ =>\n        println(\"Test parallelization disabled.\")\n        Seq.empty[Setting[_]] // Run tests normally\n    }\n  }\n}\n"
  },
  {
    "path": "project/README.md",
    "content": "# Updating delta-spark TestParallelization Top 50 Slowest Test Suites List\n\n- Cherry-pick changes from https://github.com/delta-io/delta/pull/3694\n- That PR adds a test report listener to delta-spark that will output csv files containing per-JVM, per-group (thread), and per-test runtimes\n- Run the CI and download the generated csv artifacts\n- You can use the following pyspark code to get the top 50 slowest test suites\n- You can copy and paste that into Chat GPT and ask it to format it as a Scala List\n\n```python\nfrom pyspark.sql.functions import col, sum\nfrom pyspark.sql.types import StructType, StructField, StringType, LongType\n\nschema = StructType([\n    StructField(\"test_suite\", StringType(), True),\n    StructField(\"test_name\", StringType(), True),\n    StructField(\"execution_time_ms\", LongType(), True),\n    StructField(\"result\", StringType(), True)\n])\n\ncsv_dir = \"...\"\n\nspark.read.csv(csv_dir, schema=schema) \\\n    .filter(col(\"execution_time_ms\") != -1) \\\n    .groupBy(\"test_suite\") \\\n    .agg((sum(\"execution_time_ms\") / 60000).alias(\"execution_time_mins\")) \\\n    .orderBy(col(\"execution_time_mins\").desc()) \\\n    .limit(50) \\\n    .select(\"test_suite\", \"execution_time_mins\") \\\n    .show(50, truncate=False)\n```\n"
  },
  {
    "path": "project/ShadedIcebergBuild.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport sbt._\nimport sbtassembly.*\n\n/**\n * Exclusion rules for not bringing in conflicting dependencies via Iceberg Jar\n */\n\nobject ShadedIcebergBuild {\n  val icebergExclusionRules = List.apply(\n    ExclusionRule(\"com.github.ben-manes.caffeine\"),\n    ExclusionRule(\"io.netty\"),\n    ExclusionRule(\"org.apache.httpcomponents.client5\"),\n    ExclusionRule(\"org.apache.httpcomponents.core5\"),\n    ExclusionRule(\"io.airlift\"),\n    ExclusionRule(\"org.apache.commons\"),\n    ExclusionRule(\"commons-io\"),\n    ExclusionRule(\"commons-compress\"),\n    ExclusionRule(\"commons-lang3\"),\n    ExclusionRule(\"commons-codec\"),\n    ExclusionRule(\"com.fasterxml.jackson.core\"),\n    ExclusionRule(\"com.fasterxml.jackson.databind\"),\n  )\n\n  val hadoopClientExclusionRules = List.apply(\n    ExclusionRule(\"org.apache.avro\"),\n    ExclusionRule(\"org.slf4j\"),\n    ExclusionRule(\"commons-beanutils\"),\n    ExclusionRule(\"org.datanucleus\"),\n    ExclusionRule(\"io.netty\")\n  )\n\n  val hiveMetastoreExclusionRules = List.apply(\n    ExclusionRule(\"org.apache.avro\"),\n    ExclusionRule(\"org.slf4j\"),\n    ExclusionRule(\"org.pentaho\"),\n    ExclusionRule(\"org.apache.hbase\"),\n    ExclusionRule(\"org.apache.logging.log4j\"),\n    ExclusionRule(\"co.cask.tephra\"),\n    ExclusionRule(\"com.google.code.findbugs\"),\n    ExclusionRule(\"org.eclipse.jetty.aggregate\"),\n    ExclusionRule(\"org.eclipse.jetty.orbit\"),\n    ExclusionRule(\"org.apache.parquet\"),\n    ExclusionRule(\"com.tdunning\"),\n    ExclusionRule(\"javax.transaction\"),\n    ExclusionRule(\"com.zaxxer\"),\n    ExclusionRule(\"org.apache.ant\"),\n    ExclusionRule(\"javax.servlet\"),\n    ExclusionRule(\"javax.jdo\"),\n    ExclusionRule(\"commons-beanutils\"),\n    ExclusionRule(\"org.datanucleus\")\n  )\n\n  /**\n   * Replace those files with our customized version\n   * Here's an overview:\n   *  PartitionSpec: sets checkConflicts to false to honor field ID assigned by Delta\n   *  HiveCatalog, HiveTableOperations: allow metadataUpdates to overwrite schema and partition spec\n   *  RESTFileScanTaskParser: fixes NoSuchElementException on empty delete-file-references arrays\n   */\n  def updateMergeStrategy(prev: String => MergeStrategy): String => MergeStrategy = {\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", s)\n      if s.matches(\"TableMetadata(\\\\$.*)?\\\\.class\") =>\n      MergeStrategy.first\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", s)\n      if s.matches(\"MetadataUpdate(\\\\$.*)?\\\\.class\") =>\n      MergeStrategy.first\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", \"PartitionSpec$Builder.class\") =>\n      MergeStrategy.first\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", \"PartitionSpec.class\") =>\n      MergeStrategy.first\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", \"rest\", \"RESTFileScanTaskParser.class\") =>\n      MergeStrategy.first\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", \"hive\", \"HiveCatalog.class\") =>\n      MergeStrategy.first\n    case PathList(\"shadedForDelta\", \"org\", \"apache\", \"iceberg\", \"hive\", \"HiveCatalog$1.class\") =>\n      MergeStrategy.first\n    case PathList(\n    \"shadedForDelta\",\n    \"org\",\n    \"apache\",\n    \"iceberg\",\n    \"hive\",\n    \"HiveCatalog$ViewAwareTableBuilder.class\"\n    ) =>\n      MergeStrategy.first\n    case PathList(\n    \"shadedForDelta\",\n    \"org\",\n    \"apache\",\n    \"iceberg\",\n    \"hive\",\n    \"HiveCatalog$TableAwareViewBuilder.class\"\n    ) =>\n      MergeStrategy.first\n    case PathList(\n    \"shadedForDelta\",\n    \"org\",\n    \"apache\",\n    \"iceberg\",\n    \"hive\",\n    \"HiveTableOperations.class\"\n    ) =>\n      MergeStrategy.first\n    case PathList(\n    \"shadedForDelta\",\n    \"org\",\n    \"apache\",\n    \"iceberg\",\n    \"hive\",\n    \"HiveTableOperations$1.class\"\n    ) =>\n      MergeStrategy.first\n    case PathList(\"org\", \"slf4j\", xs @ _*) =>\n      // SLF4J is provided by Spark runtime, exclude from assembly\n      MergeStrategy.discard\n    case PathList(\"org\", \"jspecify\", \"annotations\", xs @ _*) =>\n      MergeStrategy.discard\n    case x => prev(x)\n  }\n}\n"
  },
  {
    "path": "project/SparkMimaExcludes.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport com.typesafe.tools.mima.core._\nimport com.typesafe.tools.mima.core.ProblemFilters._\n\n/**\n * The list of Mima errors to exclude.\n */\nobject SparkMimaExcludes {\n  val ignoredABIProblems = Seq(\n      // scalastyle:off line.size.limit\n      ProblemFilters.exclude[Problem](\"org.*\"),\n      ProblemFilters.exclude[Problem](\"io.delta.sql.parser.*\"),\n      ProblemFilters.exclude[Problem](\"io.delta.tables.execution.*\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.apply\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.executeGenerate\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.executeHistory\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.executeVacuum\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.executeVacuum$default$3\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.this\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.deltaLog\"),\n\n      // Changes in 0.6.0\n      ProblemFilters.exclude[IncompatibleResultTypeProblem](\"io.delta.tables.DeltaTable.makeUpdateTable\"),\n      ProblemFilters.exclude[IncompatibleMethTypeProblem](\"io.delta.tables.DeltaMergeBuilder.withClause\"),\n      ProblemFilters.exclude[IncompatibleMethTypeProblem](\"io.delta.tables.DeltaTable.this\"),\n\n      // ... removed unnecessarily public methods in DeltaMergeBuilder\n      ProblemFilters.exclude[MissingTypesProblem](\"io.delta.tables.DeltaMergeBuilder\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordUsage\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordUsage$default$3\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$7\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordUsage$default$6\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logError\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.log\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordDeltaOperation$default$3\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$4\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordEvent$default$3\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logName\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordDeltaEvent\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.withStatusCode$default$3\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.isTraceEnabled\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.withStatusCode\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordEvent\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordDeltaEvent$default$4\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logDebug\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logInfo\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logInfo\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordUsage$default$5\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$6\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logTrace\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.initializeLogIfNecessary\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$9\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordEvent$default$2\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordUsage$default$4\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logWarning\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordUsage$default$7\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordDeltaEvent$default$3\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$2\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordDeltaOperation\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.logConsole\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$5\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordEvent$default$4\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.recordOperation$default$8\"),\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaMergeBuilder.initializeLogIfNecessary$default$2\"),\n\n      // Changes in 0.7.0\n      ProblemFilters.exclude[DirectMissingMethodProblem](\"io.delta.tables.DeltaTable.makeUpdateTable\"),\n\n      // Changes in 1.2.0\n      ProblemFilters.exclude[MissingClassProblem](\"io.delta.storage.LogStore\"),\n      ProblemFilters.exclude[MissingClassProblem](\"io.delta.storage.CloseableIterator\"),\n\n      // Changes in 4.0.0\n      ProblemFilters.exclude[IncompatibleResultTypeProblem](\"io.delta.tables.DeltaTable.improveUnsupportedOpError\"),\n      ProblemFilters.exclude[IncompatibleResultTypeProblem](\"io.delta.tables.DeltaMergeBuilder.improveUnsupportedOpError\"),\n      ProblemFilters.exclude[IncompatibleResultTypeProblem](\"io.delta.tables.DeltaMergeBuilder.execute\"),\n\n      // Changes in 4.1.0\n      // TODO: change in type hierarchy due to removal of DeltaThrowableConditionShim\n      ProblemFilters.exclude[MissingTypesProblem](\"io.delta.exceptions.*\")\n\n      // scalastyle:on line.size.limit\n  )\n}\n\n"
  },
  {
    "path": "project/StandaloneMimaExcludes.scala",
    "content": "/*\n * Copyright (2020-present) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport com.typesafe.tools.mima.core._\n\n/**\n * The list of Mima errors to exclude in the Standalone project.\n */\nobject StandaloneMimaExcludes {\n  val ignoredABIProblems = Seq(\n    // scalastyle:off line.size.limit\n\n    // Ignore changes to internal Scala codes\n    ProblemFilters.exclude[Problem](\"io.delta.standalone.internal.*\"),\n\n    // Public API changes in 0.2.0 -> 0.3.0\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.DeltaLog.getChanges\"),\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.DeltaLog.startTransaction\"),\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.Snapshot.scan\"),\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.DeltaLog.tableExists\"),\n\n    // Switch to using delta-storage LogStore API in 0.4.0 -> 0.5.0\n    ProblemFilters.exclude[MissingClassProblem](\"io.delta.standalone.storage.LogStore\"),\n\n    // Ignore missing shaded attributes\n    ProblemFilters.exclude[Problem](\"shadedelta.*\"),\n\n    // Public API changes in 0.4.0 -> 0.5.0\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.DeltaLog.getVersionBeforeOrAtTimestamp\"),\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.DeltaLog.getVersionAtOrAfterTimestamp\"),\n\n    // ParquetSchemaConverter etc. were moved to project standalone-parquet\n    ProblemFilters.exclude[MissingClassProblem](\"io.delta.standalone.util.ParquetSchemaConverter\"),\n    ProblemFilters.exclude[MissingClassProblem](\"io.delta.standalone.util.ParquetSchemaConverter$ParquetOutputTimestampType\"),\n\n    // Public API changes in 0.5.0 -> 0.6.0\n    ProblemFilters.exclude[ReversedMissingMethodProblem](\"io.delta.standalone.OptimisticTransaction.readVersion\"),\n\n    // scalastyle:on line.size.limit\n  )\n}\n"
  },
  {
    "path": "project/TestParallelization.scala",
    "content": "import scala.util.hashing.MurmurHash3\n\nimport sbt.Keys._\nimport sbt._\n\nobject TestParallelization {\n\n  lazy val numShardsOpt = sys.env.get(\"NUM_SHARDS\").map(_.toInt)\n  lazy val shardIdOpt = sys.env.get(\"SHARD_ID\").map(_.toInt)\n  lazy val testParallelismOpt = sys.env.get(\"TEST_PARALLELISM_COUNT\").map(_.toInt)\n\n  lazy val settings = {\n    println(\n      s\"Test parallelization settings: numShardsOpt=$numShardsOpt, \" +\n          s\"shardIdOpt=$shardIdOpt, testParallelismOpt=$testParallelismOpt\"\n    )\n    if ((numShardsOpt.exists(_ > 1) && shardIdOpt.exists(_ >= 0)) ||\n          testParallelismOpt.exists(_ > 1)) {\n      customTestGroupingSettings ++ simpleGroupingStrategySettings\n    } else {\n      Seq.empty[Setting[_]]\n    }\n  }\n\n  /**\n   * Replace the default value for Test / testGrouping settingKey and set it to a new value\n   * calculated by using the custom Task [[testGroupingStrategy]].\n   *\n   * Adding these settings to the build will require us to separately provide a value for the\n   * TaskKey [[testGroupingStrategy]]\n   */\n  lazy val customTestGroupingSettings = {\n    Seq(\n      Test / testGrouping := {\n        val tests = (Test / definedTests).value\n        val groupingStrategy = (Test / testGroupingStrategy).value\n        val grouping = tests.foldLeft(groupingStrategy) {\n          case (strategy, testDefinition) => strategy.add(testDefinition)\n        }\n        val logger = streams.value.log\n        logger.info(s\"Tests will be grouped in ${grouping.testGroups.size} groups\")\n        val groups = grouping.testGroups\n        groups.foreach { group => logger.info(s\"${group.name} contains ${group.tests.size} tests\") }\n        logger.info(groupingStrategy.toString)\n        groups\n      }\n    )\n  }\n\n  /**\n   * Sets the Test / testGroupingStrategy Task to an instance of the MinShardGroupDurationStrategy\n   */\n  lazy val simpleGroupingStrategySettings = Seq(\n    Test / forkTestJVMCount := {\n      testParallelismOpt.getOrElse(java.lang.Runtime.getRuntime.availableProcessors)\n    },\n    Test / shardId := { shardIdOpt.getOrElse(0) },\n    Test / testGroupingStrategy := {\n      val groupsCount = (Test / forkTestJVMCount).value\n      val shard = (Test / shardId).value\n      val baseJvmDir = baseDirectory.value\n      MinShardGroupDurationStrategy(groupsCount, baseJvmDir, shard, defaultForkOptions.value)\n    },\n    Test / parallelExecution := true,\n    Global / concurrentRestrictions := {\n      Seq(Tags.limit(Tags.ForkedTestGroup, (Test / forkTestJVMCount).value))\n    }\n  )\n\n  val shardId = SettingKey[Int](\"shard id\", \"The shard id assigned\")\n\n  val forkTestJVMCount = SettingKey[Int](\n    \"fork test jvm count\",\n    \"The number of separate JVM to use for tests\"\n  )\n\n  val testGroupingStrategy = TaskKey[GroupingStrategy](\n    \"test grouping strategy\",\n    \"The strategy to allocate different tests into groups,\" +\n        \"potentially using multiple JVMS for their execution\"\n  )\n\n  private val defaultForkOptions = Def.task {\n    ForkOptions(\n      javaHome = javaHome.value,\n      outputStrategy = outputStrategy.value,\n      bootJars = Vector.empty,\n      // Use Test/baseDirectory instead of baseDirectory to support modules where these differ\n      // (e.g. spark-combined module where Test/baseDirectory points to spark/ source directory)\n      workingDirectory = Some((Test / baseDirectory).value),\n      runJVMOptions = (Test / javaOptions).value.toVector,\n      connectInput = connectInput.value,\n      envVars = (Test / envVars).value\n    )\n  }\n\n  /**\n   * Base trait to group tests.\n   *\n   * By default, SBT will run all tests as if they belong to a single group, but allows tests to be\n   * grouped. Setting [[sbt.Keys.testGrouping]] to a list of groups replaces the default\n   * single-group definition.\n   *\n   * When creating an instance of [[sbt.Tests.Group]] it is possible to specify an\n   * [[sbt.Tests.TestRunPolicy]]: this parameter can be used to use multiple subprocesses for test\n   * execution\n   */\n  sealed trait GroupingStrategy {\n\n    /**\n     * Adds an [[sbt.TestDefinition]] to this GroupingStrategy and returns an updated Grouping\n     * Strategy\n     */\n    def add(testDefinition: TestDefinition): GroupingStrategy\n\n    /** Returns the test groups built from this GroupingStrategy */\n    def testGroups: List[Tests.Group]\n  }\n\n  /**\n   * GreedyHashStrategy is a grouping strategy used to distribute test suites across multiple shards\n   * and groups (threads) based on their estimated duration. It aims to balance the test load across\n   * the shards and groups by utilizing a greedy assignment algorithm that assigns test suites to\n   * the group with the smallest estimated runtime.\n   *\n   * @param groups The initial mapping of group indices to their respective [[sbt.Tests.Group]]\n   *               objects, which hold test definitions.\n   * @param shardId The shard ID that this instance is responsible for.\n   * @param highDurationTestAssignment Precomputed assignments of high-duration test suites to\n   *                                   specific groups within the shard.\n   * @param groupRuntimes Array holding the current total runtime for each group within the shard.\n   */\n  class MinShardGroupDurationStrategy private(\n      groups: scala.collection.mutable.Map[Int, Tests.Group],\n      shardId: Int,\n      highDurationTestAssignment: Array[Set[String]],\n      var groupRuntimes: Array[Double]\n  ) extends GroupingStrategy {\n    import TestParallelization.MinShardGroupDurationStrategy._\n\n    if (shardId < 0 || shardId >= NUM_SHARDS) {\n      throw new IllegalArgumentException(\n        s\"Assigned shard ID $shardId is not between 0 and ${NUM_SHARDS - 1} inclusive\")\n    }\n\n    lazy val testGroups = groups.values.toList\n\n    override def add(testDefinition: TestDefinition): GroupingStrategy = {\n      val testSuiteName = testDefinition.name\n      val isHighDurationTest = TOP_N_HIGH_DURATION_TEST_SUITES.exists(_._1 == testSuiteName)\n\n      if (isHighDurationTest) {\n        val highDurationTestGroupIndex =\n          highDurationTestAssignment.indexWhere(_.contains(testSuiteName))\n\n        if (highDurationTestGroupIndex >= 0) {\n          // Case 1: this is a high duration test that was pre-computed in the optimal assignment to\n          // belong to this shard. Assign it.\n          val duration = TOP_N_HIGH_DURATION_TEST_SUITES.find(_._1 == testSuiteName).get._2\n\n          val currentGroup = groups(highDurationTestGroupIndex)\n          val updatedGroup = currentGroup.withTests(currentGroup.tests :+ testDefinition)\n          groups(highDurationTestGroupIndex) = updatedGroup\n\n          // Do NOT update groupRuntimes -- this was already included in the initial value of\n          // groupRuntimes\n\n          this\n        } else {\n          // Case 2: this is a high duration test that does NOT belong to this shard. Skip it.\n          this\n        }\n      } else if (math.abs(MurmurHash3.stringHash(testDefinition.name) % NUM_SHARDS) == shardId) {\n        // Case 3: this is a normal test that belongs to this shard. Assign it.\n\n        val minDurationGroupIndex = groupRuntimes.zipWithIndex.minBy(_._1)._2\n        val currentGroup = groups(minDurationGroupIndex)\n        val updatedGroup = currentGroup.withTests(currentGroup.tests :+ testDefinition)\n        groups(minDurationGroupIndex) = updatedGroup\n\n        groupRuntimes(minDurationGroupIndex) += AVG_TEST_SUITE_DURATION_EXCLUDING_TOP_N\n\n        this\n      } else {\n        // Case 4: this is a normal test that does NOT belong to this shard. Skip it.\n        this\n      }\n    }\n\n    override def toString: String = {\n      val actualDurationsStr = groupRuntimes.zipWithIndex.map {\n        case (actualDuration, groupIndex) =>\n          f\"  Group $groupIndex: Estimated Duration = $actualDuration%.2f mins, \" +\n              f\"Count = ${groups(groupIndex).tests.size}\"\n      }.mkString(\"\\n\")\n\n      s\"\"\"\n         |Shard ID: $shardId\n         |Suite Group Assignments:\n         |$actualDurationsStr\n      \"\"\".stripMargin\n    }\n  }\n\n  object MinShardGroupDurationStrategy {\n\n    val NUM_SHARDS = numShardsOpt.getOrElse(1)\n\n    val AVG_TEST_SUITE_DURATION_EXCLUDING_TOP_N = 0.83\n\n    /**\n     * High-duration test suites loaded from project/test-durations.csv.\n     *\n     * To update, run: python3 project/scripts/collect_test_durations.py\n     */\n    val TOP_N_HIGH_DURATION_TEST_SUITES: List[(String, Double)] = {\n      val csvFile = new java.io.File(\"project/test-durations.csv\")\n      if (!csvFile.exists()) {\n        println(s\"Warning: ${csvFile.getPath} not found, using empty test durations\")\n        List.empty\n      } else {\n        val source = scala.io.Source.fromFile(csvFile)\n        try {\n          source.getLines().drop(1).filter(_.trim.nonEmpty).map { line =>\n            val idx = line.lastIndexOf(',')\n            (line.substring(0, idx), line.substring(idx + 1).toDouble)\n          }.toList\n        } finally {\n          source.close()\n        }\n      }\n    }\n\n    /**\n     * Generates the optimal test assignment across shards and groups for high duration test suites.\n     *\n     * Will assign the high duration test suites in descending order, always assigning to the\n     * group with the smallest total duration. In case of ties (e.g. early on when some group\n     * durations are still 0, will assign to the shard with the smallest total duration).\n     *\n     * Here's a simple example using 3 shards and 2 groups per shard:\n     *\n     * Test 1: DeltaRetentionWithCatalogOwnedBatch1Suite (22.66 mins) --> Shard 0, Group 0\n     * - Shard 0: Group 0 = 22.66 mins, Group 1 = 0.0 mins\n     * - Shard 1: Group 0 = 0.0 mins, Group 1 = 0.0 mins\n     * - Shard 2: Group 0 = 0.0 mins, Group 1 = 0.0 mins\n     *\n     * Test 2: DeltaRetentionSuite (21.46 mins) --> Shard 1, Group 0\n     * - Shard 0: Group 0 = 22.66 mins, Group 1 = 0.0 mins\n     * - Shard 1: Group 0 = 21.46 mins, Group 1 = 0.0 mins\n     * - Shard 2: Group 0 = 0.0 mins, Group 1 = 0.0 mins\n     *\n     * Test 3: DeletionVectorsSuite (16.85 mins) --> Shard 2, Group 0\n     * - Shard 0: Group 0 = 22.66 mins, Group 1 = 0.0 mins\n     * - Shard 1: Group 0 = 21.46 mins, Group 1 = 0.0 mins\n     * - Shard 2: Group 0 = 16.85 mins, Group 1 = 0.0 mins\n     *\n     * Test 4: DataSkippingDeltaV1WithCatalogOwnedBatch100Suite (12.48 mins) --> Shard 2, Group 1\n     * - Shard 0: Group 0 = 22.66 mins, Group 1 = 0.0 mins\n     * - Shard 1: Group 0 = 21.46 mins, Group 1 = 0.0 mins\n     * - Shard 2: Group 0 = 16.85 mins, Group 1 = 12.48 mins\n     *\n     * Test 5: DataSkippingDeltaV1WithCatalogOwnedBatch2Suite (11.68 mins) --> Shard 1, Group 1\n     * - Shard 0: Group 0 = 22.66 mins, Group 1 = 0.0 mins\n     * - Shard 1: Group 0 = 21.46 mins, Group 1 = 11.68 mins\n     * - Shard 2: Group 0 = 16.85 mins, Group 1 = 12.48 mins\n     *\n     * Test 6: DeltaFastDropFeatureSuite (11.04 mins) --> Shard 0, Group 1\n     * - Shard 0: Group 0 = 22.66 mins, Group 1 = 11.04 mins\n     * - Shard 1: Group 0 = 21.46 mins, Group 1 = 11.68 mins\n     * - Shard 2: Group 0 = 16.85 mins, Group 1 = 12.48 mins\n     */\n    def highDurationOptimalAssignment(numGroups: Int):\n        (Array[Array[Set[String]]], Array[Array[Double]]) = {\n      val assignment = Array.fill(NUM_SHARDS)(Array.fill(numGroups)(List.empty[String]))\n      val groupDurations = Array.fill(NUM_SHARDS)(Array.fill(numGroups)(0.0))\n      val shardDurations = Array.fill(NUM_SHARDS)(0.0)\n      val sortedTestSuites = TOP_N_HIGH_DURATION_TEST_SUITES.sortBy(-_._2)\n\n      sortedTestSuites.foreach { case (testSuiteName, duration) =>\n        val (shardIdx, groupIdx) =\n          findShardAndGroupWithLowestDuration(numGroups, shardDurations, groupDurations)\n\n        assignment(shardIdx)(groupIdx) = assignment(shardIdx)(groupIdx) :+ testSuiteName\n        groupDurations(shardIdx)(groupIdx) += duration\n        shardDurations(shardIdx) += duration\n      }\n\n      (assignment.map(_.map(_.toSet)), groupDurations)\n    }\n\n    /**\n     * Finds the best shard and group to assign the next test suite.\n     *\n     * Selects the group with the smallest total duration, and in case of ties, selects the shard\n     * with the smallest total duration.\n     *\n     * @param numShards      Number of shards\n     * @param numGroups      Number of groups per shard\n     * @param shardDurations Total duration per shard\n     * @param groupDurations Total duration per group in each shard\n     * @return Tuple of (shard index, group index) for the optimal assignment\n     */\n    private def findShardAndGroupWithLowestDuration(\n        numGroups: Int,\n        shardDurations: Array[Double],\n        groupDurations: Array[Array[Double]]): (Int, Int) = {\n      var bestShardIdx = -1\n      var bestGroupIdx = -1\n      var minGroupDuration = Double.MaxValue\n      var minShardDuration = Double.MaxValue\n\n      for (shardIdx <- 0 until NUM_SHARDS) {\n        for (groupIdx <- 0 until numGroups) {\n          val currentGroupDuration = groupDurations(shardIdx)(groupIdx)\n          val currentShardDuration = shardDurations(shardIdx)\n\n          if (currentGroupDuration < minGroupDuration ||\n              (currentGroupDuration == minGroupDuration &&\n                currentShardDuration < minShardDuration)) {\n            minGroupDuration = currentGroupDuration\n            minShardDuration = currentShardDuration\n            bestShardIdx = shardIdx\n            bestGroupIdx = groupIdx\n          }\n        }\n      }\n\n      (bestShardIdx, bestGroupIdx)\n    }\n\n    def apply(\n        groupCount: Int,\n        baseDir: File,\n        shardId: Int,\n        forkOptionsTemplate: ForkOptions): GroupingStrategy = {\n      val testGroups = scala.collection.mutable.Map((0 until groupCount).map {\n        groupIdx =>\n          val tmpDir = s\"$baseDir/target/tmp/$groupIdx\"\n          java.nio.file.Files.createDirectories(java.nio.file.Paths.get(tmpDir))\n\n          val forkOptions = forkOptionsTemplate.withRunJVMOptions(\n            runJVMOptions = forkOptionsTemplate.runJVMOptions ++\n                Seq(s\"-Djava.io.tmpdir=$tmpDir\")\n          )\n          val group = Tests.Group(\n            name = s\"Test group $groupIdx\",\n            tests = Nil,\n            runPolicy = Tests.SubProcess(forkOptions)\n          )\n          groupIdx -> group\n      }: _*)\n\n      val (allShardsTestAssignments, allShardsGroupDurations) =\n        highDurationOptimalAssignment(groupCount)\n\n      new MinShardGroupDurationStrategy(\n        testGroups,\n        shardId,\n        allShardsTestAssignments(shardId),\n        allShardsGroupDurations(shardId)\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "project/Unidoc.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport sbt._\nimport sbt.Keys._\nimport sbtunidoc._\nimport sbtunidoc.BaseUnidocPlugin.autoImport._\nimport sbtunidoc.ScalaUnidocPlugin.autoImport._\nimport sbtunidoc.JavaUnidocPlugin.autoImport._\n\nobject Unidoc {\n\n  /**\n   * Patterns are strings to do simple substring matches on the full path of every source file.\n   */\n  case class SourceFilePattern(patterns: Seq[String], project: Option[Project] = None)\n\n  object SourceFilePattern {\n    def apply(patterns: String*): SourceFilePattern = SourceFilePattern(patterns.toSeq, None)\n  }\n\n  val unidocSourceFilePatterns = settingKey[Seq[SourceFilePattern]](\n      \"Patterns to match (simple substring match) against full source file paths. \" +\n        \"Matched files will be selected for generating API docs.\")\n\n  implicit class PatternsHelper(patterns: Seq[SourceFilePattern]) {\n    def scopeToProject(projectToAdd: Project): Seq[SourceFilePattern] = {\n      patterns.map(_.copy(project = Some(projectToAdd)))\n    }\n  }\n\n  implicit class UnidocHelper(val projectToUpdate: Project) {\n    def configureUnidoc(\n      docTitle: String = null,\n      generatedJavaDoc: Boolean = true,\n      generateScalaDoc: Boolean = false,\n      classPathToSkip: String = null\n    ): Project = {\n      if (sys.env.contains(\"DISABLE_UNIDOC\")) return projectToUpdate\n      if (!generatedJavaDoc && !generateScalaDoc) return projectToUpdate\n\n      var updatedProject: Project = projectToUpdate\n      if (generateScalaDoc) {\n        updatedProject = updatedProject.enablePlugins(ScalaUnidocPlugin)\n      }\n      updatedProject\n        .enablePlugins(GenJavadocPlugin, JavaUnidocPlugin)\n        // TODO: Allows maven publishing to use unidoc doc jar, but it currently throws errors.\n        // .enablePlugins(PublishJavadocPlugin)\n        .settings(\n          libraryDependencies ++= Seq(\n            // Ensure genJavaDoc plugin is of the right version that works with Scala 2.13.16\n            compilerPlugin(\n              \"com.typesafe.genjavadoc\" %% \"genjavadoc-plugin\" % \"0.19\" cross CrossVersion.full)\n          ),\n\n          generateUnidocSettings(docTitle, generateScalaDoc, classPathToSkip),\n\n          // Ensure unidoc is run with tests.\n          (Test / test) := ((Test / test) dependsOn (Compile / unidoc)).value,\n\n          // hide package private types and methods in javadoc\n          scalacOptions ++= Seq(\n            \"-P:genjavadoc:strictVisibility=true\"\n          ),\n        )\n    }\n\n    private def generateUnidocSettings(\n        customDocTitle: String,\n        generateScalaDoc: Boolean,\n        classPathToSkip : String): Def.SettingsDefinition = {\n\n      val internalFilePattern = Seq(\"/internal/\", \"/execution/\", \"$\")\n\n      // Generate the full doc title\n      def fullDocTitle(projectName: String, version: String, isScalaDoc: Boolean): String = {\n        val namePart = Option(customDocTitle).getOrElse {\n          projectName.split(\"-\").map(_.capitalize).mkString(\" \")\n        }\n        val versionPart = version.replaceAll(\"-SNAPSHOT\", \"\")\n        val langPart = if (isScalaDoc) \"Scala API Docs\" else \"Java API Docs\"\n        s\"$namePart $versionPart - $langPart\"\n      }\n\n      // Remove source files that does not match the pattern\n      def ignoreUndocumentedSources(\n        allSourceFiles: Seq[Seq[java.io.File]],\n        sourceFilePatternsToKeep: Seq[SourceFilePattern]\n      ): Seq[Seq[java.io.File]] = {\n        if (sourceFilePatternsToKeep.isEmpty) return Nil\n\n        val projectSrcDirToFilePatternsToKeep = sourceFilePatternsToKeep.map {\n          case SourceFilePattern(dirs, projOption) =>\n            val projectPath = projOption.getOrElse(projectToUpdate).base.getCanonicalPath\n            projectPath -> dirs\n        }.toMap\n\n        def shouldKeep(path: String): Boolean = {\n          projectSrcDirToFilePatternsToKeep.foreach { case (projBaseDir, filePatterns) =>\n            def isInProjectSrcDir =\n              path.contains(s\"$projBaseDir/src\") || path.contains(s\"$projBaseDir/target/java/\")\n            def matchesFilePattern = filePatterns.exists(path.contains(_))\n            def matchesInternalFilePattern = internalFilePattern.exists(path.contains(_))\n            if (isInProjectSrcDir && matchesFilePattern && !matchesInternalFilePattern) return true\n          }\n          false\n        }\n        allSourceFiles.map {_.filter(f => shouldKeep(f.getCanonicalPath))}\n      }\n\n      val javaUnidocSettings = Seq(\n        // Configure Java unidoc\n        JavaUnidoc / unidoc / javacOptions := Seq(\n          \"-public\",\n          \"-windowtitle\",\n          fullDocTitle((projectToUpdate / name).value, version.value, isScalaDoc = false),\n          \"-noqualifier\", \"java.lang\",\n          \"-tag\", \"implNote:a:Implementation Note:\",\n          \"-tag\", \"apiNote:a:API Note:\",\n          \"-Xdoclint:none\"\n        ),\n\n        JavaUnidoc / unidoc / unidocAllSources := {\n          ignoreUndocumentedSources(\n            allSourceFiles = (JavaUnidoc / unidoc / unidocAllSources).value,\n            sourceFilePatternsToKeep = unidocSourceFilePatterns.value)\n        },\n\n        // Settings for plain, old Java doc needed for successful doc generation during publishing.\n        Compile / doc / javacOptions ++= Seq(\n          \"-public\",\n          \"-noqualifier\", \"java.lang\",\n          \"-tag\", \"implNote:a:Implementation Note:\",\n          \"-tag\", \"apiNote:a:API Note:\",\n          \"-Xdoclint:all\")\n      )\n\n      val scalaUnidocSettings = if (generateScalaDoc) Seq(\n        // Configure Scala unidoc\n        ScalaUnidoc / unidoc / scalacOptions ++= Seq(\n          \"-doc-title\",\n          fullDocTitle((projectToUpdate / name).value, version.value, isScalaDoc = true),\n        ),\n\n        ScalaUnidoc / unidoc / unidocAllSources := {\n          ignoreUndocumentedSources(\n            allSourceFiles = (ScalaUnidoc / unidoc / unidocAllSources).value,\n            sourceFilePatternsToKeep = unidocSourceFilePatterns.value\n          )\n        },\n\n        ScalaUnidoc / unidoc / fullClasspath := {\n          (ScalaUnidoc / unidoc / fullClasspath).value\n            .filter(f =>\n              classPathToSkip == null || !f.data.getCanonicalPath.contains(classPathToSkip))\n        }\n      ) else Nil\n\n      javaUnidocSettings ++ scalaUnidocSettings\n    }\n  }\n}\n\n"
  },
  {
    "path": "project/build.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# This file contains code from the Apache Spark project (original license above).\n# It contains modifications, which are licensed as follows:\n#\n\n#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n\nsbt.version=1.9.9\n"
  },
  {
    "path": "project/plugins.sbt",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlibraryDependencies += \"org.apache.commons\" % \"commons-compress\" % \"1.0\"\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-release\" % \"1.1.0\")\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-pgp\" % \"2.1.2\")\n\naddSbtPlugin(\"org.scalastyle\" %% \"scalastyle-sbt-plugin\" % \"1.0.0\")\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-unidoc\" % \"0.5.0\")\n\naddSbtPlugin(\"com.eed3si9n\" % \"sbt-assembly\" % \"2.1.0\")\n\naddSbtPlugin(\"com.typesafe\" % \"sbt-mima-plugin\" % \"1.1.3\")\n\naddSbtPlugin(\"com.simplytyped\" % \"sbt-antlr4\" % \"0.8.3\")\n\naddSbtPlugin(\"org.xerial.sbt\" % \"sbt-sonatype\" % \"3.11.3\")\n\naddSbtPlugin(\"org.scoverage\" % \"sbt-scoverage\" % \"2.4.0\")\n//Upgrade sbt-scoverage to 2.0.3+ because 2.0.0 is not compatible to Scala 2.12.17:\n//sbt.librarymanagement.ResolveException: Error downloading org.scoverage:scalac-scoverage-plugin_2.12.17:2.0.0\n\n//It caused a conflict issue:\n//[error] java.lang.RuntimeException: found version conflict(s) in library dependencies; some are suspected to be binary incompatible:\n//[error] \n//[error] \t* org.scala-lang.modules:scala-xml_2.12:2.1.0 (early-semver) is selected over 1.0.6\n//[error] \t    +- org.scoverage:scalac-scoverage-reporter_2.12:2.0.7 (depends on 2.1.0)\n//[error] \t    +- org.scalariform:scalariform_2.12:0.2.0             (depends on 1.0.6)\n//The following fix the conflict:\nlibraryDependencySchemes += \"org.scala-lang.modules\" %% \"scala-xml\" % VersionScheme.Always % \"test\"\n\naddSbtPlugin(\"com.github.sbt.junit\" % \"sbt-jupiter-interface\" % \"0.17.0\")\n\naddSbtPlugin(\"software.purpledragon\" % \"sbt-checkstyle-plugin\" % \"4.0.1\")\n// By default, sbt-checkstyle-plugin uses checkstyle version 6.15, but we should set it to use the\n// same version as Spark\ndependencyOverrides += \"com.puppycrawl.tools\" % \"checkstyle\" % \"9.3\"\n\naddSbtPlugin(\"com.thesamet\" % \"sbt-protoc\" % \"1.0.7\")\n\naddSbtPlugin(\"com.lightbend.sbt\" % \"sbt-java-formatter\" % \"0.8.0\")\n\naddSbtPlugin(\"org.scalameta\" % \"sbt-scalafmt\" % \"2.5.4\")\n"
  },
  {
    "path": "project/project/plugins.sbt",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\naddSbtPlugin(\"com.eed3si9n\" % \"sbt-assembly\" % \"0.14.9\")\n"
  },
  {
    "path": "project/scripts/collect_test_durations.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nCollect per-suite test durations from CI and write project/test-durations.csv.\n\nUsage:\n    # Update test-durations.csv from last 30 successful runs on master (default):\n    python3 project/scripts/collect_test_durations.py\n\n    # Use more runs for averaging:\n    python3 project/scripts/collect_test_durations.py --last-n-runs 50\n\n    # Time-bound the run to 5 minutes (stops downloading more artifacts after the limit):\n    python3 project/scripts/collect_test_durations.py --max-minutes 5\n\n    # Disable the time limit (process all --last-n-runs runs unconditionally):\n    python3 project/scripts/collect_test_durations.py --max-minutes 0\n\nRequirements:\n    - gh CLI authenticated with access to delta-io/delta\n    - Python 3.6+\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport tempfile\nimport time\nimport zipfile\nfrom collections import defaultdict\nfrom xml.etree import ElementTree\n\n\nREPO = \"delta-io/delta\"\nCSV_FILE = \"project/test-durations.csv\"\nDEFAULT_LAST_N_RUNS = 30\nDEFAULT_TOP_N = 200\nDEFAULT_MAX_MINUTES = 5\n\n\ndef run_gh(args):\n    \"\"\"Run a gh CLI command and return stdout.\"\"\"\n    cmd = [\"gh\"] + args\n    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n    if result.returncode != 0:\n        print(f\"Error running: {' '.join(cmd)}\", file=sys.stderr)\n        print(result.stderr.decode(), file=sys.stderr)\n        sys.exit(1)\n    return result.stdout.decode()\n\n\ndef get_run_ids(last_n):\n    \"\"\"Find the latest N successful workflow run IDs on master.\"\"\"\n    runs_json = run_gh([\n        \"run\", \"list\", \"--repo\", REPO,\n        \"--branch\", \"master\",\n        \"--workflow\", \"spark_test.yaml\",\n        \"--status\", \"success\",\n        \"--limit\", str(last_n),\n        \"--json\", \"databaseId,headSha,createdAt\"\n    ])\n    runs = json.loads(runs_json)\n    if not runs:\n        print(\"No successful runs found on master\", file=sys.stderr)\n        sys.exit(1)\n\n    for run in runs:\n        print(f\"  Run {run['databaseId']} (commit: {run['headSha'][:8]}, \"\n              f\"created: {run['createdAt']})\")\n\n    return [run[\"databaseId\"] for run in runs]\n\n\ndef list_artifacts(run_id):\n    \"\"\"List test-report artifacts from a workflow run.\"\"\"\n    artifacts_json = run_gh([\n        \"api\", f\"repos/{REPO}/actions/runs/{run_id}/artifacts\",\n        \"--paginate\", \"--jq\", \".artifacts[]\"\n    ])\n\n    artifacts = []\n    for line in artifacts_json.strip().split('\\n'):\n        if not line.strip():\n            continue\n        art = json.loads(line)\n        match = re.match(r\"test-reports-spark([\\d.]+)-shard(\\d+)\", art[\"name\"])\n        if match:\n            artifacts.append({\n                \"id\": art[\"id\"],\n                \"spark_version\": match.group(1),\n                \"shard\": int(match.group(2)),\n            })\n\n    artifacts.sort(key=lambda a: (a[\"spark_version\"], a[\"shard\"]))\n    return artifacts\n\n\ndef download_and_extract(artifact_id, dest_dir):\n    \"\"\"Download and extract a GitHub Actions artifact zip.\"\"\"\n    zip_path = os.path.join(dest_dir, f\"{artifact_id}.zip\")\n    cmd = [\"gh\", \"api\", f\"repos/{REPO}/actions/artifacts/{artifact_id}/zip\"]\n    with open(zip_path, 'wb') as f:\n        result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE)\n    if result.returncode != 0:\n        print(f\"Error downloading artifact {artifact_id}\", file=sys.stderr)\n        sys.exit(1)\n\n    extract_dir = os.path.join(dest_dir, str(artifact_id))\n    os.makedirs(extract_dir, exist_ok=True)\n    with zipfile.ZipFile(zip_path, 'r') as zf:\n        zf.extractall(extract_dir)\n    return extract_dir\n\n\ndef parse_junit_xmls(directory):\n    \"\"\"Parse all JUnit XML files in a directory. Returns {suite_name: duration_minutes}.\"\"\"\n    durations = {}\n    for dirpath, _, filenames in os.walk(directory):\n        for fname in filenames:\n            if not fname.endswith('.xml'):\n                continue\n            try:\n                tree = ElementTree.parse(os.path.join(dirpath, fname))\n            except ElementTree.ParseError:\n                continue\n\n            root = tree.getroot()\n            suites = [root] if root.tag == \"testsuite\" else root.findall(\"testsuite\")\n            for suite in suites:\n                name = suite.get(\"name\", \"\")\n                if not name:\n                    continue\n                try:\n                    dur = round(float(suite.get(\"time\", \"0\")) / 60, 2)\n                except ValueError:\n                    continue\n                if name not in durations or dur > durations[name]:\n                    durations[name] = dur\n    return durations\n\n\ndef collect_from_run(run_id, tmpdir):\n    \"\"\"Collect test durations from a single CI run.\"\"\"\n    artifacts = list_artifacts(run_id)\n    if not artifacts:\n        print(f\"  No artifacts in run {run_id}\", file=sys.stderr)\n        return {}\n\n    run_durations = {}\n    for art in artifacts:\n        print(f\"    Spark {art['spark_version']}, Shard {art['shard']}...\")\n        artifact_dir = download_and_extract(art[\"id\"], tmpdir)\n        for name, dur in parse_junit_xmls(artifact_dir).items():\n            if name not in run_durations or dur > run_durations[name]:\n                run_durations[name] = dur\n\n    return run_durations\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Collect test durations from CI and update project/test-durations.csv\"\n    )\n    parser.add_argument(\"--last-n-runs\", type=int, default=DEFAULT_LAST_N_RUNS,\n                        help=f\"Number of runs to average (default: {DEFAULT_LAST_N_RUNS})\")\n    parser.add_argument(\"--top-n\", type=int, default=DEFAULT_TOP_N,\n                        help=f\"Number of slowest suites to keep (default: {DEFAULT_TOP_N})\")\n    parser.add_argument(\"--max-minutes\", type=float, default=DEFAULT_MAX_MINUTES,\n                        help=f\"Stop downloading after this many minutes (default: {DEFAULT_MAX_MINUTES},\"\n                             \" 0 = no limit)\")\n    args = parser.parse_args()\n\n    # Fetch run IDs\n    print(\"Finding recent successful runs on master...\")\n    run_ids = get_run_ids(args.last_n_runs)\n\n    deadline = time.monotonic() + args.max_minutes * 60 if args.max_minutes > 0 else None\n\n    # Collect durations from each run\n    all_durations = defaultdict(list)\n    with tempfile.TemporaryDirectory() as tmpdir:\n        for i, run_id in enumerate(run_ids):\n            if deadline is not None and time.monotonic() >= deadline:\n                print(f\"\\nTime limit reached after {i} run(s); stopping early.\")\n                break\n            print(f\"\\nRun {i + 1}/{len(run_ids)}: {run_id}\")\n            for name, dur in collect_from_run(run_id, tmpdir).items():\n                all_durations[name].append(dur)\n\n    # Average and sort\n    averaged = {\n        name: round(sum(durs) / len(durs), 2)\n        for name, durs in all_durations.items()\n    }\n    sorted_suites = sorted(averaged.items(), key=lambda x: -x[1])[:args.top_n]\n\n    if not sorted_suites:\n        print(\"\\nNo test-report artifacts found. Has the JUnit XML upload been enabled on master?\")\n        print(f\"{CSV_FILE} was NOT modified.\")\n        sys.exit(1)\n\n    # Write CSV\n    with open(CSV_FILE, 'w') as f:\n        f.write(\"suite_name,duration_minutes\\n\")\n        for name, dur in sorted_suites:\n            f.write(f\"{name},{dur}\\n\")\n\n    print(f\"\\nWrote {len(sorted_suites)} suites to {CSV_FILE}\")\n    print(f\"Total duration: {sum(d for _, d in sorted_suites):.1f} min\")\n    print(f\"Slowest: {sorted_suites[0][0]} ({sorted_suites[0][1]} min)\")\n    print(f\"Fastest included: {sorted_suites[-1][0]} ({sorted_suites[-1][1]} min)\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "project/scripts/get_spark_version_info.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nGenerate Spark version information for CI/CD from CrossSparkVersions.scala\n\nThis script reads the JSON file generated by `build/sbt exportSparkVersionsJson`\nand provides utilities for GitHub Actions workflows.\n\nThe script automatically generates the JSON file if it doesn't exist.\n\nUsage:\n    # Get all Spark versions as JSON array\n    python project/scripts/get_spark_version_info.py --all-spark-versions\n    # Output: [\"4.0\", \"4.1\"] or [\"master\", \"4.0\"] if master is present\n\n    # Get only released Spark versions (no snapshots)\n    python project/scripts/get_spark_version_info.py --released-spark-versions\n    # Output: [\"4.0\", \"4.1\"] (excludes versions with -SNAPSHOT)\n\n    # Get a specific field for a Spark version (using short version or \"master\")\n    python project/scripts/get_spark_version_info.py --get-field 4.0 targetJvm\n    python project/scripts/get_spark_version_info.py --get-field master targetJvm\n    # Output: \"17\"\n\"\"\"\n\nimport argparse\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\n\n\ndef generate_spark_versions_json(repo_root: Path) -> bool:\n    \"\"\"Generate the spark-versions.json file by running sbt exportSparkVersionsJson.\"\"\"\n    try:\n        print(\"Generating spark-versions.json...\", file=sys.stderr)\n        subprocess.run(\n            [\"build/sbt\", \"exportSparkVersionsJson\"],\n            cwd=repo_root,\n            check=True,\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.PIPE\n        )\n        return True\n    except subprocess.CalledProcessError as e:\n        print(f\"ERROR: Failed to generate spark-versions.json: {e}\", file=sys.stderr)\n        return False\n\n\ndef load_spark_versions(json_path: Path, repo_root: Path):\n    \"\"\"Load Spark versions from JSON file, generating it if necessary.\"\"\"\n    if not json_path.exists():\n        if not generate_spark_versions_json(repo_root):\n            sys.exit(1)\n    \n    if not json_path.exists():\n        print(\n            f\"ERROR: spark-versions.json not found at {json_path} even after generation\",\n            file=sys.stderr\n        )\n        sys.exit(1)\n\n    with open(json_path, 'r') as f:\n        return json.load(f)\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Generate Spark version information from CrossSparkVersions.scala\"\n    )\n    parser.add_argument(\n        \"--all-spark-versions\",\n        action=\"store_true\",\n        help=\"Output all Spark versions as JSON array (e.g., [\\\"4.0\\\", \\\"4.1\\\"] or [\\\"master\\\", \\\"4.0\\\"])\"\n    )\n    parser.add_argument(\n        \"--released-spark-versions\",\n        action=\"store_true\",\n        help=\"Output only released Spark versions (excluding snapshots) as JSON array\"\n    )\n    parser.add_argument(\n        \"--get-field\",\n        nargs=2,\n        metavar=(\"SPARK_VERSION\", \"FIELD\"),\n        help=\"Get a specific field for a Spark version (e.g., --get-field 4.0 targetJvm or --get-field master targetJvm)\"\n    )\n\n    args = parser.parse_args()\n\n    # Determine JSON path (relative to repo root)\n    script_dir = Path(__file__).parent\n    repo_root = script_dir.parent.parent\n    json_path = repo_root / \"target\" / \"spark-versions.json\"\n\n    try:\n        versions = load_spark_versions(json_path, repo_root)\n\n        if args.all_spark_versions:\n            # For master version, use \"master\"; for others, use short version\n            matrix_versions = []\n            for v in versions:\n                if v.get(\"isMaster\", False):\n                    matrix_versions.append(\"master\")\n                else:\n                    matrix_versions.append(v[\"shortVersion\"])\n            print(json.dumps(matrix_versions))\n\n        elif args.released_spark_versions:\n            # Only include released versions (no -SNAPSHOT in fullVersion)\n            matrix_versions = []\n            for v in versions:\n                if \"-SNAPSHOT\" not in v[\"fullVersion\"]:\n                    matrix_versions.append(v[\"shortVersion\"])\n            print(json.dumps(matrix_versions))\n\n        elif args.get_field:\n            spark_version, field = args.get_field\n            \n            # Find the version entry by matching:\n            # - \"master\" matches isMaster=true\n            # - short version like \"4.0\" matches shortVersion\n            # - full version like \"4.0.1\" matches fullVersion\n            version_entry = None\n            for v in versions:\n                if spark_version == \"master\" and v.get(\"isMaster\", False):\n                    version_entry = v\n                    break\n                elif spark_version == v[\"shortVersion\"] or spark_version == v[\"fullVersion\"]:\n                    version_entry = v\n                    break\n\n            if not version_entry:\n                print(f\"ERROR: Spark version '{spark_version}' not found\", file=sys.stderr)\n                sys.exit(1)\n\n            if field not in version_entry:\n                print(\n                    f\"ERROR: Field '{field}' not found for Spark version {spark_version}\\n\"\n                    f\"Available fields: {', '.join(version_entry.keys())}\",\n                    file=sys.stderr\n                )\n                sys.exit(1)\n\n            # Print as JSON for proper formatting\n            print(json.dumps(version_entry[field]))\n\n        else:\n            parser.print_help()\n            sys.exit(1)\n\n    except Exception as e:\n        print(f\"ERROR: {e}\", file=sys.stderr)\n        import traceback\n        traceback.print_exc()\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "project/test-durations.csv",
    "content": "suite_name,duration_minutes\norg.apache.spark.sql.delta.DeltaRetentionWithCatalogOwnedBatch1Suite,22.66\norg.apache.spark.sql.delta.DeltaRetentionSuite,21.46\norg.apache.spark.sql.delta.deletionvectors.DeletionVectorsSuite,16.85\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1WithCatalogOwnedBatch100Suite,12.48\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1WithCatalogOwnedBatch2Suite,11.68\norg.apache.spark.sql.delta.DeltaFastDropFeatureSuite,11.04\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1ParquetCheckpointV2Suite,10.29\nio.delta.sharing.spark.DeltaSharingDataSourceDeltaSuite,10.28\norg.apache.spark.sql.delta.DeltaSourceLargeLogWithCoordinatedCommitsBatch1Suite,9.89\norg.apache.spark.sql.delta.DeltaSourceWithCoordinatedCommitsBatch100Suite,9.2\norg.apache.spark.sql.delta.DeltaSourceLargeLogSuite,8.86\norg.apache.spark.sql.delta.DeltaInsertIntoSchemaEvolutionSuite,8.7\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1WithCatalogOwnedBatch1Suite,8.68\norg.apache.spark.sql.delta.DeltaSourceWithCoordinatedCommitsBatch1Suite,8.64\norg.apache.spark.sql.delta.DeltaSourceWithCoordinatedCommitsBatch10Suite,8.61\norg.apache.spark.sql.delta.CheckpointsWithCatalogOwnedBatch1Suite,8.55\norg.apache.spark.sql.delta.DeltaSourceLargeLogWithCoordinatedCommitsBatch100Suite,8.39\norg.apache.spark.sql.delta.DeltaVacuumSuite,8.16\norg.apache.spark.sql.delta.ImplicitMergeCastingSuite,8.12\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1JsonCheckpointV2Suite,7.87\norg.apache.spark.sql.delta.DescribeDeltaHistoryWithCatalogOwnedBatch100Suite,7.73\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1NameColumnMappingSuite,7.58\norg.apache.spark.sql.delta.typewidening.TypeWideningInsertSchemaEvolutionExtendedSuite,7.3\norg.apache.spark.sql.delta.DeltaCDCScalaWithCatalogOwnedBatch2Suite,7.24\norg.apache.spark.sql.delta.DeltaInsertIntoMissingColumnSuite,7.08\norg.apache.spark.sql.delta.DescribeDeltaHistorySuite,6.76\norg.apache.spark.sql.delta.generatedsuites.DeltaInsertIntoImplicitCastSuite,6.63\nio.delta.sharing.spark.DeltaSharingDataSourceCMSuite,6.43\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedMapStructEvolutionNullnessSQLNameBasedPreserveNullSourceOffPreserveNullSourceUpd7CQVRRQSuite,6.38\nio.delta.sharing.spark.DeltaFormatSharingSourceSuite,6.26\norg.apache.spark.sql.delta.DeltaCDCScalaWithCatalogOwnedBatch1Suite,6.14\norg.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite,6.04\norg.apache.spark.sql.delta.ImplicitStreamingMergeCastingSuite,5.99\nio.delta.tables.DeltaTableSuite,5.93\norg.apache.spark.sql.delta.DeltaWithCatalogOwnedBatch2Suite,5.89\norg.apache.spark.sql.delta.stats.DataSkippingDeltaV1Suite,5.88\norg.apache.spark.sql.delta.DeltaWithCatalogOwnedBatch1Suite,5.8\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionInsertSQLPathBasedCDCOnDVsPredPushOffSuite,5.77\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedArrayStructEvolutionNullnessSQLNameBasedPreserveNullSourceOnPreserveNullSourceUpR36OX5ISuite,5.7\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSuiteBaseMiscSQLPathBasedCDCOnDVsPredPushOnSuite,5.69\norg.apache.spark.sql.delta.GenerateIdentityValuesSuite,5.59\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedCDCOnDVsPredPushOffSuite,5.51\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedArrayStructEvolutionNullnessSQLNameBasedPreserveNullSourceOffPreserveNullSourceU6MQ3SIISuite,5.51\norg.apache.spark.sql.delta.stats.StatsCollectionSuite,5.42\norg.apache.spark.sql.delta.DeltaInsertIntoColumnOrderSuite,5.38\norg.apache.spark.sql.delta.columnmapping.RemoveColumnMappingCDCSuite,5.36\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedCDCOnDVsPredPushOffSuite,5.07\norg.apache.spark.sql.delta.DeltaLiteVacuumSuite,5.05\norg.apache.spark.sql.delta.DeltaProtocolVersionSuite,5.04\norg.apache.spark.sql.delta.CheckpointsWithCatalogOwnedBatch2Suite,4.97\norg.apache.spark.sql.delta.GeneratedColumnSuite,4.95\norg.apache.spark.sql.delta.DeltaSourceSuite,4.87\norg.apache.spark.sql.connect.delta.DeltaConnectPlannerSuite,4.86\norg.apache.spark.sql.delta.DeltaSuite,4.83\norg.apache.spark.sql.delta.deletionvectors.DeletionVectorsWithPredicatePushdownSuite,4.83\norg.apache.spark.sql.delta.DeltaWithCatalogOwnedBatch100Suite,4.8\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedDVsPredPushOffSuite,4.68\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedMapStructEvolutionNullnessSQLNameBasedPreserveNullSourceOnPreserveNullSourceUpdaH76RPFYSuite,4.66\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSuiteBaseMiscSQLPathBasedDVsPredPushOffSuite,4.62\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSuiteBaseMiscSQLPathBasedCDCOnDVsPredPushOffSuite,4.59\norg.apache.spark.sql.delta.CheckpointsSuite,4.55\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedArrayStructEvolutionNullnessSQLNameBasedPreserveNullSourceOnPreserveNullSourceUpFQ7PINASuite,4.46\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedMapStructEvolutionNullnessSQLNameBasedPreserveNullSourceOnPreserveNullSourceUpda5FZ34QYSuite,4.46\norg.apache.spark.sql.delta.test.DeltaV2SourceSuite,4.42\norg.apache.spark.sql.delta.IdentityColumnSyncScalaSuite,4.39\norg.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite,4.33\norg.apache.spark.sql.delta.typewidening.TypeWideningTableFeatureDropSuite,4.32\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedCDCOnSuite,4.32\norg.apache.spark.sql.delta.DeltaSinkImplicitCastWithCoordinatedCommitsBatch100Suite,4.29\norg.apache.spark.sql.delta.DeltaCDCScalaWithDeletionVectorsSuite,4.27\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionNullnessSQLNameBasedPreserveNullSourceOnPreserveNullSourceUpdateStarOnSuite,4.25\norg.apache.spark.sql.delta.CheckpointsWithCatalogOwnedBatch100Suite,4.24\norg.apache.spark.sql.delta.generatedsuites.UpdateBaseMiscSQLPathBasedCDCOnSuite,4.23\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionInsertSQLPathBasedCDCOnSuite,4.21\norg.apache.spark.sql.delta.ChecksumDVMetricsSuite,4.21\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionNullnessSQLNameBasedPreserveNullSourceOffPreserveNullSourceUpdateStarOffSuite,4.15\norg.apache.spark.sql.delta.DeltaSourceNameColumnMappingSuite,4.14\norg.apache.spark.sql.delta.DeltaInsertIntoSQLByPathSuite,4.12\norg.apache.spark.sql.delta.hudi.ConvertToHudiSuite,4.07\norg.apache.spark.sql.delta.generatedsuites.MergeIntoExtendedSyntaxSQLPathBasedDVsPredPushOnSuite,4.03\norg.apache.spark.sql.delta.schema.InvariantEnforcementSuite,3.97\norg.apache.spark.sql.delta.DeltaLogSuite,3.96\norg.apache.spark.sql.delta.IdentityColumnIngestionScalaSuite,3.96\norg.apache.spark.sql.delta.typewidening.TypeWideningAlterTableSuite,3.92\norg.apache.spark.sql.delta.generatedsuites.UpdateBaseMiscSQLPathBasedCDCOnDVSuite,3.92\norg.apache.spark.sql.delta.generatedsuites.RowTrackingMergeCommonNameBasedRowTrackingMergeDVSuite,3.91\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionNullnessSQLNameBasedPreserveNullSourceOffPreserveNullSourceUpdateStarOnSuite,3.88\norg.apache.spark.sql.delta.DeltaProtocolTransitionsSuite,3.85\norg.apache.spark.sql.delta.stats.PartitionLikeDataSkippingSuite,3.8\norg.apache.spark.sql.delta.generatedsuites.MergeIntoSchemaEvolutionBaseNewColumnScalaSuite,3.8\norg.apache.spark.sql.delta.DeltaSourceIdColumnMappingSuite,3.78\norg.apache.spark.sql.delta.generatedsuites.MergeIntoBasicSQLPathBasedCDCOnDVsPredPushOnSuite,3.71\norg.apache.spark.sql.delta.DeltaTimeTravelWithCatalogOwnedBatch1Suite,3.64\norg.apache.spark.sql.delta.ConvertToDeltaScalaSuite,3.63\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedSuite,3.63\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNestedStructEvolutionUpdateOnlyScalaSuite,3.59\norg.apache.spark.sql.delta.InCommitTimestampWithCatalogOwnedBatch2Suite,3.58\norg.apache.spark.sql.delta.generatedsuites.MergeIntoTopLevelStructEvolutionNullnessSQLNameBasedPreserveNullSourceOnPreserveNullSourceUpdateStarOnSuite,3.57\norg.apache.spark.sql.delta.generatedsuites.MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedCDCOnDVsPredPushOnSuite,3.55\norg.apache.spark.sql.delta.DeltaLogWithCatalogOwnedBatch1Suite,3.54\n"
  },
  {
    "path": "project/tests/test_cross_spark_publish.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nCross-Spark Version Build Testing\n\nTests the Delta Lake build system by validating JAR file names for:\n1. Default publish (publishM2) - publishes ALL modules WITH Spark suffix\n2. Backward-compat publish (skipSparkSuffix=true) - publishes WITHOUT suffix\n3. Full cross-version workflow publishes both with and without suffix\n\nUsage:\n    python project/tests/test_cross_spark_publish.py\n\nThe script will:\n1. Test default publishM2 command publishes all modules WITH Spark suffix\n2. Test skipSparkSuffix=true publishes WITHOUT suffix (backward compatibility)\n3. Test full cross-version build workflow (both with and without suffix)\n4. Exit with status 0 on success, 1 on failure\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import List, Set, Dict\n\n\n# Spark-related modules (requiresCrossSparkBuild := true)\n# These modules get a Spark version suffix (e.g., _4.0) for non-default versions\n# Template format: {suffix} = short Spark version suffix (e.g., \"\", \"_4.0\")\n#                  {version} = full Delta version (e.g., \"3.4.0-SNAPSHOT\")\nSPARK_RELATED_JAR_TEMPLATES = [\n    \"delta-spark{suffix}_2.13-{version}.jar\",\n    \"delta-connect-common{suffix}_2.13-{version}.jar\",\n    \"delta-connect-client{suffix}_2.13-{version}.jar\",\n    \"delta-connect-server{suffix}_2.13-{version}.jar\",\n    \"delta-sharing-spark{suffix}_2.13-{version}.jar\",\n]\n\n# Iceberg-related modules - only built for Spark versions with supportIceberg=true\n# delta-iceberg has no Spark suffix (always delta-iceberg_2.13) because it only supports Spark 4.0\nDELTA_ICEBERG_JAR_TEMPLATES = [\n    \"delta-iceberg_2.13-{version}.jar\",\n]\n\n# Hudi-related modules - only built for Spark versions with supportHudi=true\n# delta-hudi has no Spark suffix (always delta-hudi_2.13)\nDELTA_HUDI_JAR_TEMPLATES = [\n    \"delta-hudi_2.13-{version}.jar\",\n]\n\n# Non-spark-related modules (built once, same for all Spark versions)\n# Template format: {version} = Delta version (e.g., \"3.4.0-SNAPSHOT\")\nNON_SPARK_RELATED_JAR_TEMPLATES = [\n    \"delta-storage-{version}.jar\",\n    \"delta-kernel-api-{version}.jar\",\n    \"delta-kernel-defaults-{version}.jar\",\n    \"delta-storage-s3-dynamodb-{version}.jar\",\n    \"delta-kernel-unitycatalog-{version}.jar\",\n    \"delta-contribs_2.13-{version}.jar\",\n]\n\n\n@dataclass\nclass SparkVersionSpec:\n    \"\"\"Configuration for a specific Spark version.\n\n    Mirrors the SparkVersionSpec in CrossSparkVersions.scala.\n    \"\"\"\n    suffix: str  # e.g., \"\" for default, \"_X.Y\" for other versions\n    support_iceberg: bool = False  # Whether this Spark version supports iceberg integration\n    support_hudi: bool = True  # Whether this Spark version supports hudi integration\n\n    def __post_init__(self):\n        \"\"\"Generate JAR templates with the suffix applied.\"\"\"\n        # Generate Spark-related JAR templates with the suffix\n        self.spark_related_jars = [\n            jar.format(suffix=self.suffix, version=\"{version}\")\n            for jar in SPARK_RELATED_JAR_TEMPLATES\n        ]\n\n        # Iceberg JARs have no Spark suffix (always delta-iceberg_2.13)\n        if self.support_iceberg:\n            self.iceberg_jars = list(DELTA_ICEBERG_JAR_TEMPLATES)\n        else:\n            self.iceberg_jars = []\n\n        # Hudi JARs have no Spark suffix (always delta-hudi_2.13)\n        if self.support_hudi:\n            self.hudi_jars = list(DELTA_HUDI_JAR_TEMPLATES)\n        else:\n            self.hudi_jars = []\n\n        # Non-Spark-related JAR templates are the same for all Spark versions\n        self.non_spark_related_jars = list(NON_SPARK_RELATED_JAR_TEMPLATES)\n\n    @property\n    def all_jars(self) -> List[str]:\n        \"\"\"All JAR templates for this Spark version.\"\"\"\n        return self.spark_related_jars + self.non_spark_related_jars + self.iceberg_jars + self.hudi_jars\n\n\n# Spark versions to test (key = full version string, value = spec with suffix)\n# By default, ALL versions get a Spark suffix (e.g., delta-spark_4.0_2.13)\n# skipSparkSuffix=true removes the suffix (used during release for backward compat)\n# These should mirror CrossSparkVersions.scala\nSPARK_VERSIONS: Dict[str, SparkVersionSpec] = {\n    \"4.0.1\": SparkVersionSpec(suffix=\"_4.0\", support_iceberg=True, support_hudi=True),\n    \"4.1.0\": SparkVersionSpec(suffix=\"_4.1\", support_iceberg=False, support_hudi=False)\n}\n\n# The default Spark version\n# This is intentionally hardcoded here to explicitly test the default version.\nDEFAULT_SPARK = \"4.1.0\"\n\n\ndef substitute_xversion(jar_templates: List[str], delta_version: str) -> Set[str]:\n    \"\"\"\n    Substitutes {version} placeholder in JAR templates with actual Delta version.\n    \"\"\"\n    return {jar.format(version=delta_version) for jar in jar_templates}\n\n\nclass CrossSparkPublishTest:\n    \"\"\"Tests cross-Spark version builds.\"\"\"\n\n    def __init__(self, delta_root: Path):\n        self.delta_root = delta_root\n        self.delta_version = self._get_delta_version()\n        self.scala_version = \"2.13\"\n\n    def _get_delta_version(self) -> str:\n        \"\"\"Reads Delta version from version.sbt.\"\"\"\n        with open(self.delta_root / \"version.sbt\", 'r') as f:\n            for line in f:\n                if 'version :=' in line:\n                    return line.split('\"')[1]\n        sys.exit(\"Error: Could not parse version from version.sbt\")\n\n    def clean_maven_cache(self) -> None:\n        \"\"\"Clears Maven local cache for io.delta artifacts.\"\"\"\n        import shutil\n\n        m2_repo = Path.home() / \".m2\" / \"repository\" / \"io\" / \"delta\"\n\n        if m2_repo.exists():\n            print(f\"Cleaning Maven cache: {m2_repo}\")\n            shutil.rmtree(m2_repo)\n            print(\"✓ Maven cache cleaned\\n\")\n        else:\n            print(\"Maven cache already clean\\n\")\n\n    def find_all_jars(self) -> Set[str]:\n        \"\"\"Finds all JAR files from Maven local repository.\"\"\"\n        m2_repo = Path.home() / \".m2\" / \"repository\" / \"io\" / \"delta\"\n\n        if not m2_repo.exists():\n            return set()\n\n        found_jars = set()\n        for version_dir in m2_repo.rglob(self.delta_version):\n            for jar_file in version_dir.glob(\"*.jar\"):\n                # Exclude test/source/javadoc JARs\n                if not any(x in jar_file.name for x in [\"-tests\", \"-sources\", \"-javadoc\"]):\n                    found_jars.add(jar_file.name)\n\n        return found_jars\n\n    def run_sbt_command(self, description: str, command: List[str]) -> bool:\n        \"\"\"Runs an SBT command and returns True if successful.\"\"\"\n        print(f\"  {description}\")\n        try:\n            subprocess.run(command, cwd=self.delta_root, check=True,\n                          stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)\n            return True\n        except subprocess.CalledProcessError:\n            print(f\"  ✗ Command failed: {' '.join(command)}\")\n            return False\n\n    def validate_jars(self, expected: Set[str], test_name: str) -> bool:\n        \"\"\"Validates that found JARs match expected JARs exactly.\"\"\"\n        found = self.find_all_jars()\n\n        print(f\"\\n{test_name} - Found JARs ({len(found)} total):\")\n        for jar in sorted(found):\n            print(f\"  {jar}\")\n\n        print(f\"\\n{test_name} - Expected JARs ({len(expected)} total):\")\n        for jar in sorted(expected):\n            print(f\"  {jar}\")\n\n        missing = expected - found\n        extra = found - expected\n\n        print()\n        if not missing and not extra:\n            print(f\"✓ {test_name} - All expected JARs found\")\n            return True\n\n        if missing:\n            print(f\"✗ {test_name} - Missing JARs ({len(missing)}):\")\n            for jar in sorted(missing):\n                print(f\"  ✗ {jar}\")\n\n        if extra:\n            print(f\"\\n✗ {test_name} - Unexpected JARs ({len(extra)}):\")\n            for jar in sorted(extra):\n                print(f\"  ✗ {jar}\")\n\n        return False\n\n    def test_default_publish(self) -> bool:\n        \"\"\"Default publishM2 should publish ALL modules WITH Spark suffix.\"\"\"\n        spark_spec = SPARK_VERSIONS[DEFAULT_SPARK]\n\n        print(\"\\n\" + \"=\"*70)\n        print(f\"TEST: Default publishM2 (should publish ALL modules WITH suffix for Spark {DEFAULT_SPARK})\")\n        print(\"=\"*70)\n\n        self.clean_maven_cache()\n\n        if not self.run_sbt_command(\n            \"Running: build/sbt publishM2\",\n            [\"build/sbt\", \"publishM2\"]\n        ):\n            return False\n\n        # Default behavior: all Spark-dependent modules have suffix (e.g., delta-spark_4.0_2.13)\n        expected = substitute_xversion(spark_spec.all_jars, self.delta_version)\n        return self.validate_jars(expected, \"Default publishM2 (with suffix)\")\n\n    def test_backward_compat_publish(self) -> bool:\n        \"\"\"skipSparkSuffix=true should publish ALL modules WITHOUT Spark suffix.\"\"\"\n        # Create a spec without suffix for backward compatibility\n        # Uses the same iceberg support as the default Spark version\n        default_spark_spec = SPARK_VERSIONS[DEFAULT_SPARK]\n        spark_spec_no_suffix = SparkVersionSpec(suffix=\"\", support_iceberg=default_spark_spec.support_iceberg, support_hudi=default_spark_spec.support_hudi)\n\n        print(\"\\n\" + \"=\"*70)\n        print(f\"TEST: skipSparkSuffix=true (backward compatibility - no suffix)\")\n        print(\"=\"*70)\n\n        self.clean_maven_cache()\n\n        if not self.run_sbt_command(\n            \"Running: build/sbt -DskipSparkSuffix=true publishM2\",\n            [\"build/sbt\", \"-DskipSparkSuffix=true\", \"publishM2\"]\n        ):\n            return False\n\n        # Expect artifacts WITHOUT suffix (e.g., delta-spark_2.13 instead of delta-spark_4.0_2.13)\n        expected = substitute_xversion(spark_spec_no_suffix.all_jars, self.delta_version)\n        return self.validate_jars(expected, \"skipSparkSuffix=true (backward compat)\")\n\n    def test_cross_spark_workflow(self) -> bool:\n        \"\"\"Full cross-Spark workflow: backward-compat (no suffix) + all versions (with suffix).\"\"\"\n        print(\"\\n\" + \"=\"*70)\n        print(\"TEST: Cross-Spark Workflow (backward-compat + all non-master with suffix)\")\n        print(\"=\"*70)\n\n        self.clean_maven_cache()\n\n        # Step 1: Publish all modules WITHOUT suffix (backward compatibility)\n        if not self.run_sbt_command(\n            \"Step 1: build/sbt -DskipSparkSuffix=true publishM2 (backward compat, no suffix)\",\n            [\"build/sbt\", \"-DskipSparkSuffix=true\", \"publishM2\"]\n        ):\n            return False\n\n        # Step 2: Publish Spark-dependent modules WITH suffix for each non-master version\n        for spark_version, spark_spec in SPARK_VERSIONS.items():\n            # Skip master/snapshot versions\n            if \"SNAPSHOT\" in spark_version:\n                continue\n\n            if not self.run_sbt_command(\n                f\"Step 2: build/sbt -DsparkVersion={spark_version} \\\"runOnlyForReleasableSparkModules publishM2\\\" (with suffix)\",\n                [\"build/sbt\", f\"-DsparkVersion={spark_version}\", \"runOnlyForReleasableSparkModules publishM2\"]\n            ):\n                return False\n\n        # Build expected JARs:\n        # 1. All modules WITHOUT suffix (from Step 1 - backward compat)\n        # 2. Spark-dependent modules WITH suffix for each non-master version (from Step 2)\n        # 3. Iceberg/Hudi JARs for supported versions (no Spark suffix)\n        expected = set()\n\n        # Step 1: All modules without suffix (uses default Spark version's iceberg support)\n        default_spark_spec = SPARK_VERSIONS[DEFAULT_SPARK]\n        no_suffix_spec = SparkVersionSpec(suffix=\"\", support_iceberg=default_spark_spec.support_iceberg, support_hudi=default_spark_spec.support_hudi)\n        expected.update(substitute_xversion(no_suffix_spec.all_jars, self.delta_version))\n\n        # Step 2: Spark-dependent modules WITH suffix for each non-master version\n        for spark_version, spark_spec in SPARK_VERSIONS.items():\n            if \"SNAPSHOT\" in spark_version:\n                continue  # Skip master/snapshot\n\n            expected.update(substitute_xversion(spark_spec.spark_related_jars, self.delta_version))\n            expected.update(substitute_xversion(spark_spec.iceberg_jars, self.delta_version))\n            expected.update(substitute_xversion(spark_spec.hudi_jars, self.delta_version))\n\n        return self.validate_jars(expected, \"Cross-Spark Workflow\")\n\n    def validate_spark_versions(self) -> None:\n        \"\"\"\n        Validates that Spark versions in this test match those in CrossSparkVersions.scala.\n\n        Uses 'build/sbt showSparkVersions' to query versions directly from the build.\n        \"\"\"\n        try:\n            # Query Spark versions from SBT\n            result = subprocess.run(\n                [\"build/sbt\", \"showSparkVersions\"],\n                cwd=self.delta_root,\n                capture_output=True,\n                text=True,\n                check=True\n            )\n\n            # Parse output - each line is a Spark version\n            # Version format: X.Y.Z or X.Y.Z-SNAPSHOT\n            import re\n            version_pattern = re.compile(r'^\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?$')\n\n            build_versions = set()\n            for line in result.stdout.strip().split('\\n'):\n                line = line.strip()\n                if version_pattern.match(line):\n                    build_versions.add(line)\n\n            # Get Python test versions\n            test_versions = set(SPARK_VERSIONS.keys())\n\n            # Compare versions\n            if build_versions != test_versions:\n                missing_in_test = build_versions - test_versions\n                extra_in_test = test_versions - build_versions\n\n                print(\"\\n\" + \"=\"*70)\n                print(\"ERROR: Spark version mismatch between test and build\")\n                print(\"=\"*70)\n\n                if missing_in_test:\n                    print(f\"\\n✗ Build defines these versions, missing in test:\")\n                    for v in sorted(missing_in_test):\n                        print(f\"    {v}\")\n\n                if extra_in_test:\n                    print(f\"\\n✗ Test defines these versions, missing in build:\")\n                    for v in sorted(extra_in_test):\n                        print(f\"    {v}\")\n\n                print(\"\\nPlease update SPARK_VERSIONS in this test to match build configuration.\")\n                print(\"=\"*70 + \"\\n\")\n                sys.exit(1)\n\n            # Success - silent validation\n            print(f\"✓ Spark versions: {', '.join(sorted(build_versions))}\\n\")\n\n        except subprocess.CalledProcessError as e:\n            print(f\"Warning: Could not validate Spark versions: {e}\\n\")\n\n\nclass SparkVersionsScriptTest:\n    \"\"\"Tests for the get_spark_version_info.py script.\"\"\"\n\n    def __init__(self, delta_root: Path):\n        self.delta_root = delta_root\n        self.json_path = delta_root / \"target\" / \"spark-versions.json\"\n        self.script_path = delta_root / \"project\" / \"scripts\" / \"get_spark_version_info.py\"\n\n    def ensure_json_exists(self) -> bool:\n        \"\"\"Ensure the JSON file exists by running exportSparkVersionsJson.\"\"\"\n        if not self.json_path.exists():\n            print(\"  Generating spark-versions.json...\")\n            try:\n                subprocess.run(\n                    [\"build/sbt\", \"exportSparkVersionsJson\"],\n                    cwd=self.delta_root,\n                    check=True,\n                    stdout=subprocess.DEVNULL,\n                    stderr=subprocess.STDOUT\n                )\n            except subprocess.CalledProcessError:\n                print(\"  ✗ Failed to generate spark-versions.json\")\n                return False\n        return True\n\n    def test_json_format(self) -> bool:\n        \"\"\"Test that the JSON file is well-formed with expected fields.\"\"\"\n        if not self.ensure_json_exists():\n            return False\n\n        try:\n            with open(self.json_path, 'r') as f:\n                data = json.load(f)\n\n            # Validate it's an array\n            if not isinstance(data, list) or len(data) == 0:\n                print(\"  ✗ JSON must be a non-empty array\")\n                return False\n\n            # Validate each entry has required fields\n            required_fields = [\"fullVersion\", \"shortVersion\", \"isMaster\", \"isDefault\", \"targetJvm\", \"packageSuffix\"]\n            for idx, entry in enumerate(data):\n                for field in required_fields:\n                    if field not in entry:\n                        print(f\"  ✗ Entry {idx} missing required field: {field}\")\n                        return False\n\n                # Validate field types\n                if not isinstance(entry[\"fullVersion\"], str) or not isinstance(entry[\"shortVersion\"], str) or \\\n                   not isinstance(entry[\"isMaster\"], bool) or not isinstance(entry[\"isDefault\"], bool) or \\\n                   not isinstance(entry[\"targetJvm\"], str) or not isinstance(entry[\"packageSuffix\"], str):\n                    print(f\"  ✗ Entry {idx}: Invalid field types\")\n                    return False\n\n            versions_str = \", \".join([entry.get(\"isMaster\") and \"master\" or entry[\"shortVersion\"] for entry in data])\n            print(f\"  ✓ JSON format valid: {len(data)} version(s) [{versions_str}]\")\n            return True\n\n        except json.JSONDecodeError as e:\n            print(f\"  ✗ Invalid JSON: {e}\")\n            return False\n        except Exception as e:\n            print(f\"  ✗ Unexpected error: {e}\")\n            return False\n\n    def test_all_spark_versions(self) -> bool:\n        \"\"\"Test that --all-spark-versions produces valid JSON array.\"\"\"\n        if not self.ensure_json_exists():\n            return False\n\n        try:\n            result = subprocess.run(\n                [\"python3\", str(self.script_path), \"--all-spark-versions\"],\n                cwd=self.delta_root,\n                capture_output=True,\n                text=True,\n                check=True\n            )\n\n            matrix_versions = json.loads(result.stdout.strip())\n\n            # Validate it's a non-empty array of strings\n            if not isinstance(matrix_versions, list) or len(matrix_versions) == 0:\n                print(\"  ✗ Must output a non-empty JSON array\")\n                return False\n\n            if not all(isinstance(v, str) for v in matrix_versions):\n                print(\"  ✗ All matrix entries must be strings\")\n                return False\n\n            # Validate consistency with JSON\n            with open(self.json_path, 'r') as f:\n                data = json.load(f)\n\n            if len(matrix_versions) != len(data):\n                print(f\"  ✗ Matrix has {len(matrix_versions)} versions, JSON has {len(data)}\")\n                return False\n\n            print(f\"  ✓ --all-spark-versions: {matrix_versions}\")\n            return True\n\n        except (subprocess.CalledProcessError, json.JSONDecodeError) as e:\n            print(f\"  ✗ Failed: {e}\")\n            return False\n\n    def test_released_spark_versions(self) -> bool:\n        \"\"\"Test that --released-spark-versions excludes snapshots.\"\"\"\n        if not self.ensure_json_exists():\n            return False\n\n        try:\n            result = subprocess.run(\n                [\"python3\", str(self.script_path), \"--released-spark-versions\"],\n                cwd=self.delta_root,\n                capture_output=True,\n                text=True,\n                check=True\n            )\n\n            released_versions = json.loads(result.stdout.strip())\n\n            # Validate it's an array of strings\n            if not isinstance(released_versions, list):\n                print(\"  ✗ Must output a JSON array\")\n                return False\n\n            if not all(isinstance(v, str) for v in released_versions):\n                print(\"  ✗ All entries must be strings\")\n                return False\n\n            # Load JSON and verify snapshots are excluded\n            with open(self.json_path, 'r') as f:\n                data = json.load(f)\n\n            expected_count = sum(1 for entry in data if \"-SNAPSHOT\" not in entry[\"fullVersion\"])\n            if len(released_versions) != expected_count:\n                print(f\"  ✗ Expected {expected_count} released versions, got {len(released_versions)}\")\n                return False\n\n            # Verify no snapshot versions included\n            for version in released_versions:\n                if \"SNAPSHOT\" in version.upper():\n                    print(f\"  ✗ Released versions should not include snapshots: {version}\")\n                    return False\n\n            print(f\"  ✓ --released-spark-versions: {released_versions} (snapshots excluded)\")\n            return True\n\n        except (subprocess.CalledProcessError, json.JSONDecodeError) as e:\n            print(f\"  ✗ Failed: {e}\")\n            return False\n\n    def test_get_field(self) -> bool:\n        \"\"\"Test that --get-field works for various version formats.\"\"\"\n        if not self.ensure_json_exists():\n            return False\n\n        try:\n            # Load the JSON to know what versions to test\n            with open(self.json_path, 'r') as f:\n                data = json.load(f)\n\n            test_cases = []\n            for entry in data:\n                # Test short version and full version\n                test_cases.append((entry[\"shortVersion\"], \"targetJvm\", entry[\"targetJvm\"]))\n                test_cases.append((entry[\"fullVersion\"], \"fullVersion\", entry[\"fullVersion\"]))\n                \n                # Test \"master\" if applicable\n                if entry[\"isMaster\"]:\n                    test_cases.append((\"master\", \"targetJvm\", entry[\"targetJvm\"]))\n\n            all_passed = True\n            for version, field, expected in test_cases:\n                result = subprocess.run(\n                    [\"python3\", str(self.script_path), \"--get-field\", version, field],\n                    cwd=self.delta_root,\n                    capture_output=True,\n                    text=True,\n                    check=True\n                )\n\n                actual = json.loads(result.stdout.strip())\n                if actual != expected:\n                    print(f\"  ✗ --get-field {version} {field}: expected {expected}, got {actual}\")\n                    all_passed = False\n\n            if all_passed:\n                print(f\"  ✓ --get-field: Tested {len(test_cases)} cases successfully\")\n            return all_passed\n\n        except (subprocess.CalledProcessError, json.JSONDecodeError) as e:\n            print(f\"  ✗ Failed: {e}\")\n            return False\n\n\ndef main():\n    \"\"\"Main entry point.\"\"\"\n    try:\n        delta_root = Path(__file__).parent.parent.parent\n        if not (delta_root / \"build.sbt\").exists():\n            print(\"Error: build.sbt not found. Run from Delta repository root.\")\n            sys.exit(1)\n\n        print(\"=\"*70)\n        print(\"Cross-Spark Build Test Suite\")\n        print(\"=\"*70)\n        print()\n\n        # Test the get_spark_version_info.py script first\n        print(\"\\n\" + \"=\"*70)\n        print(\"PART 1: Spark Versions Script Tests\")\n        print(\"=\"*70)\n        script_test = SparkVersionsScriptTest(delta_root)\n        script_test1_passed = script_test.test_json_format()\n        script_test2_passed = script_test.test_all_spark_versions()\n        script_test3_passed = script_test.test_released_spark_versions()\n        script_test4_passed = script_test.test_get_field()\n\n        # Test cross-Spark build workflow\n        print(\"\\n\" + \"=\"*70)\n        print(\"PART 2: Cross-Spark Build Tests\")\n        print(\"=\"*70)\n        build_test = CrossSparkPublishTest(delta_root)\n        build_test.validate_spark_versions()\n\n        # Run all build tests\n        build_test1_passed = build_test.test_default_publish()\n        build_test2_passed = build_test.test_backward_compat_publish()\n        build_test3_passed = build_test.test_cross_spark_workflow()\n\n        # Summary\n        print(\"\\n\" + \"=\"*70)\n        print(\"TEST SUMMARY\")\n        print(\"=\"*70)\n        print(\"\\nPart 1: Spark Versions Script Tests\")\n        print(f\"  JSON Format:                            {'✓ PASSED' if script_test1_passed else '✗ FAILED'}\")\n        print(f\"  All Spark Versions Output:              {'✓ PASSED' if script_test2_passed else '✗ FAILED'}\")\n        print(f\"  Released Spark Versions Output:         {'✓ PASSED' if script_test3_passed else '✗ FAILED'}\")\n        print(f\"  Get Field Functionality:                {'✓ PASSED' if script_test4_passed else '✗ FAILED'}\")\n        print(\"\\nPart 2: Cross-Spark Build Tests\")\n        print(f\"  Default publishM2 (with suffix):        {'✓ PASSED' if build_test1_passed else '✗ FAILED'}\")\n        print(f\"  skipSparkSuffix (backward compat):      {'✓ PASSED' if build_test2_passed else '✗ FAILED'}\")\n        print(f\"  Cross-Spark Workflow (both):            {'✓ PASSED' if build_test3_passed else '✗ FAILED'}\")\n        print(\"=\"*70)\n\n        all_tests_passed = (\n            script_test1_passed and script_test2_passed and script_test3_passed and script_test4_passed and\n            build_test1_passed and build_test2_passed and build_test3_passed\n        )\n\n        if all_tests_passed:\n            print(\"\\n✓ ALL TESTS PASSED\")\n            sys.exit(0)\n        else:\n            print(\"\\n✗ SOME TESTS FAILED\")\n            sys.exit(1)\n    except Exception as e:\n        print(f\"\\n✗ TEST EXECUTION FAILED WITH ERROR: {e}\")\n        import traceback\n        traceback.print_exc()\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "protocol_rfcs/README.md",
    "content": "# Protocol RFCs\n\nThis directory contains information about the process of making Delta protocol changes via RFCs and all the RFCs that have been proposed since\n this process was adopted.\n \n - [Table of RFCs](#table-of-rfcs)\n    - [Proposed RFCs](#proposed-rfcs)\n    - [Accepted RFCs](#accepted-rfcs)\n    - [Rejected RFCs](#rejected-rfcs)\n - [RFC Process](#rfc-process)\n\n\n## Table of RFCs\n\nHere is the history of all the RFCs propose/accepted/rejected since Feb 6, 2024, when this process was introduced.\n\n### Proposed RFCs\n\n| Date proposed | RFC file                                                                                                                         | Github issue                                  | RFC title                              |\n|:--------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------|:---------------------------------------|\n| 2023-02-26    | [column-mapping-usage.tracking.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/column-mapping-usage-tracking.md) | https://github.com/delta-io/delta/issues/2682 | Column Mapping Usage Tracking          |\n| 2023-04-24    | [variant-type.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md)                                   | https://github.com/delta-io/delta/issues/2864 | Variant Data Type                      |\n| 2024-04-30    | [collated-string-type.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/collated-string-type.md)                   | https://github.com/delta-io/delta/issues/2894 | Collated String Type                   |\n| 2025-03-13    | [checkpoint-protection.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/checkpoint-protection.md)                 | https://github.com/delta-io/delta/issues/4152 | Checkpoint Protection                  |\n| 2025-03-18    | [iceberg-writer-compat-v1.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/iceberg-writer-compat-v1.md)           | https://github.com/delta-io/delta/issues/4284 | IcebergWriterCompatV1                  |\n| 2025-05-06    | [variant-shredding.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-shredding.md)                         | https://github.com/delta-io/delta/issues/4032 | Variant Shredding                      |\n| 2025-11-20    | [materialize-partition-columns.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/materialize-partition-columns.md)                         | https://github.com/delta-io/delta/issues/5555 | Materialize Partition Columns                      |\n\n### Accepted RFCs\n\n| Date proposed | Date accepted | RFC file | Github issue | RFC title |\n|:-|:-|:-|:-|:-|\n| 2025-04-07    | 2026-02-17    |[catalog-managed.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/accepted/catalog-managed.md)            | https://github.com/delta-io/delta/issues/4381 | Catalog-Managed Tables         |\n| 2023-02-28    | 2023-03-26    |[vacuum-protocol-check.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/accepted/vacuum-protocol-check.md)| https://github.com/delta-io/delta/issues/2630 | Enforce Vacuum Protocol Check  |\n| 2023-02-02    | 2023-07-24    |[in-commit-timestamps.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/accepted/in-commit-timestamps.md)  | https://github.com/delta-io/delta/issues/2532 | In-Commit Timestamps           |\n| 2023-02-09    | 2025-01-28    |[type-widening.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/accepted/type-widening.md)                | https://github.com/delta-io/delta/issues/2623 | Type Widening                  |\n\n### Rejected RFCs\n\n| Date proposed | Date rejected | RFC file                                                                                                      | Github issue                                  | RFC title        |\n|:--------------|:--------------|:--------------------------------------------------------------------------------------------------------------|:----------------------------------------------|:-----------------|\n| 2023-02-14    | 2025-04-07    | [managed-commits.md](https://github.com/delta-io/delta/blob/master/protocol_rfcs/rejected/managed-commits.md) | https://github.com/delta-io/delta/issues/2598 | Managed Commits  |\n\n\n## RFC process\n\n###  **1. Make initial proposal** \nCreate a Github issue of type [Protocol Change Request].\n- The description of the issue may have links to design docs, etc.\n- This issue will serve as the central location for all discussions related to the protocol change.\n- If the proposal comes with a prototype or other pathfinding, the changes should be in an open PR. \n\n### **2. Add the RFC doc** \nAfter creating the issue and discussing with the community, if a basic consensus is reached that this feature should be implemented, then create a PR to add the protocol RFC before merging code in master.\n- Clone the RFC template `template.md` and create a new RFC markdown doc.\n- Cross-link with the issue with \"see #xxx\". DONT USE \"closes #xxx\" or \"fixes #xxx\" or \"resolves #xxx\" because we don't want the issue to be closed when this RFC PR is merged.\n\nNote:\n- For table features, it is strongly recommended that any experimental support for the feature uses a temporary feature name with a suffix like `-dev`. This will communicate to the users that are about to use experimental feature with no future compatibility guarantee.\n- Code related to a proposed feature should not be merged into the main branch until the RFC attains \"proposed\" status (that is, the RFC PR has been through public review and merged). Until the RFC has been accepted (that is, the proposed changes have been merged into the Delta specification), any code changes should be isolated from production code behind feature flags, etc. so that existing users are not affected in any way.\n\n###  **3. Finally, accept or reject the RFC** \nFor a RFC to be accepted, it must satisfy the following criteria:\n- There is a production implementation (for example, in delta-spark) of the feature that has been thoroughly well tested.\n- There is at least some discussion and/or prototype (preferred) that ensure the feasibility of the feature in Delta Kernel. \n\nWhen the success criteria are met, then the protocol can be finalized by making a PR to make the following changes:\n-  Closely validate that the protocol spec changes are actually consistent with the production implementation.\n-  Cross-link the PR with the original issue with \"closes #xxx\" as now we are ready to close the issue. In addition, update the title of the issue to say `[ACCEPTED]` to make it obvious how the proposal was resolved.\n-  Update `protocol.md`.\n-  Move the RFC doc to the `accepted` subdirectory, and update the state in index.md.\n-  Remove the temporary/preview suffix like `-dev` in the table feature name from all the code. \n\nHowever, if the RFC is to be rejected, then make a PR to do the following changes:\n- Cross-link the PR with the original issue with \"closes #xxx\" as now we are ready to close the issue. In addition, update the title of the issue to say `[REJECTED]` to make it obvious how the proposal was resolved.\n - Move the RFC doc to the `rejected` subdirectory.\n - Update the state in `index.md`.\n - Remove any experimental/preview code related to the feature.\n"
  },
  {
    "path": "protocol_rfcs/accepted/catalog-managed.md",
    "content": "# Catalog-Managed Tables\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/4381**\n\nThis RFC proposes a new reader-writer table feature `catalogManaged` which changes the way Delta Lake\ndiscovers and accesses tables.\n\nToday’s Delta protocol relies entirely on the filesystem for read-time discovery as well as\nwrite-time commit atomicity. This feature request is to allow catalog-managed Delta tables whose\ndiscovery and commits go through the table's managing catalog instead of going directly to the\nfilesystem (s3, abfs, etc). In particular, the catalog becomes the source of truth about whether a\ngiven commit attempt succeeded or not, instead of relying exclusively on filesystem PUT-if-absent\nprimitives.\n\nMaking the catalog the source of truth for commits to a table brings several important advantages:\n\n1. Allows the catalog to broker all commits to the tables it manages, and to reject filesystem-based\n   commits that would bypass the catalog. Otherwise, the catalog cannot reliably stay in sync with\n   the table state, nor can it reject invalid commits, because it doesn’t even know about writes\n   until they are already durable and visible to readers. For instance, a catalog might want to block\n   low-privilege writers from modifying table metadata (e.g. schema, table features, or table\n   properties) while still allowing normal reads and writes. Similarly, if a column is referenced by\n   a foreign key, the catalog might want to prevent dropping its NOT NULL constraint.\n\n2. Opens a clear path to transactions that could span multiple tables and/or involve non-table\n   catalog updates. Otherwise, the catalog cannot participate in commit at all, because\n   filesystem-based commits (i.e. using PUT-if-absent) do not admit any way to coordinate with other\n   entities.\n\n3. Allows the catalog to facilitate efficient writes of the table, e.g. by directly hosting the\n   content of small commits instead of forcing clients to write them to cloud storage first. Otherwise,\n   the catalog is not a source of truth, and at best it can only mirror stale copies of table state.\n\n4. Allows the catalog to facilitate efficient reads of the table. Examples include vending storage\n   credentials, as well as serving up the content of small commits and/or table state such as [version\n   checksum file](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#version-checksum-file), so\n   that clients do not have to read those files from cloud storage.\n\n5. Allows the catalog to be the authoritative source of the latest table version, no longer\n   requiring Delta clients to LIST the `_delta_log` to discover it. This saves time and can also\n   allow implementations of Delta on file systems where LIST is not ordered, such as S3 Express One\n   Zone.\n\n6. Allows the catalog to trigger followup actions based on a commit, such as VACUUMing, data layout\n   optimizations, automatic UniForm conversions, or triggering arbitrary listeners such as downstream\n   ETL or streaming pipelines.\n\n--------\n\n# Changes to existing sections\n\n### Delta Log Entries\n\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#delta-log-entries)***\n\n<ins>Delta Log Entries, also known as Delta files</ins>, are JSON files stored in the `_delta_log`\ndirectory at the root of the table. Together with checkpoints, they make up the log of all changes\nthat have occurred to a table. Delta files are the unit of atomicity for a table, and are named\nusing the next available version number, zero-padded to 20 digits.\n\nFor example:\n\n```\n./_delta_log/00000000000000000000.json\n```\n\n<ins>Delta files use newline-delimited JSON format, where every action is stored as a single-line\nJSON document. A Delta file, corresponding to version `v`, contains an atomic set of\n[_actions_](#Actions) that should be applied to the previous table state corresponding to version\n`v-1`, in order to construct the `v`th snapshot of the table. An action changes one aspect of the\ntable's state, for example, adding or removing a file.</ins>\n\n<ins>**Note:** If the [`catalogManaged` table feature](#catalog-managed-tables) is enabled on the table,\nrecently [ratified commits](#ratified-commit) may not yet be published to the `_delta_log` directory as normal Delta\nfiles - they may be stored directly by the catalog or reside in the `_delta_log/_staged_commits`\ndirectory. Delta clients must contact the table's managing catalog in order to find the information\nabout these [ratified, potentially-unpublished commits](#publishing-commits).</ins>\n\n<ins>The `_delta_log/_staged_commits` directory is the staging area for [staged](#staged-commit)\ncommits. Delta files in this directory have a UUID embedded into them and follow the pattern\n`<version>.<uuid>.json`, where the version corresponds to the proposed commit version, zero-padded\nto 20 digits.</ins>\n\n<ins>For example:</ins>\n\n```\n./_delta_log/_staged_commits/00000000000000000000.3a0d65cd-4056-49b8-937b-95f9e3ee90e5.json\n./_delta_log/_staged_commits/00000000000000000001.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\n./_delta_log/_staged_commits/00000000000000000001.016ae953-37a9-438e-8683-9a9a4a79a395.json\n./_delta_log/_staged_commits/00000000000000000002.3ae45b72-24e1-865a-a211-34987ae02f2a.json\n```\n\n<ins>NOTE: The (proposed) version number of a staged commit is authoritative - file\n`00000000000000000100.<uuid>.json` always corresponds to a commit attempt for version 100. Besides\nsimplifying implementations, it also acknowledges the fact that commit files cannot safely be reused\nfor multiple commit attempts. For example, resolving conflicts in a table with [row\ntracking](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#row-tracking) enabled requires\nrewriting all file actions to update their `baseRowId` field.</ins>\n\n<ins>The [catalog](#terminology-catalogs) is the source of truth about which staged commit files in\nthe `_delta_log/_staged_commits` directory correspond to ratified versions, and Delta clients should\nnot attempt to directly interpret the contents of that directory. Refer to\n[catalog-managed tables](#catalog-managed-tables) for more details.</ins>\n\n~~Delta files use new-line delimited JSON format, where every action is stored as a single line JSON\ndocument. A delta file, `n.json`, contains an atomic set of [_actions_](#actions) that should be\napplied to the previous table state, `n-1.json`, in order to construct the `n`th snapshot of the\ntable. An action changes one aspect of the table's state, for example, adding or removing a file.~~ \n\n### Commit Provenance Information\n\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#commit-provenance-information)***\n\n<ins>When the `catalogManaged` table feature is enabled, the `commitInfo` action must have a field\n`txnId` that stores a unique transaction identifier string.</ins>\n\n### Metadata Cleanup\n\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#metadata-cleanup)***\n\n2. Identify the newest checkpoint that is not newer than the `cutOffCommit`. A checkpoint at the\n   `cutOffCommit` is ideal, but an older one will do. Let's call it `cutOffCheckpoint`. We need to\n   preserve the `cutOffCheckpoint` and all <ins>published</ins> commits after it, because we need\n   them to enable time travel for commits between `cutOffCheckpoint` and the next available\n   checkpoint.\n    - <ins>If no `cutOffCheckpoint` can be found, do not proceed with metadata cleanup as there is\n      nothing to cleanup.</ins>\n3. Delete all [delta log entries](#delta-log-entries), [checkpoint files](#checkpoints), <ins>and\n   [version checksum files](#version-checksum-file)</ins> before the `cutOffCheckpoint` checkpoint. Also delete all the [log compaction files](#log-compaction-files)\n   having startVersion <= `cutOffCheckpoint`'s version.\n    - <ins>Also delete all the [staged commit files](#staged-commit) having version <=\n      `cutOffCheckpoint`'s version from the `_delta_log/_staged_commits` directory.</ins>\n\n--------\n\n> ***The next set of sections will be added to the existing spec just before [Iceberg Compatibility V1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1) section***\n\n# Catalog-managed tables\n\nWith this feature enabled, the [catalog](#terminology-catalogs) that manages the table becomes the\nsource of truth for whether a given commit attempt succeeded.\n\nThe table feature defines the parts of the [commit protocol](#commit-protocol) that directly impact\nthe Delta table (e.g. atomicity requirements, publishing, etc). The Delta client and catalog\ntogether are responsible for implementing the Delta-specific aspects of commit as defined by this\nspec, but are otherwise free to define their own APIs and protocols for communication with each\nother.\n\n**NOTE**: Filesystem-based access to catalog-managed tables is not supported. Delta clients are\nexpected to discover and access catalog-managed tables through the managing catalog, not by direct\nlisting in the filesystem. This feature is primarily designed to warn filesystem-based readers that\nmight attempt to access a catalog-managed table's storage location without going through the catalog\nfirst, and to block filesystem-based writers who could otherwise corrupt both the table and the\ncatalog by failing to commit through the catalog.\n\nBefore we can go into details of this protocol feature, we must first align our terminology.\n\n## Terminology: Commits\n\nA commit is a set of [actions](#actions) that transform a Delta table from version `v - 1` to `v`.\nIt contains the same kind of content as is stored in a [Delta file](#delta-log-entries).\n\nA commit may be stored in the file system as a Delta file - either _published_ or _staged_ - or\nstored _inline_ in the managing catalog, using whatever format the catalog prefers.\n\nThere are several types of commits:\n\n1. **Proposed commit**:  A commit that a Delta client has proposed for the next version of the\n   table. It could be _staged_ or _inline_. It will either become _ratified_ or be rejected.\n\n2. <a name=\"staged-commit\">**Staged commit**</a>: A commit that is written to disk at\n   `_delta_log/_staged_commits/<v>.<uuid>.json`. It has the same content and format as a published\n   Delta file.\n    - Here, the `uuid` is a random UUID that is generated for each commit and `v` is the version\n      which is proposed to be committed, zero-padded to 20 digits.\n    - The mere existence of a staged commit does not mean that the file has been ratified or even\n      proposed. It might correspond to a failed or in-progress commit attempt.\n    - The catalog is the source of truth around which staged commits are ratified.\n    - The catalog stores only the location, not the content, of a staged (and ratified) commit.\n\n3. <a name=\"inline-commit\">**Inline commit**</a>: A proposed commit that is not written to disk but\n   rather has its content sent to the catalog for the catalog to store directly.\n\n4. <a name=\"ratified-commit\">**Ratified commit**</a>: A proposed commit that a catalog has\n   determined has won the commit at the desired version of the table.\n    - The catalog must store ratified commits (that is, the staged commit's location or the inline\n      commit's content) until they are published to the `_delta_log` directory.\n    - A ratified commit may or may not yet be published.\n    - A ratified commit may or may not even be stored by the catalog at all - the catalog may\n      have just atomically published it to the filesystem directly, relying on PUT-if-absent\n      primitives to facilitate the ratification and publication all in one step.\n\n5. <a name=\"published-commit\">**Published commit**</a>: A ratified commit that has been copied into\n   the `_delta_log` as a normal Delta file, i.e. `_delta_log/<v>.json`.\n    - Here, the `v` is the version which is being committed, zero-padded to 20 digits.\n    - The existence of a `<v>.json` file proves that the corresponding version `v` is ratified,\n      regardless of whether the table is catalog-managed or filesystem-based. The catalog is allowed\n      to return information about published commits, but Delta clients can also use filesystem\n      listing operations to directly discover them.\n    - Published commits do not need to be stored by the catalog.\n\n## Terminology: Delta Client\n\nThis is the component that implements support for reading and writing Delta tables, and implements\nthe logic required by the `catalogManaged` table feature. Among other things, it\n- triggers the filesystem listing, if needed, to discover published commits\n- generates the commit content (the set of [actions](#actions))\n- works together with the query engine to trigger the commit process and invoke the client-side\n  catalog component with the commit content\n\nThe Delta client is also responsible for defining the client-side API that catalogs should target.\nThat is, there must be _some_ API that the [catalog client](#catalog-client) can use to communicate\nto the Delta client the subset of catalog-managed information that the Delta client cares about.\nThis protocol feature is concerned with what information Delta cares about, but leaves to Delta\nclients the design of the API they use to obtain that information from catalog clients.\n\n## Terminology: Catalogs\n\n1. **Catalog**: A catalog is an entity which manages a Delta table, including its creation, writes,\n   reads, and eventual deletion.\n    - It could be backed by a database, a filesystem, or any other persistence mechanism.\n    - Each catalog has its own spec around how catalog clients should interact with them, and how\n      they perform a commit.\n\n2. <a name=\"catalog-client\">**Catalog Client**</a>: The catalog always has a client-side component\n   which the Delta client interacts with directly. This client-side component has two primary\n   responsibilities:\n    - implement any client-side catalog-specific logic (such as staging or\n      [publishing](#publishing-commits) commits)\n    - communicate with the Catalog Server, if any\n\n3. **Catalog Server**: The catalog may also involve a server-side component which the client-side\n   component would be responsible to communicate with.\n    - This server is responsible for coordinating commits and potentially persisting table metadata\n      and enforcing authorization policies.\n    - Not all catalogs require a server; some may be entirely client-side, e.g. filesystem-backed\n      catalogs, or they may make use of a generic database server and implement all of the catalog's\n      business logic client-side.\n\n**NOTE**: This specification outlines the responsibilities and actions that catalogs must implement.\nThis spec does its best not to assume any specific catalog _implementation_, though it does call out\nlikely client-side and server-side responsibilities. Nonetheless, what a given catalog does\nclient-side or server-side is up to each catalog implementation to decide for itself.\n\n## Catalog Responsibilities\n\nWhen the `catalogManaged` table feature is enabled, a catalog performs commits to the table on behalf\nof the Delta client.\n\nAs stated above, the Delta spec does not mandate any particular client-server design or API for\ncatalogs that manage Delta tables. However, the catalog does need to provide certain capabilities\nfor reading and writing Delta tables:\n\n- Atomically commit a version `v` with a given set of `actions`. This is explained in detail in the\n  [commit protocol](#commit-protocol) section.\n- Retrieve information about recent ratified commits and the latest ratified version on the table.\n  This is explained in detail in the [Getting Ratified Commits from the Catalog](#getting-ratified-commits-from-the-catalog) section.\n- Though not required, it is encouraged that catalogs also return the latest table-level metadata,\n  such as the latest Protocol and Metadata actions, for the table. This can provide significant\n  performance advantages to conforming Delta clients, who may forgo log replay and instead trust\n  the information provided by the catalog during query planning.\n\n## Reading Catalog-managed Tables\n\nA catalog-managed table can have a mix of (a) published and (b) ratified but non-published commits.\nThe catalog is the source of truth for ratified commits. Also recall that ratified commits can be\n[staged commits](#staged-commit) that are persisted to the `_delta_log/_staged_commits` directory,\nor [inline commits](#inline-commit) whose content the catalog stores directly.\n\nFor example, suppose the `_delta_log` directory contains the following files:\n\n```\n00000000000000000000.json\n00000000000000000001.json\n00000000000000000002.checkpoint.parquet\n00000000000000000002.json\n00000000000000000003.00000000000000000005.compacted.json\n00000000000000000003.json\n00000000000000000004.json\n00000000000000000005.json\n00000000000000000006.json\n00000000000000000007.json\n_staged_commits/00000000000000000007.016ae953-37a9-438e-8683-9a9a4a79a395.json // ratified and published\n_staged_commits/00000000000000000008.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json // ratified\n_staged_commits/00000000000000000008.b91807ba-fe18-488c-a15e-c4807dbd2174.json // rejected\n_staged_commits/00000000000000000010.0f707846-cd18-4e01-b40e-84ee0ae987b0.json // not yet ratified\n_staged_commits/00000000000000000010.7a980438-cb67-4b89-82d2-86f73239b6d6.json // partial file\n```\n\nFurther, suppose the catalog stores the following ratified commits:\n```\n{\n  7  -> \"00000000000000000007.016ae953-37a9-438e-8683-9a9a4a79a395.json\",\n  8  -> \"00000000000000000008.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\",\n  9  -> <inline commit: content stored by the catalog directly>\n}\n```\n\nSome things to note are:\n- the catalog isn't aware that commit 7 was already published - perhaps the response from the\n  filesystem was dropped\n- commit 9 is an inline commit\n- neither of the two staged commits for version 10 have been ratified\n\nTo read such tables, Delta clients must first contact the catalog to get the ratified commits. This\ninforms the Delta client of commits [7, 9] as well as the latest ratified version, 9.\n\nIf this information is insufficient to construct a complete snapshot of the table, Delta clients\nmust LIST the `_delta_log` directory to get information about the published commits. For commits\nthat are both returned by the catalog and already published, Delta clients must treat the catalog's\nversion as authoritative and read the commit returned by the catalog. Additionally, Delta clients\nmust ignore any files with versions greater than the latest ratified commit version returned by the\ncatalog.\n\nCombining these two sets of files and commits enables Delta clients to generate a snapshot at the\nlatest version of the table.\n\n**NOTE**: This spec prescribes the _minimum_ required interactions between Delta clients and\ncatalogs for commits. Catalogs may very well expose APIs and work with Delta clients to be\ninformed of other non-commit [file types](#file-types), such as checkpoint, log\ncompaction, and version checksum files. This would allow catalogs to return additional\ninformation to Delta clients during query and scan planning, potentially allowing Delta\nclients to avoid LISTing the filesystem altogether.\n\n## Commit Protocol\n\nTo start, Delta Clients send the desired actions to be committed to the client-side component of the\ncatalog.\n\nThis component then has several options for proposing, ratifying, and publishing the commit,\ndetailed below.\n\n- Option 1: Write the actions (likely client-side) to a [staged commit file](#staged-commit) in the\n  `_delta_log/_staged_commits` directory and then ratify the staged commit (likely server-side) by\n  atomically recording (in persistent storage of some kind) that the file corresponds to version `v`.\n- Option 2: Treat this as an [inline commit](#inline-commit) (i.e. likely that the client-side\n  component sends the contents to the server-side component) and atomically record (in persistent\n  storage of some kind) the content of the commit as version `v` of the table.\n- Option 3: Catalog implementations that use PUT-if-absent (client- or server-side) can ratify and\n  publish all-in-one by atomically writing a [published commit file](#published-commit)\n  in the `_delta_log` directory. Note that this commit will be considered to have succeeded as soon\n  as the file becomes visible in the filesystem, regardless of when or whether the catalog is made\n  aware of the successful publish. The catalog does not need to store these files.\n\nA catalog must not ratify version `v` until it has ratified version `v - 1`, and it must ratify\nversion `v` at most once.\n\nThe catalog must store both flavors of ratified commits (staged or inline) and make them available\nto readers until they are [published](#publishing-commits).\n\nFor performance reasons, Delta clients are encouraged to establish an API contract where the catalog\nprovides the latest ratified commit information whenever a commit fails due to version conflict.\n\n## Getting Ratified Commits from the Catalog\n\nEven after a commit is ratified, it is not discoverable through filesystem operations until it is\n[published](#publishing-commits).\n\nThe catalog-client is responsible to implement an API (defined by the Delta client) that Delta clients can\nuse to retrieve the latest ratified commit version (authoritative), as well as the set of ratified\ncommits the catalog is still storing for the table. If some commits needed to complete the snapshot\nare not stored by the catalog, as they are already published, Delta clients can issue a filesystem\nLIST operation to retrieve them.\n\nDelta clients must establish an API contract where the catalog provides ratified commit information\nas part of the standard table resolution process performed at query planning time.\n\n## Publishing Commits\n\nPublishing is the process of copying the ratified commit with version `<v>` to\n`_delta_log/<v>.json`. The ratified commit may be a staged commit located in\n`_delta_log/_staged_commits/<v>.<uuid>.json`, or it may be an inline commit whose content the\ncatalog stores itself. Because the content of a ratified commit is immutable, it does not matter\nwhether the client-side, server-side, or both catalog components initiate publishing.\n\nImplementations are strongly encouraged to publish commits promptly. This reduces the number of\ncommits the catalog needs to store internally (and serve up to readers).\n\nCommits must be published _in order_. That is, version `v - 1` must be published _before_ version\n`v`.\n\n**NOTE**: Because commit publishing can happen at any time after the commit succeeds, the file\nmodification timestamp of the published file will not accurately reflect the original commit time.\nFor this reason, catalog-managed tables must use [in-commit-timestamps](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#in-commit-timestamps)\nto ensure stability of time travel reads. Refer to [Writer Requirements for Catalog-managed Tables](#writer-requirements-for-catalog-managed-tables)\nsection for more details.\n\n## Maintenance Operations on Catalog-managed Tables\n\n[Checkpoints](#checkpoints-1) and [Log Compaction Files](#log-compaction-files) can only be created\nfor versions that are already published in the `_delta_log`. In other words, in order to checkpoint\nversion `v` or produce a log compaction file for commit range `x <= v <= y`, `_delta_log/<v>.json`\nmust exist.\n\nNotably, the [Version Checksum File](#version-checksum-file) for version `v` _can_ be created in the\n`_delta_log` even if the commit for version `v` is not published.\n\nBy default, maintenance operations are prohibited unless the managing catalog explicitly permits\nthe client to run them. The only exceptions are checkpoints, log compaction, and version checksum,\nas they are essential for all basic table operations (e.g. reads and writes) to operate reliably.\nAll other maintenance operations such as the following are not allowed by default.\n- [Log and other metadata files clean up](#metadata-cleanup).\n- Data files cleanup, for example VACUUM.\n- Data layout changes, for example OPTIMIZE and REORG.\n\n## Creating and Dropping Catalog-managed Tables\n\nThe catalog and query engine ultimately dictate how to create and drop catalog-managed tables.\n\nAs one example, table creation often works in three phases:\n\n1. An initial catalog operation to obtain a unique storage location which serves as an unnamed\n   \"staging\" table\n2. A table operation that physically initializes a new `catalogManaged`-enabled table at the staging\n   location.\n3. A final catalog operation that registers the new table with its intended name.\n\nDelta clients would primarily be involved with the second step, but an implementation could choose\nto combine the second and third steps so that a single catalog call registers the table as part of\nthe table's first commit.\n\nAs another example, dropping a table can be as simple as removing its name from the catalog (a \"soft\ndelete\"), followed at some later point by a \"hard delete\" that physically purges the data. The Delta\nclient would not be involved at all in this process, because no commits are made to the table.\n\n## Catalog-managed Table Enablement\n\nThe `catalogManaged` table feature is supported and active when:\n- The table is on Reader Version 3 and Writer Version 7.\n- The table has a `protocol` action with `readerFeatures` and `writerFeatures` both containing the\n  feature `catalogManaged`.\n\n## Writer Requirements for Catalog-managed tables\n\nWhen supported and active:\n\n- Writers must discover and access the table using catalog calls, which happens _before_ the table's\n  protocol is known. See [Table Discovery](#table-discovery) for more details.\n- The [in-commit-timestamps](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#in-commit-timestamps)\n  table feature must be supported and active.\n- The `commitInfo` action must also contain a field `txnId` that stores a unique transaction\n  identifier string\n- Writers must follow the catalog's [commit protocol](#commit-protocol) and must not perform\n  ordinary filesystem-based commits against the table.\n- Writers must follow the catalog's [maintenance operation protocol](#maintenance-operations-on-catalog-managed-tables)\n\n## Reader Requirements for Catalog-managed tables\n\nWhen supported and active:\n\n- Readers must discover the table using catalog calls, which happens before the table's protocol\n  is known. See [Table Discovery](#table-discovery) for more details.\n- Readers must contact the catalog for information about unpublished ratified commits.\n- Readers must follow the rules described in the [Reading Catalog-managed Tables](#reading-catalog-managed-tables)\n  section above. Notably\n  - If the catalog said `v` is the latest version, clients must ignore any later versions that may\n    have been published\n  - When the catalog returns a ratified commit for version `v`, readers must use that\n    catalog-supplied commit and ignore any published Delta file for version `v` that might also be\n    present.\n\n## Table Discovery\n\nThe requirements above state that readers and writers must discover and access the table using\ncatalog calls, which occurs _before_ the table's protocol is known. This raises an important\nquestion: how can a client discover a `catalogManaged` Delta table without first knowing that it\n_is_, in fact, `catalogManaged` (according to the protocol)?\n\nTo solve this, first note that, in practice, catalog-integrated engines already ask the catalog to\nresolve a table name to its storage location during the name resolution step. This protocol\ntherefore encourages that the same name resolution step also indicate whether the table is\ncatalog-managed. Surfacing this at the very moment the catalog returns the path imposes no extra\nround-trips, yet it lets the client decide — early and unambiguously — whether to follow the\n`catalogManaged` read and write rules.\n\n## Sample Catalog Client API\n\nThe following is an example of a possible API which a Java-based Delta client might require catalog\nimplementations to target:\n\n```scala\n\ninterface CatalogManagedTable {\n    /**\n     * Commits the given set of `actions` to the given commit `version`.\n     *\n     * @param version The version we want to commit.\n     * @param actions Actions that need to be committed.\n     *\n     * @return CommitResponse which has details around the new committed delta file.\n     */\n    def commit(\n        version: Long,\n        actions: Iterator[String]): CommitResponse\n\n    /**\n     * Retrieves a (possibly empty) suffix of ratified commits in the range [startVersion,\n     * endVersion] for this table.\n     * \n     * Some of these ratified commits may already have been published. Some of them may be staged,\n     * in which case the staged commit file path is returned; others may be inline, in which case\n     * the inline commit content is returned.\n     * \n     * The returned commits are sorted in ascending version number and are contiguous.\n     *\n     * If neither start nor end version is specified, the catalog will return all available ratified\n     * commits (possibly empty, if all commits have been published).\n     *\n     * In all cases, the response also includes the table's latest ratified commit version.\n     *\n     * @return GetCommitsResponse which contains an ordered list of ratified commits\n     *         stored by the catalog, as well as table's latest commit version.\n     */\n    def getRatifiedCommits(\n        startVersion: Option[Long],\n        endVersion: Option[Long]): GetCommitsResponse\n}\n```\n\nNote that the above is only one example of a possible Catalog Client API. It is also _NOT_ a catalog\nAPI (no table discovery, ACL, create/drop, etc). The Delta protocol is agnostic to API details, and\nthe API surface Delta clients define should only cover the specific catalog capabilities that Delta\nclient needs to correctly read and write catalog-managed tables.\n"
  },
  {
    "path": "protocol_rfcs/accepted/in-commit-timestamps.md",
    "content": "# In-Commit Timestamps\n\nThis RFC proposes a new Writer table feature called In-Commit Timestamps. When enabled, commit metadata includes a monotonically increasing timestamp that allows for reliable TIMESTAMP AS OF time travel even if filesystem operations change a commit file's modification timestamp.\n\n**For further discussions about this protocol change, please refer to the Github issue - https://github.com/delta-io/delta/issues/2532**\n\n--------\n\n\n### Commit Provenance Information\n> ***Change to existing section***\n\nA delta file can optionally contain additional provenance information about what higher-level operation was being performed as well as who executed it.\n\nImplementations are free to store any valid JSON [object literal](https://www.w3schools.com/js/js_json_objects.asp) as the `commitInfo` action <ins>unless some table feature (e.g. [In-Commit Timestamps](#in-commit-timestamps)) imposes additional requirements on the data</ins>.\n\n<ins>When In-Commit Timestamp are enabled, writers are required to include a commitInfo action with every commit, which must include the `inCommitTimestamp` field.</ins>\n\n#### Reader Requirements for AddCDCFile\n> ***Change to existing section***\n\n...\n3. Change data readers should return the following extra columns:\n\nField Name | Data Type | Description\n-|-|-\n_commit_version|`Long`| The table version containing the change. This can be derived from the name of the Delta log file that contains actions.\n_commit_timestamp|`Timestamp`| The timestamp associated when the commit was created. ~~This can be derived from the file modification time of the Delta log file that contains actions.~~ <ins>Depending on whether [In-Commit Timestamps](#in-commit-timestamps) are enabled, this is derived from either the `inCommitTimestamp` field of the `commitInfo` action of the version's Delta log, or from the Delta log's file modification time.</ins>\n\n# In-Commit Timestamps\n> ***New Section after the [Clustered Table](#clustered-table) section***\n\nThe In-Commit Timestamps writer feature strongly associates a monotonically increasing timestamp with each commit by storing it in the commit's metadata.\n\nEnablement:\n- The table must be on Writer Version 7.\n- The feature `inCommitTimestamps` must exist in the table `protocol`'s `writerFeatures`.\n- The table property `delta.enableInCommitTimestamps` must be set to `true`.\n\n## Writer Requirements for In-Commit Timestamps\n\nWhen In-Commit Timestamps is enabled, then:\n1. Writers must write the `commitInfo` (see [Commit Provenance Information](#commit-provenance-information)) action in the commit.\n2. The `commitInfo` action must be the first action in the commit.\n3. The `commitInfo` action must include a field named `inCommitTimestamp`, of type `long` (see [Primitive Types](#primitive-types)), which represents the time (in milliseconds since the Unix epoch) when the commit is considered to have succeeded. It is the larger of two values:\n   - The time, in milliseconds since the Unix epoch, at which the writer attempted the commit\n   - One millisecond later than the previous commit's `inCommitTimestamp`\n4. If the table has commits from a period when this feature was not enabled, provenance information around when this feature was enabled must be tracked in table properties:\n   - The property `delta.inCommitTimestampEnablementVersion` must be used to track the version of the table when this feature was enabled.\n   - The property `delta.inCommitTimestampEnablementTimestamp` must be the same as the `inCommitTimestamp` of the commit when this feature was enabled.\n5. The `inCommitTimestamp` of the commit that enables this feature must be greater than the file modification time of the immediately preceding commit.\n\n## Recommendations for Readers of Tables with In-Commit Timestamps\n\nFor tables with In-Commit timestamps enabled, readers should use the `inCommitTimestamp` as the commit timestamp for operations like time travel and [`DESCRIBE HISTORY`](https://docs.delta.io/latest/delta-utility.html#retrieve-delta-table-history).\nIf a table has commits from a period before In-Commit timestamps were enabled, the table properties `delta.inCommitTimestampEnablementVersion` and `delta.inCommitTimestampEnablementTimestamp` would be set and can be used to identify commits that don't have `inCommitTimestamp`.\nTo correctly determine the commit timestamp for these tables, readers can use the following rules:\n1. For commits with version >= `delta.inCommitTimestampEnablementVersion`, readers should use the `inCommitTimestamp` field of the `commitInfo` action.\n2. For commits with version < `delta.inCommitTimestampEnablementVersion`, readers should use the file modification timestamp.\n\nFurthermore, when attempting timestamp-based time travel where table state must be fetched as of `timestamp X`, readers should use the following rules:\n1. If `timestamp X` >= `delta.inCommitTimestampEnablementTimestamp`, only table versions >= `delta.inCommitTimestampEnablementVersion` should be considered for the query.\n2. Otherwise, only table versions less than `delta.inCommitTimestampEnablementVersion` should be considered for the query.\n"
  },
  {
    "path": "protocol_rfcs/accepted/type-widening.md",
    "content": "# Type Widening\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/2623**\n\nThis protocol change introduces the Type Widening feature, which enables changing the type of a column or field in an existing Delta table to a wider type.\n\n--------\n\n# Type Widening\n> ***New Section after the [Clustered Table](#clustered-table) section***\n\nThe Type Widening feature enables changing the type of a column or field in an existing Delta table to a wider type.\n\nThe supported type changes are:\n- Integer widening:\n  - `Byte` -> `Short` -> `Int` -> `Long`\n- Floating-point widening:\n  - `Float` -> `Double`\n  - `Byte`, `Short` or `Int` -> `Double`\n- Date widening:\n  - `Date` -> `Timestamp without timezone`\n- Decimal widening - `p` and `s` denote the decimal precision and scale respectively.\n  - `Decimal(p, s)` -> `Decimal(p + k1, s + k2)` where `k1 >= k2 >= 0`.\n  - `Byte`, `Short` or `Int` -> `Decimal(10 + k1, k2)` where `k1 >= k2 >= 0`.\n  - `Long` -> `Decimal(20 + k1, k2)` where `k1 >= k2 >= 0`.\n\nTo support this feature:\n- The table must be on Reader version 3 and Writer Version 7.\n- The feature `typeWidening` must exist in the table `protocol`'s `readerFeatures` and `writerFeatures`, either during its creation or at a later stage.\n\nWhen supported:\n - A table may have a metadata property `delta.enableTypeWidening` in the Delta schema set to `true`. Writers must reject widening type changes when this property isn't set to `true`.\n - The `metadata` for a column or field in the table schema may contain the key `delta.typeChanges` storing a history of type changes for that column or field.\n\n### Type Change Metadata\n\nType changes applied to a table are recorded in the table schema and stored in the `metadata` of their nearest ancestor [StructField](#struct-field) using the key `delta.typeChanges`.\nThe value for the key `delta.typeChanges` must be a JSON list of objects, where each object contains the following fields:\nField Name | optional/required | Description\n-|-|-\n`fromType`| required | The type of the column or field before the type change.\n`toType`| required | The type of the column or field after the type change.\n`fieldPath`| optional | When updating the type of a map key/value or array element only: the path from the struct field holding the metadata to the map key/value or array element that was updated.\n\nThe `fieldPath` value is \"key\", \"value\" and \"element\"  when updating resp. the type of a map key, map value and array element.\nThe `fieldPath` value for nested maps and nested arrays are prefixed by their parents's path, separated by dots.\n\nThe following is an example for the definition of a column that went through two type changes:\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : \"long\",\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.typeChanges\": [\n        {\n          \"fromType\": \"short\",\n          \"toType\": \"integer\"\n        },\n        {\n          \"fromType\": \"integer\",\n          \"toType\": \"long\"\n        }\n      ]\n    }\n  }\n```\n\nThe following is an example for the definition of a column after changing the type of a map key:\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : {\n      \"type\": \"map\",\n      \"keyType\": \"double\",\n      \"valueType\": \"integer\",\n      \"valueContainsNull\": true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.typeChanges\": [\n        {\n          \"fromType\": \"float\",\n          \"toType\": \"double\",\n          \"fieldPath\": \"key\"\n        }\n      ]\n    }\n  }\n```\n\nThe following is an example for the definition of a column after changing the type of a map value nested in an array:\n```json\n{\n    \"name\" : \"e\",\n    \"type\" : {\n      \"type\": \"array\",\n      \"elementType\": {\n        \"type\": \"map\",\n        \"keyType\": \"string\",\n        \"valueType\": \"decimal(10, 4)\",\n        \"valueContainsNull\": true\n      },\n      \"containsNull\": true\n    },\n    \"nullable\" : true,\n    \"metadata\" : { \n      \"delta.typeChanges\": [\n        {\n          \"fromType\": \"decimal(6, 2)\",\n          \"toType\": \"decimal(10, 4)\",\n          \"fieldPath\": \"element.value\"\n        }\n      ]\n    }\n  }\n```\n\n## Writer Requirements for Type Widening\n\nWhen Type Widening is supported (when the `writerFeatures` field of a table's `protocol` action contains `typeWidening`), then:\n- Writers must reject applying any unsupported type change.\n- Writers must reject applying type changes not supported by [Iceberg V2](https://iceberg.apache.org/spec/#schema-evolution)\n  when either the [Iceberg Compatibility V1](#iceberg-compatibility-v1) or [Iceberg Compatibility V2](#iceberg-compatibility-v2) table feature is supported:\n  - `Byte`, `Short` or `Int` -> `Double`\n  - `Date`  -> `Timestamp without timezone`\n  - Decimal scale increase\n  - `Byte`, `Short`, `Int` or `Long` -> `Decimal`\n- Writers must record type change information in the `metadata` of the nearest ancestor [StructField](#struct-field). See [Type Change Metadata](#type-change-metadata).\n- Writers must preserve the `delta.typeChanges` field in the metadata fields in the schema when the table schema is updated.\n- Writers may remove the `delta.typeChanges` metadata in the table schema if all data files use the same field types as the table schema.\n\nWhen Type Widening is enabled (when the table property `delta.enableTypeWidening` is set to `true`), then:\n- Writers should allow updating the table schema to apply a supported type change to a column, struct field, map key/value or array element.\n\nWhen removing the Type Widening table feature from the table, in the version that removes `typeWidening` from the `writerFeatures` and `readerFeatures` fields of the table's `protocol` action:\n- Writers must ensure no `delta.typeChanges` metadata key is present in the table schema. This may require rewriting existing data files to ensure that all data files use the same field types as the table schema in order to fulfill the requirement to remove type widening metadata.\n- Writers must ensure that the table property `delta.enableTypeWidening` is not set.\n\n## Reader Requirements for Type Widening\nWhen Type Widening is supported (when the `readerFeatures` field of a table's `protocol` action contains `typeWidening`), then:\n- Readers must allow reading data files written before the table underwent any supported type change, and must convert such values to the current, wider type.\n- Readers must validate that they support all type changes in the `delta.typeChanges` field in the table schema for the table version they are reading and fail when finding any unsupported type change.\n\n## Writer Requirements for IcebergCompatV1\n> ***Change to existing section (underlined)***\n\nWhen supported and active, writers must:\n- Require that Column Mapping be enabled and set to either `name` or `id` mode\n- Require that Deletion Vectors are not supported (and, consequently, not active, either). i.e., the `deletionVectors` table feature is not present in the table `protocol`.\n- Require that partition column values are materialized into any Parquet data file that is present in the table, placed *after* the data columns in the parquet schema\n- Require that all `AddFile`s committed to the table have the `numRecords` statistic populated in their `stats` field\n- <ins>When the [Type Widening](#type-widening) table feature is supported, require that all type changes applied on the table are supported by [Iceberg V2](https://iceberg.apache.org/spec/#schema-evolution), based on the [Type Change Metadata](#type-change-metadata) recorded in the table schema.<ins>\n\n## Writer Requirements for IcebergCompatV2\n> ***Change to existing section (underlined)***\n\nWhen this feature is supported and enabled, writers must:\n- Require that Column Mapping be enabled and set to either `name` or `id` mode\n- Require that the nested `element` field of ArrayTypes and the nested `key` and `value` fields of MapTypes be assigned 32 bit integer identifiers. These identifiers must be unique and different from those used in [Column Mapping](#column-mapping), and must be stored in the metadata of their nearest ancestor [StructField](#struct-field) of the Delta table schema. Identifiers belonging to the same `StructField` must be organized as a `Map[String, Long]` and stored in metadata with key `parquet.field.nested.ids`. The keys of the map are \"element\", \"key\", or \"value\", prefixed by the name of the nearest ancestor StructField, separated by dots. The values are the identifiers. The keys for fields in nested arrays or nested maps are prefixed by their parents' key, separated by dots. An [example](#example-of-storing-identifiers-for-nested-fields-in-arraytype-and-maptype) is provided below to demonstrate how the identifiers are stored. These identifiers must be also written to the `field_id` field of the `SchemaElement` struct in the [Parquet Thrift specification](https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift) when writing parquet files.\n- Require that IcebergCompatV1 is not active, which means either the `icebergCompatV1` table feature is not present in the table protocol or the table property `delta.enableIcebergCompatV1` is not set to `true`\n- Require that Deletion Vectors are not active, which means either the `deletionVectors` table feature is not present in the table protocol or the table property `delta.enableDeletionVectors` is not set to `true`\n- Require that partition column values be materialized when writing Parquet data files\n- Require that all new `AddFile`s committed to the table have the `numRecords` statistic populated in their `stats` field\n- Require writing timestamp columns as int64\n- Require that the table schema contains only data types in the following allow-list: [`byte`, `short`, `integer`, `long`, `float`, `double`, `decimal`, `string`, `binary`, `boolean`, `timestamp`, `timestampNTZ`, `date`, `array`, `map`, `struct`].\n- <ins>When the [Type Widening](#type-widening) table feature is supported, require that all type changes applied on the table are supported by [Iceberg V2](https://iceberg.apache.org/spec/#schema-evolution), based on the [Type Change Metadata](#type-change-metadata) recorded in the table schema.<ins>\n\n### Column Metadata\n> ***Change to existing section (underlined)***\n\nA column metadata stores various information about the column.\nFor example, this MAY contain some keys like [`delta.columnMapping`](#column-mapping) or [`delta.generationExpression`](#generated-columns) or [`CURRENT_DEFAULT`](#default-columns).  \nField Name | Description\n-|-\ndelta.columnMapping.*| These keys are used to store information about the mapping between the logical column name to  the physical name. See [Column Mapping](#column-mapping) for details.\ndelta.identity.*| These keys are for defining identity columns. See [Identity Columns](#identity-columns) for details.\ndelta.invariants| JSON string contains SQL expression information. See [Column Invariants](#column-invariants) for details.\ndelta.generationExpression| SQL expression string. See [Generated Columns](#generated-columns) for details.\n<ins>delta.typeChanges</ins>| <ins>JSON string containing information about previous type changes applied to this column. See [Type Change Metadata](#type-change-metadata) for details.</ins>\n"
  },
  {
    "path": "protocol_rfcs/accepted/vacuum-protocol-check.md",
    "content": "# Vacuum Protocol Check\n\nThis RFC introduces a new ReaderWriter feature named `vacuumProtocolCheck`. This feature ensures that the Vacuum operation consistently performs both reader and writer protocol check. The motivation for this change is to address inconsistencies in Vacuum's behavior across different delta implementations, as some of them skip the writer protocol checks in practice. This omission blocks any protocol changes that might impact vacuum, including improvements to vacuum itself. The writer protocol check addresses an initial oversight in the original Delta specification where an older Delta Client executing a Vacuum command might incorrectly delete files that are still in use by newer versions, potentially leading to data corruption.\n\n**For further discussions about this protocol change, please refer to the Github issue - https://github.com/delta-io/delta/issues/2630**\n\n--------\n\n\n> ***New Section***\n# VACUUM Protocol Check\n\nThe `vacuumProtocolCheck` ReaderWriter feature ensures consistent application of reader and writer protocol checks during `VACUUM` operations, addressing potential protocol discrepancies and mitigating the risk of data corruption due to skipped writer checks.\n\nEnablement:\n- The table must be on Writer Version 7 and Reader Version 3.\n- The feature `vacuumProtocolCheck` must exist in the table `protocol`'s `writerFeatures` and `readerFeatures`.\n\n## Writer Requirements for Vacuum Protocol Check\n\nThis feature affects only the VACUUM operations; standard commits remain unaffected.\n\nBefore performing a VACUUM operation, writers must ensure that they check the table's write protocol. This is most easily implemented by adding an unconditional write protocol check for all tables, which removes the need to examine individual table properties.\n\nWriters that do not implement VACUUM do not need to change anything and can safely write to tables that enable the feature.\n\n## Recommendations for Readers of Tables with Vacuum Protocol Check feature\n\nFor tables with Vacuum Protocol Check enabled, readers don’t need to understand or change anything new; they just need to acknowledge the feature exists."
  },
  {
    "path": "protocol_rfcs/accepted/variant-type.md",
    "content": "# Variant Data Type\n**Folded into [PROTOCOL.md](../../protocol.md#variant-data-type)**\n\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/2864**\n\nThis protocol change adds support for the Variant data type.\nThe Variant data type is beneficial for storing and processing semi-structured data.\n\n--------\n\n> ***New Section after the [Clustered Table](#clustered-table) section***\n\n# Variant Data Type\n\nThis feature enables support for the `variant` data type, which stores semi-structured data.\nThe schema serialization method is described in [Schema Serialization Format](#schema-serialization-format).\n\nTo support this feature:\n- The table must be on Reader Version 3 and Writer Version 7\n- The feature `variantType` must exist in the table `protocol`'s `readerFeatures` and `writerFeatures`.\n\n## Example JSON-Encoded Delta Table Schema with Variant types\n\n```\n{\n  \"type\" : \"struct\",\n  \"fields\" : [ {\n    \"name\" : \"raw_data\",\n    \"type\" : \"variant\",\n    \"nullable\" : true,\n    \"metadata\" : { }\n  }, {\n    \"name\" : \"variant_array\",\n    \"type\" : {\n      \"type\" : \"array\",\n      \"elementType\" : {\n        \"type\" : \"variant\"\n      },\n      \"containsNull\" : false\n    },\n    \"nullable\" : false,\n    \"metadata\" : { }\n  } ]\n}\n```\n\n## Variant data in Parquet\n\nThe Variant data type is represented as two binary encoded values, according to the [Spark Variant binary encoding specification](https://github.com/apache/spark/blob/master/common/variant/README.md).\nThe two binary values are named `value` and `metadata`.\n\nWhen writing Variant data to parquet files, the Variant data is written as a single Parquet struct, with the following fields:\n\nStruct field name | Parquet primitive type | Description\n-|-|-\nvalue | binary | The binary-encoded Variant value, as described in [Variant binary encoding](https://github.com/apache/spark/blob/master/common/variant/README.md)\nmetadata | binary | The binary-encoded Variant metadata, as described in [Variant binary encoding](https://github.com/apache/spark/blob/master/common/variant/README.md)\n\nThe parquet struct must include the two struct fields `value` and `metadata`.\nSupported writers must write the two binary fields, and supported readers must read the two binary fields.\n\n[Variant shredding](https://github.com/apache/parquet-format/blob/master/VariantShredding.md) will be introduced in a separate `variantShredding` table feature. will be introduced later, as a separate `variantShredding` table feature.\n\n## Writer Requirements for Variant Data Type\n\nWhen Variant type is supported (`writerFeatures` field of a table's `protocol` action contains `variantType`), writers:\n- must write a column of type `variant` to parquet as a struct containing the fields `value` and `metadata` and storing values that conform to the [Variant binary encoding specification](https://github.com/apache/spark/blob/master/common/variant/README.md)\n- must not write a parquet struct field named `typed_value` to avoid confusion with the field required by [Variant shredding](https://github.com/apache/parquet-format/blob/master/VariantShredding.md) with the same name.\n\n## Reader Requirements for Variant Data Type\n\nWhen Variant type is supported (`readerFeatures` field of a table's `protocol` action contains `variantType`), readers:\n- must recognize and tolerate a `variant` data type in a Delta schema\n- must use the correct physical schema (struct-of-binary, with fields `value` and `metadata`) when reading a Variant data type from file\n- must make the column available to the engine:\n    - [Recommended] Expose and interpret the struct-of-binary as a single Variant field in accordance with the [Spark Variant binary encoding specification](https://github.com/apache/spark/blob/master/common/variant/README.md).\n    - [Alternate] Expose the raw physical struct-of-binary, e.g. if the engine does not support Variant.\n    - [Alternate] Convert the struct-of-binary to a string, and expose the string representation, e.g. if the engine does not support Variant.\n\n## Compatibility with other Delta Features\n\nFeature | Support for Variant Data Type\n-|-\nPartition Columns | **Supported:** A Variant column is allowed to be a non-partitioned column of a partitioned table. <br/> **Unsupported:** Variant is not a comparable data type, so it cannot be included in a partition column.\nClustered Tables | **Supported:** A Variant column is allowed to be a non-clustering column of a clustered table. <br/> **Unsupported:** Variant is not a comparable data type, so it cannot be included in a clustering column.\nDelta Column Statistics | **Supported:** A Variant column supports the `nullCount` statistic. <br/> **Unsupported:** Variant is not a comparable data type, so a Variant column does not support the `minValues` and `maxValues` statistics.\nGenerated Columns | **Supported:** A Variant column is allowed to be used as a source in a generated column expression, as long as the Variant type is not the result type of the generated column expression. <br/> **Unsupported:** The Variant data type is not allowed to be the result type of a generated column expression.\nDelta CHECK Constraints | **Supported:** A Variant column is allowed to be used for a CHECK constraint expression.\nDefault Column Values | **Supported:** A Variant column is allowed to have a default column value.\nChange Data Feed | **Supported:** A table using the Variant data type is allowed to enable the Delta Change Data Feed.\n\n--------\n\n> ***New Sub-Section after the [Map Type](#map-type) sub-section within the [Schema Serialization Format](#schema-serialization-format) section***\n\n### Variant Type\n\nVariant data uses the Delta type name `variant` for Delta schema serialization.\n\nField Name | Description\n-|-\ntype | Always the string \"variant\"\n"
  },
  {
    "path": "protocol_rfcs/checkpoint-protection.md",
    "content": "# Checkpoint Protection\n\nThis RFC introduces a new Writer feature named `checkpointProtection`. When the feature is present in the protocol, no checkpoint removal/creation before `delta.requireCheckpointProtectionBeforeVersion` is allowed during metadata cleanup, unless everything is cleaned up in one go.\n\nThe motivation is to improve the drop feature functionality. Today, dropping a feature requires truncating the history of a Delta table at the version boundary where the feature is removed from the protocol. This is necessary because the Delta protocol only safely supports table protocols that are monotonically increasing with table versions. And because it is unsafe to truncate the history of a Delta table while transactions are running, dropping a feature requires a 24-hour wait time to avoid corrupting the table.\n\nWe can improve this process by setting up the table's history (including checkpoints) in such a way that older readers will be able to handle it correctly, i.e., to read correctly at versions for which they support the read protocol, and to reject reading of versions for which they do not support all features. The `checkpointProtection` feature is needed to ensure that this very specific setup of the history _stays in place_ until the feature removal is cleaned up from the retained version history.\n\nA key component of this solution is a special set of protected checkpoints at the DROP FEATURE boundary that are guaranteed to persist until all history is truncated up to the checkpoints in one go. These checkpoints act as barriers that hide unsupported commit\nrecords behind them. By \"hiding\", we mean that older readers will not need to replay those commits that they _don't_ support in order to reconstruct the table state at a later version that they _do_ support. With the `checkpointProtection`, we can guarantee these checkpoints will persist until history is truncated.\n\nFurthermore, with the new drop feature method, it is no longer guaranteed that protocols are monotonically increasing. This means that clients that validate against the latest protocol can no longer assume that they can also operate on earlier versions correctly. In particular, writers are allowed to create checkpoints at earlier versions, but if they do this without checking the protocol at that specific version, and then they may write corrupted checkpoints for table versions for which they do not support the protocol. The `checkpointProtection` table feature also protects against these cases by requiring writers to check the protocol versions at historical table versions before creating a new checkpoint.\n\nWith these changes, we can drop table features without needing to truncate history. More importantly, they simplify the drop feature user journey by requiring a single execution of the DROP FEATURE command.\n\n**For further discussions about this protocol change, please refer to the Github issue - https://github.com/delta-io/delta/issues/4152**\n\n--------\n\n> ***Add a new section at the [Table Features](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#table-features) section***\n# Checkpoint Protection\n\nThe `checkpointProtection` is a Writer feature that protects checkpoints before the version indicated by table property `delta.requireCheckpointProtectionBeforeVersion`, and that forbids writers from creating checkpoints before that version unless they confirm that they support the table protocol at that version.\n\nEnablement:\n- The table must be at least on Writer Version 7 and Reader Version 1.\n- The feature `checkpointProtection` must exist in the table `protocol`'s `writerFeatures`.\n\n## Writer Requirements for Checkpoint Protection\n\nFor tables with `checkpointProtection` supported in the protocol:\n\na) Writers must not clean up any checkpoints for table versions before the version given by table property `delta.requireCheckpointProtectionBeforeVersion`.\n\nb) Writers must not create new checkpoints for table versions before the version given by table property `delta.requireCheckpointProtectionBeforeVersion` unless they support all of the features in the table protocol at that version.\n\nc) Writers must not clean up version history for table versions for which they do not support the protocol. A writer is allowed to clean up a range of versions if it supports all table features for every version that is being cleaned up. If a writer does not support the protocol for some of the versions that are being cleaned up, then the cleanup is allowed if and only if the cleanup includes _all_ table versions before the version given by  `delta.requireCheckpointProtectionBeforeVersion`. In this case, a single cleanup operation should truncate the history up to that boundary version in one go as opposed to several cleanup operations truncating in chunks.\n\nd) In version history cleanup, writers must remove commits _before_ removing the associated checkpoints, so that requirement (a) is satisfied even during the cleanup.\n\n## Recommendations for Readers of Tables with Checkpoint Protection feature\n\nFor tables with `checkpointProtection` supported in the protocol, readers do not need to understand or change anything new; they just need to acknowledge the feature exists."
  },
  {
    "path": "protocol_rfcs/collated-string-type.md",
    "content": "# Collated String Type\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/2894**\n\nThis protocol change adds support for collated strings. It consists of three changes to the protocol:\n\n* Collations in the table schema\n* Per-column statistics are annotated with the collation that was used to collect them\n* Domain metadata with active collation version\n\n--------\n> *** Add New Section after the [Clustered Table](#clustered-table) section***\n# Collations Table Feature\n\nTo support this feature:\n* The table must have Writer Version 7. \n* The feature `collations` must exist in the table's `writerFeatures`.\n* The feature `domainMetadata` must exist in the table's `writerFeatures`. \n\n## Reader Requirements for Collations:\n\nWhen Collations are supported (when the `writerFeatures` field of a table's protocol action contains `collations`), then:\n- Readers could do comparisons and sorting of strings based on the collation specified in the schema. \n- If the collation is not specified for a string type, then the reader must use the default comparison operators for the binary representation of strings under UTF-8 encoding.\n- Readers must only do file skipping based on column statistics for a collation if the filter operator used for the data skipping is specified to treat the column as having that same collation. For example, when filtering a string column using the string equality comparison operator that is configured with the collation `ICU.en_US.72`, the reader must not use file skipping statistics from the collation `spark.UTF8_LCASE.75.1`. It should also not use the statistics from `ICU.en_US.69` because the collation version number does not match.\n\n## Writer Requirements for Collations:\n\nWhen Collations are supported (when the `writerFeatures` field of a table's protocol action contains `collations`), then:\n- Writers must write the collation identifier in the schema metadata for a column with non-default collation, i.e., any collation that is not comparing strings using their binary representations under UTF-8 encoding.\n- Writers must not write the collation identifier in the schema metadata for a column with default collation (comparisons using binary representation of the strings under UTF-8 encoding).\n- Writers could write per-file statistics for string columns with non-default collations in `statsWithCollation`. See [Per-file Statistics](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#per-file-statistics) for more details.\n- If a writer adds per-file statistics for a new version of a collation, the writer should also update the `domainMetadata` for the `collations` table feature to include the new collation versions that are used to collect statistics.\n- Writers could remove a collation version from the `domainMetadata` for the `collations` table feature if stats collection for the collation version is no longer desired. For example, the engine upgrades their ICU library and now desires a newer version for a collation.\n\n> ***Add a new section in front of the [Primitive Types](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#primitive-types) section.***\n\n### Collations\nCollations are a set of rules for how strings are compared. Collations do not affect how strings are stored. Collations are applied when comparing strings for equality or to determine the sort order of two strings. Case insensitive comparison is one example of a collation where case is ignored when string are compared for equality and the lower cased variant of a string is used to determine its sort order.\n\nEach string field can have a collation, which is specified in the table schema. It is also possible to store statistics per collation version. This is required because the min and max values of a column can differ based on the used collation or collation version.\n\nBy default, all strings are collated using binary collation. That means that strings compare equal if their binary UTF-8 encoded representations are equal. The binary UTF-8 encoded representation is also used to sort them. Note that in Delta all strings are encoded in UTF-8.\n\nThe `collations` table feature is a writer only feature and allows clients that do not support collations to read the table using UTF-8 binary collation. To support the table feature clients must preserve collations when they change the schema. Collecting collated statistics is optional and it is valid to store UTF-8 binary collated statistics for fields with a collation other than UTF-8 binary.\n\nThe column level collation indicates the default collation that readers should use to operate on a column. However, readers are responsible for choosing what collation to actually apply on operations. An engine may apply a different collation than the schema collation based on the engine's collation precedence rules. However, an engine must take care to only use column statistics for file skipping from a collation that is identical to the one specified in the filtering operation in all aspects, including the collation version.\n\n#### Collation identifiers\n\nCollations can be referred to using collation identifiers. The Delta format does not specify any collation rules other than binary collation, but supports the concept of collation providers such that engines can use providers like [ICU](https://icu.unicode.org/) and mark statistics accordingly.\n\nA collation identifier consists of 3 parts, which are combined into one identifier using dots as separators. Dots are not allowed to be part of provider and collation names, but can be used in versions.\n\nPart | Description\n-|-\nProvider | Name of the provider. Must not contain dots\nName | Name of the collation as provided by the provider. Must not contain dots\nVersion | Version string. Is allowed to contain dots. This part is optional. Collations without a version are used in the schema because readers are not forced to use a specific version of the collation. Statistics are annotated with versioned collations to guarantee correctness.\n\n#### Specifying collations in the table schema\n\nCollations can be specified for any string type in a schema. This includes string fields, but also the key and value type of maps and the element type of arrays. Collations are stored in the `__COLLATIONS` key of the metadata of the nearest ancestor [StructField](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#struct-field) of the Delta table schema. Nested maps and arrays are encoded the same way as ids in [IcebergCompatV2](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#writer-requirements-for-icebergcompatv2). Collation identifiers are stored without key because the version of a collation is not enforced for reading.\n\nThis example provides an overview of how collations are stored in the schema. Note that irrelevant fields have been stripped.\n\nExample schema\n\n```\n|-- col1: string\n|-- col2: array\n|       |-- elementType: map\n|                      |-- keyType: string\n|                      |-- valueType: struct\n|                                   |-- f1: string\n```\n\nSchema with collation information\n\n```\n{\n   \"type\":\"struct\",\n   \"fields\":[\n      {\n         \"name\":\"col1\",\n         \"type\":\"string\",\n         \"metadata\":{\n            \"__COLLATIONS\":{\n               \"col1\":\"ICU.de_DE\"\n            }\n         }\n      },\n      {\n         \"name\":\"col2\",\n         \"type\":{\n            \"type\":\"array\",\n            \"elementType\":{\n               \"type\":\"map\",\n               \"keyType\":\"string\",\n               \"valueType\":{\n                  \"type\":\"struct\",\n                  \"fields\":[\n                     {\n                        \"name\":\"f1\",\n                        \"type\":\"string\",\n                        \"metadata\":{\n                           \"__COLLATIONS\":{\n                              \"f1\":\"ICU.de_DE\"\n                           }\n                        }\n                     }\n                  ]\n               }\n            }\n         },\n         \"metadata\":{\n            \"__COLLATIONS\":{\n               \"col2.element.key\":\"ICU.en_US\"\n            }\n         }\n      }\n   ]\n}\n```\n\n#### Collation versions\n\nThe [Domain Metadata](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#domain-metadata) for the `collations` table feature contains hints for which versions of a collations clients should produce statistics when writing to the table. The hints allow clients to choose a collation version without having to look at the statistics of all AddFiles first. Clients are allowed to ignore the hints.\n\n`collations` [Domain Metadata](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#domain-metadata)\n\n```\n{\n  \"writeVersions\": {\n    \"ICU.en_US\": [\"72\", \"73\"]\n  }\n}\n```\n\n> ***Update the string row in the [Primitive Types](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#primitive-types) table.***\n\n### Primitive Types\n\nType Name | Description\n-|-\nstring| UTF-8 encoded string of characters. A collation can be specified in [Column Metadata](#specifying-collations-in-the-table-schema), otherwise binary collation is used as the default.\n\n> ***Add new rows to the [Column Metadata](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#column-metadata) table.***\n\nField Name | Description\n-|-\n__COLLATIONS | Collations for strings stored in the field or combinations of maps and arrays that are stored in this field and do not have nested structs. Refer to [Specifying collations in the table schema](#specifying-collations-in-the-table-schema) for more details.\n\n> ***Edit the [Per-file Statistics](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#per-file-statistics) section and change it from the \"Per-column statistics\" section onwards.***\n\nPer-column statistics record information for each column in the file and they are encoded, mirroring the schema of the actual data. Statistic are optional and it is allowed to provide UTF-8 binary statistics for strings when the field has a different collation.\nFor example, given the following data schema:\n\n```\n|-- a: struct\n|    |-- b: struct\n|    |    |-- c: long\n|-- d: struct\n     |-- e: string collate ICU.en_US.72\n```\n\nStatistics could be stored with the following schema:\n```\n|-- stats: struct\n|    |-- numRecords: long\n|    |-- tightBounds: boolean\n|    |-- minValues: struct\n|    |    |-- a: struct\n|    |    |    |-- b: struct\n|    |    |    |    |-- c: long\n|    |-- maxValues: struct\n|    |    |-- a: struct\n|    |    |    |-- b: struct\n|    |    |    |    |-- c: long\n|    |-- statsWithCollation: struct\n|    |    |-- ICU.en_US.72: struct\n|    |    |    |-- minValues: struct\n|    |    |    |    |-- d: struct\n|    |    |    |    |    | e: string\n|    |    |    |-- maxValues: struct\n|    |    |    |    |-- d: struct\n|    |    |    |    |    | e: string\n```\n\nThe following per-column statistics are currently supported:\n\nName | Description (`stats.tightBounds=true`) | Description (`stats.tightBounds=false`)\n-|-|-\nnullCount | The number of `null` values for this column | <p>If the `nullCount` for a column equals the physical number of records (`stats.numRecords`) then **all** valid rows for this column must have `null` values (the reverse is not necessarily true).</p><p>If the `nullCount` for a column equals 0 then **all** valid rows are non-`null` in this column (the reverse is not necessarily true).</p><p>If the `nullCount` for a column is any value other than these two special cases, the value carries no information and should be treated as if absent.</p>\nminValues | A value that is equal to the smallest valid value[^1] present in the file for this column. If all valid rows are null, this carries no information. | A value that is less than or equal to all valid values[^1] present in this file for this column. If all valid rows are null, this carries no information.\nmaxValues | A value that is equal to the largest valid value[^1] present in the file for this column. If all valid rows are null, this carries no information. | A value that is greater than or equal to all valid values[^1] present in this file for this column. If all valid rows are null, this carries no information.\nstatsWithCollation | minValues and maxValues for string columns that are not using binary collation. | Has the same semantics as the top level minValues and maxValues, but wraps both minValues and maxValues into an object keyed by the collation used the generate them.\n\n[^1]: String columns are cut off at a fixed prefix length. Timestamp columns are truncated down to milliseconds.\n"
  },
  {
    "path": "protocol_rfcs/column-mapping-usage-tracking.md",
    "content": "# Column Mapping Usage Tracking\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/2682**\n\nThis RFC proposes an extension for Column Mapping to track where columns have been dropped or renamed during the history of a table.\nThis allows using the (logical) name of a column as the physical name of a column, while still ensuring that all physical names are unique.\nThis helps with the disablement of Column Mapping proposed in [#2481](https://github.com/delta-io/delta/issues/2481), as in this case it is no longer required to rewrite the table, and it simply suffices to change the mode to none.\n\n--------\n\n> New subsection at the end of the `Column Mapping` section\n\n## Usage Tracking\n\nColumn Mapping Usage Tracking is an extension of the column mapping feature that allows Delta to track whether a column has been dropped or renamed.\nThis is tracked by the table property `delta.columnMapping.hasDroppedOrRenamed`. This table property is set to `false` when the table is created, and flipped to `true` when the first column is either dropped or renamed.\nThe writer table feature `columnMappingUsageTracking` is added to the `writerFeatures` in the `protocol` to ensure that all writers correctly track when columns are dropped or renamed.\n\n--------\n\n> Modification to the `Writer Requirements for Column Mapping` subsection\n\n- Assign a globally unique identifier as the physical name for each new column that is added to the schema. This is especially important for supporting cheap column deletions in `name` mode. In addition, column identifiers need to be assigned to each column. The maximum id that is assigned to a column is tracked as the table property `delta.columnMapping.maxColumnId`. This is an internal table property that cannot be configured by users. This value must increase monotonically as new columns are introduced and committed to the table alongside the introduction of the new columns to the schema.\n\n**is replaced by**\n\n- Assign a unique physical name to each column.\n    - When enabling column mapping on existing table, the physical name of the column must be set to the (logical) name of the column.\n    - If the feature `columnMappingUsageTracking` is supported, then when adding a new column to a table and `delta.columnMapping.hasDroppedOrRenamed` column property is `false` the (logical) name of the column should be used as the physical name.\n    - Otherwise the physical column name must contain a universally unique identifier (UUID) to guarantee uniqueness.\n- Assign a column id to each column. The maximum id that is assigned to a column is tracked as the table property `delta.columnMapping.maxColumnId`. This is an internal table property that cannot be configured by users. This value must increase monotonically as new columns are introduced and committed to the table alongside the introduction of the new columns to the schema.\n\n--------\n\n> New subsection at the end of the `Writer Requirements for Column Mapping` subsection\n\n### Writer Requirements for Usage Tracking\n\nIn order to support column mapping usage tracking, writers must:\n- Write `protocol` and `metaData` actions when Column Mapping Usage Tracking is turned on for the first time:\n    - Write a `protocol` action with writer version 7 and the feature `columnMappingUsageTracking` in the `writerFeatures`.\n    - Write a `metaData` action with the table property `delta.columnMapping.hasDroppedOrRenamed` set to `false` when creating a new table or enabling the feature on an existing table without column mapping enabled, and set to `true` when enabling usage tracking on an existing table with column mapping enabled.\n- When dropping or renaming a column `delta.columnMapping.hasDroppedOrRenamed` must be set to `true`.\n- After `delta.columnMapping.hasDroppedOrRenamed` is set to `true` it must never be set back to `false` again.\n"
  },
  {
    "path": "protocol_rfcs/iceberg-compat-v3.md",
    "content": "# IcebergCompatV3\n\nThis protocol change introduces a compatibility flag, which ensures that a delta table can be safely\nread and written as an Apache Iceberg™ format table, similar to\n[IcebergCompatV1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1)\nand\n[IcebergCompatV2](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v2).\n\n--------\n\n# IcebergCompatV3\n> ***New Section after [Iceberg Compatibility V2](#iceberg-compatibility-v2)***\n\n# Iceberg Compatibility V3\n\nThis table feature (`icebergCompatV3`) ensures that Delta tables can be converted to Apache Iceberg™ format, though this table feature does not implement or specify that conversion.\n\nTo support this feature:\n- Since this table feature depends on Column Mapping, the table must be on Reader Version = 2, or it must be on Reader Version >= 3 and the feature `columnMapping` must exist in the `protocol`'s `readerFeatures`.\n- The table must be on Writer Version 7.\n- The feature `icebergCompatV3` must exist in the table protocol's `writerFeatures`.\n\nThis table feature is enabled when the table property `delta.enableIcebergCompatV3` is set to `true`.\n\n> **NOTE:** Unlike IcebergCompatV1 and IcebergCompatV2, this feature does _NOT_ forbid supporting and enabling Deletion Vectors on the table.\n\n## Writer Requirements for IcebergCompatV3\n\nWhen this feature is supported and enabled, writers must:\n- Require that Column Mapping be enabled and set to either `name` or `id` mode\n- Require that Row Tracking to be enabled on the table.\n  - Materialized Row ID column must use field ID 2147483540\n  - Materialized Row Commit Version column must use field ID 2147483539\n- Require that the nested `element` field of ArrayTypes and the nested `key` and `value` fields of MapTypes be assigned 32 bit integer identifiers. The requirement to ID allocation is the same as that in IcebergCompatV2.\n- Require that IcebergCompatV1 and IcebergCompatV2 are not active on the table\n- Require that partition column values be materialized when writing Parquet data files\n- Require that all new `AddFile`s committed to the table have the `numRecords` statistic populated in their `stats` field\n- Require writing timestamp columns as int64\n- Block replacing partitioned tables with a differently-named partition spec\n  - e.g. replacing a table partitioned by `part_a INT` with partition spec `part_b INT` must be blocked\n  - e.g. replacing a table partitioned by `part_a INT` with partition spec `part_a LONG` is allowed"
  },
  {
    "path": "protocol_rfcs/iceberg-writer-compat-v1.md",
    "content": "# IcebergWriterCompatV1\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/4284\n\nThis protocol change introduces a compatibility flag, which ensures that a delta table can be safely\nread and written as an Apache Iceberg™ format table, similar to\n[IcebergCompatV1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1)\nand\n[IcebergCompatV2](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v2).\n\n--------\n\n# IcebergWriterCompatV1\n> ***New Section after [Iceberg Compatibility V2](#iceberg-compatibility-v2)***\n\nThis table feature (`icebergWriterCompatV1`) ensures that Delta tables can be converted to Apache\nIceberg™ format, though this table feature does not implement or specify that conversion.\n\nTo support this feature:\n- Since this table feature depends on Column Mapping, the table must be on Reader Version = 2, or it must be on Reader Version >= 3 and the feature `columnMapping` must exist in the `protocol`'s `readerFeatures`.\n- The table must be on Writer Version 7.\n- The feature `icebergCompatV2` must exist in the table protocol's `writerFeatures`.\n- The feature `icebergWriterCompatV1` must exist in the table protocol's `writerFeatures`.\n\nThis table feature is enabled when the table property `delta.enableIcebergWriterCompatV1` is set to `true`.\n\n## Writer Requirements for IcebergWriterCompatV1\nFor `IcebergWriterCompatV1` writers must ensure:\n\n- The table is using [Column Mapping](#column-mapping) and that it is set to `id` mode.\n  - Note this is a tightening of the `IcebergCompatV2` requirement which supports `name` and `id` mode.\n\n- Each field _must_ have a column mapping physical name that is exactly `col-[column id]`. That is the `delta.columnMapping.physicalName` in the column metadata _must_ be equal to `col-[delta.columnMapping.id]`. The following is an example compliant schema definition:\n\n```json\n{\n  \"type\": \"struct\",\n  \"fields\": [\n    {\n      \"name\": \"a\",\n      \"type\": \"integer\",\n      \"nullable\": false,\n      \"metadata\": {\n        \"delta.columnMapping.id\": 1,\n        \"delta.columnMapping.physicalName\": \"col-1\"\n      }\n    },\n    {\n      \"name\": \"b\",\n      \"type\": \"string\",\n      \"nullable\": false,\n      \"metadata\": {\n        \"delta.columnMapping.id\": 2,\n        \"delta.columnMapping.physicalName\": \"col-2\"\n      }\n    }\n  ]\n}\n```\n\n- The table does not contain any columns with the type `byte` or `short`\n  - Note that these types _are_ allowed by `IcebergCompatV2`\n  - Therefore the list of allowed types for a table with `IcebergWriterCompatV1` enabled is: [`integer`, `long`, `float`, `double`, `decimal`, `string`, `binary`, `boolean`, `timestamp`, `timestampNTZ`, `date`, `array`, `map`, `struct`].\n\n- [Iceberg Compatibility V2](#iceberg-compatibility-v2) is **enabled** on the table.\n  - This means _all_ the conditions that [Iceberg Compatibility V2](#iceberg-compatibility-v2) imposes are met.\n\n- The writer **must** block *any* schema changes to a `struct` that is used as a `map` key.\n  - For example, if the schema contains `map MAP<STRUCT<s: STRING>, INT>`, then any schema change to `map.key` must be disallowed.\n  - Changes to the schema of the value are allowed.\n  - This matches Iceberg's behavior, which is documented\n    [here](https://iceberg.apache.org/docs/nightly/spark-ddl/#alter-table-add-column). In practice\n    Iceberg writers block any changes, not just column additions.\n\n- Any enabled features are in the [allowlist](#allowed-supported-list-of-features)\n\n- All [Disallowed features](#disallowed-features) are not supported and/or inactive (see below)\n\n### Disallowed Features\nFor this section, we use the specific meanings of \"supported\" and \"active\" from [Supported Features](#supported-features). All the following features must not be used in the table. For legacy features (any feature introduced before writer version 7), the feature _can_ be \"supported\", but must _not_ be \"active\".\n\n| Feature                                                                                           | Legacy | Can be [\"supported\"](#supported-features)? | Not Active Check                                                                                                                                                                                                                                         |\n|---------------------------------------------------------------------------------------------------|--------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [column invariants](#column-invariants)                                                           | Yes    | Yes, if not active                         | No column includes `delta.invariants` in its [Metadata]                                                                                                                                                                                                  |\n| [Change Data Feed](#add-cdc-file)                                                                 | Yes    | Yes, if not active                         | The `delta.enableChangeDataFeed` configuration flag in the [Metadata] of the table does not exist (or is `disabled`?)                                                                                                                                    |\n| [CHECK Constraints](#check-constraints)                                                           | Yes    | Yes, if not active                         | No keys in the `configuration` field of [Metadata] start with `delta.constraints.`.                                                                                                                                                                      |\n| [Identity Columns](#identity-columns)                                                             | Yes    | Yes, if not active                         | No columns exist in the schema with any of the properties specified in [Identity Columns](#identity-columns) in the column metadata: `delta.identity.start`, `delta.identity.step`, `delta.identity.highWaterMark`, `delta.identity.allowExplicitInsert` |\n| [Generated Columns](#default-columns)                                                             | Yes    | Yes, if not active                         | No column metadata contains the key `delta.generationExpression`                                                                                                                                                                                         |\n| [Default Columns](#default-columns)                                                               | No     | No                                         | N/A                                                                                                                                                                                                                                                      |\n| [Row Tracking](#row-tracking)                                                                     | No     | Yes, if not active                         | The delta.enableRowTracking configuration flag in the Metadata of the table does not exist (or has a value of false)                                                                                                                                     |\n| [Collations](https://github.com/delta-io/delta/blob/master/protocol_rfcs/collated-string-type.md) | No     | No                                         | N/A                                                                                                                                                                                                                                                      |\n| [Variant Types](#variant-data-type)                                                               | No     | No                                         | N/A                                                                                                                                                                                                                                                      |\n### Allowed Supported list of features\nTo ensure that future features do not break tables with `IcebergWriterCompatV1` enabled, all enabled features must also be checked against an allowlist. Any enabled table features _must_ be in the list: [`appendOnly`, `columnMapping`, `icebergWriterCompatV1`, `icebergCompatV2`, `domainMetadata`, `vacuumProtocolCheck`, `v2Checkpoint`, `inCommitTimestamp`, `clustering`, `timestampNtz`, `typeWidening`]\n\nAdditionally, the following features are allowed to be \"supported\", but must not be \"active\" (see [Disallowed Features](#disallowed-features)): [`invariants`, `changeDataFeed`, `checkConstraints`, `identityColumns`, `generatedColumns`, `rowTracking`]. These features, if supported, must be verified to be \"inactive\" via the checks specified above.\n\nWe allow these legacy features to be \"supported\" because protocol updates can cause features to be carried over even though they are not in use. For example, if a table is on writer version 2, and then is updated to version 7, `invariants` can appear in the `writerFeatures` list because it was implicitly supported at version 2, even if it was not in use.\n\n[Metadata]: #change-metadata\n\n"
  },
  {
    "path": "protocol_rfcs/materialize-partition-columns.md",
    "content": "# Materialize Partition Columns\n\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/5555**\n\n## Overview\n\nCurrently, Delta tables store partition column values primarily in the table metadata (specifically in the `partitionValues` field of `AddFile` actions), and by default these columns are not physically written into the Parquet data files themselves.\n\nThis RFC proposes a new writer-only table feature called `materializePartitionColumns`. When supported, this feature requires partition columns to be physically materialized in Parquet data files alongside the data columns.\n\n## Motivation\n\nThis feature provides a mechanism to require partition column materialization at the protocol level, ensuring all writers to the table comply with this requirement during the period when the feature is supported.\n\nMaterializing partition columns enhances compatibility with Parquet readers that access Parquet files directly and do not interpret Delta’s AddFile metadata, as well as with Iceberg readers, which expect partition columns to be stored within the data files.\n\nAdditionally, having partition information embedded in the data files themselves enables more flexible data reorganization strategies. The same parquet files could be linked in future versions of a table that do not have the same (or any) partition columns.\n\n--------\n\n\n> ***New Section after Identity Columns section***\n## Materialize Partition Columns\n\nWhen this feature is supported, partition columns are physically written to Parquet files alongside the data columns. To support this feature:\n - The table must be on Writer Version 7, and a feature name `materializePartitionColumns` must exist in the table `protocol`'s `writerFeatures`.\n\nWhen supported:\n - When the writer feature `materializePartitionColumns` is set in the protocol, writers must materialize partition columns into any newly created data file, placing them after the data columns in the parquet\n  schema. This mimics the same partition column materialization requirement from [IcebergCompatV1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1)\nand\n[IcebergCompatV2](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v2). As such, the `materializePartitionColumns` feature can be seen as a subset of the requirements imposed by those features, providing the partition column materialization guarantee independently without requiring full\n  Iceberg compatibility.\n - When the writer feature `materializePartitionColumns` is not set in the table protocol, writers are not required to write partition columns to data files. Note that other features might still require materialization of partition values, such as [IcebergCompatV1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1)\n\nThis feature does not impose any requirements on readers. All Delta readers must be able to read the table regardless of whether partition columns are materialized in the data files. If partition values are present in both parquet and AddFile metadata, Delta readers should continue to read partition values from AddFile metadata.\n"
  },
  {
    "path": "protocol_rfcs/rejected/managed-commits.md",
    "content": "# Managed Commits\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/2598**\n\nThis RFC proposes a new table feature `managedCommit` which changes the way Delta Lake performs commits.\n\nToday’s Delta commit protocol relies on the filesystem to provide commit atomicity. This feature request is to allow Delta tables which gets commit atomicity using an external commit-owner and not\nthe filesystem (s3, abfs etc). This allows us to deal with various limitations of Delta:\n\n1. No reliable way for the table's owner to participate in commits.\n    - The table's owner (such as a catalog) cannot reliably stay in sync with the table state, nor reject commit attempts it wouldn’t like, because it doesn’t even know about writes until they are already durable (and visible to readers).\n    - No clear path to transactions that could span multiple tables and/or involve catalog updates, because filesystem commits cannot be made conditionally or atomically.\n2. No way to tie commit ownership to a table.\n    - In general, Delta tables have no way to advertise that they are managed by catalog or LogStore X (at endpoint Y).\n    - No way to express different commit owners for different tables. For example, Delta spark supports a notion of a \"[log store](https://delta.io/blog/2022-05-18-multi-cluster-writes-to-delta-lake-storage-in-s3/)\" or commit service for enforcing commit atomicity in S3, but it's a cluster-level setting that affects all tables indiscriminately, with no way to validate whether the mapping is even correct.\n    - There is no central entity that needs to be contacted in order to commit to the table. So if the underlying file system is missing _putIfAbsent_ semantics, then there is no way to ensure that a commit is atomic, which could lead\n      to lost writes when concurrent writers are writing to the table.\n\n--------\n\n\n### Delta Log Entries\n\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#delta-log-entries)***\n\nDelta files are stored as JSON in a directory at the root of the table named `_delta_log`, and together with checkpoints make up the log of all changes that have occurred to a table.\n~~Delta files are the unit of atomicity for a table, and are named using the next available version number, zero-padded to 20 digits.~~\n<ins>They are the unit of atomicity for a table.</ins>\n\n<ins>**Note:** If [managed commits](#managed-commits) table feature is enabled on the table, recently committed delta files may reside in the `_delta_log/_commits` directory. Delta clients have to contact\nthe corresponding commit-owner of the table in order to find the information about the [un-backfilled commits](#commit-backfills).</ins>\n<ins>The delta files in `_delta_log` directory are named using the next available version number, zero-padded to 20 digits.</ins>\n\nFor example:\n\n```\n./_delta_log/00000000000000000000.json\n```\n\n<ins>The delta files in the `_delta_log/_commits` directory have a UUID embedded into them and follow the pattern `<version>.<uuid>.json`, where the version corresponds to the next attempt version zero-padded to 20 digits.</ins>\n\nFor example:\n\n```\n./_delta_log/_commits/00000000000000000000.3a0d65cd-4056-49b8-937b-95f9e3ee90e5.json\n./_delta_log/_commits/00000000000000000001.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\n./_delta_log/_commits/00000000000000000001.016ae953-37a9-438e-8683-9a9a4a79a395.json\n./_delta_log/_commits/00000000000000000002.3ae45b72-24e1-865a-a211-34987ae02f2a.json\n```\n\nThe `_delta_log/_commits` directory may contain uncommitted delta files. The [commit-owner](#commit-owner) is the source of truth about which of those delta\nfiles map to committed versions. Refer to [managed commits](#managed-commits) for more details.\n\n~~Delta files use new-line delimited JSON format, where every action is stored as a single line JSON document.\nA delta file, `n.json`, contains an atomic set of [_actions_](#Actions) that should be applied to the previous table state, `n-1.json`, in order to the construct `n`th snapshot of the table.\nAn action changes one aspect of the table's state, for example, adding or removing a file.~~\n\n<ins>Delta files use newline-delimited JSON format, where every action is stored as a single line JSON document.\nA delta file, corresponding to version `n`, contains an atomic set of [_actions_](#Actions) that should be applied to the previous table state, corresponding to `n-1`, in order to construct the `n`th snapshot of the table.\nAn action changes one aspect of the table's state, for example, adding or removing a file.</ins>\n\n### Metadata Cleanup\n\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#metadata-cleanup)***\n\n2. Identify the newest checkpoint that is not newer than the `cutOffCommit`. A checkpoint at the `cutOffCommit` is ideal, but an older one will do. Lets call it `cutOffCheckpoint`.\n   We need to preserve the `cutOffCheckpoint` and all commits after it, because we need them to enable\n   time travel for commits between `cutOffCheckpoint` and the next available checkpoint.\n    - <ins>If no `cutOffCheckpoint` can be found, do not proceed with metadata cleanup as there is nothing to cleanup.</ins>\n3. Delete all [delta log entries](#delta-log-entries) and [checkpoint files](#checkpoints) before the\n   `cutOffCheckpoint` checkpoint. Also delete all the [log compaction files](#log-compaction-files) having\n   startVersion <= `cutOffCheckpoint`'s version.\n    - <ins>Also delete all the [un-backfilled commit files](#commit-files) having version <= `cutOffCheckpoint`'s version from the `_delta_log/_commits` directory.</ins>\n\n### Checkpoints\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#checkpoints)***\n\nCheckpoints are also stored in the `_delta_log` directory, and can be created at any time, for any committed version of the table.\nFor performance reasons, readers should prefer to use the newest complete checkpoint possible.\n\n<ins>**Note:** If [managed commits](#managed-commits) table feature is enabled on the table, a checkpoint can be created only for commit\nversions which are backfilled. Refer to [maintenance operations on managed-commit tables](#maintenance-operations-on-managed-commit-tables) section\nfor more details</ins>\n\n### Log Compaction Files\n> ***Change to [existing section](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#log-compaction-files)***\n\n<ins>**Note:** If [managed commits](#managed-commits) table feature is enabled on the table, a log compaction file for commit\nrange `[x, y]` i.e. `x.y.compacted.json` can be created only when commit `y` is already backfilled i.e. `_delta_log/<y>.json` must exist.\nRefer to [maintenance operations on managed-commit tables](#maintenance-operations-on-managed-commit-tables) section\nfor more details.</ins>\n\n> ***The next set of sections will be added to the existing spec just before [Iceberg Compatibility V1](https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v1) section***\n\n# Managed Commits\n\nWith this feature enabled:\n- The file system remains the source of truth for the _content_ of a (proposed) commit.\n- The [commit-owner](#commit-owner) becomes the source of truth for whether a given commit succeeded.\n\nThe following is a high-level overview of how commits work in a table with managed-commits enabled:\n\n1. Delta client passes the actions that need to be committed to the [commit-owner](#commit-owner).\n2. The [commit-owner](#commit-owner) abstracts the commit process and defines the atomicity protocol for\n   commits to that table. It writes the actions in a [delta file](#delta-log-entries) and atomically makes\n   this file part of the table. Refer to [commit protocol](#commit protocol) section for details around how\n   the commit-owner performs commits.\n3. In case of no conflict, the [commit-owner](#commit-owner) responds with success to the delta client.\n4. Delta clients could contact the commit-owner to get the information about the table's most recent commits.\n\nEssentially the [managed-commits](#managed-commits) table feature defines the overall [commit protocol](#commit-protocol) (e.g. atomicity requirements, backfills, etc), and the\ncommit-owner is responsible to implement that protocol.\n\n## Commit Owner\n\nA commit-owner is an external entity which manages the commits on a delta table. It could be backed by a database, a file system, or any other persistence mechanism. Each commit-owner has its own spec around how Delta clients should contact them, and how they perform a commit.\n\n## Commit Files\n\nA commit file is a [delta file](#delta-log-entries) that contains the actions which are committed / need to be committed.\n\nThere are two types of commit files:\n1. **Un-backfilled commit files**: These reside in the `_delta_log/_commits` directory.\n    - The filename must follow the pattern: `<version>.<uuid>.json`. Here the `uuid` is a random UUID that is generated for each commit and `version` is the version `v` which is being committed, zero-padded to 20 digits.\n    - Mere existence of these files does not mean that the file is a _valid_ commit. It might correspond to a failed or in-progress commit.\n      The commit-owner is the source of truth around which un-backfilled commits are valid.\n    - The commit-owner must track these files until they are backfilled to the `_delta_log` directory.\n\n2. **Backfilled commit files**: These reside in the `_delta_log` directory.\n    - The filename must follow the pattern: `<version>.json`. Here the `version` is the version `v` which is being committed, zero-padded to 20 digits.\n    - The existence of a `<version>.json` file proves that the corresponding version `v` is committed, even for managed-commit tables. Filesystem based Delta clients can use filesystem listing operations to directly discover such commits.\n\nWithout [managed-commits](#managed-commits), a delta client must always write commit files directly to the `_delta_log` directory, relying on filesystem atomicity\nto prevent lost writes when multiple writers attempt to commit the same version at the same time.\n\nWith [managed-commits](#managed-commits), the delta client asks the [commit-owner](#commit-owner) to commit the version `v` and the commit-owner\ndecides which type of commit file to write, based on the [managed commit protocol](#commit-protocol).\n\n## Commit Owner API\n\nWhen managed commits are enabled, a `commit-owner` performs commits to the table on behalf of the Delta client. A commit-owner always has a client-side component (which the Delta client interacts with directly). It may also\ninvolve a server-side component (which the client-side component would be responsible to communicate with). The Delta client is responsible to define the client-side API that commit-owners should target, and commit-owners\nare responsible to define the commit atomicity and backfill protocols which the commit-owner client should implement.\n\nAt a high level, the `commit-owner` needs to provide:\n- API to atomically commit a version `x` with given set of `actions`. This is explained in detail in the [commit protocol](#commit-protocol) section.\n- API to retrieve information about the recent commits and the latest ratified version on the table. This is explained in detail in the [getting un-backfilled commits from commit-owner](#getting-un-backfilled-commits-from-commit-owner) section.\n\n### Commit Protocol\n\nWhen a `commit-owner` receives a request to commit version `v`, it must first verify that the previous version `v-1` already exists, and that version `v` does not yet exist. It then has following choices to publish the commit:\n1. Write the actions to an 'un-backfilled' [commit file](#commit-files) in the `_delta_log/_commits` directory, and **atomically** record that the new file now corresponds to version `v`.\n2. Atomically write a backfilled [commit file](#commit-files) in the `_delta_log` directory. Note that the commit will be considered to have succeeded as soon as the file becomes visible to\n   other clients in the filesystem, regardless of when or whether the originating client receives a response.\n    - A commit-owner must not write a backfilled commit until the previous commit has been backfilled.\n\nThe commit-owner must track the un-backfilled commits until they are [backfilled](#commit-backfills).\n\n### Getting Un-backfilled Commits from Commit Owner\n\nEven after a commit succeeds, Delta clients can only discover the commit through filesystem operations if the commit is [backfilled](#backfills). If the commit is not backfilled, then delta implementations\nhave no way to determine which file in `_delta_log/_commits` directory corresponds to the actual commit `v`.\n\nThe commit-owner is responsible to implement an API (defined by the Delta client) that Delta clients can use to retrieve information about un-backfilled commits maintained\nby the commit-owner. The API must also return the latest version of the table ratified by the commit-owner (if any).\nProviding the latest ratified table version helps address potential race conditions between listing commits and contacting the commit-owner.\nFor example, if a client performs a listing before a recently ratified commit is backfilled, and then contacts the commit-owner after the backfill completes,\nthe commit-owner may return an empty list of un-backfilled commits. Without knowing the latest ratified version, the client might incorrectly assume their listing was complete\nand read a stale snapshot.\n\nDelta clients who are unaware of the commit-owner (or unwilling to talk to it), may not see recent un-backfilled commits and thus may encounter stale reads.\n\n\n## Sample Commit Owner API\n\nThe following is an example of a possible commit-owner API which some Java-based Delta client might require commit-owner implementations to target:\n\n```java\n\ninterface CommitStore {\n    /**\n     * Commits the given set of `actions` to the given commit `version`.\n     *\n     * @param version The version we want to commit.\n     * @param actions Actions that need to be committed.\n     *\n     * @return CommitResponse which has details around the new committed delta file.\n     */\n    def commit(\n        version: Long,\n        actions: Iterator[String]): CommitResponse\n\n    /**\n     * API to get the un-backfilled commits for the table represented by the given `tablePath` where\n     * `startVersion` <= version <= endVersion.\n     * If endVersion is -1, then it means that we want to get all the commits starting from `startVersion`\n     * till the latest version tracked by commit-owner.\n     * The returned commits are contiguous and in ascending version order.\n     * Note that the first version returned by this API may not be equal to the `startVersion`. This\n     * happens when few versions starting from `startVersion` are already backfilled and so\n     * CommitStore may have stopped tracking them.\n     * The returned latestTableVersion is the maximum commit version ratified by the Commit-Owner.\n     * Note that returning latestTableVersion as -1 is acceptable only if the commit-owner never\n     * ratified any version i.e. it never accepted any un-backfilled commit.\n     *\n     * @return GetCommitsResponse which contains a list of `Commit`s and the latestTableVersion\n     *         tracked by the commit-owner.\n     */\n    def getCommits(\n        startVersion: Long,\n        endVersion: Long): GetCommitsResponse\n\n    /**\n     * API to ask the commit-owner to backfill all commits <= given `version`.\n     */\n    def backfillToVersion(version: Long): Unit\n}\n```\n\n## Commit Backfills\nBackfilling is the process of copying the un-backfilled commits i.e. `_delta_log/_commits/<version>.<uuid>.json` to `_delta_log/<version>.json`.\nWith the help of backfilling, the [delta files](#delta-log-entries) are visible even to the filesystem based Delta clients that do not\nunderstand `managed-commits`. Backfill also allows the commit-owner to reduce the number of commits it must track internally.\n\nBackfill must be sequential. In other words, a commit-owner must ensure that backfill of commit `v-1` is complete before initiating backfill of commit `v`.\n\n`commit-owner`s are encouraged to backfill the commits frequently. This has several advantages:\n1. Filesystem-based Delta implementations may only understand backfilled commits, and frequent backfill allows them to access the most recent table snapshots.\n2. Frequent backfilling minimizes the impact to readers in case the `commit-owner` is unavailable or loses state.\n3. Some maintenance operations (such as checkpoints, log compaction, and metadata cleanup) can be performed only on the backfilled part of the table. Refer to the [Maintenance operations on managed-commit tables](#maintenance-operations-on-managed-commit-tables) section for more details.\n\nThe commit-owner also needs to expose an API to backfill the commits. This will allow clients to ask the commit-owner to backfill the commits if needed in order to do some maintenance operations.\n\nSince commit backfills may happen at a later point in time, so the `file modification timestamp` of the backfilled file might be very different than the time of actual commit. For this reason, the `managed-commit` feature depends on another writer feature called [in-commit-timestamps](#TODO-Put-Relevant-Link) to make the commit timestamps more reliable. Refer to [Writer Requirements for Managed Commits](#writer-requirements-for-managed-commits) section for more details.\n\n## Converting an existing filesystem based table to managed-commit table\nIn order for a commit-owner to successfully take over an existing filesystem-based Delta table, the following invariants must hold:\n- The commit-owner must agree to take ownership of the table, by accepting a proposed commit that would install it. This essentially follows the normal commit protocol, except…\n- The commit-owner and client must both recognize that the ownership change only officially takes effect when the ownership-change is successfully backfilled. Unlike the backfill of a normal commit, this ownership-change backfill must\n  be atomic because it is also a filesystem-based commit that potentially races with other filesystem-based commit attempts.\n\nAssuming the client follows the commit-owner’s protocol for ownership changes, the commit-owner MUST NOT refuse ownership after the backfill succeeds. Otherwise, the table would become permanently unusable, because the advertised commit-owner refuses\nto ratify the very commits that would repair the table by removing that commit-owner.\n\nThus, the commit-owner and client effectively perform a two-phase commit, where the commit-owner persists its commitment to own the table, and the actual commit point is the PUT-if-absent.\nNotifying the commit-owner that backfill has completed becomes a post-commit cleanup operation. If the put-if-absent fails (because somebody else gets there first), the commit-owner forgets\nabout the proposed ownership change.\n\nOnce the backfill succeeds, clients will start contacting the commit-owner for any further commits. Meanwhile, any clients who were already attempting filesystem-based commits will encounter\na physical conflict, see the protocol change, and either abort the commit or route it to the new owner.\n\n## Creating a new managed-commit table\n\nConceptually, creating a new managed-commit table is very similar to proposing an ownership change of an existing filesystem-based table that happens to not yet contain any commits. This means that, until commit 0\nhas been backfilled, there is a risk of multiple clients racing to create the same table with different commit-owners (or to create a filesystem-based table).\n\nTo avoid such races, Commit-owners are encouraged to use a put-if-absent API (if available) to write the backfilled commit directly (i.e. `_delta_log/00000000000000000000.json`).\nIf such put-if-absent is not available, then it is the responsibility of commit-owners to take whatever measures they deem appropriate to avoid or respond to such races.\n\n## Converting a managed-commit table to filesystem table\n\nIn order to convert a managed-commit table to a filesystem-based table, the Delta client needs to initiate a commit which tries to remove the commit-owner information\nfrom [change-metadata](#change-metadata) and also removes the table feature from the [protocol](#protocol-evolution) action. The commit-owner is not required to give\nup ownership, and may reject the request. If it chooses to honor such a request, it must:\n\n1. Ensure that all prior commit files are backfilled.\n2. Not accept any new commits on the table.\n3. Write the commit which removes the ownership.\n    - Either the commit-owner writes the backfilled commit file directly.\n    - Or it writes an unbackfilled commit and ensures that it is backfilled reliably. Until the backfill is done, table will be in unusable state:\n        - the filesystem based delta clients won't be able to write to such table as they still believe that table has managed-commit enabled.\n        - the managed-commit aware delta clients won't be able to write to such table as the commit-owner won't accept any new\n          commits. In such a scenario, they could backfill required commit themselves (preferably using PUT-if-absent) to unblock themselves.\n\n## Reading managed-commit tables\n\nWith `managed-commits` enabled, a table could have some part of table already backfilled and some part of the table yet-to-be-backfilled.\nThe precise information about what are the valid un-backfilled commits is maintained by the commit-owner.\n\nE.g.\n```\n_delta_log/00000000000000000000.json\n_delta_log/00000000000000000001.json\n_delta_log/00000000000000000002.json\n_delta_log/00000000000000000002.checkpoint.parquet\n_delta_log/00000000000000000003.json\n_delta_log/00000000000000000003.00000000000000000005.compacted.json\n_delta_log/00000000000000000004.json\n_delta_log/00000000000000000005.json\n_delta_log/00000000000000000006.json\n_delta_log/00000000000000000007.json\n_delta_log/_commits/00000000000000000006.3a0d65cd-4056-49b8-937b-95f9e3ee90e5.json\n_delta_log/_commits/00000000000000000007.016ae953-37a9-438e-8683-9a9a4a79a395.json\n_delta_log/_commits/00000000000000000008.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\n_delta_log/_commits/00000000000000000008.b91807ba-fe18-488c-a15e-c4807dbd2174.json\n_delta_log/_commits/00000000000000000009.41bf693a-f5b9-4478-9434-af7475d5a9f0.json\n_delta_log/_commits/00000000000000000010.0f707846-cd18-4e01-b40e-84ee0ae987b0.json\n_delta_log/_commits/00000000000000000010.7a980438-cb67-4b89-82d2-86f73239b6d6.json\n```\n\nSuppose the commit-owner is tracking:\n```\n{\n  6  -> \"00000000000000000006.3a0d65cd-4056-49b8-937b-95f9e3ee90e5.json\",\n  7  -> \"00000000000000000007.016ae953-37a9-438e-8683-9a9a4a79a395.json\",\n  8  -> \"00000000000000000008.7d17ac10-5cc3-401b-bd1a-9c82dd2ea032.json\",\n  9  -> \"00000000000000000009.41bf693a-f5b9-4478-9434-af7475d5a9f0.json\"\n}\n```\n\nDelta clients have two choices to read such tables:\n1. Any Delta client can read such table by listing the `_delta_log` directory and reading the delta/checkpoint/log-compaction files.\n   Without contacting the commit owner, they cannot access recent un-backfilled commits in the `_delta_log/_commits` directory, and may construct a stale snapshot.\n    - In the above example, such delta implementation will see version 7 as the latest snapshot.\n2. A client can guarantee freshness by additionally requesting the set of recent un-backfilled commits from the commit-owner.\n    - In the above example, a delta implementation could get information about versions 0 through 7 from `_delta_log` directory and get information about un-backfilled commits (v8, v9) from the commit-owner.\n\n## Maintenance operations on managed-commit tables\n\n[Checkpoints](#checkpoints-1) and [log compaction files](#log-compaction-files) can only be created for commits in the `_delta_log` directory. In other words, in order to\ncheckpoint version `v` or produce a compacted log file for commit range x <= v <= y, `_delta_log/<v>.json` must exist. Otherwise, filesystem-based readers who encountered\nthe seemingly-extra files might think the table metadata was corrupted.\n\n## Managed Commit Enablement\n\nThe managed-commit feature is supported and active when:\n- The table must be on Writer Version 7.\n- The table has a `protocol` action with `writerFeatures` containing the feature `managedCommit`.\n- The table has a metadata property `delta.managedCommit.commitOwner` in the [change-metadata](#change-metadata)'s configuration.\n- The table may have a metadata property `delta.managedCommit.commitOwnerConf` in the [change-metadata](#change-metadata)'s configuration. The value of this property is a json-coded string-to-string map.\n    - A commit-owner can store additional information (e.g. configuration information such as service endpoints) in this field, for use by the commit-owner client (it is opaque to the Delta client).\n    - This field should never include secrets such as auth tokens or credentials, because any reader with access to the table's storage location can see them.\n\nNote that a table is in invalid state if the change-metadata contains the `delta.managedCommit.commitOwner` property but the table does not have the `managedCommit` feature in the `protocol` action (or vice versa).\n\nE.g.\n```json\n{\n   \"metaData\":{\n      \"id\":\"af23c9d7-fff1-4a5a-a2c8-55c59bd782aa\",\n      \"format\":{\"provider\":\"parquet\",\"options\":{}},\n      \"schemaString\":\"...\",\n      \"partitionColumns\":[],\n      \"configuration\":{\n         \"appendOnly\": \"true\",\n         \"delta.managedCommit.commitOwner\": \"commit-owner-1\",\n         \"delta.managedCommit.commitOwnerConf\":\n             \"{\\\"endpoint\\\":\\\"http://sample-url.com/commit\\\", \\\"authenticationMode\\\":\\\"oauth2\\\"}\"\n      }\n   }\n}\n```\n\n## Writer Requirements for Managed Commits\n\nWhen supported and active:\n- The `inCommitTimestamp` table feature must also be supported and active.\n- Writer must follow the commit-owner's [commit protocol](#commit-protocol) and must not perform filesystem-based commits.\n- Writer must only create checkpoints or log compaction files for commits in the `_delta_log` directory.\n- Metadata cleanup must always preserve the newest k >= 1 backfilled commits.\n\n## Reader Requirements for Managed Commits\nManaged commits is a writer feature. So it doesn't put any restrictions on the reader.\n\n- Filesystem-based delta readers which do not understand [managed commits](#managed-commits) may only\n  be able to read the backfilled commits. They may see a stale snapshot of the table if the recent commits are not backfilled.\n\n- The [managed commits](#managed-commits) aware delta readers could additionally contact the commit-owner to\n  get the information about the recent un-backfilled commits. This allows them to get the most recent snapshot of the table.\n"
  },
  {
    "path": "protocol_rfcs/template.md",
    "content": "# Table feature name / meaningful name\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/XXXX**\n<!-- Remove this: Replace XXXX with the actual github issue number -->\n\n\n<!--  Give a general description / context of the protocol change, and remove this comment. -->\n\n--------\n\n<!-- Remove this: Add your proposed protocol.md modifications here, and remove this comment. -->\n\n"
  },
  {
    "path": "protocol_rfcs/variant-shredding.md",
    "content": "# Variant Shredding\n**Associated Github issue for discussions: https://github.com/delta-io/delta/issues/4032**\n\nThis protocol change adds support for Variant shredding for the Variant data type.\nShredding allows Variant data to be be more efficiently stored and queried.\n\n--------\n\n> ***New Section after the `Variant Data Type` section***\n\n# Variant Shredding\n\nThis feature enables support for shredding of the Variant data type, to store and query Variant data more efficiently.\nShredding a Variant value is taking paths from the Variant value, and storing them as a typed column in the file.\nThe shredding does not duplicate data, so if a value is stored in the typed column, it is removed from the Variant binary.\nStoring Variant values as typed columns is faster to access, and enables data skipping with statistics.\n\nThe `variantShredding` feature depends on the `variantType` feature.\n\nTo support this feature:\n- The table must be on Reader Version 3 and Writer Version 7\n- The feature `variantType` must exist in the table `protocol`'s `readerFeatures` and `writerFeatures`.\n- The feature `variantShredding` must exist in the table `protocol`'s `readerFeatures` and `writerFeatures`.\n\n## Shredded Variant data in Parquet\n\nShredded Variant data is stored according to the [Parquet Variant Shredding specification](https://github.com/apache/parquet-format/blob/master/VariantShredding.md)\nThe shredded Variant data written to parquet files is written as a single Parquet struct, with the following fields:\n\nStruct field name | Parquet primitive type | Description\n-|-|-\nmetadata | binary | (required) The binary-encoded Variant metadata, as described in [Parquet Variant binary encoding](https://github.com/apache/parquet-format/blob/master/VariantEncoding.md)\nvalue | binary | (optional) The binary-encoded Variant value, as described in [Parquet Variant binary encoding](https://github.com/apache/parquet-format/blob/master/VariantEncoding.md)\ntyped_value | * | (optional) This can be any Parquet type, representing the data stored in the Variant. Details of the shredding scheme is found in the [Variant Shredding specification](https://github.com/apache/parquet-format/blob/master/VariantShredding.md)\n\n## Writer Requirements for Variant Shredding\n\nWhen Variant Shredding is supported (`writerFeatures` field of a table's `protocol` action contains `variantShredding`), writers:\n- must respect the `delta.enableVariantShredding` table property configuration. If `delta.enableVariantShredding=false`, a column of type `variant` must not be written as a shredded Variant, but as an unshredded Variant. If `delta.enableVariantShredding=true`, the writer can choose to shred a Variant column according to the [Parquet Variant Shredding specification](https://github.com/apache/parquet-format/blob/master/VariantShredding.md)\n\n## Reader Requirements for Variant Shredding\n\nWhen Variant type is supported (`readerFeatures` field of a table's `protocol` action contains `variantShredding`), readers:\n- must recognize and tolerate a `variant` data type in a Delta schema\n- must recognize and correctly process a parquet schema that is either unshredded (only `metadata` and `value` struct fields) or shredded (`metadata`, `value`, and `typed_value` struct fields) when reading a Variant data type from file.\n\n> ***Update the `Per-file Statistics` section***\n\n> After the description and examples starting from: `Per-column statistics record information for each column in the file and they are encoded, mirroring the schema of the actual data. For example, given the following data schema:`\n\n### Statistics for Variant Columns\n\n- The `nullCount` stat for a Variant column is a LONG representing the nullcount for the Variant column itself (nullcount stats are not captured for individual paths within the Variant).\n- The `minValues` and `maxValues` stats for a Variant column are Variant objects, where the object keys are [normalized JSON path expressions](https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths), and the object values are the primitive Variant values representing the lower and upper bound for that field.\n- In JSON, the `minValues` and `maxValues` stats for a Variant column are [binary-encoded](https://github.com/apache/parquet-format/blob/master/VariantEncoding.md) Variant values, concatenating the `metadata` and `value`, and serialized to strings using [z85](https://rfc.zeromq.org/spec/32/) encoding (see example below).\n- In Parquet, the `minValues` and `maxValues` stats for a Variant column are Parquet Variant columns, following the Parquet Variant [encoding](https://github.com/apache/parquet-format/blob/master/VariantEncoding.md) and [shredding](https://github.com/apache/parquet-format/blob/master/VariantShredding.md) specifications.\n- In Parquet, the Variant `minValues` and `maxValues` stats are allowed to be shredded, but it is not required.\n- Each path in the Variant `minValues` (`maxValues`) value is the independently computed min (max) stat for the corresponding path in the file's Variant data, so e.g. `minValues.v:a` and `minValues.v:b` could come from different rows in the file.\n- Min/max stats may only be written for primitive (leaf) values, packed into a Variant representation.\n- Min/max stats may only be written for a path if that path has the same data type in every row of the data file.\n- The paths and types inside `minValues` and `maxValues` must be the same within any one file, but can vary from file to file.\n- Subject to the above constraints, the writer of a given file determines which Variant leaf paths (if any) to emit statistics for.\n\nFor a table with a single Variant column (`varCol: variant`) in its data schema, example statistics in JSON would look like:\n\n```\n\"stats\": {\n  \"nullCount\": {\n    \"varCol\": 2\n  }\n  \"minValues\": {\n    \"varCol\": \"0S&u501fk+ze0(tB98CpzF6vU0rJl95HpNdvjbtatpi(cu0wW^cTu\"\n  },\n  \"maxValues\": {\n    \"varCol\": \"0S&u500&]LC42A9vqZe}wb#-i1}-a+cT!xdbWhT9cTx}7v<+K\"\n  }\n}\n```\nThe corresponding human-readable form is:\n```\n\"stats\": {\n  \"nullCount\": {\n    \"varCol\": 2\n  }\n  \"minValues\": {\n    \"varCol\": {\n      \"$['a']\" : \"min-string\",\n      \"$['b']['c']\" : 1\n    }\n  },\n  \"maxValues\": {\n    \"varCol\": {\n      \"$['a']\" : \"variant\",\n      \"$['b']['c']\" : 100\n    }\n  }\n}\n```\n"
  },
  {
    "path": "python/README.md",
    "content": "# Delta Lake\n\n[Delta Lake](https://delta.io) is an open source storage layer that brings reliability to data lakes. Delta Lake provides ACID transactions, scalable metadata handling, and unifies streaming and batch data processing. Delta Lake runs on top of your existing data lake and is fully compatible with Apache Spark APIs.\n\nThis PyPi package contains the Python APIs for using Delta Lake with Apache Spark.\n\n## Installation and usage\n\n1. Install using `pip install delta-spark`\n2. To use the Delta Lake with Apache Spark, you have to set additional configurations when creating the SparkSession. See the online [project web page](https://docs.delta.io/latest/delta-intro.html) for details.\n\n## Documentation\n\nThis README file only contains basic information related to pip installed Delta Lake. You can find the full documentation on the [project web page](https://docs.delta.io/latest/delta-intro.html)\n"
  },
  {
    "path": "python/delta/__init__.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom delta.tables import DeltaTable\nfrom delta.pip_utils import configure_spark_with_delta_pip\nfrom delta.version import __version__\n\n__all__ = ['DeltaTable', 'configure_spark_with_delta_pip', '__version__']\n"
  },
  {
    "path": "python/delta/_typing.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Dict, Optional, Union\nfrom pyspark.sql.column import Column\n\nExpressionOrColumn = Union[str, Column]\nOptionalExpressionOrColumn = Optional[ExpressionOrColumn]\nColumnMapping = Dict[str, ExpressionOrColumn]\nOptionalColumnMapping = Optional[ColumnMapping]\n"
  },
  {
    "path": "python/delta/connect/__init__.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom delta.connect.tables import DeltaTable\n\n__all__ = ['DeltaTable']\n"
  },
  {
    "path": "python/delta/connect/_typing.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Dict, Optional, Union\nfrom pyspark.sql.connect.column import Column\n\nExpressionOrColumn = Union[str, Column]\nOptionalExpressionOrColumn = Optional[ExpressionOrColumn]\nColumnMapping = Dict[str, ExpressionOrColumn]\nOptionalColumnMapping = Optional[ColumnMapping]\n"
  },
  {
    "path": "python/delta/connect/exceptions.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport json\nfrom typing import TYPE_CHECKING\n\nfrom pyspark.errors.exceptions.connect import SparkConnectException\n\nfrom delta.exceptions.base import (\n    DeltaConcurrentModificationException as BaseDeltaConcurrentModificationException,\n    ConcurrentWriteException as BaseConcurrentWriteException,\n    MetadataChangedException as BaseMetadataChangedException,\n    ProtocolChangedException as BaseProtocolChangedException,\n    ConcurrentAppendException as BaseConcurrentAppendException,\n    ConcurrentDeleteReadException as BaseConcurrentDeleteReadException,\n    ConcurrentDeleteDeleteException as BaseConcurrentDeleteDeleteException,\n    ConcurrentTransactionException as BaseConcurrentTransactionException,\n)\n\nif TYPE_CHECKING:\n    from google.rpc.error_details_pb2 import ErrorInfo\n\n\nclass DeltaConcurrentModificationException(SparkConnectException, BaseDeltaConcurrentModificationException):\n    \"\"\"\n    The basic class for all Delta commit conflict exceptions.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentWriteException(SparkConnectException, BaseConcurrentWriteException):\n    \"\"\"\n    Thrown when a concurrent transaction has written data after the current transaction read the\n    table.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass MetadataChangedException(SparkConnectException, BaseMetadataChangedException):\n    \"\"\"\n    Thrown when the metadata of the Delta table has changed between the time of read\n    and the time of commit.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ProtocolChangedException(SparkConnectException, BaseProtocolChangedException):\n    \"\"\"\n    Thrown when the protocol version has changed between the time of read\n    and the time of commit.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentAppendException(SparkConnectException, BaseConcurrentAppendException):\n    \"\"\"\n    Thrown when files are added that would have been read by the current transaction.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentDeleteReadException(SparkConnectException, BaseConcurrentDeleteReadException):\n    \"\"\"\n    Thrown when the current transaction reads data that was deleted by a concurrent transaction.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentDeleteDeleteException(SparkConnectException, BaseConcurrentDeleteDeleteException):\n    \"\"\"\n    Thrown when the current transaction deletes data that was deleted by a concurrent transaction.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentTransactionException(SparkConnectException, BaseConcurrentTransactionException):\n    \"\"\"\n    Thrown when concurrent transaction both attempt to update the same idempotent transaction.\n\n    .. versionadded:: 4.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\ndef _convert_delta_exception(info: \"ErrorInfo\", message: str):\n    classes = []\n    if \"classes\" in info.metadata:\n        classes = json.loads(info.metadata[\"classes\"])\n\n    if \"io.delta.exceptions.ConcurrentWriteException\" in classes:\n        return ConcurrentWriteException(message)\n    if \"io.delta.exceptions.MetadataChangedException\" in classes:\n        return MetadataChangedException(message)\n    if \"io.delta.exceptions.ProtocolChangedException\" in classes:\n        return ProtocolChangedException(message)\n    if \"io.delta.exceptions.ConcurrentAppendException\" in classes:\n        return ConcurrentAppendException(message)\n    if \"io.delta.exceptions.ConcurrentDeleteReadException\" in classes:\n        return ConcurrentDeleteReadException(message)\n    if \"io.delta.exceptions.ConcurrentDeleteDeleteException\" in classes:\n        return ConcurrentDeleteDeleteException(message)\n    if \"io.delta.exceptions.ConcurrentTransactionException\" in classes:\n        return ConcurrentTransactionException(message)\n    if \"io.delta.exceptions.DeltaConcurrentModificationException\" in classes:\n        return DeltaConcurrentModificationException(message)\n    return None\n"
  },
  {
    "path": "python/delta/connect/plan.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import cast, Dict, List, Optional, Union\n\nimport delta.connect.proto as proto\n\nfrom pyspark.sql.connect.client import SparkConnectClient\nfrom pyspark.sql.connect.column import Column\nfrom pyspark.sql.connect.plan import LogicalPlan\nimport pyspark.sql.connect.proto as spark_proto\nfrom pyspark.sql.connect.types import pyspark_types_to_proto_types\nfrom pyspark.sql.types import StructType\n\n\nclass DeltaLogicalPlan(LogicalPlan):\n    def __init__(self, child: Optional[LogicalPlan]) -> None:\n        super().__init__(child)\n\n    def plan(self, session: SparkConnectClient) -> spark_proto.Relation:\n        plan = self._create_proto_relation()\n        plan.extension.Pack(self.to_delta_relation(session))\n        return plan\n\n    def to_delta_relation(self, session: SparkConnectClient) -> proto.DeltaRelation:\n        ...\n\n    def command(self, session: SparkConnectClient) -> spark_proto.Command:\n        command = spark_proto.Command()\n        command.extension.Pack(self.to_delta_command(session))\n        return command\n\n    def to_delta_command(self, session: SparkConnectClient) -> proto.DeltaCommand:\n        ...\n\n\nclass DeltaScan(DeltaLogicalPlan):\n    def __init__(self, table: proto.DeltaTable) -> None:\n        super().__init__(None)\n        self._table = table\n\n    def to_delta_relation(self, client: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.scan.table.CopyFrom(self._table)\n        return relation\n\n\nclass Generate(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        mode: str\n    ) -> None:\n        super().__init__(None)\n        self._mode = mode\n        self._table = table\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.generate.table.CopyFrom(self._table)\n        command.generate.mode = self._mode\n        return command\n\n\nclass DeleteFromTable(DeltaLogicalPlan):\n    def __init__(self, target: Optional[LogicalPlan], condition: Optional[Column]) -> None:\n        super().__init__(target)\n        self._target = cast(LogicalPlan, target)\n        self._condition = condition\n\n    def to_delta_relation(self, session: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.delete_from_table.target.CopyFrom(self._target.plan(session))\n        if self._condition is not None:\n            relation.delete_from_table.condition.CopyFrom(self._condition.to_plan(session))\n        return relation\n\n\nclass Assignment:\n    def __init__(self, field: Column, value: Column) -> None:\n        self._field = field\n        self._value = value\n\n    def to_proto(self, session: SparkConnectClient) -> proto.Assignment:\n        assignment = proto.Assignment()\n        assignment.field.CopyFrom(self._field.to_plan(session))\n        assignment.value.CopyFrom(self._value.to_plan(session))\n        return assignment\n\n\nclass UpdateTable(DeltaLogicalPlan):\n    def __init__(\n        self,\n        target: Optional[LogicalPlan],\n        condition: Optional[Column],\n        assignments: List[Assignment],\n    ) -> None:\n        super().__init__(target)\n        self._target = cast(LogicalPlan, target)\n        self._condition = condition\n        self._assignments = assignments\n\n    def to_delta_relation(self, session: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.update_table.target.CopyFrom(self._target.plan(session))\n        if self._condition is not None:\n            relation.update_table.condition.CopyFrom(self._condition.to_plan(session))\n        relation.update_table.assignments.extend(\n            [assignment.to_proto(session) for assignment in self._assignments]\n        )\n        return relation\n\n\nclass MergeAction(object):\n    def __init__(self, condition: Optional[Column]) -> None:\n        self._condition = condition\n\n    def to_proto(self, session: SparkConnectClient) -> proto.MergeIntoTable.Action:\n        action = proto.MergeIntoTable.Action()\n        if self._condition is not None:\n            action.condition.CopyFrom(self._condition.to_plan(session))\n        return action\n\n\nclass UpdateAction(MergeAction):\n    def __init__(\n        self,\n        condition: Optional[Column],\n        assignments: List[Assignment],\n    ) -> None:\n        super().__init__(condition)\n        self._assignments = assignments\n\n    def to_proto(self, session: SparkConnectClient) -> proto.MergeIntoTable.Action:\n        action = super().to_proto(session)\n        action.update_action.assignments.extend(\n            [assignment.to_proto(session) for assignment in self._assignments]\n        )\n        return action\n\n\nclass UpdateStarAction(MergeAction):\n    def __init__(self, condition: Optional[Column]) -> None:\n        super().__init__(condition)\n\n    def to_proto(self, session: SparkConnectClient) -> proto.MergeIntoTable.Action:\n        action = super().to_proto(session)\n        action.update_star_action.SetInParent()\n        return action\n\n\nclass DeleteAction(MergeAction):\n    def __init__(self, condition: Optional[Column]) -> None:\n        super().__init__(condition)\n\n    def to_proto(self, session: SparkConnectClient) -> proto.MergeIntoTable.Action:\n        action = super().to_proto(session)\n        action.delete_action.SetInParent()\n        return action\n\n\nclass InsertAction(MergeAction):\n    def __init__(\n        self,\n        condition: Optional[Column],\n        assignments: List[Assignment],\n    ) -> None:\n        super().__init__(condition)\n        self._assignments = assignments\n\n    def to_proto(self, session: SparkConnectClient) -> proto.MergeIntoTable.Action:\n        action = super().to_proto(session)\n        action.insert_action.assignments.extend(\n            [assignment.to_proto(session) for assignment in self._assignments]\n        )\n        return action\n\n\nclass InsertStarAction(MergeAction):\n    def __init__(self, condition: Optional[Column]) -> None:\n        super().__init__(condition)\n\n    def to_proto(self, session: SparkConnectClient) -> proto.MergeIntoTable.Action:\n        action = super().to_proto(session)\n        action.insert_star_action.SetInParent()\n        return action\n\n\nclass MergeIntoTable(DeltaLogicalPlan):\n    def __init__(\n        self,\n        target: Optional[LogicalPlan],\n        source: LogicalPlan,\n        condition: Column,\n        matched_actions: List[MergeAction],\n        not_matched_actions: List[MergeAction],\n        not_matched_by_source_actions: List[MergeAction],\n        with_schema_evolution: Optional[bool]\n    ) -> None:\n        super().__init__(target)\n        self._target = cast(LogicalPlan, target)\n        self._source = source\n        self._condition = condition\n        self._matched_actions = matched_actions\n        self._not_matched_actions = not_matched_actions\n        self._not_matched_by_source_actions = not_matched_by_source_actions\n        self._with_schema_evolution = with_schema_evolution or False\n\n    def to_delta_relation(self, session: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.merge_into_table.target.CopyFrom(self._target.plan(session))\n        relation.merge_into_table.source.CopyFrom(self._source.plan(session))\n        relation.merge_into_table.condition.CopyFrom(self._condition.to_plan(session))\n        relation.merge_into_table.matched_actions.extend(\n            [action.to_proto(session) for action in self._matched_actions]\n        )\n        relation.merge_into_table.not_matched_actions.extend(\n            [action.to_proto(session) for action in self._not_matched_actions]\n        )\n        relation.merge_into_table.not_matched_by_source_actions.extend(\n            [action.to_proto(session) for action in self._not_matched_by_source_actions]\n        )\n        relation.merge_into_table.with_schema_evolution = self._with_schema_evolution\n        return relation\n\n\nclass Vacuum(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        retentionHours: Optional[float]\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._retentionHours = retentionHours\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.vacuum_table.table.CopyFrom(self._table)\n        if self._retentionHours is not None:\n            command.vacuum_table.retention_hours = self._retentionHours\n        return command\n\n\nclass DescribeHistory(DeltaLogicalPlan):\n    def __init__(self, table: proto.DeltaTable) -> None:\n        super().__init__(None)\n        self._table = table\n\n    def to_delta_relation(self, session: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.describe_history.table.CopyFrom(self._table)\n        return relation\n\n\nclass DescribeDetail(DeltaLogicalPlan):\n    def __init__(self, table: proto.DeltaTable) -> None:\n        super().__init__(None)\n        self._table = table\n\n    def to_delta_relation(self, client: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.describe_detail.table.CopyFrom(self._table)\n        return relation\n\n\nclass ConvertToDelta(DeltaLogicalPlan):\n    def __init__(\n        self,\n        identifier: str,\n        partitionSchema: Optional[Union[str, StructType]]\n    ) -> None:\n        super().__init__(None)\n        self._identifier = identifier\n        self._partitionSchema = partitionSchema\n\n    def to_delta_relation(self, client: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.convert_to_delta.identifier = self._identifier\n        if self._partitionSchema is not None:\n            if isinstance(self._partitionSchema, str):\n                relation.convert_to_delta.partition_schema_string = self._partitionSchema\n            if isinstance(self._partitionSchema, StructType):\n                relation.convert_to_delta.partition_schema_struct.CopyFrom(\n                    pyspark_types_to_proto_types(self._partitionSchema)\n                )\n        return relation\n\n\nclass IsDeltaTable(DeltaLogicalPlan):\n    def __init__(self, path: str):\n        super().__init__(None)\n        self._path = path\n\n    def to_delta_relation(self, session: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.is_delta_table.path = self._path\n        return relation\n\n\nclass CreateDeltaTable(DeltaLogicalPlan):\n    def __init__(\n        self,\n        mode: proto.CreateDeltaTable.Mode,\n        tableName: Optional[str],\n        location: Optional[str],\n        comment: Optional[str],\n        columns: List[proto.CreateDeltaTable.Column],\n        partitioningColumns: List[str],\n        properties: Dict[str, str],\n        clusteringColumns: List[str]\n    ) -> None:\n        super().__init__(None)\n        self._mode = mode\n        self._tableName = tableName\n        self._location = location\n        self._comment = comment\n        self._columns = columns\n        self._partitioningColumns = partitioningColumns\n        self._clusteringColumns = clusteringColumns\n        self._properties = properties\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.create_delta_table.mode = self._mode\n        if self._tableName is not None:\n            command.create_delta_table.table_name = self._tableName\n        if self._location is not None:\n            command.create_delta_table.location = self._location\n        if self._comment is not None:\n            command.create_delta_table.comment = self._comment\n        command.create_delta_table.columns.extend(self._columns)\n        command.create_delta_table.partitioning_columns.extend(self._partitioningColumns)\n        command.create_delta_table.clustering_columns.extend(self._clusteringColumns)\n        for k, v in self._properties.items():\n            command.create_delta_table.properties[k] = v\n        return command\n\n\nclass UpgradeTableProtocol(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        readerVersion: int,\n        writerVersion: int\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._readerVersion = readerVersion\n        self._writerVersion = writerVersion\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.upgrade_table_protocol.table.CopyFrom(self._table)\n        command.upgrade_table_protocol.reader_version = self._readerVersion\n        command.upgrade_table_protocol.writer_version = self._writerVersion\n        return command\n\n\nclass AddFeatureSupport(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        featureName: str\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._featureName = featureName\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.add_feature_support.table.CopyFrom(self._table)\n        command.add_feature_support.feature_name = self._featureName\n        return command\n\n\nclass DropFeatureSupport(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        featureName: str,\n        truncateHistory: Optional[bool]\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._featureName = featureName\n        self._truncateHistory = truncateHistory\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.drop_feature_support.table.CopyFrom(self._table)\n        command.drop_feature_support.feature_name = self._featureName\n        if self._truncateHistory is not None:\n            command.drop_feature_support.truncate_history = self._truncateHistory\n        return command\n\n\nclass RestoreTable(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        version: Optional[int] = None,\n        timestamp: Optional[str] = None\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._version = version\n        self._timestamp = timestamp\n\n    def to_delta_relation(self, client: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.restore_table.table.CopyFrom(self._table)\n        if self._version is not None:\n            relation.restore_table.version = self._version\n        if self._timestamp is not None:\n            relation.restore_table.timestamp = self._timestamp\n        return relation\n\n\nclass OptimizeTable(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        partitionFilters: List[str],\n        zOrderCols: List[str]\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._partitionFilters = partitionFilters\n        self._zOrderCols = zOrderCols\n\n    def to_delta_relation(self, client: SparkConnectClient) -> proto.DeltaRelation:\n        relation = proto.DeltaRelation()\n        relation.optimize_table.table.CopyFrom(self._table)\n        relation.optimize_table.partition_filters.extend(self._partitionFilters)\n        relation.optimize_table.zorder_columns.extend(self._zOrderCols)\n        return relation\n\n\nclass CloneTable(DeltaLogicalPlan):\n    def __init__(\n        self,\n        table: proto.DeltaTable,\n        target: str,\n        isShallow: bool,\n        replace: bool,\n        properties: Optional[Dict[str, str]],\n        version: Optional[int] = None,\n        timestamp: Optional[str] = None,\n    ) -> None:\n        super().__init__(None)\n        self._table = table\n        self._target = target\n        self._isShallow = isShallow\n        self._replace = replace\n        self._properties = properties or {}\n        self._version = version\n        self._timestamp = timestamp\n\n    def to_delta_command(self, client: SparkConnectClient) -> proto.DeltaCommand:\n        command = proto.DeltaCommand()\n        command.clone_table.table.CopyFrom(self._table)\n        command.clone_table.target = self._target\n        command.clone_table.is_shallow = self._isShallow\n        command.clone_table.replace = self._replace\n        for k, v in self._properties.items():\n            command.clone_table.properties[k] = v\n        if self._version is not None:\n            command.clone_table.version = self._version\n        if self._timestamp is not None:\n            command.clone_table.timestamp = self._timestamp\n        return command\n"
  },
  {
    "path": "python/delta/connect/proto/__init__.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom delta.connect.proto.base_pb2 import *\nfrom delta.connect.proto.commands_pb2 import *\nfrom delta.connect.proto.relations_pb2 import *\n"
  },
  {
    "path": "python/delta/connect/proto/base_pb2.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: delta/connect/base.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(\n    b'\\n\\x18\\x64\\x65lta/connect/base.proto\\x12\\rdelta.connect\"\\xad\\x02\\n\\nDeltaTable\\x12\\x34\\n\\x04path\\x18\\x01 \\x01(\\x0b\\x32\\x1e.delta.connect.DeltaTable.PathH\\x00R\\x04path\\x12-\\n\\x12table_or_view_name\\x18\\x02 \\x01(\\tH\\x00R\\x0ftableOrViewName\\x1a\\xaa\\x01\\n\\x04Path\\x12\\x12\\n\\x04path\\x18\\x01 \\x01(\\tR\\x04path\\x12O\\n\\x0bhadoop_conf\\x18\\x02 \\x03(\\x0b\\x32..delta.connect.DeltaTable.Path.HadoopConfEntryR\\nhadoopConf\\x1a=\\n\\x0fHadoopConfEntry\\x12\\x10\\n\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x02\\x38\\x01\\x42\\r\\n\\x0b\\x61\\x63\\x63\\x65ss_typeB\\x1a\\n\\x16io.delta.connect.protoP\\x01\\x62\\x06proto3'\n)\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, \"delta.connect.proto.base_pb2\", globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n    DESCRIPTOR._options = None\n    DESCRIPTOR._serialized_options = b\"\\n\\026io.delta.connect.protoP\\001\"\n    _DELTATABLE_PATH_HADOOPCONFENTRY._options = None\n    _DELTATABLE_PATH_HADOOPCONFENTRY._serialized_options = b\"8\\001\"\n    _DELTATABLE._serialized_start = 44\n    _DELTATABLE._serialized_end = 345\n    _DELTATABLE_PATH._serialized_start = 160\n    _DELTATABLE_PATH._serialized_end = 330\n    _DELTATABLE_PATH_HADOOPCONFENTRY._serialized_start = 269\n    _DELTATABLE_PATH_HADOOPCONFENTRY._serialized_end = 330\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "python/delta/connect/proto/base_pb2.pyi",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\"\"\"\n@generated by mypy-protobuf.  Do not edit manually!\nisort:skip_file\n\nCopyright (2024) The Delta Lake Project Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\"\"\"\nimport builtins\nimport collections.abc\nimport google.protobuf.descriptor\nimport google.protobuf.internal.containers\nimport google.protobuf.message\nimport sys\n\nif sys.version_info >= (3, 8):\n    import typing as typing_extensions\nelse:\n    import typing_extensions\n\nDESCRIPTOR: google.protobuf.descriptor.FileDescriptor\n\nclass DeltaTable(google.protobuf.message.Message):\n    \"\"\"Information required to access a Delta table either by name or by path.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    class Path(google.protobuf.message.Message):\n        DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n        class HadoopConfEntry(google.protobuf.message.Message):\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            KEY_FIELD_NUMBER: builtins.int\n            VALUE_FIELD_NUMBER: builtins.int\n            key: builtins.str\n            value: builtins.str\n            def __init__(\n                self,\n                *,\n                key: builtins.str = ...,\n                value: builtins.str = ...,\n            ) -> None: ...\n            def ClearField(\n                self, field_name: typing_extensions.Literal[\"key\", b\"key\", \"value\", b\"value\"]\n            ) -> None: ...\n\n        PATH_FIELD_NUMBER: builtins.int\n        HADOOP_CONF_FIELD_NUMBER: builtins.int\n        path: builtins.str\n        \"\"\"(Required) Path to the Delta table.\"\"\"\n        @property\n        def hadoop_conf(\n            self,\n        ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]:\n            \"\"\"(Optional) Hadoop configuration used to access the file system.\"\"\"\n        def __init__(\n            self,\n            *,\n            path: builtins.str = ...,\n            hadoop_conf: collections.abc.Mapping[builtins.str, builtins.str] | None = ...,\n        ) -> None: ...\n        def ClearField(\n            self,\n            field_name: typing_extensions.Literal[\"hadoop_conf\", b\"hadoop_conf\", \"path\", b\"path\"],\n        ) -> None: ...\n\n    PATH_FIELD_NUMBER: builtins.int\n    TABLE_OR_VIEW_NAME_FIELD_NUMBER: builtins.int\n    @property\n    def path(self) -> global___DeltaTable.Path: ...\n    table_or_view_name: builtins.str\n    def __init__(\n        self,\n        *,\n        path: global___DeltaTable.Path | None = ...,\n        table_or_view_name: builtins.str = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"access_type\",\n            b\"access_type\",\n            \"path\",\n            b\"path\",\n            \"table_or_view_name\",\n            b\"table_or_view_name\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"access_type\",\n            b\"access_type\",\n            \"path\",\n            b\"path\",\n            \"table_or_view_name\",\n            b\"table_or_view_name\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"access_type\", b\"access_type\"]\n    ) -> typing_extensions.Literal[\"path\", \"table_or_view_name\"] | None: ...\n\nglobal___DeltaTable = DeltaTable\n"
  },
  {
    "path": "python/delta/connect/proto/commands_pb2.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: delta/connect/commands.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom delta.connect.proto import base_pb2 as delta_dot_connect_dot_base__pb2\nfrom pyspark.sql.connect.proto import types_pb2 as spark_dot_connect_dot_types__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(\n    b'\\n\\x1c\\x64\\x65lta/connect/commands.proto\\x12\\rdelta.connect\\x1a\\x18\\x64\\x65lta/connect/base.proto\\x1a\\x19spark/connect/types.proto\"\\xad\\x04\\n\\x0c\\x44\\x65ltaCommand\\x12<\\n\\x0b\\x63lone_table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.CloneTableH\\x00R\\ncloneTable\\x12?\\n\\x0cvacuum_table\\x18\\x02 \\x01(\\x0b\\x32\\x1a.delta.connect.VacuumTableH\\x00R\\x0bvacuumTable\\x12[\\n\\x16upgrade_table_protocol\\x18\\x03 \\x01(\\x0b\\x32#.delta.connect.UpgradeTableProtocolH\\x00R\\x14upgradeTableProtocol\\x12\\x35\\n\\x08generate\\x18\\x04 \\x01(\\x0b\\x32\\x17.delta.connect.GenerateH\\x00R\\x08generate\\x12O\\n\\x12\\x63reate_delta_table\\x18\\x05 \\x01(\\x0b\\x32\\x1f.delta.connect.CreateDeltaTableH\\x00R\\x10\\x63reateDeltaTable\\x12R\\n\\x13\\x61\\x64\\x64_feature_support\\x18\\x06 \\x01(\\x0b\\x32 .delta.connect.AddFeatureSupportH\\x00R\\x11\\x61\\x64\\x64\\x46\\x65\\x61tureSupport\\x12U\\n\\x14\\x64rop_feature_support\\x18\\x07 \\x01(\\x0b\\x32!.delta.connect.DropFeatureSupportH\\x00R\\x12\\x64ropFeatureSupportB\\x0e\\n\\x0c\\x63ommand_type\"\\xec\\x02\\n\\nCloneTable\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12\\x16\\n\\x06target\\x18\\x02 \\x01(\\tR\\x06target\\x12\\x1a\\n\\x07version\\x18\\x03 \\x01(\\x05H\\x00R\\x07version\\x12\\x1e\\n\\ttimestamp\\x18\\x04 \\x01(\\tH\\x00R\\ttimestamp\\x12\\x1d\\n\\nis_shallow\\x18\\x05 \\x01(\\x08R\\tisShallow\\x12\\x18\\n\\x07replace\\x18\\x06 \\x01(\\x08R\\x07replace\\x12I\\n\\nproperties\\x18\\x07 \\x03(\\x0b\\x32).delta.connect.CloneTable.PropertiesEntryR\\nproperties\\x1a=\\n\\x0fPropertiesEntry\\x12\\x10\\n\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x02\\x38\\x01\\x42\\x16\\n\\x14version_or_timestamp\"\\x80\\x01\\n\\x0bVacuumTable\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12,\\n\\x0fretention_hours\\x18\\x02 \\x01(\\x01H\\x00R\\x0eretentionHours\\x88\\x01\\x01\\x42\\x12\\n\\x10_retention_hours\"\\x95\\x01\\n\\x14UpgradeTableProtocol\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12%\\n\\x0ereader_version\\x18\\x02 \\x01(\\x05R\\rreaderVersion\\x12%\\n\\x0ewriter_version\\x18\\x03 \\x01(\\x05R\\rwriterVersion\"O\\n\\x08Generate\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12\\x12\\n\\x04mode\\x18\\x02 \\x01(\\tR\\x04mode\"\\xd0\\x08\\n\\x10\\x43reateDeltaTable\\x12\\x38\\n\\x04mode\\x18\\x01 \\x01(\\x0e\\x32$.delta.connect.CreateDeltaTable.ModeR\\x04mode\\x12\"\\n\\ntable_name\\x18\\x02 \\x01(\\tH\\x00R\\ttableName\\x88\\x01\\x01\\x12\\x1f\\n\\x08location\\x18\\x03 \\x01(\\tH\\x01R\\x08location\\x88\\x01\\x01\\x12\\x1d\\n\\x07\\x63omment\\x18\\x04 \\x01(\\tH\\x02R\\x07\\x63omment\\x88\\x01\\x01\\x12@\\n\\x07\\x63olumns\\x18\\x05 \\x03(\\x0b\\x32&.delta.connect.CreateDeltaTable.ColumnR\\x07\\x63olumns\\x12\\x31\\n\\x14partitioning_columns\\x18\\x06 \\x03(\\tR\\x13partitioningColumns\\x12O\\n\\nproperties\\x18\\x07 \\x03(\\x0b\\x32/.delta.connect.CreateDeltaTable.PropertiesEntryR\\nproperties\\x12-\\n\\x12\\x63lustering_columns\\x18\\x08 \\x03(\\tR\\x11\\x63lusteringColumns\\x1a\\xc5\\x03\\n\\x06\\x43olumn\\x12\\x12\\n\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x34\\n\\tdata_type\\x18\\x02 \\x01(\\x0b\\x32\\x17.spark.connect.DataTypeR\\x08\\x64\\x61taType\\x12\\x1a\\n\\x08nullable\\x18\\x03 \\x01(\\x08R\\x08nullable\\x12\\x33\\n\\x13generated_always_as\\x18\\x04 \\x01(\\tH\\x00R\\x11generatedAlwaysAs\\x88\\x01\\x01\\x12\\x1d\\n\\x07\\x63omment\\x18\\x05 \\x01(\\tH\\x01R\\x07\\x63omment\\x88\\x01\\x01\\x12]\\n\\ridentity_info\\x18\\x06 \\x01(\\x0b\\x32\\x33.delta.connect.CreateDeltaTable.Column.IdentityInfoH\\x02R\\x0cidentityInfo\\x88\\x01\\x01\\x1al\\n\\x0cIdentityInfo\\x12\\x14\\n\\x05start\\x18\\x01 \\x01(\\x03R\\x05start\\x12\\x12\\n\\x04step\\x18\\x02 \\x01(\\x03R\\x04step\\x12\\x32\\n\\x15\\x61llow_explicit_insert\\x18\\x03 \\x01(\\x08R\\x13\\x61llowExplicitInsertB\\x16\\n\\x14_generated_always_asB\\n\\n\\x08_commentB\\x10\\n\\x0e_identity_info\\x1a=\\n\\x0fPropertiesEntry\\x12\\x10\\n\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x02\\x38\\x01\"z\\n\\x04Mode\\x12\\x14\\n\\x10MODE_UNSPECIFIED\\x10\\x00\\x12\\x0f\\n\\x0bMODE_CREATE\\x10\\x01\\x12\\x1d\\n\\x19MODE_CREATE_IF_NOT_EXISTS\\x10\\x02\\x12\\x10\\n\\x0cMODE_REPLACE\\x10\\x03\\x12\\x1a\\n\\x16MODE_CREATE_OR_REPLACE\\x10\\x04\\x42\\r\\n\\x0b_table_nameB\\x0b\\n\\t_locationB\\n\\n\\x08_comment\"g\\n\\x11\\x41\\x64\\x64\\x46\\x65\\x61tureSupport\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12!\\n\\x0c\\x66\\x65\\x61ture_name\\x18\\x02 \\x01(\\tR\\x0b\\x66\\x65\\x61tureName\"\\xad\\x01\\n\\x12\\x44ropFeatureSupport\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12!\\n\\x0c\\x66\\x65\\x61ture_name\\x18\\x02 \\x01(\\tR\\x0b\\x66\\x65\\x61tureName\\x12.\\n\\x10truncate_history\\x18\\x03 \\x01(\\x08H\\x00R\\x0ftruncateHistory\\x88\\x01\\x01\\x42\\x13\\n\\x11_truncate_historyB\\x1a\\n\\x16io.delta.connect.protoP\\x01\\x62\\x06proto3'\n)\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, \"delta.connect.proto.commands_pb2\", globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n    DESCRIPTOR._options = None\n    DESCRIPTOR._serialized_options = b\"\\n\\026io.delta.connect.protoP\\001\"\n    _CLONETABLE_PROPERTIESENTRY._options = None\n    _CLONETABLE_PROPERTIESENTRY._serialized_options = b\"8\\001\"\n    _CREATEDELTATABLE_PROPERTIESENTRY._options = None\n    _CREATEDELTATABLE_PROPERTIESENTRY._serialized_options = b\"8\\001\"\n    _DELTACOMMAND._serialized_start = 101\n    _DELTACOMMAND._serialized_end = 658\n    _CLONETABLE._serialized_start = 661\n    _CLONETABLE._serialized_end = 1025\n    _CLONETABLE_PROPERTIESENTRY._serialized_start = 940\n    _CLONETABLE_PROPERTIESENTRY._serialized_end = 1001\n    _VACUUMTABLE._serialized_start = 1028\n    _VACUUMTABLE._serialized_end = 1156\n    _UPGRADETABLEPROTOCOL._serialized_start = 1159\n    _UPGRADETABLEPROTOCOL._serialized_end = 1308\n    _GENERATE._serialized_start = 1310\n    _GENERATE._serialized_end = 1389\n    _CREATEDELTATABLE._serialized_start = 1392\n    _CREATEDELTATABLE._serialized_end = 2496\n    _CREATEDELTATABLE_COLUMN._serialized_start = 1816\n    _CREATEDELTATABLE_COLUMN._serialized_end = 2269\n    _CREATEDELTATABLE_COLUMN_IDENTITYINFO._serialized_start = 2107\n    _CREATEDELTATABLE_COLUMN_IDENTITYINFO._serialized_end = 2215\n    _CREATEDELTATABLE_PROPERTIESENTRY._serialized_start = 940\n    _CREATEDELTATABLE_PROPERTIESENTRY._serialized_end = 1001\n    _CREATEDELTATABLE_MODE._serialized_start = 2334\n    _CREATEDELTATABLE_MODE._serialized_end = 2456\n    _ADDFEATURESUPPORT._serialized_start = 2498\n    _ADDFEATURESUPPORT._serialized_end = 2601\n    _DROPFEATURESUPPORT._serialized_start = 2604\n    _DROPFEATURESUPPORT._serialized_end = 2777\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "python/delta/connect/proto/commands_pb2.pyi",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\"\"\"\n@generated by mypy-protobuf.  Do not edit manually!\nisort:skip_file\n\nCopyright (2024) The Delta Lake Project Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\"\"\"\nimport builtins\nimport collections.abc\nimport delta.connect.proto.proto.base_pb2\nimport google.protobuf.descriptor\nimport google.protobuf.internal.containers\nimport google.protobuf.internal.enum_type_wrapper\nimport google.protobuf.message\nimport pyspark.sql.connect.proto.types_pb2\nimport sys\nimport typing\n\nif sys.version_info >= (3, 10):\n    import typing as typing_extensions\nelse:\n    import typing_extensions\n\nDESCRIPTOR: google.protobuf.descriptor.FileDescriptor\n\nclass DeltaCommand(google.protobuf.message.Message):\n    \"\"\"Message to hold all command extensions in Delta Connect.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    CLONE_TABLE_FIELD_NUMBER: builtins.int\n    VACUUM_TABLE_FIELD_NUMBER: builtins.int\n    UPGRADE_TABLE_PROTOCOL_FIELD_NUMBER: builtins.int\n    GENERATE_FIELD_NUMBER: builtins.int\n    CREATE_DELTA_TABLE_FIELD_NUMBER: builtins.int\n    ADD_FEATURE_SUPPORT_FIELD_NUMBER: builtins.int\n    DROP_FEATURE_SUPPORT_FIELD_NUMBER: builtins.int\n    @property\n    def clone_table(self) -> global___CloneTable: ...\n    @property\n    def vacuum_table(self) -> global___VacuumTable: ...\n    @property\n    def upgrade_table_protocol(self) -> global___UpgradeTableProtocol: ...\n    @property\n    def generate(self) -> global___Generate: ...\n    @property\n    def create_delta_table(self) -> global___CreateDeltaTable: ...\n    @property\n    def add_feature_support(self) -> global___AddFeatureSupport: ...\n    @property\n    def drop_feature_support(self) -> global___DropFeatureSupport: ...\n    def __init__(\n        self,\n        *,\n        clone_table: global___CloneTable | None = ...,\n        vacuum_table: global___VacuumTable | None = ...,\n        upgrade_table_protocol: global___UpgradeTableProtocol | None = ...,\n        generate: global___Generate | None = ...,\n        create_delta_table: global___CreateDeltaTable | None = ...,\n        add_feature_support: global___AddFeatureSupport | None = ...,\n        drop_feature_support: global___DropFeatureSupport | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"add_feature_support\",\n            b\"add_feature_support\",\n            \"clone_table\",\n            b\"clone_table\",\n            \"command_type\",\n            b\"command_type\",\n            \"create_delta_table\",\n            b\"create_delta_table\",\n            \"drop_feature_support\",\n            b\"drop_feature_support\",\n            \"generate\",\n            b\"generate\",\n            \"upgrade_table_protocol\",\n            b\"upgrade_table_protocol\",\n            \"vacuum_table\",\n            b\"vacuum_table\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"add_feature_support\",\n            b\"add_feature_support\",\n            \"clone_table\",\n            b\"clone_table\",\n            \"command_type\",\n            b\"command_type\",\n            \"create_delta_table\",\n            b\"create_delta_table\",\n            \"drop_feature_support\",\n            b\"drop_feature_support\",\n            \"generate\",\n            b\"generate\",\n            \"upgrade_table_protocol\",\n            b\"upgrade_table_protocol\",\n            \"vacuum_table\",\n            b\"vacuum_table\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"command_type\", b\"command_type\"]\n    ) -> (\n        typing_extensions.Literal[\n            \"clone_table\",\n            \"vacuum_table\",\n            \"upgrade_table_protocol\",\n            \"generate\",\n            \"create_delta_table\",\n            \"add_feature_support\",\n            \"drop_feature_support\",\n        ]\n        | None\n    ): ...\n\nglobal___DeltaCommand = DeltaCommand\n\nclass CloneTable(google.protobuf.message.Message):\n    \"\"\"Command that creates a copy of a DeltaTable in the specified target location.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    class PropertiesEntry(google.protobuf.message.Message):\n        DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n        KEY_FIELD_NUMBER: builtins.int\n        VALUE_FIELD_NUMBER: builtins.int\n        key: builtins.str\n        value: builtins.str\n        def __init__(\n            self,\n            *,\n            key: builtins.str = ...,\n            value: builtins.str = ...,\n        ) -> None: ...\n        def ClearField(\n            self, field_name: typing_extensions.Literal[\"key\", b\"key\", \"value\", b\"value\"]\n        ) -> None: ...\n\n    TABLE_FIELD_NUMBER: builtins.int\n    TARGET_FIELD_NUMBER: builtins.int\n    VERSION_FIELD_NUMBER: builtins.int\n    TIMESTAMP_FIELD_NUMBER: builtins.int\n    IS_SHALLOW_FIELD_NUMBER: builtins.int\n    REPLACE_FIELD_NUMBER: builtins.int\n    PROPERTIES_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The source Delta table to clone.\"\"\"\n    target: builtins.str\n    \"\"\"(Required) Path to the location where the cloned table should be stored.\"\"\"\n    version: builtins.int\n    \"\"\"Clones the source table as of the provided version.\"\"\"\n    timestamp: builtins.str\n    \"\"\"Clones the source table as of the provided timestamp.\"\"\"\n    is_shallow: builtins.bool\n    \"\"\"(Required) Performs a clone when true, this field should always be set to true.\"\"\"\n    replace: builtins.bool\n    \"\"\"(Required) Overwrites the target location when true.\"\"\"\n    @property\n    def properties(\n        self,\n    ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]:\n        \"\"\"(Required) User-defined table properties that override properties with the same key in the\n        source table.\n        \"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        target: builtins.str = ...,\n        version: builtins.int = ...,\n        timestamp: builtins.str = ...,\n        is_shallow: builtins.bool = ...,\n        replace: builtins.bool = ...,\n        properties: collections.abc.Mapping[builtins.str, builtins.str] | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"table\",\n            b\"table\",\n            \"timestamp\",\n            b\"timestamp\",\n            \"version\",\n            b\"version\",\n            \"version_or_timestamp\",\n            b\"version_or_timestamp\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"is_shallow\",\n            b\"is_shallow\",\n            \"properties\",\n            b\"properties\",\n            \"replace\",\n            b\"replace\",\n            \"table\",\n            b\"table\",\n            \"target\",\n            b\"target\",\n            \"timestamp\",\n            b\"timestamp\",\n            \"version\",\n            b\"version\",\n            \"version_or_timestamp\",\n            b\"version_or_timestamp\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self,\n        oneof_group: typing_extensions.Literal[\"version_or_timestamp\", b\"version_or_timestamp\"],\n    ) -> typing_extensions.Literal[\"version\", \"timestamp\"] | None: ...\n\nglobal___CloneTable = CloneTable\n\nclass VacuumTable(google.protobuf.message.Message):\n    \"\"\"Command that deletes files and directories in the table that are not needed by the table for\n    maintaining older versions up to the given retention threshold.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    RETENTION_HOURS_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to vacuum.\"\"\"\n    retention_hours: builtins.float\n    \"\"\"(Optional) Number of hours retain history for. If not specified, then the default retention\n    period will be used.\n    \"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        retention_hours: builtins.float | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_retention_hours\",\n            b\"_retention_hours\",\n            \"retention_hours\",\n            b\"retention_hours\",\n            \"table\",\n            b\"table\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_retention_hours\",\n            b\"_retention_hours\",\n            \"retention_hours\",\n            b\"retention_hours\",\n            \"table\",\n            b\"table\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"_retention_hours\", b\"_retention_hours\"]\n    ) -> typing_extensions.Literal[\"retention_hours\"] | None: ...\n\nglobal___VacuumTable = VacuumTable\n\nclass UpgradeTableProtocol(google.protobuf.message.Message):\n    \"\"\"Command to updates the protocol version of the table so that new features can be used.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    READER_VERSION_FIELD_NUMBER: builtins.int\n    WRITER_VERSION_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to upgrade the protocol of.\"\"\"\n    reader_version: builtins.int\n    \"\"\"(Required) The minimum required reader protocol version.\"\"\"\n    writer_version: builtins.int\n    \"\"\"(Required) The minimum required writer protocol version.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        reader_version: builtins.int = ...,\n        writer_version: builtins.int = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"reader_version\",\n            b\"reader_version\",\n            \"table\",\n            b\"table\",\n            \"writer_version\",\n            b\"writer_version\",\n        ],\n    ) -> None: ...\n\nglobal___UpgradeTableProtocol = UpgradeTableProtocol\n\nclass Generate(google.protobuf.message.Message):\n    \"\"\"Command that generates manifest files for a given Delta table.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    MODE_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to generate the manifest files for.\"\"\"\n    mode: builtins.str\n    \"\"\"(Required) The type of manifest file to be generated.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        mode: builtins.str = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self, field_name: typing_extensions.Literal[\"mode\", b\"mode\", \"table\", b\"table\"]\n    ) -> None: ...\n\nglobal___Generate = Generate\n\nclass CreateDeltaTable(google.protobuf.message.Message):\n    \"\"\"Command that creates or replace a Delta table (depending on the mode).\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    class _Mode:\n        ValueType = typing.NewType(\"ValueType\", builtins.int)\n        V: typing_extensions.TypeAlias = ValueType\n\n    class _ModeEnumTypeWrapper(\n        google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[\n            CreateDeltaTable._Mode.ValueType\n        ],\n        builtins.type,\n    ):  # noqa: F821\n        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor\n        MODE_UNSPECIFIED: CreateDeltaTable._Mode.ValueType  # 0\n        MODE_CREATE: CreateDeltaTable._Mode.ValueType  # 1\n        \"\"\"Create the table if it does not exist, and throw an error otherwise.\"\"\"\n        MODE_CREATE_IF_NOT_EXISTS: CreateDeltaTable._Mode.ValueType  # 2\n        \"\"\"Create the table if it does not exist, and do nothing otherwise.\"\"\"\n        MODE_REPLACE: CreateDeltaTable._Mode.ValueType  # 3\n        \"\"\"Replace the table if it already exists, and throw an error otherwise.\"\"\"\n        MODE_CREATE_OR_REPLACE: CreateDeltaTable._Mode.ValueType  # 4\n        \"\"\"Create the table if it does not exist, and replace it otherwise.\"\"\"\n\n    class Mode(_Mode, metaclass=_ModeEnumTypeWrapper): ...\n    MODE_UNSPECIFIED: CreateDeltaTable.Mode.ValueType  # 0\n    MODE_CREATE: CreateDeltaTable.Mode.ValueType  # 1\n    \"\"\"Create the table if it does not exist, and throw an error otherwise.\"\"\"\n    MODE_CREATE_IF_NOT_EXISTS: CreateDeltaTable.Mode.ValueType  # 2\n    \"\"\"Create the table if it does not exist, and do nothing otherwise.\"\"\"\n    MODE_REPLACE: CreateDeltaTable.Mode.ValueType  # 3\n    \"\"\"Replace the table if it already exists, and throw an error otherwise.\"\"\"\n    MODE_CREATE_OR_REPLACE: CreateDeltaTable.Mode.ValueType  # 4\n    \"\"\"Create the table if it does not exist, and replace it otherwise.\"\"\"\n\n    class Column(google.protobuf.message.Message):\n        \"\"\"Column in the schema of the table.\"\"\"\n\n        DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n        class IdentityInfo(google.protobuf.message.Message):\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            START_FIELD_NUMBER: builtins.int\n            STEP_FIELD_NUMBER: builtins.int\n            ALLOW_EXPLICIT_INSERT_FIELD_NUMBER: builtins.int\n            start: builtins.int\n            \"\"\"(Required) The start value of the identity column.\"\"\"\n            step: builtins.int\n            \"\"\"(Required) The increment value of the identity column.\"\"\"\n            allow_explicit_insert: builtins.bool\n            \"\"\"(Required) Whether the identity column is BY DEFAULT (true) or ALWAYS (false).\"\"\"\n            def __init__(\n                self,\n                *,\n                start: builtins.int = ...,\n                step: builtins.int = ...,\n                allow_explicit_insert: builtins.bool = ...,\n            ) -> None: ...\n            def ClearField(\n                self,\n                field_name: typing_extensions.Literal[\n                    \"allow_explicit_insert\",\n                    b\"allow_explicit_insert\",\n                    \"start\",\n                    b\"start\",\n                    \"step\",\n                    b\"step\",\n                ],\n            ) -> None: ...\n\n        NAME_FIELD_NUMBER: builtins.int\n        DATA_TYPE_FIELD_NUMBER: builtins.int\n        NULLABLE_FIELD_NUMBER: builtins.int\n        GENERATED_ALWAYS_AS_FIELD_NUMBER: builtins.int\n        COMMENT_FIELD_NUMBER: builtins.int\n        IDENTITY_INFO_FIELD_NUMBER: builtins.int\n        name: builtins.str\n        \"\"\"(Required) Name of the column.\"\"\"\n        @property\n        def data_type(self) -> pyspark.sql.connect.proto.types_pb2.DataType:\n            \"\"\"(Required) Data type of the column.\"\"\"\n        nullable: builtins.bool\n        \"\"\"(Required) Whether the column is nullable.\"\"\"\n        generated_always_as: builtins.str\n        \"\"\"(Optional) SQL Expression that is used to generate the values in the column.\"\"\"\n        comment: builtins.str\n        \"\"\"(Optional) Comment to describe the column.\"\"\"\n        @property\n        def identity_info(self) -> global___CreateDeltaTable.Column.IdentityInfo:\n            \"\"\"(Optional) Identity information for the column.\"\"\"\n        def __init__(\n            self,\n            *,\n            name: builtins.str = ...,\n            data_type: pyspark.sql.connect.proto.types_pb2.DataType | None = ...,\n            nullable: builtins.bool = ...,\n            generated_always_as: builtins.str | None = ...,\n            comment: builtins.str | None = ...,\n            identity_info: global___CreateDeltaTable.Column.IdentityInfo | None = ...,\n        ) -> None: ...\n        def HasField(\n            self,\n            field_name: typing_extensions.Literal[\n                \"_comment\",\n                b\"_comment\",\n                \"_generated_always_as\",\n                b\"_generated_always_as\",\n                \"_identity_info\",\n                b\"_identity_info\",\n                \"comment\",\n                b\"comment\",\n                \"data_type\",\n                b\"data_type\",\n                \"generated_always_as\",\n                b\"generated_always_as\",\n                \"identity_info\",\n                b\"identity_info\",\n            ],\n        ) -> builtins.bool: ...\n        def ClearField(\n            self,\n            field_name: typing_extensions.Literal[\n                \"_comment\",\n                b\"_comment\",\n                \"_generated_always_as\",\n                b\"_generated_always_as\",\n                \"_identity_info\",\n                b\"_identity_info\",\n                \"comment\",\n                b\"comment\",\n                \"data_type\",\n                b\"data_type\",\n                \"generated_always_as\",\n                b\"generated_always_as\",\n                \"identity_info\",\n                b\"identity_info\",\n                \"name\",\n                b\"name\",\n                \"nullable\",\n                b\"nullable\",\n            ],\n        ) -> None: ...\n        @typing.overload\n        def WhichOneof(\n            self, oneof_group: typing_extensions.Literal[\"_comment\", b\"_comment\"]\n        ) -> typing_extensions.Literal[\"comment\"] | None: ...\n        @typing.overload\n        def WhichOneof(\n            self,\n            oneof_group: typing_extensions.Literal[\"_generated_always_as\", b\"_generated_always_as\"],\n        ) -> typing_extensions.Literal[\"generated_always_as\"] | None: ...\n        @typing.overload\n        def WhichOneof(\n            self, oneof_group: typing_extensions.Literal[\"_identity_info\", b\"_identity_info\"]\n        ) -> typing_extensions.Literal[\"identity_info\"] | None: ...\n\n    class PropertiesEntry(google.protobuf.message.Message):\n        DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n        KEY_FIELD_NUMBER: builtins.int\n        VALUE_FIELD_NUMBER: builtins.int\n        key: builtins.str\n        value: builtins.str\n        def __init__(\n            self,\n            *,\n            key: builtins.str = ...,\n            value: builtins.str = ...,\n        ) -> None: ...\n        def ClearField(\n            self, field_name: typing_extensions.Literal[\"key\", b\"key\", \"value\", b\"value\"]\n        ) -> None: ...\n\n    MODE_FIELD_NUMBER: builtins.int\n    TABLE_NAME_FIELD_NUMBER: builtins.int\n    LOCATION_FIELD_NUMBER: builtins.int\n    COMMENT_FIELD_NUMBER: builtins.int\n    COLUMNS_FIELD_NUMBER: builtins.int\n    PARTITIONING_COLUMNS_FIELD_NUMBER: builtins.int\n    PROPERTIES_FIELD_NUMBER: builtins.int\n    CLUSTERING_COLUMNS_FIELD_NUMBER: builtins.int\n    mode: global___CreateDeltaTable.Mode.ValueType\n    \"\"\"(Required) Mode that determines what to do when a table with the given name or location\n    already exists.\n    \"\"\"\n    table_name: builtins.str\n    \"\"\"(Optional) Qualified name of the table.\"\"\"\n    location: builtins.str\n    \"\"\"(Optional) Path to the directory where the table date is stored.\"\"\"\n    comment: builtins.str\n    \"\"\"(Optional) Comment describing the table.\"\"\"\n    @property\n    def columns(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[\n        global___CreateDeltaTable.Column\n    ]:\n        \"\"\"(Optional) Columns in the schema of the table.\"\"\"\n    @property\n    def partitioning_columns(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:\n        \"\"\"(Optional) Columns used for partitioning the table.\"\"\"\n    @property\n    def properties(\n        self,\n    ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]:\n        \"\"\"(Optional) Properties of the table.\"\"\"\n    @property\n    def clustering_columns(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:\n        \"\"\"(Optional) Columns used for clustering the table.\"\"\"\n    def __init__(\n        self,\n        *,\n        mode: global___CreateDeltaTable.Mode.ValueType = ...,\n        table_name: builtins.str | None = ...,\n        location: builtins.str | None = ...,\n        comment: builtins.str | None = ...,\n        columns: collections.abc.Iterable[global___CreateDeltaTable.Column] | None = ...,\n        partitioning_columns: collections.abc.Iterable[builtins.str] | None = ...,\n        properties: collections.abc.Mapping[builtins.str, builtins.str] | None = ...,\n        clustering_columns: collections.abc.Iterable[builtins.str] | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_comment\",\n            b\"_comment\",\n            \"_location\",\n            b\"_location\",\n            \"_table_name\",\n            b\"_table_name\",\n            \"comment\",\n            b\"comment\",\n            \"location\",\n            b\"location\",\n            \"table_name\",\n            b\"table_name\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_comment\",\n            b\"_comment\",\n            \"_location\",\n            b\"_location\",\n            \"_table_name\",\n            b\"_table_name\",\n            \"clustering_columns\",\n            b\"clustering_columns\",\n            \"columns\",\n            b\"columns\",\n            \"comment\",\n            b\"comment\",\n            \"location\",\n            b\"location\",\n            \"mode\",\n            b\"mode\",\n            \"partitioning_columns\",\n            b\"partitioning_columns\",\n            \"properties\",\n            b\"properties\",\n            \"table_name\",\n            b\"table_name\",\n        ],\n    ) -> None: ...\n    @typing.overload\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"_comment\", b\"_comment\"]\n    ) -> typing_extensions.Literal[\"comment\"] | None: ...\n    @typing.overload\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"_location\", b\"_location\"]\n    ) -> typing_extensions.Literal[\"location\"] | None: ...\n    @typing.overload\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"_table_name\", b\"_table_name\"]\n    ) -> typing_extensions.Literal[\"table_name\"] | None: ...\n\nglobal___CreateDeltaTable = CreateDeltaTable\n\nclass AddFeatureSupport(google.protobuf.message.Message):\n    \"\"\"Command to add a supported feature to the table by modifying the protocol.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    FEATURE_NAME_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to add the supported feature to.\"\"\"\n    feature_name: builtins.str\n    \"\"\"(Required) The name of the supported feature to add.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        feature_name: builtins.str = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\"feature_name\", b\"feature_name\", \"table\", b\"table\"],\n    ) -> None: ...\n\nglobal___AddFeatureSupport = AddFeatureSupport\n\nclass DropFeatureSupport(google.protobuf.message.Message):\n    \"\"\"Command to drop a supported feature from the table by modifying the protocol.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    FEATURE_NAME_FIELD_NUMBER: builtins.int\n    TRUNCATE_HISTORY_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to drop the supported feature from.\"\"\"\n    feature_name: builtins.str\n    \"\"\"(Required) The name of the supported feature to drop.\"\"\"\n    truncate_history: builtins.bool\n    \"\"\"(optional) Whether to truncate history. When not specified, history is not truncated.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        feature_name: builtins.str = ...,\n        truncate_history: builtins.bool | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_truncate_history\",\n            b\"_truncate_history\",\n            \"table\",\n            b\"table\",\n            \"truncate_history\",\n            b\"truncate_history\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_truncate_history\",\n            b\"_truncate_history\",\n            \"feature_name\",\n            b\"feature_name\",\n            \"table\",\n            b\"table\",\n            \"truncate_history\",\n            b\"truncate_history\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"_truncate_history\", b\"_truncate_history\"]\n    ) -> typing_extensions.Literal[\"truncate_history\"] | None: ...\n\nglobal___DropFeatureSupport = DropFeatureSupport\n"
  },
  {
    "path": "python/delta/connect/proto/relations_pb2.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: delta/connect/relations.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\nfrom google.protobuf import symbol_database as _symbol_database\n\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\nfrom delta.connect.proto import base_pb2 as delta_dot_connect_dot_base__pb2\nfrom pyspark.sql.connect.proto import expressions_pb2 as spark_dot_connect_dot_expressions__pb2\nfrom pyspark.sql.connect.proto import relations_pb2 as spark_dot_connect_dot_relations__pb2\nfrom pyspark.sql.connect.proto import types_pb2 as spark_dot_connect_dot_types__pb2\n\n\nDESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(\n    b'\\n\\x1d\\x64\\x65lta/connect/relations.proto\\x12\\rdelta.connect\\x1a\\x18\\x64\\x65lta/connect/base.proto\\x1a\\x1fspark/connect/expressions.proto\\x1a\\x1dspark/connect/relations.proto\\x1a\\x19spark/connect/types.proto\"\\xd7\\x05\\n\\rDeltaRelation\\x12)\\n\\x04scan\\x18\\x01 \\x01(\\x0b\\x32\\x13.delta.connect.ScanH\\x00R\\x04scan\\x12K\\n\\x10\\x64\\x65scribe_history\\x18\\x02 \\x01(\\x0b\\x32\\x1e.delta.connect.DescribeHistoryH\\x00R\\x0f\\x64\\x65scribeHistory\\x12H\\n\\x0f\\x64\\x65scribe_detail\\x18\\x03 \\x01(\\x0b\\x32\\x1d.delta.connect.DescribeDetailH\\x00R\\x0e\\x64\\x65scribeDetail\\x12I\\n\\x10\\x63onvert_to_delta\\x18\\x04 \\x01(\\x0b\\x32\\x1d.delta.connect.ConvertToDeltaH\\x00R\\x0e\\x63onvertToDelta\\x12\\x42\\n\\rrestore_table\\x18\\x05 \\x01(\\x0b\\x32\\x1b.delta.connect.RestoreTableH\\x00R\\x0crestoreTable\\x12\\x43\\n\\x0eis_delta_table\\x18\\x06 \\x01(\\x0b\\x32\\x1b.delta.connect.IsDeltaTableH\\x00R\\x0cisDeltaTable\\x12L\\n\\x11\\x64\\x65lete_from_table\\x18\\x07 \\x01(\\x0b\\x32\\x1e.delta.connect.DeleteFromTableH\\x00R\\x0f\\x64\\x65leteFromTable\\x12?\\n\\x0cupdate_table\\x18\\x08 \\x01(\\x0b\\x32\\x1a.delta.connect.UpdateTableH\\x00R\\x0bupdateTable\\x12I\\n\\x10merge_into_table\\x18\\t \\x01(\\x0b\\x32\\x1d.delta.connect.MergeIntoTableH\\x00R\\x0emergeIntoTable\\x12\\x45\\n\\x0eoptimize_table\\x18\\n \\x01(\\x0b\\x32\\x1c.delta.connect.OptimizeTableH\\x00R\\roptimizeTableB\\x0f\\n\\rrelation_type\"7\\n\\x04Scan\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\"B\\n\\x0f\\x44\\x65scribeHistory\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\"A\\n\\x0e\\x44\\x65scribeDetail\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\"\\xd1\\x01\\n\\x0e\\x43onvertToDelta\\x12\\x1e\\n\\nidentifier\\x18\\x01 \\x01(\\tR\\nidentifier\\x12\\x38\\n\\x17partition_schema_string\\x18\\x02 \\x01(\\tH\\x00R\\x15partitionSchemaString\\x12Q\\n\\x17partition_schema_struct\\x18\\x03 \\x01(\\x0b\\x32\\x17.spark.connect.DataTypeH\\x00R\\x15partitionSchemaStructB\\x12\\n\\x10partition_schema\"\\x93\\x01\\n\\x0cRestoreTable\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12\\x1a\\n\\x07version\\x18\\x02 \\x01(\\x03H\\x00R\\x07version\\x12\\x1e\\n\\ttimestamp\\x18\\x03 \\x01(\\tH\\x00R\\ttimestampB\\x16\\n\\x14version_or_timestamp\"\"\\n\\x0cIsDeltaTable\\x12\\x12\\n\\x04path\\x18\\x01 \\x01(\\tR\\x04path\"{\\n\\x0f\\x44\\x65leteFromTable\\x12/\\n\\x06target\\x18\\x01 \\x01(\\x0b\\x32\\x17.spark.connect.RelationR\\x06target\\x12\\x37\\n\\tcondition\\x18\\x02 \\x01(\\x0b\\x32\\x19.spark.connect.ExpressionR\\tcondition\"\\xb4\\x01\\n\\x0bUpdateTable\\x12/\\n\\x06target\\x18\\x01 \\x01(\\x0b\\x32\\x17.spark.connect.RelationR\\x06target\\x12\\x37\\n\\tcondition\\x18\\x02 \\x01(\\x0b\\x32\\x19.spark.connect.ExpressionR\\tcondition\\x12;\\n\\x0b\\x61ssignments\\x18\\x03 \\x03(\\x0b\\x32\\x19.delta.connect.AssignmentR\\x0b\\x61ssignments\"\\x8c\\n\\n\\x0eMergeIntoTable\\x12/\\n\\x06target\\x18\\x01 \\x01(\\x0b\\x32\\x17.spark.connect.RelationR\\x06target\\x12/\\n\\x06source\\x18\\x02 \\x01(\\x0b\\x32\\x17.spark.connect.RelationR\\x06source\\x12\\x37\\n\\tcondition\\x18\\x03 \\x01(\\x0b\\x32\\x19.spark.connect.ExpressionR\\tcondition\\x12M\\n\\x0fmatched_actions\\x18\\x04 \\x03(\\x0b\\x32$.delta.connect.MergeIntoTable.ActionR\\x0ematchedActions\\x12T\\n\\x13not_matched_actions\\x18\\x05 \\x03(\\x0b\\x32$.delta.connect.MergeIntoTable.ActionR\\x11notMatchedActions\\x12\\x66\\n\\x1dnot_matched_by_source_actions\\x18\\x06 \\x03(\\x0b\\x32$.delta.connect.MergeIntoTable.ActionR\\x19notMatchedBySourceActions\\x12\\x37\\n\\x15with_schema_evolution\\x18\\x07 \\x01(\\x08H\\x00R\\x13withSchemaEvolution\\x88\\x01\\x01\\x1a\\xfe\\x05\\n\\x06\\x41\\x63tion\\x12\\x37\\n\\tcondition\\x18\\x01 \\x01(\\x0b\\x32\\x19.spark.connect.ExpressionR\\tcondition\\x12X\\n\\rdelete_action\\x18\\x02 \\x01(\\x0b\\x32\\x31.delta.connect.MergeIntoTable.Action.DeleteActionH\\x00R\\x0c\\x64\\x65leteAction\\x12X\\n\\rupdate_action\\x18\\x03 \\x01(\\x0b\\x32\\x31.delta.connect.MergeIntoTable.Action.UpdateActionH\\x00R\\x0cupdateAction\\x12\\x65\\n\\x12update_star_action\\x18\\x04 \\x01(\\x0b\\x32\\x35.delta.connect.MergeIntoTable.Action.UpdateStarActionH\\x00R\\x10updateStarAction\\x12X\\n\\rinsert_action\\x18\\x05 \\x01(\\x0b\\x32\\x31.delta.connect.MergeIntoTable.Action.InsertActionH\\x00R\\x0cinsertAction\\x12\\x65\\n\\x12insert_star_action\\x18\\x06 \\x01(\\x0b\\x32\\x35.delta.connect.MergeIntoTable.Action.InsertStarActionH\\x00R\\x10insertStarAction\\x1a\\x0e\\n\\x0c\\x44\\x65leteAction\\x1aK\\n\\x0cUpdateAction\\x12;\\n\\x0b\\x61ssignments\\x18\\x01 \\x03(\\x0b\\x32\\x19.delta.connect.AssignmentR\\x0b\\x61ssignments\\x1a\\x12\\n\\x10UpdateStarAction\\x1aK\\n\\x0cInsertAction\\x12;\\n\\x0b\\x61ssignments\\x18\\x01 \\x03(\\x0b\\x32\\x19.delta.connect.AssignmentR\\x0b\\x61ssignments\\x1a\\x12\\n\\x10InsertStarActionB\\r\\n\\x0b\\x61\\x63tion_typeB\\x18\\n\\x16_with_schema_evolution\"n\\n\\nAssignment\\x12/\\n\\x05\\x66ield\\x18\\x01 \\x01(\\x0b\\x32\\x19.spark.connect.ExpressionR\\x05\\x66ield\\x12/\\n\\x05value\\x18\\x02 \\x01(\\x0b\\x32\\x19.spark.connect.ExpressionR\\x05value\"\\x94\\x01\\n\\rOptimizeTable\\x12/\\n\\x05table\\x18\\x01 \\x01(\\x0b\\x32\\x19.delta.connect.DeltaTableR\\x05table\\x12+\\n\\x11partition_filters\\x18\\x02 \\x03(\\tR\\x10partitionFilters\\x12%\\n\\x0ezorder_columns\\x18\\x03 \\x03(\\tR\\rzorderColumnsB\\x1a\\n\\x16io.delta.connect.protoP\\x01\\x62\\x06proto3'\n)\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, \"delta.connect.proto.relations_pb2\", globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n    DESCRIPTOR._options = None\n    DESCRIPTOR._serialized_options = b\"\\n\\026io.delta.connect.protoP\\001\"\n    _DELTARELATION._serialized_start = 166\n    _DELTARELATION._serialized_end = 893\n    _SCAN._serialized_start = 895\n    _SCAN._serialized_end = 950\n    _DESCRIBEHISTORY._serialized_start = 952\n    _DESCRIBEHISTORY._serialized_end = 1018\n    _DESCRIBEDETAIL._serialized_start = 1020\n    _DESCRIBEDETAIL._serialized_end = 1085\n    _CONVERTTODELTA._serialized_start = 1088\n    _CONVERTTODELTA._serialized_end = 1297\n    _RESTORETABLE._serialized_start = 1300\n    _RESTORETABLE._serialized_end = 1447\n    _ISDELTATABLE._serialized_start = 1449\n    _ISDELTATABLE._serialized_end = 1483\n    _DELETEFROMTABLE._serialized_start = 1485\n    _DELETEFROMTABLE._serialized_end = 1608\n    _UPDATETABLE._serialized_start = 1611\n    _UPDATETABLE._serialized_end = 1791\n    _MERGEINTOTABLE._serialized_start = 1794\n    _MERGEINTOTABLE._serialized_end = 3086\n    _MERGEINTOTABLE_ACTION._serialized_start = 2294\n    _MERGEINTOTABLE_ACTION._serialized_end = 3060\n    _MERGEINTOTABLE_ACTION_DELETEACTION._serialized_start = 2837\n    _MERGEINTOTABLE_ACTION_DELETEACTION._serialized_end = 2851\n    _MERGEINTOTABLE_ACTION_UPDATEACTION._serialized_start = 2853\n    _MERGEINTOTABLE_ACTION_UPDATEACTION._serialized_end = 2928\n    _MERGEINTOTABLE_ACTION_UPDATESTARACTION._serialized_start = 2930\n    _MERGEINTOTABLE_ACTION_UPDATESTARACTION._serialized_end = 2948\n    _MERGEINTOTABLE_ACTION_INSERTACTION._serialized_start = 2950\n    _MERGEINTOTABLE_ACTION_INSERTACTION._serialized_end = 3025\n    _MERGEINTOTABLE_ACTION_INSERTSTARACTION._serialized_start = 3027\n    _MERGEINTOTABLE_ACTION_INSERTSTARACTION._serialized_end = 3045\n    _ASSIGNMENT._serialized_start = 3088\n    _ASSIGNMENT._serialized_end = 3198\n    _OPTIMIZETABLE._serialized_start = 3201\n    _OPTIMIZETABLE._serialized_end = 3349\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "python/delta/connect/proto/relations_pb2.pyi",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\"\"\"\n@generated by mypy-protobuf.  Do not edit manually!\nisort:skip_file\n\nCopyright (2024) The Delta Lake Project Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\"\"\"\nimport builtins\nimport collections.abc\nimport delta.connect.proto.proto.base_pb2\nimport google.protobuf.descriptor\nimport google.protobuf.internal.containers\nimport google.protobuf.message\nimport pyspark.sql.connect.proto.expressions_pb2\nimport pyspark.sql.connect.proto.relations_pb2\nimport pyspark.sql.connect.proto.types_pb2\nimport sys\n\nif sys.version_info >= (3, 8):\n    import typing as typing_extensions\nelse:\n    import typing_extensions\n\nDESCRIPTOR: google.protobuf.descriptor.FileDescriptor\n\nclass DeltaRelation(google.protobuf.message.Message):\n    \"\"\"Message to hold all relation extensions in Delta Connect.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    SCAN_FIELD_NUMBER: builtins.int\n    DESCRIBE_HISTORY_FIELD_NUMBER: builtins.int\n    DESCRIBE_DETAIL_FIELD_NUMBER: builtins.int\n    CONVERT_TO_DELTA_FIELD_NUMBER: builtins.int\n    RESTORE_TABLE_FIELD_NUMBER: builtins.int\n    IS_DELTA_TABLE_FIELD_NUMBER: builtins.int\n    DELETE_FROM_TABLE_FIELD_NUMBER: builtins.int\n    UPDATE_TABLE_FIELD_NUMBER: builtins.int\n    MERGE_INTO_TABLE_FIELD_NUMBER: builtins.int\n    OPTIMIZE_TABLE_FIELD_NUMBER: builtins.int\n    @property\n    def scan(self) -> global___Scan: ...\n    @property\n    def describe_history(self) -> global___DescribeHistory: ...\n    @property\n    def describe_detail(self) -> global___DescribeDetail: ...\n    @property\n    def convert_to_delta(self) -> global___ConvertToDelta: ...\n    @property\n    def restore_table(self) -> global___RestoreTable: ...\n    @property\n    def is_delta_table(self) -> global___IsDeltaTable: ...\n    @property\n    def delete_from_table(self) -> global___DeleteFromTable: ...\n    @property\n    def update_table(self) -> global___UpdateTable: ...\n    @property\n    def merge_into_table(self) -> global___MergeIntoTable: ...\n    @property\n    def optimize_table(self) -> global___OptimizeTable: ...\n    def __init__(\n        self,\n        *,\n        scan: global___Scan | None = ...,\n        describe_history: global___DescribeHistory | None = ...,\n        describe_detail: global___DescribeDetail | None = ...,\n        convert_to_delta: global___ConvertToDelta | None = ...,\n        restore_table: global___RestoreTable | None = ...,\n        is_delta_table: global___IsDeltaTable | None = ...,\n        delete_from_table: global___DeleteFromTable | None = ...,\n        update_table: global___UpdateTable | None = ...,\n        merge_into_table: global___MergeIntoTable | None = ...,\n        optimize_table: global___OptimizeTable | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"convert_to_delta\",\n            b\"convert_to_delta\",\n            \"delete_from_table\",\n            b\"delete_from_table\",\n            \"describe_detail\",\n            b\"describe_detail\",\n            \"describe_history\",\n            b\"describe_history\",\n            \"is_delta_table\",\n            b\"is_delta_table\",\n            \"merge_into_table\",\n            b\"merge_into_table\",\n            \"optimize_table\",\n            b\"optimize_table\",\n            \"relation_type\",\n            b\"relation_type\",\n            \"restore_table\",\n            b\"restore_table\",\n            \"scan\",\n            b\"scan\",\n            \"update_table\",\n            b\"update_table\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"convert_to_delta\",\n            b\"convert_to_delta\",\n            \"delete_from_table\",\n            b\"delete_from_table\",\n            \"describe_detail\",\n            b\"describe_detail\",\n            \"describe_history\",\n            b\"describe_history\",\n            \"is_delta_table\",\n            b\"is_delta_table\",\n            \"merge_into_table\",\n            b\"merge_into_table\",\n            \"optimize_table\",\n            b\"optimize_table\",\n            \"relation_type\",\n            b\"relation_type\",\n            \"restore_table\",\n            b\"restore_table\",\n            \"scan\",\n            b\"scan\",\n            \"update_table\",\n            b\"update_table\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"relation_type\", b\"relation_type\"]\n    ) -> (\n        typing_extensions.Literal[\n            \"scan\",\n            \"describe_history\",\n            \"describe_detail\",\n            \"convert_to_delta\",\n            \"restore_table\",\n            \"is_delta_table\",\n            \"delete_from_table\",\n            \"update_table\",\n            \"merge_into_table\",\n            \"optimize_table\",\n        ]\n        | None\n    ): ...\n\nglobal___DeltaRelation = DeltaRelation\n\nclass Scan(google.protobuf.message.Message):\n    \"\"\"Relation that reads from a Delta table.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to scan.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(self, field_name: typing_extensions.Literal[\"table\", b\"table\"]) -> None: ...\n\nglobal___Scan = Scan\n\nclass DescribeHistory(google.protobuf.message.Message):\n    \"\"\"Relation containing information of the latest commits on a Delta table.\n    The information is in reverse chronological order.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to read the history of.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(self, field_name: typing_extensions.Literal[\"table\", b\"table\"]) -> None: ...\n\nglobal___DescribeHistory = DescribeHistory\n\nclass DescribeDetail(google.protobuf.message.Message):\n    \"\"\"Relation containing the details of a Delta table such as the format, name, and size.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to describe the details of.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(self, field_name: typing_extensions.Literal[\"table\", b\"table\"]) -> None: ...\n\nglobal___DescribeDetail = DescribeDetail\n\nclass ConvertToDelta(google.protobuf.message.Message):\n    \"\"\"Command that turns a Parquet table into a Delta table.\n\n    This needs to be a Relation as it returns the identifier of the resulting table.\n    We cannot simply reuse the input identifier, as it could be a path-based identifier,\n    and in that case we need to replace \"parquet.`...`\" with \"delta.`...`\".\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    IDENTIFIER_FIELD_NUMBER: builtins.int\n    PARTITION_SCHEMA_STRING_FIELD_NUMBER: builtins.int\n    PARTITION_SCHEMA_STRUCT_FIELD_NUMBER: builtins.int\n    identifier: builtins.str\n    \"\"\"(Required) Parquet table identifier formatted as \"parquet.`path`\" \"\"\"\n    partition_schema_string: builtins.str\n    \"\"\"Hive DDL formatted string\"\"\"\n    @property\n    def partition_schema_struct(self) -> pyspark.sql.connect.proto.types_pb2.DataType:\n        \"\"\"Struct with names and types of partitioning columns\"\"\"\n    def __init__(\n        self,\n        *,\n        identifier: builtins.str = ...,\n        partition_schema_string: builtins.str = ...,\n        partition_schema_struct: pyspark.sql.connect.proto.types_pb2.DataType | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"partition_schema\",\n            b\"partition_schema\",\n            \"partition_schema_string\",\n            b\"partition_schema_string\",\n            \"partition_schema_struct\",\n            b\"partition_schema_struct\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"identifier\",\n            b\"identifier\",\n            \"partition_schema\",\n            b\"partition_schema\",\n            \"partition_schema_string\",\n            b\"partition_schema_string\",\n            \"partition_schema_struct\",\n            b\"partition_schema_struct\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self, oneof_group: typing_extensions.Literal[\"partition_schema\", b\"partition_schema\"]\n    ) -> typing_extensions.Literal[\"partition_schema_string\", \"partition_schema_struct\"] | None: ...\n\nglobal___ConvertToDelta = ConvertToDelta\n\nclass RestoreTable(google.protobuf.message.Message):\n    \"\"\"Command that restores the DeltaTable to an older version of the table specified by either a\n    version number or a timestamp.\n\n    Needs to be a Relation, as it returns a row containing the execution metrics.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    VERSION_FIELD_NUMBER: builtins.int\n    TIMESTAMP_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to restore to an earlier version.\"\"\"\n    version: builtins.int\n    \"\"\"The version number to restore to.\"\"\"\n    timestamp: builtins.str\n    \"\"\"The timestamp to restore to.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        version: builtins.int = ...,\n        timestamp: builtins.str = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"table\",\n            b\"table\",\n            \"timestamp\",\n            b\"timestamp\",\n            \"version\",\n            b\"version\",\n            \"version_or_timestamp\",\n            b\"version_or_timestamp\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"table\",\n            b\"table\",\n            \"timestamp\",\n            b\"timestamp\",\n            \"version\",\n            b\"version\",\n            \"version_or_timestamp\",\n            b\"version_or_timestamp\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self,\n        oneof_group: typing_extensions.Literal[\"version_or_timestamp\", b\"version_or_timestamp\"],\n    ) -> typing_extensions.Literal[\"version\", \"timestamp\"] | None: ...\n\nglobal___RestoreTable = RestoreTable\n\nclass IsDeltaTable(google.protobuf.message.Message):\n    \"\"\"Relation containing a single row containing a single boolean that indicates whether the provided\n    path contains a Delta table.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    PATH_FIELD_NUMBER: builtins.int\n    path: builtins.str\n    \"\"\"(Required) The path to check.\"\"\"\n    def __init__(\n        self,\n        *,\n        path: builtins.str = ...,\n    ) -> None: ...\n    def ClearField(self, field_name: typing_extensions.Literal[\"path\", b\"path\"]) -> None: ...\n\nglobal___IsDeltaTable = IsDeltaTable\n\nclass DeleteFromTable(google.protobuf.message.Message):\n    \"\"\"Command that deletes data from the target table that matches the given condition.\n\n    Needs to be a Relation, as it returns a row containing the execution metrics.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TARGET_FIELD_NUMBER: builtins.int\n    CONDITION_FIELD_NUMBER: builtins.int\n    @property\n    def target(self) -> pyspark.sql.connect.proto.relations_pb2.Relation:\n        \"\"\"(Required) Target table to delete data from. Must either be a DeltaRelation containing a Scan\n        or a SubqueryAlias with a DeltaRelation containing a Scan as its input.\n        \"\"\"\n    @property\n    def condition(self) -> pyspark.sql.connect.proto.expressions_pb2.Expression:\n        \"\"\"(Optional) Expression returning a boolean.\"\"\"\n    def __init__(\n        self,\n        *,\n        target: pyspark.sql.connect.proto.relations_pb2.Relation | None = ...,\n        condition: pyspark.sql.connect.proto.expressions_pb2.Expression | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"condition\", b\"condition\", \"target\", b\"target\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self, field_name: typing_extensions.Literal[\"condition\", b\"condition\", \"target\", b\"target\"]\n    ) -> None: ...\n\nglobal___DeleteFromTable = DeleteFromTable\n\nclass UpdateTable(google.protobuf.message.Message):\n    \"\"\"Command that updates data in the target table using the given assignments for rows that matches\n    the given condition.\n\n    Needs to be a Relation, as it returns a row containing the execution metrics.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TARGET_FIELD_NUMBER: builtins.int\n    CONDITION_FIELD_NUMBER: builtins.int\n    ASSIGNMENTS_FIELD_NUMBER: builtins.int\n    @property\n    def target(self) -> pyspark.sql.connect.proto.relations_pb2.Relation:\n        \"\"\"(Required) Target table to delete data from. Must either be a DeltaRelation containing a Scan\n        or a SubqueryAlias with a DeltaRelation containing a Scan as its input.\n        \"\"\"\n    @property\n    def condition(self) -> pyspark.sql.connect.proto.expressions_pb2.Expression:\n        \"\"\"(Optional) Condition that determines which rows must be updated.\n        Must be an expression returning a boolean.\n        \"\"\"\n    @property\n    def assignments(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Assignment]:\n        \"\"\"(Optional) Set of assignments to apply to the rows matching the condition.\"\"\"\n    def __init__(\n        self,\n        *,\n        target: pyspark.sql.connect.proto.relations_pb2.Relation | None = ...,\n        condition: pyspark.sql.connect.proto.expressions_pb2.Expression | None = ...,\n        assignments: collections.abc.Iterable[global___Assignment] | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"condition\", b\"condition\", \"target\", b\"target\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"assignments\", b\"assignments\", \"condition\", b\"condition\", \"target\", b\"target\"\n        ],\n    ) -> None: ...\n\nglobal___UpdateTable = UpdateTable\n\nclass MergeIntoTable(google.protobuf.message.Message):\n    \"\"\"Command that merges a source query/table into a Delta table,\n\n    Needs to be a Relation, as it returns a row containing the execution metrics.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    class Action(google.protobuf.message.Message):\n        \"\"\"Rule that specifies how the target table should be modified.\"\"\"\n\n        DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n        class DeleteAction(google.protobuf.message.Message):\n            \"\"\"Action that deletes the target row.\"\"\"\n\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            def __init__(\n                self,\n            ) -> None: ...\n\n        class UpdateAction(google.protobuf.message.Message):\n            \"\"\"Action that updates the target row using a set of assignments.\"\"\"\n\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            ASSIGNMENTS_FIELD_NUMBER: builtins.int\n            @property\n            def assignments(\n                self,\n            ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[\n                global___Assignment\n            ]:\n                \"\"\"(Optional) Set of assignments to apply.\"\"\"\n            def __init__(\n                self,\n                *,\n                assignments: collections.abc.Iterable[global___Assignment] | None = ...,\n            ) -> None: ...\n            def ClearField(\n                self, field_name: typing_extensions.Literal[\"assignments\", b\"assignments\"]\n            ) -> None: ...\n\n        class UpdateStarAction(google.protobuf.message.Message):\n            \"\"\"Action that updates the target row by overwriting all columns.\"\"\"\n\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            def __init__(\n                self,\n            ) -> None: ...\n\n        class InsertAction(google.protobuf.message.Message):\n            \"\"\"Action that inserts the source row into the target using a set of assignments.\"\"\"\n\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            ASSIGNMENTS_FIELD_NUMBER: builtins.int\n            @property\n            def assignments(\n                self,\n            ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[\n                global___Assignment\n            ]:\n                \"\"\"(Optional) Set of assignments to apply.\"\"\"\n            def __init__(\n                self,\n                *,\n                assignments: collections.abc.Iterable[global___Assignment] | None = ...,\n            ) -> None: ...\n            def ClearField(\n                self, field_name: typing_extensions.Literal[\"assignments\", b\"assignments\"]\n            ) -> None: ...\n\n        class InsertStarAction(google.protobuf.message.Message):\n            \"\"\"Action that inserts the source row into the target by setting all columns.\"\"\"\n\n            DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n            def __init__(\n                self,\n            ) -> None: ...\n\n        CONDITION_FIELD_NUMBER: builtins.int\n        DELETE_ACTION_FIELD_NUMBER: builtins.int\n        UPDATE_ACTION_FIELD_NUMBER: builtins.int\n        UPDATE_STAR_ACTION_FIELD_NUMBER: builtins.int\n        INSERT_ACTION_FIELD_NUMBER: builtins.int\n        INSERT_STAR_ACTION_FIELD_NUMBER: builtins.int\n        @property\n        def condition(self) -> pyspark.sql.connect.proto.expressions_pb2.Expression:\n            \"\"\"(Optional) Condition for the action to be applied.\"\"\"\n        @property\n        def delete_action(self) -> global___MergeIntoTable.Action.DeleteAction: ...\n        @property\n        def update_action(self) -> global___MergeIntoTable.Action.UpdateAction: ...\n        @property\n        def update_star_action(self) -> global___MergeIntoTable.Action.UpdateStarAction: ...\n        @property\n        def insert_action(self) -> global___MergeIntoTable.Action.InsertAction: ...\n        @property\n        def insert_star_action(self) -> global___MergeIntoTable.Action.InsertStarAction: ...\n        def __init__(\n            self,\n            *,\n            condition: pyspark.sql.connect.proto.expressions_pb2.Expression | None = ...,\n            delete_action: global___MergeIntoTable.Action.DeleteAction | None = ...,\n            update_action: global___MergeIntoTable.Action.UpdateAction | None = ...,\n            update_star_action: global___MergeIntoTable.Action.UpdateStarAction | None = ...,\n            insert_action: global___MergeIntoTable.Action.InsertAction | None = ...,\n            insert_star_action: global___MergeIntoTable.Action.InsertStarAction | None = ...,\n        ) -> None: ...\n        def HasField(\n            self,\n            field_name: typing_extensions.Literal[\n                \"action_type\",\n                b\"action_type\",\n                \"condition\",\n                b\"condition\",\n                \"delete_action\",\n                b\"delete_action\",\n                \"insert_action\",\n                b\"insert_action\",\n                \"insert_star_action\",\n                b\"insert_star_action\",\n                \"update_action\",\n                b\"update_action\",\n                \"update_star_action\",\n                b\"update_star_action\",\n            ],\n        ) -> builtins.bool: ...\n        def ClearField(\n            self,\n            field_name: typing_extensions.Literal[\n                \"action_type\",\n                b\"action_type\",\n                \"condition\",\n                b\"condition\",\n                \"delete_action\",\n                b\"delete_action\",\n                \"insert_action\",\n                b\"insert_action\",\n                \"insert_star_action\",\n                b\"insert_star_action\",\n                \"update_action\",\n                b\"update_action\",\n                \"update_star_action\",\n                b\"update_star_action\",\n            ],\n        ) -> None: ...\n        def WhichOneof(\n            self, oneof_group: typing_extensions.Literal[\"action_type\", b\"action_type\"]\n        ) -> (\n            typing_extensions.Literal[\n                \"delete_action\",\n                \"update_action\",\n                \"update_star_action\",\n                \"insert_action\",\n                \"insert_star_action\",\n            ]\n            | None\n        ): ...\n\n    TARGET_FIELD_NUMBER: builtins.int\n    SOURCE_FIELD_NUMBER: builtins.int\n    CONDITION_FIELD_NUMBER: builtins.int\n    MATCHED_ACTIONS_FIELD_NUMBER: builtins.int\n    NOT_MATCHED_ACTIONS_FIELD_NUMBER: builtins.int\n    NOT_MATCHED_BY_SOURCE_ACTIONS_FIELD_NUMBER: builtins.int\n    WITH_SCHEMA_EVOLUTION_FIELD_NUMBER: builtins.int\n    @property\n    def target(self) -> pyspark.sql.connect.proto.relations_pb2.Relation:\n        \"\"\"(Required) Target table to merge into.\"\"\"\n    @property\n    def source(self) -> pyspark.sql.connect.proto.relations_pb2.Relation:\n        \"\"\"(Required) Source data to merge from.\"\"\"\n    @property\n    def condition(self) -> pyspark.sql.connect.proto.expressions_pb2.Expression:\n        \"\"\"(Required) Condition for a source row to match with a target row.\"\"\"\n    @property\n    def matched_actions(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[\n        global___MergeIntoTable.Action\n    ]:\n        \"\"\"(Optional) Actions to apply when a source row matches a target row.\"\"\"\n    @property\n    def not_matched_actions(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[\n        global___MergeIntoTable.Action\n    ]:\n        \"\"\"(Optional) Actions to apply when a source row does not match a target row.\"\"\"\n    @property\n    def not_matched_by_source_actions(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[\n        global___MergeIntoTable.Action\n    ]:\n        \"\"\"(Optional) Actions to apply when a target row does not match a source row.\"\"\"\n    with_schema_evolution: builtins.bool\n    \"\"\"(Optional) Whether Schema Evolution is enabled for this command.\"\"\"\n    def __init__(\n        self,\n        *,\n        target: pyspark.sql.connect.proto.relations_pb2.Relation | None = ...,\n        source: pyspark.sql.connect.proto.relations_pb2.Relation | None = ...,\n        condition: pyspark.sql.connect.proto.expressions_pb2.Expression | None = ...,\n        matched_actions: collections.abc.Iterable[global___MergeIntoTable.Action] | None = ...,\n        not_matched_actions: collections.abc.Iterable[global___MergeIntoTable.Action] | None = ...,\n        not_matched_by_source_actions: collections.abc.Iterable[global___MergeIntoTable.Action]\n        | None = ...,\n        with_schema_evolution: builtins.bool | None = ...,\n    ) -> None: ...\n    def HasField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_with_schema_evolution\",\n            b\"_with_schema_evolution\",\n            \"condition\",\n            b\"condition\",\n            \"source\",\n            b\"source\",\n            \"target\",\n            b\"target\",\n            \"with_schema_evolution\",\n            b\"with_schema_evolution\",\n        ],\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"_with_schema_evolution\",\n            b\"_with_schema_evolution\",\n            \"condition\",\n            b\"condition\",\n            \"matched_actions\",\n            b\"matched_actions\",\n            \"not_matched_actions\",\n            b\"not_matched_actions\",\n            \"not_matched_by_source_actions\",\n            b\"not_matched_by_source_actions\",\n            \"source\",\n            b\"source\",\n            \"target\",\n            b\"target\",\n            \"with_schema_evolution\",\n            b\"with_schema_evolution\",\n        ],\n    ) -> None: ...\n    def WhichOneof(\n        self,\n        oneof_group: typing_extensions.Literal[\"_with_schema_evolution\", b\"_with_schema_evolution\"],\n    ) -> typing_extensions.Literal[\"with_schema_evolution\"] | None: ...\n\nglobal___MergeIntoTable = MergeIntoTable\n\nclass Assignment(google.protobuf.message.Message):\n    \"\"\"Represents an assignment of a value to a field.\"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    FIELD_FIELD_NUMBER: builtins.int\n    VALUE_FIELD_NUMBER: builtins.int\n    @property\n    def field(self) -> pyspark.sql.connect.proto.expressions_pb2.Expression:\n        \"\"\"(Required) Expression identifying the (struct) field that is assigned a new value.\"\"\"\n    @property\n    def value(self) -> pyspark.sql.connect.proto.expressions_pb2.Expression:\n        \"\"\"(Required) Expression that produces the value to assign to the field.\"\"\"\n    def __init__(\n        self,\n        *,\n        field: pyspark.sql.connect.proto.expressions_pb2.Expression | None = ...,\n        value: pyspark.sql.connect.proto.expressions_pb2.Expression | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"field\", b\"field\", \"value\", b\"value\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self, field_name: typing_extensions.Literal[\"field\", b\"field\", \"value\", b\"value\"]\n    ) -> None: ...\n\nglobal___Assignment = Assignment\n\nclass OptimizeTable(google.protobuf.message.Message):\n    \"\"\"Command that optimizes the layout of a Delta table by either compacting small files or\n    by ordering the data. Allows specifying partition filters to limit the scope of the data\n    reorganization.\n\n    Needs to be a Relation, as it returns a row containing the execution metrics.\n    \"\"\"\n\n    DESCRIPTOR: google.protobuf.descriptor.Descriptor\n\n    TABLE_FIELD_NUMBER: builtins.int\n    PARTITION_FILTERS_FIELD_NUMBER: builtins.int\n    ZORDER_COLUMNS_FIELD_NUMBER: builtins.int\n    @property\n    def table(self) -> delta.connect.proto.base_pb2.DeltaTable:\n        \"\"\"(Required) The Delta table to optimize.\"\"\"\n    @property\n    def partition_filters(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:\n        \"\"\"(Optional) Partition filters that limit the operation to the files in the matched partitions.\"\"\"\n    @property\n    def zorder_columns(\n        self,\n    ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:\n        \"\"\"(Optional) Columns to z-order by. Compaction is performed when no z-order columns are provided.\"\"\"\n    def __init__(\n        self,\n        *,\n        table: delta.connect.proto.base_pb2.DeltaTable | None = ...,\n        partition_filters: collections.abc.Iterable[builtins.str] | None = ...,\n        zorder_columns: collections.abc.Iterable[builtins.str] | None = ...,\n    ) -> None: ...\n    def HasField(\n        self, field_name: typing_extensions.Literal[\"table\", b\"table\"]\n    ) -> builtins.bool: ...\n    def ClearField(\n        self,\n        field_name: typing_extensions.Literal[\n            \"partition_filters\",\n            b\"partition_filters\",\n            \"table\",\n            b\"table\",\n            \"zorder_columns\",\n            b\"zorder_columns\",\n        ],\n    ) -> None: ...\n\nglobal___OptimizeTable = OptimizeTable\n"
  },
  {
    "path": "python/delta/connect/tables.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    Dict,\n    Iterable,\n    List,\n    NoReturn,\n    Optional,\n    Tuple,\n    Union,\n    overload\n)\n\nfrom delta.connect._typing import (\n    ColumnMapping,\n    OptionalColumnMapping,\n    ExpressionOrColumn,\n    OptionalExpressionOrColumn\n)\nfrom delta.connect.plan import (\n    AddFeatureSupport,\n    Assignment,\n    CloneTable,\n    ConvertToDelta,\n    CreateDeltaTable,\n    DeleteAction,\n    DeleteFromTable,\n    DeltaScan,\n    DescribeHistory,\n    DescribeDetail,\n    DropFeatureSupport,\n    Generate,\n    InsertAction,\n    InsertStarAction,\n    IsDeltaTable,\n    MergeIntoTable,\n    OptimizeTable,\n    RestoreTable,\n    UpdateAction,\n    UpdateStarAction,\n    UpdateTable,\n    UpgradeTableProtocol,\n    Vacuum,\n)\nimport delta.connect.proto as proto\nfrom delta.tables import (\n    DeltaTable as LocalDeltaTable,\n    DeltaTableBuilder as LocalDeltaTableBuilder,\n    DeltaMergeBuilder as LocalDeltaMergeBuilder,\n    DeltaOptimizeBuilder as LocalDeltaOptimizeBuilder,\n    IdentityGenerator,\n)\n\nfrom pyspark.sql.connect import functions\nfrom pyspark.sql.connect.column import Column\nfrom pyspark.sql.connect.dataframe import DataFrame\nfrom pyspark.sql.connect.plan import LogicalPlan, SubqueryAlias\nfrom pyspark.sql.connect.session import SparkSession\nfrom pyspark.sql.connect.types import pyspark_types_to_proto_types\nfrom pyspark.sql.types import DataType, StructField, StructType\n\n\nclass DeltaTable(object):\n    __doc__ = LocalDeltaTable.__doc__\n\n    def __init__(\n        self,\n        spark: SparkSession,\n        path: Optional[str] = None,\n        tableOrViewName: Optional[str] = None,\n        hadoopConf: Dict[str, str] = dict(),\n        plan: Optional[LogicalPlan] = None\n    ) -> None:\n        self._spark = spark\n        self._path = path\n        self._tableOrViewName = tableOrViewName\n        self._hadoopConf = hadoopConf\n        if plan is not None:\n            self._plan = plan\n        else:\n            self._plan = DeltaScan(self._to_proto())\n\n    def toDF(self) -> DataFrame:\n        return DataFrame(self._plan, session=self._spark)\n\n    toDF.__doc__ = LocalDeltaTable.toDF.__doc__\n\n    def alias(self, aliasName: str) -> \"DeltaTable\":\n        return DeltaTable(\n            self._spark,\n            self._path,\n            self._tableOrViewName,\n            self._hadoopConf,\n            SubqueryAlias(self._plan, aliasName)\n        )\n\n    alias.__doc__ = LocalDeltaTable.alias.__doc__\n\n    def generate(self, mode: str) -> None:\n        command = Generate(self._to_proto(), mode).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n\n    generate.__doc__ = LocalDeltaTable.generate.__doc__\n\n    def delete(self, condition: OptionalExpressionOrColumn = None) -> DataFrame:\n        plan = DeleteFromTable(\n            self._plan,\n            DeltaTable._condition_to_column(condition)\n        )\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    delete.__doc__ = LocalDeltaTable.delete.__doc__\n\n    @overload\n    def update(\n        self, condition: ExpressionOrColumn, set: ColumnMapping\n    ) -> None:\n        ...\n\n    @overload\n    def update(self, *, set: ColumnMapping) -> None:\n        ...\n\n    def update(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        set: OptionalColumnMapping = None\n    ) -> DataFrame:\n        assignments = DeltaTable._dict_to_assignments(set, \"'set'\")\n        condition = DeltaTable._condition_to_column(condition)\n        plan = UpdateTable(\n            self._plan,\n            condition,\n            assignments\n        )\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    update.__doc__ = LocalDeltaTable.update.__doc__\n\n    def merge(\n        self, source: DataFrame, condition: ExpressionOrColumn\n    ) -> \"DeltaMergeBuilder\":\n        if source is None:\n            raise ValueError(\"'source' in merge cannot be None\")\n        elif not isinstance(source, DataFrame):\n            raise TypeError(\"Type of 'source' in merge must be DataFrame. {}\".format(type(source)))\n        if condition is None:\n            raise ValueError(\"'condition' in merge cannot be None\")\n\n        return DeltaMergeBuilder(\n            self._spark,\n            self._plan,\n            source._plan,\n            DeltaTable._condition_to_column(condition))\n\n    merge.__doc__ = LocalDeltaTable.merge.__doc__\n\n    def vacuum(self, retentionHours: Optional[float] = None) -> DataFrame:\n        command = Vacuum(self._to_proto(), retentionHours).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n        return None  # TODO: Return empty DataFrame\n\n    vacuum.__doc__ = LocalDeltaTable.vacuum.__doc__\n\n    def history(self, limit: Optional[int] = None) -> DataFrame:\n        df = DataFrame(DescribeHistory(self._to_proto()), session=self._spark)\n        if limit is not None:\n            df = df.limit(limit)\n        return df\n\n    history.__doc__ = LocalDeltaTable.history.__doc__\n\n    def detail(self) -> DataFrame:\n        return DataFrame(DescribeDetail(self._to_proto()), session=self._spark)\n\n    detail.__doc__ = LocalDeltaTable.detail.__doc__\n\n    @classmethod\n    def convertToDelta(\n        cls,\n        sparkSession: SparkSession,\n        identifier: str,\n        partitionSchema: Optional[Union[str, StructType]] = None,\n    ) -> \"DeltaTable\":\n        assert sparkSession is not None\n\n        pdf = DataFrame(\n            ConvertToDelta(identifier, partitionSchema),\n            session=sparkSession\n        ).toPandas()\n        identifier = pdf.iloc[0].iloc[0]\n\n        return DeltaTable.forName(sparkSession, identifier)\n\n    convertToDelta.__func__.__doc__ = LocalDeltaTable.convertToDelta.__doc__\n\n    @classmethod\n    def forPath(\n        cls,\n        sparkSession: SparkSession,\n        path: str,\n        hadoopConf: Dict[str, str] = dict()\n    ) -> \"DeltaTable\":\n        assert sparkSession is not None\n        return DeltaTable(sparkSession, path=path, hadoopConf=hadoopConf)\n\n    forPath.__func__.__doc__ = LocalDeltaTable.forPath.__doc__\n\n    @classmethod\n    def forName(\n        cls, sparkSession: SparkSession, tableOrViewName: str\n    ) -> \"DeltaTable\":\n        assert sparkSession is not None\n        return DeltaTable(sparkSession, tableOrViewName=tableOrViewName)\n\n    forName.__func__.__doc__ = LocalDeltaTable.forName.__doc__\n\n    @classmethod\n    def create(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        return DeltaTableBuilder(\n            sparkSession,\n            proto.CreateDeltaTable.Mode.MODE_CREATE)\n\n    create.__func__.__doc__ = LocalDeltaTable.create.__doc__\n\n    @classmethod\n    def createIfNotExists(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        return DeltaTableBuilder(\n            sparkSession,\n            proto.CreateDeltaTable.Mode.MODE_CREATE_IF_NOT_EXISTS)\n\n    createIfNotExists.__func__.__doc__ = LocalDeltaTable.createIfNotExists.__doc__\n\n    @classmethod\n    def replace(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        return DeltaTableBuilder(\n            sparkSession,\n            proto.CreateDeltaTable.Mode.MODE_REPLACE)\n\n    replace.__func__.__doc__ = LocalDeltaTable.replace.__doc__\n\n    @classmethod\n    def createOrReplace(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        return DeltaTableBuilder(\n            sparkSession,\n            proto.CreateDeltaTable.Mode.MODE_CREATE_OR_REPLACE)\n\n    createOrReplace.__func__.__doc__ = LocalDeltaTable.createOrReplace.__doc__\n\n    @classmethod\n    def isDeltaTable(cls, sparkSession: SparkSession, identifier: str) -> bool:\n        assert sparkSession is not None\n\n        pdf = DataFrame(\n            IsDeltaTable(identifier),\n            session=sparkSession\n        ).toPandas()\n        return pdf.iloc[0].iloc[0]\n\n    isDeltaTable.__func__.__doc__ = LocalDeltaTable.isDeltaTable.__doc__\n\n    def upgradeTableProtocol(self, readerVersion: int, writerVersion: int) -> None:\n        if not isinstance(readerVersion, int):\n            raise ValueError(\"The readerVersion needs to be an integer but got '%s'.\" %\n                             type(readerVersion))\n        if not isinstance(writerVersion, int):\n            raise ValueError(\"The writerVersion needs to be an integer but got '%s'.\" %\n                             type(writerVersion))\n        command = UpgradeTableProtocol(\n            self._to_proto(),\n            readerVersion,\n            writerVersion\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n\n    upgradeTableProtocol.__doc__ = LocalDeltaTable.upgradeTableProtocol.__doc__\n\n    def addFeatureSupport(self, featureName: str) -> None:\n        LocalDeltaTable._verify_type_str(featureName, \"featureName\")\n        command = AddFeatureSupport(\n            self._to_proto(),\n            featureName\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n\n    addFeatureSupport.__doc__ = LocalDeltaTable.addFeatureSupport.__doc__\n\n    def dropFeatureSupport(self, featureName: str, truncateHistory: Optional[bool] = None) -> None:\n        LocalDeltaTable._verify_type_str(featureName, \"featureName\")\n        if truncateHistory is not None:\n            LocalDeltaTable._verify_type_bool(truncateHistory, \"truncateHistory\")\n        command = DropFeatureSupport(\n            self._to_proto(),\n            featureName,\n            truncateHistory\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n\n    dropFeatureSupport.__doc__ = LocalDeltaTable.dropFeatureSupport.__doc__\n\n    def restoreToVersion(self, version: int) -> DataFrame:\n        LocalDeltaTable._verify_type_int(version, \"version\")\n        plan = RestoreTable(self._to_proto(), version=version)\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    restoreToVersion.__doc__ = LocalDeltaTable.restoreToVersion.__doc__\n\n    def restoreToTimestamp(self, timestamp: str) -> DataFrame:\n        LocalDeltaTable._verify_type_str(timestamp, \"timestamp\")\n        plan = RestoreTable(self._to_proto(), timestamp=timestamp)\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    restoreToTimestamp.__doc__ = LocalDeltaTable.restoreToTimestamp.__doc__\n\n    def optimize(self) -> \"DeltaOptimizeBuilder\":\n        return DeltaOptimizeBuilder(self._spark, self)\n\n    optimize.__doc__ = LocalDeltaTable.optimize.__doc__\n\n    def clone(\n        self,\n        target: str,\n        isShallow: bool = False,\n        replace: bool = False,\n        properties: Optional[Dict[str, str]] = None\n    ) -> \"DeltaTable\":\n        LocalDeltaTable._verify_clone_types(target, isShallow, replace, properties)\n        command = CloneTable(\n            self._to_proto(),\n            target,\n            isShallow,\n            replace,\n            properties\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n        return DeltaTable.forName(self._spark, target)\n\n    clone.__doc__ = LocalDeltaTable.clone.__doc__\n\n    def cloneAtVersion(\n        self,\n        version: int,\n        target: str,\n        isShallow: bool = False,\n        replace: bool = False,\n        properties: Optional[Dict[str, str]] = None\n    ) -> \"DeltaTable\":\n        LocalDeltaTable._verify_clone_types(target, isShallow, replace, properties, version=version)\n        command = CloneTable(\n            self._to_proto(),\n            target,\n            isShallow,\n            replace,\n            properties,\n            version=version\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n        return DeltaTable.forName(self._spark, target)\n\n    cloneAtVersion.__doc__ = LocalDeltaTable.cloneAtVersion.__doc__\n\n    def cloneAtTimestamp(\n        self,\n        timestamp: str,\n        target: str,\n        isShallow: bool = False,\n        replace: bool = False,\n        properties: Optional[Dict[str, str]] = None\n    ) -> \"DeltaTable\":\n        LocalDeltaTable._verify_clone_types(target, isShallow, replace, properties, timestamp)\n        command = CloneTable(\n            self._to_proto(),\n            target,\n            isShallow,\n            replace,\n            properties,\n            timestamp=timestamp\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n        return DeltaTable.forName(self._spark, target)\n\n    cloneAtTimestamp.__doc__ = LocalDeltaTable.cloneAtTimestamp.__doc__\n\n    def _to_proto(self) -> proto.DeltaTable:\n        result = proto.DeltaTable()\n        if self._path is not None:\n            result.path.path = self._path\n        if self._tableOrViewName is not None:\n            result.table_or_view_name = self._tableOrViewName\n        return result\n\n    @staticmethod\n    def _dict_to_assignments(\n        mapping: OptionalColumnMapping,\n        argname: str,\n    ) -> Optional[List[Assignment]]:\n        if mapping is None:\n            raise ValueError(\"%s cannot be None\" % argname)\n        elif type(mapping) is not dict:\n            e = \"%s must be a dict, found to be %s\" % (argname, str(type(dict)))\n            raise TypeError(e)\n\n        result = []\n        for col, expr in mapping.items():\n            if type(col) is not str:\n                e = (\"Keys of dict in %s must contain only strings with column names\" % argname) + \\\n                    (\", found '%s' of type '%s\" % (str(col), str(type(col))))\n                raise TypeError(e)\n            field = functions.col(col)\n\n            if isinstance(expr, Column):\n                value = expr\n            elif isinstance(expr, str):\n                value = functions.expr(expr)\n            else:\n                e = (\"Values of dict in %s must contain only Spark SQL Columns \" % argname) + \\\n                    \"or strings (expressions in SQL syntax) as values, \" + \\\n                    (\"found '%s' of type '%s'\" % (str(expr), str(type(expr))))\n                raise TypeError(e)\n            result.append(Assignment(field, value))\n\n        return result\n\n    @staticmethod\n    def _condition_to_column(\n        condition: OptionalExpressionOrColumn, argname: str = \"'condition'\"\n    ) -> Column:\n        if condition is None:\n            result = None\n        elif isinstance(condition, Column):\n            result = condition\n        elif isinstance(condition, str):\n            result = functions.expr(condition)\n        else:\n            e = (\"%s must be a Spark SQL Column or a string (expression in SQL syntax)\" % argname) \\\n                + \", found to be of type %s\" % str(type(condition))\n            raise TypeError(e)\n        return result\n\n\nclass DeltaMergeBuilder(object):\n    __doc__ = LocalDeltaMergeBuilder.__doc__\n\n    def __init__(\n        self,\n        spark: SparkSession,\n        target: LogicalPlan,\n        source: LogicalPlan,\n        condition: ExpressionOrColumn\n    ) -> None:\n        self._spark = spark\n        self._target = target\n        self._source = source\n        self._condition = condition\n        self._matchedActions = []\n        self._notMatchedActions = []\n        self._notMatchedBySourceActions = []\n        self._with_schema_evolution = False\n\n    @overload\n    def whenMatchedUpdate(\n        self, condition: OptionalExpressionOrColumn, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    @overload\n    def whenMatchedUpdate(\n        self, *, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    def whenMatchedUpdate(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        set: OptionalColumnMapping = None\n    ) -> \"DeltaMergeBuilder\":\n        assignments = DeltaTable._dict_to_assignments(set, \"'set' in whenMatchedUpdate\")\n        condition = DeltaTable._condition_to_column(condition)\n        self._matchedActions.append(UpdateAction(condition, assignments))\n        return self\n\n    whenMatchedUpdate.__doc__ = LocalDeltaMergeBuilder.whenMatchedUpdate.__doc__\n\n    def whenMatchedUpdateAll(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        self._matchedActions.append(UpdateStarAction(DeltaTable._condition_to_column(condition)))\n        return self\n\n    whenMatchedUpdateAll.__doc__ = LocalDeltaMergeBuilder.whenMatchedUpdateAll.__doc__\n\n    def whenMatchedDelete(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        self._matchedActions.append(DeleteAction(DeltaTable._condition_to_column(condition)))\n        return self\n\n    whenMatchedDelete.__doc__ = LocalDeltaMergeBuilder.whenMatchedDelete.__doc__\n\n    @overload\n    def whenNotMatchedInsert(\n        self, condition: ExpressionOrColumn, values: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    @overload\n    def whenNotMatchedInsert(\n        self, *, values: ColumnMapping = ...\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    def whenNotMatchedInsert(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        values: OptionalColumnMapping = None\n    ) -> \"DeltaMergeBuilder\":\n        assignments = DeltaTable._dict_to_assignments(values, \"'values' in whenNotMatchedInsert\")\n        condition = DeltaTable._condition_to_column(condition)\n        self._notMatchedActions.append(InsertAction(condition, assignments))\n        return self\n\n    whenNotMatchedInsert.__doc__ = LocalDeltaMergeBuilder.whenNotMatchedInsert.__doc__\n\n    def whenNotMatchedInsertAll(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        self._notMatchedActions.append(\n            InsertStarAction(DeltaTable._condition_to_column(condition))\n        )\n        return self\n\n    whenNotMatchedInsertAll.__doc__ = LocalDeltaMergeBuilder.whenNotMatchedInsertAll.__doc__\n\n    @overload\n    def whenNotMatchedBySourceUpdate(\n        self, condition: OptionalExpressionOrColumn, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    @overload\n    def whenNotMatchedBySourceUpdate(\n        self, *, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    def whenNotMatchedBySourceUpdate(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        set: OptionalColumnMapping = None\n    ) -> \"DeltaMergeBuilder\":\n        assignments = DeltaTable._dict_to_assignments(set, \"'set' in whenNotMatchedBySourceUpdate\")\n        condition = DeltaTable._condition_to_column(condition)\n        self._notMatchedBySourceActions.append(UpdateAction(condition, assignments))\n        return self\n\n    whenNotMatchedBySourceUpdate.__doc__ = LocalDeltaMergeBuilder.whenNotMatchedBySourceUpdate.__doc__\n\n    def whenNotMatchedBySourceDelete(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        action = DeleteAction(DeltaTable._condition_to_column(condition))\n        self._notMatchedBySourceActions.append(action)\n        return self\n\n    whenNotMatchedBySourceDelete.__doc__ = LocalDeltaMergeBuilder.whenNotMatchedBySourceDelete.__doc__\n\n    def withSchemaEvolution(self) -> \"DeltaMergeBuilder\":\n        self._with_schema_evolution = True\n        return self\n\n    def execute(self) -> DataFrame:\n        plan = MergeIntoTable(\n            self._target,\n            self._source,\n            self._condition,\n            self._matchedActions,\n            self._notMatchedActions,\n            self._notMatchedBySourceActions,\n            self._with_schema_evolution\n        )\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    execute.__doc__ = LocalDeltaMergeBuilder.execute.__doc__\n\n\nclass DeltaTableBuilder(object):\n    __doc__ = LocalDeltaTableBuilder.__doc__\n\n    def __init__(\n        self,\n        spark: SparkSession,\n        mode: proto.CreateDeltaTable.Mode\n    ) -> None:\n        self._spark = spark\n        self._mode = mode\n        self._tableName = None\n        self._location = None\n        self._comment = None\n        self._columns = []\n        self._properties = {}\n        self._partitioningColumns = []\n        self._clusteringColumns = []\n\n    def _raise_type_error(self, msg: str, objs: Iterable[Any]) -> NoReturn:\n        errorMsg = msg\n        for obj in objs:\n            errorMsg += \" Found %s with type %s\" % ((str(obj)), str(type(obj)))\n        raise TypeError(errorMsg)\n\n    def _check_identity_column_spec(self, identityGenerator: IdentityGenerator) -> None:\n        if identityGenerator.step == 0:\n            raise ValueError(\"Column identity generation requires step to be non-zero.\")\n\n    def tableName(self, identifier: str) -> \"DeltaTableBuilder\":\n        if type(identifier) is not str:\n            self._raise_type_error(\"Identifier must be str.\", [identifier])\n        self._tableName = identifier\n        return self\n\n    tableName.__doc__ = LocalDeltaTableBuilder.tableName.__doc__\n\n    def location(self, location: str) -> \"DeltaTableBuilder\":\n        if type(location) is not str:\n            self._raise_type_error(\"Location must be str.\", [location])\n        self._location = location\n        return self\n\n    location.__doc__ = LocalDeltaTableBuilder.location.__doc__\n\n    def comment(self, comment: str) -> \"DeltaTableBuilder\":\n        if type(comment) is not str:\n            self._raise_type_error(\"Table comment must be str.\", [comment])\n        self._comment = comment\n        return self\n\n    comment.__doc__ = LocalDeltaTableBuilder.comment.__doc__\n\n    def addColumn(\n        self,\n        colName: str,\n        dataType: Union[str, DataType],\n        nullable: bool = True,\n        generatedAlwaysAs: Optional[Union[str, IdentityGenerator]] = None,\n        generatedByDefaultAs: Optional[IdentityGenerator] = None,\n        comment: Optional[str] = None,\n    ) -> \"DeltaTableBuilder\":\n        if type(colName) is not str:\n            self._raise_type_error(\"Column name must be str.\", [colName])\n        if type(dataType) is not str and not isinstance(dataType, DataType):\n            self._raise_type_error(\n                \"Column data type must be str or DataType.\", [dataType])\n        if type(nullable) is not bool:\n            self._raise_type_error(\"Column nullable must be bool.\", [nullable])\n        if generatedAlwaysAs is not None and generatedByDefaultAs is not None:\n            raise ValueError(\n                \"generatedByDefaultAs and generatedAlwaysAs cannot both be set.\",\n                [generatedByDefaultAs, generatedAlwaysAs])\n        if generatedAlwaysAs is not None:\n            if isinstance(generatedAlwaysAs, IdentityGenerator):\n                self._check_identity_column_spec(generatedAlwaysAs)\n            elif type(generatedAlwaysAs) is not str:\n                self._raise_type_error(\n                    \"Generated always as expression must be str or IdentityGenerator.\",\n                    [generatedAlwaysAs])\n        elif generatedByDefaultAs is not None:\n            if not isinstance(generatedByDefaultAs, IdentityGenerator):\n                self._raise_type_error(\n                    \"Generated by default expression must be IdentityGenerator.\",\n                    [generatedByDefaultAs])\n            self._check_identity_column_spec(generatedByDefaultAs)\n\n        if comment is not None and type(comment) is not str:\n            self._raise_type_error(\"Comment must be str or None.\", [colName])\n\n        column = proto.CreateDeltaTable.Column()\n        column.name = colName\n        if type(dataType) is str:\n            column.data_type.unparsed.data_type_string = dataType\n        elif isinstance(dataType, DataType):\n            column.data_type.CopyFrom(pyspark_types_to_proto_types(dataType))\n        column.nullable = nullable\n        if generatedAlwaysAs is not None:\n            if type(generatedAlwaysAs) is str:\n                column.generated_always_as = generatedAlwaysAs\n            else:\n                identity_info = proto.CreateDeltaTable.Column.IdentityInfo(\n                    start=generatedAlwaysAs.start,\n                    step=generatedAlwaysAs.step,\n                    allow_explicit_insert=False)\n                column.identity_info.CopyFrom(identity_info)\n        if generatedByDefaultAs is not None:\n            identity_info = proto.CreateDeltaTable.Column.IdentityInfo(\n                start=generatedByDefaultAs.start,\n                step=generatedByDefaultAs.step,\n                allow_explicit_insert=True)\n            column.identity_info.CopyFrom(identity_info)\n        if comment is not None:\n            column.comment = comment\n        self._columns.append(column)\n        return self\n\n    addColumn.__doc__ = LocalDeltaTableBuilder.addColumn.__doc__\n\n    def addColumns(\n        self, cols: Union[StructType, List[StructField]]\n    ) -> \"DeltaTableBuilder\":\n        if isinstance(cols, list):\n            for col in cols:\n                if type(col) is not StructField:\n                    self._raise_type_error(\n                        \"Column in existing schema must be StructField.\", [col])\n            cols = StructType(cols)\n        if type(cols) is not StructType:\n            self._raise_type_error(\n                \"Schema must be StructType or a list of StructField.\", [cols])\n\n        for col in cols:\n            self.addColumn(col.name, col.dataType, col.nullable)\n        return self\n\n    addColumns.__doc__ = LocalDeltaTableBuilder.addColumns.__doc__\n\n    @overload\n    def partitionedBy(\n        self, *cols: str\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    @overload\n    def partitionedBy(\n        self, __cols: Union[List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    def partitionedBy(\n        self, *cols: Union[str, List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        if len(cols) == 1 and isinstance(cols[0], (list, tuple)):\n            cols = cols[0]  # type: ignore[assignment]\n        for c in cols:\n            if type(c) is not str:\n                self._raise_type_error(\"Partitioning column must be str.\", [c])\n\n        self._partitioningColumns.extend(cols)\n        return self\n\n    partitionedBy.__doc__ = LocalDeltaTableBuilder.partitionedBy.__doc__\n\n    @overload\n    def clusterBy(\n        self, *cols: str\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    @overload\n    def clusterBy(\n        self, __cols: Union[List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    def clusterBy(\n        self, *cols: Union[str, List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        if len(cols) == 1 and isinstance(cols[0], (list, tuple)):\n            cols = cols[0]  # type: ignore[assignment]\n        for c in cols:\n            if type(c) is not str:\n                self._raise_type_error(\"Clustering column must be str.\", [c])\n\n        self._clusteringColumns.extend(cols)\n        return self\n\n    clusterBy.__doc__ = LocalDeltaTableBuilder.clusterBy.__doc__\n\n    def property(self, key: str, value: str) -> \"DeltaTableBuilder\":\n        if type(key) is not str or type(value) is not str:\n            self._raise_type_error(\n                \"Key and value of property must be string.\", [key, value])\n\n        self._properties[key] = value\n        return self\n\n    property.__doc__ = LocalDeltaTableBuilder.property.__doc__\n\n    def execute(self) -> DeltaTable:\n        command = CreateDeltaTable(\n            self._mode,\n            self._tableName,\n            self._location,\n            self._comment,\n            self._columns,\n            self._partitioningColumns,\n            self._properties,\n            self._clusteringColumns\n        ).command(session=self._spark.client)\n        self._spark.client.execute_command(command)\n        if self._tableName is not None:\n            return DeltaTable.forName(self._spark, self._tableName)\n        else:\n            return DeltaTable.forPath(self._spark, self._location)\n\n    execute.__doc__ = LocalDeltaTableBuilder.execute.__doc__\n\n\nclass DeltaOptimizeBuilder(object):\n    __doc__ = LocalDeltaOptimizeBuilder.__doc__\n\n    def __init__(self, spark: SparkSession, table: \"DeltaTable\"):\n        self._spark = spark\n        self._table = table\n        self._partitionFilters = []\n\n    def where(self, partitionFilter: str) -> \"DeltaOptimizeBuilder\":\n        self._partitionFilters.append(partitionFilter)\n        return self\n\n    where.__doc__ = LocalDeltaOptimizeBuilder.where.__doc__\n\n    def executeCompaction(self) -> DataFrame:\n        plan = OptimizeTable(self._table._to_proto(), self._partitionFilters, [])\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    executeCompaction.__doc__ = LocalDeltaOptimizeBuilder.executeCompaction.__doc__\n\n    def executeZOrderBy(self, *cols: Union[str, List[str], Tuple[str, ...]]) -> DataFrame:\n        if len(cols) == 1 and isinstance(cols[0], (list, tuple)):\n            cols = cols[0]  # type: ignore[assignment]\n        for c in cols:\n            if type(c) is not str:\n                errorMsg = \"Z-order column must be str. \"\n                errorMsg += \"Found %s with type %s\" % ((str(c)), str(type(c)))\n                raise TypeError(errorMsg)\n\n        plan = OptimizeTable(self._table._to_proto(), self._partitionFilters, cols)\n        df = DataFrame(plan, session=self._spark)\n        return self._spark.createDataFrame(df.toPandas())\n\n    executeZOrderBy.__doc__ = LocalDeltaOptimizeBuilder.executeZOrderBy.__doc__\n"
  },
  {
    "path": "python/delta/connect/testing/__init__.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "python/delta/connect/testing/utils.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport tempfile\nimport shutil\nimport os\nimport uuid\n\nfrom contextlib import contextmanager\nfrom pyspark import SparkConf\nfrom pyspark.testing.connectutils import ReusedConnectTestCase\nfrom typing import Generator\n\n\nclass DeltaTestCase(ReusedConnectTestCase):\n    \"\"\"\n    Test suite base for setting up a properly configured SparkSession for using Delta Connect.\n    \"\"\"\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        # Spark Connect will set SPARK_CONNECT_TESTING_REMOTE, and it does not allow MASTER\n        # to be set simultaneously, so we need to clear it.\n        # TODO(long.vu): Find a cleaner way to clear \"MASTER\".\n        if \"MASTER\" in os.environ:\n            del os.environ[\"MASTER\"]\n        super(DeltaTestCase, cls).setUpClass()\n\n    @classmethod\n    def conf(cls) -> SparkConf:\n        _conf = super(DeltaTestCase, cls).conf()\n        _conf.set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n        _conf.set(\"spark.sql.catalog.spark_catalog\",\n                  \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n        _conf.set(\"spark.connect.extensions.relation.classes\",\n                  \"org.apache.spark.sql.connect.delta.DeltaRelationPlugin\")\n        _conf.set(\"spark.connect.extensions.command.classes\",\n                  \"org.apache.spark.sql.connect.delta.DeltaCommandPlugin\")\n        return _conf\n\n    def setUp(self) -> None:\n        super(DeltaTestCase, self).setUp()\n        self.tempPath = tempfile.mkdtemp()\n        self.tempFile = os.path.join(self.tempPath, \"tempFile\")\n\n    def tearDown(self) -> None:\n        super(DeltaTestCase, self).tearDown()\n        shutil.rmtree(self.tempPath)\n\n    @contextmanager\n    def tempTable(self) -> Generator[str, None, None]:\n        table_name = \"table_\" + str(uuid.uuid4()).replace(\"-\", \"_\")\n\n        with super(DeltaTestCase, self).table(table_name):\n            yield table_name\n"
  },
  {
    "path": "python/delta/connect/tests/__init__.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "python/delta/connect/tests/test_deltatable.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport unittest\nimport sys\n\nfrom delta.connect.testing.utils import DeltaTestCase\n\npath_to_delta_connect_tests_folder = os.path.dirname(os.path.abspath(__file__))\npath_to_delta_folder = os.path.dirname(os.path.dirname(path_to_delta_connect_tests_folder))\nsys.path.append(path_to_delta_folder)\n\nfrom tests.test_deltatable import DeltaTableTestsMixin\n\n\nclass DeltaTableTests(DeltaTableTestsMixin, DeltaTestCase):\n    @unittest.skip(\"relies on jvm\")\n    def test_verify_paritionedBy_compatibility(self):\n        pass\n\n\nif __name__ == \"__main__\":\n    try:\n        import xmlrunner\n        testRunner = xmlrunner.XMLTestRunner(output='target/test-reports', verbosity=4)\n    except ImportError:\n        testRunner = None\n    unittest.main(testRunner=testRunner, verbosity=4)\n"
  },
  {
    "path": "python/delta/exceptions/__init__.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom delta.exceptions.base import (\n    DeltaConcurrentModificationException,\n    ConcurrentWriteException,\n    MetadataChangedException,\n    ProtocolChangedException,\n    ConcurrentAppendException,\n    ConcurrentDeleteReadException,\n    ConcurrentDeleteDeleteException,\n    ConcurrentTransactionException,\n)\n\n__all__ = [\n    \"DeltaConcurrentModificationException\",\n    \"ConcurrentWriteException\",\n    \"MetadataChangedException\",\n    \"ProtocolChangedException\",\n    \"ConcurrentAppendException\",\n    \"ConcurrentDeleteReadException\",\n    \"ConcurrentDeleteDeleteException\",\n    \"ConcurrentTransactionException\",\n]\n"
  },
  {
    "path": "python/delta/exceptions/base.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom pyspark.errors.exceptions.base import PySparkException\n\n\nclass DeltaConcurrentModificationException(PySparkException):\n    \"\"\"\n    The basic class for all Delta commit conflict exceptions.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentWriteException(PySparkException):\n    \"\"\"\n    Thrown when a concurrent transaction has written data after the current transaction read the\n    table.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass MetadataChangedException(PySparkException):\n    \"\"\"\n    Thrown when the metadata of the Delta table has changed between the time of read\n    and the time of commit.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ProtocolChangedException(PySparkException):\n    \"\"\"\n    Thrown when the protocol version has changed between the time of read\n    and the time of commit.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentAppendException(PySparkException):\n    \"\"\"\n    Thrown when files are added that would have been read by the current transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentDeleteReadException(PySparkException):\n    \"\"\"\n    Thrown when the current transaction reads data that was deleted by a concurrent transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentDeleteDeleteException(PySparkException):\n    \"\"\"\n    Thrown when the current transaction deletes data that was deleted by a concurrent transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentTransactionException(PySparkException):\n    \"\"\"\n    Thrown when concurrent transaction both attempt to update the same idempotent transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n"
  },
  {
    "path": "python/delta/exceptions/captured.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pyspark import SparkContext\nfrom pyspark.errors.exceptions import captured\nfrom pyspark.errors.exceptions.captured import CapturedException\n\nfrom delta.exceptions.base import (\n    DeltaConcurrentModificationException as BaseDeltaConcurrentModificationException,\n    ConcurrentWriteException as BaseConcurrentWriteException,\n    MetadataChangedException as BaseMetadataChangedException,\n    ProtocolChangedException as BaseProtocolChangedException,\n    ConcurrentAppendException as BaseConcurrentAppendException,\n    ConcurrentDeleteReadException as BaseConcurrentDeleteReadException,\n    ConcurrentDeleteDeleteException as BaseConcurrentDeleteDeleteException,\n    ConcurrentTransactionException as BaseConcurrentTransactionException,\n)\n\nif TYPE_CHECKING:\n    from py4j.java_gateway import JavaObject, JVMView  # type: ignore[import]\n\n\nclass DeltaConcurrentModificationException(\n    CapturedException, BaseDeltaConcurrentModificationException\n):\n    \"\"\"\n    The basic class for all Delta commit conflict exceptions.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentWriteException(CapturedException, BaseConcurrentWriteException):\n    \"\"\"\n    Thrown when a concurrent transaction has written data after the current transaction read the\n    table.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass MetadataChangedException(CapturedException, BaseMetadataChangedException):\n    \"\"\"\n    Thrown when the metadata of the Delta table has changed between the time of read\n    and the time of commit.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ProtocolChangedException(CapturedException, BaseProtocolChangedException):\n    \"\"\"\n    Thrown when the protocol version has changed between the time of read\n    and the time of commit.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentAppendException(CapturedException, BaseConcurrentAppendException):\n    \"\"\"\n    Thrown when files are added that would have been read by the current transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentDeleteReadException(CapturedException, BaseConcurrentDeleteReadException):\n    \"\"\"\n    Thrown when the current transaction reads data that was deleted by a concurrent transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentDeleteDeleteException(CapturedException, BaseConcurrentDeleteDeleteException):\n    \"\"\"\n    Thrown when the current transaction deletes data that was deleted by a concurrent transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\nclass ConcurrentTransactionException(CapturedException, BaseConcurrentTransactionException):\n    \"\"\"\n    Thrown when concurrent transaction both attempt to update the same idempotent transaction.\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n\n\n_delta_exception_patched = False\n\n\ndef _convert_delta_exception(e: \"JavaObject\") -> Optional[CapturedException]:\n    \"\"\"\n    Convert Delta's Scala concurrent exceptions to the corresponding Python exceptions.\n    \"\"\"\n    s: str = e.toString()\n    c: \"JavaObject\" = e.getCause()\n\n    jvm: \"JVMView\" = SparkContext._jvm  # type: ignore[attr-defined]\n    gw = SparkContext._gateway  # type: ignore[attr-defined]\n    stacktrace = jvm.org.apache.spark.util.Utils.exceptionString(e)\n\n    if s.startswith('io.delta.exceptions.DeltaConcurrentModificationException: '):\n        return DeltaConcurrentModificationException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.ConcurrentWriteException: '):\n        return ConcurrentWriteException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.MetadataChangedException: '):\n        return MetadataChangedException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.ProtocolChangedException: '):\n        return ProtocolChangedException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.ConcurrentAppendException: '):\n        return ConcurrentAppendException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.ConcurrentDeleteReadException: '):\n        return ConcurrentDeleteReadException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.ConcurrentDeleteDeleteException: '):\n        return ConcurrentDeleteDeleteException(s.split(': ', 1)[1], stacktrace, c)\n    if s.startswith('io.delta.exceptions.ConcurrentTransactionException: '):\n        return ConcurrentTransactionException(s.split(': ', 1)[1], stacktrace, c)\n    return None\n\n\ndef _patch_convert_exception() -> None:\n    \"\"\"\n    Patch PySpark's exception convert method to convert Delta's Scala concurrent exceptions to the\n    corresponding Python exceptions.\n    \"\"\"\n    original_convert_sql_exception = captured.convert_exception\n\n    def convert_delta_exception(e: \"JavaObject\") -> CapturedException:\n        delta_exception = _convert_delta_exception(e)\n        if delta_exception is not None:\n            return delta_exception\n        return original_convert_sql_exception(e)\n\n    captured.convert_exception = convert_delta_exception\n\n\nif not _delta_exception_patched:\n    _patch_convert_exception()\n    _delta_exception_patched = True\n"
  },
  {
    "path": "python/delta/integration_tests/unity-catalog-commit-coordinator-integration-tests.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport datetime\nimport os\nimport py4j\nimport unittest\n\nfrom delta.tables import DeltaTable\nfrom pyspark.errors.exceptions.captured import AnalysisException, UnsupportedOperationException\nfrom pyspark.sql import SparkSession, DataFrame\nfrom pyspark.sql.functions import lit\nfrom pyspark.sql.types import IntegerType, StructType, StructField\nfrom pyspark.testing import assertDataFrameEqual\n\n\"\"\"\nRun this script in root dir of repository:\n\n===== Mandatory input from user =====\nexport CATALOG_TOKEN=___\nexport CATALOG_URI=___\nexport CATALOG_NAME=___\nexport SCHEMA=___\nexport MANAGED_CC_TABLE=___\nexport MANAGED_NON_CC_TABLE=___\n\n./run-integration-tests.py --use-local --unity-catalog-commit-coordinator-integration-tests \\\n    --packages \\\n    io.unitycatalog:unitycatalog-spark_2.13:0.3.0,org.apache.spark:spark-hadoop-cloud_2.13:4.0.0\n\"\"\"\n\nCATALOG_NAME = os.environ.get(\"CATALOG_NAME\")\nCATALOG_TOKEN = os.environ.get(\"CATALOG_TOKEN\")\nCATALOG_URI = os.environ.get(\"CATALOG_URI\")\nMANAGED_CC_TABLE = os.environ.get(\"MANAGED_CC_TABLE\")\nSCHEMA = os.environ.get(\"SCHEMA\")\nMANAGED_NON_CC_TABLE = os.environ.get(\"MANAGED_NON_CC_TABLE\")\n\nspark = SparkSession \\\n    .builder \\\n    .appName(\"coordinated_commit_tester\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(f\"spark.sql.catalog.{CATALOG_NAME}\", \"io.unitycatalog.spark.UCSingleCatalog\") \\\n    .config(f\"spark.sql.catalog.{CATALOG_NAME}.token\", CATALOG_TOKEN) \\\n    .config(f\"spark.sql.catalog.{CATALOG_NAME}.uri\", CATALOG_URI) \\\n    .config(\"spark.databricks.delta.replaceWhere.constraintCheck.enabled\", True) \\\n    .config(\"spark.hadoop.fs.s3.impl\", \"org.apache.hadoop.fs.s3a.S3AFileSystem\") \\\n    .getOrCreate()\n\nMANAGED_CATALOG_OWNED_TABLE_FULL_NAME = f\"{CATALOG_NAME}.{SCHEMA}.{MANAGED_CC_TABLE}\"\nMANAGED_NON_CATALOG_OWNED_TABLE_FULL_NAME = f\"{CATALOG_NAME}.{SCHEMA}.{MANAGED_NON_CC_TABLE}\"\n\n\nclass UnityCatalogManagedTableTestBase(unittest.TestCase):\n    \"\"\"\n    Shared helpers and test setup for test suites below.\n    \"\"\"\n    setup_df = spark.createDataFrame([(1, ), (2, ), (3, )],\n                                     schema=StructType([StructField(\"id\", IntegerType(), True)]))\n\n    def setUp(self) -> None:\n        self.setup_df.write.mode(\"overwrite\").insertInto(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n\n    # Helper methods\n    def read(self, table_name: str) -> DataFrame:\n        return spark.read.table(table_name)\n\n    def current_version(self, table_name: str) -> int:\n        # Access the delta table's max version.\n        dt = DeltaTable.forName(spark, table_name)\n        return dt.history().selectExpr(\"max(version)\").collect()[0][0]\n\n    def read_with_cdf_timestamp(self, timestamp: str, table_name: str) -> DataFrame:\n        return spark.read.option('readChangeFeed', 'true').option(\n            \"startingTimestamp\", timestamp).table(table_name)\n\n    def read_with_cdf_version(self, version: int, table_name: str) -> DataFrame:\n        return spark.read.option('readChangeFeed', 'true').option(\n            \"startingVersion\", version).table(table_name)\n\n    def create_df_with_rows(self, list_of_rows: list) -> DataFrame:\n        return spark.createDataFrame(list_of_rows,\n                                     schema=StructType([StructField(\"id\", IntegerType(), True)]))\n\n    def get_table_history(self, table_name: str) -> DataFrame:\n        return spark.sql(f\"DESCRIBE HISTORY {table_name}\")\n\n    def append(self, table_name: str) -> None:\n        single_col_df = spark.createDataFrame(\n            [(4, ),  (5, )], schema=StructType([StructField(\"id\", IntegerType(), True)]))\n        single_col_df.writeTo(table_name).append()\n\n\nclass UnityCatalogManagedTableBasicSuite(UnityCatalogManagedTableTestBase):\n    \"\"\"\n    Suite covering basic functionality of catalog owned tables.\n    \"\"\"\n\n    def test_read_from_managed_table_without_catalog_owned(self) -> None:\n        self.read(MANAGED_NON_CATALOG_OWNED_TABLE_FULL_NAME)\n\n    def test_write_to_managed_catalog_owned_table(self) -> None:\n        self.append(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )]))\n\n    def test_read_from_managed_catalog_owned_table(self) -> None:\n        self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.setup_df)\n\n    # Writing to tables that are not catalog owned is not supported.\n    def test_write_to_managed_table_without_catalog_owned(self) -> None:\n        try:\n            self.append(MANAGED_NON_CATALOG_OWNED_TABLE_FULL_NAME)\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"[TASK_WRITE_FAILED] Task failed while writing rows to s3\" in str(error))\n\n    def test_unset_catalog_owned_feature(self) -> None:\n        try:\n            spark.sql(f\"ALTER TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                      f\"UNSET TBLPROPERTIES ('delta.feature.catalogManaged')\")\n        except UnsupportedOperationException as error:\n            assert(\"Altering a table is not supported yet\" in str(error))\n\n    def test_drop_catalog_owned_property(self) -> None:\n        try:\n            spark.sql(f\"ALTER TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                      f\"DROP FEATURE 'catalogManaged'\")\n        except UnsupportedOperationException as error:\n            assert(\"Altering a table is not supported yet\" in str(error))\n\n\nclass UnityCatalogManagedTableDMLSuite(UnityCatalogManagedTableTestBase):\n    \"\"\"\n    Suite covering DMLs (INSERT, MERGE, UPDATE, DELETE) on catalog owned tables.\n    \"\"\"\n\n    def test_update(self) -> None:\n        dt = DeltaTable.forName(spark, MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        dt.update(condition=\"id = 1\", set={\"id\": \"4\"})\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(4, ), (2, ), (3, )]))\n\n    def test_sql_update(self) -> None:\n        spark.sql(f\"UPDATE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} SET id=4 WHERE id=1\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(4, ), (2, ), (3, )]))\n\n    def test_delete(self) -> None:\n        dt = DeltaTable.forName(spark, MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        dt.delete(condition=\"id = 1\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(2, ), (3, )]))\n\n    def test_sql_delete(self) -> None:\n        spark.sql(f\"DELETE FROM {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} where id=1\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(2, ), (3, )]))\n\n    def test_merge(self) -> None:\n        dt = DeltaTable.forName(spark, MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        src = self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )])\n        dt.alias(\"target\") \\\n            .merge(\n                source=src.alias(\"src\"),\n                condition=\"src.id = target.id\") \\\n            .whenNotMatchedInsertAll() \\\n            .execute()\n\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )]))\n\n    def test_sql_merge(self) -> None:\n        spark.sql(f\"MERGE INTO {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} AS target \"\n                  f\"USING (VALUES 2, 3, 4, 5 AS src(id)) AS src \"\n                  f\"ON src.id = target.id WHEN NOT MATCHED THEN INSERT *\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )]))\n\n    def test_merge_schema_evolution(self) -> None:\n        spark.conf.set(\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")\n        try:\n            spark.sql(f\"MERGE INTO {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} AS target \"\n                      f\"USING (VALUES (2, 2), (3, 3), (4, 4), (5, 5) AS src(id, extra)) AS src \"\n                      f\"ON src.id = target.id WHEN NOT MATCHED THEN INSERT *\")\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\n                \"A table's Delta metadata can only be changed from a cluster or warehouse\"\n                in str(error)\n            )\n        finally:\n            spark.conf.unset(\"spark.databricks.delta.schema.autoMerge.enabled\")\n\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.setup_df)\n\n    def test_insert_schema_evolution(self) -> None:\n        two_cols_df = spark.createDataFrame([(4, 4), (5, 5)], schema=[\"id, extra\"])\n        try:\n            two_cols_df\\\n                .write.mode(\"append\").option(\"mergeSchema\", \"true\")\\\n                .insertInto(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\n                \"A table's Delta metadata can only be changed from a cluster or warehouse\"\n                in str(error)\n            )\n\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.setup_df)\n\n    def test_sql_insert(self) -> None:\n        spark.sql(f\"INSERT INTO {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                  f\"VALUES (4), (5)\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1,), (2,), (3,), (4,), (5,)]))\n\n    def test_sql_insert_overwrite(self) -> None:\n        spark.sql(f\"INSERT OVERWRITE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                  f\"VALUES (2), (3), (4), (5)\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(2,), (3,), (4,), (5,)]))\n\n    def test_sql_insert_replace_where(self) -> None:\n        spark.sql(f\"INSERT INTO {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                  f\"REPLACE WHERE id = 1 \"\n                  f\"VALUES (1)\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1,), (2,), (3,)]))\n\n    def test_sql_insert_dynamic_partition_overwrite(self) -> None:\n        spark.conf.set(\"spark.databricks.delta.dynamicPartitionOverwrite.enabled\", \"true\")\n        try:\n            spark.sql(f\"INSERT INTO {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} VALUES (5)\")\n            updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n            assertDataFrameEqual(updated_tbl,\n                                 self.create_df_with_rows([(1,), (2,), (3,), (5,)]))\n        finally:\n            spark.conf.unset(\"spark.databricks.delta.dynamicPartitionOverwrite.enabled\")\n\n    # Dataframe Writer V1 Tests #\n    def test_insert_into_append(self) -> None:\n        single_col_df = spark.createDataFrame([(4, ), (5, )], schema=[\"id\"])\n        single_col_df.write.mode(\"append\").insertInto(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )]))\n\n    def test_insert_into_overwrite(self) -> None:\n        single_col_df = spark.createDataFrame([(5, )], schema=[\"id\"])\n        single_col_df.write.mode(\"overwrite\").insertInto(\n            MANAGED_CATALOG_OWNED_TABLE_FULL_NAME, True)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(5, )]))\n\n    def test_insert_into_overwrite_replace_where(self) -> None:\n        single_col_df = spark.createDataFrame([(5, )], schema=[\"id\"])\n        single_col_df.write.mode(\"overwrite\").option(\"replaceWhere\", \"id > 1\").insertInto(\n            f\"{MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\", True)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(1, ), (5, )]))\n\n    def test_insert_into_overwrite_partition_overwrite(self) -> None:\n        single_col_df = spark.createDataFrame([(5,)], schema=[\"id\"])\n        single_col_df.write.mode(\"overwrite\").option(\n            \"partitionOverwriteMode\", \"dynamic\").insertInto(\n            MANAGED_CATALOG_OWNED_TABLE_FULL_NAME, True)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(5,)]))\n\n    def test_save_as_table_append_existing_table(self) -> None:\n        single_col_df = spark.createDataFrame(\n            [(4, ), (5, )], schema=StructType([StructField(\"id\", IntegerType(), True)]))\n        single_col_df.write.format(\"delta\").mode(\"append\").saveAsTable(\n            MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )]))\n\n    # Setting mode to append should work, however cc tables do not allow path based access.\n    def test_save_append_using_path(self) -> None:\n        single_col_df = spark.createDataFrame([(4, ), (5, )])\n        # Fetch managed table path and attempt to side-step UC\n        # and directly update table using path based access.\n        tbl_path = spark.sql(\n            f\"DESCRIBE formatted {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\").collect()[5].data_type\n        try:\n            single_col_df.write.format(\"delta\").save(mode=\"append\", path=tbl_path)\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"AccessDeniedException\" in str(error))\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.setup_df)\n\n    # DataFrame V2 Tests #\n    def test_append(self) -> None:\n        self.append(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl,\n                             self.create_df_with_rows([(1, ), (2, ), (3, ), (4, ), (5, )]))\n\n    def test_overwrite(self) -> None:\n        single_col_df = spark.createDataFrame(\n            [(5,)], schema=StructType([StructField(\"id\", IntegerType(), True)]))\n        single_col_df.writeTo(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).overwrite(lit(True))\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(5,)]))\n\n    def test_overwrite_partitions(self) -> None:\n        single_col_df = spark.createDataFrame(\n            [(5,)], schema=StructType([StructField(\"id\", IntegerType(), True)]))\n        single_col_df.writeTo(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).overwritePartitions()\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(5,)]))\n\n\nclass UnityCatalogManagedTableDDLSuite(UnityCatalogManagedTableTestBase):\n    \"\"\"\n    Suite covering DDLs (CREATE, REPLACE, CLONE, ALTER) on catalog owned tables.\n    \"\"\"\n\n    def test_create_non_delta(self) -> None:\n        single_col_df = spark.createDataFrame(\n            [(5,)], schema=StructType([StructField(\"id\", IntegerType(), True)]))\n        try:\n            # CREATE TABLE is currently not supported by UC.\n            single_col_df.writeTo(f\"{CATALOG_NAME}.{SCHEMA}.created_table\").create()\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"io.unitycatalog.spark.UCProxy.createTable\" in str(error))\n\n    def test_create_delta(self) -> None:\n        single_col_df = spark.createDataFrame(\n            [(5,)], schema=StructType([StructField(\"id\", IntegerType(), True)]))\n        try:\n            # CREATE TABLE is currently not supported by UC.\n            single_col_df.writeTo(f\"{CATALOG_NAME}.{SCHEMA}.created_table\").using(\"delta\").create()\n        except AnalysisException as error:\n            assert(\n                f\"[SCHEMA_NOT_FOUND] The schema `spark_catalog`.`{SCHEMA}` cannot be found\"\n                in str(error)\n            )\n\n    def test_sql_create(self) -> None:\n        try:\n            # This ignores the catalog name passed and tries to create the table under\n            # 'spark_catalog'.\n            spark.sql(f\"CREATE TABLE {CATALOG_NAME}.{SCHEMA}.created_table (a int) USING DELTA\")\n        except AnalysisException as error:\n            assert(\n                f\"[SCHEMA_NOT_FOUND] The schema `spark_catalog`.`{SCHEMA}` cannot be found\"\n                in str(error)\n            )\n\n    def test_create_non_catalog_owned(self) -> None:\n        try:\n            # This ignores the catalog name passed and tries to create the table under\n            # 'spark_catalog'.\n            spark.sql(f\"CREATE TABLE {CATALOG_NAME}.{SCHEMA}.created_table (id int) USING DELTA\")\n        except AnalysisException as error:\n            assert(\n                f\"[SCHEMA_NOT_FOUND] The schema `spark_catalog`.`{SCHEMA}` cannot be found\"\n                in str(error)\n            )\n\n    def test_clone_into_catalog_owned(self) -> None:\n        try:\n            # CLONE fails with an assertion error in UCSingleCatalog\n            spark.sql(f\"CREATE TABLE {CATALOG_NAME}.{SCHEMA}.created_table\" +\n                      f\" SHALLOW CLONE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\")\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"java.lang.AssertionError: assertion failed\" in str(error))\n\n    def test_clone_into_non_catalog_owned(self) -> None:\n        try:\n            # CLONE fails with an assertion error in UCSingleCatalog\n            spark.sql(f\"CREATE TABLE {CATALOG_NAME}.{SCHEMA}.created_table\" +\n                      f\" SHALLOW CLONE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                      f\"TBLPROPERTIES ('delta.feature.catalogManaged' = 'false')\")\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"java.lang.AssertionError: assertion failed\" in str(error))\n\n    def test_alter_table_comment(self) -> None:\n        try:\n            spark.sql(f\"ALTER TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                      f\"ALTER COLUMN id COMMENT 'comment'\")\n        except UnsupportedOperationException as error:\n            assert(\"Altering a table is not supported yet\" in str(error))\n\n    def test_alter_table_add_column(self) -> None:\n        try:\n            spark.sql(f\"ALTER TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} ADD COLUMN extra INT\")\n        except UnsupportedOperationException as error:\n            assert(\"Altering a table is not supported yet\" in str(error))\n\n    def test_alter_table_set_tbl_properties(self) -> None:\n        try:\n            spark.sql(f\"ALTER TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                      f\"SET TBLPROPERTIES ('customProp' = 'customValue')\")\n        except UnsupportedOperationException as error:\n            assert(\"Altering a table is not supported yet\" in str(error))\n\n        description = spark.sql(f\"DESCRIBE EXTENDED {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\")\\\n            .filter(\"col_name = 'Table Properties'\").collect()[0][1]\n        assert(\"customProp\" not in description)\n\n\nclass UnityCatalogManagedTableUtilitySuite(UnityCatalogManagedTableTestBase):\n    \"\"\"\n    Suite covering utility operations on a managed table in Unity Catalog: OPTIMIZE, ANALYZE,\n    VACUUM, ...\n    \"\"\"\n    def test_optimize(self) -> None:\n        spark.sql(f\"OPTIMIZE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(1, ), (2, ), (3, )]))\n\n    def test_optimize_sql(self) -> None:\n        spark.sql(f\"OPTIMIZE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(1, ), (2, ), (3, )]))\n\n    def test_zorder_by(self) -> None:\n        spark.sql(f\"OPTIMIZE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} ZORDER BY (id)\")\n        updated_tbl = self.read(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).toDF(\"id\")\n        assertDataFrameEqual(updated_tbl, self.create_df_with_rows([(1, ), (2, ), (3, )]))\n\n    def test_analyze(self) -> None:\n        try:\n            spark.sql(f\"ANALYZE TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} COMPUTE STATISTICS\")\n        except AnalysisException as error:\n            assert(\n                \"[NOT_SUPPORTED_COMMAND_FOR_V2_TABLE] ANALYZE TABLE is not supported for v2 tables.\"\n                in str(error)\n            )\n\n    def test_describe_table(self) -> None:\n        description = spark.sql(f\"DESCRIBE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\").collect()\n        expected = spark.createDataFrame([(\"id\", \"int\", None)],\n                                         \"col_name string, data_type string, comment string\")\n        assertDataFrameEqual(description, expected)\n\n    def test_history(self) -> None:\n        try:\n            # DESCRIBE HISTORY is currently unsupported on catalog owned tables.\n            self.get_table_history(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).collect()\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"catalog-managed\" in str(error).lower())\n\n    def test_vacuum(self) -> None:\n        try:\n            # VACUUM is currently unsupported on catalog owned tables.\n            spark.sql(f\"VACUUM {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\")\n        except UnsupportedOperationException as error:\n            assert(\"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\" in str(error))\n\n    def test_restore(self) -> None:\n        # Intentionally add a new data change commit.\n        self.append(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        try:\n            current_version = self.current_version(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n            # Restore is currently unsupported on catalog owned tables.\n            spark.sql(f\"RESTORE TABLE {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} TO \"\n                      f\"VERSION AS OF {current_version-1}\")\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"UPDATE_DELTA_METADATA\" in str(error))\n\n\nclass UnityCatalogManagedTableReadSuite(UnityCatalogManagedTableTestBase):\n    \"\"\"\n    Suite covering reading from a managed table in Unity Catalog/\n    \"\"\"\n\n    def test_time_travel_read(self) -> None:\n        dt = DeltaTable.forName(spark, MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        current_version = dt.history().selectExpr(\"max(version)\").collect()[0][0]\n        current_timestamp = str(datetime.datetime.now())\n        self.append(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n\n        result = spark.read.option(\"timestampAsOf\", current_timestamp)\\\n            .table(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        assertDataFrameEqual(result, self.setup_df)\n\n        result = spark.read.option(\"versionAsOf\", current_version)\\\n            .table(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        assertDataFrameEqual(result, self.setup_df)\n\n        result = spark.sql(f\"SELECT * FROM {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                           f\"TIMESTAMP AS OF '{current_timestamp}'\")\n        assertDataFrameEqual(result, self.setup_df)\n\n        result = spark.sql(f\"SELECT * FROM {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME} \"\n                           f\"VERSION AS OF {current_version}\")\n        assertDataFrameEqual(result, self.setup_df)\n\n    # CDC (Timestamps, Versions) are currently unsupported for Catalog owned tables.\n    def test_change_data_feed_with_timestamp(self) -> None:\n        timestamp = str(datetime.datetime.now())\n        self.append(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        try:\n            self.read_with_cdf_timestamp(\n                timestamp, MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).select(\"id\", \"_change_type\")\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"Path based access is not supported for Catalog-Owned table\" in str(error))\n\n    def test_change_data_feed_with_version(self) -> None:\n        # Intentionally add a new data change commit.\n        self.append(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n        try:\n            current_version = self.current_version(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\n            self.read_with_cdf_version(\n                current_version - 1,\n                MANAGED_CATALOG_OWNED_TABLE_FULL_NAME).select(\"id\", \"_change_type\")\n        except py4j.protocol.Py4JJavaError as error:\n            assert(\"UPDATE_DELTA_METADATA\" in str(error))\n\n    def test_delta_table_for_path(self) -> None:\n        tbl_path = spark.sql(\n            f\"DESCRIBE formatted {MANAGED_CATALOG_OWNED_TABLE_FULL_NAME}\").collect()[5].data_type\n        try:\n            DeltaTable.forPath(spark, tbl_path)\n        except py4j.protocol.Py4JJavaError as error:\n            # Path-based access isn't supported. This could throw a better error than just\n            # 'access denied' though.\n            assert(\"AccessDeniedException\" in str(error))\n\n    def test_streaming_read(self) -> None:\n        try:\n            spark.readStream\\\n                .table(MANAGED_CATALOG_OWNED_TABLE_FULL_NAME)\\\n                .writeStream\\\n                .option(\"checkpointLocation\", \"test\")\\\n                .toTable(\"output_table\")\n        except py4j.protocol.Py4JJavaError as error:\n            # Streaming from a catalog owned table fails as it attempts to access the table by path.\n            # This could also throw a better error than jsut 'access denied'.\n            assert(\"AccessDeniedException\" in str(error))\n\n\nif __name__ == \"__main__\":\n    \"\"\"\n    Change this to select tests to run, for example:\n    - '__main__': all tests in this file.\n    - '__main__.UnityCatalogManagedTableDMLSuite': all tests in that single suites.\n    - '__main__.UnityCatalogManagedTableDDLSuite.test_sql_create': only that single test.\n    \"\"\"\n    test_name = \"__main__\"\n\n    suite = unittest.TestLoader().loadTestsFromName(test_name)\n    unittest.TextTestRunner(verbosity=2).run(suite)\n"
  },
  {
    "path": "python/delta/pip_utils.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nfrom typing import List, Optional\n\nfrom pyspark.sql import SparkSession\n\n\ndef configure_spark_with_delta_pip(\n    spark_session_builder: SparkSession.Builder,\n    extra_packages: Optional[List[str]] = None\n) -> SparkSession.Builder:\n    \"\"\"\n    Utility function to configure a SparkSession builder such that the generated SparkSession\n    will automatically download the required Delta Lake JARs from Maven. This function is\n    required when you want to\n\n    1. Install Delta Lake locally using pip, and\n\n    2. Execute your Python code using Delta Lake + Pyspark directly, that is, not using\n       `spark-submit --packages io.delta:...` or `pyspark --packages io.delta:...`.\n\n        builder = SparkSession.builder \\\n            .master(\"local[*]\") \\\n            .appName(\"test\")\n\n        spark = configure_spark_with_delta_pip(builder).getOrCreate()\n\n    3. If you would like to add more packages, use the `extra_packages` parameter.\n\n        builder = SparkSession.builder \\\n            .master(\"local[*]\") \\\n            .appName(\"test\")\n        my_packages = [\"org.apache.spark:spark-sql-kafka-0-10_2.12:x.y.z\"]\n        spark = configure_spark_with_delta_pip(builder, extra_packages=my_packages).getOrCreate()\n\n    :param spark_session_builder: SparkSession.Builder object being used to configure and\n                                  create a SparkSession.\n    :param extra_packages: Set other packages to add to Spark session besides Delta Lake.\n    :return: Updated SparkSession.Builder object\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n    import importlib_metadata  # load this library only when this function is called\n\n    if type(spark_session_builder) is not SparkSession.Builder:\n        msg = f'''\nThis function must be called with a SparkSession builder as the argument.\nThe argument found is of type {str(type(spark_session_builder))}.\nSee the online documentation for the correct usage of this function.\n        '''\n        raise TypeError(msg)\n\n    try:\n        delta_version = importlib_metadata.version(\"delta_spark\")\n    except Exception as e:\n        msg = '''\nThis function can be used only when Delta Lake has been locally installed with pip.\nSee the online documentation for the correct usage of this function.\n        '''\n        raise Exception(msg) from e\n\n    # Get Spark version from pyspark module\n    import pyspark\n    spark_version = pyspark.__version__\n\n    scala_version = \"2.13\"\n\n    # Determine the Spark major.minor version for artifact name\n    # Artifact names include Spark version suffix when spark_version is known\n    # (e.g., delta-spark_4.0_2.13). Falls back to no suffix for backward compatibility.\n    if spark_version:\n        spark_major_minor = \".\".join(spark_version.split(\".\")[:2])  # e.g., \"4.0\" or \"4.1\"\n        artifact_name = f\"delta-spark_{spark_major_minor}_{scala_version}\"\n    else:\n        # Fallback to artifact without suffix for backward compatibility\n        artifact_name = f\"delta-spark_{scala_version}\"\n\n    maven_artifact = f\"io.delta:{artifact_name}:{delta_version}\"\n\n    extra_packages = extra_packages if extra_packages is not None else []\n    all_artifacts = [maven_artifact] + extra_packages\n    packages_str = \",\".join(all_artifacts)\n\n    return spark_session_builder.config(\"spark.jars.packages\", packages_str)\n"
  },
  {
    "path": "python/delta/py.typed",
    "content": ""
  },
  {
    "path": "python/delta/tables.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom dataclasses import dataclass\nfrom typing import (\n    TYPE_CHECKING, cast, overload, Any, Dict, Iterable, Optional, Union, NoReturn, List, Tuple\n)\n\nfrom delta._typing import (\n    ColumnMapping, OptionalColumnMapping, ExpressionOrColumn, OptionalExpressionOrColumn\n)\n\nfrom pyspark import since\nfrom pyspark.sql import Column, DataFrame, functions, SparkSession\nfrom pyspark.sql.types import DataType, StructType, StructField\nfrom pyspark.sql.utils import is_remote\n\n\nif TYPE_CHECKING:\n    from py4j.java_gateway import JavaObject, JVMView  # type: ignore[import]\n    from py4j.java_collections import JavaMap  # type: ignore[import]\n\n\nclass DeltaTable(object):\n    \"\"\"\n        Main class for programmatically interacting with Delta tables.\n        You can create DeltaTable instances using the path of the Delta table.::\n\n            deltaTable = DeltaTable.forPath(spark, \"/path/to/table\")\n\n        In addition, you can convert an existing Parquet table in place into a Delta table.::\n\n            deltaTable = DeltaTable.convertToDelta(spark, \"parquet.`/path/to/table`\")\n\n        .. versionadded:: 0.4\n    \"\"\"\n    def __init__(self, spark: SparkSession, jdt: \"JavaObject\"):\n        self._spark = spark\n        self._jdt = jdt\n\n    @since(0.4)  # type: ignore[arg-type]\n    def toDF(self) -> DataFrame:\n        \"\"\"\n        Get a DataFrame representation of this Delta table.\n        \"\"\"\n        return DataFrame(\n            self._jdt.toDF(),\n            # Simple trick to avoid warnings from Spark 3.3.0. `_wrapped`\n            # in SparkSession is removed in Spark 3.3.0, see also SPARK-38121.\n            getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n        )\n\n    @since(0.4)  # type: ignore[arg-type]\n    def alias(self, aliasName: str) -> \"DeltaTable\":\n        \"\"\"\n        Apply an alias to the Delta table.\n        \"\"\"\n        jdt = self._jdt.alias(aliasName)\n        return DeltaTable(self._spark, jdt)\n\n    @since(0.5)  # type: ignore[arg-type]\n    def generate(self, mode: str) -> None:\n        \"\"\"\n        Generate manifest files for the given delta table.\n\n        :param mode: mode for the type of manifest file to be generated\n                     The valid modes are as follows (not case sensitive):\n\n                     - \"symlink_format_manifest\": This will generate manifests in symlink format\n                                                  for Presto and Athena read support.\n\n                     See the online documentation for more information.\n        \"\"\"\n        self._jdt.generate(mode)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def delete(self, condition: OptionalExpressionOrColumn = None) -> None:\n        \"\"\"\n        Delete data from the table that match the given ``condition``.\n\n        Example::\n\n            deltaTable.delete(\"date < '2017-01-01'\")        # predicate using SQL formatted string\n\n            deltaTable.delete(col(\"date\") < \"2017-01-01\")   # predicate using Spark SQL functions\n\n        :param condition: condition of the update\n        :type condition: str or pyspark.sql.Column\n        \"\"\"\n        if condition is None:\n            self._jdt.delete()\n        else:\n            self._jdt.delete(DeltaTable._condition_to_jcolumn(condition))\n\n    @overload\n    def update(\n        self, condition: ExpressionOrColumn, set: ColumnMapping\n    ) -> None:\n        ...\n\n    @overload\n    def update(self, *, set: ColumnMapping) -> None:\n        ...\n\n    def update(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        set: OptionalColumnMapping = None\n    ) -> None:\n        \"\"\"\n        Update data from the table on the rows that match the given ``condition``,\n        which performs the rules defined by ``set``.\n\n        Example::\n\n            # condition using SQL formatted string\n            deltaTable.update(\n                condition = \"eventType = 'clck'\",\n                set = { \"eventType\": \"'click'\" } )\n\n            # condition using Spark SQL functions\n            deltaTable.update(\n                condition = col(\"eventType\") == \"clck\",\n                set = { \"eventType\": lit(\"click\") } )\n\n        :param condition: Optional condition of the update\n        :type condition: str or pyspark.sql.Column\n        :param set: Defines the rules of setting the values of columns that need to be updated.\n                    *Note: This param is required.* Default value None is present to allow\n                    positional args in same order across languages.\n        :type set: dict with str as keys and str or pyspark.sql.Column as values\n\n        .. versionadded:: 0.4\n        \"\"\"\n        jmap = DeltaTable._dict_to_jmap(self._spark, set, \"'set'\")\n        jcolumn = DeltaTable._condition_to_jcolumn(condition)\n        if condition is None:\n            self._jdt.update(jmap)\n        else:\n            self._jdt.update(jcolumn, jmap)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def merge(\n        self, source: DataFrame, condition: ExpressionOrColumn\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Merge data from the `source` DataFrame based on the given merge `condition`. This returns\n        a :class:`DeltaMergeBuilder` object that can be used to specify the update, delete, or\n        insert actions to be performed on rows based on whether the rows matched the condition or\n        not. See :class:`DeltaMergeBuilder` for a full description of this operation and what\n        combinations of update, delete and insert operations are allowed.\n\n        Example 1 with conditions and update expressions as SQL formatted string::\n\n            deltaTable.alias(\"events\").merge(\n                source = updatesDF.alias(\"updates\"),\n                condition = \"events.eventId = updates.eventId\"\n              ).whenMatchedUpdate(set =\n                {\n                  \"data\": \"updates.data\",\n                  \"count\": \"events.count + 1\"\n                }\n              ).whenNotMatchedInsert(values =\n                {\n                  \"date\": \"updates.date\",\n                  \"eventId\": \"updates.eventId\",\n                  \"data\": \"updates.data\",\n                  \"count\": \"1\"\n                }\n              ).execute()\n\n        Example 2 with conditions and update expressions as Spark SQL functions::\n\n            from pyspark.sql.functions import *\n\n            deltaTable.alias(\"events\").merge(\n                source = updatesDF.alias(\"updates\"),\n                condition = expr(\"events.eventId = updates.eventId\")\n              ).whenMatchedUpdate(set =\n                {\n                  \"data\" : col(\"updates.data\"),\n                  \"count\": col(\"events.count\") + 1\n                }\n              ).whenNotMatchedInsert(values =\n                {\n                  \"date\": col(\"updates.date\"),\n                  \"eventId\": col(\"updates.eventId\"),\n                  \"data\": col(\"updates.data\"),\n                  \"count\": lit(\"1\")\n                }\n              ).execute()\n\n        :param source: Source DataFrame\n        :type source: pyspark.sql.DataFrame\n        :param condition: Condition to match sources rows with the Delta table rows.\n        :type condition: str or pyspark.sql.Column\n\n        :return: builder object to specify whether to update, delete or insert rows based on\n                 whether the condition matched or not\n        :rtype: :py:class:`delta.tables.DeltaMergeBuilder`\n        \"\"\"\n        if source is None:\n            raise ValueError(\"'source' in merge cannot be None\")\n        elif not isinstance(source, DataFrame):\n            raise TypeError(\"Type of 'source' in merge must be DataFrame.\")\n        if condition is None:\n            raise ValueError(\"'condition' in merge cannot be None\")\n\n        jbuilder = self._jdt.merge(source._jdf, DeltaTable._condition_to_jcolumn(condition))\n        return DeltaMergeBuilder(self._spark, jbuilder)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def vacuum(self, retentionHours: Optional[float] = None) -> DataFrame:\n        \"\"\"\n        Recursively delete files and directories in the table that are not needed by the table for\n        maintaining older versions up to the given retention threshold. This method will return an\n        empty DataFrame on successful completion.\n\n        Example::\n\n            deltaTable.vacuum()     # vacuum files not required by versions more than 7 days old\n\n            deltaTable.vacuum(100)  # vacuum files not required by versions more than 100 hours old\n\n        :param retentionHours: Optional number of hours retain history. If not specified, then the\n                               default retention period of 168 hours (7 days) will be used.\n        \"\"\"\n        jdt = self._jdt\n        if retentionHours is None:\n            return DataFrame(\n                jdt.vacuum(),\n                getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n            )\n        else:\n            return DataFrame(\n                jdt.vacuum(float(retentionHours)),\n                getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n            )\n\n    @since(0.4)  # type: ignore[arg-type]\n    def history(self, limit: Optional[int] = None) -> DataFrame:\n        \"\"\"\n        Get the information of the latest `limit` commits on this table as a Spark DataFrame.\n        The information is in reverse chronological order.\n\n        Example::\n\n            fullHistoryDF = deltaTable.history()    # get the full history of the table\n\n            lastOperationDF = deltaTable.history(1) # get the last operation\n\n        :param limit: Optional, number of latest commits to returns in the history.\n        :return: Table's commit history. See the online Delta Lake documentation for more details.\n        :rtype: pyspark.sql.DataFrame\n        \"\"\"\n        jdt = self._jdt\n        if limit is None:\n            return DataFrame(\n                jdt.history(),\n                getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n            )\n        else:\n            return DataFrame(\n                jdt.history(limit),\n                getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n            )\n\n    @since(2.1)  # type: ignore[arg-type]\n    def detail(self) -> DataFrame:\n        \"\"\"\n        Get the details of a Delta table such as the format, name, and size.\n\n        Example::\n\n            detailDF = deltaTable.detail() # get the full details of the table\n\n        :return Information of the table (format, name, size, etc.)\n        :rtype: pyspark.sql.DataFrame\n\n        .. note:: Evolving\n        \"\"\"\n        return DataFrame(\n            self._jdt.detail(),\n            getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n        )\n\n    @classmethod\n    @since(0.4)  # type: ignore[arg-type]\n    def convertToDelta(\n        cls,\n        sparkSession: SparkSession,\n        identifier: str,\n        partitionSchema: Optional[Union[str, StructType]] = None\n    ) -> \"DeltaTable\":\n        \"\"\"\n        Create a DeltaTable from the given parquet table. Takes an existing parquet table and\n        constructs a delta transaction log in the base path of the table.\n        Note: Any changes to the table during the conversion process may not result in a consistent\n        state at the end of the conversion. Users should stop any changes to the table before the\n        conversion is started.\n\n        Example::\n\n            # Convert unpartitioned parquet table at path 'path/to/table'\n            deltaTable = DeltaTable.convertToDelta(\n                spark, \"parquet.`path/to/table`\")\n\n            # Convert partitioned parquet table at path 'path/to/table' and partitioned by\n            # integer column named 'part'\n            partitionedDeltaTable = DeltaTable.convertToDelta(\n                spark, \"parquet.`path/to/table`\", \"part int\")\n\n        :param sparkSession: SparkSession to use for the conversion\n        :type sparkSession: pyspark.sql.SparkSession\n        :param identifier: Parquet table identifier formatted as \"parquet.`path`\"\n        :type identifier: str\n        :param partitionSchema: Hive DDL formatted string, or pyspark.sql.types.StructType\n        :return: DeltaTable representing the converted Delta table\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n        \"\"\"\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.convertToDelta(sparkSession, identifier, partitionSchema)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        if partitionSchema is None:\n            jdt = jvm.io.delta.tables.DeltaTable.convertToDelta(\n                jsparkSession, identifier\n            )\n        else:\n            if not isinstance(partitionSchema, str):\n                partitionSchema = jsparkSession.parseDataType(partitionSchema.json())\n            jdt = jvm.io.delta.tables.DeltaTable.convertToDelta(\n                jsparkSession, identifier,\n                partitionSchema)\n        return DeltaTable(sparkSession, jdt)\n\n    @classmethod\n    @since(0.4)  # type: ignore[arg-type]\n    def forPath(\n        cls,\n        sparkSession: SparkSession,\n        path: str,\n        hadoopConf: Dict[str, str] = dict()\n    ) -> \"DeltaTable\":\n        \"\"\"\n        Instantiate a :class:`DeltaTable` object representing the data at the given path,\n        If the given path is invalid (i.e. either no table exists or an existing table is\n        not a Delta table), it throws a `not a Delta table` error.\n\n        :param sparkSession: SparkSession to use for loading the table\n        :type sparkSession: pyspark.sql.SparkSession\n        :param hadoopConf: Hadoop configuration starting with \"fs.\" or \"dfs.\" will be picked\n                           up by `DeltaTable` to access the file system when executing queries.\n                           Other configurations will not be allowed.\n        :type hadoopConf: optional dict with str as key and str as value.\n        :return: loaded Delta table\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n\n        Example::\n\n            hadoopConf = {\"fs.s3a.access.key\" : \"<access-key>\",\n                       \"fs.s3a.secret.key\": \"secret-key\"}\n            deltaTable = DeltaTable.forPath(\n                           spark,\n                           \"/path/to/table\",\n                           hadoopConf)\n        \"\"\"\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.forPath(sparkSession, path, hadoopConf)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        jdt = jvm.io.delta.tables.DeltaTable.forPath(jsparkSession, path, hadoopConf)\n        return DeltaTable(sparkSession, jdt)\n\n    @classmethod\n    @since(0.7)  # type: ignore[arg-type]\n    def forName(\n        cls, sparkSession: SparkSession, tableOrViewName: str\n    ) -> \"DeltaTable\":\n        \"\"\"\n        Instantiate a :class:`DeltaTable` object using the given table name. If the given\n        tableOrViewName is invalid (i.e. either no table exists or an existing table is not a\n        Delta table), it throws a `not a Delta table` error. Note: Passing a view name will\n        also result in this error as views are not supported.\n\n        The given tableOrViewName can also be the absolute path of a delta datasource (i.e.\n        delta.`path`), If so, instantiate a :class:`DeltaTable` object representing the data at\n        the given path (consistent with the `forPath`).\n\n        :param sparkSession: SparkSession to use for loading the table\n        :param tableOrViewName: name of the table or view\n        :return: loaded Delta table\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n\n        Example::\n\n            deltaTable = DeltaTable.forName(spark, \"tblName\")\n        \"\"\"\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.forName(sparkSession, tableOrViewName)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        jdt = jvm.io.delta.tables.DeltaTable.forName(jsparkSession, tableOrViewName)\n        return DeltaTable(sparkSession, jdt)\n\n    @classmethod\n    @since(1.0)  # type: ignore[arg-type]\n    def create(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Return :class:`DeltaTableBuilder` object that can be used to specify\n        the table name, location, columns, partitioning columns, table comment,\n        and table properties to create a Delta table, error if the table exists\n        (the same as SQL `CREATE TABLE`).\n\n        See :class:`DeltaTableBuilder` for a full description and examples\n        of this operation.\n\n        :param sparkSession: SparkSession to use for creating the table\n        :return: an instance of DeltaTableBuilder\n        :rtype: :py:class:`~delta.tables.DeltaTableBuilder`\n\n        .. note:: Evolving\n        \"\"\"\n        if sparkSession is None:\n            sparkSession = SparkSession.getActiveSession()\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.create(sparkSession)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        jdt = jvm.io.delta.tables.DeltaTable.create(jsparkSession)\n        return DeltaTableBuilder(sparkSession, jdt)\n\n    @classmethod\n    @since(1.0)  # type: ignore[arg-type]\n    def createIfNotExists(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Return :class:`DeltaTableBuilder` object that can be used to specify\n        the table name, location, columns, partitioning columns, table comment,\n        and table properties to create a Delta table,\n        if it does not exists (the same as SQL `CREATE TABLE IF NOT EXISTS`).\n\n        See :class:`DeltaTableBuilder` for a full description and examples\n        of this operation.\n\n        :param sparkSession: SparkSession to use for creating the table\n        :return: an instance of DeltaTableBuilder\n        :rtype: :py:class:`~delta.tables.DeltaTableBuilder`\n\n        .. note:: Evolving\n        \"\"\"\n        if sparkSession is None:\n            sparkSession = SparkSession.getActiveSession()\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.createIfNotExists(sparkSession)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        jdt = jvm.io.delta.tables.DeltaTable.createIfNotExists(jsparkSession)\n        return DeltaTableBuilder(sparkSession, jdt)\n\n    @classmethod\n    @since(1.0)  # type: ignore[arg-type]\n    def replace(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Return :class:`DeltaTableBuilder` object that can be used to specify\n        the table name, location, columns, partitioning columns, table comment,\n        and table properties to replace a Delta table,\n        error if the table doesn't exist (the same as SQL `REPLACE TABLE`).\n\n        See :class:`DeltaTableBuilder` for a full description and examples\n        of this operation.\n\n        :param sparkSession: SparkSession to use for creating the table\n        :return: an instance of DeltaTableBuilder\n        :rtype: :py:class:`~delta.tables.DeltaTableBuilder`\n\n        .. note:: Evolving\n        \"\"\"\n        if sparkSession is None:\n            sparkSession = SparkSession.getActiveSession()\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.replace(sparkSession)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        jdt = jvm.io.delta.tables.DeltaTable.replace(jsparkSession)\n        return DeltaTableBuilder(sparkSession, jdt)\n\n    @classmethod\n    @since(1.0)  # type: ignore[arg-type]\n    def createOrReplace(\n        cls, sparkSession: Optional[SparkSession] = None\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Return :class:`DeltaTableBuilder` object that can be used to specify\n        the table name, location, columns, partitioning columns, table comment,\n        and table properties replace a Delta table,\n        error if the table doesn't exist (the same as SQL `REPLACE TABLE`).\n\n        See :class:`DeltaTableBuilder` for a full description and examples\n        of this operation.\n\n        :param sparkSession: SparkSession to use for creating the table\n        :return: an instance of DeltaTableBuilder\n        :rtype: :py:class:`~delta.tables.DeltaTableBuilder`\n\n        .. note:: Evolving\n        \"\"\"\n        if sparkSession is None:\n            sparkSession = SparkSession.getActiveSession()\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.createOrReplace(sparkSession)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        jdt = jvm.io.delta.tables.DeltaTable.createOrReplace(jsparkSession)\n        return DeltaTableBuilder(sparkSession, jdt)\n\n    @classmethod\n    @since(0.4)  # type: ignore[arg-type]\n    def isDeltaTable(cls, sparkSession: SparkSession, identifier: str) -> bool:\n        \"\"\"\n        Check if the provided `identifier` string, in this case a file path,\n        is the root of a Delta table using the given SparkSession.\n\n        :param sparkSession: SparkSession to use to perform the check\n        :param path: location of the table\n        :return: If the table is a delta table or not\n        :rtype: bool\n\n        Example::\n\n            DeltaTable.isDeltaTable(spark, \"/path/to/table\")\n        \"\"\"\n        assert sparkSession is not None\n        if is_remote():\n            from pyspark.sql.connect.session import SparkSession as RemoteSparkSession\n            if isinstance(sparkSession, RemoteSparkSession):\n                from delta.connect.tables import DeltaTable as RemoteDeltaTable\n                return RemoteDeltaTable.isDeltaTable(sparkSession, identifier)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = sparkSession._jsparkSession  # type: ignore[attr-defined]\n\n        return jvm.io.delta.tables.DeltaTable.isDeltaTable(jsparkSession, identifier)\n\n    @since(0.8)  # type: ignore[arg-type]\n    def upgradeTableProtocol(self, readerVersion: int, writerVersion: int) -> None:\n        \"\"\"\n        Updates the protocol version of the table to leverage new features. Upgrading the reader\n        version will prevent all clients that have an older version of Delta Lake from accessing\n        this table. Upgrading the writer version will prevent older versions of Delta Lake to write\n        to this table. The reader or writer version cannot be downgraded.\n\n        See online documentation and Delta's protocol specification at PROTOCOL.md for more details.\n        \"\"\"\n        jdt = self._jdt\n        if not isinstance(readerVersion, int):\n            raise ValueError(\"The readerVersion needs to be an integer but got '%s'.\" %\n                             type(readerVersion))\n        if not isinstance(writerVersion, int):\n            raise ValueError(\"The writerVersion needs to be an integer but got '%s'.\" %\n                             type(writerVersion))\n        jdt.upgradeTableProtocol(readerVersion, writerVersion)\n\n    @since(3.3)  # type: ignore[arg-type]\n    def addFeatureSupport(self, featureName: str) -> None:\n        \"\"\"\n        Modify the protocol to add a supported feature, and if the table does not support table\n        features, upgrade the protocol automatically. In such a case when the provided feature is\n        writer-only, the table's writer version will be upgraded to `7`, and when the provided\n        feature is reader-writer, both reader and writer versions will be upgraded, to `(3, 7)`.\n\n        See online documentation and Delta's protocol specification at PROTOCOL.md for more details.\n        \"\"\"\n        DeltaTable._verify_type_str(featureName, \"featureName\")\n        self._jdt.addFeatureSupport(featureName)\n\n    @since(3.4)  # type: ignore[arg-type]\n    def dropFeatureSupport(self, featureName: str, truncateHistory: Optional[bool] = None) -> None:\n        \"\"\"\n        Modify the protocol to drop a supported feature. The operation always normalizes the\n        resulting protocol. Protocol normalization is the process of converting a table features\n        protocol to the weakest possible form. This primarily refers to converting a table features\n        protocol to a legacy protocol. A table features protocol can be represented with the legacy\n        representation only when the feature set of the former exactly matches a legacy protocol.\n        Normalization can also decrease the reader version of a table features protocol when it is\n        higher than necessary. For example:\n\n        (1, 7, None, {AppendOnly, Invariants, CheckConstraints}) -> (1, 3)\n        (3, 7, None, {RowTracking}) -> (1, 7, RowTracking)\n\n        The dropFeatureSupport method can be used as follows:\n        delta.tables.DeltaTable.dropFeatureSupport(\"rowTracking\")\n\n        :param featureName: The name of the feature to drop.\n        :param truncateHistory: Optional value whether to truncate history. If not specified,\n                                the history is not truncated.\n        :return: None.\n        \"\"\"\n        DeltaTable._verify_type_str(featureName, \"featureName\")\n        if truncateHistory is None:\n            self._jdt.dropFeatureSupport(featureName)\n        else:\n            DeltaTable._verify_type_bool(truncateHistory, \"truncateHistory\")\n            self._jdt.dropFeatureSupport(featureName, truncateHistory)\n\n    @since(1.2)  # type: ignore[arg-type]\n    def restoreToVersion(self, version: int) -> DataFrame:\n        \"\"\"\n        Restore the DeltaTable to an older version of the table specified by version number.\n\n        Example::\n\n            delta.tables.DeltaTable.restoreToVersion(1)\n\n        :param version: target version of restored table\n        :return: Dataframe with metrics of restore operation.\n        :rtype: pyspark.sql.DataFrame\n        \"\"\"\n\n        DeltaTable._verify_type_int(version, \"version\")\n        return DataFrame(\n            self._jdt.restoreToVersion(version),\n            getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n        )\n\n    @since(1.2)  # type: ignore[arg-type]\n    def restoreToTimestamp(self, timestamp: str) -> DataFrame:\n        \"\"\"\n        Restore the DeltaTable to an older version of the table specified by a timestamp.\n        Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss\n\n        Example::\n\n            delta.tables.DeltaTable.restoreToTimestamp('2021-01-01')\n            delta.tables.DeltaTable.restoreToTimestamp('2021-01-01 01:01:01')\n\n        :param timestamp: target timestamp of restored table\n        :return: Dataframe with metrics of restore operation.\n        :rtype: pyspark.sql.DataFrame\n        \"\"\"\n\n        DeltaTable._verify_type_str(timestamp, \"timestamp\")\n        return DataFrame(\n            self._jdt.restoreToTimestamp(timestamp),\n            getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n        )\n\n    @since(2.0)  # type: ignore[arg-type]\n    def optimize(self) -> \"DeltaOptimizeBuilder\":\n        \"\"\"\n        Optimize the data layout of the table. This returns\n        a :py:class:`~delta.tables.DeltaOptimizeBuilder` object that can\n        be used to specify the partition filter to limit the scope of\n        optimize and also execute different optimization techniques\n        such as file compaction or order data using Z-Order curves.\n\n        See the :py:class:`~delta.tables.DeltaOptimizeBuilder` for a\n        full description of this operation.\n\n        Example::\n\n            deltaTable.optimize().where(\"date='2021-11-18'\").executeCompaction()\n\n        :return: an instance of DeltaOptimizeBuilder.\n        :rtype: :py:class:`~delta.tables.DeltaOptimizeBuilder`\n        \"\"\"\n        jbuilder = self._jdt.optimize()\n        return DeltaOptimizeBuilder(self._spark, jbuilder)\n\n    def clone(  # type: ignore[no-untyped-def]\n        self, target, isShallow=False, replace=False, properties=None\n    ) -> \"DeltaTable\":\n        \"\"\"\n        Clone the latest state of a DeltaTable to a destination which mirrors the existing\n        table's data and metadata at that version.\n        Example::\n            # Shallow clone a table to path '/path/to/table'\n            deltaTable = DeltaTable.clone(\"/path/to/table\", False, True)\n        :param self: The current instance\n        :type self: :py:class:`~delta.tables.DeltaTable`\n        :param target: Path where we should clone the Delta table\n        :type target: str\n        :param isShallow: True for shallow clones, false for deep clones\n        :type isShallow: bool\n        :param replace: True if the desired behavior is to overwrite the target table if one exists\n                        otherwise throw an error if table exists at the target\n        :type replace: bool\n        :param properties: user-defined table properties that should override any properties with\n                           the same key from the source table\n        :type properties: dict\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n        \"\"\"\n\n        DeltaTable._verify_clone_types(target, isShallow, replace, properties)\n        return self._jdt.clone(target, isShallow, replace, properties)\n\n    def cloneAtVersion(  # type: ignore[no-untyped-def]\n        self, version, target, isShallow=False, replace=False, properties=None\n    ) -> \"DeltaTable\":\n        \"\"\"\n        Clone a DeltaTable at the given version to a destination which mirrors the existing\n        table's data and metadata at that version.\n        Example::\n            # Shallow clone a table to path '/path/to/table' at version 1\n            deltaTable = DeltaTable.cloneAtVersion(1, \"/path/to/table\", False)\n        :param self: The current instance\n        :type self: :py:class:`~delta.tables.DeltaTable`\n        :param version: Version at which to clone the source directory. Take the metadata at this\n                        version of the table as well.\n        :type version: number\n        :param target: Path where we should clone the Delta table\n        :type target: str\n        :param isShallow: True for shallow clones, false for deep clones\n        :type isShallow: bool\n        :param replace: True if the desired behavior is to overwrite the target table if one exists\n                        otherwise throw an error if table exists at the target\n        :type replace: bool\n        :param properties: user-defined table properties that should override any properties with\n                           the same key from the source table\n        :type properties: dict\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n        \"\"\"\n\n        DeltaTable._verify_clone_types(target, isShallow, replace, properties, version=version)\n        return self._jdt.cloneAtVersion(version, target, isShallow, replace, properties)\n\n    def cloneAtTimestamp(  # type: ignore[no-untyped-def]\n        self, timestamp, target, isShallow=False, replace=False, properties=None\n    ) -> \"DeltaTable\":\n        \"\"\"\n        Clone a DeltaTable at the given timestamp to a destination which mirrors the existing\n        table's data and metadata at that timestamp.\n        Example::\n            # Shallow clone a table to path '/path/to/table' at time of format yyyy-MM-dd'T'HH:mm:ss\n            # or yyyy-MM-dd\n            deltaTable = DeltaTable.cloneAtTimestamp(\n                \"2019-01-01\",\n                \"/path/to/table\",\n                False)\n        :param self: The current instance\n        :type self: :py:class:`~delta.tables.DeltaTable`\n        :param timestamp: Timestamp at which to clone the source directory. Take the metadata at\n                          this timestamp as well.\n        :type timestamp: str\n        :param target: Path where we should clone the Delta table\n        :type target: str\n        :param isShallow: True for shallow clones, false for deep clones\n        :type isShallow: bool\n        :param replace: True if the desired behavior is to overwrite the target table if one exists\n                        otherwise throw an error if table exists at the target\n        :type replace: bool\n        :param properties: user-defined table properties that should override any properties with\n                           the same key from the source table\n        :type properties: dict\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n        \"\"\"\n\n        DeltaTable._verify_clone_types(target, isShallow, replace, properties, timestamp)\n        return self._jdt.cloneAtTimestamp(timestamp, target, isShallow, replace, properties)\n\n    @classmethod\n    def _verify_clone_types(\n        self,\n        target: str,\n        isShallow: bool,\n        replace: bool,\n        properties: dict,\n        timestamp: str = \"\",\n        version: int = 0\n    ) -> None:\n        \"\"\"\n        Throw an error if any of the types passed in to Clone do not\n        adhere to the types that we expect\n        \"\"\"\n        DeltaTable._verify_type_str(timestamp, \"timestamp\")\n        DeltaTable._verify_type_int(version, \"version\")\n        DeltaTable._verify_type_str(target, \"target\")\n        DeltaTable._verify_type_bool(isShallow, \"isShallow\")\n        DeltaTable._verify_type_bool(replace, \"replace\")\n\n        if properties is not None:\n            DeltaTable._verify_type_dict(properties, \"properties\")\n            for property, value in properties.items():\n                DeltaTable._verify_type_str(property, \"All property keys including %s\" % property)\n                DeltaTable._verify_type_str(value, \"All property values including %s\" % value)\n\n    @classmethod\n    def _verify_type_dict(cls, variable: dict, name: str) -> None:\n        if not isinstance(variable, dict):\n            raise ValueError(\"%s needs to be a dict but got '%s'.\" % (name, type(variable)))\n\n    @classmethod  # type: ignore[arg-type]\n    def _verify_type_bool(self, variable: bool, name: str) -> None:\n        if not isinstance(variable, bool) or variable is None:\n            raise ValueError(\"%s needs to be a boolean but got '%s'.\" % (name, type(variable)))\n\n    @staticmethod  # type: ignore[arg-type]\n    def _verify_type_str(variable: str, name: str) -> None:\n        if not isinstance(variable, str) or variable is None:\n            raise ValueError(\"%s needs to be a string but got '%s'.\" % (name, type(variable)))\n\n    @staticmethod  # type: ignore[arg-type]\n    def _verify_type_int(variable: int, name: str) -> None:\n        if not isinstance(variable, int) or variable is None:\n            raise ValueError(\"%s needs to be an int but got '%s'.\" % (name, type(variable)))\n\n    @staticmethod\n    def _dict_to_jmap(\n        sparkSession: SparkSession,\n        pydict: OptionalColumnMapping,\n        argname: str,\n    ) -> \"JavaObject\":\n        \"\"\"\n        convert dict<str, pColumn/str> to Map<str, jColumn>\n        \"\"\"\n        # Get the Java map for pydict\n        if pydict is None:\n            raise ValueError(\"%s cannot be None\" % argname)\n        elif type(pydict) is not dict:\n            e = \"%s must be a dict, found to be %s\" % (argname, str(type(pydict)))\n            raise TypeError(e)\n\n        jvm: \"JVMView\" = sparkSession._sc._jvm  # type: ignore[attr-defined]\n\n        jmap: \"JavaMap\" = jvm.java.util.HashMap()\n        for col, expr in pydict.items():\n            if type(col) is not str:\n                e = (\"Keys of dict in %s must contain only strings with column names\" % argname) + \\\n                    (\", found '%s' of type '%s\" % (str(col), str(type(col))))\n                raise TypeError(e)\n            if isinstance(expr, Column) and hasattr(expr, \"_jc\"):\n                jmap.put(col, expr._jc)\n            elif type(expr) is str:\n                jmap.put(col, functions.expr(expr)._jc)\n            else:\n                e = (\"Values of dict in %s must contain only Spark SQL Columns \" % argname) + \\\n                    \"or strings (expressions in SQL syntax) as values, \" + \\\n                    (\"found '%s' of type '%s'\" % (str(expr), str(type(expr))))\n                raise TypeError(e)\n        return jmap\n\n    @staticmethod\n    def _condition_to_jcolumn(\n        condition: OptionalExpressionOrColumn, argname: str = \"'condition'\"\n    ) -> \"JavaObject\":\n        if condition is None:\n            jcondition = None\n        elif isinstance(condition, Column) and hasattr(condition, \"_jc\"):\n            jcondition = condition._jc\n        elif type(condition) is str:\n            jcondition = functions.expr(condition)._jc\n        else:\n            e = (\"%s must be a Spark SQL Column or a string (expression in SQL syntax)\" % argname) \\\n                + \", found to be of type %s\" % str(type(condition))\n            raise TypeError(e)\n        return jcondition\n\n\nclass DeltaMergeBuilder(object):\n    \"\"\"\n    Builder to specify how to merge data from source DataFrame into the target Delta table.\n    Use :py:meth:`delta.tables.DeltaTable.merge` to create an object of this class.\n    Using this builder, you can specify any number of ``whenMatched``, ``whenNotMatched`` and\n    ``whenNotMatchedBySource`` clauses. Here are the constraints on these clauses.\n\n    - Constraints in the ``whenMatched`` clauses:\n\n      - The condition in a ``whenMatched`` clause is optional. However, if there are multiple\n        ``whenMatched`` clauses, then only the last one may omit the condition.\n\n      - When there are more than one ``whenMatched`` clauses and there are conditions (or the lack\n        of) such that a row satisfies multiple clauses, then the action for the first clause\n        satisfied is executed. In other words, the order of the ``whenMatched`` clauses matters.\n\n      - If none of the ``whenMatched`` clauses match a source-target row pair that satisfy\n        the merge condition, then the target rows will not be updated or deleted.\n\n      - If you want to update all the columns of the target Delta table with the\n        corresponding column of the source DataFrame, then you can use the\n        ``whenMatchedUpdateAll()``. This is equivalent to::\n\n            whenMatchedUpdate(set = {\n              \"col1\": \"source.col1\",\n              \"col2\": \"source.col2\",\n              ...    # for all columns in the delta table\n            })\n\n    - Constraints in the ``whenNotMatched`` clauses:\n\n      - The condition in a ``whenNotMatched`` clause is optional. However, if there are\n        multiple ``whenNotMatched`` clauses, then only the last one may omit the condition.\n\n      - When there are more than one ``whenNotMatched`` clauses and there are conditions (or the\n        lack of) such that a row satisfies multiple clauses, then the action for the first clause\n        satisfied is executed. In other words, the order of the ``whenNotMatched`` clauses matters.\n\n      - If no ``whenNotMatched`` clause is present or if it is present but the non-matching source\n        row does not satisfy the condition, then the source row is not inserted.\n\n      - If you want to insert all the columns of the target Delta table with the\n        corresponding column of the source DataFrame, then you can use\n        ``whenNotMatchedInsertAll()``. This is equivalent to::\n\n            whenNotMatchedInsert(values = {\n              \"col1\": \"source.col1\",\n              \"col2\": \"source.col2\",\n              ...    # for all columns in the delta table\n            })\n\n    - Constraints in the ``whenNotMatchedBySource`` clauses:\n\n      - The condition in a ``whenNotMatchedBySource`` clause is optional. However, if there are\n        multiple ``whenNotMatchedBySource`` clauses, then only the last ``whenNotMatchedBySource``\n        clause may omit the condition.\n\n      - Conditions and update expressions  in ``whenNotMatchedBySource`` clauses may only refer to\n        columns from the target Delta table.\n\n      - When there are more than one ``whenNotMatchedBySource`` clauses and there are conditions (or\n        the lack of) such that a row satisfies multiple clauses, then the action for the first\n        clause satisfied is executed. In other words, the order of the ``whenNotMatchedBySource``\n        clauses matters.\n\n      - If no ``whenNotMatchedBySource`` clause is present or if it is present but the\n        non-matching target row does not satisfy any of the ``whenNotMatchedBySource`` clause\n        condition, then the target row will not be updated or deleted.\n\n    Example 1 with conditions and update expressions as SQL formatted string::\n\n        deltaTable.alias(\"events\").merge(\n            source = updatesDF.alias(\"updates\"),\n            condition = \"events.eventId = updates.eventId\"\n          ).whenMatchedUpdate(set =\n            {\n              \"data\": \"updates.data\",\n              \"count\": \"events.count + 1\"\n            }\n          ).whenNotMatchedInsert(values =\n            {\n              \"date\": \"updates.date\",\n              \"eventId\": \"updates.eventId\",\n              \"data\": \"updates.data\",\n              \"count\": \"1\",\n              \"missed_count\": \"0\"\n            }\n          ).whenNotMatchedBySourceUpdate(set =\n            {\n              \"missed_count\": \"events.missed_count + 1\"\n            }\n          ).execute()\n\n    Example 2 with conditions and update expressions as Spark SQL functions::\n\n        from pyspark.sql.functions import *\n\n        deltaTable.alias(\"events\").merge(\n            source = updatesDF.alias(\"updates\"),\n            condition = expr(\"events.eventId = updates.eventId\")\n          ).whenMatchedUpdate(set =\n            {\n              \"data\" : col(\"updates.data\"),\n              \"count\": col(\"events.count\") + 1\n            }\n          ).whenNotMatchedInsert(values =\n            {\n              \"date\": col(\"updates.date\"),\n              \"eventId\": col(\"updates.eventId\"),\n              \"data\": col(\"updates.data\"),\n              \"count\": lit(\"1\"),\n              \"missed_count\": lit(\"0\")\n            }\n          ).whenNotMatchedBySourceUpdate(set =\n            {\n              \"missed_count\": col(\"events.missed_count\") + 1\n            }\n          ).execute()\n\n    .. versionadded:: 0.4\n    \"\"\"\n    def __init__(self, spark: SparkSession, jbuilder: \"JavaObject\"):\n        self._spark = spark\n        self._jbuilder = jbuilder\n\n    @overload\n    def whenMatchedUpdate(\n        self, condition: OptionalExpressionOrColumn, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    @overload\n    def whenMatchedUpdate(\n        self, *, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    def whenMatchedUpdate(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        set: OptionalColumnMapping = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Update a matched table row based on the rules defined by ``set``.\n        If a ``condition`` is specified, then it must evaluate to true for the row to be updated.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the update\n        :type condition: str or pyspark.sql.Column\n        :param set: Defines the rules of setting the values of columns that need to be updated.\n                    *Note: This param is required.* Default value None is present to allow\n                    positional args in same order across languages.\n        :type set: dict with str as keys and str or pyspark.sql.Column as values\n        :return: this builder\n\n        .. versionadded:: 0.4\n        \"\"\"\n        jset = DeltaTable._dict_to_jmap(self._spark, set, \"'set' in whenMatchedUpdate\")\n        new_jbuilder = self.__getMatchedBuilder(condition).update(jset)\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def whenMatchedUpdateAll(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Update all the columns of the matched table row with the values of the  corresponding\n        columns in the source row. If a ``condition`` is specified, then it must be\n        true for the new row to be updated.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the insert\n        :type condition: str or pyspark.sql.Column\n        :return: this builder\n        \"\"\"\n        new_jbuilder = self.__getMatchedBuilder(condition).updateAll()\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def whenMatchedDelete(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Delete a matched row from the table only if the given ``condition`` (if specified) is\n        true for the matched row.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the delete\n        :type condition: str or pyspark.sql.Column\n        :return: this builder\n        \"\"\"\n        new_jbuilder = self.__getMatchedBuilder(condition).delete()\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @overload\n    def whenNotMatchedInsert(\n        self, condition: ExpressionOrColumn, values: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    @overload\n    def whenNotMatchedInsert(\n        self, *, values: ColumnMapping = ...\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    def whenNotMatchedInsert(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        values: OptionalColumnMapping = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Insert a new row to the target table based on the rules defined by ``values``. If a\n        ``condition`` is specified, then it must evaluate to true for the new row to be inserted.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the insert\n        :type condition: str or pyspark.sql.Column\n        :param values: Defines the rules of setting the values of columns that need to be updated.\n                       *Note: This param is required.* Default value None is present to allow\n                       positional args in same order across languages.\n        :type values: dict with str as keys and str or pyspark.sql.Column as values\n        :return: this builder\n\n        .. versionadded:: 0.4\n        \"\"\"\n        jvalues = DeltaTable._dict_to_jmap(self._spark, values, \"'values' in whenNotMatchedInsert\")\n        new_jbuilder = self.__getNotMatchedBuilder(condition).insert(jvalues)\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def whenNotMatchedInsertAll(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Insert a new target Delta table row by assigning the target columns to the values of the\n        corresponding columns in the source row. If a ``condition`` is specified, then it must\n        evaluate to true for the new row to be inserted.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the insert\n        :type condition: str or pyspark.sql.Column\n        :return: this builder\n        \"\"\"\n        new_jbuilder = self.__getNotMatchedBuilder(condition).insertAll()\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @overload\n    def whenNotMatchedBySourceUpdate(\n        self, condition: OptionalExpressionOrColumn, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    @overload\n    def whenNotMatchedBySourceUpdate(\n        self, *, set: ColumnMapping\n    ) -> \"DeltaMergeBuilder\":\n        ...\n\n    def whenNotMatchedBySourceUpdate(\n        self,\n        condition: OptionalExpressionOrColumn = None,\n        set: OptionalColumnMapping = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Update a target row that has no matches in the source based on the rules defined by ``set``.\n        If a ``condition`` is specified, then it must evaluate to true for the row to be updated.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the update\n        :type condition: str or pyspark.sql.Column\n        :param set: Defines the rules of setting the values of columns that need to be updated.\n                    *Note: This param is required.* Default value None is present to allow\n                    positional args in same order across languages.\n        :type set: dict with str as keys and str or pyspark.sql.Column as values\n        :return: this builder\n\n        .. versionadded:: 2.3\n        \"\"\"\n        jset = DeltaTable._dict_to_jmap(self._spark, set, \"'set' in whenNotMatchedBySourceUpdate\")\n        new_jbuilder = self.__getNotMatchedBySourceBuilder(condition).update(jset)\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @since(2.3)  # type: ignore[arg-type]\n    def whenNotMatchedBySourceDelete(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Delete a target row that has no matches in the source from the table only if the given\n        ``condition`` (if specified) is true for the target row.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :param condition: Optional condition of the delete\n        :type condition: str or pyspark.sql.Column\n        :return: this builder\n        \"\"\"\n        new_jbuilder = self.__getNotMatchedBySourceBuilder(condition).delete()\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @since(3.2)  # type: ignore[arg-type]\n    def withSchemaEvolution(self) -> \"DeltaMergeBuilder\":\n        \"\"\"\n        Enable schema evolution for the merge operation. This allows the target table schema to\n        be automatically updated based on the schema of the source DataFrame.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n\n        :return: this builder\n        \"\"\"\n        new_jbuilder = self._jbuilder.withSchemaEvolution()\n        return DeltaMergeBuilder(self._spark, new_jbuilder)\n\n    @since(0.4)  # type: ignore[arg-type]\n    def execute(self) -> DataFrame:\n        \"\"\"\n        Execute the merge operation based on the built matched and not matched actions.\n\n        See :py:class:`~delta.tables.DeltaMergeBuilder` for complete usage details.\n        \"\"\"\n        return DataFrame(\n            self._jbuilder.execute(),\n            getattr(self._spark, \"_wrapped\", self._spark))  # type: ignore[attr-defined]\n\n    def __getMatchedBuilder(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"JavaObject\":\n        if condition is None:\n            return self._jbuilder.whenMatched()\n        else:\n            return self._jbuilder.whenMatched(DeltaTable._condition_to_jcolumn(condition))\n\n    def __getNotMatchedBuilder(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"JavaObject\":\n        if condition is None:\n            return self._jbuilder.whenNotMatched()\n        else:\n            return self._jbuilder.whenNotMatched(DeltaTable._condition_to_jcolumn(condition))\n\n    def __getNotMatchedBySourceBuilder(\n        self, condition: OptionalExpressionOrColumn = None\n    ) -> \"JavaObject\":\n        if condition is None:\n            return self._jbuilder.whenNotMatchedBySource()\n        else:\n            return self._jbuilder.whenNotMatchedBySource(\n                DeltaTable._condition_to_jcolumn(condition))\n\n\n@dataclass\nclass IdentityGenerator:\n    \"\"\"\n    Identity generator specifications for the identity column in the Delta table.\n    :param start: the start for the identity column. Default is 1.\n    :type start: int\n    :param step: the step for the identity column. Default is 1.\n    :type step: int\n    \"\"\"\n    start: int = 1\n    step: int = 1\n\n\nclass DeltaTableBuilder(object):\n    \"\"\"\n    Builder to specify how to create / replace a Delta table.\n    You must specify the table name or the path before executing the builder.\n    You can specify the table columns, the partitioning columns,\n    the location of the data, the table comment and the property,\n    and how you want to create / replace the Delta table.\n\n    After executing the builder, a :py:class:`~delta.tables.DeltaTable`\n    object is returned.\n\n    Use :py:meth:`delta.tables.DeltaTable.create`,\n    :py:meth:`delta.tables.DeltaTable.createIfNotExists`,\n    :py:meth:`delta.tables.DeltaTable.replace`,\n    :py:meth:`delta.tables.DeltaTable.createOrReplace` to create an object of this class.\n\n    Example 1 to create a Delta table with separate columns, using the table name::\n\n        deltaTable = DeltaTable.create(sparkSession)\n            .tableName(\"testTable\")\n            .addColumn(\"c1\", dataType = \"INT\", nullable = False)\n            .addColumn(\"c2\", dataType = IntegerType(), generatedAlwaysAs = \"c1 + 1\")\n            .partitionedBy(\"c1\")\n            .execute()\n\n    Example 2 to replace a Delta table with existing columns, using the location::\n\n        df = spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n\n        deltaTable = DeltaTable.replace(sparkSession)\n            .tableName(\"testTable\")\n            .addColumns(df.schema)\n            .execute()\n\n    .. versionadded:: 1.0\n\n    .. note:: Evolving\n    \"\"\"\n    def __init__(self, spark: SparkSession, jbuilder: \"JavaObject\"):\n        self._spark = spark\n        self._jbuilder = jbuilder\n\n    def _raise_type_error(self, msg: str, objs: Iterable[Any]) -> NoReturn:\n        errorMsg = msg\n        for obj in objs:\n            errorMsg += \" Found %s with type %s\" % ((str(obj)), str(type(obj)))\n        raise TypeError(errorMsg)\n\n    def _check_identity_column_spec(self, identityGenerator: IdentityGenerator) -> None:\n        if identityGenerator.step == 0:\n            raise ValueError(\"Column identity generation requires step to be non-zero.\")\n\n    @since(1.0)  # type: ignore[arg-type]\n    def tableName(self, identifier: str) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify the table name.\n        Optionally qualified with a database name [database_name.] table_name.\n\n        :param identifier: the table name\n        :type identifier: str\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        if type(identifier) is not str:\n            self._raise_type_error(\"Identifier must be str.\", [identifier])\n        self._jbuilder = self._jbuilder.tableName(identifier)\n        return self\n\n    @since(1.0)  # type: ignore[arg-type]\n    def location(self, location: str) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify the path to the directory where table data is stored,\n        which could be a path on distributed storage.\n\n        :param location: the data stored location\n        :type location: str\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        if type(location) is not str:\n            self._raise_type_error(\"Location must be str.\", [location])\n        self._jbuilder = self._jbuilder.location(location)\n        return self\n\n    @since(1.0)  # type: ignore[arg-type]\n    def comment(self, comment: str) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Comment to describe the table.\n\n        :param comment: the table comment\n        :type comment: str\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        if type(comment) is not str:\n            self._raise_type_error(\"Table comment must be str.\", [comment])\n        self._jbuilder = self._jbuilder.comment(comment)\n        return self\n\n    @since(1.0)  # type: ignore[arg-type]\n    def addColumn(\n        self,\n        colName: str,\n        dataType: Union[str, DataType],\n        nullable: bool = True,\n        generatedAlwaysAs: Optional[Union[str, IdentityGenerator]] = None,\n        generatedByDefaultAs: Optional[IdentityGenerator] = None,\n        comment: Optional[str] = None,\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify a column in the table\n\n        :param colName: the column name\n        :type colName: str\n        :param dataType: the column data type\n        :type dataType: str or pyspark.sql.types.DataType\n        :param nullable: whether column is nullable\n        :type nullable: bool\n        :param generatedAlwaysAs: a SQL expression if the column is always generated\n                                  as a function of other columns;\n                                  an IdentityGenerator object if the column is always\n                                  generated using identity generator\n                                  See online documentation for details on Generated Columns.\n        :type generatedAlwaysAs: str or delta.tables.IdentityGenerator\n        :param generatedByDefaultAs: an IdentityGenerator object to generate identity values\n                                     if the user does not provide values for the column\n                                  See online documentation for details on Generated Columns.\n        :type generatedByDefaultAs: delta.tables.IdentityGenerator\n        :param comment: the column comment\n        :type comment: str\n\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        if type(colName) is not str:\n            self._raise_type_error(\"Column name must be str.\", [colName])\n        if type(dataType) is not str and not isinstance(dataType, DataType):\n            self._raise_type_error(\"Column data type must be str or DataType.\",\n                                   [dataType])\n\n        jvm: \"JVMView\" = self._spark._sc._jvm  # type: ignore[attr-defined]\n        jsparkSession: \"JavaObject\" = self._spark._jsparkSession  # type: ignore[attr-defined]\n\n        _col_jbuilder = jvm.io.delta.tables.DeltaTable.columnBuilder(jsparkSession, colName)\n        if isinstance(dataType, DataType):\n            dataType = jsparkSession.parseDataType(dataType.json())\n        _col_jbuilder = _col_jbuilder.dataType(dataType)\n        if type(nullable) is not bool:\n            self._raise_type_error(\"Column nullable must be bool.\", [nullable])\n        _col_jbuilder = _col_jbuilder.nullable(nullable)\n\n        if generatedAlwaysAs is not None and generatedByDefaultAs is not None:\n            raise ValueError(\n                \"generatedByDefaultAs and generatedAlwaysAs cannot both be set.\",\n                [generatedByDefaultAs, generatedAlwaysAs])\n        if generatedAlwaysAs is not None:\n            if type(generatedAlwaysAs) is str:\n                _col_jbuilder = _col_jbuilder.generatedAlwaysAs(generatedAlwaysAs)\n            elif isinstance(generatedAlwaysAs, IdentityGenerator):\n                self._check_identity_column_spec(generatedAlwaysAs)\n                _col_jbuilder = _col_jbuilder.generatedAlwaysAsIdentity(\n                    generatedAlwaysAs.start, generatedAlwaysAs.step)\n            else:\n                self._raise_type_error(\n                    \"Generated always as expression must be str or IdentityGenerator.\",\n                    [generatedAlwaysAs])\n        elif generatedByDefaultAs is not None:\n            if not isinstance(generatedByDefaultAs, IdentityGenerator):\n                self._raise_type_error(\n                    \"Generated by default expression must be IdentityGenerator.\",\n                    [generatedByDefaultAs])\n            self._check_identity_column_spec(generatedByDefaultAs)\n            _col_jbuilder = _col_jbuilder.generatedByDefaultAsIdentity(\n                generatedByDefaultAs.start, generatedByDefaultAs.step)\n\n        if comment is not None:\n            if type(comment) is not str:\n                self._raise_type_error(\"Column comment must be str.\", [comment])\n            _col_jbuilder = _col_jbuilder.comment(comment)\n        self._jbuilder = self._jbuilder.addColumn(_col_jbuilder.build())\n        return self\n\n    @since(1.0)  # type: ignore[arg-type]\n    def addColumns(\n        self, cols: Union[StructType, List[StructField]]\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify columns in the table using an existing schema\n\n        :param cols: the columns in the existing schema\n        :type cols: pyspark.sql.types.StructType\n                    or a list of pyspark.sql.types.StructType.\n\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        if isinstance(cols, list):\n            for col in cols:\n                if type(col) is not StructField:\n                    self._raise_type_error(\n                        \"Column in existing schema must be StructField.\", [col])\n            cols = StructType(cols)\n        if type(cols) is not StructType:\n            self._raise_type_error(\"Schema must be StructType \" +\n                                   \"or a list of StructField.\",\n                                   [cols])\n\n        jsparkSession: \"JavaObject\" = self._spark._jsparkSession  # type: ignore[attr-defined]\n\n        scalaSchema = jsparkSession.parseDataType(cols.json())\n        self._jbuilder = self._jbuilder.addColumns(scalaSchema)\n        return self\n\n    @overload\n    def partitionedBy(\n        self, *cols: str\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    @overload\n    def partitionedBy(\n        self, __cols: Union[List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    @since(1.0)  # type: ignore[arg-type]\n    def partitionedBy(\n        self, *cols: Union[str, List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify columns for partitioning\n\n        :param cols: the partitioning cols\n        :type cols: str or list name of columns\n\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        try:\n            from pyspark.sql.column import _to_seq  # type: ignore[attr-defined]\n        except ImportError:\n            # Spark 4\n            from pyspark.sql.classic.column import _to_seq  # type: ignore[import, no-redef]\n\n        if len(cols) == 1 and isinstance(cols[0], (list, tuple)):\n            cols = cols[0]  # type: ignore[assignment]\n        for c in cols:\n            if type(c) is not str:\n                self._raise_type_error(\"Partitioning column must be str.\", [c])\n        self._jbuilder = self._jbuilder.partitionedBy(_to_seq(\n            self._spark._sc,  # type: ignore[attr-defined]\n            cast(Iterable[Union[Column, str]], cols)\n        ))\n        return self\n\n    @overload\n    def clusterBy(\n        self, *cols: str\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    @overload\n    def clusterBy(\n        self, __cols: Union[List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        ...\n\n    @since(3.2)  # type: ignore[arg-type]\n    def clusterBy(\n        self, *cols: Union[str, List[str], Tuple[str, ...]]\n    ) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify columns for clustering\n\n        :param cols: the clustering cols\n        :type cols: str or list name of columns\n\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        try:\n            from pyspark.sql.column import _to_seq  # type: ignore[attr-defined]\n        except ImportError:\n            # Spark 4\n            from pyspark.sql.classic.column import _to_seq  # type: ignore[import, no-redef]\n\n        if len(cols) == 1 and isinstance(cols[0], (list, tuple)):\n            cols = cols[0]  # type: ignore[assignment]\n        for c in cols:\n            if type(c) is not str:\n                self._raise_type_error(\"Clustering column must be str.\", [c])\n        self._jbuilder = self._jbuilder.clusterBy(_to_seq(\n            self._spark._sc,  # type: ignore[attr-defined]\n            cast(Iterable[Union[Column, str]], cols)\n        ))\n        return self\n\n    @since(1.0)  # type: ignore[arg-type]\n    def property(self, key: str, value: str) -> \"DeltaTableBuilder\":\n        \"\"\"\n        Specify a table property\n\n        :param key: the table property key\n        :type value: the table property value\n\n        :return: this builder\n\n        .. note:: Evolving\n        \"\"\"\n        if type(key) is not str or type(value) is not str:\n            self._raise_type_error(\"Key and value of property must be string.\",\n                                   [key, value])\n        self._jbuilder = self._jbuilder.property(key, value)\n        return self\n\n    @since(1.0)  # type: ignore[arg-type]\n    def execute(self) -> DeltaTable:\n        \"\"\"\n        Execute Table Creation.\n\n        :rtype: :py:class:`~delta.tables.DeltaTable`\n\n        .. note:: Evolving\n        \"\"\"\n        jdt = self._jbuilder.execute()\n        return DeltaTable(self._spark, jdt)\n\n\nclass DeltaOptimizeBuilder(object):\n    \"\"\"\n    Builder class for constructing OPTIMIZE command and executing.\n\n    Use :py:meth:`delta.tables.DeltaTable.optimize` to create an instance of this class.\n\n    .. versionadded:: 2.0.0\n    \"\"\"\n    def __init__(self, spark: SparkSession, jbuilder: \"JavaObject\"):\n        self._spark = spark\n        self._jbuilder = jbuilder\n\n    @since(2.0)  # type: ignore[arg-type]\n    def where(self, partitionFilter: str) -> \"DeltaOptimizeBuilder\":\n        \"\"\"\n        Apply partition filter on this optimize command builder to limit\n        the operation on selected partitions.\n\n        :param partitionFilter: The partition filter to apply\n        :type partitionFilter: str\n        :return: DeltaOptimizeBuilder with partition filter applied\n        :rtype: :py:class:`~delta.tables.DeltaOptimizeBuilder`\n        \"\"\"\n        self._jbuilder = self._jbuilder.where(partitionFilter)\n        return self\n\n    @since(2.0)  # type: ignore[arg-type]\n    def executeCompaction(self) -> DataFrame:\n        \"\"\"\n        Compact the small files in selected partitions.\n\n        :return: DataFrame containing the OPTIMIZE execution metrics\n        :rtype: pyspark.sql.DataFrame\n        \"\"\"\n        return DataFrame(\n            self._jbuilder.executeCompaction(),\n            getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n        )\n\n    @since(2.0)  # type: ignore[arg-type]\n    def executeZOrderBy(self, *cols: Union[str, List[str], Tuple[str, ...]]) -> DataFrame:\n        \"\"\"\n        Z-Order the data in selected partitions using the given columns.\n\n        :param cols: the Z-Order cols\n        :type cols: str or list name of columns\n\n        :return: DataFrame containing the OPTIMIZE execution metrics\n        :rtype: pyspark.sql.DataFrame\n        \"\"\"\n        try:\n            from pyspark.sql.column import _to_seq  # type: ignore[attr-defined]\n        except ImportError:\n            # Spark 4\n            from pyspark.sql.classic.column import _to_seq  # type: ignore[import, no-redef]\n\n        if len(cols) == 1 and isinstance(cols[0], (list, tuple)):\n            cols = cols[0]  # type: ignore[assignment]\n        for c in cols:\n            if type(c) is not str:\n                errorMsg = \"Z-order column must be str. \"\n                errorMsg += \"Found %s with type %s\" % ((str(c)), str(type(c)))\n                raise TypeError(errorMsg)\n\n        return DataFrame(\n            self._jbuilder.executeZOrderBy(_to_seq(\n                self._spark._sc,  # type: ignore[attr-defined]\n                cast(Iterable[Union[Column, str]], cols)\n            )),\n            getattr(self._spark, \"_wrapped\", self._spark)  # type: ignore[attr-defined]\n        )\n"
  },
  {
    "path": "python/delta/testing/__init__.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n__all__ = ['utils']\n"
  },
  {
    "path": "python/delta/testing/log4j2.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Set everything to be logged to the console\nrootLogger.level = warn\nrootLogger.appenderRef.stdout.ref = STDOUT\n\nappender.console.type = Console\nappender.console.name = STDOUT\nappender.console.target = SYSTEM_OUT\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = %d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n\n\n# Settings to quiet third party logs that are too verbose\nlogger.jetty.name = org.sparkproject.jetty\nlogger.jetty.level = warn\nlogger.jetty2.name = org.sparkproject.jetty.util.component.AbstractLifeCycle\nlogger.jetty2.level = error\nlogger.repl1.name = org.apache.spark.repl.SparkIMain$exprTyper\nlogger.repl1.level = info\nlogger.repl2.name = org.apache.spark.repl.SparkILoop$SparkILoopInterpreter\nlogger.repl2.level = info\n\n# Set the default spark-shell log level to WARN. When running the spark-shell, the\n# log level for this class is used to overwrite the root logger's log level, so that\n# the user can have different defaults for the shell and regular Spark apps.\nlogger.repl.name = org.apache.spark.repl.Main\nlogger.repl.level = warn\n\n# SPARK-9183: Settings to avoid annoying messages when looking up nonexistent UDFs\n# in SparkSQL with Hive support\nlogger.metastore.name = org.apache.hadoop.hive.metastore.RetryingHMSHandler\nlogger.metastore.level = fatal\nlogger.hive_functionregistry.name = org.apache.hadoop.hive.ql.exec.FunctionRegistry\nlogger.hive_functionregistry.level = error\n\n# Parquet related logging\nlogger.parquet.name = org.apache.parquet.CorruptStatistics\nlogger.parquet.level = error\nlogger.parquet2.name = parquet.CorruptStatistics\nlogger.parquet2.level = error\n\n"
  },
  {
    "path": "python/delta/testing/utils.py",
    "content": "#\n# Copyright (2023) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport shutil\nimport sys\nimport tempfile\nimport unittest\nimport uuid\n\nfrom contextlib import contextmanager\nfrom pyspark import SparkConf\nfrom pyspark.testing.sqlutils import ReusedSQLTestCase  # type: ignore[import]\nfrom typing import Generator\n\n\nclass DeltaTestCase(ReusedSQLTestCase):\n    \"\"\"Test class base that sets up a correctly configured SparkSession for querying Delta tables.\n    \"\"\"\n\n    @classmethod\n    def conf(cls) -> SparkConf:\n        _conf = super(DeltaTestCase, cls).conf()\n        _conf.set(\"spark.app.name\", cls.__name__)\n        _conf.set(\"spark.master\", \"local[4]\")\n        _conf.set(\"spark.ui.enabled\", \"false\")\n        _conf.set(\"spark.databricks.delta.snapshotPartitions\", \"2\")\n        _conf.set(\"spark.sql.shuffle.partitions\", \"5\")\n        _conf.set(\"delta.log.cacheSize\", \"3\")\n        _conf.set(\"spark.databricks.delta.delta.log.cacheSize\", \"3\")\n        _conf.set(\"spark.sql.sources.parallelPartitionDiscovery.parallelism\", \"5\")\n        _conf.set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n        _conf.set(\"spark.sql.catalog.spark_catalog\",\n                  \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n        return _conf\n\n    def setUp(self) -> None:\n        super(DeltaTestCase, self).setUp()\n        self.tempPath = tempfile.mkdtemp()\n        self.tempFile = os.path.join(self.tempPath, \"tempFile\")\n\n    def tearDown(self) -> None:\n        super(DeltaTestCase, self).tearDown()\n        shutil.rmtree(self.tempPath, ignore_errors=True)\n\n    @contextmanager\n    def tempTable(self) -> Generator[str, None, None]:\n        table_name = \"table_\" + str(uuid.uuid4()).replace(\"-\", \"_\")\n\n        with super(DeltaTestCase, self).table(table_name):\n            yield table_name\n"
  },
  {
    "path": "python/delta/tests/__init__.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "python/delta/tests/test_deltatable.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# mypy: disable-error-code=\"union-attr, attr-defined\"\n\nimport unittest\nimport os\nfrom multiprocessing.pool import ThreadPool\nfrom typing import List, Set, Dict, Optional, Any, Callable, Union, Tuple\n\nfrom py4j.java_gateway import JavaObject\nfrom py4j.protocol import Py4JJavaError\nfrom pyspark.errors.exceptions.base import UnsupportedOperationException\nfrom pyspark.sql import DataFrame, Row\nfrom pyspark.sql.functions import col, lit, expr, floor\nfrom pyspark.sql.types import StructType, StructField, StringType, IntegerType, LongType, DataType\nfrom pyspark.sql.utils import AnalysisException, ParseException\n\nfrom delta.tables import DeltaTable, DeltaTableBuilder, DeltaOptimizeBuilder, IdentityGenerator\nfrom delta.testing.utils import DeltaTestCase\n\n\nclass DeltaTableTestsMixin:\n\n    def test_forPath(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile).toDF()\n        self.__checkAnswer(dt, [('a', 1), ('b', 2), ('c', 3)])\n\n    def test_forPathWithOptions(self) -> None:\n        path = self.tempFile\n        fsOptions = {\"fs.fake.impl\": \"org.apache.spark.sql.delta.FakeFileSystem\",\n                     \"fs.fake.impl.disable.cache\": \"true\"}\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3)])\n        dt = DeltaTable.forPath(self.spark, path, fsOptions).toDF()\n        self.__checkAnswer(dt, [('a', 1), ('b', 2), ('c', 3)])\n\n    def test_forName(self) -> None:\n        with self.tempTable() as tableName:\n            self.__writeAsTable([('a', 1), ('b', 2), ('c', 3)], tableName)\n            df = DeltaTable.forName(self.spark, tableName).toDF()\n            self.__checkAnswer(df, [('a', 1), ('b', 2), ('c', 3)])\n\n    def test_alias_and_toDF(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile).toDF()\n        self.__checkAnswer(\n            dt.alias(\"myTable\").select('myTable.key', 'myTable.value'),\n            [('a', 1), ('b', 2), ('c', 3)])\n\n    def test_delete(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3), ('d', 4)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # delete with condition as str\n        dt.delete(\"key = 'a'\")\n        self.__checkAnswer(dt.toDF(), [('b', 2), ('c', 3), ('d', 4)])\n\n        # delete with condition as Column\n        dt.delete(col(\"key\") == lit(\"b\"))\n        self.__checkAnswer(dt.toDF(), [('c', 3), ('d', 4)])\n\n        # delete without condition\n        dt.delete()\n        self.__checkAnswer(dt.toDF(), [])\n\n        # bad args\n        with self.assertRaises(TypeError):\n            dt.delete(condition=1)  # type: ignore[arg-type]\n\n    def test_generate(self) -> None:\n        # create a delta table\n        numFiles = 10\n        self.spark.range(100).repartition(numFiles).write.format(\"delta\").save(self.tempFile)\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # Generate the symlink format manifest\n        dt.generate(\"symlink_format_manifest\")\n\n        # check the contents of the manifest\n        # NOTE: this is not a correctness test, we are testing correctness in the scala suite\n        manifestPath = os.path.join(self.tempFile,\n                                    os.path.join(\"_symlink_format_manifest\", \"manifest\"))\n        files = []\n        with open(manifestPath) as f:\n            files = f.readlines()\n\n        # the number of files we write should equal the number of lines in the manifest\n        self.assertEqual(len(files), numFiles)\n\n    def test_update(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3), ('d', 4)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # update with condition as str and with set exprs as str\n        dt.update(\"key = 'a' or key = 'b'\", {\"value\": \"1\"})\n        self.__checkAnswer(dt.toDF(), [('a', 1), ('b', 1), ('c', 3), ('d', 4)])\n\n        # update with condition as Column and with set exprs as Columns\n        dt.update(expr(\"key = 'a' or key = 'b'\"), {\"value\": expr(\"0\")})\n        self.__checkAnswer(dt.toDF(), [('a', 0), ('b', 0), ('c', 3), ('d', 4)])\n\n        # update without condition\n        dt.update(set={\"value\": \"200\"})\n        self.__checkAnswer(dt.toDF(), [('a', 200), ('b', 200), ('c', 200), ('d', 200)])\n\n        # bad args\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            dt.update({\"value\": \"200\"})  # type: ignore[call-overload]\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            dt.update(condition='a')  # type: ignore[call-overload]\n\n        with self.assertRaisesRegex(TypeError, \"must be a dict\"):\n            dt.update(set=1)  # type: ignore[call-overload]\n\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            dt.update(1, {})  # type: ignore[call-overload]\n\n        with self.assertRaisesRegex(TypeError, \"Values of dict in .* must contain only\"):\n            dt.update(set={\"value\": 1})  # type: ignore[dict-item]\n\n        with self.assertRaisesRegex(TypeError, \"Keys of dict in .* must contain only\"):\n            dt.update(set={1: \"\"})  # type: ignore[dict-item]\n\n        with self.assertRaises(TypeError):\n            dt.update(set=1)  # type: ignore[call-overload]\n\n    def test_merge(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3), ('d', 4)])\n        source = self.spark.createDataFrame([('a', -1), ('b', 0), ('e', -5), ('f', -6)], [\"k\", \"v\"])\n\n        def reset_table() -> None:\n            self.__overwriteDeltaTable([('a', 1), ('b', 2), ('c', 3), ('d', 4)])\n\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # ============== Test basic syntax ==============\n\n        # String expressions in merge condition and dicts\n        reset_table()\n        merge_output = dt.merge(source, \"key = k\") \\\n            .whenMatchedUpdate(set={\"value\": \"v + 0\"}) \\\n            .whenNotMatchedInsert(values={\"key\": \"k\", \"value\": \"v + 0\"}) \\\n            .whenNotMatchedBySourceUpdate(set={\"value\": \"value + 0\"}) \\\n            .execute()\n        self.__checkAnswer(merge_output,\n                           ([Row(6,  # type: ignore[call-overload]\n                                 4,  # updated rows (a and b in WHEN MATCHED\n                                     # and c and d in WHEN NOT MATCHED BY SOURCE)\n                                 0,  # deleted rows\n                                 2)]),  # inserted rows (e and f)\n                           StructType([StructField('num_affected_rows', LongType(), False),\n                                        StructField('num_updated_rows', LongType(), False),\n                                        StructField('num_deleted_rows', LongType(), False),\n                                        StructField('num_inserted_rows', LongType(), False)]))\n        self.__checkAnswer(dt.toDF(),\n                           ([('a', -1), ('b', 0), ('c', 3), ('d', 4), ('e', -5), ('f', -6)]))\n\n        # Column expressions in merge condition and dicts\n        reset_table()\n        merge_output = dt.merge(source, expr(\"key = k\")) \\\n            .whenMatchedUpdate(set={\"value\": col(\"v\") + 0}) \\\n            .whenNotMatchedInsert(values={\"key\": \"k\", \"value\": col(\"v\") + 0}) \\\n            .whenNotMatchedBySourceUpdate(set={\"value\": col(\"value\") + 0}) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(),\n                           ([('a', -1), ('b', 0), ('c', 3), ('d', 4), ('e', -5), ('f', -6)]))\n\n        # Multiple matched update clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenMatchedUpdate(condition=\"key = 'a'\", set={\"value\": \"5\"}) \\\n            .whenMatchedUpdate(set={\"value\": \"0\"}) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', 5), ('b', 0), ('c', 3), ('d', 4)]))\n\n        # Multiple matched delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenMatchedDelete(condition=\"key = 'a'\") \\\n            .whenMatchedDelete() \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('c', 3), ('d', 4)]))\n\n        # Redundant matched update and delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenMatchedUpdate(condition=\"key = 'a'\", set={\"value\": \"5\"}) \\\n            .whenMatchedUpdate(condition=\"key = 'a'\", set={\"value\": \"0\"}) \\\n            .whenMatchedUpdate(condition=\"key = 'b'\", set={\"value\": \"6\"}) \\\n            .whenMatchedDelete(condition=\"key = 'b'\") \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', 5), ('b', 6), ('c', 3), ('d', 4)]))\n\n        # Interleaved matched update and delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenMatchedDelete(condition=\"key = 'a'\") \\\n            .whenMatchedUpdate(condition=\"key = 'a'\", set={\"value\": \"5\"}) \\\n            .whenMatchedDelete(condition=\"key = 'b'\") \\\n            .whenMatchedUpdate(set={\"value\": \"6\"}) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('c', 3), ('d', 4)]))\n\n        # Multiple not matched insert clauses\n        reset_table()\n        dt.alias(\"t\")\\\n            .merge(source.toDF(\"key\", \"value\").alias(\"s\"), expr(\"t.key = s.key\")) \\\n            .whenNotMatchedInsert(condition=\"s.key = 'e'\",\n                                  values={\"t.key\": \"s.key\", \"t.value\": \"5\"}) \\\n            .whenNotMatchedInsertAll() \\\n            .execute()\n        self.__checkAnswer(dt.toDF(),\n                           ([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', -6)]))\n\n        # Redundant not matched update and delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenNotMatchedInsert(condition=\"k = 'e'\", values={\"key\": \"k\", \"value\": \"5\"}) \\\n            .whenNotMatchedInsert(condition=\"k = 'e'\", values={\"key\": \"k\", \"value\": \"6\"}) \\\n            .whenNotMatchedInsert(condition=\"k = 'f'\", values={\"key\": \"k\", \"value\": \"7\"}) \\\n            .whenNotMatchedInsert(condition=\"k = 'f'\", values={\"key\": \"k\", \"value\": \"8\"}) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(),\n                           ([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', 7)]))\n\n        # Multiple not matched by source update clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenNotMatchedBySourceUpdate(condition=\"key = 'c'\", set={\"value\": \"5\"}) \\\n            .whenNotMatchedBySourceUpdate(set={\"value\": \"0\"}) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', 1), ('b', 2), ('c', 5), ('d', 0)]))\n\n        # Multiple not matched by source delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenNotMatchedBySourceDelete(condition=\"key = 'c'\") \\\n            .whenNotMatchedBySourceDelete() \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', 1), ('b', 2)]))\n\n        # Redundant not matched by source update and delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenNotMatchedBySourceUpdate(condition=\"key = 'c'\", set={\"value\": \"5\"}) \\\n            .whenNotMatchedBySourceUpdate(condition=\"key = 'c'\", set={\"value\": \"0\"}) \\\n            .whenNotMatchedBySourceUpdate(condition=\"key = 'd'\", set={\"value\": \"6\"}) \\\n            .whenNotMatchedBySourceDelete(condition=\"key = 'd'\") \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', 1), ('b', 2), ('c', 5), ('d', 6)]))\n\n        # Interleaved update and delete clauses\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenNotMatchedBySourceDelete(condition=\"key = 'c'\") \\\n            .whenNotMatchedBySourceUpdate(condition=\"key = 'c'\", set={\"value\": \"5\"}) \\\n            .whenNotMatchedBySourceDelete(condition=\"key = 'd'\") \\\n            .whenNotMatchedBySourceUpdate(set={\"value\": \"6\"}) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', 1), ('b', 2)]))\n\n        # ============== Test clause conditions ==============\n\n        # String expressions in all conditions and dicts\n        reset_table()\n        dt.merge(source, \"key = k\") \\\n            .whenMatchedUpdate(condition=\"k = 'a'\", set={\"value\": \"v + 0\"}) \\\n            .whenMatchedDelete(condition=\"k = 'b'\") \\\n            .whenNotMatchedInsert(condition=\"k = 'e'\", values={\"key\": \"k\", \"value\": \"v + 0\"}) \\\n            .whenNotMatchedBySourceUpdate(condition=\"key = 'c'\", set={\"value\": col(\"value\") + 0}) \\\n            .whenNotMatchedBySourceDelete(condition=\"key = 'd'\") \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', -1), ('c', 3), ('e', -5)]))\n\n        # Column expressions in all conditions and dicts\n        reset_table()\n        dt.merge(source, expr(\"key = k\")) \\\n            .whenMatchedUpdate(\n                condition=expr(\"k = 'a'\"),\n                set={\"value\": col(\"v\") + 0}) \\\n            .whenMatchedDelete(condition=expr(\"k = 'b'\")) \\\n            .whenNotMatchedInsert(\n                condition=expr(\"k = 'e'\"),\n                values={\"key\": \"k\", \"value\": col(\"v\") + 0}) \\\n            .whenNotMatchedBySourceUpdate(\n                condition=expr(\"key = 'c'\"),\n                set={\"value\": col(\"value\") + 0}) \\\n            .whenNotMatchedBySourceDelete(condition=expr(\"key = 'd'\")) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', -1), ('c', 3), ('e', -5)]))\n\n        # Positional arguments\n        reset_table()\n        dt.merge(source, \"key = k\") \\\n            .whenMatchedUpdate(\"k = 'a'\", {\"value\": \"v + 0\"}) \\\n            .whenMatchedDelete(\"k = 'b'\") \\\n            .whenNotMatchedInsert(\"k = 'e'\", {\"key\": \"k\", \"value\": \"v + 0\"}) \\\n            .whenNotMatchedBySourceUpdate(\"key = 'c'\", {\"value\": \"value + 0\"}) \\\n            .whenNotMatchedBySourceDelete(\"key = 'd'\") \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', -1), ('c', 3), ('e', -5)]))\n\n        # ============== Test updateAll/insertAll ==============\n\n        # No clause conditions and insertAll/updateAll + aliases\n        reset_table()\n        dt.alias(\"t\") \\\n            .merge(source.toDF(\"key\", \"value\").alias(\"s\"), expr(\"t.key = s.key\")) \\\n            .whenMatchedUpdateAll() \\\n            .whenNotMatchedInsertAll() \\\n            .execute()\n        self.__checkAnswer(dt.toDF(),\n                           ([('a', -1), ('b', 0), ('c', 3), ('d', 4), ('e', -5), ('f', -6)]))\n\n        # String expressions in all clause conditions and insertAll/updateAll + aliases\n        reset_table()\n        dt.alias(\"t\") \\\n            .merge(source.toDF(\"key\", \"value\").alias(\"s\"), \"s.key = t.key\") \\\n            .whenMatchedUpdateAll(\"s.key = 'a'\") \\\n            .whenNotMatchedInsertAll(\"s.key = 'e'\") \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', -1), ('b', 2), ('c', 3), ('d', 4), ('e', -5)]))\n\n        # Column expressions in all clause conditions and insertAll/updateAll + aliases\n        reset_table()\n        dt.alias(\"t\") \\\n            .merge(source.toDF(\"key\", \"value\").alias(\"s\"), expr(\"t.key = s.key\")) \\\n            .whenMatchedUpdateAll(expr(\"s.key = 'a'\")) \\\n            .whenNotMatchedInsertAll(expr(\"s.key = 'e'\")) \\\n            .execute()\n        self.__checkAnswer(dt.toDF(), ([('a', -1), ('b', 2), ('c', 3), ('d', 4), ('e', -5)]))\n\n        # Schema evolution\n        reset_table()\n        dt.alias(\"t\") \\\n            .merge(source.toDF(\"key\", \"extra\").alias(\"s\"), expr(\"t.key = s.key\")) \\\n            .whenMatchedUpdate(set={\"extra\": \"-1\"}) \\\n            .whenNotMatchedInsertAll() \\\n            .withSchemaEvolution() \\\n            .execute()\n        self.__checkAnswer(\n            DeltaTable.forPath(self.spark, self.tempFile).toDF(),  # reload the table\n            ([('a', 1, -1), ('b', 2, -1), ('c', 3, None), ('d', 4, None), ('e', None, -5),\n              ('f', None, -6)]),\n            [\"key\", \"value\", \"extra\"])\n\n        # ============== Test bad args ==============\n        # ---- bad args in merge()\n        with self.assertRaisesRegex(TypeError, \"must be DataFrame\"):\n            dt.merge(1, \"key = k\")  # type: ignore[arg-type]\n\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            dt.merge(source, 1)  # type: ignore[arg-type]\n\n        # ---- bad args in whenMatchedUpdate()\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate({\"value\": \"v\"}))\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(1))\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(condition=\"key = 'a'\"))\n\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(1, {\"value\": \"v\"}))\n\n        with self.assertRaisesRegex(TypeError, \"must be a dict\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(\"k = 'a'\", 1))\n\n        with self.assertRaisesRegex(TypeError, \"Values of dict in .* must contain only\"):\n            (dt\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(set={\"value\": 1}))  # type: ignore[dict-item]\n\n        with self.assertRaisesRegex(TypeError, \"Keys of dict in .* must contain only\"):\n            (dt\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(set={1: \"\"}))  # type: ignore[dict-item]\n\n        with self.assertRaises(TypeError):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenMatchedUpdate(set=\"k = 'a'\", condition={\"value\": 1}))\n\n        # bad args in whenMatchedDelete()\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            dt.merge(source, \"key = k\").whenMatchedDelete(1)  # type: ignore[arg-type]\n\n        # ---- bad args in whenNotMatchedInsert()\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert({\"value\": \"v\"}))\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            dt.merge(source, \"key = k\").whenNotMatchedInsert(1)  # type: ignore[call-overload]\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert(condition=\"key = 'a'\"))\n\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert(1, {\"value\": \"v\"}))\n\n        with self.assertRaisesRegex(TypeError, \"must be a dict\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert(\"k = 'a'\", 1))\n\n        with self.assertRaisesRegex(TypeError, \"Values of dict in .* must contain only\"):\n            (dt\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert(values={\"value\": 1}))  # type: ignore[dict-item]\n\n        with self.assertRaisesRegex(TypeError, \"Keys of dict in .* must contain only\"):\n            (dt\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert(values={1: \"value\"}))  # type: ignore[dict-item]\n\n        with self.assertRaises(TypeError):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedInsert(values=\"k = 'a'\", condition={\"value\": 1}))\n\n        # ---- bad args in whenNotMatchedBySourceUpdate()\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate({\"value\": \"value\"}))\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(1))\n\n        with self.assertRaisesRegex(ValueError, \"cannot be None\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(condition=\"key = 'a'\"))\n\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(1, {\"value\": \"value\"}))\n\n        with self.assertRaisesRegex(TypeError, \"must be a dict\"):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(\"key = 'a'\", 1))\n\n        with self.assertRaisesRegex(TypeError, \"Values of dict in .* must contain only\"):\n            (dt\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(set={\"value\": 1}))  # type: ignore[dict-item]\n\n        with self.assertRaisesRegex(TypeError, \"Keys of dict in .* must contain only\"):\n            (dt\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(set={1: \"\"}))  # type: ignore[dict-item]\n\n        with self.assertRaises(TypeError):\n            (dt  # type: ignore[call-overload]\n                .merge(source, \"key = k\")\n                .whenNotMatchedBySourceUpdate(set=\"key = 'a'\", condition={\"value\": 1}))\n\n        # bad args in whenNotMatchedBySourceDelete()\n        with self.assertRaisesRegex(TypeError, \"must be a Spark SQL Column or a string\"):\n            dt.merge(source, \"key = k\").whenNotMatchedBySourceDelete(1)  # type: ignore[arg-type]\n\n    def test_merge_with_inconsistent_sessions(self) -> None:\n        source_path = os.path.join(self.tempFile, \"source\")\n        target_path = os.path.join(self.tempFile, \"target\")\n        spark = self.spark\n\n        def f(spark):  # type: ignore[no-untyped-def]\n            spark.range(20) \\\n                .withColumn(\"x\", col(\"id\")) \\\n                .withColumn(\"y\", col(\"id\")) \\\n                .write.mode(\"overwrite\").format(\"delta\").save(source_path)\n            spark.range(1) \\\n                .withColumn(\"x\", col(\"id\")) \\\n                .write.mode(\"overwrite\").format(\"delta\").save(target_path)\n            target = DeltaTable.forPath(spark, target_path)\n            source = spark.read.format(\"delta\").load(source_path).alias(\"s\")\n            target.alias(\"t\") \\\n                .merge(source, \"t.id = s.id\") \\\n                .whenMatchedUpdate(set={\"t.x\": \"t.x + 1\"}) \\\n                .whenNotMatchedInsertAll() \\\n                .execute()\n            assert(spark.read.format(\"delta\").load(target_path).count() == 20)\n\n        pool = ThreadPool(3)\n        spark.conf.set(\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")\n        try:\n            pool.starmap(f, [(spark,)])\n        finally:\n            spark.conf.unset(\"spark.databricks.delta.schema.autoMerge.enabled\")\n\n    def test_history(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3)])\n        self.__overwriteDeltaTable([('a', 3), ('b', 2), ('c', 1)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n        operations = dt.history().select('operation')\n        self.__checkAnswer(operations,\n                           [Row(\"WRITE\"), Row(\"WRITE\")],\n                           StructType([StructField(\n                               \"operation\", StringType(), True)]))\n\n        lastMode = dt.history(1).select('operationParameters.mode')\n        self.__checkAnswer(\n            lastMode,\n            [Row(\"Overwrite\")],\n            StructType([StructField(\"operationParameters.mode\", StringType(), True)]))\n\n    def test_cdc(self) -> None:\n        self.spark.range(0, 5).write.format(\"delta\").save(self.tempFile)\n        deltaTable = DeltaTable.forPath(self.spark, self.tempFile)\n        # Enable Change Data Feed\n        self.spark.sql(\n            \"ALTER TABLE delta.`{}` SET TBLPROPERTIES (delta.enableChangeDataFeed = true)\"\n            .format(self.tempFile))\n\n        # Perform some operations\n        deltaTable.update(\"id = 1\", {\"id\": \"10\"})\n        deltaTable.delete(\"id = 2\")\n        self.spark.range(5, 10).write.format(\"delta\").mode(\"append\").save(self.tempFile)\n\n        # Check the Change Data Feed\n        expected = [\n            (1, \"update_preimage\"),\n            (10, \"update_postimage\"),\n            (2, \"delete\"),\n            (5, \"insert\"),\n            (6, \"insert\"),\n            (7, \"insert\"),\n            (8, \"insert\"),\n            (9, \"insert\")\n        ]\n        # Read Change Data Feed\n        # (Test handling of the option as boolean and string and with different cases)\n        for option in [True, \"true\", \"tRuE\"]:\n            cdf = self.spark.read.format(\"delta\") \\\n                .option(\"readChangeData\", option) \\\n                .option(\"startingVersion\", \"1\") \\\n                .load(self.tempFile)\n\n            result = [(row.id, row._change_type) for row in cdf.collect()]\n            self.assertEqual(sorted(result), sorted(expected))\n\n    def test_detail(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n        details = dt.detail()\n        self.__checkAnswer(\n            details.select('format'),\n            [Row('delta')],\n            StructType([StructField('format', StringType(), True)])\n        )\n\n    def test_vacuum(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3)])\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n        self.__createFile('abc.txt', 'abcde')\n        self.__createFile('bac.txt', 'abcdf')\n        self.assertEqual(True, self.__checkFileExists('abc.txt'))\n        dt.vacuum()  # will not delete files as default retention is used.\n        dt.vacuum(1000)  # test whether integers work\n\n        self.assertEqual(True, self.__checkFileExists('bac.txt'))\n        retentionConf = \"spark.databricks.delta.retentionDurationCheck.enabled\"\n        self.spark.conf.set(retentionConf, \"false\")\n        dt.vacuum(0.0)\n        self.spark.conf.set(retentionConf, \"true\")\n        self.assertEqual(False, self.__checkFileExists('bac.txt'))\n        self.assertEqual(False, self.__checkFileExists('abc.txt'))\n\n    def test_convertToDelta(self) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        df.write.format(\"parquet\").save(self.tempFile)\n        dt = DeltaTable.convertToDelta(self.spark, \"parquet.`%s`\" % self.tempFile)\n        self.__checkAnswer(\n            self.spark.read.format(\"delta\").load(self.tempFile),\n            [('a', 1), ('b', 2), ('c', 3)])\n\n        # test if convert to delta with partition columns work\n        tempFile2 = self.tempFile + \"_2\"\n        df.write.partitionBy(\"value\").format(\"parquet\").save(tempFile2)\n        schema = StructType()\n        schema.add(\"value\", IntegerType(), True)\n        dt = DeltaTable.convertToDelta(\n            self.spark,\n            \"parquet.`%s`\" % tempFile2,\n            schema)\n        self.__checkAnswer(\n            self.spark.read.format(\"delta\").load(tempFile2),\n            [('a', 1), ('b', 2), ('c', 3)])\n        self.assertEqual(type(dt), type(DeltaTable.forPath(self.spark, tempFile2)))\n\n        # convert to delta with partition column provided as a string\n        tempFile3 = self.tempFile + \"_3\"\n        df.write.partitionBy(\"value\").format(\"parquet\").save(tempFile3)\n        dt = DeltaTable.convertToDelta(\n            self.spark,\n            \"parquet.`%s`\" % tempFile3,\n            \"value int\")\n        self.__checkAnswer(\n            self.spark.read.format(\"delta\").load(tempFile3),\n            [('a', 1), ('b', 2), ('c', 3)])\n        self.assertEqual(type(dt), type(DeltaTable.forPath(self.spark, tempFile3)))\n\n    def test_isDeltaTable(self) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        df.write.format(\"parquet\").save(self.tempFile)\n        tempFile2 = self.tempFile + '_2'\n        df.write.format(\"delta\").save(tempFile2)\n        self.assertEqual(DeltaTable.isDeltaTable(self.spark, self.tempFile), False)\n        self.assertEqual(DeltaTable.isDeltaTable(self.spark, tempFile2), True)\n\n    def __verify_table_schema(self, tableName: str, schema: StructType, cols: List[str],\n                              types: List[DataType], nullables: Set[str] = set(),\n                              comments: Dict[str, str] = {},\n                              properties: Dict[str, str] = {},\n                              partitioningColumns: List[str] = [],\n                              clusteringColumns: List[str] = [],\n                              tblComment: Optional[str] = None) -> None:\n        fields = []\n        for i in range(len(cols)):\n            col = cols[i]\n            dataType = types[i]\n            metadata = {}\n            if col in comments:\n                metadata[\"comment\"] = comments[col]\n            fields.append(StructField(col, dataType, col in nullables, metadata))\n        self.assertEqual(StructType(fields), schema)\n        if len(properties) > 0:\n            result = (\n                self.spark.sql(  # type: ignore[assignment, misc]\n                    \"SHOW TBLPROPERTIES {}\".format(tableName)\n                )\n                .collect())\n            tablePropertyMap = {row.key: row.value for row in result}\n            for key in properties:\n                self.assertIn(key, tablePropertyMap)\n                self.assertEqual(tablePropertyMap[key], properties[key])\n        tableDetails = self.spark.sql(\"DESCRIBE DETAIL {}\".format(tableName))\\\n            .collect()[0]\n        self.assertEqual(tableDetails.format, \"delta\")\n        actualComment = tableDetails.description\n        self.assertEqual(actualComment, tblComment)\n        partitionCols = tableDetails.partitionColumns\n        self.assertEqual(sorted(partitionCols), sorted((partitioningColumns)))\n        clusterByCols = tableDetails.clusteringColumns\n        self.assertEqual(sorted(clusterByCols), sorted(clusteringColumns))\n\n    def __verify_generated_column(self, tableName: str, deltaTable: DeltaTable) -> None:\n        cmd = \"INSERT INTO {table} (col1, col2) VALUES (1, 11)\".format(table=tableName)\n        self.spark.sql(cmd)\n        deltaTable.update(expr(\"col2 = 11\"), {\"col1\": expr(\"2\")})\n        self.__checkAnswer(deltaTable.toDF(), [(2, 12)], schema=[\"col1\", \"col2\"])\n\n    def __verify_identity_column(self, tableName: str, deltaTable: DeltaTable) -> None:\n        for i in range(2):\n            cmd = \"INSERT INTO {table} (val) VALUES ({i})\".format(table=tableName, i=i)\n            self.spark.sql(cmd)\n        cmd = \"INSERT INTO {table} (id3, val) VALUES (8, 2)\".format(table=tableName)\n        self.spark.sql(cmd)\n        self.__checkAnswer(deltaTable.toDF(),\n                           expectedAnswer=[(1, 2, 2, 0), (2, 3, 4, 1), (3, 4, 8, 2)],\n                           schema=[\"id1\", \"id2\", \"id3\", \"val\"])\n\n    def __build_delta_table(self, builder: DeltaTableBuilder) -> DeltaTable:\n        return builder.addColumn(\"col1\", \"int\", comment=\"foo\", nullable=False) \\\n            .addColumn(\"col2\", IntegerType(), generatedAlwaysAs=\"col1 + 10\") \\\n            .property(\"foo\", \"bar\") \\\n            .comment(\"comment\") \\\n            .partitionedBy(\"col1\").execute()\n\n    def __create_table(self, ifNotExists: bool,\n                       tableName: Optional[str] = None,\n                       location: Optional[str] = None) -> DeltaTable:\n        builder = DeltaTable.createIfNotExists(self.spark) if ifNotExists \\\n            else DeltaTable.create(self.spark)\n        if tableName:\n            builder = builder.tableName(tableName)\n        if location:\n            builder = builder.location(location)\n        return self.__build_delta_table(builder)\n\n    def __replace_table(self,\n                        orCreate: bool,\n                        tableName: Optional[str] = None,\n                        location: Optional[str] = None) -> DeltaTable:\n        builder = DeltaTable.createOrReplace(self.spark) if orCreate \\\n            else DeltaTable.replace(self.spark)\n        if tableName:\n            builder = builder.tableName(tableName)\n        if location:\n            builder = builder.location(location)\n        return self.__build_delta_table(builder)\n\n    def test_create_table_with_existing_schema(self) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        with self.tempTable() as tableName:\n            deltaTable = DeltaTable.create(self.spark).tableName(tableName) \\\n                .addColumns(df.schema) \\\n                .addColumn(\"value2\", dataType=\"int\")\\\n                .partitionedBy([\"value2\", \"value\"])\\\n                .execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"key\", \"value\", \"value2\"],\n                                       [StringType(), LongType(), IntegerType()],\n                                       nullables={\"key\", \"value\", \"value2\"},\n                                       partitioningColumns=[\"value\", \"value2\"])\n\n        with self.tempTable() as tableName:\n            # verify creating table with list of structFields\n            deltaTable2 = DeltaTable.create(self.spark).tableName(tableName).addColumns(\n                df.schema.fields) \\\n                .addColumn(\"value2\", dataType=\"int\") \\\n                .partitionedBy(\"value2\", \"value\")\\\n                .execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable2.toDF().schema,\n                                       [\"key\", \"value\", \"value2\"],\n                                       [StringType(), LongType(), IntegerType()],\n                                       nullables={\"key\", \"value\", \"value2\"},\n                                       partitioningColumns=[\"value\", \"value2\"])\n\n    def test_create_replace_table_with_cluster_by(self) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        with self.tempTable() as tableName:\n            # verify creating table with list of structFields\n            deltaTable = DeltaTable.create(self.spark).tableName(tableName).addColumns(\n                df.schema.fields) \\\n                .addColumn(\"value2\", dataType=\"int\") \\\n                .clusterBy(\"value2\", \"value\")\\\n                .execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"key\", \"value\", \"value2\"],\n                                       [StringType(), LongType(), IntegerType()],\n                                       nullables={\"key\", \"value\", \"value2\"},\n                                       clusteringColumns=[\"value2\", \"value\"],\n                                       partitioningColumns=[])\n\n            deltaTable = DeltaTable.replace(self.spark).tableName(tableName).addColumns(\n                df.schema.fields) \\\n                .addColumn(\"value2\", dataType=\"int\") \\\n                .clusterBy(\"value2\", \"value\")\\\n                .execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"key\", \"value\", \"value2\"],\n                                       [StringType(), LongType(), IntegerType()],\n                                       nullables={\"key\", \"value\", \"value2\"},\n                                       clusteringColumns=[\"value2\", \"value\"],\n                                       partitioningColumns=[])\n\n    def test_create_replace_table_with_no_spark_session_passed(self) -> None:\n        with self.tempTable() as tableName:\n            # create table.\n            deltaTable = DeltaTable.create().tableName(tableName)\\\n                .addColumn(\"value\", dataType=\"int\").execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"value\"],\n                                       [IntegerType()],\n                                       nullables={\"value\"})\n\n            # ignore existence with createIfNotExists\n            deltaTable = DeltaTable.createIfNotExists().tableName(tableName) \\\n                .addColumn(\"value2\", dataType=\"int\").execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"value\"],\n                                       [IntegerType()],\n                                       nullables={\"value\"})\n\n            # replace table with replace\n            deltaTable = DeltaTable.replace().tableName(tableName) \\\n                .addColumn(\"key\", dataType=\"int\").execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"key\"],\n                                       [IntegerType()],\n                                       nullables={\"key\"})\n\n            # replace with a new column again\n            deltaTable = DeltaTable.createOrReplace().tableName(tableName) \\\n                .addColumn(\"col1\", dataType=\"int\").execute()\n\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"col1\"],\n                                       [IntegerType()],\n                                       nullables={\"col1\"})\n\n    def test_create_table_with_name_only(self) -> None:\n        for ifNotExists in (False, True):\n            with self.tempTable() as tableName:\n                deltaTable = self.__create_table(ifNotExists, tableName=tableName)\n\n                self.__verify_table_schema(tableName,\n                                           deltaTable.toDF().schema,\n                                           [\"col1\", \"col2\"],\n                                           [IntegerType(), IntegerType()],\n                                           nullables={\"col2\"},\n                                           comments={\"col1\": \"foo\"},\n                                           properties={\"foo\": \"bar\"},\n                                           partitioningColumns=[\"col1\"],\n                                           tblComment=\"comment\")\n                # verify generated columns.\n                self.__verify_generated_column(tableName, deltaTable)\n\n    def test_create_table_with_location_only(self) -> None:\n        for ifNotExists in (False, True):\n            path = self.tempFile + str(ifNotExists)\n            deltaTable = self.__create_table(ifNotExists, location=path)\n\n            self.__verify_table_schema(\"delta.`{}`\".format(path),\n                                       deltaTable.toDF().schema,\n                                       [\"col1\", \"col2\"],\n                                       [IntegerType(), IntegerType()],\n                                       nullables={\"col2\"},\n                                       comments={\"col1\": \"foo\"},\n                                       partitioningColumns=[\"col1\"],\n                                       tblComment=\"comment\")\n            # verify generated columns.\n            self.__verify_generated_column(\"delta.`{}`\".format(path), deltaTable)\n\n    def test_create_table_with_name_and_location(self) -> None:\n        for ifNotExists in (False, True):\n            path = self.tempFile + str(ifNotExists)\n            with self.tempTable() as tableName:\n                deltaTable = self.__create_table(\n                    ifNotExists, tableName=tableName, location=path)\n\n                self.__verify_table_schema(tableName,\n                                           deltaTable.toDF().schema,\n                                           [\"col1\", \"col2\"],\n                                           [IntegerType(), IntegerType()],\n                                           nullables={\"col2\"},\n                                           comments={\"col1\": \"foo\"},\n                                           properties={\"foo\": \"bar\"},\n                                           partitioningColumns=[\"col1\"],\n                                           tblComment=\"comment\")\n                # verify generated columns.\n                self.__verify_generated_column(tableName, deltaTable)\n\n    def test_create_table_behavior(self) -> None:\n        with self.tempTable() as tableName:\n            self.spark.sql(f\"CREATE TABLE {tableName} (c1 int) USING DELTA\")\n\n            # Errors out if doesn't ignore.\n            with self.assertRaises(AnalysisException) as error_ctx:\n                self.__create_table(False, tableName=tableName)\n            msg = str(error_ctx.exception)\n            assert (tableName in msg and \"already exists\" in msg)\n\n            # ignore table creation.\n            self.__create_table(True, tableName=tableName)\n            schema = self.spark.read.format(\"delta\").table(tableName).schema\n            self.__verify_table_schema(tableName,\n                                       schema,\n                                       [\"c1\"],\n                                       [IntegerType()],\n                                       nullables={\"c1\"})\n\n    def test_replace_table_with_name_only(self) -> None:\n        for orCreate in (False, True):\n            with self.tempTable() as tableName:\n                self.spark.sql(\"CREATE TABLE {} (c1 int) USING DELTA\".format(tableName))\n                deltaTable = self.__replace_table(orCreate, tableName=tableName)\n\n                self.__verify_table_schema(tableName,\n                                           deltaTable.toDF().schema,\n                                           [\"col1\", \"col2\"],\n                                           [IntegerType(), IntegerType()],\n                                           nullables={\"col2\"},\n                                           comments={\"col1\": \"foo\"},\n                                           properties={\"foo\": \"bar\"},\n                                           partitioningColumns=[\"col1\"],\n                                           tblComment=\"comment\")\n                # verify generated columns.\n                self.__verify_generated_column(tableName, deltaTable)\n\n    def test_replace_table_with_location_only(self) -> None:\n        for orCreate in (False, True):\n            path = self.tempFile + str(orCreate)\n            self.__create_table(False, location=path)\n            deltaTable = self.__replace_table(orCreate, location=path)\n\n            self.__verify_table_schema(\"delta.`{}`\".format(path),\n                                       deltaTable.toDF().schema,\n                                       [\"col1\", \"col2\"],\n                                       [IntegerType(), IntegerType()],\n                                       nullables={\"col2\"},\n                                       comments={\"col1\": \"foo\"},\n                                       properties={\"foo\": \"bar\"},\n                                       partitioningColumns=[\"col1\"],\n                                       tblComment=\"comment\")\n            # verify generated columns.\n            self.__verify_generated_column(\"delta.`{}`\".format(path), deltaTable)\n\n    def test_replace_table_with_name_and_location(self) -> None:\n        for orCreate in (False, True):\n            path = self.tempFile + str(orCreate)\n            with self.tempTable() as tableName:\n                self.spark.sql(\"CREATE TABLE {} (col int) USING DELTA LOCATION '{}'\"\n                               .format(tableName, path))\n                deltaTable = self.__replace_table(\n                    orCreate, tableName=tableName, location=path)\n\n                self.__verify_table_schema(tableName,\n                                           deltaTable.toDF().schema,\n                                           [\"col1\", \"col2\"],\n                                           [IntegerType(), IntegerType()],\n                                           nullables={\"col2\"},\n                                           comments={\"col1\": \"foo\"},\n                                           properties={\"foo\": \"bar\"},\n                                           partitioningColumns=[\"col1\"],\n                                           tblComment=\"comment\")\n                # verify generated columns.\n                self.__verify_generated_column(tableName, deltaTable)\n\n    def test_replace_table_behavior(self) -> None:\n        with self.tempTable() as tableName:\n            with self.assertRaises(AnalysisException) as error_ctx:\n                self.__replace_table(False, tableName=tableName)\n            msg = str(error_ctx.exception)\n            self.assertIn(tableName, msg.lower())\n            self.assertTrue(\"did not exist\" in msg or \"cannot be found\" in msg)\n            deltaTable = self.__replace_table(True, tableName=tableName)\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"col1\", \"col2\"],\n                                       [IntegerType(), IntegerType()],\n                                       nullables={\"col2\"},\n                                       comments={\"col1\": \"foo\"},\n                                       properties={\"foo\": \"bar\"},\n                                       partitioningColumns=[\"col1\"],\n                                       tblComment=\"comment\")\n\n    def test_verify_paritionedBy_compatibility(self) -> None:\n        try:\n            from pyspark.sql.column import _to_seq  # type: ignore[attr-defined]\n        except ImportError:\n            # Spark 4\n            from pyspark.sql.classic.column import _to_seq  # type: ignore\n\n        with self.tempTable() as tableName:\n            tableBuilder = DeltaTable.create(self.spark).tableName(tableName) \\\n                .addColumn(\"col1\", \"int\", comment=\"foo\", nullable=False) \\\n                .addColumn(\"col2\", IntegerType(), generatedAlwaysAs=\"col1 + 10\") \\\n                .property(\"foo\", \"bar\") \\\n                .comment(\"comment\")\n            tableBuilder._jbuilder = tableBuilder._jbuilder.partitionedBy(\n                _to_seq(self.spark._sc, [\"col1\"])  # type: ignore[attr-defined]\n            )\n            deltaTable = tableBuilder.execute()\n            self.__verify_table_schema(tableName,\n                                       deltaTable.toDF().schema,\n                                       [\"col1\", \"col2\"],\n                                       [IntegerType(), IntegerType()],\n                                       nullables={\"col2\"},\n                                       comments={\"col1\": \"foo\"},\n                                       properties={\"foo\": \"bar\"},\n                                       partitioningColumns=[\"col1\"],\n                                       tblComment=\"comment\")\n\n    def test_create_table_with_identity_column(self) -> None:\n        for ifNotExists in (False, True):\n            with self.tempTable() as tableName:\n                try:\n                    self.spark.conf.set(\"spark.databricks.delta.identityColumn.enabled\", \"true\")\n                    builder = (\n                        DeltaTable.createIfNotExists(self.spark)\n                        if ifNotExists\n                        else DeltaTable.create(self.spark))\n                    builder = builder.tableName(tableName)\n                    builder = (\n                        builder.addColumn(\n                            \"id1\", LongType(), generatedAlwaysAs=IdentityGenerator())\n                        .addColumn(\n                            \"id2\",\n                            \"BIGINT\",\n                            generatedAlwaysAs=IdentityGenerator(start=2))\n                        .addColumn(\n                            \"id3\",\n                            \"bigint\",\n                            generatedByDefaultAs=IdentityGenerator(start=2, step=2))\n                        .addColumn(\"val\", \"bigint\", nullable=False))\n\n                    deltaTable = builder.execute()\n                    self.__verify_table_schema(\n                        tableName,\n                        deltaTable.toDF().schema,\n                        [\"id1\", \"id2\", \"id3\", \"val\"],\n                        [LongType(), LongType(), LongType(), LongType()],\n                        nullables={\"id1\", \"id2\", \"id3\"})\n                    self.__verify_identity_column(tableName, deltaTable)\n                finally:\n                    self.spark.conf.unset(\"spark.databricks.delta.identityColumn.enabled\")\n\n    def test_delta_table_builder_with_bad_args(self) -> None:\n        builder = DeltaTable.create(self.spark).location(self.tempFile)\n\n        # bad table name\n        with self.assertRaises(TypeError):\n            builder.tableName(1)  # type: ignore[arg-type]\n\n        # bad location\n        with self.assertRaises(TypeError):\n            builder.location(1)  # type: ignore[arg-type]\n\n        # bad comment\n        with self.assertRaises(TypeError):\n            builder.comment(1)  # type: ignore[arg-type]\n\n        # bad column name\n        with self.assertRaises(TypeError):\n            builder.addColumn(1, \"int\")  # type: ignore[arg-type]\n\n        # bad datatype.\n        with self.assertRaises(TypeError):\n            builder.addColumn(\"a\", 1)  # type: ignore[arg-type]\n\n        # bad column datatype - can't be parsed\n        with self.assertRaises(ParseException):\n            builder.addColumn(\"a\", \"1\")\n            builder.execute()\n\n        # reset the builder\n        builder = DeltaTable.create(self.spark).location(self.tempFile)\n\n        # bad comment\n        with self.assertRaises(TypeError):\n            builder.addColumn(\"a\", \"int\", comment=1)  # type: ignore[arg-type]\n\n        # bad generatedAlwaysAs\n        with self.assertRaises(TypeError):\n            builder.addColumn(\"a\", \"int\", generatedAlwaysAs=1)  # type: ignore[arg-type]\n\n        # bad generatedAlwaysAs - identity column data type must be Long\n        with self.assertRaises(UnsupportedOperationException):\n            builder.addColumn(\n                \"a\",\n                \"int\",\n                generatedAlwaysAs=IdentityGenerator()\n            )  # type: ignore[arg-type]\n            # exception is thrown in builder.execute() for delta connect\n            builder.execute()\n\n        # reset the builder\n        builder = DeltaTable.create(self.spark).location(self.tempFile)\n        # bad generatedAlwaysAs - step can't be 0\n        with self.assertRaises(ValueError):\n            builder.addColumn(\n                \"a\",\n                \"bigint\",\n                generatedAlwaysAs=IdentityGenerator(step=0)\n            )  # type: ignore[arg-type]\n\n        # bad generatedByDefaultAs - can't be set with generatedAlwaysAs\n        with self.assertRaises(ValueError):\n            builder.addColumn(\n                \"a\",\n                \"bigint\",\n                generatedAlwaysAs=\"\",\n                generatedByDefaultAs=IdentityGenerator()\n            )  # type: ignore[arg-type]\n\n        # bad generatedByDefaultAs - argument type must be IdentityGenerator\n        with self.assertRaises(TypeError):\n            builder.addColumn(\n                \"a\",\n                \"bigint\",\n                generatedByDefaultAs=\"\"  # type: ignore[arg-type]\n            )\n\n        # bad generatedByDefaultAs - identity column data type must be Long\n        with self.assertRaises(UnsupportedOperationException):\n            builder.addColumn(\n                \"a\",\n                \"int\",\n                generatedByDefaultAs=IdentityGenerator()\n            )  # type: ignore[arg-type]\n            # exception is thrown in builder.execute() for delta connect\n            builder.execute()\n\n        # reset the builder\n        builder = DeltaTable.create(self.spark).location(self.tempFile)\n\n        # bad generatedByDefaultAs - step can't be 0\n        with self.assertRaises(ValueError):\n            builder.addColumn(\n                \"a\",\n                \"bigint\",\n                generatedByDefaultAs=IdentityGenerator(step=0)\n            )  # type: ignore[arg-type]\n\n        # bad nullable\n        with self.assertRaises(TypeError):\n            builder.addColumn(\"a\", \"int\", nullable=1)  # type: ignore[arg-type]\n\n        # bad existing schema\n        with self.assertRaises(TypeError):\n            builder.addColumns(1)  # type: ignore[arg-type]\n\n        # bad existing schema.\n        with self.assertRaises(TypeError):\n            builder.addColumns([StructField(\"1\", IntegerType()), 1])  # type: ignore[list-item]\n\n        # bad partitionedBy col name\n        with self.assertRaises(TypeError):\n            builder.partitionedBy(1)  # type: ignore[call-overload]\n\n        with self.assertRaises(TypeError):\n            builder.partitionedBy(1, \"1\")   # type: ignore[call-overload]\n\n        with self.assertRaises(TypeError):\n            builder.partitionedBy([1])  # type: ignore[list-item]\n\n        # bad clusterBy col name\n        with self.assertRaises(TypeError):\n            builder.clusterBy(1)  # type: ignore[call-overload]\n\n        with self.assertRaises(TypeError):\n            builder.clusterBy(1, \"1\")   # type: ignore[call-overload]\n\n        with self.assertRaises(TypeError):\n            builder.clusterBy([1])  # type: ignore[list-item]\n\n        # bad property key\n        with self.assertRaises(TypeError):\n            builder.property(1, \"1\")  # type: ignore[arg-type]\n\n        # bad property value\n        with self.assertRaises(TypeError):\n            builder.property(\"1\", 1)  # type: ignore[arg-type]\n\n    def __create_df_for_feature_tests(self) -> DeltaTable:\n        try:\n            self.spark.conf.set('spark.databricks.delta.minReaderVersion', '1')\n            self.spark.conf.set('spark.databricks.delta.minWriterVersion', '2')\n            self.__writeDeltaTable([('a', 1), ('b', 2), ('c', 3), ('d', 4)])\n            return DeltaTable.forPath(self.spark, self.tempFile)\n        finally:\n            self.spark.conf.unset('spark.databricks.delta.minReaderVersion')\n            self.spark.conf.unset('spark.databricks.delta.minWriterVersion')\n\n    def test_protocolUpgrade(self) -> None:\n        dt = self.__create_df_for_feature_tests()\n        dt.upgradeTableProtocol(1, 3)\n\n        # cannot downgrade once upgraded\n        dt.upgradeTableProtocol(1, 2)\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 1,\n                        \"The upgrade should be a no-op, because downgrades aren't allowed\")\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 3,\n                        \"The upgrade should be a no-op, because downgrades aren't allowed\")\n\n        # bad args\n        with self.assertRaisesRegex(ValueError, \"readerVersion\"):\n            dt.upgradeTableProtocol(\"abc\", 3)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"readerVersion\"):\n            dt.upgradeTableProtocol([1], 3)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"readerVersion\"):\n            dt.upgradeTableProtocol([], 3)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"readerVersion\"):\n            dt.upgradeTableProtocol({}, 3)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"writerVersion\"):\n            dt.upgradeTableProtocol(1, \"abc\")  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"writerVersion\"):\n            dt.upgradeTableProtocol(1, [3])  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"writerVersion\"):\n            dt.upgradeTableProtocol(1, [])  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"writerVersion\"):\n            dt.upgradeTableProtocol(1, {})  # type: ignore[arg-type]\n\n    def test_addFeatureSupport(self) -> None:\n        dt = self.__create_df_for_feature_tests()\n\n        # bad args\n        with self.assertRaisesRegex(Exception, \"DELTA_UNSUPPORTED_FEATURES_IN_CONFIG\"):\n            dt.addFeatureSupport(\"abc\")\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.addFeatureSupport(12345)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.addFeatureSupport([12345])  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.addFeatureSupport({})  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.addFeatureSupport([])  # type: ignore[arg-type]\n\n        # good args\n        dt.addFeatureSupport(\"appendOnly\")\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 1, \"The upgrade should be a no-op\")\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 2, \"The upgrade should be a no-op\")\n        self.assertEqual(sorted(dt_details[\"tableFeatures\"]), [\"appendOnly\", \"invariants\"])\n\n        dt.addFeatureSupport(\"deletionVectors\")\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 3, \"DV requires reader version 3\")\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 7, \"DV requires writer version 7\")\n        self.assertEqual(sorted(dt_details[\"tableFeatures\"]),\n                         [\"appendOnly\", \"deletionVectors\", \"invariants\"])\n\n    def test_dropFeatureSupport(self) -> None:\n        # The expected results below are based on drop feature with history truncation.\n        # Fast drop feature, adds a writer feature when dropped. The relevant behavior is tested\n        # in the DeltaFastDropFeatureSuite.\n        self.spark.conf.set('spark.databricks.delta.tableFeatures.fastDropFeature.enabled', 'false')\n        dt = self.__create_df_for_feature_tests()\n\n        dt.addFeatureSupport(\"testRemovableWriter\")\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 1)\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 7, \"Should upgrade to table features\")\n        self.assertEqual(sorted(dt_details[\"tableFeatures\"]),\n                         [\"appendOnly\", \"invariants\", \"testRemovableWriter\"])\n\n        # Attempt truncating the history when dropping a feature that is not required.\n        # This verifies the truncateHistory option was correctly passed.\n        with self.assertRaisesRegex(Exception,\n                                    \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\"):\n            dt.dropFeatureSupport(\"testRemovableWriter\", True)\n\n        dt.dropFeatureSupport(\"testRemovableWriter\")\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 1)\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 2, \"Should return to legacy protocol\")\n\n        dt.addFeatureSupport(\"testRemovableReaderWriter\")\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 3, \"Should upgrade to table features\")\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 7, \"Should upgrade to table features\")\n        self.assertEqual(sorted(dt_details[\"tableFeatures\"]),\n                         [\"appendOnly\", \"invariants\", \"testRemovableReaderWriter\"])\n\n        dt.dropFeatureSupport(\"testRemovableReaderWriter\")\n        dt_details = dt.detail().collect()[0].asDict()\n        self.assertTrue(dt_details[\"minReaderVersion\"] == 1, \"Should return to legacy protocol\")\n        self.assertTrue(dt_details[\"minWriterVersion\"] == 2, \"Should return to legacy protocol\")\n\n        # Try to drop an unsupported feature.\n        with self.assertRaisesRegex(Exception, \"DELTA_FEATURE_DROP_UNSUPPORTED_CLIENT_FEATURE\"):\n            dt.dropFeatureSupport(\"__invalid_feature__\")\n\n        # Try to drop a feature that is not present in the protocol.\n        with self.assertRaisesRegex(Exception, \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\"):\n            dt.dropFeatureSupport(\"testRemovableReaderWriter\")\n\n        # Try to drop a non-removable feature.\n        dt.addFeatureSupport(\"testReaderWriter\")\n        with self.assertRaisesRegex(Exception, \"DELTA_FEATURE_DROP_NONREMOVABLE_FEATURE\"):\n            dt.dropFeatureSupport(\"testReaderWriter\")\n\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.dropFeatureSupport(12345)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.dropFeatureSupport([12345])  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.dropFeatureSupport({})  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"featureName needs to be a string\"):\n            dt.dropFeatureSupport([])  # type: ignore[arg-type]\n\n        with self.assertRaisesRegex(ValueError, \"truncateHistory needs to be a boolean\"):\n            dt.dropFeatureSupport(\"testRemovableWriter\", 12345)  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"truncateHistory needs to be a boolean\"):\n            dt.dropFeatureSupport(\"testRemovableWriter\", [12345])  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"truncateHistory needs to be a boolean\"):\n            dt.dropFeatureSupport(\"testRemovableWriter\", {})  # type: ignore[arg-type]\n        with self.assertRaisesRegex(ValueError, \"truncateHistory needs to be a boolean\"):\n            dt.dropFeatureSupport(\"testRemovableWriter\", [])  # type: ignore[arg-type]\n\n    def test_restore_to_version(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2)])\n        self.__overwriteDeltaTable([('a', 3), ('b', 2)],\n                                   schema=[\"key_new\", \"value_new\"],\n                                   overwriteSchema='true')\n\n        overwritten = DeltaTable.forPath(self.spark, self.tempFile).toDF()\n        self.__checkAnswer(overwritten,\n                           [Row(key_new='a', value_new=3), Row(key_new='b', value_new=2)])\n\n        DeltaTable.forPath(self.spark, self.tempFile).restoreToVersion(0)\n        restored = DeltaTable.forPath(self.spark, self.tempFile).toDF()\n\n        self.__checkAnswer(restored, [Row(key='a', value=1), Row(key='b', value=2)])\n\n    def test_restore_to_timestamp(self) -> None:\n        self.__writeDeltaTable([('a', 1), ('b', 2)])\n        timestampToRestore = DeltaTable.forPath(self.spark, self.tempFile) \\\n            .history() \\\n            .head() \\\n            .timestamp \\\n            .strftime('%Y-%m-%d %H:%M:%S.%f')\n\n        self.__overwriteDeltaTable([('a', 3), ('b', 2)],\n                                   schema=[\"key_new\", \"value_new\"],\n                                   overwriteSchema='true')\n\n        overwritten = DeltaTable.forPath(self.spark, self.tempFile).toDF()\n        self.__checkAnswer(overwritten,\n                           [Row(key_new='a', value_new=3), Row(key_new='b', value_new=2)])\n\n        DeltaTable.forPath(self.spark, self.tempFile).restoreToTimestamp(timestampToRestore)\n\n        restored = DeltaTable.forPath(self.spark, self.tempFile).toDF()\n        self.__checkAnswer(restored, [Row(key='a', value=1), Row(key='b', value=2)])\n\n        # we cannot test the actual working of restore to timestamp here but we can make sure\n        # that the api is being called at least\n        def runRestore() -> None:\n            DeltaTable.forPath(self.spark, self.tempFile).restoreToTimestamp('05/04/1999')\n        self.__intercept(runRestore, \"The provided timestamp ('05/04/1999') \"\n                                     \"cannot be converted to a valid timestamp\")\n\n    def test_restore_invalid_inputs(self) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        df.write.format(\"delta\").save(self.tempFile)\n\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        def runRestoreToTimestamp() -> None:\n            dt.restoreToTimestamp(12342323232)  # type: ignore[arg-type]\n        self.__intercept(runRestoreToTimestamp,\n                         \"timestamp needs to be a string but got '<class 'int'>'\")\n\n        def runRestoreToVersion() -> None:\n            dt.restoreToVersion(\"0\")  # type: ignore[arg-type]\n        self.__intercept(runRestoreToVersion,\n                         \"version needs to be an int but got '<class 'str'>'\")\n\n    def test_optimize(self) -> None:\n        # write an unoptimized delta table\n        df = self.spark.createDataFrame([(\"a\", 1), (\"a\", 2)], [\"key\", \"value\"]).repartition(1)\n        df.write.format(\"delta\").save(self.tempFile)\n        df = self.spark.createDataFrame([(\"a\", 3), (\"a\", 4)], [\"key\", \"value\"]).repartition(1)\n        df.write.format(\"delta\").save(self.tempFile, mode=\"append\")\n        df = self.spark.createDataFrame([(\"b\", 1), (\"b\", 2)], [\"key\", \"value\"]).repartition(1)\n        df.write.format(\"delta\").save(self.tempFile, mode=\"append\")\n\n        # create DeltaTable\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # execute bin compaction\n        optimizer = dt.optimize()\n        res = optimizer.executeCompaction()\n        op_params = dt.history().first().operationParameters\n\n        # assertions\n        self.assertEqual(1, res.first().metrics.numFilesAdded)\n        self.assertEqual(3, res.first().metrics.numFilesRemoved)\n        self.assertEqual('[]', op_params['predicate'])\n\n        # test non-partition column\n        def optimize() -> None:\n            dt.optimize().where(\"key = 'a'\").executeCompaction()\n        self.__intercept(optimize,\n                         \"Predicate references non-partition column 'key'. \"\n                         \"Only the partition columns may be referenced: []\")\n\n    def test_optimize_w_partition_filter(self) -> None:\n        # write an unoptimized delta table\n        df = self.spark.createDataFrame([(\"a\", 1), (\"a\", 2)], [\"key\", \"value\"]).repartition(1)\n        df.write.partitionBy(\"key\").format(\"delta\").save(self.tempFile)\n        df = self.spark.createDataFrame([(\"a\", 3), (\"a\", 4)], [\"key\", \"value\"]).repartition(1)\n        df.write.partitionBy(\"key\").format(\"delta\").save(self.tempFile, mode=\"append\")\n        df = self.spark.createDataFrame([(\"b\", 1), (\"b\", 2)], [\"key\", \"value\"]).repartition(1)\n        df.write.partitionBy(\"key\").format(\"delta\").save(self.tempFile, mode=\"append\")\n\n        # create DeltaTable\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # execute bin compaction\n        optimizer = dt.optimize().where(\"key = 'a'\")\n        res = optimizer.executeCompaction()\n        op_params = dt.history().first().operationParameters\n\n        # assertions\n        self.assertEqual(1, res.first().metrics.numFilesAdded)\n        self.assertEqual(2, res.first().metrics.numFilesRemoved)\n        self.assertEqual('''[\"('key = a)\"]''', op_params['predicate'])\n\n        # test non-partition column\n        def optimize() -> None:\n            dt.optimize().where(\"value = 1\").executeCompaction()\n        self.__intercept(optimize,\n                         \"Predicate references non-partition column 'value'. \"\n                         \"Only the partition columns may be referenced: [key]\")\n\n    def test_optimize_zorder_by(self) -> None:\n        # write an unoptimized delta table\n        self.spark.createDataFrame([i for i in range(0, 100)], IntegerType()) \\\n            .withColumn(\"col1\", floor(col(\"value\") % 7)) \\\n            .withColumn(\"col2\", floor(col(\"value\") % 27)) \\\n            .withColumn(\"p\", floor(col(\"value\") % 10)) \\\n            .repartition(4).write.partitionBy(\"p\").format(\"delta\").save(self.tempFile)\n\n        # get the number of data files in the current version\n        numDataFilesPreZOrder = self.spark.read.format(\"delta\").load(self.tempFile) \\\n            .select(\"_metadata.file_path\").distinct().count()\n\n        # create DeltaTable\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # execute Z-Order Optimization\n        optimizer = dt.optimize()\n        result = optimizer.executeZOrderBy([\"col1\", \"col2\"])\n        metrics = result.select(\"metrics.*\").head()\n\n        # expect there is only one file after the Z-Order as Z-Order also\n        # does the compaction implicitly and all small files are written to one file\n        # for each partition. Ther are 10 partitions in the table, so expect 10 final files\n        numDataFilesPostZOrder = 10\n\n        self.assertEqual(numDataFilesPostZOrder, metrics.numFilesAdded)\n        self.assertEqual(numDataFilesPreZOrder, metrics.numFilesRemoved)\n        self.assertEqual(0, metrics.totalFilesSkipped)\n        self.assertEqual(numDataFilesPreZOrder, metrics.totalConsideredFiles)\n        self.assertEqual('all', metrics.zOrderStats.strategyName)\n        self.assertEqual(10, metrics.zOrderStats.numOutputCubes)  # one for each partition\n\n        # negative test: Z-Order on partition column\n        def optimize() -> None:\n            dt.optimize().where(\"p = 1\").executeZOrderBy([\"p\"])\n        self.__intercept(optimize,\n                         \"p is a partition column. \"\n                         \"Z-Ordering can only be performed on data columns\")\n\n    def test_optimize_zorder_by_w_partition_filter(self) -> None:\n        # write an unoptimized delta table\n        df = self.spark.createDataFrame([i for i in range(0, 100)], IntegerType()) \\\n            .withColumn(\"col1\", floor(col(\"value\") % 7)) \\\n            .withColumn(\"col2\", floor(col(\"value\") % 27)) \\\n            .withColumn(\"p\", floor(col(\"value\") % 10)) \\\n            .repartition(4).write.partitionBy(\"p\")\n\n        df.format(\"delta\").save(self.tempFile)\n\n        # get the number of data files in the current version in partition p = 2\n        numDataFilesPreZOrder = self.spark.read.format(\"delta\").load(self.tempFile) \\\n            .filter(\"p=2\").select(\"_metadata.file_path\").distinct().count()\n\n        # create DeltaTable\n        dt = DeltaTable.forPath(self.spark, self.tempFile)\n\n        # execute Z-OrderBy\n        optimizer = dt.optimize().where(\"p = 2\")\n        result = optimizer.executeZOrderBy([\"col1\", \"col2\"])\n        metrics = result.select(\"metrics.*\").head()\n\n        # expect there is only one file after the Z-Order as Z-Order also\n        # does the compaction implicitly and all small files are written to one file\n        numDataFilesPostZOrder = 1\n\n        self.assertEqual(numDataFilesPostZOrder, metrics.numFilesAdded)\n        self.assertEqual(numDataFilesPreZOrder, metrics.numFilesRemoved)\n        self.assertEqual(0, metrics.totalFilesSkipped)\n        # expected to consider all input files for Z-Order\n        self.assertEqual(numDataFilesPreZOrder, metrics.totalConsideredFiles)\n        self.assertEqual('all', metrics.zOrderStats.strategyName)\n        self.assertEqual(1, metrics.zOrderStats.numOutputCubes)  # one per each affected partition\n\n        def test_clone(self) -> None:  # type: ignore[no-untyped-def]\n            df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n            df2 = self.spark.createDataFrame([('d', 4), ('e', 5), ('f', 6)], [\"key\", \"value\"])\n            df.write.format(\"delta\").save(self.tempFile)\n            df2.write.format(\"delta\").mode(\"overwrite\").save(self.tempFile)\n            # source\n            dt = DeltaTable.forPath(self.spark, self.tempFile)\n            tempFile2 = self.tempFile + \"_2\"\n            tempFile3 = self.tempFile + \"_3\"\n\n            dt.clone(tempFile2, True, False, {\"foo\": \"bar\"})\n            props = self.spark.sql('''SHOW TBLPROPERTIES delta.`{}`(\"foo\")\n            '''.format(tempFile2))\n            self.__checkAnswer(props, [(\"foo\", \"bar\")])\n\n            self.__checkAnswer(\n                self.spark.read.format(\"delta\").load(tempFile2),\n                [('d', 4), ('e', 5), ('f', 6)])\n\n            dt.cloneAtVersion(0, tempFile3, True)\n            self.__checkAnswer(\n                self.spark.read.format(\"delta\").load(tempFile3),\n                [('a', 1), ('b', 2), ('c', 3)])\n\n            # clone over tempFile3 with source at current version\n            dt.clone(tempFile3, True, True)\n            self.__checkAnswer(\n                self.spark.read.format(\"delta\").load(tempFile3),\n                [('d', 4), ('e', 5), ('f', 6)])\n\n        def test_clone_invalid_inputs(self) -> None:  # type: ignore[no-untyped-def]\n            df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n            df.write.format(\"delta\").save(self.tempFile)\n            # source\n            dt = DeltaTable.forPath(self.spark, self.tempFile)\n            tempFile2 = self.tempFile + \"_2\"\n\n            def incorrectTarget() -> \"DeltaTable\":\n                return dt.clone(10)\n\n            self.__intercept(incorrectTarget, \"target needs to be a string but got int\")\n\n            def incorrectShallow() -> \"DeltaTable\":\n                return dt.clone(tempFile2, isShallow=10)\n\n            self.__intercept(incorrectShallow, \"isShallow needs to be a boolean but got int\")\n\n            def incorrectReplace() -> \"DeltaTable\":\n                return dt.clone(tempFile2, False, replace=10)\n\n            self.__intercept(incorrectReplace, \"replace needs to be a boolean but got int\")\n\n            def incorrectProperties() -> \"DeltaTable\":\n                return dt.clone(tempFile2, False, False, properties=10)\n\n            self.__intercept(incorrectProperties, \"properties needs to be a dict but got int\")\n\n            def incorrectPropertyValue() -> \"DeltaTable\":\n                return dt.clone(tempFile2, False, False, properties={\"key\": 10})\n\n            self.__intercept(incorrectPropertyValue, \"All property values including 10\"\n                                                     \" needs to be a str but got int\")\n\n            def incorrectVersion() -> \"DeltaTable\":\n                return dt.cloneAtVersion(\"0\", tempFile2, False, False)\n\n            self.__intercept(incorrectVersion, \"version needs to be an int but got string\")\n\n            def incorrectTimestamp() -> \"DeltaTable\":\n                return dt.cloneAtTimestamp(10, tempFile2, False, False)\n\n            self.__intercept(incorrectTimestamp, \"timestamp needs to be a string but got int\")\n\n    def test_create_table_with_cluster_by(self) -> None:\n        with self.tempTable() as tableName:\n            builder = DeltaTable.create(self.spark)\n            self.__test_table_with_cluster_by(\n                tableName,\n                builder,\n                lambda builder: builder.clusterBy([\"value2\", \"value\"]),\n                expected=[\"value\", \"value2\"])\n\n    def test_replace_table_with_cluster_by(self) -> None:\n        with self.tempTable() as tableName:\n            self.spark.sql(f\"CREATE TABLE {tableName} (c1 int) USING DELTA\")\n            builder = DeltaTable.replace(self.spark)\n            self.__test_table_with_cluster_by(\n                tableName,\n                builder,\n                lambda builder: builder.clusterBy(\"value2\", \"value\"),\n                expected=[\"value\", \"value2\"])\n\n    # type: ignore[arg-type]\n    def __test_table_with_cluster_by(self,\n                                     tableName: str,\n                                     builder: \"JavaObject\",\n                                     setClusterBy: Callable[[\"JavaObject\"], None],\n                                     expected: List[str]) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        builder = builder.tableName(tableName) \\\n            .addColumns(df.schema) \\\n            .addColumn(\"value2\", dataType=\"int\")\n        setClusterBy(builder)\n        deltaTable = builder.execute()\n        self.__verify_table_schema(tableName,\n                                   deltaTable.toDF().schema,\n                                   [\"key\", \"value\", \"value2\"],\n                                   [StringType(), LongType(), IntegerType()],\n                                   nullables={\"key\", \"value\", \"value2\"},\n                                   clusteringColumns=expected)\n\n    def test_cluster_by_bad_args(self) -> None:\n        builder = DeltaTable.create(self.spark).location(self.tempFile)\n        # bad clusterBy col name\n        with self.assertRaises(TypeError):\n            builder.clusterBy(1)  # type: ignore[call-overload]\n\n        with self.assertRaises(TypeError):\n            builder.clusterBy(1, \"1\")   # type: ignore[call-overload]\n\n        with self.assertRaises(TypeError):\n            builder.clusterBy([1])  # type: ignore[list-item]\n\n    def __checkAnswer(self, df: DataFrame,\n                      expectedAnswer: List[Any],\n                      schema: Union[StructType, List[str]] = [\"key\", \"value\"]) -> None:\n        if not expectedAnswer:\n            self.assertEqual(df.count(), 0)\n            return\n        expectedDF = self.spark.createDataFrame(expectedAnswer, schema)\n        try:\n            self.assertEqual(df.count(), expectedDF.count())\n            self.assertEqual(len(df.columns), len(expectedDF.columns))\n            self.assertEqual([], df.subtract(expectedDF).take(1))\n            self.assertEqual([], expectedDF.subtract(df).take(1))\n        except AssertionError:\n            print(\"Expected:\")\n            expectedDF.show()\n            print(\"Found:\")\n            df.show()\n            raise\n\n    def __writeDeltaTable(self, datalist: List[Tuple[Any, Any]]) -> None:\n        df = self.spark.createDataFrame(datalist, [\"key\", \"value\"])\n        df.write.format(\"delta\").save(self.tempFile)\n\n    def __writeAsTable(self, datalist: List[Tuple[Any, Any]], tblName: str) -> None:\n        df = self.spark.createDataFrame(datalist, [\"key\", \"value\"])\n        df.write.format(\"delta\").saveAsTable(tblName)\n\n    def __overwriteDeltaTable(self, datalist: List[Tuple[Any, Any]],\n                              schema: Union[StructType, List[str]] = [\"key\", \"value\"],\n                              overwriteSchema: str = 'false') -> None:\n        df = self.spark.createDataFrame(datalist, schema)\n        df.write.format(\"delta\") \\\n            .option('overwriteSchema', overwriteSchema) \\\n            .mode(\"overwrite\") \\\n            .save(self.tempFile)\n\n    def __createFile(self, fileName: str, content: Any) -> None:\n        with open(os.path.join(self.tempFile, fileName), 'w') as f:\n            f.write(content)\n\n    def __checkFileExists(self, fileName: str) -> bool:\n        return os.path.exists(os.path.join(self.tempFile, fileName))\n\n    def __intercept(self, func: Callable[[], None], exceptionMsg: str) -> None:\n        seenTheRightException = False\n        try:\n            func()\n        except Exception as e:\n            if exceptionMsg in str(e):\n                seenTheRightException = True\n        assert seenTheRightException, (\"Did not catch expected Exception:\" + exceptionMsg)\n\n\nclass DeltaTableTests(DeltaTableTestsMixin, DeltaTestCase):\n    pass\n\n\nif __name__ == \"__main__\":\n    try:\n        import xmlrunner\n        testRunner = xmlrunner.XMLTestRunner(output='target/test-reports', verbosity=4)\n    except ImportError:\n        testRunner = None\n    unittest.main(testRunner=testRunner, verbosity=4)\n"
  },
  {
    "path": "python/delta/tests/test_exceptions.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Any, Callable, TYPE_CHECKING\nimport unittest\n\nimport delta.exceptions.captured as exceptions\n\nfrom delta.testing.utils import DeltaTestCase\nfrom pyspark.sql.utils import AnalysisException, IllegalArgumentException\n\nif TYPE_CHECKING:\n    from py4j.java_gateway import JVMView  # type: ignore[import]\n\n\nclass DeltaExceptionTests(DeltaTestCase):\n\n    def setUp(self) -> None:\n        super(DeltaExceptionTests, self).setUp()\n        self.jvm: \"JVMView\" = self.spark.sparkContext._jvm  # type: ignore[attr-defined]\n\n    def _raise_concurrent_exception(self, exception_type: Callable[[Any], Any]) -> None:\n        e = exception_type(\"\")\n        self.jvm.scala.util.Failure(e).get()\n\n    def test_capture_concurrent_write_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.ConcurrentWriteException\n        self.assertRaises(exceptions.ConcurrentWriteException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_metadata_changed_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.MetadataChangedException\n        self.assertRaises(exceptions.MetadataChangedException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_protocol_changed_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.ProtocolChangedException\n        self.assertRaises(exceptions.ProtocolChangedException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_concurrent_append_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.ConcurrentAppendException\n        self.assertRaises(exceptions.ConcurrentAppendException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_concurrent_delete_read_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.ConcurrentDeleteReadException\n        self.assertRaises(exceptions.ConcurrentDeleteReadException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_concurrent_delete_delete_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.ConcurrentDeleteDeleteException\n        self.assertRaises(exceptions.ConcurrentDeleteDeleteException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_concurrent_transaction_exception(self) -> None:\n        e = self.jvm.io.delta.exceptions.ConcurrentTransactionException\n        self.assertRaises(exceptions.ConcurrentTransactionException,\n                          lambda: self._raise_concurrent_exception(e))\n\n    def test_capture_delta_analysis_exception(self) -> None:\n        e = self.jvm.org.apache.spark.sql.delta.DeltaErrors.invalidColumnName\n        self.assertRaises(AnalysisException,\n                          lambda: self.jvm.scala.util.Failure(e(\"invalid\")).get())\n\n    def test_capture_delta_illegal_argument_exception(self) -> None:\n        e = self.jvm.org.apache.spark.sql.delta.DeltaErrors\n        method = e.throwDeltaIllegalArgumentException\n        self.assertRaises(IllegalArgumentException,\n                          lambda: self.jvm.scala.util.Failure(method()).get())\n\n\nif __name__ == \"__main__\":\n    try:\n        import xmlrunner\n        testRunner = xmlrunner.XMLTestRunner(output='target/test-reports', verbosity=4)\n    except ImportError:\n        testRunner = None\n    unittest.main(testRunner=testRunner, verbosity=4)\n"
  },
  {
    "path": "python/delta/tests/test_pip_utils.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport shutil\nimport tempfile\nimport unittest\nfrom typing import List, Optional\n\nfrom pyspark.sql import SparkSession\nimport delta\n\n\nclass PipUtilsTests(unittest.TestCase):\n\n    def setUp(self) -> None:\n        builder = SparkSession.builder \\\n            .appName(\"pip-test\") \\\n            .master(\"local[*]\") \\\n            .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n            .config(\"spark.sql.catalog.spark_catalog\",\n                    \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n\n        self.spark = delta.configure_spark_with_delta_pip(builder).getOrCreate()\n        self.tempPath = tempfile.mkdtemp()\n        self.tempFile = os.path.join(self.tempPath, \"tempFile\")\n\n    def tearDown(self) -> None:\n        self.spark.stop()\n        shutil.rmtree(self.tempPath)\n\n    def test_maven_jar_loaded(self) -> None:\n        # Read and write Delta table to check that the maven jars are loaded and Delta works.\n        self.spark.range(0, 5).write.format(\"delta\").save(self.tempFile)\n        self.spark.read.format(\"delta\").load(self.tempFile)\n\n\nclass PipUtilsCustomJarsTests(unittest.TestCase):\n\n    def setUp(self) -> None:\n        builder = SparkSession.builder \\\n            .appName(\"pip-test\") \\\n            .master(\"local[*]\") \\\n            .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n            .config(\"spark.sql.catalog.spark_catalog\",\n                    \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n\n        import importlib_metadata\n        scala_version = \"2.12\"\n        delta_version = importlib_metadata.version(\"delta_spark\")\n        maven_artifacts = [f\"io.delta:delta-spark_{scala_version}:{delta_version}\"]\n        # configure extra packages\n        self.spark = delta.configure_spark_with_delta_pip(builder, maven_artifacts).getOrCreate()\n\n        self.tempPath = tempfile.mkdtemp()\n        self.tempFile = os.path.join(self.tempPath, \"tempFile\")\n\n    def tearDown(self) -> None:\n        self.spark.stop()\n        shutil.rmtree(self.tempPath)\n\n    def test_maven_jar_loaded(self) -> None:\n        packagesConf: Optional[str] = self.spark.conf.get(\"spark.jars.packages\")\n        assert packagesConf is not None  # mypi needs this to assign type str from Optional[str]\n        packages: str = packagesConf\n        packagesList: List[str] = packages.split(\",\")\n        # Check `spark.jars.packages` contains `extra_packages`\n        self.assertTrue(len(packagesList) == 2, \"There should only be 2 packages\")\n\n        # Read and write Delta table to check that the maven jars are loaded and Delta works.\n        self.spark.range(0, 5).write.format(\"delta\").save(self.tempFile)\n        self.spark.read.format(\"delta\").load(self.tempFile)\n\n\nif __name__ == \"__main__\":\n    try:\n        import xmlrunner\n        testRunner = xmlrunner.XMLTestRunner(output='target/test-reports', verbosity=4)\n    except ImportError:\n        testRunner = None\n    unittest.main(testRunner=testRunner, verbosity=4)\n"
  },
  {
    "path": "python/delta/tests/test_sql.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# mypy: disable-error-code=\"union-attr\"\n# mypy: disable-error-code=\"attr-defined\"\n\nimport unittest\nimport tempfile\nimport shutil\nimport os\nfrom typing import List, Any\n\nfrom pyspark.sql import DataFrame\n\nfrom delta.testing.utils import DeltaTestCase\n\n\nclass DeltaSqlTests(DeltaTestCase):\n\n    def setUp(self) -> None:\n        super(DeltaSqlTests, self).setUp()\n        # Create a simple Delta table inside the temp directory to test SQL commands.\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        df.write.format(\"delta\").save(self.tempFile)\n        df.write.mode(\"overwrite\").format(\"delta\").save(self.tempFile)\n\n    def test_vacuum(self) -> None:\n        self.spark.sql(\"set spark.databricks.delta.retentionDurationCheck.enabled = false\")\n        try:\n            deleted_files = self.spark.sql(\"VACUUM '%s' RETAIN 0 HOURS\" % self.tempFile).collect()\n            # Verify `VACUUM` did delete some data files\n            self.assertTrue(self.tempFile in deleted_files[0][0])\n        finally:\n            self.spark.sql(\"set spark.databricks.delta.retentionDurationCheck.enabled = true\")\n\n    def test_describe_history(self) -> None:\n        self.assertGreater(\n            len(self.spark.sql(\"desc history delta.`%s`\" % (self.tempFile)).collect()), 0)\n\n    def test_generate(self) -> None:\n        # create a delta table\n        temp_path = tempfile.mkdtemp()\n        temp_file = os.path.join(temp_path, \"delta_sql_test_table\")\n        numFiles = 10\n        self.spark.range(100).repartition(numFiles).write.format(\"delta\").save(temp_file)\n\n        # Generate the symlink format manifest\n        self.spark.sql(\"GENERATE SYMLINK_FORMAT_MANIFEST FOR TABLE delta.`{}`\"\n                       .format(temp_file))\n\n        # check the contents of the manifest\n        # NOTE: this is not a correctness test, we are testing correctness in the scala suite\n        manifestPath = os.path.join(temp_file,\n                                    os.path.join(\"_symlink_format_manifest\", \"manifest\"))\n        files = []\n        with open(manifestPath) as f:\n            files = f.readlines()\n\n        shutil.rmtree(temp_path)\n        # the number of files we write should equal the number of lines in the manifest\n        self.assertEqual(len(files), numFiles)\n\n    def test_convert(self) -> None:\n        df = self.spark.createDataFrame([('a', 1), ('b', 2), ('c', 3)], [\"key\", \"value\"])\n        temp_path2 = tempfile.mkdtemp()\n        temp_path3 = tempfile.mkdtemp()\n        temp_file2 = os.path.join(temp_path2, \"delta_sql_test2\")\n        temp_file3 = os.path.join(temp_path3, \"delta_sql_test3\")\n\n        df.write.format(\"parquet\").save(temp_file2)\n        self.spark.sql(\"CONVERT TO DELTA parquet.`\" + temp_file2 + \"`\")\n        self.__checkAnswer(\n            self.spark.read.format(\"delta\").load(temp_file2),\n            [('a', 1), ('b', 2), ('c', 3)])\n\n        # test if convert to delta with partition columns work\n        df.write.partitionBy(\"value\").format(\"parquet\").save(temp_file3)\n        self.spark.sql(\"CONVERT TO DELTA parquet.`\" + temp_file3 + \"` PARTITIONED BY (value INT)\")\n        self.__checkAnswer(\n            self.spark.read.format(\"delta\").load(temp_file3),\n            [('a', 1), ('b', 2), ('c', 3)])\n\n        shutil.rmtree(temp_path2)\n        shutil.rmtree(temp_path3)\n\n    def test_ddls(self) -> None:\n        table = \"deltaTable\"\n        table2 = \"deltaTable2\"\n        with self.table(table, table + \"_part\", table2):\n            def read_table() -> DataFrame:\n                return self.spark.sql(f\"SELECT * FROM {table}\")\n\n            self.spark.sql(f\"DROP TABLE IF EXISTS {table}\")\n            self.spark.sql(f\"DROP TABLE IF EXISTS {table}_part\")\n            self.spark.sql(f\"DROP TABLE IF EXISTS {table2}\")\n\n            self.spark.sql(f\"CREATE TABLE {table}(a LONG, b String NOT NULL) USING delta\")\n            self.assertEqual(read_table().count(), 0)\n            self.spark.sql(f\"CREATE TABLE {table}_part(a LONG, b String NOT NULL)\"\n                           \" USING delta PARTITIONED BY (a)\")\n\n            # Unpartitioned table does not include partitioning information in Spark 3.4+\n            answer = [(\"a\", \"bigint\"), (\"b\", \"string\")]\n            self.__checkAnswer(\n                self.spark.sql(f\"DESCRIBE TABLE {table}\").select(\"col_name\", \"data_type\"),\n                answer,\n                schema=[\"col_name\", \"data_type\"])\n\n            answer_part = [(\"a\", \"bigint\"), (\"b\", \"string\"), (\"# Partition Information\", \"\"),\n                           (\"# col_name\", \"data_type\"), (\"a\", \"bigint\")]\n            self.__checkAnswer(\n                self.spark.sql(f\"DESCRIBE TABLE {table}_part\").select(\"col_name\", \"data_type\"),\n                answer_part,\n                schema=[\"col_name\", \"data_type\"])\n\n            self.spark.sql(f\"ALTER TABLE {table} CHANGE COLUMN a a LONG AFTER b\")\n            self.assertSequenceEqual([\"b\", \"a\"], [f.name for f in read_table().schema.fields])\n\n            self.spark.sql(f\"ALTER TABLE {table} ALTER COLUMN b DROP NOT NULL\")\n            self.assertIn(True, [f.nullable for f in read_table().schema.fields if f.name == \"b\"])\n\n            self.spark.sql(f\"ALTER TABLE {table} ADD COLUMNS (x LONG)\")\n            self.assertIn(\"x\", [f.name for f in read_table().schema.fields])\n\n            self.spark.sql(f\"ALTER TABLE {table} SET TBLPROPERTIES ('k' = 'v')\")\n            self.__checkAnswer(self.spark.sql(f\"SHOW TBLPROPERTIES {table}\"),\n                               [('k', 'v'),\n                                ('delta.minReaderVersion', '1'),\n                                ('delta.minWriterVersion', '2')])\n\n            self.spark.sql(f\"ALTER TABLE {table} UNSET TBLPROPERTIES ('k')\")\n            self.__checkAnswer(self.spark.sql(f\"SHOW TBLPROPERTIES {table}\"),\n                               [('delta.minReaderVersion', '1'),\n                                ('delta.minWriterVersion', '2')])\n\n            self.spark.sql(f\"ALTER TABLE {table} RENAME TO {table2}\")\n            self.assertEqual(self.spark.sql(f\"SELECT * FROM {table2}\").count(), 0)\n\n            test_dir = os.path.join(tempfile.mkdtemp(), table2)\n            self.spark.createDataFrame([(\"\", 0, 0)], [\"b\", \"a\", \"x\"]) \\\n                .write.format(\"delta\").save(test_dir)\n\n            self.spark.sql(f\"ALTER TABLE {table2} SET LOCATION '{test_dir}'\")\n            self.assertEqual(self.spark.sql(f\"SELECT * FROM {table2}\").count(), 1)\n\n    def __checkAnswer(self, df: DataFrame,\n                      expectedAnswer: List[Any],\n                      schema: List[str] = [\"key\", \"value\"]) -> None:\n        if not expectedAnswer:\n            self.assertEqual(df.count(), 0)\n            return\n        expectedDF = self.spark.createDataFrame(expectedAnswer, schema)\n        self.assertEqual(df.count(), expectedDF.count())\n        self.assertEqual(len(df.columns), len(expectedDF.columns))\n        self.assertEqual([], df.subtract(expectedDF).take(1))\n        self.assertEqual([], expectedDF.subtract(df).take(1))\n\n\nif __name__ == \"__main__\":\n    try:\n        import xmlrunner\n        testRunner = xmlrunner.XMLTestRunner(output='target/test-reports', verbosity=4)\n    except ImportError:\n        testRunner = None\n    unittest.main(testRunner=testRunner, verbosity=4)\n"
  },
  {
    "path": "python/delta/tests/test_version.py",
    "content": "#\n# Copyright (2026) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport unittest\nfrom packaging.version import Version\n\n\nclass VersionAPITests(unittest.TestCase):\n\n    version_sbt_path = os.path.abspath(os.path.join(\n        os.path.dirname(__file__), '..', '..', '..', 'version.sbt')\n    )\n\n    def verify_version(self, version: str) -> None:\n        self.assertIsNotNone(version)\n        self.assertIsInstance(version, str)\n        self.assertEqual(version.count(\".\"), 2, \"Version should have major.minor.patch format\")\n        # version should be parseable by packaging.version.Version\n        Version(version)\n\n    def test_version_import_from_module(self) -> None:\n        \"\"\"Test that __version__ can be imported from delta.version\"\"\"\n        from delta.version import __version__\n        self.verify_version(__version__)\n\n    def test_version_import_from_package(self) -> None:\n        \"\"\"Test that __version__ can be imported from delta package\"\"\"\n        from delta import __version__\n        self.verify_version(__version__)\n\n    def test_version_consistency_across_imports(self) -> None:\n        \"\"\"Test that version is consistent across import methods\"\"\"\n        from delta.version import __version__ as version_from_module\n        from delta import __version__ as version_from_package\n\n        self.assertEqual(version_from_module, version_from_package)\n\n    def test_version_sbt_exists(self) -> None:\n        \"\"\"Verify version.sbt exists\"\"\"\n        self.assertTrue(\n            os.path.exists(self.version_sbt_path),\n            f\"version.sbt not found at {self.version_sbt_path}\"\n        )\n\n    def test_version_sbt_and_version_py_consistency(self) -> None:\n        with open(self.version_sbt_path) as f:\n            sbt_content = f.read()\n            # Extract version from: ThisBuild / version := \"x.y.z-SNAPSHOT\" -> \"x.y.z\"\n            sbt_version = sbt_content.split('\"')[1].removesuffix(\"-SNAPSHOT\")\n\n        from delta import __version__\n\n        self.assertEqual(\n            __version__,\n            sbt_version,\n            f\"version.py ({__version__}) does not match version.sbt ({sbt_version}). \"\n            f\"Run: build/sbt sparkV1/generatePythonVersion\"\n        )\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "python/delta/version.py",
    "content": "#\n# Copyright (2026) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This file is auto-generated by the build.sbt generatePythonVersion task.\n# Do not edit manually - edit version.sbt instead and run:\n#   build/sbt sparkV1/generatePythonVersion\n\n__version__ = \"4.1.0\"\n"
  },
  {
    "path": "python/environment.yml",
    "content": "name: delta_python_tests\nchannels:\n  - defaults\n  - https://repo.anaconda.com/pkgs/main\n  - https://repo.anaconda.com/pkgs/r\ndependencies:\n  - _libgcc_mutex=0.1=main\n  - _openmp_mutex=5.1=1_gnu\n  - ca-certificates=2023.08.22=h06a4308_0\n  - ld_impl_linux-64=2.40=h12ee557_0\n  - libcxx=14.0.6=h83ecd13_0\n  - libcxxabi=14.0.6=h06a4308_0\n  - libffi=3.4.4=h6a678d5_1\n  - libgcc-ng=11.2.0=h1234567_1\n  - libgomp=11.2.0=h1234567_1\n  - libstdcxx-ng=11.2.0=h1234567_1\n  - ncurses=6.4=h6a678d5_0\n  - openssl=3.0.11=h7f8727e_2\n  - python=3.8.18=h955ad1f_0\n  - readline=8.2=h5eee18b_0\n  - sqlite=3.41.2=h5eee18b_0\n  - tk=8.6.12=h1ccaba5_0\n  - xz=5.4.2=h5eee18b_0\n  - zlib=1.2.13=h5eee18b_1\n  - pip:\n      - alabaster==0.7.13\n      - babel==2.13.0\n      - backports-tarfile==1.2.0\n      - black==23.9.1\n      - certifi==2023.7.22\n      - cffi==1.17.1\n      - charset-normalizer==3.3.0\n      - click==8.1.8\n      - colorama==0.4.6\n      - cryptography==37.0.4\n      - delta-spark==3.4.0-SNAPSHOT\n      - docutils==0.15.2\n      - flake8==3.5.0\n      - idna==3.4\n      - imagesize==1.4.1\n      - importlib-metadata==8.5.0\n      - importlib-resources==6.4.5\n      - jaraco-classes==3.4.0\n      - jaraco-context==6.0.1\n      - jaraco-functools==4.1.0\n      - jeepney==0.9.0\n      - jinja2==2.11.3\n      - keyring==25.5.0\n      - livereload==2.6.3\n      - markdown-it-py==3.0.0\n      - markupsafe==2.0.0\n      - mccabe==0.6.1\n      - mdurl==0.1.2\n      - more-itertools==10.5.0\n      - mypy==0.982\n      - mypy-extensions==1.0.0\n      - mypy-protobuf==3.3.0\n      - nh3==0.2.21\n      - numpy==1.24.4\n      - packaging==23.2\n      - pandas==1.1.3\n      - pathspec==0.12.1\n      - pip==24.0\n      - pkginfo==1.12.1.2\n      - platformdirs==4.3.6\n      - protobuf==5.29.3\n      - py4j==0.10.9.7\n      - pyarrow==8.0.0\n      - pycodestyle==2.3.1\n      - pycparser==2.22\n      - pydocstyle==3.0.0\n      - pyflakes==1.6.0\n      - pygments==2.16.1\n      - pypandoc==1.3.3\n      - pyspark==3.5.3\n      - python-dateutil==2.9.0.post0\n      - pytz==2023.3.post1\n      - readme-renderer==43.0\n      - requests==2.31.0\n      - requests-toolbelt==1.0.0\n      - rfc3986==2.0.0\n      - rich==13.9.4\n      - secretstorage==3.3.3\n      - setuptools==41.1.0\n      - six==1.16.0\n      - snowballstemmer==2.2.0\n      - sphinx==2.0.1\n      - sphinx-autobuild==2021.3.14\n      - sphinxcontrib-applehelp==1.0.4\n      - sphinxcontrib-devhelp==1.0.2\n      - sphinxcontrib-htmlhelp==2.0.1\n      - sphinxcontrib-jsmath==1.0.1\n      - sphinxcontrib-qthelp==1.0.3\n      - sphinxcontrib-serializinghtml==1.1.5\n      - tomli==2.2.1\n      - tornado==6.3.3\n      - twine==4.0.1\n      - types-protobuf==5.29.1.20241207\n      - typing-extensions==4.12.2\n      - urllib3==2.0.6\n      - wheel==0.33.4\n      - zipp==3.20.2\nprefix: <home>/miniconda3/envs/delta_python_tests\n"
  },
  {
    "path": "python/mypy.ini",
    "content": ";\n;  Copyright (2021) The Delta Lake Project Authors.\n;\n;  Licensed under the Apache License, Version 2.0 (the \"License\");\n;  you may not use this file except in compliance with the License.\n;  You may obtain a copy of the License at\n;\n;  http://www.apache.org/licenses/LICENSE-2.0\n;\n;  Unless required by applicable law or agreed to in writing, software\n;  distributed under the License is distributed on an \"AS IS\" BASIS,\n;  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n;  See the License for the specific language governing permissions and\n;  limitations under the License.\n;\n\n[mypy]\nstrict_optional = True\nno_implicit_optional = True\ndisallow_untyped_defs = True\nshow_error_codes = True\n\n[mypy-xmlrunner.*]\nignore_missing_imports = True\n\n[mypy-delta.connect.proto.proto.*]\nignore_errors = True\nignore_missing_imports = True\n\n[mypy-delta.connect.*]\nignore_errors = True\nignore_missing_imports = True\n\n[mypy-google.*]\nignore_missing_imports = True\n\n[mypy-py4j.*]\nignore_missing_imports = True\n"
  },
  {
    "path": "python/run-tests.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport subprocess\nimport shutil\nfrom os import path\nimport json\n\n\ndef test(root_dir, code_dir, packages):\n    # Test the codes in the code_dir directory using its \"tests\" subdirectory,\n    # each of them has main entry point to execute, which is python's unittest testing\n    # framework.\n    python_root_dir = path.join(root_dir, \"python\")\n    test_dir = path.join(python_root_dir, path.join(code_dir, \"tests\"))\n    test_files = [os.path.join(test_dir, f) for f in os.listdir(test_dir)\n                  if os.path.isfile(os.path.join(test_dir, f)) and\n                  f.endswith(\".py\") and not f.startswith(\"_\")]\n    extra_class_path = path.join(python_root_dir, path.join(code_dir, \"testing\"))\n\n    # Include Maven local repository to resolve locally published Delta artifacts\n    maven_local_repo = \"file://\" + os.path.expanduser(\"~/.m2/repository\")\n\n    for test_file in test_files:\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,\n                   \"--repositories\",\n                   (f\"{maven_local_repo},\"\n                    \"https://maven-central.storage-download.googleapis.com/maven2/,\"\n                       \"https://repo1.maven.org/maven2/,\"\n                       \"https://repository.apache.org/content/repositories/orgapachespark-1484\"),\n                   \"--packages\", \",\".join(packages), test_file]\n            print(\"Running tests in %s\\n=============\" % test_file)\n            print(\"Command: %s\" % str(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed tests in %s\" % (test_file))\n            raise\n\n\ndef delete_if_exists(path):\n    # if path exists, delete it.\n    if os.path.exists(path):\n        shutil.rmtree(path)\n        print(\"Deleted %s \" % path)\n\n\ndef prepare(root_dir, spark_version):\n    print(\"##### Preparing python tests & building packages #####\")\n    # Build package with python files in it\n    sbt_path = path.join(root_dir, path.join(\"build\", \"sbt\"))\n    ivy_caches_to_clear = [\n        filepath for filepath in os.listdir(os.path.expanduser(\"~\"))\n        if filepath.startswith(\".ivy\")\n    ]\n    print(f\"Clearing Ivy caches in: {ivy_caches_to_clear}\")\n    for filepath in ivy_caches_to_clear:\n        delete_if_exists(os.path.expanduser(f\"~/{filepath}/cache/io.delta\"))\n    delete_if_exists(os.path.expanduser(\"~/.m2/repository/io/delta/\"))\n    sbt_command = [sbt_path]\n    sbt_command = sbt_command + [f\"-DsparkVersion={spark_version}\"]\n    run_cmd(sbt_command + [\"clean\", \"publishM2\"], stream_output=True)\n\n\ndef get_local_package(package_name, spark_version, root_dir):\n    \"\"\"Get the Maven coordinates for a Delta package.\n\n    Queries CrossSparkVersions for the packageSuffix (e.g., \"\", \"_4.1\").\n\n    Args:\n        package_name: Name of the package (e.g., \"delta-spark\", \"delta-connect-server\")\n        spark_version: Spark version string (e.g., \"4.0\", \"4.1\", or \"default\")\n        root_dir: Root directory of the Delta repository\n\n    Returns:\n        Maven coordinates string (e.g., \"io.delta:delta-spark_2.13:4.1.0-SNAPSHOT\")\n    \"\"\"\n    # Get current release version\n    version = '0.0.0'\n    with open(os.path.join(root_dir, \"version.sbt\")) as fd:\n        version = fd.readline().split('\"')[1]\n\n    # Get package suffix directly from CrossSparkVersions (single source of truth)\n    script_path = os.path.join(root_dir, \"project\", \"scripts\", \"get_spark_version_info.py\")\n    try:\n        result = subprocess.run(\n            [\"python3\", script_path, \"--get-field\", spark_version, \"packageSuffix\"],\n            cwd=root_dir,\n            capture_output=True,\n            text=True,\n            check=True\n        )\n        package_name_suffix = json.loads(result.stdout.strip())\n    except Exception as e:\n        print(f\"Warning: Could not determine package suffix for Spark {spark_version}: {e}\")\n        print(f\"Falling back to empty suffix\")\n        package_name_suffix = \"\"\n\n    return f\"io.delta:{package_name}{package_name_suffix}_2.13:\" + version\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, print_cmd=True, **kwargs):\n    if print_cmd:\n        print(\"### Executing cmd: \" + \" \".join(cmd))\n\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % (exit_code))\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return (exit_code, stdout, stderr)\n\n\ndef run_python_style_checks(root_dir):\n    print(\"##### Running python style tests #####\")\n    run_cmd([os.path.join(root_dir, \"dev\", \"lint-python\")], stream_output=True)\n\n\ndef run_mypy_tests(root_dir):\n    print(\"##### Running mypy tests #####\")\n    python_package_root = path.join(root_dir, path.join(\"python\", \"delta\"))\n    mypy_config_path = path.join(root_dir, path.join(\"python\", \"mypy.ini\"))\n    run_cmd([\n        \"mypy\",\n        \"--config-file\", mypy_config_path,\n        python_package_root\n    ], stream_output=True)\n\n\ndef run_pypi_packaging_tests(root_dir):\n    \"\"\"\n    We want to test that the delta-spark PyPi artifact for this delta version can be generated,\n    locally installed, and used in python tests.\n\n    We will uninstall any existing local delta-spark PyPi artifact.\n    We will generate a new local delta-spark PyPi artifact.\n    We will install it into the local PyPi repository.\n    And then we will run relevant python tests to ensure everything works as expected.\n    \"\"\"\n    print(\"##### Running PyPi Packaging tests #####\")\n\n    version = '0.0.0'\n    with open(os.path.join(root_dir, \"version.sbt\")) as fd:\n        version = fd.readline().split('\"')[1]\n\n    # uninstall packages if they exist\n    run_cmd([\"pip3\", \"uninstall\", \"--yes\", \"delta-spark\"], stream_output=True)\n\n    wheel_dist_dir = path.join(root_dir, \"dist\")\n\n    print(\"### Deleting `dist` directory if it exists\")\n    delete_if_exists(wheel_dist_dir)\n\n    # generate artifacts\n    run_cmd(\n        [\"python3\", \"setup.py\", \"bdist_wheel\"],\n        stream_output=True,\n        stderr=open('/dev/null', 'w'))\n\n    run_cmd([\"python3\", \"setup.py\", \"sdist\"], stream_output=True)\n\n    # we need, for example, 1.1.0_SNAPSHOT not 1.1.0-SNAPSHOT\n    version_formatted = version.replace(\"-\", \"_\")\n    delta_whl_name = \"delta_spark-\" + version_formatted + \"-py3-none-any.whl\"\n\n    # this will install delta-spark-$version\n    install_whl_cmd = [\"pip3\", \"install\", path.join(wheel_dist_dir, delta_whl_name)]\n    run_cmd(install_whl_cmd, stream_output=True)\n\n    # run test python file directly with python and not with spark-submit\n    test_file = path.join(root_dir, path.join(\"examples\", \"python\", \"using_with_pip.py\"))\n    test_cmd = [\"python3\", test_file]\n    try:\n        print(\"### Starting tests...\")\n        run_cmd(test_cmd, stream_output=True)\n    except:\n        print(\"Failed pip installation tests in %s\" % (test_file))\n        raise\n\n\ndef run_delta_connect_codegen_python(root_dir):\n    print(\"##### Running generated Delta Connect Python protobuf codes syncing tests #####\")\n    test_file = os.path.join(root_dir, \"dev\", \"check-delta-connect-codegen-python.py\")\n    test_cmd = [\"python3\", test_file]\n    run_cmd(test_cmd, stream_output=True)\n\n\nif __name__ == \"__main__\":\n    print(\"##### Running python tests #####\")\n    root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n    spark_version = os.getenv(\"SPARK_VERSION\") or \"default\"\n    prepare(root_dir, spark_version)\n    delta_spark_package = get_local_package(\"delta-spark\", spark_version, root_dir)\n\n    run_python_style_checks(root_dir)\n    run_mypy_tests(root_dir)\n    run_pypi_packaging_tests(root_dir)\n    test(root_dir, \"delta\", [delta_spark_package])\n\n    # Run Delta Connect tests as well\n    run_delta_connect_codegen_python(root_dir)\n    # TODO: In the future, find a way to get these\n    # packages locally instead of downloading from Maven.\n    # Get the full Spark version for spark-connect artifact\n    script_path = os.path.join(root_dir, \"project\", \"scripts\", \"get_spark_version_info.py\")\n    result = subprocess.run(\n        [\"python3\", script_path, \"--get-field\", spark_version, \"fullVersion\"],\n        cwd=root_dir,\n        capture_output=True,\n        text=True,\n        check=True\n    )\n    spark_full_version = json.loads(result.stdout.strip())\n\n    delta_connect_packages = [\"com.google.protobuf:protobuf-java:3.25.1\",\n                              f\"org.apache.spark:spark-connect_2.13:{spark_full_version}\",\n                              get_local_package(\"delta-connect-server\", spark_version, root_dir)]\n\n    test(root_dir, path.join(\"delta\", \"connect\"), delta_connect_packages)\n"
  },
  {
    "path": "run-integration-tests.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Integration test script for Delta Lake. Builds artifacts locally and runs\n# Scala, Python, and pip tests against them.\n#\n# Usage:\n#   python run-integration-tests.py --use-local              # Run all tests\n#   python run-integration-tests.py --use-local --scala-only # Scala tests only\n#   python run-integration-tests.py --use-local --python-only # Python tests only\n#\n# Setup:\n#   With --use-local, tests run across all Spark versions defined in\n#   CrossSparkVersions.scala. Each Spark version needs a local distribution at:\n#     ~/spark-{version}-bin-hadoop3/\n#\n#   Download them from Apache:\n#     wget https://archive.apache.org/dist/spark/spark-{version}/spark-{version}-bin-hadoop3.tgz\n#     tar xzf spark-{version}-bin-hadoop3.tgz -C ~/\n#\n\nimport os\nimport subprocess\nfrom os import path\nimport shutil\nimport argparse\nimport json\n\n\n_original_path = os.environ.get(\"PATH\", \"\")\n\n\ndef set_spark_env(spark_version):\n    \"\"\"\n    Sets SPARK_HOME and prepends its bin/ to PATH for the given Spark version.\n    Resets PATH to its original value first to avoid accumulation.\n\n    This must override any existing SPARK_HOME because the multi-variant loop tests\n    different Spark versions in sequence (e.g., 4.0.1 then 4.1.0).\n    \"\"\"\n    os.environ[\"PATH\"] = _original_path\n\n    # In non-local mode, spark_version is \"\" — tests resolve artifacts from Maven Central\n    # and use whatever spark-submit is already on PATH. SNAPSHOT versions also have no\n    # pre-built distribution to look up, so we fall back to PATH.\n    if not spark_version or \"-SNAPSHOT\" in spark_version:\n        print(\"Using spark-submit from PATH for version %s\" % (spark_version or \"unspecified\"))\n        return\n\n    spark_home = os.path.expanduser(\"~/spark-%s-bin-hadoop3\" % spark_version)\n    if not os.path.isdir(spark_home):\n        raise Exception(\n            \"Spark %s not found at %s. Please download it first:\\n\"\n            \"  wget https://archive.apache.org/dist/spark/spark-%s/spark-%s-bin-hadoop3.tgz\\n\"\n            \"  tar xzf spark-%s-bin-hadoop3.tgz -C ~/\"\n            % (spark_version, spark_home, spark_version, spark_version, spark_version))\n\n    os.environ[\"SPARK_HOME\"] = spark_home\n    spark_bin = os.path.join(spark_home, \"bin\")\n    os.environ[\"PATH\"] = spark_bin + os.pathsep + _original_path\n    print(\"Using SPARK_HOME=%s\" % spark_home)\n\n\ndef delete_if_exists(path):\n    # if path exists, delete it.\n    if os.path.exists(path):\n        shutil.rmtree(path)\n        print(\"Deleted %s \" % path)\n\n\ndef load_spark_version_specs(root_dir):\n    \"\"\"\n    Loads Spark version specs from target/spark-versions.json (single source of truth).\n    Runs `build/sbt exportSparkVersionsJson` if the file doesn't exist yet.\n    Returns a list of dicts with keys: fullVersion, shortVersion, isMaster, isDefault,\n    targetJvm, packageSuffix, supportIceberg, supportHudi.\n    \"\"\"\n    json_path = path.join(root_dir, \"target\", \"spark-versions.json\")\n    if not path.exists(json_path):\n        print(\"Generating %s via exportSparkVersionsJson...\" % json_path)\n        run_cmd([\"build/sbt\", \"exportSparkVersionsJson\"], stream_output=True)\n    with open(json_path) as f:\n        return json.load(f)\n\n\ndef publish_all_variants(root_dir, spark_specs):\n    \"\"\"\n    Publishes all artifact variants once upfront (replaces per-function publishM2 calls).\n\n    Step 1: Publish all modules WITHOUT Spark suffix (backward compatibility)\n    Step 2: Publish Spark-dependent modules WITH suffix for each non-master Spark version\n    \"\"\"\n    # Step 1: unsuffixed (backward compat)\n    print(\"\\n##### Publishing all modules without Spark suffix (backward compat) #####\")\n    run_cmd([\"build/sbt\", \"-DskipSparkSuffix=true\", \"publishM2\"], stream_output=True)\n\n    # Step 2: suffixed for each non-master Spark version\n    # Clean between publishes to avoid stale class files from different Spark shims\n    for spec in spark_specs:\n        if spec.get(\"isMaster\", False):\n            continue\n        spark_version = spec[\"fullVersion\"]\n        print(\"\\n##### Publishing Spark-dependent modules for Spark %s #####\" % spark_version)\n        run_cmd(\n            [\"build/sbt\", \"-DsparkVersion=%s\" % spark_version,\n             \"runOnlyForReleasableSparkModules clean\",\n             \"runOnlyForReleasableSparkModules publishM2\"],\n            stream_output=True)\n\n\ndef get_spark_variants(spark_specs):\n    \"\"\"\n    Builds the list of artifact variants to test from the Spark version specs.\n\n    Each variant is a dict with:\n      - suffix: Maven artifact suffix, e.g. \"\" (unsuffixed), \"_4.0\", \"_4.1\"\n      - spark_version: full Spark version, e.g. \"4.1.0\", \"4.0.1\"\n      - support_iceberg: \"true\" or \"false\"\n      - support_hudi: \"true\" or \"false\"\n\n    The first variant is always unsuffixed (backward compat) using the DEFAULT spec's metadata.\n    Remaining variants are suffixed, one per non-master Spark version.\n\n    Example return value (given Spark 4.0 and 4.1 specs, with 4.1 as default):\n      [\n        {\"suffix\": \"\",     \"spark_version\": \"4.1.0\", \"support_iceberg\": \"false\", \"support_hudi\": \"false\"},\n        {\"suffix\": \"_4.0\", \"spark_version\": \"4.0.1\", \"support_iceberg\": \"true\",  \"support_hudi\": \"true\"},\n        {\"suffix\": \"_4.1\", \"spark_version\": \"4.1.0\", \"support_iceberg\": \"false\", \"support_hudi\": \"false\"},\n      ]\n    \"\"\"\n    variants = []\n\n    # Find the default spec for the unsuffixed backward-compat variant\n    default_spec = None\n    for spec in spark_specs:\n        if spec.get(\"isDefault\", False):\n            default_spec = spec\n            break\n    if default_spec is None and spark_specs:\n        default_spec = spark_specs[-1]  # fallback to last spec\n\n    # Unsuffixed variant (backward compat) - uses default's metadata\n    if default_spec:\n        variants.append({\n            \"suffix\": \"\",\n            \"spark_version\": default_spec[\"fullVersion\"],\n            \"support_iceberg\": default_spec.get(\"supportIceberg\", \"false\"),\n            \"support_hudi\": default_spec.get(\"supportHudi\", \"false\"),\n        })\n\n    # Suffixed variants for each non-master spec\n    for spec in spark_specs:\n        if spec.get(\"isMaster\", False):\n            continue\n        variants.append({\n            \"suffix\": spec[\"packageSuffix\"],\n            \"spark_version\": spec[\"fullVersion\"],\n            \"support_iceberg\": spec.get(\"supportIceberg\", \"false\"),\n            \"support_hudi\": spec.get(\"supportHudi\", \"false\"),\n        })\n\n    return variants\n\n\ndef run_scala_integration_tests(root_dir, version, test_name, extra_maven_repo, scala_version,\n                                variant):\n    \"\"\"\n    Runs Scala integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    spark_version = variant[\"spark_version\"]\n    support_iceberg = variant[\"support_iceberg\"]\n    label = \" (suffix=%s, spark=%s)\" % (suffix or \"none\", spark_version) if suffix or spark_version else \"\"\n\n    print(\"\\n\\n##### Running Scala tests%s on delta %s, scala %s #####\"\n          % (label, str(version), scala_version))\n\n    test_dir = path.join(root_dir, \"examples\", \"scala\")\n    test_src_dir = path.join(test_dir, \"src\", \"main\", \"scala\", \"example\")\n    test_classes = [f.replace(\".scala\", \"\") for f in os.listdir(test_src_dir)\n                    if f.endswith(\".scala\") and not f.startswith(\"_\")]\n\n    # Set env vars that examples/scala/build.sbt reads to resolve dependencies:\n    # SPARK_PACKAGE_SUFFIX -> artifact suffix (e.g., \"_4.0\")\n    # SPARK_VERSION -> Spark version for spark-sql/spark-hive deps (e.g., \"4.0.1\")\n    # SUPPORT_ICEBERG -> whether to include Iceberg deps and compile IcebergCompat examples\n    env = {\"DELTA_VERSION\": str(version), \"SCALA_VERSION\": scala_version}\n    if suffix:\n        env[\"SPARK_PACKAGE_SUFFIX\"] = suffix\n    if spark_version:\n        env[\"SPARK_VERSION\"] = spark_version\n    if support_iceberg == \"true\":\n        env[\"SUPPORT_ICEBERG\"] = \"true\"\n    if extra_maven_repo:\n        env[\"EXTRA_MAVEN_REPO\"] = extra_maven_repo\n\n    with WorkingDirectory(test_dir):\n        for test_class in test_classes:\n            if test_name is not None and test_name not in test_class:\n                print(\"\\nSkipping Scala tests in %s\\n=====================\" % test_class)\n                continue\n\n            # Skip Iceberg tests for variants that don't support Iceberg\n            if \"IcebergCompat\" in test_class and support_iceberg != \"true\":\n                print(\"\\nSkipping %s (Iceberg not supported for this variant)\\n=====================\" % test_class)\n                continue\n\n            try:\n                cmd = [\"build/sbt\", \"runMain example.%s\" % test_class]\n                print(\"\\nRunning Scala tests in %s%s\\n=====================\" % (test_class, label))\n                print(\"Command: %s\" % \" \".join(cmd))\n                run_cmd(cmd, stream_output=True, env=env)\n            except:\n                print(\"Failed Scala tests in %s%s\" % (test_class, label))\n                raise\n\n\ndef get_artifact_name(version):\n    \"\"\"\n    version: string representation, e.g. 2.3.0 or 3.0.0.rc1\n    return: either \"core\" or \"spark\"\n    \"\"\"\n    return \"spark\" if int(version[0]) >= 3 else \"core\"\n\n\ndef run_python_integration_tests(root_dir, version, test_name, extra_maven_repo, variant):\n    \"\"\"\n    Runs Python integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    label = \" (suffix=%s)\" % (suffix or \"none\") if suffix else \"\"\n\n    print(\"\\n\\n##### Running Python tests%s on version %s #####\" % (label, str(version)))\n\n    test_dir = path.join(root_dir, path.join(\"examples\", \"python\"))\n    files_to_skip = {\"using_with_pip.py\", \"missing_delta_storage_jar.py\", \"image_storage.py\", \"delta_connect.py\"}\n\n    test_files = [path.join(test_dir, f) for f in os.listdir(test_dir)\n                  if path.isfile(path.join(test_dir, f)) and\n                  f.endswith(\".py\") and not f.startswith(\"_\") and\n                  f not in files_to_skip]\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n    repo = extra_maven_repo if extra_maven_repo else \"\"\n\n    # Build Maven coordinate with the variant's suffix\n    # e.g., \"io.delta:delta-spark_2.13:4.0.0\" or \"io.delta:delta-spark_4.0_2.13:4.0.0\"\n    artifact_name = get_artifact_name(version)\n    package = \"io.delta:delta-%s%s_2.13:%s\" % (artifact_name, suffix, version)\n    print(\"Package: %s\" % package)\n\n    for test_file in test_files:\n        if test_name is not None and test_name not in test_file:\n            print(\"\\nSkipping Python tests in %s\\n=====================\" % test_file)\n            continue\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n                   \"--packages\", package,\n                   \"--repositories\", repo, test_file]\n            print(\"\\nRunning Python tests in %s%s\\n=============\" % (test_file, label))\n            print(\"Command: %s\" % \" \".join(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed Python tests in %s%s\" % (test_file, label))\n            raise\n\n\ndef test_missing_delta_storage_jar(root_dir, version, use_local):\n    if not use_local:\n        print(\"Skipping 'missing_delta_storage_jar' - test should only run in local mode\")\n        return\n\n    print(\"\\n\\n##### Running 'missing_delta_storage_jar' on version %s #####\" % str(version))\n\n    # The unsuffixed artifact was published via publish_all_variants upfront.\n    # Clear only the delta-storage artifact to test the missing JAR scenario.\n    print(\"Clearing delta-storage artifact\")\n    delete_if_exists(os.path.expanduser(\"~/.m2/repository/io/delta/delta-storage\"))\n    delete_if_exists(os.path.expanduser(\"~/.ivy2/cache/io.delta/delta-storage\"))\n    delete_if_exists(os.path.expanduser(\"~/.ivy2/local/io.delta/delta-storage\"))\n    delete_if_exists(os.path.expanduser(\"~/.ivy2.5.2/local/io.delta/delta-storage\"))\n    delete_if_exists(os.path.expanduser(\"~/.ivy2.5.2/cache/io.delta/delta-storage\"))\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n    test_file = path.join(root_dir, path.join(\"examples\", \"python\", \"missing_delta_storage_jar.py\"))\n    artifact_name = get_artifact_name(version)\n    # Uses unsuffixed artifact name (published via -DskipSparkSuffix=true)\n    jar = path.join(\n        os.path.expanduser(\"~/.m2/repository/io/delta/\"),\n        \"delta-%s_2.13\" % artifact_name,\n        version,\n        \"delta-%s_2.13-%s.jar\" % (artifact_name, str(version)))\n\n    try:\n        cmd = [\"spark-submit\",\n               \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n               \"--jars\", jar, test_file]\n        print(\"\\nRunning Python tests in %s\\n=============\" % test_file)\n        print(\"Command: %s\" % \" \".join(cmd))\n        run_cmd(cmd, stream_output=True)\n    except:\n        print(\"Failed Python tests in %s\" % (test_file))\n        raise\n\n\ndef run_dynamodb_logstore_integration_tests(root_dir, version, test_name, extra_maven_repo,\n                                            extra_packages, conf, variant):\n    \"\"\"\n    Runs DynamoDB logstore integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    label = \" (suffix=%s)\" % (suffix or \"none\") if suffix else \"\"\n\n    print(\n        \"\\n\\n##### Running DynamoDB logstore integration tests%s on version %s #####\"\n        % (label, str(version))\n    )\n\n    test_dir = path.join(root_dir, path.join(\"storage-s3-dynamodb\", \"integration_tests\"))\n    test_files = [path.join(test_dir, f) for f in os.listdir(test_dir)\n                  if path.isfile(path.join(test_dir, f)) and\n                  f.endswith(\".py\") and not f.startswith(\"_\")]\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n\n    conf_args = []\n    if conf:\n        for i in conf:\n            conf_args.extend([\"--conf\", i])\n\n    repo_args = [\"--repositories\", extra_maven_repo] if extra_maven_repo else []\n\n    # Build package string: delta-spark with suffix + delta-storage-s3-dynamodb (Spark-independent, no suffix)\n    artifact_name = get_artifact_name(version)\n    packages = \"io.delta:delta-%s%s_2.13:%s\" % (artifact_name, suffix, version)\n    packages += \",\" + \"io.delta:delta-storage-s3-dynamodb:\" + version\n    if extra_packages:\n        packages += \",\" + extra_packages\n\n    for test_file in test_files:\n        if test_name is not None and test_name not in test_file:\n            print(\"\\nSkipping DynamoDB logstore integration tests in %s\\n============\" % test_file)\n            continue\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n                   \"--packages\", packages] + repo_args + conf_args + [test_file]\n            print(\"\\nRunning DynamoDB logstore integration tests in %s%s\\n=============\" % (test_file, label))\n            print(\"Command: %s\" % \" \".join(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed DynamoDB logstore integration tests tests in %s%s\" % (test_file, label))\n            raise\n\ndef run_dynamodb_commit_coordinator_integration_tests(root_dir, version, test_name, extra_maven_repo,\n                                                extra_packages, conf, variant):\n    \"\"\"\n    Runs DynamoDB Commit Coordinator integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    label = \" (suffix=%s)\" % (suffix or \"none\") if suffix else \"\"\n\n    print(\n        \"\\n\\n##### Running DynamoDB Commit Coordinator integration tests%s on version %s #####\"\n        % (label, str(version))\n    )\n\n    test_dir = path.join(root_dir, \\\n        path.join(\"spark\", \"src\", \"main\", \"java\", \"io\", \"delta\", \"dynamodbcommitcoordinator\", \"integration_tests\"))\n    test_files = [path.join(test_dir, f) for f in os.listdir(test_dir)\n                  if path.isfile(path.join(test_dir, f)) and\n                  f.endswith(\".py\") and not f.startswith(\"_\")]\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n\n    conf_args = []\n    if conf:\n        for i in conf:\n            conf_args.extend([\"--conf\", i])\n\n    repo_args = [\"--repositories\", extra_maven_repo] if extra_maven_repo else []\n\n    # Build package string with the variant's suffix\n    artifact_name = get_artifact_name(version)\n    packages = \"io.delta:delta-%s%s_2.13:%s\" % (artifact_name, suffix, version)\n    if extra_packages:\n        packages += \",\" + extra_packages\n\n    for test_file in test_files:\n        if test_name is not None and test_name not in test_file:\n            print(\"\\nSkipping DynamoDB Commit Coordinator integration tests in %s\\n============\" % test_file)\n            continue\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n                   \"--packages\", packages] + repo_args + conf_args + [test_file]\n            print(\"\\nRunning DynamoDB Commit Coordinator integration tests in %s%s\\n=============\" % (test_file, label))\n            print(\"Command: %s\" % \" \".join(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed DynamoDB Commit Coordinator integration tests in %s%s\" % (test_file, label))\n            raise\n\ndef run_s3_log_store_util_integration_tests():\n    print(\"\\n\\n##### Running S3LogStoreUtil tests #####\")\n\n    env = { \"S3_LOG_STORE_UTIL_TEST_ENABLED\": \"true\" }\n    assert os.environ.get(\"S3_LOG_STORE_UTIL_TEST_BUCKET\") is not None, \"S3_LOG_STORE_UTIL_TEST_BUCKET must be set\"\n    assert os.environ.get(\"S3_LOG_STORE_UTIL_TEST_RUN_UID\") is not None, \"S3_LOG_STORE_UTIL_TEST_RUN_UID must be set\"\n\n    try:\n        cmd = [\"build/sbt\", \"project storage\", \"testOnly -- -n IntegrationTest\"]\n        print(\"\\nRunning IntegrationTests of storage\\n=====================\")\n        print(\"Command: %s\" % \" \".join(cmd))\n        run_cmd(cmd, stream_output=True, env=env)\n    except:\n        print(\"Failed IntegrationTests\")\n        raise\n\n\ndef run_iceberg_integration_tests(root_dir, version, iceberg_version, extra_maven_repo, variant):\n    \"\"\"\n    Runs Iceberg integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n             spark_version is used to derive the iceberg-spark-runtime artifact name\n             (e.g., \"4.0.1\" -> iceberg-spark-runtime-4.0_2.13).\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    spark_version = variant[\"spark_version\"]\n    label = \" (suffix=%s)\" % (suffix or \"none\") if suffix else \"\"\n\n    print(\"\\n\\n##### Running Iceberg tests%s on version %s #####\" % (label, str(version)))\n\n    test_dir = path.join(root_dir, path.join(\"iceberg\", \"integration_tests\"))\n\n    # Add more Iceberg tests here if needed ...\n    test_files_names = [\"iceberg_converter.py\"]\n    test_files = [path.join(test_dir, f) for f in test_files_names]\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n    repo = extra_maven_repo if extra_maven_repo else \"\"\n\n    artifact_name = get_artifact_name(version)\n\n    # Derive major.minor Spark version for iceberg-spark-runtime artifact name\n    # e.g., \"4.0.1\" -> \"4.0\", or \"4.0\" stays \"4.0\"\n    parts = spark_version.split(\".\")\n    iceberg_spark_ver = \"%s.%s\" % (parts[0], parts[1]) if len(parts) >= 2 else spark_version\n\n    # Build package string with suffixed Delta artifacts + Iceberg runtime\n    package = ','.join([\n        \"io.delta:delta-%s%s_2.13:%s\" % (artifact_name, suffix, version),\n        \"io.delta:delta-iceberg_2.13:%s\" % (version),\n        \"org.apache.iceberg:iceberg-spark-runtime-{}_2.13:{}\".format(iceberg_spark_ver, iceberg_version)])\n\n    print(\"Package: %s\" % package)\n\n    for test_file in test_files:\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n                   \"--packages\", package,\n                   \"--repositories\", repo, test_file]\n            print(\"\\nRunning Iceberg tests in %s%s\\n=============\" % (test_file, label))\n            print(\"Command: %s\" % \" \".join(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed Iceberg tests in %s%s\" % (test_file, label))\n            raise\n\ndef run_uniform_hudi_integration_tests(root_dir, version, hudi_version, extra_maven_repo, variant):\n    \"\"\"\n    Runs Uniform Hudi integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n             spark_version is used to derive the hudi-spark-bundle artifact name\n             (e.g., \"4.0.1\" -> hudi-spark4.0-bundle_2.13).\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    spark_version = variant[\"spark_version\"]\n    label = \" (suffix=%s)\" % (suffix or \"none\") if suffix else \"\"\n\n    print(\"\\n\\n##### Running Uniform hudi tests%s on version %s #####\" % (label, str(version)))\n\n    test_dir = path.join(root_dir, path.join(\"hudi\", \"integration_tests\"))\n\n    # Add more tests here if needed ...\n    test_files_names = [\"write_uniform_hudi.py\"]\n    test_files = [path.join(test_dir, f) for f in test_files_names]\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n    # The hudi assembly JAR path uses name.value (no suffix), not moduleName\n    jars = path.join(root_dir, \"hudi/target/scala-2.13/delta-hudi-assembly_2.13-%s.jar\" % (version))\n    repo = extra_maven_repo if extra_maven_repo else \"\"\n\n    artifact_name = get_artifact_name(version)\n\n    # Derive major.minor Spark version for hudi-spark-bundle artifact name\n    # e.g., \"4.0.1\" -> \"4.0\", or \"4.0\" stays \"4.0\"\n    parts = spark_version.split(\".\")\n    hudi_spark_ver = \"%s.%s\" % (parts[0], parts[1]) if len(parts) >= 2 else spark_version\n\n    # Build package string with suffixed Delta artifact + Hudi bundle\n    package = ','.join([\n        \"io.delta:delta-%s%s_2.13:%s\" % (artifact_name, suffix, version),\n        \"org.apache.hudi:hudi-spark%s-bundle_2.13:%s\" % (hudi_spark_ver, hudi_version)\n    ])\n\n    print(\"Package: %s\" % package)\n\n    for test_file in test_files:\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n                   \"--packages\", package,\n                   \"--jars\", jars,\n                   \"--repositories\", repo, test_file]\n            print(\"\\nRunning Uniform Hudi tests in %s%s\\n=============\" % (test_file, label))\n            print(\"Command: %s\" % \" \".join(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed Uniform Hudi tests in %s%s\" % (test_file, label))\n            raise\n\ndef run_pip_installation_tests(root_dir, version, use_testpypi, use_localpypi, extra_maven_repo):\n    print(\"\\n\\n##### Running pip installation tests on version %s #####\" % str(version))\n    # Note: no clear_artifact_cache() here. Pip tests install from PyPI, not local M2.\n    delta_pip_name = \"delta-spark\"\n    # uninstall packages if they exist\n    run_cmd([\"pip\", \"uninstall\", \"--yes\", delta_pip_name, \"pyspark\"], stream_output=True)\n\n    # install packages\n    delta_pip_name_with_version = \"%s==%s\" % (delta_pip_name, str(version))\n    if use_testpypi:\n        install_cmd = [\"pip\", \"install\",\n                       \"--extra-index-url\", \"https://test.pypi.org/simple/\",\n                       delta_pip_name_with_version]\n    elif use_localpypi:\n        pip_wheel_file_name = \"%s-%s-py3-none-any.whl\" % \\\n                              (delta_pip_name.replace(\"-\", \"_\"), str(version))\n        pip_wheel_file_path = os.path.join(use_localpypi, pip_wheel_file_name)\n        install_cmd = [\"pip\", \"install\", pip_wheel_file_path]\n    else:\n        install_cmd = [\"pip\", \"install\", delta_pip_name_with_version]\n    print(\"pip install command: %s\" % str(install_cmd))\n    run_cmd(install_cmd, stream_output=True)\n\n    # run test python file directly with python and not with spark-submit\n    env = {}\n    if extra_maven_repo:\n        env[\"EXTRA_MAVEN_REPO\"] = extra_maven_repo\n    tests = [\"image_storage.py\", \"using_with_pip.py\"]\n    for test in tests:\n        test_file = path.join(root_dir, path.join(\"examples\", \"python\", test))\n        print(\"\\nRunning Python tests in %s\\n=============\" % test_file)\n        test_cmd = [\"python3\", test_file]\n        print(\"Test command: %s\" % str(test_cmd))\n        try:\n            run_cmd(test_cmd, stream_output=True, env=env)\n        except:\n            print(\"Failed pip installation tests in %s\" % (test_file))\n            raise\n\ndef run_unity_catalog_commit_coordinator_integration_tests(root_dir, version, test_name,\n                                                           variant, extra_packages):\n    \"\"\"\n    Runs Unity Catalog commit coordinator integration tests for a single artifact variant.\n\n    variant: dict with suffix, spark_version, support_iceberg, support_hudi.\n             See get_spark_variants() for the format and example.\n    \"\"\"\n    suffix = variant[\"suffix\"]\n    label = \" (suffix=%s)\" % (suffix or \"none\") if suffix else \"\"\n\n    print(\n        \"\\n\\n##### Running Unity Catalog commit coordinator integration tests%s on version %s #####\"\n        % (label, str(version))\n    )\n\n    test_dir = path.join(root_dir, \\\n        path.join(\"python\", \"delta\", \"integration_tests\"))\n    test_files = [path.join(test_dir, f) for f in os.listdir(test_dir)\n                  if path.isfile(path.join(test_dir, f)) and\n                  f.endswith(\".py\") and not f.startswith(\"_\")]\n\n    print(\"\\n\\nTests compiled\\n\\n\")\n\n    python_root_dir = path.join(root_dir, \"python\")\n    extra_class_path = path.join(python_root_dir, path.join(\"delta\", \"testing\"))\n\n    # Build package string with the variant's suffix\n    artifact_name = get_artifact_name(version)\n    packages = \"io.delta:delta-%s%s_2.13:%s\" % (artifact_name, suffix, version)\n    if extra_packages:\n        packages += \",\" + extra_packages\n\n    for test_file in test_files:\n        if test_name is not None and test_name not in test_file:\n            print(\"\\nSkipping Unity Catalog commit coordinator integration tests in %s\\n============\" % test_file)\n            continue\n        try:\n            cmd = [\"spark-submit\",\n                   \"--driver-class-path=%s\" % extra_class_path,  # for less verbose logging\n                   \"--packages\", packages] + [test_file]\n            print(\"\\nRunning External uc managed tables integration tests in %s%s\\n=============\" % (test_file, label))\n            print(\"Command: %s\" % \" \".join(cmd))\n            run_cmd(cmd, stream_output=True)\n        except:\n            print(\"Failed Unity Catalog commit coordinator integration tests in %s%s\" % (test_file, label))\n            raise\n\ndef clear_artifact_cache():\n    print(\"Clearing Delta artifacts from ivy2 and mvn cache\")\n    ivy_caches_to_clear = [filepath for filepath in os.listdir(os.path.expanduser(\"~\")) if filepath.startswith(\".ivy\")]\n    print(f\"Clearing Ivy caches in: {ivy_caches_to_clear}\")\n    for filepath in ivy_caches_to_clear:\n        delete_if_exists(os.path.expanduser(f\"~/{filepath}/cache/io.delta\"))\n        delete_if_exists(os.path.expanduser(f\"~/{filepath}/local/io.delta\"))\n    delete_if_exists(os.path.expanduser(\"~/.m2/repository/io/delta/\"))\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, **kwargs):\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % (exit_code))\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return (exit_code, stdout, stderr)\n\n\n# pylint: disable=too-few-public-methods\nclass WorkingDirectory(object):\n    def __init__(self, working_directory):\n        self.working_directory = working_directory\n        self.old_workdir = os.getcwd()\n\n    def __enter__(self):\n        os.chdir(self.working_directory)\n\n    def __exit__(self, tpe, value, traceback):\n        os.chdir(self.old_workdir)\n\n\nif __name__ == \"__main__\":\n    \"\"\"\n        Script to run integration tests which are located in the examples directory.\n        call this by running \"python run-integration-tests.py\"\n        additionally the version can be provided as a command line argument.\n        \"\n    \"\"\"\n\n    # get the version of the package\n    root_dir = path.dirname(__file__)\n    with open(path.join(root_dir, \"version.sbt\")) as fd:\n        default_version = fd.readline().split('\"')[1]\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--version\",\n        required=False,\n        default=default_version,\n        help=\"Delta version to use to run the integration tests\")\n    parser.add_argument(\n        \"--python-only\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run only Python tests\")\n    parser.add_argument(\n        \"--scala-only\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run only Scala tests\")\n    parser.add_argument(\n        \"--s3-log-store-util-only\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run only S3LogStoreUtil tests\")\n    parser.add_argument(\n        \"--scala-version\",\n        required=False,\n        default=\"2.13\",\n        help=\"Specify scala version for scala tests only, valid values are '2.13'\")\n    parser.add_argument(\n        \"--pip-only\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run only pip installation tests\")\n    parser.add_argument(\n        \"--no-pip\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Do not run pip installation tests\")\n    parser.add_argument(\n        \"--test\",\n        required=False,\n        default=None,\n        help=\"Run a specific test by substring-match with Scala/Python file name\")\n    parser.add_argument(\n        \"--maven-repo\",\n        required=False,\n        default=None,\n        help=\"Additional Maven repo to resolve staged new release artifacts\")\n    parser.add_argument(\n        \"--use-testpypi\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Use testpypi for testing pip installation\")\n    parser.add_argument(\n        \"--use-localpypiartifact\",\n        required=False,\n        default=None,\n        help=\"Directory path where the downloaded pypi artifacts are present. \" +\n            \"It should have two files: e.g. delta_spark-3.1.0.tar.gz, delta_spark-3.1.0-py3-none-any.whl\")\n    parser.add_argument(\n        \"--use-local\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Generate JARs from local source code and use to run tests\")\n    parser.add_argument(\n        \"--run-storage-s3-dynamodb-integration-tests\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run the DynamoDB integration tests (and only them)\")\n    parser.add_argument(\n        \"--packages\",\n        required=False,\n        default=None,\n        help=\"Additional packages required for integration tests\")\n    parser.add_argument(\n        \"--dbb-conf\",\n        required=False,\n        default=None,\n        nargs=\"+\",\n        help=\"All `--conf` values passed to `spark-submit` for DynamoDB logstore/commit-coordinator integration tests\")\n    parser.add_argument(\n        \"--run-dynamodb-commit-coordinator-integration-tests\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run the DynamoDB Commit Coordinator tests (and only them)\")\n    parser.add_argument(\n        \"--run-iceberg-integration-tests\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run the Iceberg integration tests (and only them)\")\n    parser.add_argument(\n        \"--run-uniform-hudi-integration-tests\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run the Uniform Hudi integration tests (and only them)\")\n    parser.add_argument(\n        \"--iceberg-spark-version\",\n        required=False,\n        default=\"4.0\",\n        help=\"Spark version for the Iceberg library (used in non-local mode)\")\n    parser.add_argument(\n        \"--iceberg-lib-version\",\n        required=False,\n        default=\"1.4.0\",\n        help=\"Iceberg Spark Runtime library version\")\n    parser.add_argument(\n        \"--hudi-spark-version\",\n        required=False,\n        default=\"4.0\",\n        help=\"Spark version for the Hudi library (used in non-local mode)\")\n    parser.add_argument(\n        \"--hudi-version\",\n        required=False,\n        default=\"0.15.0\",\n        help=\"Hudi library version\"\n    )\n    parser.add_argument(\n        \"--unity-catalog-commit-coordinator-integration-tests\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Run the Unity Catalog Commit Coordinator tests (and only them)\"\n    )\n\n    args = parser.parse_args()\n\n    if args.scala_version not in [\"2.13\"]:\n        raise Exception(\"Scala version can only be specified as --scala-version 2.13\")\n\n    if args.pip_only and args.no_pip:\n        raise Exception(\"Cannot specify both --pip-only and --no-pip\")\n\n    if args.use_local and (args.version != default_version):\n        raise Exception(\"Cannot specify --use-local with a --version different than in version.sbt\")\n\n    # When --use-local, publish all artifact variants once upfront and build the variant list\n    # from CrossSparkVersions.scala. In non-local mode, use a single default (unsuffixed) variant.\n    default_variant = {\n        \"suffix\": \"\", \"spark_version\": \"\", \"support_iceberg\": \"false\", \"support_hudi\": \"false\"\n    }\n    spark_specs = None\n    variants = [default_variant]\n    if args.use_local:\n        spark_specs = load_spark_version_specs(root_dir)\n        clear_artifact_cache()\n        publish_all_variants(root_dir, spark_specs)\n        variants = get_spark_variants(spark_specs)\n\n    run_python = not args.scala_only and not args.pip_only\n    run_scala = not args.python_only and not args.pip_only\n    run_pip = not args.python_only and not args.scala_only and not args.no_pip\n\n    if args.run_iceberg_integration_tests:\n        # In local mode, only test variants that support Iceberg.\n        # In non-local mode, run once with --iceberg-spark-version from CLI args.\n        if spark_specs:\n            iceberg_variants = [v for v in variants if v[\"support_iceberg\"] == \"true\"]\n            if not iceberg_variants:\n                print(\"No Spark variants support Iceberg - skipping Iceberg integration tests\")\n                quit()\n        else:\n            iceberg_variants = [{\n                \"suffix\": \"\", \"spark_version\": args.iceberg_spark_version,\n                \"support_iceberg\": \"true\", \"support_hudi\": \"false\"\n            }]\n        for variant in iceberg_variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_iceberg_integration_tests(\n                root_dir, args.version, args.iceberg_lib_version, args.maven_repo, variant)\n        quit()\n\n    if args.run_uniform_hudi_integration_tests:\n        # Build hudi assembly once before running tests (needs specific Spark version)\n        if args.use_local:\n            hudi_spark_ver = None\n            if spark_specs:\n                for spec in spark_specs:\n                    if spec.get(\"supportHudi\", \"false\") == \"true\":\n                        hudi_spark_ver = spec[\"fullVersion\"]\n                        break\n            if hudi_spark_ver:\n                run_cmd([\"build/sbt\", \"-DsparkVersion=%s\" % hudi_spark_ver, \"hudi/assembly\"],\n                        stream_output=True)\n            else:\n                run_cmd([\"build/sbt\", \"hudi/assembly\"], stream_output=True)\n\n        # In local mode, only test variants that support Hudi.\n        # In non-local mode, run once with --hudi-spark-version from CLI args.\n        if spark_specs:\n            hudi_variants = [v for v in variants if v[\"support_hudi\"] == \"true\"]\n            if not hudi_variants:\n                print(\"No Spark variants support Hudi - skipping Hudi integration tests\")\n                quit()\n        else:\n            hudi_variants = [{\n                \"suffix\": \"\", \"spark_version\": args.hudi_spark_version,\n                \"support_iceberg\": \"false\", \"support_hudi\": \"true\"\n            }]\n        for variant in hudi_variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_uniform_hudi_integration_tests(\n                root_dir, args.version, args.hudi_version, args.maven_repo, variant)\n        quit()\n\n    if args.run_storage_s3_dynamodb_integration_tests:\n        for variant in variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_dynamodb_logstore_integration_tests(root_dir, args.version, args.test,\n                                                    args.maven_repo, args.packages,\n                                                    args.dbb_conf, variant)\n        quit()\n\n    if args.run_dynamodb_commit_coordinator_integration_tests:\n        for variant in variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_dynamodb_commit_coordinator_integration_tests(root_dir, args.version, args.test,\n                                                        args.maven_repo, args.packages,\n                                                        args.dbb_conf, variant)\n        quit()\n\n    if args.s3_log_store_util_only:\n        run_s3_log_store_util_integration_tests()\n        quit()\n\n    if args.unity_catalog_commit_coordinator_integration_tests:\n        for variant in variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_unity_catalog_commit_coordinator_integration_tests(root_dir, args.version,\n                                                                    args.test, variant,\n                                                                    args.packages)\n        quit()\n\n    # Run the standard test suite: Scala, Python, pip\n    # Each test function is called once per variant (the loop is here, not inside the functions)\n    if run_scala:\n        for variant in variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_scala_integration_tests(root_dir, args.version, args.test, args.maven_repo,\n                                        args.scala_version, variant)\n\n    if run_python:\n        for variant in variants:\n            set_spark_env(variant[\"spark_version\"])\n            run_python_integration_tests(root_dir, args.version, args.test, args.maven_repo,\n                                         variant)\n\n        test_missing_delta_storage_jar(root_dir, args.version, args.use_local)\n\n    if run_pip:\n        if args.use_testpypi and args.use_localpypiartifact is not None:\n            raise Exception(\"Cannot specify both --use-testpypi and --use-localpypiartifact.\")\n\n        run_pip_installation_tests(root_dir, args.version, args.use_testpypi,\n                                   args.use_localpypiartifact, args.maven_repo)\n"
  },
  {
    "path": "run-tests.py",
    "content": "#!/usr/bin/env python3\n\n#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport subprocess\nimport shlex\nfrom os import path\nimport argparse\n\n# Define groups of subprojects that can be tested separately from other groups.\n# As of now, we have only defined project groups in the SBT build, so these must match\n# the group names defined in build.sbt.\nvalid_project_groups = [\"spark\", \"iceberg\", \"kernel\", \"spark-python\"]\n\n\ndef get_args():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"--group\",\n        required=False,\n        default=None,\n        choices=valid_project_groups,\n        help=\"Run tests on a group of SBT projects\"\n    )\n    parser.add_argument(\n        \"--coverage\",\n        required=False,\n        default=False,\n        action=\"store_true\",\n        help=\"Enables test coverage and generates an aggregate report for all subprojects\")\n    parser.add_argument(\n        \"--shard\",\n        required=False,\n        default=None,\n        help=\"some shard\")\n    parser.add_argument(\n        \"--spark-version\",\n        required=False,\n        default=None,\n        help=\"Spark version to use (passed as -DsparkVersion to SBT)\")\n    return parser.parse_args()\n\n\ndef run_sbt_tests(root_dir, test_group, coverage, scala_version=None, shard=None, spark_version=None):\n    print(\"##### Running SBT tests #####\")\n\n    sbt_path = path.join(root_dir, path.join(\"build\", \"sbt\"))\n    cmd = [sbt_path]\n    \n    # Pass Spark version as system property to SBT (must come before commands)\n    if spark_version:\n        cmd.append(f\"-DsparkVersion={spark_version}\")\n    \n    cmd.append(\"clean\")\n\n    test_cmd = \"test\"\n    if shard:\n        os.environ[\"SHARD_ID\"] = str(shard)\n\n    if test_group:\n        # if test group is specified, then run tests only on that test group\n        test_cmd = \"{}Group/test\".format(test_group)\n\n    if coverage:\n        cmd += [\"coverage\"]\n\n    if scala_version is None:\n        # when no scala version is specified, run test with all scala versions\n        cmd += [\"+ %s\" % test_cmd]  # build/sbt ... \"+ project/test\" ...\n    else:\n        # when no scala version is specified, run test with only the specified scala version\n        cmd += [\"++ %s\" % scala_version, test_cmd]  # build/sbt ... \"++ 2.13.16\" \"project/test\" ...\n\n    if coverage:\n        cmd += [\"coverageAggregate\", \"coverageOff\"]\n    cmd += [\"-v\"]  # show java options used\n\n    # https://docs.oracle.com/javase/7/docs/technotes/guides/vm/G1.html\n    # a GC that is optimized for larger multiprocessor machines with large memory\n    cmd += [\"-J-XX:+UseG1GC\"]\n    # 6x the default heap size (set in delta/built.sbt)\n    cmd += [\"-J-Xmx6G\"]\n    run_cmd(cmd, stream_output=True)\n\n\ndef run_python_tests(root_dir):\n    print(\"##### Running Python tests #####\")\n    python_test_script = path.join(root_dir, path.join(\"python\", \"run-tests.py\"))\n    print(\"Calling script %s\", python_test_script)\n    run_cmd([\"python3\", python_test_script], env={'DELTA_TESTING': '1'}, stream_output=True)\n\n\ndef run_cmd(cmd, throw_on_error=True, env=None, stream_output=False, **kwargs):\n    if isinstance(cmd, str):\n        old_cmd = cmd\n        cmd = shlex.split(cmd)\n\n    cmd_env = os.environ.copy()\n    if env:\n        cmd_env.update(env)\n    print(\"Running command: \" + str(cmd))\n    if stream_output:\n        child = subprocess.Popen(cmd, env=cmd_env, **kwargs)\n        exit_code = child.wait()\n        if throw_on_error and exit_code != 0:\n            raise Exception(\"Non-zero exitcode: %s\" % (exit_code))\n        return exit_code\n    else:\n        child = subprocess.Popen(\n            cmd,\n            env=cmd_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            **kwargs)\n        (stdout, stderr) = child.communicate()\n        exit_code = child.wait()\n        if not isinstance(stdout, str):\n            # Python 3 produces bytes which needs to be converted to str\n            stdout = stdout.decode(\"utf-8\")\n            stderr = stderr.decode(\"utf-8\")\n        if throw_on_error and exit_code != 0:\n            raise Exception(\n                \"Non-zero exitcode: %s\\n\\nSTDOUT:\\n%s\\n\\nSTDERR:%s\" %\n                (exit_code, stdout, stderr))\n        return (exit_code, stdout, stderr)\n\n\ndef pull_or_build_docker_image(root_dir):\n    \"\"\"\n    This method prepare the docker image for running tests. It uses a hash of the Dockerfile\n    to generate the image tag/name so that we reuse images until the Dockerfile has changed.\n    Then it tries to prepare that image by either pulling from a Docker registry\n    (if configured with environment variable DOCKER_REGISTRY) or by building it from\n    scratch using the Dockerfile. If pulling from registry fails, then it will fallback\n    to building it from scratch, but it will also attempt to push to the registry to\n    avoid image builds in the future.\n    \"\"\"\n\n    dockerfile_path = os.path.join(root_dir, \"Dockerfile\")\n    _, out, _ = run_cmd(\"md5sum %s\" % dockerfile_path)\n    dockerfile_hash = out.strip().split(\" \")[0].strip()\n    print(\"Dockerfile hash: %s\" % dockerfile_hash)\n\n    test_env_image_tag = \"delta_test_env:%s\" % dockerfile_hash\n    print(\"Test env image: %s\" % test_env_image_tag)\n\n    docker_registry = os.getenv(\"DOCKER_REGISTRY\")\n    print(\"Docker registry set as \" + str(docker_registry))\n\n\n    def build_image():\n        print(\"---\\nBuilding image %s ...\" % test_env_image_tag)\n        run_cmd(\"docker build --tag=%s %s\" % (test_env_image_tag, root_dir))\n        print(\"Built image %s\" % test_env_image_tag)\n\n    def pull_image(registry_image_tag):\n        try:\n            print(\"---\\nPulling image %s ...\" % registry_image_tag)\n            run_cmd(\"docker pull %s\" % registry_image_tag)\n            run_cmd(\"docker tag %s %s\" % (registry_image_tag, test_env_image_tag))\n            print(\"Pulling image %s succeeded\" % registry_image_tag)\n            return True\n        except Exception as e:\n            print(\"Pulling image %s failed: %s\" % (registry_image_tag, repr(e)))\n            return False\n\n    def push_image(registry_image_tag):\n        try:\n            print(\"---\\nPushing image %s ...\" % registry_image_tag)\n            run_cmd(\"docker tag %s %s\" % (test_env_image_tag, registry_image_tag))\n            run_cmd(\"docker push %s\" % registry_image_tag)\n            print(\"Pushing image %s succeeded\" % registry_image_tag)\n            return True\n        except Exception as e:\n            print(\"Pushing image %s failed: %s\" % (registry_image_tag, repr(e)))\n            return False\n\n    if docker_registry is not None:\n        print(\"Attempting to use the docker registry\")\n        test_env_image_tag_with_registry = docker_registry + \"/delta/\" + test_env_image_tag\n        success = pull_image(test_env_image_tag_with_registry)\n        if not success:\n            build_image()\n            push_image(test_env_image_tag_with_registry)\n    else:\n        build_image()\n    return test_env_image_tag\n\n\ndef run_tests_in_docker(image_tag, test_group):\n    \"\"\"\n    Run the necessary tests in a docker container made from the given image.\n    It starts the container with the delta repo mounted in it, and then\n    executes this script.\n    \"\"\"\n\n    # Note: Pass only relevant env that the script needs to run in the docker container.\n    # Do not pass docker related env variable as we want this script to run natively in\n    # the container and not attempt to recursively another docker container.\n    envs = \"-e JENKINS_URL -e SBT_1_5_5_MIRROR_JAR_URL \"\n    scala_version = os.getenv(\"SCALA_VERSION\")\n    if scala_version is not None:\n        envs = envs + \"-e SCALA_VERSION=%s \" % scala_version\n\n    test_parallelism = os.getenv(\"TEST_PARALLELISM_COUNT\")\n    if test_parallelism is not None:\n        envs = envs + \"-e TEST_PARALLELISM_COUNT=%s \" % test_parallelism\n\n    disable_unidoc = os.getenv(\"DISABLE_UNIDOC\")\n    if disable_unidoc is not None:\n        envs = envs + \"-e DISABLE_UNIDOC=%s \" % disable_unidoc\n\n    cwd = os.getcwd()\n    test_script = os.path.basename(__file__)\n\n    test_script_args = \"\"\n    if test_group:\n        test_script_args += \" --group %s\" % test_group\n\n    test_run_cmd = \"docker run --rm  -v %s:%s -w %s %s %s ./%s %s\" % (\n        cwd, cwd, cwd, envs, image_tag, test_script, test_script_args\n    )\n    run_cmd(test_run_cmd, stream_output=True)\n\n\ndef print_configuration(args: argparse.Namespace) -> None:\n    print(\"=\" * 60)\n    print(\"DELTA LAKE TEST RUNNER CONFIGURATION\")\n    print(\"=\" * 60)\n\n    # Print parsed arguments\n    print(\"-\" * 25)\n    print(\"Command Line Arguments:\")\n    print(\"-\" * 25)\n    args_dict = vars(args)\n    for key, value in args_dict.items():\n        if value is not None:\n            print(f\"  {key:<12}: {value}\")\n        else:\n            print(f\"  {key:<12}: <not set>\")\n\n    # Print relevant environment variables\n    print(\"-\" * 25)\n    print(\"Environment Variables:\")\n    print(\"-\" * 22)\n    env_vars = [\n        \"USE_DOCKER\", \"SCALA_VERSION\", \"DISABLE_UNIDOC\", \"DOCKER_REGISTRY\",\n        \"NUM_SHARDS\", \"SHARD_ID\", \"TEST_PARALLELISM_COUNT\", \"JENKINS_URL\",\n        \"SBT_1_5_5_MIRROR_JAR_URL\", \"DELTA_TESTING\", \"SBT_OPTS\"\n    ]\n\n    for var in env_vars:\n        value = os.getenv(var)\n        if value is not None:\n            print(f\"  {var:<22}: {value}\")\n        else:\n            print(f\"  {var:<22}: <not set>\")\n\n    print(\"=\" * 60)\n\n\nif __name__ == \"__main__\":\n    root_dir = os.path.dirname(os.path.abspath(__file__))\n    args = get_args()\n\n    print_configuration(args)\n\n    if os.getenv(\"USE_DOCKER\") is not None:\n        test_env_image_tag = pull_or_build_docker_image(root_dir)\n        run_tests_in_docker(test_env_image_tag, args.group)\n    elif args.group == \"spark-python\":\n        run_python_tests(root_dir)\n    else:\n        scala_version = os.getenv(\"SCALA_VERSION\")\n        spark_version = args.spark_version or os.getenv(\"SPARK_VERSION\")\n        run_sbt_tests(root_dir, args.group, args.coverage, scala_version, args.shard, spark_version)\n"
  },
  {
    "path": "scalastyle-config.xml",
    "content": "<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~    http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<!--\n  ~ This file contains code from the Apache Spark project (original license above).\n  ~It contains modifications, which are licensed as follows:\n  -->\n<!--\n  ~ Copyright (2021) The Delta Lake Project Authors.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~\n  ~ http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<!--\n\nIf you wish to turn off checking for a section of code, you can put a comment in the source\nbefore and after the section, with the following syntax:\n\n  // scalastyle:off\n  ...  // stuff that breaks the styles\n  // scalastyle:on\n\nYou can also disable only one rule, by specifying its rule id, as specified in:\n  http://www.scalastyle.org/rules-0.7.0.html\n\n  // scalastyle:off no.finalize\n  override def finalize(): Unit = ...\n  // scalastyle:on no.finalize\n\nThis file is divided into 3 sections:\n (1) rules that we enforce.\n (2) rules that we would like to enforce, but haven't cleaned up the codebase to turn on yet\n     (or we need to make the scalastyle rule more configurable).\n (3) rules that we don't want to enforce.\n-->\n\n<scalastyle>\n  <name>Scalastyle standard configuration</name>\n\n  <!-- ================================================================================ -->\n  <!--                               rules we enforce                                   -->\n  <!-- ================================================================================ -->\n\n  <check level=\"error\" class=\"org.scalastyle.file.FileTabChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.HeaderMatchesChecker\" enabled=\"true\">\n    <parameters>\n       <parameter name=\"regex\">true</parameter>\n       <parameter name=\"header\"><![CDATA[(?:\\Q/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n\\E)?\\Q/*\n * Copyright (\\E\\d{4}\\Q) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\\E]]></parameter>\n    </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SpacesAfterPlusChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SpacesBeforePlusChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.WhitespaceEndOfLineChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.FileLineLengthChecker\" enabled=\"true\">\n    <parameters>\n      <parameter name=\"maxLineLength\"><![CDATA[100]]></parameter>\n      <parameter name=\"tabSize\"><![CDATA[2]]></parameter>\n      <parameter name=\"ignoreImports\">true</parameter>\n    </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.ClassNamesChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\"><![CDATA[[A-Z][A-Za-z]*]]></parameter></parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.ObjectNamesChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\"><![CDATA[(config|[A-Z][A-Za-z]*)]]></parameter></parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.PackageObjectNamesChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\"><![CDATA[^[a-z][A-Za-z]*$]]></parameter></parameters>\n  </check>\n\n  <check customId=\"argcount\" level=\"error\" class=\"org.scalastyle.scalariform.ParameterNumberChecker\" enabled=\"true\">\n    <parameters><parameter name=\"maxParameters\"><![CDATA[10]]></parameter></parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoFinalizeChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.CovariantEqualsChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.StructuralTypeChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.UppercaseLChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.IfBraceChecker\" enabled=\"true\">\n    <parameters>\n      <parameter name=\"singleLineAllowed\"><![CDATA[true]]></parameter>\n      <parameter name=\"doubleLineAllowed\"><![CDATA[true]]></parameter>\n    </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.PublicMethodsHaveTypeChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.file.NewLineAtEofChecker\" enabled=\"true\"></check>\n\n  <check customId=\"nonascii\" level=\"error\" class=\"org.scalastyle.scalariform.NonASCIICharacterChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SpaceAfterCommentStartChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.EnsureSingleSpaceBeforeTokenChecker\" enabled=\"true\">\n   <parameters>\n     <parameter name=\"tokens\">ARROW, EQUALS, ELSE, TRY, CATCH, FINALLY, LARROW, RARROW</parameter>\n   </parameters>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.EnsureSingleSpaceAfterTokenChecker\" enabled=\"true\">\n    <parameters>\n     <parameter name=\"tokens\">ARROW, EQUALS, COMMA, COLON, IF, ELSE, DO, WHILE, FOR, MATCH, TRY, CATCH, FINALLY, LARROW, RARROW</parameter>\n    </parameters>\n  </check>\n\n  <!-- ??? usually shouldn't be checked into the code base. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NotImplementedErrorUsage\" enabled=\"true\"></check>\n\n  <!-- As of SPARK-7558, all tests in Spark should extend o.a.s.SparkFunSuite instead of FunSuite directly -->\n  <check customId=\"funsuite\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">^FunSuite[A-Za-z]*$</parameter></parameters>\n    <customMessage>Tests must extend org.apache.spark.SparkFunSuite instead.</customMessage>\n  </check>\n\n  <!-- As of SPARK-7977 all printlns need to be wrapped in '// scalastyle:off/on println' -->\n  <check customId=\"println\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">^println$</parameter></parameters>\n    <customMessage><![CDATA[Are you sure you want to println? If yes, wrap the code block with\n      // scalastyle:off println\n      println(...)\n      // scalastyle:on println]]></customMessage>\n  </check>\n\n  <check customId=\"hadoopconfiguration\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">spark(.sqlContext)?.sparkContext.hadoopConfiguration</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use sparkContext.hadoopConfiguration? In most cases, you should use\n      spark.sessionState.newHadoopConf() instead, so that the hadoop configurations specified in Spark session\n      configuration will come into effect.\n      If you must use sparkContext.hadoopConfiguration, wrap the code block with\n      // scalastyle:off hadoopconfiguration\n      spark.sparkContext.hadoopConfiguration...\n      // scalastyle:on hadoopconfiguration\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"deltahadoopconfiguration\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">sessionState.newHadoopConf</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use sessionState.newHadoopConf? In most cases, you should use\n      deltaLog.newDeltaHadoopConf() instead, so that the hadoop file system configurations specified\n      in DataFrame options will come into effect.\n      If you must use sessionState.newHadoopConf, wrap the code block with\n      // scalastyle:off deltahadoopconfiguration\n      spark.sessionState.newHadoopConf...\n      // scalastyle:on deltahadoopconfiguration\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"visiblefortesting\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">@VisibleForTesting</parameter></parameters>\n    <customMessage><![CDATA[\n      @VisibleForTesting causes classpath issues. Please note this in the java doc instead (SPARK-11615).\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"runtimeaddshutdownhook\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Runtime\\.getRuntime\\.addShutdownHook</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Runtime.getRuntime.addShutdownHook? In most cases, you should use\n      ShutdownHookManager.addShutdownHook instead.\n      If you must use Runtime.getRuntime.addShutdownHook, wrap the code block with\n      // scalastyle:off runtimeaddshutdownhook\n      Runtime.getRuntime.addShutdownHook(...)\n      // scalastyle:on runtimeaddshutdownhook\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"mutablesynchronizedbuffer\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">mutable\\.SynchronizedBuffer</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use mutable.SynchronizedBuffer? In most cases, you should use\n      java.util.concurrent.ConcurrentLinkedQueue instead.\n      If you must use mutable.SynchronizedBuffer, wrap the code block with\n      // scalastyle:off mutablesynchronizedbuffer\n      mutable.SynchronizedBuffer[...]\n      // scalastyle:on mutablesynchronizedbuffer\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"classforname\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Class\\.forName</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Class.forName? In most cases, you should use Utils.classForName instead.\n      If you must use Class.forName, wrap the code block with\n      // scalastyle:off classforname\n      Class.forName(...)\n      // scalastyle:on classforname\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"awaitresult\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Await\\.result</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Await.result? In most cases, you should use ThreadUtils.awaitResult instead.\n      If you must use Await.result, wrap the code block with\n      // scalastyle:off awaitresult\n      Await.result(...)\n      // scalastyle:on awaitresult\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"awaitready\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">Await\\.ready</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use Await.ready? In most cases, you should use ThreadUtils.awaitReady instead.\n      If you must use Await.ready, wrap the code block with\n      // scalastyle:off awaitready\n      Await.ready(...)\n      // scalastyle:on awaitready\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"caselocale\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">(\\.toUpperCase|\\.toLowerCase)(?!(\\(|\\(Locale.ROOT\\)))</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to use toUpperCase or toLowerCase without the root locale? In most cases, you\n      should use toUpperCase(Locale.ROOT) or toLowerCase(Locale.ROOT) instead.\n      If you must use toUpperCase or toLowerCase without the root locale, wrap the code block with\n      // scalastyle:off caselocale\n      .toUpperCase\n      .toLowerCase\n      // scalastyle:on caselocale\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"typedlit\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">typed[lL]it</parameter></parameters>\n    <customMessage><![CDATA[\n      'typedlit' or `typedLit` uses ScalaReflection to resolve the data type which is inefficient in concurrent\n      queries. In most cases, you can use `lit` or `new Column(Literal(...))` instead.\n      If you must use it, wrap the code block with\n      // scalastyle:off typedlit\n      typedLit(\"foo\")\n      // scalastyle:on typedlit\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"sparkimplicits\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">spark(Session)?.implicits._</parameter></parameters>\n    <customMessage><![CDATA[\n      When importing `spark.implicits._`, it's easy to create an `Encoder` unintentionally which can hurt\n      the performance a lot in concurrent queries. You can use `import com.databricks.sql.transaction.tahoe.implicits._`\n      instead to use `Encoder`s we cache for reusing. If this doesn't work, you can define new `Encoder`s\n      in `DeltaEncoders` to cache and reuse them.\n      If you must use it, wrap the code block with\n      // scalastyle:off sparkimplicits\n      import spark.implicits._\n      // scalastyle:on sparkimplicits\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"throwerror\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">throw new \\w+Error\\(</parameter></parameters>\n    <customMessage><![CDATA[\n      Are you sure that you want to throw Error? In most cases, you should use appropriate Exception instead.\n      If you must throw Error, wrap the code block with\n      // scalastyle:off throwerror\n      throw new XXXError(...)\n      // scalastyle:on throwerror\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"countstring\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">count\\(\"</parameter></parameters>\n    <customMessage><![CDATA[\n      'count(String)' returns 'TypedColumn' and touches ScalaReflection which is inefficient in concurrent queries.\n      In most cases, you don't need a 'TypedColumn' and you should use 'count(new Column(\"...\"))' instead.\n      If you must use it, wrap the code block with\n      // scalastyle:off countstring\n      count(\"foo\")\n      // scalastyle:on countstring\n    ]]></customMessage>\n  </check>\n\n  <!-- As of SPARK-9613 JavaConversions should be replaced with JavaConverters -->\n  <check customId=\"javaconversions\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">JavaConversions</parameter></parameters>\n    <customMessage>Instead of importing implicits in scala.collection.JavaConversions._, import\n    scala.collection.JavaConverters._ and use .asScala / .asJava methods</customMessage>\n  </check>\n\n  <check customId=\"commonslang2\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">org\\.apache\\.commons\\.lang\\.</parameter></parameters>\n    <customMessage>Use Commons Lang 3 classes (package org.apache.commons.lang3.*) instead\n    of Commons Lang 2 (package org.apache.commons.lang.*)</customMessage>\n  </check>\n\n  <check customId=\"extractopt\" level=\"error\" class=\"org.scalastyle.scalariform.TokenChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">extractOpt</parameter></parameters>\n    <customMessage>Use jsonOption(x).map(.extract[T]) instead of .extractOpt[T], as the latter\n    is slower.  </customMessage>\n  </check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.DisallowSpaceBeforeTokenChecker\" enabled=\"true\">\n    <parameters>\n      <parameter name=\"tokens\">COMMA</parameter>\n    </parameters>\n  </check>\n\n  <!-- SPARK-3854: Single Space between ')' and '{' -->\n  <check customId=\"SingleSpaceBetweenRParenAndLCurlyBrace\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">\\)\\{</parameter></parameters>\n    <customMessage><![CDATA[\n      Single Space between ')' and `{`.\n    ]]></customMessage>\n  </check>\n\n  <check customId=\"NoScalaDoc\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">(?m)^(\\s*)/[*][*].*$(\\r|)\\n^\\1  [*]</parameter></parameters>\n    <customMessage>Use Javadoc style indentation for multiline comments</customMessage>\n  </check>\n\n  <check customId=\"OmitBracesInCase\" level=\"error\" class=\"org.scalastyle.file.RegexChecker\" enabled=\"true\">\n    <parameters><parameter name=\"regex\">case[^\\n>]*=>\\s*\\{</parameter></parameters>\n    <customMessage>Omit braces in case clauses.</customMessage>\n  </check>\n\n  <!-- SPARK-16877: Avoid Java annotations -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.OverrideJavaChecker\" enabled=\"true\"></check>\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.DeprecatedJavaChecker\" enabled=\"true\"></check>\n\n  <!-- ================================================================================ -->\n  <!--       rules we'd like to enforce, but haven't cleaned up the codebase yet        -->\n  <!-- ================================================================================ -->\n\n  <!-- We cannot turn the following two on, because it'd fail a lot of string interpolation use cases. -->\n  <!-- Ideally the following two rules should be configurable to rule out string interpolation. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoWhitespaceBeforeLeftBracketChecker\" enabled=\"false\"></check>\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoWhitespaceAfterLeftBracketChecker\" enabled=\"false\"></check>\n\n  <!-- This breaks symbolic method names so we don't turn it on. -->\n  <!-- Maybe we should update it to allow basic symbolic names, and then we are good to go. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.MethodNamesChecker\" enabled=\"false\">\n    <parameters>\n    <parameter name=\"regex\"><![CDATA[^[a-z][A-Za-z0-9]*$]]></parameter>\n    </parameters>\n  </check>\n\n  <!-- Should turn this on, but we have a few places that need to be fixed first -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.EqualsHashCodeChecker\" enabled=\"true\"></check>\n\n  <!-- ================================================================================ -->\n  <!--                               rules we don't want                                -->\n  <!-- ================================================================================ -->\n\n  <check level=\"error\" class=\"org.scalastyle.scalariform.IllegalImportsChecker\" enabled=\"false\">\n    <parameters><parameter name=\"illegalImports\"><![CDATA[sun._,java.awt._]]></parameter></parameters>\n  </check>\n\n  <!-- We want the opposite of this: NewLineAtEofChecker -->\n  <check level=\"error\" class=\"org.scalastyle.file.NoNewLineAtEofChecker\" enabled=\"false\"></check>\n\n  <!-- This one complains about all kinds of random things. Disable. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.SimplifyBooleanExpressionChecker\" enabled=\"false\"></check>\n\n  <!-- We use return quite a bit for control flows and guards -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.ReturnChecker\" enabled=\"false\"></check>\n\n  <!-- We use null a lot in low level code and to interface with 3rd party code -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NullChecker\" enabled=\"false\"></check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NoCloneChecker\" enabled=\"false\"></check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.file.FileLengthChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxFileLength\">800></parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NumberOfTypesChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxTypes\">30</parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.CyclomaticComplexityChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maximum\">10</parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.MethodLengthChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxLength\">50</parameter></parameters>\n  </check>\n\n  <!-- Not exactly feasible to enforce this right now. -->\n  <!-- It is also infrequent that somebody introduces a new class with a lot of methods. -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.NumberOfMethodsInTypeChecker\" enabled=\"false\">\n    <parameters><parameter name=\"maxMethods\"><![CDATA[30]]></parameter></parameters>\n  </check>\n\n  <!-- Doesn't seem super big deal here, and we have a lot of magic numbers ... -->\n  <check level=\"error\" class=\"org.scalastyle.scalariform.MagicNumberChecker\" enabled=\"false\">\n    <parameters><parameter name=\"ignore\">-1,0,1,2,3</parameter></parameters>\n  </check>\n\n</scalastyle>\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport os\nimport sys\n\nfrom setuptools import setup\nfrom setuptools.command.install import install\n\n\n# delta.io version\ndef get_version_from_sbt():\n    with open(\"version.sbt\") as fp:\n        version = fp.read().strip()\n    return version.split('\"')[1]\n\nVERSION = get_version_from_sbt()\n\nclass VerifyVersionCommand(install):\n    \"\"\"Custom command to verify that the git tag matches our version\"\"\"\n    description = 'verify that the git tag matches our version'\n\n    def run(self):\n        tag = os.getenv('CIRCLE_TAG')\n\n        if tag != VERSION:\n            info = \"Git tag: {0} does not match the version of this app: {1}\".format(\n                tag, VERSION\n            )\n            sys.exit(info)\n\n\nwith open(\"python/README.md\", \"r\", encoding=\"utf-8\") as fh:\n    long_description = fh.read()\n\ninstall_requires_arg = ['pyspark>=4.0.1', 'importlib_metadata>=1.0.0']\npython_requires_arg = '>=3.10'\n\nsetup(\n    name=\"delta_spark\",\n    version=VERSION,\n    description=\"Python APIs for using Delta Lake with Apache Spark\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/delta-io/delta/\",\n    project_urls={\n        'Source': 'https://github.com/delta-io/delta',\n        'Documentation': 'https://docs.delta.io/latest/index.html',\n        'Issues': 'https://github.com/delta-io/delta/issues'\n    },\n    author=\"The Delta Lake Project Authors\",\n    author_email=\"delta-users@googlegroups.com\",\n    license=\"Apache-2.0\",\n    classifiers=[\n        \"Development Status :: 5 - Production/Stable\",\n        \"Intended Audience :: Developers\",\n        \"License :: OSI Approved :: Apache Software License\",\n        \"Operating System :: OS Independent\",\n        \"Topic :: Software Development :: Libraries :: Python Modules\",\n        \"Programming Language :: Python :: 3\",\n        \"Typing :: Typed\",\n    ],\n    keywords='delta.io',\n    package_dir={'': 'python'},\n    packages=['delta', 'delta.connect', 'delta.connect.proto', 'delta.exceptions'],\n    package_data={\n        'delta': ['py.typed'],\n    },\n    install_requires=install_requires_arg,\n    python_requires=python_requires_arg,\n    cmdclass={\n        'verify': VerifyVersionCommand,\n    }\n)\n"
  },
  {
    "path": "sharing/src/main/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister",
    "content": "io.delta.sharing.spark.DeltaSharingDataSource"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaFormatSharingLimitPushDown.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport io.delta.sharing.client.util.ConfUtils\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.IntegerLiteral\nimport org.apache.spark.sql.catalyst.plans.logical.{LocalLimit, LogicalPlan}\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelationWithTable}\n\n// A spark rule that applies limit pushdown to DeltaSharingFileIndex, when the config is enabled.\n// To allow only fetching needed files from delta sharing server.\nobject DeltaFormatSharingLimitPushDown extends Rule[LogicalPlan] {\n\n  def setup(spark: SparkSession): Unit = synchronized {\n    if (!spark.experimental.extraOptimizations.contains(DeltaFormatSharingLimitPushDown)) {\n      spark.experimental.extraOptimizations ++= Seq(DeltaFormatSharingLimitPushDown)\n    }\n  }\n\n  def apply(p: LogicalPlan): LogicalPlan = {\n    p transform {\n      case localLimit @ LocalLimit(\n            literalExpr @ IntegerLiteral(limit),\n            l @ LogicalRelationWithTable(\n              r @ HadoopFsRelation(remoteIndex: DeltaSharingFileIndex, _, _, _, _, _),\n              _\n            )\n          ) if (ConfUtils.limitPushdownEnabled(p.conf) && remoteIndex.limitHint.isEmpty) =>\n          val spark = SparkSession.active\n          val newRel = r.copy(location = remoteIndex.copy(limitHint = Some(limit)))(spark)\n          LocalLimit(literalExpr, l.copy(relation = newRel))\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaFormatSharingSource.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.lang.ref.WeakReference\nimport java.util.UUID\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta.{\n  DeltaErrors,\n  DeltaLog,\n  DeltaOptions,\n  SnapshotDescriptor\n}\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.{\n  DeltaDataSource,\n  DeltaSource,\n  DeltaSourceOffset\n}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport io.delta.sharing.client.DeltaSharingClient\nimport io.delta.sharing.client.util.ConfUtils\nimport io.delta.sharing.client.model.{Table => DeltaSharingTable}\n\nimport org.apache.spark.delta.sharing.CachedTableManager\nimport org.apache.spark.sql.{DataFrame, SparkSession}\nimport org.apache.spark.sql.connector.read.streaming\nimport org.apache.spark.sql.connector.read.streaming.{ReadLimit, SupportsAdmissionControl}\nimport org.apache.spark.sql.execution.streaming.{Offset, Source}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A streaming source for a Delta Sharing table.\n *\n * This class wraps a DeltaSource to read data out of locally constructed delta log.\n * When a new stream is started, delta sharing starts by fetching delta log from the server side,\n * constructing a local delta log, and call delta source apis to compute offset or read data.\n *\n * TODO: Support CDC Streaming, SupportsTriggerAvailableNow and SupportsConcurrentExecution.\n */\ncase class DeltaFormatSharingSource(\n    spark: SparkSession,\n    client: DeltaSharingClient,\n    table: DeltaSharingTable,\n    options: DeltaSharingOptions,\n    parameters: Map[String, String],\n    sqlConf: SQLConf,\n    metadataPath: String)\n    extends Source\n    with SupportsAdmissionControl\n    with DeltaLogging {\n\n  private val sourceId = Some(UUID.randomUUID().toString().split('-').head)\n\n  private var tableId: String = \"unset_table_id\"\n\n  private val tablePath = options.options.getOrElse(\n    \"path\",\n    throw DeltaSharingErrors.pathNotSpecifiedException\n  )\n\n  // A unique string composed of a formatted timestamp and an uuid.\n  // Used as a suffix for the table name and its delta log path of a delta sharing table in a\n  // streaming job, to avoid overwriting the delta log from multiple references of the same delta\n  // sharing table in one streaming job.\n  private val timestampWithUUID = DeltaSharingUtils.getFormattedTimestampWithUUID()\n  private val customTablePathWithUUIDSuffix = DeltaSharingUtils.getTablePathWithIdSuffix(\n    client.getProfileProvider.getCustomTablePath(tablePath),\n    timestampWithUUID\n  )\n  private val deltaLogPath =\n    s\"${DeltaSharingLogFileSystem.encode(customTablePathWithUUIDSuffix).toString}/_delta_log\"\n\n  // The latest metadata of the shared table, fetched at the initialization time of the\n  // DeltaFormatSharingSource, used to initialize the wrapped DeltaSource.\n  private lazy val deltaSharingTableMetadata =\n    DeltaSharingUtils.getDeltaSharingTableMetadata(client, table)\n\n  private lazy val deltaSource = initDeltaSource()\n\n  private def initDeltaSource(): DeltaSource = {\n    val (localDeltaLog, snapshotDescriptor) = DeltaSharingUtils.getDeltaLogAndSnapshotDescriptor(\n      spark,\n      deltaSharingTableMetadata,\n      customTablePathWithUUIDSuffix\n    )\n    // Delta sharing delta log doesn't have catalog table and `localDeltaLog` is not binded to\n    // catalog table.\n    val schemaTrackingLogOpt =\n      DeltaDataSource.getMetadataTrackingLogForDeltaSource(\n        spark,\n        snapshotDescriptor,\n        catalogTableOpt = None,\n        parameters,\n        // Pass in the metadata path opt so we can use it for validation\n        sourceMetadataPathOpt = Some(metadataPath)\n      )\n\n    val readSchema = schemaTrackingLogOpt\n      .flatMap(_.getCurrentTrackedMetadata.map(_.dataSchema))\n      .getOrElse(snapshotDescriptor.schema)\n\n    if (readSchema.isEmpty) {\n      throw DeltaErrors.schemaNotSetException\n    }\n\n    // Catalog table represents the table's catalog metadata and it's managed by Unity Catalog.\n    // Delta sharing delta log doesn't have it and `localDeltaLog` is not bound to it.\n    DeltaSource(\n      spark = spark,\n      deltaLog = localDeltaLog,\n      catalogTableOpt = None,\n      options = new DeltaOptions(parameters, sqlConf),\n      snapshotAtSourceInit = snapshotDescriptor,\n      metadataPath = metadataPath,\n      metadataTrackingLog = schemaTrackingLogOpt\n    )\n  }\n\n  // schema of the streaming source, based on the latest metadata of the shared table.\n  override val schema: StructType = {\n    val schemaWithoutCDC = deltaSharingTableMetadata.metadata.schema\n    tableId = deltaSharingTableMetadata.metadata.deltaMetadata.id\n    if (options.readChangeFeed) {\n      CDCReader.cdcReadSchema(schemaWithoutCDC)\n    } else {\n      schemaWithoutCDC\n    }\n  }\n\n  // Latest endOffset of the getBatch call, used to compute startingOffset which will then be used\n  // to compare with the the latest table version on server to decide whether to fetch new data.\n  private var latestProcessedEndOffsetOption: Option[DeltaSourceOffset] = None\n\n  // Latest table version for the data fetched from the delta sharing server, and stored in the\n  // local delta log. Used to check whether all fetched files are processed by the DeltaSource.\n  private var latestTableVersionInLocalDeltaLogOpt: Option[Long] = None\n\n  // This is needed because DeltaSource is not advancing the offset to the next version\n  // automatically when scanning through a snapshot, so DeltaFormatSharingSource needs to count the\n  // number of files in the min version and advance the offset to the next version when the offset\n  // is at the last index of the version.\n  private var numFileActionsInStartingSnapshotOpt: Option[Int] = None\n\n  // Latest timestamp for getTableVersion rpc from the server, used to compare with the current\n  // timestamp, to ensure the gap QUERY_TABLE_VERSION_INTERVAL_MILLIS between two rpcs, to avoid\n  // a high traffic load to the server.\n  private var lastTimestampForGetVersionFromServer: Long = -1\n\n  // The minimum gap between two getTableVersion rpcs, to avoid a high traffic load to the server.\n  private val QUERY_TABLE_VERSION_INTERVAL_MILLIS = {\n    val intervalSeconds = ConfUtils.MINIMUM_TABLE_VERSION_INTERVAL_SECONDS.max(\n      ConfUtils.streamingQueryTableVersionIntervalSeconds(spark.sessionState.conf)\n    )\n    logInfo(s\"Configured queryTableVersionIntervalSeconds:${intervalSeconds},\" +\n      getTableInfoForLogging)\n    if (intervalSeconds < ConfUtils.MINIMUM_TABLE_VERSION_INTERVAL_SECONDS) {\n      throw new IllegalArgumentException(s\"QUERY_TABLE_VERSION_INTERVAL_MILLIS($intervalSeconds) \" +\n        s\"must not be less than ${ConfUtils.MINIMUM_TABLE_VERSION_INTERVAL_SECONDS} seconds,\"\n        + getTableInfoForLogging)\n    }\n    TimeUnit.SECONDS.toMillis(intervalSeconds)\n  }\n\n  // Maximum number of versions of getFiles() rpc when fetching files from the server. Used to\n  // reduce the number of files returned to avoid timeout of the rpc on the server.\n  private val maxVersionsPerRpc: Int = options.maxVersionsPerRpc.getOrElse(\n    DeltaSharingOptions.MAX_VERSIONS_PER_RPC_DEFAULT\n  )\n\n  private lazy val getTableInfoForLogging: String =\n    s\" for table(id:$tableId, name:${table.toString}, source:$sourceId)\"\n\n  private def getQueryIdForLogging: String = {\n    s\", with queryId(${client.getQueryId})\"\n  }\n\n  // A variable to store the latest table version on server, returned from the getTableVersion rpc.\n  // Used to store the latest table version for getOrUpdateLatestTableVersion when not getting\n  // updates from the server.\n  // For all other callers, please use getOrUpdateLatestTableVersion instead of this variable.\n  private var latestTableVersionOnServer: Long = -1\n\n  /**\n   * Check the latest table version from the delta sharing server through the client.getTableVersion\n   * RPC. Adding a minimum interval of QUERY_TABLE_VERSION_INTERVAL_MILLIS between two consecutive\n   * rpcs to avoid traffic jam on the delta sharing server.\n   *\n   * @return the latest table version on the server.\n   */\n  private def getOrUpdateLatestTableVersion: Long = {\n    val currentTimeMillis = System.currentTimeMillis()\n    if ((currentTimeMillis - lastTimestampForGetVersionFromServer) >=\n      QUERY_TABLE_VERSION_INTERVAL_MILLIS) {\n      val serverVersion = client.getTableVersion(table)\n      if (serverVersion < 0) {\n        throw new IllegalStateException(s\"Delta Sharing Server returning negative table version:\" +\n          s\"$serverVersion,\" + getTableInfoForLogging)\n      } else if (serverVersion < latestTableVersionOnServer) {\n        logWarning(\n          s\"Delta Sharing Server returning smaller table version: $serverVersion < \" +\n          s\"$latestTableVersionOnServer,\" + getTableInfoForLogging\n        )\n      }\n      logInfo(s\"Got table version $serverVersion from Delta Sharing Server,$getTableInfoForLogging\")\n      latestTableVersionOnServer = serverVersion\n      lastTimestampForGetVersionFromServer = currentTimeMillis\n    }\n    latestTableVersionOnServer\n  }\n\n  /**\n   * NOTE: need to match with the logic in DeltaSource.extractStartingState().\n   *\n   * Get the starting offset used to send rpc to delta sharing server, to fetch needed files.\n   * Use input startOffset when it's defined, otherwise use user defined starting version, otherwise\n   * use input endOffset if it's defined, the least option is the latest table version returned from\n   * the delta sharing server (which is usually used when a streaming query starts from scratch).\n   *\n   * @param startOffsetOption optional start offset, return it if defined. It's empty when the\n   *                          streaming query starts from scratch. It's set for following calls.\n   * @param endOffsetOption   optional end offset. It's set when the function is called from\n   *                          getBatch and is empty when called from latestOffset.\n   * @return The starting offset.\n   */\n  private def getStartingOffset(\n      startOffsetOption: Option[DeltaSourceOffset],\n      endOffsetOption: Option[DeltaSourceOffset]): DeltaSourceOffset = {\n    if (startOffsetOption.isEmpty) {\n      val (version, isInitialSnapshot) = getStartingVersion match {\n        case Some(v) => (v, false)\n        case None =>\n          if (endOffsetOption.isDefined) {\n            if (endOffsetOption.get.isInitialSnapshot) {\n              (endOffsetOption.get.reservoirVersion, true)\n            } else {\n              assert(\n                endOffsetOption.get.reservoirVersion > 0,\n                s\"invalid reservoirVersion in endOffset: ${endOffsetOption.get}\"\n              )\n              // Load from snapshot `endOffset.reservoirVersion - 1L` so that `index` in `endOffset`\n              // is still valid.\n              // It's OK to use the previous version as the updated initial snapshot, even if the\n              // initial snapshot might have been different from the last time when this starting\n              // offset was computed.\n              (endOffsetOption.get.reservoirVersion - 1L, true)\n            }\n          } else {\n            (getOrUpdateLatestTableVersion, true)\n          }\n      }\n      // Constructed the same way as DeltaSource.buildOffsetFromIndexedFile\n      DeltaSourceOffset(\n        reservoirId = tableId,\n        reservoirVersion = version,\n        index = DeltaSourceOffset.BASE_INDEX,\n        isInitialSnapshot = isInitialSnapshot\n      )\n    } else {\n      startOffsetOption.get\n    }\n  }\n\n  /**\n   * Converts an offset from the checkpoint to DeltaSourceOffset, and returns whether it was\n   * converted from legacy format (DeltaSharingSourceOffset or legacy JSON tableId/tableVersion).\n   * @return (DeltaSourceOffset, fromLegacy)\n   * Visible for testing (private[spark]).\n   */\n  private[spark] def forceToDeltaSourceOffset(\n      offset: streaming.Offset): (DeltaSourceOffset, Boolean) = {\n    if (offset == null) {\n      throw new IllegalArgumentException(\"offset cannot be null\")\n    }\n    offset match {\n      case o: DeltaSourceOffset =>\n        (o, false)\n      case o: DeltaSharingSourceOffset =>\n        (convertDeltaSharingSourceOffsetToDeltaSourceOffset(o), true)\n      case _ =>\n        // For JSON (SerializedOffset): parse as DeltaSourceOffset first,\n        // if that throws or yields empty reservoirId, parse as legacy DeltaSharingSourceOffset.\n        try {\n          val deltaOffset = deltaSource.toDeltaSourceOffset(offset)\n          val reservoirIdEmpty =\n            deltaOffset.reservoirId == null || deltaOffset.reservoirId.isEmpty\n          if (reservoirIdEmpty) {\n            // Throw to let the catcher handle the exception.\n            throw new IllegalArgumentException(s\"Invalid offset format: $offset\")\n          } else {\n            logInfo(\"Offset JSON parsed as Delta format\")\n            (deltaOffset, false)\n          }\n        } catch {\n          // Parsing legacy Offset JSON using DeltaSourceOffset\n          // yields a null reservoirId, causing an exception\n          // since toDeltaSourceOffset expects it to match tableId.\n          case e: Exception =>\n            val autoResolve = sqlConf.getConf(\n              DeltaSQLConf.DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT)\n            if (!autoResolve) {\n              throw e\n            }\n            logInfo(s\"Offset JSON not valid Delta format, parsing as legacy: ${e.getMessage}\")\n            (toDeltaSourceOffsetFromLegacyJson(offset), true)\n        }\n    }\n  }\n\n  /** Parse the given offset as DeltaSharingSourceOffset (tableId/tableVersion)\n   * and convert to DeltaSourceOffset.\n   */\n  private def toDeltaSourceOffsetFromLegacyJson(offset: streaming.Offset): DeltaSourceOffset = {\n    val legacy = DeltaSharingSourceOffset(tableId, offset)\n    convertDeltaSharingSourceOffsetToDeltaSourceOffset(legacy)\n  }\n\n  private def convertDeltaSharingSourceOffsetToDeltaSourceOffset(\n      o: DeltaSharingSourceOffset): DeltaSourceOffset = {\n    // Legacy DeltaSharingSourceOffset hardcodes -1 as the\n    // version boundary index, but DeltaSourceOffset uses\n    // BASE_INDEX (which varies across versions) and rejects\n    // -1. Convert the index to BASE_INDEX for compatibility.\n    val index = if (o.index == -1L) DeltaSourceOffset.BASE_INDEX else o.index\n    logInfo(s\"Converted DeltaSharingSourceOffset to DeltaSourceOffset: reservoirId=${o.tableId}, \" +\n      s\"reservoirVersion=${o.tableVersion}, index=$index, isInitialSnapshot=${o.isStartingVersion}\")\n    DeltaSourceOffset(\n      reservoirId = o.tableId,\n      reservoirVersion = o.tableVersion,\n      index = index,\n      isInitialSnapshot = o.isStartingVersion\n    )\n  }\n\n  /**\n   * The ending version used in rpc is restricted by both the latest table version and\n   * maxVersionsPerRpc, to avoid loading too many files from the server to cause a timeout.\n   * @param startingOffset The start offset used in the rpc.\n   * @param latestTableVersion The latest table version at the server.\n   * @return the ending version used in the rpc.\n   */\n  private def getEndingVersionForRpc(\n      startingOffset: DeltaSourceOffset,\n      latestTableVersion: Long): Long = {\n    if (startingOffset.isInitialSnapshot) {\n      // ending version is the same as starting version for snapshot query.\n      return startingOffset.reservoirVersion\n    }\n    // using \"startVersion + maxVersionsPerRpc - 1\" because the endingVersion is inclusive.\n    val endingVersionForQuery = latestTableVersion.min(\n      startingOffset.reservoirVersion + maxVersionsPerRpc - 1\n    )\n    if (endingVersionForQuery < latestTableVersion) {\n      logInfo(\n        s\"Reducing ending version for delta sharing rpc from latestTableVersion(\" +\n        s\"$latestTableVersion) to endingVersionForQuery($endingVersionForQuery), \" +\n        s\"startVersion:${startingOffset.reservoirVersion}, maxVersionsPerRpc:$maxVersionsPerRpc, \" +\n          getTableInfoForLogging\n      )\n    }\n    endingVersionForQuery\n  }\n\n  override def getDefaultReadLimit: ReadLimit = {\n    deltaSource.getDefaultReadLimit\n  }\n\n  override def latestOffset(startOffset: streaming.Offset, limit: ReadLimit): streaming.Offset = {\n    logInfo(s\"latestOffset with startOffset($startOffset), limit($limit)\")\n    // startOffset is null for initialSnapshot.\n    val (startDeltaSourceOffsetOpt, wasConvertedFromLegacy) =\n      Option(startOffset).map(forceToDeltaSourceOffset) match {\n        case Some((offset, fromLegacy)) => (Some(offset), fromLegacy)\n        case None => (None, false)\n      }\n    // When DVs are enabled on a shared table with an existing\n    // LegacySource streaming query, the query fails. On restart\n    // this Source is freshly instantiated so\n    // latestProcessedEndOffsetOption is None. We must use the\n    // legacy offset as the starting point to fetch files.\n    val deltaSourceOffset =\n      if (latestProcessedEndOffsetOption.isEmpty &&\n        startDeltaSourceOffsetOpt.nonEmpty && wasConvertedFromLegacy) {\n        startDeltaSourceOffsetOpt.get\n      } else {\n        getStartingOffset(latestProcessedEndOffsetOption, None)\n      }\n\n    if (deltaSourceOffset.reservoirVersion < 0) {\n      return null\n    }\n\n    maybeGetLatestFileChangesFromServer(deltaSourceOffset)\n\n    maybeMoveToNextVersion(deltaSource.latestOffset(startDeltaSourceOffsetOpt.orNull, limit))\n  }\n\n  // Advance the DeltaSourceOffset to the next version when the offset is at the last index of the\n  // version.\n  // This is because DeltaSource is not advancing the offset automatically when processing a\n  // snapshot (isStartingVersion = true), and advancing the offset is necessary for delta sharing\n  // streaming to fetch new files from the delta sharing server.\n  private def maybeMoveToNextVersion(\n      latestOffsetFromDeltaSource: streaming.Offset): DeltaSourceOffset = {\n    val deltaLatestOffset = deltaSource.toDeltaSourceOffset(latestOffsetFromDeltaSource)\n    if (deltaLatestOffset.isInitialSnapshot &&\n      (numFileActionsInStartingSnapshotOpt.exists(_ == deltaLatestOffset.index + 1))) {\n      DeltaSourceOffset(\n        reservoirId = deltaLatestOffset.reservoirId,\n        reservoirVersion = deltaLatestOffset.reservoirVersion + 1,\n        index = DeltaSourceOffset.BASE_INDEX,\n        isInitialSnapshot = false\n      )\n    } else {\n      deltaLatestOffset\n    }\n  }\n\n  /**\n   * Whether need to fetch new files from the delta sharing server.\n   * @param startingOffset  the startingOffset of the next batch asked by spark streaming engine.\n   * @param latestTableVersion  the latest table version on the delta sharing server.\n   * @return whether need to fetch new files from the delta sharing server, this is needed when all\n   *         files are processed in the local delta log, and there are new files on the delta\n   *         sharing server.\n   *         And we avoid fetching new files when files in the delta log are not fully processed.\n   */\n  private def needNewFilesFromServer(\n      startingOffset: DeltaSourceOffset,\n      latestTableVersion: Long): Boolean = {\n    if (latestTableVersionInLocalDeltaLogOpt.isEmpty) {\n      return true\n    }\n\n    val allLocalFilesProcessed = latestTableVersionInLocalDeltaLogOpt.exists(\n      _ < startingOffset.reservoirVersion\n    )\n    val newChangesOnServer = latestTableVersionInLocalDeltaLogOpt.exists(_ < latestTableVersion)\n    allLocalFilesProcessed && newChangesOnServer\n  }\n\n  /**\n   * Check whether we need to fetch new files from the server and calls getTableFileChanges if true.\n   *\n   * @param startingOffset the starting offset used to fetch files, the 3 parameters will be useful:\n   *                       - reservoirVersion: initially would be the startingVersion or the latest\n   *                         table version.\n   *                       - index: index of a file within the same version.\n   *                       - isInitialSnapshot: If true, will load fromVersion as a table snapshot(\n   *                         including files from previous versions). If false, will only load files\n   *                         since fromVersion.\n   *                       2 usages: 1) used to compare with latestTableVersionInLocalDeltaLogOpt to\n   *                       check whether new files are needed. 2) used for getTableFileChanges,\n   *                       check more details in the function header.\n   */\n  private def maybeGetLatestFileChangesFromServer(startingOffset: DeltaSourceOffset): Unit = {\n    // Use a local variable to avoid a difference in the two usages below.\n    val latestTableVersion = getOrUpdateLatestTableVersion\n\n    if (needNewFilesFromServer(startingOffset, latestTableVersion)) {\n      val endingVersionForQuery =\n        getEndingVersionForRpc(startingOffset, latestTableVersion)\n\n      if (startingOffset.isInitialSnapshot || !options.readChangeFeed) {\n        getTableFileChanges(startingOffset, endingVersionForQuery)\n      } else {\n        throw new UnsupportedOperationException(\"CDF Streaming is not supported yet.\")\n      }\n    }\n  }\n\n  /**\n   * Fetch the table changes from delta sharing server starting from (version, index) of the\n   * startingOffset, and store them in locally constructed delta log.\n   *\n   * @param startingOffset  Includes a reservoirVersion, an index of a file within the same version,\n   *                        and an isInitialSnapshot.\n   *                        If isInitialSnapshot is true, will load startingOffset.reservoirVersion\n   *                        as a table snapshot (including files from previous versions). If false,\n   *                        it will only load files since startingOffset.reservoirVersion.\n   * @param endingVersionForQuery The ending version used for the query, always smaller than\n   *                              the latest table version on server.\n   */\n  private def getTableFileChanges(\n      startingOffset: DeltaSourceOffset,\n      endingVersionForQuery: Long): Unit = {\n    logInfo(\n      s\"Fetching files with table version(${startingOffset.reservoirVersion}), \" +\n      s\"index(${startingOffset.index}), isInitialSnapshot(${startingOffset.isInitialSnapshot}),\" +\n      s\" endingVersionForQuery($endingVersionForQuery), server version\" +\n      s\"($latestTableVersionOnServer),\" + getTableInfoForLogging\n    )\n\n    val (tableFiles, refreshFunc) = if (startingOffset.isInitialSnapshot) {\n      // If isInitialSnapshot is true, it means to fetch the snapshot at the fromVersion, which may\n      // include table changes from previous versions.\n      val tableFiles = client.getFiles(\n        table = table,\n        predicates = Nil,\n        limit = None,\n        versionAsOf = Some(startingOffset.reservoirVersion),\n        timestampAsOf = None,\n        jsonPredicateHints = None,\n        refreshToken = None,\n        fileIdHash = None\n      )\n      val refreshFunc = DeltaSharingUtils.getRefresherForGetFiles(\n        client = client,\n        table = table,\n        predicates = Nil,\n        limit = None,\n        versionAsOf = Some(startingOffset.reservoirVersion),\n        timestampAsOf = None,\n        jsonPredicateHints = None,\n        useRefreshToken = false\n      )\n      logInfo(\n        s\"Fetched ${tableFiles.lines.size} lines for table version ${tableFiles.version} from\" +\n        \" delta sharing server.\" + getTableInfoForLogging + getQueryIdForLogging\n      )\n      (tableFiles, refreshFunc)\n    } else {\n      // If isStartingVersion is false, it means to fetch files for data changes since fromVersion,\n      // not including files from previous versions.\n      val tableFiles = client.getFiles(\n        table = table,\n        startingVersion = startingOffset.reservoirVersion,\n        endingVersion = Some(endingVersionForQuery),\n        fileIdHash = None\n      )\n      val refreshFunc = DeltaSharingUtils.getRefresherForGetFilesWithStartingVersion(\n        client = client,\n        table = table,\n        startingVersion = startingOffset.reservoirVersion,\n        endingVersion = Some(endingVersionForQuery)\n      )\n      logInfo(\n        s\"Fetched ${tableFiles.lines.size} lines from startingVersion \" +\n        s\"${startingOffset.reservoirVersion} to enedingVersion ${endingVersionForQuery} from \" +\n        \"delta sharing server,\" + getTableInfoForLogging + getQueryIdForLogging\n      )\n      (tableFiles, refreshFunc)\n    }\n\n    val deltaLogMetadata = DeltaSharingLogFileSystem.constructLocalDeltaLogAcrossVersions(\n      lines = tableFiles.lines,\n      customTablePath = customTablePathWithUUIDSuffix,\n      startingVersionOpt = Some(startingOffset.reservoirVersion),\n      endingVersionOpt = Some(endingVersionForQuery)\n    )\n    assert(\n      deltaLogMetadata.maxVersion > 0,\n      s\"Invalid table version in delta sharing response: ${tableFiles.lines}.\"\n    )\n    latestTableVersionInLocalDeltaLogOpt = Some(deltaLogMetadata.maxVersion)\n    logInfo(s\"Setting latestTableVersionInLocalDeltaLogOpt to ${deltaLogMetadata.maxVersion}\" +\n      getTableInfoForLogging)\n    assert(\n      deltaLogMetadata.numFileActionsInMinVersionOpt.isDefined,\n      \"numFileActionsInMinVersionOpt missing after constructed delta log.\"\n    )\n    if (startingOffset.isInitialSnapshot) {\n      numFileActionsInStartingSnapshotOpt = deltaLogMetadata.numFileActionsInMinVersionOpt\n    }\n\n    CachedTableManager.INSTANCE.register(\n      tablePath = DeltaSharingUtils.getTablePathWithIdSuffix(tablePath, timestampWithUUID),\n      idToUrl = deltaLogMetadata.idToUrl,\n      refs = Seq(new WeakReference(this)),\n      profileProvider = client.getProfileProvider,\n      refresher = refreshFunc,\n      expirationTimestamp =\n        if (CachedTableManager.INSTANCE\n            .isValidUrlExpirationTime(deltaLogMetadata.minUrlExpirationTimestamp)) {\n          deltaLogMetadata.minUrlExpirationTimestamp.get\n        } else {\n          System.currentTimeMillis() + CachedTableManager.INSTANCE.preSignedUrlExpirationMs\n        },\n      refreshToken = tableFiles.refreshToken\n    )\n  }\n\n  override def getBatch(startOffsetOption: Option[Offset], end: Offset): DataFrame = {\n    logInfo(s\"getBatch with startOffsetOption($startOffsetOption) and end($end),\" +\n      getTableInfoForLogging)\n    // When DVs are enabled on a shared table with an existing\n    // LegacySource streaming query still reading an initial\n    // snapshot, startOffsetOption is None and endOffset\n    // specifies the starting version. On restart, this Source\n    // is instantiated, so we convert legacy offsets to\n    // DeltaSourceOffset.\n    val endOffset = forceToDeltaSourceOffset(end)._1\n\n    // When the query is past the initial snapshot,\n    // startOffsetOption is defined and contains the starting\n    // version. Convert from legacy offset if needed.\n    val startDeltaOffsetOption =\n      startOffsetOption.map(o => forceToDeltaSourceOffset(o)._1)\n    val startingOffset = getStartingOffset(startDeltaOffsetOption, Some(endOffset))\n\n    // Files should already be fetched in latestOffset; this\n    // is a safeguard in case files for startOffset are not\n    // present. Whether startOffset was converted from legacy\n    // does not matter here.\n    maybeGetLatestFileChangesFromServer(startingOffset = startingOffset)\n    // Reset latestProcessedEndOffsetOption only when endOffset is larger.\n    // Because with microbatch pipelining, we may get getBatch requests out of order.\n    if (latestProcessedEndOffsetOption.isEmpty ||\n      endOffset.reservoirVersion > latestProcessedEndOffsetOption.get.reservoirVersion ||\n      (endOffset.reservoirVersion == latestProcessedEndOffsetOption.get.reservoirVersion &&\n      endOffset.index > latestProcessedEndOffsetOption.get.index)) {\n      latestProcessedEndOffsetOption = Some(endOffset)\n      logInfo(s\"Setting latestProcessedEndOffsetOption to $endOffset,\" + getTableInfoForLogging)\n    }\n\n    deltaSource.getBatch(startDeltaOffsetOption, endOffset)\n  }\n\n  override def getOffset: Option[Offset] = {\n    throw new UnsupportedOperationException(\n      \"latestOffset(Offset, ReadLimit) should be called instead of this method.\"\n    )\n  }\n\n  /**\n   * Extracts whether users provided the option to time travel a relation. If a query restarts from\n   * a checkpoint and the checkpoint has recorded the offset, this method should never been called.\n   */\n  private lazy val getStartingVersion: Option[Long] = {\n\n    /** DeltaOption validates input and ensures that only one is provided. */\n    if (options.startingVersion.isDefined) {\n      val v = options.startingVersion.get match {\n        case StartingVersionLatest =>\n          getOrUpdateLatestTableVersion + 1\n        case StartingVersion(version) =>\n          version\n      }\n      Some(v)\n    } else if (options.startingTimestamp.isDefined) {\n      Some(client.getTableVersion(table, options.startingTimestamp))\n    } else {\n      None\n    }\n  }\n\n  override def stop(): Unit = {\n    deltaSource.stop()\n\n    DeltaSharingLogFileSystem.tryToCleanUpDeltaLog(deltaLogPath)\n  }\n\n  // Calls deltaSource.commit for checks related to column mapping.\n  override def commit(end: Offset): Unit = {\n    logInfo(s\"Commit end offset: $end,\" + getTableInfoForLogging)\n    val endOffset = forceToDeltaSourceOffset(end)._1\n    // If DeltaSource detects a metadata change at endOffset\n    // version, deltaSource.commit throws an exception so the\n    // stream restarts from the checkpoint with the new schema.\n    deltaSource.commit(endOffset)\n\n    // Clean up processed versions in block manager regardless\n    // of whether endOffset is from legacy format.\n    DeltaSharingLogFileSystem.tryToCleanUpPreviousBlocks(\n      deltaLogPath,\n      endOffset.reservoirVersion - 1\n    )\n  }\n\n  override def toString(): String = s\"DeltaFormatSharingSource[${table.toString}]\"\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaSharingCDFUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.lang.ref.WeakReference\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport com.google.common.hash.Hashing\nimport io.delta.sharing.client.DeltaSharingClient\nimport io.delta.sharing.client.model.{Table => DeltaSharingTable}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.delta.sharing.CachedTableManager\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.SQLContext\nimport org.apache.spark.sql.sources.BaseRelation\n\nobject DeltaSharingCDFUtils extends Logging {\n\n  private def getDuration(start: Long): Double = {\n    (System.currentTimeMillis() - start) / 1000.0\n  }\n\n  /**\n   * Prepares the BaseRelation for cdf queries on a delta sharing table. Since there's no limit\n   * pushdown or filter pushdown involved, it wiill firatly fetch all the files from the delta\n   * sharing server, prepare the local delta log, and leverage DeltaTableV2 to produce the relation.\n   */\n  private[sharing] def prepareCDFRelation(\n      sqlContext: SQLContext,\n      options: DeltaSharingOptions,\n      table: DeltaSharingTable,\n      client: DeltaSharingClient): BaseRelation = {\n    val startTime = System.currentTimeMillis()\n    // 1. Get all files with DeltaSharingClient.\n    // includeHistoricalMetadata is always set to true, to get the metadata at the startingVersion\n    // and also any metadata changes between [startingVersion, endingVersion], to put them in the\n    // delta log. This is to allow delta library to check the metadata change and handle it\n    // properly -- currently it throws error for column mapping changes.\n    val deltaTableFiles = client.getCDFFiles(\n      table, options.cdfOptions, includeHistoricalMetadata = true, fileIdHash = None\n    )\n    logInfo(\n      s\"Fetched ${deltaTableFiles.lines.size} lines with cdf options ${options.cdfOptions} \" +\n      s\"for table ${table} from delta sharing server, took ${getDuration(startTime)}s.\"\n    )\n\n    val path = options.options.getOrElse(\"path\", throw DeltaSharingErrors.pathNotSpecifiedException)\n    // 2. Prepare local delta log\n    val queryCustomTablePath = client.getProfileProvider.getCustomTablePath(path)\n    val queryParamsHashId = DeltaSharingUtils.getQueryParamsHashId(options.cdfOptions)\n    val tablePathWithHashIdSuffix =\n      DeltaSharingUtils.getTablePathWithIdSuffix(queryCustomTablePath, queryParamsHashId)\n    val deltaLogMetadata = DeltaSharingLogFileSystem.constructLocalDeltaLogAcrossVersions(\n      lines = deltaTableFiles.lines,\n      customTablePath = tablePathWithHashIdSuffix,\n      startingVersionOpt = None,\n      endingVersionOpt = None\n    )\n\n    // 3. Register parquet file id to url mapping\n    CachedTableManager.INSTANCE.register(\n      // Using path instead of queryCustomTablePath because it will be customized within\n      // CachedTableManager.\n      tablePath = DeltaSharingUtils.getTablePathWithIdSuffix(path, queryParamsHashId),\n      idToUrl = deltaLogMetadata.idToUrl,\n      // A weak reference is needed by the CachedTableManager to decide whether the query is done\n      // and it's ok to clean up the id to url mapping for this table.\n      refs = Seq(new WeakReference(this)),\n      profileProvider = client.getProfileProvider,\n      refresher = DeltaSharingUtils.getRefresherForGetCDFFiles(\n        client = client,\n        table = table,\n        cdfOptions = options.cdfOptions\n      ),\n      expirationTimestamp =\n        if (CachedTableManager.INSTANCE\n            .isValidUrlExpirationTime(deltaLogMetadata.minUrlExpirationTimestamp)) {\n          deltaLogMetadata.minUrlExpirationTimestamp.get\n        } else {\n          System.currentTimeMillis() + CachedTableManager.INSTANCE.preSignedUrlExpirationMs\n        },\n      refreshToken = None\n    )\n\n    // 4. return Delta\n    val localDeltaCdfOptions = Map(\n      DeltaSharingOptions.CDF_START_VERSION -> deltaLogMetadata.minVersion.toString,\n      DeltaSharingOptions.CDF_END_VERSION -> deltaLogMetadata.maxVersion.toString,\n      DeltaSharingOptions.CDF_READ_OPTION -> \"true\"\n    )\n    DeltaTableV2(\n      spark = sqlContext.sparkSession,\n      path = DeltaSharingLogFileSystem.encode(tablePathWithHashIdSuffix),\n      options = localDeltaCdfOptions\n    ).toBaseRelation\n  }\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaSharingDataSource.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{\n  DeltaErrors,\n  DeltaTableUtils => TahoeDeltaTableUtils\n}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.{DeltaDataSource, DeltaSQLConf}\nimport io.delta.sharing.client.{DeltaSharingClient, DeltaSharingRestClient, ParsedDeltaSharingTablePath}\nimport io.delta.sharing.client.model.{DeltaTableMetadata, Table => DeltaSharingTable}\nimport io.delta.sharing.client.util.{ConfUtils, JsonUtils}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.delta.sharing.PreSignedUrlCache\nimport org.apache.spark.sql.{SparkSession, SQLContext}\nimport org.apache.spark.sql.execution.datasources.HadoopFsRelation\nimport org.apache.spark.sql.execution.streaming.Source\nimport org.apache.spark.sql.sources.{\n  BaseRelation,\n  DataSourceRegister,\n  RelationProvider,\n  StreamSourceProvider\n}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A DataSource for Delta Sharing, used to support all types of queries on a delta sharing table:\n * batch, cdf, streaming, time travel, filters, etc.\n */\nprivate[sharing] class DeltaSharingDataSource\n    extends RelationProvider\n    with StreamSourceProvider\n    with DataSourceRegister\n    with DeltaLogging {\n\n  override def sourceSchema(\n      sqlContext: SQLContext,\n      schema: Option[StructType],\n      providerName: String,\n      parameters: Map[String, String]): (String, StructType) = {\n    DeltaSharingDataSource.setupFileSystem(sqlContext)\n    if (schema.nonEmpty && schema.get.nonEmpty) {\n      throw DeltaErrors.specifySchemaAtReadTimeException\n    }\n    val options = new DeltaSharingOptions(parameters)\n    if (options.isTimeTravel) {\n      throw DeltaErrors.timeTravelNotSupportedException\n    }\n    val path = options.options.getOrElse(\"path\", throw DeltaSharingErrors.pathNotSpecifiedException)\n\n    val (responseFormat, parsedPath, deltaTableMetadataOpt) =\n      autoResolveStreamingSource(sqlContext, path, options)\n\n    if (responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_PARQUET) {\n      logInfo(s\"sourceSchema with parquet format for table path:$path, parameters:$parameters\")\n      val deltaLog = RemoteDeltaLog(\n        path,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = true,\n        responseFormat = DeltaSharingOptions.RESPONSE_FORMAT_PARQUET,\n        callerOrg = options.callerOrg\n      )\n      val schemaToUse = deltaLog.snapshot().schema\n      if (schemaToUse.isEmpty) {\n        throw DeltaSharingErrors.schemaNotSetException\n      }\n\n      if (options.readChangeFeed) {\n        (shortName(), DeltaTableUtils.addCdcSchema(schemaToUse))\n      } else {\n        (shortName(), schemaToUse)\n      }\n    } else if (responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_DELTA) {\n      logInfo(s\"sourceSchema with delta format for table path:$path, parameters:$parameters\")\n      if (options.readChangeFeed) {\n        throw new UnsupportedOperationException(\n          s\"Delta sharing cdc streaming is not supported when responseformat=delta.\"\n        )\n      }\n      //  1. create delta sharing client\n      val client = DeltaSharingRestClient(\n        profileFile = parsedPath.profileFile,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = true,\n        responseFormat = DeltaSharingOptions.RESPONSE_FORMAT_DELTA,\n        // comma separated delta reader features, used to tell delta sharing server what delta\n        // reader features the client is able to process.\n        readerFeatures = DeltaSharingUtils.STREAMING_SUPPORTED_READER_FEATURES.mkString(\",\"),\n        callerOrg = options.callerOrg\n      )\n      val dsTable = DeltaSharingTable(\n        share = parsedPath.share,\n        schema = parsedPath.schema,\n        name = parsedPath.table\n      )\n\n      //  2. getMetadata for schema to be used in the file index.\n      val deltaSharingTableMetadata = deltaTableMetadataOpt match {\n        case Some(metadata) =>\n          DeltaSharingUtils.getDeltaSharingTableMetadata(\n            table = dsTable, deltaTableMetadata = metadata)\n        case None =>\n          DeltaSharingUtils.getDeltaSharingTableMetadata(client = client, table = dsTable)\n      }\n      val customTablePathWithUUIDSuffix = DeltaSharingUtils.getTablePathWithIdSuffix(\n        client.getProfileProvider.getCustomTablePath(path),\n        DeltaSharingUtils.getFormattedTimestampWithUUID()\n      )\n      val deltaLogPath =\n        s\"${DeltaSharingLogFileSystem.encode(customTablePathWithUUIDSuffix).toString}/_delta_log\"\n      val (_, snapshotDescriptor) = DeltaSharingUtils.getDeltaLogAndSnapshotDescriptor(\n        sqlContext.sparkSession,\n        deltaSharingTableMetadata,\n        customTablePathWithUUIDSuffix\n      )\n\n      // This is the analyzed schema for Delta streaming\n      val readSchema = {\n        // Check if we would like to merge consecutive schema changes, this would allow customers\n        // to write queries based on their latest changes instead of an arbitrary schema in the\n        // past.\n        val shouldMergeConsecutiveSchemas = sqlContext.sparkSession.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING_MERGE_CONSECUTIVE_CHANGES\n        )\n\n        // This method is invoked during the analysis phase and would determine the schema for the\n        // streaming dataframe. We only need to merge consecutive schema changes here because the\n        // process would create a new entry in the schema log such that when the schema log is\n        // looked up again in the execution phase, we would use the correct schema.\n        // Delta sharing delta log doesn't have a catalog table, so we pass None here.\n        DeltaDataSource\n          .getMetadataTrackingLogForDeltaSource(\n            sqlContext.sparkSession,\n            snapshotDescriptor,\n            catalogTableOpt = None,\n            parameters,\n            mergeConsecutiveSchemaChanges = shouldMergeConsecutiveSchemas\n          )\n          .flatMap(_.getCurrentTrackedMetadata.map(_.dataSchema))\n          .getOrElse(snapshotDescriptor.schema)\n      }\n\n      val schemaToUse = TahoeDeltaTableUtils.removeInternalWriterMetadata(\n        sqlContext.sparkSession,\n        readSchema\n      )\n      if (schemaToUse.isEmpty) {\n        throw DeltaErrors.schemaNotSetException\n      }\n\n      DeltaSharingLogFileSystem.tryToCleanUpDeltaLog(deltaLogPath)\n      (shortName(), schemaToUse)\n    } else {\n      throw new UnsupportedOperationException(\n        s\"responseformat(${responseFormat}) is not \" +\n        s\"supported in delta sharing.\"\n      )\n    }\n  }\n\n  override def createSource(\n      sqlContext: SQLContext,\n      metadataPath: String,\n      schema: Option[StructType],\n      providerName: String,\n      parameters: Map[String, String]): Source = {\n    DeltaSharingDataSource.setupFileSystem(sqlContext)\n    if (schema.nonEmpty && schema.get.nonEmpty) {\n      throw DeltaSharingErrors.specifySchemaAtReadTimeException\n    }\n    val options = new DeltaSharingOptions(parameters)\n    val path = options.options.getOrElse(\"path\", throw DeltaSharingErrors.pathNotSpecifiedException)\n\n    val (responseFormat, parsedPath, _) =\n      autoResolveStreamingSource(sqlContext, path, options)\n\n    if (responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_PARQUET) {\n      logInfo(s\"createSource with parquet format for table path:$path, parameters:$parameters\")\n      val deltaLog = RemoteDeltaLog(\n        path,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = true,\n        responseFormat = DeltaSharingOptions.RESPONSE_FORMAT_PARQUET,\n        callerOrg = options.callerOrg\n      )\n      DeltaSharingSource(SparkSession.active, deltaLog, options)\n    } else if (responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_DELTA) {\n      logInfo(s\"createSource with delta format for table path:$path, parameters:$parameters\")\n      if (options.readChangeFeed) {\n        throw new UnsupportedOperationException(\n          s\"Delta sharing cdc streaming is not supported when responseformat=delta.\"\n        )\n      }\n      //  1. create delta sharing client\n      val client = DeltaSharingRestClient(\n        profileFile = parsedPath.profileFile,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = true,\n        responseFormat = DeltaSharingOptions.RESPONSE_FORMAT_DELTA,\n        // comma separated delta reader features, used to tell delta sharing server what delta\n        // reader features the client is able to process.\n        readerFeatures = DeltaSharingUtils.STREAMING_SUPPORTED_READER_FEATURES.mkString(\",\"),\n        callerOrg = options.callerOrg\n      )\n      val dsTable = DeltaSharingTable(\n        share = parsedPath.share,\n        schema = parsedPath.schema,\n        name = parsedPath.table\n      )\n\n      DeltaFormatSharingSource(\n        spark = sqlContext.sparkSession,\n        client = client,\n        table = dsTable,\n        options = options,\n        parameters = parameters,\n        sqlConf = sqlContext.sparkSession.sessionState.conf,\n        metadataPath = metadataPath\n      )\n    } else {\n      throw new UnsupportedOperationException(\n        s\"responseformat(${responseFormat}) is not \" +\n        s\"supported in delta sharing.\"\n      )\n    }\n  }\n\n  /**\n   * Resolves the response format for streaming: when\n   * DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT is true, calls getMetadata on the\n   * table and uses the server's responded format; otherwise uses the user's responseFormat option.\n   * Returns (responseFormat, parsedPath, deltaTableMetadataOpt). When the conf is on,\n   * deltaTableMetadataOpt is Some; sourceSchema's delta path reuses it to avoid a second RPC.\n   */\n  private def autoResolveStreamingSource(\n      sqlContext: SQLContext,\n      path: String,\n      options: DeltaSharingOptions\n  ): (String, ParsedDeltaSharingTablePath, Option[DeltaTableMetadata]) = {\n    val useGetMetadata = sqlContext.sparkSession.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT)\n    val parsedPath =\n      DeltaSharingRestClient.parsePath(path, options.shareCredentialsOptions)\n\n    if (!useGetMetadata) {\n      (options.responseFormat, parsedPath, None)\n    } else if (options.responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_DELTA) {\n      // User explicitly requested delta; no need to call getMetadata.\n      // Delta format can handle both parquet tables and delta tables.\n      // Parquet format can only handle parquet tables.\n      (DeltaSharingOptions.RESPONSE_FORMAT_DELTA, parsedPath, None)\n    } else {\n      val (_, deltaTableMetadata) = createClientAndQueryMetadata(\n        sqlContext = sqlContext,\n        parsedPath = parsedPath,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = true,\n        versionAsOf = None,\n        timestampAsOf = None,\n        callerOrg = options.callerOrg\n      )\n      logInfo(s\"Streaming format resolved via getMetadata: ${deltaTableMetadata.respondedFormat} \" +\n        s\"for path:$path\")\n      (deltaTableMetadata.respondedFormat, parsedPath, Some(deltaTableMetadata))\n    }\n  }\n\n  /**\n   * Creates a Delta Sharing client (accepting parquet and/or delta per conf), a DeltaSharingTable,\n   * and queries getMetadata. Used by streaming auto-resolve and batch auto-resolve.\n   * Returns (dsTable, deltaTableMetadata); the client is not returned as callers either discard it\n   * or create a format-specific client when needed.\n   */\n  private def createClientAndQueryMetadata(\n      sqlContext: SQLContext,\n      parsedPath: ParsedDeltaSharingTablePath,\n      shareCredentialsOptions: Map[String, String],\n      forStreaming: Boolean,\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String],\n      callerOrg: Option[String] = None): (DeltaSharingTable, DeltaTableMetadata) = {\n    val responseFormat = {\n      if (sqlContext.sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_SHARING_FORCE_DELTA_FORMAT)) {\n        // If the Spark config is enabled, force the query to return results in Delta format.\n        // This is primarily used for testing the Delta format code path, even when the source\n        // table doesn't include advanced features like deletion vector.\n        logInfo(\"Set delta sharing client to only accept delta format due to Spark config setting.\")\n        DeltaSharingOptions.RESPONSE_FORMAT_DELTA\n      } else {\n        s\"${DeltaSharingOptions.RESPONSE_FORMAT_PARQUET},\" +\n          s\"${DeltaSharingOptions.RESPONSE_FORMAT_DELTA}\"\n      }\n    }\n    // comma separated delta reader features, used to tell delta sharing server what delta\n    // reader features the client is able to process.\n    val readerFeatures = if (forStreaming) {\n      DeltaSharingUtils.STREAMING_SUPPORTED_READER_FEATURES.mkString(\",\")\n    } else {\n      DeltaSharingUtils.SUPPORTED_READER_FEATURES.mkString(\",\")\n    }\n    val client = DeltaSharingRestClient(\n      profileFile = parsedPath.profileFile,\n      shareCredentialsOptions = shareCredentialsOptions,\n      forStreaming = forStreaming,\n      // Indicating that the client is able to process response format in both parquet and delta.\n      responseFormat = responseFormat,\n      readerFeatures = readerFeatures,\n      callerOrg = callerOrg\n    )\n    val dsTable = DeltaSharingTable(\n      share = parsedPath.share,\n      schema = parsedPath.schema,\n      name = parsedPath.table\n    )\n    val deltaTableMetadata = DeltaSharingUtils.queryDeltaTableMetadata(\n      client = client,\n      table = dsTable,\n      versionAsOf = versionAsOf,\n      timestampAsOf = timestampAsOf\n    )\n    (dsTable, deltaTableMetadata)\n  }\n\n  override def createRelation(\n      sqlContext: SQLContext,\n      parameters: Map[String, String]): BaseRelation = {\n    DeltaSharingDataSource.setupFileSystem(sqlContext)\n    val options = new DeltaSharingOptions(parameters)\n\n    val userInputResponseFormat = options.options.get(DeltaSharingOptions.RESPONSE_FORMAT)\n    if (userInputResponseFormat.isEmpty && !options.readChangeFeed) {\n      return autoResolveBaseRelationForSnapshotQuery(options, sqlContext)\n    }\n\n    val path = options.options.getOrElse(\"path\", throw DeltaSharingErrors.pathNotSpecifiedException)\n    if (options.responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_PARQUET) {\n      // When user explicitly set responseFormat=parquet, to query shared tables without advanced\n      // delta features.\n      logInfo(s\"createRelation with parquet format for table path:$path, parameters:$parameters\")\n      val deltaLog = RemoteDeltaLog(\n        path,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = false,\n        responseFormat = options.responseFormat,\n        callerOrg = options.callerOrg\n      )\n      deltaLog.createRelation(\n        options.versionAsOf,\n        options.timestampAsOf,\n        options.cdfOptions\n      )\n    } else if (options.responseFormat == DeltaSharingOptions.RESPONSE_FORMAT_DELTA) {\n      // When user explicitly set responseFormat=delta, to query shared tables with advanced\n      // delta features.\n      logInfo(s\"createRelation with delta format for table path:$path, parameters:$parameters\")\n      //  1. create delta sharing client\n      val parsedPath =\n        DeltaSharingRestClient.parsePath(path, options.shareCredentialsOptions)\n      val client = DeltaSharingRestClient(\n        profileFile = parsedPath.profileFile,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = false,\n        responseFormat = options.responseFormat,\n        // comma separated delta reader features, used to tell delta sharing server what delta\n        // reader features the client is able to process.\n        readerFeatures = DeltaSharingUtils.SUPPORTED_READER_FEATURES.mkString(\",\"),\n        callerOrg = options.callerOrg\n      )\n      val dsTable = DeltaSharingTable(\n        share = parsedPath.share,\n        schema = parsedPath.schema,\n        name = parsedPath.table\n      )\n\n      if (options.readChangeFeed) {\n        return DeltaSharingCDFUtils.prepareCDFRelation(sqlContext, options, dsTable, client)\n      }\n      //  2. getMetadata for schema to be used in the file index.\n      val deltaTableMetadata = DeltaSharingUtils.queryDeltaTableMetadata(\n        client = client,\n        table = dsTable,\n        versionAsOf = options.versionAsOf,\n        timestampAsOf = options.timestampAsOf\n      )\n      val deltaSharingTableMetadata = DeltaSharingUtils.getDeltaSharingTableMetadata(\n        table = dsTable,\n        deltaTableMetadata = deltaTableMetadata\n      )\n\n      //  3. Prepare HadoopFsRelation\n      getHadoopFsRelationForDeltaSnapshotQuery(\n        path = path,\n        options = options,\n        dsTable = dsTable,\n        client = client,\n        deltaSharingTableMetadata = deltaSharingTableMetadata\n      )\n    } else {\n      throw new UnsupportedOperationException(\n        s\"responseformat(${options.responseFormat}) is not supported in delta sharing.\"\n      )\n    }\n  }\n\n  /**\n   * \"parquet format sharing\" leverages the existing set of remote classes to directly handle the\n   * list of presigned urls and read data.\n   * \"delta format sharing\" instead constructs a local delta log and leverages the delta library to\n   * read data.\n   * Firstly we sends a getMetadata call to the delta sharing server the suggested response format\n   * of the shared table by the server (based on whether there are advanced delta features in the\n   * shared table), and then decide the code path on the client side.\n   */\n  private def autoResolveBaseRelationForSnapshotQuery(\n      options: DeltaSharingOptions,\n      sqlContext: SQLContext): BaseRelation = {\n    val path = options.options.getOrElse(\"path\", throw DeltaSharingErrors.pathNotSpecifiedException)\n    logInfo(s\"autoResolving BaseRelation for path:${path}, \" +\n      s\"with options:${DeltaSharingDataSource.redactOptions(options.options)}.\")\n    val parsedPath =\n      DeltaSharingRestClient.parsePath(path, options.shareCredentialsOptions)\n\n    val (dsTable, deltaTableMetadata) = createClientAndQueryMetadata(\n      sqlContext = sqlContext,\n      parsedPath = parsedPath,\n      shareCredentialsOptions = options.shareCredentialsOptions,\n      forStreaming = false,\n      versionAsOf = options.versionAsOf,\n      timestampAsOf = options.timestampAsOf,\n      callerOrg = options.callerOrg\n    )\n\n    if (deltaTableMetadata.respondedFormat == DeltaSharingOptions.RESPONSE_FORMAT_PARQUET) {\n      logInfo(s\"Resolved as parquet format for table path:$path, \" +\n        s\"parameters:${DeltaSharingDataSource.redactOptions(options.options)}\")\n      val deltaLog = RemoteDeltaLog(\n        path = path,\n        options.shareCredentialsOptions,\n        forStreaming = false,\n        responseFormat = DeltaSharingOptions.RESPONSE_FORMAT_PARQUET,\n        initDeltaTableMetadata = Some(deltaTableMetadata),\n        callerOrg = options.callerOrg\n      )\n      deltaLog.createRelation(options.versionAsOf, options.timestampAsOf, options.cdfOptions)\n    } else if (deltaTableMetadata.respondedFormat == DeltaSharingOptions.RESPONSE_FORMAT_DELTA) {\n      logInfo(s\"Resolved as delta format for table path:$path, \" +\n        s\"parameters:${DeltaSharingDataSource.redactOptions(options.options)}\")\n      val deltaSharingTableMetadata = DeltaSharingUtils.getDeltaSharingTableMetadata(\n        table = dsTable,\n        deltaTableMetadata = deltaTableMetadata\n      )\n      val deltaOnlyClient = DeltaSharingRestClient(\n        profileFile = parsedPath.profileFile,\n        shareCredentialsOptions = options.shareCredentialsOptions,\n        forStreaming = false,\n        // Indicating that the client request delta format in response.\n        responseFormat = DeltaSharingOptions.RESPONSE_FORMAT_DELTA,\n        // comma separated delta reader features, used to tell delta sharing server what delta\n        // reader features the client is able to process.\n        readerFeatures = DeltaSharingUtils.SUPPORTED_READER_FEATURES.mkString(\",\"),\n        callerOrg = options.callerOrg\n      )\n      getHadoopFsRelationForDeltaSnapshotQuery(\n        path = path,\n        options = options,\n        dsTable = dsTable,\n        client = deltaOnlyClient,\n        deltaSharingTableMetadata = deltaSharingTableMetadata\n      )\n    } else {\n      throw new UnsupportedOperationException(\n        s\"Unexpected respondedFormat for getMetadata rpc:${deltaTableMetadata.respondedFormat}.\"\n      )\n    }\n  }\n\n  /**\n   * Prepare a HadoopFsRelation for the snapshot query on a delta sharing table. It will contain a\n   * DeltaSharingFileIndex which is used to handle delta sharing rpc, and construct the local delta\n   * log, and then build a TahoeFileIndex on top of the delta log.\n   */\n  private def getHadoopFsRelationForDeltaSnapshotQuery(\n      path: String,\n      options: DeltaSharingOptions,\n      dsTable: DeltaSharingTable,\n      client: DeltaSharingClient,\n      deltaSharingTableMetadata: DeltaSharingUtils.DeltaSharingTableMetadata): BaseRelation = {\n    // Prepare DeltaSharingFileIndex\n    val spark = SparkSession.active\n    val params = new DeltaSharingFileIndexParams(\n      new Path(path),\n      spark,\n      deltaSharingTableMetadata,\n      options\n    )\n    if (ConfUtils.limitPushdownEnabled(spark.sessionState.conf)) {\n      DeltaFormatSharingLimitPushDown.setup(spark)\n    }\n    // limitHint is always None here and will be overridden in DeltaFormatSharingLimitPushDown.\n    val fileIndex = DeltaSharingFileIndex(\n      params = params,\n      table = dsTable,\n      client = client,\n      limitHint = None\n    )\n\n    //  return HadoopFsRelation with the DeltaSharingFileIndex.\n    HadoopFsRelation(\n      location = fileIndex,\n      // This is copied from DeltaLog.buildHadoopFsRelationWithFileIndex.\n      // Dropping column mapping metadata because it is not relevant for partition schema.\n      partitionSchema = TahoeDeltaTableUtils.removeInternalDeltaMetadata(\n        spark, TahoeDeltaTableUtils.removeInternalWriterMetadata(spark, fileIndex.partitionSchema)\n      ),\n      // This is copied from DeltaLog.buildHadoopFsRelationWithFileIndex, original comment:\n      // We pass all table columns as `dataSchema` so that Spark will preserve the partition\n      // column locations. Otherwise, for any partition columns not in `dataSchema`, Spark would\n      // just append them to the end of `dataSchema`.\n      dataSchema = TahoeDeltaTableUtils.removeInternalDeltaMetadata(\n        spark,\n        TahoeDeltaTableUtils.removeInternalWriterMetadata(\n          spark,\n          SchemaUtils.dropNullTypeColumns(deltaSharingTableMetadata.metadata.schema)\n        )\n      ),\n      bucketSpec = None,\n      // Handle column mapping metadata in schema.\n      fileFormat = fileIndex.fileFormat(\n        deltaSharingTableMetadata.protocol.deltaProtocol,\n        deltaSharingTableMetadata.metadata.deltaMetadata\n      ),\n      options = Map.empty\n    )(spark)\n  }\n\n  override def shortName(): String = \"deltaSharing\"\n}\n\nprivate[sharing] object DeltaSharingDataSource {\n  def setupFileSystem(sqlContext: SQLContext): Unit = {\n    sqlContext.sparkContext.hadoopConfiguration\n      .setIfUnset(\"fs.delta-sharing.impl\", \"io.delta.sharing.client.DeltaSharingFileSystem\")\n    sqlContext.sparkContext.hadoopConfiguration\n      .setIfUnset(\n        \"fs.delta-sharing-log.impl\",\n        \"io.delta.sharing.spark.DeltaSharingLogFileSystem\"\n      )\n    PreSignedUrlCache.registerIfNeeded(SparkEnv.get)\n  }\n\n  def redactOptions(options: Map[String, String]): Map[String, String] = {\n    options.map {\n      case (k, _) if k.equalsIgnoreCase(\"bearerToken\") => (k, \"REDACTED\")\n      case (k, _) if k.equalsIgnoreCase(\"clientId\") => (k, \"REDACTED\")\n      case (k, _) if k.equalsIgnoreCase(\"clientSecret\") => (k, \"REDACTED\")\n      case (k, _) if k.equalsIgnoreCase(\"scope\") => (k, \"REDACTED\")\n      case (k, v) => (k, v)\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaSharingFileIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.lang.ref.WeakReference\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.{DeltaFileFormat, DeltaLog}\nimport org.apache.spark.sql.delta.files.{SupportsRowIndexFilters, TahoeLogFileIndex}\nimport io.delta.sharing.client.DeltaSharingClient\nimport io.delta.sharing.client.model.{Table => DeltaSharingTable}\nimport io.delta.sharing.client.util.{ConfUtils, JsonUtils}\nimport io.delta.sharing.filters.{AndOp, BaseOp, OpConverter}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.delta.sharing.CachedTableManager\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.execution.datasources.{FileIndex, PartitionDirectory}\nimport org.apache.spark.sql.types.StructType\n\nprivate[sharing] case class DeltaSharingFileIndexParams(\n    path: Path,\n    spark: SparkSession,\n    deltaSharingTableMetadata: DeltaSharingUtils.DeltaSharingTableMetadata,\n    options: DeltaSharingOptions)\n\n/**\n * A file index for delta sharing batch queries, that wraps a delta sharing table and client, which\n * is used to issue rpcs to delta sharing server to fetch pre-signed urls, then a local delta log is\n * constructed, and a TahoeFileIndex can be built on top of it.\n */\ncase class DeltaSharingFileIndex(\n    params: DeltaSharingFileIndexParams,\n    table: DeltaSharingTable,\n    client: DeltaSharingClient,\n    limitHint: Option[Long])\n    extends FileIndex\n    with SupportsRowIndexFilters\n    with DeltaFileFormat\n    with Logging {\n  // Use the head of an uuid as fileIndexId, which will be used in the key of the map from a delta\n  // log path to a deltaLog class, to allow different queries with the same parameters on the\n  // same fileIndex to reuse delta log.\n  // Also, the uuid is used to differentiate different fileIndices on the same table in the same\n  // cluster, the head of an uuid should be sufficient given the low collision chance and small\n  // number of classes that could be created in the same computing environment.\n  private val fileIndexId = UUID.randomUUID().toString().split('-').head\n\n  override def spark: SparkSession = params.spark\n\n  override def refresh(): Unit = {}\n\n  override def sizeInBytes: Long =\n    Option(params.deltaSharingTableMetadata.metadata.size).getOrElse {\n      // Throw error if metadata.size is not returned, to urge the server to respond a table size.\n      throw new IllegalStateException(\n        \"size is null in the metadata returned from the delta \" +\n        s\"sharing server: ${params.deltaSharingTableMetadata.metadata}.\"\n      )\n    }\n\n  override def partitionSchema: StructType =\n    params.deltaSharingTableMetadata.metadata.partitionSchema\n\n  // Returns the partition columns of the shared delta table based on the returned metadata.\n  def partitionColumns: Seq[String] =\n    params.deltaSharingTableMetadata.metadata.deltaMetadata.partitionColumns\n\n  override def rootPaths: Seq[Path] = params.path :: Nil\n\n  override def inputFiles: Array[String] = {\n    throw new UnsupportedOperationException(\"DeltaSharingFileIndex.inputFiles\")\n  }\n\n  // A map that from queriedTableQueryId that we've issued delta sharing rpc, to the deltaLog\n  // constructed with the response.\n  // It is because this function will be called twice or more in a spark query, with this set, we\n  // can avoid doing duplicated work of making expensive rpc and constructing the delta log.\n  private val queriedTableQueryIdToDeltaLog = scala.collection.mutable.Map[String, DeltaLog]()\n\n  def fetchFilesAndConstructDeltaLog(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression],\n      overrideLimit: Option[Long]): DeltaLog = {\n    val jsonPredicateHints = convertToJsonPredicate(partitionFilters, dataFilters)\n    val queryParamsHashId = DeltaSharingUtils.getQueryParamsHashId(\n      params.options,\n      // Using .sql instead of toString because it doesn't include class pointer, which\n      // keeps the string the same for the same filters.\n      partitionFilters.map(_.sql).mkString(\";\"),\n      dataFilters.map(_.sql).mkString(\";\"),\n      jsonPredicateHints.getOrElse(\"\"),\n      overrideLimit.map(_.toString).getOrElse(\"\"),\n      params.deltaSharingTableMetadata.version\n    )\n    // listFiles will be called twice or more in a spark query, with this check we can avoid\n    // duplicated work of making expensive rpc and constructing the delta log.\n    val tableKey = DeltaSharingUtils.getTablePathWithIdSuffix(\n      fileIndexId + \".\" + params.path.toString,\n      queryParamsHashId\n    )\n    queriedTableQueryIdToDeltaLog.get(tableKey) match {\n      case Some(deltaLog) =>\n        logInfo(s\"Reusing deltaLog for tableKey:$tableKey.partitionFilters:$partitionFilters,\" +\n          s\"dataFilters:$dataFilters,overrideLimit:$overrideLimit.\")\n        deltaLog\n      case None =>\n        val newDeltaLog = createDeltaLog(\n          jsonPredicateHints,\n          queryParamsHashId,\n          overrideLimit\n        )\n        // In theory there should only be one entry in this set since each query creates its own\n        // FileIndex class. This is purged together with the FileIndex class when the query\n        // finishes.\n        queriedTableQueryIdToDeltaLog.put(tableKey, newDeltaLog)\n        logInfo(s\"Added new deltaLog for tableKey:$tableKey.partitionFilters:$partitionFilters,\" +\n          s\"dataFilters:$dataFilters,overrideLimit:$overrideLimit.\")\n        newDeltaLog\n    }\n  }\n\n  private def createDeltaLog(\n      jsonPredicateHints: Option[String],\n      queryParamsHashId: String,\n      overrideLimit: Option[Long]): DeltaLog = {\n    //  1. Call client.getFiles.\n    val startTime = System.currentTimeMillis()\n    val deltaTableFiles = client.getFiles(\n      table = table,\n      predicates = Nil,\n      limit = overrideLimit.orElse(limitHint),\n      versionAsOf = params.options.versionAsOf,\n      timestampAsOf = params.options.timestampAsOf,\n      jsonPredicateHints = jsonPredicateHints,\n      refreshToken = None,\n      fileIdHash = None\n    )\n    logInfo(\n      s\"Fetched ${deltaTableFiles.lines.size} lines for table $table with version \" +\n      s\"${deltaTableFiles.version} from delta sharing server, took \" +\n      s\"${(System.currentTimeMillis() - startTime) / 1000.0}s.\"\n    )\n\n    // 2. Prepare a DeltaLog.\n    val tablePathWithHashIdSuffix = DeltaSharingUtils.getTablePathWithIdSuffix(\n      client.getProfileProvider.getCustomTablePath(\n        params.path.toString\n      ),\n      queryParamsHashId\n    )\n    val deltaLogMetadata =\n      DeltaSharingLogFileSystem.constructLocalDeltaLogAtVersionZero(\n        deltaTableFiles.lines,\n        tablePathWithHashIdSuffix\n      )\n\n    // 3. Register parquet file id to url mapping\n    CachedTableManager.INSTANCE.register(\n      // Using params.path directly because it will be customized within CachedTableManager.\n      tablePath = DeltaSharingUtils.getTablePathWithIdSuffix(\n        params.path.toString,\n        queryParamsHashId\n      ),\n      idToUrl = deltaLogMetadata.idToUrl,\n      refs = Seq(new WeakReference(this)),\n      profileProvider = client.getProfileProvider,\n      refresher = DeltaSharingUtils.getRefresherForGetFiles(\n        client = client,\n        table = table,\n        predicates = Nil,\n        limit = overrideLimit.orElse(limitHint),\n        versionAsOf = params.options.versionAsOf,\n        timestampAsOf = params.options.timestampAsOf,\n        jsonPredicateHints = jsonPredicateHints,\n        useRefreshToken = true\n      ),\n      expirationTimestamp =\n        if (CachedTableManager.INSTANCE\n            .isValidUrlExpirationTime(deltaLogMetadata.minUrlExpirationTimestamp)) {\n          deltaLogMetadata.minUrlExpirationTimestamp.get\n        } else {\n          System.currentTimeMillis() + CachedTableManager.INSTANCE.preSignedUrlExpirationMs\n        },\n      refreshToken = deltaTableFiles.refreshToken\n    )\n\n    // 4. Create a local file index and call listFiles of this class.\n    val deltaLog = DeltaLog.forTable(\n      params.spark,\n      DeltaSharingLogFileSystem.encode(tablePathWithHashIdSuffix)\n    )\n\n    deltaLog\n  }\n\n  def asTahoeFileIndex(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): TahoeLogFileIndex = {\n    val deltaLog = fetchFilesAndConstructDeltaLog(partitionFilters, dataFilters, None)\n    TahoeLogFileIndex(params.spark, deltaLog, catalogTableOpt = None)\n  }\n\n  override def listFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[PartitionDirectory] = {\n    // NOTE: The server is not required to apply all filters, so we apply them client-side as well.\n    asTahoeFileIndex(partitionFilters, dataFilters).listFiles(partitionFilters, dataFilters)\n  }\n\n  // Converts the specified SQL expressions to a json predicate.\n  //\n  // If jsonPredicatesV2 are enabled, converts both partition and data filters\n  // and combines them using an AND.\n  //\n  // If the conversion fails, returns a None, which will imply that we will\n  // not perform json predicate based filtering.\n  private def convertToJsonPredicate(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Option[String] = {\n    if (!ConfUtils.jsonPredicatesEnabled(params.spark.sessionState.conf)) {\n      return None\n    }\n\n    // Convert the partition filters.\n    val partitionOp = try {\n      OpConverter.convert(partitionFilters)\n    } catch {\n      case e: Exception =>\n        log.error(\"Error while converting partition filters: \" + e)\n        None\n    }\n\n    // If V2 predicates are enabled, also convert the data filters.\n    val dataOp = try {\n      if (ConfUtils.jsonPredicatesV2Enabled(params.spark.sessionState.conf)) {\n        log.info(\"Converting data filters\")\n        OpConverter.convert(dataFilters)\n      } else {\n        None\n      }\n    } catch {\n      case e: Exception =>\n        log.error(\"Error while converting data filters: \" + e)\n        None\n    }\n\n    // Combine partition and data filters using an AND operation.\n    val combinedOp = if (partitionOp.isDefined && dataOp.isDefined) {\n      Some(AndOp(Seq(partitionOp.get, dataOp.get)))\n    } else if (partitionOp.isDefined) {\n      partitionOp\n    } else {\n      dataOp\n    }\n    log.info(\"Using combined predicate: \" + combinedOp)\n\n    if (combinedOp.isDefined) {\n      Some(JsonUtils.toJson[BaseOp](combinedOp.get))\n    } else {\n      None\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaSharingLogFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.io.{ByteArrayInputStream, FileNotFoundException}\nimport java.net.{URI, URLDecoder, URLEncoder}\nimport java.nio.charset.StandardCharsets\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.{ArrayBuffer, Builder}\nimport scala.reflect.ClassTag\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, DeletionVectorDescriptor, RemoveFile, SingleAction}\nimport org.apache.spark.sql.delta.util.FileNames\nimport io.delta.sharing.client.util.JsonUtils\nimport io.delta.sharing.spark.DeltaSharingUtils.{DeltaSharingTableMetadata, FAKE_CHECKPOINT_BYTE_ARRAY}\nimport org.apache.hadoop.fs._\nimport org.apache.hadoop.fs.permission.FsPermission\nimport org.apache.hadoop.util.Progressable\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.storage.BlockId\n\n/** Read-only file system for delta sharing log.\n * This is a faked file system to serve data under path delta-sharing-log:/. The delta log will be\n * prepared by DeltaSharingDataSource and its related classes, put in blockManager, and then serve\n * to DeltaLog with a path pointing to this file system.\n * In executor, when it tries to read data from the delta log, this file system class will return\n * the data fetched from the block manager.\n */\nprivate[sharing] class DeltaSharingLogFileSystem extends FileSystem with Logging {\n  import DeltaSharingLogFileSystem._\n\n  override def getScheme: String = SCHEME\n\n  override def getUri(): URI = URI.create(s\"$SCHEME:///\")\n\n  override def open(f: Path, bufferSize: Int): FSDataInputStream = {\n    if (FileNames.isCheckpointFile(f)) {\n      new FSDataInputStream(\n        new SeekableByteArrayInputStream(DeltaSharingUtils.FAKE_CHECKPOINT_BYTE_ARRAY)\n      )\n    } else if (FileNames.isDeltaFile(f)) {\n      val iterator =\n        SparkEnv.get.blockManager.get[String](getDeltaSharingLogBlockId(f.toString)) match {\n          case Some(block) => block.data.asInstanceOf[Iterator[String]]\n          case _ => throw new FileNotFoundException(s\"Failed to open delta log file: $f.\")\n        }\n      // Explicitly call hasNext to allow the reader lock on the block to be released.\n      val arrayBuilder = Array.newBuilder[Byte]\n      while (iterator.hasNext) {\n        val actionJsonStr = iterator.next()\n        arrayBuilder ++= actionJsonStr.getBytes(StandardCharsets.UTF_8)\n      }\n      // We still have to load the full content of a delta log file in memory to serve them.\n      // This still exposes the risk of OOM.\n      new FSDataInputStream(new SeekableByteArrayInputStream(arrayBuilder.result()))\n    } else {\n      val content = getBlockAndReleaseLockHelper[String](f, None, \"open\")\n      new FSDataInputStream(new SeekableByteArrayInputStream(\n        content.getBytes(StandardCharsets.UTF_8)\n      ))\n    }\n  }\n\n  override def exists(f: Path): Boolean = {\n    // The reason of using the variable exists is to allow us to explicitly release the reader lock\n    // on the blockId.\n    val blockId = getDeltaSharingLogBlockId(f.toString)\n    val exists = SparkEnv.get.blockManager.get(blockId).isDefined\n    if (exists) {\n      releaseLockHelper(blockId)\n    }\n    exists\n  }\n\n  // Delta sharing log file system serves checkpoint file with a CONSTANT value so we construct the\n  // FileStatus when the function is being called.\n  // For other files, they will be constructed and put into block manager when constructing the\n  // delta log based on the rpc response from the server.\n  override def getFileStatus(f: Path): FileStatus = {\n    val status = if (FileNames.isCheckpointFile(f)) {\n      DeltaSharingLogFileStatus(\n        path = f.toString,\n        size = FAKE_CHECKPOINT_BYTE_ARRAY.size,\n        modificationTime = 0L\n      )\n    } else {\n      getBlockAndReleaseLockHelper[DeltaSharingLogFileStatus](f, Some(\"_status\"), \"getFileStatus\")\n    }\n\n    new FileStatus(\n      /* length */ status.size,\n      /* isdir */ false,\n      /* block_replication */ 0,\n      /* blocksize */ 1,\n      /* modification_time */ status.modificationTime,\n      /* path */ new Path(status.path)\n    )\n  }\n\n  /**\n   * @param f: a Path pointing to a delta log directory of a delta sharing table, example:\n   *            delta-sharing-log:/customized-delta-sharing-table/_delta_log\n   *            The iterator contains a list of tuple(json_file_path, json_file_size) which are\n   *            pre-prepared and set in the block manager by DeltaSharingDataSource and its related\n   *            classes.\n   * @return the list of json files under the /_delta_log directory, if prepared.\n   */\n  override def listStatus(f: Path): Array[FileStatus] = {\n    val iterator =\n      SparkEnv.get.blockManager\n        .get[DeltaSharingLogFileStatus](getDeltaSharingLogBlockId(f.toString)) match {\n        case Some(block) => block.data.asInstanceOf[Iterator[DeltaSharingLogFileStatus]]\n        case _ => throw new FileNotFoundException(s\"Failed to listStatus for path: $f.\")\n      }\n\n    // Explicitly call hasNext to allow the reader lock on the block to be released.\n    val arrayBuilder = Array.newBuilder[FileStatus]\n    while (iterator.hasNext) {\n      val fileStatus = iterator.next()\n      arrayBuilder += new FileStatus(\n        /* length */ fileStatus.size,\n        /* isdir */ false,\n        /* block_replication */ 0,\n        /* blocksize */ 1,\n        /* modification_time */ fileStatus.modificationTime,\n        /* path */ new Path(fileStatus.path)\n      )\n    }\n    arrayBuilder.result()\n  }\n\n  override def create(\n      f: Path,\n      permission: FsPermission,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream = {\n    throw new UnsupportedOperationException(s\"create: $f\")\n  }\n\n  override def append(f: Path, bufferSize: Int, progress: Progressable): FSDataOutputStream = {\n    throw new UnsupportedOperationException(s\"append: $f\")\n  }\n\n  override def rename(src: Path, dst: Path): Boolean = {\n    throw new UnsupportedOperationException(s\"rename: src:$src, dst:$dst\")\n  }\n\n  override def delete(f: Path, recursive: Boolean): Boolean = {\n    throw new UnsupportedOperationException(s\"delete: $f\")\n  }\n  override def listStatusIterator(f: Path): RemoteIterator[FileStatus] = {\n    throw new UnsupportedOperationException(s\"listStatusIterator: $f\")\n  }\n\n  override def setWorkingDirectory(newDir: Path): Unit =\n    throw new UnsupportedOperationException(s\"setWorkingDirectory: $newDir\")\n\n  override def getWorkingDirectory: Path = new Path(getUri)\n\n  override def mkdirs(f: Path, permission: FsPermission): Boolean = {\n    throw new UnsupportedOperationException(s\"mkdirs: $f\")\n  }\n\n  override def close(): Unit = {\n    super.close()\n  }\n\n  private def getBlockAndReleaseLockHelper[T: ClassTag](\n      f: Path, suffix: Option[String], caller: String): T = {\n    val blockId = getDeltaSharingLogBlockId(suffix.foldLeft(f.toString)(_ + _))\n    val result = SparkEnv.get.blockManager.getSingle[T](blockId).getOrElse {\n      throw new FileNotFoundException(s\"Failed to $caller for $f.\")\n    }\n    releaseLockHelper(blockId)\n\n    result\n  }\n\n  private def releaseLockHelper(blockId: BlockId): Unit = {\n    try {\n      SparkEnv.get.blockManager.releaseLock(blockId)\n    } catch {\n      // releaseLock may fail when the lock is not hold by this thread, we are not exactly sure\n      // when it fails or not, but no need to fail the entire delta sharing query.\n      case e: Throwable => logWarning(s\"Error while releasing lock for blockId:$blockId: $e.\")\n    }\n  }\n}\n\n/**\n * A case class including the metadata for the constructed delta log based on the delta sharing\n * rpc response.\n * @param idToUrl stores the id to url mapping, used to register to CachedTableManager\n * @param minUrlExpirationTimestamp used to indicate when to refresh urls in CachedTableManager\n * @param numFileActionsInMinVersionOpt This is needed because DeltaSource is not advancing the\n *                                      offset to the next version automatically when scanning\n *                                      through a snapshot, so DeltaSharingSource needs to count the\n *                                      number of files in the min version and advance the offset to\n *                                      the next version when the offset is at the last index of the\n *                                      version.\n * @param minVersion  minVersion of all the files returned from server\n * @param maxVersion  maxVersion of all the files returned from server\n */\ncase class ConstructedDeltaLogMetadata(\n    idToUrl: Map[String, String],\n    minUrlExpirationTimestamp: Option[Long],\n    numFileActionsInMinVersionOpt: Option[Int],\n    minVersion: Long,\n    maxVersion: Long)\n\n/** Public constants for DeltaSharingLogFileSystem accessible visible outside the package */\nobject DeltaSharingLogFileSystemConstants {\n  /** The URI scheme used for delta-sharing fake delta-logs. */\n  final val SCHEME = \"delta-sharing-log\"\n}\n\nprivate[sharing] object DeltaSharingLogFileSystem extends Logging {\n\n  val SCHEME = DeltaSharingLogFileSystemConstants.SCHEME\n\n  // The constant added as prefix to all delta sharing block ids.\n  private val BLOCK_ID_TEST_PREFIX = \"test_\"\n\n  // It starts with test_ to match the prefix of TestBlockId.\n  // In the meantime, we'll investigate in an option to add a general purposed BlockId subclass\n  // and use it in delta sharing.\n  val DELTA_SHARING_LOG_BLOCK_ID_PREFIX = \"test_delta-sharing-log:\"\n\n  def getDeltaSharingLogBlockId(path: String): BlockId = {\n    BlockId(BLOCK_ID_TEST_PREFIX + path)\n  }\n\n  /**\n   * Encode `tablePath` to a `Path` in the following format:\n   *\n   * ```\n   * delta-sharing-log:///<url encoded table path>\n   * ```\n   *\n   * This format can be decoded by `DeltaSharingLogFileSystem.decode`.\n   * It will be used to:\n   * 1) construct a DeltaLog class which points to a delta sharing table.\n   * 2) construct a block id to look for commit files of the delta sharing table.\n   */\n  def encode(tablePath: String): Path = {\n    val encodedTablePath = URLEncoder.encode(tablePath, \"UTF-8\")\n    new Path(s\"$SCHEME:///$encodedTablePath\")\n  }\n\n  def decode(path: Path): String = {\n    val encodedTablePath = path.toString\n      .stripPrefix(s\"$SCHEME:///\")\n      .stripPrefix(s\"$SCHEME:/\")\n    URLDecoder.decode(encodedTablePath, \"UTF-8\")\n  }\n\n  // Convert a deletion vector path to a delta sharing path.\n  // Only paths needs to be converted since it's pre-signed url. Inline DV should be handled\n  // in place. And UUID should throw error since it should be converted to pre-signed url when\n  // returned from the server.\n  private def getDeltaSharingDeletionVectorDescriptor(\n      fileAction: model.DeltaSharingFileAction,\n      customTablePath: String): DeletionVectorDescriptor = {\n    if (fileAction.getDeletionVectorOpt.isEmpty) {\n      null\n    } else {\n      val deletionVector = fileAction.getDeletionVectorOpt.get\n      deletionVector.storageType match {\n        case DeletionVectorDescriptor.PATH_DV_MARKER =>\n          deletionVector.copy(\n            pathOrInlineDv = fileAction.getDeletionVectorDeltaSharingPath(customTablePath)\n          )\n        case DeletionVectorDescriptor.INLINE_DV_MARKER => deletionVector\n        case storageType =>\n          throw new IllegalStateException(\n            s\"Unexpected DV storage type:\" +\n            s\"$storageType in the delta sharing response for ${fileAction.json}.\"\n          )\n      }\n    }\n  }\n\n  /**\n   * Convert DeltaSharingFileAction with delta sharing file path and serialize as json to store in\n   * the delta log.\n   *\n   * @param fileAction               The DeltaSharingFileAction to convert.\n   * @param customTablePath The table path used to construct action.path field.\n   * @return json serialization of delta action.\n   */\n  private def getActionWithDeltaSharingPath(\n      fileAction: model.DeltaSharingFileAction,\n      customTablePath: String): String = {\n    val deltaSharingPath = fileAction.getDeltaSharingPath(customTablePath)\n    val newSingleAction = fileAction.deltaSingleAction.unwrap match {\n      case add: AddFile =>\n        add.copy(\n          path = deltaSharingPath,\n          deletionVector = getDeltaSharingDeletionVectorDescriptor(fileAction, customTablePath)\n        )\n      case cdc: AddCDCFile =>\n        assert(\n          cdc.deletionVector == null,\n          \"deletionVector not null in the AddCDCFile from delta\" +\n          s\" sharing response: ${cdc.json}\"\n        )\n        cdc.copy(path = deltaSharingPath)\n      case remove: RemoveFile =>\n        remove.copy(\n          path = deltaSharingPath,\n          deletionVector = getDeltaSharingDeletionVectorDescriptor(fileAction, customTablePath)\n        )\n      case action =>\n        throw new IllegalStateException(\n          s\"unexpected action in delta sharing \" +\n          s\"response: ${action.json}\"\n        )\n    }\n    newSingleAction.json\n  }\n\n  // Sort by id to keep a stable order of the files within a version in the delta log.\n  private def deltaSharingFileActionIncreaseOrderFunc(\n      f1: model.DeltaSharingFileAction,\n      f2: model.DeltaSharingFileAction): Boolean = {\n    f1.id < f2.id\n  }\n\n  /**\n   * Cleanup the delta log upon explicit stop of a query on a delta sharing table.\n   *\n   * @param deltaLogPath deltaLogPath is constructed per query with credential scope id as prefix\n   *                     and a uuid as suffix, which is very unique to the query and won't interfere\n   *                     with other queries.\n   */\n  def tryToCleanUpDeltaLog(deltaLogPath: String): Unit = {\n    def shouldCleanUp(blockId: BlockId): Boolean = {\n      if (!blockId.name.startsWith(DELTA_SHARING_LOG_BLOCK_ID_PREFIX)) {\n        return false\n      }\n      val blockName = blockId.name\n      // deltaLogPath is constructed per query with credential scope id as prefix and a uuid as\n      // suffix, which is very unique to the query and won't interfere with other queries.\n      blockName.startsWith(BLOCK_ID_TEST_PREFIX + deltaLogPath)\n    }\n\n    val blockManager = SparkEnv.get.blockManager\n    val matchingBlockIds = blockManager.getMatchingBlockIds(shouldCleanUp(_))\n    logInfo(\n      s\"Trying to clean up ${matchingBlockIds.size} blocks for $deltaLogPath.\"\n    )\n\n    val problematicBlockIds = Seq.newBuilder[BlockId]\n    matchingBlockIds.foreach { b =>\n      try {\n        blockManager.removeBlock(b)\n      } catch {\n        case _: Throwable => problematicBlockIds += b\n      }\n    }\n\n    val problematicBlockIdsSeq = problematicBlockIds.result().toSeq\n    if (problematicBlockIdsSeq.size > 0) {\n      logWarning(\n        s\"Done cleaning up ${matchingBlockIds.size} blocks for $deltaLogPath, but \" +\n        s\"failed to remove: ${problematicBlockIdsSeq}.\"\n      )\n    } else {\n      logInfo(\n        s\"Done cleaning up ${matchingBlockIds.size} blocks for $deltaLogPath.\"\n      )\n    }\n  }\n\n  private def prepareCheckpointFile(\n    deltaLogPath: String,\n    checkpointVersion: Long,\n    fileSizeTsSeq: Builder[DeltaSharingLogFileStatus, Seq[DeltaSharingLogFileStatus]]): Unit = {\n    // 1) store the checkpoint byte array in BlockManager for future read.\n    val checkpointParquetFileName =\n      FileNames.checkpointFileSingular(new Path(deltaLogPath), checkpointVersion).toString\n    fileSizeTsSeq += DeltaSharingLogFileStatus(\n      path = checkpointParquetFileName,\n      size = FAKE_CHECKPOINT_BYTE_ARRAY.size,\n      modificationTime = 0L\n    )\n\n    // 2) Prepare the content for _last_checkpoint\n    val lastCheckpointContent =\n      s\"\"\"{\"version\":${checkpointVersion},\"size\":${FAKE_CHECKPOINT_BYTE_ARRAY.size}}\"\"\"\n    val lastCheckpointPath = new Path(deltaLogPath, \"_last_checkpoint\").toString\n    fileSizeTsSeq += DeltaSharingLogFileStatus(\n      path = lastCheckpointPath,\n      size = lastCheckpointContent.length,\n      modificationTime = 0L\n    )\n    DeltaSharingUtils.overrideSingleBlock[String](\n      blockId = getDeltaSharingLogBlockId(lastCheckpointPath),\n      value = lastCheckpointContent\n    )\n  }\n\n  private def updateListingDeltaLog(deltaLogPath: String, checkpointVersion: Long): Unit = {\n    val fileSizeTsSeq = Seq.newBuilder[DeltaSharingLogFileStatus]\n    prepareCheckpointFile(deltaLogPath, checkpointVersion, fileSizeTsSeq)\n\n    val iterator = SparkEnv.get.blockManager\n      .get[DeltaSharingLogFileStatus](getDeltaSharingLogBlockId(deltaLogPath)) match {\n      case Some(block) => block.data.asInstanceOf[Iterator[DeltaSharingLogFileStatus]]\n      case _ => throw new FileNotFoundException(s\"Failed to list files for path: $deltaLogPath.\")\n    }\n    // Explicitly materialize iterator to allow the reader lock on the block to be released.\n    val files = iterator.flatMap { deltaSharingLogFileStatus =>\n      val filePath = new Path(deltaSharingLogFileStatus.path)\n      filePath match {\n        case FileNames.CheckpointFile(_, version) if version > checkpointVersion =>\n          Some(deltaSharingLogFileStatus)\n        case FileNames.DeltaFile(_, version) if version > checkpointVersion =>\n          Some(deltaSharingLogFileStatus)\n        case _ => None\n      }\n    }.toIndexedSeq\n\n    DeltaSharingUtils.overrideIteratorBlock[DeltaSharingLogFileStatus](\n      getDeltaSharingLogBlockId(deltaLogPath),\n      (fileSizeTsSeq.result() ++ files).toIterator\n    )\n  }\n\n  /**\n   * @param deltaLogPath The delta log directory to clean up. It is constructed per query with\n   *                     credential scope id as prefix and a uuid as suffix, which is very unique\n   *                     to the query and won't interfere with other queries.\n   * @param maxVersion maxVersion of any checkpoint or delta file that needs clean up, inclusive.\n   */\n  def tryToCleanUpPreviousBlocks(deltaLogPath: String, maxVersion: Long): Unit = {\n    if (maxVersion < 0) {\n      logInfo(\n        s\"Skipping clean up previous blocks for $deltaLogPath because maxVersion(\" +\n        s\"$maxVersion) < 0.\"\n      )\n      return\n    }\n\n    def shouldCleanUp(blockId: BlockId): Boolean = {\n      if (!blockId.name.startsWith(DELTA_SHARING_LOG_BLOCK_ID_PREFIX)) {\n        return false\n      }\n      val blockName = blockId.name\n      blockName.startsWith(BLOCK_ID_TEST_PREFIX + deltaLogPath) && FileNames\n        .getFileVersionOpt(new Path(blockName.stripPrefix(BLOCK_ID_TEST_PREFIX)))\n        .exists(_ <= maxVersion)\n    }\n\n    val blockManager = SparkEnv.get.blockManager\n    // 1) try to update the listing of the delta log files:\n    //   - add a new checkpoint at maxVersion.\n    //   - update the _last_checkpoint file.\n    //   - update the result of listStatus for deltaLogPath.\n    try {\n      updateListingDeltaLog(deltaLogPath, maxVersion)\n    } catch {\n      case NonFatal(e) =>\n        logWarning(\n          s\"Stopped cleaning up the delta log for [$deltaLogPath], because updating the \" +\n            s\"listStatus of deltaLog() failed due to [${e.toString}].\"\n        )\n        return\n    }\n\n    // 2) try to clean up the delta log (.json) file for each version that has been read.\n    val matchingBlockIds = blockManager.getMatchingBlockIds(shouldCleanUp(_))\n    logInfo(\n      s\"Trying to clean up ${matchingBlockIds.size} previous blocks for $deltaLogPath \" +\n      s\"before version: $maxVersion.\"\n    )\n\n    val problematicBlockIds = Seq.newBuilder[BlockId]\n    matchingBlockIds.foreach { b =>\n      try {\n        blockManager.removeBlock(b)\n      } catch {\n        case _: Throwable => problematicBlockIds += b\n      }\n    }\n\n    val problematicBlockIdsSeq = problematicBlockIds.result().toSeq\n    if (problematicBlockIdsSeq.size > 0) {\n      logWarning(\n        s\"Done cleaning up ${matchingBlockIds.size} previous blocks for $deltaLogPath \" +\n        s\"before version: $maxVersion, but failed to remove: ${problematicBlockIdsSeq}.\"\n      )\n    } else {\n      logInfo(\n        s\"Done cleaning up ${matchingBlockIds.size} previous blocks for $deltaLogPath \" +\n        s\"before version: $maxVersion.\"\n      )\n    }\n  }\n\n  /**\n   * Construct local delta log based on delta log actions returned from delta sharing server.\n   *\n   * @param lines           a list of delta actions, to be processed and put in the local delta log,\n   *                        each action contains a version field to indicate the version of log to\n   *                        put it in.\n   * @param customTablePath query customized table path, used to construct action.path field for\n   *                        DeltaSharingFileSystem\n   * @param startingVersionOpt If set, used to construct the delta file (.json log file) from the\n   *                           given startingVersion. This is needed by DeltaSharingSource to\n   *                           construct the delta log for the rpc no matter if there are files in\n   *                           that version or not, so DeltaSource can read delta actions from the\n   *                           starting version (instead from checkpoint).\n   * @param endingVersionOpt If set, used to construct the delta file (.json log file) until the\n   *                         given endingVersion. This is needed by DeltaSharingSource to construct\n   *                         the delta log for the rpc no matter if there are files in that version\n   *                         or not.\n   *                         NOTE: DeltaSource will not advance the offset if there are no files in\n   *                         a version of the delta log, but we still create the delta log file for\n   *                         that version to avoid missing delta log (json) files.\n   * @return ConstructedDeltaLogMetadata, which contains 3 fields:\n   *          - idToUrl: mapping from file id to pre-signed url\n   *          - minUrlExpirationTimestamp timestamp indicating the when to refresh pre-signed urls.\n   *            Both are used to register to CachedTableManager.\n   *          - maxVersion: the max version returned in the http response, used by\n   *            DeltaSharingSource to quickly understand the progress of rpcs from the server.\n   */\n  def constructLocalDeltaLogAcrossVersions(\n      lines: Seq[String],\n      customTablePath: String,\n      startingVersionOpt: Option[Long],\n      endingVersionOpt: Option[Long]): ConstructedDeltaLogMetadata = {\n    val startTime = System.currentTimeMillis()\n    assert(\n      startingVersionOpt.isDefined == endingVersionOpt.isDefined,\n      s\"startingVersionOpt($startingVersionOpt) and endingVersionOpt($endingVersionOpt) should be\" +\n      \" both defined or not.\"\n    )\n    if (startingVersionOpt.isDefined) {\n      assert(\n        startingVersionOpt.get <= endingVersionOpt.get,\n        s\"startingVersionOpt($startingVersionOpt) must be smaller than \" +\n        s\"endingVersionOpt($endingVersionOpt).\"\n      )\n    }\n    var minVersion = Long.MaxValue\n    var maxVersion = 0L\n    var minUrlExpirationTimestamp: Option[Long] = None\n    val idToUrl = scala.collection.mutable.Map[String, String]()\n    val versionToDeltaSharingFileActions =\n      scala.collection.mutable.Map[Long, ArrayBuffer[model.DeltaSharingFileAction]]()\n    val versionToMetadata = scala.collection.mutable.Map[Long, model.DeltaSharingMetadata]()\n    val versionToJsonLogBuilderMap = scala.collection.mutable.Map[Long, ArrayBuffer[String]]()\n    val versionToJsonLogSize = scala.collection.mutable.Map[Long, Long]().withDefaultValue(0L)\n    var numFileActionsInMinVersion = 0\n    val versionToTimestampMap = scala.collection.mutable.Map[Long, Long]()\n    var startingMetadataLineOpt: Option[String] = None\n    var startingProtocolLineOpt: Option[String] = None\n\n    lines.foreach { line =>\n      val action = JsonUtils.fromJson[model.DeltaSharingSingleAction](line).unwrap\n      action match {\n        case fileAction: model.DeltaSharingFileAction =>\n          minVersion = minVersion.min(fileAction.version)\n          maxVersion = maxVersion.max(fileAction.version)\n          // Store file actions in an array to sort them based on id later.\n          versionToDeltaSharingFileActions.getOrElseUpdate(\n            fileAction.version,\n            ArrayBuffer[model.DeltaSharingFileAction]()\n          ) += fileAction\n        case metadata: model.DeltaSharingMetadata =>\n          if (metadata.version != null) {\n            // This is to handle the cdf and streaming query result.\n            minVersion = minVersion.min(metadata.version)\n            maxVersion = maxVersion.max(metadata.version)\n            versionToMetadata(metadata.version) = metadata\n            if (metadata.version == minVersion) {\n              startingMetadataLineOpt = Some(metadata.deltaMetadata.json + \"\\n\")\n            }\n          } else {\n            // This is to handle the snapshot query result from DeltaSharingSource.\n            startingMetadataLineOpt = Some(metadata.deltaMetadata.json + \"\\n\")\n          }\n        case protocol: model.DeltaSharingProtocol =>\n          startingProtocolLineOpt = Some(protocol.deltaProtocol.json + \"\\n\")\n        case _ => // do nothing, ignore the line.\n      }\n    }\n\n    if (startingVersionOpt.isDefined) {\n      minVersion = minVersion.min(startingVersionOpt.get)\n    } else if (minVersion == Long.MaxValue) {\n      // This means there are no files returned from server for this cdf request.\n      // A 0.json file will be prepared with metadata and protocol only.\n      minVersion = 0\n    }\n    if (endingVersionOpt.isDefined) {\n      maxVersion = maxVersion.max(endingVersionOpt.get)\n    }\n    // Store the starting protocol and metadata in the minVersion.json.\n    val protocolAndMetadataStr = startingMetadataLineOpt.getOrElse(\"\") + startingProtocolLineOpt\n        .getOrElse(\"\")\n    versionToJsonLogBuilderMap.getOrElseUpdate(\n      minVersion,\n      ArrayBuffer[String]()\n    ) += protocolAndMetadataStr\n    versionToJsonLogSize(minVersion) += protocolAndMetadataStr.getBytes(\n      StandardCharsets.UTF_8\n    ).length\n    numFileActionsInMinVersion = versionToDeltaSharingFileActions\n      .getOrElseUpdate(minVersion, ArrayBuffer[model.DeltaSharingFileAction]())\n      .size\n\n    // Write metadata to the delta log json file.\n    versionToMetadata.foreach {\n      case (version, metadata) =>\n        if (version != minVersion) {\n          val metadataStr = metadata.deltaMetadata.json + \"\\n\"\n          versionToJsonLogBuilderMap.getOrElseUpdate(\n            version,\n            ArrayBuffer[String]()\n          ) += metadataStr\n          versionToJsonLogSize(version) += metadataStr.getBytes(StandardCharsets.UTF_8).length\n        }\n    }\n    // Write file actions to the delta log json file.\n    var previousIdOpt: Option[String] = None\n    versionToDeltaSharingFileActions.foreach {\n      case (version, actions) =>\n        previousIdOpt = None\n        actions.toSeq.sortWith(deltaSharingFileActionIncreaseOrderFunc).foreach { fileAction =>\n          assert(\n            // Using > instead of >= because there can be a removeFile and addFile pointing to the\n            // same parquet file which result in the same file id, since id is a hash of file path.\n            // This is ok because eventually it can read data out of the correct parquet file.\n            !previousIdOpt.exists(_ > fileAction.id),\n            s\"fileActions must be in increasing order by id: ${previousIdOpt} is not smaller than\" +\n            s\" ${fileAction.id}, in version:$version.\"\n          )\n          previousIdOpt = Some(fileAction.id)\n\n          // 1. build it to url mapping\n          idToUrl(fileAction.id) = fileAction.path\n          if (DeltaSharingUtils.requiresIdToUrlForDV(fileAction.getDeletionVectorOpt)) {\n            idToUrl(fileAction.deletionVectorFileId) =\n              fileAction.getDeletionVectorOpt.get.pathOrInlineDv\n          }\n\n          // 2. prepare json log content.\n          versionToTimestampMap.getOrElseUpdate(version, fileAction.timestamp)\n          val actionJsonStr = getActionWithDeltaSharingPath(fileAction, customTablePath) + \"\\n\"\n          versionToJsonLogBuilderMap.getOrElseUpdate(\n            version,\n            ArrayBuffer[String]()\n          ) += actionJsonStr\n          versionToJsonLogSize(version) += actionJsonStr.getBytes(StandardCharsets.UTF_8).length\n\n          // 3. process expiration timestamp\n          if (fileAction.expirationTimestamp != null) {\n            minUrlExpirationTimestamp = minUrlExpirationTimestamp\n              .filter(_ < fileAction.expirationTimestamp)\n              .orElse(Some(fileAction.expirationTimestamp))\n          }\n        }\n    }\n\n    val encodedTablePath = DeltaSharingLogFileSystem.encode(customTablePath)\n    val deltaLogPath = s\"${encodedTablePath.toString}/_delta_log\"\n    val fileSizeTsSeq = Seq.newBuilder[DeltaSharingLogFileStatus]\n\n    if (minVersion > 0) {\n      // If the minVersion is not 0 in the response, then prepare checkpoint at minVersion - 1:\n      // need to prepare two files: 1) (minVersion-1).checkpoint.parquet 2) _last_checkpoint\n      prepareCheckpointFile(\n        deltaLogPath, checkpointVersion = minVersion - 1, fileSizeTsSeq = fileSizeTsSeq)\n    }\n\n    for (version <- minVersion to maxVersion) {\n      val jsonFilePath = FileNames.unsafeDeltaFile(new Path(deltaLogPath), version).toString\n      DeltaSharingUtils.overrideIteratorBlock[String](\n        getDeltaSharingLogBlockId(jsonFilePath),\n        versionToJsonLogBuilderMap.getOrElse(version, Seq.empty).toIterator\n      )\n      fileSizeTsSeq += DeltaSharingLogFileStatus(\n        path = jsonFilePath,\n        size = versionToJsonLogSize.getOrElse(version, 0),\n        modificationTime = versionToTimestampMap.get(version).getOrElse(0L)\n      )\n    }\n\n    DeltaSharingUtils.overrideIteratorBlock[DeltaSharingLogFileStatus](\n      getDeltaSharingLogBlockId(deltaLogPath),\n      fileSizeTsSeq.result().toIterator\n    )\n    logInfo(\n      s\"It takes ${(System.currentTimeMillis() - startTime) / 1000.0}s to construct delta log\" +\n      s\"for $customTablePath from $minVersion to $maxVersion, with ${idToUrl.toMap.size} urls.\"\n    )\n    ConstructedDeltaLogMetadata(\n      idToUrl = idToUrl.toMap,\n      minUrlExpirationTimestamp = minUrlExpirationTimestamp,\n      numFileActionsInMinVersionOpt = Some(numFileActionsInMinVersion),\n      minVersion = minVersion,\n      maxVersion = maxVersion\n    )\n  }\n\n  /** Set the modificationTime to zero, this is to align with the time returned from\n   * DeltaSharingFileSystem.getFileStatus\n   */\n  private def setModificationTimestampToZero(deltaSingleAction: SingleAction): SingleAction = {\n    deltaSingleAction.unwrap match {\n      case a: AddFile => a.copy(modificationTime = 0).wrap\n      case _ => deltaSingleAction\n    }\n  }\n\n  /**\n   * Construct local delta log at version zero based on lines returned from delta sharing server,\n   * to support latest snapshot or time travel queries. Storing both protocol/metadata and\n   * the actual data actions in version 0 will simplify both the log construction and log reply.\n   *\n   * @param lines           a list of delta actions, to be processed and put in the local delta log,\n   *                        each action contains a version field to indicate the version of log to\n   *                        put it in.\n   * @param customTablePath query customized table path, used to construct action.path field for\n   *                        DeltaSharingFileSystem\n   * @return ConstructedDeltaLogMetadata, which contains 3 fields:\n   *          - idToUrl: mapping from file id to pre-signed url\n   *          - minUrlExpirationTimestamp timestamp indicating the when to refresh pre-signed urls.\n   *            Both are used to register to CachedTableManager.\n   *          - maxVersion: to be 0.\n   */\n  def constructLocalDeltaLogAtVersionZero(\n      lines: Seq[String],\n      customTablePath: String): ConstructedDeltaLogMetadata = {\n    val startTime = System.currentTimeMillis()\n    val jsonLogSeq = Seq.newBuilder[String]\n    var jsonLogSize = 0\n    var minUrlExpirationTimestamp: Option[Long] = None\n    val fileActionsSeq = ArrayBuffer[model.DeltaSharingFileAction]()\n    val idToUrl = scala.collection.mutable.Map[String, String]()\n    lines.foreach { line =>\n      val action = JsonUtils.fromJson[model.DeltaSharingSingleAction](line).unwrap\n      action match {\n        case fileAction: model.DeltaSharingFileAction =>\n          // Store file actions in an array to sort them based on id later.\n          fileActionsSeq += fileAction.copy(\n            deltaSingleAction = setModificationTimestampToZero(fileAction.deltaSingleAction)\n          )\n        case protocol: model.DeltaSharingProtocol =>\n          val protocolJsonStr = protocol.deltaProtocol.json + \"\\n\"\n          jsonLogSize += protocolJsonStr.getBytes(StandardCharsets.UTF_8).length\n          jsonLogSeq += protocolJsonStr\n        case metadata: model.DeltaSharingMetadata =>\n          val metadataJsonStr = metadata.deltaMetadata.json + \"\\n\"\n          jsonLogSize += metadataJsonStr.getBytes(StandardCharsets.UTF_8).length\n          jsonLogSeq += metadataJsonStr\n        case _ =>\n          throw new IllegalStateException(\n            s\"unknown action in the delta sharing \" +\n            s\"response: $line\"\n          )\n      }\n    }\n    var previousIdOpt: Option[String] = None\n    fileActionsSeq.toSeq.sortWith(deltaSharingFileActionIncreaseOrderFunc).foreach { fileAction =>\n      assert(\n        // Using > instead of >= because there can be a removeFile and addFile pointing to the same\n        // parquet file which result in the same file id, since id is a hash of file path.\n        // This is ok because eventually it can read data out of the correct parquet file.\n        !previousIdOpt.exists(_ > fileAction.id),\n        s\"fileActions must be in increasing order by id: ${previousIdOpt} is not smaller than\" +\n        s\" ${fileAction.id}.\"\n      )\n      previousIdOpt = Some(fileAction.id)\n\n      // 1. build id to url mapping\n      idToUrl(fileAction.id) = fileAction.path\n      if (DeltaSharingUtils.requiresIdToUrlForDV(fileAction.getDeletionVectorOpt)) {\n        idToUrl(fileAction.deletionVectorFileId) =\n          fileAction.getDeletionVectorOpt.get.pathOrInlineDv\n      }\n\n      // 2. prepare json log content.\n      val actionJsonStr = getActionWithDeltaSharingPath(fileAction, customTablePath) + \"\\n\"\n      jsonLogSize += actionJsonStr.getBytes(StandardCharsets.UTF_8).length\n      jsonLogSeq += actionJsonStr\n\n      // 3. process expiration timestamp\n      if (fileAction.expirationTimestamp != null) {\n        minUrlExpirationTimestamp =\n          if (minUrlExpirationTimestamp.isDefined &&\n            minUrlExpirationTimestamp.get < fileAction.expirationTimestamp) {\n            minUrlExpirationTimestamp\n          } else {\n            Some(fileAction.expirationTimestamp)\n          }\n      }\n    }\n\n    val encodedTablePath = DeltaSharingLogFileSystem.encode(customTablePath)\n\n    // Always use 0.json for snapshot queries.\n    val deltaLogPath = s\"${encodedTablePath.toString}/_delta_log\"\n    val jsonFilePath = FileNames.unsafeDeltaFile(new Path(deltaLogPath), 0).toString\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      getDeltaSharingLogBlockId(jsonFilePath),\n      jsonLogSeq.result().toIterator\n    )\n\n    val fileStatusSeq = Seq(\n      DeltaSharingLogFileStatus(path = jsonFilePath, size = jsonLogSize, modificationTime = 0L)\n    )\n    DeltaSharingUtils.overrideIteratorBlock[DeltaSharingLogFileStatus](\n      getDeltaSharingLogBlockId(deltaLogPath),\n      fileStatusSeq.toIterator\n    )\n    logInfo(\n      s\"It takes ${(System.currentTimeMillis() - startTime) / 1000.0}s to construct delta\" +\n      s\" log for $customTablePath with ${jsonLogSize} bytes for ${idToUrl.toMap.size} urls.\"\n    )\n    ConstructedDeltaLogMetadata(\n      idToUrl = idToUrl.toMap,\n      minUrlExpirationTimestamp = minUrlExpirationTimestamp,\n      numFileActionsInMinVersionOpt = None,\n      minVersion = 0,\n      maxVersion = 0\n    )\n  }\n\n  // Create a delta log directory with protocol and metadata at version 0.\n  // Used by DeltaSharingSource to initialize a DeltaLog class, which is then used to initialize\n  // a DeltaSource class, also the metadata id will be used for schemaTrackingLocation.\n  // There are no data files in the delta log because the DeltaSource class is initialized before\n  // any rpcs to the delta sharing server, so no data files are available yet.\n  def constructDeltaLogWithMetadataAtVersionZero(\n      customTablePath: String,\n      deltaSharingTableMetadata: DeltaSharingTableMetadata): Unit = {\n    val encodedTablePath = DeltaSharingLogFileSystem.encode(customTablePath)\n    val deltaLogPath = s\"${encodedTablePath.toString}/_delta_log\"\n\n    // Always use 0.json for snapshot queries.\n    val jsonLogStr = deltaSharingTableMetadata.protocol.deltaProtocol.json + \"\\n\" +\n      deltaSharingTableMetadata.metadata.deltaMetadata.json + \"\\n\"\n\n    val jsonFilePath = FileNames.unsafeDeltaFile(new Path(deltaLogPath), 0).toString\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      getDeltaSharingLogBlockId(jsonFilePath),\n      Seq(jsonLogStr).toIterator\n    )\n\n    val fileStatusSeq = Seq(\n      DeltaSharingLogFileStatus(\n        path = jsonFilePath,\n        size = jsonLogStr.getBytes(StandardCharsets.UTF_8).length,\n        modificationTime = 0L\n      )\n    )\n    DeltaSharingUtils.overrideIteratorBlock[DeltaSharingLogFileStatus](\n      getDeltaSharingLogBlockId(deltaLogPath),\n      fileStatusSeq.toIterator\n    )\n  }\n}\n\n/**\n * A ByteArrayInputStream that implements interfaces required by FSDataInputStream, which is the\n * return type of DeltaSharingLogFileSystem.open. It will convert the string content as array of\n * bytes and allow caller to read data out of it.\n * The string content are list of json serializations of delta actions in a json delta log file.\n */\nprivate[sharing] class SeekableByteArrayInputStream(bytes: Array[Byte])\n    extends ByteArrayInputStream(bytes)\n    with Seekable\n    with PositionedReadable {\n  assert(available == bytes.length)\n\n  override def seek(pos: Long): Unit = {\n    if (mark != 0) {\n      throw new IllegalStateException(\"Cannot seek if mark is set\")\n    }\n    reset()\n    skip(pos)\n  }\n\n  override def seekToNewSource(pos: Long): Boolean = {\n    false // there aren't multiple sources available\n  }\n\n  override def getPos(): Long = {\n    bytes.length - available\n  }\n\n  override def read(buffer: Array[Byte], offset: Int, length: Int): Int = {\n    super.read(buffer, offset, length)\n  }\n\n  override def read(pos: Long, buffer: Array[Byte], offset: Int, length: Int): Int = {\n    if (pos >= bytes.length) {\n      return -1\n    }\n    val readSize = math.min(length, bytes.length - pos).toInt\n    System.arraycopy(bytes, pos.toInt, buffer, offset, readSize)\n    readSize\n  }\n\n  override def readFully(pos: Long, buffer: Array[Byte], offset: Int, length: Int): Unit = {\n    System.arraycopy(bytes, pos.toInt, buffer, offset, length)\n  }\n\n  override def readFully(pos: Long, buffer: Array[Byte]): Unit = {\n    System.arraycopy(bytes, pos.toInt, buffer, 0, buffer.length)\n  }\n}\n\ncase class DeltaSharingLogFileStatus(path: String, size: Long, modificationTime: Long)\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/DeltaSharingUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.text.SimpleDateFormat\nimport java.util.{TimeZone, UUID}\n\nimport scala.reflect.ClassTag\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{DeletionVectorDescriptor, Metadata, Protocol}\nimport com.google.common.hash.Hashing\nimport io.delta.sharing.client.{DeltaSharingClient, DeltaSharingRestClient}\nimport io.delta.sharing.client.model.{DeltaTableFiles, DeltaTableMetadata, Table}\nimport io.delta.sharing.client.util.JsonUtils\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.delta.sharing.TableRefreshResult\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.datasources.FileFormat\nimport org.apache.spark.storage.{BlockId, StorageLevel}\n\nobject DeltaSharingUtils extends Logging {\n\n  val STREAMING_SUPPORTED_READER_FEATURES: Seq[String] =\n    Seq(\n      DeletionVectorsTableFeature.name,\n      ColumnMappingTableFeature.name,\n      TimestampNTZTableFeature.name,\n      TypeWideningPreviewTableFeature.name,\n      TypeWideningTableFeature.name,\n      VariantTypePreviewTableFeature.name,\n      VariantTypeTableFeature.name,\n      VariantShreddingPreviewTableFeature.name\n    )\n\n  val SUPPORTED_READER_FEATURES: Seq[String] =\n    Seq(\n      DeletionVectorsTableFeature.name,\n      ColumnMappingTableFeature.name,\n      TimestampNTZTableFeature.name,\n      TypeWideningPreviewTableFeature.name,\n      TypeWideningTableFeature.name,\n      VariantTypePreviewTableFeature.name,\n      VariantTypeTableFeature.name,\n      VariantShreddingPreviewTableFeature.name\n    )\n\n  // The prefix will be used for block ids of all blocks that store the delta log in BlockManager.\n  // It's used to ensure delta sharing queries don't mess up with blocks with other applications.\n  val DELTA_SHARING_BLOCK_ID_PREFIX = \"test_delta-sharing\"\n\n  // Refresher function for CachedTableManager to use.\n  // It takes refreshToken: Option[String] as a parameter and return TableRefreshResult.\n  type RefresherFunction = Option[String] => TableRefreshResult\n\n  case class DeltaSharingTableMetadata(\n      version: Long,\n      protocol: model.DeltaSharingProtocol,\n      metadata: model.DeltaSharingMetadata\n  )\n\n  // A wrapper function for streaming query to get the latest version/protocol/metadata of the\n  // shared table.\n  def getDeltaSharingTableMetadata(\n      client: DeltaSharingClient,\n      table: Table): DeltaSharingTableMetadata = {\n    val deltaTableMetadata = client.getMetadata(table)\n    getDeltaSharingTableMetadata(table, deltaTableMetadata)\n  }\n\n  def queryDeltaTableMetadata(\n      client: DeltaSharingClient,\n      table: Table,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None): DeltaTableMetadata = {\n    val deltaTableMetadata = client.getMetadata(table, versionAsOf, timestampAsOf)\n    logInfo(\n      s\"getMetadata returned in ${deltaTableMetadata.respondedFormat} format for table \" +\n      s\"$table with v_${versionAsOf.map(_.toString).getOrElse(\"None\")} \" +\n      s\"t_${timestampAsOf.getOrElse(\"None\")} from delta sharing server.\"\n    )\n    deltaTableMetadata\n  }\n\n  /**\n   * parse the protocol and metadata from rpc response for getMetadata.\n   */\n  def getDeltaSharingTableMetadata(\n      table: Table,\n      deltaTableMetadata: DeltaTableMetadata): DeltaSharingTableMetadata = {\n\n    var metadataOption: Option[model.DeltaSharingMetadata] = None\n    var protocolOption: Option[model.DeltaSharingProtocol] = None\n\n    deltaTableMetadata.lines\n      .map(\n        JsonUtils.fromJson[model.DeltaSharingSingleAction](_).unwrap\n      )\n      .foreach {\n        case m: model.DeltaSharingMetadata => metadataOption = Some(m)\n        case p: model.DeltaSharingProtocol => protocolOption = Some(p)\n        case _ => // ignore other lines\n      }\n\n    DeltaSharingTableMetadata(\n      version = deltaTableMetadata.version,\n      protocol = protocolOption.getOrElse {\n        throw new IllegalStateException(\n          s\"Failed to get Protocol for ${table.toString}, \" +\n          s\"response from server:${deltaTableMetadata.lines}.\"\n        )\n      },\n      metadata = metadataOption.getOrElse {\n        throw new IllegalStateException(\n          s\"Failed to get Metadata for ${table.toString}, \" +\n          s\"response from server:${deltaTableMetadata.lines}.\"\n        )\n      }\n    )\n  }\n\n  // Only absolute path (which is pre-signed url) need to be put in IdToUrl mapping.\n  // inline DV should be processed in place, and UUID should throw error.\n  def requiresIdToUrlForDV(deletionVectorOpt: Option[DeletionVectorDescriptor]): Boolean = {\n    deletionVectorOpt.isDefined &&\n      deletionVectorOpt.get.storageType == DeletionVectorDescriptor.PATH_DV_MARKER\n  }\n\n  private def getTableRefreshResult(tableFiles: DeltaTableFiles): TableRefreshResult = {\n    var minUrlExpiration: Option[Long] = None\n    // Collect the id to url mapping from the table files, which includes the file actions\n    // and deletion vectors.\n    val idToUrl = tableFiles.lines\n      .map(JsonUtils.fromJson[model.DeltaSharingSingleAction](_).unwrap)\n      .collect {\n        case fileAction: model.DeltaSharingFileAction =>\n          val baseEntries = Seq(fileAction.id -> fileAction.path)\n          val dvEntries = if (requiresIdToUrlForDV(fileAction.getDeletionVectorOpt)) {\n            Seq(\n              fileAction.deletionVectorFileId -> fileAction.getDeletionVectorOpt.get.pathOrInlineDv\n            )\n          } else {\n            Seq.empty\n          }\n          if (fileAction.expirationTimestamp != null) {\n            minUrlExpiration = minUrlExpiration\n              .filter(_ < fileAction.expirationTimestamp)\n              .orElse(Some(fileAction.expirationTimestamp))\n          }\n          baseEntries ++ dvEntries\n      }\n      .flatten\n      .toMap\n\n    TableRefreshResult(idToUrl, minUrlExpiration, tableFiles.refreshToken)\n  }\n\n  /**\n   * Get the refresher function for a delta sharing table who calls client.getFiles with the\n   * provided parameters.\n   *\n   * @return A refresher function used by the CachedTableManager to refresh urls.\n   */\n  def getRefresherForGetFiles(\n      client: DeltaSharingClient,\n      table: Table,\n      predicates: Seq[String],\n      limit: Option[Long],\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String],\n      jsonPredicateHints: Option[String],\n      useRefreshToken: Boolean): RefresherFunction = { refreshTokenOpt =>\n    {\n      // If versionAsOf is specified, ignore refresh token (e.g., in streaming queries)\n      val tableFiles = client\n        .getFiles(\n          table = table,\n          predicates = predicates,\n          limit = limit,\n          versionAsOf = versionAsOf,\n          timestampAsOf = timestampAsOf,\n          jsonPredicateHints = jsonPredicateHints,\n          refreshToken = if (useRefreshToken) refreshTokenOpt else None,\n          fileIdHash = None\n        )\n      getTableRefreshResult(tableFiles)\n    }\n  }\n\n  /**\n   * Get the refresher function for a delta sharing table who calls client.getCDFFiles with the\n   * provided parameters.\n   *\n   * @return A refresher function used by the CachedTableManager to refresh urls.\n   */\n  def getRefresherForGetCDFFiles(\n      client: DeltaSharingClient,\n      table: Table,\n      cdfOptions: Map[String, String]): RefresherFunction = { (_: Option[String]) =>\n    {\n      val tableFiles = client.getCDFFiles(\n        table = table,\n        cdfOptions = cdfOptions,\n        includeHistoricalMetadata = true,\n        fileIdHash = None\n      )\n      getTableRefreshResult(tableFiles)\n    }\n  }\n\n  /**\n   * Get the refresher function for a delta sharing table who calls client.getFiles with the\n   * provided parameters.\n   *\n   * @return A refresher function used by the CachedTableManager to refresh urls.\n   */\n  def getRefresherForGetFilesWithStartingVersion(\n      client: DeltaSharingClient,\n      table: Table,\n      startingVersion: Long,\n      endingVersion: Option[Long]): RefresherFunction = { (_: Option[String]) =>\n    {\n      val tableFiles = client\n        .getFiles(\n          table = table,\n          startingVersion = startingVersion,\n          endingVersion = endingVersion,\n          fileIdHash = None\n        )\n      getTableRefreshResult(tableFiles)\n    }\n  }\n\n  def overrideSingleBlock[T: ClassTag](blockId: BlockId, value: T): Unit = {\n    assert(\n      blockId.name.startsWith(DELTA_SHARING_BLOCK_ID_PREFIX),\n      s\"invalid delta sharing log block id: $blockId\"\n    )\n    removeBlockForJsonLogIfExists(blockId)\n    SparkEnv.get.blockManager.putSingle[T](\n      blockId = blockId,\n      value = value,\n      level = StorageLevel.MEMORY_AND_DISK_SER,\n      tellMaster = true\n    )\n  }\n\n  def overrideIteratorBlock[T: ClassTag](blockId: BlockId, values: Iterator[T]): Unit = {\n    assert(\n      blockId.name.startsWith(DELTA_SHARING_BLOCK_ID_PREFIX),\n      s\"invalid delta sharing log block id: $blockId\"\n    )\n    removeBlockForJsonLogIfExists(blockId)\n    SparkEnv.get.blockManager.putIterator[T](\n      blockId = blockId,\n      values = values,\n      level = StorageLevel.MEMORY_AND_DISK_SER,\n      tellMaster = true\n    )\n  }\n\n  // A helper function used by DeltaSharingSource and DeltaSharingDataSource to get\n  // SnapshotDescriptor used for delta sharing streaming.\n  def getDeltaLogAndSnapshotDescriptor(\n      spark: SparkSession,\n      deltaSharingTableMetadata: DeltaSharingTableMetadata,\n      customTablePathWithUUIDSuffix: String): (DeltaLog, SnapshotDescriptor) = {\n    // Create a delta log with metadata at version 0.\n    // Used by DeltaSharingSource to initialize a DeltaLog class, which is then used to initialize\n    // a DeltaSource class, also the metadata id will be used for schemaTrackingLocation.\n    DeltaSharingLogFileSystem.constructDeltaLogWithMetadataAtVersionZero(\n      customTablePathWithUUIDSuffix,\n      deltaSharingTableMetadata\n    )\n    val tablePath = DeltaSharingLogFileSystem.encode(customTablePathWithUUIDSuffix).toString\n    val localDeltaLog = DeltaLog.forTable(spark, tablePath)\n    (\n      localDeltaLog,\n      new SnapshotDescriptor {\n        val deltaLog: DeltaLog = localDeltaLog\n        val metadata: Metadata = deltaSharingTableMetadata.metadata.deltaMetadata\n        val protocol: Protocol = deltaSharingTableMetadata.protocol.deltaProtocol\n        val version = deltaSharingTableMetadata.version\n        val numOfFilesIfKnown = None\n        val sizeInBytesIfKnown = None\n      }\n    )\n  }\n\n  // Get a query hash id based on the query parameters: time travel options and filters.\n  // The id concatenated with table name and used in local DeltaLog and CachedTableManager.\n  // This is to uniquely identify the delta sharing table used twice in the same query but with\n  // different query parameters, so we can differentiate their delta log and entries in the\n  // CachedTableManager.\n  private[sharing] def getQueryParamsHashId(\n      options: DeltaSharingOptions,\n      partitionFiltersString: String,\n      dataFiltersString: String,\n      jsonPredicateHints: String,\n      limitHint: String,\n      version: Long): String = {\n    val fullQueryString = s\"${options.versionAsOf}_${options.timestampAsOf}_\" +\n      s\"${partitionFiltersString}_${dataFiltersString}_${jsonPredicateHints}_${limitHint}_\" +\n      s\"${version}\"\n    Hashing.sha256().hashString(fullQueryString, UTF_8).toString\n  }\n\n  // Get a query hash id based on the query parameters: cdfOptions.\n  // The id concatenated with table name and used in local DeltaLoc and CachedTableManager.\n  // This is to uniquely identify the delta sharing table used twice in the same query but with\n  // different query parameters, so we can differentiate their delta log and entries in the\n  // CachedTableManager.\n  private[sharing] def getQueryParamsHashId(cdfOptions: Map[String, String]): String = {\n    Hashing.sha256().hashString(cdfOptions.toString, UTF_8).toString\n  }\n\n  // Concatenate table path with an id as a suffix, to uniquely identify a delta sharing table and\n  // its corresponding delta log in a query.\n  private[sharing] def getTablePathWithIdSuffix(customTablePath: String, id: String): String = {\n    s\"${customTablePath}_${id}\"\n  }\n\n  // Get a unique string composed of a formatted timestamp and an uuid.\n  // Used as a suffix for the table name and its delta log path of a delta sharing table in a\n  // streaming job, to avoid overwriting the delta log from multiple references of the same delta\n  // sharing table in one streaming job.\n  private[sharing] def getFormattedTimestampWithUUID(): String = {\n    val dateFormat = new SimpleDateFormat(\"yyyyMMdd_HHmmss\")\n    dateFormat.setTimeZone(TimeZone.getTimeZone(\"UTC\"))\n    val formattedDateTime = dateFormat.format(System.currentTimeMillis())\n    val uuid = UUID.randomUUID().toString().split('-').head\n    s\"${formattedDateTime}_${uuid}\"\n  }\n\n  private def removeBlockForJsonLogIfExists(blockId: BlockId): Unit = {\n    val blockManager = SparkEnv.get.blockManager\n    blockManager.getMatchingBlockIds(_.name == blockId.name).foreach { b =>\n      logWarning(s\"Found and removing existing block for $blockId.\")\n      blockManager.removeBlock(b)\n    }\n  }\n\n  // This is a base64 encoded string of the content of an empty delta checkpoint file.\n  // Will be used to fake a checkpoint file in the locally constructed delta log for cdf and\n  // streaming queries.\n  val FAKE_CHECKPOINT_FILE_BASE64_ENCODED_STRING =\n    \"\"\"\nUEFSMRUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMAAAADAAAVABUOFRIV\n+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVGhUeFdWf39gCHBUEFQAV\nBhUGAAANMAIAAAADAAMAAAADAAAVABUcFSAV7J+l5AIcFQQVABUGFQYAAA40AgAAAAMABAAAAAMAAAAVABUOFRIV+tzH6QMcFQQV\nABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMA\nAAADAAAVABUaFR4V1Z/f2AIcFQQVABUGFQYAAA0wAgAAAAMAAwAAAAMAABUAFRwVIBXsn6XkAhwVBBUAFQYVBgAADjQCAAAAAwAE\nAAAAAwAAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMAAAADAAAVABUO\nFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUE\nFQAVBhUIAAAHGAMAAAADAAAVABUOFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgD\nAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMAAAADAAAVABUOFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFQ4V\nEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMAAAADAAAVABUOFRIV+tzH6QMcFQQV\nABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMA\nAAADAAAVABUaFR4V1Z/f2AIcFQQVABUGFQYAAA0wAgAAAAMAAwAAAAMAABUAFRwVIBXsn6XkAhwVBBUAFQYVBgAADjQCAAAAAwAE\nAAAAAwAAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMAAAADAAAVABUO\nFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUE\nFQAVBhUIAAAHGAMAAAADAAAVABUOFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgD\nAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAVBhUIAAAHGAMAAAADAAAVABUOFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABUAFSIV\nJhWRlf/uBxwVBBUAFQYVCAAAEUADAAAAAwgABgAAAHRlc3RJZBUAFQ4VEhXyyKGvDxwVBBUAFQYVCAAABxgDAAAAAwQAFQAVDhUS\nFfLIoa8PHBUEFQAVBhUIAAAHGAMAAAADBAAVABUkFSgV3dDvmgccFQQVABUGFQgAABJEAwAAAAMMAAcAAABwYXJxdWV0FQAVHBUg\nFfzUikccFQQVABUGFQYAAA40AgAAAAMABAAAAAMYAAAVABUcFSAV/NSKRxwVBBUAFQYVBgAADjQCAAAAAwAEAAAAAxgAABUAFQ4V\nEhXyyKGvDxwVBBUAFQYVCAAABxgDAAAAAwQAFQAVHBUgFYySkKYBHBUEFQAVBhUGAAAONAIAAAADAAQAAAADEAAAFQAVGhUeFbrI\n7KoEHBUEFQAVBhUGAAANMAIAAAADAAMAAAADCAAVABUcFSAVjJKQpgEcFQQVABUGFQYAAA40AgAAAAMABAAAAAMQAAAVABUOFRIV\n8sihrw8cFQQVABUGFQgAAAcYAwAAAAMEABUAFRYVGhXVxIjAChwVBBUAFQYVCAAACygDAAAAAwIAAQAAABUAFRYVGhWJ+6XrCBwV\nBBUAFQYVCAAACygDAAAAAwIAAgAAABUAFRwVIBWCt7b4AhwVBBUAFQYVBgAADjQCAAAAAwAEAAAAAwEAABUAFRwVIBWCt7b4AhwV\nBBUAFQYVBgAADjQCAAAAAwAEAAAAAwEAABUAFQ4VEhX63MfpAxwVBBUAFQYVCAAABxgDAAAAAwAAFQAVDhUSFfrcx+kDHBUEFQAV\nBhUIAAAHGAMAAAADAAAVABUOFRIV+tzH6QMcFQQVABUGFQgAAAcYAwAAAAMAABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYE\nABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRAhkYBnRlc3RJZBkY\nBnRlc3RJZBUCGRYCABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRAhkYB3BhcnF1ZXQZGAdwYXJxdWV0FQIZFgIAGREB\nGRgAGRgAFQIZFgQAGREBGRgAGRgAFQIZFgQAGREBGRgAGRgAFQIZFgQAGREBGRgAGRgAFQIZFgQAGREBGRgAGRgAFQIZFgQAGREB\nGRgAGRgAFQIZFgQAGREBGRgAGRgAFQIZFgQAGRECGRgEAQAAABkYBAEAAAAVAhkWAgAZEQIZGAQCAAAAGRgEAgAAABUCGRYCABkR\nARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkRARkYABkYABUCGRYEABkc\nFggVQBYAAAAZHBZIFUAWAAAAGRwWiAEVQBYAAAAZHBbIARVAFgAAABkcFogCFUwWAAAAGRwW1AIVThYAAAAZHBaiAxVAFgAAABkc\nFuIDFUAWAAAAGRwWogQVQBYAAAAZHBbiBBVMFgAAABkcFq4FFU4WAAAAGRwW/AUVQBYAAAAZHBa8BhVAFgAAABkcFvwGFUAWAAAA\nGRwWvAcVQBYAAAAZHBb8BxVAFgAAABkcFrwIFUAWAAAAGRwW/AgVQBYAAAAZHBa8CRVAFgAAABkcFvwJFUAWAAAAGRwWvAoVQBYA\nAAAZHBb8ChVAFgAAABkcFrwLFUAWAAAAGRwW/AsVQBYAAAAZHBa8DBVAFgAAABkcFvwMFUwWAAAAGRwWyA0VThYAAAAZHBaWDhVA\nFgAAABkcFtYOFUAWAAAAGRwWlg8VQBYAAAAZHBbWDxVAFgAAABkcFpYQFUAWAAAAGRwW1hAVQBYAAAAZHBaWERVAFgAAABkcFtYR\nFUAWAAAAGRwWlhIVQBYAAAAZHBbWEhVUFgAAABkcFqoTFUAWAAAAGRwW6hMVQBYAAAAZHBaqFBVWFgAAABkcFoAVFUwWAAAAGRwW\nzBUVTBYAAAAZHBaYFhVAFgAAABkcFtgWFU4WAAAAGRwWphcVTBYAAAAZHBbyFxVOFgAAABkcFsAYFUAWAAAAGRwWgBkVSBYAAAAZ\nHBbIGRVIFgAAABkcFpAaFU4WAAAAGRwW3hoVThYAAAAZHBasGxVAFgAAABkcFuwbFUAWAAAAGRwWrBwVQBYAAAAVAhn8UUgMc3Bh\ncmtfc2NoZW1hFQwANQIYA3R4bhUGABUMJQIYBWFwcElkJQBMHAAAABUEJQIYB3ZlcnNpb24AFQQlAhgLbGFzdFVwZGF0ZWQANQIY\nA2FkZBUWABUMJQIYBHBhdGglAEwcAAAANQIYD3BhcnRpdGlvblZhbHVlcxUCFQJMLAAAADUEGAlrZXlfdmFsdWUVBAAVDCUAGANr\nZXklAEwcAAAAFQwlAhgFdmFsdWUlAEwcAAAAFQQlAhgEc2l6ZQAVBCUCGBBtb2RpZmljYXRpb25UaW1lABUAJQIYCmRhdGFDaGFu\nZ2UANQIYBHRhZ3MVAhUCTCwAAAA1BBgJa2V5X3ZhbHVlFQQAFQwlABgDa2V5JQBMHAAAABUMJQIYBXZhbHVlJQBMHAAAADUCGA5k\nZWxldGlvblZlY3RvchUMABUMJQIYC3N0b3JhZ2VUeXBlJQBMHAAAABUMJQIYDnBhdGhPcklubGluZUR2JQBMHAAAABUCJQIYBm9m\nZnNldAAVAiUCGAtzaXplSW5CeXRlcwAVBCUCGAtjYXJkaW5hbGl0eQAVBCUCGAttYXhSb3dJbmRleAAVBCUCGAliYXNlUm93SWQA\nFQQlAhgXZGVmYXVsdFJvd0NvbW1pdFZlcnNpb24AFQwlAhgFc3RhdHMlAEwcAAAANQIYDHN0YXRzX3BhcnNlZBUCABUEJQIYCm51\nbVJlY29yZHMANQIYBnJlbW92ZRUSABUMJQIYBHBhdGglAEwcAAAAFQQlAhgRZGVsZXRpb25UaW1lc3RhbXAAFQAlAhgKZGF0YUNo\nYW5nZQAVACUCGBRleHRlbmRlZEZpbGVNZXRhZGF0YQA1AhgPcGFydGl0aW9uVmFsdWVzFQIVAkwsAAAANQQYCWtleV92YWx1ZRUE\nABUMJQAYA2tleSUATBwAAAAVDCUCGAV2YWx1ZSUATBwAAAAVBCUCGARzaXplADUCGA5kZWxldGlvblZlY3RvchUMABUMJQIYC3N0\nb3JhZ2VUeXBlJQBMHAAAABUMJQIYDnBhdGhPcklubGluZUR2JQBMHAAAABUCJQIYBm9mZnNldAAVAiUCGAtzaXplSW5CeXRlcwAV\nBCUCGAtjYXJkaW5hbGl0eQAVBCUCGAttYXhSb3dJbmRleAAVBCUCGAliYXNlUm93SWQAFQQlAhgXZGVmYXVsdFJvd0NvbW1pdFZl\ncnNpb24ANQIYCG1ldGFEYXRhFRAAFQwlAhgCaWQlAEwcAAAAFQwlAhgEbmFtZSUATBwAAAAVDCUCGAtkZXNjcmlwdGlvbiUATBwA\nAAA1AhgGZm9ybWF0FQQAFQwlAhgIcHJvdmlkZXIlAEwcAAAANQIYB29wdGlvbnMVAhUCTCwAAAA1BBgJa2V5X3ZhbHVlFQQAFQwl\nABgDa2V5JQBMHAAAABUMJQIYBXZhbHVlJQBMHAAAABUMJQIYDHNjaGVtYVN0cmluZyUATBwAAAA1AhgQcGFydGl0aW9uQ29sdW1u\ncxUCFQZMPAAAADUEGARsaXN0FQIAFQwlAhgHZWxlbWVudCUATBwAAAA1AhgNY29uZmlndXJhdGlvbhUCFQJMLAAAADUEGAlrZXlf\ndmFsdWUVBAAVDCUAGANrZXklAEwcAAAAFQwlAhgFdmFsdWUlAEwcAAAAFQQlAhgLY3JlYXRlZFRpbWUANQIYCHByb3RvY29sFQgA\nFQIlAhgQbWluUmVhZGVyVmVyc2lvbgAVAiUCGBBtaW5Xcml0ZXJWZXJzaW9uADUCGA5yZWFkZXJGZWF0dXJlcxUCFQZMPAAAADUE\nGARsaXN0FQIAFQwlAhgHZWxlbWVudCUATBwAAAA1AhgOd3JpdGVyRmVhdHVyZXMVAhUGTDwAAAA1BBgEbGlzdBUCABUMJQIYB2Vs\nZW1lbnQlAEwcAAAANQIYDmRvbWFpbk1ldGFkYXRhFQYAFQwlAhgGZG9tYWluJQBMHAAAABUMJQIYDWNvbmZpZ3VyYXRpb24lAEwc\nAAAAFQAlAhgHcmVtb3ZlZAAWBBkcGfw2JggcFQwZNQAGCBkoA3R4bgVhcHBJZBUCFgQWPBZAJgg8NgQAGRwVABUAFQIAABaUKhUU\nFuwcFR4AJkgcFQQZNQAGCBkoA3R4bgd2ZXJzaW9uFQIWBBY8FkAmSDw2BAAZHBUAFQAVAgAAFqgqFRQWih0VHgAmiAEcFQQZNQAG\nCBkoA3R4bgtsYXN0VXBkYXRlZBUCFgQWPBZAJogBPDYEABkcFQAVABUCAAAWvCoVFhaoHRUeACbIARwVDBk1AAYIGSgDYWRkBHBh\ndGgVAhYEFjwWQCbIATw2BAAZHBUAFQAVAgAAFtIqFRYWxh0VHgAmiAIcFQwZJQAGGUgDYWRkD3BhcnRpdGlvblZhbHVlcwlrZXlf\ndmFsdWUDa2V5FQIWBBZIFkwmiAI8NgQAGRwVABUAFQIAABboKhUWFuQdFR4AJtQCHBUMGSUABhlIA2FkZA9wYXJ0aXRpb25WYWx1\nZXMJa2V5X3ZhbHVlBXZhbHVlFQIWBBZKFk4m1AI8NgQAGRwVABUAFQIAABb+KhUWFoIeFR4AJqIDHBUEGTUABggZKANhZGQEc2l6\nZRUCFgQWPBZAJqIDPDYEABkcFQAVABUCAAAWlCsVFhagHhUeACbiAxwVBBk1AAYIGSgDYWRkEG1vZGlmaWNhdGlvblRpbWUVAhYE\nFjwWQCbiAzw2BAAZHBUAFQAVAgAAFqorFRYWvh4VHgAmogQcFQAZNQAGCBkoA2FkZApkYXRhQ2hhbmdlFQIWBBY8FkAmogQ8NgQA\nGRwVABUAFQIAABbAKxUWFtweFR4AJuIEHBUMGSUABhlIA2FkZAR0YWdzCWtleV92YWx1ZQNrZXkVAhYEFkgWTCbiBDw2BAAZHBUA\nFQAVAgAAFtYrFRYW+h4VHgAmrgUcFQwZJQAGGUgDYWRkBHRhZ3MJa2V5X3ZhbHVlBXZhbHVlFQIWBBZKFk4mrgU8NgQAGRwVABUA\nFQIAABbsKxUWFpgfFR4AJvwFHBUMGTUABggZOANhZGQOZGVsZXRpb25WZWN0b3ILc3RvcmFnZVR5cGUVAhYEFjwWQCb8BTw2BAAZ\nHBUAFQAVAgAAFoIsFRYWth8VHgAmvAYcFQwZNQAGCBk4A2FkZA5kZWxldGlvblZlY3Rvcg5wYXRoT3JJbmxpbmVEdhUCFgQWPBZA\nJrwGPDYEABkcFQAVABUCAAAWmCwVFhbUHxUeACb8BhwVAhk1AAYIGTgDYWRkDmRlbGV0aW9uVmVjdG9yBm9mZnNldBUCFgQWPBZA\nJvwGPDYEABkcFQAVABUCAAAWriwVFhbyHxUeACa8BxwVAhk1AAYIGTgDYWRkDmRlbGV0aW9uVmVjdG9yC3NpemVJbkJ5dGVzFQIW\nBBY8FkAmvAc8NgQAGRwVABUAFQIAABbELBUWFpAgFR4AJvwHHBUEGTUABggZOANhZGQOZGVsZXRpb25WZWN0b3ILY2FyZGluYWxp\ndHkVAhYEFjwWQCb8Bzw2BAAZHBUAFQAVAgAAFtosFRYWriAVHgAmvAgcFQQZNQAGCBk4A2FkZA5kZWxldGlvblZlY3RvcgttYXhS\nb3dJbmRleBUCFgQWPBZAJrwIPDYEABkcFQAVABUCAAAW8CwVFhbMIBUeACb8CBwVBBk1AAYIGSgDYWRkCWJhc2VSb3dJZBUCFgQW\nPBZAJvwIPDYEABkcFQAVABUCAAAWhi0VFhbqIBUeACa8CRwVBBk1AAYIGSgDYWRkF2RlZmF1bHRSb3dDb21taXRWZXJzaW9uFQIW\nBBY8FkAmvAk8NgQAGRwVABUAFQIAABacLRUWFoghFR4AJvwJHBUMGTUABggZKANhZGQFc3RhdHMVAhYEFjwWQCb8CTw2BAAZHBUA\nFQAVAgAAFrItFRYWpiEVHgAmvAocFQQZNQAGCBk4A2FkZAxzdGF0c19wYXJzZWQKbnVtUmVjb3JkcxUCFgQWPBZAJrwKPDYEABkc\nFQAVABUCAAAWyC0VFhbEIRUeACb8ChwVDBk1AAYIGSgGcmVtb3ZlBHBhdGgVAhYEFjwWQCb8Cjw2BAAZHBUAFQAVAgAAFt4tFRYW\n4iEVHgAmvAscFQQZNQAGCBkoBnJlbW92ZRFkZWxldGlvblRpbWVzdGFtcBUCFgQWPBZAJrwLPDYEABkcFQAVABUCAAAW9C0VFhaA\nIhUeACb8CxwVABk1AAYIGSgGcmVtb3ZlCmRhdGFDaGFuZ2UVAhYEFjwWQCb8Czw2BAAZHBUAFQAVAgAAFoouFRYWniIVHgAmvAwc\nFQAZNQAGCBkoBnJlbW92ZRRleHRlbmRlZEZpbGVNZXRhZGF0YRUCFgQWPBZAJrwMPDYEABkcFQAVABUCAAAWoC4VFha8IhUeACb8\nDBwVDBklAAYZSAZyZW1vdmUPcGFydGl0aW9uVmFsdWVzCWtleV92YWx1ZQNrZXkVAhYEFkgWTCb8DDw2BAAZHBUAFQAVAgAAFrYu\nFRYW2iIVHgAmyA0cFQwZJQAGGUgGcmVtb3ZlD3BhcnRpdGlvblZhbHVlcwlrZXlfdmFsdWUFdmFsdWUVAhYEFkoWTibIDTw2BAAZ\nHBUAFQAVAgAAFswuFRYW+CIVHgAmlg4cFQQZNQAGCBkoBnJlbW92ZQRzaXplFQIWBBY8FkAmlg48NgQAGRwVABUAFQIAABbiLhUW\nFpYjFR4AJtYOHBUMGTUABggZOAZyZW1vdmUOZGVsZXRpb25WZWN0b3ILc3RvcmFnZVR5cGUVAhYEFjwWQCbWDjw2BAAZHBUAFQAV\nAgAAFvguFRYWtCMVHgAmlg8cFQwZNQAGCBk4BnJlbW92ZQ5kZWxldGlvblZlY3Rvcg5wYXRoT3JJbmxpbmVEdhUCFgQWPBZAJpYP\nPDYEABkcFQAVABUCAAAWji8VFhbSIxUeACbWDxwVAhk1AAYIGTgGcmVtb3ZlDmRlbGV0aW9uVmVjdG9yBm9mZnNldBUCFgQWPBZA\nJtYPPDYEABkcFQAVABUCAAAWpC8VFhbwIxUeACaWEBwVAhk1AAYIGTgGcmVtb3ZlDmRlbGV0aW9uVmVjdG9yC3NpemVJbkJ5dGVz\nFQIWBBY8FkAmlhA8NgQAGRwVABUAFQIAABa6LxUWFo4kFR4AJtYQHBUEGTUABggZOAZyZW1vdmUOZGVsZXRpb25WZWN0b3ILY2Fy\nZGluYWxpdHkVAhYEFjwWQCbWEDw2BAAZHBUAFQAVAgAAFtAvFRYWrCQVHgAmlhEcFQQZNQAGCBk4BnJlbW92ZQ5kZWxldGlvblZl\nY3RvcgttYXhSb3dJbmRleBUCFgQWPBZAJpYRPDYEABkcFQAVABUCAAAW5i8VFhbKJBUeACbWERwVBBk1AAYIGSgGcmVtb3ZlCWJh\nc2VSb3dJZBUCFgQWPBZAJtYRPDYEABkcFQAVABUCAAAW/C8VFhboJBUeACaWEhwVBBk1AAYIGSgGcmVtb3ZlF2RlZmF1bHRSb3dD\nb21taXRWZXJzaW9uFQIWBBY8FkAmlhI8NgQAGRwVABUAFQIAABaSMBUWFoYlFR4AJtYSHBUMGTUABggZKAhtZXRhRGF0YQJpZBUC\nFgQWUBZUJtYSPBgGdGVzdElkGAZ0ZXN0SWQWAigGdGVzdElkGAZ0ZXN0SWQAGRwVABUAFQIAABaoMBUWFqQlFTYAJqoTHBUMGTUA\nBggZKAhtZXRhRGF0YQRuYW1lFQIWBBY8FkAmqhM8NgQAGRwVABUAFQIAABa+MBUWFtolFR4AJuoTHBUMGTUABggZKAhtZXRhRGF0\nYQtkZXNjcmlwdGlvbhUCFgQWPBZAJuoTPDYEABkcFQAVABUCAAAW1DAVFhb4JRUeACaqFBwVDBk1AAYIGTgIbWV0YURhdGEGZm9y\nbWF0CHByb3ZpZGVyFQIWBBZSFlYmqhQ8GAdwYXJxdWV0GAdwYXJxdWV0FgIoB3BhcnF1ZXQYB3BhcnF1ZXQAGRwVABUAFQIAABbq\nMBUWFpYmFToAJoAVHBUMGSUABhlYCG1ldGFEYXRhBmZvcm1hdAdvcHRpb25zCWtleV92YWx1ZQNrZXkVAhYEFkgWTCaAFTw2BAAZ\nHBUAFQAVAgAAFoAxFRYW0CYVHgAmzBUcFQwZJQAGGVgIbWV0YURhdGEGZm9ybWF0B29wdGlvbnMJa2V5X3ZhbHVlBXZhbHVlFQIW\nBBZIFkwmzBU8NgQAGRwVABUAFQIAABaWMRUWFu4mFR4AJpgWHBUMGTUABggZKAhtZXRhRGF0YQxzY2hlbWFTdHJpbmcVAhYEFjwW\nQCaYFjw2BAAZHBUAFQAVAgAAFqwxFRYWjCcVHgAm2BYcFQwZJQAGGUgIbWV0YURhdGEQcGFydGl0aW9uQ29sdW1ucwRsaXN0B2Vs\nZW1lbnQVAhYEFkoWTibYFjw2BAAZHBUAFQAVAgAAFsIxFRYWqicVHgAmphccFQwZJQAGGUgIbWV0YURhdGENY29uZmlndXJhdGlv\nbglrZXlfdmFsdWUDa2V5FQIWBBZIFkwmphc8NgQAGRwVABUAFQIAABbYMRUWFsgnFR4AJvIXHBUMGSUABhlICG1ldGFEYXRhDWNv\nbmZpZ3VyYXRpb24Ja2V5X3ZhbHVlBXZhbHVlFQIWBBZKFk4m8hc8NgQAGRwVABUAFQIAABbuMRUWFuYnFR4AJsAYHBUEGTUABggZ\nKAhtZXRhRGF0YQtjcmVhdGVkVGltZRUCFgQWPBZAJsAYPDYEABkcFQAVABUCAAAWhDIVFhaEKBUeACaAGRwVAhk1AAYIGSgIcHJv\ndG9jb2wQbWluUmVhZGVyVmVyc2lvbhUCFgQWRBZIJoAZPBgEAQAAABgEAQAAABYCKAQBAAAAGAQBAAAAABkcFQAVABUCAAAWmjIV\nFhaiKBUuACbIGRwVAhk1AAYIGSgIcHJvdG9jb2wQbWluV3JpdGVyVmVyc2lvbhUCFgQWRBZIJsgZPBgEAgAAABgEAgAAABYCKAQC\nAAAAGAQCAAAAABkcFQAVABUCAAAWsDIVFhbQKBUuACaQGhwVDBklAAYZSAhwcm90b2NvbA5yZWFkZXJGZWF0dXJlcwRsaXN0B2Vs\nZW1lbnQVAhYEFkoWTiaQGjw2BAAZHBUAFQAVAgAAFsYyFRYW/igVHgAm3hocFQwZJQAGGUgIcHJvdG9jb2wOd3JpdGVyRmVhdHVy\nZXMEbGlzdAdlbGVtZW50FQIWBBZKFk4m3ho8NgQAGRwVABUAFQIAABbcMhUWFpwpFR4AJqwbHBUMGTUABggZKA5kb21haW5NZXRh\nZGF0YQZkb21haW4VAhYEFjwWQCasGzw2BAAZHBUAFQAVAgAAFvIyFRYWuikVHgAm7BscFQwZNQAGCBkoDmRvbWFpbk1ldGFkYXRh\nDWNvbmZpZ3VyYXRpb24VAhYEFjwWQCbsGzw2BAAZHBUAFQAVAgAAFogzFRYW2CkVHgAmrBwcFQAZNQAGCBkoDmRvbWFpbk1ldGFk\nYXRhB3JlbW92ZWQVAhYEFjwWQCasHDw2BAAZHBUAFQAVAgAAFp4zFRYW9ikVHgAWjBsWBCYIFuQcFAAAGVwYGW9yZy5hcGFjaGUu\nc3BhcmsudGltZVpvbmUYE0FtZXJpY2EvTG9zX0FuZ2VsZXMAGBxvcmcuYXBhY2hlLnNwYXJrLmxlZ2FjeUlOVDk2GAAAGBhvcmcu\nYXBhY2hlLnNwYXJrLnZlcnNpb24YBTQuMC4wABgpb3JnLmFwYWNoZS5zcGFyay5zcWwucGFycXVldC5yb3cubWV0YWRhdGEYiyV7\nInR5cGUiOiJzdHJ1Y3QiLCJmaWVsZHMiOlt7Im5hbWUiOiJ0eG4iLCJ0eXBlIjp7InR5cGUiOiJzdHJ1Y3QiLCJmaWVsZHMiOlt7\nIm5hbWUiOiJhcHBJZCIsInR5cGUiOiJzdHJpbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJ2ZXJz\naW9uIiwidHlwZSI6ImxvbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJsYXN0VXBkYXRlZCIsInR5\ncGUiOiJsb25nIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX1dfSwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0s\neyJuYW1lIjoiYWRkIiwidHlwZSI6eyJ0eXBlIjoic3RydWN0IiwiZmllbGRzIjpbeyJuYW1lIjoicGF0aCIsInR5cGUiOiJzdHJp\nbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJwYXJ0aXRpb25WYWx1ZXMiLCJ0eXBlIjp7InR5cGUi\nOiJtYXAiLCJrZXlUeXBlIjoic3RyaW5nIiwidmFsdWVUeXBlIjoic3RyaW5nIiwidmFsdWVDb250YWluc051bGwiOnRydWV9LCJu\ndWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJzaXplIiwidHlwZSI6ImxvbmciLCJudWxsYWJsZSI6dHJ1ZSwi\nbWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJtb2RpZmljYXRpb25UaW1lIiwidHlwZSI6ImxvbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0\nYWRhdGEiOnt9fSx7Im5hbWUiOiJkYXRhQ2hhbmdlIiwidHlwZSI6ImJvb2xlYW4iLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEi\nOnt9fSx7Im5hbWUiOiJ0YWdzIiwidHlwZSI6eyJ0eXBlIjoibWFwIiwia2V5VHlwZSI6InN0cmluZyIsInZhbHVlVHlwZSI6InN0\ncmluZyIsInZhbHVlQ29udGFpbnNOdWxsIjp0cnVlfSwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoiZGVs\nZXRpb25WZWN0b3IiLCJ0eXBlIjp7InR5cGUiOiJzdHJ1Y3QiLCJmaWVsZHMiOlt7Im5hbWUiOiJzdG9yYWdlVHlwZSIsInR5cGUi\nOiJzdHJpbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJwYXRoT3JJbmxpbmVEdiIsInR5cGUiOiJz\ndHJpbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJvZmZzZXQiLCJ0eXBlIjoiaW50ZWdlciIsIm51\nbGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6InNpemVJbkJ5dGVzIiwidHlwZSI6ImludGVnZXIiLCJudWxsYWJs\nZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJjYXJkaW5hbGl0eSIsInR5cGUiOiJsb25nIiwibnVsbGFibGUiOnRydWUs\nIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoibWF4Um93SW5kZXgiLCJ0eXBlIjoibG9uZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0\nYSI6e319XX0sIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6ImJhc2VSb3dJZCIsInR5cGUiOiJsb25nIiwi\nbnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoiZGVmYXVsdFJvd0NvbW1pdFZlcnNpb24iLCJ0eXBlIjoibG9u\nZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6InN0YXRzIiwidHlwZSI6InN0cmluZyIsIm51bGxhYmxl\nIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6InN0YXRzX3BhcnNlZCIsInR5cGUiOnsidHlwZSI6InN0cnVjdCIsImZpZWxk\ncyI6W3sibmFtZSI6Im51bVJlY29yZHMiLCJ0eXBlIjoibG9uZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319XX0sIm51\nbGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319XX0sIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6InJlbW92\nZSIsInR5cGUiOnsidHlwZSI6InN0cnVjdCIsImZpZWxkcyI6W3sibmFtZSI6InBhdGgiLCJ0eXBlIjoic3RyaW5nIiwibnVsbGFi\nbGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoiZGVsZXRpb25UaW1lc3RhbXAiLCJ0eXBlIjoibG9uZyIsIm51bGxhYmxl\nIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6ImRhdGFDaGFuZ2UiLCJ0eXBlIjoiYm9vbGVhbiIsIm51bGxhYmxlIjp0cnVl\nLCJtZXRhZGF0YSI6e319LHsibmFtZSI6ImV4dGVuZGVkRmlsZU1ldGFkYXRhIiwidHlwZSI6ImJvb2xlYW4iLCJudWxsYWJsZSI6\ndHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJwYXJ0aXRpb25WYWx1ZXMiLCJ0eXBlIjp7InR5cGUiOiJtYXAiLCJrZXlUeXBl\nIjoic3RyaW5nIiwidmFsdWVUeXBlIjoic3RyaW5nIiwidmFsdWVDb250YWluc051bGwiOnRydWV9LCJudWxsYWJsZSI6dHJ1ZSwi\nbWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJzaXplIiwidHlwZSI6ImxvbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7\nIm5hbWUiOiJkZWxldGlvblZlY3RvciIsInR5cGUiOnsidHlwZSI6InN0cnVjdCIsImZpZWxkcyI6W3sibmFtZSI6InN0b3JhZ2VU\neXBlIiwidHlwZSI6InN0cmluZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6InBhdGhPcklubGluZUR2\nIiwidHlwZSI6InN0cmluZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6Im9mZnNldCIsInR5cGUiOiJp\nbnRlZ2VyIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoic2l6ZUluQnl0ZXMiLCJ0eXBlIjoiaW50ZWdl\nciIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6ImNhcmRpbmFsaXR5IiwidHlwZSI6ImxvbmciLCJudWxs\nYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJtYXhSb3dJbmRleCIsInR5cGUiOiJsb25nIiwibnVsbGFibGUiOnRy\ndWUsIm1ldGFkYXRhIjp7fX1dfSwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoiYmFzZVJvd0lkIiwidHlw\nZSI6ImxvbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJkZWZhdWx0Um93Q29tbWl0VmVyc2lvbiIs\nInR5cGUiOiJsb25nIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX1dfSwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7\nfX0seyJuYW1lIjoibWV0YURhdGEiLCJ0eXBlIjp7InR5cGUiOiJzdHJ1Y3QiLCJmaWVsZHMiOlt7Im5hbWUiOiJpZCIsInR5cGUi\nOiJzdHJpbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJuYW1lIiwidHlwZSI6InN0cmluZyIsIm51\nbGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6ImRlc2NyaXB0aW9uIiwidHlwZSI6InN0cmluZyIsIm51bGxhYmxl\nIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6ImZvcm1hdCIsInR5cGUiOnsidHlwZSI6InN0cnVjdCIsImZpZWxkcyI6W3si\nbmFtZSI6InByb3ZpZGVyIiwidHlwZSI6InN0cmluZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFtZSI6Im9w\ndGlvbnMiLCJ0eXBlIjp7InR5cGUiOiJtYXAiLCJrZXlUeXBlIjoic3RyaW5nIiwidmFsdWVUeXBlIjoic3RyaW5nIiwidmFsdWVD\nb250YWluc051bGwiOnRydWV9LCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fV19LCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRh\ndGEiOnt9fSx7Im5hbWUiOiJzY2hlbWFTdHJpbmciLCJ0eXBlIjoic3RyaW5nIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7\nfX0seyJuYW1lIjoicGFydGl0aW9uQ29sdW1ucyIsInR5cGUiOnsidHlwZSI6ImFycmF5IiwiZWxlbWVudFR5cGUiOiJzdHJpbmci\nLCJjb250YWluc051bGwiOnRydWV9LCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5hbWUiOiJjb25maWd1cmF0aW9u\nIiwidHlwZSI6eyJ0eXBlIjoibWFwIiwia2V5VHlwZSI6InN0cmluZyIsInZhbHVlVHlwZSI6InN0cmluZyIsInZhbHVlQ29udGFp\nbnNOdWxsIjp0cnVlfSwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoiY3JlYXRlZFRpbWUiLCJ0eXBlIjoi\nbG9uZyIsIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319XX0sIm51bGxhYmxlIjp0cnVlLCJtZXRhZGF0YSI6e319LHsibmFt\nZSI6InByb3RvY29sIiwidHlwZSI6eyJ0eXBlIjoic3RydWN0IiwiZmllbGRzIjpbeyJuYW1lIjoibWluUmVhZGVyVmVyc2lvbiIs\nInR5cGUiOiJpbnRlZ2VyIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoibWluV3JpdGVyVmVyc2lvbiIs\nInR5cGUiOiJpbnRlZ2VyIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoicmVhZGVyRmVhdHVyZXMiLCJ0\neXBlIjp7InR5cGUiOiJhcnJheSIsImVsZW1lbnRUeXBlIjoic3RyaW5nIiwiY29udGFpbnNOdWxsIjp0cnVlfSwibnVsbGFibGUi\nOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoid3JpdGVyRmVhdHVyZXMiLCJ0eXBlIjp7InR5cGUiOiJhcnJheSIsImVsZW1l\nbnRUeXBlIjoic3RyaW5nIiwiY29udGFpbnNOdWxsIjp0cnVlfSwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX1dfSwibnVs\nbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0seyJuYW1lIjoiZG9tYWluTWV0YWRhdGEiLCJ0eXBlIjp7InR5cGUiOiJzdHJ1Y3Qi\nLCJmaWVsZHMiOlt7Im5hbWUiOiJkb21haW4iLCJ0eXBlIjoic3RyaW5nIiwibnVsbGFibGUiOnRydWUsIm1ldGFkYXRhIjp7fX0s\neyJuYW1lIjoiY29uZmlndXJhdGlvbiIsInR5cGUiOiJzdHJpbmciLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fSx7Im5h\nbWUiOiJyZW1vdmVkIiwidHlwZSI6ImJvb2xlYW4iLCJudWxsYWJsZSI6dHJ1ZSwibWV0YWRhdGEiOnt9fV19LCJudWxsYWJsZSI6\ndHJ1ZSwibWV0YWRhdGEiOnt9fV19ABgfb3JnLmFwYWNoZS5zcGFyay5sZWdhY3lEYXRlVGltZRgAABhacGFycXVldC1tciB2ZXJz\naW9uIDEuMTIuMy1kYXRhYnJpY2tzLTAwMDIgKGJ1aWxkIDI0ODRhOTVkYmUxNmEwMDIzZTNlYjI5YzIwMWY5OWZmOWVhNzcxZWUp\nGfw2HAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAA\nHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAAHAAA\nHAAAHAAAHAAAHAAAHAAAAJUqAABQQVIx\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n\n  // Pre-prepare the byte array for (minVersion-1).checkpoint.parquet.\n  val FAKE_CHECKPOINT_BYTE_ARRAY = {\n    java.util.Base64.getDecoder.decode(FAKE_CHECKPOINT_FILE_BASE64_ENCODED_STRING)\n  }\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/PrepareDeltaSharingScan.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport org.apache.spark.sql.delta.{DeltaTableUtils => SqlDeltaTableUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.{PreparedDeltaFileIndex, PrepareDeltaScan}\nimport io.delta.sharing.client.util.ConfUtils\nimport io.delta.sharing.spark.DeltaSharingFileIndex\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical._\n\n/**\n * Before query planning, we prepare any scans over delta sharing tables by pushing\n * any filters or limits to delta sharing server through RPC, allowing us to return only needed\n * files and gather more accurate statistics for CBO and metering.\n */\nclass PrepareDeltaSharingScan(override val spark: SparkSession) extends PrepareDeltaScan(spark) {\n\n  /**\n   * Prepares delta sharing scans sequentially.\n   */\n  override protected def prepareDeltaScan(plan: LogicalPlan): LogicalPlan = {\n    transformWithSubqueries(plan) {\n      case scan @ DeltaSharingTableScan(_, filters, dsFileIndex, limit, _) =>\n        val partitionCols = dsFileIndex.partitionColumns\n        val (partitionFilters, dataFilters) = filters.partition { e =>\n          SqlDeltaTableUtils.isPredicatePartitionColumnsOnly(e, partitionCols, spark)\n        }\n        logInfo(s\"Classified filters: partition: $partitionFilters, data: $dataFilters, \" +\n          s\"limit: $limit.\")\n        val deltaLog = dsFileIndex.fetchFilesAndConstructDeltaLog(\n          partitionFilters,\n          dataFilters,\n          limit.map(_.toLong)\n        )\n        val snapshot = deltaLog.snapshot\n        val deltaScan = limit match {\n          case Some(limit) => snapshot.filesForScan(limit, filters)\n          case _ => snapshot.filesForScan(filters)\n        }\n        val preparedIndex = PreparedDeltaFileIndex(\n          spark,\n          deltaLog,\n          deltaLog.dataPath,\n          catalogTableOpt = None,\n          preparedScan = deltaScan,\n          versionScanned = Some(snapshot.version)\n        )\n        SqlDeltaTableUtils.replaceFileIndex(scan, preparedIndex)\n    }\n  }\n\n  // Just return the plan if statistics based skipping is off.\n  // It will fall back to just partition pruning at planning time.\n  // When data skipping is disabled, just convert Delta sharing scans to normal tahoe scans.\n  // NOTE: File skipping is only disabled on the client, so we still pass filters to the server.\n  override protected def prepareDeltaScanWithoutFileSkipping(plan: LogicalPlan): LogicalPlan = {\n    plan.transformDown {\n      case scan@DeltaSharingTableScan(_, filters, sharingIndex, _, _) =>\n        val partitionCols = sharingIndex.partitionColumns\n        val (partitionFilters, dataFilters) = filters.partition { e =>\n          SqlDeltaTableUtils.isPredicatePartitionColumnsOnly(e, partitionCols, spark)\n        }\n        logInfo(s\"Classified filters: partition: $partitionFilters, data: $dataFilters\")\n        val fileIndex = sharingIndex.asTahoeFileIndex(partitionFilters, dataFilters)\n        SqlDeltaTableUtils.replaceFileIndex(scan, fileIndex)\n    }\n  }\n\n  // TODO: Support metadata-only query optimization!\n  override def optimizeQueryWithMetadata(plan: LogicalPlan): LogicalPlan = plan\n\n  /**\n   * This is an extractor object. See https://docs.scala-lang.org/tour/extractor-objects.html.\n   */\n  object DeltaSharingTableScan extends DeltaTableScan[DeltaSharingFileIndex] {\n    // Since delta library is used to read the data on constructed delta log, this should also\n    // consider the spark config for delta limit pushdown.\n    override def limitPushdownEnabled(plan: LogicalPlan): Boolean =\n      ConfUtils.limitPushdownEnabled(plan.conf) &&\n        (spark.conf.get(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key) == \"true\")\n\n    override def getPartitionColumns(fileIndex: DeltaSharingFileIndex): Seq[String] =\n      fileIndex.partitionColumns\n\n    override def getPartitionFilters(fileIndex: DeltaSharingFileIndex): Seq[Expression] =\n      Seq.empty[Expression]\n\n  }\n}\n"
  },
  {
    "path": "sharing/src/main/scala/io/delta/sharing/spark/model.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark.model\n\nimport java.net.URLEncoder\n\nimport org.apache.spark.sql.delta.actions.{\n  AddCDCFile,\n  AddFile,\n  DeletionVectorDescriptor,\n  FileAction,\n  Metadata,\n  Protocol,\n  RemoveFile,\n  SingleAction\n}\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.annotation._\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport io.delta.sharing.client.DeltaSharingFileSystem\n\nimport org.apache.spark.sql.types.{DataType, StructType}\n\n// Represents a single action in the response of a Delta Sharing rpc.\nsealed trait DeltaSharingAction {\n  def wrap: DeltaSharingSingleAction\n  def json: String = JsonUtils.toJson(wrap)\n}\n\n/** A serialization helper to create a common action envelope, for delta sharing actions in the\n * response of a rpc.\n */\ncase class DeltaSharingSingleAction(\n    protocol: DeltaSharingProtocol = null,\n    metaData: DeltaSharingMetadata = null,\n    file: DeltaSharingFileAction = null) {\n  def unwrap: DeltaSharingAction = {\n    if (file != null) {\n      file\n    } else if (metaData != null) {\n      metaData\n    } else if (protocol != null) {\n      protocol\n    } else {\n      null\n    }\n  }\n}\n\n/**\n * The delta sharing protocol from the response of a rpc. It only wraps a delta protocol now, but\n * can be extended with additional delta sharing fields if needed later.\n */\ncase class DeltaSharingProtocol(deltaProtocol: Protocol) extends DeltaSharingAction {\n\n  override def wrap: DeltaSharingSingleAction = DeltaSharingSingleAction(protocol = this)\n}\n\n/**\n * The delta sharing metadata from the response of a rpc.\n * It wraps a delta metadata, and adds three delta sharing fields:\n *     - version: the version of the metadata, used to generate faked delta log file on the client\n *                side.\n *     - size: the estimated size of the table at the version, used to estimate query size.\n *     - numFiles: the number of files of the table at the version, used to estimate query size.\n */\ncase class DeltaSharingMetadata(\n    version: java.lang.Long = null,\n    size: java.lang.Long = null,\n    numFiles: java.lang.Long = null,\n    deltaMetadata: Metadata)\n    extends DeltaSharingAction {\n\n  /** Returns the schema as a [[StructType]] */\n  @JsonIgnore\n  lazy val schema: StructType = deltaMetadata.schema\n\n  /** Returns the partitionSchema as a [[StructType]] */\n  @JsonIgnore\n  lazy val partitionSchema: StructType = deltaMetadata.partitionSchema\n\n  override def wrap: DeltaSharingSingleAction = DeltaSharingSingleAction(metaData = this)\n}\n\n/**\n * DeltaResponseFileAction used in delta sharing protocol. It wraps a delta single action,\n *   and adds 4 delta sharing related fields: id/version/timestamp/expirationTimestamp.\n *       - id: used to uniquely identify a file, and in idToUrl mapping for executor to get\n *             presigned url.\n *       - version/timestamp: the version and timestamp of the commit, used to generate faked delta\n *                            log file on the client side.\n *       - expirationTimestamp: indicate when the presigned url is going to expire and need a\n *                              refresh.\n *   The server is responsible to redact sensitive fields such as \"tags\" before returning.\n */\ncase class DeltaSharingFileAction(\n    id: String,\n    version: java.lang.Long = null,\n    timestamp: java.lang.Long = null,\n    expirationTimestamp: java.lang.Long = null,\n    deletionVectorFileId: String = null,\n    deltaSingleAction: SingleAction)\n    extends DeltaSharingAction {\n\n  lazy val path: String = {\n    deltaSingleAction.unwrap match {\n      case file: FileAction => file.path\n      case action =>\n        throw new IllegalStateException(\n          s\"unexpected action in delta sharing \" +\n          s\"response: ${action.json}\"\n        )\n    }\n  }\n\n  lazy val size: Long = {\n    deltaSingleAction.unwrap match {\n      case add: AddFile => add.size\n      case cdc: AddCDCFile => cdc.size\n      case remove: RemoveFile =>\n        remove.size.getOrElse {\n          throw new IllegalStateException(\n            \"size is missing for the remove file returned from server\" +\n            s\", which is required by delta sharing client, response:${remove.json}.\"\n          )\n        }\n      case action =>\n        throw new IllegalStateException(\n          s\"unexpected action in delta sharing \" +\n          s\"response: ${action.json}\"\n        )\n    }\n  }\n\n  def getDeletionVectorOpt: Option[DeletionVectorDescriptor] = {\n    deltaSingleAction.unwrap match {\n      case file: FileAction => Option.apply(file.deletionVector)\n      case _ => None\n    }\n  }\n\n  def getDeletionVectorDeltaSharingPath(tablePath: String): String = {\n    getDeletionVectorOpt.map { deletionVector =>\n      // Adding offset to dvFileSize so it can load all needed bytes in memory,\n      // starting from the beginning of the file instead of the `offset`.\n      // There could be other DVs beyond this length in the file, but not needed by this DV.\n      val dvFileSize = DeletionVectorStore.getTotalSizeOfDVFieldsInFile(\n        deletionVector.sizeInBytes\n      ) + deletionVector.offset.getOrElse(0)\n      // This path is going to be put in the delta log file and processed by delta code, where\n      // absolutePath() is applied to the path in all places, such as TahoeFileIndex and\n      // DeletionVectorDescriptor, and in absolutePath, URI will apply a decode of the path.\n      // Additional encoding on the tablePath and table id to allow the path still able to be\n      // processed by DeltaSharingFileSystem after URI decodes it.\n      DeltaSharingFileSystem\n        .DeltaSharingPath(\n          URLEncoder.encode(tablePath, \"UTF-8\"),\n          URLEncoder.encode(deletionVectorFileId, \"UTF-8\"),\n          dvFileSize\n        )\n        .toPath\n        .toString\n    }.orNull\n  }\n\n  /**\n   * A helper function to get the delta sharing path for this file action to put in delta log,\n   * in the format below:\n   * ```\n   * delta-sharing:///<url encoded table path>/<url encoded file id>/<size>\n   * ```\n   *\n   * This is to make a unique and unchanged path for each file action, which will be mapped to\n   * pre-signed url by DeltaSharingFileSystem.open(). size is needed to know how much bytes to read\n   * from the FSDataInputStream.\n   */\n  def getDeltaSharingPath(tablePath: String): String = {\n    // This path is going to be put in the delta log file and processed by delta code, where\n    // absolutePath() is applied to the path in all places, such as TahoeFileIndex and\n    // DeletionVectorDescriptor, and in absolutePath, URI will apply a decode of the path.\n    // Additional encoding on the tablePath and table id to allow the path still able to be\n    // processed by DeltaSharingFileSystem after URI decodes it.\n    DeltaSharingFileSystem\n      .DeltaSharingPath(\n        URLEncoder.encode(tablePath, \"UTF-8\"),\n        URLEncoder.encode(id, \"UTF-8\"),\n        size\n      )\n      .toPath\n      .toString\n  }\n\n  override def wrap: DeltaSharingSingleAction = DeltaSharingSingleAction(file = this)\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaFormatSharingSourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.time.LocalDateTime\n\nimport org.apache.spark.sql.delta.{DeltaIllegalStateException, DeltaLog}\nimport org.apache.spark.sql.delta.DeltaOptions.{\n  IGNORE_CHANGES_OPTION,\n  IGNORE_DELETES_OPTION,\n  SKIP_CHANGE_COMMITS_OPTION\n}\nimport org.apache.spark.sql.delta.sources.{DeltaSourceOffset, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport io.delta.sharing.client.DeltaSharingRestClient\nimport io.delta.sharing.client.model.{Table => DeltaSharingTable}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport io.delta.sharing.spark.test.shims.SharingStreamingTestShims.{\n  CheckpointFileManager,\n  CommitMetadata,\n  SerializedOffset,\n  StreamingCheckpointConstants,\n  StreamMetadata\n}\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.streaming.{StreamingQuery, StreamingQueryException, StreamTest}\nimport org.apache.spark.sql.types.{\n  DateType,\n  IntegerType,\n  LongType,\n  StringType,\n  StructType,\n  TimestampType\n}\n\nclass DeltaFormatSharingSourceSuite\n    extends StreamTest\n    with DeltaSQLCommandTest\n    with DeltaSharingTestSparkUtils\n    with DeltaSharingDataSourceDeltaTestUtils {\n\n  import testImplicits._\n\n  private def getSource(parameters: Map[String, String]): DeltaFormatSharingSource = {\n    val options = new DeltaSharingOptions(parameters)\n    val path = options.options.getOrElse(\n      \"path\",\n      throw DeltaSharingErrors.pathNotSpecifiedException\n    )\n    val parsedPath = DeltaSharingRestClient.parsePath(path, Map.empty)\n    val client = DeltaSharingRestClient(\n      profileFile = parsedPath.profileFile,\n      shareCredentialsOptions = Map.empty,\n      forStreaming = true,\n      responseFormat = \"delta\",\n      readerFeatures = DeltaSharingUtils.STREAMING_SUPPORTED_READER_FEATURES.mkString(\",\")\n    )\n    val dsTable = DeltaSharingTable(\n      share = parsedPath.share,\n      schema = parsedPath.schema,\n      name = parsedPath.table\n    )\n    DeltaFormatSharingSource(\n      spark = spark,\n      client = client,\n      table = dsTable,\n      options = options,\n      parameters = parameters,\n      sqlConf = sqlContext.sparkSession.sessionState.conf,\n      metadataPath = \"\"\n    )\n  }\n\n  private def assertBlocksAreCleanedUp(): Unit = {\n    val blockManager = SparkEnv.get.blockManager\n    val matchingBlockIds = blockManager.getMatchingBlockIds(\n      _.name.startsWith(DeltaSharingLogFileSystem.DELTA_SHARING_LOG_BLOCK_ID_PREFIX)\n    )\n    assert(matchingBlockIds.isEmpty, \"delta sharing blocks are not cleaned up.\")\n  }\n\n  private def cleanUpDeltaSharingBlocks(): Unit = {\n    val blockManager = SparkEnv.get.blockManager\n    val matchingBlockIds = blockManager.getMatchingBlockIds(\n      _.name.startsWith(\n        DeltaSharingLogFileSystem.DELTA_SHARING_LOG_BLOCK_ID_PREFIX)\n    )\n    matchingBlockIds.foreach(blockManager.removeBlock(_))\n  }\n\n  test(\"DeltaFormatSharingSource able to get schema\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_schema\"\n      withTable(deltaTableName) {\n        createTable(deltaTableName)\n        val sharedTableName = \"shared_table_schema\"\n        prepareMockedClientMetadata(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        val profileFile = prepareProfileFile(tempDir)\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val deltaSharingSource = getSource(\n            Map(\"path\" -> s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n          )\n          val expectedSchema: StructType = new StructType()\n            .add(\"c1\", IntegerType)\n            .add(\"c2\", StringType)\n            .add(\"c3\", DateType)\n            .add(\"c4\", TimestampType)\n          assert(deltaSharingSource.schema == expectedSchema)\n\n          // CDF schema\n          val cdfDeltaSharingSource = getSource(\n            Map(\n              \"path\" -> s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\",\n              \"readChangeFeed\" -> \"true\"\n            )\n          )\n          val expectedCdfSchema: StructType = expectedSchema\n            .copy()\n            .add(\"_change_type\", StringType)\n            .add(\"_commit_version\", LongType)\n            .add(\"_commit_timestamp\", TimestampType)\n          assert(cdfDeltaSharingSource.schema == expectedCdfSchema)\n        }\n      }\n    }\n  }\n\n  test(\"DeltaFormatSharingSource do not support cdc\") {\n    withTempDir { tempDir =>\n      val sharedTableName = \"shared_streaming_table_nocdc\"\n      val profileFile = prepareProfileFile(tempDir)\n      withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n        val e = intercept[Exception] {\n          val df = spark.readStream\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .load(tablePath)\n          testStream(df)(\n            AssertOnQuery { q =>\n              q.processAllAvailable(); true\n            }\n          )\n        }\n        assert(e.getMessage.contains(\"Delta sharing cdc streaming is not supported\"))\n      }\n    }\n  }\n\n  test(\"DeltaFormatSharingSource getTableVersion error\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_version_error\"\n      withTable(deltaTableName) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $deltaTableName (value STRING)\n             |USING DELTA\n             |\"\"\".stripMargin)\n        val sharedTableName = \"shared_streaming_table_version_error\"\n        val profileFile = prepareProfileFile(tempDir)\n        prepareMockedClientMetadata(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName, Some(-1L))\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n          val e = intercept[Exception] {\n            val df = spark.readStream\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n            testStream(df)(\n              AssertOnQuery { q =>\n                q.processAllAvailable(); true\n              }\n            )\n          }\n          assert(\n            e.getMessage.contains(\"Delta Sharing Server returning negative table version:-1,\")\n          )\n        }\n      }\n    }\n  }\n\n  // Test forceToDeltaSourceOffset directly: pass DeltaSharingSourceOffset JSON, call util.\n  // Source construction requires getMetadata; use a real delta table and prepare mocks for\n  // shared table \"some_table\". Flag on -> (DeltaSourceOffset, true); flag off -> throw.\n  Seq(true, false).foreach { case autoResolve: Boolean =>\n    test(s\"forceToDeltaSourceOffset: DeltaSharingSourceOffset JSON with flag \" +\n      s\"autoResolve=$autoResolve\") {\n      withTempDir { tempDir =>\n        val deltaTableName = \"delta_table_util_offset\"\n        withTable(deltaTableName) {\n          createTable(deltaTableName)\n          val sharedTableName = \"some_table\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          val profileFile = prepareProfileFile(tempDir)\n          val tableId = \"test-table-id\"\n          val autoResolveKey = DeltaSQLConf\n            .DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT\n            .key\n          withSQLConf(\n            (getDeltaSharingClassesSQLConf ++ Seq(\n              autoResolveKey -> autoResolve.toString\n            )).toSeq: _*\n          ) {\n            val source = getSource(\n              Map(\"path\" -> s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n            )\n            val tableIdField = source.getClass.getDeclaredField(\"tableId\")\n            tableIdField.setAccessible(true)\n            tableIdField.set(source, tableId)\n            val legacyJson = \"{\\\"sourceVersion\\\":1,\" +\n              s\"\"\"\"tableId\":\"$tableId\",\"\"\" +\n              \"\\\"tableVersion\\\":1,\" +\n              \"\\\"index\\\":-1,\" +\n              \"\\\"isStartingVersion\\\":true}\"\n            val serializedOffset = SerializedOffset(legacyJson)\n            if (autoResolve) {\n              val (deltaOffset, fromLegacy) = source.forceToDeltaSourceOffset(serializedOffset)\n              assert(fromLegacy, \"fromLegacy should be true for DeltaSharingSourceOffset JSON\")\n              assert(deltaOffset.reservoirId === tableId)\n              assert(deltaOffset.reservoirVersion === 1L)\n              assert(deltaOffset.index === DeltaSourceOffset.BASE_INDEX)\n              assert(deltaOffset.isInitialSnapshot)\n            } else {\n              intercept[Exception](source.forceToDeltaSourceOffset(serializedOffset))\n            }\n            cleanUpDeltaSharingBlocks()\n          }\n        }\n      }\n    }\n  }\n\n  // E2E: Custom checkpoint with legacy DeltaSharingSourceOffset format;\n  // restart with delta streaming using that checkpoint.\n  // Flag on/off. Mocks use delta table only.\n  Seq(\n    (true, \"flag on: restart with delta succeeds\"),\n    (false, \"flag off: restart fails parsing legacy checkpoint\")\n  ).foreach { case (autoResolve, desc) =>\n    test(s\"E2E: parquet streaming checkpoint then restart \" +\n      s\"with delta streaming [$desc]\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_e2e_parquet_then_delta\"\n      withTable(deltaTableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $deltaTableName (value STRING)\n               |USING DELTA\n               |\"\"\".stripMargin)\n        sql(s\"INSERT INTO $deltaTableName VALUES ('p1'), ('p2')\")\n        val tableId = DeltaLog.forTable(spark, new TableIdentifier(deltaTableName))\n          .update().metadata.id\n        val sharedTableName = \"shared_streaming_table_e2e\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n        spark.sessionState.conf.setConfString(\n          \"spark.delta.sharing.streaming.queryTableVersionIntervalSeconds\",\n          \"10s\"\n        )\n\n        // Build custom checkpoint with legacy DeltaSharingSourceOffset (no parquet stream run).\n        val checkpointPath = new Path(checkpointDir.getCanonicalPath)\n        // scalastyle:off deltahadoopconfiguration\n        val hadoopConf = spark.sessionState.newHadoopConf()\n        // scalastyle:on deltahadoopconfiguration\n        val fileManager = CheckpointFileManager.create(checkpointPath, hadoopConf)\n        val offsetsDir = StreamingCheckpointConstants.DIR_NAME_OFFSETS\n        val commitsDir = StreamingCheckpointConstants.DIR_NAME_COMMITS\n        val metaDir = StreamingCheckpointConstants.DIR_NAME_METADATA\n        fileManager.mkdirs(new Path(checkpointPath, offsetsDir))\n        fileManager.mkdirs(new Path(checkpointPath, commitsDir))\n        val metadataPath = new Path(checkpointPath, metaDir)\n        val streamId = java.util.UUID.randomUUID.toString\n        StreamMetadata.write(\n          StreamMetadata(streamId), metadataPath, hadoopConf)\n        val legacyOffsetJson =\n          \"{\\\"sourceVersion\\\":1,\" +\n            s\"\"\"\"tableId\":\"$tableId\",\"\"\" +\n            \"\\\"tableVersion\\\":1,\" +\n            \"\\\"index\\\":-1,\" +\n            \"\\\"isStartingVersion\\\":true}\"\n        val offsetMetadataJson =\n          \"\"\"{\"batchWatermarkMs\":0,\"\"\" +\n            \"\"\"\"batchTimestampMs\":0,\"\"\" +\n            \"\"\"\"conf\":{},\"\"\" +\n            \"\"\"\"sourceMetadataInfo\":{}}\"\"\"\n        val offsetContent =\n          s\"v1\\n$offsetMetadataJson\\n$legacyOffsetJson\"\n            .getBytes(java.nio.charset.StandardCharsets.UTF_8)\n        val offsetBatchPath = new Path(\n          new Path(checkpointPath, StreamingCheckpointConstants.DIR_NAME_OFFSETS), \"0\")\n        val offsetOut = fileManager.createAtomic(offsetBatchPath, overwriteIfPossible = true)\n        offsetOut.write(offsetContent)\n        offsetOut.close()\n        val commitContent = s\"v1\\n${CommitMetadata(0).json}\"\n          .getBytes(java.nio.charset.StandardCharsets.UTF_8)\n        val commitBatchPath = new Path(\n          new Path(checkpointPath, StreamingCheckpointConstants.DIR_NAME_COMMITS), \"0\")\n        val commitOut = fileManager.createAtomic(commitBatchPath, overwriteIfPossible = true)\n        commitOut.write(commitContent)\n        commitOut.close()\n\n        val autoResolveKey = DeltaSQLConf\n          .DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT\n          .key\n        withSQLConf(\n          (getDeltaSharingClassesSQLConf ++ Seq(\n            autoResolveKey -> autoResolve.toString\n          )).toSeq: _*\n        ) {\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          // Snapshot getFiles(versionAsOf=1) for initial batch when resuming from legacy offset\n          prepareMockedClientAndFileSystemResult(\n            deltaTableName, sharedTableName, versionAsOf = Some(1L))\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName, sharedTableName, 1L, 1L)\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n          if (autoResolve) {\n            val q = spark.readStream\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .writeStream\n              .format(\"delta\")\n              .option(\"checkpointLocation\", checkpointDir.toString)\n              .start(outputDir.toString)\n            try {\n              q.processAllAvailable()\n            } finally {\n              q.stop()\n            }\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"p1\", \"p2\").toDF())\n          } else {\n            var q: StreamingQuery = null\n            val e = intercept[Exception] {\n              q = spark.readStream\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .writeStream\n                .format(\"delta\")\n                .option(\"checkpointLocation\", checkpointDir.toString)\n                .start(outputDir.toString)\n              try {\n                q.processAllAvailable()\n              } finally {\n                if (q != null) q.stop()\n              }\n            }\n            assert(e.getMessage != null && (\n              e.getMessage.contains(\"legacy\") || e.getMessage.contains(\"checkpoint\") ||\n              e.getCause != null && (e.getCause.getMessage.contains(\"legacy\") ||\n                e.getCause.getMessage.contains(\"checkpoint\"))),\n              s\"Expected legacy/checkpoint-related error, got: $e\")\n          }\n        }\n      }\n    }\n    }\n  }\n\n  // E2E: Legacy checkpoint with isStartingVersion=false (incremental\n  // mode). The stream already processed through version 2, so on\n  // restart it should pick up version 3 data.\n  Seq(\n    (true, \"flag on: restart succeeds\"),\n    (false, \"flag off: restart fails parsing legacy checkpoint\")\n  ).foreach { case (autoResolve, desc) =>\n    test(s\"E2E: legacy checkpoint isStartingVersion=false \" +\n      s\"then restart with delta streaming [$desc]\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName =\n        \"delta_table_e2e_not_starting_version\"\n      withTable(deltaTableName) {\n        sql(s\"\"\"CREATE TABLE $deltaTableName (value STRING)\n               |USING DELTA\"\"\".stripMargin)\n        sql(\n          s\"INSERT INTO $deltaTableName VALUES ('p1'), ('p2')\")\n        sql(\n          s\"INSERT INTO $deltaTableName VALUES ('p3'), ('p4')\")\n        sql(\n          s\"INSERT INTO $deltaTableName VALUES ('p5'), ('p6')\")\n        val tableId = DeltaLog.forTable(\n          spark, new TableIdentifier(deltaTableName))\n          .update().metadata.id\n        val sharedTableName =\n          \"shared_streaming_table_e2e_nsv\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath +\n          s\"#share1.default.$sharedTableName\"\n        spark.sessionState.conf.setConfString(\n          \"spark.delta.sharing.streaming\" +\n            \".queryTableVersionIntervalSeconds\",\n          \"10s\"\n        )\n\n        // Two committed batches so that populateStartOffsets\n        // calls getBatch(offset_0, offset_1) with a valid\n        // startOffset instead of None.\n        val checkpointPath =\n          new Path(checkpointDir.getCanonicalPath)\n        // scalastyle:off deltahadoopconfiguration\n        val hadoopConf = spark.sessionState.newHadoopConf()\n        // scalastyle:on deltahadoopconfiguration\n        val fileManager =\n          CheckpointFileManager.create(checkpointPath, hadoopConf)\n        val offsetsDir =\n          StreamingCheckpointConstants.DIR_NAME_OFFSETS\n        val commitsDir =\n          StreamingCheckpointConstants.DIR_NAME_COMMITS\n        val metaDir =\n          StreamingCheckpointConstants.DIR_NAME_METADATA\n        fileManager.mkdirs(\n          new Path(checkpointPath, offsetsDir))\n        fileManager.mkdirs(\n          new Path(checkpointPath, commitsDir))\n        val metadataPath =\n          new Path(checkpointPath, metaDir)\n        val streamId = java.util.UUID.randomUUID.toString\n        StreamMetadata.write(\n          StreamMetadata(streamId), metadataPath, hadoopConf)\n        val offsetMetadataJson =\n          \"\"\"{\"batchWatermarkMs\":0,\"\"\" +\n            \"\"\"\"batchTimestampMs\":0,\"\"\" +\n            \"\"\"\"conf\":{},\"\"\" +\n            \"\"\"\"sourceMetadataInfo\":{}}\"\"\"\n\n        // Batch 0: legacy offset at version 1\n        val legacyOffset0Json =\n          \"{\\\"sourceVersion\\\":1,\" +\n            s\"\"\"\"tableId\":\"$tableId\",\"\"\" +\n            \"\\\"tableVersion\\\":1,\" +\n            \"\\\"index\\\":-1,\" +\n            \"\\\"isStartingVersion\\\":false}\"\n        val offset0Content =\n          s\"v1\\n$offsetMetadataJson\\n$legacyOffset0Json\"\n            .getBytes(java.nio.charset.StandardCharsets.UTF_8)\n        val offset0Path = new Path(new Path(\n          checkpointPath, offsetsDir), \"0\")\n        val offset0Out = fileManager.createAtomic(\n          offset0Path, overwriteIfPossible = true)\n        offset0Out.write(offset0Content)\n        offset0Out.close()\n        val commit0Content =\n          s\"v1\\n${CommitMetadata(0).json}\"\n            .getBytes(java.nio.charset.StandardCharsets.UTF_8)\n        val commit0Path = new Path(new Path(\n          checkpointPath, commitsDir), \"0\")\n        val commit0Out = fileManager.createAtomic(\n          commit0Path, overwriteIfPossible = true)\n        commit0Out.write(commit0Content)\n        commit0Out.close()\n\n        // Batch 1: legacy offset at version 2\n        val legacyOffset1Json =\n          \"{\\\"sourceVersion\\\":1,\" +\n            s\"\"\"\"tableId\":\"$tableId\",\"\"\" +\n            \"\\\"tableVersion\\\":2,\" +\n            \"\\\"index\\\":-1,\" +\n            \"\\\"isStartingVersion\\\":false}\"\n        val offset1Content =\n          s\"v1\\n$offsetMetadataJson\\n$legacyOffset1Json\"\n            .getBytes(java.nio.charset.StandardCharsets.UTF_8)\n        val offset1Path = new Path(new Path(\n          checkpointPath, offsetsDir), \"1\")\n        val offset1Out = fileManager.createAtomic(\n          offset1Path, overwriteIfPossible = true)\n        offset1Out.write(offset1Content)\n        offset1Out.close()\n        val commit1Content =\n          s\"v1\\n${CommitMetadata(1).json}\"\n            .getBytes(java.nio.charset.StandardCharsets.UTF_8)\n        val commit1Path = new Path(new Path(\n          checkpointPath, commitsDir), \"1\")\n        val commit1Out = fileManager.createAtomic(\n          commit1Path, overwriteIfPossible = true)\n        commit1Out.write(commit1Content)\n        commit1Out.close()\n\n        val autoResolveKey = DeltaSQLConf\n          .DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT\n          .key\n        withSQLConf(\n          (getDeltaSharingClassesSQLConf ++ Seq(\n            autoResolveKey -> autoResolve.toString\n          )).toSeq: _*\n        ) {\n          prepareMockedClientMetadata(\n            deltaTableName, sharedTableName)\n          // getBatch(offset_0, offset_1) uses offset_0 as\n          // startingOffset (isInitialSnapshot=false) so the\n          // streaming API is used from version 1 to 3.\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName, sharedTableName, 1L, 3L)\n          prepareMockedClientGetTableVersion(\n            deltaTableName, sharedTableName)\n\n          if (autoResolve) {\n            val q = spark.readStream\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .writeStream\n              .format(\"delta\")\n              .option(\"checkpointLocation\",\n                checkpointDir.toString)\n              .start(outputDir.toString)\n            try {\n              q.processAllAvailable()\n            } finally {\n              q.stop()\n            }\n            checkAnswer(\n              spark.read.format(\"delta\")\n                .load(outputDir.getCanonicalPath),\n              Seq(\"p3\", \"p4\", \"p5\", \"p6\").toDF())\n          } else {\n            var q: StreamingQuery = null\n            val e = intercept[Exception] {\n              q = spark.readStream\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .writeStream\n                .format(\"delta\")\n                .option(\"checkpointLocation\",\n                  checkpointDir.toString)\n                .start(outputDir.toString)\n              try {\n                q.processAllAvailable()\n              } finally {\n                if (q != null) q.stop()\n              }\n            }\n            assert(e.getMessage != null && (\n              e.getMessage.contains(\"legacy\") ||\n              e.getMessage.contains(\"checkpoint\") ||\n              e.getCause != null && (\n                e.getCause.getMessage.contains(\"legacy\") ||\n                e.getCause.getMessage\n                  .contains(\"checkpoint\"))),\n              s\"Expected legacy/checkpoint error, got: $e\")\n          }\n        }\n      }\n    }\n    }\n  }\n\n  test(\"DeltaFormatSharingSource simple query works\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_simple\"\n      withTable(deltaTableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $deltaTableName (value STRING)\n               |USING DELTA\n               |\"\"\".stripMargin)\n\n        val sharedTableName = \"shared_streaming_table_simple\"\n        prepareMockedClientMetadata(deltaTableName, sharedTableName)\n\n        val profileFile = prepareProfileFile(tempDir)\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n          def InsertToDeltaTable(values: String): Unit = {\n            sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n          }\n\n          InsertToDeltaTable(\"\"\"(\"keep1\"), (\"keep2\"), (\"drop3\")\"\"\")\n          prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName, Some(1L))\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n          val df = spark.readStream\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .load(tablePath)\n            .filter($\"value\" contains \"keep\")\n\n          spark.sessionState.conf.setConfString(\n            \"spark.delta.sharing.streaming.queryTableVersionIntervalSeconds\",\n            \"9s\"\n          )\n          val e = intercept[Exception] {\n            testStream(df)(\n              AssertOnQuery { q =>\n                q.processAllAvailable(); true\n              }\n            )\n          }\n          assert(e.getMessage.contains(\"must not be less than 10 seconds\"))\n\n          spark.sessionState.conf.setConfString(\n            \"spark.delta.sharing.streaming.queryTableVersionIntervalSeconds\",\n            \"10s\"\n          )\n          testStream(df)(\n            AssertOnQuery { q =>\n              q.processAllAvailable(); true\n            },\n            CheckAnswer(\"keep1\", \"keep2\"),\n            StopStream\n          )\n        }\n      }\n    }\n  }\n\n  // Mirror of batch auto-resolve test: grid over flag. When ON, getMetadata is used and we send\n  // its format (delta or parquet); when OFF, user's responseFormat is used.\n  Seq(\n    (true, \"shared_streaming_table_auto_resolve\", \"delta\"),\n    (true, \"shared_parquet_table_auto_resolve\", \"parquet\"),\n    (false, \"shared_parquet_table_streaming\", \"parquet\"),\n    (false, \"shared_streaming_table_delta\", \"delta\")\n  ).foreach { case (autoResolve, sharedTableName, expectedFormat) =>\n    test(s\"streaming auto-resolve [flag=$autoResolve, \" +\n      s\"format=$expectedFormat]\") {\n      withTempDir { tempDir =>\n        val deltaTableName = \"delta_table_auto_resolve\"\n        withTable(deltaTableName) {\n          sql(s\"DROP TABLE IF EXISTS $deltaTableName\")\n          sql(\n            s\"\"\"CREATE TABLE $deltaTableName (value STRING)\n               |USING DELTA\"\"\".stripMargin)\n          val profileFile = prepareProfileFile(tempDir)\n          val tablePath =\n            profileFile.getCanonicalPath +\n              s\"#share1.default.$sharedTableName\"\n          sql(s\"INSERT INTO $deltaTableName VALUES ('a'), ('b')\")\n          spark.sessionState.conf.setConfString(\n            \"spark.delta.sharing.streaming\" +\n              \".queryTableVersionIntervalSeconds\",\n            \"10s\"\n          )\n          if (autoResolve) {\n            prepareMockedClientMetadata(\n              deltaTableName, sharedTableName)\n            if (expectedFormat == \"delta\") {\n              prepareMockedClientAndFileSystemResult(\n                deltaTableName, sharedTableName, Some(1L))\n            } else {\n              prepareMockedClientAndFileSystemResultForParquet(\n                deltaTableName, sharedTableName)\n              prepareMockedClientAndFileSystemResultForParquet(\n                deltaTableName, sharedTableName,\n                versionAsOf = Some(1L))\n              prepareMockedClientAndFileSystemResultForStreaming(\n                deltaTableName, sharedTableName, 1L, 1L)\n            }\n          } else {\n            if (expectedFormat == \"parquet\") {\n              prepareMockedClientAndFileSystemResultForParquet(\n                deltaTableName, sharedTableName)\n              prepareMockedClientAndFileSystemResultForParquet(\n                deltaTableName, sharedTableName,\n                versionAsOf = Some(1L))\n              prepareMockedClientAndFileSystemResultForStreaming(\n                deltaTableName, sharedTableName, 1L, 1L)\n            } else {\n              prepareMockedClientMetadata(\n                deltaTableName, sharedTableName)\n              prepareMockedClientAndFileSystemResult(\n                deltaTableName, sharedTableName, Some(1L))\n            }\n          }\n          prepareMockedClientGetTableVersion(\n            deltaTableName, sharedTableName)\n          val userResponseFormat =\n            if (autoResolve) \"parquet\" else expectedFormat\n          val autoResolveKey =\n            DeltaSQLConf\n              .DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT\n              .key\n          withSQLConf(\n            (getDeltaSharingClassesSQLConf +\n              (autoResolveKey -> autoResolve.toString))\n              .toSeq: _*\n          ) {\n            val df = spark.readStream\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", userResponseFormat)\n              .load(tablePath)\n            testStream(df)(\n              AssertOnQuery { q =>\n                q.processAllAvailable(); true\n              },\n              CheckAnswer(\"a\", \"b\"),\n              StopStream\n            )\n            assertRequestedFormat(\n              s\"share1.default.$sharedTableName\",\n              Seq(expectedFormat))\n          }\n        }\n      }\n    }\n  }\n\n    test(\n      \"restart works sharing\"\n    ) {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        val deltaTableName = \"delta_table_restart\"\n        withTable(deltaTableName) {\n          createTableForStreaming(deltaTableName)\n          val sharedTableName = \"shared_streaming_table_restart\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          val profileFile = prepareProfileFile(inputDir)\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            def InsertToDeltaTable(values: String): Unit = {\n              sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n            }\n\n            // TODO: check testStream() function helper\n            def processAllAvailableInStream(): Unit = {\n              val q =\n                  spark.readStream\n                    .format(\"deltaSharing\")\n                    .option(\"responseFormat\", \"delta\")\n                    .load(tablePath)\n                    .filter($\"value\" contains \"keep\")\n                    .writeStream\n                    .format(\"delta\")\n                    .option(\"checkpointLocation\", checkpointDir.toString)\n                    .start(outputDir.toString)\n\n              try {\n                q.processAllAvailable()\n              } finally {\n                q.stop()\n              }\n            }\n\n            // Able to stream snapshot at version 1.\n            InsertToDeltaTable(\"\"\"(\"keep1\"), (\"keep2\"), (\"drop1\")\"\"\")\n            prepareMockedClientAndFileSystemResult(\n              deltaTable = deltaTableName,\n              sharedTable = sharedTableName,\n              versionAsOf = Some(1L)\n            )\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\").toDF()\n            )\n\n            // No new data, so restart will not process any new data.\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\").toDF()\n            )\n\n            // Able to stream new data at version 2.\n            InsertToDeltaTable(\"\"\"(\"keep3\"), (\"keep4\"), (\"drop2\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              2,\n              2\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            sql(s\"\"\"OPTIMIZE $deltaTableName\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              2,\n              3\n            )\n            // Optimize doesn't produce new data, so restart will not process any new data.\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            // Able to stream new data at version 3.\n            InsertToDeltaTable(\"\"\"(\"keep5\"), (\"keep6\"), (\"drop3\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              3,\n              4\n            )\n\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\", \"keep5\", \"keep6\").toDF()\n            )\n            assertBlocksAreCleanedUp()\n          }\n        }\n      }\n    }\n\n    test(\n      \"restart works sharing with special chars\"\n    ) {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        val deltaTableName = \"delta_table_restart_special\"\n        withTable(deltaTableName) {\n          // scalastyle:off nonascii\n          sql(s\"\"\"CREATE TABLE $deltaTableName (`第一列` STRING) USING DELTA\"\"\".stripMargin)\n          val sharedTableName = \"shared_streaming_table_special\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          val profileFile = prepareProfileFile(inputDir)\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            def InsertToDeltaTable(values: String): Unit = {\n              sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n            }\n\n            // TODO: check testStream() function helper\n            def processAllAvailableInStream(): Unit = {\n              val q =\n                  spark.readStream\n                    .format(\"deltaSharing\")\n                    .option(\"responseFormat\", \"delta\")\n                    .load(tablePath)\n                    .filter($\"第一列\" contains \"keep\")\n                    .writeStream\n                    .format(\"delta\")\n                    .option(\"checkpointLocation\", checkpointDir.toString)\n                    .start(outputDir.toString)\n                  // scalastyle:on nonascii\n\n              try {\n                q.processAllAvailable()\n              } finally {\n                q.stop()\n              }\n            }\n\n            // Able to stream snapshot at version 1.\n            InsertToDeltaTable(\"\"\"(\"keep1\"), (\"keep2\"), (\"drop1\")\"\"\")\n            prepareMockedClientAndFileSystemResult(\n              deltaTable = deltaTableName,\n              sharedTable = sharedTableName,\n              versionAsOf = Some(1L)\n            )\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\").toDF()\n            )\n\n            // No new data, so restart will not process any new data.\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\").toDF()\n            )\n\n            // Able to stream new data at version 2.\n            InsertToDeltaTable(\"\"\"(\"keep3\"), (\"keep4\"), (\"drop2\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              2,\n              2\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            sql(s\"\"\"OPTIMIZE $deltaTableName\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              2,\n              3\n            )\n            // Optimize doesn't produce new data, so restart will not process any new data.\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            // Able to stream new data at version 3.\n            InsertToDeltaTable(\"\"\"(\"keep5\"), (\"keep6\"), (\"drop3\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              3,\n              4\n            )\n\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\", \"keep5\", \"keep6\").toDF()\n            )\n            assertBlocksAreCleanedUp()\n          }\n        }\n      }\n    }\n\n  test(\"streaming works with deletes on basic table\") {\n    withTempDir { inputDir =>\n      val deltaTableName = \"delta_table_deletes\"\n      withTable(deltaTableName) {\n        createTableForStreaming(deltaTableName)\n        val sharedTableName = \"shared_streaming_table_deletes\"\n        prepareMockedClientMetadata(deltaTableName, sharedTableName)\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          def InsertToDeltaTable(values: String): Unit = {\n            sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n          }\n\n          def processAllAvailableInStream(\n              sourceOptions: Map[String, String],\n              expectations: StreamAction*): Unit = {\n            val df = spark.readStream\n              .format(\"deltaSharing\")\n              .options(sourceOptions)\n              .load(tablePath)\n\n            val base = Seq(StartStream(), ProcessAllAvailable())\n            testStream(df)((base ++ expectations): _*)\n          }\n\n          // Insert at version 1 and 2.\n          InsertToDeltaTable(\"\"\"(\"keep1\")\"\"\")\n          InsertToDeltaTable(\"\"\"(\"keep2\")\"\"\")\n          // delete at version 3.\n          sql(s\"\"\"DELETE FROM $deltaTableName WHERE value = \"keep1\" \"\"\")\n          // update at version 4.\n          sql(s\"\"\"UPDATE $deltaTableName SET value = \"keep3\" WHERE value = \"keep2\" \"\"\")\n\n          prepareMockedClientAndFileSystemResult(\n            deltaTable = deltaTableName,\n            sharedTable = sharedTableName,\n            versionAsOf = Some(4L)\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          processAllAvailableInStream(\n            Map(\"responseFormat\" -> \"delta\"),\n            CheckAnswer(\"keep3\")\n          )\n\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            0,\n            4\n          )\n\n          // The streaming query will fail because changes detected in version 4.\n          // This is the original delta behavior.\n          val e = intercept[Exception] {\n            processAllAvailableInStream(\n              Map(\"responseFormat\" -> \"delta\", \"startingVersion\" -> \"0\")\n            )\n          }\n          for (msg <- Seq(\n              \"Detected\",\n              \"not supported\",\n              \"true\"\n            )) {\n            assert(e.getMessage.contains(msg))\n          }\n\n          // The streaming query will fail because changes detected in version 4.\n          // This is the original delta behavior.\n          val e2 = intercept[Exception] {\n            processAllAvailableInStream(\n              Map(\n                \"responseFormat\" -> \"delta\",\n                \"startingVersion\" -> \"0\",\n                IGNORE_DELETES_OPTION -> \"true\"\n              )\n            )\n          }\n          for (msg <- Seq(\n              \"Detected\",\n              \"not supported\",\n              \"true\"\n            )) {\n            assert(e2.getMessage.contains(msg))\n          }\n\n          // The streaming query will succeed because ignoreChanges helps to ignore the updates, but\n          // added updated data \"keep3\".\n          processAllAvailableInStream(\n            Map(\n              \"responseFormat\" -> \"delta\",\n              \"startingVersion\" -> \"0\",\n              IGNORE_CHANGES_OPTION -> \"true\"\n            ),\n            CheckAnswer(\"keep1\", \"keep2\", \"keep3\")\n          )\n\n          // The streaming query will succeed because skipChangeCommits helps to ignore the whole\n          // commit with data update, so updated data is not produced either.\n          processAllAvailableInStream(\n            Map(\n              \"responseFormat\" -> \"delta\",\n              \"startingVersion\" -> \"0\",\n              SKIP_CHANGE_COMMITS_OPTION -> \"true\"\n            ),\n            CheckAnswer(\"keep1\", \"keep2\")\n          )\n          assertBlocksAreCleanedUp()\n        }\n      }\n    }\n  }\n\n  test(\"streaming works with DV\") {\n    withTempDir { inputDir =>\n      val deltaTableName = \"delta_table_dv\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        spark.sql(\n          s\"ALTER TABLE $deltaTableName SET TBLPROPERTIES('delta.enableDeletionVectors' = true)\"\n        )\n        val sharedTableName = \"shared_streaming_table_dv\"\n        prepareMockedClientMetadata(deltaTableName, sharedTableName)\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          def InsertToDeltaTable(values: String): Unit = {\n            sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n          }\n\n          def processAllAvailableInStream(\n              sourceOptions: Map[String, String],\n              expectations: StreamAction*): Unit = {\n            val df = spark.readStream\n              .format(\"deltaSharing\")\n              .options(sourceOptions)\n              .load(tablePath)\n              .filter($\"c2\" contains \"keep\")\n              .select(\"c1\")\n\n            val base = Seq(StartStream(), ProcessAllAvailable())\n            testStream(df)((base ++ expectations): _*)\n          }\n\n          // Insert at version 2.\n          InsertToDeltaTable(\"\"\"(1, \"keep1\"),(2, \"keep1\"),(3, \"keep1\"),(1,\"drop1\")\"\"\")\n          // delete at version 3.\n          sql(s\"\"\"DELETE FROM $deltaTableName WHERE c1 >= 2 \"\"\")\n\n          prepareMockedClientAndFileSystemResult(\n            deltaTable = deltaTableName,\n            sharedTable = sharedTableName,\n            versionAsOf = Some(3L)\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          processAllAvailableInStream(\n            Map(\"responseFormat\" -> \"delta\"),\n            CheckAnswer(1)\n          )\n\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            startingVersion = 0,\n            endingVersion = 3,\n            assertDVExists = true\n          )\n\n          // The streaming query will fail because deletes detected in version 3. And there are no\n          // options provided to ignore the deletion.\n          val e = intercept[Exception] {\n            processAllAvailableInStream(\n              Map(\"responseFormat\" -> \"delta\", \"startingVersion\" -> \"0\")\n            )\n          }\n          for (msg <- Seq(\n              \"Detected a data update\",\n              \"not supported\",\n              SKIP_CHANGE_COMMITS_OPTION,\n              \"true\"\n            )) {\n            assert(e.getMessage.contains(msg))\n          }\n\n          // The streaming query will fail because deletes detected in version 3, and it's\n          // recognized as updates and ignoreDeletes doesn't help. This is the original delta\n          // behavior.\n          val e2 = intercept[Exception] {\n            processAllAvailableInStream(\n              Map(\n                \"responseFormat\" -> \"delta\",\n                \"startingVersion\" -> \"0\",\n                IGNORE_DELETES_OPTION -> \"true\"\n              )\n            )\n          }\n          for (msg <- Seq(\n              \"Detected a data update\",\n              \"not supported\",\n              SKIP_CHANGE_COMMITS_OPTION,\n              \"true\"\n            )) {\n            assert(e2.getMessage.contains(msg))\n          }\n\n          // The streaming query will succeed because ignoreChanges helps to ignore the delete, but\n          // added duplicated data 1.\n          processAllAvailableInStream(\n            Map(\n              \"responseFormat\" -> \"delta\",\n              \"startingVersion\" -> \"0\",\n              IGNORE_CHANGES_OPTION -> \"true\"\n            ),\n            CheckAnswer(1, 2, 3, 1)\n          )\n\n          // The streaming query will succeed because skipChangeCommits helps to ignore the whole\n          // commit with data update, so no duplicated data is produced either.\n          processAllAvailableInStream(\n            Map(\n              \"responseFormat\" -> \"delta\",\n              \"startingVersion\" -> \"0\",\n              SKIP_CHANGE_COMMITS_OPTION -> \"true\"\n            ),\n            CheckAnswer(1, 2, 3)\n          )\n          assertBlocksAreCleanedUp()\n        }\n      }\n    }\n  }\n\n  test(\"streaming works with timestampNTZ\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_timestampNTZ\"\n      withTable(deltaTableName) {\n        sql(s\"CREATE TABLE $deltaTableName(c1 TIMESTAMP_NTZ) USING DELTA\")\n        val sharedTableName = \"shared_table_timestampNTZ\"\n        prepareMockedClientMetadata(deltaTableName, sharedTableName)\n        val profileFile = prepareProfileFile(tempDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          def InsertToDeltaTable(values: String): Unit = {\n            sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n          }\n\n          def processAllAvailableInStream(\n            sourceOptions: Map[String, String],\n            expectations: StreamAction*): Unit = {\n            val df = spark.readStream\n              .format(\"deltaSharing\")\n              .options(sourceOptions)\n              .load(tablePath)\n              .select(\"c1\")\n\n            val base = Seq(StartStream(), ProcessAllAvailable())\n            testStream(df)((base ++ expectations): _*)\n          }\n\n          // Insert at version 1.\n          InsertToDeltaTable(\"\"\"('2022-01-01 02:03:04.123456')\"\"\")\n          // Insert at version 2.\n          InsertToDeltaTable(\"\"\"('2022-02-02 03:04:05.123456')\"\"\")\n\n          prepareMockedClientAndFileSystemResult(\n            deltaTable = deltaTableName,\n            sharedTable = sharedTableName,\n            versionAsOf = Some(2L)\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          processAllAvailableInStream(\n            Map(\"responseFormat\" -> \"delta\"),\n            CheckAnswer(\n              LocalDateTime.parse(\"2022-01-01T02:03:04.123456\"),\n              LocalDateTime.parse(\"2022-02-02T03:04:05.123456\")\n            )\n          )\n\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            startingVersion = 2,\n            endingVersion = 2\n          )\n          processAllAvailableInStream(\n            Map(\n              \"responseFormat\" -> \"delta\",\n              \"startingVersion\" -> \"2\"\n            ),\n            CheckAnswer(LocalDateTime.parse(\"2022-02-02T03:04:05.123456\"))\n          )\n          assertBlocksAreCleanedUp()\n        }\n      }\n    }\n  }\n\n    test(\n      \"startingVersion works\"\n    ) {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        val deltaTableName = \"delta_table_startVersion\"\n        withTable(deltaTableName) {\n          createTableForStreaming(deltaTableName)\n          val sharedTableName = \"shared_streaming_table_startVersion\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          val profileFile = prepareProfileFile(inputDir)\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            def InsertToDeltaTable(values: String): Unit = {\n              sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n            }\n\n            def processAllAvailableInStream(): Unit = {\n              val q =\n                  spark.readStream\n                    .format(\"deltaSharing\")\n                    .option(\"responseFormat\", \"delta\")\n                    .option(\"startingVersion\", 0)\n                    .load(tablePath)\n                    .filter($\"value\" contains \"keep\")\n                    .writeStream\n                    .format(\"delta\")\n                    .option(\"checkpointLocation\", checkpointDir.toString)\n                    .start(outputDir.toString)\n\n              try {\n                q.processAllAvailable()\n              } finally {\n                q.stop()\n              }\n            }\n\n            // Able to stream snapshot at version 1.\n            InsertToDeltaTable(\"\"\"(\"keep1\"), (\"keep2\"), (\"drop1\")\"\"\")\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTable = deltaTableName,\n              sharedTable = sharedTableName,\n              startingVersion = 0L,\n              endingVersion = 1L\n            )\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\").toDF()\n            )\n\n            // No new data, so restart will not process any new data.\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\").toDF()\n            )\n\n            // Able to stream new data at version 2.\n            InsertToDeltaTable(\"\"\"(\"keep3\"), (\"keep4\"), (\"drop2\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              0,\n              2\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            sql(s\"\"\"OPTIMIZE $deltaTableName\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              2,\n              3\n            )\n            // Optimize doesn't produce new data, so restart will not process any new data.\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            // No new data, so restart will not process any new data. It will ask for the\n            // last commit so that it can figure out that there's nothing to do.\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              3,\n              3\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\").toDF()\n            )\n\n            // Able to stream new data at version 3.\n            InsertToDeltaTable(\"\"\"(\"keep5\"), (\"keep6\"), (\"drop3\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              3,\n              4\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\", \"keep5\", \"keep6\").toDF()\n            )\n\n            // No new data, so restart will not process any new data. It will ask for the\n            // last commit so that it can figure out that there's nothing to do.\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              4,\n              4\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq(\"keep1\", \"keep2\", \"keep3\", \"keep4\", \"keep5\", \"keep6\").toDF()\n            )\n            assertBlocksAreCleanedUp()\n          }\n        }\n      }\n    }\n\n  test(\n    \"files are in a stable order for streaming\"\n  ) {\n    // This test function is to check that DeltaSharingLogFileSystem puts the files in the delta log\n    // in a stable order for each commit, regardless of the returning order from the server, so that\n    // the DeltaSource can produce a stable file index.\n    // We are using maxBytesPerTrigger which causes the streaming to stop in the middle of a commit\n    // to be able to test this behavior.\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      withTempDirs { (_, outputDir2, checkpointDir2) =>\n        val deltaTableName = \"delta_table_order\"\n        withTable(deltaTableName) {\n          createSimpleTable(deltaTableName, enableCdf = false)\n          val sharedTableName = \"shared_streaming_table_order\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          val profileFile = prepareProfileFile(inputDir)\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n          def InsertToDeltaTable(values: String): Unit = {\n            sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n          }\n\n          // Able to stream snapshot at version 1.\n          InsertToDeltaTable(\"\"\"(1, \"one\"), (2, \"two\"), (3, \"three\")\"\"\")\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            def processAllAvailableInStream(\n                outputDirStr: String,\n                checkpointDirStr: String): Unit = {\n              val q = spark.readStream\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .option(\"maxBytesPerTrigger\", \"1b\")\n                .load(tablePath)\n                .writeStream\n                .format(\"delta\")\n                .option(\"checkpointLocation\", checkpointDirStr)\n                .start(outputDirStr)\n\n              try {\n                q.processAllAvailable()\n                val progress = q.recentProgress.filter(_.numInputRows != 0)\n                assert(progress.length === 3)\n                progress.foreach { p =>\n                  assert(p.numInputRows === 1)\n                }\n              } finally {\n                q.stop()\n              }\n            }\n\n            // First output, without reverseFileOrder\n            prepareMockedClientAndFileSystemResult(\n              deltaTable = deltaTableName,\n              sharedTable = sharedTableName,\n              versionAsOf = Some(1L)\n            )\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            processAllAvailableInStream(outputDir.toString, checkpointDir.toString)\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              Seq((1, \"one\"), (2, \"two\"), (3, \"three\")).toDF()\n            )\n\n            // Second output, with reverseFileOrder = true\n            prepareMockedClientAndFileSystemResult(\n              deltaTable = deltaTableName,\n              sharedTable = sharedTableName,\n              versionAsOf = Some(1L),\n              reverseFileOrder = true\n            )\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            processAllAvailableInStream(outputDir2.toString, checkpointDir2.toString)\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir2.getCanonicalPath),\n              Seq((1, \"one\"), (2, \"two\"), (3, \"three\")).toDF()\n            )\n\n            // Check each version of the two output are the same, which means the files are sorted\n            // by DeltaSharingLogFileSystem, and are processed in a deterministic order by the\n            // DeltaSource.\n            val deltaLog = DeltaLog.forTable(spark, new Path(outputDir.toString))\n            Seq(0, 1, 2).foreach { v =>\n              val version = deltaLog.snapshot.version - v\n              val df1 = spark.read\n                .format(\"delta\")\n                .option(\"versionAsOf\", version)\n                .load(outputDir.getCanonicalPath)\n              val df2 = spark.read\n                .format(\"delta\")\n                .option(\"versionAsOf\", version)\n                .load(outputDir2.getCanonicalPath)\n              checkAnswer(df1, df2)\n              assert(df1.count() == (3 - v))\n            }\n            assertBlocksAreCleanedUp()\n          }\n        }\n      }\n    }\n  }\n\n    test(\n      \"DeltaFormatSharingSource query with two delta sharing tables works\"\n    ) {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        val deltaTableName = \"delta_table_two\"\n\n        def InsertToDeltaTable(values: String): Unit = {\n          sql(s\"INSERT INTO $deltaTableName VALUES $values\")\n        }\n\n        withTable(deltaTableName) {\n          createSimpleTable(deltaTableName, enableCdf = false)\n          val sharedTableName = \"shared_streaming_table_two\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n\n          val profileFile = prepareProfileFile(inputDir)\n          val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            InsertToDeltaTable(\"\"\"(1, \"one\"), (2, \"one\")\"\"\")\n            InsertToDeltaTable(\"\"\"(1, \"two\"), (2, \"two\")\"\"\")\n            InsertToDeltaTable(\"\"\"(1, \"three\"), (2, \"three\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResult(\n              deltaTableName,\n              sharedTableName,\n              Some(3L)\n            )\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              startingVersion = 1,\n              endingVersion = 3\n            )\n\n            def processAllAvailableInStream(): Unit = {\n              val dfLatest = spark.readStream\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n              val dfV1 = spark.readStream\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .option(\"startingVersion\", 1)\n                .load(tablePath)\n                .select(col(\"c2\"), col(\"c1\").as(\"v1c1\"))\n                .filter(col(\"v1c1\") === 1)\n\n              val q =\n                  dfLatest\n                    .join(dfV1, \"c2\")\n                    .writeStream\n                    .format(\"delta\")\n                    .option(\"checkpointLocation\", checkpointDir.toString)\n                    .start(outputDir.toString)\n\n              try {\n                q.processAllAvailable()\n              } finally {\n                q.stop()\n              }\n            }\n\n            // c1 from dfLatest, c2 from dfLatest, c1 from dfV1\n            var expected = Seq(\n              Row(\"one\", 1, 1),\n              Row(\"one\", 2, 1),\n              Row(\"two\", 1, 1),\n              Row(\"two\", 2, 1),\n              Row(\"three\", 1, 1),\n              Row(\"three\", 2, 1)\n            )\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              expected\n            )\n\n            InsertToDeltaTable(\"\"\"(1, \"four\"), (2, \"four\")\"\"\")\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              startingVersion = 4,\n              endingVersion = 4\n            )\n            prepareMockedClientAndFileSystemResultForStreaming(\n              deltaTableName,\n              sharedTableName,\n              startingVersion = 1,\n              endingVersion = 4\n            )\n\n            expected = expected ++ Seq(Row(\"four\", 1, 1), Row(\"four\", 2, 1))\n            processAllAvailableInStream()\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              expected\n            )\n            assertBlocksAreCleanedUp()\n          }\n        }\n      }\n    }\n\n    Seq(\n      (\"add a partition column\", Seq(\"part\"), Seq(\"is_even\", \"part\")),\n      (\"change partition order\", Seq(\"part\", \"is_even\"), Seq(\"is_even\", \"part\")),\n      (\"different partition column\", Seq(\"part\"), Seq(\"is_even\"))\n    ).foreach {\n      case (repartitionTestCase, initPartitionCols, overwritePartitionCols) =>\n        test(\n          \"deltaSharing - repartition delta source should fail by default \" +\n          s\"unless unsafe flag is set - $repartitionTestCase\"\n        ) {\n          withTempDirs { (inputDir, outputDir, checkpointDir) =>\n            val deltaTableName = \"basic_delta_table_partition_check\"\n            withTable(deltaTableName) {\n              spark.sql(\n                s\"\"\"CREATE TABLE $deltaTableName (id LONG, part INT, is_even BOOLEAN)\n                   |USING DELTA PARTITIONED BY (${initPartitionCols.mkString(\", \")})\n                   |\"\"\".stripMargin\n              )\n              val sharedTableName = \"shared_streaming_table_partition_check_\" +\n                s\"${repartitionTestCase.replace(' ', '_')}\"\n              prepareMockedClientMetadata(deltaTableName, sharedTableName)\n              val profileFile = prepareProfileFile(inputDir)\n              val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n              withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n\n                def processAllAvailableInStream(startingVersion: Int): Unit = {\n                  val q =\n                      spark.readStream\n                        .format(\"deltaSharing\")\n                        .option(\"responseFormat\", \"delta\")\n                        .option(\"skipChangeCommits\", \"true\")\n                        .option(\"startingVersion\", startingVersion)\n                        .load(tablePath)\n                        .writeStream\n                        .format(\"delta\")\n                        .option(\"checkpointLocation\", checkpointDir.toString)\n                        .start(outputDir.toString)\n\n                  try {\n                    q.processAllAvailable()\n                  } finally {\n                    q.stop()\n                  }\n                }\n\n                spark.range(10).withColumn(\"part\", lit(1))\n                  .withColumn(\"is_even\", $\"id\" % 2 === 0).write\n                  .format(\"delta\").partitionBy(initPartitionCols: _*)\n                  .mode(\"append\")\n                  .saveAsTable(deltaTableName)\n                spark.range(2).withColumn(\"part\", lit(2))\n                  .withColumn(\"is_even\", $\"id\" % 2 === 0).write\n                  .format(\"delta\").partitionBy(initPartitionCols: _*)\n                  .mode(\"append\").saveAsTable(deltaTableName)\n                spark.range(10).withColumn(\"part\", lit(1))\n                  .withColumn(\"is_even\", $\"id\" % 2 === 0).write\n                  .format(\"delta\").partitionBy(overwritePartitionCols: _*)\n                  .option(\"overwriteSchema\", \"true\").mode(\"overwrite\")\n                  .saveAsTable(deltaTableName)\n                spark.range(2).withColumn(\"part\", lit(2))\n                  .withColumn(\"is_even\", $\"id\" % 2 === 0).write\n                  .format(\"delta\").partitionBy(overwritePartitionCols: _*)\n                  .mode(\"append\").saveAsTable(deltaTableName)\n\n                prepareMockedClientAndFileSystemResultForStreaming(\n                  deltaTable = deltaTableName,\n                  sharedTable = sharedTableName,\n                  startingVersion = 0L,\n                  endingVersion = 4L\n                )\n                prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n                var e = intercept[StreamingQueryException] {\n                  processAllAvailableInStream(0)\n                }\n                assert(e.getCause.asInstanceOf[DeltaIllegalStateException].getErrorClass\n                  == \"DELTA_SCHEMA_CHANGED_WITH_STARTING_OPTIONS\")\n                assert(e.getMessage.contains(\"Detected schema change in version 3\"))\n\n                // delta table created using sql with specified partition col\n                // will construct their initial snapshot on the initial definition\n                prepareMockedClientAndFileSystemResultForStreaming(\n                  deltaTable = deltaTableName,\n                  sharedTable = sharedTableName,\n                  startingVersion = 4L,\n                  endingVersion = 4L\n                )\n                prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n                e = intercept[StreamingQueryException] {\n                  processAllAvailableInStream(4)\n                }\n                assert(e.getMessage.contains(\"Detected schema change in version 4\"))\n\n                // Streaming query made progress without throwing error when\n                // unsafe flag is set to true\n                withSQLConf(\n                  DeltaSQLConf.DELTA_STREAMING_UNSAFE_READ_ON_PARTITION_COLUMN_CHANGE.key -> \"true\"\n                ) {\n                  processAllAvailableInStream(0)\n                }\n              }\n            }\n          }\n        }\n    }\n\n    test(\"streaming variant query works\") {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        val deltaTableName = \"variant_table\"\n        withTable(deltaTableName) {\n          sql(s\"create table $deltaTableName (v VARIANT) using delta\")\n\n          val sharedTableName = \"shared_variant_table\"\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n\n          val profileFile = prepareProfileFile(inputDir)\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n            sql(s\"\"\"insert into table $deltaTableName\n                select parse_json(format_string('{\"key\": %s}', id))\n                from range(0, 10)\n            \"\"\")\n\n            prepareMockedClientAndFileSystemResult(\n              deltaTableName,\n              sharedTableName,\n              versionAsOf = Some(1L)\n            )\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n            val q =\n                spark.readStream\n                  .format(\"deltaSharing\")\n                  .option(\"responseFormat\", \"delta\")\n                  .load(tablePath)\n                  .writeStream\n                  .format(\"delta\")\n                  .option(\"checkpointLocation\", checkpointDir.toString)\n                  .start(outputDir.toString)\n\n            try {\n              q.processAllAvailable()\n            } finally {\n              q.stop()\n            }\n\n            checkAnswer(\n              spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n              spark.sql(s\"select * from $deltaTableName\")\n            )\n          }\n        }\n      }\n    }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingCDFUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport io.delta.sharing.client.{\n  DeltaSharingClient,\n  DeltaSharingProfileProvider,\n  DeltaSharingRestClient\n}\nimport io.delta.sharing.client.model.{DeltaTableFiles, DeltaTableMetadata, Table, TemporaryCredentials}\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.{SparkConf, SparkEnv}\nimport org.apache.spark.delta.sharing.{PreSignedUrlCache, PreSignedUrlFetcher}\nimport org.apache.spark.sql.{QueryTest, SparkSession}\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport org.apache.spark.sql.test.{SharedSparkSession}\n\nprivate object CDFTesTUtils {\n  val paths = Seq(\"http://path1\", \"http://path2\")\n\n  val SparkConfForReturnExpTime = \"spark.delta.sharing.fileindexsuite.returnexptime\"\n\n  // 10 seconds\n  val expirationTimeMs = 10000\n\n  def getExpirationTimestampStr(returnExpTime: Boolean): String = {\n    if (returnExpTime) {\n      s\"\"\"\"expirationTimestamp\":${System.currentTimeMillis() + expirationTimeMs},\"\"\"\n    } else {\n      \"\"\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  val fileStr1Id = \"11d9b72771a72f178a6f2839f7f08528\"\n  val metaDataStr =\n    \"\"\"{\"metaData\":{\"size\":809,\"deltaMetadata\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"c2\"],\"configuration\":{\"delta.enableChangeDataFeed\":\"true\"},\"createdTime\":1691734718560}}}\"\"\"\n  def getAddFileStr1(path: String, returnExpTime: Boolean = false): String = {\n    s\"\"\"{\"file\":{\"id\":\"11d9b72771a72f178a6f2839f7f08528\",${getExpirationTimestampStr(\n      returnExpTime\n    )}\"deltaSingleAction\":{\"add\":{\"path\":\"${path}\",\"\"\" + \"\"\"\"partitionValues\":{\"c2\":\"one\"},\"size\":809,\"modificationTime\":1691734726073,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"c1\\\":1,\\\"c2\\\":\\\"one\\\"},\\\"maxValues\\\":{\\\"c1\\\":2,\\\"c2\\\":\\\"one\\\"},\\\"nullCount\\\":{\\\"c1\\\":0,\\\"c2\\\":0}}\",\"tags\":{\"INSERTION_TIME\":\"1691734726073000\",\"MIN_INSERTION_TIME\":\"1691734726073000\",\"MAX_INSERTION_TIME\":\"1691734726073000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}}}\"\"\"\n  }\n  def getAddFileStr2(returnExpTime: Boolean = false): String = {\n    s\"\"\"{\"file\":{\"id\":\"22d9b72771a72f178a6f2839f7f08529\",${getExpirationTimestampStr(\n      returnExpTime\n    )}\"\"\" + \"\"\"\"deltaSingleAction\":{\"add\":{\"path\":\"http://path2\",\"partitionValues\":{\"c2\":\"two\"},\"size\":809,\"modificationTime\":1691734726073,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"c1\\\":1,\\\"c2\\\":\\\"two\\\"},\\\"maxValues\\\":{\\\"c1\\\":2,\\\"c2\\\":\\\"two\\\"},\\\"nullCount\\\":{\\\"c1\\\":0,\\\"c2\\\":0}}\",\"tags\":{\"INSERTION_TIME\":\"1691734726073000\",\"MIN_INSERTION_TIME\":\"1691734726073000\",\"MAX_INSERTION_TIME\":\"1691734726073000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}}}\"\"\"\n  }\n  // scalastyle:on line.size.limit\n}\n\n/**\n * A mocked delta sharing client for unit tests.\n */\nclass TestDeltaSharingClientForCDFUtils(\n    profileProvider: DeltaSharingProfileProvider,\n    timeoutInSeconds: Int = 120,\n    numRetries: Int = 3,\n    maxRetryDuration: Long = Long.MaxValue,\n    retrySleepInterval: Long = 1000,\n    sslTrustAll: Boolean = false,\n    forStreaming: Boolean = false,\n    responseFormat: String = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA,\n    readerFeatures: String = \"\",\n    queryTablePaginationEnabled: Boolean = false,\n    maxFilesPerReq: Int = 100000,\n    endStreamActionEnabled: Boolean = false,\n    enableAsyncQuery: Boolean = false,\n    asyncQueryPollIntervalMillis: Long = 10000L,\n    asyncQueryMaxDuration: Long = 600000L,\n    tokenExchangeMaxRetries: Int = 5,\n    tokenExchangeMaxRetryDurationInSeconds: Int = 60,\n    tokenRenewalThresholdInSeconds: Int = 600,\n    callerOrg: String = \"\",\n    skipFileIdHashVerification: Boolean = false)\n    extends DeltaSharingClient {\n\n  import CDFTesTUtils._\n\n  private lazy val returnExpirationTimestamp = SparkSession.active.sessionState.conf\n    .getConfString(\n      SparkConfForReturnExpTime\n    )\n    .toBoolean\n\n  var numGetFileCalls: Int = -1\n\n  override def listAllTables(): Seq[Table] = throw new UnsupportedOperationException(\"not needed\")\n\n  override def getMetadata(\n      table: Table,\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String]): DeltaTableMetadata = {\n    throw new UnsupportedOperationException(\"getMetadata is not supported now.\")\n  }\n\n  override def getTableVersion(table: Table, startingTimestamp: Option[String] = None): Long = {\n    throw new UnsupportedOperationException(\"getTableVersion is not supported now.\")\n  }\n\n  override def getFiles(\n      table: Table,\n      predicates: Seq[String],\n      limit: Option[Long],\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String],\n      jsonPredicateHints: Option[String],\n      refreshToken: Option[String],\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    throw new UnsupportedOperationException(\"getFiles is not supported now.\")\n  }\n\n  override def getFiles(\n      table: Table,\n      startingVersion: Long,\n      endingVersion: Option[Long],\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    throw new UnsupportedOperationException(s\"getFiles with startingVersion($startingVersion)\")\n  }\n\n  override def getCDFFiles(\n      table: Table,\n      cdfOptions: Map[String, String],\n      includeHistoricalMetadata: Boolean,\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    numGetFileCalls += 1\n    DeltaTableFiles(\n      version = 0,\n      lines = Seq[String](\n        \"\"\"{\"protocol\":{\"deltaProtocol\":{\"minReaderVersion\": 1, \"minWriterVersion\": 1}}}\"\"\",\n        metaDataStr,\n        getAddFileStr1(paths(numGetFileCalls.min(1)), returnExpirationTimestamp),\n        getAddFileStr2(returnExpirationTimestamp)\n      ),\n      respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA\n    )\n  }\n\n  override def generateTemporaryTableCredential(\n      table: Table,\n      location: Option[String]): TemporaryCredentials = {\n    throw new UnsupportedOperationException(\"generateTemporaryTableCredential is not implemented\")\n  }\n\n  override def getForStreaming(): Boolean = forStreaming\n\n  override def getProfileProvider: DeltaSharingProfileProvider = profileProvider\n}\n\nclass DeltaSharingCDFUtilsSuite\n    extends QueryTest\n    with DeltaSQLCommandTest\n    with SharedSparkSession\n    with DeltaSharingTestSparkUtils {\n\n  import CDFTesTUtils._\n\n  private val shareName = \"share\"\n  private val schemaName = \"default\"\n  private val sharedTableName = \"table\"\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(\"spark.delta.sharing.preSignedUrl.expirationMs\", expirationTimeMs.toString)\n      .set(\"spark.delta.sharing.driver.refreshCheckIntervalMs\", \"1000\")\n      .set(\"spark.delta.sharing.driver.refreshThresholdMs\", \"2000\")\n      .set(\"spark.delta.sharing.driver.accessThresholdToExpireMs\", \"60000\")\n  }\n\n  test(\"refresh works\") {\n    PreSignedUrlCache.registerIfNeeded(SparkEnv.get)\n\n    withTempDir { tempDir =>\n      val profileFile = new File(tempDir, \"foo.share\")\n      FileUtils.writeStringToFile(\n        profileFile,\n        s\"\"\"{\n           |  \"shareCredentialsVersion\": 1,\n           |  \"endpoint\": \"https://localhost:12345/not-used-endpoint\",\n           |  \"bearerToken\": \"mock\"\n           |}\"\"\".stripMargin,\n        \"utf-8\"\n      )\n\n      def test(): Unit = {\n        val profilePath = profileFile.getCanonicalPath\n        val tablePath = new Path(s\"$profilePath#$shareName.$schemaName.$sharedTableName\")\n        val client = DeltaSharingRestClient(profilePath, Map.empty, false, \"delta\")\n        val dsTable = Table(share = shareName, schema = schemaName, name = sharedTableName)\n\n        val options = new DeltaSharingOptions(Map(\"path\" -> tablePath.toString))\n        DeltaSharingCDFUtils.prepareCDFRelation(\n          SparkSession.active.sqlContext,\n          options,\n          dsTable,\n          client\n        )\n\n        val preSignedUrlCacheRef = PreSignedUrlCache.getEndpointRefInExecutor(SparkEnv.get)\n        val path = options.options.getOrElse(\n          \"path\",\n          throw DeltaSharingErrors.pathNotSpecifiedException\n        )\n        val fetcher = new PreSignedUrlFetcher(\n          preSignedUrlCacheRef,\n          DeltaSharingUtils.getTablePathWithIdSuffix(\n            path,\n            DeltaSharingUtils.getQueryParamsHashId(options.cdfOptions)\n          ),\n          fileStr1Id,\n          1000\n        )\n        // sleep for 25000ms to ensure that the urls are refreshed.\n        Thread.sleep(25000)\n\n        // Verify that the url is refreshed as paths(1), not paths(0) anymore.\n        assert(fetcher.getUrl == paths(1))\n      }\n\n      withSQLConf(\n        \"spark.delta.sharing.client.class\" -> classOf[TestDeltaSharingClientForCDFUtils].getName,\n        \"fs.delta-sharing-log.impl\" -> classOf[DeltaSharingLogFileSystem].getName,\n        \"spark.delta.sharing.profile.provider.class\" ->\n        \"io.delta.sharing.client.DeltaSharingFileProfileProvider\",\n        SparkConfForReturnExpTime -> \"true\"\n      ) {\n        test()\n      }\n\n      withSQLConf(\n        \"spark.delta.sharing.client.class\" -> classOf[TestDeltaSharingClientForCDFUtils].getName,\n        \"fs.delta-sharing-log.impl\" -> classOf[DeltaSharingLogFileSystem].getName,\n        \"spark.delta.sharing.profile.provider.class\" ->\n        \"io.delta.sharing.client.DeltaSharingFileProfileProvider\",\n        SparkConfForReturnExpTime -> \"false\"\n      ) {\n        test()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingDataSourceCMSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.{\n  BatchCDFSchemaEndVersion,\n  BatchCDFSchemaLatest,\n  BatchCDFSchemaLegacy,\n  DeltaUnsupportedOperationException\n}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.streaming.{StreamingQueryException, StreamTest, Trigger}\nimport org.apache.spark.sql.types.{IntegerType, StringType, StructType}\n\n// Unit tests to verify that delta format sharing support column mapping (CM).\nclass DeltaSharingDataSourceCMSuite\n    extends StreamTest\n    with DeltaSQLCommandTest\n    with DeltaSharingTestSparkUtils\n    with DeltaSharingDataSourceDeltaTestUtils {\n\n  import testImplicits._\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    spark.conf.set(\"spark.databricks.delta.streaming.allowSourceColumnRenameAndDrop\", \"false\")\n  }\n\n\n  private def testReadCMTable(\n      deltaTableName: String,\n      sharedTablePath: String,\n      dropC1: Boolean = false): Unit = {\n    val expectedSchema: StructType = if (deltaTableName == \"cm_id_table\") {\n      spark.read.format(\"delta\").table(deltaTableName).schema\n    } else {\n      if (dropC1) {\n        new StructType()\n          .add(\"c2rename\", StringType)\n      } else {\n        new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2rename\", StringType)\n      }\n    }\n    assert(\n      expectedSchema == spark.read\n        .format(\"deltaSharing\")\n        .option(\"responseFormat\", \"delta\")\n        .load(sharedTablePath)\n        .schema\n    )\n\n    val sharingDf =\n      spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(sharedTablePath)\n    val deltaDf = spark.read.format(\"delta\").table(deltaTableName)\n    checkAnswer(sharingDf, deltaDf)\n    assert(sharingDf.count() > 0)\n\n    val filteredSharingDf =\n      spark.read\n        .format(\"deltaSharing\")\n        .option(\"responseFormat\", \"delta\")\n        .load(sharedTablePath)\n        .filter(col(\"c2rename\") === \"one\")\n    val filteredDeltaDf =\n      spark.read\n        .format(\"delta\")\n        .table(deltaTableName)\n        .filter(col(\"c2rename\") === \"one\")\n    checkAnswer(filteredSharingDf, filteredDeltaDf)\n    assert(filteredSharingDf.count() > 0)\n  }\n\n  private def testReadCMCdf(\n      deltaTableName: String,\n      sharedTablePath: String,\n      startingVersion: Int): Unit = {\n    val schema = spark.read\n      .format(\"deltaSharing\")\n      .option(\"responseFormat\", \"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", startingVersion)\n      .load(sharedTablePath)\n      .schema\n    val expectedSchema = spark.read\n      .format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", startingVersion)\n      .table(deltaTableName)\n      .schema\n    assert(expectedSchema == schema)\n\n    val deltaDf = spark.read\n      .format(\"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", startingVersion)\n      .table(deltaTableName)\n    val sharingDf = spark.read\n      .format(\"deltaSharing\")\n      .option(\"responseFormat\", \"delta\")\n      .option(\"readChangeFeed\", \"true\")\n      .option(\"startingVersion\", startingVersion)\n      .load(sharedTablePath)\n    if (startingVersion <= 2) {\n      Seq(BatchCDFSchemaEndVersion, BatchCDFSchemaLatest, BatchCDFSchemaLegacy).foreach { m =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key ->\n          m.name\n        ) {\n          val deltaException = intercept[DeltaUnsupportedOperationException] {\n            deltaDf.collect()\n          }\n          assert(\n            deltaException.getMessage.contains(\"Retrieving table changes between\") &&\n            deltaException.getMessage.contains(\"failed because of an incompatible\")\n          )\n          val sharingException = intercept[DeltaUnsupportedOperationException] {\n            sharingDf.collect()\n          }\n          assert(\n            sharingException.getMessage.contains(\"Retrieving table changes between\") &&\n            sharingException.getMessage.contains(\"failed because of an incompatible\")\n          )\n        }\n      }\n    } else {\n      checkAnswer(sharingDf, deltaDf)\n      assert(sharingDf.count() > 0)\n    }\n  }\n\n  private def testReadingSharedCMTable(\n      tempDir: File,\n      deltaTableName: String,\n      sharedTableNameBase: String): Unit = {\n    val sharedTableNameBasic = sharedTableNameBase + \"_one\"\n    prepareMockedClientAndFileSystemResult(\n      deltaTable = deltaTableName,\n      sharedTable = sharedTableNameBasic\n    )\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableNameBasic)\n\n    withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n      val profileFile = prepareProfileFile(tempDir)\n      testReadCMTable(\n        deltaTableName = deltaTableName,\n        sharedTablePath = s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameBasic\"\n      )\n    }\n\n    val sharedTableNameCdf = sharedTableNameBase + \"_cdf\"\n    // Test CM and CDF\n    // Error when reading cdf with startingVersion <= 2, matches delta behavior.\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableNameCdf)\n    prepareMockedClientAndFileSystemResultForCdf(\n      deltaTableName,\n      sharedTableNameCdf,\n      startingVersion = 0\n    )\n    prepareMockedClientAndFileSystemResultForCdf(\n      deltaTableName,\n      sharedTableNameCdf,\n      startingVersion = 2\n    )\n    prepareMockedClientAndFileSystemResultForCdf(\n      deltaTableName,\n      sharedTableNameCdf,\n      startingVersion = 3\n    )\n\n    withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n      val profileFile = prepareProfileFile(tempDir)\n      testReadCMCdf(\n        deltaTableName,\n        s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameCdf\",\n        0\n      )\n      testReadCMCdf(\n        deltaTableName,\n        s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameCdf\",\n        2\n      )\n      testReadCMCdf(\n        deltaTableName,\n        s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameCdf\",\n        3\n      )\n    }\n\n    val sharedTableNameDrop = sharedTableNameBase + \"_drop\"\n    // DROP COLUMN\n    sql(s\"ALTER TABLE $deltaTableName DROP COLUMN c1\")\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableNameDrop)\n    prepareMockedClientAndFileSystemResult(\n      deltaTable = deltaTableName,\n      sharedTable = sharedTableNameDrop\n    )\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableNameDrop)\n    withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n      val profileFile = prepareProfileFile(tempDir)\n      testReadCMTable(\n        deltaTableName = deltaTableName,\n        sharedTablePath = s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameDrop\",\n        dropC1 = true\n      )\n    }\n  }\n\n  /**\n   * column mapping tests\n   */\n  test(\n    \"DeltaSharingDataSource able to read data for cm name mode\"\n  ) {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_cm_name\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = true)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\"), (2, \"one\")\"\"\")\n        spark.sql(\n          s\"\"\"ALTER TABLE $deltaTableName SET TBLPROPERTIES('delta.minReaderVersion' = '2',\n             |'delta.minWriterVersion' = '5',\n             |'delta.columnMapping.mode' = 'name')\"\"\".stripMargin\n        )\n        sql(s\"\"\"ALTER TABLE $deltaTableName RENAME COLUMN c2 TO c2rename\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"two\"), (2, \"two\")\"\"\")\n\n        sql(s\"\"\"DELETE FROM $deltaTableName where c1=1\"\"\")\n        sql(s\"\"\"UPDATE $deltaTableName set c1=\"3\" where c2rename=\"one\"\"\"\")\n\n        val sharedTableName = \"shared_table_cm_name\"\n        testReadingSharedCMTable(tempDir, deltaTableName, sharedTableName)\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data for cm id mode\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_cm_id\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\"), (2, \"one\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"two\"), (2, \"two\")\"\"\")\n\n        sql(s\"\"\"ALTER TABLE $deltaTableName RENAME COLUMN c2 TO c2rename\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"two\"), (2, \"two\")\"\"\")\n\n        sql(s\"\"\"DELETE FROM $deltaTableName where c1=1\"\"\")\n        sql(s\"\"\"UPDATE $deltaTableName set c1=\"3\" where c2rename=\"one\"\"\"\")\n\n        val sharedTableName = \"shared_table_cm_id\"\n        testReadingSharedCMTable(tempDir, deltaTableName, sharedTableName)\n      }\n    }\n  }\n\n  /**\n   * Streaming Test\n   */\n  private def InsertToDeltaTable(tableName: String, values: String): Unit = {\n    sql(s\"INSERT INTO $tableName VALUES $values\")\n  }\n\n  private def processAllAvailableInStream(\n      tablePath: String,\n      checkpointDirStr: String,\n      outputDirStr: String): Unit = {\n    val q = spark.readStream\n      .format(\"deltaSharing\")\n      .option(\"responseFormat\", \"delta\")\n      .load(tablePath)\n      .writeStream\n      .format(\"delta\")\n      .option(\"checkpointLocation\", checkpointDirStr)\n      .option(\"mergeSchema\", \"true\")\n      .start(outputDirStr)\n\n    try {\n      q.processAllAvailable()\n    } finally {\n      q.stop()\n    }\n  }\n\n  private def processStreamWithSchemaTracking(\n      tablePath: String,\n      checkpointDirStr: String,\n      outputDirStr: String,\n      trigger: Option[Trigger] = None,\n      maxFilesPerTrigger: Option[Int] = None): Unit = {\n    var dataStreamReader = spark.readStream\n      .format(\"deltaSharing\")\n      .option(\"schemaTrackingLocation\", checkpointDirStr)\n      .option(\"responseFormat\", \"delta\")\n    if (maxFilesPerTrigger.isDefined || trigger.isDefined) {\n      // When trigger.Once is defined, maxFilesPerTrigger is ignored -- this is the\n      // behavior of the streaming engine. And AvailableNow is converted as Once for delta sharing.\n      dataStreamReader =\n        dataStreamReader.option(\"maxFilesPerTrigger\", maxFilesPerTrigger.getOrElse(1))\n    }\n    var dataStreamWriter = dataStreamReader\n      .load(tablePath)\n      .writeStream\n      .format(\"delta\")\n      .option(\"checkpointLocation\", checkpointDirStr)\n      .option(\"mergeSchema\", \"true\")\n    if (trigger.isDefined) {\n      dataStreamWriter = dataStreamWriter.trigger(trigger.get)\n    }\n\n    val q = dataStreamWriter.start(outputDirStr)\n\n    try {\n      q.processAllAvailable()\n      if (maxFilesPerTrigger.isDefined && trigger.isEmpty) {\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        // 2 batches -- 2 files are processed, this is how the delta table is constructed.\n        assert(progress.length === 2)\n        progress.foreach { p =>\n          assert(p.numInputRows === 2) // 2 rows per batch -- 2 rows in each file.\n        }\n      }\n    } finally {\n      q.stop()\n    }\n  }\n\n  private def prepareProcessAndCheckInitSnapshot(\n      deltaTableName: String,\n      sharedTableName: String,\n      sharedTablePath: String,\n      checkpointDirStr: String,\n      outputDir: File,\n      useSchemaTracking: Boolean,\n      trigger: Option[Trigger] = None\n  ): Unit = {\n    InsertToDeltaTable(deltaTableName, \"\"\"(1, \"one\"), (2, \"one\"), (1, \"two\")\"\"\")\n    prepareMockedClientAndFileSystemResult(\n      deltaTable = deltaTableName,\n      sharedTable = sharedTableName,\n      versionAsOf = Some(1L)\n    )\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n    prepareMockedClientMetadata(deltaTableName, sharedTableName)\n    if (useSchemaTracking) {\n      processStreamWithSchemaTracking(\n        sharedTablePath,\n        checkpointDirStr,\n        outputDir.toString,\n        trigger\n      )\n    } else {\n      processAllAvailableInStream(\n        sharedTablePath,\n        checkpointDirStr,\n        outputDir.toString\n      )\n    }\n\n    checkAnswer(\n      spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n      Seq((1, \"one\"), (2, \"one\"), (1, \"two\")).toDF()\n    )\n  }\n\n  def prepareNewInsert(\n      deltaTableName: String,\n      sharedTableName: String,\n      values: String,\n      startingVersion: Long,\n      endingVersion: Long): Unit = {\n    InsertToDeltaTable(deltaTableName, values)\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n    prepareMockedClientAndFileSystemResultForStreaming(\n      deltaTableName,\n      sharedTableName,\n      startingVersion,\n      endingVersion\n    )\n  }\n\n  private def renameColumnAndPrepareRpcResponse(\n      deltaTableName: String,\n      sharedTableName: String,\n      startingVersion: Long,\n      endingVersion: Long,\n      insertAfterRename: Boolean): Unit = {\n    // Rename on the original delta table.\n    sql(s\"\"\"ALTER TABLE $deltaTableName RENAME COLUMN c2 TO c2rename\"\"\")\n    if (insertAfterRename) {\n      InsertToDeltaTable(deltaTableName, \"\"\"(1, \"three\")\"\"\")\n      InsertToDeltaTable(deltaTableName, \"\"\"(2, \"three\")\"\"\")\n    }\n    // Prepare all the delta sharing rpcs.\n    prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n    prepareMockedClientMetadata(deltaTableName, sharedTableName)\n    prepareMockedClientAndFileSystemResultForStreaming(\n      deltaTableName,\n      sharedTableName,\n      startingVersion,\n      endingVersion\n    )\n  }\n\n  private def expectUseSchemaLogException(\n      tablePath: String,\n      checkpointDirStr: String,\n      outputDirStr: String): Unit = {\n    val error = intercept[StreamingQueryException] {\n      processAllAvailableInStream(\n        tablePath,\n        checkpointDirStr,\n        outputDirStr\n      )\n    }.toString\n    assert(error.contains(\"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_USE_SCHEMA_LOG\"))\n    assert(error.contains(\"Please provide a 'schemaTrackingLocation'\"))\n  }\n\n  private def expectMetadataEvolutionException(\n      tablePath: String,\n      checkpointDirStr: String,\n      outputDirStr: String,\n      trigger: Option[Trigger] = None,\n      maxFilesPerTrigger: Option[Int] = None): Unit = {\n    val error = intercept[StreamingQueryException] {\n      processStreamWithSchemaTracking(\n        tablePath,\n        checkpointDirStr,\n        outputDirStr,\n        trigger,\n        maxFilesPerTrigger\n      )\n    }.toString\n    assert(error.contains(\"DELTA_STREAMING_METADATA_EVOLUTION\"))\n    assert(error.contains(\"Please restart the stream to continue\"))\n  }\n\n  private def expectSqlConfException(\n      tablePath: String,\n      checkpointDirStr: String,\n      outputDirStr: String,\n      trigger: Option[Trigger] = None,\n      maxFilesPerTrigger: Option[Int] = None): Unit = {\n    val error = intercept[StreamingQueryException] {\n      processStreamWithSchemaTracking(\n        tablePath,\n        checkpointDirStr,\n        outputDirStr,\n        trigger,\n        maxFilesPerTrigger\n      )\n    }.toString\n    assert(error.contains(\"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\"))\n    assert(error.contains(\"delta.streaming.allowSourceColumnRename\") ||\n      error.contains(\"delta.streaming.allowSourceColumnDrop\"))\n  }\n\n  private def processWithSqlConf(\n      tablePath: String,\n      checkpointDirStr: String,\n      outputDirStr: String,\n      trigger: Option[Trigger] = None,\n      maxFilesPerTrigger: Option[Int] = None): Unit = {\n    // Using allowSourceColumnRenameAndDrop instead of\n    // allowSourceColumnRenameAndDrop.[checkpoint_hash] because the checkpointDir changes\n    // every test.\n    spark.conf\n      .set(\"spark.databricks.delta.streaming.allowSourceColumnRenameAndDrop\", \"always\")\n    processStreamWithSchemaTracking(\n      tablePath,\n      checkpointDirStr,\n      outputDirStr,\n      trigger,\n      maxFilesPerTrigger\n    )\n  }\n\n  private def testRestartStreamingFourTimes(\n      tablePath: String,\n      checkpointDir: java.io.File,\n      outputDirStr: String): Unit = {\n    val checkpointDirStr = checkpointDir.toString\n\n    // 1. Followed the previous error message to use schemaTrackingLocation, but received\n    // error suggesting restart.\n    expectMetadataEvolutionException(tablePath, checkpointDirStr, outputDirStr)\n\n    // 2. Followed the previous error message to restart, but need to restart again for\n    // DeltaSource to handle offset movement, this is the SAME behavior as stream reading from\n    // the delta table directly.\n    expectMetadataEvolutionException(tablePath, checkpointDirStr, outputDirStr)\n\n    // 3. Followed the previous error message to restart, but cannot write to the dest table.\n    expectSqlConfException(tablePath, checkpointDirStr, outputDirStr)\n\n    // 4. Restart with new sqlConf, able to process new data and writing to a new column.\n    // Not using allowSourceColumnRenameAndDrop.[checkpoint_hash] because the checkpointDir\n    // changes every test, using allowSourceColumnRenameAndDrop=always instead.\n    processWithSqlConf(tablePath, checkpointDirStr, outputDirStr)\n  }\n\n  test(\"cm streaming works with newly added schemaTrackingLocation\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_cm_streaming_basic\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        val sharedTableName = \"shared_table_cm_streaming_basic\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          // The streaming is started without schemaTrackingLocation.\n          prepareProcessAndCheckInitSnapshot(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            sharedTablePath = tablePath,\n            checkpointDirStr = checkpointDir.toString,\n            outputDir = outputDir,\n            useSchemaTracking = false\n          )\n\n          // 2. Able to stream new data at version 2.\n          // The streaming is continued without schemaTrackingLocation.\n          prepareNewInsert(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            values = \"\"\"(2, \"two\")\"\"\",\n            startingVersion = 2,\n            endingVersion = 2\n          )\n          processAllAvailableInStream(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString\n          )\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq((1, \"one\"), (2, \"one\"), (1, \"two\"), (2, \"two\")).toDF()\n          )\n\n          // 3. column renaming at version 3, and expect exception.\n          renameColumnAndPrepareRpcResponse(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            startingVersion = 2,\n            endingVersion = 3,\n            insertAfterRename = false\n          )\n          expectUseSchemaLogException(tablePath, checkpointDir.toString, outputDir.toString)\n\n          // 4. insert new data at version 4.\n          prepareNewInsert(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            values = \"\"\"(1, \"three\"), (2, \"three\")\"\"\",\n            startingVersion = 2,\n            endingVersion = 4\n          )\n          // Additional preparation for rpc because deltaSource moved the offset to (3, -20) and\n          // (3, -19) after restart.\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            3,\n            4\n          )\n\n          // 5. with 4 restarts, able to continue the streaming\n          // The streaming is re-started WITH schemaTrackingLocation, and it's able to capture the\n          // schema used in previous version, based on the initial call of getBatch for the latest\n          // offset, which pulls the metadata from the server.\n          testRestartStreamingFourTimes(tablePath, checkpointDir, outputDir.toString)\n\n          // An additional column is added to the output table.\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq(\n              (1, \"one\", null),\n              (2, \"one\", null),\n              (1, \"two\", null),\n              (2, \"two\", null),\n              (1, null, \"three\"),\n              (2, null, \"three\")\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n\n  test(\"cm streaming works with restart on snapshot query\") {\n    // The main difference in this test is the rename happens after processing the initial snapshot,\n    // (instead of after making continuous progress), to test that the restart could fetch the\n    // latest metadata and the metadata from lastest offset.\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_streaming_restart\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        val sharedTableName = \"shared_table_streaming_restart\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          prepareProcessAndCheckInitSnapshot(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            sharedTablePath = tablePath,\n            checkpointDirStr = checkpointDir.toString,\n            outputDir = outputDir,\n            useSchemaTracking = false\n          )\n\n          // 2. column renaming at version 2, and expect exception.\n          renameColumnAndPrepareRpcResponse(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            startingVersion = 2,\n            endingVersion = 2,\n            insertAfterRename = false\n          )\n          expectUseSchemaLogException(tablePath, checkpointDir.toString, outputDir.toString)\n\n          // 3. insert new data at version 3.\n          prepareNewInsert(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            values = \"\"\"(1, \"three\"), (2, \"three\")\"\"\",\n            startingVersion = 2,\n            endingVersion = 3\n          )\n\n          // 4. with 4 restarts, able to continue the streaming\n          testRestartStreamingFourTimes(tablePath, checkpointDir, outputDir.toString)\n\n          // An additional column is added to the output table.\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq(\n              (1, \"one\", null),\n              (2, \"one\", null),\n              (1, \"two\", null),\n              (1, null, \"three\"),\n              (2, null, \"three\")\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n\n  test(\"cm streaming works with schemaTracking used at start\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_streaming_schematracking\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        val sharedTableName = \"shared_table_streaming_schematracking\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          prepareProcessAndCheckInitSnapshot(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            sharedTablePath = tablePath,\n            checkpointDirStr = checkpointDir.toString,\n            outputDir = outputDir,\n            useSchemaTracking = true\n          )\n\n          // 2. Able to stream new data at version 2.\n          prepareNewInsert(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            values = \"\"\"(2, \"two\")\"\"\",\n            startingVersion = 2,\n            endingVersion = 2\n          )\n          processStreamWithSchemaTracking(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString\n          )\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq((1, \"one\"), (2, \"one\"), (1, \"two\"), (2, \"two\")).toDF()\n          )\n\n          // 3. column renaming at version 3, and expect exception.\n          renameColumnAndPrepareRpcResponse(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            startingVersion = 2,\n            endingVersion = 3,\n            insertAfterRename = false\n          )\n          expectMetadataEvolutionException(tablePath, checkpointDir.toString, outputDir.toString)\n\n          // 4. First see exception, then with sql conf, able to stream new data at version 4.\n          prepareNewInsert(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            values = \"\"\"(1, \"three\"), (2, \"three\")\"\"\",\n            startingVersion = 3,\n            endingVersion = 4\n          )\n          expectSqlConfException(tablePath, checkpointDir.toString, outputDir.toString)\n          processWithSqlConf(tablePath, checkpointDir.toString, outputDir.toString)\n\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq(\n              (1, \"one\", null),\n              (2, \"one\", null),\n              (1, \"two\", null),\n              (2, \"two\", null),\n              (1, null, \"three\"),\n              (2, null, \"three\")\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n\n  test(\"cm streaming works with restart with accumulated inserts after rename\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_streaming_accumulate\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        val sharedTableName = \"shared_table_streaming_accumulate\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          prepareProcessAndCheckInitSnapshot(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            sharedTablePath = tablePath,\n            checkpointDirStr = checkpointDir.toString,\n            outputDir = outputDir,\n            useSchemaTracking = false\n          )\n\n          // 2. column renaming at version 2, and expect exception.\n          renameColumnAndPrepareRpcResponse(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            startingVersion = 2,\n            endingVersion = 4,\n            insertAfterRename = true\n          )\n          expectUseSchemaLogException(tablePath, checkpointDir.toString, outputDir.toString)\n\n          // 4. with 4 restarts, able to continue the streaming\n          testRestartStreamingFourTimes(tablePath, checkpointDir, outputDir.toString)\n\n          // An additional column is added to the output table.\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq(\n              (1, \"one\", null),\n              (2, \"one\", null),\n              (1, \"two\", null),\n              (1, null, \"three\"),\n              (2, null, \"three\")\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n\n  test(\"cm streaming works with column drop and add\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_column_drop\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        val sharedTableName = \"shared_table_column_drop\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          prepareProcessAndCheckInitSnapshot(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            sharedTablePath = tablePath,\n            checkpointDirStr = checkpointDir.toString,\n            outputDir = outputDir,\n            useSchemaTracking = true\n          )\n\n          // 2. drop column c1 at version 2\n          sql(s\"ALTER TABLE $deltaTableName DROP COLUMN c1\")\n          // 3. add column c3 at version 3\n          sql(s\"ALTER TABLE $deltaTableName ADD COLUMN (c3 int)\")\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            2,\n            3\n          )\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            3,\n            3\n          )\n\n          // Needs a 3 restarts for deltaSource to catch up.\n          expectMetadataEvolutionException(tablePath, checkpointDir.toString, outputDir.toString)\n          expectSqlConfException(tablePath, checkpointDir.toString, outputDir.toString)\n          spark.conf\n            .set(\"spark.databricks.delta.streaming.allowSourceColumnRenameAndDrop\", \"always\")\n          expectMetadataEvolutionException(tablePath, checkpointDir.toString, outputDir.toString)\n          processWithSqlConf(tablePath, checkpointDir.toString, outputDir.toString)\n\n          // 4. insert at version 4\n          InsertToDeltaTable(deltaTableName, \"\"\"(\"four\", 4)\"\"\")\n          // 5. insert at version 5\n          InsertToDeltaTable(deltaTableName, \"\"\"(\"five\", 5)\"\"\")\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            3,\n            5\n          )\n\n          processStreamWithSchemaTracking(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString\n          )\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq[(java.lang.Integer, String, java.lang.Integer)](\n              (1, \"one\", null),\n              (2, \"one\", null),\n              (1, \"two\", null),\n              (null, \"four\", 4),\n              (null, \"five\", 5)\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n\n  test(\"streaming works with column type widened\") {\n    // Technically not a column mapping test, but type widening and column mapping are handled in\n    // the same way in DeltaSource, as non-additive schema changes.\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_column_type_widened\"\n      withTable(deltaTableName) {\n        sql(s\"\"\"CREATE TABLE $deltaTableName (c1 BYTE, c2 STRING) USING DELTA PARTITIONED BY (c2)\n           |TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\n           |\"\"\".stripMargin)\n        val sharedTableName = \"shared_table_column_type_widened\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          prepareProcessAndCheckInitSnapshot(\n            deltaTableName = deltaTableName,\n            sharedTableName = sharedTableName,\n            sharedTablePath = tablePath,\n            checkpointDirStr = checkpointDir.toString,\n            outputDir = outputDir,\n            useSchemaTracking = true\n          )\n\n          // Enable type widening on the sink to automatically change the type when writing to it\n          // after widening the type in the source.\n          sql(s\"\"\"ALTER TABLE delta.`$outputDir`\n              |SET TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\n              |\"\"\".stripMargin)\n\n          // 2. change column type at version 2\n          sql(s\"ALTER TABLE $deltaTableName ALTER COLUMN c1 TYPE INT\")\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            2,\n            2\n          )\n\n          // Needs 3 restarts for deltaSource to catch up.\n          expectMetadataEvolutionException(tablePath, checkpointDir.toString, outputDir.toString)\n          val error = intercept[StreamingQueryException] {\n            processStreamWithSchemaTracking(tablePath, checkpointDir.toString, outputDir.toString)\n          }.toString()\n          assert(error.contains(\"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\"))\n          assert(error.contains(\"TYPE WIDENING\"))\n          assert(error.contains(\"delta.streaming.allowSourceColumnTypeChange\"))\n\n          // Unblocking allows the type change to go through\n          spark.conf.set(\"spark.databricks.delta.streaming.allowSourceColumnTypeChange\", \"always\")\n          processStreamWithSchemaTracking(tablePath, checkpointDir.toString, outputDir.toString)\n\n          // 3. insert at version 3\n          InsertToDeltaTable(deltaTableName, s\"\"\"(${Int.MaxValue}, \"max\")\"\"\")\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            2,\n            3\n          )\n\n          processStreamWithSchemaTracking(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString\n          )\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq[(java.lang.Integer, String)](\n              (1, \"one\"),\n              (2, \"one\"),\n              (1, \"two\"),\n              (Int.MaxValue, \"max\")\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n\n\n  test(\"cm streaming works with MaxFilesPerTrigger\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_maxfiles\"\n      withTable(deltaTableName) {\n        createCMIdTableWithCdf(deltaTableName)\n        val sharedTableName = \"shared_table_maxfiles\"\n        val profileFile = prepareProfileFile(inputDir)\n        val tablePath = profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          // 1. Able to stream snapshot at version 1.\n          InsertToDeltaTable(deltaTableName, \"\"\"(1, \"one\"), (2, \"one\"), (1, \"two\"), (2, \"two\")\"\"\")\n          prepareMockedClientAndFileSystemResult(\n            deltaTable = deltaTableName,\n            sharedTable = sharedTableName,\n            versionAsOf = Some(1L)\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n\n          // process with maxFilesPerTrigger.\n          processStreamWithSchemaTracking(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString,\n            trigger = None,\n            maxFilesPerTrigger = Some(1)\n          )\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq((1, \"one\"), (2, \"one\"), (1, \"two\"), (2, \"two\")).toDF()\n          )\n\n          // 2. column renaming at version 2, no exception because of Trigger.Once.\n          sql(s\"\"\"ALTER TABLE $deltaTableName RENAME COLUMN c2 TO c2rename\"\"\")\n\n          // Prepare all the delta sharing rpcs.\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientMetadata(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            startingVersion = 1,\n            endingVersion = 2\n          )\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            startingVersion = 2,\n            endingVersion = 2\n          )\n\n          // maxFilesPerTrigger doesn't change whether exception is thrown or not.\n          expectMetadataEvolutionException(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString,\n            trigger = None,\n            maxFilesPerTrigger = Some(1)\n          )\n\n          // 4. First see exception, then with sql conf, able to stream new data at version 4 and 5.\n          InsertToDeltaTable(\n            deltaTableName,\n            \"\"\"(1, \"three\"), (2, \"three\"), (1, \"four\"), (2, \"four\")\"\"\"\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForStreaming(\n            deltaTableName,\n            sharedTableName,\n            2,\n            3\n          )\n\n          expectSqlConfException(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString,\n            trigger = None,\n            maxFilesPerTrigger = Some(1)\n          )\n          processWithSqlConf(\n            tablePath,\n            checkpointDir.toString,\n            outputDir.toString,\n            trigger = None,\n            maxFilesPerTrigger = Some(1)\n          )\n\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            Seq(\n              (1, \"one\", null),\n              (2, \"one\", null),\n              (1, \"two\", null),\n              (2, \"two\", null),\n              (1, null, \"three\"),\n              (2, null, \"three\"),\n              (1, null, \"four\"),\n              (2, null, \"four\")\n            ).toDF()\n          )\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingDataSourceDeltaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\n\n// scalastyle:off import.ordering.noEmptyLine\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, VariantShreddingPreviewTableFeature, VariantTypePreviewTableFeature, VariantTypeTableFeature}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.{\n  DateType,\n  IntegerType,\n  LongType,\n  StringType,\n  StructType,\n  TimestampNTZType,\n  TimestampType\n}\n\ntrait DeltaSharingDataSourceDeltaSuiteBase\n    extends QueryTest\n    with DeltaSQLCommandTest\n    with DeltaSharingTestSparkUtils\n    with DeltaSharingDataSourceDeltaTestUtils {\n\n  override def beforeEach(): Unit = {\n    spark.sessionState.conf.setConfString(\n      \"spark.delta.sharing.jsonPredicateV2Hints.enabled\",\n      \"false\"\n    )\n  }\n\n  /**\n   * metadata tests\n   */\n  test(\"failed to getMetadata\") {\n    withTempDir { tempDir =>\n      val sharedTableName = \"shared_table_broken_json\"\n\n      def test(tablePath: String, tableFullName: String): Unit = {\n        DeltaSharingUtils.overrideIteratorBlock[String](\n          blockId = TestClientForDeltaFormatSharing.getBlockId(sharedTableName, \"getMetadata\"),\n          values = Seq(\"bad protocol string\", \"bad metadata string\").toIterator\n        )\n        DeltaSharingUtils.overrideSingleBlock[Long](\n          blockId = TestClientForDeltaFormatSharing.getBlockId(sharedTableName, \"getTableVersion\"),\n          value = 1\n        )\n        // JsonParseException on \"bad protocol string\"\n        val exception = intercept[com.fasterxml.jackson.core.JsonParseException] {\n          spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath).schema\n        }\n        assert(exception.getMessage.contains(\"Unrecognized token 'bad'\"))\n\n        // table_with_broken_protocol\n        // able to parse as a DeltaSharingSingleAction, but it's an addFile, not metadata.\n        DeltaSharingUtils.overrideIteratorBlock[String](\n          blockId = TestClientForDeltaFormatSharing.getBlockId(sharedTableName, \"getMetadata\"),\n          // scalastyle:off line.size.limit\n          values = Seq(\n            \"\"\"{\"add\": {\"path\":\"random\",\"id\":\"random\",\"partitionValues\":{},\"size\":1,\"motificationTime\":1,\"dataChange\":false}}\"\"\"\n          ).toIterator\n        )\n        val exception2 = intercept[IllegalStateException] {\n          spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath).schema\n        }\n        assert(\n          exception2.getMessage\n            .contains(s\"Failed to get Protocol for $tableFullName\")\n        )\n\n        // table_with_broken_metadata\n        // able to parse as a DeltaSharingSingleAction, but it's an addFile, not metadata.\n        DeltaSharingUtils.overrideIteratorBlock[String](\n          blockId = TestClientForDeltaFormatSharing.getBlockId(sharedTableName, \"getMetadata\"),\n          values = Seq(\n            \"\"\"{\"protocol\":{\"minReaderVersion\":1}}\"\"\"\n          ).toIterator\n        )\n        val exception3 = intercept[IllegalStateException] {\n          spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath).schema\n        }\n        assert(\n          exception3.getMessage\n            .contains(s\"Failed to get Metadata for $tableFullName\")\n        )\n      }\n\n      withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n        val profileFile = prepareProfileFile(tempDir)\n        val tableFullName = s\"share1.default.$sharedTableName\"\n        test(s\"${profileFile.getCanonicalPath}#$tableFullName\", tableFullName)\n      }\n    }\n  }\n\n  def assertLimit(tableName: String, expectedLimit: Seq[Long]): Unit = {\n    assert(expectedLimit ==\n      TestClientForDeltaFormatSharing.limits.filter(_._1.contains(tableName)).map(_._2))\n  }\n\n  def assertJsonPredicateHints(tableName: String, expectedHints: Seq[String]): Unit = {\n    assert(expectedHints ==\n      TestClientForDeltaFormatSharing.jsonPredicateHints.filter(_._1.contains(tableName)).map(_._2)\n    )\n  }\n  /**\n   * snapshot queries\n   */\n  test(\"DeltaSharingDataSource able to read simple data\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_simple\"\n      withTable(deltaTableName) {\n        createTable(deltaTableName)\n        sql(\n          s\"INSERT INTO $deltaTableName\" +\n          \"\"\" VALUES (1, \"one\", \"2023-01-01\", \"2023-01-01 00:00:00\"),\n              |(2, \"two\", \"2023-02-02\", \"2023-02-02 00:00:00\")\"\"\".stripMargin\n        )\n\n        val sharedTableName = \"shared_table_simple\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        val expectedSchema: StructType = new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2\", StringType)\n          .add(\"c3\", DateType)\n          .add(\"c4\", TimestampType)\n        val expected = Seq(\n          Row(1, \"one\", sqlDate(\"2023-01-01\"), sqlTimestamp(\"2023-01-01 00:00:00\")),\n          Row(2, \"two\", sqlDate(\"2023-02-02\"), sqlTimestamp(\"2023-02-02 00:00:00\"))\n        )\n\n        Seq(true, false).foreach { skippingEnabled =>\n          Seq(true, false).foreach { sharingConfig =>\n            Seq(true, false).foreach { deltaConfig =>\n              val sharedTableName = s\"shared_table_simple_\" +\n                s\"${skippingEnabled}_${sharingConfig}_$deltaConfig\"\n              prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n              prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName, limitHint = Some(1))\n              prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n              def test(tablePath: String, tableName: String): Unit = {\n                assert(\n                  expectedSchema == spark.read\n                    .format(\"deltaSharing\")\n                    .option(\"responseFormat\", \"delta\")\n                    .load(tablePath)\n                    .schema\n                )\n                val df =\n                  spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n                  checkAnswer(df, expected)\n                assert(df.count() > 0)\n                assertLimit(tableName, Seq.empty[Long])\n                val limitDf = spark.read\n                  .format(\"deltaSharing\")\n                  .option(\"responseFormat\", \"delta\")\n                  .load(tablePath)\n                  .limit(1)\n                assert(limitDf.collect().size == 1)\n                assertLimit(tableName, Some(1L).filter(_ => skippingEnabled && sharingConfig && deltaConfig).toSeq)\n              }\n\n              val limitPushdownConfigs = Map(\n                \"spark.delta.sharing.limitPushdown.enabled\" -> sharingConfig.toString,\n                DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key -> deltaConfig.toString,\n                DeltaSQLConf.DELTA_STATS_SKIPPING.key -> skippingEnabled.toString\n              )\n              withSQLConf((limitPushdownConfigs ++ getDeltaSharingClassesSQLConf).toSeq: _*) {\n                val profileFile = prepareProfileFile(tempDir)\n                val tableName = s\"share1.default.$sharedTableName\"\n                test(s\"${profileFile.getCanonicalPath}#$tableName\", tableName)\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with changes\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_change\"\n\n      def test(tablePath: String, expectedCount: Int, expectedSchema: StructType): Unit = {\n        assert(\n          expectedSchema == spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .load(tablePath)\n            .schema\n        )\n\n        val deltaDf = spark.read.format(\"delta\").table(deltaTableName)\n        val sharingDf =\n          spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n        checkAnswer(deltaDf, sharingDf)\n        assert(sharingDf.count() == expectedCount)\n      }\n\n      withTable(deltaTableName) {\n        val sharedTableName = \"shared_table_change\"\n        createTable(deltaTableName)\n\n        // test 1: insert 2 rows\n        sql(\n          s\"INSERT INTO $deltaTableName\" +\n            \"\"\" VALUES (1, \"one\", \"2023-01-01\", \"2023-01-01 00:00:00\"),\n              |(2, \"two\", \"2023-02-02\", \"2023-02-02 00:00:00\")\"\"\".stripMargin\n        )\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        val expectedSchema: StructType = new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2\", StringType)\n          .add(\"c3\", DateType)\n          .add(\"c4\", TimestampType)\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          val tableName = s\"share1.default.$sharedTableName\"\n          test(s\"${profileFile.getCanonicalPath}#$tableName\", 2, expectedSchema)\n        }\n\n        // test 2: insert 2 more rows, and rename a column\n        spark.sql(\n          s\"\"\"ALTER TABLE $deltaTableName SET TBLPROPERTIES('delta.minReaderVersion' = '2',\n             |'delta.minWriterVersion' = '5',\n             |'delta.columnMapping.mode' = 'name', 'delta.enableDeletionVectors' = true)\"\"\".stripMargin\n        )\n        sql(\n          s\"INSERT INTO $deltaTableName\" +\n            \"\"\" VALUES (3, \"three\", \"2023-03-03\", \"2023-03-03 00:00:00\"),\n              |(4, \"four\", \"2023-04-04\", \"2023-04-04 00:00:00\")\"\"\".stripMargin\n        )\n        sql(s\"\"\"ALTER TABLE $deltaTableName RENAME COLUMN c3 TO c3rename\"\"\")\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        val expectedNewSchema: StructType = new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2\", StringType)\n          .add(\"c3rename\", DateType)\n          .add(\"c4\", TimestampType)\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          val tableName = s\"share1.default.$sharedTableName\"\n          test(s\"${profileFile.getCanonicalPath}#$tableName\", 4, expectedNewSchema)\n        }\n\n        // test 3: delete 1 row\n        sql(s\"DELETE FROM $deltaTableName WHERE c1 = 2\")\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          val tableName = s\"share1.default.$sharedTableName\"\n          test(s\"${profileFile.getCanonicalPath}#$tableName\", 3, expectedNewSchema)\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to auto resolve responseFormat\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_auto\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        sql(\n          s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\"), (2, \"one\")\"\"\".stripMargin\n        )\n        sql(\n          s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"two\"), (2, \"two\")\"\"\".stripMargin\n        )\n\n        val expectedSchema: StructType = new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2\", StringType)\n\n        def testAutoResolve(tablePath: String, tableName: String, expectedFormat: String): Unit = {\n          assert(\n            expectedSchema == spark.read\n              .format(\"deltaSharing\")\n              .load(tablePath)\n              .schema\n          )\n\n          val deltaDf = spark.read.format(\"delta\").table(deltaTableName)\n          val sharingDf = spark.read.format(\"deltaSharing\").load(tablePath)\n          checkAnswer(deltaDf, sharingDf)\n          assert(sharingDf.count() > 0)\n          assertLimit(tableName, Seq.empty[Long])\n          assertRequestedFormat(tableName, Seq(expectedFormat))\n\n          val limitDf = spark.read\n            .format(\"deltaSharing\")\n            .load(tablePath)\n            .limit(1)\n          assert(limitDf.collect().size == 1)\n          assertLimit(tableName, Seq(1L))\n\n          val deltaDfV1 = spark.read.format(\"delta\").option(\"versionAsOf\", 1).table(deltaTableName)\n          val sharingDfV1 =\n            spark.read.format(\"deltaSharing\").option(\"versionAsOf\", 1).load(tablePath)\n          checkAnswer(deltaDfV1, sharingDfV1)\n          assert(sharingDfV1.count() > 0)\n          assertRequestedFormat(tableName, Seq(expectedFormat))\n        }\n\n        // Test for delta format response\n        val sharedDeltaTable = \"shared_delta_table\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedDeltaTable)\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedDeltaTable, limitHint = Some(1))\n        prepareMockedClientAndFileSystemResult(\n          deltaTableName,\n          sharedDeltaTable,\n          versionAsOf = Some(1)\n        )\n        prepareMockedClientGetTableVersion(deltaTableName, sharedDeltaTable)\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testAutoResolve(\n            s\"${profileFile.getCanonicalPath}#share1.default.$sharedDeltaTable\",\n            s\"share1.default.$sharedDeltaTable\",\n            \"delta\"\n          )\n        }\n\n        // Test for parquet format response\n        val sharedParquetTable = \"shared_parquet_table\"\n        prepareMockedClientAndFileSystemResultForParquet(\n          deltaTableName,\n          sharedParquetTable\n        )\n        prepareMockedClientAndFileSystemResultForParquet(\n          deltaTableName,\n          sharedParquetTable,\n          limitHint = Some(1)\n        )\n        prepareMockedClientAndFileSystemResultForParquet(\n          deltaTableName,\n          sharedParquetTable,\n          versionAsOf = Some(1)\n        )\n        prepareMockedClientGetTableVersion(deltaTableName, sharedParquetTable)\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testAutoResolve(\n            s\"${profileFile.getCanonicalPath}#share1.default.$sharedParquetTable\",\n            s\"share1.default.$sharedParquetTable\",\n            \"parquet\"\n          )\n        }\n\n        // Build a parquet table and query with delta format\n        // Use a unique table name for this test as assertRequestedFormat is using a global map\n        val sharedParquetTableForDeltaFormat = \"shared_parquet_table_for_delta_format\"\n        // Use prepareMockedClientAndFileSystemResult not ForParquet because fromJson requires DeltaSharingMetadata\n        prepareMockedClientAndFileSystemResult(\n          deltaTableName,\n          sharedParquetTableForDeltaFormat\n        )\n        prepareMockedClientAndFileSystemResult(\n          deltaTableName,\n          sharedParquetTableForDeltaFormat,\n          limitHint = Some(1)\n        )\n        prepareMockedClientAndFileSystemResult(\n          deltaTableName,\n          sharedParquetTableForDeltaFormat,\n          versionAsOf = Some(1)\n        )\n        prepareMockedClientGetTableVersion(deltaTableName, sharedParquetTableForDeltaFormat)\n        val overrideConfigs = Map(DeltaSQLConf.DELTA_SHARING_FORCE_DELTA_FORMAT.key -> \"true\")\n        withSQLConf((overrideConfigs ++ getDeltaSharingClassesSQLConf).toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testAutoResolve(\n            s\"${profileFile.getCanonicalPath}#share1.default.$sharedParquetTableForDeltaFormat\",\n            s\"share1.default.$sharedParquetTableForDeltaFormat\",\n            \"delta\"\n          )\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with filters and select\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_filters\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"first\"), (2, \"first\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"second\"), (2, \"second\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"third\"), (2, \"third\")\"\"\")\n\n        Seq(\"c1\", \"c2\", \"c1c2\").foreach { filterColumn =>\n          val sharedTableName = s\"shared_table_filters_$filterColumn\"\n          prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n          spark.sessionState.conf.setConfString(\n            \"spark.delta.sharing.jsonPredicateV2Hints.enabled\",\n            \"true\"\n          )\n\n          // The files returned from delta sharing client are the same for these queries.\n          // This is to test the filters are passed correctly to TahoeLogFileIndex for the local delta\n          // log.\n          def testFiltersAndSelect(tablePath: String, tableName: String): Unit = {\n            // select\n            var expected = Seq(Row(1), Row(1), Row(1), Row(2), Row(2), Row(2))\n            var df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .select(\"c1\")\n            checkAnswer(df, expected)\n            assertJsonPredicateHints(tableName, Seq.empty[String])\n\n            expected = Seq(\n              Row(\"first\"),\n              Row(\"first\"),\n              Row(\"second\"),\n              Row(\"second\"),\n              Row(\"third\"),\n              Row(\"third\")\n            )\n            df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .select(\"c2\")\n            checkAnswer(df, expected)\n            assertJsonPredicateHints(tableName, Seq.empty[String])\n\n            // filter\n            var expectedJson = \"\"\n            if (filterColumn == \"c1c2\") {\n              expected = Seq(Row(1, \"first\"), Row(1, \"second\"), Row(1, \"third\"), Row(2, \"second\"))\n              df = spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .filter(col(\"c1\") === 1 || col(\"c2\") === \"second\")\n              checkAnswer(df, expected)\n              expectedJson =\n                \"\"\"{\"op\":\"or\",\"children\":[\n                  |  {\"op\":\"equal\",\"children\":[\n                  |    {\"op\":\"column\",\"name\":\"c1\",\"valueType\":\"int\"},\n                  |    {\"op\":\"literal\",\"value\":\"1\",\"valueType\":\"int\"}]},\n                  |  {\"op\":\"equal\",\"children\":[\n                  |    {\"op\":\"column\",\"name\":\"c2\",\"valueType\":\"string\"},\n                  |    {\"op\":\"literal\",\"value\":\"second\",\"valueType\":\"string\"}]}\n                  |]}\"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\")\n              assertJsonPredicateHints(tableName, Seq(expectedJson))\n            } else if (filterColumn == \"c1\") {\n              expected = Seq(Row(1, \"first\"), Row(1, \"second\"), Row(1, \"third\"))\n              df = spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .filter(col(\"c1\") === 1)\n              checkAnswer(df, expected)\n              expectedJson =\n                \"\"\"{\"op\":\"and\",\"children\":[\n                  |  {\"op\":\"not\",\"children\":[\n                  |    {\"op\":\"isNull\",\"children\":[\n                  |      {\"op\":\"column\",\"name\":\"c1\",\"valueType\":\"int\"}]}]},\n                  |  {\"op\":\"equal\",\"children\":[\n                  |    {\"op\":\"column\",\"name\":\"c1\",\"valueType\":\"int\"},\n                  |    {\"op\":\"literal\",\"value\":\"1\",\"valueType\":\"int\"}]}\n                  |]}\"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\")\n              assertJsonPredicateHints(tableName, Seq(expectedJson))\n            } else {\n              assert(filterColumn == \"c2\")\n              expected = Seq(Row(1, \"second\"), Row(2, \"second\"))\n              expectedJson =\n                \"\"\"{\"op\":\"and\",\"children\":[\n                  |  {\"op\":\"not\",\"children\":[\n                  |    {\"op\":\"isNull\",\"children\":[\n                  |      {\"op\":\"column\",\"name\":\"c2\",\"valueType\":\"string\"}]}]},\n                  |  {\"op\":\"equal\",\"children\":[\n                  |    {\"op\":\"column\",\"name\":\"c2\",\"valueType\":\"string\"},\n                  |    {\"op\":\"literal\",\"value\":\"second\",\"valueType\":\"string\"}]}\n                  |]}\"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\")\n              df = spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .filter(col(\"c2\") === \"second\")\n              checkAnswer(df, expected)\n              assertJsonPredicateHints(tableName, Seq(expectedJson))\n\n              // filters + select as well\n              expected = Seq(Row(1), Row(2))\n              df = spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .filter(col(\"c2\") === \"second\")\n                .select(\"c1\")\n              checkAnswer(df, expected)\n              assertJsonPredicateHints(tableName, Seq(expectedJson))\n            }\n          }\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            testFiltersAndSelect(\n              s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\",\n              s\"share1.default.$sharedTableName\"\n            )\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with different filters\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_diff_filter\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"first\"), (2, \"first\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"second\"), (2, \"second\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"third\"), (2, \"third\")\"\"\")\n\n        val sharedTableName = s\"shared_table_filters_diff_filter\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName, limitHint = Some(2))\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        spark.sessionState.conf.setConfString(\n          \"spark.delta.sharing.jsonPredicateV2Hints.enabled\",\n          \"true\"\n        )\n\n        // The files returned from delta sharing client are the same for these queries.\n        // This is to test the filters are passed correctly to TahoeLogFileIndex for the local delta\n        // log.\n        def testDiffFilter(tablePath: String, tableName: String): Unit = {\n          val df = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .load(tablePath)\n\n          // limit\n          assert(df.limit(2).count() == 2)\n          // full\n          val expectedFull = Seq(\n            Row(1, \"first\"), Row(1, \"second\"), Row(1, \"third\"),\n            Row(2, \"first\"), Row(2, \"second\"), Row(2, \"third\")\n          )\n          checkAnswer(df, expectedFull)\n        }\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testDiffFilter(\n            s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\",\n            s\"share1.default.$sharedTableName\"\n          )\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data for time travel queries\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_time_travel\"\n      withTable(deltaTableName) {\n        createTable(deltaTableName)\n\n        sql(\n          s\"INSERT INTO $deltaTableName\" +\n          \"\"\" VALUES (1, \"one\", \"2023-01-01\", \"2023-01-01 00:00:00\")\"\"\".stripMargin\n        )\n        sql(\n          s\"INSERT INTO $deltaTableName\" +\n          \"\"\" VALUES (2, \"two\", \"2023-02-02\", \"2023-02-02 00:00:00\")\"\"\".stripMargin\n        )\n        sql(\n          s\"INSERT INTO $deltaTableName\" +\n          \"\"\" VALUES (3, \"three\", \"2023-03-03\", \"2023-03-03 00:00:00\")\"\"\".stripMargin\n        )\n\n        val sharedTableNameV1 = \"shared_table_v1\"\n        prepareMockedClientAndFileSystemResult(\n          deltaTable = deltaTableName,\n          sharedTable = sharedTableNameV1,\n          versionAsOf = Some(1L)\n        )\n\n        def testVersionAsOf1(tablePath: String): Unit = {\n          val dfV1 = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"versionAsOf\", 1)\n            .load(tablePath)\n          val expectedV1 = Seq(\n            Row(1, \"one\", sqlDate(\"2023-01-01\"), sqlTimestamp(\"2023-01-01 00:00:00\"))\n          )\n            checkAnswer(dfV1, expectedV1)\n        }\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testVersionAsOf1(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameV1\")\n        }\n\n        // using different table name because spark caches the content read from a file, i.e.,\n        // the delta log from 0.json.\n        // TODO: figure out how to get a per query id and use it in getCustomTablePath to\n        //  differentiate the same table used in different queries.\n        // TODO: Also check if it's possible to disable the file cache.\n        val sharedTableNameV3 = \"shared_table_v3\"\n        prepareMockedClientAndFileSystemResult(\n          deltaTable = deltaTableName,\n          sharedTable = sharedTableNameV3,\n          versionAsOf = Some(3L)\n        )\n\n        def testVersionAsOf3(tablePath: String): Unit = {\n          val dfV3 = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"versionAsOf\", 3)\n            .load(tablePath)\n          val expectedV3 = Seq(\n            Row(1, \"one\", sqlDate(\"2023-01-01\"), sqlTimestamp(\"2023-01-01 00:00:00\")),\n            Row(2, \"two\", sqlDate(\"2023-02-02\"), sqlTimestamp(\"2023-02-02 00:00:00\")),\n            Row(3, \"three\", sqlDate(\"2023-03-03\"), sqlTimestamp(\"2023-03-03 00:00:00\"))\n          )\n            checkAnswer(dfV3, expectedV3)\n        }\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testVersionAsOf3(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameV3\")\n        }\n\n        val sharedTableNameTs = \"shared_table_ts\"\n        // Given the result of delta sharing rpc is mocked, the actual value of the timestampStr\n        // can be any thing that's valid for DeltaSharingOptions, and formattedTimestamp is the\n        // parsed result and will be sent in the delta sharing rpc.\n        val timestampStr = \"2023-01-01 00:00:00\"\n        val formattedTimestamp = \"2023-01-01T08:00:00Z\"\n\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableNameTs)\n        prepareMockedClientAndFileSystemResult(\n          deltaTable = deltaTableName,\n          sharedTable = sharedTableNameTs,\n          versionAsOf = None,\n          timestampAsOf = Some(formattedTimestamp)\n        )\n\n        def testTimestampQuery(tablePath: String): Unit = {\n          val dfTs = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"timestampAsOf\", timestampStr)\n            .load(tablePath)\n          val expectedTs = Seq(\n            Row(1, \"one\", sqlDate(\"2023-01-01\"), sqlTimestamp(\"2023-01-01 00:00:00\")),\n            Row(2, \"two\", sqlDate(\"2023-02-02\"), sqlTimestamp(\"2023-02-02 00:00:00\")),\n            Row(3, \"three\", sqlDate(\"2023-03-03\"), sqlTimestamp(\"2023-03-03 00:00:00\"))\n          )\n            checkAnswer(dfTs, expectedTs)\n        }\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testTimestampQuery(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableNameTs\")\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with more entries\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_more\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        // The table operations take about 6~10 seconds.\n        for (i <- 0 to 9) {\n          val iteration = s\"iteration $i\"\n          val valuesBuilder = Seq.newBuilder[String]\n          for (j <- 0 to 49) {\n            valuesBuilder += s\"\"\"(${i * 10 + j}, \"$iteration\")\"\"\"\n          }\n          sql(s\"INSERT INTO $deltaTableName VALUES ${valuesBuilder.result().mkString(\",\")}\")\n        }\n\n        val sharedTableName = \"shared_table_more\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        val expectedSchema: StructType = new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2\", StringType)\n        val expected = spark.read.format(\"delta\").table(deltaTableName)\n\n        def test(tablePath: String): Unit = {\n          assert(\n            expectedSchema == spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .schema\n          )\n          val df =\n            spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n          checkAnswer(df, expected)\n        }\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          test(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with join on the same table\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_join\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"first\"), (2, \"first\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"second\"), (2, \"second\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"third\"), (2, \"third\")\"\"\")\n\n        val sharedTableName = \"shared_table_join\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        prepareMockedClientAndFileSystemResult(\n          deltaTableName,\n          sharedTableName,\n          versionAsOf = Some(1L)\n        )\n\n        def testJoin(tablePath: String): Unit = {\n          // Query the same latest version\n          val deltaDfLatest = spark.read.format(\"delta\").table(deltaTableName)\n          val deltaDfV1 = spark.read.format(\"delta\").option(\"versionAsOf\", 1).table(deltaTableName)\n          val sharingDfLatest =\n            spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n          val sharingDfV1 =\n            spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"versionAsOf\", 1)\n              .load(tablePath)\n\n          var deltaDfJoined = deltaDfLatest.join(deltaDfLatest, \"c1\")\n          var sharingDfJoined = sharingDfLatest.join(sharingDfLatest, \"c1\")\n          // CheckAnswer ensures that delta sharing produces the same result as delta.\n          // The check on the size is used to double check that a valid dataframe is generated.\n          checkAnswer(deltaDfJoined, sharingDfJoined)\n          assert(sharingDfJoined.count() > 0)\n\n          // Query the same versionAsOf\n          deltaDfJoined = deltaDfV1.join(deltaDfV1, \"c1\")\n          sharingDfJoined = sharingDfV1.join(sharingDfV1, \"c1\")\n          checkAnswer(deltaDfJoined, sharingDfJoined)\n          assert(sharingDfJoined.count() > 0)\n\n          // Query with different versions\n          deltaDfJoined = deltaDfLatest.join(deltaDfV1, \"c1\")\n          sharingDfJoined = sharingDfLatest.join(sharingDfV1, \"c1\")\n          checkAnswer(deltaDfJoined, sharingDfJoined)\n          // Size is 6 because for each of the 6 rows in latest, there is 1 row with the same c1\n          // value in v1.\n          assert(sharingDfJoined.count() > 0)\n        }\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testJoin(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read empty data\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_empty\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = true)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"first\"), (2, \"first\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"second\"), (2, \"second\")\"\"\")\n        sql(s\"DELETE FROM $deltaTableName WHERE c1 <= 2\")\n        // This command is just to create an empty table version at version 4.\n        spark.sql(s\"ALTER TABLE $deltaTableName SET TBLPROPERTIES('delta.minReaderVersion' = 1)\")\n\n        val sharedTableName = \"shared_table_empty\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        def testEmpty(tablePath: String): Unit = {\n          val deltaDf = spark.read.format(\"delta\").table(deltaTableName)\n          val sharingDf =\n            spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n          checkAnswer(deltaDf, sharingDf)\n          assert(sharingDf.count() == 0)\n\n          val deltaCdfDf = spark.read\n            .format(\"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", 4)\n            .table(deltaTableName)\n          val sharingCdfDf = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", 4)\n            .load(tablePath)\n          checkAnswer(deltaCdfDf, sharingCdfDf)\n          assert(sharingCdfDf.count() == 0)\n        }\n\n        // There's only metadata change but not actual files in version 4.\n        prepareMockedClientAndFileSystemResultForCdf(\n          deltaTableName,\n          sharedTableName,\n          startingVersion = 4\n        )\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testEmpty(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n        }\n      }\n    }\n  }\n\n  /**\n   * cdf queries\n   */\n  test(\"DeltaSharingDataSource able to read data for simple cdf query\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_cdf\"\n      withTable(deltaTableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $deltaTableName (c1 INT, c2 STRING) USING DELTA PARTITIONED BY (c2)\n               |TBLPROPERTIES (delta.enableChangeDataFeed = true)\n               |\"\"\".stripMargin)\n        // 2 inserts in version 1, 1 with c1=2\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\"), (2, \"two\")\"\"\")\n        // 1 insert in version 2, 0 with c1=2\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (3, \"two\")\"\"\")\n        // 0 operations in version 3\n        sql(s\"\"\"OPTIMIZE $deltaTableName\"\"\")\n        // 2 updates in version 4, 2 with c1=2\n        sql(s\"\"\"UPDATE $deltaTableName SET c2=\"new two\" where c1=2\"\"\")\n        // 1 delete in version 5, 1 with c1=2\n        sql(s\"\"\"DELETE FROM $deltaTableName WHERE c1 = 2\"\"\")\n\n        val sharedTableName = \"shard_table_cdf\"\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        Seq(0, 1, 2, 3, 4, 5).foreach { startingVersion =>\n          val ts = getTimeStampForVersion(deltaTableName, startingVersion)\n          val startingTimestamp = DateTimeUtils.toJavaTimestamp(ts * 1000).toInstant.toString\n          prepareMockedClientAndFileSystemResultForCdf(\n            deltaTableName,\n            sharedTableName,\n            startingVersion,\n            Some(startingTimestamp)\n          )\n\n          def test(tablePath: String): Unit = {\n            val expectedSchema: StructType = new StructType()\n              .add(\"c1\", IntegerType)\n              .add(\"c2\", StringType)\n              .add(\"_change_type\", StringType)\n              .add(\"_commit_version\", LongType)\n              .add(\"_commit_timestamp\", TimestampType)\n            val schema = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n              .schema\n            assert(expectedSchema == schema)\n\n            val expected = spark.read\n              .format(\"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .table(deltaTableName)\n            val df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n            checkAnswer(df, expected)\n            assert(df.count() > 0)\n          }\n\n          def testFiltersAndSelect(tablePath: String): Unit = {\n            val expectedSchema: StructType = new StructType()\n              .add(\"c2\", StringType)\n              .add(\"_change_type\", StringType)\n              .add(\"_commit_version\", LongType)\n            val schema = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n              .select(\"c2\", \"_change_type\", \"_commit_version\")\n              .schema\n            assert(expectedSchema == schema)\n\n            val expected = spark.read\n              .format(\"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .table(deltaTableName)\n              .select(\"c2\", \"_change_type\", \"_commit_version\")\n            val dfVersion = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n              .select(\"c2\", \"_change_type\", \"_commit_version\")\n            checkAnswer(dfVersion, expected)\n            val dfTime = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingTimestamp\", startingTimestamp)\n              .load(tablePath)\n              .select(\"c2\", \"_change_type\", \"_commit_version\")\n            checkAnswer(dfTime, expected)\n            assert(dfTime.count() > 0)\n\n            val expectedFiltered = spark.read\n              .format(\"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .table(deltaTableName)\n              .select(\"c2\", \"_change_type\", \"_commit_version\")\n              .filter(col(\"c1\") === 2)\n            val dfFiltered = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n              .select(\"c2\", \"_change_type\", \"_commit_version\")\n              .filter(col(\"c1\") === 2)\n            checkAnswer(dfFiltered, expectedFiltered)\n            assert(dfFiltered.count() > 0)\n          }\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            test(profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\")\n            testFiltersAndSelect(\n              profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\"\n            )\n          }\n        }\n\n        // test join on the same table in cdf query\n        def testJoin(tablePath: String): Unit = {\n          val deltaV0 = spark.read\n            .format(\"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", 0)\n            .table(deltaTableName)\n          val deltaV3 = spark.read\n            .format(\"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", 3)\n            .table(deltaTableName)\n          val sharingV0 = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", 0)\n            .load(tablePath)\n          val sharingV3 = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", 3)\n            .load(tablePath)\n\n          def testJoinedDf(\n              deltaLeft: DataFrame,\n              deltaRight: DataFrame,\n              sharingLeft: DataFrame,\n              sharingRight: DataFrame,\n              expectedSize: Int): Unit = {\n            val deltaJoined = deltaLeft.join(deltaRight, usingColumns = Seq(\"c1\", \"c2\"))\n            val sharingJoined = sharingLeft.join(sharingRight, usingColumns = Seq(\"c1\", \"c2\"))\n            checkAnswer(deltaJoined, sharingJoined)\n            assert(sharingJoined.count() > 0)\n          }\n          testJoinedDf(deltaV0, deltaV0, sharingV0, sharingV0, 10)\n          testJoinedDf(deltaV3, deltaV3, sharingV3, sharingV3, 5)\n          testJoinedDf(deltaV0, deltaV3, sharingV0, sharingV3, 6)\n        }\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testJoin(profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\")\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data for cdf query with more entries\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_cdf_more\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = true)\n        // The table operations take about 20~30 seconds.\n        for (i <- 0 to 9) {\n          val iteration = s\"iteration $i\"\n          val valuesBuilder = Seq.newBuilder[String]\n          for (j <- 0 to 49) {\n            valuesBuilder += s\"\"\"(${i * 10 + j}, \"$iteration\")\"\"\"\n          }\n          sql(s\"INSERT INTO $deltaTableName VALUES ${valuesBuilder.result().mkString(\",\")}\")\n          sql(s\"\"\"UPDATE $deltaTableName SET c1 = c1 + 100 where c2 = \"${iteration}\"\"\"\")\n          sql(s\"\"\"DELETE FROM $deltaTableName where c2 = \"${iteration}\"\"\"\")\n        }\n\n        val sharedTableName = \"shard_table_cdf_more\"\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        Seq(0, 10, 20, 30).foreach { startingVersion =>\n          prepareMockedClientAndFileSystemResultForCdf(\n            deltaTableName,\n            sharedTableName,\n            startingVersion\n          )\n\n          val expected = spark.read\n            .format(\"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", startingVersion)\n            .table(deltaTableName)\n\n          def test(tablePath: String): Unit = {\n            val df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n            checkAnswer(df, expected)\n            assert(df.count() > 0)\n          }\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            test(profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with special chars\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_special\"\n      withTable(deltaTableName) {\n        // scalastyle:off nonascii\n        sql(s\"\"\"CREATE TABLE $deltaTableName (`第一列` INT, c2 STRING)\n               |USING DELTA PARTITIONED BY (c2)\n               |\"\"\".stripMargin)\n        // The table operations take about 6~10 seconds.\n        for (i <- 0 to 99) {\n          val iteration = s\"iteration $i\"\n          val valuesBuilder = Seq.newBuilder[String]\n          for (j <- 0 to 99) {\n            valuesBuilder += s\"\"\"(${i * 10 + j}, \"$iteration\")\"\"\"\n          }\n          sql(s\"INSERT INTO $deltaTableName VALUES ${valuesBuilder.result().mkString(\",\")}\")\n        }\n\n        val sharedTableName = \"shared_table_more\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        val expectedSchema: StructType = new StructType()\n          .add(\"第一列\", IntegerType)\n          .add(\"c2\", StringType)\n        // scalastyle:on nonascii\n        val expected = spark.read.format(\"delta\").table(deltaTableName)\n\n        def test(tablePath: String): Unit = {\n          assert(\n            expectedSchema == spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .schema\n          )\n          val df =\n            spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n          checkAnswer(df, expected)\n        }\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          test(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read cdf with special chars\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_cdf_special\"\n      withTable(deltaTableName) {\n        // scalastyle:off nonascii\n        sql(s\"\"\"CREATE TABLE $deltaTableName (`第一列` INT, c2 STRING)\n               |USING DELTA PARTITIONED BY (c2)\n               |TBLPROPERTIES(\n               |delta.enableChangeDataFeed = true\n               |)\"\"\".stripMargin)\n        // The table operations take about 20~30 seconds.\n        for (i <- 0 to 9) {\n          val iteration = s\"iteration $i\"\n          val valuesBuilder = Seq.newBuilder[String]\n          for (j <- 0 to 49) {\n            valuesBuilder += s\"\"\"(${i * 10 + j}, \"$iteration\")\"\"\"\n          }\n          sql(s\"INSERT INTO $deltaTableName VALUES ${valuesBuilder.result().mkString(\",\")}\")\n          sql(s\"\"\"UPDATE $deltaTableName SET `第一列` = `第一列` + 100 where c2 = \"${iteration}\"\"\"\")\n          // scalastyle:on nonascii\n          sql(s\"\"\"DELETE FROM $deltaTableName where c2 = \"${iteration}\"\"\"\")\n        }\n\n        val sharedTableName = \"shard_table_cdf_special\"\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        Seq(0, 10, 20, 30).foreach { startingVersion =>\n          prepareMockedClientAndFileSystemResultForCdf(\n            deltaTableName,\n            sharedTableName,\n            startingVersion\n          )\n\n          val expected = spark.read\n            .format(\"delta\")\n            .option(\"readChangeFeed\", \"true\")\n            .option(\"startingVersion\", startingVersion)\n            .table(deltaTableName)\n\n          def test(tablePath: String): Unit = {\n            val df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n            checkAnswer(df, expected)\n            assert(df.count() > 0)\n          }\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            test(profileFile.getCanonicalPath + s\"#share1.default.$sharedTableName\")\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * deletion vector tests\n   */\n  test(\"DeltaSharingDataSource able to read data for dv table\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_dv\"\n      withTable(deltaTableName) {\n        spark\n          .range(start = 0, end = 100)\n          .withColumn(\"partition\", col(\"id\").divide(10).cast(\"int\"))\n          .write\n          .partitionBy(\"partition\")\n          .format(\"delta\")\n          .saveAsTable(deltaTableName)\n        spark\n          .range(start = 100, end = 200)\n          .withColumn(\"partition\", col(\"id\").mod(100).divide(10).cast(\"int\"))\n          .write\n          .mode(\"append\")\n          .partitionBy(\"partition\")\n          .format(\"delta\")\n          .saveAsTable(deltaTableName)\n        spark.sql(\n          s\"ALTER TABLE $deltaTableName SET TBLPROPERTIES('delta.enableDeletionVectors' = true)\"\n        )\n\n        // Delete 2 rows per partition.\n        sql(s\"\"\"DELETE FROM $deltaTableName where mod(id, 10) < 2\"\"\")\n        // Delete 1 more row per partition.\n        sql(s\"\"\"DELETE FROM $deltaTableName where mod(id, 10) = 3\"\"\")\n        // Delete 1 more row per partition.\n        sql(s\"\"\"DELETE FROM $deltaTableName where mod(id, 10) = 6\"\"\")\n\n        Seq(true, false).foreach { skippingEnabled =>\n          val sharedTableName = s\"shared_table_dv_$skippingEnabled\"\n          prepareMockedClientAndFileSystemResult(\n            deltaTable = deltaTableName,\n            sharedTable = sharedTableName,\n            assertMultipleDvsInOneFile = true\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n          def testReadDVTable(tablePath: String): Unit = {\n            val expectedSchema: StructType = new StructType()\n              .add(\"id\", LongType)\n              .add(\"partition\", IntegerType)\n            assert(\n              expectedSchema == spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .schema\n            )\n\n            val sharingDf =\n              spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n            val deltaDf = spark.read.format(\"delta\").table(deltaTableName)\n            val filteredSharingDf =\n              spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .filter(col(\"id\").mod(10) > 5)\n            val filteredDeltaDf =\n              spark.read\n                .format(\"delta\")\n                .table(deltaTableName)\n                .filter(col(\"id\").mod(10) > 5)\n\n            if (!skippingEnabled) {\n              def assertError(dataFrame: DataFrame): Unit = {\n                val ex = intercept[IllegalArgumentException] {\n                  dataFrame.collect()\n                }\n                assert(ex.getMessage contains\n                  \"Cannot work with a non-pinned table snapshot of the TahoeFileIndex\")\n              }\n              assertError(sharingDf)\n              assertError(filteredDeltaDf)\n            } else {\n            checkAnswer(sharingDf, deltaDf)\n            assert(sharingDf.count() > 0)\n            checkAnswer(filteredSharingDf, filteredDeltaDf)\n            assert(filteredSharingDf.count() > 0)\n            }\n          }\n\n          val additionalConfigs = Map(\n            DeltaSQLConf.DELTA_STATS_SKIPPING.key -> skippingEnabled.toString\n          )\n          withSQLConf((additionalConfigs ++ getDeltaSharingClassesSQLConf).toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            testReadDVTable(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data for dv and cdf\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_dv_cdf\"\n      withTable(deltaTableName) {\n        createDVTableWithCdf(deltaTableName)\n        // version 1: 20 inserts\n        spark\n          .range(start = 0, end = 20)\n          .select(col(\"id\").cast(\"int\").as(\"c1\"))\n          .withColumn(\"partition\", col(\"c1\").divide(10).cast(\"int\"))\n          .write\n          .mode(\"append\")\n          .format(\"delta\")\n          .saveAsTable(deltaTableName)\n        // version 2: 20 inserts\n        spark\n          .range(start = 100, end = 120)\n          .select(col(\"id\").cast(\"int\").as(\"c1\"))\n          .withColumn(\"partition\", col(\"c1\").mod(100).divide(10).cast(\"int\"))\n          .write\n          .mode(\"append\")\n          .format(\"delta\")\n          .saveAsTable(deltaTableName)\n        // version 3: 20 updates\n        sql(s\"\"\"UPDATE $deltaTableName SET c1=c1+5 where partition=0\"\"\")\n        // This deletes will create one DV file used by AddFile from both version 1 and version 2.\n        // version 4: 14 deletes\n        sql(s\"\"\"DELETE FROM $deltaTableName WHERE mod(c1, 100)<=10\"\"\")\n\n        val sharedTableName = \"shard_table_dv_cdf\"\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        Seq(0, 1, 2, 3, 4).foreach { startingVersion =>\n          prepareMockedClientAndFileSystemResultForCdf(\n            deltaTableName,\n            sharedTableName,\n            startingVersion,\n            assertMultipleDvsInOneFile = true\n          )\n\n          def testReadDVCdf(tablePath: String): Unit = {\n            val schema = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n              .schema\n            val expectedSchema: StructType = new StructType()\n              .add(\"c1\", IntegerType)\n              .add(\"partition\", IntegerType)\n              .add(\"_change_type\", StringType)\n              .add(\"_commit_version\", LongType)\n              .add(\"_commit_timestamp\", TimestampType)\n            assert(expectedSchema == schema)\n\n            val deltaDf = spark.read\n              .format(\"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .table(deltaTableName)\n            val sharingDf = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", startingVersion)\n              .load(tablePath)\n            checkAnswer(sharingDf, deltaDf)\n            assert(sharingDf.count() > 0)\n          }\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            testReadDVCdf(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data for inline dv\") {\n    import org.apache.spark.sql.delta.deletionvectors.RoaringBitmapArrayFormat\n    Seq(RoaringBitmapArrayFormat.Portable, RoaringBitmapArrayFormat.Native).foreach { format =>\n      withTempDir { tempDir =>\n        val deltaTableName = s\"delta_table_inline_dv_$format\"\n        withTable(deltaTableName) {\n          createDVTableWithCdf(deltaTableName)\n          // Use divide 10 to set partition column to 0 for all values, then use repartition to\n          // ensure the 5 values are written in one file.\n          spark\n            .range(start = 0, end = 5)\n            .select(col(\"id\").cast(\"int\").as(\"c1\"))\n            .withColumn(\"partition\", col(\"c1\").divide(10).cast(\"int\"))\n            .repartition(1)\n            .write\n            .mode(\"append\")\n            .format(\"delta\")\n            .saveAsTable(deltaTableName)\n\n          val sharedTableName = s\"shared_table_inline_dv_$format\"\n          prepareMockedClientAndFileSystemResult(\n            deltaTable = deltaTableName,\n            sharedTable = sharedTableName,\n            inlineDvFormat = Some(format)\n          )\n          prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n          prepareMockedClientAndFileSystemResultForCdf(\n            deltaTableName,\n            sharedTableName,\n            startingVersion = 1,\n            inlineDvFormat = Some(format)\n          )\n\n          def testReadInlineDVCdf(tablePath: String): Unit = {\n            val deltaDf = spark.read\n              .format(\"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", 1)\n              .table(deltaTableName)\n              .filter(col(\"c1\") > 1)\n            val sharingDf = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", 1)\n              .load(tablePath)\n            checkAnswer(sharingDf, deltaDf)\n            assert(sharingDf.count() > 0)\n          }\n\n          def testReadInlineDV(tablePath: String): Unit = {\n            val expectedSchema: StructType = new StructType()\n              .add(\"c1\", IntegerType)\n              .add(\"partition\", IntegerType)\n            assert(\n              expectedSchema == spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .schema\n            )\n\n            val sharingDf =\n              spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n            val expectedDf = Seq(Row(1, 0), Row(3, 0), Row(4, 0))\n            checkAnswer(sharingDf, expectedDf)\n\n            val filteredSharingDf =\n              spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n                .filter(col(\"c1\") < 4)\n            val expectedFilteredDf = Seq(Row(1, 0), Row(3, 0))\n            checkAnswer(filteredSharingDf, expectedFilteredDf)\n          }\n\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            testReadInlineDV(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n            testReadInlineDVCdf(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read timestampNTZ table\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_timestampNTZ\"\n      withTable(deltaTableName) {\n        sql(s\"CREATE TABLE $deltaTableName(c1 TIMESTAMP_NTZ) USING DELTA\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES ('2022-01-02 03:04:05.123456')\"\"\")\n\n        val sharedTableName = \"shared_table_timestampNTZ\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        def testReadTimestampNTZ(tablePath: String): Unit = {\n          val expectedSchema: StructType = new StructType()\n            .add(\"c1\", TimestampNTZType)\n          assert(\n            expectedSchema == spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .load(tablePath)\n              .schema\n          )\n          val sharingDf =\n            spark.read.format(\"deltaSharing\").option(\"responseFormat\", \"delta\").load(tablePath)\n          val deltaDf = spark.read.format(\"delta\").table(deltaTableName)\n          checkAnswer(sharingDf, deltaDf)\n          assert(sharingDf.count() > 0)\n        }\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(tempDir)\n          testReadTimestampNTZ(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n        }\n      }\n    }\n  }\n\n  Seq(\n    VariantTypePreviewTableFeature,\n    VariantTypeTableFeature,\n    VariantShreddingPreviewTableFeature\n  ).foreach { feature =>\n    test(s\"basic variant test - table feature: $feature\") {\n      withTempDir { tempDir =>\n        val extraConfs = feature match {\n          case VariantShreddingPreviewTableFeature => Map(\n            \"spark.sql.variant.writeShredding.enabled\" -> \"true\",\n            \"spark.sql.variant.allowReadingShredded\" -> \"true\",\n            \"spark.sql.variant.forceShreddingSchemaForTest\" -> \"a long\"\n          )\n          case _ => Map.empty\n        }\n        withSQLConf(extraConfs.toSeq: _*) {\n          val deltaTableName = s\"variant_table_${feature.name.replaceAll(\"-\", \"_\")}\"\n          withTable(deltaTableName) {\n            if (feature == VariantShreddingPreviewTableFeature) {\n              spark.sql(s\"CREATE TABLE $deltaTableName(v variant) USING DELTA \" +\n                s\"TBLPROPERTIES('${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'true')\")\n            } else {\n              spark.sql(s\"CREATE TABLE $deltaTableName(v variant) USING DELTA \" +\n                s\"TBLPROPERTIES('delta.feature.${feature.name}' = 'supported')\")\n            }\n\n            spark.range(0, 10000, 1, 1)\n              .selectExpr(\"\"\"parse_json(format_string('{\"a\": %d}', id)) v\"\"\")\n              .write\n              .format(\"delta\")\n              .mode(\"append\")\n              .insertInto(deltaTableName)\n\n            val sharedTableName = s\"shared_table_variant_${feature.name.replaceAll(\"-\", \"_\")}\"\n            prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n            prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n            val expectedSchemaString = \"StructType(StructField(v,VariantType,true))\"\n            val expected = spark.read.format(\"delta\").table(deltaTableName)\n\n            def test(tablePath: String): Unit = {\n              val sharedDf = spark.read\n                .format(\"deltaSharing\")\n                .option(\"responseFormat\", \"delta\")\n                .load(tablePath)\n              assert(expectedSchemaString == sharedDf.schema.toString)\n              checkAnswer(sharedDf, expected)\n            }\n\n            withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n              val profileFile = prepareProfileFile(tempDir)\n              test(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"DeltaSharingDataSource able to read data with inline credentials\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_inline_creds\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\"), (2, \"two\")\"\"\")\n\n        val sharedTableName = \"shared_table_inline_creds\"\n        prepareMockedClientAndFileSystemResult(deltaTableName, sharedTableName)\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n\n        val map = Map(\n          \"shareCredentialsVersion\" -> \"1\",\n          \"bearerToken\" -> \"xxx\",\n          \"endpoint\" -> \"https://xxx/delta-sharing/\",\n          \"expirationTime\" -> \"2099-01-01T00:00:00.000Z\"\n        )\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val expectedSchema: StructType = new StructType()\n            .add(\"c1\", IntegerType)\n            .add(\"c2\", StringType)\n\n          val df = spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .options(map)\n            .load(s\"share1.default.$sharedTableName\")\n\n          assert(expectedSchema == df.schema)\n          val expected = spark.read.format(\"delta\").table(deltaTableName)\n          checkAnswer(df, expected)\n        }\n      }\n    }\n  }\n\n  test(\"deleted file retention duration check is not applied for time-travel on delta-sharing tables\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_time_travel_retention\"\n      withTable(deltaTableName) {\n        // file and log retention is set to 0 but still able to time-travel because of skipping enforcement.\n        sql(s\"\"\"\n               |CREATE TABLE $deltaTableName (c1 INT, c2 STRING) USING DELTA PARTITIONED BY (c2)\n               |TBLPROPERTIES ('delta.deletedFileRetentionDuration' = '0 hours',\n               |'delta.logRetentionDuration' = '0 hours')\n               |\"\"\".stripMargin)\n\n        // Insert multiple versions\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (2, \"two\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (3, \"three\")\"\"\")\n\n        val sharedTableName = \"shared_table_time_travel_retention\"\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        prepareMockedClientAndFileSystemResult(\n          deltaTable = deltaTableName,\n          sharedTable = sharedTableName,\n          versionAsOf = Some(1L)\n        )\n\n        // Enable enforcement config - delta-sharing tables should still skip enforcement\n        withSQLConf(\n          DeltaSQLConf.ENFORCE_TIME_TRAVEL_WITHIN_DELETED_FILE_RETENTION_DURATION.key -> \"true\"\n        ) {\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            val tablePath = s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\"\n\n            // This should succeed even with enforcement enabled because delta-sharing\n            // tables use \"delta-sharing-log\" filesystem scheme and skip enforcement\n            val df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"versionAsOf\", 1)\n              .load(tablePath)\n\n            val expected = Seq(Row(1, \"one\"))\n              checkAnswer(df, expected)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"deleted file retention duration check is not applied for cdf on delta-sharing tables\") {\n    withTempDir { tempDir =>\n      val deltaTableName = \"delta_table_cdc_retention\"\n      withTable(deltaTableName) {\n        // file and log retention is set to 0 but still able to time-travel because of skipping enforcement.\n        sql(s\"\"\"\n               |CREATE TABLE $deltaTableName (c1 INT, c2 STRING) USING DELTA PARTITIONED BY (c2)\n               |TBLPROPERTIES (delta.enableChangeDataFeed = true,\n               |'delta.deletedFileRetentionDuration' = '0 hours',\n               |'delta.logRetentionDuration' = '0 hours')\n               |\"\"\".stripMargin)\n\n        // Insert multiple versions\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (2, \"two\")\"\"\")\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (3, \"three\")\"\"\")\n\n        val sharedTableName = \"shared_table_cdc_retention\"\n        prepareMockedClientGetTableVersion(deltaTableName, sharedTableName)\n        prepareMockedClientAndFileSystemResultForCdf(\n          deltaTable = deltaTableName,\n          sharedTable = sharedTableName,\n          startingVersion = 0L\n        )\n\n        // Enable enforcement config - delta-sharing tables should still skip enforcement\n        withSQLConf(\n          DeltaSQLConf.ENFORCE_TIME_TRAVEL_WITHIN_DELETED_FILE_RETENTION_DURATION.key -> \"true\"\n        ) {\n          withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n            val profileFile = prepareProfileFile(tempDir)\n            val tablePath = s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\"\n\n            val df = spark.read\n              .format(\"deltaSharing\")\n              .option(\"responseFormat\", \"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", 0)\n              .load(tablePath)\n              .select(\"c1\", \"c2\", \"_change_type\", \"_commit_version\")\n\n            // CDF should return inserts for all 3 versions (1, 2, 3)\n            // Version 0 is table creation, inserts start from version 1\n            val expected = Seq(\n              Row(1, \"one\", \"insert\", 1L),\n              Row(2, \"two\", \"insert\", 2L),\n              Row(3, \"three\", \"insert\", 3L)\n            )\n            checkAnswer(df, expected)\n          }\n        }\n      }\n    }\n  }\n  test(\"callerOrg option is passed to DeltaSharingRestClient\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaTableName = \"delta_table_caller_org\"\n      withTable(deltaTableName) {\n        createSimpleTable(deltaTableName, enableCdf = false)\n        sql(s\"\"\"INSERT INTO $deltaTableName VALUES (1, \"one\")\"\"\")\n\n        val sharedTableName = \"shared_table_caller_org\"\n        prepareMockedClientAndFileSystemResult(\n          deltaTableName, sharedTableName)\n        DeltaSharingUtils.overrideSingleBlock[Long](\n          blockId = TestClientForDeltaFormatSharing.getBlockId(\n            sharedTableName, \"getTableVersion\"),\n          value = 1\n        )\n\n        withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n          val profileFile = prepareProfileFile(inputDir)\n          val tablePath =\n            s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\"\n\n          TestClientForDeltaFormatSharing.lastCallerOrg = \"\"\n          spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .option(DeltaSharingOptions.CALLER_ORG_OPTION, \"test-org\")\n            .load(tablePath)\n            .collect()\n          assert(\n            TestClientForDeltaFormatSharing.lastCallerOrg == \"test-org\",\n            \"callerOrg should be passed through to the client\"\n          )\n\n          TestClientForDeltaFormatSharing.lastCallerOrg = \"\"\n          spark.read\n            .format(\"deltaSharing\")\n            .option(\"responseFormat\", \"delta\")\n            .load(tablePath)\n            .collect()\n          assert(\n            TestClientForDeltaFormatSharing.lastCallerOrg == \"\",\n            \"callerOrg should be empty when not set\"\n          )\n        }\n      }\n    }\n  }\n}\n\nclass DeltaSharingDataSourceDeltaSuite extends DeltaSharingDataSourceDeltaSuiteBase {}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingDataSourceDeltaTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.{DeltaLog, Snapshot}\nimport org.apache.spark.sql.delta.actions.{\n  Action,\n  AddCDCFile,\n  AddFile,\n  DeletionVectorDescriptor,\n  Metadata,\n  RemoveFile\n}\nimport org.apache.spark.sql.delta.deletionvectors.{\n  RoaringBitmapArray,\n  RoaringBitmapArrayFormat\n}\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport com.google.common.hash.Hashing\nimport io.delta.sharing.client.model.{\n  AddFile => ClientAddFile,\n  Metadata => ClientMetadata,\n  Protocol => ClientProtocol\n}\nimport io.delta.sharing.spark.model.{\n  DeltaSharingFileAction,\n  DeltaSharingMetadata,\n  DeltaSharingProtocol\n}\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait DeltaSharingDataSourceDeltaTestUtils extends SharedSparkSession {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // close DeltaSharingFileSystem to avoid impact from other unit tests.\n    FileSystem.closeAll()\n  }\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(\"spark.delta.sharing.preSignedUrl.expirationMs\", \"30000\")\n      .set(\"spark.delta.sharing.driver.refreshCheckIntervalMs\", \"3000\")\n      .set(\"spark.delta.sharing.driver.refreshThresholdMs\", \"10000\")\n      .set(\"spark.delta.sharing.driver.accessThresholdToExpireMs\", \"300000\")\n  }\n\n  private[spark] def removePartitionPrefix(filePath: String): String = {\n    filePath.split(\"/\").last\n  }\n\n  private def getResponseDVAndId(\n      sharedTable: String,\n      deletionVector: DeletionVectorDescriptor): (DeletionVectorDescriptor, String) = {\n    if (deletionVector != null) {\n      if (deletionVector.storageType == DeletionVectorDescriptor.INLINE_DV_MARKER) {\n        (deletionVector, Hashing.sha256().hashString(deletionVector.uniqueId, UTF_8).toString)\n      } else {\n        val dvPath = deletionVector.absolutePath(new Path(\"not-used\"))\n        (\n          deletionVector.copy(\n            pathOrInlineDv = TestDeltaSharingFileSystem.encode(sharedTable,\n              SparkPath.fromPathString(dvPath.getName).urlEncoded),\n            storageType = DeletionVectorDescriptor.PATH_DV_MARKER\n          ),\n          Hashing.sha256().hashString(deletionVector.uniqueId, UTF_8).toString\n        )\n      }\n    } else {\n      (null, null)\n    }\n  }\n\n  private def isDataFile(filePath: String): Boolean = {\n    filePath.endsWith(\".parquet\") || filePath.endsWith(\".bin\")\n  }\n\n  // Convert from delta AddFile to DeltaSharingFileAction to serialize to json.\n  private def getDeltaSharingFileActionForAddFile(\n      addFile: AddFile,\n      sharedTable: String,\n      version: Long,\n      timestamp: Long): DeltaSharingFileAction = {\n    val parquetFile = removePartitionPrefix(addFile.path)\n\n    val (responseDV, dvFileId) = getResponseDVAndId(sharedTable, addFile.deletionVector)\n\n    DeltaSharingFileAction(\n      id = Hashing.sha256().hashString(parquetFile, UTF_8).toString,\n      version = version,\n      timestamp = timestamp,\n      deletionVectorFileId = dvFileId,\n      deltaSingleAction = addFile\n        .copy(\n          path = TestDeltaSharingFileSystem.encode(sharedTable, parquetFile),\n          deletionVector = responseDV\n        )\n        .wrap\n    )\n  }\n\n  // Convert from delta RemoveFile to DeltaSharingFileAction to serialize to json.\n  // scalastyle:off removeFile\n  private def getDeltaSharingFileActionForRemoveFile(\n      removeFile: RemoveFile,\n      sharedTable: String,\n      version: Long,\n      timestamp: Long): DeltaSharingFileAction = {\n    val parquetFile = removePartitionPrefix(removeFile.path)\n\n    val (responseDV, dvFileId) = getResponseDVAndId(sharedTable, removeFile.deletionVector)\n\n    DeltaSharingFileAction(\n      id = Hashing.sha256().hashString(parquetFile, UTF_8).toString,\n      version = version,\n      timestamp = timestamp,\n      deletionVectorFileId = dvFileId,\n      deltaSingleAction = removeFile\n        .copy(\n          path = TestDeltaSharingFileSystem.encode(sharedTable, parquetFile),\n          deletionVector = responseDV\n        )\n        .wrap\n    )\n    // scalastyle:on removeFile\n  }\n\n  // Reset the result for client.GetTableVersion for the sharedTable based on the latest table\n  // version of the deltaTable, use BlockManager to store the result.\n  private[spark] def prepareMockedClientGetTableVersion(\n      deltaTable: String,\n      sharedTable: String,\n      inputVersion: Option[Long] = None): Unit = {\n    DeltaSharingUtils.overrideSingleBlock[Long](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(sharedTable, \"getTableVersion\"),\n      value = inputVersion.getOrElse(getSnapshotToUse(deltaTable, None).version)\n    )\n  }\n\n  def getTimeStampForVersion(deltaTable: String, version: Long): Long = {\n    val snapshotToUse = getSnapshotToUse(deltaTable, None)\n    FileUtils\n      .listFiles(new File(snapshotToUse.deltaLog.logPath.toUri()), null, true)\n      .asScala\n      .foreach { f =>\n        if (FileNames.isDeltaFile(new Path(f.getName))) {\n          if (FileNames.getFileVersion(new Path(f.getName)) == version) {\n            return f.lastModified\n          }\n        }\n      }\n    0\n  }\n\n  // Prepare the result(Protocol and Metadata) for client.GetMetadata for the sharedTable based on\n  // the latest table info of the deltaTable, store them in BlockManager.\n  private[spark] def prepareMockedClientMetadata(deltaTable: String, sharedTable: String): Unit = {\n    val snapshotToUse = getSnapshotToUse(deltaTable, None)\n    val dsProtocol: DeltaSharingProtocol = DeltaSharingProtocol(snapshotToUse.protocol)\n    val dsMetadata: DeltaSharingMetadata = DeltaSharingMetadata(\n      deltaMetadata = snapshotToUse.metadata\n    )\n\n    // Put the metadata in blockManager for DeltaSharingClient to return for getMetadata.\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(sharedTable, \"getMetadata\"),\n      values = Seq(dsProtocol.json, dsMetadata.json).toIterator\n    )\n  }\n\n  private def updateAddFileWithInlineDV(\n      addFile: AddFile,\n      inlineDvFormat: RoaringBitmapArrayFormat.Value,\n      bitmap: RoaringBitmapArray): AddFile = {\n    val dv = DeletionVectorDescriptor.inlineInLog(\n      bitmap.serializeAsByteArray(inlineDvFormat),\n      bitmap.cardinality\n    )\n    addFile\n      .removeRows(\n        deletionVector = dv,\n        updateStats = true\n      )\n      ._1\n  }\n\n  private def updateDvPathToCount(\n      addFile: AddFile,\n      pathToCount: scala.collection.mutable.Map[String, Int]): Unit = {\n    if (addFile.deletionVector != null &&\n      addFile.deletionVector.storageType != DeletionVectorDescriptor.INLINE_DV_MARKER) {\n      val dvPath = addFile.deletionVector.pathOrInlineDv\n      pathToCount.put(dvPath, pathToCount.getOrElse(dvPath, 0) + 1)\n    }\n  }\n\n  // Sort by id in decreasing order.\n  private def deltaSharingFileActionDecreaseOrderFunc(\n      f1: model.DeltaSharingFileAction,\n      f2: model.DeltaSharingFileAction): Boolean = {\n    f1.id > f2.id\n  }\n\n  // Sort by id in increasing order.\n  private def deltaSharingFileActionIncreaseOrderFunc(\n      f1: model.DeltaSharingFileAction,\n      f2: model.DeltaSharingFileAction): Boolean = {\n    f1.id < f2.id\n  }\n\n  private def getSnapshotToUse(deltaTable: String, versionAsOf: Option[Long]): Snapshot = {\n    val deltaLog = DeltaLog.forTable(spark, new TableIdentifier(deltaTable))\n    if (versionAsOf.isDefined) {\n      deltaLog.getSnapshotAt(versionAsOf.get)\n    } else {\n      deltaLog.update()\n    }\n  }\n\n  // This function does 2 jobs:\n  // 1. Prepare the result for functions of delta sharing rest client, i.e., (Protocol, Metadata)\n  // for getMetadata, (Protocol, Metadata, and list of lines from delta actions) for getFiles, use\n  // BlockManager to store the data to make them available across different classes. All the lines\n  // are for responseFormat=parquet.\n  // 2. Put the parquet file in blockManager for DeltaSharingFileSystem to load bytes out of it.\n  private[spark] def prepareMockedClientAndFileSystemResultForParquet(\n      deltaTable: String,\n      sharedTable: String,\n      versionAsOf: Option[Long] = None,\n      limitHint: Option[Long] = None): Unit = {\n    val lines = Seq.newBuilder[String]\n    var totalSize = 0L\n    val clientAddFilesArrayBuffer = ArrayBuffer[ClientAddFile]()\n\n    // To prepare faked delta sharing responses with needed files for DeltaSharingClient.\n    val snapshotToUse = getSnapshotToUse(deltaTable, versionAsOf)\n\n    snapshotToUse.allFiles.collect().foreach { addFile =>\n      val parquetFile = removePartitionPrefix(addFile.path)\n      val clientAddFile = ClientAddFile(\n        url = TestDeltaSharingFileSystem.encode(sharedTable, parquetFile),\n        id = Hashing.md5().hashString(parquetFile, UTF_8).toString,\n        partitionValues = addFile.partitionValues,\n        size = addFile.size,\n        stats = null,\n        version = snapshotToUse.version,\n        timestamp = snapshotToUse.timestamp\n      )\n      totalSize = totalSize + addFile.size\n      clientAddFilesArrayBuffer += clientAddFile\n    }\n\n    // Scan through the parquet files of the local delta table, and prepare the data of parquet file\n    // reading in DeltaSharingFileSystem.\n    val files =\n      FileUtils.listFiles(new File(snapshotToUse.deltaLog.dataPath.toUri()), null, true).asScala\n    files.foreach { f =>\n      val filePath = f.getCanonicalPath\n      val fileName = SparkPath.fromPathString(f.getName).urlEncoded\n      if (isDataFile(filePath)) {\n        // Put the parquet file in blockManager for DeltaSharingFileSystem to load bytes out of it.\n        DeltaSharingUtils.overrideIteratorBlock[Byte](\n          blockId = TestDeltaSharingFileSystem.getBlockId(sharedTable, fileName),\n          values = FileUtils.readFileToByteArray(f).toIterator\n        )\n      }\n    }\n\n    val clientProtocol = ClientProtocol(minReaderVersion = 1)\n    // This is specifically to set the size of the metadata.\n    val deltaMetadata = snapshotToUse.metadata\n    val clientMetadata = ClientMetadata(\n      id = deltaMetadata.id,\n      name = deltaMetadata.name,\n      description = deltaMetadata.description,\n      schemaString = deltaMetadata.schemaString,\n      configuration = deltaMetadata.configuration,\n      partitionColumns = deltaMetadata.partitionColumns,\n      size = totalSize\n    )\n    lines += JsonUtils.toJson(clientProtocol.wrap)\n    lines += JsonUtils.toJson(clientMetadata.wrap)\n    clientAddFilesArrayBuffer.toSeq.foreach { clientAddFile =>\n      lines += JsonUtils.toJson(clientAddFile.wrap)\n    }\n\n    // Put the metadata in blockManager for DeltaSharingClient to return metadata when being asked.\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(\n        sharedTableName = sharedTable,\n        queryType = \"getMetadata\",\n        versionAsOf = versionAsOf\n      ),\n      values = Seq(\n        JsonUtils.toJson(clientProtocol.wrap),\n        JsonUtils.toJson(clientMetadata.wrap)\n      ).toIterator\n    )\n\n    // Put the delta log (list of actions) in blockManager for DeltaSharingClient to return as the\n    // http response when getFiles is called.\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(\n        sharedTableName = sharedTable,\n        queryType = \"getFiles\",\n        versionAsOf = versionAsOf,\n        limit = limitHint\n      ),\n      values = lines.result().toIterator\n    )\n  }\n\n  // This function does 2 jobs:\n  // 1. Prepare the result for functions of delta sharing rest client, i.e., (Protocol, Metadata)\n  // for getMetadata, (Protocol, Metadata, and list of lines from delta actions) for getFiles, use\n  // BlockManager to store the data to make them available across different classes.\n  // 2. Put the parquet file in blockManager for DeltaSharingFileSystem to load bytes out of it.\n  private[spark] def prepareMockedClientAndFileSystemResult(\n      deltaTable: String,\n      sharedTable: String,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      inlineDvFormat: Option[RoaringBitmapArrayFormat.Value] = None,\n      assertMultipleDvsInOneFile: Boolean = false,\n      reverseFileOrder: Boolean = false,\n      limitHint: Option[Long] = None): Unit = {\n    val lines = Seq.newBuilder[String]\n    var totalSize = 0L\n\n    // To prepare faked delta sharing responses with needed files for DeltaSharingClient.\n    val snapshotToUse = getSnapshotToUse(deltaTable, versionAsOf)\n    val fileActionsArrayBuffer = ArrayBuffer[model.DeltaSharingFileAction]()\n    val dvPathToCount = scala.collection.mutable.Map[String, Int]()\n    var numRecords = 0L\n    snapshotToUse.allFiles.collect().foreach { addFile =>\n      if (assertMultipleDvsInOneFile) {\n        updateDvPathToCount(addFile, dvPathToCount)\n      }\n\n      val updatedAdd = if (inlineDvFormat.isDefined) {\n        // Remove row 0 and 2 in the AddFile.\n        updateAddFileWithInlineDV(addFile, inlineDvFormat.get, RoaringBitmapArray(0L, 2L))\n      } else {\n        addFile\n      }\n\n      if (limitHint.isEmpty || limitHint.map(_ > numRecords).getOrElse(true)) {\n        val dsAddFile = getDeltaSharingFileActionForAddFile(\n          updatedAdd,\n          sharedTable,\n          snapshotToUse.version,\n          snapshotToUse.timestamp\n        )\n        numRecords += addFile.numLogicalRecords.getOrElse(0L)\n        totalSize = totalSize + addFile.size\n        fileActionsArrayBuffer += dsAddFile\n      }\n    }\n    val fileActionSeq = if (reverseFileOrder) {\n      fileActionsArrayBuffer.toSeq.sortWith(deltaSharingFileActionDecreaseOrderFunc)\n    } else {\n      fileActionsArrayBuffer.toSeq.sortWith(deltaSharingFileActionIncreaseOrderFunc)\n    }\n    var previousIdOpt: Option[String] = None\n    fileActionSeq.foreach { fileAction =>\n      if (reverseFileOrder) {\n        assert(\n          // Using < instead of <= because there can be a removeFile and addFile pointing to the\n          // same parquet file which result in the same file id, since id is a hash of file path.\n          // This is ok because eventually it can read data out of the correct parquet file.\n          !previousIdOpt.exists(_ < fileAction.id),\n          s\"fileActions must be in decreasing order by id: ${previousIdOpt} is not smaller than\" +\n          s\" ${fileAction.id}.\"\n        )\n        previousIdOpt = Some(fileAction.id)\n      }\n      lines += fileAction.json\n    }\n    if (assertMultipleDvsInOneFile) {\n      assert(dvPathToCount.max._2 > 1)\n    }\n\n    // Scan through the parquet files of the local delta table, and prepare the data of parquet file\n    // reading in DeltaSharingFileSystem.\n    val files =\n      FileUtils.listFiles(new File(snapshotToUse.deltaLog.dataPath.toUri()), null, true).asScala\n    files.foreach { f =>\n      val filePath = f.getCanonicalPath\n      val fileName = SparkPath.fromPathString(f.getName).urlEncoded\n      if (isDataFile(filePath)) {\n        // Put the parquet file in blockManager for DeltaSharingFileSystem to load bytes out of it.\n        DeltaSharingUtils.overrideIteratorBlock[Byte](\n          blockId = TestDeltaSharingFileSystem.getBlockId(sharedTable, fileName),\n          values = FileUtils.readFileToByteArray(f).toIterator\n        )\n      }\n    }\n\n    // This is specifically to set the size of the metadata.\n    val dsMetadata = DeltaSharingMetadata(\n      deltaMetadata = snapshotToUse.metadata,\n      size = totalSize\n    )\n    val dsProtocol = DeltaSharingProtocol(deltaProtocol = snapshotToUse.protocol)\n    // Put the metadata in blockManager for DeltaSharingClient to return metadata when being asked.\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(\n        sharedTableName = sharedTable,\n        queryType = \"getMetadata\",\n        versionAsOf = versionAsOf,\n        timestampAsOf = timestampAsOf\n      ),\n      values = Seq(dsProtocol.json, dsMetadata.json).toIterator\n    )\n\n    lines += dsProtocol.json\n    lines += dsMetadata.json\n    // Put the delta log (list of actions) in blockManager for DeltaSharingClient to return as the\n    // http response when getFiles is called.\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(\n        sharedTableName = sharedTable,\n        queryType = \"getFiles\",\n        versionAsOf = versionAsOf,\n        timestampAsOf = timestampAsOf,\n        limit = limitHint\n      ),\n      values = lines.result().toIterator\n    )\n  }\n\n  private[spark] def prepareMockedClientAndFileSystemResultForStreaming(\n      deltaTable: String,\n      sharedTable: String,\n      startingVersion: Long,\n      endingVersion: Long,\n      assertDVExists: Boolean = false): Unit = {\n    val actionLines = Seq.newBuilder[String]\n\n    var maxVersion = -1L\n    var totalSize = 0L\n\n    val deltaLog = DeltaLog.forTable(spark, new TableIdentifier(deltaTable))\n    val startingSnapshot = deltaLog.getSnapshotAt(startingVersion)\n    actionLines += DeltaSharingProtocol(deltaProtocol = startingSnapshot.protocol).json\n    actionLines += DeltaSharingMetadata(\n      deltaMetadata = startingSnapshot.metadata,\n      version = startingVersion\n    ).json\n\n    val logFiles =\n      FileUtils.listFiles(new File(deltaLog.logPath.toUri()), null, true).asScala\n    var dvExists = false\n    logFiles.foreach { f =>\n      if (FileNames.isDeltaFile(new Path(f.getName))) {\n        val version = FileNames.getFileVersion(new Path(f.getName))\n        if (version >= startingVersion && version <= endingVersion) {\n          // protocol/metadata are processed from startingSnapshot, only process versions greater\n          // than startingVersion for real actions and possible metadata changes.\n          maxVersion = maxVersion.max(version)\n          val timestamp = f.lastModified\n\n          FileUtils.readLines(f).asScala.foreach { l =>\n            val action = Action.fromJson(l)\n            action match {\n              case m: Metadata =>\n                actionLines += DeltaSharingMetadata(\n                  deltaMetadata = m,\n                  version = version\n                ).json\n              case addFile: AddFile if addFile.dataChange =>\n                // Convert from delta AddFile to DeltaSharingAddFile to serialize to json.\n                val dsAddFile =\n                  getDeltaSharingFileActionForAddFile(addFile, sharedTable, version, timestamp)\n                dvExists = dvExists || (dsAddFile.deletionVectorFileId != null)\n                totalSize = totalSize + addFile.size\n                actionLines += dsAddFile.json\n              case removeFile: RemoveFile if removeFile.dataChange =>\n                // scalastyle:off removeFile\n                val dsRemoveFile = getDeltaSharingFileActionForRemoveFile(\n                  removeFile,\n                  sharedTable,\n                  version,\n                  timestamp\n                )\n                // scalastyle:on removeFile\n                dvExists = dvExists || (dsRemoveFile.deletionVectorFileId != null)\n                totalSize = totalSize + removeFile.size.getOrElse(0L)\n                actionLines += dsRemoveFile.json\n              case _ => // ignore all other actions such as CommitInfo.\n            }\n          }\n        }\n      }\n    }\n    val dataFiles =\n      FileUtils.listFiles(new File(deltaLog.dataPath.toUri()), null, true).asScala\n    dataFiles.foreach { f =>\n      val fileName = SparkPath.fromPathString(f.getName).urlEncoded\n      if (isDataFile(f.getCanonicalPath)) {\n        DeltaSharingUtils.overrideIteratorBlock[Byte](\n          blockId = TestDeltaSharingFileSystem.getBlockId(sharedTable, fileName),\n          values = FileUtils.readFileToByteArray(f).toIterator\n        )\n      }\n    }\n\n    if (assertDVExists) {\n      assert(dvExists, \"There should be DV in the files returned from server.\")\n    }\n\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId = TestClientForDeltaFormatSharing.getBlockId(\n        sharedTable,\n        s\"getFiles_${startingVersion}_$endingVersion\"\n      ),\n      values = actionLines.result().toIterator\n    )\n  }\n\n  private[spark] def prepareMockedClientAndFileSystemResultForCdf(\n      deltaTable: String,\n      sharedTable: String,\n      startingVersion: Long,\n      startingTimestamp: Option[String] = None,\n      inlineDvFormat: Option[RoaringBitmapArrayFormat.Value] = None,\n      assertMultipleDvsInOneFile: Boolean = false): Unit = {\n    val actionLines = Seq.newBuilder[String]\n\n    var maxVersion = -1L\n    var totalSize = 0L\n\n    val deltaLog = DeltaLog.forTable(spark, new TableIdentifier(deltaTable))\n    val startingSnapshot = deltaLog.getSnapshotAt(startingVersion)\n    actionLines += DeltaSharingProtocol(deltaProtocol = startingSnapshot.protocol).json\n    actionLines += DeltaSharingMetadata(\n      deltaMetadata = startingSnapshot.metadata,\n      version = startingVersion\n    ).json\n\n    val dvPathToCount = scala.collection.mutable.Map[String, Int]()\n    val files =\n      FileUtils.listFiles(new File(deltaLog.logPath.toUri()), null, true).asScala\n    files.foreach { f =>\n      if (FileNames.isDeltaFile(new Path(f.getName))) {\n        val version = FileNames.getFileVersion(new Path(f.getName))\n        if (version >= startingVersion) {\n          // protocol/metadata are processed from startingSnapshot, only process versions greater\n          // than startingVersion for real actions and possible metadata changes.\n          maxVersion = maxVersion.max(version)\n          val timestamp = f.lastModified\n          FileUtils.readLines(f).asScala.foreach { l =>\n            val action = Action.fromJson(l)\n            action match {\n              case m: Metadata =>\n                actionLines += DeltaSharingMetadata(\n                  deltaMetadata = m,\n                  version = version\n                ).json\n              case addFile: AddFile if addFile.dataChange =>\n                if (assertMultipleDvsInOneFile) {\n                  updateDvPathToCount(addFile, dvPathToCount)\n                }\n                val updatedAdd = if (inlineDvFormat.isDefined) {\n                  // Remove row 0 and 1 in the AddFile.\n                  updateAddFileWithInlineDV(addFile, inlineDvFormat.get, RoaringBitmapArray(0L, 1L))\n                } else {\n                  addFile\n                }\n                val dsAddFile =\n                  getDeltaSharingFileActionForAddFile(updatedAdd, sharedTable, version, timestamp)\n                totalSize = totalSize + updatedAdd.size\n                actionLines += dsAddFile.json\n              case removeFile: RemoveFile if removeFile.dataChange =>\n                // scalastyle:off removeFile\n                val dsRemoveFile = getDeltaSharingFileActionForRemoveFile(\n                  removeFile,\n                  sharedTable,\n                  version,\n                  timestamp\n                )\n                // scalastyle:on removeFile\n                totalSize = totalSize + removeFile.size.getOrElse(0L)\n                actionLines += dsRemoveFile.json\n              case cdcFile: AddCDCFile =>\n                val parquetFile = removePartitionPrefix(cdcFile.path)\n\n                // Convert from delta AddCDCFile to DeltaSharingFileAction to serialize to json.\n                val dsCDCFile = DeltaSharingFileAction(\n                  id = Hashing.sha256().hashString(parquetFile, UTF_8).toString,\n                  version = version,\n                  timestamp = timestamp,\n                  deltaSingleAction = cdcFile\n                    .copy(\n                      path = TestDeltaSharingFileSystem.encode(sharedTable, parquetFile)\n                    )\n                    .wrap\n                )\n                totalSize = totalSize + cdcFile.size\n                actionLines += dsCDCFile.json\n              case _ => // ignore other lines\n            }\n          }\n        }\n      }\n    }\n    val dataFiles =\n      FileUtils.listFiles(new File(deltaLog.dataPath.toUri()), null, true).asScala\n    dataFiles.foreach { f =>\n      val filePath = f.getCanonicalPath\n      val fileName = SparkPath.fromPathString(f.getName).urlEncoded\n      if (isDataFile(filePath)) {\n        DeltaSharingUtils.overrideIteratorBlock[Byte](\n          blockId = TestDeltaSharingFileSystem.getBlockId(sharedTable, fileName),\n          values = FileUtils.readFileToByteArray(f).toIterator\n        )\n      }\n    }\n\n    if (assertMultipleDvsInOneFile) {\n      assert(dvPathToCount.max._2 > 1)\n    }\n\n    DeltaSharingUtils.overrideIteratorBlock[String](\n      blockId =\n        TestClientForDeltaFormatSharing.getBlockId(sharedTable, s\"getCDFFiles_$startingVersion\"),\n      values = actionLines.result().toIterator\n    )\n    if (startingTimestamp.isDefined) {\n      DeltaSharingUtils.overrideIteratorBlock[String](\n        blockId = TestClientForDeltaFormatSharing.getBlockId(\n          sharedTable,\n          s\"getCDFFiles_${startingTimestamp.get}\"\n        ),\n        values = actionLines.result().toIterator\n      )\n    }\n  }\n\n  protected def getDeltaSharingClassesSQLConf: Map[String, String] = {\n    Map(\n      \"fs.delta-sharing.impl\" -> classOf[TestDeltaSharingFileSystem].getName,\n      \"spark.delta.sharing.client.class\" ->\n      classOf[TestClientForDeltaFormatSharing].getName,\n      \"spark.delta.sharing.profile.provider.class\" ->\n      \"io.delta.sharing.client.DeltaSharingFileProfileProvider\"\n    )\n  }\n\n  /** Assert the response format recorded by the test client for the given table. */\n  protected def assertRequestedFormat(tableName: String, expectedFormat: Seq[String]): Unit = {\n    assert(\n      expectedFormat ==\n        TestClientForDeltaFormatSharing.requestedFormat.filter(_._1.contains(tableName)).map(_._2))\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingDataSourceTypeWideningSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport org.apache.spark.sql.delta.DeltaConfigs\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{Column, DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types._\n\n// Unit tests to verify that type widening works with delta sharing.\nclass DeltaSharingDataSourceTypeWideningSuite\n    extends QueryTest\n    with DeltaSQLCommandTest\n    with DeltaSharingTestSparkUtils\n    with DeltaSharingDataSourceDeltaTestUtils {\n\n  import testImplicits._\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaConfigs.ENABLE_TYPE_WIDENING.defaultTablePropertyKey, true.toString)\n  }\n\n  /** Sets up delta sharing mocks to read a table and validates results. */\n  private def testReadingDeltaShare(\n      tableName: String,\n      versionAsOf: Option[Long],\n      filter: Option[Column] = None,\n      expectedSchema: StructType,\n      expectedJsonPredicate: Seq[String] = Seq.empty,\n      expectedResult: DataFrame): Unit = {\n    withTempDir { tempDir =>\n      val sharedTableName = tableName + \"shared_delta_table\"\n      prepareMockedClientMetadata(tableName, sharedTableName)\n      prepareMockedClientGetTableVersion(tableName, sharedTableName, versionAsOf)\n      prepareMockedClientAndFileSystemResult(tableName, sharedTableName, versionAsOf)\n\n      var reader = spark.read\n        .format(\"deltaSharing\")\n        .option(\"responseFormat\", DeltaSharingOptions.RESPONSE_FORMAT_DELTA)\n      versionAsOf.foreach { version =>\n        reader = reader.option(\"versionAsOf\", version)\n      }\n\n      TestClientForDeltaFormatSharing.jsonPredicateHints.clear()\n      withSQLConf(getDeltaSharingClassesSQLConf.toSeq: _*) {\n        val profileFile = prepareProfileFile(tempDir)\n        var result = reader\n          .load(s\"${profileFile.getCanonicalPath}#share1.default.$sharedTableName\")\n        filter.foreach { f =>\n          result = result.filter(f)\n        }\n        assert(result.schema === expectedSchema)\n        checkAnswer(result, expectedResult)\n        assert(getJsonPredicateHints(tableName) === expectedJsonPredicate)\n      }\n    }\n  }\n\n  /** Fetches JSON predicates passed to the test client when reading a table. */\n  private def getJsonPredicateHints(tableName: String): Seq[String] = {\n    TestClientForDeltaFormatSharing\n      .jsonPredicateHints\n      .filterKeys(_.contains(tableName))\n      .values\n      .toSeq\n  }\n\n  /** Creates a table and applies a type change to it. */\n  private def withTestTable(testBody: String => Unit): Unit = {\n    val deltaTableName = \"type_widening\"\n    withTable(deltaTableName) {\n      sql(s\"CREATE TABLE $deltaTableName (value SMALLINT) USING DELTA\")\n      sql(s\"INSERT INTO $deltaTableName VALUES (1), (2)\")\n      sql(s\"ALTER TABLE $deltaTableName CHANGE COLUMN value TYPE INT\")\n      sql(s\"INSERT INTO $deltaTableName VALUES (3), (${Int.MaxValue})\")\n      sql(s\"INSERT INTO $deltaTableName VALUES (4), (5)\")\n      testBody(deltaTableName)\n    }\n  }\n\n  test(s\"Delta sharing with type widening\") {\n    withTestTable { tableName =>\n      testReadingDeltaShare(\n        tableName,\n        versionAsOf = None,\n        expectedSchema = new StructType().add(\"value\", IntegerType),\n        expectedResult = Seq(1, 2, 3, Int.MaxValue, 4, 5).toDF(\"value\"))\n    }\n  }\n\n  test(\"Delta sharing with type widening, time travel\") {\n    withTestTable { tableName =>\n      testReadingDeltaShare(\n        tableName,\n        versionAsOf = Some(3),\n        expectedSchema = new StructType().add(\"value\", IntegerType),\n        expectedResult = Seq(1, 2, 3, Int.MaxValue).toDF(\"value\"))\n\n      testReadingDeltaShare(\n        tableName,\n        versionAsOf = Some(2),\n        expectedSchema = new StructType().add(\"value\", IntegerType),\n        expectedResult = Seq(1, 2).toDF(\"value\"))\n\n      testReadingDeltaShare(\n        tableName,\n        versionAsOf = Some(1),\n        expectedSchema = new StructType()\n          .add(\"value\", ShortType),\n        expectedResult = Seq(1, 2).toDF(\"value\"))\n    }\n  }\n\n  test(\"jsonPredicateHints on non-partition column after type widening\") {\n    withTestTable { tableName =>\n      testReadingDeltaShare(\n        tableName,\n        versionAsOf = None,\n        filter = Some(col(\"value\") === Int.MaxValue),\n        expectedSchema = new StructType().add(\"value\", IntegerType),\n        expectedResult = Seq(Int.MaxValue).toDF(\"value\"),\n        expectedJsonPredicate = Seq(\n          \"\"\"\n            |{\"op\":\"and\",\"children\":[\n            |  {\"op\":\"not\",\"children\":[\n            |    {\"op\":\"isNull\",\"children\":[\n            |      {\"op\":\"column\",\"name\":\"value\",\"valueType\":\"int\"}]}]},\n            |  {\"op\":\"equal\",\"children\":[\n            |    {\"op\":\"column\",\"name\":\"value\",\"valueType\":\"int\"},\n            |    {\"op\":\"literal\",\"value\":\"2147483647\",\"valueType\":\"int\"}]}]}\n          \"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\"))\n      )\n    }\n  }\n\n  test(\"jsonPredicateHints on partition column after type widening\") {\n    val deltaTableName = \"type_widening_partitioned\"\n    withTable(deltaTableName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $deltaTableName (part SMALLINT, value SMALLINT)\n           |USING DELTA\n           |PARTITIONED BY (part)\n         \"\"\".stripMargin\n      )\n      sql(s\"INSERT INTO $deltaTableName VALUES (1, 1), (2, 2)\")\n      sql(s\"ALTER TABLE $deltaTableName CHANGE COLUMN part TYPE INT\")\n      sql(s\"INSERT INTO $deltaTableName VALUES (3, 3), (${Int.MaxValue}, 4)\")\n\n      testReadingDeltaShare(\n        deltaTableName,\n        versionAsOf = None,\n        filter = Some(col(\"part\") === Int.MaxValue),\n        expectedSchema = new StructType()\n          .add(\"part\", IntegerType)\n          .add(\"value\", ShortType),\n        expectedResult = Seq((Int.MaxValue, 4)).toDF(\"part\", \"value\"),\n        expectedJsonPredicate = Seq(\n          \"\"\"\n            |{\"op\":\"and\",\"children\":[\n            |  {\"op\":\"not\",\"children\":[\n            |    {\"op\":\"isNull\",\"children\":[\n            |      {\"op\":\"column\",\"name\":\"part\",\"valueType\":\"int\"}]}]},\n            |  {\"op\":\"equal\",\"children\":[\n            |    {\"op\":\"column\",\"name\":\"part\",\"valueType\":\"int\"},\n            |    {\"op\":\"literal\",\"value\":\"2147483647\",\"valueType\":\"int\"}]}]}\n          \"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\"))\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingFileIndexSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport io.delta.sharing.client.{\n  DeltaSharingClient,\n  DeltaSharingFileSystem,\n  DeltaSharingProfileProvider,\n  DeltaSharingRestClient\n}\nimport io.delta.sharing.client.model.{DeltaTableFiles, DeltaTableMetadata, Table, TemporaryCredentials}\nimport io.delta.sharing.client.util.JsonUtils\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.delta.sharing.{PreSignedUrlCache, PreSignedUrlFetcher}\nimport org.apache.spark.sql.{QueryTest, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.{\n  AttributeReference => SqlAttributeReference,\n  EqualTo => SqlEqualTo,\n  Literal => SqlLiteral\n}\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport org.apache.spark.sql.types.{FloatType, IntegerType}\n\nprivate object TestUtils {\n  val paths = Seq(\"http://path1\", \"http://path2\")\n  val refreshTokens = Seq(\"token1\", \"token2\", \"token3\")\n\n  val SparkConfForReturnExpTime = \"spark.delta.sharing.fileindexsuite.returnexptime\"\n  val SparkConfForUrlExpirationMs = \"spark.delta.sharing.fileindexsuite.urlExpirationMs\"\n\n  // 10 seconds\n  val defaultUrlExpirationMs = 10000\n\n  def getExpirationTimestampStr(urlExpirationMs: Option[Int]): String = {\n    if (urlExpirationMs.isDefined) {\n      s\"\"\"\"expirationTimestamp\":${System.currentTimeMillis() + urlExpirationMs.get},\"\"\"\n    } else {\n      \"\"\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  val protocolStr =\n    \"\"\"{\"protocol\":{\"deltaProtocol\":{\"minReaderVersion\": 1, \"minWriterVersion\": 1}}}\"\"\"\n  val metaDataStr =\n    \"\"\"{\"metaData\":{\"size\":809,\"deltaMetadata\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"c2\"],\"configuration\":{},\"createdTime\":1691734718560}}}\"\"\"\n  val metaDataWithoutSizeStr =\n    \"\"\"{\"metaData\":{\"deltaMetadata\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"c2\"],\"configuration\":{},\"createdTime\":1691734718560}}}\"\"\"\n  def getAddFileStr1(path: String, urlExpirationMs: Option[Int] = None): String = {\n    s\"\"\"{\"file\":{\"id\":\"11d9b72771a72f178a6f2839f7f08528\",${getExpirationTimestampStr(\n      urlExpirationMs\n    )}\"deltaSingleAction\":{\"add\":{\"path\":\"${path}\",\"\"\" + \"\"\"\"partitionValues\":{\"c2\":\"one\"},\"size\":809,\"modificationTime\":1691734726073,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"c1\\\":1,\\\"c2\\\":\\\"one\\\"},\\\"maxValues\\\":{\\\"c1\\\":2,\\\"c2\\\":\\\"one\\\"},\\\"nullCount\\\":{\\\"c1\\\":0,\\\"c2\\\":0}}\",\"tags\":{\"INSERTION_TIME\":\"1691734726073000\",\"MIN_INSERTION_TIME\":\"1691734726073000\",\"MAX_INSERTION_TIME\":\"1691734726073000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}}}\"\"\"\n  }\n  def getAddFileStr2(urlExpirationMs: Option[Int] = None): String = {\n    s\"\"\"{\"file\":{\"id\":\"22d9b72771a72f178a6f2839f7f08529\",${getExpirationTimestampStr(\n      urlExpirationMs\n    )}\"\"\" + \"\"\"\"deltaSingleAction\":{\"add\":{\"path\":\"http://path2\",\"partitionValues\":{\"c2\":\"two\"},\"size\":809,\"modificationTime\":1691734726073,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"c1\\\":1,\\\"c2\\\":\\\"two\\\"},\\\"maxValues\\\":{\\\"c1\\\":2,\\\"c2\\\":\\\"two\\\"},\\\"nullCount\\\":{\\\"c1\\\":0,\\\"c2\\\":0}}\",\"tags\":{\"INSERTION_TIME\":\"1691734726073000\",\"MIN_INSERTION_TIME\":\"1691734726073000\",\"MAX_INSERTION_TIME\":\"1691734726073000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}}}\"\"\"\n  }\n  // scalastyle:on line.size.limit\n}\n\n/**\n * A mocked delta sharing client for unit tests.\n */\nclass TestDeltaSharingClientForFileIndex(\n    profileProvider: DeltaSharingProfileProvider,\n    timeoutInSeconds: Int = 120,\n    numRetries: Int = 3,\n    maxRetryDuration: Long = Long.MaxValue,\n    retrySleepInterval: Long = 1000,\n    sslTrustAll: Boolean = false,\n    forStreaming: Boolean = false,\n    responseFormat: String = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA,\n    readerFeatures: String = \"\",\n    queryTablePaginationEnabled: Boolean = false,\n    maxFilesPerReq: Int = 100000,\n    endStreamActionEnabled: Boolean = false,\n    enableAsyncQuery: Boolean = false,\n    asyncQueryPollIntervalMillis: Long = 10000L,\n    asyncQueryMaxDuration: Long = 600000L,\n    tokenExchangeMaxRetries: Int = 5,\n    tokenExchangeMaxRetryDurationInSeconds: Int = 60,\n    tokenRenewalThresholdInSeconds: Int = 600,\n    callerOrg: String = \"\",\n    skipFileIdHashVerification: Boolean = false)\n    extends DeltaSharingClient {\n\n  import TestUtils._\n\n  private lazy val returnExpirationTimestamp = SparkSession.active.sessionState.conf\n    .getConfString(\n      SparkConfForReturnExpTime,\n      \"false\"\n    )\n    .toBoolean\n  private lazy val urlExpirationMsOpt = if (returnExpirationTimestamp) {\n    val urlExpirationMs = SparkSession.active.sessionState.conf\n      .getConfString(\n        SparkConfForUrlExpirationMs,\n        defaultUrlExpirationMs.toString\n      )\n      .toInt\n    Some(urlExpirationMs)\n  } else {\n    None\n  }\n\n  var numGetFileCalls: Int = -1\n\n  var savedLimits = Seq.empty[Long]\n  var savedJsonPredicateHints = Seq.empty[String]\n\n  override def listAllTables(): Seq[Table] = throw new UnsupportedOperationException(\"not needed\")\n\n  override def getMetadata(\n      table: Table,\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String]): DeltaTableMetadata = {\n    throw new UnsupportedOperationException(\"getMetadata is not supported now.\")\n  }\n\n  override def getTableVersion(table: Table, startingTimestamp: Option[String] = None): Long = {\n    throw new UnsupportedOperationException(\"getTableVersion is not supported now.\")\n  }\n\n  override def getFiles(\n      table: Table,\n      predicates: Seq[String],\n      limit: Option[Long],\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String],\n      jsonPredicateHints: Option[String],\n      refreshToken: Option[String],\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    numGetFileCalls += 1\n    limit.foreach(lim => savedLimits = savedLimits :+ lim)\n    jsonPredicateHints.foreach(p => {\n      savedJsonPredicateHints = savedJsonPredicateHints :+ p\n    })\n    if (numGetFileCalls > 0 && refreshToken.isDefined) {\n      assert(refreshToken.get == refreshTokens(numGetFileCalls.min(2) - 1))\n    }\n\n    DeltaTableFiles(\n      version = 0,\n      lines = Seq[String](\n        protocolStr,\n        metaDataStr,\n        getAddFileStr1(paths(numGetFileCalls.min(1)), urlExpirationMsOpt),\n        getAddFileStr2(urlExpirationMsOpt)\n      ),\n      refreshToken = Some(refreshTokens(numGetFileCalls.min(2))),\n      respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA\n    )\n  }\n\n  override def getFiles(\n      table: Table,\n      startingVersion: Long,\n      endingVersion: Option[Long],\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    throw new UnsupportedOperationException(s\"getFiles with startingVersion($startingVersion)\")\n  }\n\n  override def getCDFFiles(\n      table: Table,\n      cdfOptions: Map[String, String],\n      includeHistoricalMetadata: Boolean,\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    throw new UnsupportedOperationException(\n      s\"getCDFFiles with cdfOptions:[$cdfOptions], \" +\n      s\"includeHistoricalMetadata:$includeHistoricalMetadata\"\n    )\n  }\n\n  override def generateTemporaryTableCredential(\n      table: Table,\n      location: Option[String]): TemporaryCredentials = {\n    throw new UnsupportedOperationException(\"generateTemporaryTableCredential is not implemented\")\n  }\n\n  override def getForStreaming(): Boolean = forStreaming\n\n  override def getProfileProvider: DeltaSharingProfileProvider = profileProvider\n\n  def clear() {\n    savedLimits = Seq.empty[Long]\n    savedJsonPredicateHints = Seq.empty[String]\n  }\n}\n\nclass DeltaSharingFileIndexSuite\n    extends QueryTest\n    with DeltaSQLCommandTest\n    with DeltaSharingDataSourceDeltaTestUtils\n    with DeltaSharingTestSparkUtils {\n\n  import TestUtils._\n\n  private def getMockedDeltaSharingMetadata(metaData: String): model.DeltaSharingMetadata = {\n    JsonUtils.fromJson[model.DeltaSharingSingleAction](metaData).metaData\n  }\n\n  private def getMockedDeltaSharingFileAction(id: String): model.DeltaSharingFileAction = {\n    if (id.startsWith(\"11\")) {\n      JsonUtils.fromJson[model.DeltaSharingSingleAction](getAddFileStr1(paths(0))).file\n    } else {\n      JsonUtils.fromJson[model.DeltaSharingSingleAction](getAddFileStr2()).file\n    }\n  }\n\n  private val shareName = \"share\"\n  private val schemaName = \"default\"\n  private val sharedTableName = \"table\"\n\n  private def prepareDeltaSharingFileIndex(\n      profilePath: String,\n      metaData: String): (Path, DeltaSharingFileIndex, DeltaSharingClient) = {\n    val tablePath = new Path(s\"$profilePath#$shareName.$schemaName.$sharedTableName\")\n    val client = DeltaSharingRestClient(profilePath, Map.empty, false, \"delta\")\n\n    val spark = SparkSession.active\n    val params = new DeltaSharingFileIndexParams(\n      tablePath,\n      spark,\n      DeltaSharingUtils.DeltaSharingTableMetadata(\n        version = 0,\n        protocol = JsonUtils.fromJson[model.DeltaSharingSingleAction](protocolStr).protocol,\n        metadata = getMockedDeltaSharingMetadata(metaData)\n      ),\n      new DeltaSharingOptions(Map(\"path\" -> tablePath.toString))\n    )\n    val dsTable = Table(share = shareName, schema = schemaName, name = sharedTableName)\n    (tablePath, new DeltaSharingFileIndex(params, dsTable, client, None), client)\n  }\n\n  test(\"basic functions works\") {\n    withTempDir { tempDir =>\n      val profileFile = new File(tempDir, \"foo.share\")\n      FileUtils.writeStringToFile(\n        profileFile,\n        s\"\"\"{\n           |  \"shareCredentialsVersion\": 1,\n           |  \"endpoint\": \"https://localhost:12345/not-used-endpoint\",\n           |  \"bearerToken\": \"mock\"\n           |}\"\"\".stripMargin,\n        \"utf-8\"\n      )\n      withSQLConf(\n        \"spark.delta.sharing.client.class\" -> classOf[TestDeltaSharingClientForFileIndex].getName,\n        \"fs.delta-sharing-log.impl\" -> classOf[DeltaSharingLogFileSystem].getName,\n        \"spark.delta.sharing.profile.provider.class\" ->\n        \"io.delta.sharing.client.DeltaSharingFileProfileProvider\"\n      ) {\n        val (tablePath, fileIndex, _) =\n          prepareDeltaSharingFileIndex(profileFile.getCanonicalPath, metaDataStr)\n\n        assert(fileIndex.sizeInBytes == 809)\n        assert(fileIndex.partitionSchema.toDDL == \"c2 STRING\")\n        assert(fileIndex.rootPaths.length == 1)\n        assert(fileIndex.rootPaths.head == tablePath)\n\n        intercept[UnsupportedOperationException] {\n          fileIndex.inputFiles\n        }\n\n        val partitionDirectoryList = fileIndex.listFiles(Seq.empty, Seq.empty)\n        assert(partitionDirectoryList.length == 2)\n        partitionDirectoryList.foreach { partitionDirectory =>\n          assert(!partitionDirectory.values.anyNull)\n          assert(\n            partitionDirectory.values.getString(0) == \"one\" ||\n            partitionDirectory.values.getString(0) == \"two\"\n          )\n\n          partitionDirectory.files.foreach { f =>\n            // Verify that the path can be decoded\n            val decodedPath = DeltaSharingFileSystem.decode(f.fileStatus.getPath)\n            val dsFileAction = getMockedDeltaSharingFileAction(decodedPath.fileId)\n            assert(decodedPath.tablePath.startsWith(tablePath.toString))\n            assert(decodedPath.fileId == dsFileAction.id)\n            assert(decodedPath.fileSize == dsFileAction.size)\n\n            assert(f.fileStatus.getLen == dsFileAction.size)\n            assert(f.fileStatus.getModificationTime == 0)\n            assert(f.fileStatus.isDirectory == false)\n          }\n        }\n\n        // Check exception is thrown when metadata doesn't have size\n        val (_, fileIndex2, _) =\n          prepareDeltaSharingFileIndex(profileFile.getCanonicalPath, metaDataWithoutSizeStr)\n        val ex = intercept[IllegalStateException] {\n          fileIndex2.sizeInBytes\n        }\n        assert(ex.toString.contains(\"size is null in the metadata\"))\n      }\n    }\n  }\n\n  test(\"refresh works\") {\n    PreSignedUrlCache.registerIfNeeded(SparkEnv.get)\n\n    withTempDir { tempDir =>\n      val profileFile = new File(tempDir, \"foo.share\")\n      FileUtils.writeStringToFile(\n        profileFile,\n        s\"\"\"{\n           |  \"shareCredentialsVersion\": 1,\n           |  \"endpoint\": \"https://localhost:12345/not-used-endpoint\",\n           |  \"bearerToken\": \"mock\"\n           |}\"\"\".stripMargin,\n        \"utf-8\"\n      )\n\n      def test(): Unit = {\n        val (_, fileIndex, _) =\n          prepareDeltaSharingFileIndex(profileFile.getCanonicalPath, metaDataStr)\n        val preSignedUrlCacheRef = PreSignedUrlCache.getEndpointRefInExecutor(SparkEnv.get)\n\n        val partitionDirectoryList = fileIndex.listFiles(Seq.empty, Seq.empty)\n        assert(partitionDirectoryList.length == 2)\n        partitionDirectoryList.foreach { partitionDirectory =>\n          partitionDirectory.files.foreach { f =>\n            val decodedPath = DeltaSharingFileSystem.decode(f.fileStatus.getPath)\n            if (decodedPath.fileId.startsWith(\"11\")) {\n              val fetcher = new PreSignedUrlFetcher(\n                preSignedUrlCacheRef,\n                decodedPath.tablePath,\n                decodedPath.fileId,\n                1000\n              )\n              // sleep for 25000ms to ensure that the urls are refreshed.\n              Thread.sleep(25000)\n\n              // Verify that the url is refreshed as paths(1), not paths(0) anymore.\n              assert(fetcher.getUrl == paths(1))\n            }\n          }\n        }\n      }\n\n      withSQLConf(\n        \"spark.delta.sharing.client.class\" -> classOf[TestDeltaSharingClientForFileIndex].getName,\n        \"fs.delta-sharing-log.impl\" -> classOf[DeltaSharingLogFileSystem].getName,\n        \"spark.delta.sharing.profile.provider.class\" ->\n        \"io.delta.sharing.client.DeltaSharingFileProfileProvider\",\n        SparkConfForReturnExpTime -> \"true\"\n      ) {\n        test()\n      }\n\n      withSQLConf(\n        \"spark.delta.sharing.client.class\" -> classOf[TestDeltaSharingClientForFileIndex].getName,\n        \"fs.delta-sharing-log.impl\" -> classOf[DeltaSharingLogFileSystem].getName,\n        \"spark.delta.sharing.profile.provider.class\" ->\n        \"io.delta.sharing.client.DeltaSharingFileProfileProvider\",\n        SparkConfForReturnExpTime -> \"false\"\n      ) {\n        test()\n      }\n    }\n  }\n\n  test(\"jsonPredicate test\") {\n    withTempDir { tempDir =>\n      val profileFile = new File(tempDir, \"foo.share\")\n      FileUtils.writeStringToFile(\n        profileFile,\n        s\"\"\"{\n           |  \"shareCredentialsVersion\": 1,\n           |  \"endpoint\": \"https://localhost:12345/not-used-endpoint\",\n           |  \"bearerToken\": \"mock\"\n           |}\"\"\".stripMargin,\n        \"utf-8\"\n      )\n      withSQLConf(\n        \"spark.delta.sharing.client.class\" -> classOf[TestDeltaSharingClientForFileIndex].getName,\n        \"fs.delta-sharing-log.impl\" -> classOf[DeltaSharingLogFileSystem].getName,\n        \"spark.delta.sharing.profile.provider.class\" ->\n        \"io.delta.sharing.client.DeltaSharingFileProfileProvider\",\n        SparkConfForReturnExpTime -> \"true\",\n        SparkConfForUrlExpirationMs -> \"3600000\" // 1h\n      ) {\n        val (tablePath, fileIndex, client) =\n          prepareDeltaSharingFileIndex(profileFile.getCanonicalPath, metaDataStr)\n        val testClient = client.asInstanceOf[TestDeltaSharingClientForFileIndex]\n\n        val spark = SparkSession.active\n        spark.sessionState.conf\n          .setConfString(\"spark.delta.sharing.jsonPredicateHints.enabled\", \"true\")\n\n        // We will send an equal op on partition filters as a SQL expression tree.\n        val partitionSqlEq = SqlEqualTo(\n          SqlAttributeReference(\"id\", IntegerType)(),\n          SqlLiteral(23, IntegerType)\n        )\n        // The client should get json for jsonPredicateHints.\n        val expectedJson =\n          \"\"\"{\"op\":\"equal\",\n             |\"children\":[\n             |  {\"op\":\"column\",\"name\":\"id\",\"valueType\":\"int\"},\n             |  {\"op\":\"literal\",\"value\":\"23\",\"valueType\":\"int\"}]\n             |}\"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\")\n        spark.sessionState.conf.setConfString(\n          \"spark.delta.sharing.jsonPredicateV2Hints.enabled\",\n          \"false\"\n        )\n        fileIndex.listFiles(Seq(partitionSqlEq), Seq.empty)\n        assert(testClient.savedJsonPredicateHints.size === 1)\n        assert(expectedJson == testClient.savedJsonPredicateHints(0))\n        testClient.clear()\n\n        // We will send another equal op as a SQL expression tree for data filters.\n        val dataSqlEq = SqlEqualTo(\n          SqlAttributeReference(\"cost\", FloatType)(),\n          SqlLiteral(23.5.toFloat, FloatType)\n        )\n\n        // With V2 predicates disabled, the client should get json for partition filters only.\n        fileIndex.listFiles(Seq(partitionSqlEq), Seq(dataSqlEq))\n        assert(testClient.savedJsonPredicateHints.size === 1)\n        assert(expectedJson == testClient.savedJsonPredicateHints(0))\n        testClient.clear()\n\n        // With V2 predicates enabled, the client should get json for partition and data filters\n        // joined at the top level by an AND operation.\n        val expectedJson2 =\n          \"\"\"{\"op\":\"and\",\"children\":[\n             |  {\"op\":\"equal\",\"children\":[\n             |    {\"op\":\"column\",\"name\":\"id\",\"valueType\":\"int\"},\n             |    {\"op\":\"literal\",\"value\":\"23\",\"valueType\":\"int\"}]},\n             |  {\"op\":\"equal\",\"children\":[\n             |    {\"op\":\"column\",\"name\":\"cost\",\"valueType\":\"float\"},\n             |    {\"op\":\"literal\",\"value\":\"23.5\",\"valueType\":\"float\"}]}\n             |]}\"\"\".stripMargin.replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\")\n        spark.sessionState.conf.setConfString(\n          \"spark.delta.sharing.jsonPredicateV2Hints.enabled\",\n          \"true\"\n        )\n        fileIndex.listFiles(Seq(partitionSqlEq), Seq(dataSqlEq))\n        assert(testClient.savedJsonPredicateHints.size === 1)\n        assert(expectedJson2 == testClient.savedJsonPredicateHints(0))\n        testClient.clear()\n\n        // With json predicates disabled, we should not get anything.\n        spark.sessionState.conf\n          .setConfString(\"spark.delta.sharing.jsonPredicateHints.enabled\", \"false\")\n        spark.sessionState.conf.setConfString(\n          \"spark.delta.sharing.jsonPredicateV2Hints.enabled\",\n          \"false\"\n        )\n        fileIndex.listFiles(Seq(partitionSqlEq), Seq.empty)\n        assert(testClient.savedJsonPredicateHints.size === 0)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingLogFileSystemSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.{SharedSparkContext, SparkEnv, SparkFunSuite}\nimport org.apache.spark.sql.delta.sharing.DeltaSharingTestSparkUtils\nimport org.apache.spark.storage.StorageLevel\n\nclass DeltaSharingLogFileSystemSuite extends SparkFunSuite with SharedSparkContext {\n  import DeltaSharingLogFileSystem._\n\n  var hadoopConf: Configuration = new Configuration\n\n  var path: Path = null\n  var fs: FileSystem = null\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    conf.set(\n      s\"spark.hadoop.fs.${DeltaSharingLogFileSystem.SCHEME}.impl\",\n      classOf[DeltaSharingLogFileSystem].getName\n    )\n    hadoopConf = DeltaSharingTestSparkUtils.getHadoopConf(conf)\n\n    path = encode(table1)\n    fs = path.getFileSystem(hadoopConf)\n  }\n\n  // constants for testing.\n  private val table1 = \"table1\"\n  private val table2 = \"table2\"\n\n  test(\"encode and decode\") {\n    assert(decode(encode(table1)) == table1)\n  }\n\n  test(\"file system should be cached\") {\n    assert(fs.isInstanceOf[DeltaSharingLogFileSystem])\n    assert(fs eq path.getFileSystem(hadoopConf))\n\n    assert(fs.getScheme == \"delta-sharing-log\")\n    assert(fs.getWorkingDirectory == new Path(\"delta-sharing-log:/\"))\n  }\n\n  test(\"unsupported functions\") {\n    intercept[UnsupportedOperationException] { fs.create(path) }\n    intercept[UnsupportedOperationException] { fs.append(path) }\n    intercept[UnsupportedOperationException] { fs.rename(path, new Path(path, \"a\")) }\n    intercept[UnsupportedOperationException] { fs.delete(path, true) }\n    intercept[UnsupportedOperationException] { fs.listStatusIterator(path) }\n    intercept[UnsupportedOperationException] { fs.setWorkingDirectory(path) }\n    intercept[UnsupportedOperationException] { fs.mkdirs(path) }\n  }\n\n  test(\"open works ok\") {\n    val content = \"this is the content\\nanother line\\nthird line\"\n    SparkEnv.get.blockManager.putSingle[String](\n      blockId = getDeltaSharingLogBlockId(path.toString),\n      value = content,\n      level = StorageLevel.MEMORY_AND_DISK_SER,\n      tellMaster = true\n    )\n    assert(scala.io.Source.fromInputStream(fs.open(path)).mkString == content)\n  }\n\n  test(\"exists works ok\") {\n    val newPath = encode(table1)\n    val fileAndSizeSeq = Seq[DeltaSharingLogFileStatus](\n      DeltaSharingLogFileStatus(\"filea\", 10, 100)\n    )\n    SparkEnv.get.blockManager.putIterator[DeltaSharingLogFileStatus](\n      blockId = getDeltaSharingLogBlockId(newPath.toString),\n      values = fileAndSizeSeq.toIterator,\n      level = StorageLevel.MEMORY_AND_DISK_SER,\n      tellMaster = true\n    )\n\n    assert(fs.exists(newPath))\n    assert(!fs.exists(new Path(newPath, \"A\")))\n  }\n\n  test(\"listStatus works ok\") {\n    val newPath = encode(table2)\n    val fileAndSizeSeq = Seq[DeltaSharingLogFileStatus](\n      DeltaSharingLogFileStatus(\"file_a\", 10, 100),\n      DeltaSharingLogFileStatus(\"file_b\", 20, 200)\n    )\n    SparkEnv.get.blockManager.putIterator[DeltaSharingLogFileStatus](\n      blockId = getDeltaSharingLogBlockId(newPath.toString),\n      values = fileAndSizeSeq.toIterator,\n      level = StorageLevel.MEMORY_AND_DISK_SER,\n      tellMaster = true\n    )\n\n    val files = fs.listStatus(newPath)\n    assert(files.length == 2)\n    assert(files(0).getPath == new Path(\"file_a\"))\n    assert(files(0).getLen == 10)\n    assert(files(0).getModificationTime == 100)\n    assert(files(1).getPath == new Path(\"file_b\"))\n    assert(files(1).getLen == 20)\n    assert(files(1).getModificationTime == 200)\n\n    intercept[java.io.FileNotFoundException] {\n      fs.listStatus(new Path(newPath, \"random\"))\n    }\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingTestSparkUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sharing\n\nimport java.io.File\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.conf.Configuration\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.deploy.SparkHadoopUtil\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils.{\n  getZoneId,\n  stringToDate,\n  stringToTimestamp,\n  toJavaDate,\n  toJavaTimestamp\n}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.unsafe.types.UTF8String\n\ntrait DeltaSharingTestSparkUtils extends DeltaSQLTestUtils {\n\n  /**\n   * Creates 3 temporary directories for use within a function.\n   *\n   * @param f function to be run with created temp directories\n   */\n  protected def withTempDirs(f: (File, File, File) => Unit): Unit = {\n    withTempDir { file1 =>\n      withTempDir { file2 =>\n        withTempDir { file3 =>\n          f(file1, file2, file3)\n        }\n      }\n    }\n  }\n\n  protected def sqlDate(date: String): java.sql.Date = {\n    toJavaDate(stringToDate(UTF8String.fromString(date)).get)\n  }\n\n  protected def sqlTimestamp(timestamp: String): java.sql.Timestamp = {\n    toJavaTimestamp(\n      stringToTimestamp(\n        UTF8String.fromString(timestamp),\n        getZoneId(SQLConf.get.sessionLocalTimeZone)\n      ).get\n    )\n  }\n\n  protected def createTable(tableName: String): Unit = {\n    sql(s\"\"\"CREATE TABLE $tableName (c1 INT, c2 STRING, c3 date, c4 timestamp)\n           |USING DELTA PARTITIONED BY (c2)\n           |\"\"\".stripMargin)\n  }\n\n  protected def createTableForStreaming(tableName: String, enableDV: Boolean = false): Unit = {\n    val tablePropertiesStr = if (enableDV) {\n      \"TBLPROPERTIES (delta.enableDeletionVectors = true)\"\n    } else {\n      \"\"\n    }\n    sql(s\"\"\"\n           |CREATE TABLE $tableName (value STRING)\n           |USING DELTA\n           |$tablePropertiesStr\n           |\"\"\".stripMargin)\n  }\n\n  protected def createSimpleTable(tableName: String, enableCdf: Boolean): Unit = {\n    val tablePropertiesStr = if (enableCdf) {\n      s\"\"\"TBLPROPERTIES (\n        |delta.minReaderVersion=1,\n        |delta.minWriterVersion=4,\n        |delta.enableChangeDataFeed = true)\"\"\".stripMargin\n    } else {\n      \"\"\n    }\n    sql(s\"\"\"CREATE TABLE $tableName (c1 INT, c2 STRING)\n           |USING DELTA PARTITIONED BY (c2)\n           |$tablePropertiesStr\n           |\"\"\".stripMargin)\n  }\n\n  protected def createCMIdTableWithCdf(tableName: String): Unit = {\n    sql(s\"\"\"CREATE TABLE $tableName (c1 INT, c2 STRING) USING DELTA PARTITIONED BY (c2)\n           |TBLPROPERTIES ('delta.columnMapping.mode' = 'id',\n           |delta.enableChangeDataFeed = true)\n           |\"\"\".stripMargin)\n  }\n\n  protected def createDVTableWithCdf(tableName: String): Unit = {\n    sql(s\"\"\"CREATE TABLE $tableName (c1 INT, partition INT) USING DELTA PARTITIONED BY (partition)\n           |TBLPROPERTIES (delta.enableDeletionVectors = true,\n           |delta.enableChangeDataFeed = true)\n           |\"\"\".stripMargin)\n  }\n\n  protected def prepareProfileFile(tempDir: File): File = {\n    val profileFile = new File(tempDir, \"foo.share\")\n    FileUtils.writeStringToFile(\n      profileFile,\n      s\"\"\"{\n         |  \"shareCredentialsVersion\": 1,\n         |  \"endpoint\": \"https://localhost:12345/not-used-endpoint\",\n         |  \"bearerToken\": \"mock\"\n         |}\"\"\".stripMargin,\n      \"utf-8\"\n    )\n    profileFile\n  }\n}\n\nobject DeltaSharingTestSparkUtils {\n  def getHadoopConf(sparkConf: SparkConf): Configuration = {\n    new SparkHadoopUtil().newConfiguration(sparkConf)\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/DeltaSharingUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport scala.reflect.ClassTag\n\nimport io.delta.sharing.client.{DeltaSharingClient, DeltaSharingRestClient}\nimport io.delta.sharing.client.model.{DeltaTableFiles, DeltaTableMetadata, Table, TemporaryCredentials}\nimport io.delta.sharing.spark.DeltaSharingUtils._\n\nimport org.apache.spark.{SharedSparkContext, SparkEnv, SparkFunSuite}\nimport org.apache.spark.delta.sharing.TableRefreshResult\nimport org.apache.spark.storage.BlockId\n\nclass DeltaSharingUtilsSuite extends SparkFunSuite with SharedSparkContext {\n\n  type RefresherFunction = Option[String] => TableRefreshResult\n  class SimpleTestDeltaSharingClient extends DeltaSharingClient {\n    def getStatsStr(): String = {\n      \"\"\"{\n        |  \"numRecords\": 20,\n        |  \"minValues\": { \"col-a\": 0 },\n        |  \"maxValues\": { \"col-a\": 19 },\n        |  \"nullCount\": { \"col-a\": 0 }\n        |}\"\"\".stripMargin\n        .replace(\"\\n\", \"\")\n        .replace(\" \", \"\")\n        .replace(\"\\\"\", \"\\\\\\\"\")\n    }\n\n    def getAddFileStr(): String = {\n      val stats = getStatsStr()\n      s\"\"\"{\n         |  \"file\": {\n         |    \"id\": \"add_file_id1\",\n         |    \"expirationTimestamp\": 1721350999999,\n         |    \"deltaSingleAction\": {\n         |      \"add\": {\n         |        \"path\": \"c000.snappy.parquet\",\n         |        \"partitionValues\": {\n         |          \"col-partition\": \"3\"\n         |        },\n         |        \"size\": 1213,\n         |        \"modificationTime\": 1721350059000,\n         |        \"dataChange\": true,\n         |        \"stats\": \"$stats\",\n         |        \"tags\": {\n         |          \"INSERTION_TIME\": \"1721350059000000\"\n         |        }\n         |      }\n         |    }\n         |  }\n         |}\"\"\".stripMargin\n    }\n\n    def getDeletionVectorStr(): String = {\n      val stats = getStatsStr()\n      s\"\"\"{\n         |  \"file\": {\n         |    \"id\": \"add_file_id2\",\n         |    \"expirationTimestamp\": 1721350999999,\n         |    \"deletionVectorFileId\": \"dv_file_id\",\n         |    \"deltaSingleAction\": {\n         |      \"add\": {\n         |        \"path\": \"c001.snappy.parquet\",\n         |        \"partitionValues\": {\n         |          \"col-partition\": \"3\"\n         |        },\n         |        \"size\": 1213,\n         |        \"modificationTime\": 1721350059000,\n         |        \"dataChange\": true,\n         |        \"stats\": \"$stats\",\n         |        \"tags\": {\n         |          \"INSERTION_TIME\": \"1721350059000000\"\n         |        },\n         |        \"deletionVector\": {\n         |          \"storageType\": \"p\",\n         |          \"pathOrInlineDv\": \"fakeurl\",\n         |          \"offset\": 1,\n         |          \"sizeInBytes\": 34,\n         |          \"cardinality\": 1\n         |        }\n         |      }\n         |    }\n         |  }\n         |}\"\"\".stripMargin\n    }\n\n    def getCdcStr(): String = {\n      s\"\"\"{\"file\":{\n         |  \"id\":\"cdc_file_id\",\n         |  \"expirationTimestamp\":1721350999999,\n         |  \"deltaSingleAction\":{\n         |    \"cdc\":{\n         |      \"path\":\"_change_data/cdc.c000.snappy.parquet\",\n         |      \"partitionValues\":{},\n         |      \"size\":1213,\n         |      \"modificationTime\":1721350059000,\n         |      \"dataChange\":false\n         |    }\n         |  }\n         |}}\"\"\".stripMargin\n    }\n    override def listAllTables(): Seq[Table] = Seq.empty\n\n    override def getTableVersion(table: Table, startingTimestamp: Option[String] = None): Long = 0\n\n    override def getMetadata(\n      table: Table,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None\n    ): DeltaTableMetadata =\n      throw new UnsupportedOperationException\n\n    override def getFiles(\n      table: Table,\n      predicates: Seq[String],\n      limit: Option[Long],\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String],\n      jsonPredicateHints: Option[String],\n      refreshToken: Option[String],\n      fileIdHash: Option[String]\n    ): DeltaTableFiles = {\n      val file = getAddFileStr()\n      val dv = getDeletionVectorStr()\n      DeltaTableFiles(\n        version = 0L,\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA,\n        lines = Seq(file, dv)\n      )\n    }\n\n    override def getFiles(\n      table: Table,\n      startingVersion: Long,\n      endingVersion: Option[Long],\n      fileIdHash: Option[String]\n    ): DeltaTableFiles = {\n      val file = getAddFileStr()\n      val dv = getDeletionVectorStr()\n      DeltaTableFiles(\n        version = 0L,\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA,\n        lines = Seq(file, dv)\n      )\n    }\n\n    override def getCDFFiles(\n      table: Table,\n      cdfOptions: Map[String, String],\n      includeHistoricalMetadata: Boolean,\n      fileIdHash: Option[String]): DeltaTableFiles = {\n      val file = getAddFileStr()\n      val dv = getDeletionVectorStr()\n      val cdc = getCdcStr()\n      DeltaTableFiles(\n        version = 0L,\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA,\n        lines = Seq(file, dv, cdc)\n      )\n    }\n\n    override def generateTemporaryTableCredential(\n        table: Table,\n        location: Option[String]): TemporaryCredentials = {\n      throw new UnsupportedOperationException(\"generateTemporaryTableCredential is not implemented\")\n    }\n  }\n\n\n  test(\"override single block in blockmanager works\") {\n    val blockId = BlockId(s\"${DeltaSharingUtils.DELTA_SHARING_BLOCK_ID_PREFIX}_1\")\n    overrideSingleBlock[Int](blockId, 1)\n    assert(SparkEnv.get.blockManager.getSingle[Int](blockId).get == 1)\n    SparkEnv.get.blockManager.releaseLock(blockId)\n    overrideSingleBlock[String](blockId, \"2\")\n    assert(SparkEnv.get.blockManager.getSingle[String](blockId).get == \"2\")\n    SparkEnv.get.blockManager.releaseLock(blockId)\n  }\n\n  def getSeqFromBlockManager[T: ClassTag](blockId: BlockId): Seq[T] = {\n    val iterator = SparkEnv.get.blockManager\n      .get[T](blockId)\n      .map(\n        _.data.asInstanceOf[Iterator[T]]\n      )\n      .get\n    val seqBuilder = Seq.newBuilder[T]\n    while (iterator.hasNext) {\n      seqBuilder += iterator.next()\n    }\n    seqBuilder.result()\n  }\n\n  test(\"override iterator block in blockmanager works\") {\n    val blockId = BlockId(s\"${DeltaSharingUtils.DELTA_SHARING_BLOCK_ID_PREFIX}_1\")\n    overrideIteratorBlock[Int](blockId, values = Seq(1, 2).toIterator)\n    assert(getSeqFromBlockManager[Int](blockId) == Seq(1, 2))\n    overrideIteratorBlock[String](blockId, values = Seq(\"3\", \"4\").toIterator)\n    assert(getSeqFromBlockManager[String](blockId) == Seq(\"3\", \"4\"))\n  }\n\n  test(\"getRefresherForGetFiles with deletion vector\") {\n    val client = new SimpleTestDeltaSharingClient()\n    val table = Table(name = \"table\", schema = \"schema\", share = \"share\")\n    val func: RefresherFunction = getRefresherForGetFiles(\n      client,\n      table,\n      Seq.empty,\n      None,\n      None,\n      None,\n      None,\n      useRefreshToken = true\n    )\n    val idToUrls = func(None).idToUrl\n    assert(idToUrls.size == 3)\n    assert(idToUrls.contains(\"add_file_id1\"))\n    assert(idToUrls.get(\"add_file_id1\") == Some(\"c000.snappy.parquet\"))\n    assert(idToUrls.contains(\"add_file_id2\"))\n    assert(idToUrls.get(\"add_file_id2\") == Some(\"c001.snappy.parquet\"))\n    assert(idToUrls.contains(\"dv_file_id\"))\n    assert(idToUrls.get(\"dv_file_id\") == Some(\"fakeurl\"))\n  }\n\n  test(\"getRefresherForGetFilesWithStartingVersion with deletion vector\") {\n    val client = new SimpleTestDeltaSharingClient()\n    val table = Table(name = \"table\", schema = \"schema\", share = \"share\")\n    val func: RefresherFunction = getRefresherForGetFilesWithStartingVersion(\n      client,\n      table,\n      0L,\n      None\n    )\n    val idToUrls = func(None).idToUrl\n    assert(idToUrls.size == 3)\n    assert(idToUrls.contains(\"add_file_id1\"))\n    assert(idToUrls.get(\"add_file_id1\") == Some(\"c000.snappy.parquet\"))\n    assert(idToUrls.contains(\"add_file_id2\"))\n    assert(idToUrls.get(\"add_file_id2\") == Some(\"c001.snappy.parquet\"))\n    assert(idToUrls.contains(\"dv_file_id\"))\n    assert(idToUrls.get(\"dv_file_id\") == Some(\"fakeurl\"))\n  }\n\n  test(\"getRefresherForGetCDFFiles with deletion vector\") {\n    val client = new SimpleTestDeltaSharingClient()\n    val table = Table(name = \"table\", schema = \"schema\", share = \"share\")\n    val func: RefresherFunction = getRefresherForGetCDFFiles(\n      client,\n      table,\n      Map[String, String](\"startingVersion\" -> \"0\")\n    )\n    val idToUrls = func(None).idToUrl\n    assert(idToUrls.size == 4)\n    assert(idToUrls.contains(\"add_file_id1\"))\n    assert(idToUrls.get(\"add_file_id1\") == Some(\"c000.snappy.parquet\"))\n    assert(idToUrls.contains(\"add_file_id2\"))\n    assert(idToUrls.get(\"add_file_id2\") == Some(\"c001.snappy.parquet\"))\n    assert(idToUrls.contains(\"dv_file_id\"))\n    assert(idToUrls.get(\"dv_file_id\") == Some(\"fakeurl\"))\n    assert(idToUrls.contains(\"cdc_file_id\"))\n    assert(idToUrls.get(\"cdc_file_id\") == Some(\"_change_data/cdc.c000.snappy.parquet\"))\n  }\n\n  test(\"getRefresherForGetFiles respects useRefreshToken parameter\") {\n    // Test client that tracks the refresh token parameter\n    class RefreshTokenTrackingClient extends SimpleTestDeltaSharingClient {\n      var lastRefreshToken: Option[String] = null\n\n      override def getFiles(\n        table: Table,\n        predicates: Seq[String],\n        limit: Option[Long],\n        versionAsOf: Option[Long],\n        timestampAsOf: Option[String],\n        jsonPredicateHints: Option[String],\n        refreshToken: Option[String],\n        fileIdHash: Option[String]\n      ): DeltaTableFiles = {\n        lastRefreshToken = refreshToken\n        super.getFiles(table, predicates, limit, versionAsOf, timestampAsOf,\n          jsonPredicateHints, refreshToken, fileIdHash)\n      }\n    }\n\n    val client = new RefreshTokenTrackingClient()\n    val table = Table(name = \"table\", schema = \"schema\", share = \"share\")\n    val testRefreshToken = Some(\"test-refresh-token\")\n\n    // Test with useRefreshToken = true - should use the provided refresh token\n    val funcWithRefreshToken: RefresherFunction = getRefresherForGetFiles(\n      client,\n      table,\n      Seq.empty,\n      None,\n      Some(0L),\n      None,\n      None,\n      useRefreshToken = true\n    )\n    funcWithRefreshToken(testRefreshToken)\n    assert(client.lastRefreshToken == testRefreshToken,\n      \"When useRefreshToken=true, the refresh token should be passed through\")\n\n    // Test with useRefreshToken = false - should ignore the provided refresh token\n    val funcWithoutRefreshToken: RefresherFunction = getRefresherForGetFiles(\n      client,\n      table,\n      Seq.empty,\n      None,\n      Some(0L),\n      None,\n      None,\n      useRefreshToken = false\n    )\n    funcWithoutRefreshToken(testRefreshToken)\n    assert(client.lastRefreshToken == None,\n      \"When useRefreshToken=false, the refresh token should be ignored and None should be used\")\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/TestClientForDeltaFormatSharing.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport io.delta.sharing.client.{\n  DeltaSharingClient,\n  DeltaSharingProfileProvider,\n  DeltaSharingRestClient\n}\nimport io.delta.sharing.client.model.{\n  AddFile => ClientAddFile,\n  DeltaTableFiles,\n  DeltaTableMetadata,\n  SingleAction,\n  Table,\n  TemporaryCredentials\n}\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.storage.BlockId\n\n/**\n * A mocked delta sharing client for DeltaFormatSharing.\n * The test suite need to prepare the mocked delta sharing rpc response and store them in\n * BlockManager. Then this client will just load the response of return upon rpc call.\n */\nprivate[spark] class TestClientForDeltaFormatSharing(\n    profileProvider: DeltaSharingProfileProvider,\n    timeoutInSeconds: Int = 120,\n    numRetries: Int = 3,\n    maxRetryDuration: Long = Long.MaxValue,\n    retrySleepInterval: Long = 1000,\n    sslTrustAll: Boolean = false,\n    forStreaming: Boolean = false,\n    responseFormat: String = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA,\n    readerFeatures: String = \"\",\n    queryTablePaginationEnabled: Boolean = false,\n    maxFilesPerReq: Int = 100000,\n    endStreamActionEnabled: Boolean = false,\n    enableAsyncQuery: Boolean = false,\n    asyncQueryPollIntervalMillis: Long = 10000L,\n    asyncQueryMaxDuration: Long = 600000L,\n    tokenExchangeMaxRetries: Int = 5,\n    tokenExchangeMaxRetryDurationInSeconds: Int = 60,\n    tokenRenewalThresholdInSeconds: Int = 600,\n    callerOrg: String = \"\",\n    skipFileIdHashVerification: Boolean = false)\n    extends DeltaSharingClient {\n\n  private val supportedReaderFeatures: Seq[String] = Seq(\n    DeletionVectorsTableFeature,\n    ColumnMappingTableFeature,\n    TimestampNTZTableFeature,\n    TypeWideningPreviewTableFeature,\n    TypeWideningTableFeature,\n    VariantTypePreviewTableFeature,\n    VariantTypeTableFeature,\n    VariantShreddingPreviewTableFeature\n  ).map(_.name)\n\n  assert(\n    responseFormat == DeltaSharingRestClient.RESPONSE_FORMAT_PARQUET ||\n      supportedReaderFeatures.forall(readerFeatures.split(\",\").contains),\n    s\"${supportedReaderFeatures.diff(readerFeatures.split(\",\")).mkString(\", \")} \" +\n      s\"should be supported in all types of queries.\"\n  )\n\n  import TestClientForDeltaFormatSharing._\n\n  TestClientForDeltaFormatSharing.lastCallerOrg = callerOrg\n\n  override def listAllTables(): Seq[Table] = throw new UnsupportedOperationException(\"not needed\")\n\n  override def getMetadata(\n      table: Table,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None): DeltaTableMetadata = {\n    val iterator = SparkEnv.get.blockManager\n      .get[String](getBlockId(table.name, \"getMetadata\", versionAsOf, timestampAsOf))\n      .map(_.data.asInstanceOf[Iterator[String]])\n      .getOrElse {\n        throw new IllegalStateException(\n          s\"getMetadata is missing for: ${table.name}, versionAsOf:$versionAsOf, \" +\n          s\"timestampAsOf:$timestampAsOf. This shouldn't happen in the unit test.\"\n        )\n      }\n    // iterator.toSeq doesn't trigger CompletionIterator in BlockManager which releases the reader\n    // lock on the underlying block. iterator hasNext does trigger it.\n    val linesBuilder = Seq.newBuilder[String]\n    while (iterator.hasNext) {\n      linesBuilder += iterator.next()\n    }\n    if (table.name.contains(\"shared_parquet_table\") &&\n      responseFormat.contains(DeltaSharingRestClient.RESPONSE_FORMAT_PARQUET)) {\n      val lines = linesBuilder.result()\n      val protocol = JsonUtils.fromJson[SingleAction](lines(0)).protocol\n      val metadata = JsonUtils.fromJson[SingleAction](lines(1)).metaData\n      DeltaTableMetadata(\n        version = versionAsOf.getOrElse(getTableVersion(table)),\n        protocol = protocol,\n        metadata = metadata,\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_PARQUET\n      )\n    } else {\n      DeltaTableMetadata(\n        version = versionAsOf.getOrElse(getTableVersion(table)),\n        lines = linesBuilder.result(),\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA\n      )\n    }\n  }\n\n  override def getTableVersion(table: Table, startingTimestamp: Option[String] = None): Long = {\n    val versionOpt = SparkEnv.get.blockManager.getSingle[Long](\n      getBlockId(table.name, \"getTableVersion\")\n    )\n    val version = versionOpt.getOrElse {\n      throw new IllegalStateException(\n        s\"getTableVersion is missing for: ${table.name}. This shouldn't happen in the unit test.\"\n      )\n    }\n    SparkEnv.get.blockManager.releaseLock(getBlockId(table.name, \"getTableVersion\"))\n    version\n  }\n\n  override def getFiles(\n      table: Table,\n      predicates: Seq[String],\n      limit: Option[Long],\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String],\n      jsonPredicateHints: Option[String],\n      refreshToken: Option[String],\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    val tableFullName = s\"${table.share}.${table.schema}.${table.name}\"\n    limit.foreach(lim => TestClientForDeltaFormatSharing.limits.put(tableFullName, lim))\n    TestClientForDeltaFormatSharing.requestedFormat.put(tableFullName, responseFormat)\n    jsonPredicateHints.foreach(p =>\n      TestClientForDeltaFormatSharing.jsonPredicateHints.put(tableFullName, p))\n\n    val iterator = SparkEnv.get.blockManager\n      .get[String](getBlockId(\n        table.name,\n        \"getFiles\",\n        versionAsOf = versionAsOf,\n        timestampAsOf = timestampAsOf,\n        limit = limit)\n      )\n      .map(_.data.asInstanceOf[Iterator[String]])\n      .getOrElse {\n        throw new IllegalStateException(\n          s\"getFiles is missing for: ${table.name} versionAsOf:$versionAsOf, \" +\n          s\"timestampAsOf:$timestampAsOf, limit: $limit. This shouldn't happen in the unit test.\"\n        )\n      }\n    // iterator.toSeq doesn't trigger CompletionIterator in BlockManager which releases the reader\n    // lock on the underlying block. iterator hasNext does trigger it.\n    val linesBuilder = Seq.newBuilder[String]\n    while (iterator.hasNext) {\n      linesBuilder += iterator.next()\n    }\n    if (table.name.contains(\"shared_parquet_table\") &&\n      responseFormat.contains(DeltaSharingRestClient.RESPONSE_FORMAT_PARQUET)) {\n      val lines = linesBuilder.result()\n      val protocol = JsonUtils.fromJson[SingleAction](lines(0)).protocol\n      val metadata = JsonUtils.fromJson[SingleAction](lines(1)).metaData\n      val files = ArrayBuffer[ClientAddFile]()\n      lines.drop(2).foreach { line =>\n        val action = JsonUtils.fromJson[SingleAction](line)\n        if (action.file != null) {\n          files.append(action.file)\n        } else {\n          throw new IllegalStateException(s\"Unexpected Line:${line}\")\n        }\n      }\n      DeltaTableFiles(\n        versionAsOf.getOrElse(getTableVersion(table)),\n        protocol,\n        metadata,\n        files.toSeq,\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_PARQUET\n      )\n    } else {\n      DeltaTableFiles(\n        version = versionAsOf.getOrElse(getTableVersion(table)),\n        lines = linesBuilder.result(),\n        respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA\n      )\n    }\n  }\n\n  override def getFiles(\n      table: Table,\n      startingVersion: Long,\n      endingVersion: Option[Long],\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    assert(\n      endingVersion.isDefined,\n      \"endingVersion is not defined. This shouldn't happen in unit test.\"\n    )\n    val tableFullName = s\"${table.share}.${table.schema}.${table.name}\"\n    TestClientForDeltaFormatSharing.requestedFormat.put(tableFullName, responseFormat)\n    val iterator = SparkEnv.get.blockManager\n      .get[String](getBlockId(table.name, s\"getFiles_${startingVersion}_${endingVersion.get}\"))\n      .map(_.data.asInstanceOf[Iterator[String]])\n      .getOrElse {\n        throw new IllegalStateException(\n          s\"getFiles is missing for: ${table.name} with [${startingVersion}, \" +\n          s\"${endingVersion.get}]. This shouldn't happen in the unit test.\"\n        )\n      }\n    // iterator.toSeq doesn't trigger CompletionIterator in BlockManager which releases the reader\n    // lock on the underlying block. iterator hasNext does trigger it.\n    val linesBuilder = Seq.newBuilder[String]\n    while (iterator.hasNext) {\n      linesBuilder += iterator.next()\n    }\n    DeltaTableFiles(\n      version = getTableVersion(table),\n      lines = linesBuilder.result(),\n      respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA\n    )\n  }\n\n  override def getCDFFiles(\n      table: Table,\n      cdfOptions: Map[String, String],\n      includeHistoricalMetadata: Boolean,\n      fileIdHash: Option[String]\n  ): DeltaTableFiles = {\n    val suffix = cdfOptions\n      .get(DeltaSharingOptions.CDF_START_VERSION)\n      .getOrElse(\n        cdfOptions.get(DeltaSharingOptions.CDF_START_TIMESTAMP).get\n      )\n    val iterator = SparkEnv.get.blockManager\n      .get[String](\n        getBlockId(\n          table.name,\n          s\"getCDFFiles_$suffix\"\n        )\n      )\n      .map(\n        _.data.asInstanceOf[Iterator[String]]\n      )\n      .getOrElse {\n        throw new IllegalStateException(\n          s\"getCDFFiles is missing for: ${table.name}. This shouldn't happen in the unit test.\"\n        )\n      }\n    // iterator.toSeq doesn't trigger CompletionIterator in BlockManager which releases the reader\n    // lock on the underlying block. iterator hasNext does trigger it.\n    val linesBuilder = Seq.newBuilder[String]\n    while (iterator.hasNext) {\n      linesBuilder += iterator.next()\n    }\n    DeltaTableFiles(\n      version = getTableVersion(table),\n      lines = linesBuilder.result(),\n      respondedFormat = DeltaSharingRestClient.RESPONSE_FORMAT_DELTA\n    )\n  }\n\n  override def generateTemporaryTableCredential(\n      table: Table,\n      location: Option[String]): TemporaryCredentials = {\n    throw new UnsupportedOperationException(\"generateTemporaryTableCredential is not implemented\")\n  }\n\n  override def getForStreaming(): Boolean = forStreaming\n\n  override def getProfileProvider: DeltaSharingProfileProvider = profileProvider\n}\n\nobject TestClientForDeltaFormatSharing {\n  def getBlockId(\n      sharedTableName: String,\n      queryType: String,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      limit: Option[Long] = None): BlockId = {\n    assert(!(versionAsOf.isDefined && timestampAsOf.isDefined))\n    val suffix = if (versionAsOf.isDefined) {\n      s\"_v${versionAsOf.get}\"\n    } else if (timestampAsOf.isDefined) {\n      s\"_t${timestampAsOf.get}\"\n    } else {\n      \"\"\n    }\n    val limitSuffix = limit.map{ l => s\"_l${l}\"}.getOrElse(\"\")\n    BlockId(\n      s\"${DeltaSharingUtils.DELTA_SHARING_BLOCK_ID_PREFIX}\" +\n      s\"_${sharedTableName}_$queryType$suffix$limitSuffix\"\n    )\n  }\n\n  val limits = scala.collection.mutable.Map[String, Long]()\n  val requestedFormat = scala.collection.mutable.Map[String, String]()\n  val jsonPredicateHints = scala.collection.mutable.Map[String, String]()\n  @volatile var lastCallerOrg: String = \"\"\n}\n"
  },
  {
    "path": "sharing/src/test/scala/io/delta/sharing/spark/TestDeltaSharingFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark\n\nimport java.io.FileNotFoundException\nimport java.net.{URI, URLDecoder, URLEncoder}\nimport java.util.concurrent.TimeUnit\n\nimport io.delta.sharing.client.DeltaSharingFileSystem\nimport org.apache.hadoop.fs._\nimport org.apache.hadoop.fs.permission.FsPermission\nimport org.apache.hadoop.util.Progressable\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.delta.sharing.{PreSignedUrlCache, PreSignedUrlFetcher}\nimport org.apache.spark.storage.BlockId\n\n/**\n * Read-only file system for DeltaSharingDataSourceDeltaSuite.\n * To replace DeltaSharingFileSystem and return the content for parquet files.\n */\nprivate[spark] class TestDeltaSharingFileSystem extends FileSystem {\n  import TestDeltaSharingFileSystem._\n\n  private lazy val preSignedUrlCacheRef = PreSignedUrlCache.getEndpointRefInExecutor(SparkEnv.get)\n\n  override def getScheme: String = SCHEME\n\n  override def getUri(): URI = URI.create(s\"$SCHEME:///\")\n\n  override def open(f: Path, bufferSize: Int): FSDataInputStream = {\n    val path = DeltaSharingFileSystem.decode(f)\n    val fetcher =\n      new PreSignedUrlFetcher(\n        preSignedUrlCacheRef,\n        path.tablePath,\n        path.fileId,\n        TimeUnit.MINUTES.toMillis(10)\n      )\n    val (tableName, parquetFilePath) = decode(fetcher.getUrl())\n    val arrayBuilder = Array.newBuilder[Byte]\n    val iterator = SparkEnv.get.blockManager\n      .get[Byte](getBlockId(tableName, parquetFilePath))\n      .map(\n        _.data.asInstanceOf[Iterator[Byte]]\n      )\n      .getOrElse {\n        throw new FileNotFoundException(f.toString)\n      }\n    while (iterator.hasNext) {\n      arrayBuilder += iterator.next()\n    }\n    new FSDataInputStream(new SeekableByteArrayInputStream(arrayBuilder.result()))\n  }\n\n  override def create(\n      f: Path,\n      permission: FsPermission,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream =\n    throw new UnsupportedOperationException(\"create\")\n\n  override def append(f: Path, bufferSize: Int, progress: Progressable): FSDataOutputStream =\n    throw new UnsupportedOperationException(\"append\")\n\n  override def rename(src: Path, dst: Path): Boolean =\n    throw new UnsupportedOperationException(\"rename\")\n\n  override def delete(f: Path, recursive: Boolean): Boolean =\n    throw new UnsupportedOperationException(\"delete\")\n\n  override def listStatus(f: Path): Array[FileStatus] =\n    throw new UnsupportedOperationException(\"listStatus\")\n\n  override def setWorkingDirectory(new_dir: Path): Unit =\n    throw new UnsupportedOperationException(\"setWorkingDirectory\")\n\n  override def getWorkingDirectory: Path = new Path(getUri)\n\n  override def mkdirs(f: Path, permission: FsPermission): Boolean =\n    throw new UnsupportedOperationException(\"mkdirs\")\n\n  override def getFileStatus(f: Path): FileStatus = {\n    val resolved = makeQualified(f)\n    new FileStatus(DeltaSharingFileSystem.decode(resolved).fileSize, false, 0, 1, 0, f)\n  }\n\n  override def close(): Unit = {\n    super.close()\n  }\n}\n\nprivate[spark] object TestDeltaSharingFileSystem {\n  val SCHEME = \"delta-sharing\"\n\n  def getBlockId(tableName: String, parquetFilePath: String): BlockId = {\n    BlockId(\n      s\"${DeltaSharingUtils.DELTA_SHARING_BLOCK_ID_PREFIX}_\" +\n      s\"{$tableName}_$parquetFilePath\"\n    )\n  }\n\n  // The encoded string is purely for testing purpose to contain the table name and file path,\n  // which will be decoded and used to find block in block manager.\n  // In real traffic, it will be a pre-signed url.\n  def encode(tableName: String, parquetFilePath: String): String = {\n    val encodedTableName = URLEncoder.encode(tableName, \"UTF-8\")\n    val encodedParquetFilePath = URLEncoder.encode(parquetFilePath, \"UTF-8\")\n    // SCHEME:/// is needed for making this path an absolute path\n    s\"$SCHEME:///$encodedTableName/$encodedParquetFilePath\"\n  }\n\n  def decode(encodedPath: String): (String, String) = {\n    val Array(tableName, parquetFilePath) = encodedPath\n      .stripPrefix(s\"$SCHEME:///\")\n      .stripPrefix(s\"$SCHEME:/\")\n      .split(\"/\")\n      .map(\n        URLDecoder.decode(_, \"UTF-8\")\n      )\n    (tableName, parquetFilePath)\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala-shims/spark-4.0/SharingStreamingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark.test.shims\n\nimport org.apache.spark.sql.execution.streaming.{\n  CheckpointFileManager => CheckpointFileManagerShim,\n  CommitMetadata => CommitMetadataShim,\n  SerializedOffset => SerializedOffsetShim,\n  StreamMetadata => StreamMetadataShim\n}\n\n/**\n * Test shims for streaming classes that were relocated in Spark 4.1.\n * In Spark 4.0, these classes are in org.apache.spark.sql.execution.streaming.\n * StreamingCheckpointConstants does not exist in Spark 4.0, so we define\n * the constants directly.\n */\nobject SharingStreamingTestShims {\n  val CheckpointFileManager: CheckpointFileManagerShim.type =\n    CheckpointFileManagerShim\n  val CommitMetadata: CommitMetadataShim.type = CommitMetadataShim\n  val SerializedOffset: SerializedOffsetShim.type = SerializedOffsetShim\n  val StreamMetadata: StreamMetadataShim.type = StreamMetadataShim\n\n  object StreamingCheckpointConstants {\n    val DIR_NAME_COMMITS = \"commits\"\n    val DIR_NAME_OFFSETS = \"offsets\"\n    val DIR_NAME_METADATA = \"metadata\"\n  }\n}\n"
  },
  {
    "path": "sharing/src/test/scala-shims/spark-4.1/SharingStreamingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark.test.shims\n\nimport org.apache.spark.sql.execution.streaming.checkpointing.{\n  CheckpointFileManager => CheckpointFileManagerShim,\n  CommitMetadata => CommitMetadataShim\n}\nimport org.apache.spark.sql.execution.streaming.runtime.{\n  SerializedOffset => SerializedOffsetShim,\n  StreamingCheckpointConstants => StreamingCheckpointConstantsShim,\n  StreamMetadata => StreamMetadataShim\n}\n\n/**\n * Test shims for streaming classes that were relocated in Spark 4.1.\n * In Spark 4.1, these classes moved to checkpointing and runtime sub-packages.\n */\nobject SharingStreamingTestShims {\n  val CheckpointFileManager: CheckpointFileManagerShim.type =\n    CheckpointFileManagerShim\n  val CommitMetadata: CommitMetadataShim.type = CommitMetadataShim\n  val SerializedOffset: SerializedOffsetShim.type = SerializedOffsetShim\n  val StreamMetadata: StreamMetadataShim.type = StreamMetadataShim\n  val StreamingCheckpointConstants: StreamingCheckpointConstantsShim.type =\n    StreamingCheckpointConstantsShim\n}\n"
  },
  {
    "path": "sharing/src/test/scala-shims/spark-4.2/SharingStreamingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sharing.spark.test.shims\n\nimport org.apache.spark.sql.execution.streaming.checkpointing.{\n  CheckpointFileManager => CheckpointFileManagerShim,\n  CommitMetadata => CommitMetadataShim\n}\nimport org.apache.spark.sql.execution.streaming.runtime.{\n  SerializedOffset => SerializedOffsetShim,\n  StreamingCheckpointConstants => StreamingCheckpointConstantsShim,\n  StreamMetadata => StreamMetadataShim\n}\n\n/**\n * Test shims for streaming classes that were relocated in Spark 4.1+.\n * In Spark 4.2, these classes remain in the same locations as Spark 4.1.\n */\nobject SharingStreamingTestShims {\n  val CheckpointFileManager: CheckpointFileManagerShim.type =\n    CheckpointFileManagerShim\n  val CommitMetadata: CommitMetadataShim.type = CommitMetadataShim\n  val SerializedOffset: SerializedOffsetShim.type = SerializedOffsetShim\n  val StreamMetadata: StreamMetadataShim.type = StreamMetadataShim\n  val StreamingCheckpointConstants: StreamingCheckpointConstantsShim.type =\n    StreamingCheckpointConstantsShim\n}\n"
  },
  {
    "path": "spark/delta-suite-generator/src/main/resources/scalafmt.conf",
    "content": "align = none\nalign.openParenDefnSite = false\nalign.openParenCallSite = false\nalign.tokens = []\nindent.extendSite = 2\nimportSelectors = \"singleLine\"\noptIn.configStyleArguments = false\ndanglingParentheses {\n  defnSite = false\n  callSite = false\n}\ndocstrings {\n  style = Asterisk\n  wrap = no\n}\nliterals.hexDigits = upper\nmaxColumn = 100\nrewrite.rules = [Imports]\nrewrite.imports.sort = scalastyle\nrewrite.imports.groups = [\n  [\"java\\\\..*\"],\n  [\"scala\\\\..*\"],\n  [\"io\\\\.delta\\\\..*\"],\n  [\"org\\\\.apache\\\\.spark\\\\.sql\\\\.delta.*\"]\n]\nrunner.dialect = scala212\nversion = 3.9.6\nnewlines.topLevelStatementBlankLines = [\n  { blanks = 1 }\n]\n"
  },
  {
    "path": "spark/delta-suite-generator/src/main/scala/io/delta/suitegenerator/ModularSuiteGenerator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.suitegenerator\n\nimport java.nio.file.{Files, Paths}\n\nimport scala.meta._\nimport scala.util.hashing.MurmurHash3\n\nimport org.apache.commons.cli.{CommandLine, DefaultParser, HelpFormatter, Option, Options}\nimport org.apache.commons.codec.binary.Base32\n\n/**\n * The main generator for the Modular Delta Suites. Generated suite combinations can be configured\n * in [[SuiteGeneratorConfig]].\n *\n * Can be run via the sbt command: `deltaSuiteGenerator / run`\n */\nobject ModularSuiteGenerator {\n\n  val GENERATED_PACKAGE = s\"org.apache.spark.sql.delta.generatedsuites\"\n\n  lazy val OUTPUT_PATH: String = \"spark/src/test/scala/\" + GENERATED_PACKAGE.replace('.', '/')\n\n  private val DEFAULT_REPO_PATH = \"~/delta\"\n\n  /**\n   * Controls when to start truncating and hashing the suite names to prevent extremely long names.\n   */\n  private val SUITE_NAME_CHAR_LIMIT = 255 - 148\n\n  private lazy val OPT_REPO_PATH = new Option(\n    /* option = */ \"p\",\n    /* longOption = */ \"repo-path\",\n    /* hasArg = */ true,\n    /* description = */ s\"Path to the repository root. Defaults to $DEFAULT_REPO_PATH\")\n  private lazy val OPT_HELP = new Option(\n    /* option = */ \"h\",\n    /* longOption = */ \"help\",\n    /* hasArg = */ false,\n    /* description = */ \"Print help\")\n  private lazy val OPTIONS = new Options().addOption(OPT_REPO_PATH).addOption(OPT_HELP)\n\n  def main(args: Array[String]): Unit = {\n    val cmd = new DefaultParser().parse(OPTIONS, args)\n\n    if (cmd.hasOption(OPT_HELP)) {\n      val formatter = new HelpFormatter()\n      formatter.printHelp(\n        \"bazel run //sql/core/delta_suite_generator:generate -- <options>\",\n        OPTIONS)\n      System.exit(0)\n    }\n\n    val suitesWriter = getWriter(cmd)\n\n    // scalastyle:off println\n    println(\"Generating suites...\")\n    generateSuites(suitesWriter)\n    println(\"Suite generation completed successfully.\")\n    // scalastyle:on println\n  }\n\n  def generateSuites(suitesWriter: SuitesWriter): Unit = {\n    for (testGroup <- SuiteGeneratorConfig.TEST_GROUPS) {\n      val suites = for {\n        testConfig <- testGroup.testConfigs\n        baseSuite <- testConfig.baseSuites\n        dimensions <- testConfig.dimensionCombinations\n      } yield dimensions\n        // Generate all combinations of dimension traits\n        .foldLeft(List(List.empty[(String, String)])) {\n          (acc, dimension) =>\n            (if (dimension.isOptional) acc else List.empty) :::\n            (for {\n              accValue <- acc\n              traitWithAlias <- dimension.traitsWithAliases\n            } yield accValue :+ traitWithAlias)\n        }\n        .filterNot(dimTraits => SuiteGeneratorConfig.isExcluded(baseSuite, dimTraits.map(_._1)))\n        .map(dimTraits => generateCode(baseSuite, dimTraits))\n\n      suitesWriter.writeGeneratedSuitesOfGroup(suites.flatten, testGroup)\n    }\n    suitesWriter.conclude()\n  }\n\n  private def getWriter(cmd: CommandLine): SuitesWriter = {\n    var repoPath = cmd.getOptionValue(OPT_REPO_PATH, DEFAULT_REPO_PATH)\n\n    // Expand `~` prefix to the user's home directory\n    if (repoPath.startsWith(\"~\")) {\n      repoPath = System.getProperty(\"user.home\") + repoPath.substring(1)\n    }\n\n    val outputPath = Paths.get(repoPath, OUTPUT_PATH)\n    assert(\n      Files.exists(outputPath.getParent),\n      s\"Repository could not be detected at $repoPath. Make sure to provide the \" +\n        s\"repository path using the --${OPT_REPO_PATH.getLongOpt} option.\")\n\n    // Prevent people with multiple repository copies/worktrees accidentally generating into\n    // the wrong one.\n    // We assume if it's specified explicitly, it's specified correctly, and we don't need to\n    // double-check.\n    if (!cmd.hasOption(OPT_REPO_PATH)) {\n      // scalastyle:off println\n      if (System.console() == null) {\n        // This is not an interactive shell, we can't ask for input.\n        println(\n          s\"\"\"Verified that a matching repository exists at target.\n             |Generation target path is: '${outputPath}'\n             |The path can be customised with the --${OPT_REPO_PATH.getLongOpt} option.\"\"\"\n             .stripMargin)\n      } else {\n        println(\n          s\"\"\"Verified that a matching repository exists at target.\n             |Please double check the path: '${outputPath}'\n             |The path can be customised with the --${OPT_REPO_PATH.getLongOpt} option.\n             |If correct, press <enter> to generate or <ctrl>+c to abort.\"\"\".stripMargin)\n        scala.io.StdIn.readLine()\n      }\n      // scalastyle:on println\n    }\n\n    new SuitesWriter(outputPath)\n  }\n\n  private lazy val BASE32 = new Base32()\n\n  private def generateCode(\n      baseSuite: String,\n      mixinsAndAliases: List[(String, String)]): TestSuite = {\n    val allMixins = SuiteGeneratorConfig\n      .applyCustomRulesAndGetAllMixins(baseSuite, mixinsAndAliases.map(_._1))\n    val suiteParents = (baseSuite :: allMixins).map(_.parse[Init].get)\n\n    // Generate suite name by combining the names of base suite and dimension aliases.\n    // Remove some redundant substrings for better readability\n    val baseSuitePrefix = baseSuite.stripSuffix(\"Suite\").stripSuffix(\"Tests\")\n    val mixinSuffix = mixinsAndAliases\n      .map(_._2.replace(\"Mixin\", \"\"))\n      .mkString(\"\")\n    var suiteName = baseSuitePrefix + mixinSuffix\n\n    // Truncate the name and replace with a consistent hash if line becomes longer than the limit\n    val maxSuiteNameLength = SUITE_NAME_CHAR_LIMIT - \"Suite\".length\n    if (suiteName.length > maxSuiteNameLength) {\n      // scalastyle:off println\n      println(s\"WARNING: Suite name is too long, truncating and hashing to fit within the limit. \" +\n        s\"Please consider renaming the base suite or defining shorter dimension aliases. \" +\n        s\"Suite: $suiteName (${suiteName.length} characters > $maxSuiteNameLength limit).\")\n      // scalastyle:on println\n\n      val hashBytes = BigInt(MurmurHash3.stringHash(suiteName)).toByteArray\n      val hashEncoded = BASE32.encodeToString(hashBytes).replace(\"=\", \"\")\n      suiteName = suiteName.substring(0, maxSuiteNameLength - hashEncoded.length) + hashEncoded\n    }\n\n    suiteName += \"Suite\"\n    TestSuite(\n      suiteName,\n      q\"\"\"class ${Type.Name(suiteName)}\n          extends ..$suiteParents\"\"\")\n  }\n}\n\ncase class TestSuite(\n    name: String,\n    classDefinition: Defn.Class\n)\n"
  },
  {
    "path": "spark/delta-suite-generator/src/main/scala/io/delta/suitegenerator/SuiteGeneratorConfig.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.suitegenerator\n\nimport scala.collection.mutable.ListBuffer\nimport scala.meta._\n\n/**\n * Represents a configuration trait that changes how the tests are executed. This can include Spark\n * configs, overrides, test excludes, and more.\n * @param name the name of the dimension.\n * @param values the possible values for this dimension, which when prepended with the name should\n * equal to the desired trait name that needs to be mixed in to generated suites.\n * @param alias an optional short alias to be used when naming suites instead of the [[name]].\n */\nabstract class Dimension(val name: String, val values: List[String], val alias: Option[String]) {\n  /**\n   * All trait names for this dimension\n   */\n  lazy val traitNames: List[String] = values.map(value => name + value)\n\n  lazy val traitsWithAliases: List[(String, String)] = values\n    .map(value => (\n      name + value,\n      alias.getOrElse(name) + value.replace(\"Enabled\", \"On\").replace(\"Disabled\", \"Off\")\n    ))\n\n  val isOptional: Boolean = false\n\n  /**\n   * same [[Dimension]] with an additional state of not being added to the suite.\n   */\n  def asOptional: Dimension = new OptionalDimension(name, values)\n\n  private class OptionalDimension(\n      override val name: String,\n      override val values: List[String]\n  ) extends Dimension(name, values, alias) {\n    override val isOptional: Boolean = true\n    override def asOptional: Dimension = this\n  }\n\n  // A bit of DSL for better readability of the test configs.\n  def and(other: Dimension): List[Dimension] = this :: other :: Nil\n  def and(others: List[Dimension]): List[Dimension] = this :: others\n  def alone: List[Dimension] = this :: Nil\n}\n\n/**\n * A default [[Dimension]] implementation for dimensions with multiple possible values, such as\n * column mapping\n */\ncase class DimensionWithMultipleValues(\n    override val name: String,\n    override val values: List[String],\n    override val alias: Option[String] = None\n) extends Dimension(name, values, alias) {\n  /**\n   * Shortcut to create a [[DimensionMixin]] with the same name and one of the values as the suffix.\n   * @param valueSelector a functions that selects a value from this dimension's values\n   */\n  def withValueAsDimension(valueSelector: List[String] => String): DimensionMixin = {\n    DimensionMixin(name, valueSelector(values), alias)\n  }\n}\n\n/**\n * A specialized [[Dimension]] that does not have any values, it is either present or not.\n */\ncase class DimensionMixin(\n    override val name: String,\n    suffix: String = \"Mixin\",\n    override val alias: Option[String] = None\n) extends Dimension(name, List(suffix), alias) {\n  lazy val traitName: String = name + suffix\n}\n\n/**\n * Main configuration class for the suite generator. It allows defining a set of base suites and the\n * dimension combinations that should be used to generate the test configurations. Suites are\n * generated for each base suite and for each value combination of the dimension combinations.\n * @param baseSuites a list of base class or trait names that contains the actual test cases.\n * Ideally, these should not contain any configuration logic, and instead rely on [[Dimension]]s to\n * make the necessary setup.\n */\ncase class TestConfig(\n    baseSuites: List[String],\n    dimensionCombinations: List[List[Dimension]] = List.empty\n)\n\n/**\n * Represents a generated Scala file with suite definitions.\n * @param name the name of the generated file.\n * @param imports a list of packages that needs to be imported in this file.\n * @param testConfigs a list of [[TestConfig]]s that should be generated in this file.\n */\ncase class TestGroup(\n    name: String,\n    imports: List[Importer],\n    testConfigs: List[TestConfig]\n)\n\nobject SuiteGeneratorConfig {\n  private object Dims {\n    // Just to improve readability of the test configurations a bit.\n    // `Dims.NONE` is clearer than just `Nil`.\n    val NONE: List[Dimension] = Nil\n\n    val TABLE_ACCESS = DimensionWithMultipleValues( // no alias needed, value is self-explanatory\n      \"DeltaDMLTestUtils\", List(\"NameBased\", \"PathBased\"), alias = Some(\"\"))\n    val PATH_BASED = TABLE_ACCESS.withValueAsDimension(_.last)\n    val NAME_BASED = TABLE_ACCESS.withValueAsDimension(_.head)\n    val MERGE_SQL = DimensionMixin(\"MergeIntoSQL\", alias = Some(\"SQL\"))\n    val MERGE_SCALA = DimensionMixin(\"MergeIntoScala\", alias = Some(\"Scala\"))\n    val MERGE_DVS = DimensionMixin(\"MergeIntoDVs\", alias = Some(\"DVs\"))\n    val PREDPUSH = DimensionWithMultipleValues(\n      \"PredicatePushdown\", List(\"Disabled\", \"Enabled\"), alias = Some(\"PredPush\"))\n    val CDC = DimensionMixin(\"CDC\", suffix = \"Enabled\")\n    // These enables/disable DVs on new tables, but leave DML command configs untouched.\n    val PERSISTENT_DV = DimensionWithMultipleValues(\n      \"PersistentDV\", List(\"Disabled\", \"Enabled\"), alias = Some(\"DV\"))\n    val PERSISTENT_DV_OFF = PERSISTENT_DV.withValueAsDimension(_.head)\n    val PERSISTENT_DV_ON = PERSISTENT_DV.withValueAsDimension(_.last)\n    val ROW_TRACKING = DimensionWithMultipleValues(\"RowTracking\", List(\"Disabled\", \"Enabled\"))\n    val ROW_TRACKING_ON = ROW_TRACKING.withValueAsDimension(_.last)\n    val MERGE_PERSISTENT_DV_OFF = DimensionMixin(\"MergePersistentDV\", suffix = \"Disabled\")\n    val MERGE_ROW_TRACKING_DV = DimensionMixin(\"RowTrackingMergeDV\")\n    val COLUMN_MAPPING = DimensionWithMultipleValues(\n      \"DeltaColumnMappingEnable\", List(\"IdMode\", \"NameMode\"), alias = Some(\"ColMap\"))\n    val UPDATE_SCALA = DimensionMixin(\"UpdateScala\", alias = Some(\"Scala\"))\n    val UPDATE_SQL = DimensionMixin(\"UpdateSQL\", alias = Some(\"SQL\"))\n    val UPDATE_DVS = DimensionMixin(\"UpdateSQLWithDeletionVectors\", alias = Some(\"DV\"))\n    val UPDATE_ROW_TRACKING_DV = DimensionMixin(\"RowTrackingUpdateDV\")\n    val DELETE_SCALA = DimensionMixin(\"DeleteScala\", alias = Some(\"Scala\"))\n    val DELETE_SQL = DimensionMixin(\"DeleteSQL\", alias = Some(\"SQL\"))\n    val DELETE_WITH_DVS = DimensionMixin(\"DeleteSQLWithDeletionVectors\", alias = Some(\"DV\"))\n  }\n\n  private object Tests {\n    val MERGE_BASE = List(\n      \"MergeIntoBasicTests\",\n      \"MergeIntoTempViewsTests\",\n      \"MergeIntoNestedDataTests\",\n      \"MergeIntoUnlimitedMergeClausesTests\",\n      \"MergeIntoAnalysisExceptionTests\",\n      \"MergeIntoExtendedSyntaxTests\",\n      \"MergeIntoSuiteBaseMiscTests\",\n      \"MergeIntoNotMatchedBySourceSuite\",\n      \"MergeIntoNotMatchedBySourceCDCPart1Tests\",\n      \"MergeIntoNotMatchedBySourceCDCPart2Tests\",\n      \"MergeIntoSchemaEvolutionCoreTests\",\n      \"MergeIntoSchemaEvolutionBaseNewColumnTests\",\n      \"MergeIntoSchemaEvolutionBaseExistingColumnTests\",\n      \"MergeIntoSchemaEvoStoreAssignmentPolicyTests\",\n      \"MergeIntoSchemaEvolutionNotMatchedBySourceTests\",\n      \"MergeIntoNestedStructInMapEvolutionTests\",\n      \"MergeIntoNestedStructEvolutionUpdateOnlyTests\",\n      \"MergeIntoNestedStructEvolutionInsertTests\"\n    )\n    val MERGE_SQL = List(\n      \"MergeIntoSQLTests\",\n      \"MergeIntoSQLNondeterministicOrderTests\"\n    )\n    val UPDATE_BASE = List(\n      \"UpdateBaseTempViewTests\",\n      \"UpdateBaseMiscTests\"\n    )\n    val DELETE_BASE = List(\n      \"DeleteTempViewTests\",\n      \"DeleteBaseTests\"\n    )\n  }\n\n  implicit class DimensionListExt(val dims: List[Dimension]) {\n    /**\n     * @return a new list of dimension combinations where each combination has the\n     * [[commonDims]] prepended to it.\n     */\n    def prependToAll(dimensionCombinations: List[Dimension]*): List[List[Dimension]] = {\n      dimensionCombinations.toList.map(dims ::: _)\n    }\n\n    def prependToAll(dimensionCombinations: List[List[Dimension]]): List[List[Dimension]] = {\n      prependToAll(dimensionCombinations: _*)\n    }\n\n    // Continued DSL from the Dimension class above to work around the different\n    // operator precedence between :: and `and`.\n    def and(other: Dimension): List[Dimension] = dims ::: other :: Nil\n    def and(others: List[Dimension]): List[Dimension] = dims ::: others\n  }\n\n  /**\n   * All [[TestGroup]] definitions. The generated suites of each group will be written\n   * to a file named after the group name. Keep in mind that [[isExcluded]] can be used to filter\n   * out some of the test configurations, so defining a configuration here does not guarantee\n   * generation of a suite for it.\n   */\n  lazy val TEST_GROUPS: List[TestGroup] = List(\n    // scalastyle:off line.size.limit\n    TestGroup(\n      name = \"MergeSuites\",\n      imports = List(\n        importer\"org.apache.spark.sql.delta._\",\n        importer\"org.apache.spark.sql.delta.cdc._\",\n        importer\"org.apache.spark.sql.delta.rowid._\"\n      ),\n      testConfigs = List(\n        TestConfig(\n          \"MergeIntoScalaTests\" :: Tests.MERGE_BASE,\n          List(\n            List(Dims.MERGE_SCALA)\n          )\n        ),\n        TestConfig(\n          \"MergeCDCTests\" :: \"MergeIntoDVsTests\" :: Tests.MERGE_SQL ::: Tests.MERGE_BASE,\n          List(Dims.MERGE_SQL).prependToAll(\n            List(Dims.NAME_BASED),\n            List(Dims.PATH_BASED, Dims.COLUMN_MAPPING.asOptional),\n            List(Dims.PATH_BASED, Dims.MERGE_DVS, Dims.PREDPUSH),\n            List(Dims.PATH_BASED, Dims.CDC),\n            List(Dims.PATH_BASED, Dims.CDC, Dims.MERGE_DVS, Dims.PREDPUSH)\n          )\n        ),\n        TestConfig(\n          List(\"MergeIntoMaterializeSourceTests\", \"MergeIntoMaterializeSourceErrorTests\"),\n          List(\n            List(Dims.MERGE_PERSISTENT_DV_OFF)\n          )\n        ),\n        TestConfig(\n          List(\"RowTrackingMergeCommonTests\"),\n          List(Dims.NAME_BASED, Dims.CDC.asOptional).prependToAll(\n            List(Dims.MERGE_ROW_TRACKING_DV.asOptional),\n            List(Dims.PERSISTENT_DV_OFF, Dims.MERGE_PERSISTENT_DV_OFF)\n          ) :::\n          List(Dims.NAME_BASED, Dims.COLUMN_MAPPING).prependToAll(\n            List(),\n            List(Dims.CDC, Dims.MERGE_ROW_TRACKING_DV)\n          )\n        ),\n        TestConfig(\n          \"MergeIntoTopLevelStructEvolutionNullnessTests\" ::\n            \"MergeIntoNestedStructEvolutionNullnessTests\" ::\n            \"MergeIntoTopLevelArrayStructEvolutionNullnessTests\" ::\n            \"MergeIntoNestedArrayStructEvolutionNullnessTests\" ::\n            \"MergeIntoTopLevelMapStructEvolutionNullnessTests\" ::\n            \"MergeIntoNestedMapStructEvolutionNullnessTests\" ::\n            \"MergeIntoStructEvolutionNullnessMultiClauseTests\" :: Nil,\n          List(\n            List(\n              Dims.MERGE_SQL, Dims.NAME_BASED\n            )\n          )\n        )\n      )\n    ),\n    TestGroup(\n      name = \"UpdateSuites\",\n      imports = List(\n        importer\"org.apache.spark.sql.delta._\",\n        importer\"org.apache.spark.sql.delta.cdc._\",\n        importer\"org.apache.spark.sql.delta.rowid._\",\n        importer\"org.apache.spark.sql.delta.rowtracking._\"\n      ),\n      testConfigs = List(\n        TestConfig(\n          \"UpdateScalaTests\" :: Tests.UPDATE_BASE,\n          List(\n            List(Dims.UPDATE_SCALA)\n          )\n        ),\n        TestConfig(\n          \"UpdateSQLTests\" :: Tests.UPDATE_BASE,\n          List(\n            List(Dims.UPDATE_SQL, Dims.NAME_BASED)\n          )\n        ),\n        TestConfig(\n          \"UpdateCDCWithDeletionVectorsTests\" ::\n            \"UpdateCDCTests\" ::\n            \"UpdateSQLWithDeletionVectorsTests\" ::\n            \"UpdateSQLTests\" ::\n            Tests.UPDATE_BASE,\n          List(\n            List(Dims.UPDATE_SQL, Dims.PATH_BASED, Dims.CDC.asOptional, Dims.ROW_TRACKING.asOptional),\n            List(Dims.UPDATE_SQL, Dims.PATH_BASED, Dims.CDC, Dims.UPDATE_DVS),\n            List(Dims.UPDATE_SQL, Dims.PATH_BASED, Dims.UPDATE_DVS, Dims.PREDPUSH)\n          )\n        ),\n        TestConfig(\n          List(\"RowTrackingUpdateCommonTests\"),\n          List(\n            List(Dims.CDC.asOptional, Dims.COLUMN_MAPPING.asOptional),\n            List(Dims.UPDATE_ROW_TRACKING_DV),\n            List(Dims.UPDATE_ROW_TRACKING_DV, Dims.CDC, Dims.COLUMN_MAPPING.asOptional)\n          )\n        )\n      )\n    ),\n    TestGroup(\n      name = \"DeleteSuites\",\n      imports = List(\n        importer\"org.apache.spark.sql.delta._\",\n        importer\"org.apache.spark.sql.delta.cdc._\",\n        importer\"org.apache.spark.sql.delta.rowid._\"\n      ),\n      testConfigs = List(\n        TestConfig(\n          \"DeleteScalaTests\" :: Tests.DELETE_BASE,\n          List(\n            List(Dims.DELETE_SCALA)\n          )\n        ),\n        TestConfig(\n          \"DeleteCDCTests\" :: \"DeleteSQLTests\" :: Tests.DELETE_BASE,\n          List(\n            List(Dims.DELETE_SQL, Dims.NAME_BASED),\n            List(Dims.DELETE_SQL, Dims.PATH_BASED, Dims.COLUMN_MAPPING.asOptional),\n            List(Dims.DELETE_SQL, Dims.PATH_BASED, Dims.DELETE_WITH_DVS, Dims.PREDPUSH),\n            List(Dims.DELETE_SQL, Dims.PATH_BASED, Dims.CDC)\n          )\n        ),\n        TestConfig(\n          List(\"RowTrackingDeleteSuiteBase\", \"RowTrackingDeleteDvBase\"),\n          List(\n            List(Dims.CDC.asOptional, Dims.PERSISTENT_DV),\n            List(Dims.PERSISTENT_DV_OFF, Dims.COLUMN_MAPPING),\n            List(Dims.CDC, Dims.PERSISTENT_DV_ON, Dims.COLUMN_MAPPING)\n          )\n        )\n      )\n    ),\n    TestGroup(\n      name = \"InsertSuites\",\n      imports = List(\n        importer\"org.apache.spark.sql.delta._\"\n      ),\n      testConfigs = List(\n        TestConfig(\n          List(\"DeltaInsertIntoImplicitCastTests\", \"DeltaInsertIntoImplicitCastStreamingWriteTests\"),\n          List(\n            List()\n          )\n        )\n      )\n    )\n    // scalastyle:on line.size.limit\n  )\n\n  /**\n   * Decides if a suite with the given base test and mixins should be generated or not. This is used\n   * to exclude certain combinations of base suites and dimensions that are known to not work\n   * together, or it can also be used to enforce presence of some dimensions for a certain base\n   * suite.\n   */\n  def isExcluded(base: String, mixins: List[String]): Boolean = {\n    base match {\n      // Exclude tempViews, because DeltaTable.forName does not resolve them correctly, so no one\n      // can use them anyway with the Scala API.\n      case \"MergeIntoTempViewsTests\" => mixins.contains(Dims.MERGE_SCALA.traitName)\n      case \"UpdateBaseTempViewTests\" => mixins.contains(Dims.UPDATE_SCALA.traitName)\n      case \"DeleteTempViewTests\" => mixins.contains(Dims.DELETE_SCALA.traitName)\n      // The following tests only make sense if the dimension is present\n      case \"MergeCDCTests\" | \"UpdateCDCTests\" | \"DeleteCDCTests\" =>\n        !mixins.contains(Dims.CDC.traitName)\n      case \"MergeIntoDVsTests\" => !mixins.contains(Dims.MERGE_DVS.traitName)\n      case \"UpdateSQLWithDeletionVectorsTests\" =>\n        !mixins.contains(Dims.UPDATE_DVS.traitName)\n      case \"UpdateCDCWithDeletionVectorsTests\" =>\n        !List(Dims.UPDATE_DVS, Dims.CDC).map(_.traitName).forall(mixins.contains)\n      case \"RowTrackingDeleteDvBase\" => !mixins.contains(Dims.PERSISTENT_DV_ON.traitName)\n      case _ => false\n    }\n  }\n\n  /**\n   * Used to add custom traits to some combinations of base suites and dimensions.\n   * @return all traits that needs to be extended for this test combination (incl. provided mixins).\n   */\n  def applyCustomRulesAndGetAllMixins(base: String, mixins: List[String]): List[String] = {\n    var finalMixins = new ListBuffer[String]\n    finalMixins ++= mixins\n\n    if (mixins.contains(Dims.MERGE_SQL.traitName)) {\n      if (Dims.COLUMN_MAPPING.traitNames.exists(mixins.contains)) {\n        finalMixins += \"MergeIntoSQLColumnMappingOverrides\"\n      }\n\n      if (mixins.contains(Dims.CDC.traitName)) {\n        finalMixins += \"MergeCDCMixin\"\n        if (mixins.contains(Dims.MERGE_DVS.traitName)) {\n          finalMixins += \"MergeCDCWithDVsMixin\"\n        }\n      }\n    }\n\n    if (mixins.contains(Dims.UPDATE_SQL.traitName)) {\n      if (mixins.contains(Dims.ROW_TRACKING.traitNames.last)) {\n        finalMixins += \"UpdateWithRowTrackingOverrides\"\n      }\n    }\n\n    if (mixins.contains(Dims.DELETE_SQL.traitName)) {\n      if (mixins.contains(Dims.CDC.traitName)) {\n        finalMixins += \"DeleteCDCMixin\"\n      }\n      if (mixins.contains(Dims.COLUMN_MAPPING.traitNames.last)) {\n        finalMixins += \"DeleteSQLNameColumnMappingMixin\"\n      }\n    }\n\n    finalMixins.result()\n  }\n}\n"
  },
  {
    "path": "spark/delta-suite-generator/src/main/scala/io/delta/suitegenerator/SuitesWriter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.suitegenerator\n\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Path}\n\nimport scala.collection.mutable.ListBuffer\nimport scala.io.Source\nimport scala.jdk.CollectionConverters._\nimport scala.meta._\n\nimport org.scalafmt.Scalafmt\n\n/**\n * Contains the constants for the SuitesWriter class\n */\nobject SuitesWriter {\n  private val LEGAL_HEADER =\n    \"\"\" Copyright (2021) The Delta Lake Project Authors.\n      |\n      | Licensed under the Apache License, Version 2.0 (the \"License\");\n      | you may not use this file except in compliance with the License.\n      | You may obtain a copy of the License at\n      |\n      | http://www.apache.org/licenses/LICENSE-2.0\n      |\n      | Unless required by applicable law or agreed to in writing, software\n      | distributed under the License is distributed on an \"AS IS\" BASIS,\n      | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n      | See the License for the specific language governing permissions and\n      | limitations under the License.\"\"\".stripMargin\n\n  private val WARNING_HEADER =\n    \"\"\" ***********************************************************************************\n      | * This file is automatically generated. Manual modification is not allowed.       *\n      | * There is a unit test that should prevent merging a manual change.               *\n      | *                                                                                 *\n      | * To make changes to the suites, modify the generator script config at            *\n      | * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n      | * sbt command deltaSuiteGenerator / run.                                          *\n      | *                                                                                 *\n      | * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n      | ***********************************************************************************\"\"\"\n      .stripMargin\n\n  private lazy val PACKAGE_NAME =\n    ModularSuiteGenerator.GENERATED_PACKAGE.parse[Term].get.asInstanceOf[Term.Ref]\n\n  private lazy val SRC_HEADERS =\n    s\"\"\"/*\n       |${LEGAL_HEADER.linesWithSeparators.map(\" *\" + _).mkString}\n       | */\n       |\n       |${WARNING_HEADER.linesWithSeparators.map(\"//\" + _).mkString}\n       |\n       |\"\"\".stripMargin\n\n  private lazy val SCALAFMT_CONFIG = {\n    val source = Source.fromURL(getClass.getClassLoader.getResource(\"scalafmt.conf\"))\n    try {\n      Scalafmt.parseHoconConfig(source.mkString).get\n    } finally {\n      source.close()\n    }\n  }\n}\n\n/**\n * Used to write the generated suites. The output is written to the given directory. This directory\n * should not contain any manually written files, otherwise the generator will throw an error.\n * @param outputDir the path to the directory where the generated suites will be written.\n */\nclass SuitesWriter(val outputDir: Path) {\n\n  import SuitesWriter._\n\n  protected val allFiles: ListBuffer[Path] = ListBuffer.empty[Path]\n\n  def writeGeneratedSuitesOfGroup(suites: List[TestSuite], testGroup: TestGroup): Unit = {\n    suites\n      // Group by parent class: first item of the extends clause (last item of class def)\n      .groupBy(suite => suite.classDefinition.children.last.children.head.text)\n      .foreach { case (baseSuite, suites) =>\n        val src = SRC_HEADERS + \"// scalastyle:off line.size.limit\\n\" +\n          source\"\"\"package $PACKAGE_NAME\n                   import ..${testGroup.imports}\n                   ..${suites.sortBy(_.name).map(_.classDefinition)}\"\"\"\n        val srcFile = outputDir.resolve(s\"${testGroup.name}$baseSuite.scala\")\n        val formattedSrc = Scalafmt.format(src, SCALAFMT_CONFIG).get\n        writeFile(srcFile, formattedSrc)\n      }\n    // scalastyle:off println\n    println(s\"Wrote ${suites.size} generated suites from ${testGroup.name} group.\")\n    // scalastyle:on println\n  }\n\n  protected def writeFile(file: Path, content: String): Unit = {\n    Files.write(file, content.getBytes(StandardCharsets.UTF_8))\n    allFiles += file\n  }\n\n  def conclude(): Unit = {\n    val additionalFiles = Files.list(outputDir)\n      .iterator()\n      .asScala\n      .filterNot(allFiles.contains)\n      .map(_.getFileName)\n      .mkString(\", \")\n    assert(additionalFiles.isEmpty, s\"Unexpected files found in $outputDir: $additionalFiles. \" +\n      s\"Please manually delete them.\")\n  }\n}\n"
  },
  {
    "path": "spark/delta-suite-generator/src/test/scala/io/delta/suitegenerator/ValidateGeneratedSuites.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.suitegenerator\n\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Path, Paths}\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ValidateGeneratedSuites extends AnyFunSuite {\n  test(\"Generated suites are not manually modified\") {\n    // This test must be executed from the repository root for this relative path to work\n    val outputDir = Paths.get(ModularSuiteGenerator.OUTPUT_PATH)\n    val suitesValidator = new SuitesValidator(outputDir)\n    ModularSuiteGenerator.generateSuites(suitesValidator)\n  }\n}\n\n/**\n * Instead of writing to the files, validates that the files match the expected content.\n */\nclass SuitesValidator(override val outputDir: Path) extends SuitesWriter(outputDir) {\n  override def writeFile(file: Path, content: String): Unit = {\n    assert(Files.exists(file), s\"File $file does not exist. Please run the generator to create it.\")\n    val fileContent = new String(Files.readAllBytes(file), StandardCharsets.UTF_8)\n    assert(fileContent == content,\n      s\"File $file does not match the expected content. Please run the generator to update it.\")\n    allFiles += file\n  }\n}\n"
  },
  {
    "path": "spark/src/main/antlr4/io/delta/sql/parser/DeltaSqlBase.g4",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\ngrammar DeltaSqlBase;\n\n@members {\n  /**\n   * Verify whether current token is a valid decimal token (which contains dot).\n   * Returns true if the character that follows the token is not a digit or letter or underscore.\n   *\n   * For example:\n   * For char stream \"2.3\", \"2.\" is not a valid decimal token, because it is followed by digit '3'.\n   * For char stream \"2.3_\", \"2.3\" is not a valid decimal token, because it is followed by '_'.\n   * For char stream \"2.3W\", \"2.3\" is not a valid decimal token, because it is followed by 'W'.\n   * For char stream \"12.0D 34.E2+0.12 \"  12.0D is a valid decimal token because it is folllowed\n   * by a space. 34.E2 is a valid decimal token because it is followed by symbol '+'\n   * which is not a digit or letter or underscore.\n   */\n  public boolean isValidDecimal() {\n    int nextChar = _input.LA(1);\n    if (nextChar >= 'A' && nextChar <= 'Z' || nextChar >= '0' && nextChar <= '9' ||\n      nextChar == '_') {\n      return false;\n    } else {\n      return true;\n    }\n  }\n}\n\ntokens {\n    DELIMITER\n}\n\nsingleStatement\n    : statement ';'* EOF\n    ;\n\n// If you add keywords here that should not be reserved, add them to 'nonReserved' list.\nstatement\n    : VACUUM (path=stringLit | table=qualifiedName)\n        vacuumModifiers                                                 #vacuumTable\n    | (DESC | DESCRIBE) DETAIL (path=stringLit | table=qualifiedName)   #describeDeltaDetail\n    | GENERATE modeName=identifier FOR TABLE table=qualifiedName        #generate\n    | (DESC | DESCRIBE) HISTORY (path=stringLit | table=qualifiedName)\n        (LIMIT limit=INTEGER_VALUE)?                                    #describeDeltaHistory\n    | CONVERT TO DELTA table=qualifiedName\n        (NO STATISTICS)? (PARTITIONED BY '(' colTypeList ')')?          #convert\n    | RESTORE TABLE? table=qualifiedName TO?\n            clause=temporalClause                                       #restore\n    | ALTER TABLE table=qualifiedName ADD CONSTRAINT name=identifier\n      constraint                                                        #addTableConstraint\n    | ALTER TABLE table=qualifiedName\n        DROP CONSTRAINT (IF EXISTS)? name=identifier                    #dropTableConstraint\n    | ALTER TABLE table=qualifiedName\n        DROP FEATURE featureName=featureNameValue (TRUNCATE HISTORY)?   #alterTableDropFeature\n    | ALTER TABLE table=qualifiedName\n        (clusterBySpec | CLUSTER BY NONE)                               #alterTableClusterBy\n    | ALTER TABLE table=qualifiedName\n        (ALTER | CHANGE) COLUMN? column=qualifiedName SYNC IDENTITY     #alterTableSyncIdentity\n    | OPTIMIZE (path=stringLit | table=qualifiedName) FULL?\n        (WHERE partitionPredicate=predicateToken)?\n        (zorderSpec)?                                                   #optimizeTable\n    | REORG TABLE table=qualifiedName\n        (\n            (WHERE partitionPredicate=predicateToken)? APPLY LEFT_PAREN PURGE RIGHT_PAREN |\n            APPLY LEFT_PAREN UPGRADE UNIFORM LEFT_PAREN ICEBERG_COMPAT_VERSION EQ version=INTEGER_VALUE RIGHT_PAREN RIGHT_PAREN\n        )                                                               #reorgTable\n    | cloneTableHeader SHALLOW CLONE source=qualifiedName clause=temporalClause?\n       (TBLPROPERTIES tableProps=propertyList)?\n       (LOCATION location=stringLit)?                                   #clone\n    | .*? clusterBySpec+ .*?                                            #clusterBy\n    | .*?                                                               #passThrough\n    ;\n\ncreateTableHeader\n    : CREATE TABLE (IF NOT EXISTS)? table=qualifiedName\n    ;\n\nreplaceTableHeader\n    : (CREATE OR)? REPLACE TABLE table=qualifiedName\n    ;\n\ncloneTableHeader\n    : createTableHeader\n    | replaceTableHeader\n    ;\n\nzorderSpec\n    : ZORDER BY LEFT_PAREN interleave+=qualifiedName (COMMA interleave+=qualifiedName)* RIGHT_PAREN\n    | ZORDER BY interleave+=qualifiedName (COMMA interleave+=qualifiedName)*\n    ;\n\nclusterBySpec\n    : CLUSTER BY LEFT_PAREN interleave+=qualifiedName (COMMA interleave+=qualifiedName)* RIGHT_PAREN\n    ;\n\ntemporalClause\n    : FOR? (SYSTEM_VERSION | VERSION) AS OF version=(INTEGER_VALUE | STRING)\n    | FOR? (SYSTEM_TIME | TIMESTAMP) AS OF timestamp=STRING\n    ;\n\nqualifiedName\n    : identifier ('.' identifier)* ('.' identifier)*\n    ;\n\npropertyList\n    : LEFT_PAREN property (COMMA property)* RIGHT_PAREN\n    ;\n\nproperty\n    : key=propertyKey (EQ? value=propertyValue)?\n    ;\n\npropertyKey\n    : identifier (DOT identifier)*\n    | stringLit\n    ;\n\npropertyValue\n    : INTEGER_VALUE\n    | DECIMAL_VALUE\n    | booleanValue\n    | identifier LEFT_PAREN stringLit COMMA stringLit RIGHT_PAREN\n    | value=stringLit\n    ;\n\nfeatureNameValue\n    : identifier\n    | stringLit\n    ;\n\nsingleStringLit\n    : STRING\n    | DOUBLEQUOTED_STRING\n    ;\n\nstringLit\n    : singleStringLit+\n    ;\n\nbooleanValue\n    : TRUE | FALSE\n    ;\n\nidentifier\n    : IDENTIFIER             #unquotedIdentifier\n    | quotedIdentifier       #quotedIdentifierAlternative\n    | nonReserved            #unquotedIdentifier\n    ;\n\nquotedIdentifier\n    : BACKQUOTED_IDENTIFIER\n    ;\n\ncolTypeList\n    : colType (',' colType)*\n    ;\n\ncolType\n    : colName=identifier dataType (NOT NULL)? (COMMENT comment=stringLit)?\n    ;\n\ndataType\n    : identifier ('(' INTEGER_VALUE (',' INTEGER_VALUE)* ')')?         #primitiveDataType\n    ;\n\nvacuumModifiers\n    : (vacuumType\n    | inventory\n    | retain\n    | dryRun)*\n    ;\n\nvacuumType\n    : LITE|FULL\n    ;\n\ninventory\n    : USING INVENTORY (inventoryTable=qualifiedName | LEFT_PAREN inventoryQuery=subQuery RIGHT_PAREN)\n    ;\n\nretain\n    : RETAIN number HOURS\n    ;\n\ndryRun\n    : DRY RUN\n    ;\n\nnumber\n    : MINUS? DECIMAL_VALUE            #decimalLiteral\n    | MINUS? INTEGER_VALUE            #integerLiteral\n    | MINUS? BIGINT_LITERAL           #bigIntLiteral\n    | MINUS? SMALLINT_LITERAL         #smallIntLiteral\n    | MINUS? TINYINT_LITERAL          #tinyIntLiteral\n    | MINUS? DOUBLE_LITERAL           #doubleLiteral\n    | MINUS? BIGDECIMAL_LITERAL       #bigDecimalLiteral\n    ;\n\nconstraint\n    : CHECK '(' exprToken+ ')'                                 #checkConstraint\n    ;\n\n// We don't have an expression rule in our grammar here, so we just grab the tokens and defer\n// parsing them to later. Although this is the same as `exprToken`, we have to re-define it to\n// workaround an ANTLR issue (https://github.com/delta-io/delta/issues/1205)\npredicateToken\n    :  .+?\n    ;\n\n// We don't have an expression rule in our grammar here, so we just grab the tokens and defer\n// parsing them to later. Although this is the same as `exprToken`, `predicateToken`, we have to re-define it to\n// workaround an ANTLR issue (https://github.com/delta-io/delta/issues/1205). Should we remove this after\n// https://github.com/delta-io/delta/pull/1800\nsubQuery\n    :  .+?\n    ;\n\n// We don't have an expression rule in our grammar here, so we just grab the tokens and defer\n// parsing them to later.\nexprToken\n    :  .+?\n    ;\n\n// Add keywords here so that people's queries don't break if they have a column name as one of\n// these tokens\nnonReserved\n    : VACUUM | FULL | LITE | USING | INVENTORY | RETAIN | HOURS | DRY | RUN\n    | CONVERT | TO | DELTA | PARTITIONED | BY\n    | DESC | DESCRIBE | LIMIT | DETAIL\n    | GENERATE | FOR | TABLE | CHECK | EXISTS | OPTIMIZE | FULL\n    | IDENTITY | SYNC | COLUMN | CHANGE\n    | REORG | APPLY | PURGE | UPGRADE | UNIFORM | ICEBERG_COMPAT_VERSION\n    | RESTORE | AS | OF\n    | ZORDER | LEFT_PAREN | RIGHT_PAREN\n    | NO | STATISTICS\n    | CLONE | SHALLOW\n    | FEATURE | TRUNCATE\n    | CLUSTER | NONE\n    ;\n\n// Define how the keywords above should appear in a user's SQL statement.\nADD: 'ADD';\nALTER: 'ALTER';\nAPPLY: 'APPLY';\nAS: 'AS';\nBY: 'BY';\nCHANGE: 'CHANGE';\nCHECK: 'CHECK';\nCLONE: 'CLONE';\nCLUSTER: 'CLUSTER';\nCOLUMN: 'COLUMN';\nCOMMA: ',';\nCOMMENT: 'COMMENT';\nCONSTRAINT: 'CONSTRAINT';\nCONVERT: 'CONVERT';\nCREATE: 'CREATE';\nDELTA: 'DELTA';\nDESC: 'DESC';\nDESCRIBE: 'DESCRIBE';\nDETAIL: 'DETAIL';\nDOT: '.';\nDROP: 'DROP';\nDRY: 'DRY';\nEXISTS: 'EXISTS';\nFALSE: 'FALSE';\nFEATURE: 'FEATURE';\nFOR: 'FOR';\nFULL: 'FULL';\nGENERATE: 'GENERATE';\nHISTORY: 'HISTORY';\nHOURS: 'HOURS';\nICEBERG_COMPAT_VERSION: 'ICEBERG_COMPAT_VERSION';\nIDENTITY: 'IDENTITY';\nIF: 'IF';\nINVENTORY: 'INVENTORY';\nLEFT_PAREN: '(';\nLIMIT: 'LIMIT';\nLITE: 'LITE';\nLOCATION: 'LOCATION';\nMINUS: '-';\nNO: 'NO';\nNONE: 'NONE';\nNOT: 'NOT' | '!';\nNULL: 'NULL';\nOF: 'OF';\nOR: 'OR';\nOPTIMIZE: 'OPTIMIZE';\nREORG: 'REORG';\nPARTITIONED: 'PARTITIONED';\nPURGE: 'PURGE';\nREPLACE: 'REPLACE';\nRESTORE: 'RESTORE';\nRETAIN: 'RETAIN';\nRIGHT_PAREN: ')';\nRUN: 'RUN';\nSHALLOW: 'SHALLOW';\nSYNC: 'SYNC';\nSYSTEM_TIME: 'SYSTEM_TIME';\nSYSTEM_VERSION: 'SYSTEM_VERSION';\nTABLE: 'TABLE';\nTBLPROPERTIES: 'TBLPROPERTIES';\nTIMESTAMP: 'TIMESTAMP';\nTRUNCATE: 'TRUNCATE';\nTO: 'TO';\nTRUE: 'TRUE';\nUNIFORM: 'UNIFORM';\nUPGRADE: 'UPGRADE';\nUSING: 'USING';\nVACUUM: 'VACUUM';\nVERSION: 'VERSION';\nWHERE: 'WHERE';\nZORDER: 'ZORDER';\nSTATISTICS: 'STATISTICS';\n\n// Multi-character operator tokens need to be defined even though we don't explicitly reference\n// them so that they can be recognized as single tokens when parsing. If we split them up and\n// end up with expression text like 'a ! = b', Spark won't be able to parse '! =' back into the\n// != operator.\nEQ  : '=' | '==';\nNSEQ: '<=>';\nNEQ : '<>';\nNEQJ: '!=';\nLTE : '<=' | '!>';\nGTE : '>=' | '!<';\nCONCAT_PIPE: '||';\n\nSTRING\n    : '\\'' ( ~('\\''|'\\\\') | ('\\\\' .) )* '\\''\n    | '\"' ( ~('\"'|'\\\\') | ('\\\\' .) )* '\"'\n    ;\n\nDOUBLEQUOTED_STRING\n    :'\"' ( ~('\"'|'\\\\') | ('\\\\' .) )* '\"'\n    ;\n\nBIGINT_LITERAL\n    : DIGIT+ 'L'\n    ;\n\nSMALLINT_LITERAL\n    : DIGIT+ 'S'\n    ;\n\nTINYINT_LITERAL\n    : DIGIT+ 'Y'\n    ;\n\nINTEGER_VALUE\n    : DIGIT+\n    ;\n\nDECIMAL_VALUE\n    : DIGIT+ EXPONENT\n    | DECIMAL_DIGITS EXPONENT? {isValidDecimal()}?\n    ;\n\nDOUBLE_LITERAL\n    : DIGIT+ EXPONENT? 'D'\n    | DECIMAL_DIGITS EXPONENT? 'D' {isValidDecimal()}?\n    ;\n\nBIGDECIMAL_LITERAL\n    : DIGIT+ EXPONENT? 'BD'\n    | DECIMAL_DIGITS EXPONENT? 'BD' {isValidDecimal()}?\n    ;\n\nIDENTIFIER\n    : (LETTER | DIGIT | '_')+\n    ;\n\nBACKQUOTED_IDENTIFIER\n    : '`' ( ~'`' | '``' )* '`'\n    ;\n\nfragment DECIMAL_DIGITS\n    : DIGIT+ '.' DIGIT*\n    | '.' DIGIT+\n    ;\n\nfragment EXPONENT\n    : 'E' [+-]? DIGIT+\n    ;\n\nfragment DIGIT\n    : [0-9]\n    ;\n\nfragment LETTER\n    : [A-Z]\n    ;\n\nSIMPLE_COMMENT\n    : '--' ~[\\r\\n]* '\\r'? '\\n'? -> channel(HIDDEN)\n    ;\n\nBRACKETED_COMMENT\n    : '/*' .*? '*/' -> channel(HIDDEN)\n    ;\n\nWS  : [ \\r\\n\\t]+ -> channel(HIDDEN)\n    ;\n\n// Catch-all for anything we can't recognize.\n// We use this to be able to ignore and recover all the text\n// when splitting statements with DelimiterLexer\nUNRECOGNIZED\n    : .\n    ;\n"
  },
  {
    "path": "spark/src/main/java/io/delta/dynamodbcommitcoordinator/DynamoDBCommitCoordinatorClient.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.dynamodbcommitcoordinator;\n\nimport com.amazonaws.services.dynamodbv2.AmazonDynamoDB;\nimport com.amazonaws.services.dynamodbv2.model.*;\nimport io.delta.storage.CloseableIterator;\nimport io.delta.storage.LogStore;\nimport io.delta.storage.commit.*;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.*;\nimport java.util.*;\n\n/**\n * A commit coordinator client that uses DynamoDB as the commit coordinator. The table schema is as follows:\n * tableId: String --- The unique identifier for the table. This is a UUID.\n * path: String --- The fully qualified path of the table in the file system. e.g. s3://bucket/path.\n * acceptingCommits: Boolean --- Whether the commit coordinator is accepting new commits. This will only\n *  be set to false when the table is converted from coordinated commits to file system commits.\n * tableVersion: Number --- The version of the latest commit.\n * tableTimestamp: Number --- The inCommitTimestamp of the latest commit.\n * schemaVersion: Number --- The version of the schema used to store the data.\n * hasAcceptedCommits: Boolean --- Whether any actual commits have been accepted by this commit coordinator\n *  after `registerTable`.\n * commits: --- The list of unbackfilled commits.\n *  version: Number --- The version of the commit.\n *  inCommitTimestamp: Number --- The inCommitTimestamp of the commit.\n *  fsName: String --- The name of the unbackfilled file.\n *  fsLength: Number --- The length of the unbackfilled file.\n *  fsTimestamp: Number --- The modification time of the unbackfilled file.\n */\npublic class DynamoDBCommitCoordinatorClient implements CommitCoordinatorClient {\n    private static final Logger LOG = LoggerFactory.getLogger(DynamoDBCommitCoordinatorClient.class);\n\n    /**\n     * The name of the DynamoDB table used to store unbackfilled commits.\n     */\n    final String coordinatedCommitsTableName;\n\n    /**\n     * The DynamoDB client used to interact with the DynamoDB table.\n     */\n    final AmazonDynamoDB client;\n\n    /**\n     * The endpoint of the DynamoDB table.\n     */\n    final String endpoint;\n\n    /**\n     * The number of write capacity units to provision for the DynamoDB table if the\n     * client ends up creating a new one.\n     */\n    final long writeCapacityUnits;\n\n    /**\n     * The number of read capacity units to provision for the DynamoDB table if the\n     * client ends up creating a new one.\n     */\n    final long readCapacityUnits;\n\n    /**\n     * The number of commits to batch backfill at once. A backfill is performed\n     * whenever commitVersion % batchSize == 0.\n     */\n    public final long backfillBatchSize;\n\n    /**\n     * Whether we should skip matching the current table path against the one stored in DynamoDB\n     * when interacting with it.\n     */\n    final boolean skipPathCheck;\n\n    /**\n     * The key used to store the tableId in the coordinated commits table configuration.\n     */\n    final static String TABLE_CONF_TABLE_ID_KEY = \"tableId\";\n\n    /**\n     * The version of the client. This is used to ensure that the client is compatible with the\n     * schema of the data stored in the DynamoDB table. A client should only be able to\n     * access a table if the schema version of the table matches the client version.\n     */\n    final int CLIENT_VERSION = 1;\n\n    private static class GetCommitsResultInternal {\n        final GetCommitsResponse response;\n        final boolean hasAcceptedCommits;\n        GetCommitsResultInternal(\n                GetCommitsResponse response,\n                boolean hasAcceptedCommits) {\n            this.response = response;\n            this.hasAcceptedCommits = hasAcceptedCommits;\n        }\n    }\n\n\n    public DynamoDBCommitCoordinatorClient(\n            String coordinatedCommitsTableName,\n            String endpoint,\n            AmazonDynamoDB client,\n            long backfillBatchSize) throws IOException {\n        this(\n            coordinatedCommitsTableName,\n            endpoint,\n            client,\n            backfillBatchSize,\n            5 /* readCapacityUnits */,\n            5 /* writeCapacityUnits */,\n            false /* skipPathCheck */);\n    }\n\n    public DynamoDBCommitCoordinatorClient(\n            String coordinatedCommitsTableName,\n            String endpoint,\n            AmazonDynamoDB client,\n            long backfillBatchSize,\n            long readCapacityUnits,\n            long writeCapacityUnits,\n            boolean skipPathCheck) throws IOException {\n        this.coordinatedCommitsTableName = coordinatedCommitsTableName;\n        this.endpoint = endpoint;\n        this.client = client;\n        this.backfillBatchSize = backfillBatchSize;\n        this.readCapacityUnits = readCapacityUnits;\n        this.writeCapacityUnits = writeCapacityUnits;\n        this.skipPathCheck = skipPathCheck;\n        tryEnsureTableExists();\n    }\n\n    private String getTableId(Map<String, String> coordinatedCommitsTableConf) {\n        if (!coordinatedCommitsTableConf.containsKey(TABLE_CONF_TABLE_ID_KEY)) {\n            throw new RuntimeException(\"tableId not found\");\n        }\n        return coordinatedCommitsTableConf.get(TABLE_CONF_TABLE_ID_KEY);\n    }\n\n    /**\n     * Fetches the entry from the commit coordinator for the given table. Only the attributes defined\n     * in attributesToGet will be fetched.\n     */\n    private GetItemResult getEntryFromCommitCoordinator(\n            Map<String, String> coordinatedCommitsTableConf, String... attributesToGet) {\n        GetItemRequest request = new GetItemRequest()\n                .withTableName(coordinatedCommitsTableName)\n                .addKeyEntry(\n                        DynamoDBTableEntryConstants.TABLE_ID,\n                        new AttributeValue().withS(getTableId(coordinatedCommitsTableConf)))\n                .withAttributesToGet(attributesToGet);\n        return client.getItem(request);\n    }\n\n    /**\n     * Commits the given file to the commit coordinator.\n     * A conditional write is performed to the DynamoDB table entry associated with this Delta\n     * table.\n     * If the conditional write goes through, the filestatus of the UUID delta file will be\n     * appended to the list of unbackfilled commits, and other updates like setting the latest\n     * table version to `attemptVersion` will be performed.\n     *\n     * For the conditional write to go through, the following conditions must be met right before\n     * the write is performed:\n     * 1. The latest table version in DynamoDB is equal to attemptVersion - 1.\n     * 2. The commit coordinator is accepting new commits.\n     * 3. The schema version of the commit coordinator matches the schema version of the client.\n     * 4. The table path stored in DynamoDB matches the path of the table. This check is skipped\n     * if `skipPathCheck` is set to true.\n     * If the conditional write fails, we retrieve the current entry in DynamoDB to figure out\n     * which condition failed. (DynamoDB does not tell us which condition failed in the rejection.)\n     * If any of (2), (3), or (4) fail, an unretryable `CommitFailedException` will be thrown.\n     * For (1):\n     * If the retrieved latest table version is greater than or equal to attemptVersion, a retryable\n     * `CommitFailedException` will be thrown.\n     * If the retrieved latest table version is less than attemptVersion - 1, an unretryable\n     * `CommitFailedException` will be thrown.\n     */\n    protected CommitResponse commitToCoordinator(\n            Path logPath,\n            Map<String, String> coordinatedCommitsTableConf,\n            long attemptVersion,\n            FileStatus commitFile,\n            long inCommitTimestamp,\n            boolean isCCtoFSConversion) throws CommitFailedException {\n        // Add conditions for the conditional update.\n        java.util.Map<String, ExpectedAttributeValue> expectedValuesBeforeUpdate = new HashMap<>();\n        expectedValuesBeforeUpdate.put(\n                DynamoDBTableEntryConstants.TABLE_LATEST_VERSION,\n                new ExpectedAttributeValue()\n                        .withValue(new AttributeValue().withN(Long.toString(attemptVersion - 1)))\n        );\n        expectedValuesBeforeUpdate.put(\n                DynamoDBTableEntryConstants.ACCEPTING_COMMITS,\n                new ExpectedAttributeValue()\n                    .withValue(new AttributeValue().withBOOL(true)));\n        if (!skipPathCheck) {\n            expectedValuesBeforeUpdate.put(\n                    DynamoDBTableEntryConstants.TABLE_PATH,\n                    new ExpectedAttributeValue()\n                        .withValue(new AttributeValue().withS(logPath.getParent().toString())));\n        }\n        expectedValuesBeforeUpdate.put(\n                DynamoDBTableEntryConstants.SCHEMA_VERSION,\n                new ExpectedAttributeValue()\n                    .withValue(new AttributeValue().withN(Integer.toString(CLIENT_VERSION))));\n\n        java.util.Map<String, AttributeValue> newCommit = new HashMap<>();\n        newCommit.put(\n                DynamoDBTableEntryConstants.COMMIT_VERSION,\n                new AttributeValue().withN(Long.toString(attemptVersion)));\n        newCommit.put(\n                DynamoDBTableEntryConstants.COMMIT_TIMESTAMP,\n                new AttributeValue().withN(Long.toString(inCommitTimestamp)));\n        newCommit.put(\n                DynamoDBTableEntryConstants.COMMIT_FILE_NAME,\n                new AttributeValue().withS(commitFile.getPath().getName()));\n        newCommit.put(\n                DynamoDBTableEntryConstants.COMMIT_FILE_LENGTH,\n                new AttributeValue().withN(Long.toString(commitFile.getLen())));\n        newCommit.put(\n                DynamoDBTableEntryConstants.COMMIT_FILE_MODIFICATION_TIMESTAMP,\n                new AttributeValue().withN(Long.toString(commitFile.getModificationTime())));\n\n        UpdateItemRequest request = new UpdateItemRequest()\n                .withTableName(coordinatedCommitsTableName)\n                .addKeyEntry(\n                        DynamoDBTableEntryConstants.TABLE_ID,\n                        new AttributeValue().withS(getTableId(coordinatedCommitsTableConf)))\n                .addAttributeUpdatesEntry(\n                        DynamoDBTableEntryConstants.TABLE_LATEST_VERSION, new AttributeValueUpdate()\n                            .withValue(new AttributeValue().withN(Long.toString(attemptVersion)))\n                            .withAction(AttributeAction.PUT))\n                // We need to set this to true to indicate that commits have been accepted after\n                // `registerTable`.\n                .addAttributeUpdatesEntry(\n                        DynamoDBTableEntryConstants.HAS_ACCEPTED_COMMITS, new AttributeValueUpdate()\n                            .withValue(new AttributeValue().withBOOL(true))\n                            .withAction(AttributeAction.PUT)\n                )\n                .addAttributeUpdatesEntry(\n                        DynamoDBTableEntryConstants.TABLE_LATEST_TIMESTAMP, new AttributeValueUpdate()\n                            .withValue(new AttributeValue().withN(Long.toString(inCommitTimestamp)))\n                            .withAction(AttributeAction.PUT))\n                .addAttributeUpdatesEntry(\n                        DynamoDBTableEntryConstants.COMMITS,\n                        new AttributeValueUpdate()\n                            .withAction(AttributeAction.ADD)\n                            .withValue(new AttributeValue().withL(\n                                    new AttributeValue().withM(newCommit)\n                                )\n                            )\n                )\n                .withExpected(expectedValuesBeforeUpdate);\n\n        if (isCCtoFSConversion) {\n            // If this table is being converted from coordinated commits to file system commits, we need\n            // to set acceptingCommits to false.\n            request = request\n                    .addAttributeUpdatesEntry(\n                            DynamoDBTableEntryConstants.ACCEPTING_COMMITS,\n                            new AttributeValueUpdate()\n                                    .withValue(new AttributeValue().withBOOL(false))\n                                    .withAction(AttributeAction.PUT)\n                    );\n        }\n\n        try {\n            client.updateItem(request);\n        } catch (ConditionalCheckFailedException e) {\n            // Conditional check failed. The exception will not indicate which condition failed.\n            // We need to check the conditions ourselves by fetching the item and checking the\n            // values.\n            GetItemResult latestEntry = getEntryFromCommitCoordinator(\n                    coordinatedCommitsTableConf,\n                    DynamoDBTableEntryConstants.TABLE_LATEST_VERSION,\n                    DynamoDBTableEntryConstants.ACCEPTING_COMMITS,\n                    DynamoDBTableEntryConstants.TABLE_PATH,\n                    DynamoDBTableEntryConstants.SCHEMA_VERSION);\n\n            int schemaVersion = Integer.parseInt(\n                    latestEntry.getItem().get(DynamoDBTableEntryConstants.SCHEMA_VERSION).getN());\n            if (schemaVersion != CLIENT_VERSION) {\n                throw new CommitFailedException(\n                        false /* retryable */,\n                        false /* conflict */,\n                        \"The schema version of the commit coordinator does not match the current\" +\n                                \"DynamoDBCommitCoordinatorClient version. The data schema version is \" +\n                                \" \" + schemaVersion + \" while the client version is \" +\n                                CLIENT_VERSION + \". Make sure that the correct client is being \" +\n                                \"used to access this table.\" );\n            }\n            long latestTableVersion = Long.parseLong(\n                    latestEntry.getItem().get(DynamoDBTableEntryConstants.TABLE_LATEST_VERSION).getN());\n            if (!skipPathCheck &&\n                    !latestEntry.getItem().get(\"path\").getS().equals(logPath.getParent().toString())) {\n                throw new CommitFailedException(\n                        false /* retryable */,\n                        false /* conflict */,\n                        \"This commit was attempted from path \" + logPath.getParent() +\n                                \" while the table is registered at \" +\n                                latestEntry.getItem().get(\"path\").getS() + \".\");\n            }\n            if (!latestEntry.getItem().get(DynamoDBTableEntryConstants.ACCEPTING_COMMITS).getBOOL()) {\n                throw new CommitFailedException(\n                        false /* retryable */,\n                        false /* conflict */,\n                        \"The commit coordinator is not accepting any new commits for this table.\");\n            }\n            if (latestTableVersion != attemptVersion - 1) {\n                // The commit is only retryable if the conflict is due to someone else committing\n                // a version greater than the expected version.\n                boolean retryable = latestTableVersion > attemptVersion - 1;\n                throw new CommitFailedException(\n                        retryable /* retryable */,\n                        retryable /* conflict */,\n                        \"Commit version \" + attemptVersion + \" is not valid. Expected version: \" +\n                                (latestTableVersion + 1) + \".\");\n            }\n        }\n        Commit resultantCommit = new Commit(attemptVersion, commitFile, inCommitTimestamp);\n        return new CommitResponse(resultantCommit);\n    }\n\n    @Override\n    public CommitResponse commit(\n            LogStore logStore,\n            Configuration hadoopConf,\n            TableDescriptor tableDesc,\n            long commitVersion,\n            Iterator<String> actions,\n            UpdatedActions updatedActions) throws CommitFailedException {\n        Path logPath = tableDesc.getLogPath();\n        if (commitVersion == 0) {\n            throw new CommitFailedException(\n                    false /* retryable */,\n                    false /* conflict */,\n                    \"Commit version 0 must go via filesystem.\");\n        }\n        try {\n            FileStatus commitFileStatus = CoordinatedCommitsUtils.writeUnbackfilledCommitFile(\n                    logStore,\n                    hadoopConf,\n                    logPath.toString(),\n                    commitVersion,\n                    actions,\n                    UUID.randomUUID().toString());\n            long inCommitTimestamp = updatedActions.getCommitInfo().getCommitTimestamp();\n            boolean isCCtoFSConversion =\n                    CoordinatedCommitsUtils.isCoordinatedCommitsToFSConversion(commitVersion, updatedActions);\n\n            LOG.info(\"Committing version {} with UUID delta file {} to DynamoDB.\",\n                    commitVersion, commitFileStatus.getPath());\n            CommitResponse res = commitToCoordinator(\n                    logPath,\n                    tableDesc.getTableConf(),\n                    commitVersion,\n                    commitFileStatus,\n                    inCommitTimestamp,\n                    isCCtoFSConversion);\n\n            LOG.info(\"Commit {} was successful.\", commitVersion);\n\n            boolean shouldBackfillOnEveryCommit = backfillBatchSize <= 1;\n            boolean isBatchBackfillDue = commitVersion % backfillBatchSize == 0;\n            boolean shouldBackfill = shouldBackfillOnEveryCommit || isBatchBackfillDue ||\n                    // Always attempt a backfill for coordinated commits to filesystem conversion.\n                    // Even if this fails, the next reader will attempt to backfill.\n                    isCCtoFSConversion;\n            if (shouldBackfill) {\n                backfillToVersion(\n                    logStore,\n                    hadoopConf,\n                    tableDesc,\n                    commitVersion,\n                    null /* lastKnownBackfilledVersion */);\n            }\n            return res;\n        } catch (IOException e) {\n            throw new CommitFailedException(false /* retryable */, false /* conflict */, e.getMessage(), e);\n        }\n    }\n\n    private GetCommitsResultInternal getCommitsImpl(\n            Path logPath,\n            Map<String, String> tableConf,\n            Long startVersion,\n            Long endVersion) throws IOException {\n        GetItemResult latestEntry = getEntryFromCommitCoordinator(\n                tableConf,\n                DynamoDBTableEntryConstants.COMMITS,\n                DynamoDBTableEntryConstants.TABLE_LATEST_VERSION,\n                DynamoDBTableEntryConstants.HAS_ACCEPTED_COMMITS);\n\n        java.util.Map<String, AttributeValue> item = latestEntry.getItem();\n        long currentVersion =\n                Long.parseLong(item.get(DynamoDBTableEntryConstants.TABLE_LATEST_VERSION).getN());\n        AttributeValue allStoredCommits = item.get(DynamoDBTableEntryConstants.COMMITS);\n        ArrayList<Commit> commits = new ArrayList<>();\n        Path unbackfilledCommitsPath = CoordinatedCommitsUtils.commitDirPath(logPath);\n        for(AttributeValue attr: allStoredCommits.getL()) {\n            java.util.Map<String, AttributeValue> commitMap = attr.getM();\n            long commitVersion =\n                    Long.parseLong(commitMap.get(DynamoDBTableEntryConstants.COMMIT_VERSION).getN());\n            boolean commitInRange = (startVersion == null || commitVersion >= startVersion) &&\n                    (endVersion == null || endVersion >= commitVersion);\n            if (commitInRange) {\n                Path filePath = new Path(\n                        unbackfilledCommitsPath,\n                        commitMap.get(DynamoDBTableEntryConstants.COMMIT_FILE_NAME).getS());\n                long length =\n                        Long.parseLong(commitMap.get(DynamoDBTableEntryConstants.COMMIT_FILE_LENGTH).getN());\n                long modificationTime = Long.parseLong(\n                        commitMap.get(DynamoDBTableEntryConstants.COMMIT_FILE_MODIFICATION_TIMESTAMP).getN());\n                FileStatus fileStatus = new FileStatus(\n                        length,\n                        false /* isDir */,\n                        0 /* blockReplication */,\n                        0 /* blockSize */,\n                        modificationTime,\n                        filePath);\n                long inCommitTimestamp =\n                        Long.parseLong(commitMap.get(DynamoDBTableEntryConstants.COMMIT_TIMESTAMP).getN());\n                commits.add(new Commit(commitVersion, fileStatus, inCommitTimestamp));\n            }\n        }\n        GetCommitsResponse response = new GetCommitsResponse(\n                new ArrayList(commits), currentVersion);\n        return new GetCommitsResultInternal(\n                response,\n                item.get(DynamoDBTableEntryConstants.HAS_ACCEPTED_COMMITS).getBOOL());\n    }\n\n    @Override\n    public GetCommitsResponse getCommits(\n            TableDescriptor tableDesc,\n            Long startVersion,\n            Long endVersion) {\n        try {\n            GetCommitsResultInternal res =\n                    getCommitsImpl(tableDesc.getLogPath(), tableDesc.getTableConf(), startVersion, endVersion);\n            long latestTableVersionToReturn = res.response.getLatestTableVersion();\n            if (!res.hasAcceptedCommits) {\n                /*\n                 * If the commit coordinator has not accepted any commits after `registerTable`, we should\n                 * return -1 as the latest table version.\n                 * ┌───────────────────────────────────┬─────────────────────────────────────────────────────┬────────────────────────────────┐\n                 * │              Action               │                   Internal State                    │ Version returned on GetCommits │\n                 * ├───────────────────────────────────┼─────────────────────────────────────────────────────┼────────────────────────────────┤\n                 * │ Table is pre-registered at X      │ hasAcceptedCommits = false, latestTableVersion = X  │             -1                 │\n                 * │ Commit X+1 after pre-registration │ hasAcceptedCommits = true, latestTableVersion = X+1 │             X+1                │\n                 * └───────────────────────────────────┴─────────────────────────────────────────────────────┴────────────────────────────────┘\n                */\n                latestTableVersionToReturn = -1;\n            }\n            return new GetCommitsResponse(res.response.getCommits(), latestTableVersionToReturn);\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n    }\n\n    /**\n     * Writes the given actions to a file.\n     * logStore.write(overwrite=false) will throw a FileAlreadyExistsException if the file already\n     * exists. However, the scala LogStore interface does not declare this as part of the function\n     * signature. This method wraps the write method and declares the exception to ensure that the\n     * caller is aware of the exception.\n     */\n    private void writeActionsToBackfilledFile(\n            LogStore logStore,\n            Path logPath,\n            long version,\n            Iterator<String> actions,\n            Configuration hadoopConf,\n            boolean shouldOverwrite) throws IOException {\n        Path targetPath = CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, version);\n        logStore.write(targetPath, actions, shouldOverwrite, hadoopConf);\n    }\n\n    private void validateBackfilledFileExists(\n            Path logPath, Configuration hadoopConf, Long lastKnownBackfilledVersion) {\n        try {\n            if (lastKnownBackfilledVersion == null) {\n                return;\n            }\n            Path lastKnownBackfilledFile = CoordinatedCommitsUtils.getBackfilledDeltaFilePath(\n                logPath, lastKnownBackfilledVersion);\n            FileSystem fs = logPath.getFileSystem(hadoopConf);\n            if (!fs.exists(lastKnownBackfilledFile)) {\n                throw new IllegalArgumentException(\n                        \"Expected backfilled file at \" + lastKnownBackfilledFile + \" does not exist.\");\n            }\n        }\n        catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n    }\n\n    /**\n     * Backfills all the unbackfilled commits returned by the commit coordinator and notifies the commit\n     * owner of the backfills.\n     * The version parameter is ignored in this implementation and all the unbackfilled commits\n     * are backfilled. This method will not throw any exception if the physical backfill\n     * succeeds but the update to the commit coordinator fails.\n     * @throws IllegalArgumentException if the requested backfill version is greater than the latest\n     *  version for the table.\n     */\n    @Override\n    public void backfillToVersion(\n            LogStore logStore,\n            Configuration hadoopConf,\n            TableDescriptor tableDesc,\n            long version,\n            Long lastKnownBackfilledVersion) throws IOException {\n        LOG.info(\"Backfilling all unbackfilled commits.\");\n        Path logPath = tableDesc.getLogPath();\n        GetCommitsResponse resp;\n        try {\n            resp = getCommitsImpl(\n                    logPath,\n                    tableDesc.getTableConf(),\n                    lastKnownBackfilledVersion,\n                    null).response;\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n        validateBackfilledFileExists(logPath, hadoopConf, lastKnownBackfilledVersion);\n        if (version > resp.getLatestTableVersion()) {\n            throw new IllegalArgumentException(\n                    \"The requested backfill version \" + version + \" is greater than the latest \" +\n                            \"version \" + resp.getLatestTableVersion() + \" for the table.\");\n        }\n        // If partial writes are visible in this filesystem, we should not try to overwrite existing\n        // files. A failed overwrite can truncate the existing file.\n        boolean shouldOverwrite = !logStore.isPartialWriteVisible(\n                logPath,\n                hadoopConf);\n        for (Commit commit: resp.getCommits()) {\n            CloseableIterator<String> actions =\n                    logStore.read(commit.getFileStatus().getPath(), hadoopConf);\n            try {\n                writeActionsToBackfilledFile(\n                        logStore,\n                        logPath,\n                        commit.getVersion(),\n                        actions,\n                        hadoopConf,\n                        shouldOverwrite);\n            } catch (java.nio.file.FileAlreadyExistsException e) {\n                // Ignore the exception. This indicates that the file has already been backfilled.\n                LOG.info(\"File {} already exists. Skipping backfill for this file.\",\n                        commit.getFileStatus().getPath());\n            } finally {\n                actions.close();\n            }\n        }\n        UpdateItemRequest request = new UpdateItemRequest()\n                .withTableName(coordinatedCommitsTableName)\n                .addKeyEntry(\n                        DynamoDBTableEntryConstants.TABLE_ID,\n                        new AttributeValue().withS(getTableId(tableDesc.getTableConf())))\n                .addAttributeUpdatesEntry(\n                        DynamoDBTableEntryConstants.COMMITS,\n                        new AttributeValueUpdate()\n                            .withAction(AttributeAction.PUT)\n                            .withValue(new AttributeValue().withL())\n                )\n                .withExpected(new HashMap<String, ExpectedAttributeValue>(){\n                    {\n                        put(DynamoDBTableEntryConstants.TABLE_LATEST_VERSION, new ExpectedAttributeValue()\n                                .withValue(\n                                        new AttributeValue()\n                                                .withN(Long.toString(resp.getLatestTableVersion())))\n                        );\n                        put(DynamoDBTableEntryConstants.TABLE_PATH, new ExpectedAttributeValue()\n                                .withValue(\n                                        new AttributeValue()\n                                                .withS(logPath.getParent().toString()))\n                        );\n                        put(DynamoDBTableEntryConstants.SCHEMA_VERSION, new ExpectedAttributeValue()\n                                .withValue(\n                                        new AttributeValue()\n                                                .withN(Integer.toString(CLIENT_VERSION)))\n                        );\n                    }\n                });\n        try {\n            client.updateItem(request);\n        } catch (ConditionalCheckFailedException e) {\n            // Ignore the exception. The backfill succeeded but the update to\n            // the commit coordinator failed. The main purpose of a backfill operation is to ensure that\n            // UUID commit is physically copied to a standard commit file path. A failed update to\n            // the commit coordinator is not critical.\n            LOG.warn(\"Backfill succeeded but the update to the commit coordinator failed. This is probably\" +\n                    \" due to a concurrent update to the commit coordinator. This is not a critical error and \" +\n                    \" should rectify itself.\");\n        }\n    }\n\n    @Override\n    public Map<String, String> registerTable(\n            Path logPath,\n            Optional<TableIdentifier> tableIdentifier,\n            long currentVersion,\n            AbstractMetadata currentMetadata,\n            AbstractProtocol currentProtocol) {\n        java.util.Map<String, AttributeValue> item = new HashMap<>();\n\n        String tableId = java.util.UUID.randomUUID().toString();\n        item.put(DynamoDBTableEntryConstants.TABLE_ID, new AttributeValue().withS(tableId));\n\n        // We maintain the invariant that a commit will only succeed if the latestVersion stored\n        // in the table is equal to attemptVersion - 1. To maintain this, even though the\n        // filesystem-based commit after register table can fail, we still treat the attemptVersion\n        // at registration as a valid version. Since it is expected that the commit coordinator will\n        // return -1 as the table version if no commits have been accepted after registration, we\n        // use another attribute (HAS_ACCEPTED_COMMITS) to track whether any commits have been\n        // accepted. This attribute is set to true whenever any commit is accepted.\n        // If HAS_ACCEPTED_COMMITS is false, in a getCommit request, we set the latest version to -1.\n        long attemptVersion = currentVersion + 1;\n        item.put(\n                DynamoDBTableEntryConstants.TABLE_LATEST_VERSION,\n                new AttributeValue().withN(Long.toString(attemptVersion)));\n        // Used to indicate that no real commits have gone through the commit coordinator yet.\n        item.put(\n                DynamoDBTableEntryConstants.HAS_ACCEPTED_COMMITS,\n                new AttributeValue().withBOOL(false));\n\n        item.put(\n                DynamoDBTableEntryConstants.TABLE_PATH,\n                new AttributeValue().withS(logPath.getParent().toString()));\n        item.put(DynamoDBTableEntryConstants.COMMITS, new AttributeValue().withL());\n        item.put(\n                DynamoDBTableEntryConstants.ACCEPTING_COMMITS, new AttributeValue().withBOOL(true));\n        item.put(\n                DynamoDBTableEntryConstants.SCHEMA_VERSION,\n                new AttributeValue().withN(Integer.toString(CLIENT_VERSION)));\n\n        PutItemRequest request = new PutItemRequest()\n                .withTableName(coordinatedCommitsTableName)\n                .withItem(item)\n                .withConditionExpression(\n                        String.format(\n                                \"attribute_not_exists(%s)\", DynamoDBTableEntryConstants.TABLE_ID));\n        client.putItem(request);\n\n        Map<String, String> tableConf = new HashMap();\n        tableConf.put(DynamoDBTableEntryConstants.TABLE_ID, tableId);\n\n        return tableConf;\n    }\n\n    // Copied from DynamoDbLogStore. TODO: add the logging back.\n\n    /**\n     * Ensures that the table used to store commits from all Delta tables exists. If the table\n     * does not exist, it will be created.\n     * @throws IOException\n     */\n    private void tryEnsureTableExists() throws IOException {\n        int retries = 0;\n        boolean created = false;\n        while(retries < 20) {\n            String status = \"CREATING\";\n            try {\n                DescribeTableResult result = client.describeTable(coordinatedCommitsTableName);\n                TableDescription descr = result.getTable();\n                status = descr.getTableStatus();\n            } catch (ResourceNotFoundException e) {\n                LOG.info(\n                        \"DynamoDB table `{}` for endpoint `{}` does not exist. \" +\n                        \"Creating it now with provisioned throughput of {} RCUs and {} WCUs.\",\n                        coordinatedCommitsTableName, endpoint, readCapacityUnits, writeCapacityUnits);\n                try {\n                    client.createTable(\n                            // attributeDefinitions\n                            java.util.Collections.singletonList(\n                                    new AttributeDefinition(\n                                            DynamoDBTableEntryConstants.TABLE_ID,\n                                            ScalarAttributeType.S)\n                            ),\n                            coordinatedCommitsTableName,\n                            // keySchema\n                            java.util.Collections.singletonList(\n                                    new KeySchemaElement(\n                                            DynamoDBTableEntryConstants.TABLE_ID,\n                                            KeyType.HASH)\n                            ),\n                            new ProvisionedThroughput(this.readCapacityUnits, this.writeCapacityUnits)\n                    );\n                    created = true;\n                } catch (ResourceInUseException e3) {\n                    // race condition - table just created by concurrent process\n                }\n            }\n            if (status.equals(\"ACTIVE\")) {\n                if (created) {\n                    LOG.info(\"Successfully created DynamoDB table `{}`\", coordinatedCommitsTableName);\n                } else {\n                    LOG.info(\"Table `{}` already exists\", coordinatedCommitsTableName);\n                }\n                break;\n            } else if (status.equals(\"CREATING\")) {\n                retries += 1;\n                LOG.info(\"Waiting for `{}` table creation\", coordinatedCommitsTableName);\n                try {\n                    Thread.sleep(1000);\n                } catch(InterruptedException e) {\n                    throw new InterruptedIOException(e.getMessage());\n                }\n            } else {\n                LOG.error(\"table `{}` status: {}\", coordinatedCommitsTableName, status);\n                throw new RuntimeException(\"DynamoDBCommitCoordinatorCliet: Unable to create table with \" +\n                        \"name \" + coordinatedCommitsTableName + \" for endpoint \" + endpoint + \". Ensure \" +\n                        \"that the credentials provided have the necessary permissions to create \" +\n                        \"tables in DynamoDB. If the table already exists, ensure that the table \" +\n                        \"is in the ACTIVE state.\");\n            }\n        };\n    }\n\n    @Override\n    public boolean semanticEquals(CommitCoordinatorClient other) {\n        if (!(other instanceof DynamoDBCommitCoordinatorClient)) {\n            return false;\n        }\n        DynamoDBCommitCoordinatorClient otherStore = (DynamoDBCommitCoordinatorClient) other;\n        return this.coordinatedCommitsTableName.equals(otherStore.coordinatedCommitsTableName)\n                && this.endpoint.equals(otherStore.endpoint);\n    }\n}\n"
  },
  {
    "path": "spark/src/main/java/io/delta/dynamodbcommitcoordinator/DynamoDBCommitCoordinatorClientBuilder.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.dynamodbcommitcoordinator;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.services.dynamodbv2.AmazonDynamoDB;\nimport com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;\nimport org.apache.spark.sql.delta.coordinatedcommits.CommitCoordinatorBuilder;\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf;\nimport io.delta.storage.commit.CommitCoordinatorClient;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.SparkSession;\nimport scala.collection.immutable.Map;\n\nimport java.io.IOException;\n\npublic class DynamoDBCommitCoordinatorClientBuilder implements CommitCoordinatorBuilder {\n\n    private final long BACKFILL_BATCH_SIZE = 1L;\n\n    @Override\n    public String getName() {\n        return \"dynamodb\";\n    }\n\n    /**\n     * Key for the name of the DynamoDB table which stores all the unbackfilled\n     * commits for this owner. The value of this key is stored in the `conf`\n     * which is passed to the `build` method.\n     */\n    private static final String COORDINATED_COMMITS_TABLE_NAME_KEY = \"dynamoDBTableName\";\n    /**\n     * The endpoint of the DynamoDB service. The value of this key is stored in the\n     * `conf` which is passed to the `build` method.\n     */\n    private static final String DYNAMO_DB_ENDPOINT_KEY = \"dynamoDBEndpoint\";\n\n    @Override\n    public CommitCoordinatorClient build(SparkSession spark, Map<String, String> conf) {\n        String coordinatedCommitsTableName = conf.get(COORDINATED_COMMITS_TABLE_NAME_KEY).getOrElse(() -> {\n            throw new RuntimeException(COORDINATED_COMMITS_TABLE_NAME_KEY + \" not found\");\n        });\n        String dynamoDBEndpoint = conf.get(DYNAMO_DB_ENDPOINT_KEY).getOrElse(() -> {\n            throw new RuntimeException(DYNAMO_DB_ENDPOINT_KEY + \" not found\");\n        });\n        String awsCredentialsProviderName =\n                spark.conf().get(DeltaSQLConf.COORDINATED_COMMITS_DDB_AWS_CREDENTIALS_PROVIDER_NAME());\n        int readCapacityUnits = Integer.parseInt(\n                spark.conf().get(DeltaSQLConf.COORDINATED_COMMITS_DDB_READ_CAPACITY_UNITS().key()));\n        int writeCapacityUnits = Integer.parseInt(\n                spark.conf().get(DeltaSQLConf.COORDINATED_COMMITS_DDB_WRITE_CAPACITY_UNITS().key()));\n        boolean skipPathCheck = Boolean.parseBoolean(\n                spark.conf().get(DeltaSQLConf.COORDINATED_COMMITS_DDB_SKIP_PATH_CHECK().key()));\n        try {\n            AmazonDynamoDB ddbClient = createAmazonDDBClient(\n                    dynamoDBEndpoint,\n                    awsCredentialsProviderName,\n                    spark.sessionState().newHadoopConf()\n            );\n            return getDynamoDBCommitCoordinatorClient(\n                    coordinatedCommitsTableName,\n                    dynamoDBEndpoint,\n                    ddbClient,\n                    BACKFILL_BATCH_SIZE,\n                    readCapacityUnits,\n                    writeCapacityUnits,\n                    skipPathCheck\n            );\n        } catch (Exception e) {\n            throw new RuntimeException(\"Failed to create DynamoDB client\", e);\n        }\n    }\n\n    protected DynamoDBCommitCoordinatorClient getDynamoDBCommitCoordinatorClient(\n            String coordinatedCommitsTableName,\n            String dynamoDBEndpoint,\n            AmazonDynamoDB ddbClient,\n            long backfillBatchSize,\n            int readCapacityUnits,\n            int writeCapacityUnits,\n            boolean skipPathCheck\n    ) throws IOException {\n        return new DynamoDBCommitCoordinatorClient(\n                coordinatedCommitsTableName,\n                dynamoDBEndpoint,\n                ddbClient,\n                backfillBatchSize,\n                readCapacityUnits,\n                writeCapacityUnits,\n                skipPathCheck\n        );\n    }\n\n    protected AmazonDynamoDB createAmazonDDBClient(\n            String endpoint,\n            String credentialProviderName,\n            Configuration hadoopConf\n    ) throws ReflectiveOperationException {\n        AWSCredentialsProvider awsCredentialsProvider =\n                ReflectionUtils.createAwsCredentialsProvider(credentialProviderName, hadoopConf);\n        AmazonDynamoDBClient client = new AmazonDynamoDBClient(awsCredentialsProvider);\n        client.setEndpoint(endpoint);\n        return client;\n    }\n}\n"
  },
  {
    "path": "spark/src/main/java/io/delta/dynamodbcommitcoordinator/DynamoDBTableEntryConstants.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.dynamodbcommitcoordinator;\n\n/**\n * Defines the field names used in the DynamoDB table entry.\n */\nfinal class DynamoDBTableEntryConstants {\n    private DynamoDBTableEntryConstants() {}\n\n    /** The primary key of the DynamoDB table. */\n    public static final String TABLE_ID = \"tableId\";\n    /** The version of the latest commit in the corresponding Delta table. */\n    public static final String TABLE_LATEST_VERSION = \"tableVersion\";\n    /** The inCommitTimestamp of the latest commit in the corresponding Delta table. */\n    public static final String TABLE_LATEST_TIMESTAMP = \"tableTimestamp\";\n    /** Whether this commit coordinator is accepting more commits for the corresponding Delta table. */\n    public static final String ACCEPTING_COMMITS = \"acceptingCommits\";\n    /** The path of the corresponding Delta table. */\n    public static final String TABLE_PATH = \"path\";\n    /** The schema version of this DynamoDB table entry. */\n    public static final String SCHEMA_VERSION = \"schemaVersion\";\n    /**\n     * Whether this commit coordinator has accepted any commits after `registerTable`.\n     */\n    public static final String HAS_ACCEPTED_COMMITS = \"hasAcceptedCommits\";\n    /** The name of the field used to store unbackfilled commits. */\n    public static final String COMMITS = \"commits\";\n    /** The unbackfilled commit version. */\n    public static final String COMMIT_VERSION = \"version\";\n    /** The inCommitTimestamp of the unbackfilled commit. */\n    public static final String COMMIT_TIMESTAMP = \"timestamp\";\n    /** The name of the unbackfilled file. e.g. 00001.uuid.json */\n    public static final String COMMIT_FILE_NAME = \"fsName\";\n    /** The length of the unbackfilled file as per the file status. */\n    public static final String COMMIT_FILE_LENGTH = \"fsLength\";\n    /** The modification timestamp of the unbackfilled file as per the file status. */\n    public static final String COMMIT_FILE_MODIFICATION_TIMESTAMP = \"fsTimestamp\";\n}\n"
  },
  {
    "path": "spark/src/main/java/io/delta/dynamodbcommitcoordinator/ReflectionUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.dynamodbcommitcoordinator;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport org.apache.hadoop.conf.Configuration;\n\nimport java.util.Arrays;\n\n/**\n * Utility class for reflection operations. Used to create AWS credentials provider from class name.\n * Same as the io.delta.storage.utils.ReflectionUtils class is used in delta/storage-s3-dynamodb.\n */\npublic class ReflectionUtils {\n\n    private static boolean readsCredsFromHadoopConf(Class<?> awsCredentialsProviderClass) {\n        return Arrays.stream(awsCredentialsProviderClass.getConstructors())\n                .anyMatch(constructor -> constructor.getParameterCount() == 1 &&\n                        Arrays.equals(constructor.getParameterTypes(), new Class[]{Configuration.class}));\n    }\n\n    /**\n     * Creates a AWS credentials provider from the given provider classname and {@link Configuration}.\n     *\n     * It first checks if AWS Credentials Provider class has a constructor with Hadoop configuration\n     * as parameter.\n     *   If yes - create instance of class using this constructor.\n     *   If no - create instance with empty parameters constructor.\n     *\n     * @param credentialsProviderClassName Fully qualified name of the desired credentials provider class.\n     * @param hadoopConf Hadoop configuration, used to create instance of AWS credentials\n     *                                      provider, if supported.\n     * @return {@link AWSCredentialsProvider} object, instantiated from the class @see {credentialsProviderClassName}\n     * @throws ReflectiveOperationException When AWS credentials provider constructor do not match.\n     *                                      Indicates that the class has neither a constructor with no args\n     *                                      nor a constructor with only Hadoop configuration as argument.\n     */\n    public static AWSCredentialsProvider createAwsCredentialsProvider(\n            String credentialsProviderClassName,\n            Configuration hadoopConf) throws ReflectiveOperationException {\n        Class<?> awsCredentialsProviderClass = Class.forName(credentialsProviderClassName);\n        if (readsCredsFromHadoopConf(awsCredentialsProviderClass))\n            return (AWSCredentialsProvider) awsCredentialsProviderClass\n                    .getConstructor(Configuration.class)\n                    .newInstance(hadoopConf);\n        else\n            return (AWSCredentialsProvider) awsCredentialsProviderClass.getConstructor().newInstance();\n    }\n\n}\n"
  },
  {
    "path": "spark/src/main/java/io/delta/dynamodbcommitcoordinator/integration_tests/dynamodb_commitcoordinator_integration_test.py",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport sys\nimport threading\nimport json\n\nfrom pyspark.sql import SparkSession\nfrom multiprocessing.pool import ThreadPool\nimport time\nimport boto3\nimport uuid\n\n\"\"\"\n\nRun this script in root dir of repository:\n\n# ===== Mandatory input from user =====\nexport RUN_ID=run001\nexport S3_BUCKET=delta-lake-dynamodb-test-00\nexport AWS_DEFAULT_REGION=us-west-2\n\n# ===== Optional input from user =====\nexport DELTA_CONCURRENT_WRITERS=20\nexport DELTA_CONCURRENT_READERS=2\nexport DELTA_NUM_ROWS=200\nexport DELTA_DYNAMO_ENDPOINT=https://dynamodb.us-west-2.amazonaws.com\n\n# ===== Optional input from user (we calculate defaults using S3_BUCKET and RUN_ID) =====\nexport RELATIVE_DELTA_TABLE_PATH=___\nexport DELTA_DYNAMO_TABLE_NAME=___\n\n./run-integration-tests.py --use-local --run-dynamodb-commit-coordinator-integration-tests \\\n    --packages org.apache.hadoop:hadoop-aws:3.4.0,com.amazonaws:aws-java-sdk-bundle:1.12.262 \\\n    --dbb-conf io.delta.storage.credentials.provider=com.amazonaws.auth.profile.ProfileCredentialsProvider \\\n               spark.hadoop.fs.s3a.aws.credentials.provider=com.amazonaws.auth.profile.ProfileCredentialsProvider\n\"\"\"\n\n# ===== Mandatory input from user =====\nrun_id = os.environ.get(\"RUN_ID\")\ns3_bucket = os.environ.get(\"S3_BUCKET\")\n\n# ===== Optional input from user =====\nconcurrent_writers = int(os.environ.get(\"DELTA_CONCURRENT_WRITERS\", 2))\nconcurrent_readers = int(os.environ.get(\"DELTA_CONCURRENT_READERS\", 2))\nnum_rows = int(os.environ.get(\"DELTA_NUM_ROWS\", 16))\ndynamo_endpoint = os.environ.get(\"DELTA_DYNAMO_ENDPOINT\", \"https://dynamodb.us-west-2.amazonaws.com\")\n\n# ===== Optional input from user (we calculate defaults using RUN_ID) =====\nrelative_delta_table_path = os.environ.get(\"RELATIVE_DELTA_TABLE_PATH\", f\"tables/table_ddb_cs_{run_id}_{str(uuid.uuid4())}\")\\\n    .rstrip(\"/\")\ndynamo_table_name = os.environ.get(\"DELTA_DYNAMO_TABLE_NAME\", \"test_ddb_cs_table_\" + run_id)\n\nrelative_delta_table1_path = relative_delta_table_path + \"_tab1\"\nrelative_delta_table2_path = relative_delta_table_path + \"_tab2\"\nbucket_prefix = \"s3a://\" + s3_bucket + \"/\"\ndelta_table1_path = bucket_prefix + relative_delta_table1_path\ndelta_table2_path = bucket_prefix + relative_delta_table2_path\n\nif delta_table1_path is None:\n    print(f\"\\nSkipping Python test {os.path.basename(__file__)} due to the missing env variable \"\n          f\"`DELTA_TABLE_PATH`\\n=====================\")\n    sys.exit(0)\n\ndynamodb_commit_coordinator_conf = json.dumps({\n    \"dynamoDBTableName\": dynamo_table_name,\n    \"dynamoDBEndpoint\": dynamo_endpoint\n})\n\ntest_log = f\"\"\"\n==========================================\nrun id: {run_id}\ndelta table1 path: {delta_table1_path}\ndelta table2 path: {delta_table1_path}\ndynamo table name: {dynamo_table_name}\n\nconcurrent writers: {concurrent_writers}\nconcurrent readers: {concurrent_readers}\nnumber of rows: {num_rows}\n\nrelative_delta_table_path: {relative_delta_table_path}\n==========================================\n\"\"\"\nprint(test_log)\n\ncommit_coordinator_property_key = \"coordinatedCommits.commitCoordinator\"\nproperty_key_suffix = \"-preview\"\n\nspark = SparkSession \\\n    .builder \\\n    .appName(\"utilities\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(f\"spark.databricks.delta.properties.defaults.{commit_coordinator_property_key}{property_key_suffix}\", \"dynamodb\") \\\n    .config(f\"spark.databricks.delta.properties.defaults.coordinatedCommits.commitCoordinatorConf{property_key_suffix}\", dynamodb_commit_coordinator_conf) \\\n    .config(f\"spark.databricks.delta.coordinatedCommits.commitCoordinator.dynamodb.awsCredentialsProviderName\", \"com.amazonaws.auth.profile.ProfileCredentialsProvider\") \\\n    .getOrCreate()\n\nprint(\"Creating table at path \", delta_table1_path)\nspark.sql(f\"CREATE table delta.`{delta_table1_path}` (id int, a int) USING DELTA\") # commit 0\n\n\ndef write_tx(n):\n    print(\"writing:\", [n, n])\n    spark.sql(f\"INSERT INTO delta.`{delta_table1_path}` VALUES ({n}, {n})\")\n\n\nstop_reading = threading.Event()\n\n\ndef read_data():\n    while not stop_reading.is_set():\n        print(\"Reading {:d} rows ...\".format(\n            spark.read.format(\"delta\").load(delta_table1_path).distinct().count())\n        )\n        time.sleep(1)\n\n\ndef start_read_thread():\n    thread = threading.Thread(target=read_data)\n    thread.start()\n    return thread\n\n\nprint(\"===================== Starting reads and writes =====================\")\nread_threads = [start_read_thread() for i in range(concurrent_readers)]\npool = ThreadPool(concurrent_writers)\nstart_t = time.time()\npool.map(write_tx, range(num_rows))\nstop_reading.set()\n\nfor thread in read_threads:\n    thread.join()\n\nprint(\"===================== Evaluating number of written rows =====================\")\nactual = spark.read.format(\"delta\").load(delta_table1_path).distinct().count()\nprint(\"Actual number of written rows:\", actual)\nprint(\"Expected number of written rows:\", num_rows)\nassert actual == num_rows\n\nt = time.time() - start_t\nprint(f\"{num_rows / t:.02f} tx / sec\")\n\ncurrent_table_version = num_rows\ndynamodb = boto3.resource('dynamodb', endpoint_url=dynamo_endpoint)\nddb_table = dynamodb.Table(dynamo_table_name)\n\ndef get_dynamo_db_table_entry_id(table_path):\n    table_properties = spark.sql(f\"DESCRIBE DETAIL delta.`{table_path}`\").select(\"properties\").collect()[0][0]\n    table_conf = table_properties.get(f\"delta.coordinatedCommits.tableConf{property_key_suffix}\", None)\n    if table_conf is None:\n        return None\n    return json.loads(table_conf).get(\"tableId\", None)\n\ndef validate_table_version_as_per_dynamodb(table_path, expected_version):\n    table_id = get_dynamo_db_table_entry_id(table_path)\n    assert table_id is not None\n    print(f\"Validating table version for tableId: {table_id}\")\n    item = ddb_table.get_item(\n        Key={\n            'tableId': table_id\n        },\n        AttributesToGet = ['tableVersion']\n    )['Item']\n    current_table_version = int(item['tableVersion'])\n    assert current_table_version == expected_version\n\ndelta_table_version = num_rows\nvalidate_table_version_as_per_dynamodb(delta_table1_path, delta_table_version)\n\ndef perform_insert_and_validate(table_path, insert_value):\n    spark.sql(f\"INSERT INTO delta.`{table_path}` VALUES ({insert_value}, {insert_value})\")\n    res = spark.sql(f\"SELECT 1 FROM delta.`{table_path}` WHERE id = {insert_value} AND a = {insert_value}\").collect()\n    assert(len(res) == 1)\n\ndef check_for_delta_file_in_filesystem(delta_table_path, version, is_backfilled, should_exist):\n    # Check for backfilled commit\n    s3_client = boto3.client(\"s3\")\n    relative_table_path = delta_table_path.replace(bucket_prefix, \"\")\n    relative_delta_log_path = relative_table_path + \"/_delta_log/\"\n    relative_commit_folder_path = relative_delta_log_path if is_backfilled else os.path.join(relative_delta_log_path, \"_staged_commits\")\n    listing_prefix = os.path.join(relative_commit_folder_path, f\"{version:020}.\").lstrip(\"/\")\n    print(f\"querying {listing_prefix} from bucket {s3_bucket} for version {version}\")\n    response = s3_client.list_objects_v2(Bucket=s3_bucket, Prefix=listing_prefix)\n    if 'Contents' not in response:\n        assert(not should_exist, f\"Listing for prefix {listing_prefix} did not return any files even though it should have.\")\n        return\n    items = response['Contents']\n    commits = filter(lambda key: \".json\" in key and \".tmp\" not in key, map(lambda x: os.path.basename(x['Key']), items))\n    expected_count = 1 if should_exist else 0\n    matching_files = list(filter(lambda key: key.split('.')[0].endswith(f\"{version:020}\"), commits))\n    assert(len(matching_files) == expected_count)\n\ndef test_downgrades_and_upgrades(delta_table_path, delta_table_version):\n    # Downgrade to filesystem based commits should work\n    print(\"===================== Evaluating downgrade to filesystem based commits =====================\")\n    spark.sql(f\"ALTER TABLE delta.`{delta_table_path}` UNSET TBLPROPERTIES ('delta.{commit_coordinator_property_key}{property_key_suffix}')\")\n    delta_table_version += 1\n\n    perform_insert_and_validate(delta_table_path, 9990)\n    delta_table_version += 1\n\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=True, should_exist=True)\n    # No UUID delta file should have been created for this version\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=False, should_exist=False)\n    print(\"[SUCCESS] Downgrade to filesystem based commits worked\")\n\n    # Upgrade to coordinated commits should work\n    print(\"===================== Evaluating upgrade to coordinated commits =====================\")\n    spark.sql(f\"ALTER TABLE delta.`{delta_table_path}` SET TBLPROPERTIES ('delta.{commit_coordinator_property_key}{property_key_suffix}' = 'dynamodb')\")\n    delta_table_version += 1\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=True, should_exist=True)\n    # No UUID delta file should have been created for the enablement commit\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=False, should_exist=False)\n\n    perform_insert_and_validate(delta_table_path, 9991)\n    delta_table_version += 1\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=True, should_exist=True)\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=False, should_exist=True)\n\n    perform_insert_and_validate(delta_table_path, 9992)\n    delta_table_version += 1\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=True, should_exist=True)\n    check_for_delta_file_in_filesystem(delta_table_path, delta_table_version, is_backfilled=False, should_exist=True)\n    validate_table_version_as_per_dynamodb(delta_table_path, delta_table_version)\n\n    print(\"[SUCCESS] Upgrade to coordinated commits worked\")\n\ntest_downgrades_and_upgrades(delta_table1_path, delta_table_version)\n\n\n\nprint(\"[SUCCESS] All tests passed for Table 1\")\n\nprint(\"===================== Evaluating Table 2 =====================\")\n\n# Table 2 is created with coordinated commits disabled\nspark.conf.unset(f\"spark.databricks.delta.properties.defaults.{commit_coordinator_property_key}{property_key_suffix}\")\n\nspark.sql(f\"CREATE table delta.`{delta_table2_path}` (id int, a int) USING DELTA\") # commit 0\ntable_2_version = 0\n\nperform_insert_and_validate(delta_table2_path, 8000)\ntable_2_version += 1\n\ncheck_for_delta_file_in_filesystem(delta_table2_path, table_2_version, is_backfilled=True, should_exist=True)\n# No UUID delta file should have been created for this version\ncheck_for_delta_file_in_filesystem(delta_table2_path, table_2_version, is_backfilled=False, should_exist=False)\n\nprint(\"===================== Evaluating Upgrade of Table 2 =====================\")\n\nspark.sql(f\"ALTER TABLE delta.`{delta_table2_path}` SET TBLPROPERTIES ('delta.{commit_coordinator_property_key}{property_key_suffix}' = 'dynamodb')\")\ntable_2_version += 1\n\nperform_insert_and_validate(delta_table2_path, 8001)\ntable_2_version += 1\n\ncheck_for_delta_file_in_filesystem(delta_table2_path, table_2_version, is_backfilled=True, should_exist=True)\n# This version should have a UUID delta file\ncheck_for_delta_file_in_filesystem(delta_table2_path, table_2_version, is_backfilled=True, should_exist=True)\n\ntest_downgrades_and_upgrades(delta_table2_path, table_2_version)\n\nprint(\"[SUCCESS] All tests passed for Table 2\")\n"
  },
  {
    "path": "spark/src/main/java/org/apache/spark/sql/delta/DeltaV2Mode.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\nimport java.util.Map;\nimport java.util.Optional;\n\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf$;\nimport org.apache.spark.sql.delta.util.CatalogTableUtils;\nimport org.apache.spark.sql.internal.SQLConf;\n\n/**\n * Centralized decision logic for Delta connector selection (sparkV2 vs sparkV1).\n *\n * <p>This class encapsulates all configuration checking for\n * {@code spark.databricks.delta.v2.enableMode} so that the rest of the codebase doesn't need to\n * directly inspect configuration values.\n *\n * <p>Configuration modes:\n * <ul>\n *   <li>NONE (default): sparkV1 connector for all operations</li>\n *   <li>AUTO: sparkV2 connector only for Unity Catalog managed tables</li>\n *   <li>STRICT: sparkV2 connector for all tables (testing mode)</li>\n * </ul>\n */\npublic class DeltaV2Mode {\n  private static final String STRICT = \"STRICT\";\n  private static final String AUTO = \"AUTO\";\n\n  private final SQLConf sqlConf;\n\n  public DeltaV2Mode(SQLConf sqlConf) {\n    this.sqlConf = sqlConf;\n  }\n\n  private String mode() {\n    return sqlConf.getConf(DeltaSQLConf$.MODULE$.V2_ENABLE_MODE());\n  }\n\n  /**\n   * Determines if streaming reads should use the sparkV2 connector.\n   *\n   * @param catalogTable Optional catalog table metadata\n   * @return true if sparkV2 streaming reads should be used\n   */\n  public boolean isStreamingReadsEnabled(Optional<CatalogTable> catalogTable) {\n    switch (mode()) {\n      case STRICT:\n        // Always use sparkV2 connector for all catalog tables\n        return true;\n      case AUTO:\n        // Only use sparkV2 connector for Unity Catalog managed tables\n        return catalogTable.map(CatalogTableUtils::isUnityCatalogManagedTable).orElse(false);\n      default:\n        // NONE or unknown: use sparkV1 streaming\n        return false;\n    }\n  }\n\n  /**\n   * Determines if catalog should return sparkV2 (SparkTable) or sparkV1 (DeltaTableV2) tables.\n   *\n   * @return true if catalog should return sparkV2 tables\n   */\n  public boolean shouldCatalogReturnV2Tables() {\n    switch (mode()) {\n      case STRICT:\n        // STRICT mode: always return sparkV2 tables\n        return true;\n      default:\n        // NONE (default) or AUTO: return sparkV1 tables\n        // Note: AUTO mode uses sparkV2 connector only for streaming via ApplyV2Streaming rule,\n        // not at catalog level\n        return false;\n    }\n  }\n\n  /**\n   * Determines if the provided schema should be trusted without validation for streaming reads.\n   * This is used to bypass DeltaLog schema loading for Unity Catalog tables where the catalog\n   * already provides the correct schema.\n   *\n   * <p>If we don't bypass, we will load schema from DeltaLog and validate against the provided\n   * schema. For UC-managed tables this extra DeltaLog access can be unnecessary and may fail when\n   * the client doesn't have direct storage access to the managed location, even though the UC\n   * schema is authoritative. For UC-managed tables, the DeltaLog schema should always match the\n   * catalog schema, so re-validating provides no additional correctness guarantees.\n   *\n   * <p>This checks the parameters map for UC markers to determine if the table is UC-managed.\n   *\n   * @param parameters DataSource parameters map containing table storage properties\n   * @return true if provided schema should be used without validation\n   */\n  public boolean shouldBypassSchemaValidationForStreaming(Map<String, String> parameters) {\n    switch (mode()) {\n      case STRICT:\n      case AUTO:\n        // In sparkV2 modes, trust the schema for Unity Catalog managed tables\n        return CatalogTableUtils.isUnityCatalogManagedTableFromProperties(parameters);\n      default:\n        // NONE or unknown: always validate schema via DeltaLog\n        return false;\n    }\n  }\n\n  /**\n   * Gets the current mode string (for logging/debugging).\n   */\n  public String getMode() {\n    return mode();\n  }\n}\n"
  },
  {
    "path": "spark/src/main/java/org/apache/spark/sql/delta/RowIndexFilter.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\nimport org.apache.spark.sql.vectorized.ColumnVector;\nimport org.apache.spark.sql.execution.vectorized.WritableColumnVector;\n\n/**\n * Provides filtering information for each row index within given range.\n * Specific filters are implemented in subclasses.\n */\npublic interface RowIndexFilter {\n\n    /**\n     * Materialize filtering information for all rows in the range [start, end)\n     * by filling a boolean column vector batch. Assumes the indexes of the rows in the batch are\n     * consecutive and start from 0.\n     *\n     * @param start  Beginning index of the filtering range (inclusive).\n     * @param end    End index of the filtering range (exclusive).\n     * @param batch  The column vector for the current batch to materialize the range into.\n     */\n    void materializeIntoVector(long start, long end, WritableColumnVector batch);\n\n    /**\n     * Materialize filtering information for all rows in the batch. This is achieved by probing\n     * the roaring bitmap with the row index of every row in the batch.\n     *\n     * @param batchSize The size of the batch.\n     * @param rowIndexColumn A column vector that contains the row index of each row in the batch.\n     * @param batch The column vector for the current batch to materialize the range into.\n     */\n    void materializeIntoVectorWithRowIndex(\n        int batchSize,\n        ColumnVector rowIndexColumn,\n        WritableColumnVector batch);\n\n    /**\n     * Materialize filtering information for batches with a single row.\n     *\n     * @param rowIndex The index of the row to materialize the filtering information.\n     * @param batch The column vector for the current batch to materialize the range into.\n     *              We assume it contains a single row.\n     */\n    void materializeSingleRowWithRowIndex(long rowIndex, WritableColumnVector batch);\n\n    /**\n     * Value that must be materialised for a row to be kept after filtering.\n     */\n    public static final byte KEEP_ROW_VALUE = 0;\n    /**\n     * Value that must be materialised for a row to be dropped during filtering.\n     */\n    public static final byte DROP_ROW_VALUE = 1;\n}\n"
  },
  {
    "path": "spark/src/main/java/org/apache/spark/sql/delta/RowIndexFilterType.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\n/** Filter types corresponding to every row index filter implementations. */\npublic enum RowIndexFilterType {\n    /** Corresponding to [[DropMarkedRowsFilter]]. */\n    IF_CONTAINED(0),\n    /** Corresponding to [[KeepMarkedRowsFilter]]. */\n    IF_NOT_CONTAINED(1),\n    /** Invalid filter type. */\n    UNKNOWN(-1);\n\n    private final int id;\n\n    RowIndexFilterType(int id) {\n      this.id = id;\n    }\n\n    public int getId() {\n      return this.id;\n    }\n}\n"
  },
  {
    "path": "spark/src/main/java/org/apache/spark/sql/delta/sources/AdmittableFile.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources;\n\n/**\n * Interface for files that can be admitted by admission control in Delta streaming sources.\n * This abstraction allows both DSv1 and DSv2 IndexedFile implementations to be used with\n * the admission control logic.\n */\npublic interface AdmittableFile {\n  /**\n   * Returns true if this file has an associated file action (AddFile, RemoveFile, or CDCFile).\n   * Placeholder IndexedFiles with no file action will return false.\n   */\n  boolean hasFileAction();\n\n  /**\n   * Returns the size of the file in bytes.\n   * This method should only be called when hasFileAction() returns true.\n   */\n  long getFileSize();\n}\n"
  },
  {
    "path": "spark/src/main/java/org/apache/spark/sql/delta/util/CatalogTableUtils.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient;\nimport java.util.Collections;\nimport java.util.Map;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport scala.jdk.javaapi.CollectionConverters;\n\n/**\n * Utility helpers for inspecting Delta-related metadata persisted on Spark {@link CatalogTable}\n * instances by Unity Catalog.\n *\n * <p>Unity Catalog marks catalog-managed tables via feature flags stored in table storage\n * properties. This helper centralises the logic for interpreting those properties so the SparkV2\n * connector can decide when to use catalog-owned (CCv2) behaviour.\n *\n * <ul>\n *   <li>{@link #isCatalogManaged(CatalogTable)} checks whether either {@code\n *       delta.feature.catalogManaged} or {@code delta.feature.catalogOwned-preview} is set to\n *       {@code supported}, signalling that a catalog manages the table.\n *   <li>{@link #isUnityCatalogManagedTable(CatalogTable)} additionally verifies the presence of the\n *       Unity Catalog table identifier ({@link UCCommitCoordinatorClient#UC_TABLE_ID_KEY}) to\n *       confirm that the table is backed by Unity Catalog.\n * </ul>\n */\npublic final class CatalogTableUtils {\n  /**\n   * Property key for catalog-managed feature flag. Corresponds to\n   * delta.feature.catalogManaged and preview variant\n   * delta.feature.catalogOwned-preview\n   */\n  static final String FEATURE_CATALOG_MANAGED = \"delta.feature.catalogManaged\";\n\n  static final String FEATURE_CATALOG_OWNED_PREVIEW = \"delta.feature.catalogOwned-preview\";\n  private static final String SUPPORTED = \"supported\";\n\n  private CatalogTableUtils() {}\n\n  /**\n   * Checks whether any catalog manages this table via CCv2 semantics.\n   *\n   * @param table Spark {@link CatalogTable} descriptor\n   * @return {@code true} when either catalog feature flag is set to {@code supported}\n   */\n  public static boolean isCatalogManaged(CatalogTable table) {\n    requireNonNull(table, \"table is null\");\n    Map<String, String> storageProperties = getStorageProperties(table);\n    return isCatalogManagedFeatureEnabled(storageProperties, FEATURE_CATALOG_MANAGED)\n        || isCatalogManagedFeatureEnabled(storageProperties, FEATURE_CATALOG_OWNED_PREVIEW);\n  }\n\n  /**\n   * Checks whether the table is Unity Catalog managed.\n   *\n   * @param table Spark {@link CatalogTable} descriptor\n   * @return {@code true} when the table is catalog managed and contains the UC identifier\n   */\n  public static boolean isUnityCatalogManagedTable(CatalogTable table) {\n    requireNonNull(table, \"table is null\");\n    Map<String, String> storageProperties = getStorageProperties(table);\n    boolean isUCBacked = storageProperties.containsKey(UCCommitCoordinatorClient.UC_TABLE_ID_KEY);\n    return isUCBacked && isCatalogManaged(table);\n  }\n\n  /**\n   * Checks whether the table is Unity Catalog managed based on storage properties map.\n   *\n   * <p>This method checks the properties map (typically from table.storage.properties) for\n   * UC markers, allowing UC table detection without requiring a full CatalogTable object.\n   * This is useful when only the properties map is available, such as in DataSource APIs.\n   *\n   * @param properties Storage properties map (e.g., from CatalogTable.storage.properties or\n   *                   DataSource parameters map)\n   * @return {@code true} when the table is catalog managed and contains the UC identifier\n   */\n  public static boolean isUnityCatalogManagedTableFromProperties(Map<String, String> properties) {\n    if (properties == null || properties.isEmpty()) {\n      return false;\n    }\n    boolean isUCBacked = properties.containsKey(UCCommitCoordinatorClient.UC_TABLE_ID_KEY);\n    boolean isCatalogManaged = isCatalogManagedFeatureEnabled(properties, FEATURE_CATALOG_MANAGED)\n        || isCatalogManagedFeatureEnabled(properties, FEATURE_CATALOG_OWNED_PREVIEW);\n    return isUCBacked && isCatalogManaged;\n  }\n\n  /**\n   * Checks whether the given feature key is enabled in the table properties.\n   *\n   * @param tableProperties The table properties\n   * @param featureKey The feature key\n   * @return {@code true} when the feature key is set to {@code supported}\n   */\n  private static boolean isCatalogManagedFeatureEnabled(\n      Map<String, String> tableProperties, String featureKey) {\n    requireNonNull(tableProperties, \"tableProperties is null\");\n    requireNonNull(featureKey, \"featureKey is null\");\n    String featureValue = tableProperties.get(featureKey);\n    if (featureValue == null) {\n      return false;\n    }\n    return featureValue.equalsIgnoreCase(SUPPORTED);\n  }\n\n  /**\n   * Returns the catalog storage properties published with a {@link CatalogTable}.\n   *\n   * @param table Spark {@link CatalogTable} descriptor\n   * @return Java map view of the storage properties, never null\n   */\n  private static Map<String, String> getStorageProperties(CatalogTable table) {\n    requireNonNull(table, \"table is null\");\n    if (table.storage() == null) {\n      return Collections.emptyMap();\n    }\n    scala.collection.immutable.Map<String, String> scalaProps = table.storage().properties();\n    if (scalaProps == null || scalaProps.isEmpty()) {\n      return Collections.emptyMap();\n    }\n    return CollectionConverters.asJava(scalaProps);\n  }\n}\n\n"
  },
  {
    "path": "spark/src/main/java-shims/spark-4.0/org/apache/spark/sql/delta/shims/VariantStatsShims.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.shims;\n\nimport org.apache.spark.QueryContext;\nimport org.apache.spark.SparkRuntimeException;\nimport scala.collection.immutable.Map$;\n\n/**\n * Shim for variant stats functionality in Spark 4.0.\n * In Spark 4.0, VariantUtil.readUnsigned is a private member, so we provide our own\n * implementation here.\n */\npublic class VariantStatsShims {\n\n  static SparkRuntimeException malformedVariant() {\n    return new SparkRuntimeException(\"MALFORMED_VARIANT\",\n        Map$.MODULE$.<String, String>empty(), null, new QueryContext[]{}, \"\");\n  }\n\n  // Check the validity of an array index `pos`. Throw `MALFORMED_VARIANT` if it is out of bound,\n  // meaning that the variant is malformed.\n  private static void checkIndex(int pos, int length) {\n    if (pos < 0 || pos >= length) throw malformedVariant();\n  }\n\n  // Read a little-endian unsigned int value from `bytes[pos, pos + numBytes)`. The value must fit\n  // into a non-negative int (`[0, Integer.MAX_VALUE]`).\n  private static int readUnsigned(byte[] bytes, int pos, int numBytes) {\n    checkIndex(pos, bytes.length);\n    checkIndex(pos + numBytes - 1, bytes.length);\n    int result = 0;\n    // Similar to the `readLong` loop, but all bytes should be unsign-extended.\n    for (int i = 0; i < numBytes; ++i) {\n      int unsignedByteValue = bytes[pos + i] & 0xFF;\n      result |= unsignedByteValue << (8 * i);\n    }\n    if (result < 0) throw malformedVariant();\n    return result;\n  }\n\n  // Get the length of metadata in the provided array. It is used to split metadata and value in\n  // situations where they are serialized as a concatenated pair (e.g. Delta stats).\n  public static int metadataSize(byte[] metadata) {\n    checkIndex(0, metadata.length);\n    // Similar to the logic from getMetadataKey where \"id\" is equal to \"dictSize\".\n    int offsetSize = ((metadata[0] >> 6) & 0x3) + 1;\n    int dictSize = readUnsigned(metadata, 1, offsetSize);\n    int lastOffset = readUnsigned(metadata, 1 + (dictSize + 1) * offsetSize, offsetSize);\n    int size = 1 + (dictSize + 2) * offsetSize + lastOffset;\n    if (size > metadata.length) {\n      throw malformedVariant();\n    }\n    return size;\n  }\n}\n"
  },
  {
    "path": "spark/src/main/java-shims/spark-4.1/org/apache/spark/sql/delta/shims/VariantStatsShims.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.shims;\n\nimport org.apache.spark.QueryContext;\nimport org.apache.spark.SparkRuntimeException;\nimport org.apache.spark.types.variant.VariantUtil;\nimport scala.collection.immutable.Map$;\n\n/**\n * Shim for variant stats functionality in Spark 4.1+.\n * In Spark 4.1, VariantUtil.readUnsigned is public, so we can use it directly.\n */\npublic class VariantStatsShims {\n\n  static SparkRuntimeException malformedVariant() {\n    return new SparkRuntimeException(\"MALFORMED_VARIANT\",\n        Map$.MODULE$.<String, String>empty(), null, new QueryContext[]{}, \"\");\n  }\n\n  // Check the validity of an array index `pos`. Throw `MALFORMED_VARIANT` if it is out of bound,\n  // meaning that the variant is malformed.\n  private static void checkIndex(int pos, int length) {\n    if (pos < 0 || pos >= length) throw malformedVariant();\n  }\n\n  // Get the length of metadata in the provided array. It is used to split metadata and value in\n  // situations where they are serialized as a concatenated pair (e.g. Delta stats).\n  public static int metadataSize(byte[] metadata) {\n    checkIndex(0, metadata.length);\n    // Similar to the logic from getMetadataKey where \"id\" is equal to \"dictSize\".\n    int offsetSize = ((metadata[0] >> 6) & 0x3) + 1;\n    int dictSize = VariantUtil.readUnsigned(metadata, 1, offsetSize);\n    int lastOffset = VariantUtil.readUnsigned(metadata, 1 + (dictSize + 1) * offsetSize, offsetSize);\n    int size = 1 + (dictSize + 2) * offsetSize + lastOffset;\n    if (size > metadata.length) {\n      throw malformedVariant();\n    }\n    return size;\n  }\n}\n"
  },
  {
    "path": "spark/src/main/java-shims/spark-4.2/org/apache/spark/sql/delta/shims/VariantStatsShims.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.shims;\n\nimport org.apache.spark.QueryContext;\nimport org.apache.spark.SparkRuntimeException;\nimport org.apache.spark.types.variant.VariantUtil;\nimport scala.collection.immutable.Map$;\n\n/**\n * Shim for variant stats functionality in Spark 4.2+.\n * In Spark 4.2, VariantUtil.readUnsigned is public, so we can use it directly.\n */\npublic class VariantStatsShims {\n\n  static SparkRuntimeException malformedVariant() {\n    return new SparkRuntimeException(\"MALFORMED_VARIANT\",\n        Map$.MODULE$.<String, String>empty(), null, new QueryContext[]{}, \"\");\n  }\n\n  // Check the validity of an array index `pos`. Throw `MALFORMED_VARIANT` if it is out of bound,\n  // meaning that the variant is malformed.\n  private static void checkIndex(int pos, int length) {\n    if (pos < 0 || pos >= length) throw malformedVariant();\n  }\n\n  // Get the length of metadata in the provided array. It is used to split metadata and value in\n  // situations where they are serialized as a concatenated pair (e.g. Delta stats).\n  public static int metadataSize(byte[] metadata) {\n    checkIndex(0, metadata.length);\n    // Similar to the logic from getMetadataKey where \"id\" is equal to \"dictSize\".\n    int offsetSize = ((metadata[0] >> 6) & 0x3) + 1;\n    int dictSize = VariantUtil.readUnsigned(metadata, 1, offsetSize);\n    int lastOffset = VariantUtil.readUnsigned(metadata, 1 + (dictSize + 1) * offsetSize, offsetSize);\n    int size = 1 + (dictSize + 2) * offsetSize + lastOffset;\n    if (size > metadata.length) {\n      throw malformedVariant();\n    }\n    return size;\n  }\n}\n"
  },
  {
    "path": "spark/src/main/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister",
    "content": "org.apache.spark.sql.delta.sources.DeltaDataSource"
  },
  {
    "path": "spark/src/main/resources/error/delta-error-classes.json",
    "content": "{\n  \"DELTA_ACTIVE_SPARK_SESSION_NOT_FOUND\" : {\n    \"message\" : [\n      \"Could not find active SparkSession\"\n    ],\n    \"sqlState\" : \"08003\"\n  },\n  \"DELTA_ACTIVE_TRANSACTION_ALREADY_SET\" : {\n    \"message\" : [\n      \"Cannot set a new txn as active when one is already active\"\n    ],\n    \"sqlState\" : \"0B000\"\n  },\n  \"DELTA_ADDING_COLUMN_WITH_INTERNAL_NAME_FAILED\" : {\n    \"message\" : [\n      \"Failed to add column <colName> because the name is reserved.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_ADDING_DELETION_VECTORS_DISALLOWED\" : {\n    \"message\" : [\n      \"The current operation attempted to add a deletion vector to a table that does not permit the creation of new deletion vectors. Please file a bug report.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_ADDING_DELETION_VECTORS_WITH_TIGHT_BOUNDS_DISALLOWED\" : {\n    \"message\" : [\n      \"All operations that add deletion vectors should set the tightBounds column in statistics to false. Please file a bug report.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_ADD_COLUMN_AT_INDEX_LESS_THAN_ZERO\" : {\n    \"message\" : [\n      \"Index <columnIndex> to add column <columnName> is lower than 0\"\n    ],\n    \"sqlState\" : \"42KD3\"\n  },\n  \"DELTA_ADD_COLUMN_PARENT_NOT_STRUCT\" : {\n    \"message\" : [\n      \"Cannot add <columnName> because its parent is not a StructType. Found <other>\"\n    ],\n    \"sqlState\" : \"42KD3\"\n  },\n  \"DELTA_ADD_COLUMN_STRUCT_NOT_FOUND\" : {\n    \"message\" : [\n      \"Struct not found at position <position>\"\n    ],\n    \"sqlState\" : \"42KD3\"\n  },\n  \"DELTA_ADD_CONSTRAINTS\" : {\n    \"message\" : [\n      \"Please use ALTER TABLE ADD CONSTRAINT to add CHECK constraints.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_AGGREGATE_IN_CHECK_CONSTRAINT\" : {\n    \"message\" : [\n      \"Found <sqlExpr> in a CHECK constraint. Aggregate expressions are not allowed in CHECK constraints.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_AGGREGATE_IN_GENERATED_COLUMN\" : {\n    \"message\" : [\n      \"Found <sqlExpr>. A generated column cannot use an aggregate expression\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_AGGREGATION_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Aggregate functions are not supported in the <operation> <predicate>.\"\n    ],\n    \"sqlState\" : \"42903\"\n  },\n  \"DELTA_ALTER_TABLE_CHANGE_COL_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"ALTER TABLE CHANGE COLUMN is not supported for changing column <currentType> to <newType>\"\n    ],\n    \"sqlState\" : \"42837\"\n  },\n  \"DELTA_ALTER_TABLE_CLUSTER_BY_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"ALTER TABLE CLUSTER BY is supported only for Delta table with clustering.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_ALTER_TABLE_CLUSTER_BY_ON_PARTITIONED_TABLE_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"ALTER TABLE CLUSTER BY cannot be applied to a partitioned table.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_ALTER_TABLE_SET_CLUSTERING_TABLE_FEATURE_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"Cannot enable <tableFeature> table feature using ALTER TABLE SET TBLPROPERTIES. Please use CREATE OR REPLACE TABLE CLUSTER BY to create a Delta table with clustering.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_AMBIGUOUS_DATA_TYPE_CHANGE\" : {\n    \"message\" : [\n      \"Cannot change data type of <column> from <from> to <to>. This change contains column removals and additions, therefore they are ambiguous. Please make these changes individually using ALTER TABLE [ADD | DROP | RENAME] COLUMN.\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_AMBIGUOUS_PARTITION_COLUMN\" : {\n    \"message\" : [\n      \"Ambiguous partition column <column> can be <colMatches>.\"\n    ],\n    \"sqlState\" : \"42702\"\n  },\n  \"DELTA_AMBIGUOUS_PATHS_IN_CREATE_TABLE\" : {\n    \"message\" : [\n      \"CREATE TABLE contains two different locations: <identifier> and <location>.\",\n      \"You can remove the LOCATION clause from the CREATE TABLE statement, or set\",\n      \"<config> to true to skip this check.\",\n      \"\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_BLOCK_COLUMN_MAPPING_AND_CDC_OPERATION\" : {\n    \"message\" : [\n      \"Operation \\\"<opName>\\\" is not allowed when the table has enabled change data feed (CDF) and has undergone schema changes using DROP COLUMN or RENAME COLUMN.\"\n    ],\n    \"sqlState\" : \"42KD4\"\n  },\n  \"DELTA_BLOOM_FILTER_DROP_ON_NON_EXISTING_COLUMNS\" : {\n    \"message\" : [\n      \"Cannot drop bloom filter indices for the following non-existent column(s): <unknownColumns>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_CANNOT_CHANGE_DATA_TYPE\" : {\n    \"message\" : [\n      \"Cannot change data type: <dataType>\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_CANNOT_CHANGE_LOCATION\" : {\n    \"message\" : [\n      \"Cannot change the 'location' of the Delta table using SET TBLPROPERTIES. Please use ALTER TABLE SET LOCATION instead.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_CANNOT_CHANGE_PROVIDER\" : {\n    \"message\" : [\n      \"'provider' is a reserved table property, and cannot be altered.\"\n    ],\n    \"sqlState\" : \"42939\"\n  },\n  \"DELTA_CANNOT_CONVERT_TO_FILEFORMAT\" : {\n    \"message\" : [\n      \"Can not convert<className> to FileFormat.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_CANNOT_CREATE_BLOOM_FILTER_NON_EXISTING_COL\" : {\n    \"message\" : [\n      \"Cannot create bloom filter indices for the following non-existent column(s): <unknownCols>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_CANNOT_CREATE_LOG_PATH\" : {\n    \"message\" : [\n      \"Cannot create <path>\"\n    ],\n    \"sqlState\" : \"42KD5\"\n  },\n  \"DELTA_CANNOT_DESCRIBE_VIEW_HISTORY\" : {\n    \"message\" : [\n      \"Cannot describe the history of a view.\"\n    ],\n    \"sqlState\" : \"42809\"\n  },\n  \"DELTA_CANNOT_DROP_BLOOM_FILTER_ON_NON_INDEXED_COLUMN\" : {\n    \"message\" : [\n      \"Cannot drop bloom filter index on a non indexed column: <columnName>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_CANNOT_DROP_CHECK_CONSTRAINT_FEATURE\" : {\n    \"message\" : [\n      \"Cannot drop the CHECK constraints table feature.\",\n      \"The following constraints must be dropped first: <constraints>.\"\n    ],\n    \"sqlState\" : \"0AKDE\"\n  },\n  \"DELTA_CANNOT_EVALUATE_EXPRESSION\" : {\n    \"message\" : [\n      \"Cannot evaluate expression: <expression>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CANNOT_FIND_VERSION\" : {\n    \"message\" : [\n      \"Cannot find 'sourceVersion' in <json>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_CANNOT_GENERATE_CODE_FOR_EXPRESSION\" : {\n    \"message\" : [\n      \"Cannot generate code for expression: <expression>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CANNOT_GENERATE_UPDATE_EXPRESSIONS\" : {\n    \"message\" : [\n      \"Calling without generated columns should always return a update expression for each column\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_CANNOT_MODIFY_APPEND_ONLY\" : {\n    \"message\" : [\n      \"This table is configured to only allow appends. If you would like to permit updates or deletes, use 'ALTER TABLE <table_name> SET TBLPROPERTIES (<config>=false)'.\"\n    ],\n    \"sqlState\" : \"42809\"\n  },\n  \"DELTA_CANNOT_MODIFY_CATALOG_MANAGED_DEPENDENCIES\" : {\n    \"message\" : [\n      \"Cannot override or unset in-commit timestamp table properties because this table is catalog-managed. Remove \\\"delta.enableInCommitTimestamps\\\", \\\"delta.inCommitTimestampEnablementVersion\\\", and \\\"delta.inCommitTimestampEnablementTimestamp\\\" from the TBLPROPERTIES clause and then retry the command.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CANNOT_MODIFY_COORDINATED_COMMITS_DEPENDENCIES\" : {\n    \"message\" : [\n      \"<Command> cannot override or unset in-commit timestamp table properties because coordinated commits is enabled in this table and depends on them. Please remove them (\\\"delta.enableInCommitTimestamps\\\", \\\"delta.inCommitTimestampEnablementVersion\\\", \\\"delta.inCommitTimestampEnablementTimestamp\\\") from the TBLPROPERTIES clause and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\" : {\n    \"message\" : [\n      \"The Delta table configuration <prop> cannot be specified by the user\"\n    ],\n    \"sqlState\" : \"42939\"\n  },\n  \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\" : {\n    \"message\" : [\n      \"<Command> cannot override coordinated commits configurations for an existing target table. Please remove them (\\\"delta.coordinatedCommits.commitCoordinator-preview\\\", \\\"delta.coordinatedCommits.commitCoordinatorConf-preview\\\", \\\"delta.coordinatedCommits.tableConf-preview\\\") from the TBLPROPERTIES clause and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CANNOT_RECONSTRUCT_PATH_FROM_URI\" : {\n    \"message\" : [\n      \"A uri (<uri>) which cannot be turned into a relative path was found in the transaction log.\"\n    ],\n    \"sqlState\" : \"22KD1\"\n  },\n  \"DELTA_CANNOT_RENAME_PATH\" : {\n    \"message\" : [\n      \"Cannot rename <currentPath> to <newPath>\"\n    ],\n    \"sqlState\" : \"22KD1\"\n  },\n  \"DELTA_CANNOT_REPLACE_MISSING_TABLE\" : {\n    \"message\" : [\n      \"Table <tableName> cannot be replaced as it does not exist. Use CREATE OR REPLACE TABLE to create the table.\"\n    ],\n    \"sqlState\" : \"42P01\"\n  },\n  \"DELTA_CANNOT_RESOLVE_COLUMN\" : {\n    \"message\" : [\n      \"Can't resolve column <columnName> in <schema>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_CANNOT_RESOLVE_SOURCE_COLUMN\" : {\n    \"message\" : [\n      \"Couldn't resolve qualified source column <columnName> within the source query.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_CANNOT_RESTORE_TABLE_VERSION\" : {\n    \"message\" : [\n      \"Cannot restore table to version <version>. Available versions: [<startVersion>, <endVersion>].\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_CANNOT_RESTORE_TIMESTAMP_EARLIER\" : {\n    \"message\" : [\n      \"Cannot restore table to timestamp (<requestedTimestamp>) as it is before the earliest version available. Please use a timestamp after (<earliestTimestamp>).\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_CANNOT_RESTORE_TIMESTAMP_GREATER\" : {\n    \"message\" : [\n      \"Cannot restore table to timestamp (<requestedTimestamp>) as it is after the latest version available. Please use a timestamp before (<latestTimestamp>)\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_CANNOT_SET_COORDINATED_COMMITS_DEPENDENCIES\" : {\n    \"message\" : [\n      \"<Command> cannot set in-commit timestamp table properties together with coordinated commits, because the latter depends on the former and sets the former internally. Please remove them (\\\"delta.enableInCommitTimestamps\\\", \\\"delta.inCommitTimestampEnablementVersion\\\", \\\"delta.inCommitTimestampEnablementTimestamp\\\") from the TBLPROPERTIES clause and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CANNOT_SET_LOCATION_MULTIPLE_TIMES\" : {\n    \"message\" : [\n      \"Can't set location multiple times. Found <location>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_CANNOT_SET_LOCATION_ON_PATH_IDENTIFIER\" : {\n    \"message\" : [\n      \"Cannot change the location of a path based table.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_CANNOT_UNSET_COORDINATED_COMMITS_CONFS\" : {\n    \"message\" : [\n      \"ALTER cannot unset coordinated commits configurations. To downgrade a table from coordinated commits, please try again using `ALTER TABLE [table-name] DROP FEATURE 'coordinatedCommits-preview'`.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CANNOT_UPDATE_ARRAY_FIELD\" : {\n    \"message\" : [\n      \"Cannot update <tableName> field <fieldName> type: update the element by updating `<fieldName>.element`.\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_CANNOT_UPDATE_MAP_FIELD\" : {\n    \"message\" : [\n      \"Cannot update <tableName> field <fieldName> type: update a map by updating `<fieldName>.key` or `<fieldName>.value`.\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_CANNOT_UPDATE_OTHER_FIELD\" : {\n    \"message\" : [\n      \"Cannot update <tableName> field of type <typeName>\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_CANNOT_UPDATE_STRUCT_FIELD\" : {\n    \"message\" : [\n      \"Cannot update <tableName> field <fieldName> type: update struct by adding, deleting, or updating its fields\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_CANNOT_USE_ALL_COLUMNS_FOR_PARTITION\" : {\n    \"message\" : [\n      \"Cannot use all columns for partition columns\"\n    ],\n    \"sqlState\" : \"428FT\"\n  },\n  \"DELTA_CANNOT_VACUUM_LITE\" : {\n    \"message\" : [\n      \"VACUUM LITE cannot delete all eligible files as some files are not referenced by the Delta log. Please run VACUUM FULL.\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_CANNOT_WRITE_INTO_VIEW\" : {\n    \"message\" : [\n      \"<table> is a view. Writes to a view are not supported.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_CAST_OVERFLOW_IN_TABLE_WRITE\" : {\n    \"message\" : [\n      \"Failed to write a value of <sourceType> type into the <targetType> type column <columnName> due to an overflow.\",\n      \"Use `try_cast` on the input value to tolerate overflow and return NULL instead.\",\n      \"If necessary, set <storeAssignmentPolicyFlag> to \\\"LEGACY\\\" to bypass this error or set <updateAndMergeCastingFollowsAnsiEnabledFlag> to true to revert to the old behaviour and follow <ansiEnabledFlag> in UPDATE and MERGE.\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_CDC_NON_CONSTANT_ARGUMENT\" : {\n    \"message\" : [\n      \"The <argumentName> argument (position <pos>) of the <functionName> function requires a constant value, but got a non-constant expression: <sqlExpr>.\",\n      \"\"\n    ],\n    \"sqlState\" : \"42K0H\"\n  },\n  \"DELTA_CDC_NOT_ALLOWED_IN_THIS_VERSION\" : {\n    \"message\" : [\n      \"Configuration delta.enableChangeDataFeed cannot be set. Change data feed from Delta is not yet available.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CDC_READ_NULL_RANGE_BOUNDARY\" : {\n    \"message\" : [\n      \"CDC read start/end parameters cannot be null. Please provide a valid version or timestamp.\"\n    ],\n    \"sqlState\" : \"22004\"\n  },\n  \"DELTA_CDC_START_VERSION_AFTER_LATEST\" : {\n    \"message\" : [\n      \"Start version <start> for change data feed exceeds the latest table version <latest>.\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_DATA_SCHEMA\" : {\n    \"message\" : [\n      \"Retrieving table changes between version <start> and <end> failed because of an incompatible data schema.\",\n      \"Your read schema is <readSchema> at version <readVersion>, but we found an incompatible data schema at version <incompatibleVersion>.\",\n      \"If possible, please retrieve the table changes using the end version's schema by setting <config> to `endVersion`, or contact support.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_SCHEMA_CHANGE\" : {\n    \"message\" : [\n      \"Retrieving table changes between version <start> and <end> failed because of an incompatible schema change.\",\n      \"Your read schema is <readSchema> at version <readVersion>, but we found an incompatible schema change at version <incompatibleVersion>.\",\n      \"If possible, please query table changes separately from version <start> to <incompatibleVersion> - 1, and from version <incompatibleVersion> to <end>.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CHANGE_TABLE_FEED_DISABLED\" : {\n    \"message\" : [\n      \"Cannot write to table with delta.enableChangeDataFeed set. Change data feed from Delta is not available.\"\n    ],\n    \"sqlState\" : \"42807\"\n  },\n  \"DELTA_CHECKPOINT_NON_EXIST_TABLE\" : {\n    \"message\" : [\n      \"Cannot checkpoint a non-existing table <path>. Did you manually delete files in the _delta_log directory?\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_CHECKPOINT_SNAPSHOT_MISMATCH\" : {\n    \"message\" : [\n      \"State of the checkpoint doesn't match that of the snapshot.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_CLONE_AMBIGUOUS_TARGET\" : {\n    \"message\" : [\n      \"\",\n      \"Two paths were provided as the CLONE target so it is ambiguous which to use. An external\",\n      \"location for CLONE was provided at <externalLocation> at the same time as the path\",\n      \"<targetIdentifier>.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_CLONE_INCOMPATIBLE_SOURCE\" : {\n    \"message\" : [\n      \"The clone source has valid format, but has unsupported feature with Delta\"\n    ],\n    \"subClass\" : {\n      \"ICEBERG_MISSING_PARTITION_SPECS\" : {\n        \"message\" : [\n          \"Source iceberg table has no partition specs in table\"\n        ]\n      },\n      \"ICEBERG_UNDERGONE_PARTITION_EVOLUTION\" : {\n        \"message\" : [\n          \"Source iceberg table has undergone partition evolution.\"\n        ]\n      }\n    },\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CLONE_UNSUPPORTED_SOURCE\" : {\n    \"message\" : [\n      \"Unsupported clone source '<name>', whose format is <format>.\",\n      \"The supported formats are 'delta', 'iceberg' and 'parquet'.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CLONE_WITH_ROW_TRACKING_WITHOUT_STATS\" : {\n    \"message\" : [\n      \"Cannot shallow clone a table without statistics and with row tracking enabled.\",\n      \"If you want to enable row tracking you need to first collect statistics on the source table by running:\",\n      \"ANALYZE TABLE table_name COMPUTE DELTA STATISTICS\",\n      \"\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_CLUSTERING_COLUMNS_DATATYPE_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"CLUSTER BY is not supported because the following column(s): <columnsWithDataTypes> don't support data skipping.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_CLUSTERING_COLUMNS_MISMATCH\" : {\n    \"message\" : [\n      \"The provided clustering columns do not match the existing table's.\",\n      \"- provided: <providedClusteringColumns>\",\n      \"- existing: <existingClusteringColumns>\"\n    ],\n    \"sqlState\" : \"42P10\"\n  },\n  \"DELTA_CLUSTERING_COLUMN_MISSING_STATS\" : {\n    \"message\" : [\n      \"Clustering requires clustering columns to have stats. Couldn't find clustering column(s) '<columns>' in stats schema:\\n<schema>\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_CLUSTERING_REPLACE_TABLE_WITH_PARTITIONED_TABLE\" : {\n    \"message\" : [\n      \"Replacing a clustered Delta table with a partitioned table is not allowed.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_CLUSTERING_WITH_PARTITION_PREDICATE\" : {\n    \"message\" : [\n      \"OPTIMIZE command for Delta table with clustering doesn't support partition predicates. Please remove the predicates: <predicates>.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_CLUSTERING_WITH_ZORDER_BY\" : {\n    \"message\" : [\n      \"OPTIMIZE command for Delta table with clustering cannot specify ZORDER BY. Please remove ZORDER BY (<zOrderBy>).\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_CLUSTER_BY_INVALID_NUM_COLUMNS\" : {\n    \"message\" : [\n      \"CLUSTER BY supports up to <numColumnsLimit> clustering columns, but the table has <actualNumColumns> clustering columns. Please remove the extra clustering columns.\"\n    ],\n    \"sqlState\" : \"54000\"\n  },\n  \"DELTA_CLUSTER_BY_WITH_PARTITIONED_BY\" : {\n    \"message\" : [\n      \"Clustering and partitioning cannot both be specified. Please remove partitionedBy if you want to create a Delta table with clustering.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_PARTITIONED_COLUMN\" : {\n    \"message\" : [\n      \"Data skipping is not supported for partition column '<column>'.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_TYPE\" : {\n    \"message\" : [\n      \"Data skipping is not supported for column '<column>' of type <type>.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_COLUMN_MAPPING_MAX_COLUMN_ID_NOT_SET\" : {\n    \"message\" : [\n      \"The max column id property (<prop>) is not set on a column mapping enabled table.\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_COLUMN_MAPPING_MAX_COLUMN_ID_NOT_SET_CORRECTLY\" : {\n    \"message\" : [\n      \"The max column id property (<prop>) on a column mapping enabled table is <tableMax>, which cannot be smaller than the max column id for all fields (<fieldMax>).\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_COLUMN_MISSING_DATA_TYPE\" : {\n    \"message\" : [\n      \"The data type of the column <colName> was not provided.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_COLUMN_NOT_FOUND\" : {\n    \"message\" : [\n      \"Unable to find the column `<columnName>` given [<columnList>]\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_COLUMN_NOT_FOUND_IN_MERGE\" : {\n    \"message\" : [\n      \"Unable to find the column '<targetCol>' of the target table from the INSERT columns: <colNames>. INSERT clause must specify value for all the columns of the target table.\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_COLUMN_NOT_FOUND_IN_SCHEMA\" : {\n    \"message\" : [\n      \"Couldn't find column <columnName> in:\\n<tableSchema>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_COLUMN_PATH_NOT_NESTED\" : {\n    \"message\" : [\n      \"Expected <columnPath> to be a nested data type, but found <other>. Was looking for the\",\n      \"index of <column> in a nested field.\",\n      \"Schema:\",\n      \"<schema>\"\n    ],\n    \"sqlState\" : \"42704\"\n  },\n  \"DELTA_COLUMN_STRUCT_TYPE_MISMATCH\" : {\n    \"message\" : [\n      \"Struct column <source> cannot be inserted into a <targetType> field <targetField> in <targetTable>.\"\n    ],\n    \"sqlState\" : \"2200G\"\n  },\n  \"DELTA_COMMAND_INVARIANT_VIOLATION\" : {\n    \"message\" : [\n      \"A command internal invariant was violated in '<operation>'.\",\n      \"Please retry the command.\",\n      \"Exception reference: <uuid>.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE\" : {\n    \"message\" : [\n      \"Cannot handle commit of table within redirect table state '<state>'.\"\n    ],\n    \"sqlState\" : \"42P01\"\n  },\n  \"DELTA_COMPLEX_TYPE_COLUMN_CONTAINS_NULL_TYPE\" : {\n    \"message\" : [\n      \" Found nested NullType in column <columName> which is of <dataType>. Delta doesn't support writing NullType in complex types.\"\n    ],\n    \"sqlState\" : \"22005\"\n  },\n  \"DELTA_CONCURRENT_APPEND\" : {\n    \"message\" : [\n      \"Transaction conflict detected. a concurrent <operation> added data to table <tableName> committed at version <version>.\"\n    ],\n    \"subClass\" : {\n      \"WITHOUT_HINT\" : {\n        \"message\" : [\n          \"The concurrent operation modified data that should have been read by this operation. Please retry the operation. Refer to <docLink> for more information.\"\n        ]\n      },\n      \"WITH_PARTITION_HINT\" : {\n        \"message\" : [\n          \"The concurrent operation modified data in the partition <partitionValues> that should have been read by this operation. Please retry the operation. Refer to <docLink> for more information.\"\n        ]\n      }\n    },\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_CONCURRENT_DELETE_DELETE\" : {\n    \"message\" : [\n      \"Transaction conflict detected, a concurrent <operation> deleted data from table <tableName> (committed at version <version>) that this transaction attempted to delete.\"\n    ],\n    \"subClass\" : {\n      \"WITHOUT_HINT\" : {\n        \"message\" : [\n          \"The concurrent operation deleted data that was read by this operation. Please retry the operation. Refer to <docLink> for more information.\"\n        ]\n      },\n      \"WITH_PARTITION_HINT\" : {\n        \"message\" : [\n          \"The concurrent operation deleted data in the partition <partitionValues> that was read by this operation. Please retry the operation. Refer to <docLink> for more information.\"\n        ]\n      }\n    },\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_CONCURRENT_DELETE_READ\" : {\n    \"message\" : [\n      \"Transaction conflict detected, a concurrent <operation> deleted data from table <tableName> (committed at version <version>) that this transaction read.\"\n    ],\n    \"subClass\" : {\n      \"WITHOUT_HINT\" : {\n        \"message\" : [\n          \"The concurrent operation deleted data that was read by this operation. Please retry the operation. Refer to <docLink> for more information.\"\n        ]\n      },\n      \"WITH_PARTITION_HINT\" : {\n        \"message\" : [\n          \"The concurrent operation deleted data in the partition <partitionValues> that was read by this operation. Please retry the operation. Refer to <docLink> for more information.\"\n        ]\n      }\n    },\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_CONCURRENT_TRANSACTION\" : {\n    \"message\" : [\n      \"ConcurrentTransactionException: This error occurs when multiple streaming queries are using the same checkpoint to write into this table. Did you run multiple instances of the same streaming query at the same time?<conflictingCommit>\\nRefer to <docLink> for more details.\"\n    ],\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_CONCURRENT_WRITE\" : {\n    \"message\" : [\n      \"ConcurrentWriteException: A concurrent transaction has written new data since the current transaction read the table. Please try the operation again.<conflictingCommit>\\nRefer to <docLink> for more details.\"\n    ],\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG\" : {\n    \"message\" : [\n      \"This Delta operation requires the SparkSession to be configured with the\",\n      \"DeltaSparkSessionExtension and the DeltaCatalog. Please set the necessary\",\n      \"configurations when creating the SparkSession as shown below.\",\n      \"\",\n      \"  SparkSession.builder()\",\n      \"    .config(\\\"spark.sql.extensions\\\", \\\"<sparkSessionExtensionName>\\\")\",\n      \"    .config(\\\"<catalogKey>\\\", \\\"<catalogClassName>\\\")\",\n      \"    ...\",\n      \"    .getOrCreate()\",\n      \"\",\n      \"If you are using spark-shell/pyspark/spark-submit, you can add the required configurations to the command as show below:\",\n      \"--conf spark.sql.extensions=<sparkSessionExtensionName> --conf <catalogKey>=<catalogClassName>\",\n      \"\"\n    ],\n    \"sqlState\" : \"56038\"\n  },\n  \"DELTA_CONFLICT_SET_COLUMN\" : {\n    \"message\" : [\n      \"There is a conflict from these SET columns: <columnList>.\"\n    ],\n    \"sqlState\" : \"42701\"\n  },\n  \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\" : {\n    \"message\" : [\n      \"During <command>, configuration \\\"<configuration>\\\" cannot be set from the command. Please remove it from the TBLPROPERTIES clause and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_SESSION\" : {\n    \"message\" : [\n      \"During <command>, configuration \\\"<configuration>\\\" cannot be set from the SparkSession configurations. Please unset it by running `spark.conf.unset(\\\"<configuration>\\\")` and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_CONSTRAINT_ALREADY_EXISTS\" : {\n    \"message\" : [\n      \"Constraint '<constraintName>' already exists. Please delete the old constraint first.\",\n      \"Old constraint:\",\n      \"<oldConstraint>\"\n    ],\n    \"sqlState\" : \"42710\"\n  },\n  \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\" : {\n    \"message\" : [\n      \"Column <columnName> has data type <columnType> and cannot be altered to data type <dataType> because this column is referenced by the following check constraint(s):\",\n      \"<constraints>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\" : {\n    \"message\" : [\n      \"Cannot alter column <columnName> because this column is referenced by the following check constraint(s):\",\n      \"<constraints>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_CONSTRAINT_DOES_NOT_EXIST\" : {\n    \"message\" : [\n      \"Cannot drop nonexistent constraint <constraintName> from table <tableName>. To avoid throwing an error, provide the parameter IF EXISTS or set the SQL session configuration <config> to <confValue>.\"\n    ],\n    \"sqlState\" : \"42704\"\n  },\n  \"DELTA_CONVERSION_NO_PARTITION_FOUND\" : {\n    \"message\" : [\n      \"Found no partition information in the catalog for table <tableName>. Have you run \\\"MSCK REPAIR TABLE\\\" on your table to discover partitions?\"\n    ],\n    \"sqlState\" : \"42KD6\"\n  },\n  \"DELTA_CONVERSION_UNSUPPORTED_COLUMN_MAPPING\" : {\n    \"message\" : [\n      \"The configuration '<config>' cannot be set to `<mode>` when using CONVERT TO DELTA.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CONVERT_NON_PARQUET_TABLE\" : {\n    \"message\" : [\n      \"CONVERT TO DELTA only supports parquet tables, but you are trying to convert a <sourceName> source: <tableId>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CONVERT_TO_DELTA_ROW_TRACKING_WITHOUT_STATS\" : {\n    \"message\" : [\n      \"Cannot enable row tracking without collecting statistics.\",\n      \"If you want to enable row tracking, do the following:\",\n      \"  1. Enable statistics collection by running the command\",\n      \"     SET <statisticsCollectionPropertyKey> = true\",\n      \"  2. Run CONVERT TO DELTA without the NO STATISTICS option.\",\n      \"\",\n      \"If you do not want to collect statistics, disable row tracking:\",\n      \"  1. Deactivate enabling the table feature by default by running the command:\",\n      \"     RESET <rowTrackingTableFeatureDefaultKey>\",\n      \"  2. Deactivate the table property by default by running:\",\n      \"     SET <rowTrackingDefaultPropertyKey> = false\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_CREATE_EXTERNAL_TABLE_WITHOUT_SCHEMA\" : {\n    \"message\" : [\n      \"\",\n      \"You are trying to create an external table <tableName>\",\n      \"from `<path>` using Delta, but the schema is not specified when the\",\n      \"input path is empty.\",\n      \"\",\n      \"To learn more about Delta, see <docLink>\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_CREATE_EXTERNAL_TABLE_WITHOUT_TXN_LOG\" : {\n    \"message\" : [\n      \"You are trying to create an external table <tableName> from `<path>` using Delta, but there is no transaction log present at `<logPath>`. Check the upstream job to make sure that it is writing using format(\\\"delta\\\") and that the path is the root of the table.\",\n      \"To learn more about Delta, see <docLink>\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_CREATE_TABLE_IDENTIFIER_LOCATION_MISMATCH\" : {\n    \"message\" : [\n      \"Creating path-based Delta table with a different location isn't supported. Identifier: <identifier>, Location: <location>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_CREATE_TABLE_MISSING_TABLE_NAME_OR_LOCATION\" : {\n    \"message\" : [\n      \"Table name or location has to be specified.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_CREATE_TABLE_SCHEME_MISMATCH\" : {\n    \"message\" : [\n      \"The specified schema does not match the existing schema at <path>.\",\n      \"\",\n      \"== Specified ==\",\n      \"<specifiedSchema>\",\n      \"\",\n      \"== Existing ==\",\n      \"<existingSchema>\",\n      \"\",\n      \"== Differences ==\",\n      \"<schemaDifferences>\",\n      \"\",\n      \"If your intention is to keep the existing schema, you can omit the\",\n      \"schema from the create table command. Otherwise please ensure that\",\n      \"the schema matches.\"\n    ],\n    \"sqlState\" : \"42KD7\"\n  },\n  \"DELTA_CREATE_TABLE_SET_CLUSTERING_TABLE_FEATURE_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"Cannot enable <tableFeature> table feature using TBLPROPERTIES. Please use CREATE OR REPLACE TABLE CLUSTER BY to create a Delta table with clustering.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_CREATE_TABLE_WITH_DIFFERENT_CLUSTERING\" : {\n    \"message\" : [\n      \"The specified clustering columns do not match the existing clustering columns at <path>.\",\n      \"== Specified ==\",\n      \"<specifiedColumns>\",\n      \"== Existing ==\",\n      \"<existingColumns>\",\n      \"\"\n    ],\n    \"sqlState\" : \"42KD7\"\n  },\n  \"DELTA_CREATE_TABLE_WITH_DIFFERENT_PARTITIONING\" : {\n    \"message\" : [\n      \"The specified partitioning does not match the existing partitioning at <path>.\",\n      \"\",\n      \"== Specified ==\",\n      \"<specifiedColumns>\",\n      \"\",\n      \"== Existing ==\",\n      \"<existingColumns>\",\n      \"\"\n    ],\n    \"sqlState\" : \"42KD7\"\n  },\n  \"DELTA_CREATE_TABLE_WITH_DIFFERENT_PROPERTY\" : {\n    \"message\" : [\n      \"The specified properties do not match the existing properties at <path>.\",\n      \"\",\n      \"== Specified ==\",\n      \"<specifiedProperties>\",\n      \"\",\n      \"== Existing ==\",\n      \"<existingProperties>\",\n      \"\"\n    ],\n    \"sqlState\" : \"42KD7\"\n  },\n  \"DELTA_CREATE_TABLE_WITH_NON_EMPTY_LOCATION\" : {\n    \"message\" : [\n      \"Cannot create table ('<tableId>'). The associated location ('<tableLocation>') is not empty and also not a Delta table.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_DATA_CHANGE_FALSE\" : {\n    \"message\" : [\n      \"Cannot change table metadata because the 'dataChange' option is set to false. Attempted operation: '<op>'.\"\n    ],\n    \"sqlState\" : \"0AKDE\"\n  },\n  \"DELTA_DELETION_VECTOR_CARDINALITY_MISMATCH\" : {\n    \"message\" : [\n      \"Deletion vector integrity check failed. Encountered a cardinality mismatch.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_DELETION_VECTOR_CHECKSUM_MISMATCH\" : {\n    \"message\" : [\n      \"Could not verify deletion vector integrity, CRC checksum verification failed.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_DELETION_VECTOR_INVALID_ROW_INDEX\" : {\n    \"message\" : [\n      \"Deletion vector integrity check failed. Encountered an invalid row index.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_DELETION_VECTOR_MISSING_NUM_RECORDS\" : {\n    \"message\" : [\n      \"It is invalid to commit files with deletion vectors that are missing the numRecords statistic.\"\n    ],\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_DELETION_VECTOR_SIZE_MISMATCH\" : {\n    \"message\" : [\n      \"Deletion vector integrity check failed. Encountered a size mismatch.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_DOMAIN_METADATA_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Detected DomainMetadata action(s) for domains <domainNames>, but DomainMetadataTableFeature is not enabled.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_DROP_COLUMN_AT_INDEX_LESS_THAN_ZERO\" : {\n    \"message\" : [\n      \"Index <columnIndex> to drop column is lower than 0\"\n    ],\n    \"sqlState\" : \"42KD8\"\n  },\n  \"DELTA_DROP_COLUMN_ON_SINGLE_FIELD_SCHEMA\" : {\n    \"message\" : [\n      \"Cannot drop column from a schema with a single column. Schema:\",\n      \"<schema>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_DUPLICATE_COLUMNS_FOUND\" : {\n    \"message\" : [\n      \"Found duplicate column(s) <coltype>: <duplicateCols>\"\n    ],\n    \"sqlState\" : \"42711\"\n  },\n  \"DELTA_DUPLICATE_COLUMNS_ON_INSERT\" : {\n    \"message\" : [\n      \"Duplicate column names in INSERT clause\"\n    ],\n    \"sqlState\" : \"42701\"\n  },\n  \"DELTA_DUPLICATE_COLUMNS_ON_UPDATE_TABLE\" : {\n    \"message\" : [\n      \"<message>\",\n      \"Please remove duplicate columns before you update your table.\"\n    ],\n    \"sqlState\" : \"42701\"\n  },\n  \"DELTA_DUPLICATE_DATA_SKIPPING_COLUMNS\" : {\n    \"message\" : [\n      \"Duplicated data skipping columns found: <columns>.\"\n    ],\n    \"sqlState\" : \"42701\"\n  },\n  \"DELTA_DUPLICATE_DOMAIN_METADATA_INTERNAL_ERROR\" : {\n    \"message\" : [\n      \"Internal error: two DomainMetadata actions within the same transaction have the same domain <domainName>\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_DV_HISTOGRAM_DESERIALIZATON\" : {\n    \"message\" : [\n      \"Could not deserialize the deleted record counts histogram during table integrity verification.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_DYNAMIC_PARTITION_OVERWRITE_DISABLED\" : {\n    \"message\" : [\n      \"Dynamic partition overwrite mode is specified by session config or write options, but it is disabled by `delta.dynamicPartitionOverwrite.enabled=false`.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_EMPTY_DATA\" : {\n    \"message\" : [\n      \"Data used in creating the Delta table doesn't have any columns.\"\n    ],\n    \"sqlState\" : \"428GU\"\n  },\n  \"DELTA_EMPTY_DIRECTORY\" : {\n    \"message\" : [\n      \"No file found in the directory: <directory>.\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_ENABLING_COLUMN_MAPPING_DISALLOWED_WHEN_COLUMN_MAPPING_METADATA_ALREADY_EXISTS\" : {\n    \"message\" : [\n      \"Enabling column mapping when column mapping metadata is already present in schema is not supported.\",\n      \"To use column mapping, create a new table and reload the data into it.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_EXCEED_CHAR_VARCHAR_LIMIT\" : {\n    \"message\" : [\n      \"Value \\\"<value>\\\" exceeds char/varchar type length limitation. Failed check: <expr>.\"\n    ],\n    \"sqlState\" : \"22001\"\n  },\n  \"DELTA_EXPRESSIONS_NOT_FOUND_IN_GENERATED_COLUMN\" : {\n    \"message\" : [\n      \"Cannot find the expressions in the generated column <columnName>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_EXTRACT_REFERENCES_FIELD_NOT_FOUND\" : {\n    \"message\" : [\n      \"Field <fieldName> could not be found when extracting references.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_FAILED_CAST_PARTITION_VALUE\" : {\n    \"message\" : [\n      \"Failed to cast partition value `<value>` to <dataType>\"\n    ],\n    \"sqlState\" : \"22018\"\n  },\n  \"DELTA_FAILED_FIND_ATTRIBUTE_IN_OUTPUT_COLUMNS\" : {\n    \"message\" : [\n      \"Could not find <newAttributeName> among the existing target output <targetOutputColumns>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_FAILED_FIND_PARTITION_COLUMN_IN_OUTPUT_PLAN\" : {\n    \"message\" : [\n      \"Could not find <partitionColumn> in output plan.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_FAILED_INFER_SCHEMA\" : {\n    \"message\" : [\n      \"Failed to infer schema from the given list of files.\"\n    ],\n    \"sqlState\" : \"42KD9\"\n  },\n  \"DELTA_FAILED_MERGE_SCHEMA_FILE\" : {\n    \"message\" : [\n      \"Failed to merge schema of file <file>:\",\n      \"<schema>\"\n    ],\n    \"sqlState\" : \"42KDA\"\n  },\n  \"DELTA_FAILED_READ_FILE_FOOTER\" : {\n    \"message\" : [\n      \"Could not read footer for file: <currentFile>\"\n    ],\n    \"sqlState\" : \"KD001\"\n  },\n  \"DELTA_FAILED_RECOGNIZE_PREDICATE\" : {\n    \"message\" : [\n      \"Cannot recognize the predicate '<predicate>'\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_FAILED_SCAN_WITH_HISTORICAL_VERSION\" : {\n    \"message\" : [\n      \"Expect a full scan of the latest version of the Delta source, but found a historical scan of version <historicalVersion>\"\n    ],\n    \"sqlState\" : \"KD002\"\n  },\n  \"DELTA_FAILED_TO_MERGE_FIELDS\" : {\n    \"message\" : [\n      \"Failed to merge fields '<currentField>' and '<updateField>'\"\n    ],\n    \"sqlState\" : \"22005\"\n  },\n  \"DELTA_FAIL_RELATIVIZE_PATH\" : {\n    \"message\" : [\n      \"Failed to relativize the path (<path>). This can happen when absolute paths make\",\n      \"it into the transaction log, which start with the scheme\",\n      \"s3://, wasbs:// or adls://.\",\n      \"\",\n      \"If this table is NOT USED IN PRODUCTION, you can set the SQL configuration\",\n      \"<config> to true.\",\n      \"Using this SQL configuration could lead to accidental data loss, therefore we do\",\n      \"not recommend the use of this flag unless this is for testing purposes.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_FEATURES_PROTOCOL_METADATA_MISMATCH\" : {\n    \"message\" : [\n      \"Unable to operate on this table because the following table features are enabled in metadata but not listed in protocol: <features>.\"\n    ],\n    \"sqlState\" : \"KD004\"\n  },\n  \"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\" : {\n    \"message\" : [\n      \"Your table schema requires manually enablement of the following table feature(s): <unsupportedFeatures>.\",\n      \"\",\n      \"To do this, run the following command for each of features listed above:\",\n      \"  ALTER TABLE table_name SET TBLPROPERTIES ('delta.feature.feature_name' = 'supported')\",\n      \"Replace \\\"table_name\\\" and \\\"feature_name\\\" with real values.\",\n      \"\",\n      \"Current supported feature(s): <supportedFeatures>.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_FEATURE_CAN_ONLY_DROP_CHECKPOINT_PROTECTION_WITH_HISTORY_TRUNCATION\" : {\n    \"message\" : [\n      \"Could not drop the Checkpoint Protection feature.\",\n      \"This feature can only be dropped by truncating history.\",\n      \"Please try again with the TRUNCATE HISTORY option:\",\n      \"\",\n      \"    ALTER TABLE table_name DROP FEATURE checkpointProtection TRUNCATE HISTORY\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_FEATURE_DROP_CHECKPOINT_FAILED\" : {\n    \"message\" : [\n      \"Dropping <featureName> failed due to a failure in checkpoint creation.\",\n      \"Please try again later. It the issue persists, contact support.\"\n    ],\n    \"sqlState\" : \"22KD0\"\n  },\n  \"DELTA_FEATURE_DROP_CHECKPOINT_PROTECTION_WAIT_FOR_RETENTION_PERIOD\" : {\n    \"message\" : [\n      \"The operation did not succeed because there are still traces of dropped features \",\n      \"in the table history. CheckpointProtection cannot be dropped until these historical\",\n      \"versions have expired.\",\n      \"\",\n      \"To drop CheckpointProtection, please wait for the historical versions to\",\n      \"expire, and then repeat this command. The retention period for historical versions is\",\n      \"currently configured to <truncateHistoryLogRetentionPeriod>.\"\n    ],\n    \"sqlState\" : \"22KD0\"\n  },\n  \"DELTA_FEATURE_DROP_CONFLICT_REVALIDATION_FAIL\" : {\n    \"message\" : [\n      \"Cannot drop feature because a concurrent transaction modified the table.\",\n      \"Please try the operation again.\",\n      \"<concurrentCommit>\"\n    ],\n    \"sqlState\" : \"40000\"\n  },\n  \"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\" : {\n    \"message\" : [\n      \"Cannot drop table feature `<feature>` because some other features (<dependentFeatures>) in this table depends on `<feature>`.\",\n      \"Consider dropping them first before dropping this feature.\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_FEATURE_DROP_FEATURE_IS_DELTA_PROPERTY\" : {\n    \"message\" : [\n      \"Cannot drop `<property>` from this table because this is a delta table property and not a table feature.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\" : {\n    \"message\" : [\n      \"Cannot drop <feature> from this table because it is not currently present in the table's protocol.\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\" : {\n    \"message\" : [\n      \"Cannot drop <feature> because the Delta log contains historical versions that use the feature.\",\n      \"Please wait until the history retention period (<logRetentionPeriodKey>=<logRetentionPeriod>) \",\n      \"has passed since the feature was last active.\",\n      \"\",\n      \"Alternatively, please wait for the TRUNCATE HISTORY retention period to expire (<truncateHistoryLogRetentionPeriod>)\",\n      \"and then run:\",\n      \"    ALTER TABLE table_name DROP FEATURE feature_name TRUNCATE HISTORY\"\n    ],\n    \"sqlState\" : \"22KD0\"\n  },\n  \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"The particular feature does not require history truncation.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_FEATURE_DROP_NONREMOVABLE_FEATURE\" : {\n    \"message\" : [\n      \"Cannot drop <feature> because dropping this feature is not supported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_FEATURE_DROP_UNSUPPORTED_CLIENT_FEATURE\" : {\n    \"message\" : [\n      \"Cannot drop <feature> because it is not supported by this Delta version.\",\n      \"Consider using Delta with a higher version.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\" : {\n    \"message\" : [\n      \"Dropping <feature> was partially successful.\",\n      \"\",\n      \"The feature is now no longer used in the current version of the table. However, the feature\",\n      \"is still present in historical versions of the table. The table feature cannot be dropped\",\n      \"from the table protocol until these historical versions have expired.\",\n      \"\",\n      \"To drop the table feature from the protocol, please wait for the historical versions to\",\n      \"expire, and then repeat this command. The retention period for historical versions is\",\n      \"currently configured as <logRetentionPeriodKey>=<logRetentionPeriod>.\",\n      \"\",\n      \"Alternatively, please wait for the TRUNCATE HISTORY retention period to expire (<truncateHistoryLogRetentionPeriod>)\",\n      \"and then run:\",\n      \"    ALTER TABLE table_name DROP FEATURE feature_name TRUNCATE HISTORY\"\n    ],\n    \"sqlState\" : \"22KD0\"\n  },\n  \"DELTA_FEATURE_REQUIRES_HIGHER_READER_VERSION\" : {\n    \"message\" : [\n      \"Unable to enable table feature <feature> because it requires a higher reader protocol version (current <current>). Consider upgrading the table's reader protocol version to <required>, or to a version which supports reader table features. Refer to <docLink> for more information on table protocol versions.\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_FEATURE_REQUIRES_HIGHER_WRITER_VERSION\" : {\n    \"message\" : [\n      \"Unable to enable table feature <feature> because it requires a higher writer protocol version (current <current>). Consider upgrading the table's writer protocol version to <required>, or to a version which supports writer table features. Refer to <docLink> for more information on table protocol versions.\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_FILE_ALREADY_EXISTS\" : {\n    \"message\" : [\n      \"Existing file path <path>\"\n    ],\n    \"sqlState\" : \"42K04\"\n  },\n  \"DELTA_FILE_LIST_AND_PATTERN_STRING_CONFLICT\" : {\n    \"message\" : [\n      \"Cannot specify both file list and pattern string.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_FILE_NOT_FOUND\" : {\n    \"message\" : [\n      \"File path <path>\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_FILE_OR_DIR_NOT_FOUND\" : {\n    \"message\" : [\n      \"No such file or directory: <path>\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_FILE_TO_OVERWRITE_NOT_FOUND\" : {\n    \"message\" : [\n      \"File (<path>) to be rewritten not found among candidate files:\\n<pathList>\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_FOUND_MAP_TYPE_COLUMN\" : {\n    \"message\" : [\n      \"A MapType was found. In order to access the key or value of a MapType, specify one\",\n      \"of:\",\n      \"<key> or\",\n      \"<value>\",\n      \"followed by the name of the column (only if that column is a struct type).\",\n      \"e.g. mymap.key.mykey\",\n      \"If the column is a basic type, mymap.key or mymap.value is sufficient.\",\n      \"Schema:\",\n      \"<schema>\"\n    ],\n    \"sqlState\" : \"KD003\"\n  },\n  \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\" : {\n    \"message\" : [\n      \"Column <columnName> has data type <columnType> and cannot be altered to data type <dataType> because this column is referenced by the following generated column(s):\",\n      \"<generatedColumns>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_GENERATED_COLUMNS_DEPENDENT_COLUMN_CHANGE\" : {\n    \"message\" : [\n      \"Cannot alter column <columnName> because this column is referenced by the following generated column(s):\",\n      \"<generatedColumns>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_GENERATED_COLUMNS_EXPR_TYPE_MISMATCH\" : {\n    \"message\" : [\n      \"The expression type of the generated column <columnName> is <expressionType>, but the column type is <columnType>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_GENERATED_COLUMN_UPDATE_TYPE_MISMATCH\" : {\n    \"message\" : [\n      \"Column <currentName> is a generated column or a column used by a generated column. The data type is <currentDataType> and cannot be converted to data type <updateDataType>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_ICEBERG_COMPAT_VIOLATION\" : {\n    \"message\" : [\n      \"The validation of IcebergCompatV<version> has failed.\"\n    ],\n    \"subClass\" : {\n      \"CHANGE_VERSION_NEED_REWRITE\" : {\n        \"message\" : [\n          \"Changing to IcebergCompatV<newVersion> requires rewriting the table. Please run REORG TABLE APPLY (UPGRADE UNIFORM ('ICEBERG_COMPAT_VERSION = <newVersion>'));\",\n          \"Note that REORG enables table feature IcebergCompatV<newVersion> and other Delta lake clients without that table feature support may not be able to write to the table.\"\n        ]\n      },\n      \"COMPAT_VERSION_NOT_SUPPORTED\" : {\n        \"message\" : [\n          \"IcebergCompatVersion = <version> is not supported. Supported versions are between 1 and <maxVersion> \"\n        ]\n      },\n      \"DELETION_VECTORS_NOT_PURGED\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> requires Deletion Vectors to be completely purged from the table. Please run the REORG TABLE APPLY (PURGE) command.\"\n        ]\n      },\n      \"DELETION_VECTORS_SHOULD_BE_DISABLED\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> requires Deletion Vectors to be disabled on the table first. Then run REORG PURGE command to purge the Deletion Vectors on the table.\"\n        ]\n      },\n      \"DISABLING_REQUIRED_TABLE_FEATURE\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> requires feature <feature> to be supported and enabled. You cannot drop it from the table. Instead, please disable IcebergCompatV<version> first.\"\n        ]\n      },\n      \"FILES_NOT_ICEBERG_COMPAT\" : {\n        \"message\" : [\n          \"Enabling Uniform Iceberg with IcebergCompatV<version> requires all files to be iceberg compatible.\",\n          \"There are <addFilesCount> files in table version <tableVersion> and <addFilesWithoutTag> files are not iceberg compatible, which is usually a result of concurrent write.\",\n          \"Please run the REORG TABLE table APPLY (UPGRADE UNIFORM (ICEBERG_COMPAT_VERSION=<version>) command again.\"\n        ]\n      },\n      \"INCOMPATIBLE_TABLE_FEATURE\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> is incompatible with feature <feature>.\"\n        ]\n      },\n      \"MISSING_REQUIRED_TABLE_FEATURE\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> requires feature <feature> to be supported and enabled.\"\n        ]\n      },\n      \"REPLACE_TABLE_CHANGE_PARTITION_NAMES\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> doesn't support replacing partitioned tables with a differently-named partition spec, because Iceberg-Spark 1.1.0 doesn't.\",\n          \"Prev Partition Spec: <prevPartitionSpec>\",\n          \"New Partition Spec: <newPartitionSpec>\"\n        ]\n      },\n      \"REWRITE_DATA_FAILED\" : {\n        \"message\" : [\n          \"Rewriting data to IcebergCompatV<version> failed.\",\n          \"Please run the REORG TABLE table APPLY (UPGRADE UNIFORM (ICEBERG_COMPAT_VERSION=<version>) command again.\"\n        ]\n      },\n      \"UNSUPPORTED_DATA_TYPE\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> does not support the data type <dataType> in your schema. Your schema:\",\n          \"<schema>\"\n        ]\n      },\n      \"UNSUPPORTED_PARTITION_DATA_TYPE\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> does not support the data type <dataType> for partition columns in your schema. Your partition schema:\",\n          \"<schema>\"\n        ]\n      },\n      \"UNSUPPORTED_TYPE_WIDENING\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> is incompatible with a type change applied to this table:\",\n          \"Field <fieldPath> was changed from <prevType> to <newType>.\"\n        ]\n      },\n      \"VERSION_MUTUAL_EXCLUSIVE\" : {\n        \"message\" : [\n          \"Only one IcebergCompat version can be enabled, please explicitly disable all other IcebergCompat versions that are not needed.\"\n        ]\n      },\n      \"WRONG_REQUIRED_TABLE_PROPERTY\" : {\n        \"message\" : [\n          \"IcebergCompatV<version> requires table property '<key>' to be set to '<requiredValue>'. Current value: '<actualValue>'.\"\n        ]\n      }\n    },\n    \"sqlState\" : \"KD00E\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_ALTER_COLUMN_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"ALTER TABLE ALTER COLUMN is not supported for IDENTITY columns.\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_ALTER_NON_DELTA_FORMAT\" : {\n    \"message\" : [\n      \"ALTER TABLE ALTER COLUMN SYNC IDENTITY is only supported by Delta.\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_ALTER_NON_IDENTITY_COLUMN\" : {\n    \"message\" : [\n      \"ALTER TABLE ALTER COLUMN SYNC IDENTITY cannot be called on non IDENTITY columns.\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_EXPLICIT_INSERT_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Providing values for GENERATED ALWAYS AS IDENTITY column <colName> is not supported.\"\n    ],\n    \"sqlState\" : \"42808\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_ILLEGAL_STEP\" : {\n    \"message\" : [\n      \"IDENTITY column step cannot be 0.\"\n    ],\n    \"sqlState\" : \"42611\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_PARTITION_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"PARTITIONED BY IDENTITY column <colName> is not supported.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_REPLACE_COLUMN_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"ALTER TABLE REPLACE COLUMNS is not supported for table with IDENTITY columns.\"\n    ],\n    \"sqlState\" : \"429BQ\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_UNSUPPORTED_DATA_TYPE\" : {\n    \"message\" : [\n      \"DataType <dataType> is not supported for IDENTITY columns.\"\n    ],\n    \"sqlState\" : \"428H2\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_UPDATE_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"UPDATE on IDENTITY column <colName> is not supported.\"\n    ],\n    \"sqlState\" : \"42808\"\n  },\n  \"DELTA_IDENTITY_COLUMNS_WITH_GENERATED_EXPRESSION\" : {\n    \"message\" : [\n      \"IDENTITY column cannot be specified with a generated column expression.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_ILLEGAL_FILE_FOUND\" : {\n    \"message\" : [\n      \"Illegal files found in a dataChange = false transaction. Files: <file>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_ILLEGAL_OPTION\" : {\n    \"message\" : [\n      \"Invalid value '<input>' for option '<name>', <explain>\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_ILLEGAL_USAGE\" : {\n    \"message\" : [\n      \"The usage of <option> is not allowed when <operation> a Delta table.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_INCONSISTENT_LOGSTORE_CONFS\" : {\n    \"message\" : [\n      \"(<setKeys>) cannot be set to different values. Please only set one of them, or set them to the same value.\"\n    ],\n    \"sqlState\" : \"F0000\"\n  },\n  \"DELTA_INCORRECT_ARRAY_ACCESS\" : {\n    \"message\" : [\n      \"Incorrectly accessing an ArrayType. Use arrayname.element.elementname position to\",\n      \"add to an array.\"\n    ],\n    \"sqlState\" : \"KD003\"\n  },\n  \"DELTA_INCORRECT_ARRAY_ACCESS_BY_NAME\" : {\n    \"message\" : [\n      \"An ArrayType was found. In order to access elements of an ArrayType, specify\",\n      \"<rightName> instead of <wrongName>.\",\n      \"Schema:\",\n      \"<schema>\"\n    ],\n    \"sqlState\" : \"KD003\"\n  },\n  \"DELTA_INCORRECT_LOG_STORE_IMPLEMENTATION\" : {\n    \"message\" : [\n      \"The error typically occurs when the default LogStore implementation, that\",\n      \"is, HDFSLogStore, is used to write into a Delta table on a non-HDFS storage system.\",\n      \"In order to get the transactional ACID guarantees on table updates, you have to use the\",\n      \"correct implementation of LogStore that is appropriate for your storage system.\",\n      \"See <docLink> for details.\",\n      \"\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_INDEX_LARGER_OR_EQUAL_THAN_STRUCT\" : {\n    \"message\" : [\n      \"Index <index> to drop column equals to or is larger than struct length: <length>\"\n    ],\n    \"sqlState\" : \"42KD8\"\n  },\n  \"DELTA_INDEX_LARGER_THAN_STRUCT\" : {\n    \"message\" : [\n      \"Index <index> to add column <columnName> is larger than struct length: <length>\"\n    ],\n    \"sqlState\" : \"42KD8\"\n  },\n  \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\" : {\n    \"message\" : [\n      \"Cannot write to '<tableName>', <columnName>; target table has <numColumns> column(s) but the inserted data has <insertColumns> column(s)\"\n    ],\n    \"sqlState\" : \"42802\"\n  },\n  \"DELTA_INSERT_COLUMN_MISMATCH\" : {\n    \"message\" : [\n      \"Column <columnName> is not specified in INSERT\"\n    ],\n    \"sqlState\" : \"42802\"\n  },\n  \"DELTA_INVALID_AUTO_COMPACT_TYPE\" : {\n    \"message\" : [\n      \"Invalid auto-compact type: <value>. Allowed values are: <allowed>.\"\n    ],\n    \"sqlState\" : \"22023\"\n  },\n  \"DELTA_INVALID_CALENDAR_INTERVAL_EMPTY\" : {\n    \"message\" : [\n      \"Interval cannot be null or blank.\"\n    ],\n    \"sqlState\" : \"2200P\"\n  },\n  \"DELTA_INVALID_CDC_RANGE\" : {\n    \"message\" : [\n      \"CDC range from start <start> to end <end> was invalid. End cannot be before start.\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAME\" : {\n    \"message\" : [\n      \"Attribute name \\\"<columnName>\\\" contains invalid character(s) among \\\" ,;{}()\\\\\\\\n\\\\\\\\t=\\\". Please use alias to rename it.\"\n    ],\n    \"sqlState\" : \"42K05\"\n  },\n  \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAMES\" : {\n    \"message\" : [\n      \"Found invalid character(s) among ' ,;{}()\\\\n\\\\t=' in the column names of your schema.\",\n      \"Invalid column names: <invalidColumnNames>.\",\n      \"Please use other characters and try again.\",\n      \"Alternatively, enable Column Mapping to keep using these characters.\"\n    ],\n    \"sqlState\" : \"42K05\"\n  },\n  \"DELTA_INVALID_CHECK_CONSTRAINT_REFERENCES\" : {\n    \"message\" : [\n      \"Found <colName> in CHECK constraint. A check constraint cannot use a non-existent column.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_INVALID_CLONE_PATH\" : {\n    \"message\" : [\n      \"The target location for CLONE needs to be an absolute path or table name. Use an\",\n      \"absolute path instead of <path>.\"\n    ],\n    \"sqlState\" : \"22KD1\"\n  },\n  \"DELTA_INVALID_COLUMN_NAMES_WHEN_REMOVING_COLUMN_MAPPING\" : {\n    \"message\" : [\n      \"Found invalid character(s) among ' ,;{}()\\\\n\\\\t=' in the column names of your schema.\",\n      \"Invalid column names: <invalidColumnNames>.\",\n      \"Column mapping cannot be removed when there are invalid characters in the column names.\",\n      \"Please rename the columns to remove the invalid characters and execute this command again.\"\n    ],\n    \"sqlState\" : \"42K05\"\n  },\n  \"DELTA_INVALID_COMMITTED_VERSION\" : {\n    \"message\" : [\n      \"The commit succeeded at version <committedVersion>, but post-commit validation detected version <currentVersion>.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_INVALID_FORMAT_FROM_SOURCE_VERSION\" : {\n    \"message\" : [\n      \"Unsupported format. Expected version should be smaller than or equal to <expectedVersion> but was <realVersion>. Please upgrade to newer version of Delta.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_INVALID_GENERATED_COLUMN_REFERENCES\" : {\n    \"message\" : [\n      \"A generated column cannot use a non-existent column or another generated column\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_INVALID_IDEMPOTENT_WRITES_OPTIONS\" : {\n    \"message\" : [\n      \"Invalid options for idempotent Dataframe writes: <reason>\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_INVALID_INTERVAL\" : {\n    \"message\" : [\n      \"<interval> is not a valid INTERVAL.\"\n    ],\n    \"sqlState\" : \"22006\"\n  },\n  \"DELTA_INVALID_INVENTORY_SCHEMA\" : {\n    \"message\" : [\n      \"The schema for the specified INVENTORY does not contain all of the required fields. Required fields are: <expectedSchema>\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_INVALID_ISOLATION_LEVEL\" : {\n    \"message\" : [\n      \"invalid isolation level '<isolationLevel>'\"\n    ],\n    \"sqlState\" : \"25000\"\n  },\n  \"DELTA_INVALID_LOGSTORE_CONF\" : {\n    \"message\" : [\n      \"(`<classConfig>`) and (`<schemeConfig>`) cannot be set at the same time. Please set only one group of them.\"\n    ],\n    \"sqlState\" : \"F0000\"\n  },\n  \"DELTA_INVALID_MANAGED_TABLE_SYNTAX_NO_SCHEMA\" : {\n    \"message\" : [\n      \"\",\n      \"You are trying to create a managed table <tableName>\",\n      \"using Delta, but the schema is not specified.\",\n      \"\",\n      \"To learn more about Delta, see <docLink>\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_INVALID_PARTITIONING_SCHEMA\" : {\n    \"message\" : [\n      \"\",\n      \"The AddFile contains partitioning schema different from the table's partitioning schema\",\n      \"expected: <neededPartitioning>\",\n      \"actual: <specifiedPartitioning>\",\n      \"To disable this check set <config> to \\\"false\\\"\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_INVALID_PARTITION_COLUMN\" : {\n    \"message\" : [\n      \"<columnName> is not a valid partition column in table <tableName>.\"\n    ],\n    \"sqlState\" : \"42996\"\n  },\n  \"DELTA_INVALID_PARTITION_COLUMN_NAME\" : {\n    \"message\" : [\n      \"Found partition columns having invalid character(s) among \\\" ,;{}()\\n\\t=\\\". Please change the name to your partition columns.\"\n    ],\n    \"sqlState\" : \"42996\"\n  },\n  \"DELTA_INVALID_PARTITION_COLUMN_TYPE\" : {\n    \"message\" : [\n      \"Using column <name> of type <dataType> as a partition column is not supported.\"\n    ],\n    \"sqlState\" : \"42996\"\n  },\n  \"DELTA_INVALID_PARTITION_PATH\" : {\n    \"message\" : [\n      \"A partition path fragment should be the form like `part1=foo/part2=bar`. The partition path: <path>\"\n    ],\n    \"sqlState\" : \"22KD1\"\n  },\n  \"DELTA_INVALID_PROTOCOL_DOWNGRADE\" : {\n    \"message\" : [\n      \"Protocol version cannot be downgraded from (<oldProtocol>) to (<newProtocol>)\"\n    ],\n    \"sqlState\" : \"KD004\"\n  },\n  \"DELTA_INVALID_PROTOCOL_VERSION\" : {\n    \"message\" : [\n      \"Unsupported Delta protocol version: table \\\"<tableNameOrPath>\\\" requires reader version <readerRequired> and writer version <writerRequired>, but Delta Lake \\\"<deltaVersion>\\\" supports reader versions <supportedReaders> and writer versions <supportedWriters>. Please upgrade to a newer release.\"\n    ],\n    \"sqlState\" : \"KD004\"\n  },\n  \"DELTA_INVALID_SOURCE_OFFSET_FORMAT\" : {\n    \"message\" : [\n      \"The stored source offset format is invalid\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_INVALID_SOURCE_VERSION\" : {\n    \"message\" : [\n      \"sourceVersion(<version>) is invalid\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_INVALID_TABLE_VALUE_FUNCTION\" : {\n    \"message\" : [\n      \"Function <function> is an unsupported table valued function for CDC reads.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_INVALID_TIMESTAMP_FORMAT\" : {\n    \"message\" : [\n      \"The provided timestamp <timestamp> does not match the expected syntax <format>.\"\n    ],\n    \"sqlState\" : \"22007\"\n  },\n  \"DELTA_INVALID_V1_TABLE_CALL\" : {\n    \"message\" : [\n      \"<callVersion> call is not expected with path based <tableVersion>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_ITERATOR_ALREADY_CLOSED\" : {\n    \"message\" : [\n      \"Iterator is closed\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_LOG_ALREADY_EXISTS\" : {\n    \"message\" : [\n      \"A Delta log already exists at <path>\"\n    ],\n    \"sqlState\" : \"42K04\"\n  },\n  \"DELTA_LOG_FILE_NOT_FOUND\" : {\n    \"message\" : [\n      \"Unable to retrieve the delta log files to construct table version <version> starting from checkpoint version <checkpointVersion> at <logPath>.\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_LOG_FILE_NOT_FOUND_FOR_STREAMING_SOURCE\" : {\n    \"message\" : [\n      \"If you never deleted it, it's likely your query is lagging behind. Please delete its checkpoint to restart from scratch. To avoid this happening again, you can update your retention policy of your Delta table\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\" : {\n    \"message\" : [\n      \"Materialized <rowTrackingColumn> column name missing for <tableName>.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_MAX_ARRAY_SIZE_EXCEEDED\" : {\n    \"message\" : [\n      \"Please use a limit less than Int.MaxValue - 8.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_MAX_COMMIT_RETRIES_EXCEEDED\" : {\n    \"message\" : [\n      \"This commit has failed as it has been tried <numAttempts> times but did not succeed.\",\n      \"This can be caused by the Delta table being committed continuously by many concurrent\",\n      \"commits.\",\n      \"\",\n      \"Commit started at version: <startVersion>\",\n      \"Commit failed at version: <failVersion>\",\n      \"Number of actions attempted to commit: <numActions>\",\n      \"Total time spent attempting this commit: <timeSpent> ms\"\n    ],\n    \"sqlState\" : \"40000\"\n  },\n  \"DELTA_MERGE_ADD_VOID_COLUMN\" : {\n    \"message\" : [\n      \"Cannot add column <newColumn> with type VOID. Please explicitly specify a non-void type.\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\" : {\n    \"message\" : [\n      \"Failed to merge incompatible data types <currentDataType> and <updateDataType>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\" : {\n    \"message\" : [\n      \"Failed to merge decimal types with incompatible <decimalRanges>\"\n    ],\n    \"sqlState\" : \"42806\"\n  },\n  \"DELTA_MERGE_MATERIALIZE_SOURCE_FAILED_REPEATEDLY\" : {\n    \"message\" : [\n      \"Keeping the source of the MERGE statement materialized has failed repeatedly.\"\n    ],\n    \"sqlState\" : \"25000\"\n  },\n  \"DELTA_MERGE_MISSING_WHEN\" : {\n    \"message\" : [\n      \"There must be at least one WHEN clause in a MERGE statement.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_MERGE_RESOLVED_ATTRIBUTE_MISSING_FROM_INPUT\" : {\n    \"message\" : [\n      \"Resolved attribute(s) <missingAttributes> missing from <input> in operator <merge>\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_MERGE_SOURCE_CACHED_DURING_EXECUTION\" : {\n    \"message\" : [\n      \"The MERGE operation failed because (part of) the source plan was cached while the MERGE operation was running.\"\n    ],\n    \"sqlState\" : \"25000\"\n  },\n  \"DELTA_MERGE_UNEXPECTED_ASSIGNMENT_KEY\" : {\n    \"message\" : [\n      \"Unexpected assignment key: <unexpectedKeyClass> - <unexpectedKeyObject>\"\n    ],\n    \"sqlState\" : \"22005\"\n  },\n  \"DELTA_MERGE_UNRESOLVED_EXPRESSION\" : {\n    \"message\" : [\n      \"Cannot resolve <sqlExpr> in <clause> given columns <cols>.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_METADATA_ABSENT\" : {\n    \"message\" : [\n      \"Couldn't find Metadata while committing the first version of the Delta table.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_METADATA_ABSENT_EXISTING_CATALOG_TABLE\" : {\n    \"message\" : [\n      \"The table <tableName> already exists in the catalog but no metadata could be found for the table at the path <tablePath>.\",\n      \"Did you manually delete files from the _delta_log directory? If so, then you should be able to recreate it as follows. First, drop the table by running `DROP TABLE <tableNameForDropCmd>`. Then, recreate it by running the current command again.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_METADATA_CHANGED\" : {\n    \"message\" : [\n      \"MetadataChangedException: The metadata of the Delta table has been changed by a concurrent update. Please try the operation again.<conflictingCommit>\\nRefer to <docLink> for more details.\"\n    ],\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_METADATA_MISMATCH\" : {\n    \"message\" : [\n      \"A metadata mismatch was detected when writing to the Delta table.\"\n    ],\n    \"subClass\" : {\n      \"OVERWRITE_REQUIRED\" : {\n        \"message\" : [\n          \"To overwrite your schema or change partitioning, please set: '.option(\\\"overwriteSchema\\\", \\\"true\\\")'.\",\n          \"Note that the schema can't be overwritten when using 'replaceWhere'.\"\n        ]\n      },\n      \"PARTITIONING_MISMATCH\" : {\n        \"message\" : [\n          \"Partition columns do not match the partition columns of the table.\",\n          \"Given: <provided>\",\n          \"Table: <original>\",\n          \"\"\n        ]\n      },\n      \"SCHEMA_MISMATCH\" : {\n        \"message\" : [\n          \"A schema mismatch detected when writing to the Delta table (Table ID: <id>).\",\n          \"To enable schema migration using DataFrameWriter or DataStreamWriter, please set: '.option(\\\"mergeSchema\\\", \\\"true\\\")'.\",\n          \"For other operations, set the session configuration spark.databricks.delta.schema.autoMerge.enabled to \\\"true\\\". See the documentation specific to the operation for details.\",\n          \"\",\n          \"Table schema:\",\n          \"<tableSchema>\",\n          \"\",\n          \"Data schema:\",\n          \"<dataSchema>\",\n          \"\"\n        ]\n      }\n    },\n    \"sqlState\" : \"42KDG\"\n  },\n  \"DELTA_MISSING_CHANGE_DATA\" : {\n    \"message\" : [\n      \"Error getting change data for range [<startVersion> , <endVersion>] as change data was not\",\n      \"recorded for version [<version>]. If you've enabled change data feed on this table,\",\n      \"use `DESCRIBE HISTORY` to see when it was first enabled.\",\n      \"Otherwise, to start recording change data, use `ALTER TABLE table_name SET TBLPROPERTIES\",\n      \"(<key>=true)`.\"\n    ],\n    \"sqlState\" : \"KD002\"\n  },\n  \"DELTA_MISSING_COLUMN\" : {\n    \"message\" : [\n      \"Cannot find <columnName> in table columns: <columnList>\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_MISSING_COMMIT_INFO\" : {\n    \"message\" : [\n      \"This table has the feature <featureName> enabled which requires the presence of the CommitInfo action in every commit. However, the CommitInfo action is missing from commit version <version>.\"\n    ],\n    \"sqlState\" : \"KD004\"\n  },\n  \"DELTA_MISSING_COMMIT_TIMESTAMP\" : {\n    \"message\" : [\n      \"This table has the feature <featureName> enabled which requires the presence of commitTimestamp in the CommitInfo action. However, this field has not been set in commit version <version>.\"\n    ],\n    \"sqlState\" : \"KD004\"\n  },\n  \"DELTA_MISSING_DELTA_TABLE\" : {\n    \"message\" : [\n      \"<tableName> is not a Delta table.\"\n    ],\n    \"sqlState\" : \"42P01\"\n  },\n  \"DELTA_MISSING_FILES_UNEXPECTED_VERSION\" : {\n    \"message\" : [\n      \"The stream from your Delta table was expecting process data from version <startVersion>,\",\n      \"but the earliest available version in the _delta_log directory is <earliestVersion>. The files\",\n      \"in the transaction log may have been deleted due to log cleanup. In order to avoid losing\",\n      \"data, we recommend that you restart your stream with a new checkpoint location and to\",\n      \"increase your delta.logRetentionDuration setting, if you have explicitly set it below 30\",\n      \"days.\",\n      \"If you would like to ignore the missed data and continue your stream from where it left\",\n      \"off, you can set the .option(\\\"<option>\\\", \\\"false\\\") as part\",\n      \"of your readStream statement.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_MISSING_ICEBERG_CLASS\" : {\n    \"message\" : [\n      \"Iceberg class was not found. Please ensure Delta Iceberg support is installed.\",\n      \"Please refer to <docLink> for more details.\"\n    ],\n    \"sqlState\" : \"56038\"\n  },\n  \"DELTA_MISSING_NOT_NULL_COLUMN_VALUE\" : {\n    \"message\" : [\n      \"Column <columnName>, which has a NOT NULL constraint, is missing from the data being written into the table.\"\n    ],\n    \"sqlState\" : \"23502\"\n  },\n  \"DELTA_MISSING_PARTITION_COLUMN\" : {\n    \"message\" : [\n      \"Partition column `<columnName>` not found in schema <columnList>\"\n    ],\n    \"sqlState\" : \"42KD6\"\n  },\n  \"DELTA_MISSING_PART_FILES\" : {\n    \"message\" : [\n      \"Couldn't find all part files of the checkpoint version: <version>\"\n    ],\n    \"sqlState\" : \"42KD6\"\n  },\n  \"DELTA_MISSING_PROVIDER_FOR_CONVERT\" : {\n    \"message\" : [\n      \"CONVERT TO DELTA only supports parquet tables. Please rewrite your target as parquet.`<path>` if it's a parquet directory.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_MISSING_SET_COLUMN\" : {\n    \"message\" : [\n      \"SET column <columnName> not found given columns: <columnList>.\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_MODE_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Specified mode '<mode>' is not supported. Supported modes are: <supportedModes>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_MULTIPLE_CDC_BOUNDARY\" : {\n    \"message\" : [\n      \"Multiple <startingOrEnding> arguments provided for CDC read. Please provide one of either <startingOrEnding>Timestamp or <startingOrEnding>Version.\"\n    ],\n    \"sqlState\" : \"42614\"\n  },\n  \"DELTA_MULTIPLE_CONF_FOR_SINGLE_COLUMN_IN_BLOOM_FILTER\" : {\n    \"message\" : [\n      \"Multiple bloom filter index configurations passed to command for column: <columnName>\"\n    ],\n    \"sqlState\" : \"42614\"\n  },\n  \"DELTA_MULTIPLE_SOURCE_ROW_MATCHING_TARGET_ROW_IN_MERGE\" : {\n    \"message\" : [\n      \"Cannot perform Merge as multiple source rows matched and attempted to modify the same\",\n      \"target row in the Delta table in possibly conflicting ways. By SQL semantics of Merge,\",\n      \"when multiple source rows match on the same target row, the result may be ambiguous\",\n      \"as it is unclear which source row should be used to update or delete the matching\",\n      \"target row. You can preprocess the source table to eliminate the possibility of\",\n      \"multiple matches. Please refer to\",\n      \"<usageReference>\"\n    ],\n    \"sqlState\" : \"21506\"\n  },\n  \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_COMMAND\" : {\n    \"message\" : [\n      \"During <command>, either both coordinated commits configurations (\\\"delta.coordinatedCommits.commitCoordinator-preview\\\", \\\"delta.coordinatedCommits.commitCoordinatorConf-preview\\\") are set in the command or neither of them. Missing: \\\"<configuration>\\\". Please specify this configuration in the TBLPROPERTIES clause or remove the other configuration, and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_SESSION\" : {\n    \"message\" : [\n      \"During <command>, either both coordinated commits configurations (\\\"coordinatedCommits.commitCoordinator-preview\\\", \\\"coordinatedCommits.commitCoordinatorConf-preview\\\") are set in the SparkSession configurations or neither of them. Missing: \\\"<configuration>\\\". Please set this configuration in the SparkSession or unset the other configuration, and then retry the command again.\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_NESTED_NOT_NULL_CONSTRAINT\" : {\n    \"message\" : [\n      \"The <nestType> type of the field <parent> contains a NOT NULL constraint. Delta does not support NOT NULL constraints nested within arrays or maps. To suppress this error and silently ignore the specified constraints, set <configKey> = true.\",\n      \"Parsed <nestType> type:\",\n      \"<nestedPrettyJson>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_NEW_CHECK_CONSTRAINT_VIOLATION\" : {\n    \"message\" : [\n      \"<numRows> rows in <tableName> violate the new CHECK constraint (<checkConstraint>)\"\n    ],\n    \"sqlState\" : \"23512\"\n  },\n  \"DELTA_NEW_NOT_NULL_VIOLATION\" : {\n    \"message\" : [\n      \"<numRows> rows in <tableName> violate the new NOT NULL constraint on <colName>\"\n    ],\n    \"sqlState\" : \"23512\"\n  },\n  \"DELTA_NON_BOOLEAN_CHECK_CONSTRAINT\" : {\n    \"message\" : [\n      \"CHECK constraint '<name>' (<expr>) should be a boolean expression.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_NON_DETERMINISTIC_EXPRESSION_IN_CHECK_CONSTRAINT\" : {\n    \"message\" : [\n      \"Found <expr> in a CHECK constraint. A CHECK constraint cannot use a nondeterministic expression.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_NON_DETERMINISTIC_EXPRESSION_IN_GENERATED_COLUMN\" : {\n    \"message\" : [\n      \"Found <expr>. A generated column cannot use a non deterministic expression.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Non-deterministic functions are not supported in the <operation> <expression>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_NON_GENERATED_COLUMN_MISSING_UPDATE_EXPR\" : {\n    \"message\" : [\n      \"<columnName> is not a generated column but is missing its update expression\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\" : {\n    \"message\" : [\n      \"When there are more than one MATCHED clauses in a MERGE statement, only the last MATCHED clause can omit the condition.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\" : {\n    \"message\" : [\n      \"When there are more than one NOT MATCHED BY SOURCE clauses in a MERGE statement, only the last NOT MATCHED BY SOURCE clause can omit the condition.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_NON_LAST_NOT_MATCHED_CLAUSE_OMIT_CONDITION\" : {\n    \"message\" : [\n      \"When there are more than one NOT MATCHED clauses in a MERGE statement, only the last NOT MATCHED clause can omit the condition\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_NON_PARSABLE_TAG\" : {\n    \"message\" : [\n      \"Could not parse tag <tag>.\",\n      \"File tags are: <tagList>\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_NON_PARTITION_COLUMN_ABSENT\" : {\n    \"message\" : [\n      \"Data written into Delta needs to contain at least one non-partitioned column.<details>\"\n    ],\n    \"sqlState\" : \"KD005\"\n  },\n  \"DELTA_NON_PARTITION_COLUMN_REFERENCE\" : {\n    \"message\" : [\n      \"Predicate references non-partition column '<columnName>'. Only the partition columns may be referenced: [<columnList>]\"\n    ],\n    \"sqlState\" : \"42P10\"\n  },\n  \"DELTA_NON_PARTITION_COLUMN_SPECIFIED\" : {\n    \"message\" : [\n      \"Non-partitioning column(s) <columnList> are specified where only partitioning columns are expected: <fragment>.\"\n    ],\n    \"sqlState\" : \"42P10\"\n  },\n  \"DELTA_NON_SINGLE_PART_NAMESPACE_FOR_CATALOG\" : {\n    \"message\" : [\n      \"Delta catalog requires a single-part namespace, but <identifier> is multi-part.\"\n    ],\n    \"sqlState\" : \"42K05\"\n  },\n  \"DELTA_NOT_A_DELTA_TABLE\" : {\n    \"message\" : [\n      \"<tableName> is not a Delta table. Please drop this table first if you would like to recreate it with Delta Lake.\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_NOT_NULL_COLUMN_NOT_FOUND_IN_STRUCT\" : {\n    \"message\" : [\n      \"Not nullable column not found in struct: <struct>\"\n    ],\n    \"sqlState\" : \"42K09\"\n  },\n  \"DELTA_NOT_NULL_CONSTRAINT_VIOLATED\" : {\n    \"message\" : [\n      \"NOT NULL constraint violated for column: <columnName>.\",\n      \"\"\n    ],\n    \"sqlState\" : \"23502\"\n  },\n  \"DELTA_NOT_NULL_NESTED_FIELD\" : {\n    \"message\" : [\n      \"A non-nullable nested field can't be added to a nullable parent. Please set the nullability of the parent column accordingly.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_NO_COMMITS_FOUND\" : {\n    \"message\" : [\n      \"No commits found at <logPath>\"\n    ],\n    \"sqlState\" : \"KD006\"\n  },\n  \"DELTA_NO_NEW_ATTRIBUTE_ID\" : {\n    \"message\" : [\n      \"Could not find a new attribute ID for column <columnName>. This should have been checked earlier.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_NO_RECREATABLE_HISTORY_FOUND\" : {\n    \"message\" : [\n      \"No recreatable commits found at <logPath>\"\n    ],\n    \"sqlState\" : \"KD006\"\n  },\n  \"DELTA_NO_REDIRECT_RULES_VIOLATED\" : {\n    \"message\" : [\n      \"Operation not allowed: <operation> cannot be performed on a table with redirect feature.\",\n      \"The no redirect rules are not satisfied <noRedirectRules>.\"\n    ],\n    \"sqlState\" : \"42P01\"\n  },\n  \"DELTA_NO_RELATION_TABLE\" : {\n    \"message\" : [\n      \"Table <tableIdent> not found\"\n    ],\n    \"sqlState\" : \"42P01\"\n  },\n  \"DELTA_NO_START_FOR_CDC_READ\" : {\n    \"message\" : [\n      \"No startingVersion or startingTimestamp provided for CDC read.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_NULL_SCHEMA_IN_STREAMING_WRITE\" : {\n    \"message\" : [\n      \"Delta doesn't accept NullTypes in the schema for streaming writes.\"\n    ],\n    \"sqlState\" : \"42P18\"\n  },\n  \"DELTA_NUM_RECORDS_MISMATCH\" : {\n    \"message\" : [\n      \"Failed to validate the number of records in <operation>.\",\n      \"Added <numAddedRecords> records and removed <numRemovedRecords> records.\",\n      \"This is a bug.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_ONEOF_IN_TIMETRAVEL\" : {\n    \"message\" : [\n      \"Please either provide 'timestampAsOf' or 'versionAsOf' for time travel.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_ONLY_OPERATION\" : {\n    \"message\" : [\n      \"<operation> is only supported for Delta tables.\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_OPERATION_MISSING_PATH\" : {\n    \"message\" : [\n      \"Please provide the path or table identifier for <operation>.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_OPERATION_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"Operation not allowed: `<operation>` is not supported for Delta tables\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_OPERATION_NOT_ALLOWED_DETAIL\" : {\n    \"message\" : [\n      \"Operation not allowed: `<operation>` is not supported for Delta tables: <tableName>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_OPERATION_ON_TEMP_VIEW_WITH_GENERATED_COLS_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"<operation> command on a temp view referring to a Delta table that contains generated columns is not supported. Please run the <operation> command on the Delta table directly\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_OPERATION_ON_VIEW_NOT_ALLOWED\" : {\n    \"message\" : [\n      \"Operation not allowed: <operation> cannot be performed on a view.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_OPTIMIZE_FULL_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"OPTIMIZE FULL is only supported for clustered tables with non-empty clustering columns.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_OVERWRITE_SCHEMA_WITH_DYNAMIC_PARTITION_OVERWRITE\" : {\n    \"message\" : [\n      \"'overwriteSchema' cannot be used in dynamic partition overwrite mode.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_PARSING_ANALYSIS_ERROR\" : {\n    \"message\" : [\n      \"<msg>\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_PARSING_ILLEGAL_TABLE_NAME\" : {\n    \"message\" : [\n      \"Illegal table name `<tableName>`.\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_PARSING_INCORRECT_CLONE_HEADER\" : {\n    \"message\" : [\n      \"Incorrect CLONE header. Expected REPLACE or CREATE table.\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_PARSING_MISSING_TABLE_NAME_OR_PATH\" : {\n    \"message\" : [\n      \"<command> command requires a file path or table name.\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_PARSING_MUTUALLY_EXCLUSIVE_CLAUSES\" : {\n    \"message\" : [\n      \"<clauseOne> cannot be used together with <clauseTwo>.\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_PARSING_UNSUPPORTED_DATA_TYPE\" : {\n    \"message\" : [\n      \"DataType <dataType> is not supported.\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_PARTITION_COLUMN_CAST_FAILED\" : {\n    \"message\" : [\n      \"Failed to cast value `<value>` to `<dataType>` for partition column `<columnName>`\"\n    ],\n    \"sqlState\" : \"22525\"\n  },\n  \"DELTA_PARTITION_COLUMN_NOT_FOUND\" : {\n    \"message\" : [\n      \"Partition column <columnName> not found in schema [<schemaMap>]\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_PARTITION_SCHEMA_IN_ICEBERG_TABLES\" : {\n    \"message\" : [\n      \"Partition schema cannot be specified when converting Iceberg tables. It is automatically inferred.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_PATH_BASED_ACCESS_TO_CATALOG_MANAGED_TABLE_BLOCKED\" : {\n    \"message\" : [\n      \"Path-based access is not allowed for Catalog-Managed table: <path>. Please access the table via its name and retry.\"\n    ],\n    \"sqlState\" : \"KD00G\"\n  },\n  \"DELTA_PATH_DOES_NOT_EXIST\" : {\n    \"message\" : [\n      \"<path> doesn't exist, or is not a Delta table.\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_PATH_EXISTS\" : {\n    \"message\" : [\n      \"Cannot write to already existent path <path> without setting OVERWRITE = 'true'.\"\n    ],\n    \"sqlState\" : \"42K04\"\n  },\n  \"DELTA_POST_COMMIT_HOOK_FAILED\" : {\n    \"message\" : [\n      \"Committing to the Delta table version <version> succeeded but error while executing post-commit hook <name><message>\"\n    ],\n    \"sqlState\" : \"2DKD0\"\n  },\n  \"DELTA_PROTOCOL_CHANGED\" : {\n    \"message\" : [\n      \"ProtocolChangedException: The protocol version of the Delta table has been changed by a concurrent update. <additionalInfo><conflictingCommit>\\nRefer to <docLink> for more details.\"\n    ],\n    \"sqlState\" : \"2D521\"\n  },\n  \"DELTA_PROTOCOL_PROPERTY_NOT_INT\" : {\n    \"message\" : [\n      \"Protocol property <key> needs to be an integer. Found <value>\"\n    ],\n    \"sqlState\" : \"42K06\"\n  },\n  \"DELTA_READ_FEATURE_PROTOCOL_REQUIRES_WRITE\" : {\n    \"message\" : [\n      \"Unable to upgrade only the reader protocol version to use table features. Writer protocol version must be at least <writerVersion> to proceed. Refer to <docLink> for more information on table protocol versions.\"\n    ],\n    \"sqlState\" : \"KD004\"\n  },\n  \"DELTA_READ_SOURCE_SCHEMA_CONFLICT\" : {\n    \"message\" : [\n      \"The schema provided for the source read doesn't match the schema of the Delta table\"\n    ],\n    \"sqlState\" : \"42K07\"\n  },\n  \"DELTA_READ_TABLE_WITHOUT_COLUMNS\" : {\n    \"message\" : [\n      \"You are trying to read a Delta table <tableName> that does not have any columns.\",\n      \"\",\n      \"Write some new data with the option `mergeSchema = true` to be able to read the table.\"\n    ],\n    \"sqlState\" : \"428GU\"\n  },\n  \"DELTA_REGEX_OPT_SYNTAX_ERROR\" : {\n    \"message\" : [\n      \"Please recheck your syntax for '<regExpOption>'\"\n    ],\n    \"sqlState\" : \"2201B\"\n  },\n  \"DELTA_RELATION_PATH_MISMATCH\" : {\n    \"message\" : [\n      \"Relation path '<relation>' mismatches with <targetType>'s path '<targetPath>'.\"\n    ],\n    \"sqlState\" : \"2201B\"\n  },\n  \"DELTA_REMOVE_FILE_CDC_MISSING_EXTENDED_METADATA\" : {\n    \"message\" : [\n      \"RemoveFile created without extended metadata is ineligible for CDC:\",\n      \"<file>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_REPLACE_WHERE_IN_OVERWRITE\" : {\n    \"message\" : [\n      \"You can't use replaceWhere in conjunction with an overwrite by filter\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_REPLACE_WHERE_MISMATCH\" : {\n    \"message\" : [\n      \"Written data does not conform to partial table overwrite condition or constraint '<replaceWhere>'.\",\n      \"<message>\"\n    ],\n    \"sqlState\" : \"44000\"\n  },\n  \"DELTA_REPLACE_WHERE_WITH_DYNAMIC_PARTITION_OVERWRITE\" : {\n    \"message\" : [\n      \"A 'replaceWhere' expression and 'partitionOverwriteMode'='dynamic' cannot both be set in the DataFrameWriter options.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_REPLACE_WHERE_WITH_FILTER_DATA_CHANGE_UNSET\" : {\n    \"message\" : [\n      \"'replaceWhere' cannot be used with data filters when 'dataChange' is set to false. Filters: <dataFilters>\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_ROW_ID_ASSIGNMENT_WITHOUT_STATS\" : {\n    \"message\" : [\n      \"Cannot assign row IDs without row count statistics.\",\n      \"Collect statistics for the table by running the ANALYZE TABLE command:\",\n      \"ANALYZE TABLE tableName COMPUTE DELTA STATISTICS\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_ROW_TRACKING_BACKFILL_RUNNING_CONCURRENTLY_WITH_UNBACKFILL\" : {\n    \"message\" : [\n      \"A Row Tracking enablement operation was detected running concurrently with a Row Tracking disablement. Aborting the disablement operation. Please retry the disablement operation if necessary when the enablement operation is complete.\"\n    ],\n    \"sqlState\" : \"22KD0\"\n  },\n  \"DELTA_ROW_TRACKING_ILLEGAL_PROPERTY_COMBINATION\" : {\n    \"message\" : [\n      \"Illegal table state was detected. Table properties `<property1>` and `<property2>` are both set to true. The issue can be resolved by disabling any of the two table properties.\"\n    ],\n    \"sqlState\" : \"55000\"\n  },\n  \"DELTA_SCHEMA_CHANGED\" : {\n    \"message\" : [\n      \"Detected schema change:\",\n      \"streaming source schema: <readSchema>\",\n      \"\",\n      \"data file schema: <dataSchema>\",\n      \"\",\n      \"Please try restarting the query. If this issue repeats across query restarts without\",\n      \"making progress, you have made an incompatible schema change and need to start your\",\n      \"query from scratch using a new checkpoint directory.\",\n      \"\"\n    ],\n    \"sqlState\" : \"KD007\"\n  },\n  \"DELTA_SCHEMA_CHANGED_WITH_STARTING_OPTIONS\" : {\n    \"message\" : [\n      \"Detected schema change in version <version>:\",\n      \"streaming source schema: <readSchema>\",\n      \"\",\n      \"data file schema: <dataSchema>\",\n      \"\",\n      \"Please try restarting the query. If this issue repeats across query restarts without\",\n      \"making progress, you have made an incompatible schema change and need to start your\",\n      \"query from scratch using a new checkpoint directory. If the issue persists after\",\n      \"changing to a new checkpoint directory, you may need to change the existing\",\n      \"'startingVersion' or 'startingTimestamp' option to start from a version newer than\",\n      \"<version> with a new checkpoint directory.\",\n      \"\"\n    ],\n    \"sqlState\" : \"KD007\"\n  },\n  \"DELTA_SCHEMA_CHANGED_WITH_VERSION\" : {\n    \"message\" : [\n      \"Detected schema change in version <version>:\",\n      \"streaming source schema: <readSchema>\",\n      \"\",\n      \"data file schema: <dataSchema>\",\n      \"\",\n      \"Please try restarting the query. If this issue repeats across query restarts without\",\n      \"making progress, you have made an incompatible schema change and need to start your\",\n      \"query from scratch using a new checkpoint directory.\",\n      \"\"\n    ],\n    \"sqlState\" : \"KD007\"\n  },\n  \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\" : {\n    \"message\" : [\n      \"The schema of your Delta table has changed in an incompatible way since your DataFrame\",\n      \"or DeltaTable object was created. Please redefine your DataFrame or DeltaTable object.\",\n      \"Changes:\",\n      \"<schemaDiff><legacyFlagMessage>\"\n    ],\n    \"sqlState\" : \"KD007\"\n  },\n  \"DELTA_SCHEMA_NOT_CONSISTENT_WITH_TARGET\" : {\n    \"message\" : [\n      \"The table schema <tableSchema> is not consistent with the target attributes: <targetAttrs>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_SCHEMA_NOT_PROVIDED\" : {\n    \"message\" : [\n      \"Table schema is not provided. Please provide the schema (column definition) of the table when using REPLACE table and an AS SELECT query is not provided.\"\n    ],\n    \"sqlState\" : \"42908\"\n  },\n  \"DELTA_SCHEMA_NOT_SET\" : {\n    \"message\" : [\n      \"Table schema is not set.  Write data into it or use CREATE TABLE to set the schema.\"\n    ],\n    \"sqlState\" : \"KD008\"\n  },\n  \"DELTA_SET_LOCATION_SCHEMA_MISMATCH\" : {\n    \"message\" : [\n      \"\",\n      \"The schema of the new Delta location is different than the current table schema.\",\n      \"original schema:\",\n      \"<original>\",\n      \"destination schema:\",\n      \"<destination>\",\n      \"\",\n      \"If this is an intended change, you may turn this check off by running:\",\n      \"%%sql set <config> = true\"\n    ],\n    \"sqlState\" : \"42KD7\"\n  },\n  \"DELTA_SHOW_PARTITION_IN_NON_PARTITIONED_COLUMN\" : {\n    \"message\" : [\n      \"Non-partitioning column(s) <badCols> are specified for SHOW PARTITIONS\"\n    ],\n    \"sqlState\" : \"42P10\"\n  },\n  \"DELTA_SHOW_PARTITION_IN_NON_PARTITIONED_TABLE\" : {\n    \"message\" : [\n      \"SHOW PARTITIONS is not allowed on a table that is not partitioned: <tableName>\"\n    ],\n    \"sqlState\" : \"42809\"\n  },\n  \"DELTA_SHREDDING_TABLE_PROPERTY_DISABLED\" : {\n    \"message\" : [\n      \"Attempted to write shredded Variants but the table does not support shredded writes. Consider setting the table property enableVariantShredding to true.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_SOURCE_IGNORE_DELETE\" : {\n    \"message\" : [\n      \"Detected deleted data (for example <removedFile>) from streaming source at version <version>. This is currently not supported. If you'd like to ignore deletes, set the option 'ignoreDeletes' to 'true'. The source table can be found at path <dataPath>.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_SOURCE_TABLE_IGNORE_CHANGES\" : {\n    \"message\" : [\n      \"Detected a data update (for example <file>) in the source table at version <version>. This is currently not supported. If this is going to happen regularly and you are okay to skip changes, set the option 'skipChangeCommits' to 'true'. If you would like the data update to be reflected, please restart this query with a fresh checkpoint directory. The source table can be found at path <dataPath>.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_SPARK_SESSION_NOT_SET\" : {\n    \"message\" : [\n      \"Active SparkSession not set.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_SPARK_THREAD_NOT_FOUND\" : {\n    \"message\" : [\n      \"Not running on a Spark task thread\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_STARTING_VERSION_AND_TIMESTAMP_BOTH_SET\" : {\n    \"message\" : [\n      \"Please either provide '<version>' or '<timestamp>'\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_STATE_RECOVER_ERROR\" : {\n    \"message\" : [\n      \"The <operation> of your Delta table could not be recovered while Reconstructing\",\n      \"version: <version>. Did you manually delete files in the _delta_log directory?\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_STATS_COLLECTION_COLUMN_NOT_FOUND\" : {\n    \"message\" : [\n      \"<statsType> stats not found for column in Parquet metadata: <columnPath>.\"\n    ],\n    \"sqlState\" : \"42000\"\n  },\n  \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\" : {\n    \"message\" : [\n      \"We've detected one or more non-additive schema change(s) (<opType>) between Delta version <previousSchemaChangeVersion> and <currentSchemaChangeVersion> in the Delta streaming source.\",\n      \"Changes: \",\n      \"<columnChangeDetails>\",\n      \"Please check if you want to manually propagate the schema change(s) to the sink table before we proceed with stream processing using the finalized schema at version <currentSchemaChangeVersion>.\",\n      \"Once you have fixed the schema of the sink table or have decided there is no need to fix, you can set the following configuration(s) to unblock the non-additive schema change(s) and continue stream processing.\",\n      \"\",\n      \"Using dataframe reader option(s):\",\n      \"To unblock for this particular stream just for this series of schema change(s):\",\n      \"<unblockChangeOptions>\",\n      \"To unblock for this particular stream:\",\n      \"<unblockStreamOptions>\",\n      \"\",\n      \"Using SQL configuration(s):\",\n      \"To unblock for this particular stream just for this series of schema change(s):\",\n      \"<unblockChangeConfs>\",\n      \"To unblock for this particular stream:\",\n      \"<unblockStreamConfs>\",\n      \"To unblock for all streams:\",\n      \"<unblockAllConfs>\",\n      \"\"\n    ],\n    \"sqlState\" : \"KD002\"\n  },\n  \"DELTA_STREAMING_CHECK_COLUMN_MAPPING_NO_SNAPSHOT\" : {\n    \"message\" : [\n      \"Failed to obtain Delta log snapshot for the start version when checking column mapping schema changes. Please choose a different start version, or force enable streaming read at your own risk by setting '<config>' to 'true'.\"\n    ],\n    \"sqlState\" : \"KD002\"\n  },\n  \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE\" : {\n    \"message\" : [\n      \"Streaming read is not supported on tables with read-incompatible schema changes (e.g. rename or drop or datatype changes).\",\n      \"For further information and possible next steps to resolve this issue, please review the documentation at <docLink>\",\n      \"Read schema: <readSchema>. Incompatible data schema: <incompatibleSchema>.\"\n    ],\n    \"sqlState\" : \"42KD4\"\n  },\n  \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_USE_SCHEMA_LOG\" : {\n    \"message\" : [\n      \"Streaming read is not supported on tables with read-incompatible schema changes (e.g. rename or drop or datatype changes).\",\n      \"Please provide a 'schemaTrackingLocation' to enable non-additive schema evolution for Delta stream processing.\",\n      \"See <docLink> for more details.\",\n      \"Read schema: <readSchema>. Incompatible data schema: <incompatibleSchema>.\"\n    ],\n    \"sqlState\" : \"42KD4\"\n  },\n  \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_V2\" : {\n    \"message\" : [\n      \"Streaming read is not supported on tables with read-incompatible schema changes (e.g. rename or drop or datatype changes).\",\n      \"Non-additive schema change handling is not supported in Delta source v2 yet.\",\n      \"For further information and possible next steps to resolve this issue, please review the documentation at <docLink>\",\n      \"Read schema: <readSchema>. Incompatible data schema: <incompatibleSchema>.\"\n    ],\n    \"sqlState\" : \"42KD4\"\n  },\n  \"DELTA_STREAMING_INITIAL_SNAPSHOT_TOO_LARGE\" : {\n    \"message\" : [\n      \"Initial snapshot for Delta streaming at table '<tablePath>' (version <snapshotVersion>) contains <numFiles> files, which exceeds the maximum allowed <maxFiles> files.\",\n      \"<suggestions>\"\n    ],\n    \"sqlState\" : \"54000\"\n  },\n  \"DELTA_STREAMING_METADATA_EVOLUTION\" : {\n    \"message\" : [\n      \"The schema, table configuration or protocol of your Delta table has changed during streaming.\",\n      \"The schema or metadata tracking log has been updated.\",\n      \"Please restart the stream to continue processing using the updated metadata.\",\n      \"Updated schema: <schema>.\",\n      \"Updated table configurations: <config>.\",\n      \"Updated table protocol: <protocol>\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOCATION_CONFLICT\" : {\n    \"message\" : [\n      \"Detected conflicting schema location '<loc>' while streaming from table or table located at '<table>'.\",\n      \"Another stream may be reusing the same schema location, which is not allowed.\",\n      \"Please provide a new unique `schemaTrackingLocation` path or `streamingSourceTrackingId` as a reader option for one of the streams from this table.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOCATION_NOT_UNDER_CHECKPOINT\" : {\n    \"message\" : [\n      \"Schema location '<schemaTrackingLocation>' must be placed under checkpoint location '<checkpointLocation>'.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOG_DESERIALIZE_FAILED\" : {\n    \"message\" : [\n      \"Incomplete log file in the Delta streaming source schema log at '<location>'.\",\n      \"The schema log may have been corrupted. Please pick a new schema location.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOG_INCOMPATIBLE_DELTA_TABLE_ID\" : {\n    \"message\" : [\n      \"Detected incompatible Delta table id when trying to read Delta stream.\",\n      \"Persisted table id: <persistedId>, Table id: <tableId>\",\n      \"The schema log might have been reused. Please pick a new schema location.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOG_INCOMPATIBLE_PARTITION_SCHEMA\" : {\n    \"message\" : [\n      \"Detected incompatible partition schema when trying to read Delta stream.\",\n      \"Persisted schema: <persistedSchema>, Delta partition schema: <partitionSchema>\",\n      \"Please pick a new schema location to reinitialize the schema log if you have manually changed the table's partition schema recently.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOG_INIT_FAILED_INCOMPATIBLE_METADATA\" : {\n    \"message\" : [\n      \"We could not initialize the Delta streaming source schema log because\",\n      \"we detected an incompatible schema or protocol change while serving a streaming batch from table version <a> to <b>.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_LOG_PARSE_SCHEMA_FAILED\" : {\n    \"message\" : [\n      \"Failed to parse the schema from the Delta streaming source schema log.\",\n      \"The schema log may have been corrupted. Please pick a new schema location.\"\n    ],\n    \"sqlState\" : \"22000\"\n  },\n  \"DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART\" : {\n    \"message\" : [\n      \"The streaming query's schema does not match the current table schema.\",\n      \"Query schema (from analysis): <querySchema>\",\n      \"\",\n      \"Current table schema: <tableSchema>\",\n      \"\",\n      \"The table schema has changed since this streaming query was created.\",\n      \"Please create a new streaming DataFrame to pick up the current schema.\"\n    ],\n    \"sqlState\" : \"KD007\"\n  },\n  \"DELTA_TABLE_ALREADY_CONTAINS_CDC_COLUMNS\" : {\n    \"message\" : [\n      \"Unable to enable Change Data Capture on the table. The table already contains\",\n      \"reserved columns <columnList> that will\",\n      \"be used internally as metadata for the table's Change Data Feed. To enable\",\n      \"Change Data Feed on the table rename/drop these columns.\",\n      \"\"\n    ],\n    \"sqlState\" : \"42711\"\n  },\n  \"DELTA_TABLE_ALREADY_EXISTS\" : {\n    \"message\" : [\n      \"Table <tableName> already exists.\"\n    ],\n    \"sqlState\" : \"42P07\"\n  },\n  \"DELTA_TABLE_FOR_PATH_UNSUPPORTED_HADOOP_CONF\" : {\n    \"message\" : [\n      \"Currently DeltaTable.forPath only supports hadoop configuration keys starting with <allowedPrefixes> but got <unsupportedOptions>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_TABLE_FOUND_IN_EXECUTOR\" : {\n    \"message\" : [\n      \"DeltaTable cannot be used in executors\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_TABLE_INVALID_REDIRECT_STATE_TRANSITION\" : {\n    \"message\" : [\n      \"Unable to update table redirection state: Invalid state transition attempted.\",\n      \"The Delta table '<table>' cannot change from '<oldState>' to '<newState>'.\"\n    ],\n    \"sqlState\" : \"22023\"\n  },\n  \"DELTA_TABLE_INVALID_REMOVE_TABLE_REDIRECT\" : {\n    \"message\" : [\n      \"Unable to remove table redirection for <table> due to its invalid state: <currentState>.\"\n    ],\n    \"sqlState\" : \"KD007\"\n  },\n  \"DELTA_TABLE_INVALID_SET_UNSET_REDIRECT\" : {\n    \"message\" : [\n      \"Unable to SET or UNSET redirect property on <table>: current property '<currentProperty>' mismatches with new property '<newProperty>'.\"\n    ],\n    \"sqlState\" : \"22023\"\n  },\n  \"DELTA_TABLE_LOCATION_MISMATCH\" : {\n    \"message\" : [\n      \"The location of the existing table <tableName> is <existingTableLocation>. It doesn't match the specified location <tableLocation>.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_TABLE_NOT_FOUND\" : {\n    \"message\" : [\n      \"Delta table <tableName> doesn't exist.\"\n    ],\n    \"sqlState\" : \"42P01\"\n  },\n  \"DELTA_TABLE_NOT_SUPPORTED_IN_OP\" : {\n    \"message\" : [\n      \"Table is not supported in <operation>. Please use a path instead.\"\n    ],\n    \"sqlState\" : \"42809\"\n  },\n  \"DELTA_TABLE_ONLY_OPERATION\" : {\n    \"message\" : [\n      \"<tableName> is not a Delta table. <operation> is only supported for Delta tables.\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_TABLE_UNRECOGNIZED_REDIRECT_SPEC\" : {\n    \"message\" : [\n      \"The Delta log contains unrecognized table redirect spec '<spec>'.\"\n    ],\n    \"sqlState\" : \"42704\"\n  },\n  \"DELTA_TARGET_TABLE_FINAL_SCHEMA_EMPTY\" : {\n    \"message\" : [\n      \"Target table final schema is empty.\"\n    ],\n    \"sqlState\" : \"428GU\"\n  },\n  \"DELTA_TIMESTAMP_EARLIER_THAN_COMMIT_RETENTION\" : {\n    \"message\" : [\n      \"The provided timestamp (<userTimestamp>) is before the earliest version available to\",\n      \"this table (<commitTs>). Please use a timestamp after <timestampString>.\"\n    ],\n    \"sqlState\" : \"42816\"\n  },\n  \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\" : {\n    \"message\" : [\n      \"The provided timestamp (<providedTimestamp>) is after the latest version available to this\",\n      \"table (<tableName>). Please use a timestamp before or at <maximumTimestamp>.\"\n    ],\n    \"sqlState\" : \"42816\"\n  },\n  \"DELTA_TIMESTAMP_INVALID\" : {\n    \"message\" : [\n      \"The provided timestamp (<expr>) cannot be converted to a valid timestamp.\"\n    ],\n    \"sqlState\" : \"42816\"\n  },\n  \"DELTA_TIME_TRAVEL_INVALID_BEGIN_VALUE\" : {\n    \"message\" : [\n      \"<timeTravelKey> needs to be a valid begin value.\"\n    ],\n    \"sqlState\" : \"42604\"\n  },\n  \"DELTA_TRUNCATED_TRANSACTION_LOG\" : {\n    \"message\" : [\n      \"<path>: Unable to reconstruct state at version <version> as the transaction log has been truncated due to manual deletion or the log retention policy (<logRetentionKey>=<logRetention>) and checkpoint retention policy (<checkpointRetentionKey>=<checkpointRetention>)\"\n    ],\n    \"sqlState\" : \"42K03\"\n  },\n  \"DELTA_TRUNCATE_TABLE_PARTITION_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Operation not allowed: TRUNCATE TABLE on Delta tables does not support partition predicates; use DELETE to delete specific partitions or rows.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_TXN_LOG_FAILED_INTEGRITY\" : {\n    \"message\" : [\n      \"The transaction log has failed integrity checks. Failed verification at version <version> of:\",\n      \"<mismatchStringOpt>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_UDF_IN_CHECK_CONSTRAINT\" : {\n    \"message\" : [\n      \"Found <expr> in a CHECK constraint. A CHECK constraint cannot use a user-defined function.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_UDF_IN_GENERATED_COLUMN\" : {\n    \"message\" : [\n      \"Found <udfExpr>. A generated column cannot use a user-defined function\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_UNEXPECTED_ACTION_EXPRESSION\" : {\n    \"message\" : [\n      \"Unexpected action expression <expression>.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_UNEXPECTED_ALIAS\" : {\n    \"message\" : [\n      \"Expected Alias but got <alias>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_UNEXPECTED_ATTRIBUTE_REFERENCE\" : {\n    \"message\" : [\n      \"Expected AttributeReference but got <ref>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_UNEXPECTED_CHANGE_FILES_FOUND\" : {\n    \"message\" : [\n      \"Change files found in a dataChange = false transaction. Files:\",\n      \"<fileList>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_UNEXPECTED_NUM_PARTITION_COLUMNS_FROM_FILE_NAME\" : {\n    \"message\" : [\n      \"Expecting <expectedColsSize> partition column(s): <expectedCols>, but found <parsedColsSize> partition column(s): <parsedCols> from parsing the file name: <path>\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_UNEXPECTED_PARTIAL_SCAN\" : {\n    \"message\" : [\n      \"Expect a full scan of Delta sources, but found a partial scan. path:<path>\"\n    ],\n    \"sqlState\" : \"KD00A\"\n  },\n  \"DELTA_UNEXPECTED_PARTITION_COLUMN_FROM_FILE_NAME\" : {\n    \"message\" : [\n      \"Expecting partition column <expectedCol>, but found partition column <parsedCol> from parsing the file name: <path>\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_UNEXPECTED_PARTITION_SCHEMA_FROM_USER\" : {\n    \"message\" : [\n      \"CONVERT TO DELTA was called with a partition schema different from the partition schema inferred from the catalog, please avoid providing the schema so that the partition schema can be chosen from the catalog.\",\n      \"\",\n      \"catalog partition schema:\",\n      \"<catalogPartitionSchema>\",\n      \"provided partition schema:\",\n      \"<userPartitionSchema>\"\n    ],\n    \"sqlState\" : \"KD009\"\n  },\n  \"DELTA_UNEXPECTED_PROJECT\" : {\n    \"message\" : [\n      \"Expected Project but got <project>\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_UNIVERSAL_FORMAT_CONVERSION_FAILED\" : {\n    \"message\" : [\n      \"Failed to convert the table version <version> to the universal format <format>. <message>\"\n    ],\n    \"sqlState\" : \"KD00E\"\n  },\n  \"DELTA_UNIVERSAL_FORMAT_VIOLATION\" : {\n    \"message\" : [\n      \"The validation of Universal Format (<format>) has failed: <violation>\"\n    ],\n    \"sqlState\" : \"KD00E\"\n  },\n  \"DELTA_UNKNOWN_CONFIGURATION\" : {\n    \"message\" : [\n      \"Unknown configuration was specified: <config>\",\n      \"To disable this check, set <disableCheckConfig>=true in the Spark session configuration.\"\n    ],\n    \"sqlState\" : \"F0000\"\n  },\n  \"DELTA_UNKNOWN_PRIVILEGE\" : {\n    \"message\" : [\n      \"Unknown privilege: <privilege>\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_UNKNOWN_READ_LIMIT\" : {\n    \"message\" : [\n      \"Unknown ReadLimit: <limit>\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_UNRECOGNIZED_COLUMN_CHANGE\" : {\n    \"message\" : [\n      \"Unrecognized column change <otherClass>. You may be running an out-of-date Delta Lake version.\"\n    ],\n    \"sqlState\" : \"42601\"\n  },\n  \"DELTA_UNRECOGNIZED_FILE_ACTION\" : {\n    \"message\" : [\n      \"Unrecognized file action <action> with type <actionClass>.\"\n    ],\n    \"sqlState\" : \"XXKDS\"\n  },\n  \"DELTA_UNRECOGNIZED_INVARIANT\" : {\n    \"message\" : [\n      \"Unrecognized invariant. Please upgrade your Spark version.\"\n    ],\n    \"sqlState\" : \"56038\"\n  },\n  \"DELTA_UNRECOGNIZED_LOGFILE\" : {\n    \"message\" : [\n      \"Unrecognized log file <filename>\"\n    ],\n    \"sqlState\" : \"KD00B\"\n  },\n  \"DELTA_UNSET_NON_EXISTENT_PROPERTY\" : {\n    \"message\" : [\n      \"Attempted to unset non-existent property '<property>' in table <tableName>\"\n    ],\n    \"sqlState\" : \"42616\"\n  },\n  \"DELTA_UNSUPPORTED_ABS_PATH_ADD_FILE\" : {\n    \"message\" : [\n      \"<path> does not support adding files with an absolute path\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\" : {\n    \"message\" : [\n      \"ALTER TABLE CHANGE COLUMN is not supported for changing column <fieldPath> from <oldField> to <newField>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_ALTER_TABLE_REPLACE_COL_OP\" : {\n    \"message\" : [\n      \"Unsupported ALTER TABLE REPLACE COLUMNS operation. Reason: <details>\",\n      \"\",\n      \"Failed to change schema from:\",\n      \"<oldSchema>\",\n      \"to:\",\n      \"<newSchema>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_CREATION\" : {\n    \"message\" : [\n      \"Creating a catalog-managed table using delta is unsupported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\" : {\n    \"message\" : [\n      \"<operation> is blocked by the catalog for Catalog-Managed tables.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_CLONE_REPLACE_SAME_TABLE\" : {\n    \"message\" : [\n      \"\",\n      \"You tried to REPLACE an existing table (<tableName>) with CLONE. This operation is\",\n      \"unsupported. Try a different target for CLONE or delete the table at the current target.\",\n      \"\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_COLUMN_MAPPING_MODE_CHANGE\" : {\n    \"message\" : [\n      \"Changing column mapping mode from '<oldMode>' to '<newMode>' is not supported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_COLUMN_MAPPING_SCHEMA_CHANGE\" : {\n    \"message\" : [\n      \"\",\n      \"Schema change is detected:\",\n      \"\",\n      \"old schema:\",\n      \"<oldTableSchema>\",\n      \"\",\n      \"new schema:\",\n      \"<newTableSchema>\",\n      \"\",\n      \"Schema changes are not allowed during the change of column mapping mode.\",\n      \"\",\n      \"\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_COLUMN_MAPPING_WRITE\" : {\n    \"message\" : [\n      \"Writing data with column mapping mode is not supported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_COLUMN_TYPE_IN_BLOOM_FILTER\" : {\n    \"message\" : [\n      \"Creating a bloom filter index on a column with type <dataType> is unsupported: <columnName>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_COMMENT_MAP_ARRAY\" : {\n    \"message\" : [\n      \"Can't add a comment to <fieldPath>. Adding a comment to a map key/value or array element is not supported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_DATA_TYPES\" : {\n    \"message\" : [\n      \"Found columns using unsupported data types: <dataTypeList>. You can set '<config>' to 'false' to disable the type check. Disabling this type check may allow users to create unsupported Delta tables and should only be used when trying to read/write legacy tables.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_DATA_TYPE_IN_GENERATED_COLUMN\" : {\n    \"message\" : [\n      \"<dataType> cannot be the result of a generated column\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_UNSUPPORTED_DEEP_CLONE\" : {\n    \"message\" : [\n      \"Deep clone is not supported by this Delta version.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_UNSUPPORTED_DESCRIBE_DETAIL_VIEW\" : {\n    \"message\" : [\n      \"<view> is a view. DESCRIBE DETAIL is only supported for tables.\"\n    ],\n    \"sqlState\" : \"42809\"\n  },\n  \"DELTA_UNSUPPORTED_DROP_CLUSTERING_COLUMN\" : {\n    \"message\" : [\n      \"Dropping clustering columns (<columnList>) is not allowed.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_DROP_COLUMN\" : {\n    \"message\" : [\n      \"DROP COLUMN is not supported for your Delta table. <advice>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_DROP_NESTED_COLUMN_FROM_NON_STRUCT_TYPE\" : {\n    \"message\" : [\n      \"Can only drop nested columns from StructType. Found <struct>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_DROP_PARTITION_COLUMN\" : {\n    \"message\" : [\n      \"Dropping partition columns (<columnList>) is not allowed.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_EXPRESSION\" : {\n    \"message\" : [\n      \"Unsupported expression type(<expType>) for <causedBy>. The supported types are [<supportedTypes>].\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_UNSUPPORTED_EXPRESSION_CHECK_CONSTRAINT\" : {\n    \"message\" : [\n      \"Found <expression> in a CHECK constraint. <expression> cannot be used in a CHECK constraint.\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_UNSUPPORTED_EXPRESSION_GENERATED_COLUMN\" : {\n    \"message\" : [\n      \"<expression> cannot be used in a generated column\"\n    ],\n    \"sqlState\" : \"42621\"\n  },\n  \"DELTA_UNSUPPORTED_FEATURES_FOR_READ\" : {\n    \"message\" : [\n      \"Unsupported Delta read feature: table \\\"<tableNameOrPath>\\\" requires reader table feature(s) that are unsupported by Delta Lake \\\"<deltaVersion>\\\": <unsupported>.\"\n    ],\n    \"sqlState\" : \"56038\"\n  },\n  \"DELTA_UNSUPPORTED_FEATURES_FOR_WRITE\" : {\n    \"message\" : [\n      \"Unsupported Delta write feature: table \\\"<tableNameOrPath>\\\" requires writer table feature(s) that are unsupported by Delta Lake \\\"<deltaVersion>\\\": <unsupported>.\"\n    ],\n    \"sqlState\" : \"56038\"\n  },\n  \"DELTA_UNSUPPORTED_FEATURES_IN_CONFIG\" : {\n    \"message\" : [\n      \"Table features configured in the following Spark configs or Delta table properties are not recognized by this version of Delta Lake: <configs>.\"\n    ],\n    \"sqlState\" : \"56038\"\n  },\n  \"DELTA_UNSUPPORTED_FEATURE_STATUS\" : {\n    \"message\" : [\n      \"Expecting the status for table feature <feature> to be \\\"supported\\\", but got \\\"<status>\\\".\"\n    ],\n    \"sqlState\" : \"0AKDE\"\n  },\n  \"DELTA_UNSUPPORTED_FIELD_UPDATE_NON_STRUCT\" : {\n    \"message\" : [\n      \"Updating nested fields is only supported for StructType, but you are trying to update a field of <columnName>, which is of type: <dataType>.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_GENERATE_WITH_DELETION_VECTORS\" : {\n    \"message\" : [\n      \"The 'GENERATE symlink_format_manifest' command is not supported on table versions with deletion vectors.\",\n      \"If you need to generate manifests, consider disabling deletion vectors on this table using 'ALTER TABLE table SET TBLPROPERTIES (delta.enableDeletionVectors = false)'.\"\n    ],\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_UNSUPPORTED_INVARIANT_NON_STRUCT\" : {\n    \"message\" : [\n      \"Invariants on nested fields other than StructTypes are not supported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_IN_SUBQUERY\" : {\n    \"message\" : [\n      \"In subquery is not supported in the <operation> condition.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_MANIFEST_GENERATION_WITH_COLUMN_MAPPING\" : {\n    \"message\" : [\n      \"Manifest generation is not supported for tables that leverage column mapping, as external readers cannot read these Delta tables. See Delta documentation for more details.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_MULTI_COL_IN_PREDICATE\" : {\n    \"message\" : [\n      \"Multi-column In predicates are not supported in the <operation> condition.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_NESTED_COLUMN_IN_BLOOM_FILTER\" : {\n    \"message\" : [\n      \"Creating a bloom filer index on a nested column is currently unsupported: <columnName>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_NESTED_FIELD_IN_OPERATION\" : {\n    \"message\" : [\n      \"Nested field is not supported in the <operation> (field = <fieldName>).\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_NON_EMPTY_CLONE\" : {\n    \"message\" : [\n      \"The clone destination table is non-empty. Please TRUNCATE or DELETE FROM the table before running CLONE.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_OUTPUT_MODE\" : {\n    \"message\" : [\n      \"Data source <dataSource> does not support <mode> output mode\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_PARTITION_COLUMN_CHANGE\" : {\n    \"message\" : [\n      \"Cannot change partition columns during <operation> operation (old: <oldPartitionColumns>, new: <newPartitionColumns>).\"\n    ],\n    \"sqlState\" : \"42P10\"\n  },\n  \"DELTA_UNSUPPORTED_PARTITION_COLUMN_IN_BLOOM_FILTER\" : {\n    \"message\" : [\n      \"Creating a bloom filter index on a partitioning column is unsupported: <columnName>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_RENAME_COLUMN\" : {\n    \"message\" : [\n      \"Column rename is not supported for your Delta table. <advice>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_SCHEMA_DURING_READ\" : {\n    \"message\" : [\n      \"Delta does not support specifying the schema at read time.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_SOURCE\" : {\n    \"message\" : [\n      \"<operation> destination only supports Delta sources.\\n<plan>\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_UNSUPPORTED_STATIC_PARTITIONS\" : {\n    \"message\" : [\n      \"Specifying static partitions in the partition spec is currently not supported during inserts\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_UNSUPPORTED_STATS_RECOMPUTE_WITH_DELETION_VECTORS\" : {\n    \"message\" : [\n      \"Statistics re-computation on a Delta table with deletion vectors is not yet supported.\"\n    ],\n    \"sqlState\" : \"0AKDD\"\n  },\n  \"DELTA_UNSUPPORTED_SUBQUERY\" : {\n    \"message\" : [\n      \"Subqueries are not supported in the <operation> (condition = <cond>).\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_SUBQUERY_IN_PARTITION_PREDICATES\" : {\n    \"message\" : [\n      \"Subquery is not supported in partition predicates.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_TIME_TRAVEL_BEYOND_DELETED_FILE_RETENTION_DURATION\" : {\n    \"message\" : [\n      \"Cannot time travel beyond delta.deletedFileRetentionDuration (<deletedFileRetention> HOURS) set on the table.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_TIME_TRAVEL_MULTIPLE_FORMATS\" : {\n    \"message\" : [\n      \"Cannot specify time travel in multiple formats.\"\n    ],\n    \"sqlState\" : \"42613\"\n  },\n  \"DELTA_UNSUPPORTED_TIME_TRAVEL_VIEWS\" : {\n    \"message\" : [\n      \"Cannot time travel views, subqueries, streams or change data feed queries.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_PREVIEW\" : {\n    \"message\" : [\n      \"This table can't be read by this version of Delta because an unsupported type change was applied. Field <fieldPath> was changed from <fromType> to <toType>.\",\n      \"Please upgrade to Delta 4.0 or higher to read this table, or drop the Type Widening table feature using a client that supports reading this table:\",\n      \"  ALTER TABLE tableName DROP FEATURE <typeWideningFeatureName>\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_SCHEMA\" : {\n    \"message\" : [\n      \"Unable to operate on this table because an unsupported type change was applied. Field <fieldName> was changed from <fromType> to <toType>.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_VACUUM_SPECIFIC_PARTITION\" : {\n    \"message\" : [\n      \"Please provide the base path (<baseDeltaPath>) when Vacuuming Delta tables. Vacuuming specific partitions is currently not supported.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UNSUPPORTED_WRITES_STAGED_TABLE\" : {\n    \"message\" : [\n      \"Table implementation does not support writes: <tableName>\"\n    ],\n    \"sqlState\" : \"42807\"\n  },\n  \"DELTA_UNSUPPORTED_WRITES_WITHOUT_COORDINATOR\" : {\n    \"message\" : [\n      \"You are trying to perform writes on a table which has been registered with the commit coordinator <coordinatorName>. However, no implementation of this coordinator is available in the current environment and writes without coordinators are not allowed.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"DELTA_UPDATE_SCHEMA_MISMATCH_EXPRESSION\" : {\n    \"message\" : [\n      \"Cannot cast <fromCatalog> to <toCatalog>. All nested columns must match.\"\n    ],\n    \"sqlState\" : \"42846\"\n  },\n  \"DELTA_USER_DEFINED_TYPE_COLUMN_CONTAINS_NULL_TYPE\" : {\n    \"message\" : [\n      \"Found NullType in column <columnName> which is of <userClass> user-defined type. Delta doesn't support writing NullType in user-defined types.\"\n    ],\n    \"sqlState\" : \"22005\"\n  },\n  \"DELTA_VACUUM_RETENTION_PERIOD_NEGATIVE\" : {\n    \"message\" : [\n      \"Retention period for Vacuum can't be less than 0 hours.\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_VACUUM_RETENTION_PERIOD_TOO_SHORT\" : {\n    \"message\" : [\n      \"The specified VACUUM retention period is too low and may corrupt this Delta table if any writes are in progress.\",\n      \"\",\n      \"If no operations (insert, upsert, delete, optimize) are running, you can disable this safety check by setting:\",\n      \"delta.retentionDurationCheck.enabled = false\",\n      \"\",\n      \"Otherwise, use a retention period of at least <configuredRetentionHours> hours.\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_VERSIONS_NOT_CONTIGUOUS\" : {\n    \"message\" : [\n      \"Versions (<versionList>) are not contiguous. \",\n      \"A gap in the delta log between versions <startVersion> and <endVersion> was detected while trying to load version <versionToLoad>.\"\n    ],\n    \"sqlState\" : \"KD00C\"\n  },\n  \"DELTA_VERSION_INVALID\" : {\n    \"message\" : [\n      \"The provided version (<version>) is not a valid version.\"\n    ],\n    \"sqlState\" : \"42815\"\n  },\n  \"DELTA_VERSION_NOT_FOUND\" : {\n    \"message\" : [\n      \"Cannot time travel Delta table to version <userVersion>. Available versions: [<earliest>, <latest>].\"\n    ],\n    \"sqlState\" : \"22003\"\n  },\n  \"DELTA_VIOLATE_CONSTRAINT_WITH_VALUES\" : {\n    \"message\" : [\n      \"CHECK constraint <constraintName> <expression> violated by row with values:\",\n      \"<values>\"\n    ],\n    \"sqlState\" : \"23001\"\n  },\n  \"DELTA_VIOLATE_TABLE_PROPERTY_VALIDATION_FAILED\" : {\n    \"message\" : [\n      \"The validation of the properties of table <table> has been violated:\"\n    ],\n    \"subClass\" : {\n      \"EXISTING_DELETION_VECTORS_WITH_INCREMENTAL_MANIFEST_GENERATION\" : {\n        \"message\" : [\n          \"Symlink manifest generation is unsupported while deletion vectors are present in the table.\",\n          \"In order to produce a version of the table without deletion vectors, run 'REORG TABLE <table> APPLY (PURGE)'.\"\n        ]\n      },\n      \"PERSISTENT_DELETION_VECTORS_IN_NON_PARQUET_TABLE\" : {\n        \"message\" : [\n          \"Persistent deletion vectors are only supported on Parquet-based Delta tables.\"\n        ]\n      },\n      \"PERSISTENT_DELETION_VECTORS_WITH_INCREMENTAL_MANIFEST_GENERATION\" : {\n        \"message\" : [\n          \"Persistent deletion vectors and incremental symlink manifest generation are mutually exclusive.\"\n        ]\n      }\n    },\n    \"sqlState\" : \"0A000\"\n  },\n  \"DELTA_ZORDERING_COLUMN_DOES_NOT_EXIST\" : {\n    \"message\" : [\n      \"Z-Ordering column <columnName> does not exist in data schema.\"\n    ],\n    \"sqlState\" : \"42703\"\n  },\n  \"DELTA_ZORDERING_ON_COLUMN_WITHOUT_STATS\" : {\n    \"message\" : [\n      \"Z-Ordering on <cols> will be ineffective, because we currently do not collect stats for these columns. You can disable this check by setting\",\n      \" SET <zorderColStatKey> = false\"\n    ],\n    \"sqlState\" : \"KD00D\"\n  },\n  \"DELTA_ZORDERING_ON_PARTITION_COLUMN\" : {\n    \"message\" : [\n      \"<colName> is a partition column. Z-Ordering can only be performed on data columns\"\n    ],\n    \"sqlState\" : \"42P10\"\n  },\n  \"DIFFERENT_DELTA_TABLE_READ_BY_STREAMING_SOURCE\" : {\n    \"message\" : [\n      \"The streaming query was reading from an unexpected Delta table (id = '<newTableId>'). \",\n      \"It used to read from another Delta table (id = '<oldTableId>') according to checkpoint. \",\n      \"This may happen when you changed the code to read from a new table or you deleted and \",\n      \"re-created a table. Please revert your change or delete your streaming query checkpoint \",\n      \"to restart from scratch.\"\n    ],\n    \"sqlState\" : \"55019\"\n  },\n  \"INCORRECT_NUMBER_OF_ARGUMENTS\" : {\n    \"message\" : [\n      \"<failure>, <functionName> requires at least <minArgs> arguments and at most <maxArgs> arguments.\"\n    ],\n    \"sqlState\" : \"42605\"\n  },\n  \"RESERVED_CDC_COLUMNS_ON_WRITE\" : {\n    \"message\" : [\n      \"\",\n      \"The write contains reserved columns <columnList> that are used\",\n      \"internally as metadata for Change Data Feed. To write to the table either rename/drop\",\n      \"these columns or disable Change Data Feed on the table by setting\",\n      \"<config> to false.\"\n    ],\n    \"sqlState\" : \"42939\"\n  },\n  \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_ALTER_TABLE_ADD_COLUMN_NOT_SUPPORTED\" : {\n    \"message\" : [\n      \"Failed to execute the command because DEFAULT values are not supported when adding new\",\n      \"columns to previously existing Delta tables; please add the column without a default\",\n      \"value first, then run a second ALTER TABLE ALTER COLUMN SET DEFAULT command to apply\",\n      \"for future inserted rows instead.\"\n    ],\n    \"sqlState\" : \"0AKDC\"\n  },\n  \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\" : {\n    \"message\" : [\n      \"Failed to execute <commandType> command because it assigned a column DEFAULT value,\",\n      \"but the corresponding table feature was not enabled. Please retry the command again\",\n      \"after executing ALTER TABLE tableName SET\",\n      \"TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported').\"\n    ],\n    \"sqlState\" : \"0AKDE\"\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0001\" : {\n    \"message\" : [\n      \"Cannot use '<name>' as the name of a CHECK constraint.\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0002\" : {\n    \"message\" : [\n      \"Cannot create bloom filter index, invalid parameter value: '<message>'.\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0003\" : {\n    \"message\" : [\n      \"You are trying to convert a table which already has a delta log where the table properties in the catalog don't match the configuration in the delta log.\",\n      \"Table properties in catalog:\",\n      \"<tableProperties>\",\n      \"Delta configuration:\",\n      \"<configuration>\",\n      \"If you would like to merge the configurations (update existing fields and insert new ones), set the SQL configuration `<metadataCheckSqlConf>` to false.\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0006\" : {\n    \"message\" : [\n      \"Inconsistent IDENTITY metadata for column <colName> detected: <hasStart>, <hasStep>, <hasInsert>\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0008\" : {\n    \"message\" : [\n      \"Error while searching for position of column <column>.\",\n      \"Schema:\",\n      \"<schema>\",\n      \"Error:\",\n      \"<message>\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0009\" : {\n    \"message\" : [\n      \"<optionalPrefixMessage>Updating nested fields is only supported for StructType.\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0010\" : {\n    \"message\" : [\n      \"<optionalPrefixMessage>Found unsupported expression <expression> while parsing target column name parts.\"\n    ]\n  },\n  \"_LEGACY_ERROR_TEMP_DELTA_0012\" : {\n    \"message\" : [\n      \"Could not resolve expression: <expression>\"\n    ]\n  }\n}\n"
  },
  {
    "path": "spark/src/main/resources/org/apache/spark/SparkLayout.json",
    "content": "{\n  \"ts\": {\n    \"$resolver\": \"timestamp\",\n    \"pattern\": {\n      \"format\": \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\",\n      \"timeZone\": \"UTC\",\n      \"locale\": \"en_US\"\n    }\n  },\n  \"level\": {\n    \"$resolver\": \"level\",\n    \"field\": \"name\"\n  },\n  \"msg\": {\n    \"$resolver\": \"message\",\n    \"stringified\": true\n  },\n  \"context\": {\n    \"$resolver\": \"mdc\"\n  },\n  \"exception\": {\n    \"class\": {\n      \"$resolver\": \"exception\",\n      \"field\": \"className\"\n    },\n    \"msg\": {\n      \"$resolver\": \"exception\",\n      \"field\": \"message\",\n      \"stringified\": true\n    },\n    \"stacktrace\": {\n      \"$resolver\": \"exception\",\n      \"field\": \"stackTrace\",\n      \"stackTrace\": {\n        \"elementTemplate\": {\n          \"class\": {\n            \"$resolver\": \"stackTraceElement\",\n            \"field\": \"className\"\n          },\n          \"method\": {\n            \"$resolver\": \"stackTraceElement\",\n            \"field\": \"methodName\"\n          },\n          \"file\": {\n            \"$resolver\": \"stackTraceElement\",\n            \"field\": \"fileName\"\n          },\n          \"line\": {\n            \"$resolver\": \"stackTraceElement\",\n            \"field\": \"lineNumber\"\n          }\n        }\n      }\n    }\n  },\n  \"logger\": {\n    \"$resolver\": \"pattern\",\n    \"pattern\": \"%c{1}\",\n    \"stackTraceEnabled\": false\n  }\n}"
  },
  {
    "path": "spark/src/main/scala/com/databricks/spark/util/DatabricksLogging.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.databricks.spark.util\n\nimport scala.collection.mutable.ArrayBuffer\n\n/**\n * This file contains stub implementation for logging that exists in Databricks.\n */\n\n/** Used to return a recorded usage record for testing. */\ncase class UsageRecord(\n    metric: String,\n    quantity: Double,\n    blob: String,\n    tags: Map[String, String] = Map.empty,\n    opType: Option[OpType] = None,\n    opTarget: Option[String] = None)\n\nclass TagDefinition(val name: String) {\n  def this() = this(\"BACKWARD COMPATIBILITY\")\n}\n\nobject TagDefinitions {\n  object TAG_TAHOE_PATH extends TagDefinition(\"tahoePath\")\n  object TAG_TAHOE_ID extends TagDefinition(\"tahoeId\")\n  object TAG_ASYNC extends TagDefinition(\"async\")\n  object TAG_LOG_STORE_CLASS extends TagDefinition(\"logStore\")\n  object TAG_OP_TYPE extends TagDefinition(\"opType\")\n}\n\ncase class OpType(typeName: String, description: String)\n\nclass MetricDefinition(val name: String) {\n  def this() = this(\"BACKWARD COMPATIBILITY\")\n}\n\nobject MetricDefinitions {\n  object EVENT_LOGGING_FAILURE extends MetricDefinition(\"loggingFailureEvent\")\n  object EVENT_TAHOE extends MetricDefinition(\"tahoeEvent\") with CentralizableMetric\n  val METRIC_OPERATION_DURATION = new MetricDefinition(\"sparkOperationDuration\")\n    with CentralizableMetric\n}\n\nobject Log4jUsageLogger {\n  @volatile var usageTracker: ArrayBuffer[UsageRecord] = null\n\n  /**\n   * Records and returns all usage logs that are emitted while running the given function.\n   * Intended for testing metrics that we expect to report. Note that this class does not\n   * support nested invocations of the tracker.\n   */\n  def track(f: => Unit): Seq[UsageRecord] = {\n    synchronized {\n      assert(usageTracker == null, \"Usage tracking does not support nested invocation.\")\n      usageTracker = new ArrayBuffer[UsageRecord]()\n    }\n    var records: ArrayBuffer[UsageRecord] = null\n    try {\n      f\n    } finally {\n      records = usageTracker\n      synchronized {\n        usageTracker = null\n      }\n    }\n    records.toSeq\n  }\n}\n\ntrait DatabricksLogging {\n  import MetricDefinitions._\n\n  // scalastyle:off println\n  def logConsole(line: String): Unit = println(line)\n  // scalastyle:on println\n\n  def recordUsage(\n      metric: MetricDefinition,\n      quantity: Double,\n      additionalTags: Map[TagDefinition, String] = Map.empty,\n      blob: String = null,\n      forceSample: Boolean = false,\n      trimBlob: Boolean = true,\n      silent: Boolean = false): Unit = {\n    Log4jUsageLogger.synchronized {\n      if (Log4jUsageLogger.usageTracker != null) {\n        val record =\n          UsageRecord(metric.name, quantity, blob, additionalTags.map(kv => (kv._1.name, kv._2)))\n        Log4jUsageLogger.usageTracker.append(record)\n      }\n    }\n  }\n\n  def recordEvent(\n      metric: MetricDefinition,\n      additionalTags: Map[TagDefinition, String] = Map.empty,\n      blob: String = null,\n      trimBlob: Boolean = true): Unit = {\n    recordUsage(metric, 1, additionalTags, blob, trimBlob)\n  }\n\n  def recordOperation[S](\n      opType: OpType,\n      opTarget: String = null,\n      extraTags: Map[TagDefinition, String],\n      isSynchronous: Boolean = true,\n      alwaysRecordStats: Boolean = false,\n      allowAuthTags: Boolean = false,\n      killJvmIfStuck: Boolean = false,\n      outputMetric: MetricDefinition = METRIC_OPERATION_DURATION,\n      silent: Boolean = true)(thunk: => S): S = {\n    try {\n      thunk\n    } finally {\n      Log4jUsageLogger.synchronized {\n        if (Log4jUsageLogger.usageTracker != null) {\n          val record = UsageRecord(outputMetric.name, 0, null,\n            extraTags.map(kv => (kv._1.name, kv._2)), Some(opType), Some(opTarget))\n          Log4jUsageLogger.usageTracker.append(record)\n        }\n      }\n    }\n  }\n\n  def recordProductUsage(\n      metric: MetricDefinition with CentralizableMetric,\n      quantity: Double,\n      additionalTags: Map[TagDefinition, String] = Map.empty,\n      blob: String = null,\n      forceSample: Boolean = false,\n      trimBlob: Boolean = true,\n      silent: Boolean = false): Unit = {\n    Log4jUsageLogger.synchronized {\n      if (Log4jUsageLogger.usageTracker != null) {\n        val record =\n          UsageRecord(metric.name, quantity, blob, additionalTags.map(kv => (kv._1.name, kv._2)))\n        Log4jUsageLogger.usageTracker.append(record)\n      }\n    }\n  }\n\n  def recordProductEvent(\n      metric: MetricDefinition with CentralizableMetric,\n      additionalTags: Map[TagDefinition, String] = Map.empty,\n      blob: String = null,\n      trimBlob: Boolean = true): Unit = {\n    recordProductUsage(metric, 1, additionalTags, blob, trimBlob)\n  }\n}\n\ntrait CentralizableMetric\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/exceptions/DeltaConcurrentExceptions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.exceptions\n\nimport org.apache.spark.sql.delta.{DeltaThrowable, DeltaThrowableHelper}\n\nimport org.apache.spark.annotation.Evolving\n\n/**\n * :: Evolving ::\n *\n * The basic class for all Delta commit conflict exceptions.\n *\n * @since 1.0.0\n */\n@Evolving\nabstract class DeltaConcurrentModificationException(message: String)\n  extends org.apache.spark.sql.delta.DeltaConcurrentModificationException(message)\n\n/**\n * :: Evolving ::\n *\n * Thrown when a concurrent transaction has written data after the current transaction read the\n * table.\n *\n * @since 1.0.0\n */\n@Evolving\nclass ConcurrentWriteException(message: String)\n  extends org.apache.spark.sql.delta.ConcurrentWriteException(message)\n    with DeltaThrowable {\n  def this(messageParameters: Array[String]) = {\n    this(DeltaThrowableHelper.getMessage(\"DELTA_CONCURRENT_WRITE\", messageParameters))\n  }\n  override def getErrorClass: String = \"DELTA_CONCURRENT_WRITE\"\n  override def getMessage: String = message\n}\n\n/**\n * :: Evolving ::\n *\n * Thrown when the metadata of the Delta table has changed between the time of read\n * and the time of commit.\n *\n * @since 1.0.0\n */\n@Evolving\nclass MetadataChangedException(message: String)\n  extends org.apache.spark.sql.delta.MetadataChangedException(message)\n    with DeltaThrowable {\n  def this(messageParameters: Array[String]) = {\n    this(DeltaThrowableHelper.getMessage(\"DELTA_METADATA_CHANGED\", messageParameters))\n  }\n  override def getErrorClass: String = \"DELTA_METADATA_CHANGED\"\n  override def getMessage: String = message\n}\n\n/**\n * :: Evolving ::\n *\n * Thrown when the protocol version has changed between the time of read\n * and the time of commit.\n *\n * @since 1.0.0\n */\n@Evolving\nclass ProtocolChangedException(message: String)\n  extends org.apache.spark.sql.delta.ProtocolChangedException(message)\n    with DeltaThrowable {\n  def this(messageParameters: Array[String]) = {\n    this(DeltaThrowableHelper.getMessage(\"DELTA_PROTOCOL_CHANGED\", messageParameters))\n  }\n  override def getErrorClass: String = \"DELTA_PROTOCOL_CHANGED\"\n  override def getMessage: String = message\n}\n\n/**\n * :: Evolving ::\n *\n * Thrown when files are added that would have been read by the current transaction.\n *\n * @since 1.0.0\n */\n@Evolving\nclass ConcurrentAppendException private (\n    errorClass: String,\n    message: String,\n    messageParameters: Array[String] = Array.empty)\n  extends org.apache.spark.sql.delta.ConcurrentAppendException(message)\n    with DeltaThrowable {\n  def this(message: String) = this(message, \"DELTA_CONCURRENT_APPEND.WITHOUT_HINT\", Array.empty)\n  def this(messageParameters: Array[String]) = {\n    this(\n      \"DELTA_CONCURRENT_APPEND.WITHOUT_HINT\",\n      DeltaThrowableHelper.getMessage(\"DELTA_CONCURRENT_APPEND.WITHOUT_HINT\", messageParameters),\n      messageParameters\n    )\n  }\n  override def getErrorClass: String = errorClass\n  override def getMessage: String = message\n  def getMessageParametersArray: Array[String] = messageParameters\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n}\n\nobject ConcurrentAppendException {\n  def apply(subClass: String, messageParameters: Array[String]): ConcurrentAppendException = {\n    val errorClass = s\"DELTA_CONCURRENT_APPEND.$subClass\"\n    val message = DeltaThrowableHelper.getMessage(errorClass, messageParameters)\n    new ConcurrentAppendException(errorClass, message, messageParameters)\n  }\n}\n\n/**\n * :: Evolving ::\n *\n * Thrown when the current transaction reads data that was deleted by a concurrent transaction.\n *\n * @since 1.0.0\n */\n@Evolving\nclass ConcurrentDeleteReadException private (\n    message: String,\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty)\n  extends org.apache.spark.sql.delta.ConcurrentDeleteReadException(message)\n    with DeltaThrowable {\n  def this(message: String) = this(message,\n    \"DELTA_CONCURRENT_DELETE_READ.WITHOUT_HINT\", Array.empty)\n  def this(messageParameters: Array[String]) = {\n    this(DeltaThrowableHelper.getMessage(\n      \"DELTA_CONCURRENT_DELETE_READ.WITHOUT_HINT\", messageParameters),\n      \"DELTA_CONCURRENT_DELETE_READ.WITHOUT_HINT\",\n      messageParameters\n    )\n  }\n  override def getErrorClass: String = errorClass\n  override def getMessage: String = message\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n}\n\nobject ConcurrentDeleteReadException {\n  def apply(subClass: String, messageParameters: Array[String]): ConcurrentDeleteReadException = {\n    val errorClass = s\"DELTA_CONCURRENT_DELETE_READ.$subClass\"\n    val message = DeltaThrowableHelper.getMessage(errorClass, messageParameters)\n    new ConcurrentDeleteReadException(message, errorClass, messageParameters)\n  }\n}\n\n/**\n * :: Evolving ::\n *\n * Thrown when the current transaction deletes data that was deleted by a concurrent transaction.\n *\n * @since 1.0.0\n */\n@Evolving\nclass ConcurrentDeleteDeleteException private (\n    message: String,\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty)\n  extends org.apache.spark.sql.delta.ConcurrentDeleteDeleteException(message)\n    with DeltaThrowable {\n  def this(message: String) = this(message,\n    \"DELTA_CONCURRENT_DELETE_DELETE.WITHOUT_HINT\", Array.empty)\n  def this(messageParameters: Array[String]) = {\n    this(DeltaThrowableHelper.getMessage(\n      \"DELTA_CONCURRENT_DELETE_DELETE.WITHOUT_HINT\", messageParameters),\n      \"DELTA_CONCURRENT_DELETE_DELETE.WITHOUT_HINT\",\n      messageParameters\n    )\n  }\n  override def getErrorClass: String = errorClass\n  override def getMessage: String = message\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n}\n\nobject ConcurrentDeleteDeleteException {\n  def apply(subClass: String, messageParameters: Array[String]): ConcurrentDeleteDeleteException = {\n    val errorClass = s\"DELTA_CONCURRENT_DELETE_DELETE.$subClass\"\n    val message = DeltaThrowableHelper.getMessage(errorClass, messageParameters)\n    new ConcurrentDeleteDeleteException(message, errorClass, messageParameters)\n  }\n}\n\n/**\n * :: Evolving ::\n *\n * Thrown when concurrent transaction both attempt to update the same idempotent transaction.\n *\n * @since 1.0.0\n */\n@Evolving\nclass ConcurrentTransactionException(message: String)\n  extends org.apache.spark.sql.delta.ConcurrentTransactionException(message)\n    with DeltaThrowable {\n  def this(messageParameters: Array[String]) = {\n    this(DeltaThrowableHelper.getMessage(\"DELTA_CONCURRENT_TRANSACTION\", messageParameters))\n  }\n  override def getErrorClass: String = \"DELTA_CONCURRENT_TRANSACTION\"\n  override def getMessage: String = message\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/implicits/package.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta\n\nimport org.apache.spark.sql.{DataFrame, DataFrameReader, DataFrameWriter}\nimport org.apache.spark.sql.streaming.{DataStreamReader, DataStreamWriter, StreamingQuery}\n\npackage object implicits {\n\n  /**\n   * Extends the DataFrameReader API by adding a delta function\n   * Usage:\n   * {{{\n   * spark.read.delta(path)\n   * }}}\n   */\n  implicit class DeltaDataFrameReader(val reader: DataFrameReader) extends AnyVal {\n    def delta(path: String): DataFrame = {\n      reader.format(\"delta\").load(path)\n    }\n  }\n\n  /**\n   * Extends the DataStreamReader API by adding a delta function\n   * Usage:\n   * {{{\n   * spark.readStream.delta(path)\n   * }}}\n   */\n  implicit class DeltaDataStreamReader(val dataStreamReader: DataStreamReader) extends AnyVal {\n    def delta(path: String): DataFrame = {\n      dataStreamReader.format(\"delta\").load(path)\n    }\n  }\n\n  /**\n   * Extends the DataFrameWriter API by adding a delta function\n   * Usage:\n   * {{{\n   * df.write.delta(path)\n   * }}}\n   */\n  implicit class DeltaDataFrameWriter[T](val dfWriter: DataFrameWriter[T]) extends AnyVal {\n    def delta(output: String): Unit = {\n      dfWriter.format(\"delta\").save(output)\n    }\n  }\n\n  /**\n   * Extends the DataStreamWriter API by adding a delta function\n   * Usage:\n   * {{{\n   * ds.writeStream.delta(path)\n   * }}}\n   */\n  implicit class DeltaDataStreamWriter[T]\n  (val dataStreamWriter: DataStreamWriter[T]) extends AnyVal {\n    def delta(path: String): StreamingQuery = {\n      dataStreamWriter.format(\"delta\").start(path)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/sql/AbstractDeltaSparkSessionExtension.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sql\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.metric.OptimizeConditionalIncrementMetric\nimport org.apache.spark.sql.delta.optimizer.RangePartitionIdRewrite\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.PrepareDeltaScan\nimport io.delta.sql.parser.DeltaSqlParser\n\nimport org.apache.spark.sql.SparkSessionExtensions\nimport org.apache.spark.sql.catalyst.optimizer.ConstantFolding\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.delta.PreprocessTimeTravel\nimport org.apache.spark.sql.internal.SQLConf\n\n\n/**\n * V1 legacy implementation. Use [[io.delta.sql.DeltaSparkSessionExtension]] instead.\n * See spark-unified/src/main/scala/io/delta/sql/DeltaSparkSessionExtension.scala\n */\nclass DeltaSparkSessionExtensionV1 extends AbstractDeltaSparkSessionExtension\n\n/**\n* Abstract base class that contains the base Delta Spark Session extension logic.\n* As part of evolution for Delta connector(e.g. Dsv2), new Spark session extension\n* will be built based on that.\n*/\nclass AbstractDeltaSparkSessionExtension extends (SparkSessionExtensions => Unit) {\n  override def apply(extensions: SparkSessionExtensions): Unit = {\n    extensions.injectParser { (_, parser) =>\n      new DeltaSqlParser(parser)\n    }\n    extensions.injectResolutionRule { session =>\n      ResolveDeltaPathTable(session)\n    }\n    extensions.injectResolutionRule { session =>\n      PreprocessTimeTravel(session)\n    }\n    extensions.injectResolutionRule { session =>\n      // To ensure the parquet field id reader is turned on, these fields are required to support\n      // id column mapping mode for Delta.\n      // Spark has the read flag default off, so we have to turn it on manually for Delta.\n      session.sessionState.conf.setConf(SQLConf.PARQUET_FIELD_ID_READ_ENABLED, true)\n      session.sessionState.conf.setConf(SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED, true)\n      new DeltaAnalysis(session)\n    }\n    // [SPARK-45383] Spark CheckAnalysis rule misses a case for RelationTimeTravel, and so a\n    // non-existent table throws an internal spark error instead of the expected AnalysisException.\n    extensions.injectCheckRule { session =>\n      new CheckUnresolvedRelationTimeTravel(session)\n    }\n    extensions.injectCheckRule { session =>\n      DeltaUnsupportedOperationsCheck(session)\n    }\n    // Rule for rewriting the place holder for range_partition_id to manually construct the\n    // `RangePartitioner` (which requires an RDD to be sampled in order to determine\n    // range partition boundaries)\n    extensions.injectOptimizerRule { session =>\n      new RangePartitionIdRewrite(session)\n    }\n    // Optimize ConditionalIncrementMetric with constant condition.\n    extensions.injectOptimizerRule { _ => OptimizeConditionalIncrementMetric }\n\n    extensions.injectPostHocResolutionRule { session =>\n      PreprocessTableUpdate(session.sessionState.conf)\n    }\n    extensions.injectPostHocResolutionRule { session =>\n      PreprocessTableMerge(session.sessionState.conf)\n    }\n    extensions.injectPostHocResolutionRule { session =>\n      PreprocessTableDelete(session.sessionState.conf)\n    }\n    // Resolve new UpCast expressions that might have been introduced by [[PreprocessTableUpdate]]\n    // and [[PreprocessTableMerge]].\n    extensions.injectPostHocResolutionRule { session =>\n      PostHocResolveUpCast(session)\n    }\n\n    extensions.injectPlanNormalizationRule { _ => GenerateRowIDs }\n\n    extensions.injectPreCBORule { session =>\n      new PrepareDeltaScan(session)\n    }\n    // Fold constants that may have been introduced by PrepareDeltaScan. This is only useful with\n    // Spark 3.5 as later versions apply constant folding after pre-CBO rules.\n    extensions.injectPreCBORule { _ => ConstantFolding }\n\n    // Add skip row column and filter.\n    extensions.injectPlannerStrategy(PreprocessTableWithDVsStrategy)\n\n    // Tries to load PrepareDeltaSharingScan class with class reflection, when delta-sharing-spark\n    // 3.1+ package is installed, this will be loaded and delta sharing batch queries with\n    // DeltaSharingFileIndex will be handled by the rule.\n    // When the package is not installed or upon any other issues, it should do nothing and not\n    // affect all the existing rules.\n    try {\n      // scalastyle:off classforname\n      val constructor = Class.forName(\"io.delta.sharing.spark.PrepareDeltaSharingScan\")\n        .getConstructor(classOf[org.apache.spark.sql.SparkSession])\n      // scalastyle:on classforname\n      extensions.injectPreCBORule { session =>\n        try {\n          // Inject the PrepareDeltaSharingScan rule if enabled, otherwise, inject the no op\n          // rule. It can be disabled if there are any issues so all existing rules are not blocked.\n          if (\n            session.conf.get(DeltaSQLConf.DELTA_SHARING_ENABLE_DELTA_FORMAT_BATCH.key) == \"true\"\n          ) {\n            constructor.newInstance(session).asInstanceOf[Rule[LogicalPlan]]\n          } else {\n            new NoOpRule\n          }\n        } catch {\n          // Inject a no op rule which doesn't apply any changes to the logical plan.\n          case NonFatal(_) => new NoOpRule\n        }\n      }\n    } catch {\n      case NonFatal(_) => // Do nothing\n    }\n\n    DeltaTableValueFunctions.supportedFnNames.foreach { fnName =>\n      extensions.injectTableFunction(\n        DeltaTableValueFunctions.getTableValueFunctionInjection(fnName))\n    }\n  }\n\n  /**\n   * An no op rule which doesn't apply any changes to the LogicalPlan. Used to be injected upon\n   * exceptions.\n   */\n  class NoOpRule extends Rule[LogicalPlan] {\n    override def apply(plan: LogicalPlan): LogicalPlan = plan\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/sql/parser/DeltaSqlParser.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sql.parser\n\nimport java.util.Locale\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.catalyst.TimeTravel\nimport org.apache.spark.sql.delta.skipping.clustering.temp.{AlterTableClusterBy, ClusterByParserUtils, ClusterByPlan, ClusterBySpec}\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands._\nimport io.delta.sql.parser.DeltaSqlBaseParser._\nimport io.delta.tables.execution.VacuumTableCommand\nimport org.antlr.v4.runtime._\nimport org.antlr.v4.runtime.atn.PredictionMode\nimport org.antlr.v4.runtime.misc.{Interval, ParseCancellationException}\nimport org.antlr.v4.runtime.tree._\n\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal}\nimport org.apache.spark.sql.catalyst.{FunctionIdentifier, TableIdentifier}\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.parser.{DeltaParseException, ParseErrorListener, ParseException, ParseExceptionShims, ParserInterface}\nimport org.apache.spark.sql.catalyst.parser.ParserUtils.{checkDuplicateClauses, string, withOrigin}\nimport org.apache.spark.sql.catalyst.plans.logical.{AlterColumnSyncIdentity, AlterTableAddConstraint, AlterTableDropConstraint, AlterTableDropFeature, CloneTableStatement, LogicalPlan, RestoreTableStatement}\nimport org.apache.spark.sql.catalyst.trees.Origin\nimport org.apache.spark.sql.connector.catalog.{CatalogV2Util, TableCatalog}\nimport org.apache.spark.sql.errors.QueryParsingErrors\nimport org.apache.spark.sql.internal.{SQLConf, VariableSubstitution}\nimport org.apache.spark.sql.types._\n\n/**\n * A SQL parser that tries to parse Delta commands. If failing to parse the SQL text, it will\n * forward the call to `delegate`.\n */\nclass DeltaSqlParser(val delegate: ParserInterface)\n    extends ParserInterface {\n  private val builder = new DeltaSqlAstBuilder\n  private val substitution = new VariableSubstitution\n\n  override def parsePlan(sqlText: String): LogicalPlan = parse(sqlText) { parser =>\n    builder.visit(parser.singleStatement()) match {\n      case clusterByPlan: ClusterByPlan =>\n        ClusterByParserUtils(clusterByPlan, delegate).parsePlan(sqlText)\n      case plan: LogicalPlan => plan\n      case _ => delegate.parsePlan(sqlText)\n    }\n  }\n\n  /**\n   * This API is used just for parsing the SELECT queries. Delta parser doesn't override\n   * the Spark parser, that means this can be delegated directly to the Spark parser.\n   */\n  override def parseQuery(sqlText: String): LogicalPlan = delegate.parseQuery(sqlText)\n\n  // scalastyle:off line.size.limit\n  /**\n   * Fork from `org.apache.spark.sql.catalyst.parser.AbstractSqlParser#parse(java.lang.String, scala.Function1)`.\n   *\n   * @see https://github.com/apache/spark/blob/v2.4.4/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/ParseDriver.scala#L81\n   */\n  // scalastyle:on\n  protected def parse[T](command: String)(toResult: DeltaSqlBaseParser => T): T = {\n    val lexer = new DeltaSqlBaseLexer(\n      new UpperCaseCharStream(CharStreams.fromString(substitution.substitute(command))))\n    lexer.removeErrorListeners()\n    lexer.addErrorListener(ParseErrorListener)\n\n    val tokenStream = new CommonTokenStream(lexer)\n    val parser = new DeltaSqlBaseParser(tokenStream)\n    parser.addParseListener(PostProcessor)\n    parser.removeErrorListeners()\n    parser.addErrorListener(ParseErrorListener)\n\n    try {\n      try {\n        // first, try parsing with potentially faster SLL mode\n        parser.getInterpreter.setPredictionMode(PredictionMode.SLL)\n        toResult(parser)\n      } catch {\n        case e: ParseCancellationException =>\n          // if we fail, parse with LL mode\n          tokenStream.seek(0) // rewind input stream\n          parser.reset()\n\n          // Try Again.\n          parser.getInterpreter.setPredictionMode(PredictionMode.LL)\n          toResult(parser)\n      }\n    } catch {\n      case e: ParseException if e.command.isDefined =>\n        throw e\n      case e: ParseException =>\n        throw e.withCommand(command)\n      case e: AnalysisException =>\n        val position = Origin(e.line, e.startPosition)\n        throw ParseExceptionShims.createParseException(\n          command = Option(command),\n          start = position,\n          stop = position,\n          errorClass = \"DELTA_PARSING_ANALYSIS_ERROR\",\n          messageParameters = Map(\"msg\" -> e.message))\n    }\n  }\n\n  override def parseExpression(sqlText: String): Expression = delegate.parseExpression(sqlText)\n\n  override def parseTableIdentifier(sqlText: String): TableIdentifier =\n    delegate.parseTableIdentifier(sqlText)\n\n  override def parseFunctionIdentifier(sqlText: String): FunctionIdentifier =\n    delegate.parseFunctionIdentifier(sqlText)\n\n  override def parseMultipartIdentifier (sqlText: String): Seq[String] =\n    delegate.parseMultipartIdentifier(sqlText)\n\n  override def parseTableSchema(sqlText: String): StructType = delegate.parseTableSchema(sqlText)\n\n  override def parseDataType(sqlText: String): DataType = delegate.parseDataType(sqlText)\n\n  override def parseRoutineParam(sqlText: String): StructType = delegate.parseRoutineParam(sqlText)\n}\n\n/**\n * Define how to convert an AST generated from `DeltaSqlBase.g4` to a `LogicalPlan`. The parent\n * class `DeltaSqlBaseBaseVisitor` defines all visitXXX methods generated from `#` instructions in\n * `DeltaSqlBase.g4` (such as `#vacuumTable`).\n */\nclass DeltaSqlAstBuilder extends DeltaSqlBaseBaseVisitor[AnyRef] {\n\n  import org.apache.spark.sql.catalyst.parser.ParserUtils._\n\n  /**\n   * Convert a property list into a key-value map.\n   * This should be called through [[visitPropertyKeyValues]] or [[visitPropertyKeys]].\n   */\n  override def visitPropertyList(\n      ctx: PropertyListContext): Map[String, String] = withOrigin(ctx) {\n    val properties = ctx.property.asScala.map { property =>\n      val key = visitPropertyKey(property.key)\n      val value = visitPropertyValue(property.value)\n      key -> value\n    }\n    // Check for duplicate property names.\n    checkDuplicateKeys(properties.toSeq, ctx)\n    properties.toMap\n  }\n\n  /**\n   * Parse a key-value map from a [[PropertyListContext]], assuming all values are specified.\n   */\n  def visitPropertyKeyValues(ctx: PropertyListContext): Map[String, String] = {\n    val props = visitPropertyList(ctx)\n    val badKeys = props.collect { case (key, null) => key }\n    if (badKeys.nonEmpty) {\n      operationNotAllowed(\n        s\"Values must be specified for key(s): ${badKeys.mkString(\"[\", \",\", \"]\")}\", ctx)\n    }\n    props\n  }\n\n  /**\n   * Parse a list of keys from a [[PropertyListContext]], assuming no values are specified.\n   */\n  def visitPropertyKeys(ctx: PropertyListContext): Seq[String] = {\n    val props = visitPropertyList(ctx)\n    val badKeys = props.filter { case (_, v) => v != null }.keys\n    if (badKeys.nonEmpty) {\n      operationNotAllowed(\n        s\"Values should not be specified for key(s): ${badKeys.mkString(\"[\", \",\", \"]\")}\", ctx)\n    }\n    props.keys.toSeq\n  }\n\n  /**\n   * A property key can either be String or a collection of dot separated elements. This\n   * function extracts the property key based on whether its a string literal or a property\n   * identifier.\n   */\n  override def visitPropertyKey(key: PropertyKeyContext): String = {\n    if (key.stringLit() != null) {\n      visitStringLit(key.stringLit())\n    } else {\n      key.getText\n    }\n  }\n\n  /**\n   * A property value can be String, Integer, Boolean or Decimal. This function extracts\n   * the property value based on whether its a string, integer, boolean or decimal literal.\n   */\n  override def visitPropertyValue(value: PropertyValueContext): String = {\n    if (value == null) {\n      null\n    } else if (value.identifier != null) {\n      value.identifier.getText\n    } else if (value.value != null) {\n      visitStringLit(value.value)\n    } else if (value.booleanValue != null) {\n      value.getText.toLowerCase(Locale.ROOT)\n    } else {\n      value.getText\n    }\n  }\n\n  override def visitStringLit(ctx: StringLitContext): String = {\n    if (ctx == null) return null\n    ctx.singleStringLit().asScala.map { singleCtx =>\n      val token = if (singleCtx.STRING != null) {\n        singleCtx.STRING.getSymbol\n      } else {\n        singleCtx.DOUBLEQUOTED_STRING.getSymbol\n      }\n      string(token)\n    }.mkString\n  }\n\n  /**\n   * Parse either create table header or replace table header.\n   * @return TableIdentifier for the target table\n   *         Boolean for whether we are creating a table\n   *         Boolean for whether we are replacing a table\n   *         Boolean for whether we are creating a table if not exists\n   */\n  override def visitCloneTableHeader(\n      ctx: CloneTableHeaderContext): (TableIdentifier, Boolean, Boolean, Boolean) = withOrigin(ctx) {\n    ctx.children.asScala.head match {\n      case createHeader: CreateTableHeaderContext =>\n        (visitTableIdentifier(createHeader.table), true, false, createHeader.EXISTS() != null)\n      case replaceHeader: ReplaceTableHeaderContext =>\n        (visitTableIdentifier(replaceHeader.table), replaceHeader.CREATE() != null, true, false)\n      case _ =>\n        throw new DeltaParseException(ctx, \"DELTA_PARSING_INCORRECT_CLONE_HEADER\")\n    }\n  }\n\n  /**\n   * Creates a [[CloneTableStatement]] logical plan. Example SQL:\n   * {{{\n   *   CREATE [OR REPLACE] TABLE <table-identifier> SHALLOW CLONE <source-table-identifier>\n   *     [TBLPROPERTIES ('propA' = 'valueA', ...)]\n   *     [LOCATION '/path/to/cloned/table']\n   * }}}\n   */\n  override def visitClone(ctx: CloneContext): LogicalPlan = withOrigin(ctx) {\n    val (target, isCreate, isReplace, ifNotExists) = visitCloneTableHeader(ctx.cloneTableHeader())\n\n    if (!isCreate && ifNotExists) {\n      throw new DeltaParseException(\n        ctx.cloneTableHeader(),\n        \"DELTA_PARSING_MUTUALLY_EXCLUSIVE_CLAUSES\",\n        Map(\"clauseOne\" -> \"IF NOT EXISTS\", \"clauseTwo\" -> \"REPLACE\")\n      )\n    }\n\n    // Get source for clone (and time travel source if necessary)\n    // The source relation can be an Iceberg table in form of `catalog.db.table` so we visit\n    // a multipart identifier instead of TableIdentifier (which does not support 3L namespace)\n    // in Spark 3.3. In Spark 3.4 we should have TableIdentifier supporting 3L namespace so we\n    // could revert back to that.\n    val sourceRelation = new UnresolvedRelation(visitMultipartIdentifier(ctx.source))\n    val maybeTimeTravelSource = maybeTimeTravelChild(ctx.clause, sourceRelation)\n    val targetRelation = UnresolvedRelation(target.nameParts)\n\n    val tablePropertyOverrides = Option(ctx.tableProps)\n      .map(visitPropertyKeyValues)\n      .getOrElse(Map.empty[String, String])\n\n    CloneTableStatement(\n      maybeTimeTravelSource,\n      targetRelation,\n      ifNotExists,\n      isReplace,\n      isCreate,\n      tablePropertyOverrides,\n      Option(ctx.location).map(visitStringLit))\n  }\n\n  /**\n   * Create a [[VacuumTableCommand]] logical plan. Example SQL:\n   * {{{\n   *   VACUUM ('/path/to/dir' | delta.`/path/to/dir`)\n   *   LITE|FULL\n   *   [RETAIN number HOURS] [DRY RUN];\n   * }}}\n   */\n  override def visitVacuumTable(ctx: VacuumTableContext): AnyRef = withOrigin(ctx) {\n    val vacuumModifiersCtx = ctx.vacuumModifiers()\n    withOrigin(vacuumModifiersCtx) {\n      checkDuplicateClauses(vacuumModifiersCtx.vacuumType(), \"LITE/FULL\", vacuumModifiersCtx)\n      checkDuplicateClauses(vacuumModifiersCtx.inventory(), \"INVENTORY\", vacuumModifiersCtx)\n      checkDuplicateClauses(vacuumModifiersCtx.retain(), \"RETAIN\", vacuumModifiersCtx)\n      checkDuplicateClauses(vacuumModifiersCtx.dryRun(), \"DRY RUN\", vacuumModifiersCtx)\n      if (!vacuumModifiersCtx.inventory().isEmpty &&\n        !vacuumModifiersCtx.vacuumType().isEmpty &&\n        vacuumModifiersCtx.vacuumType().asScala.head.LITE != null) {\n        operationNotAllowed(\"Inventory option is not compatible with LITE\", vacuumModifiersCtx)\n      }\n    }\n    VacuumTableCommand(\n      path = Option(ctx.path).map(visitStringLit),\n      table = Option(ctx.table).map(visitTableIdentifier),\n      inventoryTable = ctx.vacuumModifiers().inventory().asScala.headOption.collect {\n        case i if i.inventoryTable != null => visitTableIdentifier(i.inventoryTable)\n      },\n      inventoryQuery = ctx.vacuumModifiers().inventory().asScala.headOption.collect {\n        case i if i.inventoryQuery != null => extractRawText(i.inventoryQuery)\n      },\n      horizonHours =\n        ctx.vacuumModifiers().retain().asScala.headOption.map(_.number.getText.toDouble),\n      dryRun =\n        ctx.vacuumModifiers().dryRun().asScala.headOption.exists(_.RUN != null),\n      vacuumType = ctx.vacuumModifiers().vacuumType().asScala.headOption.map {\n        t => if (t.LITE != null) \"LITE\" else \"FULL\"\n      },\n      options = Map.empty\n    )\n  }\n\n  /** Provides a list of unresolved attributes for multi dimensional clustering. */\n  override def visitZorderSpec(ctx: ZorderSpecContext): Seq[UnresolvedAttribute] = {\n    ctx.interleave.asScala\n      .map(_.identifier.asScala.map(_.getText).toSeq)\n      .map(new UnresolvedAttribute(_)).toSeq\n  }\n\n  /**\n   * Create a [[OptimizeTableCommand]] logical plan.\n   * Syntax:\n   * {{{\n   *    OPTIMIZE <table-identifier>\n   *      [WHERE predicate-using-partition-columns]\n   *      [ZORDER BY [(] col1, col2 ..[)]]\n   * }}}\n   * Examples:\n   * {{{\n   *    OPTIMIZE '/path/to/delta/table';\n   *    OPTIMIZE delta_table_name;\n   *    OPTIMIZE delta.`/path/to/delta/table`;\n   *    OPTIMIZE delta_table_name WHERE partCol = 25;\n   *    OPTIMIZE delta_table_name WHERE partCol = 25 ZORDER BY col2, col2;\n   * }}}\n   */\n  override def visitOptimizeTable(ctx: OptimizeTableContext): AnyRef = withOrigin(ctx) {\n    if (ctx.path == null && ctx.table == null) {\n      throw new DeltaParseException(\n        ctx,\n        \"DELTA_PARSING_MISSING_TABLE_NAME_OR_PATH\",\n        Map(\"command\" -> \"OPTIMIZE\")\n      )\n    }\n    val interleaveBy = Option(ctx.zorderSpec).map(visitZorderSpec).getOrElse(Seq.empty)\n    OptimizeTableCommand(\n      Option(ctx.path).map(visitStringLit),\n      Option(ctx.table).map(visitTableIdentifier),\n      Option(ctx.partitionPredicate).map(extractRawText(_)).toSeq,\n      DeltaOptimizeContext(isFull = ctx.FULL != null))(interleaveBy)\n  }\n\n  /**\n   * Creates a [[DeltaReorgTable]] logical plan.\n   * Examples:\n   * {{{\n   *   -- Physically delete dropped rows and columns of target table\n   *   REORG TABLE (delta.`/path/to/table` | delta_table_name)\n   *    [WHERE partition_predicate] APPLY (PURGE)\n   *\n   *   -- Rewrite the files in UNIFORM(ICEBERG) compliant way.\n   *   REORG TABLE table_name (delta.`/path/to/table` | catalog.db.table)\n   *    APPLY (UPGRADE UNIFORM(ICEBERG_COMPAT_VERSION=version))\n   * }}}\n   */\n  override def visitReorgTable(ctx: ReorgTableContext): AnyRef = withOrigin(ctx) {\n    if (ctx.table == null) {\n      throw new DeltaParseException(\n        ctx,\n        \"DELTA_PARSING_MISSING_TABLE_NAME_OR_PATH\",\n        Map(\"command\" -> \"REORG\")\n      )\n    }\n\n    val targetIdentifier = visitTableIdentifier(ctx.table)\n    val targetTable = UnresolvedTable(targetIdentifier.nameParts, \"REORG\")\n\n    val reorgTableSpec = if (ctx.PURGE != null) {\n      DeltaReorgTableSpec(DeltaReorgTableMode.PURGE, None)\n    } else if (ctx.ICEBERG_COMPAT_VERSION != null) {\n      DeltaReorgTableSpec(DeltaReorgTableMode.UNIFORM_ICEBERG, Option(ctx.version).map(_.getText.toInt))\n    } else {\n      throw new ParseException(\n        \"Invalid syntax: REORG TABLE only support PURGE/UPGRADE UNIFORM.\",\n        ctx)\n    }\n\n    DeltaReorgTable(targetTable, reorgTableSpec)(Option(ctx.partitionPredicate).map(extractRawText(_)).toSeq)\n  }\n\n  override def visitDescribeDeltaDetail(\n      ctx: DescribeDeltaDetailContext): LogicalPlan = withOrigin(ctx) {\n    DescribeDeltaDetailCommand(\n      Option(ctx.path).map(visitStringLit),\n      Option(ctx.table).map(visitTableIdentifier),\n      Map.empty)\n  }\n\n  override def visitDescribeDeltaHistory(\n      ctx: DescribeDeltaHistoryContext): LogicalPlan = withOrigin(ctx) {\n    DescribeDeltaHistory(\n      Option(ctx.path).map(visitStringLit),\n      Option(ctx.table).map(visitTableIdentifier),\n      Option(ctx.limit).map(_.getText.toInt))\n  }\n\n  override def visitGenerate(ctx: GenerateContext): LogicalPlan = withOrigin(ctx) {\n    DeltaGenerateCommand(\n      UnresolvedTable(visitTableIdentifier(ctx.table).nameParts, DeltaGenerateCommand.COMMAND_NAME),\n      modeName = ctx.modeName.getText)\n  }\n\n  override def visitConvert(ctx: ConvertContext): LogicalPlan = withOrigin(ctx) {\n    ConvertToDeltaCommand(\n      visitTableIdentifier(ctx.table),\n      Option(ctx.colTypeList).map(colTypeList => StructType(visitColTypeList(colTypeList))),\n      ctx.STATISTICS() == null, None)\n  }\n\n  override def visitRestore(ctx: RestoreContext): LogicalPlan = withOrigin(ctx) {\n    val tableRelation = UnresolvedRelation(visitTableIdentifier(ctx.table).nameParts)\n    val timeTravelTableRelation = maybeTimeTravelChild(ctx.clause, tableRelation)\n    RestoreTableStatement(timeTravelTableRelation.asInstanceOf[TimeTravel])\n  }\n\n  /**\n   * Captures any CLUSTER BY clause and creates a [[ClusterByPlan]] logical plan.\n   * The plan will be used as a sentinel for DeltaSqlParser to process it further.\n   */\n  override def visitClusterBy(ctx: ClusterByContext): LogicalPlan = withOrigin(ctx) {\n    val clusterBySpecCtx = ctx.clusterBySpec.asScala.head\n    checkDuplicateClauses(ctx.clusterBySpec, \"CLUSTER BY\", clusterBySpecCtx)\n    val columnNames =\n      clusterBySpecCtx.interleave.asScala\n        .map(_.identifier.asScala.map(_.getText).toSeq)\n        .map(_.asInstanceOf[Seq[String]]).toSeq\n    // get CLUSTER BY clause positions.\n    val startIndex = clusterBySpecCtx.getStart.getStartIndex\n    val stopIndex = clusterBySpecCtx.getStop.getStopIndex\n\n    // get CLUSTER BY parenthesis positions.\n    val parenStartIndex = clusterBySpecCtx.LEFT_PAREN().getSymbol.getStartIndex\n    val parenStopIndex = clusterBySpecCtx.RIGHT_PAREN().getSymbol.getStopIndex\n    ClusterByPlan(\n      ClusterBySpec(columnNames),\n      startIndex,\n      stopIndex,\n      parenStartIndex,\n      parenStopIndex,\n      clusterBySpecCtx)\n  }\n\n  /**\n   * Time travel the table to the given version or timestamp.\n   */\n  private def maybeTimeTravelChild(ctx: TemporalClauseContext, child: LogicalPlan): LogicalPlan = {\n    if (ctx == null) return child\n    TimeTravel(\n      child,\n      Option(ctx.timestamp).map(token => Literal(token.getText.replaceAll(\"^'|'$\", \"\"))),\n      Option(ctx.version).map(_.getText.toLong),\n      Some(\"sql\"))\n  }\n\n  override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {\n    visit(ctx.statement).asInstanceOf[LogicalPlan]\n  }\n\n  protected def visitTableIdentifier(ctx: QualifiedNameContext): TableIdentifier = withOrigin(ctx) {\n    ctx.identifier.asScala.toSeq match {\n      case Seq(tbl) => TableIdentifier(tbl.getText)\n      case Seq(db, tbl) => TableIdentifier(tbl.getText, Some(db.getText))\n      case Seq(catalog, db, tbl) =>\n        TableIdentifier(tbl.getText, Some(db.getText), Some(catalog.getText))\n      case _ => throw new DeltaParseException(\n        ctx,\n        \"DELTA_PARSING_ILLEGAL_TABLE_NAME\",\n        Map(\"table\" -> ctx.getText))\n    }\n  }\n\n  protected def visitMultipartIdentifier(ctx: QualifiedNameContext): Seq[String] = withOrigin(ctx) {\n    ctx.identifier.asScala.map(_.getText).toSeq\n  }\n\n  override def visitPassThrough(ctx: PassThroughContext): LogicalPlan = null\n\n  override def visitColTypeList(ctx: ColTypeListContext): Seq[StructField] = withOrigin(ctx) {\n    ctx.colType().asScala.map(visitColType).toSeq\n  }\n\n  override def visitColType(ctx: ColTypeContext): StructField = withOrigin(ctx) {\n    import ctx._\n\n    val builder = new MetadataBuilder\n\n    StructField(\n      ctx.colName.getText,\n      typedVisit[DataType](ctx.dataType),\n      nullable = NOT == null,\n      builder.build())\n  }\n\n  // Build the text of the CHECK constraint expression. The user-specified whitespace is in the\n  // HIDDEN channel where we can't get to it, so we just paste together all the tokens with a single\n  // space. This produces some strange spacing (e.g. `structCol . arr [ 0 ]`), but right now we\n  // think that's preferable to the additional complexity involved in trying to produce cleaner\n  // output.\n  private def buildCheckConstraintText(tokens: Seq[ExprTokenContext]): String = {\n    tokens.map(_.getText).mkString(\" \")\n  }\n\n  private def extractRawText(exprContext: ParserRuleContext): String = {\n    // Extract the raw expression which will be parsed later\n    exprContext.getStart.getInputStream.getText(new Interval(\n      exprContext.getStart.getStartIndex,\n      exprContext.getStop.getStopIndex))\n  }\n\n  override def visitAddTableConstraint(\n      ctx: AddTableConstraintContext): LogicalPlan = withOrigin(ctx) {\n    val checkConstraint = ctx.constraint().asInstanceOf[CheckConstraintContext]\n\n    AlterTableAddConstraint(\n      UnresolvedTable(ctx.table.identifier.asScala.map(_.getText).toSeq,\n        \"ALTER TABLE ... ADD CONSTRAINT\"),\n      ctx.name.getText,\n      buildCheckConstraintText(checkConstraint.exprToken().asScala.toSeq))\n  }\n\n  override def visitDropTableConstraint(\n      ctx: DropTableConstraintContext): LogicalPlan = withOrigin(ctx) {\n    AlterTableDropConstraint(\n      UnresolvedTable(ctx.table.identifier.asScala.map(_.getText).toSeq,\n        \"ALTER TABLE ... DROP CONSTRAINT\"),\n      ctx.name.getText,\n      ifExists = ctx.EXISTS != null)\n  }\n\n  /**\n   * `ALTER TABLE ... ALTER (CHANGE) COLUMN ... SYNC IDENTITY` command.\n   */\n  override def visitAlterTableSyncIdentity(\n      ctx: AlterTableSyncIdentityContext): LogicalPlan = withOrigin(ctx) {\n    val verb = if (ctx.CHANGE != null) \"CHANGE\" else \"ALTER\"\n    AlterColumnSyncIdentity(\n      UnresolvedTable(ctx.table.identifier.asScala.map(_.getText).toSeq,\n        s\"ALTER TABLE ... $verb COLUMN\"),\n      UnresolvedFieldName(visitMultipartIdentifier(ctx.column))\n    )\n  }\n\n  /**\n   * A featureNameValue can either be String or an identifier. This function extracts\n   * the featureNameValue based on whether its a string literal or an identifier.\n   */\n  override def visitFeatureNameValue(featureNameValue: FeatureNameValueContext): String = {\n    if (featureNameValue.stringLit() != null) {\n      visitStringLit(featureNameValue.stringLit())\n    } else {\n      featureNameValue.getText\n    }\n  }\n\n  /**\n   * Parse an ALTER TABLE DROP FEATURE command.\n   */\n  override def visitAlterTableDropFeature(ctx: AlterTableDropFeatureContext): LogicalPlan = {\n    val truncateHistory = ctx.TRUNCATE != null && ctx.HISTORY != null\n    AlterTableDropFeature(\n      UnresolvedTable(ctx.table.identifier.asScala.map(_.getText).toSeq,\n        \"ALTER TABLE ... DROP FEATURE\"),\n      visitFeatureNameValue(ctx.featureName),\n      truncateHistory)\n  }\n\n  /**\n   * Parse an ALTER TABLE CLUSTER BY command.\n   */\n  override def visitAlterTableClusterBy(ctx: AlterTableClusterByContext): LogicalPlan = {\n    val table =\n      UnresolvedTable(ctx.table.identifier.asScala.map(_.getText).toSeq,\n      \"ALTER TABLE ... CLUSTER BY\")\n    if (ctx.NONE() != null) {\n      AlterTableClusterBy(table, None)\n    } else {\n      assert(ctx.clusterBySpec() != null)\n      val columnNames =\n        ctx.clusterBySpec().interleave.asScala\n          .map(_.identifier.asScala.map(_.getText).toSeq)\n          .map(_.asInstanceOf[Seq[String]]).toSeq\n      AlterTableClusterBy(table, Some(ClusterBySpec(columnNames)))\n    }\n  }\n\n  protected def typedVisit[T](ctx: ParseTree): T = {\n    ctx.accept(this).asInstanceOf[T]\n  }\n\n  override def visitPrimitiveDataType(ctx: PrimitiveDataTypeContext): DataType = withOrigin(ctx) {\n    val dataType = ctx.identifier.getText.toLowerCase(Locale.ROOT)\n    (dataType, ctx.INTEGER_VALUE().asScala.toList) match {\n      case (\"boolean\", Nil) => BooleanType\n      case (\"tinyint\" | \"byte\", Nil) => ByteType\n      case (\"smallint\" | \"short\", Nil) => ShortType\n      case (\"int\" | \"integer\", Nil) => IntegerType\n      case (\"bigint\" | \"long\", Nil) => LongType\n      case (\"float\", Nil) => FloatType\n      case (\"double\", Nil) => DoubleType\n      case (\"date\", Nil) => DateType\n      case (\"timestamp\", Nil) => TimestampType\n      case (\"string\", Nil) => StringType\n      case (\"char\", length :: Nil) => CharType(length.getText.toInt)\n      case (\"varchar\", length :: Nil) => VarcharType(length.getText.toInt)\n      case (\"binary\", Nil) => BinaryType\n      case (\"decimal\", Nil) => DecimalType.USER_DEFAULT\n      case (\"decimal\", precision :: Nil) => DecimalType(precision.getText.toInt, 0)\n      case (\"decimal\", precision :: scale :: Nil) =>\n        DecimalType(precision.getText.toInt, scale.getText.toInt)\n      case (\"interval\", Nil) => CalendarIntervalType\n      case (dt, params) =>\n        val dtStr = if (params.nonEmpty) s\"$dt(${params.mkString(\",\")})\" else dt\n        throw new DeltaParseException(\n          ctx,\n          \"DELTA_PARSING_UNSUPPORTED_DATA_TYPE\",\n          Map(\"dataType\" -> dtStr)\n        )\n    }\n  }\n}\n\n// scalastyle:off line.size.limit\n/**\n * Fork from `org.apache.spark.sql.catalyst.parser.UpperCaseCharStream`.\n *\n * @see https://github.com/apache/spark/blob/v2.4.4/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/ParseDriver.scala#L157\n */\n// scalastyle:on\nclass UpperCaseCharStream(wrapped: CodePointCharStream) extends CharStream {\n  override def consume(): Unit = wrapped.consume\n  override def getSourceName(): String = wrapped.getSourceName\n  override def index(): Int = wrapped.index\n  override def mark(): Int = wrapped.mark\n  override def release(marker: Int): Unit = wrapped.release(marker)\n  override def seek(where: Int): Unit = wrapped.seek(where)\n  override def size(): Int = wrapped.size\n\n  override def getText(interval: Interval): String = {\n    // ANTLR 4.7's CodePointCharStream implementations have bugs when\n    // getText() is called with an empty stream, or intervals where\n    // the start > end. See\n    // https://github.com/antlr/antlr4/commit/ac9f7530 for one fix\n    // that is not yet in a released ANTLR artifact.\n    if (size() > 0 && (interval.b - interval.a >= 0)) {\n      wrapped.getText(interval)\n    } else {\n      \"\"\n    }\n  }\n\n  override def LA(i: Int): Int = {\n    val la = wrapped.LA(i)\n    if (la == 0 || la == IntStream.EOF) la\n    else Character.toUpperCase(la)\n  }\n}\n\n// scalastyle:off line.size.limit\n/**\n * Fork from `org.apache.spark.sql.catalyst.parser.PostProcessor`.\n *\n * @see https://github.com/apache/spark/blob/v2.4.4/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/ParseDriver.scala#L248\n */\n// scalastyle:on\ncase object PostProcessor extends DeltaSqlBaseBaseListener {\n\n  /** Remove the back ticks from an Identifier. */\n  override def exitQuotedIdentifier(ctx: QuotedIdentifierContext): Unit = {\n    replaceTokenByIdentifier(ctx, 1) { token =>\n      // Remove the double back ticks in the string.\n      token.setText(token.getText.replace(\"``\", \"`\"))\n      token\n    }\n  }\n\n  /** Treat non-reserved keywords as Identifiers. */\n  override def exitNonReserved(ctx: NonReservedContext): Unit = {\n    replaceTokenByIdentifier(ctx, 0)(identity)\n  }\n\n  private def replaceTokenByIdentifier(\n    ctx: ParserRuleContext,\n    stripMargins: Int)(\n    f: CommonToken => CommonToken = identity): Unit = {\n    val parent = ctx.getParent\n    parent.removeLastChild()\n    val token = ctx.getChild(0).getPayload.asInstanceOf[Token]\n    val newToken = new CommonToken(\n      new org.antlr.v4.runtime.misc.Pair(token.getTokenSource, token.getInputStream),\n      DeltaSqlBaseParser.IDENTIFIER,\n      token.getChannel,\n      token.getStartIndex + stripMargins,\n      token.getStopIndex - stripMargins)\n    parent.addChild(new TerminalNodeImpl(f(newToken)))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/DeltaColumnBuilder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport org.apache.spark.sql.delta.{DeltaErrors, IdentityColumn}\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.{GENERATION_EXPRESSION_METADATA_KEY, IDENTITY_INFO_ALLOW_EXPLICIT_INSERT, IDENTITY_INFO_START, IDENTITY_INFO_STEP}\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.annotation._\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.types.{DataType, LongType, MetadataBuilder, StructField}\n\n/**\n * :: Evolving ::\n *\n * Builder to specify a table column.\n *\n * See [[DeltaTableBuilder]] for examples.\n * @since 1.0.0\n */\n@Evolving\nclass DeltaColumnBuilder private[tables](\n    private val spark: SparkSession,\n    private val colName: String) {\n  private var dataType: DataType = _\n  private var nullable: Boolean = true\n  private var generationExpr: Option[String] = None\n  private var comment: Option[String] = None\n  private var identityStart: Option[Long] = None\n  private var identityStep: Option[Long] = None\n  private var identityAllowExplicitInsert: Option[Boolean] = None\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the column data type.\n   *\n   * @param dataType string column data type\n   * @since 1.0.0\n   */\n  @Evolving\n  def dataType(dataType: String): DeltaColumnBuilder = {\n    this.dataType = spark.sessionState.sqlParser.parseDataType(dataType)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the column data type.\n   *\n   * @param dataType DataType column data type\n   * @since 1.0.0\n   */\n  @Evolving\n  def dataType(dataType: DataType): DeltaColumnBuilder = {\n    this.dataType = dataType\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify whether the column can be null.\n   *\n   * @param nullable boolean whether the column can be null or not.\n   * @since 1.0.0\n   */\n  @Evolving\n  def nullable(nullable: Boolean): DeltaColumnBuilder = {\n    this.nullable = nullable\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a expression if the column is always generated as a function of other columns.\n   *\n   * @param expr string the the generation expression\n   * @since 1.0.0\n   */\n  @Evolving\n  def generatedAlwaysAs(expr: String): DeltaColumnBuilder = {\n    this.generationExpr = Option(expr)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column with default values that is always generated\n   * by the system (i.e. does not allow user-specified values).\n   *\n   * @since 3.3.0\n   */\n  @Evolving\n  def generatedAlwaysAsIdentity(): DeltaColumnBuilder = {\n    generatedAlwaysAsIdentity(IdentityColumn.defaultStart, IdentityColumn.defaultStep)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column that is always generated by the system (i.e. does not\n   * allow user-specified values).\n   *\n   * @param start the start value of the identity column\n   * @param step the increment step of the identity column\n   * @since 3.3.0\n   */\n  @Evolving\n  def generatedAlwaysAsIdentity(start: Long, step: Long): DeltaColumnBuilder = {\n    this.identityStart = Some(start)\n    this.identityStep = Some(step)\n    this.identityAllowExplicitInsert = Some(false)\n\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column that allows user-specified values such that the\n   * generated values use default start and step values.\n   *\n   * @since 3.3.0\n   */\n  @Evolving\n  def generatedByDefaultAsIdentity(): DeltaColumnBuilder = {\n    generatedByDefaultAsIdentity(IdentityColumn.defaultStart, IdentityColumn.defaultStep)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column that allows user-specified values.\n   *\n   * @param start the start value of the identity column\n   * @param step the increment step of the identity column\n   * @since 3.3.0\n   */\n  @Evolving\n  def generatedByDefaultAsIdentity(start: Long, step: Long): DeltaColumnBuilder = {\n    this.identityStart = Some(start)\n    this.identityStep = Some(step)\n    this.identityAllowExplicitInsert = Some(true)\n\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column comment.\n   *\n   * @param comment string column description\n   * @since 1.0.0\n   */\n  @Evolving\n  def comment(comment: String): DeltaColumnBuilder = {\n    this.comment = Option(comment)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Build the column as a structField.\n   *\n   * @since 1.0.0\n   */\n  @Evolving\n  def build(): StructField = {\n    val metadataBuilder = new MetadataBuilder()\n    if (generationExpr.nonEmpty) {\n      metadataBuilder.putString(GENERATION_EXPRESSION_METADATA_KEY, generationExpr.get)\n    }\n\n    identityAllowExplicitInsert.ifDefined { allowExplicitInsert =>\n      if (generationExpr.nonEmpty) {\n        throw DeltaErrors.identityColumnWithGenerationExpression()\n      }\n\n      if (dataType != null && dataType != LongType) {\n        throw DeltaErrors.identityColumnDataTypeNotSupported(dataType)\n      }\n\n      metadataBuilder.putBoolean(\n        IDENTITY_INFO_ALLOW_EXPLICIT_INSERT, allowExplicitInsert)\n      metadataBuilder.putLong(IDENTITY_INFO_START, identityStart.get)\n      val step = identityStep.get\n      if (step == 0L) {\n        throw DeltaErrors.identityColumnIllegalStep()\n      }\n      metadataBuilder.putLong(IDENTITY_INFO_STEP, identityStep.get)\n    }\n\n    if (comment.nonEmpty) {\n      metadataBuilder.putString(\"comment\", comment.get)\n    }\n    val fieldMetadata = metadataBuilder.build()\n    if (dataType == null) {\n      throw DeltaErrors.columnBuilderMissingDataType(colName)\n    }\n    StructField(\n      colName,\n      dataType,\n      nullable = nullable,\n      metadata = fieldMetadata)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/DeltaMergeBuilder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.JavaConverters._\nimport scala.collection.Map\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, PostHocResolveUpCast, PreprocessTableMerge, ResolveDeltaMergeInto}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaTableUtils.withActiveSession\nimport org.apache.spark.sql.delta.DeltaViewHelper\nimport org.apache.spark.sql.delta.util.AnalysisHelper\n\nimport org.apache.spark.annotation._\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.ExtendedAnalysisException\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.AttributeReference\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.functions.expr\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Builder to specify how to merge data from source DataFrame into the target Delta table.\n * You can specify any number of `whenMatched` and `whenNotMatched` clauses.\n * Here are the constraints on these clauses.\n *\n *   - `whenMatched` clauses:\n *\n *     - The condition in a `whenMatched` clause is optional. However, if there are multiple\n *       `whenMatched` clauses, then only the last one may omit the condition.\n *\n *     - When there are more than one `whenMatched` clauses and there are conditions (or the lack\n *       of) such that a row satisfies multiple clauses, then the action for the first clause\n *       satisfied is executed. In other words, the order of the `whenMatched` clauses matters.\n *\n *     - If none of the `whenMatched` clauses match a source-target row pair that satisfy\n *       the merge condition, then the target rows will not be updated or deleted.\n *\n *     - If you want to update all the columns of the target Delta table with the\n *       corresponding column of the source DataFrame, then you can use the\n *       `whenMatched(...).updateAll()`. This is equivalent to\n *       <pre>\n *         whenMatched(...).updateExpr(Map(\n *           (\"col1\", \"source.col1\"),\n *           (\"col2\", \"source.col2\"),\n *           ...))\n *       </pre>\n *\n *   - `whenNotMatched` clauses:\n *\n *     - The condition in a `whenNotMatched` clause is optional. However, if there are\n *       multiple `whenNotMatched` clauses, then only the last one may omit the condition.\n *\n *     - When there are more than one `whenNotMatched` clauses and there are conditions (or the\n *       lack of) such that a row satisfies multiple clauses, then the action for the first clause\n *       satisfied is executed. In other words, the order of the `whenNotMatched` clauses matters.\n *\n *     - If no `whenNotMatched` clause is present or if it is present but the non-matching source\n *       row does not satisfy the condition, then the source row is not inserted.\n *\n *     - If you want to insert all the columns of the target Delta table with the\n *       corresponding column of the source DataFrame, then you can use\n *       `whenNotMatched(...).insertAll()`. This is equivalent to\n *       <pre>\n *         whenNotMatched(...).insertExpr(Map(\n *           (\"col1\", \"source.col1\"),\n *           (\"col2\", \"source.col2\"),\n *           ...))\n *       </pre>\n *\n *   - `whenNotMatchedBySource` clauses:\n *\n *     - The condition in a `whenNotMatchedBySource` clause is optional. However, if there are\n *       multiple `whenNotMatchedBySource` clauses, then only the last one may omit the condition.\n *\n *     - When there are more than one `whenNotMatchedBySource` clauses and there are conditions (or\n *       the lack of) such that a row satisfies multiple clauses, then the action for the first\n *       clause satisfied is executed. In other words, the order of the `whenNotMatchedBySource`\n *       clauses matters.\n *\n *     - If no `whenNotMatchedBySource` clause is present or if it is present but the\n *       non-matching target row does not satisfy any of the `whenNotMatchedBySource` clause\n *       condition, then the target row will not be updated or deleted.\n *\n *\n * Scala example to update a key-value Delta table with new key-values from a source DataFrame:\n * {{{\n *    deltaTable\n *     .as(\"target\")\n *     .merge(\n *       source.as(\"source\"),\n *       \"target.key = source.key\")\n *     .withSchemaEvolution()\n *     .whenMatched()\n *     .updateExpr(Map(\n *       \"value\" -> \"source.value\"))\n *     .whenNotMatched()\n *     .insertExpr(Map(\n *       \"key\" -> \"source.key\",\n *       \"value\" -> \"source.value\"))\n *     .whenNotMatchedBySource()\n *     .updateExpr(Map(\n *       \"value\" -> \"target.value + 1\"))\n *     .execute()\n * }}}\n *\n * Java example to update a key-value Delta table with new key-values from a source DataFrame:\n * {{{\n *    deltaTable\n *     .as(\"target\")\n *     .merge(\n *       source.as(\"source\"),\n *       \"target.key = source.key\")\n *     .withSchemaEvolution()\n *     .whenMatched()\n *     .updateExpr(\n *        new HashMap<String, String>() {{\n *          put(\"value\", \"source.value\");\n *        }})\n *     .whenNotMatched()\n *     .insertExpr(\n *        new HashMap<String, String>() {{\n *         put(\"key\", \"source.key\");\n *         put(\"value\", \"source.value\");\n *       }})\n *     .whenNotMatchedBySource()\n *     .updateExpr(\n *        new HashMap<String, String>() {{\n *         put(\"value\", \"target.value + 1\");\n *       }})\n *     .execute();\n * }}}\n *\n * @since 0.3.0\n */\nclass DeltaMergeBuilder private(\n    private val targetTable: DeltaTable,\n    private val source: DataFrame,\n    private val onCondition: Column,\n    private val whenClauses: Seq[DeltaMergeIntoClause],\n    private val schemaEvolutionEnabled: Boolean)\n  extends AnalysisHelper\n  with Logging\n  {\n\n  def this(\n      targetTable: DeltaTable,\n      source: DataFrame,\n      onCondition: Column,\n      whenClauses: Seq[DeltaMergeIntoClause]) =\n    this(targetTable, source, onCondition, whenClauses, schemaEvolutionEnabled = false)\n\n  /**\n   * Build the actions to perform when the merge condition was matched.  This returns\n   * [[DeltaMergeMatchedActionBuilder]] object which can be used to specify how\n   * to update or delete the matched target table row with the source row.\n   * @since 0.3.0\n   */\n  def whenMatched(): DeltaMergeMatchedActionBuilder = {\n    DeltaMergeMatchedActionBuilder(this, None)\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was matched and\n   * the given `condition` is true. This returns [[DeltaMergeMatchedActionBuilder]] object\n   * which can be used to specify how to update or delete the matched target table row with the\n   * source row.\n   *\n   * @param condition boolean expression as a SQL formatted string\n   * @since 0.3.0\n   */\n  def whenMatched(condition: String): DeltaMergeMatchedActionBuilder = {\n    whenMatched(expr(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was matched and\n   * the given `condition` is true. This returns a [[DeltaMergeMatchedActionBuilder]] object\n   * which can be used to specify how to update or delete the matched target table row with the\n   * source row.\n   *\n   * @param condition boolean expression as a Column object\n   * @since 0.3.0\n   */\n  def whenMatched(condition: Column): DeltaMergeMatchedActionBuilder = {\n    DeltaMergeMatchedActionBuilder(this, Some(condition))\n  }\n\n  /**\n   * Build the action to perform when the merge condition was not matched. This returns\n   * [[DeltaMergeNotMatchedActionBuilder]] object which can be used to specify how\n   * to insert the new sourced row into the target table.\n   * @since 0.3.0\n   */\n  def whenNotMatched(): DeltaMergeNotMatchedActionBuilder = {\n    DeltaMergeNotMatchedActionBuilder(this, None)\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched and\n   * the given `condition` is true. This returns [[DeltaMergeMatchedActionBuilder]] object\n   * which can be used to specify how to insert the new sourced row into the target table.\n   *\n   * @param condition boolean expression as a SQL formatted string\n   * @since 0.3.0\n   */\n  def whenNotMatched(condition: String): DeltaMergeNotMatchedActionBuilder = {\n    whenNotMatched(expr(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched and\n   * the given `condition` is true. This returns [[DeltaMergeMatchedActionBuilder]] object\n   * which can be used to specify how to insert the new sourced row into the target table.\n   *\n   * @param condition boolean expression as a Column object\n   * @since 0.3.0\n   */\n  def whenNotMatched(condition: Column): DeltaMergeNotMatchedActionBuilder = {\n    DeltaMergeNotMatchedActionBuilder(this, Some(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched by the source. This\n   * returns [[DeltaMergeNotMatchedBySourceActionBuilder]] object which can be used to specify how\n   * to update or delete the target table row.\n   * @since 2.3.0\n   */\n  def whenNotMatchedBySource(): DeltaMergeNotMatchedBySourceActionBuilder = {\n    DeltaMergeNotMatchedBySourceActionBuilder(this, None)\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched by the source and the\n   * given `condition` is true. This returns [[DeltaMergeNotMatchedBySourceActionBuilder]] object\n   * which can be used to specify how to update or delete the target table row.\n   *\n   * @param condition boolean expression as a SQL formatted string\n   * @since 2.3.0\n   */\n  def whenNotMatchedBySource(condition: String): DeltaMergeNotMatchedBySourceActionBuilder = {\n    whenNotMatchedBySource(expr(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched by the source and the\n   * given `condition` is true. This returns [[DeltaMergeNotMatchedBySourceActionBuilder]] object\n   * which can be used to specify how to update or delete the target table row .\n   *\n   * @param condition boolean expression as a Column object\n   * @since 2.3.0\n   */\n  def whenNotMatchedBySource(condition: Column): DeltaMergeNotMatchedBySourceActionBuilder = {\n    DeltaMergeNotMatchedBySourceActionBuilder(this, Some(condition))\n  }\n\n  /**\n   * Enable schema evolution for the merge operation. This allows the schema of the target\n   * table/columns to be automatically updated based on the schema of the source table/columns.\n   *\n   * @since 3.2.0\n   */\n  def withSchemaEvolution(): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(\n      this.targetTable,\n      this.source,\n      this.onCondition,\n      this.whenClauses,\n      schemaEvolutionEnabled = true)\n  }\n\n  /**\n   * Execute the merge operation based on the built matched and not matched actions.\n   *\n   * @since 0.3.0\n   */\n  def execute(): DataFrame = improveUnsupportedOpError {\n    val sparkSession = targetTable.toDF.sparkSession\n    withActiveSession(sparkSession) {\n      // Note: We are explicitly resolving DeltaMergeInto plan rather than going to through the\n      // Analyzer using `Dataset.ofRows()` because the Analyzer incorrectly resolves all\n      // references in the DeltaMergeInto using both source and target child plans, even before\n      // DeltaAnalysis rule kicks in. This is because the Analyzer  understands only MergeIntoTable,\n      // and handles that separately by skipping resolution (for Delta) and letting the\n      // DeltaAnalysis rule do the resolving correctly. This can be solved by generating\n      // MergeIntoTable instead, which blocked by the different issue with MergeIntoTable as\n      // explained in the function `mergePlan` and\n      // https://issues.apache.org/jira/browse/SPARK-34962.\n      val resolvedMergeInto =\n      ResolveDeltaMergeInto.resolveReferencesAndSchema(mergePlan, sparkSession.sessionState.conf)(\n        tryResolveReferencesForExpressions(sparkSession))\n\n      val strippedMergeInto = resolvedMergeInto.copy(\n        target = DeltaViewHelper.stripTempViewForMerge(resolvedMergeInto.target, SQLConf.get)\n      )\n      // Preprocess the actions and verify\n      var mergeIntoCommand =\n        PreprocessTableMerge(sparkSession.sessionState.conf)(strippedMergeInto)\n      // Resolve UpCast expressions that `PreprocessTableMerge` may have introduced.\n      mergeIntoCommand = PostHocResolveUpCast(sparkSession).apply(mergeIntoCommand)\n      sparkSession.sessionState.analyzer.checkAnalysis(mergeIntoCommand)\n      toDataset(sparkSession, mergeIntoCommand)\n    }\n  }\n\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def withClause(clause: DeltaMergeIntoClause): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(\n      this.targetTable,\n      this.source,\n      this.onCondition,\n      this.whenClauses :+ clause,\n      this.schemaEvolutionEnabled)\n  }\n\n  private def mergePlan: DeltaMergeInto = {\n    var targetPlan = targetTable.toDF.queryExecution.analyzed\n    var sourcePlan = source.queryExecution.analyzed\n    var condition = onCondition.expr\n    var clauses = whenClauses\n\n    // If source and target have duplicate, pre-resolved references (can happen with self-merge),\n    // then rewrite the references in target with new exprId to avoid ambiguity.\n    // We rewrite the target instead of ths source because the source plan can be arbitrary and\n    // we know that the target plan is simple combination of LogicalPlan and an\n    // optional SubqueryAlias.\n    val duplicateResolvedRefs = targetPlan.outputSet.intersect(sourcePlan.outputSet)\n    if (duplicateResolvedRefs.nonEmpty) {\n      val exprs = (condition +: clauses).map(_.transform {\n        // If any expression contain duplicate, pre-resolved references, we can't simply\n        // replace the references in the same way as the target because we don't know\n        // whether the user intended to refer to the source or the target columns. Instead,\n        // we unresolve them (only the duplicate refs) and let the analysis resolve the ambiguity\n        // and throw the usual error messages when needed.\n        case a: AttributeReference if duplicateResolvedRefs.contains(a) =>\n          UnresolvedAttribute(a.qualifier :+ a.name)\n      })\n      // Deduplicate the attribute IDs in the target and source plans, and all the MERGE\n      // expressions (condition and MERGE clauses), so that we can avoid duplicated attribute ID\n      // when building the MERGE command later.\n      val fakePlan = AnalysisHelper.FakeLogicalPlan(exprs, Seq(sourcePlan, targetPlan))\n      val newPlan = org.apache.spark.sql.catalyst.analysis.DeduplicateRelations(fakePlan)\n        .asInstanceOf[AnalysisHelper.FakeLogicalPlan]\n      sourcePlan = newPlan.children(0)\n      targetPlan = newPlan.children(1)\n      condition = newPlan.exprs.head\n      clauses = newPlan.exprs.takeRight(clauses.size).asInstanceOf[Seq[DeltaMergeIntoClause]]\n    }\n\n    // Note: The Scala API cannot generate MergeIntoTable just like the SQL parser because\n    // UpdateAction in MergeIntoTable does not have any way to differentiate between\n    // the representations of `updateAll()` and `update(some-condition, empty-actions)`.\n    // More specifically, UpdateAction with a list of empty Assignments implicitly represents\n    // `updateAll()`, so there is no way to represent `update()` with zero column assignments\n    // (possible in Scala API, but syntactically not possible in SQL). This issue is tracked\n    // by https://issues.apache.org/jira/browse/SPARK-34962.\n    val merge = DeltaMergeInto(\n      targetPlan, sourcePlan, condition, clauses, withSchemaEvolution = schemaEvolutionEnabled)\n    logDebug(\"Generated merged plan:\\n\" + merge)\n    merge\n  }\n}\n\nobject DeltaMergeBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      targetTable: DeltaTable,\n      source: DataFrame,\n      onCondition: Column): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(targetTable, source, onCondition, Nil)\n  }\n}\n\n/**\n * Builder class to specify the actions to perform when a target table row has matched a\n * source row based on the given merge condition and optional match condition.\n *\n * See [[DeltaMergeBuilder]] for more information.\n *\n * @since 0.3.0\n */\nclass DeltaMergeMatchedActionBuilder private(\n    private val mergeBuilder: DeltaMergeBuilder,\n    private val matchCondition: Option[Column]) {\n\n  /**\n   * Update the matched table rows based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 0.3.0\n   */\n  def update(set: Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set)\n  }\n\n  /**\n   * Update the matched table rows based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def updateExpr(set: Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set))\n  }\n\n  /**\n   * Update a matched table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as Column objects.\n   * @since 0.3.0\n   */\n  def update(set: java.util.Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set.asScala)\n  }\n\n  /**\n   * Update a matched table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def updateExpr(set: java.util.Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set.asScala))\n  }\n\n  /**\n   * Update all the columns of the matched table row with the values of the\n   * corresponding columns in the source row.\n   * @since 0.3.0\n   */\n  def updateAll(): DeltaMergeBuilder = {\n    val updateClause = DeltaMergeIntoMatchedUpdateClause(\n      matchCondition.map(_.expr),\n      DeltaMergeIntoClause.toActions(Nil, Nil))\n    mergeBuilder.withClause(updateClause)\n  }\n\n  /**\n   * Delete a matched row from the table.\n   * @since 0.3.0\n   */\n  def delete(): DeltaMergeBuilder = {\n    val deleteClause = DeltaMergeIntoMatchedDeleteClause(matchCondition.map(_.expr))\n    mergeBuilder.withClause(deleteClause)\n  }\n\n  private def addUpdateClause(set: Map[String, Column]): DeltaMergeBuilder = {\n    if (set.isEmpty && matchCondition.isEmpty) {\n      // This is a catch all clause that doesn't update anything: we can ignore it.\n      mergeBuilder\n    } else {\n      val setActions = set.toSeq\n      val updateActions = DeltaMergeIntoClause.toActions(\n        colNames = setActions.map(x => UnresolvedAttribute.quotedString(x._1)),\n        exprs = setActions.map(x => x._2.expr),\n        isEmptySeqEqualToStar = false)\n      val updateClause = DeltaMergeIntoMatchedUpdateClause(\n        matchCondition.map(_.expr),\n        updateActions)\n      mergeBuilder.withClause(updateClause)\n    }\n  }\n\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] =\n    map.mapValues(functions.expr(_)).toMap\n}\n\nobject DeltaMergeMatchedActionBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      mergeBuilder: DeltaMergeBuilder,\n      matchCondition: Option[Column]): DeltaMergeMatchedActionBuilder = {\n    new DeltaMergeMatchedActionBuilder(mergeBuilder, matchCondition)\n  }\n}\n\n\n/**\n * Builder class to specify the actions to perform when a source row has not matched any target\n * Delta table row based on the merge condition, but has matched the additional condition\n * if specified.\n *\n * See [[DeltaMergeBuilder]] for more information.\n *\n * @since 0.3.0\n */\nclass DeltaMergeNotMatchedActionBuilder private(\n    private val mergeBuilder: DeltaMergeBuilder,\n    private val notMatchCondition: Option[Column]) {\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Scala map between target column names and\n   *               corresponding expressions as Column objects.\n   * @since 0.3.0\n   */\n  def insert(values: Map[String, Column]): DeltaMergeBuilder = {\n    addInsertClause(values)\n  }\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Scala map between target column names and\n   *               corresponding expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def insertExpr(values: Map[String, String]): DeltaMergeBuilder = {\n    addInsertClause(toStrColumnMap(values))\n  }\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Java map between target column names and\n   *               corresponding expressions as Column objects.\n   * @since 0.3.0\n   */\n  def insert(values: java.util.Map[String, Column]): DeltaMergeBuilder = {\n    addInsertClause(values.asScala)\n  }\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Java map between target column names and\n   *               corresponding expressions as SQL formatted strings.\n   *\n   * @since 0.3.0\n   */\n  def insertExpr(values: java.util.Map[String, String]): DeltaMergeBuilder = {\n    addInsertClause(toStrColumnMap(values.asScala))\n  }\n\n  /**\n   * Insert a new target Delta table row by assigning the target columns to the values of the\n   * corresponding columns in the source row.\n   * @since 0.3.0\n   */\n  def insertAll(): DeltaMergeBuilder = {\n    val insertClause = DeltaMergeIntoNotMatchedInsertClause(\n      notMatchCondition.map(_.expr),\n      DeltaMergeIntoClause.toActions(Nil, Nil))\n    mergeBuilder.withClause(insertClause)\n  }\n\n  private def addInsertClause(setValues: Map[String, Column]): DeltaMergeBuilder = {\n    val values = setValues.toSeq\n    val insertActions = DeltaMergeIntoClause.toActions(\n      colNames = values.map(x => UnresolvedAttribute.quotedString(x._1)),\n      exprs = values.map(x => x._2.expr),\n      isEmptySeqEqualToStar = false)\n    val insertClause = DeltaMergeIntoNotMatchedInsertClause(\n      notMatchCondition.map(_.expr),\n      insertActions)\n    mergeBuilder.withClause(insertClause)\n  }\n\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] =\n    map.mapValues(functions.expr(_)).toMap\n}\n\nobject DeltaMergeNotMatchedActionBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      mergeBuilder: DeltaMergeBuilder,\n      notMatchCondition: Option[Column]): DeltaMergeNotMatchedActionBuilder = {\n    new DeltaMergeNotMatchedActionBuilder(mergeBuilder, notMatchCondition)\n  }\n}\n\n/**\n * Builder class to specify the actions to perform when a target table row has no match in the\n * source table based on the given merge condition and optional match condition.\n *\n * See [[DeltaMergeBuilder]] for more information.\n *\n * @since 2.3.0\n */\nclass DeltaMergeNotMatchedBySourceActionBuilder private(\n    private val mergeBuilder: DeltaMergeBuilder,\n    private val notMatchBySourceCondition: Option[Column]) {\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 2.3.0\n   */\n  def update(set: Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set)\n  }\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 2.3.0\n   */\n  def updateExpr(set: Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set))\n  }\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as Column objects.\n   * @since 2.3.0\n   */\n  def update(set: java.util.Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set.asScala)\n  }\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as SQL formatted strings.\n   * @since 2.3.0\n   */\n  def updateExpr(set: java.util.Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set.asScala))\n  }\n\n  /**\n   * Delete an unmatched row from the target table.\n   * @since 2.3.0\n   */\n  def delete(): DeltaMergeBuilder = {\n    val deleteClause =\n      DeltaMergeIntoNotMatchedBySourceDeleteClause(notMatchBySourceCondition.map(_.expr))\n    mergeBuilder.withClause(deleteClause)\n  }\n\n  private def addUpdateClause(set: Map[String, Column]): DeltaMergeBuilder = {\n    if (set.isEmpty && notMatchBySourceCondition.isEmpty) {\n      // This is a catch all clause that doesn't update anything: we can ignore it.\n      mergeBuilder\n    } else {\n      val setActions = set.toSeq\n      val updateActions = DeltaMergeIntoClause.toActions(\n        colNames = setActions.map(x => UnresolvedAttribute.quotedString(x._1)),\n        exprs = setActions.map(x => x._2.expr),\n        isEmptySeqEqualToStar = false)\n      val updateClause = DeltaMergeIntoNotMatchedBySourceUpdateClause(\n        notMatchBySourceCondition.map(_.expr),\n        updateActions)\n      mergeBuilder.withClause(updateClause)\n    }\n  }\n\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] =\n    map.mapValues(functions.expr(_)).toMap\n}\n\nobject DeltaMergeNotMatchedBySourceActionBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      mergeBuilder: DeltaMergeBuilder,\n      notMatchBySourceCondition: Option[Column]): DeltaMergeNotMatchedBySourceActionBuilder = {\n    new DeltaMergeNotMatchedBySourceActionBuilder(mergeBuilder, notMatchBySourceCondition)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/DeltaOptimizeBuilder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaTableUtils.withActiveSession\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.DeltaOptimizeContext\nimport org.apache.spark.sql.delta.commands.OptimizeTableCommand\nimport org.apache.spark.sql.delta.util.AnalysisHelper\n\nimport org.apache.spark.annotation._\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{ResolvedTable, UnresolvedAttribute}\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.connector.catalog.{Identifier, TableCatalog}\n\n/**\n * Builder class for constructing OPTIMIZE command and executing.\n *\n * @param sparkSession SparkSession to use for execution\n * @param tableIdentifier Id of the table on which to\n *        execute the optimize\n * @param options Hadoop file system options for read and write.\n * @since 2.0.0\n */\nclass DeltaOptimizeBuilder private(table: DeltaTableV2) extends AnalysisHelper {\n  private var partitionFilter: Seq[String] = Seq.empty\n\n  private lazy val tableIdentifier: String =\n    table.tableIdentifier.getOrElse(s\"delta.`${table.deltaLog.dataPath.toString}`\")\n\n  /**\n   * Apply partition filter on this optimize command builder to limit\n   * the operation on selected partitions.\n   * @param partitionFilter The partition filter to apply\n   * @return [[DeltaOptimizeBuilder]] with partition filter applied\n   * @since 2.0.0\n   */\n  def where(partitionFilter: String): DeltaOptimizeBuilder = {\n    this.partitionFilter = this.partitionFilter :+ partitionFilter\n    this\n  }\n\n  /**\n   * Compact the small files in selected partitions.\n   * @return DataFrame containing the OPTIMIZE execution metrics\n   * @since 2.0.0\n   */\n  def executeCompaction(): DataFrame = {\n    execute(Seq.empty)\n  }\n\n   /**\n   * Z-Order the data in selected partitions using the given columns.\n   * @param columns Zero or more columns to order the data\n   *                using Z-Order curves\n   * @return DataFrame containing the OPTIMIZE execution metrics\n   * @since 2.0.0\n   */\n  @scala.annotation.varargs\n  def executeZOrderBy(columns: String *): DataFrame = {\n    val attrs = columns.map(c => UnresolvedAttribute(c))\n    execute(attrs)\n  }\n\n  private def execute(zOrderBy: Seq[UnresolvedAttribute]): DataFrame = {\n    val sparkSession = table.spark\n    withActiveSession(sparkSession) {\n      val tableId: TableIdentifier = sparkSession\n        .sessionState\n        .sqlParser\n        .parseTableIdentifier(tableIdentifier)\n      val id = Identifier.of(tableId.database.toArray, tableId.identifier)\n      val catalogPlugin = sparkSession.sessionState.catalogManager.currentCatalog\n      val catalog = catalogPlugin match {\n        case tableCatalog: TableCatalog => tableCatalog\n        case _ => throw new IllegalArgumentException(\n          s\"Catalog ${catalogPlugin.name} does not support tables\")\n      }\n      val resolvedTable = ResolvedTable.create(catalog, id, table)\n      val optimize = OptimizeTableCommand(\n        resolvedTable, partitionFilter, DeltaOptimizeContext())(zOrderBy = zOrderBy)\n      toDataset(sparkSession, optimize)\n    }\n  }\n}\n\nprivate[delta] object DeltaOptimizeBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(table: DeltaTableV2): DeltaOptimizeBuilder =\n    new DeltaOptimizeBuilder(table)\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/DeltaTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaTableUtils.withActiveSession\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.catalog.{CatalogResolver, DeltaTableV2}\nimport org.apache.spark.sql.delta.commands.{AlterTableDropFeatureDeltaCommand, AlterTableSetPropertiesDeltaCommand}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport io.delta.tables.execution._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.annotation._\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Main class for programmatically interacting with Delta tables.\n * You can create DeltaTable instances using the static methods.\n * {{{\n *   DeltaTable.forPath(sparkSession, pathToTheDeltaTable)\n * }}}\n *\n * @since 0.3.0\n */\nclass DeltaTable private[tables](\n    @transient private val _df: Dataset[Row],\n    @transient private val table: DeltaTableV2)\n  extends DeltaTableOperations with Serializable {\n\n  protected def deltaLog: DeltaLog = {\n    /** Assert the codes run in the driver. */\n    if (table == null) {\n      throw DeltaErrors.deltaTableFoundInExecutor()\n    }\n\n    table.deltaLog\n  }\n\n  protected def df: Dataset[Row] = {\n    /** Assert the codes run in the driver. */\n    if (_df == null) {\n      throw DeltaErrors.deltaTableFoundInExecutor()\n    }\n\n    _df\n  }\n\n  /**\n   * Apply an alias to the DeltaTable. This is similar to `Dataset.as(alias)` or\n   * SQL `tableName AS alias`.\n   *\n   * @since 0.3.0\n   */\n  def as(alias: String): DeltaTable = new DeltaTable(df.as(alias), table)\n\n  /**\n   * Apply an alias to the DeltaTable. This is similar to `Dataset.as(alias)` or\n   * SQL `tableName AS alias`.\n   *\n   * @since 0.3.0\n   */\n  def alias(alias: String): DeltaTable = as(alias)\n\n  /**\n   * Get a DataFrame (that is, Dataset[Row]) representation of this Delta table.\n   *\n   * @since 0.3.0\n   */\n  def toDF: Dataset[Row] = df\n\n  /**\n   * Recursively delete files and directories in the table that are not needed by the table for\n   * maintaining older versions up to the given retention threshold. This method will return an\n   * empty DataFrame on successful completion.\n   *\n   * @param retentionHours The retention threshold in hours. Files required by the table for\n   *                       reading versions earlier than this will be preserved and the\n   *                       rest of them will be deleted.\n   * @since 0.3.0\n   */\n  def vacuum(retentionHours: Double): DataFrame = {\n    executeVacuum(table, Some(retentionHours))\n  }\n\n  /**\n   * Recursively delete files and directories in the table that are not needed by the table for\n   * maintaining older versions up to the given retention threshold. This method will return an\n   * empty DataFrame on successful completion.\n   *\n   * note: This will use the default retention period of 7 days.\n   *\n   * @since 0.3.0\n   */\n  def vacuum(): DataFrame = {\n    executeVacuum(table, retentionHours = None)\n  }\n\n  /**\n   * Get the information of the latest `limit` commits on this table as a Spark DataFrame.\n   * The information is in reverse chronological order.\n   *\n   * @param limit The number of previous commands to get history for\n   *\n   * @since 0.3.0\n   */\n  def history(limit: Int): DataFrame = {\n    executeHistory(deltaLog, Some(limit), table.catalogTable)\n  }\n\n  /**\n   * Get the information available commits on this table as a Spark DataFrame.\n   * The information is in reverse chronological order.\n   *\n   * @since 0.3.0\n   */\n  def history(): DataFrame = {\n    executeHistory(deltaLog, catalogTable = table.catalogTable)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Get the details of a Delta table such as the format, name, and size.\n   *\n   * @since 2.1.0\n   */\n  @Evolving\n  def detail(): DataFrame = {\n    executeDetails(deltaLog.dataPath.toString, table.getTableIdentifierIfExists)\n  }\n\n  /**\n   * Generate a manifest for the given Delta Table\n   *\n   * @param mode Specifies the mode for the generation of the manifest.\n   *             The valid modes are as follows (not case sensitive):\n   *              - \"symlink_format_manifest\" : This will generate manifests in symlink format\n   *                                            for Presto and Athena read support.\n   *             See the online documentation for more information.\n   * @since 0.5.0\n   */\n  def generate(mode: String): Unit = {\n    executeGenerate(deltaLog.dataPath.toString, table.getTableIdentifierIfExists, mode)\n  }\n\n  /**\n   * Delete data from the table that match the given `condition`.\n   *\n   * @param condition Boolean SQL expression\n   *\n   * @since 0.3.0\n   */\n  def delete(condition: String): Unit = {\n    delete(functions.expr(condition))\n  }\n\n  /**\n   * Delete data from the table that match the given `condition`.\n   *\n   * @param condition Boolean SQL expression\n   *\n   * @since 0.3.0\n   */\n  def delete(condition: Column): Unit = {\n    executeDelete(Some(condition.expr))\n  }\n\n  /**\n   * Delete data from the table.\n   *\n   * @since 0.3.0\n   */\n  def delete(): Unit = {\n    executeDelete(None)\n  }\n\n  /**\n   * Optimize the data layout of the table. This returns\n   * a [[DeltaOptimizeBuilder]] object that can be used to specify\n   * the partition filter to limit the scope of optimize and\n   * also execute different optimization techniques such as file\n   * compaction or order data using Z-Order curves.\n   *\n   * See the [[DeltaOptimizeBuilder]] for a full description\n   * of this operation.\n   *\n   * Scala example to run file compaction on a subset of\n   * partitions in the table:\n   * {{{\n   *    deltaTable\n   *     .optimize()\n   *     .where(\"date='2021-11-18'\")\n   *     .executeCompaction();\n   * }}}\n   *\n   * @since 2.0.0\n   */\n  def optimize(): DeltaOptimizeBuilder = DeltaOptimizeBuilder(table)\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.functions._\n   *\n   *    deltaTable.update(Map(\"data\" -> col(\"data\") + 1))\n   * }}}\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 0.3.0\n   */\n  def update(set: Map[String, Column]): Unit = {\n    executeUpdate(set, None)\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.Column;\n   *    import org.apache.spark.sql.functions;\n   *\n   *    deltaTable.update(\n   *      new HashMap<String, Column>() {{\n   *        put(\"data\", functions.col(\"data\").plus(1));\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 0.3.0\n   */\n  def update(set: java.util.Map[String, Column]): Unit = {\n    executeUpdate(set.asScala, None)\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`\n   * based on the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.functions._\n   *\n   *    deltaTable.update(\n   *      col(\"date\") > \"2018-01-01\",\n   *      Map(\"data\" -> col(\"data\") + 1))\n   * }}}\n   *\n   * @param condition boolean expression as Column object specifying which rows to update.\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 0.3.0\n   */\n  def update(condition: Column, set: Map[String, Column]): Unit = {\n    executeUpdate(set, Some(condition))\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`\n   * based on the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.Column;\n   *    import org.apache.spark.sql.functions;\n   *\n   *    deltaTable.update(\n   *      functions.col(\"date\").gt(\"2018-01-01\"),\n   *      new HashMap<String, Column>() {{\n   *        put(\"data\", functions.col(\"data\").plus(1));\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param condition boolean expression as Column object specifying which rows to update.\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 0.3.0\n   */\n  def update(condition: Column, set: java.util.Map[String, Column]): Unit = {\n    executeUpdate(set.asScala, Some(condition))\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    deltaTable.updateExpr(Map(\"data\" -> \"data + 1\")))\n   * }}}\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def updateExpr(set: Map[String, String]): Unit = {\n    executeUpdate(toStrColumnMap(set), None)\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    deltaTable.updateExpr(\n   *      new HashMap<String, String>() {{\n   *        put(\"data\", \"data + 1\");\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def updateExpr(set: java.util.Map[String, String]): Unit = {\n    executeUpdate(toStrColumnMap(set.asScala), None)\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`,\n   * which performs the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    deltaTable.update(\n   *      \"date > '2018-01-01'\",\n   *      Map(\"data\" -> \"data + 1\"))\n   * }}}\n   *\n   * @param condition boolean expression as SQL formatted string object specifying\n   *                  which rows to update.\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def updateExpr(condition: String, set: Map[String, String]): Unit = {\n    executeUpdate(toStrColumnMap(set), Some(functions.expr(condition)))\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`,\n   * which performs the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    deltaTable.update(\n   *      \"date > '2018-01-01'\",\n   *      new HashMap<String, String>() {{\n   *        put(\"data\", \"data + 1\");\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param condition boolean expression as SQL formatted string object specifying\n   *                  which rows to update.\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 0.3.0\n   */\n  def updateExpr(condition: String, set: java.util.Map[String, String]): Unit = {\n    executeUpdate(toStrColumnMap(set.asScala), Some(functions.expr(condition)))\n  }\n\n  /**\n   * Merge data from the `source` DataFrame based on the given merge `condition`. This returns\n   * a [[DeltaMergeBuilder]] object that can be used to specify the update, delete, or insert\n   * actions to be performed on rows based on whether the rows matched the condition or not.\n   *\n   * See the [[DeltaMergeBuilder]] for a full description of this operation and what combinations of\n   * update, delete and insert operations are allowed.\n   *\n   * Scala example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(Map(\n   *       \"value\" -> \"source.value\"))\n   *     .whenNotMatched\n   *     .insertExpr(Map(\n   *       \"key\" -> \"source.key\",\n   *       \"value\" -> \"source.value\"))\n   *     .execute()\n   * }}}\n   *\n   * Java example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(\n   *        new HashMap<String, String>() {{\n   *          put(\"value\" -> \"source.value\");\n   *        }})\n   *     .whenNotMatched\n   *     .insertExpr(\n   *        new HashMap<String, String>() {{\n   *         put(\"key\", \"source.key\");\n   *         put(\"value\", \"source.value\");\n   *       }})\n   *     .execute();\n   * }}}\n   *\n   * @param source source Dataframe to be merged.\n   * @param condition boolean expression as SQL formatted string\n   * @since 0.3.0\n   */\n  def merge(source: DataFrame, condition: String): DeltaMergeBuilder = {\n    merge(source, functions.expr(condition))\n  }\n\n  /**\n   * Merge data from the `source` DataFrame based on the given merge `condition`. This returns\n   * a [[DeltaMergeBuilder]] object that can be used to specify the update, delete, or insert\n   * actions to be performed on rows based on whether the rows matched the condition or not.\n   *\n   * See the [[DeltaMergeBuilder]] for a full description of this operation and what combinations of\n   * update, delete and insert operations are allowed.\n   *\n   * Scala example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(Map(\n   *       \"value\" -> \"source.value\"))\n   *     .whenNotMatched\n   *     .insertExpr(Map(\n   *       \"key\" -> \"source.key\",\n   *       \"value\" -> \"source.value\"))\n   *     .execute()\n   * }}}\n   *\n   * Java example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(\n   *        new HashMap<String, String>() {{\n   *          put(\"value\" -> \"source.value\")\n   *        }})\n   *     .whenNotMatched\n   *     .insertExpr(\n   *        new HashMap<String, String>() {{\n   *         put(\"key\", \"source.key\");\n   *         put(\"value\", \"source.value\");\n   *       }})\n   *     .execute()\n   * }}}\n   *\n   * @param source source Dataframe to be merged.\n   * @param condition boolean expression as a Column object\n   * @since 0.3.0\n   */\n  def merge(source: DataFrame, condition: Column): DeltaMergeBuilder = {\n    DeltaMergeBuilder(this, source, condition)\n  }\n\n  /**\n   * Restore the DeltaTable to an older version of the table specified by version number.\n   *\n   * An example would be\n   * {{{ io.delta.tables.DeltaTable.restoreToVersion(7) }}}\n   *\n   * @since 1.2.0\n   */\n  def restoreToVersion(version: Long): DataFrame = {\n    executeRestore(table, Some(version), None)\n  }\n\n  /**\n   * Restore the DeltaTable to an older version of the table specified by a timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss\n   *\n   * An example would be\n   * {{{ io.delta.tables.DeltaTable.restoreToTimestamp(\"2019-01-01\") }}}\n   *\n   * @since 1.2.0\n   */\n  def restoreToTimestamp(timestamp: String): DataFrame = {\n    executeRestore(table, None, Some(timestamp))\n  }\n\n  /**\n   * Updates the protocol version of the table to leverage new features. Upgrading the reader\n   * version will prevent all clients that have an older version of Delta Lake from accessing this\n   * table. Upgrading the writer version will prevent older versions of Delta Lake to write to this\n   * table. The reader or writer version cannot be downgraded.\n   *\n   * See online documentation and Delta's protocol specification at PROTOCOL.md for more details.\n   *\n   * @since 0.8.0\n   */\n  def upgradeTableProtocol(readerVersion: Int, writerVersion: Int): Unit =\n    withActiveSession(sparkSession) {\n      val alterTableCmd = AlterTableSetPropertiesDeltaCommand(\n        table,\n        DeltaConfigs.validateConfigurations(\n          Map(\n            \"delta.minReaderVersion\" -> readerVersion.toString,\n            \"delta.minWriterVersion\" -> writerVersion.toString)))\n      toDataset(sparkSession, alterTableCmd)\n    }\n\n  /**\n   * Modify the protocol to add a supported feature, and if the table does not support table\n   * features, upgrade the protocol automatically. In such a case when the provided feature is\n   * writer-only, the table's writer version will be upgraded to `7`, and when the provided\n   * feature is reader-writer, both reader and writer versions will be upgraded, to `(3, 7)`.\n   *\n   * See online documentation and Delta's protocol specification at PROTOCOL.md for more details.\n   *\n   * @since 2.3.0\n   */\n  def addFeatureSupport(featureName: String): Unit = withActiveSession(sparkSession) {\n    // Do not check for the correctness of the provided feature name. The ALTER TABLE command will\n    // do that in a transaction.\n    val alterTableCmd = AlterTableSetPropertiesDeltaCommand(\n      table,\n      Map(\n        TableFeatureProtocolUtils.propertyKey(featureName) ->\n          TableFeatureProtocolUtils.FEATURE_PROP_SUPPORTED))\n    toDataset(sparkSession, alterTableCmd)\n  }\n\n  private def executeDropFeature(featureName: String, truncateHistory: Option[Boolean]): Unit = {\n    val alterTableCmd = AlterTableDropFeatureDeltaCommand(\n      table = table,\n      featureName = featureName,\n      truncateHistory = truncateHistory.getOrElse(false))\n    toDataset(sparkSession, alterTableCmd)\n  }\n\n  /**\n   * Modify the protocol to drop a supported feature. The operation always normalizes the\n   * resulting protocol. Protocol normalization is the process of converting a table features\n   * protocol to the weakest possible form. This primarily refers to converting a table features\n   * protocol to a legacy protocol. A table features protocol can be represented with the legacy\n   * representation only when the feature set of the former exactly matches a legacy protocol.\n   * Normalization can also decrease the reader version of a table features protocol when it is\n   * higher than necessary. For example:\n   *\n   * (1, 7, None, {AppendOnly, Invariants, CheckConstraints}) -> (1, 3)\n   * (3, 7, None, {RowTracking}) -> (1, 7, RowTracking)\n   *\n   * The dropFeatureSupport method can be used as follows:\n   * {{{\n   *   io.delta.tables.DeltaTable.dropFeatureSupport(\"rowTracking\")\n   * }}}\n   *\n   * See online documentation for more details.\n   *\n   * @param featureName The name of the feature to drop.\n   * @param truncateHistory Whether to truncate history before downgrading the protocol.\n   * @return None.\n   * @since 3.4.0\n   */\n  def dropFeatureSupport(\n      featureName: String,\n      truncateHistory: Boolean): Unit = withActiveSession(sparkSession) {\n    executeDropFeature(featureName, Some(truncateHistory))\n  }\n\n  /**\n   * Modify the protocol to drop a supported feature. The operation always normalizes the\n   * resulting protocol. Protocol normalization is the process of converting a table features\n   * protocol to the weakest possible form. This primarily refers to converting a table features\n   * protocol to a legacy protocol. A table features protocol can be represented with the legacy\n   * representation only when the feature set of the former exactly matches a legacy protocol.\n   * Normalization can also decrease the reader version of a table features protocol when it is\n   * higher than necessary. For example:\n   *\n   * (1, 7, None, {AppendOnly, Invariants, CheckConstraints}) -> (1, 3)\n   * (3, 7, None, {RowTracking}) -> (1, 7, RowTracking)\n   *\n   * The dropFeatureSupport method can be used as follows:\n   * {{{\n   *   io.delta.tables.DeltaTable.dropFeatureSupport(\"rowTracking\")\n   * }}}\n   *\n   * Note, this command will not truncate history.\n   *\n   * See online documentation for more details.\n   *\n   * @param featureName The name of the feature to drop.\n   * @return None.\n   * @since 3.4.0\n   */\n  def dropFeatureSupport(featureName: String): Unit = withActiveSession(sparkSession) {\n    executeDropFeature(featureName, None)\n  }\n\n  /**\n   * Clone a DeltaTable to a given destination to mirror the existing table's data and metadata.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *  io.delta.tables.DeltaTable.clone(\n   *   \"/some/path/to/table\",\n   *   true,\n   *   true,\n   *   Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   *\n   * @since 3.3.0\n   */\n  def clone(\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String]): DeltaTable = {\n    executeClone(\n      table,\n      target,\n      isShallow,\n      replace,\n      properties,\n      versionAsOf = None,\n      timestampAsOf = None)\n  }\n\n  /**\n   * clone used by Python implementation using java.util.HashMap for the properties argument.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.clone(\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   */\n  def clone(\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: java.util.HashMap[String, String]): DeltaTable = {\n    val scalaProps = Option(properties).map(_.asScala.toMap).getOrElse(Map.empty[String, String])\n    executeClone(\n      table,\n      target,\n      isShallow,\n      replace,\n      scalaProps,\n      versionAsOf = None,\n      timestampAsOf = None)\n  }\n\n  /**\n   * Clone a DeltaTable to a given destination to mirror the existing table's data and metadata.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.clone(\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true)\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   *\n   * @since 3.3.0\n   */\n  def clone(target: String, isShallow: Boolean, replace: Boolean): DeltaTable = {\n    clone(target, isShallow, replace, properties = Map.empty[String, String])\n  }\n\n  /**\n   * Clone a DeltaTable to a given destination to mirror the existing table's data and metadata.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.clone(\n   *     \"/some/path/to/table\",\n   *     true)\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   *\n   * @since 3.3.0\n   */\n  def clone(target: String, isShallow: Boolean): DeltaTable = {\n    clone(target, isShallow, replace = false)\n  }\n\n  /**\n   * Clone a DeltaTable at a specific version to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   *\n   * @since 3.3.0\n   */\n  def cloneAtVersion(\n      version: Long,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String]): DeltaTable = {\n    executeClone(\n      table,\n      target,\n      isShallow,\n      replace,\n      properties,\n      versionAsOf = Some(version),\n      timestampAsOf = None)\n  }\n\n  /**\n   * cloneAtVersion used by Python implementation using java.util.HashMap for the properties\n   * argument.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     new java.util.HashMap[String, String](Map(\"foo\" -> \"bar\").asJava))\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   */\n  def cloneAtVersion(\n      version: Long,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: java.util.HashMap[String, String]): DeltaTable = {\n    val scalaProps = Option(properties).map(_.asScala.toMap).getOrElse(Map.empty[String, String])\n    executeClone(\n      table,\n      target,\n      isShallow,\n      replace,\n      scalaProps,\n      versionAsOf = Some(version),\n      timestampAsOf = None)\n  }\n\n  /**\n   * Clone a DeltaTable at a specific version to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true)\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   *\n   * @since 3.3.0\n   */\n  def cloneAtVersion(\n      version: Long,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean): DeltaTable = {\n    cloneAtVersion(version, target, isShallow, replace, properties = Map.empty[String, String])\n  }\n\n  /**\n   * Clone a DeltaTable at a specific version to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true)\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   *\n   * @since 3.3.0\n   */\n  def cloneAtVersion(version: Long, target: String, isShallow: Boolean): DeltaTable = {\n    cloneAtVersion(version, target, isShallow, replace = false)\n  }\n\n  /**\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtTimestamp(\n   *     \"2019-01-01\",\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   *\n   * @since 3.3.0\n   */\n  def cloneAtTimestamp(\n      timestamp: String,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String]): DeltaTable = {\n    executeClone(\n      table,\n      target,\n      isShallow,\n      replace,\n      properties,\n      versionAsOf = None,\n      timestampAsOf = Some(timestamp)\n    )\n  }\n\n  /**\n   * cloneAtTimestamp used by Python implementation using java.util.HashMap for the properties\n   * argument.\n   *\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     new java.util.HashMap[String, String](Map(\"foo\" -> \"bar\").asJava)\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   */\n  def cloneAtTimestamp(\n      timestamp: String,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: java.util.HashMap[String, String]): DeltaTable = {\n    val scalaProps = Option(properties).map(_.asScala.toMap).getOrElse(Map.empty[String, String])\n    executeClone(\n      table,\n      target,\n      isShallow,\n      replace,\n      scalaProps,\n      versionAsOf = None,\n      timestampAsOf = Some(timestamp))\n  }\n\n  /**\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtTimestamp(\n   *     \"2019-01-01\",\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true)\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   *\n   * @since 3.3.0\n   */\n  def cloneAtTimestamp(\n      timestamp: String,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean): DeltaTable = {\n    cloneAtTimestamp(timestamp, target, isShallow, replace, properties = Map.empty[String, String])\n  }\n\n  /**\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtTimestamp(\n   *     \"2019-01-01\",\n   *     \"/some/path/to/table\",\n   *     true)\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   *\n   * @since 3.3.0\n   */\n  def cloneAtTimestamp(timestamp: String, target: String, isShallow: Boolean): DeltaTable = {\n    cloneAtTimestamp(timestamp, target, isShallow, replace = false)\n  }\n}\n\n/**\n * Companion object to create DeltaTable instances.\n *\n * {{{\n *   DeltaTable.forPath(sparkSession, pathToTheDeltaTable)\n * }}}\n *\n * @since 0.3.0\n */\nobject DeltaTable {\n\n  /**\n   * Create a DeltaTable from the given parquet table and partition schema.\n   * Takes an existing parquet table and constructs a delta transaction log in the base path of\n   * that table.\n   *\n   * Note: Any changes to the table during the conversion process may not result in a consistent\n   * state at the end of the conversion. Users should stop any changes to the table before the\n   * conversion is started.\n   *\n   * An example usage would be\n   * {{{\n   *  io.delta.tables.DeltaTable.convertToDelta(\n   *   spark,\n   *   \"parquet.`/path`\",\n   *   new StructType().add(StructField(\"key1\", LongType)).add(StructField(\"key2\", StringType)))\n   * }}}\n   *\n   * @since 0.4.0\n   */\n  def convertToDelta(\n      spark: SparkSession,\n      identifier: String,\n      partitionSchema: StructType): DeltaTable = {\n    val tableId: TableIdentifier = spark.sessionState.sqlParser.parseTableIdentifier(identifier)\n    DeltaConvert.executeConvert(spark, tableId, Some(partitionSchema), None)\n  }\n\n  /**\n   * Create a DeltaTable from the given parquet table and partition schema.\n   * Takes an existing parquet table and constructs a delta transaction log in the base path of\n   * that table.\n   *\n   * Note: Any changes to the table during the conversion process may not result in a consistent\n   * state at the end of the conversion. Users should stop any changes to the table before the\n   * conversion is started.\n   *\n   * An example usage would be\n   * {{{\n   *  io.delta.tables.DeltaTable.convertToDelta(\n   *   spark,\n   *   \"parquet.`/path`\",\n   *   \"key1 long, key2 string\")\n   * }}}\n   *\n   * @since 0.4.0\n   */\n  def convertToDelta(\n      spark: SparkSession,\n      identifier: String,\n      partitionSchema: String): DeltaTable = {\n    val tableId: TableIdentifier = spark.sessionState.sqlParser.parseTableIdentifier(identifier)\n    DeltaConvert.executeConvert(spark, tableId, Some(StructType.fromDDL(partitionSchema)), None)\n  }\n\n  /**\n   * Create a DeltaTable from the given parquet table. Takes an existing parquet table and\n   * constructs a delta transaction log in the base path of the table.\n   *\n   * Note: Any changes to the table during the conversion process may not result in a consistent\n   * state at the end of the conversion. Users should stop any changes to the table before the\n   * conversion is started.\n   *\n   * An Example would be\n   * {{{\n   *  io.delta.tables.DeltaTable.convertToDelta(\n   *   spark,\n   *   \"parquet.`/path`\"\n   * }}}\n   *\n   * @since 0.4.0\n   */\n  def convertToDelta(\n      spark: SparkSession,\n      identifier: String): DeltaTable = {\n    val tableId: TableIdentifier = spark.sessionState.sqlParser.parseTableIdentifier(identifier)\n    DeltaConvert.executeConvert(spark, tableId, None, None)\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object representing the data at the given path, If the given\n   * path is invalid (i.e. either no table exists or an existing table is not a Delta table),\n   * it throws a `not a Delta table` error.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 0.3.0\n   */\n  def forPath(path: String): DeltaTable = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    forPath(sparkSession, path)\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object representing the data at the given path, If the given\n   * path is invalid (i.e. either no table exists or an existing table is not a Delta table),\n   * it throws a `not a Delta table` error.\n   *\n   * @since 0.3.0\n   */\n  def forPath(sparkSession: SparkSession, path: String): DeltaTable = {\n    forPath(sparkSession, path, Map.empty[String, String])\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object representing the data at the given path, If the given\n   * path is invalid (i.e. either no table exists or an existing table is not a Delta table),\n   * it throws a `not a Delta table` error.\n   *\n   * @param hadoopConf Hadoop configuration starting with \"fs.\" or \"dfs.\" will be picked up\n   *                    by `DeltaTable` to access the file system when executing queries.\n   *                    Other configurations will not be allowed.\n   *\n   * {{{\n   *   val hadoopConf = Map(\n   *     \"fs.s3a.access.key\" -> \"<access-key>\",\n   *     \"fs.s3a.secret.key\" -> \"<secret-key>\"\n   *   )\n   *   DeltaTable.forPath(spark, \"/path/to/table\", hadoopConf)\n   * }}}\n   * @since 2.2.0\n   */\n  def forPath(\n      sparkSession: SparkSession,\n      path: String,\n      hadoopConf: scala.collection.Map[String, String]): DeltaTable = {\n    // We only pass hadoopConf so that we won't pass any unsafe options to Delta.\n    val badOptions = hadoopConf.filterKeys { k =>\n      !DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n    }.toMap\n    if (!badOptions.isEmpty) {\n      throw DeltaErrors.unsupportedDeltaTableForPathHadoopConf(badOptions)\n    }\n    val fileSystemOptions: Map[String, String] = hadoopConf.toMap\n    val hdpPath = new Path(path)\n    if (DeltaTableUtils.isDeltaTable(sparkSession, hdpPath, fileSystemOptions)) {\n      new DeltaTable(sparkSession.read.format(\"delta\").options(fileSystemOptions).load(path),\n        DeltaTableV2(\n          spark = sparkSession,\n          path = hdpPath,\n          options = fileSystemOptions))\n    } else {\n      throw DeltaErrors.notADeltaTableException(DeltaTableIdentifier(path = Some(path)))\n    }\n  }\n\n  /**\n  * Java friendly API to instantiate a [[DeltaTable]] object representing the data at the given\n  * path, If the given path is invalid (i.e. either no table exists or an existing table is not a\n  * Delta table), it throws a `not a Delta table` error.\n  *\n  * @param hadoopConf Hadoop configuration starting with \"fs.\" or \"dfs.\" will be picked up\n  *                    by `DeltaTable` to access the file system when executing queries.\n  *                    Other configurations will be ignored.\n  *\n  * {{{\n  *   val hadoopConf = Map(\n  *     \"fs.s3a.access.key\" -> \"<access-key>\",\n  *     \"fs.s3a.secret.key\", \"<secret-key>\"\n  *   )\n  *   DeltaTable.forPath(spark, \"/path/to/table\", hadoopConf)\n  * }}}\n  * @since 2.2.0\n  */\n  def forPath(\n      sparkSession: SparkSession,\n      path: String,\n      hadoopConf: java.util.Map[String, String]): DeltaTable = {\n    val fsOptions = hadoopConf.asScala.toMap\n    forPath(sparkSession, path, fsOptions)\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object using the given table name. If the given\n   * tableOrViewName is invalid (i.e. either no table exists or an existing table is not a\n   * Delta table), it throws a `not a Delta table` error. Note: Passing a view name will also\n   * result in this error as views are not supported.\n   *\n   * The given tableOrViewName can also be the absolute path of a delta datasource (i.e.\n   * delta.`path`), If so, instantiate a [[DeltaTable]] object representing the data at\n   * the given path (consistent with the [[forPath]]).\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   */\n  def forName(tableOrViewName: String): DeltaTable = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    forName(sparkSession, tableOrViewName)\n  }\n\n  // Helper to resolve a table using SessionCatalog\n  private def getDeltaTableFromSessionCatalog(\n      spark: SparkSession,\n      tableName: String): DeltaTable = {\n    val tableId = spark.sessionState.sqlParser.parseTableIdentifier(tableName)\n    if (DeltaTableUtils.isDeltaTable(spark, tableId)) {\n      val tbl = spark.sessionState.catalog.getTableMetadata(tableId)\n      new DeltaTable(\n        spark.table(tableName),\n        DeltaTableV2(spark, new Path(tbl.location), Some(tbl), Some(tableName)))\n    } else if (DeltaTableUtils.isValidPath(tableId)) {\n      forPath(spark, tableId.table)\n    } else {\n      throw DeltaErrors.notADeltaTableException(DeltaTableIdentifier(table = Some(tableId)))\n    }\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object using one of the following:\n   * 1. The given tableName using the given SparkSession and SessionCatalog.\n   * 2. The tableName can also be the absolute path of a delta datasource (i.e.\n   * delta.`path`), If so, instantiate a [[DeltaTable]] object representing the data at\n   * the given path (consistent with the [[forPath]]).\n   * 3. A fully qualified tableName is passed in the form `catalog.db.table`, If so\n   * the table is resolved through the specified catalog instead of the default *SessionCatalog*\n   *\n   * If the given tableName is invalid (i.e. either no table exists or an\n   * existing table is not a Delta table), it throws a `not a Delta table` error. Note:\n   * Passing a view name will also result in this error as views are not supported.\n   */\n  def forName(sparkSession: SparkSession, tableName: String): DeltaTable = {\n    sparkSession.sessionState.sqlParser.parseMultipartIdentifier(tableName) match {\n      case parts if parts.length == 3 =>\n        val (catalog, ident) =\n          CatalogResolver.getCatalogPluginAndIdentifier(sparkSession, parts.head, parts.tail)\n        new DeltaTable(\n          sparkSession.table(tableName),\n          CatalogResolver.getDeltaTableFromCatalog(sparkSession, catalog, ident)\n        )\n      case _ =>\n        getDeltaTableFromSessionCatalog(sparkSession, tableName)\n    }\n  }\n\n  /**\n   * Check if the provided `identifier` string, in this case a file path,\n   * is the root of a Delta table using the given SparkSession.\n   *\n   * An example would be\n   * {{{\n   *   DeltaTable.isDeltaTable(spark, \"path/to/table\")\n   * }}}\n   *\n   * @since 0.4.0\n   */\n  def isDeltaTable(sparkSession: SparkSession, identifier: String): Boolean = {\n    val identifierPath = new Path(identifier)\n    if (sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_STRICT_CHECK_DELTA_TABLE)) {\n      val rootOption = DeltaTableUtils.findDeltaTableRoot(sparkSession, identifierPath)\n      rootOption.isDefined && DeltaLog.forTable(sparkSession, rootOption.get).tableExists\n    } else {\n      DeltaTableUtils.isDeltaTable(sparkSession, identifierPath)\n    }\n  }\n\n  /**\n   * Check if the provided `identifier` string, in this case a file path,\n   * is the root of a Delta table.\n   *\n   * Note: This uses the active SparkSession in the current thread to search for the table. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * An example would be\n   * {{{\n   *   DeltaTable.isDeltaTable(spark, \"/path/to/table\")\n   * }}}\n   *\n   * @since 0.4.0\n   */\n  def isDeltaTable(identifier: String): Boolean = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    isDeltaTable(sparkSession, identifier)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * error if the table exists (the same as SQL `CREATE TABLE`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 1.0.0\n   */\n  @Evolving\n  def create(): DeltaTableBuilder = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    create(sparkSession)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * error if the table exists (the same as SQL `CREATE TABLE`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @since 1.0.0\n   */\n  @Evolving\n  def create(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, CreateTableOptions(ifNotExists = false))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * if it does not exists (the same as SQL `CREATE TABLE IF NOT EXISTS`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 1.0.0\n   */\n  @Evolving\n  def createIfNotExists(): DeltaTableBuilder = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    createIfNotExists(sparkSession)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * if it does not exists (the same as SQL `CREATE TABLE IF NOT EXISTS`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @since 1.0.0\n   */\n  @Evolving\n  def createIfNotExists(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, CreateTableOptions(ifNotExists = true))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table,\n   * error if the table doesn't exist (the same as SQL `REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 1.0.0\n   */\n  @Evolving\n  def replace(): DeltaTableBuilder = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    replace(sparkSession)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table,\n   * error if the table doesn't exist (the same as SQL `REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @since 1.0.0\n   */\n  @Evolving\n  def replace(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, ReplaceTableOptions(orCreate = false))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table\n   * or create table if not exists (the same as SQL `CREATE OR REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 1.0.0\n   */\n  @Evolving\n  def createOrReplace(): DeltaTableBuilder = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    createOrReplace(sparkSession)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table,\n   * or create table if not exists (the same as SQL `CREATE OR REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user.\n   * @since 1.0.0\n   */\n  @Evolving\n  def createOrReplace(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, ReplaceTableOptions(orCreate = true))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaColumnBuilder]] to specify a column.\n   * Refer to [[DeltaTableBuilder]] for examples and [[DeltaColumnBuilder]] detailed APIs.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @param colName string the column name\n   * @since 1.0.0\n   */\n  @Evolving\n  def columnBuilder(colName: String): DeltaColumnBuilder = {\n    val sparkSession = SparkSession.getActiveSession.getOrElse {\n      throw DeltaErrors.activeSparkSessionNotFound()\n    }\n    columnBuilder(sparkSession, colName)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaColumnBuilder]] to specify a column.\n   * Refer to [[DeltaTableBuilder]] for examples and [[DeltaColumnBuilder]] detailed APIs.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @param colName string the column name\n   * @since 1.0.0\n   */\n  @Evolving\n  def columnBuilder(spark: SparkSession, colName: String): DeltaColumnBuilder = {\n    new DeltaColumnBuilder(spark, colName)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/DeltaTableBuilder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaTableUtils}\nimport org.apache.spark.sql.delta.DeltaTableUtils.withActiveSession\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport io.delta.tables.execution._\n\nimport org.apache.spark.annotation._\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.plans.logical.{ColumnDefinition, CreateTable, ReplaceTable}\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.connector.expressions.Transform\nimport org.apache.spark.sql.execution.SQLExecution\nimport org.apache.spark.sql.types.{DataType, StructField, StructType}\n\n/**\n * :: Evolving ::\n *\n * Builder to specify how to create / replace a Delta table.\n * You must specify the table name or the path before executing the builder.\n * You can specify the table columns, the partitioning columns, the location of the data,\n * the table comment and the property, and how you want to create / replace the Delta table.\n *\n * After executing the builder, an instance of [[DeltaTable]] is returned.\n *\n * Scala example to create a Delta table with generated columns, using the table name:\n * {{{\n *   val table: DeltaTable = DeltaTable.create()\n *     .tableName(\"testTable\")\n *     .addColumn(\"c1\",  dataType = \"INT\", nullable = false)\n *     .addColumn(\n *       DeltaTable.columnBuilder(\"c2\")\n *         .dataType(\"INT\")\n *         .generatedAlwaysAs(\"c1 + 10\")\n *         .build()\n *     )\n *     .addColumn(\n *       DeltaTable.columnBuilder(\"c3\")\n *         .dataType(\"INT\")\n *         .comment(\"comment\")\n *         .nullable(true)\n *         .build()\n *     )\n *     .partitionedBy(\"c1\", \"c2\")\n *     .execute()\n * }}}\n *\n * Scala example to create a delta table using the location:\n * {{{\n *   val table: DeltaTable = DeltaTable.createIfNotExists(spark)\n *     .location(\"/foo/`bar`\")\n *     .addColumn(\"c1\", dataType = \"INT\", nullable = false)\n *     .addColumn(\n *       DeltaTable.columnBuilder(spark, \"c2\")\n *         .dataType(\"INT\")\n *         .generatedAlwaysAs(\"c1 + 10\")\n *         .build()\n *     )\n *     .addColumn(\n *       DeltaTable.columnBuilder(spark, \"c3\")\n *         .dataType(\"INT\")\n *         .comment(\"comment\")\n *         .nullable(true)\n *         .build()\n *     )\n *     .partitionedBy(\"c1\", \"c2\")\n *     .execute()\n * }}}\n *\n * Java Example to replace a table:\n * {{{\n *   DeltaTable table = DeltaTable.replace()\n *     .tableName(\"db.table\")\n *     .addColumn(\"c1\",  \"INT\", false)\n *     .addColumn(\n *       DeltaTable.columnBuilder(\"c2\")\n *         .dataType(\"INT\")\n *         .generatedAlwaysBy(\"c1 + 10\")\n *         .build()\n *     )\n *     .execute();\n * }}}\n *\n * @since 1.0.0\n */\n@Evolving\nclass DeltaTableBuilder private[tables](\n    spark: SparkSession,\n    builderOption: DeltaTableBuilderOptions) {\n  private var identifier: String = null\n  private var partitioningColumns: Option[Seq[String]] = None\n  private var clusteringColumns: Option[Seq[String]] = None\n  private var columns: mutable.Seq[StructField] = mutable.Seq.empty\n  private var location: Option[String] = None\n  private var tblComment: Option[String] = None\n  private var properties =\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.TABLE_BUILDER_FORCE_TABLEPROPERTY_LOWERCASE)) {\n      CaseInsensitiveMap(Map.empty[String, String])\n    } else {\n      Map.empty[String, String]\n    }\n\n\n  private val FORMAT_NAME: String = \"delta\"\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the table name, optionally qualified with a database name [database_name.] table_name\n   *\n   * @param identifier string the table name\n   * @since 1.0.0\n   */\n  @Evolving\n  def tableName(identifier: String): DeltaTableBuilder = {\n    this.identifier = identifier\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the table comment to describe the table.\n   *\n   * @param comment string table comment\n   * @since 1.0.0\n   */\n  @Evolving\n  def comment(comment: String): DeltaTableBuilder = {\n    tblComment = Option(comment)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the path to the directory where table data is stored,\n   * which could be a path on distributed storage.\n   *\n   * @param location string the data location\n   * @since 1.0.0\n   */\n  @Evolving\n  def location(location: String): DeltaTableBuilder = {\n    this.location = Option(location)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType string the DDL data type\n   * @since 1.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: String): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType dataType the DDL data type\n   * @since 1.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: DataType): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType string the DDL data type\n   * @param nullable boolean whether the column is nullable\n   * @since 1.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: String, nullable: Boolean): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).nullable(nullable).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType dataType the DDL data type\n   * @param nullable boolean whether the column is nullable\n   * @since 1.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: DataType, nullable: Boolean): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).nullable(nullable).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param col structField the column struct\n   * @since 1.0.0\n   */\n  @Evolving\n  def addColumn(col: StructField): DeltaTableBuilder = {\n    columns = columns :+ col\n    this\n  }\n\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify columns with an existing schema.\n   *\n   * @param cols structType the existing schema for columns\n   * @since 1.0.0\n   */\n  @Evolving\n  def addColumns(cols: StructType): DeltaTableBuilder = {\n    columns = columns ++ cols.toSeq\n    this\n  }\n\n  /**\n   * Validate that clusterBy is not used with partitionedBy.\n   */\n  private def validatePartitioning(): Unit = {\n    if (partitioningColumns.nonEmpty && clusteringColumns.nonEmpty) {\n      throw DeltaErrors.clusterByWithPartitionedBy()\n    }\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the columns to partition the output on the file system.\n   *\n   * Note: This should only include table columns already defined in schema.\n   *\n   * @param colNames string* column names for partitioning\n   * @since 1.0.0\n   */\n  @Evolving\n  @scala.annotation.varargs\n  def partitionedBy(colNames: String*): DeltaTableBuilder = {\n    partitioningColumns = Option(colNames)\n    validatePartitioning()\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the columns to cluster the output on the file system.\n   *\n   * Note: This should only include table columns already defined in schema.\n   *\n   * @param colNames string* column names for clustering\n   * @since 3.2.0\n   */\n  @Evolving\n  @scala.annotation.varargs\n  def clusterBy(colNames: String*): DeltaTableBuilder = {\n    clusteringColumns = Option(colNames)\n    validatePartitioning()\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a key-value pair to tag the table definition.\n   *\n   * @param key string the table property key\n   * @param value string the table property value\n   * @since 1.0.0\n   */\n  @Evolving\n  def property(key: String, value: String): DeltaTableBuilder = {\n    this.properties = this.properties + (key -> value)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Execute the command to create / replace a Delta table and returns a instance of [[DeltaTable]].\n   *\n   * @since 1.0.0\n   */\n  @Evolving\n  def execute(): DeltaTable = withActiveSession(spark) {\n    if (identifier == null && location.isEmpty) {\n      throw DeltaErrors.createTableMissingTableNameOrLocation()\n    }\n\n    if (this.identifier == null) {\n      identifier = s\"delta.`${location.get}`\"\n    }\n\n    // Return DeltaTable Object.\n    val tableId: TableIdentifier = spark.sessionState.sqlParser.parseTableIdentifier(identifier)\n\n    if (DeltaTableUtils.isValidPath(tableId) && location.nonEmpty\n        && tableId.table != location.get) {\n      throw DeltaErrors.createTableIdentifierLocationMismatch(identifier, location.get)\n    }\n\n    val table = spark.sessionState.sqlParser.parseMultipartIdentifier(identifier)\n\n    val partitioning = partitioningColumns.map { colNames =>\n      colNames.map(name => DeltaTableUtils.parseColToTransform(name))\n    }.getOrElse(Seq.empty[Transform]) ++ (clusteringColumns.map { colNames =>\n      DeltaTableUtils.parseColsToClusterByTransform(colNames)\n    })\n\n    val tableSpec = org.apache.spark.sql.catalyst.plans.logical.TableSpec(\n      properties = properties,\n      provider = Some(FORMAT_NAME),\n      options = Map.empty,\n      location = location,\n      comment = tblComment,\n      collation = None,\n      serde = None,\n      external = false)\n\n    val stmt = builderOption match {\n      case CreateTableOptions(ifNotExists) =>\n        val unresolvedTable = org.apache.spark.sql.catalyst.analysis.UnresolvedIdentifier(table)\n        CreateTable(\n          unresolvedTable,\n          columns.map(ColumnDefinition.fromV1Column(_, spark.sessionState.sqlParser)).toSeq,\n          partitioning,\n          tableSpec,\n          ifNotExists)\n      case ReplaceTableOptions(orCreate) =>\n        val unresolvedTable = org.apache.spark.sql.catalyst.analysis.UnresolvedIdentifier(table)\n        ReplaceTable(\n          unresolvedTable,\n          columns.map(ColumnDefinition.fromV1Column(_, spark.sessionState.sqlParser)).toSeq,\n          partitioning,\n          tableSpec,\n          orCreate)\n    }\n    val qe = spark.sessionState.executePlan(stmt)\n    // call `QueryExecution.toRDD` to trigger the execution of commands.\n    SQLExecution.withNewExecutionId(qe, Some(\"create delta table\"))(qe.toRdd)\n\n    // Return DeltaTable Object.\n    if (DeltaTableUtils.isValidPath(tableId)) {\n      DeltaTable.forPath(spark, location.get)\n    } else {\n      DeltaTable.forName(spark, this.identifier)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/execution/DeltaConvert.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables.execution\n\nimport org.apache.spark.sql.delta.DeltaTableUtils.withActiveSession\nimport org.apache.spark.sql.delta.commands.ConvertToDeltaCommand\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types.StructType\n\ntrait DeltaConvertBase {\n  def executeConvert(\n      spark: SparkSession,\n      tableIdentifier: TableIdentifier,\n      partitionSchema: Option[StructType],\n      deltaPath: Option[String]): DeltaTable = withActiveSession(spark) {\n    val cvt = ConvertToDeltaCommand(tableIdentifier, partitionSchema, collectStats = true,\n      deltaPath)\n    cvt.run(spark)\n    if (cvt.isCatalogTable(spark.sessionState.analyzer, tableIdentifier)) {\n      DeltaTable.forName(spark, tableIdentifier.toString)\n    } else {\n      DeltaTable.forPath(spark, tableIdentifier.table)\n    }\n  }\n}\n\nobject DeltaConvert extends DeltaConvertBase {}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/execution/DeltaTableBuilderOptions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables.execution\n\n/**\n * DeltaTableBuilder option to indicate whether it's to create / replace the table.\n */\nsealed trait DeltaTableBuilderOptions\n\n/**\n * Specify that the builder is to create a Delta table.\n *\n * @param ifNotExists boolean whether to ignore if the table already exists.\n */\ncase class CreateTableOptions(ifNotExists: Boolean) extends DeltaTableBuilderOptions\n\n/**\n * Specify that the builder is to replace a Delta table.\n *\n * @param orCreate boolean whether to create the table if the table doesn't exist.\n */\ncase class ReplaceTableOptions(orCreate: Boolean) extends DeltaTableBuilderOptions\n\n\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/execution/DeltaTableOperations.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables.execution\n\nimport scala.collection.Map\n\nimport org.apache.spark.sql.catalyst.TimeTravel\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaTableUtils.withActiveSession\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.{DeltaGenerateCommand, DescribeDeltaDetailCommand, VacuumCommand}\nimport org.apache.spark.sql.delta.util.AnalysisHelper\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{functions, Column, DataFrame}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{UnresolvedAttribute, UnresolvedRelation}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\n\n/**\n * Interface to provide the actual implementations of DeltaTable operations.\n */\ntrait DeltaTableOperations extends AnalysisHelper { self: io.delta.tables.DeltaTable =>\n\n  protected def executeDelete(condition: Option[Expression]): Unit = improveUnsupportedOpError {\n    withActiveSession(sparkSession) {\n      val delete = DeleteFromTable(\n        self.toDF.queryExecution.analyzed,\n        condition.getOrElse(Literal.TrueLiteral))\n      toDataset(sparkSession, delete)\n    }\n  }\n\n  protected def executeHistory(\n      deltaLog: DeltaLog,\n      limit: Option[Int] = None,\n      catalogTable: Option[CatalogTable] = None): DataFrame = withActiveSession(sparkSession) {\n    val history = deltaLog.history\n    sparkSession.createDataFrame(history.getHistory(limit, catalogTable))\n  }\n\n  protected def executeDetails(\n      path: String,\n      tableIdentifier: Option[TableIdentifier]): DataFrame = withActiveSession(sparkSession) {\n    val details = DescribeDeltaDetailCommand(Option(path), tableIdentifier, self.deltaLog.options)\n    toDataset(sparkSession, details)\n  }\n\n  protected def executeGenerate(\n      path: String,\n      tableIdentifier: Option[TableIdentifier],\n      mode: String): Unit = withActiveSession(sparkSession) {\n    val generate = DeltaGenerateCommand(Option(path), tableIdentifier, mode, self.deltaLog.options)\n    toDataset(sparkSession, generate)\n  }\n\n  protected def executeUpdate(\n      set: Map[String, Column],\n      condition: Option[Column]): Unit = improveUnsupportedOpError {\n    withActiveSession(sparkSession) {\n      val assignments = set.map { case (targetColName, column) =>\n        Assignment(UnresolvedAttribute.quotedString(targetColName), column.expr)\n      }.toSeq\n      val update =\n        UpdateTable(self.toDF.queryExecution.analyzed, assignments, condition.map(_.expr))\n      toDataset(sparkSession, update)\n    }\n  }\n\n  protected def executeVacuum(\n      table: DeltaTableV2,\n      retentionHours: Option[Double]): DataFrame = withActiveSession(sparkSession) {\n    val tableId = table.getTableIdentifierIfExists\n    val path = Option.when(tableId.isEmpty)(deltaLog.dataPath.toString)\n    val vacuum = VacuumTableCommand(\n      path,\n      tableId,\n      inventoryTable = None,\n      inventoryQuery = None,\n      retentionHours,\n      dryRun = false,\n      vacuumType = None,\n      deltaLog.options)\n    toDataset(sparkSession, vacuum)\n    sparkSession.emptyDataFrame\n  }\n\n  protected def executeRestore(\n      table: DeltaTableV2,\n      versionAsOf: Option[Long],\n      timestampAsOf: Option[String]): DataFrame = withActiveSession(sparkSession) {\n    val identifier = table.getTableIdentifierIfExists.map(\n      id => Identifier.of(id.database.toArray, id.table))\n    val sourceRelation = DataSourceV2Relation.create(table, None, identifier)\n\n    val restore = RestoreTableStatement(\n      TimeTravel(\n        sourceRelation,\n        timestampAsOf.map(Literal(_)),\n        versionAsOf,\n        Some(\"deltaTable\"))\n      )\n    toDataset(sparkSession, restore)\n  }\n\n  protected def executeClone(\n      table: DeltaTableV2,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String],\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None\n  ): io.delta.tables.DeltaTable = withActiveSession(sparkSession) {\n    if (!isShallow) {\n      throw DeltaErrors.unsupportedDeepCloneException()\n    }\n\n    val sourceIdentifier = table.getTableIdentifierIfExists.map(id =>\n      Identifier.of(id.database.toArray, id.table))\n    val sourceRelation = DataSourceV2Relation.create(table, None, sourceIdentifier)\n\n    val maybeTimeTravelSource = if (versionAsOf.isDefined || timestampAsOf.isDefined) {\n      TimeTravel(\n        sourceRelation,\n        timestampAsOf.map(Literal(_)),\n        versionAsOf,\n        Some(\"deltaTable\")\n      )\n    } else {\n      sourceRelation\n    }\n\n    val targetIsAbsolutePath = new Path(target).isAbsolute()\n    val targetIdentifier = if (targetIsAbsolutePath) s\"delta.`$target`\" else target\n    val targetRelation = UnresolvedRelation(\n      sparkSession.sessionState.sqlParser.parseTableIdentifier(targetIdentifier))\n\n    val clone = CloneTableStatement(\n      maybeTimeTravelSource,\n      targetRelation,\n      ifNotExists = false,\n      replace,\n      isCreateCommand = true,\n      tablePropertyOverrides = properties.toMap,\n      targetLocation = None)\n\n    toDataset(sparkSession, clone)\n\n    if (targetIsAbsolutePath) {\n      io.delta.tables.DeltaTable.forPath(sparkSession, target)\n    } else {\n      io.delta.tables.DeltaTable.forName(sparkSession, target)\n    }\n  }\n\n  protected def toStrColumnMap(map: Map[String, String]): Map[String, Column] = {\n    map.toSeq.map { case (k, v) => k -> functions.expr(v) }.toMap\n  }\n\n  protected def sparkSession = self.toDF.sparkSession\n}\n"
  },
  {
    "path": "spark/src/main/scala/io/delta/tables/execution/VacuumTableCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables.execution\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference}\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedTable\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, DeltaTableIdentifier, DeltaTableUtils, UnresolvedDeltaPathOrIdentifier}\nimport org.apache.spark.sql.delta.commands.DeltaCommand\nimport org.apache.spark.sql.delta.commands.VacuumCommand\nimport org.apache.spark.sql.delta.commands.VacuumCommand.getDeltaTable\nimport org.apache.spark.sql.execution.command.{LeafRunnableCommand, RunnableCommand}\nimport org.apache.spark.sql.types.StringType\n\n/**\n * The `vacuum` command implementation for Spark SQL. Example SQL:\n * {{{\n *    VACUUM ('/path/to/dir' | delta.`/path/to/dir`)\n *    [USING INVENTORY (delta.`/path/to/dir`| ( sub_query ))]\n *    [RETAIN number HOURS] [DRY RUN];\n * }}}\n */\ncase class VacuumTableCommand(\n    override val child: LogicalPlan,\n    horizonHours: Option[Double],\n    inventoryTable: Option[LogicalPlan],\n    inventoryQuery: Option[String],\n    dryRun: Boolean,\n    vacuumType: Option[String]) extends RunnableCommand with UnaryNode with DeltaCommand {\n\n  override val output: Seq[Attribute] =\n    Seq(AttributeReference(\"path\", StringType, nullable = true)())\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan =\n    copy(child = newChild)\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaTable = getDeltaTable(child, \"VACUUM\")\n    // The VACUUM command is only supported on existing delta tables. If the target table doesn't\n    // exist or it is based on a partition directory, an exception will be thrown.\n    if (!deltaTable.tableExists || deltaTable.hasPartitionFilters) {\n      throw DeltaErrors.notADeltaTableException(\n        \"VACUUM\",\n        DeltaTableIdentifier(path = Some(deltaTable.path.toString)))\n    }\n    val inventory = inventoryTable.map(sparkSession.sessionState.analyzer.execute)\n        .map(p => Some(getDeltaTable(p, \"VACUUM\").toDf(sparkSession)))\n        .getOrElse(inventoryQuery.map(sparkSession.sql))\n    VacuumCommand.gc(sparkSession, deltaTable, dryRun, horizonHours,\n      inventory, vacuumType).collect()\n  }\n}\n\nobject VacuumTableCommand {\n  def apply(\n      path: Option[String],\n      table: Option[TableIdentifier],\n      inventoryTable: Option[TableIdentifier],\n      inventoryQuery: Option[String],\n      horizonHours: Option[Double],\n      dryRun: Boolean,\n      vacuumType: Option[String],\n      options: Map[String, String]): VacuumTableCommand = {\n    val child = UnresolvedDeltaPathOrIdentifier(path, table, options, \"VACUUM\")\n    val unresolvedInventoryTable = inventoryTable.map(rt => UnresolvedTable(rt.nameParts, \"VACUUM\"))\n    VacuumTableCommand(child, horizonHours, unresolvedInventoryTable, inventoryQuery, dryRun,\n      vacuumType)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/TimeTravel.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst\n\n// scalastyle:off import.ordering.noEmptyLine\n\nimport com.databricks.spark.util.DatabricksLogging\n\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical.{LeafNode, LogicalPlan}\n\n/**\n * A logical node used to time travel the child relation to the given `timestamp` or `version`.\n * The `child` must support time travel, e.g. Delta, and cannot be a view, subquery or stream.\n * The timestamp expression cannot be a subquery. It must be a timestamp expression.\n * @param creationSource The API used to perform time travel, e.g. `atSyntax`, `dfReader` or SQL\n */\ncase class TimeTravel(\n    relation: LogicalPlan,\n    timestamp: Option[Expression],\n    version: Option[Long],\n    creationSource: Option[String]) extends LeafNode with DatabricksLogging {\n\n  assert(version.isEmpty ^ timestamp.isEmpty,\n    \"Either the version or timestamp should be provided for time travel\")\n\n  override def output: Seq[Attribute] = Nil\n\n  override lazy val resolved: Boolean = false\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregation/BitmapAggregator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.expressions.aggregation\n\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{Expression, GenericInternalRow, ImplicitCastInputTypes}\nimport org.apache.spark.sql.catalyst.expressions.aggregate.{ImperativeAggregate, TypedImperativeAggregate}\nimport org.apache.spark.sql.catalyst.trees.UnaryLike\nimport org.apache.spark.sql.types._\n\n/**\n * This function returns a bitmap representing the set of values of the underlying column.\n *\n * The bitmap is simply a compressed representation of the set of all integral values that\n * appear in the column being aggregated over.\n *\n * @param child child expression that can produce a column value with `child.eval(inputRow)`\n */\ncase class BitmapAggregator(\n    child: Expression,\n    override val mutableAggBufferOffset: Int,\n    override val inputAggBufferOffset: Int,\n    // Take the format as string instead of [[RoaringBitmapArrayFormat.Value]],\n    // because String is safe to serialize.\n    serializationFormatString: String)\n  extends TypedImperativeAggregate[RoaringBitmapArray] with ImplicitCastInputTypes\n  with UnaryLike[Expression] {\n\n  def this(child: Expression, serializationFormat: RoaringBitmapArrayFormat.Value) =\n    this(child, 0, 0, serializationFormat.toString)\n\n  override def createAggregationBuffer(): RoaringBitmapArray = new RoaringBitmapArray()\n\n  override def update(buffer: RoaringBitmapArray, input: InternalRow): RoaringBitmapArray = {\n    val value = child.eval(input)\n    // Ignore empty rows\n    if (value != null) {\n      buffer.add(value.asInstanceOf[Long])\n    }\n    buffer\n  }\n\n  override def merge(buffer: RoaringBitmapArray, input: RoaringBitmapArray): RoaringBitmapArray = {\n    buffer.merge(input)\n    buffer\n  }\n\n  /**\n   * Return bitmap cardinality, last and serialized bitmap.\n   */\n  override def eval(bitmapIntegerSet: RoaringBitmapArray): GenericInternalRow = {\n    // reduce the serialized size via RLE optimisation\n    bitmapIntegerSet.runOptimize()\n    new GenericInternalRow(Array(\n      bitmapIntegerSet.cardinality,\n      bitmapIntegerSet.last.getOrElse(null),\n      serialize(bitmapIntegerSet)))\n  }\n\n  override def serialize(buffer: RoaringBitmapArray): Array[Byte] = {\n    val serializationFormat = RoaringBitmapArrayFormat.withName(serializationFormatString)\n    buffer.serializeAsByteArray(serializationFormat)\n  }\n\n  override def deserialize(storageFormat: Array[Byte]): RoaringBitmapArray = {\n    RoaringBitmapArray.readFrom(storageFormat)\n  }\n\n  override def withNewMutableAggBufferOffset(newMutableAggBufferOffset: Int)\n    : ImperativeAggregate = copy(mutableAggBufferOffset = newMutableAggBufferOffset)\n\n  override def withNewInputAggBufferOffset(newInputAggBufferOffset: Int)\n    : ImperativeAggregate = copy(inputAggBufferOffset = newInputAggBufferOffset)\n\n  override def nullable: Boolean = false\n\n  override def dataType: StructType = StructType(\n    Seq(\n      StructField(\"cardinality\", LongType),\n      StructField(\"last\", LongType),\n      StructField(\"bitmap\", BinaryType)\n    )\n  )\n\n  override def inputTypes: Seq[AbstractDataType] = Seq(LongType)\n\n  override protected def withNewChildInternal(newChild: Expression): BitmapAggregator =\n    copy(child = newChild)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CloneTableStatement.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.expressions.Attribute\n\n/**\n * CLONE TABLE statement, as parsed from SQL\n *\n * @param source source plan for table to be cloned\n * @param target target path or table name where clone should be instantiated\n * @param ifNotExists if a table exists at the target, we should not go through with the clone\n * @param isReplaceCommand when true, replace the target table if one exists\n * @param isCreateCommand when true, create the target table if none exists\n * @param tablePropertyOverrides user-defined table properties that should override any properties\n *                        with the same key from the source table\n * @param targetLocation if target is a table name then user can provide a targetLocation to\n *                       create an external table with this location\n */\ncase class CloneTableStatement(\n    source: LogicalPlan,\n    target: LogicalPlan,\n    ifNotExists: Boolean,\n    isReplaceCommand: Boolean,\n    isCreateCommand: Boolean,\n    tablePropertyOverrides: Map[String, String],\n    targetLocation: Option[String]) extends BinaryNode {\n  override def output: Seq[Attribute] = Nil\n\n  override def left: LogicalPlan = source\n  override def right: LogicalPlan = target\n  override protected def withNewChildrenInternal(\n      newLeft: LogicalPlan, newRight: LogicalPlan): CloneTableStatement =\n    copy(source = newLeft, target = newRight)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/DeltaDelete.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}\n\n// This only used by Delta which needs to be compatible with DBR 6 and can't use the new class\n// added in Spark 3.0: `DeleteFromTable`.\ncase class DeltaDelete(\n    child: LogicalPlan,\n    condition: Option[Expression])\n  extends UnaryNode {\n  override def output: Seq[Attribute] = Seq.empty\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): DeltaDelete =\n    copy(child = newChild)\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/DeltaUpdateTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.delta.DeltaAnalysisException\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, Expression, ExtractValue, GetStructField}\n\n/**\n * Perform UPDATE on a table\n *\n * @param child the logical plan representing target table\n * @param updateColumns: the to-be-updated target columns\n * @param updateExpressions: the corresponding update expression if the condition is matched\n * @param condition: Only rows that match the condition will be updated\n */\ncase class DeltaUpdateTable(\n    child: LogicalPlan,\n    updateColumns: Seq[Expression],\n    updateExpressions: Seq[Expression],\n    condition: Option[Expression])\n  extends UnaryNode {\n\n  assert(updateColumns.size == updateExpressions.size)\n\n  override def output: Seq[Attribute] = Seq.empty\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): DeltaUpdateTable =\n    copy(child = newChild)\n}\n\nobject DeltaUpdateTable {\n\n  /**\n   * Extracts name parts from a resolved expression referring to a nested or non-nested column\n   * - For non-nested column, the resolved expression will be like `AttributeReference(...)`.\n   * - For nested column, the resolved expression will be like `Alias(GetStructField(...))`.\n   *\n   * In the nested case, the function recursively traverses through the expression to find\n   * the name parts. For example, a nested field of a.b.c would be resolved to an expression\n   *\n   *    `Alias(c, GetStructField(c, GetStructField(b, AttributeReference(a)))`\n   *\n   * for which this method recursively extracts the name parts as follows:\n   *\n   *    `Alias(c, GetStructField(c, GetStructField(b, AttributeReference(a)))`\n   *    ->  `GetStructField(c, GetStructField(b, AttributeReference(a)))`\n   *      ->  `GetStructField(b, AttributeReference(a))` ++ Seq(c)\n   *        ->  `AttributeReference(a)` ++ Seq(b, c)\n   *          ->  [a, b, c]\n   */\n  def getTargetColNameParts(\n      resolvedTargetCol: Expression, errMsg: => String = null): Seq[String] = {\n    def extractRecursively(expr: Expression): Seq[String] = expr match {\n      case attr: Attribute => Seq(attr.name)\n\n      case Alias(c, _) => extractRecursively(c)\n\n      case GetStructField(c, _, Some(name)) => extractRecursively(c) :+ name\n\n      case _: ExtractValue =>\n        throw new DeltaAnalysisException(\n          errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0009\",\n          messageParameters = Array(Option(errMsg).map(_ + \" - \").getOrElse(\"\"))\n        )\n\n      case other =>\n        throw new DeltaAnalysisException(\n          errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0010\",\n          messageParameters = Array(Option(errMsg).map(_ + \" - \").getOrElse(\"\"), other.sql)\n        )\n    }\n\n    extractRecursively(resolvedTargetCol)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/RestoreTableStatement.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.TimeTravel\n\nimport org.apache.spark.sql.catalyst.expressions.Attribute\n\n/**\n * RESTORE TABLE statement as parsed from SQL\n *\n * @param table - logical node of the table that will be restored, internally contains either\n *              version or timestamp.\n */\ncase class RestoreTableStatement(table: TimeTravel) extends UnaryNode {\n\n  override def child: LogicalPlan = table\n\n  override def output: Seq[Attribute] = Nil\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): RestoreTableStatement =\n    copy(table = newChild.asInstanceOf[TimeTravel])\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/SyncIdentity.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.analysis.FieldName\nimport org.apache.spark.sql.connector.catalog.TableChange\nimport org.apache.spark.sql.connector.catalog.TableChange.ColumnChange\n\n/**\n * A `ColumnChange` to model `ALTER TABLE ... ALTER (CHANGE) COLUMN ... SYNC IDENTITY` command.\n *\n * @param fieldNames The (potentially nested) column name.\n */\ncase class SyncIdentity(fieldNames: Array[String]) extends ColumnChange {\n  require(fieldNames.size == 1, \"IDENTITY column cannot be a nested column.\")\n}\n\ncase class AlterColumnSyncIdentity(table: LogicalPlan, column: FieldName)\n    extends AlterTableCommand {\n  override def changes: Seq[TableChange] = {\n    require(column.resolved, \"FieldName should be resolved before it's converted to TableChange.\")\n    val colName = column.name.toArray\n    Seq(SyncIdentity(colName))\n  }\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan =\n    copy(table = newChild)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/deltaConstraints.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.delta.constraints.{AddConstraint => AddDeltaConstraint, DropConstraint => DropDeltaConstraint} // Aliased to avoid conflicts with Spark's AddConstraint/DropConstraint\n\nimport org.apache.spark.sql.connector.catalog.TableChange\n\n/**\n * The logical plan of the ALTER TABLE ... ADD CONSTRAINT command.\n */\ncase class AlterTableAddConstraint(\n    table: LogicalPlan, constraintName: String, expr: String) extends AlterTableCommand {\n  override def changes: Seq[TableChange] = Seq(AddDeltaConstraint(constraintName, expr))\n\n  protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = copy(table = newChild)\n}\n\n/**\n * The logical plan of the ALTER TABLE ... DROP CONSTRAINT command.\n */\ncase class AlterTableDropConstraint(\n    table: LogicalPlan, constraintName: String, ifExists: Boolean) extends AlterTableCommand {\n  override def changes: Seq[TableChange] = Seq(DropDeltaConstraint(constraintName, ifExists))\n\n  protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = copy(table = newChild)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/deltaMerge.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, DeltaIllegalArgumentException, DeltaUnsupportedOperationException}\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, UnaryExpression}\nimport org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode}\nimport org.apache.spark.sql.types.{DataType, StructType}\n\n/**\n * A copy of Spark SQL Unevaluable for cross-version compatibility. In 3.0, implementers of\n * the original Unevaluable must explicitly override foldable to false; in 3.1 onwards, this\n * explicit override is invalid.\n */\ntrait DeltaUnevaluable extends Expression {\n  final override def foldable: Boolean = false\n\n  final override def eval(input: InternalRow = null): Any = {\n    throw new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_CANNOT_EVALUATE_EXPRESSION\",\n      messageParameters = Array(s\"$this\")\n    )\n  }\n\n  final override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode =\n    throw new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_CANNOT_GENERATE_CODE_FOR_EXPRESSION\",\n      messageParameters = Array(s\"$this\")\n    )\n}\n\n/**\n * Determines the nullness of target-only struct fields when generating target struct expressions\n * to align with the evolved target schema during MERGE operations with schema evolution enabled.\n *\n * Target-only struct fields are nested fields that exist in the target table's struct column\n * but not in the corresponding source struct column. For example, if the target has\n * `struct(a, b, c)` and the source has `struct(a, b)`, then field `c` is target-only.\n *\n * This behavior only applies when schema evolution is enabled, as target-only fields are not\n * allowed when schema evolution is disabled.\n */\nobject TargetOnlyStructFieldBehavior extends Enumeration {\n  type TargetOnlyStructFieldBehavior = Value\n\n  /**\n   * Preserve target-only struct fields with their original values from the target table.\n   * Used for: `UPDATE * [EXCEPT]` clauses with schema evolution enabled.\n   * Example: Target row has `struct(a=1, b=2, c=3)`, source has `struct(a=10, b=20)`.\n   *          Result: `struct(a=10, b=20, c=3)` - field `c` is preserved.\n   */\n  val PRESERVE = Value\n\n  /**\n   * Overwrite target-only struct fields with null.\n   * Used for: Explicit column assignments (e.g., `UPDATE SET col = expr`),\n   *           `INSERT` clauses (no existing row to preserve values from)\n   * Example: Target row has `struct(a=1, b=2, c=3)`, source has `struct(a=10, b=20)`.\n   *          Result: `struct(a=10, b=20, c=null)` - field `c` is set to null.\n   */\n  val NULLIFY = Value\n\n  /**\n   * The expression has been fully resolved and aligned to the evolved target schema.\n   */\n  val TARGET_ALIGNED = Value\n}\n\n/**\n * Represents an action in MERGE's UPDATE or INSERT clause where a target columns is assigned the\n * value of an expression\n *\n * @param targetColNameParts The name parts of the target column. This is a sequence to support\n *                           nested fields as targets.\n * @param expr Expression to generate the value of the target column.\n * @param targetOnlyStructFieldBehavior Determines the nullness of target-only struct fields.\n *                                      Note: This parameter only takes effect when schema evolution\n *                                      is enabled; otherwise it is ignored.\n * @param targetColNameResolved Whether the targetColNameParts have undergone resolution and checks\n *                              for validity.\n */\ncase class DeltaMergeAction(\n    targetColNameParts: Seq[String],\n    expr: Expression,\n    targetOnlyStructFieldBehavior: TargetOnlyStructFieldBehavior.Value,\n    targetColNameResolved: Boolean = false)\n  extends UnaryExpression with DeltaUnevaluable {\n  override def child: Expression = expr\n  override def dataType: DataType = expr.dataType\n  override lazy val resolved: Boolean = {\n    childrenResolved && checkInputDataTypes().isSuccess && targetColNameResolved\n  }\n  override def sql: String = s\"$targetColString = ${expr.sql}\"\n  override def toString: String = s\"$targetColString = $expr\"\n  private lazy val targetColString: String = targetColNameParts.mkString(\"`\", \"`.`\", \"`\")\n\n  override protected def withNewChildInternal(newChild: Expression): DeltaMergeAction =\n    copy(expr = newChild)\n}\n\n\n/**\n * Trait that represents a WHEN clause in MERGE. See [[DeltaMergeInto]]. It extends [[Expression]]\n * so that Catalyst can find all the expressions in the clause implementations.\n */\nsealed trait DeltaMergeIntoClause extends Expression with DeltaUnevaluable {\n  /** Optional condition of the clause */\n  def condition: Option[Expression]\n\n  /**\n   * Sequence of actions represented as expressions. Note that this can be only be either\n   * UnresolvedStar, or MergeAction.\n   */\n  def actions: Seq[Expression]\n\n  /**\n   * Sequence of resolved actions represented as Aliases. Actions, once resolved, must\n   * be Aliases and not any other NamedExpressions. So it should be safe to do this casting\n   * as long as this is called after the clause has been resolved.\n   */\n  def resolvedActions: Seq[DeltaMergeAction] = {\n    assert(actions.forall(_.resolved), \"all actions have not been resolved yet\")\n    actions.map(_.asInstanceOf[DeltaMergeAction])\n  }\n\n  /**\n   * String representation of the clause type: Update, Delete or Insert.\n   */\n  def clauseType: String\n\n  override def toString: String = {\n    val condStr = condition.map { c => s\"condition: $c\" }\n    val actionStr = if (actions.isEmpty) None else {\n      Some(\"actions: \" + actions.mkString(\"[\", \", \", \"]\"))\n    }\n    s\"$clauseType \" + Seq(condStr, actionStr).flatten.mkString(\"[\", \", \", \"]\")\n  }\n\n  override def nullable: Boolean = false\n  override def dataType: DataType = null\n  override def children: Seq[Expression] = condition.toSeq ++ actions\n\n  /** Verify whether the expressions in the actions are of the right type */\n  protected[logical] def verifyActions(): Unit = actions.foreach {\n    case _: UnresolvedStar =>\n    case _: DeltaMergeAction =>\n    case a => throw new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_UNEXPECTED_ACTION_EXPRESSION\",\n      messageParameters = Array(s\"$a\"))\n  }\n}\n\n\nobject DeltaMergeIntoClause {\n  /**\n   * Convert the parsed columns names and expressions into action for MergeInto. Note:\n   * - Size of column names and expressions must be the same.\n   * - If the sizes are zeros and `emptySeqIsStar` is true, this function assumes\n   *   that query had `*` as an action, and therefore generates a single action\n   *   with `UnresolvedStar`. This will be expanded later during analysis.\n   * - Otherwise, this will convert the names and expressions to MergeActions.\n   */\n  def toActions(\n      colNames: Seq[UnresolvedAttribute],\n      exprs: Seq[Expression],\n      isEmptySeqEqualToStar: Boolean = true): Seq[Expression] = {\n    assert(colNames.size == exprs.size)\n    if (colNames.isEmpty && isEmptySeqEqualToStar) {\n      Seq(UnresolvedStar(None))\n    } else {\n      (colNames, exprs).zipped.map { (col, expr) =>\n        DeltaMergeAction(\n          targetColNameParts = col.nameParts,\n          expr = expr,\n          // Explicit column assignments overwrite target-only struct fields with null.\n          targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.NULLIFY)\n      }\n    }\n  }\n\n  def toActions(assignments: Seq[Assignment]): Seq[Expression] = {\n    if (assignments.isEmpty) {\n      Seq[Expression](UnresolvedStar(None))\n    } else {\n      assignments.map {\n        case Assignment(key: UnresolvedAttribute, expr) =>\n          DeltaMergeAction(\n            targetColNameParts = key.nameParts,\n            expr = expr,\n            // Explicit column assignments overwrite target-only struct fields with null.\n            targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.NULLIFY)\n        case Assignment(key: Attribute, expr) =>\n          DeltaMergeAction(\n            targetColNameParts = Seq(key.name),\n            expr = expr,\n            // Explicit column assignments overwrite target-only struct fields with null.\n            targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.NULLIFY)\n        case other =>\n          throw new DeltaAnalysisException(\n            errorClass = \"DELTA_MERGE_UNEXPECTED_ASSIGNMENT_KEY\",\n            messageParameters = Array(s\"${other.getClass}\", s\"$other\"))\n      }\n    }\n  }\n}\n\n/** Trait that represents WHEN MATCHED clause in MERGE. See [[DeltaMergeInto]]. */\nsealed trait DeltaMergeIntoMatchedClause extends DeltaMergeIntoClause\n\n/** Represents the clause WHEN MATCHED THEN UPDATE in MERGE. See [[DeltaMergeInto]]. */\ncase class DeltaMergeIntoMatchedUpdateClause(\n    condition: Option[Expression],\n    actions: Seq[Expression])\n  extends DeltaMergeIntoMatchedClause {\n\n  def this(cond: Option[Expression], cols: Seq[UnresolvedAttribute], exprs: Seq[Expression]) =\n    this(cond, DeltaMergeIntoClause.toActions(cols, exprs))\n\n  override def clauseType: String = \"Update\"\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): DeltaMergeIntoMatchedUpdateClause = {\n    if (condition.isDefined) {\n      copy(condition = Some(newChildren.head), actions = newChildren.tail)\n    } else {\n      copy(condition = None, actions = newChildren)\n    }\n  }\n}\n\n/** Represents the clause WHEN MATCHED THEN DELETE in MERGE. See [[DeltaMergeInto]]. */\ncase class DeltaMergeIntoMatchedDeleteClause(condition: Option[Expression])\n    extends DeltaMergeIntoMatchedClause {\n  def this(condition: Option[Expression], actions: Seq[DeltaMergeAction]) = this(condition)\n\n  override def clauseType: String = \"Delete\"\n  override def actions: Seq[Expression] = Seq.empty\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): DeltaMergeIntoMatchedDeleteClause =\n    copy(condition = if (condition.isDefined) Some(newChildren.head) else None)\n}\n\n/** Trait that represents WHEN NOT MATCHED clause in MERGE. See [[DeltaMergeInto]]. */\nsealed trait DeltaMergeIntoNotMatchedClause extends DeltaMergeIntoClause\n\n/** Represents the clause WHEN NOT MATCHED THEN INSERT in MERGE. See [[DeltaMergeInto]]. */\ncase class DeltaMergeIntoNotMatchedInsertClause(\n    condition: Option[Expression],\n    actions: Seq[Expression])\n  extends DeltaMergeIntoNotMatchedClause {\n\n  def this(cond: Option[Expression], cols: Seq[UnresolvedAttribute], exprs: Seq[Expression]) =\n    this(cond, DeltaMergeIntoClause.toActions(cols, exprs))\n\n  override def clauseType: String = \"Insert\"\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): DeltaMergeIntoNotMatchedInsertClause =\n    if (condition.isDefined) {\n      copy(condition = Some(newChildren.head), actions = newChildren.tail)\n    } else {\n      copy(condition = None, actions = newChildren)\n    }\n}\n\n/** Trait that represents WHEN NOT MATCHED BY SOURCE clause in MERGE. See [[DeltaMergeInto]]. */\nsealed trait DeltaMergeIntoNotMatchedBySourceClause extends DeltaMergeIntoClause\n\n/** Represents the clause WHEN NOT MATCHED BY SOURCE THEN UPDATE in MERGE. See\n * [[DeltaMergeInto]]. */\ncase class DeltaMergeIntoNotMatchedBySourceUpdateClause(\n    condition: Option[Expression],\n    actions: Seq[Expression])\n  extends DeltaMergeIntoNotMatchedBySourceClause {\n\n  def this(cond: Option[Expression], cols: Seq[UnresolvedAttribute], exprs: Seq[Expression]) =\n    this(cond, DeltaMergeIntoClause.toActions(cols, exprs))\n\n  override def clauseType: String = \"Update\"\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): DeltaMergeIntoNotMatchedBySourceUpdateClause = {\n    if (condition.isDefined) {\n      copy(condition = Some(newChildren.head), actions = newChildren.tail)\n    } else {\n      copy(condition = None, actions = newChildren)\n    }\n  }\n}\n\n/** Represents the clause WHEN NOT MATCHED BY SOURCE THEN DELETE in MERGE. See\n * [[DeltaMergeInto]]. */\ncase class DeltaMergeIntoNotMatchedBySourceDeleteClause(condition: Option[Expression])\n  extends DeltaMergeIntoNotMatchedBySourceClause {\n  def this(condition: Option[Expression], actions: Seq[DeltaMergeAction]) = this(condition)\n\n  override def clauseType: String = \"Delete\"\n  override def actions: Seq[Expression] = Seq.empty\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): DeltaMergeIntoNotMatchedBySourceDeleteClause =\n    copy(condition = if (condition.isDefined) Some(newChildren.head) else None)\n}\n\n/**\n * Merges changes specified in the source plan into a target table, based on the given search\n * condition and the actions to perform when the condition is matched or not matched by the rows.\n *\n * The syntax of the MERGE statement is as follows.\n * {{{\n *    MERGE [WITH SCHEMA EVOLUTION] INTO <target_table_with_alias>\n *    USING <source_table_with_alias>\n *    ON <search_condition>\n *    [ WHEN MATCHED [ AND <condition> ] THEN <matched_action> ]\n *    [ WHEN MATCHED [ AND <condition> ] THEN <matched_action> ]\n *    ...\n *    [ WHEN NOT MATCHED [BY TARGET] [ AND <condition> ] THEN <not_matched_action> ]\n *    [ WHEN NOT MATCHED [BY TARGET] [ AND <condition> ] THEN <not_matched_action> ]\n *    ...\n *    [ WHEN NOT MATCHED BY SOURCE [ AND <condition> ] THEN <not_matched_by_source_action> ]\n *    [ WHEN NOT MATCHED BY SOURCE [ AND <condition> ] THEN <not_matched_by_source_action> ]\n *    ...\n *\n *\n *    where\n *    <matched_action> =\n *      DELETE |\n *      UPDATE SET column1 = value1 [, column2 = value2 ...] |\n *      UPDATE SET * [EXCEPT (column1, ...)]\n *    <not_matched_action> = INSERT (column1 [, column2 ...]) VALUES (expr1 [, expr2 ...])\n *    <not_matched_by_source_action> =\n *      DELETE |\n *      UPDATE SET column1 = value1 [, column2 = value2 ...]\n * }}}\n *\n * - There can be any number of WHEN clauses.\n * - WHEN MATCHED clauses:\n *    - Each WHEN MATCHED clause can have an optional condition. However, if there are multiple\n * WHEN MATCHED clauses, only the last can omit the condition.\n *    - WHEN MATCHED clauses are dependent on their ordering; that is, the first clause that\n * satisfies the clause's condition has its corresponding action executed.\n * - WHEN NOT MATCHED clause:\n *    - Can only have the INSERT action. If present, they must follow the last WHEN MATCHED clause.\n *    - Each WHEN NOT MATCHED clause can have an optional condition. However, if there are multiple\n * clauses, only the last can omit the condition.\n *    - WHEN NOT MATCHED clauses are dependent on their ordering; that is, the first clause that\n * satisfies the clause's condition has its corresponding action executed.\n * - WHEN NOT MATCHED BY SOURCE clauses:\n *    - Each WHEN NOT MATCHED BY SOURCE clause can have an optional condition. However, if there are\n * multiple WHEN NOT MATCHED BY SOURCE clauses, only the last can omit the condition.\n *    - WHEN NOT MATCHED BY SOURCE clauses are dependent on their ordering; that is, the first\n * clause that satisfies the clause's condition has its corresponding action executed.\n */\ncase class DeltaMergeInto(\n    target: LogicalPlan,\n    source: LogicalPlan,\n    condition: Expression,\n    matchedClauses: Seq[DeltaMergeIntoMatchedClause],\n    notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause],\n    notMatchedBySourceClauses: Seq[DeltaMergeIntoNotMatchedBySourceClause],\n    withSchemaEvolution: Boolean,\n    finalSchema: Option[StructType])\n  extends Command with SupportsSubquery {\n\n  (matchedClauses ++ notMatchedClauses ++ notMatchedBySourceClauses).foreach(_.verifyActions())\n\n  // TODO: extend BinaryCommand once the new Spark version is released\n  override def children: Seq[LogicalPlan] = Seq(target, source)\n  override def output: Seq[Attribute] = Seq.empty\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[LogicalPlan]): DeltaMergeInto =\n    copy(target = newChildren(0), source = newChildren(1))\n}\n\nobject DeltaMergeInto {\n  def apply(\n      target: LogicalPlan,\n      source: LogicalPlan,\n      condition: Expression,\n      whenClauses: Seq[DeltaMergeIntoClause],\n      withSchemaEvolution: Boolean): DeltaMergeInto = {\n    val notMatchedClauses = whenClauses.collect { case x: DeltaMergeIntoNotMatchedClause => x }\n    val matchedClauses = whenClauses.collect { case x: DeltaMergeIntoMatchedClause => x }\n    val notMatchedBySourceClauses =\n      whenClauses.collect { case x: DeltaMergeIntoNotMatchedBySourceClause => x }\n\n    // grammar enforcement goes here.\n    if (whenClauses.isEmpty) {\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_MERGE_MISSING_WHEN\",\n        messageParameters = Array.empty\n      )\n    }\n\n    // Check that only the last MATCHED clause omits the condition.\n    if (matchedClauses.length > 1 && !matchedClauses.init.forall(_.condition.nonEmpty)) {\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\",\n        messageParameters = Array.empty)\n    }\n\n    // Check that only the last NOT MATCHED clause omits the condition.\n    if (notMatchedClauses.length > 1 && !notMatchedClauses.init.forall(_.condition.nonEmpty)) {\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_NON_LAST_NOT_MATCHED_CLAUSE_OMIT_CONDITION\",\n        messageParameters = Array.empty)\n    }\n\n    // Check that only the last NOT MATCHED BY SOURCE clause omits the condition.\n    if (notMatchedBySourceClauses.length > 1 &&\n      !notMatchedBySourceClauses.init.forall(_.condition.nonEmpty)) {\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\",\n        messageParameters = Array.empty)\n    }\n\n    DeltaMergeInto(\n      target,\n      source,\n      condition,\n      matchedClauses,\n      notMatchedClauses,\n      notMatchedBySourceClauses,\n      withSchemaEvolution,\n      finalSchema = Some(target.schema))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/deltaTableFeatures.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.delta.tablefeatures.DropFeature\n\nimport org.apache.spark.sql.connector.catalog.TableChange\n\n/**\n * The logical plan of the ALTER TABLE ... DROP FEATURE command.\n */\ncase class AlterTableDropFeature(\n    table: LogicalPlan,\n    featureName: String,\n    truncateHistory: Boolean) extends AlterTableCommand {\n  override def changes: Seq[TableChange] = Seq(DropFeature(featureName, truncateHistory))\n  protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = copy(table = newChild)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/AllowedUserProvidedExpressions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.reflect.ClassTag\n\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.xml._\n\n/** This class defines the list of expressions that can be used when providing custom expressions.\n * e.g. in a generated column or a check constraint.\n * */\nobject AllowedUserProvidedExpressions {\n\n  /**\n   * This method has the same signature as `FunctionRegistry.expression` so that we can define the\n   * list in the same format as `FunctionRegistry.expressions` and that's easy to diff.\n   */\n  private def expression[T <: Expression : ClassTag](\n      name: String,\n      setAlias: Boolean = false): Class[_] = {\n    implicitly[ClassTag[T]].runtimeClass\n  }\n\n  // scalastyle:off\n  /**\n   * The white list is copied from `FunctionRegistry.expressions()` except the following types of\n   * functions:\n   * - explode functions. In other words, generate multiple rows from one row.\n   * - aggerate functions.\n   * - window functions.\n   * - grouping sets.\n   * - non deterministic functions.\n   * - deterministic functions in one query but non deterministic in multiple queries,\n   *   such as, current_timestamp, rand, etc.\n   *\n   * To review the difference, you can run\n   * `diff sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala sql/core/src/main/scala/com/databricks/sql/transaction/SupportedGenerationExpression.scala`\n   */\n  // scalastyle:on\n  val expressions: Set[Class[_]] = Set(\n    // misc non-aggregate functions\n    expression[Abs](\"abs\"),\n    expression[Coalesce](\"coalesce\"),\n    expression[Greatest](\"greatest\"),\n    expression[If](\"if\"),\n    expression[If](\"iff\", true),\n    expression[IsNaN](\"isnan\"),\n    expression[Nvl](\"ifnull\", true),\n    expression[IsNull](\"isnull\"),\n    expression[IsNotNull](\"isnotnull\"),\n    expression[Least](\"least\"),\n    expression[NaNvl](\"nanvl\"),\n    expression[NullIf](\"nullif\"),\n    expression[Nvl](\"nvl\"),\n    expression[Nvl2](\"nvl2\"),\n    expression[CaseWhen](\"when\"),\n\n    // math functions\n    expression[Acos](\"acos\"),\n    expression[Acosh](\"acosh\"),\n    expression[Asin](\"asin\"),\n    expression[Asinh](\"asinh\"),\n    expression[Atan](\"atan\"),\n    expression[Atan2](\"atan2\"),\n    expression[Atanh](\"atanh\"),\n    expression[Bin](\"bin\"),\n    expression[BRound](\"bround\"),\n    expression[Cbrt](\"cbrt\"),\n    expression[Ceil](\"ceil\"),\n    expression[Ceil](\"ceiling\", true),\n    expression[Cos](\"cos\"),\n    expression[Cosh](\"cosh\"),\n    expression[Conv](\"conv\"),\n    expression[ToDegrees](\"degrees\"),\n    expression[EulerNumber](\"e\"),\n    expression[Exp](\"exp\"),\n    expression[Expm1](\"expm1\"),\n    expression[Floor](\"floor\"),\n    expression[Factorial](\"factorial\"),\n    expression[Hex](\"hex\"),\n    expression[Hypot](\"hypot\"),\n    expression[Logarithm](\"log\"),\n    expression[Log10](\"log10\"),\n    expression[Log1p](\"log1p\"),\n    expression[Log2](\"log2\"),\n    expression[Log](\"ln\"),\n    expression[Remainder](\"mod\", true),\n    expression[UnaryMinus](\"negative\", true),\n    expression[Pi](\"pi\"),\n    expression[Pmod](\"pmod\"),\n    expression[UnaryPositive](\"positive\"),\n    expression[Pow](\"pow\", true),\n    expression[Pow](\"power\"),\n    expression[ToRadians](\"radians\"),\n    expression[Rint](\"rint\"),\n    expression[Round](\"round\"),\n    expression[ShiftLeft](\"shiftleft\"),\n    expression[ShiftRight](\"shiftright\"),\n    expression[ShiftRightUnsigned](\"shiftrightunsigned\"),\n    expression[Signum](\"sign\", true),\n    expression[Signum](\"signum\"),\n    expression[Sin](\"sin\"),\n    expression[Sinh](\"sinh\"),\n    expression[StringToMap](\"str_to_map\"),\n    expression[Sqrt](\"sqrt\"),\n    expression[Tan](\"tan\"),\n    expression[Cot](\"cot\"),\n    expression[Tanh](\"tanh\"),\n\n    expression[Add](\"+\"),\n    expression[Subtract](\"-\"),\n    expression[Multiply](\"*\"),\n    expression[Divide](\"/\"),\n    expression[IntegralDivide](\"div\"),\n    expression[Remainder](\"%\"),\n\n    // string functions\n    expression[Ascii](\"ascii\"),\n    expression[Chr](\"char\", true),\n    expression[Chr](\"chr\"),\n    expression[Base64](\"base64\"),\n    expression[BitLength](\"bit_length\"),\n    expression[Length](\"char_length\", true),\n    expression[Length](\"character_length\", true),\n    expression[ConcatWs](\"concat_ws\"),\n    expression[Decode](\"decode\"),\n    expression[Elt](\"elt\"),\n    expression[Encode](\"encode\"),\n    expression[FindInSet](\"find_in_set\"),\n    expression[FormatNumber](\"format_number\"),\n    expression[FormatString](\"format_string\"),\n    expression[GetJsonObject](\"get_json_object\"),\n    expression[InitCap](\"initcap\"),\n    expression[StringInstr](\"instr\"),\n    expression[Lower](\"lcase\", true),\n    expression[Length](\"length\"),\n    expression[Levenshtein](\"levenshtein\"),\n    expression[Like](\"like\"),\n    expression[Lower](\"lower\"),\n    expression[OctetLength](\"octet_length\"),\n    expression[StringLocate](\"locate\"),\n    expression[StringLPad](\"lpad\"),\n    expression[StringTrimLeft](\"ltrim\"),\n    expression[JsonTuple](\"json_tuple\"),\n    expression[ParseUrl](\"parse_url\"),\n    expression[StringLocate](\"position\", true),\n    expression[StringLocate](\"charindex\", true),\n    expression[FormatString](\"printf\", true),\n    expression[RegExpExtract](\"regexp_extract\"),\n    expression[RegExpReplace](\"regexp_replace\"),\n    expression[RLike](\"regexp_like\", true),\n    expression[StringRepeat](\"repeat\"),\n    expression[StringReplace](\"replace\"),\n    expression[Overlay](\"overlay\"),\n    expression[RLike](\"rlike\"),\n    expression[StringRPad](\"rpad\"),\n    expression[StringTrimRight](\"rtrim\"),\n    expression[Sentences](\"sentences\"),\n    expression[SoundEx](\"soundex\"),\n    expression[StringSpace](\"space\"),\n    expression[StringSplit](\"split\"),\n    expression[Substring](\"substr\", true),\n    expression[Substring](\"substring\"),\n    expression[Left](\"left\"),\n    expression[Right](\"right\"),\n    expression[SubstringIndex](\"substring_index\"),\n    expression[StringTranslate](\"translate\"),\n    expression[StringTrim](\"trim\"),\n    expression[Upper](\"ucase\", true),\n    expression[UnBase64](\"unbase64\"),\n    expression[Unhex](\"unhex\"),\n    expression[Upper](\"upper\"),\n    expression[XPathList](\"xpath\"),\n    expression[XPathBoolean](\"xpath_boolean\"),\n    expression[XPathDouble](\"xpath_double\"),\n    expression[XPathDouble](\"xpath_number\", true),\n    expression[XPathFloat](\"xpath_float\"),\n    expression[XPathInt](\"xpath_int\"),\n    expression[XPathLong](\"xpath_long\"),\n    expression[XPathShort](\"xpath_short\"),\n    expression[XPathString](\"xpath_string\"),\n\n    // datetime functions\n    expression[AddMonths](\"add_months\"),\n    expression[DateDiff](\"datediff\"),\n    expression[DateAdd](\"date_add\"),\n    expression[DateFormatClass](\"date_format\"),\n    expression[DateSub](\"date_sub\"),\n    expression[DayOfMonth](\"day\", true),\n    expression[DayOfYear](\"dayofyear\"),\n    expression[DayOfMonth](\"dayofmonth\"),\n    expression[FromUnixTime](\"from_unixtime\"),\n    expression[FromUTCTimestamp](\"from_utc_timestamp\"),\n    expression[Hour](\"hour\"),\n    expression[LastDay](\"last_day\"),\n    expression[Minute](\"minute\"),\n    expression[Month](\"month\"),\n    expression[MonthsBetween](\"months_between\"),\n    expression[NextDay](\"next_day\"),\n    expression[Now](\"now\"),\n    expression[Quarter](\"quarter\"),\n    expression[Second](\"second\"),\n    expression[TimestampAdd](\"timestampadd\"),\n    expression[TimestampDiff](\"timestampdiff\"),\n    expression[ParseToTimestamp](\"to_timestamp\"),\n    expression[ParseToDate](\"to_date\"),\n    // `gettimestamp` is not a Spark built-in class but `ParseToDate` will refer to\n    // `gettimestamp` when a format is given, so it needs to be on the allowed list\n    expression[GetTimestamp](\"gettimestamp\"),\n    expression[ToUnixTimestamp](\"to_unix_timestamp\"),\n    expression[ToUTCTimestamp](\"to_utc_timestamp\"),\n    expression[TruncDate](\"trunc\"),\n    expression[TruncTimestamp](\"date_trunc\"),\n    expression[UnixTimestamp](\"unix_timestamp\"),\n    expression[DayOfWeek](\"dayofweek\"),\n    expression[WeekDay](\"weekday\"),\n    expression[WeekOfYear](\"weekofyear\"),\n    expression[Year](\"year\"),\n    expression[TimeWindow](\"window\"),\n    expression[MakeDate](\"make_date\"),\n    expression[MakeTimestamp](\"make_timestamp\"),\n    expression[MakeInterval](\"make_interval\"),\n    expression[Extract](\"date_part\", setAlias = true),\n    expression[Extract](\"extract\"),\n\n    // collection functions\n    expression[CreateArray](\"array\"),\n    expression[ArrayContains](\"array_contains\"),\n    expression[ArraysOverlap](\"arrays_overlap\"),\n    expression[ArrayIntersect](\"array_intersect\"),\n    expression[ArrayJoin](\"array_join\"),\n    expression[ArrayPosition](\"array_position\"),\n    expression[ArraySort](\"array_sort\"),\n    expression[ArrayExcept](\"array_except\"),\n    expression[ArrayUnion](\"array_union\"),\n    expression[CreateMap](\"map\"),\n    expression[CreateNamedStruct](\"named_struct\"),\n    expression[ElementAt](\"element_at\"),\n    expression[MapFromArrays](\"map_from_arrays\"),\n    expression[MapKeys](\"map_keys\"),\n    expression[MapValues](\"map_values\"),\n    expression[MapEntries](\"map_entries\"),\n    expression[MapFromEntries](\"map_from_entries\"),\n    expression[MapConcat](\"map_concat\"),\n    expression[Size](\"size\"),\n    expression[Slice](\"slice\"),\n    expression[Size](\"cardinality\", true),\n    expression[ArraysZip](\"arrays_zip\"),\n    expression[SortArray](\"sort_array\"),\n    expression[ArrayMin](\"array_min\"),\n    expression[ArrayMax](\"array_max\"),\n    expression[Reverse](\"reverse\"),\n    expression[Concat](\"concat\"),\n    expression[Flatten](\"flatten\"),\n    expression[Sequence](\"sequence\"),\n    expression[ArrayRepeat](\"array_repeat\"),\n    expression[ArrayRemove](\"array_remove\"),\n    expression[ArrayDistinct](\"array_distinct\"),\n    expression[ArrayTransform](\"transform\"),\n    expression[MapFilter](\"map_filter\"),\n    expression[ArrayFilter](\"filter\"),\n    expression[ArrayExists](\"exists\"),\n    expression[ArrayForAll](\"forall\"),\n    expression[ArrayAggregate](\"aggregate\"),\n    expression[ArrayAggregate](\"reduce\"),\n    expression[TransformValues](\"transform_values\"),\n    expression[TransformKeys](\"transform_keys\"),\n    expression[MapZipWith](\"map_zip_with\"),\n    expression[ZipWith](\"zip_with\"),\n\n    // misc functions\n    expression[AssertTrue](\"assert_true\"),\n    expression[Crc32](\"crc32\"),\n    expression[Md5](\"md5\"),\n    expression[Murmur3Hash](\"hash\"),\n    expression[XxHash64](\"xxhash64\"),\n    expression[Sha1](\"sha\", true),\n    expression[Sha1](\"sha1\"),\n    expression[Sha2](\"sha2\"),\n    expression[TypeOf](\"typeof\"),\n\n    // predicates\n    expression[And](\"and\"),\n    expression[In](\"in\"),\n    expression[Not](\"not\"),\n    expression[Or](\"or\"),\n\n    // comparison operators\n    expression[EqualNullSafe](\"<=>\"),\n    expression[EqualTo](\"=\"),\n    expression[EqualTo](\"==\"),\n    expression[GreaterThan](\">\"),\n    expression[GreaterThanOrEqual](\">=\"),\n    expression[LessThan](\"<\"),\n    expression[LessThanOrEqual](\"<=\"),\n    expression[Not](\"!\"),\n\n    // bitwise\n    expression[BitwiseAnd](\"&\"),\n    expression[BitwiseNot](\"~\"),\n    expression[BitwiseOr](\"|\"),\n    expression[BitwiseXor](\"^\"),\n    expression[BitwiseCount](\"bit_count\"),\n\n    // json\n    expression[StructsToJson](\"to_json\"),\n    expression[JsonToStructs](\"from_json\"),\n    expression[SchemaOfJson](\"schema_of_json\"),\n\n    // cast\n    expression[Cast](\"cast\"),\n    // We don't need to define `castAlias` since they will use the same `Cast` expression.\n\n    // csv\n    expression[CsvToStructs](\"from_csv\"),\n    expression[SchemaOfCsv](\"schema_of_csv\"),\n    expression[StructsToCsv](\"to_csv\"),\n\n    // Special expressions that are not built-in expressions.\n    expression[AttributeReference](\"col\"),\n    expression[Literal](\"lit\")\n  )\n\n  val checkConstraintExpressions: Set[Class[_]] = Set(\n    expression[Contains](\"contains\"),\n    expression[StartsWith](\"startswith\"),\n    expression[EndsWith](\"endswith\"),\n    expression[InSet](\"inset\"),\n    // Lambda Functions\n    expression[LambdaFunction](\"lambdafunction\"),\n    expression[NamedLambdaVariable](\"namedlambdavariable\"),\n    // Date/Time Functions\n    expression[CurrentDate](\"current_date\"),\n    expression[CurrentTimestamp](\"current_timestamp\"),\n    // Used by Extract when applied to interval types\n    expression[ExtractANSIIntervalDays](\"extractansiintervaldays\"),\n    expression[ExtractANSIIntervalHours](\"extractansiintervalhours\"),\n    expression[ExtractANSIIntervalMinutes](\"extractansiintervalminutes\"),\n    expression[ExtractANSIIntervalSeconds](\"extractansiintervalseconds\"),\n    expression[ExtractANSIIntervalYears](\"extractansiintervalyears\"),\n    expression[ExtractANSIIntervalMonths](\"extractansiintervalmonths\"),\n    expression[ExtractIntervalYears](\"extractintervalyears\"),\n    expression[ExtractIntervalMonths](\"extractintervalmonths\"),\n    expression[ExtractIntervalDays](\"extractintervaldays\"),\n    expression[ExtractIntervalHours](\"extractintervalhours\"),\n    expression[ExtractIntervalMinutes](\"extractintervalminutes\"),\n    expression[ExtractIntervalSeconds](\"extractintervalseconds\"),\n    // Date/time arithmetic expressions\n    expression[DatetimeSub](\"datetimesub\"),\n    // Date/time arithmetic with intervals\n    expression[TimestampAddYMInterval](\"timestampaddyminterval\"),\n    expression[DateAddInterval](\"dateaddinterval\"),\n    expression[DateAddYMInterval](\"dateaddyminterval\"),\n\n    // Comparison functions\n    expression[ILike](\"ilike\"),\n    expression[LikeAny](\"likeany\"),\n    expression[NotLikeAny](\"notlikeany\"),\n    expression[LikeAll](\"likeall\"),\n    expression[NotLikeAll](\"notlikeall\"),\n\n    // Try arithmetic functions\n    expression[TryAdd](\"try_add\"),\n    expression[TrySubtract](\"try_subtract\"),\n    expression[TryMultiply](\"try_multiply\"),\n    expression[TryDivide](\"try_divide\"),\n\n    // Try parsing/conversion functions\n    expression[TryToBinary](\"try_to_binary\"),\n    expression[TryToNumber](\"try_to_number\"),\n    expression[ToNumber](\"to_number\"),\n\n\n    // Collection functions\n    expression[ArraySize](\"array_size\"),\n    expression[ArrayCompact](\"array_compact\"),\n    expression[ArrayAppend](\"array_append\"),\n    expression[ArrayPrepend](\"array_prepend\"),\n    expression[ArrayInsert](\"array_insert\")\n  )\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/CheckUnresolvedRelationTimeTravel.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.RelationTimeTravel\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.trees.TreePattern.RELATION_TIME_TRAVEL\n\n/**\n * Custom check rule that compensates for [SPARK-45383]. It checks the (unresolved) child relation\n * of each [[RelationTimeTravel]] in the plan, in order to trigger a helpful table-not-found\n * [[AnalysisException]] instead of the internal spark error that would otherwise result.\n */\nclass CheckUnresolvedRelationTimeTravel(spark: SparkSession) extends (LogicalPlan => Unit) {\n  override def apply(plan: LogicalPlan): Unit = {\n    // Short circuit: We only care about (unresolved) plans containing [[RelationTimeTravel]].\n    if (plan.containsPattern(RELATION_TIME_TRAVEL)) {\n      plan.foreachUp {\n        case tt: RelationTimeTravel => spark.sessionState.analyzer.checkAnalysis0(tt.relation)\n        case _ => ()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/CheckpointProvider.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.duration.Duration\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.SnapshotManagement.checkpointV2ThreadPool\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LogStore\nimport org.apache.spark.sql.delta.util.FileNames._\nimport org.apache.spark.sql.delta.util.threads.NonFateSharingFuture\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.sql.Dataset\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Represents basic information about a checkpoint.\n * This is the info we always can know about a checkpoint, without doing any additional I/O.\n */\ntrait UninitializedCheckpointProvider {\n\n  /** True if the checkpoint provider is empty (does not refer to a valid checkpoint) */\n  def isEmpty: Boolean = version < 0\n\n  /** Checkpoint version */\n  def version: Long\n\n  /**\n   * Top level files that represents this checkpoint.\n   * These files could be reused again to initialize the [[CheckpointProvider]].\n   */\n  def topLevelFiles: Seq[FileStatus]\n\n  /**\n   * File index which could help derive actions stored in top level files\n   * for the checkpoint.\n   * This could be used to get [[Protocol]], [[Metadata]] etc from a checkpoint.\n   * This could also be used if we want to shallow copy a checkpoint.\n   */\n  def topLevelFileIndex: Option[DeltaLogFileIndex]\n}\n\n/**\n * A trait which provides information about a checkpoint to the Snapshot.\n */\ntrait CheckpointProvider extends UninitializedCheckpointProvider {\n\n  /** Effective size of checkpoint across all files */\n  def effectiveCheckpointSizeInBytes(): Long\n\n  /**\n   * List of different file indexes which could help derive full state-reconstruction\n   * for the checkpoint.\n   */\n  def allActionsFileIndexes(): Seq[DeltaLogFileIndex]\n\n  /**\n   * The type of checkpoint (V2 vs Classic). This will be None when no checkpoint is available.\n   * This is only intended to be used for logging and metrics.\n   */\n  def checkpointPolicy: Option[CheckpointPolicy.Policy]\n\n  /**\n   * List of different file indexes and corresponding schemas which could help derive full\n   * state-reconstruction for the checkpoint.\n   * Different FileIndexes could have different schemas depending on `stats_parsed` / `stats`\n   * columns in the underlying file(s).\n   */\n  def allActionsFileIndexesAndSchemas(\n    spark: SparkSession, deltaLog: DeltaLog): Seq[(DeltaLogFileIndex, StructType)]\n}\n\nobject CheckpointProvider extends DeltaLogging {\n\n  /** Helper method to convert non-empty checkpoint files to DeltaLogFileIndex */\n  def checkpointFileIndex(checkpointFiles: Seq[FileStatus]): DeltaLogFileIndex = {\n    assert(checkpointFiles.nonEmpty, \"checkpointFiles must not be empty\")\n    DeltaLogFileIndex(DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_PARQUET, checkpointFiles).get\n  }\n\n  /** Converts an [[UninitializedCheckpointProvider]] into a [[CheckpointProvider]] */\n  def apply(\n      spark: SparkSession,\n      snapshotDescriptor: SnapshotDescriptor,\n      checksumOpt: Option[VersionChecksum],\n      uninitializedCheckpointProvider: UninitializedCheckpointProvider)\n      : CheckpointProvider = uninitializedCheckpointProvider match {\n    // Note: snapshotDescriptor.protocol should be accessed as late as possible inside the futures\n    // as it might need I/O.\n    case uninitializedV2CheckpointProvider: UninitializedV2CheckpointProvider =>\n      new LazyCompleteCheckpointProvider(uninitializedV2CheckpointProvider) {\n        override def createCheckpointProvider(): CheckpointProvider = {\n          val (checkpointMetadataOpt, sidecarFiles) =\n            uninitializedV2CheckpointProvider.nonFateSharingCheckpointReadFuture.get(Duration.Inf)\n          // This must be a v2 checkpoint, so checkpointMetadataOpt must be non empty.\n          val checkpointMetadata = checkpointMetadataOpt.getOrElse {\n            val checkpointFile = uninitializedV2CheckpointProvider.topLevelFiles.head\n            throw new IllegalStateException(s\"V2 Checkpoint ${checkpointFile.getPath} \" +\n              s\"has no CheckpointMetadata action\")\n          }\n          require(isV2CheckpointEnabled(snapshotDescriptor.protocol))\n          V2CheckpointProvider(\n            uninitializedV2CheckpointProvider,\n            checkpointMetadata,\n            sidecarFiles,\n            snapshotDescriptor.deltaLog)\n        }\n      }\n    case provider: UninitializedV1OrV2ParquetCheckpointProvider\n        if isV2CheckpointEnabled(checksumOpt).contains(false) =>\n      // V2 checkpoints are specifically disabled, so it must be V1\n      PreloadedCheckpointProvider(provider.topLevelFiles, provider.lastCheckpointInfoOpt)\n    case provider: UninitializedV1OrV2ParquetCheckpointProvider =>\n      // Either v2 checkpoints are explicitly enabled, or we lack a Protocol to prove otherwise.\n      // We can't tell immediately whether it's V1 or V2, just by looking at the file name.\n\n      // Start a future to start reading the v2 actions from the parquet checkpoint and return\n      // a lazy checkpoint provider wrapping the future. we won't wait on the future unless/until\n      // somebody calls a complete checkpoint provider method.\n      val future = checkpointV2ThreadPool.submitNonFateSharing { spark: SparkSession =>\n        readV2ActionsFromParquetCheckpoint(\n          spark, provider.logPath, provider.fileStatus, snapshotDescriptor.deltaLog.options)\n      }\n      new LazyCompleteCheckpointProvider(provider) {\n        override def createCheckpointProvider(): CheckpointProvider = {\n          val (checkpointMetadataOpt, sidecarFiles) = future.get(Duration.Inf)\n          checkpointMetadataOpt match {\n            case Some(cm) =>\n              require(isV2CheckpointEnabled(snapshotDescriptor))\n              V2CheckpointProvider(provider, cm, sidecarFiles, snapshotDescriptor.deltaLog)\n            case None =>\n              PreloadedCheckpointProvider(provider.topLevelFiles, provider.lastCheckpointInfoOpt)\n          }\n        }\n      }\n  }\n\n  private[delta] def isV2CheckpointEnabled(protocol: Protocol): Boolean =\n    protocol.isFeatureSupported(V2CheckpointTableFeature)\n\n  /**\n   * Returns whether V2 Checkpoints are enabled or not.\n   * This means an underlying checkpoint in this table could be a V2Checkpoint with sidecar files.\n   */\n  def isV2CheckpointEnabled(snapshotDescriptor: SnapshotDescriptor): Boolean =\n    isV2CheckpointEnabled(snapshotDescriptor.protocol)\n\n  /**\n   * Returns:\n   * - Some(true) if V2 Checkpoints are enabled for the snapshot corresponding to the given\n   *   `checksumOpt`.\n   * - Some(false) if V2 Checkpoints are disabled for the snapshot\n   * - None if the given checksumOpt is not sufficient to identify if v2 checkpoints are enabled or\n   *   not.\n   */\n  def isV2CheckpointEnabled(checksumOpt: Option[VersionChecksum]): Option[Boolean] = {\n    checksumOpt.flatMap(checksum => Option(checksum.protocol)).map(isV2CheckpointEnabled)\n  }\n\n  private[delta] def getParquetSchema(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      parquetFile: FileStatus,\n      schemaFromLastCheckpoint: Option[StructType]): StructType = {\n    // Try to get the checkpoint schema from the last_checkpoint.\n    // If it is not there then get it from filesystem by doing I/O.\n    val fetchChkSchemaFromLastCheckpoint = spark.sessionState.conf.getConf(\n      DeltaSQLConf.USE_CHECKPOINT_SCHEMA_FROM_CHECKPOINT_METADATA)\n    schemaFromLastCheckpoint match {\n      case Some(schema) if fetchChkSchemaFromLastCheckpoint => schema\n      case _ =>\n        recordDeltaOperation(deltaLog, \"snapshot.checkpointSchema.fromFileSystem\") {\n          Snapshot.getParquetFileSchemaAndRowCount(spark, deltaLog, parquetFile)._1\n        }\n    }\n  }\n\n  private def sendEventForV2CheckpointRead(\n      startTimeMs: Long,\n      fileStatus: FileStatus,\n      fileType: String,\n      logPath: Path,\n      exception: Option[Throwable]): Unit = {\n    recordDeltaEvent(\n      deltaLog = null,\n      opType = \"delta.checkpointV2.readV2ActionsFromCheckpoint\",\n      data = Map(\n        \"timeTakenMs\" -> (System.currentTimeMillis() - startTimeMs),\n        \"v2CheckpointPath\" -> fileStatus.getPath.toString,\n        \"v2CheckpointSize\" -> fileStatus.getLen,\n        \"errorMessage\" -> exception.map(_.toString).getOrElse(\"\"),\n        \"fileType\" -> fileType\n      ),\n      path = Some(logPath.getParent)\n    )\n  }\n\n  /** Reads and returns the [[CheckpointMetadata]] and [[SidecarFile]]s from a json v2 checkpoint */\n  private[delta] def readV2ActionsFromJsonCheckpoint(\n      logStore: LogStore,\n      logPath: Path,\n      fileStatus: FileStatus,\n      hadoopConf: Configuration): (CheckpointMetadata, Seq[SidecarFile]) = {\n    val startTimeMs = System.currentTimeMillis()\n    try {\n      var checkpointMetadataOpt: Option[CheckpointMetadata] = None\n      val sidecarFileActions: ArrayBuffer[SidecarFile] = ArrayBuffer.empty\n      logStore.readAsIterator(fileStatus, hadoopConf).processAndClose { _\n        .map(Action.fromJson)\n        .foreach {\n          case cm: CheckpointMetadata if checkpointMetadataOpt.isEmpty =>\n            checkpointMetadataOpt = Some(cm)\n          case cm: CheckpointMetadata =>\n            throw new IllegalStateException(\n              \"More than 1 CheckpointMetadata actions found in the checkpoint file\")\n          case sidecarFile: SidecarFile =>\n            sidecarFileActions.append(sidecarFile)\n          case _ => ()\n        }\n      }\n      val checkpointMetadata = checkpointMetadataOpt.getOrElse {\n        throw new IllegalStateException(\"Json V2 Checkpoint has no CheckpointMetadata action\")\n      }\n      sendEventForV2CheckpointRead(startTimeMs, fileStatus, \"json\", logPath, exception = None)\n      (checkpointMetadata, sidecarFileActions.toSeq)\n    } catch {\n      case NonFatal(e) =>\n        sendEventForV2CheckpointRead(startTimeMs, fileStatus, \"json\", logPath, exception = Some(e))\n        throw e\n    }\n  }\n\n  /**\n   * Reads and returns the optional [[CheckpointMetadata]], [[SidecarFile]]s from a parquet\n   * checkpoint.\n   * The checkpoint metadata returned might be None if the underlying parquet file is not a v2\n   * checkpoint.\n   */\n  private[delta] def readV2ActionsFromParquetCheckpoint(\n      spark: SparkSession,\n      logPath: Path,\n      fileStatus: FileStatus,\n      deltaLogOptions: Map[String, String]): (Option[CheckpointMetadata], Seq[SidecarFile]) = {\n    val startTimeMs = System.currentTimeMillis()\n    try {\n      val relation = DeltaLog.indexToRelation(\n        spark, checkpointFileIndex(Seq(fileStatus)), deltaLogOptions, Action.logSchema)\n      import implicits._\n      val rows = DataFrameUtils.ofRows(spark, relation)\n        .select(\"checkpointMetadata\", \"sidecar\")\n        .where(\"checkpointMetadata.version is not null or sidecar.path is not null\")\n        .as[(CheckpointMetadata, SidecarFile)]\n        .collect()\n\n      var checkpointMetadata: Option[CheckpointMetadata] = None\n      val checkpointSidecarFiles = ArrayBuffer.empty[SidecarFile]\n      rows.foreach {\n        case (cm: CheckpointMetadata, _) if checkpointMetadata.isEmpty =>\n          checkpointMetadata = Some(cm)\n        case (cm: CheckpointMetadata, _) =>\n          throw new IllegalStateException(\n            \"More than 1 CheckpointMetadata actions found in the checkpoint file\")\n        case (_, sf: SidecarFile) =>\n          checkpointSidecarFiles.append(sf)\n      }\n      if (checkpointMetadata.isEmpty && checkpointSidecarFiles.nonEmpty) {\n        throw new IllegalStateException(\n          \"sidecar files present in checkpoint even when checkpoint metadata is missing\")\n      }\n      sendEventForV2CheckpointRead(startTimeMs, fileStatus, \"parquet\", logPath, exception = None)\n      (checkpointMetadata, checkpointSidecarFiles.toSeq)\n    } catch {\n      case NonFatal(e) =>\n        sendEventForV2CheckpointRead(startTimeMs, fileStatus, \"parquet\", logPath, Some(e))\n        throw e\n    }\n  }\n}\n\n/**\n * An implementation of [[CheckpointProvider]] where the information about checkpoint files\n * (i.e. Seq[FileStatus]) is already known in advance.\n *\n * @param topLevelFiles - file statuses that describes the checkpoint\n * @param lastCheckpointInfoOpt - optional [[LastCheckpointInfo]] corresponding to this checkpoint.\n *                                This comes from _last_checkpoint file\n */\ncase class PreloadedCheckpointProvider(\n    override val topLevelFiles: Seq[FileStatus],\n    lastCheckpointInfoOpt: Option[LastCheckpointInfo])\n  extends CheckpointProvider\n  with DeltaLogging {\n\n  require(topLevelFiles.nonEmpty, \"There should be atleast 1 checkpoint file\")\n  private lazy val fileIndex = CheckpointProvider.checkpointFileIndex(topLevelFiles)\n\n  override def version: Long = checkpointVersion(topLevelFiles.head)\n\n  override def effectiveCheckpointSizeInBytes(): Long = fileIndex.sizeInBytes\n\n  override def allActionsFileIndexes(): Seq[DeltaLogFileIndex] = Seq(fileIndex)\n\n  override lazy val topLevelFileIndex: Option[DeltaLogFileIndex] = Some(fileIndex)\n\n  override def checkpointPolicy: Option[CheckpointPolicy.Policy] = Some(CheckpointPolicy.Classic)\n\n  override def allActionsFileIndexesAndSchemas(\n      spark: SparkSession, deltaLog: DeltaLog): Seq[(DeltaLogFileIndex, StructType)] = {\n    Seq((fileIndex, checkpointSchema(spark, deltaLog)))\n  }\n\n  private val checkpointSchemaWithCaching = new LazyCheckpointSchemaGetter {\n    override def fileStatus: FileStatus = topLevelFiles.head\n    override def schemaFromLastCheckpoint: Option[StructType] =\n      lastCheckpointInfoOpt.flatMap(_.checkpointSchema)\n  }\n  private def checkpointSchema(spark: SparkSession, deltaLog: DeltaLog): StructType =\n    checkpointSchemaWithCaching.get(spark, deltaLog)\n\n}\n\n/**\n * An implementation for [[CheckpointProvider]] which could be used to represent a scenario when\n * checkpoint doesn't exist. This helps us simplify the code by making\n * [[LogSegment.checkpointProvider]] as non-optional.\n *\n * The [[CheckpointProvider.isEmpty]] method returns true for [[EmptyCheckpointProvider]]. Also\n * version is returned as -1.\n * For a real checkpoint, this will be returned true and version will be >= 0.\n */\nobject EmptyCheckpointProvider extends CheckpointProvider {\n  override def version: Long = -1\n  override def topLevelFiles: Seq[FileStatus] = Nil\n  override def effectiveCheckpointSizeInBytes(): Long = 0L\n  override def allActionsFileIndexes(): Seq[DeltaLogFileIndex] = Nil\n  override def topLevelFileIndex: Option[DeltaLogFileIndex] = None\n  override def checkpointPolicy: Option[CheckpointPolicy.Policy] = None\n  override def allActionsFileIndexesAndSchemas(\n    spark: SparkSession, deltaLog: DeltaLog): Seq[(DeltaLogFileIndex, StructType)] = Nil\n}\n\n/** A trait representing a v2 [[UninitializedCheckpointProvider]] */\ntrait UninitializedV2LikeCheckpointProvider extends UninitializedCheckpointProvider {\n  def fileStatus: FileStatus\n  def logPath: Path\n  def lastCheckpointInfoOpt: Option[LastCheckpointInfo]\n  def v2CheckpointFormat: V2Checkpoint.Format\n\n  override lazy val topLevelFiles: Seq[FileStatus] = Seq(fileStatus)\n  override lazy val topLevelFileIndex: Option[DeltaLogFileIndex] =\n    DeltaLogFileIndex(v2CheckpointFormat.fileFormat, topLevelFiles)\n}\n\n/**\n * An implementation of [[UninitializedCheckpointProvider]] to represent a parquet checkpoint\n * which could be either a v1 checkpoint or v2 checkpoint.\n * This needs to be resolved into a [[PreloadedCheckpointProvider]] or a [[V2CheckpointProvider]]\n * depending on whether the [[CheckpointMetadata]] action is present or not in the underlying\n * parquet file.\n */\ncase class UninitializedV1OrV2ParquetCheckpointProvider(\n    override val version: Long,\n    override val fileStatus: FileStatus,\n    override val logPath: Path,\n    override val lastCheckpointInfoOpt: Option[LastCheckpointInfo]\n) extends UninitializedV2LikeCheckpointProvider {\n\n  override val v2CheckpointFormat: V2Checkpoint.Format = V2Checkpoint.Format.PARQUET\n}\n\n/**\n * An implementation of [[UninitializedCheckpointProvider]] to for v2 checkpoints.\n * This needs to be resolved into a [[V2CheckpointProvider]].\n * This class starts an I/O to fetch the V2 actions ([[CheckpointMetadata]], [[SidecarFile]]) as\n * soon as the class is initialized so that the extra overhead could be parallelized with other\n * operations like reading CRC.\n */\ncase class UninitializedV2CheckpointProvider(\n    override val version: Long,\n    override val fileStatus: FileStatus,\n    override val logPath: Path,\n    hadoopConf: Configuration,\n    deltaLogOptions: Map[String, String],\n    logStore: LogStore,\n    override val lastCheckpointInfoOpt: Option[LastCheckpointInfo]\n) extends UninitializedV2LikeCheckpointProvider {\n\n  override val v2CheckpointFormat: V2Checkpoint.Format =\n    V2Checkpoint.toFormat(fileStatus.getPath.getName)\n\n  // Try to get the required actions from LastCheckpointInfo\n  private val v2ActionsFromLastCheckpointOpt: Option[(CheckpointMetadata, Seq[SidecarFile])] = {\n    lastCheckpointInfoOpt\n      .flatMap(_.v2Checkpoint)\n      .map(v2 => (v2.checkpointMetadataOpt, v2.sidecarFiles))\n      .collect {\n        case (Some(checkpointMetadata), Some(sidecarFiles)) =>\n          (checkpointMetadata, sidecarFiles)\n      }\n  }\n\n  /** Helper method to do I/O and read v2 actions from the underlying v2 checkpoint file */\n  private def readV2Actions(spark: SparkSession): (Option[CheckpointMetadata], Seq[SidecarFile]) = {\n    v2CheckpointFormat match {\n      case V2Checkpoint.Format.JSON =>\n        val (checkpointMetadata, sidecars) = CheckpointProvider.readV2ActionsFromJsonCheckpoint(\n            logStore, logPath, fileStatus, hadoopConf)\n        (Some(checkpointMetadata), sidecars)\n      case V2Checkpoint.Format.PARQUET =>\n        CheckpointProvider.readV2ActionsFromParquetCheckpoint(\n            spark, logPath, fileStatus, deltaLogOptions)\n    }\n  }\n\n  val nonFateSharingCheckpointReadFuture\n      : NonFateSharingFuture[(Option[CheckpointMetadata], Seq[SidecarFile])] = {\n    checkpointV2ThreadPool.submitNonFateSharing { spark: SparkSession =>\n      v2ActionsFromLastCheckpointOpt match {\n        case Some((cm, sidecars)) => Some(cm) -> sidecars\n        case None => readV2Actions(spark)\n      }\n    }\n  }\n}\n\n/**\n * A wrapper implementation of [[CheckpointProvider]] which wraps\n * `underlyingCheckpointProviderFuture` and `uninitializedCheckpointProvider` for implementing all\n * the [[UninitializedCheckpointProvider]] and [[CheckpointProvider]] APIs.\n *\n * @param uninitializedCheckpointProvider the underlying [[UninitializedCheckpointProvider]]\n */\nabstract class LazyCompleteCheckpointProvider(\n    uninitializedCheckpointProvider: UninitializedCheckpointProvider)\n  extends CheckpointProvider {\n\n  override def version: Long = uninitializedCheckpointProvider.version\n  override def topLevelFiles: Seq[FileStatus] = uninitializedCheckpointProvider.topLevelFiles\n  override def topLevelFileIndex: Option[DeltaLogFileIndex] =\n    uninitializedCheckpointProvider.topLevelFileIndex\n\n  protected def createCheckpointProvider(): CheckpointProvider\n\n  lazy val underlyingCheckpointProvider: CheckpointProvider = createCheckpointProvider()\n\n  override def effectiveCheckpointSizeInBytes(): Long =\n    underlyingCheckpointProvider.effectiveCheckpointSizeInBytes()\n\n  override def allActionsFileIndexes(): Seq[DeltaLogFileIndex] =\n    underlyingCheckpointProvider.allActionsFileIndexes()\n\n  override def checkpointPolicy: Option[CheckpointPolicy.Policy] =\n    underlyingCheckpointProvider.checkpointPolicy\n\n  override def allActionsFileIndexesAndSchemas(\n      spark: SparkSession, deltaLog: DeltaLog): Seq[(DeltaLogFileIndex, StructType)] = {\n    underlyingCheckpointProvider.allActionsFileIndexesAndSchemas(spark, deltaLog)\n  }\n}\n\n/**\n * [[CheckpointProvider]] implementation for Json/Parquet V2 checkpoints.\n *\n * @param version               checkpoint version for the underlying checkpoint\n * @param v2CheckpointFile      [[FileStatus]] for the json/parquet v2 checkpoint file\n * @param v2CheckpointFormat    format (json/parquet) for the v2 checkpoint\n * @param checkpointMetadata    [[CheckpointMetadata]] for the v2 checkpoint\n * @param sidecarFiles          seq of [[SidecarFile]] for the v2 checkpoint\n * @param lastCheckpointInfoOpt optional last checkpoint info for the v2 checkpoint\n * @param logPath               delta log path for the underlying delta table\n * @param sidecarSchemaFetcher     function to fetch sidecar schema.\n *                              Returns None if there are no sidecar files.\n */\ncase class V2CheckpointProvider(\n    override val version: Long,\n    v2CheckpointFile: FileStatus,\n    v2CheckpointFormat: V2Checkpoint.Format,\n    checkpointMetadata: CheckpointMetadata,\n    sidecarFiles: Seq[SidecarFile],\n    lastCheckpointInfoOpt: Option[LastCheckpointInfo],\n    logPath: Path,\n    sidecarSchemaFetcher: () => Option[StructType]\n  ) extends CheckpointProvider with DeltaLogging {\n\n  private[delta] def sidecarFileStatuses: Seq[FileStatus] =\n    sidecarFiles.map(_.toFileStatus(logPath))\n\n  protected lazy val fileIndexesForSidecarFiles: Seq[DeltaLogFileIndex] = {\n    // V2 checkpoints without sidecars are legal.\n    if (sidecarFileStatuses.isEmpty) {\n      Seq.empty\n    } else {\n      Seq(CheckpointProvider.checkpointFileIndex(sidecarFileStatuses))\n    }\n  }\n\n  protected lazy val fileIndexForV2Checkpoint: DeltaLogFileIndex =\n    DeltaLogFileIndex(v2CheckpointFormat.fileFormat, Seq(v2CheckpointFile)).head\n\n  override lazy val topLevelFiles: Seq[FileStatus] = Seq(v2CheckpointFile)\n  override lazy val topLevelFileIndex: Option[DeltaLogFileIndex] = Some(fileIndexForV2Checkpoint)\n  override def effectiveCheckpointSizeInBytes(): Long =\n    sidecarFiles.map(_.sizeInBytes).sum + v2CheckpointFile.getLen\n  override def allActionsFileIndexes(): Seq[DeltaLogFileIndex] =\n    topLevelFileIndex ++: fileIndexesForSidecarFiles\n\n  override def checkpointPolicy: Option[CheckpointPolicy.Policy] = Some(CheckpointPolicy.V2)\n\n  private val v2SchemaWithCaching = new LazyCheckpointSchemaGetter {\n    override def fileStatus: FileStatus = v2CheckpointFile\n    override def schemaFromLastCheckpoint: Option[StructType] =\n      lastCheckpointInfoOpt.flatMap(_.checkpointSchema)\n  }\n\n  protected def schemaForV2Checkpoint(\n      spark: SparkSession, deltaLog: DeltaLog): StructType = {\n    if (v2CheckpointFormat != V2Checkpoint.Format.PARQUET) {\n      return Action.logSchema\n    }\n    v2SchemaWithCaching.get(spark, deltaLog)\n  }\n\n  protected def schemaForSidecarFile(spark: SparkSession, deltaLog: DeltaLog): StructType = {\n    sidecarSchemaFetcher()\n      .getOrElse {\n        throw DeltaErrors.assertionFailedError(\"Sidecar schema asked without any sidecar files\")\n      }\n  }\n\n  override def allActionsFileIndexesAndSchemas(\n      spark: SparkSession, deltaLog: DeltaLog): Seq[(DeltaLogFileIndex, StructType)] = {\n    (fileIndexForV2Checkpoint, schemaForV2Checkpoint(spark, deltaLog)) +:\n      fileIndexesForSidecarFiles.map((_, schemaForSidecarFile(spark, deltaLog)))\n  }\n}\n\nobject V2CheckpointProvider {\n  /** Alternate constructor which uses [[UninitializedV2LikeCheckpointProvider]] */\n  def apply(\n      uninitializedV2LikeCheckpointProvider: UninitializedV2LikeCheckpointProvider,\n      checkpointMetadata: CheckpointMetadata,\n      sidecarFiles: Seq[SidecarFile],\n      deltaLog: DeltaLog): V2CheckpointProvider = {\n    def getSidecarSchemaFetcher: () => Option[StructType] = {\n      val nonFateSharingSidecarSchemaFuture: NonFateSharingFuture[Option[StructType]] = {\n        checkpointV2ThreadPool.submitNonFateSharing { spark: SparkSession =>\n          sidecarFiles.headOption.map { sidecarFile =>\n            val sidecarFileStatus =\n              sidecarFile.toFileStatus(uninitializedV2LikeCheckpointProvider.logPath)\n            CheckpointProvider.getParquetSchema(\n              spark, deltaLog, sidecarFileStatus, schemaFromLastCheckpoint = None)\n          }\n        }\n      }\n      () => nonFateSharingSidecarSchemaFuture.get(Duration.Inf)\n    }\n    V2CheckpointProvider(\n      uninitializedV2LikeCheckpointProvider.version,\n      uninitializedV2LikeCheckpointProvider.fileStatus,\n      uninitializedV2LikeCheckpointProvider.v2CheckpointFormat,\n      checkpointMetadata,\n      sidecarFiles,\n      uninitializedV2LikeCheckpointProvider.lastCheckpointInfoOpt,\n      uninitializedV2LikeCheckpointProvider.logPath,\n      getSidecarSchemaFetcher\n    )\n  }\n}\n\nabstract class LazyCheckpointSchemaGetter {\n  protected def fileStatus: FileStatus\n  protected def schemaFromLastCheckpoint: Option[StructType]\n\n  private var lazySchema = Option.empty[StructType]\n\n  def get(spark: SparkSession, deltaLog: DeltaLog): StructType = {\n    lazySchema.getOrElse {\n      this.synchronized {\n        // re-check with lock held, in case of races with other initializers\n        if (lazySchema.isEmpty) {\n          lazySchema = Some(CheckpointProvider.getParquetSchema(\n            spark, deltaLog, fileStatus, schemaFromLastCheckpoint))\n        }\n        lazySchema.get\n      }\n    }\n  }\n\n  def getIfKnown: Option[StructType] = lazySchema\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/Checkpoints.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.FileNotFoundException\nimport java.util.UUID\n\nimport scala.collection.mutable\nimport scala.math.Ordering.Implicits._\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{Action, CheckpointMetadata, Metadata, SidecarFile, SingleAction}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LogStore\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, DeltaLogGroupingIterator, FileNames}\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.FileNames._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, Path}\nimport org.apache.hadoop.mapred.{JobConf, TaskAttemptContextImpl, TaskAttemptID}\nimport org.apache.hadoop.mapreduce.{Job, TaskType}\n\nimport org.apache.spark.TaskContext\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.{Column, DataFrame, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Cast, ElementAt, Literal}\nimport org.apache.spark.sql.delta.expressions.DecodeNestedZ85EncodedVariant\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.shims.VariantShreddingShims\nimport org.apache.spark.sql.execution.SQLExecution\nimport org.apache.spark.sql.execution.datasources.FileFormat\nimport org.apache.spark.sql.execution.datasources.OutputWriter\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.functions.{coalesce, col, struct, when}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.SerializableConfiguration\nimport org.apache.spark.util.Utils\n\n/**\n * A class to help with comparing checkpoints with each other, where we may have had concurrent\n * writers that checkpoint with different number of parts.\n * The `numParts` field will be present only for multipart checkpoints (represented by\n * Format.WITH_PARTS).\n * The `fileName` field is present only for V2 Checkpoints (represented by Format.V2)\n * These additional fields are used as a tie breaker when comparing multiple checkpoint\n * instance of same Format for the same `version`.\n */\ncase class CheckpointInstance(\n    version: Long,\n    format: CheckpointInstance.Format,\n    fileName: Option[String] = None,\n    numParts: Option[Int] = None) extends Ordered[CheckpointInstance] {\n\n  // Assert that numParts are present when checkpoint format is Format.WITH_PARTS.\n  // For other formats, numParts must be None.\n  require((format == CheckpointInstance.Format.WITH_PARTS) == numParts.isDefined,\n    s\"numParts ($numParts) must be present for checkpoint format\" +\n      s\" ${CheckpointInstance.Format.WITH_PARTS.name}\")\n  // Assert that filePath is present only when checkpoint format is Format.V2.\n  // For other formats, filePath must be None.\n  require((format == CheckpointInstance.Format.V2) == fileName.isDefined,\n    s\"fileName ($fileName) must be present for checkpoint format\" +\n      s\" ${CheckpointInstance.Format.V2.name}\")\n\n  /**\n   * Returns a [[CheckpointProvider]] which can tell the files corresponding to this\n   * checkpoint.\n   * The `lastCheckpointInfoHint` might be passed to [[CheckpointProvider]] so that underlying\n   * [[CheckpointProvider]] provides more precise info.\n   */\n  def getCheckpointProvider(\n      deltaLog: DeltaLog,\n      filesForCheckpointConstruction: Seq[FileStatus],\n      lastCheckpointInfoHint: Option[LastCheckpointInfo] = None)\n      : UninitializedCheckpointProvider = {\n    val logPath = deltaLog.logPath\n    val lastCheckpointInfo = lastCheckpointInfoHint.filter(cm => CheckpointInstance(cm) == this)\n    val cpFiles = filterFiles(deltaLog, filesForCheckpointConstruction)\n    format match {\n      // Treat single file checkpoints also as V2 Checkpoints because we don't know if it is\n      // actually a V2 checkpoint until we read it.\n      case CheckpointInstance.Format.V2 | CheckpointInstance.Format.SINGLE =>\n        assert(cpFiles.size == 1)\n        val fileStatus = cpFiles.head\n        if (format == CheckpointInstance.Format.V2) {\n          val hadoopConf = deltaLog.newDeltaHadoopConf()\n          UninitializedV2CheckpointProvider(\n            version,\n            fileStatus,\n            logPath,\n            hadoopConf,\n            deltaLog.options,\n            deltaLog.store,\n            lastCheckpointInfo)\n        } else {\n          UninitializedV1OrV2ParquetCheckpointProvider(\n            version, fileStatus, logPath, lastCheckpointInfo)\n        }\n      case CheckpointInstance.Format.WITH_PARTS =>\n        PreloadedCheckpointProvider(cpFiles, lastCheckpointInfo)\n      case CheckpointInstance.Format.SENTINEL =>\n        throw DeltaErrors.assertionFailedError(\n          s\"invalid checkpoint format ${CheckpointInstance.Format.SENTINEL}\")\n    }\n  }\n\n  def filterFiles(deltaLog: DeltaLog,\n                  filesForCheckpointConstruction: Seq[FileStatus]) : Seq[FileStatus] = {\n    val logPath = deltaLog.logPath\n    format match {\n      // Treat Single File checkpoints also as V2 Checkpoints because we don't know if it is\n      // actually a V2 checkpoint until we read it.\n      case format if format.usesSidecars =>\n        val checkpointFileName = format match {\n          case CheckpointInstance.Format.V2 => fileName.get\n          case CheckpointInstance.Format.SINGLE => checkpointFileSingular(logPath, version).getName\n          case other =>\n            throw new IllegalStateException(s\"Unknown checkpoint format $other supporting sidecars\")\n        }\n        val fileStatus = filesForCheckpointConstruction\n          .find(_.getPath.getName == checkpointFileName)\n          .getOrElse {\n            throw new IllegalStateException(\"Failed in getting the file information for:\\n\" +\n              fileName.get + \"\\namong\\n\" +\n              filesForCheckpointConstruction.map(_.getPath.getName).mkString(\" -\", \"\\n -\", \"\"))\n          }\n        Seq(fileStatus)\n      case CheckpointInstance.Format.WITH_PARTS | CheckpointInstance.Format.SINGLE =>\n        val filePaths = if (format == CheckpointInstance.Format.WITH_PARTS) {\n          checkpointFileWithParts(logPath, version, numParts.get).toSet\n        } else {\n          Set(checkpointFileSingular(logPath, version))\n        }\n        val newCheckpointFileArray =\n          filesForCheckpointConstruction.filter(f => filePaths.contains(f.getPath))\n        assert(newCheckpointFileArray.length == filePaths.size,\n          \"Failed in getting the file information for:\\n\" +\n            filePaths.mkString(\" -\", \"\\n -\", \"\") + \"\\namong\\n\" +\n            filesForCheckpointConstruction.map(_.getPath).mkString(\" -\", \"\\n -\", \"\"))\n        newCheckpointFileArray\n      case CheckpointInstance.Format.SENTINEL =>\n        throw DeltaErrors.assertionFailedError(\n          s\"invalid checkpoint format ${CheckpointInstance.Format.SENTINEL}\")\n    }\n  }\n\n  /**\n   * Comparison rules:\n   * 1. A [[CheckpointInstance]] with higher version is greater than the one with lower version.\n   * 2. For [[CheckpointInstance]]s with same version, a Multi-part checkpoint is greater than a\n   *    Single part checkpoint.\n   * 3. For Multi-part [[CheckpointInstance]]s corresponding to same version, the one with more\n   *    parts is greater than the one with less parts.\n   * 4. For V2 Checkpoints corresponding to same version, we use the fileName as tie breaker.\n   */\n  override def compare(other: CheckpointInstance): Int = {\n      (version, format, numParts, fileName) compare\n        (other.version, other.format, other.numParts, other.fileName)\n  }\n}\n\nobject CheckpointInstance {\n  sealed abstract class Format(val ordinal: Int, val name: String) extends Ordered[Format] {\n    override def compare(other: Format): Int = ordinal compare other.ordinal\n    def usesSidecars: Boolean = this.isInstanceOf[FormatUsesSidecars]\n  }\n  trait FormatUsesSidecars\n\n  object Format {\n    def unapply(name: String): Option[Format] = name match {\n      case SINGLE.name => Some(SINGLE)\n      case WITH_PARTS.name => Some(WITH_PARTS)\n      case V2.name => Some(V2)\n      case _ => None\n    }\n\n    /** single-file checkpoint format */\n    object SINGLE extends Format(0, \"SINGLE\") with FormatUsesSidecars\n    /** multi-file checkpoint format */\n    object WITH_PARTS extends Format(1, \"WITH_PARTS\")\n    /** V2 Checkpoint format */\n    object V2 extends Format(2, \"V2\") with FormatUsesSidecars\n    /** Sentinel, for internal use only */\n    object SENTINEL extends Format(Int.MaxValue, \"SENTINEL\")\n  }\n\n  def apply(path: Path): CheckpointInstance = {\n    // Three formats to worry about:\n    // * <version>.checkpoint.parquet\n    // * <version>.checkpoint.<i>.<n>.parquet\n    // * <version>.checkpoint.<u>.parquet where u is a unique string\n    path.getName.split(\"\\\\.\") match {\n      case Array(v, \"checkpoint\", uniqueStr, format) if Seq(\"json\", \"parquet\").contains(format) =>\n        CheckpointInstance(\n          version = v.toLong,\n          format = Format.V2,\n          numParts = None,\n          fileName = Some(path.getName))\n      case Array(v, \"checkpoint\", \"parquet\") =>\n        CheckpointInstance(v.toLong, Format.SINGLE, numParts = None)\n      case Array(v, \"checkpoint\", _, n, \"parquet\") =>\n        CheckpointInstance(v.toLong, Format.WITH_PARTS, numParts = Some(n.toInt))\n      case _ =>\n        throw DeltaErrors.assertionFailedError(s\"Unrecognized checkpoint path format: $path\")\n    }\n  }\n\n  def apply(version: Long): CheckpointInstance = {\n    CheckpointInstance(version, Format.SINGLE, numParts = None)\n  }\n\n  def apply(metadata: LastCheckpointInfo): CheckpointInstance = {\n    CheckpointInstance(\n      version = metadata.version,\n      format = metadata.getFormatEnum(),\n      fileName = metadata.v2Checkpoint.map(_.path),\n      numParts = metadata.parts)\n  }\n\n  val MaxValue: CheckpointInstance = sentinelValue(versionOpt = None)\n\n  def sentinelValue(versionOpt: Option[Long]): CheckpointInstance = {\n    val version = versionOpt.getOrElse(Long.MaxValue)\n    CheckpointInstance(version, Format.SENTINEL, numParts = None)\n  }\n}\n\ntrait Checkpoints extends DeltaLogging {\n  self: DeltaLog =>\n\n  def logPath: Path\n  def dataPath: Path\n  protected def store: LogStore\n\n  /** Used to clean up stale log files. */\n  protected def doLogCleanup(\n    snapshotToCleanup: Snapshot,\n    catalogTableOpt: Option[CatalogTable]): Unit\n\n  /** Returns the checkpoint interval for this log. Not transactional. */\n  def checkpointInterval(metadata: Metadata): Int =\n    DeltaConfigs.CHECKPOINT_INTERVAL.fromMetaData(metadata)\n\n  /** The path to the file that holds metadata about the most recent checkpoint. */\n  val LAST_CHECKPOINT = new Path(logPath, Checkpoints.LAST_CHECKPOINT_FILE_NAME)\n\n  /**\n   * Catch non-fatal exceptions related to checkpointing, since the checkpoint is written\n   * after the commit has completed. From the perspective of the user, the commit has\n   * completed successfully. However, throw if this is in a testing environment -\n   * that way any breaking changes can be caught in unit tests.\n   */\n  protected def withCheckpointExceptionHandling(\n      deltaLog: DeltaLog, opType: String)(thunk: => Unit): Unit = {\n    try {\n      thunk\n    } catch {\n      case NonFatal(e) =>\n        recordDeltaEvent(\n          deltaLog,\n          opType,\n          data = Map(\"exception\" -> e.getMessage(), \"stackTrace\" -> e.getStackTrace())\n        )\n        logWarning(log\"Error when writing checkpoint-related files\", e)\n        val throwError = DeltaUtils.isTesting ||\n          spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CHECKPOINT_THROW_EXCEPTION_WHEN_FAILED)\n        if (throwError) throw e\n    }\n  }\n\n  /**\n   * Creates a checkpoint using the default snapshot.\n   *\n   * WARNING: This API is being deprecated, and will be removed in future versions.\n   * Please use the checkpoint(Snapshot) function below to write checkpoints to the delta log.\n   */\n  @deprecated(\"This method is deprecated and will be removed in future versions.\", \"12.0\")\n  def checkpoint(): Unit = checkpoint(unsafeVolatileSnapshot)\n\n  /**\n   * Creates a checkpoint using snapshotToCheckpoint. By default it uses the current log version.\n   * Note that this function captures and logs all exceptions, since the checkpoint shouldn't fail\n   * the overall commit operation.\n   */\n  def checkpoint(\n      snapshotToCheckpoint: Snapshot,\n      catalogTableOpt: Option[CatalogTable] = None): Unit =\n    recordDeltaOperation(this, \"delta.checkpoint\") {\n    withCheckpointExceptionHandling(snapshotToCheckpoint.deltaLog, \"delta.checkpoint.sync.error\") {\n      if (snapshotToCheckpoint.version < 0) {\n        throw DeltaErrors.checkpointNonExistTable(dataPath)\n      }\n      checkpointAndCleanUpDeltaLog(snapshotToCheckpoint, catalogTableOpt)\n    }\n  }\n\n  /**\n   * Creates a checkpoint at given version. Does not invoke metadata cleanup as part of it.\n   * @param version - version at which we want to create a checkpoint.\n   */\n  def createCheckpointAtVersion(version: Long): Unit =\n    recordDeltaOperation(this, \"delta.createCheckpointAtVersion\") {\n      val snapshot = getSnapshotAt(version)\n      withCheckpointExceptionHandling(this, \"delta.checkpoint.sync.error\") {\n        if (snapshot.version < 0) {\n          throw DeltaErrors.checkpointNonExistTable(dataPath)\n        }\n        writeCheckpointFiles(snapshot)\n      }\n    }\n\n  def checkpointAndCleanUpDeltaLog(\n      snapshotToCheckpoint: Snapshot,\n      catalogTableOpt: Option[CatalogTable]): Unit = {\n    val lastCheckpointInfo = writeCheckpointFiles(snapshotToCheckpoint, catalogTableOpt)\n    writeLastCheckpointFile(\n      snapshotToCheckpoint.deltaLog, lastCheckpointInfo, LastCheckpointInfo.checksumEnabled(spark))\n    doLogCleanup(snapshotToCheckpoint, catalogTableOpt)\n  }\n\n  protected[delta] def writeLastCheckpointFile(\n      deltaLog: DeltaLog,\n      lastCheckpointInfo: LastCheckpointInfo,\n      addChecksum: Boolean): Unit = {\n    withCheckpointExceptionHandling(deltaLog, \"delta.lastCheckpoint.write.error\") {\n      val suppressOptionalFields = spark.sessionState.conf.getConf(\n        DeltaSQLConf.SUPPRESS_OPTIONAL_LAST_CHECKPOINT_FIELDS)\n      val lastCheckpointInfoToWrite = lastCheckpointInfo\n      val json = LastCheckpointInfo.serializeToJson(\n        lastCheckpointInfoToWrite,\n        addChecksum,\n        suppressOptionalFields)\n      store.write(LAST_CHECKPOINT, Iterator(json), overwrite = true, newDeltaHadoopConf())\n    }\n  }\n\n  protected def writeCheckpointFiles(\n      snapshotToCheckpoint: Snapshot,\n      catalogTableOpt: Option[CatalogTable] = None): LastCheckpointInfo = {\n    Checkpoints.writeCheckpoint(spark, this, snapshotToCheckpoint, catalogTableOpt)\n  }\n\n  /** Returns information about the most recent checkpoint. */\n  private[delta] def readLastCheckpointFile(): Option[LastCheckpointInfo] = {\n    loadMetadataFromFile(0)\n  }\n\n  /**\n   * Reads the checkpoint metadata from the `_last_checkpoint` file. This method doesn't handle any\n   * exceptions that can be thrown, for example IOExceptions thrown when reading the data such as\n   * FileNotFoundExceptions which is expected for a new Delta table or JSON deserialization errors.\n   */\n  protected def unsafeLoadMetadataFromFile(): LastCheckpointInfo = {\n    val lastCheckpointInfoJson = store.read(LAST_CHECKPOINT, newDeltaHadoopConf())\n    val validate = LastCheckpointInfo.checksumEnabled(spark)\n    LastCheckpointInfo.deserializeFromJson(lastCheckpointInfoJson.head, validate)\n  }\n\n  /** Loads the checkpoint metadata from the _last_checkpoint file. */\n  protected def loadMetadataFromFile(tries: Int): Option[LastCheckpointInfo] =\n    recordDeltaOperation(self, \"delta.deltaLog.loadMetadataFromFile\") {\n      try {\n        Some(unsafeLoadMetadataFromFile())\n      } catch {\n        case _: FileNotFoundException =>\n          None\n        case NonFatal(e) if tries < 3 =>\n          logWarning(log\"Failed to parse ${MDC(DeltaLogKeys.PATH, LAST_CHECKPOINT)}. \" +\n            log\"This may happen if there was an error during read operation, \" +\n            log\"or a file appears to be partial. Sleeping and trying again.\", e)\n          Thread.sleep(1000)\n          loadMetadataFromFile(tries + 1)\n        case NonFatal(e) =>\n          recordDeltaEvent(\n            self,\n            \"delta.lastCheckpoint.read.corruptedJson\",\n            data = Map(\"exception\" -> Utils.exceptionString(e))\n          )\n\n          logWarning(log\"${MDC(DeltaLogKeys.PATH, LAST_CHECKPOINT)} is corrupted. \" +\n            log\"Will search the checkpoint files directly\", e)\n          // Hit a partial file. This could happen on Azure as overwriting _last_checkpoint file is\n          // not atomic. We will try to list all files to find the latest checkpoint and restore\n          // LastCheckpointInfo from it.\n          val verifiedCheckpoint = findLastCompleteCheckpointBefore(checkpointInstance = None)\n          verifiedCheckpoint.map(manuallyLoadCheckpoint)\n      }\n    }\n\n  /** Loads the given checkpoint manually to come up with the [[LastCheckpointInfo]] */\n  protected def manuallyLoadCheckpoint(cv: CheckpointInstance): LastCheckpointInfo = {\n    LastCheckpointInfo(\n      version = cv.version,\n      size = -1,\n      parts = cv.numParts,\n      sizeInBytes = None,\n      numOfAddFiles = None,\n      checkpointSchema = None\n    )\n  }\n\n  /**\n   * Finds the first verified, complete checkpoint before the given version.\n   * Note that the returned checkpoint will always be < `version`.\n   * @param version The checkpoint version to compare against\n   */\n  private[delta] def findLastCompleteCheckpointBefore(version: Long): Option[CheckpointInstance] = {\n    val upperBound = CheckpointInstance(version, CheckpointInstance.Format.SINGLE, numParts = None)\n    findLastCompleteCheckpointBefore(Some(upperBound))\n  }\n\n  /**\n   * Finds the first verified, complete checkpoint before the given [[CheckpointInstance]].\n   * If `checkpointInstance` is passed as None, then we return the last complete checkpoint in the\n   * deltalog directory.\n   * @param checkpointInstance The checkpoint instance to compare against\n   */\n  private[delta] def findLastCompleteCheckpointBefore(\n      checkpointInstance: Option[CheckpointInstance] = None): Option[CheckpointInstance] = {\n    val eventData = mutable.Map[String, String]()\n    val startTimeMs = System.currentTimeMillis()\n    def sendUsageLog(): Unit = {\n      eventData(\"totalTimeTakenMs\") = (System.currentTimeMillis() - startTimeMs).toString\n      recordDeltaEvent(\n        self, opType = \"delta.findLastCompleteCheckpointBefore\", data = eventData.toMap)\n    }\n    try {\n      val resultOpt = findLastCompleteCheckpointBeforeInternal(eventData, checkpointInstance)\n      eventData(\"resultantCheckpointVersion\") = resultOpt.map(_.version).getOrElse(-1L).toString\n      sendUsageLog()\n      resultOpt\n    } catch {\n      case e@(NonFatal(_) | _: InterruptedException | _: java.io.InterruptedIOException |\n              _: java.nio.channels.ClosedByInterruptException) =>\n        eventData(\"exception\") = Utils.exceptionString(e)\n        sendUsageLog()\n        throw e\n    }\n  }\n\n  private def findLastCompleteCheckpointBeforeInternal(\n      eventData: mutable.Map[String, String],\n      checkpointInstance: Option[CheckpointInstance]): Option[CheckpointInstance] = {\n    val upperBoundCv =\n      checkpointInstance\n        // If someone passes the upperBound as 0 or sentinel value, we should not do backward\n        // listing. Instead we should list the entire directory from 0 and return the latest\n        // available checkpoint.\n        .filterNot(cv => cv.version < 0 || cv.version == CheckpointInstance.MaxValue.version)\n        .getOrElse {\n          logInfo(\n            log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] Try to \" +\n            log\"find Delta last complete checkpoint\")\n          eventData(\"listingFromZero\") = true.toString\n          return findLastCompleteCheckpoint()\n        }\n    eventData(\"efficientBackwardListingEnabled\") = true.toString\n    eventData(\"upperBoundVersion\") = upperBoundCv.version.toString\n    eventData(\"upperBoundCheckpointType\") = upperBoundCv.format.name\n    var iterations: Long = 0L\n    var numFilesScanned: Long = 0L\n    logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] \" +\n      log\"Try to find Delta last complete checkpoint before version \" +\n      log\"${MDC(DeltaLogKeys.VERSION, upperBoundCv.version)}\")\n    var listingEndVersion = upperBoundCv.version\n\n    // Do a backward listing from the upperBoundCv version. We list in chunks of 1000 versions.\n    // ...........................................................................................\n    //                                                                        |\n    //                                                               upper bound cv's version\n    //                                          [ iter-1 looks in this window ]\n    //                          [ iter-2 window ]\n    //         [ iter-3 window  ]\n    //              |\n    //        latest checkpoint\n    while (listingEndVersion >= 0) {\n      iterations += 1\n      eventData(\"iterations\") = iterations.toString\n      val listingStartVersion = math.max(0, listingEndVersion - 1000)\n      val checkpoints = store\n        .listFrom(listingPrefix(logPath, listingStartVersion), newDeltaHadoopConf())\n        .map { file => numFilesScanned += 1 ; file }\n        .collect {\n          // Also collect delta files from the listing result so that the next takeWhile helps us\n          // terminate iterator early if no checkpoint exists upto the `listingEndVersion`\n          // version.\n          case DeltaFile(file, version) => (file, FileType.DELTA, version)\n          case CheckpointFile(file, version) => (file, FileType.CHECKPOINT, version)\n        }\n        .takeWhile { case (_, _, currentFileVersion) => currentFileVersion <= listingEndVersion }\n        // Checkpoint files of 0 size are invalid but Spark will ignore them silently when\n        // reading such files, hence we drop them so that we never pick up such checkpoints.\n        .collect { case (file, FileType.CHECKPOINT, _) if file.getLen > 0 =>\n          CheckpointInstance(file.getPath)\n        }\n        // We still need to filter on `upperBoundCv` to eliminate checkpoint files which are\n        // same version as `upperBoundCv` but have higher [[CheckpointInstance.Format]]. e.g.\n        // upperBoundCv is a V2_Checkpoint and we have a Single part checkpoint and a v2\n        // checkpoint at the same version. In such a scenario, we should not consider the\n        // v2 checkpoint as it is nor lower than the upperBoundCv.\n        .filter(_ < upperBoundCv)\n        .toArray\n      val lastCheckpoint =\n        getLatestCompleteCheckpointFromList(checkpoints, Some(upperBoundCv.version))\n      eventData(\"numFilesScanned\") = numFilesScanned.toString\n      if (lastCheckpoint.isDefined) {\n        logInfo(\n          log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] Delta \" +\n          log\"checkpoint is found at version \" +\n          log\"${MDC(DeltaLogKeys.VERSION, lastCheckpoint.get.version)}\")\n        return lastCheckpoint\n      }\n      listingEndVersion = listingEndVersion - 1000\n    }\n    logInfo(\n      log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] No checkpoint \" +\n      log\"found for Delta table before version ${MDC(DeltaLogKeys.VERSION, upperBoundCv.version)}\")\n    None\n  }\n\n  /** Returns whether a checkpoint exists at `version`. */\n  def checkpointExistsAtVersion(version: Long): Boolean = {\n    val upperBoundVersion = Some(CheckpointInstance(version = version + 1))\n    val lastVerifiedCheckpoint = findLastCompleteCheckpointBefore(upperBoundVersion)\n    lastVerifiedCheckpoint.exists(_.version == version)\n  }\n\n  /** Returns the last complete checkpoint in the delta log directory (if any) */\n  private def findLastCompleteCheckpoint(): Option[CheckpointInstance] = {\n    val hadoopConf = newDeltaHadoopConf()\n    val listingResult = store\n      .listFrom(listingPrefix(logPath, 0L), hadoopConf)\n      // Checkpoint files of 0 size are invalid but Spark will ignore them silently when\n      // reading such files, hence we drop them so that we never pick up such checkpoints.\n      .collect { case CheckpointFile(file, _) if file.getLen != 0 => file }\n    new DeltaLogGroupingIterator(listingResult)\n      .flatMap { case (_, files) =>\n        getLatestCompleteCheckpointFromList(files.map(f => CheckpointInstance(f.getPath)).toArray)\n      }.foldLeft(Option.empty[CheckpointInstance])((_, right) => Some(right))\n    // ^The foldLeft here emulates the non-existing Iterator.tailOption method.\n  }\n\n  /**\n   * Given a list of checkpoint files, pick the latest complete checkpoint instance which is not\n   * later than `notLaterThan`.\n   */\n  protected[delta] def getLatestCompleteCheckpointFromList(\n      instances: Array[CheckpointInstance],\n      notLaterThanVersion: Option[Long] = None): Option[CheckpointInstance] = {\n    val sentinelCv = CheckpointInstance.sentinelValue(notLaterThanVersion)\n    val complete = instances.filter(_ <= sentinelCv).groupBy(identity).filter {\n      case (ci, matchingCheckpointInstances) =>\n       ci.format match {\n         case CheckpointInstance.Format.SINGLE =>\n           matchingCheckpointInstances.length == 1\n         case CheckpointInstance.Format.WITH_PARTS =>\n           assert(ci.numParts.nonEmpty, \"Multi-Part Checkpoint must have non empty numParts\")\n           matchingCheckpointInstances.length == ci.numParts.get\n         case CheckpointInstance.Format.V2 =>\n           matchingCheckpointInstances.length == 1\n         case CheckpointInstance.Format.SENTINEL =>\n           false\n       }\n    }\n    if (complete.isEmpty) None else Some(complete.keys.max)\n  }\n}\n\nobject Checkpoints\n  extends DeltaLogging\n  {\n\n  /** The name of the last checkpoint file */\n  val LAST_CHECKPOINT_FILE_NAME = \"_last_checkpoint\"\n\n  /**\n   * Determines the V2 checkpoint format to use for the given snapshot, if applicable.\n   *\n   * This method evaluates whether V2 checkpoints should be used based on the table's\n   * checkpoint policy and configuration settings. It performs the following checks:\n   *\n   * 1. Force Classic Checkpoint Check (Edge): If the Spark configuration\n   *    [[DeltaSQLConf.FORCE_CLASSIC_CHECKPOINT]] is set to true (typically due to\n   *    a file action count mismatch), this method returns None to force the use\n   *    of classic checkpoints.\n   *\n   * 2. V2 Checkpoint Policy Check: Examines the table's checkpoint policy from\n   *    the snapshot metadata to determine if V2 checkpoint support is required.\n   *\n   * 3. Format Selection: If V2 checkpoints are enabled, determines the format\n   *    for the top-level checkpoint file based on the\n   *    [[DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT]] configuration:\n   *    - JSON format (default if not specified)\n   *    - PARQUET format\n   *\n   * @param spark The SparkSession to retrieve configuration settings\n   * @param snapshot The snapshot for which to determine the checkpoint format\n   * @return Some(V2Checkpoint.Format) if V2 checkpoints should be used with the\n   *         specified format (JSON or PARQUET), or None if classic checkpoints\n   *         should be used\n   * @throws IllegalStateException if an unknown checkpoint format is specified\n   *         in the configuration\n   */\n  def getV2CheckpointFormatOpt(\n      spark: SparkSession,\n      snapshot: Snapshot): Option[V2Checkpoint.Format] = {\n    val policy = DeltaConfigs.CHECKPOINT_POLICY.fromMetaData(snapshot.metadata)\n    if (policy.needsV2CheckpointSupport) {\n      assert(CheckpointProvider.isV2CheckpointEnabled(snapshot))\n      val v2Format = spark.conf.getOption(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key)\n      // The format of the top level file in V2 checkpoints can be configured through\n      // the optional config [[DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT]].\n      // If nothing is specified, we use the json format. In the future, we may\n      // write json/parquet dynamically based on heuristics.\n      v2Format match {\n        case Some(V2Checkpoint.Format.JSON.name) | None => Some(V2Checkpoint.Format.JSON)\n        case Some(V2Checkpoint.Format.PARQUET.name) => Some(V2Checkpoint.Format.PARQUET)\n        case _ => throw new IllegalStateException(\"unknown checkpoint format\")\n      }\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Returns the checkpoint schema that should be written to the last checkpoint file based on\n   * [[DeltaSQLConf.CHECKPOINT_SCHEMA_WRITE_THRESHOLD_LENGTH]] conf.\n   */\n  private[delta] def checkpointSchemaToWriteInLastCheckpointFile(\n      spark: SparkSession,\n      schema: StructType): Option[StructType] = {\n    val checkpointSchemaSizeThreshold = spark.sessionState.conf.getConf(\n      DeltaSQLConf.CHECKPOINT_SCHEMA_WRITE_THRESHOLD_LENGTH)\n    Some(schema).filter(s => JsonUtils.toJson(s).length <= checkpointSchemaSizeThreshold)\n  }\n\n  /**\n   * Writes out the contents of a [[Snapshot]] into a checkpoint file that\n   * can be used to short-circuit future replays of the log.\n   *\n   * Returns the checkpoint metadata to be committed to a file. We will use the value\n   * in this file as the source of truth of the last valid checkpoint.\n   */\n  private[delta] def writeCheckpoint(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      snapshot: Snapshot,\n      catalogTableOpt: Option[CatalogTable]): LastCheckpointInfo = recordFrameProfile(\n      \"Delta\", \"Checkpoints.writeCheckpoint\") {\n    if (spark.conf.get(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED)) {\n      snapshot.validateChecksum(Map(\"context\" -> \"writeCheckpoint\"))\n    }\n    // Verify allFiles in checksum during checkpoint if we are not doing so already on every\n    // commit.\n    val allFilesInCRCEnabled = Snapshot.allFilesInCrcWritePathEnabled(spark, snapshot)\n    val shouldVerifyAllFilesInCRCEveryCommit =\n      Snapshot.allFilesInCrcVerificationEnabled(spark, snapshot)\n    if (allFilesInCRCEnabled && !shouldVerifyAllFilesInCRCEveryCommit) {\n      snapshot.checksumOpt.foreach { checksum =>\n        snapshot.validateFileListAgainstCRC(\n          checksum, contextOpt = Some(\"triggeredFromCheckpoint\"))\n      }\n    }\n\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n\n    // The writing of checkpoints doesn't go through log store, so we need to check with the\n    // log store and decide whether to use rename.\n    val useRename = deltaLog.store.isPartialWriteVisible(deltaLog.logPath, hadoopConf)\n\n    val v2CheckpointFormatOpt = getV2CheckpointFormatOpt(spark, snapshot)\n    val v2CheckpointEnabled = v2CheckpointFormatOpt.nonEmpty\n    if (!v2CheckpointEnabled) {\n      // Ensures that commit files are backfilled for Catalog-Managed (CC) tables when\n      // writing Classic checkpoints.\n      //\n      // For CC tables with Classic checkpoint format (V2 checkpoint disabled), this method\n      // ensures that commit files are *synchronously* backfilled from staged commits to the\n      // _delta_log directory before writing the checkpoint. This prevents gaps in the\n      // directory structure that could cause issues for readers not communicating with\n      // the commit coordinator.\n      //\n      // Without backfilling, the directory structure might have gaps like:\n      // {{{\n      // _delta_log/\n      //   _staged_commits/\n      //     00017.$uuid.json\n      //     00018.$uuid.json\n      //   00015.json\n      //   00016.json\n      //   00018.checkpoint.parquet  // Gap: missing 00017.json\n      // }}}\n      snapshot.ensureCommitFilesBackfilled(catalogTableOpt)\n    }\n\n    val checkpointRowCount = spark.sparkContext.longAccumulator(\"checkpointRowCount\")\n    val numOfFiles = spark.sparkContext.longAccumulator(\"numOfFiles\")\n\n    val sessionConf = spark.sessionState.conf\n    val checkpointPartSize =\n        sessionConf.getConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE)\n\n    val numParts = checkpointPartSize.map { partSize =>\n      math.ceil((snapshot.numOfFiles + snapshot.numOfRemoves).toDouble / partSize).toLong\n    }.getOrElse(1L).toInt\n    val legacyMultiPartCheckpoint = !v2CheckpointEnabled && numParts > 1\n\n    val base = {\n      val repartitioned = snapshot.stateDS\n        .repartition(numParts, coalesce(col(\"add.path\"), col(\"remove.path\")))\n        .map { action =>\n          if (action.add != null) {\n            numOfFiles.add(1)\n          }\n          action\n        }\n      // commitInfo, cdc and remove.tags are not included in both classic and V2 checkpoints.\n      if (v2CheckpointEnabled) {\n        // When V2 Checkpoint is enabled, the baseCheckpoint refers to the sidecar files which will\n        // only have AddFile and RemoveFile actions. The other non-file actions will be written\n        // separately after sidecar files are written.\n        repartitioned\n          .select(\"add\", \"remove\")\n          .withColumn(\"remove\", col(\"remove\").dropFields(\"tags\", \"stats\"))\n          .where(\"add is not null or remove is not null\")\n      } else {\n        // When V2 Checkpoint is disabled, the baseCheckpoint refers to the main classic checkpoint\n        // which has all actions except \"commitInfo\", \"cdc\", \"checkpointMetadata\", \"sidecar\".\n        repartitioned\n          .drop(\"commitInfo\", \"cdc\", \"checkpointMetadata\", \"sidecar\")\n          .withColumn(\"remove\", col(\"remove\").dropFields(\"tags\", \"stats\"))\n      }\n    }\n\n    val chk = buildCheckpoint(base, snapshot)\n    val schema = chk.schema.asNullable\n\n    val (factory, serConf) = {\n      val format = new ParquetFileFormat()\n      val job = Job.getInstance(hadoopConf)\n      // Right now, we don't shred variant stats in checkpoints.\n      val writeOptions = VariantShreddingShims.getVariantInferShreddingSchemaOptions(false)\n      (format.prepareWrite(spark, job, Map.empty ++ writeOptions, schema),\n        new SerializableConfiguration(job.getConfiguration))\n    }\n\n    // Use the SparkPath in the closure as Path is not Serializable.\n    val logSparkPath = SparkPath.fromPath(snapshot.path)\n    val version = snapshot.version\n\n    // This is a hack to get spark to write directly to a file.\n    val qe = chk.queryExecution\n    def executeFinalCheckpointFiles(): Array[SerializableFileStatus] = qe\n      .executedPlan\n      .execute()\n      .mapPartitions { case iter =>\n        val actualNumParts = Option(TaskContext.get()).map(_.numPartitions())\n          .getOrElse(numParts)\n        val partition = TaskContext.getPartitionId()\n        val (writtenPath, finalPath) = Checkpoints.getCheckpointWritePath(\n          serConf.value,\n          logSparkPath.toPath,\n          version,\n          actualNumParts,\n          partition,\n          useRename,\n          v2CheckpointEnabled)\n        val fs = writtenPath.getFileSystem(serConf.value)\n        val writeAction = () => {\n          try {\n            val writer = factory.newInstance(\n              writtenPath.toString,\n              schema,\n              new TaskAttemptContextImpl(\n                new JobConf(serConf.value),\n                new TaskAttemptID(\"\", 0, TaskType.REDUCE, 0, 0)))\n\n            iter.foreach { row =>\n              checkpointRowCount.add(1)\n              writer.write(row)\n            }\n            // Note: `writer.close()` is not put in a `finally` clause because we don't want to\n            // close it when an exception happens. Closing the file would flush the content to the\n            // storage and create an incomplete file. A concurrent reader might see it and fail.\n            // This would leak resources but we don't have a way to abort the storage request here.\n            writer.close()\n          } catch {\n            case e: org.apache.hadoop.fs.FileAlreadyExistsException if !useRename =>\n              if (fs.exists(writtenPath)) {\n                // The file has been written by a zombie task. We can just use this checkpoint file\n                // rather than failing a Delta commit.\n              } else {\n                throw e\n              }\n          }\n        }\n        if (isGCSPath(serConf.value, writtenPath)) {\n          // GCS may upload an incomplete file when the current thread is interrupted, hence we move\n          // the write to a new thread so that the write cannot be interrupted.\n          // TODO Remove this hack when the GCS Hadoop connector fixes the issue.\n          DeltaFileOperations.runInNewThread(\"delta-gcs-checkpoint-write\") {\n            writeAction()\n          }\n        } else {\n          writeAction()\n        }\n        if (useRename) {\n          renameAndCleanupTempPartFile(writtenPath, finalPath, fs)\n        }\n        val finalPathFileStatus = try {\n          fs.getFileStatus(finalPath)\n        } catch {\n          case _: FileNotFoundException if useRename =>\n            throw DeltaErrors.failOnCheckpointRename(writtenPath, finalPath)\n        }\n\n        Iterator(SerializableFileStatus.fromStatus(finalPathFileStatus))\n      }.collect()\n\n    val finalCheckpointFiles = SQLExecution.withNewExecutionId(qe, Some(\"Delta checkpoint\")) {\n      executeFinalCheckpointFiles()\n    }\n\n    if (numOfFiles.value != snapshot.numOfFiles) {\n      throw DeltaErrors.checkpointMismatchWithSnapshot\n    }\n\n    val parquetFilesSizeInBytes = finalCheckpointFiles.map(_.length).sum\n    var overallCheckpointSizeInBytes = parquetFilesSizeInBytes\n    var overallNumCheckpointActions: Long = checkpointRowCount.value\n    var checkpointSchemaToWriteInLastCheckpoint: Option[StructType] =\n      Checkpoints.checkpointSchemaToWriteInLastCheckpointFile(spark, schema)\n\n    val v2Checkpoint = if (v2CheckpointEnabled) {\n      // For CC tables, ensure commit files are backfilled right before publishing the\n      // V2 checkpoint manifest.\n      // At this moment, any existing async commit backfill operations almost certainly\n      // would have completed as the full state reconstruction usually takes longer than\n      // commit backfilling.\n      snapshot.ensureCommitFilesBackfilled(catalogTableOpt)\n      val (v2CheckpointFileStatus, nonFileActionsWriten, v2Checkpoint, checkpointSchema) =\n        Checkpoints.writeTopLevelV2Checkpoint(\n          v2CheckpointFormatOpt.get,\n          finalCheckpointFiles,\n          spark,\n          schema,\n          snapshot,\n          deltaLog,\n          overallNumCheckpointActions,\n          parquetFilesSizeInBytes,\n          hadoopConf,\n          useRename\n        )\n      overallCheckpointSizeInBytes += v2CheckpointFileStatus.getLen\n      overallNumCheckpointActions += nonFileActionsWriten.size\n      checkpointSchemaToWriteInLastCheckpoint = checkpointSchema\n\n      Some(v2Checkpoint)\n    } else {\n      None\n    }\n\n    if (!v2CheckpointEnabled && checkpointRowCount.value == 0) {\n      // In case of V2 Checkpoints, zero row count is possible.\n      logWarning(DeltaErrors.EmptyCheckpointErrorMessage)\n    }\n\n    // If we don't parallelize, we use None for backwards compatibility\n    val checkpointParts = if (legacyMultiPartCheckpoint) Some(numParts) else None\n\n    LastCheckpointInfo(\n      version = snapshot.version,\n      size = overallNumCheckpointActions,\n      parts = checkpointParts,\n      sizeInBytes = Some(overallCheckpointSizeInBytes),\n      numOfAddFiles = Some(snapshot.numOfFiles),\n      v2Checkpoint = v2Checkpoint,\n      checkpointSchema = checkpointSchemaToWriteInLastCheckpoint\n    )\n  }\n\n  /**\n   * Generate a tuple of the file to write the checkpoint and where it may later need\n   * to be copied. Should be used within a task, so that task or stage retries don't\n   * create the same files.\n   */\n  def getCheckpointWritePath(\n      conf: Configuration,\n      logPath: Path,\n      version: Long,\n      numParts: Int,\n      part: Int,\n      useRename: Boolean,\n      v2CheckpointEnabled: Boolean): (Path, Path) = {\n    def getCheckpointWritePath(path: Path): Path = {\n      if (useRename) {\n        val tempPath =\n          new Path(path.getParent, s\".${path.getName}.${UUID.randomUUID}.tmp\")\n        DeltaFileOperations.registerTempFileDeletionTaskFailureListener(conf, tempPath)\n        tempPath\n      } else {\n        path\n      }\n    }\n    val destinationName: Path = if (v2CheckpointEnabled) {\n      newV2CheckpointSidecarFile(logPath, version, numParts, part + 1)\n    } else {\n      if (numParts > 1) {\n        assert(part < numParts, s\"Asked to create part: $part of max $numParts in checkpoint.\")\n        checkpointFileWithParts(logPath, version, numParts)(part)\n      } else {\n        checkpointFileSingular(logPath, version)\n      }\n    }\n\n    getCheckpointWritePath(destinationName) -> destinationName\n  }\n\n  /**\n   * Writes a top-level V2 Checkpoint file which may point to multiple\n   * sidecar files.\n   *\n   * @param v2CheckpointFormat The format in which the top-level file should be\n   *                           written. Currently, json and parquet are supported.\n   * @param sidecarCheckpointFiles The list of sidecar files that have already been\n   *                               written. The top-level file will store this list.\n   * @param spark The current spark session\n   * @param sidecarSchema The schema of the sidecar parquet files.\n   * @param snapshot The snapshot for which the checkpoint is being written.\n   * @param deltaLog The deltaLog instance pointing to our tables deltaLog.\n   * @param rowsWrittenInCheckpointJob The number of rows that were written in total\n   *                                   to the sidecar files.\n   * @param parquetFilesSizeInBytes The combined size of all sidecar files in bytes.\n   * @param hadoopConf The hadoopConf to use for the filesystem operation.\n   * @param useRename Whether we should first write to a temporary file and then\n   *                  rename it to the target file name during the write.\n   * @return A tuple containing\n   *          1. [[FileStatus]] of the newly created top-level V2Checkpoint.\n   *          2. The sequence of actions that were written to the top-level file.\n   *          3. An instance of the LastCheckpointV2 containing V2-checkpoint related\n   *           metadata which can later be written to LAST_CHECKPOINT\n   *          4. Schema of the newly written top-level file (only for parquet files)\n   */\n  protected[delta] def writeTopLevelV2Checkpoint(\n      v2CheckpointFormat: V2Checkpoint.Format,\n      sidecarCheckpointFiles: Array[SerializableFileStatus],\n      spark: SparkSession,\n      sidecarSchema: StructType,\n      snapshot: Snapshot,\n      deltaLog: DeltaLog,\n      rowsWrittenInCheckpointJob: Long,\n      parquetFilesSizeInBytes: Long,\n      hadoopConf: Configuration,\n      useRename: Boolean) : (FileStatus, Seq[Action], LastCheckpointV2, Option[StructType]) = {\n    // Write the main v2 checkpoint file.\n    val sidecarFilesWritten = sidecarCheckpointFiles.map(SidecarFile(_)).toSeq\n    // Filter out the sidecar schema if it is too large.\n    val sidecarFileSchemaOpt =\n      Checkpoints.checkpointSchemaToWriteInLastCheckpointFile(spark, sidecarSchema)\n    val checkpointMetadata = CheckpointMetadata(snapshot.version)\n\n    val nonFileActionsToWrite =\n      (checkpointMetadata +: sidecarFilesWritten) ++ snapshot.nonFileActions\n    val (v2CheckpointPath, checkpointSchemaToWriteInLastCheckpoint) =\n      if (v2CheckpointFormat == V2Checkpoint.Format.JSON) {\n        val v2CheckpointPath = newV2CheckpointJsonFile(deltaLog.logPath, snapshot.version)\n        // We don't need a putIfAbsent for this write, so we set overwrite to true.\n        // However, this can be dangerous if the cloud makes partial writes visible.\n        val isPartialWriteVisible =\n          deltaLog.store.isPartialWriteVisible(v2CheckpointPath, hadoopConf)\n        deltaLog.store.write(\n          v2CheckpointPath,\n          nonFileActionsToWrite.map(_.json).toIterator,\n          overwrite = !isPartialWriteVisible,\n          hadoopConf = hadoopConf\n        )\n        (v2CheckpointPath, None)\n      } else if (v2CheckpointFormat == V2Checkpoint.Format.PARQUET) {\n        val sparkSession = spark\n        // scalastyle:off sparkimplicits\n        import sparkSession.implicits._\n        // scalastyle:on sparkimplicits\n        val dfToWrite = nonFileActionsToWrite.map(_.wrap).toDF()\n        val v2CheckpointPath = newV2CheckpointParquetFile(deltaLog.logPath, snapshot.version)\n        val schemaOfDfWritten = createCheckpointV2ParquetFile(\n          spark, dfToWrite, v2CheckpointPath, hadoopConf, useRename)\n        (v2CheckpointPath, Some(schemaOfDfWritten))\n      } else {\n        throw DeltaErrors.assertionFailedError(\n          s\"Unrecognized checkpoint V2 format: $v2CheckpointFormat\")\n      }\n    // Main Checkpoint V2 File written successfully. Now create the last checkpoint v2 blob so\n    // that we can persist it in _last_checkpoint file.\n    val v2CheckpointFileStatus =\n      v2CheckpointPath.getFileSystem(hadoopConf).getFileStatus(v2CheckpointPath)\n    val unfilteredV2Checkpoint = LastCheckpointV2(\n      fileStatus = v2CheckpointFileStatus,\n      nonFileActions = Some((snapshot.nonFileActions :+ checkpointMetadata).map(_.wrap)),\n      sidecarFiles = Some(sidecarFilesWritten)\n    )\n    (\n      v2CheckpointFileStatus,\n      nonFileActionsToWrite,\n      trimLastCheckpointV2(unfilteredV2Checkpoint, spark),\n      checkpointSchemaToWriteInLastCheckpoint\n    )\n  }\n\n  /**\n   * Helper method to create a V2 Checkpoint parquet file or the V2 Checkpoint Compat file.\n   * V2 Checkpoint Compat files follow the same naming convention as classic checkpoints\n   * and they are needed so that V2Checkpoint-unaware readers can read them to understand\n   * that they don't have the capability to read table for which they were created.\n   * This is needed in cases where commit 0 has been cleaned up and the reader needs to\n   * read a checkpoint to read the [[Protocol]].\n   */\n  def createCheckpointV2ParquetFile(\n      spark: SparkSession,\n      ds: Dataset[Row],\n      finalPath: Path,\n      hadoopConf: Configuration,\n      useRename: Boolean): StructType = recordFrameProfile(\n        \"Checkpoints\", \"createCheckpointV2ParquetFile\") {\n    val df = ds.select(\n      \"txn\", \"add\", \"remove\", \"metaData\", \"protocol\", \"domainMetadata\",\n      \"checkpointMetadata\", \"sidecar\")\n    val schema = df.schema.asNullable\n    val format = new ParquetFileFormat()\n    val job = Job.getInstance(hadoopConf)\n    val factory = format.prepareWrite(spark, job, Map.empty, schema)\n    val serConf = new SerializableConfiguration(job.getConfiguration)\n    val finalSparkPath = SparkPath.fromPath(finalPath)\n\n    df.repartition(1)\n      .queryExecution\n      .executedPlan\n      .execute()\n      .mapPartitions { iter =>\n        val actualNumParts = Option(TaskContext.get()).map(_.numPartitions()).getOrElse(1)\n        require(actualNumParts == 1, \"The parquet V2 checkpoint must be written in 1 file\")\n        val partition = TaskContext.getPartitionId()\n        val finalPath = finalSparkPath.toPath\n        val writePath = if (useRename) {\n          val tempPath =\n            new Path(finalPath.getParent, s\".${finalPath.getName}.${UUID.randomUUID}.tmp\")\n          DeltaFileOperations.registerTempFileDeletionTaskFailureListener(serConf.value, tempPath)\n          tempPath\n        } else {\n          finalPath\n        }\n\n        val fs = writePath.getFileSystem(serConf.value)\n\n        val attemptId = 0\n        val taskAttemptContext = new TaskAttemptContextImpl(\n          new JobConf(serConf.value),\n          new TaskAttemptID(\"\", 0, TaskType.REDUCE, partition, attemptId))\n\n        var writerOpt: Option[OutputWriter] = None\n\n        try {\n          writerOpt = Some(factory.newInstance(\n            writePath.toString,\n            schema,\n            taskAttemptContext))\n\n          val writer = writerOpt.get\n          iter.foreach { row =>\n            writer.write(row)\n          }\n          // Note: `writer.close()` is not put in a `finally` clause because we don't want to\n          // close it when an exception happens. Closing the file would flush the content to the\n          // storage and create an incomplete file. A concurrent reader might see it and fail.\n          // This would leak resources but we don't have a way to abort the storage request here.\n          writer.close()\n        } catch {\n          case _: org.apache.hadoop.fs.FileAlreadyExistsException\n            if !useRename && fs.exists(writePath) =>\n          // The file has been written by a zombie task. We can just use this checkpoint file\n          // rather than failing a Delta commit.\n          case t: Throwable =>\n            throw t\n        }\n        if (useRename) {\n          renameAndCleanupTempPartFile(writePath, finalPath, fs)\n        }\n        val finalPathFileStatus = try {\n          fs.getFileStatus(finalPath)\n        } catch {\n          case _: FileNotFoundException if useRename =>\n            throw DeltaErrors.failOnCheckpointRename(writePath, finalPath)\n        }\n        Iterator(SerializableFileStatus.fromStatus(finalPathFileStatus))\n      }.collect()\n    schema\n  }\n\n  /** Bounds the size of a [[LastCheckpointV2]] by removing any oversized optional fields */\n  def trimLastCheckpointV2(\n      lastCheckpointV2: LastCheckpointV2,\n      spark: SparkSession): LastCheckpointV2 = {\n    val nonFileActionThreshold =\n      spark.sessionState.conf.getConf(DeltaSQLConf.LAST_CHECKPOINT_NON_FILE_ACTIONS_THRESHOLD)\n    val sidecarThreshold =\n      spark.sessionState.conf.getConf(DeltaSQLConf.LAST_CHECKPOINT_SIDECARS_THRESHOLD)\n    lastCheckpointV2.copy(\n      sidecarFiles = lastCheckpointV2.sidecarFiles.filter(_.size <= sidecarThreshold),\n      nonFileActions = lastCheckpointV2.nonFileActions.filter(_.size <= nonFileActionThreshold))\n  }\n\n  /**\n   * Helper method to rename a `tempPath` checkpoint part file to `finalPath` checkpoint part file.\n   * This also tries to handle any race conditions with Zombie tasks.\n   */\n  private[delta] def renameAndCleanupTempPartFile(\n      tempPath: Path, finalPath: Path, fs: FileSystem): Unit = {\n    // If rename fails because the final path already exists, it's ok -- some zombie\n    // task probably got there first.\n    // We rely on the fact that all checkpoint writers write the same content to any given\n    // checkpoint part file. So it shouldn't matter which writer wins the race.\n    val renameSuccessful = try {\n      // Note that the fs.exists check here is redundant as fs.rename should fail if destination\n      // file already exists as per File System spec. But the LocalFS doesn't follow this and it\n      // overrides the final path even if it already exists. So we use exists here to handle that\n      // case.\n      // TODO: Remove isTesting and fs.exists check after fixing LocalFS\n      if (DeltaUtils.isTesting && fs.exists(finalPath)) {\n        false\n      } else {\n        fs.rename(tempPath, finalPath)\n      }\n    } catch {\n      case _: org.apache.hadoop.fs.FileAlreadyExistsException => false\n    }\n    if (!renameSuccessful) {\n      try {\n        fs.delete(tempPath, false)\n      } catch { case NonFatal(e) =>\n        logWarning(log\"Error while deleting the temporary checkpoint part file \" +\n          log\"${MDC(DeltaLogKeys.PATH, tempPath)}\", e)\n      }\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  /**\n   * All GCS paths can only have the scheme of \"gs\". Note: the scheme checking is case insensitive.\n   * See:\n   * - https://github.com/databricks/hadoop-connectors/blob/master/gcs/src/main/java/com/google/cloud/hadoop/fs/gcs/GoogleHadoopFileSystemBase.java#L493\n   * - https://github.com/GoogleCloudDataproc/hadoop-connectors/blob/v2.2.3/gcsio/src/main/java/com/google/cloud/hadoop/gcsio/GoogleCloudStorageFileSystem.java#L88\n   */\n  // scalastyle:on line.size.limit\n  private[delta] def isGCSPath(hadoopConf: Configuration, path: Path): Boolean = {\n    val scheme = path.toUri.getScheme\n    if (scheme != null) {\n      scheme.equalsIgnoreCase(\"gs\")\n    } else {\n      // When the schema is not available in the path, we check the file system scheme resolved from\n      // the path.\n      path.getFileSystem(hadoopConf).getScheme.equalsIgnoreCase(\"gs\")\n    }\n  }\n\n  /**\n   * Modify the contents of the add column based on the table properties\n   */\n  private[delta] def buildCheckpoint(state: DataFrame, snapshot: Snapshot): DataFrame = {\n    val additionalCols = new mutable.ArrayBuffer[Column]()\n    val sessionConf = state.sparkSession.sessionState.conf\n    if (Checkpoints.shouldWriteStatsAsJson(snapshot)) {\n      additionalCols += col(\"add.stats\").as(\"stats\")\n    }\n    // We provide fine grained control using the session conf for now, until users explicitly\n    // opt in our out of the struct conf.\n    val includeStructColumns = shouldWriteStatsAsStruct(sessionConf, snapshot)\n    if (includeStructColumns) {\n      val partitionValues = Checkpoints.extractPartitionValues(\n        snapshot.metadata.partitionSchema, \"add.partitionValues\")\n      additionalCols ++= partitionValues\n      additionalCols ++= Checkpoints.extractStats(snapshot.statsSchema, \"add.stats\")\n    }\n    state.withColumn(\"add\",\n      when(col(\"add\").isNotNull, struct(Seq(\n        col(\"add.path\"),\n        col(\"add.partitionValues\"),\n        col(\"add.size\"),\n        col(\"add.modificationTime\"),\n        col(\"add.dataChange\"), // actually not really useful here\n        col(\"add.tags\"),\n        col(\"add.deletionVector\"),\n        col(\"add.baseRowId\"),\n        col(\"add.defaultRowCommitVersion\"),\n        col(\"add.clusteringProvider\")) ++\n        additionalCols: _*\n      ))\n    )\n  }\n\n  def shouldWriteStatsAsStruct(conf: SQLConf, snapshot: Snapshot): Boolean = {\n    DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.fromMetaData(snapshot.metadata) &&\n      !conf.getConf(DeltaSQLConf.STATS_AS_STRUCT_IN_CHECKPOINT_FORCE_DISABLED).getOrElse(false)\n  }\n\n  def shouldWriteStatsAsJson(snapshot: Snapshot): Boolean = {\n    DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_JSON.fromMetaData(snapshot.metadata)\n  }\n\n  val STRUCT_PARTITIONS_COL_NAME = \"partitionValues_parsed\"\n  val STRUCT_STATS_COL_NAME = \"stats_parsed\"\n\n  /**\n   * Creates a nested struct column of partition values that extract the partition values\n   * from the original MapType.\n   */\n  def extractPartitionValues(partitionSchema: StructType, partitionValuesColName: String):\n      Option[Column] = {\n    val partitionValues = partitionSchema.map { field =>\n      val physicalName = DeltaColumnMapping.getPhysicalName(field)\n      val attribute = UnresolvedAttribute.quotedString(partitionValuesColName)\n      Column(Cast(\n        ElementAt(\n          attribute,\n          Literal(physicalName),\n          failOnError = false),\n        field.dataType,\n        ansiEnabled = false)\n      ).as(physicalName)\n    }\n    if (partitionValues.isEmpty) {\n      None\n    } else Some(struct(partitionValues: _*).as(STRUCT_PARTITIONS_COL_NAME))\n  }\n  // This method can be overridden in tests to create a checkpoint with parsed stats.\n  def includeStatsParsedInCheckpoint(): Boolean = true\n\n  /** Parse the stats from JSON and keep as a struct field when available. */\n  def extractStats(statsSchema: StructType, statsColName: String): Option[Column] = {\n    import org.apache.spark.sql.functions.from_json\n    Option.when(includeStatsParsedInCheckpoint() && statsSchema.nonEmpty) {\n      val parsedStats = from_json(col(statsColName), statsSchema,\n        DeltaFileProviderUtils.jsonStatsParseOption)\n      // If schema contains variant types, decode Z85-encoded strings to actual Variant values.\n      // In JSON stats, variant values are stored as Z85-encoded strings. from_json creates\n      // Variant objects containing those strings. DecodeNestedZ85EncodedVariant decodes them\n      // to proper binary Variant representation.\n      val decodedStats = if (SchemaUtils.checkForVariantTypeColumnsRecursively(statsSchema)) {\n        Column(DecodeNestedZ85EncodedVariant(parsedStats.expr))\n      } else {\n        parsedStats\n      }\n      decodedStats.as(Checkpoints.STRUCT_STATS_COL_NAME)\n    }\n  }\n}\n\nobject V2Checkpoint {\n  /** Format for V2 Checkpoints */\n  sealed abstract class Format(val name: String) {\n    def fileFormat: FileFormat\n  }\n\n  def toFormat(fileName: String): Format = fileName match {\n    case _ if fileName.endsWith(Format.JSON.name) => Format.JSON\n    case _ if fileName.endsWith(Format.PARQUET.name) => Format.PARQUET\n    case _ => throw new IllegalStateException(s\"Unknown v2 checkpoint file format: ${fileName}\")\n  }\n\n  object Format {\n    /** json v2 checkpoint */\n    object JSON extends Format(\"json\") {\n      override def fileFormat: FileFormat = DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_JSON\n    }\n\n    /** parquet v2 checkpoint */\n    object PARQUET extends Format(\"parquet\") {\n      override def fileFormat: FileFormat = DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_PARQUET\n    }\n\n    /** All valid formats for the top level file of v2 checkpoints. */\n    val ALL: Set[Format] = Set(Format.JSON, Format.PARQUET)\n\n    /** The string representations of all the valid formats. */\n    val ALL_AS_STRINGS: Set[String] = ALL.map(_.name)\n  }\n}\n\nobject CheckpointPolicy {\n\n  sealed abstract class Policy(val name: String) {\n    override def toString: String = name\n    def needsV2CheckpointSupport: Boolean = true\n  }\n\n  /**\n   * Write classic single file/multi-part checkpoints when this policy is enabled.\n   * Note that [[V2CheckpointTableFeature]] is not required for this checkpoint policy.\n   */\n  case object Classic extends Policy(\"classic\") {\n    override def needsV2CheckpointSupport: Boolean = false\n  }\n\n  /**\n   * Write V2 checkpoints when this policy is enabled.\n   * This needs [[V2CheckpointTableFeature]] to be enabled on the table.\n   */\n  case object V2 extends Policy(\"v2\")\n\n  /** ALl checkpoint policies */\n  val ALL: Seq[Policy] = Seq(Classic, V2)\n\n  /** Converts a `name` String into a [[Policy]] */\n  def fromName(name: String): Policy = ALL.find(_.name == name).getOrElse {\n    throw new IllegalArgumentException(s\"Invalid policy $name\")\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/Checksum.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.FileNotFoundException\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.TimeZone\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.immutable.ListMap\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeletedRecordCountsHistogram\nimport org.apache.spark.sql.delta.stats.FileSizeHistogram\nimport org.apache.spark.sql.delta.storage.LogStore\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport com.fasterxml.jackson.annotation.JsonAlias\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.util.{SerializableConfiguration, Utils}\n\n/**\n * Stats calculated within a snapshot, which we store along individual transactions for\n * verification.\n *\n * @param txnId Optional transaction identifier\n * @param tableSizeBytes The size of the table in bytes\n * @param numFiles Number of `AddFile` actions in the snapshot\n * @param numDeletedRecordsOpt  The number of deleted records with Deletion Vectors.\n * @param numDeletionVectorsOpt The number of Deletion Vectors present in the snapshot.\n * @param numMetadata Number of `Metadata` actions in the snapshot\n * @param numProtocol Number of `Protocol` actions in the snapshot\n * @param histogramOpt Optional file size histogram. Note: the Delta spec field name is\n *                     `fileSizeHistogram` (used by Kernel/Java/Rust). Delta-Spark historically\n *                     wrote `histogramOpt`. The `@JsonAlias` allows reading both field names so\n *                     that CRC files written by either Kernel or Delta-Spark are compatible.\n * @param deletedRecordCountsHistogramOpt A histogram of the deleted records count distribution\n *                                        for all the files in the snapshot.\n */\ncase class VersionChecksum(\n    txnId: Option[String],\n    tableSizeBytes: Long,\n    numFiles: Long,\n    @JsonDeserialize(contentAs = classOf[Long])\n    numDeletedRecordsOpt: Option[Long],\n    @JsonDeserialize(contentAs = classOf[Long])\n    numDeletionVectorsOpt: Option[Long],\n    numMetadata: Long,\n    numProtocol: Long,\n    @JsonDeserialize(contentAs = classOf[Long])\n    inCommitTimestampOpt: Option[Long],\n    setTransactions: Option[Seq[SetTransaction]],\n    domainMetadata: Option[Seq[DomainMetadata]],\n    metadata: Metadata,\n    protocol: Protocol,\n    // Accept both \"histogramOpt\" (legacy Delta-Spark) and\n    // \"fileSizeHistogram\" (Delta spec / Kernel).\n    @JsonAlias(Array(\"fileSizeHistogram\"))\n    histogramOpt: Option[FileSizeHistogram],\n    deletedRecordCountsHistogramOpt: Option[DeletedRecordCountsHistogram],\n    allFiles: Option[Seq[AddFile]]) {\n\n  /**\n   * Converts to the protocol-compliant representation that serializes the histogram field as\n   * `fileSizeHistogram` (the Delta spec field name) instead of `histogramOpt`.\n   */\n  def toProtocolCompliant: VersionChecksumProtocolCompliant = VersionChecksumProtocolCompliant(\n    txnId = txnId,\n    tableSizeBytes = tableSizeBytes,\n    numFiles = numFiles,\n    numDeletedRecordsOpt = numDeletedRecordsOpt,\n    numDeletionVectorsOpt = numDeletionVectorsOpt,\n    numMetadata = numMetadata,\n    numProtocol = numProtocol,\n    inCommitTimestampOpt = inCommitTimestampOpt,\n    setTransactions = setTransactions,\n    domainMetadata = domainMetadata,\n    metadata = metadata,\n    protocol = protocol,\n    fileSizeHistogram = histogramOpt,\n    deletedRecordCountsHistogramOpt = deletedRecordCountsHistogramOpt,\n    allFiles = allFiles\n  )\n}\n\n/**\n * Protocol-compliant version of [[VersionChecksum]] that serializes the file size histogram\n * using the Delta spec field name `fileSizeHistogram` instead of the legacy `histogramOpt`.\n * Used only for CRC file writes when the protocol-compliant flag is enabled.\n */\ncase class VersionChecksumProtocolCompliant(\n    txnId: Option[String],\n    tableSizeBytes: Long,\n    numFiles: Long,\n    @JsonDeserialize(contentAs = classOf[Long])\n    numDeletedRecordsOpt: Option[Long],\n    @JsonDeserialize(contentAs = classOf[Long])\n    numDeletionVectorsOpt: Option[Long],\n    numMetadata: Long,\n    numProtocol: Long,\n    @JsonDeserialize(contentAs = classOf[Long])\n    inCommitTimestampOpt: Option[Long],\n    setTransactions: Option[Seq[SetTransaction]],\n    domainMetadata: Option[Seq[DomainMetadata]],\n    metadata: Metadata,\n    protocol: Protocol,\n    fileSizeHistogram: Option[FileSizeHistogram],\n    deletedRecordCountsHistogramOpt: Option[DeletedRecordCountsHistogram],\n    allFiles: Option[Seq[AddFile]])\n\n/**\n * Record the state of the table as a checksum file along with a commit.\n */\ntrait RecordChecksum extends DeltaLogging {\n  val deltaLog: DeltaLog\n  protected def spark: SparkSession\n\n  private lazy val writer =\n    CheckpointFileManager.create(deltaLog.logPath, deltaLog.newDeltaHadoopConf())\n\n  private def getChecksum(snapshot: Snapshot): VersionChecksum = snapshot.computeChecksum\n\n  protected def writeChecksumFile(txnId: String, snapshot: Snapshot): Unit = {\n    if (!spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED)) {\n      return\n    }\n\n    val checksumWithoutTxnId = getChecksum(snapshot)\n    val checksum = checksumWithoutTxnId.copy(txnId = Some(txnId))\n    writeChecksumFile(snapshot.version, checksum)\n  }\n\n  protected def writeChecksumFile(version: Long, checksum: VersionChecksum): Unit = {\n    val eventData = mutable.Map[String, Any](\"operationSucceeded\" -> false)\n    eventData(\"numAddFileActions\") = checksum.allFiles.map(_.size).getOrElse(-1)\n    eventData(\"numSetTransactionActions\") = checksum.setTransactions.map(_.size).getOrElse(-1)\n    val startTimeMs = System.currentTimeMillis()\n    try {\n      val toWrite = (if (spark.conf.get(\n          DeltaSQLConf.DELTA_CHECKSUM_HISTOGRAM_FIELD_FOLLOWS_PROTOCOL)) {\n        JsonUtils.toJson(checksum.toProtocolCompliant)\n      } else {\n        JsonUtils.toJson(checksum)\n      }) + \"\\n\"\n      eventData(\"jsonSerializationTimeTakenMs\") = System.currentTimeMillis() - startTimeMs\n      eventData(\"checksumLength\") = toWrite.length\n      val stream = writer.createAtomic(\n        FileNames.checksumFile(deltaLog.logPath, version),\n        overwriteIfPossible = false)\n      try {\n        stream.write(toWrite.getBytes(UTF_8))\n        stream.close()\n        eventData(\"overallTimeTakenMs\") = System.currentTimeMillis() - startTimeMs\n        eventData(\"operationSucceeded\") = true\n      } catch {\n        case NonFatal(e) =>\n          logWarning(log\"Failed to write the checksum for version: \" +\n            log\"${MDC(DeltaLogKeys.VERSION, version)}\", e)\n          stream.cancel()\n      }\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Failed to write the checksum for version: \" +\n          log\"${MDC(DeltaLogKeys.VERSION, version)}\", e)\n    }\n    recordDeltaEvent(\n      deltaLog,\n      opType = \"delta.checksum.write\",\n      data = eventData)\n  }\n\n  /**\n   * Incrementally derive checksum for the just-committed or about-to-be committed snapshot.\n   * @param spark The SparkSession\n   * @param deltaLog The DeltaLog\n   * @param versionToCompute The version for which we want to compute the checksum\n   * @param actions The actions corresponding to the version `versionToCompute`\n   * @param metadataOpt The metadata corresponding to the version `versionToCompute` (if known)\n   * @param protocolOpt The protocol corresponding to the version `versionToCompute` (if known)\n   * @param operationName The operation name corresponding to the version `versionToCompute`\n   * @param txnIdOpt The transaction identifier for the version `versionToCompute`\n   * @param previousVersionState Contains either the versionChecksum corresponding to\n   *                             `versionToCompute - 1` or a snapshot. Note that the snapshot may\n   *                             belong to any version and this method will only use the snapshot if\n   *                             it corresponds to `versionToCompute - 1`.\n   * @param includeAddFilesInCrc True if the new checksum should include a [[AddFile]]s.\n   * @return Either the new checksum or an error code string if the checksum could not be computed.\n   */\n  // scalastyle:off argcount\n  def incrementallyDeriveChecksum(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      versionToCompute: Long,\n      actions: Seq[Action],\n      metadataOpt: Option[Metadata],\n      protocolOpt: Option[Protocol],\n      operationName: String,\n      txnIdOpt: Option[String],\n      previousVersionState: Either[Snapshot, VersionChecksum],\n      includeAddFilesInCrc: Boolean\n  ): Either[String, VersionChecksum] = {\n    // scalastyle:on argcount\n    if (!deltaLog.incrementalCommitEnabled) {\n      return Left(\"INCREMENTAL_COMMITS_DISABLED\")\n    }\n\n    // Do not incrementally derive checksum for ManualUpdate operations since it may\n    // include actions that violate delta protocol invariants.\n    if (operationName == DeltaOperations.ManualUpdate.name) {\n      return Left(\"INVALID_OPERATION_MANUAL_UPDATE\")\n    }\n\n    // Try to incrementally compute a VersionChecksum for the just-committed snapshot.\n    val expectedVersion = versionToCompute - 1\n    val (oldVersionChecksum, oldSnapshot) = previousVersionState match {\n      case Right(checksum) => checksum -> None\n      case Left(snapshot) if snapshot.version == expectedVersion =>\n        // The original snapshot is still fresh so use it directly. Note this could trigger\n        // a state reconstruction if there is not an existing checksumOpt in the snapshot\n        // or if the existing checksumOpt contains missing information e.g.\n        // a null valued metadata or protocol. However, if we do not obtain a checksum here,\n        // then we cannot incrementally derive a new checksum for the new snapshot.\n        logInfo(log\"Incremental commit: starting with snapshot version \" +\n          log\"${MDC(DeltaLogKeys.VERSION, expectedVersion)}\")\n        getChecksum(snapshot).copy(numMetadata = 1, numProtocol = 1) -> Some(snapshot)\n      case _ =>\n        previousVersionState.swap.foreach { snapshot =>\n          // Occurs when snapshot is no longer fresh due to concurrent writers.\n          // Read CRC file and validate checksum information is complete.\n          recordDeltaEvent(deltaLog, opType = \"delta.commit.snapshotAgedOut\", data = Map(\n            \"snapshotVersion\" -> snapshot.version,\n            \"commitAttemptVersion\" -> versionToCompute\n          ))\n        }\n        val oldCrcOpt = deltaLog.readChecksum(expectedVersion)\n        if (oldCrcOpt.isEmpty) {\n          return Left(\"MISSING_OLD_CRC\")\n        }\n        val oldCrcFiltered = oldCrcOpt\n          .filterNot(_.metadata == null)\n          .filterNot(_.protocol == null)\n\n        val oldCrc = oldCrcFiltered.getOrElse {\n          return Left(\"OLD_CRC_INCOMPLETE\")\n        }\n        oldCrc -> None\n    }\n\n    // Incrementally compute the new version checksum, if the old one is available.\n    val ignoreAddFilesInOperation =\n      RecordChecksum.operationNamesWhereAddFilesIgnoredForIncrementalCrc.contains(operationName)\n    val ignoreRemoveFilesInOperation =\n      RecordChecksum.operationNamesWhereRemoveFilesIgnoredForIncrementalCrc.contains(operationName)\n    // Retrieve protocol/metadata in order of precedence:\n    // 1. Use provided protocol/metadata if available\n    // 2. Look for a protocol/metadata action in the incremental set of actions to be applied\n    // 3. Use protocol/metadata from previous version's checksum\n    // 4. Return PROTOCOL_MISSING/METADATA_MISSING error if all attempts fail\n    val protocol = protocolOpt\n      .orElse(actions.collectFirst { case p: Protocol => p })\n      .orElse(Option(oldVersionChecksum.protocol))\n      .getOrElse {\n        return Left(\"PROTOCOL_MISSING\")\n      }\n    val metadata = metadataOpt\n      .orElse(actions.collectFirst { case m: Metadata => m })\n      .orElse(Option(oldVersionChecksum.metadata))\n      .getOrElse {\n        return Left(\"METADATA_MISSING\")\n      }\n    val persistentDVsOnTableReadable =\n      DeletionVectorUtils.deletionVectorsReadable(protocol, metadata)\n    val persistentDVsOnTableWritable =\n      DeletionVectorUtils.deletionVectorsWritable(protocol, metadata)\n\n    computeNewChecksum(\n      versionToCompute,\n      operationName,\n      txnIdOpt,\n      oldVersionChecksum,\n      oldSnapshot,\n      actions,\n      ignoreAddFilesInOperation,\n      ignoreRemoveFilesInOperation,\n      includeAddFilesInCrc,\n      persistentDVsOnTableReadable,\n      persistentDVsOnTableWritable\n    )\n  }\n\n  /**\n   * Incrementally derive new checksum from old checksum + actions.\n   *\n   * @param attemptVersion commit attempt version for which we want to generate CRC.\n   * @param operationName operation name for the attempted commit.\n   * @param txnId transaction identifier.\n   * @param oldVersionChecksum from previous commit (attemptVersion - 1).\n   * @param oldSnapshot snapshot representing previous commit version (i.e. attemptVersion - 1),\n   *                    None if not available.\n   * @param actions used to incrementally compute new checksum.\n   * @param ignoreAddFiles for transactions whose add file actions refer to already-existing files\n   *                       e.g., [[DeltaOperations.ComputeStats]] transactions.\n   * @param ignoreRemoveFiles for transactions that generate RemoveFiles for auxiliary files\n   *                          e.g., [[DeltaOperations.AddDeletionVectorsTombstones]].\n   * @param persistentDVsOnTableReadable Indicates whether commands modifying this table are allowed\n   *                                      to read deletion vectors.\n   * @param persistentDVsOnTableWritable Indicates whether commands modifying this table are allowed\n   *                                      to create new deletion vectors.\n   * @return Either the new checksum or error code string if the checksum could not be computed\n   *         incrementally due to some reason.\n   */\n  // scalastyle:off argcount\n  private[delta] def computeNewChecksum(\n      attemptVersion: Long,\n      operationName: String,\n      txnIdOpt: Option[String],\n      oldVersionChecksum: VersionChecksum,\n      oldSnapshot: Option[Snapshot],\n      actions: Seq[Action],\n      ignoreAddFiles: Boolean,\n      ignoreRemoveFiles: Boolean,\n      includeAllFilesInCRC: Boolean,\n      persistentDVsOnTableReadable: Boolean,\n      persistentDVsOnTableWritable: Boolean\n  ) : Either[String, VersionChecksum] = {\n    // scalastyle:on argcount\n    oldSnapshot.foreach(s => require(s.version == (attemptVersion - 1)))\n    var tableSizeBytes = oldVersionChecksum.tableSizeBytes\n    var numFiles = oldVersionChecksum.numFiles\n    var protocol = oldVersionChecksum.protocol\n    var metadata = oldVersionChecksum.metadata\n\n    // In incremental computation, tables initialized with DVs disabled contain None DV\n    // statistics. DV statistics remain None even if DVs are enabled at a random point\n    // during the lifecycle of a table. That can only change if a full snapshot recomputation\n    // is invoked while DVs are enabled for the table.\n    val conf = spark.sessionState.conf\n    val isFirstVersion = oldSnapshot.forall(_.version == -1)\n    val checksumDVMetricsEnabled = conf.getConf(DeltaSQLConf.DELTA_CHECKSUM_DV_METRICS_ENABLED)\n    val deletedRecordCountsHistogramEnabled =\n      conf.getConf(DeltaSQLConf.DELTA_DELETED_RECORD_COUNTS_HISTOGRAM_ENABLED)\n\n    // For tables where DVs were disabled later on in the table lifecycle we want to maintain DV\n    // statistics.\n    val computeDVMetricsWhenDVsNotWritable = persistentDVsOnTableReadable &&\n      oldVersionChecksum.numDeletionVectorsOpt.isDefined && !isFirstVersion\n\n    val computeDVMetrics = checksumDVMetricsEnabled &&\n      (persistentDVsOnTableWritable || computeDVMetricsWhenDVsNotWritable)\n\n    // DV-related metrics. When the old checksum does not contain DV statistics, we attempt to\n    // pick them up from the old snapshot.\n    var numDeletedRecordsOpt = if (computeDVMetrics) {\n      oldVersionChecksum.numDeletedRecordsOpt\n        .orElse(oldSnapshot.flatMap(_.numDeletedRecordsOpt))\n    } else None\n    var numDeletionVectorsOpt = if (computeDVMetrics) {\n      oldVersionChecksum.numDeletionVectorsOpt\n        .orElse(oldSnapshot.flatMap(_.numDeletionVectorsOpt))\n    } else None\n    val deletedRecordCountsHistogramOpt =\n      if (computeDVMetrics && deletedRecordCountsHistogramEnabled) {\n        oldVersionChecksum.deletedRecordCountsHistogramOpt\n          .orElse(oldSnapshot.flatMap(_.deletedRecordCountsHistogramOpt))\n          .map(h => DeletedRecordCountsHistogram(h.deletedRecordCounts.clone()))\n      } else None\n\n    var inCommitTimestamp : Option[Long] = None\n    actions.foreach {\n      case a: AddFile if !ignoreAddFiles =>\n        tableSizeBytes += a.size\n        numFiles += 1\n\n        // Only accumulate DV statistics when base stats are not None.\n        val (dvCount, dvCardinality) =\n          Option(a.deletionVector).map(1L -> _.cardinality).getOrElse(0L -> 0L)\n        numDeletedRecordsOpt = numDeletedRecordsOpt.map(_ + dvCardinality)\n        numDeletionVectorsOpt = numDeletionVectorsOpt.map(_ + dvCount)\n        deletedRecordCountsHistogramOpt.foreach(_.insert(dvCardinality))\n\n      case _: RemoveFile if ignoreRemoveFiles => ()\n\n      // extendedFileMetadata == true implies fields partitionValues, size, and tags are present\n      case r: RemoveFile if r.extendedFileMetadata == Some(true) =>\n        val size = r.size.get\n        tableSizeBytes -= size\n        numFiles -= 1\n\n        // Only accumulate DV statistics when base stats are not None.\n        val (dvCount, dvCardinality) =\n          Option(r.deletionVector).map(1L -> _.cardinality).getOrElse(0L -> 0L)\n        numDeletedRecordsOpt = numDeletedRecordsOpt.map(_ - dvCardinality)\n        numDeletionVectorsOpt = numDeletionVectorsOpt.map(_ - dvCount)\n        deletedRecordCountsHistogramOpt.foreach(_.remove(dvCardinality))\n\n      case r: RemoveFile =>\n        // Report the failure to usage logs.\n        val msg = s\"A remove action with a missing file size was detected in file ${r.path} \" +\n          \"causing incremental commit to fallback to state reconstruction.\"\n        recordDeltaEvent(\n          this.deltaLog,\n          \"delta.checksum.compute\",\n          data = Map(\"error\" -> msg))\n        return Left(\"ENCOUNTERED_REMOVE_FILE_MISSING_SIZE\")\n      case p: Protocol =>\n        protocol = p\n      case m: Metadata =>\n        metadata = m\n      case ci: CommitInfo =>\n        inCommitTimestamp = ci.inCommitTimestamp\n      case _ =>\n    }\n\n    val setTransactions = incrementallyComputeSetTransactions(\n      oldSnapshot, oldVersionChecksum, attemptVersion, actions)\n\n    val domainMetadata = incrementallyComputeDomainMetadatas(\n      oldSnapshot, oldVersionChecksum, attemptVersion, actions)\n\n    val computeAddFiles = if (includeAllFilesInCRC) {\n      incrementallyComputeAddFiles(\n        oldSnapshot = oldSnapshot,\n        oldVersionChecksum = oldVersionChecksum,\n        attemptVersion = attemptVersion,\n        numFilesAfterCommit = numFiles,\n        actionsToCommit = actions)\n    } else if (numFiles == 0) {\n      // If the table becomes empty after the commit, addFiles should be empty.\n      Option(Nil)\n    } else {\n      None\n    }\n\n    val allFiles = computeAddFiles.filter { files =>\n      val computedNumFiles = files.size\n      val computedTableSizeBytes = files.map(_.size).sum\n      // Validate checksum of Incrementally computed files against the computed checksum from\n      // incremental commits.\n      if (computedNumFiles != numFiles || computedTableSizeBytes != tableSizeBytes) {\n        val filePathsFromPreviousVersion = oldVersionChecksum.allFiles\n          .orElse {\n            recordFrameProfile(\"Delta\", \"VersionChecksum.computeNewChecksum.allFiles\") {\n              oldSnapshot.map(_.allFiles.collect().toSeq)\n            }\n          }\n          .getOrElse(Seq.empty)\n          .map(_.path)\n        val addFilePathsInThisCommit = actions.collect { case af: AddFile => af.path }\n        val removeFilePathsInThisCommit = actions.collect { case rf: RemoveFile => rf.path }\n        logWarning(log\"Incrementally computed files does not match the incremental checksum \" +\n          log\"for commit attempt: ${MDC(DeltaLogKeys.VERSION, attemptVersion)}. \" +\n          log\"addFilePathsInThisCommit: [${MDC(DeltaLogKeys.PATHS,\n            addFilePathsInThisCommit.mkString(\",\"))}], \" +\n          log\"removeFilePathsInThisCommit: [${MDC(DeltaLogKeys.PATHS2,\n            removeFilePathsInThisCommit.mkString(\",\"))}], \" +\n          log\"filePathsFromPreviousVersion: [${MDC(DeltaLogKeys.PATHS3,\n            filePathsFromPreviousVersion.mkString(\",\"))}], \" +\n          log\"computedFiles: [${MDC(DeltaLogKeys.PATHS4,\n            files.map(_.path).mkString(\",\"))}]\")\n        val eventData = Map(\n          \"attemptVersion\" -> attemptVersion,\n          \"expectedNumFiles\" -> numFiles,\n          \"expectedTableSizeBytes\" -> tableSizeBytes,\n          \"computedNumFiles\" -> computedNumFiles,\n          \"computedTableSizeBytes\" -> computedTableSizeBytes,\n          \"numAddFilePathsInThisCommit\" -> addFilePathsInThisCommit.size,\n          \"numRemoveFilePathsInThisCommit\" -> removeFilePathsInThisCommit.size,\n          \"numFilesInPreviousVersion\" -> filePathsFromPreviousVersion.size,\n          \"operationName\" -> operationName,\n          \"addFilePathsInThisCommit\" -> JsonUtils.toJson(addFilePathsInThisCommit.take(10)),\n          \"removeFilePathsInThisCommit\" -> JsonUtils.toJson(removeFilePathsInThisCommit.take(10)),\n          \"filePathsFromPreviousVersion\" -> JsonUtils.toJson(filePathsFromPreviousVersion.take(10)),\n          \"computedFiles\" -> JsonUtils.toJson(files.take(10))\n        )\n        recordDeltaEvent(\n          deltaLog,\n          opType = \"delta.allFilesInCrc.checksumMismatch.aggregated\",\n          data = eventData)\n        if (DeltaUtils.isTesting) {\n          throw new IllegalStateException(\"Incrementally Computed State failed checksum check\" +\n            s\" for commit $attemptVersion [$eventData]\")\n        }\n        false\n      } else {\n        true\n      }\n    }\n\n    Right(VersionChecksum(\n      txnId = txnIdOpt,\n      tableSizeBytes = tableSizeBytes,\n      numFiles = numFiles,\n      numDeletedRecordsOpt = numDeletedRecordsOpt,\n      numDeletionVectorsOpt = numDeletionVectorsOpt,\n      numMetadata = 1,\n      numProtocol = 1,\n      inCommitTimestampOpt = inCommitTimestamp,\n      metadata = metadata,\n      protocol = protocol,\n      setTransactions = setTransactions,\n      domainMetadata = domainMetadata,\n      allFiles = allFiles,\n      deletedRecordCountsHistogramOpt = deletedRecordCountsHistogramOpt,\n      histogramOpt = None\n    ))\n  }\n\n  /**\n   * Incrementally compute [[Snapshot.setTransactions]] for the commit `attemptVersion`.\n   *\n   * @param oldSnapshot - snapshot corresponding to `attemptVersion` - 1\n   * @param oldVersionChecksum - [[VersionChecksum]] corresponding to `attemptVersion` - 1\n   * @param attemptVersion - version which we want to commit\n   * @param actionsToCommit - actions for commit `attemptVersion`\n   * @return Optional sequence of incrementally computed [[SetTransaction]]s for commit\n   *         `attemptVersion`.\n   */\n  private def incrementallyComputeSetTransactions(\n      oldSnapshot: Option[Snapshot],\n      oldVersionChecksum: VersionChecksum,\n      attemptVersion: Long,\n      actionsToCommit: Seq[Action]): Option[Seq[SetTransaction]] = {\n    // Check-1: check conf\n    if (!spark.conf.get(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC)) {\n      return None\n    }\n\n    // Check-2: check `minSetTransactionRetentionTimestamp` is not set\n    val newMetadataToCommit = actionsToCommit.collectFirst { case m: Metadata => m }\n    // TODO: Add support for incrementally computing [[SetTransaction]]s even when\n    //  `minSetTransactionRetentionTimestamp` is set.\n    // We don't incrementally compute [[SetTransaction]]s when user has configured\n    // `minSetTransactionRetentionTimestamp` as it makes verification non-deterministic.\n    // Check all places to figure out whether `minSetTransactionRetentionTimestamp` is set:\n    // 1. oldSnapshot corresponding to `attemptVersion - 1`\n    // 2. old VersionChecksum's MetaData (corresponding to `attemptVersion-1`)\n    // 3. new VersionChecksum's MetaData (corresponding to `attemptVersion`)\n    val setTransactionRetentionTimestampConfigured =\n      (oldSnapshot.map(_.metadata) ++ Option(oldVersionChecksum.metadata) ++ newMetadataToCommit)\n        .exists(DeltaLog.minSetTransactionRetentionInterval(_).nonEmpty)\n    if (setTransactionRetentionTimestampConfigured) return None\n\n    // Check-3: Check old setTransactions are available so that we can incrementally compute new.\n    val oldSetTransactions = oldVersionChecksum.setTransactions\n      .getOrElse { return None }\n\n    // Check-4: old/new setTransactions are within the threshold.\n    val setTransactionsToCommit = actionsToCommit.filter(_.isInstanceOf[SetTransaction])\n    val threshold = spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_MAX_SET_TRANSACTIONS_IN_CRC)\n    if (Math.max(setTransactionsToCommit.size, oldSetTransactions.size) > threshold) return None\n\n    // We currently don't attempt incremental [[SetTransaction]] when\n    // `minSetTransactionRetentionTimestamp` is set. So passing this as None here explicitly.\n    // We can also ignore file retention because that only affects [[RemoveFile]] actions.\n    val logReplay = new InMemoryLogReplay(\n      minFileRetentionTimestamp = None,\n      minSetTransactionRetentionTimestamp = None)\n\n    logReplay.append(attemptVersion - 1, oldSetTransactions.toIterator)\n    logReplay.append(attemptVersion, setTransactionsToCommit.toIterator)\n    Some(logReplay.getTransactions.toSeq).filter(_.size <= threshold)\n  }\n\n  /**\n   * Incrementally compute [[Snapshot.domainMetadata]] for the commit `attemptVersion`.\n   *\n   * @param oldVersionChecksum - [[VersionChecksum]] corresponding to `attemptVersion` - 1\n   * @param attemptVersion - version which we want to commit\n   * @param actionsToCommit - actions for commit `attemptVersion`\n   * @return Sequence of incrementally computed [[DomainMetadata]]s for commit\n   *         `attemptVersion`.\n   */\n  private def incrementallyComputeDomainMetadatas(\n      oldSnapshot: Option[Snapshot],\n      oldVersionChecksum: VersionChecksum,\n      attemptVersion: Long,\n      actionsToCommit: Seq[Action]): Option[Seq[DomainMetadata]] = {\n    // Check old DomainMetadatas are available so that we can incrementally compute new.\n    val oldDomainMetadatas = oldVersionChecksum.domainMetadata\n      .getOrElse { return None }\n    val newDomainMetadatas = actionsToCommit.filter(_.isInstanceOf[DomainMetadata])\n\n    // We only work with DomainMetadata, so RemoveFile and SetTransaction retention don't matter.\n    val logReplay = new InMemoryLogReplay(\n      minFileRetentionTimestamp = None,\n      minSetTransactionRetentionTimestamp = None)\n\n    val threshold = spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_MAX_DOMAIN_METADATAS_IN_CRC)\n\n    logReplay.append(attemptVersion - 1, oldDomainMetadatas.iterator)\n    logReplay.append(attemptVersion, newDomainMetadatas.iterator)\n    // We don't truncate the set of DomainMetadata actions. Instead, we either store all of them or\n    // none of them. The advantage of this is that you can then determine presence based on the\n    // checksum, i.e. if the checksum contains domain metadatas but it doesn't contain the one you\n    // are looking for, then it's not there.\n    //\n    // It's also worth noting that we can distinguish \"no domain metadatas\" versus\n    // \"domain metadatas not stored\" as [[Some]] vs. [[None]].\n    Some(logReplay.getDomainMetadatas.toSeq).filter(_.size <= threshold)\n  }\n\n  /**\n   * Incrementally compute [[Snapshot.allFiles]] for the commit `attemptVersion`.\n   *\n   * @param oldSnapshot - snapshot corresponding to `attemptVersion` - 1\n   * @param oldVersionChecksum - [[VersionChecksum]] corresponding to `attemptVersion` - 1\n   * @param attemptVersion - version which we want to commit\n   * @param numFilesAfterCommit - number of files in the table after the attemptVersion commit.\n   * @param actionsToCommit - actions for commit `attemptVersion`\n   * @return Optional sequence of AddFiles which represents the incrementally computed state for\n   *         commit `attemptVersion`\n   */\n  private def incrementallyComputeAddFiles(\n      oldSnapshot: Option[Snapshot],\n      oldVersionChecksum: VersionChecksum,\n      attemptVersion: Long,\n      numFilesAfterCommit: Long,\n      actionsToCommit: Seq[Action]): Option[Seq[AddFile]] = {\n\n    // We must enumerate both the pre- and post-commit file lists; give up if they are too big.\n    val incrementalAllFilesThreshold =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_THRESHOLD_FILES)\n    val numFilesBeforeCommit = oldVersionChecksum.numFiles\n    if (Math.max(numFilesAfterCommit, numFilesBeforeCommit) > incrementalAllFilesThreshold) {\n      return None\n    }\n\n    // We try to get files for `attemptVersion - 1` from the old CRC first. If the old CRC doesn't\n    // have those files, then we will try to get that info from the oldSnapshot (corresponding to\n    // attemptVersion - 1). Note that oldSnapshot might not be present if another concurrent commits\n    // have happened in between. In this case we return and not store incrementally computed state\n    // to crc.\n    val oldAllFiles = oldVersionChecksum.allFiles\n      .orElse {\n        recordFrameProfile(\"Delta\", \"VersionChecksum.incrementallyComputeAddFiles\") {\n          oldSnapshot.map(_.allFiles.collect().toSeq)\n        }\n      }\n      .getOrElse { return None }\n\n    val canonicalPath = new DeltaLog.CanonicalPathFunction(() => deltaLog.newDeltaHadoopConf())\n    def normalizePath(action: Action): Action = action match {\n      case af: AddFile => af.copy(path = canonicalPath(af.path))\n      case rf: RemoveFile => rf.copy(path = canonicalPath(rf.path))\n      case others => others\n    }\n\n    // We only work with AddFile, so RemoveFile and SetTransaction retention don't matter.\n    val logReplay = new InMemoryLogReplay(\n      minFileRetentionTimestamp = None,\n      minSetTransactionRetentionTimestamp = None)\n\n    logReplay.append(attemptVersion - 1, oldAllFiles.map(normalizePath).toIterator)\n    logReplay.append(attemptVersion, actionsToCommit.map(normalizePath).toIterator)\n    Some(logReplay.allFiles)\n  }\n}\n\nobject RecordChecksum {\n  // Operations where we should ignore AddFiles in the incremental checksum computation.\n  private[delta] val operationNamesWhereAddFilesIgnoredForIncrementalCrc = Set(\n    // The transaction that computes stats is special -- it re-adds files that already exist, in\n    // order to update their min/max stats. We should not count those against the totals.\n    DeltaOperations.ComputeStats(Seq.empty).name,\n    // Backfill/Tagging re-adds existing AddFiles without changing the underlying data files.\n    // Incremental commits should ignore backfill commits.\n    DeltaOperations.RowTrackingBackfill().name,\n    // Same as Backfill.\n    DeltaOperations.RowTrackingUnBackfill().name,\n    // Dropping a feature may re-add existing AddFiles without changing the underlying data files.\n    DeltaOperations.OP_DROP_FEATURE\n  )\n\n  // Operations where we should ignore RemoveFiles in the incremental checksum computation.\n  private[delta] val operationNamesWhereRemoveFilesIgnoredForIncrementalCrc = Set(\n    // Deletion vector tombstones are only required to protect DVs from vacuum. They should be\n    // ignored in checksum calculation.\n    DeltaOperations.AddDeletionVectorsTombstones.name\n  )\n}\n\n/**\n * Read checksum files.\n */\ntrait ReadChecksum extends DeltaLogging { self: DeltaLog =>\n\n  val logPath: Path\n  private[delta] def store: LogStore\n\n  private[delta] def readChecksum(\n      version: Long,\n      checksumFileStatusHintOpt: Option[FileStatus] = None): Option[VersionChecksum] = {\n    recordDeltaOperation(self, \"delta.readChecksum\") {\n      val checksumFilePath = FileNames.checksumFile(logPath, version)\n      val verifiedChecksumFileStatusOpt =\n        checksumFileStatusHintOpt.filter(_.getPath == checksumFilePath)\n      var exception: Option[String] = None\n      val content = try Some(\n        verifiedChecksumFileStatusOpt\n          .map(store.read(_, newDeltaHadoopConf()))\n          .getOrElse(store.read(checksumFilePath, newDeltaHadoopConf()))\n      ) catch {\n        case NonFatal(e) =>\n          // We expect FileNotFoundException; if it's another kind of exception, we still catch them\n          // here but we log them in the checksum error event below.\n          if (!e.isInstanceOf[FileNotFoundException]) {\n            exception = Some(Utils.exceptionString(e))\n          }\n          None\n      }\n\n      if (content.isEmpty) {\n        // We may not find the checksum file in two cases:\n        //  - We just upgraded our Spark version from an old one\n        //  - Race conditions where we commit a transaction, and before we can write the checksum\n        //    this reader lists the new version, and uses it to create the snapshot.\n        recordDeltaEvent(\n          this,\n          \"delta.checksum.error.missing\",\n          data = Map(\"version\" -> version) ++ exception.map(\"exception\" -> _))\n\n        return None\n      }\n      val checksumData = content.get\n      if (checksumData.isEmpty) {\n        recordDeltaEvent(\n          this,\n          \"delta.checksum.error.empty\",\n          data = Map(\"version\" -> version))\n        return None\n      }\n      try {\n        Option(JsonUtils.mapper.readValue[VersionChecksum](checksumData.head))\n      } catch {\n        case NonFatal(e) =>\n          recordDeltaEvent(\n            this,\n            \"delta.checksum.error.parsing\",\n            data = Map(\"exception\" -> Utils.exceptionString(e)))\n          None\n      }\n    }\n  }\n}\n\n/**\n * Verify the state of the table using the checksum information.\n */\ntrait ValidateChecksum extends DeltaLogging { self: Snapshot =>\n\n  /**\n   * Validate checksum (if any) by comparing it against the snapshot's state reconstruction.\n   * @param contextInfo caller context that will be added to the logging if validation fails\n   * @return True iff validation succeeded.\n   * @throws IllegalStateException if validation failed and corruption is configured as fatal.\n   */\n  def validateChecksum(contextInfo: Map[String, String] = Map.empty): Boolean = {\n    val contextSuffix = contextInfo.get(\"context\").map(c => s\".context-$c\").getOrElse(\"\")\n    val computedStateAccessor = s\"ValidateChecksum.checkMismatch$contextSuffix\"\n    val computedStateToCompareAgainst = computedState\n    val (mismatchErrorMap, detailedErrorMapForUsageLogs) = checksumOpt\n        .map(checkMismatch(_, computedStateToCompareAgainst))\n        .getOrElse((Map.empty[String, String], Map.empty[String, String]))\n    logAndThrowValidationFailure(mismatchErrorMap, detailedErrorMapForUsageLogs, contextInfo)\n  }\n\n  private def logAndThrowValidationFailure(\n      mismatchErrorMap: Map[String, String],\n      detailedErrorMapForUsageLogs: Map[String, String],\n      contextInfo: Map[String, String]): Boolean = {\n    if (mismatchErrorMap.isEmpty) return true\n    val mismatchString = mismatchErrorMap.values.mkString(\"\\n\")\n\n    // We get the active SparkSession, which may be different than the SparkSession of the\n    // Snapshot that was created, since we cache `DeltaLog`s.\n    val sparkOpt = SparkSession.getActiveSession\n\n    // Report the failure to usage logs.\n    recordDeltaEvent(\n      this.deltaLog,\n      \"delta.checksum.invalid\",\n      data = Map(\n        \"error\" -> mismatchString,\n        \"mismatchingFields\" -> mismatchErrorMap.keys.toSeq,\n        \"detailedErrorMap\" -> detailedErrorMapForUsageLogs,\n        \"v2CheckpointEnabled\" ->\n          CheckpointProvider.isV2CheckpointEnabled(this),\n        \"checkpointProviderCheckpointPolicy\" ->\n          checkpointProvider.checkpointPolicy.map(_.name).getOrElse(\"\")\n      ) ++ contextInfo)\n\n    val spark = sparkOpt.getOrElse {\n      throw DeltaErrors.sparkSessionNotSetException()\n    }\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CHECKSUM_MISMATCH_IS_FATAL)) {\n      throw DeltaErrors.logFailedIntegrityCheck(version, mismatchString)\n    }\n    false\n  }\n\n  /**\n   * Validate [[Snapshot.allFiles]] against given checksum.allFiles.\n   * Returns true if validation succeeds, else return false.\n   * In Unit Tests, this method throws [[IllegalStateException]] so that issues can be caught during\n   * development.\n   */\n  def validateFileListAgainstCRC(checksum: VersionChecksum, contextOpt: Option[String]): Boolean = {\n    val fileSortKey = (f: AddFile) => (f.path, f.modificationTime, f.size)\n    val filesFromCrc = checksum.allFiles.map(_.sortBy(fileSortKey)).getOrElse { return true }\n    val filesFromStateReconstruction = recordFrameProfile(\"Delta\", \"snapshot.allFiles\") {\n      allFilesViaStateReconstruction.collect().toSeq.sortBy(fileSortKey)\n    }\n    if (filesFromCrc == filesFromStateReconstruction) return true\n\n    val filesFromCrcWithoutStats = filesFromCrc.map(_.copy(stats = \"\"))\n    val filesFromStateReconstructionWithoutStats =\n      filesFromStateReconstruction.map(_.copy(stats = \"\"))\n    val mismatchWithStatsOnly =\n      filesFromCrcWithoutStats == filesFromStateReconstructionWithoutStats\n\n    if (mismatchWithStatsOnly) {\n      // Normalize stats in CRC as per the table schema\n      val filesFromStateReconstructionMap =\n        filesFromStateReconstruction.map(af => (af.path, af)).toMap\n      val parser = DeltaFileProviderUtils.createJsonStatsParser(statsSchema)\n      var normalizedStatsDiffer = false\n      filesFromCrc.foreach { addFile =>\n        val statsFromSR = filesFromStateReconstructionMap(addFile.path).stats\n        val statsFromSRParsed = parser(statsFromSR)\n        val statsFromCrcParsed = parser(addFile.stats)\n        if (statsFromSRParsed != statsFromCrcParsed) {\n          normalizedStatsDiffer = true\n        }\n      }\n      if (!normalizedStatsDiffer) return true\n    }\n    // If incremental all-files-in-crc validation fails, then there is a possibility that the\n    // issue is not just with incremental all-files-in-crc computation but with overall incremental\n    // commits. So run the incremental commit crc validation and find out whether that is also\n    // failing.\n    val contextForIncrementalCommitCheck = contextOpt.map(c => s\"$c.\").getOrElse(\"\") +\n      \"delta.allFilesInCrc.checksumMismatch.validateFileListAgainstCRC\"\n    var errorForIncrementalCommitCrcValidation = \"\"\n    val incrementalCommitCrcValidationPassed = try {\n      validateChecksum(Map(\"context\" -> contextForIncrementalCommitCheck))\n    } catch {\n      case NonFatal(e) =>\n        errorForIncrementalCommitCrcValidation += e.getMessage\n        false\n    }\n    val eventData = Map(\n      \"version\" -> version,\n      \"mismatchWithStatsOnly\" -> mismatchWithStatsOnly,\n      \"filesCountFromCrc\" -> filesFromCrc.size,\n      \"filesCountFromStateReconstruction\" -> filesFromStateReconstruction.size,\n      \"filesFromCrc\" -> JsonUtils.toJson(filesFromCrc),\n      \"incrementalCommitCrcValidationPassed\" -> incrementalCommitCrcValidationPassed,\n      \"errorForIncrementalCommitCrcValidation\" -> errorForIncrementalCommitCrcValidation,\n      \"context\" -> contextOpt.getOrElse(\"\")\n    )\n    val message = s\"Incremental state reconstruction validation failed for version \" +\n      s\"$version [${eventData.mkString(\",\")}]\"\n    logInfo(message)\n    recordDeltaEvent(\n      this.deltaLog,\n      opType = \"delta.allFilesInCrc.checksumMismatch.differentAllFiles\",\n      data = eventData)\n    if (DeltaUtils.isTesting) throw new IllegalStateException(message)\n    false\n  }\n  /**\n   * Validates the given `checksum` against [[Snapshot.computedState]].\n   * Returns an tuple of Maps:\n   *  - first Map contains fieldName to user facing errorMessage mapping.\n   *  - second Map is just for usage logs purpose and contains more details for different fields.\n   *    Adding info to this map is optional.\n   */\n  private def checkMismatch(\n      checksum: VersionChecksum,\n      computedStateToCheckAgainst: SnapshotState\n  ): (Map[String, String], Map[String, String]) = {\n    var errorMap = ListMap[String, String]()\n    var detailedErrorMapForUsageLogs = ListMap[String, String]()\n\n    def compare(expected: Long, found: Long, title: String, field: String): Unit = {\n      if (expected != found) {\n        errorMap += (field -> s\"$title - Expected: $expected Computed: $found\")\n      }\n    }\n    def compareAction(expected: Action, found: Action, title: String, field: String): Unit = {\n      // only compare when expected is not null for being backward compatible to the checksum\n      // without protocol and metadata\n      Option(expected).filterNot(_.equals(found)).foreach { expected =>\n        errorMap += (field -> s\"$title - Expected: $expected Computed: $found\")\n      }\n    }\n\n    def compareSetTransactions(\n        setTransactionsInCRC: Seq[SetTransaction],\n        setTransactionsComputed: Seq[SetTransaction]): Unit = {\n      val appIdsFromCrc = setTransactionsInCRC.map(_.appId)\n      val repeatedEntriesForSameAppId = appIdsFromCrc.size != appIdsFromCrc.toSet.size\n      val setTransactionsInCRCSet = setTransactionsInCRC.toSet\n      val setTransactionsFromComputeStateSet = setTransactionsComputed.toSet\n      val exactMatchFailed = setTransactionsInCRCSet != setTransactionsFromComputeStateSet\n      if (repeatedEntriesForSameAppId || exactMatchFailed) {\n        val repeatedAppIds = appIdsFromCrc.groupBy(identity).filter(_._2.size > 1).keySet.toSeq\n        val matchedActions = setTransactionsInCRCSet.intersect(setTransactionsFromComputeStateSet)\n        val unmatchedActionsInCrc = setTransactionsInCRCSet -- matchedActions\n        val unmatchedActionsInComputed = setTransactionsFromComputeStateSet -- matchedActions\n        val eventData = Map(\n          \"unmatchedSetTransactionsCRC\" -> unmatchedActionsInCrc,\n          \"unmatchedSetTransactionsComputedState\" -> unmatchedActionsInComputed,\n          \"version\" -> version,\n          \"minSetTransactionRetentionTimestamp\" -> minSetTransactionRetentionTimestamp,\n          \"repeatedEntriesForSameAppId\" -> repeatedAppIds,\n          \"exactMatchFailed\" -> exactMatchFailed)\n        errorMap += (\"setTransactions\" -> s\"SetTransaction mismatch\")\n        detailedErrorMapForUsageLogs += (\"setTransactions\" -> JsonUtils.toJson(eventData))\n      }\n    }\n\n    def compareDomainMetadata(\n        domainMetadataInCRC: Seq[DomainMetadata],\n        domainMetadataComputed: Seq[DomainMetadata]): Unit = {\n      val domainMetadataInCRCSet = domainMetadataInCRC.toSet\n      // Remove any tombstones from the reconstructed set before comparison.\n      val domainMetadataInComputeStateSet = domainMetadataComputed.filterNot(_.removed).toSet\n      val exactMatchFailed = domainMetadataInCRCSet != domainMetadataInComputeStateSet\n      if (exactMatchFailed) {\n        val matchedActions = domainMetadataInCRCSet.intersect(domainMetadataInComputeStateSet)\n        val unmatchedActionsInCRC = domainMetadataInCRCSet -- matchedActions\n        val unmatchedActionsInComputed = domainMetadataInComputeStateSet -- matchedActions\n        val eventData = Map(\n          \"unmatchedDomainMetadataInCRC\" -> unmatchedActionsInCRC,\n          \"unmatchedDomainMetadataInComputedState\" -> unmatchedActionsInComputed,\n          \"version\" -> version)\n        errorMap += (\"domainMetadata\" -> \"domainMetadata mismatch\")\n        detailedErrorMapForUsageLogs += (\"domainMetadata\" -> JsonUtils.toJson(eventData))\n      }\n    }\n    // Deletion vectors metrics.\n    if (DeletionVectorUtils.deletionVectorsReadable(self)) {\n      (checksum.numDeletedRecordsOpt zip computedState.numDeletedRecordsOpt).foreach {\n        case (a, b) => compare(a, b, \"Number of deleted records\", \"numDeletedRecordsOpt\")\n      }\n      (checksum.numDeletionVectorsOpt zip computedState.numDeletionVectorsOpt).foreach {\n        case (a, b) => compare(a, b, \"Number of deleted vectors\", \"numDeletionVectorsOpt\")\n      }\n    }\n\n    compareAction(checksum.metadata, computedStateToCheckAgainst.metadata, \"Metadata\", \"metadata\")\n    compareAction(checksum.protocol, computedStateToCheckAgainst.protocol, \"Protocol\", \"protocol\")\n    compare(\n      checksum.tableSizeBytes,\n      computedStateToCheckAgainst.sizeInBytes,\n      title = \"Table size (bytes)\",\n      field = \"tableSizeBytes\")\n    compare(\n      checksum.numFiles,\n      computedStateToCheckAgainst.numOfFiles,\n      title = \"Number of files\",\n      field = \"numFiles\")\n    compare(\n      checksum.numMetadata,\n      computedStateToCheckAgainst.numOfMetadata,\n      title = \"Metadata updates\",\n      field = \"numOfMetadata\")\n    compare(\n      checksum.numProtocol,\n      computedStateToCheckAgainst.numOfProtocol,\n      title = \"Protocol updates\",\n      field = \"numOfProtocol\")\n\n    checksum.setTransactions.foreach { setTransactionsInCRC =>\n      compareSetTransactions(setTransactionsInCRC, computedStateToCheckAgainst.setTransactions)\n    }\n\n    checksum.domainMetadata.foreach(\n      compareDomainMetadata(_, computedStateToCheckAgainst.domainMetadata))\n\n    (errorMap, detailedErrorMapForUsageLogs)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ClassicColumnConversions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.classic.ClassicConversions\nimport org.apache.spark.sql.classic.ColumnConversions\nimport org.apache.spark.sql.classic.ColumnNodeToExpressionConverter\nimport org.apache.spark.sql.classic.{SparkSession => SparkSessionImpl}\n\n/**\n * Conversions from a [[org.apache.spark.sql.Column]] to an\n * [[org.apache.spark.sql.catalyst.expressions.Expression]], and vice versa.\n *\n * @note [[org.apache.spark.sql.internal.ExpressionUtils#expression]] is a cheap alternative for\n *       [[org.apache.spark.sql.Column]] to [[org.apache.spark.sql.catalyst.expressions.Expression]]\n *       conversions. However this can only be used when the produced expression is used in a Column\n *       later on.\n */\nobject ClassicColumnConversions\n  extends ClassicConversions\n  with ColumnConversions {\n  override def converter: ColumnNodeToExpressionConverter = ColumnNodeToExpressionConverter\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ColumnWithDefaultExprUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.mutable\nimport scala.concurrent.duration\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.constraints.{Constraint, Constraints}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf, DeltaStreamUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.GeneratedColumnValidateOnWriteMode\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{Column, DataFrame}\nimport org.apache.spark.sql.catalyst.expressions.EqualNullSafe\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.catalyst.util.ResolveDefaultColumns._\nimport org.apache.spark.sql.execution.QueryExecution\nimport org.apache.spark.sql.types.{MetadataBuilder, StructField, StructType}\n\n/**\n * Provide utilities to handle columns with default expressions.\n * Currently we support three types of such columns:\n * (1) GENERATED columns.\n * (2) IDENTITY columns.\n * (3) Columns with user-specified default value expression.\n */\nobject ColumnWithDefaultExprUtils extends DeltaLogging {\n  val USE_NULL_AS_DEFAULT_DELTA_OPTION = \"__use_null_as_default\"\n\n  // Returns true if column `field` is defined as an IDENTITY column.\n  def isIdentityColumn(field: StructField): Boolean = {\n    val md = field.metadata\n    val hasStart = md.contains(DeltaSourceUtils.IDENTITY_INFO_START)\n    val hasStep = md.contains(DeltaSourceUtils.IDENTITY_INFO_STEP)\n    val hasInsert = md.contains(DeltaSourceUtils.IDENTITY_INFO_ALLOW_EXPLICIT_INSERT)\n    // Verify that we have all or none of the three fields.\n    if (!((hasStart == hasStep) && (hasStart == hasInsert))) {\n      throw DeltaErrors.identityColumnInconsistentMetadata(field.name, hasStart, hasStep, hasInsert)\n    }\n    hasStart && hasStep && hasInsert\n  }\n\n  // Return true if `schema` contains any number of IDENTITY column.\n  def hasIdentityColumn(schema: StructType): Boolean = schema.exists(isIdentityColumn)\n\n  // Return if `protocol` satisfies the requirement for IDENTITY columns.\n  def satisfiesIdentityColumnProtocol(protocol: Protocol): Boolean =\n    protocol.isFeatureSupported(IdentityColumnsTableFeature) ||\n    protocol.minWriterVersion == 6 || protocol.writerFeatureNames.contains(\"identityColumns\")\n\n  // Return true if the column `col` has default expressions (and can thus be omitted from the\n  // insertion list).\n  def columnHasDefaultExpr(\n      protocol: Protocol,\n      col: StructField,\n      nullAsDefault: Boolean): Boolean = {\n    isIdentityColumn(col) ||\n    col.metadata.contains(CURRENT_DEFAULT_COLUMN_METADATA_KEY) ||\n    (col.nullable && nullAsDefault) ||\n    GeneratedColumn.isGeneratedColumn(protocol, col)\n  }\n\n  // Return true if the table with `metadata` has default expressions.\n  def tableHasDefaultExpr(\n      protocol: Protocol,\n      metadata: Metadata,\n      nullAsDefault: Boolean): Boolean = {\n    hasIdentityColumn(metadata.schema) ||\n    metadata.schema.exists { f =>\n      f.metadata.contains(CURRENT_DEFAULT_COLUMN_METADATA_KEY) ||\n        (f.nullable && nullAsDefault)\n    } ||\n    GeneratedColumn.enforcesGeneratedColumns(protocol, metadata)\n  }\n\n  /**\n   * If there are columns with default expressions in `schema`, add a new project to generate\n   * those columns missing in the schema, and return constraints for generated columns existing in\n   * the schema.\n   *\n   * @param deltaLog The table's [[DeltaLog]] used for logging.\n   * @param queryExecution Used to check whether the original query is a streaming query or not.\n   * @param schema Table schema.\n   * @param data The data to be written into the table.\n   * @param nullAsDefault If true, use null literal as the default value for missing columns.\n   * @return The data with potentially additional default expressions projected and constraints\n   *         from generated columns if any. This includes IDENTITY column names for which we\n   *         should track the high water marks.\n   */\n  def addDefaultExprsOrReturnConstraints(\n      deltaLog: DeltaLog,\n      protocol: Protocol,\n      queryExecution: QueryExecution,\n      schema: StructType,\n      data: DataFrame,\n      nullAsDefault: Boolean): (DataFrame, Seq[Constraint], Set[String]) = {\n    val topLevelOutputNames = CaseInsensitiveMap(data.schema.map(f => f.name -> f).toMap)\n    lazy val metadataOutputNames = CaseInsensitiveMap(schema.map(f => f.name -> f).toMap)\n    val constraints = mutable.ArrayBuffer[Constraint]()\n    // Column names for which we will track high water marks.\n    val track = mutable.Set[String]()\n    val generatedColumnsValidateMode =\n      GeneratedColumnValidateOnWriteMode.fromConf(data.sparkSession.sessionState.conf)\n    generatedColumnsValidateMode match {\n      case GeneratedColumnValidateOnWriteMode.LOG_ONLY |\n           GeneratedColumnValidateOnWriteMode.ASSERT =>\n        try {\n          val startTime = System.nanoTime()\n          GeneratedColumn.validateGeneratedColumns(data.sparkSession, schema)\n          val durationMs =\n            duration.NANOSECONDS.toMillis(System.nanoTime() - startTime)\n          logInfo(\n            log\"Validated Generated Column expressions on table \" +\n            log\"${MDC(DeltaLogKeys.TABLE_ID, deltaLog.unsafeVolatileTableId)} \" +\n            log\"in ${MDC(DeltaLogKeys.TIME_MS, durationMs)} ms\"\n          )\n        } catch {\n          case NonFatal(e) =>\n            val errorClassName = e match {\n              case deltaException: DeltaAnalysisException => deltaException.getErrorClass\n              case _ => e.getClass\n            }\n            recordDeltaEvent(\n              deltaLog,\n              \"delta.generatedColumns.writeValidationFailure\",\n              data = Map(\n                \"errorClassName\" -> errorClassName,\n                \"errorMessage\" -> e.getMessage\n              )\n            )\n            if (generatedColumnsValidateMode == GeneratedColumnValidateOnWriteMode.ASSERT) {\n              throw e\n            }\n        }\n      case GeneratedColumnValidateOnWriteMode.OFF =>\n    }\n    var selectExprs = schema.flatMap { f =>\n      GeneratedColumn.getGenerationExpression(f) match {\n        case Some(expr) if GeneratedColumn.satisfyGeneratedColumnProtocol(protocol) =>\n          if (topLevelOutputNames.contains(f.name)) {\n            val column = SchemaUtils.fieldToColumn(f)\n            // Add a constraint to make sure the value provided by the user is the same as the value\n            // calculated by the generation expression.\n            constraints += Constraints.Check(s\"Generated Column\", EqualNullSafe(column.expr, expr))\n            Some(column)\n          } else {\n            Some(Column(expr).alias(f.name))\n          }\n        case _ =>\n          if (isIdentityColumn(f)) {\n            if (topLevelOutputNames.contains(f.name)) {\n              Some(SchemaUtils.fieldToColumn(f))\n            } else {\n              // Track high water marks for generated IDENTITY values.\n              track += f.name\n              Some(IdentityColumn.createIdentityColumnGenerationExprAsColumn(f))\n            }\n          } else {\n            if (topLevelOutputNames.contains(f.name) ||\n                !data.sparkSession.conf.get(DeltaSQLConf.GENERATED_COLUMN_ALLOW_NULLABLE)) {\n              Some(SchemaUtils.fieldToColumn(f))\n            } else {\n              // we only want to consider columns that are in the data's schema or are generated\n              // to allow DataFrame with null columns to be written.\n              // The actual check for nullability on data is done in the DeltaInvariantCheckerExec\n              getDefaultValueExprOrNullLit(f, nullAsDefault).map(Column(_))\n            }\n          }\n      }\n    }\n    val cdcSelectExprs = CDCReader.CDC_COLUMNS_IN_DATA.flatMap { cdcColumnName =>\n      topLevelOutputNames.get(cdcColumnName).flatMap { cdcField =>\n        if (metadataOutputNames.contains(cdcColumnName)) {\n          // The column is in the table schema. It's not a CDC auto generated column. Skip it since\n          // it's already in `selectExprs`.\n          None\n        } else {\n          // The column is not in the table schema,\n          // so it must be a column generated by CDC. Adding it back as it's not in `selectExprs`.\n          Some(SchemaUtils.fieldToColumn(cdcField).alias(cdcField.name))\n        }\n      }\n    }\n    selectExprs = selectExprs ++ cdcSelectExprs\n\n    val rowIdExprs = data.queryExecution.analyzed.output\n      .filter(RowId.RowIdMetadataAttribute.isRowIdColumn)\n      .map(Column(_))\n    selectExprs = selectExprs ++ rowIdExprs\n\n    val rowCommitVersionExprs = data.queryExecution.analyzed.output\n      .filter(RowCommitVersion.MetadataAttribute.isRowCommitVersionColumn)\n      .map(Column(_))\n    selectExprs = selectExprs ++ rowCommitVersionExprs\n\n    val newData = queryExecution match {\n      case incrementalExecution: IncrementalExecution =>\n        DeltaStreamUtils.selectFromStreamingDataFrame(incrementalExecution, data, selectExprs: _*)\n      case _ => data.select(selectExprs: _*)\n    }\n    recordDeltaEvent(deltaLog, \"delta.generatedColumns.write\")\n    (newData, constraints.toSeq, track.toSet)\n  }\n\n  // Removes the default expressions properties from the schema. If `keepGeneratedColumns` is\n  // true, generated column expressions are kept. If `keepIdentityColumns` is true, IDENTITY column\n  // properties are kept.\n  def removeDefaultExpressions(\n      schema: StructType,\n      keepGeneratedColumns: Boolean = false,\n      keepIdentityColumns: Boolean = false): StructType = {\n    var updated = false\n    val updatedSchema = schema.map { field =>\n      if (!keepGeneratedColumns && GeneratedColumn.isGeneratedColumn(field)) {\n        updated = true\n        val newMetadata = new MetadataBuilder()\n          .withMetadata(field.metadata)\n          .remove(DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY)\n          .build()\n        field.copy(metadata = newMetadata)\n      } else if (!keepIdentityColumns && isIdentityColumn(field)) {\n        updated = true\n        val newMetadata = new MetadataBuilder()\n          .withMetadata(field.metadata)\n          .remove(DeltaSourceUtils.IDENTITY_INFO_ALLOW_EXPLICIT_INSERT)\n          .remove(DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK)\n          .remove(DeltaSourceUtils.IDENTITY_INFO_START)\n          .remove(DeltaSourceUtils.IDENTITY_INFO_STEP)\n          .build()\n        field.copy(metadata = newMetadata)\n      } else {\n        field\n      }\n    }\n    if (updated) {\n      StructType(updatedSchema)\n    } else {\n      schema\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/CommittedTransaction.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, CommitInfo}\nimport org.apache.spark.sql.delta.hooks.PostCommitHook\n\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * Represents a successfully committed transaction.\n *\n * This class encapsulates all relevant information about a transaction that has been successfully\n * committed. The main usage of this class is in running the post-commit hooks.\n *\n * @param txnId                 the unique identifier of the committed transaction.\n * @param deltaLog              the [[DeltaLog]] instance for the table the transaction\n *                              committed on.\n * @param catalogTable          the catalog table at the start of the transaction for the\n *                              committed table.\n * @param readSnapshot          the snapshot of the table at the time of the transaction's read.\n * @param committedVersion      the version of the table after the txn committed.\n * @param committedActions      the actions that were committed in this transaction.\n * @param postCommitSnapshot    the snapshot of the table after the txn successfully committed.\n *                              NOTE: This may not match the committedVersion, if racing\n *                              commits were written while the snapshot was computed.\n * @param postCommitHooks       the list of post-commit hooks to run after the commit.\n * @param txnExecutionTimeMs    the time taken to execute the transaction.\n * @param needsCheckpoint       whether a checkpoint is needed after the commit.\n * @param partitionsAddedToOpt  the partitions that this txn added new files to.\n * @param isBlindAppend         whether this transaction was a blind append.\n */\ncase class CommittedTransaction(\n    txnId: String,\n    deltaLog: DeltaLog,\n    catalogTable: Option[CatalogTable],\n    readSnapshot: Snapshot,\n    committedVersion: Long,\n    committedActions: Seq[Action],\n    postCommitSnapshot: Snapshot,\n    postCommitHooks: Seq[PostCommitHook],\n    txnExecutionTimeMs: Long,\n    needsCheckpoint: Boolean,\n    partitionsAddedToOpt: Option[mutable.HashSet[Map[String, String]]],\n    isBlindAppend: Boolean\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ConcurrencyHelpers.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.concurrent.duration._\n\nobject ConcurrencyHelpers {\n  /**\n   * Keep checking if `check` returns `true` until it's the case or `waitTime` expires.\n   *\n   * Return `true` when the `check` returned `true`, and `false` if `waitTime` expired.\n   *\n   * Note: This function is used as a helper function for the Concurrency Testing framework,\n   * and should not be used in production code. Production code should not use polling\n   * and should instead use signalling to coordinate.\n   */\n  def busyWaitFor(\n      check: => Boolean,\n      waitTime: FiniteDuration): Boolean = {\n    val DEFAULT_SLEEP_TIME: Duration = 10.millis\n    val deadline = waitTime.fromNow\n\n    do {\n      if (check) {\n        return true\n      }\n      val sleepTimeMs = DEFAULT_SLEEP_TIME.min(deadline.timeLeft).toMillis\n      Thread.sleep(sleepTimeMs)\n    } while (deadline.hasTimeLeft())\n    false\n  }\n\n  def withOptimisticTransaction[T](\n    activeTransaction: Option[OptimisticTransaction])(block: => T): T = {\n    if (activeTransaction.isDefined) {\n      OptimisticTransaction.withActive(activeTransaction.get) {\n        block\n      }\n    } else {\n      block\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ConflictChecker.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DeltaOperations.{OP_SET_TBLPROPERTIES, ROW_TRACKING_BACKFILL_OPERATION_NAME, ROW_TRACKING_UNBACKFILL_OPERATION_NAME}\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.DeltaSparkPlanUtils.CheckDeterministicOptions\nimport org.apache.spark.sql.delta.util.FileNames\nimport io.delta.storage.commit.UpdatedActions\nimport org.apache.hadoop.fs.FileStatus\n\nimport org.apache.spark.internal.{MDC, MessageWithContext}\nimport org.apache.spark.sql.{DataFrame, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Expression, ExpressionSet, Or}\nimport org.apache.spark.sql.types.{Metadata => FieldMetadata, MetadataBuilder, StructType}\n\n/**\n * A class representing different attributes of current transaction needed for conflict detection.\n *\n * @param readPredicates predicates by which files have been queried by the transaction\n * @param readFiles files that have been seen by the transaction\n * @param readWholeTable whether the whole table was read during the transaction\n * @param readAppIds appIds that have been seen by the transaction\n * @param metadata table metadata for the transaction\n * @param actions delta log actions that the transaction wants to commit\n * @param readSnapshot read [[Snapshot]] used for the transaction\n * @param commitInfo [[CommitInfo]] for the commit\n */\nprivate[delta] case class CurrentTransactionInfo(\n    val txnId: String,\n    val readPredicates: Vector[DeltaTableReadPredicate],\n    val readFiles: Set[AddFile],\n    val readWholeTable: Boolean,\n    val readAppIds: Set[String],\n    val metadata: Metadata,\n    val protocol: Protocol,\n    val actions: Seq[Action],\n    val readSnapshot: Snapshot,\n    val commitInfo: Option[CommitInfo],\n    val readRowIdHighWatermark: Long,\n    val catalogTable: Option[CatalogTable],\n    val domainMetadata: Seq[DomainMetadata],\n    val op: DeltaOperations.Operation) {\n\n  /**\n   * Final actions to commit - including the [[CommitInfo]] which should always come first so we can\n   * extract it easily from a commit without having to parse an arbitrarily large file.\n   *\n   * TODO: We might want to cluster all non-file actions at the front, for similar reasons.\n   */\n  lazy val finalActionsToCommit: Seq[Action] = commitInfo ++: actions\n\n  private var newMetadata: Option[Metadata] = None\n\n  actions.foreach {\n    case m: Metadata => newMetadata = Some(m)\n    case _ => // do nothing\n  }\n  def getUpdatedActions(\n      oldMetadata: Metadata,\n      oldProtocol: Protocol): UpdatedActions = {\n    new UpdatedActions(commitInfo.get, metadata, protocol, oldMetadata, oldProtocol)\n  }\n\n  /** Whether this transaction wants to make any [[Metadata]] update */\n  lazy val metadataChanged: Boolean = newMetadata.nonEmpty\n\n  /**\n   * Partition schema corresponding to the read snapshot for this transaction.\n   * NOTE: In conflict detection, we should be careful around whether we want to use the new schema\n   * which this txn wants to update OR the old schema from the read snapshot.\n   * e.g. the ConcurrentAppend check makes sure that no new files have been added concurrently\n   * that this transaction should have read. So this should use the read snapshot partition schema\n   * and not the new partition schema which this txn is introducing. Using the new schema can cause\n   * issues.\n   */\n  val partitionSchemaAtReadTime: StructType = readSnapshot.metadata.partitionSchema\n\n  // Whether this is a row tracking backfill transaction or not.\n  val isRowTrackingBackfillTxn = op.name == ROW_TRACKING_BACKFILL_OPERATION_NAME\n  val isRowTrackingUnBackfillTxn = op.name == ROW_TRACKING_UNBACKFILL_OPERATION_NAME\n\n  def isConflict(winningTxn: SetTransaction): Boolean = readAppIds.contains(winningTxn.appId)\n}\n\n/**\n * Summary of the Winning commit against which we want to check the conflict\n * @param actions - delta log actions committed by the winning commit\n * @param fileStatus - descriptor for the commit file\n * @param readTimeMs - time taken to read the commit file\n */\nprivate[delta] class WinningCommitSummary(\n      val actions: Seq[Action],\n      val fileStatus: FileStatus,\n      val readTimeMs: Long) {\n\n  val commitVersion: Long = FileNames.deltaVersion(fileStatus)\n  val commitFileTimestamp: Long = fileStatus.getModificationTime\n  val metadataUpdates: Seq[Metadata] = actions.collect { case a: Metadata => a }\n  val appLevelTransactions: Seq[SetTransaction] = actions.collect { case a: SetTransaction => a }\n  val protocol: Option[Protocol] = actions.collectFirst { case a: Protocol => a }\n  val commitInfo: Option[CommitInfo] = actions.collectFirst { case a: CommitInfo => a }.map(\n    ci => ci.copy(version = Some(commitVersion)))\n  // Whether this is a row tracking backfill transaction or not.\n  val isRowTrackingBackfillTxn =\n    commitInfo.exists(_.operation == ROW_TRACKING_BACKFILL_OPERATION_NAME)\n  val isRowTrackingUnBackfillTxn =\n    commitInfo.exists(_.operation == ROW_TRACKING_UNBACKFILL_OPERATION_NAME)\n  val removedFiles: Seq[RemoveFile] = actions.collect { case a: RemoveFile => a }\n  val addedFiles: Seq[AddFile] = actions.collect { case a: AddFile => a }\n  // This is used in resolveRowTrackingBackfillConflicts.\n  lazy val addedFilePathToActionMap: Map[String, AddFile] =\n    addedFiles.map(af => (af.path, af)).toMap\n  val isBlindAppendOption: Option[Boolean] = commitInfo.flatMap(_.isBlindAppend)\n  val blindAppendAddedFiles: Seq[AddFile] = if (isBlindAppendOption.getOrElse(false)) {\n    addedFiles\n  } else {\n    Seq()\n  }\n  val changedDataAddedFiles: Seq[AddFile] = if (isBlindAppendOption.getOrElse(false)) {\n    Seq()\n  } else {\n    addedFiles\n  }\n  val onlyAddFiles: Boolean = actions.collect { case f: FileAction => f }\n    .forall(_.isInstanceOf[AddFile])\n\n  // This indicates this commit contains metadata action that is solely for the purpose for\n  // updating IDENTITY high water marks. This is used by [[ConflictChecker]] to avoid certain\n  // conflict in [[checkNoMetadataUpdates]].\n  val identityOnlyMetadataUpdate = DeltaCommitTag\n    .getTagValueFromCommitInfo(commitInfo, DeltaSourceUtils.IDENTITY_COMMITINFO_TAG)\n    .exists(_.toBoolean)\n}\n\nobject WinningCommitSummary {\n\n  /**\n   * Read a commit file and create the [[WinningCommitSummary]].\n   */\n  def createFromFileStatus(\n      deltaLog: DeltaLog,\n      fileStatus: FileStatus): WinningCommitSummary = {\n    val startTimeNs = System.nanoTime()\n\n    val actions = deltaLog.store.read(\n      fileStatus,\n      deltaLog.newDeltaHadoopConf()\n    ).map(Action.fromJson)\n\n    val readTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)\n\n    new WinningCommitSummary(\n      actions = actions,\n      fileStatus = fileStatus,\n      readTimeMs = readTimeMs\n    )\n  }\n}\n\nprivate[delta] class ConflictChecker(\n    spark: SparkSession,\n    initialCurrentTransactionInfo: CurrentTransactionInfo,\n    winningCommitSummary: WinningCommitSummary,\n    isolationLevel: IsolationLevel)\n  extends DeltaLogging with ConflictCheckerPredicateElimination {\n\n  protected val winningCommitVersion = winningCommitSummary.commitVersion\n  protected val startTimeMs = System.currentTimeMillis()\n  protected val timingStats = mutable.HashMap[String, Long]()\n  protected val deltaLog = initialCurrentTransactionInfo.readSnapshot.deltaLog\n\n  protected var currentTransactionInfo: CurrentTransactionInfo = initialCurrentTransactionInfo\n\n  protected def recordSkippedPhase(phase: String): Unit = timingStats += phase -> 0\n\n  /**\n   * This function checks conflict of the `initialCurrentTransactionInfo` against the\n   * `winningCommitVersion` and returns an updated [[CurrentTransactionInfo]] that represents\n   * the transaction as if it had started while reading the `winningCommitVersion`.\n   */\n  def checkConflicts(): CurrentTransactionInfo = {\n    // Add time to read commit in the metrics.\n    recordTime(\"initialize-old-commit\", winningCommitSummary.readTimeMs)\n\n    // Check early the protocol and metadata compatibility that is required for subsequent\n    // file-level checks.\n    checkProtocolCompatibility()\n    if (spark.conf.get(DeltaSQLConf.FEATURE_ENABLEMENT_CONFLICT_RESOLUTION_ENABLED)) {\n      attemptToResolveMetadataConflicts()\n    } else {\n      checkNoMetadataUpdates()\n    }\n    checkIfDomainMetadataConflict()\n\n    // Perform cheap check for transaction dependencies before we start checks files.\n    checkForUpdatedApplicationTransactionIdsThatCurrentTxnDependsOn()\n\n    resolveRowTrackingBackfillConflicts()\n    resolveRowTrackingUnBackfillConflicts()\n    // Row Tracking reconciliation. We perform this before the file checks to ensure that\n    // no files have duplicate row IDs and avoid interacting with files that don't comply with\n    // the protocol.\n    reassignOverlappingRowIds()\n    reassignRowCommitVersions()\n\n    // Update the table version in newly added type widening metadata.\n    updateTypeWideningMetadata()\n\n    // Data file checks.\n    checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn()\n    checkForDeletedFilesAgainstCurrentTxnReadFiles()\n    checkForDeletedFilesAgainstCurrentTxnDeletedFiles()\n    resolveTimestampOrderingConflicts()\n\n    logMetrics()\n    currentTransactionInfo\n  }\n\n  /**\n   * Asserts that the client is up to date with the protocol and is allowed to read and write\n   * against the protocol set by the committed transaction.\n   */\n  protected def checkProtocolCompatibility(): Unit = {\n    if (winningCommitSummary.protocol.nonEmpty) {\n      winningCommitSummary.protocol.foreach { p =>\n        deltaLog.protocolRead(p)\n        deltaLog.protocolWrite(p)\n        currentTransactionInfo = currentTransactionInfo.copy(protocol = p)\n      }\n      if (currentTransactionInfo.actions.exists(_.isInstanceOf[Protocol])) {\n        throw DeltaErrors.protocolChangedException(winningCommitSummary.commitInfo)\n      }\n      // When a protocol downgrade occurs all other interleaved txns abort. Note, that in the\n      // opposite scenario, when the current transaction is the protocol downgrade, we resolve\n      // the conflict and proceed with the downgrade. This is because a protocol downgrade would\n      // be hard to succeed in concurrent workloads. On the other hand, a protocol downgrade is\n      // a rare event and thus not that disruptive if other concurrent transactions fail.\n      val winningProtocol = winningCommitSummary.protocol.get\n      val readProtocol = currentTransactionInfo.readSnapshot.protocol\n      val isWinnerDroppingFeatures = TableFeature.isProtocolRemovingFeatures(\n        newProtocol = winningProtocol,\n        oldProtocol = readProtocol)\n      if (isWinnerDroppingFeatures) {\n        throw DeltaErrors.protocolChangedException(winningCommitSummary.commitInfo)\n      }\n\n      if (spark.conf.get(\n          DeltaSQLConf.DELTA_CONFLICT_CHECKER_ENFORCE_FEATURE_ENABLEMENT_VALIDATION)) {\n        // Check if the winning protocol adds features that should fail concurrent transactions at\n        // upgrade. These features are identified by the `failConcurrentTransactionsAtUpgrade`\n        // method returning true. These features impose write-time requirements that need to be\n        // respected by all writers beyond the protocol upgrade, and there's no custom feature\n        // specific conflict resolution logic below to be able to have the current transaction meet\n        // these requirements on-the-fly.\n        val winningTxnAddedFeatures = TableFeature.getAddedFeatures(winningProtocol, readProtocol)\n\n        val winningTxnUnsafeAddedFeatures = winningTxnAddedFeatures\n          .filter(_.failConcurrentTransactionsAtUpgrade)\n        if (winningTxnUnsafeAddedFeatures.nonEmpty) {\n          throw DeltaErrors.protocolChangedException(winningCommitSummary.commitInfo)\n        }\n      }\n    }\n    // When the winning transaction does not change the protocol but the losing txn is\n    // a protocol downgrade, we re-validate the invariants of the removed feature.\n    // Furthermore, when dropping with the fast drop feature we need to adjust\n    // requireCheckpointProtectionBeforeVersion.\n    // TODO: only revalidate against the snapshot of the last interleaved txn.\n    val newProtocol = currentTransactionInfo.protocol\n    val readProtocol = currentTransactionInfo.readSnapshot.protocol\n    if (TableFeature.isProtocolRemovingFeatures(newProtocol, readProtocol)) {\n      // Feature specific conflict resolution logic.\n      if (TableFeature.isFeatureDropped(newProtocol, readProtocol, RowTrackingFeature)) {\n        currentTransactionInfo = resolveRowTrackingUnBackfillConflicts(\n          currentTransactionInfo, winningCommitSummary)\n      } else {\n        val winningSnapshot = deltaLog.getSnapshotAt(\n          winningCommitSummary.commitVersion,\n          catalogTableOpt = currentTransactionInfo.catalogTable)\n        val isDowngradeCommitValid = TableFeature.validateFeatureRemovalAtSnapshot(\n          newProtocol = newProtocol,\n          oldProtocol = readProtocol,\n          table = DeltaTableV2(\n            spark = spark,\n            path = deltaLog.dataPath,\n            catalogTable = currentTransactionInfo.catalogTable),\n          snapshot = winningSnapshot)\n        if (!isDowngradeCommitValid) {\n          throw DeltaErrors.dropTableFeatureConflictRevalidationFailed(\n            winningCommitSummary.commitInfo)\n        }\n      }\n      // When the current transaction is removing a feature and CheckpointProtectionTableFeature\n      // is enabled, the current transaction will set the requireCheckpointProtectionBeforeVersion\n      // table property to the version of the current transaction.\n      // So we need to update it after resolving conflicts with winning transactions.\n      if (newProtocol.isFeatureSupported(CheckpointProtectionTableFeature) &&\n          TableFeature.isProtocolRemovingFeatureWithHistoryProtection(newProtocol, readProtocol)) {\n        val newVersion = winningCommitVersion + 1L\n        val newMetadata = CheckpointProtectionTableFeature.metadataWithCheckpointProtection(\n          currentTransactionInfo.metadata, newVersion)\n        val newActions = currentTransactionInfo.actions.collect {\n          // Sanity check.\n          case m: Metadata if m != currentTransactionInfo.metadata =>\n            recordDeltaEvent(\n              deltaLog = currentTransactionInfo.readSnapshot.deltaLog,\n              opType = \"dropFeature.conflictCheck.metadataMismatch\",\n              data = Map(\n                \"transactionInfoMetadata\" -> currentTransactionInfo.metadata,\n                \"actionMetadata\" -> m))\n            CheckpointProtectionTableFeature.metadataWithCheckpointProtection(m, newVersion)\n          case _: Metadata => newMetadata\n          case a => a\n        }\n        currentTransactionInfo = currentTransactionInfo.copy(\n          metadata = newMetadata, actions = newActions)\n      }\n    }\n  }\n\n  /**\n   * RowTrackingBackfill (or backfill for short for this function) is a special operation that\n   * materializes and recommits all existing files in table using one or several commits to ensure\n   * that every AddFile has a base row ID and a default row commit version. When enabling\n   * row tracking on an existing table, the following occurs:\n   *    1. (If necessary) Protocol upgrade + Table Feature Support is added\n   *    2. RowTrackingBackfill commit(s)\n   *    3. Table property and metadata are updated.\n   * RowTrackingBackfill does not do any data change. It doesn't matter whether a file is\n   * recommitted after the table feature support from Backfill or some other concurrent transaction;\n   * every AddFile just needs to have a base row ID and a default row commit version somehow.\n   * However, correctness issues can arise if we don't do the checks in this method.\n   *\n   * Check that RowTrackingBackfill is not resurrecting files that were removed concurrently and\n   * that an AddFile and its corresponding RemoveFile have the same base row ID and\n   * default row commit version. To do this, we:\n   *    1. remove AddFile's from a backfill commit if an AddFile or a RemoveFile with the same path\n   *       was added in the winning concurrent transactions. Files in a winning transaction can be\n   *       removed from backfill because they were already re-committed.\n   *    2. copy over base row IDs and default row commit versions if the current transaction re-adds\n   *       or delete an AddFile with the same path as an Addfile from a winning backfill commit.\n   */\n  private def resolveRowTrackingBackfillConflicts(): Unit = {\n    // If row tracking is not supported, there can be no backfill commit.\n    if (!RowTracking.isSupported(currentTransactionInfo.protocol)) {\n      assert(!currentTransactionInfo.isRowTrackingBackfillTxn)\n      assert(!winningCommitSummary.isRowTrackingBackfillTxn)\n      return\n    }\n\n    val timerPhaseName = \"checked-row-tracking-backfill\"\n    if (currentTransactionInfo.isRowTrackingBackfillTxn) {\n      recordTime(timerPhaseName) {\n        // Any winning commit seen by backfill must have row IDs and row commit versions, because\n        // `reassignOverlappingRowIds` will add a base row ID and `reassignRowCommitVersions`\n        // will add a default row commit versions to all files. So we don't need\n        // Backfill to commit the same file again.\n        val filePathsToRemoveFromBackfill = winningCommitSummary.actions.collect {\n          case a: AddFile => a.path\n          case r: RemoveFile => r.path\n        }.toSet\n\n        // Remove files from this Backfill commit if they were removed or re-committed by\n        // a concurrent winning txn.\n        if (filePathsToRemoveFromBackfill.nonEmpty) {\n          // We keep the Row Tracking high-water mark action here but it might\n          // be outdated since the winning commit could have increased the high-water mark.\n          // We will reassign the current transaction's high water-mark if that is\n          // the case, in `reassignOverlappingRowIds` which is called after\n          // `resolveRowTrackingBackfillConflicts` in `checkConflicts`.\n          val newActions = currentTransactionInfo.actions.filterNot {\n            case a: AddFile => filePathsToRemoveFromBackfill.contains(a.path)\n            case d: DomainMetadata if RowTrackingMetadataDomain.isSameDomain(d) => false\n            case _ => throw new IllegalStateException(\n              \"RowTrackingBackfill commit has an unexpected action\")\n          }\n\n          val newReadFiles = currentTransactionInfo.readFiles.filterNot(\n            a => filePathsToRemoveFromBackfill.contains(a.path))\n\n          currentTransactionInfo = currentTransactionInfo.copy(\n            actions = newActions, readFiles = newReadFiles)\n        }\n      }\n    }\n\n    if (winningCommitSummary.isRowTrackingBackfillTxn) {\n      recordTime(timerPhaseName) {\n        val backfillActionMap = winningCommitSummary.addedFilePathToActionMap\n        // Copy over the base row ID and default row commit version assigned so that the AddFiles\n        // and RemoveFiles have matching base row ID and default row commit version.\n        // If an AddFile is re-committed, it should have the same base row ID and\n        // default row commit version as the one assigned by Backfill.\n        val newActions = currentTransactionInfo.actions.map {\n          case a: AddFile if backfillActionMap.contains(a.path) =>\n            val backfillAction = backfillActionMap(a.path)\n            a.copy(baseRowId = backfillAction.baseRowId,\n              defaultRowCommitVersion = backfillAction.defaultRowCommitVersion)\n          case r: RemoveFile if backfillActionMap.contains(r.path) =>\n            val backfillAction = backfillActionMap(r.path)\n            r.copy(baseRowId = backfillAction.baseRowId,\n              defaultRowCommitVersion = backfillAction.defaultRowCommitVersion)\n          case a => a\n        }\n        currentTransactionInfo = currentTransactionInfo.copy(actions = newActions)\n      }\n    }\n  }\n\n  /**\n   * Row tracking unbackfill is an operation that removes row tracking metadata from the table.\n   * This is achieved by recommiting existing add files without base row ID and default\n   * row commit version. The operation is invoked as part of the cleanup process when dropping\n   * the row tracking feature from the table.\n   *\n   * In general, Delta writers should never generate baseRowIds while\n   * `delta.rowTrackingSuspended` is enabled. However, the delta protocol does not enforce\n   * the config and as a result third party writers may not respect it. The unbackfill conflict\n   * resolver unbackfills the addFiles of the winning commits to compensate for this.\n   */\n  private def resolveRowTrackingUnBackfillConflicts(): Unit = {\n    // If row tracking is not supported, there can be no unbackfill commit.\n    if (!RowTracking.isSupported(currentTransactionInfo.protocol)) {\n      assert(!currentTransactionInfo.isRowTrackingUnBackfillTxn)\n      assert(!winningCommitSummary.isRowTrackingUnBackfillTxn)\n      return\n    }\n\n    if (!currentTransactionInfo.isRowTrackingUnBackfillTxn) {\n      return\n    }\n    // Third party writers might not use the same operation name for backfill.\n    // In that case we will proceed to conflict resolution.\n    if (winningCommitSummary.isRowTrackingBackfillTxn) {\n      throw DeltaErrors.rowTrackingBackfillRunningConcurrentlyWithUnbackfill()\n    }\n\n    val timerPhaseName = \"checked-row-tracking-unbackfill\"\n    recordTime(timerPhaseName) {\n      currentTransactionInfo = resolveRowTrackingUnBackfillConflicts(\n        currentTransactionInfo,\n        winningCommitSummary)\n    }\n  }\n\n  /**\n   * Resolve conflicts by cleaning up addFiles of winning commits. Furthermore, make sure\n   * sure that removed files are not resurrected.\n   */\n  private def resolveRowTrackingUnBackfillConflicts(\n      currentTransactionInfo: CurrentTransactionInfo,\n      winningCommitSummary: WinningCommitSummary): CurrentTransactionInfo = {\n\n    // Unbackfill new AddFiles. This has the advantage that will cleanup commits\n    // from third party writers that do not respect `delta.rowTrackingSuspended`.\n    val (pathsToRemoveFromUnBackfill, filesToAddToUnBackfill) =\n      winningCommitSummary.actions.collect {\n        case a: AddFile =>\n          val fileToAdd = if (a.baseRowId.nonEmpty || a.defaultRowCommitVersion.nonEmpty) {\n            Some(a.copy(dataChange = false, baseRowId = None, defaultRowCommitVersion = None))\n          } else {\n            None\n          }\n          (a.path, fileToAdd)\n        case r: RemoveFile => (r.path, None)\n      }.unzip\n    val pathsToRemoveFromUnBackfillSet = pathsToRemoveFromUnBackfill.toSet\n    val filesToAddToUnBackfillSet = filesToAddToUnBackfill.flatten.toSet\n\n    val newActions = currentTransactionInfo.actions.filterNot {\n      case a: AddFile => pathsToRemoveFromUnBackfillSet.contains(a.path)\n      case _ => false\n    } ++ filesToAddToUnBackfillSet\n\n    // We can remove pruned files from the read list. However, we should not add\n    // the new AddFiles because that would cause a conflict, albeit, we already\n    // resolved it.\n    val newReadFiles = currentTransactionInfo.readFiles.filterNot(\n      a => pathsToRemoveFromUnBackfillSet.contains(a.path))\n\n    currentTransactionInfo.copy(actions = newActions, readFiles = newReadFiles)\n  }\n\n  /**\n   * If the winning commit only does row tracking enablement (i.e. set the table property to\n   * true and assigns materialized row tracking column names), we can safely allow the metadata\n   * update not to fail the current txn if we copy over the table property, materialized column\n   * name assignments and correctly tag the current commit as not preserving row tracking data. It\n   * is not possible to preserve row tracking data prior to the table property being set to true\n   * since there is no guarantee of row tracking data being available on all rows.\n   */\n  protected def tryResolveRowTrackingEnablementOnlyMetadataUpdateConflict(): Boolean = {\n    if (RowTracking.canResolveMetadataUpdateConflict(\n        currentTransactionInfo, winningCommitSummary)) {\n      currentTransactionInfo = RowTracking.resolveRowTrackingEnablementOnlyMetadataUpdateConflict(\n        currentTransactionInfo, winningCommitSummary)\n      return true\n    }\n    false\n  }\n\n  // scalastyle:off line.size.limit\n  /**\n   * Check if the committed transaction has changed metadata.\n   *\n   * We want to deal with (and optimize for) the case where the winning commit's metadata update is\n   * solely for updating IDENTITY high water marks. In addition, we want to allow a metadata update\n   * that only sets the table property for row tracking enablement to true not to fail concurrent\n   * transactions if the current transaction does not do a metadata update.\n   *\n   * The conflict matrix is as follows:\n   *\n   * |                                               | Winning Metadata (id) | Winning Metadata Row Tracking Enablement Only | Winning Metadata (other) | Winning No Metadata |\n   * | --------------------------------------------- | --------------------- | --------------------------------------------- | ------------------------ | ------------------- |\n   * | Current Metadata (id)                         | Conflict              | Conflict (3)                                  | Conflict                 | No conflict         |\n   * | Current Metadata Row Tracking Enablement Only | Conflict (1)          | Conflict (3)                                  | Conflict                 | No conflict         |\n   * | Current Metadata (other)                      | Conflict (1)          | Conflict (3)                                  | Conflict                 | No conflict         |\n   * | Current No Metadata                           | No conflict (2)       | No conflict (4)                               | Conflict                 | No conflict         |\n   *\n   * The differences in cases (1), (2), (3), and (4) are:\n   * (1) This is a case we could have done something to avoid conflict, e.g., current transaction\n   * adds a column, while winning transaction does blind append that generates IDENTITY values. But\n   * it's not a common case and the change to avoid conflict is non-trivial (we have to somehow\n   * merge the metadata from winning txn and current txn). We decide to not do that and let it\n   * conflict.\n   * (2) This is a case that is more common (e.g., current = delete/update, winning = update high\n   * water mark) and we will not let it conflict here. Note that it might still cause conflict in\n   * other conflict checks.\n   * (3) If the current txn changes the metadata too, we will fail the current txn. While it is\n   * possible to copy over the metadata information, this scenario is unlikely to happen in practice\n   * and properly handling this for the many edge case (e.g current txn sets the table property\n   * to false) is risky.\n   * (4) In a row tracking enablement only metadata update, the only difference with the previous\n   * metadata are the row tracking table property and materialized column names. These metadata\n   * information only affect the preservation of row tracking. If we copy over the new metadata\n   * configurations and mark the current txn as not preserving row tracking, then the current txn\n   * is respecting the metadata update and does not need to fail.\n   *\n   */\n  // scalastyle:on line.size.limit\n  protected def checkNoMetadataUpdates(): Unit = {\n    // If winning commit does not contain metadata update, no conflict.\n    if (winningCommitSummary.metadataUpdates.isEmpty) return\n\n    if (tryResolveRowTrackingEnablementOnlyMetadataUpdateConflict()) {\n      return\n    }\n\n    // The only case in the remaining cases that we will not conflict is winning commit is\n    // identity only metadata update and current commit has no metadata update.\n    val tolerateIdentityOnlyMetadataUpdate = winningCommitSummary.identityOnlyMetadataUpdate &&\n      !currentTransactionInfo.metadataChanged\n\n    if (!tolerateIdentityOnlyMetadataUpdate) {\n      if (winningCommitSummary.identityOnlyMetadataUpdate) {\n        IdentityColumn.logTransactionAbort(deltaLog)\n      }\n      throw DeltaErrors.metadataChangedException(winningCommitSummary.commitInfo)\n    }\n  }\n\n  /**\n   * Attempts to resolve metadata conflicts between the current and winning transactions.\n   * Currently, we only support the resolution of configuration changes. This is achieved with\n   * the use of an allow-list that defines which configuration changes are allowed.\n   *\n   * We primarily focus on feature enablement. Features should be considered on a case-by-case\n   * basis whether they are eligible for white listing. The main consideration is whether\n   * transactions that produce the output before the feature enablement are safe to commit\n   * with the feature enabled. For some features the answer might be simply yes while some other\n   * features might require reconciliation logic at conflict resolution. Features that require\n   * data rewrite for reconciliation are not good candidates for white listing.\n   */\n  protected def attemptToResolveMetadataConflicts(): Unit = {\n    def throwMetadataChangedException(): Unit =\n      throw DeltaErrors.metadataChangedException(winningCommitSummary.commitInfo)\n\n    // If winning commit does not contain metadata update, no conflict.\n    if (winningCommitSummary.metadataUpdates.isEmpty) return\n\n    // Cannot resolve when both transactions have metadata updates.\n    if (currentTransactionInfo.metadataChanged) {\n      if (winningCommitSummary.identityOnlyMetadataUpdate) {\n        IdentityColumn.logTransactionAbort(deltaLog)\n      }\n      throwMetadataChangedException()\n    }\n\n    // Add all special cases here.\n    if (winningCommitSummary.identityOnlyMetadataUpdate) {\n      return\n    }\n\n    val currentMetadata = currentTransactionInfo.metadata\n    val winningCommitMetadata = winningCommitSummary.metadataUpdates.head\n    val propertyNamesDiff = currentMetadata.diffFieldNames(winningCommitMetadata)\n\n    // We only support the resolution of configuration changes at the moment and metadata\n    // only schema changes.\n    if (!propertyNamesDiff.subsetOf(Set(\"configuration\", \"schemaString\"))) {\n      throwMetadataChangedException()\n    }\n\n    // Clear configuration changes.\n    var configurationChanges = ConfigurationChanges(areValid = false)\n    if (propertyNamesDiff.contains(\"configuration\")) {\n      configurationChanges = checkConfigurationChangesForConflicts(\n        currentMetadata, winningCommitMetadata)\n      if (!configurationChanges.areValid) {\n        throwMetadataChangedException()\n      }\n    }\n\n    // Clear schema changes.\n    if (propertyNamesDiff.contains(\"schemaString\")) {\n      if (!checkSchemaChangesForConflicts(currentMetadata, winningCommitMetadata)) {\n        throwMetadataChangedException()\n      }\n    }\n\n    // Metadata changes are accepted. Consolidate them.\n    val rowTrackingEnabled = configurationChanges\n      .addedAndChanged\n      .getOrElse(DeltaConfigs.ROW_TRACKING_ENABLED.key, \"false\")\n      .toBoolean\n    if (rowTrackingEnabled) {\n      currentTransactionInfo = currentTransactionInfo.copy(\n        commitInfo = currentTransactionInfo\n          .commitInfo\n          .map(RowTracking.addRowTrackingNotPreservedTag))\n    }\n\n    currentTransactionInfo = currentTransactionInfo.copy(metadata = winningCommitMetadata)\n  }\n\n  /**\n   * Return type of [[checkConfigurationChangesForConflicts]]. It indicates whether the\n   * configuration changes are valid and provides the details of the changes.\n   */\n  private[delta] case class ConfigurationChanges(\n      areValid: Boolean,\n      removed: Set[String] = Set.empty,\n      added: Map[String, String] = Map.empty,\n      changed: Map[String, String] = Map.empty) {\n    def addedAndChanged : Map[String, String] = added ++ changed\n  }\n\n  /** Allow list for [[checkConfigurationChangesForConflicts]]. */\n  private lazy val metadataConfigurationChangeAllowList: Set[String] = {\n    val rowTrackingAllowList =\n        Set(\n          MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP,\n          MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP,\n          DeltaConfigs.ROW_TRACKING_ENABLED.key)\n\n    // We can suppress column mapping enablement conflict error since we do not need any\n    // data rewrite to reconcile the txns. No metadata is pushed to the parquet\n    // footers. The new schema with all the necessary column metadata is copied over\n    // to the current transaction.\n    val columnMappingAllowList =\n        Set(\n          DeltaConfigs.COLUMN_MAPPING_MODE.key,\n          DeltaConfigs.COLUMN_MAPPING_MAX_ID.key)\n\n    // Resolving a deletion vectors enablement conflict with another transaction is equivalent\n    // of the latter transaction choosing not to generate DVs although DVs are enabled. This\n    // is valid behavior.\n    val dvsAllowList =\n        Set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key)\n\n    rowTrackingAllowList ++ columnMappingAllowList ++ dvsAllowList\n  }\n\n  /**\n   * Validates configuration changes between the current metadata and the winning metadata.\n   * Returns a [[ConfigurationChanges]] object that indicates whether the changes are valid.\n   */\n  protected[delta] def checkConfigurationChangesForConflicts(\n      currentMetadata: Metadata,\n      winningMetadata: Metadata,\n      allowList: Set[String] = metadataConfigurationChangeAllowList): ConfigurationChanges = {\n\n    val currentConf = currentMetadata.configuration\n    val winningConf = winningMetadata.configuration\n    val currentConfKeys = currentConf.keySet\n    val winningConfKeys = winningConf.keySet\n\n    val removedKeys = currentConfKeys -- winningConfKeys\n    val addedKeys = winningConfKeys -- currentConfKeys\n    val changedKeys = currentConfKeys.intersect(winningConfKeys).filter { key =>\n      currentConf(key) != winningConf(key)\n    }\n    val addedAndChangedKeys = addedKeys ++ changedKeys\n\n    def configurationChanges(areValid: Boolean): ConfigurationChanges = {\n      ConfigurationChanges(\n        areValid = areValid,\n        removed = removedKeys,\n        added = addedKeys.map(key => key -> winningConf(key)).toMap,\n        changed = changedKeys.map(key => key -> winningConf(key)).toMap)\n    }\n\n    def INVALID_CONFIGURATION_CHANGES = configurationChanges(areValid = false)\n    def VALID_CONFIGURATION_CHANGES = configurationChanges(areValid = true)\n\n    // Unsetting a configuration is not supported at the moment.\n    if (removedKeys.nonEmpty) {\n      return INVALID_CONFIGURATION_CHANGES\n    }\n\n    // Every added or changed configuration must be in the allow list.\n    if (!addedAndChangedKeys.subsetOf(allowList)) {\n      return INVALID_CONFIGURATION_CHANGES\n    }\n\n    // Schema: Key, value, isNew.\n    val allChanges =\n      addedKeys.map(key => (key, winningConf(key), true)) ++\n      changedKeys.map(key => (key, winningConf(key), false))\n\n    val validChanges = allChanges.map { case (key, value, isNew) =>\n      key match {\n        // Row tracking related configurations.\n        case DeltaConfigs.ROW_TRACKING_ENABLED.key =>\n          isRowTrackingConfigChangeConflictFree(value.toBoolean)\n        case MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP =>\n          areRowTrackingPropertyChangesConflictFree(winningMetadata)\n        case MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP =>\n          areRowTrackingPropertyChangesConflictFree(winningMetadata)\n        // Column mapping related configurations.\n        case DeltaConfigs.COLUMN_MAPPING_MODE.key =>\n          areColumnMappingChangesConflictFree(currentMetadata, winningMetadata)\n        case DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key =>\n          currentTransactionInfo.protocol.isFeatureSupported(DeletionVectorsTableFeature) &&\n            value.toBoolean\n        case _ => true\n      }\n    }\n\n    if (validChanges.contains(false)) {\n      return INVALID_CONFIGURATION_CHANGES\n    }\n\n    VALID_CONFIGURATION_CHANGES\n  }\n\n  protected def isRowTrackingConfigChangeConflictFree(value: Boolean): Boolean = {\n    if (!currentTransactionInfo.protocol.isFeatureSupported(RowTrackingFeature)) {\n      return false\n    }\n    // Currently, we only allow enabling row tracking.\n    value\n  }\n\n  protected def areRowTrackingPropertyChangesConflictFree(winningMetadata: Metadata): Boolean = {\n    winningMetadata\n      .configuration\n      .getOrElse(DeltaConfigs.ROW_TRACKING_ENABLED.key, \"false\")\n      .toBoolean\n  }\n\n  protected def areColumnMappingChangesConflictFree(\n      currentMetadata: Metadata,\n      winningMetadata: Metadata): Boolean = {\n    // Enabling column mapping name mode is the only transition we allow.\n    // Enabling ID mapping on an existing table is generally not allowed.\n    // This should be already blocked by column mapping at an earlier stage.\n    // We add an extra check here for safety.\n    val columnMappingEnabled =\n      currentMetadata.columnMappingMode == NoMapping &&\n      winningMetadata.columnMappingMode == NameMapping\n    if (!columnMappingEnabled) {\n      return false\n    }\n\n    currentTransactionInfo.protocol.isFeatureSupported(ColumnMappingTableFeature)\n  }\n\n  /** Allows key comparison between two sql.types.Metadata objects. */\n  class DeltaFieldMetadataComparator(metadata: FieldMetadata) extends MetadataBuilder {\n    withMetadata(metadata)\n\n    /** Returns a set of added keys by `other`. */\n    def addedKeys(other: DeltaFieldMetadataComparator): Set[String] = {\n      other.getMap.keySet -- getMap.keySet\n    }\n\n    /** Returns a set of removed keys by `other`. */\n    def removedKeys(other: DeltaFieldMetadataComparator): Set[String] = {\n      getMap.keySet -- other.getMap.keySet\n    }\n\n    /** Returns a set of changed keys by `other`. */\n    def changedKeys(other: DeltaFieldMetadataComparator): Set[String] = {\n      getMap.keySet.intersect(other.getMap.keySet).filterNot { key =>\n        val ourValue = getMap(key)\n        val otherValue = other.getMap(key)\n        (ourValue, otherValue) match {\n          case (v0: Array[Long], v1: Array[Long]) => java.util.Arrays.equals(v0, v1)\n          case (v0: Array[Double], v1: Array[Double]) => java.util.Arrays.equals(v0, v1)\n          case (v0: Array[Boolean], v1: Array[Boolean]) => java.util.Arrays.equals(v0, v1)\n          case (v0: Array[AnyRef], v1: Array[AnyRef]) => java.util.Arrays.equals(v0, v1)\n          case (v0, v1) => v0 == v1\n        }\n      }\n    }\n\n    /** Returns a set of keys that were either added, removed or changed by `other`. */\n    def keysWithAnyChanges(other: DeltaFieldMetadataComparator): Set[String] = {\n      removedKeys(other)\n        .union(addedKeys(other))\n        .union(changedKeys(other))\n    }\n  }\n\n  /** Verifies whether any changes between currentMetadata and winningMetadata are valid. */\n  protected def checkSchemaChangesForConflicts(\n      currentMetadata: Metadata,\n      winningMetadata: Metadata): Boolean = {\n\n    val currentSchema = currentMetadata.schema\n    val winningSchema = winningMetadata.schema\n\n    if (currentSchema.fields.length != winningSchema.fields.length) {\n      return false\n    }\n\n    // Currently we only support column mapping metadata changes. If column mapping is not\n    // enabled fail (assumes the method was called because schema changes were detected).\n    val columnMappingEnabled =\n      currentMetadata.columnMappingMode == NoMapping &&\n      winningMetadata.columnMappingMode == NameMapping\n    if (!columnMappingEnabled) {\n      return false\n    }\n\n    val allowedMetadataFields = DeltaColumnMapping.COLUMN_MAPPING_METADATA_KEYS\n\n    currentSchema.fields.zipWithIndex.foreach { case (currentField, index) =>\n      val winningField = winningSchema.fields(index)\n      // Currently we only allow metadata changes.\n      if (currentField.name != winningField.name ||\n          currentField.dataType != winningField.dataType ||\n          currentField.nullable != winningField.nullable) {\n        return false\n      }\n\n      if (currentField.metadata != winningField.metadata) {\n        val currentFieldMetadataComparator = new DeltaFieldMetadataComparator(currentField.metadata)\n        val winningFieldMetadataComparator = new DeltaFieldMetadataComparator(winningField.metadata)\n        val keysWithAnyChanges = currentFieldMetadataComparator\n          .keysWithAnyChanges(winningFieldMetadataComparator)\n\n        // We allow all operations on white listed metadata fields.\n        if (!keysWithAnyChanges.subsetOf(allowedMetadataFields)) {\n          return false\n        }\n      }\n    }\n\n    true\n  }\n\n  /**\n   * Filters the [[files]] list with the partition predicates of the current transaction\n   * and returns the first file that is matching.\n   */\n  protected def getFirstFileMatchingPartitionPredicates(files: Seq[AddFile]): Option[AddFile] = {\n    // Blind appends do not read the table.\n    if (currentTransactionInfo.commitInfo.flatMap(_.isBlindAppend).getOrElse(false)) {\n      assert(currentTransactionInfo.readPredicates.isEmpty)\n      return None\n    }\n\n    // There is no reason to filter files if the table is not partitioned.\n    if (currentTransactionInfo.readWholeTable ||\n        currentTransactionInfo.readSnapshot.metadata.partitionColumns.isEmpty) {\n      return files.headOption\n    }\n\n    import org.apache.spark.sql.delta.implicits._\n    val filesDf = files.toDF(spark)\n\n    spark.conf.get(DeltaSQLConf.DELTA_CONFLICT_DETECTION_WIDEN_NONDETERMINISTIC_PREDICATES) match {\n      case DeltaSQLConf.NonDeterministicPredicateWidening.OFF =>\n        getFirstFileMatchingPartitionPredicatesInternal(\n          filesDf, shouldWidenNonDeterministicPredicates = false, shouldWidenAllUdf = false)\n      case wideningMode =>\n        val fileWithWidening = getFirstFileMatchingPartitionPredicatesInternal(\n          filesDf, shouldWidenNonDeterministicPredicates = true, shouldWidenAllUdf = true)\n\n        fileWithWidening.flatMap { fileWithWidening =>\n          val fileWithoutWidening =\n            getFirstFileMatchingPartitionPredicatesInternal(\n              filesDf, shouldWidenNonDeterministicPredicates = false, shouldWidenAllUdf = false)\n          if (fileWithoutWidening.isEmpty) {\n            // Conflict due to widening of non-deterministic predicate.\n            recordDeltaEvent(deltaLog,\n              opType = \"delta.conflictDetection.partitionLevelConcurrency.\" +\n                \"additionalConflictDueToWideningOfNonDeterministicPredicate\",\n              data = Map(\n                \"wideningMode\" -> wideningMode,\n                \"predicate\" ->\n                  currentTransactionInfo.readPredicates.map(_.partitionPredicate.toString),\n                \"deterministicUDFs\" -> containsDeterministicUDF(\n                  currentTransactionInfo.readPredicates, partitionedOnly = true))\n            )\n          }\n          if (wideningMode == DeltaSQLConf.NonDeterministicPredicateWidening.ON) {\n            Some(fileWithWidening)\n          } else {\n            fileWithoutWidening\n          }\n        }\n    }\n  }\n\n  private def getFirstFileMatchingPartitionPredicatesInternal(\n      filesDf: DataFrame,\n      shouldWidenNonDeterministicPredicates: Boolean,\n      shouldWidenAllUdf: Boolean): Option[AddFile] = {\n\n    def rewritePredicateFn(\n        predicate: Expression,\n        shouldRewriteFilter: Boolean): DeltaTableReadPredicate = {\n      val rewrittenPredicate = if (shouldWidenNonDeterministicPredicates) {\n        val checkDeterministicOptions =\n          CheckDeterministicOptions(allowDeterministicUdf = !shouldWidenAllUdf)\n        eliminateNonDeterministicPredicates(Seq(predicate), checkDeterministicOptions).newPredicates\n      } else {\n        Seq(predicate)\n      }\n      DeltaTableReadPredicate(\n        partitionPredicates = rewrittenPredicate,\n        shouldRewriteFilter = shouldRewriteFilter)\n    }\n\n    // we need to canonicalize the partition predicates per each group of rewrites vs. nonRewrites\n    val canonicalPredicates = currentTransactionInfo.readPredicates\n      .partition(_.shouldRewriteFilter) match {\n        case (rewrites, nonRewrites) =>\n          val canonicalRewrites =\n            ExpressionSet(rewrites.map(_.partitionPredicate)).map(\n              predicate => rewritePredicateFn(predicate, shouldRewriteFilter = true))\n          val canonicalNonRewrites =\n            ExpressionSet(nonRewrites.map(_.partitionPredicate)).map(\n              predicate => rewritePredicateFn(predicate, shouldRewriteFilter = false))\n          canonicalRewrites ++ canonicalNonRewrites\n      }\n\n    import org.apache.spark.sql.delta.implicits._\n    val filesMatchingPartitionPredicates = canonicalPredicates.iterator\n      .flatMap { readPredicate =>\n        val matchingFileOpt = DeltaLog.filterFileList(\n          partitionSchema = currentTransactionInfo.partitionSchemaAtReadTime,\n          files = filesDf,\n          partitionFilters = readPredicate.partitionPredicates,\n          shouldRewritePartitionFilters = readPredicate.shouldRewriteFilter\n        ).as[AddFile].head(1).headOption\n        matchingFileOpt.foreach { f =>\n          logInfo(log\"Partition predicate is matching a file changed by the winning transaction: \" +\n            log\"predicate=${MDC(DeltaLogKeys.DATA_FILTER,\n              readPredicate.partitionPredicates.toVector)}, \" +\n            log\"matchingFile=${MDC(DeltaLogKeys.PATH, f.path)}\")\n        }\n        matchingFileOpt\n      }.take(1).toArray\n\n    filesMatchingPartitionPredicates.headOption\n  }\n\n  /**\n   * RowTrackingBackfill does not do any data change. If backfill is the winning commit, the\n   * current transaction does not need to read its AddFiles -- the exact same AddFiles have\n   * already been read. If the current commit is backfill, it doesn't need to read the AddFiles\n   * added by the winning transaction. Any winning transaction seen by backfill will commit base\n   * row IDs and default row commit versions, since backfill is only done after table feature\n   * support is added. Removing duplicate AddFiles is handled in\n   * [[resolveRowTrackingBackfillConflicts]].\n   *\n   * RowTrackingUnBackfill behaves in a similar way. It does not do any data change. When it is\n   * the winning commit, the current transaction does not need to read its AddFiles. However, when\n   * unbackfill it is the current transaction, it pulls the addFiles added by the winning\n   * transaction and unbackfills them. Again, this is a metadata only change. AddFile deduplication\n   * is handled in [[resolveRowTrackingUnBackfillConflicts]].\n   */\n  protected def skipCheckedAppendsIfExistsRowTrackingBackfillTransaction(): Boolean = {\n    if (winningCommitSummary.isRowTrackingBackfillTxn ||\n        winningCommitSummary.isRowTrackingUnBackfillTxn ||\n        currentTransactionInfo.isRowTrackingBackfillTxn ||\n        currentTransactionInfo.isRowTrackingUnBackfillTxn) {\n      recordSkippedPhase(\"checked-appends\")\n      return true\n    }\n    false\n  }\n\n  /**\n   * Check if the new files added by the already committed transactions should have been read by\n   * the current transaction.\n   */\n  protected def checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn(): Unit = {\n    if (skipCheckedAppendsIfExistsRowTrackingBackfillTransaction()) {\n      return\n    }\n\n    recordTime(\"checked-appends\") {\n      // Fail if new files have been added that the txn should have read.\n      val addedFilesToCheckForConflicts = isolationLevel match {\n        case WriteSerializable if !currentTransactionInfo.metadataChanged =>\n          winningCommitSummary.changedDataAddedFiles // don't conflict with blind appends\n        case Serializable | WriteSerializable =>\n          winningCommitSummary.changedDataAddedFiles ++ winningCommitSummary.blindAppendAddedFiles\n        case SnapshotIsolation =>\n          Seq.empty\n      }\n\n      val fileMatchingPartitionReadPredicates =\n        getFirstFileMatchingPartitionPredicates(addedFilesToCheckForConflicts)\n\n      if (fileMatchingPartitionReadPredicates.nonEmpty) {\n        throw DeltaErrors.concurrentAppendException(\n          winningCommitSummary.commitInfo,\n          getTableNameOrPath,\n          winningCommitVersion,\n          getPrettyPartitionMessage(fileMatchingPartitionReadPredicates.get.partitionValues))\n      }\n    }\n  }\n\n  /**\n   * Check if [[RemoveFile]] actions added by already committed transactions conflicts with files\n   * read by the current transaction.\n   */\n  protected def checkForDeletedFilesAgainstCurrentTxnReadFiles(): Unit = {\n    recordTime(\"checked-deletes\") {\n      // Fail if files have been deleted that the txn read.\n      val readFilePaths = currentTransactionInfo.readFiles.map(\n        f => f.path -> f.partitionValues).toMap\n      val deleteReadOverlap = winningCommitSummary.removedFiles\n        .find(r => readFilePaths.contains(r.path))\n      if (deleteReadOverlap.nonEmpty) {\n        val partitionOpt = getPrettyPartitionMessage(readFilePaths(deleteReadOverlap.get.path))\n        throw DeltaErrors.concurrentDeleteReadException(\n          winningCommitSummary.commitInfo,\n          getTableNameOrPath,\n          winningCommitVersion,\n          partitionOpt)\n      }\n      if (winningCommitSummary.removedFiles.nonEmpty && currentTransactionInfo.readWholeTable) {\n        throw DeltaErrors.concurrentDeleteReadException(\n          winningCommitSummary.commitInfo,\n          getTableNameOrPath,\n          winningCommitVersion,\n          partitionOpt = None)\n      }\n    }\n  }\n\n  /**\n   * Check if [[RemoveFile]] actions added by already committed transactions conflicts with\n   * [[RemoveFile]] actions this transaction is trying to add.\n   */\n  protected def checkForDeletedFilesAgainstCurrentTxnDeletedFiles(): Unit = {\n    recordTime(\"checked-2x-deletes\") {\n      // Fail if a file is deleted twice.\n      val deletedFilePaths = currentTransactionInfo.actions\n        .collect { case r: RemoveFile => r.path -> r.partitionValues }\n        .toMap\n      val deleteOverlap = winningCommitSummary.removedFiles\n        .find(r => deletedFilePaths.contains(r.path))\n      if (deleteOverlap.nonEmpty) {\n        val partitionOpt = getPrettyPartitionMessage(deletedFilePaths(deleteOverlap.get.path))\n        throw DeltaErrors.concurrentDeleteDeleteException(\n          winningCommitSummary.commitInfo,\n          getTableNameOrPath,\n          winningCommitVersion,\n          partitionOpt)\n      }\n    }\n  }\n\n  /**\n   * Checks if the winning transaction corresponds to some AppId on which current transaction\n   * also depends.\n   */\n  protected def checkForUpdatedApplicationTransactionIdsThatCurrentTxnDependsOn(): Unit = {\n    // Fail if the appIds seen by the current transaction has been updated by the winning\n    // transaction i.e. the winning transaction have [[SetTransaction]] corresponding to\n    // some appId on which current transaction depends on. Example - This can happen when\n    // multiple instances of the same streaming query are running at the same time.\n    if (winningCommitSummary.appLevelTransactions.exists(currentTransactionInfo.isConflict(_))) {\n      throw DeltaErrors.concurrentTransactionException(winningCommitSummary.commitInfo)\n    }\n  }\n\n  private lazy val currentTransactionIsReplaceTable: Boolean = currentTransactionInfo.op match {\n    case _: DeltaOperations.ReplaceTable => true\n    case _ => false\n  }\n\n  /**\n   * Checks [[DomainMetadata]] to capture whether the current transaction conflicts with the\n   * winning transaction at any domain.\n   *     1. Accept the current transaction if its set of metadata domains do not overlap with the\n   *        winning transaction's set of metadata domains.\n   *     2. Otherwise, fail the current transaction unless each conflicting domain is associated\n   *        with a table feature that defines a domain-specific way of resolving the conflict.\n   */\n  private def checkIfDomainMetadataConflict(): Unit = {\n    if (!DomainMetadataUtils.domainMetadataSupported(currentTransactionInfo.protocol)) {\n      return\n    }\n    val winningDomainMetadataMap =\n      DomainMetadataUtils.extractDomainMetadatasMap(winningCommitSummary.actions)\n\n    /**\n     * Any new well-known domains that need custom conflict resolution need to add new cases in\n     * below case match clause. E.g.\n     * case MonotonicCounter(value), Some(MonotonicCounter(conflictingValue)) =>\n     *   MonotonicCounter(Math.max(value, conflictingValue))\n     */\n    def resolveConflict(domainMetadataFromCurrentTransaction: DomainMetadata): DomainMetadata =\n      (domainMetadataFromCurrentTransaction,\n        winningDomainMetadataMap.get(domainMetadataFromCurrentTransaction.domain)) match {\n        // No-conflict case.\n        case (domain, None) => domain\n        case (domain, _) if RowTrackingMetadataDomain.isSameDomain(domain) => domain\n        case (_, Some(_)) =>\n          // Any conflict not specifically handled by a previous case must fail the transaction.\n          throw new io.delta.exceptions.ConcurrentTransactionException(\n            s\"A conflicting metadata domain ${domainMetadataFromCurrentTransaction.domain} is \" +\n              \"added.\")\n      }\n\n    val mergedDomainMetadata = mutable.Buffer.empty[DomainMetadata]\n    // Resolve physical [[DomainMetadata]] conflicts (fail on logical conflict).\n    val updatedActions: Seq[Action] = currentTransactionInfo.actions.map {\n      case domainMetadata: DomainMetadata =>\n        val mergedAction = resolveConflict(domainMetadata)\n        mergedDomainMetadata += mergedAction\n        mergedAction\n      case other => other\n    }\n\n\n    // For the REPLACE TABLE command, if domain metadata of a given domain is added for the first\n    // time by the winning transaction, it may need to be marked as removed.\n    val replaceTableRemoveNewDomainMetadataEnabled = spark.conf.get(\n      DeltaSQLConf.DELTA_CONFLICT_DETECTION_ALLOW_REPLACE_TABLE_TO_REMOVE_NEW_DOMAIN_METADATA)\n    val (finalUpdatedActions, finalMergedDomainMetadata) =\n      if (replaceTableRemoveNewDomainMetadataEnabled && currentTransactionIsReplaceTable) {\n        val (domainMetadataActions, nonDomainMetadataActions) =\n          currentTransactionInfo.actions.partition(_.isInstanceOf[DomainMetadata])\n        val updatedDomainMetadataActions = DomainMetadataUtils.handleDomainMetadataForReplaceTable(\n          winningDomainMetadataMap.values.toSeq,\n          domainMetadataActions.map(_.asInstanceOf[DomainMetadata]))\n        ((nonDomainMetadataActions ++ updatedDomainMetadataActions), updatedDomainMetadataActions)\n      } else {\n        (updatedActions, mergedDomainMetadata)\n      }\n\n    currentTransactionInfo = currentTransactionInfo.copy(\n      domainMetadata = finalMergedDomainMetadata.toSeq,\n      actions = finalUpdatedActions)\n  }\n\n  /**\n   * Metadata is recorded in the table schema on type changes. This includes the table version that\n   * the change was made in, which needs to be updated when there's a conflict.\n   */\n  private def updateTypeWideningMetadata(): Unit = {\n    if (!TypeWidening.isEnabled(currentTransactionInfo.protocol, currentTransactionInfo.metadata)) {\n      return\n    }\n    val newActions = currentTransactionInfo.actions.map {\n      case metadata: Metadata =>\n        val updatedSchema = TypeWideningMetadata.updateTypeChangeVersion(\n          schema = metadata.schema,\n          fromVersion = winningCommitVersion,\n          toVersion = winningCommitVersion + 1L)\n        metadata.copy(schemaString = updatedSchema.json)\n      case a => a\n    }\n    currentTransactionInfo = currentTransactionInfo.copy(actions = newActions)\n  }\n\n  /**\n   * Checks whether the Row IDs assigned by the current transaction overlap with the Row IDs\n   * assigned by the winning transaction. I.e. this function checks whether both the winning and the\n   * current transaction assigned new Row IDs. If this the case, then this check assigns new Row IDs\n   * to the new files added by the current transaction so that they no longer overlap.\n   */\n  private def reassignOverlappingRowIds(): Unit = {\n    // The current transaction should only assign Row Ids if they are supported.\n    val currentProtocol = currentTransactionInfo.protocol\n    val currentMetadata = currentTransactionInfo.metadata\n    if (!RowId.isSupported(currentProtocol)) return\n    if (RowTracking.isSuspended(spark, currentMetadata)) return\n\n    val readHighWaterMark = currentTransactionInfo.readRowIdHighWatermark\n\n    // The winning transaction might have bumped the high water mark or not in case it did\n    // not add new files to the table.\n    val winningHighWaterMark = winningCommitSummary.actions.collectFirst {\n      case RowTrackingMetadataDomain(domain) => domain.rowIdHighWaterMark\n    }.getOrElse(readHighWaterMark)\n\n    var highWaterMark = winningHighWaterMark\n    val actionsWithReassignedRowIds = currentTransactionInfo.actions.flatMap {\n      // We should only set missing row IDs and update the row IDs that were assigned by this\n      // transaction, and not the row IDs that were assigned by an earlier transaction and merely\n      // copied over to a new AddFile as part of this transaction. I.e., we should only update the\n      // base row IDs that are larger than the read high watermark.\n      case a: AddFile if !a.baseRowId.exists(_ <= readHighWaterMark) =>\n        val newBaseRowId = highWaterMark + 1L\n        highWaterMark += a.numPhysicalRecords.getOrElse {\n          throw DeltaErrors.rowIdAssignmentWithoutStats\n        }\n        Some(a.copy(baseRowId = Some(newBaseRowId)))\n      // The row ID high water mark will be replaced if it exists.\n      case d: DomainMetadata if RowTrackingMetadataDomain.isSameDomain(d) => None\n      case a => Some(a)\n    }\n    currentTransactionInfo = currentTransactionInfo.copy(\n      // Add row ID high water mark at the front for faster retrieval.\n      actions = RowTrackingMetadataDomain(highWaterMark).toDomainMetadata +:\n        actionsWithReassignedRowIds,\n      readRowIdHighWatermark = winningHighWaterMark)\n  }\n\n  /**\n   * Reassigns default row commit versions to correctly handle the winning transaction.\n   * Concretely:\n   *  1. Reassigns all default row commit versions (of AddFiles in the current transaction) equal to\n   *     the version of the winning transaction to the next commit version.\n   *  2. Assigns all unassigned default row commit versions that do not have one assigned yet\n   *     to handle the row tracking feature being enabled by the winning transaction.\n   */\n  private def reassignRowCommitVersions(): Unit = {\n    if (!RowId.isSupported(currentTransactionInfo.protocol)) return\n    if (RowTracking.isSuspended(spark, currentTransactionInfo.metadata)) return\n\n    val newActions = currentTransactionInfo.actions.map {\n      case a: AddFile if a.defaultRowCommitVersion.contains(winningCommitVersion) =>\n        a.copy(defaultRowCommitVersion = Some(winningCommitVersion + 1L))\n\n      case a: AddFile if a.defaultRowCommitVersion.isEmpty =>\n        // A concurrent transaction has turned on support for Row Tracking.\n        a.copy(defaultRowCommitVersion = Some(winningCommitVersion + 1L))\n\n      case a => a\n    }\n\n    currentTransactionInfo = currentTransactionInfo.copy(actions = newActions)\n  }\n\n  /**\n   * Adjust the current transaction's commit timestamp to account for the winning\n   * transaction's commit timestamp. If this transaction newly enabled ICT, also update\n   * the table properties to reflect the adjusted enablement version and timestamp.\n   */\n  private def resolveTimestampOrderingConflicts(): Unit = {\n    if (!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(currentTransactionInfo.metadata)) {\n      return\n    }\n\n    val winningCommitTimestamp =\n      if (InCommitTimestampUtils.didCurrentTransactionEnableICT(\n              currentTransactionInfo.metadata, currentTransactionInfo.readSnapshot)) {\n        // Since the current transaction enabled inCommitTimestamps, we should use the file\n        // timestamp from the winning transaction as its commit timestamp.\n        winningCommitSummary.commitFileTimestamp\n    } else {\n      // Get the inCommitTimestamp from the winning transaction.\n      CommitInfo.getRequiredInCommitTimestamp(\n        winningCommitSummary.commitInfo, winningCommitVersion.toString)\n    }\n    val currentTransactionTimestamp = CommitInfo.getRequiredInCommitTimestamp(\n      currentTransactionInfo.commitInfo, \"NEW_COMMIT\")\n    // getRequiredInCommitTimestamp will throw an exception if commitInfo is None.\n    val currentTransactionCommitInfo = currentTransactionInfo.commitInfo.get\n    val updatedCommitTimestamp = Math.max(currentTransactionTimestamp, winningCommitTimestamp + 1)\n    val updatedCommitInfo =\n      currentTransactionCommitInfo.copy(inCommitTimestamp = Some(updatedCommitTimestamp))\n    currentTransactionInfo = currentTransactionInfo.copy(commitInfo = Some(updatedCommitInfo))\n    val nextAvailableVersion = winningCommitVersion + 1L\n    val updatedMetadata =\n      InCommitTimestampUtils.getUpdatedMetadataWithICTEnablementInfo(\n        spark,\n        updatedCommitTimestamp,\n        currentTransactionInfo.readSnapshot,\n        currentTransactionInfo.metadata,\n        nextAvailableVersion)\n    updatedMetadata.foreach { updatedMetadata =>\n      currentTransactionInfo = currentTransactionInfo.copy(\n        metadata = updatedMetadata,\n        actions = currentTransactionInfo.actions.map {\n          case _: Metadata => updatedMetadata\n          case other => other\n        }\n      )\n    }\n  }\n\n  /** A helper function for pretty printing a specific partition directory. */\n  protected def getPrettyPartitionMessage(partitionValues: Map[String, String]): Option[String] = {\n    val partitionColumns = currentTransactionInfo.partitionSchemaAtReadTime\n    if (partitionColumns.isEmpty || partitionValues == null) {\n      None\n    } else {\n      Some(\n        partitionColumns.map { field =>\n          s\"${field.name}=${partitionValues(DeltaColumnMapping.getPhysicalName(field))}\"\n        }.mkString(\"[\", \", \", \"]\")\n      )\n    }\n  }\n\n  protected def getTableNameOrPath: String = {\n    val tableName = currentTransactionInfo.catalogTable.map(_.qualifiedName)\n      .getOrElse(currentTransactionInfo.metadata.name)\n    if (tableName != null) {\n      tableName\n    } else {\n      s\"delta.`${currentTransactionInfo.readSnapshot.deltaLog.dataPath}`\"\n    }\n  }\n\n  protected def recordTime[T](phase: String)(f: => T): T = {\n    val startTimeNs = System.nanoTime()\n    val ret = f\n    val timeTakenMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)\n    timingStats += phase -> timeTakenMs\n    ret\n  }\n\n  protected def recordTime(phase: String, timeTakenMs: Long) = {\n    timingStats += phase -> timeTakenMs\n  }\n\n  protected def logMetrics(): Unit = {\n    val totalTimeTakenMs = System.currentTimeMillis() - startTimeMs\n    val timingStr = timingStats.keys.toSeq.sorted.map(k => s\"$k=${timingStats(k)}\").mkString(\",\")\n    logInfo(log\"[\" + logPrefix + log\"] Timing stats against \" +\n      log\"${MDC(DeltaLogKeys.VERSION, winningCommitVersion)} \" +\n      log\"[${MDC(DeltaLogKeys.TIME_STATS, timingStr)}, totalTimeTakenMs: \" +\n      log\"${MDC(DeltaLogKeys.TIME_MS, totalTimeTakenMs)}]\")\n  }\n\n  protected lazy val logPrefix: MessageWithContext = {\n    def truncate(uuid: String): String = uuid.split(\"-\").head\n    log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID,\n      truncate(initialCurrentTransactionInfo.readSnapshot.metadata.id))},\" +\n    log\"txnId=${MDC(DeltaLogKeys.TXN_ID, truncate(initialCurrentTransactionInfo.txnId))}] \"\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ConflictCheckerPredicateElimination.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.util.DeltaSparkPlanUtils\nimport org.apache.spark.sql.delta.util.DeltaSparkPlanUtils.CheckDeterministicOptions\n\nimport org.apache.spark.sql.catalyst.expressions.{And, EmptyRow, Expression, Literal, Or}\nimport org.apache.spark.sql.catalyst.expressions.Literal.TrueLiteral\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\n\nprivate[delta] trait ConflictCheckerPredicateElimination extends DeltaSparkPlanUtils {\n\n  /**\n   * This class represents the state of a expression tree transformation, whereby we try to\n   * eliminate predicates that are non-deterministic in a way that widens the set of rows to\n   * include any row that could be read by the original predicate.\n   *\n   * Example: `c1 = 5 AND c2 IN (SELECT c FROM <parquet table>)` would be widened to\n   * `c1 = 5 AND True` eliminating the non-deterministic parquet table read by assuming it would\n   * have matched all c2 values.\n   *\n   * `c1 = 5 OR NOT some_udf(c2)` would be widened to `c1 = 5 OR True`, eliminating the\n   * non-deterministic `some_udf` by assuming `NOT some_udf(c2)` would have selected all rows.\n   *\n   * @param newPredicates\n   *   The (potentially widened) list of predicates.\n   * @param eliminatedPredicates\n   *   The predicates that were eliminated as non-deterministic.\n   */\n  protected case class PredicateElimination(\n      newPredicates: Seq[Expression],\n      eliminatedPredicates: Seq[String])\n  protected object PredicateElimination {\n    final val EMPTY: PredicateElimination = PredicateElimination(Seq.empty, Seq.empty)\n\n    def eliminate(p: Expression, eliminated: Option[String] = None): PredicateElimination =\n      PredicateElimination(\n        // Always eliminate with a `TrueLiteral`, implying that the eliminated expression would\n        // have read the entire table.\n        newPredicates = Seq(TrueLiteral),\n        eliminatedPredicates = Seq(eliminated.getOrElse(p.prettyName)))\n\n    def keep(p: Expression): PredicateElimination =\n      PredicateElimination(newPredicates = Seq(p), eliminatedPredicates = Seq.empty)\n\n    def recurse(\n        p: Expression,\n        recFun: Seq[Expression] => PredicateElimination): PredicateElimination = {\n      val eliminatedChildren = recFun(p.children)\n      if (eliminatedChildren.eliminatedPredicates.isEmpty) {\n        // All children were ok, so keep the current expression.\n        keep(p)\n      } else {\n        // Fold the new predicates after sub-expression widening.\n        val newPredicate = p.withNewChildren(eliminatedChildren.newPredicates) match {\n          case p if p.foldable => Literal.create(p.eval(EmptyRow), p.dataType)\n          case Or(TrueLiteral, _) => TrueLiteral\n          case Or(_, TrueLiteral) => TrueLiteral\n          case And(left, TrueLiteral) => left\n          case And(TrueLiteral, right) => right\n          case p => p\n        }\n        PredicateElimination(\n          newPredicates = Seq(newPredicate),\n          eliminatedPredicates = eliminatedChildren.eliminatedPredicates)\n      }\n    }\n  }\n\n  /**\n   * Replace non-deterministic expressions in a way that can only increase the number of selected\n   * files when these predicates are used for file skipping.\n   */\n  protected def eliminateNonDeterministicPredicates(\n      predicates: Seq[Expression],\n      checkDeterministicOptions: CheckDeterministicOptions): PredicateElimination = {\n    eliminateUnsupportedPredicates(predicates) {\n      case p @ SubqueryExpression(plan) =>\n        findFirstNonDeltaScan(plan) match {\n          case Some(plan) => PredicateElimination.eliminate(p, eliminated = Some(plan.nodeName))\n          case None =>\n            findFirstNonDeterministicNode(plan, checkDeterministicOptions) match {\n              case Some(node) =>\n                PredicateElimination.eliminate(p, eliminated = Some(planOrExpressionName(node)))\n              case None => PredicateElimination.keep(p)\n            }\n        }\n      // And and Or can safely be recursed through. Replacing any non-deterministic sub-tree\n      // with `True` will lead us to at most select more files than necessary later.\n      case p: And => PredicateElimination.recurse(p,\n        p => eliminateNonDeterministicPredicates(p, checkDeterministicOptions))\n      case p: Or => PredicateElimination.recurse(p,\n        p => eliminateNonDeterministicPredicates(p, checkDeterministicOptions))\n      // All other expressions must either be completely deterministic,\n      // or must be replaced entirely, since replacing only their non-deterministic children\n      // may lead to files wrongly being deselected (e.g. `NOT True`).\n      case p =>\n        // We always look for non-deterministic child nodes, whether or not `p` is actually\n        // deterministic. This gives us better feedback on what caused the non-determinism in\n        // cases where `p` itself it deterministic but `p.deterministic = false` due to correctly\n        // detected non-deterministic child nodes.\n        findFirstNonDeterministicChildNode(p.children, checkDeterministicOptions) match {\n          case Some(node) =>\n            PredicateElimination.eliminate(p, eliminated = Some(planOrExpressionName(node)))\n          case None => if (p.deterministic) {\n            PredicateElimination.keep(p)\n          } else {\n            PredicateElimination.eliminate(p)\n          }\n        }\n    }\n  }\n\n  private def eliminateUnsupportedPredicates(predicates: Seq[Expression])(\n     eliminatePredicates: Expression => PredicateElimination): PredicateElimination = {\n    predicates\n      .map(eliminatePredicates)\n      .foldLeft(PredicateElimination.EMPTY) { case (acc, predicates) =>\n        acc.copy(\n          newPredicates = acc.newPredicates ++ predicates.newPredicates,\n          eliminatedPredicates = acc.eliminatedPredicates ++ predicates.eliminatedPredicates)\n      }\n  }\n\n  private def planOrExpressionName(e: Either[LogicalPlan, Expression]): String = e match {\n    case scala.util.Left(plan) => plan.nodeName\n    case scala.util.Right(expression) => expression.prettyName\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DataFrameUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.{Column, DataFrame, Encoders, SparkSession}\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.classic.ClassicConversions._\nimport org.apache.spark.sql.classic.Dataset\nimport org.apache.spark.sql.execution.QueryExecution\n\nobject DataFrameUtils {\n  def ofRows(spark: SparkSession, plan: LogicalPlan): DataFrame = Dataset.ofRows(spark, plan)\n  def ofRows(queryExecution: QueryExecution): DataFrame = {\n    val ds = new Dataset(queryExecution, Encoders.row(queryExecution.analyzed.schema))\n    ds.asInstanceOf[DataFrame]\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DefaultRowCommitVersion.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.FileSourceConstantMetadataStructField\nimport org.apache.spark.sql.types\nimport org.apache.spark.sql.types.{LongType, MetadataBuilder, StructField}\n\nobject DefaultRowCommitVersion {\n  def assignIfMissing(\n      spark: SparkSession,\n      protocol: Protocol,\n      snapshot: Snapshot,\n      actions: Iterator[Action],\n      version: Long): Iterator[Action] = {\n    if (!RowTracking.isSupported(protocol)) {\n      return actions\n    }\n    // Do not propagate defaultRowCommitVersions if generation is suspended.\n    if (RowTracking.isSuspended(spark, snapshot.metadata)) {\n      actions.map {\n        case a: AddFile if a.defaultRowCommitVersion.isDefined =>\n          a.copy(defaultRowCommitVersion = None)\n        case a => a\n      }\n    } else {\n      actions.map {\n        case a: AddFile if a.defaultRowCommitVersion.isEmpty =>\n          a.copy(defaultRowCommitVersion = Some(version))\n        case a =>\n          a\n      }\n    }\n  }\n\n  def createDefaultRowCommitVersionField(\n      protocol: Protocol, metadata: Metadata, nullable: Boolean): Option[StructField] = {\n    Option.when(RowTracking.isEnabled(protocol, metadata)) {\n      MetadataStructField(nullable)\n    }\n  }\n\n  val METADATA_STRUCT_FIELD_NAME = \"default_row_commit_version\"\n\n  private object MetadataStructField {\n    private val METADATA_COL_ATTR_KEY = \"__default_row_version_metadata_col\"\n\n    def apply(nullable: Boolean): StructField =\n      StructField(\n        METADATA_STRUCT_FIELD_NAME,\n        LongType,\n        nullable,\n        metadata = metadata)\n\n    def unapply(field: StructField): Option[StructField] =\n      Some(field).filter(isValid)\n\n    private def metadata: types.Metadata = new MetadataBuilder()\n      .withMetadata(FileSourceConstantMetadataStructField.metadata(METADATA_STRUCT_FIELD_NAME))\n      .putBoolean(METADATA_COL_ATTR_KEY, value = true)\n      .build()\n\n\n    private def isValid(s: StructField): Boolean = {\n      FileSourceConstantMetadataStructField.isValid(s.dataType, s.metadata) &&\n        metadata.contains(METADATA_COL_ATTR_KEY) &&\n        metadata.getBoolean(METADATA_COL_ATTR_KEY)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaAnalysis.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.util.{Failure, Success, Try}\nimport scala.util.control.NonFatal\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.catalyst.TimeTravel\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.DeltaErrors.{TemporallyUnstableInputException, TimestampEarlierThanCommitRetentionException}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.catalog.DeltaCatalogV1\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.catalog.IcebergTablePlaceHolder\nimport org.apache.spark.sql.delta.commands._\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.convert.ConvertUtils\nimport org.apache.spark.sql.delta.constraints.{AddConstraint, DropConstraint}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CoordinatedCommitsUtils}\nimport org.apache.spark.sql.delta.files.{TahoeFileIndex, TahoeLogFileIndex}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.AllowAutomaticWideningMode\nimport org.apache.spark.sql.delta.util.AnalysisHelper\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{AnalysisException, Dataset, SaveMode, SparkSession}\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType, HiveTableRelation}\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.parser.CatalystSqlParser\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.plans.logical.CloneTableStatement\nimport org.apache.spark.sql.catalyst.plans.logical.RestoreTableStatement\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.catalyst.streaming.WriteToStream\nimport org.apache.spark.sql.catalyst.trees.TreeNodeTag\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttribute\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.connector.catalog.{CatalogV2Util, Identifier, TableCatalog}\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\nimport org.apache.spark.sql.connector.expressions.{FieldReference, IdentityTransform, Transform}\nimport org.apache.spark.sql.errors.QueryCompilationErrors\nimport org.apache.spark.sql.execution.command.CreateTableLikeCommand\nimport org.apache.spark.sql.execution.command.RunnableCommand\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.execution.datasources.v2.{DataSourceV2Relation, DataSourceV2RelationShim}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n\n/**\n * Analysis rules for Delta. Currently, these rules enable schema enforcement / evolution with\n * INSERT INTO.\n */\nclass DeltaAnalysis(session: SparkSession)\n  extends Rule[LogicalPlan] with AnalysisHelper with DeltaLogging {\n\n  type CastFunction = (Expression, DataType, String) => Expression\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperatorsDown {\n    // INSERT INTO by ordinal and df.insertInto()\n    case a @ AppendDelta(r, d) if !a.isByName &&\n        needsSchemaAdjustmentByOrdinal(d, a.query, r.schema, a.writeOptions) =>\n      val projection = resolveQueryColumnsByOrdinal(a.query, r.output, d, a.writeOptions)\n      if (projection != a.query) {\n        a.copy(query = projection)\n      } else {\n        a\n      }\n\n\n    // INSERT INTO by name\n    // AppendData.byName is also used for DataFrame append so we check for the SQL origin text\n    // since we only want to up-cast for SQL insert into by name\n    case a @ AppendDelta(r, d) if a.isByName && a.origin.sqlText.nonEmpty &&\n        needsSchemaAdjustmentByName(a.query, r.output, d, a.writeOptions) =>\n      val projection = resolveQueryColumnsByName(\n        query = a.query,\n        targetAttrs = r.output,\n        deltaTable = d,\n        writeOptions = a.writeOptions,\n        allowSchemaEvolution = true)\n      if (projection != a.query) {\n        a.copy(query = projection)\n      } else {\n        a\n      }\n\n    /**\n     * Handling create table like when a delta target (provider)\n     * is provided explicitly or when the source table is a delta table\n     */\n    case EligibleCreateTableLikeCommand(ctl, src) =>\n      val deltaTableIdentifier = DeltaTableIdentifier(session, ctl.targetTable)\n\n      // Check if table is given by path\n      val isTableByPath = DeltaTableIdentifier.isDeltaPath(session, ctl.targetTable)\n\n      // Check if targetTable is given by path\n      val targetTableIdentifier =\n        if (isTableByPath) {\n          TableIdentifier(deltaTableIdentifier.toString)\n        } else {\n          ctl.targetTable\n        }\n\n      val newStorage =\n        if (ctl.fileFormat.inputFormat.isDefined) {\n          ctl.fileFormat\n        } else if (isTableByPath) {\n          src.storage.copy(locationUri =\n            Some(deltaTableIdentifier.get.getPath(session).toUri))\n        } else {\n          src.storage.copy(locationUri = ctl.fileFormat.locationUri)\n        }\n\n      // If the location is specified or target table is given\n      // by path, we create an external table.\n      // Otherwise create a managed table.\n      val tblType =\n        if (newStorage.locationUri.isEmpty && !isTableByPath) {\n          CatalogTableType.MANAGED\n        } else {\n          CatalogTableType.EXTERNAL\n        }\n\n\n      // Whether we are enabling Catalog-Owned via explicit property overrides.\n      var isEnablingCatalogOwnedViaExplicitPropertyOverrides: Boolean = false\n\n      val catalogTableTarget =\n        // If source table is Delta format\n        if (src.provider.exists(DeltaSourceUtils.isDeltaDataSourceName)) {\n          val deltaLogSrc = DeltaTableV2(session, new Path(src.location))\n\n          // Column mapping and row tracking fields cannot be set externally. If the features are\n          // used on the source delta table, then the corresponding fields would be set for the\n          // sourceTable and needs to be removed from the targetTable's configuration. The fields\n          // will then be set in the targetTable's configuration internally after.\n          // Coordinated commits/Catalog-Owned configurations from the source delta table should\n          // also be left out, since CREATE LIKE is similar to CLONE, and we do not copy the\n          // commit coordinator from the source table.\n          // If users want a commit coordinator for the target table, they can\n          // specify the configurations in the CREATE LIKE command explicitly.\n          val sourceMetadata = deltaLogSrc.initialSnapshot.metadata\n\n          // Catalog-Owned: Specifying the table UUID in the TBLPROPERTIES clause\n          // should be blocked.\n          CatalogOwnedTableUtils.validateUCTableIdNotPresent(property = ctl.properties)\n\n          // Check whether we are trying to enable Catalog-Owned via explicit property overrides.\n          // The reason to check this is, if the source table is a Catalog-Owned table, and\n          // we are also trying to enable Catalog-Owned for the target table - We do *NOT*\n          // want to filter out [[CatalogOwnedTableFeature]] from the source table. If we do that,\n          // the resulting target table's protocol will *NOT* have CatalogOwned table feature\n          // present though we have explicitly specified it in the TBLPROPERTIES clause.\n          // This only applies to cases where source table has Catalog-Owned enabled.\n          // It works as intended if source table is a normal delta table.\n          if (TableFeatureProtocolUtils.getSupportedFeaturesFromTableConfigs(\n                configs = ctl.properties).contains(CatalogOwnedTableFeature)) {\n            isEnablingCatalogOwnedViaExplicitPropertyOverrides = true\n          }\n\n          val config =\n            sourceMetadata.configuration.-(\"delta.columnMapping.maxColumnId\")\n              .-(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP)\n              .-(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP)\n              .filterKeys(!CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS.contains(_)).toMap\n              // Catalog-Owned: Do not copy table UUID from source table\n              .filterKeys(_ != UCCommitCoordinatorClient.UC_TABLE_ID_KEY).toMap\n\n          new CatalogTable(\n            identifier = targetTableIdentifier,\n            tableType = tblType,\n            storage = newStorage,\n            schema = sourceMetadata.schema,\n            properties = config ++ ctl.properties,\n            partitionColumnNames = sourceMetadata.partitionColumns,\n            provider = Some(\"delta\"),\n            comment = Option(sourceMetadata.description)\n          )\n        } else { // Source table is not delta format\n            new CatalogTable(\n              identifier = targetTableIdentifier,\n              tableType = tblType,\n              storage = newStorage,\n              schema = src.schema,\n              properties = src.properties ++ ctl.properties,\n              partitionColumnNames = src.partitionColumnNames,\n              provider = Some(\"delta\"),\n              comment = src.comment\n            )\n        }\n      val saveMode =\n        if (ctl.ifNotExists) {\n          SaveMode.Ignore\n        } else {\n          SaveMode.ErrorIfExists\n        }\n\n      val protocol =\n        if (src.provider.exists(DeltaSourceUtils.isDeltaDataSourceName)) {\n          Some(DeltaTableV2(session, new Path(src.location)).initialSnapshot.protocol)\n        } else {\n          None\n        }\n      // Catalog-Owned: Do not copy over [[CatalogOwnedTableFeature]] from source table\n      //                except the certain case.\n      val protocolAfterFilteringCatalogOwnedFromSource = protocol match {\n        case Some(p) if !isEnablingCatalogOwnedViaExplicitPropertyOverrides =>\n          // Only filter out [[CatalogOwnedTableFeature]] when target table is not enabling\n          // CatalogOwned.\n          // E.g.,\n          // - CREATE TABLE t1 LIKE t2\n          //   - Filter CatalogOwned table feature out since target table is not enabling\n          //     CatalogOwned explicitly.\n          // - CREATE TABLE t1 LIKE t2 TBLPROPERTIES (\n          //     'delta.feature.catalogManaged' = 'supported'\n          //   )\n          //   - Do not filter CatalogOwned table feature out if target table is enabling\n          //     CatalogOwned.\n          Some(p.removeFeature(targetFeature = CatalogOwnedTableFeature))\n        case _ =>\n          protocol\n      }\n      val newDeltaCatalog = new DeltaCatalogV1()\n      val existingTableOpt = newDeltaCatalog.getExistingTableIfExists(\n        catalogTableTarget.identifier,\n        identOpt = None,\n        operation = TableCreationModes.Create)\n      val newTable = newDeltaCatalog\n        .verifyTableAndSolidify(\n          catalogTableTarget,\n          None\n        )\n      CreateDeltaTableCommand(\n        table = newTable,\n        existingTableOpt = existingTableOpt,\n        mode = saveMode,\n        query = None,\n        output = ctl.output,\n        protocol = protocolAfterFilteringCatalogOwnedFromSource,\n        tableByPath = isTableByPath)\n\n    // INSERT OVERWRITE by ordinal and df.insertInto()\n    case o @ OverwriteDelta(r, d) if !o.isByName &&\n        needsSchemaAdjustmentByOrdinal(d, o.query, r.schema, o.writeOptions) =>\n      val projection = resolveQueryColumnsByOrdinal(o.query, r.output, d, o.writeOptions)\n      if (projection != o.query) {\n        val aliases = AttributeMap(o.query.output.zip(projection.output).collect {\n          case (l: AttributeReference, r: AttributeReference) if !l.sameRef(r) => (l, r)\n        })\n        val newDeleteExpr = o.deleteExpr.transformUp {\n          case a: AttributeReference => aliases.getOrElse(a, a)\n        }\n        o.copy(deleteExpr = newDeleteExpr, query = projection)\n      } else {\n        o\n      }\n\n    // INSERT OVERWRITE by name\n    // OverwriteDelta.byName is also used for DataFrame append so we check for the SQL origin text\n    // since we only want to up-cast for SQL insert into by name\n    case o @ OverwriteDelta(r, d) if o.isByName && o.origin.sqlText.nonEmpty &&\n        needsSchemaAdjustmentByName(o.query, r.output, d, o.writeOptions) =>\n      val projection = resolveQueryColumnsByName(\n        query = o.query,\n        targetAttrs = r.output,\n        deltaTable = d,\n        writeOptions = o.writeOptions,\n        allowSchemaEvolution = true)\n      if (projection != o.query) {\n        val aliases = AttributeMap(o.query.output.zip(projection.output).collect {\n          case (l: AttributeReference, r: AttributeReference) if !l.sameRef(r) => (l, r)\n        })\n        val newDeleteExpr = o.deleteExpr.transformUp {\n          case a: AttributeReference => aliases.getOrElse(a, a)\n        }\n        o.copy(deleteExpr = newDeleteExpr, query = projection)\n      } else {\n        o\n      }\n\n\n    // INSERT OVERWRITE with dynamic partition overwrite\n    case o @ DynamicPartitionOverwriteDelta(r, d) if o.resolved\n      =>\n      val adjustedQuery = if (!o.isByName &&\n          needsSchemaAdjustmentByOrdinal(d, o.query, r.schema, o.writeOptions)) {\n        // INSERT OVERWRITE by ordinal and df.insertInto()\n        resolveQueryColumnsByOrdinal(o.query, r.output, d, o.writeOptions)\n      } else if (o.isByName && o.origin.sqlText.nonEmpty &&\n          needsSchemaAdjustmentByName(o.query, r.output, d, o.writeOptions)) {\n        // INSERT OVERWRITE by name\n        // OverwriteDelta.byName is also used for DataFrame append so we check for the SQL origin\n        // text since we only want to up-cast for SQL insert into by name\n        resolveQueryColumnsByName(\n          query = o.query,\n          targetAttrs = r.output,\n          deltaTable = d,\n          writeOptions = o.writeOptions,\n          allowSchemaEvolution = true)\n      } else {\n        o.query\n      }\n      DeltaDynamicPartitionOverwriteCommand(r, d, adjustedQuery, o.writeOptions, o.isByName)\n\n    case ResolveDeltaTableWithPartitionFilters(plan) => plan\n\n    // SQL CDC table value functions \"table_changes\" and \"table_changes_by_path\"\n    case stmt: CDCStatementBase if stmt.functionArgs.forall(_.resolved) =>\n      stmt.toTableChanges(session)\n\n    case tc: TableChanges if tc.child.resolved => tc.toReadQuery\n\n\n    // Here we take advantage of CreateDeltaTableCommand which takes a LogicalPlan for CTAS in order\n    // to perform CLONE. We do this by passing the CloneTableCommand as the query in\n    // CreateDeltaTableCommand and let Create handle the creation + checks of creating a table in\n    // the metastore instead of duplicating that effort in CloneTableCommand.\n    case cloneStatement: CloneTableStatement =>\n      // Get the info necessary to CreateDeltaTableCommand\n      EliminateSubqueryAliases(cloneStatement.source) match {\n        case DataSourceV2RelationShim(table: DeltaTableV2, _, _, _, _) =>\n          resolveCloneCommand(cloneStatement.target, new CloneDeltaSource(table), cloneStatement)\n\n        // Pass the traveled table if a previous version is to be cloned\n        case tt @ TimeTravel(DataSourceV2RelationShim(tbl: DeltaTableV2, _, _, _, _), _, _, _)\n            if tt.expressions.forall(_.resolved) =>\n          val ttSpec = DeltaTimeTravelSpec(tt.timestamp, tt.version, tt.creationSource)\n          val traveledTable = tbl.copy(timeTravelOpt = Some(ttSpec))\n          resolveCloneCommand(\n            cloneStatement.target, new CloneDeltaSource(traveledTable), cloneStatement)\n\n        case DataSourceV2RelationShim(table: IcebergTablePlaceHolder, _, _, _, _) =>\n          resolveCloneCommand(\n            cloneStatement.target,\n            CloneIcebergSource(\n              metadataLocation = table.tableIdentifier.table,\n              tableNameOpt = None,\n              tablePoliciesOpt = None,\n              deltaSnapshotOpt = None,\n              session),\n            cloneStatement)\n\n        case DataSourceV2RelationShim(table, _, _, _, _)\n            if table.getClass.getName.endsWith(\"org.apache.iceberg.spark.source.SparkTable\") =>\n          val metadataLocation = ConvertUtils.getIcebergMetadataLocationFromSparkTable(table)\n          resolveCloneCommand(\n            cloneStatement.target,\n            CloneIcebergSource(\n              metadataLocation,\n              tableNameOpt = Some(table.name()),\n              tablePoliciesOpt =\n                None,\n              deltaSnapshotOpt = None,\n              session\n            ),\n            cloneStatement)\n\n        case u: UnresolvedRelation =>\n          u.tableNotFound(u.multipartIdentifier)\n\n        case TimeTravel(u: UnresolvedRelation, _, _, _) =>\n          u.tableNotFound(u.multipartIdentifier)\n\n        case LogicalRelationWithTable(\n            HadoopFsRelation(location, _, _, _, _: ParquetFileFormat, _), catalogTable) =>\n          val tableIdent = catalogTable.map(_.identifier)\n            .getOrElse(TableIdentifier(location.rootPaths.head.toString, Some(\"parquet\")))\n          val provider = if (catalogTable.isDefined) {\n            catalogTable.get.provider.getOrElse(\"Unknown\")\n          } else {\n            \"parquet\"\n          }\n          // Only plain Parquet sources are eligible for CLONE, extensions like 'deltaSharing' are\n          // NOT supported.\n          if (!provider.equalsIgnoreCase(\"parquet\")) {\n            throw DeltaErrors.cloneFromUnsupportedSource(\n              tableIdent.unquotedString,\n              provider)\n          }\n\n          resolveCloneCommand(\n            cloneStatement.target,\n            CloneParquetSource(tableIdent, catalogTable, session), cloneStatement)\n\n        case HiveTableRelation(catalogTable, _, _, _, _) =>\n          if (!ConvertToDeltaCommand.isHiveStyleParquetTable(catalogTable)) {\n            throw DeltaErrors.cloneFromUnsupportedSource(\n              catalogTable.identifier.unquotedString,\n              catalogTable.storage.serde.getOrElse(\"Unknown\"))\n          }\n          resolveCloneCommand(\n            cloneStatement.target,\n            CloneParquetSource(catalogTable.identifier, Some(catalogTable), session),\n            cloneStatement)\n\n        case v: View =>\n          throw DeltaErrors.cloneFromUnsupportedSource(\n            v.desc.identifier.unquotedString, \"View\")\n\n        case l: LogicalPlan =>\n          throw DeltaErrors.cloneFromUnsupportedSource(\n            l.toString, \"Unknown\")\n      }\n\n    case restoreStatement @ RestoreTableStatement(target) =>\n      EliminateSubqueryAliases(target) match {\n        // Pass the traveled table if a previous version is to be cloned\n        case tt @ TimeTravel(DataSourceV2RelationShim(tbl: DeltaTableV2, _, _, _, _), _, _, _)\n            if tt.expressions.forall(_.resolved) =>\n          val ttSpec = DeltaTimeTravelSpec(tt.timestamp, tt.version, tt.creationSource)\n          val traveledTable = tbl.copy(timeTravelOpt = Some(ttSpec))\n          // restoring to same version as latest should be a no-op.\n          val sourceSnapshot = try {\n            traveledTable.initialSnapshot\n          } catch {\n            case v: VersionNotFoundException =>\n              throw DeltaErrors.restoreVersionNotExistException(v.userVersion, v.earliest, v.latest)\n            case tEarlier: TimestampEarlierThanCommitRetentionException =>\n              throw DeltaErrors.restoreTimestampBeforeEarliestException(\n                tEarlier.userTimestamp.toString,\n                tEarlier.commitTs.toString\n              )\n            case tUnstable: TemporallyUnstableInputException =>\n              throw DeltaErrors.restoreTimestampGreaterThanLatestException(\n                tUnstable.userTs.toString,\n                tUnstable.lastCommitTs.toString\n              )\n          }\n          // TODO: Fetch the table version from deltaLog.update().version to guarantee freshness.\n          //  This can also be used by RestoreTableCommand\n          if (sourceSnapshot.version == traveledTable.deltaLog.unsafeVolatileSnapshot.version) {\n            return LocalRelation(restoreStatement.output)\n          }\n\n          RestoreTableCommand(traveledTable)\n\n        case u: UnresolvedRelation =>\n          u.tableNotFound(u.multipartIdentifier)\n\n        case TimeTravel(u: UnresolvedRelation, _, _, _) =>\n          u.tableNotFound(u.multipartIdentifier)\n\n        case _ =>\n          throw DeltaErrors.notADeltaTableException(\"RESTORE\")\n      }\n\n    // Resolve as a resolved table if the path is for delta table. For non delta table, we keep the\n    // path and pass it along in a ResolvedPathBasedNonDeltaTable. This is needed as DESCRIBE DETAIL\n    // supports both delta and non delta paths.\n    case u: UnresolvedPathBasedTable =>\n      val table = getPathBasedDeltaTable(u.path, u.options)\n      if (Try(table.tableExists).getOrElse(false)) {\n        // Resolve it as a path-based Delta table\n        val catalog = session.sessionState.catalogManager.currentCatalog.asTableCatalog\n        ResolvedTable.create(\n          catalog, Identifier.of(Array(DeltaSourceUtils.ALT_NAME), u.path), table)\n      } else {\n        // Resolve it as a placeholder, to identify it as a non-Delta table.\n        ResolvedPathBasedNonDeltaTable(u.path, u.options, u.commandName)\n      }\n\n    case u: UnresolvedPathBasedDeltaTable =>\n      val table = getPathBasedDeltaTable(u.path, u.options)\n      if (!table.tableExists) {\n        throw DeltaErrors.notADeltaTableException(u.commandName, u.deltaTableIdentifier)\n      }\n      val catalog = session.sessionState.catalogManager.currentCatalog.asTableCatalog\n      ResolvedTable.create(catalog, u.identifier, table)\n\n    case u: UnresolvedPathBasedDeltaTableRelation =>\n      val table = getPathBasedDeltaTable(u.path, u.options.asScala.toMap)\n      if (!table.tableExists) {\n        throw DeltaErrors.notADeltaTableException(u.deltaTableIdentifier)\n      }\n      DataSourceV2Relation.create(table, None, Some(u.identifier), u.options)\n\n    case d: DescribeDeltaHistory if d.childrenResolved => d.toCommand\n\n    case FallbackToV1DeltaRelation(v1Relation) => v1Relation\n\n    case ResolvedTable(_, _, d: DeltaTableV2, _) if d.catalogTable.isEmpty && !d.tableExists =>\n      // This is DDL on a path based table that doesn't exist. CREATE will not hit this path, most\n      // SHOW / DESC code paths will hit this\n      throw DeltaErrors.notADeltaTableException(DeltaTableIdentifier(path = Some(d.path.toString)))\n\n    // DML - TODO: Remove these Delta-specific DML logical plans and use Spark's plans directly\n\n    case d @ DeleteFromTable(table, condition) if d.childrenResolved =>\n      // rewrites Delta from V2 to V1\n      val newTarget = stripTempViewWrapper(table).transformUp { case DeltaRelation(lr) => lr }\n      val indices = newTarget.collect {\n        case DeltaFullTable(_, index) => index\n      }\n      if (indices.isEmpty) {\n        // Not a Delta table at all, do not transform\n        d\n      } else if (indices.size == 1 && indices(0).deltaLog.tableExists) {\n        // It is a well-defined Delta table with a schema\n        DeltaDelete(newTarget, Some(condition))\n      } else {\n        // Not a well-defined Delta table\n        throw DeltaErrors.notADeltaSourceException(\"DELETE\", Some(d))\n      }\n\n    case u @ UpdateTable(table, assignments, condition) if u.childrenResolved =>\n      val (cols, expressions) = assignments.map(a => a.key -> a.value).unzip\n      // rewrites Delta from V2 to V1\n      val newTable = stripTempViewWrapper(table).transformUp { case DeltaRelation(lr) => lr }\n        newTable.collectLeaves().headOption match {\n          case Some(DeltaFullTable(_, index)) =>\n            DeltaUpdateTable(newTable, cols, expressions, condition)\n          case o =>\n            // not a Delta table\n            u\n        }\n\n\n    case merge: MergeIntoTable if merge.childrenResolved =>\n      val matchedActions = merge.matchedActions.map {\n        case update: UpdateAction =>\n          DeltaMergeIntoMatchedUpdateClause(\n            update.condition,\n            DeltaMergeIntoClause.toActions(update.assignments))\n        case update: UpdateStarAction =>\n          DeltaMergeIntoMatchedUpdateClause(update.condition, DeltaMergeIntoClause.toActions(Nil))\n        case delete: DeleteAction =>\n          DeltaMergeIntoMatchedDeleteClause(delete.condition)\n        case other =>\n          throw new IllegalArgumentException(\n            s\"${other.prettyName} clauses cannot be part of the WHEN MATCHED clause in MERGE INTO.\")\n      }\n      val notMatchedActions = merge.notMatchedActions.map {\n        case insert: InsertAction =>\n          DeltaMergeIntoNotMatchedInsertClause(\n            insert.condition,\n            DeltaMergeIntoClause.toActions(insert.assignments))\n        case insert: InsertStarAction =>\n          DeltaMergeIntoNotMatchedInsertClause(\n            insert.condition, DeltaMergeIntoClause.toActions(Nil))\n        case other =>\n          throw new IllegalArgumentException(\n            s\"${other.prettyName} clauses cannot be part of the WHEN NOT MATCHED clause in MERGE \" +\n             \"INTO.\")\n      }\n      val notMatchedBySourceActions = merge.notMatchedBySourceActions.map {\n        case update: UpdateAction =>\n          DeltaMergeIntoNotMatchedBySourceUpdateClause(\n            update.condition,\n            DeltaMergeIntoClause.toActions(update.assignments))\n        case delete: DeleteAction =>\n          DeltaMergeIntoNotMatchedBySourceDeleteClause(delete.condition)\n        case other =>\n          throw new IllegalArgumentException(\n            s\"${other.prettyName} clauses cannot be part of the WHEN NOT MATCHED BY SOURCE \" +\n             \"clause in MERGE INTO.\")\n      }\n      // rewrites Delta from V2 to V1\n      var isDelta = false\n      val newTarget = stripTempViewForMergeWrapper(merge.targetTable).transformUp {\n        case DeltaRelation(lr) =>\n          isDelta = true\n          lr\n      }\n\n      if (isDelta) {\n        // Even if we're merging into a non-Delta target, we will catch it later and throw an\n        // exception.\n        val deltaMerge = DeltaMergeInto(\n          newTarget,\n          merge.sourceTable,\n          merge.mergeCondition,\n          matchedActions ++ notMatchedActions ++ notMatchedBySourceActions,\n          // TODO: We are waiting for Spark to support the SQL \"WITH SCHEMA EVOLUTION\" syntax.\n          // After that this argument will be `merge.withSchemaEvolution`.\n          withSchemaEvolution = false\n        )\n\n        ResolveDeltaMergeInto.resolveReferencesAndSchema(deltaMerge, conf)(\n          tryResolveReferencesForExpressions(session))\n      } else {\n        merge\n      }\n\n    case merge: MergeIntoTable if merge.targetTable.exists(_.isInstanceOf[DataSourceV2Relation]) =>\n      // When we hit here, it means the MERGE source is not resolved and we can't convert the MERGE\n      // command to the Delta variant. We need to add a special marker to the target table, so that\n      // this rule does not convert it to v1 relation too early, as we need to keep it as a v2\n      // relation to bypass the OSS MERGE resolution code in the rule `ResolveReferences`.\n      merge.targetTable.foreach {\n        // TreeNodeTag is not very reliable, but it's OK to use it here, as we will use it very\n        // soon: when this rule transforms down the plan tree and hits the MERGE target table.\n        // There is no chance in this rule that we will drop this tag. At the end, This rule will\n        // turn MergeIntoTable into DeltaMergeInto, and convert all Delta relations inside it to\n        // v1 relations (no need to clean up this tag).\n        case r: DataSourceV2Relation => r.setTagValue(DeltaRelation.KEEP_AS_V2_RELATION_TAG, ())\n        case _ =>\n      }\n      merge\n\n    case reorg @ DeltaReorgTable(resolved @ ResolvedTable(_, _, _: DeltaTableV2, _), spec) =>\n      DeltaReorgTableCommand(resolved, spec)(reorg.predicates)\n\n    case DeltaReorgTable(ResolvedTable(_, _, t, _), _) =>\n      throw DeltaErrors.notADeltaTable(t.name())\n\n    case cmd @ ShowColumns(child @ ResolvedTable(_, _, table: DeltaTableV2, _), namespace, _) =>\n      // Adapted from the rule in spark ResolveSessionCatalog.scala, which V2 tables don't trigger.\n      // NOTE: It's probably a spark bug to check head instead of tail, for 3-part identifiers.\n      val resolver = session.sessionState.analyzer.resolver\n      val v1TableName = child.identifier.asTableIdentifier\n      namespace.foreach { ns =>\n        if (v1TableName.database.exists(!resolver(_, ns.head))) {\n          throw DeltaThrowableHelper.showColumnsWithConflictDatabasesError(ns, v1TableName)\n        }\n      }\n      ShowDeltaTableColumnsCommand(child)\n\n    case deltaMerge: DeltaMergeInto =>\n      val d = if (deltaMerge.childrenResolved && !deltaMerge.resolved) {\n        ResolveDeltaMergeInto.resolveReferencesAndSchema(deltaMerge, conf)(\n          tryResolveReferencesForExpressions(session))\n      } else deltaMerge\n      d.copy(target = stripTempViewForMergeWrapper(d.target))\n\n    case origStreamWrite: WriteToStream =>\n      // The command could have Delta as source and/or sink. We need to look at both.\n      val streamWrite = origStreamWrite match {\n        case WriteToStream(_, _, sink @ DeltaSink(_, _, _, _, _, None), _, _, _, _, Some(ct)) =>\n          // The command has a catalog table, but the DeltaSink does not. This happens because\n          // DeltaDataSource.createSink (Spark API) didn't have access to the catalog table when it\n          // created the DeltaSink. Fortunately we can fix it up here.\n          origStreamWrite.copy(sink = sink.copy(catalogTable = Some(ct)))\n        case _ => origStreamWrite\n      }\n\n      // We also need to validate the source schema location, if the command has a Delta source.\n      verifyDeltaSourceSchemaLocation(\n        streamWrite.inputQuery, streamWrite.resolvedCheckpointLocation)\n      streamWrite\n\n  }\n\n  /**\n   * Creates a catalog table for CreateDeltaTableCommand.\n   *\n   * @param targetPath Target path containing the target path to clone to\n   * @param byPath Whether the target is a path based table\n   * @param tableIdent Table Identifier for the target table\n   * @param targetLocation User specified target location for the new table\n   * @param existingTable Existing table definition if we're going to be replacing the table\n   * @param srcTable The source table to clone\n   * @return catalog to CreateDeltaTableCommand with\n   */\n  private def createCatalogTableForCloneCommand(\n      targetPath: Path,\n      byPath: Boolean,\n      tableIdent: TableIdentifier,\n      targetLocation: Option[String],\n      existingTable: Option[CatalogTable],\n      srcTable: CloneSource,\n      propertiesOverrides: Map[String, String]): CatalogTable = {\n    // If external location is defined then then table is an external table\n    // If the table is a path-based table, we also say that the table is external even if no\n    // metastore table will be created. This is done because we are still explicitly providing a\n    // locationUri which is behavior expected only of external tables\n    // In the case of ifNotExists being true and a table existing at the target destination, create\n    // a managed table so we don't have to pass a fake path\n    val (tableType, storage) = if (targetLocation.isDefined || byPath) {\n      (CatalogTableType.EXTERNAL,\n        CatalogStorageFormat.empty.copy(locationUri = Some(targetPath.toUri)))\n    } else {\n      (CatalogTableType.MANAGED, CatalogStorageFormat.empty)\n    }\n    var properties = srcTable.metadata.configuration\n    val validatedOverrides = DeltaConfigs.validateConfigurations(propertiesOverrides)\n    properties = properties.filterKeys(!validatedOverrides.keySet.contains(_)).toMap ++\n      validatedOverrides\n\n    new CatalogTable(\n      identifier = tableIdent,\n      tableType = tableType,\n      storage = storage,\n      schema = srcTable.schema,\n      properties = properties,\n      provider = Some(\"delta\"),\n      stats = existingTable.flatMap(_.stats)\n    )\n  }\n\n  private def getPathBasedDeltaTable(path: String, options: Map[String, String]): DeltaTableV2 = {\n    DeltaTableV2(session, new Path(path), options = options)\n  }\n\n  private def resolveCreateTableMode(\n      isCreate: Boolean,\n      isReplace: Boolean,\n      ifNotExist: Boolean): (SaveMode, TableCreationModes.CreationMode) = {\n    val saveMode = if (isReplace) {\n      SaveMode.Overwrite\n    } else if (ifNotExist) {\n      SaveMode.Ignore\n    } else {\n      SaveMode.ErrorIfExists\n    }\n\n    val tableCreationMode = if (isCreate && isReplace) {\n      TableCreationModes.CreateOrReplace\n    } else if (isCreate) {\n      TableCreationModes.Create\n    } else {\n      TableCreationModes.Replace\n    }\n\n    (saveMode, tableCreationMode)\n  }\n\n  /**\n   * Instantiates a CreateDeltaTableCommand with CloneTableCommand as the child query.\n   *\n   * @param targetPlan the target of Clone as passed in a LogicalPlan\n   * @param sourceTbl the DeltaTableV2 that was resolved as the source of the clone command\n   * @return Resolve the clone command as the query in a CreateDeltaTableCommand.\n   */\n  private def resolveCloneCommand(\n      targetPlan: LogicalPlan,\n      sourceTbl: CloneSource,\n      statement: CloneTableStatement): LogicalPlan = {\n    val isReplace = statement.isReplaceCommand\n    val isCreate = statement.isCreateCommand\n    val ifNotExists = statement.ifNotExists\n\n    val analyzer = session.sessionState.analyzer\n    import analyzer.{NonSessionCatalogAndIdentifier, SessionCatalogAndIdentifier}\n    val targetLocation = statement.targetLocation\n    val (saveMode, tableCreationMode) = resolveCreateTableMode(isCreate, isReplace, ifNotExists)\n    // We don't use information in the catalog if the table is time travelled\n    val sourceCatalogTable = if (sourceTbl.timeTravelOpt.isDefined) None else sourceTbl.catalogTable\n\n    EliminateSubqueryAliases(targetPlan) match {\n      // Target is a path based table\n      case DataSourceV2RelationShim(targetTbl: DeltaTableV2, _, _, _, _)\n        if !targetTbl.tableExists =>\n        val path = targetTbl.path\n        val tblIdent = TableIdentifier(path.toString, Some(\"delta\"))\n        if (!isCreate) {\n          throw DeltaErrors.cannotReplaceMissingTableException(\n            Identifier.of(Array(\"delta\"), path.toString))\n        }\n        // Trying to clone something on itself should be a no-op\n        if (sourceTbl == new CloneDeltaSource(targetTbl)) {\n          return LocalRelation()\n        }\n        // If this is a path based table and an external location is also defined throw an error\n        if (statement.targetLocation.exists(loc => new Path(loc).toString != path.toString)) {\n          throw DeltaErrors.cloneAmbiguousTarget(statement.targetLocation.get, tblIdent)\n        }\n        // We're creating a table by path and there won't be a place to store catalog stats\n        val catalog = createCatalogTableForCloneCommand(path, byPath = true, tblIdent,\n          targetLocation, sourceCatalogTable, sourceTbl, statement.tablePropertyOverrides)\n        CreateDeltaTableCommand(\n          catalog,\n          None,\n          saveMode,\n          Some(CloneTableCommand(\n            sourceTbl,\n            tblIdent,\n            statement.tablePropertyOverrides,\n            path)),\n          tableByPath = true,\n          output = CloneTableCommand.output)\n\n      // Target is a metastore table\n      case UnresolvedRelation(SessionCatalogAndIdentifier(catalog, ident), _, _) =>\n        if (!isCreate) {\n          throw DeltaErrors.cannotReplaceMissingTableException(ident)\n        }\n        val tblIdent = ident\n          .asTableIdentifier\n        val finalTarget = new Path(statement.targetLocation.getOrElse(\n          session.sessionState.catalog.defaultTablePath(tblIdent).toString))\n        val catalogTable = createCatalogTableForCloneCommand(finalTarget, byPath = false, tblIdent,\n          targetLocation, sourceCatalogTable, sourceTbl, statement.tablePropertyOverrides)\n        val catalogTableWithPath = if (targetLocation.isEmpty) {\n          catalogTable.copy(\n            storage = CatalogStorageFormat.empty.copy(locationUri = Some(finalTarget.toUri)))\n        } else {\n          catalogTable\n        }\n        CreateDeltaTableCommand(\n          catalogTableWithPath,\n          None,\n          saveMode,\n          Some(CloneTableCommand(\n            sourceTbl,\n            tblIdent,\n            statement.tablePropertyOverrides,\n            finalTarget)),\n          operation = tableCreationMode,\n          output = CloneTableCommand.output)\n\n      case UnresolvedRelation(NonSessionCatalogAndIdentifier(catalog: TableCatalog, ident), _, _) =>\n        if (!isCreate) {\n          throw DeltaErrors.cannotReplaceMissingTableException(ident)\n        }\n        val partitions: Array[Transform] = sourceTbl.metadata.partitionColumns.map { col =>\n          new IdentityTransform(new FieldReference(Seq(col)))\n        }.toArray\n        // HACK ALERT: since there is no DSV2 API for getting table path before creation,\n        //             here we create a table to get the path, then overwrite it with the\n        //             cloned table.\n        val sourceConfig = sourceTbl.metadata.configuration.asJava\n        val newTable = catalog.createTable(\n            ident,\n            CatalogV2Util.structTypeToV2Columns(sourceTbl.schema),\n            partitions,\n            sourceConfig\n          )\n        try {\n          newTable match {\n            case targetTable: DeltaTableV2 =>\n              val path = targetTable.path\n              val tblIdent = TableIdentifier(path.toString, Some(\"delta\"))\n              val catalogTable = createCatalogTableForCloneCommand(path, byPath = true, tblIdent,\n                targetLocation, sourceCatalogTable, sourceTbl, statement.tablePropertyOverrides)\n              CreateDeltaTableCommand(\n                table = catalogTable,\n                existingTableOpt = None,\n                mode = SaveMode.Overwrite,\n                query = Some(\n                  CloneTableCommand(\n                    sourceTable = sourceTbl,\n                    targetIdent = tblIdent,\n                    tablePropertyOverrides = statement.tablePropertyOverrides,\n                    targetPath = path)),\n                tableByPath = true,\n                operation = TableCreationModes.Replace,\n                output = CloneTableCommand.output)\n            case _ =>\n              throw DeltaErrors.notADeltaSourceException(\"CREATE TABLE CLONE\", Some(statement))\n          }\n        } catch {\n          case NonFatal(e) =>\n            catalog.dropTable(ident)\n            throw e\n        }\n      // Delta metastore table already exists at target\n      case DataSourceV2RelationShim(deltaTableV2: DeltaTableV2, _, _, _, _) =>\n        val path = deltaTableV2.path\n        val existingTable = deltaTableV2.catalogTable\n        val tblIdent = existingTable match {\n          case Some(existingCatalog) => existingCatalog.identifier\n          case None => TableIdentifier(path.toString, Some(\"delta\"))\n        }\n        val catalogTable = createCatalogTableForCloneCommand(\n          path,\n          byPath = existingTable.isEmpty,\n          tblIdent,\n          targetLocation,\n          sourceCatalogTable,\n          sourceTbl,\n          statement.tablePropertyOverrides)\n\n        CreateDeltaTableCommand(\n          catalogTable,\n          existingTable,\n          saveMode,\n          Some(CloneTableCommand(\n            sourceTbl,\n            tblIdent,\n            statement.tablePropertyOverrides,\n            path)),\n          tableByPath = existingTable.isEmpty,\n          operation = tableCreationMode,\n          output = CloneTableCommand.output)\n\n      // Non-delta metastore table already exists at target\n      case LogicalRelationWithTable(_, existingCatalogTable @ Some(catalogTable)) =>\n        val tblIdent = catalogTable.identifier\n        val path = new Path(catalogTable.location)\n        val newCatalogTable = createCatalogTableForCloneCommand(path, byPath = false, tblIdent,\n          targetLocation, sourceCatalogTable, sourceTbl, statement.tablePropertyOverrides)\n        CreateDeltaTableCommand(\n          newCatalogTable,\n          existingCatalogTable,\n          saveMode,\n          Some(CloneTableCommand(\n            sourceTbl,\n            tblIdent,\n            statement.tablePropertyOverrides,\n            path)),\n          operation = tableCreationMode,\n          output = CloneTableCommand.output)\n\n      case _ => throw DeltaErrors.notADeltaTableException(\"CLONE\")\n    }\n  }\n\n  /**\n   * Conditionally wraps a struct expression with an IF expression to preserve NULL source values\n   * when `DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS` is enabled:\n   *   IF(sourceExpr IS NULL, NULL, createStructExpr)\n   *\n   * This prevents null expansion where a null struct would be incorrectly expanded to a struct\n   * with all fields set to NULL during INSERT operations.\n   *\n   * @param sourceExpr The source struct expression\n   * @param createStructExpr The generated CreateStruct expression\n   * @return The potentially wrapped expression with null preservation logic\n   */\n  private def maybeWrapWithNullPreservationForInsert(\n      sourceExpr: Expression,\n      createStructExpr: Expression): Expression = {\n    if (conf.getConf(DeltaSQLConf.DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS)) {\n      val sourceNullCondition = IsNull(sourceExpr)\n      val targetType = createStructExpr.dataType\n      If(\n        sourceNullCondition,\n        Literal.create(null, targetType),\n        createStructExpr\n      )\n    } else {\n      createStructExpr\n    }\n  }\n\n  /**\n   * Performs the schema adjustment by adding UpCasts (which are safe) and Aliases so that we\n   * can check if the by-ordinal schema of the insert query matches our Delta table.\n   * The schema adjustment also include string length check if it's written into a char/varchar\n   * type column/field.\n   */\n  private def resolveQueryColumnsByOrdinal(\n      query: LogicalPlan,\n      targetAttrs: Seq[Attribute],\n      deltaTable: DeltaTableV2,\n      writeOptions: Map[String, String]): LogicalPlan = {\n    // always add a Cast. it will be removed in the optimizer if it is unnecessary.\n    val project = query.output.zipWithIndex.map { case (attr, i) =>\n      if (i < targetAttrs.length) {\n        val targetAttr = targetAttrs(i)\n        addCastToColumn(attr, targetAttr, deltaTable.name(),\n          typeWideningMode = getTypeWideningMode(deltaTable, writeOptions)\n        )\n      } else {\n        attr\n      }\n    }\n    Project(project, query)\n  }\n\n  /**\n   * Performs the schema adjustment by adding UpCasts (which are safe) so that we can insert into\n   * the Delta table when the input data types doesn't match the table schema. Unlike\n   * `resolveQueryColumnsByOrdinal` which ignores the names in `targetAttrs` and maps attributes\n   * directly to query output, this method will use the names in the query output to find the\n   * corresponding attribute to use. This method also allows users to not provide values for\n   * generated columns. If values of any columns are not in the query output, they must be generated\n   * columns.\n   */\n  private def resolveQueryColumnsByName(\n      query: LogicalPlan,\n      targetAttrs: Seq[Attribute],\n      deltaTable: DeltaTableV2,\n      writeOptions: Map[String, String],\n      allowSchemaEvolution: Boolean = false): LogicalPlan = {\n    // Schema evolution is only effective when mergeSchema is enabled in write options AND\n    // the feature is enabled via SQL conf.\n    val effectiveSchemaEvolution = allowSchemaEvolution &&\n      new DeltaOptions(deltaTable.options ++ writeOptions, conf).canMergeSchema &&\n      session.conf.get(DeltaSQLConf.DELTA_INSERT_BY_NAME_SCHEMA_EVOLUTION_ENABLED)\n\n    insertIntoByNameMissingColumn(query, targetAttrs, deltaTable, effectiveSchemaEvolution)\n\n    // This is called before resolveOutputColumns in postHocResolutionRules, so we need to duplicate\n    // the schema validation here.\n    if (!effectiveSchemaEvolution && query.output.length > targetAttrs.length) {\n      throw QueryCompilationErrors.cannotWriteTooManyColumnsToTableError(\n        tableName = deltaTable.name(),\n        expected = targetAttrs.map(_.name),\n        queryOutput = query.output)\n    }\n\n    val project = query.output.map { attr =>\n      val targetAttr = targetAttrs.find(t => session.sessionState.conf.resolver(t.name, attr.name))\n        .getOrElse {\n          if (effectiveSchemaEvolution) {\n            attr\n          } else {\n            // Extra columns in the source are not allowed when schema evolution is disabled.\n            throw DeltaErrors.missingColumn(attr, targetAttrs)\n          }\n        }\n      addCastToColumn(attr, targetAttr, deltaTable.name(),\n        typeWideningMode = getTypeWideningMode(deltaTable, writeOptions)\n      )\n    }\n    Project(project, query)\n  }\n\n  private def addCastToColumn(\n      attr: NamedExpression,\n      targetAttr: NamedExpression,\n      tblName: String,\n      typeWideningMode: TypeWideningMode): NamedExpression = {\n    val expr = (attr.dataType, targetAttr.dataType) match {\n      case (s, t) if s == t =>\n        attr\n      case (s: StructType, t: StructType) if s != t =>\n        addCastsToStructs(tblName, attr, s, t, typeWideningMode)\n      case (ArrayType(s: StructType, sNull: Boolean), ArrayType(t: StructType, tNull: Boolean))\n        if s != t && sNull == tNull =>\n        addCastsToArrayStructs(tblName, attr, s, t, sNull, typeWideningMode)\n      case (s: AtomicType, t: AtomicType)\n        if typeWideningMode.shouldWidenTo(fromType = t, toType = s) =>\n        // Keep the type from the query, the target schema will be updated to widen the existing\n        // type to match it.\n        attr\n      case (s: MapType, t: MapType)\n        if !DataType.equalsStructurally(s, t, ignoreNullability = true) =>\n        // only trigger addCastsToMaps if exists differences like extra fields, renaming or type\n        // differences.\n        addCastsToMaps(tblName, attr, s, t, typeWideningMode)\n      case _ =>\n        getCastFunction(attr, targetAttr.dataType, targetAttr.name)\n    }\n    Alias(expr, targetAttr.name)(explicitMetadata = Option(targetAttr.metadata))\n  }\n\n  /**\n   * Returns the type widening mode to use for the given delta table. A type widening mode indicates\n   * for (fromType, toType) tuples whether `fromType` is eligible to be automatically widened to\n   * `toType` when ingesting data. If it is, the table schema is updated to `toType` before\n   * ingestion and values are written using their original `toType` type. Otherwise, the table type\n   * `fromType` is retained and values are downcasted on write.\n   */\n  private def getTypeWideningMode(\n      deltaTable: DeltaTableV2,\n      writeOptions: Map[String, String]): TypeWideningMode = {\n    val options = new DeltaOptions(deltaTable.options ++ writeOptions, conf)\n    val snapshot = deltaTable.initialSnapshot\n    val typeWideningEnabled = TypeWidening.isEnabled(snapshot.protocol, snapshot.metadata)\n    val schemaEvolutionEnabled = options.canMergeSchema\n\n    if (typeWideningEnabled && schemaEvolutionEnabled) {\n      TypeWideningMode.TypeEvolution(\n        uniformIcebergCompatibleOnly = UniversalFormat.icebergEnabled(snapshot.metadata),\n        allowAutomaticWidening = AllowAutomaticWideningMode.fromConf(conf))\n    } else {\n      TypeWideningMode.NoTypeWidening\n    }\n  }\n\n  /**\n   * With Delta, we ACCEPT_ANY_SCHEMA, meaning that Spark doesn't automatically adjust the schema\n   * of INSERT INTO. This allows us to perform better schema enforcement/evolution. Since Spark\n   * skips this step, we see if we need to perform any schema adjustment here.\n   */\n  private def needsSchemaAdjustmentByOrdinal(\n      deltaTable: DeltaTableV2,\n      query: LogicalPlan,\n      schema: StructType,\n      writeOptions: Map[String, String]): Boolean = {\n    val output = query.output\n    if (output.length < schema.length) {\n      throw DeltaErrors.notEnoughColumnsInInsert(deltaTable.name(), output.length, schema.length)\n    }\n    // Now we should try our best to match everything that already exists, and leave the rest\n    // for schema evolution to WriteIntoDelta\n    val existingSchemaOutput = output.take(schema.length)\n    existingSchemaOutput.map(_.name) != schema.map(_.name) ||\n      !SchemaUtils.isReadCompatible(schema.asNullable, existingSchemaOutput.toStructType,\n        typeWideningMode = getTypeWideningMode(deltaTable, writeOptions))\n  }\n\n  /**\n   * Checks for missing columns in a insert by name query and throws an exception if found.\n   * Delta does not require users to provide values for generated columns, so any columns missing\n   * from the query output must have a default expression.\n   * See [[ColumnWithDefaultExprUtils.columnHasDefaultExpr]].\n   */\n  private def insertIntoByNameMissingColumn(\n      query: LogicalPlan,\n      targetAttrs: Seq[Attribute],\n      deltaTable: DeltaTableV2,\n      allowSchemaEvolution: Boolean = false): Unit = {\n    // When allowing the source schema to contain extra columns, it can still\n    // be missing required columns from the target schema.\n    //\n    // The total column count alone is not sufficient for validation.\n    // A source may have more columns overall but still omit specific columns\n    // that are required by the target schema.\n    //\n    // Example:\n    //   Target: [a, b]\n    //   Source: [a, x, y]\n    // Since the source has 3 columns vs target's 2, but is missing column b, this should be caught.\n    if (allowSchemaEvolution || query.output.length < targetAttrs.length) {\n      val userSpecifiedNames = if (session.sessionState.conf.caseSensitiveAnalysis) {\n        query.output.map(a => (a.name, a)).toMap\n      } else {\n        CaseInsensitiveMap(query.output.map(a => (a.name, a)).toMap)\n      }\n      val tableSchema = deltaTable.initialSnapshot.metadata.schema\n      if (tableSchema.length != targetAttrs.length) {\n        // The target attributes may contain the metadata columns by design. Throwing an exception\n        // here in case target attributes may have the metadata columns for Delta in future.\n        throw DeltaErrors.schemaNotConsistentWithTarget(s\"$tableSchema\", s\"$targetAttrs\")\n      }\n      val nullAsDefault = deltaTable.spark.sessionState.conf.useNullsForMissingDefaultColumnValues\n      deltaTable.initialSnapshot.metadata.schema.foreach { col =>\n        if (!userSpecifiedNames.contains(col.name) &&\n          !ColumnWithDefaultExprUtils.columnHasDefaultExpr(\n            deltaTable.initialSnapshot.protocol, col, nullAsDefault)) {\n          throw DeltaErrors.missingColumnsInInsertInto(col.name)\n        }\n      }\n    }\n  }\n\n  /**\n   * With Delta, we ACCEPT_ANY_SCHEMA, meaning that Spark doesn't automatically adjust the schema\n   * of INSERT INTO. Here we check if we need to perform any schema adjustment for INSERT INTO by\n   * name queries. We also check that any columns not in the list of user-specified columns must\n   * have a default expression.\n   */\n  private def needsSchemaAdjustmentByName(\n      query: LogicalPlan,\n      targetAttrs: Seq[Attribute],\n      deltaTable: DeltaTableV2,\n      writeOptions: Map[String, String]): Boolean = {\n    insertIntoByNameMissingColumn(query, targetAttrs, deltaTable)\n    val userSpecifiedNames = if (session.sessionState.conf.caseSensitiveAnalysis) {\n      query.output.map(a => (a.name, a)).toMap\n    } else {\n      CaseInsensitiveMap(query.output.map(a => (a.name, a)).toMap)\n    }\n    val specifiedTargetAttrs = targetAttrs.filter(col => userSpecifiedNames.contains(col.name))\n    !SchemaUtils.isReadCompatible(\n      specifiedTargetAttrs.toStructType.asNullable,\n      query.output.toStructType,\n      typeWideningMode = getTypeWideningMode(deltaTable, writeOptions)\n    )\n  }\n\n  // Get cast operation for the level of strictness in the schema a user asked for\n  private def getCastFunction: CastFunction = {\n    val timeZone = conf.sessionLocalTimeZone\n    conf.storeAssignmentPolicy match {\n      case SQLConf.StoreAssignmentPolicy.LEGACY =>\n        (input: Expression, dt: DataType, _) =>\n          Cast(input, dt, Option(timeZone), ansiEnabled = false)\n      case SQLConf.StoreAssignmentPolicy.ANSI =>\n        (input: Expression, dt: DataType, name: String) => {\n          val cast = Cast(input, dt, Option(timeZone), ansiEnabled = true)\n          cast.setTagValue(Cast.BY_TABLE_INSERTION, ())\n          TableOutputResolver.checkCastOverflowInTableInsert(cast, name)\n        }\n      case SQLConf.StoreAssignmentPolicy.STRICT =>\n        (input: Expression, dt: DataType, _) =>\n          UpCast(input, dt)\n    }\n  }\n\n  /**\n   * Recursively casts struct data types in case the source/target type differs.\n   */\n  private def addCastsToStructs(\n      tableName: String,\n      parent: NamedExpression,\n      source: StructType,\n      target: StructType,\n      typeWideningMode: TypeWideningMode): NamedExpression = {\n    if (source.length < target.length) {\n      throw DeltaErrors.notEnoughColumnsInInsert(\n        tableName, source.length, target.length, Some(parent.qualifiedName))\n    }\n    // Extracts the field at a given index in the target schema. Only matches if the index is valid.\n    object TargetIndex {\n      def unapply(index: Int): Option[StructField] = target.lift(index)\n    }\n\n    val fields = source.zipWithIndex.map {\n      case (StructField(name, nested: StructType, _, metadata), i @ TargetIndex(targetField)) =>\n        targetField.dataType match {\n          case t: StructType =>\n            val subField = Alias(GetStructField(parent, i, Option(name)), targetField.name)(\n              explicitMetadata = Option(metadata))\n            addCastsToStructs(tableName, subField, nested, t, typeWideningMode)\n          case o =>\n            val field = parent.qualifiedName + \".\" + name\n            val targetName = parent.qualifiedName + \".\" + targetField.name\n            throw DeltaErrors.cannotInsertIntoColumn(tableName, field, targetName, o.simpleString)\n        }\n\n      case (StructField(name, sourceType: AtomicType, _, _),\n            i @ TargetIndex(StructField(targetName, targetType: AtomicType, _, targetMetadata)))\n          if typeWideningMode.shouldWidenTo(fromType = targetType, toType = sourceType) =>\n        Alias(\n          GetStructField(parent, i, Option(name)),\n          targetName)(explicitMetadata = Option(targetMetadata))\n      case (sourceField, i @ TargetIndex(targetField)) =>\n        Alias(\n          getCastFunction(GetStructField(parent, i, Option(sourceField.name)),\n            targetField.dataType, targetField.name),\n          targetField.name)(explicitMetadata = Option(targetField.metadata))\n\n      case (sourceField, i) =>\n        // This is a new column, so leave to schema evolution as is. Do not lose it's name so\n        // wrap with an alias\n        Alias(\n          GetStructField(parent, i, Option(sourceField.name)),\n          sourceField.name)(explicitMetadata = Option(sourceField.metadata))\n    }\n\n    // Fix for null expansion caused by struct type cast by preserving NULL source structs.\n    //\n    // Problem: When inserting a struct column, if the source struct is NULL, the casting logic\n    // will expand the NULL into a non-null struct with all fields set to NULL:\n    //   NULL -> struct(field1: null, field2: null, ..., newField: null)\n    //\n    // Expected: The target struct should remain NULL when the source struct is NULL:\n    //   NULL -> NULL\n    //\n    // Solution: Wrap the CreateStruct expression in an IF expression that preserves NULL:\n    //   IF(source_struct IS NULL, NULL, CreateStruct(...))\n    //\n    // This is controlled by the DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS config.\n    val createStructExpr = CreateStruct(fields)\n    val wrappedWithNullPreservation =\n      maybeWrapWithNullPreservationForInsert(\n        sourceExpr = parent,\n        createStructExpr = createStructExpr)\n    Alias(wrappedWithNullPreservation, parent.name)(\n      parent.exprId, parent.qualifier, Option(parent.metadata))\n  }\n\n  private def addCastsToArrayStructs(\n      tableName: String,\n      parent: NamedExpression,\n      source: StructType,\n      target: StructType,\n      sourceNullable: Boolean,\n      typeWideningMode: TypeWideningMode): Expression = {\n    val structConverter: (Expression, Expression) => Expression = (_, i) =>\n      addCastsToStructs(\n        tableName, Alias(GetArrayItem(parent, i), i.toString)(), source, target, typeWideningMode)\n    val transformLambdaFunc = {\n      val elementVar = NamedLambdaVariable(\"elementVar\", source, sourceNullable)\n      val indexVar = NamedLambdaVariable(\"indexVar\", IntegerType, false)\n      LambdaFunction(structConverter(elementVar, indexVar), Seq(elementVar, indexVar))\n    }\n    ArrayTransform(parent, transformLambdaFunc)\n  }\n\n  private def stripTempViewWrapper(plan: LogicalPlan): LogicalPlan = {\n    DeltaViewHelper.stripTempView(plan, conf)\n  }\n\n  private def stripTempViewForMergeWrapper(plan: LogicalPlan): LogicalPlan = {\n    DeltaViewHelper.stripTempViewForMerge(plan, conf)\n  }\n\n  /**\n   * Recursively casts map data types in case the key/value type differs.\n   */\n  private def addCastsToMaps(\n      tableName: String,\n      parent: NamedExpression,\n      sourceMapType: MapType,\n      targetMapType: MapType,\n      typeWideningMode: TypeWideningMode): Expression = {\n    val transformedKeys =\n      if (sourceMapType.keyType != targetMapType.keyType) {\n        // Create a transformation for the keys\n        ArrayTransform(MapKeys(parent), {\n          val key = NamedLambdaVariable(\n            \"key\", sourceMapType.keyType, nullable = false)\n\n          val keyAttr = AttributeReference(\n            \"key\", targetMapType.keyType, nullable = false)()\n\n          val castedKey =\n            addCastToColumn(\n              key,\n              keyAttr,\n              tableName,\n              typeWideningMode\n            )\n          LambdaFunction(castedKey, Seq(key))\n        })\n      } else {\n        MapKeys(parent)\n      }\n\n    val transformedValues =\n      if (sourceMapType.valueType != targetMapType.valueType) {\n        // Create a transformation for the values\n        ArrayTransform(MapValues(parent), {\n          val value = NamedLambdaVariable(\n            \"value\", sourceMapType.valueType, sourceMapType.valueContainsNull)\n\n          val valueAttr = AttributeReference(\n            \"value\", targetMapType.valueType, sourceMapType.valueContainsNull)()\n\n          val castedValue =\n            addCastToColumn(\n              value,\n              valueAttr,\n              tableName,\n              typeWideningMode\n            )\n          LambdaFunction(castedValue, Seq(value))\n        })\n      } else {\n        MapValues(parent)\n      }\n    // Create new map from transformed keys and values\n    MapFromArrays(transformedKeys, transformedValues)\n  }\n\n  /**\n   * Verify the input plan for a SINGLE streaming query with the following:\n   * 1. Schema location must be under checkpoint location, if not lifted by flag\n   * 2. No two duplicating delta source can share the same schema location\n   */\n  private def verifyDeltaSourceSchemaLocation(\n      inputQuery: LogicalPlan,\n      checkpointLocation: String): Unit = {\n    // Maps StreamingRelation to schema location, similar to how MicroBatchExecution converts\n    // StreamingRelation to StreamingExecutionRelation.\n    val schemaLocationMap = mutable.Map[StreamingRelation, String]()\n    val allowSchemaLocationOutsideOfCheckpoint = session.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_STREAMING_ALLOW_SCHEMA_LOCATION_OUTSIDE_CHECKPOINT_LOCATION)\n    inputQuery.foreach {\n      case streamingRelation @ StreamingRelation(dataSourceV1, sourceName, _)\n        if DeltaSourceUtils.isDeltaDataSourceName(sourceName) =>\n          DeltaDataSource.extractSchemaTrackingLocationConfig(\n            session, dataSourceV1.options\n          ).foreach { rootSchemaTrackingLocation =>\n            assert(dataSourceV1.options.contains(\"path\"), \"Path for Delta table must be defined\")\n            val tableId =\n              dataSourceV1.options(\"path\").replace(\":\", \"\").replace(\"/\", \"_\")\n            val sourceIdOpt = dataSourceV1.options.get(DeltaOptions.STREAMING_SOURCE_TRACKING_ID)\n            val schemaTrackingLocation =\n              DeltaSourceMetadataTrackingLog.fullMetadataTrackingLocation(\n                rootSchemaTrackingLocation, tableId, sourceIdOpt)\n            // Make sure schema location is under checkpoint\n            if (!allowSchemaLocationOutsideOfCheckpoint) {\n              assertSchemaTrackingLocationUnderCheckpoint(\n                checkpointLocation,\n                schemaTrackingLocation\n              )\n            }\n            // Save schema location for this streaming relation\n            schemaLocationMap.put(streamingRelation, schemaTrackingLocation.stripSuffix(\"/\"))\n          }\n      case _ =>\n    }\n\n    // Now verify all schema locations are distinct\n    val conflictSchemaOpt = schemaLocationMap\n      .keys\n      .groupBy { rel => schemaLocationMap(rel) }\n      .find(_._2.size > 1)\n    conflictSchemaOpt.foreach { case (schemaLocation, relations) =>\n      val ds = relations.head.dataSource\n      // Pick one source that has conflict to make it more actionable for the user\n      val oneTableWithConflict = ds.catalogTable\n        .map(_.identifier.toString)\n        .getOrElse {\n          // `path` must exist\n          CaseInsensitiveMap(ds.options).get(\"path\").get\n        }\n      throw DeltaErrors.sourcesWithConflictingSchemaTrackingLocation(\n        schemaLocation, oneTableWithConflict)\n    }\n  }\n\n  /**\n   * Check and assert whether the schema tracking location is under the checkpoint location.\n   *\n   * Visible for testing.\n   */\n  private[delta] def assertSchemaTrackingLocationUnderCheckpoint(\n      checkpointLocation: String,\n      schemaTrackingLocation: String): Unit = {\n    val checkpointPath = new Path(checkpointLocation)\n    // scalastyle:off deltahadoopconfiguration\n    val checkpointFs = checkpointPath.getFileSystem(session.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    val qualifiedCheckpointPath = checkpointFs.makeQualified(checkpointPath)\n    val qualifiedSchemaTrackingLocationPath = try {\n      checkpointFs.makeQualified(new Path(schemaTrackingLocation))\n    } catch {\n      case NonFatal(e) =>\n        // This can happen when the file system for the checkpoint location is completely different\n        // from that of the schema tracking location.\n        logWarning(\"Failed to make a qualified path for schema tracking location\", e)\n        throw DeltaErrors.schemaTrackingLocationNotUnderCheckpointLocation(\n          schemaTrackingLocation, checkpointLocation)\n    }\n    // If we couldn't qualify the schema location or after relativization, the result is still an\n    // absolute path, we know the schema location is not under checkpoint.\n    if (qualifiedCheckpointPath.toUri.relativize(\n        qualifiedSchemaTrackingLocationPath.toUri).isAbsolute) {\n      throw DeltaErrors.schemaTrackingLocationNotUnderCheckpointLocation(\n        schemaTrackingLocation, checkpointLocation)\n    }\n  }\n\n  object EligibleCreateTableLikeCommand {\n    def unapply(arg: LogicalPlan): Option[(CreateTableLikeCommand, CatalogTable)] = arg match {\n      case c: CreateTableLikeCommand =>\n        val src = session.sessionState.catalog.getTempViewOrPermanentTableMetadata(c.sourceTable)\n        if (src.provider.contains(\"delta\") ||\n          c.provider.exists(DeltaSourceUtils.isDeltaDataSourceName)) {\n          Some(c, src)\n        } else {\n          None\n        }\n      case _ =>\n        None\n    }\n  }\n}\n\n/** Matchers for dealing with a Delta table. */\nobject DeltaRelation extends DeltaLogging {\n  val KEEP_AS_V2_RELATION_TAG = new TreeNodeTag[Unit](\"__keep_as_v2_relation\")\n\n  def unapply(plan: LogicalPlan): Option[LogicalRelation] = plan match {\n    case dsv2 @ DataSourceV2RelationShim(d: DeltaTableV2, _, _, _, options) =>\n      Some(fromV2Relation(d, dsv2.asInstanceOf[DataSourceV2Relation], options))\n    case lr @ DeltaTable(_) => Some(lr)\n    case _ => None\n  }\n\n  def fromV2Relation(\n      d: DeltaTableV2,\n      v2Relation: DataSourceV2Relation,\n      options: CaseInsensitiveStringMap): LogicalRelation = {\n    recordFrameProfile(\"DeltaAnalysis\", \"fromV2Relation\") {\n      val relation = d.withOptions(options.asScala.toMap).toBaseRelation\n      val output = if (CDCReader.isCDCRead(options)) {\n        // Handles cdc for the spark.read.options().table() code path\n        // Mapping needed for references to the table's columns coming from Spark Connect.\n        val newOutput = toAttributes(relation.schema)\n        newOutput.map { a =>\n          val existingReference = v2Relation.output\n            .find(e => e.name == a.name && e.dataType == a.dataType && e.nullable == a.nullable)\n          existingReference.map { e =>\n            e.copy(metadata = a.metadata)(exprId = e.exprId, qualifier = e.qualifier)\n          }.getOrElse(a)\n        }\n      } else {\n        v2Relation.output\n      }\n      LogicalRelation(relation, output, d.ttSafeCatalogTable, isStreaming = false, stream = None)\n    }\n  }\n}\n\nobject AppendDelta {\n  def unapply(a: AppendData): Option[(DataSourceV2Relation, DeltaTableV2)] = {\n    if (a.query.resolved) {\n      a.table match {\n        case r: DataSourceV2Relation if r.table.isInstanceOf[DeltaTableV2] =>\n          Some((r, r.table.asInstanceOf[DeltaTableV2]))\n        case _ => None\n      }\n    } else {\n      None\n    }\n  }\n}\n\nobject OverwriteDelta {\n  def unapply(o: OverwriteByExpression): Option[(DataSourceV2Relation, DeltaTableV2)] = {\n    if (o.query.resolved) {\n      o.table match {\n        case r: DataSourceV2Relation if r.table.isInstanceOf[DeltaTableV2] =>\n          Some((r, r.table.asInstanceOf[DeltaTableV2]))\n        case _ => None\n      }\n    } else {\n      None\n    }\n  }\n}\n\nobject DynamicPartitionOverwriteDelta {\n  def unapply(o: OverwritePartitionsDynamic): Option[(DataSourceV2Relation, DeltaTableV2)] = {\n    if (o.query.resolved) {\n      o.table match {\n        case r: DataSourceV2Relation if r.table.isInstanceOf[DeltaTableV2] =>\n          Some((r, r.table.asInstanceOf[DeltaTableV2]))\n        case _ => None\n      }\n    } else {\n      None\n    }\n  }\n}\n\n/**\n * A `RunnableCommand` that will execute dynamic partition overwrite using [[WriteIntoDelta]].\n *\n * This is a workaround of Spark not supporting V1 fallback for dynamic partition overwrite.\n * Note the following details:\n * - Extends `V2WriteCommmand` so that Spark can transform this plan in the same as other\n *   commands like `AppendData`.\n * - Exposes the query as a child so that the Spark optimizer can optimize it.\n */\ncase class DeltaDynamicPartitionOverwriteCommand(\n    table: NamedRelation,\n    deltaTable: DeltaTableV2,\n    query: LogicalPlan,\n    writeOptions: Map[String, String],\n    isByName: Boolean,\n    analyzedQuery: Option[LogicalPlan] = None) extends RunnableCommand with V2WriteCommand {\n\n  override def child: LogicalPlan = query\n\n  override def withNewQuery(newQuery: LogicalPlan): DeltaDynamicPartitionOverwriteCommand = {\n    copy(query = newQuery)\n  }\n\n  override def withNewTable(newTable: NamedRelation): DeltaDynamicPartitionOverwriteCommand = {\n    copy(table = newTable)\n  }\n\n  override def storeAnalyzedQuery(): Command = copy(analyzedQuery = Some(query))\n\n  override protected def withNewChildInternal(\n      newChild: LogicalPlan): DeltaDynamicPartitionOverwriteCommand = copy(query = newChild)\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaOptions = new DeltaOptions(\n      CaseInsensitiveMap[String](\n        deltaTable.options ++\n        writeOptions ++\n        Seq(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION ->\n          DeltaOptions.PARTITION_OVERWRITE_MODE_DYNAMIC)),\n      sparkSession.sessionState.conf)\n\n    // TODO: The configuration can be fetched directly from WriteIntoDelta's txn. Don't pass\n    //  in the default snapshot's metadata config here.\n    WriteIntoDelta(\n      deltaTable.deltaLog,\n      SaveMode.Overwrite,\n      deltaOptions,\n      partitionColumns = Nil,\n      deltaTable.deltaLog.unsafeVolatileSnapshot.metadata.configuration,\n      DataFrameUtils.ofRows(sparkSession, query),\n      deltaTable.catalogTable\n    ).run(sparkSession)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaColumnMapping.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.{Locale, UUID}\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.RowId.RowIdMetadataStructField\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.json4s.DefaultFormats\nimport org.json4s.jackson.JsonMethods._\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, QuotingUtils}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{ArrayType, DataType, MapType, Metadata => SparkMetadata, MetadataBuilder, StructField, StructType}\n\n/**\n * Information regarding a single dropped column.\n * @param fieldPath The logical path of the dropped column.\n */\nprivate[delta] case class DroppedColumn(fieldPath: Seq[String])\n\n/**\n * Information regarding a single renamed column.\n * @param fromFieldPath The logical path of the column before the rename.\n * @param toFieldPath The logical path of the column after the rename.\n */\nprivate[delta] case class RenamedColumn(fromFieldPath: Seq[String], toFieldPath: Seq[String])\n\ntrait DeltaColumnMappingBase extends DeltaLogging {\n  val PARQUET_FIELD_ID_METADATA_KEY = \"parquet.field.id\"\n  val PARQUET_FIELD_NESTED_IDS_METADATA_KEY = \"parquet.field.nested.ids\"\n  val COLUMN_MAPPING_METADATA_PREFIX = \"delta.columnMapping.\"\n  val COLUMN_MAPPING_METADATA_ID_KEY = COLUMN_MAPPING_METADATA_PREFIX + \"id\"\n  val COLUMN_MAPPING_PHYSICAL_NAME_KEY = COLUMN_MAPPING_METADATA_PREFIX + \"physicalName\"\n  val COLUMN_MAPPING_METADATA_NESTED_IDS_KEY = COLUMN_MAPPING_METADATA_PREFIX + \"nested.ids\"\n  val PARQUET_LIST_ELEMENT_FIELD_NAME = \"element\"\n  val PARQUET_MAP_KEY_FIELD_NAME = \"key\"\n  val PARQUET_MAP_VALUE_FIELD_NAME = \"value\"\n\n  /**\n   * The list of column mapping metadata for each column in the schema.\n   */\n  val COLUMN_MAPPING_METADATA_KEYS: Set[String] = Set(\n    COLUMN_MAPPING_METADATA_ID_KEY,\n    COLUMN_MAPPING_PHYSICAL_NAME_KEY,\n    COLUMN_MAPPING_METADATA_NESTED_IDS_KEY,\n    PARQUET_FIELD_ID_METADATA_KEY,\n    PARQUET_FIELD_NESTED_IDS_METADATA_KEY\n  )\n\n  /**\n   * This list of internal columns (and only this list) is allowed to have missing\n   * column mapping metadata such as field id and physical name because\n   * they might not be present in user's table schema.\n   *\n   * These fields, if materialized to parquet, will always be matched by their display name in the\n   * downstream parquet reader even under column mapping modes.\n   *\n   * For future developers who want to utilize additional internal columns without generating\n   * column mapping metadata, please add them here.\n   *\n   * This list is case-insensitive.\n   */\n  protected val DELTA_INTERNAL_COLUMNS: Set[String] =\n    (CDCReader.CDC_COLUMNS_IN_DATA ++ Seq(\n      CDCReader.CDC_COMMIT_VERSION,\n      CDCReader.CDC_COMMIT_TIMESTAMP,\n      /**\n       * Whenever `_metadata` column is selected, Spark adds the format generated metadata\n       * columns to `ParquetFileFormat`'s required output schema. Column `_metadata` contains\n       * constant value subfields metadata such as `file_path` and format specific custom metadata\n       * subfields such as `row_index` in Parquet. Spark creates the file format object with\n       * data schema plus additional custom metadata columns required from file format to fill up\n       * the `_metadata` column.\n       */\n      ParquetFileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME,\n      DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME,\n      DeltaParquetFileFormat.ROW_INDEX_COLUMN_NAME)\n    ).map(_.toLowerCase(Locale.ROOT)).toSet\n\n  val supportedModes: Set[DeltaColumnMappingMode] =\n    Set(IdMapping, NoMapping, NameMapping)\n\n  def isInternalField(field: StructField): Boolean =\n    DELTA_INTERNAL_COLUMNS.contains(field.name.toLowerCase(Locale.ROOT)) ||\n      RowIdMetadataStructField.isRowIdColumn(field) ||\n      RowCommitVersion.MetadataStructField.isRowCommitVersionColumn(field)\n\n  /**\n   * Allow NameMapping -> NoMapping transition behind a feature flag.\n   * Otherwise only NoMapping -> NameMapping is allowed.\n   */\n  private def allowMappingModeChange(\n      oldMode: DeltaColumnMappingMode,\n      newMode: DeltaColumnMappingMode): Boolean = {\n    val removalAllowed = SparkSession.getActiveSession\n      .exists(_.conf.get(DeltaSQLConf.ALLOW_COLUMN_MAPPING_REMOVAL))\n    // No change.\n    (oldMode == newMode) ||\n      // Downgrade allowed with a flag.\n      (removalAllowed && (oldMode != NoMapping && newMode == NoMapping)) ||\n      // Upgrade always allowed.\n      (oldMode == NoMapping && newMode == NameMapping)\n  }\n\n  def isColumnMappingUpgrade(\n      oldMode: DeltaColumnMappingMode,\n      newMode: DeltaColumnMappingMode): Boolean = {\n    oldMode == NoMapping && newMode != NoMapping\n  }\n\n  /**\n   * If the table is already on the column mapping protocol, we block:\n   *     - changing column mapping config\n   * otherwise, we block\n   *     - upgrading to the column mapping Protocol through configurations\n   */\n  def verifyAndUpdateMetadataChange(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      oldProtocol: Protocol,\n      oldMetadata: Metadata,\n      newMetadata: Metadata,\n      isCreatingNewTable: Boolean,\n      isOverwriteSchema: Boolean): Metadata = {\n    // field in new metadata should have been dropped\n    val oldMappingMode = oldMetadata.columnMappingMode\n    val newMappingMode = newMetadata.columnMappingMode\n\n    if (!supportedModes.contains(newMappingMode)) {\n      throw DeltaErrors.unsupportedColumnMappingMode(newMappingMode.name)\n    }\n\n    val isChangingModeOnExistingTable = oldMappingMode != newMappingMode && !isCreatingNewTable\n    if (isChangingModeOnExistingTable && !allowMappingModeChange(oldMappingMode, newMappingMode)) {\n      throw DeltaErrors.changeColumnMappingModeNotSupported(\n        oldMappingMode.name, newMappingMode.name)\n    }\n\n    var updatedMetadata = newMetadata\n\n    // If column mapping is disabled, we need to strip any column mapping metadata from the schema,\n    // because Delta code will use them even when column mapping is not enabled. However, we cannot\n    // strip column mapping metadata that already exist in the schema, because this would break\n    // the table.\n    if (newMappingMode == NoMapping &&\n        schemaHasColumnMappingMetadata(newMetadata.schema)) {\n      val addsColumnMappingMetadata = !schemaHasColumnMappingMetadata(oldMetadata.schema)\n      if (addsColumnMappingMetadata &&\n          spark.conf.get(DeltaSQLConf.DELTA_COLUMN_MAPPING_STRIP_METADATA)) {\n        recordDeltaEvent(deltaLog, opType = \"delta.columnMapping.stripMetadata\")\n        val strippedSchema = dropColumnMappingMetadata(newMetadata.schema)\n        updatedMetadata = newMetadata.copy(schemaString = strippedSchema.json)\n      } else {\n        recordDeltaEvent(\n          deltaLog,\n          opType = \"delta.columnMapping.updateSchema.metadataPresentButFeatureDisabled\",\n          data = Map(\n            \"addsColumnMappingMetadata\" -> addsColumnMappingMetadata.toString,\n            \"isCreatingNewTable\" -> isCreatingNewTable.toString,\n            \"isOverwriteSchema\" -> isOverwriteSchema.toString)\n        )\n      }\n    }\n\n    // If column mapping was disabled, but there was already column mapping in the schema, it is\n    // a result of a bug in the previous version of Delta. This should no longer happen with the\n    // stripping done above. For existing tables with this issue, we should not allow enabling\n    // column mapping, to prevent further corruption.\n    if (spark.conf.get(DeltaSQLConf.\n        DELTA_COLUMN_MAPPING_DISALLOW_ENABLING_WHEN_METADATA_ALREADY_EXISTS)) {\n      if (oldMappingMode == NoMapping && newMappingMode != NoMapping &&\n          schemaHasColumnMappingMetadata(oldMetadata.schema)) {\n        throw DeltaErrors.enablingColumnMappingDisallowedWhenColumnMappingMetadataAlreadyExists()\n      }\n    }\n\n    updatedMetadata = updateColumnMappingMetadata(\n      oldMetadata, updatedMetadata, isChangingModeOnExistingTable, isOverwriteSchema)\n\n    // record column mapping table creation/upgrade\n    if (newMappingMode != NoMapping) {\n      if (isCreatingNewTable) {\n        recordDeltaEvent(deltaLog, \"delta.columnMapping.createTable\")\n      } else if (oldMappingMode != newMappingMode) {\n        recordDeltaEvent(deltaLog, \"delta.columnMapping.upgradeTable\")\n      }\n    }\n\n    updatedMetadata\n  }\n\n  def hasColumnId(field: StructField): Boolean =\n    field.metadata.contains(COLUMN_MAPPING_METADATA_ID_KEY)\n\n  def getColumnId(field: StructField): Int =\n    field.metadata.getLong(COLUMN_MAPPING_METADATA_ID_KEY).toInt\n\n  def hasNestedColumnIds(field: StructField): Boolean =\n    field.metadata.contains(COLUMN_MAPPING_METADATA_NESTED_IDS_KEY)\n\n  def getNestedColumnIds(field: StructField): SparkMetadata =\n    field.metadata.getMetadata(COLUMN_MAPPING_METADATA_NESTED_IDS_KEY)\n\n  def getNestedColumnIdsAsLong(field: StructField): Iterable[Long] = {\n    val nestedColumnMetadata = getNestedColumnIds(field)\n    metadataToMap[Map[String, Long]](nestedColumnMetadata).values\n  }\n\n  private def metadataToMap[T <: Map[_, _]](metadata: SparkMetadata)(implicit m: Manifest[T]): T = {\n    implicit val formats: DefaultFormats.type = DefaultFormats\n    parse(metadata.json).extract[T]\n  }\n\n  def hasPhysicalName(field: StructField): Boolean =\n    field.metadata.contains(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n\n  /**\n   * Gets the required column metadata for each column based on the column mapping mode.\n   */\n  def getColumnMappingMetadata(field: StructField, mode: DeltaColumnMappingMode): SparkMetadata = {\n    mode match {\n      case NoMapping =>\n        // drop all column mapping related fields\n        new MetadataBuilder()\n          .withMetadata(field.metadata)\n          .remove(COLUMN_MAPPING_METADATA_ID_KEY)\n          .remove(COLUMN_MAPPING_METADATA_NESTED_IDS_KEY)\n          .remove(PARQUET_FIELD_ID_METADATA_KEY)\n          .remove(PARQUET_FIELD_NESTED_IDS_METADATA_KEY)\n          .remove(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n          .build()\n\n      case IdMapping | NameMapping =>\n        if (!hasColumnId(field)) {\n          throw DeltaErrors.missingColumnId(mode, field.name)\n        }\n        if (!hasPhysicalName(field)) {\n          throw DeltaErrors.missingPhysicalName(mode, field.name)\n        }\n        // Delta spec requires writer to always write field_id in parquet schema for column mapping\n        // Reader strips PARQUET_FIELD_ID_METADATA_KEY in\n        // DeltaParquetFileFormat:prepareSchemaForRead\n        val builder = new MetadataBuilder()\n          .withMetadata(field.metadata)\n          .putLong(PARQUET_FIELD_ID_METADATA_KEY, getColumnId(field))\n\n        // Nested field IDs for the 'element' and 'key'/'value' fields of Arrays\n        // and Maps are written when Uniform with IcebergCompatV2 is enabled on a table.\n        if (hasNestedColumnIds(field)) {\n          builder.putMetadata(PARQUET_FIELD_NESTED_IDS_METADATA_KEY, getNestedColumnIds(field))\n        }\n\n        builder.build()\n\n      case mode =>\n        throw DeltaErrors.unsupportedColumnMappingMode(mode.name)\n    }\n  }\n\n  /** Recursively renames columns in the given schema with their physical schema. */\n  def renameColumns(schema: StructType): StructType = {\n    SchemaMergingUtils.transformColumns(schema) { (_, field, _) =>\n      field.copy(name = getPhysicalName(field))\n    }\n  }\n\n  def assignPhysicalName(field: StructField, physicalName: String): StructField = {\n    field.copy(metadata = new MetadataBuilder()\n      .withMetadata(field.metadata)\n      .putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, physicalName)\n      .build())\n  }\n\n  def assignPhysicalNames(schema: StructType, reuseLogicalName: Boolean = false): StructType = {\n    SchemaMergingUtils.transformColumns(schema) { (_, field, _) =>\n      if (hasPhysicalName(field)) field else {\n        if (reuseLogicalName) assignPhysicalName(field, field.name)\n        else assignPhysicalName(field, generatePhysicalName)\n      }\n    }\n  }\n\n  /**\n   * Set physical name based on field path, skip if field path not found in the map. All comparisons\n   * are case-insensitive.\n   */\n  def setPhysicalNames(\n      schema: StructType,\n      fieldPathToPhysicalName: Map[Seq[String], String]): StructType = {\n    if (fieldPathToPhysicalName.isEmpty) {\n      schema\n    } else {\n      val lowerCasedFieldPathToPhysicalNameMap =\n        fieldPathToPhysicalName.map { case (k, v) => k.map(_.toLowerCase(Locale.ROOT)) -> v }\n      SchemaMergingUtils.transformColumns(schema) { (parent, field, _) =>\n        // Column comparison is case-insensitive.\n        val path = (parent :+ field.name).map(_.toLowerCase(Locale.ROOT))\n        if (lowerCasedFieldPathToPhysicalNameMap.contains(path)) {\n          assignPhysicalName(field, lowerCasedFieldPathToPhysicalNameMap(path))\n        } else {\n          field\n        }\n      }\n    }\n  }\n\n  def generatePhysicalName: String = \"col-\" + UUID.randomUUID()\n\n  def getPhysicalName(field: StructField): String = {\n    if (field.metadata.contains(COLUMN_MAPPING_PHYSICAL_NAME_KEY)) {\n      field.metadata.getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n    } else {\n      field.name\n    }\n  }\n\n  private def updateColumnMappingMetadata(\n      oldMetadata: Metadata,\n      newMetadata: Metadata,\n      isChangingModeOnExistingTable: Boolean,\n      isOverwritingSchema: Boolean): Metadata = {\n    val newMappingMode = DeltaConfigs.COLUMN_MAPPING_MODE.fromMetaData(newMetadata)\n    newMappingMode match {\n      case IdMapping | NameMapping =>\n        assignColumnIdAndPhysicalName(\n          newMetadata, oldMetadata, isChangingModeOnExistingTable, isOverwritingSchema)\n      case NoMapping =>\n        newMetadata\n      case mode =>\n         throw DeltaErrors.unsupportedColumnMappingMode(mode.name)\n    }\n  }\n\n  def findMaxColumnId(schema: StructType): Long = {\n    var maxId: Long = 0\n    SchemaMergingUtils.transformColumns(schema)((_, f, _) => {\n      if (hasColumnId(f)) {\n        maxId = maxId max getColumnId(f)\n        if (hasNestedColumnIds(f)) {\n          val nestedIds = getNestedColumnIdsAsLong(f)\n          maxId = maxId max (if (nestedIds.nonEmpty) nestedIds.max else 0)\n        }\n      }\n      f\n    })\n    maxId\n  }\n\n  /**\n   * Verify the metadata for valid column mapping metadata assignment. This is triggered for every\n   * commit as a last defense.\n   *\n   * 1. Ensure column mapping metadata is set for the appropriate mode\n   * 2. Ensure no duplicate column id/physical names set\n   * 3. Ensure max column id is in a good state (set, and greater than all field ids available)\n   */\n  def checkColumnIdAndPhysicalNameAssignments(metadata: Metadata): Unit = {\n    val schema = metadata.schema\n    val mode = metadata.columnMappingMode\n\n    // physical name/column id -> full field path\n    val columnIds = mutable.Set[Int]()\n    val physicalNames = mutable.Set[String]()\n    // use id mapping to keep all column mapping metadata\n    // this method checks for missing physical name & column id already\n    val physicalSchema = createPhysicalSchema(schema, schema, IdMapping, checkSupportedMode = false)\n\n    // Check id / physical name duplication\n    SchemaMergingUtils.transformColumns(physicalSchema) ((parentPhysicalPath, field, _) => {\n      // field.name is now physical name\n      // We also need to apply backticks to column paths with dots in them to prevent a possible\n      // false alarm in which a column `a.b` is duplicated with `a`.`b`\n      val curFullPhysicalPath = UnresolvedAttribute(parentPhysicalPath :+ field.name).name\n      val columnId = getColumnId(field)\n      if (columnIds.contains(columnId)) {\n        throw DeltaErrors.duplicatedColumnId(mode, columnId, schema)\n      }\n      columnIds.add(columnId)\n\n      // We should check duplication by full physical name path, because nested fields\n      // such as `a.b.c` shouldn't conflict with `x.y.c` due to same column name.\n      if (physicalNames.contains(curFullPhysicalPath)) {\n        throw DeltaErrors.duplicatedPhysicalName(mode, curFullPhysicalPath, schema)\n      }\n      physicalNames.add(curFullPhysicalPath)\n\n      field\n    })\n\n    // Check assignment of the max id property\n    if (SQLConf.get.getConf(DeltaSQLConf.DELTA_COLUMN_MAPPING_CHECK_MAX_COLUMN_ID)) {\n      if (!metadata.configuration.contains(DeltaConfigs.COLUMN_MAPPING_MAX_ID.key)) {\n        throw DeltaErrors.maxColumnIdNotSet\n      }\n      val fieldMaxId = DeltaColumnMapping.findMaxColumnId(schema)\n      if (metadata.columnMappingMaxId < DeltaColumnMapping.findMaxColumnId(schema)) {\n        throw DeltaErrors.maxColumnIdNotSetCorrectly(metadata.columnMappingMaxId, fieldMaxId)\n      }\n    }\n  }\n\n  /**\n   * For each column/field in a Metadata's schema, assign id using the current maximum id\n   * as the basis and increment from there, and assign physical name using UUID\n   * @param newMetadata The new metadata to assign Ids and physical names\n   * @param oldMetadata The old metadata\n   * @param isChangingModeOnExistingTable whether this is part of a commit that changes the\n   *                                      mapping mode on a existing table\n   * @return new metadata with Ids and physical names assigned\n   */\n  def assignColumnIdAndPhysicalName(\n      newMetadata: Metadata,\n      oldMetadata: Metadata,\n      isChangingModeOnExistingTable: Boolean,\n      isOverwritingSchema: Boolean): Metadata = {\n    val rawSchema = newMetadata.schema\n    var maxId = DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMetaData(newMetadata) max\n      DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMetaData(oldMetadata) max\n      findMaxColumnId(rawSchema)\n    val startId = maxId\n    val newSchema =\n      SchemaMergingUtils.transformColumns(rawSchema)((path, field, _) => {\n        val builder = new MetadataBuilder().withMetadata(field.metadata)\n\n        lazy val fullName = path :+ field.name\n        lazy val existingFieldOpt =\n          SchemaUtils.findNestedFieldIgnoreCase(\n            oldMetadata.schema, fullName, includeCollections = true)\n        lazy val canReuseColumnMappingMetadataDuringOverwrite = {\n          val canReuse =\n            isOverwritingSchema &&\n              SparkSession.getActiveSession.exists(\n                _.conf.get(DeltaSQLConf.REUSE_COLUMN_MAPPING_METADATA_DURING_OVERWRITE)) &&\n              existingFieldOpt.exists { existingField =>\n                // Ensure data type & nullability are compatible\n                DataType.equalsIgnoreCompatibleNullability(\n                  from = existingField.dataType,\n                  to = field.dataType\n                )\n              }\n          if (canReuse) {\n            require(!isChangingModeOnExistingTable,\n              \"Cannot change column mapping mode while overwriting the table\")\n            assert(hasColumnId(existingFieldOpt.get) && hasPhysicalName(existingFieldOpt.get))\n          }\n          canReuse\n        }\n\n        if (!hasColumnId(field)) {\n          val columnId = if (canReuseColumnMappingMetadataDuringOverwrite) {\n            getColumnId(existingFieldOpt.get)\n          } else {\n            maxId += 1\n            maxId\n          }\n\n          builder.putLong(COLUMN_MAPPING_METADATA_ID_KEY, columnId)\n        }\n        if (!hasPhysicalName(field)) {\n          val physicalName = if (isChangingModeOnExistingTable) {\n            if (existingFieldOpt.isEmpty) {\n              if (oldMetadata.schema.isEmpty) {\n                // We should relax the check for tables that have both an empty schema\n                // and no data. Assumption: no schema => no data\n                generatePhysicalName\n              } else throw DeltaErrors.schemaChangeDuringMappingModeChangeNotSupported(\n                oldMetadata.schema, newMetadata.schema)\n            } else {\n              // When changing from NoMapping to NameMapping mode, we directly use old display names\n              // as physical names. This is by design: 1) We don't need to rewrite the\n              // existing Parquet files, and 2) display names in no-mapping mode have all the\n              // properties required for physical names: unique, stable and compliant with Parquet\n              // column naming restrictions.\n              existingFieldOpt.get.name\n            }\n          } else if (canReuseColumnMappingMetadataDuringOverwrite) {\n            // Copy the physical name metadata over from the existing field if possible\n            getPhysicalName(existingFieldOpt.get)\n          } else {\n            generatePhysicalName\n          }\n\n          builder.putString(COLUMN_MAPPING_PHYSICAL_NAME_KEY, physicalName)\n        }\n        field.copy(metadata = builder.build())\n      })\n\n    // Starting from IcebergCompatV2, we require writing field-id for List/Map nested fields\n    val (finalSchema, newMaxId) = if (IcebergCompat.isGeqEnabled(newMetadata, 2)) {\n      rewriteFieldIdsForIceberg(newSchema, maxId)\n    } else {\n      (newSchema, maxId)\n    }\n\n    newMetadata.copy(\n      schemaString = finalSchema.json,\n      configuration = newMetadata.configuration\n        ++ Map(DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> newMaxId.toString)\n    )\n  }\n\n  def dropColumnMappingMetadata(schema: StructType): StructType = {\n    SchemaMergingUtils.transformColumns(schema) { (_, field, _) =>\n      var strippedMetadataBuilder = new MetadataBuilder().withMetadata(field.metadata)\n      for (key <- COLUMN_MAPPING_METADATA_KEYS) {\n        strippedMetadataBuilder = strippedMetadataBuilder.remove(key)\n      }\n      val strippedMetadata = strippedMetadataBuilder.build()\n      field.copy(metadata = strippedMetadata)\n    }\n  }\n\n  def filterColumnMappingProperties(properties: Map[String, String]): Map[String, String] = {\n    properties.filterKeys(_ != DeltaConfigs.COLUMN_MAPPING_MAX_ID.key).toMap\n  }\n\n  // Verify the values of internal column mapping properties are the same in two sets of config\n  // ONLY if the config is present in both sets of properties.\n  def verifyInternalProperties(one: Map[String, String], two: Map[String, String]): Boolean = {\n    val key = DeltaConfigs.COLUMN_MAPPING_MAX_ID.key\n    one.get(key).forall(value => value == two.getOrElse(key, value))\n  }\n\n  /**\n   * Create a physical schema for the given schema using the Delta table schema as a reference.\n   *\n   * @param schema the given logical schema (potentially without any metadata)\n   * @param referenceSchema the schema from the delta log, which has all the metadata\n   * @param columnMappingMode column mapping mode of the delta table, which determines which\n   *                          metadata to fill in\n   * @param checkSupportedMode whether we should check of the column mapping mode is supported\n   */\n  def createPhysicalSchema(\n      schema: StructType,\n      referenceSchema: StructType,\n      columnMappingMode: DeltaColumnMappingMode,\n      checkSupportedMode: Boolean = true): StructType = {\n    if (columnMappingMode == NoMapping) {\n      return schema\n    }\n\n    // createPhysicalSchema is the narrow-waist for both read/write code path\n    // so we could check for mode support here\n    if (checkSupportedMode && !supportedModes.contains(columnMappingMode)) {\n      throw DeltaErrors.unsupportedColumnMappingMode(columnMappingMode.name)\n    }\n\n    val referenceSchemaColumnMap: Map[String, StructField] =\n      SchemaMergingUtils.explode(referenceSchema).map { case (path, field) =>\n        QuotingUtils.quoteNameParts(path).toLowerCase(Locale.ROOT) -> field\n      }.toMap\n\n    SchemaMergingUtils.transformColumns(schema) { (path, field, _) =>\n      val fullName = path :+ field.name\n      val inSchema =\n        referenceSchemaColumnMap.get(QuotingUtils.quoteNameParts(fullName).toLowerCase(Locale.ROOT))\n      inSchema.map { refField =>\n        val sparkMetadata = getColumnMappingMetadata(refField, columnMappingMode)\n        field.copy(metadata = sparkMetadata, name = getPhysicalName(refField))\n      }.getOrElse {\n        if (isInternalField(field)) {\n          field\n        } else {\n          throw DeltaErrors.columnNotFound(fullName, referenceSchema)\n        }\n      }\n    }\n  }\n\n  /**\n   * Create a list of physical attributes for the given attributes using the table schema as a\n   * reference.\n   *\n   * @param output the list of attributes (potentially without any metadata)\n   * @param referenceSchema   the table schema with all the metadata\n   * @param columnMappingMode column mapping mode of the delta table, which determines which\n   *                          metadata to fill in\n   */\n  def createPhysicalAttributes(\n      output: Seq[Attribute],\n      referenceSchema: StructType,\n      columnMappingMode: DeltaColumnMappingMode): Seq[Attribute] = {\n    // Assign correct column mapping info to columns according to the schema\n    val struct = createPhysicalSchema(output.toStructType, referenceSchema, columnMappingMode)\n    output.zip(struct).map { case (attr, field) =>\n      attr.withDataType(field.dataType) // for recursive column names and metadata\n        .withMetadata(field.metadata)\n        .withName(field.name)\n    }\n  }\n\n  /**\n   * Returns a map of physicalNamePath -> field for the given `schema`, where\n   * physicalNamePath is the [$parentPhysicalName, ..., $fieldPhysicalName] list of physical names\n   * for every field (including nested) in the `schema`.\n   *\n   * Must be called after `checkColumnIdAndPhysicalNameAssignments`, so that we know the schema\n   * is valid.\n   */\n  def getPhysicalNameFieldMap(schema: StructType): Map[Seq[String], StructField] = {\n    val physicalSchema = renameColumns(schema)\n    val physicalSchemaFieldPaths = SchemaMergingUtils.explode(physicalSchema).map(_._1)\n    val originalSchemaFields = SchemaMergingUtils.explode(schema).map(_._2)\n    physicalSchemaFieldPaths.zip(originalSchemaFields).toMap\n  }\n\n  /**\n   * Returns a map from the logical name paths to the physical name paths for the given schema.\n   * The logical name path is the result of splitting a multi-part identifier, and the physical name\n   * path is result of replacing all names in the logical name path with their physical names.\n   */\n  def getLogicalNameToPhysicalNameMap(schema: StructType): Map[Seq[String], Seq[String]] = {\n    val physicalSchema = renameColumns(schema)\n    val logicalSchemaFieldPaths = SchemaMergingUtils.explode(schema).map(_._1)\n    val physicalSchemaFieldPaths = SchemaMergingUtils.explode(physicalSchema).map(_._1)\n    logicalSchemaFieldPaths.zip(physicalSchemaFieldPaths).toMap\n  }\n\n  /**\n   * Returns a map from the physical name paths to the logical name paths for the given schema.\n   * The logical name path is the result of splitting a multi-part identifier, and the physical name\n   * path is result of replacing all names in the logical name path with their physical names.\n   */\n  def getPhysicalNameToLogicalNameMap(schema: StructType): Map[Seq[String], Seq[String]] = {\n    getLogicalNameToPhysicalNameMap(schema).map(_.swap)\n  }\n\n  /**\n   * Returns true if Column Mapping mode is enabled and the newMetadata's schema, when compared to\n   * the currentMetadata's schema, is indicative of a DROP COLUMN operation.\n   *\n   * We detect DROP COLUMNS by checking if any physical name in `currentSchema` is missing in\n   * `newSchema`.\n   */\n  def isDropColumnOperation(\n      newSchema: StructType,\n      currentSchema: StructType,\n      isBothColumnMappingEnabled: Boolean): Boolean = {\n\n    // We will need to compare the new schema's physical columns to the current schema's physical\n    // columns. So, they both must have column mapping enabled.\n    if (!isBothColumnMappingEnabled) {\n      return false\n    }\n\n    val newPhysicalToLogicalMap = getPhysicalNameFieldMap(newSchema)\n    val currentPhysicalToLogicalMap = getPhysicalNameFieldMap(currentSchema)\n\n    // are any of the current physical names missing in the new schema?\n    currentPhysicalToLogicalMap\n      .keys\n      .exists { k => !newPhysicalToLogicalMap.contains(k) }\n  }\n\n  /**\n   * Collects the columns that were dropped between the new schema and the current schema.\n   * @param newSchema The new schema after a potential drop.\n   * @param currentSchema The current schema before the drop.\n   * @return A sequence of column names that were dropped\n   */\n  def collectDroppedColumns(\n      newSchema: StructType,\n      currentSchema: StructType): Seq[DroppedColumn] = {\n    val newPhysicalToLogicalMap = getPhysicalNameToLogicalNameMap(newSchema)\n    val currentPhysicalToLogicalMap = getPhysicalNameToLogicalNameMap(currentSchema)\n\n    // are any of the current physical names missing in the new schema?\n    currentPhysicalToLogicalMap\n      .keySet\n      .diff(newPhysicalToLogicalMap.keySet)\n      .map { droppedPhysicalPath =>\n        DroppedColumn(currentPhysicalToLogicalMap(droppedPhysicalPath))\n      }\n      .toSeq\n  }\n\n  /**\n   * Returns true if Column Mapping mode is enabled and the newMetadata's schema, when compared to\n   * the currentMetadata's schema, is indicative of a RENAME COLUMN operation.\n   *\n   * We detect RENAME COLUMNS by checking if any two columns with the same physical name have\n   * different logical names\n   */\n  def isRenameColumnOperation(\n      newSchema: StructType,\n      currentSchema: StructType,\n      isBothColumnMappingEnabled: Boolean): Boolean = {\n\n    // We will need to compare the new schema's physical columns to the current schema's physical\n    // columns. So, they both must have column mapping enabled.\n    if (!isBothColumnMappingEnabled) {\n      return false\n    }\n\n    val newPhysicalToLogicalMap = getPhysicalNameFieldMap(newSchema)\n    val currentPhysicalToLogicalMap = getPhysicalNameFieldMap(currentSchema)\n\n    // do any two columns with the same physical name have different logical names?\n    currentPhysicalToLogicalMap\n      .exists { case (physicalPath, field) =>\n        newPhysicalToLogicalMap.get(physicalPath).exists(_.name != field.name)\n      }\n  }\n\n  /**\n   * Returns true if there is a column mapping schema change (drop/rename) or an incompatible\n   * partition column change between the new and current schemas.\n   */\n  def hasColMappingOrPartitionSchemaChange(\n      newSchema: StructType,\n      currentSchema: StructType,\n      newPartitionColumns: Seq[String],\n      oldPartitionColumns: Seq[String],\n      isBothColumnMappingEnabled: Boolean): Boolean = {\n    isDropColumnOperation(newSchema, currentSchema, isBothColumnMappingEnabled) ||\n      isRenameColumnOperation(newSchema, currentSchema, isBothColumnMappingEnabled) ||\n      !SchemaUtils.isPartitionCompatible(newPartitionColumns, oldPartitionColumns)\n  }\n\n  /**\n   * Collects the column rename operations between the new schema and the current schema.\n   * @param newSchema The new schema after a potential rename.\n   * @param currentSchema The current schema before the rename.\n   * @return A sequence of (oldName, newName) tuples representing the column before and after rename\n   */\n  def collectRenamedColumns(\n      newSchema: StructType,\n      currentSchema: StructType): Seq[RenamedColumn] = {\n    val newPhysicalToLogicalMap = getPhysicalNameToLogicalNameMap(newSchema)\n    val currentPhysicalToLogicalMap = getPhysicalNameToLogicalNameMap(currentSchema)\n\n    // do any two columns with the same physical name have different logical names?\n    currentPhysicalToLogicalMap\n      .flatMap { case (physicalPath, logicalPath) =>\n        newPhysicalToLogicalMap.get(physicalPath).flatMap { newLogicalPath =>\n          if (logicalPath.last != newLogicalPath.last) {\n            Some(RenamedColumn(logicalPath, newLogicalPath))\n          } else None\n        }\n      }.toSet.toSeq\n  }\n\n  /**\n   * Compare the old metadata's schema with new metadata's schema for column mapping schema changes.\n   * Also check for repartition because we need to fail fast when repartition detected.\n   *\n   * newMetadata's snapshot version must be >= oldMetadata's snapshot version so we could reliably\n   * detect the difference between ADD COLUMN and DROP COLUMN.\n   *\n   * As of now, `newMetadata` is column mapping read compatible with `oldMetadata` if\n   * no rename column or drop column has happened in-between.\n   */\n  def hasNoColumnMappingSchemaChanges(newMetadata: Metadata, oldMetadata: Metadata,\n      allowUnsafeReadOnPartitionChanges: Boolean = false): Boolean = {\n    def hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata: Metadata,\n        oldMetadata: Metadata): Boolean = {\n      val isBothColumnMappingEnabled =\n        newMetadata.columnMappingMode != NoMapping && oldMetadata.columnMappingMode != NoMapping\n      hasColMappingOrPartitionSchemaChange(\n        newMetadata.schema,\n        oldMetadata.schema,\n        // if allow unsafe row read for partition change, ignore the check\n        if (allowUnsafeReadOnPartitionChanges) Seq.empty else newMetadata.partitionColumns,\n        if (allowUnsafeReadOnPartitionChanges) Seq.empty else oldMetadata.partitionColumns,\n        isBothColumnMappingEnabled)\n    }\n\n    val (oldMode, newMode) = (oldMetadata.columnMappingMode, newMetadata.columnMappingMode)\n    if (oldMode != NoMapping && newMode != NoMapping) {\n      require(oldMode == newMode, \"changing mode is not supported\")\n      // Both changes are post column mapping enabled\n      !hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata, oldMetadata)\n    } else if (oldMode == NoMapping && newMode != NoMapping) {\n      // The old metadata does not have column mapping while the new metadata does, in this case\n      // we assume an upgrade has happened in between.\n      // So we manually construct a post-upgrade schema for the old metadata and compare that with\n      // the new metadata, as the upgrade would use the logical name as the physical name, we could\n      // easily capture any difference in the schema using the same is{Drop,Rename}ColumnOperation\n      // utils.\n      var upgradedMetadata = assignColumnIdAndPhysicalName(\n        oldMetadata, oldMetadata, isChangingModeOnExistingTable = true, isOverwritingSchema = false\n      )\n      // need to change to a column mapping mode too so the utils below can recognize\n      upgradedMetadata = upgradedMetadata.copy(\n        configuration = upgradedMetadata.configuration ++\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> newMetadata.columnMappingMode.name)\n      )\n      // use the same check\n      !hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata, upgradedMetadata)\n    } else {\n      // Prohibit reading across a downgrade.\n      val isDowngrade = oldMode != NoMapping && newMode == NoMapping\n      !isDowngrade\n    }\n  }\n\n  /**\n   * Adds the nested field IDs required by Iceberg.\n   *\n   * In parquet, list-type columns have a nested, implicitly defined [[element]] field and\n   * map-type columns have implicitly defined [[key]] and [[value]] fields. By default,\n   * Spark does not write field IDs for these fields in the parquet files. However, Iceberg\n   * requires these *nested* field IDs to be present. This method rewrites the specified\n   * Spark schema to add those nested field IDs.\n   *\n   * As list and map types are not [[StructField]]s themselves, nested field IDs are stored in\n   * a map as part of the metadata of the *nearest* parent [[StructField]]. For example, consider\n   * the following schema:\n   *\n   * col1 ARRAY(INT)\n   * col2 MAP(INT, INT)\n   * col3 STRUCT(a INT, b ARRAY(STRUCT(c INT, d MAP(INT, INT))))\n   *\n   * col1 is a list and so requires one nested field ID for the [[element]] field in parquet.\n   * This nested field ID will be stored in a map that is part of col1's [[StructField.metadata]].\n   * The same applies to the nested field IDs for col2's implicit [[key]] and [[value]] fields.\n   * col3 itself is a Struct, consisting of an integer field and a list field named 'b'. The\n   * nested field ID for the list of 'b' is stored in b's StructField metadata. Finally, the\n   * list type itself is again a struct consisting of an integer field and a map field named 'd'.\n   * The nested field IDs for the map of 'd' are stored in d's StructField metadata.\n   *\n   * @param schema  The schema to which nested field IDs should be added\n   * @param startId The first field ID to use for the nested field IDs\n   */\n  def rewriteFieldIdsForIceberg(schema: StructType, startId: Long): (StructType, Long) = {\n    var currFieldId = startId\n\n    def initNestedIdsMetadata(field: StructField): MetadataBuilder = {\n      if (hasNestedColumnIds(field)) {\n        new MetadataBuilder().withMetadata(getNestedColumnIds(field))\n      } else {\n        new MetadataBuilder()\n      }\n    }\n\n    /*\n     * Helper to add the next field ID to the specified [[MetadataBuilder]] under\n     * the specified key. This method first checks whether this is an existing nested\n     * field or a newly added nested field. New field IDs are only assigned to newly\n     * added nested fields.\n     */\n    def updateFieldId(metadata: MetadataBuilder, key: String): Unit = {\n      if (!metadata.build().contains(key)) {\n        currFieldId += 1\n        metadata.putLong(key, currFieldId)\n      }\n    }\n\n    /*\n     * Recursively adds nested field IDs for the passed data type in pre-order,\n     * ensuring uniqueness of field IDs.\n     *\n     * @param dt The data type that should be transformed\n     * @param nestedIds A MetadataBuilder that keeps track of the nested field ID\n     *                  assignment. This metadata is added to the parent field.\n     * @param path The current field path relative to the parent field\n     */\n    def transform[E <: DataType](dt: E, nestedIds: MetadataBuilder, path: Seq[String]): E = {\n      val newDt = dt match {\n        case StructType(fields) =>\n          StructType(fields.map { field =>\n            val newNestedIds = initNestedIdsMetadata(field)\n            val newDt = transform(field.dataType, newNestedIds, Seq(getPhysicalName(field)))\n            val newFieldMetadata = new MetadataBuilder().withMetadata(field.metadata).putMetadata(\n              COLUMN_MAPPING_METADATA_NESTED_IDS_KEY, newNestedIds.build()).build()\n            field.copy(dataType = newDt, metadata = newFieldMetadata)\n          })\n        case ArrayType(elementType, containsNull) =>\n          // update element type metadata and recurse into element type\n          val elemPath = path :+ PARQUET_LIST_ELEMENT_FIELD_NAME\n          updateFieldId(nestedIds, elemPath.mkString(\".\"))\n          val elementDt = transform(elementType, nestedIds, elemPath)\n          // return new array type with updated metadata\n          ArrayType(elementDt, containsNull)\n        case MapType(keyType, valType, valueContainsNull) =>\n          // update key type metadata and recurse into key type\n          val keyPath = path :+ PARQUET_MAP_KEY_FIELD_NAME\n          updateFieldId(nestedIds, keyPath.mkString(\".\"))\n          val keyDt = transform(keyType, nestedIds, keyPath)\n          // update value type metadata and recurse into value type\n          val valPath = path :+ PARQUET_MAP_VALUE_FIELD_NAME\n          updateFieldId(nestedIds, valPath.mkString(\".\"))\n          val valDt = transform(valType, nestedIds, valPath)\n          // return new map type with updated metadata\n          MapType(keyDt, valDt, valueContainsNull)\n        case other => other\n      }\n      newDt.asInstanceOf[E]\n    }\n\n    (transform(schema, new MetadataBuilder(), Seq.empty), currFieldId)\n  }\n\n  /**\n   * Returns whether the schema contains any metadata reserved for column mapping.\n   */\n  def schemaHasColumnMappingMetadata(schema: StructType): Boolean = {\n    SchemaMergingUtils.explode(schema).exists { case (_, col) =>\n      COLUMN_MAPPING_METADATA_KEYS.exists(k => col.metadata.contains(k))\n    }\n  }\n}\n\nobject DeltaColumnMapping extends DeltaColumnMappingBase\n\n/**\n * A trait for Delta column mapping modes.\n */\nsealed trait DeltaColumnMappingMode {\n  def name: String\n}\n\n/**\n * No mapping mode uses a column's display name as its true identifier to\n * read and write data.\n *\n * This is the default mode and is the same mode as Delta always has been.\n */\ncase object NoMapping extends DeltaColumnMappingMode {\n  val name = \"none\"\n}\n\n/**\n * Id Mapping uses column ID as the true identifier of a column. Column IDs are stored as\n * StructField metadata in the schema and will be used when reading and writing Parquet files.\n * The Parquet files in this mode will also have corresponding field Ids for each column in their\n * file schema.\n *\n * This mode is used for tables converted from Iceberg.\n */\ncase object IdMapping extends DeltaColumnMappingMode {\n  val name = \"id\"\n}\n\n/**\n * Name Mapping uses the physical column name as the true identifier of a column. The physical name\n * is stored as part of StructField metadata in the schema and will be used when reading and writing\n * Parquet files. Even if id mapping can be used for reading the physical files, name mapping is\n * used for reading statistics and partition values in the DeltaLog.\n */\ncase object NameMapping extends DeltaColumnMappingMode {\n  val name = \"name\"\n}\n\nobject DeltaColumnMappingMode {\n  def apply(columnMappingModeString: String): DeltaColumnMappingMode = {\n    val columnMappingModeLowerCaseString =\n      Option(columnMappingModeString)\n        .map(_.toLowerCase(Locale.ROOT))\n        .getOrElse(throw DeltaErrors.unsupportedColumnMappingModeException(columnMappingModeString))\n    columnMappingModeLowerCaseString match {\n      case NoMapping.name => NoMapping\n      case IdMapping.name => IdMapping\n      case NameMapping.name => NameMapping\n      case mode => throw DeltaErrors.unsupportedColumnMappingMode(mode)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaCommitTag.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.CommitInfo\n\n/** Marker trait for a commit tag used by delta. */\nsealed trait DeltaCommitTag {\n\n  /** Key to be used in the commit tags `Map[String, String]`. */\n  def key: String\n\n  /**\n   * Combine tags coming from multiple sub-jobs into a single tag according to the tags'\n   * semantics.\n   */\n  def merge(left: String, right: String): String\n}\n\nobject DeltaCommitTag {\n\n  trait TypedCommitTag[ValueT] extends DeltaCommitTag {\n\n    /**\n     * Combine tags coming from multiple sub-jobs into a single tag according to the tags'\n     * semantics.\n     */\n    def mergeTyped(left: ValueT, right: ValueT): ValueT\n\n    override def merge(left: String, right: String): String =\n      valueToString(mergeTyped(valueFromString(left), valueFromString(right)))\n\n    /**\n     * Combine tags coming from multiple sub-jobs into a single tag according to the tags'\n     * semantics.\n     *\n     * This variant is used when adding a new typed value to a potentially existing value from a\n     * `Map[<tagtype>, String]`.\n     */\n    def mergeWithNewTypedValue(existingOpt: Option[String], newValue: ValueT): String = {\n      existingOpt match {\n        case Some(existing) => valueToString(mergeTyped(valueFromString(existing), newValue))\n        case None => valueToString(newValue)\n      }\n    }\n\n    /** Deserialize a value for this tag from String. */\n    def valueFromString(s: String): ValueT\n\n    /** Serialize a value for this tag to String. */\n    def valueToString(value: ValueT): String = value.toString\n\n    def withValue(value: ValueT): TypedCommitTagPair[ValueT] = TypedCommitTagPair(this, value)\n  }\n\n  final case class TypedCommitTagPair[ValueT](tag: TypedCommitTag[ValueT], value: ValueT) {\n    /** Produce a tuple for inserting into `Map[DeltaCommitTag, String]` instances. */\n    def stringValue: (DeltaCommitTag, String) = tag -> tag.valueToString(value)\n\n    /** Produce a tuple for inserting into `Map[String, String]` instances. */\n    def stringPair: (String, String) = tag.key -> tag.valueToString(value)\n  }\n\n  /** Any [[DeltaCommitTag]] where `ValueT` is `Boolean`. */\n  trait BooleanCommitTag extends TypedCommitTag[Boolean] {\n    override def valueFromString(value: String): Boolean = value.toBoolean\n  }\n\n  /**\n   * Tag to indicate whether the operation preserved row tracking. If not set, it is assumed that\n   * the operation did not preserve row tracking.\n   */\n  case object PreservedRowTrackingTag extends BooleanCommitTag {\n    override val key = \"delta.rowTracking.preserved\"\n\n    override def mergeTyped(left: Boolean, right: Boolean): Boolean = left && right\n  }\n\n  /**\n   * Tag to indicate whether the commit only does row tracking enablement in its metadata update.\n   * Used to allow some concurrent txns not to fail on metadata update.\n   */\n  case object RowTrackingEnablementOnlyTag extends BooleanCommitTag {\n    override val key = \"rowTrackingEnablementOnly\"\n\n    override def mergeTyped(left: Boolean, right: Boolean): Boolean = left && right\n  }\n\n  /**\n   * Returns the tagKey value in the CommitInfo, if it exists.\n   */\n  def getTagValueFromCommitInfo(commitInfo: Option[CommitInfo], tagKey: String): Option[String] = {\n    commitInfo.flatMap { ci =>\n      ci.tags.flatMap(_.get(tagKey))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaConfig.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.{HashMap, Locale}\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.actions.{Action, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.hooks.AutoCompactType\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.{DataSkippingReaderConf, StatisticsCollection}\nimport org.apache.spark.sql.delta.util.{DeltaSqlParserUtils, JsonUtils}\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.util.{DateTimeConstants, IntervalUtils}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.unsafe.types.{CalendarInterval, UTF8String}\nimport org.apache.spark.util.Utils\n\ncase class DeltaConfig[T](\n    key: String,\n    defaultValue: String,\n    fromString: String => T,\n    validationFunction: T => Boolean,\n    helpMessage: String,\n    editable: Boolean = true,\n    alternateKeys: Seq[String] = Seq.empty) {\n  /**\n   * Recover the saved value of this configuration from `Metadata`. If undefined, fall back to\n   * alternate keys, returning defaultValue if none matches.\n   */\n  def fromMetaData(metadata: Metadata): T = {\n    fromMap(metadata.configuration)\n  }\n\n  /**\n   * Recover the saved value of this configuration from `Metadata`. If undefined, fall back to\n   * alternate keys, returning `None` if none matches.\n   */\n  protected[delta] def fromMetaDataOption(metadata: Metadata): Option[T] = {\n    fromMapOption(metadata.configuration)\n  }\n\n  def fromMap(configs: Map[String, String]): T = {\n    fromMapOption(configs).getOrElse(fromString(defaultValue))\n  }\n\n  protected[delta] def fromMapOption(configs: Map[String, String]): Option[T] = {\n    for (k <- key +: alternateKeys) {\n      configs.get(k) match {\n        case Some(value) => return Some(fromString(value))\n        case None => // keep looking\n      }\n    }\n    None\n  }\n\n  /** Validate the setting for this configuration */\n  private def validate(value: String): Unit = {\n    if (!editable) {\n      throw DeltaErrors.cannotModifyTableProperty(key)\n    }\n    val onErrorMessage = s\"$key $helpMessage\"\n    try {\n      require(validationFunction(fromString(value)), onErrorMessage)\n    } catch {\n      case e: NumberFormatException =>\n        throw new IllegalArgumentException(onErrorMessage, e)\n    }\n  }\n\n  /**\n   * Validate this configuration and return the key - value pair to save into the metadata.\n   */\n  def apply(value: String): (String, String) = {\n    validate(value)\n    key -> value\n  }\n\n  /**\n   * SQL configuration to set for ensuring that all newly created tables have this table property.\n   */\n  def defaultTablePropertyKey: String = DeltaConfigs.sqlConfPrefix + key.stripPrefix(\"delta.\")\n}\n\n/**\n * Contains list of reservoir configs and validation checks.\n */\ntrait DeltaConfigsBase extends DeltaLogging {\n\n  // Special properties stored in the Hive MetaStore that specifies which version last updated\n  // the entry in the MetaStore with the latest schema and table property information\n  val METASTORE_LAST_UPDATE_VERSION = \"delta.lastUpdateVersion\"\n  val METASTORE_LAST_COMMIT_TIMESTAMP = \"delta.lastCommitTimestamp\"\n\n  /**\n   * Convert a string to [[CalendarInterval]]. This method is case-insensitive and will throw\n   * [[IllegalArgumentException]] when the input string is not a valid interval.\n   *\n   * TODO Remove this method and use `CalendarInterval.fromCaseInsensitiveString` instead when\n   * upgrading Spark. This is a fork version of `CalendarInterval.fromCaseInsensitiveString` which\n   * will be available in the next Spark release (See SPARK-27735).\n   *\n   * @throws IllegalArgumentException if the string is not a valid internal.\n   */\n  def parseCalendarInterval(s: String): CalendarInterval = {\n    if (s == null || s.trim.isEmpty) {\n      throw DeltaErrors.emptyCalendarInterval\n    }\n    val sInLowerCase = s.trim.toLowerCase(Locale.ROOT)\n    val interval =\n      if (sInLowerCase.startsWith(\"interval \")) sInLowerCase else \"interval \" + sInLowerCase\n    val cal = IntervalUtils.safeStringToInterval(UTF8String.fromString(interval))\n    if (cal == null) {\n      throw DeltaErrors.invalidInterval(s)\n    }\n    cal\n  }\n\n  /**\n   * The prefix for a category of special configs for delta universal format to support the\n   * user facing config naming convention for different table formats:\n   * \"delta.universalFormat.config.[iceberg/hudi].[config_name]\"\n   * Note that config_name can be arbitrary.\n   */\n  final val DELTA_UNIVERSAL_FORMAT_CONFIG_PREFIX = \"delta.universalformat.config.\"\n\n  final val DELTA_UNIVERSAL_FORMAT_ICEBERG_CONFIG_PREFIX =\n    s\"${DELTA_UNIVERSAL_FORMAT_CONFIG_PREFIX}iceberg.\"\n\n  /**\n   * A global default value set as a SQLConf will overwrite the default value of a DeltaConfig.\n   * For example, user can run:\n   *   set spark.databricks.delta.properties.defaults.randomPrefixLength = 5\n   * This setting will be populated to a Delta table during its creation time and overwrites\n   * the default value of delta.randomPrefixLength.\n   *\n   * We accept these SQLConfs as strings and only perform validation in DeltaConfig. All the\n   * DeltaConfigs set in SQLConf should adopt the same prefix.\n   */\n  val sqlConfPrefix = \"spark.databricks.delta.properties.defaults.\"\n\n  private[delta] val entries = new HashMap[String, DeltaConfig[_]]\n\n  protected def buildConfig[T](\n      key: String,\n      defaultValue: String,\n      fromString: String => T,\n      validationFunction: T => Boolean,\n      helpMessage: String,\n      userConfigurable: Boolean = true,\n      alternateConfs: Seq[DeltaConfig[T]] = Seq.empty): DeltaConfig[T] = {\n\n    val deltaConfig = DeltaConfig(s\"delta.$key\",\n      defaultValue,\n      fromString,\n      validationFunction,\n      helpMessage,\n      userConfigurable,\n      alternateConfs.map(_.key))\n\n    entries.put(key.toLowerCase(Locale.ROOT), deltaConfig)\n    deltaConfig\n  }\n\n  /**\n   * Validates specified configurations and returns the normalized key -> value map.\n   */\n  def validateConfigurations(configurations: Map[String, String]): Map[String, String] = {\n    val allowArbitraryProperties = SparkSession.active.sessionState.conf\n      .getConf(DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES)\n\n    configurations.map { case kv @ (key, value) =>\n      key.toLowerCase(Locale.ROOT) match {\n        case lKey if lKey.startsWith(\"delta.constraints.\") =>\n          // This is a CHECK constraint, we should allow it.\n          kv\n        case lKey if lKey.startsWith(TableFeatureProtocolUtils.FEATURE_PROP_PREFIX) =>\n          // This is a table feature, we should allow it.\n          lKey -> value\n        case lKey if lKey.startsWith(\"delta.\") =>\n          Option(entries.get(lKey.stripPrefix(\"delta.\"))) match {\n            case Some(deltaConfig) if (\n              lKey == DeltaConfigs.TOMBSTONE_RETENTION.key.toLowerCase(Locale.ROOT) ||\n              lKey == DeltaConfigs.LOG_RETENTION.key.toLowerCase(Locale.ROOT)) =>\n              val ret = deltaConfig(value) // validate the value\n              validateTombstoneAndLogRetentionDurationCompatibility(configurations)\n              ret\n            case Some(deltaConfig) =>\n              deltaConfig(value) // validate the value\n            case None if lKey.startsWith(DELTA_UNIVERSAL_FORMAT_CONFIG_PREFIX) =>\n              // always allow any delta universal format config with key converted to lower case\n              lKey -> value\n            case None if allowArbitraryProperties =>\n              logConsole(\n                s\"You are setting a property: $key that is not recognized by this \" +\n                  \"version of Delta\")\n              kv\n            case None => throw DeltaErrors.unknownConfigurationKeyException(key)\n          }\n        case _ =>\n          if (entries.containsKey(key)) {\n            logConsole(s\"\"\"\n              |You are trying to set a property the key of which is the same as Delta config: $key.\n              |If you are trying to set a Delta config, prefix it with \"delta.\", e.g. 'delta.$key'.\n            \"\"\".stripMargin)\n          }\n          kv\n      }\n    }\n  }\n\n  /**\n   * Table properties for new tables can be specified through SQL Configurations using the\n   * [[sqlConfPrefix]] and [[TableFeatureProtocolUtils.DEFAULT_FEATURE_PROP_PREFIX]]. This method\n   * checks to see if any of the configurations exist among the SQL configurations and merges them\n   * with the user provided configurations. User provided configs take precedence.\n   *\n   * When `ignoreProtocolConfsOpt` is `true` (or `false`), this method will not (or will) copy\n   * protocol-related configs. If `ignoreProtocolConfsOpt` is None, whether to copy\n   * protocol-related configs will be depending on the existence of\n   * [[DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS]] (`delta.ignoreProtocolDefaults`) in\n   * SQL or table configs.\n   *\n   * <i>\"Protocol-related configs\" includes `delta.minReaderVersion`, `delta.minWriterVersion`,\n   * `delta.ignoreProtocolDefaults`, and anything that starts with `delta.feature.`</i>\n   */\n  def mergeGlobalConfigs(\n      sqlConfs: SQLConf,\n      tableConf: Map[String, String],\n      ignoreProtocolConfsOpt: Option[Boolean] = None): Map[String, String] = {\n\n    val ignoreProtocolConfs =\n      ignoreProtocolConfsOpt.getOrElse(ignoreProtocolDefaultsIsSet(sqlConfs, tableConf))\n\n    val shouldCopyFunc: (String => Boolean) =\n      !ignoreProtocolConfs || !TableFeatureProtocolUtils.isTableProtocolProperty(_)\n\n    val globalConfs = entries.asScala\n      .filter { case (_, config) => shouldCopyFunc(config.key) }\n      .flatMap { case (_, config) =>\n        val sqlConfKey = sqlConfPrefix + config.key.stripPrefix(\"delta.\")\n        Option(sqlConfs.getConfString(sqlConfKey, null)).map(config(_))\n      }\n\n    // Table features configured in session must be merged manually because there's no\n    // ConfigEntry registered for table features in SQL configs or Table props.\n    val globalFeatureConfs = if (ignoreProtocolConfs) {\n      Map.empty[String, String]\n    } else {\n      sqlConfs.getAllConfs\n        .filterKeys(_.startsWith(TableFeatureProtocolUtils.DEFAULT_FEATURE_PROP_PREFIX))\n        .map { case (key, value) =>\n          val featureName = key.stripPrefix(TableFeatureProtocolUtils.DEFAULT_FEATURE_PROP_PREFIX)\n          val tableKey = TableFeatureProtocolUtils.FEATURE_PROP_PREFIX + featureName\n          tableKey -> value\n        }\n    }\n\n    globalConfs.toMap ++ globalFeatureConfs.toMap ++ tableConf\n  }\n\n  /**\n   * Whether [[DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS]] is set in Spark session\n   * configs or table properties.\n   */\n  private[delta] def ignoreProtocolDefaultsIsSet(\n      sqlConfs: SQLConf,\n      tableConf: Map[String, String]): Boolean = {\n    tableConf\n      .getOrElse(\n        DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key,\n        sqlConfs.getConfString(\n          DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.defaultTablePropertyKey,\n          \"false\"))\n      .toBoolean\n  }\n\n  /**\n   * Normalize the specified property keys if the key is for a Delta config.\n   */\n  def normalizeConfigKeys(propKeys: Seq[String]): Seq[String] = {\n    propKeys.map { key =>\n      key.toLowerCase(Locale.ROOT) match {\n        case lKey if lKey.startsWith(TableFeatureProtocolUtils.FEATURE_PROP_PREFIX) =>\n          lKey\n        case lKey if lKey.startsWith(\"delta.\") =>\n          Option(entries.get(lKey.stripPrefix(\"delta.\"))).map(_.key).getOrElse(key)\n        case _ => key\n      }\n    }\n  }\n\n  /**\n   * Normalize the specified property key if the key is for a Delta config.\n   */\n  def normalizeConfigKey(propKey: Option[String]): Option[String] = {\n    propKey.map { key =>\n      key.toLowerCase(Locale.ROOT) match {\n        case lKey if lKey.startsWith(TableFeatureProtocolUtils.FEATURE_PROP_PREFIX) =>\n          lKey\n        case lKey if lKey.startsWith(\"delta.\") =>\n          Option(entries.get(lKey.stripPrefix(\"delta.\"))).map(_.key).getOrElse(key)\n        case _ => key\n      }\n    }\n  }\n\n  def getMilliSeconds(i: CalendarInterval): Long = {\n    getMicroSeconds(i) / 1000L\n  }\n\n  private def getMicroSeconds(i: CalendarInterval): Long = {\n    assert(i.months == 0)\n    i.days * DateTimeConstants.MICROS_PER_DAY + i.microseconds\n  }\n\n  private def validateTombstoneAndLogRetentionDurationCompatibility(\n    configs: Map[String, String]): Unit = {\n    if (!SparkSession.active.sessionState.conf\n      .getConf(DeltaSQLConf.ENFORCE_DELETED_FILE_AND_LOG_RETENTION_DURATION_COMPATIBILITY)) {\n      return\n    }\n    val lowerCaseConfigs = configs.iterator.map {\n      case (k, v) => k.toLowerCase(Locale.ROOT) -> v\n    }.toMap\n    val logRetention = DeltaConfigs.LOG_RETENTION\n    val tombstoneRetention = DeltaConfigs.TOMBSTONE_RETENTION\n    val logRetentionDuration: CalendarInterval = logRetention.fromString(\n      lowerCaseConfigs.get(logRetention.key.toLowerCase(Locale.ROOT))\n        .getOrElse(logRetention.defaultValue))\n    val tombstoneRetentionDuration: CalendarInterval = tombstoneRetention.fromString(\n      lowerCaseConfigs.get(tombstoneRetention.key.toLowerCase(Locale.ROOT))\n        .getOrElse(tombstoneRetention.defaultValue))\n\n    val logRetentionFound = lowerCaseConfigs.get(\n      logRetention.key.toLowerCase(Locale.ROOT)).isDefined\n\n    val errorMessage = if (logRetentionFound) {\n      s\"The table property ${DeltaConfigs.LOG_RETENTION.key}(${logRetentionDuration.toString}) \" +\n        s\"needs to be greater than or equal to ${DeltaConfigs.TOMBSTONE_RETENTION.key}\" +\n        s\"(${tombstoneRetentionDuration.toString}).\"\n    } else {\n      s\"The table property ${DeltaConfigs.TOMBSTONE_RETENTION.key}\" +\n        s\"(${tombstoneRetentionDuration.toString}) needs to be less than or equal to \" +\n        s\"${DeltaConfigs.LOG_RETENTION.key}(${logRetentionDuration.toString}).\"\n    }\n\n    require(getMilliSeconds(logRetentionDuration) >= getMilliSeconds(tombstoneRetentionDuration),\n      errorMessage)\n  }\n\n  /**\n   * For configs accepting an interval, we require the user specified string must obey:\n   *\n   * - Doesn't use months or years, since an internal like this is not deterministic.\n   * - The microseconds parsed from the string value must be a non-negative value.\n   *\n   * The method returns whether a [[CalendarInterval]] satisfies the requirements.\n   */\n  def isValidIntervalConfigValue(i: CalendarInterval): Boolean = {\n    i.months == 0 && getMicroSeconds(i) >= 0\n  }\n\n  /**\n   * Return all Delta configurations, including both set and unset ones.\n   */\n  def getAllConfigs: Map[String, DeltaConfig[_]] = {\n    entries.asScala.toMap\n  }\n\n  /**\n   * The protocol reader version modelled as a table property. This property is *not* stored as\n   * a table property in the `Metadata` action. It is stored as its own action. Having it modelled\n   * as a table property makes it easier to upgrade, and view the version.\n   */\n  val MIN_READER_VERSION = buildConfig[Int](\n    \"minReaderVersion\",\n    Action.supportedProtocolVersion().minReaderVersion.toString,\n    _.toInt,\n    v => Action.supportedReaderVersionNumbers.contains(v),\n    s\"needs to be one of ${Action.supportedReaderVersionNumbers.toSeq.sorted.mkString(\", \")}.\")\n\n  /**\n   * The protocol reader version modelled as a table property. This property is *not* stored as\n   * a table property in the `Metadata` action. It is stored as its own action. Having it modelled\n   * as a table property makes it easier to upgrade, and view the version.\n   */\n  val MIN_WRITER_VERSION = buildConfig[Int](\n    \"minWriterVersion\",\n    Action.supportedProtocolVersion().minWriterVersion.toString,\n    _.toInt,\n    v => Action.supportedWriterVersionNumbers.contains(v),\n    s\"needs to be one of ${Action.supportedWriterVersionNumbers.toSeq.sorted.mkString(\", \")}.\")\n\n  /**\n   * Ignore protocol-related configs set in SQL config.\n   * When set to true, CREATE TABLE and REPLACE TABLE commands will not consider default\n   * protocol versions and table features in the current Spark session.\n   */\n  val CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS = buildConfig[Boolean](\n    \"ignoreProtocolDefaults\",\n    defaultValue = \"false\",\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n\n  /**\n   * The shortest duration we have to keep delta files around before deleting them. We can only\n   * delete delta files that are before a compaction. We may keep files beyond this duration until\n   * the next calendar day.\n   */\n  val LOG_RETENTION = buildConfig[CalendarInterval](\n    \"logRetentionDuration\",\n    \"interval 30 days\",\n    parseCalendarInterval,\n    isValidIntervalConfigValue,\n    \"needs to be provided as a calendar interval such as '2 weeks'. Months \" +\n    \"and years are not accepted. You may specify '365 days' for a year instead.\")\n\n  /**\n   * The shortest duration we have to keep delta sample files around before deleting them.\n   */\n  val SAMPLE_RETENTION = buildConfig[CalendarInterval](\n    \"sampleRetentionDuration\",\n    \"interval 7 days\",\n    parseCalendarInterval,\n    isValidIntervalConfigValue,\n    \"needs to be provided as a calendar interval such as '2 weeks'. Months \" +\n      \"and years are not accepted. You may specify '365 days' for a year instead.\")\n\n  /**\n   * The shortest duration we have to keep checkpoint files around before deleting them. Note that\n   * we'll never delete the most recent checkpoint. We may keep checkpoint files beyond this\n   * duration until the next calendar day.\n   */\n  val CHECKPOINT_RETENTION_DURATION = buildConfig[CalendarInterval](\n    \"checkpointRetentionDuration\",\n    \"interval 2 days\",\n    parseCalendarInterval,\n    isValidIntervalConfigValue,\n    \"needs to be provided as a calendar interval such as '2 weeks'. Months \" +\n      \"and years are not accepted. You may specify '365 days' for a year instead.\")\n\n  /** How often to checkpoint the delta log. */\n  val CHECKPOINT_INTERVAL = buildConfig[Int](\n    \"checkpointInterval\",\n    \"10\",\n    _.toInt,\n    _ > 0,\n    \"needs to be a positive integer.\")\n\n  /**\n   * This is the property that describes the table redirection detail. It is a JSON string format\n   * of the `TableRedirectConfiguration` class, which includes following attributes:\n   * - type(String): The type of redirection.\n   * - state(String): The current state of the redirection:\n   *                  ENABLE-REDIRECT-IN-PROGRESS, REDIRECT-READY, DROP-REDIRECT-IN-PROGRESS.\n   * - spec(JSON String): The specification of accessing redirect destination table. This is free\n   *                      form json object. Each delta service provider can customize its own\n   *                      implementation.\n   */\n  val REDIRECT_READER_WRITER: DeltaConfig[Option[String]] =\n    buildConfig[Option[String]](\n      \"redirectReaderWriter-preview\",\n      null,\n      v => Option(v),\n      _ => true,\n      \"A JSON representation of the TableRedirectConfiguration class, which contains all \" +\n        \"information of redirect reader writer feature.\")\n\n  /**\n   * This table feature is same as REDIRECT_READER_WRITER except it is a writer only table feature.\n   */\n  val REDIRECT_WRITER_ONLY: DeltaConfig[Option[String]] =\n    buildConfig[Option[String]](\n      \"redirectWriterOnly-preview\",\n      null,\n      v => Option(v),\n      _ => true,\n      \"A JSON representation of the TableRedirectConfiguration class, which contains all \" +\n        \"information of redirect writer only feature.\")\n\n  /**\n   * Enable auto compaction for a Delta table. When enabled, we will check if files already\n   * written to a Delta table can leverage compaction after a commit. If so, we run a post-commit\n   * hook to compact the files.\n   *  It can be enabled by setting the property to `true`\n   * Note that the behavior from table property can be overridden by the config:\n   * [[org.apache.spark.sql.delta.sources.DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED]]\n   */\n  val AUTO_COMPACT = buildConfig[Option[String]](\n    \"autoOptimize.autoCompact\",\n    null,\n    v => Option(v).map(_.toLowerCase(Locale.ROOT)),\n    v => v.isEmpty || AutoCompactType.ALLOWED_VALUES.contains(v.get),\n      s\"\"\"\"needs to be one of: ${AutoCompactType.ALLOWED_VALUES.mkString(\",\")}\"\"\")\n\n  /** Whether to clean up expired checkpoints and delta logs. */\n  val ENABLE_EXPIRED_LOG_CLEANUP = buildConfig[Boolean](\n    \"enableExpiredLogCleanup\",\n    \"true\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * If true, a delta table can be rolled back to any point within LOG_RETENTION. Leaving this on\n   * requires converting the oldest delta file we have into a checkpoint, which we do once a day. If\n   * doing that operation is too expensive, it can be turned off, but the table can only be rolled\n   * back CHECKPOINT_RETENTION_DURATION ago instead of LOG_RETENTION ago.\n   */\n  val ENABLE_FULL_RETENTION_ROLLBACK = buildConfig[Boolean](\n    \"enableFullRetentionRollback\",\n    \"true\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\"\n  )\n\n  /**\n   * The logRetention period to be used in DROP FEATURE ... TRUNCATE HISTORY command.\n   * The value should represent the expected duration of the longest running transaction. Setting\n   * this to a lower value than the longest running transaction may corrupt the table.\n   */\n  val TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION = buildConfig[CalendarInterval](\n    \"dropFeatureTruncateHistory.retentionDuration\",\n    \"interval 24 hours\",\n    parseCalendarInterval,\n    isValidIntervalConfigValue,\n    \"needs to be provided as a calendar interval such as '2 weeks'. Months \" +\n    \"and years are not accepted. You may specify '365 days' for a year instead.\")\n\n  /**\n   * The shortest duration we have to keep logically deleted data files around before deleting them\n   * physically. This is to prevent failures in stale readers after compactions or partition\n   * overwrites.\n   *\n   * Note: this value should be large enough:\n   * - It should be larger than the longest possible duration of a job if you decide to run \"VACUUM\"\n   *   when there are concurrent readers or writers accessing the table.\n   * - If you are running a streaming query reading from the table, you should make sure the query\n   *   doesn't stop longer than this value. Otherwise, the query may not be able to restart as it\n   *   still needs to read old files.\n   */\n  val TOMBSTONE_RETENTION = buildConfig[CalendarInterval](\n    \"deletedFileRetentionDuration\",\n    \"interval 1 week\",\n    parseCalendarInterval,\n    isValidIntervalConfigValue,\n    \"needs to be provided as a calendar interval such as '2 weeks'. Months \" +\n    \"and years are not accepted. You may specify '365 days' for a year instead.\")\n\n  /**\n   * Whether to use a random prefix in a file path instead of partition information. This is\n   * required for very high volume S3 calls to better be partitioned across S3 servers.\n   */\n  val RANDOMIZE_FILE_PREFIXES = buildConfig[Boolean](\n    \"randomizeFilePrefixes\",\n    \"false\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * Whether to use a random prefix in a file path instead of partition information. This is\n   * required for very high volume S3 calls to better be partitioned across S3 servers.\n   */\n  val RANDOM_PREFIX_LENGTH = buildConfig[Int](\n    \"randomPrefixLength\",\n    \"2\",\n    _.toInt,\n    a => a > 0,\n    \"needs to be greater than 0.\")\n\n  /**\n   * Whether this Delta table is append-only. Files can't be deleted, or values can't be updated.\n   */\n  val IS_APPEND_ONLY = buildConfig[Boolean](\n    key = \"appendOnly\",\n    defaultValue = \"false\",\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n  /**\n   * Whether commands modifying this Delta table are allowed to create new deletion vectors.\n   */\n  val ENABLE_DELETION_VECTORS_CREATION = buildConfig[Boolean](\n    key = \"enableDeletionVectors\",\n    defaultValue = \"false\",\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n  val ENABLE_VARIANT_SHREDDING = buildConfig[Boolean](\n    key = \"enableVariantShredding\",\n    defaultValue = \"false\",\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n  /**\n   * Whether this table will automatically optimize the layout of files during writes.\n   */\n  val AUTO_OPTIMIZE = buildConfig[Option[Boolean]](\n    \"autoOptimize\",\n    null,\n    v => Option(v).map(_.toBoolean),\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * The number of columns to collect stats on for data skipping. A value of -1 means collecting\n   * stats for all columns. Updating this conf does not trigger stats re-collection, but redefines\n   * the stats schema of table, i.e., it will change the behavior of future stats collection\n   * (e.g., in append and OPTIMIZE) as well as data skipping (e.g., the column stats beyond this\n   * number will be ignored even when they exist).\n   */\n  val DATA_SKIPPING_NUM_INDEXED_COLS = buildConfig[Int](\n    \"dataSkippingNumIndexedCols\",\n    DataSkippingReaderConf.DATA_SKIPPING_NUM_INDEXED_COLS_DEFAULT_VALUE.toString,\n    _.toInt,\n    a => a >= -1,\n    \"needs to be larger than or equal to -1.\")\n\n  /**\n   * The names of specific columns to collect stats on for data skipping. If present, it takes\n   * precedences over dataSkippingNumIndexedCols config, and the system will only collect stats for\n   * columns that exactly match those specified. If a nested column is specified, the system will\n   * collect stats for all leaf fields of that column. If a non-existent column is specified, it\n   * will be ignored. Updating this conf does not trigger stats re-collection, but redefines the\n   * stats schema of table, i.e., it will change the behavior of future stats collection (e.g., in\n   * append and OPTIMIZE) as well as data skipping (e.g., the column stats not mentioned by this\n   * config will be ignored even if they exist).\n   */\n  val DATA_SKIPPING_STATS_COLUMNS = buildConfig[Option[String]](\n    \"dataSkippingStatsColumns\",\n    null,\n    v => Option(v),\n    vOpt => vOpt.forall(v => DeltaSqlParserUtils.parseMultipartColumnList(v).isDefined),\n    \"\"\"\n      |The dataSkippingStatsColumns parameter is a comma-separated list of case-insensitive column\n      |identifiers. Each column identifier can consist of letters, digits, and underscores.\n      |Multiple column identifiers can be listed, separated by commas.\n      |\n      |If a column identifier includes special characters such as !@#$%^&*()_+-={}|[]:\";'<>,.?/,\n      |the column name should be enclosed in backticks (`) to escape the special characters.\n      |\n      |A column identifier can refer to one of the following: the name of a non-struct column, the\n      |leaf field's name of a struct column, or the name of a struct column. When a struct column's\n      |name is specified in dataSkippingStatsColumns, statistics for all its leaf fields will be\n      |collected.\n      |\"\"\".stripMargin)\n\n  /**\n   * For string columns, how long prefix to store in the data skipping index.\n   * Note that the behavior from table property overrides the config:\n   * [[DeltaSQLConf.DATA_SKIPPING_STRING_PREFIX_LENGTH]]\n   */\n  val DATA_SKIPPING_STRING_PREFIX_LENGTH = buildConfig[Option[Int]](\n    \"dataSkippingStringPrefixLength\",\n    null,\n    v => Option(v).map(_.toInt),\n    v => v.forall(_ >= 0),\n    \"needs to be greater or equal to zero.\")\n\n  val SYMLINK_FORMAT_MANIFEST_ENABLED = buildConfig[Boolean](\n    s\"${hooks.GenerateSymlinkManifest.CONFIG_NAME_ROOT}.enabled\",\n    \"false\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * When enabled, we will write file statistics in the checkpoint in JSON format as the \"stats\"\n   * column.\n   */\n  val CHECKPOINT_WRITE_STATS_AS_JSON = buildConfig[Boolean](\n    \"checkpoint.writeStatsAsJson\",\n    \"true\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * When enabled, we will write file statistics in the checkpoint in the struct format in the\n   * \"stats_parsed\" column. We will also write partition values as a struct as\n   * \"partitionValues_parsed\".\n   */\n  val CHECKPOINT_WRITE_STATS_AS_STRUCT = buildConfig[Boolean](\n    \"checkpoint.writeStatsAsStruct\",\n    \"true\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * Deprecated in favor of CHANGE_DATA_FEED.\n   */\n  private val CHANGE_DATA_FEED_LEGACY = buildConfig[Boolean](\n    \"enableChangeDataCapture\",\n    \"false\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\")\n\n  /**\n   * Enable change data feed output.\n   * When enabled, DELETE, UPDATE, and MERGE INTO operations will need to do additional work to\n   * output their change data in an efficiently readable format.\n   */\n  val CHANGE_DATA_FEED = buildConfig[Boolean](\n    \"enableChangeDataFeed\",\n    \"false\",\n    _.toBoolean,\n    _ => true,\n    \"needs to be a boolean.\",\n    alternateConfs = Seq(CHANGE_DATA_FEED_LEGACY))\n\n  val COLUMN_MAPPING_MODE = buildConfig[DeltaColumnMappingMode](\n    \"columnMapping.mode\",\n    \"none\",\n    DeltaColumnMappingMode(_),\n    _ => true,\n    \"\")\n\n  /**\n   * Maximum columnId used in the schema so far for column mapping. Internal property that cannot\n   * be set by users.\n   */\n  val COLUMN_MAPPING_MAX_ID = buildConfig[Long](\n    \"columnMapping.maxColumnId\",\n    \"0\",\n    _.toLong,\n    _ => true,\n    \"\",\n    userConfigurable = false)\n\n\n  /**\n   * The shortest duration within which new [[Snapshot]]s will retain transaction identifiers (i.e.\n   * [[SetTransaction]]s). When a new [[Snapshot]] sees a transaction identifier older than or equal\n   * to the specified TRANSACTION_ID_RETENTION_DURATION, it considers it expired and ignores it.\n   */\n  val TRANSACTION_ID_RETENTION_DURATION = buildConfig[Option[CalendarInterval]](\n    \"setTransactionRetentionDuration\",\n    null,\n    v => if (v == null) None else Some(parseCalendarInterval(v)),\n    opt => opt.forall(isValidIntervalConfigValue),\n    \"needs to be provided as a calendar interval such as '2 weeks'. Months \" +\n      \"and years are not accepted. You may specify '365 days' for a year instead.\")\n\n  /**\n   * The isolation level of a table defines the degree to which a transaction must be isolated from\n   * modifications made by concurrent transactions. Delta currently supports one isolation level:\n   * Serializable.\n   */\n  val ISOLATION_LEVEL = buildConfig[IsolationLevel](\n    \"isolationLevel\",\n    Serializable.toString,\n    IsolationLevel.fromString(_),\n    _ == Serializable,\n    \"must be Serializable\"\n  )\n\n  /** Policy to decide what kind of checkpoint to write to a table. */\n  val CHECKPOINT_POLICY = buildConfig[CheckpointPolicy.Policy](\n    key = \"checkpointPolicy\",\n    defaultValue = CheckpointPolicy.Classic.name,\n    fromString = str => CheckpointPolicy.fromName(str),\n    validationFunction = (v => CheckpointPolicy.ALL.exists(_.name == v.name)),\n    helpMessage = s\"can be one of the \" +\n      s\"following: ${CheckpointPolicy.Classic.name}, ${CheckpointPolicy.V2.name}\")\n\n  /**\n   * Indicates whether Row Tracking is enabled on the table. When this flag is turned on, all rows\n   * are guaranteed to have Row IDs and Row Commit Versions assigned to them, and writers are\n   * expected to preserve them by materializing them to hidden columns in the data files.\n   */\n  val ROW_TRACKING_ENABLED = buildConfig[Boolean](\n    key = \"enableRowTracking\",\n    defaultValue = false.toString,\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n  /**\n   * Controls whether row tracking operations should be suspended. It blocks the assignment of new\n   * baseRowIds as well as copying existing baseRowIds. It is intended to be used when dropping\n   * row tracking. It can be enabled after setting `delta.enableRowTracking` to false.\n   *\n   * WARNING 1: Should never be enabled when `delta.enableRowTracking` is set to true.\n   * WARNING 2: It should never be manually set. It is only safe to be used in the context of\n   *            DROP FEATURE.\n   */\n  val ROW_TRACKING_SUSPENDED = buildConfig[Boolean](\n    key = \"rowTrackingSuspended\",\n    defaultValue = false.toString,\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n  /**\n   * Convert the table's metadata into other storage formats after each Delta commit.\n   * Only Iceberg is supported for now\n   */\n  val UNIVERSAL_FORMAT_ENABLED_FORMATS = buildConfig[Seq[String]](\n    \"universalFormat.enabledFormats\",\n    \"\",\n    fromString = str =>\n      if (str == null || str.isEmpty) Nil\n      else str.split(\",\"),\n    validationFunction = seq =>\n      if (seq.distinct.length != seq.length) false\n      else seq.toSet.subsetOf(UniversalFormat.SUPPORTED_FORMATS),\n    s\"Must be a comma-separated list of formats from the list: \" +\n    s\"${UniversalFormat.SUPPORTED_FORMATS.mkString(\"{\", \",\", \"}\")}.\"\n  )\n\n  val ICEBERG_COMPAT_V1_ENABLED = buildConfig[Option[Boolean]](\n    \"enableIcebergCompatV1\",\n    null,\n    v => Option(v).map(_.toBoolean),\n    _ => true,\n    \"needs to be a boolean.\"\n  )\n\n  val ICEBERG_COMPAT_V2_ENABLED = buildConfig[Option[Boolean]](\n    key = \"enableIcebergCompatV2\",\n    defaultValue = null,\n    fromString = v => Option(v).map(_.toBoolean),\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\"\n  )\n\n  val CAST_ICEBERG_TIME_TYPE = buildConfig[Boolean](\n    key = \"castIcebergTimeType\",\n    defaultValue = \"false\",\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"Casting Iceberg TIME type to Spark Long type enabled\"\n  )\n\n  val IGNORE_ICEBERG_BUCKET_PARTITION = buildConfig[Boolean](\n    key = \"ignoreIcebergBucketPartition\",\n    defaultValue = \"false\",\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"Ignore Iceberg bucket partition, which means \" +\n      \"converting source iceberg table to a non-partition delta table\"\n  )\n  /**\n   * Enable optimized writes into a Delta table. Optimized writes adds an adaptive shuffle before\n   * the write to write compacted files into a Delta table during a write.\n   */\n  val OPTIMIZE_WRITE = buildConfig[Option[Boolean]](\n    \"autoOptimize.optimizeWrite\",\n    null,\n    v => Option(v).map(_.toBoolean),\n    _ => true,\n    \"needs to be a boolean.\"\n  )\n\n  /**\n   * Whether widening the type of an existing column or field is allowed, either manually using\n   * ALTER TABLE CHANGE COLUMN or automatically if automatic schema evolution is enabled.\n   */\n  val ENABLE_TYPE_WIDENING = buildConfig[Boolean](\n    key = \"enableTypeWidening\",\n    defaultValue = false.toString,\n    fromString = _.toBoolean,\n    validationFunction = _ => true,\n    helpMessage = \"needs to be a boolean.\")\n\n  val COORDINATED_COMMITS_COORDINATOR_NAME = buildConfig[Option[String]](\n    \"coordinatedCommits.commitCoordinator-preview\",\n    null,\n    v => Option(v),\n    _ => true,\n    \"\"\"The commit-coordinator name for this table. This is used to determine which\n      |implementation of commit-coordinator to use when committing to this table. If this property\n      |is not set, the table will be considered as file system table and commits will be done via\n      |atomically publishing the commit file.\n      |\"\"\".stripMargin)\n\n  val COORDINATED_COMMITS_COORDINATOR_CONF = buildConfig[Map[String, String]](\n    \"coordinatedCommits.commitCoordinatorConf-preview\",\n    null,\n    v => JsonUtils.fromJson[Map[String, String]](Option(v).getOrElse(\"{}\")),\n    _ => true,\n    \"A string-to-string map of configuration properties for the coordinated commits-coordinator.\")\n\n  val COORDINATED_COMMITS_TABLE_CONF = buildConfig[Map[String, String]](\n    \"coordinatedCommits.tableConf-preview\",\n    null,\n    v => JsonUtils.fromJson[Map[String, String]](Option(v).getOrElse(\"{}\")),\n    _ => true,\n    \"A string-to-string map of configuration properties for describing the table to\" +\n      \" commit-coordinator.\")\n\n  val IN_COMMIT_TIMESTAMPS_ENABLED = buildConfig[Boolean](\n    \"enableInCommitTimestamps\",\n    false.toString,\n    _.toBoolean,\n    validationFunction = _ => true,\n    \"needs to be a boolean.\"\n  )\n\n  /**\n   * This table property is used to track the version of the table at which\n   * inCommitTimestamps were enabled.\n   */\n  val IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION = buildConfig[Option[Long]](\n    \"inCommitTimestampEnablementVersion\",\n    null,\n    v => Option(v).map(_.toLong),\n    validationFunction = _ => true,\n    \"needs to be a long.\"\n  )\n\n  /**\n   * This table property is used to track the timestamp at which inCommitTimestamps\n   * were enabled. More specifically, it is the inCommitTimestamp of the commit with\n   * the version specified in [[IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION]].\n   */\n  val IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP = buildConfig[Option[Long]](\n    \"inCommitTimestampEnablementTimestamp\",\n    null,\n    v => Option(v).map(_.toLong),\n    validationFunction = _ => true,\n    \"needs to be a long.\")\n\n  /**\n   * This property is used by CheckpointProtectionTableFeature and denotes the\n   * version up to which the checkpoints are required to be cleaned up only together with the\n   * corresponding commits. If this is not possible, and metadata cleanup creates a new checkpoint\n   * prior to requireCheckpointProtectionBeforeVersion, it should validate write support against\n   * all protocols included in the commits that are being removed, or else abort. This is needed\n   * to make sure that the writer understands how to correctly create a checkpoint for the\n   * historic commit.\n   *\n   * Note, this is an internal config and should never be manually altered.\n   */\n  val REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION = buildConfig[Long](\n    \"requireCheckpointProtectionBeforeVersion\",\n    \"0\",\n    _.toLong,\n    _ >= 0,\n    \"needs to be greater or equal to zero.\")\n\n  /**\n   * If true, enables the MaterializePartitionColumns table feature which requires partition\n   * columns to be materialized for future parquet data files.\n   */\n  val ENABLE_MATERIALIZE_PARTITION_COLUMNS_FEATURE = buildConfig[Option[Boolean]](\n    \"enableMaterializePartitionColumnsFeature\",\n    null,\n    v => Option(v).map(_.toBoolean),\n    _ => true,\n    \"needs to be a boolean.\")\n}\n\nobject DeltaConfigs extends DeltaConfigsBase\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaErrors.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.{FileNotFoundException, IOException}\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.{ConcurrentModificationException, UUID}\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.skipping.clustering.temp.{ClusterBySpec}\nimport org.apache.spark.sql.delta.actions.{CommitInfo, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.commands.{AlterTableDropFeatureDeltaCommand, DeltaGenerateCommand}\nimport org.apache.spark.sql.delta.constraints.Constraints\nimport org.apache.spark.sql.delta.hooks.AutoCompactType\nimport org.apache.spark.sql.delta.hooks.PostCommitHook\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.redirect.NoRedirectRule\nimport org.apache.spark.sql.delta.redirect.RedirectSpec\nimport org.apache.spark.sql.delta.redirect.RedirectState\nimport org.apache.spark.sql.delta.schema.{DeltaInvariantViolationException, InvariantViolationException, SchemaUtils, UnsupportedDataTypeInfo}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport io.delta.exceptions\nimport org.apache.hadoop.fs.{ChecksumException, Path}\n\nimport org.apache.spark.{SparkConf, SparkEnv, SparkException}\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.errors.QueryErrorsBase\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{DataType, StructField, StructType}\n\n\ntrait DocsPath {\n  /**\n   * The URL for the base path of Delta's docs. When changing this path, ensure that the new path\n   * works with the error messages below.\n   */\n  protected def baseDocsPath(conf: SparkConf): String = \"https://docs.delta.io/latest\"\n\n  def assertValidCallingFunction(): Unit = {\n    val callingMethods = Thread.currentThread.getStackTrace\n    callingMethods.foreach { method =>\n      if (errorsWithDocsLinks.contains(method.getMethodName)) {\n        return\n      }\n    }\n    assert(assertion = false, \"The method throwing the error which contains a doc link must be a \" +\n      s\"part of DocsPath.errorsWithDocsLinks\")\n  }\n\n  /**\n   * Get the link to the docs for the given relativePath. Validates that the error generating the\n   * link is added to docsLinks.\n   * Please only use this function if SparkConf is directly available.\n   * If needed to retrieve SparkConf from SparkSession,\n   * please use more safe function [[generateDocsLinkOption]].\n   *\n   * @param relativePath the relative path after the base url to access.\n   * @param skipValidation whether to validate that the function generating the link is\n   *                       in the allowlist.\n   * @return The entire URL of the documentation link\n   */\n  def generateDocsLink(\n      conf: SparkConf,\n      relativePath: String,\n      skipValidation: Boolean = false): String = {\n    require(conf != null)\n    if (!skipValidation) assertValidCallingFunction()\n    baseDocsPath(conf) + relativePath\n  }\n\n  /** Safe alternative to [[generateDocsLink]] that validates sparkContext before accessing it. */\n  def generateDocsLinkOption(\n      spark: SparkSession,\n      relativePath: String,\n      skipValidation: Boolean = false): Option[String] =\n    Option(spark.sparkContext)\n      .map(context => generateDocsLink(context.getConf, relativePath, skipValidation))\n\n  /**\n   * List of error function names for all errors that have URLs. When adding your error to this list\n   * remember to also add it to the list of errors in DeltaErrorsSuite\n   *\n   * @note add your error to DeltaErrorsSuiteBase after adding it to this list so that the url can\n   *       be tested\n   */\n  def errorsWithDocsLinks: Seq[String] = Seq(\n    \"createExternalTableWithoutLogException\",\n    \"createExternalTableWithoutSchemaException\",\n    \"createManagedTableWithoutSchemaException\",\n    \"multipleSourceRowMatchingTargetRowInMergeException\",\n    \"ignoreStreamingUpdatesAndDeletesWarning\",\n    \"concurrentAppendException\",\n    \"concurrentDeleteDeleteException\",\n    \"concurrentDeleteReadException\",\n    \"concurrentWriteException\",\n    \"concurrentTransactionException\",\n    \"metadataChangedException\",\n    \"protocolChangedException\",\n    \"concurrentModificationExceptionMsg\",\n    \"incorrectLogStoreImplementationException\",\n    \"sourceNotDeterministicInMergeException\",\n    \"columnMappingAdviceMessage\",\n    \"icebergClassMissing\",\n    \"tableFeatureReadRequiresWriteException\",\n    \"tableFeatureRequiresHigherReaderProtocolVersion\",\n    \"tableFeatureRequiresHigherWriterProtocolVersion\",\n    \"blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges\"\n  )\n}\n\n/**\n * A holder object for Delta errors.\n *\n *\n * IMPORTANT: Any time you add a test that references the docs, add to the Seq defined in\n * DeltaErrorsSuite so that the doc links that are generated can be verified to work in\n * docs.delta.io\n */\ntrait DeltaErrorsBase\n    extends DocsPath\n    with DeltaLogging\n    with QueryErrorsBase {\n\n  def baseDocsPath(spark: SparkSession): String = baseDocsPath(spark.sparkContext.getConf)\n\n  val faqRelativePath: String = \"/delta-intro.html#frequently-asked-questions\"\n\n  val EmptyCheckpointErrorMessage =\n    s\"\"\"\n       |Attempted to write an empty checkpoint without any actions. This checkpoint will not be\n       |useful in recomputing the state of the table. However this might cause other checkpoints to\n       |get deleted based on retention settings.\n     \"\"\".stripMargin\n\n  // scalastyle:off\n  def assertionFailedError(msg: String): Throwable = new AssertionError(msg)\n  // scalastyle:on\n\n  def deltaSourceIgnoreDeleteError(\n      version: Long,\n      removedFile: String,\n      dataPath: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_SOURCE_IGNORE_DELETE\",\n      messageParameters = Array(removedFile, version.toString, dataPath)\n    )\n  }\n\n  def initialSnapshotTooLargeForStreaming(\n      snapshotVersion: Long,\n      numFiles: Long,\n      maxFiles: Int,\n      tablePath: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_STREAMING_INITIAL_SNAPSHOT_TOO_LARGE\",\n      messageParameters = Array(\n        tablePath,\n        snapshotVersion.toString,\n        numFiles.toString,\n        maxFiles.toString,\n        s\"\"\"To fix this issue, choose one of:\n           |\n           |  1. Increase spark.databricks.delta.streaming.initialSnapshotMaxFiles\n           |     (current: $maxFiles)\n           |\n           |  2. Use 'startingVersion' option to skip the initial snapshot and start\n           |     from a specific version\"\"\".stripMargin\n      )\n    )\n  }\n\n  def deltaSourceIgnoreChangesError(\n      version: Long,\n      removedFile: String,\n      dataPath: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_SOURCE_TABLE_IGNORE_CHANGES\",\n      messageParameters = Array(removedFile, version.toString, dataPath)\n    )\n  }\n\n  def unknownReadLimit(limit: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNKNOWN_READ_LIMIT\",\n      messageParameters = Array(limit)\n    )\n  }\n\n  def unknownPrivilege(privilege: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_UNKNOWN_PRIVILEGE\",\n      messageParameters = Array(privilege)\n    )\n  }\n\n  def columnNotFound(path: Seq[String], schema: StructType): Throwable = {\n    val name = UnresolvedAttribute(path).name\n    cannotResolveColumn(name, schema)\n  }\n\n  def failedMergeSchemaFile(file: String, schema: String, cause: Throwable): Throwable = {\n    new DeltaSparkException(\n      errorClass = \"DELTA_FAILED_MERGE_SCHEMA_FILE\",\n      messageParameters = Array(file, schema),\n      cause = cause)\n  }\n\n  def missingCommitInfo(featureName: String, commitVersion: String): DeltaIllegalStateException = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MISSING_COMMIT_INFO\",\n      messageParameters = Array(featureName, commitVersion))\n  }\n\n  def missingCommitTimestamp(commitVersion: String): DeltaIllegalStateException = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MISSING_COMMIT_TIMESTAMP\",\n      messageParameters = Array(InCommitTimestampTableFeature.name, commitVersion))\n  }\n\n  def failOnCheckpointRename(src: Path, dest: Path): DeltaIllegalStateException = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CANNOT_RENAME_PATH\",\n      messageParameters = Array(s\"${src.toString}\", s\"${dest.toString}\"))\n  }\n\n  def checkpointMismatchWithSnapshot : Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CHECKPOINT_SNAPSHOT_MISMATCH\",\n      messageParameters = Array.empty\n    )\n  }\n\n  /**\n   * Thrown when main table data contains columns that are reserved for CDF, such as `_change_type`.\n   */\n  def cdcColumnsInData(columns: Seq[String]): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"RESERVED_CDC_COLUMNS_ON_WRITE\",\n      messageParameters = Array(columns.mkString(\"[\", \",\", \"]\"), DeltaConfigs.CHANGE_DATA_FEED.key)\n    )\n  }\n\n  /**\n   * Thrown when main table data already contains columns that are reserved for CDF, such as\n   * `_change_type`, but CDF is not yet enabled on that table.\n   */\n  def tableAlreadyContainsCDCColumns(columns: Seq[String]): Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_TABLE_ALREADY_CONTAINS_CDC_COLUMNS\",\n      messageParameters = Array(columns.mkString(\"[\", \",\", \"]\")))\n  }\n\n  /**\n   * Thrown when a CDC query contains conflict 'starting' or 'ending' options, e.g. when both\n   * starting version and starting timestamp are specified.\n   *\n   * @param position Specifies which option was duplicated in the read. Values are \"starting\" or\n   *                 \"ending\"\n   */\n  def multipleCDCBoundaryException(position: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MULTIPLE_CDC_BOUNDARY\",\n      messageParameters = Array(position, position, position)\n    )\n  }\n\n  def formatColumn(colName: String): String = s\"`$colName`\"\n\n  def formatColumnList(colNames: Seq[String]): String =\n    colNames.map(formatColumn).mkString(\"[\", \", \", \"]\")\n\n  def formatSchema(schema: StructType): String = schema.treeString\n\n  def notNullColumnMissingException(constraint: Constraints.NotNull): Throwable = {\n    new DeltaInvariantViolationException(\n      errorClass = \"DELTA_MISSING_NOT_NULL_COLUMN_VALUE\",\n      messageParameters = Array(s\"${UnresolvedAttribute(constraint.column).name}\"))\n  }\n\n  def nestedNotNullConstraint(\n      parent: String, nested: DataType, nestType: String): AnalysisException = {\n        new DeltaAnalysisException(\n          errorClass = \"DELTA_NESTED_NOT_NULL_CONSTRAINT\",\n          messageParameters = Array(\n            s\"$nestType\",\n            s\"$parent\",\n            s\"${DeltaSQLConf.ALLOW_UNENFORCED_NOT_NULL_CONSTRAINTS.key}\",\n            s\"$nestType\",\n            s\"${nested.prettyJson}\"\n          )\n        )\n  }\n\n  def nullableParentWithNotNullNestedField : Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NOT_NULL_NESTED_FIELD\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def constraintAlreadyExists(name: String, oldExpr: String): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONSTRAINT_ALREADY_EXISTS\",\n      messageParameters = Array(name, oldExpr)\n    )\n  }\n\n  def invalidConstraintName(name: String): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0001\",\n      messageParameters = Array(name)\n    )\n  }\n\n  def nonexistentConstraint(constraintName: String, tableName: String): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONSTRAINT_DOES_NOT_EXIST\",\n      messageParameters = Array(\n        constraintName,\n        tableName,\n        DeltaSQLConf.DELTA_ASSUMES_DROP_CONSTRAINT_IF_EXISTS.key,\n        \"true\"))\n  }\n\n  def checkConstraintNotBoolean(name: String, expr: String): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_BOOLEAN_CHECK_CONSTRAINT\",\n      messageParameters = Array(name, expr)\n    )\n  }\n\n  def newCheckConstraintViolated(num: Long, tableName: String, expr: String): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NEW_CHECK_CONSTRAINT_VIOLATION\",\n      messageParameters = Array(s\"$num\", tableName, expr)\n    )\n  }\n\n  def newNotNullViolated(\n      num: Long, tableName: String, col: UnresolvedAttribute): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NEW_NOT_NULL_VIOLATION\",\n      messageParameters = Array(s\"$num\", tableName, col.name)\n    )\n  }\n\n  def useAddConstraints: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ADD_CONSTRAINTS\",\n      messageParameters = Array.empty)\n  }\n\n  def cannotDropCheckConstraintFeature(constraintNames: Seq[String]): AnalysisException = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_DROP_CHECK_CONSTRAINT_FEATURE\",\n      messageParameters = Array(constraintNames.map(formatColumn).mkString(\", \"))\n    )\n  }\n\n  def checkConstraintReferToWrongColumns(colName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_CHECK_CONSTRAINT_REFERENCES\",\n      messageParameters = Array(colName)\n    )\n  }\n\n  def checkConstraintUDF(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UDF_IN_CHECK_CONSTRAINT\",\n      messageParameters = Array(expr.sql))\n  }\n\n  def checkConstraintNonDeterministicExpression(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_DETERMINISTIC_EXPRESSION_IN_CHECK_CONSTRAINT\",\n      messageParameters = Array(expr.sql))\n  }\n\n  def checkConstraintAggregateExpression(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_AGGREGATE_IN_CHECK_CONSTRAINT\",\n      messageParameters = Array(expr.sql))\n  }\n\n  def checkConstraintUnsupportedExpression(expr: Expression): Throwable = {\n    val expressionSql = expr.sql\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_EXPRESSION_CHECK_CONSTRAINT\",\n      messageParameters = Array(expressionSql, expressionSql)\n    )\n  }\n\n  def deltaRelationPathMismatch(\n      relationPath: Seq[String],\n      targetType: String,\n      targetPath: Seq[String]\n  ): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_RELATION_PATH_MISMATCH\",\n      messageParameters = Array(\n        relationPath.mkString(\".\"),\n        targetType,\n        targetPath.mkString(\".\")\n      )\n    )\n  }\n\n  def unrecognizedRedirectSpec(spec: RedirectSpec): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_TABLE_UNRECOGNIZED_REDIRECT_SPEC\",\n      messageParameters = Array(spec.toString)\n    )\n  }\n\n  def invalidSetUnSetRedirectCommand(\n      table: String,\n      newProperty: String,\n      existingProperty: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_TABLE_INVALID_SET_UNSET_REDIRECT\",\n      messageParameters = Array(table, existingProperty, newProperty)\n    )\n  }\n\n  def invalidRedirectStateTransition(\n      table: String,\n      oldState: RedirectState,\n      newState: RedirectState): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_TABLE_INVALID_REDIRECT_STATE_TRANSITION\",\n      messageParameters = Array(table, oldState.name, newState.name)\n    )\n  }\n\n  def invalidRemoveTableRedirect(table: String, currentState: RedirectState): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_TABLE_INVALID_REMOVE_TABLE_REDIRECT\",\n      messageParameters = Array(table, table, currentState.name)\n    )\n  }\n\n  def invalidCommitIntermediateRedirectState(state: RedirectState): Throwable = {\n    new DeltaIllegalStateException (\n      errorClass = \"DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE\",\n      messageParameters = Array(state.name)\n    )\n  }\n\n  def noRedirectRulesViolated(\n      op: DeltaOperations.Operation,\n      noRedirectRules: Set[NoRedirectRule]): Throwable = {\n    new DeltaIllegalStateException (\n      errorClass = \"DELTA_NO_REDIRECT_RULES_VIOLATED\",\n      messageParameters =\n        Array(op.name, noRedirectRules.map(\"\\\"\" + _ + \"\\\"\").mkString(\"[\", \",\\n\", \"]\"))\n    )\n  }\n\n  def incorrectLogStoreImplementationException(\n      sparkConf: SparkConf,\n      cause: Throwable): Throwable = {\n    new DeltaIOException(\n      errorClass = \"DELTA_INCORRECT_LOG_STORE_IMPLEMENTATION\",\n      messageParameters = Array(generateDocsLink(sparkConf, \"/delta-storage.html\")),\n      cause = cause)\n  }\n\n  def failOnDataLossException(expectedVersion: Long, seenVersion: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MISSING_FILES_UNEXPECTED_VERSION\",\n      messageParameters = Array(s\"$expectedVersion\", s\"$seenVersion\",\n        s\"${DeltaOptions.FAIL_ON_DATA_LOSS_OPTION}\")\n    )\n  }\n\n  def staticPartitionsNotSupportedException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_STATIC_PARTITIONS\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def zOrderingOnPartitionColumnException(colName: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_ZORDERING_ON_PARTITION_COLUMN\",\n      messageParameters = Array(colName)\n    )\n  }\n\n  def zOrderingOnColumnWithNoStatsException(\n      colNames: Seq[String],\n      spark: SparkSession): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ZORDERING_ON_COLUMN_WITHOUT_STATS\",\n      messageParameters = Array(colNames.mkString(\"[\", \", \", \"]\"),\n        DeltaSQLConf.DELTA_OPTIMIZE_ZORDER_COL_STAT_CHECK.key)\n    )\n  }\n\n  def zOrderingColumnDoesNotExistException(colName: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_ZORDERING_COLUMN_DOES_NOT_EXIST\",\n      messageParameters = Array(colName))\n  }\n\n  /**\n   * Throwable used when CDC options contain no 'start'.\n   */\n  def noStartVersionForCDC(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NO_START_FOR_CDC_READ\",\n      messageParameters = Array.empty\n    )\n  }\n\n  /**\n   * Throwable used when CDC is not enabled according to table metadata.\n   */\n  def changeDataNotRecordedException(version: Long, start: Long, end: Long): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MISSING_CHANGE_DATA\",\n      messageParameters = Array(start.toString, end.toString, version.toString,\n        DeltaConfigs.CHANGE_DATA_FEED.key))\n  }\n\n  def deletedRecordCountsHistogramDeserializationException(): Throwable = {\n    new DeltaChecksumException(\n      errorClass = \"DELTA_DV_HISTOGRAM_DESERIALIZATON\",\n      messageParameters = Array.empty,\n      pos = 0)\n  }\n\n  /** Throwable used when a non-constant expression is used as a version/timestamp arg in CDC. */\n  def cdcNonConstantArgument(\n      fnName: String, paramName: String, position: Int, expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CDC_NON_CONSTANT_ARGUMENT\",\n      messageParameters = Array(s\"`$paramName`\", position.toString, s\"`$fnName`\", expr.sql)\n    )\n  }\n\n  /** Throwable used when a null 'start' or 'end' is provided in CDC reads. */\n  def nullRangeBoundaryInCDCRead(): Throwable = {\n    new DeltaIllegalArgumentException(errorClass = \"DELTA_CDC_READ_NULL_RANGE_BOUNDARY\")\n  }\n\n  /**\n   * Throwable used for invalid CDC 'start' and 'end' options, where end < start\n   */\n  def endBeforeStartVersionInCDC(start: Long, end: Long): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_CDC_RANGE\",\n      messageParameters = Array(start.toString, end.toString)\n    )\n  }\n\n  /**\n   * Throwable used for invalid CDC 'start' and 'latest' options, where latest < start\n   */\n  def startVersionAfterLatestVersion(start: Long, latest: Long): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_CDC_START_VERSION_AFTER_LATEST\",\n      messageParameters = Array(start.toString, latest.toString))\n  }\n\n  def setTransactionVersionConflict(appId: String, version1: Long, version2: Long): Throwable = {\n    new IllegalArgumentException(\n      s\"Two SetTransaction actions within the same transaction have the same appId ${appId} but \" +\n        s\"different versions ${version1} and ${version2}.\")\n  }\n\n  def unexpectedChangeFilesFound(changeFiles: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_UNEXPECTED_CHANGE_FILES_FOUND\",\n      messageParameters = Array(changeFiles))\n  }\n\n  def addColumnAtIndexLessThanZeroException(pos: String, col: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ADD_COLUMN_AT_INDEX_LESS_THAN_ZERO\",\n      messageParameters = Array(pos, col))\n  }\n\n  def dropColumnAtIndexLessThanZeroException(pos: Int): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_DROP_COLUMN_AT_INDEX_LESS_THAN_ZERO\",\n      messageParameters = Array(s\"$pos\")\n    )\n  }\n\n  def columnNameNotFoundException(colName: String, scheme: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_NOT_FOUND\",\n      messageParameters = Array(colName, scheme))\n  }\n\n  def foundDuplicateColumnsException(colType: String, duplicateCols: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_DUPLICATE_COLUMNS_FOUND\",\n      messageParameters = Array(colType, duplicateCols))\n  }\n\n  def addColumnStructNotFoundException(pos: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ADD_COLUMN_STRUCT_NOT_FOUND\",\n      messageParameters = Array(pos))\n  }\n\n  def addColumnParentNotStructException(column: StructField, other: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ADD_COLUMN_PARENT_NOT_STRUCT\",\n      messageParameters = Array(s\"${column.name}\", s\"$other\"))\n  }\n\n  def operationNotSupportedException(\n      operation: String, tableIdentifier: TableIdentifier): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_OPERATION_NOT_ALLOWED_DETAIL\",\n      messageParameters = Array(operation, tableIdentifier.toString))\n  }\n\n  def operationNotSupportedException(operation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_OPERATION_NOT_ALLOWED\",\n      messageParameters = Array(operation))\n  }\n\n  def emptyDataException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_EMPTY_DATA\", messageParameters = Array.empty)\n  }\n\n  def fileNotFoundException(path: String): Throwable = {\n    new DeltaFileNotFoundException(\n      errorClass = \"DELTA_FILE_NOT_FOUND\",\n      messageParameters = Array(path))\n  }\n\n  def fileOrDirectoryNotFoundException(path: String): Throwable = {\n    new DeltaFileNotFoundException(\n      errorClass = \"DELTA_FILE_OR_DIR_NOT_FOUND\",\n      messageParameters = Array(path))\n  }\n\n  def excludeRegexOptionException(regexOption: String, cause: Throwable = null): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_REGEX_OPT_SYNTAX_ERROR\",\n      messageParameters = Array(regexOption),\n      cause = cause)\n  }\n\n  def notADeltaTableException(deltaTableIdentifier: DeltaTableIdentifier): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MISSING_DELTA_TABLE\",\n      messageParameters = Array(s\"$deltaTableIdentifier\"))\n  }\n\n  def notADeltaTableException(\n      operation: String, deltaTableIdentifier: DeltaTableIdentifier): Throwable = {\n    notADeltaTableException(operation, deltaTableIdentifier.toString)\n  }\n\n  def notADeltaTableException(operation: String, tableName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TABLE_ONLY_OPERATION\",\n      messageParameters = Array(tableName, operation))\n  }\n\n  def notADeltaTableException(operation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ONLY_OPERATION\",\n      messageParameters = Array(operation)\n    )\n  }\n\n  def notADeltaSourceException(command: String, plan: Option[LogicalPlan] = None): Throwable = {\n    val planName = if (plan.isDefined) plan.toString else \"\"\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_SOURCE\",\n      messageParameters = Array(command, s\"$planName\")\n    )\n  }\n\n  def partitionColumnCastFailed(\n      columnValue: String,\n      dataType: String,\n      columnName: String): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_PARTITION_COLUMN_CAST_FAILED\",\n      messageParameters = Array(columnValue, dataType, columnName))\n  }\n\n  def schemaChangedSinceAnalysis(\n      atAnalysis: StructType,\n      latestSchema: StructType,\n      mentionLegacyFlag: Boolean = false): Throwable = {\n    val schemaDiff = SchemaUtils.reportDifferences(atAnalysis, latestSchema)\n      .map(_.replace(\"Specified\", \"Latest\"))\n    val legacyFlagMessage = if (mentionLegacyFlag) {\n      s\"\"\"\n         |This check can be turned off by setting the session configuration key\n         |${DeltaSQLConf.DELTA_SCHEMA_ON_READ_CHECK_ENABLED.key} to false.\"\"\".stripMargin\n    } else {\n      \"\"\n    }\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\",\n      messageParameters = Array(schemaDiff.mkString(\"\\n\"), legacyFlagMessage)\n    )\n  }\n\n  def cloneWithRowTrackingWithoutStats(): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_CLONE_WITH_ROW_TRACKING_WITHOUT_STATS\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def incorrectArrayAccess(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INCORRECT_ARRAY_ACCESS\",\n      messageParameters = Array.empty)\n  }\n  def invalidColumnName(name: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAME\",\n      messageParameters = Array(name))\n  }\n  def invalidInventorySchema(expectedSchema: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_INVENTORY_SCHEMA\",\n      messageParameters = Array(expectedSchema)\n    )\n  }\n  def invalidIsolationLevelException(s: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_ISOLATION_LEVEL\",\n      messageParameters = Array(s))\n  }\n\n  def invalidPartitionColumn(col: String, tbl: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_PARTITION_COLUMN\",\n      messageParameters = Array(col, tbl))\n  }\n\n  def invalidPartitionColumn(e: AnalysisException): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_PARTITION_COLUMN_NAME\",\n      messageParameters = Array.empty,\n      cause = Option(e))\n  }\n\n  def invalidTimestampFormat(\n      ts: String,\n      format: String,\n      cause: Option[Throwable] = None): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_TIMESTAMP_FORMAT\",\n      messageParameters = Array(ts, format),\n      cause = cause)\n  }\n\n  def missingTableIdentifierException(operationName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_OPERATION_MISSING_PATH\",\n      messageParameters = Array(operationName)\n    )\n  }\n\n  def unsupportedDeepCloneException(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_UNSUPPORTED_DEEP_CLONE\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def viewInDescribeDetailException(view: TableIdentifier): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_DESCRIBE_DETAIL_VIEW\",\n      messageParameters = Array(s\"$view\")\n    )\n  }\n\n  def addCommentToMapArrayException(fieldPath: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_COMMENT_MAP_ARRAY\",\n      messageParameters = Array(fieldPath)\n    )\n  }\n\n  def alterTableChangeColumnException(\n      fieldPath: String,\n      oldField: StructField,\n      newField: StructField): Throwable = {\n    def fieldToString(field: StructField): String =\n      field.dataType.sql + (if (!field.nullable) \" NOT NULL\" else \"\")\n\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n      messageParameters = Array(\n        fieldPath,\n        fieldToString(oldField),\n        fieldToString(newField))\n    )\n  }\n\n  def alterTableReplaceColumnsException(\n      oldSchema: StructType,\n      newSchema: StructType,\n      reason: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_ALTER_TABLE_REPLACE_COL_OP\",\n      messageParameters = Array(reason, formatSchema(oldSchema), formatSchema(newSchema))\n    )\n  }\n\n  def unsupportedTypeChangeInPreview(\n      fieldPath: Seq[String],\n      fromType: DataType,\n      toType: DataType,\n      feature: TypeWideningTableFeatureBase): Throwable =\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_PREVIEW\",\n      messageParameters = Array(\n        SchemaUtils.prettyFieldName(fieldPath),\n        fromType.sql,\n        toType.sql,\n        feature.name\n    ))\n\n  def unsupportedTypeChangeInSchema(\n      fieldPath: Seq[String],\n      fromType: DataType,\n      toType: DataType)\n    : Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_SCHEMA\",\n      messageParameters = Array(SchemaUtils.prettyFieldName(fieldPath), fromType.sql, toType.sql)\n    )\n  }\n\n  def cannotWriteIntoView(table: TableIdentifier): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_WRITE_INTO_VIEW\",\n      messageParameters = Array(s\"$table\")\n    )\n  }\n\n  def castingCauseOverflowErrorInTableWrite(\n      from: DataType,\n      to: DataType,\n      columnName: String): ArithmeticException = {\n    new DeltaArithmeticException(\n      errorClass = \"DELTA_CAST_OVERFLOW_IN_TABLE_WRITE\",\n      messageParameters = Array(\n        toSQLType(from), // sourceType\n        toSQLType(to), // targetType\n        toSQLId(columnName), // columnName\n        SQLConf.STORE_ASSIGNMENT_POLICY.key, // storeAssignmentPolicyFlag\n        // updateAndMergeCastingFollowsAnsiEnabledFlag\n        DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key,\n        SQLConf.ANSI_ENABLED.key // ansiEnabledFlag\n      )\n    )\n  }\n\n  def notADeltaTable(table: String): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_NOT_A_DELTA_TABLE\",\n      messageParameters = Array(table))\n  }\n\n  def unsupportedWriteStagedTable(tableName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_WRITES_STAGED_TABLE\",\n      messageParameters = Array(tableName)\n    )\n  }\n\n  def notEnoughColumnsInInsert(\n      table: String,\n      query: Int,\n      target: Int,\n      nestedField: Option[String] = None): Throwable = {\n    val nestedFieldStr = nestedField.map(f => s\"not enough nested fields in $f\")\n      .getOrElse(\"not enough data columns\")\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\",\n      messageParameters = Array(table, nestedFieldStr, target.toString, query.toString))\n  }\n\n  def notFoundFileToBeRewritten(absolutePath: String, candidates: Iterable[String]): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_FILE_TO_OVERWRITE_NOT_FOUND\",\n      messageParameters = Array(absolutePath, candidates.mkString(\"\\n\")))\n  }\n\n  def cannotFindSourceVersionException(json: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CANNOT_FIND_VERSION\",\n      messageParameters = Array(json))\n  }\n\n  def cannotInsertIntoColumn(\n      tableName: String,\n      source: String,\n      target: String,\n      targetType: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_STRUCT_TYPE_MISMATCH\",\n      messageParameters = Array(source, targetType, target, tableName))\n  }\n\n  def ambiguousPartitionColumnException(\n      columnName: String, colMatches: Seq[StructField]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_AMBIGUOUS_PARTITION_COLUMN\",\n      messageParameters = Array(formatColumn(columnName).toString,\n        formatColumnList(colMatches.map(_.name)))\n    )\n  }\n\n  def tableNotSupportedException(operation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TABLE_NOT_SUPPORTED_IN_OP\",\n      messageParameters = Array(operation)\n    )\n  }\n\n  def vacuumBasePathMissingException(baseDeltaPath: Path): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_VACUUM_SPECIFIC_PARTITION\",\n      messageParameters = Array(s\"$baseDeltaPath\")\n    )\n  }\n\n  def unexpectedDataChangeException(op: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_DATA_CHANGE_FALSE\",\n      messageParameters = Array(op)\n    )\n  }\n\n  def unknownConfigurationKeyException(confKey: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNKNOWN_CONFIGURATION\",\n      messageParameters = Array(confKey, DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES.key))\n  }\n\n  def cdcNotAllowedInThisVersion(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CDC_NOT_ALLOWED_IN_THIS_VERSION\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def cdcWriteNotAllowedInThisVersion(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CHANGE_TABLE_FEED_DISABLED\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def pathNotSpecifiedException: Throwable = {\n    new IllegalArgumentException(\"'path' is not specified\")\n  }\n\n  def pathNotExistsException(path: String): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_PATH_DOES_NOT_EXIST\",\n      messageParameters = Array(path))\n  }\n\n  def directoryNotFoundException(path: String): Throwable = {\n    new FileNotFoundException(s\"$path doesn't exist\")\n  }\n\n  def pathAlreadyExistsException(path: Path): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_PATH_EXISTS\",\n      messageParameters = Array(s\"$path\")\n    )\n  }\n\n  def truncatedTransactionLogException(\n      path: Path,\n      version: Long,\n      metadata: Metadata): Throwable = {\n    val logRetention = DeltaConfigs.LOG_RETENTION.fromMetaData(metadata)\n    val checkpointRetention = DeltaConfigs.CHECKPOINT_RETENTION_DURATION.fromMetaData(metadata)\n    new DeltaFileNotFoundException(\n      errorClass = \"DELTA_TRUNCATED_TRANSACTION_LOG\",\n      messageParameters = Array(\n        path.toString,\n        version.toString,\n        DeltaConfigs.LOG_RETENTION.key,\n        logRetention.toString,\n        DeltaConfigs.CHECKPOINT_RETENTION_DURATION.key,\n        checkpointRetention.toString)\n    )\n  }\n\n  def logFileNotFoundException(\n      path: Path,\n      version: Option[Long],\n      checkpointVersion: Long): Throwable = {\n    new DeltaFileNotFoundException(\n      errorClass = \"DELTA_LOG_FILE_NOT_FOUND\",\n      messageParameters = Array(\n        version.map(_.toString).getOrElse(\"LATEST\"),\n        checkpointVersion.toString,\n        path.toString)\n    )\n  }\n\n  def logFileNotFoundExceptionForStreamingSource(e: FileNotFoundException): Throwable = {\n    new DeltaFileNotFoundException(\n      errorClass = \"DELTA_LOG_FILE_NOT_FOUND_FOR_STREAMING_SOURCE\",\n      messageParameters = Array.empty\n    ).initCause(e)\n  }\n\n  def logFailedIntegrityCheck(version: Long, mismatchOption: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_TXN_LOG_FAILED_INTEGRITY\",\n      messageParameters = Array(version.toString, mismatchOption)\n    )\n  }\n\n  def checkpointNonExistTable(path: Path): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CHECKPOINT_NON_EXIST_TABLE\",\n      messageParameters = Array(s\"$path\"))\n  }\n\n  def multipleLoadPathsException(paths: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"MULTIPLE_LOAD_PATH\",\n      messageParameters = Array(paths.mkString(\"[\", \",\", \"]\")))\n  }\n\n  def partitionColumnNotFoundException(colName: String, schema: Seq[Attribute]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_PARTITION_COLUMN_NOT_FOUND\",\n      messageParameters = Array(\n        s\"${formatColumn(colName)}\",\n        s\"${schema.map(_.name).mkString(\", \")}\"\n      )\n    )\n  }\n\n  def partitionPathParseException(fragment: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_PARTITION_PATH\",\n      messageParameters = Array(fragment))\n  }\n\n  def partitionPathInvolvesNonPartitionColumnException(\n      badColumns: Seq[String], fragment: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_PARTITION_COLUMN_SPECIFIED\",\n      messageParameters = Array(formatColumnList(badColumns), fragment)\n    )\n  }\n\n  def unsupportedPartitionColumnChange(\n      operation: String,\n      oldPartitionColumns: Seq[String],\n      newPartitionColumns: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_PARTITION_COLUMN_CHANGE\",\n      messageParameters = Array(\n        operation,\n        oldPartitionColumns.mkString(\", \"),\n        newPartitionColumns.mkString(\", \")\n      )\n    )\n  }\n\n  def nonPartitionColumnAbsentException(colsDropped: Boolean): Throwable = {\n    val msg = if (colsDropped) {\n      \" Columns which are of NullType have been dropped.\"\n    } else {\n      \"\"\n    }\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_PARTITION_COLUMN_ABSENT\",\n      messageParameters = Array(msg)\n    )\n  }\n\n  def replaceWhereMismatchException(\n      replaceWhere: String,\n      invariantViolation: InvariantViolationException): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_REPLACE_WHERE_MISMATCH\",\n      messageParameters = Array(replaceWhere, invariantViolation.getMessage),\n      cause = Some(invariantViolation))\n  }\n\n  def replaceWhereMismatchException(replaceWhere: String, badPartitions: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_REPLACE_WHERE_MISMATCH\",\n      messageParameters = Array(replaceWhere,\n        s\"Invalid data would be written to partitions $badPartitions.\"))\n  }\n\n  def illegalFilesFound(file: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_ILLEGAL_FILE_FOUND\",\n      messageParameters = Array(file))\n  }\n\n  def illegalDeltaOptionException(name: String, input: String, explain: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_ILLEGAL_OPTION\",\n      messageParameters = Array(input, name, explain))\n  }\n\n  def invalidIdempotentWritesOptionsException(explain: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_IDEMPOTENT_WRITES_OPTIONS\",\n      messageParameters = Array(explain))\n  }\n\n  def invalidInterval(interval: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_INTERVAL\",\n      messageParameters = Array(interval)\n      )\n  }\n\n  def invalidTableValueFunction(function: String) : Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_TABLE_VALUE_FUNCTION\",\n      messageParameters = Array(function)\n    )\n  }\n\n  def startingVersionAndTimestampBothSetException(\n      versionOptKey: String,\n      timestampOptKey: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_STARTING_VERSION_AND_TIMESTAMP_BOTH_SET\",\n      messageParameters = Array(versionOptKey, timestampOptKey))\n  }\n\n  def unrecognizedLogFile(path: Path): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNRECOGNIZED_LOGFILE\",\n      messageParameters = Array(s\"$path\")\n    )\n  }\n\n  def modifyAppendOnlyTableException(tableName: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_CANNOT_MODIFY_APPEND_ONLY\",\n      // `tableName` could be null here, so convert to string first.\n      messageParameters = Array(s\"$tableName\", DeltaConfigs.IS_APPEND_ONLY.key)\n    )\n  }\n\n  def missingPartFilesException(version: Long, ae: Exception): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MISSING_PART_FILES\",\n      messageParameters = Array(s\"$version\"),\n      cause = ae\n    )\n  }\n\n  def deltaVersionsNotContiguousException(\n      spark: SparkSession,\n      deltaVersions: Seq[Long],\n      startVersion: Long,\n      endVersion: Long,\n      versionToLoad: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_VERSIONS_NOT_CONTIGUOUS\",\n      messageParameters = Array(\n        deltaVersions.mkString(\", \"),\n        startVersion.toString,\n        endVersion.toString,\n        versionToLoad.toString\n      )\n    )\n  }\n\n  def actionNotFoundException(action: String, version: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_STATE_RECOVER_ERROR\",\n      messageParameters = Array(action, version.toString))\n  }\n\n\n  def schemaChangedException(\n      readSchema: StructType,\n      dataSchema: StructType,\n      retryable: Boolean,\n      version: Option[Long],\n      includeStartingVersionOrTimestampMessage: Boolean): Throwable = {\n    def newException(errorClass: String, messageParameters: Array[String]): Throwable = {\n      new DeltaIllegalStateException(errorClass, messageParameters)\n    }\n\n    if (version.isEmpty) {\n      newException(\"DELTA_SCHEMA_CHANGED\", Array(\n        formatSchema(readSchema),\n        formatSchema(dataSchema)\n        ))\n    } else if (!includeStartingVersionOrTimestampMessage) {\n      newException(\"DELTA_SCHEMA_CHANGED_WITH_VERSION\", Array(\n        version.get.toString,\n        formatSchema(readSchema),\n        formatSchema(dataSchema)\n      ))\n    } else {\n      newException(\"DELTA_SCHEMA_CHANGED_WITH_STARTING_OPTIONS\", Array(\n        version.get.toString,\n        formatSchema(readSchema),\n        formatSchema(dataSchema),\n        version.get.toString\n      ))\n    }\n  }\n\n  def streamingSchemaMismatchOnRestart(\n      querySchema: StructType,\n      tableSchema: StructType): RuntimeException = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART\",\n      messageParameters = Array(formatSchema(querySchema), formatSchema(tableSchema)))\n  }\n\n  def streamWriteNullTypeException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NULL_SCHEMA_IN_STREAMING_WRITE\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def schemaNotSetException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_SCHEMA_NOT_SET\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def specifySchemaAtReadTimeException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_SCHEMA_DURING_READ\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def readSourceSchemaConflictException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_READ_SOURCE_SCHEMA_CONFLICT\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def schemaNotProvidedException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_SCHEMA_NOT_PROVIDED\",\n      messageParameters = Array.empty)\n  }\n\n  def outputModeNotSupportedException(dataSource: String, outputMode: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_OUTPUT_MODE\",\n      messageParameters = Array(dataSource, outputMode)\n    )\n  }\n\n  def updateSetColumnNotFoundException(col: String, colList: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MISSING_SET_COLUMN\",\n      messageParameters = Array(formatColumn(col), formatColumnList(colList)))\n  }\n\n  def updateSetConflictException(cols: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONFLICT_SET_COLUMN\",\n      messageParameters = Array(formatColumnList(cols)))\n  }\n\n  def updateNonStructTypeFieldNotSupportedException(col: String, s: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_FIELD_UPDATE_NON_STRUCT\",\n      messageParameters = Array(s\"${formatColumn(col)}\", s\"$s\")\n    )\n  }\n\n  def truncateTablePartitionNotSupportedException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TRUNCATE_TABLE_PARTITION_NOT_SUPPORTED\", messageParameters = Array.empty\n    )\n  }\n\n  def bloomFilterOnPartitionColumnNotSupportedException(name: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_PARTITION_COLUMN_IN_BLOOM_FILTER\",\n      messageParameters = Array(name))\n  }\n\n  def bloomFilterOnNestedColumnNotSupportedException(name: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_NESTED_COLUMN_IN_BLOOM_FILTER\",\n      messageParameters = Array(name))\n  }\n\n  def bloomFilterOnColumnTypeNotSupportedException(name: String, dataType: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_COLUMN_TYPE_IN_BLOOM_FILTER\",\n      messageParameters = Array(s\"${dataType.catalogString}\", name))\n  }\n\n  def bloomFilterMultipleConfForSingleColumnException(name: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MULTIPLE_CONF_FOR_SINGLE_COLUMN_IN_BLOOM_FILTER\",\n      messageParameters = Array(name))\n  }\n\n  def bloomFilterCreateOnNonExistingColumnsException(unknownColumns: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_CREATE_BLOOM_FILTER_NON_EXISTING_COL\",\n      messageParameters = Array(unknownColumns.mkString(\", \")))\n  }\n\n  def bloomFilterInvalidParameterValueException(message: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0002\",\n      messageParameters = Array(message)\n    )\n  }\n\n  def bloomFilterDropOnNonIndexedColumnException(name: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_DROP_BLOOM_FILTER_ON_NON_INDEXED_COLUMN\",\n      messageParameters = Array(name))\n  }\n\n  def bloomFilterDropOnNonExistingColumnsException(unknownColumns: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_BLOOM_FILTER_DROP_ON_NON_EXISTING_COLUMNS\",\n      messageParameters = Array(unknownColumns.mkString(\", \"))\n    )\n  }\n\n\n  def cannotRenamePath(tempPath: String, path: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CANNOT_RENAME_PATH\", messageParameters = Array(tempPath, path))\n  }\n\n  def cannotSpecifyBothFileListAndPatternString(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_FILE_LIST_AND_PATTERN_STRING_CONFLICT\",\n      messageParameters = Array.empty)\n  }\n\n  def cannotUpdateArrayField(table: String, field: String): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_CANNOT_UPDATE_ARRAY_FIELD\",\n      messageParameters = Array(table, field, field))\n  }\n\n  def cannotUpdateMapField(table: String, field: String): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_CANNOT_UPDATE_MAP_FIELD\",\n      messageParameters = Array(table, field, field, field))\n  }\n\n  def cannotUpdateStructField(table: String, field: String): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_CANNOT_UPDATE_STRUCT_FIELD\",\n      messageParameters = Array(table, field))\n  }\n\n  def cannotUpdateOtherField(tableName: String, dataType: DataType): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_CANNOT_UPDATE_OTHER_FIELD\",\n      messageParameters = Array(tableName, s\"$dataType\"))\n  }\n\n  def cannotUseDataTypeForPartitionColumnError(field: StructField): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_PARTITION_COLUMN_TYPE\",\n      messageParameters = Array(s\"${field.name}\", s\"${field.dataType}\")\n    )\n  }\n\n  def unexpectedPartitionSchemaFromUserException(\n    catalogPartitionSchema: StructType, userPartitionSchema: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNEXPECTED_PARTITION_SCHEMA_FROM_USER\",\n      messageParameters = Array(\n        formatSchema(catalogPartitionSchema), formatSchema(userPartitionSchema))\n    )\n  }\n\n  def multipleSourceRowMatchingTargetRowInMergeException(spark: SparkSession): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_MULTIPLE_SOURCE_ROW_MATCHING_TARGET_ROW_IN_MERGE\",\n      messageParameters = Array(generateDocsLinkOption(spark,\n        \"/delta-update.html#upsert-into-a-table-using-merge\").getOrElse(\"-\"))\n    )\n  }\n\n  def sourceMaterializationFailedRepeatedlyInMerge: Throwable =\n    new DeltaRuntimeException(errorClass = \"DELTA_MERGE_MATERIALIZE_SOURCE_FAILED_REPEATEDLY\")\n\n  def sourceNotDeterministicInMergeException(spark: SparkSession): Throwable = {\n    val docRefer =\n      generateDocsLinkOption(spark, \"/delta-update.html#operation-semantics\")\n        .map(link => s\" Please refer to $link for more information.\")\n        .getOrElse(\"\")\n    new UnsupportedOperationException(\n      s\"Cannot perform Merge because the source dataset is not deterministic.$docRefer\"\n    )\n  }\n\n  def mergeConcurrentOperationCachedSourceException(): Throwable =\n    new DeltaRuntimeException(errorClass = \"DELTA_MERGE_SOURCE_CACHED_DURING_EXECUTION\")\n\n  def columnOfTargetTableNotFoundInMergeException(targetCol: String,\n      colNames: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_NOT_FOUND_IN_MERGE\",\n      messageParameters = Array(targetCol, colNames)\n    )\n  }\n\n  def subqueryNotSupportedException(op: String, cond: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_SUBQUERY\",\n      messageParameters = Array(op, cond.sql)\n    )\n  }\n\n  def multiColumnInPredicateNotSupportedException(operation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_MULTI_COL_IN_PREDICATE\",\n      messageParameters = Array(operation)\n    )\n  }\n\n  def nestedFieldNotSupported(operation: String, field: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_NESTED_FIELD_IN_OPERATION\",\n      messageParameters = Array(operation, field)\n    )\n  }\n\n  def inSubqueryNotSupportedException(operation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_IN_SUBQUERY\",\n      messageParameters = Array(operation))\n  }\n\n  def convertMetastoreMetadataMismatchException(\n      tableProperties: Map[String, String],\n      deltaConfiguration: Map[String, String]): Throwable = {\n    def prettyMap(m: Map[String, String]): String = {\n      m.map(e => s\"${e._1}=${e._2}\").mkString(\"[\", \", \", \"]\")\n    }\n    new DeltaAnalysisException(\n      errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0003\",\n      messageParameters = Array(\n        prettyMap(tableProperties),\n        prettyMap(deltaConfiguration),\n        DeltaSQLConf.DELTA_CONVERT_METADATA_CHECK_ENABLED.key)\n    )\n  }\n\n  def createExternalTableWithoutLogException(\n      path: Path, tableName: String, spark: SparkSession): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_EXTERNAL_TABLE_WITHOUT_TXN_LOG\",\n      messageParameters = Array(\n        tableName,\n        path.toString,\n        new Path(path, \"_delta_log\").toString,\n        generateDocsLinkOption(spark, \"/index.html\").getOrElse(\"-\")))\n  }\n\n  def createExternalTableWithoutSchemaException(\n      path: Path, tableName: String, spark: SparkSession): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_EXTERNAL_TABLE_WITHOUT_SCHEMA\",\n      messageParameters = Array(tableName, path.toString,\n        generateDocsLinkOption(spark, \"/index.html\").getOrElse(\"-\")))\n  }\n\n  def createManagedTableWithoutSchemaException(\n      tableName: String, spark: SparkSession): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_MANAGED_TABLE_SYNTAX_NO_SCHEMA\",\n      messageParameters = Array(tableName,\n        generateDocsLinkOption(spark, \"/index.html\").getOrElse(\"-\"))\n    )\n  }\n\n  def readTableWithoutSchemaException(identifier: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_READ_TABLE_WITHOUT_COLUMNS\",\n      messageParameters = Array(identifier))\n  }\n\n  def createTableWithDifferentSchemaException(\n      path: Path,\n      specifiedSchema: StructType,\n      existingSchema: StructType,\n      diffs: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_SCHEME_MISMATCH\",\n      messageParameters = Array(path.toString,\n        specifiedSchema.treeString, existingSchema.treeString,\n        diffs.map(\"\\n\".r.replaceAllIn(_, \"\\n  \")).mkString(\"- \", \"\\n- \", \"\")))\n  }\n\n  def createTableWithDifferentPartitioningException(\n      path: Path,\n      specifiedColumns: Seq[String],\n      existingColumns: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_WITH_DIFFERENT_PARTITIONING\",\n      messageParameters = Array(\n        path.toString,\n        specifiedColumns.mkString(\", \"),\n        existingColumns.mkString(\", \")\n      )\n    )\n  }\n\n  def createTableWithDifferentPropertiesException(\n      path: Path,\n      specifiedProperties: Map[String, String],\n      existingProperties: Map[String, String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_WITH_DIFFERENT_PROPERTY\",\n      messageParameters = Array(path.toString,\n        specifiedProperties.toSeq.sorted.map { case (k, v) => s\"$k=$v\" }.mkString(\"\\n\"),\n        existingProperties.toSeq.sorted.map { case (k, v) => s\"$k=$v\" }.mkString(\"\\n\"))\n    )\n  }\n\n  def aggsNotSupportedException(op: String, cond: Expression): Throwable = {\n    val condStr = s\"(condition = ${cond.sql})\"\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_AGGREGATION_NOT_SUPPORTED\",\n      messageParameters = Array(op, condStr)\n    )\n  }\n\n  def targetTableFinalSchemaEmptyException(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TARGET_TABLE_FINAL_SCHEMA_EMPTY\",\n      messageParameters = Array.empty)\n  }\n\n  def nonDeterministicNotSupportedException(op: String, cond: Expression): Throwable = {\n    val condStr = s\"(condition = ${cond.sql}).\"\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\",\n      messageParameters = Array(op, s\"$condStr\")\n    )\n  }\n\n  def noHistoryFound(logPath: Path): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NO_COMMITS_FOUND\",\n      messageParameters = Array(logPath.toString))\n  }\n\n  def noRecreatableHistoryFound(logPath: Path): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NO_RECREATABLE_HISTORY_FOUND\",\n      messageParameters = Array(s\"$logPath\"))\n  }\n\n  def unsupportedAbsPathAddFile(str: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNSUPPORTED_ABS_PATH_ADD_FILE\",\n      messageParameters = Array(str)\n    )\n  }\n\n  case class TimestampEarlierThanCommitRetentionException(\n      userTimestamp: java.sql.Timestamp,\n      commitTs: java.sql.Timestamp,\n      timestampString: String) extends DeltaAnalysisException(\n    errorClass = \"DELTA_TIMESTAMP_EARLIER_THAN_COMMIT_RETENTION\",\n    messageParameters = Array(userTimestamp.toString, commitTs.toString, timestampString)\n  )\n\n  def timestampGreaterThanLatestCommit(\n      userTs: java.sql.Timestamp,\n      lastCommitTs: java.sql.Timestamp,\n      maximumTsStr: String): Throwable = {\n    TemporallyUnstableInputException(userTs, lastCommitTs, maximumTsStr)\n  }\n\n  def timestampInvalid(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TIMESTAMP_INVALID\",\n      messageParameters = Array(s\"${expr.sql}\")\n    )\n  }\n\n  def versionInvalid(version: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_VERSION_INVALID\",\n      messageParameters = Array(s\"$version\")\n    )\n  }\n\n  case class TemporallyUnstableInputException(\n      userTs: java.sql.Timestamp,\n      lastCommitTs: java.sql.Timestamp,\n      maximumTsStr: String)\n    extends DeltaAnalysisException(\n      errorClass = \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n      messageParameters = Array(s\"$userTs\", s\"$lastCommitTs\", maximumTsStr))\n\n  def restoreVersionNotExistException(\n      userVersion: Long,\n      earliest: Long,\n      latest: Long): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_RESTORE_TABLE_VERSION\",\n      messageParameters = Array(userVersion.toString, earliest.toString, latest.toString))\n  }\n\n  def restoreTimestampGreaterThanLatestException(\n      userTimestamp: String,\n      latestTimestamp: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_RESTORE_TIMESTAMP_GREATER\",\n      messageParameters = Array(userTimestamp, latestTimestamp)\n    )\n  }\n\n  def restoreTimestampBeforeEarliestException(\n      userTimestamp: String,\n      earliestTimestamp: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_RESTORE_TIMESTAMP_EARLIER\",\n      messageParameters = Array(userTimestamp, earliestTimestamp)\n    )\n  }\n\n  def timeTravelNotSupportedException: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_TIME_TRAVEL_VIEWS\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def multipleTimeTravelSyntaxUsed: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_TIME_TRAVEL_MULTIPLE_FORMATS\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def timeTravelBeyondDeletedFileRetentionDurationException(\n    deletedFileRetentionDurationHours: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_TIME_TRAVEL_BEYOND_DELETED_FILE_RETENTION_DURATION\",\n      messageParameters = Array(deletedFileRetentionDurationHours)\n    )\n  }\n\n  def nonExistentDeltaTable(tableId: DeltaTableIdentifier): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TABLE_NOT_FOUND\",\n      messageParameters = Array(s\"$tableId\"))\n  }\n\n  def differentDeltaTableReadByStreamingSource(\n      newTableId: String, oldTableId: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DIFFERENT_DELTA_TABLE_READ_BY_STREAMING_SOURCE\",\n      messageParameters = Array(newTableId, oldTableId))\n  }\n\n  def nonExistentColumnInSchema(column: String, schema: String): Throwable = {\n    new DeltaAnalysisException(\"DELTA_COLUMN_NOT_FOUND_IN_SCHEMA\",\n      Array(column, schema))\n  }\n\n  def noRelationTable(tableIdent: Identifier): Throwable = {\n    new DeltaNoSuchTableException(\n      errorClass = \"DELTA_NO_RELATION_TABLE\",\n      errorMessageParameters = Array(s\"${tableIdent.quoted}\"))\n  }\n\n  def provideOneOfInTimeTravel: Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_ONEOF_IN_TIMETRAVEL\", messageParameters = Array.empty)\n  }\n\n  def emptyCalendarInterval: Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_CALENDAR_INTERVAL_EMPTY\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def unexpectedPartialScan(path: Path): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNEXPECTED_PARTIAL_SCAN\",\n      messageParameters = Array(s\"$path\")\n    )\n  }\n\n  def deltaLogAlreadyExistsException(path: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_LOG_ALREADY_EXISTS\",\n      messageParameters = Array(path)\n    )\n  }\n\n  def missingProviderForConvertException(path: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MISSING_PROVIDER_FOR_CONVERT\",\n      messageParameters = Array(path))\n  }\n\n  def convertNonParquetTablesException(ident: TableIdentifier, sourceName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONVERT_NON_PARQUET_TABLE\",\n      messageParameters = Array(sourceName, ident.toString)\n    )\n  }\n\n  def unexpectedPartitionColumnFromFileNameException(\n      path: String, parsedCol: String, expectedCol: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNEXPECTED_PARTITION_COLUMN_FROM_FILE_NAME\",\n      messageParameters = Array(\n        formatColumn(expectedCol),\n        formatColumn(parsedCol),\n        path)\n      )\n  }\n\n  def unexpectedNumPartitionColumnsFromFileNameException(\n      path: String, parsedCols: Seq[String], expectedCols: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNEXPECTED_NUM_PARTITION_COLUMNS_FROM_FILE_NAME\",\n      messageParameters = Array(\n        expectedCols.size.toString,\n        formatColumnList(expectedCols),\n        parsedCols.size.toString,\n        formatColumnList(parsedCols),\n        path)\n    )\n  }\n\n  def castPartitionValueException(partitionValue: String, dataType: DataType): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_FAILED_CAST_PARTITION_VALUE\",\n      messageParameters = Array(partitionValue, dataType.toString))\n  }\n\n  def emptyDirectoryException(directory: String): Throwable = {\n    new DeltaFileNotFoundException(\n      errorClass = \"DELTA_EMPTY_DIRECTORY\",\n      messageParameters = Array(directory)\n    )\n  }\n\n  def alterTableSetLocationSchemaMismatchException(\n      original: StructType, destination: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_SET_LOCATION_SCHEMA_MISMATCH\",\n      messageParameters = Array(formatSchema(original), formatSchema(destination),\n        DeltaSQLConf.DELTA_ALTER_LOCATION_BYPASS_SCHEMA_CHECK.key))\n  }\n\n  def sparkSessionNotSetException(): Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_SPARK_SESSION_NOT_SET\")\n  }\n\n  def setLocationNotSupportedOnPathIdentifiers(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_SET_LOCATION_ON_PATH_IDENTIFIER\",\n      messageParameters = Array.empty)\n  }\n\n  def useSetLocation(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_CHANGE_LOCATION\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def cannotSetLocationMultipleTimes(locations : Seq[String]) : Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_CANNOT_SET_LOCATION_MULTIPLE_TIMES\",\n      messageParameters = Array(s\"${locations}\")\n    )\n  }\n\n  def cannotReplaceMissingTableException(itableIdentifier: Identifier): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_REPLACE_MISSING_TABLE\",\n      messageParameters = Array(itableIdentifier.toString))\n  }\n\n  def cannotCreateLogPathException(logPath: String, cause: Throwable = null): Throwable = {\n    new DeltaIOException(\n      errorClass = \"DELTA_CANNOT_CREATE_LOG_PATH\",\n      messageParameters = Array(logPath),\n      cause = cause)\n  }\n\n  def cannotChangeProvider(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_CHANGE_PROVIDER\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def describeViewHistory: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_DESCRIBE_VIEW_HISTORY\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def viewNotSupported(operationName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_OPERATION_ON_VIEW_NOT_ALLOWED\",\n      messageParameters = Array(operationName)\n    )\n  }\n\n  def postCommitHookFailedException(\n      failedHook: PostCommitHook,\n      failedOnCommitVersion: Long,\n      extraErrorMessage: String,\n      error: Throwable): Throwable = {\n    var errorMessage = \"\"\n    if (extraErrorMessage != null && extraErrorMessage.nonEmpty) {\n      errorMessage += s\": $extraErrorMessage\"\n    }\n    val ex = new DeltaRuntimeException(\n      errorClass = \"DELTA_POST_COMMIT_HOOK_FAILED\",\n      messageParameters = Array(s\"$failedOnCommitVersion\", failedHook.name, errorMessage)\n    )\n    ex.initCause(error)\n    ex\n  }\n\n  private def unsupportedModeException(modeName: String, supportedModes: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_MODE_NOT_SUPPORTED\",\n      messageParameters = Array(modeName, supportedModes))\n  }\n\n  def unsupportedColumnMappingModeException(modeName: String): Throwable = {\n    val supportedColumnMappingModes =\n      DeltaColumnMapping.supportedModes.map(_.name).toSeq.mkString(\", \")\n    unsupportedModeException(modeName, supportedColumnMappingModes)\n  }\n\n  def unsupportedGenerateModeException(modeName: String): Throwable = {\n    val supportedGenerateCommandModes =\n      DeltaGenerateCommand.modeNameToGenerationFunc.keys.toSeq.mkString(\", \")\n    unsupportedModeException(modeName, supportedGenerateCommandModes)\n  }\n\n  def illegalUsageException(option: String, operation: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_ILLEGAL_USAGE\",\n      messageParameters = Array(option, operation))\n  }\n\n  def foundMapTypeColumnException(key: String, value: String, schema: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_FOUND_MAP_TYPE_COLUMN\",\n      messageParameters = Array(key, value, dataTypeToString(schema))\n    )\n  }\n  def columnNotInSchemaException(column: String, schema: DataType): Throwable = {\n    nonExistentColumnInSchema(column, dataTypeToString(schema))\n  }\n\n  def metadataAbsentException(): Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_METADATA_ABSENT\",\n      messageParameters = Array.empty)\n  }\n\n  def metadataAbsentForExistingCatalogTable(tableName: String, tablePath: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_METADATA_ABSENT_EXISTING_CATALOG_TABLE\",\n      messageParameters = Array(tableName, tablePath, tableName))\n  }\n\n  def deltaCannotVacuumLite(): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CANNOT_VACUUM_LITE\")\n  }\n\n  def vacuumRetentionPeriodNegative(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_VACUUM_RETENTION_PERIOD_NEGATIVE\")\n  }\n\n  def vacuumRetentionPeriodTooShort(configuredRetentionHours: Long): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_VACUUM_RETENTION_PERIOD_TOO_SHORT\",\n      messageParameters = Array(configuredRetentionHours.toString))\n  }\n\n  def updateSchemaMismatchExpression(from: StructType, to: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UPDATE_SCHEMA_MISMATCH_EXPRESSION\",\n      messageParameters = Array(from.catalogString, to.catalogString)\n    )\n  }\n\n  def extractReferencesFieldNotFound(field: String, exception: Throwable): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_EXTRACT_REFERENCES_FIELD_NOT_FOUND\",\n      messageParameters = Array(field),\n      cause = exception)\n  }\n\n  def addFilePartitioningMismatchException(\n    addFilePartitions: Seq[String],\n    metadataPartitions: Seq[String]): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_INVALID_PARTITIONING_SCHEMA\",\n      messageParameters = Array(s\"${DeltaErrors.formatColumnList(metadataPartitions)}\",\n        s\"${DeltaErrors.formatColumnList(addFilePartitions)}\",\n        s\"${DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED.key}\")\n    )\n  }\n\n  def concurrentModificationExceptionMsg(\n      sparkConf: SparkConf,\n      baseMessage: String,\n      commit: Option[CommitInfo]): String = {\n    baseMessage +\n      commit.map(ci => s\"\\nConflicting commit: ${JsonUtils.toJson(ci)}\").getOrElse(\"\") +\n      s\"\\nRefer to \" +\n      s\"${DeltaErrors.generateDocsLink(sparkConf, \"/concurrency-control.html\")} \" +\n      \"for more details.\"\n  }\n\n  def ignoreStreamingUpdatesAndDeletesWarning(spark: SparkSession): String = {\n    val docPage =\n      generateDocsLinkOption(spark, \"/delta-streaming.html#ignoring-updates-and-deletes\")\n        .map(link => s\" Refer to $link for details.\")\n        .getOrElse(\"\")\n    s\"\"\"WARNING: The 'ignoreFileDeletion' option is deprecated. Switch to using one of\n       |'ignoreDeletes' or 'ignoreChanges'.$docPage\n         \"\"\".stripMargin\n  }\n\n  def configureSparkSessionWithExtensionAndCatalog(\n      originalException: Option[Throwable]): Throwable = {\n    val catalogImplConfig = SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG\",\n      messageParameters = Array(\"io.delta.sql.DeltaSparkSessionExtension\",\n        catalogImplConfig, \"org.apache.spark.sql.delta.catalog.DeltaCatalog\",\n        \"io.delta.sql.DeltaSparkSessionExtension\",\n        catalogImplConfig, \"org.apache.spark.sql.delta.catalog.DeltaCatalog\"),\n      cause = originalException)\n  }\n\n  def duplicateColumnsOnUpdateTable(originalException: Throwable): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_DUPLICATE_COLUMNS_ON_UPDATE_TABLE\",\n      messageParameters = Array(originalException.getMessage),\n      cause = Some(originalException))\n  }\n\n  def maxCommitRetriesExceededException(\n      attemptNumber: Int,\n      attemptVersion: Long,\n      initAttemptVersion: Long,\n      numActions: Int,\n      totalCommitAttemptTime: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MAX_COMMIT_RETRIES_EXCEEDED\",\n      messageParameters = Array(s\"$attemptNumber\", s\"$initAttemptVersion\", s\"$attemptVersion\",\n        s\"$numActions\", s\"$totalCommitAttemptTime\"))\n  }\n\n  def generatedColumnsReferToWrongColumns(e: AnalysisException): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_GENERATED_COLUMN_REFERENCES\", Array.empty, cause = Some(e))\n  }\n\n  def generatedColumnsUpdateColumnType(current: StructField, update: StructField): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_GENERATED_COLUMN_UPDATE_TYPE_MISMATCH\",\n      messageParameters = Array(\n        s\"${current.name}\",\n        s\"${current.dataType.sql}\",\n        s\"${update.dataType.sql}\"\n      )\n    )\n  }\n\n  def generatedColumnsUDF(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UDF_IN_GENERATED_COLUMN\",\n      messageParameters = Array(s\"${expr.sql}\"))\n  }\n\n  def generatedColumnsNonDeterministicExpression(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_DETERMINISTIC_EXPRESSION_IN_GENERATED_COLUMN\",\n      messageParameters = Array(s\"${expr.sql}\"))\n  }\n\n  def generatedColumnsAggregateExpression(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_AGGREGATE_IN_GENERATED_COLUMN\",\n      messageParameters = Array(expr.sql.toString)\n    )\n  }\n\n  def generatedColumnsUnsupportedExpression(expr: Expression): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_EXPRESSION_GENERATED_COLUMN\",\n      messageParameters = Array(s\"${expr.sql}\")\n    )\n  }\n\n  def generatedColumnsUnsupportedType(dt: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_DATA_TYPE_IN_GENERATED_COLUMN\",\n      messageParameters = Array(s\"${dt.sql}\")\n    )\n  }\n\n  def generatedColumnsExprTypeMismatch(\n      column: String,\n      columnType: DataType,\n      exprType: DataType): Throwable = {\n    val exprTypeSql = exprType.sql\n    val columnTypeSql = columnType.sql\n    val (exprTypeString, columnTypeString) = if (exprTypeSql == columnTypeSql) {\n      // We need to add some more information for the error message to be useful.\n      (exprType.json, columnType.json)\n    } else {\n      (exprTypeSql, columnTypeSql)\n    }\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_GENERATED_COLUMNS_EXPR_TYPE_MISMATCH\",\n      messageParameters = Array(column, exprTypeString, columnTypeString)\n    )\n  }\n\n  def generatedColumnsDataTypeMismatch(\n      columnPath: Seq[String],\n      columnType: DataType,\n      dataType: DataType,\n      generatedColumns: Map[String, String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n      messageParameters = Array(\n        SchemaUtils.prettyFieldName(columnPath),\n        columnType.sql,\n        dataType.sql,\n        generatedColumns.mkString(\"\\n\"))\n    )\n  }\n\n  def constraintDataTypeMismatch(\n      columnPath: Seq[String],\n      columnType: DataType,\n      dataType: DataType,\n      constraints: Map[String, String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\",\n      messageParameters = Array(\n        SchemaUtils.prettyFieldName(columnPath),\n        columnType.sql,\n        dataType.sql,\n        constraints.mkString(\"\\n\"))\n    )\n  }\n\n  def expressionsNotFoundInGeneratedColumn(column: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_EXPRESSIONS_NOT_FOUND_IN_GENERATED_COLUMN\",\n      messageParameters = Array(column)\n    )\n  }\n\n  def cannotChangeDataType(msg: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_CHANGE_DATA_TYPE\",\n      messageParameters = Array(msg)\n    )\n  }\n\n  def ambiguousDataTypeChange(column: String, from: StructType, to: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_AMBIGUOUS_DATA_TYPE_CHANGE\",\n      messageParameters = Array(column, from.toDDL, to.toDDL)\n    )\n  }\n\n  def unsupportedDataTypes(\n      unsupportedDataType: UnsupportedDataTypeInfo,\n      moreUnsupportedDataTypes: UnsupportedDataTypeInfo*): Throwable = {\n    val prettyMessage = (unsupportedDataType +: moreUnsupportedDataTypes)\n      .map(dt => s\"${dt.column}: ${dt.dataType}\")\n      .mkString(\"[\", \", \", \"]\")\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_DATA_TYPES\",\n      messageParameters = Array(prettyMessage, DeltaSQLConf.DELTA_SCHEMA_TYPE_CHECK.key)\n    )\n  }\n\n  def tableAlreadyExists(table: CatalogTable): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TABLE_ALREADY_EXISTS\",\n      messageParameters = Array(s\"${table.identifier.quotedString}\")\n    )\n  }\n\n  def tableLocationMismatch(table: CatalogTable, existingTable: CatalogTable): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_TABLE_LOCATION_MISMATCH\",\n      messageParameters = Array(\n        s\"${table.identifier.quotedString}\",\n        s\"`${existingTable.location}`\",\n        s\"`${table.location}`\")\n    )\n  }\n\n  def nonSinglePartNamespaceForCatalog(ident: String): Throwable = {\n    new DeltaNoSuchTableException(\n      errorClass = \"DELTA_NON_SINGLE_PART_NAMESPACE_FOR_CATALOG\",\n      errorMessageParameters = Array(ident))\n  }\n\n  def indexLargerThanStruct(pos: Int, column: StructField, len: Int): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INDEX_LARGER_THAN_STRUCT\",\n      messageParameters = Array(s\"$pos\", s\"$column\", s\"$len\")\n    )\n  }\n\n  def indexLargerOrEqualThanStruct(pos: Int, len: Int): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INDEX_LARGER_OR_EQUAL_THAN_STRUCT\",\n      messageParameters = Array(s\"$pos\", s\"$len\")\n    )\n  }\n\n  def invalidV1TableCall(callVersion: String, tableVersion: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_INVALID_V1_TABLE_CALL\",\n      messageParameters = Array(callVersion, tableVersion)\n    )\n  }\n\n  def cannotGenerateUpdateExpressions(): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CANNOT_GENERATE_UPDATE_EXPRESSIONS\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def unrecognizedInvariant(): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNRECOGNIZED_INVARIANT\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def unrecognizedColumnChange(otherClass: String) : Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNRECOGNIZED_COLUMN_CHANGE\",\n      messageParameters = Array(otherClass)\n    )\n  }\n\n  def notNullColumnNotFoundInStruct(struct: String): Throwable = {\n    new DeltaIndexOutOfBoundsException(\n      errorClass = \"DELTA_NOT_NULL_COLUMN_NOT_FOUND_IN_STRUCT\",\n      messageParameters = Array(struct)\n    )\n  }\n\n  def unSupportedInvariantNonStructType: Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNSUPPORTED_INVARIANT_NON_STRUCT\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def cannotResolveColumn(fieldName: String, schema: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CANNOT_RESOLVE_COLUMN\",\n      messageParameters = Array(fieldName, schema.treeString)\n    )\n  }\n\n  def unsupportedTruncateSampleTables: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_TRUNCATE_SAMPLE_TABLES\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def unrecognizedFileAction(otherAction: String, otherClass: String) : Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_UNRECOGNIZED_FILE_ACTION\",\n      messageParameters = Array(otherAction, otherClass)\n    )\n  }\n\n  def operationOnTempViewWithGenerateColsNotSupported(op: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_OPERATION_ON_TEMP_VIEW_WITH_GENERATED_COLS_NOT_SUPPORTED\",\n      messageParameters = Array(op, op))\n  }\n\n  def cannotModifyTableProperty(prop: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\",\n      messageParameters = Array(prop))\n  }\n\n  /**\n   * We have plans to support more column mapping modes, but they are not implemented yet,\n   * so we error for now to be forward compatible with tables created in the future.\n   */\n  def unsupportedColumnMappingMode(mode: String): Throwable =\n    new ColumnMappingUnsupportedException(s\"The column mapping mode `$mode` is \" +\n      s\"not supported for this Delta version. Please upgrade if you want to use this mode.\")\n\n  def missingColumnId(mode: DeltaColumnMappingMode, field: String): Throwable = {\n    ColumnMappingException(s\"Missing column ID in column mapping mode `${mode.name}`\" +\n      s\" in the field: $field\", mode)\n  }\n\n  def missingPhysicalName(mode: DeltaColumnMappingMode, field: String): Throwable =\n    ColumnMappingException(s\"Missing physical name in column mapping mode `${mode.name}`\" +\n      s\" in the field: $field\", mode)\n\n  def duplicatedColumnId(\n      mode: DeltaColumnMappingMode,\n      id: Long,\n      schema: StructType): Throwable = {\n    ColumnMappingException(\n      s\"Found duplicated column id `$id` in column mapping mode `${mode.name}` \\n\" +\n      s\"schema: \\n ${schema.prettyJson}\", mode\n    )\n  }\n\n  def duplicatedPhysicalName(\n      mode: DeltaColumnMappingMode,\n      physicalName: String,\n      schema: StructType): Throwable = {\n    ColumnMappingException(\n      s\"Found duplicated physical name `$physicalName` in column mapping mode `${mode.name}` \\n\\t\" +\n      s\"schema: \\n ${schema.prettyJson}\", mode\n    )\n  }\n\n  def maxColumnIdNotSet: Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_MAPPING_MAX_COLUMN_ID_NOT_SET\",\n      messageParameters = Array(DeltaConfigs.COLUMN_MAPPING_MAX_ID.key)\n    )\n  }\n\n  def maxColumnIdNotSetCorrectly(tableMax: Long, fieldMax: Long): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_MAPPING_MAX_COLUMN_ID_NOT_SET_CORRECTLY\",\n      messageParameters = Array(\n        DeltaConfigs.COLUMN_MAPPING_MAX_ID.key, tableMax.toString, fieldMax.toString)\n    )\n  }\n\n  def changeColumnMappingModeNotSupported(oldMode: String, newMode: String): Throwable = {\n    new DeltaColumnMappingUnsupportedException(\n      errorClass = \"DELTA_UNSUPPORTED_COLUMN_MAPPING_MODE_CHANGE\",\n      messageParameters = Array(oldMode, newMode))\n  }\n\n  def enablingColumnMappingDisallowedWhenColumnMappingMetadataAlreadyExists(): Throwable = {\n    new DeltaColumnMappingUnsupportedException(\n      errorClass =\n        \"DELTA_ENABLING_COLUMN_MAPPING_DISALLOWED_WHEN_COLUMN_MAPPING_METADATA_ALREADY_EXISTS\")\n  }\n\n  def generateManifestWithColumnMappingNotSupported: Throwable = {\n    new DeltaColumnMappingUnsupportedException(\n      errorClass = \"DELTA_UNSUPPORTED_MANIFEST_GENERATION_WITH_COLUMN_MAPPING\")\n  }\n\n  def convertToDeltaNoPartitionFound(tableName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONVERSION_NO_PARTITION_FOUND\",\n      messageParameters = Array(tableName)\n    )\n  }\n\n  def convertToDeltaWithColumnMappingNotSupported(mode: DeltaColumnMappingMode): Throwable = {\n    new DeltaColumnMappingUnsupportedException(\n      errorClass = \"DELTA_CONVERSION_UNSUPPORTED_COLUMN_MAPPING\",\n      messageParameters = Array(\n        DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey,\n        mode.name))\n  }\n\n  protected def columnMappingAdviceMessage(\n      requiredProtocol: Protocol = ColumnMappingTableFeature.minProtocolVersion): String = {\n    val readerVersion = requiredProtocol.minReaderVersion\n    val writerVersion = requiredProtocol.minWriterVersion\n    s\"\"\"\n       |Please enable Column Mapping on your Delta table with mapping mode 'name'.\n       |You can use one of the following commands.\n       |\n       |ALTER TABLE table_name SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\n       |\n       |Note, if your table is not on the required protocol version it will be upgraded.\n       |Column mapping requires at least protocol ($readerVersion, $writerVersion)\n       |\"\"\".stripMargin\n  }\n\n  def columnRenameNotSupported: Throwable = {\n    val adviceMsg = columnMappingAdviceMessage()\n    new DeltaAnalysisException(\"DELTA_UNSUPPORTED_RENAME_COLUMN\", Array(adviceMsg))\n  }\n\n  def dropColumnNotSupported(suggestUpgrade: Boolean): Throwable = {\n    val adviceMsg = if (suggestUpgrade) columnMappingAdviceMessage() else \"\"\n    new DeltaAnalysisException(\"DELTA_UNSUPPORTED_DROP_COLUMN\", Array(adviceMsg))\n  }\n\n  def dropNestedColumnsFromNonStructTypeException(struct : DataType) : Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_DROP_NESTED_COLUMN_FROM_NON_STRUCT_TYPE\",\n      messageParameters = Array(s\"$struct\")\n    )\n  }\n\n  def dropPartitionColumnNotSupported(droppingPartCols: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\"DELTA_UNSUPPORTED_DROP_PARTITION_COLUMN\",\n      Array(droppingPartCols.mkString(\",\")))\n  }\n\n  def schemaChangeDuringMappingModeChangeNotSupported(\n      oldSchema: StructType,\n      newSchema: StructType): Throwable =\n    new DeltaColumnMappingUnsupportedException(\n      errorClass = \"DELTA_UNSUPPORTED_COLUMN_MAPPING_SCHEMA_CHANGE\",\n      messageParameters = Array(\n        formatSchema(oldSchema),\n        formatSchema(newSchema)))\n\n  def foundInvalidCharsInColumnNames(invalidColumnNames: Seq[String]): Throwable =\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAMES\",\n      messageParameters = Array(invalidColumnNames.mkString(\", \")))\n\n  def foundInvalidColumnNamesWhenRemovingColumnMapping(columnNames: Seq[String])\n    : Throwable =\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_COLUMN_NAMES_WHEN_REMOVING_COLUMN_MAPPING\",\n      messageParameters = Array(columnNames.mkString(\", \")))\n\n  def foundViolatingConstraintsForColumnChange(\n      columnName: String,\n      constraints: Map[String, String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\",\n      messageParameters = Array(columnName, constraints.mkString(\"\\n\"))\n    )\n  }\n\n  def foundViolatingGeneratedColumnsForColumnChange(\n      columnName: String,\n      generatedColumns: Map[String, String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_GENERATED_COLUMNS_DEPENDENT_COLUMN_CHANGE\",\n      messageParameters = Array(columnName, generatedColumns.mkString(\"\\n\"))\n    )\n  }\n\n  def missingColumnsInInsertInto(column: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INSERT_COLUMN_MISMATCH\",\n      messageParameters = Array(column))\n  }\n\n  def schemaNotConsistentWithTarget(tableSchema: String, targetAttr: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_SCHEMA_NOT_CONSISTENT_WITH_TARGET\",\n      messageParameters = Array(tableSchema, targetAttr)\n    )\n  }\n\n  def logStoreConfConflicts(classConf: Seq[(String, String)],\n      schemeConf: Seq[(String, String)]): Throwable = {\n    val classConfStr = classConf.map(_._1).mkString(\", \")\n    val schemeConfStr = schemeConf.map(_._1).mkString(\", \")\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INVALID_LOGSTORE_CONF\",\n      messageParameters = Array(classConfStr, schemeConfStr)\n    )\n  }\n\n  def inconsistentLogStoreConfs(setKeys: Seq[(String, String)]): Throwable = {\n    val setKeyStr = setKeys.map(_.productIterator.mkString(\" = \")).mkString(\", \")\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INCONSISTENT_LOGSTORE_CONFS\",\n      messageParameters = Array(setKeyStr)\n    )\n  }\n\n  def ambiguousPathsInCreateTableException(identifier: String, location: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_AMBIGUOUS_PATHS_IN_CREATE_TABLE\",\n      messageParameters = Array(identifier, location,\n        DeltaSQLConf.DELTA_LEGACY_ALLOW_AMBIGUOUS_PATHS.key))\n  }\n\n  def concurrentWriteException(\n      conflictingCommit: Option[CommitInfo]): io.delta.exceptions.ConcurrentWriteException = {\n    new io.delta.exceptions.ConcurrentWriteException(\n      Array(\n        conflictingCommit.map(ci => s\"\\nConflicting commit: ${JsonUtils.toJson(ci)}\").getOrElse(\"\"),\n        DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\"))\n    )\n  }\n\n  def metadataChangedException(\n      conflictingCommit: Option[CommitInfo]): io.delta.exceptions.MetadataChangedException = {\n    new io.delta.exceptions.MetadataChangedException(\n      Array(\n        conflictingCommit.map(ci => s\"\\nConflicting commit: ${JsonUtils.toJson(ci)}\").getOrElse(\"\"),\n        DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\"))\n    )\n  }\n\n  def protocolPropNotIntException(key: String, value: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_PROTOCOL_PROPERTY_NOT_INT\",\n      Array(key, value))\n  }\n\n  def protocolChangedException(\n      conflictingCommit: Option[CommitInfo]): io.delta.exceptions.ProtocolChangedException = {\n    val additionalInfo = conflictingCommit.map { v =>\n      if (v.version.getOrElse(-1) == 0) {\n        \"This happens when multiple writers are writing to an empty directory. \" +\n          \"Creating the table ahead of time will avoid this conflict. \"\n      } else {\n        \"\"\n      }\n    }.getOrElse(\"\")\n    new io.delta.exceptions.ProtocolChangedException(\n      Array(\n        additionalInfo,\n        conflictingCommit.map(ci => s\"\\nConflicting commit: ${JsonUtils.toJson(ci)}\").getOrElse(\"\"),\n        DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\")\n      )\n    )\n  }\n\n  def unsupportedReaderTableFeaturesInTableException(\n    tableNameOrPath: String,\n    unsupported: Iterable[String]): DeltaUnsupportedTableFeatureException = {\n    new DeltaUnsupportedTableFeatureException(\n      errorClass = \"DELTA_UNSUPPORTED_FEATURES_FOR_READ\",\n      tableNameOrPath = tableNameOrPath,\n      unsupported = unsupported)\n  }\n\n  def unsupportedWriterTableFeaturesInTableException(\n    tableNameOrPath: String,\n    unsupported: Iterable[String]): DeltaUnsupportedTableFeatureException = {\n    new DeltaUnsupportedTableFeatureException(\n      errorClass = \"DELTA_UNSUPPORTED_FEATURES_FOR_WRITE\",\n      tableNameOrPath = tableNameOrPath,\n      unsupported = unsupported)\n  }\n\n  def unsupportedTableFeatureConfigsException(\n      configs: Iterable[String]): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_UNSUPPORTED_FEATURES_IN_CONFIG\",\n      messageParameters = Array(configs.mkString(\", \")))\n  }\n\n  def unsupportedTableFeatureStatusException(\n      feature: String,\n      status: String): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_UNSUPPORTED_FEATURE_STATUS\",\n      messageParameters = Array(feature, status))\n  }\n\n  def tableFeatureReadRequiresWriteException(\n      requiredWriterVersion: Int): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_READ_FEATURE_PROTOCOL_REQUIRES_WRITE\",\n      messageParameters = Array(\n        requiredWriterVersion.toString,\n        generateDocsLinkOption(SparkSession.active, \"/index.html\").getOrElse(\"-\")))\n  }\n\n  def tableFeatureRequiresHigherReaderProtocolVersion(\n      feature: String,\n      currentVersion: Int,\n      requiredVersion: Int): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_REQUIRES_HIGHER_READER_VERSION\",\n      messageParameters = Array(\n        feature,\n        currentVersion.toString,\n        requiredVersion.toString,\n        generateDocsLinkOption(SparkSession.active, \"/index.html\").getOrElse(\"-\")))\n  }\n\n  def tableFeatureRequiresHigherWriterProtocolVersion(\n      feature: String,\n      currentVersion: Int,\n      requiredVersion: Int): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_REQUIRES_HIGHER_WRITER_VERSION\",\n      messageParameters = Array(\n        feature,\n        currentVersion.toString,\n        requiredVersion.toString,\n        generateDocsLinkOption(SparkSession.active, \"/index.html\").getOrElse(\"-\")))\n  }\n\n  def tableFeatureMismatchException(features: Iterable[String]): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURES_PROTOCOL_METADATA_MISMATCH\",\n      messageParameters = Array(features.mkString(\", \")))\n  }\n\n  def tableFeaturesRequireManualEnablementException(\n      unsupportedFeatures: Iterable[TableFeature],\n      supportedFeatures: Iterable[TableFeature]): Throwable = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\",\n      messageParameters = Array(\n        unsupportedFeatures.map(_.name).toSeq.sorted.mkString(\", \"),\n        supportedFeatures.map(_.name).toSeq.sorted.mkString(\", \")))\n  }\n\n  case class LogRetentionConfig(key: String, value: String, truncateHistoryRetention: String)\n\n  private def logRetentionConfig(metadata: Metadata): LogRetentionConfig = {\n    val logRetention = DeltaConfigs.LOG_RETENTION\n    val truncateHistoryRetention = DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION\n    LogRetentionConfig(\n      logRetention.key,\n      logRetention.fromMetaData(metadata).toString,\n      truncateHistoryRetention.fromMetaData(metadata).toString)\n  }\n\n  def dropTableFeatureHistoricalVersionsExist(\n      feature: String,\n      metadata: Metadata): DeltaTableFeatureException = {\n    val config = logRetentionConfig(metadata)\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n      messageParameters = Array(feature, config.key, config.value, config.truncateHistoryRetention)\n    )\n  }\n\n  def dropTableFeatureWaitForRetentionPeriod(\n      feature: String,\n      metadata: Metadata): DeltaTableFeatureException = {\n    val config = logRetentionConfig(metadata)\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n      messageParameters = Array(feature, config.key, config.value, config.truncateHistoryRetention)\n    )\n  }\n\n  def dropCheckpointProtectionWaitForRetentionPeriod(\n      metadata: Metadata): DeltaTableFeatureException = {\n    val config = logRetentionConfig(metadata)\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_CHECKPOINT_PROTECTION_WAIT_FOR_RETENTION_PERIOD\",\n      messageParameters = Array(config.truncateHistoryRetention))\n  }\n\n  def tableFeatureDropHistoryTruncationNotAllowed(): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\",\n      messageParameters = Array.empty)\n  }\n\n  def dropTableFeatureNonRemovableFeature(feature: String): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_NONREMOVABLE_FEATURE\",\n      messageParameters = Array(feature))\n  }\n\n  def dropTableFeatureFailedBecauseOfDependentFeatures(\n      feature: String,\n      dependentFeatures: Seq[String]): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\",\n      messageParameters = Array(feature, dependentFeatures.mkString(\", \"), feature))\n  }\n\n  def dropTableFeatureConflictRevalidationFailed(\n      conflictingCommit: Option[CommitInfo] = None): DeltaTableFeatureException = {\n    val concurrentCommit = DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf, \"\", conflictingCommit)\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_CONFLICT_REVALIDATION_FAIL\",\n      messageParameters = Array(concurrentCommit))\n  }\n\n  def dropTableFeatureFeatureNotSupportedByClient(\n      feature: String): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_UNSUPPORTED_CLIENT_FEATURE\",\n      messageParameters = Array(feature))\n  }\n\n  def dropTableFeatureFeatureNotSupportedByProtocol(\n      feature: String): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\",\n      messageParameters = Array(feature))\n  }\n\n  def dropTableFeatureFeatureIsADeltaProperty(feature: String): DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_FEATURE_IS_DELTA_PROPERTY\",\n      messageParameters = Array(feature))\n  }\n\n  def dropTableFeatureNotDeltaTableException(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ONLY_OPERATION\",\n      messageParameters = Array(\"ALTER TABLE DROP FEATURE\")\n    )\n  }\n\n  def dropTableFeatureCheckpointFailedException(featureName: String): Throwable = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_DROP_CHECKPOINT_FAILED\",\n      messageParameters = Array(featureName))\n  }\n\n  def canOnlyDropCheckpointProtectionWithHistoryTruncationException: DeltaTableFeatureException = {\n    new DeltaTableFeatureException(\n      errorClass = \"DELTA_FEATURE_CAN_ONLY_DROP_CHECKPOINT_PROTECTION_WITH_HISTORY_TRUNCATION\",\n      messageParameters = Array.empty)\n  }\n\n  def concurrentAppendException(\n      commitInfo: Option[CommitInfo],\n      tableName: String,\n      version: Long,\n      partitionOpt: Option[String]): io.delta.exceptions.ConcurrentAppendException = {\n    val operation = commitInfo.map(_.operation).getOrElse(\"TRANSACTION\")\n    val docLink = DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\")\n    val subClass = if (partitionOpt.nonEmpty) {\n      \"WITH_PARTITION_HINT\"\n    } else {\n      \"WITHOUT_HINT\"\n    }\n    val messageParameters = subClass match {\n      case \"WITH_PARTITION_HINT\" =>\n        Array(operation, tableName, version.toString, partitionOpt.getOrElse(\"\"), docLink)\n      case _ =>\n        Array(operation, tableName, version.toString, docLink)\n    }\n    io.delta.exceptions.ConcurrentAppendException(subClass, messageParameters)\n  }\n\n  def concurrentDeleteReadException(\n      commitInfo: Option[CommitInfo],\n      tableName: String,\n      version: Long,\n      partitionOpt: Option[String]): io.delta.exceptions.ConcurrentDeleteReadException = {\n    val operation = commitInfo.map(_.operation).getOrElse(\"TRANSACTION\")\n    val docLink = DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\")\n    val subClass = if (partitionOpt.nonEmpty) {\n      \"WITH_PARTITION_HINT\"\n    } else {\n      \"WITHOUT_HINT\"\n    }\n    val messageParameters = subClass match {\n      case \"WITH_PARTITION_HINT\" =>\n        Array(operation, tableName, version.toString, partitionOpt.getOrElse(\"\"), docLink)\n      case _ =>\n        Array(operation, tableName, version.toString, docLink)\n    }\n    io.delta.exceptions.ConcurrentDeleteReadException(subClass, messageParameters)\n  }\n\n  def concurrentDeleteDeleteException(\n      commitInfo: Option[CommitInfo],\n      tableName: String,\n      version: Long,\n      partitionOpt: Option[String]): io.delta.exceptions.ConcurrentDeleteDeleteException = {\n    val operation = commitInfo.map(_.operation).getOrElse(\"TRANSACTION\")\n    val docLink = DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\")\n    val subClass = if (partitionOpt.nonEmpty) {\n      \"WITH_PARTITION_HINT\"\n    } else {\n      \"WITHOUT_HINT\"\n    }\n    val messageParameters = subClass match {\n      case \"WITH_PARTITION_HINT\" =>\n        Array(operation, tableName, version.toString, partitionOpt.getOrElse(\"\"), docLink)\n      case _ =>\n        Array(operation, tableName, version.toString, docLink)\n    }\n    io.delta.exceptions.ConcurrentDeleteDeleteException(subClass, messageParameters)\n  }\n\n  def concurrentTransactionException(\n      conflictingCommit: Option[CommitInfo]): io.delta.exceptions.ConcurrentTransactionException = {\n    new io.delta.exceptions.ConcurrentTransactionException(\n      Array(\n        conflictingCommit.map(ci => s\"\\nConflicting commit: ${JsonUtils.toJson(ci)}\").getOrElse(\"\"),\n        DeltaErrors.generateDocsLink(SparkEnv.get.conf, \"/concurrency-control.html\"))\n    )\n  }\n\n  def restoreMissedDataFilesError(missedFiles: Array[String], version: Long): Throwable =\n    new IllegalArgumentException(\n      s\"\"\"Not all files from version $version are available in file system.\n         | Missed files (top 100 files): ${missedFiles.mkString(\",\")}.\n         | Please use more recent version or timestamp for restoring.\n         | To disable check update option ${SQLConf.IGNORE_MISSING_FILES.key}\"\"\"\n        .stripMargin\n    )\n\n  def unexpectedAlias(alias : String) : Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_UNEXPECTED_ALIAS\",\n      messageParameters = Array(alias)\n    )\n  }\n\n  def unexpectedProject(project : String) : Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_UNEXPECTED_PROJECT\",\n      messageParameters = Array(project)\n    )\n  }\n\n  def unexpectedAttributeReference(ref: String): Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_UNEXPECTED_ATTRIBUTE_REFERENCE\",\n      messageParameters = Array(ref))\n  }\n\n  def unsetNonExistentProperty(key: String, table: String): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_UNSET_NON_EXISTENT_PROPERTY\", Array(key, table))\n  }\n\n  def identityColumnWithGenerationExpression(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_WITH_GENERATED_EXPRESSION\", Array.empty)\n  }\n\n  def identityColumnIllegalStep(): Throwable = {\n    new DeltaAnalysisException(errorClass = \"DELTA_IDENTITY_COLUMNS_ILLEGAL_STEP\", Array.empty)\n  }\n\n  def identityColumnDataTypeNotSupported(unsupportedType: DataType): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_UNSUPPORTED_DATA_TYPE\",\n      messageParameters = Array(unsupportedType.typeName)\n    )\n  }\n\n  def identityColumnAlterNonIdentityColumnError(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_ALTER_NON_IDENTITY_COLUMN\",\n      messageParameters = Array.empty)\n  }\n\n  def identityColumnAlterNonDeltaFormatError(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_ALTER_NON_DELTA_FORMAT\",\n      messageParameters = Array.empty)\n  }\n\n  def identityColumnInconsistentMetadata(\n      colName: String,\n      hasStart: Boolean,\n      hasStep: Boolean,\n      hasInsert: Boolean): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0006\",\n      messageParameters = Array(colName, s\"$hasStart\", s\"$hasStep\", s\"$hasInsert\")\n    )\n  }\n\n  def identityColumnExplicitInsertNotSupported(colName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_EXPLICIT_INSERT_NOT_SUPPORTED\",\n      messageParameters = Array(colName))\n  }\n\n  def identityColumnAlterColumnNotSupported(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_ALTER_COLUMN_NOT_SUPPORTED\",\n      messageParameters = Array.empty)\n  }\n\n  def identityColumnReplaceColumnsNotSupported(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_REPLACE_COLUMN_NOT_SUPPORTED\",\n      messageParameters = Array.empty)\n  }\n\n  def identityColumnPartitionNotSupported(colName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_PARTITION_NOT_SUPPORTED\",\n      messageParameters = Array(colName))\n  }\n\n  def identityColumnUpdateNotSupported(colName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_IDENTITY_COLUMNS_UPDATE_NOT_SUPPORTED\",\n      messageParameters = Array(colName))\n  }\n\n  def activeSparkSessionNotFound(): Throwable = {\n    new DeltaIllegalArgumentException(errorClass = \"DELTA_ACTIVE_SPARK_SESSION_NOT_FOUND\")\n  }\n\n  def sparkTaskThreadNotFound: Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_SPARK_THREAD_NOT_FOUND\")\n  }\n\n  def iteratorAlreadyClosed(): Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_ITERATOR_ALREADY_CLOSED\")\n  }\n\n  def activeTransactionAlreadySet(): Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_ACTIVE_TRANSACTION_ALREADY_SET\")\n  }\n\n  def deltaStatsCollectionColumnNotFound(statsType: String, columnPath: String): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_STATS_COLLECTION_COLUMN_NOT_FOUND\",\n      messageParameters = Array(statsType, columnPath)\n    )\n  }\n\n  def convertToDeltaRowTrackingEnabledWithoutStatsCollection: Throwable = {\n    val statisticsCollectionPropertyKey = DeltaSQLConf.DELTA_COLLECT_STATS.key\n    val rowTrackingTableFeatureDefaultKey =\n      TableFeatureProtocolUtils.defaultPropertyKey(RowTrackingFeature)\n    val rowTrackingDefaultPropertyKey = DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_CONVERT_TO_DELTA_ROW_TRACKING_WITHOUT_STATS\",\n      messageParameters = Array(\n        statisticsCollectionPropertyKey,\n        rowTrackingTableFeatureDefaultKey,\n        rowTrackingDefaultPropertyKey))\n  }\n\n  def rowTrackingBackfillRunningConcurrentlyWithUnbackfill(): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_ROW_TRACKING_BACKFILL_RUNNING_CONCURRENTLY_WITH_UNBACKFILL\")\n  }\n\n  def rowTrackingIllegalPropertyCombination(): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_ROW_TRACKING_ILLEGAL_PROPERTY_COMBINATION\",\n      messageParameters = Array(\n        DeltaConfigs.ROW_TRACKING_ENABLED.key,\n        DeltaConfigs.ROW_TRACKING_SUSPENDED.key))\n  }\n\n  /** This is a method only used for testing Py4J exception handling. */\n  def throwDeltaIllegalArgumentException(): Throwable = {\n    new DeltaIllegalArgumentException(errorClass = \"DELTA_UNRECOGNIZED_INVARIANT\")\n  }\n\n  def invalidSourceVersion(version: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_INVALID_SOURCE_VERSION\",\n      messageParameters = Array(version)\n    )\n  }\n\n  def invalidSourceOffsetFormat(): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_INVALID_SOURCE_OFFSET_FORMAT\"\n    )\n  }\n\n  def invalidCommittedVersion(attemptVersion: Long, currentVersion: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_INVALID_COMMITTED_VERSION\",\n      messageParameters = Array(attemptVersion.toString, currentVersion.toString)\n    )\n  }\n\n  def nonPartitionColumnReference(colName: String, partitionColumns: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_NON_PARTITION_COLUMN_REFERENCE\",\n      messageParameters = Array(colName, partitionColumns.mkString(\", \"))\n    )\n  }\n\n  def missingColumn(attr: Attribute, targetAttrs: Seq[Attribute]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MISSING_COLUMN\",\n      messageParameters = Array(attr.name, targetAttrs.map(_.name).mkString(\", \"))\n    )\n  }\n\n  def missingPartitionColumn(col: String, schemaCatalog: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MISSING_PARTITION_COLUMN\",\n      messageParameters = Array(col, schemaCatalog)\n    )\n  }\n\n  def noNewAttributeId(oldAttr: AttributeReference): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_NO_NEW_ATTRIBUTE_ID\",\n      messageParameters = Array(oldAttr.qualifiedName)\n    )\n  }\n\n  def nonGeneratedColumnMissingUpdateExpression(columnName: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_NON_GENERATED_COLUMN_MISSING_UPDATE_EXPR\",\n      messageParameters = Array(columnName)\n    )\n  }\n\n  def failedInferSchema: Throwable = {\n    new DeltaRuntimeException(\"DELTA_FAILED_INFER_SCHEMA\")\n  }\n\n  def failedReadFileFooter(file: String, e: Throwable): Throwable = {\n    new DeltaIOException(\n      errorClass = \"DELTA_FAILED_READ_FILE_FOOTER\",\n      messageParameters = Array(file),\n      cause = e\n    )\n  }\n\n  def failedScanWithHistoricalVersion(historicalVersion: Long): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_FAILED_SCAN_WITH_HISTORICAL_VERSION\",\n      messageParameters = Array(historicalVersion.toString)\n    )\n  }\n\n  def failedRecognizePredicate(predicate: String, cause: Throwable): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_FAILED_RECOGNIZE_PREDICATE\", messageParameters = Array(predicate),\n      cause = Some(cause)\n    )\n  }\n\n  def failedFindAttributeInOutputColumns(newAttrName: String, targetColNames: String): Throwable =\n  {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_FAILED_FIND_ATTRIBUTE_IN_OUTPUT_COLUMNS\",\n      messageParameters = Array(newAttrName, targetColNames)\n    )\n  }\n\n  def failedFindPartitionColumnInOutputPlan(partitionColumn: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_FAILED_FIND_PARTITION_COLUMN_IN_OUTPUT_PLAN\",\n      messageParameters = Array(partitionColumn))\n  }\n\n  def deltaTableFoundInExecutor(): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_TABLE_FOUND_IN_EXECUTOR\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def variantShreddingUnsupported(): Throwable = {\n    new DeltaSparkException(\n      errorClass = \"DELTA_SHREDDING_TABLE_PROPERTY_DISABLED\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def unsupportSubqueryInPartitionPredicates(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_SUBQUERY_IN_PARTITION_PREDICATES\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def fileAlreadyExists(file: String): Throwable = {\n    new DeltaFileAlreadyExistsException(\n      errorClass = \"DELTA_FILE_ALREADY_EXISTS\",\n      messageParameters = Array(file)\n    )\n  }\n\n  def replaceWhereUsedWithDynamicPartitionOverwrite(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_REPLACE_WHERE_WITH_DYNAMIC_PARTITION_OVERWRITE\"\n    )\n  }\n\n  def overwriteSchemaUsedWithDynamicPartitionOverwrite(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_OVERWRITE_SCHEMA_WITH_DYNAMIC_PARTITION_OVERWRITE\"\n    )\n  }\n\n  def replaceWhereUsedInOverwrite(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_REPLACE_WHERE_IN_OVERWRITE\", messageParameters = Array.empty\n    )\n  }\n\n  def deltaDynamicPartitionOverwriteDisabled(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_DYNAMIC_PARTITION_OVERWRITE_DISABLED\"\n    )\n  }\n\n  def incorrectArrayAccessByName(\n      rightName: String,\n      wrongName: String,\n      schema: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_INCORRECT_ARRAY_ACCESS_BY_NAME\",\n      messageParameters = Array(\n        rightName,\n        wrongName,\n        dataTypeToString(schema)\n      )\n    )\n  }\n\n  def columnPathNotNested(\n      columnPath: String,\n      other: DataType,\n      column: Seq[String],\n      schema: DataType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_PATH_NOT_NESTED\",\n      messageParameters = Array(\n        s\"$columnPath\",\n        s\"$other\",\n        s\"${SchemaUtils.prettyFieldName(column)}\",\n        dataTypeToString(schema)\n      )\n    )\n  }\n\n  def showPartitionInNotPartitionedTable(tableName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_SHOW_PARTITION_IN_NON_PARTITIONED_TABLE\",\n      messageParameters = Array(tableName)\n    )\n  }\n\n  def showPartitionInNotPartitionedColumn(badColumns: Set[String]): Throwable = {\n    val badCols = badColumns.mkString(\"[\", \", \", \"]\")\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_SHOW_PARTITION_IN_NON_PARTITIONED_COLUMN\",\n      messageParameters = Array(badCols)\n    )\n  }\n\n  def duplicateColumnOnInsert(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_DUPLICATE_COLUMNS_ON_INSERT\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def timeTravelInvalidBeginValue(timeTravelKey: String, cause: Throwable): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_TIME_TRAVEL_INVALID_BEGIN_VALUE\",\n      messageParameters = Array(timeTravelKey),\n      cause = cause\n    )\n  }\n\n  def removeFileCDCMissingExtendedMetadata(fileName: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_REMOVE_FILE_CDC_MISSING_EXTENDED_METADATA\",\n      messageParameters = Array(fileName)\n    )\n  }\n\n  def failRelativizePath(pathName: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_FAIL_RELATIVIZE_PATH\", messageParameters = Array(\n        pathName,\n        DeltaSQLConf.DELTA_VACUUM_RELATIVIZE_IGNORE_ERROR.key)\n    )\n  }\n\n  def invalidFormatFromSourceVersion(wrongVersion: Long, expectedVersion: Integer): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_INVALID_FORMAT_FROM_SOURCE_VERSION\",\n      messageParameters = Array(expectedVersion.toString, wrongVersion.toString)\n    )\n  }\n\n  def createTableWithNonEmptyLocation(tableId: String, tableLocation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_WITH_NON_EMPTY_LOCATION\",\n      messageParameters = Array(tableId, tableLocation)\n    )\n  }\n\n  def maxArraySizeExceeded(): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_MAX_ARRAY_SIZE_EXCEEDED\", messageParameters = Array.empty\n    )\n  }\n\n  def replaceWhereWithFilterDataChangeUnset(dataFilters: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_REPLACE_WHERE_WITH_FILTER_DATA_CHANGE_UNSET\",\n      messageParameters = Array(dataFilters)\n    )\n  }\n\n  def blockColumnMappingAndCdcOperation(op: DeltaOperations.Operation): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_BLOCK_COLUMN_MAPPING_AND_CDC_OPERATION\",\n      messageParameters = Array(op.name)\n    )\n  }\n\n  def missingDeltaStorageJar(e: NoClassDefFoundError): Throwable = {\n    // scalastyle:off line.size.limit\n    new NoClassDefFoundError(\n      s\"\"\"${e.getMessage}\n         |Please ensure that the delta-storage dependency is included.\n         |\n         |If using Python, please ensure you call `configure_spark_with_delta_pip` or use\n         |`--packages io.delta:delta-spark_<scala-version>:<delta-lake-version>`.\n         |See https://docs.delta.io/latest/quick-start.html#python.\n         |\n         |More information about this dependency and how to include it can be found here:\n         |https://docs.delta.io/latest/porting.html#delta-lake-1-1-or-below-to-delta-lake-1-2-or-above.\n         |\"\"\".stripMargin)\n    // scalastyle:on line.size.limit\n  }\n\n  /**\n   * If `isSchemaChange` is false, this means the `incompatVersion` actually refers to a data schema\n   * instead of a schema change. This happens when we could not find any read-incompatible schema\n   * changes within the querying range, but the read schema is still NOT compatible with the data\n   * files being queried, which could happen if user falls back to `legacy` mode and read past data\n   * using some diverged latest schema or time-travelled schema. In this uncommon case, we should\n   * tell the user to try setting it back to endVersion, OR ask us to give them the flag to force\n   * unblock.\n   */\n  def blockBatchCdfReadWithIncompatibleSchemaChange(\n      start: Long,\n      end: Long,\n      readSchema: StructType,\n      readVersion: Long,\n      incompatVersion: Long,\n      isSchemaChange: Boolean = true): Throwable = {\n    new DeltaUnsupportedOperationException(\n      if (isSchemaChange) {\n        \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_SCHEMA_CHANGE\"\n      } else {\n        \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_DATA_SCHEMA\"\n      },\n      messageParameters = Array(\n        start.toString, end.toString,\n        readSchema.json, readVersion.toString, incompatVersion.toString) ++ {\n          if (isSchemaChange) {\n            Array(start.toString, incompatVersion.toString, incompatVersion.toString, end.toString)\n          } else {\n            Array(DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key)\n          }\n        }\n    )\n  }\n\n  def blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges(\n      spark: SparkSession,\n      readSchema: StructType,\n      incompatibleSchema: StructType,\n      detectedDuringStreaming: Boolean,\n      isV2DataSource: Boolean = false): Throwable = {\n    val docLink = \"/versioning.html#column-mapping\"\n    val enableNonAdditiveSchemaEvolution = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING)\n    new DeltaStreamingNonAdditiveSchemaIncompatibleException(\n      readSchema,\n      incompatibleSchema,\n      generateDocsLinkOption(spark, docLink).getOrElse(\"-\"),\n      enableNonAdditiveSchemaEvolution,\n      additionalProperties = Map(\n        \"detectedDuringStreaming\" -> detectedDuringStreaming.toString,\n        \"isV2DataSource\" -> isV2DataSource.toString\n      ))\n  }\n\n  def failedToGetSnapshotDuringColumnMappingStreamingReadCheck(cause: Throwable): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_STREAMING_CHECK_COLUMN_MAPPING_NO_SNAPSHOT\",\n      messageParameters = Array(DeltaSQLConf\n        .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES.key),\n      cause = Some(cause))\n  }\n\n  def unsupportedDeltaTableForPathHadoopConf(unsupportedOptions: Map[String, String]): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_TABLE_FOR_PATH_UNSUPPORTED_HADOOP_CONF\",\n      messageParameters = Array(\n        DeltaTableUtils.validDeltaTableHadoopPrefixes.mkString(\"[\", \",\", \"]\"),\n        unsupportedOptions.mkString(\",\"))\n    )\n  }\n\n  def cloneOnRelativePath(path: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_CLONE_PATH\",\n      messageParameters = Array(path))\n  }\n\n  def cloneAmbiguousTarget(externalLocation: String, targetIdent: TableIdentifier): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_CLONE_AMBIGUOUS_TARGET\",\n      messageParameters = Array(externalLocation, s\"$targetIdent\")\n    )\n  }\n\n  def cloneFromUnsupportedSource(name: String, format: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLONE_UNSUPPORTED_SOURCE\",\n      messageParameters = Array(name, format)\n    )\n  }\n\n  def cloneReplaceUnsupported(tableIdentifier: TableIdentifier): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_UNSUPPORTED_CLONE_REPLACE_SAME_TABLE\",\n      messageParameters = Array(s\"$tableIdentifier\")\n    )\n  }\n\n  def cloneReplaceNonEmptyTable: Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_UNSUPPORTED_NON_EMPTY_CLONE\"\n    )\n  }\n\n  def cloneFromIcebergSourceWithPartitionEvolution(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLONE_INCOMPATIBLE_SOURCE.ICEBERG_UNDERGONE_PARTITION_EVOLUTION\",\n      messageParameters = Array()\n    )\n  }\n\n  def cloneFromIcebergSourceWithoutSpecs(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLONE_INCOMPATIBLE_SOURCE.ICEBERG_MISSING_PARTITION_SPECS\",\n      messageParameters = Array()\n    )\n  }\n\n  def partitionSchemaInIcebergTables: Throwable = {\n    new DeltaIllegalArgumentException(errorClass = \"DELTA_PARTITION_SCHEMA_IN_ICEBERG_TABLES\")\n  }\n\n  def icebergClassMissing(sparkConf: SparkConf, cause: Throwable): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MISSING_ICEBERG_CLASS\",\n      messageParameters = Array(\n        generateDocsLink(\n          sparkConf, \"/delta-utility.html#convert-a-parquet-table-to-a-delta-table\")),\n      cause = cause)\n  }\n\n  def hudiClassMissing(sparkConf: SparkConf, cause: Throwable): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MISSING_HUDI_CLASS\",\n      messageParameters = Array(\n        generateDocsLink(\n          sparkConf, \"/delta-utility.html#convert-a-parquet-table-to-a-delta-table\")),\n      cause = cause)\n  }\n\n  def streamingMetadataEvolutionException(\n      newSchema: StructType,\n      newConfigs: Map[String, String],\n      newProtocol: Protocol): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_STREAMING_METADATA_EVOLUTION\",\n      messageParameters = Array(\n        formatSchema(newSchema),\n        newConfigs.map { case (k, v) =>\n          s\"$k:$v\"\n        }.mkString(\", \"),\n        newProtocol.simpleString\n      ))\n  }\n\n  def streamingMetadataLogInitFailedIncompatibleMetadataException(\n      startVersion: Long,\n      endVersion: Long): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_LOG_INIT_FAILED_INCOMPATIBLE_METADATA\",\n      messageParameters = Array(startVersion.toString, endVersion.toString)\n    )\n  }\n\n  def failToDeserializeSchemaLog(location: String): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_LOG_DESERIALIZE_FAILED\",\n      messageParameters = Array(location)\n    )\n  }\n\n  def failToParseSchemaLog: Throwable = {\n    new DeltaRuntimeException(errorClass = \"DELTA_STREAMING_SCHEMA_LOG_PARSE_SCHEMA_FAILED\")\n  }\n\n  def sourcesWithConflictingSchemaTrackingLocation(\n      schemaTrackingLocatiob: String,\n      tableOrPath: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_LOCATION_CONFLICT\",\n      messageParameters = Array(schemaTrackingLocatiob, tableOrPath))\n  }\n\n  def incompatibleSchemaLogPartitionSchema(\n      persistedPartitionSchema: StructType,\n      tablePartitionSchema: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_LOG_INCOMPATIBLE_PARTITION_SCHEMA\",\n      messageParameters = Array(persistedPartitionSchema.json, tablePartitionSchema.json))\n  }\n\n  def incompatibleSchemaLogDeltaTable(\n      persistedTableId: String,\n      tableId: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_LOG_INCOMPATIBLE_DELTA_TABLE_ID\",\n      messageParameters = Array(persistedTableId, tableId))\n  }\n\n  def schemaTrackingLocationNotUnderCheckpointLocation(\n      schemaTrackingLocation: String,\n      checkpointLocation: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_STREAMING_SCHEMA_LOCATION_NOT_UNDER_CHECKPOINT\",\n      messageParameters = Array(schemaTrackingLocation, checkpointLocation))\n  }\n\n  def cannotContinueStreamingPostSchemaEvolution(\n      nonAdditiveSchemaChangeOpType: String,\n      previousSchemaChangeVersion: Long,\n      currentSchemaChangeVersion: Long,\n      checkpointHash: Int,\n      readerOptionsUnblock: Seq[String],\n      sqlConfsUnblock: Seq[String],\n      prettyColumnChangeDetails: String): Throwable = {\n    val unblockChangeOptions = readerOptionsUnblock.map { option =>\n        s\"\"\"  .option(\"$option\", \"$currentSchemaChangeVersion\")\"\"\"\n      }.mkString(\"\\n\")\n    val unblockStreamOptions = readerOptionsUnblock.map { option =>\n        s\"\"\"  .option(\"$option\", \"always\")\"\"\"\n      }.mkString(\"\\n\")\n    val unblockChangeConfs = sqlConfsUnblock.map { conf =>\n        s\"\"\"  SET $conf.ckpt_$checkpointHash = $currentSchemaChangeVersion;\"\"\"\n      }.mkString(\"\\n\")\n    val unblockStreamConfs = sqlConfsUnblock.map { conf =>\n        s\"\"\"  SET $conf.ckpt_$checkpointHash = \"always\";\"\"\"\n      }.mkString(\"\\n\")\n    val unblockAllConfs = sqlConfsUnblock.map { conf =>\n        s\"\"\"  SET $conf = \"always\";\"\"\"\n      }.mkString(\"\\n\")\n\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\",\n      messageParameters = Array(\n        nonAdditiveSchemaChangeOpType,\n        previousSchemaChangeVersion.toString,\n        currentSchemaChangeVersion.toString,\n        prettyColumnChangeDetails,\n        currentSchemaChangeVersion.toString,\n        unblockChangeOptions,\n        unblockStreamOptions,\n        unblockChangeConfs,\n        unblockStreamConfs,\n        unblockAllConfs\n      )\n    )\n  }\n\n  def cannotReconstructPathFromURI(uri: String): Throwable =\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_CANNOT_RECONSTRUCT_PATH_FROM_URI\",\n      messageParameters = Array(uri))\n\n  def deletionVectorCardinalityMismatch(): Throwable = {\n    new DeltaChecksumException(\n      errorClass = \"DELTA_DELETION_VECTOR_CARDINALITY_MISMATCH\",\n      messageParameters = Array.empty,\n      pos = 0\n    )\n  }\n\n  def deletionVectorSizeMismatch(): Throwable = {\n    new DeltaChecksumException(\n      errorClass = \"DELTA_DELETION_VECTOR_SIZE_MISMATCH\",\n      messageParameters = Array.empty,\n      pos = 0)\n  }\n\n  def deletionVectorInvalidRowIndex(): Throwable = {\n    new DeltaChecksumException(\n      errorClass = \"DELTA_DELETION_VECTOR_INVALID_ROW_INDEX\",\n      messageParameters = Array.empty,\n      pos = 0)\n  }\n\n  def deletionVectorChecksumMismatch(): Throwable = {\n    new DeltaChecksumException(\n      errorClass = \"DELTA_DELETION_VECTOR_CHECKSUM_MISMATCH\",\n      messageParameters = Array.empty,\n      pos = 0)\n  }\n\n  def statsRecomputeNotSupportedOnDvTables(): Throwable = {\n    new DeltaCommandUnsupportedWithDeletionVectorsException(\n      errorClass = \"DELTA_UNSUPPORTED_STATS_RECOMPUTE_WITH_DELETION_VECTORS\",\n      messageParameters = Array.empty\n    )\n  }\n\n  def addFileWithDVsAndTightBoundsException(): Throwable =\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_ADDING_DELETION_VECTORS_WITH_TIGHT_BOUNDS_DISALLOWED\")\n\n  def addFileWithDVsMissingNumRecordsException: Throwable =\n    new DeltaRuntimeException(errorClass = \"DELTA_DELETION_VECTOR_MISSING_NUM_RECORDS\")\n\n  def generateNotSupportedWithDeletionVectors(): Throwable =\n    new DeltaCommandUnsupportedWithDeletionVectorsException(\n      errorClass = \"DELTA_UNSUPPORTED_GENERATE_WITH_DELETION_VECTORS\")\n\n  def addingDeletionVectorsDisallowedException(): Throwable =\n    new DeltaCommandUnsupportedWithDeletionVectorsException(\n      errorClass = \"DELTA_ADDING_DELETION_VECTORS_DISALLOWED\")\n\n  def unsupportedExpression(\n    causedBy: String,\n    expType: DataType,\n    supportedTypes: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_UNSUPPORTED_EXPRESSION\",\n      messageParameters = Array(s\"$expType\", causedBy, supportedTypes.mkString(\",\"))\n    )\n  }\n\n  def rowIdAssignmentWithoutStats: Throwable = {\n    new DeltaIllegalStateException(errorClass = \"DELTA_ROW_ID_ASSIGNMENT_WITHOUT_STATS\")\n  }\n\n  def addingColumnWithInternalNameFailed(colName: String): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_ADDING_COLUMN_WITH_INTERNAL_NAME_FAILED\",\n      messageParameters = Array(colName)\n    )\n  }\n\n  def materializedRowIdMetadataMissing(tableName: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\",\n      messageParameters = Array(\"Row ID\", tableName)\n    )\n  }\n\n  def materializedRowCommitVersionMetadataMissing(tableName: String): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\",\n      messageParameters = Array(\"Row Commit Version\", tableName)\n    )\n  }\n\n  def domainMetadataDuplicate(domainName: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_DUPLICATE_DOMAIN_METADATA_INTERNAL_ERROR\",\n      messageParameters = Array(domainName)\n    )\n  }\n\n  def domainMetadataTableFeatureNotSupported(domainNames: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_DOMAIN_METADATA_NOT_SUPPORTED\",\n      messageParameters = Array(domainNames)\n    )\n  }\n\n  def uniFormIcebergRequiresIcebergCompat(): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNIVERSAL_FORMAT_VIOLATION\",\n      messageParameters = Array(\n        UniversalFormat.ICEBERG_FORMAT,\n        \"Requires IcebergCompat to be explicitly enabled in order for Universal Format (Iceberg) \" +\n        \"to be enabled on an existing table. To enable IcebergCompatV2, set the table property \" +\n        \"'delta.enableIcebergCompatV2' = 'true'.\"\n      )\n    )\n  }\n\n  def uniFormHudiDeleteVectorCompat(): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNIVERSAL_FORMAT_VIOLATION\",\n      messageParameters = Array(\n        UniversalFormat.HUDI_FORMAT,\n        \"Requires delete vectors to be disabled.\"\n      )\n    )\n  }\n\n  def uniFormHudiSchemaCompat(unsupportedType: DataType): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNIVERSAL_FORMAT_VIOLATION\",\n      messageParameters = Array(\n        UniversalFormat.HUDI_FORMAT,\n        s\"DataType: $unsupportedType is not currently supported.\"\n      )\n    )\n  }\n\n  def icebergCompatVersionMutualExclusive(version: Int): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.VERSION_MUTUAL_EXCLUSIVE\",\n      messageParameters = Array(version.toString)\n    )\n  }\n\n  def icebergCompatChangeVersionNeedRewrite(version: Int, newVersion: Int): Throwable = {\n    val newVersionString = newVersion.toString\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.CHANGE_VERSION_NEED_REWRITE\",\n      messageParameters = Array(newVersionString, newVersionString, newVersionString,\n        newVersionString)\n    )\n  }\n\n  def icebergCompatVersionNotSupportedException(\n      currVersion: Int,\n      maxVersion: Int): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.COMPAT_VERSION_NOT_SUPPORTED\",\n      messageParameters = Array(\n        currVersion.toString,\n        currVersion.toString,\n        maxVersion.toString\n      )\n    )\n  }\n\n  def icebergCompatReorgAddFileTagsMissingException(\n      tableVersion: Long,\n      icebergCompatVersion: Int,\n      addFilesCount: Long,\n      addFilesWithTagsCount: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.FILES_NOT_ICEBERG_COMPAT\",\n      messageParameters = Array(\n        icebergCompatVersion.toString,\n        icebergCompatVersion.toString,\n        addFilesCount.toString,\n        tableVersion.toString,\n        (addFilesCount - addFilesWithTagsCount).toString,\n        icebergCompatVersion.toString\n      )\n    )\n  }\n\n  def icebergCompatDataFileRewriteFailedException(\n      icebergCompatVersion: Int,\n      cause: Throwable): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.REWRITE_DATA_FAILED\",\n      messageParameters = Array(\n        icebergCompatVersion.toString,\n        icebergCompatVersion.toString,\n        icebergCompatVersion.toString\n      ),\n      cause\n    )\n  }\n\n  def icebergCompatReplacePartitionedTableException(\n      version: Int,\n      prevPartitionCols: Seq[String],\n      newPartitionCols: Seq[String]): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.REPLACE_TABLE_CHANGE_PARTITION_NAMES\",\n      messageParameters = Array(\n        version.toString,\n        version.toString,\n        prevPartitionCols.mkString(\"(\", \",\", \")\"),\n        newPartitionCols.mkString(\"(\", \",\", \")\")\n      )\n    )\n  }\n\n  def icebergCompatUnsupportedDataTypeException(\n      version: Int, dataType: DataType, schema: StructType): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.UNSUPPORTED_DATA_TYPE\",\n      messageParameters = Array(version.toString, version.toString,\n        dataType.typeName, schema.treeString)\n    )\n  }\n\n  def icebergCompatUnsupportedFieldException(\n      version: Int, field: StructField, schema: StructType): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.UNSUPPORTED_DATA_TYPE\",\n      messageParameters = Array(version.toString, version.toString,\n        s\"${field.dataType.typeName}:${field.name}\", schema.treeString)\n    )\n  }\n\n  def icebergCompatUnsupportedPartitionDataTypeException(\n      version: Int, dataType: DataType, schema: StructType): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.UNSUPPORTED_PARTITION_DATA_TYPE\",\n      messageParameters = Array(version.toString, version.toString,\n        dataType.typeName, schema.treeString)\n    )\n  }\n\n  def icebergCompatMissingRequiredTableFeatureException(\n      version: Int, tf: TableFeature): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.MISSING_REQUIRED_TABLE_FEATURE\",\n      messageParameters = Array(version.toString, version.toString, tf.name)\n    )\n  }\n\n  def icebergCompatDisablingRequiredTableFeatureException(\n      version: Int, tf: TableFeature): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.DISABLING_REQUIRED_TABLE_FEATURE\",\n      messageParameters = Array(version.toString, version.toString, tf.name, version.toString)\n    )\n  }\n\n  def icebergCompatIncompatibleTableFeatureException(\n      version: Int, tf: TableFeature): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.INCOMPATIBLE_TABLE_FEATURE\",\n      messageParameters = Array(version.toString, version.toString, tf.name)\n    )\n  }\n\n  def icebergCompatDeletionVectorsShouldBeDisabledException(version: Int): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.DELETION_VECTORS_SHOULD_BE_DISABLED\",\n      messageParameters = Array(version.toString, version.toString)\n    )\n  }\n\n  def icebergCompatDeletionVectorsNotPurgedException(version: Int): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.DELETION_VECTORS_NOT_PURGED\",\n      messageParameters = Array(version.toString, version.toString)\n    )\n  }\n\n  def icebergCompatWrongRequiredTablePropertyException(\n      version: Int,\n      key: String,\n      actualValue: String,\n      requiredValue: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.WRONG_REQUIRED_TABLE_PROPERTY\",\n      messageParameters = Array(version.toString, version.toString, key, requiredValue, actualValue)\n    )\n  }\n\n  def icebergCompatUnsupportedTypeWideningException(\n      version: Int,\n      fieldPath: Seq[String],\n      oldType: DataType,\n      newType: DataType): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_ICEBERG_COMPAT_VIOLATION.UNSUPPORTED_TYPE_WIDENING\",\n      messageParameters = Array(\n        version.toString,\n        version.toString,\n        SchemaUtils.prettyFieldName(fieldPath),\n        toSQLType(oldType),\n        toSQLType(newType)\n      )\n    )\n  }\n\n  def universalFormatConversionFailedException(\n      failedOnCommitVersion: Long,\n      format: String,\n      errorMessage: String): Throwable = {\n    new DeltaRuntimeException(\n      errorClass = \"DELTA_UNIVERSAL_FORMAT_CONVERSION_FAILED\",\n      messageParameters = Array(s\"$failedOnCommitVersion\", format, errorMessage)\n    )\n  }\n\n  def invalidAutoCompactType(value: String): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_INVALID_AUTO_COMPACT_TYPE\",\n      messageParameters = Array(value, AutoCompactType.ALLOWED_VALUES.mkString(\"(\", \",\", \")\"))\n    )\n  }\n\n  def clusterByInvalidNumColumnsException(\n      numColumnsLimit: Int,\n      actualNumColumns: Int): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLUSTER_BY_INVALID_NUM_COLUMNS\",\n      messageParameters = Array(numColumnsLimit.toString, actualNumColumns.toString)\n    )\n  }\n\n  def clusteringColumnMissingStats(\n      clusteringColumnWithoutStats: String,\n      statsSchema: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLUSTERING_COLUMN_MISSING_STATS\",\n      messageParameters = Array(clusteringColumnWithoutStats, statsSchema)\n    )\n  }\n\n  def clusteringColumnUnsupportedDataTypes(clusteringColumnsWithDataTypes: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLUSTERING_COLUMNS_DATATYPE_NOT_SUPPORTED\",\n      messageParameters = Array(clusteringColumnsWithDataTypes)\n    )\n  }\n\n  def clusteringColumnsMismatchException(\n      providedClusteringColumns: String,\n      existingClusteringColumns: String): Throwable = {\n    new DeltaAnalysisException(\n      \"DELTA_CLUSTERING_COLUMNS_MISMATCH\",\n      Array(providedClusteringColumns, existingClusteringColumns)\n    )\n  }\n\n  def clusterByWithPartitionedBy(): Throwable = {\n    new DeltaAnalysisException(\n      \"DELTA_CLUSTER_BY_WITH_PARTITIONED_BY\",\n      Array.empty)\n  }\n\n  def dropClusteringColumnNotSupported(droppingClusteringCols: Seq[String]): Throwable = {\n    new DeltaAnalysisException(\n      \"DELTA_UNSUPPORTED_DROP_CLUSTERING_COLUMN\",\n      Array(droppingClusteringCols.mkString(\",\")))\n  }\n\n  def replacingClusteredTableWithPartitionedTableNotAllowed(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLUSTERING_REPLACE_TABLE_WITH_PARTITIONED_TABLE\",\n      messageParameters = Array.empty)\n  }\n\n  def clusteringWithPartitionPredicatesException(predicates: Seq[String]): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_CLUSTERING_WITH_PARTITION_PREDICATE\",\n      messageParameters = Array(s\"${predicates.mkString(\" \")}\"))\n  }\n\n  def clusteringWithZOrderByException(zOrderBy: Seq[UnresolvedAttribute]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CLUSTERING_WITH_ZORDER_BY\",\n      messageParameters = Array(s\"${zOrderBy.map(_.name).mkString(\", \")}\"))\n  }\n\n  def optimizeFullNotSupportedException(): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_OPTIMIZE_FULL_NOT_SUPPORTED\",\n      messageParameters = Array.empty)\n  }\n\n  def alterClusterByNotOnDeltaTableException(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ONLY_OPERATION\",\n      messageParameters = Array(\"ALTER TABLE CLUSTER BY\"))\n  }\n\n  def alterClusterByNotAllowedException(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ALTER_TABLE_CLUSTER_BY_NOT_ALLOWED\",\n      messageParameters = Array.empty)\n  }\n\n  def alterTableSetClusteringTableFeatureException(tableFeature: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ALTER_TABLE_SET_CLUSTERING_TABLE_FEATURE_NOT_ALLOWED\",\n      messageParameters = Array(tableFeature))\n  }\n\n  def createTableSetClusteringTableFeatureException(tableFeature: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_SET_CLUSTERING_TABLE_FEATURE_NOT_ALLOWED\",\n      messageParameters = Array(tableFeature))\n  }\n\n  def mergeAddVoidColumn(columnName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_MERGE_ADD_VOID_COLUMN\",\n      messageParameters = Array(toSQLId(columnName))\n    )\n  }\n\n  def columnBuilderMissingDataType(colName: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_COLUMN_MISSING_DATA_TYPE\",\n      messageParameters = Array(toSQLId(colName)))\n  }\n\n  def createTableMissingTableNameOrLocation(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_MISSING_TABLE_NAME_OR_LOCATION\",\n      messageParameters = Array.empty)\n  }\n\n  def createTableIdentifierLocationMismatch(identifier: String, location: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_IDENTIFIER_LOCATION_MISMATCH\",\n      messageParameters = Array(identifier, location))\n  }\n\n  def dropColumnOnSingleFieldSchema(schema: StructType): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_DROP_COLUMN_ON_SINGLE_FIELD_SCHEMA\",\n      messageParameters = Array(schema.treeString))\n  }\n\n  def errorFindingColumnPosition(\n      columnPath: Seq[String], schema: DataType, extraErrMsg: String): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0008\",\n      messageParameters = Array(\n        UnresolvedAttribute(columnPath).name, dataTypeToString(schema), extraErrMsg))\n  }\n\n  def alterTableClusterByOnPartitionedTableException(): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_ALTER_TABLE_CLUSTER_BY_ON_PARTITIONED_TABLE_NOT_ALLOWED\",\n      messageParameters = Array.empty)\n  }\n\n  def createTableWithDifferentClusteringException(\n      path: Path,\n      specifiedClusterBySpec: Option[ClusterBySpec],\n      existingClusterBySpec: Option[ClusterBySpec]): Throwable = {\n    new DeltaAnalysisException(\n      errorClass = \"DELTA_CREATE_TABLE_WITH_DIFFERENT_CLUSTERING\",\n      messageParameters = Array(\n        path.toString,\n        specifiedClusterBySpec\n          .map(_.columnNames.map(_.toString))\n          .getOrElse(Seq.empty)\n          .mkString(\", \"),\n        existingClusterBySpec\n          .map(_.columnNames.map(_.toString))\n          .getOrElse(Seq.empty)\n          .mkString(\", \")))\n  }\n\n  def unsupportedWritesWithMissingCoordinators(coordinatorName: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNSUPPORTED_WRITES_WITHOUT_COORDINATOR\",\n      messageParameters = Array(coordinatorName))\n  }\n\n  private def dataTypeToString(dt: DataType): String = dt match {\n    case s: StructType => s.treeString\n    case other => other.simpleString\n  }\n\n  def operationBlockedOnCatalogManagedTable(operation: String): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\",\n      messageParameters = Array(operation))\n  }\n\n  def deltaCannotCreateCatalogManagedTable(): Throwable = {\n    new DeltaUnsupportedOperationException(\n      errorClass = \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_CREATION\",\n      messageParameters = Array.empty)\n  }\n\n  def numRecordsMismatch(\n      operation: String,\n      numAddedRecords: Long,\n      numRemovedRecords: Long): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_NUM_RECORDS_MISMATCH\",\n      messageParameters = Array(operation, numAddedRecords.toString, numRemovedRecords.toString)\n    )\n  }\n\n  def commandInvariantViolationException(\n      operation: String,\n      id: UUID): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_COMMAND_INVARIANT_VIOLATION\",\n      messageParameters = Array(operation, id.toString)\n    )\n  }\n\n  def catalogManagedTablePathBasedAccessNotAllowed(path: Path): Throwable = {\n    new DeltaIllegalStateException(\n      errorClass = \"DELTA_PATH_BASED_ACCESS_TO_CATALOG_MANAGED_TABLE_BLOCKED\",\n      messageParameters = Array(path.toString)\n    )\n  }\n\n  def cannotResolveSourceColumnException(columnPath: Seq[String]): Throwable = {\n    new DeltaIllegalArgumentException(\n      errorClass = \"DELTA_CANNOT_RESOLVE_SOURCE_COLUMN\",\n      messageParameters = Array(s\"${UnresolvedAttribute(columnPath).name}\"))\n  }\n}\n\nobject DeltaErrors extends DeltaErrorsBase\n/** The basic class for all Tahoe commit conflict exceptions. */\nabstract class DeltaConcurrentModificationException(message: String)\n  extends ConcurrentModificationException(message) {\n\n  /**\n   * Type of the commit conflict.\n   */\n  def conflictType: String = this.getClass.getSimpleName.stripSuffix(\"Exception\")\n}\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.ConcurrentWriteException]] instead.\n */\nclass ConcurrentWriteException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(conflictingCommit: Option[CommitInfo]) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      s\"A concurrent transaction has written new data since the current transaction \" +\n        s\"read the table. Please try the operation again.\",\n      conflictingCommit))\n}\n\n/**\n * Thrown when time travelling to a version that does not exist in the Delta Log.\n * @param userVersion - the version time travelling to\n * @param earliest - earliest version available in the Delta Log\n * @param latest - The latest version available in the Delta Log\n */\ncase class VersionNotFoundException(\n    userVersion: Long,\n    earliest: Long,\n    latest: Long) extends DeltaAnalysisException(\n  errorClass = \"DELTA_VERSION_NOT_FOUND\",\n  messageParameters = Array(userVersion.toString, earliest.toString, latest.toString)\n)\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.MetadataChangedException]] instead.\n */\nclass MetadataChangedException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(conflictingCommit: Option[CommitInfo]) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      \"The metadata of the Delta table has been changed by a concurrent update. \" +\n        \"Please try the operation again.\",\n      conflictingCommit))\n}\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.ProtocolChangedException]] instead.\n */\nclass ProtocolChangedException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(conflictingCommit: Option[CommitInfo]) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      \"The protocol version of the Delta table has been changed by a concurrent update. \" +\n        \"Please try the operation again.\",\n      conflictingCommit))\n}\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.ConcurrentAppendException]] instead.\n */\nclass ConcurrentAppendException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(\n    conflictingCommit: Option[CommitInfo],\n    partition: String,\n    customRetryMsg: Option[String] = None) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      s\"Files were added to $partition by a concurrent update. \" +\n        customRetryMsg.getOrElse(\"Please try the operation again.\"),\n      conflictingCommit))\n}\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.ConcurrentDeleteReadException]] instead.\n */\nclass ConcurrentDeleteReadException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(conflictingCommit: Option[CommitInfo], file: String) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      \"This transaction attempted to read one or more files that were deleted\" +\n        s\" (for example $file) by a concurrent update. Please try the operation again.\",\n      conflictingCommit))\n}\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.ConcurrentDeleteDeleteException]] instead.\n */\nclass ConcurrentDeleteDeleteException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(conflictingCommit: Option[CommitInfo], file: String) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      \"This transaction attempted to delete one or more files that were deleted \" +\n        s\"(for example $file) by a concurrent update. Please try the operation again.\",\n      conflictingCommit))\n}\n\n/**\n * This class is kept for backward compatibility.\n * Use [[io.delta.exceptions.ConcurrentTransactionException]] instead.\n */\nclass ConcurrentTransactionException(message: String)\n  extends io.delta.exceptions.DeltaConcurrentModificationException(message) {\n  def this(conflictingCommit: Option[CommitInfo]) = this(\n    DeltaErrors.concurrentModificationExceptionMsg(\n      SparkEnv.get.conf,\n      s\"This error occurs when multiple streaming queries are using the same checkpoint to write \" +\n        \"into this table. Did you run multiple instances of the same streaming query\" +\n        \" at the same time?\",\n      conflictingCommit))\n}\n\n/** A helper class in building a helpful error message in case of metadata mismatches. */\nclass MetadataMismatchErrorBuilder {\n  private var subErrors: Seq[(String, Array[String])] = Nil\n\n  def addSchemaMismatch(original: StructType, data: StructType, id: String): Unit = {\n    subErrors :+= (\"SCHEMA_MISMATCH\", Array(\n      id,\n      DeltaErrors.formatSchema(original),\n      DeltaErrors.formatSchema(data)\n    ))\n  }\n\n  def addPartitioningMismatch(original: Seq[String], provided: Seq[String]): Unit = {\n    subErrors :+= (\"PARTITIONING_MISMATCH\", Array(\n      DeltaErrors.formatColumnList(provided),\n      DeltaErrors.formatColumnList(original)\n    ))\n  }\n\n  def addOverwriteBit(): Unit = {\n    subErrors :+= (\"OVERWRITE_REQUIRED\", Array.empty[String])\n  }\n\n  def finalizeAndThrow(conf: SQLConf): Unit = {\n    throw new DeltaAnalysisExceptionWithSubErrors(\n      errorClass = \"DELTA_METADATA_MISMATCH\",\n      messageParameters = Array.empty[String],\n      subErrors = subErrors\n    )\n  }\n}\n\nclass DeltaColumnMappingUnsupportedException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty)\n  extends ColumnMappingUnsupportedException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\nclass DeltaFileNotFoundException(\n  errorClass: String,\n  messageParameters: Array[String] = Array.empty)\n  extends FileNotFoundException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\nclass DeltaFileAlreadyExistsException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty)\n  extends FileAlreadyExistsException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\nclass DeltaIOException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty,\n    cause: Throwable = null)\n  extends IOException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters), cause)\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\nclass DeltaIllegalStateException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty,\n    cause: Throwable = null)\n  extends IllegalStateException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters), cause)\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\nclass DeltaIndexOutOfBoundsException(\n  errorClass: String,\n  messageParameters: Array[String] = Array.empty)\n  extends IndexOutOfBoundsException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\n/** Thrown when the protocol version of a table is greater than supported by this client. */\ncase class InvalidProtocolVersionException(\n    tableNameOrPath: String,\n    readerRequiredVersion: Int,\n    writerRequiredVersion: Int,\n    supportedReaderVersions: Seq[Int],\n    supportedWriterVersions: Seq[Int])\n  extends RuntimeException(DeltaThrowableHelper.getMessage(\n    errorClass = \"DELTA_INVALID_PROTOCOL_VERSION\",\n    messageParameters = Array(\n      tableNameOrPath,\n      readerRequiredVersion.toString,\n      writerRequiredVersion.toString,\n      io.delta.VERSION,\n      supportedReaderVersions.sorted.mkString(\", \"),\n      supportedWriterVersions.sorted.mkString(\", \"))))\n  with DeltaThrowable {\n  override def getErrorClass: String = \"DELTA_INVALID_PROTOCOL_VERSION\"\n}\n\nclass ProtocolDowngradeException(oldProtocol: Protocol, newProtocol: Protocol)\n  extends RuntimeException(DeltaThrowableHelper.getMessage(\n    errorClass = \"DELTA_INVALID_PROTOCOL_DOWNGRADE\",\n    messageParameters = Array(oldProtocol.simpleString, newProtocol.simpleString)\n  )) with DeltaThrowable {\n  override def getErrorClass: String = \"DELTA_INVALID_PROTOCOL_DOWNGRADE\"\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(\n      \"DELTA_INVALID_PROTOCOL_DOWNGRADE\",\n      errorSubClass = null,\n      Array(oldProtocol.simpleString, newProtocol.simpleString))\n  }\n}\n\nclass DeltaTableFeatureException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty)\n  extends DeltaRuntimeException(errorClass, messageParameters)\n\ncase class DeltaUnsupportedTableFeatureException(\n    errorClass: String,\n    tableNameOrPath: String,\n    unsupported: Iterable[String])\n  extends DeltaTableFeatureException(\n    errorClass,\n    Array(tableNameOrPath, io.delta.VERSION, unsupported.mkString(\", \")))\n\nclass DeltaRuntimeException(\n    errorClass: String,\n    val messageParameters: Array[String] = Array.empty)\n  extends RuntimeException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n}\n\nclass DeltaSparkException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty,\n    cause: Throwable = null)\n  extends SparkException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters), cause)\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n}\n\nclass DeltaNoSuchTableException(\n    errorClass: String,\n    errorMessageParameters: Array[String] = Array.empty)\n  extends AnalysisException(\n    DeltaThrowableHelper.getMessage(errorClass, errorMessageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper\n      .getMessageParameters(errorClass, errorSubClass = null, errorMessageParameters)\n  }\n}\n\nclass DeltaCommandUnsupportedWithDeletionVectorsException(\n  errorClass: String,\n  messageParameters: Array[String] = Array.empty)\n  extends UnsupportedOperationException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n}\n\nsealed trait DeltaTablePropertyValidationFailedSubClass {\n  def tag: String\n  /** Can be overridden in case subclasses need the table name as well. */\n  def messageParameters(table: String): Array[String] = Array(table)\n}\n\nfinal object DeltaTablePropertyValidationFailedSubClass {\n  final case object PersistentDeletionVectorsWithIncrementalManifestGeneration\n    extends DeltaTablePropertyValidationFailedSubClass {\n    override val tag = \"PERSISTENT_DELETION_VECTORS_WITH_INCREMENTAL_MANIFEST_GENERATION\"\n  }\n  final case object ExistingDeletionVectorsWithIncrementalManifestGeneration\n    extends DeltaTablePropertyValidationFailedSubClass {\n    override val tag = \"EXISTING_DELETION_VECTORS_WITH_INCREMENTAL_MANIFEST_GENERATION\"\n    /** This subclass needs the table parameters in two places. */\n    override def messageParameters(table: String): Array[String] = Array(table, table)\n  }\n  final case object PersistentDeletionVectorsInNonParquetTable\n    extends DeltaTablePropertyValidationFailedSubClass {\n    override val tag = \"PERSISTENT_DELETION_VECTORS_IN_NON_PARQUET_TABLE\"\n  }\n}\n\nclass DeltaTablePropertyValidationFailedException(\n    table: String,\n    subClass: DeltaTablePropertyValidationFailedSubClass)\n  extends RuntimeException(DeltaThrowableHelper.getMessage(\n    errorClass = \"DELTA_VIOLATE_TABLE_PROPERTY_VALIDATION_FAILED\" + \".\" + subClass.tag,\n    messageParameters = subClass.messageParameters(table)))\n    with DeltaThrowable {\n\n  override def getMessageParameters: java.util.Map[String, String] =\n    DeltaThrowableHelper.getMessageParameters(\n      \"DELTA_VIOLATE_TABLE_PROPERTY_VALIDATION_FAILED\",\n      subClass.tag,\n      subClass.messageParameters(table))\n\n  override def getErrorClass: String =\n    \"DELTA_VIOLATE_TABLE_PROPERTY_VALIDATION_FAILED.\" + subClass.tag\n}\n\n/** Errors thrown around column mapping. */\nclass ColumnMappingUnsupportedException(msg: String)\n  extends UnsupportedOperationException(msg)\ncase class ColumnMappingException(msg: String, mode: DeltaColumnMappingMode)\n  extends AnalysisException(msg)\n\nclass DeltaChecksumException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty,\n    pos: Long)\n  extends ChecksumException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters), pos)\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n}\n\n/**\n * Errors thrown when an operation is not supported with non-additive schema changes\n * (rename / drop column, type change).\n *\n * To make compatible with existing behavior for those who accidentally has already used this\n * operation, user should always be able to use `escapeConfigName` to fall back at own risk.\n */\nclass DeltaStreamingNonAdditiveSchemaIncompatibleException(\n    val readSchema: StructType,\n    val incompatibleSchema: StructType,\n    val docLink: String,\n    val enableNonAdditiveSchemaEvolution: Boolean = false,\n    val additionalProperties: Map[String, String] = Map.empty)\n  extends DeltaUnsupportedOperationException(\n    errorClass = if (additionalProperties.getOrElse(\"isV2DataSource\", \"false\") == \"true\") {\n      \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_V2\"\n    } else if (enableNonAdditiveSchemaEvolution) {\n      \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_USE_SCHEMA_LOG\"\n    } else {\n      \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE\"\n    },\n    messageParameters = Array(\n      docLink,\n      readSchema.json,\n      incompatibleSchema.json)\n  )\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaFileFormat.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.datasources.FileFormat\n\ntrait DeltaFileFormat {\n  // TODO: Add support for column mapping\n  /** Return the current Spark session used. */\n  protected def spark: SparkSession\n\n  /**\n   * Build the underlying Spark `FileFormat` of the Delta table with specified metadata.\n   *\n   * With column mapping, some properties of the underlying file format might change during\n   * transaction, so if possible, we should always pass in the latest transaction's metadata\n   * instead of one from a past snapshot.\n   */\n  def fileFormat(protocol: Protocol, metadata: Metadata): FileFormat =\n    new DeltaParquetFileFormat(protocol, metadata)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaFileProviderUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.Action\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.storage.ClosableIterator\nimport org.apache.spark.sql.delta.util.FileNames.DeltaFile\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.FileStatus\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.JsonToStructs\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.unsafe.types.UTF8String\n\nobject DeltaFileProviderUtils extends DeltaLogging {\n\n  protected def readThreadPool = SnapshotManagement.deltaLogAsyncUpdateThreadPool\n\n  /** Put any future parsing options here. */\n  val jsonStatsParseOption = Map.empty[String, String]\n\n  private[delta] def createJsonStatsParser(schemaToUse: StructType): String => InternalRow = {\n    val parser = JsonToStructs(\n      schema = schemaToUse,\n      options = jsonStatsParseOption,\n      child = null,\n      timeZoneId = Some(SQLConf.get.sessionLocalTimeZone)\n    )\n    (json: String) => {\n      val utf8json = UTF8String.fromString(json)\n      parser.nullSafeEval(utf8json).asInstanceOf[InternalRow]\n    }\n  }\n\n  /**\n   * Get the Delta json files present in the delta log in the range [startVersion, endVersion].\n   * Returns the files in sorted order, and throws if any in the range are missing.\n   */\n  def getDeltaFilesInVersionRange(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      startVersion: Long,\n      endVersion: Long,\n      catalogTableOpt: Option[CatalogTable]): Seq[FileStatus] = {\n    // Pass `failOnDataLoss = false` as we are doing an explicit validation on the result ourselves\n    // to identify that there are no gaps.\n    val result =\n      deltaLog\n        .getChangeLogFiles(startVersion, endVersion, catalogTableOpt, failOnDataLoss = false)\n        .map(_._2)\n        .collect { case DeltaFile(fs, v) => (fs, v) }\n        .toSeq\n    // Verify that we got the entire range requested\n    if (result.size.toLong != endVersion - startVersion + 1) {\n      // [[unsafeVolatileSnapshot]] maybe null, which needs to be explicitly filtered out.\n      val snapshot = Some(deltaLog.unsafeVolatileSnapshot).filter(_ != null)\n      recordDeltaEvent(\n        deltaLog = deltaLog,\n        opType = \"delta.exceptions.deltaVersionsNotContiguous\",\n        data = Map(\n          // Remove the first element of the stack trace since this represents\n          // the [[Thread.getStackTrace]] call itself.\n          \"stackTrace\" -> Thread.currentThread().getStackTrace.tail.mkString(\"\\n\\t\"),\n          \"startVersion\" -> startVersion,\n          \"endVersion\" -> endVersion,\n          \"unsafeVolatileSnapshot.latestCheckpointVersion\" ->\n            snapshot.map(_.checkpointProvider.version).getOrElse(-1L),\n          \"unsafeVolatileSnapshot.latestSnapshotVersion\" ->\n            snapshot.map(_.version).getOrElse(-1L),\n          \"unsafeVolatileSnapshot.checksumOpt\" ->\n            snapshot.map(_.checksumOpt).orNull\n        ))\n      throw DeltaErrors.deltaVersionsNotContiguousException(\n        spark = spark,\n        deltaVersions = result.map(_._2),\n        startVersion = startVersion,\n        endVersion = endVersion,\n        // Get the latest snapshot version for visibility when throwing the exception,\n        // this is not exactly \"the version to load snapshot\" but\n        // we just use the latest snapshot version here.\n        versionToLoad = snapshot.map(_.version).getOrElse(-1L))\n    }\n    result.map(_._1)\n  }\n\n  /** Helper method to read and parse the delta files parallelly into [[Action]]s. */\n  def parallelReadAndParseDeltaFilesAsIterator(\n      deltaLog: DeltaLog,\n      spark: SparkSession,\n      files: Seq[FileStatus]): Seq[ClosableIterator[String]] = {\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n    parallelReadDeltaFilesBase(spark, files, hadoopConf, { file: FileStatus =>\n      deltaLog.store.readAsIterator(file, hadoopConf)\n    })\n  }\n\n  protected def parallelReadDeltaFilesBase[A](\n      spark: SparkSession,\n      files: Seq[FileStatus],\n      hadoopConf: Configuration,\n      f: FileStatus => A): Seq[A] = {\n    readThreadPool.parallelMap(spark, files) { file =>\n      f(file)\n    }.toSeq\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaHistoryManager.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.FileNotFoundException\nimport java.sql.Timestamp\nimport java.util.concurrent.CompletableFuture\n\nimport scala.collection.mutable\nimport scala.concurrent.{ExecutionContext, ExecutionContextExecutorService, Future}\nimport scala.concurrent.duration.Duration\n\nimport org.apache.spark.sql.delta.actions.{Action, CommitInfo, CommitMarker, JobInfo, NotebookInfo}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LogStore\nimport org.apache.spark.sql.delta.util.{DateTimeUtils, DeltaCommitFileProvider, FileNames, TimestampFormatter}\nimport org.apache.spark.sql.delta.util.FileNames._\nimport org.apache.spark.sql.delta.util.threads.DeltaThreadPool\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.util.{SerializableConfiguration, ThreadUtils}\n\n/**\n * This class keeps tracks of the version of commits and their timestamps for a Delta table to\n * help with operations like describing the history of a table.\n *\n * @param deltaLog The transaction log of this table\n * @param maxKeysPerList How many commits to list when performing a parallel search. Exposed for\n *                       tests. Currently set to `1000`, which is the maximum keys returned by S3\n *                       per list call. Azure can return `5000`, therefore we choose 1000.\n */\nclass DeltaHistoryManager(\n    deltaLog: DeltaLog,\n    maxKeysPerList: Int = 1000) extends DeltaLogging {\n\n  private def spark: SparkSession = SparkSession.active\n\n  private def getSerializableHadoopConf: SerializableConfiguration = {\n    new SerializableConfiguration(deltaLog.newDeltaHadoopConf())\n  }\n\n  import DeltaHistoryManager._\n\n  /**\n   * Returns the information of the latest `limit` commits made to this table in reverse\n   * chronological order.\n   */\n  def getHistory(\n      limitOpt: Option[Int],\n      catalogTableOpt: Option[CatalogTable]): Seq[DeltaHistory] = {\n    val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n    val listStart = limitOpt\n      .map { limit => math.max(snapshot.version - limit + 1, 0) }\n      .getOrElse(getEarliestDeltaFile(deltaLog))\n    getHistory(listStart, end = Some(snapshot.version), catalogTableOpt)\n  }\n\n  /**\n   * Returns the information of the latest `limit` commits made to this table in reverse\n   * chronological order. This version does not take in a catalog table and should only be\n   * used in testing.\n   */\n  def getHistory(limitOpt: Option[Int]): Seq[DeltaHistory] = {\n    getHistory(limitOpt, catalogTableOpt = None)\n  }\n\n  /**\n   * Get the commit information of the Delta table from commit `[start, end]` in\n   * reverse chronological order. An empty Seq is returned when `start > end`.\n   *\n   * @param useInCommitTimestamps Whether ICT should be used as the commit-timestamp for\n   *                              the commits.\n   *                              If `true`, all commits in the range must have ICTs and\n   *                              the timestamp returned for each commit will be the ICT.\n   *                              If `false`, the file modification time will be used as the\n   *                              timestamp.\n   */\n  private[delta] def getHistoryImpl(\n      start: Long,\n      end: Long,\n      useInCommitTimestamps: Boolean,\n      commitFileProvider: DeltaCommitFileProvider): Seq[DeltaHistory] = {\n    import org.apache.spark.sql.delta.implicits._\n    val conf = getSerializableHadoopConf\n    val logPath = deltaLog.logPath.toString\n    // We assume that commits are contiguous, therefore we try to load all of them in order\n    val info = spark.range(start, end + 1)\n      .mapPartitions { versions =>\n        val logStore = LogStore(SparkEnv.get.conf, conf.value)\n        val basePath = new Path(logPath)\n        val fs = basePath.getFileSystem(conf.value)\n        versions.flatMap { commit =>\n          try {\n            val deltaFile = commitFileProvider.deltaFile(commit)\n            val commitInfoOpt = DeltaHistoryManager\n              .getCommitInfoOpt(logStore, deltaFile, conf.value)\n            val timestamp = if (useInCommitTimestamps) {\n              CommitInfo.getRequiredInCommitTimestamp(commitInfoOpt, commit.toString)\n            } else {\n              fs.getFileStatus(deltaFile).getModificationTime\n            }\n            val ci = commitInfoOpt.getOrElse(CommitInfo.empty(Some(commit)))\n            Some(ci.withTimestamp(timestamp))\n          } catch {\n            case _: FileNotFoundException =>\n              // We have a race-condition where files can be deleted while reading. It's fine to\n              // skip those files\n              None\n          }\n        }.map(DeltaHistory.fromCommitInfo)\n      }\n    val monotonizedCommits = if (useInCommitTimestamps) {\n      // ICT timestamps are guaranteed to be monotonically increasing.\n      info.collect()\n    } else {\n      monotonizeCommitTimestamps(info.collect())\n    }\n    // Spark should return the commits in increasing order as well\n    monotonizedCommits.reverse\n  }\n\n  /**\n   * Get the commit information of the Delta table from commit `[start, end]` in reverse\n   * chronological order. If `end` is `None`, we return all commits from start to now.\n   * @param start The start of the commit range, inclusive.\n   * @param end The end of the commit range, inclusive.\n   * @param catalogTableOpt the catalog table associated with the Delta table.\n   */\n  def getHistory(\n      start: Long,\n      end: Option[Long],\n      catalogTableOpt: Option[CatalogTable] = None): Seq[DeltaHistory] = {\n    val currentSnapshot = deltaLog.unsafeVolatileSnapshot\n    val (snapshotNewerThanResolvedEnd, resolvedEnd) = end match {\n        case Some(endInclusive) if currentSnapshot.version >= endInclusive =>\n          // Use the cache snapshot if it's fresh enough for the [start, endInclusive] query.\n          (currentSnapshot, math.min(currentSnapshot.version, endInclusive))\n        case _ =>\n          // Either end doesn't exist or the currently cached snapshot isn't new enough to\n          // satisfy it.\n          val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n          val endInclusive = end.getOrElse(snapshot.version).min(snapshot.version)\n          (snapshot, endInclusive)\n      }\n\n    val commitFileProvider = DeltaCommitFileProvider(snapshotNewerThanResolvedEnd)\n    if (!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(\n        snapshotNewerThanResolvedEnd.metadata)) {\n      getHistoryImpl(start, resolvedEnd, useInCommitTimestamps = false, commitFileProvider)\n    } else {\n      val ictEnablementCommit =\n        InCommitTimestampUtils.getValidatedICTEnablementInfo(snapshotNewerThanResolvedEnd.metadata)\n      ictEnablementCommit match {\n        case Some(Commit(ictEarliest, _)) =>\n          // getHistoryImpl will return an empty Seq if start > end.\n          val nonICTCommits = getHistoryImpl(\n              start,\n              math.min(resolvedEnd, ictEarliest - 1),\n              useInCommitTimestamps = false,\n              commitFileProvider)\n          val ictCommits = getHistoryImpl(\n              math.max(ictEarliest, start),\n              resolvedEnd,\n              useInCommitTimestamps = true,\n              commitFileProvider)\n          // Merge the two sequences, ensuring ICT commits are listed first as they are more recent,\n          // followed by non-ICT commits, maintaining the reverse chronological order.\n          ictCommits ++ nonICTCommits\n        case _ => // Enablement info not found, ICT is enabled for all available commits.\n          getHistoryImpl(start, resolvedEnd, useInCommitTimestamps = true, commitFileProvider)\n      }\n    }\n  }\n\n  /**\n   * Returns the latest commit that happened at or before `time` in the range `[start, end)`.\n   * All the commits in the range `[start, end)` are assumed to not have inCommitTimestamps.\n   * If no such commit exists, the earliest commit is returned.\n   */\n  def getCommitFromNonICTRange(start: Long, end: Long, time: Long): Commit = {\n    if (end - start > 2 * maxKeysPerList) {\n      parallelSearch(time, start, end)\n    } else {\n      val commits = getCommitsWithNonIctTimestamps(\n        deltaLog.store,\n        deltaLog.logPath,\n        start,\n        Some(end),\n        deltaLog.newDeltaHadoopConf())\n      if (commits.isEmpty) {\n        throw DeltaErrors.noHistoryFound(deltaLog.logPath)\n      }\n      lastCommitBeforeTimestamp(commits, time).getOrElse(commits.head)\n    }\n  }\n\n  /**\n   * Returns the latest commit that happened at or before `time`.\n   * @param timestamp The timestamp to search for\n   * @param canReturnLastCommit Whether we can return the latest version of the table if the\n   *                            provided timestamp is after the latest commit\n   * @param mustBeRecreatable Whether the state at the given commit should be recreatable\n   * @param canReturnEarliestCommit Whether we can return the earliest commit if no such commit\n   *                                exists.\n   */\n  def getActiveCommitAtTime(\n      timestamp: Timestamp,\n      catalogTableOpt: Option[CatalogTable],\n      canReturnLastCommit: Boolean,\n      mustBeRecreatable: Boolean = true,\n      canReturnEarliestCommit: Boolean = false): Commit = {\n    val time = timestamp.getTime\n    val earliestVersion = if (mustBeRecreatable) {\n      getEarliestRecreatableCommit\n    } else {\n      getEarliestDeltaFile(deltaLog)\n    }\n    val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n    val commitFileProvider = DeltaCommitFileProvider(snapshot)\n    val latestVersion = snapshot.version\n\n    // In most cases, the earliest commit should not be the result of this search.\n    // When ICT is enabled, use -1L as the placeholder timestamp for the earliest commit\n    // for the search and only fetch the real timestamp if the earliest commit is\n    // the result of the search. We can potentially avoid one unnecessary IO this way.\n    val placeholderEarliestCommit = Commit(earliestVersion, -1L)\n    val ictEnablementCommit =\n      if (DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata)) {\n        InCommitTimestampUtils.getValidatedICTEnablementInfo(snapshot.metadata)\n          // If missing, ICT is enabled for all available versions\n          .getOrElse(placeholderEarliestCommit)\n      } else {\n        // Pretend ICT will be enabled after the latest version and requested timestamp.\n        // This will force us to use the non-ICT search path below.\n        Commit(latestVersion + 1, time + 1)\n      }\n\n    var commitOpt = if (ictEnablementCommit.timestamp <= time) {\n      // ICT was enabled as-of the requested time\n      if (snapshot.timestamp <= time) {\n        // We just proved we should use the latest snapshot\n        Some(Commit(snapshot.version, snapshot.timestamp))\n      } else {\n        // start ICT search over [earliest available ICT version, latestVersion)\n        val ictEnabledForEntireWindow = (ictEnablementCommit.version <= earliestVersion)\n        val searchWindowLowerBoundCommit =\n          if (ictEnabledForEntireWindow) placeholderEarliestCommit else ictEnablementCommit\n\n        // Note that this search can return `placeholderEarliestCommit`.\n        // The real timestamp of the earliest commit will be fetched later.\n        getActiveCommitAtTimeFromICTRange(\n          time,\n          searchWindowLowerBoundCommit,\n          latestVersion + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 10,\n          spark,\n          commitFileProvider)\n      }\n    } else {\n      // ICT was NOT enabled as-of the requested time\n      if (ictEnablementCommit.version <= earliestVersion) {\n        // We're searching for a non-ICT time but the non-ICT commits are all missing.\n        // If `canReturnEarliestCommit` is `false`, we need the details of the\n        // earliest commit to populate the TimestampEarlierThanCommitRetentionException\n        // error correctly.\n        // Else, when `canReturnEarliestCommit` is `true`, the earliest commit\n        // is the desired result.\n        // The real timestamp of the earliest commit will be fetched later.\n        Some(placeholderEarliestCommit)\n      } else {\n        // start non-ICT search over [earliestVersion, ictEnablementVersion)\n        Some(getCommitFromNonICTRange(earliestVersion, end = ictEnablementCommit.version, time))\n      }\n    }\n\n    // We need to fetch the correct timestamp for the earliest commit if it was the result of the\n    // search.\n    // If commitOpt == ictEnablementCommit, we also need to validate the existence of the ICT\n    // enablement commit.\n    if (commitOpt.contains(placeholderEarliestCommit) || commitOpt.contains(ictEnablementCommit)) {\n      commitOpt = getFirstCommitAndICTAfter(\n        commitOpt.get.version,\n        latestVersion,\n        deltaLog.logPath,\n        deltaLog.store,\n        deltaLog.newDeltaHadoopConf(),\n        commitFileProvider)\n    }\n\n    // Error handling\n    val commit = commitOpt.getOrElse {\n      throw DeltaErrors.noHistoryFound(deltaLog.logPath)\n    }\n    val commitTs = new Timestamp(commit.timestamp)\n    val timestampFormatter = TimestampFormatter(\n      DateTimeUtils.getTimeZone(SQLConf.get.sessionLocalTimeZone))\n    val tsString = DateTimeUtils.timestampToString(\n      timestampFormatter, DateTimeUtils.fromJavaTimestamp(commitTs))\n    if (commit.timestamp > time && !canReturnEarliestCommit) {\n      throw DeltaErrors.TimestampEarlierThanCommitRetentionException(timestamp, commitTs, tsString)\n    } else if (commit.version == latestVersion && !canReturnLastCommit) {\n      if (commit.timestamp < time) {\n        throw DeltaErrors.timestampGreaterThanLatestCommit(timestamp, commitTs, tsString)\n      }\n    }\n    commit\n  }\n\n  /**\n   * Check whether the given version exists.\n   * @param mustBeRecreatable whether the snapshot of this version needs to be recreated.\n   * @param allowOutOfRange whether to allow the version is exceeding the latest snapshot version.\n   */\n  def checkVersionExists(\n      version: Long,\n      catalogTableOpt: Option[CatalogTable],\n      mustBeRecreatable: Boolean = true,\n      allowOutOfRange: Boolean = false): Unit = {\n    val earliest = if (mustBeRecreatable) {\n      getEarliestRecreatableCommit\n    } else {\n      getEarliestDeltaFile(deltaLog)\n    }\n    val latest = deltaLog.update(catalogTableOpt = catalogTableOpt).version\n    if (version < earliest || ((version > latest) && !allowOutOfRange)) {\n      throw VersionNotFoundException(version, earliest, latest)\n    }\n  }\n\n  /**\n   * Searches for the latest commit with the timestamp, which has happened at or before `time` in\n   * the range `[start, end)`.\n   */\n  private def parallelSearch(\n      time: Long,\n      start: Long,\n      end: Long): Commit = {\n    parallelSearch0(\n      spark,\n      getSerializableHadoopConf,\n      deltaLog.logPath.toString,\n      time,\n      start,\n      end,\n      maxKeysPerList)\n  }\n\n  /**\n   * Get the earliest commit, which we can recreate. Note that this version isn't guaranteed to\n   * exist when performing an action as a concurrent operation can delete the file during cleanup.\n   * This value must be used as a lower bound.\n   *\n   * We search for the earliest checkpoint we have, or whether we have the 0th delta file, because\n   * that way we can reconstruct the entire history of the table. This method assumes that the\n   * commits are contiguous.\n   */\n  private[delta] def getEarliestRecreatableCommit: Long = {\n    val files = deltaLog.store.listFrom(\n        FileNames.listingPrefix(deltaLog.logPath, 0),\n        deltaLog.newDeltaHadoopConf())\n      .filter(f => FileNames.isDeltaFile(f) || FileNames.isCheckpointFile(f))\n\n    // A map of checkpoint version and number of parts, to number of parts observed\n    val checkpointMap = new scala.collection.mutable.HashMap[(Long, Int), Int]()\n    var smallestDeltaVersion = Long.MaxValue\n    var lastCompleteCheckpoint: Option[Long] = None\n\n    // Iterate through the log files - this will be in order starting from the lowest version.\n    // Checkpoint files come before deltas, so when we see a checkpoint, we remember it and\n    // return it once we detect that we've seen a smaller or equal delta version.\n    while (files.hasNext) {\n      val nextFilePath = files.next().getPath\n      if (FileNames.isDeltaFile(nextFilePath)) {\n        val version = FileNames.deltaVersion(nextFilePath)\n        if (version == 0L) return version\n        smallestDeltaVersion = math.min(version, smallestDeltaVersion)\n\n        // Note that we also check this condition at the end of the function - we check it\n        // here too to try and avoid more file listing when it's unnecessary.\n        if (lastCompleteCheckpoint.exists(_ >= smallestDeltaVersion - 1)) {\n          return lastCompleteCheckpoint.get\n        }\n      } else if (FileNames.isCheckpointFile(nextFilePath)) {\n        val checkpointVersion = FileNames.checkpointVersion(nextFilePath)\n        val parts = FileNames.numCheckpointParts(nextFilePath)\n        if (parts.isEmpty) {\n          lastCompleteCheckpoint = Some(checkpointVersion)\n        } else {\n          // if we have a multi-part checkpoint, we need to check that all parts exist\n          val numParts = parts.getOrElse(1)\n          val preCount = checkpointMap.getOrElse(checkpointVersion -> numParts, 0)\n          if (numParts == preCount + 1) {\n            lastCompleteCheckpoint = Some(checkpointVersion)\n          }\n          checkpointMap.put(checkpointVersion -> numParts, preCount + 1)\n        }\n      }\n    }\n\n    if (lastCompleteCheckpoint.exists(_ >= smallestDeltaVersion)) {\n      return lastCompleteCheckpoint.get\n    } else if (smallestDeltaVersion < Long.MaxValue) {\n      throw DeltaErrors.noRecreatableHistoryFound(deltaLog.logPath)\n    } else {\n      // For Catalog Owned tables, there are two cases in which a DELTA_NO_COMMITS_FOUND\n      // exception could be thrown:\n      //\n      // 1. If there is no checkpoint or commit 0, and there are only unbackfilled commits\n      //    in the table.\n      //\n      //    In this case, the DELTA_NO_COMMITS_FOUND exception would be incorrect because there\n      //    are commits in the table, we just did not list them when trying to find the earliest\n      //    recreatable commit. Ideally we should throw the above NO_RECREATABLE_HISTORY_FOUND\n      //    exception but that requires an additional lookup of any unbackfilled commits at\n      //    the commit coordinator.\n      //\n      //    It is not worth doing the extra lookup just to throw the correct exception because:\n      //    1) We are already throwing a DELTA_NO_COMMITS_FOUND exception indicating\n      //       a potential problem.\n      //    2) The table must be corrupted already to end up in this scenario.\n      //\n      //    An example delta log structure for this case is shown below:\n      //    ```\n      //      _delta_log/\n      //        [x] 00000000000000000000.json -- Commit 0 has been backfilled but deleted.\n      //        _staged_commits/\n      //          [√] 00000000000000000001.<uuid>.json -- Commit 1 and 2 have *not* been backfilled.\n      //          [√] 00000000000000000002.<uuid>.json\n      //    ```\n      //    For the above table, we will throw DELTA_NO_COMMITS_FOUND exception when commit\n      //    `0.json` has been manually deleted and users are running commands like `versionAsOf 0`\n      //    on the table at the same time.\n      //\n      // 2. It indicates a real NO_RECREATABLE_HISTORY_FOUND exception, if all commits have been\n      //    backfilled, and we still can't find the recreatable commit.\n      //\n      //    An example delta log structure for this case is shown below:\n      //    ```\n      //      _delta_log/\n      //        [x] 00000000000000000000.json -- Commit 0/1/2 have been backfilled but deleted.\n      //        [x] 00000000000000000001.json\n      //        [x] 00000000000000000002.json\n      //        _staged_commits/\n      //          <empty>\n      //    ```\n      throw DeltaErrors.noHistoryFound(deltaLog.logPath)\n    }\n  }\n}\n\n/** Contains many utility methods that can also be executed on Spark executors. */\nobject DeltaHistoryManager extends DeltaLogging {\n\n  /**\n   * This thread pool is used by `getActiveCommitAtTime` to parallelize the search for\n   * relevant commits when the feature inCommitTimestamps is enabled.\n   */\n  private[delta] lazy val threadPool: DeltaThreadPool = DeltaThreadPool(\n      \"delta-history-manager\",\n      SparkEnv.get.conf.get(DeltaSQLConf.DELTA_HISTORY_MANAGER_THREAD_POOL_SIZE)\n    )\n\n  /** Get the persisted commit info (if available) for the given delta file. */\n  def getCommitInfoOpt(\n      logStore: LogStore,\n      deltaFile: Path,\n      hadoopConf: Configuration): Option[CommitInfo] = {\n    val logs = logStore.readAsIterator(deltaFile, hadoopConf)\n    try {\n      logs\n        .map(Action.fromJson)\n        .collectFirst { case c: CommitInfo => c.copy(version = Some(deltaVersion(deltaFile))) }\n    } finally {\n      logs.close()\n    }\n  }\n\n  /**\n   * Get the earliest commit available for this table. Note that this version isn't guaranteed to\n   * exist when performing an action as a concurrent operation can delete the file during cleanup.\n   * This value must be used as a lower bound.\n   */\n  def getEarliestDeltaFile(deltaLog: DeltaLog): Long = {\n    deltaLog.store\n      .listFrom(\n        path = FileNames.listingPrefix(deltaLog.logPath, 0),\n        hadoopConf = deltaLog.newDeltaHadoopConf())\n      .collectFirst { case DeltaFile(_, version) => version }\n      .getOrElse {\n        throw DeltaErrors.noHistoryFound(deltaLog.logPath)\n      }\n  }\n\n  private def getCommitWithInCommitTimestamp(\n      version: Long,\n      commitFileStatus: FileStatus,\n      logStore: LogStore,\n      conf: Configuration): Option[Commit] = {\n    val logs = logStore.readAsIterator(commitFileStatus, conf)\n    try {\n      val ci = logs\n        .map(Action.fromJson)\n        .collectFirst { case c: CommitInfo => c }\n      Some(Commit(version, CommitInfo.getRequiredInCommitTimestamp(ci, version.toString)))\n    } catch {\n      case _: FileNotFoundException =>\n        None\n    } finally {\n      logs.close()\n    }\n  }\n\n  /**\n   * Returns the first available commit in the range [version, upperBoundExclusive).\n   * The timestamp of the returned commit will be the ICT. If no ICT is found for the commit,\n   * an exception will be thrown.\n   * The function optimistically tries to read the commit info for `version` first.\n   * For commits that have been backfilled as per `commitFileProvider`: if the\n   * no commit is found at that version, it falls back to a listing.\n   * For unbackfilled commits, an IllegalStateException is thrown if the commit is not found.\n   */\n  private[delta] def getFirstCommitAndICTAfter(\n      version: Long,\n      upperBoundExclusive: Long,\n      basePath: Path,\n      logStore: LogStore,\n      conf: Configuration,\n      commitFileProvider: DeltaCommitFileProvider): Option[Commit] = {\n    val deltaFile = commitFileProvider.deltaFile(version)\n    val commitInfoOpt = try {\n      getCommitInfoOpt(logStore, deltaFile, conf)\n    } catch {\n      case _: FileNotFoundException => None\n    }\n    if (commitInfoOpt.isDefined) {\n      val timestamp = CommitInfo.getRequiredInCommitTimestamp(commitInfoOpt, version.toString)\n      Some(Commit(version, timestamp))\n    } else if (version >= commitFileProvider.minUnbackfilledVersion) {\n      // Unbackfilled commits should never disappear during the lifetime of time travel\n      // query.\n      throw new IllegalStateException(\n        s\"Could not find commit $version which was expected to be at path ${deltaFile.toString}.\")\n    } else {\n      logStore\n        .listFrom(FileNames.listingPrefix(basePath, version), conf)\n        .takeWhile {\n          fs => FileNames.getFileVersionOpt(fs.getPath).forall(_ < upperBoundExclusive)\n        }\n        .collectFirst { case DeltaFile(f, v) =>\n          getCommitWithInCommitTimestamp(v, f, logStore, conf)\n        }\n        .flatten\n    }\n  }\n\n  /**\n   * Returns the latest commit (with its inCommitTimestamp) that happened at or before\n   * `searchTimestamp` in the range `[startCommit.version, end)`.\n   * If no such commit exists, None is returned.\n   *\n   * The algorithm divides the range into `numChunks` chunks. It then finds the last\n   * chunk where the ICT of its first available commit is less than or equal to `searchTimestamp`.\n   * This chunk is then further divided into `numChunks` chunks and the process is repeated.\n   */\n  private[delta] def getActiveCommitAtTimeFromICTRange(\n      searchTimestamp: Long,\n      startCommit: Commit,\n      end: Long,\n      conf: Configuration,\n      basePath: Path,\n      logStore: LogStore,\n      numChunks: Long,\n      spark: SparkSession,\n      commitFileProvider: DeltaCommitFileProvider): Option[Commit] = {\n    require(startCommit.version < end, \"start must be less than end\")\n    var curStartCommit = startCommit\n    var curEnd = end\n    while (curStartCommit.version < curEnd) {\n      val numVersionsInRange = curEnd - curStartCommit.version\n      val chunkSize = math.max(numVersionsInRange / numChunks, 1)\n\n      // min(chunkSize) = 1 and curStartCommit.version < end\n      // therefore, getChunkEnd(chunkStart) will always be > chunkStart\n      def getChunkEnd(chunkStart: Long): Long = math.min(chunkStart + chunkSize, curEnd)\n\n      val chunkStartICTFutures =\n        (curStartCommit.version until curEnd by chunkSize).map { chunkStart =>\n          if (chunkStart == curStartCommit.version) {\n            CompletableFuture.completedFuture(Option(curStartCommit))\n          } else {\n            threadPool.submit(spark) {\n              getFirstCommitAndICTAfter(\n                chunkStart,\n                upperBoundExclusive = getChunkEnd(chunkStart),\n                basePath,\n                logStore,\n                conf,\n                commitFileProvider\n              )\n            }\n          }\n        }\n      val knownTightestLowerBoundCommit = chunkStartICTFutures\n        .map(ThreadUtils.awaitResult(_, Duration.Inf))\n        .takeWhile(_.forall(_.timestamp <= searchTimestamp))\n        .flatten\n        .lastOption\n        .getOrElse {\n          return None\n        }\n      val nextStartCommit = knownTightestLowerBoundCommit\n      val nextEnd = getChunkEnd(nextStartCommit.version)\n      if (nextStartCommit.version + 2 > nextEnd ||\n          knownTightestLowerBoundCommit.timestamp == searchTimestamp) {\n        return Some(knownTightestLowerBoundCommit)\n      }\n      curStartCommit = nextStartCommit\n      curEnd = nextEnd\n    }\n    None\n  }\n\n  /**\n   * When calling getCommits, the initial few timestamp values may be wrong because they are not\n   * properly monotonized. Callers should pass a start value at least\n   * this far behind the first timestamp they care about if they need correct values.\n   */\n  private[delta] val POTENTIALLY_UNMONOTONIZED_TIMESTAMPS = 100\n\n  /**\n   * Returns the commit version and timestamps of all commits in `[start, end)`. If `end` is not\n   * specified, will return all commits that exist after `start`. Will guarantee that the commits\n   * returned will have both monotonically increasing versions as well as timestamps.\n   * Note that this function will return non-ICT timestamps even for commits where\n   * InCommitTimestamps are enabled. The caller is responsible for ensuring that the appropriate\n   * timestamps are used.\n   */\n  private[delta] def getCommitsWithNonIctTimestamps(\n      logStore: LogStore,\n      logPath: Path,\n      start: Long,\n      end: Option[Long],\n      hadoopConf: Configuration): Array[Commit] = {\n    val until = end.getOrElse(Long.MaxValue)\n    val commits =\n      logStore\n        .listFrom(listingPrefix(logPath, start), hadoopConf)\n        .collect { case DeltaFile(file, version) => Commit(version, file.getModificationTime) }\n        .takeWhile(_.version < until)\n\n    monotonizeCommitTimestamps(commits.toArray)\n  }\n\n  /**\n   * Makes sure that the commit timestamps are monotonically increasing with respect to commit\n   * versions. Requires the input commits to be sorted by the commit version.\n   */\n  private def monotonizeCommitTimestamps[T <: CommitMarker](commits: Array[T]): Array[T] = {\n    var i = 0\n    val length = commits.length\n    while (i < length - 1) {\n      val prevTimestamp = commits(i).getTimestamp\n      assert(commits(i).getVersion < commits(i + 1).getVersion, \"Unordered commits provided.\")\n      if (prevTimestamp >= commits(i + 1).getTimestamp) {\n        logWarning(log\"Found Delta commit ${MDC(DeltaLogKeys.VERSION, commits(i).getVersion)} \" +\n          log\"with a timestamp ${MDC(DeltaLogKeys.TIMESTAMP, prevTimestamp)} \" +\n          log\"which is greater than the next commit timestamp \" +\n          log\"${MDC(DeltaLogKeys.TIMESTAMP2, commits(i + 1).getTimestamp)}.\")\n        commits(i + 1) = commits(i + 1).withTimestamp(prevTimestamp + 1).asInstanceOf[T]\n      }\n      i += 1\n    }\n    commits\n  }\n\n  /**\n   * Searches for the latest commit with the timestamp, which has happened at or before `time` in\n   * the range `[start, end)`. The algorithm works as follows:\n   *  1. We use Spark to list our commit history in parallel `maxKeysPerList` at a time.\n   *  2. We then perform our search in each fragment of commits containing at most `maxKeysPerList`\n   *     elements.\n   *  3. All fragments that are before `time` will return the last commit in the fragment.\n   *  4. All fragments that are after `time` will exit early and return the first commit in the\n   *     fragment.\n   *  5. The fragment that contains the version we are looking for will return the version we are\n   *     looking for.\n   *  6. Once all the results are returned from Spark, we make sure that the commit timestamps are\n   *     monotonically increasing across the fragments, because we couldn't adjust for the\n   *     boundaries when working in parallel.\n   *  7. We then return the version we are looking for in this smaller list on the Driver.\n   * We will return the first available commit if the condition cannot be met. This method works\n   * even for boundary commits, and can be best demonstrated through an example:\n   * Imagine we have commits 999, 1000, 1001, 1002. t_999 < t_1000 but t_1000 > t_1001 and\n   * t_1001 < t_1002. So at the the boundary, we will need to eventually adjust t_1001. Assume the\n   * result needs to be t_1001 after the adjustment as t_search < t_1002 and t_search > t_1000.\n   * What will happen is that the first fragment will return t_1000, and the second fragment will\n   * return t_1001. On the Driver, we will adjust t_1001 = t_1000 + 1 milliseconds, and our linear\n   * search will return t_1001.\n   *\n   * Placed in the static object to avoid serializability issues.\n   *\n   * @param spark The active SparkSession\n   * @param conf The session specific Hadoop Configuration\n   * @param logPath The path of the DeltaLog\n   * @param time The timestamp to search for in milliseconds\n   * @param start Earliest available commit version (approximate is acceptable)\n   * @param end Latest available commit version (approximate is acceptable)\n   * @param step The number with which to chunk each linear search across commits. Provide the\n   *             max number of keys returned by the underlying FileSystem for in a single RPC for\n   *             best results.\n   */\n  private def parallelSearch0(\n      spark: SparkSession,\n      conf: SerializableConfiguration,\n      logPath: String,\n      time: Long,\n      start: Long,\n      end: Long,\n      step: Long): Commit = {\n    import org.apache.spark.sql.delta.implicits._\n    val possibleCommits = spark.range(start, end, step).mapPartitions { startVersions =>\n      val logStore = LogStore(SparkEnv.get.conf, conf.value)\n      val basePath = new Path(logPath)\n      startVersions.map { startVersion =>\n        val commits = getCommitsWithNonIctTimestamps(\n          logStore,\n          basePath,\n          startVersion,\n          Some(math.min(startVersion + step, end)),\n          conf.value)\n        if (commits.isEmpty) {\n          None\n        } else {\n          Some(lastCommitBeforeTimestamp(commits, time).getOrElse(commits.head))\n        }\n      }\n    }.collect()\n\n    // Spark should return the commits in increasing order as well\n    val commitList = monotonizeCommitTimestamps(possibleCommits.flatten)\n    if (commitList.isEmpty) {\n      throw DeltaErrors.noHistoryFound(new Path(logPath))\n    }\n    lastCommitBeforeTimestamp(commitList, time).getOrElse(commitList.head)\n  }\n\n  /** Returns the latest commit that happened at or before `time`. */\n  private def lastCommitBeforeTimestamp(commits: Seq[Commit], time: Long): Option[Commit] = {\n    val i = commits.lastIndexWhere(_.timestamp <= time)\n    if (i < 0) None else Some(commits(i))\n  }\n\n  /** A helper class to represent the timestamp and version of a commit. */\n  case class Commit(version: Long, timestamp: Long) extends CommitMarker {\n    override def withTimestamp(timestamp: Long): Commit = this.copy(timestamp = timestamp)\n\n    override def getTimestamp: Long = timestamp\n\n    override def getVersion: Long = version\n  }\n\n  /**\n   * An iterator that helps select old log files for deletion. It takes the input iterator of log\n   * files from the earliest file, and returns should-be-deleted files until the given maxTimestamp\n   * or maxVersion to delete is reached. Note that this iterator may stop deleting files earlier\n   * than maxTimestamp or maxVersion if it finds that files that need to be preserved for adjusting\n   * the timestamps of subsequent files. Let's go through an example. Assume the following commit\n   * history:\n   *\n   * +---------+-----------+--------------------+\n   * | Version | Timestamp | Adjusted Timestamp |\n   * +---------+-----------+--------------------+\n   * |       0 |         0 |                  0 |\n   * |       1 |         5 |                  5 |\n   * |       2 |        10 |                 10 |\n   * |       3 |         7 |                 11 |\n   * |       4 |         8 |                 12 |\n   * |       5 |        14 |                 14 |\n   * +---------+-----------+--------------------+\n   *\n   * As you can see from the example, we require timestamps to be monotonically increasing with\n   * respect to the version of the commit, and each commit to have a unique timestamp. If we have\n   * a commit which doesn't obey one of these two requirements, we adjust the timestamp of that\n   * commit to be one millisecond greater than the previous commit.\n   *\n   * Given the above commit history, the behavior of this iterator will be as follows:\n   *  - For maxVersion = 1 and maxTimestamp = 9, we can delete versions 0 and 1\n   *  - Until we receive maxVersion >= 4 and maxTimestamp >= 12, we can't delete versions 2 and 3.\n   *    This is because version 2 is used to adjust the timestamps of commits up to version 4.\n   *  - For maxVersion >= 5 and maxTimestamp >= 14 we can delete everything\n   * The semantics of time travel guarantee that for a given timestamp, the user will ALWAYS get the\n   * same version. Consider a user asks to get the version at timestamp 11. If all files are there,\n   * we would return version 3 (timestamp 11) for this query. If we delete versions 0-2, the\n   * original timestamp of version 3 (7) will not have an anchor to adjust on, and if the time\n   * travel query is re-executed we would return version 4. This is the motivation behind this\n   * iterator implementation.\n   *\n   * The implementation maintains an internal \"maybeDelete\" buffer of files that we are unsure of\n   * deleting because they may be necessary to adjust time of future files. For each file we get\n   * from the underlying iterator, we check whether it needs time adjustment or not. If it does need\n   * time adjustment, then we cannot immediately decide whether it is safe to delete that file or\n   * not and therefore we put it in each the buffer. Then we iteratively peek ahead at the future\n   * files and accordingly decide whether to delete all the buffered files or retain them.\n   *\n   * @param underlying The iterator which gives the list of files in ascending version order\n   * @param maxTimestamp The timestamp until which we can delete (inclusive).\n   * @param maxVersion The version until which we can delete (inclusive).\n   * @param versionGetter A method to get the commit version from the file path.\n   */\n  class BufferingLogDeletionIterator(\n      underlying: Iterator[FileStatus],\n      maxTimestamp: Long,\n      maxVersion: Long,\n      versionGetter: Path => Long) extends Iterator[FileStatus] {\n    /**\n     * Our output iterator\n     */\n    private val filesToDelete = new mutable.Queue[FileStatus]()\n    /**\n     * Our intermediate buffer which will buffer files as long as the last file requires a timestamp\n     * adjustment.\n     */\n    private val maybeDeleteFiles = new mutable.ArrayBuffer[FileStatus]()\n    private var lastFile: FileStatus = _\n    private var hasNextCalled: Boolean = false\n    // A map to keep track of multi-part checkpoints.\n    val checkpointMap = new scala.collection.mutable.HashMap[(Long, Int),\n      collection.mutable.Buffer[FileStatus]]()\n\n    private def init(): Unit = {\n      if (underlying.hasNext) {\n        lastFile = underlying.next()\n        maybeDeleteFiles.append(lastFile)\n      }\n    }\n\n    init()\n\n    /** Whether the given file can be deleted based on the version and retention timestamp input. */\n    private def shouldDeleteFile(file: FileStatus): Boolean = {\n      file.getModificationTime <= maxTimestamp && versionGetter(file.getPath) <= maxVersion\n    }\n\n    /**\n     * Files need a time adjustment if their timestamp isn't later than the lastFile.\n     */\n    private def needsTimeAdjustment(file: FileStatus): Boolean = {\n      versionGetter(lastFile.getPath) < versionGetter(file.getPath) &&\n        lastFile.getModificationTime >= file.getModificationTime\n    }\n\n    /**\n     * Enqueue the files in the buffer if the last file is safe to delete. Clears the buffer.\n     */\n    private def flushBuffer(): Unit = {\n      if (maybeDeleteFiles.lastOption.exists(shouldDeleteFile)) {\n        filesToDelete ++= maybeDeleteFiles\n      }\n      maybeDeleteFiles.clear()\n    }\n\n    /**\n     * Peeks at the next file in the iterator. Based on the next file we can have three\n     * possible outcomes:\n     * - The underlying iterator returned a file, which doesn't require timestamp adjustment. If\n     *   the file in the buffer has expired, flush the buffer to our output queue.\n     * - The underlying iterator returned a file, which requires timestamp adjustment. In this case,\n     *   we add this file to the buffer and fetch the next file\n     * - The underlying iterator is empty. In this case, we check the last file in the buffer. If\n     *   it has expired, then flush the buffer to the output queue.\n     * Once this method returns, the buffer is expected to have 1 file (last file of the\n     * underlying iterator) unless the underlying iterator is fully consumed.\n     */\n    private def queueFilesInBuffer(): Unit = {\n      var continueBuffering = true\n      while (continueBuffering && underlying.hasNext) {\n        var currentFile = underlying.next()\n        require(currentFile != null, \"FileStatus iterator returned null\")\n        if (needsTimeAdjustment(currentFile)) {\n          currentFile = new FileStatus(\n            currentFile.getLen, currentFile.isDirectory, currentFile.getReplication,\n            currentFile.getBlockSize, lastFile.getModificationTime + 1, currentFile.getPath)\n          maybeDeleteFiles.append(currentFile)\n        } else if (FileNames.isCheckpointFile(currentFile) && currentFile.getLen > 0) {\n          // Only flush the buffer when we find a checkpoint. This is because we don't want to\n          // delete the delta log files unless we have a checkpoint to ensure that non-expired\n          // subsequent delta logs are valid.\n          val numParts = FileNames.numCheckpointParts(currentFile.getPath)\n\n          if (numParts.isEmpty) { // Single-part or V2\n            flushBuffer()\n            maybeDeleteFiles.append(currentFile)\n            continueBuffering = false\n          } else {\n            // Multi-part checkpoint\n            val mpKey = versionGetter(currentFile.getPath) -> numParts.get\n            val partBuffer = checkpointMap.getOrElse(mpKey, mutable.ArrayBuffer())\n            partBuffer.append(currentFile)\n            checkpointMap.put(mpKey, partBuffer)\n            if (numParts.get == partBuffer.size) {\n              flushBuffer()\n              partBuffer.foreach(f => maybeDeleteFiles.append(f))\n              checkpointMap.remove(mpKey)\n              continueBuffering = false\n            }\n          }\n        } else {\n          maybeDeleteFiles.append(currentFile)\n        }\n        lastFile = currentFile\n      }\n    }\n\n    override def hasNext: Boolean = {\n      hasNextCalled = true\n      if (filesToDelete.isEmpty) queueFilesInBuffer()\n      filesToDelete.nonEmpty\n    }\n\n    override def next(): FileStatus = {\n      if (!hasNextCalled) throw new NoSuchElementException()\n      hasNextCalled = false\n      filesToDelete.dequeue()\n    }\n  }\n}\n\n/**\n * class describing the output schema of\n * [[org.apache.spark.sql.delta.commands.DescribeDeltaHistoryCommand]]\n */\ncase class DeltaHistory(\n    version: Option[Long],\n    timestamp: Timestamp,\n    userId: Option[String],\n    userName: Option[String],\n    operation: String,\n    operationParameters: Map[String, String],\n    job: Option[JobInfo],\n    notebook: Option[NotebookInfo],\n    clusterId: Option[String],\n    readVersion: Option[Long],\n    isolationLevel: Option[String],\n    isBlindAppend: Option[Boolean],\n    operationMetrics: Option[Map[String, String]],\n    userMetadata: Option[String],\n    engineInfo: Option[String]) extends CommitMarker {\n\n  override def withTimestamp(timestamp: Long): DeltaHistory = {\n    this.copy(timestamp = new Timestamp(timestamp))\n  }\n\n  override def getTimestamp: Long = timestamp.getTime\n\n  override def getVersion: Long = version.get\n}\n\nobject DeltaHistory {\n  /** Create an instance of [[DeltaHistory]] from [[CommitInfo]] */\n  def fromCommitInfo(ci: CommitInfo): DeltaHistory = {\n    val operationParameters =\n      CommitInfo.getLegacyPostDeserializationOperationParameters(ci.operationParameters)\n    DeltaHistory(\n      version = ci.version,\n      timestamp = ci.timestamp,\n      userId = ci.userId,\n      userName = ci.userName,\n      operation = ci.operation,\n      operationParameters = operationParameters,\n      job = ci.job,\n      notebook = ci.notebook,\n      clusterId = ci.clusterId,\n      readVersion = ci.readVersion,\n      isolationLevel = ci.isolationLevel,\n      isBlindAppend = ci.isBlindAppend,\n      operationMetrics = ci.operationMetrics,\n      userMetadata = ci.userMetadata,\n      engineInfo = ci.engineInfo)\n  }\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaLog.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.IOException\nimport java.lang.ref.WeakReference\nimport java.net.URI\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\nimport com.databricks.spark.util.TagDefinitions._\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.WriteIntoDelta\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsUtils\nimport org.apache.spark.sql.delta.files.{TahoeBatchFileIndex, TahoeLogFileIndex}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.redirect.RedirectFeature\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.sources._\nimport org.apache.spark.sql.delta.storage.LogStoreProvider\nimport org.apache.spark.sql.delta.util.{FileNames, PathWithFileSystem, Utils => DeltaUtils}\nimport com.google.common.cache.{Cache, CacheBuilder, RemovalNotification}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, Path}\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.{FileSourceOptions, TableIdentifier}\nimport org.apache.spark.sql.catalyst.analysis.{Resolver, UnresolvedAttribute}\nimport org.apache.spark.sql.catalyst.catalog.{BucketSpec, CatalogStatistics, CatalogTable}\nimport org.apache.spark.sql.catalyst.expressions.{And, Attribute, Cast, Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical.AnalysisHelper\nimport org.apache.spark.sql.catalyst.util.FailFastMode\nimport org.apache.spark.sql.execution.datasources._\nimport org.apache.spark.sql.expressions.UserDefinedFunction\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.sources.{BaseRelation, InsertableRelation}\nimport org.apache.spark.sql.types.{StructField, StructType}\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\nimport org.apache.spark.util._\n\n/**\n * Used to query the current state of the log as well as modify it by adding\n * new atomic collections of actions.\n *\n * Internally, this class implements an optimistic concurrency control\n * algorithm to handle multiple readers or writers. Any single read\n * is guaranteed to see a consistent snapshot of the table.\n *\n * @param logPath Path of the Delta log JSONs.\n * @param dataPath Path of the data files.\n * @param options Filesystem options filtered from `allOptions`.\n * @param allOptions All options provided by the user, for example via `df.write.option()`. This\n *                   includes but not limited to filesystem and table properties.\n * @param clock Clock to be used when starting a new transaction.\n * @param initialCatalogTable The catalog table given when the log is initialized.\n */\nclass DeltaLog private(\n    val logPath: Path,\n    val dataPath: Path,\n    val options: Map[String, String],\n    val allOptions: Map[String, String],\n    val clock: Clock,\n    val initialCatalogTable: Option[CatalogTable]\n  ) extends Checkpoints\n  with MetadataCleanup\n  with LogStoreProvider\n  with SnapshotManagement\n  with DeltaFileFormat\n  with ProvidesUniFormConverters\n  with ReadChecksum {\n\n  import org.apache.spark.sql.delta.files.TahoeFileIndex\n  import org.apache.spark.sql.delta.util.FileNames._\n\n  /**\n   * Path to sidecar directory.\n   * This is intentionally kept `lazy val` as otherwise any other constructor codepaths in DeltaLog\n   * (e.g. SnapshotManagement etc) will see it as null as they are executed before this line is\n   * called.\n   */\n  lazy val sidecarDirPath: Path = FileNames.sidecarDirPath(logPath)\n\n\n  protected def spark = SparkSession.active\n\n  checkRequiredConfigurations()\n\n  /**\n   * Keep a reference to `SparkContext` used to create `DeltaLog`. `DeltaLog` cannot be used when\n   * `SparkContext` is stopped. We keep the reference so that we can check whether the cache is\n   * still valid and drop invalid `DeltaLog`` objects.\n   */\n  private val sparkContext = new WeakReference(spark.sparkContext)\n\n  /**\n   * Returns the Hadoop [[Configuration]] object which can be used to access the file system. All\n   * Delta code should use this method to create the Hadoop [[Configuration]] object, so that the\n   * hadoop file system configurations specified in DataFrame options will come into effect.\n   */\n  // scalastyle:off deltahadoopconfiguration\n  final def newDeltaHadoopConf(): Configuration =\n    spark.sessionState.newHadoopConfWithOptions(options)\n  // scalastyle:on deltahadoopconfiguration\n\n  /** Used to read and write physical log files and checkpoints. */\n  lazy val store = createLogStore(spark)\n\n  /** Delta History Manager containing version and commit history. */\n  lazy val history = new DeltaHistoryManager(\n    this, spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_HISTORY_PAR_SEARCH_THRESHOLD))\n\n  /** Initialize the variables in SnapshotManagement. */\n  createSnapshotAtInit(initialCatalogTable)\n\n  /* --------------- *\n   |  Configuration  |\n   * --------------- */\n\n  /**\n   * The max lineage length of a Snapshot before Delta forces to build a Snapshot from scratch.\n   * Delta will build a Snapshot on top of the previous one if it doesn't see a checkpoint.\n   * However, there is a race condition that when two writers are writing at the same time,\n   * a writer may fail to pick up checkpoints written by another one, and the lineage will grow\n   * and finally cause StackOverflowError. Hence we have to force to build a Snapshot from scratch\n   * when the lineage length is too large to avoid hitting StackOverflowError.\n   */\n  def maxSnapshotLineageLength: Int =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_MAX_SNAPSHOT_LINEAGE_LENGTH)\n\n  private[delta] def incrementalCommitEnabled: Boolean = {\n    spark.conf.get(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED)\n  }\n\n  private[delta] def shouldVerifyIncrementalCommit: Boolean = {\n    spark.conf.get(DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY) ||\n      (DeltaUtils.isTesting\n        && spark.conf.get(DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS))\n  }\n\n  /**\n   * The unique identifier for this table.\n   *\n   * WARNING: This value is volatile and can change during the lifetime of a DeltaLog instance,\n   * e.g., when the snapshot is updated and the new snapshot has a different table id. Use with\n   * care.\n   */\n  def unsafeVolatileTableId: String = unsafeVolatileMetadata.id\n\n  /** Returns the truncated table ID for logging purposes. */\n  private[delta] def truncatedUnsafeVolatileTableId: String =\n    unsafeVolatileTableId.split(\"-\").head\n\n  /**\n   * WARNING: This API is unsafe and deprecated. It will be removed in future versions.\n   * Use the above unsafeVolatileTableId to get the most recently cached table id.\n   */\n  @deprecated(\"This method is deprecated and will be removed in future versions. \" +\n    \"Use unsafeVolatileTableId instead\", \"18.0\")\n  def tableId: String = unsafeVolatileTableId\n\n  def getInitialCatalogTable: Option[CatalogTable] = initialCatalogTable\n  /**\n   * Combines the table id with the path of the table to ensure uniqueness. Normally the table id\n   * should be globally unique, but nothing stops users from copying a Delta table directly to\n   * a separate location, where the transaction log is copied directly, causing the table ids to\n   * match. When users mutate the copied table, and then try to perform some checks joining the\n   * two tables, optimizations that depend on the table id alone may not be correct. Hence we use a\n   * composite id.\n   */\n  private[delta] def compositeId: (String, Path) = unsafeVolatileTableId -> dataPath\n\n  /**\n   * Creates a [[LogicalRelation]] for a given [[DeltaLogFileIndex]], with all necessary file source\n   * options taken from the Delta Log. All reads of Delta metadata files should use this method.\n   */\n  def indexToRelation(\n      index: DeltaLogFileIndex,\n      schema: StructType = Action.logSchema): LogicalRelation = {\n    DeltaLog.indexToRelation(spark, index, options, schema)\n  }\n\n  /**\n   * Load the data using the FileIndex. This allows us to skip many checks that add overhead, e.g.\n   * file existence checks, partitioning schema inference.\n   */\n  def loadIndex(\n      index: DeltaLogFileIndex,\n      schema: StructType = Action.logSchema): DataFrame = {\n    DataFrameUtils.ofRows(spark, indexToRelation(index, schema))\n  }\n\n  /* ------------------ *\n   |  Delta Management  |\n   * ------------------ */\n\n  /**\n   * Returns a new [[OptimisticTransaction]] that can be used to read the current state of the log\n   * and then commit updates. The reads and updates will be checked for logical conflicts with any\n   * concurrent writes to the log, and post-commit hooks can be used to notify the table's catalog\n   * of schema changes, etc.\n   *\n   * Note that all reads in a transaction must go through the returned transaction object, and not\n   * directly to the [[DeltaLog]] otherwise they will not be checked for conflicts.\n   *\n   * @param catalogTableOpt The [[CatalogTable]] for the table this transaction updates. Passing\n   * None asserts this is a path-based table with no catalog entry.\n   *\n   * @param snapshotOpt THe [[Snapshot]] this transaction should use, if not latest.\n   */\n  def startTransaction(\n      catalogTableOpt: Option[CatalogTable],\n      snapshotOpt: Option[Snapshot] = None): OptimisticTransaction = {\n    TransactionExecutionObserver.getObserver.startingTransaction {\n      new OptimisticTransaction(this, catalogTableOpt, snapshotOpt)\n    }\n  }\n\n  /** Legacy/compat overload that does not require catalog table information. Avoid prod use. */\n  @deprecated(\"Please use the CatalogTable overload instead\", \"3.0\")\n  def startTransaction(): OptimisticTransaction = {\n    startTransaction(catalogTableOpt = None, snapshotOpt = None)\n  }\n\n  /**\n   * Execute a piece of code within a new [[OptimisticTransaction]]. Reads/write sets will\n   * be recorded for this table, and all other tables will be read\n   * at a snapshot that is pinned on the first access.\n   *\n   * @param catalogTableOpt The [[CatalogTable]] for the table this transaction updates. Passing\n   * None asserts this is a path-based table with no catalog entry.\n   *\n   * @param snapshotOpt THe [[Snapshot]] this transaction should use, if not latest.\n   * @note This uses thread-local variable to make the active transaction visible. So do not use\n   *       multi-threaded code in the provided thunk.\n   */\n  def withNewTransaction[T](\n      catalogTableOpt: Option[CatalogTable],\n      snapshotOpt: Option[Snapshot] = None)(\n      thunk: OptimisticTransaction => T): T = {\n    val txn = startTransaction(catalogTableOpt, snapshotOpt)\n    OptimisticTransaction.setActive(txn)\n    try {\n      thunk(txn)\n    } finally {\n      OptimisticTransaction.clearActive()\n    }\n  }\n\n  /** Legacy/compat overload that does not require catalog table information. Avoid prod use. */\n  @deprecated(\"Please use the CatalogTable overload instead\", \"3.0\")\n  def withNewTransaction[T](thunk: OptimisticTransaction => T): T = {\n    val txn = startTransaction()\n    OptimisticTransaction.setActive(txn)\n    try {\n      thunk(txn)\n    } finally {\n      OptimisticTransaction.clearActive()\n    }\n  }\n\n\n  /**\n   * Upgrade the table's protocol version, by default to the maximum recognized reader and writer\n   * versions in this Delta release. This method only upgrades protocol version, and will fail if\n   * the new protocol version is not a superset of the original one used by the snapshot.\n   */\n  def upgradeProtocol(\n      catalogTable: Option[CatalogTable],\n      snapshot: Snapshot,\n      newVersion: Protocol): Unit = {\n    val currentVersion = snapshot.protocol\n    if (newVersion == currentVersion) {\n      logConsole(s\"Table $dataPath is already at protocol version $newVersion.\")\n      return\n    }\n    if (!currentVersion.canUpgradeTo(newVersion)) {\n      throw new ProtocolDowngradeException(currentVersion, newVersion)\n    }\n\n    val txn = startTransaction(catalogTable, Some(snapshot))\n    try {\n      SchemaMergingUtils.checkColumnNameDuplication(txn.metadata.schema, \"in the table schema\")\n    } catch {\n      case e: AnalysisException =>\n        throw DeltaErrors.duplicateColumnsOnUpdateTable(e)\n    }\n    txn.commit(Seq(newVersion), DeltaOperations.UpgradeProtocol(newVersion))\n    logConsole(s\"Upgraded table at $dataPath to $newVersion.\")\n  }\n\n  /**\n   * Get all actions starting from \"startVersion\" (inclusive). If `startVersion` doesn't exist,\n   * return an empty Iterator.\n   * Callers are encouraged to use the other override which takes the endVersion if available to\n   * avoid I/O and improve performance of this method.\n   */\n  def getChanges(\n      startVersion: Long,\n      catalogTableOpt: Option[CatalogTable] = None,\n      failOnDataLoss: Boolean = false): Iterator[(Long, Seq[Action])] = {\n    getChangeLogFiles(\n        startVersion, catalogTableOpt, failOnDataLoss).map { case (version, status) =>\n      (version, store.read(status, newDeltaHadoopConf()).map(Action.fromJson(_)))\n    }\n  }\n\n  private[sql] def getChanges(\n      startVersion: Long,\n      endVersion: Long,\n      catalogTableOpt: Option[CatalogTable],\n      failOnDataLoss: Boolean): Iterator[(Long, Seq[Action])] = {\n    getChangeLogFiles(\n        startVersion,\n        endVersion,\n        catalogTableOpt,\n        failOnDataLoss).map { case (version, status) =>\n      (version, store.read(status, newDeltaHadoopConf()).map(Action.fromJson(_)))\n    }\n  }\n\n  private[sql] def getChangeLogFiles(\n      startVersion: Long,\n      endVersion: Long,\n      catalogTableOpt: Option[CatalogTable],\n      failOnDataLoss: Boolean): Iterator[(Long, FileStatus)] = {\n    implicit class IteratorWithStopAtHelper[T](underlying: Iterator[T]) {\n      // This method is used to stop the iterator when the condition is met.\n      def stopAt(stopAtFunc: (T) => Boolean): Iterator[T] = new Iterator[T] {\n        var shouldStop = false\n\n        override def hasNext: Boolean = !shouldStop && underlying.hasNext\n\n        override def next(): T = {\n          val v = underlying.next()\n          shouldStop = stopAtFunc(v)\n          v\n        }\n      }\n    }\n\n    getChangeLogFiles(startVersion, catalogTableOpt, failOnDataLoss)\n      // takeWhile always looks at one extra item, which can trigger unnecessary work. Instead, we\n      // stop if we've seen the item we believe should be the last interesting item, without\n      // examining the one that follows.\n      .stopAt { case (version, _) => version >= endVersion }\n      // The last element in this iterator may not be <= endVersion, so we need to filter it out.\n      .takeWhile { case (version, _) => version <= endVersion }\n  }\n\n  /**\n   * Get access to all actions starting from \"startVersion\" (inclusive) via [[FileStatus]].\n   * If `startVersion` doesn't exist, return an empty Iterator.\n   * Callers are encouraged to use the other override which takes the endVersion if available to\n   * avoid I/O and improve performance of this method.\n   */\n  def getChangeLogFiles(\n      startVersion: Long,\n      catalogTableOpt: Option[CatalogTable] = None,\n      failOnDataLoss: Boolean = false): Iterator[(Long, FileStatus)] = {\n    val deltasWithVersion = CoordinatedCommitsUtils.commitFilesIterator(\n      this, catalogTableOpt, startVersion)\n    // Subtract 1 to ensure that we have the same check for the inclusive startVersion\n    var lastSeenVersion = startVersion - 1\n    deltasWithVersion.map { case (status, version) =>\n      if (failOnDataLoss && version > lastSeenVersion + 1) {\n        throw DeltaErrors.failOnDataLossException(lastSeenVersion + 1, version)\n      }\n      lastSeenVersion = version\n      (version, status)\n    }\n  }\n\n  /* --------------------- *\n   |  Protocol validation  |\n   * --------------------- */\n\n  /**\n   * Asserts the highest protocol supported by this client is not less than what required by the\n   * table for performing read or write operations. This ensures the client to support a\n   * greater-or-equal protocol versions and recognizes/supports all features enabled by the table.\n   *\n   * The operation type to be checked is passed as a string in `readOrWrite`. Valid values are\n   * `read` and `write`.\n   */\n  private def protocolCheck(tableProtocol: Protocol, readOrWrite: String): Unit = {\n    val unsupportedTestFeatures =\n      if (spark.conf.get(DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED)) {\n        TableFeature.testUnsupportedFeatures.toSeq\n      } else {\n        Seq.empty\n      }\n\n    val clientSupportedProtocol =\n      Action.supportedProtocolVersion(featuresToExclude = unsupportedTestFeatures)\n    // Depending on the operation, pull related protocol versions out of Protocol objects.\n    // `getEnabledFeatures` is a pointer to pull reader/writer features out of a Protocol.\n    val (clientSupportedVersions, tableRequiredVersion, getEnabledFeatures) = readOrWrite match {\n      case \"read\" => (\n        Action.supportedReaderVersionNumbers,\n        tableProtocol.minReaderVersion,\n        (f: Protocol) => f.readerFeatureNames)\n      case \"write\" => (\n        Action.supportedWriterVersionNumbers,\n        tableProtocol.minWriterVersion,\n        (f: Protocol) => f.writerFeatureNames)\n      case _ =>\n        throw new IllegalArgumentException(\"Table operation must be either `read` or `write`.\")\n    }\n\n    // Check is complete when both the protocol version and all referenced features are supported.\n    val clientSupportedFeatureNames = getEnabledFeatures(clientSupportedProtocol)\n    val tableEnabledFeatureNames = getEnabledFeatures(tableProtocol)\n    if (tableEnabledFeatureNames.subsetOf(clientSupportedFeatureNames) &&\n      clientSupportedVersions.contains(tableRequiredVersion)) {\n      return\n    }\n\n    // Otherwise, either the protocol version, or few features referenced by the table, is\n    // unsupported.\n    val clientUnsupportedFeatureNames =\n      tableEnabledFeatureNames.diff(clientSupportedFeatureNames)\n    // Prepare event log constants and the appropriate error message handler.\n    val (opType, versionKey, unsupportedFeaturesException) = readOrWrite match {\n      case \"read\" => (\n          \"delta.protocol.failure.read\",\n          \"minReaderVersion\",\n          DeltaErrors.unsupportedReaderTableFeaturesInTableException _)\n      case \"write\" => (\n          \"delta.protocol.failure.write\",\n          \"minWriterVersion\",\n          DeltaErrors.unsupportedWriterTableFeaturesInTableException _)\n    }\n    recordDeltaEvent(\n      this,\n      opType,\n      data = Map(\n        \"clientVersion\" -> clientSupportedVersions.max,\n        versionKey -> tableRequiredVersion,\n        \"clientFeatures\" -> clientSupportedFeatureNames.mkString(\",\"),\n        \"clientUnsupportedFeatures\" -> clientUnsupportedFeatureNames.mkString(\",\")))\n    if (!clientSupportedVersions.contains(tableRequiredVersion)) {\n      throw new InvalidProtocolVersionException(\n        dataPath.toString(),\n        tableProtocol.minReaderVersion,\n        tableProtocol.minWriterVersion,\n        Action.supportedReaderVersionNumbers.toSeq,\n        Action.supportedWriterVersionNumbers.toSeq)\n    } else {\n      throw unsupportedFeaturesException(dataPath.toString(), clientUnsupportedFeatureNames)\n    }\n  }\n\n  /**\n   * Asserts that the table's protocol enabled all features that are active in the metadata.\n   *\n   * A mismatch shouldn't happen when the table has gone through a proper write process because we\n   * require all active features during writes. However, other clients may void this guarantee.\n   */\n  def assertTableFeaturesMatchMetadata(\n      targetProtocol: Protocol,\n      targetMetadata: Metadata): Unit = {\n    if (!targetProtocol.supportsReaderFeatures && !targetProtocol.supportsWriterFeatures) return\n\n    val protocolEnabledFeatures = targetProtocol.writerFeatureNames\n      .flatMap(TableFeature.featureNameToFeature)\n    val activeFeatures =\n      Protocol.extractAutomaticallyEnabledFeatures(spark, targetMetadata, targetProtocol)\n    val activeButNotEnabled = activeFeatures.diff(protocolEnabledFeatures)\n    if (activeButNotEnabled.nonEmpty) {\n      throw DeltaErrors.tableFeatureMismatchException(activeButNotEnabled.map(_.name))\n    }\n  }\n\n  /**\n   * Asserts that the client is up to date with the protocol and allowed to read the table that is\n   * using the given `protocol`.\n   */\n  def protocolRead(protocol: Protocol): Unit = {\n    protocolCheck(protocol, \"read\")\n  }\n\n  /**\n   * Asserts that the client is up to date with the protocol and allowed to write to the table\n   * that is using the given `protocol`.\n   */\n  def protocolWrite(protocol: Protocol): Unit = {\n    protocolCheck(protocol, \"write\")\n  }\n\n  /* ---------------------------------------- *\n   |  Log Directory Management and Retention  |\n   * ---------------------------------------- */\n\n  /**\n   * Whether a Delta table exists at this directory.\n   * It is okay to use the cached volatile snapshot here, since the worst case is that the table\n   * has recently started existing which hasn't been picked up here. If so, any subsequent command\n   * that updates the table will see the right value.\n   */\n  def tableExists: Boolean = unsafeVolatileSnapshot.version >= 0\n\n  def isSameLogAs(otherLog: DeltaLog): Boolean = this.compositeId == otherLog.compositeId\n\n  /** Creates the log directory and commit directory if it does not exist. */\n  def createLogDirectoriesIfNotExists(): Unit = {\n    val fs = PathWithFileSystem.withConf(logPath, newDeltaHadoopConf()).fs\n    def createDirIfNotExists(path: Path): Unit = {\n      // Optimistically attempt to create the directory first without checking its existence.\n      // This is efficient because we're assuming it's more likely that the directory doesn't\n      // exist and it saves an filesystem existence check in that case.\n      val (success, mkdirsIOExceptionOpt) = try {\n        // Return value of false should mean the directory already existed (not an error) but\n        // we will verify below because we're paranoid about buggy FileSystem implementations.\n        (fs.mkdirs(path), None)\n      } catch {\n        // A FileAlreadyExistsException is expected if a non-directory object exists but an explicit\n        // check is needed because buggy Hadoop FileSystem.mkdir wrongly throws the exception even\n        // on existing directories.\n        case io: IOException =>\n          val dirExists =\n            try {\n              fs.getFileStatus(path).isDirectory\n            } catch {\n              case NonFatal(_) => false\n            }\n          (dirExists, Some(io))\n      }\n      if (!success) {\n        throw DeltaErrors.cannotCreateLogPathException(\n          logPath = logPath.toString,\n          cause = mkdirsIOExceptionOpt.orNull)\n      }\n    }\n    createDirIfNotExists(FileNames.commitDirPath(logPath))\n  }\n\n  /* ------------  *\n   |  Integration  |\n   * ------------  */\n\n  /**\n   * Returns a [[org.apache.spark.sql.DataFrame]] containing the new files within the specified\n   * version range.\n   */\n  def createDataFrame(\n      snapshot: SnapshotDescriptor,\n      addFiles: Seq[AddFile],\n      isStreaming: Boolean = false,\n      actionTypeOpt: Option[String] = None): DataFrame = {\n    val actionType = actionTypeOpt.getOrElse(if (isStreaming) \"streaming\" else \"batch\")\n    // It's ok to not pass down the partitionSchema to TahoeBatchFileIndex. Schema evolution will\n    // ensure any partitionSchema changes will be captured, and upon restart, the new snapshot will\n    // be initialized with the correct partition schema again.\n    val fileIndex = new TahoeBatchFileIndex(spark, actionType, addFiles, this, dataPath, snapshot)\n    // Drop null type columns from the relation's schema if it's not a streaming query until\n    // null type columns are fully supported.\n    val dropNullTypeColumnsFromSchema = if (isStreaming) {\n      // Can force the legacy behavior(dropping nullType columns) with a flag.\n      SQLConf.get.getConf(DeltaSQLConf.DELTA_STREAMING_CREATE_DATAFRAME_DROP_NULL_COLUMNS)\n    } else {\n      // Allow configurable behavior for non-streaming sources. This is used for testing.\n      SQLConf.get.getConf(DeltaSQLConf.DELTA_CREATE_DATAFRAME_DROP_NULL_COLUMNS)\n    }\n    val relation = buildHadoopFsRelationWithFileIndex(\n      snapshot,\n      fileIndex,\n      bucketSpec = None,\n      dropNullTypeColumnsFromSchema = dropNullTypeColumnsFromSchema)\n    DataFrameUtils.ofRows(spark, LogicalRelation(relation, isStreaming = isStreaming))\n  }\n\n  /**\n   * Returns a [[BaseRelation]] that contains all of the data present\n   * in the table. This relation will be continually updated\n   * as files are added or removed from the table. However, new [[BaseRelation]]\n   * must be requested in order to see changes to the schema.\n   */\n  def createRelation(\n      partitionFilters: Seq[Expression] = Nil,\n      snapshotToUseOpt: Option[Snapshot] = None,\n      catalogTableOpt: Option[CatalogTable] = None,\n      isTimeTravelQuery: Boolean = false): BaseRelation = {\n\n    /** Used to link the files present in the table into the query planner. */\n    // TODO: If snapshotToUse is unspecified, get the correct snapshot from update()\n    val snapshotToUse = snapshotToUseOpt.getOrElse(unsafeVolatileSnapshot)\n    if (snapshotToUse.version < 0) {\n      // A negative version here means the dataPath is an empty directory. Read query should error\n      // out in this case.\n      throw DeltaErrors.pathNotExistsException(dataPath.toString)\n    }\n\n    val fileIndex = TahoeLogFileIndex(\n      spark, this, dataPath, snapshotToUse, catalogTableOpt, partitionFilters, isTimeTravelQuery)\n    var bucketSpec: Option[BucketSpec] = None\n\n    val r = buildHadoopFsRelationWithFileIndex(snapshotToUse, fileIndex, bucketSpec = bucketSpec)\n    new HadoopFsRelation(\n      r.location,\n      r.partitionSchema,\n      r.dataSchema,\n      r.bucketSpec,\n      r.fileFormat,\n      r.options\n    )(spark) with InsertableRelation {\n      def insert(data: DataFrame, overwrite: Boolean): Unit = {\n        val mode = if (overwrite) SaveMode.Overwrite else SaveMode.Append\n        WriteIntoDelta(\n          deltaLog = DeltaLog.this,\n          mode = mode,\n          new DeltaOptions(Map.empty[String, String], spark.sessionState.conf),\n          partitionColumns = Seq.empty,\n          configuration = Map.empty,\n          data = data,\n          catalogTableOpt = catalogTableOpt).run(spark)\n      }\n    }\n  }\n\n  def buildHadoopFsRelationWithFileIndex(\n      snapshot: SnapshotDescriptor,\n      fileIndex: TahoeFileIndex,\n      bucketSpec: Option[BucketSpec],\n      dropNullTypeColumnsFromSchema: Boolean = true): HadoopFsRelation = {\n    val dataSchema = if (dropNullTypeColumnsFromSchema) {\n      SchemaUtils.dropNullTypeColumns(snapshot.metadata.schema)\n    } else {\n      snapshot.metadata.schema\n    }\n    HadoopFsRelation(\n      fileIndex,\n      partitionSchema = DeltaTableUtils.removeInternalDeltaMetadata(\n        spark,\n        DeltaTableUtils.removeInternalWriterMetadata(spark, snapshot.metadata.partitionSchema)\n      ),\n      // We pass all table columns as `dataSchema` so that Spark will preserve the partition\n      // column locations. Otherwise, for any partition columns not in `dataSchema`, Spark would\n      // just append them to the end of `dataSchema`.\n      dataSchema = DeltaTableUtils.removeInternalDeltaMetadata(\n        spark, DeltaTableUtils.removeInternalWriterMetadata(spark, dataSchema)\n      ),\n      bucketSpec = bucketSpec,\n      fileFormat(snapshot.protocol, snapshot.metadata),\n      // `metadata.format.options` is not set today. Even if we support it in future, we shouldn't\n      // store any file system options since they may contain credentials. Hence, it will never\n      // conflict with `DeltaLog.options`.\n      snapshot.metadata.format.options ++ options)(spark)\n  }\n\n  /**\n   * Verify the required Spark conf for delta\n   * Throw `DeltaErrors.configureSparkSessionWithExtensionAndCatalog` exception if\n   * `spark.sql.catalog.spark_catalog` config is missing. We do not check for\n   * `spark.sql.extensions` because DeltaSparkSessionExtension can alternatively\n   * be activated using the `.withExtension()` API. This check can be disabled\n   * by setting DELTA_CHECK_REQUIRED_SPARK_CONF to false.\n   */\n  protected def checkRequiredConfigurations(): Unit = {\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_REQUIRED_SPARK_CONFS_CHECK)) {\n      if (!spark.conf.contains(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key)) {\n        throw DeltaErrors.configureSparkSessionWithExtensionAndCatalog(None)\n      }\n    }\n  }\n\n  /**\n   * Returns a proper path canonicalization function for the current Delta log.\n   *\n   * If `runsOnExecutors` is true, the returned method will use a broadcast Hadoop Configuration\n   * so that the method is suitable for execution on executors. Otherwise, the returned method\n   * will use a local Hadoop Configuration and the method can only be executed on the driver.\n   */\n  private[delta] def getCanonicalPathFunction(runsOnExecutors: Boolean): String => String = {\n    val hadoopConf = newDeltaHadoopConf()\n    // Wrap `hadoopConf` with a method to delay the evaluation to run on executors.\n    val getHadoopConf = if (runsOnExecutors) {\n      val broadcastHadoopConf =\n        spark.sparkContext.broadcast(new SerializableConfiguration(hadoopConf))\n      () => broadcastHadoopConf.value.value\n    } else {\n      () => hadoopConf\n    }\n\n    new DeltaLog.CanonicalPathFunction(getHadoopConf)\n  }\n\n  /**\n   * Returns a proper path canonicalization UDF for the current Delta log.\n   *\n   * If `runsOnExecutors` is true, the returned UDF will use a broadcast Hadoop Configuration.\n   * Otherwise, the returned UDF will use a local Hadoop Configuration and the UDF can\n   * only be executed on the driver.\n   */\n  private[delta] def getCanonicalPathUdf(runsOnExecutors: Boolean = true): UserDefinedFunction = {\n    DeltaUDF.stringFromString(getCanonicalPathFunction(runsOnExecutors))\n  }\n}\n\nobject DeltaLog extends DeltaLogging {\n\n  /**\n   * The key type of `DeltaLog` cache. It consists of\n   * - The canonicalized table path\n   * - File system options (options starting with \"fs.\" or \"dfs.\" prefix) passed into\n   *   `DataFrameReader/Writer`\n   */\n  case class DeltaLogCacheKey(\n    path: Path,\n    fsOptions: Map[String, String]\n  )\n\n  /** The name of the subdirectory that holds Delta metadata files */\n  private[delta] val LOG_DIR_NAME = \"_delta_log\"\n\n  private[delta] def logPathFor(dataPath: String): Path = logPathFor(new Path(dataPath))\n  private[delta] def logPathFor(dataPath: Path): Path =\n    DeltaTableUtils.safeConcatPaths(dataPath, LOG_DIR_NAME)\n\n  /**\n   * We create only a single [[DeltaLog]] for any given `DeltaLogCacheKey` to avoid wasted work\n   * in reconstructing the log.\n   */\n  private[delta] def getOrCreateCache(conf: SQLConf):\n      Cache[DeltaLogCacheKey, DeltaLog] = synchronized {\n    deltaLogCache match {\n      case Some(c) => c\n      case None =>\n        val builder = createCacheBuilder(conf)\n          .removalListener(\n              (removalNotification: RemovalNotification[DeltaLogCacheKey, DeltaLog]) => {\n            val log = removalNotification.getValue\n            // TODO: We should use ref-counting to uncache snapshots instead of a manual timed op\n            try log.unsafeVolatileSnapshot.uncache() catch {\n              case _: java.lang.NullPointerException =>\n              // Various layers will throw null pointer if the RDD is already gone.\n            }\n          })\n        deltaLogCache = Some(builder.build[DeltaLogCacheKey, DeltaLog]())\n        deltaLogCache.get\n    }\n  }\n\n  private var deltaLogCache: Option[Cache[DeltaLogCacheKey, DeltaLog]] = None\n\n  /**\n   * Helper to create delta log caches\n   */\n  private def createCacheBuilder(conf: SQLConf): CacheBuilder[AnyRef, AnyRef] = {\n    val cacheRetention = conf.getConf(DeltaSQLConf.DELTA_LOG_CACHE_RETENTION_MINUTES)\n    val cacheSize = conf\n      .getConf(DeltaSQLConf.DELTA_LOG_CACHE_SIZE)\n      .max(sys.props.get(\"delta.log.cacheSize\").map(_.toLong).getOrElse(0L))\n\n    CacheBuilder\n      .newBuilder()\n      .expireAfterAccess(cacheRetention, TimeUnit.MINUTES)\n      .maximumSize(cacheSize)\n  }\n\n\n  /**\n   * Creates a [[LogicalRelation]] for a given [[DeltaLogFileIndex]], with all necessary file source\n   * options taken from the Delta Log. All reads of Delta metadata files should use this method.\n   */\n  def indexToRelation(\n      spark: SparkSession,\n      index: DeltaLogFileIndex,\n      additionalOptions: Map[String, String],\n      schema: StructType = Action.logSchema): LogicalRelation = {\n    val formatSpecificOptions: Map[String, String] = index.format match {\n      case DeltaLogFileIndex.COMMIT_FILE_FORMAT =>\n        jsonCommitParseOption\n      case _ => Map.empty\n    }\n    // Delta should NEVER ignore missing or corrupt metadata files, because doing so can render the\n    // entire table unusable. Hard-wire that into the file source options so the user can't override\n    // it by setting spark.sql.files.ignoreCorruptFiles or spark.sql.files.ignoreMissingFiles.\n    val allOptions = additionalOptions ++ formatSpecificOptions ++ Map(\n      FileSourceOptions.IGNORE_CORRUPT_FILES -> \"false\",\n      FileSourceOptions.IGNORE_MISSING_FILES -> \"false\"\n    )\n    val fsRelation = HadoopFsRelation(\n      index, index.partitionSchema, schema, None, index.format, allOptions)(spark)\n    LogicalRelation(fsRelation)\n  }\n\n  // Don't tolerate malformed JSON when parsing Delta log actions (default is PERMISSIVE)\n  val jsonCommitParseOption = Map(\"mode\" -> FailFastMode.name)\n\n  /** Helper for creating a log when it stored at the root of the data. */\n  def forTable(spark: SparkSession, dataPath: String): DeltaLog = {\n    apply(\n      spark,\n      logPathFor(dataPath),\n      options = Map.empty,\n      initialCatalogTable = None,\n      new SystemClock)\n  }\n\n  /** Helper for creating a log when it stored at the root of the data. */\n  def forTable(spark: SparkSession, dataPath: Path): DeltaLog = {\n    apply(spark, logPathFor(dataPath), initialCatalogTable = None, new SystemClock)\n  }\n\n  /** Helper for creating a log when it stored at the root of the data. */\n  def forTable(spark: SparkSession, dataPath: Path, options: Map[String, String]): DeltaLog = {\n    apply(spark, logPathFor(dataPath), options, initialCatalogTable = None, new SystemClock)\n  }\n\n  /** Helper for creating a log when it stored at the root of the data. */\n  def forTable(spark: SparkSession, dataPath: Path, clock: Clock): DeltaLog = {\n    apply(spark, logPathFor(dataPath), initialCatalogTable = None, clock)\n  }\n\n  /** Helper for creating a log for the table. */\n  def forTable(spark: SparkSession, tableName: TableIdentifier): DeltaLog = {\n    forTable(spark, tableName, new SystemClock)\n  }\n\n  /** Helper for creating a log for the table. */\n  def forTable(spark: SparkSession, table: CatalogTable): DeltaLog = {\n    forTable(spark, table, new SystemClock)\n  }\n\n  /** Helper for creating a log for the table. */\n  def forTable(spark: SparkSession, tableName: TableIdentifier, clock: Clock): DeltaLog = {\n    if (DeltaTableIdentifier.isDeltaPath(spark, tableName)) {\n      forTable(spark, new Path(tableName.table), clock)\n    } else {\n      forTable(spark, spark.sessionState.catalog.getTableMetadata(tableName), clock)\n    }\n  }\n\n  /** Helper for creating a log for the table. */\n  def forTable(spark: SparkSession, table: CatalogTable, options: Map[String, String]): DeltaLog = {\n    apply(\n      spark,\n      logPathFor(new Path(table.location)),\n      options,\n      Some(table),\n      new SystemClock)\n  }\n\n  /** Helper for creating a log for the table. */\n  def forTable(spark: SparkSession, table: CatalogTable, clock: Clock): DeltaLog = {\n    apply(spark, logPathFor(new Path(table.location)), Some(table), clock)\n  }\n\n  private def apply(\n      spark: SparkSession,\n      rawPath: Path,\n      initialCatalogTable: Option[CatalogTable],\n      clock: Clock): DeltaLog =\n    apply(spark, rawPath, options = Map.empty, initialCatalogTable, clock)\n\n\n  /** Helper for creating a log for the table. */\n  private[delta] def forTable(\n      spark: SparkSession,\n      dataPath: Path,\n      options: Map[String, String],\n      catalogTable: Option[CatalogTable]): DeltaLog =\n    apply(spark, logPathFor(dataPath), options, catalogTable, new SystemClock)\n\n  /** Helper for getting a log, as well as the latest snapshot, of the table */\n  def forTableWithSnapshot(spark: SparkSession, dataPath: String): (DeltaLog, Snapshot) =\n    withFreshSnapshot { clock =>\n      (forTable(spark, new Path(dataPath), clock), None)\n    }\n\n  /** Helper for getting a log, as well as the latest snapshot, of the table */\n  def forTableWithSnapshot(spark: SparkSession, dataPath: Path): (DeltaLog, Snapshot) =\n    withFreshSnapshot { clock =>\n      (forTable(spark, dataPath, clock), None)\n    }\n\n  /** Helper for getting a log, as well as the latest snapshot, of the table */\n  def forTableWithSnapshot(\n      spark: SparkSession,\n      tableName: TableIdentifier): (DeltaLog, Snapshot) = {\n    withFreshSnapshot { clock =>\n      if (DeltaTableIdentifier.isDeltaPath(spark, tableName)) {\n        (forTable(spark, new Path(tableName.table)), None)\n      } else {\n        val catalogTable = spark.sessionState.catalog.getTableMetadata(tableName)\n        (forTable(spark, catalogTable, clock), Some(catalogTable))\n      }\n    }\n  }\n\n  /** Helper for getting a log, as well as the latest snapshot, of the table */\n  def forTableWithSnapshot(\n      spark: SparkSession,\n      dataPath: Path,\n      options: Map[String, String]): (DeltaLog, Snapshot) =\n    withFreshSnapshot { clock =>\n      val deltaLog =\n        apply(spark, logPathFor(dataPath), options, initialCatalogTable = None, clock)\n      (deltaLog, None)\n    }\n\n  /** Helper for getting a log, as well as the latest snapshot, of the table */\n  def forTableWithSnapshot(\n      spark: SparkSession,\n      table: CatalogTable,\n      options: Map[String, String]): (DeltaLog, Snapshot) =\n    withFreshSnapshot { clock =>\n      val deltaLog =\n        apply(spark, logPathFor(new Path(table.location)), options, Some(table), clock)\n      (deltaLog, Some(table))\n    }\n\n  /**\n   * Helper method for transforming a given delta log path to the consistent formal path format.\n   */\n  def formalizeDeltaPath(\n      spark: SparkSession,\n      options: Map[String, String],\n      rootPath: Path): Path = {\n    val fileSystemOptions: Map[String, String] =\n      if (spark.sessionState.conf.getConf(\n        DeltaSQLConf.LOAD_FILE_SYSTEM_CONFIGS_FROM_DATAFRAME_OPTIONS)) {\n        options.filterKeys { k =>\n          DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n        }.toMap\n      } else {\n        Map.empty\n      }\n    // scalastyle:off deltahadoopconfiguration\n    val hadoopConf = spark.sessionState.newHadoopConfWithOptions(fileSystemOptions)\n    // scalastyle:on deltahadoopconfiguration\n    PathWithFileSystem\n      .withConf(rootPath, hadoopConf)\n      .fs\n      .makeQualified(rootPath)\n  }\n\n  /**\n   * Helper function to be used with the forTableWithSnapshot calls. Thunk is a\n   * partially applied DeltaLog.forTable call, which we can then wrap around with a\n   * snapshot update. We use the system clock to avoid back-to-back updates.\n   */\n  private[delta] def withFreshSnapshot(\n      thunk: Clock => (DeltaLog, Option[CatalogTable])): (DeltaLog, Snapshot) = {\n    val clock = new SystemClock\n    val ts = clock.getTimeMillis()\n    val (deltaLog, catalogTableOpt) = thunk(clock)\n    val snapshot =\n      deltaLog.update(checkIfUpdatedSinceTs = Some(ts), catalogTableOpt = catalogTableOpt)\n    (deltaLog, snapshot)\n  }\n\n  private def apply(\n      spark: SparkSession,\n      rawPath: Path,\n      options: Map[String, String],\n      initialCatalogTable: Option[CatalogTable],\n      clock: Clock\n  ): DeltaLog = {\n    // Construct the filesystem options based on the DataFrameReader/Writer options, and if it's\n    // a catalog based table, we need combine both options and catalog-based table storage\n    // properties since all cloud credential information are stored in storage properties.\n    val catalogTableStorageProps = initialCatalogTable\n      .map(t => t.storage.properties.filter { case (k, _) =>\n          DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n        })\n      .getOrElse(Map.empty)\n    val fileSystemOptions: Map[String, String] =\n      if (spark.sessionState.conf.getConf(\n          DeltaSQLConf.LOAD_FILE_SYSTEM_CONFIGS_FROM_DATAFRAME_OPTIONS)) {\n        // We pick up only file system options so that we don't pass any parquet or json options to\n        // the code that reads Delta transaction logs.\n        catalogTableStorageProps ++ options.filterKeys { k =>\n          DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n        }.toMap\n      } else {\n        catalogTableStorageProps\n      }\n\n    // scalastyle:off deltahadoopconfiguration\n    val hadoopConf = spark.sessionState.newHadoopConfWithOptions(fileSystemOptions)\n    // scalastyle:on deltahadoopconfiguration\n    val path = PathWithFileSystem\n      .withConf(rawPath, hadoopConf)\n      .fs\n      .makeQualified(rawPath)\n    def createDeltaLog(tablePath: Path = path): DeltaLog = recordDeltaOperation(\n      null,\n      \"delta.log.create\",\n      Map(TAG_TAHOE_PATH -> tablePath.getParent.toString)) {\n        AnalysisHelper.allowInvokingTransformsInAnalyzer {\n          new DeltaLog(\n            logPath = tablePath,\n            dataPath = tablePath.getParent,\n            options = fileSystemOptions,\n            allOptions = options,\n            clock = clock,\n            initialCatalogTable = initialCatalogTable\n          )\n        }\n    }\n    val cacheKey = DeltaLogCacheKey(\n      path,\n      fileSystemOptions)\n\n    def getDeltaLogFromCache: DeltaLog = {\n      // The following cases will still create a new ActionLog even if there is a cached\n      // ActionLog using a different format path:\n      // - Different `scheme`\n      // - Different `authority` (e.g., different user tokens in the path)\n      // - Different mount point.\n      try {\n        getOrCreateCache(spark.sessionState.conf)\n          .get(cacheKey, () => {\n            createDeltaLog()\n          }\n        )\n      } catch {\n        case e: com.google.common.util.concurrent.UncheckedExecutionException => throw e.getCause\n        case e: java.util.concurrent.ExecutionException => throw e.getCause\n      }\n    }\n\n    def initializeDeltaLog(): DeltaLog = {\n      val deltaLog = getDeltaLogFromCache\n      if (Option(deltaLog.sparkContext.get).map(_.isStopped).getOrElse(true)) {\n        // Invalid the cached `DeltaLog` and create a new one because the `SparkContext` of the\n        // cached `DeltaLog` has been stopped.\n        getOrCreateCache(spark.sessionState.conf).invalidate(cacheKey)\n        getDeltaLogFromCache\n      } else {\n        deltaLog\n      }\n    }\n\n    val deltaLog = initializeDeltaLog()\n    // The deltaLog object may be cached while other session updates table redirect property.\n    // To avoid this potential race condition, we would add a validation inside deltaLog.update\n    // method to ensure deltaLog points to correct place after snapshot is updated.\n    val redirectConfigOpt = RedirectFeature.needDeltaLogRedirect(\n      spark,\n      deltaLog,\n      initialCatalogTable\n    )\n    redirectConfigOpt.map { redirectConfig =>\n      val (redirectLoc, catalogTableOpt) = RedirectFeature\n        .getRedirectLocationAndTable(spark, deltaLog, redirectConfig)\n      val formalizedPath = formalizeDeltaPath(spark, options, redirectLoc)\n      //  with redirect prefix to prevent interference between redirection and normal access.\n      val redirectKey = new Path(RedirectFeature.DELTALOG_PREFIX, redirectLoc)\n      val deltaLogCacheKey = DeltaLogCacheKey(\n        redirectKey,\n        fileSystemOptions)\n      getOrCreateCache(spark.sessionState.conf).get(\n        deltaLogCacheKey,\n        () => {\n            var redirectedDeltaLog = new DeltaLog(\n              logPath = formalizedPath,\n              dataPath = formalizedPath.getParent,\n              options = fileSystemOptions,\n              allOptions = options,\n              clock = clock,\n              initialCatalogTable = catalogTableOpt\n            )\n            redirectedDeltaLog\n        }\n      )\n    }.getOrElse(deltaLog)\n  }\n\n  /** Invalidate the cached DeltaLog object for the given `dataPath`. */\n  def invalidateCache(spark: SparkSession, dataPath: Path): Unit = {\n    try {\n      val rawPath = logPathFor(dataPath)\n      // scalastyle:off deltahadoopconfiguration\n      // This method cannot be called from DataFrameReader/Writer so it's safe to assume the user\n      // has set the correct file system configurations in the session configs.\n      val fs = PathWithFileSystem.withConf(rawPath, spark.sessionState.newHadoopConf()).fs\n      // scalastyle:on deltahadoopconfiguration\n      val path = fs.makeQualified(rawPath)\n\n      val deltaLogCache = getOrCreateCache(spark.sessionState.conf)\n      if (spark.sessionState.conf.getConf(\n          DeltaSQLConf.LOAD_FILE_SYSTEM_CONFIGS_FROM_DATAFRAME_OPTIONS)) {\n        // We rely on the fact that accessing the key set doesn't modify the entry access time. See\n        // `CacheBuilder.expireAfterAccess`.\n        val keysToBeRemoved = mutable.ArrayBuffer[DeltaLogCacheKey]()\n        val iter = deltaLogCache.asMap().keySet().iterator()\n        while (iter.hasNext) {\n          val key = iter.next()\n          if (key.path == path) {\n            keysToBeRemoved += key\n          }\n        }\n        deltaLogCache.invalidateAll(keysToBeRemoved.asJava)\n      } else {\n        deltaLogCache.invalidate(DeltaLogCacheKey(\n          path,\n          fsOptions = Map.empty))\n      }\n    } catch {\n      case NonFatal(e) => logWarning(e.getMessage, e)\n    }\n  }\n\n  def clearCache(): Unit = {\n    deltaLogCache.foreach(_.invalidateAll())\n  }\n\n  /** Unset the caches. Exposing for testing */\n  private[delta] def unsetCache(): Unit = {\n    synchronized {\n      deltaLogCache = None\n    }\n  }\n\n  /** Return the number of cached `DeltaLog`s. Exposing for testing */\n  private[delta] def cacheSize: Long = {\n    deltaLogCache.map(_.size()).getOrElse(0L)\n  }\n\n  /**\n   * Filters the given [[Dataset]] by the given `partitionFilters`, returning those that match.\n   * @param files The active files in the DeltaLog state, which contains the partition value\n   *              information\n   * @param partitionFilters Filters on the partition columns\n   * @param partitionColumnPrefixes The path to the `partitionValues` column, if it's nested\n   * @param shouldRewritePartitionFilters Whether to rewrite `partitionFilters` to be over the\n   *                                      [[AddFile]] schema\n   */\n  def filterFileList(\n      partitionSchema: StructType,\n      files: DataFrame,\n      partitionFilters: Seq[Expression],\n      partitionColumnPrefixes: Seq[String] = Nil,\n      shouldRewritePartitionFilters: Boolean = true): DataFrame = {\n\n    val rewrittenFilters = if (shouldRewritePartitionFilters) {\n      rewritePartitionFilters(\n        partitionSchema,\n        files.sparkSession.sessionState.conf.resolver,\n        partitionFilters,\n        partitionColumnPrefixes)\n    } else {\n      partitionFilters\n    }\n    val expr = rewrittenFilters.reduceLeftOption(And).getOrElse(Literal.TrueLiteral)\n    val columnFilter = Column(expr)\n    files.filter(columnFilter)\n  }\n\n  /**\n   * Rewrite the given `partitionFilters` to be used for filtering partition values.\n   * We need to explicitly resolve the partitioning columns here because the partition columns\n   * are stored as keys of a Map type instead of attributes in the AddFile schema (below) and thus\n   * cannot be resolved automatically.\n   *\n   * @param partitionFilters Filters on the partition columns\n   * @param partitionColumnPrefixes The path to the `partitionValues` column, if it's nested\n   */\n  def rewritePartitionFilters(\n      partitionSchema: StructType,\n      resolver: Resolver,\n      partitionFilters: Seq[Expression],\n      partitionColumnPrefixes: Seq[String] = Nil): Seq[Expression] = {\n    partitionFilters\n      .map(_.transformUp {\n      case a: Attribute =>\n        // If we have a special column name, e.g. `a.a`, then an UnresolvedAttribute returns\n        // the column name as '`a.a`' instead of 'a.a', therefore we need to strip the backticks.\n        val unquoted = a.name.stripPrefix(\"`\").stripSuffix(\"`\")\n        val partitionCol = partitionSchema.find { field => resolver(field.name, unquoted) }\n        partitionCol match {\n          case Some(f: StructField) =>\n            val name = DeltaColumnMapping.getPhysicalName(f)\n            Cast(\n              UnresolvedAttribute(partitionColumnPrefixes ++ Seq(\"partitionValues\", name)),\n              f.dataType)\n          case None =>\n            // This should not be able to happen, but the case was present in the original code so\n            // we kept it to be safe.\n            log.error(s\"Partition filter referenced column ${a.name} not in the partition schema\")\n            UnresolvedAttribute(partitionColumnPrefixes ++ Seq(\"partitionValues\", a.name))\n        }\n    })\n  }\n\n\n  /**\n   * Checks whether this table only accepts appends. If so it will throw an error in operations that\n   * can remove data such as DELETE/UPDATE/MERGE.\n   */\n  def assertRemovable(snapshot: Snapshot): Unit = {\n    val metadata = snapshot.metadata\n    if (DeltaConfigs.IS_APPEND_ONLY.fromMetaData(metadata)) {\n      throw DeltaErrors.modifyAppendOnlyTableException(metadata.name)\n    }\n  }\n\n  /** How long to keep around SetTransaction actions before physically deleting them. */\n  def minSetTransactionRetentionInterval(metadata: Metadata): Option[Long] = {\n    DeltaConfigs.TRANSACTION_ID_RETENTION_DURATION\n      .fromMetaData(metadata)\n      .map(DeltaConfigs.getMilliSeconds)\n  }\n  /** How long to keep around logically deleted files before physically deleting them. */\n  def tombstoneRetentionMillis(metadata: Metadata): Long = {\n    DeltaConfigs.getMilliSeconds(DeltaConfigs.TOMBSTONE_RETENTION.fromMetaData(metadata))\n  }\n\n  /** Get a function that canonicalizes a given `path`. */\n  private[delta] class CanonicalPathFunction(getHadoopConf: () => Configuration)\n      extends Function[String, String] with Serializable {\n    // Mark it `@transient lazy val` so that de-serialization happens only once on every executor.\n    @transient\n    private lazy val fs = {\n      // scalastyle:off FileSystemGet\n      FileSystem.get(getHadoopConf())\n      // scalastyle:on FileSystemGet\n    }\n\n    override def apply(path: String): String = {\n      // scalastyle:off pathfromuri\n      val hadoopPath = new Path(new URI(path))\n      // scalastyle:on pathfromuri\n      if (hadoopPath.isAbsoluteAndSchemeAuthorityNull) {\n        fs.makeQualified(hadoopPath).toUri.toString\n      } else {\n        // return untouched if it is a relative path or is already fully qualified\n        hadoopPath.toUri.toString\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaLogFileIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.hadoop.fs._\n\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.execution.datasources.{FileFormat, FileIndex, PartitionDirectory}\nimport org.apache.spark.sql.execution.datasources.json.JsonFileFormat\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.types.{LongType, StructField, StructType}\n\n/**\n * A specialized file index for files found in the _delta_log directory. By using this file index,\n * we avoid any additional file listing, partitioning inference, and file existence checks when\n * computing the state of a Delta table.\n *\n * @param format The file format of the log files. Currently \"parquet\" or \"json\"\n * @param files The files to read\n */\ncase class DeltaLogFileIndex private (\n    format: FileFormat,\n    files: Array[FileStatus])\n  extends FileIndex\n  with Logging {\n\n  import DeltaLogFileIndex._\n\n  override lazy val rootPaths: Seq[Path] = files.map(_.getPath)\n\n  def listAllFiles(): Seq[PartitionDirectory] = {\n    files\n      .groupBy(f => FileNames.getFileVersionOpt(f.getPath).getOrElse(-1L))\n      .map { case (version, files) => PartitionDirectory(InternalRow(version), files) }\n      .toSeq\n  }\n\n  override def listFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[PartitionDirectory] = {\n    if (partitionFilters.isEmpty) {\n      listAllFiles()\n    } else {\n      val predicate = partitionFilters.reduce(And)\n      val boundPredicate = predicate.transform {\n        case a: AttributeReference =>\n          val index = partitionSchema.indexWhere(a.name == _.name)\n          BoundReference(index, partitionSchema(index).dataType, partitionSchema(index).nullable)\n      }\n      val predicateEvaluator = Predicate.create(boundPredicate, Nil)\n      listAllFiles().filter(d => predicateEvaluator.eval(d.values))\n    }\n  }\n\n  override val inputFiles: Array[String] = files.map(_.getPath.toString)\n\n  override def refresh(): Unit = {}\n\n  override val sizeInBytes: Long = files.map(_.getLen).sum\n\n  override val partitionSchema: StructType =\n    new StructType().add(COMMIT_VERSION_COLUMN, LongType, nullable = false)\n\n  override def toString: String =\n    s\"DeltaLogFileIndex($format, numFilesInSegment: ${files.size}, totalFileSize: $sizeInBytes)\"\n\n  logInfo(log\"Created ${MDC(DeltaLogKeys.FILE_INDEX, this)}\")\n}\n\nobject DeltaLogFileIndex {\n  val COMMIT_VERSION_COLUMN = \"version\"\n\n  lazy val COMMIT_FILE_FORMAT = new JsonFileFormat\n  lazy val CHECKPOINT_FILE_FORMAT_PARQUET = new ParquetFileFormat\n  lazy val CHECKPOINT_FILE_FORMAT_JSON = new JsonFileFormat\n  lazy val CHECKSUM_FILE_FORMAT = new JsonFileFormat\n\n  def apply(format: FileFormat, fs: FileSystem, paths: Seq[Path]): DeltaLogFileIndex = {\n    DeltaLogFileIndex(format, paths.map(fs.getFileStatus).toArray)\n  }\n\n  def apply(format: FileFormat, files: Seq[FileStatus]): Option[DeltaLogFileIndex] = {\n    if (files.isEmpty) None else Some(DeltaLogFileIndex(format, files.toArray))\n  }\n\n  def apply(format: FileFormat, filesOpt: Option[Seq[FileStatus]]): Option[DeltaLogFileIndex] = {\n    filesOpt.flatMap(DeltaLogFileIndex(format, _))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaMergeActionResolver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.ResolveDeltaMergeInto.ResolveExpressionsFn\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.internal.SQLConf\n\ncase class TargetTableResolutionResult(\n    unresolvedAttribute: UnresolvedAttribute,\n    expr: Expression\n)\n\n/** Base trait with helpers for resolving DeltaMergeAction. */\ntrait DeltaMergeActionResolverBase {\n  /** The SQL configuration for this query. */\n  def conf: SQLConf\n  /** Function we want to use for resolving expressions. */\n  def resolveExprsFn: ResolveExpressionsFn\n  /** The resolved target plan of the MERGE INTO statement. */\n  def target: LogicalPlan\n  /** The resolved source plan of the MERGE INTO statement. */\n  def source: LogicalPlan\n\n  /** Used for constructing error messages. */\n  private lazy val colsAsSQLText = target.output.map(_.sql).mkString(\", \")\n\n  /** Try to resolve a single target column in the Merge action. */\n  protected def resolveSingleTargetColumn(\n      unresolvedAttribute: UnresolvedAttribute,\n      mergeClauseTypeStr: String,\n      shouldTryUnresolvedTargetExprOnSource: Boolean): Expression = {\n    // Resolve the target column name without database/table/view qualifiers\n    // If clause allows nested field to be target, then this will return all the\n    // parts of the name (e.g., \"a.b\" -> Seq(\"a\", \"b\")). Otherwise, this will\n    // return only one string.\n    try {\n      ResolveDeltaMergeInto.resolveSingleExprOrFail(\n        resolveExprsFn = resolveExprsFn,\n        expr = unresolvedAttribute,\n        plansToResolveExpr = Seq(target),\n        mergeClauseTypeStr = mergeClauseTypeStr\n      )\n    } catch {\n      // Allow schema evolution for update and insert non-star when the column is not in\n      // the target.\n      case _: AnalysisException if shouldTryUnresolvedTargetExprOnSource =>\n        ResolveDeltaMergeInto.resolveSingleExprOrFail(\n          resolveExprsFn = resolveExprsFn,\n          expr = unresolvedAttribute,\n          plansToResolveExpr = Seq(source),\n          mergeClauseTypeStr = mergeClauseTypeStr\n        )\n    }\n  }\n\n  /**\n   * Takes the resolvedKey which refers to the target column in the relation and\n   * the corresponding resolvedRHSExpr which describes the assignment value and return\n   * a resolved DeltaMergeAction.\n   */\n  protected def buildDeltaMergeAction(\n      resolvedKey: Expression,\n      resolvedRHSExpr: Expression,\n      mergeClauseTypeStr: String): DeltaMergeAction = {\n    lazy val sqlText = resolvedKey.sql\n    lazy val resolutionErrorMsg =\n      s\"Cannot resolve $sqlText in target columns in $mergeClauseTypeStr given \" +\n        s\"columns $colsAsSQLText\"\n    val resolvedNameParts =\n      DeltaUpdateTable.getTargetColNameParts(resolvedKey, resolutionErrorMsg)\n    DeltaMergeAction(\n      targetColNameParts = resolvedNameParts,\n      expr = resolvedRHSExpr,\n      // Explicit column assignments overwrite target-only struct fields with null.\n      targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.NULLIFY,\n      targetColNameResolved = true)\n  }\n\n  /**\n   * Takes a sequence of DeltaMergeActions and returns the\n   * corresponding resolved DeltaMergeActions.\n   */\n  def resolve(\n      clauseType: String,\n      plansToResolveAction: Seq[LogicalPlan],\n      shouldTryUnresolvedTargetExprOnSource: Boolean,\n      deltaMergeActions: Seq[DeltaMergeAction]): Seq[DeltaMergeAction]\n}\n\nclass IndividualDeltaMergeActionResolver(\n    override val target: LogicalPlan,\n    override val source: LogicalPlan,\n    override val conf: SQLConf,\n    override val resolveExprsFn: ResolveExpressionsFn\n  ) extends DeltaMergeActionResolverBase {\n\n  /** Resolve DeltaMergeAction, one at a time. */\n  override def resolve(\n      mergeClauseTypeStr: String,\n      plansToResolveAction: Seq[LogicalPlan],\n      shouldTryUnresolvedTargetExprOnSource: Boolean,\n      deltaMergeActions: Seq[DeltaMergeAction]): Seq[DeltaMergeAction] = {\n    deltaMergeActions.map {\n      case d: DeltaMergeAction if !d.resolved =>\n        val unresolvedAttrib = UnresolvedAttribute(d.targetColNameParts)\n        val resolvedKey = resolveSingleTargetColumn(\n          unresolvedAttrib, mergeClauseTypeStr, shouldTryUnresolvedTargetExprOnSource)\n        val resolvedExpr =\n          resolveExprsFn(Seq(d.expr), plansToResolveAction).head\n        ResolveDeltaMergeInto.throwIfNotResolved(\n          resolvedExpr,\n          plansToResolveAction,\n          mergeClauseTypeStr)\n\n        buildDeltaMergeAction(resolvedKey, resolvedExpr, mergeClauseTypeStr)\n      // Already resolved\n      case d => d\n    }\n  }\n}\n\nclass BatchedDeltaMergeActionResolver(\n    override val target: LogicalPlan,\n    override val source: LogicalPlan,\n    override val conf: SQLConf,\n    override val resolveExprsFn: ResolveExpressionsFn\n  ) extends DeltaMergeActionResolverBase {\n\n  /**\n   * Attempt to batch resolve the target columns reference all at once. If we are\n   * unable to resolve against the target plan, we retry against the source plan\n   * if schema evolution is enabled and it's appropriate for the clause type.\n   *\n   * @return The resolved expressions for the target columns. The sequence of\n   *         expressions is ordered the same as the unresolved attributes\n   *         sequence passed in.\n   */\n  private def batchResolveTargetColumns(\n      unresolvedAttrSeq: Seq[UnresolvedAttribute],\n      shouldTryUnresolvedTargetExprOnSource: Boolean,\n      mergeClauseTypeStr: String): Seq[Expression] = {\n    val resolvedExprs = try {\n      // Unlike [[resolveSingleTargetColumn]], this is not a [[resolveOrFail]].\n      // We will not throw an exception if something was not resolved, because we\n      // want to resolve as much as possible and only retry to resolve against the\n      // source the few columns that failed to resolve. But we must wrap this in a\n      // try-catch to swallow exception that come from other parts of invoking the\n      // analyzer. We need this to preserve the behaviour where we throw a different\n      // exception in PreprocessTableMerge later on...\n      resolveExprsFn(unresolvedAttrSeq, Seq(target))\n    } catch {\n      // We don't know which attribute in the Seq lead to this exception.\n      // We need to resolve this one by one, so we can return early here.\n      case _: AnalysisException if shouldTryUnresolvedTargetExprOnSource =>\n        return unresolvedAttrSeq.map(\n          resolveSingleTargetColumn(_, mergeClauseTypeStr, shouldTryUnresolvedTargetExprOnSource))\n    }\n    assert(unresolvedAttrSeq.length == resolvedExprs.length, \"The number of \" +\n      \"resolved expressions should match the number of unresolved expressions\")\n\n    val targetTableResolutionResult: Seq[TargetTableResolutionResult] =\n      unresolvedAttrSeq.zip(resolvedExprs).map { case (unresolvedAttr, expr) =>\n        TargetTableResolutionResult(unresolvedAttr, expr)\n      }\n    val remainingUnresolvedExprs: Seq[Expression] =\n      targetTableResolutionResult.filterNot(_.expr.resolved).map(_.unresolvedAttribute)\n\n    val orderedResolvedTargetExprs = if (remainingUnresolvedExprs.isEmpty) {\n      // Everything was resolved, we can return the resolved expressions.\n      resolvedExprs\n    } else {\n      // We were not able to resolve all the target columns against the target plan.\n      // If we are not supposed to resolve the target column against the source and\n      // we were not able to resolve the column, then we should throw an exception\n      // at this point.\n      if (!shouldTryUnresolvedTargetExprOnSource) {\n        ResolveDeltaMergeInto.throwIfNotResolved(\n          // Use the first of the unresolved attributes to throw the exception.\n          targetTableResolutionResult.find(!_.expr.resolved).map(_.expr).get,\n          Seq(target),\n          mergeClauseTypeStr\n        )\n      }\n\n      // Try to resolve against the source, will throw an exception if it can't.\n      val resolvedExprAgainstSource: Seq[Expression] = ResolveDeltaMergeInto.resolveOrFail(\n        resolveExprsFn = resolveExprsFn,\n        exprs = remainingUnresolvedExprs,\n        plansToResolveExprs = Seq(source),\n        mergeClauseTypeStr = mergeClauseTypeStr\n      )\n\n      // Put the expressions that we resolved using the source back into the resolution result\n      // in the correct locations. The order needs to be preserved so that we can match it with\n      // the corresponding resolved assignment expressions.\n      var index = -1\n      targetTableResolutionResult.map { case TargetTableResolutionResult(_, expr) =>\n        if (expr.resolved) {\n          expr\n        } else {\n          index += 1\n          resolvedExprAgainstSource(index)\n        }\n      }\n    }\n\n    orderedResolvedTargetExprs\n  }\n\n  /**\n   * Batch the resolution of the target column name parts against the target relation\n   * and the resolution of assignment expression together.\n   *\n   * Fundamental requirement: Column/expression ordering must be preserved\n   * by [[resolveExprsFn]].\n   */\n  override def resolve(\n      mergeClauseTypeStr: String,\n      plansToResolveAction: Seq[LogicalPlan],\n      shouldTryUnresolvedTargetExprOnSource: Boolean,\n      deltaMergeActions: Seq[DeltaMergeAction]): Seq[DeltaMergeAction] = {\n    val (alreadyResolvedDeltaMergeActions, unresolvedDeltaMergeActions) =\n      deltaMergeActions.partition(_.resolved)\n\n    // Batch the unresolved attributes to resolve them in a single pass.\n    val unresolvedAttrSeq = unresolvedDeltaMergeActions\n      .map(mergeAction => UnresolvedAttribute(mergeAction.targetColNameParts))\n    val orderedResolvedTargetExprs = batchResolveTargetColumns(\n      unresolvedAttrSeq,\n      shouldTryUnresolvedTargetExprOnSource,\n      mergeClauseTypeStr)\n\n    // Now we deal with the expressions for each target column (RHS assignment).\n    val unresolvedRHSExprSeq = unresolvedDeltaMergeActions.map(_.expr)\n    val resolvedExprsSeq =\n      resolveExprsFn(unresolvedRHSExprSeq, plansToResolveAction)\n    assert(resolvedExprsSeq.length == orderedResolvedTargetExprs.length)\n    resolvedExprsSeq.foreach(\n      ResolveDeltaMergeInto.throwIfNotResolved(_, plansToResolveAction, mergeClauseTypeStr))\n\n    // Combine the resolved target columns and the resolved expressions to create\n    // the final resolved DeltaMergeAction\n    val resolvedDeltaMergeActions: Seq[DeltaMergeAction] =\n      orderedResolvedTargetExprs.zip(resolvedExprsSeq).map {\n        case (resolvedKey, resolvedExpr) =>\n          buildDeltaMergeAction(resolvedKey, resolvedExpr, mergeClauseTypeStr)\n      }\n\n    // The order for this Seq doesn't matter.\n    alreadyResolvedDeltaMergeActions ++ resolvedDeltaMergeActions\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaOperations.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaOperationMetrics.MetricsTransformer\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.logical.DeltaMergeIntoClause\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types.{StructField, StructType}\n\n/**\n * Exhaustive list of operations that can be performed on a Delta table. These operations are\n * tracked as the first line in delta logs, and power `DESCRIBE HISTORY` for Delta tables.\n */\nobject DeltaOperations {\n\n  /**\n   * An operation that can be performed on a Delta table.\n   * @param name The name of the operation.\n   */\n  sealed abstract class Operation(val name: String) {\n    def parameters: Map[String, Any]\n\n    lazy val jsonEncodedValues: Map[String, String] =\n      parameters.mapValues(JsonUtils.toJson(_)).toMap\n\n    val operationMetrics: Set[String] = Set()\n\n    def transformMetrics(metrics: Map[String, SQLMetric]): Map[String, String] = {\n      metrics.filterKeys( s =>\n        operationMetrics.contains(s)\n      ).mapValues(_.value.toString).toMap\n    }\n\n    val userMetadata: Option[String] = None\n\n    /** Whether this operation changes data */\n    def changesData: Boolean = false\n\n    /**\n     * Manually transform the deletion vector metrics, because they are not part of\n     * `operationMetrics` and are filtered out by the super.transformMetrics() call.\n     */\n    def transformDeletionVectorMetrics(\n        allMetrics: Map[String, SQLMetric],\n        dvMetrics: Map[String, MetricsTransformer] = DeltaOperationMetrics.DELETION_VECTORS)\n    : Map[String, String] = {\n      dvMetrics.flatMap { case (metric, transformer) =>\n        transformer.transformToString(metric, allMetrics)\n      }\n    }\n\n    /**\n     * A transaction that commits AddFile actions with deletionVector should have column stats that\n     * are not tight bounds. An exception to this is ComputeStats operation, which recomputes stats\n     * on these files, and the new stats are tight bounds. Some other operations that merely take an\n     * existing AddFile action and commit a copy of it, not changing the deletionVector or stats,\n     * can then also recommit AddFile with deletionVector and tight bound stats that were recomputed\n     * before.\n     *\n     * An operation for which this can happen, and there is no way that it could be committing\n     * new deletion vectors, should set this to false to bypass this check.\n     * All other operations should set this to true, so that this is validated during commit.\n     *\n     * This is abstract to force the implementers of all operations to think about this setting.\n     * All operations should add a comment justifying this setting.\n     * Any operation that sets this to false should add a test in TightBoundsSuite.\n     */\n    def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean\n\n    /**\n     * Whether the transaction is updating metadata of existing files.\n     *\n     * The Delta protocol allows committing AddFile actions for files that already exist on the\n     * latest version of the table, without committing corresponding RemoveFile actions. This is\n     * used to update the metadata of existing files, e.g. to recompute statistics or add tags.\n     *\n     * Such operations need special handling during conflict checking, especially against\n     * no-data-change transactions, because the read/delete conflict can be resolved with\n     * read-file-remapping and because there is no RemoveFile action to trigger a delete/delete\n     * conflict. In case you are adding such operation, make sure to include a test for conflicts\n     * with business *and* no-data-change transactions, e.g. optimize.\n     */\n    def isInPlaceFileMetadataUpdate: Option[Boolean]\n\n\n    /**\n     * Whether this operation is allowed to change the set and order of partition columns.\n     * Operations creating tables may always change the partitioning, so it's considered supported\n     * implicitly and checked in OptimisticTransaction. It is ignored what is returned here for\n     * operations that create a new table. Operations can return false in that case. Operations\n     * that replace tables or insert may return true depending on their mode and parameters.\n     * Most other operations should return false.\n     */\n    def canChangePartitionColumns: Boolean\n  }\n\n  abstract class OperationWithPredicates(name: String, val predicates: Seq[Expression])\n      extends Operation(name) {\n    private val predicateString = JsonUtils.toJson(predicatesToString(predicates))\n    override def parameters: Map[String, Any] = Map(\"predicate\" -> predicateString)\n  }\n\n  /** Recorded during batch inserts. Predicates can be provided for overwrites. */\n  val OP_WRITE = \"WRITE\"\n  case class Write(\n      mode: SaveMode,\n      partitionBy: Option[Seq[String]] = None,\n      predicate: Option[String] = None,\n      override val userMetadata: Option[String] = None,\n      isDynamicPartitionOverwrite: Option[Boolean] = None,\n      canOverwriteSchema: Option[Boolean] = None,\n      canMergeSchema: Option[Boolean] = None\n  ) extends Operation(OP_WRITE) {\n    override val parameters: Map[String, Any] = Map(\"mode\" -> mode.name()\n    ) ++\n      partitionBy.map(\"partitionBy\" -> JsonUtils.toJson(_)) ++\n      // Only log these fields when explicitly set to avoid noise in DESCRIBE HISTORY when users do\n      // not set them. This means we don't distinguish between explicitly disabled (false) and unset\n      // (defaults to disabled), but that's fine as the distinction is not particularly interesting.\n      predicate.map(\"predicate\" -> _) ++\n      isDynamicPartitionOverwrite.map(\"isDynamicPartitionOverwrite\" -> _) ++\n      canOverwriteSchema.map(\"canOverwriteSchema\" -> _) ++\n      canMergeSchema.map(\"canMergeSchema\" -> _)\n\n    val replaceWhereMetricsEnabled = SparkSession.active.conf.get(\n      DeltaSQLConf.REPLACEWHERE_METRICS_ENABLED)\n\n    val insertOverwriteRemoveMetricsEnabled = SparkSession.active.conf.get(\n      DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED)\n\n    override def transformMetrics(metrics: Map[String, SQLMetric]): Map[String, String] = {\n      // Need special handling for replaceWhere as it is implemented as a Write + Delete.\n      if (predicate.nonEmpty && replaceWhereMetricsEnabled) {\n        var strMetrics = super.transformMetrics(metrics)\n        // find the case where deletedRows are not captured\n        if (strMetrics.get(\"numDeletedRows\").exists(_ == \"0\") &&\n          strMetrics.get(\"numRemovedFiles\").exists(_ != \"0\")) {\n          // identify when row level metrics are unavailable. This will happen when the entire\n          // table or partition are deleted.\n          strMetrics -= \"numDeletedRows\"\n          strMetrics -= \"numCopiedRows\"\n          strMetrics -= \"numAddedFiles\"\n        }\n\n        // in the case when stats are not collected we need to remove all row based metrics\n        // If the DF provided to replaceWhere is an empty DataFrame and we don't have stats\n        // we won't return row level metrics.\n        if (strMetrics.get(\"numOutputRows\").exists(_ == \"0\") &&\n            strMetrics.get(\"numFiles\").exists(_ != 0)) {\n          strMetrics -= \"numDeletedRows\"\n          strMetrics -= \"numOutputRows\"\n          strMetrics -= \"numCopiedRows\"\n        }\n\n        strMetrics\n      } else {\n        super.transformMetrics(metrics)\n      }\n    }\n\n    override val operationMetrics: Set[String] =\n      if (predicate.isEmpty || !replaceWhereMetricsEnabled) {\n        // Remove metrics are included to replaceWhere metrics\n        // so they need to be added only when replaceWhere metrics are not presented\n        val overwriteMetrics =\n          if (mode == SaveMode.Overwrite && insertOverwriteRemoveMetricsEnabled) {\n            DeltaOperationMetrics.OVERWRITE_REMOVES\n          } else {\n            Set.empty\n          }\n        DeltaOperationMetrics.WRITE ++ overwriteMetrics\n      } else {\n        // Need special handling for replaceWhere as rows/files are deleted as well.\n        DeltaOperationMetrics.WRITE_REPLACE_WHERE\n      }\n\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    // DVs can be introduced by the replaceWhere operation.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = {\n      // We don't have to return true if it is a new table, only on overwrite.\n      mode == SaveMode.Overwrite && canOverwriteSchema.getOrElse(false)\n    }\n  }\n\n  case class RemoveColumnMapping(\n      override val userMetadata: Option[String] = None) extends Operation(\"REMOVE COLUMN MAPPING\") {\n    override def parameters: Map[String, Any] = Map()\n\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.REMOVE_COLUMN_MAPPING\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded during streaming inserts. */\n  case class StreamingUpdate(\n      outputMode: OutputMode,\n      queryId: String,\n      epochId: Long,\n      override val userMetadata: Option[String] = None\n  ) extends Operation(\"STREAMING UPDATE\") {\n    override val parameters: Map[String, Any] =\n      Map(\"outputMode\" -> outputMode.toString, \"queryId\" -> queryId, \"epochId\" -> epochId.toString\n      )\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.STREAMING_UPDATE\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n  /** Recorded while deleting certain partitions. */\n  val OP_DELETE = \"DELETE\"\n  case class Delete(predicate: Seq[Expression])\n      extends OperationWithPredicates(OP_DELETE, predicate) {\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.DELETE\n\n    override def transformMetrics(metrics: Map[String, SQLMetric]): Map[String, String] = {\n      var strMetrics = super.transformMetrics(metrics)\n      // find the case where deletedRows are not captured\n      if (strMetrics(\"numDeletedRows\") == \"0\" && strMetrics(\"numRemovedFiles\") != \"0\") {\n        // identify when row level metrics are unavailable. This will happen when the entire\n        // table or partition are deleted.\n        strMetrics -= \"numDeletedRows\"\n        strMetrics -= \"numCopiedRows\"\n        strMetrics -= \"numAddedFiles\"\n      }\n\n      val dvMetrics = transformDeletionVectorMetrics(metrics)\n      strMetrics ++ dvMetrics\n    }\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n  /** Recorded when truncating the table. */\n  case class Truncate() extends Operation(\"TRUNCATE\") {\n    override val parameters: Map[String, Any] = Map.empty\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.TRUNCATE\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when converting a table into a Delta table. */\n  case class Convert(\n      numFiles: Long,\n      partitionBy: Seq[String],\n      collectStats: Boolean,\n      catalogTable: Option[String],\n      sourceFormat: Option[String]) extends Operation(\"CONVERT\") {\n    override val parameters: Map[String, Any] = Map(\n      \"numFiles\" -> numFiles,\n      \"partitionedBy\" -> JsonUtils.toJson(partitionBy),\n      \"collectStats\" -> collectStats) ++\n        catalogTable.map(\"catalogTable\" -> _) ++\n        sourceFormat.map(\"sourceFormat\" -> _)\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.CONVERT\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions with DVs and non-tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Represents the predicates and action type (insert, update, delete) for a Merge clause */\n  case class MergePredicate(\n      predicate: Option[String],\n      actionType: String)\n\n  object MergePredicate {\n    def apply(mergeClause: DeltaMergeIntoClause): MergePredicate = {\n      MergePredicate(\n        predicate = mergeClause.condition.map(_.simpleString(SQLConf.get.maxToStringFields)),\n        mergeClause.clauseType.toLowerCase())\n    }\n  }\n\n  /**\n   * Recorded when a merge operation is committed to the table.\n   *\n   * `updatePredicate`, `deletePredicate`, and `insertPredicate` are DEPRECATED.\n   * Only use `predicate`, `matchedPredicates`, `notMatchedPredicates` and\n   * `notMatchedBySourcePredicates` to record the merge.\n   */\n  val OP_MERGE = \"MERGE\"\n  case class Merge(\n      predicate: Option[Expression],\n      updatePredicate: Option[String],\n      deletePredicate: Option[String],\n      insertPredicate: Option[String],\n      matchedPredicates: Seq[MergePredicate],\n      notMatchedPredicates: Seq[MergePredicate],\n      notMatchedBySourcePredicates: Seq[MergePredicate]\n  )\n    extends OperationWithPredicates(OP_MERGE, predicate.toSeq) {\n\n    override val parameters: Map[String, Any] = {\n      super.parameters ++\n        updatePredicate.map(\"updatePredicate\" -> _).toMap ++\n        deletePredicate.map(\"deletePredicate\" -> _).toMap ++\n        insertPredicate.map(\"insertPredicate\" -> _).toMap +\n        (\"matchedPredicates\" -> JsonUtils.toJson(matchedPredicates)) +\n        (\"notMatchedPredicates\" -> JsonUtils.toJson(notMatchedPredicates)) +\n        (\"notMatchedBySourcePredicates\" -> JsonUtils.toJson(notMatchedBySourcePredicates))\n    }\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.MERGE\n\n    override def transformMetrics(metrics: Map[String, SQLMetric]): Map[String, String] = {\n\n      var strMetrics = super.transformMetrics(metrics)\n\n      strMetrics += \"numSourceRows\" -> metrics(\"operationNumSourceRows\").value.toString\n\n      // We have to recalculate \"numOutputRows\" to avoid counting CDC rows\n      if (metrics.contains(\"numTargetRowsInserted\") &&\n          metrics.contains(\"numTargetRowsUpdated\") &&\n          metrics.contains(\"numTargetRowsCopied\")) {\n        val actualNumOutputRows = metrics(\"numTargetRowsInserted\").value +\n          metrics(\"numTargetRowsUpdated\").value +\n          metrics(\"numTargetRowsCopied\").value\n        strMetrics += \"numOutputRows\" -> actualNumOutputRows.toString\n      }\n\n      val dvMetrics = transformDeletionVectorMetrics(\n        metrics, dvMetrics = DeltaOperationMetrics.MERGE_DELETION_VECTORS)\n      strMetrics ++= dvMetrics\n\n      strMetrics\n    }\n\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions with DVs and non-tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  object Merge {\n    /** constructor to provide default values for deprecated fields */\n    def apply(\n        predicate: Option[Expression],\n        matchedPredicates: Seq[MergePredicate],\n        notMatchedPredicates: Seq[MergePredicate],\n        notMatchedBySourcePredicates: Seq[MergePredicate]\n    ): Merge = Merge(\n          predicate,\n          updatePredicate = None,\n          deletePredicate = None,\n          insertPredicate = None,\n          matchedPredicates,\n          notMatchedPredicates,\n          notMatchedBySourcePredicates\n    )\n  }\n\n  /** Recorded when an update operation is committed to the table. */\n  val OP_UPDATE = \"UPDATE\"\n  case class Update(predicate: Option[Expression])\n      extends OperationWithPredicates(OP_UPDATE, predicate.toSeq) {\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.UPDATE\n\n    override def changesData: Boolean = true\n\n    override def transformMetrics(metrics: Map[String, SQLMetric]): Map[String, String] = {\n      val dvMetrics = transformDeletionVectorMetrics(metrics)\n      super.transformMetrics(metrics) ++ dvMetrics\n    }\n\n    // This operation shouldn't be introducing AddFile actions with DVs and non-tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n  /** Recorded when the table is created. */\n  case class CreateTable(\n      metadata: Metadata,\n      isManaged: Boolean,\n      asSelect: Boolean = false,\n      clusterBy: Option[Seq[String]] = None\n  ) extends Operation(\"CREATE TABLE\" + s\"${if (asSelect) \" AS SELECT\" else \"\"}\") {\n    override val parameters: Map[String, Any] = Map(\n      \"isManaged\" -> isManaged.toString,\n      \"description\" -> Option(metadata.description),\n      \"partitionBy\" -> JsonUtils.toJson(metadata.partitionColumns),\n      CLUSTERING_PARAMETER_KEY -> JsonUtils.toJson(clusterBy.getOrElse(Seq.empty)),\n      \"properties\" -> JsonUtils.toJson(metadata.configuration)\n    )\n    override val operationMetrics: Set[String] = if (!asSelect) {\n      Set()\n    } else {\n      DeltaOperationMetrics.WRITE\n    }\n    override def changesData: Boolean = asSelect\n\n    // This operation shouldn't be introducing AddFile actions with DVs and non-tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = true\n  }\n  /** Recorded when the table is replaced. */\n  case class ReplaceTable(\n      metadata: Metadata,\n      isManaged: Boolean,\n      orCreate: Boolean,\n      asSelect: Boolean = false,\n      override val userMetadata: Option[String] = None,\n      clusterBy: Option[Seq[String]] = None,\n      predicate: Option[String] = None,\n      isDynamicPartitionOverwrite: Option[Boolean] = None,\n      canOverwriteSchema: Option[Boolean] = None,\n      canMergeSchema: Option[Boolean] = None,\n      isV1SaveAsTableOverwrite: Option[Boolean] = None\n  ) extends Operation(s\"${if (orCreate) \"CREATE OR \" else \"\"}REPLACE TABLE\" +\n      s\"${if (asSelect) \" AS SELECT\" else \"\"}\") {\n    override val parameters: Map[String, Any] = Map(\n      \"isManaged\" -> isManaged.toString,\n      \"description\" -> Option(metadata.description),\n      \"partitionBy\" -> JsonUtils.toJson(metadata.partitionColumns),\n      CLUSTERING_PARAMETER_KEY -> JsonUtils.toJson(clusterBy.getOrElse(Seq.empty)),\n      \"properties\" -> JsonUtils.toJson(metadata.configuration)\n  ) ++\n    // Only log these fields when explicitly set to avoid noise in DESCRIBE HISTORY when users do\n    // not set them. This means we don't distinguish between explicitly disabled (false) and unset\n    // (defaults to disabled), but that's fine as the distinction is not particularly interesting.\n    predicate.map(\"predicate\" -> _) ++\n    isDynamicPartitionOverwrite.map(\"isDynamicPartitionOverwrite\" -> _) ++\n    canOverwriteSchema.map(\"canOverwriteSchema\" -> _) ++\n    canMergeSchema.map(\"canMergeSchema\" -> _) ++\n    isV1SaveAsTableOverwrite.map(\"isV1SaveAsTableOverwrite\" -> _)\n\n    private val insertOverwriteRemoveMetricsEnabled = SparkSession.active.conf.get(\n      DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED)\n\n    override val operationMetrics: Set[String] = if (!asSelect) {\n      Set()\n    } else {\n      val overwriteMetrics =\n        if (insertOverwriteRemoveMetricsEnabled) DeltaOperationMetrics.OVERWRITE_REMOVES\n        else Set.empty\n      DeltaOperationMetrics.WRITE ++ overwriteMetrics\n    }\n    override def changesData: Boolean = true\n\n    // This operation shouldn't be introducing AddFile actions with DVs and non-tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    // We allow ReplaceTable operations to change partition columns when they are\n    // 1) creating/replacing a new table, 2) not invoked via saveAsTable or 3) invoked via\n    // saveAsTable but with schema overwrite.\n    override def canChangePartitionColumns: Boolean = !isV1SaveAsTableOverwrite.getOrElse(false) ||\n      (isV1SaveAsTableOverwrite.getOrElse(false) && canOverwriteSchema.getOrElse(false))\n  }\n  /** Recorded when the table properties are set. */\n  val OP_SET_TBLPROPERTIES = \"SET TBLPROPERTIES\"\n  case class SetTableProperties(\n      properties: Map[String, String]) extends Operation(OP_SET_TBLPROPERTIES) {\n    override val parameters: Map[String, Any] = Map(\"properties\" -> JsonUtils.toJson(properties))\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    // Note: This operation may trigger additional actions and additional commits. For example\n    // RowTrackingBackfill. These are separate transactions, and this check is performed separately.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n  /** Recorded when the table properties are unset. */\n  case class UnsetTableProperties(\n      propKeys: Seq[String],\n      ifExists: Boolean) extends Operation(\"UNSET TBLPROPERTIES\") {\n    override val parameters: Map[String, Any] = Map(\n      \"properties\" -> JsonUtils.toJson(propKeys),\n      \"ifExists\" -> ifExists)\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n  /** Recorded when dropping a table feature. */\n  val OP_DROP_FEATURE = \"DROP FEATURE\"\n  case class DropTableFeature(\n      featureName: String,\n      truncateHistory: Boolean) extends Operation(OP_DROP_FEATURE) {\n    override val parameters: Map[String, Any] = Map(\n      \"featureName\" -> featureName,\n      \"truncateHistory\" -> truncateHistory)\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    // Note: this operation may trigger additional actions and additional commits. These would be\n    // separate transactions, and this check is performed separately.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(true)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /**\n   * Recorded when dropping deletion vectors. Deletion Vector tombstones directly reference\n   * deletion vector files within the retention period. This is to protect them from deletion\n   * against oblivious writers when vacuuming.\n   */\n  object AddDeletionVectorsTombstones extends Operation(\"Deletion Vector Tombstones\") {\n    override val parameters: Map[String, Any] = Map.empty\n\n    // This operation should only introduce RemoveFile actions.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when columns are added. */\n  case class AddColumns(\n      colsToAdd: Seq[QualifiedColTypeWithPositionForLog]) extends Operation(\"ADD COLUMNS\") {\n\n    override val parameters: Map[String, Any] = Map(\n      \"columns\" -> JsonUtils.toJson(colsToAdd.map {\n        case QualifiedColTypeWithPositionForLog(columnPath, column, colPosition) =>\n          Map(\n            \"column\" -> structFieldToMap(columnPath, column)\n          ) ++ colPosition.map(\"position\" -> _.toString)\n      }))\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when columns are dropped. */\n  val OP_DROP_COLUMN = \"DROP COLUMNS\"\n  case class DropColumns(\n    colsToDrop: Seq[Seq[String]]) extends Operation(OP_DROP_COLUMN) {\n\n    override val parameters: Map[String, Any] = Map(\n      \"columns\" -> JsonUtils.toJson(colsToDrop.map(UnresolvedAttribute(_).name)))\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when column is renamed */\n  val OP_RENAME_COLUMN = \"RENAME COLUMN\"\n  case class RenameColumn(oldColumnPath: Seq[String], newColumnPath: Seq[String])\n    extends Operation(OP_RENAME_COLUMN) {\n    override val parameters: Map[String, Any] = Map(\n      \"oldColumnPath\" -> UnresolvedAttribute(oldColumnPath).name,\n      \"newColumnPath\" -> UnresolvedAttribute(newColumnPath).name\n    )\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = true\n  }\n\n  /** Recorded when columns are changed. */\n  case class ChangeColumn(\n      columnPath: Seq[String],\n      columnName: String,\n      newColumn: StructField,\n      colPosition: Option[String]) extends Operation(\"CHANGE COLUMN\") {\n\n    override val parameters: Map[String, Any] = Map(\n      \"column\" -> JsonUtils.toJson(structFieldToMap(columnPath, newColumn))\n    ) ++ colPosition.map(\"position\" -> _)\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when columns are changed in bulk. */\n  case class ChangeColumns(columns: Seq[ChangeColumn]) extends Operation(\"CHANGE COLUMNS\") {\n\n    override val parameters: Map[String, Any] = Map(\n      \"columns\" -> JsonUtils.toJson(\n        columns.map(col =>\n          structFieldToMap(col.columnPath, col.newColumn) ++ col.colPosition.map(\"position\" -> _))\n      )\n    )\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when columns are replaced. */\n  case class ReplaceColumns(\n      columns: Seq[StructField]) extends Operation(\"REPLACE COLUMNS\") {\n\n    override val parameters: Map[String, Any] = Map(\n      \"columns\" -> JsonUtils.toJson(columns.map(structFieldToMap(Seq.empty, _))))\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  case class UpgradeProtocol(newProtocol: Protocol) extends Operation(\"UPGRADE PROTOCOL\") {\n    override val parameters: Map[String, Any] = Map(\"newProtocol\" -> JsonUtils.toJson(Map(\n      \"minReaderVersion\" -> newProtocol.minReaderVersion,\n      \"minWriterVersion\" -> newProtocol.minWriterVersion,\n      \"readerFeatures\" -> newProtocol.readerFeatures,\n      \"writerFeatures\" -> newProtocol.writerFeatures\n    )))\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  object ManualUpdate extends Operation(\"Manual Update\") {\n    override val parameters: Map[String, Any] = Map.empty\n\n    // Unsafe manual update disables checks.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = false\n\n    // Manual update operations can commit arbitrary actions. In case this field is needed consider\n    // adding a new Delta operation. For test-only code use TestOperation.\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = None\n\n    override def canChangePartitionColumns: Boolean = true\n  }\n\n  /** A commit without any actions. Could be used to force creation of new checkpoints. */\n  object EmptyCommit extends Operation(\"Empty Commit\") {\n    override val parameters: Map[String, Any] = Map.empty\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  case class UpdateColumnMetadata(\n      operationName: String,\n      columns: Seq[(Seq[String], StructField)])\n    extends Operation(operationName) {\n    override val parameters: Map[String, Any] = {\n      Map(\"columns\" -> JsonUtils.toJson(columns.map {\n        case (path, field) => structFieldToMap(path, field)\n      }))\n    }\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  case class UpdateSchema(oldSchema: StructType, newSchema: StructType)\n      extends Operation(\"UPDATE SCHEMA\") {\n    override val parameters: Map[String, Any] = Map(\n      \"oldSchema\" -> JsonUtils.toJson(oldSchema),\n      \"newSchema\" -> JsonUtils.toJson(newSchema))\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  case class AddConstraint(\n      constraintName: String, expr: String) extends Operation(\"ADD CONSTRAINT\") {\n    override val parameters: Map[String, Any] = Map(\"name\" -> constraintName, \"expr\" -> expr)\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  case class DropConstraint(\n      constraintName: String, expr: Option[String]) extends Operation(\"DROP CONSTRAINT\") {\n    override val parameters: Map[String, Any] = {\n      expr.map { e =>\n        Map(\"name\" -> constraintName, \"expr\" -> e, \"existed\" -> \"true\")\n      }.getOrElse {\n        Map(\"name\" -> constraintName, \"existed\" -> \"false\")\n      }\n    }\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when recomputing stats on the table. */\n  case class ComputeStats(predicate: Seq[Expression])\n      extends OperationWithPredicates(\"COMPUTE STATS\", predicate) {\n\n    // ComputeStats operation commits AddFiles with recomputed stats which are always tight bounds,\n    // even when DVs are present. This check should be disabled.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = false\n\n    // ComputeStats operation only updates statistics of existing files.\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(true)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when restoring a Delta table to an older version. */\n  val OP_RESTORE = \"RESTORE\"\n  case class Restore(\n      version: Option[Long],\n      timestamp: Option[String]) extends Operation(OP_RESTORE) {\n    override val parameters: Map[String, Any] = Map(\n      \"version\" -> version,\n      \"timestamp\" -> timestamp)\n    override def changesData: Boolean = true\n\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.RESTORE\n\n    // Restore operation commits AddFiles with files, DVs and stats from the version it restores to.\n    // It can happen that tight bound stats were recomputed before by ComputeStats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = false\n\n    // The restore operation could perform in-place file metadata updates. However, the difference\n    // between the current and the restored state is computed using only the (path, DV) pairs as\n    // identifiers, meaning that metadata differences are ignored.\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  sealed abstract class OptimizeOrReorg(override val name: String, predicates: Seq[Expression])\n    extends OperationWithPredicates(name, predicates)\n\n  /** operation name for ROW TRACKING BACKFILL command */\n  val ROW_TRACKING_BACKFILL_OPERATION_NAME = \"ROW TRACKING BACKFILL\"\n  val ROW_TRACKING_UNBACKFILL_OPERATION_NAME = \"ROW TRACKING UNBACKFILL\"\n\n  /** parameter key to indicate whether it's an Auto Compaction */\n  val AUTO_COMPACTION_PARAMETER_KEY = \"auto\"\n\n  /** operation name for REORG command */\n  val REORG_OPERATION_NAME = \"REORG\"\n  /** operation name for OPTIMIZE command */\n  val OPTIMIZE_OPERATION_NAME = \"OPTIMIZE\"\n  /** parameter key to indicate which columns to z-order by */\n  val ZORDER_PARAMETER_KEY = \"zOrderBy\"\n  /** parameter key to indicate clustering columns */\n  val CLUSTERING_PARAMETER_KEY = \"clusterBy\"\n  /** parameter key to indicate the operation for `OPTIMIZE tbl FULL` */\n  val CLUSTERING_IS_FULL_KEY = \"isFull\"\n\n  /** Recorded when optimizing the table. */\n  case class Optimize(\n      predicate: Seq[Expression],\n      zOrderBy: Seq[String] = Seq.empty,\n      auto: Boolean = false,\n      clusterBy: Option[Seq[String]] = None,\n      isFull: Boolean = false\n  ) extends OptimizeOrReorg(OPTIMIZE_OPERATION_NAME, predicate) {\n    override val parameters: Map[String, Any] = super.parameters ++ Map(\n      // When clustering columns are specified, set the zOrderBy key to empty.\n      ZORDER_PARAMETER_KEY -> JsonUtils.toJson(if (clusterBy.isEmpty) zOrderBy else Seq.empty),\n      CLUSTERING_PARAMETER_KEY -> JsonUtils.toJson(clusterBy.getOrElse(Seq.empty)),\n      AUTO_COMPACTION_PARAMETER_KEY -> auto\n    )\n    // `isFull` is not relevant for non-clustering tables, so skip it.\n    .++(clusterBy.filter(_.nonEmpty).map(_ => CLUSTERING_IS_FULL_KEY -> isFull))\n\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.OPTIMIZE\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when cloning a Delta table into a new location. */\n  val OP_CLONE = \"CLONE\"\n  case class Clone(\n      source: String,\n      sourceVersion: Long\n  ) extends Operation(OP_CLONE) {\n    override val parameters: Map[String, Any] = Map(\n      \"source\" -> source,\n      \"sourceVersion\" -> sourceVersion\n    )\n    override def changesData: Boolean = true\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.CLONE\n\n    // Clone operation commits AddFiles with files, DVs and stats copied over from the source table.\n    // It can happen that tight bound stats were recomputed before by ComputeStats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = false\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = true\n  }\n\n  /**\n   * @param retentionCheckEnabled - whether retention check was enabled for this run of vacuum.\n   * @param specifiedRetentionMillis - specified retention interval\n   * @param defaultRetentionMillis - default retention period for the table\n   */\n  case class VacuumStart(\n      retentionCheckEnabled: Boolean,\n      specifiedRetentionMillis: Option[Long],\n      defaultRetentionMillis: Long) extends Operation(VacuumStart.OPERATION_NAME) {\n    override val parameters: Map[String, Any] = Map(\n      \"retentionCheckEnabled\" -> retentionCheckEnabled,\n      \"defaultRetentionMillis\" -> defaultRetentionMillis\n    ) ++ specifiedRetentionMillis.map(\"specifiedRetentionMillis\" -> _)\n\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.VACUUM_START\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  object VacuumStart {\n    val OPERATION_NAME = \"VACUUM START\"\n  }\n\n  /**\n   * @param status - whether the vacuum operation was successful; either \"COMPLETED\" or \"FAILED\"\n   */\n  case class VacuumEnd(status: String) extends Operation(VacuumEnd.OPERATION_NAME) {\n    override val parameters: Map[String, Any] = Map(\n      \"status\" -> status\n    )\n\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.VACUUM_END\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  object VacuumEnd {\n    val OPERATION_NAME = \"VACUUM END\"\n  }\n\n  /** Recorded when running REORG on the table. */\n  case class Reorg(\n      predicate: Seq[Expression],\n      applyPurge: Boolean = true) extends OptimizeOrReorg(REORG_OPERATION_NAME, predicate) {\n    override val parameters: Map[String, Any] = super.parameters ++ Map(\n      \"applyPurge\" -> applyPurge\n    )\n\n    override val operationMetrics: Set[String] = DeltaOperationMetrics.OPTIMIZE\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when clustering columns are changed on clustered tables. */\n  case class ClusterBy(\n      oldClusteringColumns: String,\n      newClusteringColumns: String) extends Operation(\"CLUSTER BY\") {\n    override val parameters: Map[String, Any] = Map(\n      \"oldClusteringColumns\" -> oldClusteringColumns,\n      \"newClusteringColumns\" -> newClusteringColumns)\n\n    // This operation shouldn't be introducing AddFile actions at all. This check should be trivial.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /** Recorded when we backfill a Delta table's existing AddFiles with row tracking data. */\n  case class RowTrackingBackfill(\n      batchId: Int = 0) extends Operation(ROW_TRACKING_BACKFILL_OPERATION_NAME) {\n    override val parameters: Map[String, Any] = Map(\n      \"batchId\" -> JsonUtils.toJson(batchId)\n    )\n\n    // RowTrackingBackfill operation commits AddFiles with files, DVs and stats copied over.\n    // It can happen that tight bound stats were recomputed before by ComputeStats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = false\n\n    // RowTrackingBackfill only updates tags of existing files.\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(true)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /**\n   * Recorded when we unbackfill a Delta table's existing row tracking data from AddFiles.\n   * This operation is used when dropping the row tracking feature.\n   */\n  case class RowTrackingUnBackfill(\n      batchId: Int = 0) extends Operation(ROW_TRACKING_UNBACKFILL_OPERATION_NAME) {\n    override val parameters: Map[String, Any] = Map(\n      \"batchId\" -> JsonUtils.toJson(batchId)\n    )\n\n    // RowTrackingUnBackfill operation commits AddFiles with files, DVs and stats copied over.\n    // It can happen that tight bound stats were recomputed before by ComputeStats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = false\n\n    // RowTrackingUnBackfill only updates metadata of existing files.\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(true)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  private def structFieldToMap(colPath: Seq[String], field: StructField): Map[String, Any] = {\n    Map(\n      \"name\" -> UnresolvedAttribute(colPath :+ field.name).name,\n      \"type\" -> field.dataType.typeName,\n      \"nullable\" -> field.nullable,\n      \"metadata\" -> JsonUtils.mapper.readValue[Map[String, Any]](field.metadata.json)\n    )\n  }\n\n  /**\n   * Recorded when cleaning up domain metadata. This process takes place when dropping\n   * the domainMetadata feature.\n   */\n  case class DomainMetadataCleanup(domainMetadataRemovedCount: Int)\n      extends Operation(\"DOMAIN METADATA CLEANUP\") {\n    override val parameters: Map[String, Any] = Map(\n      \"domainMetadataRemovedCount\" -> domainMetadataRemovedCount)\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    // Only removes domain metadata.\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /**\n   * Qualified column type with position. We define a copy of the type here to avoid depending on\n   * the parser output classes in our logging.\n   */\n  case class QualifiedColTypeWithPositionForLog(\n     columnPath: Seq[String],\n     column: StructField,\n     colPosition: Option[String])\n\n  /** Dummy operation only for testing with arbitrary operation names */\n  case class TestOperation(\n    operationName: String = \"TEST\",\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = None\n  ) extends Operation(operationName) {\n    override val parameters: Map[String, Any] = Map.empty\n\n    // Perform the check for testing.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n\n  /**\n   * Helper method to convert a sequence of command predicates in the form of an\n   * [[Expression]]s to a sequence of Strings so be stored in the commit info.\n   */\n  def predicatesToString(predicates: Seq[Expression]): Seq[String] = {\n    val maxToStringFields = SQLConf.get.maxToStringFields\n    predicates.map(_.simpleString(maxToStringFields))\n  }\n\n  /** Recorded when the table properties are set. */\n  private val OP_UPGRADE_UNIFORM_BY_REORG = \"REORG TABLE UPGRADE UNIFORM\"\n\n  /**\n   * recorded when upgrading a table set uniform properties by REORG TABLE ... UPGRADE UNIFORM\n   */\n  case class UpgradeUniformProperties(properties: Map[String, String]) extends Operation(\n      OP_UPGRADE_UNIFORM_BY_REORG) {\n    override val parameters: Map[String, Any] = Map(\"properties\" -> JsonUtils.toJson(properties))\n\n    // This operation shouldn't be introducing AddFile actions with DVs and tight bounds stats.\n    override def checkAddFileWithDeletionVectorStatsAreNotTightBounds: Boolean = true\n\n    override val isInPlaceFileMetadataUpdate: Option[Boolean] = Some(false)\n\n    override def canChangePartitionColumns: Boolean = false\n  }\n}\n\nprivate[delta] object DeltaOperationMetrics {\n  val WRITE = Set(\n    \"numFiles\", // number of files written\n    \"numOutputBytes\", // size in bytes of the written contents\n    \"numOutputRows\" // number of rows written\n  )\n\n  val OVERWRITE_REMOVES = Set(\n    \"numRemovedFiles\",\n    \"numRemovedBytes\"\n  )\n\n  val REMOVE_COLUMN_MAPPING: Set[String] = Set(\n    \"numRewrittenFiles\",\n    \"numOutputBytes\",\n    \"numRemovedBytes\",\n    \"numCopiedRows\",\n    \"numDeletionVectorsRemoved\"\n  )\n\n  val STREAMING_UPDATE = Set(\n    \"numAddedFiles\", // number of files added\n    \"numRemovedFiles\", // number of files removed\n    \"numOutputRows\", // number of rows written\n    \"numOutputBytes\" // number of output writes\n  )\n\n  val DELETE = Set(\n    \"numAddedFiles\", // number of files added\n    \"numRemovedFiles\", // number of files removed\n    \"numDeletionVectorsAdded\", // number of deletion vectors added\n    \"numDeletionVectorsRemoved\", // number of deletion vectors removed\n    \"numDeletionVectorsUpdated\", // number of deletion vectors updated\n    \"numAddedChangeFiles\", // number of CDC files\n    \"numDeletedRows\", // number of rows removed\n    \"numCopiedRows\", // number of rows copied in the process of deleting files\n    \"executionTimeMs\", // time taken to execute the entire operation\n    \"scanTimeMs\", // time taken to scan the files for matches\n    \"rewriteTimeMs\", // time taken to rewrite the matched files\n    \"numRemovedBytes\", // number of bytes removed\n    \"numAddedBytes\" // number of bytes added\n  )\n\n  val WRITE_REPLACE_WHERE = Set(\n    \"numFiles\", // number of files written\n    \"numOutputBytes\", // size in bytes of the written\n    \"numOutputRows\", // number of rows written\n    \"numRemovedFiles\", // number of files removed\n    \"numAddedChangeFiles\", // number of CDC files\n    \"numDeletedRows\", // number of rows removed\n    \"numCopiedRows\", // number of rows copied in the process of deleting files\n    \"numRemovedBytes\" // number of bytes removed\n  )\n\n  val WRITE_REPLACE_WHERE_PARTITIONS = Set(\n    \"numFiles\", // number of files written\n    \"numOutputBytes\", // size in bytes of the written contents\n    \"numOutputRows\", // number of rows written\n    \"numAddedChangeFiles\", // number of CDC files\n    \"numRemovedFiles\", // number of files removed\n    // Records below only exist when DELTA_DML_METRICS_FROM_METADATA is enabled\n    \"numCopiedRows\", // number of rows copied\n    \"numDeletedRows\", // number of rows deleted\n    \"numRemovedBytes\" // number of bytes removed\n  )\n\n  /**\n   * Deleting the entire table or partition will record row level metrics when\n   * DELTA_DML_METRICS_FROM_METADATA is enabled\n   * * DELETE_PARTITIONS is used only in test to verify specific delete cases.\n   */\n  val DELETE_PARTITIONS = Set(\n    \"numRemovedFiles\", // number of files removed\n    \"numAddedChangeFiles\", // number of CDC files generated - generally 0 in this case\n    \"numDeletionVectorsAdded\", // number of deletion vectors added\n    \"numDeletionVectorsRemoved\", // number of deletion vectors removed\n    \"numDeletionVectorsUpdated\", // number of deletion vectors updated\n    \"executionTimeMs\", // time taken to execute the entire operation\n    \"scanTimeMs\", // time taken to scan the files for matches\n    \"rewriteTimeMs\", // time taken to rewrite the matched files\n    // Records below only exist when DELTA_DML_METRICS_FROM_METADATA is enabled\n    \"numCopiedRows\", // number of rows copied\n    \"numDeletedRows\", // number of rows deleted\n    \"numAddedFiles\", // number of files added\n    \"numRemovedBytes\", // number of bytes removed\n    \"numAddedBytes\" // number of bytes added\n  )\n\n\n  trait MetricsTransformer {\n    /**\n     * Produce the output metric `metricName`, given all available metrics.\n     *\n     * If one or more input metrics are missing, the output metrics may be skipped by\n     * returning `None`.\n     */\n    def transform(\n        metricName: String,\n        allMetrics: Map[String, SQLMetric]): Option[(String, Long)]\n\n    def transformToString(\n        metricName: String,\n        allMetrics: Map[String, SQLMetric]): Option[(String, String)] = {\n      this.transform(metricName, allMetrics).map { case (name, metric) =>\n        name -> metric.toString\n      }\n    }\n  }\n\n  /** Pass metric on unaltered. */\n  final object PassMetric extends MetricsTransformer {\n    override def transform(\n        metricName: String,\n        allMetrics: Map[String, SQLMetric]): Option[(String, Long)] =\n      allMetrics.get(metricName).map(metric => metricName -> metric.value)\n  }\n\n  /**\n   * Produce a new metric by summing up the values of `inputMetrics`.\n   *\n   * Treats missing metrics at 0.\n   */\n  final case class SumMetrics(inputMetrics: String*)\n    extends MetricsTransformer {\n\n    override def transform(\n        metricName: String,\n        allMetrics: Map[String, SQLMetric]): Option[(String, Long)] = {\n      var atLeastOneMetricExists = false\n      val total = inputMetrics.map { name =>\n        val metricValueOpt = allMetrics.get(name)\n        atLeastOneMetricExists |= metricValueOpt.isDefined\n        metricValueOpt.map(_.value).getOrElse(0L)\n      }.sum\n      if (atLeastOneMetricExists) {\n        Some(metricName -> total)\n      } else {\n        None\n      }\n    }\n  }\n\n  val DELETION_VECTORS: Map[String, MetricsTransformer] = Map(\n    // Adding \"numDeletionVectorsUpdated\" here makes the values line up with how\n    // \"numFilesAdded\"/\"numFilesRemoved\" behave.\n    \"numDeletionVectorsAdded\" -> SumMetrics(\"numDeletionVectorsAdded\", \"numDeletionVectorsUpdated\"),\n    \"numDeletionVectorsRemoved\" ->\n      SumMetrics(\"numDeletionVectorsRemoved\", \"numDeletionVectorsUpdated\")\n  )\n\n  // The same as [[DELETION_VECTORS]] but with the \"Target\" prefix that is used by MERGE.\n  val MERGE_DELETION_VECTORS = Map(\n    // Adding \"numDeletionVectorsUpdated\" here makes the values line up with how\n    // \"numFilesAdded\"/\"numFilesRemoved\" behave.\n    \"numTargetDeletionVectorsAdded\" ->\n      SumMetrics(\"numTargetDeletionVectorsAdded\", \"numTargetDeletionVectorsUpdated\"),\n    \"numTargetDeletionVectorsRemoved\" ->\n      SumMetrics(\"numTargetDeletionVectorsRemoved\", \"numTargetDeletionVectorsUpdated\")\n  )\n\n  val TRUNCATE = Set(\n    \"numRemovedFiles\", // number of files removed\n    \"executionTimeMs\" // time taken to execute the entire operation\n  )\n\n  val CONVERT = Set(\n    \"numConvertedFiles\" // number of parquet files that have been converted.\n  )\n\n  val MERGE = Set(\n    \"numSourceRows\", // number of rows in the source dataframe\n    \"numTargetRowsInserted\", // number of rows inserted into the target table.\n    \"numTargetRowsUpdated\", // number of rows updated in the target table.\n    \"numTargetRowsMatchedUpdated\", // number of rows updated by a matched clause.\n    // number of rows updated by a not matched by source clause.\n    \"numTargetRowsNotMatchedBySourceUpdated\",\n    \"numTargetRowsDeleted\", // number of rows deleted in the target table.\n    \"numTargetRowsMatchedDeleted\", // number of rows deleted by a matched clause.\n    // number of rows deleted by a not matched by source clause.\n    \"numTargetRowsNotMatchedBySourceDeleted\",\n    \"numTargetRowsCopied\", // number of target rows copied\n    \"numTargetBytesAdded\", // number of target bytes added\n    \"numTargetBytesRemoved\", // number of target bytes removed\n    \"numOutputRows\", // total number of rows written out\n    \"numTargetFilesAdded\", // num files added to the sink(target)\n    \"numTargetFilesRemoved\", // number of files removed from the sink(target)\n    \"numTargetChangeFilesAdded\", // number of CDC files\n    \"executionTimeMs\",  // time taken to execute the entire operation\n    \"materializeSourceTimeMs\", // time taken to materialize source (or determine it's not needed)\n    \"scanTimeMs\", // time taken to scan the files for matches\n    \"rewriteTimeMs\", // time taken to rewrite the matched files\n    \"numTargetDeletionVectorsAdded\", // number of deletion vectors added\n    \"numTargetDeletionVectorsRemoved\", // number of deletion vectors removed\n    \"numTargetDeletionVectorsUpdated\" // number of deletion vectors updated\n  )\n\n  val UPDATE = Set(\n    \"numAddedFiles\", // number of files added\n    \"numRemovedFiles\", // number of files removed\n    \"numAddedChangeFiles\", // number of CDC files\n    \"numDeletionVectorsAdded\", // number of deletion vectors added\n    \"numDeletionVectorsRemoved\", // number of deletion vectors removed\n    \"numDeletionVectorsUpdated\", // number of deletion vectors updated\n    \"numUpdatedRows\", // number of rows updated\n    \"numCopiedRows\", // number of rows just copied over in the process of updating files.\n    \"executionTimeMs\",  // time taken to execute the entire operation\n    \"scanTimeMs\", // time taken to scan the files for matches\n    \"rewriteTimeMs\", // time taken to rewrite the matched files\n    \"numRemovedBytes\", // number of bytes removed\n    \"numAddedBytes\" // number of bytes added\n  )\n\n  val OPTIMIZE = Set(\n    \"numAddedFiles\", // number of data files added\n    \"numRemovedFiles\", // number of data files removed\n    \"numAddedBytes\", // number of data bytes added by optimize\n    \"numRemovedBytes\", // number of data bytes removed by optimize\n    \"minFileSize\", // the size of the smallest file\n    \"p25FileSize\", // the size of the 25th percentile file\n    \"p50FileSize\", // the median file size\n    \"p75FileSize\", // the 75th percentile of the file sizes\n    \"maxFileSize\", // the size of the largest file\n    \"numDeletionVectorsRemoved\" // number of deletion vectors removed by optimize\n  )\n\n  val RESTORE = Set(\n    \"tableSizeAfterRestore\", // table size in bytes after restore\n    \"numOfFilesAfterRestore\", // number of files in the table after restore\n    \"numRemovedFiles\", // number of files removed by the restore operation\n    \"numRestoredFiles\", // number of files that were added as a result of the restore\n    \"removedFilesSize\", // size in bytes of files removed by the restore\n    \"restoredFilesSize\" // size in bytes of files added by the restore\n  )\n\n  val CLONE = Set(\n    \"sourceTableSize\", // size in bytes of source table at version\n    \"sourceNumOfFiles\", // number of files in source table at version\n    \"numRemovedFiles\", // number of files removed from target table if delta table was replaced\n    \"numCopiedFiles\", // number of files that were cloned - 0 for shallow tables\n    \"removedFilesSize\", // size in bytes of files removed from an existing Delta table if one exists\n    \"copiedFilesSize\" // size of files copied - 0 for shallow tables\n  )\n\n  val VACUUM_START = Set(\n    \"numFilesToDelete\", // number of files that will be deleted by vacuum\n    \"sizeOfDataToDelete\" // total size in bytes of files that will be deleted by vacuum\n  )\n\n  val VACUUM_END = Set(\n    \"numDeletedFiles\", // number of files deleted by vacuum\n    \"numVacuumedDirectories\" // number of directories vacuumed\n  )\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaOptions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.Locale\nimport java.util.regex.PatternSyntaxException\n\nimport scala.util.Try\nimport scala.util.matching.Regex\n\nimport org.apache.spark.sql.connector.catalog.SupportsV1OverwriteWithSaveAsTable\nimport org.apache.spark.sql.delta.DeltaOptions.{DATA_CHANGE_OPTION, IS_DATAFRAME_WRITER_V1_SAVE_AS_TABLE_OVERWRITE, MERGE_SCHEMA_OPTION, OVERWRITE_SCHEMA_OPTION, PARTITION_OVERWRITE_MODE_OPTION}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.network.util.{ByteUnit, JavaUtils}\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.internal.SQLConf\n\n\ntrait DeltaOptionParser {\n  protected def sqlConf: SQLConf\n  protected def options: CaseInsensitiveMap[String]\n\n  def toBoolean(input: String, name: String): Boolean = {\n    Try(input.toBoolean).toOption.getOrElse {\n      throw DeltaErrors.illegalDeltaOptionException(name, input, \"must be 'true' or 'false'\")\n    }\n  }\n}\n\ntrait DeltaWriteOptions\n  extends DeltaWriteOptionsImpl\n  with DeltaOptionParser {\n\n  import DeltaOptions._\n\n  val replaceWhere: Option[String] = options.get(REPLACE_WHERE_OPTION)\n  val userMetadata: Option[String] = options.get(USER_METADATA_OPTION)\n\n  /**\n   * Whether to add an adaptive shuffle before writing out the files to break skew, and coalesce\n   * data into chunkier files.\n   */\n  val optimizeWrite: Option[Boolean] = options.get(OPTIMIZE_WRITE_OPTION)\n    .map(toBoolean(_, OPTIMIZE_WRITE_OPTION))\n\n}\n\ntrait DeltaWriteOptionsImpl extends DeltaOptionParser {\n  import DeltaOptions._\n\n  /**\n   * Whether the user has enabled auto schema merging in writes using either a DataFrame option\n   * or SQL Session configuration. Automerging is off when table ACLs are enabled.\n   * We always respect the DataFrame writer configuration over the session config.\n   */\n  def canMergeSchema: Boolean = {\n    options.get(MERGE_SCHEMA_OPTION)\n      .map(toBoolean(_, MERGE_SCHEMA_OPTION))\n      .getOrElse(sqlConf.getConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE))\n  }\n\n  /**\n   * Whether to allow overwriting the schema of a Delta table in an overwrite mode operation. If\n   * ACLs are enabled, we can't change the schema of an operation through a write, which requires\n   * MODIFY permissions, when schema changes require OWN permissions.\n   */\n  def canOverwriteSchema: Boolean = {\n    options.get(OVERWRITE_SCHEMA_OPTION).exists(toBoolean(_, OVERWRITE_SCHEMA_OPTION))\n  }\n\n  /**\n   * Whether this write is coming from DataFrameWriter V1 saveAsTable.\n   */\n  def isDataFrameWriterV1SaveAsTableOverwrite: Boolean = {\n    options.get(IS_DATAFRAME_WRITER_V1_SAVE_AS_TABLE_OVERWRITE)\n      .exists(toBoolean(_, IS_DATAFRAME_WRITER_V1_SAVE_AS_TABLE_OVERWRITE))\n  }\n\n  /**\n   * Whether to write new data to the table or just rearrange data that is already\n   * part of the table. This option declares that the data being written by this job\n   * does not change any data in the table and merely rearranges existing data.\n   * This makes sure streaming queries reading from this table will not see any new changes\n   */\n  def rearrangeOnly: Boolean = {\n    options.get(DATA_CHANGE_OPTION).exists(!toBoolean(_, DATA_CHANGE_OPTION))\n  }\n\n  val txnVersion = options.get(TXN_VERSION).map { str =>\n    Try(str.toLong).toOption.filter(_ >= 0).getOrElse {\n      throw DeltaErrors.illegalDeltaOptionException(\n        TXN_VERSION, str, \"must be a non-negative integer\")\n    }\n  }\n\n  val txnAppId = options.get(TXN_APP_ID)\n\n  private def validateIdempotentWriteOptions(): Unit = {\n    // Either both txnVersion and txnAppId must be specified to get idempotent writes or\n    // neither must be given. In all other cases, throw an exception.\n    val numOptions = txnVersion.size + txnAppId.size\n    if (numOptions != 0 && numOptions != 2) {\n      throw DeltaErrors.invalidIdempotentWritesOptionsException(\"Both txnVersion and txnAppId \" +\n      \"must be specified for idempotent data frame writes\")\n    }\n  }\n\n  validateIdempotentWriteOptions()\n\n  /** Whether partitionOverwriteMode is provided as a DataFrameWriter option. */\n  val partitionOverwriteModeInOptions: Boolean =\n    options.contains(PARTITION_OVERWRITE_MODE_OPTION)\n\n  /** Whether to only overwrite partitions that have data written into it at runtime. */\n  def isDynamicPartitionOverwriteMode: Boolean = {\n    val mode = options.get(PARTITION_OVERWRITE_MODE_OPTION)\n      .getOrElse(sqlConf.getConf(SQLConf.PARTITION_OVERWRITE_MODE).toString)\n    val modeIsDynamic = mode != null && mode.equalsIgnoreCase(PARTITION_OVERWRITE_MODE_DYNAMIC)\n    if (!sqlConf.getConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED)) {\n      // Raise an exception when DYNAMIC_PARTITION_OVERWRITE_ENABLED=false\n      // but users explicitly request dynamic partition overwrite.\n      if (modeIsDynamic) {\n        throw DeltaErrors.deltaDynamicPartitionOverwriteDisabled()\n      }\n      // If dynamic partition overwrite mode is disabled, fallback to the default behavior\n      false\n    } else {\n      if (mode == null ||\n        !DeltaOptions.PARTITION_OVERWRITE_MODE_VALUES.exists(mode.equalsIgnoreCase(_))) {\n        val acceptableStr =\n          DeltaOptions.PARTITION_OVERWRITE_MODE_VALUES.map(\"'\" + _ + \"'\").mkString(\" or \")\n        throw DeltaErrors.illegalDeltaOptionException(\n          PARTITION_OVERWRITE_MODE_OPTION, mode, s\"must be ${acceptableStr}\"\n        )\n      }\n      modeIsDynamic\n    }\n  }\n}\n\ntrait DeltaReadOptions extends DeltaOptionParser {\n  import DeltaOptions._\n\n  val maxFilesPerTrigger = options.get(MAX_FILES_PER_TRIGGER_OPTION).map { str =>\n    Try(str.toInt).toOption.filter(_ > 0).getOrElse {\n      throw DeltaErrors.illegalDeltaOptionException(\n        MAX_FILES_PER_TRIGGER_OPTION, str, \"must be a positive integer\")\n    }\n  }\n\n  val maxBytesPerTrigger = options.get(MAX_BYTES_PER_TRIGGER_OPTION).map { str =>\n    Try(JavaUtils.byteStringAs(str, ByteUnit.BYTE)).toOption.filter(_ > 0).getOrElse {\n      throw DeltaErrors.illegalDeltaOptionException(\n        MAX_BYTES_PER_TRIGGER_OPTION, str, \"must be a size configuration such as '10g'\")\n    }\n  }\n\n  val ignoreFileDeletion = options.get(IGNORE_FILE_DELETION_OPTION)\n    .exists(toBoolean(_, IGNORE_FILE_DELETION_OPTION))\n\n  val ignoreChanges = options.get(IGNORE_CHANGES_OPTION).exists(toBoolean(_, IGNORE_CHANGES_OPTION))\n\n  val ignoreDeletes = options.get(IGNORE_DELETES_OPTION).exists(toBoolean(_, IGNORE_DELETES_OPTION))\n\n  val skipChangeCommits = options.get(SKIP_CHANGE_COMMITS_OPTION)\n    .exists(toBoolean(_, SKIP_CHANGE_COMMITS_OPTION))\n\n  val failOnDataLoss = options.get(FAIL_ON_DATA_LOSS_OPTION)\n    .forall(toBoolean(_, FAIL_ON_DATA_LOSS_OPTION)) // thanks to forall: by default true\n\n  val readChangeFeed = options.get(CDC_READ_OPTION).exists(toBoolean(_, CDC_READ_OPTION)) ||\n    options.get(CDC_READ_OPTION_LEGACY).exists(toBoolean(_, CDC_READ_OPTION_LEGACY))\n\n\n  val excludeRegex: Option[Regex] = try options.get(EXCLUDE_REGEX_OPTION).map(_.r) catch {\n    case e: PatternSyntaxException =>\n      throw DeltaErrors.excludeRegexOptionException(EXCLUDE_REGEX_OPTION, e)\n  }\n\n  val startingVersion: Option[DeltaStartingVersion] = options.get(STARTING_VERSION_OPTION).map {\n    case \"latest\" => StartingVersionLatest\n    case str =>\n      Try(str.toLong).toOption.filter(_ >= 0).map(StartingVersion).getOrElse{\n        throw DeltaErrors.illegalDeltaOptionException(\n          STARTING_VERSION_OPTION, str, \"must be greater than or equal to zero\")\n      }\n  }\n\n  val startingTimestamp = options.get(STARTING_TIMESTAMP_OPTION)\n\n  private def provideOneStartingOption(): Unit = {\n    if (startingTimestamp.isDefined && startingVersion.isDefined) {\n      throw DeltaErrors.startingVersionAndTimestampBothSetException(\n        STARTING_VERSION_OPTION,\n        STARTING_TIMESTAMP_OPTION)\n    }\n  }\n\n  def containsStartingVersionOrTimestamp: Boolean = {\n    options.contains(STARTING_VERSION_OPTION) || options.contains(STARTING_TIMESTAMP_OPTION)\n  }\n\n  provideOneStartingOption()\n\n  val schemaTrackingLocation = options.get(SCHEMA_TRACKING_LOCATION)\n\n  val sourceTrackingId = options.get(STREAMING_SOURCE_TRACKING_ID)\n\n  val allowSourceColumnRename = options.get(ALLOW_SOURCE_COLUMN_RENAME)\n\n  val allowSourceColumnDrop = options.get(ALLOW_SOURCE_COLUMN_DROP)\n\n  val allowSourceColumnTypeChange = options.get(ALLOW_SOURCE_COLUMN_TYPE_CHANGE)\n}\n\n\n/**\n * Options for the Delta data source.\n */\nclass DeltaOptions(\n    @transient protected[delta] val options: CaseInsensitiveMap[String],\n    @transient protected val sqlConf: SQLConf)\n  extends DeltaWriteOptions with DeltaReadOptions with Serializable {\n\n  DeltaOptions.verifyOptions(options)\n\n  def this(options: Map[String, String], conf: SQLConf) = this(CaseInsensitiveMap(options), conf)\n}\n\nobject DeltaOptions extends DeltaLogging {\n\n  /** Internal option to indicate write originated from DataFrameWriter V1 saveAsTable. */\n  val IS_DATAFRAME_WRITER_V1_SAVE_AS_TABLE_OVERWRITE =\n    SupportsV1OverwriteWithSaveAsTable.OPTION_NAME\n\n  /** An option to overwrite only the data that matches predicates over partition columns. */\n  val REPLACE_WHERE_OPTION = \"replaceWhere\"\n  /** An option to allow automatic schema merging during a write operation. */\n  val MERGE_SCHEMA_OPTION = \"mergeSchema\"\n  /** An option to allow overwriting schema and partitioning during an overwrite write operation. */\n  val OVERWRITE_SCHEMA_OPTION = \"overwriteSchema\"\n  /** An option to specify user-defined metadata in commitInfo */\n  val USER_METADATA_OPTION = \"userMetadata\"\n\n  val PARTITION_OVERWRITE_MODE_OPTION = \"partitionOverwriteMode\"\n  val PARTITION_OVERWRITE_MODE_DYNAMIC = \"DYNAMIC\"\n  val PARTITION_OVERWRITE_MODE_STATIC = \"STATIC\"\n  val PARTITION_OVERWRITE_MODE_VALUES =\n    Set(PARTITION_OVERWRITE_MODE_STATIC, PARTITION_OVERWRITE_MODE_DYNAMIC)\n\n  val MAX_FILES_PER_TRIGGER_OPTION = \"maxFilesPerTrigger\"\n  val MAX_FILES_PER_TRIGGER_OPTION_DEFAULT = 1000\n  val MAX_BYTES_PER_TRIGGER_OPTION = \"maxBytesPerTrigger\"\n  val EXCLUDE_REGEX_OPTION = \"excludeRegex\"\n  val IGNORE_FILE_DELETION_OPTION = \"ignoreFileDeletion\"\n  val IGNORE_CHANGES_OPTION = \"ignoreChanges\"\n  val IGNORE_DELETES_OPTION = \"ignoreDeletes\"\n  val SKIP_CHANGE_COMMITS_OPTION = \"skipChangeCommits\"\n  val FAIL_ON_DATA_LOSS_OPTION = \"failOnDataLoss\"\n  val OPTIMIZE_WRITE_OPTION = \"optimizeWrite\"\n  val DATA_CHANGE_OPTION = \"dataChange\"\n  val STARTING_VERSION_OPTION = \"startingVersion\"\n  val STARTING_TIMESTAMP_OPTION = \"startingTimestamp\"\n  val CDC_START_VERSION = \"startingVersion\"\n  val CDC_START_TIMESTAMP = \"startingTimestamp\"\n  val CDC_END_VERSION = \"endingVersion\"\n  val CDC_END_TIMESTAMP = \"endingTimestamp\"\n  val CDC_READ_OPTION = \"readChangeFeed\"\n  val CDC_READ_OPTION_LEGACY = \"readChangeData\"\n\n  val VERSION_AS_OF = \"versionAsOf\"\n  val TIMESTAMP_AS_OF = \"timestampAsOf\"\n\n  val COMPRESSION = \"compression\"\n  val MAX_RECORDS_PER_FILE = \"maxRecordsPerFile\"\n  val TXN_APP_ID = \"txnAppId\"\n  val TXN_VERSION = \"txnVersion\"\n\n  /**\n   * An option to allow column mapping enabled tables to conduct schema evolution during streaming\n   */\n  val SCHEMA_TRACKING_LOCATION = \"schemaTrackingLocation\"\n  /**\n   * Alias for `schemaTrackingLocation`, so users familiar with AutoLoader can migrate easily.\n   */\n  val SCHEMA_TRACKING_LOCATION_ALIAS = \"schemaLocation\"\n  /**\n   * An option to instruct DeltaSource to pick a customized subdirectory for schema log in case of\n   * rare conflicts such as when a stream needs to do a self-union of two Delta sources from the\n   * same table.\n   * The final schema log location will be $parent/_schema_log_${tahoeId}_${sourceTrackingId}.\n   */\n  val STREAMING_SOURCE_TRACKING_ID = \"streamingSourceTrackingId\"\n\n  val ALLOW_SOURCE_COLUMN_DROP = \"allowSourceColumnDrop\"\n  val ALLOW_SOURCE_COLUMN_RENAME = \"allowSourceColumnRename\"\n  val ALLOW_SOURCE_COLUMN_TYPE_CHANGE = \"allowSourceColumnTypeChange\"\n\n  /**\n   * An option to control if delta will write partition columns to data files\n   */\n  val WRITE_PARTITION_COLUMNS = \"writePartitionColumns\"\n\n  val validOptionKeys : Set[String] = Set(\n    IS_DATAFRAME_WRITER_V1_SAVE_AS_TABLE_OVERWRITE,\n    REPLACE_WHERE_OPTION,\n    MERGE_SCHEMA_OPTION,\n    EXCLUDE_REGEX_OPTION,\n    OVERWRITE_SCHEMA_OPTION,\n    USER_METADATA_OPTION,\n    PARTITION_OVERWRITE_MODE_OPTION,\n    MAX_FILES_PER_TRIGGER_OPTION,\n    IGNORE_FILE_DELETION_OPTION,\n    IGNORE_CHANGES_OPTION,\n    IGNORE_DELETES_OPTION,\n    FAIL_ON_DATA_LOSS_OPTION,\n    OPTIMIZE_WRITE_OPTION,\n    DATA_CHANGE_OPTION,\n    STARTING_TIMESTAMP_OPTION,\n    STARTING_VERSION_OPTION,\n    CDC_READ_OPTION,\n    CDC_READ_OPTION_LEGACY,\n    CDC_START_TIMESTAMP,\n    CDC_END_TIMESTAMP,\n    CDC_START_VERSION,\n    CDC_END_VERSION,\n    COMPRESSION,\n    MAX_RECORDS_PER_FILE,\n    TXN_APP_ID,\n    TXN_VERSION,\n    SCHEMA_TRACKING_LOCATION,\n    SCHEMA_TRACKING_LOCATION_ALIAS,\n    STREAMING_SOURCE_TRACKING_ID,\n    \"queryName\",\n    \"checkpointLocation\",\n    \"path\",\n    VERSION_AS_OF,\n    TIMESTAMP_AS_OF,\n    WRITE_PARTITION_COLUMNS\n  )\n\n\n  /** Iterates over all user passed options and logs any that are not valid. */\n  def verifyOptions(options: CaseInsensitiveMap[String]): Unit = {\n    val invalidUserOptions = SQLConf.get.redactOptions(options --\n      validOptionKeys.map(_.toLowerCase(Locale.ROOT)))\n    if (invalidUserOptions.nonEmpty) {\n      recordDeltaEvent(null,\n        \"delta.option.invalid\",\n        data = invalidUserOptions\n      )\n    }\n  }\n}\n\n/**\n * Definitions for the batch read schema mode for CDF\n */\nsealed trait DeltaBatchCDFSchemaMode {\n  def name: String\n}\n\n/**\n * `latest` batch CDF schema mode specifies that the latest schema should be used when serving\n * the CDF batch.\n */\ncase object BatchCDFSchemaLatest extends DeltaBatchCDFSchemaMode {\n  val name = \"latest\"\n}\n\n/**\n * `endVersion` batch CDF schema mode specifies that the query range's end version's schema should\n * be used for serving the CDF batch.\n * This is the current default for column mapping enabled tables so we could read using the exact\n * schema at the versions being queried to reduce schema read compatibility mismatches.\n */\ncase object BatchCDFSchemaEndVersion extends DeltaBatchCDFSchemaMode {\n  val name = \"endversion\"\n}\n\n/**\n * `legacy` batch CDF schema mode specifies that neither latest nor end version's schema is\n * strictly used for serving the CDF batch, e.g. when user uses TimeTravel with batch CDF and wants\n * to respect the time travelled schema.\n * This is the current default for non-column mapping tables.\n */\ncase object BatchCDFSchemaLegacy extends DeltaBatchCDFSchemaMode {\n  val name = \"legacy\"\n}\n\nobject DeltaBatchCDFSchemaMode {\n  def apply(name: String): DeltaBatchCDFSchemaMode = {\n    name.toLowerCase(Locale.ROOT) match {\n      case BatchCDFSchemaLatest.name => BatchCDFSchemaLatest\n      case BatchCDFSchemaEndVersion.name => BatchCDFSchemaEndVersion\n      case BatchCDFSchemaLegacy.name => BatchCDFSchemaLegacy\n    }\n  }\n}\n\n/**\n * Definitions for the starting version of a Delta stream.\n */\nsealed trait DeltaStartingVersion\ncase object StartingVersionLatest extends DeltaStartingVersion\ncase class StartingVersion(version: Long) extends DeltaStartingVersion\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaParquetFileFormat.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.RowIndexFilterType\nimport org.apache.spark.sql.delta.DeltaParquetFileFormat._\nimport org.apache.spark.sql.delta.actions.{DeletionVectorDescriptor, Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils.deletionVectorsReadable\nimport org.apache.spark.sql.delta.deletionvectors.{DropMarkedRowsFilter, KeepAllRowsFilter, KeepMarkedRowsFilter}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.hadoop.mapreduce.Job\nimport org.apache.parquet.hadoop.ParquetOutputFormat\nimport org.apache.parquet.hadoop.util.ContextUtil\n\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.FileSourceConstantMetadataStructField\nimport org.apache.spark.sql.execution.datasources.OutputWriterFactory\nimport org.apache.spark.sql.execution.datasources.PartitionedFile\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.execution.vectorized.{OffHeapColumnVector, OnHeapColumnVector, WritableColumnVector}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.sources._\nimport org.apache.spark.sql.types.{ByteType, LongType, MetadataBuilder, StringType, StructField, StructType}\nimport org.apache.spark.sql.vectorized.{ColumnarBatch, ColumnarBatchRow, ColumnVector}\nimport org.apache.spark.util.SerializableConfiguration\n\n/**\n * Base class for Delta Parquet file format that uses ProtocolMetadataAdapter abstraction.\n * A thin wrapper over the Parquet file format to support\n *  - columns names without restrictions.\n *  - populated a column from the deletion vector of this file (if exists) to indicate\n *    whether the row is deleted or not according to the deletion vector. Consumers\n *    of this scan can use the column values to filter out the deleted rows.\n *\n * @param protocolMetadataAdapter Adapter providing protocol and metadata info for the table\n * @param nullableRowTrackingConstantFields If true, row tracking constant fields (e.g., base row\n *                                          ID, default row commit version) are nullable in schema\n * @param nullableRowTrackingGeneratedFields If true, row tracking generated fields are nullable\n * @param optimizationsEnabled Whether to enable optimizations (file splitting, predicate pushdown)\n * @param tablePath Table path for deletion vector support; None disables DV processing\n * @param isCDCRead Whether this is a CDC (Change Data Capture) read\n * @param useMetadataRowIndexOpt Controls row index source for DV filtering. When provided,\n *                               must match optimizationsEnabled (true enables _metadata.row_index\n *                               and file splitting; false uses internal counter, no splitting).\n *                               When None, reads from session config.\n */\nabstract class DeltaParquetFileFormatBase(\n    protected val protocolMetadataAdapter: ProtocolMetadataAdapter,\n    protected val nullableRowTrackingConstantFields: Boolean = false,\n    protected val nullableRowTrackingGeneratedFields: Boolean = false,\n    protected val optimizationsEnabled: Boolean = true,\n    protected val tablePath: Option[String] = None,\n    protected val isCDCRead: Boolean = false,\n    protected val useMetadataRowIndexOpt: Option[Boolean] = None)\n  extends ParquetFileFormat with Logging {\n\n  // Validate either we have all arguments for DV enabled read or none of them.\n  if (hasTablePath) {\n    useMetadataRowIndexOpt.foreach { useMetadataRowIndex =>\n      require(useMetadataRowIndex == optimizationsEnabled,\n        \"Wrong arguments for Delta table scan with deletion vectors\")\n    }\n  }\n\n  SparkSession.getActiveSession.ifDefined { session =>\n    protocolMetadataAdapter.assertTableReadable(session)\n  }\n\n  require(!nullableRowTrackingConstantFields || nullableRowTrackingGeneratedFields)\n\n  val columnMappingMode: DeltaColumnMappingMode = protocolMetadataAdapter.columnMappingMode\n  val referenceSchema: StructType = protocolMetadataAdapter.getReferenceSchema\n\n  if (columnMappingMode == IdMapping) {\n    val requiredReadConf = SQLConf.PARQUET_FIELD_ID_READ_ENABLED\n    require(SparkSession.getActiveSession.exists(_.sessionState.conf.getConf(requiredReadConf)),\n      s\"${requiredReadConf.key} must be enabled to support Delta id column mapping mode\")\n    val requiredWriteConf = SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED\n    require(SparkSession.getActiveSession.exists(_.sessionState.conf.getConf(requiredWriteConf)),\n      s\"${requiredWriteConf.key} must be enabled to support Delta id column mapping mode\")\n  }\n\n  /**\n   * prepareSchemaForRead must only be used for parquet read.\n   * It removes \"PARQUET_FIELD_ID_METADATA_KEY\" for name mapping mode which address columns by\n   * physical name instead of id.\n   */\n  def prepareSchemaForRead(inputSchema: StructType): StructType = {\n    val schema = DeltaColumnMapping.createPhysicalSchema(\n      inputSchema, referenceSchema, columnMappingMode)\n    if (columnMappingMode == NameMapping) {\n      SchemaMergingUtils.transformColumns(schema) { (_, field, _) =>\n        field.copy(metadata = new MetadataBuilder()\n          .withMetadata(field.metadata)\n          .remove(DeltaColumnMapping.PARQUET_FIELD_ID_METADATA_KEY)\n          .remove(DeltaColumnMapping.PARQUET_FIELD_NESTED_IDS_METADATA_KEY)\n          .build())\n      }\n    } else schema\n  }\n\n  /**\n   * Prepares filters so that they can be pushed down into the Parquet reader.\n   *\n   * If column mapping is enabled, then logical column names in the filters will be replaced with\n   * their corresponding physical column names. This is necessary as the Parquet files will use\n   * physical column names, and the requested schema pushed down in the Parquet reader will also use\n   * physical column names.\n   */\n  private def prepareFiltersForRead(filters: Seq[Filter]): Seq[Filter] = {\n    if (!optimizationsEnabled) {\n      Seq.empty\n    } else if (columnMappingMode != NoMapping) {\n      import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper\n      val physicalNameMap = DeltaColumnMapping.getLogicalNameToPhysicalNameMap(referenceSchema)\n        .map { case (logicalName, physicalName) => (logicalName.quoted, physicalName.quoted) }\n      filters.flatMap(translateFilterForColumnMapping(_, physicalNameMap))\n    } else {\n      filters\n    }\n  }\n\n  override def isSplitable(\n    sparkSession: SparkSession,\n    options: Map[String, String],\n    path: Path): Boolean = optimizationsEnabled\n\n  def hasTablePath: Boolean = tablePath.isDefined\n\n  override def buildReaderWithPartitionValues(\n      sparkSession: SparkSession,\n      dataSchema: StructType,\n      partitionSchema: StructType,\n      requiredSchema: StructType,\n      filters: Seq[Filter],\n      options: Map[String, String],\n      hadoopConf: Configuration): PartitionedFile => Iterator[InternalRow] = {\n\n    // Use explicitly provided value if available, otherwise read from config\n    val useMetadataRowIndex = useMetadataRowIndexOpt.getOrElse(\n      sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX))\n\n    val parquetDataReader: PartitionedFile => Iterator[InternalRow] =\n      super.buildReaderWithPartitionValues(\n        sparkSession,\n        prepareSchemaForRead(dataSchema),\n        prepareSchemaForRead(partitionSchema),\n        prepareSchemaForRead(requiredSchema),\n        prepareFiltersForRead(filters),\n        options,\n        hadoopConf)\n\n    val schemaWithIndices = requiredSchema.fields.zipWithIndex\n    def findColumn(name: String): Option[ColumnMetadata] = {\n      val results = schemaWithIndices.filter(_._1.name == name)\n      if (results.length > 1) {\n        throw new IllegalArgumentException(\n          s\"There are more than one column with name=`$name` requested in the reader output\")\n      }\n      results.headOption.map(e => ColumnMetadata(e._2, e._1))\n    }\n\n    val isRowDeletedColumn = findColumn(IS_ROW_DELETED_COLUMN_NAME)\n    val rowIndexColumnName = if (useMetadataRowIndex) {\n      ParquetFileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME\n    } else {\n      ROW_INDEX_COLUMN_NAME\n    }\n    val rowIndexColumn = findColumn(rowIndexColumnName)\n\n    // We don't have any additional columns to generate, just return the original reader as is.\n    if (isRowDeletedColumn.isEmpty && rowIndexColumn.isEmpty) return parquetDataReader\n\n    // We are using the row_index col generated by the parquet reader and there are no more\n    // columns to generate.\n    if (useMetadataRowIndex && isRowDeletedColumn.isEmpty) return parquetDataReader\n\n    // Verify that either predicate pushdown with metadata column is enabled or optimizations\n    // are disabled.\n    require(useMetadataRowIndex || !optimizationsEnabled,\n      \"Cannot generate row index related metadata with file splitting or predicate pushdown\")\n\n    if (hasTablePath && isRowDeletedColumn.isEmpty) {\n        throw new IllegalArgumentException(\n          s\"Expected a column $IS_ROW_DELETED_COLUMN_NAME in the schema\")\n    }\n\n    val serializableHadoopConf = new SerializableConfiguration(hadoopConf)\n\n    val useOffHeapBuffers = sparkSession.sessionState.conf.offHeapColumnVectorEnabled\n    (partitionedFile: PartitionedFile) => {\n      val rowIteratorFromParquet = parquetDataReader(partitionedFile)\n      try {\n        val iterToReturn =\n          iteratorWithAdditionalMetadataColumns(\n            partitionedFile,\n            rowIteratorFromParquet,\n            isRowDeletedColumn,\n            rowIndexColumn,\n            useOffHeapBuffers,\n            serializableHadoopConf,\n            useMetadataRowIndex)\n        iterToReturn.asInstanceOf[Iterator[InternalRow]]\n      } catch {\n        case NonFatal(e) =>\n          // Close the iterator if it is a closeable resource. The `ParquetFileFormat` opens\n          // the file and returns `RecordReaderIterator` (which implements `AutoCloseable` and\n          // `Iterator`) instance as a `Iterator`.\n          rowIteratorFromParquet match {\n            case resource: AutoCloseable => DeltaParquetFileFormat.closeQuietly(resource)\n            case _ => // do nothing\n          }\n          throw e\n      }\n    }\n  }\n\n  override def supportFieldName(name: String): Boolean = {\n    if (columnMappingMode != NoMapping) true else super.supportFieldName(name)\n  }\n\n  override def metadataSchemaFields: Seq[StructField] = {\n    // TODO(SPARK-47731): Parquet reader in Spark has a bug where a file containing 2b+ rows\n    // in a single rowgroup causes it to run out of the `Integer` range.\n    // For Delta Parquet readers don't expose the row_index field as a metadata field when it is\n    // not strictly required. We do expose it when Row Tracking or DVs are enabled.\n    // In general, having 2b+ rows in a single rowgroup is not a common use case. When the issue is\n    // hit an exception is thrown.\n    if (protocolMetadataAdapter.isRowIdEnabled && !isCDCRead) {\n      // We should not expose row tracking fields for CDC reads.\n      val extraFields = protocolMetadataAdapter.createRowTrackingMetadataFields(\n        nullableRowTrackingConstantFields, nullableRowTrackingGeneratedFields)\n      super.metadataSchemaFields ++ extraFields\n    } else if (protocolMetadataAdapter.isDeletionVectorReadable) {\n      super.metadataSchemaFields\n    } else {\n      super.metadataSchemaFields.filter(_ != ParquetFileFormat.ROW_INDEX_FIELD)\n    }\n  }\n\n  override def prepareWrite(\n       sparkSession: SparkSession,\n       job: Job,\n       options: Map[String, String],\n       dataSchema: StructType): OutputWriterFactory = {\n    val factory = super.prepareWrite(sparkSession, job, options, dataSchema)\n    val conf = ContextUtil.getConfiguration(job)\n    // Always write timestamp as TIMESTAMP_MICROS for IcebergCompat based on Iceberg spec\n    if (protocolMetadataAdapter.isIcebergCompatAnyEnabled) {\n      conf.set(SQLConf.PARQUET_OUTPUT_TIMESTAMP_TYPE.key,\n        SQLConf.ParquetOutputTimestampType.TIMESTAMP_MICROS.toString)\n    }\n    if (protocolMetadataAdapter.isIcebergCompatGeqEnabled(2)) {\n      // Starting from IcebergCompatV2, we need to write nested field IDs for list and map\n      // types to the parquet schema. Spark currently does not support it so we hook in our\n      // own write support class.\n      ParquetOutputFormat.setWriteSupportClass(job, classOf[DeltaParquetWriteSupport])\n    }\n    factory\n  }\n\n  override def fileConstantMetadataExtractors: Map[String, PartitionedFile => Any] = {\n    val extractBaseRowId: PartitionedFile => Any = { file =>\n      file.otherConstantMetadataColumnValues.getOrElse(RowId.BASE_ROW_ID, {\n        if (nullableRowTrackingConstantFields) {\n          null\n        } else {\n          throw new IllegalStateException(\n            s\"Missing ${RowId.BASE_ROW_ID} value for file '${file.filePath}'\")\n        }\n      })\n    }\n    val extractDefaultRowCommitVersion: PartitionedFile => Any = { file =>\n      file.otherConstantMetadataColumnValues\n        .getOrElse(DefaultRowCommitVersion.METADATA_STRUCT_FIELD_NAME, {\n          if (nullableRowTrackingConstantFields) {\n            null\n          } else {\n            throw new IllegalStateException(\n              s\"Missing ${DefaultRowCommitVersion.METADATA_STRUCT_FIELD_NAME} value \" +\n                s\"for file '${file.filePath}'\")\n          }\n        })\n    }\n    super.fileConstantMetadataExtractors\n      .updated(RowId.BASE_ROW_ID, extractBaseRowId)\n      .updated(DefaultRowCommitVersion.METADATA_STRUCT_FIELD_NAME, extractDefaultRowCommitVersion)\n  }\n\n  /**\n   * Modifies the data read from underlying Parquet reader by populating one or both of the\n   * following metadata columns.\n   *   - [[IS_ROW_DELETED_COLUMN_NAME]] - row deleted status from deletion vector corresponding\n   *   to this file\n   *   - [[ROW_INDEX_COLUMN_NAME]] - index of the row within the file. Note, this column is only\n   *     populated when we are not using _metadata.row_index column.\n   */\n  private def iteratorWithAdditionalMetadataColumns(\n      partitionedFile: PartitionedFile,\n      iterator: Iterator[Object],\n      isRowDeletedColumnOpt: Option[ColumnMetadata],\n      rowIndexColumnOpt: Option[ColumnMetadata],\n      useOffHeapBuffers: Boolean,\n      serializableHadoopConf: SerializableConfiguration,\n      useMetadataRowIndex: Boolean): Iterator[Object] = {\n    require(!useMetadataRowIndex || rowIndexColumnOpt.isDefined,\n      \"useMetadataRowIndex is enabled but rowIndexColumn is not defined.\")\n\n    val rowIndexFilterOpt = isRowDeletedColumnOpt.map { col =>\n      // Fetch the DV descriptor from the broadcast map and create a row index filter\n      val dvDescriptorOpt = partitionedFile.otherConstantMetadataColumnValues\n        .get(FILE_ROW_INDEX_FILTER_ID_ENCODED)\n      val filterTypeOpt = partitionedFile.otherConstantMetadataColumnValues\n        .get(FILE_ROW_INDEX_FILTER_TYPE)\n      if (dvDescriptorOpt.isDefined && filterTypeOpt.isDefined) {\n        val rowIndexFilter = filterTypeOpt.get match {\n          case RowIndexFilterType.IF_CONTAINED => DropMarkedRowsFilter\n          case RowIndexFilterType.IF_NOT_CONTAINED => KeepMarkedRowsFilter\n          case unexpectedFilterType => throw new IllegalStateException(\n            s\"Unexpected row index filter type: ${unexpectedFilterType}\")\n        }\n        rowIndexFilter.createInstance(\n          DeletionVectorDescriptor.deserializeFromBase64(dvDescriptorOpt.get.asInstanceOf[String]),\n          serializableHadoopConf.value,\n          tablePath.map(new Path(_)))\n      } else if (dvDescriptorOpt.isDefined || filterTypeOpt.isDefined) {\n        throw new IllegalStateException(\n          s\"Both ${FILE_ROW_INDEX_FILTER_ID_ENCODED} and ${FILE_ROW_INDEX_FILTER_TYPE} \" +\n            \"should either both have values or no values at all.\")\n      } else {\n        KeepAllRowsFilter\n      }\n    }\n\n    // We only generate the row index column when predicate pushdown is not enabled.\n    val rowIndexColumnToWriteOpt = if (useMetadataRowIndex) None else rowIndexColumnOpt\n    val metadataColumnsToWrite =\n      Seq(isRowDeletedColumnOpt, rowIndexColumnToWriteOpt).filter(_.nonEmpty).map(_.get)\n\n    // When metadata.row_index is not used there is no way to verify the Parquet index is\n    // starting from 0. We disable the splits, so the assumption is ParquetFileFormat respects\n    // that.\n    var rowIndex: Long = 0\n\n    // Used only when non-column row batches are received from the Parquet reader\n    val tempVector = new OnHeapColumnVector(1, ByteType)\n\n    iterator.map { row =>\n      row match {\n        case batch: ColumnarBatch => // When vectorized Parquet reader is enabled.\n          val size = batch.numRows()\n          // Create vectors for all needed metadata columns.\n          // We can't use the one from Parquet reader as it set the\n          // [[WritableColumnVector.isAllNulls]] to true and it can't be reset with using any\n          // public APIs.\n          DeltaParquetFileFormat.trySafely(\n            useOffHeapBuffers, size, metadataColumnsToWrite) { writableVectors =>\n            val indexVectorTuples = new ArrayBuffer[(Int, ColumnVector)]\n\n            // When predicate pushdown is enabled we use _metadata.row_index. Therefore,\n            // we only need to construct the isRowDeleted column.\n            var index = 0\n            isRowDeletedColumnOpt.foreach { columnMetadata =>\n              val isRowDeletedVector = writableVectors(index)\n              if (useMetadataRowIndex) {\n                rowIndexFilterOpt.get.materializeIntoVectorWithRowIndex(\n                  size, batch.column(rowIndexColumnOpt.get.index), isRowDeletedVector)\n              } else {\n                rowIndexFilterOpt.get\n                  .materializeIntoVector(rowIndex, rowIndex + size, isRowDeletedVector)\n              }\n              indexVectorTuples += (columnMetadata.index -> isRowDeletedVector)\n              index += 1\n            }\n\n            rowIndexColumnToWriteOpt.foreach { columnMetadata =>\n              val rowIndexVector = writableVectors(index)\n              // populate the row index column value.\n              for (i <- 0 until size) {\n                rowIndexVector.putLong(i, rowIndex + i)\n              }\n\n              indexVectorTuples += (columnMetadata.index -> rowIndexVector)\n              index += 1\n            }\n\n            val newBatch = DeltaParquetFileFormat.replaceVectors(batch, indexVectorTuples.toSeq: _*)\n            rowIndex += size\n            newBatch\n          }\n\n        case columnarRow: ColumnarBatchRow =>\n          // When vectorized reader is enabled but returns immutable rows instead of\n          // columnar batches [[ColumnarBatchRow]]. So we have to copy the row as a\n          // mutable [[InternalRow]] and set the `row_index` and `is_row_deleted`\n          // column values. This is not efficient. It should affect only the wide\n          // tables. https://github.com/delta-io/delta/issues/2246\n          val newRow = columnarRow.copy();\n          isRowDeletedColumnOpt.foreach { columnMetadata =>\n            val rowIndexForFiltering = if (useMetadataRowIndex) {\n              columnarRow.getLong(rowIndexColumnOpt.get.index)\n            } else {\n              rowIndex\n            }\n            rowIndexFilterOpt.get.materializeSingleRowWithRowIndex(rowIndexForFiltering, tempVector)\n            newRow.setByte(columnMetadata.index, tempVector.getByte(0))\n          }\n\n          rowIndexColumnToWriteOpt\n            .foreach(columnMetadata => newRow.setLong(columnMetadata.index, rowIndex))\n          rowIndex += 1\n\n          newRow\n        case rest: InternalRow => // When vectorized Parquet reader is disabled\n          // Temporary vector variable used to get DV values from RowIndexFilter\n          // Currently the RowIndexFilter only supports writing into a columnar vector\n          // and doesn't have methods to get DV value for a specific row index.\n          // TODO: This is not efficient, but it is ok given the default reader is vectorized\n          isRowDeletedColumnOpt.foreach { columnMetadata =>\n            val rowIndexForFiltering = if (useMetadataRowIndex) {\n              rest.getLong(rowIndexColumnOpt.get.index)\n            } else {\n              rowIndex\n            }\n            rowIndexFilterOpt.get.materializeSingleRowWithRowIndex(rowIndexForFiltering, tempVector)\n            rest.setByte(columnMetadata.index, tempVector.getByte(0))\n          }\n\n          rowIndexColumnToWriteOpt\n            .foreach(columnMetadata => rest.setLong(columnMetadata.index, rowIndex))\n          rowIndex += 1\n          rest\n        case others =>\n          throw new RuntimeException(\n            s\"Parquet reader returned an unknown row type: ${others.getClass.getName}\")\n      }\n    }\n  }\n\n  /**\n   * Translates the filter to use physical column names instead of logical column names.\n   * This is needed when the column mapping mode is set to `NameMapping` or `IdMapping`\n   * to match the requested schema that's passed to the [[ParquetFileFormat]].\n   */\n  private def translateFilterForColumnMapping(\n      filter: Filter,\n      physicalNameMap: Map[String, String]): Option[Filter] = {\n    object PhysicalAttribute {\n      def unapply(attribute: String): Option[String] = {\n        physicalNameMap.get(attribute)\n      }\n    }\n\n    filter match {\n      case EqualTo(PhysicalAttribute(physicalAttribute), value) =>\n        Some(EqualTo(physicalAttribute, value))\n      case EqualNullSafe(PhysicalAttribute(physicalAttribute), value) =>\n        Some(EqualNullSafe(physicalAttribute, value))\n      case GreaterThan(PhysicalAttribute(physicalAttribute), value) =>\n        Some(GreaterThan(physicalAttribute, value))\n      case GreaterThanOrEqual(PhysicalAttribute(physicalAttribute), value) =>\n        Some(GreaterThanOrEqual(physicalAttribute, value))\n      case LessThan(PhysicalAttribute(physicalAttribute), value) =>\n        Some(LessThan(physicalAttribute, value))\n      case LessThanOrEqual(PhysicalAttribute(physicalAttribute), value) =>\n        Some(LessThanOrEqual(physicalAttribute, value))\n      case In(PhysicalAttribute(physicalAttribute), values) =>\n        Some(In(physicalAttribute, values))\n      case IsNull(PhysicalAttribute(physicalAttribute)) =>\n        Some(IsNull(physicalAttribute))\n      case IsNotNull(PhysicalAttribute(physicalAttribute)) =>\n        Some(IsNotNull(physicalAttribute))\n      case And(left, right) =>\n        val newLeft = translateFilterForColumnMapping(left, physicalNameMap)\n        val newRight = translateFilterForColumnMapping(right, physicalNameMap)\n        (newLeft, newRight) match {\n          case (Some(l), Some(r)) => Some(And(l, r))\n          case (Some(l), None) => Some(l)\n          case (_, _) => newRight\n        }\n      case Or(left, right) =>\n        val newLeft = translateFilterForColumnMapping(left, physicalNameMap)\n        val newRight = translateFilterForColumnMapping(right, physicalNameMap)\n        (newLeft, newRight) match {\n          case (Some(l), Some(r)) => Some(Or(l, r))\n          case (_, _) => None\n        }\n      case Not(child) =>\n        translateFilterForColumnMapping(child, physicalNameMap).map(Not)\n      case StringStartsWith(PhysicalAttribute(physicalAttribute), value) =>\n        Some(StringStartsWith(physicalAttribute, value))\n      case StringEndsWith(PhysicalAttribute(physicalAttribute), value) =>\n        Some(StringEndsWith(physicalAttribute, value))\n      case StringContains(PhysicalAttribute(physicalAttribute), value) =>\n        Some(StringContains(physicalAttribute, value))\n      case AlwaysTrue() => Some(AlwaysTrue())\n      case AlwaysFalse() => Some(AlwaysFalse())\n      case _ =>\n        logError(log\"Failed to translate filter ${MDC(DeltaLogKeys.FILTER, filter)}\")\n        None\n    }\n  }\n}\n\n/**\n * DeltaParquetFileFormat case class that uses Delta Spark's Protocol and Metadata.\n * Used by Delta spark v1 connector\n */\ncase class DeltaParquetFileFormat(\n    protocol: Protocol,\n    metadata: Metadata,\n    override val nullableRowTrackingConstantFields: Boolean = false,\n    override val nullableRowTrackingGeneratedFields: Boolean = false,\n    override val optimizationsEnabled: Boolean = true,\n    override val tablePath: Option[String] = None,\n    override val isCDCRead: Boolean = false)\n  extends DeltaParquetFileFormatBase(\n    protocolMetadataAdapter = ProtocolMetadataAdapterV1(protocol, metadata),\n    nullableRowTrackingConstantFields = nullableRowTrackingConstantFields,\n    nullableRowTrackingGeneratedFields = nullableRowTrackingGeneratedFields,\n    optimizationsEnabled = optimizationsEnabled,\n    tablePath = tablePath,\n    isCDCRead = isCDCRead,\n    // V1: capture config at construction, used in buildReaderWithPartitionValues\n    useMetadataRowIndexOpt = SparkSession.getActiveSession.map(\n      _.sessionState.conf.getConf(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX))) {\n\n  /**\n   * We sometimes need to replace FileFormat within LogicalPlans, so we have to override\n   * `equals` to ensure file format changes are captured\n   */\n  override def equals(other: Any): Boolean = {\n    other match {\n      case ff: DeltaParquetFileFormat =>\n        ff.columnMappingMode == columnMappingMode &&\n          ff.referenceSchema == referenceSchema &&\n          ff.nullableRowTrackingConstantFields == nullableRowTrackingConstantFields &&\n          ff.optimizationsEnabled == optimizationsEnabled\n      case _ => false\n    }\n  }\n\n  override def hashCode(): Int = getClass.getCanonicalName.hashCode()\n\n  def copyWithDVInfo(\n      tablePath: String,\n      optimizationsEnabled: Boolean): DeltaParquetFileFormat = {\n    // When predicate pushdown is enabled we allow both splits and predicate pushdown.\n    this.copy(\n      optimizationsEnabled = optimizationsEnabled,\n      tablePath = Some(tablePath))\n  }\n}\n\nobject DeltaParquetFileFormat {\n  /**\n   * Column name used to identify whether the row read from the parquet file is marked\n   * as deleted according to the Delta table deletion vectors\n   */\n  val IS_ROW_DELETED_COLUMN_NAME = \"__delta_internal_is_row_deleted\"\n  val IS_ROW_DELETED_STRUCT_FIELD = StructField(IS_ROW_DELETED_COLUMN_NAME, ByteType)\n\n  /** Row index for each column */\n  val ROW_INDEX_COLUMN_NAME = \"__delta_internal_row_index\"\n  val ROW_INDEX_STRUCT_FIELD = StructField(ROW_INDEX_COLUMN_NAME, LongType)\n\n  /** The key to the encoded row index filter identifier value of the\n   * [[PartitionedFile]]'s otherConstantMetadataColumnValues map. */\n  val FILE_ROW_INDEX_FILTER_ID_ENCODED = \"row_index_filter_id_encoded\"\n\n  /** The key to the row index filter type value of the\n   * [[PartitionedFile]]'s otherConstantMetadataColumnValues map. */\n  val FILE_ROW_INDEX_FILTER_TYPE = \"row_index_filter_type\"\n\n  /** Utility method to create a new writable vector */\n  private[delta] def newVector(\n      useOffHeapBuffers: Boolean, size: Int, dataType: StructField): WritableColumnVector = {\n    if (useOffHeapBuffers) {\n      OffHeapColumnVector.allocateColumns(size, Seq(dataType).toArray)(0)\n    } else {\n      OnHeapColumnVector.allocateColumns(size, Seq(dataType).toArray)(0)\n    }\n  }\n\n  /** Try the operation, if the operation fails release the created resource */\n  private[delta] def trySafely[R <: WritableColumnVector, T](\n      useOffHeapBuffers: Boolean,\n      size: Int,\n      columns: Seq[ColumnMetadata])(f: Seq[WritableColumnVector] => T): T = {\n    val resources = new ArrayBuffer[WritableColumnVector](columns.size)\n    try {\n      columns.foreach(col => resources.append(newVector(useOffHeapBuffers, size, col.structField)))\n      f(resources.toSeq)\n    } catch {\n      case NonFatal(e) =>\n        resources.foreach(closeQuietly(_))\n        throw e\n    }\n  }\n\n  /** Utility method to quietly close an [[AutoCloseable]] */\n  private[delta] def closeQuietly(closeable: AutoCloseable): Unit = {\n    if (closeable != null) {\n      try {\n        closeable.close()\n      } catch {\n        case NonFatal(_) => // ignore\n      }\n    }\n  }\n\n  /**\n   * Helper method to replace the vectors in given [[ColumnarBatch]].\n   * New vectors and its index in the batch are given as tuples.\n   */\n  private[delta] def replaceVectors(\n      batch: ColumnarBatch,\n      indexVectorTuples: (Int, ColumnVector) *): ColumnarBatch = {\n    val vectors = ArrayBuffer[ColumnVector]()\n    for (i <- 0 until batch.numCols()) {\n      var replaced: Boolean = false\n      for (indexVectorTuple <- indexVectorTuples) {\n        val index = indexVectorTuple._1\n        val vector = indexVectorTuple._2\n        if (indexVectorTuple._1 == i) {\n          vectors += indexVectorTuple._2\n          // Make sure to close the existing vector allocated in the Parquet\n          batch.column(i).close()\n          replaced = true\n        }\n      }\n      if (!replaced) {\n        vectors += batch.column(i)\n      }\n    }\n    new ColumnarBatch(vectors.toArray, batch.numRows())\n  }\n\n  /** Helper class to encapsulate column info */\n  case class ColumnMetadata(index: Int, structField: StructField)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaParquetWriteSupport.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.util.Try\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.parquet.hadoop.api.WriteSupport.WriteContext\nimport org.apache.parquet.schema.{LogicalTypeAnnotation, Type, Types}\nimport org.apache.parquet.schema.LogicalTypeAnnotation.{ListLogicalTypeAnnotation, MapLogicalTypeAnnotation}\n\nimport org.apache.spark.SparkRuntimeException\nimport org.apache.spark.sql.catalyst.parser.LegacyTypeStringParser\nimport org.apache.spark.sql.catalyst.trees.Origin\nimport org.apache.spark.sql.errors.QueryCompilationErrors\nimport org.apache.spark.sql.execution.datasources.parquet.{ParquetSchemaConverter, ParquetWriteSupport}\nimport org.apache.spark.sql.types.{DataType, StructField, StructType}\n\nclass DeltaParquetWriteSupport extends ParquetWriteSupport {\n\n  private def getNestedFieldId(field: StructField, path: Seq[String]): Int = {\n    field.metadata\n      .getMetadata(PARQUET_FIELD_NESTED_IDS_METADATA_KEY)\n      .getLong(path.mkString(\".\"))\n      .toInt\n  }\n\n  private def findFieldInSparkSchema(schema: StructType, path: Seq[String]): StructField = {\n    schema.findNestedField(path, true) match {\n      case Some((_, field)) => field\n      case None => throw QueryCompilationErrors.invalidFieldName(Seq(path.head), path, Origin())\n    }\n  }\n\n  override def init(configuration: Configuration): WriteContext = {\n    val writeContext = super.init(configuration)\n    // Parse the Spark schema. This is the same as is done in super.init, however, the\n    // parsed schema is stored in [[ParquetWriteSupport.schema]], which is private so\n    // we can't access it here and need to parse it again.\n    val schemaString = configuration.get(ParquetWriteSupport.SPARK_ROW_SCHEMA)\n    // This code is copied from Spark StructType.fromString because it is not accessible here\n    val parsedSchema = Try(DataType.fromJson(schemaString)).getOrElse(\n      LegacyTypeStringParser.parseString(schemaString)) match {\n        case t: StructType => t\n        case _ =>\n          // This code is copied from DataTypeErrors.failedParsingStructTypeError because\n          // it is not accessible here\n          throw new SparkRuntimeException(\n            errorClass = \"FAILED_PARSE_STRUCT_TYPE\",\n            messageParameters = Map(\"raw\" -> s\"'$schemaString'\"))\n    }\n\n    val messageType = writeContext.getSchema\n    val newMessageTypeBuilder = Types.buildMessage()\n    messageType.getFields.forEach { field =>\n      val parentField = findFieldInSparkSchema(parsedSchema, Seq(field.getName))\n      newMessageTypeBuilder.addField(convert(\n        field, parentField, parsedSchema, Seq(field.getName), Seq(field.getName)))\n    }\n    val newMessageType = newMessageTypeBuilder.named(\n      ParquetSchemaConverter.SPARK_PARQUET_SCHEMA_NAME)\n    new WriteContext(newMessageType, writeContext.getExtraMetaData)\n  }\n\n  /**\n   * Recursively rewrites the parquet [[Type]] by adding the nested field\n   * IDs to list and map subtypes as defined in the schema. The\n   * recursion needs to keep track of the absolute field path in order\n   * to correctly identify the StructField in the spark schema for a\n   * corresponding parquet field. As nested field IDs are referenced\n   * by their relative path in a field's metadata, the recursion also needs\n   * to keep track of the relative path.\n   *\n   * For example, consider the following column type\n   * col1 STRUCT(a INT, b STRUCT(c INT, d ARRAY(INT)))\n   *\n   * The absolute path to the nested [[element]] field of the list is\n   * col1.b.d.element whereas the relative path is d.element, i.e. relative\n   * to the parent struct field.\n   */\n  private def convert(\n      field: Type,\n      parentField: StructField,\n      sparkSchema: StructType,\n      absolutePath: Seq[String],\n      relativePath: Seq[String]): Type = {\n    field.getLogicalTypeAnnotation match {\n      case _: ListLogicalTypeAnnotation =>\n        val relElemFieldPath = relativePath :+ PARQUET_LIST_ELEMENT_FIELD_NAME\n        val id = getNestedFieldId(parentField, relElemFieldPath)\n        val elementField =\n          field.asGroupType().getFields.get(0).asGroupType().getFields.get(0).withId(id)\n        val builder = Types\n          .buildGroup(field.getRepetition).as(LogicalTypeAnnotation.listType())\n          .addField(\n            Types.repeatedGroup()\n              .addField(convert(elementField, parentField, sparkSchema,\n                absolutePath :+ PARQUET_LIST_ELEMENT_FIELD_NAME, relElemFieldPath))\n              .named(\"list\"))\n          if (field.getId != null) {\n            builder.id(field.getId.intValue())\n          }\n          builder.named(field.getName)\n      case _: MapLogicalTypeAnnotation =>\n        val relKeyFieldPath = relativePath :+ PARQUET_MAP_KEY_FIELD_NAME\n        val relValFieldPath = relativePath :+ PARQUET_MAP_VALUE_FIELD_NAME\n        val keyId = getNestedFieldId(parentField, relKeyFieldPath)\n        val valId = getNestedFieldId(parentField, relValFieldPath)\n        val keyField =\n          field.asGroupType().getFields.get(0).asGroupType().getFields.get(0).withId(keyId)\n        val valueField =\n          field.asGroupType().getFields.get(0).asGroupType().getFields.get(1).withId(valId)\n        val builder = Types\n          .buildGroup(field.getRepetition).as(LogicalTypeAnnotation.mapType())\n          .addField(\n            Types\n              .repeatedGroup()\n              .addField(convert(keyField, parentField, sparkSchema,\n                absolutePath :+ PARQUET_MAP_KEY_FIELD_NAME, relKeyFieldPath))\n              .addField(convert(valueField, parentField, sparkSchema,\n                absolutePath :+ PARQUET_MAP_VALUE_FIELD_NAME, relValFieldPath))\n              .named(\"key_value\"))\n        if (field.getId != null) {\n          builder.id(field.getId.intValue())\n        }\n        builder.named(field.getName)\n      case _ if field.isPrimitive => field\n      case _ =>\n        val builder = Types.buildGroup(field.getRepetition)\n        field.asGroupType().getFields.forEach { field =>\n          val absPath = absolutePath :+ field.getName\n          val parentField = findFieldInSparkSchema(sparkSchema, absPath)\n          builder.addField(convert(field, parentField, sparkSchema, absPath, Seq(field.getName)))\n        }\n        if (field.getId != null) {\n          builder.id(field.getId.intValue())\n        }\n        builder.named(field.getName)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaSharedExceptions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.JavaConverters._\n\nimport org.antlr.v4.runtime.ParserRuleContext\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.parser.{DeltaParseException, ParseException, ParseExceptionShims, ParserUtils}\nimport org.apache.spark.sql.catalyst.trees.Origin\nimport org.apache.spark.QueryContext\n\nclass DeltaAnalysisException(\n    errorClass: String,\n    messageParameters: Array[String],\n    cause: Option[Throwable] = None,\n    origin: Option[Origin] = None,\n    precomputedMessage: Option[String] = None,\n    precomputedMessageParametersMap: Option[java.util.Map[String, String]] = None)\n  extends AnalysisException(\n    message = precomputedMessage.getOrElse(\n      DeltaThrowableHelper.getMessage(errorClass, messageParameters)),\n    messageParameters = precomputedMessageParametersMap.getOrElse(DeltaThrowableHelper\n      .getMessageParameters(errorClass, errorSubClass = null, messageParameters)).asScala.toMap,\n    errorClass = Some(errorClass),\n    line = origin.flatMap(_.line),\n    startPosition = origin.flatMap(_.startPosition),\n    context = origin.map(_.getQueryContext).getOrElse(Array.empty),\n    cause = cause)\n  with DeltaThrowable {\n  def getMessageParametersArray: Array[String] = messageParameters\n  override def getErrorClass: String = errorClass\n  override def getMessageParameters: java.util.Map[String, String] =\n    precomputedMessageParametersMap.getOrElse(\n      DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n    )\n  override def withPosition(origin: Origin): AnalysisException =\n    new DeltaAnalysisException(errorClass, messageParameters, cause, Some(origin),\n      precomputedMessage, precomputedMessageParametersMap)\n}\n\nclass DeltaAnalysisExceptionWithSubErrors(\n    errorClass: String,\n    messageParameters: Array[String],\n    subErrors: Seq[(String, Array[String])])\n  extends DeltaAnalysisException(\n    errorClass = errorClass,\n    messageParameters = messageParameters,\n    cause = None,\n    origin = None,\n    precomputedMessage = Some(\n      DeltaThrowableHelper.getMessageWithSubErrors(errorClass, messageParameters, subErrors)),\n    precomputedMessageParametersMap = Some(\n      DeltaThrowableHelper.getMainErrorMessageParameters(errorClass, messageParameters))\n  )\n\nclass DeltaIllegalArgumentException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty,\n    cause: Throwable = null)\n  extends IllegalArgumentException(\n      DeltaThrowableHelper.getMessage(errorClass, messageParameters), cause)\n    with DeltaThrowable {\n    override def getErrorClass: String = errorClass\n  def getMessageParametersArray: Array[String] = messageParameters\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n\n  override def getQueryContext: Array[QueryContext] = new Array(0);\n}\n\nclass DeltaUnsupportedOperationException(\n    errorClass: String,\n    messageParameters: Array[String] = Array.empty)\n  extends UnsupportedOperationException(\n      DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n  def getMessageParametersArray: Array[String] = messageParameters\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n\n  override def getQueryContext: Array[QueryContext] = new Array(0);\n}\n\n// DeltaParseException is now defined in ParseExceptionShims\n// (see scala-spark-*/shims/ParseExceptionShims.scala) to handle the different ParseException\n// constructor signatures between Spark versions.\n// In Spark 4.1, ParseException removed the 'stop' parameter from its constructor.\n\nclass DeltaArithmeticException(\n    errorClass: String,\n    messageParameters: Array[String])\n  extends ArithmeticException(\n      DeltaThrowableHelper.getMessage(errorClass, messageParameters))\n    with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n\n  override def getMessageParameters: java.util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.util.{Failure, Success, Try}\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.files.{TahoeFileIndex, TahoeLogFileIndex}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.{DeltaLogging, LogThrottler}\nimport org.apache.spark.sql.delta.skipping.clustering.temp.{ClusterByTransform => TempClusterByTransform}\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.{Column, DataFrame, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{NoSuchTableException, UnresolvedLeafNode, UnresolvedTable}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, SessionCatalog}\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke\nimport org.apache.spark.sql.catalyst.planning.NodeWithOnlyDeterministicProjectAndFilter\nimport org.apache.spark.sql.catalyst.plans.logical.{Filter, LeafNode, LogicalPlan, Project}\nimport org.apache.spark.sql.catalyst.util.CharVarcharCodegenUtils\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.connector.expressions.{FieldReference, IdentityTransform}\nimport org.apache.spark.sql.execution.datasources.{FileFormat, FileIndex, HadoopFsRelation, LogicalRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Extractor Object for pulling out the file index of a logical relation.\n */\nobject RelationFileIndex {\n  def unapply(a: LogicalRelation): Option[FileIndex] = a match {\n    case LogicalRelationWithTable(hrel: HadoopFsRelation, _) => Some(hrel.location)\n    case _ => None\n  }\n}\n\n/**\n * Extractor Object for pulling out the table scan of a Delta table. It could be a full scan\n * or a partial scan.\n */\nobject DeltaTable {\n  def unapply(a: LogicalRelation): Option[TahoeFileIndex] = a match {\n    case RelationFileIndex(fileIndex: TahoeFileIndex) => Some(fileIndex)\n    case _ => None\n  }\n}\n\n/**\n * Extractor Object for pulling out the full table scan of a Delta table.\n */\nobject DeltaFullTable {\n  def unapply(a: LogicalPlan): Option[(LogicalRelation, TahoeLogFileIndex)] = a match {\n    // `DeltaFullTable` is not only used to match a certain query pattern, but also does\n    // some validations to throw errors. We need to match both Project and Filter here,\n    // so that we can check if Filter is present or not during validations.\n    case NodeWithOnlyDeterministicProjectAndFilter(lr @ DeltaTable(index: TahoeLogFileIndex)) =>\n      if (!index.deltaLog.tableExists) return None\n      val hasFilter = a.find(_.isInstanceOf[Filter]).isDefined\n      if (index.partitionFilters.isEmpty && index.versionToUse.isEmpty && !hasFilter) {\n        Some(lr -> index)\n      } else if (index.versionToUse.nonEmpty) {\n        throw DeltaErrors.failedScanWithHistoricalVersion(index.versionToUse.get)\n      } else {\n        throw DeltaErrors.unexpectedPartialScan(index.path)\n      }\n    // Convert V2 relations to V1 and perform the check\n    case DeltaRelation(lr) => unapply(lr)\n    case _ =>\n      None\n  }\n}\n\nobject DeltaTableUtils extends PredicateHelper\n  with DeltaLogging {\n\n  // The valid hadoop prefixes passed through `DeltaTable.forPath` or DataFrame APIs.\n  val validDeltaTableHadoopPrefixes: List[String] = List(\"fs.\", \"dfs.\")\n\n  /** Check whether this table is a Delta table based on information from the Catalog. */\n  def isDeltaTable(table: CatalogTable): Boolean = DeltaSourceUtils.isDeltaTable(table.provider)\n\n\n  /**\n   * Check whether the provided table name is a Delta table based on information from the Catalog.\n   */\n  def isDeltaTable(spark: SparkSession, tableName: TableIdentifier): Boolean = {\n    val catalog = spark.sessionState.catalog\n    val tableIsNotTemporaryTable = !catalog.isTempView(tableName)\n    val tableExists = {\n        (tableName.database.isEmpty || catalog.databaseExists(tableName.database.get)) &&\n        catalog.tableExists(tableName)\n    }\n    tableIsNotTemporaryTable && tableExists && isDeltaTable(catalog.getTableMetadata(tableName))\n  }\n\n  /** Check if the provided path is the root or the children of a Delta table. */\n  def isDeltaTable(\n      spark: SparkSession,\n      path: Path,\n      options: Map[String, String] = Map.empty): Boolean = {\n    findDeltaTableRoot(spark, path, options).isDefined\n  }\n\n  /**\n   * Checks whether TableIdentifier is a path or a table name\n   * We assume it is a path unless the table and database both exist in the catalog\n   * @param catalog session catalog used to check whether db/table exist\n   * @param tableIdent the provided table or path\n   * @return true if using table name, false if using path, error otherwise\n   */\n  def isCatalogTable(catalog: SessionCatalog, tableIdent: TableIdentifier): Boolean = {\n    val (dbExists, assumePath) = dbExistsAndAssumePath(catalog, tableIdent)\n\n    // If we don't need to check that the table exists, return false since we think the tableIdent\n    // refers to a path at this point, because the database doesn't exist\n    if (assumePath) return false\n\n    // check for dbexists otherwise catalog.tableExists may throw NoSuchDatabaseException\n    if ((dbExists || tableIdent.database.isEmpty)\n        && Try(catalog.tableExists(tableIdent)).getOrElse(false)) {\n      true\n    } else if (isValidPath(tableIdent)) {\n      false\n    } else {\n      throw new NoSuchTableException(tableIdent.database.getOrElse(\"\"), tableIdent.table)\n    }\n  }\n\n  /**\n   * It's possible that checking whether database exists can throw an exception. In that case,\n   * we want to surface the exception only if the provided tableIdentifier cannot be a path.\n   *\n   * @param catalog session catalog used to check whether db/table exist\n   * @param ident the provided table or path\n   * @return tuple where first indicates whether database exists and second indicates whether there\n   *         is a need to check whether table exists\n   */\n  private def dbExistsAndAssumePath(\n      catalog: SessionCatalog,\n      ident: TableIdentifier): (Boolean, Boolean) = {\n    def databaseExists = {\n          ident.database.forall(catalog.databaseExists)\n    }\n\n    Try(databaseExists) match {\n      // DB exists, check table exists only if path is not valid\n      case Success(true) => (true, false)\n      // DB does not exist, check table exists only if path does not exist\n      case Success(false) => (false, new Path(ident.table).isAbsolute)\n      // Checking DB exists threw exception, if the path is still valid then check for table exists\n      case Failure(_) if isValidPath(ident) => (false, true)\n      // Checking DB exists threw exception, path is not valid so throw the initial exception\n      case Failure(e) => throw e\n    }\n  }\n\n  /**\n   * @param tableIdent the provided table or path\n   * @return whether or not the provided TableIdentifier can specify a path for parquet or delta\n   */\n  def isValidPath(tableIdent: TableIdentifier): Boolean = {\n    // If db doesnt exist or db is called delta/tahoe then check if path exists\n    DeltaSourceUtils.isDeltaDataSourceName(tableIdent.database.getOrElse(\"\")) &&\n      new Path(tableIdent.table).isAbsolute\n  }\n\n  /** Find the root of a Delta table from the provided path. */\n  def findDeltaTableRoot(\n      spark: SparkSession,\n      path: Path,\n      options: Map[String, String] = Map.empty): Option[Path] = {\n    // scalastyle:off deltahadoopconfiguration\n    val fs = path.getFileSystem(spark.sessionState.newHadoopConfWithOptions(options))\n    // scalastyle:on deltahadoopconfiguration\n\n\n    findDeltaTableRoot(fs, path)\n  }\n\n  /** Finds the root of a Delta table given a path if it exists. */\n  def findDeltaTableRoot(fs: FileSystem, path: Path): Option[Path] = {\n    findDeltaTableRoot(\n      fs,\n      path,\n      throwOnError = SparkSession.active.conf.get(DeltaSQLConf.DELTA_IS_DELTA_TABLE_THROW_ON_ERROR))\n  }\n\n  /** Finds the root of a Delta table given a path if it exists. */\n  private[delta] def findDeltaTableRoot(\n      fs: FileSystem,\n      path: Path,\n      throwOnError: Boolean): Option[Path] = {\n    if (throwOnError) {\n      findDeltaTableRootThrowOnError(fs, path)\n    } else {\n      findDeltaTableRootNoExceptions(fs, path)\n    }\n  }\n\n  /**\n   * Finds the root of a Delta table given a path if it exists.\n   *\n   * Does not throw any exceptions, but returns `None` when uncertain (old behaviour).\n   */\n  private def findDeltaTableRootNoExceptions(fs: FileSystem, path: Path): Option[Path] = {\n    var currentPath = path\n    while (currentPath != null && currentPath.getName != \"_delta_log\" &&\n        currentPath.getName != \"_samples\") {\n      val deltaLogPath = safeConcatPaths(currentPath, \"_delta_log\")\n      if (Try(fs.exists(deltaLogPath)).getOrElse(false)) {\n        return Option(currentPath)\n      }\n      currentPath = currentPath.getParent\n    }\n    None\n  }\n\n  /**\n   * Finds the root of a Delta table given a path if it exists.\n   *\n   * If there are errors and no root could be found, throw the first error (new behaviour)\n   */\n  private def findDeltaTableRootThrowOnError(fs: FileSystem, path: Path): Option[Path] = {\n    var firstError: Option[Throwable] = None\n    // Return `None` if `firstError` is empty, throw `firstError` otherwise.\n    def noneOrError(): Option[Path] = {\n      firstError match {\n        case Some(ex) =>\n          throw ex\n        case None =>\n          None\n      }\n    }\n    var currentPath = path\n    while (currentPath != null && currentPath.getName != \"_delta_log\" &&\n        currentPath.getName != \"_samples\") {\n      val deltaLogPath = safeConcatPaths(currentPath, \"_delta_log\")\n      try {\n        if (fs.exists(deltaLogPath)) {\n          return Option(currentPath)\n        }\n      } catch {\n        case NonFatal(ex) if currentPath == path =>\n          // Store errors for the first path, but keep going up the hierarchy,\n          // in case the error at this level does not matter and the delta log is found at a parent.\n          firstError = Some(ex)\n        case NonFatal(ex) =>\n          // If we find errors higher up the path we either treat it as a non-Delta table or\n          // return the error we found at the original path, if any.\n          // This gives us best-effort detection of delta logs in the hierarchy, but with more\n          // useful error messages when access was actually missing.\n          logThrottler.throttledWithSkippedLogMessage { skippedStr =>\n            logWarning(log\"Access error while exploring path hierarchy for a delta log.\"\n                + log\"original path=${MDC(DeltaLogKeys.PATH, path)}, \"\n                + log\"path with error=${MDC(DeltaLogKeys.PATH2, currentPath)}.\"\n                + skippedStr,\n              ex)\n          }\n          return noneOrError()\n      }\n      currentPath = currentPath.getParent\n    }\n    noneOrError()\n  }\n\n  private val logThrottler = new LogThrottler()\n\n  /** Whether a path should be hidden for delta-related file operations, such as Vacuum and Fsck. */\n  def isHiddenDirectory(\n      partitionColumnNames: Seq[String],\n      pathName: String,\n      shouldIcebergMetadataDirBeHidden: Boolean = true): Boolean = {\n    // Names of the form partitionCol=[value] are partition directories, and should be\n    // GCed even if they'd normally be hidden. The _db_index directory contains (bloom filter)\n    // indexes and these must be GCed when the data they are tied to is GCed.\n    // metadata name is reserved for converted iceberg metadata with delta universal format\n    (shouldIcebergMetadataDirBeHidden && pathName.equals(\"metadata\")) ||\n    (pathName.startsWith(\".\") || pathName.startsWith(\"_\")) &&\n      !pathName.startsWith(\"_delta_index\") && !pathName.startsWith(\"_change_data\") &&\n      !partitionColumnNames.exists(c => pathName.startsWith(c ++ \"=\"))\n  }\n\n  /**\n   * Does the predicate only contains partition columns?\n   */\n  def isPredicatePartitionColumnsOnly(\n      condition: Expression,\n      partitionColumns: Seq[String],\n      spark: SparkSession): Boolean = {\n    val nameEquality = spark.sessionState.analyzer.resolver\n    condition.references.forall { r =>\n      partitionColumns.exists(nameEquality(r.name, _))\n    }\n  }\n\n  /**\n   * Partition the given condition into two sequence of conjunctive predicates:\n   * - predicates that can be evaluated using metadata only.\n   * - other predicates.\n   */\n  def splitMetadataAndDataPredicates(\n      condition: Expression,\n      partitionColumns: Seq[String],\n      spark: SparkSession): (Seq[Expression], Seq[Expression]) = {\n    val (metadataPredicates, dataPredicates) =\n      splitConjunctivePredicates(condition).partition(\n        isPredicateMetadataOnly(_, partitionColumns, spark))\n    // Extra metadata predicates that can partially extracted from `dataPredicates`.\n    val extraMetadataPredicates =\n      if (dataPredicates.nonEmpty) {\n        extractMetadataPredicates(dataPredicates.reduce(And), partitionColumns, spark)\n          .map(splitConjunctivePredicates)\n          .getOrElse(Seq.empty)\n      } else {\n        Seq.empty\n      }\n    (metadataPredicates ++ extraMetadataPredicates, dataPredicates)\n  }\n\n  /**\n   * Returns a predicate that its reference is a subset of `partitionColumns` and it contains the\n   * maximum constraints from `condition`.\n   * When there is no such filter, `None` is returned.\n   */\n  private def extractMetadataPredicates(\n      condition: Expression,\n      partitionColumns: Seq[String],\n      spark: SparkSession): Option[Expression] = {\n    condition match {\n      case And(left, right) =>\n        val lhs = extractMetadataPredicates(left, partitionColumns, spark)\n        val rhs = extractMetadataPredicates(right, partitionColumns, spark)\n        (lhs.toSeq ++ rhs.toSeq).reduceOption(And)\n\n    // The Or predicate is convertible when both of its children can be pushed down.\n    // That is to say, if one/both of the children can be partially pushed down, the Or\n    // predicate can be partially pushed down as well.\n    //\n    // Here is an example used to explain the reason.\n    // Let's say we have\n    // condition: (a1 AND a2) OR (b1 AND b2),\n    // outputSet: AttributeSet(a1, b1)\n    // a1 and b1 is convertible, while a2 and b2 is not.\n    // The predicate can be converted as\n    // (a1 OR b1) AND (a1 OR b2) AND (a2 OR b1) AND (a2 OR b2)\n    // As per the logical in And predicate, we can push down (a1 OR b1).\n    case Or(left, right) =>\n      for {\n        lhs <- extractMetadataPredicates(left, partitionColumns, spark)\n        rhs <- extractMetadataPredicates(right, partitionColumns, spark)\n      } yield Or(lhs, rhs)\n\n    // Here we assume all the `Not` operators is already below all the `And` and `Or` operators\n    // after the optimization rule `BooleanSimplification`, so that we don't need to handle the\n    // `Not` operators here.\n    case other =>\n      if (isPredicatePartitionColumnsOnly(other, partitionColumns, spark)) {\n        Some(other)\n      } else {\n        None\n      }\n    }\n  }\n\n  /**\n   * Check if condition involves a subquery expression.\n   */\n  def containsSubquery(condition: Expression): Boolean = {\n    SubqueryExpression.hasSubquery(condition)\n  }\n\n  /**\n   * Check if condition can be evaluated using only metadata. In Delta, this means the condition\n   * only references partition columns and involves no subquery.\n   */\n  def isPredicateMetadataOnly(\n      condition: Expression,\n      partitionColumns: Seq[String],\n      spark: SparkSession): Boolean = {\n    isPredicatePartitionColumnsOnly(condition, partitionColumns, spark) &&\n      !containsSubquery(condition)\n  }\n\n  /**\n   * Replace the file index in a logical plan and return the updated plan.\n   * It's a common pattern that, in Delta commands, we use data skipping to determine a subset of\n   * files that can be affected by the command, so we replace the whole-table file index in the\n   * original logical plan with a new index of potentially affected files, while everything else in\n   * the original plan, e.g., resolved references, remain unchanged.\n   *\n   * @param target the logical plan in which we replace the file index\n   * @param fileIndex the new file index\n   */\n  def replaceFileIndex(\n      target: LogicalPlan,\n      fileIndex: FileIndex): LogicalPlan = {\n    target transform {\n      case l @ LogicalRelationWithTable(hfsr: HadoopFsRelation, _) =>\n        l.copy(relation = hfsr.copy(location = fileIndex)(hfsr.sparkSession))\n    }\n  }\n\n  /**\n   * Transform the file format in a logical plan and return the updated plan.\n   *\n   * @param target the logical plan in which the file format is replaced.\n   * @param rule   the rule to apply to the file format.\n   */\n  def transformFileFormat(\n      target: LogicalPlan)(\n      rule: PartialFunction[DeltaParquetFileFormat, DeltaParquetFileFormat]): LogicalPlan = {\n    target.transform {\n      case l@LogicalRelationWithTable(hfsr: HadoopFsRelation, _) =>\n        val newFileFormat = hfsr.fileFormat match {\n          case format: DeltaParquetFileFormat =>\n            rule.applyOrElse(format, identity[DeltaParquetFileFormat])\n        }\n        l.copy(relation = hfsr.copy(fileFormat = newFileFormat)(hfsr.sparkSession))\n    }\n  }\n\n  /**\n   * Many Delta meta-queries involve nondeterminstic functions, which interfere with automatic\n   * column pruning, so columns can be manually pruned from the scan. Note that partition columns\n   * can never be dropped even if they're not referenced in the rest of the query.\n   *\n   * @param spark the spark session to use\n   * @param target the logical plan in which drop columns\n   * @param columnsToDrop columns to drop from the scan\n   */\n  def dropColumns(\n      spark: SparkSession,\n      target: LogicalPlan,\n      columnsToDrop: Seq[String]): LogicalPlan = {\n    val resolver = spark.sessionState.analyzer.resolver\n\n    // Spark does char type read-side padding via an additional Project over the scan node.\n    // When char type read-side padding is applied, we need to apply column pruning for the\n    // Project as well, otherwise the Project will contain missing attributes.\n    val hasChar = target.exists {\n      case Project(projectList, _) =>\n        def hasCharPadding(e: Expression): Boolean = e.exists {\n          case s: StaticInvoke => s.staticObject == classOf[CharVarcharCodegenUtils] &&\n            s.functionName == \"readSidePadding\"\n          case _ => false\n        }\n        projectList.exists {\n          case a: Alias => hasCharPadding(a.child) && a.references.size == 1\n          case _ => false\n        }\n      case _ => false\n    }\n\n    target transformUp {\n      case l@LogicalRelationWithTable(hfsr: HadoopFsRelation, _) =>\n        // Prune columns from the scan.\n        val prunedOutput = l.output.filterNot { col =>\n          columnsToDrop.exists(resolver(_, col.name))\n        }\n\n        val prunedSchema = StructType(prunedOutput.map(attr =>\n          StructField(attr.name, attr.dataType, attr.nullable, attr.metadata)))\n        val newBaseRelation = hfsr.copy(dataSchema = prunedSchema)(hfsr.sparkSession)\n        l.copy(relation = newBaseRelation, output = prunedOutput)\n\n      case p @ Project(projectList, child) if hasChar =>\n        val newProjectList = projectList.filter { e =>\n          e.references.subsetOf(child.outputSet)\n        }\n        p.copy(projectList = newProjectList)\n    }\n  }\n\n  /** Finds and returns the file source metadata column from a dataframe */\n  def getFileMetadataColumn(df: DataFrame): Column =\n    df.metadataColumn(FileFormat.METADATA_NAME)\n\n  /**\n   * Update FileFormat for a plan and return the updated plan\n   *\n   * @param target Target plan to update\n   * @param updatedFileFormat Updated file format\n   * @return Updated logical plan\n   */\n  def replaceFileFormat(\n      target: LogicalPlan,\n      updatedFileFormat: FileFormat): LogicalPlan = {\n    target transform {\n      case l @ LogicalRelationWithTable(hfsr: HadoopFsRelation, _) =>\n        l.copy(\n          relation = hfsr.copy(fileFormat = updatedFileFormat)(hfsr.sparkSession))\n    }\n  }\n\n  /**\n   * Check if the given path contains time travel syntax with the `@`. If the path genuinely exists,\n   * return `None`. If the path doesn't exist, but is specifying time travel, return the\n   * `DeltaTimeTravelSpec` as well as the real path.\n   */\n  def extractIfPathContainsTimeTravel(\n      session: SparkSession,\n      path: String,\n      options: Map[String, String]): (String, Option[DeltaTimeTravelSpec]) = {\n    val conf = session.sessionState.conf\n    if (!DeltaTimeTravelSpec.isApplicable(conf, path)) return path -> None\n\n    val maybePath = new Path(path)\n\n    // scalastyle:off deltahadoopconfiguration\n    val fs = maybePath.getFileSystem(session.sessionState.newHadoopConfWithOptions(options))\n    // scalastyle:on deltahadoopconfiguration\n\n    // If the folder really exists, quit\n    if (fs.exists(maybePath)) return path -> None\n\n    val (tt, realPath) = DeltaTimeTravelSpec.resolvePath(conf, path)\n    realPath -> Some(tt)\n  }\n\n  /**\n   * Given a time travel node, resolve which version it is corresponding to for the given table and\n   * return the resolved version as well as the access type, i.e. by `version` or `timestamp`.\n   */\n  def resolveTimeTravelVersion(\n      conf: SQLConf,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      tt: DeltaTimeTravelSpec,\n      canReturnLastCommit: Boolean = false): (Long, String) = {\n    if (tt.version.isDefined) {\n      val userVersion = tt.version.get\n      deltaLog.history.checkVersionExists(userVersion, catalogTableOpt)\n      userVersion -> \"version\"\n    } else {\n      val timestamp = tt.getTimestamp(conf)\n      val commit =\n        deltaLog.history.getActiveCommitAtTime(timestamp, catalogTableOpt, canReturnLastCommit)\n      commit.version -> \"timestamp\"\n    }\n  }\n\n  def parseColToTransform(col: String): IdentityTransform = {\n    IdentityTransform(FieldReference(Seq(col)))\n  }\n\n  def parseColsToClusterByTransform(cols: Seq[String]): TempClusterByTransform = {\n    TempClusterByTransform(cols.map(FieldReference(_)))\n  }\n\n  // Workaround for withActive not being visible in io/delta.\n  def withActiveSession[T](spark: SparkSession)(body: => T): T = spark.withActive(body)\n\n  /**\n   * Uses org.apache.hadoop.fs.Path.mergePaths to concatenate a base path and a relative child path.\n   *\n   * This method is designed to address two specific issues in Hadoop Path:\n   *\n   * Issue 1:\n   * When the base path represents a Uri with an empty path component, such as concatenating\n   * \"s3://my-bucket\" and \"childPath\". In this case, the child path is converted to an absolute\n   * path at the root, i.e. /childPath. This prevents a \"URISyntaxException: Relative path in\n   * absolute URI\", which would be thrown by org.apache.hadoop.fs.Path(Path, String) because it\n   * tries to convert the base path to a Uri and then resolve the child on top of it. This is\n   * invalid for an empty base path and a relative child path according to the Uri specification,\n   * which states that if an authority is defined, the path component needs to be either empty or\n   * start with a '/'.\n   *\n   * Issue 2 (only when [[DeltaSQLConf.DELTA_WORK_AROUND_COLONS_IN_HADOOP_PATHS]] is `true`):\n   * When the child path contains a special character ':', such as \"aaaa:bbbb.csv\".\n   * This is valid in many file systems such as S3, but is actually ambiguous because it can be\n   * parsed either as an absolute path with a scheme (\"aaaa\") and authority (\"bbbb.csv\"), or as\n   * a relative path with a colon in the name (\"aaaa:bbbb.csv\"). Hadoop Path will always interpret\n   * it as the former, which is not what we want in this case. Therefore, we prepend a '/' to the\n   * child path to ensure that it is always interpreted as a relative path.\n   * See [[https://issues.apache.org/jira/browse/HDFS-14762]] for more details.\n   */\n  def safeConcatPaths(basePath: Path, relativeChildPath: String): Path = {\n    val useWorkaround = SparkSession.getActiveSession.map(_.sessionState.conf)\n      .exists(_.getConf(DeltaSQLConf.DELTA_WORK_AROUND_COLONS_IN_HADOOP_PATHS))\n    if (useWorkaround) {\n      Path.mergePaths(basePath, new Path(s\"/$relativeChildPath\"))\n    } else {\n      if (basePath.toUri.getPath.isEmpty) {\n        new Path(basePath, s\"/$relativeChildPath\")\n      } else {\n        new Path(basePath, relativeChildPath)\n      }\n    }\n  }\n\n  /**\n   * A list of Spark internal metadata keys that we may save in a Delta table schema\n   * unintentionally due to SPARK-43123. We need to remove them before handing over the schema to\n   * Spark to avoid Spark interpreting table columns incorrectly.\n   *\n   * Hard-coded strings are used intentionally as we want to capture possible keys used before\n   * SPARK-43123 regardless Spark versions. For example, if Spark changes any key string in future\n   * after SPARK-43123, the new string won't be leaked, but we still want to clean up the old key.\n   */\n  val SPARK_INTERNAL_METADATA_KEYS = Seq(\n    \"__autoGeneratedAlias\",\n    \"__metadata_col\",\n    \"__supports_qualified_star\", // A key used by an old version. Doesn't exist in latest code\n    \"__qualified_access_only\",\n    \"__file_source_metadata_col\",\n    \"__file_source_constant_metadata_col\",\n    \"__file_source_generated_metadata_col\"\n  )\n\n  /**\n   * Remove leaked metadata keys from the persisted table schema. Old versions might leak metadata\n   * intentionally. This method removes all possible metadata keys to avoid Spark interpreting\n   * table columns incorrectly.\n   */\n  def removeSparkInternalMetadata(spark: SparkSession, schema: StructType): StructType = {\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SCHEMA_REMOVE_SPARK_INTERNAL_METADATA)) {\n      var updated = false\n      val updatedSchema = schema.map { field =>\n        if (SPARK_INTERNAL_METADATA_KEYS.exists(field.metadata.contains)) {\n          updated = true\n          val newMetadata = new MetadataBuilder().withMetadata(field.metadata)\n          SPARK_INTERNAL_METADATA_KEYS.foreach(newMetadata.remove)\n          field.copy(metadata = newMetadata.build())\n        } else {\n          field\n        }\n      }\n      if (updated) {\n        StructType(updatedSchema)\n      } else {\n        schema\n      }\n    } else {\n      schema\n    }\n  }\n\n  /**\n   * Removes from the given schema all the metadata keys that are not used when reading a Delta\n   * table. This includes typically all metadata used by writer-only table features.\n   * Note that this also removes all leaked Spark internal metadata.\n   */\n  def removeInternalWriterMetadata(spark: SparkSession, schema: StructType): StructType = {\n    ColumnWithDefaultExprUtils.removeDefaultExpressions(\n      removeSparkInternalMetadata(spark, schema)\n    )\n  }\n\n  /**\n   * Removes internal Delta metadata from the given schema. This includes tyically metadata used by\n   * reader-writer table features that shouldn't leak outside of the table. Use\n   * [[removeInternalWriterMetadata]] in addition / instead to remove metadata for writer-only table\n   * features.\n   */\n  def removeInternalDeltaMetadata(spark: SparkSession, schema: StructType): StructType = {\n    val cleanedSchema = DeltaColumnMapping.dropColumnMappingMetadata(schema)\n\n    val conf = spark.sessionState.conf\n    if (conf.getConf(DeltaSQLConf.DELTA_TYPE_WIDENING_REMOVE_SCHEMA_METADATA)) {\n      TypeWideningMetadata.removeTypeWideningMetadata(cleanedSchema)._1\n    } else {\n      cleanedSchema\n    }\n  }\n\n}\n\nsealed abstract class UnresolvedPathBasedDeltaTableBase(path: String) extends UnresolvedLeafNode {\n  def identifier: Identifier = Identifier.of(Array(DeltaSourceUtils.ALT_NAME), path)\n  def deltaTableIdentifier: DeltaTableIdentifier = DeltaTableIdentifier(Some(path), None)\n\n}\n\n/** Resolves to a [[ResolvedTable]] if the DeltaTable exists */\ncase class UnresolvedPathBasedDeltaTable(\n    path: String,\n    options: Map[String, String],\n    commandName: String) extends UnresolvedPathBasedDeltaTableBase(path)\n\n/** Resolves to a [[DataSourceV2Relation]] if the DeltaTable exists */\ncase class UnresolvedPathBasedDeltaTableRelation(\n    path: String,\n    options: CaseInsensitiveStringMap) extends UnresolvedPathBasedDeltaTableBase(path)\n\n/**\n * This operator represents path-based tables in general including both Delta or non-Delta tables.\n * It resolves to a [[ResolvedTable]] if the path is for delta table,\n * [[ResolvedPathBasedNonDeltaTable]] if the path is for a non-Delta table.\n */\ncase class UnresolvedPathBasedTable(\n    path: String,\n    options: Map[String, String],\n    commandName: String) extends LeafNode {\n  override lazy val resolved: Boolean = false\n  override val output: Seq[Attribute] = Nil\n}\n\n/**\n * This operator is a placeholder that identifies a non-Delta path-based table. Given the fact\n * that some Delta commands (e.g. DescribeDeltaDetail) support non-Delta table, we introduced\n * ResolvedPathBasedNonDeltaTable as the resolved placeholder after analysis on a non delta path\n * from UnresolvedPathBasedTable.\n */\ncase class ResolvedPathBasedNonDeltaTable(\n    path: String,\n    options: Map[String, String],\n    commandName: String) extends LeafNode {\n  override val output: Seq[Attribute] = Nil\n}\n\n/**\n * A helper object with an apply method to transform a path or table identifier to a LogicalPlan.\n * If the path is set, it will be resolved to an [[UnresolvedPathBasedDeltaTable]] whereas if the\n * tableIdentifier is set, the LogicalPlan will be an [[UnresolvedTable]]. If neither of the two\n * options or both of them are set, [[apply]] will throw an exception.\n */\nobject UnresolvedDeltaPathOrIdentifier {\n  def apply(\n      path: Option[String],\n      tableIdentifier: Option[TableIdentifier],\n      options: Map[String, String],\n      cmd: String): LogicalPlan = {\n    (path, tableIdentifier) match {\n      case (Some(p), None) => UnresolvedPathBasedDeltaTable(p, options, cmd)\n      case (None, Some(t)) => UnresolvedTable(t.nameParts, cmd)\n      case _ => throw new IllegalArgumentException(\n        s\"Exactly one of path or tableIdentifier must be provided to $cmd\")\n    }\n  }\n\n  def apply(\n      path: Option[String],\n      tableIdentifier: Option[TableIdentifier],\n      cmd: String): LogicalPlan =\n    this(path, tableIdentifier, Map.empty, cmd)\n}\n\n/**\n * A helper object with an apply method to transform a path or table identifier to a LogicalPlan.\n * This is required by Delta commands that can also run against non-Delta tables, e.g. DESC DETAIL,\n * VACUUM command. If the tableIdentifier is set, the LogicalPlan will be an [[UnresolvedTable]].\n * If the tableIdentifier is not set but the path is set, it will be resolved to an\n * [[UnresolvedPathBasedTable]] since we can not tell if the path is for delta table or non delta\n * table at this stage. If neither of the two are set, throws an exception.\n */\nobject UnresolvedPathOrIdentifier {\n  def apply(\n      path: Option[String],\n      tableIdentifier: Option[TableIdentifier],\n      options: Map[String, String],\n      cmd: String): LogicalPlan = {\n    (path, tableIdentifier) match {\n      case (_, Some(t)) => UnresolvedTable(t.nameParts, cmd)\n      case (Some(p), None) => UnresolvedPathBasedTable(p, options, cmd)\n      case _ => throw new IllegalArgumentException(\n        s\"At least one of path or tableIdentifier must be provided to $cmd\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaTableIdentifier.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\n// scalastyle:on import.ordering.noEmptyLine\n\n/**\n * An identifier for a Delta table containing one of the path or the table identifier.\n */\ncase class DeltaTableIdentifier(\n    path: Option[String] = None,\n    table: Option[TableIdentifier] = None) {\n  assert(path.isDefined ^ table.isDefined, \"Please provide one of the path or the table identifier\")\n\n  val identifier: String = path.getOrElse(table.get.identifier)\n\n  def database: Option[String] = table.flatMap(_.database)\n\n  def getPath(spark: SparkSession): Path = {\n    path.map(new Path(_)).getOrElse {\n      val metadata = spark.sessionState.catalog.getTableMetadata(table.get)\n      new Path(metadata.location)\n    }\n  }\n\n  /**\n   * Escapes back-ticks within the identifier name with double-back-ticks.\n   */\n  private def quoteIdentifier(name: String): String = name.replace(\"`\", \"``\")\n\n  def quotedString: String = {\n    val replacedId = quoteIdentifier(identifier)\n    val replacedDb = database.map(quoteIdentifier)\n\n    if (replacedDb.isDefined) s\"`${replacedDb.get}`.`$replacedId`\" else s\"`$replacedId`\"\n  }\n\n  def unquotedString: String = {\n    if (database.isDefined) s\"${database.get}.$identifier\" else identifier\n  }\n\n  override def toString: String = quotedString\n}\n\n/**\n * Utilities for DeltaTableIdentifier.\n * TODO(burak): Get rid of these utilities. DeltaCatalog should be the skinny-waist for figuring\n * these things out.\n */\nobject DeltaTableIdentifier extends DeltaLogging {\n\n  /**\n   * Check the specified table identifier represents a Delta path.\n   */\n  def isDeltaPath(spark: SparkSession, identifier: TableIdentifier): Boolean = {\n    val catalog = spark.sessionState.catalog\n    def tableIsTemporaryTable = catalog.isTempView(identifier)\n    def tableExists: Boolean = {\n      try {\n        catalog.databaseExists(identifier.database.get) && catalog.tableExists(identifier)\n      } catch {\n        case e: AnalysisException if gluePermissionError(e) =>\n          logWarning(log\"Received an access denied error from Glue. Will check to see if this \" +\n            log\"identifier (${MDC(DeltaLogKeys.TABLE_NAME, identifier)}) is path based.\", e)\n          false\n      }\n    }\n\n    spark.sessionState.conf.runSQLonFile &&\n      new Path(identifier.table).isAbsolute &&\n      DeltaSourceUtils.isDeltaTable(identifier.database) &&\n      !tableIsTemporaryTable &&\n      !tableExists\n  }\n\n  /**\n   * Creates a [[DeltaTableIdentifier]] if the specified table identifier represents a Delta table,\n   * otherwise returns [[None]].\n   */\n  def apply(spark: SparkSession, identifier: TableIdentifier)\n      : Option[DeltaTableIdentifier] = recordFrameProfile(\n          \"DeltaAnalysis\", \"DeltaTableIdentifier.resolve\") {\n    if (isDeltaPath(spark, identifier)) {\n      Some(DeltaTableIdentifier(path = Option(identifier.table)))\n    } else if (DeltaTableUtils.isDeltaTable(spark, identifier)) {\n      Some(DeltaTableIdentifier(table = Option(identifier)))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * When users try to access Delta tables by path, e.g. delta.`/some/path`, we need to first check\n   * if such a table exists in the MetaStore (due to Spark semantics :/). The Glue MetaStore may\n   * return Access Denied errors during this check. This method matches on this failure mode.\n   */\n  def gluePermissionError(e: AnalysisException): Boolean = e.getCause match {\n    case h: Exception if h.getClass.getName == \"org.apache.hadoop.hive.ql.metadata.HiveException\" =>\n      Seq(\"AWSGlue\", \"AccessDeniedException\").forall { kw =>\n        h.getMessage.contains(kw)\n      }\n    case _ => false\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaTableValueFunctions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.text.SimpleDateFormat\nimport java.util.{Date, Locale}\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.DeltaDataSource\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.FunctionIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{FunctionRegistryBase, NamedRelation, TableFunctionRegistry, UnresolvedLeafNode, UnresolvedRelation}\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, ExpressionInfo, Literal, StringLiteral}\nimport org.apache.spark.sql.catalyst.plans.logical.{LeafNode, LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.connector.catalog.V1Table\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.execution.datasources.v2.{DataSourceV2Relation, DataSourceV2RelationShim}\nimport org.apache.spark.sql.types.{IntegerType, LongType, StringType, TimestampType}\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Resolve Delta specific table-value functions.\n */\nobject DeltaTableValueFunctions {\n  val CDC_NAME_BASED = \"table_changes\"\n  val CDC_PATH_BASED = \"table_changes_by_path\"\n  val supportedFnNames = Seq(CDC_NAME_BASED, CDC_PATH_BASED)\n\n  // For use with SparkSessionExtensions\n  type TableFunctionDescription =\n    (FunctionIdentifier, ExpressionInfo, TableFunctionRegistry.TableFunctionBuilder)\n\n  /**\n   * For a supported Delta table value function name, get the TableFunctionDescription to be\n   * injected in DeltaSparkSessionExtension\n   */\n  def getTableValueFunctionInjection(fnName: String): TableFunctionDescription = {\n    val (info, builder) = fnName match {\n      case CDC_NAME_BASED => FunctionRegistryBase.build[CDCNameBased](fnName, since = None)\n      case CDC_PATH_BASED => FunctionRegistryBase.build[CDCPathBased](fnName, since = None)\n      case _ => throw DeltaErrors.invalidTableValueFunction(fnName)\n    }\n    val ident = FunctionIdentifier(fnName)\n    (ident, info, builder)\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////\n//                     Logical plans for Delta TVFs                      //\n///////////////////////////////////////////////////////////////////////////\n\n/**\n * Represents an unresolved Delta Table Value Function\n */\ntrait DeltaTableValueFunction extends UnresolvedLeafNode {\n  def fnName: String\n  val functionArgs: Seq[Expression]\n}\n\n/**\n * Base trait for analyzing `table_changes` and `table_changes_for_path`. The resolution works as\n * follows:\n *  1. The TVF logical plan is resolved using the TableFunctionRegistry in the Analyzer. This uses\n *     reflection to create one of `CDCNameBased` or `CDCPathBased` by passing all the arguments.\n *  2. DeltaAnalysis turns the plans to a `TableChanges` node to resolve the DeltaTable. This can\n *     be resolved by the DeltaCatalog for tables or DeltaAnalysis for the path based use.\n *  3. TableChanges then turns into a LogicalRelation that returns the CDC relation.\n */\ntrait CDCStatementBase extends DeltaTableValueFunction {\n  /** Get the table that the function is being called on as an unresolved relation */\n  protected def getTable(spark: SparkSession, name: Expression): LogicalPlan\n\n  if (functionArgs.size < 2) {\n    throw new DeltaAnalysisException(\n      errorClass = \"INCORRECT_NUMBER_OF_ARGUMENTS\",\n      messageParameters = Array(\n        \"not enough args\", // failure\n        fnName,\n        \"2\", // minArgs\n        \"3\")) // maxArgs\n  }\n  if (functionArgs.size > 3) {\n    throw new DeltaAnalysisException(\n      errorClass = \"INCORRECT_NUMBER_OF_ARGUMENTS\",\n      messageParameters = Array(\n        \"too many args\", // failure\n        fnName,\n        \"2\", // minArgs\n        \"3\")) // maxArgs\n  }\n\n  protected def getOptions: CaseInsensitiveStringMap = {\n    def toDeltaOption(keyPrefix: String, value: Expression, position: Int): (String, String) = {\n      val evaluated = try {\n        val fakePlan = util.AnalysisHelper.FakeLogicalPlan(Seq(value), Nil)\n        val timestampExpression =\n          org.apache.spark.sql.catalyst.optimizer.ComputeCurrentTime(fakePlan).expressions.head\n        timestampExpression.eval().toString\n      } catch {\n        case _: NullPointerException => throw DeltaErrors.nullRangeBoundaryInCDCRead()\n        case e: SparkException if e.getErrorClass == \"INTERNAL_ERROR\" =>\n          throw DeltaErrors.cdcNonConstantArgument(fnName, keyPrefix, position, value)\n      }\n      value.dataType match {\n        // We dont need to explicitly handle ShortType as it is parsed as IntegerType.\n        case _: IntegerType | LongType => (keyPrefix + \"Version\") -> evaluated\n        case _: StringType => (keyPrefix + \"Timestamp\") -> evaluated\n        case _: TimestampType => (keyPrefix + \"Timestamp\") -> {\n          val fmt = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\")\n          // when evaluated the time is represented with microseconds, which needs to be trimmed.\n          fmt.format(new Date(evaluated.toLong / 1000))\n        }\n        case _ =>\n          throw DeltaErrors.unsupportedExpression(s\"${keyPrefix} option\", value.dataType,\n            Seq(\"IntegerType\", \"LongType\", \"StringType\", \"TimestampType\"))\n      }\n    }\n\n    val startingOption = toDeltaOption(\"starting\", functionArgs(1), 2)\n    val endingOption = functionArgs.drop(2).headOption.map(toDeltaOption(\"ending\", _, 3))\n    val options = Map(DeltaDataSource.CDC_ENABLED_KEY -> \"true\", startingOption) ++ endingOption\n    new CaseInsensitiveStringMap(options.asJava)\n  }\n\n  protected def getStringLiteral(e: Expression, whatFor: String): String = e match {\n    case StringLiteral(value) => value\n    case o =>\n      throw DeltaErrors.unsupportedExpression(whatFor, o.dataType, Seq(\"StringType literal\"))\n  }\n\n  def toTableChanges(spark: SparkSession): TableChanges =\n    TableChanges(getTable(spark, functionArgs.head), fnName)\n}\n\n/**\n * Plan for the \"table_changes\" function\n */\ncase class CDCNameBased(override val functionArgs: Seq[Expression])\n  extends CDCStatementBase {\n  override def fnName: String = DeltaTableValueFunctions.CDC_NAME_BASED\n  // Provide a constructor to get a better error message, when no expressions are provided\n  def this() = this(Nil)\n\n  override protected def getTable(spark: SparkSession, name: Expression): LogicalPlan = {\n    val stringId = getStringLiteral(name, \"table name\")\n    val identifier = spark.sessionState.sqlParser.parseMultipartIdentifier(stringId)\n    UnresolvedRelation(identifier, getOptions, isStreaming = false)\n  }\n}\n\n/**\n * Plan for the \"table_changes_by_path\" function\n */\ncase class CDCPathBased(override val functionArgs: Seq[Expression])\n  extends CDCStatementBase {\n  override def fnName: String = DeltaTableValueFunctions.CDC_PATH_BASED\n  // Provide a constructor to get a better error message, when no expressions are provided\n  def this() = this(Nil)\n\n  override protected def getTable(spark: SparkSession, name: Expression): LogicalPlan = {\n    UnresolvedPathBasedDeltaTableRelation(getStringLiteral(name, \"table path\"), getOptions)\n  }\n}\n\ncase class TableChanges(\n    child: LogicalPlan,\n    fnName: String,\n    cdcAttr: Seq[Attribute] = CDCReader.cdcAttributes) extends UnaryNode {\n\n  override lazy val resolved: Boolean = false\n  override def withNewChildInternal(newChild: LogicalPlan): LogicalPlan =\n    this.copy(child = newChild)\n\n  override def output: Seq[Attribute] = Nil\n\n  /** Converts the table changes plan to a query over a Delta table */\n  def toReadQuery: LogicalPlan = child.transformUp {\n    case DataSourceV2RelationShim(d: DeltaTableV2, _, _, _, options) =>\n      // withOptions empties the catalog table stats\n      d.withOptions(options.asScala.toMap).toLogicalRelation\n    case r: NamedRelation =>\n      throw DeltaErrors.notADeltaTableException(fnName, r.name)\n    case l: LogicalRelation =>\n      val relationName = l.catalogTable.map(_.identifier.toString).getOrElse(\"relation\")\n      throw DeltaErrors.notADeltaTableException(fnName, relationName)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaThrowable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.SparkThrowable\n\n/**\n * The trait for all exceptions of Delta code path.\n */\ntrait DeltaThrowable extends SparkThrowable {\n\n  override def getCondition(): String = getErrorClass()\n\n  // Portable error identifier across SQL engines\n  // If null, error class or SQLSTATE is not set\n  override def getSqlState: String =\n    DeltaThrowableHelper.getSqlState(this.getErrorClass.split('.').head)\n\n  // True if this error is an internal error.\n  override def isInternalError: Boolean = DeltaThrowableHelper.isInternalError(this.getErrorClass)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaThrowableHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.FileNotFoundException\nimport java.net.URL\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.errors.QueryCompilationErrors\nimport org.apache.spark.{SparkException, SparkThrowable}\n\nimport org.apache.spark.ErrorClassesJsonReader\nimport org.apache.spark.util.Utils\n\n/**\n * The helper object for Delta code base to pick error class template and compile\n * the exception message.\n */\nobject DeltaThrowableHelper\n{\n  /**\n   * Handles a breaking change (SPARK-46810) between Spark 3.5 and Spark Master (4.0) where\n   * `error-classes.json` was renamed to `error-conditions.json`.\n   */\n  val SPARK_ERROR_CLASS_SOURCE_FILE = \"error/error-conditions.json\"\n\n  def showColumnsWithConflictDatabasesError(\n      db: Seq[String], v1TableName: TableIdentifier): Throwable = {\n    QueryCompilationErrors.showColumnsWithConflictNamespacesError(\n      namespaceA = db,\n      namespaceB = v1TableName.database.get :: Nil)\n  }\n\n  /**\n   * Try to find the error class source file and throw exception if it is no found.\n   */\n  private def safeGetErrorClassesSource(sourceFile: String): URL = {\n    val classLoader = Utils.getContextOrSparkClassLoader\n    Option(classLoader.getResource(sourceFile)).getOrElse {\n      throw new FileNotFoundException(\n        s\"\"\"Cannot find the error class definition file on path $sourceFile\" through the \"\"\" +\n          s\"class loader ${classLoader.toString}\")\n    }\n  }\n\n  lazy val sparkErrorClassSource: URL = {\n    safeGetErrorClassesSource(SPARK_ERROR_CLASS_SOURCE_FILE)\n  }\n\n  def deltaErrorClassSource: URL = {\n    safeGetErrorClassesSource(\"error/delta-error-classes.json\")\n  }\n\n  private val errorClassReader = new ErrorClassesJsonReader(\n    Seq(deltaErrorClassSource, sparkErrorClassSource))\n\n  /**\n   * @return The formated error message. The format for standalone error classes is:\n   *         [ERROR_CLASS] Main error message\n   *         The format for errors with sub-error classes:\n   *         [MAIN_CLASS.SUB_CLASS] Main error message Sub-error message\n   */\n  def getMessage(errorClass: String, messageParameters: Array[String]): String = {\n    validateParameterValues(errorClass, errorSubClass = null, messageParameters)\n    val template = errorClassReader.getMessageTemplate(errorClass)\n    val message = formatMessage(errorClass, messageParameters, template)\n    s\"[$errorClass] $message\"\n  }\n\n  private def formatMessage(\n      errorClass: String,\n      messageParameters: Array[String],\n      template: String) = {\n    String.format(template.replaceAll(\"<[a-zA-Z0-9_-]+>\", \"%s\"), messageParameters: _*)\n  }\n\n  /**\n   * Returns a combined error message for an error class with multiple sub-error classes.\n   * Use [[getMessage]] to load a single sub-error class message prefixed with\n   * the main class message.\n   * @return The formatted error message including main and sub-error messages. The format is:\n   *         [ERROR_CLASS] Main error message\n   *         - Sub-error message 1\n   *         - Sub-error message 2\n   *         ...\n   */\n  def getMessageWithSubErrors(\n      mainErrorClass: String,\n      mainMessageParameters: Array[String],\n      subErrorInformationSeq: Seq[(String, Array[String])]): String = {\n    require(subErrorInformationSeq.nonEmpty)\n    // Get main message\n    val mainMessage = {\n      val template = getMainMessageTemplate(mainErrorClass)\n      formatMessage(mainErrorClass, mainMessageParameters, template)\n    }\n\n    // Get sub-error messages\n    val subMessageSeq = subErrorInformationSeq.map {\n      case (subErrorClass, subMessageParameters) =>\n        val fullErrorClass = s\"$mainErrorClass.$subErrorClass\"\n        val template = getSubMessageTemplate(fullErrorClass)\n        formatMessage(fullErrorClass, subMessageParameters, template)\n    }\n\n    // Combine main and sub errors\n    s\"[$mainErrorClass] $mainMessage\\n${subMessageSeq.map(\"- \" + _ + \"\\n\")\n      .mkString.stripSuffix(\"\\n\")}\"\n  }\n\n  /**\n   * Get the message template for a main error class.\n   * @param errorClass The main error class. It can only be MAIN_CLASS (not MAIN_CLASS.SUB_CLASS).\n   * @return The message template.\n   */\n  def getMainMessageTemplate(errorClass: String): String = {\n    val errorClasses = errorClass.split(\"\\\\.\")\n    assert(errorClasses.length == 1)\n\n    val mainErrorClass = errorClasses.head\n    val errorInfo = errorClassReader.errorInfoMap.getOrElse(\n      mainErrorClass,\n      throw SparkException.internalError(s\"Cannot find main error class '$errorClass'\"))\n\n    errorInfo.messageTemplate\n  }\n\n  /**\n   * Get the message template for a sub error class without prefixing with the main error template.\n   * @param errorClass The sub error class. It can only be MAIN_CLASS.SUB_CLASS (not MAIN_CLASS).\n   * @return The message template.\n   */\n  def getSubMessageTemplate(errorClass: String): String = {\n    val errorClasses = errorClass.split(\"\\\\.\")\n    assert(errorClasses.length == 2)\n\n    val mainErrorClass = errorClasses.head\n    val subErrorClass = errorClasses.last\n    val errorInfo = errorClassReader.errorInfoMap.getOrElse(\n      mainErrorClass,\n      throw SparkException.internalError(s\"Cannot find main error class '$errorClass'\"))\n    assert(errorInfo.subClass.isDefined, errorClass)\n\n    val errorSubInfo = errorInfo.subClass.get.getOrElse(\n      subErrorClass,\n      throw SparkException.internalError(s\"Cannot find sub error class '$errorClass'\"))\n    errorSubInfo.messageTemplate\n  }\n\n  def getSqlState(errorClass: String): String = errorClassReader.getSqlState(errorClass)\n\n  def isInternalError(errorClass: String): Boolean = errorClass == \"INTERNAL_ERROR\"\n\n  def getParameterNames(errorClass: String, errorSubClass: String): Array[String] = {\n    val wholeErrorClass = if (errorSubClass == null) {\n      errorClass\n    } else {\n      errorClass + \".\" + errorSubClass\n    }\n    val parameterizedMessage = errorClassReader.getMessageTemplate(wholeErrorClass)\n    parsePrameterNamesFromParameterizedMessage(parameterizedMessage)\n  }\n\n  private def parsePrameterNamesFromParameterizedMessage(parameterizedMessage: String) = {\n    val pattern = \"<[a-zA-Z0-9_-]+>\".r\n    val matches = pattern.findAllIn(parameterizedMessage)\n    val parameterSeq = matches.toArray\n    val parameterNames = parameterSeq.map(p => p.stripPrefix(\"<\").stripSuffix(\">\"))\n    parameterNames\n  }\n\n  def getMessageParameters(\n      errorClass: String,\n      errorSubClass: String,\n      parameterValues: Array[String]): java.util.Map[String, String] = {\n    validateParameterValues(errorClass, errorSubClass, parameterValues)\n    getParameterNames(errorClass, errorSubClass).zip(parameterValues).toMap.asJava\n  }\n\n  def getMainErrorMessageParameters(\n      errorClass: String,\n      parameterValues: Array[String]): java.util.Map[String, String] = {\n    val parameterizedMessage = getMainMessageTemplate(errorClass)\n    parsePrameterNamesFromParameterizedMessage(parameterizedMessage)\n      .zip(parameterValues).toMap.asJava\n  }\n\n  /**\n   * Verify that the provided parameter values match the parameter names in the error message\n   * template. The number of parameters must match, and the parameters with the same name must\n   * have the same value.\n   */\n  private def validateParameterValues(\n      errorClass: String,\n      errorSubClass: String,\n      parameterValues: Array[String]): Unit = if (Utils.isTesting) {\n    val parameterNames = getParameterNames(errorClass, errorSubClass)\n    assert(parameterNames.size == parameterValues.size, \"The number of parameter values provided \" +\n      s\"to error class $errorClass ${Option(errorSubClass).getOrElse(\"\")}  does not match the \" +\n      s\"number of parameters in the error message template.\\n\" +\n      s\"Parameters in the template: ${parameterNames.mkString(\", \")}\\n\" +\n      s\"Parameter values provided: ${parameterValues.mkString(\", \")}\")\n    val parameterPairs = parameterNames.zip(parameterValues)\n    val parameterMap = parameterPairs.toMap\n    parameterPairs.foreach { case (name, value) =>\n      assert(parameterMap(name) == value, s\"Parameter <$name> in the error message for error \" +\n        s\"class $errorClass ${Option(errorSubClass).getOrElse(\"\")} was assigned two different \" +\n        s\"values: ${parameterMap(name)} and $value\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaTimeTravelSpec.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\n\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.commons.lang3.time.FastDateFormat\n\nimport org.apache.spark.sql.catalyst.expressions.{Cast, Expression, Literal, PreciseTimestampConversion, RuntimeReplaceable, Unevaluable}\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{LongType, TimestampType}\n\n/**\n * The specification to time travel a Delta Table to the given `timestamp` or `version`.\n * @param timestamp An expression that can be evaluated into a timestamp. The expression cannot\n *                  be a subquery.\n * @param version The version of the table to time travel to. Must be >= 0.\n * @param creationSource The API used to perform time travel, e.g. `atSyntax`, `dfReader` or SQL\n * @param enforceRetention Whether to enforce file delete retention and block access to expired\n *                         snapshot regardless of the VACUUM status.\n */\ncase class DeltaTimeTravelSpec(\n    timestamp: Option[Expression],\n    version: Option[Long],\n    creationSource: Option[String],\n    enforceRetention: Boolean = true) extends DeltaLogging {\n\n  assert(version.isEmpty ^ timestamp.isEmpty,\n    \"Either the version or timestamp should be provided for time travel\")\n\n  /**\n   * Compute the timestamp to use for time travelling the relation from the given expression for\n   * the given time zone.\n   */\n  def getTimestamp(conf: SQLConf): Timestamp = {\n    // note @brkyvz (2020-04-13): not great that we need to handle RuntimeReplaceable expressions...\n    val timeZone = conf.sessionLocalTimeZone\n    val evaluable = timestamp match {\n      case Some(e) => e.transform {\n        case rr: RuntimeReplaceable =>\n          rr.children.head\n        case e: Unevaluable =>\n          recordDeltaEvent(null, \"delta.timeTravel.unexpected\", data = e.sql)\n          throw new IllegalStateException(s\"Unsupported expression (${e.sql}) for time travel.\")\n      }\n      case None =>\n        // scalastyle:off throwerror\n        throw new AssertionError(\n          \"Should not ask to get Timestamp for time travel when the timestamp was not available\")\n      // scalastyle:on throwerror\n    }\n    val strict = conf.getConf(DeltaSQLConf.DELTA_TIME_TRAVEL_STRICT_TIMESTAMP_PARSING)\n    val castResult = Cast(evaluable, TimestampType, Option(timeZone), ansiEnabled = false).eval()\n    if (strict && castResult == null) {\n      throw DeltaErrors.timestampInvalid(evaluable)\n    }\n    DateTimeUtils.toJavaTimestamp(castResult.asInstanceOf[java.lang.Long])\n  }\n\n  /**\n   * Compute the timestamp to use for time travelling the relation from the given expression for\n   * the given time zone if this spec has a timestamp defined.\n   */\n  def getTimestampOpt(conf: SQLConf): Option[Timestamp] = {\n    timestamp.map(_ => getTimestamp(conf))\n  }\n}\n\nobject DeltaTimeTravelSpec {\n  /** A regex which looks for the pattern ...@v(some numbers) for extracting the version number */\n  private val VERSION_URI_FOR_TIME_TRAVEL = \".*@[vV](\\\\d+)$\".r\n\n  /** The timestamp format which we accept after the `@` character. */\n  private val TIMESTAMP_FORMAT = \"yyyyMMddHHmmssSSS\"\n\n  /** Length of yyyyMMddHHmmssSSS */\n  private val TIMESTAMP_FORMAT_LENGTH = TIMESTAMP_FORMAT.length\n\n  /** A regex which looks for the pattern ...@(yyyyMMddHHmmssSSS) for extracting timestamps. */\n  private val TIMESTAMP_URI_FOR_TIME_TRAVEL = s\".*@(\\\\d{$TIMESTAMP_FORMAT_LENGTH})$$\".r\n\n  /** Returns whether the given table identifier may contain time travel syntax. */\n  def isApplicable(conf: SQLConf, identifier: String): Boolean = {\n    conf.getConf(DeltaSQLConf.RESOLVE_TIME_TRAVEL_ON_IDENTIFIER) &&\n      identifierContainsTimeTravel(identifier)\n  }\n\n  /** Checks if the table identifier contains patterns that resemble time travel syntax. */\n  private def identifierContainsTimeTravel(identifier: String): Boolean = identifier match {\n    case TIMESTAMP_URI_FOR_TIME_TRAVEL(ts) => true\n    case VERSION_URI_FOR_TIME_TRAVEL(v) => true\n    case _ => false\n  }\n\n  /** Adds a time travel node based on the special syntax in the table identifier. */\n  def resolvePath(conf: SQLConf, identifier: String): (DeltaTimeTravelSpec, String) = {\n    identifier match {\n      case TIMESTAMP_URI_FOR_TIME_TRAVEL(ts) =>\n        val timestamp = parseTimestamp(ts, conf.sessionLocalTimeZone)\n        // Drop the 18 characters in the right, which is the timestamp format and the @ character.\n        val realIdentifier = identifier.dropRight(TIMESTAMP_FORMAT_LENGTH + 1)\n\n        DeltaTimeTravelSpec(Some(timestamp), None, Some(\"atSyntax.path\")) -> realIdentifier\n      case VERSION_URI_FOR_TIME_TRAVEL(v) =>\n        // Drop the version, and `@v` characters from the identifier\n        val realIdentifier = identifier.dropRight(v.length + 2)\n        DeltaTimeTravelSpec(None, Some(v.toLong), Some(\"atSyntax.path\")) -> realIdentifier\n    }\n  }\n\n  /**\n   * Parse the given timestamp string into a proper Catalyst TimestampType. We support millisecond\n   * level precision, therefore don't use standard SQL timestamp functions, which only support\n   * second level precision.\n   *\n   * @throws `AnalysisException` when the timestamp format doesn't match our criteria\n   */\n  private def parseTimestamp(ts: String, timeZone: String): Expression = {\n    val format = FastDateFormat.getInstance(TIMESTAMP_FORMAT, DateTimeUtils.getTimeZone(timeZone))\n\n    try {\n      val sqlTs = DateTimeUtils.fromJavaTimestamp(new java.sql.Timestamp(format.parse(ts).getTime))\n      PreciseTimestampConversion(Literal(sqlTs), LongType, TimestampType)\n    } catch {\n      case e: java.text.ParseException =>\n        throw DeltaErrors.invalidTimestampFormat(ts, TIMESTAMP_FORMAT, Some(e))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaUDF.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{DeletionVectorDescriptor, Protocol}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeletedRecordCountsHistogram\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.encoders.encoderFor\nimport org.apache.spark.sql.expressions.{SparkUserDefinedFunction, UserDefinedFunction}\nimport org.apache.spark.sql.functions.udf\n\n/**\n * Define a few templates for udfs used by Delta. Use these templates to create\n * `SparkUserDefinedFunction` to avoid creating new Encoders. This would save us from touching\n * `ScalaReflection` to reduce the lock contention in concurrent queries.\n */\nobject DeltaUDF {\n\n  def stringFromString(f: String => String): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(stringFromStringTemplate, f, udf(f))\n\n  def intFromString(f: String => Int): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(intFromStringTemplate, f, udf(f))\n\n  def intFromStringBoolean(f: (String, Boolean) => Int): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(intFromStringBooleanTemplate, f, udf(f))\n\n  def boolean(f: () => Boolean): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(booleanTemplate, f, udf(f))\n\n  def stringFromMap(f: Map[String, String] => String): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(stringFromMapTemplate, f, udf(f))\n\n  def deletedRecordCountsHistogramFromArrayLong(\n      f: Array[Long] => DeletedRecordCountsHistogram): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(deletedRecordCountsHistogramFromArrayLongTemplate, f, udf(f))\n\n  def stringFromDeletionVectorDescriptor(\n      f: DeletionVectorDescriptor => String): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(stringFromDeletionVectorDescriptorTemplate, f, udf(f))\n\n  def stringOptionFromDeletionVectorDescriptor(\n      f: DeletionVectorDescriptor => Option[String]): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(stringOptionFromDeletionVectorDescriptorTemplate, f, udf(f))\n\n  def booleanFromDeletionVectorDescriptor(\n      f: DeletionVectorDescriptor => Boolean): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(booleanFromDeletionVectorDescriptorTemplate, f, udf(f))\n\n  def booleanFromString(s: String => Boolean): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(booleanFromStringTemplate, s, udf(s))\n\n  def booleanFromProtocol(f: Protocol => Boolean): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(booleanFromProtocol, f, udf(f))\n\n  def booleanFromMap(f: Map[String, String] => Boolean): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(booleanFromMapTemplate, f, udf(f))\n\n  def booleanFromByte(x: Byte => Boolean): UserDefinedFunction =\n    createUdfFromTemplateUnsafe(booleanFromByteTemplate, x, udf(x))\n\n  private lazy val stringFromStringTemplate =\n    udf[String, String](identity).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val booleanTemplate = udf(() => true).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val intFromStringTemplate =\n    udf((_: String) => 1).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val intFromStringBooleanTemplate =\n    udf((_: String, _: Boolean) => 1).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val stringFromMapTemplate =\n    udf((_: Map[String, String]) => \"\").asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val deletedRecordCountsHistogramFromArrayLongTemplate =\n    udf((_: Array[Long]) => DeletedRecordCountsHistogram(Array.empty))\n      .asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val stringFromDeletionVectorDescriptorTemplate =\n    udf((_: DeletionVectorDescriptor) => \"\").asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val stringOptionFromDeletionVectorDescriptorTemplate =\n    udf((_: DeletionVectorDescriptor) => Some(\"\")).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val booleanFromDeletionVectorDescriptorTemplate =\n    udf((_: DeletionVectorDescriptor) => false).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val booleanFromStringTemplate =\n    udf((_: String) => false).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val booleanFromProtocol =\n    udf((_: Protocol) => true).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val booleanFromMapTemplate =\n    udf((_: Map[String, String]) => true).asInstanceOf[SparkUserDefinedFunction]\n\n  private lazy val booleanFromByteTemplate =\n    udf((_: Byte) => true).asInstanceOf[SparkUserDefinedFunction]\n\n  /**\n   * Return a `UserDefinedFunction` for the given `f` from `template` if\n   * `INTERNAL_UDF_OPTIMIZATION_ENABLED` is enabled. Otherwise, `orElse` will be called to create a\n   * new `UserDefinedFunction`.\n   */\n  private def createUdfFromTemplateUnsafe(\n      template: SparkUserDefinedFunction,\n      f: AnyRef,\n      orElse: => UserDefinedFunction): UserDefinedFunction = {\n    if (SparkSession.active.sessionState.conf\n      .getConf(DeltaSQLConf.INTERNAL_UDF_OPTIMIZATION_ENABLED)) {\n      val inputEncoders = template.inputEncoders.map(_.map(e => encoderFor(e)))\n      val outputEncoder = template.outputEncoder.map(e => encoderFor(e))\n      template.copy(f = f, inputEncoders = inputEncoders, outputEncoder = outputEncoder)\n    } else {\n      orElse\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaUnsupportedOperationsCheck.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.util.control.NonFatal\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.ResolvedTable\nimport org.apache.spark.sql.catalyst.catalog.CatalogTableType\nimport org.apache.spark.sql.catalyst.plans.logical.{AppendData, DropTable, LogicalPlan, OverwriteByExpression, ShowCreateTable, V2WriteCommand}\nimport org.apache.spark.sql.execution.command._\nimport org.apache.spark.sql.execution.datasources.v2.{DataSourceV2Relation, DataSourceV2RelationShim}\n\n/**\n * A rule to add helpful error messages when Delta is being used with unsupported Hive operations\n * or if an unsupported operation is being made, e.g. a DML operation like\n * INSERT/UPDATE/DELETE/MERGE when a table doesn't exist.\n */\ncase class DeltaUnsupportedOperationsCheck(spark: SparkSession)\n  extends (LogicalPlan => Unit)\n  with DeltaLogging {\n\n  private def fail(operation: String, tableIdent: TableIdentifier): Unit = {\n    val metadata = try Some(spark.sessionState.catalog.getTableMetadata(tableIdent)) catch {\n      case NonFatal(_) => None\n    }\n    if (metadata.exists(DeltaTableUtils.isDeltaTable)) {\n      throw DeltaErrors.operationNotSupportedException(operation, tableIdent)\n    }\n  }\n\n  private def fail(operation: String, provider: String): Unit = {\n    if (DeltaSourceUtils.isDeltaDataSourceName(provider)) {\n      throw DeltaErrors.operationNotSupportedException(operation)\n    }\n  }\n\n  def apply(plan: LogicalPlan): Unit = plan.foreach {\n    // Unsupported Hive commands\n\n    case a: AnalyzePartitionCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.analyzePartition\")\n      fail(operation = \"ANALYZE TABLE PARTITION\", a.tableIdent)\n\n    case a: AlterTableAddPartitionCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.addPartition\")\n      fail(operation = \"ALTER TABLE ADD PARTITION\", a.tableName)\n\n    case a: AlterTableDropPartitionCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.dropPartition\")\n      fail(operation = \"ALTER TABLE DROP PARTITION\", a.tableName)\n\n    case a: RepairTableCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.recoverPartitions\")\n      fail(operation = \"ALTER TABLE RECOVER PARTITIONS\", a.tableName)\n\n    case a: AlterTableSerDePropertiesCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.alterSerDe\")\n      fail(operation = \"ALTER TABLE SET SERDEPROPERTIES\", a.tableName)\n\n    case l: LoadDataCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.loadData\")\n      fail(operation = \"LOAD DATA\", l.table)\n\n    case i: InsertIntoDataSourceDirCommand =>\n      recordDeltaEvent(null, \"delta.unsupported.insertDirectory\")\n      fail(operation = \"INSERT OVERWRITE DIRECTORY\", i.provider)\n\n    case ShowCreateTable(t: ResolvedTable, _, _) if t.table.isInstanceOf[DeltaTableV2] =>\n        recordDeltaEvent(null, \"delta.unsupported.showCreateTable\")\n        fail(operation = \"SHOW CREATE TABLE\", \"DELTA\")\n\n    // Delta table checks\n    case append: AppendData =>\n      val op = if (append.isByName) \"APPEND\" else \"INSERT\"\n      checkDeltaTableExists(append, op)\n\n    case overwrite: OverwriteByExpression =>\n      checkDeltaTableExists(overwrite, \"OVERWRITE\")\n\n    case _: DropTable =>\n      // For Delta tables being dropped, we do not need the underlying Delta log to exist so this is\n      // OK\n      return\n\n    case DataSourceV2RelationShim(tbl: DeltaTableV2, _, _, _, _) if !tbl.tableExists =>\n      throw DeltaErrors.pathNotExistsException(tbl.deltaLog.dataPath.toString)\n\n    case r: ResolvedTable if r.table.isInstanceOf[DeltaTableV2] &&\n        !r.table.asInstanceOf[DeltaTableV2].tableExists =>\n      throw DeltaErrors.pathNotExistsException(\n        r.table.asInstanceOf[DeltaTableV2].deltaLog.dataPath.toString)\n\n    case _ => // OK\n  }\n\n  /**\n   * Check that the given operation is being made on a full Delta table that exists.\n   */\n  private def checkDeltaTableExists(command: V2WriteCommand, operation: String): Unit = {\n    command.table match {\n      case DeltaRelation(lr) =>\n        // the extractor performs the check that we want if this is indeed being called on a Delta\n        // table. It should leave others unchanged\n        if (DeltaFullTable.unapply(lr).isEmpty) {\n          throw DeltaErrors.notADeltaTableException(operation)\n        }\n      case _ =>\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DeltaViewHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.catalyst.analysis.EliminateSubqueryAliases\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, Cast, NamedExpression}\nimport org.apache.spark.sql.catalyst.optimizer.CollapseProject\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project, SubqueryAlias, View, ViewShims}\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.internal.SQLConf\n\nobject DeltaViewHelper {\n  def stripTempViewForMerge(plan: LogicalPlan, conf: SQLConf): LogicalPlan = {\n    // Check that the two expression lists have the same names and types in the same order, and\n    // are either attributes or direct casts of attributes.\n    def attributesMatch(left: Seq[NamedExpression], right: Seq[NamedExpression]): Boolean = {\n      if (left.length != right.length) return false\n\n      val allowedExprs = (left ++ right).forall {\n        case _: Attribute => true\n        case Alias(Cast(attr: Attribute, _, _, _), name) => conf.resolver(attr.name, name)\n        case _ => false\n      }\n\n      val exprsMatch = left.zip(right).forall {\n        case (a, b) => a.dataType == b.dataType && conf.resolver(a.name, b.name)\n      }\n\n      allowedExprs && exprsMatch\n    }\n\n\n    // We have to do a pretty complicated transformation here to support using two specific things\n    // which are not a Delta table as the target of Delta DML commands:\n    // A view defined as `SELECT * FROM underlying_tbl`\n    // A view defined as `SELECT * FROM underlying_tbl as alias`\n    // This requires stripping their intermediate nodes and pulling out just the scan, because\n    // some of our internal attribute fiddling requires the target plan to have the same attribute\n    // IDs as the underlying scan.\n    object ViewPlan {\n      def unapply(\n          plan: LogicalPlan): Option[(CatalogTable, Seq[NamedExpression], LogicalRelation)] = {\n        // A `SELECT * from underlying_table` view will have:\n        // * A View node marking it as a view.\n        // * An outer Project explicitly casting the scanned types to the types defined in the\n        //   metastore for the view. We don't need this cast for Delta DML commands and it will\n        //   end up being eliminated.\n        // * An arbitrary number of inner no-op project and subquery aliases.\n        // * The actual scan of the Delta table.\n        // We check for this by removing all subquery aliases and collapsing all Projects into one\n        // and ensuring that the project list exactly match the output of the scan.\n        CollapseProject(EliminateSubqueryAliases(plan)) match {\n          case ViewShims.TempViewWithChild(desc,\n              Project(outerList, scan: LogicalRelation))\n            if attributesMatch(outerList, scan.output) =>\n            Some(desc, outerList, scan)\n          case _ => None\n        }\n      }\n    }\n\n    plan.transformUp {\n      case ViewPlan(desc, outerList, scan) =>\n       // Produce a scan with the outer list's attribute IDs aliased to the view's name.\n        val newOutput = scan.output.map { oldAttr =>\n          val newId = outerList.collectFirst {\n            case newAttr if conf.resolver(oldAttr.name, newAttr.name) =>\n              newAttr.exprId\n          }.getOrElse {\n            throw DeltaErrors.noNewAttributeId(oldAttr)\n          }\n          oldAttr.withExprId(newId)\n        }\n        SubqueryAlias(desc.qualifiedName, scan.copy(output = newOutput))\n\n      case v: View if v.isTempView =>\n        v.child\n    }\n  }\n\n  def stripTempView(plan: LogicalPlan, conf: SQLConf): LogicalPlan = {\n    plan.transformUp {\n      case v: View if v.isTempView => v.child\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/DomainMetadataUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.actions.{Action, DomainMetadata, Protocol}\nimport org.apache.spark.sql.delta.clustering.ClusteringMetadataDomain\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\n/**\n * Domain metadata utility functions.\n */\ntrait DomainMetadataUtilsBase extends DeltaLogging {\n  // List of metadata domains that will be removed for the REPLACE TABLE operation.\n  protected val METADATA_DOMAINS_TO_REMOVE_FOR_REPLACE_TABLE: Set[String] = Set(\n    ClusteringMetadataDomain.domainName)\n\n  // List of metadata domains that will be copied from the table we are restoring to.\n  // Note that ClusteringMetadataDomain are recreated in handleDomainMetadataForRestoreTable\n  // instead of being blindly copied over.\n  protected val METADATA_DOMAIN_TO_COPY_FOR_RESTORE_TABLE: Set[String] = Set.empty\n\n  // List of metadata domains that will be copied from the table on a CLONE operation.\n  protected val METADATA_DOMAIN_TO_COPY_FOR_CLONE_TABLE: Set[String] = Set(\n    ClusteringMetadataDomain.domainName)\n\n  /**\n   * Returns whether the protocol version supports the [[DomainMetadata]] action.\n   */\n  def domainMetadataSupported(protocol: Protocol): Boolean =\n    protocol.isFeatureSupported(DomainMetadataTableFeature)\n\n  /**\n   * Given a list of [[Action]]s, build a domain name to [[DomainMetadata]] map.\n   * Note duplicated domain name is not expected otherwise an internal error is thrown.\n   */\n  def extractDomainMetadatasMap(actions: Seq[Action]): Map[String, DomainMetadata] = {\n    actions\n      .collect { case action: DomainMetadata => action }\n      .groupBy(_.domain)\n      .map { case (name, domains) =>\n        if (domains.length != 1) {\n          throw DeltaErrors.domainMetadataDuplicate(domains.head.domain)\n        }\n        name -> domains.head\n      }\n  }\n\n  /**\n   * Validate there are no two [[DomainMetadata]] actions with the same domain name. An internal\n   * exception is thrown if any duplicated domains are detected.\n   *\n   * @param actions: Actions the current transaction wants to commit.\n   */\n  def validateDomainMetadataSupportedAndNoDuplicate(\n      actions: Seq[Action], protocol: Protocol): Seq[DomainMetadata] = {\n    val domainMetadatas = extractDomainMetadatasMap(actions)\n    if (domainMetadatas.nonEmpty && !domainMetadataSupported(protocol)) {\n      throw DeltaErrors.domainMetadataTableFeatureNotSupported(\n        domainMetadatas.map(_._2.domain).mkString(\"[\", \",\", \"]\"))\n    }\n    domainMetadatas.values.toSeq\n  }\n\n  /**\n   * Generates a new sequence of DomainMetadata to commits for REPLACE TABLE.\n   *  - By default, existing metadata domains survive as long as they don't appear in the\n   *    new metadata domains, in which case new metadata domains overwrite the existing ones.\n   *  - Existing domains will be removed only if they appear in the pre-defined\n   *    \"removal\" list (e.g., table features require some specific domains to be removed).\n   */\n  def handleDomainMetadataForReplaceTable(\n      existingDomainMetadatas: Seq[DomainMetadata],\n      newDomainMetadatas: Seq[DomainMetadata]): Seq[DomainMetadata] = {\n    val newDomainNames = newDomainMetadatas.map(_.domain).toSet\n    existingDomainMetadatas\n      // Filter out metadata domains unless they are in the list to be removed\n      // and they don't appear in the new metadata domains.\n      .filter(m => !newDomainNames.contains(m.domain) &&\n        METADATA_DOMAINS_TO_REMOVE_FOR_REPLACE_TABLE.contains(m.domain))\n      .map(_.copy(removed = true)) ++ newDomainMetadatas\n  }\n\n  /**\n   * Generates a new sequence of DomainMetadata to commits for RESTORE TABLE.\n   *  - Domains in the toSnapshot will be copied if they appear in the pre-defined\n   *    \"copy\" list (e.g., table features require some specific domains to be copied).\n   *  - All other domains not in the list are dropped from the \"toSnapshot\".\n   *\n   * For clustering metadata domain, it overwrites the existing domain metadata in the\n   * fromSnapshot with the following clustering columns.\n   * 1. If toSnapshot is not a clustered table or missing domain metadata, use empty clustering\n   *    columns.\n   * 2. If toSnapshot is a clustered table, use the clustering columns from toSnapshot.\n   *\n   * @param toSnapshot    The snapshot being restored to, which is referred as \"source\" table.\n   * @param fromSnapshot  The snapshot being restored from, which is the current state.\n   */\n  def handleDomainMetadataForRestoreTable(\n      toSnapshot: Snapshot,\n      fromSnapshot: Snapshot): Seq[DomainMetadata] = {\n    val filteredDomainMetadata = toSnapshot.domainMetadata.filter { m =>\n      METADATA_DOMAIN_TO_COPY_FOR_RESTORE_TABLE.contains(m.domain)\n    }\n    val clusteringColumnsToRestore = ClusteredTableUtils.getClusteringColumnsOptional(toSnapshot)\n\n    val isRestoringToClusteredTable =\n      ClusteredTableUtils.isSupported(toSnapshot.protocol) && clusteringColumnsToRestore.nonEmpty\n    val clusteringColumns = if (isRestoringToClusteredTable) {\n      // We overwrite the clustering columns in the fromSnapshot with the clustering columns\n      // in the toSnapshot.\n      clusteringColumnsToRestore.get\n    } else {\n      // toSnapshot is not a clustered table or missing domain metadata, so we write domain\n      // metadata with empty clustering columns.\n      Seq.empty\n    }\n\n    val matchingMetadataDomain =\n      ClusteredTableUtils.getMatchingMetadataDomain(\n        clusteringColumns,\n        fromSnapshot.domainMetadata)\n\n    // RESTORE table is effectively replacing the current table state (`fromSnapshot`) with a\n    // previous snapshot (`toSnapshot`). Like for REPLACE table, this means any DomainMetadata in\n    // the previous `fromSnapshot` without an equivalent domain in the `fromSnapshot` must be marked\n    // as removed.\n    handleDomainMetadataForReplaceTable(\n      toSnapshot.domainMetadata,\n      filteredDomainMetadata ++ matchingMetadataDomain.clusteringDomainOpt)\n  }\n\n  /**\n   *  Generates sequence of DomainMetadata to commit for CLONE TABLE command.\n   */\n  def handleDomainMetadataForCloneTable(\n      sourceSnapshot: Snapshot,\n      targetSnapshot: Snapshot): Seq[DomainMetadata] = {\n    val newDomainMetadata = sourceSnapshot.domainMetadata.filter { m =>\n      METADATA_DOMAIN_TO_COPY_FOR_CLONE_TABLE.contains(m.domain)\n    }\n\n    // A CLONE operation may overwrite an existing snapshot (effectively a REPLACE operation).\n    // Handle the removed DomainMetadata accordingly.\n    handleDomainMetadataForReplaceTable(targetSnapshot.domainMetadata, newDomainMetadata)\n  }\n}\n\nobject DomainMetadataUtils extends DomainMetadataUtilsBase\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/FallbackToV1Relations.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\n\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\n\n/**\n * Fall back to V1 nodes, since we don't have a V2 reader for Delta right now\n */\nobject FallbackToV1DeltaRelation {\n  def unapply(dsv2: DataSourceV2Relation): Option[LogicalRelation] = dsv2.table match {\n    case d: DeltaTableV2 if dsv2.getTagValue(DeltaRelation.KEEP_AS_V2_RELATION_TAG).isEmpty =>\n      Some(DeltaRelation.fromV2Relation(d, dsv2, dsv2.options))\n    case _ => None\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/FileMetadataMaterializationTracker.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.Semaphore\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.spark.sql.delta.FileMetadataMaterializationTracker.TaskLevelPermitAllocator\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.SparkSession\n\n/**\n * An instance of this class tracks and controls the materialization usage of a single command\n * query (e.g. Backfill) with respect to the driver limits. Each query must use one instance of the\n * FileMaterializationTracker.\n *\n * tasks - tasks are the basic unit of computation.\n * For example, in Backfill, each task bins multiple files into batches to be executed.\n *\n * A task has to be materialized in its entirety, so in the case where we are unable to acquire\n * permits to materialize a task we acquire an over allocation lock that will allow tasks to\n * complete materializing. Over allocation is only allowed for one thread at once in the driver.\n * This allows us to restrict the amount of file metadata being materialized at once on the driver.\n *\n * Accessed by the thread materializing files and by the thread releasing resources after execution.\n *\n */\nclass FileMetadataMaterializationTracker extends Logging {\n\n  /** The number of permits allocated from the global file materialization semaphore */\n  @volatile private var numPermitsFromSemaphore: Int = 0\n\n  /** The number of permits over allocated by holding the overAllocationLock */\n  @volatile private var numOverAllocatedPermits: Int = 0\n\n  private val materializationMetrics = new FileMetadataMaterializationMetrics()\n\n  /**\n   * @return The collected materialization metrics for this query.\n   */\n  def getMetrics(): FileMetadataMaterializationMetrics = {\n    materializationMetrics\n  }\n\n  /**\n   * Signals to execute the batch early in the event that we overallocated to\n   * materialize a task.\n   */\n  def executeBatchEarly(): Boolean = {\n    numOverAllocatedPermits > 0\n  }\n\n  /**\n   * A per task permit allocator which allows materializing a new task.\n   * @return - TaskLevelPermitAllocator to be used to materialize a task\n   */\n  def createTaskLevelPermitAllocator(): TaskLevelPermitAllocator = {\n    new TaskLevelPermitAllocator(this)\n  }\n\n  /**\n   * Acquire a permit from the materialization semaphore, if there is no permit available the thread\n   * acquires the overAllocationLock which allows it to freely acquire permits in the future.\n   * Only one thread can over allocate at once.\n   *\n   * @param isNewTask - indicates whether the permit is being acquired for a new task, this will\n   *                 allow us to prevent overallocation to spill over to new tasks.\n   */\n  private def acquirePermit(isNewTask: Boolean = false): Unit = {\n    var hasAcquiredPermit = false\n    if (isNewTask) {\n      FileMetadataMaterializationTracker.materializationSemaphore.acquire(1)\n      hasAcquiredPermit = true\n    } else if (numOverAllocatedPermits > 0) {\n      materializationMetrics.overAllocFilesMaterializedCount += 1\n    } else if (!FileMetadataMaterializationTracker.materializationSemaphore.tryAcquire(1)) {\n      // we acquire the overAllocationLock for this thread\n      logInfo(log\"Acquiring over allocation lock for this query.\")\n      val startTime = System.currentTimeMillis()\n      FileMetadataMaterializationTracker.overAllocationLock.acquire(1)\n      val waitTime = System.currentTimeMillis() - startTime\n      logInfo(log\"Acquired over allocation lock for this query in \" +\n        log\"${MDC(DeltaLogKeys.TIME_MS, waitTime)} ms\")\n      materializationMetrics.overAllocWaitTimeMs += waitTime\n      materializationMetrics.overAllocWaitCount += 1\n      materializationMetrics.overAllocFilesMaterializedCount += 1\n    } else {\n      // tryAcquire was successful\n      hasAcquiredPermit = true\n    }\n    if (hasAcquiredPermit) {\n      this.synchronized {\n        numPermitsFromSemaphore += 1\n      }\n    } else {\n      this.synchronized {\n        numOverAllocatedPermits += 1\n      }\n    }\n    materializeOneFile()\n  }\n\n  /** Increment the number of materialized file in materializationMetrics. */\n  def materializeOneFile(): Unit = materializationMetrics.filesMaterializedCount += 1\n\n  /**\n   * Release `numPermits` file permits and release overAllocationLock lock if held by the thread\n   * and the number of over allocated files is 0.\n   */\n  def releasePermits(numPermits: Int): Unit = {\n    var permitsToRelease = numPermits\n    this.synchronized {\n      if (numOverAllocatedPermits > 0) {\n        val overAllocatedPermitsToRelease = Math.min(numOverAllocatedPermits, numPermits)\n        numOverAllocatedPermits -= overAllocatedPermitsToRelease\n        permitsToRelease -= overAllocatedPermitsToRelease\n        if (numOverAllocatedPermits == 0) {\n          FileMetadataMaterializationTracker.overAllocationLock.release(1)\n          logInfo(log\"Released over allocation lock.\")\n        }\n      }\n      numPermitsFromSemaphore -= permitsToRelease\n    }\n    FileMetadataMaterializationTracker.materializationSemaphore.release(permitsToRelease)\n  }\n\n  /**\n   * This will release all acquired file permits by the tracker.\n   */\n  def releaseAllPermits(): Unit = {\n    this.synchronized {\n      if (numOverAllocatedPermits > 0) {\n        FileMetadataMaterializationTracker.overAllocationLock.release(1)\n      }\n      if (numPermitsFromSemaphore > 0) {\n        FileMetadataMaterializationTracker.materializationSemaphore.release(numPermitsFromSemaphore)\n      }\n      numPermitsFromSemaphore = 0\n      numOverAllocatedPermits = 0\n    }\n  }\n}\n\nobject FileMetadataMaterializationTracker extends DeltaLogging {\n  // Global limit for number of files that can be materialized at once on the driver\n  private val globalFileMaterializationLimit: AtomicInteger = new AtomicInteger(-1)\n\n  // Semaphore to control file materialization\n  private var materializationSemaphore: Semaphore = _\n\n  /**\n   * Global lock that is held by a thread and allows it to materialize files without\n   * acquiring permits the materializationSemaphore.\n   *\n   * This lock is released when the thread completes executing the command's job that\n   * acquired it, or when all permits are released during bin packing.\n   */\n  private val overAllocationLock = new Semaphore(1)\n\n  /**\n   * Initialize the global materialization semaphore using an existing semaphore. This is used\n   * for unit tests.\n   */\n  private[sql] def initializeSemaphoreForTests(semaphore: Semaphore): Unit = {\n    globalFileMaterializationLimit.set(semaphore.availablePermits())\n    materializationSemaphore = semaphore\n  }\n\n  /**\n   * Initialize materialization semaphore if this is the first query running on the cluster that\n   * uses the file materialization tracker.\n   */\n  private def initializeMaterializationSemaphore(spark: SparkSession): Unit = {\n    if (globalFileMaterializationLimit.compareAndSet(-1, spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_COMMAND_FILE_MATERIALIZATION_LIMIT))) {\n      if (globalFileMaterializationLimit.get() > 0) {\n        materializationSemaphore = new Semaphore(globalFileMaterializationLimit.get)\n      }\n    }\n  }\n\n  def withTracker(\n      origTxn: OptimisticTransaction,\n      spark: SparkSession,\n      metricsOpType: String)(f: FileMetadataMaterializationTracker => Unit): Unit = {\n    initializeMaterializationSemaphore(spark)\n    val shouldTrack = spark.conf.get(\n      DeltaSQLConf.DELTA_COMMAND_FILE_MATERIALIZATION_TRACKING_ENABLED)\n    val tracker = if (shouldTrack) {\n      new FileMetadataMaterializationTracker()\n    } else {\n      logInfo(log\"File metadata materialization tracking is disabled for this query.\" +\n        log\" Please set ${MDC(DeltaLogKeys.CONFIG_KEY,\n          DeltaSQLConf.DELTA_COMMAND_FILE_MATERIALIZATION_TRACKING_ENABLED.key)} \" +\n        log\"to true to enable it.\")\n      noopTracker\n    }\n    try {\n      f(tracker)\n      val trackerMetrics = tracker.getMetrics()\n      logInfo(log\"File metadata materialization metrics for the completed query: \" +\n        log\"${MDC(DeltaLogKeys.METRICS, trackerMetrics)}\")\n      recordDeltaEvent(\n        deltaLog = origTxn.deltaLog,\n        opType = metricsOpType,\n        data = trackerMetrics)\n    } finally { tracker.releaseAllPermits()  }\n  }\n\n  /**\n   * @return - return a version of the FileMetadataMaterializationTracker where every operation\n   *         is a noop\n   */\n  val noopTracker:\n      FileMetadataMaterializationTracker = new FileMetadataMaterializationTracker() {\n\n    override def releasePermits(numPermits: Int): Unit = { }\n\n    override def createTaskLevelPermitAllocator() = new TaskLevelPermitAllocator(this) {\n      override def acquirePermit(): Unit = { }\n    }\n\n    override def executeBatchEarly(): Boolean = false\n\n    override def releaseAllPermits(): Unit = { }\n\n    override def getMetrics(): FileMetadataMaterializationMetrics = {\n      new FileMetadataMaterializationMetrics()\n    }\n  }\n\n  /**\n   * A per task level allocator that controls permit allocation and releasing for the task\n   */\n  class TaskLevelPermitAllocator(tracker: FileMetadataMaterializationTracker) {\n\n    /** Indicates whether the file materialization is for a new task */\n    var isNewTask = true\n\n    /**\n     * Acquire a single file materialization permit.\n     */\n    def acquirePermit(): Unit = {\n      if (isNewTask) {\n        logInfo(log\"Acquiring file materialization permits for a new task\")\n      }\n      tracker.acquirePermit(isNewTask = isNewTask)\n      isNewTask = false\n    }\n  }\n}\n\n/**\n * Instance of this class is used for recording metrics of the FileMetadataMaterializationTracker\n */\ncase class FileMetadataMaterializationMetrics(\n  /** Total number of files materialized */\n  var filesMaterializedCount: Long = 0L,\n\n  /** Number of times we wait to acquire the over allocation lock */\n  var overAllocWaitCount: Long = 0L,\n\n  /** Total time waited to acquire the over allocation lock in ms */\n  var overAllocWaitTimeMs: Long = 0L,\n\n  /** Number of files materialized by using over allocation lock */\n  var overAllocFilesMaterializedCount: Long = 0L) {\n\n  override def toString(): String = {\n    s\"Number of files materialized: $filesMaterializedCount, \" +\n      s\"Number of times over-allocated: $overAllocWaitCount, \" +\n      s\"Total time spent waiting to acquire over-allocation lock: $overAllocWaitTimeMs, \" +\n      s\"Files materialized by over allocation: $overAllocFilesMaterializedCount\"\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/GenerateIdentityValues.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport com.databricks.spark.util.MetricDefinitions\nimport com.databricks.spark.util.TagDefinitions.TAG_OP_TYPE\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.{SparkException, TaskContext}\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{Expression, LeafExpression, Nondeterministic}\nimport org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, CodeGenerator, ExprCode, FalseLiteral}\nimport org.apache.spark.sql.catalyst.expressions.codegen.Block._\nimport org.apache.spark.sql.types.{DataType, LongType}\n\n/**\n * Returns the next generated IDENTITY column value based on the underlying\n * [[PartitionIdentityValueGenerator]].\n */\ncase class GenerateIdentityValues(generator: PartitionIdentityValueGenerator)\n  extends LeafExpression with Nondeterministic {\n\n  override protected def initializeInternal(partitionIndex: Int): Unit = {\n    generator.initialize(partitionIndex)\n  }\n\n  override protected def evalInternal(input: InternalRow): Long = generator.next()\n\n  override def nullable: Boolean = false\n\n  /**\n   * Returns Java source code that can be compiled to evaluate this expression.\n   * The default behavior is to call the eval method of the expression. Concrete expression\n   * implementations should override this to do actual code generation.\n   *\n   * @param ctx a [[CodegenContext]]\n   * @param ev  an [[ExprCode]] with unique terms.\n   * @return an [[ExprCode]] containing the Java source code to generate the given expression\n   */\n  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {\n    val generatorTerm = ctx.addReferenceObj(\"generator\", generator,\n      classOf[PartitionIdentityValueGenerator].getName)\n\n    ctx.addPartitionInitializationStatement(s\"$generatorTerm.initialize(partitionIndex);\")\n    ev.copy(code = code\"\"\"\n        final ${CodeGenerator.javaType(dataType)} ${ev.value} = $generatorTerm.next();\n        \"\"\", isNull = FalseLiteral)\n  }\n\n  /**\n   * Returns the [[DataType]] of the result of evaluating this expression.  It is\n   * invalid to query the dataType of an unresolved expression (i.e., when `resolved` == false).\n   */\n  override def dataType: DataType = LongType\n}\n\nobject GenerateIdentityValues {\n  def apply(start: Long, step: Long, highWaterMarkOpt: Option[Long]): GenerateIdentityValues = {\n    new GenerateIdentityValues(PartitionIdentityValueGenerator(start, step, highWaterMarkOpt))\n  }\n}\n\n/**\n * Generator of IDENTITY value for one partition.\n *\n * @param start The configured start value for the identity column.\n * @param highWaterMarkOpt The optional high watermark for the identity value generation. If this is\n *                      None, that means that no identity values has been generated in the past and\n *                      we should start the identity value generation from the `start`.\n * @param step IDENTITY value increment.\n */\ncase class PartitionIdentityValueGenerator(\n    start: Long,\n    step: Long,\n    highWaterMarkOpt: Option[Long]) {\n\n  require(step != 0)\n  // The value generation logic requires high water mark to follow the start and step configuration.\n  highWaterMarkOpt.foreach(highWaterMark => require((highWaterMark - start) % step == 0))\n\n  private lazy val base = highWaterMarkOpt.map(Math.addExact(_, step)).getOrElse(start)\n  private var partitionIndex: Int = -1\n  private var nextValue: Long = -1L\n  private var increment: Long = -1L\n\n\n  def initialize(partitionIndex: Int): Unit = {\n    if (this.partitionIndex < 0) {\n      this.partitionIndex = partitionIndex\n      this.nextValue = try {\n        Math.addExact(base, Math.multiplyExact(partitionIndex, step))\n      } catch {\n        case e: ArithmeticException =>\n          IdentityOverflowLogger.logOverflow()\n          throw e\n      }\n      // Each value is incremented by numPartitions * step from the previous value.\n      this.increment = try {\n        // Total number of partitions. In local execution case where TaskContext is not set, the\n        // task is executed as a single partition.\n        val numPartitions = Option(TaskContext.get()).map(_.numPartitions()).getOrElse(1)\n        Math.multiplyExact(numPartitions, step)\n      } catch {\n        case e: ArithmeticException =>\n          IdentityOverflowLogger.logOverflow()\n          throw e\n      }\n    } else if (this.partitionIndex != partitionIndex) {\n      throw SparkException.internalError(\"Same PartitionIdentityValueGenerator object \" +\n        s\"initialized with two different partitionIndex [oldValue: ${this.partitionIndex}, \" +\n        s\"newValue: $partitionIndex]\")\n\n    }\n  }\n\n  private def assertInitialized(): Unit = if (partitionIndex == -1) {\n    throw SparkException.internalError(\"PartitionIdentityValueGenerator is not initialized.\")\n  }\n\n  // Generate the next IDENTITY value.\n  def next(): Long = {\n    try {\n      assertInitialized()\n      val ret = nextValue\n      nextValue = Math.addExact(nextValue, increment)\n      ret\n    } catch {\n      case e: ArithmeticException =>\n        IdentityOverflowLogger.logOverflow()\n        throw e\n    }\n  }\n}\n\nobject IdentityOverflowLogger extends DeltaLogging {\n  def logOverflow(): Unit = {\n    recordEvent(\n      MetricDefinitions.EVENT_TAHOE,\n      Map(TAG_OP_TYPE -> \"delta.identityColumn.overflow\")\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/GenerateRowIDs.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.catalyst.trees.TreePattern.PLAN_EXPRESSION\nimport org.apache.spark.sql.execution.datasources.{FileFormat, HadoopFsRelation, LogicalRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.types.StructType\n\n/**\n * This rule adds a Project on top of Delta tables that support the Row tracking table feature to\n * provide a default generated Row ID and row commit version for rows that don't have them\n * materialized in the data file.\n */\nobject GenerateRowIDs extends Rule[LogicalPlan] {\n\n  /**\n   * Matcher for a scan on a Delta table that has Row tracking enabled.\n   */\n  private object DeltaScanWithRowTrackingEnabled {\n    def unapply(plan: LogicalPlan): Option[LogicalRelation] = plan match {\n      case scan @ LogicalRelationWithTable(relation: HadoopFsRelation, _) =>\n        relation.fileFormat match {\n          case format: DeltaParquetFileFormat\n            if RowTracking.isEnabled(format.protocol, format.metadata) => Some(scan)\n          case _ => None\n        }\n      case _ => None\n    }\n  }\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.transformUpWithNewOutput {\n    case DeltaScanWithRowTrackingEnabled(\n            scan @ LogicalRelationWithTable(baseRelation: HadoopFsRelation, _)) =>\n      // While Row IDs and commit versions are non-nullable, we'll use the Row ID & commit\n      // version attributes to read the materialized values from now on, which can be null. We make\n      // the materialized Row ID & commit version attributes nullable in the scan here.\n\n      // Update nullability in the scan `metadataOutput` by updating the delta file format.\n      val newFileFormat = baseRelation.fileFormat match {\n        case format: DeltaParquetFileFormat =>\n          format.copy(nullableRowTrackingGeneratedFields = true)\n      }\n      val newBaseRelation = baseRelation.copy(fileFormat = newFileFormat)(baseRelation.sparkSession)\n\n      // Update the output metadata column's data type (now with nullable row tracking fields).\n      val newOutput = scan.output.map {\n        case MetadataAttributeWithLogicalName(metadata, FileFormat.METADATA_NAME) =>\n          metadata.withDataType(newFileFormat.createFileMetadataCol().dataType)\n        case other => other\n      }\n      val newScan = scan.copy(relation = newBaseRelation, output = newOutput)\n      newScan.copyTagsFrom(scan)\n\n      // Add projection with row tracking column expressions.\n      val updatedAttributes = mutable.Buffer.empty[(Attribute, Attribute)]\n      val projectList = newOutput.map {\n        case MetadataAttributeWithLogicalName(metadata, FileFormat.METADATA_NAME) =>\n          val updatedMetadata = metadataWithRowTrackingColumnsProjection(metadata)\n          updatedAttributes += metadata -> updatedMetadata.toAttribute\n          updatedMetadata\n        case other => other\n      }\n      Project(projectList = projectList, child = newScan) -> updatedAttributes.toSeq\n    case o =>\n      val newPlan = o.transformExpressionsWithPruning(_.containsPattern(PLAN_EXPRESSION)) {\n        // Recurse into subquery plans. Similar to how [[transformUpWithSubqueries]] works except\n        // that it allows us to still use [[transformUpWithNewOutput]] on subquery plans to\n        // correctly update references to the metadata attribute when going up the plan.\n        // Get around type erasure by explicitly checking the plan type and removing warning.\n        case planExpression: PlanExpression[LogicalPlan @unchecked]\n          if planExpression.plan.isInstanceOf[LogicalPlan] =>\n            planExpression.withNewPlan(apply(planExpression.plan))\n      }\n      newPlan -> Nil\n  }\n\n  /**\n   * Expression that reads the Row IDs from the materialized Row ID column if the value is\n   * present and returns the default generated Row ID using the file's base Row ID and current row\n   * index if not:\n   *    coalesce(_metadata.row_id, _metadata.base_row_id + _metadata.row_index).\n   */\n  private def rowIdExpr(metadata: AttributeReference): Expression = {\n    Coalesce(Seq(\n      getField(metadata, RowId.ROW_ID),\n      Add(\n        getField(metadata, RowId.BASE_ROW_ID),\n        getField(metadata, ParquetFileFormat.ROW_INDEX))))\n  }\n\n  /**\n   * Expression that reads the Row commit versions from the materialized Row commit version column\n   * if the value is present and returns the default Row commit version from the file if not:\n   *    coalesce(_metadata.row_commit_Version, _metadata.default_row_commit_version).\n   */\n  private def rowCommitVersionExpr(metadata: AttributeReference): Expression = {\n    Coalesce(Seq(\n      getField(metadata, RowCommitVersion.METADATA_STRUCT_FIELD_NAME),\n      getField(metadata, DefaultRowCommitVersion.METADATA_STRUCT_FIELD_NAME)))\n  }\n\n  /**\n   * Extract a field from the metadata column.\n   */\n  private def getField(metadata: AttributeReference, name: String): GetStructField = {\n    ExtractValue(metadata, Literal(name), conf.resolver) match {\n      case field: GetStructField => field\n      case _ =>\n        throw new IllegalStateException(s\"The metadata column '${metadata.name}' is not a struct.\")\n    }\n  }\n\n  /**\n   * Create a new metadata struct where the Row ID and row commit version values are populated using\n   * the materialized values if present, or the default Row ID / row commit version values if not.\n   */\n  private def metadataWithRowTrackingColumnsProjection(\n      metadata: AttributeReference): NamedExpression = {\n    val metadataFields = metadata.dataType.asInstanceOf[StructType].map {\n      case field if field.name == RowId.ROW_ID =>\n        field -> rowIdExpr(metadata)\n      case field if field.name == RowCommitVersion.METADATA_STRUCT_FIELD_NAME =>\n        field -> rowCommitVersionExpr(metadata)\n      case field =>\n        field -> getField(metadata, field.name)\n    }.flatMap { case (oldField, newExpr) =>\n      // Propagate the type metadata from the old fields to the new fields.\n      val newField = Alias(newExpr, oldField.name)(explicitMetadata = Some(oldField.metadata))\n      Seq(Literal(oldField.name), newField)\n    }\n    Alias(CreateNamedStruct(metadataFields), metadata.name)()\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/GeneratedColumn.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.Locale\n\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.files.{TahoeBatchFileIndex, TahoeFileIndex}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils.quoteIdentifier\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.AnalysisHelper\n\nimport org.apache.spark.sql.{AnalysisException, Column, Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.Analyzer\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression\nimport org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke\nimport org.apache.spark.sql.catalyst.optimizer.CollapseProject\nimport org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, LogicalPlan, Project}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.catalyst.util.{quoteIfNeeded, CaseInsensitiveMap, CharVarcharCodegenUtils}\nimport org.apache.spark.sql.execution.SQLExecution\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelation}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.types.{Metadata => FieldMetadata}\n/**\n * Provide utility methods to implement Generated Columns for Delta. Users can use the following\n * SQL syntax to create a table with generated columns.\n *\n * ```\n * CREATE TABLE table_identifier(\n * column_name column_type,\n * column_name column_type GENERATED ALWAYS AS ( generation_expr ),\n * ...\n * )\n * USING delta\n * [ PARTITIONED BY (partition_column_name, ...) ]\n * ```\n *\n * This is an example:\n * ```\n * CREATE TABLE foo(\n * id bigint,\n * type string,\n * subType string GENERATED ALWAYS AS ( SUBSTRING(type FROM 0 FOR 4) ),\n * data string,\n * eventTime timestamp,\n * day date GENERATED ALWAYS AS ( days(eventTime) )\n * USING delta\n * PARTITIONED BY (type, day)\n * ```\n *\n * When writing to a table, for these generated columns:\n * - If the output is missing a generated column, we will add an expression to generate it.\n * - If a generated column exists in the output, in other words, we will add a constraint to ensure\n *   the given value doesn't violate the generation expression.\n */\nobject GeneratedColumn extends DeltaLogging with AnalysisHelper {\n\n  def satisfyGeneratedColumnProtocol(protocol: Protocol): Boolean =\n    protocol.isFeatureSupported(GeneratedColumnsTableFeature)\n\n  /**\n   * Whether the field contains the generation expression. Note: this doesn't mean the column is a\n   * generated column. A column is a generated column only if the table's\n   * `minWriterVersion` >= `GeneratedColumn.MIN_WRITER_VERSION` and the column metadata contains\n   * generation expressions. Use the other `isGeneratedColumn` to check whether it's a generated\n   * column instead.\n   */\n  private[delta] def isGeneratedColumn(field: StructField): Boolean = {\n    field.metadata.contains(GENERATION_EXPRESSION_METADATA_KEY)\n  }\n\n  /** Whether a column is a generated column. */\n  def isGeneratedColumn(protocol: Protocol, field: StructField): Boolean = {\n    satisfyGeneratedColumnProtocol(protocol) && isGeneratedColumn(field)\n  }\n\n  /**\n   * Whether any generation expressions exist in the schema. Note: this doesn't mean the table\n   * contains generated columns. A table has generated columns only if its protocol satisfies\n   * Generated Column (listed in Table Features or supported implicitly) and some of columns in\n   * the table schema contain generation expressions. Use `enforcesGeneratedColumns` to check\n   * generated column tables instead.\n   */\n  def hasGeneratedColumns(schema: StructType): Boolean = {\n    schema.exists(isGeneratedColumn)\n  }\n\n  /**\n   * Returns the generated columns of a table. A column is a generated column requires:\n   * - The table writer protocol >= GeneratedColumn.MIN_WRITER_VERSION;\n   * - It has a generation expression in the column metadata.\n   */\n  def getGeneratedColumns(snapshot: SnapshotDescriptor): Seq[StructField] = {\n    if (satisfyGeneratedColumnProtocol(snapshot.protocol)) {\n      snapshot.metadata.schema.partition(isGeneratedColumn)._1\n    } else {\n      Nil\n    }\n  }\n\n  /**\n   * Whether the table has generated columns. A table has generated columns only if its\n   * protocol satisfies Generated Column (listed in Table Features or supported implicitly) and\n   * some of columns in the table schema contain generation expressions.\n   *\n   * As Spark will propagate column metadata storing the generation expression through\n   * the entire plan, old versions that don't support generated columns may create tables whose\n   * schema contain generation expressions. However, since these old versions has a lower writer\n   * version, we can use the table's `minWriterVersion` to identify such tables and treat them as\n   * normal tables.\n   *\n   * @param protocol the table protocol.\n   * @param metadata the table metadata.\n   */\n  def enforcesGeneratedColumns(protocol: Protocol, metadata: Metadata): Boolean = {\n    satisfyGeneratedColumnProtocol(protocol) && metadata.schema.exists(isGeneratedColumn)\n  }\n\n  /** Return the generation expression from a field metadata if any. */\n  def getGenerationExpressionStr(metadata: FieldMetadata): Option[String] = {\n    if (metadata.contains(GENERATION_EXPRESSION_METADATA_KEY)) {\n      Some(metadata.getString(GENERATION_EXPRESSION_METADATA_KEY))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Return the generation expression from a field if any. This method doesn't check the protocl.\n   * The caller should make sure the table writer protocol meets `satisfyGeneratedColumnProtocol`\n   * before calling method.\n   */\n  def getGenerationExpression(field: StructField): Option[Expression] = {\n    getGenerationExpressionStr(field.metadata).map { exprStr =>\n      parseGenerationExpression(SparkSession.active, exprStr)\n    }\n  }\n\n  /** Return the generation expression from a field if any. */\n  private def getGenerationExpressionStr(field: StructField): Option[String] = {\n    getGenerationExpressionStr(field.metadata)\n  }\n\n  /** Parse a generation expression string and convert it to an [[Expression]] object. */\n  private def parseGenerationExpression(spark: SparkSession, exprString: String): Expression = {\n    spark.sessionState.sqlParser.parseExpression(exprString)\n  }\n\n  /**\n   * SPARK-27561 added support for lateral column alias. This means generation expressions that\n   * reference other generated columns no longer fail analysis in `validateGeneratedColumns`.\n   *\n   * This method checks for and throws an error if:\n   * - A generated column references itself\n   * - A generated column references another generated column\n   */\n  def validateColumnReferences(\n      spark: SparkSession,\n      fieldName: String,\n      expression: Expression,\n      schema: StructType): Unit = {\n    val allowedBaseColumns = schema\n      .filterNot(_.name == fieldName) // Can't reference itself\n      .filterNot(isGeneratedColumn) // Can't reference other generated columns\n    val relation = new LocalRelation(toAttributes(StructType(allowedBaseColumns)))\n    try {\n      val analyzer: Analyzer = spark.sessionState.analyzer\n      val analyzed = analyzer.execute(Project(Seq(Alias(expression, fieldName)()), relation))\n      analyzer.checkAnalysis(analyzed)\n    } catch {\n      case ex: AnalysisException =>\n        // Improve error message if possible\n        if (ex.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\") {\n          throw DeltaErrors.generatedColumnsReferToWrongColumns(ex)\n        }\n        throw ex\n    }\n  }\n\n  /**\n   * If the schema contains generated columns, check the following unsupported cases:\n   * - Refer to a non-existent column or another generated column.\n   * - Use an unsupported expression.\n   * - The expression type is not the same as the column type.\n   */\n  def validateGeneratedColumns(spark: SparkSession, schema: StructType): Unit = {\n    val (generatedColumns, normalColumns) = schema.partition(isGeneratedColumn)\n    generatedColumns.foreach { c =>\n      // Generated columns cannot be variant types because the writer must be able to enforce that\n      // the <variant value> <=> <generated expression>. Variants are currently not comprable so\n      // this condition is impossible to enforce.\n      if (c.dataType.isInstanceOf[VariantType]) {\n        throw DeltaErrors.generatedColumnsUnsupportedType(c.dataType)\n      }\n    }\n\n    // Create a fake relation using the normal columns and add a project with generation expressions\n    // on top of it to ask Spark to analyze the plan. This will help us find out the following\n    // errors:\n    // - Refer to a non existent column in a generation expression.\n    // - Refer to a generated column in another one.\n    val relation = new LocalRelation(toAttributes(StructType(normalColumns)))\n    val selectExprs = generatedColumns.map { f =>\n      getGenerationExpressionStr(f) match {\n        case Some(exprString) =>\n          val expr = parseGenerationExpression(spark, exprString)\n          validateColumnReferences(spark, f.name, expr, schema)\n          Column(expr).alias(f.name)\n        case None =>\n          // Should not happen\n          throw DeltaErrors.expressionsNotFoundInGeneratedColumn(f.name)\n      }\n    }\n    val dfWithExprs = try {\n      val plan = Project(selectExprs.map(_.expr.asInstanceOf[NamedExpression]), relation)\n      DataFrameUtils.ofRows(spark, plan)\n    } catch {\n      case e: AnalysisException if e.getMessage != null =>\n        val regexCandidates = Seq(\n          (\"A column, variable, or function parameter with name .*?cannot be resolved. \" +\n            \"Did you mean one of the following?.*?\").r,\n          \"cannot resolve.*?given input columns:.*?\".r,\n          \"Column.*?does not exist.\".r\n        )\n        if (regexCandidates.exists(_.findFirstMatchIn(e.getMessage).isDefined)) {\n          throw DeltaErrors.generatedColumnsReferToWrongColumns(e)\n        } else {\n          throw e\n        }\n    }\n\n    // Check whether the generation expressions are valid\n    dfWithExprs.queryExecution.analyzed.transformAllExpressions {\n      case expr: Alias =>\n        // Alias will be non deterministic if it points to a non deterministic expression.\n        // Skip `Alias` to provide a better error for a non deterministic expression.\n        expr\n      case expr @ (_: GetStructField | _: GetArrayItem) =>\n        // The complex type extractors don't have a function name, so we need to check them\n        // separately. `GetMapValue` and `GetArrayStructFields` are not supported because Delta\n        // Invariant Check doesn't support them.\n        expr\n      case expr: UserDefinedExpression =>\n        throw DeltaErrors.generatedColumnsUDF(expr)\n      case expr if !expr.deterministic =>\n        throw DeltaErrors.generatedColumnsNonDeterministicExpression(expr)\n      case expr if expr.isInstanceOf[AggregateExpression] =>\n        throw DeltaErrors.generatedColumnsAggregateExpression(expr)\n      case expr if !AllowedUserProvidedExpressions.expressions.contains(expr.getClass) =>\n        throw DeltaErrors.generatedColumnsUnsupportedExpression(expr)\n    }\n    // Compare the columns types defined in the schema and the expression types.\n    generatedColumns.zip(dfWithExprs.schema).foreach { case (column, expr) =>\n      if (!DataType.equalsIgnoreNullability(column.dataType, expr.dataType)) {\n        throw DeltaErrors.generatedColumnsExprTypeMismatch(\n          column.name, column.dataType, expr.dataType)\n      }\n    }\n  }\n\n  def getGeneratedColumnsAndColumnsUsedByGeneratedColumns(schema: StructType): Set[String] = {\n    val generationExprs = schema.flatMap { col =>\n      getGenerationExpressionStr(col).map { exprStr =>\n        val expr = parseGenerationExpression(SparkSession.active, exprStr)\n        Column(expr).alias(col.name)\n      }\n    }\n    if (generationExprs.isEmpty) {\n      return Set.empty\n    }\n\n    val df = DataFrameUtils.ofRows(SparkSession.active, new LocalRelation(toAttributes(schema)))\n    val generatedColumnsAndColumnsUsedByGeneratedColumns =\n      df.select(generationExprs: _*).queryExecution.analyzed match {\n        case Project(exprs, _) =>\n          exprs.flatMap {\n            case Alias(expr, column) =>\n              expr.references.map {\n                case a: AttributeReference => a.name\n                case other =>\n                  // Should not happen since the columns should be resolved\n                  throw DeltaErrors.unexpectedAttributeReference(s\"$other\")\n              }.toSeq :+ column\n            case other =>\n              // Should not happen since we use `Alias` expressions.\n              throw DeltaErrors.unexpectedAlias(s\"$other\")\n          }\n        case other =>\n          // Should not happen since `select` should use `Project`.\n          throw DeltaErrors.unexpectedProject(other.toString())\n      }\n    // Converting columns to lower case is fine since Delta's schema is always case insensitive.\n    generatedColumnsAndColumnsUsedByGeneratedColumns.map(_.toLowerCase(Locale.ROOT)).toSet\n  }\n\n  private def createFieldPath(nameParts: Seq[String]): String = {\n    nameParts.map(quoteIfNeeded _).mkString(\".\")\n  }\n\n  /**\n   * Try to get `OptimizablePartitionExpression`s of a data column when a partition column is\n   * defined as a generated column and refers to this data column.\n   *\n   * @param schema the table schema\n   * @param partitionSchema the partition schema. If a partition column is defined as a generated\n   *                        column, its column metadata should contain the generation expression.\n   */\n  def getOptimizablePartitionExpressions(\n      schema: StructType,\n      partitionSchema: StructType): Map[String, Seq[OptimizablePartitionExpression]] = {\n    val partitionGenerationExprs = partitionSchema.flatMap { col =>\n      getGenerationExpressionStr(col).map { exprStr =>\n        val expr = parseGenerationExpression(SparkSession.active, exprStr)\n        Column(expr).alias(col.name)\n      }\n    }\n    if (partitionGenerationExprs.isEmpty) {\n      return Map.empty\n    }\n\n    val spark = SparkSession.active\n    val resolver = spark.sessionState.analyzer.resolver\n\n    // `a.name` comes from the generation expressions which users may use different cases. We\n    // need to normalize it to the same case so that we can group expressions for the same\n    // column name together.\n    val nameNormalizer: String => String =\n      if (spark.sessionState.conf.caseSensitiveAnalysis) x => x else _.toLowerCase(Locale.ROOT)\n\n    /**\n     * Returns a normalized column name with its `OptimizablePartitionExpression`\n     */\n    def createExpr(nameParts: Seq[String])(func: => OptimizablePartitionExpression):\n      Option[(String, OptimizablePartitionExpression)] = {\n      if (schema.findNestedField(nameParts, resolver = resolver).isDefined) {\n        Some(nameNormalizer(createFieldPath(nameParts)) -> func)\n      } else {\n        None\n      }\n    }\n\n    val df = DataFrameUtils.ofRows(SparkSession.active, new LocalRelation(toAttributes(schema)))\n    val extractedPartitionExprs =\n      df.select(partitionGenerationExprs: _*).queryExecution.analyzed match {\n        case Project(exprs, _) =>\n          exprs.flatMap {\n            case Alias(expr, partColName) =>\n              expr match {\n                case Cast(ExtractBaseColumn(name, TimestampType), DateType, _, _) =>\n                  createExpr(name)(DatePartitionExpr(partColName))\n                case Cast(ExtractBaseColumn(name, DateType), DateType, _, _) =>\n                  createExpr(name)(DatePartitionExpr(partColName))\n                case Year(ExtractBaseColumn(name, DateType)) =>\n                  createExpr(name)(YearPartitionExpr(partColName))\n                case Year(Cast(ExtractBaseColumn(name, TimestampType), DateType, _, _)) =>\n                  createExpr(name)(YearPartitionExpr(partColName))\n                case Year(Cast(ExtractBaseColumn(name, DateType), DateType, _, _)) =>\n                  createExpr(name)(YearPartitionExpr(partColName))\n                case Month(Cast(ExtractBaseColumn(name, TimestampType), DateType, _, _)) =>\n                  createExpr(name)(MonthPartitionExpr(partColName))\n                case DateFormatClass(\n                  Cast(ExtractBaseColumn(name, DateType), TimestampType, _, _),\n                      StringLiteral(format), _) =>\n                    format match {\n                      case DATE_FORMAT_YEAR_MONTH =>\n                        createExpr(name)(\n                          DateFormatPartitionExpr(partColName, DATE_FORMAT_YEAR_MONTH))\n                      case _ => None\n                    }\n                case DateFormatClass(ExtractBaseColumn(name, TimestampType),\n                    StringLiteral(format), _) =>\n                  format match {\n                    case DATE_FORMAT_YEAR_MONTH =>\n                      createExpr(name)(\n                        DateFormatPartitionExpr(partColName, DATE_FORMAT_YEAR_MONTH))\n                    case DATE_FORMAT_YEAR_MONTH_DAY =>\n                      createExpr(name)(\n                        DateFormatPartitionExpr(partColName, DATE_FORMAT_YEAR_MONTH_DAY))\n                    case DATE_FORMAT_YEAR_MONTH_DAY_HOUR =>\n                      createExpr(name)(\n                        DateFormatPartitionExpr(partColName, DATE_FORMAT_YEAR_MONTH_DAY_HOUR))\n                    case _ => None\n                  }\n                case DayOfMonth(Cast(ExtractBaseColumn(name, TimestampType),\n                    DateType, _, _)) =>\n                  createExpr(name)(DayPartitionExpr(partColName))\n                case Hour(ExtractBaseColumn(name, TimestampType), _) =>\n                  createExpr(name)(HourPartitionExpr(partColName))\n                case Substring(ExtractBaseColumn(name, StringType), IntegerLiteral(pos),\n                    IntegerLiteral(len)) =>\n                  createExpr(name)(SubstringPartitionExpr(partColName, pos, len))\n                case TruncTimestamp(\n                  StringLiteral(format), ExtractBaseColumn(name, TimestampType), _) =>\n                    createExpr(name)(TimestampTruncPartitionExpr(format, partColName))\n                case TruncTimestamp(\n                  StringLiteral(format),\n                  Cast(ExtractBaseColumn(name, DateType), TimestampType, _, _), _) =>\n                    createExpr(name)(TimestampTruncPartitionExpr(format, partColName))\n                case ExtractBaseColumn(name, _) =>\n                  createExpr(name)(IdentityPartitionExpr(partColName))\n                case TruncDate(ExtractBaseColumn(name, DateType), StringLiteral(format)) =>\n                  createExpr(name)(TruncDatePartitionExpr(partColName,\n                    format))\n                case TruncDate(Cast(\n                ExtractBaseColumn(name, TimestampType | StringType), DateType, _, _),\n                StringLiteral(format)) =>\n                  createExpr(name)(TruncDatePartitionExpr(partColName,\n                    format))\n                case _ => None\n              }\n            case other =>\n              // Should not happen since we use `Alias` expressions.\n              throw DeltaErrors.unexpectedAlias(s\"$other\")\n          }\n        case other =>\n          // Should not happen since `select` should use `Project`.\n          throw DeltaErrors.unexpectedProject(other.toString())\n      }\n    extractedPartitionExprs.groupBy(_._1).map { case (name, group) =>\n      val groupedExprs = group.map(_._2)\n      val mergedExprs = mergePartitionExpressionsIfPossible(groupedExprs)\n      if (log.isDebugEnabled) {\n        logDebug(s\"Optimizable partition expressions for column $name:\")\n        mergedExprs.foreach(expr => logDebug(expr.toString))\n      }\n      name -> mergedExprs\n    }\n  }\n\n  /**\n   * Merge multiple partition expressions into one if possible. For example, users may define\n   * three partitions columns, `year`, `month` and `day`, rather than defining a single `date`\n   * partition column. Hence, we need to take the multiple partition columns into a single\n   * part to consider when optimizing queries.\n   */\n  private def mergePartitionExpressionsIfPossible(\n      exprs: Seq[OptimizablePartitionExpression]): Seq[OptimizablePartitionExpression] = {\n    def isRedundantPartitionExpr(f: OptimizablePartitionExpression): Boolean = {\n      f.isInstanceOf[YearPartitionExpr] ||\n        f.isInstanceOf[MonthPartitionExpr] ||\n        f.isInstanceOf[DayPartitionExpr] ||\n        f.isInstanceOf[HourPartitionExpr]\n    }\n\n    // Take the first option because it's safe to drop other duplicate partition expressions\n    val year = exprs.collect { case y: YearPartitionExpr => y }.headOption\n    val month = exprs.collect { case m: MonthPartitionExpr => m }.headOption\n    val day = exprs.collect { case d: DayPartitionExpr => d }.headOption\n    val hour = exprs.collect { case h: HourPartitionExpr => h }.headOption\n    (year ++ month ++ day ++ hour) match {\n      case Seq(\n          year: YearPartitionExpr,\n          month: MonthPartitionExpr,\n          day: DayPartitionExpr,\n          hour: HourPartitionExpr) =>\n        exprs.filterNot(isRedundantPartitionExpr) :+\n          YearMonthDayHourPartitionExpr(year.yearPart, month.monthPart, day.dayPart, hour.hourPart)\n      case Seq(year: YearPartitionExpr, month: MonthPartitionExpr, day: DayPartitionExpr) =>\n        exprs.filterNot(isRedundantPartitionExpr) :+\n          YearMonthDayPartitionExpr(year.yearPart, month.monthPart, day.dayPart)\n      case Seq(year: YearPartitionExpr, month: MonthPartitionExpr) =>\n        exprs.filterNot(isRedundantPartitionExpr) :+\n          YearMonthPartitionExpr(year.yearPart, month.monthPart)\n      case _ =>\n        exprs\n    }\n  }\n\n  def partitionFilterOptimizationEnabled(spark: SparkSession): Boolean = {\n    spark.sessionState.conf\n      .getConf(DeltaSQLConf.GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED)\n  }\n\n\n  /**\n   * Try to generate partition filters from data filters if possible.\n   *\n   * @param delta the logical plan that outputs the same attributes as the table schema. This will\n   *              be used to resolve auto generated expressions.\n   */\n  def generatePartitionFilters(\n      spark: SparkSession,\n      snapshot: SnapshotDescriptor,\n      dataFilters: Seq[Expression],\n      delta: LogicalPlan): Seq[Expression] = {\n    if (!satisfyGeneratedColumnProtocol(snapshot.protocol)) {\n      return Nil\n    }\n    if (snapshot.metadata.optimizablePartitionExpressions.isEmpty) {\n      return Nil\n    }\n\n    val optimizablePartitionExpressions =\n      if (spark.sessionState.conf.caseSensitiveAnalysis) {\n        snapshot.metadata.optimizablePartitionExpressions\n      } else {\n        CaseInsensitiveMap(snapshot.metadata.optimizablePartitionExpressions)\n      }\n\n    /**\n     * Preprocess the data filter such as reordering to ensure the column name appears on the left\n     * and the literal appears on the right.\n     */\n    def preprocess(filter: Expression): Expression = filter match {\n      case LessThan(lit: Literal, e: Expression) =>\n        GreaterThan(e, lit)\n      case LessThanOrEqual(lit: Literal, e: Expression) =>\n        GreaterThanOrEqual(e, lit)\n      case EqualTo(lit: Literal, e: Expression) =>\n        EqualTo(e, lit)\n      case GreaterThan(lit: Literal, e: Expression) =>\n        LessThan(e, lit)\n      case GreaterThanOrEqual(lit: Literal, e: Expression) =>\n        LessThanOrEqual(e, lit)\n      case e => e\n    }\n\n    /**\n     * Find the `OptimizablePartitionExpression`s of column `a` and apply them to get the partition\n     * filters.\n     */\n    def toPartitionFilter(\n        nameParts: Seq[String],\n        func: (OptimizablePartitionExpression) => Option[Expression]): Seq[Expression] = {\n      optimizablePartitionExpressions.get(createFieldPath(nameParts)).toSeq.flatMap { exprs =>\n        exprs.flatMap(expr => func(expr))\n      }\n    }\n\n    val partitionFilters = dataFilters.flatMap { filter =>\n      preprocess(filter) match {\n        case LessThan(ExtractBaseColumn(nameParts, _), lit: Literal) =>\n          toPartitionFilter(nameParts, _.lessThan(lit))\n        case LessThanOrEqual(ExtractBaseColumn(nameParts, _), lit: Literal) =>\n          toPartitionFilter(nameParts, _.lessThanOrEqual(lit))\n        case EqualTo(ExtractBaseColumn(nameParts, _), lit: Literal) =>\n          toPartitionFilter(nameParts, _.equalTo(lit))\n        case GreaterThan(ExtractBaseColumn(nameParts, _), lit: Literal) =>\n          toPartitionFilter(nameParts, _.greaterThan(lit))\n        case GreaterThanOrEqual(ExtractBaseColumn(nameParts, _), lit: Literal) =>\n          toPartitionFilter(nameParts, _.greaterThanOrEqual(lit))\n        case IsNull(ExtractBaseColumn(nameParts, _)) =>\n          toPartitionFilter(nameParts, _.isNull())\n        case _ => Nil\n      }\n    }\n\n    val resolvedPartitionFilters = resolveReferencesForExpressions(spark, partitionFilters, delta)\n\n    if (log.isDebugEnabled) {\n      logDebug(\"User provided data filters:\")\n      dataFilters.foreach(f => logDebug(f.sql))\n      logDebug(\"Auto generated partition filters:\")\n      partitionFilters.foreach(f => logDebug(f.sql))\n      logDebug(\"Resolved generated partition filters:\")\n      resolvedPartitionFilters.foreach(f => logDebug(f.sql))\n    }\n\n    val executionId = Option(spark.sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY))\n      .getOrElse(\"unknown\")\n    recordDeltaEvent(\n      snapshot.deltaLog,\n      \"delta.generatedColumns.optimize\",\n      data = Map(\n        \"executionId\" -> executionId,\n        \"triggered\" -> resolvedPartitionFilters.nonEmpty\n      ))\n\n    resolvedPartitionFilters\n  }\n\n  /**\n   * Check whether executing DML (Merge or Update) with this plan as the target plan and\n   * generated column is allowed.\n   * It is already checked by the caller that the table is a Delta table, and that it has\n   * generated columns, so it is not checked again here.\n   *\n   * In general it is allowed to Merge or Update into a temporary view over a Delta table, but\n   * this is not allowed if the table contains a generated column. This is because the generated\n   * column definition is a SQL expression text, and it would not handle any transformation done by\n   * the view (e.g. if the view was `SELECT a as b, b as a FROM table`, the generated column would\n   * not handle the aliasing).\n   *\n   * This function checks if the target plan is a bare reference to the Delta table, or if the\n   * transformations are the result of internal processing introduced not by the user, but\n   * internally during analysis, which need to be taken into account and allowed.\n   *\n   * @param deltaLogicalPlan Target plan of the DML (Merge or Update)\n   * @param conf SQLConf object.\n   * @return true if allowed,\n   *         false if DeltaErrors.operationOnTempViewWithGenerateColsNotSupported should be thrown.\n   */\n  def allowDMLTargetPlan(deltaLogicalPlan: LogicalPlan, conf: SQLConf): Boolean = {\n    // Simple quick path: pure scan.\n    // It is already checked by PreprocessTable{Merge|Update} that this is a Delta scan.\n    deltaLogicalPlan.isInstanceOf[LogicalRelation] || (\n      CollapseProject(deltaLogicalPlan) match {\n        case Project(projectList, r: LogicalRelation) if conf.readSideCharPadding =>\n          // Check if s is a char padding applied to a.\n          def isCharPadding(s: StaticInvoke, a: Attribute): Boolean = {\n            s.staticObject == classOf[CharVarcharCodegenUtils] &&\n            s.functionName == \"readSidePadding\" &&\n            s.arguments.size == 2 &&\n            (s.arguments(0) match {\n              case arg: Attribute => arg.exprId == a.exprId\n              case _ => false\n            })\n          }\n\n          projectList.length == r.output.length &&\n          projectList.zip(r.output).forall {\n            // Attribute forwarding.\n            case (p: Attribute, a: Attribute) if p.exprId == a.exprId => true\n            // See Spark's ApplyCharTypePaddingHelper.readSidePadding which applies this projection.\n            // p alias must have the same name as input attribute a,\n            // and be char padding applied to it.\n            case (p: Alias, a: Attribute) if conf.resolver(p.name, a.name) =>\n              p.child match {\n                case s: StaticInvoke if isCharPadding(s, a) => true\n                case _ => false\n              }\n            case _ => false\n          }\n\n        // Pure scan.\n        // It is already checked by PreprocessTable{Merge|Update} that this is a Delta scan.\n        case _: LogicalRelation =>\n          true\n\n        case _ => false\n      }\n    )\n  }\n\n  private val DATE_FORMAT_YEAR_MONTH = \"yyyy-MM\"\n  private val DATE_FORMAT_YEAR_MONTH_DAY = \"yyyy-MM-dd\"\n  private val DATE_FORMAT_YEAR_MONTH_DAY_HOUR = \"yyyy-MM-dd-HH\"\n}\n\n/**\n * Finds the full dot-separated path to a field and the data type of the field. This unifies\n * handling of nested and non-nested fields, and allows pattern matching on the data type.\n */\nobject ExtractBaseColumn {\n  def unapply(e: Expression): Option[(Seq[String], DataType)] = e match {\n    case AttributeReference(name, dataType, _, _) =>\n      Some(Seq(name), dataType)\n    case g: GetStructField => g.child match {\n      case ExtractBaseColumn(nameParts, _) =>\n        Some(nameParts :+ g.extractFieldName, g.dataType)\n      case _ => None\n    }\n    case _ => None\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/IcebergCompat.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaConfigs._\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.types._\n\n/**\n * Utils to validate the IcebergCompatV1 table feature, which is responsible for keeping Delta\n * tables in valid states (see the Delta spec for full invariants, dependencies, and requirements)\n * so that they are capable of having Delta to Iceberg metadata conversion applied to them. The\n * IcebergCompatV1 table feature does not implement, specify, or control the actual metadata\n * conversion; that is handled by the Delta UniForm feature.\n *\n * Note that UniForm (Iceberg) depends on IcebergCompatV1, but IcebergCompatV1 does not depend on or\n * require UniForm (Iceberg). It is perfectly valid for a Delta table to have IcebergCompatV1\n * enabled but UniForm (Iceberg) not enabled.\n */\n\nobject IcebergCompatV1 extends IcebergCompatBase(\n  version = 1,\n  icebergFormatVersion = 2,\n  config = DeltaConfigs.ICEBERG_COMPAT_V1_ENABLED,\n  tableFeature = IcebergCompatV1TableFeature,\n  requiredTableProperties = Seq(RequireColumnMapping),\n  incompatibleTableFeatures = Set(DeletionVectorsTableFeature),\n  checks = Seq(\n    CheckOnlySingleVersionEnabled,\n    CheckAddFileHasStats,\n    CheckNoPartitionEvolution,\n    CheckNoListMapNullType,\n    CheckDeletionVectorDisabled,\n    CheckTypeWideningSupported\n  )\n)\n\nobject IcebergCompatV2 extends IcebergCompatBase(\n  version = 2,\n  icebergFormatVersion = 2,\n  config = DeltaConfigs.ICEBERG_COMPAT_V2_ENABLED,\n  tableFeature = IcebergCompatV2TableFeature,\n  requiredTableProperties = Seq(RequireColumnMapping),\n  incompatibleTableFeatures = Set(DeletionVectorsTableFeature),\n  checks = Seq(\n    CheckOnlySingleVersionEnabled,\n    CheckAddFileHasStats,\n    CheckTypeInV2AllowList,\n    CheckPartitionDataTypeInV2AllowList,\n    CheckNoPartitionEvolution,\n    CheckDeletionVectorDisabled,\n    CheckTypeWideningSupported\n  )\n)\n\n/**\n * All IcebergCompatVx should extend from this base class\n *\n * @param version the compat version number\n * @param icebergFormatVersion iceberg format version written by this compat\n * @param config  the DeltaConfig for this IcebergCompat version\n * @param requiredTableFeatures a list of table features it relies on\n * @param requiredTableProperties a list of table properties it relies on.\n *                                See [[RequiredDeltaTableProperty]]\n * @param incompatibleTableFeatures a set of table features it is incompatible\n *                                  with. Used by [[IcebergCompat.isAnyIncompatibleEnabled]]\n * @param checks  a list of checks this IcebergCompatVx will perform.\n *                @see [[RequiredDeltaTableProperty]]\n */\ncase class IcebergCompatBase(\n    version: Int,\n    icebergFormatVersion: Int,\n    config: DeltaConfig[Option[Boolean]],\n    tableFeature: TableFeature,\n    requiredTableProperties: Seq[RequiredDeltaTableProperty[_<:Any]],\n    incompatibleTableFeatures: Set[TableFeature] = Set.empty,\n    checks: Seq[IcebergCompatCheck]) extends DeltaLogging {\n  def isEnabled(metadata: Metadata): Boolean = config.fromMetaData(metadata).getOrElse(false)\n\n  /**\n   * @return true if the feature should be auto enabled on the table created / updated with\n   *         the schema\n   */\n  def shouldAutoEnable(schema: StructType, properties: Map[String, String]): Boolean = false\n  /**\n   * Expected to be called after the newest metadata and protocol have been ~ finalized.\n   *\n   * Furthermore, this should be called *after*\n   * [[UniversalFormat.enforceIcebergInvariantsAndDependencies]].\n   *\n   * If you are enabling IcebergCompatV1 and are creating a new table, this method will\n   * automatically upgrade the table protocol to support ColumnMapping and set it to 'name' mode,\n   * too.\n   *\n   * If you are disabling IcebergCompatV1, this method will also disable Universal Format (Iceberg),\n   * if it is enabled.\n   *\n   * @param actions The actions to be committed in the txn. We will only look at the [[AddFile]]s.\n   *\n   * @return tuple of options of (updatedProtocol, updatedMetadata). For either action, if no\n   *         updates need to be applied, will return None.\n   */\n  def enforceInvariantsAndDependencies(\n      spark: SparkSession,\n      catalogTable: Option[CatalogTable],\n      prevSnapshot: Snapshot,\n      newestProtocol: Protocol,\n      newestMetadata: Metadata,\n      operation: Option[DeltaOperations.Operation],\n      actions: Seq[Action]): (Option[Protocol], Option[Metadata]) = {\n    val prevProtocol = prevSnapshot.protocol\n    val prevMetadata = prevSnapshot.metadata\n    val wasEnabled = this.isEnabled(prevMetadata)\n    val isEnabled = this.isEnabled(newestMetadata)\n    val tableId = newestMetadata.id\n\n    val isCreatingOrReorgTable = UniversalFormat.isCreatingOrReorgTable(operation)\n\n    (wasEnabled, isEnabled) match {\n      case (_, false) => (None, None) // not enable or disabling, Ignore\n      case (_, true) => // Enabling now or already-enabled\n        val tblFeatureUpdates = scala.collection.mutable.Set.empty[TableFeature]\n        val tblPropertyUpdates = scala.collection.mutable.Map.empty[String, String]\n\n        // Check we have all required table features\n        tableFeature.requiredFeatures.foreach { f =>\n          (prevProtocol.isFeatureSupported(f), newestProtocol.isFeatureSupported(f)) match {\n            case (_, true) => // all good\n            case (false, false) => // txn has not supported it! auto-add the table feature\n              tblFeatureUpdates += f\n            case (true, false) => // txn is removing/un-supporting it!\n              handleDisablingRequiredTableFeature(f)\n          }\n        }\n\n        // Check we have all required delta table properties\n        requiredTableProperties.foreach {\n          case RequiredDeltaTableProperty(\n              deltaConfig, validator, autoSetValue, autoEnableOnExistingTable) =>\n            val newestValue = deltaConfig.fromMetaData(newestMetadata)\n            val newestValueOkay = validator(newestValue)\n            val newestValueExplicitlySet = newestMetadata.configuration.contains(deltaConfig.key)\n\n            if (!newestValueOkay) {\n              if (!newestValueExplicitlySet &&\n                  (isCreatingOrReorgTable || autoEnableOnExistingTable)) {\n                // This case covers both CREATE and REPLACE TABLE commands that\n                // did not explicitly specify the required deltaConfig. In these\n                // cases, we set the property automatically.\n                // If autoEnableOnExistingTable = true, it auto sets in all cases\n                tblPropertyUpdates += deltaConfig.key -> autoSetValue\n              } else {\n                // In all other cases, if the property value is not compatible\n                // with the IcebergV1 requirements, we fail\n                handleMissingRequiredTableProperties(\n                  deltaConfig.key, newestValue.toString, autoSetValue)\n              }\n            }\n        }\n\n        // Update Protocol and Metadata if necessary\n        val protocolResult = if (tblFeatureUpdates.nonEmpty) {\n          logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" +\n            log\"IcebergCompatV1 auto-supporting table features: \" +\n            log\"${MDC(DeltaLogKeys.TABLE_FEATURES, tblFeatureUpdates.map(_.name))}\")\n          Some(newestProtocol.merge(tblFeatureUpdates.map(Protocol.forTableFeature).toSeq: _*))\n        } else None\n\n        val metadataResult = if (tblPropertyUpdates.nonEmpty) {\n          logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" +\n            log\"IcebergCompatV1 auto-setting table properties: \" +\n            log\"${MDC(DeltaLogKeys.TBL_PROPERTIES, tblPropertyUpdates)}\")\n          val newConfiguration = newestMetadata.configuration ++ tblPropertyUpdates.toMap\n          var tmpNewMetadata = newestMetadata.copy(configuration = newConfiguration)\n\n          requiredTableProperties.foreach { tp =>\n            tmpNewMetadata = tp.postProcess(prevMetadata, tmpNewMetadata, isCreatingOrReorgTable)\n          }\n\n          Some(tmpNewMetadata)\n        } else None\n\n        // Apply additional checks\n        val context = IcebergCompatContext(\n          spark,\n          catalogTable,\n          prevSnapshot,\n          protocolResult.getOrElse(newestProtocol),\n          metadataResult.getOrElse(newestMetadata),\n          operation,\n          actions,\n          tableId,\n          version\n        )\n        checks.foreach(_.apply(context))\n\n        (protocolResult, metadataResult)\n    }\n  }\n\n  protected def handleMissingTableFeature(feature: TableFeature): Unit =\n    throw DeltaErrors.icebergCompatMissingRequiredTableFeatureException(version, feature)\n\n  protected def handleDisablingRequiredTableFeature(feature: TableFeature): Unit =\n    throw DeltaErrors.icebergCompatDisablingRequiredTableFeatureException(version, feature)\n\n  protected def handleMissingRequiredTableProperties(\n      confKey: String, actualVal: String, requiredVal: String): Unit =\n    throw DeltaErrors.icebergCompatWrongRequiredTablePropertyException(\n      version, confKey, actualVal, requiredVal)\n}\n\n/**\n * Util methods to manage between IcebergCompat versions\n */\ncase class IcebergCompatVersionBase(knownVersions: Set[IcebergCompatBase]) {\n  /**\n   * Fetch from Metadata the current enabled IcebergCompat version.\n   * @return a number indicate the version. E.g., 1 for CompatV1.\n   *         None if no version enabled.\n   */\n  def getEnabledVersion(metadata: Metadata): Option[Int] =\n    knownVersions\n      .find{ _.config.fromMetaData(metadata).getOrElse(false) }\n      .map{ _.version }\n\n  /**\n   * Get the IcebergCompat by version. If version is not valid,\n   * throw an exception.\n   * @return the IcebergCompatVx object\n   */\n  def getForVersion(version: Int): IcebergCompatBase =\n    knownVersions\n      .find(_.version == version)\n      .getOrElse(\n        throw DeltaErrors.icebergCompatVersionNotSupportedException(\n          version, knownVersions.size\n        )\n    )\n\n  /**\n   * @return any enabled IcebergCompat in the conf\n   */\n  def anyEnabled(conf: Map[String, String]): Option[IcebergCompatBase] =\n    knownVersions.find { compat =>\n      conf.getOrElse[String](compat.config.key, \"false\").toBoolean\n    }\n\n  def anyEnabled(metadata: Metadata): Option[IcebergCompatBase] =\n    knownVersions.find { _.config.fromMetaData(metadata).getOrElse(false) }\n\n  /**\n   * @return true if any version of IcebergCompat is enabled\n   */\n  def isAnyEnabled(conf: Map[String, String]): Boolean = anyEnabled(conf).nonEmpty\n\n  def isAnyEnabled(metadata: Metadata): Boolean =\n    knownVersions.exists { _.config.fromMetaData(metadata).getOrElse(false) }\n\n  /**\n   * @return true if a CompatVx greater or eq to the required version is enabled\n   */\n  def isGeqEnabled(metadata: Metadata, requiredVersion: Int): Boolean =\n    anyEnabled(metadata).exists(_.version >= requiredVersion)\n  /**\n   * @return true if any version of IcebergCompat is enabled, and is incompatible\n   *         with the given table feature\n   */\n  def isAnyIncompatibleEnabled(\n      configuration: Map[String, String], feature: TableFeature): Boolean =\n    knownVersions.exists { compat =>\n      configuration.getOrElse[String](compat.config.key, \"false\").toBoolean &&\n        compat.incompatibleTableFeatures.contains(feature)\n    }\n}\n\nobject IcebergCompat extends IcebergCompatVersionBase(\n    Set(IcebergCompatV1, IcebergCompatV2)\n  ) with DeltaLogging\n\n\n\n/**\n * Wrapper class for table property validation\n *\n * @param deltaConfig [[DeltaConfig]] we are checking\n * @param validator A generic method to validate the given value\n * @param autoSetValue The value to set if we can auto-set this value\n * @param autoEnableOnExistingTable this can be true only when the feature\n *                                  can be confidently enabled on existing table\n */\ncase class RequiredDeltaTableProperty[T](\n      deltaConfig: DeltaConfig[T],\n      validator: T => Boolean,\n      autoSetValue: String,\n      autoEnableOnExistingTable: Boolean = false) {\n  /**\n   * A callback after all required properties are added to the new metadata.\n   * @return Updated metadata. None if no change\n   */\n  def postProcess(\n      prevMetadata: Metadata,\n      newMetadata: Metadata,\n      isCreatingNewTable: Boolean) : Metadata = newMetadata\n}\n\nclass RequireColumnMapping(allowedModes: Seq[DeltaColumnMappingMode])\n  extends RequiredDeltaTableProperty(\n    deltaConfig = DeltaConfigs.COLUMN_MAPPING_MODE,\n    validator = (mode: DeltaColumnMappingMode) => allowedModes.contains(mode),\n    autoSetValue = if (allowedModes.contains(NameMapping)) NameMapping.name else IdMapping.name) {\n\n  override def postProcess(\n      prevMetadata: Metadata,\n      newMetadata: Metadata,\n      isCreatingNewTable: Boolean): Metadata = {\n    if (!prevMetadata.configuration.contains(DeltaConfigs.COLUMN_MAPPING_MODE.key) &&\n        newMetadata.configuration.contains(DeltaConfigs.COLUMN_MAPPING_MODE.key)) {\n      val tmpNewMetadata = DeltaColumnMapping.assignColumnIdAndPhysicalName(\n        newMetadata = newMetadata,\n        oldMetadata = prevMetadata,\n        isChangingModeOnExistingTable = false,\n        isOverwritingSchema = false\n      )\n      DeltaColumnMapping.checkColumnIdAndPhysicalNameAssignments(tmpNewMetadata)\n      tmpNewMetadata\n    } else {\n      newMetadata\n    }\n  }\n}\n\nobject RequireColumnMapping extends RequireColumnMapping(Seq(NameMapping, IdMapping))\n\n\ncase class IcebergCompatContext(\n    spark: SparkSession,\n    catalogTable: Option[CatalogTable],\n    prevSnapshot: Snapshot,\n    newestProtocol: Protocol,\n    newestMetadata: Metadata,\n    operation: Option[DeltaOperations.Operation],\n    actions: Seq[Action],\n    tableId: String,\n    version: Integer) {\n  def prevMetadata: Metadata = prevSnapshot.metadata\n\n  def prevProtocol: Protocol = prevSnapshot.protocol\n}\n\ntrait IcebergCompatCheck extends (IcebergCompatContext => Unit)\n\n/**\n * Checks that ensures no more than one IcebergCompatVx is enabled.\n */\nobject CheckOnlySingleVersionEnabled extends IcebergCompatCheck {\n  override def apply(context: IcebergCompatContext): Unit = {\n    val numEnabled = IcebergCompat.knownVersions.toSeq\n      .map { compat =>\n        if (compat.isEnabled(context.newestMetadata)) 1 else 0\n      }.sum\n    if (numEnabled > 1) {\n      throw DeltaErrors.icebergCompatVersionMutualExclusive(context.version)\n    }\n  }\n}\n\nobject CheckAddFileHasStats extends IcebergCompatCheck {\n  override def apply(context: IcebergCompatContext): Unit = {\n    // If this field is empty, then the AddFile is missing the `numRecords` statistic.\n    context.actions.collect { case a: AddFile if a.numLogicalRecords.isEmpty =>\n      throw new UnsupportedOperationException(s\"[tableId=${context.tableId}] \" +\n        s\"IcebergCompatV${context.version} requires all AddFiles to contain \" +\n        s\"the numRecords statistic. AddFile ${a.path} is missing this statistic. \" +\n        s\"Stats: ${a.stats}\")\n    }\n  }\n}\n\nobject CheckNoPartitionEvolution extends IcebergCompatCheck {\n  override def apply(context: IcebergCompatContext): Unit = {\n    // Note: Delta doesn't support partition evolution, but you can change the partitionColumns\n    // by doing a REPLACE or DataFrame overwrite.\n    //\n    // Iceberg-Spark itself *doesn't* support the following cases\n    // - CREATE TABLE partitioned by colA; REPLACE TABLE partitioned by colB\n    // - CREATE TABLE partitioned by colA; REPLACE TABLE not partitioned\n    //\n    // While Iceberg-Spark *does* support\n    // - CREATE TABLE not partitioned; REPLACE TABLE not partitioned\n    // - CREATE TABLE not partitioned; REPLACE TABLE partitioned by colA\n    // - CREATE TABLE partitioned by colA dataType1; REPLACE TABLE partitioned by colA dataType2\n    if (context.prevMetadata.partitionColumns.nonEmpty &&\n      context.prevMetadata.partitionColumns != context.newestMetadata.partitionColumns) {\n      throw DeltaErrors.icebergCompatReplacePartitionedTableException(\n        context.version,\n        context.prevMetadata.partitionColumns,\n        context.newestMetadata.partitionColumns)\n    }\n  }\n}\n\nobject CheckNoListMapNullType extends IcebergCompatCheck {\n  override def apply(context: IcebergCompatContext): Unit = {\n    SchemaUtils.findAnyTypeRecursively(context.newestMetadata.schema) { f =>\n      f.isInstanceOf[MapType] || f.isInstanceOf[ArrayType] || f.isInstanceOf[NullType]\n    } match {\n      case Some(unsupportedType) =>\n        throw DeltaErrors.icebergCompatUnsupportedDataTypeException(\n          context.version, unsupportedType, context.newestMetadata.schema)\n      case _ =>\n    }\n  }\n}\n\nclass CheckTypeInAllowList extends IcebergCompatCheck {\n  def allowTypes: Set[Class[_]] = Set()\n\n  override def apply(context: IcebergCompatContext): Unit = {\n    SchemaUtils\n      .findAnyTypeRecursively(context.newestMetadata.schema)(t => !allowTypes.contains(t.getClass))\n    match {\n      case Some(unsupportedType) =>\n        throw DeltaErrors.icebergCompatUnsupportedDataTypeException(\n          context.version, unsupportedType, context.newestMetadata.schema)\n      case _ =>\n    }\n  }\n}\n\nobject CheckTypeInV2AllowList extends CheckTypeInAllowList {\n  override val allowTypes: Set[Class[_]] = Set[Class[_]] (\n    ByteType.getClass, ShortType.getClass,\n    IntegerType.getClass, LongType.getClass,\n    FloatType.getClass, DoubleType.getClass, classOf[DecimalType],\n    StringType.getClass, BinaryType.getClass,\n    BooleanType.getClass,\n    TimestampType.getClass, TimestampNTZType.getClass, DateType.getClass,\n    classOf[ArrayType], classOf[MapType], classOf[StructType])\n}\n\n\nobject CheckPartitionDataTypeInV2AllowList extends IcebergCompatCheck {\n  private val allowedTypes = Set[Class[_]] (\n    ByteType.getClass, ShortType.getClass, IntegerType.getClass, LongType.getClass,\n    FloatType.getClass, DoubleType.getClass, DecimalType.getClass,\n    StringType.getClass, BinaryType.getClass,\n    BooleanType.getClass,\n    TimestampType.getClass, TimestampNTZType.getClass, DateType.getClass\n  )\n  override def apply(context: IcebergCompatContext): Unit = {\n    val partitionSchema = context.newestMetadata.partitionSchema\n    partitionSchema.fields.find(field => !allowedTypes.contains(field.dataType.getClass))\n    match {\n      case Some(field) =>\n        throw DeltaErrors.icebergCompatUnsupportedPartitionDataTypeException(\n            context.version, field.dataType, partitionSchema)\n       case _ =>\n    }\n  }\n}\n\n/**\n * Check if the deletion vector has been disabled by previous snapshot\n * or newest metadata and protocol depending on whether the operation\n * is REORG UPGRADE UNIFORM or not.\n */\nobject CheckDeletionVectorDisabled extends IcebergCompatCheck {\n  override def apply(context: IcebergCompatContext): Unit = {\n    if (context.newestProtocol.isFeatureSupported(DeletionVectorsTableFeature)) {\n      // note: user will need to *separately* disable deletion vectors if this check fails,\n      //       i.e., ALTER TABLE SET TBLPROPERTIES ('delta.enableDeletionVectors' = 'false');\n      val isReorgUpgradeUniform = UniversalFormat.isReorgUpgradeUniform(context.operation)\n      // for REORG UPGRADE UNIFORM, we only need to check whether DV\n      // is enabled in the newest metadata and protocol, this conforms with\n      // the semantics of REORG UPGRADE UNIFORM, which will automatically disable\n      // DV and rewrite all the parquet files with DV removed as for now.\n      if (isReorgUpgradeUniform) {\n        if (DeletionVectorUtils.deletionVectorsWritable(\n              protocol = context.newestProtocol,\n              metadata = context.newestMetadata\n        )) {\n          throw DeltaErrors.icebergCompatDeletionVectorsShouldBeDisabledException(context.version)\n        }\n      } else {\n        // for other commands, we need to check whether DV is disabled from the\n        // previous snapshot, in case there are concurrent writers.\n        // plus, we also need to check from the newest metadata and protocol,\n        // in case we are creating a new uniform table with DV enabled.\n        if (DeletionVectorUtils.deletionVectorsWritable(context.prevSnapshot) ||\n          DeletionVectorUtils.deletionVectorsWritable(\n            protocol = context.newestProtocol,\n            metadata = context.newestMetadata\n          )) {\n          throw DeltaErrors.icebergCompatDeletionVectorsShouldBeDisabledException(context.version)\n        }\n      }\n    }\n  }\n}\n\n/**\n * Checks that the table didn't go through any type changes that Iceberg doesn't support. See\n * `TypeWidening.isTypeChangeSupportedByIceberg()` for supported type changes.\n * Note that this check covers both:\n * - When the table had an unsupported type change applied in the past and Uniform is being enabled.\n * - When Uniform is enabled and a new, unsupported type change is being applied.\n */\nobject CheckTypeWideningSupported extends IcebergCompatCheck {\n  override def apply(context: IcebergCompatContext): Unit = {\n    val skipCheck = context.spark.sessionState.conf\n      .getConf(DeltaSQLConf.DELTA_TYPE_WIDENING_ALLOW_UNSUPPORTED_ICEBERG_TYPE_CHANGES)\n\n    if (skipCheck || !TypeWidening.isSupported(context.newestProtocol)) return\n\n    TypeWideningMetadata.getAllTypeChanges(context.newestMetadata.schema).foreach {\n      case (fieldPath, TypeChange(_, fromType: AtomicType, toType: AtomicType, _))\n        // We ignore type changes that are not generally supported with type widening to reduce the\n        // risk of this check misfiring. These are handled by `TypeWidening.assertTableReadable()`.\n        // The error here only captures type changes that are supported in Delta but not Iceberg.\n        if TypeWidening.isTypeChangeSupported(fromType, toType) &&\n          !TypeWidening.isTypeChangeSupportedByIceberg(fromType, toType) =>\n        throw DeltaErrors.icebergCompatUnsupportedTypeWideningException(\n          context.version, fieldPath, fromType, toType)\n      case _ => () // ignore\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/IdentityColumn.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.{DeltaFileStatistics, DeltaJobStatisticsTracker}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{Column, DataFrame, Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, LogicalPlan}\nimport org.apache.spark.sql.execution.datasources.WriteTaskStats\nimport org.apache.spark.sql.functions.{array, max, min, to_json}\nimport org.apache.spark.sql.types.{MetadataBuilder, StructField, StructType}\n\n/**\n * This object holds String constants used the field `debugInfo` for\n * logging [[IdentityColumn.opTypeHighWaterMarkUpdate]].\n * Each string represents an unexpected or notable event while calculating the high water mark.\n */\nobject IdentityColumnHighWaterMarkUpdateInfo {\n  val EXISTING_WATER_MARK_BEFORE_START = \"existing_water_mark_before_start\"\n  val CANDIDATE_HIGH_WATER_MARK_ROUNDED = \"candidate_high_watermark_rounded\"\n  val CANDIDATE_HIGH_WATER_MARK_BEFORE_START = \"candidate_high_water_mark_before_start\"\n}\n\n/**\n * Provide utility methods related to IDENTITY column support for Delta.\n */\nobject IdentityColumn extends DeltaLogging {\n  case class IdentityInfo(start: Long, step: Long, highWaterMark: Option[Long])\n  // Default start and step configuration if not specified by user.\n  val defaultStart = 1\n  val defaultStep = 1\n  // Operation types in usage logs.\n  // When IDENTITY columns are defined.\n  val opTypeDefinition = \"delta.identityColumn.definition\"\n  // When table with IDENTITY columns are written into.\n  val opTypeWrite = \"delta.identityColumn.write\"\n  // When IDENTITY column update causes transaction to abort.\n  val opTypeAbort = \"delta.identityColumn.abort\"\n  // When we update the high watermark of an IDENTITY column.\n  val opTypeHighWaterMarkUpdate = \"delta.identityColumn.highWaterMarkUpdate\"\n\n  // Return true if `field` is an identity column that allows explicit insert. Caller must ensure\n  // `isIdentityColumn(field)` is true.\n  def allowExplicitInsert(field: StructField): Boolean = {\n    field.metadata.getBoolean(IDENTITY_INFO_ALLOW_EXPLICIT_INSERT)\n  }\n\n  // Return all the IDENTITY columns from `schema`.\n  def getIdentityColumns(schema: StructType): Seq[StructField] = {\n    schema.filter(ColumnWithDefaultExprUtils.isIdentityColumn)\n  }\n\n  // Return the number of IDENTITY columns in `schema`.\n  private def getNumberOfIdentityColumns(schema: StructType): Int = {\n    getIdentityColumns(schema).size\n  }\n\n  // Create expression to generate IDENTITY values for the column `field`.\n  def createIdentityColumnGenerationExpr(field: StructField): Expression = {\n    val info = IdentityColumn.getIdentityInfo(field)\n    GenerateIdentityValues(info.start, info.step, info.highWaterMark)\n  }\n\n  // Create a column to generate IDENTITY values for the column `field`.\n  def createIdentityColumnGenerationExprAsColumn(field: StructField): Column = {\n    Column(createIdentityColumnGenerationExpr(field)).alias(field.name)\n  }\n\n  /**\n   * Create a stats tracker to collect IDENTITY column high water marks if its values are system\n   * generated.\n   *\n   * @param spark The SparkSession associated with this query.\n   * @param hadoopConf The Hadoop configuration object to use on an executor.\n   * @param path Root Reservoir path\n   * @param schema The schema of the table to be written into.\n   * @param statsDataSchema The schema of the output data (this does not include partition columns).\n   * @param trackHighWaterMarks Column names for which we should track high water marks.\n   * @return The stats tracker.\n   */\n  def createIdentityColumnStatsTracker(\n      spark: SparkSession,\n      hadoopConf: Configuration,\n      path: Path,\n      schema: StructType,\n      statsDataSchema: Seq[Attribute],\n      trackHighWaterMarks: Set[String]\n    ) : Option[DeltaIdentityColumnStatsTracker] = {\n    if (trackHighWaterMarks.isEmpty) return None\n    val identityColumnInfo = schema\n      .filter(f => trackHighWaterMarks.contains(f.name))\n      .map(f => DeltaColumnMapping.getPhysicalName(f) ->  // Get identity column physical names\n        (f.metadata.getLong(IDENTITY_INFO_STEP) > 0L))\n    // We should have found all IDENTITY columns to track high water marks.\n    assert(identityColumnInfo.size == trackHighWaterMarks.size,\n      s\"expect: $trackHighWaterMarks, found (physical names): ${identityColumnInfo.map(_._1)}\")\n    // Build the expression to collect high water marks of all IDENTITY columns as a single\n    // expression. It is essentially a json array containing one max or min aggregate expression\n    // for each IDENTITY column.\n    //\n    // Example: for the following table\n    //\n    //   CREATE TABLE t1 (\n    //     id1 BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT BY 1),\n    //     id2 BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT BY -1),\n    //     value STRING\n    //   ) USING delta;\n    //\n    // The expression will be: to_json(array(max(id1), min(id2)))\n    val aggregates = identityColumnInfo.map {\n      case (name, positiveStep) =>\n        val col = Column(UnresolvedAttribute.quoted(name))\n        if (positiveStep) max(col) else min(col)\n    }\n    val unresolvedExpr = to_json(array(aggregates: _*))\n\n    // Resolve the collection expression by constructing a query to select the expression from a\n    // table with the statsSchema and get the analyzed expression.\n    val resolvedPlan = DataFrameUtils.ofRows(spark, LocalRelation(statsDataSchema))\n      .select(unresolvedExpr).queryExecution.analyzed\n\n    // We have to use the new attributes with regenerated attribute IDs, because the Analyzer\n    // doesn't guarantee that attributes IDs will stay the same\n    val newStatsDataSchema = resolvedPlan.children.head.output\n\n    Some(new DeltaIdentityColumnStatsTracker(\n      hadoopConf,\n      path,\n      newStatsDataSchema,\n      resolvedPlan.expressions.head,\n      identityColumnInfo\n    ))\n  }\n\n  /** Round `value` to the next value that follows start and step configuration. */\n  protected[delta] def roundToNext(start: Long, step: Long, value: Long): Long = {\n    val valueOffset = Math.subtractExact(value, start)\n    if (valueOffset % step == 0) {\n      value\n    } else {\n      // An identity value follows the formula start + step * n. So n = (value - start) / step.\n      // Where n is a non-negative integer if the value respects the start.\n      // Since the value doesn't follow this formula, we need to ceil n.\n      // corrected value = start + step * ceil(n).\n      // However, we can't cast to Double for division because it's only accurate up to 54 bits.\n      // Instead, we will do a floored division and add 1.\n      // start + step * ((value - start) / step + 1)\n      val quotient = valueOffset / step\n      // `valueOffset` will have the same sign as `step` if `value` respects the start.\n      val stepMultiple = if (Math.signum(valueOffset) == Math.signum(step)) {\n        Math.addExact(quotient, 1L)\n      } else {\n        // Don't add one. Otherwise, we end up rounding 2 values up, which may skip the start.\n        quotient\n      }\n      Math.addExact(\n        start,\n        Math.multiplyExact(step, stepMultiple)\n      )\n    }\n  }\n\n  /**\n   * Update the high water mark of the IDENTITY column based on `candidateHighWaterMark`.\n   *\n   * We validate against the identity column definition (start, step) and may insert a high\n   * watermark that's different from `candidateHighWaterMark` if it's not valid. This method\n   * may also not update the high watermark if the candidate doesn't respect the start, is\n   * below the current watermark or is a NOOP.\n   *\n   * @param field The IDENTITY column to update.\n   * @param candidateHighWaterMark The candidate high water mark to update to.\n   * @param allowLoweringHighWaterMarkForSyncIdentity Whether to allow lowering the high water mark.\n   *                                                  Lowering the high water mark is NOT SAFE in\n   *                                                  general, but may be a valid operation in SYNC\n   *                                                  IDENTITY (e.g. repair a high water mark after\n   *                                                  a bad sync).\n   * @return A new `StructField` with the high water mark updated to `candidateHighWaterMark` and\n   *         a Seq[String] that contains debug information for logging.\n   */\n  protected[delta] def updateToValidHighWaterMark(\n      field: StructField,\n      candidateHighWaterMark: Long,\n      allowLoweringHighWaterMarkForSyncIdentity: Boolean\n    ): (StructField, Seq[String]) = {\n    require(ColumnWithDefaultExprUtils.isIdentityColumn(field))\n\n    val info = getIdentityInfo(field)\n    val positiveStep = info.step > 0\n    val orderInStepDirection = if (positiveStep) Ordering.Long else Ordering.Long.reverse\n\n    val loggingBuffer = new mutable.ArrayBuffer[String]\n\n    // We check `candidateHighWaterMark` and not `newHighWaterMark` because\n    // newHighWaterMark may not be part of the column. E.g. a generated by default column\n    // has candidateHighWaterMark = 9, start = 10, step = 3, and previous highWaterMark = None.\n    // We don't want to bump the high water mark to 10 because the next value generated will\n    // be 13, and we'll miss the specified start entirely.\n    val isBeforeStart = orderInStepDirection.lt(candidateHighWaterMark, info.start)\n    if (isBeforeStart) {\n      loggingBuffer.append(\n        IdentityColumnHighWaterMarkUpdateInfo.CANDIDATE_HIGH_WATER_MARK_BEFORE_START)\n    }\n\n    // We must round on the generated by default case because the candidate may be a user inserted\n    // value and may not follow the identity column definition. We're not skipping this check\n    // for the generated always case. It's effectively a NOOP since generated always values should\n    // theoretically always respect the identity column definition. If the high watermark was\n    // wrong (for some reason), this is our chance to fix it.\n    val roundedCandidateHighWaterMark = roundToNext(info.start, info.step, candidateHighWaterMark)\n    if (roundedCandidateHighWaterMark != candidateHighWaterMark) {\n      loggingBuffer.append(IdentityColumnHighWaterMarkUpdateInfo.CANDIDATE_HIGH_WATER_MARK_ROUNDED)\n    }\n\n    // If allowLoweringHighWaterMarkForSyncIdentity is true, we can ignore the existing high water\n    // mark.\n    val newHighWaterMark = info.highWaterMark match {\n      case Some(oldWaterMark) if !allowLoweringHighWaterMarkForSyncIdentity =>\n        orderInStepDirection.max(oldWaterMark, roundedCandidateHighWaterMark)\n      case _ => roundedCandidateHighWaterMark\n    }\n\n    val tableHasBadHighWaterMark = info.highWaterMark.exists(oldWaterMark =>\n      orderInStepDirection.lt(oldWaterMark, info.start))\n    if (tableHasBadHighWaterMark) {\n      loggingBuffer.append(\n        IdentityColumnHighWaterMarkUpdateInfo.EXISTING_WATER_MARK_BEFORE_START)\n    }\n\n    val isChanged = !info.highWaterMark.contains(newHighWaterMark)\n\n    // If a table already has a bad high water mark, we shouldn't prevent them from updating the\n    // high water mark. Always try to update to newHighWaterMark, which is guaranteed to be a better\n    // choice than the existing one since we do a max().\n    // Note that means if a table has bad water mark, we can set the high water to the start due to\n    // the rounding logic.\n    // Don't update if it's before start or the high watermark is the same.\n    if (tableHasBadHighWaterMark || (!isBeforeStart && isChanged)) {\n      val newMetadata = new MetadataBuilder().withMetadata(field.metadata)\n        .putLong(IDENTITY_INFO_HIGHWATERMARK, newHighWaterMark)\n        .build()\n      (field.copy(metadata = newMetadata), loggingBuffer.toIndexedSeq)\n    } else {\n      // If we don't update the high watermark, we don't need to log the update.\n      (field, Nil)\n   }\n  }\n\n  /**\n   * Return a new schema with IDENTITY high water marks updated in the schema.\n   * The new high watermarks are decided based on the `updatedIdentityHighWaterMarks` and old high\n   * watermark values present in the passed `schema`.\n   */\n  def updateSchema(\n      deltaLog: DeltaLog,\n      schema: StructType,\n      updatedIdentityHighWaterMarks: Seq[(String, Long)]\n    ): StructType = {\n    val updatedIdentityHighWaterMarksGrouped =\n      updatedIdentityHighWaterMarks.groupBy(_._1).mapValues(v => v.map(_._2))\n    StructType(schema.map { f =>\n      updatedIdentityHighWaterMarksGrouped.get(DeltaColumnMapping.getPhysicalName(f)) match {\n        case Some(newWatermarks) if ColumnWithDefaultExprUtils.isIdentityColumn(f) =>\n          val oldIdentityInfo = getIdentityInfo(f)\n          val positiveStep = oldIdentityInfo.step > 0\n          val candidateHighWaterMark = if (positiveStep) {\n            newWatermarks.max\n          } else {\n            newWatermarks.min\n          }\n          val (newField, loggingSeq) = updateToValidHighWaterMark(\n            f, candidateHighWaterMark, allowLoweringHighWaterMarkForSyncIdentity = false)\n          if (loggingSeq.nonEmpty) {\n            recordDeltaEvent(\n              deltaLog = deltaLog,\n              opType = opTypeHighWaterMarkUpdate,\n              data = Map(\n                \"columnName\" -> f.name,\n                \"debugInfo\" -> loggingSeq.mkString(\", \"),\n                \"oldHighWaterMark\" -> oldIdentityInfo.highWaterMark,\n                \"candidateHighWaterMark\" -> candidateHighWaterMark,\n                \"updatedFrom\" -> \"updateSchema\"\n              )\n            )\n          }\n          newField\n        case _ =>\n          f\n      }\n    })\n  }\n\n  // Block explicitly provided IDENTITY values if column definition does not allow so.\n  def blockExplicitIdentityColumnInsert(\n      schema: StructType,\n      query: LogicalPlan): Unit = {\n    val nonInsertableIdentityColumns = schema.filter { f =>\n      ColumnWithDefaultExprUtils.isIdentityColumn(f) && !IdentityColumn.allowExplicitInsert(f)\n    }.map(_.name)\n    blockIdentityColumn(\n      nonInsertableIdentityColumns,\n      query.output.map(attr => Seq(attr.name)),\n      isUpdate = false\n    )\n  }\n\n  // Block explicitly provided IDENTITY values if column definition does not allow so.\n  def blockExplicitIdentityColumnInsert(\n      identityColumns: Seq[StructField],\n      insertedColNameParts: Seq[Seq[String]]): Unit = {\n    val nonInsertableIdentityColumns = identityColumns\n      .filter(!allowExplicitInsert(_))\n      .map(_.name)\n    blockIdentityColumn(\n      nonInsertableIdentityColumns,\n      insertedColNameParts,\n      isUpdate = false)\n  }\n\n  // Block updating IDENTITY columns.\n  def blockIdentityColumnUpdate(\n      schema: StructType,\n      updatedColNameParts: Seq[Seq[String]]): Unit = {\n    blockIdentityColumnUpdate(getIdentityColumns(schema), updatedColNameParts)\n  }\n\n  // Block updating IDENTITY columns.\n  def blockIdentityColumnUpdate(\n      identityColumns: Seq[StructField],\n      updatedColNameParts: Seq[Seq[String]]): Unit = {\n    blockIdentityColumn(\n      identityColumns.map(_.name),\n      updatedColNameParts,\n      isUpdate = true)\n  }\n\n  def logTableCreation(deltaLog: DeltaLog, schema: StructType): Unit = {\n    val numIdentityColumns = getNumberOfIdentityColumns(schema)\n    if (numIdentityColumns != 0) {\n      recordDeltaEvent(\n        deltaLog,\n        opTypeDefinition,\n        data = Map(\n          \"numIdentityColumns\" -> numIdentityColumns\n        )\n      )\n    }\n  }\n\n  def logTableWrite(\n      snapshot: Snapshot,\n      generatedIdentityColumns: Set[String],\n      numInsertedRowsOpt: Option[Long]): Unit = {\n    val identityColumns = getIdentityColumns(snapshot.schema)\n    if (identityColumns.nonEmpty) {\n      val explicitIdentityColumns = identityColumns.filter {\n        f => !generatedIdentityColumns.contains(f.name)\n      }.map(_.name)\n      recordDeltaEvent(\n        snapshot.deltaLog,\n        opTypeWrite,\n        data = Map(\n          \"numInsertedRows\" -> numInsertedRowsOpt,\n          \"generatedIdentityColumnNames\" -> generatedIdentityColumns.mkString(\",\"),\n          \"generatedIdentityColumnCount\" -> generatedIdentityColumns.size,\n          \"explicitIdentityColumnNames\" -> explicitIdentityColumns.mkString(\",\"),\n          \"explicitIdentityColumnCount\" -> explicitIdentityColumns.size\n        )\n      )\n    }\n  }\n\n  def logTransactionAbort(deltaLog: DeltaLog): Unit = {\n    recordDeltaEvent(deltaLog, opTypeAbort)\n  }\n\n  // Calculate the sync'ed IDENTITY high water mark based on actual data and returns a\n  // potentially updated `StructField`.\n  def syncIdentity(\n      deltaLog: DeltaLog,\n      field: StructField,\n      df: DataFrame,\n      allowLoweringHighWaterMarkForSyncIdentity: Boolean\n  ): StructField = {\n    assert(ColumnWithDefaultExprUtils.isIdentityColumn(field))\n    // Run a query to get the actual high water mark (max or min value of the IDENTITY column) from\n    // the actual data.\n    val info = getIdentityInfo(field)\n    val positiveStep = info.step > 0\n    val expr = if (positiveStep) max(field.name) else min(field.name)\n    val resultRow = df.select(expr).collect().head\n\n    if (!resultRow.isNullAt(0)) {\n      val candidateHighWaterMark = resultRow.getLong(0)\n      val (newField, loggingSeq) = updateToValidHighWaterMark(\n        field, candidateHighWaterMark, allowLoweringHighWaterMarkForSyncIdentity)\n      if (loggingSeq.nonEmpty) {\n        recordDeltaEvent(\n          deltaLog = deltaLog,\n          opType = opTypeHighWaterMarkUpdate,\n          data = Map(\n            \"columnName\" -> field.name,\n            \"debugInfo\" -> loggingSeq.mkString(\", \"),\n            \"oldHighWaterMark\" -> info.highWaterMark,\n            \"candidateHighWaterMark\" -> candidateHighWaterMark,\n            \"updatedFrom\" -> \"syncIdentity\"\n          )\n        )\n      }\n      newField\n    } else {\n      field\n    }\n  }\n\n  /**\n   * Returns a copy of `schemaToCopy` in which the high water marks of the identity columns have\n   * been merged with the corresponding high water marks of `schemaWithHighWaterMarksToMerge`.\n   */\n  def copySchemaWithMergedHighWaterMarks(\n      deltaLog: DeltaLog,\n      schemaToCopy: StructType,\n      schemaWithHighWaterMarksToMerge: StructType\n    ): StructType = {\n    val newHighWatermarks = getIdentityColumns(schemaWithHighWaterMarksToMerge).flatMap { f =>\n      val info = getIdentityInfo(f)\n      info.highWaterMark.map(waterMark => DeltaColumnMapping.getPhysicalName(f) -> waterMark)\n    }\n    updateSchema(\n      deltaLog,\n      schemaToCopy,\n      newHighWatermarks\n    )\n  }\n\n\n  // Check `colNameParts` does not contain any column from `columnNamesToBlock`.\n  private def blockIdentityColumn(\n      columnNamesToBlock: Seq[String],\n      colNameParts: Seq[Seq[String]],\n      isUpdate: Boolean): Unit = {\n    if (columnNamesToBlock.nonEmpty) {\n      val resolver = SparkSession.active.sessionState.analyzer.resolver\n      for (namePart <- colNameParts) {\n        // IDENTITY column cannot be nested columns, so we only need to check top level columns.\n        if (namePart.size == 1) {\n          val colName = namePart.head\n          if (columnNamesToBlock.exists(resolver(_, colName))) {\n            if (isUpdate) {\n              throw DeltaErrors.identityColumnUpdateNotSupported(colName)\n            } else {\n              throw DeltaErrors.identityColumnExplicitInsertNotSupported(colName)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  // Return IDENTITY information of column `field`. Caller must ensure `isIdentityColumn(field)`\n  // is true.\n  def getIdentityInfo(field: StructField): IdentityInfo = {\n    val md = field.metadata\n    val start = md.getLong(IDENTITY_INFO_START)\n    val step = md.getLong(IDENTITY_INFO_STEP)\n    // If system hasn't generated IDENTITY values for this column (either it hasn't been\n    // inserted into, or every inserts provided values for this IDENTITY column), high water mark\n    // field will not present in column metadata. In this case, high water mark will be set to\n    // (start - step) so that the first value generated is start (high water mark + step).\n    val highWaterMark = if (md.contains(IDENTITY_INFO_HIGHWATERMARK)) {\n        Some(md.getLong(IDENTITY_INFO_HIGHWATERMARK))\n      } else {\n        None\n      }\n    IdentityInfo(start, step, highWaterMark)\n  }\n}\n\n/**\n * Stats tracker for IDENTITY column high water marks. The only difference between this class and\n * `DeltaJobStatisticsTracker` is how the stats are aggregated on the driver.\n *\n * @param hadoopConf The Hadoop configuration object to use on an executor.\n * @param path Root Reservoir path\n * @param dataCols Resolved data (i.e. non-partitionBy) columns of the dataframe to be written.\n * @param statsColExpr The expression to collect high water marks.\n * @param identityColumnInfo Information of IDENTITY columns. It contains a pair of column name\n *                           and whether it has a positive step for each IDENTITY column.\n */\nclass DeltaIdentityColumnStatsTracker(\n    @transient private val hadoopConf: Configuration,\n    @transient path: Path,\n    dataCols: Seq[Attribute],\n    statsColExpr: Expression,\n    val identityColumnInfo: Seq[(String, Boolean)]\n  )\n  extends DeltaJobStatisticsTracker(\n    hadoopConf,\n    path,\n    dataCols,\n    statsColExpr\n  ) {\n\n  // Map of column name to its corresponding collected high water mark.\n  var highWaterMarks = scala.collection.mutable.Map[String, Long]()\n\n  // Process the stats on the driver. In `stats` we have a sequence of `DeltaFileStatistics`,\n  // whose stats is a map of file path to its corresponding array of high water marks in json.\n  override def processStats(stats: Seq[WriteTaskStats], jobCommitTime: Long): Unit = {\n    stats.map(_.asInstanceOf[DeltaFileStatistics]).flatMap(_.stats).map {\n      case (_, statsString) =>\n        val fileHighWaterMarks = JsonUtils.fromJson[Array[Long]](statsString)\n        // We must have high water marks collected for all IDENTITY columns and we have guaranteed\n        // that their orders in the array follow the orders in `identityInfo` by aligning the\n        // order of expression and `identityColumnInfo` in `createIdentityColumnStatsTracker`.\n        require(fileHighWaterMarks.size == identityColumnInfo.size)\n        identityColumnInfo.zip(fileHighWaterMarks).map {\n          case ((name, positiveStep), value) =>\n            val updated = highWaterMarks.get(name).map { v =>\n              if (positiveStep) v.max(value) else v.min(value)\n            }.getOrElse(value)\n            highWaterMarks.update(name, updated)\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/JsonMetadataDomain.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.DomainMetadata\nimport org.apache.spark.sql.delta.util.JsonUtils\n\n/**\n * A trait for capturing metadata domain of type T.\n */\ntrait JsonMetadataDomain[T] {\n  val domainName: String\n\n  /**\n   * Creates [[DomainMetadata]] with configuration set as a JSON-serialized value of\n   * the metadata domain of type T.\n   */\n  def toDomainMetadata[T: Manifest]: DomainMetadata =\n    DomainMetadata(domainName, JsonUtils.toJson(this.asInstanceOf[T]), removed = false)\n}\n\nabstract class JsonMetadataDomainUtils[T: Manifest] {\n  protected val domainName: String\n\n  /**\n   * Returns the metadata domain's configuration as type T for domain metadata that\n   * matches \"domainName\" in the given snapshot. Returns None if there is no matching\n   * domain metadata.\n   */\n  def fromSnapshot(snapshot: Snapshot): Option[T] = {\n    snapshot.domainMetadata\n      .find(_.domain == domainName)\n      .map(m => fromJsonConfiguration(m))\n  }\n\n  protected def fromJsonConfiguration(domain: DomainMetadata): T =\n    JsonUtils.fromJson[T](domain.configuration)\n\n  def isSameDomain(d: DomainMetadata): Boolean = d.domain == domainName\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/LastCheckpointInfo.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.actions.{CheckpointMetadata, SidecarFile, SingleAction}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.FileNames.{checkpointVersion, numCheckpointParts}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonIgnoreProperties, JsonPropertyOrder}\nimport com.fasterxml.jackson.databind.{DeserializationFeature, JsonNode}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport org.apache.commons.codec.digest.DigestUtils\nimport org.apache.hadoop.fs.FileStatus\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Information about the V2 Checkpoint in the LAST_CHECKPOINT file\n * @param path             file name corresponding to the uuid-named v2 checkpoint\n * @param sizeInBytes      size in bytes for the uuid-named v2 checkpoint\n * @param modificationTime modification time for the uuid-named v2 checkpoint\n * @param nonFileActions   all non file actions for the v2 checkpoint. This info may or may not be\n *                         available. A None value means that info is missing.\n *                         If it is not None, then it should have all the non-FileAction\n *                         corresponding to the checkpoint.\n * @param sidecarFiles     sidecar files corresponding to the v2 checkpoint. This info may or may\n *                         not be available. A None value means that this info is missing.\n *                         An empty list denotes that the v2 checkpoint has no sidecars.\n */\ncase class LastCheckpointV2(\n    path: String,\n    sizeInBytes: Long,\n    modificationTime: Long,\n    nonFileActions: Option[Seq[SingleAction]],\n    sidecarFiles: Option[Seq[SidecarFile]]) {\n\n  @JsonIgnore\n  lazy val checkpointMetadataOpt: Option[CheckpointMetadata] =\n    nonFileActions.flatMap(_.map(_.unwrap).collectFirst { case cm: CheckpointMetadata => cm })\n\n}\n\nobject LastCheckpointV2 {\n  def apply(\n      fileStatus: FileStatus,\n      nonFileActions: Option[Seq[SingleAction]] = None,\n      sidecarFiles: Option[Seq[SidecarFile]] = None): LastCheckpointV2 = {\n    LastCheckpointV2(\n      path = fileStatus.getPath.getName,\n      sizeInBytes = fileStatus.getLen,\n      modificationTime = fileStatus.getModificationTime,\n      nonFileActions = nonFileActions,\n      sidecarFiles = sidecarFiles)\n  }\n}\n\n/**\n * Records information about a checkpoint.\n *\n * This class provides the checksum validation logic, needed to ensure that content of\n * LAST_CHECKPOINT file points to a valid json. The readers might read some part from old file and\n * some part from the new file (if the file is read across multiple requests). In some rare\n * scenarios, the split read might produce a valid json and readers will be able to parse it and\n * convert it into a [[LastCheckpointInfo]] object that contains invalid data. In order to prevent\n * using it, we do a checksum match on the read json to validate that it is consistent.\n *\n * For old Delta versions, which do not have checksum logic, we want to make sure that the old\n * fields (i.e. version, size, parts) are together in the beginning of last_checkpoint json. All\n * these fields together are less than 50 bytes, so even in split read scenario, we want to make\n * sure that old delta readers which do not do have checksum validation logic, gets all 3 fields\n * from one read request. For this reason, we use `JsonPropertyOrder` to force them in the beginning\n * together.\n *\n * @param version the version of this checkpoint\n * @param size the number of actions in the checkpoint, -1 if the information is unavailable.\n * @param parts the number of parts when the checkpoint has multiple parts. None if this is a\n *              singular checkpoint\n * @param sizeInBytes the number of bytes of the checkpoint\n * @param numOfAddFiles the number of AddFile actions in the checkpoint\n * @param checkpointSchema the schema of the underlying checkpoint files\n * @param checksum the checksum of the [[LastCheckpointInfo]].\n */\n@JsonPropertyOrder(Array(\"version\", \"size\", \"parts\"))\ncase class LastCheckpointInfo(\n    version: Long,\n    size: Long,\n    parts: Option[Int],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    sizeInBytes: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numOfAddFiles: Option[Long],\n    checkpointSchema: Option[StructType],\n    v2Checkpoint: Option[LastCheckpointV2] = None,\n    checksum: Option[String] = None) {\n\n  @JsonIgnore\n  def getFormatEnum(): CheckpointInstance.Format = parts match {\n    case _ if v2Checkpoint.nonEmpty => CheckpointInstance.Format.V2\n    case Some(_) => CheckpointInstance.Format.WITH_PARTS\n    case None => CheckpointInstance.Format.SINGLE\n  }\n\n  /** Whether two [[LastCheckpointInfo]] represents the same checkpoint */\n  def semanticEquals(other: LastCheckpointInfo): Boolean = {\n    CheckpointInstance(this) == CheckpointInstance(other)\n  }\n}\n\nobject LastCheckpointInfo {\n\n  val STORED_CHECKSUM_KEY = \"checksum\"\n\n  /** Whether to store checksum OR do checksum validations around [[LastCheckpointInfo]]  */\n  def checksumEnabled(spark: SparkSession): Boolean =\n    spark.sessionState.conf.getConf(DeltaSQLConf.LAST_CHECKPOINT_CHECKSUM_ENABLED)\n\n  /**\n   * Returns the json representation of this [[LastCheckpointInfo]] object.\n   * Also adds the checksum to the returned json if `addChecksum` is set. The checksum can be\n   * used by readers to validate consistency of the [[LastCheckpointInfo]].\n   * It is calculated using rules mentioned in \"JSON checksum\" section in PROTOCOL.md.\n   */\n  def serializeToJson(\n      lastCheckpointInfo: LastCheckpointInfo,\n      addChecksum: Boolean,\n      suppressOptionalFields: Boolean = false): String = {\n    if (suppressOptionalFields) {\n      return JsonUtils.toJson(\n        LastCheckpointInfo(\n          lastCheckpointInfo.version,\n          lastCheckpointInfo.size,\n          lastCheckpointInfo.parts,\n          sizeInBytes = None,\n          numOfAddFiles = None,\n          v2Checkpoint = None,\n          checkpointSchema = None))\n    }\n\n    val jsonStr: String = JsonUtils.toJson(lastCheckpointInfo.copy(checksum = None))\n    if (!addChecksum) return jsonStr\n    val rootNode = JsonUtils.mapper.readValue(jsonStr, classOf[ObjectNode])\n    val checksum = treeNodeToChecksum(rootNode)\n    rootNode.put(STORED_CHECKSUM_KEY, checksum).toString\n  }\n\n  /**\n   * Converts the given `jsonStr` into a [[LastCheckpointInfo]] object.\n   * if `validate` is set, then it also validates the consistency of the json:\n   *  - calculating the checksum and comparing it with the `storedChecksum`.\n   *  - json should not have any duplicates.\n   */\n  def deserializeFromJson(jsonStr: String, validate: Boolean): LastCheckpointInfo = {\n    if (validate) {\n      val (storedChecksumOpt, actualChecksum) = LastCheckpointInfo.getChecksums(jsonStr)\n      storedChecksumOpt.filter(_ != actualChecksum).foreach { storedChecksum =>\n        throw new IllegalStateException(s\"Checksum validation failed for json: $jsonStr,\\n\" +\n          s\"storedChecksum:$storedChecksum, actualChecksum:$actualChecksum\")\n      }\n    }\n\n    // This means:\n    // 1) EITHER: Checksum validation is config-disabled\n    // 2) OR: The json lacked a checksum (e.g. written by old client). Nothing to validate.\n    // 3) OR: The Stored checksum matches the calculated one. Validation succeeded.\n    JsonUtils.fromJson[LastCheckpointInfo](jsonStr)\n  }\n\n  /**\n   * Analyzes the json representation of [[LastCheckpointInfo]] and returns checksum tuple where\n   * - first element refers to the stored checksum in the json representation of\n   *   [[LastCheckpointInfo]], None if the checksum is not present.\n   * - second element refers to the checksum computed from the canonicalized json representation of\n   *   the [[LastCheckpointInfo]].\n   */\n  def getChecksums(jsonStr: String): (Option[String], String) = {\n    val reader =\n      JsonUtils.mapper.reader().withFeatures(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)\n    val rootNode = reader.readTree(jsonStr)\n    val storedChecksum = if (rootNode.has(STORED_CHECKSUM_KEY)) {\n      Some(rootNode.get(STORED_CHECKSUM_KEY).asText())\n    } else {\n      None\n    }\n    val actualChecksum = treeNodeToChecksum(rootNode)\n    storedChecksum -> actualChecksum\n  }\n\n  /**\n   * Canonicalizes the given `treeNode` json and returns its md5 checksum.\n   * Refer to \"JSON checksum\" section in PROTOCOL.md for canonicalization steps.\n   */\n  def treeNodeToChecksum(treeNode: JsonNode): String = {\n    val jsonEntriesBuffer = ArrayBuffer.empty[(String, String)]\n\n    import scala.collection.JavaConverters._\n    def traverseJsonNode(currentNode: JsonNode, prefix: ArrayBuffer[String]): Unit = {\n      if (currentNode.isObject) {\n        currentNode.fields().asScala.foreach { entry =>\n          prefix.append(encodeString(entry.getKey))\n          traverseJsonNode(entry.getValue, prefix)\n          prefix.trimEnd(1)\n        }\n      } else if (currentNode.isArray) {\n        currentNode.asScala.zipWithIndex.foreach { case (jsonNode, index) =>\n          prefix.append(index.toString)\n          traverseJsonNode(jsonNode, prefix)\n          prefix.trimEnd(1)\n        }\n      } else {\n        var nodeValue = currentNode.asText()\n        if (currentNode.isTextual) nodeValue = encodeString(nodeValue)\n        jsonEntriesBuffer.append(prefix.mkString(\"+\") -> nodeValue)\n      }\n    }\n    traverseJsonNode(treeNode, prefix = ArrayBuffer.empty)\n    import Ordering.Implicits._\n    val normalizedJsonKeyValues = jsonEntriesBuffer\n      .filter { case (k, _) => k != s\"\"\"\"$STORED_CHECKSUM_KEY\"\"\"\" }\n      .map { case (k, v) => s\"$k=$v\" }\n      .sortBy(_.toSeq: Seq[Char])\n      .mkString(\",\")\n    DigestUtils.md5Hex(normalizedJsonKeyValues)\n  }\n\n  private val isUnreservedOctet =\n    (Set.empty ++ ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') ++ \"-._~\").map(_.toByte)\n\n  /**\n   * URL encodes a String based on the following rules:\n   * 1. Use uppercase hexadecimals for all percent encodings\n   * 2. percent-encode everything other than unreserved characters\n   * 3. unreserved characters are = a-z / A-Z / 0-9 / \"-\" / \".\" / \"_\" / \"~\"\n   */\n  private def encodeString(str: String): String = {\n    val result = str.getBytes(java.nio.charset.StandardCharsets.UTF_8).map {\n      case b if isUnreservedOctet(b) => b.toChar.toString\n      case b =>\n        // convert to char equivalent of unsigned byte\n        val c = (b & 0xff)\n        f\"%%$c%02X\"\n    }.mkString\n    s\"\"\"\"$result\"\"\"\"\n  }\n\n  def fromFiles(files: Seq[FileStatus]): LastCheckpointInfo = {\n    assert(files.nonEmpty, \"files should be non empty to construct LastCheckpointInfo\")\n    LastCheckpointInfo(\n      version = checkpointVersion(files.head),\n      size = -1L,\n      parts = numCheckpointParts(files.head.getPath),\n      sizeInBytes = Some(files.map(_.getLen).sum),\n      numOfAddFiles = None,\n      checkpointSchema = None\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/MaterializedRowTrackingColumn.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.expressions.Attribute\n\n/**\n * Represents a materialized row tracking column. Concrete implementations are [[MaterializedRowId]]\n * and [[MaterializedRowCommitVersion]].\n */\nabstract class MaterializedRowTrackingColumn {\n  /**\n   * Table metadata configuration property name storing the name of this materialized row tracking\n   * column.\n   */\n  val MATERIALIZED_COLUMN_NAME_PROP: String\n\n  /** Prefix to use for the name of this materialized row tracking column */\n  val MATERIALIZED_COLUMN_NAME_PREFIX: String\n\n  /**\n   * Returns the exception to throw when the materialized column name is not set in the table\n   * metadata. The table name is passed as argument.\n   */\n  def missingMetadataException: String => Throwable\n\n  /**\n   * Generate a random name for a materialized row tracking column. The generated name contains a\n   * unique UUID, we assume it shall not conflict with existing column.\n   */\n  private def generateMaterializedColumnName: String =\n    MATERIALIZED_COLUMN_NAME_PREFIX + UUID.randomUUID().toString\n\n  /**\n   * Update this materialized row tracking column name in the metadata.\n   *   - If row tracking is not allowed or not supported, this operation is a noop.\n   *   - If row tracking is supported on the table and no name is assigned to the old metadata, we\n   *   assign a name. If a name was already assigned, we copy over this name.\n   * Throws in case the assignment of a new name fails due to a conflict.\n   */\n  private[delta] def updateMaterializedColumnName(\n      protocol: Protocol,\n      oldMetadata: Metadata,\n      newMetadata: Metadata): Metadata = {\n    if (!RowTracking.isSupported(protocol)) {\n      // During a CLONE we might not enable row tracking, but still receive the materialized column\n      // name from the source. In this case, we need to remove the column name to not have the same\n      // column name in two different tables.\n      return newMetadata.copy(\n        configuration = newMetadata.configuration - MATERIALIZED_COLUMN_NAME_PROP)\n    }\n\n    // Take the materialized column name from the old metadata, as this is the materialized column\n    // name of the current table. We overwrite the materialized column name of the new metadata as\n    // it could contain a materialized column name from another table, e.g. the source table during\n    // a CLONE.\n    val materializedColumnName = oldMetadata.configuration\n      .getOrElse(MATERIALIZED_COLUMN_NAME_PROP, generateMaterializedColumnName)\n    newMetadata.copy(configuration = newMetadata.configuration +\n      (MATERIALIZED_COLUMN_NAME_PROP -> materializedColumnName))\n  }\n\n  /**\n   * Throws an exception if row tracking is allowed and the materialized column name conflicts with\n   * another column name.\n   */\n  private[delta] def throwIfMaterializedColumnNameConflictsWithSchema(metadata: Metadata): Unit = {\n    val logicalColumnNames = metadata.schema.fields.map(_.name)\n    val physicalColumnNames = metadata.schema.fields\n      .map(field => DeltaColumnMapping.getPhysicalName(field))\n\n    metadata.configuration.get(MATERIALIZED_COLUMN_NAME_PROP).foreach { columnName =>\n      if (logicalColumnNames.contains(columnName) || physicalColumnNames.contains(columnName)) {\n        throw DeltaErrors.addingColumnWithInternalNameFailed(columnName)\n      }\n    }\n  }\n\n  /** Extract the materialized column name from the [[Metadata]] of a [[DeltaLog]]. */\n  def getMaterializedColumnName(protocol: Protocol, metadata: Metadata): Option[String] = {\n    if (RowTracking.isEnabled(protocol, metadata)) {\n      metadata.configuration.get(MATERIALIZED_COLUMN_NAME_PROP)\n    } else {\n      None\n    }\n  }\n\n  /** Convenience method that throws if the materialized column name cannot be extracted. */\n  def getMaterializedColumnNameOrThrow(\n      protocol: Protocol, metadata: Metadata, tableId: String): String = {\n    getMaterializedColumnName(protocol, metadata).getOrElse {\n      throw missingMetadataException(tableId)\n    }\n  }\n\n  /**\n   * If Row tracking is enabled, return an Expression referencing this Row tracking column Attribute\n   * in 'dataFrame' if one is available. Otherwise returns None.\n   */\n  private[delta] def getAttribute(\n      snapshot: Snapshot, dataFrame: DataFrame): Option[Attribute] = {\n    if (!RowTracking.isEnabled(snapshot.protocol, snapshot.metadata)) {\n      return None\n    }\n\n    val materializedColumnName = getMaterializedColumnNameOrThrow(\n      snapshot.protocol, snapshot.metadata, snapshot.deltaLog.unsafeVolatileTableId)\n\n    val analyzedPlan = dataFrame.queryExecution.analyzed\n    analyzedPlan.outputSet.view.find(attr => materializedColumnName == attr.name)\n  }\n}\n\nobject MaterializedRowId extends MaterializedRowTrackingColumn {\n  /**\n   * Table metadata configuration property name storing the name of the column in which the\n   * Row IDs are materialized.\n   */\n  val MATERIALIZED_COLUMN_NAME_PROP = \"delta.rowTracking.materializedRowIdColumnName\"\n\n  /** Prefix to use for the name of the materialized Row ID column */\n  val MATERIALIZED_COLUMN_NAME_PREFIX = \"_row-id-col-\"\n\n  def missingMetadataException: String => Throwable = DeltaErrors.materializedRowIdMetadataMissing\n}\n\nobject MaterializedRowCommitVersion extends MaterializedRowTrackingColumn {\n  /**\n   * Table metadata configuration property name storing the name of the column in which the\n   * Row commit versions are materialized.\n   */\n  val MATERIALIZED_COLUMN_NAME_PROP = \"delta.rowTracking.materializedRowCommitVersionColumnName\"\n\n  /** Prefix to use for the name of the materialized Row commit version column */\n  val MATERIALIZED_COLUMN_NAME_PREFIX = \"_row-commit-version-col-\"\n\n  def missingMetadataException: String => Throwable =\n    DeltaErrors.materializedRowCommitVersionMetadataMissing\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/MetadataCleanup.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.{Calendar, TimeZone}\n\nimport scala.collection.immutable.NumericRange\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.DeltaHistoryManager.BufferingLogDeletionIterator\nimport org.apache.spark.sql.delta.TruncationGranularity.{DAY, HOUR, MINUTE, TruncationGranularity}\nimport org.apache.spark.sql.delta.actions.{Action, Metadata}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.delta.util.FileNames._\nimport org.apache.commons.lang3.time.DateUtils\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, Path}\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.functions.{col, isnull, lit, not, when}\n\nprivate[delta] object TruncationGranularity extends Enumeration {\n  type TruncationGranularity = Value\n  val DAY, HOUR, MINUTE = Value\n}\n\n/** Cleans up expired Delta table metadata. */\ntrait MetadataCleanup extends DeltaLogging {\n  self: DeltaLog =>\n\n  protected type VersionRange = NumericRange.Inclusive[Long]\n\n  protected def versionRange(start: Long, end: Long): VersionRange =\n    NumericRange.inclusive[Long](start = start, end = end, step = 1)\n\n  /** Whether to clean up expired log files and checkpoints. */\n  def enableExpiredLogCleanup(metadata: Metadata): Boolean =\n    DeltaConfigs.ENABLE_EXPIRED_LOG_CLEANUP.fromMetaData(metadata)\n\n  /**\n   * Returns the duration in millis for how long to keep around obsolete logs. We may keep logs\n   * beyond this duration until the next calendar day to avoid constantly creating checkpoints.\n   */\n  def deltaRetentionMillis(metadata: Metadata): Long = {\n    val interval = DeltaConfigs.LOG_RETENTION.fromMetaData(metadata)\n    DeltaConfigs.getMilliSeconds(interval)\n  }\n\n  override def doLogCleanup(\n      snapshotToCleanup: Snapshot,\n      catalogTableOpt: Option[CatalogTable]): Unit = {\n    if (enableExpiredLogCleanup(unsafeVolatileSnapshot.metadata)) {\n      cleanUpExpiredLogs(snapshotToCleanup, catalogTableOpt)\n    }\n  }\n\n  /** Clean up expired delta and checkpoint logs. Exposed for testing. */\n  private[delta] def cleanUpExpiredLogs(\n      snapshotToCleanup: Snapshot,\n      catalogTableOpt: Option[CatalogTable] = None,\n      deltaRetentionMillisOpt: Option[Long] = None,\n      cutoffTruncationGranularity: TruncationGranularity = DAY): Unit = {\n    recordDeltaOperation(this, \"delta.log.cleanup\") {\n      val retentionMillis =\n        deltaRetentionMillisOpt.getOrElse(deltaRetentionMillis(unsafeVolatileSnapshot.metadata))\n      val fileCutOffTime =\n        truncateDate(clock.getTimeMillis() - retentionMillis, cutoffTruncationGranularity).getTime\n      val formattedDate = fileCutOffTime.toGMTString\n      logInfo(\n        log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] \" +\n        log\"Starting the deletion of log files older than ${MDC(DeltaLogKeys.DATE, formattedDate)}\")\n\n      if (!metadataCleanupAllowed(snapshotToCleanup, fileCutOffTime.getTime)) {\n        logInfo(\"Metadata cleanup was skipped due to not satisfying the requirements \" +\n          \"of CheckpointProtectionTableFeature.\")\n        return\n      }\n\n      val fs = logPath.getFileSystem(newDeltaHadoopConf())\n      var numDeleted = 0\n      val expiredDeltaLogs = listExpiredDeltaLogs(fileCutOffTime.getTime)\n      if (expiredDeltaLogs.hasNext) {\n        // Trigger compatibility checkpoint creation logic only when this round of metadata cleanup\n        // is going to delete any deltas/checkpoint files.\n        // We need to create compat checkpoint before deleting delta/checkpoint files so that we\n        // don't have a window in b/w where the old checkpoint is deleted and there is no\n        // compat-checkpoint available.\n        val v2CompatCheckpointMetrics = new V2CompatCheckpointMetrics\n        createSinglePartCheckpointForBackwardCompat(snapshotToCleanup, v2CompatCheckpointMetrics)\n        logInfo(log\"Compatibility checkpoint creation metrics: \" +\n          log\"${MDC(DeltaLogKeys.METRICS, v2CompatCheckpointMetrics)}\")\n      }\n      var wasCheckpointDeleted = false\n      var maxBackfilledVersionDeleted = -1L\n      expiredDeltaLogs.map(_.getPath).foreach { path =>\n        // recursive = false\n        if (fs.delete(path, false)) {\n          numDeleted += 1\n          if (FileNames.isCheckpointFile(path)) {\n            wasCheckpointDeleted = true\n          }\n          if (FileNames.isDeltaFile(path)) {\n            maxBackfilledVersionDeleted =\n              Math.max(maxBackfilledVersionDeleted, FileNames.deltaVersion(path))\n          }\n        }\n      }\n      val commitDirPath = FileNames.commitDirPath(logPath)\n      // Commit Directory might not exist on tables created in older versions and\n      // never updated since.\n      val expiredUnbackfilledDeltaLogs: Iterator[FileStatus] =\n        if (fs.exists(commitDirPath)) {\n          store\n            .listFrom(listingPrefix(commitDirPath, 0), newDeltaHadoopConf())\n            .takeWhile { case UnbackfilledDeltaFile(_, fileVersion, _) =>\n              fileVersion <= maxBackfilledVersionDeleted\n            }\n        } else {\n          Iterator.empty\n        }\n      val numDeletedUnbackfilled = expiredUnbackfilledDeltaLogs.count(\n        log => fs.delete(log.getPath, false))\n      if (wasCheckpointDeleted) {\n        // Trigger sidecar deletion only when some checkpoints have been deleted as part of this\n        // round of Metadata cleanup.\n        val sidecarDeletionMetrics = new SidecarDeletionMetrics\n        identifyAndDeleteUnreferencedSidecarFiles(\n          snapshotToCleanup,\n          fileCutOffTime.getTime,\n          sidecarDeletionMetrics)\n        logInfo(log\"Sidecar deletion metrics: ${MDC(DeltaLogKeys.METRICS, sidecarDeletionMetrics)}\")\n      }\n      logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] \" +\n        log\"Deleted ${MDC(DeltaLogKeys.NUM_FILES, numDeleted.toLong)} log files and \" +\n        log\"${MDC(DeltaLogKeys.NUM_FILES2, numDeletedUnbackfilled.toLong)} unbackfilled commit \" +\n        log\"files older than ${MDC(DeltaLogKeys.DATE, formattedDate)}\")\n    }\n  }\n\n  /** Helper function for getting the version of a checkpoint or a commit. */\n  def getDeltaFileChecksumOrCheckpointVersion(filePath: Path): Long = {\n    require(isCheckpointFile(filePath) || isDeltaFile(filePath) || isChecksumFile(filePath))\n    getFileVersion(filePath)\n  }\n\n  /**\n   * Returns an iterator of expired delta logs that can be cleaned up. For a delta log to be\n   * considered as expired, it must:\n   *  - have a checkpoint file after it\n   *  - be older than `fileCutOffTime`\n   */\n  private def listExpiredDeltaLogs(fileCutOffTime: Long): Iterator[FileStatus] = {\n    val latestCheckpoint = readLastCheckpointFile()\n    if (latestCheckpoint.isEmpty) return Iterator.empty\n    val threshold = latestCheckpoint.get.version - 1L\n    val files = store.listFrom(listingPrefix(logPath, 0), newDeltaHadoopConf())\n      .filter(f => isCheckpointFile(f) || isDeltaFile(f) || isChecksumFile(f))\n\n    new BufferingLogDeletionIterator(\n      files, fileCutOffTime, threshold, getDeltaFileChecksumOrCheckpointVersion)\n  }\n\n  protected def checkpointExistsAtCleanupBoundary(deltaLog: DeltaLog, version: Long): Boolean = {\n    if (spark.conf.get(DeltaSQLConf.ALLOW_METADATA_CLEANUP_CHECKPOINT_EXISTENCE_CHECK_DISABLED)) {\n      return false\n    }\n\n    val upperBoundVersion = Some(CheckpointInstance(version = version + 1))\n    deltaLog\n      .findLastCompleteCheckpointBefore(upperBoundVersion)\n      .exists(_.version == version)\n  }\n\n  /**\n   * Validates whether the metadata cleanup adheres to the CheckpointProtectionTableFeature\n   * requirements. Metadata cleanup is only allowed if we can cleanup everything before\n   * requireCheckpointProtectionBeforeVersion. If this is not possible, we can still cleanup\n   * if there is already a checkpoint at the cleanup boundary version.\n   *\n   * If none of the invariants above are satisfied, we validate whether we support all\n   * protocols in the commit range we are planning to delete. If we encounter an unsupported\n   * protocol we skip the cleanup.\n   */\n  private def metadataCleanupAllowed(\n      snapshot: Snapshot,\n      fileCutOffTime: Long): Boolean = {\n    def expandVersionRange(currentRange: VersionRange, versionToCover: Long): VersionRange =\n      versionRange(currentRange.start.min(versionToCover), currentRange.end.max(versionToCover))\n\n    val checkpointProtectionVersion =\n      CheckpointProtectionTableFeature.getCheckpointProtectionVersion(snapshot)\n    if (checkpointProtectionVersion <= 0) return true\n\n    val expiredDeltaLogs = listExpiredDeltaLogs(fileCutOffTime)\n    if (expiredDeltaLogs.isEmpty) return true\n\n    val deltaLog = snapshot.deltaLog\n    val toCleanVersionRange = expiredDeltaLogs\n      .filter(isDeltaFile)\n      .collect { case DeltaFile(_, version) => version }\n      // Stop early if we cannot cleanup beyond the checkpointProtectionVersion.\n      // We include equality for the CheckpointProtection invariant check below.\n      // Assumes commit versions are continuous.\n      .takeWhile { _ <= checkpointProtectionVersion - 1 }\n      .foldLeft(versionRange(Long.MaxValue, 0L))(expandVersionRange)\n\n    // CheckpointProtectionTableFeature main invariant.\n    if (toCleanVersionRange.end >= checkpointProtectionVersion - 1) return true\n    // If we cannot delete until the checkpoint protection version. Check if a checkpoint already\n    // exists at the cleanup boundary. If it does, it is safe to clean up to the boundary.\n    if (checkpointExistsAtCleanupBoundary(deltaLog, toCleanVersionRange.end + 1L)) return true\n\n    // If the CheckpointProtectionTableFeature invariants do not hold, we must support all\n    // protocols for commits that we are cleaning up. Also, we have to support the first\n    // commit that we retain, because we will be creating a new checkpoint for that commit.\n    allProtocolsSupported(\n      deltaLog,\n      versionRange(toCleanVersionRange.start, toCleanVersionRange.end + 1L))\n  }\n\n  /**\n   * Validates whether the client supports read for all the protocols in the provided checksums\n   * as well as write for `versionThatRequiresWriteSupport`.\n   *\n   * @param deltaLog The log of the delta table.\n   * @param checksumsToValidate An iterator with the checksum files we need to validate. The client\n   *                            needs read support for all the encountered protocols.\n   * @param versionThatRequiresWriteSupport The version the client needs write support. This\n   *                                        is the version we are creating a new checkpoint.\n   * @param expectedChecksumFileCount The expected number of checksum files. If the iterator\n   *                                  contains less files, the function returns false.\n   * @return Returns false if there is a non-supported or null protocol in the provided checksums.\n   *         Returns true otherwise.\n   */\n  protected[delta] def allProtocolsSupported(\n      deltaLog: DeltaLog,\n      checksumsToValidate: Iterator[FileStatus],\n      versionThatRequiresWriteSupport: Long,\n      expectedChecksumFileCount: Long): Boolean = {\n    if (!spark.conf.get(DeltaSQLConf.ALLOW_METADATA_CLEANUP_WHEN_ALL_PROTOCOLS_SUPPORTED)) {\n      return false\n    }\n\n    val schemaToUse = Action.logSchema(Set(\"protocol\"))\n    val supportedForRead = DeltaUDF.booleanFromProtocol(_.supportedForRead())(col(\"protocol\"))\n    val supportedForWrite = DeltaUDF.booleanFromProtocol(_.supportedForWrite())(col(\"protocol\"))\n    val supportedForReadAndWrite = supportedForRead && supportedForWrite\n    val supported =\n      when(col(\"version\") === lit(versionThatRequiresWriteSupport), supportedForReadAndWrite)\n      .otherwise(supportedForRead)\n\n    val fileIndexOpt =\n      DeltaLogFileIndex(DeltaLogFileIndex.CHECKSUM_FILE_FORMAT, checksumsToValidate.toSeq)\n    val fileIndexSupportedOpt = fileIndexOpt.map { index =>\n      if (index.inputFiles.length != expectedChecksumFileCount) return false\n\n      deltaLog\n        .loadIndex(index, schemaToUse)\n        // If we find any CRC with no protocol definition we need to abort.\n        .filter(isnull(col(\"protocol\")) || not(supported))\n        .take(1)\n        .isEmpty\n    }\n    fileIndexSupportedOpt.getOrElse(true)\n  }\n\n  protected[delta] def allProtocolsSupported(\n      deltaLog: DeltaLog,\n      versionRange: VersionRange): Boolean = {\n    // We only expect back filled commits in the range.\n    val checksumsToValidate = deltaLog\n      .listFrom(versionRange.start)\n      .collect { case ChecksumFile(fileStatus, version) => (fileStatus, version) }\n      .takeWhile { case (_, version) => version <= versionRange.end }\n      .map { case (fileStatus, _) => fileStatus }\n\n    allProtocolsSupported(\n      deltaLog,\n      checksumsToValidate,\n      versionThatRequiresWriteSupport = versionRange.end,\n      expectedChecksumFileCount = versionRange.end - versionRange.start + 1)\n  }\n\n  /**\n   * Truncates a timestamp down to a given unit. The unit can be either DAY, HOUR or MINUTE.\n   * - DAY: The timestamp it truncated to the previous midnight.\n   * - HOUR: The timestamp it truncated to the last hour.\n   * - MINUTE: The timestamp it truncated to the last minute.\n   */\n  private[delta] def truncateDate(timeMillis: Long, unit: TruncationGranularity): Calendar = {\n    val date = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"))\n    date.setTimeInMillis(timeMillis)\n\n    val calendarUnit = unit match {\n      case DAY => Calendar.DAY_OF_MONTH\n      case HOUR => Calendar.HOUR_OF_DAY\n      case MINUTE => Calendar.MINUTE\n    }\n\n    DateUtils.truncate(date, calendarUnit)\n  }\n\n  /** Truncates a timestamp down to the previous midnight and returns the time. */\n  private[delta] def truncateDay(timeMillis: Long): Calendar = {\n    truncateDate(timeMillis, TruncationGranularity.DAY)\n  }\n\n  /**\n   * Helper method to create a compatibility classic single file checkpoint file for this table.\n   * This is needed so that any legacy reader which do not understand [[V2CheckpointTableFeature]]\n   * could read the legacy classic checkpoint file and fail gracefully with Protocol requirement\n   * failure.\n   */\n  protected[delta] def createSinglePartCheckpointForBackwardCompat(\n      snapshotToCleanup: Snapshot,\n      metrics: V2CompatCheckpointMetrics): Unit = {\n    // Do nothing if this table does not use V2 Checkpoints, or has no checkpoints at all.\n    if (!CheckpointProvider.isV2CheckpointEnabled(snapshotToCleanup)) return\n    if (snapshotToCleanup.checkpointProvider.isEmpty) return\n\n    val startTimeMs = System.currentTimeMillis()\n    val hadoopConf = newDeltaHadoopConf()\n    val checkpointInstance =\n      CheckpointInstance(snapshotToCleanup.checkpointProvider.topLevelFiles.head.getPath)\n    // The current checkpoint provider is already using a checkpoint with the naming\n    // scheme of classic checkpoints. There is no need to create a compatibility checkpoint\n    // in this case.\n    if (checkpointInstance.format != CheckpointInstance.Format.V2) return\n\n    val checkpointVersion = snapshotToCleanup.checkpointProvider.version\n    val checkpoints = listFrom(checkpointVersion)\n      .takeWhile(file => FileNames.getFileVersionOpt(file.getPath).exists(_ <= checkpointVersion))\n      .collect {\n        case file if FileNames.isCheckpointFile(file) => CheckpointInstance(file.getPath)\n      }\n      .filter(_.format != CheckpointInstance.Format.V2)\n      .toArray\n    val availableNonV2Checkpoints =\n      getLatestCompleteCheckpointFromList(checkpoints, Some(checkpointVersion))\n    if (availableNonV2Checkpoints.nonEmpty) {\n      metrics.v2CheckpointCompatLogicTimeTakenMs = System.currentTimeMillis() - startTimeMs\n      return\n    }\n\n    // topLevelFileIndex must be non-empty when topLevelFiles are present\n    val shallowCopyDf =\n      loadIndex(snapshotToCleanup.checkpointProvider.topLevelFileIndex.get, Action.logSchema)\n    val finalPath =\n      FileNames.checkpointFileSingular(snapshotToCleanup.deltaLog.logPath, checkpointVersion)\n    Checkpoints.createCheckpointV2ParquetFile(\n      spark,\n      shallowCopyDf,\n      finalPath,\n      hadoopConf,\n      useRename = false)\n    metrics.v2CheckpointCompatLogicTimeTakenMs = System.currentTimeMillis() - startTimeMs\n    metrics.checkpointVersion = checkpointVersion\n  }\n\n  /** Deletes any unreferenced files from the sidecar directory `_delta_log/_sidecar` */\n  protected def identifyAndDeleteUnreferencedSidecarFiles(\n      snapshotToCleanup: Snapshot,\n      checkpointRetention: Long,\n      metrics: SidecarDeletionMetrics): Unit = {\n    val startTimeMs = System.currentTimeMillis()\n    // If v2 checkpoints are not enabled on the table, we don't need to attempt the sidecar cleanup.\n    if (!CheckpointProvider.isV2CheckpointEnabled(snapshotToCleanup)) return\n\n    val hadoopConf = newDeltaHadoopConf()\n    val fs = sidecarDirPath.getFileSystem(hadoopConf)\n    // This can happen when the V2 Checkpoint feature is present in the Protocol but\n    // only Classic checkpoints have been created for the table.\n    if (!fs.exists(sidecarDirPath)) return\n\n    val (parquetCheckpointFiles, otherFiles) = store\n      .listFrom(listingPrefix(logPath, 0), hadoopConf)\n      .collect { case CheckpointFile(status, _) => (status, CheckpointInstance(status.getPath)) }\n      .collect { case (fileStatus, ci) if ci.format.usesSidecars => fileStatus }\n      .toSeq\n      .partition(_.getPath.getName.endsWith(\"parquet\"))\n    val (jsonCheckpointFiles, unknownFormatCheckpointFiles) =\n      otherFiles.partition(_.getPath.getName.endsWith(\"json\"))\n    if (unknownFormatCheckpointFiles.nonEmpty) {\n      logWarning(\n        log\"Found checkpoint files other than parquet and json: \" +\n        log\"${MDC(DeltaLogKeys.PATHS,\n          unknownFormatCheckpointFiles.map(_.getPath.toString).mkString(\",\"))}\")\n    }\n    metrics.numActiveParquetCheckpointFiles = parquetCheckpointFiles.size\n    metrics.numActiveJsonCheckpointFiles = jsonCheckpointFiles.size\n    val parquetCheckpointsFileIndex =\n      DeltaLogFileIndex(DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_PARQUET, parquetCheckpointFiles)\n    val jsonCheckpointsFileIndex =\n      DeltaLogFileIndex(DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_JSON, jsonCheckpointFiles)\n    val identifyActiveSidecarsStartTimeMs = System.currentTimeMillis()\n    metrics.activeCheckpointsListingTimeTakenMs = identifyActiveSidecarsStartTimeMs - startTimeMs\n    import org.apache.spark.sql.delta.implicits._\n    val df = (parquetCheckpointsFileIndex ++ jsonCheckpointsFileIndex)\n      .map(loadIndex(_, Action.logSchema(Set(\"sidecar\"))))\n      .reduceOption(_ union _)\n      .getOrElse { return }\n\n    val activeSidecarFiles = df\n      .select(\"sidecar.path\")\n      .where(\"path is not null\")\n      .as[String]\n      .collect()\n      .map(p => new Path(p).getName) // Get bare file names\n      .toSet\n\n    val identifyAndDeleteSidecarsStartTimeMs = System.currentTimeMillis()\n    metrics.identifyActiveSidecarsTimeTakenMs =\n      identifyAndDeleteSidecarsStartTimeMs - identifyActiveSidecarsStartTimeMs\n    // Retain all files created in the checkpoint retention window - irrespective of whether they\n    // are referenced in a checkpoint or not. This is to make sure that we don't end up deleting an\n    // in-progress checkpoint.\n    val retentionTimestamp: Long = checkpointRetention\n    val sidecarFilesIterator = new Iterator[FileStatus] {\n      // Hadoop's RemoteIterator is neither java nor scala Iterator, so have to wrap it\n      val remoteIterator = fs.listStatusIterator(sidecarDirPath)\n      override def hasNext: Boolean = remoteIterator.hasNext()\n      override def next(): FileStatus = remoteIterator.next()\n    }\n    val sidecarFilesToDelete = sidecarFilesIterator\n      .collect { case file if file.getModificationTime < retentionTimestamp => file.getPath }\n      .filterNot(path => activeSidecarFiles.contains(path.getName))\n    val sidecarDeletionStartTimeMs = System.currentTimeMillis()\n    logInfo(\n      log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] \" +\n      log\"Starting the deletion of unreferenced sidecar files\")\n    val count = deleteMultiple(fs, sidecarFilesToDelete)\n\n    logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] Deleted \" +\n      log\"${MDC(DeltaLogKeys.COUNT, count)} sidecar files\")\n    metrics.numSidecarFilesDeleted = count\n    val endTimeMs = System.currentTimeMillis()\n    metrics.identifyAndDeleteSidecarsTimeTakenMs =\n      sidecarDeletionStartTimeMs - identifyAndDeleteSidecarsStartTimeMs\n    metrics.overallSidecarProcessingTimeTakenMs = endTimeMs - startTimeMs\n  }\n\n  private def deleteMultiple(fs: FileSystem, paths: Iterator[Path]): Long = {\n      paths.map { path =>\n        if (fs.delete(path, false)) 1L else 0L\n      }.sum\n  }\n\n  /** Class to track metrics related to V2 Checkpoint Sidecars deletion. */\n  protected class SidecarDeletionMetrics {\n    // number of sidecar files deleted\n    var numSidecarFilesDeleted: Long = -1\n    // number of active parquet checkpoint files present in delta log directory\n    var numActiveParquetCheckpointFiles: Long = -1\n    // number of active json checkpoint files present in delta log directory\n    var numActiveJsonCheckpointFiles: Long = -1\n    // time taken (in ms) to list and identify active checkpoints\n    var activeCheckpointsListingTimeTakenMs: Long = -1\n    // time taken (in ms) to list the sidecar directory to get all sidecars and delete those which\n    // aren't referenced by any checkpoint anymore\n    var identifyAndDeleteSidecarsTimeTakenMs: Long = -1\n    // time taken (in ms) to read the active checkpoint json / parquet files and identify active\n    // sidecar files\n    var identifyActiveSidecarsTimeTakenMs: Long = -1\n    // time taken (in ms) for everything related to sidecar processing\n    var overallSidecarProcessingTimeTakenMs: Long = -1\n  }\n\n  /** Class to track metrics related to V2 Compatibility checkpoint creation. */\n  protected[delta] class V2CompatCheckpointMetrics {\n    // time taken (in ms) to run the v2 checkpoint compat logic\n    var v2CheckpointCompatLogicTimeTakenMs: Long = -1\n\n    // the version at which we have created a v2 compat checkpoint, -1 if no compat checkpoint was\n    // created.\n    var checkpointVersion: Long = -1\n  }\n\n  /**\n   * Finds a checkpoint such that we are able to construct table snapshot for all versions at or\n   * greater than the checkpoint version returned.\n   */\n  def findEarliestReliableCheckpoint: Option[Long] = {\n    val hadoopConf = newDeltaHadoopConf()\n    var earliestCheckpointVersionOpt: Option[Long] = None\n    // This is used to collect the checkpoint files from the current version that we are listing.\n    // When we list a file that is not part of the checkpoint, then we must have seen the entire\n    // checkpoint. We then verify if the checkpoint was complete, and if it is not, we clear the\n    // collection and wait for the next checkpoint to appear in the file listing.\n    // Whenever we see a complete checkpoint for the first time, we remember it as the earliest\n    // checkpoint.\n    val currentCheckpointFiles = ArrayBuffer.empty[Path]\n    var prevCommitVersion = 0L\n\n    def currentCheckpointVersionOpt: Option[Long] =\n      currentCheckpointFiles.headOption.map(checkpointVersion(_))\n\n    def isCurrentCheckpointComplete: Boolean = {\n      val instances = currentCheckpointFiles.map(CheckpointInstance(_)).toArray\n      getLatestCompleteCheckpointFromList(instances).isDefined\n    }\n\n    // Iterate logs files in ascending order to find the earliest reliable checkpoint, for the same\n    // version, checkpoint is always processed before commit so that we can identify the candidate\n    // checkpoint first and then verify commits since the candidate's version (inclusive)\n    store.listFrom(listingPrefix(logPath, 0L), hadoopConf)\n      .map(_.getPath)\n      .foreach {\n        case CheckpointFile(f, checkpointVersion)\n          // Invalidate the candidate if we observe missing commits before the current checkpoint.\n          // the incoming commit will invalidate the candidate as well, but then we miss the current\n          // checkpoint, which is also a valid candidate.\n          if earliestCheckpointVersionOpt.isEmpty || checkpointVersion > prevCommitVersion + 1 =>\n            earliestCheckpointVersionOpt = None\n            if (!currentCheckpointVersionOpt.contains(checkpointVersion)) {\n              // If it's a different checkpoint, clear the existing one.\n              currentCheckpointFiles.clear()\n            }\n            currentCheckpointFiles += f\n        case DeltaFile(_, deltaVersion) =>\n          if (earliestCheckpointVersionOpt.isEmpty && isCurrentCheckpointComplete) {\n            // We have found a complete checkpoint, but we should not stop here. If a future\n            // commit version is missing, then this checkpoint will be discarded and we will need\n            // to restart the search from that point.\n\n            // Ensure that the commit json is there at the checkpoint version. If it's not there,\n            // we don't consider such a checkpoint as a reliable checkpoint.\n            if (currentCheckpointVersionOpt.contains(deltaVersion)) {\n              earliestCheckpointVersionOpt = currentCheckpointVersionOpt\n              prevCommitVersion = deltaVersion\n            }\n          }\n          // Need to clear it so that if there is a gap in commit versions, we are forced to\n          // look for a new complete checkpoint.\n          currentCheckpointFiles.clear()\n          if (deltaVersion > prevCommitVersion + 1) {\n            // Missing commit versions. Restart the search.\n            earliestCheckpointVersionOpt = None\n          }\n          prevCommitVersion = deltaVersion\n        case _ =>\n      }\n\n    earliestCheckpointVersionOpt\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/NumRecordsStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, RemoveFile}\nimport org.apache.spark.sql.util.ScalaExtensions._\n\n/**\n * Container class for statistics related to number of records in a Delta commit.\n */\ncase class NumRecordsStats (\n    // Number of logical records in AddFile actions with numRecords.\n    numLogicalRecordsAddedPartial: Long,\n    // Number of logical records in RemoveFile actions with numRecords.\n    numLogicalRecordsRemovedPartial: Long,\n    numDeletionVectorRecordsAdded: Long,\n    numDeletionVectorRecordsRemoved: Long,\n    numFilesAddedWithoutNumRecords: Long,\n    numFilesRemovedWithoutNumRecords: Long,\n    numLogicalRecordsAddedInFilesWithDeletionVectorsPartial: Long) {\n\n  def allFilesHaveNumRecords: Boolean =\n    numFilesAddedWithoutNumRecords == 0 && numFilesRemovedWithoutNumRecords == 0\n\n  /**\n   * The number of logical records in all AddFile actions or None if any file does not contain\n   * statistics.\n   */\n  def numLogicalRecordsAdded: Option[Long] = Option.when(numFilesAddedWithoutNumRecords == 0)(\n    numLogicalRecordsAddedPartial)\n\n  /**\n   * The number of logical records in all RemoveFile actions or None if any file does not contain\n   * statistics.\n   */\n  def numLogicalRecordsRemoved: Option[Long] = Option.when(numFilesRemovedWithoutNumRecords == 0)(\n    numLogicalRecordsRemovedPartial)\n\n  /**\n   * The number of logical records in all AddFile actions that have a deletion vector or None\n   * if any file does not contain statistics.\n   */\n  def numLogicalRecordsAddedInFilesWithDeletionVectors: Option[Long] =\n    Option.when(numFilesAddedWithoutNumRecords == 0)(\n      numLogicalRecordsAddedInFilesWithDeletionVectorsPartial)\n}\n\nobject NumRecordsStats {\n  def fromActions(actions: Seq[Action]): NumRecordsStats = {\n    var numFilesAdded = 0L\n    var numFilesRemoved = 0L\n    var numFilesAddedWithoutNumRecords = 0L\n    var numFilesRemovedWithoutNumRecords = 0L\n    var numLogicalRecordsAddedPartial: Long = 0L\n    var numLogicalRecordsRemovedPartial: Long = 0L\n    var numDeletionVectorRecordsAdded = 0L\n    var numDeletionVectorRecordsRemoved = 0L\n    var numLogicalRecordsAddedInFilesWithDeletionVectorsPartial = 0L\n\n    actions.foreach {\n      case a: AddFile =>\n        numFilesAdded += 1\n        numLogicalRecordsAddedPartial += a.numLogicalRecords.getOrElse {\n          numFilesAddedWithoutNumRecords += 1\n          0L\n        }\n        numDeletionVectorRecordsAdded += a.numDeletedRecords\n        if (a.deletionVector != null) {\n          numLogicalRecordsAddedInFilesWithDeletionVectorsPartial +=\n            a.numLogicalRecords.getOrElse(0L)\n        }\n      case r: RemoveFile =>\n        numFilesRemoved += 1\n        numLogicalRecordsRemovedPartial += r.numLogicalRecords.getOrElse {\n          numFilesRemovedWithoutNumRecords += 1\n          0L\n        }\n        numDeletionVectorRecordsRemoved += r.numDeletedRecords\n      case _ =>\n        // Do nothing\n    }\n    NumRecordsStats(\n      numLogicalRecordsAddedPartial = numLogicalRecordsAddedPartial,\n      numLogicalRecordsRemovedPartial = numLogicalRecordsRemovedPartial,\n      numDeletionVectorRecordsAdded = numDeletionVectorRecordsAdded,\n      numDeletionVectorRecordsRemoved = numDeletionVectorRecordsRemoved,\n      numFilesAddedWithoutNumRecords = numFilesAddedWithoutNumRecords,\n      numFilesRemovedWithoutNumRecords = numFilesRemovedWithoutNumRecords,\n      numLogicalRecordsAddedInFilesWithDeletionVectorsPartial =\n        numLogicalRecordsAddedInFilesWithDeletionVectorsPartial\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/OptimisticTransaction.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.{ConcurrentModificationException, Optional, UUID}\nimport java.util.concurrent.TimeUnit.{MINUTES, NANOSECONDS}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.collection.mutable.{ArrayBuffer, HashSet}\nimport scala.util.control.NonFatal\n\nimport com.databricks.spark.util.TagDefinitions.TAG_LOG_STORE_CLASS\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaOperations.{ChangeColumn, ChangeColumns, CreateTable, Operation, ReplaceColumns, ReplaceTable, UpdateSchema}\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.constraints.{Constraints, Invariants}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CoordinatedCommitsUtils, TableCommitCoordinatorClient, UCCommitCoordinatorBuilder}\nimport org.apache.spark.sql.delta.files._\nimport org.apache.spark.sql.delta.hooks.{CheckpointHook, ChecksumHook, GenerateSymlinkManifest, HudiConverterHook, IcebergConverterHook, PostCommitHook, UpdateCatalogFactory}\nimport org.apache.spark.sql.delta.implicits.addFileEncoder\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.redirect.{RedirectFeature, TableRedirectConfiguration}\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.stats._\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, JsonUtils, TransactionHelper}\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport io.delta.storage.commit._\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\nimport org.apache.commons.lang3.NotImplementedException\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.internal.{MDC, MessageWithContext}\nimport org.apache.spark.sql.{AnalysisException, Column, DataFrame, SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical.UnsetTableProperties\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.catalyst.util.{CharVarcharUtils, ResolveDefaultColumns}\nimport org.apache.spark.sql.delta.clustering.ClusteringMetadataDomain\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{StructField, StructType}\nimport org.apache.spark.util.{Clock, Utils}\n\nobject CoordinatedCommitType extends Enumeration {\n  type CoordinatedCommitType = Value\n  val FS_COMMIT, CC_COMMIT, CO_COMMIT,\n    FS_TO_CC_UPGRADE_COMMIT, FS_TO_CO_UPGRADE_COMMIT, CC_TO_FS_DOWNGRADE_COMMIT = Value\n}\n\ncase class CoordinatedCommitsStats(\n  coordinatedCommitsType: String,\n  commitCoordinatorName: String,\n  commitCoordinatorConf: Map[String, String])\n\n/** Record metrics about a successful commit. */\ncase class CommitStats(\n  /** The version read by the txn when it starts. */\n  startVersion: Long,\n  /** The version committed by the txn. */\n  commitVersion: Long,\n  /** The version read by the txn right after it commits. It usually equals to commitVersion,\n   * but can be larger than commitVersion when there are concurrent commits. */\n  readVersion: Long,\n  txnDurationMs: Long,\n  commitDurationMs: Long,\n  fsWriteDurationMs: Long,\n  stateReconstructionDurationMs: Long,\n  numAdd: Int,\n  numRemove: Int,\n  /** The number of [[SetTransaction]] actions in the committed actions. */\n  numSetTransaction: Int,\n  bytesNew: Long,\n  /** The number of files in the table as of version `readVersion`. */\n  numFilesTotal: Long,\n  /** The table size in bytes as of version `readVersion`. */\n  sizeInBytesTotal: Long,\n  /** The number and size of CDC files added in this operation. */\n  numCdcFiles: Long,\n  cdcBytesNew: Long,\n  /** The protocol as of version `readVersion`. */\n  protocol: Protocol,\n  /** The size of the newly committed (usually json) file */\n  commitSizeBytes: Long,\n  /** The size of the checkpoint committed, if present */\n  checkpointSizeBytes: Long,\n  totalCommitsSizeSinceLastCheckpoint: Long,\n  /** Will we attempt a checkpoint after this commit is completed */\n  checkpointAttempt: Boolean,\n  info: CommitInfo,\n  newMetadata: Option[Metadata],\n  numAbsolutePathsInAdd: Int,\n  numDistinctPartitionsInAdd: Int,\n  numPartitionColumnsInTable: Int,\n  isolationLevel: String,\n  coordinatedCommitsInfo: CoordinatedCommitsStats,\n  fileSizeHistogram: Option[FileSizeHistogram] = None,\n  addFilesHistogram: Option[FileSizeHistogram] = None,\n  removeFilesHistogram: Option[FileSizeHistogram] = None,\n  numOfDomainMetadatas: Long = 0,\n  txnId: Option[String] = None\n)\n\n/**\n * Represents the partition and data predicates of a query on a Delta table.\n *\n * Partition predicates can either reference the table's logical partition columns, or the\n * physical [[AddFile]]'s schema. When a predicate refers to the logical partition columns it needs\n * to be rewritten to be over the [[AddFile]]'s schema before filtering files. This is indicated\n * with shouldRewriteFilter=true.\n *\n * Currently the only path for a predicate with shouldRewriteFilter=false is through DPO\n * (dynamic partition overwrite) since we filter directly on [[AddFile.partitionValues]].\n *\n * For example, consider a table with the schema below and partition column \"a\"\n * |-- a: integer {physicalName = \"XX\"}\n * |-- b: integer {physicalName = \"YY\"}\n *\n * An example of a predicate that needs to be written is: (a = 0)\n * Before filtering the [[AddFile]]s, this predicate needs to be rewritten to:\n * (partitionValues.XX = 0)\n *\n * An example of a predicate that does not need to be rewritten is:\n * (partitionValues = Map(XX -> 0))\n */\nprivate[delta] case class DeltaTableReadPredicate(\n    partitionPredicates: Seq[Expression] = Seq.empty,\n    dataPredicates: Seq[Expression] = Seq.empty,\n    shouldRewriteFilter: Boolean = true) {\n\n  val partitionPredicate: Expression =\n    partitionPredicates.reduceLeftOption(And).getOrElse(Literal.TrueLiteral)\n}\n\n  /**\n * Used to perform a set of reads in a transaction and then commit a set of updates to the\n * state of the log.  All reads from the [[DeltaLog]], MUST go through this instance rather\n * than directly to the [[DeltaLog]] otherwise they will not be check for logical conflicts\n * with concurrent updates.\n *\n * This class is not thread-safe.\n *\n * @param deltaLog The Delta Log for the table this transaction is modifying.\n * @param snapshot The snapshot that this transaction is reading at.\n */\nclass OptimisticTransaction(\n    override val deltaLog: DeltaLog,\n    override val catalogTable: Option[CatalogTable],\n    override val snapshot: Snapshot)\n  extends OptimisticTransactionImpl\n  with DeltaLogging {\n  def this(\n      deltaLog: DeltaLog,\n      catalogTable: Option[CatalogTable],\n      snapshotOpt: Option[Snapshot] = None) =\n    this(\n      deltaLog,\n      catalogTable,\n      snapshotOpt.getOrElse(deltaLog.update(catalogTableOpt = catalogTable)))\n}\n\nobject CommitConflictFailure {\n  def unapply(e: Exception): Option[Exception] = e match {\n    case _: FileAlreadyExistsException => Some(e)\n    case e: CommitFailedException if e.getConflict => Some(e)\n    case _ => None\n  }\n}\n\nobject OptimisticTransaction {\n\n  private val active = new ThreadLocal[OptimisticTransaction]\n\n  /** Get the active transaction */\n  def getActive(): Option[OptimisticTransaction] = Option(active.get())\n\n  /**\n   * Runs the passed block of code with the given active transaction. This fails if a transaction is\n   * already active unless `overrideExistingTransaction` is set.\n   */\n  def withActive[T](\n      activeTransaction: OptimisticTransaction,\n      overrideExistingTransaction: Boolean = false)(block: => T): T = {\n    val original = getActive()\n    if (overrideExistingTransaction) {\n      clearActive()\n    }\n    setActive(activeTransaction)\n    try {\n      block\n    } finally {\n      clearActive()\n      if (original.isDefined) {\n        setActive(original.get)\n      }\n    }\n  }\n\n  /**\n   * Sets a transaction as the active transaction.\n   *\n   * @note This is not meant for being called directly, only from\n   *       `OptimisticTransaction.withNewTransaction`. Use that to create and set active txns.\n   */\n  private[delta] def setActive(txn: OptimisticTransaction): Unit = {\n    getActive() match {\n      case Some(activeTxn) =>\n        if (!(activeTxn eq txn)) {\n          throw DeltaErrors.activeTransactionAlreadySet()\n        }\n      case _ =>\n        active.set(txn)\n    }\n  }\n\n  /**\n   * Clears the active transaction as the active transaction.\n   *\n   * @note This is not meant for being called directly, `OptimisticTransaction.withNewTransaction`.\n   */\n  private[delta] def clearActive(): Unit = {\n    active.set(null)\n  }\n}\n\n/**\n * Used to perform a set of reads in a transaction and then commit a set of updates to the\n * state of the log.  All reads from the [[DeltaLog]], MUST go through this instance rather\n * than directly to the [[DeltaLog]] otherwise they will not be check for logical conflicts\n * with concurrent updates.\n *\n * This trait is not thread-safe.\n */\ntrait OptimisticTransactionImpl extends TransactionHelper\n  with TransactionalWrite\n  with SQLMetricsReporting\n  with DeltaScanGenerator\n  with RecordChecksum\n  with DeltaLogging {\n\n  import org.apache.spark.sql.delta.util.FileNames._\n\n  // Intentionally cache the values of these configs to ensure stable commit code path\n  // and avoid race conditions between committing and dynamic config changes.\n  protected val incrementalCommitEnabled = deltaLog.incrementalCommitEnabled\n  protected val shouldVerifyIncrementalCommit = deltaLog.shouldVerifyIncrementalCommit\n  protected val forcedChecksumValidationInterval =\n    spark.conf.get(DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_INTERVAL)\n  protected val forcedChecksumValidationMinTimeIntervalMinutes =\n    spark.conf.get(DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_MIN_TIME_INTERVAL_MINUTES)\n\n  def clock: Clock = deltaLog.clock\n\n  // This would be a quick operation if we already validated the checksum\n  // Otherwise, we should at least perform the validation here.\n  // NOTE: When incremental commits are enabled, skip validation unless it was specifically\n  // requested. This allows us to maintain test converage internally, while avoiding the extreme\n  // overhead of those checks in prod or benchmark settings.\n  if (!incrementalCommitEnabled || shouldVerifyIncrementalCommit) {\n    snapshot.validateChecksum(Map(\"context\" -> \"transactionInitialization\"))\n  } else if (\n      forcedChecksumValidationInterval >= 0 &&\n      snapshot.version - snapshot.checkpointProvider.version >= forcedChecksumValidationInterval) {\n    val fileToUseForFreshnessCheck = snapshot.checkpointProvider.topLevelFiles\n      .headOption\n      .getOrElse(snapshot.logSegment.deltas.head)\n    // If the table is very fast moving, the checkpoint could much longer than\n    // forcedChecksumValidationInterval to land. To avoid slowing down\n    // such tables, we skip validation if the checkpoint is fresh as per\n    // the modification time.\n    val skipValidationForFastMovingTable = {\n      val checkpointModificationTime = fileToUseForFreshnessCheck.getModificationTime\n      val currentTime = System.currentTimeMillis()\n      val timeGapMillis = Math.max(currentTime - checkpointModificationTime, 0L)\n      // Only force validation if checkpoint is older than the minimum time gap\n      timeGapMillis < MINUTES.toMillis(forcedChecksumValidationMinTimeIntervalMinutes)\n    }\n\n    if (\n      !skipValidationForFastMovingTable\n      ) {\n      snapshot.validateChecksum(\n        Map(\n          \"context\" -> \"forceValidateChecksumDueToStaleCheckpoint::transactionInitialization\",\n          \"currentVersion\" -> snapshot.version.toString,\n          \"checkpointVersion\" -> snapshot.checkpointProvider.version.toString,\n          \"forcedValidationInterval\" -> forcedChecksumValidationInterval.toString,\n          \"forcedValidationMinTimeGap\" -> forcedChecksumValidationMinTimeIntervalMinutes.toString\n        )\n      )\n    }\n  }\n\n  /** Tracks the appIds that have been seen by this transaction. */\n  protected val readTxn = new ArrayBuffer[String]\n\n  /**\n   * Tracks the data that could have been seen by recording the partition\n   * predicates by which files have been queried by this transaction.\n   */\n  protected val readPredicates =\n    new java.util.concurrent.ConcurrentLinkedQueue[DeltaTableReadPredicate]\n\n  /** Tracks specific files that have been seen by this transaction. */\n  protected val readFiles = new HashSet[AddFile]\n\n  /** Whether the whole table was read during the transaction. */\n  protected var readTheWholeTable = false\n\n  /** Tracks if this transaction has already committed. */\n  protected var committed: Option[CommittedTransaction] = None\n  def getCommitted: Option[CommittedTransaction] = committed\n\n  /** Contains the execution instrumentation set via thread-local. No-op by default. */\n  protected[delta] var executionObserver: TransactionExecutionObserver =\n    TransactionExecutionObserver.getObserver\n\n  /**\n   * Stores the updated metadata (if any) that will result from this txn.\n   *\n   * This is just one way to change metadata.\n   * New metadata can also be added during commit from actions.\n   * But metadata should *not* be updated via both paths.\n   */\n  protected var newMetadata: Option[Metadata] = None\n\n  /** Stores the updated protocol (if any) that will result from this txn. */\n  protected var newProtocol: Option[Protocol] = None\n\n  /** The transaction start time. */\n  private val txnStartNano = System.nanoTime()\n\n  override val snapshotToScan: Snapshot = snapshot\n\n  /**\n   * Tracks the first-access snapshots of other Delta logs read by this transaction.\n   * The snapshots are keyed by the log's unique id.\n   */\n  protected var readSnapshots = new java.util.concurrent.ConcurrentHashMap[(String, Path), Snapshot]\n\n  /** The transaction commit start time. */\n  protected var commitStartNano = -1L\n\n  /** The transaction commit end time. */\n  protected var commitEndNano = -1L;\n\n  protected var commitInfo: CommitInfo = _\n  def getCommitInfo: CommitInfo = commitInfo\n\n  /** Whether the txn should trigger a checkpoint after the commit */\n  private[delta] var needsCheckpoint = false\n\n  // Whether this transaction is creating a new table.\n  private var isCreatingNewTable: Boolean = false\n\n  // Whether this transaction is overwriting the existing schema (i.e. overwriteSchema = true).\n  // When overwriting schema (and data) of a table, `isCreatingNewTable` should not be true,\n  // except for config REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE is set to true.\n  private var isOverwritingSchema: Boolean = false\n\n  // Whether this is a transaction that can select any new protocol, potentially downgrading\n  // the existing protocol of the table during REPLACE table operations.\n  private def canAssignAnyNewProtocol: Boolean =\n    readVersion == -1 ||\n      (isCreatingNewTable && spark.conf.get(DeltaSQLConf.REPLACE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED))\n\n  /**\n   * Tracks the start time since we started trying to write a particular commit.\n   * Used for logging duration of retried transactions.\n   */\n  protected var commitAttemptStartTimeMillis: Long = _\n\n  /**\n   * Tracks actions within the transaction, will commit along with the passed-in actions in the\n   * commit function.\n   */\n  protected val actions = new ArrayBuffer[Action]\n\n  /**\n   * Record a SetTransaction action that will be committed as part of this transaction.\n   */\n  def updateSetTransaction(appId: String, version: Long, lastUpdate: Option[Long]): Unit = {\n    actions += SetTransaction(appId, version, lastUpdate)\n  }\n\n  /** The version that this transaction is reading from. */\n  def readVersion: Long = snapshot.version\n\n  /** Creates new metadata with global Delta configuration defaults. */\n  private def withGlobalConfigDefaults(metadata: Metadata): Metadata = {\n    val isActiveReplaceCommand = isCreatingNewTable && readVersion != -1\n    val shouldUnsetCatalogOwnedConf =\n      isActiveReplaceCommand && CatalogOwnedTableUtils.defaultCatalogOwnedEnabled(spark)\n    val conf = if (shouldUnsetCatalogOwnedConf) {\n      // Unset default CatalogOwned enablement iff:\n      // 0. `isCreatingNewTable` indicates that this either is a REPLACE or CREATE command.\n      // 1. `readVersion != 1` indicates the table already exists.\n      //    - 0) and 1) suggest that this is an active REPLACE command.\n      // 2. Default CC enablement is set in the spark conf.\n      // This prevents any unintended modifications to the `newProtocol`.\n      // E.g., [[CatalogOwnedTableFeature]] and its dependent features\n      //       [[InCommitTimestampTableFeature]] & [[VacuumProtocolCheckTableFeature]].\n      //\n      // Note that this does *not* affect global spark conf state as we are modifying\n      // the copy of `spark.sessionState.conf`. Thus, `defaultCatalogOwnedFeatureEnabledKey`\n      // will remain unchanged for any concurrent operations that use the same SparkSession.\n      val defaultCatalogOwnedFeatureEnabledKey =\n        TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature)\n      // Isolate the spark conf to be used in the subsequent [[DeltaConfigs.mergeGlobalConfigs]]\n      // by cloning the existing configuration.\n      // Note: [[SQLConf.clone]] is already atomic so no extra synchronization is needed.\n      val clonedConf = spark.sessionState.conf.clone()\n      // Unset default CC conf on the cloned spark conf.\n      clonedConf.unsetConf(defaultCatalogOwnedFeatureEnabledKey)\n      clonedConf\n    } else {\n      spark.sessionState.conf\n    }\n    metadata.copy(configuration = DeltaConfigs.mergeGlobalConfigs(\n      conf, metadata.configuration))\n  }\n\n  protected val postCommitHooks = new ArrayBuffer[PostCommitHook]()\n  registerPostCommitHook(ChecksumHook)\n  catalogTable.foreach { ct =>\n    registerPostCommitHook(UpdateCatalogFactory.getUpdateCatalogHook(ct, spark))\n  }\n  // The CheckpointHook will only checkpoint if necessary, so always register it to run.\n  registerPostCommitHook(CheckpointHook)\n  registerPostCommitHook(HudiConverterHook)\n\n  /** The protocol of the snapshot that this transaction is reading at. */\n  def protocol: Protocol = newProtocol.getOrElse(snapshot.protocol)\n\n  /** Start time of txn in nanoseconds */\n  def txnStartTimeNs: Long = txnStartNano\n\n  /** Unique identifier for the transaction */\n  val txnId: String = UUID.randomUUID().toString\n\n  /** Whether to check unsupported data type when updating the table schema */\n  protected var checkUnsupportedDataType: Boolean =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SCHEMA_TYPE_CHECK)\n\n  // An array of tuples where each tuple represents a pair (colName, newHighWatermark).\n  // This is collected after a write into Delta table with IDENTITY columns. If it's not\n  // empty, we will update the high water marks during transaction commit. Note that the same\n  // column can have multiple entries here if A single transaction involves multiple write\n  // operations. E.g. Overwrite+ReplaceWhere operation involves two phases: Phase-1 to write just\n  // new data and Phase-2 to delete old data. So both phases can generate tuples for a given column\n  // here.\n  protected val updatedIdentityHighWaterMarks = ArrayBuffer.empty[(String, Long)]\n\n  // The names of columns for which we will track the IDENTITY high water marks at transaction\n  // writes.\n  protected var trackHighWaterMarks: Option[Set[String]] = None\n\n  // Set to true if this transaction is ALTER TABLE ALTER COLUMN SYNC IDENTITY.\n  protected var syncIdentity: Boolean = false\n\n  def setTrackHighWaterMarks(track: Set[String]): Unit = {\n    assert(trackHighWaterMarks.isEmpty, \"The tracking set shouldn't have been set\")\n    trackHighWaterMarks = Some(track)\n  }\n\n  def setSyncIdentity(): Unit = {\n    syncIdentity = true\n  }\n\n  /**\n   * Records an update to the metadata that should be committed with this transaction. As this is\n   * called after write, it skips checking `!hasWritten`. We do not have a full protocol of what\n   * `updating metadata after write` should behave, as currently this is only used to update\n   * IDENTITY columns high water marks. As a result, it goes through all the steps needed to update\n   * schema BEFORE writes, except skipping the check mentioned above. Note that schema evolution\n   * and IDENTITY update can happen inside a single transaction so this function does not check\n   * we have only one metadata update in a transaction.\n   *\n   * IMPORTANT: It is the responsibility of the caller to ensure that files currently present in\n   * the table and written by this transaction are valid under the new metadata.\n   */\n  private def updateMetadataAfterWrite(updatedMetadata: Metadata): Unit = {\n    updateMetadataInternal(updatedMetadata, ignoreDefaultProperties = false)\n  }\n\n  // Returns whether this transaction updates metadata solely for IDENTITY high water marks (this\n  // can be either a write that generates IDENTITY values or an ALTER TABLE ALTER COLUMN SYNC\n  // IDENTITY command). This must be called before precommitUpdateSchemaWithIdentityHighWaterMarks\n  // as it might update `newMetadata`.\n  def isIdentityOnlyMetadataUpdate(): Boolean = {\n    syncIdentity || (updatedIdentityHighWaterMarks.nonEmpty && newMetadata.isEmpty)\n  }\n\n  // Called before commit to update table schema with collected IDENTITY column high water marks\n  // so that the change can be committed to delta log.\n  def precommitUpdateSchemaWithIdentityHighWaterMarks(): Unit = {\n    if (updatedIdentityHighWaterMarks.nonEmpty) {\n      val newSchema = IdentityColumn.updateSchema(\n        deltaLog,\n        metadata.schema,\n        updatedIdentityHighWaterMarks.toSeq\n      )\n      val updatedMetadata = metadata.copy(schemaString = newSchema.json)\n      updateMetadataAfterWrite(updatedMetadata)\n    }\n  }\n\n  /** The set of distinct partitions that contain added files by current transaction. */\n  protected[delta] var partitionsAddedToOpt: Option[mutable.HashSet[Map[String, String]]] = None\n\n  /** True if this transaction is a blind append. This is only valid after commit. */\n  protected[delta] var isBlindAppend: Boolean = false\n\n  /**\n   * The logSegment of the snapshot prior to the commit.\n   * Will be updated only when retrying due to a conflict.\n   */\n  private[delta] var preCommitLogSegment: LogSegment =\n    snapshot.logSegment.copy(checkpointProvider = snapshot.checkpointProvider)\n\n  /** The end to end execution time of this transaction. */\n  def txnExecutionTimeMs: Option[Long] = if (commitEndNano == -1) {\n    None\n  } else {\n    Some(NANOSECONDS.toMillis(commitEndNano - txnStartNano))\n  }\n\n  /** Gets the stats collector for the table at the snapshot this transaction has. */\n  def statsCollector: Column = snapshot.statsCollector\n\n  /**\n   * Returns the metadata for this transaction. The metadata refers to the metadata of the snapshot\n   * at the transaction's read version unless updated during the transaction.\n   */\n  def metadata: Metadata = newMetadata.getOrElse(snapshot.metadata)\n\n  /**\n   * Records an update to the metadata that should be committed with this transaction.\n   * Note that this must be done before writing out any files so that file writing\n   * and checks happen with the final metadata for the table.\n   *\n   * IMPORTANT: It is the responsibility of the caller to ensure that files currently\n   * present in the table are still valid under the new metadata.\n   */\n  def updateMetadata(\n      proposedNewMetadata: Metadata,\n      ignoreDefaultProperties: Boolean = false): Unit = {\n    assert(!hasWritten,\n      \"Cannot update the metadata in a transaction that has already written data.\")\n    assert(newMetadata.isEmpty,\n      \"Cannot change the metadata more than once in a transaction.\")\n    updateMetadataInternal(proposedNewMetadata, ignoreDefaultProperties)\n    // Temporary: block metadata changes on UC-managed CatalogOwned tables until Delta supports\n    // propagating metadata updates to UC. UC is identified by catalog implementation class (handles\n    // \"spark_catalog\" registration). New table creation is naturally excluded because\n    // isCatalogOwned is false until the first commit. REPLACE TABLE is currently also blocked\n    // here and will need to be explicitly allowed once UC supports metadata propagation.\n    // Intentionally conservative: configuration is compared as a whole map, which also\n    // catches Delta-internal additions (e.g. table-feature flags). This is acceptable for\n    // a temporary kill switch - once Delta supports propagating metadata updates to UC,\n    // this check will be removed entirely.\n    if (!isCreatingNewTable) {\n      throwIfUCManagedMetadataChanged(snapshot.metadata, context = \"updateMetadata\")\n    }\n  }\n\n  /**\n   * Returns true if the proposed metadata differs from the existing metadata for a UC-managed\n   * table.\n   */\n  private def hasUCManagedMetadataChange(\n      existingMetadata: Metadata,\n      proposedMetadata: Metadata): Boolean = {\n    proposedMetadata.schemaString != existingMetadata.schemaString ||\n      proposedMetadata.partitionColumns != existingMetadata.partitionColumns ||\n      proposedMetadata.description != existingMetadata.description ||\n      proposedMetadata.configuration != existingMetadata.configuration\n  }\n\n  private def throwIfUCManagedMetadataChanged(\n      existingMetadata: Metadata,\n      context: String): Unit = {\n    val proposedMetadata = newMetadata.getOrElse(existingMetadata)\n    if (isUCManagedTable && hasUCManagedMetadataChange(existingMetadata, proposedMetadata)) {\n      logWarning(log\"Blocking UC-managed metadata update during \" +\n        log\"${MDC(DeltaLogKeys.OPERATION, context)} because metadata changed: \" +\n        log\"${MDC(DeltaLogKeys.METADATA_OLD, existingMetadata)} => \" +\n        log\"${MDC(DeltaLogKeys.METADATA_NEW, proposedMetadata)}\")\n      throw DeltaErrors.operationNotSupportedException(\n        \"Metadata changes on Unity Catalog managed tables\")\n    }\n  }\n\n  /**\n   * True if this transaction targets a UC-managed CatalogOwned table.\n   *\n   * Computed once as a lazy val because catalogTable and SparkSession are immutable for\n   * the lifetime of a transaction. Visibility is protected[delta] (not private) to allow\n   * test subclasses to override without requiring UCSingleCatalog.\n   */\n  protected[delta] lazy val isUCManagedTable: Boolean = {\n    snapshot.isCatalogOwned &&\n      catalogTable.exists { ct =>\n        ct.tableType == CatalogTableType.MANAGED &&\n          CatalogOwnedTableUtils.getCatalogName(spark, ct.identifier)\n            .contains(UCCommitCoordinatorBuilder.COORDINATOR_NAME)\n      }\n  }\n\n  /**\n   * Returns true if committing [[dm]] would change the clustering columns on a UC-managed\n   * CatalogOwned table and should therefore be blocked.\n   *\n   * Both a missing entry and a removed=true tombstone mean \"no clustering\", so the effective\n   * configuration is normalised to Option[String] before comparison.\n   */\n  private def isClusteringChangedOnUCManagedTable(dm: DomainMetadata): Boolean = {\n    if (dm.domain != ClusteringMetadataDomain.domainName) return false\n    if (!isUCManagedTable) return false\n    val existingConfig =\n      snapshot.domainMetadata\n        .find(_.domain == ClusteringMetadataDomain.domainName)\n        .filterNot(_.removed)\n        .map(_.configuration)\n    val incomingConfig = if (dm.removed) None else Some(dm.configuration)\n    incomingConfig != existingConfig\n  }\n\n  /**\n   * Can this transaction still update the metadata?\n   * This is allowed only once per transaction.\n   */\n  def canUpdateMetadata: Boolean = {\n    !hasWritten && newMetadata.isEmpty\n  }\n\n  /**\n   * This updates the protocol for the table with a given protocol.\n   * Note that the protocol set by this method can be overwritten by other methods,\n   * such as [[updateMetadata]].\n   */\n  def updateProtocol(protocol: Protocol): Unit = {\n      newProtocol = Some(protocol)\n  }\n\n  /**\n   * Do the actual checks and works to update the metadata and save it into the `newMetadata`\n   * field, which will be added to the actions to commit in [[prepareCommit]].\n   */\n  protected def updateMetadataInternal(\n      proposedNewMetadata: Metadata,\n      ignoreDefaultProperties: Boolean): Unit = {\n    var newMetadataTmp = proposedNewMetadata\n    // Validate all indexed columns are inside table's schema.\n    StatisticsCollection.validateDeltaStatsColumns(newMetadataTmp)\n    if (readVersion == -1 || isCreatingNewTable) {\n      // We need to ignore the default properties when trying to create an exact copy of a table\n      // (as in CLONE and SHALLOW CLONE).\n      if (!ignoreDefaultProperties) {\n        newMetadataTmp = withGlobalConfigDefaults(newMetadataTmp)\n      }\n      isCreatingNewTable = true\n    }\n\n    val identityColumnAllowed =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_IDENTITY_COLUMN_ENABLED)\n    if (!identityColumnAllowed &&\n        ColumnWithDefaultExprUtils.hasIdentityColumn(newMetadataTmp.schema)) {\n      throw DeltaErrors.unsupportedWriterTableFeaturesInTableException(\n        deltaLog.dataPath.toString, Seq(IdentityColumnsTableFeature.name))\n    }\n\n    val protocolBeforeUpdate = protocol\n    // `readVersion == -1` indicates the current transaction is reading from a snapshot\n    // where the table has not existed yet.\n    // `isCreatingNewTable` will be true for commands like REPLACE and CREATE,\n    // this is just a double check since we only want to auto-enable QoL features\n    // when creating a new CatalogOwned table through CREATE.\n    if (CatalogOwnedTableUtils.shouldEnableCatalogOwned(\n        spark, propertyOverrides = newMetadataTmp.configuration) &&\n        isCreatingNewTable && this.readVersion == -1) {\n\n      // For CatalogOwned table, we add \"quality of life\" table features as a part of CCv2 table\n      // creation. Look for [[CatalogOwnedTableUtils.updateMetadataForQoLFeatures]] to see\n      // what features are in the list.\n      // Note that we need to add features here because features like `ColumnMapping` or\n      // `RowTracking` have their own validation/update logic below.\n      newMetadataTmp = CatalogOwnedTableUtils.updateMetadataForQoLFeatures(\n        spark,\n        metadata = newMetadataTmp\n        )\n    }\n    // The `.schema` cannot be generated correctly unless the column mapping metadata is correctly\n    // filled for all the fields. Therefore, the column mapping changes need to happen first.\n    newMetadataTmp = DeltaColumnMapping.verifyAndUpdateMetadataChange(\n      spark,\n      deltaLog,\n      protocolBeforeUpdate,\n      snapshot.metadata,\n      newMetadataTmp,\n      isCreatingNewTable,\n      isOverwritingSchema)\n\n    if (newMetadataTmp.schemaString != null) {\n      // Replace CHAR and VARCHAR with StringType\n      val schema = CharVarcharUtils.replaceCharVarcharWithStringInSchema(\n        newMetadataTmp.schema)\n      newMetadataTmp = newMetadataTmp.copy(schemaString = schema.json)\n    }\n\n    newMetadataTmp = if (snapshot.metadata.schemaString == newMetadataTmp.schemaString) {\n      // Shortcut when the schema hasn't changed to avoid generating spurious schema change logs.\n      // It's fine if two different but semantically equivalent schema strings skip this special\n      // case - that indicates that something upstream attempted to do a no-op schema change, and\n      // we'll just end up doing a bit of redundant work in the else block.\n      newMetadataTmp\n    } else {\n      val fixedSchema = SchemaUtils.removeUnenforceableNotNullConstraints(\n        newMetadataTmp.schema, spark.sessionState.conf).json\n      newMetadataTmp.copy(schemaString = fixedSchema)\n    }\n\n\n    if (canAssignAnyNewProtocol) {\n      // Check for the new protocol version after the removal of the unenforceable not null\n      // constraints\n      newProtocol = Some(Protocol.forNewTable(spark, Some(newMetadataTmp)))\n    } else if (newMetadataTmp.configuration.contains(Protocol.MIN_READER_VERSION_PROP) ||\n      newMetadataTmp.configuration.contains(Protocol.MIN_WRITER_VERSION_PROP)) {\n      // Table features Part 1: bump protocol version numbers\n      //\n      // Collect new reader and writer versions from table properties, which could be provided by\n      // the user in `ALTER TABLE TBLPROPERTIES` or copied over from session defaults.\n      val readerVersionAsTableProp =\n        Protocol.getReaderVersionFromTableConf(newMetadataTmp.configuration)\n          .getOrElse(protocolBeforeUpdate.minReaderVersion)\n      val writerVersionAsTableProp =\n        Protocol.getWriterVersionFromTableConf(newMetadataTmp.configuration)\n          .getOrElse(protocolBeforeUpdate.minWriterVersion)\n\n      val newProtocolForLatestMetadata =\n        Protocol(readerVersionAsTableProp, writerVersionAsTableProp)\n\n      // The user-supplied protocol version numbers are treated as a group of features\n      // that must all be enabled. This ensures that the feature-enabling behavior is the\n      // same on Table Features-enabled protocols as on legacy protocols, i.e., exactly\n      // the same set of features are enabled.\n      //\n      // This is useful for supporting protocol downgrades to legacy protocol versions.\n      // When the protocol versions are explicitly set on table features protocol we may\n      // normalize to legacy protocol versions. Legacy protocol versions can only be\n      // used if a table supports *exactly* the set of features in that legacy protocol\n      // version, with no \"gaps\". By merging in the protocol features from a particular\n      // protocol version, we may end up with such a \"gap-free\" protocol. E.g. if a table\n      // has only table feature \"checkConstraints\" (added by writer protocol version 3)\n      // but not \"invariants\" and \"appendOnly\", then setting the minWriterVersion to\n      // 2 or 3 will add \"invariants\" and \"appendOnly\", filling in the gaps for writer\n      // protocol version 3, and then we can downgrade to version 3.\n      val proposedNewProtocol = protocolBeforeUpdate.merge(newProtocolForLatestMetadata)\n\n      if (proposedNewProtocol != protocolBeforeUpdate) {\n        // The merged protocol has higher versions and/or supports more features.\n        // It's a valid upgrade.\n        newProtocol = Some(proposedNewProtocol)\n      } else {\n        // The merged protocol is identical to the original one. Two possibilities:\n        // (1) the provided versions are lower than the original one, and all features supported by\n        //     the provided versions are already supported. This is a no-op.\n        if (readerVersionAsTableProp < protocolBeforeUpdate.minReaderVersion ||\n          writerVersionAsTableProp < protocolBeforeUpdate.minWriterVersion) {\n          recordProtocolChanges(\n            \"delta.protocol.downgradeIgnored\",\n            fromProtocol = protocolBeforeUpdate,\n            toProtocol = newProtocolForLatestMetadata,\n            isCreatingNewTable = false)\n        } else {\n          // (2) the new protocol versions is identical to the existing versions. Also a no-op.\n        }\n      }\n    }\n\n    newMetadataTmp = if (isCreatingNewTable) {\n      // Creating a new table will drop all existing data, so we don't need to fix the old\n      // metadata.\n      newMetadataTmp\n    } else {\n      // This is not a new table. The new schema may be merged from the existing schema. We\n      // decide whether we should keep the Generated or IDENTITY columns by checking whether the\n      // protocol satisfies the requirements.\n      val keepGeneratedColumns =\n        GeneratedColumn.satisfyGeneratedColumnProtocol(protocolBeforeUpdate)\n      val keepIdentityColumns =\n        ColumnWithDefaultExprUtils.satisfiesIdentityColumnProtocol(protocolBeforeUpdate)\n      if (keepGeneratedColumns && keepIdentityColumns) {\n        // If a protocol satisfies both requirements, we do nothing here.\n        newMetadataTmp\n      } else {\n        // As the protocol doesn't match, this table is created by an old version that doesn't\n        // support generated columns or identity columns. We should remove the generation\n        // expressions to fix the schema to avoid bumping the writer version incorrectly.\n        val newSchema = ColumnWithDefaultExprUtils.removeDefaultExpressions(\n          newMetadataTmp.schema,\n          keepGeneratedColumns = keepGeneratedColumns,\n          keepIdentityColumns = keepIdentityColumns)\n        if (newSchema ne newMetadataTmp.schema) {\n          newMetadataTmp.copy(schemaString = newSchema.json)\n        } else {\n          newMetadataTmp\n        }\n      }\n    }\n\n    // Table features Part 2: add manually-supported features specified in table properties, aka\n    // those start with [[FEATURE_PROP_PREFIX]].\n    //\n    // This transaction's new metadata might contain some table properties to support some\n    // features (props start with [[FEATURE_PROP_PREFIX]]). We silently add them to the `protocol`\n    // action, and bump the protocol version to (3, 7) or (_, 7), depending on the existence of\n    // any reader-writer feature.\n    val newProtocolBeforeAddingFeatures = newProtocol.getOrElse(protocolBeforeUpdate)\n    val newFeaturesFromTableConf =\n      TableFeatureProtocolUtils.getSupportedFeaturesFromTableConfigs(newMetadataTmp.configuration)\n    val readerVersionForNewProtocol = {\n      // All features including those required features are considered to decide reader version.\n      if (Protocol()\n        .withFeatures(newFeaturesFromTableConf)\n        .readerAndWriterFeatureNames\n        .flatMap(TableFeature.featureNameToFeature)\n        .exists(_.isReaderWriterFeature)) {\n        TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION\n      } else {\n        newProtocolBeforeAddingFeatures.minReaderVersion\n      }\n    }\n    val existingFeatureNames = newProtocolBeforeAddingFeatures.readerAndWriterFeatureNames\n    if (!newFeaturesFromTableConf.map(_.name).subsetOf(existingFeatureNames)) {\n      newProtocol = Some(\n        Protocol(\n          readerVersionForNewProtocol,\n          TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeatures(newFeaturesFromTableConf)\n          .merge(newProtocolBeforeAddingFeatures))\n    }\n    // For CatalogOwned table feature, we don't support the upgrade yet.\n    newProtocol.foreach { p =>\n      if (!isCreatingNewTable &&\n          p.readerAndWriterFeatureNames.contains(CatalogOwnedTableFeature.name) &&\n          !existingFeatureNames.contains(CatalogOwnedTableFeature.name)) {\n        throw new NotImplementedException(\"Upgrading to CatalogOwned table is not yet \" +\n          s\"supported. Please create a new table with the CatalogOwned table feature.\")\n      }\n    }\n\n    // We are done with protocol versions and features, time to remove related table properties.\n    val configsWithoutProtocolProps =\n      Protocol.filterProtocolPropsFromTableProps(newMetadataTmp.configuration)\n    // Table features Part 3: add automatically-enabled features by looking at the new table\n    // metadata.\n    //\n    // This code path is for existing tables and during `REPLACE` if the downgrade flag is not set.\n    // The new table case has been handled by [[Protocol.forNewTable]] earlier in this method.\n    if (!canAssignAnyNewProtocol) {\n      setNewProtocolWithFeaturesEnabledByMetadata(newMetadataTmp)\n    }\n\n    if (isCreatingNewTable) {\n      IdentityColumn.logTableCreation(deltaLog, newMetadataTmp.schema)\n    }\n\n    newMetadataTmp = newMetadataTmp.copy(configuration = configsWithoutProtocolProps)\n    Protocol.assertMetadataContainsNoProtocolProps(newMetadataTmp)\n\n    newMetadataTmp = MaterializedRowId.updateMaterializedColumnName(\n      protocol, oldMetadata = snapshot.metadata, newMetadataTmp)\n    newMetadataTmp = MaterializedRowCommitVersion.updateMaterializedColumnName(\n      protocol, oldMetadata = snapshot.metadata, newMetadataTmp)\n\n    assertMetadata(newMetadataTmp)\n    logInfo(log\"Updated metadata from \" +\n      log\"${MDC(DeltaLogKeys.METADATA_OLD, newMetadata.getOrElse(\"-\"))} to \" +\n      log\"${MDC(DeltaLogKeys.METADATA_NEW, newMetadataTmp)}\")\n    newMetadata = Some(newMetadataTmp)\n\n    // Check that the metadata change is valid for CDC enabled tables.\n    performCdcMetadataCheck()\n  }\n\n  /**\n   * Records an update to the metadata that should be committed with this transaction and when\n   * this transaction is logically creating a new table, e.g. replacing a previous table with new\n   * metadata. Note that this must be done before writing out any files so that file writing\n   * and checks happen with the final metadata for the table.\n   * IMPORTANT: It is the responsibility of the caller to ensure that files currently\n   * present in the table are still valid under the new metadata.\n   */\n  def updateMetadataForNewTable(metadata: Metadata): Unit = {\n    isCreatingNewTable = true\n    updateMetadata(metadata)\n  }\n\n  /**\n   * Updates the metadata of the target table in an effective REPLACE command. Note that replacing\n   * a table is similar to dropping a table and then recreating it. However, the backing catalog\n   * object does not change. For now, for Coordinated Commit tables, this function retains the\n   * coordinator details (and other associated Coordinated Commits properties) from the original\n   * table during a REPLACE. And if the table had a coordinator, existing ICT properties are also\n   * retained; otherwise, default ICT properties are included.\n   * TODO (YumingxuanGuo): Remove this once the exact semantic on default Coordinated Commits\n   *   configurations is finalized.\n   */\n  def updateMetadataForNewTableInReplace(metadata: Metadata): Unit = {\n    assert(CoordinatedCommitsUtils.getExplicitCCConfigurations(metadata.configuration).isEmpty,\n      \"Command-specified Coordinated Commits configurations should have been blocked earlier.\")\n    assert(!metadata.configuration.contains(UCCommitCoordinatorClient.UC_TABLE_ID_KEY),\n      \"Command-specified Catalog-Owned table UUID (ucTableId) should have been blocked earlier.\")\n    // Extract any existing ucTableId from the snapshot metadata.\n    val existingUCTableIdConf: Map[String, String] =\n      snapshot.metadata.configuration.filter { case (k, v) =>\n        k == UCCommitCoordinatorClient.UC_TABLE_ID_KEY\n      }\n    // Extract the existing Coordinated Commits configurations and ICT dependency configurations\n    // from the existing table metadata.\n    val existingCCConfs =\n      CoordinatedCommitsUtils.getExplicitCCConfigurations(snapshot.metadata.configuration)\n    val existingICTConfs =\n      CoordinatedCommitsUtils.getExplicitICTConfigurations(snapshot.metadata.configuration)\n    val existingQoLConfs =\n      CoordinatedCommitsUtils.getExplicitQoLConfigurations(snapshot.metadata.configuration)\n    val oldMappingMode = snapshot.metadata.columnMappingMode\n    val newMappingMode = metadata.columnMappingMode\n    val shouldReuseColumnMetadataForReplaceTable =\n      spark.conf.get(DeltaSQLConf.REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE)\n\n    if (oldMappingMode == newMappingMode && shouldReuseColumnMetadataForReplaceTable) {\n      isOverwritingSchema = true\n    }\n    // Update the metadata.\n    updateMetadataForNewTable(metadata)\n    // Now the `txn.metadata` contains all the command-specified properties and all the default\n    // properties. The latter might still contain Coordinated Commits configurations, so we need\n    // to remove them and retain the Coordinated Commits configurations from the existing table.\n    val newConfsWithoutCC = newMetadata.get.configuration --\n      CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS\n    val existingQoLConfsToRetain = existingQoLConfs.filterNot { case (key, _) =>\n      newConfsWithoutCC.contains(key)\n    }\n    var newConfs: Map[String, String] =\n      newConfsWithoutCC ++ existingCCConfs ++ existingQoLConfsToRetain ++ existingUCTableIdConf\n    // We also need to retain the existing ICT dependency configurations, but only when the\n    // existing table does have Coordinated Commits configurations or Catalog-Owned enabled.\n    // Otherwise, we treat the ICT configurations the same as any other configurations,\n    // by merging them from the default.\n    val isCatalogOwnedEnabledBeforeReplace = snapshot.protocol\n      .readerAndWriterFeatureNames.contains(CatalogOwnedTableFeature.name)\n    if (existingCCConfs.nonEmpty || isCatalogOwnedEnabledBeforeReplace) {\n      val newConfsWithoutICT = newConfs -- CoordinatedCommitsUtils.ICT_TABLE_PROPERTY_KEYS\n      newConfs = newConfsWithoutICT ++ existingICTConfs\n    }\n    newMetadata = Some(newMetadata.get.copy(configuration = newConfs))\n    throwIfUCManagedMetadataChanged(\n      snapshot.metadata,\n      context = \"updateMetadataForNewTableInReplace\")\n  }\n\n  /**\n   * Records an update to the metadata that should be committed with this transaction and when\n   * this transaction is attempt to overwrite the data and schema using .mode('overwrite') and\n   * .option('overwriteSchema', true).\n   * REPLACE the table is not considered in this category, because that is logically equivalent\n   * to DROP and RECREATE the table.\n   */\n  def updateMetadataForTableOverwrite(proposedNewMetadata: Metadata): Unit = {\n    isOverwritingSchema = true\n    updateMetadata(proposedNewMetadata)\n  }\n\n  /**\n   * Remove the 'EXISTS_DEFAULT' metadata key from the schema. This is used for new tables that are\n   * not re-using data files of existing tables (i.e. CREATE TABLE, REPLACE TABLE, CTAS). It is not\n   * used on code paths of commands that create new tables but re-use data files (i.e. CONVERT TO\n   * DELTA, CLONE) because we cannot assure that 'EXISTS_DEFAULT' values is actually not required\n   * without reading the data.\n   */\n  def removeExistsDefaultFromSchema(): Unit = {\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA)) {\n      if (newMetadata.isDefined) {\n        val schemaWithRemovedExistsDefaults =\n          SchemaUtils.removeExistsDefaultMetadata(newMetadata.get.schema)\n        if (schemaWithRemovedExistsDefaults != newMetadata.get.schema) {\n          newMetadata = newMetadata.map(_.copy(schemaString = schemaWithRemovedExistsDefaults.json))\n        }\n      }\n    }\n  }\n\n  protected def assertMetadata(metadata: Metadata): Unit = {\n    assert(!CharVarcharUtils.hasCharVarchar(metadata.schema),\n      \"The schema in Delta log should not contain char/varchar type.\")\n    SchemaMergingUtils.checkColumnNameDuplication(metadata.schema, \"in the metadata update\")\n    if (metadata.columnMappingMode == NoMapping) {\n      SchemaUtils.checkSchemaFieldNames(metadata.dataSchema, metadata.columnMappingMode)\n      val partitionColCheckIsFatal =\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_PARTITION_COLUMN_CHECK_ENABLED)\n      try {\n        SchemaUtils.checkFieldNames(metadata.partitionColumns)\n      } catch {\n        case e: AnalysisException =>\n          recordDeltaEvent(\n            deltaLog,\n            \"delta.schema.invalidPartitionColumn\",\n            data = Map(\n              \"checkEnabled\" -> partitionColCheckIsFatal,\n              \"columns\" -> metadata.partitionColumns\n            )\n          )\n          if (partitionColCheckIsFatal) throw DeltaErrors.invalidPartitionColumn(e)\n      }\n    } else {\n      DeltaColumnMapping.checkColumnIdAndPhysicalNameAssignments(metadata)\n    }\n\n    if (GeneratedColumn.hasGeneratedColumns(metadata.schema)) {\n      recordDeltaOperation(deltaLog, \"delta.generatedColumns.check\") {\n        GeneratedColumn.validateGeneratedColumns(spark, metadata.schema)\n      }\n      recordDeltaEvent(deltaLog, \"delta.generatedColumns.definition\")\n    }\n\n    if (checkUnsupportedDataType) {\n      val unsupportedTypes = SchemaUtils.findUnsupportedDataTypes(metadata.schema)\n      if (unsupportedTypes.nonEmpty) {\n        throw DeltaErrors.unsupportedDataTypes(unsupportedTypes.head, unsupportedTypes.tail: _*)\n      }\n    }\n\n    if (spark.conf.get(DeltaSQLConf.DELTA_TABLE_PROPERTY_CONSTRAINTS_CHECK_ENABLED)) {\n      Protocol.assertTablePropertyConstraintsSatisfied(spark, metadata, snapshot)\n    }\n    MaterializedRowId.throwIfMaterializedColumnNameConflictsWithSchema(metadata)\n    MaterializedRowCommitVersion.throwIfMaterializedColumnNameConflictsWithSchema(metadata)\n  }\n\n  /**\n   * Some features require their pre-requisite features to not only be present\n   * in the protocol but also be enabled. This method sets the flags required\n   * to enable these pre-requisite features.\n   */\n  private def getMetadataWithDependentFeaturesEnabled(\n      metadata: Metadata, protocols: Seq[Protocol]): Metadata = {\n    if (DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.fromMetaData(metadata).isDefined ||\n        protocols.exists(_.readerAndWriterFeatureNames.contains(CatalogOwnedTableFeature.name))) {\n      // coordinated-commits/catalog-owned require ICT to be enabled as per the spec.\n      // If ICT is just in Protocol and not in Metadata,\n      // then it is in a 'supported' state but not enabled.\n      // In order to enable ICT, we have to set the table property in Metadata.\n      val ictEnablementConfigOpt =\n        Option.when(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(metadata))(\n          (DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"true\"))\n      val configWithICT = metadata.configuration ++ ictEnablementConfigOpt\n      metadata.copy(configuration = configWithICT)\n    } else {\n      metadata\n    }\n  }\n\n  private def setNewProtocolWithFeaturesEnabledByMetadata(metadata: Metadata): Unit = {\n    val requiredProtocolOpt =\n      Protocol.upgradeProtocolFromMetadataForExistingTable(spark, metadata, protocol)\n    if (requiredProtocolOpt.isDefined) {\n      newProtocol = requiredProtocolOpt\n    }\n  }\n\n  // Make sure shredded writes are only performed if the shredding table property was set.\n  private def assertShreddingStateConsistent() = {\n    if (!DeltaConfigs.ENABLE_VARIANT_SHREDDING.fromMetaData(metadata)) {\n      val isVariantShreddingSchemaForced =\n        spark.sessionState.conf\n          .getConfString(\"spark.sql.variant.forceShreddingSchemaForTest\", \"\").nonEmpty\n      if (isVariantShreddingSchemaForced) {\n        throw DeltaErrors.variantShreddingUnsupported()\n      }\n    }\n  }\n\n  /**\n   * Must make sure that deletion vectors are never added to a table where that isn't allowed.\n   * Note, statistics recomputation is still allowed even though DVs might be currently disabled.\n   *\n   * This method returns a function that can be used to validate a single Action.\n   */\n  protected def getAssertDeletionVectorWellFormedFunc(\n      spark: SparkSession,\n      op: DeltaOperations.Operation): (Action => Unit) = {\n    val commitCheckEnabled = spark.conf.get(DeltaSQLConf.DELETION_VECTORS_COMMIT_CHECK_ENABLED)\n    if (!commitCheckEnabled) {\n      return _ => {}\n    }\n\n    // Whether DVs are supported, i.e. the table is allowed to contain any DVs.\n    val deletionVectorsSupported =\n      DeletionVectorUtils.deletionVectorsReadable(snapshot, newProtocol, newMetadata)\n    // Whether DVs are enabled, i.e. operations are allowed to create new DVs.\n    val deletionVectorsEnabled =\n      DeletionVectorUtils.deletionVectorsWritable(snapshot, newProtocol, newMetadata)\n\n    // If the operation does not define whether it performs in-place metadata updates, we are\n    // conservative and assume that it is not, which makes the check stricter.\n    val isInPlaceFileMetadataUpdate = op.isInPlaceFileMetadataUpdate.getOrElse(false)\n    val deletionVectorAllowedForAddFiles =\n      deletionVectorsSupported && (deletionVectorsEnabled || isInPlaceFileMetadataUpdate)\n\n    val addFileMustHaveWideBounds = op.checkAddFileWithDeletionVectorStatsAreNotTightBounds\n\n    action => action match {\n      case a: AddFile if a.deletionVector != null =>\n        if (!deletionVectorAllowedForAddFiles) {\n          throw DeltaErrors.addingDeletionVectorsDisallowedException()\n        }\n\n        // Protocol requirement checks:\n        // 1. All files with DVs must have `stats` with `numRecords`.\n        if (a.stats == null || a.numPhysicalRecords.isEmpty) {\n          throw DeltaErrors.addFileWithDVsMissingNumRecordsException\n        }\n\n        // 2. All operations that add new DVs should always turn bounds to wide.\n        //    Operations that only update files with existing DVs may opt-out from this rule\n        //    via `checkAddFileWithDeletionVectorStatsAreNotTightBounds`.\n        //    See that field comment in DeltaOperation for more details.\n        //    Note, the absence of the tightBounds column when DVs exist is also an illegal state.\n        if (addFileMustHaveWideBounds &&\n            // Extra inversion to also catch absent `tightBounds`.\n            !a.tightBounds.contains(false)) {\n          throw DeltaErrors.addFileWithDVsAndTightBoundsException()\n        }\n      case _ => // Not an AddFile, nothing to do.\n    }\n  }\n\n  /**\n   * Returns the [[DeltaScanGenerator]] for the given log, which will be used to generate\n   * [[DeltaScan]]s. Every time this method is called on a log, the returned generator\n   * generator will read a snapshot that is pinned on the first access for that log.\n   *\n   * Internally, if the given log is the same as the log associated with this\n   * transaction, then it returns this transaction, otherwise it will return a snapshot of\n   * given log\n   */\n  def getDeltaScanGenerator(index: TahoeLogFileIndex): DeltaScanGenerator = {\n    if (index.deltaLog.isSameLogAs(deltaLog)) return this\n\n    val compositeId = index.deltaLog.compositeId\n    // Will be called only when the log is accessed the first time\n    readSnapshots.computeIfAbsent(compositeId, _ => index.getSnapshot)\n  }\n\n  /** Returns a[[DeltaScan]] based on the given filters. */\n  override def filesForScan(\n    filters: Seq[Expression],\n    keepNumRecords: Boolean = false\n  ): DeltaScan = {\n    val scan = snapshot.filesForScan(filters, keepNumRecords)\n    trackReadPredicates(filters)\n    trackFilesRead(scan.files)\n    scan\n  }\n\n  /** Returns a[[DeltaScan]] based on the given partition filters, projections and limits. */\n  override def filesForScan(\n      limit: Long,\n      partitionFilters: Seq[Expression]): DeltaScan = {\n    partitionFilters.foreach { f =>\n      assert(\n        DeltaTableUtils.isPredicatePartitionColumnsOnly(f, metadata.partitionColumns, spark),\n        s\"Only filters on partition columns [${metadata.partitionColumns.mkString(\", \")}]\" +\n          s\" expected, found $f\")\n    }\n    val scan = snapshot.filesForScan(limit, partitionFilters)\n    trackReadPredicates(partitionFilters, partitionOnly = true)\n    trackFilesRead(scan.files)\n    scan\n  }\n\n  override def filesWithStatsForScan(partitionFilters: Seq[Expression]): DataFrame = {\n    val metadata = snapshot.filesWithStatsForScan(partitionFilters)\n    trackReadPredicates(partitionFilters, partitionOnly = true)\n    trackFilesRead(filterFiles(partitionFilters))\n    metadata\n  }\n\n  /** Returns files matching the given predicates. */\n  def filterFiles(): Seq[AddFile] = filterFiles(Seq(Literal.TrueLiteral))\n\n  /** Returns files matching the given predicates. */\n  def filterFiles(filters: Seq[Expression], keepNumRecords: Boolean = false): Seq[AddFile] = {\n    val scan = snapshot.filesForScan(filters, keepNumRecords)\n    trackReadPredicates(filters)\n    trackFilesRead(scan.files)\n    scan.files\n  }\n\n  /**\n   * Returns files within the given partitions.\n   *\n   * `partitions` is a set of the `partitionValues` stored in [[AddFile]]s. This means they refer to\n   * the physical column names, and values are stored as strings.\n   * */\n  def filterFiles(partitions: Set[Map[String, String]]): Seq[AddFile] = {\n    import org.apache.spark.sql.functions.col\n    val df = snapshot.allFiles.toDF()\n    val isFileInTouchedPartitions =\n      DeltaUDF.booleanFromMap(partitions.contains)(col(\"partitionValues\"))\n    val filteredFiles = df\n      .filter(isFileInTouchedPartitions)\n      .withColumn(\"stats\", DataSkippingReader.nullStringLiteral)\n      .as[AddFile]\n      .collect()\n    trackReadPredicates(\n      Seq(isFileInTouchedPartitions.expr), partitionOnly = true, shouldRewriteFilter = false)\n    filteredFiles\n  }\n\n  /** Mark the entire table as tainted by this transaction. */\n  def readWholeTable(): Unit = {\n    trackReadPredicates(Seq.empty)\n    readTheWholeTable = true\n  }\n\n  /** Mark the given files as read within this transaction. */\n  def trackFilesRead(files: Seq[AddFile]): Unit = {\n    readFiles ++= files\n  }\n\n  /** Mark the predicates that have been queried by this transaction. */\n  def trackReadPredicates(\n      filters: Seq[Expression],\n      partitionOnly: Boolean = false,\n      shouldRewriteFilter: Boolean = true): Unit = {\n    val (partitionFilters, dataFilters) = if (partitionOnly) {\n      (filters, Seq.empty[Expression])\n    } else {\n      filters.partition { f =>\n        DeltaTableUtils.isPredicatePartitionColumnsOnly(f, metadata.partitionColumns, spark)\n      }\n    }\n\n    readPredicates.add(DeltaTableReadPredicate(\n      partitionPredicates = partitionFilters,\n      dataPredicates = dataFilters,\n      shouldRewriteFilter = shouldRewriteFilter)\n    )\n  }\n\n  /**\n   * Returns the latest version that has committed for the idempotent transaction with given `id`.\n   */\n  def txnVersion(id: String): Long = {\n    readTxn += id\n    snapshot.transactions.getOrElse(id, -1L)\n  }\n\n  /**\n   * Return the operation metrics for the operation if it is enabled\n   */\n  def getOperationMetrics(op: Operation): Option[Map[String, String]] = {\n    if (spark.conf.get(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED)) {\n      Some(getMetricsForOperation(op))\n    } else {\n      None\n    }\n  }\n\n  def reportAutoCompactStatsError(e: Throwable): Unit = {\n    recordDeltaEvent(deltaLog, \"delta.collectStats\", data = Map(\"message\" -> e.getMessage))\n    logError(e.getMessage)\n  }\n\n  /**\n   * Collects auto optimize stats from the given actions.\n   * This method computes the stats as a side effect of iterating through the actions.\n   * The computed stats are only available after the returned iterator is fully consumed.\n   * `finalizeStats` must be called on the returned collector to finalize the stats.\n   * @param actions An iterator of actions that are being committed in this transaction.\n   * @return A tuple containing:\n   *         1. An iterator of actions with the auto optimize stats computed as a side effect.\n   *         2. An instance of [[AutoCompactPartitionStatsCollector]] that contains the\n   *          computed stats.\n   */\n  private def collectAutoOptimizeStats(\n      actions: Iterator[Action]): (Iterator[Action], AutoCompactPartitionStatsCollector) = {\n    val collector = createAutoCompactStatsCollector()\n    if (collector.isInstanceOf[DisabledAutoCompactPartitionStatsCollector]) {\n      return (actions, collector)\n    }\n    val actionsIter = AutoCompactPartitionStats.instance(spark)\n      .collectPartitionStats(collector, actions)\n    (actionsIter, collector)\n  }\n\n  /**\n   * Collects auto optimize stats from the given actions and finalizes the stats.\n   * This method consumes the actions iterator to compute the stats and then finalizes them.\n   * @param actions A sequence of actions that are being committed in this transaction.\n   * @param tableId The ID of the table for which the stats are being collected.\n   */\n  def collectAutoOptimizeStatsAndFinalize(\n      actions: Seq[Action],\n      tableId: String): Unit = {\n    val (actionsIter, acStatsCollector) =\n      collectAutoOptimizeStats(actions.toIterator)\n    // Consume the iterator to hydrate the stats collector.\n    actionsIter.foreach(_ => ())\n    acStatsCollector.finalizeStats(tableId)\n  }\n\n  /**\n   * A subclass of AutoCompactPartitionStatsCollector that's to be used if the config to collect\n   * auto compaction stats is turned off. This subclass intentionally does nothing.\n   */\n  class DisabledAutoCompactPartitionStatsCollector extends AutoCompactPartitionStatsCollector {\n    override def collectPartitionStatsForAdd(file: AddFile): Unit = {}\n    override def collectPartitionStatsForRemove(file: RemoveFile): Unit = {}\n    override def finalizeStats(tableId: String): Unit = {}\n  }\n\n  def createAutoCompactStatsCollector(): AutoCompactPartitionStatsCollector = {\n    try {\n      if (spark.conf.get(DeltaSQLConf.DELTA_AUTO_COMPACT_RECORD_PARTITION_STATS_ENABLED)) {\n        val minFileSize = spark.conf\n              .get(DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_FILE_SIZE)\n              .getOrElse(Long.MaxValue)\n        return AutoCompactPartitionStats.instance(spark)\n          .createStatsCollector(minFileSize, reportAutoCompactStatsError)\n      }\n    } catch {\n      case NonFatal(e) => reportAutoCompactStatsError(e)\n    }\n\n    // If config-disabled, or error caught, fall though and use a no-op stats collector.\n    new DisabledAutoCompactPartitionStatsCollector\n  }\n\n  /**\n   * Checks if the new schema contains any CDC columns (which is invalid) and throws the appropriate\n   * error\n   */\n  protected def performCdcMetadataCheck(): Unit = {\n    if (newMetadata.nonEmpty) {\n      CDCReader.checkMetadataChange(\n        spark,\n        newMetadata = newMetadata.get,\n        oldMetadata = snapshot.metadata)\n    }\n  }\n\n  /**\n   * Validates that an AddFile does not reference a zero-byte parquet file.\n   * Logs a delta event regardless of the flag; throws only when\n   * [[DeltaSQLConf.DELTA_EMPTY_FILE_CHECK_THROW_ENABLED]] is set.\n   */\n  protected def validateAddFileNotEmpty(addFile: AddFile): Unit = {\n    if (addFile.size == 0) {\n      recordDeltaEvent(\n        deltaLog,\n        \"delta.sanityCheck.emptyParquetFile\",\n        data = Map(\n          \"addFile\" -> addFile.json,\n          \"stackTrace\" -> Thread.currentThread().getStackTrace.take(20).mkString(\"\\n\")\n        ))\n      if (spark.conf.get(DeltaSQLConf.DELTA_EMPTY_FILE_CHECK_THROW_ENABLED)) {\n        throw new IllegalStateException(\n          s\"AddFile ${addFile.path} references a zero-byte (empty) parquet file\")\n      }\n    }\n  }\n\n  /**\n   * Validates that partition columns that have NOT NULL\n   * constraints are not null in the AddFile action.\n   *\n   * @param addFile The AddFile action to validate\n   * @param notNullPartitionCols Partition columns with NOT NULL constraints\n   */\n  protected def validateAddFileForNullPartitions(\n      addFile: AddFile,\n      notNullPartitionCols: Set[String]): Unit = {\n    notNullPartitionCols.foreach { col =>\n      addFile.partitionValues.get(col) match {\n        case None | Some(null) =>\n          recordDeltaEvent(\n            deltaLog,\n            \"delta.constraints.nullPartitionViolation\",\n            data = Map(\n              \"addFile\" -> addFile.json,\n              \"notNullPartitionCols\" -> notNullPartitionCols.toSeq.mkString(\",\"),\n              \"stackTrace\" -> Thread.currentThread().getStackTrace.take(20).mkString(\"\\n\")\n            ))\n          if (spark.conf.get(DeltaSQLConf.DELTA_NULL_PARTITION_CHECK_THROW_ENABLED)) {\n            throw new IllegalStateException(\n              s\"AddFile ${addFile.path} has null partition value for NOT NULL column '$col'\")\n          }\n        case Some(_) => // Valid non-null partition value\n      }\n    }\n  }\n\n  /**\n   * Returns the physical names of partition columns that have NOT NULL constraints.\n   * Physical names are used because AddFile.partitionValues keys use physical column names\n   * when column mapping is enabled.\n   */\n  protected def getNotNullPartitionCols(metadata: Metadata): Set[String] = {\n    val notNullColumns = Invariants.getFromSchema(metadata.schema, spark)\n      .collect { case Constraints.NotNull(cols) => cols.mkString(\".\") }\n      .toSet\n    metadata.partitionSchema\n      .filter(f => notNullColumns.contains(f.name))\n      .map(DeltaColumnMapping.getPhysicalName)\n      .toSet\n  }\n\n  /**\n   * Runs all AddFile sanity checks: empty-file detection and null-partition validation.\n   */\n  protected def validateAddFileInvariants(\n      addFile: AddFile,\n      notNullPartitionCols: Set[String]): Unit = {\n    validateAddFileNotEmpty(addFile)\n    validateAddFileForNullPartitions(addFile, notNullPartitionCols)\n  }\n\n  /**\n   * Iterates over all actions and validates AddFile invariants.\n   */\n  protected def validateActionsAddFileInvariants(\n      actions: Seq[Action],\n      metadata: Metadata): Unit = {\n    val notNullPartitionCols = getNotNullPartitionCols(metadata)\n    actions.foreach {\n      case a: AddFile => validateAddFileInvariants(a, notNullPartitionCols)\n      case _ =>\n    }\n  }\n\n  /**\n   * Checks if the passed-in actions have internal SetTransaction conflicts, will throw exceptions\n   * in case of conflicts. This function will also remove duplicated [[SetTransaction]]s.\n   */\n  protected def checkForSetTransactionConflictAndDedup(actions: Seq[Action]): Seq[Action] = {\n    val finalActions = new ArrayBuffer[Action]\n    val txnIdToVersionMap = new mutable.HashMap[String, Long].empty\n    for (action <- actions) {\n      action match {\n        case st: SetTransaction =>\n          txnIdToVersionMap.get(st.appId).map { version =>\n            if (version != st.version) {\n              throw DeltaErrors.setTransactionVersionConflict(st.appId, version, st.version)\n            }\n          } getOrElse {\n            txnIdToVersionMap += (st.appId -> st.version)\n            finalActions += action\n          }\n        case _ => finalActions += action\n      }\n    }\n    finalActions.toSeq\n  }\n\n  /**\n   * We want to future-proof and explicitly block any occurrences of\n   * - table has CDC enabled and there are FileActions to write, AND\n   * - table has column mapping enabled and there is a column mapping related metadata action\n   *\n   * This is because the semantics for this combination of features and file changes is undefined.\n   */\n  private def performCdcColumnMappingCheck(\n      actions: Seq[Action],\n      op: DeltaOperations.Operation): Unit = {\n    if (newMetadata.nonEmpty) {\n      val _newMetadata = newMetadata.get\n      val _currentMetadata = snapshot.metadata\n\n      val cdcEnabled = CDCReader.isCDCEnabledOnTable(_newMetadata, spark)\n\n      val columnMappingEnabled = _newMetadata.columnMappingMode != NoMapping\n\n      val isColumnMappingUpgrade = DeltaColumnMapping.isColumnMappingUpgrade(\n        oldMode = _currentMetadata.columnMappingMode,\n        newMode = _newMetadata.columnMappingMode\n      )\n\n      val isBothColumnMappingEnabled =\n        _newMetadata.columnMappingMode != NoMapping &&\n          _currentMetadata.columnMappingMode != NoMapping\n\n      def dropColumnOp: Boolean = DeltaColumnMapping.isDropColumnOperation(\n        _newMetadata.schema, _currentMetadata.schema, isBothColumnMappingEnabled)\n\n      def renameColumnOp: Boolean = DeltaColumnMapping.isRenameColumnOperation(\n        _newMetadata.schema, _currentMetadata.schema, isBothColumnMappingEnabled)\n\n      def columnMappingChange: Boolean = isColumnMappingUpgrade || dropColumnOp || renameColumnOp\n\n      def existsFileActions: Boolean = actions.exists { _.isInstanceOf[FileAction] }\n\n      if (cdcEnabled && columnMappingEnabled && columnMappingChange && existsFileActions) {\n        throw DeltaErrors.blockColumnMappingAndCdcOperation(op)\n      }\n    }\n  }\n\n  /**\n   * Validates that partition column changes are only performed by operations that explicitly\n   * allow them. This check helps prevent accidental partition schema changes that could lead\n   * to data inconsistencies.\n   *\n   * The validation can be configured via [[DeltaSQLConf.DELTA_PARTITION_COLUMN_CHANGE_CHECK]].\n   *\n   * @param op The operation being performed\n   * @param newMetadata The new metadata (if any) being set in this transaction\n   */\n  private def validatePartitionColumnChanges(\n      op: DeltaOperations.Operation,\n      newMetadata: Option[Metadata]): Unit = {\n    val checkMode = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_PARTITION_COLUMN_CHANGE_CHECK)\n\n    if (checkMode != DeltaSQLConf.BooleanStringOrLogOnly.FALSE) {\n      val isNewTable = snapshot.version == -1L\n\n      // Validate that partition column changes are only performed by allowed operations.\n      newMetadata.foreach { newMeta =>\n        val oldCols = snapshot.metadata.partitionColumns\n        val newCols = newMeta.partitionColumns\n        val partitionColsChanged = oldCols != newCols\n        val illegalColChange = !isNewTable && partitionColsChanged && !op.canChangePartitionColumns\n\n        if (illegalColChange) {\n          recordDeltaEvent(\n            deltaLog = deltaLog,\n            opType = \"delta.metadataCheck.illegalPartitionColumnChange\",\n            data = Map(\n              \"operation\" -> op.name,\n              \"operationParameters\" -> op.jsonEncodedValues,\n              \"oldPartitionColumns\" -> oldCols,\n              \"newPartitionColumns\" -> newCols\n            )\n          )\n          if (checkMode == DeltaSQLConf.BooleanStringOrLogOnly.TRUE) {\n            throw DeltaErrors.unsupportedPartitionColumnChange(\n              operation = op.name,\n              oldPartitionColumns = oldCols,\n              newPartitionColumns = newCols\n            )\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Modifies the state of the log by adding a new commit that is based on a read at\n   * [[readVersion]]. In the case of a conflict with a concurrent writer this\n   * method will throw an exception.\n   *\n   * Also skips creating the commit if the configured [[IsolationLevel]] doesn't need us to record\n   * the commit from correctness perspective.\n   *\n   * Returns the new version the transaction committed or None if the commit was skipped.\n   */\n  def commitIfNeeded(\n      actions: Seq[Action],\n      op: DeltaOperations.Operation,\n      tags: Map[String, String] = Map.empty): Option[Long] = {\n    commitImpl(actions, op, canSkipEmptyCommits = true, tags = tags)\n  }\n\n  /**\n   * Modifies the state of the log by adding a new commit that is based on a read at\n   * [[readVersion]]. In the case of a conflict with a concurrent writer this\n   * method will throw an exception.\n   *\n   * @param actions     Set of actions to commit\n   * @param op          Details of operation that is performing this transactional commit\n   */\n  def commit(\n      actions: Seq[Action],\n      op: DeltaOperations.Operation): Long = {\n    commitImpl(actions, op, canSkipEmptyCommits = false, tags = Map.empty).getOrElse {\n      throw new SparkException(s\"Unknown error while trying to commit for operation $op\")\n    }\n  }\n\n  /**\n   * Modifies the state of the log by adding a new commit that is based on a read at\n   * [[readVersion]]. In the case of a conflict with a concurrent writer this\n   * method will throw an exception.\n   *\n   * @param actions     Set of actions to commit\n   * @param op          Details of operation that is performing this transactional commit\n   * @param tags        Extra tags to set to the CommitInfo action\n   */\n  def commit(\n      actions: Seq[Action],\n      op: DeltaOperations.Operation,\n      tags: Map[String, String]): Long = {\n    commitImpl(actions, op, canSkipEmptyCommits = false, tags = tags).getOrElse {\n      throw new SparkException(s\"Unknown error while trying to commit for operation $op\")\n    }\n  }\n\n  /**\n   * This method goes through all no-redirect-rules inside redirect feature to determine\n   * whether the current operation is valid to run on this table.\n   */\n  private def performNoRedirectRulesCheck(\n      op: DeltaOperations.Operation,\n      redirectConfig: TableRedirectConfiguration\n  ): Unit = {\n    // If this transaction commits to the redirect destination location, then there is no\n    // need to validate the subsequent no-redirect rules.\n    val configuration = deltaLog.newDeltaHadoopConf()\n    val dataPath = snapshot.deltaLog.dataPath.toUri.getPath\n    val catalog = spark.sessionState.catalog\n    val isRedirectDest = redirectConfig.spec.isRedirectDest(catalog, configuration, dataPath)\n    if (isRedirectDest) return\n    // Find all rules that match with the current application name.\n    // If appName is not present, its no-redirect-rule are included.\n    // If appName is present, includes its no-redirect-rule only when appName\n    // matches with \"spark.app.name\".\n    val rulesOfMatchedApps = redirectConfig.noRedirectRules.filter { rule =>\n      rule.appName.forall(_.equalsIgnoreCase(spark.conf.get(\"spark.app.name\")))\n    }\n    // Determine whether any rule is satisfied the given operation.\n    val noRuleSatisfied = !rulesOfMatchedApps.exists(_.allowedOperations.contains(op.name))\n    // If there is no rule satisfied, block the given operation.\n    if (noRuleSatisfied) {\n      throw DeltaErrors.noRedirectRulesViolated(op, redirectConfig.noRedirectRules)\n    }\n  }\n\n  /**\n   * This method determines whether `op` is valid when the table redirect feature is\n   * set on current table.\n   * 1. If redirect table feature is in progress state, no DML/DDL is allowed to execute.\n   * 2. If user tries to access redirect source table, only the allowed operations listed\n   *    inside no-redirect-rules are valid.\n   */\n  protected def performRedirectCheck(op: DeltaOperations.Operation): Unit = {\n    // If redirect conflict check is not enable, skips all remaining validations.\n    if (spark.conf.get(DeltaSQLConf.SKIP_REDIRECT_FEATURE)) return\n    // If redirect feature is not set, then skips validation.\n    if (!RedirectFeature.isFeatureSupported(snapshot)) return\n    // If this transaction tried to unset redirect feature, then skips validation.\n    if (RedirectFeature.isUpdateProperty(snapshot, op)) return\n    // If this transaction tried to drop redirect feature, then skips validation.\n    if (RedirectFeature.isDropFeature(op)) return\n    // Get the redirect configuration from current snapshot.\n    val redirectConfigOpt = RedirectFeature.getRedirectConfiguration(snapshot)\n    redirectConfigOpt.foreach { redirectConfig =>\n      // If the redirect state is in EnableRedirectInProgress or DropRedirectInProgress,\n      // all DML and DDL operation should be aborted.\n      if (redirectConfig.isInProgressState) {\n        throw DeltaErrors.invalidCommitIntermediateRedirectState(redirectConfig.redirectState)\n      }\n      // Validates the no redirect rules on the transactions that access redirect source table.\n      performNoRedirectRulesCheck(op, redirectConfig)\n    }\n  }\n\n  /**\n   * Records a delta event for a commit conflict exception, including the operation type\n   * of the winning/conflicting transaction for observability purposes.\n   */\n  protected def recordConflictEvent(e: DeltaConcurrentModificationException): Unit = {\n    // Extract the operation of the winning/conflicting transaction from the exception message.\n    // This is for visibility/observability purpose to track which type of transaction\n    // (e.g., OPTIMIZE/VACUUM) is causing the conflict.\n    // Handle two message formats:\n    // 1. New structured errors: \"A concurrent <operation> added/modified/deleted data...\"\n    // 2. Old JSON format in conflicting commit: \"operation\":\"<operation>\"\n    val newFormatPattern = \"\"\"[Aa] concurrent (.+?) (?:added|modified|deleted)\"\"\".r\n    val oldFormatPattern = \"\"\"\"operation\"\\s*:\\s*\"([^\"]+)\"\"\"\".r\n    val winningTxnOperation = newFormatPattern\n      .findFirstMatchIn(e.getMessage).map(_.group(1))\n      .orElse(oldFormatPattern.findFirstMatchIn(e.getMessage).map(_.group(1)))\n      .getOrElse(\"Unknown Operation\")\n    recordDeltaEvent(\n      deltaLog,\n      opType = \"delta.commit.conflict.\" + e.conflictType,\n      data = Map(\"winningTxnOperation\" -> winningTxnOperation))\n  }\n\n  @throws(classOf[ConcurrentModificationException])\n  protected def commitImpl(\n      actions: Seq[Action],\n      op: DeltaOperations.Operation,\n      canSkipEmptyCommits: Boolean,\n      tags: Map[String, String]): Option[Long] = recordDeltaOperation(deltaLog, \"delta.commit\") {\n    commitStartNano = System.nanoTime()\n\n    val version = try {\n      // Check for satisfaction of no redirect rules\n      performRedirectCheck(op)\n\n      // Check for CDC metadata columns\n      performCdcMetadataCheck()\n\n      // Check for internal SetTransaction conflicts and dedup.\n      val finalActions = checkForSetTransactionConflictAndDedup(actions ++ this.actions.toSeq)\n\n      val identityOnlyMetadataUpdate = isIdentityOnlyMetadataUpdate()\n      // Update schema for IDENTITY column writes if necessary. This has to be called before\n      // `prepareCommit` because it might change metadata and `prepareCommit` is responsible for\n      // converting updated metadata into a `Metadata` action.\n      precommitUpdateSchemaWithIdentityHighWaterMarks()\n\n      // Try to commit at the next version.\n      var preparedActions =\n        executionObserver.preparingCommit {\n          prepareCommit(finalActions, op)\n        }\n\n      validateActionsAddFileInvariants(preparedActions, metadata)\n\n      // Find the isolation level to use for this commit\n      val isolationLevelToUse = getIsolationLevelToUse(preparedActions, op)\n\n      // Check for duplicated [[MetadataAction]] with the same domain names and validate the table\n      // feature is enabled if [[MetadataAction]] is submitted.\n      val domainMetadata =\n        DomainMetadataUtils.validateDomainMetadataSupportedAndNoDuplicate(finalActions, protocol)\n\n      isBlindAppend = {\n        val dependsOnFiles = !readPredicates.isEmpty || readFiles.nonEmpty\n        val onlyAddFiles =\n          preparedActions.collect { case f: FileAction => f }.forall(_.isInstanceOf[AddFile])\n        onlyAddFiles && !dependsOnFiles\n      }\n\n      val readRowIdHighWatermark =\n        RowId.extractHighWatermark(snapshot).getOrElse(RowId.MISSING_HIGH_WATER_MARK)\n\n      val autoTags = mutable.HashMap.empty[String, String]\n      if (identityOnlyMetadataUpdate) {\n        autoTags += (DeltaSourceUtils.IDENTITY_COMMITINFO_TAG -> \"true\")\n      }\n      val allTags = tags ++ autoTags\n\n      commitAttemptStartTimeMillis = clock.getTimeMillis()\n      commitInfo = CommitInfo(\n        time = commitAttemptStartTimeMillis,\n        operation = op.name,\n        inCommitTimestamp =\n          generateInCommitTimestampForFirstCommitAttempt(commitAttemptStartTimeMillis),\n        operationParameters = op.jsonEncodedValues,\n        commandContext = Map.empty,\n        readVersion = Some(readVersion).filter(_ >= 0),\n        isolationLevel = Option(isolationLevelToUse.toString),\n        isBlindAppend = Some(isBlindAppend),\n        operationMetrics = getOperationMetrics(op),\n        userMetadata = getUserMetadata(op),\n        tags = if (allTags.nonEmpty) Some(allTags) else None,\n        txnId = Some(txnId))\n\n      val firstAttemptVersion = getFirstAttemptVersion\n      val metadataUpdatedWithCoordinatedCommitsInfo = updateMetadataWithCoordinatedCommitsConfs()\n      val metadataUpdatedWithIctInfo = updateMetadataWithInCommitTimestamp(commitInfo)\n      if (metadataUpdatedWithIctInfo || metadataUpdatedWithCoordinatedCommitsInfo) {\n        preparedActions = preparedActions.map {\n          case _: Metadata => metadata\n          case other => other\n        }\n      }\n      val currentTransactionInfo = CurrentTransactionInfo(\n        txnId = txnId,\n        readPredicates = readPredicates.asScala.toVector,\n        readFiles = readFiles.toSet,\n        readWholeTable = readTheWholeTable,\n        readAppIds = readTxn.toSet,\n        metadata = metadata,\n        protocol = protocol,\n        actions = preparedActions,\n        readSnapshot = snapshot,\n        commitInfo = Some(commitInfo),\n        readRowIdHighWatermark = readRowIdHighWatermark,\n        catalogTable = catalogTable,\n        domainMetadata = domainMetadata,\n        op = op)\n\n      // Register post-commit hooks if any\n      lazy val hasFileActions = preparedActions.exists {\n        case _: FileAction => true\n        case _ => false\n      }\n      if (DeltaConfigs.SYMLINK_FORMAT_MANIFEST_ENABLED.fromMetaData(metadata) && hasFileActions) {\n        registerPostCommitHook(GenerateSymlinkManifest)\n      }\n\n      if (preparedActions.isEmpty && canSkipEmptyCommits &&\n          skipRecordingEmptyCommitAllowed(isolationLevelToUse)) {\n        return None\n      }\n\n      // Try to commit at the next version.\n      executionObserver.beginDoCommit()\n\n      val (commitVersion, postCommitSnapshot, updatedCurrentTransactionInfo) =\n        doCommitRetryIteratively(firstAttemptVersion, currentTransactionInfo, isolationLevelToUse)\n      setCommitted(commitVersion, postCommitSnapshot, updatedCurrentTransactionInfo.actions)\n      logInfo(log\"Committed delta #${MDC(DeltaLogKeys.VERSION, commitVersion)} to \" +\n        log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)}\")\n      commitVersion\n    } catch {\n      case e: DeltaConcurrentModificationException =>\n        recordConflictEvent(e)\n        executionObserver.transactionAborted()\n        throw e\n      case NonFatal(e) =>\n        recordDeltaEvent(\n          deltaLog, \"delta.commit.failure\", data = Map(\"exception\" -> Utils.exceptionString(e)))\n        executionObserver.transactionAborted()\n        throw e\n    }\n\n    runPostCommitHooks(committed.get)\n\n    executionObserver.transactionCommitted()\n    Some(version)\n  }\n\n  /**\n   * This method makes the necessary changes to Metadata based on ICT: If ICT is getting enabled as\n   * part of this commit, then it updates the Metadata with the ICT enablement information.\n   *\n   * @param commitInfo commitInfo for the commit\n   * @return true if changes were made to Metadata else false.\n   */\n  protected def updateMetadataWithInCommitTimestamp(commitInfo: CommitInfo): Boolean = {\n    val firstAttemptVersion = getFirstAttemptVersion\n    val metadataWithIctInfo = commitInfo.inCommitTimestamp\n      .flatMap { inCommitTimestamp =>\n        InCommitTimestampUtils.getUpdatedMetadataWithICTEnablementInfo(\n          spark, inCommitTimestamp, snapshot, metadata, firstAttemptVersion)\n      }.getOrElse { return false }\n    newMetadata = Some(metadataWithIctInfo)\n    true\n  }\n\n  /**\n   * This method makes the necessary changes to Metadata based on coordinated-commits: If the table\n   * is being converted from file-system to coordinated commits, then it registers the table with\n   * the commit-coordinator and updates the Metadata with the necessary configuration information\n   * from the commit-coordinator.\n   *\n   * @return A boolean which represents whether we have updated the table Metadata with\n   *         coordinated-commits information. If no changed were made, returns false.\n   */\n  protected def updateMetadataWithCoordinatedCommitsConfs(): Boolean = {\n    validateCoordinatedCommitsConfInMetadata(newMetadata)\n    val newCoordinatedCommitsTableConfOpt =\n      registerTableForCoordinatedCommitsIfNeeded(metadata, protocol)\n    val newCoordinatedCommitsTableConf = newCoordinatedCommitsTableConfOpt.getOrElse {\n      return false\n    }\n\n    // FS to CC conversion\n    val finalMetadata = metadata\n    val coordinatedCommitsTableConfJson = JsonUtils.toJson(newCoordinatedCommitsTableConf)\n    val extraKVConf =\n      DeltaConfigs.COORDINATED_COMMITS_TABLE_CONF.key -> coordinatedCommitsTableConfJson\n    newMetadata = Some(finalMetadata.copy(\n      configuration = finalMetadata.configuration + extraKVConf))\n    true\n  }\n\n  protected def validateCoordinatedCommitsConfInMetadata(newMetadataOpt: Option[Metadata]): Unit = {\n    // Validate that the [[DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF]] is json parse-able.\n    // Also do this validation if this table property has changed.\n    newMetadataOpt\n      .filter { newMetadata =>\n        val newCoordinatedCommitsConf =\n          newMetadata.configuration.get(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key)\n        val oldCoordinatedCommitsConf =\n          snapshot.metadata.configuration.get(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key)\n        newCoordinatedCommitsConf != oldCoordinatedCommitsConf\n      }.foreach(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.fromMetaData)\n  }\n\n\n  /** Whether to skip recording the commit in DeltaLog */\n  protected def skipRecordingEmptyCommitAllowed(isolationLevelToUse: IsolationLevel): Boolean = {\n    if (!spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SKIP_RECORDING_EMPTY_COMMITS)) {\n      return false\n    }\n    // Recording of empty commits in deltalog can be skipped only for SnapshotIsolation and\n    // Serializable mode.\n    Seq(SnapshotIsolation, Serializable).contains(isolationLevelToUse)\n  }\n\n  /**\n   * Create a large commit on the Delta log by directly writing an iterator of FileActions to the\n   * LogStore. This function only commits the next possible version and will not check whether the\n   * commit is retry-able. If the next version has already been committed, then this function\n   * will fail.\n   * This bypasses all optimistic concurrency checks. We assume that transaction conflicts should be\n   * rare because this method is typically used to create new tables (e.g. CONVERT TO DELTA) or\n   * apply some commands which rarely receive other transactions (e.g. CLONE/RESTORE).\n   * In addition, the expectation is that the list of actions performed by the transaction\n   * remains an iterator and is never materialized, given the nature of a large commit potentially\n   * touching many files.\n   * The `nonProtocolMetadataActions` parameter should only contain non-{protocol, metadata}\n   * actions only. If the protocol of table needs to be updated, it should be passed in the\n   * `newProtocolOpt` parameter.\n   */\n  def commitLarge(\n      spark: SparkSession,\n      nonProtocolMetadataActions: Iterator[Action],\n      newProtocolOpt: Option[Protocol],\n      op: DeltaOperations.Operation,\n      context: Map[String, String],\n      metrics: Map[String, String]\n  ): (Long, Snapshot) = recordDeltaOperation(deltaLog, \"delta.commit.large\") {\n    assert(committed.isEmpty, \"Transaction already committed.\")\n    commitStartNano = System.nanoTime()\n    val attemptVersion = getFirstAttemptVersion\n    executionObserver.preparingCommit()\n\n    // From this point onwards, newProtocolOpt should not be used.\n    // `newProtocol` or `protocol` should be used instead.\n    // The updateMetadataAndProtocolWithRequiredFeatures method will\n    // directly update the global `newProtocol` if needed.\n    newProtocol = newProtocolOpt\n    // If a feature requires another feature to be enabled, we enable the required\n    // feature in the metadata (if needed) and add it to the protocol.\n    // e.g. Coordinated Commits requires ICT and VacuumProtocolCheck to be enabled.\n    updateMetadataAndProtocolWithRequiredFeatures(newMetadata, newProtocol.toSeq)\n\n    def recordCommitLargeFailure(ex: Throwable, op: DeltaOperations.Operation): Unit = {\n      val coordinatedCommitsExceptionOpt = ex match {\n        case e: CommitFailedException => Some(e)\n        case _ => None\n      }\n      val data = Map(\n        \"exception\" -> Utils.exceptionString(ex),\n        \"operation\" -> op.name,\n        \"fromCoordinatedCommits\" -> coordinatedCommitsExceptionOpt.isDefined,\n        \"fromCoordinatedCommitsConflict\" ->\n          coordinatedCommitsExceptionOpt.map(_.getConflict).getOrElse(\"\"),\n        \"fromCoordinatedCommitsRetryable\" ->\n          coordinatedCommitsExceptionOpt.map(_.getRetryable).getOrElse(\"\"))\n      recordDeltaEvent(deltaLog, \"delta.commitLarge.failure\", data = data)\n    }\n\n    try {\n      val tags = Map.empty[String, String]\n      val commitTimestampMs = clock.getTimeMillis()\n      val commitInfo = CommitInfo(\n        commitTimestampMs,\n        operation = op.name,\n        generateInCommitTimestampForFirstCommitAttempt(commitTimestampMs),\n        operationParameters = op.jsonEncodedValues,\n        context,\n        readVersion = Some(readVersion),\n        isolationLevel = Some(Serializable.toString),\n        isBlindAppend = Some(false),\n        Some(metrics),\n        userMetadata = getUserMetadata(op),\n        tags = if (tags.nonEmpty) Some(tags) else None,\n        txnId = Some(txnId))\n\n      val assertDeletionVectorWellFormed = getAssertDeletionVectorWellFormedFunc(spark, op)\n      updateMetadataWithCoordinatedCommitsConfs()\n      updateMetadataWithInCommitTimestamp(commitInfo)\n\n      // Precompute NOT NULL partition columns for validation during action processing\n      val notNullPartitionCols = getNotNullPartitionCols(metadata)\n      var allActions =\n        Iterator(commitInfo, metadata) ++\n          nonProtocolMetadataActions ++\n          newProtocol.toIterator\n      allActions = allActions.map { action =>\n        action match {\n          case dm: DomainMetadata if isClusteringChangedOnUCManagedTable(dm) =>\n            // Temporary: block clustering changes on UC-managed tables (commitLarge() path).\n            // commitLarge() bypasses prepareCommit(), so this guard is needed separately.\n            // The check is intentionally inside the lazy map: commitLarge streams actions to\n            // avoid materialising large sets, so an eager pre-scan is not practical. The\n            // exception is thrown before any data is written to the commit coordinator because\n            // the iterator is consumed first during serialisation.\n            throw DeltaErrors.operationNotSupportedException(\n              \"Clustering column changes on Unity Catalog managed tables\")\n          case a: AddFile =>\n            assertDeletionVectorWellFormed(a)\n            validateAddFileInvariants(a, notNullPartitionCols)\n          case p: Protocol =>\n            recordProtocolChanges(\n              \"delta.protocol.change\",\n              fromProtocol = snapshot.protocol,\n              toProtocol = p,\n              isCreatingNewTable)\n            DeltaTableV2.withEnrichedUnsupportedTableException(catalogTable) {\n              deltaLog.protocolWrite(p)\n            }\n          case _ =>\n        }\n        action\n      }\n      val (allActions2, acStatsCollector) = collectAutoOptimizeStats(allActions)\n      allActions = allActions2\n\n      // Validate protocol support, specifically writer features.\n      DeltaTableV2.withEnrichedUnsupportedTableException(catalogTable) {\n        deltaLog.protocolWrite(snapshot.protocol)\n      }\n\n      allActions = RowId.assignFreshRowIds(spark, protocol, snapshot, allActions, op)\n      allActions = DefaultRowCommitVersion.assignIfMissing(\n        spark, protocol, snapshot, allActions, getFirstAttemptVersion)\n\n      val commitStatsComputer = new CommitStatsComputer()\n      allActions = commitStatsComputer.addToCommitStats(allActions)\n      executionObserver.beginDoCommit()\n      if (readVersion < 0) {\n        deltaLog.createLogDirectoriesIfNotExists()\n      }\n      val fsWriteStartNano = System.nanoTime()\n      val jsonActions = allActions.map(_.json)\n      var commitSizeBytes = 0L\n      jsonActions.map { action =>\n          commitSizeBytes += action.size\n      }\n      val effectiveTableCommitCoordinatorClient =\n        readSnapshotTableCommitCoordinatorClientOpt.getOrElse {\n          TableCommitCoordinatorClient(\n            commitCoordinatorClient = new FileSystemBasedCommitCoordinatorClient(deltaLog),\n            deltaLog = deltaLog,\n            coordinatedCommitsTableConf = snapshot.metadata.coordinatedCommitsTableConf)\n        }\n      val updatedActions = new UpdatedActions(\n        commitInfo, metadata, protocol, snapshot.metadata, snapshot.protocol)\n      val commitResponse = TransactionExecutionObserver.withObserver(executionObserver) {\n        effectiveTableCommitCoordinatorClient.commit(\n          attemptVersion, jsonActions, updatedActions, catalogTable.map(_.identifier))\n      }\n      // TODO(coordinated-commits): Use the right timestamp method on top of CommitInfo once ICT is\n      //  merged.\n      partitionsAddedToOpt = Some(commitStatsComputer.getPartitionsAddedByTransaction)\n      // If the metadata didn't change, `newMetadata` is empty, and we can re-use the old id.\n      acStatsCollector.finalizeStats(newMetadata.map(_.id).getOrElse(snapshot.metadata.id))\n      spark.sessionState.conf.setConf(\n        DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION,\n        Some(attemptVersion))\n      commitEndNano = System.nanoTime()\n      executionObserver.beginPostCommit()\n      // NOTE: commitLarge cannot run postCommitHooks (such as the CheckpointHook).\n      // Instead, manually run any necessary actions in updateAndCheckpoint.\n      val postCommitSnapshot = updateAndCheckpoint(\n        spark,\n        deltaLog,\n        commitStatsComputer.getNumActions,\n        attemptVersion,\n        commitResponse.getCommit,\n        txnId)\n      setCommitted(attemptVersion, postCommitSnapshot, committedActions = Seq.empty)\n      val postCommitReconstructionTime = System.nanoTime()\n      commitStatsComputer.finalizeAndEmitCommitStats(\n        spark,\n        attemptVersion,\n        snapshot.version,\n        commitDurationMs = NANOSECONDS.toMillis(commitEndNano - commitStartNano),\n        fsWriteDurationMs = NANOSECONDS.toMillis(commitEndNano - fsWriteStartNano),\n        txnExecutionTimeMs = NANOSECONDS.toMillis(commitEndNano - txnStartTimeNs),\n        stateReconstructionDurationMs =\n            NANOSECONDS.toMillis(postCommitReconstructionTime - commitEndNano),\n        postCommitSnapshot,\n        // We manually triggered a checkpoint in `updateAndCheckpoint` above.\n        computedNeedsCheckpoint = true,\n        isolationLevel = Serializable,\n        commitInfoOpt = Some(commitInfo),\n        commitSizeBytes = commitSizeBytes\n      )\n\n      executionObserver.transactionCommitted()\n      (attemptVersion, postCommitSnapshot)\n    } catch {\n      case e: Throwable =>\n        e match {\n          case CommitConflictFailure(e) =>\n            recordCommitLargeFailure(e, op)\n            // Actions of a commit which went in before ours.\n            // Requires updating deltaLog to retrieve these actions, as another writer may have used\n            // CommitCoordinatorClient for writing.\n            val fileProvider = DeltaCommitFileProvider(\n              deltaLog.update(catalogTableOpt = catalogTable))\n            val logs = deltaLog.store.readAsIterator(\n              fileProvider.deltaFile(attemptVersion),\n              deltaLog.newDeltaHadoopConf())\n            try {\n              val winningCommitActions = logs.map(Action.fromJson)\n              val commitInfo = winningCommitActions.collectFirst { case a: CommitInfo => a }\n                .map(ci => ci.copy(version = Some(attemptVersion)))\n              throw DeltaErrors.concurrentWriteException(commitInfo)\n            } finally {\n              logs.close()\n              executionObserver.transactionAborted()\n            }\n          case NonFatal(_) =>\n            recordCommitLargeFailure(e, op)\n            executionObserver.transactionAborted()\n            throw e\n          case _ =>\n            throw e\n        }\n    }\n  }\n\n  /**\n   * Splits a transaction into smaller child transactions that operate on disjoint sets of the files\n   * read by the parent transaction. This function is typically used when you want to break a large\n   * operation into one that can be committed separately / incrementally.\n   *\n   * @param readFilesSubset The subset of files read by the current transaction that will be handled\n   *                        by the new transaction.\n   */\n  def split(readFilesSubset: Seq[AddFile]): OptimisticTransaction = {\n    assert(newMetadata.isEmpty)\n    assert(OptimisticTransaction.getActive().isEmpty,\n      \"Splitting a transaction is not supported when there is an active transaction.\")\n\n    val t = new OptimisticTransaction(deltaLog, catalogTable, snapshot)\n    t.executionObserver = executionObserver.createChild()\n    t.readPredicates.addAll(readPredicates)\n    t.readFiles ++= readFilesSubset\n    t.readTxn ++= readTxn\n    t\n  }\n\n  /**\n   * This method registers the table with the commit-coordinator via the [[CommitCoordinatorClient]]\n   * if the table is transitioning from file-system based table to coordinated-commits table.\n   * @param finalMetadata the effective [[Metadata]] of the table. Note that this refers to the\n   *                      new metadata if this commit is updating the table Metadata.\n   * @param finalProtocol the effective [[Protocol]] of the table. Note that this refers to the\n   *                      new protocol if this commit is updating the table Protocol.\n   * @return The new coordinated-commits table metadata if the table is transitioning from\n   *         file-system based table to coordinated-commits table. Otherwise, None.\n   *         This metadata should be added to the [[Metadata.configuration]] before doing the\n   *         commit.\n   */\n  protected def registerTableForCoordinatedCommitsIfNeeded(\n      finalMetadata: Metadata,\n      finalProtocol: Protocol): Option[Map[String, String]] = {\n    val (oldOwnerName, oldOwnerConf) =\n      CoordinatedCommitsUtils.getCoordinatedCommitsConfs(snapshot.metadata)\n    var newCoordinatedCommitsTableConf: Option[Map[String, String]] = None\n    if (finalMetadata.configuration != snapshot.metadata.configuration || snapshot.version == -1L) {\n      val newCommitCoordinatorClientOpt = CoordinatedCommitsUtils.getCommitCoordinatorClient(\n        spark, deltaLog, finalMetadata, finalProtocol, failIfImplUnavailable = true)\n      (newCommitCoordinatorClientOpt, readSnapshotTableCommitCoordinatorClientOpt) match {\n        case (Some(newCommitCoordinatorClient), None) =>\n          // FS -> CC conversion\n          val (commitCoordinatorName, commitCoordinatorConf) =\n            CoordinatedCommitsUtils.getCoordinatedCommitsConfs(finalMetadata)\n          logInfo(log\"Table ${MDC(DeltaLogKeys.PATH, deltaLog.logPath)} transitioning from \" +\n            log\"file-system based table to coordinated-commits table: \" +\n            log\"[commit-coordinator: ${MDC(DeltaLogKeys.COORDINATOR_NAME, commitCoordinatorName)}\" +\n            log\", conf: ${MDC(DeltaLogKeys.COORDINATOR_CONF, commitCoordinatorConf)}]\")\n          val tableIdentifierOpt =\n            CoordinatedCommitsUtils.toCCTableIdentifier(catalogTable.map(_.identifier))\n          newCoordinatedCommitsTableConf = Some(newCommitCoordinatorClient.registerTable(\n            deltaLog.logPath,\n            tableIdentifierOpt,\n            readVersion,\n            finalMetadata,\n            protocol).asScala.toMap)\n        case (None, Some(readCommitCoordinatorClient)) =>\n          // CC -> FS conversion\n          val (newOwnerName, newOwnerConf) =\n            CoordinatedCommitsUtils.getCoordinatedCommitsConfs(snapshot.metadata)\n          logInfo(log\"Table ${MDC(DeltaLogKeys.PATH, deltaLog.logPath)} transitioning from \" +\n            log\"coordinated-commits table to file-system table: \" +\n            log\"[commit-coordinator: ${MDC(DeltaLogKeys.COORDINATOR_NAME, newOwnerName)}, \" +\n            log\"conf: ${MDC(DeltaLogKeys.COORDINATOR_CONF, newOwnerConf)}]\")\n        case (Some(newCommitCoordinatorClient), Some(readCommitCoordinatorClient))\n            if !readCommitCoordinatorClient.semanticsEquals(newCommitCoordinatorClient) =>\n          // CC1 -> CC2 conversion is not allowed.\n          // In order to transfer the table from one commit-coordinator to another, transfer the\n          // table from current commit-coordinator to filesystem first and then filesystem to the\n          // commit-coordinator.\n          val (newOwnerName, newOwnerConf) =\n            CoordinatedCommitsUtils.getCoordinatedCommitsConfs(finalMetadata)\n          val message = s\"Transition of table ${deltaLog.logPath} from one commit-coordinator to\" +\n            s\" another commit-coordinator is not allowed: [old commit-coordinator: $oldOwnerName,\" +\n            s\" new commit-coordinator: $newOwnerName, old commit-coordinator conf: $oldOwnerConf,\" +\n            s\" new commit-coordinator conf: $newOwnerConf].\"\n          throw new IllegalStateException(message)\n        case _ =>\n          // no owner change\n          ()\n      }\n    }\n    newCoordinatedCommitsTableConf\n  }\n\n  /** Update the table now that the commit has been made, and write a checkpoint. */\n  protected def updateAndCheckpoint(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      commitSize: Int,\n      attemptVersion: Long,\n      commit: Commit,\n      txnId: String): Snapshot = {\n\n    val currentSnapshot = deltaLog.updateAfterCommit(\n      attemptVersion,\n      commit,\n      newChecksumOpt = None,\n      preCommitLogSegment = preCommitLogSegment,\n      catalogTable)\n    if (currentSnapshot.version != attemptVersion) {\n      throw DeltaErrors.invalidCommittedVersion(attemptVersion, currentSnapshot.version)\n    }\n\n    logInfo(log\"Committed delta #${MDC(DeltaLogKeys.VERSION, attemptVersion)} to \" +\n      log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)}. Wrote \" +\n      log\"${MDC(DeltaLogKeys.NUM_ACTIONS, commitSize.toLong)} actions.\")\n\n    deltaLog.checkpoint(currentSnapshot, catalogTable)\n    currentSnapshot\n  }\n\n  /**\n   * A metadata update can enable a feature that requires a protocol upgrade.\n   * Furthermore, a feature can have dependencies on other features. This method\n   * enables the dependent features in the metadata.\n   * It then updates the protocol with the features enabled by the metadata.\n   * The global `newMetadata` and `newProtocol` are updated with the new\n   * metadata and protocol if needed.\n   * @param metadataOpt The new metadata that is being set.\n   * @param protocols The new protocols that are being set.\n   */\n  protected def updateMetadataAndProtocolWithRequiredFeatures(\n      metadataOpt: Option[Metadata], protocols: Seq[Protocol]): Unit = {\n    metadataOpt.foreach { m =>\n      assertMetadata(m)\n      val metadataWithRequiredFeatureEnablementFlags =\n        getMetadataWithDependentFeaturesEnabled(m, protocols)\n      setNewProtocolWithFeaturesEnabledByMetadata(metadataWithRequiredFeatureEnablementFlags)\n\n      // Also update `newMetadata` so that the behaviour later is consistent irrespective of whether\n      // metadata was set via `updateMetadata` or `actions`.\n      newMetadata = Some(metadataWithRequiredFeatureEnablementFlags)\n    }\n  }\n\n  /**\n   * Prepare for a commit by doing all necessary pre-commit checks and modifications to the actions.\n   * @return The finalized set of actions.\n   */\n  protected def prepareCommit(\n      actions: Seq[Action],\n      op: DeltaOperations.Operation): Seq[Action] = {\n\n    assert(committed.isEmpty, \"Transaction already committed.\")\n\n    val (metadatasAndProtocols, otherActions) = actions\n      .partition(a => a.isInstanceOf[Metadata] || a.isInstanceOf[Protocol])\n\n    // New metadata can come either from `newMetadata` or from the `actions` there.\n    val metadataChanges =\n      newMetadata.toSeq ++ metadatasAndProtocols.collect { case m: Metadata => m }\n    if (metadataChanges.length > 1) {\n      recordDeltaEvent(deltaLog, \"delta.metadataCheck.multipleMetadataActions\", data = Map(\n        \"metadataChanges\" -> metadataChanges\n      ))\n      assert(\n        metadataChanges.length <= 1, \"Cannot change the metadata more than once in a transaction.\")\n    }\n    // There be at most one metadata entry at this point.\n    // Update the global `newMetadata` and `newProtocol` with any extra metadata and protocol\n    // changes needed for pre-requisite features.\n    val protocolActions = metadatasAndProtocols.collect { case p: Protocol => p }\n    val protocolChangesBeforeUpdate = newProtocol.toSeq ++ protocolActions\n    updateMetadataAndProtocolWithRequiredFeatures(\n      metadataChanges.headOption, protocolChangesBeforeUpdate)\n\n    // A protocol change can be *explicit*, i.e. specified as a Protocol action as part of the\n    // commit actions, or *implicit*. Implicit protocol changes are mostly caused by setting\n    // new table properties that enable features that require a protocol upgrade. These implicit\n    // changes are usually captured in newProtocol. In case there is more than one protocol action,\n    // it is likely that it is due to a mix of explicit and implicit changes.\n    val protocolChanges = newProtocol.toSeq ++ protocolActions\n    if (protocolChanges.length > 1) {\n      recordDeltaEvent(deltaLog, \"delta.protocolCheck.multipleProtocolActions\", data = Map(\n        \"protocolChanges\" -> protocolChanges\n      ))\n      assert(protocolChanges.length <= 1, \"Cannot change the protocol more than once in a \" +\n        \"transaction. More than one protocol change in a transaction is likely due to an \" +\n        \"explicitly specified Protocol action and an implicit protocol upgrade triggered by \" +\n        \"a table property.\")\n    }\n    // Update newProtocol so that the behaviour later is consistent irrespective of whether\n    // the protocol was set via update/verifyMetadata or actions.\n    // NOTE: There is at most one protocol change at this point.\n    protocolChanges.foreach { p =>\n      newProtocol = Some(p)\n      recordProtocolChanges(\n        \"delta.protocol.change\",\n        snapshot.protocol,\n        p,\n        isCreatingNewTable,\n        operationNameOpt = Some(op.name))\n      DeltaTableV2.withEnrichedUnsupportedTableException(catalogTable) {\n        deltaLog.protocolWrite(p)\n      }\n    }\n\n    // Now, we know that there is at most 1 Metadata change (stored in newMetadata) and at most 1\n    // Protocol change (stored in newProtocol)\n\n    val (protocolUpdate1, metadataUpdate1) =\n      UniversalFormat.enforceInvariantsAndDependencies(\n        spark,\n        catalogTable,\n        // Note: if this txn has no protocol or metadata updates, then `prev` will equal `newest`.\n        snapshot,\n        newestProtocol = protocol, // Note: this will try to use `newProtocol`\n        newestMetadata = metadata, // Note: this will try to use `newMetadata`\n        Some(op),\n        otherActions\n      )\n    newProtocol = protocolUpdate1.orElse(newProtocol)\n    newMetadata = metadataUpdate1.orElse(newMetadata)\n\n    var finalActions = newMetadata.toSeq ++ newProtocol.toSeq ++ otherActions\n\n    // Block future cases of CDF + Column Mapping changes + file changes\n    // This check requires having called\n    // DeltaColumnMapping.checkColumnIdAndPhysicalNameAssignments which is done in the\n    // `assertMetadata` call above.\n    performCdcColumnMappingCheck(finalActions, op)\n\n    // Ensure Commit Directory exists when coordinated commits is enabled on an existing table.\n    lazy val isFsToCcConversion = snapshot.metadata.coordinatedCommitsCoordinatorName.isEmpty &&\n      newMetadata.flatMap(_.coordinatedCommitsCoordinatorName).nonEmpty\n    val shouldCreateLogDirs = snapshot.version == -1 || isFsToCcConversion\n    if (shouldCreateLogDirs) {\n      deltaLog.createLogDirectoriesIfNotExists()\n    }\n\n    if (snapshot.version == -1) {\n      // If this is the first commit and no protocol is specified, initialize the protocol version.\n      if (!finalActions.exists(_.isInstanceOf[Protocol])) {\n        finalActions = protocol +: finalActions\n      }\n      // If this is the first commit and no metadata is specified, throw an exception\n      if (!finalActions.exists(_.isInstanceOf[Metadata])) {\n        recordDeltaEvent(\n          deltaLog,\n          opType = \"delta.metadataCheck.noMetadataInInitialCommit\",\n          data =\n            Map(\"stacktrace\" -> Thread.currentThread.getStackTrace.toSeq.take(20).mkString(\"\\n\\t\"))\n        )\n        if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED)) {\n          throw DeltaErrors.metadataAbsentException()\n        }\n        logWarning(\n          log\"Detected no metadata in initial commit but commit validation was turned off.\")\n      }\n    }\n\n    // Validate that partition column changes are only performed by allowed operations.\n    validatePartitionColumnChanges(op, newMetadata)\n\n    val partitionColumns = metadata.physicalPartitionSchema.fieldNames.toSet\n    finalActions = finalActions.map {\n      case newVersion: Protocol =>\n        require(newVersion.minReaderVersion > 0, \"The reader version needs to be greater than 0\")\n        require(newVersion.minWriterVersion > 0, \"The writer version needs to be greater than 0\")\n        if (!canAssignAnyNewProtocol) {\n          val currentVersion = snapshot.protocol\n          if (!currentVersion.canTransitionTo(newVersion, op)) {\n            throw new ProtocolDowngradeException(currentVersion, newVersion)\n          }\n        }\n        newVersion\n\n      case a: AddFile if partitionColumns != a.partitionValues.keySet =>\n        // If the partitioning in metadata does not match the partitioning in the AddFile\n        recordDeltaEvent(deltaLog, \"delta.metadataCheck.partitionMismatch\", data = Map(\n          \"tablePartitionColumns\" -> metadata.partitionColumns,\n          \"filePartitionValues\" -> a.partitionValues\n        ))\n        if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED)) {\n          throw DeltaErrors.addFilePartitioningMismatchException(\n            a.partitionValues.keySet.toSeq, partitionColumns.toSeq)\n        }\n        logWarning(\n          log\"\"\"\n             |Detected mismatch in partition values between AddFile and table metadata but\n             |commit validation was turned off.\n             |To turn it back on set\n             |${MDC(DeltaLogKeys.CONFIG_KEY, DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED.key)}\n             |to \"true\"\n          \"\"\".stripMargin)\n        a\n      case other => other\n    }\n\n    DeltaTableV2.withEnrichedUnsupportedTableException(catalogTable) {\n      newProtocol.foreach(deltaLog.protocolWrite)\n      deltaLog.protocolWrite(snapshot.protocol)\n    }\n\n    finalActions = RowId.assignFreshRowIds(\n      spark, protocol, snapshot, finalActions.toIterator, op).toList\n    finalActions = DefaultRowCommitVersion.assignIfMissing(\n      spark, protocol, snapshot, finalActions.toIterator, getFirstAttemptVersion).toList\n\n    // We make sure that this isn't an appendOnly table as we check if we need to delete\n    // files.\n    val removes = actions.collect { case r: RemoveFile => r }\n    if (removes.exists(_.dataChange)) DeltaLog.assertRemovable(snapshot)\n\n    val assertDeletionVectorWellFormed = getAssertDeletionVectorWellFormedFunc(spark, op)\n    actions.foreach(assertDeletionVectorWellFormed)\n\n    // Make sure shredded writes are only performed if the shredding table property was set\n    assertShreddingStateConsistent()\n\n    // Make sure this operation does not include default column values if the corresponding table\n    // feature is not enabled.\n    if (!protocol.isFeatureSupported(AllowColumnDefaultsTableFeature)) {\n      checkNoColumnDefaults(op)\n    }\n\n    finalActions\n  }\n\n  // Returns the isolation level to use for committing the transaction\n  protected def getIsolationLevelToUse(\n      preparedActions: Seq[Action], op: DeltaOperations.Operation): IsolationLevel = {\n    val isolationLevelToUse =\n      if (canDowngradeToSnapshotIsolation(preparedActions, op.changesData)) {\n        SnapshotIsolation\n      } else {\n        getDefaultIsolationLevel()\n      }\n    isolationLevelToUse\n  }\n\n  /** Log protocol change events. */\n  private def recordProtocolChanges(\n      opType: String,\n      fromProtocol: Protocol,\n      toProtocol: Protocol,\n      isCreatingNewTable: Boolean,\n      operationNameOpt: Option[String] = None): Unit = {\n    val payload: Map[String, Any] = if (isCreatingNewTable) {\n      Map(\"toProtocol\" -> toProtocol.fieldsForLogging,\n        \"operationName\" -> \"CREATE TABLE\")\n    } else {\n      Map(\n        \"fromProtocol\" -> fromProtocol.fieldsForLogging,\n        \"toProtocol\" -> toProtocol.fieldsForLogging,\n        \"operationName\" -> operationNameOpt.orNull)\n    }\n    recordDeltaEvent(deltaLog, opType, data = payload)\n  }\n\n  private[delta] def isCommitLockEnabled: Boolean = {\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_COMMIT_LOCK_ENABLED).getOrElse(\n      deltaLog.store.isPartialWriteVisible(deltaLog.logPath, deltaLog.newDeltaHadoopConf()))\n  }\n\n  private def lockCommitIfEnabled[T](body: => T): T = {\n    if (isCommitLockEnabled) {\n      // We are borrowing the `snapshotLock` even for commits. Ideally we should be\n      // using a separate lock for this purpose, because multiple threads fighting over\n      // a commit shouldn't interfere with normal snapshot updates by readers.\n      deltaLog.withSnapshotLockInterruptibly(body)\n    } else {\n      body\n    }\n  }\n\n  /**\n   * Commit the txn represented by `currentTransactionInfo` using `attemptVersion` version number.\n   * If there are any conflicts that are found, we will retry a fixed number of times.\n   *\n   * @return the real version that was committed, the postCommitSnapshot, and the txn info\n   *         NOTE: The postCommitSnapshot may not be the same as the version committed if racing\n   *         commits were written while we updated the snapshot.\n   */\n  protected def doCommitRetryIteratively(\n      attemptVersion: Long,\n      currentTransactionInfo: CurrentTransactionInfo,\n      isolationLevel: IsolationLevel)\n    : (Long, Snapshot, CurrentTransactionInfo) = recordDeltaOperation(\n      deltaLog, \"delta.commit.allAttempts\") {\n    lockCommitIfEnabled {\n      var commitVersion = attemptVersion\n      var updatedCurrentTransactionInfo = currentTransactionInfo\n      val isFsToCcCommit =\n        snapshot.metadata.coordinatedCommitsCoordinatorName.isEmpty &&\n          metadata.coordinatedCommitsCoordinatorName.nonEmpty\n      val maxRetryAttempts = spark.conf.get(DeltaSQLConf.DELTA_MAX_RETRY_COMMIT_ATTEMPTS)\n      val maxNonConflictRetryAttempts =\n        spark.conf.get(DeltaSQLConf.DELTA_MAX_NON_CONFLICT_RETRY_COMMIT_ATTEMPTS)\n      var nonConflictAttemptNumber = 0\n      var shouldCheckForConflicts = false\n\n      for (attemptNumber <- 0 to maxRetryAttempts) {\n        try {\n          val postCommitSnapshot = if (!shouldCheckForConflicts) {\n            doCommit(commitVersion, updatedCurrentTransactionInfo, attemptNumber, isolationLevel)\n          } else recordDeltaOperation(deltaLog, \"delta.commit.retry\") {\n            val (newCommitVersion, newCurrentTransactionInfo) = checkForConflicts(\n              commitVersion, updatedCurrentTransactionInfo, attemptNumber, isolationLevel)\n            commitVersion = newCommitVersion\n            updatedCurrentTransactionInfo = newCurrentTransactionInfo\n            doCommit(commitVersion, updatedCurrentTransactionInfo, attemptNumber, isolationLevel)\n          }\n          return (commitVersion, postCommitSnapshot, updatedCurrentTransactionInfo)\n        } catch {\n          case _: FileAlreadyExistsException if isFsToCcCommit =>\n            // Don't retry if this commit tries to upgrade the table from filesystem to managed\n            // commits and the first attempt failed due to a conflict.\n            throw DeltaErrors.concurrentWriteException(conflictingCommit = None)\n          case _: FileAlreadyExistsException\n            if readSnapshotTableCommitCoordinatorClientOpt.isEmpty =>\n            // For filesystem based tables, we use LogStore to do the commit. On a conflict,\n            // LogStore returns FileAlreadyExistsException necessitating conflict resolution.\n            // For commit-coordinators, FileAlreadyExistsException isn't expected under normal\n            // operations and thus retries are not performed if this exception is thrown by\n            // CommitCoordinatorClient.\n            shouldCheckForConflicts = true\n            // Do nothing, retry with next available attemptVersion\n          case ex: CommitFailedException if ex.getRetryable && ex.getConflict =>\n            shouldCheckForConflicts = true\n            // Reset nonConflictAttemptNumber if a conflict is detected.\n            nonConflictAttemptNumber = 0\n            // For coordinated-commits, only retry with next available attemptVersion when\n            // retryable is set and it was a case of conflict.\n          case ex: CommitFailedException if ex.getRetryable && !ex.getConflict =>\n            if (nonConflictAttemptNumber < maxNonConflictRetryAttempts) {\n              nonConflictAttemptNumber += 1\n            } else {\n              // Rethrow the exception if max retries for non-conflict case have been reached\n              throw ex\n            }\n        }\n      }\n\n      // retries all failed\n      val totalCommitAttemptTime = clock.getTimeMillis() - commitAttemptStartTimeMillis\n      throw DeltaErrors.maxCommitRetriesExceededException(\n        maxRetryAttempts + 1,\n        commitVersion,\n        attemptVersion,\n        updatedCurrentTransactionInfo.finalActionsToCommit.length,\n        totalCommitAttemptTime)\n    }\n  }\n\n  /**\n   * Commit `actions` using `attemptVersion` version number. Throws a FileAlreadyExistsException\n   * if any conflicts are detected.\n   *\n   * @return the post-commit snapshot of the deltaLog\n   */\n  protected def doCommit(\n      attemptVersion: Long,\n      currentTransactionInfo: CurrentTransactionInfo,\n      attemptNumber: Int,\n      isolationLevel: IsolationLevel): Snapshot = {\n    val actions = currentTransactionInfo.finalActionsToCommit\n    logInfo(\n      log\"Attempting to commit version ${MDC(DeltaLogKeys.VERSION, attemptVersion)} with \" +\n      log\"${MDC(DeltaLogKeys.NUM_ACTIONS, actions.size.toLong)} actions with \" +\n      log\"${MDC(DeltaLogKeys.ISOLATION_LEVEL, isolationLevel)} isolation level\")\n\n    if (readVersion > -1 && metadata.id != snapshot.metadata.id) {\n      val msg = s\"Change in the table id detected in txn. Table id for txn on table at \" +\n        s\"${deltaLog.dataPath} was ${snapshot.metadata.id} when the txn was created and \" +\n        s\"is now changed to ${metadata.id}.\"\n      logWarning(msg)\n      recordDeltaEvent(deltaLog, \"delta.metadataCheck.commit\", data = Map(\n        \"readSnapshotVersion\" -> snapshot.version,\n        \"readSnapshotMetadata\" -> snapshot.metadata,\n        \"txnMetadata\" -> metadata,\n        \"commitAttemptVersion\" -> attemptVersion,\n        \"commitAttemptNumber\" -> attemptNumber))\n    }\n\n    val fsWriteStartNano = System.nanoTime()\n    val jsonActions = actions.map(_.json)\n\n    val (newChecksumOpt, commit) =\n      writeCommitFile(attemptVersion, jsonActions.toIterator, currentTransactionInfo)\n\n    spark.sessionState.conf.setConf(\n      DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION,\n      Some(attemptVersion))\n\n    commitEndNano = System.nanoTime()\n\n    executionObserver.beginPostCommit()\n    val postCommitSnapshot = deltaLog.updateAfterCommit(\n      attemptVersion,\n      commit,\n      newChecksumOpt,\n      preCommitLogSegment,\n      catalogTable)\n    val postCommitReconstructionTime = System.nanoTime()\n    needsCheckpoint = isCheckpointNeeded(attemptVersion, postCommitSnapshot)\n    val commitStatsComputer = new CommitStatsComputer()\n    // Add to commit stats and consume the returned iterator.\n    commitStatsComputer.addToCommitStats(actions.toIterator).foreach(_ => ())\n    partitionsAddedToOpt = Some(commitStatsComputer.getPartitionsAddedByTransaction)\n    collectAutoOptimizeStatsAndFinalize(actions, deltaLog.unsafeVolatileTableId)\n    val commitSizeBytes: Long = jsonActions.map(_.length.toLong).sum\n    commitStatsComputer.finalizeAndEmitCommitStats(\n      spark,\n      attemptVersion,\n      startVersion = snapshot.version,\n      commitDurationMs = NANOSECONDS.toMillis(commitEndNano - commitStartNano),\n      fsWriteDurationMs = NANOSECONDS.toMillis(commitEndNano - fsWriteStartNano),\n      txnExecutionTimeMs = NANOSECONDS.toMillis(commitEndNano - txnStartNano),\n      stateReconstructionDurationMs =\n        NANOSECONDS.toMillis(postCommitReconstructionTime - commitEndNano),\n      postCommitSnapshot = postCommitSnapshot,\n      computedNeedsCheckpoint = needsCheckpoint,\n      isolationLevel = isolationLevel,\n      commitInfoOpt = currentTransactionInfo.commitInfo,\n      commitSizeBytes = commitSizeBytes\n    )\n\n    postCommitSnapshot\n  }\n\n  class FileSystemBasedCommitCoordinatorClient(val deltaLog: DeltaLog)\n    extends CommitCoordinatorClient {\n    override def commit(\n        logStore: io.delta.storage.LogStore,\n        hadoopConf: Configuration,\n        tableDesc: TableDescriptor,\n        commitVersion: Long,\n        actions: java.util.Iterator[String],\n        updatedActions: UpdatedActions): CommitResponse = {\n      val logPath = tableDesc.getLogPath\n      // Get thread local observer for Fuzz testing purpose.\n      val executionObserver = TransactionExecutionObserver.getObserver\n      val commitFile = util.FileNames.unsafeDeltaFile(logPath, commitVersion)\n      val commitFileStatus =\n        doCommit(logStore, hadoopConf, logPath, commitFile, commitVersion, actions)\n      executionObserver.beginBackfill()\n      val ictEnabled = updatedActions.getNewMetadata.getConfiguration.asScala.getOrElse(\n        DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key, \"false\") == \"true\"\n      val commitTimestamp = if (ictEnabled) {\n        // CommitInfo.getCommitTimestamp will return the inCommitTimestamp.\n        updatedActions.getCommitInfo.getCommitTimestamp\n      } else {\n        commitFileStatus.getModificationTime\n      }\n      new CommitResponse(new Commit(\n        commitVersion,\n        commitFileStatus,\n        commitTimestamp\n      ))\n    }\n\n    protected def doCommit(\n        logStore: io.delta.storage.LogStore,\n        hadoopConf: Configuration,\n        logPath: Path,\n        commitFile: Path,\n        commitVersion: Long,\n        actions: java.util.Iterator[String]): FileStatus = {\n      logStore.write(commitFile, actions, false, hadoopConf)\n      logPath.getFileSystem(hadoopConf).getFileStatus(commitFile)\n    }\n\n    override def getCommits(\n        tableDesc: TableDescriptor,\n        startVersion: java.lang.Long,\n        endVersion: java.lang.Long): GetCommitsResponse =\n      new GetCommitsResponse(Seq.empty.asJava, -1)\n\n    override def backfillToVersion(\n        logStore: io.delta.storage.LogStore,\n        hadoopConf: Configuration,\n        tableDesc: TableDescriptor,\n        version: Long,\n        lastKnownBackfilledVersion: java.lang.Long): Unit = {}\n\n    /**\n     * [[FileSystemBasedCommitCoordinatorClient]] is supposed to be treated as a singleton object\n     * for a Delta Log and is equal to all other instances of\n     * [[FileSystemBasedCommitCoordinatorClient]] for the same Delta Log.\n     */\n    override def semanticEquals(other: CommitCoordinatorClient): Boolean = {\n      other match {\n        case fsCommitCoordinatorClient: FileSystemBasedCommitCoordinatorClient =>\n          fsCommitCoordinatorClient.deltaLog == deltaLog\n        case _ => false\n      }\n    }\n\n    override def registerTable(\n        logPath: Path,\n        tableIdentifier: Optional[TableIdentifier],\n        currentVersion: Long,\n        currentMetadata: AbstractMetadata,\n        currentProtocol: AbstractProtocol): java.util.Map[String, String] =\n      Map.empty[String, String].asJava\n  }\n\n  /**\n   * Writes the json actions provided to the commit file corresponding to attemptVersion.\n   * If coordinated-commits are enabled, this method must return a non-empty [[Commit]]\n   * since we can't guess it from the FileSystem.\n   */\n  protected def writeCommitFile(\n      attemptVersion: Long,\n      jsonActions: Iterator[String],\n      currentTransactionInfo: CurrentTransactionInfo)\n      : (Option[VersionChecksum], Commit) = {\n    val commitCoordinatorClient = readSnapshotTableCommitCoordinatorClientOpt.getOrElse {\n      TableCommitCoordinatorClient(\n        new FileSystemBasedCommitCoordinatorClient(deltaLog),\n        deltaLog,\n        snapshot.metadata.coordinatedCommitsTableConf)\n    }\n    val commitFile = writeCommitFileImpl(\n      attemptVersion, jsonActions, commitCoordinatorClient, currentTransactionInfo)\n    val newChecksumOpt = incrementallyDeriveChecksum(attemptVersion, currentTransactionInfo)\n    (newChecksumOpt, commitFile)\n  }\n\n  protected def writeCommitFileImpl(\n    attemptVersion: Long,\n    jsonActions: Iterator[String],\n    tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n    currentTransactionInfo: CurrentTransactionInfo\n  ): Commit = {\n    val updatedActions =\n      currentTransactionInfo.getUpdatedActions(snapshot.metadata, snapshot.protocol)\n    val commitResponse = TransactionExecutionObserver.withObserver(executionObserver) {\n      tableCommitCoordinatorClient.commit(\n        attemptVersion, jsonActions, updatedActions, catalogTable.map(_.identifier))\n    }\n    if (attemptVersion == 0L) {\n      val expectedPathForCommitZero = unsafeDeltaFile(deltaLog.logPath, version = 0L).toUri\n      val actualCommitPath = commitResponse.getCommit.getFileStatus.getPath.toUri\n      if (actualCommitPath != expectedPathForCommitZero) {\n        throw new IllegalStateException(\"Expected 0th commit to be written to \" +\n          s\"$expectedPathForCommitZero but was written to $actualCommitPath\")\n      }\n    }\n    commitResponse.getCommit\n  }\n\n\n  /**\n   * Given an attemptVersion, obtain checksum for previous snapshot version\n   * (i.e., attemptVersion - 1) and incrementally derives a new checksum from\n   * the actions of the current transaction.\n   *\n   * @param attemptVersion that the current transaction is committing\n   * @param currentTransactionInfo containing actions of the current transaction\n   * @return\n   */\n  protected def incrementallyDeriveChecksum(\n      attemptVersion: Long,\n      currentTransactionInfo: CurrentTransactionInfo): Option[VersionChecksum] = {\n    incrementallyDeriveChecksum(\n      spark,\n      deltaLog,\n      attemptVersion,\n      actions = currentTransactionInfo.finalActionsToCommit,\n      metadataOpt = Some(currentTransactionInfo.metadata),\n      protocolOpt = Some(currentTransactionInfo.protocol),\n      operationName = currentTransactionInfo.op.name,\n      txnIdOpt = Some(currentTransactionInfo.txnId),\n      previousVersionState = scala.Left(snapshot),\n      includeAddFilesInCrc = Snapshot.shouldIncludeAddFilesInCrc(spark, snapshot, metadata)\n    ).toOption\n  }\n\n  /**\n   * Looks at actions that have happened since the txn started and checks for logical\n   * conflicts with the read/writes. Resolve conflicts and returns a tuple representing\n   * the commit version to attempt next and the commit summary which we need to commit.\n   */\n  protected def checkForConflicts(\n      checkVersion: Long,\n      currentTransactionInfo: CurrentTransactionInfo,\n      attemptNumber: Int,\n      commitIsolationLevel: IsolationLevel)\n    : (Long, CurrentTransactionInfo) = recordDeltaOperation(\n        deltaLog,\n        \"delta.commit.retry.conflictCheck\",\n        tags = Map(TAG_LOG_STORE_CLASS -> deltaLog.store.getClass.getName)) {\n\n    DeltaTableV2.withEnrichedUnsupportedTableException(catalogTable) {\n      val fileStatuses = getConflictingVersions(checkVersion)\n      val nextAttemptVersion = checkVersion + fileStatuses.size\n\n      // validate that information about conflicting winning commit files is continuous and in the\n      // right order.\n      val expected = (checkVersion until nextAttemptVersion)\n      val found = fileStatuses.map(deltaVersion)\n      val mismatch = expected.zip(found).dropWhile{ case (v1, v2) => v1 == v2 }.take(10)\n      assert(mismatch.isEmpty,\n        s\"Expected ${mismatch.map(_._1).mkString(\",\")} but got ${mismatch.map(_._2).mkString(\",\")}\")\n\n      val logPrefix = log\"[attempt ${MDC(DeltaLogKeys.NUM_ATTEMPT, attemptNumber)}] \"\n      val txnDetailsLog = {\n        var adds = 0L\n        var removes = 0L\n        currentTransactionInfo.actions.foreach {\n          case _: AddFile => adds += 1\n          case _: RemoveFile => removes += 1\n          case _ =>\n        }\n        log\"${MDC(DeltaLogKeys.NUM_ACTIONS, adds)} adds, \" +\n        log\"${MDC(DeltaLogKeys.NUM_ACTIONS2, removes)} removes, \" +\n        log\"${MDC(DeltaLogKeys.NUM_PREDICATES, readPredicates.size)} read predicates, \" +\n        log\"${MDC(DeltaLogKeys.NUM_FILES, readFiles.size.toLong)} read files\"\n      }\n\n      logInfo(logPrefix +\n        log\"Checking for conflicts with versions \" +\n        log\"[${MDC(DeltaLogKeys.VERSION, checkVersion)}, \" +\n        log\"${MDC(DeltaLogKeys.VERSION2, nextAttemptVersion)}) \" +\n        log\"with current txn having \" + txnDetailsLog)\n\n      val updatedCurrentTransactionInfo = {\n        if (expected.isEmpty) {\n          currentTransactionInfo\n        }\n        else {\n          resolveConflicts(\n            currentTransactionInfo = currentTransactionInfo,\n            firstWinningVersion = expected.head,\n            lastWinningVersion = expected.last,\n            conflictingCommitFiles = fileStatuses,\n            commitIsolationLevel = commitIsolationLevel)\n        }\n      }\n\n\n      logInfo(logPrefix +\n        log\"No conflicts with versions \" +\n        log\"[${MDC(DeltaLogKeys.VERSION, checkVersion)}, \" +\n        log\"${MDC(DeltaLogKeys.VERSION2, nextAttemptVersion)}) \" +\n        log\"with current txn having \" + txnDetailsLog +\n        log\"${MDC(DeltaLogKeys.TIME_MS, clock.getTimeMillis() - commitAttemptStartTimeMillis)} \" +\n        log\"ms since start\")\n      (nextAttemptVersion, updatedCurrentTransactionInfo)\n    }\n  }\n\n  /**\n   * Loads the summaries of the conflicting commits and uses [[ConflictChecker]] to\n   * resolve conflicts.\n   *\n   * @param currentTransactionInfo The current transaction information to check for conflicts\n   * @param firstWinningVersion The first version number for conflict checking (inclusive)\n   * @param lastWinningVersion The last version number for conflict checking (inclusive)\n   * @param conflictingCommitFiles The sequence of file statuses representing conflicting commits\n   * @param commitIsolationLevel The isolation level to use for conflict checking\n   * @return Updated transaction information after resolving all conflicts\n   */\n  protected def resolveConflicts(\n      currentTransactionInfo: CurrentTransactionInfo,\n      firstWinningVersion: Long,\n      lastWinningVersion: Long,\n      conflictingCommitFiles: Seq[FileStatus],\n      commitIsolationLevel: IsolationLevel) : CurrentTransactionInfo = {\n\n    var updatedCurrentTransactionInfo = currentTransactionInfo\n    (firstWinningVersion to lastWinningVersion)\n      .zip(conflictingCommitFiles)\n      .foreach { case (otherCommitVersion, otherCommitFileStatus) =>\n        val winningCommitSummary = WinningCommitSummary.createFromFileStatus(\n          deltaLog, otherCommitFileStatus)\n\n        val conflictChecker = new ConflictChecker(\n          spark,\n          updatedCurrentTransactionInfo,\n          winningCommitSummary,\n          commitIsolationLevel)\n\n        updatedCurrentTransactionInfo = conflictChecker.checkConflicts()\n\n        logInfo(logPrefix +\n          log\"No conflicts in version ${MDC(DeltaLogKeys.VERSION, otherCommitVersion)}, \" +\n          log\"${MDC(DeltaLogKeys.DURATION,\n            clock.getTimeMillis() - commitAttemptStartTimeMillis)} ms since start\")\n      }\n    updatedCurrentTransactionInfo\n  }\n\n  /** Returns the version that the first attempt will try to commit at. */\n  private[delta] def getFirstAttemptVersion: Long = readVersion + 1L\n\n  /** Returns the conflicting commit information */\n  protected def getConflictingVersions(previousAttemptVersion: Long): Seq[FileStatus] = {\n    assert(previousAttemptVersion == preCommitLogSegment.version + 1)\n    val (newPreCommitLogSegment, newCommitFileStatuses) = deltaLog.getUpdatedLogSegment(\n      preCommitLogSegment,\n      readSnapshotTableCommitCoordinatorClientOpt,\n      catalogTable)\n    assert(preCommitLogSegment.version + newCommitFileStatuses.size ==\n      newPreCommitLogSegment.version)\n    preCommitLogSegment = newPreCommitLogSegment\n    newCommitFileStatuses\n  }\n\n  protected def setCommitted(\n      committedVersion: Long,\n      postCommitSnapshot: Snapshot,\n      committedActions: Seq[Action]): Unit =\n    committed = Some(CommittedTransaction(\n      txnId = txnId,\n      deltaLog = deltaLog,\n      catalogTable = catalogTable,\n      readSnapshot = snapshot,\n      committedVersion = committedVersion,\n      committedActions = committedActions,\n      postCommitSnapshot = postCommitSnapshot,\n      postCommitHooks = postCommitHooks.toSeq,\n      txnExecutionTimeMs = txnExecutionTimeMs.get,\n      needsCheckpoint = needsCheckpoint,\n      partitionsAddedToOpt = partitionsAddedToOpt,\n      isBlindAppend = isBlindAppend\n    ))\n\n  /** Register a hook that will be executed once a commit is successful. */\n  def registerPostCommitHook(hook: PostCommitHook): Unit = {\n    if (!postCommitHooks.contains(hook)) {\n      postCommitHooks.append(hook)\n    }\n  }\n\n  def containsPostCommitHook(hook: PostCommitHook): Boolean = postCommitHooks.contains(hook)\n\n  /** Executes the registered post commit hooks. */\n  protected def runPostCommitHooks(committedTransaction: CommittedTransaction): Unit = {\n    assert(committed.isDefined, \"Can't call post commit hooks before committing\")\n    val postCommitHooksToRun = committedTransaction.postCommitHooks\n\n    // Keep track of the active txn because hooks may create more txns and overwrite the active one.\n    val activeCommit = OptimisticTransaction.getActive()\n    OptimisticTransaction.clearActive()\n\n    try {\n      postCommitHooksToRun.foreach(runPostCommitHook(_, committedTransaction))\n    } finally {\n      activeCommit.foreach(OptimisticTransaction.setActive)\n    }\n  }\n\n  private[delta] def unregisterPostCommitHooksWhere(predicate: PostCommitHook => Boolean): Unit =\n    postCommitHooks --= postCommitHooks.filter(predicate)\n\n  protected lazy val logPrefix: MessageWithContext = {\n    def truncate(uuid: String): String = uuid.split(\"-\").head\n    log\"[tableId=${MDC(DeltaLogKeys.METADATA_ID, truncate(snapshot.metadata.id))},\" +\n    log\"txnId=${MDC(DeltaLogKeys.TXN_ID, truncate(txnId))}] \"\n  }\n\n  def logInfo(msg: MessageWithContext): Unit = {\n    super.logInfo(logPrefix + msg)\n  }\n\n  def logWarning(msg: MessageWithContext): Unit = {\n    super.logWarning(logPrefix + msg)\n  }\n\n  def logWarning(msg: MessageWithContext, throwable: Throwable): Unit = {\n    super.logWarning(logPrefix + msg, throwable)\n  }\n\n  def logError(msg: MessageWithContext): Unit = {\n    super.logError(logPrefix + msg)\n  }\n\n  def logError(msg: MessageWithContext, throwable: Throwable): Unit = {\n    super.logError(logPrefix + msg, throwable)\n  }\n\n  /**\n   * If the operation assigns or modifies column default values, this method checks that the\n   * corresponding table feature is enabled and throws an error if not.\n   */\n  protected def checkNoColumnDefaults(op: DeltaOperations.Operation): Unit = {\n    def usesDefaults(column: StructField): Boolean = {\n      column.metadata.contains(ResolveDefaultColumns.CURRENT_DEFAULT_COLUMN_METADATA_KEY) ||\n        column.metadata.contains(ResolveDefaultColumns.EXISTS_DEFAULT_COLUMN_METADATA_KEY)\n    }\n\n    def throwError(errorClass: String, parameters: Array[String]): Unit = {\n      throw new DeltaAnalysisException(\n        errorClass = errorClass,\n        messageParameters = parameters)\n    }\n\n    op match {\n      case change: ChangeColumn if usesDefaults(change.newColumn) =>\n        throwError(\"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          Array(\"ALTER TABLE\"))\n      case changes: ChangeColumns if changes.columns.exists(c => usesDefaults(c.newColumn)) =>\n        throwError(\"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          Array(\"ALTER TABLE\"))\n      case create: CreateTable if create.metadata.schema.fields.exists(usesDefaults) =>\n        throwError(\"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          Array(\"CREATE TABLE\"))\n      case replace: ReplaceColumns if replace.columns.exists(usesDefaults) =>\n        throwError(\"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          Array(\"CREATE TABLE\"))\n      case replace: ReplaceTable if replace.metadata.schema.fields.exists(usesDefaults) =>\n        throwError(\"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          Array(\"CREATE TABLE\"))\n      case update: UpdateSchema if update.newSchema.fields.exists(usesDefaults) =>\n        throwError(\"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          Array(\"ALTER TABLE\"))\n      case _ =>\n    }\n  }\n\n  // Backfill any unbackfilled commits if coordinated commits are disabled -- in the Optimistic\n  // Transaction constructor.\n  CoordinatedCommitsUtils.backfillWhenCoordinatedCommitsDisabled(snapshot)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PostHocResolveUpCast.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.{Rule, RuleExecutor}\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Post-hoc resolution rules [[PreprocessTableMerge]] and [[PreprocessTableUpdate]] may introduce\n * new unresolved UpCast expressions that won't be resolved by [[ResolveUpCast]] that ran in the\n * previous resolution phase. This rule ensures these UpCast expressions get resolved in the\n * Post-hoc resolution phase.\n *\n * Note: we can't inject [[ResolveUpCast]] directly because we need an initialized analyzer instance\n * for that which is not available at the time Delta rules are injected. [[PostHocResolveUpCast]] is\n * delaying the access to the analyzer until after it's initialized.\n */\ncase class PostHocResolveUpCast(spark: SparkSession)\n  extends Rule[LogicalPlan] {\n\n  override def apply(plan: LogicalPlan): LogicalPlan =\n    if (!plan.resolved) PostHocUpCastResolver.execute(plan) else plan\n\n  /**\n   * A rule executor that runs [[ResolveUpCast]] until all UpCast expressions have been resolved.\n   */\n  object PostHocUpCastResolver extends RuleExecutor[LogicalPlan] {\n    final override protected def batches: Seq[Batch] = Seq(\n      Batch(\n        \"Post-hoc UpCast Resolution\",\n        FixedPoint(\n          conf.analyzerMaxIterations,\n          errorOnExceed = true,\n          maxIterationsSetting = SQLConf.ANALYZER_MAX_ITERATIONS.key),\n        spark.sessionState.analyzer.ResolveUpCast)\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreDowngradeTableFeatureCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.TimeUnit\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.actions.{DeletionVectorDescriptor, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.{AlterTableSetPropertiesDeltaCommand, AlterTableUnsetPropertiesDeltaCommand, DeltaReorgTableCommand, DeltaReorgTableMode, DeltaReorgTableSpec}\nimport org.apache.spark.sql.delta.commands.backfill.RowTrackingUnBackfillCommand\nimport org.apache.spark.sql.delta.commands.columnmapping.RemoveColumnMappingCommand\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.constraints.Constraints\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsUtils\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.ResolvedTable\nimport org.apache.spark.sql.functions.{approx_count_distinct, col, not}\n\n/**\n * Used as the return type of `removeFeatureTracesIfNeeded`. The contents are the following:\n *\n * 1) performedChanges. True when the preDowngrade command performed a cleaning action.\n *    False otherwise.\n * 2) lastCommitVersionOpt. Optionally, it returns the version of the last commit. This is used as\n *    a starting version for the protocol downgrade commit. Defining the last commit allows\n *    to conflict resolve all the commits that occurred between the last pre-downgrade commit\n *    and the protocol downgrade commit.\n */\nsealed case class PreDowngradeStatus(\n    performedChanges: Boolean,\n    lastCommitVersionOpt: Option[Long] = None)\n\nobject PreDowngradeStatus {\n  val DID_NOT_PERFORM_CHANGES = PreDowngradeStatus(performedChanges = false)\n  val PERFORMED_CHANGES = PreDowngradeStatus(performedChanges = true)\n}\n\n/**\n * A base class for implementing a preparation command for removing table features.\n * Must implement a run method. Note, the run method must be implemented in a way that when\n * it finishes, the table does not use the feature that is being removed, and nobody is\n * allowed to start using it again implicitly. One way to achieve this is by\n * disabling the feature on the table before proceeding to the actual removal.\n * See [[RemovableFeature.preDowngradeCommand]].\n */\nsealed abstract class PreDowngradeTableFeatureCommand {\n  def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus\n}\n\ncase class TestWriterFeaturePreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n  // To remove the feature we only need to remove the table property.\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    // Make sure feature data/metadata exist before proceeding.\n    if (TestRemovableWriterFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    if (DeltaUtils.isTesting) {\n      recordDeltaEvent(table.deltaLog, \"delta.test.TestWriterFeaturePreDowngradeCommand\")\n    }\n\n    val properties = Seq(TestRemovableWriterFeature.TABLE_PROP_KEY)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class TestUnsupportedReaderWriterFeaturePreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand {\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus =\n    PreDowngradeStatus.PERFORMED_CHANGES\n}\n\ncase class TestUnsupportedWriterFeaturePreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand {\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus =\n    PreDowngradeStatus.PERFORMED_CHANGES\n}\n\ncase class TestWriterWithHistoryValidationFeaturePreDowngradeCommand(table: DeltaTableV2)\n    extends PreDowngradeTableFeatureCommand\n    with DeltaLogging {\n  // To remove the feature we only need to remove the table property.\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    // Make sure feature data/metadata exist before proceeding.\n    val snapshot = table.initialSnapshot\n    if (TestRemovableWriterWithHistoryTruncationFeature.validateDropInvariants(table, snapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val properties = Seq(TestRemovableWriterWithHistoryTruncationFeature.TABLE_PROP_KEY)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class TestReaderWriterFeaturePreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n  // To remove the feature we only need to remove the table property.\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    // Make sure feature data/metadata exist before proceeding.\n    if (TestRemovableReaderWriterFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    if (DeltaUtils.isTesting) {\n      recordDeltaEvent(table.deltaLog, \"delta.test.TestReaderWriterFeaturePreDowngradeCommand\")\n    }\n\n    val properties = Seq(TestRemovableReaderWriterFeature.TABLE_PROP_KEY)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class TestLegacyWriterFeaturePreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand {\n  /** Return true if we removed the property, false if no action was needed. */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    if (TestRemovableLegacyWriterFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val properties = Seq(TestRemovableLegacyWriterFeature.TABLE_PROP_KEY)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class TestLegacyReaderWriterFeaturePreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand {\n  /** Return true if we removed the property, false if no action was needed. */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    val snapshot = table.initialSnapshot\n    if (TestRemovableLegacyReaderWriterFeature.validateDropInvariants(table, snapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val properties = Seq(TestRemovableLegacyReaderWriterFeature.TABLE_PROP_KEY)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\nprivate[delta] class DeletionVectorsRemovalMetrics(\n    val numDeletionVectorsToRemove: Long,\n    val numDeletionVectorRowsToRemove: Long,\n    var dvTombstonesWithinRetentionPeriod: Long = 0L,\n    var addDVTombstonesTime: Long = 0L,\n    var downgradeTimeMs: Long = 0L)\n\ncase class DeletionVectorsPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n\n  /**\n   * Create RemoveFiles (tombstones) that directly reference deletion vector within the retention\n   * period. These protect the latter from accidental removal from clients that do not support\n   * deletion vectors.\n   *\n   * Note, we always create the DV tombstones even for the drop feature with history\n   * truncation implementation. This is to protect against a corner case where the user run\n   * drop feature with fastDropFeature.enabled = false and then run again with\n   * fastDropFeature.enabled = true.\n   *\n   * @param checkIfSnapshotUpdatedSinceTs The timestamp to use for updating the snapshot.\n   * @param metrics The deletion vectors removal metrics. This function only updates the DV\n   *                tombstone related metrics.\n   */\n  private def generateDVTombstones(\n      spark: SparkSession,\n      checkIfSnapshotUpdatedSinceTs: Long,\n      metrics: DeletionVectorsRemovalMetrics): Unit = {\n    import scala.jdk.CollectionConverters._\n    import org.apache.spark.sql.delta.implicits._\n\n    if (!spark.conf.get(DeltaSQLConf.FAST_DROP_FEATURE_GENERATE_DV_TOMBSTONES)) return\n\n    val startTimeNs = System.nanoTime()\n    val snapshotToUse = table.update(checkIfUpdatedSinceTs = Some(checkIfSnapshotUpdatedSinceTs))\n\n    val deletionVectorPath =\n        DeletionVectorDescriptor.urlEncodedRelativePathIfExists(\n          deletionVectorCol = col(\"deletionVector\"),\n          tablePath = table.deltaLog.dataPath)\n\n    val isInlineDeletionVector = DeletionVectorDescriptor.isInline(col(\"deletionVector\"))\n\n    // SnapshotToUse.tombstones returns only the tombstones within the retention period. The\n    // default tombstone retention period is 7 days. Note, that if a RemoveFile contains\n    // DeletionVectorDescriptor, it is guaranteed it is not a DV Tombstone. Furthermore, we\n    // use distinct to deduplicate the DV references. This is because we merge DVs, and as a\n    // result, several AddFiles may point to the same DV file.\n    val removeFilesWithDVs = snapshotToUse.tombstones\n      .filter(col(\"deletionVector\").isNotNull)\n      .filter(not(isInlineDeletionVector))\n      .select(deletionVectorPath.as(\"path\"))\n      .filter(col(\"path\").isNotNull)\n      .distinct()\n\n    // This is a union of the DV tombstones and the regular data file tombstones without DVs (we\n    // cannot tell the difference). We use it to identify which DV tombstones are already created.\n    val filesWithoutDVs = snapshotToUse.tombstones\n      .filter(col(\"deletionVector\").isNull)\n      .select(\"path\")\n\n    val dvTombstonePathsToAdd = removeFilesWithDVs\n      .join(filesWithoutDVs, \"path\", \"left_anti\")\n      .as[String]\n\n    val actionsToCommit = dvTombstonePathsToAdd.toLocalIterator().asScala.map { dvPath =>\n      // Disable scala style rules to ignore warning that RemoveFile files should never be\n      // instantiated directly.\n      // scalastyle:off\n      RemoveFile(\n        path = dvPath,\n        deletionTimestamp = Some(table.deltaLog.clock.getTimeMillis()),\n        dataChange = false)\n      // scalastyle:on\n    }\n\n    // We pay some overhead here to estimate the memory required to hold the results.\n    // Above some threshold we use commitLarge. This allows to use an iterator instead of\n    // materializing results in memory. However, it comes with some disadvantages: if there is a\n    // conflict the commit is not retried.\n    // A cheaper alternative would be to use snapshot.numDeletionVectorsOpt\n    // (right before the reorg in drop feature) but this does not capture deduplication as well as\n    // any reorgs that occurred before dropping DVs.\n    // We assume 1024 bytes are required per RemoveFile.\n    val tombstonesToAddCount =\n      dvTombstonePathsToAdd.select(approx_count_distinct(\"path\")).as[Long].first\n\n    val tombstoneCountThreshold =\n      spark.conf.get(DeltaSQLConf.FAST_DROP_FEATURE_DV_TOMBSTONE_COUNT_THRESHOLD)\n\n    if (tombstonesToAddCount > tombstoneCountThreshold) {\n      table.startTransaction(Some(snapshotToUse)).commitLarge(\n        spark,\n        nonProtocolMetadataActions = actionsToCommit,\n        op = DeltaOperations.AddDeletionVectorsTombstones,\n        newProtocolOpt = None,\n        context = Map.empty,\n        metrics = Map(\"dvTombstonesWithinRetentionPeriod\" -> tombstonesToAddCount.toString))\n    } else {\n      table.startTransaction(Some(snapshotToUse))\n        .commit(actionsToCommit.toList, DeltaOperations.AddDeletionVectorsTombstones)\n    }\n\n    metrics.addDVTombstonesTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)\n    metrics.dvTombstonesWithinRetentionPeriod = tombstonesToAddCount\n  }\n\n  private def reorgTable(spark: SparkSession) = {\n    // Wrap `table` in a ResolvedTable that can be passed to DeltaReorgTableCommand. The catalog &\n    // table ID won't be used by DeltaReorgTableCommand.\n    import org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\n    val catalog = table.spark.sessionState.catalogManager.currentCatalog.asTableCatalog\n    val tableId = Seq(table.name()).asIdentifier\n\n    DeltaReorgTableCommand(target = ResolvedTable.create(catalog, tableId, table))(Nil)\n      .run(table.spark)\n  }\n\n  /**\n   * We first remove the table feature property to prevent any transactions from committing\n   * new DVs. This will cause any concurrent transactions tox fail. Then, we run PURGE\n   * to remove existing DVs from the latest snapshot.\n   * Note, during the protocol downgrade phase we validate whether all invariants still hold.\n   * This should detect if any concurrent txns enabled the feature and/or added DVs again.\n   *\n   * @return Returns true if it removed DV metadata property and/or DVs. False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(\n      spark: SparkSession): PreDowngradeStatus = {\n    val startTimeNs = table.deltaLog.clock.nanoTime()\n\n    // Latest snapshot looks clean. No action is required. We may proceed\n    // to the protocol downgrade phase.\n    val snapshot = table.update()\n    val tracesFound = !DeletionVectorsTableFeature.validateDropInvariants(table, snapshot)\n    if (tracesFound) {\n      val properties = Seq(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key)\n      AlterTableUnsetPropertiesDeltaCommand(\n        table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n\n      reorgTable(spark)\n    }\n\n    val metrics = new DeletionVectorsRemovalMetrics(\n      numDeletionVectorsToRemove = snapshot.numDeletionVectorsOpt.getOrElse(0L),\n      numDeletionVectorRowsToRemove = snapshot.numDeletedRecordsOpt.getOrElse(0L))\n\n    reorgTable(spark)\n\n    // Even if there no DV traces in the table we check if there are missing DV tombstones.\n    // This is to protect against an edge case where all DV traces are cleaned before invoking\n    // the drop feature command.\n    generateDVTombstones(spark, startTimeNs, metrics)\n\n    metrics.downgradeTimeMs =\n      TimeUnit.NANOSECONDS.toMillis(table.deltaLog.clock.nanoTime() - startTimeNs)\n\n    recordDeltaEvent(\n      table.deltaLog,\n      opType = \"delta.deletionVectorsFeatureRemovalMetrics\",\n      data = metrics)\n    PreDowngradeStatus(performedChanges = tracesFound)\n  }\n}\n\ncase class V2CheckpointPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n  /**\n   * We set the checkpoint policy to classic to prevent any transactions from creating\n   * v2 checkpoints.\n   *\n   * @return True if it changed checkpoint policy metadata property to classic.\n   *         False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n\n    if (V2CheckpointTableFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val startTimeNs = System.nanoTime()\n    val properties = Map(DeltaConfigs.CHECKPOINT_POLICY.key -> CheckpointPolicy.Classic.name)\n    AlterTableSetPropertiesDeltaCommand(table, properties).run(spark)\n\n    recordDeltaEvent(\n      table.deltaLog,\n      opType = \"delta.v2CheckpointFeatureRemovalMetrics\",\n      data =\n        Map((\"downgradeTimeMs\", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)))\n    )\n\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class InCommitTimestampsPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n  /**\n   * We disable the feature by:\n   * - Removing the table properties:\n   *    1. DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP\n   *    2. DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION\n   * - Setting the table property DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED to false.\n   * Technically, only setting IN_COMMIT_TIMESTAMPS_ENABLED to false is enough to disable the\n   * feature. However, we can use this opportunity to clean up the metadata.\n   *\n   * @return true if any change to the metadata (the three properties listed above) was made.\n   *         False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    val startTimeNs = System.nanoTime()\n    val currentMetadata = table.initialSnapshot.metadata\n    val currentTableProperties = currentMetadata.configuration\n\n    val enablementProperty = DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED\n    val ictEnabledInMetadata = enablementProperty.fromMetaData(currentMetadata)\n    val provenanceProperties = Seq(\n      DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.key,\n      DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key)\n    val propertiesToRemove = provenanceProperties.filter(currentTableProperties.contains)\n\n    val traceRemovalNeeded = propertiesToRemove.nonEmpty || ictEnabledInMetadata\n    if (traceRemovalNeeded) {\n      val propertiesToDisable =\n        Option.when(ictEnabledInMetadata)(enablementProperty.key -> \"false\")\n      val desiredTableProperties = currentTableProperties\n        .filterNot{ case (k, _) => propertiesToRemove.contains(k) } ++ propertiesToDisable\n\n      val deltaOperation = DeltaOperations.UnsetTableProperties(\n        (propertiesToRemove ++ propertiesToDisable.map(_._1)).toSeq, ifExists = true)\n      table.startTransaction().commit(\n        Seq(currentMetadata.copy(configuration = desiredTableProperties.toMap)), deltaOperation)\n    }\n\n    val provenancePropertiesPresenceLogs = provenanceProperties.map { prop =>\n      prop -> currentTableProperties.contains(prop).toString\n    }\n    recordDeltaEvent(\n      table.deltaLog,\n      opType = \"delta.inCommitTimestampFeatureRemovalMetrics\",\n      data = Map(\n          \"downgradeTimeMs\" -> TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs),\n          \"traceRemovalNeeded\" -> traceRemovalNeeded.toString,\n          enablementProperty.key -> ictEnabledInMetadata\n          ) ++ provenancePropertiesPresenceLogs\n\n    )\n    PreDowngradeStatus(performedChanges = traceRemovalNeeded)\n  }\n}\n\ncase class VacuumProtocolCheckPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n\n  /**\n   * Returns true when it performs a cleaning action. When no action was required\n   * it returns false.\n   * For downgrading the [[VacuumProtocolCheckTableFeature]], we don't need remove any traces, we\n   * just need to remove the feature from the [[Protocol]].\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus =\n    PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n}\n\ncase class CoordinatedCommitsPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n\n  /**\n   * We disable the feature by removing the following table properties:\n   *    1. DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key\n   *    2. DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key\n   *    3. DeltaConfigs.COORDINATED_COMMITS_TABLE_CONF.key\n   * If these properties have been removed but unbackfilled commits are still present, we\n   * backfill them.\n   *\n   * @return true if any change to the metadata (the three properties listed above) was made OR\n   *         if there were any unbackfilled commits that were backfilled.\n   *         false otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    val startTimeNs = System.nanoTime()\n\n    var traceRemovalNeeded = false\n    var exceptionOpt = Option.empty[Throwable]\n    val propertyPresenceLogs = CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS.map( key =>\n      key -> table.initialSnapshot.metadata.configuration.contains(key).toString\n    )\n    if (CoordinatedCommitsUtils.tablePropertiesPresent(table.initialSnapshot.metadata)) {\n      traceRemovalNeeded = true\n      try {\n        AlterTableUnsetPropertiesDeltaCommand(\n          table,\n          CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS,\n          ifExists = true,\n          fromDropFeatureCommand = true\n        ).run(spark)\n      } catch {\n        case NonFatal(e) =>\n          exceptionOpt = Some(e)\n      }\n    }\n    var postDisablementUnbackfilledCommitsPresent = false\n    if (exceptionOpt.isEmpty) {\n      val snapshotAfterDisabling = table.update()\n      assert(snapshotAfterDisabling.getTableCommitCoordinatorForWrites.isEmpty)\n      postDisablementUnbackfilledCommitsPresent =\n        CoordinatedCommitsUtils.unbackfilledCommitsPresent(snapshotAfterDisabling)\n      if (postDisablementUnbackfilledCommitsPresent) {\n        traceRemovalNeeded = true\n        // Coordinated commits have already been disabled but there are unbackfilled commits.\n        CoordinatedCommitsUtils.backfillWhenCoordinatedCommitsDisabled(snapshotAfterDisabling)\n      }\n    }\n    recordDeltaEvent(\n      table.deltaLog,\n      opType = \"delta.coordinatedCommitsFeatureRemovalMetrics\",\n      data = Map(\n          \"downgradeTimeMs\" -> TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs),\n          \"traceRemovalNeeded\" -> traceRemovalNeeded.toString,\n          \"traceRemovalSuccess\" -> exceptionOpt.isEmpty.toString,\n          \"traceRemovalException\" -> exceptionOpt.map(_.getMessage).getOrElse(\"\"),\n          \"postDisablementUnbackfilledCommitsPresent\" ->\n            postDisablementUnbackfilledCommitsPresent.toString\n      ) ++ propertyPresenceLogs\n    )\n    exceptionOpt.foreach(throw _)\n    PreDowngradeStatus(performedChanges = traceRemovalNeeded)\n  }\n}\n\ncase class TypeWideningPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n  with DeltaLogging {\n\n  /**\n   * Unset the type widening table property to prevent new type changes to be applied to the table,\n   * then removes traces of the feature:\n   * - Rewrite files that have columns or fields with a different type than in the current table\n   *   schema. These are all files not added or modified after the last type change.\n   * - Remove the type widening metadata attached to fields in the current table schema.\n   *\n   * @return Return true if files were rewritten or metadata was removed. False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    if (TypeWideningTableFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val startTimeNs = System.nanoTime()\n    val properties = Seq(DeltaConfigs.ENABLE_TYPE_WIDENING.key)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = true, fromDropFeatureCommand = true).run(spark)\n    val numFilesRewritten = rewriteFilesIfNeeded(spark)\n    val metadataRemoved = removeMetadataIfNeeded()\n\n    recordDeltaEvent(\n      table.deltaLog,\n      opType = \"delta.typeWidening.featureRemoval\",\n      data = Map(\n        \"downgradeTimeMs\" -> TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs),\n        \"numFilesRewritten\" -> numFilesRewritten,\n        \"metadataRemoved\" -> metadataRemoved\n        )\n    )\n    PreDowngradeStatus(performedChanges = numFilesRewritten > 0 || metadataRemoved)\n  }\n\n  /**\n   * Rewrite files that have columns or fields with a different type than in the current table\n   * schema. These are all files not added or modified after the last type change.\n   * @return Return the number of files rewritten.\n   */\n  private def rewriteFilesIfNeeded(spark: SparkSession): Long = {\n    if (!TypeWideningMetadata.containsTypeWideningMetadata(table.initialSnapshot.schema)) {\n      return 0L\n    }\n\n    // Wrap `table` in a ResolvedTable that can be passed to DeltaReorgTableCommand. The catalog &\n    // table ID won't be used by DeltaReorgTableCommand.\n    import org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\n    val catalog = spark.sessionState.catalogManager.currentCatalog.asTableCatalog\n    val tableId = Seq(table.name()).asIdentifier\n\n    val reorg = DeltaReorgTableCommand(\n      target = ResolvedTable.create(catalog, tableId, table),\n      reorgTableSpec = DeltaReorgTableSpec(DeltaReorgTableMode.REWRITE_TYPE_WIDENING, None)\n    )(Nil)\n\n    val rows = reorg.run(spark)\n    val metrics = rows.head.getAs[OptimizeMetrics](1)\n    metrics.numFilesRemoved\n  }\n\n  /**\n   * Remove the type widening metadata attached to fields in the current table schema.\n   * @return Return true if any metadata was removed. False otherwise.\n   */\n  private def removeMetadataIfNeeded(): Boolean = {\n    if (!TypeWideningMetadata.containsTypeWideningMetadata(table.initialSnapshot.schema)) {\n      return false\n    }\n\n    val txn = table.startTransaction()\n    val metadata = txn.metadata\n    val (cleanedSchema, changes) =\n      TypeWideningMetadata.removeTypeWideningMetadata(metadata.schema)\n    txn.commit(\n      metadata.copy(schemaString = cleanedSchema.json) :: Nil,\n      DeltaOperations.UpdateColumnMetadata(\"DROP FEATURE\", changes))\n    true\n  }\n}\ncase class ColumnMappingPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n    with DeltaLogging {\n\n  /**\n   * We first remove the table feature property to prevent any transactions from writing data\n   * files with the physical names. This will cause any concurrent transactions to fail.\n   * Then, we run RemoveColumnMappingCommand to rewrite the files rename columns.\n   * Note, during the protocol downgrade phase we validate whether all invariants still hold.\n   * This should detect if any concurrent txns enabled the table property again.\n   *\n   * @return Returns true if it removed table property and/or has rewritten the data.\n   *         False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    // Latest snapshot looks clean. No action is required. We may proceed\n    // to the protocol downgrade phase.\n    if (ColumnMappingTableFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    recordDeltaOperation(\n      table.deltaLog,\n      opType = \"delta.columnMappingFeatureRemoval\") {\n      RemoveColumnMappingCommand(table.deltaLog, table.catalogTable)\n        .run(spark, removeColumnMappingTableProperty = true)\n    }\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class CheckConstraintsPreDowngradeTableFeatureCommand(table: DeltaTableV2)\n    extends PreDowngradeTableFeatureCommand {\n\n  /**\n   * Throws an exception if the table has CHECK constraints, and returns false otherwise (as no\n   * action was required).\n   *\n   * We intentionally error out instead of removing the CHECK constraints here, as dropping a\n   * table feature should not never alter the logical representation of a table (only its physical\n   * representation). Instead, we ask the user to explicitly drop the constraints before the table\n   * feature can be dropped.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    val checkConstraintNames = Constraints.getCheckConstraintNames(table.initialSnapshot.metadata)\n    if (checkConstraintNames.isEmpty) return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    throw DeltaErrors.cannotDropCheckConstraintFeature(checkConstraintNames)\n  }\n}\n\ncase class CheckpointProtectionPreDowngradeCommand(table: DeltaTableV2)\n    extends PreDowngradeTableFeatureCommand {\n  import org.apache.spark.sql.delta.actions.DropTableFeatureUtils._\n  import org.apache.spark.sql.delta.CheckpointProtectionTableFeature._\n\n  /**\n   * To remove the feature we need to truncate all history prior to the atomic cleanup version.\n   * For this cleanup operation we use a shorter log retention period of 24 hours as defined in\n   * (delta.dropFeatureTruncateHistory.retentionDuration). The history truncation here needs to\n   * adhere to all the invariants established by the CheckpointProtectionTableFeature, similarly\n   * to any other metadata cleanup invocations (see doc in CheckpointProtectionTableFeature and\n   * REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION).\n   *\n   * The pre-downgrade process here mimics the downgrade process of the legacy drop feature\n   * implementation for features with requiresHistoryProtection=true.\n   *\n   * Note, this feature can only be dropped with the TRUNCATE HISTORY option. Therefore, the\n   * removal of CheckpointProtection does not require the addition of CheckpointProtection to\n   * protect history.\n   *\n   * Always returns false since we do not perform any modifications that require history\n   * expiration. This allows the drop process to proceed immediately after we cleanup the history\n   * prior to requireCheckpointProtectionBeforeVersion.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    val snapshot = table.initialSnapshot\n\n    if (!historyPriorToCheckpointProtectionVersionIsTruncated(snapshot, table.catalogTable)) {\n      // Add a checkpoint here to make sure we can cleanup up everything before this commit.\n      // This is because metadata cleanup operations, can only clean up to the latest checkpoint.\n      createEmptyCommitAndCheckpoint(table, table.deltaLog.clock.nanoTime())\n\n      table.deltaLog.cleanUpExpiredLogs(\n        snapshot,\n        table.catalogTable,\n        deltaRetentionMillisOpt = Some(truncateHistoryLogRetentionMillis(snapshot.metadata)),\n        cutoffTruncationGranularity = TruncationGranularity.MINUTE)\n\n      if (!historyPriorToCheckpointProtectionVersionIsTruncated(snapshot, table.catalogTable)) {\n        throw DeltaErrors.dropCheckpointProtectionWaitForRetentionPeriod(\n          table.initialSnapshot.metadata)\n      }\n    }\n\n    // If history is truncated we do not need the property anymore.\n    val property = DeltaConfigs.REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION.key\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, Seq(property), ifExists = true, fromDropFeatureCommand = true).run(spark)\n\n    // We did not do any changes that require history expiration. It is ok if the removed property\n    // exists in history.\n    PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n  }\n}\n\ncase class RedirectWriterOnlyPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n    with DeltaLogging {\n  /**\n   * We disable the feature by removing [[DeltaConfigs.REDIRECT_WRITER_ONLY]].\n   *\n   * @return True if the property is removed. False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    // Make sure feature data/metadata exist before proceeding.\n    if (RedirectWriterOnlyFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val properties = Seq(DeltaConfigs.REDIRECT_WRITER_ONLY.key)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = false, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class RedirectReaderWriterPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand\n    with DeltaLogging {\n  /**\n   * We disable the feature by removing [[DeltaConfigs.REDIRECT_READER_WRITER]].\n   *\n   * @return True if the property is removed. False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    // Make sure feature data/metadata exist before proceeding.\n    if (RedirectReaderWriterFeature.validateDropInvariants(table, table.initialSnapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val properties = Seq(DeltaConfigs.REDIRECT_READER_WRITER.key)\n    AlterTableUnsetPropertiesDeltaCommand(\n      table, properties, ifExists = false, fromDropFeatureCommand = true).run(spark)\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\ncase class RowTrackingPreDowngradeCommand(table: DeltaTableV2)\n    extends PreDowngradeTableFeatureCommand\n    with DeltaLogging {\n\n  /**\n   * Disabling the feature involves the following steps:\n   *\n   *  1) Set `delta.enableRowTracking` to false so clients do not expect anymore all files\n   *     to have row IDs.\n   *  2) Set `delta.rowTrackingSuspended` to true to suspend row ID generation.\n   *  3) Unbackfill all existing row IDs.\n   *\n   *  Note, the remaining relevant properties/metadataDomains are removed at the downgrade protocol\n   *  commit.\n   *\n   * @return True if the feature traces are removed. False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    if (RowTrackingFeature.validateDropInvariants(table, table.update())) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val propertiesToSet = Map(\n      DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"false\",\n      DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"true\")\n    AlterTableSetPropertiesDeltaCommand(table, propertiesToSet).run(spark)\n\n    val commitSeq = RowTrackingUnBackfillCommand(\n      table.deltaLog,\n      nameOfTriggeringOperation = DeltaOperations.OP_DROP_FEATURE,\n      table.catalogTable).run(spark)\n\n    PreDowngradeStatus(\n      performedChanges = true,\n      commitSeq.lastOption.map(_.getLong(0)))\n  }\n}\n\ncase class DomainMetadataPreDowngradeCommand(table: DeltaTableV2)\n    extends PreDowngradeTableFeatureCommand {\n\n  /**\n   * Removes domain metadata from the table. In general, each feature should be responsible\n   * for removing its own domain metadata when dropped. The cleanup process here is to make sure\n   * the domainMetadata feature can be dropped in cases where domain metadata is leaked.\n   *\n   * Note, the domainMetadata feature can only be dropped when no dependent features are present\n   * in the table. This ensures that any domain metadata found on the table are leaked metadata.\n   *\n   * @return True if the feature traces are removed. False otherwise.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    val snapshot = table.update()\n    if (DomainMetadataTableFeature.validateDropInvariants(table, snapshot)) {\n      return PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n    }\n\n    val actionsToCommit = snapshot\n      .domainMetadata\n      .map(_.copy(removed = true))\n\n    table\n      .startTransaction()\n      .commit(actionsToCommit, DeltaOperations.DomainMetadataCleanup(actionsToCommit.length))\n    PreDowngradeStatus.PERFORMED_CHANGES\n  }\n}\n\n/**\n * PreDowngrade command for MaterializePartitionColumns feature.\n * This feature doesn't require any special cleanup actions when being dropped.\n */\ncase class MaterializePartitionColumnsPreDowngradeCommand(table: DeltaTableV2)\n  extends PreDowngradeTableFeatureCommand {\n\n  /**\n   * No cleanup actions are needed. The table property is automatically removed by the DROP FEATURE\n   * via tablePropertiesToRemoveAtDowngradeCommit.\n   */\n  override def removeFeatureTracesIfNeeded(spark: SparkSession): PreDowngradeStatus = {\n    PreDowngradeStatus.DID_NOT_PERFORM_CHANGES\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreprocessTableDelete.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.commands.DeleteCommand\n\nimport org.apache.spark.sql.catalyst.expressions.SubqueryExpression\nimport org.apache.spark.sql.catalyst.plans.logical.{DeltaDelete, LogicalPlan}\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Preprocess the [[DeltaDelete]] plan to convert to [[DeleteCommand]].\n */\ncase class PreprocessTableDelete(sqlConf: SQLConf) extends Rule[LogicalPlan] {\n\n  override def apply(plan: LogicalPlan): LogicalPlan = {\n    plan.resolveOperators {\n      case d: DeltaDelete if d.resolved =>\n        d.condition.foreach { cond =>\n          if (SubqueryExpression.hasSubquery(cond)) {\n            throw DeltaErrors.subqueryNotSupportedException(\"DELETE\", cond)\n          }\n        }\n        DeleteCommand(d)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreprocessTableMerge.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.time.{Instant, LocalDateTime}\nimport java.util.Locale\n\nimport scala.collection.mutable\nimport scala.reflect.ClassTag\n\nimport org.apache.spark.sql.delta.commands.MergeIntoCommand\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.EliminateSubqueryAliases\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression\nimport org.apache.spark.sql.catalyst.optimizer.ComputeCurrentTime\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.catalyst.trees.TreePattern.CURRENT_LIKE\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils.{instantToMicros, localDateTimeToMicros}\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{DataType, DateType, StringType, StructField, StructType, TimestampNTZType, TimestampType}\n\ncase class PreprocessTableMerge(override val conf: SQLConf)\n  extends Rule[LogicalPlan] with UpdateExpressionsSupport {\n\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {\n    case m: DeltaMergeInto if m.resolved => apply(m, true)\n  }\n\n  def apply(mergeInto: DeltaMergeInto, transformToCommand: Boolean): LogicalPlan = {\n    val DeltaMergeInto(\n      target,\n      source,\n      condition,\n      matched,\n      notMatched,\n      notMatchedBySource,\n      withSchemaEvolution,\n      finalSchemaOpt) = mergeInto\n\n    if (finalSchemaOpt.isEmpty) {\n      throw DeltaErrors.targetTableFinalSchemaEmptyException()\n    }\n\n    val postEvolutionTargetSchema = finalSchemaOpt.get\n\n    def checkCondition(cond: Expression, conditionName: String): Unit = {\n      if (!cond.deterministic) {\n        throw DeltaErrors.nonDeterministicNotSupportedException(\n          s\"$conditionName condition of MERGE operation\", cond)\n      }\n      if (cond.find(_.isInstanceOf[AggregateExpression]).isDefined) {\n        throw DeltaErrors.aggsNotSupportedException(\n          s\"$conditionName condition of MERGE operation\", cond)\n      }\n      if (SubqueryExpression.hasSubquery(cond)) {\n        throw DeltaErrors.subqueryNotSupportedException(\n          s\"$conditionName condition of MERGE operation\", cond)\n      }\n    }\n\n    checkCondition(condition, \"search\")\n    (matched ++ notMatched ++ notMatchedBySource).filter(_.condition.nonEmpty).foreach { clause =>\n      checkCondition(clause.condition.get, clause.clauseType.toUpperCase(Locale.ROOT))\n    }\n\n    val deltaLogicalPlan = EliminateSubqueryAliases(target)\n    val tahoeFileIndex = deltaLogicalPlan match {\n      case DeltaFullTable(_, index) => index\n      case o => throw DeltaErrors.notADeltaSourceException(\"MERGE\", Some(o))\n    }\n    val generatedColumns = GeneratedColumn.getGeneratedColumns(\n      tahoeFileIndex.snapshotAtAnalysis)\n    if (generatedColumns.nonEmpty &&\n        !GeneratedColumn.allowDMLTargetPlan(deltaLogicalPlan, conf)) {\n      throw DeltaErrors.operationOnTempViewWithGenerateColsNotSupported(\"MERGE INTO\")\n    }\n\n    val identityColumns = IdentityColumn.getIdentityColumns(\n      tahoeFileIndex.snapshotAtAnalysis.metadata.schema)\n    // A mapping from the identity column struct field to the GenerateIdentityColumnValues\n    // expression for the target table in the MERGE clause.\n    val identityColumnExpressionMap = mutable.Map[StructField, Expression]()\n    // Column names for which we need to track IDENTITY high water marks.\n    var trackHighWaterMarks = Set[String]()\n\n    val processedMatched = matched.map {\n      case m: DeltaMergeIntoMatchedUpdateClause =>\n        val alignedActions = alignUpdateActions(\n          target,\n          m.resolvedActions,\n          whenClauses = matched ++ notMatched ++ notMatchedBySource,\n          identityColumns = identityColumns,\n          generatedColumns = generatedColumns,\n          allowSchemaEvolution = withSchemaEvolution,\n          postEvolutionTargetSchema = postEvolutionTargetSchema)\n        m.copy(m.condition, alignedActions)\n      case m: DeltaMergeIntoMatchedDeleteClause => m // Delete does not need reordering\n    }\n    val processedNotMatchedBySource = notMatchedBySource.map {\n      case m: DeltaMergeIntoNotMatchedBySourceUpdateClause =>\n        val alignedActions = alignUpdateActions(\n          target,\n          m.resolvedActions,\n          whenClauses = matched ++ notMatched ++ notMatchedBySource,\n          identityColumns = identityColumns,\n          generatedColumns = generatedColumns,\n          allowSchemaEvolution = withSchemaEvolution,\n          postEvolutionTargetSchema = postEvolutionTargetSchema)\n        m.copy(m.condition, alignedActions)\n      case m: DeltaMergeIntoNotMatchedBySourceDeleteClause => m // Delete does not need reordering\n    }\n\n    val processedNotMatched = notMatched.map { case m: DeltaMergeIntoNotMatchedInsertClause =>\n      // Check if columns are distinct. All actions should have targetColNameParts.size = 1.\n      m.resolvedActions.foreach { a =>\n        if (a.targetColNameParts.size > 1) {\n          throw DeltaErrors.nestedFieldNotSupported(\n            \"INSERT clause of MERGE operation\",\n            a.targetColNameParts.mkString(\"`\", \"`.`\", \"`\")\n          )\n        }\n      }\n\n      IdentityColumn.blockExplicitIdentityColumnInsert(\n        identityColumns,\n        m.resolvedActions.map(_.targetColNameParts))\n\n      val targetColNames = m.resolvedActions.map(_.targetColNameParts.head)\n      if (targetColNames.distinct.size < targetColNames.size) {\n        throw DeltaErrors.duplicateColumnOnInsert()\n      }\n\n      // Generate actions for columns that are not explicitly inserted. They might come from\n      // the original schema of target table or the schema evolved columns. In either case they are\n      // covered by `finalSchema`.\n      val implicitActions = postEvolutionTargetSchema.filterNot { col =>\n        m.resolvedActions.exists { insertAct =>\n          conf.resolver(insertAct.targetColNameParts.head, col.name)\n        }\n      }.map { col =>\n        import org.apache.spark.sql.catalyst.util.ResolveDefaultColumns.getDefaultValueExprOrNullLit\n        val defaultValue: Expression =\n          getDefaultValueExprOrNullLit(col, conf.useNullsForMissingDefaultColumnValues)\n            .getOrElse(Literal(null, col.dataType))\n        DeltaMergeAction(\n          targetColNameParts = Seq(col.name),\n          expr = defaultValue,\n          // INSERT * operations set target-only struct fields to null, since there is no existing\n          // target row to preserve values from.\n          targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.NULLIFY,\n          targetColNameResolved = true)\n      }\n\n      val actions = m.resolvedActions ++ implicitActions\n      val (actionsWithGeneratedColumns, trackFromInsert) = resolveImplicitColumns(\n        m.resolvedActions,\n        actions,\n        source,\n        generatedColumns.map(f => (f, true)) ++ identityColumns.map(f => (f, false)),\n        postEvolutionTargetSchema,\n        identityColumnExpressionMap)\n\n      trackHighWaterMarks ++= trackFromInsert\n\n      val alignedActions: Seq[DeltaMergeAction] = postEvolutionTargetSchema.map { targetAttrib =>\n        actionsWithGeneratedColumns.find { a =>\n          conf.resolver(targetAttrib.name, a.targetColNameParts.head)\n        }.map { a =>\n          DeltaMergeAction(\n            targetColNameParts = Seq(targetAttrib.name),\n            expr = castIfNeeded(\n              a.expr,\n              targetAttrib.dataType,\n              castingBehavior = MergeOrUpdateCastingBehavior(withSchemaEvolution),\n              targetAttrib.name),\n            // The action has been aligned/cast to the target schema.\n            targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.TARGET_ALIGNED,\n            targetColNameResolved = true)\n        }.getOrElse {\n          // If a target table column was not found in the INSERT columns and expressions,\n          // then throw exception as there must be an expression to set every target column.\n          throw DeltaErrors.columnOfTargetTableNotFoundInMergeException(\n            targetAttrib.name, targetColNames.mkString(\", \"))\n        }\n      }\n\n      m.copy(m.condition, alignedActions)\n    }\n\n    if (transformToCommand) {\n      val (relation, tahoeFileIndex) = EliminateSubqueryAliases(target) match {\n        case DeltaFullTable(rel, index) => rel -> index\n        case o => throw DeltaErrors.notADeltaSourceException(\"MERGE\", Some(o))\n      }\n\n      /**\n       * Because source and target are not children of MergeIntoCommand they are not processed when\n       * invoking the [[ComputeCurrentTime]] rule. This is why they need special handling.\n       */\n      val now = Instant.now()\n      // Transform timestamps for the MergeIntoCommand, source, and target using the same instant.\n      // Called explicitly because source and target are not children of MergeIntoCommand.\n      transformTimestamps(\n        MergeIntoCommand(\n          transformTimestamps(source, now),\n          transformTimestamps(target, now),\n          relation.catalogTable,\n          tahoeFileIndex,\n          condition,\n          processedMatched,\n          processedNotMatched,\n          processedNotMatchedBySource,\n          migratedSchema = finalSchemaOpt,\n          trackHighWaterMarks = trackHighWaterMarks,\n          schemaEvolutionEnabled = withSchemaEvolution),\n        now)\n    } else {\n      DeltaMergeInto(\n        source,\n        target,\n        condition,\n        processedMatched,\n        processedNotMatched,\n        processedNotMatchedBySource,\n        withSchemaEvolution,\n        finalSchemaOpt)\n    }\n  }\n\n  private def transformTimestamps(plan: LogicalPlan, instant: Instant): LogicalPlan = {\n    import org.apache.spark.sql.delta.implicits._\n\n    val currentTimestampMicros = instantToMicros(instant)\n    val currentTime = Literal.create(currentTimestampMicros, TimestampType)\n    val timezone = Literal.create(conf.sessionLocalTimeZone, StringType)\n\n    plan.transformUpWithSubqueries {\n      case subQuery =>\n        subQuery.transformAllExpressionsUpWithPruning(_.containsPattern(CURRENT_LIKE)) {\n          case cd: CurrentDate =>\n            Literal.create(DateTimeUtils.microsToDays(currentTimestampMicros, cd.zoneId), DateType)\n          case CurrentTimestamp() | Now() => currentTime\n          case CurrentTimeZone() => timezone\n          case localTimestamp: LocalTimestamp =>\n            val asDateTime = LocalDateTime.ofInstant(instant, localTimestamp.zoneId)\n            Literal.create(localDateTimeToMicros(asDateTime), TimestampNTZType)\n        }\n    }\n  }\n\n  /**\n   * Generates update expressions for columns that are not present in the target table and are\n   * introduced by one of the update or insert merge clauses. The generated update expressions and\n   * the update expressions for the existing columns are aligned to match the order in the\n   * target output schema.\n   *\n   * @param target Logical plan node of the target table of merge.\n   * @param resolvedActions Merge actions of the update clause being processed.\n   * @param whenClauses All merge clauses of the merge operation.\n   * @param identityColumns Additional identity columns present in the table.\n   * @param generatedColumns List of the generated columns in the table. See\n   *                         [[UpdateExpressionsSupport]].\n   * @param allowSchemaEvolution Whether to allow schema to evolve. See\n   *                             [[UpdateExpressionsSupport]].\n   * @param postEvolutionTargetSchema The schema of the target table after the merge operation.\n   * @return Update actions aligned on the target output schema `postEvolutionTargetSchema`.\n   */\n  private def alignUpdateActions(\n      target: LogicalPlan,\n      resolvedActions: Seq[DeltaMergeAction],\n      whenClauses: Seq[DeltaMergeIntoClause],\n      identityColumns: Seq[StructField],\n      generatedColumns: Seq[StructField],\n      allowSchemaEvolution: Boolean,\n      postEvolutionTargetSchema: StructType)\n    : Seq[DeltaMergeAction] = {\n    IdentityColumn.blockIdentityColumnUpdate(\n      identityColumns,\n      resolvedActions.map(_.targetColNameParts))\n    // Get the operations for columns that already exist...\n    val existingUpdateOps = resolvedActions.map { a =>\n      UpdateOperation(\n        targetColNameParts = a.targetColNameParts,\n        updateExpr = a.expr,\n        targetOnlyStructFieldBehavior = a.targetOnlyStructFieldBehavior)\n    }\n\n    val newUpdateOps =\n      if (UpdateExpressionsSupport.isWholeStructAssignmentPreserveNullSourceStructsEnabled(conf)) {\n        // We don't want to call `generateUpdateOpsForNewTargetFields` here because:\n        // 1. `generateUpdateExpressions` (below) already handles any fields in the evolved schema\n        // that don't have corresponding actions by assigning them default expressions (NULL for\n        // new fields or the existing target value for existing target fields).\n        // 2. `generateUpdateOpsForNewTargetFields` generates leaf-level operations for new target\n        // fields. If these fields are null structs in the source, they will be expanded to\n        // non-null structs with null fields in the target.\n        Seq.empty\n      } else {\n        // And construct operations for columns that the insert/update clauses will add.\n        generateUpdateOpsForNewTargetFields(target, postEvolutionTargetSchema, resolvedActions)\n      }\n\n    // Use the helper methods in UpdateExpressionsSupport to generate expressions such that nested\n    // fields can be updated (only for existing columns).\n    val alignedExprs = generateUpdateExpressions(\n      targetSchema = postEvolutionTargetSchema,\n      updateOps = existingUpdateOps ++ newUpdateOps,\n      defaultExprs = target.output,\n      resolver = conf.resolver,\n      allowSchemaEvolution = allowSchemaEvolution,\n      generatedColumns = generatedColumns)\n\n    val alignedExprsWithGenerationExprs =\n      if (alignedExprs.forall(_.nonEmpty)) {\n        alignedExprs.map(_.get)\n      } else {\n        generateUpdateExprsForGeneratedColumns(target, generatedColumns, alignedExprs,\n          Some(postEvolutionTargetSchema))\n      }\n\n    alignedExprsWithGenerationExprs\n      .zip(postEvolutionTargetSchema)\n      .map { case (expr, field) =>\n        DeltaMergeAction(\n          targetColNameParts = Seq(field.name),\n          expr = expr,\n          // The action has been aligned to target schema.\n          targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.TARGET_ALIGNED,\n          targetColNameResolved = true)\n      }\n  }\n\n  /**\n   * Generate expressions to set to null the new (potentially nested) fields that are added to the\n   * target table by schema evolution and are not already set by any of the `resolvedActions` from\n   * the merge clause.\n   *\n   * @param target Logical plan node of the target table of merge.\n   * @param postEvolutionTargetSchema The schema of the target table after the merge operation.\n   * @param resolvedActions Merge actions of the update clause being processed.\n   * @return List of update operations\n   */\n  private def generateUpdateOpsForNewTargetFields(\n      target: LogicalPlan,\n      postEvolutionTargetSchema: StructType,\n      resolvedActions: Seq[DeltaMergeAction])\n    : Seq[UpdateOperation] = {\n    // Collect all fields in the final schema that were added by schema evolution.\n    // `SchemaPruning.pruneSchema` only prunes nested fields, we then filter out top-level fields\n    // ourself.\n    val targetSchemaBeforeEvolution =\n      target.schema.map(SchemaPruning.RootField(_, derivedFromAtt = false))\n    val newTargetFields =\n      StructType(SchemaPruning.pruneSchema(postEvolutionTargetSchema, targetSchemaBeforeEvolution)\n      .filterNot { topLevelField => target.schema.exists(_.name == topLevelField.name) })\n\n    /**\n     * Remove the field corresponding to `pathFilter` (if any) from `schema`.\n     */\n    def filterSchema(schema: StructType, pathFilter: Seq[String])\n      : Seq[StructField] = schema.flatMap {\n        case StructField(name, struct: StructType, _, _)\n            if name == pathFilter.head && pathFilter.length > 1 =>\n          Some(StructField(name, StructType(filterSchema(struct, pathFilter.drop(1)))))\n        case f: StructField if f.name == pathFilter.head => None\n        case f => Some(f)\n    }\n    // Then filter out fields that are set by one of the merge actions.\n    val newTargetFieldsWithoutAssignment = resolvedActions\n      .map(_.targetColNameParts)\n      .foldRight(newTargetFields) {\n        (pathFilter, schema) => StructType(filterSchema(schema, pathFilter))\n      }\n\n    /**\n     * Generate the list of all leaf fields and their corresponding data type from `schema`.\n     */\n    def leafFields(schema: StructType, prefix: Seq[String] = Seq.empty)\n      : Seq[(Seq[String], DataType)] = schema.flatMap { field =>\n        val name = prefix :+ field.name.toLowerCase(Locale.ROOT)\n        field.dataType match {\n          case struct: StructType => leafFields(struct, name)\n          case dataType => Seq((name, dataType))\n      }\n    }\n    // Finally, generate an update operation for each remaining field to set it to null.\n    leafFields(newTargetFieldsWithoutAssignment).map {\n      case (name, dataType) =>\n        UpdateOperation(\n          targetColNameParts = name,\n          updateExpr = Literal(null, dataType),\n          // Leaf-level operations are aligned with the target schema naturally.\n          targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.TARGET_ALIGNED)\n    }\n  }\n\n  /**\n   * Resolves any non explicitly inserted generated columns in `allActions` to its\n   * corresponding generated expression.\n   *\n   * For each action, if it's a generated column that is not explicitly inserted, we will\n   * use its generated expression to calculate its value by resolving to a fake project of all the\n   * inserted values. Note that this fake project is created after we set all non explicitly\n   * inserted columns to nulls. This guarantees that all columns referenced by the generated\n   * column, regardless of whether they are explicitly inserted or not, will have a\n   * corresponding expression in the fake project and hence the generated expression can\n   * always be resolved.\n   *\n   * @param explicitActions Actions explicitly specified by users.\n   * @param allActions Actions with non explicitly specified columns added with nulls.\n   * @param sourcePlan Logical plan node of the source table of merge.\n   * @param columnWithDefaultExpr All the generated columns in the target table.\n   * @param identityColumnExpressionMap A mapping from identity column struct fields to expressions\n   * @return `allActions` with expression for non explicitly inserted generated columns expression\n   *        resolved, and columns names for which we will track high water marks.\n   */\n  private def resolveImplicitColumns(\n      explicitActions: Seq[DeltaMergeAction],\n      allActions: Seq[DeltaMergeAction],\n      sourcePlan: LogicalPlan,\n      columnWithDefaultExpr: Seq[(StructField, Boolean)],\n      postEvolutionTargetSchema: StructType,\n      identityColumnExpressionMap: mutable.Map[StructField, Expression])\n    : (Seq[DeltaMergeAction], Set[String]) = {\n    val implicitColumns = columnWithDefaultExpr.filter {\n      case (field, _) =>\n        !explicitActions.exists { insertAct =>\n          conf.resolver(insertAct.targetColNameParts.head, field.name)\n        }\n    }\n    if (implicitColumns.isEmpty) {\n      return (allActions, Set[String]())\n    }\n    assert(postEvolutionTargetSchema.size == allActions.size,\n      \"Invalid number of columns in INSERT clause with generated columns. Expected schema: \" +\n      s\"$postEvolutionTargetSchema, INSERT actions: $allActions\")\n\n    val track = mutable.Set[String]()\n\n    // Fake projection used to resolve generated column expressions.\n    val fakeProjectMap = allActions.map {\n      action => {\n        val exprForProject = Alias(action.expr, action.targetColNameParts.head)()\n        exprForProject.exprId -> exprForProject\n      }\n    }.toMap\n    val fakeProject = Project(fakeProjectMap.values.toArray[Alias], sourcePlan)\n\n    val resolvedActions = allActions.map { action =>\n      val colName = action.targetColNameParts.head\n      implicitColumns.find {\n        case (field, _) => conf.resolver(field.name, colName)\n      } match {\n        case Some((field, true)) =>\n          val expr = GeneratedColumn.getGenerationExpression(field).get\n          val resolvedExpr = resolveReferencesForExpressions(SparkSession.active, expr :: Nil,\n            fakeProject).head\n          // Replace references to fakeProject with original expression.\n          val transformedExpr = resolvedExpr.transform {\n            case a: AttributeReference if fakeProjectMap.contains(a.exprId) =>\n              fakeProjectMap(a.exprId).child\n          }\n          action.copy(expr = transformedExpr)\n        case Some((field, false)) =>\n          // This is the IDENTITY column case. Track the high water marks collection and produce\n          // IDENTITY value generation function.\n          track += field.name\n          // Reuse the existing identityExp which we might have already generated. This is to make\n          // sure that we use the same identity column generation expression across different\n          // WHEN NOT MATCHED branches for a given identity column - so that we can generate\n          // identity values from the same generator and prevent duplicate identity values.\n          val identityExp = identityColumnExpressionMap.getOrElseUpdate(\n            field, IdentityColumn.createIdentityColumnGenerationExpr(field))\n          action.copy(expr = identityExp)\n        case _ => action\n      }\n    }\n    (resolvedActions, track.toSet)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreprocessTableUpdate.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.commands.UpdateCommand\n\nimport org.apache.spark.sql.catalyst.analysis.EliminateSubqueryAliases\nimport org.apache.spark.sql.catalyst.expressions.SubqueryExpression\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Preprocesses the [[DeltaUpdateTable]] logical plan before converting it to [[UpdateCommand]].\n * - Adjusts the column order, which could be out of order, based on the destination table\n * - Generates expressions to compute the value of all target columns in Delta table, while taking\n * into account that the specified SET clause may only update some columns or nested fields of\n * columns.\n */\ncase class PreprocessTableUpdate(sqlConf: SQLConf)\n  extends Rule[LogicalPlan] with UpdateExpressionsSupport {\n\n  override def conf: SQLConf = sqlConf\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {\n    case u: DeltaUpdateTable if u.resolved =>\n      u.condition.foreach { cond =>\n        if (SubqueryExpression.hasSubquery(cond)) {\n          throw DeltaErrors.subqueryNotSupportedException(\"UPDATE\", cond)\n        }\n      }\n      toCommand(u)\n  }\n\n  def toCommand(update: DeltaUpdateTable): UpdateCommand = {\n    val deltaLogicalNode = EliminateSubqueryAliases(update.child)\n    val (relation, index) = deltaLogicalNode match {\n      case DeltaFullTable(rel, tahoeFileIndex) => rel -> tahoeFileIndex\n      case o =>\n        throw DeltaErrors.notADeltaSourceException(\"UPDATE\", Some(o))\n    }\n\n    val generatedColumns = GeneratedColumn.getGeneratedColumns(index)\n    if (generatedColumns.nonEmpty &&\n        !GeneratedColumn.allowDMLTargetPlan(deltaLogicalNode, conf)) {\n      // Disallow temp views referring to a Delta table that contains generated columns. When the\n      // user doesn't provide expressions for generated columns, we need to create update\n      // expressions for them automatically. Currently, we assume `update.child.output` is the same\n      // as the table schema when checking whether a column in `update.child.output` is a generated\n      // column in the table.\n      throw DeltaErrors.operationOnTempViewWithGenerateColsNotSupported(\"UPDATE\")\n    }\n\n    val targetColNameParts = update.updateColumns.map(DeltaUpdateTable.getTargetColNameParts(_))\n\n    IdentityColumn.blockIdentityColumnUpdate(index.snapshotAtAnalysis.schema, targetColNameParts)\n\n    val alignedUpdateExprs = generateUpdateExpressions(\n      targetSchema = update.child.schema,\n      defaultExprs = update.child.output,\n      nameParts = targetColNameParts,\n      updateExprs = update.updateExpressions,\n      resolver = conf.resolver,\n      generatedColumns = generatedColumns\n    )\n    val alignedUpdateExprsAfterAddingGenerationExprs =\n      if (alignedUpdateExprs.forall(_.nonEmpty)) {\n        alignedUpdateExprs.map(_.get)\n      } else {\n        // Some expressions for generated columns are not specified by the user, so we need to\n        // create them based on the generation expressions.\n        generateUpdateExprsForGeneratedColumns(update.child, generatedColumns, alignedUpdateExprs)\n      }\n    UpdateCommand(\n      index,\n      relation.catalogTable,\n      update.child,\n      alignedUpdateExprsAfterAddingGenerationExprs,\n      update.condition)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreprocessTableWithDVs.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaParquetFileFormat._\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils.deletionVectorsReadable\nimport org.apache.spark.sql.delta.files.{TahoeFileIndex, TahoeLogFileIndex}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.{AttributeReference, EqualTo, Literal}\nimport org.apache.spark.sql.catalyst.expressions.Literal.TrueLiteral\nimport org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan, Project}\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.execution.datasources.FileFormat.METADATA_NAME\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Plan transformer to inject a filter that removes the rows marked as deleted according to\n * deletion vectors. For tables with no deletion vectors, this transformation has no effect.\n *\n * It modifies for plan for tables with deletion vectors as follows:\n * Before rule: <Parent Node> -> Delta Scan (key, value).\n *   - Here we are reading `key`, `value`` columns from the Delta table\n * After rule:\n *   <Parent Node> ->\n *     Project(key, value) ->\n *       Filter (__skip_row == 0) ->\n *         Delta Scan (key, value, __skip_row)\n *   - Here we insert a new column `__skip_row` in Delta scan. This value is populated by the\n *     Parquet reader using the DV corresponding to the Parquet file read\n *     (See [[DeltaParquetFileFormat]]) and it contains 0 if we want to keep the row.\n *   - Filter created filters out rows with __skip_row equals to 0\n *   - And at the end we have a Project to keep the plan node output same as before the rule is\n *     applied.\n */\ntrait PreprocessTableWithDVs extends SubqueryTransformerHelper {\n  def preprocessTablesWithDVs(plan: LogicalPlan): LogicalPlan = {\n    transformWithSubqueries(plan) {\n      case ScanWithDeletionVectors(dvScan) => dvScan\n    }\n  }\n}\n\nobject ScanWithDeletionVectors {\n  def unapply(a: LogicalRelation): Option[LogicalPlan] = a match {\n    case scan @ LogicalRelationWithTable(\n            relation @ HadoopFsRelation(\n            index: TahoeFileIndex, _, _, _, format: DeltaParquetFileFormat, _), _) =>\n      dvEnabledScanFor(scan, relation, format, index)\n    case _ => None\n  }\n\n  def dvEnabledScanFor(\n      scan: LogicalRelation,\n      hadoopRelation: HadoopFsRelation,\n      fileFormat: DeltaParquetFileFormat,\n      index: TahoeFileIndex): Option[LogicalPlan] = {\n    // If the table has no DVs enabled, no change needed\n    if (!deletionVectorsReadable(index.protocol, index.metadata)) return None\n\n    // See if the relation is already modified to include DV reads as part of\n    // a previous invocation of this rule on this table\n    if (fileFormat.hasTablePath) return None\n\n    // See if any files actually have a DV.\n\n    // IMPORTANT: Check this BEFORE requiring pinned snapshot:\n    // 1. Tables can have DV feature enabled without any DV files (e.g., no DELETEs performed yet)\n    // 2. Reading such tables doesn't require DV processing -> doesn't need pinned snapshots\n    // 3. Unnecessary pinned snapshot requirements break legitimate use cases like transaction\n    //    read tracking with TahoeLogFileIndex (e.g., OptimisticTransaction.withNewTransaction)\n    // 4. Performance: Avoids forcing expensive pinned snapshots when not needed\n    val filesWithDVs = index\n      .matchingFiles(partitionFilters = Seq(TrueLiteral), dataFilters = Seq(TrueLiteral))\n      .filter(_.deletionVector != null)\n    if (filesWithDVs.isEmpty) return None\n\n    // At this point, we know there are actual files with DVs, so we need a pinned snapshot.\n    // TahoeLogFileIndex (non-pinned) cannot be used with deletion vectors as it may not\n    // have a consistent view of the table state required for correct DV filtering.\n    require(!index.isInstanceOf[TahoeLogFileIndex],\n      \"Cannot work with a non-pinned table snapshot of the TahoeFileIndex\")\n\n    // Get the list of columns in the output of the `LogicalRelation` we are\n    // trying to modify. At the end of the plan, we need to return a\n    // `LogicalRelation` that has the same output as this `LogicalRelation`\n    val planOutput = scan.output\n\n    val spark = SparkSession.getActiveSession.get\n    val newScan = createScanWithSkipRowColumn(spark, scan, fileFormat, index, hadoopRelation)\n\n    // On top of the scan add a filter that filters out the rows which have\n    // skip row column value non-zero\n    val rowIndexFilter = createRowIndexFilterNode(newScan)\n\n    // Now add a project on top of the row index filter node to\n    // remove the skip row column\n    Some(Project(planOutput, rowIndexFilter))\n  }\n\n  /**\n   * Helper function that adds row_index column to _metadata if missing.\n   */\n  private def addRowIndexIfMissing(attribute: AttributeReference): AttributeReference = {\n    require(attribute.name == METADATA_NAME)\n\n    val dataType = attribute.dataType.asInstanceOf[StructType]\n    if (dataType.fieldNames.contains(ParquetFileFormat.ROW_INDEX)) return attribute\n\n    val newDatatype = dataType.add(ParquetFileFormat.ROW_INDEX_FIELD)\n    attribute.copy(\n      dataType = newDatatype)(exprId = attribute.exprId, qualifier = attribute.qualifier)\n  }\n\n  /**\n   * Helper method that creates a new `LogicalRelation` for existing scan that outputs\n   * an extra column which indicates whether the row needs to be skipped or not.\n   */\n  private def createScanWithSkipRowColumn(\n      spark: SparkSession,\n      inputScan: LogicalRelation,\n      fileFormat: DeltaParquetFileFormat,\n      tahoeFileIndex: TahoeFileIndex,\n      hadoopFsRelation: HadoopFsRelation): LogicalRelation = {\n    val useMetadataRowIndex =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX)\n\n    // Create a new `LogicalRelation` that has modified `DeltaFileFormat` and output with an extra\n    // column to indicate whether to skip the row or not\n\n    // Add a column for SKIP_ROW to the base output. Value of 0 means the row needs be kept, any\n    // other values mean the row needs be skipped.\n    val skipRowField = IS_ROW_DELETED_STRUCT_FIELD\n\n    val scanOutputWithMetadata = if (useMetadataRowIndex) {\n      // When predicate pushdown is enabled, make sure the output contains metadata.row_index.\n      if (inputScan.output.map(_.name).contains(METADATA_NAME)) {\n        // If the scan already contains a metadata column without a row_index, add it.\n        inputScan.output.collect {\n          case a: AttributeReference if a.name == METADATA_NAME => addRowIndexIfMissing(a)\n          case o => o\n        }\n      } else {\n        inputScan.output :+ fileFormat.createFileMetadataCol()\n      }\n    } else {\n      inputScan.output\n    }\n\n    val newScanOutput =\n      scanOutputWithMetadata :+ AttributeReference(skipRowField.name, skipRowField.dataType)()\n\n    // Data schema and scan schema could be different. The scan schema may contain additional\n    // columns such as `_metadata.file_path` (metadata columns) which are populated in Spark scan\n    // operator after the data is read from the underlying file reader.\n    val newDataSchema = hadoopFsRelation.dataSchema.add(skipRowField)\n\n    val newFileFormat = fileFormat.copyWithDVInfo(\n      tablePath = tahoeFileIndex.path.toString,\n      optimizationsEnabled = useMetadataRowIndex)\n\n    val newRelation = hadoopFsRelation.copy(\n      fileFormat = newFileFormat,\n      dataSchema = newDataSchema)(hadoopFsRelation.sparkSession)\n\n    // Create a new scan LogicalRelation\n    inputScan.copy(relation = newRelation, output = newScanOutput)\n  }\n\n  private def createRowIndexFilterNode(newScan: LogicalRelation): Filter = {\n    val skipRowColumnRefs = newScan.output.filter(_.name == IS_ROW_DELETED_COLUMN_NAME)\n    require(skipRowColumnRefs.size == 1,\n      s\"Expected only one column with name=$IS_ROW_DELETED_COLUMN_NAME\")\n    val skipRowColumnRef = skipRowColumnRefs.head\n\n    Filter(EqualTo(skipRowColumnRef, Literal(RowIndexFilter.KEEP_ROW_VALUE)), newScan)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreprocessTableWithDVsStrategy.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.planning.ScanOperation\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.execution.{SparkPlan, SparkStrategy}\nimport org.apache.spark.sql.execution.datasources.{FileSourceStrategy, HadoopFsRelation, LogicalRelationWithTable}\n\n/**\n * Strategy to process tables with DVs and add the skip row column and filters.\n *\n * This strategy will apply all transformations needed to tables with DVs and delegate to\n * [[FileSourceStrategy]] to create the final plan. The DV filter will be the bottom-most filter in\n * the plan and so it will be pushed down to the FileSourceScanExec at the beginning of the filter\n * list.\n */\ncase class PreprocessTableWithDVsStrategy(session: SparkSession)\n    extends SparkStrategy\n    with PreprocessTableWithDVs {\n\n  override def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {\n    case ScanOperation(_, _, _, _ @ LogicalRelationWithTable(_: HadoopFsRelation, _)) =>\n      val updatedPlan = preprocessTablesWithDVs(plan)\n      FileSourceStrategy(updatedPlan)\n    case _ => Nil\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/PreprocessTimeTravel.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.catalyst.TimeTravel\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.{EliminateSubqueryAliases, ResolvedTable, UnresolvedRelation}\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Resolves the [[UnresolvedRelation]] in command 's child [[TimeTravel]].\n *   Currently Delta depends on Spark 3.2 which does not resolve the [[UnresolvedRelation]]\n *   in [[TimeTravel]]. Once Delta upgrades to Spark 3.3, this code can be removed.\n *\n * TODO: refactoring this analysis using Spark's native [[TimeTravelRelation]] logical plan\n */\ncase class PreprocessTimeTravel(sparkSession: SparkSession) extends Rule[LogicalPlan] {\n\n  override def conf: SQLConf = sparkSession.sessionState.conf\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {\n    case _ @ RestoreTableStatement(tt @ TimeTravel(ur @ UnresolvedRelation(_, _, _), _, _, _)) =>\n      val sourceRelation = resolveTimeTravelTable(sparkSession, ur, \"RESTORE\")\n      return RestoreTableStatement(\n        TimeTravel(\n          sourceRelation,\n          tt.timestamp,\n          tt.version,\n          tt.creationSource))\n\n    case ct @ CloneTableStatement(\n        tt @ TimeTravel(ur: UnresolvedRelation, _, _, _), _,\n          _, _, _, _, _) =>\n      val sourceRelation = resolveTimeTravelTable(sparkSession, ur, \"CLONE TABLE\")\n      ct.copy(source = TimeTravel(\n        sourceRelation,\n        tt.timestamp,\n        tt.version,\n        tt.creationSource))\n  }\n\n  /**\n   * Helper to resolve a [[TimeTravel]] logical plan to Delta DSv2 relation.\n   */\n  private def resolveTimeTravelTable(\n      sparkSession: SparkSession,\n      ur: UnresolvedRelation,\n      commandName: String): LogicalPlan = {\n    // Since TimeTravel is a leaf node, the table relation within TimeTravel won't be resolved\n    // automatically by the Apache Spark analyzer rule `ResolveRelations`.\n    // Thus, we need to explicitly use the rule `ResolveRelations` to table resolution here.\n    EliminateSubqueryAliases(sparkSession.sessionState.analyzer.ResolveRelations(ur)) match {\n      case _: View =>\n        // If the identifier is a view, throw not supported error\n        throw DeltaErrors.notADeltaTableException(commandName)\n      case tableRelation if tableRelation.resolved =>\n        tableRelation\n      case _ =>\n        // If the identifier doesn't exist as a table, try resolving it as a path table.\n        ResolveDeltaPathTable.resolveAsPathTableRelation(sparkSession, ur).getOrElse {\n          ur.tableNotFound(ur.multipartIdentifier)\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ProtocolMetadataAdapter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{StructField, StructType}\n\n/**\n * Abstraction layer over Delta Protocol and Metadata hide implementation details of\n * Protocol and Metadata classes, enabling DeltaParquetFileFormat to be reused without depending on\n * specific action class implementations.\n * This helps delta kernel based connector reusing DeltaParquetFileFormat.\n */\ntrait ProtocolMetadataAdapter {\n\n  /**\n   * Returns the column mapping mode for this table.\n   */\n  def columnMappingMode: DeltaColumnMappingMode\n\n  /**\n   * Returns the logical schema of the table.\n   */\n  def getReferenceSchema: StructType\n\n  /**\n   * Returns whether Row IDs(Row tracking) are enabled on this table.\n   */\n  def isRowIdEnabled: Boolean\n\n  /**\n   * Returns whether Deletion Vectors are readable on this table.\n   */\n  def isDeletionVectorReadable: Boolean\n\n  /**\n   * Returns whether any version of IcebergCompat is enabled on this table.\n   */\n  def isIcebergCompatAnyEnabled: Boolean\n\n  /**\n   * Returns whether IcebergCompat is enabled at or above the specified version.\n   * @param version The IcebergCompat version to check (e.g., 2, 3)\n   */\n  def isIcebergCompatGeqEnabled(version: Int): Boolean\n\n  /**\n   * Asserts that the table is readable given the current configuration.\n   * Throws an exception if the table cannot be read.\n   *\n   * @param sparkSession The current Spark session\n   */\n  def assertTableReadable(sparkSession: SparkSession): Unit\n\n  /**\n   * Creates the metadata struct fields for row tracking.\n   *\n   * @param nullableRowTrackingConstantFields whether constant fields should be nullable\n   * @param nullableRowTrackingGeneratedFields whether generated fields should be nullable\n   * @return metadata fields for row tracking (_metadata.row_id, _metadata.base_row_id, etc.)\n   */\n  def createRowTrackingMetadataFields(\n      nullableRowTrackingConstantFields: Boolean,\n      nullableRowTrackingGeneratedFields: Boolean): Iterable[StructField]\n\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ProtocolMetadataAdapterV1.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.types.{StructField, StructType}\n\n/**\n * Implementation of ProtocolMetadataAdapter for delta-spark v1 Protocol and Metadata.\n *\n * This class adapts the existing delta-spark Protocol and Metadata actions to the\n * ProtocolMetadataAdapter interface, enabling code reuse in DeltaParquetFileFormat.\n */\ncase class ProtocolMetadataAdapterV1(\n    protocol: Protocol,\n    metadata: Metadata) extends ProtocolMetadataAdapter {\n\n  override def columnMappingMode: DeltaColumnMappingMode = metadata.columnMappingMode\n\n  override def getReferenceSchema: StructType = metadata.schema\n\n  override def isRowIdEnabled: Boolean = RowId.isEnabled(protocol, metadata)\n\n  override def isDeletionVectorReadable: Boolean =\n    DeletionVectorUtils.deletionVectorsReadable(protocol, metadata)\n\n  override def isIcebergCompatAnyEnabled: Boolean = IcebergCompat.isAnyEnabled(metadata)\n\n  override def isIcebergCompatGeqEnabled(version: Int): Boolean =\n    IcebergCompat.isGeqEnabled(metadata, version)\n\n  override def assertTableReadable(sparkSession: SparkSession): Unit = {\n    TypeWidening.assertTableReadable(sparkSession.sessionState.conf, protocol, metadata)\n  }\n\n  override def createRowTrackingMetadataFields(\n      nullableRowTrackingConstantFields: Boolean,\n      nullableRowTrackingGeneratedFields: Boolean): Iterable[StructField] = {\n    RowTracking.createMetadataStructFields(\n      protocol,\n      metadata,\n      nullableConstantFields = nullableRowTrackingConstantFields,\n      nullableGeneratedFields = nullableRowTrackingGeneratedFields)\n  }\n\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ProvidesUniFormConverters.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.lang.reflect.InvocationTargetException\n\nimport org.apache.commons.lang3.exception.ExceptionUtils\n\nimport org.apache.spark.util.Utils\n\ntrait ProvidesUniFormConverters { self: DeltaLog =>\n  /**\n   * Helper trait to instantiate the icebergConverter member variable of the [[DeltaLog]]. We do\n   * this through reflection so that delta-spark doesn't have a compile-time dependency on the\n   * shaded iceberg module.\n   */\n  protected lazy val _icebergConverter: UniversalFormatConverter = try {\n    val clazz = Utils.classForName(\"org.apache.spark.sql.delta.icebergShaded.IcebergConverter\")\n    clazz.getConstructor().newInstance()\n  } catch {\n    case e: ClassNotFoundException =>\n      logError(log\"Failed to find Iceberg converter class\", e)\n      throw DeltaErrors.icebergClassMissing(spark.sparkContext.getConf, e)\n    case e: InvocationTargetException =>\n      logError(log\"Got error when creating an Iceberg converter\", e)\n      // The better error is within the cause\n      throw ExceptionUtils.getRootCause(e)\n  }\n\n  protected lazy val _hudiConverter: UniversalFormatConverter = try {\n    val clazz = Utils.classForName(\"org.apache.spark.sql.delta.hudi.HudiConverter\")\n    clazz.getConstructor().newInstance()\n  } catch {\n    case e: ClassNotFoundException =>\n      logError(log\"Failed to find Hudi converter class\", e)\n      throw DeltaErrors.hudiClassMissing(spark.sparkContext.getConf, e)\n    case e: InvocationTargetException =>\n      logError(log\"Got error when creating an Hudi converter\", e)\n      // The better error is within the cause\n      throw ExceptionUtils.getRootCause(e)\n  }\n\n\n  /** Visible for tests (to be able to mock). */\n  private[delta] var testIcebergConverter: Option[UniversalFormatConverter] = None\n  private[delta] var testHudiConverter: Option[UniversalFormatConverter] = None\n\n  def icebergConverter: UniversalFormatConverter = testIcebergConverter.getOrElse(_icebergConverter)\n  def hudiConverter: UniversalFormatConverter = testHudiConverter.getOrElse(_hudiConverter)\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ResolveDeltaMergeInto.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Locale\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.AllowAutomaticWideningMode\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{AtomicType, StructField, StructType}\n\n/**\n * Implements logic to resolve conditions and actions in MERGE clauses and handles schema evolution.\n */\nobject ResolveDeltaMergeInto {\n  type ResolveExpressionsFn = (Seq[Expression], Seq[LogicalPlan]) => Seq[Expression]\n\n  def throwIfNotResolved(\n      expr: Expression,\n      plans: Seq[LogicalPlan],\n      mergeClauseTypeStr: String): Unit = {\n    for (a <- expr.flatMap(_.references).filterNot(_.resolved)) {\n      // Note: This will throw error only on unresolved attribute issues,\n      // not other resolution errors like mismatched data types.\n      val cols = plans.flatMap(_.output).map(_.sql).mkString(\", \")\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n        messageParameters = Array(a.sql, mergeClauseTypeStr, cols),\n        origin = Some(a.origin))\n    }\n  }\n\n  /**\n   * Resolves expressions against given plans or fail using given message. It makes a best-effort\n   * attempt to throw specific error messages on which part of the query has a problem.\n   */\n  def resolveOrFail(\n      resolveExprsFn: ResolveExpressionsFn,\n      exprs: Seq[Expression],\n      plansToResolveExprs: Seq[LogicalPlan],\n      mergeClauseTypeStr: String): Seq[Expression] = {\n    val resolvedExprs = resolveExprsFn(exprs, plansToResolveExprs)\n    resolvedExprs.foreach(throwIfNotResolved(_, plansToResolveExprs, mergeClauseTypeStr))\n    resolvedExprs\n  }\n\n  /**\n   * Convenience wrapper around `resolveOrFail()` when resolving a single expression.\n   */\n  def resolveSingleExprOrFail(\n      resolveExprsFn: ResolveExpressionsFn,\n      expr: Expression,\n      plansToResolveExpr: Seq[LogicalPlan],\n      mergeClauseTypeStr: String): Expression = {\n    resolveOrFail(resolveExprsFn, Seq(expr), plansToResolveExpr, mergeClauseTypeStr).head\n  }\n\n  /**\n   * Computes the target schema after applying schema evolution.\n   *\n   * When schema evolution is enabled, this method adds new columns or nested fields from the source\n   * that are assigned to in merge actions. It filters the source schema to retain only referenced\n   * fields, then merges this with the target schema.\n   *\n   * @param canEvolveSchema whether schema evolution is enabled\n   * @param resolvedMatchedClauses resolved MATCHED clauses\n   * @param resolvedNotMatchedClauses resolved NOT MATCHED clauses\n   * @param target the target table plan\n   * @param source the source data plan\n   * @param conf SQL configuration\n   * @return the evolved target schema (or original target schema if evolution is disabled)\n   */\n  private def computePostEvolutionTargetSchema(\n      canEvolveSchema: Boolean,\n      resolvedMatchedClauses: Seq[DeltaMergeIntoClause],\n      resolvedNotMatchedClauses: Seq[DeltaMergeIntoClause],\n      target: LogicalPlan,\n      source: LogicalPlan,\n      conf: SQLConf): StructType = {\n    if (canEvolveSchema) {\n      // When schema evolution is enabled, add to the target table new columns or nested fields that\n      // are assigned to in merge actions and not already part of the target schema. This is done by\n      // collecting all assignments from merge actions and using them to filter out the source\n      // schema before merging it with the target schema. We don't consider NOT MATCHED BY SOURCE\n      // clauses since these can't by definition reference source columns and thus can't introduce\n      // new columns in the target schema.\n      val actions = (resolvedMatchedClauses ++ resolvedNotMatchedClauses).flatMap(_.actions)\n      val assignments = actions.collect { case a: DeltaMergeAction => a.targetColNameParts }\n      val containsStarAction = actions.exists {\n        case _: UnresolvedStar => true\n        case _ => false\n      }\n\n\n      // Filter the source schema to retain only fields that are referenced by at least one merge\n      // clause, then merge this schema with the target to give the final schema.\n      def filterSchema(sourceSchema: StructType, basePath: Seq[String]): StructType =\n        StructType(sourceSchema.flatMap { field =>\n          val fieldPath = basePath :+ field.name\n\n          // Helper method to check if a given field path is a prefix of another path. Delegates\n          // equality to conf.resolver to correctly handle case sensitivity.\n          def isPrefix(prefix: Seq[String], path: Seq[String]): Boolean =\n            prefix.length <= path.length && prefix.zip(path).forall {\n              case (prefixNamePart, pathNamePart) => conf.resolver(prefixNamePart, pathNamePart)\n            }\n\n          // Helper method to check if a given field path is equal to another path.\n          def isEqual(path1: Seq[String], path2: Seq[String]): Boolean =\n            path1.length == path2.length && isPrefix(path1, path2)\n\n\n          field.dataType match {\n            // Specifically assigned to in one clause: always keep, including all nested attributes\n            case _ if assignments.exists(isEqual(_, fieldPath)) => Some(field)\n            // If this is a struct and one of the children is being assigned to in a merge clause,\n            // keep it and continue filtering children.\n            case struct: StructType if assignments.exists(isPrefix(fieldPath, _)) =>\n              Some(field.copy(dataType = filterSchema(struct, fieldPath)))\n            // The field isn't assigned to directly or indirectly (i.e. its children) in any non-*\n            // clause. Check if it should be kept with any * action.\n            case struct: StructType if containsStarAction =>\n              Some(field.copy(dataType = filterSchema(struct, fieldPath)))\n            case _ if containsStarAction => Some(field)\n            // The field and its children are not assigned to in any * or non-* action, drop it.\n            case _ => None\n          }\n        })\n\n      val migrationSchema = filterSchema(source.schema, Seq.empty)\n\n      val typeWideningMode =\n        target.collectFirst {\n          case DeltaTable(index) if TypeWidening.isEnabled(index.protocol, index.metadata) =>\n            TypeWideningMode.TypeEvolution(\n              uniformIcebergCompatibleOnly = UniversalFormat.icebergEnabled(index.metadata),\n              allowAutomaticWidening = AllowAutomaticWideningMode.fromConf(conf))\n        }.getOrElse(TypeWideningMode.NoTypeWidening)\n\n      // The implicit conversions flag allows any type to be merged from source to target if Spark\n      // SQL considers the source type implicitly castable to the target. Normally, mergeSchemas\n      // enforces Parquet-level write compatibility, which would mean an INT source can't be merged\n      // into a LONG target.\n      SchemaMergingUtils.mergeSchemas(\n        target.schema,\n        migrationSchema,\n        allowImplicitConversions = true,\n        typeWideningMode = typeWideningMode\n      )\n    } else {\n      target.schema\n    }\n  }\n\n  /**\n   * Resolves a merge clause by resolving its actions and condition.\n   *\n   * Actions are split into two groups:\n   * (1) DeltaMergeActions (like `UPDATE SET x = a, y = b`): resolved with DeltaMergeActionResolver\n   * (2) Star expressions (like `UPDATE SET *` or `INSERT *`): resolved with resolveStar(Except)\n   *\n   * @param clause the merge clause to resolve (MATCHED UPDATE, NOT MATCHED INSERT, etc.)\n   * @param plansToResolveAction the logical plans to use for resolving action expressions\n   * @param target the target table plan\n   * @param source the source data plan\n   * @param canEvolveSchema whether schema evolution is enabled\n   * @param mergeActionResolver resolver for DeltaMergeAction expressions\n   * @param resolveExprsFn function to resolve expressions\n   * @param conf SQL configuration\n   * @return the resolved clause\n   */\n  private def resolveClause[T <: DeltaMergeIntoClause](\n      clause: T,\n      plansToResolveAction: Seq[LogicalPlan],\n      target: LogicalPlan,\n      source: LogicalPlan,\n      canEvolveSchema: Boolean,\n      mergeActionResolver: DeltaMergeActionResolverBase,\n      resolveExprsFn: ResolveExpressionsFn,\n      conf: SQLConf): T = {\n\n    val clauseType = clause.clauseType.toUpperCase(Locale.ROOT)\n    val mergeClauseTypeStr = s\"$clauseType clause\"\n\n    // We split the actions of a clause (expressions) into two mutually exclusive groups:\n    // 1) DeltaMergeActions and 2) everything else (UnresolvedStar).\n    // The DeltaMergeActions can be resolved already or unresolved at this point.\n    // Unresolved DeltaMergeActions correspond to actions like `UPDATE SET x = a, y = b` or\n    // `INSERT (x, y) VALUES (a, b)`.\n    // By the end of this function, every action needs to be transformed into a resolved\n    // DeltaMergeAction. We handle the DeltaMergeActions separately in [[DeltaMergeActionResolver]]\n    // as we have different strategies to enable better analysis performance.\n    val (deltaMergeActions, allOtherExpressions) = clause.actions.partition {\n      case _: DeltaMergeAction => true\n      case _ => false\n    }\n    assert(\n      deltaMergeActions.isEmpty || allOtherExpressions.isEmpty,\n      s\"Cannot have DeltaMergeActions combined with other expressions in a $mergeClauseTypeStr\")\n\n    val shouldTryUnresolvedTargetExprOnSource = clause match {\n      case _: DeltaMergeIntoMatchedUpdateClause |\n           _: DeltaMergeIntoNotMatchedClause => canEvolveSchema\n      case _ => false\n    }\n    val resolvedDeltaMergeActions: Seq[DeltaMergeAction] = mergeActionResolver.resolve(\n      mergeClauseTypeStr,\n      plansToResolveAction,\n      shouldTryUnresolvedTargetExprOnSource,\n      deltaMergeActions.map(_.asInstanceOf[DeltaMergeAction])\n    )\n\n    val resolvedOtherExpressions: Seq[DeltaMergeAction] = allOtherExpressions.flatMap { action =>\n      action match {\n        // For actions like `UPDATE SET *` or `INSERT *`\n        case _: UnresolvedStar =>\n          resolveStar(\n            clause, target, source, canEvolveSchema, resolveExprsFn, mergeClauseTypeStr, conf)\n\n\n        case _ =>\n          action.failAnalysis(\"INTERNAL_ERROR\",\n            Map(\"message\" -> s\"Unexpected action expression '$action' in clause $clause\"))\n      }\n    }\n\n    val resolvedCondition = clause.condition.map { condExpr =>\n      resolveSingleExprOrFail(\n        resolveExprsFn,\n        condExpr,\n        plansToResolveAction,\n        mergeClauseTypeStr = s\"$clauseType condition\")\n    }\n    clause.makeCopy(Array(resolvedCondition,\n        resolvedDeltaMergeActions ++ resolvedOtherExpressions\n    )).asInstanceOf[T]\n  }\n\n  /**\n   * Resolves UnresolvedStar (`*`) for `UPDATE SET *` or `INSERT *` actions.\n   *\n   * When schema evolution is disabled: expands `*` for all target columns\n   *\n   * When schema evolution is enabled:\n   * - For INSERT clauses: expands `*` for all source columns\n   * - For UPDATE clauses: expands `*` for all source leaf fields\n   *\n   * @param clause the merge clause being resolved (INSERT or UPDATE)\n   * @param target the target table plan\n   * @param source the source data plan\n   * @param canEvolveSchema whether schema evolution is enabled\n   * @param resolveExprsFn function to resolve expressions\n   * @param mergeClauseTypeStr string description of the clause type for error messages\n   * @param conf SQL configuration\n   * @return sequence of resolved DeltaMergeActions\n   */\n  private def resolveStar(\n      clause: DeltaMergeIntoClause,\n      target: LogicalPlan,\n      source: LogicalPlan,\n      canEvolveSchema: Boolean,\n      resolveExprsFn: ResolveExpressionsFn,\n      mergeClauseTypeStr: String,\n      conf: SQLConf): Seq[DeltaMergeAction] = {\n    if (!canEvolveSchema) {\n      // Expand `*` into seq of [ `columnName = sourceColumnBySameName` ] for every target\n      // column name. The target columns do not need resolution. The right hand side\n      // expression (i.e. sourceColumnBySameName) needs to be resolved only by the source plan.\n      val unresolvedExprs = target.output.map { attr =>\n        UnresolvedAttribute.quotedString(s\"`${attr.name}`\")\n      }\n      val resolvedExprs = resolveOrFail(\n        resolveExprsFn = resolveExprsFn,\n        exprs = unresolvedExprs,\n        plansToResolveExprs = Seq(source),\n        mergeClauseTypeStr = mergeClauseTypeStr)\n      (resolvedExprs, target.output.map(_.name))\n        .zipped\n        .map { (resolvedExpr, targetColName) =>\n          DeltaMergeAction(\n            targetColNameParts = Seq(targetColName),\n            expr = resolvedExpr,\n            // Schema evolution is disabled, so the action expression should already be aligned\n            // with the target schema.\n            targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.TARGET_ALIGNED,\n            targetColNameResolved = true)\n        }\n    } else {\n      clause match {\n        case _: DeltaMergeIntoNotMatchedInsertClause =>\n          // Expand `*` into seq of [ `columnName = sourceColumnBySameName` ] for every source\n          // column name. Target columns not present in the source will be filled in\n          // with null later.\n          source.output.map { attr =>\n            DeltaMergeAction(\n              targetColNameParts = Seq(attr.name),\n              expr = attr,\n              // INSERT sets target-only struct fields to null since there is no existing target\n              // row to preserve values from.\n              targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.NULLIFY,\n              targetColNameResolved = true)\n          }\n        case _: DeltaMergeIntoMatchedUpdateClause =>\n          // Expand `*` into seq of [ `columnName = sourceColumnBySameName` ] for every source\n          // column name. Target columns not present in the source will be filled in with\n          // no-op actions later.\n          if (UpdateExpressionsSupport.isUpdateStarPreserveNullSourceStructsEnabled(conf)) {\n            // Expand `*` into column-level actions to fix null expansion in UPDATE *, i.e. a null\n            // source struct is expanded into a non-null struct with all fields set to null.\n            source.output.map { attr =>\n              DeltaMergeAction(\n                targetColNameParts = Seq(attr.name),\n                expr = attr,\n                // Preserve the original value of target-only struct fields to be consistent with\n                // the behavior without the null expansion fix. This avoids the breaking change that\n                // causes data loss.\n                targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.PRESERVE,\n                targetColNameResolved = true)\n            }\n          } else {\n            // Nested columns are unfolded to accommodate the case where a source struct has a\n            // subset of the nested columns in the target. If a source struct (a, b) is writing\n            // into a target (a, b, c), the final struct after filling in the no-op actions will\n            // be (s.a, s.b, t.c).\n            getLeafActionsForSchema(source.schema, Seq.empty, source, conf)\n          }\n      }\n    }\n  }\n\n  /**\n   * Returns the sequence of [[DeltaMergeActions]] corresponding to\n   * [ `columnName = sourceColumnBySameName` ] for every column name in the schema. Nested\n   * columns are unfolded to create an assignment for each leaf.\n   *\n   * @param currSchema: schema to generate DeltaMergeAction for every 'leaf'\n   * @param qualifier: used to recurse to leaves; represents the qualifier of the current schema\n   * @param source: source plan to resolve expressions against\n   * @param conf: SQL configuration\n   * @return seq of DeltaMergeActions corresponding to columnName = sourceColumnName updates\n   */\n  private def getLeafActionsForSchema(\n      currSchema: StructType,\n      qualifier: Seq[String],\n      source: LogicalPlan,\n      conf: SQLConf): Seq[DeltaMergeAction] = {\n    currSchema.flatMap {\n      case StructField(name, struct: StructType, _, _) =>\n        getLeafActionsForSchema(struct, qualifier :+ name, source, conf)\n      case StructField(name, _, _, _) =>\n        val nameParts = qualifier :+ name\n        val sourceExpr = source.resolve(nameParts, conf.resolver).getOrElse {\n          // if we use getActions to expand target columns, this will fail on target columns not\n          // present in the source\n          throw DeltaErrors.cannotResolveSourceColumnException(nameParts)\n        }\n        Seq(\n          DeltaMergeAction(\n            targetColNameParts = nameParts,\n            expr = sourceExpr,\n            // Leaf-level operations are aligned with the target schema naturally.\n            targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.TARGET_ALIGNED,\n            targetColNameResolved = true))\n    }\n  }\n\n  def resolveReferencesAndSchema(\n      merge: DeltaMergeInto,\n      conf: SQLConf)(resolveExprsFn: ResolveExpressionsFn): DeltaMergeInto = {\n    val DeltaMergeInto(\n      target,\n      source,\n      condition,\n      matchedClauses,\n      notMatchedClauses,\n      notMatchedBySourceClauses,\n      withSchemaEvolution,\n      _) = merge\n\n    val canEvolveSchema =\n      withSchemaEvolution || conf.getConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE)\n\n    val mergeActionResolver =\n      if (conf.getConf(DeltaSQLConf.DELTA_MERGE_ANALYSIS_BATCH_RESOLUTION)) {\n        new BatchedDeltaMergeActionResolver(target, source, conf, resolveExprsFn)\n      } else {\n        new IndividualDeltaMergeActionResolver(target, source, conf, resolveExprsFn)\n    }\n\n    // We must do manual resolution as the expressions in different clauses of the MERGE have\n    // visibility of the source, the target or both.\n    val resolvedCond = resolveSingleExprOrFail(\n      resolveExprsFn,\n      expr = condition,\n      plansToResolveExpr = Seq(target, source),\n      mergeClauseTypeStr = \"search condition\")\n    val resolvedMatchedClauses = matchedClauses.map {\n      resolveClause(\n        _, Seq(target, source), target, source, canEvolveSchema,\n        mergeActionResolver, resolveExprsFn, conf)\n    }\n    val resolvedNotMatchedClauses = notMatchedClauses.map {\n      resolveClause(\n        _, Seq(source), target, source, canEvolveSchema,\n        mergeActionResolver, resolveExprsFn, conf)\n    }\n    val resolvedNotMatchedBySourceClauses = notMatchedBySourceClauses.map {\n      resolveClause(\n        _, Seq(target), target, source, canEvolveSchema,\n        mergeActionResolver, resolveExprsFn, conf)\n    }\n\n    val postEvolutionTargetSchema = computePostEvolutionTargetSchema(\n      canEvolveSchema, resolvedMatchedClauses, resolvedNotMatchedClauses,\n      target, source, conf)\n\n    val resolvedMerge = DeltaMergeInto(\n      target,\n      source,\n      resolvedCond,\n      resolvedMatchedClauses,\n      resolvedNotMatchedClauses,\n      resolvedNotMatchedBySourceClauses,\n      withSchemaEvolution = canEvolveSchema,\n      finalSchema = Some(postEvolutionTargetSchema))\n\n    // Its possible that pre-resolved expressions (e.g. `sourceDF(\"key\") = targetDF(\"key\")`) have\n    // attribute references that are not present in the output attributes of the children (i.e.,\n    // incorrect DataFrame was used in the `df(\"col\")` form).\n    if (resolvedMerge.missingInput.nonEmpty) {\n      val missingAttributes = resolvedMerge.missingInput.mkString(\",\")\n      val input = resolvedMerge.inputSet.mkString(\",\")\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_MERGE_RESOLVED_ATTRIBUTE_MISSING_FROM_INPUT\",\n        messageParameters = Array(missingAttributes, input,\n          resolvedMerge.simpleString(SQLConf.get.maxToStringFields)),\n        origin = Some(resolvedMerge.origin)\n      )\n    }\n\n    resolvedMerge\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ResolveDeltaPathTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.{ResolvedTable, UnresolvedRelation, UnresolvedTable}\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits.{CatalogHelper, MultipartIdentifierHelper}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\n\n/**\n * Replaces [[UnresolvedTable]]s if the plan is for direct query on files.\n */\ncase class ResolveDeltaPathTable(sparkSession: SparkSession) extends Rule[LogicalPlan] {\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {\n    case u: UnresolvedTable =>\n      ResolveDeltaPathTable\n        .resolveAsPathTable(sparkSession, u.multipartIdentifier)\n        .getOrElse(u)\n  }\n}\n\nobject ResolveDeltaPathTable {\n\n  /**\n   * Try resolving the input table as a Path table.\n   * If the path table exists, return a [[DataSourceV2Relation]] instance. Otherwise, return None.\n   */\n  def resolveAsPathTableRelation(\n      sparkSession: SparkSession,\n      u: UnresolvedRelation) : Option[DataSourceV2Relation] = {\n    resolveAsPathTable(sparkSession, u.multipartIdentifier)\n      .map { resolvedTable =>\n        DataSourceV2Relation.create(\n          resolvedTable.table, Some(resolvedTable.catalog), Some(resolvedTable.identifier))\n      }\n  }\n\n  /**\n   * Try resolving the input table as a Path table.\n   * If the path table exists, return a [[ResolvedTable]] instance. Otherwise, return None.\n   */\n  private def resolveAsPathTable(\n      sparkSession: SparkSession,\n      multipartIdentifier: Seq[String],\n      options: Map[String, String] = Map.empty): Option[ResolvedTable] = {\n    val sessionState = sparkSession.sessionState\n    if (!sessionState.conf.runSQLonFile || multipartIdentifier.size != 2) {\n      return None\n    }\n    val tableId = multipartIdentifier.asTableIdentifier\n    if (!DeltaTableUtils.isValidPath(tableId)) {\n      return None\n    }\n    val deltaTableV2 = DeltaTableV2(sparkSession, new Path(tableId.table), options = options)\n    val sessionCatalog = sessionState.catalogManager.v2SessionCatalog.asTableCatalog\n    Some(ResolvedTable.create(sessionCatalog, multipartIdentifier.asIdentifier, deltaTableV2))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ResolveDeltaTableWithPartitionFilters.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.files.TahoeLogFileIndex\n\nimport org.apache.spark.sql.catalyst.expressions.And\nimport org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan}\n\n/**\n * Pull out the partition filter that may be part of the FileIndex. This can happen when someone\n * queries a Delta table such as spark.read.format(\"delta\").load(\"/some/table/partition=2\")\n */\nobject ResolveDeltaTableWithPartitionFilters {\n  def unapply(plan: LogicalPlan): Option[LogicalPlan] = plan match {\n    case relation @ DeltaTable(index: TahoeLogFileIndex) if index.partitionFilters.nonEmpty =>\n      val result = Filter(\n        index.partitionFilters.reduce(And),\n        DeltaTableUtils.replaceFileIndex(relation, index.copy(partitionFilters = Nil))\n      )\n      Some(result)\n    case _ => None\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/RowCommitVersion.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping.PARQUET_FIELD_ID_METADATA_KEY\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql.{types, Column, DataFrame}\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, FileSourceGeneratedMetadataStructField}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.execution.datasources.FileFormat\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.types.{DataType, LongType, MetadataBuilder, StructField}\n\nobject RowCommitVersion {\n\n  val METADATA_STRUCT_FIELD_NAME = \"row_commit_version\"\n\n  val QUALIFIED_COLUMN_NAME = s\"${FileFormat.METADATA_NAME}.$METADATA_STRUCT_FIELD_NAME\"\n\n  def createMetadataStructField(\n      protocol: Protocol,\n      metadata: Metadata,\n      nullable: Boolean = false,\n      shouldSetIcebergReservedFieldId: Boolean): Option[StructField] =\n    MaterializedRowCommitVersion.getMaterializedColumnName(protocol, metadata)\n      .map(MetadataStructField(_, nullable, shouldSetIcebergReservedFieldId))\n\n  /**\n   * Add a new column to `dataFrame` that has the name of the materialized Row Commit Version column\n   * and holds Row Commit Versions. The column also is tagged with the appropriate metadata such\n   * that it can be used to write materialized Row Commit Versions.\n   */\n  private[delta] def preserveRowCommitVersions(\n      dataFrame: DataFrame,\n      snapshot: SnapshotDescriptor): DataFrame = {\n    if (!RowTracking.isEnabled(snapshot.protocol, snapshot.metadata)) {\n      return dataFrame\n    }\n\n    val materializedColumnName = MaterializedRowCommitVersion.getMaterializedColumnNameOrThrow(\n      snapshot.protocol, snapshot.metadata, snapshot.deltaLog.unsafeVolatileTableId)\n\n    val rowCommitVersionColumn =\n      DeltaTableUtils.getFileMetadataColumn(dataFrame).getField(METADATA_STRUCT_FIELD_NAME)\n    val shouldSetIcebergReservedFieldId = IcebergCompat.isGeqEnabled(snapshot.metadata, 3)\n\n    preserveRowCommitVersionsUnsafe(\n      dataFrame,\n      materializedColumnName,\n      rowCommitVersionColumn,\n      shouldSetIcebergReservedFieldId\n    )\n  }\n\n  private[delta] def preserveRowCommitVersionsUnsafe(\n      dataFrame: DataFrame,\n      materializedColumnName: String,\n      rowCommitVersionColumn: Column,\n      shouldSetIcebergReservedFieldId: Boolean): DataFrame = {\n    dataFrame\n      .withColumn(materializedColumnName, rowCommitVersionColumn)\n      .withMetadata(\n        materializedColumnName,\n        MetadataStructField.metadata(materializedColumnName, shouldSetIcebergReservedFieldId))\n  }\n\n  object MetadataStructField {\n    private val METADATA_COL_ATTR_KEY = \"__row_commit_version_metadata_col\"\n\n    def apply(\n        materializedColumnName: String,\n        nullable: Boolean = false,\n        shouldSetIcebergReservedFieldId: Boolean): StructField =\n      StructField(\n        METADATA_STRUCT_FIELD_NAME,\n        LongType,\n        // The Row commit version field is used to read the materialized Row commit version value\n        // which is nullable. The actual Row commit version expression is created using a projection\n        // injected before the optimizer pass by the [[GenerateRowIDs] rule at which point the Row\n        // commit version field is non-nullable.\n        nullable,\n        metadata = metadata(materializedColumnName, shouldSetIcebergReservedFieldId))\n\n    def unapply(field: StructField): Option[StructField] =\n      Option.when(isValid(field.dataType, field.metadata))(field)\n\n    def metadata(\n        materializedColumnName: String,\n        shouldSetIcebergReservedFieldId: Boolean): types.Metadata = {\n      val metadataBuilder = new MetadataBuilder()\n        .withMetadata(\n          FileSourceGeneratedMetadataStructField.metadata(\n            METADATA_STRUCT_FIELD_NAME, materializedColumnName))\n        .putBoolean(METADATA_COL_ATTR_KEY, value = true)\n\n      // If IcebergCompatV3 or higher is enabled, assign the field ID of Delta\n      // Row commit version column to match the reserved `_last_updated_sequence_number`\n      // field defined in the Iceberg spec.\n      // This ensures that Iceberg can recognize and track the same column for row lineage purposes.\n      if (shouldSetIcebergReservedFieldId) {\n        metadataBuilder.putLong(\n          PARQUET_FIELD_ID_METADATA_KEY,\n          IcebergConstants.ICEBERG_ROW_TRACKING_LAST_UPDATED_SEQUENCE_NUMBER_FIELD_ID\n        )\n      }\n      metadataBuilder.build()\n    }\n\n    /** Return true if the column is a Row Commit Version column. */\n    def isRowCommitVersionColumn(structField: StructField): Boolean =\n      isValid(structField.dataType, structField.metadata)\n\n    private[delta] def isValid(dataType: DataType, metadata: types.Metadata): Boolean = {\n      FileSourceGeneratedMetadataStructField.isValid(dataType, metadata) &&\n        metadata.contains(METADATA_COL_ATTR_KEY) &&\n        metadata.getBoolean(METADATA_COL_ATTR_KEY)\n    }\n  }\n\n  def columnMetadata(\n      materializedColumnName: String,\n      shouldSetIcebergReservedFieldId: Boolean): types.Metadata =\n    MetadataStructField.metadata(materializedColumnName, shouldSetIcebergReservedFieldId)\n\n  object MetadataAttribute {\n    def apply(\n        materializedColumnName: String,\n        shouldSetIcebergReservedFieldId: Boolean): AttributeReference =\n      DataTypeUtils\n        .toAttribute(\n          MetadataStructField(\n            materializedColumnName,\n            shouldSetIcebergReservedFieldId = shouldSetIcebergReservedFieldId\n          ))\n        .withName(materializedColumnName)\n\n    def unapply(attr: Attribute): Option[Attribute] =\n      if (isRowCommitVersionColumn(attr)) Some(attr) else None\n\n    /** Return true if the column is a Row Commit Version column. */\n    def isRowCommitVersionColumn(attr: Attribute): Boolean =\n      MetadataStructField.isValid(attr.dataType, attr.metadata)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/RowId.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaColumnMapping.PARQUET_FIELD_ID_METADATA_KEY\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, DomainMetadata, Metadata, Protocol}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.propertyKey\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql.{Column, DataFrame, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, FileSourceConstantMetadataStructField, FileSourceGeneratedMetadataStructField}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.execution.datasources.FileFormat\nimport org.apache.spark.sql.types\nimport org.apache.spark.sql.types.{DataType, LongType, MetadataBuilder, StructField}\n\n/**\n * Collection of helpers to handle Row IDs.\n *\n * This file includes the following Row ID features:\n * - Enabling Row IDs using table feature and table property.\n * - Assigning fresh Row IDs.\n * - Reading back Row IDs.\n * - Preserving stable Row IDs.\n */\nobject RowId {\n  /**\n   * Metadata domain for the high water mark stored using a [[DomainMetadata]] action.\n   */\n  case class RowTrackingMetadataDomain(rowIdHighWaterMark: Long)\n      extends JsonMetadataDomain[RowTrackingMetadataDomain] {\n    override val domainName: String = RowTrackingMetadataDomain.domainName\n  }\n\n  object RowTrackingMetadataDomain extends JsonMetadataDomainUtils[RowTrackingMetadataDomain] {\n    override protected val domainName = \"delta.rowTracking\"\n\n    def unapply(action: Action): Option[RowTrackingMetadataDomain] = action match {\n      case d: DomainMetadata if d.domain == domainName => Some(fromJsonConfiguration(d))\n      case _ => None\n    }\n  }\n\n  val MISSING_HIGH_WATER_MARK: Long = -1L\n\n  /**\n   * Returns whether the protocol version supports the Row ID table feature. Whenever Row IDs are\n   * supported, fresh Row IDs must be assigned to all newly committed files, even when Row IDs are\n   * disabled in the current table version.\n   */\n  def isSupported(protocol: Protocol): Boolean = RowTracking.isSupported(protocol)\n\n  /**\n   * Returns whether Row IDs are enabled on this table version. Checks that Row IDs are supported,\n   * which is a pre-requisite for enabling Row IDs, throws an error if not.\n   */\n  def isEnabled(protocol: Protocol, metadata: Metadata): Boolean = {\n    val isEnabled = DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(metadata)\n    if (isEnabled && !isSupported(protocol)) {\n      throw new IllegalStateException(\n        s\"Table property '${DeltaConfigs.ROW_TRACKING_ENABLED.key}' is \" +\n        s\"set on the table but this table version doesn't support table feature \" +\n        s\"'${propertyKey(RowTrackingFeature)}'.\")\n    }\n    isEnabled\n  }\n\n  /**\n   * Assigns fresh row IDs to all AddFiles inside `actions` that do not have row IDs yet and emits\n   * a [[RowIdHighWaterMark]] action with the new high-water mark.\n   */\n  private[delta] def assignFreshRowIds(\n      spark: SparkSession,\n      protocol: Protocol,\n      snapshot: Snapshot,\n      actions: Iterator[Action],\n      operation: DeltaOperations.Operation): Iterator[Action] = {\n    if (!isSupported(protocol)) return actions\n\n    def metadataDomainSetException = new IllegalStateException(\n      \"Manually setting the Row ID high water mark is not allowed\")\n\n    // Do not propagate row IDs if generation is suspended.\n    if (RowTracking.isSuspended(spark, snapshot.metadata)) {\n      return actions.map {\n        case a: AddFile if a.baseRowId.isDefined => a.copy(baseRowId = None)\n        case d: DomainMetadata if RowTrackingMetadataDomain.isSameDomain(d) =>\n          throw metadataDomainSetException\n        case o => o\n      }\n    }\n\n    val oldHighWatermark = extractHighWatermark(snapshot).getOrElse(MISSING_HIGH_WATER_MARK)\n\n    var newHighWatermark = oldHighWatermark\n\n    val actionsWithFreshRowIds = actions.map {\n      case a: AddFile if a.baseRowId.isEmpty =>\n        val baseRowId = newHighWatermark + 1L\n        newHighWatermark += a.numPhysicalRecords.getOrElse {\n          operation match {\n            case op: DeltaOperations.Clone =>\n              throw DeltaErrors.cloneWithRowTrackingWithoutStats()\n            case _ =>\n              throw DeltaErrors.rowIdAssignmentWithoutStats\n          }\n        }\n        a.copy(baseRowId = Some(baseRowId))\n      case d: DomainMetadata if RowTrackingMetadataDomain.isSameDomain(d) =>\n        throw metadataDomainSetException\n      case other => other\n    }\n\n    val newHighWatermarkAction: Iterator[Action] = new Iterator[Action] {\n      // Iterators are lazy, so the first call to `hasNext` won't happen until after we\n      // exhaust the remapped actions iterator. At that point, the watermark (changed or not)\n      // decides whether the iterator is empty or infinite; take(1) below to bound it.\n      override def hasNext: Boolean = newHighWatermark != oldHighWatermark\n      override def next(): Action = RowTrackingMetadataDomain(newHighWatermark).toDomainMetadata\n    }\n    actionsWithFreshRowIds ++ newHighWatermarkAction.take(1)\n  }\n\n  /**\n   * Extracts the high watermark of row IDs from a snapshot.\n   */\n  private[delta] def extractHighWatermark(snapshot: Snapshot): Option[Long] =\n    if (isSupported(snapshot.protocol)) {\n      RowTrackingMetadataDomain.fromSnapshot(snapshot).map(_.rowIdHighWaterMark)\n    } else {\n      None\n    }\n\n  /** Base Row ID column name */\n  val BASE_ROW_ID = \"base_row_id\"\n\n  /*\n   * A specialization of [[FileSourceConstantMetadataStructField]] used to represent base RowId\n   * columns.\n   */\n  object BaseRowIdMetadataStructField {\n    private val BASE_ROW_ID_METADATA_COL_ATTR_KEY = s\"__base_row_id_metadata_col\"\n\n    def metadata: types.Metadata = new MetadataBuilder()\n      .withMetadata(FileSourceConstantMetadataStructField.metadata(BASE_ROW_ID))\n      .putBoolean(BASE_ROW_ID_METADATA_COL_ATTR_KEY, value = true)\n      .build()\n\n    def apply(nullable: Boolean): StructField =\n      StructField(\n        BASE_ROW_ID,\n        LongType,\n        nullable,\n        metadata = metadata)\n\n    def unapply(field: StructField): Option[StructField] =\n      Some(field).filter(isBaseRowIdColumn)\n\n    /** Return true if the column is a base Row ID column. */\n    def isBaseRowIdColumn(structField: StructField): Boolean =\n      isValid(structField.dataType, structField.metadata)\n\n    def isValid(dataType: DataType, metadata: types.Metadata): Boolean = {\n      FileSourceConstantMetadataStructField.isValid(dataType, metadata) &&\n        metadata.contains(BASE_ROW_ID_METADATA_COL_ATTR_KEY) &&\n        metadata.getBoolean(BASE_ROW_ID_METADATA_COL_ATTR_KEY)\n    }\n  }\n\n  /**\n   * The field readers can use to access the base row id column.\n   */\n  def createBaseRowIdField(\n      protocol: Protocol, metadata: Metadata, nullable: Boolean): Option[StructField] =\n    Option.when(RowId.isEnabled(protocol, metadata)) {\n      BaseRowIdMetadataStructField(nullable)\n    }\n\n  /** Row ID column name */\n  val ROW_ID = \"row_id\"\n\n  val QUALIFIED_COLUMN_NAME = s\"${FileFormat.METADATA_NAME}.${ROW_ID}\"\n\n  /** Column metadata to be used in conjunction [[QUALIFIED_COLUMN_NAME]] to mark row id columns */\n  def columnMetadata(\n      materializedColumnName: String,\n      shouldSetIcebergReservedFieldId: Boolean): types.Metadata =\n    RowIdMetadataStructField.metadata(materializedColumnName, shouldSetIcebergReservedFieldId)\n\n  /**\n   * The field readers can use to access the generated row id column. The scanner's internal column\n   * name is obtained from the table's metadata.\n   */\n  def createRowIdField(\n    protocol: Protocol,\n    metadata: Metadata,\n    nullable: Boolean,\n    shouldSetIcebergReservedFieldId: Boolean): Option[StructField] =\n    MaterializedRowId.getMaterializedColumnName(protocol, metadata)\n      .map(RowIdMetadataStructField(_, nullable, shouldSetIcebergReservedFieldId))\n\n  /*\n   * A specialization of [[FileSourceGeneratedMetadataStructField]] used to represent RowId columns.\n   *\n   * - Row ID columns can be read by adding '_metadata.row_id' to the read schema\n   * - To write to the materialized Row ID column\n   *     - use the materialized Row ID column name which can be obtained using\n   *       [[getMaterializedColumnName]]\n   *     - add [[COLUMN_METADATA]] which is part of [[RowId]] as metadata to the column\n   *     - nulls are replaced with fresh Row IDs\n   */\n  object RowIdMetadataStructField {\n\n    val ROW_ID_METADATA_COL_ATTR_KEY = \"__row_id_metadata_col\"\n\n    def metadata(\n        materializedColumnName: String,\n        shouldSetIcebergReservedFieldId: Boolean): types.Metadata = {\n      val metadataBuilder = new MetadataBuilder()\n        .withMetadata(\n          FileSourceGeneratedMetadataStructField.metadata(RowId.ROW_ID, materializedColumnName))\n        .putBoolean(ROW_ID_METADATA_COL_ATTR_KEY, value = true)\n\n      // If IcebergCompatV3 or higher is enabled, assign the field ID of Delta row id column\n      // to match the reserved `_row_id` field defined in the Iceberg spec.\n      // This ensures that Iceberg can recognize and track the same column for row lineage purposes.\n      if (shouldSetIcebergReservedFieldId) {\n        metadataBuilder.putLong(\n          PARQUET_FIELD_ID_METADATA_KEY,\n          IcebergConstants.ICEBERG_ROW_TRACKING_ROW_ID_FIELD_ID\n        )\n      }\n      metadataBuilder.build()\n    }\n\n    def apply(\n         materializedColumnName: String,\n         nullable: Boolean = false,\n         shouldSetIcebergReservedFieldId: Boolean): StructField =\n      StructField(\n        RowId.ROW_ID,\n        LongType,\n        // The Row ID field is used to read the materialized Row ID value which is nullable. The\n        // actual Row ID expression is created using a projection injected before the optimizer pass\n        // by the [[GenerateRowIDs] rule at which point the Row ID field is non-nullable.\n        nullable,\n        metadata = metadata(materializedColumnName, shouldSetIcebergReservedFieldId))\n\n    def unapply(field: StructField): Option[StructField] =\n      if (isRowIdColumn(field)) Some(field) else None\n\n    /** Return true if the column is a Row Id column. */\n    def isRowIdColumn(structField: StructField): Boolean =\n      isValid(structField.dataType, structField.metadata)\n\n    def isValid(dataType: DataType, metadata: types.Metadata): Boolean = {\n      FileSourceGeneratedMetadataStructField.isValid(dataType, metadata) &&\n        metadata.contains(ROW_ID_METADATA_COL_ATTR_KEY) &&\n        metadata.getBoolean(ROW_ID_METADATA_COL_ATTR_KEY)\n    }\n  }\n\n  object RowIdMetadataAttribute {\n    /** Creates an attribute for writing out the materialized column name */\n    def apply(\n        materializedColumnName: String,\n        shouldSetIcebergReservedFieldId: Boolean): AttributeReference =\n      DataTypeUtils\n        .toAttribute(\n          RowIdMetadataStructField(\n            materializedColumnName,\n            shouldSetIcebergReservedFieldId = shouldSetIcebergReservedFieldId))\n        .withName(materializedColumnName)\n\n    def unapply(attr: Attribute): Option[Attribute] =\n      if (isRowIdColumn(attr)) Some(attr) else None\n\n    /** Return true if the column is a Row Id column. */\n    def isRowIdColumn(attr: Attribute): Boolean =\n      RowIdMetadataStructField.isValid(attr.dataType, attr.metadata)\n  }\n\n  /**\n   * Throw if row tracking is supported and columns in the write schema tagged as materialized row\n   * IDs do not reference the materialized row id column name.\n   */\n  private[delta] def throwIfMaterializedRowIdColumnNameIsInvalid(\n      data: DataFrame, metadata: Metadata, protocol: Protocol, tableId: String): Unit = {\n    if (!RowTracking.isEnabled(protocol, metadata)) {\n      return\n    }\n\n    val materializedColumnName =\n      metadata.configuration.get(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP)\n\n    if (materializedColumnName.isEmpty) {\n      // If row tracking is enabled, a missing materialized column name is a bug and we need to\n      // throw an error. If row tracking is only supported, we should just return, as it's fine\n      // for the materialized column to not be assigned.\n      if (RowTracking.isEnabled(protocol, metadata)) {\n        throw DeltaErrors.materializedRowIdMetadataMissing(tableId)\n      }\n      return\n    }\n\n    toAttributes(data.schema).foreach {\n      case RowIdMetadataAttribute(attribute) =>\n        if (attribute.name != materializedColumnName.get) {\n          throw new UnsupportedOperationException(\"Materialized Row IDs column name \" +\n            s\"${attribute.name} is invalid. Must be ${materializedColumnName.get}.\")\n        }\n      case _ =>\n    }\n  }\n\n  /**\n   * Add a new column to 'dataFrame' that has the name of the materialized Row ID column and holds\n   * Row IDs. The column also is tagged with the appropriate metadata such that it can be used to\n   * write materialized Row IDs.\n   */\n  private[delta] def preserveRowIds(\n      dataFrame: DataFrame,\n      snapshot: SnapshotDescriptor): DataFrame = {\n    if (!isEnabled(snapshot.protocol, snapshot.metadata)) {\n      return dataFrame\n    }\n\n    val materializedColumnName = MaterializedRowId.getMaterializedColumnNameOrThrow(\n      snapshot.protocol, snapshot.metadata, snapshot.deltaLog.unsafeVolatileTableId)\n\n    val rowIdColumn = DeltaTableUtils.getFileMetadataColumn(dataFrame).getField(ROW_ID)\n    val shouldSetIcebergReservedFieldId = IcebergCompat.isGeqEnabled(snapshot.metadata, 3)\n\n    preserveRowIdsUnsafe(\n      dataFrame, materializedColumnName, rowIdColumn, shouldSetIcebergReservedFieldId)\n  }\n\n  /**\n   * Add a new column to 'dataFrame' that has 'materializedColumnName' and holds Row IDs. The column\n   * is also tagged with the appropriate metadata so it can be used to write materialized Row IDs.\n   *\n   * Internal method, exposed only for testing.\n   */\n  private[delta] def preserveRowIdsUnsafe(\n      dataFrame: DataFrame,\n      materializedColumnName: String,\n      rowIdColumn: Column,\n      shouldSetIcebergReservedFieldId: Boolean): DataFrame = {\n    dataFrame\n      .withColumn(materializedColumnName, rowIdColumn)\n      .withMetadata(\n        materializedColumnName,\n        columnMetadata(materializedColumnName, shouldSetIcebergReservedFieldId))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/RowTracking.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaCommitTag.PreservedRowTrackingTag\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.actions.CommitInfo\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\n\nimport org.apache.spark.sql.{DataFrame, SparkSession}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.StructField\n\n/**\n * Utility functions for Row Tracking that are shared between Row IDs and Row Commit Versions.\n */\nobject RowTracking {\n  /**\n   * Returns whether the protocol version supports the Row Tracking table feature. Whenever Row\n   * Tracking is support, fresh Row IDs and Row Commit Versions must be assigned to all newly\n   * committed files, even when Row IDs are disabled in the current table version.\n   */\n  def isSupported(protocol: Protocol): Boolean = protocol.isFeatureSupported(RowTrackingFeature)\n\n  /**\n   * Returns whether Row Tracking is enabled on this table version. Checks that Row Tracking is\n   * supported, which is a pre-requisite for enabling Row Tracking, throws an error if not.\n   */\n  def isEnabled(protocol: Protocol, metadata: Metadata): Boolean = {\n    val isEnabled = DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(metadata)\n    if (isEnabled && !isSupported(protocol)) {\n      throw new IllegalStateException(\n        s\"Table property '${DeltaConfigs.ROW_TRACKING_ENABLED.key}' is \" +\n          s\"set on the table but this table version doesn't support table feature \" +\n          s\"'${TableFeatureProtocolUtils.propertyKey(RowTrackingFeature)}'.\")\n    }\n    isEnabled\n  }\n\n  def isSuspended(spark: SparkSession, metadata: Metadata): Boolean = {\n    val ignoreIsSuspended = spark.conf.get(DeltaSQLConf.DELTA_ROW_TRACKING_IGNORE_SUSPENSION)\n    if (DeltaUtils.isTesting && ignoreIsSuspended) return false\n\n    val isEnabled = DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(metadata)\n    val isSuspended = DeltaConfigs.ROW_TRACKING_SUSPENDED.fromMetaData(metadata)\n    // Make sure a third party client did not miss ROW_TRACKING_SUSPENDED table property.\n    if (isEnabled && isSuspended) {\n      throw DeltaErrors.rowTrackingIllegalPropertyCombination()\n    }\n    isSuspended\n  }\n\n  /**\n   * Checks whether CONVERT TO DELTA collects statistics if row tracking is supported. If it does\n   * not collect statistics, we cannot assign fresh row IDs, hence we throw an error to either rerun\n   * the command without enabling the row tracking table feature, or to enable the necessary\n   * flags to collect statistics.\n   */\n  private[delta] def checkStatsCollectedIfRowTrackingSupported(\n      protocol: Protocol,\n      convertToDeltaShouldCollectStats: Boolean,\n      statsCollectionEnabled: Boolean): Unit = {\n    if (!isSupported(protocol)) return\n    if (!convertToDeltaShouldCollectStats || !statsCollectionEnabled) {\n      throw DeltaErrors.convertToDeltaRowTrackingEnabledWithoutStatsCollection\n    }\n  }\n\n  /**\n   * @return the Row Tracking metadata fields for the file's _metadata\n   *         when Row Tracking is enabled.\n   */\n  def createMetadataStructFields(\n      protocol: Protocol,\n      metadata: Metadata,\n      nullableConstantFields: Boolean,\n      nullableGeneratedFields: Boolean): Iterable[StructField] = {\n    val shouldSetIcebergReservedFieldId = false\n    RowId.createRowIdField(\n      protocol, metadata, nullableGeneratedFields, shouldSetIcebergReservedFieldId) ++\n      RowId.createBaseRowIdField(protocol, metadata, nullableConstantFields) ++\n      DefaultRowCommitVersion.createDefaultRowCommitVersionField(\n        protocol, metadata, nullableConstantFields) ++\n      RowCommitVersion.createMetadataStructField(\n        protocol, metadata, nullableGeneratedFields, shouldSetIcebergReservedFieldId)\n  }\n\n  /**\n   * @param preserved The value of [[DeltaCommitTag.PreservedRowTrackingTag.key]] tag\n   * @return A copy of ``tagsMap`` with the [[DeltaCommitTag.PreservedRowTrackingTag.key]] tag added\n   *         or replaced with the new value.\n   */\n  private def addPreservedRowTrackingTag(\n      tagsMap: Map[String, String],\n      preserved: Boolean = true): Map[String, String] = {\n    tagsMap + (DeltaCommitTag.PreservedRowTrackingTag.key -> preserved.toString)\n  }\n\n  /**\n   * Sets the [[DeltaCommitTag.PreservedRowTrackingTag.key]] tag to true if not set. We add the tag\n   * to every operation because we assume all operations preserve row tracking by default. The\n   * absence of the tag means that row tracking is not preserved.\n   * Operations can set the tag to mark row tracking as preserved/not preserved.\n   */\n  private[delta] def addPreservedRowTrackingTagIfNotSet(\n      snapshot: SnapshotDescriptor,\n      tagsMap: Map[String, String] = Map.empty): Map[String, String] = {\n    if (!isEnabled(snapshot.protocol, snapshot.metadata) ||\n      tagsMap.contains(PreservedRowTrackingTag.key)) {\n      return tagsMap\n    }\n    addPreservedRowTrackingTag(tagsMap)\n  }\n\n  /**\n   * Returns a copy of the CommitInfo passed in with the PreservedRowTrackingTag tag set to false.\n   */\n  private[delta] def addRowTrackingNotPreservedTag(commitInfo: CommitInfo): CommitInfo = {\n    val tagsMap = commitInfo.tags.getOrElse(Map.empty[String, String])\n    val newCommitInfoTags = addPreservedRowTrackingTag(tagsMap, preserved = false)\n    commitInfo.copy(tags = Some(newCommitInfoTags))\n  }\n\n  /**\n   * Checks whether the CommitInfo has the RowTrackingEnablementOnly tag set to true.\n   * If omitted, we assume it is false.\n   */\n  private def isRowTrackingEnablementOnlyCommit(commitInfo: Option[CommitInfo]): Boolean = {\n    DeltaCommitTag\n        .getTagValueFromCommitInfo(commitInfo, DeltaCommitTag.RowTrackingEnablementOnlyTag.key)\n        .exists(_.toBoolean)\n  }\n\n  /**\n   * Returns a Boolean indicating whether it is safe the resolve the metadata update conflict\n   * between the current and winning transaction conflict, from the perspective of row tracking\n   * enablement.\n   */\n  def canResolveMetadataUpdateConflict(\n      currentTransactionInfo: CurrentTransactionInfo,\n      winningCommitSummary: WinningCommitSummary): Boolean = {\n    if (!isSupported(currentTransactionInfo.protocol)) return false\n\n    RowTracking.isRowTrackingEnablementOnlyCommit(winningCommitSummary.commitInfo) &&\n      !currentTransactionInfo.metadataChanged\n  }\n\n  /**\n   * Update the currentTransactionInfo properly to resolve a metadata update conflict when the\n   * winning commit is tagged as RowTrackingEnablementOnly. It is only safe to call this function if\n   * [[RowTracking.canResolveMetadataUpdateConflict]] returns true.\n   *\n   * See [[ConflictCheckerEdge.checkNoMetadataUpdates()]] for more details.\n   */\n  def resolveRowTrackingEnablementOnlyMetadataUpdateConflict(\n      currentTransactionInfo: CurrentTransactionInfo,\n      winningCommitSummary: WinningCommitSummary): CurrentTransactionInfo = {\n    require(canResolveMetadataUpdateConflict(currentTransactionInfo, winningCommitSummary))\n    // If the CommitInfo is None, do nothing because the absence of the\n    // [[DeltaCommitTag.PreservedRowTrackingTag]] means it is false.\n    val newCommitInfo =\n        currentTransactionInfo.commitInfo.map(RowTracking.addRowTrackingNotPreservedTag)\n\n    val newMetadata = winningCommitSummary.metadataUpdates.head\n\n    // OptimisticTransactions sets the metadata seen by the current txn\n    // (currentTransactionInfo.metadata) to the updated metadata. To be consistent, let's do this\n    // even if the current txn does not update metadata.\n    currentTransactionInfo.copy(\n      metadata = newMetadata,\n      commitInfo = newCommitInfo\n    )\n  }\n\n  def preserveRowTrackingColumns(\n      dfWithoutRowTrackingColumns: DataFrame,\n      snapshot: SnapshotDescriptor): DataFrame = {\n    val dfWithRowIds = RowId.preserveRowIds(dfWithoutRowTrackingColumns, snapshot)\n    RowCommitVersion.preserveRowCommitVersions(dfWithRowIds, snapshot)\n  }\n  /**\n   * Verifies that the [[RowTrackingFeature]] is enabled and all files have base row IDs in the\n   * given snapshot. These invariants need to hold to enable the RowTracking table property.\n   */\n  def verifyInvariantsForTablePropertyEnablement(snapshot: Snapshot): Unit = {\n    if (!snapshot.protocol.isFeatureSupported(RowTrackingFeature)) {\n      throw new ProtocolChangedException(None)\n    }\n    val filesRequiringBackfill = snapshot.allFiles.where(col(\"baseRowId\").isNull)\n    if (!filesRequiringBackfill.isEmpty) {\n      throw new ProtocolChangedException(None)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/Snapshot.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.{Locale, TimeZone}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.actions.Action.logSchema\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CommitCoordinatorClient, CommitCoordinatorProvider, CoordinatedCommitsUsageLogs, CoordinatedCommitsUtils, TableCommitCoordinatorClient}\nimport org.apache.spark.sql.delta.expressions.EncodeNestedVariantAsZ85String\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DataSkippingReader\nimport org.apache.spark.sql.delta.stats.DataSkippingReaderConf\nimport org.apache.spark.sql.delta.stats.DeltaStatsColumnSpec\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\nimport org.apache.spark.sql.delta.util.DeltaCommitFileProvider\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.delta.util.StateCache\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport io.delta.storage.commit.CommitCoordinatorClient\nimport org.apache.hadoop.fs.{FileStatus, Path}\nimport org.apache.parquet.format.converter.ParquetMetadataConverter.NO_FILTER\nimport org.apache.parquet.hadoop.Footer\nimport org.apache.parquet.hadoop.ParquetFileReader\n\nimport org.apache.spark.internal.{MDC, MessageWithContext}\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.execution.datasources.parquet.{ParquetFileFormat, ParquetToSparkSchemaConverter}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\n\n/**\n * A description of a Delta [[Snapshot]], including basic information such its [[DeltaLog]]\n * metadata, protocol, and version.\n */\ntrait SnapshotDescriptor {\n  def deltaLog: DeltaLog\n  def version: Long\n  def metadata: Metadata\n  def protocol: Protocol\n\n  def schema: StructType = metadata.schema\n\n  protected[delta] def numOfFilesIfKnown: Option[Long]\n  protected[delta] def sizeInBytesIfKnown: Option[Long]\n\n  /** Whether the table has [[CatalogOwnedTableFeature]] enabled */\n  def isCatalogOwned: Boolean = {\n    version >= 0 &&\n      protocol.readerAndWriterFeatureNames.contains(CatalogOwnedTableFeature.name)\n  }\n}\n\n/**\n * An immutable snapshot of the state of the log at some delta version. Internally\n * this class manages the replay of actions stored in checkpoint or delta files.\n *\n * After resolving any new actions, it caches the result and collects the\n * following basic information to the driver:\n *  - Protocol Version\n *  - Metadata\n *  - Transaction state\n *\n * @param inCommitTimestampOpt The in-commit-timestamp of the latest commit in milliseconds. Can\n *                  be set to None if\n *                   1. The timestamp has not been read yet - generally the case for cold tables.\n *                   2. Or the table has not been initialized, i.e. `version = -1`.\n *                   3. Or the table does not have [[InCommitTimestampTableFeature]] enabled.\n *\n */\nclass Snapshot(\n    val path: Path,\n    override val version: Long,\n    val logSegment: LogSegment,\n    override val deltaLog: DeltaLog,\n    val checksumOpt: Option[VersionChecksum]\n  )\n  extends SnapshotDescriptor\n  with SnapshotStateManager\n  with StateCache\n  with StatisticsCollection\n  with DataSkippingReader\n  with ValidateChecksum\n  with DeltaLogging {\n\n  import Snapshot._\n  import DeltaLogFileIndex.COMMIT_VERSION_COLUMN\n  // For implicits which re-use Encoder:\n  import org.apache.spark.sql.delta.implicits._\n\n  protected def spark = SparkSession.active\n\n  /** Snapshot to scan by the DeltaScanGenerator for metadata query optimizations */\n  override val snapshotToScan: Snapshot = this\n\n  override def columnMappingMode: DeltaColumnMappingMode = metadata.columnMappingMode\n\n  /**\n   * Returns the timestamp of the latest commit of this snapshot.\n   * For an uninitialized snapshot, this returns -1.\n   *\n   * When InCommitTimestampTableFeature is enabled, the timestamp\n   * is retrieved from the CommitInfo of the latest commit which\n   * can result in an IO operation.\n   */\n  def timestamp: Long =\n    getInCommitTimestampOpt.getOrElse(logSegment.lastCommitFileModificationTimestamp)\n\n  /**\n   * Returns the inCommitTimestamp if ICT is enabled, otherwise returns None.\n   * This potentially triggers an IO operation to read the inCommitTimestamp.\n   * This is a lazy val, so repeated calls will not trigger multiple IO operations.\n   */\n  protected lazy val getInCommitTimestampOpt: Option[Long] =\n    Option.when(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(metadata)) {\n      _reconstructedProtocolMetadataAndICT.inCommitTimestamp\n        .getOrElse {\n          val startTime = System.currentTimeMillis()\n          var exception = Option.empty[Throwable]\n          try {\n            val commitInfoOpt = DeltaHistoryManager.getCommitInfoOpt(\n              deltaLog.store,\n              DeltaCommitFileProvider(this).deltaFile(version),\n              deltaLog.newDeltaHadoopConf())\n            CommitInfo.getRequiredInCommitTimestamp(commitInfoOpt, version.toString)\n          } catch {\n            case e: Throwable =>\n              exception = Some(e)\n              throw e\n          } finally {\n            recordDeltaEvent(\n              deltaLog,\n              \"delta.inCommitTimestamp.read\",\n              data = Map(\n                \"version\" -> version,\n                \"callSite\" -> \"Snapshot.getInCommitTimestampOpt\",\n                \"checkpointVersion\" -> logSegment.checkpointProvider.version,\n                \"durationMs\" -> (System.currentTimeMillis() - startTime),\n                \"exceptionMessage\" -> exception.map(_.getMessage).getOrElse(\"\"),\n                \"exceptionStackTrace\" ->\n                  exception.map(_.getStackTrace.mkString(\"\\n\")).getOrElse(\"\"),\n                \"isCRCPresent\" -> checksumOpt.isDefined\n              )\n            )\n          }\n        }\n    }\n\n\n  private[delta] lazy val nonFileActions: Seq[Action] = {\n    Seq(protocol, metadata) ++\n      setTransactions ++\n      domainMetadata\n  }\n\n  @volatile private[delta] var stateReconstructionTriggered = false\n\n  /**\n   * The last known backfilled version of this snapshot. This can be larger than the last\n   * backfilled file in the snapshot's LogSegment so is separately tracked in this mutable\n   * variable. The reason why this is needed is as follows:\n   *\n   * In general, we update a snapshot's LogSegment after a commit by appending the latest\n   * commit file. This can be an unbackfilled commit. The next time we call update(), we\n   * check, if we can reuse the post commit snapshot or if we need to create a new snapshot.\n   * The update performs a listing and creates a new LogSegment and the criteria for\n   * keeping or replacing the old snapshot is whether the old snapshot's LogSegment is equal\n   * to the LogSegment created by the update() call (see getSnapshotForLogSegment).\n   *\n   * If an unbackfilled commit has been backfilled before update() is called, the new LogSegment\n   * would contain the backfilled version of this commit and so the old and new LogSegments are\n   * determined to be different and the snapshot is swapped. However, the snapshots are in fact\n   * identical and so swapping the snapshot is not necessary and wold only lead to a loss of the\n   * cached state of the old snapshot.\n   *\n   * To prevent this, we don't swap the snapshot in this case (see\n   * LogSegment.lastMatchingBackfilledCommitIsEqual). This means that we'll continue to use\n   * the old LogSegment, which contains the unbackfilled commit(s). To correctly keep track of\n   * the fact that all commits in the LogSegment have indeed been backfilled, we keep the\n   * last known backfilled version of the snapshot in this variable and update it each time\n   * during LogSegment comparison. This allows callers to figure out whether this snapshot\n   * indeed contains any unbackfilled commits or the LogSegment is just based on an older\n   * version.\n   */\n  @volatile private var lastKnownBackfilledVersion: Long =\n    logSegment.lastBackfilledVersionInSegment\n\n  def getLastKnownBackfilledVersion: Long = lastKnownBackfilledVersion\n\n  def updateLastKnownBackfilledVersion(newVersion: Long): Unit = {\n    if (newVersion > this.version) {\n      throw new IllegalStateException(\"Can't update the last known backfilled version \" +\n        \"to a version greater than the snapshot's version.\")\n    }\n    lastKnownBackfilledVersion = math.max(lastKnownBackfilledVersion, newVersion)\n  }\n\n  /**\n   * Helper method to determine, whether this snapshot contains \"actual\" unbackfilled\n   * commits. See [[Snapshot.lastKnownBackfilledVersion]] for more details on why a\n   * LogSegment may contain unbackfilled commits, even though these files have already\n   * been backfilled.\n   */\n  private[delta] def allCommitsBackfilled: Boolean = {\n    lastKnownBackfilledVersion >= FileNames.getFileVersion(logSegment.deltas.last) &&\n      // This should always be true because we synchronously backfill during checkpoint\n      // creation and always create a new snapshot after that, which will force the\n      // latest LogSegment to be used.\n      lastKnownBackfilledVersion >= logSegment.checkpointProvider.version\n  }\n\n  /**\n   * Use [[stateReconstruction]] to create a representation of the actions in this table.\n   * Cache the resultant output.\n   */\n  private lazy val cachedState = recordFrameProfile(\"Delta\", \"snapshot.cachedState\") {\n    stateReconstructionTriggered = true\n    cacheDS(stateReconstruction, s\"Delta Table State #$version - $redactedPath\")\n  }\n\n  /**\n   * Given the list of files from `LogSegment`, create respective file indices to help create\n   * a DataFrame and short-circuit the many file existence and partition schema inference checks\n   * that exist in DataSource.resolveRelation().\n   */\n  protected[delta] lazy val deltaFileIndexOpt: Option[DeltaLogFileIndex] = {\n    assertLogFilesBelongToTable(path, logSegment.deltas)\n    DeltaLogFileIndex(DeltaLogFileIndex.COMMIT_FILE_FORMAT, logSegment.deltas)\n  }\n\n  protected lazy val fileIndices: Seq[DeltaLogFileIndex] = {\n    val checkpointFileIndexes = checkpointProvider.allActionsFileIndexes()\n    checkpointFileIndexes ++ deltaFileIndexOpt.toSeq\n  }\n\n  /**\n   * Protocol, Metadata, and In-Commit Timestamp retrieved through\n   * `protocolMetadataAndICTReconstruction` which skips a full state reconstruction.\n   */\n  case class ReconstructedProtocolMetadataAndICT(\n      protocol: Protocol,\n      metadata: Metadata,\n      inCommitTimestamp: Option[Long])\n\n  /**\n   * Generate the protocol and metadata for this snapshot. This is usually cheaper than a\n   * full state reconstruction, but still only compute it when necessary.\n   */\n  private lazy val _reconstructedProtocolMetadataAndICT: ReconstructedProtocolMetadataAndICT =\n      {\n    // Should be small. At most 'checkpointInterval' rows, unless new commits are coming\n    // in before a checkpoint can be written\n    var protocol: Protocol = null\n    var metadata: Metadata = null\n    var inCommitTimestamp: Option[Long] = None\n    protocolMetadataAndICTReconstruction().foreach {\n      case ReconstructedProtocolMetadataAndICT(p: Protocol, _, _) => protocol = p\n      case ReconstructedProtocolMetadataAndICT(_, m: Metadata, _) => metadata = m\n      case ReconstructedProtocolMetadataAndICT(_, _, ict: Option[Long]) => inCommitTimestamp = ict\n    }\n\n    if (protocol == null) {\n      recordDeltaEvent(\n        deltaLog,\n        opType = \"delta.assertions.missingAction\",\n        data = Map(\n          \"version\" -> version.toString, \"action\" -> \"Protocol\", \"source\" -> \"Snapshot\"))\n      throw DeltaErrors.actionNotFoundException(\"protocol\", version)\n    }\n\n    if (metadata == null) {\n      recordDeltaEvent(\n        deltaLog,\n        opType = \"delta.assertions.missingAction\",\n        data = Map(\n          \"version\" -> version.toString, \"action\" -> \"Metadata\", \"source\" -> \"Snapshot\"))\n      throw DeltaErrors.actionNotFoundException(\"metadata\", version)\n    }\n\n    ReconstructedProtocolMetadataAndICT(protocol, metadata, inCommitTimestamp)\n  }\n\n  /**\n   * [[CommitCoordinatorClient]] for the given delta table as of this snapshot.\n   * - This should not be None when a coordinator has been configured for this table. However, if\n   *   the configured coordinator implementation has not been registered, this will be None. In such\n   *   cases, the user will see potentially stale reads for the table. For strict enforcement of\n   *   coordinated commits, the user can set the configuration\n   *   [[DeltaSQLConf.COORDINATED_COMMITS_IGNORE_MISSING_COORDINATOR_IMPLEMENTATION]] to false.\n   * - This must be None when coordinated commits is disabled.\n   */\n  val tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient] = {\n    val failIfImplUnavailable =\n      !spark.conf.get(DeltaSQLConf.COORDINATED_COMMITS_IGNORE_MISSING_COORDINATOR_IMPLEMENTATION)\n    CoordinatedCommitsUtils.getTableCommitCoordinator(\n      spark,\n      deltaLog,\n      this,\n      failIfImplUnavailable\n    )\n  }\n\n  /**\n   * Returns the [[TableCommitCoordinatorClient]] that should be used for any type of mutation\n   * operation on the table. This includes, data writes, backfills etc.\n   * This method will throw an error if the configured coordinator could not be instantiated.\n   * @return [[TableCommitCoordinatorClient]] if the table is configured for coordinated commits,\n   *         None if the table is not configured for coordinated commits.\n   */\n  def getTableCommitCoordinatorForWrites: Option[TableCommitCoordinatorClient] = {\n    val coordinatorOpt = tableCommitCoordinatorClientOpt\n      val coordinatorName =\n        DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.fromMetaData(metadata)\n      if (coordinatorName.isDefined && coordinatorOpt.isEmpty) {\n        recordDeltaEvent(\n          deltaLog,\n          CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_MISSING_IMPLEMENTATION_WRITE,\n          data = Map(\n            \"commitCoordinatorName\" -> coordinatorName.get,\n            \"registeredCommitCoordinators\" ->\n              CommitCoordinatorProvider.getRegisteredCoordinatorNames.mkString(\", \"),\n            \"readVersion\" -> version.toString\n          )\n        )\n        throw DeltaErrors.unsupportedWritesWithMissingCoordinators(coordinatorName.get)\n      }\n      coordinatorOpt\n  }\n\n  /** Number of columns to collect stats on for data skipping */\n  override lazy val statsColumnSpec: DeltaStatsColumnSpec =\n    StatisticsCollection.configuredDeltaStatsColumnSpec(metadata)\n\n  /** Performs validations during initialization */\n  protected def init(): Unit = {\n    deltaLog.protocolRead(protocol)\n    deltaLog.assertTableFeaturesMatchMetadata(protocol, metadata)\n    SchemaUtils.recordUndefinedTypes(deltaLog, metadata.schema)\n  }\n\n  /** The current set of actions in this [[Snapshot]] as plain Rows */\n  def stateDF: DataFrame = recordFrameProfile(\"Delta\", \"stateDF\") {\n    cachedState.getDF\n  }\n\n  /** The current set of actions in this [[Snapshot]] as a typed Dataset. */\n  def stateDS: Dataset[SingleAction] = recordFrameProfile(\"Delta\", \"stateDS\") {\n    cachedState.getDS\n  }\n\n  private[delta] def allFilesViaStateReconstruction: Dataset[AddFile] = {\n    stateDS.where(\"add IS NOT NULL\").select(col(\"add\").as[AddFile])\n  }\n\n  // Here we need to bypass the ACL checks for SELECT anonymous function permissions.\n  /** All of the files present in this [[Snapshot]]. */\n  def allFiles: Dataset[AddFile] = allFilesViaStateReconstruction\n\n  /** All unexpired tombstones. */\n  def tombstones: Dataset[RemoveFile] = {\n    // Temporary workarround for SPARK-51356.\n    stateDS.where(\"remove IS NOT NULL\").map(_.remove)\n  }\n\n  def deltaFileSizeInBytes(): Long = deltaFileIndexOpt.map(_.sizeInBytes).getOrElse(0L)\n\n  def checkpointSizeInBytes(): Long = checkpointProvider.effectiveCheckpointSizeInBytes()\n\n  override def metadata: Metadata = _reconstructedProtocolMetadataAndICT.metadata\n\n  override def protocol: Protocol = _reconstructedProtocolMetadataAndICT.protocol\n\n  /**\n   * Tries to retrieve the protocol, metadata, and in-commit-timestamp (if needed) from the\n   * checksum file. If the checksum file is not present or if the protocol or metadata is missing\n   * this will return None.\n   */\n  protected def getProtocolMetadataAndIctFromCrc(checksumOpt: Option[VersionChecksum]):\n    Option[Array[ReconstructedProtocolMetadataAndICT]] = {\n      if (!spark.sessionState.conf.getConf(\n          DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED)) {\n        return None\n      }\n      checksumOpt.map(c => (c.protocol, c.metadata, c.inCommitTimestampOpt)).flatMap {\n        case (p: Protocol, m: Metadata, ict: Option[Long]) =>\n          Some(Array((p, null, None), (null, m, None), (null, null, ict))\n            .map(ReconstructedProtocolMetadataAndICT.tupled))\n\n        case (p, m, _) if p != null || m != null =>\n          // One was missing from the .crc file... warn and fall back to an optimized query\n          val protocolStr = Option(p).map(_.toString).getOrElse(\"null\")\n          val metadataStr = Option(m).map(_.toString).getOrElse(\"null\")\n          recordDeltaEvent(\n            deltaLog,\n            opType = \"delta.assertions.missingEitherProtocolOrMetadataFromChecksum\",\n            data = Map(\n              \"version\" -> version.toString, \"protocol\" -> protocolStr, \"source\" -> metadataStr))\n          logWarning(log\"Either protocol or metadata is null from checksum; \" +\n            log\"version:${MDC(DeltaLogKeys.VERSION, version)} \" +\n            log\"protocol:${MDC(DeltaLogKeys.PROTOCOL, protocolStr)} \" +\n            log\"metadata:${MDC(DeltaLogKeys.DELTA_METADATA, metadataStr)}\")\n          None\n\n        case _ => None // both missing... fall back to an optimized query\n      }\n  }\n\n  /**\n   * Pulls the protocol and metadata of the table from the files that are used to compute the\n   * Snapshot directly--without triggering a full state reconstruction. This is important, because\n   * state reconstruction depends on protocol and metadata for correctness.\n   * If the current table version does not have a checkpoint, this function will also return the\n   * in-commit-timestamp of the latest commit if available.\n   *\n   * Also this method should only access methods defined in [[UninitializedCheckpointProvider]]\n   * which are not present in [[CheckpointProvider]]. This is because initialization of\n   * [[Snapshot.checkpointProvider]] depends on [[Snapshot.protocolMetadataAndICTReconstruction()]]\n   * and so if [[Snapshot.protocolMetadataAndICTReconstruction()]] starts depending on\n   * [[Snapshot.checkpointProvider]] then there will be cyclic dependency.\n   */\n  protected def protocolMetadataAndICTReconstruction():\n      Array[ReconstructedProtocolMetadataAndICT] = {\n    import implicits._\n\n    getProtocolMetadataAndIctFromCrc(checksumOpt).foreach { protocolMetadataAndIctFromCrc =>\n      return protocolMetadataAndIctFromCrc\n    }\n\n    val schemaToUse = Action.logSchema(Set(\"protocol\", \"metaData\", \"commitInfo\"))\n    val checkpointOpt = checkpointProvider.topLevelFileIndex.map { index =>\n      deltaLog.loadIndex(index, schemaToUse)\n        .withColumn(COMMIT_VERSION_COLUMN, lit(checkpointProvider.version))\n    }\n    (checkpointOpt ++ deltaFileIndexOpt.map(deltaLog.loadIndex(_, schemaToUse)).toSeq)\n      .reduceOption(_.union(_)).getOrElse(emptyDF)\n      .select(\"protocol\", \"metaData\", \"commitInfo.inCommitTimestamp\", COMMIT_VERSION_COLUMN)\n      .where(\"protocol.minReaderVersion is not null or metaData.id is not null \" +\n        s\"or (commitInfo.inCommitTimestamp is not null and version = $version)\")\n      .as[(Protocol, Metadata, Option[Long], Long)]\n      .collect()\n      .sortBy(_._4)\n      .map {\n        case (p, m, ict, _) => ReconstructedProtocolMetadataAndICT(p, m, ict)\n      }\n  }\n\n  // Reconstruct the state by applying deltas in order to the checkpoint.\n  // We partition by path as it is likely the bulk of the data is add/remove.\n  // Non-path based actions will be collocated to a single partition.\n  protected def stateReconstruction: Dataset[SingleAction] = {\n    recordFrameProfile(\"Delta\", \"snapshot.stateReconstruction\") {\n      // for serializability\n      val localMinFileRetentionTimestamp = minFileRetentionTimestamp\n      val localMinSetTransactionRetentionTimestamp = minSetTransactionRetentionTimestamp\n\n      val canonicalPath = deltaLog.getCanonicalPathUdf()\n\n      // Canonicalize the paths so we can repartition the actions correctly, but only rewrite the\n      // add/remove actions themselves after partitioning and sorting are complete. Otherwise, the\n      // optimizer can generate a really bad plan that re-evaluates _EVERY_ field of the rewritten\n      // struct(...)  projection every time we touch _ANY_ field of the rewritten struct.\n      //\n      // NOTE: We sort by [[COMMIT_VERSION_COLUMN]] (provided by [[loadActions]]), to ensure that\n      // actions are presented to InMemoryLogReplay in the ascending version order it expects.\n      val ADD_PATH_CANONICAL_COL_NAME = \"add_path_canonical\"\n      val REMOVE_PATH_CANONICAL_COL_NAME = \"remove_path_canonical\"\n      loadActions\n        .withColumn(ADD_PATH_CANONICAL_COL_NAME, when(\n          col(\"add.path\").isNotNull, canonicalPath(col(\"add.path\"))))\n        .withColumn(REMOVE_PATH_CANONICAL_COL_NAME, when(\n          col(\"remove.path\").isNotNull, canonicalPath(col(\"remove.path\"))))\n        .repartition(\n          getNumPartitions,\n          coalesce(col(ADD_PATH_CANONICAL_COL_NAME), col(REMOVE_PATH_CANONICAL_COL_NAME)))\n        .sortWithinPartitions(COMMIT_VERSION_COLUMN)\n        .withColumn(\"add\", when(\n          col(\"add.path\").isNotNull,\n          struct(\n            col(ADD_PATH_CANONICAL_COL_NAME).as(\"path\"),\n            col(\"add.partitionValues\"),\n            col(\"add.size\"),\n            col(\"add.modificationTime\"),\n            col(\"add.dataChange\"),\n            col(ADD_STATS_TO_USE_COL_NAME).as(\"stats\"),\n            col(\"add.tags\"),\n            col(\"add.deletionVector\"),\n            col(\"add.baseRowId\"),\n            col(\"add.defaultRowCommitVersion\"),\n            col(\"add.clusteringProvider\")\n          )))\n        .withColumn(\"remove\", when(\n          col(\"remove.path\").isNotNull,\n          col(\"remove\").withField(\"path\", col(REMOVE_PATH_CANONICAL_COL_NAME))))\n        .as[SingleAction]\n        .mapPartitions { iter =>\n          val state: LogReplay =\n            new InMemoryLogReplay(\n              Some(localMinFileRetentionTimestamp),\n              localMinSetTransactionRetentionTimestamp)\n          state.append(0, iter.map(_.unwrap))\n          state.checkpoint.map(_.wrap)\n        }\n    }\n  }\n\n  /**\n   * Loads the file indices into a DataFrame that can be used for LogReplay.\n   *\n   * In addition to the usual nested columns provided by the SingleAction schema, it should provide\n   * two additional columns to simplify the log replay process: [[COMMIT_VERSION_COLUMN]] (which,\n   * when sorted in ascending order, will order older actions before newer ones, as required by\n   * [[InMemoryLogReplay]]); and [[ADD_STATS_TO_USE_COL_NAME]] (to handle certain combinations of\n   * config settings for delta.checkpoint.writeStatsAsJson and delta.checkpoint.writeStatsAsStruct).\n   * When we see a V2 checkpoint without the old stats column, but the stats_parsed column, we\n   * json encode the stats_parsed column back as \"stats\" again. This is a temporary correctness\n   * hack.\n   */\n  protected def loadActions: DataFrame = {\n    if (fileIndices.isEmpty) return emptyDF\n\n    // Augment the schema with a NullType add.stats_parsed column, as a place-holder for\n    // compatibility with the checkpoint parquet. Both deltas and checkpoints generally use this\n    // schema. HOWEVER, IF (and only if) a checkpoint actually exists, AND it provides an\n    // add.stats_parsed column AND it lacks an add.stats column, THEN (and only then) the checkpoint\n    // DF includes the actual add.stats_parsed column -- not a NullType placeholder -- from which we\n    // generate the add_stats_to_use column (add.stats is unused in that case). Meanwhile, JSON\n    // deltas always map add.stats to add_stats_to_use, and always use the placeholder.\n    val logSchemaToUse = Action.logSchema\n    val jsonStatsCol = col(\"add.stats\")\n    val deltas = deltaFileIndexOpt.map(deltaLog.loadIndex(_, logSchemaToUse))\n      .map(_.withColumn(ADD_STATS_TO_USE_COL_NAME, jsonStatsCol))\n\n    val checkpointDataframes = checkpointProvider\n      .allActionsFileIndexesAndSchemas(spark, deltaLog)\n      .map { case (index, schema) =>\n        val addSchema = schema(\"add\").dataType.asInstanceOf[StructType]\n        val (checkpointSchemaToUse, checkpointStatsColToUse) =\n          if (addSchema.exists(_.name == \"stats_parsed\") && !addSchema.exists(_.name == \"stats\")) {\n            val statsParsedSchema = addSchema(\"stats_parsed\").dataType.asInstanceOf[StructType]\n            val checkpointSchemaToUse =\n              Action.logSchemaWithAddStatsParsed(addSchema(\"stats_parsed\"))\n            val statsCol = col(\"add.stats_parsed\")\n            // Only use EncodeNestedVariantAsZ85String if the schema contains VariantType.\n            // This avoids performance overhead for tables without variant columns.\n            val encodedStatsCol =\n              if (SchemaUtils.checkForVariantTypeColumnsRecursively(statsParsedSchema)) {\n                Column(EncodeNestedVariantAsZ85String(statsCol.expr))\n              } else {\n                statsCol\n              }\n            (\n              checkpointSchemaToUse,\n              to_json(encodedStatsCol)\n            )\n          } else {\n            // Normal (JSON-like) schema suffices\n            (logSchemaToUse, jsonStatsCol)\n          }\n\n        // For schema compat, make sure to discard add.stats_parsed (if present)\n        deltaLog.loadIndex(index, checkpointSchemaToUse)\n          .withColumn(COMMIT_VERSION_COLUMN, lit(checkpointProvider.version))\n          .withColumn(ADD_STATS_TO_USE_COL_NAME, checkpointStatsColToUse)\n          .withColumn(\"add\", col(\"add\").dropFields(\"stats_parsed\"))\n      }\n      (checkpointDataframes ++ deltas).reduce(_.union(_))\n  }\n\n  /**\n   * Tombstones before the [[minFileRetentionTimestamp]] timestamp will be dropped from the\n   * checkpoint.\n   */\n  private[delta] def minFileRetentionTimestamp: Long = {\n    deltaLog.clock.getTimeMillis() - DeltaLog.tombstoneRetentionMillis(metadata)\n  }\n\n  /**\n   * [[SetTransaction]]s before [[minSetTransactionRetentionTimestamp]] will be considered expired\n   * and dropped from the snapshot.\n   */\n  private[delta] def minSetTransactionRetentionTimestamp: Option[Long] = {\n    DeltaLog.minSetTransactionRetentionInterval(metadata).map(deltaLog.clock.getTimeMillis() - _)\n  }\n\n  private[delta] def getNumPartitions: Int = {\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SNAPSHOT_PARTITIONS)\n      .getOrElse(Snapshot.defaultNumSnapshotPartitions)\n  }\n\n  /**\n   * Computes all the information that is needed by the checksum for the current snapshot.\n   * May kick off state reconstruction if needed by any of the underlying fields.\n   * Note that it's safe to set txnId to none, since the snapshot doesn't always have a txn\n   * attached. E.g. if a snapshot is created by reading a checkpoint, then no txnId is present.\n   */\n  def computeChecksum: VersionChecksum = VersionChecksum(\n    txnId = None,\n    inCommitTimestampOpt = getInCommitTimestampOpt,\n    metadata = metadata,\n    protocol = protocol,\n    allFiles = checksumOpt.flatMap(_.allFiles),\n    tableSizeBytes = checksumOpt.map(_.tableSizeBytes).getOrElse(sizeInBytes),\n    numFiles = checksumOpt.map(_.numFiles).getOrElse(numOfFiles),\n    numMetadata = checksumOpt.map(_.numMetadata).getOrElse(numOfMetadata),\n    numProtocol = checksumOpt.map(_.numProtocol).getOrElse(numOfProtocol),\n    // Only return setTransactions and domainMetadata if they are either already present\n    // in the checksum or if they have already been computed in the current snapshot.\n    setTransactions = checksumOpt.flatMap(_.setTransactions)\n      .orElse {\n        Option.when(_computedStateTriggered &&\n            // Only extract it from the current snapshot if set transaction\n            // writes are enabled.\n            spark.conf.get(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC)) {\n          setTransactions\n        }\n      },\n    domainMetadata = checksumOpt.flatMap(_.domainMetadata)\n      .orElse(Option.when(_computedStateTriggered)(domainMetadata)),\n    numDeletedRecordsOpt = checksumOpt.flatMap(_.numDeletedRecordsOpt)\n      .orElse(Option.when(_computedStateTriggered)(numDeletedRecordsOpt).flatten)\n      .filter(_ => deletionVectorsReadableAndMetricsEnabled),\n    numDeletionVectorsOpt = checksumOpt.flatMap(_.numDeletionVectorsOpt)\n      .orElse(Option.when(_computedStateTriggered)(numDeletionVectorsOpt).flatten)\n      .filter(_ => deletionVectorsReadableAndMetricsEnabled),\n    deletedRecordCountsHistogramOpt = checksumOpt.flatMap(_.deletedRecordCountsHistogramOpt)\n      .orElse(Option.when(_computedStateTriggered)(deletedRecordCountsHistogramOpt).flatten)\n      .filter(_ => deletionVectorsReadableAndHistogramEnabled),\n    histogramOpt = checksumOpt.flatMap(_.histogramOpt)\n  )\n\n  /** Returns the data schema of the table, used for reading stats */\n  def tableSchema: StructType = metadata.dataSchema\n\n  def outputTableStatsSchema: StructType = metadata.dataSchema\n\n  def outputAttributeSchema: StructType = metadata.dataSchema\n\n  /** Returns the schema of the columns written out to file (overridden in write path) */\n  def dataSchema: StructType = metadata.dataSchema\n\n  /** Return the set of properties of the table. */\n  def getProperties: mutable.Map[String, String] = {\n    Snapshot.getProperties(metadata, protocol)\n  }\n\n  /** The [[CheckpointProvider]] for the underlying checkpoint */\n  lazy val checkpointProvider: CheckpointProvider = logSegment.checkpointProvider match {\n    case cp: CheckpointProvider => cp\n    case uninitializedProvider: UninitializedCheckpointProvider =>\n      CheckpointProvider(spark, this, checksumOpt, uninitializedProvider)\n    case o => throw new IllegalStateException(s\"Unknown checkpoint provider: ${o.getClass.getName}\")\n  }\n\n  def redactedPath: String =\n    Utils.redact(spark.sessionState.conf.stringRedactionPattern, path.toUri.toString)\n\n  /**\n   * Ensures that commit files are backfilled up to the current version in the snapshot.\n   *\n   * This method checks if there are any un-backfilled versions up to the current version and\n   * triggers the backfilling process using the commit-coordinator. It verifies that the delta file\n   * for the current version exists after the backfilling process.\n   *\n   * @throws IllegalStateException\n   *   if the delta file for the current version is not found after backfilling.\n   */\n  def ensureCommitFilesBackfilled(catalogTableOpt: Option[CatalogTable]): Unit = {\n    val tableCommitCoordinatorClientOpt = if (isCatalogOwned) {\n      CatalogOwnedTableUtils.populateTableCommitCoordinatorFromCatalog(spark, catalogTableOpt, this)\n    } else {\n      getTableCommitCoordinatorForWrites\n    }\n    val tableCommitCoordinatorClient = tableCommitCoordinatorClientOpt.getOrElse {\n      return\n    }\n    val minUnbackfilledVersion = DeltaCommitFileProvider(this).minUnbackfilledVersion\n    if (minUnbackfilledVersion <= version) {\n      val hadoopConf = deltaLog.newDeltaHadoopConf()\n      tableCommitCoordinatorClient.backfillToVersion(\n        catalogTableOpt.map(_.identifier),\n        version,\n        lastKnownBackfilledVersion = Some(minUnbackfilledVersion - 1))\n      val fs = deltaLog.logPath.getFileSystem(hadoopConf)\n      val expectedBackfilledDeltaFile = FileNames.unsafeDeltaFile(deltaLog.logPath, version)\n      if (!fs.exists(expectedBackfilledDeltaFile)) {\n        throw new IllegalStateException(\"Backfilling of commit files failed. \" +\n          s\"Expected delta file $expectedBackfilledDeltaFile not found.\")\n      }\n    }\n  }\n\n\n  protected def emptyDF: DataFrame =\n    spark.createDataFrame(spark.sparkContext.emptyRDD[Row], logSchema)\n\n\n  def logInfo(msg: MessageWithContext): Unit = {\n    val tableId = deltaLog.unsafeVolatileTableId\n    super.logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" + msg)\n  }\n\n  def logWarning(msg: MessageWithContext): Unit = {\n    val tableId = deltaLog.unsafeVolatileTableId\n    super.logWarning(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" + msg)\n  }\n\n  def logWarning(msg: MessageWithContext, throwable: Throwable): Unit = {\n    val tableId = deltaLog.unsafeVolatileTableId\n    super.logWarning(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" + msg, throwable)\n  }\n\n  def logError(msg: MessageWithContext): Unit = {\n    val tableId = deltaLog.unsafeVolatileTableId\n    super.logError(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" + msg)\n  }\n\n  def logError(msg: MessageWithContext, throwable: Throwable): Unit = {\n    val tableId = deltaLog.unsafeVolatileTableId\n    super.logError(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" + msg, throwable)\n  }\n\n  override def toString: String =\n    s\"${getClass.getSimpleName}(path=$path, version=$version, metadata=$metadata, \" +\n      s\"logSegment=$logSegment, checksumOpt=$checksumOpt)\"\n\n  logInfo(log\"Created snapshot ${MDC(DeltaLogKeys.SNAPSHOT, this)}\")\n  init()\n}\n\nobject Snapshot extends DeltaLogging {\n\n  // Used by [[loadActions]] and [[stateReconstruction]]\n  val ADD_STATS_TO_USE_COL_NAME = \"add_stats_to_use\"\n\n  private val defaultNumSnapshotPartitions: Int = 50\n\n  /** Verifies that a set of delta or checkpoint files to be read actually belongs to this table. */\n  private def assertLogFilesBelongToTable(logBasePath: Path, files: Seq[FileStatus]): Unit = {\n    val logPath = new Path(logBasePath.toUri)\n    val commitDirPath = FileNames.commitDirPath(logPath)\n    files.map(_.getPath).foreach { filePath =>\n      val commitParent = new Path(filePath.toUri).getParent\n      if (commitParent != logPath && commitParent != commitDirPath) {\n        // scalastyle:off throwerror\n        throw new AssertionError(s\"File ($filePath) doesn't belong in the \" +\n          s\"transaction log at $logBasePath.\")\n        // scalastyle:on throwerror\n      }\n    }\n  }\n\n  /** Whether to write allFiles in [[VersionChecksum.allFiles]] */\n  private[delta] def allFilesInCrcWritePathEnabled(\n      spark: SparkSession,\n      snapshot: Snapshot): Boolean = {\n    // disable if config is off.\n    if (!spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_ENABLED)) return false\n\n    // Also disable if all stats (structs/json) are disabled in checkpoints.\n    // When checkpoint stats are disabled (both in terms of structs/json), then the\n    // snapshot.allFiles from state reconstruction may/may not have stats (files coming from\n    // checkpoint won't have stats and files coming from deltas will have stats).\n    // But CRC.allFiles will have stats as VersionChecksum.allFiles is created\n    // incrementally using each commit. To prevent this inconsistency, we disable the feature when\n    // both json/struct stats are disabled for checkpoint.\n    if (!Checkpoints.shouldWriteStatsAsJson(snapshot) &&\n      !Checkpoints.shouldWriteStatsAsStruct(spark.sessionState.conf, snapshot)) {\n      return false\n    }\n\n    // Disable if table is configured to collect stats on more than the default number of columns\n    // to avoid bloating the .crc file.\n    val numIndexedColsThreshold = spark.sessionState.conf\n      .getConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_THRESHOLD_INDEXED_COLS)\n      .getOrElse(DataSkippingReaderConf.DATA_SKIPPING_NUM_INDEXED_COLS_DEFAULT_VALUE)\n    val configuredNumIndexCols =\n      DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.fromMetaData(snapshot.metadata)\n    if (configuredNumIndexCols > numIndexedColsThreshold) return false\n\n    true\n  }\n\n  /**\n   * If true, force a verification of [[VersionChecksum.allFiles]] irrespective of the value of\n   * DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED flag (if they're written).\n   */\n  private[delta] def allFilesInCrcVerificationForceEnabled(\n      spark: SparkSession): Boolean = {\n    val forceVerificationForNonUTCEnabled = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED)\n    if (!forceVerificationForNonUTCEnabled) return false\n\n    // This is necessary because timestamps for older dates (pre-1883) are not correctly serialized\n    // in non-UTC timezones due to unusual historical offsets (e.g. -07:52:58 for LA).\n    // These serialization discrepancies can lead to spurious CRC verification failures.\n    // By forcing verification of all files in non-UTC environments, we can continue to detect and\n    // work towards fixing this issues.\n    // Note: Display Name for UTC is Etc/UTC, so we check for UTC substring in the timezone.\n    val sparkSessionTimeZone = spark.sessionState.conf.sessionLocalTimeZone\n    val defaultJVMTimeZone = TimeZone.getDefault.getID\n    val systemTimeZone = System.getProperty(\"user.timezone\", \"Etc/UTC\")\n\n    val isNonUtcTimeZone = List(sparkSessionTimeZone, defaultJVMTimeZone, systemTimeZone)\n      .exists(!_.toLowerCase(Locale.ROOT).contains(\"utc\"))\n\n    isNonUtcTimeZone\n  }\n\n  /**\n   * If true, do verification of [[VersionChecksum.allFiles]] computed by incremental commit CRC\n   * by doing state-reconstruction.\n   */\n  private[delta] def allFilesInCrcVerificationEnabled(\n      spark: SparkSession,\n      snapshot: Snapshot): Boolean = {\n    val verificationConfEnabled = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED)\n    val shouldVerify = verificationConfEnabled || allFilesInCrcVerificationForceEnabled(spark)\n    allFilesInCrcWritePathEnabled(spark, snapshot) && shouldVerify\n  }\n\n  /**\n   * Don't include [[AddFile]]s in CRC if this commit is modifying the schema of table in some\n   * way. This is to make sure we don't carry any DROPPED column from previous CRC to this CRC\n   * forever and can start fresh from next commit.\n   * If the oldSnapshot itself is missing, we don't incrementally compute the checksum.\n   */\n  private[delta] def shouldIncludeAddFilesInCrc(\n      spark: SparkSession, snapshot: Snapshot, metadata: Metadata): Boolean = {\n    allFilesInCrcWritePathEnabled(spark, snapshot) &&\n      (snapshot.version == -1 || snapshot.metadata.schema == metadata.schema)\n  }\n\n  /**\n   * Return the set of properties for a given metadata and protocol.\n   */\n  def getProperties(metadata: Metadata, protocol: Protocol): mutable.Map[String, String] = {\n    val base = new mutable.LinkedHashMap[String, String]()\n    metadata.configuration.foreach { case (k, v) =>\n      if (k != \"path\") {\n        base.put(k, v)\n      }\n    }\n    base.put(Protocol.MIN_READER_VERSION_PROP, protocol.minReaderVersion.toString)\n    base.put(Protocol.MIN_WRITER_VERSION_PROP, protocol.minWriterVersion.toString)\n    if (protocol.supportsReaderFeatures || protocol.supportsWriterFeatures) {\n      val features = protocol.readerAndWriterFeatureNames.map(name =>\n        s\"${TableFeatureProtocolUtils.FEATURE_PROP_PREFIX}$name\" ->\n          TableFeatureProtocolUtils.FEATURE_PROP_SUPPORTED)\n      base ++ features.toSeq.sorted\n    } else {\n      base\n    }\n  }\n\n  /**\n   * Gets the schema of a single parquet file by reading its footer. Code here is copied from\n   * ParquetFileFormat.\n   */\n  private[delta] def getParquetFileSchemaAndRowCount(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      file: FileStatus): (StructType, Long) = {\n    // Converter used to convert Parquet `MessageType` to Spark SQL `StructType`\n    val converter = new ParquetToSparkSchemaConverter(\n      assumeBinaryIsString = spark.sessionState.conf.isParquetBinaryAsString,\n      assumeInt96IsTimestamp = spark.sessionState.conf.isParquetINT96AsTimestamp)\n\n    val conf = deltaLog.newDeltaHadoopConf()\n\n    val parquetMetadata = {\n      ParquetFileReader.readFooter(deltaLog.newDeltaHadoopConf(), file.getPath)\n    }\n    val rowCount = parquetMetadata.getBlocks.asScala.map(_.getRowCount).sum\n\n    val footer = new Footer(file.getPath(), parquetMetadata)\n    (ParquetFileFormat.readSchemaFromFooter(footer, converter), rowCount)\n  }\n}\n\n/**\n * A dummy snapshot with only metadata and protocol specified. It is used for a targeted table\n * version that does not exist yet before commiting a change. This can be used to create a\n * DataFrame, or to derive the stats schema from an existing Parquet table when converting it to\n * Delta or cloning it to a Delta table prior to the actual snapshot being available after a commit.\n *\n * Note that the snapshot state reconstruction contains only the protocol and metadata - it does not\n * include add/remove actions, appids, or metadata domains, even if the actual table currently has\n * or will have them in the future.\n *\n * @param logPath the path to transaction log\n * @param deltaLog the delta log object\n * @param metadata the metadata of the table\n * @param protocolOpt the protocol version of the table (optional). If not specified, a default\n *                    protocol will be computed based on the metadata. This must be explicitly\n *                    specified when replacing an existing Delta table, otherwise using the metadata\n *                    to compute the protocol might result in a protocol downgrade for the table.\n */\nclass DummySnapshot(\n    val logPath: Path,\n    override val deltaLog: DeltaLog,\n    override val metadata: Metadata,\n    protocolOpt: Option[Protocol] = None)\n  extends Snapshot(\n    path = logPath,\n    version = -1,\n    logSegment = LogSegment.empty(logPath),\n    deltaLog = deltaLog,\n    checksumOpt = None\n  ) {\n\n  def this(logPath: Path, deltaLog: DeltaLog) = this(\n    logPath,\n    deltaLog,\n    Metadata(\n      configuration = DeltaConfigs.mergeGlobalConfigs(\n        sqlConfs = SparkSession.active.sessionState.conf,\n        tableConf = Map.empty,\n        ignoreProtocolConfsOpt = Some(\n          DeltaConfigs.ignoreProtocolDefaultsIsSet(\n            sqlConfs = SparkSession.active.sessionState.conf,\n            tableConf = deltaLog.allOptions))),\n      createdTime = Some(System.currentTimeMillis())))\n\n  override def stateDS: Dataset[SingleAction] = emptyDF.as[SingleAction]\n  override def stateDF: DataFrame = emptyDF\n  override def protocol: Protocol =\n    protocolOpt.getOrElse(Protocol.forNewTable(spark, Some(metadata)))\n\n  override protected lazy val computedState: SnapshotState = initialState(metadata, protocol)\n  override protected lazy val getInCommitTimestampOpt: Option[Long] = None\n  _computedStateTriggered = true\n\n  // The [[InitialSnapshot]] is not backed by any external commit-coordinator.\n  override val tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient] = None\n\n  // Commit 0 cannot be performed through a commit coordinator.\n  override def getTableCommitCoordinatorForWrites: Option[TableCommitCoordinatorClient] = None\n\n  override def timestamp: Long = -1L\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/SnapshotManagement.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.FileNotFoundException\nimport java.util.Objects\nimport java.util.concurrent.{CompletableFuture, Future}\nimport java.util.concurrent.locks.ReentrantLock\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.duration._\nimport scala.util.control.NonFatal\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.TagDefinitions.TAG_ASYNC\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CoordinatedCommitsUsageLogs, CoordinatedCommitsUtils, TableCommitCoordinatorClient}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.FileNames._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.delta.util.threads.DeltaThreadPool\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport io.delta.storage.commit.{Commit, GetCommitsResponse}\nimport org.apache.hadoop.fs.{BlockLocation, FileStatus, LocatedFileStatus, Path}\n\nimport org.apache.spark.{SparkContext, SparkException}\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.util.ThreadUtils\n\n/**\n * Wraps the most recently updated snapshot along with the timestamp the update was started.\n * Defined outside the class since it's used in tests.\n */\ncase class CapturedSnapshot(snapshot: Snapshot, updateTimestamp: Long)\n\n\n/**\n * Manages the creation, computation, and access of Snapshot's for Delta tables. Responsibilities\n * include:\n *  - Figuring out the set of files that are required to compute a specific version of a table\n *  - Updating and exposing the latest snapshot of the Delta table in a thread-safe manner\n */\ntrait SnapshotManagement { self: DeltaLog =>\n  import SnapshotManagement.verifyDeltaVersions\n\n  @volatile private[delta] var asyncUpdateTask: Future[Unit] = _\n\n  /** Use ReentrantLock to allow us to call `lockInterruptibly` */\n  protected val snapshotLock = new ReentrantLock()\n\n  /**\n   * Cached fileStatus for the latest CRC file seen in the deltaLog.\n   */\n  @volatile protected var lastSeenChecksumFileStatusOpt: Option[FileStatus] = None\n  /**\n   * Cached latest snapshot. This is initialized in `createSnapshotAtInit`\n   */\n  @volatile protected var currentSnapshot: CapturedSnapshot = _\n  /**\n   * Run `body` inside `snapshotLock` lock using `lockInterruptibly` so that the thread\n   * can be interrupted when waiting for the lock.\n   */\n  def withSnapshotLockInterruptibly[T](body: => T): T = {\n    snapshotLock.lockInterruptibly()\n    try {\n      body\n    } finally {\n      snapshotLock.unlock()\n    }\n  }\n\n  /** Get an iterator of files in the _delta_log directory starting with the startVersion. */\n  private[delta] def listFrom(startVersion: Long): Iterator[FileStatus] = {\n    store.listFrom(listingPrefix(logPath, startVersion), newDeltaHadoopConf())\n  }\n\n  /** Returns true if the path is delta log files. Delta log files can be delta commit file\n   * (e.g., 000000000.json), or checkpoint file. (e.g., 000000001.checkpoint.00001.00003.parquet)\n   * @param path Path of a file\n   * @return Boolean Whether the file is delta log files\n   */\n  protected def isDeltaCommitOrCheckpointFile(path: Path): Boolean = {\n    isCheckpointFile(path) || isDeltaFile(path)\n  }\n\n  /**\n   * @return A tuple where the first element is an array of log files (possibly empty, if no\n   *         usable log files are found), and the second element is the latest checksum file found\n   *         which has a version less than or equal to `versionToLoad`.\n   */\n  private[delta] def listFromFileSystemInternal(\n      startVersion: Long,\n      versionToLoad: Option[Long],\n      includeMinorCompactions: Boolean\n  ): (Option[Array[(FileStatus, FileType.Value, Long)]], Option[FileStatus]) = {\n    var latestAvailableChecksumFileStatus = Option.empty[FileStatus]\n    // LIST the directory, starting from the provided lower bound (treat missing dir as empty).\n    // NOTE: \"empty/missing\" is _NOT_ equivalent to \"contains no useful commit files.\"\n    val filesOpt = try {\n      Some(listFrom(startVersion)).filterNot(_.isEmpty)\n    } catch {\n      case _: FileNotFoundException => None\n    }\n    val files =\n      filesOpt.map {\n      _.flatMap {\n        case DeltaFile(f, fileVersion) =>\n          Some((f, FileType.DELTA, fileVersion))\n        case CompactedDeltaFile(f, startVersion, endVersion)\n            if includeMinorCompactions && versionToLoad.forall(endVersion <= _) =>\n          Some((f, FileType.COMPACTED_DELTA, startVersion))\n        case CheckpointFile(f, fileVersion) if f.getLen > 0 =>\n          Some((f, FileType.CHECKPOINT, fileVersion))\n        case ChecksumFile(f, version) if versionToLoad.forall(version <= _) =>\n          latestAvailableChecksumFileStatus = Some(f)\n          None\n        case _ =>\n          None\n      }\n      // take files up to the version we want to load\n      .takeWhile { case (_, _, fileVersion) => versionToLoad.forall(fileVersion <= _) }\n      .toArray\n    }\n    (files, latestAvailableChecksumFileStatus)\n  }\n\n  /**\n   * This method is designed to efficiently and reliably list delta, compacted delta, and\n   * checkpoint files associated with a Delta Lake table. It makes parallel calls to both the\n   * file-system and a commit-coordinator (if available), reconciles the results to account for\n   * asynchronous backfill operations, and ensures a comprehensive list of file statuses without\n   * missing any concurrently backfilled files.\n   * *Note*: If table is a coordinated-commits table, the commit coordinator MUST be passed to\n   * correctly list the commits.\n   * The function also collects the latest checksum file found in the listings and returns it.\n   *\n   * @param startVersion the version to start. Inclusive.\n   * @param tableCommitCoordinatorClientOpt the optional commit coordinator to use for fetching\n   *        un-backfilled commits.\n   * @param catalogTableOpt the optional catalog table to pass to the commit coordinator client.\n   * @param versionToLoad the optional parameter to set the max version we should return. Inclusive.\n   * @param includeMinorCompactions Whether to include minor compaction files in the result\n   * @return A tuple where the first element is an array of log files (possibly empty, if no\n   *         usable log files are found), and the second element is the latest checksum file found\n   *         which has a version less than or equal to `versionToLoad`.\n   */\n  protected def listDeltaCompactedDeltaCheckpointFilesAndLatestChecksumFile(\n      startVersion: Long,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      versionToLoad: Option[Long],\n      includeMinorCompactions: Boolean): (Option[Array[FileStatus]], Option[FileStatus]) = {\n    val tableCommitCoordinatorClient = tableCommitCoordinatorClientOpt.getOrElse {\n      val (filesOpt, checksumOpt) =\n        listFromFileSystemInternal(\n          startVersion,\n          versionToLoad,\n          includeMinorCompactions\n        )\n      return (filesOpt.map(_.map(_._1)), checksumOpt)\n    }\n\n    // Submit a potential async call to get commits from commit coordinator if available\n    val threadPool = SnapshotManagement.commitCoordinatorGetCommitsThreadPool\n    def getCommitsTask(isAsyncRequest: Boolean): GetCommitsResponse = {\n      CoordinatedCommitsUtils.getCommitsFromCommitCoordinatorWithUsageLogs(\n        this, tableCommitCoordinatorClient, catalogTableOpt,\n        startVersion, versionToLoad, isAsyncRequest)\n    }\n    def getGetCommitsResponseFuture(): Future[GetCommitsResponse] = {\n      if (threadPool.getActiveCount < threadPool.getMaximumPoolSize) {\n        threadPool.submit[GetCommitsResponse](spark) { getCommitsTask(isAsyncRequest = true) }\n      } else {\n        // If the thread pool is full, we should not submit more tasks to it. Instead, we should\n        // run the task in the current thread.\n        logInfo(log\"Getting un-backfilled commits from commit coordinator in the same \" +\n          log\"thread for table ${MDC(DeltaLogKeys.PATH, dataPath)}\")\n        recordDeltaEvent(\n          this, CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_LISTING_THREADPOOL_FULL)\n        CompletableFuture.completedFuture(getCommitsTask(isAsyncRequest = false))\n      }\n    }\n    val unbackfilledCommitsResponseFuture = getGetCommitsResponseFuture\n\n    var maxDeltaVersionSeen = startVersion - 1\n    val (initialLogTuplesFromFsListingOpt, initialChecksumOpt) =\n      listFromFileSystemInternal(\n        startVersion,\n        versionToLoad,\n        includeMinorCompactions\n      )\n    // Ideally listFromFileSystemInternal should return lexicographically sorted files and so\n    // maxDeltaVersionSeen should be equal to the last delta version. But we are being\n    // defensive here and taking max of all the delta fileVersions seen.\n    initialLogTuplesFromFsListingOpt.foreach { logTuples =>\n      logTuples.filter(_._2 == FileType.DELTA).map(_._3).foreach { deltaVersion =>\n        maxDeltaVersionSeen = Math.max(maxDeltaVersionSeen, deltaVersion)\n      }\n    }\n    val unbackfilledCommitsResponse = try {\n      unbackfilledCommitsResponseFuture.get()\n    } catch {\n      case e: java.util.concurrent.ExecutionException =>\n        throw new CommitCoordinatorGetCommitsFailedException(e.getCause)\n    }\n\n    def requiresAdditionalListing(): Boolean = {\n      // A gap in delta versions may occur if some delta files are backfilled \"after\" the\n      // file-system listing but before the commit-coordinator listing. To handle this scenario, we\n      // perform an additional listing from the file system because those missing files would be\n      // backfilled by now and show up in the file-system.\n      // Note: We only care about missing delta files with version <= versionToLoad\n      val areDeltaFilesMissing = unbackfilledCommitsResponse.getCommits.asScala.headOption match {\n        case Some(commit) =>\n          // Missing Delta files: [maxDeltaVersionSeen + 1, commit.head.version - 1]\n          maxDeltaVersionSeen + 1 < commit.getVersion\n        case None =>\n          // Missing Delta files: [maxDeltaVersionSeen + 1, latestTableVersion]\n          // When there are no commits, we should consider the latestTableVersion from the commit\n          // store to detect if ALL trailing commits were concurrently backfilled.\n          unbackfilledCommitsResponse.getLatestTableVersion >= 0 &&\n            maxDeltaVersionSeen < unbackfilledCommitsResponse.getLatestTableVersion\n      }\n      versionToLoad.forall(maxDeltaVersionSeen < _) && areDeltaFilesMissing\n    }\n\n    val initialMaxDeltaVersionSeen = maxDeltaVersionSeen\n    val (additionalLogTuplesFromFsListingOpt, additionalChecksumOpt) =\n      if (requiresAdditionalListing()) {\n        recordDeltaEvent(\n          this, CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_ADDITIONAL_LISTING_REQUIRED)\n        listFromFileSystemInternal(\n          startVersion = initialMaxDeltaVersionSeen + 1,\n          versionToLoad,\n          includeMinorCompactions\n        )\n      } else {\n        (None, initialChecksumOpt)\n      }\n    additionalLogTuplesFromFsListingOpt.foreach { logTuples =>\n      logTuples.filter(_._2 == FileType.DELTA).map(_._3).foreach { deltaVersion =>\n        maxDeltaVersionSeen = Math.max(maxDeltaVersionSeen, deltaVersion)\n      }\n    }\n    if (requiresAdditionalListing()) {\n      // We should not have any gaps in File-System versions and CommitCoordinator versions after\n      // the additional listing.\n      val eventData = Map(\n        \"initialCommitVersionsFromFsListingOpt\" ->\n          initialLogTuplesFromFsListingOpt.map(_.map(_._3).toSeq),\n        \"initialMaxDeltaVersionSeen\" -> initialMaxDeltaVersionSeen,\n        \"additionalCommitVersionsFromFsListingOpt\" ->\n          additionalLogTuplesFromFsListingOpt.map(_.map(_._3).toSeq),\n        \"maxDeltaVersionSeen\" -> maxDeltaVersionSeen,\n        \"unbackfilledCommitVersions\" ->\n          unbackfilledCommitsResponse.getCommits.asScala.map(commit => commit.getVersion),\n        \"latestCommitVersion\" -> unbackfilledCommitsResponse.getLatestTableVersion)\n      recordDeltaEvent(\n        deltaLog = this,\n        opType = CoordinatedCommitsUsageLogs.FS_COMMIT_COORDINATOR_LISTING_UNEXPECTED_GAPS,\n        data = eventData)\n      if (DeltaUtils.isTesting) {\n        throw new IllegalStateException(\n          s\"Delta table at $dataPath unexpectedly still requires additional file-system listing \" +\n          s\"after an additional file-system listing was already performed to reconcile the gap \" +\n          s\"between concurrent file-system and commit-owner calls. Details: $eventData\"\n        )\n      }\n    }\n\n    val finalLogTuplesFromFsListingOpt: Option[Array[(FileStatus, FileType.Value, Long)]] =\n      (initialLogTuplesFromFsListingOpt, additionalLogTuplesFromFsListingOpt) match {\n        case (Some(initial), Some(additional)) =>\n          // Filter initial list to exclude files with versions beyond\n          // `initialListingMaxDeltaVersionSeen` to prevent duplicating non-delta files with\n          // higher versions in the combined list. Ideally we shouldn't need this, but we are\n          // being defensive here if the log has missing files.\n          // E.g. initial = [0.json, 1.json, 2.checkpoint], initialListingMaxDeltaVersionSeen = 1,\n          // additional = [2.checkpoint], final = [0.json, 1.json, 2.checkpoint]\n          Some(initial.takeWhile(_._3 <= initialMaxDeltaVersionSeen) ++ additional)\n        case (Some(initial), None) => Some(initial)\n        case (None, Some(additional)) => Some(additional)\n        case _ => None\n      }\n\n    val unbackfilledCommitsFiltered = unbackfilledCommitsResponse.getCommits.asScala\n      .dropWhile(_.getVersion <= maxDeltaVersionSeen)\n      .takeWhile(commit => versionToLoad.forall(commit.getVersion <= _))\n      .map(_.getFileStatus)\n\n    // If result from fs listing is None and result from commit-coordinator is empty, return none.\n    // This is used by caller to distinguish whether table doesn't exist.\n    val logTuplesToReturn = finalLogTuplesFromFsListingOpt.map { logTuplesFromFsListing =>\n      logTuplesFromFsListing.map(_._1) ++ unbackfilledCommitsFiltered\n    }\n    val latestChecksumOpt = additionalChecksumOpt.orElse(initialChecksumOpt)\n    (logTuplesToReturn, latestChecksumOpt)\n  }\n\n  /**\n   * This method is designed to efficiently and reliably list delta, compacted delta, and\n   * checkpoint files associated with a Delta Lake table. It makes parallel calls to both the\n   * file-system and a commit-coordinator (if available), reconciles the results to account for\n   * asynchronous backfill operations, and ensures a comprehensive list of file statuses without\n   * missing any concurrently backfilled files.\n   * *Note*: If table is a coordinated-commits table, the commit-coordinator client MUST be passed\n   * to correctly list the commits.\n   *\n   * @param startVersion the version to start. Inclusive.\n   * @param tableCommitCoordinatorClientOpt the optional commit-coordinator client to use for\n   *                                        fetching un-backfilled commits.\n   * @param catalogTableOpt the optional catalog table to pass to the commit coordinator client.\n   * @param versionToLoad the optional parameter to set the max version we should return. Inclusive.\n   * @param includeMinorCompactions Whether to include minor compaction files in the result\n   * @return Some array of files found (possibly empty, if no usable commit files are present), or\n   *         None if the listing returned no files at all.\n   */\n  protected final def listDeltaCompactedDeltaAndCheckpointFiles(\n      startVersion: Long,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      versionToLoad: Option[Long],\n      includeMinorCompactions: Boolean): Option[Array[FileStatus]] = {\n    recordDeltaOperation(self, \"delta.deltaLog.listDeltaAndCheckpointFiles\") {\n      val (logTuplesOpt, latestChecksumOpt) =\n        listDeltaCompactedDeltaCheckpointFilesAndLatestChecksumFile(\n          startVersion,\n          tableCommitCoordinatorClientOpt,\n          catalogTableOpt,\n          versionToLoad,\n          includeMinorCompactions)\n      lastSeenChecksumFileStatusOpt = latestChecksumOpt\n      logTuplesOpt\n    }\n  }\n\n  /**\n   * Get a list of files that can be used to compute a Snapshot at version `versionToLoad`, If\n   * `versionToLoad` is not provided, will generate the list of files that are needed to load the\n   * latest version of the Delta table. This method also performs checks to ensure that the delta\n   * files are contiguous.\n   *\n   * @param versionToLoad A specific version to load. Typically used with time travel and the\n   *                      Delta streaming source. If not provided, we will try to load the latest\n   *                      version of the table.\n   * @param oldCheckpointProviderOpt The [[CheckpointProvider]] from the previous snapshot. This is\n   *                              used as a start version for the listing when `startCheckpoint` is\n   *                              unavailable. This is also used to initialize the [[LogSegment]].\n   * @param tableCommitCoordinatorClientOpt the optional commit-coordinator client to use for\n   *                                        fetching un-backfilled commits.\n   * @param catalogTableOpt the optional catalog table to pass to the commit coordinator client.\n   * @param lastCheckpointInfo [[LastCheckpointInfo]] from the _last_checkpoint. This could be\n   *                           used to initialize the Snapshot's [[LogSegment]].\n   * @return Some LogSegment to build a Snapshot if files do exist after the given\n   *         startCheckpoint. None, if the directory was missing or empty.\n   */\n  protected def createLogSegment(\n      versionToLoad: Option[Long] = None,\n      oldCheckpointProviderOpt: Option[UninitializedCheckpointProvider] = None,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient] = None,\n      catalogTableOpt: Option[CatalogTable] = None,\n      lastCheckpointInfo: Option[LastCheckpointInfo] = None): Option[LogSegment] = {\n    // List based on the last known checkpoint version.\n    // if that is -1, list from version 0L\n    val lastCheckpointVersion = getCheckpointVersion(lastCheckpointInfo, oldCheckpointProviderOpt)\n    val listingStartVersion = Math.max(0L, lastCheckpointVersion)\n    val includeMinorCompactions =\n      spark.conf.get(DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS)\n    val newFiles = listDeltaCompactedDeltaAndCheckpointFiles(\n      listingStartVersion,\n      tableCommitCoordinatorClientOpt,\n      catalogTableOpt,\n      versionToLoad,\n      includeMinorCompactions)\n    getLogSegmentForVersion(\n      versionToLoad,\n      newFiles,\n      validateLogSegmentWithoutCompactedDeltas = true,\n      oldCheckpointProviderOpt = oldCheckpointProviderOpt,\n      tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n      catalogTableOpt = catalogTableOpt,\n      lastCheckpointInfo = lastCheckpointInfo\n    )\n  }\n\n  private def createLogSegment(\n      previousSnapshot: Snapshot,\n      catalogTableOpt: Option[CatalogTable],\n      commitCoordinatorOpt: Option[TableCommitCoordinatorClient]): Option[LogSegment] = {\n    createLogSegment(\n      oldCheckpointProviderOpt = Some(previousSnapshot.checkpointProvider),\n      tableCommitCoordinatorClientOpt = commitCoordinatorOpt,\n      catalogTableOpt = catalogTableOpt)\n  }\n\n  /**\n   * Returns the last known checkpoint version based on [[LastCheckpointInfo]] or\n   * [[CheckpointProvider]].\n   * Returns -1 if both the info is not available.\n   */\n  protected def getCheckpointVersion(\n      lastCheckpointInfoOpt: Option[LastCheckpointInfo],\n      oldCheckpointProviderOpt: Option[UninitializedCheckpointProvider]): Long = {\n    lastCheckpointInfoOpt.map(_.version)\n      .orElse(oldCheckpointProviderOpt.map(_.version))\n      .getOrElse(-1)\n  }\n\n  /**\n   * Helper method to validate that selected deltas are contiguous from checkpoint version till\n   * the required `versionToLoad`.\n   * @param selectedDeltas - deltas selected for snapshot creation.\n   * @param checkpointVersion - checkpoint version selected for snapshot creation. Should be `-1` if\n   *                            no checkpoint is selected.\n   * @param versionToLoad - version for which we want to create the Snapshot.\n   */\n  private def validateDeltaVersions(\n      selectedDeltas: Array[FileStatus],\n      checkpointVersion: Long,\n      versionToLoad: Option[Long]): Unit = {\n    // checkpointVersion should be passed as -1 if no checkpoint is needed for the LogSegment.\n\n    // We may just be getting a checkpoint file.\n    selectedDeltas.headOption.foreach { headDelta =>\n      val headDeltaVersion = deltaVersion(headDelta)\n      val lastDeltaVersion = selectedDeltas.last match {\n        case CompactedDeltaFile(_, _, endV) => endV\n        case DeltaFile(_, v) => v\n      }\n\n      if (headDeltaVersion != checkpointVersion + 1) {\n        throw DeltaErrors.truncatedTransactionLogException(\n          unsafeDeltaFile(logPath, checkpointVersion + 1),\n          lastDeltaVersion,\n          unsafeVolatileMetadata) // metadata is best-effort only\n      }\n      val deltaVersions = selectedDeltas.flatMap {\n        case CompactedDeltaFile(_, startV, endV) => (startV to endV)\n        case DeltaFile(_, v) => Seq(v)\n      }\n      verifyDeltaVersions(\n        spark = spark,\n        versions = deltaVersions,\n        expectedStartVersion = Some(checkpointVersion + 1),\n        expectedEndVersion = versionToLoad,\n        cachedSnapshot = Some(unsafeVolatileSnapshot))\n    }\n  }\n\n  /**\n   * Helper function for the getLogSegmentForVersion above. Called with a provided files list,\n   * and will then try to construct a new LogSegment using that.\n   * *Note*: If table is a coordinated-commits table, the commit-coordinator MUST be passed to\n   * correctly list the commits.\n   */\n  protected def getLogSegmentForVersion(\n      versionToLoad: Option[Long],\n      files: Option[Array[FileStatus]],\n      validateLogSegmentWithoutCompactedDeltas: Boolean,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      oldCheckpointProviderOpt: Option[UninitializedCheckpointProvider],\n      lastCheckpointInfo: Option[LastCheckpointInfo]): Option[LogSegment] = {\n    recordFrameProfile(\"Delta\", \"SnapshotManagement.getLogSegmentForVersion\") {\n      val lastCheckpointVersion = getCheckpointVersion(lastCheckpointInfo, oldCheckpointProviderOpt)\n      val newFiles = files.filterNot(_.isEmpty)\n        .getOrElse {\n          // No files found even when listing from 0 => empty directory => table does not exist yet.\n          if (lastCheckpointVersion < 0) return None\n          // We always write the commit and checkpoint files before updating _last_checkpoint.\n          // If the listing came up empty, then we either encountered a list-after-put\n          // inconsistency in the underlying log store, or somebody corrupted the table by\n          // deleting files. Either way, we can't safely continue.\n          //\n          // For now, we preserve existing behavior by returning Array.empty, which will trigger a\n          // recursive call to [[createLogSegment]] below.\n          Array.empty[FileStatus]\n        }\n\n      if (newFiles.isEmpty && lastCheckpointVersion < 0) {\n        // We can't construct a snapshot because the directory contained no usable commit\n        // files... but we can't return None either, because it was not truly empty.\n        throw DeltaErrors.logFileNotFoundException(logPath, versionToLoad, lastCheckpointVersion)\n      } else if (newFiles.isEmpty) {\n        // The directory may be deleted and recreated and we may have stale state in our DeltaLog\n        // singleton, so try listing from the first version\n        return createLogSegment(\n          versionToLoad = versionToLoad,\n          catalogTableOpt = catalogTableOpt)\n      }\n      val (checkpoints, deltasAndCompactedDeltas) = newFiles.partition(isCheckpointFile)\n      val (deltas, compactedDeltas) = deltasAndCompactedDeltas.partition(isDeltaFile)\n      // Find the latest checkpoint in the listing that is not older than the versionToLoad\n      val checkpointFiles = checkpoints.map(f => CheckpointInstance(f.getPath))\n      val newCheckpoint = getLatestCompleteCheckpointFromList(checkpointFiles, versionToLoad)\n      val newCheckpointVersion = newCheckpoint.map(_.version).getOrElse {\n        // If we do not have any checkpoint, pass new checkpoint version as -1 so that first\n        // delta version can be 0.\n        if (lastCheckpointVersion >= 0) {\n          // `startCheckpoint` was given but no checkpoint found on delta log. This means that the\n          // last checkpoint we thought should exist (the `_last_checkpoint` file) no longer exists.\n          // Try to look up another valid checkpoint and create `LogSegment` from it.\n          // This case can arise if the user deleted the table (all commits and checkpoints) but\n          // left the _last_checkpoint intact.\n          recordDeltaEvent(this, \"delta.checkpoint.error.partial\")\n          val snapshotVersion = versionToLoad.getOrElse(deltaVersion(deltas.last))\n          getLogSegmentWithMaxExclusiveCheckpointVersion(\n            snapshotVersion,\n            lastCheckpointVersion,\n            tableCommitCoordinatorClientOpt,\n            catalogTableOpt\n          ).foreach { alternativeLogSegment =>\n            return Some(alternativeLogSegment)\n          }\n\n          // No alternative found, but the directory contains files so we cannot return None.\n          throw DeltaErrors.missingPartFilesException(\n            lastCheckpointVersion, new FileNotFoundException(\n              s\"Checkpoint file to load version: $lastCheckpointVersion is missing.\"))\n        }\n        -1L\n      }\n\n      // If there is a new checkpoint, start new lineage there. If `newCheckpointVersion` is -1,\n      // it will list all existing delta files.\n      val deltasAfterCheckpoint = deltas.filter { file =>\n        deltaVersion(file) > newCheckpointVersion\n      }\n\n      // Here we validate that we are able to create a valid LogSegment by just using commit deltas\n      // and without considering minor-compacted deltas. We want to fail early if log is messed up\n      // i.e. some commit deltas are missing (although compacted-deltas are present).\n      // We should not do this validation when we want to update the logSegment after a conflict\n      // via the [[SnapshotManagement.getUpdatedLogSegment]] method. In that specific flow, we just\n      // list from the committed version and reuse existing pre-commit logsegment together with\n      // listing result to create the new pre-commit logsegment. Because of this, we don't have info\n      // about all the delta files (e.g. when minor compactions are used in existing preCommit log\n      // segment) and hence the validation if attempted will fail. So we need to set\n      // `validateLogSegmentWithoutCompactedDeltas` to false in that case.\n      if (validateLogSegmentWithoutCompactedDeltas) {\n        validateDeltaVersions(\n          selectedDeltas = deltasAfterCheckpoint,\n          checkpointVersion = newCheckpointVersion,\n          versionToLoad = versionToLoad)\n      }\n\n      val newVersion =\n        deltasAfterCheckpoint.lastOption.map(deltaVersion).getOrElse(newCheckpoint.get.version)\n      // reuse the oldCheckpointProvider if it is same as what we are looking for.\n      val checkpointProviderOpt = newCheckpoint.map { ci =>\n        oldCheckpointProviderOpt\n          .collect { case cp if cp.version == ci.version => cp }\n          .getOrElse(ci.getCheckpointProvider(this, checkpoints, lastCheckpointInfo))\n      }\n      // In the case where `deltasAfterCheckpoint` is empty, `deltas` should still not be empty,\n      // they may just be before the checkpoint version unless we have a bug in log cleanup.\n      if (deltas.isEmpty) {\n        throw new IllegalStateException(s\"Could not find any delta files for version $newVersion\")\n      }\n      if (versionToLoad.exists(_ != newVersion)) {\n        throwNonExistentVersionError(versionToLoad.get)\n      }\n      val lastCommitTimestamp = deltas.last.getModificationTime\n\n      val deltasAndCompactedDeltasForLogSegment = useCompactedDeltasForLogSegment(\n        deltasAndCompactedDeltas,\n        deltasAfterCheckpoint,\n        latestCommitVersion = newVersion,\n        checkpointVersionToUse = newCheckpointVersion)\n\n      validateDeltaVersions(\n        selectedDeltas = deltasAndCompactedDeltasForLogSegment,\n        checkpointVersion = newCheckpointVersion,\n        versionToLoad = versionToLoad)\n\n      Some(LogSegment(\n        logPath,\n        newVersion,\n        deltasAndCompactedDeltasForLogSegment,\n        checkpointProviderOpt,\n        lastCommitTimestamp))\n    }\n  }\n\n  /**\n   * @param deltasAndCompactedDeltas - all deltas or compacted deltas which could be used\n   * @param deltasAfterCheckpoint - deltas after the last checkpoint file\n   * @param latestCommitVersion - commit version for which we are trying to create Snapshot for\n   * @param checkpointVersionToUse - underlying checkpoint version to use in Snapshot, -1 if no\n   *                               checkpoint is used.\n   * @return Returns a list of deltas/compacted-deltas which can be used to construct the\n   *         [[LogSegment]] instead of `deltasAfterCheckpoint`.\n   */\n  protected def useCompactedDeltasForLogSegment(\n      deltasAndCompactedDeltas: Seq[FileStatus],\n      deltasAfterCheckpoint: Array[FileStatus],\n      latestCommitVersion: Long,\n      checkpointVersionToUse: Long): Array[FileStatus] = {\n\n    val selectedDeltas = mutable.ArrayBuffer.empty[FileStatus]\n    var highestVersionSeen = checkpointVersionToUse\n    val commitRangeCovered = mutable.ArrayBuffer.empty[Long]\n    // track if there is at least 1 compacted delta in `deltasAndCompactedDeltas`\n    var hasCompactedDeltas = false\n    for (file <- deltasAndCompactedDeltas) {\n      val (startVersion, endVersion) = file match {\n        case CompactedDeltaFile(_, startVersion, endVersion) =>\n          hasCompactedDeltas = true\n          (startVersion, endVersion)\n        case DeltaFile(_, version) =>\n          (version, version)\n      }\n\n      // select the compacted delta if the startVersion doesn't straddle `highestVersionSeen` and\n      // the endVersion doesn't cross the latestCommitVersion.\n      if (highestVersionSeen < startVersion && endVersion <= latestCommitVersion) {\n        commitRangeCovered.appendAll(startVersion to endVersion)\n        selectedDeltas += file\n        highestVersionSeen = endVersion\n      }\n    }\n    // If there are no compacted deltas in the `deltasAndCompactedDeltas` list, return from this\n    // method.\n    if (!hasCompactedDeltas) return deltasAfterCheckpoint\n    // Validation-1: Commits represented by `compactedDeltasToUse` should be unique and there must\n    // not be any duplicates.\n    val coveredCommits = commitRangeCovered.toSet\n    val hasDuplicates = (commitRangeCovered.size != coveredCommits.size)\n\n    // Validation-2: All commits from (CheckpointVersion + 1) to latestCommitVersion should be\n    // either represented by compacted delta or by the delta.\n    val requiredCommits = (checkpointVersionToUse + 1) to latestCommitVersion\n    val missingCommits = requiredCommits.toSet -- coveredCommits\n    if (!hasDuplicates && missingCommits.isEmpty) return selectedDeltas.toArray\n\n    // If the above check failed, that means the compacted delta validation failed.\n    // Just record that event and return just the deltas (deltasAfterCheckpoint).\n    val eventData = Map(\n      \"deltasAndCompactedDeltas\" -> deltasAndCompactedDeltas.map(_.getPath.getName),\n      \"deltasAfterCheckpoint\" -> deltasAfterCheckpoint.map(_.getPath.getName),\n      \"latestCommitVersion\" -> latestCommitVersion,\n      \"checkpointVersionToUse\" -> checkpointVersionToUse,\n      \"hasDuplicates\" -> hasDuplicates,\n      \"missingCommits\" -> missingCommits\n    )\n    recordDeltaEvent(\n      deltaLog = this,\n      opType = \"delta.getLogSegmentForVersion.compactedDeltaValidationFailed\",\n      data = eventData)\n    if (DeltaUtils.isTesting) {\n      assert(false, s\"Validation around Compacted deltas failed while creating Snapshot. \" +\n        s\"[${JsonUtils.toJson(eventData)}]\")\n    }\n    deltasAfterCheckpoint\n  }\n\n  def throwNonExistentVersionError(versionToLoad: Long): Unit = {\n    throw new IllegalStateException(\n      s\"Trying to load a non-existent version $versionToLoad\")\n  }\n\n  /**\n   * Load the Snapshot for this Delta table at initialization. This method uses the `lastCheckpoint`\n   * file as a hint on where to start listing the transaction log directory. If the _delta_log\n   * directory doesn't exist, this method will return an `InitialSnapshot`.\n   */\n  protected def createSnapshotAtInit(initialCatalogTable: Option[CatalogTable]): Unit =\n    withSnapshotLockInterruptibly {\n      recordFrameProfile(\"Delta\", \"SnapshotManagement.createSnapshotAtInit\") {\n        val snapshotInitWallclockTime = clock.getTimeMillis()\n        val lastCheckpointOpt = readLastCheckpointFile()\n        val initialSegmentForNewSnapshot = createLogSegment(\n          versionToLoad = None,\n          catalogTableOpt = initialCatalogTable,\n          lastCheckpointInfo = lastCheckpointOpt)\n        val snapshot = getUpdatedSnapshot(\n          oldSnapshotOpt = None,\n          initialSegmentForNewSnapshot = initialSegmentForNewSnapshot,\n          initialTableCommitCoordinatorClient = None,\n          catalogTableOpt = initialCatalogTable,\n          isAsync = false)\n        currentSnapshot = CapturedSnapshot(snapshot, snapshotInitWallclockTime)\n      }\n    }\n\n  /**\n   * Returns the current snapshot. This does not automatically `update()`.\n   *\n   * WARNING: This is not guaranteed to give you the latest snapshot of the log, nor stay\n   * consistent across multiple accesses. If you need the latest snapshot, it is recommended\n   * to fetch it using `deltaLog.update()`; and save the returned snapshot so it does not\n   * unexpectedly change from under you. See how [[OptimisticTransaction]] and [[DeltaScan]]\n   * use the snapshot as examples for write/read paths respectively.\n   * This API should only be used in scenarios where any recent snapshot will suffice and an\n   * update is undesired, or by internal code that holds the DeltaLog lock to prevent races.\n   */\n  def unsafeVolatileSnapshot: Snapshot = Option(currentSnapshot).map(_.snapshot).orNull\n\n  /**\n   * WARNING: This API is unsafe and deprecated. It will be removed in future versions.\n   * Use the above unsafeVolatileSnapshot to get the most recently cached snapshot on\n   * the cluster.\n   */\n  @deprecated(\"This method is deprecated and will be removed in future versions. \" +\n    \"Use unsafeVolatileSnapshot instead\", \"12.0\")\n  def snapshot: Snapshot = unsafeVolatileSnapshot\n\n  /**\n   * Unsafe due to thread races that can change it at any time without notice, even between two\n   * calls in the same method. Like [[unsafeVolatileSnapshot]] it depends on, this method should be\n   * used only with extreme care in production code (or by unit tests where no races are possible).\n   */\n  private[delta] def unsafeVolatileMetadata =\n    Option(unsafeVolatileSnapshot).map(_.metadata).getOrElse(Metadata())\n\n  protected def createSnapshot(\n      initSegment: LogSegment,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      checksumOpt: Option[VersionChecksum]): Snapshot = {\n    val startingFrom = if (!initSegment.checkpointProvider.isEmpty) {\n      log\" starting from checkpoint version \" +\n      log\"${MDC(DeltaLogKeys.START_VERSION, initSegment.checkpointProvider.version)}.\"\n    } else log\".\"\n    logInfo(log\"[tableId=${MDC(DeltaLogKeys.TABLE_ID, truncatedUnsafeVolatileTableId)}] \" +\n      log\"Loading version ${MDC(DeltaLogKeys.VERSION, initSegment.version)}\" + startingFrom)\n    createSnapshotFromGivenOrEquivalentLogSegment(\n        initSegment, tableCommitCoordinatorClientOpt, catalogTableOpt) { segment =>\n      new Snapshot(\n        path = logPath,\n        version = segment.version,\n        logSegment = segment,\n        deltaLog = this,\n        checksumOpt = checksumOpt.orElse(\n          readChecksum(segment.version, lastSeenChecksumFileStatusOpt))\n      )\n    }\n  }\n\n  /**\n   * Returns a [[LogSegment]] for reading `snapshotVersion` such that the segment's checkpoint\n   * version (if checkpoint present) is LESS THAN `maxExclusiveCheckpointVersion`.\n   * This is useful when trying to skip a bad checkpoint. Returns `None` when we are not able to\n   * construct such [[LogSegment]], for example, no checkpoint can be used but we don't have the\n   * entire history from version 0 to version `snapshotVersion`.\n   * *Note*: If table is a coordinated-commits table, the commit-coordinator MUST be passed to\n   * correctly list the commits.\n   */\n  private def getLogSegmentWithMaxExclusiveCheckpointVersion(\n      snapshotVersion: Long,\n      maxExclusiveCheckpointVersion: Long,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable]): Option[LogSegment] = {\n    assert(\n      snapshotVersion >= maxExclusiveCheckpointVersion,\n      s\"snapshotVersion($snapshotVersion) is less than \" +\n        s\"maxExclusiveCheckpointVersion($maxExclusiveCheckpointVersion)\")\n    val upperBoundVersion = math.min(snapshotVersion + 1, maxExclusiveCheckpointVersion)\n    val previousCp =\n      if (upperBoundVersion > 0) findLastCompleteCheckpointBefore(upperBoundVersion) else None\n    previousCp match {\n      case Some(cp) =>\n        val filesSinceCheckpointVersion = listDeltaCompactedDeltaAndCheckpointFiles(\n          startVersion = cp.version,\n          tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n          catalogTableOpt = catalogTableOpt,\n          versionToLoad = Some(snapshotVersion),\n          includeMinorCompactions = false\n        ).getOrElse(Array.empty)\n        val (checkpoints, deltas) = filesSinceCheckpointVersion.partition(isCheckpointFile)\n        if (deltas.isEmpty) {\n          // We cannot find any delta files. Returns None as we cannot construct a `LogSegment` only\n          // from checkpoint files. This is because in order to create a `LogSegment`, we need to\n          // set `LogSegment.lastCommitTimestamp`, and it must be read from the file modification\n          // time of the delta file for `snapshotVersion`. It cannot be the file modification time\n          // of a checkpoint file because it should be deterministic regardless how we construct the\n          // Snapshot, and only delta json log files can ensure that.\n          return None\n        }\n        // `checkpoints` may contain multiple checkpoints for different part sizes, we need to\n        // search `FileStatus`s of the checkpoint files for `cp`.\n        val checkpointProvider =\n          cp.getCheckpointProvider(this, checkpoints, lastCheckpointInfoHint = None)\n        // Create the list of `FileStatus`s for delta files after `cp.version`.\n        val deltasAfterCheckpoint = deltas.filter { file =>\n          deltaVersion(file) > cp.version\n        }\n        val deltaVersions = deltasAfterCheckpoint.map(deltaVersion)\n        // `deltaVersions` should not be empty and `verifyDeltaVersions` will verify it\n        try {\n          verifyDeltaVersions(\n            spark = spark,\n            versions = deltaVersions,\n            expectedStartVersion = Some(cp.version + 1),\n            expectedEndVersion = Some(snapshotVersion),\n            cachedSnapshot = Some(unsafeVolatileSnapshot))\n        } catch {\n          case NonFatal(e) =>\n            logWarning(log\"Failed to find a valid LogSegment for \" +\n              log\"${MDC(DeltaLogKeys.VERSION, snapshotVersion)}\", e)\n            return None\n        }\n        Some(LogSegment(\n          logPath,\n          snapshotVersion,\n          deltas,\n          Some(checkpointProvider),\n          deltas.last.getModificationTime))\n      case None =>\n        val listFromResult =\n          listDeltaCompactedDeltaAndCheckpointFiles(\n            startVersion = 0,\n            tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n            catalogTableOpt = catalogTableOpt,\n            versionToLoad = Some(snapshotVersion),\n            includeMinorCompactions = false)\n        val (deltas, deltaVersions) =\n          listFromResult\n            .getOrElse(Array.empty)\n            .flatMap(DeltaFile.unapply(_))\n            .unzip\n        try {\n          verifyDeltaVersions(\n            spark = spark,\n            versions = deltaVersions,\n            expectedStartVersion = Some(0),\n            expectedEndVersion = Some(snapshotVersion),\n            cachedSnapshot = Some(unsafeVolatileSnapshot))\n        } catch {\n          case NonFatal(e) =>\n            logWarning(log\"Failed to find a valid LogSegment for \" +\n              log\"${MDC(DeltaLogKeys.VERSION, snapshotVersion)}\", e)\n            return None\n        }\n        Some(LogSegment(\n          logPath = logPath,\n          version = snapshotVersion,\n          deltas = deltas,\n          checkpointProviderOpt = None,\n          lastCommitTimestamp = deltas.last.getModificationTime))\n    }\n  }\n\n  /**\n   * Used to compute the LogSegment after a commit, by adding the delta file with the specified\n   * version to the preCommitLogSegment (which must match the immediately preceding version).\n   */\n  protected[delta] def getLogSegmentAfterCommit(\n      committedVersion: Long,\n      newChecksumOpt: Option[VersionChecksum],\n      preCommitLogSegment: LogSegment,\n      commit: Commit,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      oldCheckpointProvider: CheckpointProvider): LogSegment = recordFrameProfile(\n    \"Delta\", \"SnapshotManagement.getLogSegmentAfterCommit\") {\n    // If the table doesn't have any competing updates, then go ahead and use the optimized\n    // incremental logSegment computation to fetch the LogSegment for the committedVersion.\n    // See the comment in the getLogSegmentAfterCommit overload for why we can't always safely\n    // return the committedVersion's snapshot when there is contention.\n    val useFastSnapshotConstruction = !snapshotLock.hasQueuedThreads\n    if (useFastSnapshotConstruction) {\n      SnapshotManagement.appendCommitToLogSegment(\n        preCommitLogSegment, commit.getFileStatus, committedVersion)\n    } else {\n      val latestCheckpointProvider =\n        Seq(preCommitLogSegment.checkpointProvider, oldCheckpointProvider).maxBy(_.version)\n      getLogSegmentAfterCommit(\n        tableCommitCoordinatorClientOpt,\n        catalogTableOpt,\n        latestCheckpointProvider)\n    }\n  }\n\n  protected[delta] def getLogSegmentAfterCommit(\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      oldCheckpointProvider: UninitializedCheckpointProvider): LogSegment = {\n    /**\n     * We can't specify `versionToLoad = committedVersion` for the call below.\n     * If there are a lot of concurrent commits to the table on the same cluster, each\n     * would generate a different snapshot, and thus each would trigger a new state\n     * reconstruction. The last commit would get stuck waiting for each of the previous\n     * jobs to finish to grab the update lock.\n     * Instead, just do a general update to the latest available version. The racing commits\n     * can then use the version check short-circuit to avoid constructing a new snapshot.\n     */\n    createLogSegment(\n      oldCheckpointProviderOpt = Some(oldCheckpointProvider),\n      tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n      catalogTableOpt = catalogTableOpt\n    ).getOrElse {\n      // This shouldn't be possible right after a commit\n      logError(log\"No delta log found for the Delta table at ${MDC(DeltaLogKeys.PATH, logPath)}\")\n      throw DeltaErrors.logFileNotFoundException(\n        logPath,\n        version = None,\n        checkpointVersion = getCheckpointVersion(None, Some(oldCheckpointProvider)))\n    }\n  }\n\n  /**\n   * Create a [[Snapshot]] from the given [[LogSegment]]. If failing to create the snapshot, we will\n   * search an equivalent [[LogSegment]] using a different checkpoint and retry up to\n   * [[DeltaSQLConf.DELTA_SNAPSHOT_LOADING_MAX_RETRIES]] times.\n   */\n  protected def createSnapshotFromGivenOrEquivalentLogSegment(\n      initSegment: LogSegment,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable])\n      (snapshotCreator: LogSegment => Snapshot): Snapshot = {\n    val numRetries =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SNAPSHOT_LOADING_MAX_RETRIES)\n    var attempt = 0\n    var segment = initSegment\n    // Remember the first error we hit. If all retries fail, we will throw the first error to\n    // provide the root cause. We catch `SparkException` because corrupt checkpoint files are\n    // detected in the executor side when a task is trying to read them.\n    var firstError: SparkException = null\n    while (true) {\n      try {\n        return snapshotCreator(segment)\n      } catch {\n        case e: SparkException if attempt < numRetries && !segment.checkpointProvider.isEmpty =>\n          if (firstError == null) {\n            firstError = e\n          }\n          logWarning(log\"Failed to create a snapshot from log segment \" +\n            log\"${MDC(DeltaLogKeys.LOG_SEGMENT, segment)}. Trying a different checkpoint.\", e)\n          segment = getLogSegmentWithMaxExclusiveCheckpointVersion(\n            segment.version,\n            segment.checkpointProvider.version,\n            tableCommitCoordinatorClientOpt,\n            catalogTableOpt).getOrElse {\n              // Throw the first error if we cannot find an equivalent `LogSegment`.\n              throw firstError\n            }\n          attempt += 1\n        case e: SparkException if firstError != null =>\n          logWarning(log\"Failed to create a snapshot from log segment \" +\n            log\"${MDC(DeltaLogKeys.LOG_SEGMENT, segment)}\", e)\n          throw firstError\n      }\n    }\n    throw new IllegalStateException(\"should not happen\")\n  }\n\n  /** Checks if the given timestamp is outside the current staleness window */\n  protected def isCurrentlyStale: Long => Boolean = {\n    val limit = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ASYNC_UPDATE_STALENESS_TIME_LIMIT)\n    val cutoffOpt = if (limit > 0) Some(math.max(0, clock.getTimeMillis() - limit)) else None\n    timestamp => cutoffOpt.forall(timestamp < _)\n  }\n\n  /**\n   * Get the newest logSegment, using the previous logSegment as a hint. This is faster than\n   * doing a full update, but it won't work if the table's log directory was replaced.\n   */\n  def getUpdatedLogSegment(\n      oldLogSegment: LogSegment,\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable]\n  ): (LogSegment, Seq[FileStatus]) = {\n    val includeCompactions = spark.conf.get(DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS)\n    val newFilesOpt = listDeltaCompactedDeltaAndCheckpointFiles(\n        startVersion = oldLogSegment.version + 1,\n        tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n        catalogTableOpt = catalogTableOpt,\n        versionToLoad = None,\n        includeMinorCompactions = includeCompactions)\n    // The file listing may return the following results:\n    // 1. None => implies the log directory does not exist or is completely empty.\n    // 2. Some(empty) => implies the log directory exists but there are no usable files.\n    // 3. Some(non-empty) => implies the log directory exists and there are some usable files.\n    // Therefore, we need to handle both cases (1) and (2) here.\n    val newFiles = newFilesOpt.filter(_.nonEmpty).getOrElse {\n      // An empty listing likely implies a list-after-write inconsistency or that somebody clobbered\n      // the Delta log.\n      return (oldLogSegment, Nil)\n    }\n    val allFiles = (\n      oldLogSegment.checkpointProvider.topLevelFiles ++\n        oldLogSegment.deltas ++\n        newFiles\n      ).toArray\n    val lastCheckpointInfo = Option.empty[LastCheckpointInfo]\n    val newLogSegment = getLogSegmentForVersion(\n      versionToLoad = None,\n      files = Some(allFiles),\n      validateLogSegmentWithoutCompactedDeltas = false,\n      tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n      catalogTableOpt = catalogTableOpt,\n      lastCheckpointInfo = lastCheckpointInfo,\n      oldCheckpointProviderOpt = Some(oldLogSegment.checkpointProvider)\n    ).getOrElse(oldLogSegment)\n    val fileStatusesOfConflictingCommits = newFiles.collect {\n      case DeltaFile(f, v) if v <= newLogSegment.version => f\n    }\n    (newLogSegment, fileStatusesOfConflictingCommits)\n  }\n\n  /**\n   * Returns the snapshot, if it has been updated since the specified timestamp.\n   *\n   * Note that this should be used differently from isSnapshotStale. Staleness is\n   * used to allow async updates if the table has been updated within the staleness\n   * window, which allows for better perf in exchange for possibly using a slightly older\n   * view of the table. For eg, if a table is queried multiple times in quick succession.\n   *\n   * On the other hand, getSnapshotIfFresh is used to identify duplicate updates within a\n   * single transaction. For eg, if a table isn't cached and the snapshot was fetched from the\n   * logstore, then updating the snapshot again in the same transaction is superfluous. We can\n   * use this function to detect and skip such an update.\n   */\n  private def getSnapshotIfFresh(\n      capturedSnapshot: CapturedSnapshot,\n      checkIfUpdatedSinceTs: Option[Long]): Option[Snapshot] = {\n    checkIfUpdatedSinceTs.collect {\n      case ts if ts <= capturedSnapshot.updateTimestamp => capturedSnapshot.snapshot\n    }\n  }\n\n  /**\n   * Update ActionLog by applying the new delta files if any.\n   *\n   * @param stalenessAcceptable Whether we can accept working with a stale version of the table. If\n   *                            the table has surpassed our staleness tolerance, we will update to\n   *                            the latest state of the table synchronously. If staleness is\n   *                            acceptable, and the table hasn't passed the staleness tolerance, we\n   *                            will kick off a job in the background to update the table state,\n   *                            and can return a stale snapshot in the meantime.\n   * @param checkIfUpdatedSinceTs Skip the update if we've already updated the snapshot since the\n   *                              specified timestamp.\n   * @param catalogTableOpt The catalog table of the current table.\n   */\n  def update(\n      stalenessAcceptable: Boolean = false,\n      checkIfUpdatedSinceTs: Option[Long] = None,\n      catalogTableOpt: Option[CatalogTable] = None): Snapshot = {\n    val startTimeMs = System.currentTimeMillis()\n    // currentSnapshot is volatile. Make a local copy of it at the start of the update call, so\n    // that there's no chance of a race condition changing the snapshot partway through the update.\n    val capturedSnapshot = currentSnapshot\n    val oldVersion = capturedSnapshot.snapshot.version\n    def sendEvent(\n      newSnapshot: Snapshot,\n      snapshotAlreadyUpdatedAfterRequiredTimestamp: Boolean = false\n    ): Unit = {\n      recordDeltaEvent(\n        this,\n        opType = \"deltaLog.update\",\n        data = Map(\n          \"snapshotAlreadyUpdatedAfterRequiredTimestamp\" ->\n            snapshotAlreadyUpdatedAfterRequiredTimestamp,\n          \"newVersion\" -> newSnapshot.version,\n          \"oldVersion\" -> oldVersion,\n          \"timeTakenMs\" -> (System.currentTimeMillis() - startTimeMs)\n        )\n      )\n    }\n    // Eagerly exit if the snapshot is already new enough to satisfy the caller\n    getSnapshotIfFresh(capturedSnapshot, checkIfUpdatedSinceTs).foreach { snapshot =>\n      sendEvent(snapshot, snapshotAlreadyUpdatedAfterRequiredTimestamp = true)\n      return snapshot\n    }\n    val doAsync = stalenessAcceptable && !isCurrentlyStale(capturedSnapshot.updateTimestamp)\n    if (!doAsync) {\n      recordFrameProfile(\"Delta\", \"SnapshotManagement.update\") {\n        val snapshot = withSnapshotLockInterruptibly {\n          val newSnapshot = updateInternal(\n            isAsync = false,\n            catalogTableOpt)\n          sendEvent(newSnapshot = newSnapshot)\n          newSnapshot\n        }\n        logCurrentSnapshot()\n        snapshot\n      }\n    } else {\n      // Kick off an async update, if one is not already obviously running. Intentionally racy.\n      if (Option(asyncUpdateTask).forall(_.isDone)) {\n        try {\n          val jobGroup = spark.sparkContext.getLocalProperty(SparkContext.SPARK_JOB_GROUP_ID)\n          asyncUpdateTask = SnapshotManagement.deltaLogAsyncUpdateThreadPool.submit(spark) {\n            spark.sparkContext.setLocalProperty(\"spark.scheduler.pool\", \"deltaStateUpdatePool\")\n            spark.sparkContext.setJobGroup(\n              jobGroup,\n              s\"Updating state of Delta table at ${capturedSnapshot.snapshot.path}\",\n              interruptOnCancel = true)\n            tryUpdate(\n              isAsync = true,\n              catalogTableOpt)\n          }\n        } catch {\n          case NonFatal(e) if !DeltaUtils.isTesting =>\n            // Failed to schedule the future -- fail in testing, but just log it in prod.\n            recordDeltaEvent(this, \"delta.snapshot.asyncUpdateFailed\", data = Map(\"exception\" -> e))\n        }\n      }\n      logCurrentSnapshot()\n      currentSnapshot.snapshot\n    }\n  }\n\n  /**\n   * Try to update ActionLog. If another thread is updating ActionLog, then this method returns\n   * at once and return the current snapshot. The return snapshot may be stale.\n   */\n  private def tryUpdate(\n    isAsync: Boolean,\n    catalogTableOpt: Option[CatalogTable]): Snapshot =\n    recordDeltaOperation(this, \"delta.log.update\", Map(TAG_ASYNC -> isAsync.toString)) {\n    if (snapshotLock.tryLock()) {\n      try {\n        updateInternal(\n          isAsync,\n          catalogTableOpt)\n      } finally {\n        snapshotLock.unlock()\n      }\n    } else {\n      currentSnapshot.snapshot\n    }\n  }\n\n  /**\n   * Queries the store for new delta files and applies them to the current state.\n   * Note: the caller should hold `snapshotLock` before calling this method.\n   */\n  protected def updateInternal(\n      isAsync: Boolean,\n      catalogTableOpt: Option[CatalogTable]): Snapshot = {\n    val updateStartTimeMs = clock.getTimeMillis()\n    val previousSnapshot = currentSnapshot.snapshot\n    val commitCoordinatorOpt = populateCommitCoordinator(spark, catalogTableOpt, previousSnapshot)\n    val segmentOpt = createLogSegment(\n      previousSnapshot,\n      catalogTableOpt,\n      commitCoordinatorOpt)\n    val newSnapshot = getUpdatedSnapshot(\n      oldSnapshotOpt = Some(previousSnapshot),\n      initialSegmentForNewSnapshot = segmentOpt,\n      initialTableCommitCoordinatorClient = commitCoordinatorOpt,\n      catalogTableOpt = catalogTableOpt,\n      isAsync = isAsync)\n    installSnapshot(newSnapshot, updateStartTimeMs)\n  }\n\n  /**\n   * Updates and installs a new snapshot in the `currentSnapshot`.\n   * This method takes care of recursively creating new snapshots if the commit-coordinator has\n   * changed.\n   * @param oldSnapshotOpt The previous snapshot, if any.\n   * @param initialSegmentForNewSnapshot the log segment constructed for the new snapshot\n   * @param initialTableCommitCoordinatorClient the commit-coordinator used for constructing the\n   *                                            `initialSegmentForNewSnapshot`\n   * @param catalogTableOpt the optional catalog table to pass to the commit coordinator client.\n   * @param isAsync Whether the update is async.\n   * @return The new snapshot.\n   */\n  protected def getUpdatedSnapshot(\n      oldSnapshotOpt: Option[Snapshot],\n      initialSegmentForNewSnapshot: Option[LogSegment],\n      initialTableCommitCoordinatorClient: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      isAsync: Boolean): Snapshot = {\n    var newSnapshot = getSnapshotForLogSegment(\n      oldSnapshotOpt,\n      initialSegmentForNewSnapshot,\n      initialTableCommitCoordinatorClient,\n      catalogTableOpt,\n      isAsync\n    )\n    // Identify whether the snapshot was created using a \"stale\" commit-coordinator. If yes, we need\n    // to again invoke [[updateSnapshot]] so that we could get the latest commits from the updated\n    // commit-coordinator client. We need to do it only once as the delta spec mandates the commit\n    // which changes the commit-coordinator to be backfilled.\n    val usedStaleCommitCoordinator =\n      newSnapshot.tableCommitCoordinatorClientOpt.exists { newStore =>\n        initialTableCommitCoordinatorClient.forall(!_.semanticsEquals(newStore))\n      }\n    // If the snapshot is catalog owned and if this call site is invoked from initial snapshot\n    // creation, we can only identify that this is CatalogOwned table after reading it from\n    // filesystem. In this case, now we know that this is CatalogOwned table, do another read to get\n    // the commits from the catalog's commit-coordinator.\n    val needToReadSnapshotUsingCatalogCommitCoordinator =\n      newSnapshot.isCatalogOwned && initialTableCommitCoordinatorClient.isEmpty\n    if (usedStaleCommitCoordinator || needToReadSnapshotUsingCatalogCommitCoordinator) {\n      val commitCoordinatorOpt =\n        populateCommitCoordinator(spark, catalogTableOpt, newSnapshot)\n      val segmentOpt = createLogSegment(\n        newSnapshot,\n        catalogTableOpt,\n        commitCoordinatorOpt)\n      newSnapshot = getSnapshotForLogSegment(\n        Some(newSnapshot),\n        segmentOpt,\n        commitCoordinatorOpt,\n        catalogTableOpt,\n        isAsync)\n    }\n    newSnapshot\n  }\n\n\n  /**\n   * Creates a Snapshot for the given `segmentOpt` and handles log segment equality.\n   */\n  protected def getSnapshotForLogSegment(\n      previousSnapshotOpt: Option[Snapshot],\n      segmentOpt: Option[LogSegment],\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      isAsync: Boolean): Snapshot = {\n    val logSegmentsEqual = previousSnapshotOpt.nonEmpty &&\n      segmentOpt.nonEmpty &&\n      previousSnapshotOpt.get.logSegment == segmentOpt.get\n    getSnapshotForLogSegmentInternal(\n        previousSnapshotOpt,\n        segmentOpt,\n        tableCommitCoordinatorClientOpt,\n        catalogTableOpt,\n        previousSnapshotLogSegmentEquals = logSegmentsEqual,\n        isAsync)\n  }\n\n  /**\n   * Creates a Snapshot for the given `segmentOpt`\n   *\n   * @param previousSnapshotOpt The previous snapshot, if any.\n   * @param segmentOpt The log segment to create a snapshot for.\n   * @param tableCommitCoordinatorClientOpt The commit coordinator client to use.\n   * @param catalogTableOpt The catalog table to use.\n   * @param prefetchedCheckpoint The prefetched checkpoint and metadata.\n   * @param previousSnapshotLogSegmentEquals Whether the previous snapshot log segment equals the\n   *                                         given segment. If `previousSnapshotOpt` or `segmentOpt`\n   *                                         is empty, this should be false.\n   * @param isAsync Whether the update is async - if so, the checksum will also be computed.\n   * @return The new snapshot.\n   */\n  protected def getSnapshotForLogSegmentInternal(\n      previousSnapshotOpt: Option[Snapshot],\n      segmentOpt: Option[LogSegment],\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      previousSnapshotLogSegmentEquals: Boolean,\n      isAsync: Boolean): Snapshot = {\n    segmentOpt.map { segment =>\n      if (previousSnapshotLogSegmentEquals) {\n        // If the previous snapshot log segment equals the given segment, the previous snapshot must\n        // be defined.\n        val previousSnapshot = previousSnapshotOpt.get\n        previousSnapshot.updateLastKnownBackfilledVersion(segment.lastBackfilledVersionInSegment)\n        previousSnapshot\n      } else {\n        val newSnapshot = createSnapshot(\n          initSegment = segment,\n          tableCommitCoordinatorClientOpt = tableCommitCoordinatorClientOpt,\n          catalogTableOpt = catalogTableOpt,\n          checksumOpt = None)\n        previousSnapshotOpt.foreach(logMetadataTableIdChange(_, newSnapshot))\n        newSnapshot\n      }\n    }.getOrElse {\n      logInfo(log\"Creating initial snapshot without metadata, because the directory is empty\")\n      new DummySnapshot(logPath, this)\n    }\n  }\n\n  /** Installs the given `newSnapshot` as the `currentSnapshot` */\n  protected def installSnapshot(newSnapshot: Snapshot, updateTimestamp: Long): Snapshot = {\n    if (!snapshotLock.isHeldByCurrentThread) {\n      if (DeltaUtils.isTesting) {\n        throw new RuntimeException(\"DeltaLog snapshot replaced without taking lock\")\n      }\n      recordDeltaEvent(this, \"delta.update.unsafeReplace\")\n    }\n    if (currentSnapshot == null) {\n      // cold snapshot initialization\n      currentSnapshot = CapturedSnapshot(newSnapshot, updateTimestamp)\n      return newSnapshot\n    }\n    val CapturedSnapshot(oldSnapshot, oldTimestamp) = currentSnapshot\n    if (oldSnapshot eq newSnapshot) {\n      // Same snapshot as before, so just refresh the timestamp\n      val timestampToUse = math.max(updateTimestamp, oldTimestamp)\n      currentSnapshot = CapturedSnapshot(newSnapshot, timestampToUse)\n    } else {\n      // Install the new snapshot and uncache the old one\n      currentSnapshot = CapturedSnapshot(newSnapshot, updateTimestamp)\n      oldSnapshot.uncache()\n    }\n    newSnapshot\n  }\n\n  /** Log a change in the metadata's table id whenever we install a newer version of a snapshot */\n  private def logMetadataTableIdChange(previousSnapshot: Snapshot, newSnapshot: Snapshot): Unit = {\n    if (previousSnapshot.version > -1 &&\n      previousSnapshot.metadata.id != newSnapshot.metadata.id) {\n      val msg = s\"Change in the table id detected while updating snapshot. \" +\n        s\"\\nPrevious snapshot = $previousSnapshot\\nNew snapshot = $newSnapshot.\"\n      logWarning(msg)\n      recordDeltaEvent(self, \"delta.metadataCheck.update\", data = Map(\n        \"prevSnapshotVersion\" -> previousSnapshot.version,\n        \"prevSnapshotMetadata\" -> previousSnapshot.metadata,\n        \"nextSnapshotVersion\" -> newSnapshot.version,\n        \"nextSnapshotMetadata\" -> newSnapshot.metadata))\n    }\n  }\n\n  private def logCurrentSnapshot(): Unit = {\n    val CapturedSnapshot(snapshot, updatedTimestamp) = currentSnapshot\n    var logLine = log\"Updated snapshot to ${MDC(DeltaLogKeys.SNAPSHOT, snapshot)}. \"\n    logLine += log\"Updated at: ${MDC(DeltaLogKeys.TIMESTAMP, updatedTimestamp)}. \"\n    // Only check table size when checksum is available to avoid triggering state reconstruction.\n    snapshot.checksumOpt.foreach { checksum =>\n      logLine += log\"Number of files: ${MDC(DeltaLogKeys.NUM_FILES, checksum.numFiles)} files. \"\n      logLine += log\"Table size: ${MDC(DeltaLogKeys.NUM_BYTES, checksum.tableSizeBytes)} bytes. \"\n      val threshold = spark.conf.get(DeltaSQLConf.DELTA_SNAPSHOT_LOGGING_MAX_FILES_THRESHOLD)\n      if (threshold > 0 && checksum.numFiles > threshold) {\n        logWarning(\n          log\"Snapshot at ${MDC(DeltaLogKeys.PATH, logPath)}, \" +\n          log\"version ${MDC(DeltaLogKeys.VERSION, snapshot.version)} has too many files \" +\n          log\"(files: ${MDC(DeltaLogKeys.NUM_FILES, checksum.numFiles)}, \" +\n          log\"threshold: ${MDC(DeltaLogKeys.NUM_FILES, threshold)}). This generally happens \" +\n          log\"when the table is over-partitioned or have lots of small files. Consider fixing \" +\n          log\"the partitioning scheme or running OPTIMIZE on the table.\")\n      }\n    }\n    logInfo(logLine)\n  }\n\n  /**\n   * Creates a snapshot for a new delta commit.\n   */\n  protected def createSnapshotAfterCommit(\n      initSegment: LogSegment,\n      newChecksumOpt: Option[VersionChecksum],\n      tableCommitCoordinatorClientOpt: Option[TableCommitCoordinatorClient],\n      catalogTableOpt: Option[CatalogTable],\n      committedVersion: Long): Snapshot = {\n    logInfo(\n      log\"Creating a new snapshot v${MDC(DeltaLogKeys.VERSION, initSegment.version)} \" +\n        log\"for commit version ${MDC(DeltaLogKeys.VERSION2, committedVersion)}\")\n    // Guard against race condition when a txn commits after this txn but before\n    // reaching createLogSegment(...) above.\n    var checksumContext = \"incrementalCommit\"\n    val passedChecksumIsUsable = newChecksumOpt.isDefined && committedVersion == initSegment.version\n    val snapChecksumOpt = newChecksumOpt\n      .filter(_ => passedChecksumIsUsable)\n      .orElse {\n        checksumContext = \"fallbackToReadChecksumFile\"\n        readChecksum(initSegment.version)\n      }\n\n    def createSnapshotWithCrc(checksumOpt: Option[VersionChecksum]): Snapshot = {\n      createSnapshot(\n        initSegment,\n        tableCommitCoordinatorClientOpt,\n        catalogTableOpt,\n        checksumOpt)\n    }\n\n    var newSnapshot = createSnapshotWithCrc(snapChecksumOpt)\n\n    // Skip validation in 0th commit when number of files in underlying snapshot is 0 in order to\n    // avoid state reconstruction - since there is nothing to verify from allFilesInCrc perspective.\n    val skipValidationForZerothCommit = committedVersion == 0 && newChecksumOpt.forall { crc =>\n      crc.numFiles == 0 && crc.allFiles.forall(_.isEmpty)\n    }\n    if (passedChecksumIsUsable && !skipValidationForZerothCommit &&\n        Snapshot.allFilesInCrcVerificationEnabled(spark, unsafeVolatileSnapshot)) {\n      snapChecksumOpt.collect { case crc if\n          !newSnapshot.validateFileListAgainstCRC(crc, contextOpt = Some(\"triggeredFromCommit\")) =>\n        // If the verification for [[VersionChecksum.allFiles]] failed, then strip off `allFiles`\n        // and create the snapshot again with new CRC (without addFiles in it).\n        newSnapshot = createSnapshotWithCrc(snapChecksumOpt.map(_.copy(allFiles = None)))\n      }\n    }\n\n    // Verify when enabled or when tests run to help future proof IC\n    if (shouldVerifyIncrementalCommit) {\n      val crcIsValid = try {\n        // NOTE: Validation is a no-op with incremental commit disabled.\n        newSnapshot.validateChecksum(Map(\"context\" -> checksumContext))\n      } catch {\n        case e: IllegalStateException if !DeltaUtils.isTesting =>\n          logWarning(log\"Incremental checksum validation failed: \" +\n            log\"${MDC(DeltaLogKeys.ERROR, e.getMessage)}\")\n          false\n      }\n\n      if (!crcIsValid) {\n        // Create snapshot without incremental checksum. This will fallback to creating\n        // a checksum based on state reconstruction. Disable incremental commit to avoid\n        // further error triggers in this session.\n        logWarning(log\"Disabling incremental commit for this session due to checksum \" +\n          log\"validation failure at version \" +\n          log\"${MDC(DeltaLogKeys.VERSION, newSnapshot.version)}\")\n        spark.sessionState.conf.setConf(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED, false)\n        spark.sessionState.conf.setConf(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC, false)\n        return createSnapshotWithCrc(checksumOpt = None)\n      }\n    }\n\n    newSnapshot\n  }\n\n  /**\n   * Called after committing a transaction and updating the state of the table.\n   *\n   * @param committedVersion the version that was committed\n   * @param commit information about the commit file.\n   * @param newChecksumOpt the checksum for the new commit, if available.\n   *                       Usually None, since the commit would have just finished.\n   * @param preCommitLogSegment the log segment of the table prior to commit\n   * @param catalogTableOpt the current catalog table\n   */\n  def updateAfterCommit(\n      committedVersion: Long,\n      commit: Commit,\n      newChecksumOpt: Option[VersionChecksum],\n      preCommitLogSegment: LogSegment,\n      catalogTableOpt: Option[CatalogTable]): Snapshot = {\n    var previousSnapshot: Snapshot = null\n    recordDeltaOperation(this, \"delta.log.updateAfterCommit\") {\n      val updatedSnapshot = withSnapshotLockInterruptibly {\n        val updateTimestamp = clock.getTimeMillis()\n        previousSnapshot = currentSnapshot.snapshot\n        // Somebody else could have already updated the snapshot while we waited for the lock\n        if (committedVersion <= previousSnapshot.version) return previousSnapshot\n        val commitCoordinatorOpt = populateCommitCoordinator(\n          spark, catalogTableOpt, previousSnapshot\n        )\n        val segment = getLogSegmentAfterCommit(\n          committedVersion,\n          newChecksumOpt,\n          preCommitLogSegment,\n          commit,\n          commitCoordinatorOpt,\n          catalogTableOpt,\n          previousSnapshot.checkpointProvider)\n\n        // This likely implies a list-after-write inconsistency\n        if (segment.version < committedVersion) {\n          recordDeltaEvent(this, \"delta.commit.inconsistentList\", data = Map(\n            \"committedVersion\" -> committedVersion,\n            \"currentVersion\" -> segment.version\n          ))\n          throw DeltaErrors.invalidCommittedVersion(committedVersion, segment.version)\n        }\n\n        val newSnapshot = createSnapshotAfterCommit(\n          segment,\n          newChecksumOpt,\n          commitCoordinatorOpt,\n          catalogTableOpt,\n          committedVersion)\n        installSnapshot(newSnapshot, updateTimestamp)\n      }\n      logMetadataTableIdChange(previousSnapshot, updatedSnapshot)\n      logCurrentSnapshot()\n      updatedSnapshot\n    }\n  }\n\n  /** Get the snapshot at `version`. */\n  def getSnapshotAt(\n      version: Long,\n      lastCheckpointHint: Option[CheckpointInstance] = None,\n      catalogTableOpt: Option[CatalogTable] = None,\n      enforceTimeTravelWithinDeletedFileRetention: Boolean = false): Snapshot = {\n    getSnapshotAt(\n      version,\n      lastCheckpointHint,\n      lastCheckpointProvider = None,\n      catalogTableOpt,\n      enforceTimeTravelWithinDeletedFileRetention)\n  }\n\n  /**\n   * Get the snapshot at `version` using the given `lastCheckpointProvider` or `lastCheckpointHint`\n   * as the listing hint.\n   */\n  private[delta] def getSnapshotAt(\n      version: Long,\n      lastCheckpointHint: Option[CheckpointInstance],\n      lastCheckpointProvider: Option[CheckpointProvider],\n      catalogTableOpt: Option[CatalogTable],\n      enforceTimeTravelWithinDeletedFileRetention: Boolean): Snapshot = {\n\n    // See if the version currently cached on the cluster satisfies the requirement\n    val currentSnapshot = unsafeVolatileSnapshot\n    val upperBoundSnapshot = if (currentSnapshot.version >= version) {\n      // current snapshot is already newer than what we are looking for. so it could be used as\n      // upper bound.\n      currentSnapshot\n    } else {\n      val latestSnapshot = update(catalogTableOpt = catalogTableOpt)\n      if (latestSnapshot.version < version) {\n        throwNonExistentVersionError(version)\n      }\n      latestSnapshot\n    }\n    if (upperBoundSnapshot.version == version) {\n      return upperBoundSnapshot\n    }\n\n    val commitCoordinatorOpt = populateCommitCoordinator(spark, catalogTableOpt, upperBoundSnapshot)\n    val (lastCheckpointInfoOpt, lastCheckpointProviderOpt) = lastCheckpointProvider match {\n      // NOTE: We must ignore any hint whose version is higher than the requested version.\n      case Some(checkpointProvider) if checkpointProvider.version <= version =>\n        // Prefer the last checkpoint provider hint, because it doesn't require any I/O to use.\n        None -> Some(checkpointProvider)\n      case _ =>\n        val lastCheckpointInfoForListing = lastCheckpointHint\n            .filter(_.version <= version)\n            .orElse(findLastCompleteCheckpointBefore(version))\n            .map(manuallyLoadCheckpoint)\n        lastCheckpointInfoForListing -> None\n    }\n    val logSegmentOpt = createLogSegment(\n      versionToLoad = Some(version),\n      oldCheckpointProviderOpt = lastCheckpointProviderOpt,\n      tableCommitCoordinatorClientOpt = commitCoordinatorOpt,\n      catalogTableOpt = catalogTableOpt,\n      lastCheckpointInfo = lastCheckpointInfoOpt)\n    val logSegment = logSegmentOpt.getOrElse {\n      // We can't return InitialSnapshot because our caller asked for a specific snapshot version.\n      throw DeltaErrors.logFileNotFoundException(\n        logPath,\n        Some(version),\n        getCheckpointVersion(lastCheckpointInfoOpt, lastCheckpointProviderOpt))\n    }\n    val ret = createSnapshot(\n      initSegment = logSegment,\n      tableCommitCoordinatorClientOpt = commitCoordinatorOpt,\n      catalogTableOpt = catalogTableOpt,\n      checksumOpt = None)\n\n    if (enforceTimeTravelWithinDeletedFileRetention) {\n      enforceTimeTravelWithinDeletedFileRetentionDuration(ret, currentSnapshot)\n    }\n    ret\n  }\n\n  private def enforceTimeTravelWithinDeletedFileRetentionDuration(\n    targetSnapshot: Snapshot, latestSnapshot: Snapshot): Unit = {\n    if (!SparkSession.active.sessionState.conf.getConf(\n      DeltaSQLConf.ENFORCE_TIME_TRAVEL_WITHIN_DELETED_FILE_RETENTION_DURATION)) {\n      return\n    }\n\n    // Skip enforcement for delta-sharing tables since they create a faked delta-log\n    // where the version timestamp may be set to 0.\n    val deltasharingLogFileSystemSchema =\n      // Cannot import io.delta.sharing.spark.DeltaSharingLogFileSystemConstants.SCHEME\n      // in the current (spark) module since sharing depends on spark; falling back to\n      // string comparison.\n      \"delta-sharing-log\"\n    if (logPath.toUri.getScheme == deltasharingLogFileSystemSchema) {\n      return\n    }\n\n    // Time travel to the latest version is always allowed\n    if (targetSnapshot.version == latestSnapshot.version) return\n\n    val deletedFileRetentionDuration = DeltaLog.tombstoneRetentionMillis(latestSnapshot.metadata)\n    val currentTime = clock.getTimeMillis()\n    if (targetSnapshot.timestamp < (currentTime - deletedFileRetentionDuration)) {\n      recordDeltaEvent(this, s\"delta.timeTravel.fail\", data = Map(\n        // Log the cached version of the table on the cluster\n        \"latestVersion\" -> latestSnapshot.version,\n        \"queriedVersion\" -> targetSnapshot.version,\n        \"currentTimestamp\" -> currentTime,\n        \"targetSnapshotTimestamp\" -> targetSnapshot.timestamp,\n        \"deletedFileRetentionDuration\" -> deletedFileRetentionDuration\n      ))\n      throw DeltaErrors.timeTravelBeyondDeletedFileRetentionDurationException(\n        deletedFileRetentionDuration.millis.toHours.toString)\n    }\n  }\n\n  // Populate commit coordinator using catalogOpt if the snapshot is catalog owned.\n  protected def populateCommitCoordinator(\n    spark: SparkSession, catalogTableOpt: Option[CatalogTable], snapshot: Snapshot)\n  : Option[TableCommitCoordinatorClient] = {\n    if (snapshot.isCatalogOwned) {\n      CatalogOwnedTableUtils.populateTableCommitCoordinatorFromCatalog(\n        spark,\n        catalogTableOpt,\n        snapshot\n      )\n    } else {\n      snapshot.tableCommitCoordinatorClientOpt\n    }\n  }\n\n  // Visible for testing\n  private[delta] def getCapturedSnapshot(): CapturedSnapshot = currentSnapshot\n}\n\nobject SnapshotManagement extends DeltaLogging {\n  // A thread pool for reading checkpoint files and collecting checkpoint v2 actions like\n  // checkpointMetadata, sidecarFiles.\n  private[delta] lazy val checkpointV2ThreadPool = {\n    val numThreads = SparkSession.active.sessionState.conf.getConf(\n      DeltaSQLConf.CHECKPOINT_V2_DRIVER_THREADPOOL_PARALLELISM)\n    DeltaThreadPool(\"checkpointV2-threadpool\", numThreads)\n  }\n\n  protected[delta] lazy val deltaLogAsyncUpdateThreadPool = {\n    val tpe = ThreadUtils.newDaemonCachedThreadPool(\"delta-state-update\", 8)\n    new DeltaThreadPool(tpe)\n  }\n\n  private lazy val commitCoordinatorGetCommitsThreadPool = {\n    val numThreads = SparkSession.active.sessionState.conf\n      .getConf(DeltaSQLConf.COORDINATED_COMMITS_GET_COMMITS_THREAD_POOL_SIZE)\n    val tpe = ThreadUtils.newDaemonCachedThreadPool(\"commit-coordinator-get-commits\", numThreads)\n    new DeltaThreadPool(tpe)\n  }\n\n  /**\n   * - Verify the versions are contiguous.\n   * - Verify the versions start with `expectedStartVersion` if it's specified.\n   * - Verify the versions end with `expectedEndVersion` if it's specified.\n   */\n  def verifyDeltaVersions(\n      spark: SparkSession,\n      versions: Array[Long],\n      expectedStartVersion: Option[Long],\n      expectedEndVersion: Option[Long],\n      cachedSnapshot: Option[Snapshot]): Unit = {\n    if (versions.nonEmpty) {\n      // Turn this to a vector so that we can compare it with a range.\n      val deltaVersions = versions.toVector\n      if ((deltaVersions.head to deltaVersions.last) != deltaVersions) {\n        // [[cachedSnapshot]] maybe null (e.g., uninitialized snapshot being passed in)\n        // in some cases, which needs to be explicitly filtered out.\n        val snapshot = cachedSnapshot.filter(_ != null)\n        recordDeltaEvent(\n          deltaLog = null,\n          opType = \"delta.exceptions.deltaVersionsNotContiguous\",\n          data = Map(\n            // Remove the first element of the stack trace since this represents\n            // the [[Thread.getStackTrace]] call itself.\n            \"stackTrace\" -> Thread.currentThread().getStackTrace.tail.mkString(\"\\n\\t\"),\n            \"startVersion\" -> deltaVersions.head,\n            \"endVersion\" -> deltaVersions.last,\n            \"versionToLoad\" -> expectedEndVersion.getOrElse(-1L),\n            \"unsafeVolatileSnapshot.latestCheckpointVersion\" ->\n              snapshot.map(_.checkpointProvider.version).getOrElse(-1L),\n            \"unsafeVolatileSnapshot.latestSnapshotVersion\" ->\n              snapshot.map(_.version).getOrElse(-1L),\n            \"unsafeVolatileSnapshot.checksumOpt\" ->\n              snapshot.map(_.checksumOpt).orNull\n          ))\n        throw DeltaErrors.deltaVersionsNotContiguousException(\n          spark = spark,\n          deltaVersions = deltaVersions,\n          startVersion = deltaVersions.head,\n          endVersion = deltaVersions.last,\n          // `expectedEndVersion` is the version we'd like to construct/load the [[Snapshot]],\n          // pass -1L if it's not available/specified.\n          versionToLoad = expectedEndVersion.getOrElse(-1L))\n      }\n    }\n    expectedStartVersion.foreach { v =>\n      require(versions.nonEmpty && versions.head == v, \"Did not get the first delta \" +\n        s\"file version: $v to compute Snapshot\")\n    }\n    expectedEndVersion.foreach { v =>\n      require(versions.nonEmpty && versions.last == v, \"Did not get the last delta \" +\n        s\"file version: $v to compute Snapshot\")\n    }\n  }\n\n  def appendCommitToLogSegment(\n      oldLogSegment: LogSegment,\n      commitFileStatus: FileStatus,\n      committedVersion: Long): LogSegment = {\n    require(oldLogSegment.version + 1 == committedVersion)\n    oldLogSegment.copy(\n      version = committedVersion,\n      deltas = oldLogSegment.deltas :+ commitFileStatus,\n      lastCommitFileModificationTimestamp = commitFileStatus.getModificationTime)\n  }\n}\n\n/** A serializable variant of HDFS's FileStatus. */\ncase class SerializableFileStatus(\n    path: String,\n    length: Long,\n    isDir: Boolean,\n    modificationTime: Long) {\n\n  // Important note! This is very expensive to compute, but we don't want to cache it\n  // as a `val` because Paths internally contain URIs and therefore consume lots of memory.\n  @JsonIgnore\n  def getHadoopPath: Path = new Path(path)\n\n  def toFileStatus: FileStatus = {\n    new FileStatus(length, isDir, 0, 0, modificationTime, new Path(path))\n  }\n\n  override def equals(obj: Any): Boolean = obj match {\n    // We only compare the paths to stay consistent with FileStatus.equals.\n    case other: SerializableFileStatus => Objects.equals(path, other.path)\n    case _ => false\n  }\n\n  // We only use the path to stay consistent with FileStatus.hashCode.\n  override def hashCode(): Int = Objects.hashCode(path)\n}\n\nobject SerializableFileStatus {\n  def fromStatus(status: FileStatus): SerializableFileStatus = {\n    SerializableFileStatus(\n      Option(status.getPath).map(_.toString).orNull,\n      status.getLen,\n      status.isDirectory,\n      status.getModificationTime)\n  }\n\n  val EMPTY: SerializableFileStatus = fromStatus(new FileStatus())\n}\n\n/**\n * Provides information around which files in the transaction log need to be read to create\n * the given version of the log.\n *\n * @param logPath The path to the _delta_log directory\n * @param version The Snapshot version to generate\n * @param deltas The delta commit files (.json) to read\n * @param checkpointProvider provider to give information about Checkpoint files.\n * @param lastCommitFileModificationTimestamp The \"unadjusted\" file modification timestamp of the\n *          last commit within this segment. By unadjusted, we mean that the commit timestamps may\n *          not necessarily be monotonically increasing for the commits within this segment.\n */\ncase class LogSegment(\n    logPath: Path,\n    version: Long,\n    deltas: Seq[FileStatus],\n    checkpointProvider: UninitializedCheckpointProvider,\n    lastCommitFileModificationTimestamp: Long) {\n\n  override def hashCode(): Int =\n    logPath.hashCode() * 31 + (lastCommitFileModificationTimestamp % 10000).toInt\n\n  /**\n   * An efficient way to check if a cached Snapshot's contents actually correspond to a new\n   * segment returned through file listing.\n   */\n  override def equals(obj: Any): Boolean = {\n    obj match {\n      case other: LogSegment =>\n        version == other.version &&\n          logPath == other.logPath &&\n          checkpointProvider.version == other.checkpointProvider.version &&\n          lastMatchingBackfilledCommitIsEqual(other)\n      case _ => false\n    }\n  }\n\n  private def lastMatchingBackfilledCommitIsEqual(other: LogSegment): Boolean = {\n    def fileStatusEquals(fileStatus1: FileStatus, fileStatus2: FileStatus): Boolean = {\n      fileStatus1.getPath == fileStatus2.getPath &&\n        fileStatus1.getLen == fileStatus2.getLen &&\n        fileStatus1.getModificationTime == fileStatus2.getModificationTime\n    }\n\n    val backfilledPrefixThis = deltas.takeWhile(isBackfilledDeltaFile)\n    val backfilledPrefixOther = other.deltas.takeWhile(isBackfilledDeltaFile)\n    val sizeToAnalyze = math.min(backfilledPrefixThis.size, backfilledPrefixOther.size)\n    val backfilledPrefixThisStripped = backfilledPrefixThis.take(sizeToAnalyze)\n    val backfilledPrefixOtherStripped = backfilledPrefixOther.take(sizeToAnalyze)\n    backfilledPrefixThisStripped.zip(backfilledPrefixOtherStripped)\n      .forall { case (delta1, delta2) => fileStatusEquals(delta1, delta2) } &&\n      checkpointProvider.topLevelFiles.size == other.checkpointProvider.topLevelFiles.size &&\n      checkpointProvider.topLevelFiles.zip(other.checkpointProvider.topLevelFiles).forall {\n        case (cp1, cp2) => fileStatusEquals(cp1, cp2)\n      }\n  }\n\n  private[delta] lazy val lastBackfilledVersionInSegment =\n    // This works if the last backfilled file is a minor-compaction, because\n    // FileNames.getFileVersion returns the minor-compaction end version,\n    // which correctly initializes the lastBackfilledVersionInSegment.\n    CoordinatedCommitsUtils.getLastBackfilledFile(deltas).map(getFileVersion)\n      .getOrElse(checkpointProvider.version)\n}\n\n/** Exception thrown When [[TableCommitCoordinatorClient.getCommits]] fails due to any reason. */\nclass CommitCoordinatorGetCommitsFailedException(cause: Throwable) extends Exception(cause)\n\nobject LogSegment {\n\n  def apply(\n      logPath: Path,\n      version: Long,\n      deltas: Seq[FileStatus],\n      checkpointProviderOpt: Option[UninitializedCheckpointProvider],\n      lastCommitTimestamp: Long): LogSegment = {\n    val checkpointProvider = checkpointProviderOpt.getOrElse(EmptyCheckpointProvider)\n    LogSegment(logPath, version, deltas, checkpointProvider, lastCommitTimestamp)\n  }\n\n  /** The LogSegment for an empty transaction log directory. */\n  def empty(path: Path): LogSegment = LogSegment(\n    logPath = path,\n    version = -1L,\n    deltas = Nil,\n    checkpointProviderOpt = None,\n    lastCommitTimestamp = -1L)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/SnapshotState.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol, SetTransaction}\nimport org.apache.spark.sql.delta.actions.DomainMetadata\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeletedRecordCountsHistogram\nimport org.apache.spark.sql.delta.stats.DeletedRecordCountsHistogramUtils\nimport org.apache.spark.sql.delta.stats.FileSizeHistogram\n\nimport org.apache.spark.sql.{Column, DataFrame}\nimport org.apache.spark.sql.functions.{coalesce, col, collect_set, count, last, lit, sum, when}\nimport org.apache.spark.util.Utils\n\n\n/**\n * Metrics and metadata computed around the Delta table.\n *\n * @param sizeInBytes The total size of the table (of active files, not including tombstones).\n * @param numOfSetTransactions Number of streams writing to this table.\n * @param numOfFiles The number of files in this table.\n * @param numOfRemoves The number of tombstones in the state.\n * @param numDeletedRecordsOpt The total number of records deleted with Deletion Vectors.\n * @param numDeletionVectorsOpt The number of Deletion Vectors present in the table.\n * @param numOfMetadata The number of metadata actions in the state. Should be 1.\n * @param numOfProtocol The number of protocol actions in the state. Should be 1.\n * @param setTransactions The streaming queries writing to this table.\n * @param metadata The metadata of the table.\n * @param protocol The protocol version of the Delta table.\n * @param fileSizeHistogram A Histogram class tracking the file counts and total bytes\n *                          in different size ranges.\n * @param deletedRecordCountsHistogramOpt A histogram of deletion records counts distribution\n *                                        for all files.\n */\ncase class SnapshotState(\n  sizeInBytes: Long,\n  numOfSetTransactions: Long,\n  numOfFiles: Long,\n  numOfRemoves: Long,\n  numDeletedRecordsOpt: Option[Long],\n  numDeletionVectorsOpt: Option[Long],\n  numOfMetadata: Long,\n  numOfProtocol: Long,\n  setTransactions: Seq[SetTransaction],\n  domainMetadata: Seq[DomainMetadata],\n  metadata: Metadata,\n  protocol: Protocol,\n  fileSizeHistogram: Option[FileSizeHistogram] = None,\n  deletedRecordCountsHistogramOpt: Option[DeletedRecordCountsHistogram] = None\n)\n\n/**\n * A helper class that manages the SnapshotState for a given snapshot. Will generate it only\n * when necessary.\n */\ntrait SnapshotStateManager extends DeltaLogging { self: Snapshot =>\n\n  // For implicits which re-use Encoder:\n  import implicits._\n  /** Whether computedState is already computed or not */\n  @volatile protected var _computedStateTriggered: Boolean = false\n\n\n  /** A map to look up transaction version by appId. */\n  lazy val transactions: Map[String, Long] = setTransactions.map(t => t.appId -> t.version).toMap\n\n  /**\n   * Compute the SnapshotState of a table. Uses the stateDF from the Snapshot to extract\n   * the necessary stats.\n   */\n  protected lazy val computedState: SnapshotState = {\n    withStatusCode(\"DELTA\", s\"Compute snapshot for version: $version\") {\n      recordFrameProfile(\"Delta\", \"snapshot.computedState\") {\n        val startTime = System.nanoTime()\n        val _computedState = extractComputedState(stateDF)\n        if (_computedState.protocol == null) {\n          recordDeltaEvent(\n            deltaLog,\n            opType = \"delta.assertions.missingAction\",\n            data = Map(\n              \"version\" -> version.toString, \"action\" -> \"Protocol\", \"source\" -> \"Snapshot\"))\n          throw DeltaErrors.actionNotFoundException(\"protocol\", version)\n        } else if (_computedState.protocol != protocol) {\n          recordDeltaEvent(\n            deltaLog,\n            opType = \"delta.assertions.mismatchedAction\",\n            data = Map(\n              \"version\" -> version.toString, \"action\" -> \"Protocol\", \"source\" -> \"Snapshot\",\n              \"computedState.protocol\" -> _computedState.protocol,\n              \"extracted.protocol\" -> protocol))\n          throw DeltaErrors.actionNotFoundException(\"protocol\", version)\n        }\n\n        if (_computedState.metadata == null) {\n          recordDeltaEvent(\n            deltaLog,\n            opType = \"delta.assertions.missingAction\",\n            data = Map(\n              \"version\" -> version.toString, \"action\" -> \"Metadata\", \"source\" -> \"Metadata\"))\n          throw DeltaErrors.actionNotFoundException(\"metadata\", version)\n        } else if (_computedState.metadata != metadata) {\n          recordDeltaEvent(\n            deltaLog,\n            opType = \"delta.assertions.mismatchedAction\",\n            data = Map(\n              \"version\" -> version.toString, \"action\" -> \"Metadata\", \"source\" -> \"Snapshot\",\n              \"computedState.metadata\" -> _computedState.metadata,\n              \"extracted.metadata\" -> metadata))\n          throw DeltaErrors.actionNotFoundException(\"metadata\", version)\n        }\n\n        _computedStateTriggered = true\n        _computedState\n      }\n    }\n  }\n\n  /**\n   * Extract the SnapshotState from the provided dataframe of actions. Requires that the dataframe\n   * has already been deduplicated (either through logReplay or some other method).\n   */\n  protected def extractComputedState(stateDF: DataFrame): SnapshotState = {\n    recordFrameProfile(\"Delta\", \"snapshot.computedState.aggregations\") {\n      val aggregations =\n        aggregationsToComputeState.map { case (alias, agg) => agg.as(alias) }.toSeq\n      stateDF.select(aggregations: _*).as[SnapshotState].first()\n    }\n  }\n\n  /**\n   * A Map of alias to aggregations which needs to be done to calculate the `computedState`\n   */\n  protected def aggregationsToComputeState: Map[String, Column] = {\n    val checksumDVMetricsEnabled =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CHECKSUM_DV_METRICS_ENABLED)\n    val deletedRecordCountsHistogramEnabled =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_DELETED_RECORD_COUNTS_HISTOGRAM_ENABLED)\n    lazy val persistentDVsOnTableSupported =\n      DeletionVectorUtils.deletionVectorsWritable(this)\n\n    val computeChecksumDVMetrics = checksumDVMetricsEnabled &&\n      persistentDVsOnTableSupported\n    val persistentDVsAggs =\n      if (computeChecksumDVMetrics) {\n        Map(\n          \"numDeletedRecordsOpt\" -> sum(coalesce(col(\"add.deletionVector.cardinality\"), lit(0L))),\n          \"numDeletionVectorsOpt\" -> count(col(\"add.deletionVector\")))\n      } else {\n        Map(\"numDeletedRecordsOpt\" -> lit(null), \"numDeletionVectorsOpt\" -> lit(null))\n      }\n\n    val histogramDVsAggExpr = if (computeChecksumDVMetrics && deletedRecordCountsHistogramEnabled) {\n      DeletedRecordCountsHistogramUtils.histogramAggregate(\n        when(col(\"add\").isNotNull, coalesce(col(\"add.deletionVector.cardinality\"), lit(0L))))\n    } else {\n      lit(null).cast(DeletedRecordCountsHistogram.schema)\n    }\n\n    val histogramDVsAgg = Seq(\"deletedRecordCountsHistogramOpt\" -> histogramDVsAggExpr)\n\n    Map(\n      // sum may return null for empty data set.\n      \"sizeInBytes\" -> coalesce(sum(col(\"add.size\")), lit(0L)),\n      \"numOfSetTransactions\" -> count(col(\"txn\")),\n      \"numOfFiles\" -> count(col(\"add\")),\n      \"numOfRemoves\" -> count(col(\"remove\")),\n      \"numOfMetadata\" -> count(col(\"metaData\")),\n      \"numOfProtocol\" -> count(col(\"protocol\")),\n      \"setTransactions\" -> collect_set(col(\"txn\")),\n      \"domainMetadata\" -> collect_set(col(\"domainMetadata\")),\n      \"metadata\" -> last(col(\"metaData\"), ignoreNulls = true),\n      \"protocol\" -> last(col(\"protocol\"), ignoreNulls = true),\n      \"fileSizeHistogram\" -> lit(null).cast(FileSizeHistogram.schema)\n    ) ++ persistentDVsAggs ++ histogramDVsAgg\n  }\n\n  /**\n   * The following is a list of convenience methods for accessing the computedState.\n   */\n  def sizeInBytes: Long = computedState.sizeInBytes\n  def numOfSetTransactions: Long = computedState.numOfSetTransactions\n  def numOfFiles: Long = computedState.numOfFiles\n  def numOfRemoves: Long = computedState.numOfRemoves\n  def numOfMetadata: Long = computedState.numOfMetadata\n  def numOfProtocol: Long = computedState.numOfProtocol\n  def setTransactions: Seq[SetTransaction] = computedState.setTransactions\n  def fileSizeHistogram: Option[FileSizeHistogram] = computedState.fileSizeHistogram\n  def domainMetadata: Seq[DomainMetadata] = computedState.domainMetadata\n  protected[delta] def sizeInBytesIfKnown: Option[Long] = Some(sizeInBytes)\n  protected[delta] def setTransactionsIfKnown: Option[Seq[SetTransaction]] = Some(setTransactions)\n  protected[delta] def numOfFilesIfKnown: Option[Long] = Some(numOfFiles)\n  protected[delta] def domainMetadatasIfKnown: Option[Seq[DomainMetadata]] = Some(domainMetadata)\n  def numDeletedRecordsOpt: Option[Long] = computedState.numDeletedRecordsOpt\n  def numDeletionVectorsOpt: Option[Long] = computedState.numDeletionVectorsOpt\n  def deletedRecordCountsHistogramOpt: Option[DeletedRecordCountsHistogram] =\n    computedState.deletedRecordCountsHistogramOpt\n\n  protected def deletionVectorsReadableAndMetricsEnabled: Boolean = {\n    val checksumDVMetricsEnabled =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CHECKSUM_DV_METRICS_ENABLED)\n    val dvsReadable = DeletionVectorUtils.deletionVectorsReadable(snapshotToScan)\n    checksumDVMetricsEnabled && dvsReadable\n  }\n\n  protected def deletionVectorsReadableAndHistogramEnabled: Boolean = {\n    val deletedRecordCountsHistogramEnabled =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_DELETED_RECORD_COUNTS_HISTOGRAM_ENABLED)\n    deletionVectorsReadableAndMetricsEnabled && deletedRecordCountsHistogramEnabled\n  }\n\n  /** Generate a default SnapshotState of a new table given the table metadata and the protocol. */\n  protected def initialState(metadata: Metadata, protocol: Protocol): SnapshotState = {\n    val deletedRecordCountsHistogramOpt = if (spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_DELETED_RECORD_COUNTS_HISTOGRAM_ENABLED)) {\n      Some(DeletedRecordCountsHistogramUtils.emptyHistogram)\n    } else None\n\n    SnapshotState(\n      sizeInBytes = 0L,\n      numOfSetTransactions = 0L,\n      numOfFiles = 0L,\n      numOfRemoves = 0L,\n      // DV metrics are initialized to Some(0) to allow incremental computation. For tables where\n      // DVs are disabled, there are turned to None by the incremental computation.\n      numDeletedRecordsOpt = Some(0),\n      numDeletionVectorsOpt = Some(0),\n      numOfMetadata = 1L,\n      numOfProtocol = 1L,\n      setTransactions = Nil,\n      domainMetadata = Nil,\n      metadata = metadata,\n      protocol = protocol,\n      deletedRecordCountsHistogramOpt = deletedRecordCountsHistogramOpt\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/SubqueryTransformerHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.catalyst.expressions.SubqueryExpression\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Subquery, SupportsSubquery}\n\n/**\n * Trait to allow processing a special transformation of [[SubqueryExpression]]\n * instances in a query plan.\n */\ntrait SubqueryTransformerHelper {\n\n  /**\n   * Transform all nodes matched by the rule in the query plan rooted at given `plan`.\n   * It traverses the tree starting from the leaves, whenever a [[SubqueryExpression]]\n   * expression is encountered, given [[rule]] is applied to the subquery plan `plan`\n   * in [[SubqueryExpression]] starting from the `plan` root until leaves.\n   *\n   * This is slightly different behavior compared to [[QueryPlan.transformUpWithSubqueries]]\n   * or [[QueryPlan.transformDownWithSubqueries]]\n   *\n   * It requires that the given plan already gone through [[OptimizeSubqueries]] and the\n   * root node denoting a subquery is removed and optimized appropriately.\n   */\n  def transformWithSubqueries(plan: LogicalPlan)\n      (rule: PartialFunction[LogicalPlan, LogicalPlan]): LogicalPlan = {\n    require(!isSubqueryRoot(plan))\n    transformSubqueries(plan, rule) transform (rule)\n  }\n\n  /** Is the give plan a subquery root. */\n  def isSubqueryRoot(plan: LogicalPlan): Boolean = {\n    plan.isInstanceOf[Subquery] || plan.isInstanceOf[SupportsSubquery]\n  }\n\n  private def transformSubqueries(\n      plan: LogicalPlan,\n      rule: PartialFunction[LogicalPlan, LogicalPlan]): LogicalPlan = {\n    import org.apache.spark.sql.delta.implicits._\n\n    plan transformAllExpressionsUp {\n      case subquery: SubqueryExpression =>\n        subquery.withNewPlan(transformWithSubqueries(subquery.plan)(rule))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/TableFeature.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Locale\n\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillCommand\nimport org.apache.spark.sql.delta.constraints.{Constraints, Invariants}\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsUtils\nimport org.apache.spark.sql.delta.redirect.{RedirectReaderWriter, RedirectWriterOnly}\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.FileNames\n\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.TimestampNTZType\n\n/* --------------------------------------- *\n |  Table features base class definitions  |\n * --------------------------------------- */\n\n/**\n * A base class for all table features.\n *\n * A feature can be <b>explicitly supported</b> by a table's protocol when the protocol contains a\n * feature's `name`. Writers (for writer-only features) or readers and writers (for reader-writer\n * features) must recognize supported features and must handle them appropriately.\n *\n * A table feature that released before Delta Table Features (reader version 3 and writer version\n * 7) is considered as a <b>legacy feature</b>. Legacy features are <b>implicitly supported</b>\n * when (a) the protocol does not support table features, i.e., has reader version less than 3 or\n * writer version less than 7 and (b) the feature's minimum reader/writer version is less than or\n * equal to the current protocol's reader/writer version.\n *\n * Separately, a feature can be automatically supported by a table's metadata when certain\n * feature-specific table properties are set. For example, `changeDataFeed` is automatically\n * supported when there's a table property `delta.enableChangeDataFeed=true`. This is independent\n * of the table's enabled features. When a feature is supported (explicitly or implicitly) by the\n * table protocol but its metadata requirements are not satisfied, then clients still have to\n * understand the feature (at least to the extent that they can read and preserve the existing\n * data in the table that uses the feature). See the documentation of\n * [[FeatureAutomaticallyEnabledByMetadata]] for more information.\n *\n * @param name\n *   a globally-unique string indicator to represent the feature. All characters must be letters\n *   (a-z, A-Z), digits (0-9), '-', or '_'. Words must be in camelCase.\n * @param minReaderVersion\n *   the minimum reader version this feature requires. For a feature that can only be explicitly\n *   supported, this is either `0` or `3` (the reader protocol version that supports table\n *   features), depending on the feature is writer-only or reader-writer. For a legacy feature\n *   that can be implicitly supported, this is the first protocol version which the feature is\n *   introduced.\n * @param minWriterVersion\n *   the minimum writer version this feature requires. For a feature that can only be explicitly\n *   supported, this is the writer protocol `7` that supports table features. For a legacy feature\n *   that can be implicitly supported, this is the first protocol version which the feature is\n *   introduced.\n */\n// @TODO: distinguish Delta and 3rd-party features and give appropriate error messages\nsealed abstract class TableFeature(\n    val name: String,\n    val minReaderVersion: Int,\n    val minWriterVersion: Int) extends java.io.Serializable {\n\n  require(name.forall(c => c.isLetterOrDigit || c == '-' || c == '_'))\n\n  /**\n   * Get a [[Protocol]] object stating the minimum reader and writer versions this feature\n   * requires. For a feature that can only be explicitly supported, this method returns a protocol\n   * version that supports table features, either `(0,7)` or `(3,7)` depending on the feature is\n   * writer-only or reader-writer. For a legacy feature that can be implicitly supported, this\n   * method returns the first protocol version which introduced the said feature.\n   *\n   * For all features, if the table's protocol version does not support table features, then the\n   * minimum protocol version is enough. However, if the protocol version supports table features\n   * for the feature type (writer-only or reader-writer), then the minimum protocol version is not\n   * enough to support a feature. In this case the feature must also be explicitly listed in the\n   * appropriate feature sets in the [[Protocol]].\n   */\n  def minProtocolVersion: Protocol = Protocol(minReaderVersion, minWriterVersion)\n\n  /** Determine if this feature applies to both readers and writers. */\n  def isReaderWriterFeature: Boolean = this.isInstanceOf[ReaderWriterFeatureType]\n\n  /**\n   * Determine if this feature is a legacy feature. See the documentation of [[TableFeature]] for\n   * more information.\n   */\n  def isLegacyFeature: Boolean = this.isInstanceOf[LegacyFeatureType]\n\n  /**\n   * True if this feature can be removed.\n   */\n  def isRemovable: Boolean = this.isInstanceOf[RemovableFeature]\n\n  /**\n   * True if the addition of this feature in the protocol is expected to fail concurrent\n   * transactions. This is desirable for features that are implicitly enabled by being present\n   * in the protocol, and also impose write-time requirements that need to be respected by all\n   * writers beyond the protocol upgrade. Note that features that do reconciliation at conflict\n   * checking time (e.g. RowTrackingFeature) should return false.\n   */\n  def failConcurrentTransactionsAtUpgrade: Boolean = true\n\n  /**\n   * Set of table features that this table feature depends on. I.e. the set of features that need\n   * to be enabled if this table feature is enabled.\n   */\n  def requiredFeatures: Set[TableFeature] = Set.empty\n}\n\n/** A trait to indicate a feature applies to readers and writers. */\nsealed trait ReaderWriterFeatureType\n\n/** A trait to indicate a feature is legacy, i.e., released before Table Features. */\nsealed trait LegacyFeatureType\n\n/**\n * A trait indicating this feature can be automatically enabled via a change in a table's\n * metadata, e.g., through setting particular values of certain feature-specific table properties.\n *\n * When the feature's metadata requirements are satisfied for <b>new tables</b>, or for\n * <b>existing tables when [[automaticallyUpdateProtocolOfExistingTables]] set to `true`</b>, the\n * client will silently add the feature to the protocol's `readerFeatures` and/or\n * `writerFeatures`. Otherwise, a proper protocol version bump must be present in the same\n * transaction.\n */\nsealed trait FeatureAutomaticallyEnabledByMetadata { this: TableFeature =>\n\n  /**\n   * Whether the feature can automatically update the protocol of an existing table when the\n   * metadata requirements are satisfied. As a rule of thumb, a table feature that requires\n   * explicit operations (e.g., turning on a table property) should set this flag to `true`, while\n   * features that are used implicitly (e.g., when using a new data type) should set this flag to\n   * `false`.\n   */\n  def automaticallyUpdateProtocolOfExistingTables: Boolean = this.isLegacyFeature\n\n  /**\n   * Determine whether the feature must be supported and enabled because its metadata requirements\n   * are satisfied.\n   */\n  def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean\n\n  require(\n    !this.isLegacyFeature || automaticallyUpdateProtocolOfExistingTables,\n    \"Legacy feature must be auto-update capable.\")\n}\n\n/**\n * A trait indicating a feature can be removed. Classes that extend the trait need to\n * implement the following four functions:\n *\n * a) preDowngradeCommand. This is where all required actions for removing the feature are\n *    implemented. For example, to remove the DVs feature we need to remove metadata config\n *    and purge all DVs from table. This action takes place before the protocol downgrade in\n *    separate commit(s). Note, the command needs to be implemented in a way concurrent\n *    transactions do not nullify the effect. For example, disabling DVs on a table before\n *    purging will stop concurrent transactions from adding DVs. During protocol downgrade\n *    we perform a validation in [[validateDropInvariants]] to make sure all invariants still hold.\n *\n * b) validateDropInvariants. Add any feature-specific checks before proceeding to the protocol\n *    downgrade. This function is guaranteed to be called at the latest version before the\n *    protocol downgrade is committed to the table. When the protocol downgrade txn conflicts,\n *    the validation is repeated against the winning txn snapshot. As soon as the protocol\n *    downgrade succeeds, all subsequent interleaved txns are aborted.\n *    The implementation should return true if there are no feature traces in the latest\n *    version. False otherwise.\n *\n * c) requiresHistoryProtection. It indicates whether the feature leaves traces in the table\n *    history that may result in incorrect behaviour if the table is read/written by a client\n *    that does not support the feature. This is by default true for all reader+writer features\n *    and false for writer features.\n *    WARNING: Disabling [[requiresHistoryProtection]] for relevant features could result in\n *    incorrect snapshot reconstruction.\n *\n * d) actionUsesFeature. For features that require history truncation we verify whether past\n *    versions contain any traces of the removed feature. This is achieved by calling\n *    [[actionUsesFeature]] for every action of every reachable commit version in the log.\n *    Note, a feature may leave traces in both data and metadata. Depending on the feature, we\n *    need to check several types of actions such as Metadata, AddFile, RemoveFile etc.\n *\n *    WARNING: actionUsesFeature should not check Protocol actions for the feature being removed,\n *    because at the time actionUsesFeature is invoked the protocol downgrade did not happen yet.\n *    Thus, the feature-to-remove is still active. As a result, any unrelated operations that\n *    produce a protocol action (while we are waiting for the retention period to expire) will\n *    \"carry\" the feature-to-remove. Checking protocol for that feature would result in an\n *    unnecessary failure during the history validation of the next DROP FEATURE call. Note,\n *    while the feature-to-remove is supported in the protocol we cannot generate a legit protocol\n *    action that adds support for that feature since it is already supported.\n *\n *    Furthermore, methods `tablePropertiesToRemoveAtDowngradeCommit` and\n *    `actionsToIncludeAtDowngradeCommit` can be optionally implemented. They can be used for\n *    defining properties/actions that need to be removed/included at the protocol downgrade\n *    commit.\n */\nsealed trait RemovableFeature { self: TableFeature =>\n  def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand\n  def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean\n  def requiresHistoryProtection: Boolean = isReaderWriterFeature\n  def actionUsesFeature(action: Action): Boolean\n  def tablePropertiesToRemoveAtDowngradeCommit: Seq[String] = Seq.empty\n  def actionsToIncludeAtDowngradeCommit(snapshot: Snapshot): Seq[Action] = Seq.empty\n\n\n  /**\n   * Examines all historical commits for traces of the removableFeature.\n   * This is achieved as follows:\n   *\n   * 1) We find the earliest valid checkpoint, recreate a snapshot at that version and we check\n   *    whether there any traces of the feature-to-remove.\n   * 2) We check all commits that exist between version 0 and the current version.\n   *    This includes the versions we validated the snapshots. This is because a commit\n   *    might include information that is not available in the snapshot. Examples include\n   *    CommitInfo, CDCInfo etc. Note, there can still be valid log commit files with\n   *    versions prior the earliest checkpoint version.\n   * 3) We do not need to recreate a snapshot at the current version because this is already being\n   *    handled by validateDropInvariants.\n   *\n   * Note, this is a slow process.\n   *\n   * @param spark The SparkSession.\n   * @param downgradeTxnReadSnapshot The read snapshot of the protocol downgrade transaction.\n   * @return True if the history contains any trace of the feature.\n   */\n  def historyContainsFeature(\n      spark: SparkSession,\n      table: DeltaTableV2,\n      downgradeTxnReadSnapshot: Snapshot): Boolean = {\n    require(requiresHistoryProtection)\n    val deltaLog = downgradeTxnReadSnapshot.deltaLog\n    val earliestCheckpointVersion = deltaLog.findEarliestReliableCheckpoint.getOrElse(0L)\n    val toVersion = downgradeTxnReadSnapshot.version\n\n    // Use the snapshot at earliestCheckpointVersion to validate the checkpoint identified by\n    // findEarliestReliableCheckpoint.\n    val earliestSnapshot = table.getSnapshotAt(earliestCheckpointVersion)\n\n    // Tombstones may contain traces of the removed feature. The earliest snapshot will include\n    // all tombstones within the tombstoneRetentionPeriod. This may disallow protocol downgrade\n    // because the log retention period is not aligned with the tombstoneRetentionPeriod.\n    // To resolve this issue, we filter out all tombstones from the earliest checkpoint.\n    // Tombstones at the earliest checkpoint should be irrelevant and should not be an\n    // issue for readers that do not support the feature.\n    if (containsFeatureTraces(earliestSnapshot.stateDS.filter(\"remove is null\"))) {\n      return true\n    }\n\n    // Check if commits between 0 version and toVersion contain any traces of the feature.\n    val allHistoricalDeltaFiles = deltaLog\n      .getChangeLogFiles(startVersion = 0, catalogTableOpt = table.catalogTable)\n      .takeWhile { case (version, _) => version <= toVersion }\n      .map { case (_, file) => file }\n      .filter(FileNames.isDeltaFile)\n      .toSeq\n    DeltaLogFileIndex(DeltaLogFileIndex.COMMIT_FILE_FORMAT, allHistoricalDeltaFiles)\n      .exists(i => containsFeatureTraces(deltaLog.loadIndex(i, Action.logSchema).as[SingleAction]))\n  }\n\n  /** Returns whether a dataset of actions contains any trace of this feature. */\n  private def containsFeatureTraces(ds: Dataset[SingleAction]): Boolean = {\n    import org.apache.spark.sql.delta.implicits._\n    ds.mapPartitions { actions =>\n      actions\n        .map(_.unwrap)\n        .collectFirst { case a if actionUsesFeature(a) => true }\n        .toIterator\n    }.take(1).nonEmpty\n  }\n}\n\n/**\n * A base class for all writer-only table features that can only be explicitly supported.\n *\n * @param name\n *   a globally-unique string indicator to represent the feature. All characters must be letters\n *   (a-z, A-Z), digits (0-9), '-', or '_'. Words must be in camelCase.\n */\nsealed abstract class WriterFeature(name: String)\n  extends TableFeature(\n    name,\n    minReaderVersion = 0,\n    minWriterVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n\n/**\n * A base class for all reader-writer table features that can only be explicitly supported.\n *\n * @param name\n *   a globally-unique string indicator to represent the feature. All characters must be letters\n *   (a-z, A-Z), digits (0-9), '-', or '_'. Words must be in camelCase.\n */\nsealed abstract class ReaderWriterFeature(name: String)\n  extends WriterFeature(name)\n  with ReaderWriterFeatureType {\n  override val minReaderVersion: Int = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION\n}\n\n/**\n * A base class for all table legacy writer-only features.\n *\n * @param name\n *   a globally-unique string indicator to represent the feature. Allowed characters are letters\n *   (a-z, A-Z), digits (0-9), '-', and '_'. Words must be in camelCase.\n * @param minWriterVersion\n *   the minimum writer protocol version that supports this feature.\n */\nsealed abstract class LegacyWriterFeature(name: String, minWriterVersion: Int)\n  extends TableFeature(name, minReaderVersion = 0, minWriterVersion = minWriterVersion)\n  with LegacyFeatureType\n\n/**\n * A base class for all legacy writer-only table features.\n *\n * @param name\n *   a globally-unique string indicator to represent the feature. Allowed characters are letters\n *   (a-z, A-Z), digits (0-9), '-', and '_'. Words must be in camelCase.\n * @param minReaderVersion\n *   the minimum reader protocol version that supports this feature.\n * @param minWriterVersion\n *   the minimum writer protocol version that supports this feature.\n */\nsealed abstract class LegacyReaderWriterFeature(\n    name: String,\n    override val minReaderVersion: Int,\n    minWriterVersion: Int)\n  extends LegacyWriterFeature(name, minWriterVersion)\n  with ReaderWriterFeatureType\n\nobject TableFeature {\n  val isTesting = DeltaUtils.isTesting\n\n  /**\n   * All table features recognized by this client. Update this set when you added a new Table\n   * Feature.\n   *\n   * Warning: Do not call `get` on this Map to get a specific feature because keys in this map are\n   * in lower cases. Use [[featureNameToFeature]] instead.\n   */\n  def allSupportedFeaturesMap: Map[String, TableFeature] = {\n    val testingFeaturesEnabled =\n      try {\n        SparkSession\n        .getActiveSession\n        .map(_.conf.get(DeltaSQLConf.TABLE_FEATURES_TEST_FEATURES_ENABLED))\n        .getOrElse(true)\n      } catch {\n          case _ => true\n      }\n   var features: Set[TableFeature] = Set(\n      AllowColumnDefaultsTableFeature,\n      AppendOnlyTableFeature,\n      ChangeDataFeedTableFeature,\n      CheckConstraintsTableFeature,\n      ClusteringTableFeature,\n      DomainMetadataTableFeature,\n      GeneratedColumnsTableFeature,\n      IdentityColumnsTableFeature,\n      InvariantsTableFeature,\n      ColumnMappingTableFeature,\n      MaterializePartitionColumnsTableFeature,\n      TimestampNTZTableFeature,\n      TypeWideningPreviewTableFeature,\n      TypeWideningTableFeature,\n      IcebergCompatV1TableFeature,\n      IcebergCompatV2TableFeature,\n      DeletionVectorsTableFeature,\n      VacuumProtocolCheckTableFeature,\n      V2CheckpointTableFeature,\n      RowTrackingFeature,\n      InCommitTimestampTableFeature,\n      VariantTypePreviewTableFeature,\n      VariantTypeTableFeature,\n      VariantShreddingPreviewTableFeature,\n      VariantShreddingTableFeature,\n      CatalogOwnedTableFeature,\n      CoordinatedCommitsTableFeature,\n      CheckpointProtectionTableFeature)\n    if (isTesting && testingFeaturesEnabled) {\n      features ++= Set(\n        RedirectReaderWriterFeature,\n        RedirectWriterOnlyFeature,\n        TestLegacyWriterFeature,\n        TestLegacyReaderWriterFeature,\n        TestWriterFeature,\n        TestUnsupportedWriterFeature,\n        TestWriterMetadataNoAutoUpdateFeature,\n        TestReaderWriterFeature,\n        TestUnsupportedReaderWriterFeature,\n        TestUnsupportedNoHistoryProtectionReaderWriterFeature,\n        TestReaderWriterMetadataAutoUpdateFeature,\n        TestReaderWriterMetadataNoAutoUpdateFeature,\n        TestRemovableWriterFeature,\n        TestRemovableWriterFeatureWithDependency,\n        TestRemovableWriterWithHistoryTruncationFeature,\n        TestRemovableLegacyWriterFeature,\n        TestRemovableReaderWriterFeature,\n        TestRemovableLegacyReaderWriterFeature,\n        TestFeatureWithDependency,\n        TestFeatureWithTransitiveDependency,\n        TestWriterFeatureWithTransitiveDependency)\n    }\n    val featureMap = features.map(f => f.name.toLowerCase(Locale.ROOT) -> f).toMap\n    require(features.size == featureMap.size, \"Lowercase feature names must not duplicate.\")\n    featureMap\n  }\n\n  /** Test only features that appear unsupported in order to test protocol validations. */\n  def testUnsupportedFeatures: Set[TableFeature] = {\n    if (!isTesting) return Set.empty\n    Set(TestUnsupportedReaderWriterFeature,\n      TestUnsupportedNoHistoryProtectionReaderWriterFeature,\n      TestUnsupportedWriterFeature)\n  }\n\n  private val allDependentFeaturesMap: Map[TableFeature, Set[TableFeature]] = {\n    val dependentFeatureTuples =\n      allSupportedFeaturesMap.values.toSeq.flatMap(f => f.requiredFeatures.map(_ -> f))\n    dependentFeatureTuples\n      .groupBy(_._1)\n      .mapValues(_.map(_._2).toSet)\n      .toMap\n  }\n\n  /** Get a [[TableFeature]] object by its name. */\n  def featureNameToFeature(featureName: String): Option[TableFeature] =\n    allSupportedFeaturesMap.get(featureName.toLowerCase(Locale.ROOT))\n\n  /** Returns a set of [[TableFeature]]s that require the given feature to be enabled. */\n  def getDependentFeatures(feature: TableFeature): Set[TableFeature] =\n    allDependentFeaturesMap.getOrElse(feature, Set.empty)\n\n  /**\n   * Extracts the removed features by comparing new and old protocols.\n   * Returns None if there are no removed features.\n   */\n  protected def getDroppedFeatures(\n      newProtocol: Protocol,\n      oldProtocol: Protocol): Set[TableFeature] = {\n    val newFeatures = newProtocol.implicitlyAndExplicitlySupportedFeatures\n    val oldFeatures = oldProtocol.implicitlyAndExplicitlySupportedFeatures\n    oldFeatures -- newFeatures\n  }\n\n  /**\n   * Extracts the added features by comparing new and old protocols.\n   * Returns None if there are no added features.\n   */\n  def getAddedFeatures(\n      newProtocol: Protocol,\n      oldProtocol: Protocol): Set[TableFeature] = {\n    val newFeatures = newProtocol.implicitlyAndExplicitlySupportedFeatures\n    val oldFeatures = oldProtocol.implicitlyAndExplicitlySupportedFeatures\n    newFeatures -- oldFeatures\n  }\n\n  /** Identifies whether there was any feature removal between two protocols. */\n  def isProtocolRemovingFeatures(newProtocol: Protocol, oldProtocol: Protocol): Boolean = {\n    getDroppedFeatures(newProtocol = newProtocol, oldProtocol = oldProtocol).nonEmpty\n  }\n\n  /** Returns true when `newProtocol` drops `feature`. */\n  def isFeatureDropped(\n      newProtocol: Protocol,\n      oldProtocol: Protocol,\n      feature: TableFeature): Boolean = {\n    getDroppedFeatures(newProtocol = newProtocol, oldProtocol = oldProtocol).contains(feature)\n  }\n\n  /**\n   * Identifies whether there were any features with requiresHistoryProtection removed\n   * between the two protocols.\n   */\n  def isProtocolRemovingFeatureWithHistoryProtection(\n      newProtocol: Protocol,\n      oldProtocol: Protocol): Boolean = {\n    getDroppedFeatures(newProtocol = newProtocol, oldProtocol = oldProtocol).exists {\n        case r: RemovableFeature if r.requiresHistoryProtection => true\n        case _ => false\n      }\n  }\n\n  /**\n   * Validates whether all requirements of a removed feature hold against the provided snapshot.\n   */\n  def validateFeatureRemovalAtSnapshot(\n      newProtocol: Protocol,\n      oldProtocol: Protocol,\n      table: DeltaTableV2,\n      snapshot: Snapshot): Boolean = {\n    val droppedFeatures = TableFeature.getDroppedFeatures(\n      newProtocol = newProtocol,\n      oldProtocol = oldProtocol)\n    val droppedFeature = droppedFeatures match {\n      case f if f.size == 1 => f.head\n      // We do not support dropping more than one feature at a time so we have to reject\n      // the validation.\n      case f if f.size > 1 => return false\n      case _ => return true\n    }\n\n    droppedFeature match {\n      case feature: RemovableFeature => feature.validateDropInvariants(table, snapshot)\n      case _ => throw DeltaErrors.dropTableFeatureFeatureNotSupportedByClient(droppedFeature.name)\n    }\n  }\n}\n\n/* ---------------------------------------- *\n |  All table features known to the client  |\n * ---------------------------------------- */\n\nobject AppendOnlyTableFeature\n  extends LegacyWriterFeature(name = \"appendOnly\", minWriterVersion = 2)\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    DeltaConfigs.IS_APPEND_ONLY.fromMetaData(metadata)\n  }\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n}\n\nobject InvariantsTableFeature\n  extends LegacyWriterFeature(name = \"invariants\", minWriterVersion = 2)\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    Invariants.getFromSchema(metadata.schema, spark).nonEmpty\n  }\n}\n\nobject CheckConstraintsTableFeature\n  extends LegacyWriterFeature(name = \"checkConstraints\", minWriterVersion = 3)\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    Constraints.getCheckConstraints(metadata, spark).nonEmpty\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    CheckConstraintsPreDowngradeTableFeatureCommand(table)\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    Constraints.getCheckConstraintNames(snapshot.metadata).isEmpty\n\n  override def actionUsesFeature(action: Action): Boolean = {\n    // This method is never called, as it is only used for ReaderWriterFeatures.\n    throw new UnsupportedOperationException()\n  }\n}\n\nobject ChangeDataFeedTableFeature\n  extends LegacyWriterFeature(name = \"changeDataFeed\", minWriterVersion = 4)\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    DeltaConfigs.CHANGE_DATA_FEED.fromMetaData(metadata)\n  }\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n}\n\nobject GeneratedColumnsTableFeature\n  extends LegacyWriterFeature(name = \"generatedColumns\", minWriterVersion = 4)\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    GeneratedColumn.hasGeneratedColumns(metadata.schema)\n  }\n}\n\nobject ColumnMappingTableFeature\n  extends LegacyReaderWriterFeature(\n    name = \"columnMapping\",\n    minReaderVersion = 2,\n    minWriterVersion = 5)\n  with RemovableFeature\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.columnMappingMode match {\n      case NoMapping => false\n      case _ => true\n    }\n  }\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    val schemaHasNoColumnMappingMetadata =\n      !DeltaColumnMapping.schemaHasColumnMappingMetadata(snapshot.schema)\n    val metadataHasNoMappingMode = snapshot.metadata.columnMappingMode match {\n      case NoMapping => true\n      case _ => false\n    }\n    schemaHasNoColumnMappingMetadata && metadataHasNoMappingMode\n  }\n\n  override def actionUsesFeature(action: Action): Boolean = action match {\n      case m: Metadata => DeltaConfigs.COLUMN_MAPPING_MODE.fromMetaData(m) != NoMapping\n      case _ => false\n    }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    ColumnMappingPreDowngradeCommand(table)\n}\n\nobject IdentityColumnsTableFeature\n  extends LegacyWriterFeature(name = \"identityColumns\", minWriterVersion = 6)\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    ColumnWithDefaultExprUtils.hasIdentityColumn(metadata.schema)\n  }\n}\n\nobject TimestampNTZTableFeature extends ReaderWriterFeature(name = \"timestampNtz\")\n    with FeatureAutomaticallyEnabledByMetadata {\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean = {\n    SchemaUtils.checkForTimestampNTZColumnsRecursively(metadata.schema)\n  }\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n}\n\nobject RedirectReaderWriterFeature\n  extends ReaderWriterFeature(name = \"redirectReaderWriter-preview\")\n  with FeatureAutomaticallyEnabledByMetadata with RemovableFeature {\n  override def metadataRequiresFeatureToBeEnabled(\n    protocol: Protocol,\n    metadata: Metadata,\n    spark: SparkSession\n  ): Boolean = RedirectReaderWriter.isFeatureSet(metadata)\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    RedirectReaderWriterPreDowngradeCommand(table)\n\n  /**\n   * [[RedirectReaderWriterPreDowngradeCommand]] will try to remove\n   * [[DeltaConfigs.REDIRECT_READER_WRITER]],\n   * we check that here to make sure there is no concurrent txn that re-enables redirection.\n   */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !RedirectReaderWriter.isFeatureSet(snapshot.metadata)\n\n  // There is no action that is associated with this feature.\n  override def actionUsesFeature(action: Action): Boolean = false\n\n  // There is no action associated with this feature, so we don't need to truncate history to remove\n  // the traces of it. Note that the table properties for this feature will be left in the history\n  // but legacy clients who don't understand this feature will simply ignore them.\n  override def requiresHistoryProtection: Boolean = false\n}\n\nobject RedirectWriterOnlyFeature extends WriterFeature(name = \"redirectWriterOnly-preview\")\n  with FeatureAutomaticallyEnabledByMetadata with RemovableFeature {\n  override def metadataRequiresFeatureToBeEnabled(\n    protocol: Protocol,\n    metadata: Metadata,\n    spark: SparkSession\n  ): Boolean = RedirectWriterOnly.isFeatureSet(metadata)\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    RedirectWriterOnlyPreDowngradeCommand(table)\n\n  /**\n   * [[RedirectWriterOnlyPreDowngradeCommand]] will try to remove\n   * [[DeltaConfigs.REDIRECT_WRITER_ONLY]],\n   * we check that here to make sure there is no concurrent txn that re-enables redirection.\n   */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !RedirectWriterOnly.isFeatureSet(snapshot.metadata)\n\n  // Writer features should directly return false, as it is only used for reader+writer features.\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\ntrait BinaryVariantTableFeature {\n  def forcePreviewTableFeature: Boolean = SparkSession\n    .getActiveSession\n    .map(_.conf.get(DeltaSQLConf.FORCE_USE_PREVIEW_VARIANT_FEATURE))\n    .getOrElse(false)\n}\n\n/**\n * Preview feature for variant. The preview feature isn't enabled automatically anymore when\n * variants are present in the table schema and the GA feature is used instead.\n *\n * Note: Users can manually add both the preview and stable features to a table using ADD FEATURE,\n * although that's undocumented. The feature spec did not change between preview and GA so the two\n * feature specifications are compatible and supported.\n */\nobject VariantTypePreviewTableFeature extends ReaderWriterFeature(name = \"variantType-preview\")\n  with FeatureAutomaticallyEnabledByMetadata\n    with BinaryVariantTableFeature {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean = {\n    if (forcePreviewTableFeature) {\n      SchemaUtils.checkForVariantTypeColumnsRecursively(metadata.schema) &&\n      // Do not require this table feature to be enabled when the 'variantType' table feature is\n      // enabled so existing tables with variant columns with only 'variantType' and not\n      // 'variantType-preview' can be operated on when the 'FORCE_USE_PREVIEW_VARIANT_FEATURE'\n      // config is enabled.\n      !protocol.isFeatureSupported(VariantTypeTableFeature)\n    } else {\n      false\n    }\n  }\n}\n\nobject VariantTypeTableFeature extends ReaderWriterFeature(name = \"variantType\")\n    with FeatureAutomaticallyEnabledByMetadata\n    with BinaryVariantTableFeature {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean = {\n    if (forcePreviewTableFeature) {\n      false\n    } else {\n      SchemaUtils.checkForVariantTypeColumnsRecursively(metadata.schema) &&\n      // Do not require this table feature to be enabled when the 'variantType-preview' table\n      // feature is enabled so old tables with only the preview table feature can be read.\n      !protocol.isFeatureSupported(VariantTypePreviewTableFeature)\n    }\n  }\n}\n\ntrait VariantShreddingTableFeatureBase {\n  def forcePreviewTableFeature: Boolean = SparkSession\n    .getActiveSession\n    .map(_.conf.get(DeltaSQLConf.FORCE_USE_PREVIEW_SHREDDING_FEATURE))\n    .getOrElse(false)\n}\n\nobject VariantShreddingPreviewTableFeature\n    extends ReaderWriterFeature(name = \"variantShredding-preview\")\n    with FeatureAutomaticallyEnabledByMetadata\n    with VariantShreddingTableFeatureBase {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean = {\n    forcePreviewTableFeature && DeltaConfigs.ENABLE_VARIANT_SHREDDING.fromMetaData(metadata) &&\n    // Do not require this table feature to be enabled when the 'variantShredding' table feature\n    // is enabled so existing tables with shredding with only 'variantShredding' and not\n    // 'variantShredding-preview' can be operated on when the\n    // 'FORCE_USE_PREVIEW_SHREDDING_FEATURE' config is enabled.\n    !protocol.isFeatureSupported(VariantShreddingTableFeature)\n  }\n}\n\nobject VariantShreddingTableFeature\n    extends ReaderWriterFeature(name = \"variantShredding\")\n    with FeatureAutomaticallyEnabledByMetadata\n    with VariantShreddingTableFeatureBase {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean =\n    VariantShreddingPreviewTableFeature.automaticallyUpdateProtocolOfExistingTables\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean = {\n    !forcePreviewTableFeature && DeltaConfigs.ENABLE_VARIANT_SHREDDING.fromMetaData(metadata) &&\n    // Do not require this table feature to be enabled when the 'variantShredding-preview' table\n    // feature is enabled so old tables with only the preview table feature can be read.\n    !protocol.isFeatureSupported(VariantShreddingPreviewTableFeature)\n  }\n}\n\nobject DeletionVectorsTableFeature\n  extends ReaderWriterFeature(name = \"deletionVectors\")\n  with RemovableFeature\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(metadata)\n  }\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  /**\n   * Validate whether all deletion vector traces are removed from the snapshot.\n   *\n   * Note, we do not need to validate whether DV tombstones exist. These are added in the\n   * pre-downgrade stage and always cover all DVs within the retention period. This invariant can\n   * never change unless we enable again DVs. If DVs are enabled before the protocol downgrade\n   * we will abort the operation.\n   */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    val dvsWritable = DeletionVectorUtils.deletionVectorsWritable(snapshot)\n    val dvsExist = snapshot.numDeletionVectorsOpt.getOrElse(0L) > 0\n\n    !(dvsWritable || dvsExist)\n  }\n\n  override def actionUsesFeature(action: Action): Boolean = {\n    action match {\n      case m: Metadata => DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(m)\n      case a: AddFile => a.deletionVector != null\n      case r: RemoveFile => r.deletionVector != null\n      // In general, CDC actions do not contain DVs. We added this for safety.\n      case cdc: AddCDCFile => cdc.deletionVector != null\n      case _ => false\n    }\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    DeletionVectorsPreDowngradeCommand(table)\n}\n\nobject RowTrackingFeature extends WriterFeature(name = \"rowTracking\")\n  with RemovableFeature\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean =\n    DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(metadata)\n\n  override def requiredFeatures: Set[TableFeature] = Set(DomainMetadataTableFeature)\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  /**\n   * When dropping row tracking we remove all relevant properties at downgrade commit.\n   * This is because concurrent transactions may still use them while the feature exists in the\n   * protocol.\n   */\n  override def tablePropertiesToRemoveAtDowngradeCommit: Seq[String] = {\n    Seq(\n      DeltaConfigs.ROW_TRACKING_ENABLED.key,\n      DeltaConfigs.ROW_TRACKING_SUSPENDED.key,\n      MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP,\n      MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP)\n  }\n\n  /** Remove rowTracking domain metadata at downgrade commit. */\n  override def actionsToIncludeAtDowngradeCommit(snapshot: Snapshot): Seq[Action] = {\n    val domainOpt = RowTrackingMetadataDomain.fromSnapshot(snapshot)\n    Seq.empty ++ domainOpt.map(_.toDomainMetadata.copy(removed = true))\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand = {\n    RowTrackingPreDowngradeCommand(table)\n  }\n\n  private[delta] def validateConfigurations(configurations: Map[String, String]): Unit = {\n    val enabled = configurations.getOrElse(\n      DeltaConfigs.ROW_TRACKING_ENABLED.key, \"false\").toBoolean\n    val suspended = configurations.getOrElse(\n      DeltaConfigs.ROW_TRACKING_SUSPENDED.key, \"false\").toBoolean\n\n    if (enabled && suspended) {\n      throw DeltaErrors.rowTrackingIllegalPropertyCombination()\n    }\n  }\n\n  private[delta] def validateAndBackfill(\n      spark: SparkSession,\n      table: DeltaTableV2,\n      newConfiguration: Map[String, String]): Unit = {\n\n    // If there is no relevant configuration change, we do not need to do anything.\n    if (!newConfiguration.contains(DeltaConfigs.ROW_TRACKING_ENABLED.key) &&\n        !newConfiguration.contains(DeltaConfigs.ROW_TRACKING_SUSPENDED.key)) {\n      return\n    }\n\n    val snapshot = table.deltaLog.update(catalogTableOpt = table.catalogTable)\n\n    // For overlapping configs, we keep the values of new configuration.\n    validateConfigurations(snapshot.metadata.configuration ++ newConfiguration)\n\n    val justEnabled = newConfiguration.getOrElse(\n      DeltaConfigs.ROW_TRACKING_ENABLED.key, \"false\").toBoolean\n\n    // If we're enabling row tracking on an existing table, we need to complete a backfill process\n    // prior to updating the table metadata.\n    if (justEnabled) {\n      RowTrackingBackfillCommand(\n        table.deltaLog,\n        nameOfTriggeringOperation = DeltaOperations.OP_SET_TBLPROPERTIES,\n        table.catalogTable).run(spark)\n    }\n  }\n\n  /**\n   * Returns true if no relevant row tracking metadata exist on the table. This excludes\n   * properties/domain metadata that are only removed at the downgrade commit.\n   *\n   * Returns false otherwise.\n   */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    val rowTrackingEnabled = DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(snapshot.metadata)\n    val rowTrackingSuspended =\n      DeltaConfigs.ROW_TRACKING_SUSPENDED.fromMetaData(snapshot.metadata)\n\n    if (rowTrackingEnabled || !rowTrackingSuspended) return false\n\n    // In most cases, we should only reach this expensive check only at the protocol downgrade\n    // commit validation.\n    snapshot\n      .allFiles\n      .filter(col(\"baseRowId\").isNotNull || col(\"defaultRowCommitVersion\").isNotNull)\n      .isEmpty\n  }\n\n  /**\n   * Even though Row tracking is a writer-only feature it could benefit from history protection.\n   * Without history protection, oblivious writers could replace past checkpoints that contain\n   * Row Tracking metadata. That could break time travel, i.e. row tracking might appear enabled\n   * in a past version but metadata might be missing.\n   *\n   * On the other hand, history protection dictates the addition of the checkpointProtection\n   * feature when dropping row tracking. For this reason, we choose not to protect history. There\n   * should be no (or very limited) uses cases where row tracking is expected to work for past\n   * versions.\n   */\n  override def requiresHistoryProtection: Boolean = false\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\nobject DomainMetadataTableFeature\n    extends WriterFeature(name = \"domainMetadata\")\n    with RemovableFeature {\n\n  /**\n   * Returns true if no domain metadata exist on the table.\n   * Returns false otherwise.\n   */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    snapshot.domainMetadata.isEmpty\n  }\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand = {\n    DomainMetadataPreDowngradeCommand(table)\n  }\n\n  override def requiresHistoryProtection: Boolean = false\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\nobject IcebergCompatV1TableFeature extends WriterFeature(name = \"icebergCompatV1\")\n  with FeatureAutomaticallyEnabledByMetadata {\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = IcebergCompatV1.isEnabled(metadata)\n\n  override def requiredFeatures: Set[TableFeature] = Set(ColumnMappingTableFeature)\n}\n\nobject IcebergCompatV2TableFeature extends WriterFeature(name = \"icebergCompatV2\")\n  with FeatureAutomaticallyEnabledByMetadata {\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = IcebergCompatV2.isEnabled(metadata)\n\n  override def requiredFeatures: Set[TableFeature] = Set(ColumnMappingTableFeature)\n}\n\n/**\n * Clustering table feature is enabled when a table is created with CLUSTER BY clause.\n */\nobject ClusteringTableFeature extends WriterFeature(\"clustering\") {\n  override val requiredFeatures: Set[TableFeature] = Set(DomainMetadataTableFeature)\n}\n\n/**\n * This table feature represents support for column DEFAULT values for Delta Lake. With this\n * feature, it is possible to assign default values to columns either at table creation time or\n * later by using commands of the form: ALTER TABLE t ALTER COLUMN c SET DEFAULT v. Thereafter,\n * queries from the table will return the specified default value instead of NULL when the\n * corresponding field is not present in storage.\n *\n * We create this as a writer-only feature rather than a reader/writer feature in order to simplify\n * the query execution implementation for scanning Delta tables. This means that commands of the\n * following form are not allowed: ALTER TABLE t ADD COLUMN c DEFAULT v. The reason is that when\n * commands of that form execute (such as for other data sources like CSV or JSON), then the data\n * source scan implementation must take responsibility to return the supplied default value for all\n * rows, including those previously present in the table before the command executed. We choose to\n * avoid this complexity for Delta table scans, so we make this a writer-only feature instead.\n * Therefore, the analyzer can take care of the entire job when processing commands that introduce\n * new rows into the table by injecting the column default value (if present) into the corresponding\n * query plan. This comes at the expense of preventing ourselves from easily adding a default value\n * to an existing non-empty table, because all data files would need to be rewritten to include the\n * new column value in an expensive backfill.\n */\nobject AllowColumnDefaultsTableFeature extends WriterFeature(name = \"allowColumnDefaults\")\n\n/**\n * This table feature requires materialization of partition columns in data files.\n *\n * This is a writer-only feature because:\n * - Writers need to understand when to materialize partition columns into data files\n * - Readers can read the data regardless of whether partition columns are materialized or not, as\n *   they read the partition values from the AddFile.\n *\n * The feature is automatically enabled when the table property\n * `delta.enableMaterializePartitionColumnsFeature` is set to true.\n *\n * This makes data files more flexible with external readers that require the presence of\n * partition columns in parquet, or for future data layout changes. This is a removable feature\n * that can be dropped when partition column materialization is no longer needed.\n */\nobject MaterializePartitionColumnsTableFeature\n    extends WriterFeature(name = \"materializePartitionColumns\")\n    with FeatureAutomaticallyEnabledByMetadata\n    with RemovableFeature {\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  /**\n   * MaterializePartitionColumnsTableFeature is always enabled when present in the protocol.\n   * The Delta protocol does not require any metadata or domain metadata configs for this\n   * feature to be effective.\n   */\n  override def failConcurrentTransactionsAtUpgrade: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    DeltaConfigs.ENABLE_MATERIALIZE_PARTITION_COLUMNS_FEATURE\n      .fromMetaData(metadata)\n      .getOrElse(false)\n  }\n\n  /** dropping this feature is always allowed without any action */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = true\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand = {\n    MaterializePartitionColumnsPreDowngradeCommand(table)\n  }\n\n  override def requiresHistoryProtection: Boolean = false\n\n  override def tablePropertiesToRemoveAtDowngradeCommit: Seq[String] = {\n    Seq(DeltaConfigs.ENABLE_MATERIALIZE_PARTITION_COLUMNS_FEATURE.key)\n  }\n\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n\n/**\n * V2 Checkpoint table feature is for checkpoints with sidecars and the new format and\n * file naming scheme.\n */\nobject V2CheckpointTableFeature\n  extends ReaderWriterFeature(name = \"v2Checkpoint\")\n  with RemovableFeature\n  with FeatureAutomaticallyEnabledByMetadata {\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  private def isV2CheckpointSupportNeededByMetadata(metadata: Metadata): Boolean =\n    DeltaConfigs.CHECKPOINT_POLICY.fromMetaData(metadata).needsV2CheckpointSupport\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = isV2CheckpointSupportNeededByMetadata(metadata)\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    // Fail validation if v2 checkpoints are still enabled in the current snapshot\n    if (isV2CheckpointSupportNeededByMetadata(snapshot.metadata)) return false\n\n    // Validation also fails if the current snapshot might depend on a v2 checkpoint.\n    // NOTE: Empty and preloaded checkpoint providers never reference v2 checkpoints.\n    snapshot.checkpointProvider match {\n      case p if p.isEmpty => true\n      case _: PreloadedCheckpointProvider => true\n      case lazyProvider: LazyCompleteCheckpointProvider =>\n        lazyProvider.underlyingCheckpointProvider.isInstanceOf[PreloadedCheckpointProvider]\n      case _ => false\n    }\n  }\n\n  override def actionUsesFeature(action: Action): Boolean = action match {\n    case m: Metadata => isV2CheckpointSupportNeededByMetadata(m)\n    case _: CheckpointMetadata => true\n    case _: SidecarFile => true\n    case _ => false\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    V2CheckpointPreDowngradeCommand(table)\n}\n\n/** Table feature to represent tables whose commits are managed by separate commit-coordinator */\nobject CoordinatedCommitsTableFeature\n  extends WriterFeature(name = \"coordinatedCommits-preview\")\n    with FeatureAutomaticallyEnabledByMetadata\n    with RemovableFeature {\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.fromMetaData(metadata).nonEmpty\n  }\n\n  override def requiredFeatures: Set[TableFeature] =\n    Set(InCommitTimestampTableFeature, VacuumProtocolCheckTableFeature)\n\n  override def preDowngradeCommand(table: DeltaTableV2)\n      : PreDowngradeTableFeatureCommand = CoordinatedCommitsPreDowngradeCommand(table)\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    !CoordinatedCommitsUtils.tablePropertiesPresent(snapshot.metadata) &&\n      !CoordinatedCommitsUtils.unbackfilledCommitsPresent(snapshot)\n  }\n\n  // This is a writer feature, so it should directly return false.\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n/** Table feature to represent tables that commits are managed by catalog */\nobject CatalogOwnedTableFeature\n  extends ReaderWriterFeature(name = \"catalogManaged\")\n  with RemovableFeature {\n\n  override def requiredFeatures: Set[TableFeature] =\n    Set(InCommitTimestampTableFeature, VacuumProtocolCheckTableFeature)\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand = {\n    // Note: We don't support downgrade for this feature yet.\n    throw DeltaErrors.dropTableFeatureFeatureNotSupportedByClient(name)\n  }\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    !CoordinatedCommitsUtils.unbackfilledCommitsPresent(snapshot)\n  }\n\n  // Before downgrade, we require to backfill all unbackfilled commits, hence time-travel is safe.\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n\n/** Common base shared by the preview and stable type widening table features. */\nabstract class TypeWideningTableFeatureBase(name: String) extends ReaderWriterFeature(name)\n    with RemovableFeature {\n\n  protected def isTypeWideningSupportNeededByMetadata(metadata: Metadata): Boolean =\n    DeltaConfigs.ENABLE_TYPE_WIDENING.fromMetaData(metadata)\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !isTypeWideningSupportNeededByMetadata(snapshot.metadata) &&\n      !TypeWideningMetadata.containsTypeWideningMetadata(snapshot.metadata.schema)\n\n  override def actionUsesFeature(action: Action): Boolean =\n    action match {\n      case m: Metadata => TypeWideningMetadata.containsTypeWideningMetadata(m.schema)\n      case _ => false\n    }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TypeWideningPreDowngradeCommand(table)\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n}\n\n/**\n * Feature used for the preview phase of type widening. Tables that enabled this feature during the\n * preview are still supported after the preview.\n *\n * Note: Users can manually add both the preview and stable features to a table using ADD FEATURE,\n * although that's undocumented for type widening. This is allowed: the two feature specifications\n * are compatible and supported.\n */\nobject TypeWideningPreviewTableFeature\n  extends TypeWideningTableFeatureBase(name = \"typeWidening-preview\")\n\n/**\n * Stable feature for type widening.\n */\nobject TypeWideningTableFeature\n  extends TypeWideningTableFeatureBase(name = \"typeWidening\")\n  with FeatureAutomaticallyEnabledByMetadata {\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = isTypeWideningSupportNeededByMetadata(metadata) &&\n    // Don't automatically enable the stable feature if the preview feature is already supported, to\n    // avoid possibly breaking old clients that only support the preview feature.\n    !protocol.isFeatureSupported(TypeWideningPreviewTableFeature)\n}\n\n/**\n * inCommitTimestamp table feature is a writer feature that makes\n * every writer write a monotonically increasing timestamp inside the commit file.\n */\nobject InCommitTimestampTableFeature\n  extends WriterFeature(name = \"inCommitTimestamp\")\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(metadata)\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    InCommitTimestampsPreDowngradeCommand(table)\n\n  override def failConcurrentTransactionsAtUpgrade: Boolean = false\n\n  /**\n   * As per the spec, we can disable ICT by just setting\n   * [[DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED]] to `false`. There is no need to remove the\n   * provenance properties. However, [[InCommitTimestampsPreDowngradeCommand]] will try to remove\n   * these properties because they can be removed as part of the same metadata update that sets\n   * [[DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED]] to `false`. We check all three properties here\n   * as well for consistency.\n   */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    val provenancePropertiesAbsent = Seq(\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.key,\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key)\n      .forall(!snapshot.metadata.configuration.contains(_))\n    val ictEnabledInMetadata =\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata)\n    provenancePropertiesAbsent && !ictEnabledInMetadata\n  }\n\n  // Writer features should directly return false, as it is only used for reader+writer features.\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n/**\n * A ReaderWriter table feature for VACUUM. If this feature is enabled:\n * A writer should follow one of the following:\n *   1. Non-Support for Vacuum: Writers can explicitly state that they do not support VACUUM for\n *      any table, regardless of whether the Vacuum Protocol Check Table feature exists.\n *   2. Implement Writer Protocol Check: Ensure that the VACUUM implementation includes a writer\n *      protocol check before any file deletions occur.\n * Readers don't need to understand or change anything new; they just need to acknowledge the\n * feature exists\n */\nobject VacuumProtocolCheckTableFeature\n  extends ReaderWriterFeature(name = \"vacuumProtocolCheck\")\n  with RemovableFeature {\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand = {\n    VacuumProtocolCheckPreDowngradeCommand(table)\n  }\n\n  // The delta snapshot doesn't have any trace of the [[VacuumProtocolCheckTableFeature]] feature.\n  // Other than it being present in PROTOCOL, which will be handled by the table feature downgrade\n  // command once this method returns true.\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = true\n\n  // None of the actions uses [[VacuumProtocolCheckTableFeature]]\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n/**\n * Writer feature that enforces writers to cleanup metadata iff metadata can be cleaned up to\n * requireCheckpointProtectionBeforeVersion in one go. This means that a single cleanup\n * operation should truncate up to requireCheckpointProtectionBeforeVersion as opposed to\n * several cleanup operations truncating in chunks.\n *\n * The are two exceptions to this rule. If any of the two holds, the rule\n * above can be ignored:\n *\n *   a) The writer verifies it supports all protocols between\n *      [start, min(requireCheckpointProtectionBeforeVersion, targetCleanupVersion)] versions\n *      it intends to truncate.\n *   b) The writer does not create any checkpoints during history cleanup and does not erase any\n *      checkpoints after the truncation version.\n *\n * The CheckpointProtectionTableFeature can only be removed if history is truncated up to\n * at least requireCheckpointProtectionBeforeVersion.\n */\nobject CheckpointProtectionTableFeature\n    extends WriterFeature(name = \"checkpointProtection\")\n    with RemovableFeature {\n  /**\n   * Gets the version requiring checkpoint protection from `metadata`. If the table property is\n   * not set, return `None`.\n   */\n  def getCheckpointProtectionVersionOption(protocol: Protocol, metadata: Metadata): Option[Long] = {\n    if (!protocol.isFeatureSupported(CheckpointProtectionTableFeature)) return None\n    DeltaConfigs.REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION.fromMetaDataOption(metadata)\n  }\n\n  /**\n   * Gets the version requiring checkpoint protection from `snapshot`. If the table property is\n   * not set, return the default value 0.\n   */\n  def getCheckpointProtectionVersion(snapshot: Snapshot): Long = {\n    getCheckpointProtectionVersionOption(snapshot.protocol, snapshot.metadata).getOrElse(0)\n  }\n\n  def metadataWithCheckpointProtection(metadata: Metadata, version: Long): Metadata = {\n    val versionPropKey = DeltaConfigs.REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION.key\n    val versionConf = versionPropKey -> version.toString\n    metadata.copy(configuration = metadata.configuration + versionConf)\n  }\n\n  /** Verify whether any deltas exist between version 0 to toVersion (inclusive). */\n  private def deltasUpToVersionAreTruncated(\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      toVersion: Long): Boolean = {\n    deltaLog\n      .getChangeLogFiles(\n        startVersion = 0,\n        endVersion = toVersion,\n        catalogTableOpt = catalogTableOpt,\n        failOnDataLoss = false)\n      .map { case (_, file) => file }\n      .filter(FileNames.isDeltaFile)\n      .take(1).isEmpty\n  }\n\n  def historyPriorToCheckpointProtectionVersionIsTruncated(\n      snapshot: Snapshot,\n      catalogTableOpt: Option[CatalogTable]): Boolean = {\n    val checkpointProtectionVersion = getCheckpointProtectionVersion(snapshot)\n    if (checkpointProtectionVersion <= 0) return true\n\n    val deltaLog = snapshot.deltaLog\n    // In most cases, the earliest checkpoint matches the version of the earliest commit. This is\n    // not true for new tables that were never cleaned up. Furthermore, if there is no checkpoint it\n    // means history is not truncated.\n    deltaLog.findEarliestReliableCheckpoint.exists(_ >= checkpointProtectionVersion) &&\n      deltasUpToVersionAreTruncated(deltaLog, catalogTableOpt, checkpointProtectionVersion - 1)\n  }\n\n  /**\n   * This is a special feature in the sense that it requires history truncation but implements it\n   * as part of its downgrade process. This is implemented like this for 2 reasons:\n   *\n   *  1. It allows us to remove the feature table property after the clean up in the preDowngrade\n   *     command is successful.\n   *  2. It does not require to scan the history for features traces as long as all history\n   *     before requireCheckpointProtectionBeforeVersion is truncated.\n   */\n  override def requiresHistoryProtection: Boolean = false\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand = {\n    CheckpointProtectionPreDowngradeCommand(table)\n  }\n\n  /** Returns true if table property is absent. */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    val property = DeltaConfigs.REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION.key\n    !snapshot.metadata.configuration.contains(property)\n  }\n\n  /**\n   * The feature uses the `requireCheckpointProtectionBeforeVersion` property. This is removed when\n   * dropping the feature but we allow it to exist in the history. This is to allow history\n   * truncation at the boundary of requireCheckpointProtectionBeforeVersion rather than the last\n   * 24 hours. Otherwise, dropping the feature would always require 24 hour waiting time.\n   */\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n/**\n * Features below are for testing only, and are being registered to the system only in the testing\n * environment. See [[TableFeature.allSupportedFeaturesMap]] for the registration.\n */\n\nobject TestLegacyWriterFeature\n  extends LegacyWriterFeature(name = \"testLegacyWriter\", minWriterVersion = 5)\n\nobject TestWriterFeature extends WriterFeature(name = \"testWriter\")\n\nobject TestWriterMetadataNoAutoUpdateFeature\n  extends WriterFeature(name = \"testWriterMetadataNoAutoUpdate\")\n  with FeatureAutomaticallyEnabledByMetadata {\n  val TABLE_PROP_KEY = \"_123testWriterMetadataNoAutoUpdate321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n}\n\nobject TestLegacyReaderWriterFeature\n  extends LegacyReaderWriterFeature(\n    name = \"testLegacyReaderWriter\",\n    minReaderVersion = 2,\n    minWriterVersion = 5)\n\nobject TestReaderWriterFeature extends ReaderWriterFeature(name = \"testReaderWriter\")\n\nobject TestReaderWriterMetadataNoAutoUpdateFeature\n  extends ReaderWriterFeature(name = \"testReaderWriterMetadataNoAutoUpdate\")\n  with FeatureAutomaticallyEnabledByMetadata {\n  val TABLE_PROP_KEY = \"_123testReaderWriterMetadataNoAutoUpdate321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n}\n\nobject TestReaderWriterMetadataAutoUpdateFeature\n  extends ReaderWriterFeature(name = \"testReaderWriterMetadataAutoUpdate\")\n  with FeatureAutomaticallyEnabledByMetadata {\n  val TABLE_PROP_KEY = \"_123testReaderWriterMetadataAutoUpdate321_\"\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n}\n\nobject TestRemovableWriterFeature\n  extends WriterFeature(name = \"testRemovableWriter\")\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n\n  val TABLE_PROP_KEY = \"_123TestRemovableWriter321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  /** Make sure the property is not enabled on the table. */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !snapshot.metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestWriterFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n/** Test feature that appears unsupported and it is used for testing protocol checks. */\nobject TestUnsupportedReaderWriterFeature\n    extends ReaderWriterFeature(name = \"testUnsupportedReaderWriter\")\n    with RemovableFeature {\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = true\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestUnsupportedReaderWriterFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\n/**\n * Test feature that appears unsupported and can be dropped without checkpoint protection.\n * it is used only for testing purposes.\n */\nobject TestUnsupportedNoHistoryProtectionReaderWriterFeature\n    extends ReaderWriterFeature(name = \"testUnsupportedNoHistoryProtectionReaderWriter\")\n    with RemovableFeature {\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = true\n\n  override def requiresHistoryProtection: Boolean = false\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestUnsupportedReaderWriterFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\nobject TestUnsupportedWriterFeature\n  extends WriterFeature(name = \"testUnsupportedWriter\")\n  with RemovableFeature {\n\n  /** Make sure the property is not enabled on the table. */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = true\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestUnsupportedWriterFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\nprivate[sql] object TestRemovableWriterFeatureWithDependency\n  extends WriterFeature(name = \"testRemovableWriterFeatureWithDependency\")\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n\n  val TABLE_PROP_KEY = \"_123TestRemovableWriterFeatureWithDependency321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  /** Make sure the property is not enabled on the table. */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !snapshot.metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestWriterFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = false\n\n  override def requiredFeatures: Set[TableFeature] =\n    Set(TestRemovableReaderWriterFeature, TestRemovableWriterFeature)\n}\n\nobject TestRemovableReaderWriterFeature\n  extends ReaderWriterFeature(name = \"testRemovableReaderWriter\")\n    with FeatureAutomaticallyEnabledByMetadata\n    with RemovableFeature {\n\n  val TABLE_PROP_KEY = \"_123TestRemovableReaderWriter321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  /** Make sure the property is not enabled on the table. */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !snapshot.metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n\n  override def actionUsesFeature(action: Action): Boolean = action match {\n    case m: Metadata => m.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n    case _ => false\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestReaderWriterFeaturePreDowngradeCommand(table)\n}\n\nobject TestRemovableLegacyWriterFeature\n  extends LegacyWriterFeature(name = \"testRemovableLegacyWriter\", minWriterVersion = 5)\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n\n  val TABLE_PROP_KEY = \"_123TestRemovableLegacyWriter321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    !snapshot.metadata.configuration.contains(TABLE_PROP_KEY)\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestLegacyWriterFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = false\n}\n\nobject TestRemovableLegacyReaderWriterFeature\n  extends LegacyReaderWriterFeature(\n      name = \"testRemovableLegacyReaderWriter\", minReaderVersion = 2, minWriterVersion = 5)\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n\n  val TABLE_PROP_KEY = \"_123TestRemovableLegacyReaderWriter321_\"\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean = {\n    !snapshot.metadata.configuration.contains(TABLE_PROP_KEY)\n  }\n\n  override def actionUsesFeature(action: Action): Boolean = {\n    action match {\n      case m: Metadata => m.configuration.contains(TABLE_PROP_KEY)\n      case _ => false\n    }\n  }\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestLegacyReaderWriterFeaturePreDowngradeCommand(table)\n}\n\nobject TestFeatureWithDependency\n  extends ReaderWriterFeature(name = \"testFeatureWithDependency\")\n  with FeatureAutomaticallyEnabledByMetadata {\n\n  val TABLE_PROP_KEY = \"_123testFeatureWithDependency321_\"\n\n  override def automaticallyUpdateProtocolOfExistingTables: Boolean = true\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol, metadata: Metadata, spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  override def requiredFeatures: Set[TableFeature] = Set(TestReaderWriterFeature)\n}\n\nobject TestFeatureWithTransitiveDependency\n  extends ReaderWriterFeature(name = \"testFeatureWithTransitiveDependency\") {\n\n  override def requiredFeatures: Set[TableFeature] = Set(TestFeatureWithDependency)\n}\n\nobject TestWriterFeatureWithTransitiveDependency\n  extends WriterFeature(name = \"testWriterFeatureWithTransitiveDependency\") {\n\n  override def requiredFeatures: Set[TableFeature] = Set(TestFeatureWithDependency)\n}\n\nobject TestRemovableWriterWithHistoryTruncationFeature\n  extends WriterFeature(name = \"TestRemovableWriterWithHistoryTruncationFeature\")\n  with FeatureAutomaticallyEnabledByMetadata\n  with RemovableFeature {\n\n  val TABLE_PROP_KEY = \"_123TestRemovableWriterWithHistoryTruncationFeature321_\"\n\n  override def metadataRequiresFeatureToBeEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n  }\n\n  /** Make sure the property is not enabled on the table. */\n  override def validateDropInvariants(table: DeltaTableV2, snapshot: Snapshot): Boolean =\n    !snapshot.metadata.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n\n  override def preDowngradeCommand(table: DeltaTableV2): PreDowngradeTableFeatureCommand =\n    TestWriterWithHistoryValidationFeaturePreDowngradeCommand(table)\n\n  override def actionUsesFeature(action: Action): Boolean = action match {\n    case m: Metadata => m.configuration.get(TABLE_PROP_KEY).exists(_.toBoolean)\n    case _ => false\n  }\n\n  override def requiresHistoryProtection: Boolean = true\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/ThreadStorageExecutionObserver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\ntrait ThreadStorageExecutionObserver[T <: ChainableExecutionObserver[T]] {\n  /** Thread-local observer instance loaded by [[T]] */\n  protected val threadObserver: ThreadLocal[T] = ThreadLocal.withInitial(() => initialValue)\n  protected def initialValue: T\n\n  def getObserver: T = threadObserver.get()\n\n  def setObserver(observer: T): Unit = threadObserver.set(observer)\n\n  /** Instrument all executions created and completed within `thunk` with `newObserver`. */\n  def withObserver[S](newObserver: T)(thunk: => S): S = {\n    val oldObserver = threadObserver.get()\n    threadObserver.set(newObserver)\n    try {\n      thunk\n    } finally {\n      // reset\n      threadObserver.set(oldObserver)\n    }\n  }\n\n  /** Update the current thread observer with its next one. */\n  def advanceToNextObserver(): Unit = threadObserver.get.advanceToNextThreadObserver()\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/TransactionExecutionObserver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\ntrait ChainableExecutionObserver[O] {\n  /**\n   * The next txn observer for this thread.\n   * The next observer is used to test threads that perform multiple transactions, i.e.\n   * commands that perform multiple commits.\n   */\n  @volatile protected var nextObserver: Option[O] = None\n\n  /** Set the next observer for this thread. */\n  def setNextObserver(nextTxnObserver: O): Unit = {\n    nextObserver = Some(nextTxnObserver)\n  }\n\n  /** Update the observer of this thread with the next observer. */\n  def advanceToNextThreadObserver(): Unit\n}\n\n/**\n * Track different stages of the execution of a transaction.\n *\n * This is mostly meant for test instrumentation.\n *\n * The default is a no-op implementation.\n */\ntrait TransactionExecutionObserver\n  extends ChainableExecutionObserver[TransactionExecutionObserver] {\n\n  /**\n   * Create a child instance of this observer for use in [[OptimisticTransactionImpl.split()]].\n   *\n   * It's up to each observer type what state new child needs to hold.\n   */\n  def createChild(): TransactionExecutionObserver\n\n  /*\n   * This is called outside the transaction object,\n   * since it wraps its creation.\n   */\n\n  /** Wraps transaction creation. */\n  def startingTransaction(f: => OptimisticTransaction): OptimisticTransaction\n\n  /*\n   * These are called from within the transaction object.\n   */\n\n  /** Wraps `prepareCommit`. */\n  def preparingCommit[T](f: => T): T\n\n  /*\n   * The next three methods before/after-style instead of wrapping like above,\n   * because the commit code is large and in a try-catch block,\n   * making wrapping impractical.\n   */\n\n  /** Called before the first `doCommit` attempt. */\n  def beginDoCommit(): Unit\n\n  /** Called after publishing the commit file but before the `backfill` attempt. */\n  def beginBackfill(): Unit\n\n  /** Called after backfill but before the `postCommit` attempt. */\n  def beginPostCommit(): Unit\n\n  /** Called once a commit succeeded. */\n  def transactionCommitted(): Unit\n\n  /**\n   * Called once the transaction failed.\n   *\n   * *Note:* It can happen that [[transactionAborted()]] is called\n   *         without [[beginDoCommit()]] being called first.\n   *         This occurs when there is an Exception thrown during the transaction's body.\n   */\n  def transactionAborted(): Unit\n\n  override def advanceToNextThreadObserver(): Unit = {\n    TransactionExecutionObserver.setObserver(\n      nextObserver.getOrElse(NoOpTransactionExecutionObserver))\n  }\n}\n\nobject TransactionExecutionObserver\n  extends ThreadStorageExecutionObserver[TransactionExecutionObserver] {\n  override protected val initialValue: TransactionExecutionObserver =\n    NoOpTransactionExecutionObserver\n}\n\n/** Default observer does nothing. */\nobject NoOpTransactionExecutionObserver extends TransactionExecutionObserver {\n  override def startingTransaction(f: => OptimisticTransaction): OptimisticTransaction = f\n\n  override def preparingCommit[T](f: => T): T = f\n\n  override def beginDoCommit(): Unit = ()\n\n  override def beginBackfill(): Unit = ()\n\n  override def beginPostCommit(): Unit = ()\n\n  override def transactionCommitted(): Unit = ()\n\n  override def transactionAborted(): Unit = ()\n\n  override def createChild(): TransactionExecutionObserver = {\n    // This mimics the original behaviour of this code.\n    TransactionExecutionObserver.getObserver\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/TypeWidening.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.catalyst.expressions.Cast\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\nobject TypeWidening {\n\n  /**\n   * Returns whether the protocol version supports the Type Widening table feature.\n   */\n  def isSupported(protocol: Protocol): Boolean =\n    Seq(TypeWideningPreviewTableFeature, TypeWideningTableFeature)\n      .exists(protocol.isFeatureSupported)\n\n  /**\n   * Returns whether Type Widening is enabled on this table version. Checks that Type Widening is\n   * supported, which is a pre-requisite for enabling Type Widening, throws an error if\n   * not. When Type Widening is enabled, the type of existing columns or fields can be widened\n   * using ALTER TABLE CHANGE COLUMN.\n   */\n  def isEnabled(protocol: Protocol, metadata: Metadata): Boolean = {\n    val isEnabled = DeltaConfigs.ENABLE_TYPE_WIDENING.fromMetaData(metadata)\n    if (isEnabled && !isSupported(protocol)) {\n      throw new IllegalStateException(\n        s\"Table property '${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' is \" +\n          s\"set on the table but this table version doesn't support table feature \" +\n          s\"'${TableFeatureProtocolUtils.propertyKey(TypeWideningTableFeature)}'.\")\n    }\n    isEnabled\n  }\n\n  /**\n   * Checks that the type widening table property wasn't disabled or enabled between the two given\n   * states, throws an errors if it was.\n   */\n  def ensureFeatureConsistentlyEnabled(\n      protocol: Protocol,\n      metadata: Metadata,\n      otherProtocol: Protocol,\n      otherMetadata: Metadata): Unit = {\n    if (isEnabled(protocol, metadata) != isEnabled(otherProtocol, otherMetadata)) {\n      throw DeltaErrors.metadataChangedException(None)\n    }\n  }\n\n  /**\n   * Returns whether the given type change is eligible for widening. This only checks atomic types.\n   * It is the responsibility of the caller to recurse into structs, maps and arrays.\n   *\n   * Type widening supports:\n   * - byte -> short -> int -> long.\n   * - float -> double.\n   * - date -> timestamp_ntz.\n   * - {byte, short, int} -> double.\n   * - decimal -> wider decimal.\n   * - {byte, short, int} -> decimal(10, 0) and wider.\n   * - long -> decimal(20, 0) and wider.\n   */\n  def isTypeChangeSupported(fromType: AtomicType, toType: AtomicType): Boolean =\n    (fromType, toType) match {\n      case (from, to) if from == to => true\n      // All supported type changes below are supposed to be widening, but to be safe, reject any\n      // non-widening change upfront.\n      case (from, to) if !Cast.canUpCast(from, to) => false\n      case (from: IntegralType, to: IntegralType) => from.defaultSize <= to.defaultSize\n      case (FloatType, DoubleType) => true\n      case (DateType, TimestampNTZType) => true\n      case (ByteType | ShortType | IntegerType, DoubleType) => true\n      case (from: DecimalType, to: DecimalType) => to.isWiderThan(from)\n      // Byte, Short, Integer are all stored as INT32 in parquet. The parquet readers support\n      // converting INT32 to Decimal(10, 0) and wider.\n      case (ByteType | ShortType | IntegerType, d: DecimalType) => d.isWiderThan(IntegerType)\n      // The parquet readers support converting INT64 to Decimal(20, 0) and wider.\n      case (LongType, d: DecimalType) => d.isWiderThan(LongType)\n      case _ => false\n    }\n\n  def isTypeChangeSupported(\n     fromType: AtomicType, toType: AtomicType, uniformIcebergCompatibleOnly: Boolean): Boolean =\n    isTypeChangeSupported(fromType, toType) &&\n      (!uniformIcebergCompatibleOnly ||\n        isTypeChangeSupportedByIceberg(fromType = fromType, toType = toType))\n\n  /**\n   * Returns whether the given type change can be applied during schema evolution. Only a\n   * subset of supported type changes are considered for schema evolution.\n   */\n  def isTypeChangeSupportedForSchemaEvolution(\n      fromType: AtomicType,\n      toType: AtomicType,\n      uniformIcebergCompatibleOnly: Boolean): Boolean = {\n    val supportedForSchemaEvolution = (fromType, toType) match {\n      case (from, to) if from == to => true\n      case (from, to) if !isTypeChangeSupported(from, to) => false\n      case (from: IntegralType, to: IntegralType) => from.defaultSize <= to.defaultSize\n      case (FloatType, DoubleType) => true\n      case (from: DecimalType, to: DecimalType) => to.isWiderThan(from)\n      case (DateType, TimestampNTZType) => true\n      case _ => false\n    }\n\n    supportedForSchemaEvolution && (\n      !uniformIcebergCompatibleOnly ||\n        isTypeChangeSupportedByIceberg(fromType = fromType, toType = toType)\n    )\n  }\n\n  /**\n   * Returns whether the given type change is supported by Iceberg, and by extension can be read\n   * using Uniform. See https://iceberg.apache.org/spec/#schema-evolution.\n   * Note that these are type promotions supported by Iceberg V1 & V2 (both support the same type\n   * promotions). Iceberg V3 will add support for date -> timestamp_ntz and void -> any but Uniform\n   * doesn't currently support Iceberg V3.\n   */\n  def isTypeChangeSupportedByIceberg(fromType: AtomicType, toType: AtomicType): Boolean =\n    (fromType, toType) match {\n      case (from, to) if from == to => true\n      case (from, to) if !isTypeChangeSupported(from, to) => false\n      case (from: IntegralType, to: IntegralType) => from.defaultSize <= to.defaultSize\n      case (FloatType, DoubleType) => true\n      case (from: DecimalType, to: DecimalType)\n        if from.scale == to.scale && from.precision <= to.precision => true\n      case _ => false\n    }\n\n  /**\n   * Asserts that the given table doesn't contain any unsupported type changes. This should never\n   * happen unless a non-compliant writer applied a type change that is not part of the feature\n   * specification.\n   */\n  def assertTableReadable(conf: SQLConf, protocol: Protocol, metadata: Metadata): Unit = {\n    if (conf.getConf(DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_UNSUPPORTED_TYPE_CHANGE_CHECK) ||\n      !isSupported(protocol) ||\n      !TypeWideningMetadata.containsTypeWideningMetadata(metadata.schema)) {\n      return\n    }\n\n    TypeWideningMetadata.getAllTypeChanges(metadata.schema).foreach {\n      case (_, TypeChange(_, from: AtomicType, to: AtomicType, _))\n        if isTypeChangeSupported(from, to) =>\n      // Char/Varchar/String type changes are allowed and independent from type widening.\n      // Implementations shouldn't record these type changes in the table metadata per the Delta\n      // spec, but in case that happen we really shouldn't block reading the table.\n      case (_, TypeChange(_,\n        _: StringType | CharType(_) | VarcharType(_),\n        _: StringType | CharType(_) | VarcharType(_), _)) =>\n      case (fieldPath, TypeChange(_, from: AtomicType, to: AtomicType, _))\n        if stableFeatureCanReadTypeChange(from, to) =>\n        val featureName = if (protocol.isFeatureSupported(TypeWideningPreviewTableFeature)) {\n          TypeWideningPreviewTableFeature\n        } else {\n          TypeWideningTableFeature\n        }\n        throw DeltaErrors.unsupportedTypeChangeInPreview(fieldPath, from, to, featureName)\n      case (fieldPath, invalidChange) =>\n        throw DeltaErrors.unsupportedTypeChangeInSchema(\n          fieldPath ++ invalidChange.fieldPath,\n          invalidChange.fromType,\n          invalidChange.toType\n        )\n    }\n  }\n\n  /**\n   * Whether the given type change is supported in the stable version of the feature. Used to\n   * provide a helpful error message during the preview phase if upgrading to Delta 4.0 would allow\n   * reading the table.\n   */\n  private def stableFeatureCanReadTypeChange(fromType: AtomicType, toType: AtomicType): Boolean =\n    (fromType, toType) match {\n      case (from, to) if from == to => true\n      case (from: IntegralType, to: IntegralType) => from.defaultSize <= to.defaultSize\n      case (FloatType, DoubleType) => true\n      case (DateType, TimestampNTZType) => true\n      case (ByteType | ShortType | IntegerType, DoubleType) => true\n      case (from: DecimalType, to: DecimalType) => to.isWiderThan(from)\n      // Byte, Short, Integer are all stored as INT32 in parquet. The parquet readers support\n      // converting INT32 to Decimal(10, 0) and wider.\n      case (ByteType | ShortType | IntegerType, d: DecimalType) => d.isWiderThan(IntegerType)\n      // The parquet readers support converting INT64 to Decimal(20, 0) and wider.\n      case (LongType, d: DecimalType) => d.isWiderThan(LongType)\n      case _ => false\n    }\n\n  /**\n   * Compares `from` and `to` and returns whether the type was widened, or, for nested types,\n   * whether one of the nested fields was widened.\n   */\n  def containsWideningTypeChanges(from: DataType, to: DataType): Boolean = (from, to) match {\n    case (from: StructType, to: StructType) =>\n      TypeWideningMetadata.collectTypeChanges(from, to).nonEmpty\n    case (from: MapType, to: MapType) =>\n      containsWideningTypeChanges(from.keyType, to.keyType) ||\n        containsWideningTypeChanges(from.valueType, to.valueType)\n    case (from: ArrayType, to: ArrayType) =>\n      containsWideningTypeChanges(from.elementType, to.elementType)\n    case (from: AtomicType, to: AtomicType) =>\n      isTypeChangeSupported(from, to)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/TypeWideningMetadata.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql.types._\n\n/**\n * Information corresponding to a single type change.\n * @param version   (Deprecated) The version of the table where the type change was made. This is\n *                  only populated by clients using the preview of type widening.\n * @param fromType  The original type before the type change.\n * @param toType    The new type after the type change.\n * @param fieldPath The path inside nested maps and arrays to the field where the type change was\n *                  made. Each path element is either `key`/`value` for maps or `element` for\n *                  arrays. The path is empty if the type change was applied inside a map or array.\n */\nprivate[delta] case class TypeChange(\n    version: Option[Long],\n    fromType: DataType,\n    toType: DataType,\n    fieldPath: Seq[String]) {\n  import TypeChange._\n\n  /** Serialize this type change to a [[Metadata]] object. */\n  def toMetadata: Metadata = {\n    val builder = new MetadataBuilder()\n    version.foreach(builder.putLong(TABLE_VERSION_METADATA_KEY, _))\n    builder\n      .putString(FROM_TYPE_METADATA_KEY, fromType.typeName)\n      .putString(TO_TYPE_METADATA_KEY, toType.typeName)\n    if (fieldPath.nonEmpty) {\n      builder.putString(FIELD_PATH_METADATA_KEY, fieldPath.mkString(\".\"))\n    }\n    builder.build()\n  }\n}\n\nprivate[delta] object TypeChange {\n  // tableVersion was a field present during the preview and removed afterwards. We preserve it if\n  // it's already present in the type change metadata of the table to avoid breaking older clients\n  // that use it to decide which files must be rewritten when dropping the feature.\n  val TABLE_VERSION_METADATA_KEY: String = \"tableVersion\"\n  val FROM_TYPE_METADATA_KEY: String = \"fromType\"\n  val TO_TYPE_METADATA_KEY: String = \"toType\"\n  val FIELD_PATH_METADATA_KEY: String = \"fieldPath\"\n\n   /** Deserialize this type change from a [[Metadata]] object. */\n  def fromMetadata(metadata: Metadata): TypeChange = {\n    val fieldPath = if (metadata.contains(FIELD_PATH_METADATA_KEY)) {\n      metadata.getString(FIELD_PATH_METADATA_KEY).split(\"\\\\.\").toSeq\n    } else {\n      Seq.empty\n    }\n    val version = if (metadata.contains(TABLE_VERSION_METADATA_KEY)) {\n      Some(metadata.getLong(TABLE_VERSION_METADATA_KEY))\n    } else {\n      None\n    }\n    TypeChange(\n      version,\n      fromType = DataType.fromDDL(metadata.getString(FROM_TYPE_METADATA_KEY)),\n      toType = DataType.fromDDL(metadata.getString(TO_TYPE_METADATA_KEY)),\n      fieldPath\n    )\n  }\n}\n\n/**\n * Represents all type change information for a single struct field\n * @param typeChanges The type changes that have been applied to the field.\n */\nprivate[delta] case class TypeWideningMetadata(typeChanges: Seq[TypeChange]) {\n\n  import TypeWideningMetadata._\n\n  /**\n   * Add the type changes to the metadata of the given field, preserving any pre-existing type\n   * widening metadata.\n   */\n  def appendToField(field: StructField): StructField = {\n    if (typeChanges.isEmpty) return field\n\n    val existingTypeChanges = fromField(field).map(_.typeChanges).getOrElse(Seq.empty)\n    val allTypeChanges = existingTypeChanges ++ typeChanges\n\n    val newMetadata = new MetadataBuilder().withMetadata(field.metadata)\n      .putMetadataArray(TYPE_CHANGES_METADATA_KEY, allTypeChanges.map(_.toMetadata).toArray)\n      .build()\n    field.copy(metadata = newMetadata)\n  }\n}\n\nprivate[delta] object TypeWideningMetadata extends DeltaLogging {\n  val TYPE_CHANGES_METADATA_KEY: String = \"delta.typeChanges\"\n\n  /** Read the type widening metadata from the given field. */\n  def fromField(field: StructField): Option[TypeWideningMetadata] = {\n    Option.when(field.metadata.contains(TYPE_CHANGES_METADATA_KEY)) {\n      val typeChanges = field.metadata.getMetadataArray(TYPE_CHANGES_METADATA_KEY)\n        .map { changeMetadata =>\n          TypeChange.fromMetadata(changeMetadata)\n        }.toSeq\n      TypeWideningMetadata(typeChanges)\n    }\n  }\n\n  /**\n   * Computes the type changes from `oldSchema` to `schema` and adds corresponding type change\n   * metadata to `schema`.\n   */\n  def addTypeWideningMetadata(\n      txn: OptimisticTransaction,\n      schema: StructType,\n      oldSchema: StructType): StructType = {\n\n    if (!TypeWidening.isEnabled(txn.protocol, txn.metadata)) return schema\n\n    if (DataType.equalsIgnoreNullability(schema, oldSchema)) return schema\n\n    val changesToRecord = mutable.Buffer.empty[TypeChange]\n    val schemaWithMetadata = SchemaMergingUtils.transformColumns(schema, oldSchema) {\n      case (_, newField, Some(oldField), _) =>\n        var typeChanges = collectTypeChangesInStructField(\n          oldField.dataType,\n          newField.dataType,\n          logNonWideningChanges = true\n        )\n        // The version field isn't used anymore but we need to populate it in case the table uses\n        // the preview feature, as preview clients may then rely on the field being present.\n        if (txn.protocol.isFeatureSupported(TypeWideningPreviewTableFeature)) {\n          typeChanges = typeChanges.map { change =>\n            change.copy(version = Some(txn.getFirstAttemptVersion))\n          }\n        }\n\n        changesToRecord ++= typeChanges\n        TypeWideningMetadata(typeChanges).appendToField(newField)\n      case (_, newField, None, _) =>\n        // The field was just added, no need to process.\n        newField\n    }\n\n    if (changesToRecord.nonEmpty) {\n      recordDeltaEvent(\n        deltaLog = txn.snapshot.deltaLog,\n        opType = \"delta.typeWidening.typeChanges\",\n        data = Map(\n          \"changes\" -> changesToRecord.map { change =>\n            Map(\n              \"fromType\" -> change.fromType.sql,\n              \"toType\" -> change.toType.sql)\n          }\n        ))\n    }\n    schemaWithMetadata\n  }\n\n  /**\n   * Recursively compare `from` and `to` to collect all primitive widening type changes, including\n   * in nested structs, maps and arrays.\n   */\n  def collectTypeChanges(from: StructType, to: StructType): Seq[TypeChange] = {\n    val changes = mutable.Buffer.empty[TypeChange]\n\n    SchemaMergingUtils.transformColumns(schema = to, other = from) {\n      case (path, newField, Some(oldField), _) =>\n        changes ++= collectTypeChangesInStructField(\n          oldField.dataType,\n          newField.dataType,\n          logNonWideningChanges = false\n        ).map { change =>\n          change.copy(fieldPath = path ++ Seq(newField.name) ++ change.fieldPath)\n        }\n        newField\n      case (_, field, _, _) => field\n    }\n    changes.toSeq\n  }\n\n  /**\n   * Collects all primitive widening type changes inside a single struct field. This includes type\n   * changes inside nested maps and arrays but not inside other nested structs.\n   * @param fromType The previous type of the struct field.\n   * @param toType The new type of the struct field.\n   * @param logNonWideningChanges Whether to log / fail in tests if a non-widening change is found.\n   */\n  private def collectTypeChangesInStructField(\n      fromType: DataType,\n      toType: DataType,\n      logNonWideningChanges: Boolean): Seq[TypeChange] = (fromType, toType) match {\n    case (from: MapType, to: MapType) =>\n      collectTypeChangesInStructField(from.keyType, to.keyType, logNonWideningChanges)\n        .map { typeChange =>\n          typeChange.copy(fieldPath = \"key\" +: typeChange.fieldPath)\n        } ++\n      collectTypeChangesInStructField(from.valueType, to.valueType, logNonWideningChanges)\n        .map { typeChange =>\n          typeChange.copy(fieldPath = \"value\" +: typeChange.fieldPath)\n        }\n    case (from: ArrayType, to: ArrayType) =>\n      collectTypeChangesInStructField(from.elementType, to.elementType, logNonWideningChanges)\n        .map { typeChange =>\n          typeChange.copy(fieldPath = \"element\" +: typeChange.fieldPath)\n        }\n    case (fromType: AtomicType, toType: AtomicType) if fromType != toType &&\n        TypeWidening.isTypeChangeSupported(fromType, toType) =>\n      Seq(TypeChange(\n        version = None,\n        fromType,\n        toType,\n        fieldPath = Seq.empty\n      ))\n    // Char/Varchar/String and collation type changes are expected and unrelated to type widening.\n    // We don't record them in the table schema metadata and don't log them as unexpected type\n    // changes either.\n    case (fromType: AtomicType, toType: AtomicType) if isStringTypeChange(fromType, toType) =>\n      Seq.empty\n    // Don't recurse inside structs, `collectTypeChanges` should be called directly on each struct\n    // fields instead to only collect type changes inside these fields.\n    case (_: StructType, _: StructType) => Seq.empty\n    case _ =>\n      deltaAssert(!logNonWideningChanges || fromType == toType,\n        name = \"typeWidening.unexpectedTypeChange\",\n        msg = s\"Trying to apply an unsupported type change: $fromType to $toType\",\n        data = Map(\n          \"fromType\" -> fromType.sql,\n          \"toType\" -> toType.sql\n        )\n      )\n      Seq.empty\n  }\n\n  /** Returns whether the given type change is Char/Varchar/String or collation type change. */\n  private def isStringTypeChange(from: AtomicType, to: AtomicType): Boolean = (from, to) match {\n    case (\n      _: StringType | CharType(_) | VarcharType(_),\n      _: StringType | CharType(_) | VarcharType(_)) => true\n    case _ => false\n  }\n\n  /**\n   * Change the `tableVersion` value in the type change metadata present in `schema`. Used during\n   * conflict resolution to update the version associated with the transaction is incremented.\n   *\n   * Note: The `tableVersion` field is only populated for tables that use the preview of type\n   * widening, we could remove this if/when there are no more tables using the preview of the\n   * feature.\n   */\n  def updateTypeChangeVersion(schema: StructType, fromVersion: Long, toVersion: Long): StructType =\n    SchemaMergingUtils.transformColumns(schema) {\n      case (_, field, _) =>\n        fromField(field) match {\n          case Some(typeWideningMetadata) =>\n            val updatedTypeChanges = typeWideningMetadata.typeChanges.map {\n              case typeChange if typeChange.version.contains(fromVersion) =>\n                typeChange.copy(version = Some(toVersion))\n              case olderTypeChange => olderTypeChange\n            }\n            val newMetadata = new MetadataBuilder().withMetadata(field.metadata)\n              .putMetadataArray(\n                TYPE_CHANGES_METADATA_KEY,\n                updatedTypeChanges.map(_.toMetadata).toArray)\n              .build()\n            field.copy(metadata = newMetadata)\n\n          case None => field\n        }\n    }\n\n  /**\n   * Remove the type widening metadata from all the fields in the given schema.\n   * Return the cleaned schema and a list of fields with their path that had type widening metadata.\n   */\n  def removeTypeWideningMetadata(schema: StructType)\n    : (StructType, Seq[(Seq[String], StructField)]) = {\n    if (!containsTypeWideningMetadata(schema)) return (schema, Seq.empty)\n\n    val changes = mutable.Buffer.empty[(Seq[String], StructField)]\n    val newSchema = SchemaMergingUtils.transformColumns(schema) {\n      case (fieldPath: Seq[String], field: StructField, _)\n        if field.metadata.contains(TYPE_CHANGES_METADATA_KEY) =>\n          changes.append((fieldPath, field))\n          val cleanMetadata = new MetadataBuilder()\n            .withMetadata(field.metadata)\n            .remove(TYPE_CHANGES_METADATA_KEY)\n            .build()\n          field.copy(metadata = cleanMetadata)\n      case (_, field: StructField, _) => field\n    }\n    newSchema -> changes.toSeq\n  }\n\n  /** Recursively checks whether any struct field in the schema contains type widening metadata. */\n  def containsTypeWideningMetadata(schema: StructType): Boolean =\n    schema.existsRecursively {\n      case s: StructType => s.exists(_.metadata.contains(TYPE_CHANGES_METADATA_KEY))\n      case _ => false\n    }\n\n  /**\n   * Return all type changes recorded in the table schema.\n   * @return A list of tuples (field path, type change).\n   */\n  def getAllTypeChanges(schema: StructType): Seq[(Seq[String], TypeChange)] = {\n    if (!containsTypeWideningMetadata(schema)) return Seq.empty\n\n    val allStructFields = SchemaUtils.filterRecursively(schema, checkComplexTypes = true) {\n      _ => true\n    }\n\n    def getTypeChanges(field: StructField): Seq[TypeChange] =\n      fromField(field)\n        .map(_.typeChanges)\n        .getOrElse(Seq.empty)\n\n    allStructFields.flatMap { case (fieldPath, field) =>\n      getTypeChanges(field).map((fieldPath :+ field.name, _))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/TypeWideningMode.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.catalyst.analysis.DecimalPrecisionTypeCoercion\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.AllowAutomaticWideningMode\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{AtomicType, ByteType, DecimalType, IntegerType, IntegralType, LongType, ShortType}\n\n/**\n * A type widening mode captures a specific set of type changes that are allowed to be applied.\n * Currently:\n *  - NoTypeWidening: No type change is allowed.\n *  - AllTypeWidening: Allows widening to the target type using any supported type change.\n *  - TypeEvolution: Only allows widening to the target type if the type change is eligible to be\n *      applied automatically during schema evolution.\n *  - AllTypeWideningToCommonWiderType: Allows widening to a common (possibly different) wider type\n *      using any supported type change.\n *  - TypeEvolutionToCommonWiderType: Allows widening to a common (possibly different) wider type\n *      using only type changes that are eligible to be applied automatically during schema\n *      evolution.\n *\n * TypeEvolution modes can be restricted to only type changes supported by Iceberg by passing\n * `uniformIcebergCompatibleOnly = truet`, to ensure that we don't automatically apply a type change\n * that would break Iceberg compatibility.\n */\nsealed trait TypeWideningMode extends DeltaLogging {\n  def getWidenedType(fromType: AtomicType, toType: AtomicType): Option[AtomicType]\n\n  def shouldWidenTo(fromType: AtomicType, toType: AtomicType): Boolean =\n    getWidenedType(fromType, toType).contains(toType)\n}\n\nobject TypeWideningMode {\n  /**\n   * No type change allowed. Typically because type widening and/or schema evolution isn't enabled.\n   */\n  case object NoTypeWidening extends TypeWideningMode {\n    override def getWidenedType(fromType: AtomicType, toType: AtomicType): Option[AtomicType] = None\n  }\n\n  /** All supported type widening changes are allowed. */\n  case object AllTypeWidening extends TypeWideningMode {\n    override def getWidenedType(fromType: AtomicType, toType: AtomicType): Option[AtomicType] =\n      Option.when(TypeWidening.isTypeChangeSupported(fromType = fromType, toType = toType))(toType)\n  }\n\n  /**\n   * Type changes that are eligible to be applied automatically during schema evolution are allowed.\n   *\n   * uniformIcebergCompatibleOnly: Restricts widenings to those supported by Iceberg.\n   * allowAutomaticWidening: Controls widening behavior. Options:\n   *   - 'always': enables all supported widenings,\n   *   - 'same_family_type': uses default behavior,\n   *   - 'never': disables all widenings.\n   */\n  case class TypeEvolution(\n      uniformIcebergCompatibleOnly: Boolean,\n      allowAutomaticWidening: AllowAutomaticWideningMode.Value) extends TypeWideningMode {\n    override def getWidenedType(fromType: AtomicType, toType: AtomicType): Option[AtomicType] = {\n      Option.when(canWiden(fromType, toType))(toType).orElse {\n        logMissedWidening(fromType = fromType, toType = toType)\n        None\n      }\n    }\n\n    private def logMissedWidening(fromType: AtomicType, toType: AtomicType): Unit = {\n      // Check if widening is possible under the least restricting conditions.\n      val allowAllTypeEvolution = TypeEvolution(\n        uniformIcebergCompatibleOnly = false,\n        allowAutomaticWidening = AllowAutomaticWideningMode.ALWAYS)\n      if (allowAllTypeEvolution.canWiden(fromType, toType)) {\n        recordDeltaEvent(null,\n          opType = \"delta.typeWidening.missedAutomaticWidening\",\n          data = Map(\n            \"fromType\" -> fromType.sql,\n            \"toType\" -> toType.sql,\n            \"uniformIcebergCompatibleOnly\" -> uniformIcebergCompatibleOnly,\n            \"allowAutomaticWidening\" -> allowAutomaticWidening\n          ))\n      }\n    }\n\n    private def canWiden(fromType: AtomicType, toType: AtomicType): Boolean = {\n      if (allowAutomaticWidening == AllowAutomaticWideningMode.ALWAYS) {\n        TypeWidening.isTypeChangeSupported(\n          fromType = fromType,\n          toType = toType,\n          uniformIcebergCompatibleOnly)\n      } else if (allowAutomaticWidening == AllowAutomaticWideningMode.SAME_FAMILY_TYPE) {\n        TypeWidening.isTypeChangeSupportedForSchemaEvolution(\n          fromType = fromType, toType = toType, uniformIcebergCompatibleOnly)\n      } else {\n        false\n      }\n    }\n  }\n\n  /**\n   * All supported type widening changes are allowed. Unlike [[AllTypeWidening]], this also allows\n   * widening `to` to `from`, and for decimals, widening to a different decimal type that is wider\n   * than both input types. Use for example when merging two unrelated schemas and we want just want\n   * to find a wider schema to use.\n   */\n  case object AllTypeWideningToCommonWiderType extends TypeWideningMode {\n    private def getDecimalType(t: IntegralType): DecimalType = {\n      t match {\n        case _: ByteType => DecimalType(3, 0)\n        case _: ShortType => DecimalType(5, 0)\n        case _: IntegerType => DecimalType(10, 0)\n        case _: LongType => DecimalType(20, 0)\n      }\n    }\n\n    override def getWidenedType(left: AtomicType, right: AtomicType): Option[AtomicType] = {\n      val allowIntegralDecimalCoercion: Boolean =\n        SQLConf.get.getConf(DeltaSQLConf.DELTA_TYPE_WIDENING_ALLOW_INTEGRAL_DECIMAL_COERCION)\n      (left, right) match {\n        case (l, r) if TypeWidening.isTypeChangeSupported(l, r) => Some(r)\n        case (l, r) if TypeWidening.isTypeChangeSupported(r, l) => Some(l)\n        case (l: IntegralType, r: DecimalType) if allowIntegralDecimalCoercion =>\n          getWidenedType(getDecimalType(l), r)\n        case (l: DecimalType, r: IntegralType) if allowIntegralDecimalCoercion =>\n          getWidenedType(getDecimalType(r), l)\n        case (l: DecimalType, r: DecimalType) =>\n          val wider = DecimalPrecisionTypeCoercion.widerDecimalType(l, r)\n          Option.when(\n            TypeWidening.isTypeChangeSupported(l, wider) &&\n            TypeWidening.isTypeChangeSupported(r, wider))(wider)\n        case _ => None\n      }\n    }\n  }\n\n  /**\n   * Type changes that are eligible to be applied automatically during schema evolution are allowed.\n   * Can be restricted to only type changes supported by Iceberg. Unlike [[TypeEvolution]], this\n   * also allows widening `to` to `from`, and for decimals, widening to a different decimal type\n   * that is wider han both input types. Use for example when merging two unrelated schemas and we\n   * want just want to find a wider schema to use.\n   */\n  case class TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly: Boolean)\n    extends TypeWideningMode {\n    override def getWidenedType(left: AtomicType, right: AtomicType): Option[AtomicType] = {\n      def typeChangeSupported: (AtomicType, AtomicType) => Boolean =\n        TypeWidening.isTypeChangeSupportedForSchemaEvolution(_, _, uniformIcebergCompatibleOnly)\n\n      (left, right) match {\n        case (l, r) if typeChangeSupported(l, r) => Some(r)\n        case (l, r) if typeChangeSupported(r, l) => Some(l)\n        case (l: DecimalType, r: DecimalType) =>\n          val wider = DecimalPrecisionTypeCoercion.widerDecimalType(l, r)\n          Option.when(typeChangeSupported(l, wider) && typeChangeSupported(r, wider))(wider)\n        case _ => None\n      }\n    }\n  }\n\n  /**\n   * Same as TypeEvolution with AllowAutomaticWideningMode.ALWAYS, but\n   * additionally gets the wider decimal type given two types that are\n   * DecimalType-compatible.\n   */\n  case object AllTypeWideningWithDecimalCoercion extends TypeWideningMode {\n    private def getDecimalType(t: IntegralType): DecimalType = {\n      t match {\n        case _: ByteType => DecimalType(3, 0)\n        case _: ShortType => DecimalType(5, 0)\n        case _: IntegerType => DecimalType(10, 0)\n        case _: LongType => DecimalType(20, 0)\n      }\n    }\n\n    private def getWiderDecimalTypeWithInteger(\n        integralType: IntegralType,\n        decimalType: DecimalType): Option[DecimalType] = {\n      val wider = DecimalPrecisionTypeCoercion.widerDecimalType(\n        getDecimalType(integralType), decimalType)\n      Option.when(\n        TypeWidening.isTypeChangeSupported(getDecimalType(integralType), wider) &&\n          TypeWidening.isTypeChangeSupported(decimalType, wider))(wider)\n    }\n\n    override def getWidenedType(fromType: AtomicType, toType: AtomicType): Option[AtomicType] =\n      (fromType, toType) match {\n        case (from, to) if TypeWidening.isTypeChangeSupported(from, to) => Some(to)\n        case (l: IntegralType, r: DecimalType) =>\n          getWiderDecimalTypeWithInteger(l, r)\n        case (l: DecimalType, r: IntegralType) =>\n          getWiderDecimalTypeWithInteger(r, l)\n        case (l: DecimalType, r: DecimalType) =>\n          val wider = DecimalPrecisionTypeCoercion.widerDecimalType(l, r)\n          Option.when(\n            TypeWidening.isTypeChangeSupported(l, wider) &&\n              TypeWidening.isTypeChangeSupported(r, wider))(wider)\n        case _ => None\n      }\n  }\n\n  /**\n   * Same as TypeEvolution with AllowAutomaticWideningMode.SAME_FAMILY_TYPE,\n   * but additionally gets the wider decimal type given two types that are\n   * DecimalType-compatible.\n   */\n  case object TypeEvolutionWithDecimalCoercion extends TypeWideningMode {\n    override def getWidenedType(fromType: AtomicType, toType: AtomicType): Option[AtomicType] = {\n      def typeChangeSupported: (AtomicType, AtomicType) => Boolean =\n        TypeWidening.isTypeChangeSupportedForSchemaEvolution(_, _,\n          uniformIcebergCompatibleOnly = false)\n\n      (fromType, toType) match {\n        case (from, to) if typeChangeSupported(from, to) => Some(to)\n        case (l: DecimalType, r: DecimalType) =>\n          val wider = DecimalPrecisionTypeCoercion.widerDecimalType(l, r)\n          Option.when(typeChangeSupported(l, wider) && typeChangeSupported(r, wider))(wider)\n        case _ => None\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/UniversalFormat.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Action, Metadata, Protocol}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.types.{ByteType, CalendarIntervalType, NullType, ShortType, TimestampNTZType}\n\n/**\n * Utils to validate the Universal Format (UniForm) Delta feature (NOT a table feature).\n *\n * The UniForm Delta feature governs and implements the actual conversion of Delta metadata into\n * other formats.\n *\n * UniForm supports both Iceberg and Hudi. When `delta.universalFormat.enabledFormats` contains\n * \"iceberg\", we say that Universal Format (Iceberg) is enabled. When it contains \"hudi\", we say\n * that Universal Format (Hudi) is enabled.\n *\n * [[enforceInvariantsAndDependencies]] ensures that all of UniForm's requirements for the\n * specified format are met (e.g. for 'iceberg' that IcebergCompatV1 or V2 is enabled).\n * It doesn't verify that its nested requirements are met (e.g. IcebergCompat's requirements,\n * like Column Mapping). That is the responsibility of format-specific validations such as\n * [[IcebergCompatV1.enforceInvariantsAndDependencies]]\n * and [[IcebergCompatV2.enforceInvariantsAndDependencies]].\n *\n *\n * Note that UniForm (Iceberg) depends on IcebergCompat, but IcebergCompat does not\n * depend on or require UniForm (Iceberg). It is perfectly valid for a Delta table to have\n * IcebergCompatV1 or V2 enabled but UniForm (Iceberg) not enabled.\n */\nobject UniversalFormat extends DeltaLogging {\n\n  val ICEBERG_FORMAT = \"iceberg\"\n  val HUDI_FORMAT = \"hudi\"\n  val SUPPORTED_FORMATS = Set(HUDI_FORMAT, ICEBERG_FORMAT)\n\n  /**\n   * Check if the operation is CREATE/REPLACE TABLE or REORG UPGRADE UNIFORM commands.\n   *\n   * @param op the delta operation to be checked.\n   * @return whether the operation is create or reorg.\n   */\n  def isCreatingOrReorgTable(op: Option[DeltaOperations.Operation]): Boolean = op match {\n    case Some(_: DeltaOperations.CreateTable) |\n         Some(_: DeltaOperations.UpgradeUniformProperties) |\n         // REPLACE TABLE is also considered creating table to preserve the\n         // the semantics for `isCreatingNewTable` in `OptimisticTransaction`.\n         Some(_: DeltaOperations.ReplaceTable) =>\n      true\n    // this is to conform with the semantics in `enforceDependenciesInConfiguration`\n    case None => true\n    case _ => false\n  }\n\n  /**\n   * Check if the operation is REORG UPGRADE UNIFORM command.\n   *\n   * @param op the delta operation to be checked.\n   * @return whether the operation is REORG UPGRADE UNIFORM.\n   */\n  def isReorgUpgradeUniform(op: Option[DeltaOperations.Operation]): Boolean = op match {\n    case Some(_: DeltaOperations.UpgradeUniformProperties) => true\n    case _ => false\n  }\n\n  def icebergEnabled(metadata: Metadata): Boolean = {\n    DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.fromMetaData(metadata).contains(ICEBERG_FORMAT)\n  }\n\n  def hudiEnabled(metadata: Metadata): Boolean = {\n    DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.fromMetaData(metadata).contains(HUDI_FORMAT)\n  }\n\n  def hudiEnabled(properties: Map[String, String]): Boolean = {\n    properties.get(DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.key)\n      .exists(value => value.contains(HUDI_FORMAT))\n  }\n\n  def icebergEnabled(properties: Map[String, String]): Boolean = {\n    properties.get(DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.key)\n      .exists(value => value.contains(ICEBERG_FORMAT))\n  }\n\n  /**\n   * Expected to be called after the newest metadata and protocol have been ~ finalized.\n   *\n   * @return tuple of options of (updatedProtocol, updatedMetadata). For either action, if no\n   *         updates need to be applied, will return None.\n   */\n  def enforceInvariantsAndDependencies(\n      spark: SparkSession,\n      catalogTable: Option[CatalogTable],\n      snapshot: Snapshot,\n      newestProtocol: Protocol,\n      newestMetadata: Metadata,\n      operation: Option[DeltaOperations.Operation],\n      actions: Seq[Action]): (Option[Protocol], Option[Metadata]) = {\n    enforceHudiDependencies(newestMetadata, snapshot)\n    enforceIcebergInvariantsAndDependencies(\n      spark, catalogTable,\n      snapshot,\n      newestProtocol, newestMetadata, operation, actions)\n  }\n\n  /**\n   * If you are enabling Hudi, this method ensures that Deletion Vectors are not enabled. New\n   * conditions may be added here in the future to make sure the source is compatible with Hudi.\n   * @param newestMetadata the newest metadata\n   * @param snapshot current snapshot\n   * @return N/A, throws exception if condition is not met\n   */\n  def enforceHudiDependencies(newestMetadata: Metadata, snapshot: Snapshot): Any = {\n    if (hudiEnabled(newestMetadata)) {\n      if (DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(newestMetadata)) {\n        throw DeltaErrors.uniFormHudiDeleteVectorCompat()\n      }\n      SchemaUtils.findAnyTypeRecursively(newestMetadata.schema) { f =>\n        f.isInstanceOf[NullType] | f.isInstanceOf[ByteType] | f.isInstanceOf[ShortType] |\n        f.isInstanceOf[TimestampNTZType]\n      } match {\n        case Some(unsupportedType) =>\n          throw DeltaErrors.uniFormHudiSchemaCompat(unsupportedType)\n        case _ =>\n      }\n    }\n  }\n\n  /**\n   * If you are enabling Universal Format (Iceberg), this method ensures that at least one of\n   * IcebergCompat is enabled. If you are disabling Universal Format (Iceberg), this method\n   * will leave the current IcebergCompat version untouched.\n   *\n   * @return tuple of options of (updatedProtocol, updatedMetadata). For either action, if no\n   *         updates need to be applied, will return None.\n   */\n  def enforceIcebergInvariantsAndDependencies(\n      spark: SparkSession,\n      catalogTable: Option[CatalogTable],\n      snapshot: Snapshot,\n      newestProtocol: Protocol,\n      newestMetadata: Metadata,\n      operation: Option[DeltaOperations.Operation],\n      actions: Seq[Action]): (Option[Protocol], Option[Metadata]) = {\n\n    val prevMetadata = snapshot.metadata\n    val uniformIcebergWasEnabled = UniversalFormat.icebergEnabled(prevMetadata)\n    val uniformIcebergIsEnabled = UniversalFormat.icebergEnabled(newestMetadata)\n    val tableId = newestMetadata.id\n    var changed = false\n\n    val (uniformProtocol, uniformMetadata) =\n      (uniformIcebergWasEnabled, uniformIcebergIsEnabled) match {\n        case (_, false) => (None, None) // Ignore\n        case (_, true) => // Enabling now or already-enabled\n          val icebergCompatWasEnabled = IcebergCompat.isAnyEnabled(prevMetadata)\n          val icebergCompatIsEnabled = IcebergCompat.isAnyEnabled(newestMetadata)\n\n          if (icebergCompatIsEnabled) {\n            (None, None)\n          } else if (icebergCompatWasEnabled) {\n            // IcebergCompat is being disabled. We need to also disable Universal Format (Iceberg)\n            val remainingSupportedFormats = DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS\n              .fromMetaData(newestMetadata)\n              .filterNot(_ == UniversalFormat.ICEBERG_FORMAT)\n\n            val newConfiguration = if (remainingSupportedFormats.isEmpty) {\n              newestMetadata.configuration - DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.key\n            } else {\n              newestMetadata.configuration ++\n                Map(DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.key ->\n                  remainingSupportedFormats.mkString(\",\"))\n            }\n\n            logInfo(log\"[${MDC(DeltaLogKeys.TABLE_ID, tableId)}] \" +\n              log\"IcebergCompat is being disabled. Auto-disabling Universal Format (Iceberg), too.\")\n\n            (None, Some(newestMetadata.copy(configuration = newConfiguration)))\n          } else {\n            throw DeltaErrors.uniFormIcebergRequiresIcebergCompat()\n          }\n      }\n\n    var protocolToCheck = uniformProtocol.getOrElse(newestProtocol)\n    var metadataToCheck = uniformMetadata.getOrElse(newestMetadata)\n    changed = uniformProtocol.nonEmpty || uniformMetadata.nonEmpty\n    var protocolUpdate: Option[Protocol] = None\n    var metadataUpdate: Option[Metadata] = None\n\n    val compatChecks: Seq[\n      (SparkSession, Option[CatalogTable], Snapshot, Protocol, Metadata,\n        Option[DeltaOperations.Operation],\n        Seq[Action]) => (Option[Protocol], Option[Metadata])] = Seq(\n      IcebergCompatV1.enforceInvariantsAndDependencies,\n      IcebergCompatV2.enforceInvariantsAndDependencies\n    )\n    compatChecks.foreach { compatCheck =>\n      val updates = compatCheck(\n        spark, catalogTable, snapshot, protocolToCheck, metadataToCheck, operation, actions\n      )\n      protocolUpdate = updates._1\n      metadataUpdate = updates._2\n      protocolToCheck = protocolUpdate.getOrElse(protocolToCheck)\n      metadataToCheck = metadataUpdate.getOrElse(metadataToCheck)\n      changed ||= protocolUpdate.nonEmpty || metadataUpdate.nonEmpty\n    }\n\n    if (changed) {\n      (\n        protocolUpdate.orElse(Some(protocolToCheck)),\n        metadataUpdate.orElse(Some(metadataToCheck))\n      )\n    } else {\n      (None, None)\n    }\n  }\n\n  /**\n   * This method is used to build UniForm metadata dependencies closure.\n   * It checks configuration conflicts and adds missing properties.\n   * It will call [[enforceIcebergInvariantsAndDependencies]] to perform the actual check.\n   * @param configuration the original metadata configuration.\n   * @return updated configuration if any changes are required,\n   *         otherwise the original configuration.\n   */\n  def enforceDependenciesInConfiguration(\n      spark: SparkSession,\n      catalogTable: CatalogTable,\n      configuration: Map[String, String],\n      snapshot: Snapshot): Map[String, String] = {\n    var metadata = snapshot.metadata.copy(configuration = configuration)\n\n    // Check UniversalFormat related property dependencies\n    val (_, universalMetadata) = UniversalFormat.enforceInvariantsAndDependencies(\n      spark,\n      catalogTable = Some(catalogTable),\n      snapshot,\n      newestProtocol = snapshot.protocol,\n      newestMetadata = metadata,\n      operation = None,\n      actions = Seq()\n    )\n\n    universalMetadata match {\n      case Some(valid) => valid.configuration\n      case _ => configuration\n    }\n  }\n\n  val ICEBERG_TABLE_TYPE_KEY = \"table_type\"\n\n  /**\n   * HiveTableOperations ensures table_type is 'ICEBERG' when uniform is enabled\n   * This enforceSupportInCatalog ensure table_type is not 'ICEBERG' when uniform is not enabled\n   *\n   * @param table    catalogTable before change\n   * @param metadata snapshot metadata\n   * @return the converted catalog, or None if no change is made\n   */\n  def enforceSupportInCatalog(table: CatalogTable, metadata: Metadata): Option[CatalogTable] = {\n    val icebergInCatalog = table.properties.get(ICEBERG_TABLE_TYPE_KEY) match {\n      case Some(value) => value.equalsIgnoreCase(ICEBERG_FORMAT)\n      case _ => false\n    }\n\n    (icebergEnabled(metadata), icebergInCatalog) match {\n      case (false, true) =>\n        Some(table.copy(properties =\n          table.properties - ICEBERG_TABLE_TYPE_KEY))\n      case _ => None\n    }\n  }\n}\n\n/** Class to facilitate the conversion of Delta into other table formats. */\nabstract class UniversalFormatConverter {\n  /** The current Spark session. */\n  def spark: SparkSession = SparkSession.active\n\n  /**\n   * Perform an asynchronous conversion.\n   *\n   * This will start an async job to run the conversion, unless there already is an async conversion\n   * running for this table. In that case, it will queue up the provided snapshot to be run after\n   * the existing job completes.\n   */\n  def enqueueSnapshotForConversion(\n    snapshotToConvert: Snapshot,\n    txn: CommittedTransaction): Unit\n\n  /**\n   * Perform a blocking conversion when performing an OptimisticTransaction\n   * on a delta table.\n   *\n   * @param snapshotToConvert the snapshot that needs to be converted to Iceberg\n   * @param txn the transaction that triggers the conversion. Used as a hint to\n   *            avoid recomputing old metadata. It must contain the catalogTable\n   *            this conversion targets.\n   * @return Converted Delta version and commit timestamp\n   */\n  def convertSnapshot(\n    snapshotToConvert: Snapshot, txn: CommittedTransaction): Option[(Long, Long)]\n\n  /**\n   * Perform a blocking conversion for the given catalogTable\n   *\n   * @param snapshotToConvert the snapshot that needs to be converted to Iceberg\n   * @param catalogTable the catalogTable this conversion targets.\n   * @return Converted Delta version and commit timestamp\n   */\n  def convertSnapshot(\n      snapshotToConvert: Snapshot, catalogTable: CatalogTable): Option[(Long, Long)]\n\n  /**\n   * Fetch the delta version corresponding to the latest conversion.\n   * @param snapshot the snapshot to be converted\n   * @param table the catalogTable with info of previous conversions\n   * @return None if no previous conversion found\n   */\n  def loadLastDeltaVersionConverted(snapshot: Snapshot, table: CatalogTable): Option[Long]\n}\n\nobject IcebergConstants {\n  val ICEBERG_TBLPROP_METADATA_LOCATION = \"metadata_location\"\n  val ICEBERG_PROVIDER = \"iceberg\"\n  val ICEBERG_NAME_MAPPING_PROPERTY = \"schema.name-mapping.default\"\n\n  // Reserved field ID for the `_row_id` column\n  // Iceberg spec: https://iceberg.apache.org/spec/?h=row#reserved-field-ids\n  val ICEBERG_ROW_TRACKING_ROW_ID_FIELD_ID = 2147483540L\n  // Reserved field ID for the `_last_updated_sequence_number` column\n  val ICEBERG_ROW_TRACKING_LAST_UPDATED_SEQUENCE_NUMBER_FIELD_ID = 2147483539L\n}\n\nobject HudiConstants {\n  val HUDI_PROVIDER = \"hudi\"\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/UpdateExpressionsSupport.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.AnalysisHelper\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.{InternalRow, SQLConfHelper}\nimport org.apache.spark.sql.catalyst.analysis.Resolver\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, CodeGenerator, ExprCode}\nimport org.apache.spark.sql.catalyst.expressions.codegen.Block._\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}\nimport org.apache.spark.sql.catalyst.plans.logical.TargetOnlyStructFieldBehavior\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Trait with helper functions to generate expressions to update target columns, even if they are\n * nested fields.\n */\ntrait UpdateExpressionsSupport extends SQLConfHelper with AnalysisHelper with DeltaLogging {\n\n  /**\n   * Specifies an operation that updates a target column with the given expression.\n   * The target column may or may not be a nested field and it is specified as a full quoted name\n   * or as a sequence of split into parts.\n   *\n   * @param targetColNameParts The name parts of the target column\n   * @param updateExpr The expression to update the column with\n   * @param targetOnlyStructFieldBehavior Determines the nullness of target-only struct fields.\n   *                                      Note: This parameter only takes effect when schema\n   *                                      evolution is enabled; otherwise it is ignored.\n   */\n  case class UpdateOperation(\n      targetColNameParts: Seq[String],\n      updateExpr: Expression,\n      targetOnlyStructFieldBehavior: TargetOnlyStructFieldBehavior.Value)\n\n  /**\n   * The following trait and classes define casting behaviors to use in `castIfNeeded()`.\n   * @param resolveStructsByName    Whether struct fields should be resolved by name or by position\n   *                                during struct cast.\n   * @param allowMissingStructField Whether missing struct fields are allowed in the data to cast.\n   *                                Only relevant when struct fields are resolved by name.\n   *                                When true, missing struct fields in the input are set to null.\n   *                                When false, an error is thrown.\n   *                                Note: this should be set to true for schema evolution to work as\n   *                                the target schema may typically contain new struct fields not\n   *                                present in the input.\n   */\n  sealed trait CastingBehavior {\n    val resolveStructsByName: Boolean\n    val allowMissingStructField: Boolean\n  }\n\n  case class CastByPosition() extends CastingBehavior {\n    val resolveStructsByName: Boolean = false\n    val allowMissingStructField: Boolean = false\n  }\n\n  case class CastByName(allowMissingStructField: Boolean) extends CastingBehavior {\n    val resolveStructsByName: Boolean = true\n  }\n\n  /*\n   * MERGE and UPDATE casting behavior is configurable using internal configs to allow reverting to\n   * legacy behavior. In particular:\n   * - 'resolveMergeUpdateStructsByName.enabled': defaults to resolution by name for struct fields,\n   *   can be disabled to revert to resolution by position.\n   * - 'updateAndMergeCastingFollowsAnsiEnabledFlag': defaults to following\n   *   'spark.sql.storeAssignmentPolicy' for the type of cast to use, can be enabled to revert to\n   *   following 'spark.sql.ansi.enabled'. See `cast()` below.\n   */\n  trait MergeOrUpdateCastingBehavior\n  object MergeOrUpdateCastingBehavior {\n    def apply(schemaEvolutionEnabled: Boolean): CastingBehavior =\n      if (conf.getConf(DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME)) {\n        new CastByName(allowMissingStructField = schemaEvolutionEnabled)\n          with MergeOrUpdateCastingBehavior\n      } else {\n        new CastByPosition() with MergeOrUpdateCastingBehavior\n      }\n  }\n\n  /**\n   * Add a cast to the child expression if it differs from the specified data type. Note that\n   * structs here are cast by name, rather than the Spark SQL default of casting by position.\n   *\n   * @param fromExpression the expression to cast\n   * @param dataType The data type to cast to.\n   * @param castingBehavior Configures the casting behavior to use, see [[CastingBehavior]].\n   * @param columnName The name of the column written to. It is used for the error message.\n   * @param originalTargetExprOpt Optional expression representing the original target column before\n   *                              the update. It is only relevant in MERGE ... UPDATE * with schema\n   *                              evolution to preserve the original values of target-only struct\n   *                              fields. In other cases, it is None and the target-only fields will\n   *                              be overwritten with null.\n   */\n  protected def castIfNeeded(\n      fromExpression: Expression,\n      dataType: DataType,\n      castingBehavior: CastingBehavior,\n      columnName: String,\n      originalTargetExprOpt: Option[Expression] = None): Expression = {\n\n    fromExpression match {\n      // Need to deal with NullType here, as some types cannot be casted from NullType, e.g.,\n      // StructType.\n      case Literal(nul, NullType) => Literal(nul, dataType)\n      case otherExpr =>\n        (fromExpression.dataType, dataType) match {\n          case (ArrayType(fromEt: StructType, fromNullable),\n              to @ ArrayType(toEt: StructType, toNullable))\n              if !(DataTypeUtils.sameType(fromEt, toEt) && fromNullable == toNullable) =>\n            fromExpression match {\n              // If fromExpression is an array function returning an array, cast the\n              // underlying array first and then perform the function on the transformed array.\n              case ArrayUnion(leftExpression, rightExpression) =>\n                val castedLeft =\n                  castIfNeeded(leftExpression, dataType, castingBehavior, columnName)\n                val castedRight =\n                  castIfNeeded(rightExpression, dataType, castingBehavior, columnName)\n                ArrayUnion(castedLeft, castedRight)\n\n              case ArrayIntersect(leftExpression, rightExpression) =>\n                val castedLeft =\n                  castIfNeeded(leftExpression, dataType, castingBehavior, columnName)\n                val castedRight =\n                  castIfNeeded(rightExpression, dataType, castingBehavior, columnName)\n                ArrayIntersect(castedLeft, castedRight)\n\n              case ArrayExcept(leftExpression, rightExpression) =>\n                val castedLeft =\n                  castIfNeeded(leftExpression, dataType, castingBehavior, columnName)\n                val castedRight =\n                  castIfNeeded(rightExpression, dataType, castingBehavior, columnName)\n                ArrayExcept(castedLeft, castedRight)\n\n              case ArrayRemove(leftExpression, rightExpression) =>\n                val castedLeft =\n                  castIfNeeded(leftExpression, dataType, castingBehavior, columnName)\n                // ArrayRemove removes all elements that equal to element from the given array.\n                // In this case, the element to be removed also needs to be casted into the target\n                // array's element type.\n                val castedRight =\n                  castIfNeeded(rightExpression, toEt, castingBehavior, columnName)\n                ArrayRemove(castedLeft, castedRight)\n\n              case ArrayDistinct(expression) =>\n                val castedExpr =\n                  castIfNeeded(expression, dataType, castingBehavior, columnName)\n                ArrayDistinct(castedExpr)\n\n              case _ =>\n                // generate a lambda function to cast each array item into to element struct type.\n                val structConverter: (Expression, Expression) => Expression = (_, i) =>\n                  castIfNeeded(\n                  GetArrayItem(fromExpression, i), toEt, castingBehavior, columnName)\n                val transformLambdaFunc = {\n                  val elementVar = NamedLambdaVariable(\"elementVar\", toEt, toNullable)\n                  val indexVar = NamedLambdaVariable(\"indexVar\", IntegerType, false)\n                  LambdaFunction(structConverter(elementVar, indexVar), Seq(elementVar, indexVar))\n                }\n                // Transforms every element in the array using the lambda function.\n                // Because castIfNeeded is called recursively for array elements, which\n                // generates nullable expression, ArrayTransform will generate an ArrayType with\n                // containsNull as true. Thus, the ArrayType to be casted to need to have\n                // containsNull as true to avoid casting failures.\n                cast(\n                  ArrayTransform(fromExpression, transformLambdaFunc),\n                  to.asNullable,\n                  castingBehavior,\n                  columnName\n                )\n            }\n          case (from: MapType, to: MapType) if !Cast.canCast(from, to) || (\n              // Structs can be nested into the MapType, so if we need to do by-name casts,\n              // we need to recurse into the children here.\n              castingBehavior.resolveStructsByName &&\n                containsNestedStruct(from) &&\n                containsNestedStruct(to) &&\n                !DataTypeUtils.equalsIgnoreCaseAndNullability(from, to)) =>\n            // Manually convert map keys and values if the types are not compatible to allow schema\n            // evolution. This is slower than direct cast so we only do it when required.\n            def createMapConverter(convert: (Expression, Expression) => Expression): Expression = {\n              val keyVar = NamedLambdaVariable(\"keyVar\", from.keyType, nullable = false)\n              val valueVar =\n                NamedLambdaVariable(\"valueVar\", from.valueType, from.valueContainsNull)\n              LambdaFunction(convert(keyVar, valueVar), Seq(keyVar, valueVar))\n            }\n\n            var transformedKeysAndValues = fromExpression\n            if (from.keyType != to.keyType) {\n              transformedKeysAndValues =\n                TransformKeys(transformedKeysAndValues, createMapConverter {\n                  (key, _) => castIfNeeded(key, to.keyType, castingBehavior, columnName)\n                })\n            }\n\n            if (from.valueType != to.valueType) {\n              transformedKeysAndValues =\n                TransformValues(transformedKeysAndValues, createMapConverter {\n                  (_, value) => castIfNeeded(value, to.valueType, castingBehavior, columnName)\n                })\n            }\n            cast(transformedKeysAndValues, to.asNullable, castingBehavior, columnName)\n          case (from: StructType, to: StructType)\n            if !DataTypeUtils.equalsIgnoreCaseAndNullability(from, to) &&\n              castingBehavior.resolveStructsByName =>\n            // All from fields must be present in the final schema, or we'll silently lose data.\n            if (from.exists { f => !to.exists(_.name.equalsIgnoreCase(f.name))}) {\n              throw DeltaErrors.updateSchemaMismatchExpression(from, to)\n            }\n\n            // In addition, if we don't allow missing struct fields, then the number of fields must\n            // necessarily match.\n            if (from.length != to.length && !castingBehavior.allowMissingStructField) {\n              throw DeltaErrors.updateSchemaMismatchExpression(from, to)\n            }\n\n            val originalTargetChildExprsOpt: Option[Map[String, Expression]] =\n              if (UpdateExpressionsSupport.isUpdateStarPreserveNullSourceStructsEnabled(conf)) {\n                extractOriginalTargetChildExprs(originalTargetExprOpt, to)\n              } else {\n                None\n              }\n\n            val nameMappedStruct = CreateNamedStruct(to.flatMap { field =>\n              val fieldNameLit = Literal(field.name)\n              // flatMap returns None if (1) originalTargetChildExprsOpt is None, or\n              // (2) originalTargetChildExprsOpt.get.get(field.name) is None, which happens when\n              // the field doesn't exist in the original target (newly added via schema evolution).\n              val targetFieldExprOpt: Option[Expression] =\n                originalTargetChildExprsOpt.flatMap(_.get(field.name))\n              val extractedField = from\n                .find { f => SchemaUtils.DELTA_COL_RESOLVER(f.name, field.name) }\n                .map { _ =>\n                  ExtractValue(fromExpression, fieldNameLit, SchemaUtils.DELTA_COL_RESOLVER)\n                }.getOrElse {\n                  // This shouldn't be possible - if all columns aren't present when missing struct\n                  // fields aren't allowed, we should have thrown an error earlier.\n                  if (!castingBehavior.allowMissingStructField) {\n                    throw DeltaErrors.extractReferencesFieldNotFound(s\"$field\",\n                      DeltaErrors.updateSchemaMismatchExpression(from, to))\n                  }\n                  // If the expression of the original target column is not provided or there is no\n                  // such field in the target column, fill the field with null.\n                  targetFieldExprOpt.getOrElse(Literal(null))\n                }\n              Seq(fieldNameLit,\n                castIfNeeded(\n                  extractedField,\n                  field.dataType,\n                  castingBehavior,\n                  field.name,\n                  targetFieldExprOpt))\n            })\n\n            // Fix for null expansion caused by struct type cast by preserving NULL source structs.\n            //\n            // Problem: When assigning a struct column, e.g., MERGE ... WHEN MATCHED THEN UPDATE SET\n            // t.col = s.col, if the source struct is NULL, the casting logic will expand the NULL\n            // into a non-null struct with all fields set to NULL:\n            //   NULL -> struct(field1: null, field2: null, ..., newField: null)\n            //\n            // Expected: The target struct should remain NULL when the source struct is NULL:\n            //   NULL -> NULL\n            //\n            // Solution: Wrap the named_struct expression in an IF expression that preserves NULL:\n            //   IF(source_struct IS NULL, NULL, named_struct(...))\n            //\n            // Additional behavior when originalTargetExpr is provided (MERGE ... UPDATE * with\n            // schema evolution):\n            // For target-only fields (fields in target but not in source), we need to preserve\n            // their original values from the target. The expression becomes:\n            //   IF(source_struct IS NULL AND target_struct IS NULL,\n            //      NULL,\n            //      named_struct(\n            //        source_fields...,\n            //        target_only_field1: original_target_value1,\n            //        target_only_field2: original_target_value2\n            //      ))\n            // This is to match the behavior of UPDATE * that target-only fields retain their\n            // values.\n            val wrappedWithNullPreservation =\n              maybeWrapWithNullPreservation(\n                sourceExpr = fromExpression,\n                sourceType = from,\n                targetType = to.asNullable,\n                targetNamedStructExpr = nameMappedStruct,\n                originalTargetExprOpt = originalTargetExprOpt)\n            cast(wrappedWithNullPreservation, to.asNullable, castingBehavior, columnName)\n\n          case (from, to) if from != to =>\n            cast(fromExpression, dataType, castingBehavior, columnName)\n          case _ => fromExpression\n        }\n    }\n  }\n\n  /**\n   * Extracts child expressions from the original target struct for fields that exist in both\n   * the original target and the evolved target schemas.\n   *\n   * This is used during MERGE UPDATE * with schema evolution to preserve target-only field values.\n   *\n   * @param originalTargetExprOpt The original target column expression (before schema evolution)\n   * @param evolvedTargetStruct The evolved target struct type (after schema evolution)\n   * @return A map from evolved field names that exist in the original target schema to extraction\n   *         expressions from the original target, or None if no original target expression is\n   *         provided.\n   */\n  private def extractOriginalTargetChildExprs(\n      originalTargetExprOpt: Option[Expression],\n      evolvedTargetStruct: StructType): Option[Map[String, Expression]] = {\n    originalTargetExprOpt.map { e =>\n      require(e.dataType.isInstanceOf[StructType],\n        s\"originalTargetExprOpt dataType must be StructType but got ${e.dataType}\")\n      val originalTargetStruct = e.dataType.asInstanceOf[StructType]\n      evolvedTargetStruct.flatMap { field =>\n        originalTargetStruct.find(f => SchemaUtils.DELTA_COL_RESOLVER(f.name, field.name))\n          .map { matchedField =>\n            // `field` is present in the target struct before schema evolution.\n            // Use matchedField.name to extract from the original target expression.\n            field.name -> ExtractValue(\n              e, Literal(matchedField.name), SchemaUtils.DELTA_COL_RESOLVER)\n          }\n      }.toMap\n    }\n  }\n\n  /**\n   * Conditionally wraps an expression with an IF expression to preserve NULL source values, when\n   * `DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS` is enabled:\n   *   IF(sourceExpr IS NULL, NULL, targetNamedStructExpr)\n   *\n   * When originalTargetExprOpt is defined (MERGE ... UPDATE * with schema evolution), the\n   * null condition is extended to check whether both the source and target struct are null:\n   *   IF(sourceExpr IS NULL AND targetExpr IS NULL, NULL, targetNamedStructExpr)\n   * This prevents data loss when the source is null but the target has non-null values in\n   * target-only fields.\n   * This is to match the behavior of UPDATE * that target-only fields retain their values.\n   *\n   * @param sourceExpr The expression of the source field\n   * @param sourceType The source struct type\n   * @param targetType The target struct type\n   * @param targetNamedStructExpr The generated target named struct expression\n   * @param originalTargetExprOpt The expression of the original target column. None when the\n   *                              fix is disabled or no original target expression is provided.\n   */\n  private def maybeWrapWithNullPreservation(\n      sourceExpr: Expression,\n      sourceType: StructType,\n      targetType: StructType,\n      targetNamedStructExpr: Expression,\n      originalTargetExprOpt: Option[Expression]): Expression = {\n    if (UpdateExpressionsSupport.isWholeStructAssignmentPreserveNullSourceStructsEnabled(conf)) {\n      val sourceNullCondition = IsNull(sourceExpr)\n      val targetHasExtraFieldsToPreserveValue =\n        UpdateExpressionsSupport.hasExtraStructFieldsToPreserveValue(sourceType, targetType)\n      val fullNullCondition = originalTargetExprOpt match {\n        case Some(originalTargetExpr) if targetHasExtraFieldsToPreserveValue =>\n          // When there are target-only fields to preserve, we need to check whether both the source\n          // and the original target are null.\n          And(sourceNullCondition, IsNull(originalTargetExpr))\n        case Some(_) if !targetHasExtraFieldsToPreserveValue =>\n          // No target-only fields to preserve values for.\n          sourceNullCondition\n        case None =>\n          // No original target expression provided, which means we overwrite the target with the\n          // source.\n          sourceNullCondition\n      }\n      If(\n        fullNullCondition,\n        Literal.create(null, targetType),\n        targetNamedStructExpr\n      )\n    } else {\n      targetNamedStructExpr\n    }\n  }\n\n  /**\n   * Given a target schema and a set of update operations, generate a list of update expressions,\n   * which are aligned with the given schema.\n   *\n   * For update operations to nested struct fields, this method recursively walks down schema tree\n   * and apply the update expressions along the way.\n   * For example, assume table `target` has the following schema:\n   *   s1 struct<a: int, b: int, c: int>, s2 struct<a: int, b: int>, z int\n   *\n   * Given an update command:\n   *\n   *  - UPDATE target SET s1.a = 1, s1.b = 2, z = 3\n   *\n   * this method works as follows:\n   *\n   * generateUpdateExpressions(\n   *   targetSchema=[s1,s2,z], defaultExprs=[s1,s2, z], updateOps=[(s1.a, 1), (s1.b, 2), (z, 3)])\n   *   -> generates expression for s1 - build recursively from child assignments\n   *   generateUpdateExpressions(\n   *     targetSchema=[a,b,c], defaultExprs=[a, b, c], updateOps=[(a, 1),(b, 2)], pathPrefix=[\"s1\"])\n   *     end-of-recursion\n   *   -> returns (1, 2, a.c)\n   *   -> generates expression for s2 - no child assignment and no update expression: use\n   *      default expression `s2`\n   *   -> generates expression for z - use available update expression `3`\n   * -> returns ((1, 2, a.c), s2, 3)\n   *\n   * @param targetSchema schema to follow to generate update expressions. Due to schema evolution,\n   *                     it may contain additional columns or fields not present in the original\n   *                     table schema.\n   * @param updateOps a set of update operations.\n   * @param defaultExprs the expressions to use when no update operation is provided for a column\n   *                      or field. This is typically the output from the base table.\n   * @param pathPrefix the path from root to the current (nested) column. Only used for printing out\n   *                   full column path in error messages.\n   * @param allowSchemaEvolution Whether to allow generating expressions for new columns or fields\n   *                             added by schema evolution.\n   * @param generatedColumns the list of the generated columns in the table. When a column is a\n   *                         generated column and the user doesn't provide a update expression, its\n   *                         update expression in the return result will be None.\n   *                         If `generatedColumns` is empty, any of the options in the return result\n   *                         must be non-empty.\n   * @return a sequence of expression options. The elements in the sequence are options because\n   *         when a column is a generated column but the user doesn't provide an update expression\n   *         for this column, we need to generate the update expression according to the generated\n   *         column definition. But this method doesn't have enough context to do that. Hence, we\n   *         return a `None` for this case so that the caller knows it should generate the update\n   *         expression for such column. For other cases, we will always return Some(expr).\n   */\n  protected def generateUpdateExpressions(\n      targetSchema: StructType,\n      updateOps: Seq[UpdateOperation],\n      defaultExprs: Seq[NamedExpression],\n      resolver: Resolver,\n      pathPrefix: Seq[String] = Nil,\n      allowSchemaEvolution: Boolean = false,\n      generatedColumns: Seq[StructField] = Nil): Seq[Option[Expression]] = {\n    // Check that the head of nameParts in each update operation can match a target col. This avoids\n    // silently ignoring invalid column names specified in update operations.\n    updateOps.foreach { u =>\n      if (!targetSchema.exists(f => resolver(f.name, u.targetColNameParts.head))) {\n        throw DeltaErrors.updateSetColumnNotFoundException(\n          (pathPrefix :+ u.targetColNameParts.head).mkString(\".\"),\n          targetSchema.map(f => (pathPrefix :+ f.name).mkString(\".\")))\n      }\n    }\n\n    // Transform each targetCol to a possibly updated expression\n    targetSchema.map { targetCol =>\n      // The prefix of a update path matches the current targetCol path.\n      val prefixMatchedOps =\n        updateOps.filter(u => resolver(u.targetColNameParts.head, targetCol.name))\n      val defaultExpr = defaultExprs.find(f => resolver(f.name, targetCol.name))\n      // No prefix matches this target column, return its original expression.\n      if (prefixMatchedOps.isEmpty) {\n        // Check whether it's a generated column or not. If so, we will return `None` so that the\n        // caller will generate an expression for this column. We cannot generate an expression at\n        // this moment because a generated column may use other columns which we don't know their\n        // update expressions yet.\n        if (generatedColumns.find(f => resolver(f.name, targetCol.name)).nonEmpty) {\n          None\n        } else if (defaultExpr.nonEmpty) {\n          if (conf.getConf(DeltaSQLConf.DELTA_MERGE_SCHEMA_EVOLUTION_FIX_NESTED_STRUCT_ALIGNMENT)) {\n            Some(castIfNeeded(\n              defaultExpr.get,\n              targetCol.dataType,\n              castingBehavior = MergeOrUpdateCastingBehavior(allowSchemaEvolution),\n              targetCol.name))\n          } else {\n            defaultExpr\n          }\n        } else {\n          // This is a new column or field added by schema evolution that doesn't have an assignment\n          // in this MERGE clause. Set it to null.\n\n          // Log an assertion for now (and fail in test) if schema evolution is disabled. We should\n          // turn this into an error in the future.\n          deltaAssert(allowSchemaEvolution,\n            name = \"generateUpdateExpressions.allowSchemaEvolution\",\n            msg = \"Generating an expression for a new column or field but schema evolution is \" +\n              \"disabled.\"\n          )\n          Some(Literal(null))\n        }\n      } else {\n        // The update operation whose path exactly matches the current targetCol path.\n        val fullyMatchedOp = prefixMatchedOps.find(_.targetColNameParts.size == 1)\n        if (fullyMatchedOp.isDefined) {\n          // If a full match is found, then it should be the ONLY prefix match. Any other match\n          // would be a conflict, whether it is a full match or prefix-only. For example,\n          // when users are updating a nested column a.b, they can't simultaneously update a\n          // descendant of a.b, such as a.b.c.\n          if (prefixMatchedOps.size > 1) {\n            throw DeltaErrors.updateSetConflictException(\n              prefixMatchedOps.map(op => (pathPrefix ++ op.targetColNameParts).mkString(\".\")))\n          }\n          val preserveTargetOnlyFields =\n            UpdateExpressionsSupport.isUpdateStarPreserveNullSourceStructsEnabled(conf) &&\n            allowSchemaEvolution &&\n            fullyMatchedOp.get.targetOnlyStructFieldBehavior ==\n              TargetOnlyStructFieldBehavior.PRESERVE\n          val originalTargetExprOpt =\n            if (preserveTargetOnlyFields) {\n              // Expression corresponding to the original target column before the update.\n              defaultExpr\n            } else {\n              None\n            }\n          // For an exact match, return the updateExpr from the update operation.\n          Some(castIfNeeded(\n            fullyMatchedOp.get.updateExpr,\n            targetCol.dataType,\n            castingBehavior = MergeOrUpdateCastingBehavior(allowSchemaEvolution),\n            targetCol.name,\n            originalTargetExprOpt))\n        } else {\n          // So there are prefix-matched update operations, but none of them is a full match. Then\n          // that means targetCol is a complex data type, so we recursively pass along the update\n          // operations to its children.\n          targetCol.dataType match {\n            case childSchema: StructType =>\n              val defaultChildExprs = defaultExpr match {\n                case Some(expr @ NamedExpression(_, StructType(fields))) =>\n                  fields.zipWithIndex.map { case (field, ordinal) =>\n                    Alias(GetStructField(expr, ordinal, Some(field.name)), field.name)()\n                  }\n                case _ => Array.empty[NamedExpression]\n              }\n              // Recursively apply update operations to the children\n              val childTargetExprs = generateUpdateExpressions(\n                childSchema,\n                prefixMatchedOps.map(u => u.copy(targetColNameParts = u.targetColNameParts.tail)),\n                defaultChildExprs,\n                resolver,\n                pathPrefix :+ targetCol.name,\n                allowSchemaEvolution,\n                // Set `generatedColumns` to Nil because they are only valid in the top level.\n                generatedColumns = Nil)\n                .map(_.getOrElse {\n                  // Should not happen\n                  throw DeltaErrors.cannotGenerateUpdateExpressions()\n                })\n              // Reconstruct the expression for targetCol using its possibly updated children\n              val namedStructExprs = childSchema\n                .zip(childTargetExprs)\n                .flatMap { case (field, expr) => Seq(Literal(field.name), expr) }\n              Some(CreateNamedStruct(namedStructExprs))\n\n            case otherType =>\n              throw DeltaErrors.updateNonStructTypeFieldNotSupportedException(\n                (pathPrefix :+ targetCol.name).mkString(\".\"), otherType)\n          }\n        }\n      }\n    }\n  }\n\n  /** See docs on overloaded method. */\n  protected def generateUpdateExpressions(\n      targetSchema: StructType,\n      defaultExprs: Seq[NamedExpression],\n      nameParts: Seq[Seq[String]],\n      updateExprs: Seq[Expression],\n      resolver: Resolver,\n      generatedColumns: Seq[StructField]): Seq[Option[Expression]] = {\n    assert(nameParts.size == updateExprs.size)\n    val updateOps = nameParts.zip(updateExprs).map {\n      case (nameParts, expr) =>\n        UpdateOperation(\n          targetColNameParts = nameParts,\n          updateExpr = expr,\n          // This method is called from regular UPDATE statements, where schema\n          // evolution is not allowed.\n          targetOnlyStructFieldBehavior = TargetOnlyStructFieldBehavior.TARGET_ALIGNED)\n    }\n    generateUpdateExpressions(\n      targetSchema = targetSchema,\n      updateOps = updateOps,\n      defaultExprs = defaultExprs,\n      resolver = resolver,\n      generatedColumns = generatedColumns\n    )\n  }\n\n  /**\n   * Generate update expressions for generated columns that the user doesn't provide a update\n   * expression. For each item in `updateExprs` that's None, we will find its generation expression\n   * from `generatedColumns`. In order to resolve this generation expression, we will create a\n   * fake Project which contains all update expressions and resolve the generation expression with\n   * this project. Source columns of a generation expression will also be replaced with their\n   * corresponding update expressions.\n   *\n   * For example, given a table that has a generated column `g` defined as `c1 + 10`. For the\n   * following update command:\n   *\n   * UPDATE target SET c1 = c2 + 100, c2 = 1000\n   *\n   * We will generate the update expression `(c2 + 100) + 10`` for column `g`. Note: in this update\n   * expression, we should use the old `c2` attribute rather than its new value 1000.\n   *\n   * @param updateTarget The logical plan of the table to be updated.\n   * @param generatedColumns A list of generated columns.\n   * @param updateExprs  The aligned (with `postEvolutionTargetSchema` if not None, or\n   *                     `updateTarget.output` otherwise) update actions.\n   * @param postEvolutionTargetSchema In case of UPDATE in MERGE when schema evolution happened,\n   *                                  this is the final schema of the target table. This might not\n   *                                  be the same as the output of `updateTarget`.\n   * @return a sequence of update expressions for all of columns in the table.\n   */\n  protected def generateUpdateExprsForGeneratedColumns(\n      updateTarget: LogicalPlan,\n      generatedColumns: Seq[StructField],\n      updateExprs: Seq[Option[Expression]],\n      postEvolutionTargetSchema: Option[StructType] = None): Seq[Expression] = {\n    val targetSchema = postEvolutionTargetSchema.getOrElse(updateTarget.schema)\n    assert(\n      targetSchema.size == updateExprs.length,\n      s\"'generateUpdateExpressions' should return expressions that are aligned with the column \" +\n        s\"list. Expected size: ${targetSchema.size}, actual size: ${updateExprs.length}\")\n    val schemaWithExprs = targetSchema.zip(updateExprs)\n    val exprsForProject = schemaWithExprs.flatMap {\n      case (field, Some(expr)) =>\n        // Create a named expression so that we can use it in Project\n        val exprForProject = Alias(expr, field.name)()\n        Some(exprForProject.exprId -> exprForProject)\n      case (_, None) => None\n    }.toMap\n    // Create a fake Project to resolve the generation expressions\n    val fakePlan = Project(exprsForProject.values.toArray[NamedExpression], updateTarget)\n    schemaWithExprs.map {\n      case (_, Some(expr)) => expr\n      case (targetCol, None) =>\n        // `targetCol` is a generated column and the user doesn't provide a update expression.\n        val resolvedExpr =\n          generatedColumns.find(f => conf.resolver(f.name, targetCol.name)) match {\n            case Some(field) =>\n              val expr = GeneratedColumn.getGenerationExpression(field).get\n              resolveReferencesForExpressions(SparkSession.active, expr :: Nil, fakePlan).head\n            case None =>\n              // Should not happen\n              throw DeltaErrors.nonGeneratedColumnMissingUpdateExpression(targetCol.name)\n          }\n        // As `resolvedExpr` will refer to attributes in `fakePlan`, we need to manually replace\n        // these attributes with their update expressions.\n        resolvedExpr.transform {\n          case a: AttributeReference if exprsForProject.contains(a.exprId) =>\n            exprsForProject(a.exprId).child\n        }\n    }\n  }\n\n  /**\n   * Replaces 'CastSupport.cast'. Selects a cast based on 'spark.sql.storeAssignmentPolicy'.\n   * Legacy behavior for UPDATE and MERGE followed 'spark.sql.ansi.enabled' instead, this legacy\n   * behavior can be re-enabled by setting\n   * 'spark.databricks.delta.updateAndMergeCastingFollowsAnsiEnabledFlag' to true.\n   */\n  private def cast(\n      child: Expression,\n      dataType: DataType,\n      castingBehavior: CastingBehavior,\n      columnName: String): Expression = {\n    if (castingBehavior.isInstanceOf[MergeOrUpdateCastingBehavior] &&\n      conf.getConf(DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG)) {\n      return Cast(child, dataType, Option(conf.sessionLocalTimeZone))\n    }\n\n    conf.storeAssignmentPolicy match {\n      case SQLConf.StoreAssignmentPolicy.LEGACY =>\n        Cast(child, dataType, Some(conf.sessionLocalTimeZone), ansiEnabled = false)\n      case SQLConf.StoreAssignmentPolicy.ANSI =>\n        val cast = Cast(child, dataType, Some(conf.sessionLocalTimeZone), ansiEnabled = true)\n        if (canCauseCastOverflow(cast)) {\n          castingBehavior match {\n            case _: MergeOrUpdateCastingBehavior =>\n              CheckOverflowInTableWrite(cast, columnName)\n            case _ =>\n              cast.setTagValue(Cast.BY_TABLE_INSERTION, ())\n              CheckOverflowInTableInsert(cast, columnName)\n          }\n        } else {\n          cast\n        }\n      case SQLConf.StoreAssignmentPolicy.STRICT =>\n        UpCast(child, dataType)\n    }\n  }\n  private def containsNestedStruct(dt: DataType): Boolean = dt match {\n    case _: StructType => true\n    case _: AtomicType => false\n    case a: ArrayType => containsNestedStruct(a.elementType)\n    case m: MapType => containsNestedStruct(m.keyType) || containsNestedStruct(m.valueType)\n    // Let's defensively pretend it might have a nested struct if we don't recognise something.\n    case _ => true\n  }\n\n  private def containsIntegralOrDecimalType(dt: DataType): Boolean = dt match {\n    case _: IntegralType | _: DecimalType => true\n    case a: ArrayType => containsIntegralOrDecimalType(a.elementType)\n    case m: MapType =>\n      containsIntegralOrDecimalType(m.keyType) || containsIntegralOrDecimalType(m.valueType)\n    case s: StructType =>\n      s.fields.exists(sf => containsIntegralOrDecimalType(sf.dataType))\n    case _ => false\n  }\n\n  private def canCauseCastOverflow(cast: Cast): Boolean = {\n    containsIntegralOrDecimalType(cast.dataType) &&\n      !Cast.canUpCast(cast.child.dataType, cast.dataType)\n  }\n}\n\ncase class CheckOverflowInTableWrite(child: Expression, columnName: String)\n  extends UnaryExpression {\n  override protected def withNewChildInternal(newChild: Expression): Expression = {\n    copy(child = newChild)\n  }\n\n  private def getCast: Option[Cast] = child match {\n    case c: Cast => Some(c)\n    case ExpressionProxy(c: Cast, _, _) => Some(c)\n    case _ => None\n  }\n\n  override def eval(input: InternalRow): Any = try {\n    child.eval(input)\n  } catch {\n    case e: ArithmeticException =>\n      getCast match {\n        case Some(cast) =>\n          throw DeltaErrors.castingCauseOverflowErrorInTableWrite(\n            cast.child.dataType,\n            cast.dataType,\n            columnName)\n        case None => throw e\n      }\n  }\n\n  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {\n    getCast match {\n      case Some(child) => doGenCodeWithBetterErrorMsg(ctx, ev, child)\n      case None => child.genCode(ctx)\n    }\n  }\n\n  def doGenCodeWithBetterErrorMsg(ctx: CodegenContext, ev: ExprCode, child: Cast): ExprCode = {\n    val childGen = child.genCode(ctx)\n    val exceptionClass = classOf[ArithmeticException].getCanonicalName\n    assert(child.isInstanceOf[Cast])\n    val cast = child.asInstanceOf[Cast]\n    val fromDt =\n      ctx.addReferenceObj(\"from\", cast.child.dataType, cast.child.dataType.getClass.getName)\n    val toDt = ctx.addReferenceObj(\"to\", child.dataType, child.dataType.getClass.getName)\n    val col = ctx.addReferenceObj(\"colName\", columnName, \"java.lang.String\")\n    // scalastyle:off line.size.limit\n    ev.copy(code =\n      code\"\"\"\n      boolean ${ev.isNull} = true;\n      ${CodeGenerator.javaType(dataType)} ${ev.value} = ${CodeGenerator.defaultValue(dataType)};\n      try {\n        ${childGen.code}\n        ${ev.isNull} = ${childGen.isNull};\n        ${ev.value} = ${childGen.value};\n      } catch ($exceptionClass e) {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .castingCauseOverflowErrorInTableWrite($fromDt, $toDt, $col);\n      }\"\"\"\n    )\n    // scalastyle:on line.size.limit\n  }\n\n  override def dataType: DataType = child.dataType\n\n  override def sql: String = child.sql\n\n  override def toString: String = child.toString\n}\n\nobject UpdateExpressionsSupport {\n\n  /**\n   * Returns true if preserving null source structs for whole-struct assignments is enabled.\n   * This fix addresses the issue where a null source struct is incorrectly expanded into\n   * a non-null struct with all fields set to null in whole-struct assignments, e.g.\n   * UPDATE SET col = s.col.\n   */\n  def isWholeStructAssignmentPreserveNullSourceStructsEnabled(conf: SQLConf): Boolean = {\n    conf.getConf(DeltaSQLConf.DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS)\n  }\n\n  /**\n   * Returns true if preserving null source structs for UPDATE * is enabled.\n   * This fix addresses the issue where a null source struct is incorrectly expanded into\n   * a non-null struct with all fields set to null in UPDATE * operations.\n   */\n  def isUpdateStarPreserveNullSourceStructsEnabled(conf: SQLConf): Boolean = {\n    isWholeStructAssignmentPreserveNullSourceStructsEnabled(conf) &&\n      conf.getConf(DeltaSQLConf.DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS_UPDATE_STAR)\n  }\n\n  /**\n   * Returns true if `targetType` contains extra struct fields compared to `sourceType` that we\n   * want to preserve values for.\n   *\n   * We recursively check target-only fields of structs nested within structs, but we do not\n   * check structs nested within arrays or maps because we don't preserve original target values\n   * for arrays or maps.\n   *\n   * Field name comparison is case-insensitive, aligning with\n   * `DataTypeUtils.equalsIgnoreCaseAndNullability`, which is used in\n   * `UpdateExpressionSupport.castIfNeeded` to decide whether struct type cast is needed.\n   *\n   * @param sourceStruct the source struct to compare against\n   * @param targetStruct the target struct to check for extra fields to preserve values for\n   * @return true if `targetStruct` has more struct fields than `sourceStruct` at any nesting level\n   *         that we want to preserve values for.\n   */\n  private def hasExtraStructFieldsToPreserveValue(\n      sourceStruct: StructType,\n      targetStruct: StructType): Boolean = {\n    // Fast check: if target has more fields, it definitely has extra fields than source.\n    if (targetStruct.length > sourceStruct.length) {\n      return true\n    }\n\n    // Partition target fields into target-only and common fields.\n    val (commonFields, targetOnlyFields) = targetStruct.partition { targetField =>\n      sourceStruct.exists(_.name.equalsIgnoreCase(targetField.name))\n    }\n\n    // If there are any target-only fields, we have extra fields to preserve.\n    if (targetOnlyFields.nonEmpty) {\n      return true\n    }\n\n    // No extra fields at this level - recursively check common fields that are `StructType`.\n    commonFields.exists { targetField =>\n      sourceStruct.find(_.name.equalsIgnoreCase(targetField.name)).exists { sourceField =>\n        (sourceField.dataType, targetField.dataType) match {\n          case (sourceStruct: StructType, targetStruct: StructType) =>\n            hasExtraStructFieldsToPreserveValue(sourceStruct, targetStruct)\n          case _ =>\n            // Don't recurse into arrays or maps, as we don't preserve values for arrays or maps.\n            false\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/actions/DeletionVectorDescriptor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\nimport java.io.{ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream}\nimport java.net.URI\nimport java.util.{Base64, UUID}\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.DeltaUDF\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Codec, DeltaEncoder, JsonUtils}\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.{Column, Encoder}\nimport org.apache.spark.sql.functions.{concat, lit, when}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/** Information about a deletion vector attached to a file action. */\ncase class DeletionVectorDescriptor(\n    /**\n     * Indicates how the DV is stored.\n     * Should be a single letter (see [[pathOrInlineDv]] below.)\n     */\n    storageType: String,\n\n    /**\n     * Contains the actual data that allows accessing the DV.\n     *\n     * Three options are currently supported:\n     * - `storageType=\"u\"` format: `<random prefix - optional><base85 encoded uuid>`\n     *                            The deletion vector is stored in a file with a path relative to\n     *                            the data directory of this Delta Table, and the file name can be\n     *                            reconstructed from the UUID.\n     *                            The encoded UUID is always exactly 20 characters, so the random\n     *                            prefix length can be determined any characters exceeding 20.\n     * - `storageType=\"i\"` format: `<base85 encoded bytes>`\n     *                            The deletion vector is stored inline in the log.\n     * - `storageType=\"p\"` format: `<absolute path>`\n     *                             The DV is stored in a file with an absolute path given by this\n     *                             url. Special characters in this path must be escaped.\n     */\n    pathOrInlineDv: String,\n    /**\n     * Start of the data for this DV in number of bytes from the beginning of the file it is stored\n     * in.\n     *\n     * Always None when storageType = \"i\".\n     */\n    @JsonDeserialize(contentAs = classOf[java.lang.Integer])\n    offset: Option[Int] = None,\n    /** Size of the serialized DV in bytes (raw data size, i.e. before base85 encoding). */\n    sizeInBytes: Int,\n    /** Number of rows the DV logically removes from the file. */\n    cardinality: Long,\n    /**\n     * Transient property that is used to validate DV correctness.\n     * It is not stored in the log.\n     */\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    maxRowIndex: Option[Long] = None) {\n\n  import DeletionVectorDescriptor._\n\n  @JsonIgnore\n  @transient\n  lazy val uniqueId: String = {\n    offset match {\n      case Some(offset) => s\"$uniqueFileId@$offset\"\n      case None => uniqueFileId\n    }\n  }\n\n  @JsonIgnore\n  @transient\n  lazy val uniqueFileId: String = s\"$storageType$pathOrInlineDv\"\n\n  @JsonIgnore\n  protected[delta] def isOnDisk: Boolean = !isInline\n\n  @JsonIgnore\n  protected[delta] def isInline: Boolean = storageType == INLINE_DV_MARKER\n\n  @JsonIgnore\n  protected[delta] def isRelative: Boolean = storageType == UUID_DV_MARKER\n\n  @JsonIgnore\n  protected[delta] def isAbsolute: Boolean = storageType == PATH_DV_MARKER\n\n  @JsonIgnore\n  protected[delta] def isEmpty: Boolean = cardinality == 0\n\n  def absolutePath(tableLocation: Path): Path = {\n    require(isOnDisk, \"Can't get a path for an inline deletion vector\")\n    storageType match {\n      case UUID_DV_MARKER =>\n        val (randomPrefix, uuid) = getRandomPrefixAndUuid.get\n        assembleDeletionVectorPath(tableLocation, uuid, randomPrefix)\n      case PATH_DV_MARKER =>\n        // Since there is no need for legacy support for relative paths for DVs,\n        // relative DVs should *always* use the UUID variant.\n        val parsedUri = new URI(pathOrInlineDv)\n        assert(parsedUri.isAbsolute, \"Relative URIs are not supported for DVs\")\n        new Path(parsedUri)\n      case _ => throw DeltaErrors.cannotReconstructPathFromURI(pathOrInlineDv)\n    }\n  }\n\n  /** Returns the url encoded absolute path of the deletion vector. */\n  def urlEncodedPath(tablePath: Path): String =\n    SparkPath.fromPath(absolutePath(tablePath)).urlEncoded\n\n  /**\n   * Returns the url encoded relative path of the deletion vector if possible.\n   * If the DV path is outside the table directory, returns None.\n   */\n  def urlEncodedRelativePathIfExists(tablePath: Path): Option[String] = {\n    if (isRelative) {\n      return Some(SparkPath.fromPath(absolutePath(new Path(\".\"))).urlEncoded)\n    }\n\n    // DV path is not relative. Attempt to relativize it.\n    val basePathUri = tablePath.toUri\n    val absolutePathUri = absolutePath(tablePath).toUri\n    val relativePath = basePathUri.relativize(absolutePathUri)\n    if (!relativePath.isAbsolute) {\n      Some(SparkPath.fromUri(relativePath).urlEncoded)\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Parse the prefix and UUID of a relative DV. Returns None if the DV is not relative.\n   */\n  @JsonIgnore\n  def getRandomPrefixAndUuid: Option[(String, UUID)] = storageType match {\n    case UUID_DV_MARKER =>\n      // If the file was written with a random prefix, we have to extract that,\n      // before decoding the UUID.\n      val randomPrefixLength = pathOrInlineDv.length - Codec.Base85Codec.ENCODED_UUID_LENGTH\n      val (randomPrefix, encodedUuid) = pathOrInlineDv.splitAt(randomPrefixLength)\n      Some((randomPrefix, Codec.Base85Codec.decodeUUID(encodedUuid)))\n    case _ =>\n      None\n  }\n  /**\n   * Produce a copy of this DV, but using an absolute path.\n   *\n   * If the DV already has an absolute path or is inline, then this is just a normal copy.\n   */\n  def copyWithAbsolutePath(tableLocation: Path): DeletionVectorDescriptor = {\n    storageType match {\n      case UUID_DV_MARKER =>\n        this.copy(\n          storageType = PATH_DV_MARKER,\n          pathOrInlineDv = urlEncodedPath(tableLocation))\n      case PATH_DV_MARKER | INLINE_DV_MARKER => this.copy()\n    }\n  }\n\n  /**\n   * Produce a copy of this DV, with `pathOrInlineDv` replaced by a relative path based on `id`\n   * and `randomPrefix`.\n   *\n   * If the DV already has a relative path or is inline, then this is just a normal copy.\n   */\n  def copyWithNewRelativePath(id: UUID, randomPrefix: String): DeletionVectorDescriptor = {\n    storageType match {\n      case PATH_DV_MARKER =>\n        this.copy(storageType = UUID_DV_MARKER, pathOrInlineDv = encodeUUID(id, randomPrefix))\n      case UUID_DV_MARKER | INLINE_DV_MARKER => this.copy()\n    }\n  }\n\n  @JsonIgnore\n  def inlineData: Array[Byte] = {\n    require(isInline, \"Can't get data for an on-disk DV from the log.\")\n    // The sizeInBytes is used to remove any padding that might have been added during encoding.\n    Codec.Base85Codec.decodeBytes(pathOrInlineDv, sizeInBytes)\n  }\n\n  /** Returns the estimated number of bytes required to serialize this object. */\n  @JsonIgnore\n  protected[delta] lazy val estimatedSerializedSize: Int = {\n    // (cardinality(8) + sizeInBytes(4)) + storageType + pathOrInlineDv + option[offset(4)]\n    12 + storageType.length + pathOrInlineDv.length + (if (offset.isDefined) 4 else 0)\n  }\n\n  /*\n   * Serialize the DV descriptor to a base64 encoded string.\n   */\n  def serializeToBase64(): String = {\n    val bs = new ByteArrayOutputStream()\n    val ds = new DataOutputStream(bs)\n    try {\n      ds.writeLong(cardinality)\n      ds.writeInt(sizeInBytes)\n\n      val storageTypeBytes = storageType.getBytes()\n      assert(storageTypeBytes.length == 1, s\"Storage type must be 1byte value: $storageType\")\n      ds.writeByte(storageTypeBytes.head)\n\n      if (storageType != INLINE_DV_MARKER) {\n        assert(offset.isDefined)\n        ds.writeInt(offset.get)\n      } else {\n        assert(offset.isEmpty)\n      }\n\n      ds.writeUTF(pathOrInlineDv)\n      Base64.getEncoder.encodeToString(bs.toByteArray)\n    } finally {\n      ds.close()\n    }\n  }\n}\n\nobject DeletionVectorDescriptor {\n\n  /** Prefix that is used in all file names generated by deletion vector store. */\n  val DELETION_VECTOR_FILE_NAME_PREFIX = SQLConf.get.getConf(DeltaSQLConf.TEST_DV_NAME_PREFIX)\n\n  /** String that is used in all file names generated by deletion vector store */\n  val DELETION_VECTOR_FILE_NAME_CORE = DELETION_VECTOR_FILE_NAME_PREFIX + \"deletion_vector\"\n\n\n  // Markers to separate different kinds of DV storage.\n  final val PATH_DV_MARKER: String = \"p\"\n  final val INLINE_DV_MARKER: String = \"i\"\n  final val UUID_DV_MARKER: String = \"u\"\n\n  private final val deletionVectorFileNameRegex =\n    raw\"${new Path(DELETION_VECTOR_FILE_NAME_CORE).toUri}_([^.]+)\\.bin\".r\n  private final val deletionVectorFileNamePattern = deletionVectorFileNameRegex.pattern\n\n  final lazy val STRUCT_TYPE: StructType =\n    Action.addFileSchema(\"deletionVector\").dataType.asInstanceOf[StructType]\n\n  private lazy val _encoder = new DeltaEncoder[DeletionVectorDescriptor]\n  implicit def encoder: Encoder[DeletionVectorDescriptor] = _encoder.get\n\n  /** Utility method to create an on-disk [[DeletionVectorDescriptor]] */\n  def onDiskWithRelativePath(\n      id: UUID,\n      randomPrefix: String = \"\",\n      sizeInBytes: Int,\n      cardinality: Long,\n      offset: Option[Int] = None,\n      maxRowIndex: Option[Long] = None): DeletionVectorDescriptor =\n    DeletionVectorDescriptor(\n      storageType = UUID_DV_MARKER,\n      pathOrInlineDv = encodeUUID(id, randomPrefix),\n      offset = offset,\n      sizeInBytes = sizeInBytes,\n      cardinality = cardinality,\n      maxRowIndex = maxRowIndex)\n\n  /** Utility method to create an on-disk [[DeletionVectorDescriptor]] */\n  def onDiskWithAbsolutePath(\n      path: String,\n      sizeInBytes: Int,\n      cardinality: Long,\n      offset: Option[Int] = None,\n      maxRowIndex: Option[Long] = None): DeletionVectorDescriptor =\n    DeletionVectorDescriptor(\n      storageType = PATH_DV_MARKER,\n      pathOrInlineDv = path,\n      offset = offset,\n      sizeInBytes = sizeInBytes,\n      cardinality = cardinality,\n      maxRowIndex = maxRowIndex)\n\n  /** Utility method to create an inline [[DeletionVectorDescriptor]] */\n  def inlineInLog(\n      data: Array[Byte],\n      cardinality: Long): DeletionVectorDescriptor =\n    DeletionVectorDescriptor(\n      storageType = INLINE_DV_MARKER,\n      pathOrInlineDv = encodeData(data),\n      sizeInBytes = data.length,\n      cardinality = cardinality)\n\n  /**\n   * Returns whether the path points to a deletion vector file.\n   * Note, external writers are no enforced to create DV files with the same naming convertions.\n   * This function is intended for testing. */\n  private[delta] def isDeletionVectorPath(path: Path): Boolean =\n    deletionVectorFileNamePattern.matcher(path.getName).matches()\n\n  /** Only for testing. */\n  private[delta] def isDeletionVectorPath(path: String): Boolean =\n    isDeletionVectorPath(new Path(path))\n\n  /** Same as above but as a column expression. Only for testing. */\n  private[delta] def isDeletionVectorPath(pathCol: Column): Column =\n    DeltaUDF.booleanFromString(isDeletionVectorPath)(pathCol)\n\n  /** Returns a boolean column that corresponds to whether each deletion vector is inline. */\n  def isInline(dv: Column): Column =\n    DeltaUDF.booleanFromDeletionVectorDescriptor(_.isInline)(dv)\n\n  /**\n   * Returns a column with the url encoded deletion vector paths.\n   *\n   * WARNING: It throws an exception if it encounters any inline DVs. The caller is responsible\n   * for handling these separately.\n   */\n  def urlEncodedPath(deletionVectorCol: Column, tablePath: Path): Column =\n    DeltaUDF.stringFromDeletionVectorDescriptor(_.urlEncodedPath(tablePath))(deletionVectorCol)\n\n  /**\n   * Returns a column with the url encoded deletion vector relative paths. For paths that cannot\n   * be relativized, it returns None.\n   *\n   * WARNING: It throws an exception if it encounters any inline DVs. The caller is responsible\n   * for handling these separately.\n   */\n  def urlEncodedRelativePathIfExists(deletionVectorCol: Column, tablePath: Path): Column =\n    DeltaUDF.stringOptionFromDeletionVectorDescriptor(\n      _.urlEncodedRelativePathIfExists(tablePath))(deletionVectorCol)\n\n  /**\n   * This produces the same output as [[DeletionVectorDescriptor.uniqueId]] but as a column\n   * expression, so it can be used directly in a Spark query.\n   */\n  def uniqueIdExpression(deletionVectorCol: Column): Column = {\n    when(deletionVectorCol(\"offset\").isNotNull,\n        concat(\n          deletionVectorCol(\"storageType\"),\n          deletionVectorCol(\"pathOrInlineDv\"),\n          lit('@'),\n          deletionVectorCol(\"offset\")))\n      .otherwise(concat(\n        deletionVectorCol(\"storageType\"),\n        deletionVectorCol(\"pathOrInlineDv\")))\n  }\n\n  /**\n   * Return the unique path under `parentPath` that is based on `id`.\n   *\n   * Optionally, prepend a `prefix` to the name.\n   */\n  def assembleDeletionVectorPath(targetParentPath: Path, id: UUID, prefix: String = \"\"): Path = {\n    val fileName = assembleDeletionVectorFileName(id)\n    if (prefix.nonEmpty) {\n      new Path(new Path(targetParentPath, prefix), fileName)\n    } else {\n      new Path(targetParentPath, fileName)\n    }\n  }\n\n  /**\n   * Return the unique file name for a deletion vector based on `id`.\n   */\n  def assembleDeletionVectorFileName(id: UUID): String =\n    s\"${DELETION_VECTOR_FILE_NAME_CORE}_${id}.bin\"\n\n  /** Descriptor for an empty stored bitmap. */\n  val EMPTY: DeletionVectorDescriptor = DeletionVectorDescriptor(\n    storageType = INLINE_DV_MARKER,\n    pathOrInlineDv = \"\",\n    sizeInBytes = 0,\n    cardinality = 0)\n\n  private[delta] def encodeUUID(id: UUID, randomPrefix: String): String = {\n    val uuidData = Codec.Base85Codec.encodeUUID(id)\n    // This should always be true and we are relying on it for separating out the\n    // prefix again later without having to spend an extra character as a separator.\n    assert(uuidData.length == 20)\n    s\"$randomPrefix$uuidData\"\n  }\n\n  def encodeData(bytes: Array[Byte]): String = Codec.Base85Codec.encodeBytes(bytes)\n\n  /*\n   * Deserialize the base64 encoded string to a DV descriptor.\n   *\n   * The format must be in sync with [[DeletionVectorDescriptor.serializeToBase64]].\n   */\n  def deserializeFromBase64(encoded: String): DeletionVectorDescriptor = {\n    val buffer = Base64.getDecoder.decode(encoded)\n    val ds = new DataInputStream(new ByteArrayInputStream(buffer))\n    try {\n      val cardinality = ds.readLong()\n      val sizeInBytes = ds.readInt()\n      val storageType = ds.readByte().toChar.toString\n      val offset = if (storageType != INLINE_DV_MARKER) Some(ds.readInt()) else None\n      val pathOrInlineDv = ds.readUTF()\n      DeletionVectorDescriptor(storageType, pathOrInlineDv, offset, sizeInBytes, cardinality)\n    } finally {\n      ds.close()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/actions/InMemoryLogReplay.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\nimport java.net.URI\n\n\n/**\n * Replays a history of actions, resolving them to produce the current state\n * of the table. The protocol for resolution is as follows:\n *  - The most recent [[AddFile]] and accompanying metadata for any `(path, dv id)` tuple wins.\n *  - [[RemoveFile]] deletes a corresponding [[AddFile]] and is retained as a\n *    tombstone until `minFileRetentionTimestamp` has passed. If `minFileRetentionTimestamp` is\n *    None, all [[RemoveFile]] actions are retained.\n *    A [[RemoveFile]] \"corresponds\" to the [[AddFile]] that matches both the parquet file URI\n *    *and* the deletion vector's URI (if any).\n *  - The most recent version for any `appId` in a [[SetTransaction]] wins.\n *  - The most recent [[Metadata]] wins.\n *  - The most recent [[Protocol]] version wins.\n *  - For each `(path, dv id)` tuple, this class should always output only one [[FileAction]]\n *    (either [[AddFile]] or [[RemoveFile]])\n *\n * This class is not thread safe.\n */\nclass InMemoryLogReplay(\n    minFileRetentionTimestamp: Option[Long],\n    minSetTransactionRetentionTimestamp: Option[Long]) extends LogReplay {\n\n  import InMemoryLogReplay._\n\n  private var currentProtocolVersion: Protocol = null\n  private var currentVersion: Long = -1\n  private var currentMetaData: Metadata = null\n  private val transactions = new scala.collection.mutable.HashMap[String, SetTransaction]()\n  private val domainMetadatas = collection.mutable.Map.empty[String, DomainMetadata]\n  private val activeFiles = new scala.collection.mutable.HashMap[UniqueFileActionTuple, AddFile]()\n  // RemoveFiles that had cancelled AddFile during replay\n  private val cancelledRemoveFiles =\n    new scala.collection.mutable.HashMap[UniqueFileActionTuple, RemoveFile]()\n  // RemoveFiles that had NOT cancelled any AddFile during replay\n  private val activeRemoveFiles =\n    new scala.collection.mutable.HashMap[UniqueFileActionTuple, RemoveFile]()\n\n  override def append(version: Long, actions: Iterator[Action]): Unit = {\n    assert(currentVersion == -1 || version == currentVersion + 1,\n      s\"Attempted to replay version $version, but state is at $currentVersion\")\n    currentVersion = version\n    actions.foreach {\n      case a: SetTransaction =>\n        transactions(a.appId) = a\n      case a: DomainMetadata if a.removed =>\n        domainMetadatas.remove(a.domain)\n      case a: DomainMetadata if !a.removed =>\n        domainMetadatas(a.domain) = a\n      case _: CheckpointOnlyAction => // Ignore this while doing LogReplay\n      case a: Metadata =>\n        currentMetaData = a\n      case a: Protocol =>\n        currentProtocolVersion = a\n      case add: AddFile =>\n        val uniquePath = UniqueFileActionTuple(add.pathAsUri, add.getDeletionVectorUniqueId)\n        activeFiles(uniquePath) = add.copy(dataChange = false)\n        // Remove the tombstone to make sure we only output one `FileAction`.\n        cancelledRemoveFiles.remove(uniquePath)\n        // Remove from activeRemoveFiles to handle commits that add a previously-removed file\n        activeRemoveFiles.remove(uniquePath)\n      case remove: RemoveFile =>\n        val uniquePath = UniqueFileActionTuple(remove.pathAsUri, remove.getDeletionVectorUniqueId)\n        activeFiles.remove(uniquePath) match {\n          case Some(_) => cancelledRemoveFiles(uniquePath) = remove\n          case None => activeRemoveFiles(uniquePath) = remove\n        }\n      case _: CommitInfo => // do nothing\n      case _: AddCDCFile => // do nothing\n      case null => // Some crazy future feature. Ignore\n    }\n  }\n\n  private def getTombstones: Iterable[FileAction] = {\n    val allRemovedFiles = cancelledRemoveFiles.values ++ activeRemoveFiles.values\n    val filteredRemovedFiles = minFileRetentionTimestamp match {\n      case None => allRemovedFiles\n      case Some(timestamp) => allRemovedFiles.filter(_.delTimestamp > timestamp)\n    }\n    filteredRemovedFiles.map(_.copy(dataChange = false))\n  }\n\n  private[delta] def getTransactions: Iterable[SetTransaction] = {\n    minSetTransactionRetentionTimestamp match {\n      case None => transactions.values\n      case Some(timestamp) =>\n        transactions.values.filter { txn => txn.lastUpdated.exists(_ > timestamp) }\n    }\n  }\n\n  private[delta] def getDomainMetadatas: Iterable[DomainMetadata] = domainMetadatas.values\n\n  /** Returns the current state of the Table as an iterator of actions. */\n  override def checkpoint: Iterator[Action] = {\n    val fileActions = (activeFiles.values ++ getTombstones).toSeq.sortBy(_.path)\n\n    Option(currentProtocolVersion).toIterator ++\n    Option(currentMetaData).toIterator ++\n    getDomainMetadatas ++\n    getTransactions ++\n    fileActions.toIterator\n  }\n\n  /** Returns all [[AddFile]] actions after the Log Replay */\n  private[delta] def allFiles: Seq[AddFile] = activeFiles.values.toSeq\n}\n\nobject InMemoryLogReplay{\n  /** The unit of path uniqueness in delta log actions is the tuple `(parquet file, dv)`. */\n  final case class UniqueFileActionTuple(fileURI: URI, deletionVectorURI: Option[String])\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/actions/LogReplay.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\n/**\n * Replays a history of actions, resolving them to produce the current state\n * of the table.\n */\ntrait LogReplay {\n  /** Append these `actions` to the state. Must only be called in ascending order of `version`. */\n  def append(version: Long, actions: Iterator[Action]): Unit\n\n  /** Returns the current state of the Table as an iterator of actions. */\n  def checkpoint: Iterator[Action]\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/actions/TableFeatureSupport.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\nimport java.util.Locale\n\nimport scala.collection.mutable\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport com.fasterxml.jackson.annotation.JsonIgnore\n\n/**\n * Trait to be mixed into the [[Protocol]] case class to enable Table Features.\n *\n * Protocol reader version 3 and writer version 7 start to support reader and writer table\n * features. Reader version 3 supports only reader-writer features in an <b>explicit</b> way,\n * by adding its name to `readerFeatures`. Similarly, writer version 7 supports only writer-only\n * or reader-writer features in an <b>explicit</b> way, by adding its name to `writerFeatures`.\n * When reading or writing a table, clients MUST respect all supported features.\n *\n * See also the document of [[TableFeature]] for feature-specific terminologies.\n */\ntrait TableFeatureSupport { this: Protocol =>\n\n  /** Check if this protocol is capable of adding features into its `readerFeatures` field. */\n  def supportsReaderFeatures: Boolean =\n    TableFeatureProtocolUtils.supportsReaderFeatures(minReaderVersion)\n\n  /** Check if this protocol is capable of adding features into its `writerFeatures` field. */\n  def supportsWriterFeatures: Boolean =\n    TableFeatureProtocolUtils.supportsWriterFeatures(minWriterVersion)\n\n  /**\n   * Get a new Protocol object that has `feature` supported. Writer-only features will be added to\n   * `writerFeatures` field, and reader-writer features will be added to `readerFeatures` and\n   * `writerFeatures` fields.\n   *\n   * If `feature` is already implicitly supported in the current protocol's legacy reader or\n   * writer protocol version, the new protocol will not modify the original protocol version,\n   * i.e., the feature will not be explicitly added to the protocol's `readerFeatures` or\n   * `writerFeatures`. This is to avoid unnecessary protocol upgrade for feature that it already\n   * supports.\n   */\n  def withFeature(feature: TableFeature): Protocol = {\n    def shouldAddRead: Boolean = {\n      if (supportsReaderFeatures) return true\n      if (feature.minReaderVersion <= minReaderVersion) return false\n\n      throw DeltaErrors.tableFeatureRequiresHigherReaderProtocolVersion(\n        feature.name,\n        minReaderVersion,\n        feature.minReaderVersion)\n    }\n\n    def shouldAddWrite: Boolean = {\n      if (supportsWriterFeatures) return true\n      if (feature.minWriterVersion <= minWriterVersion) return false\n\n      throw DeltaErrors.tableFeatureRequiresHigherWriterProtocolVersion(\n        feature.name,\n        minWriterVersion,\n        feature.minWriterVersion)\n    }\n\n    var shouldAddToReaderFeatures = feature.isReaderWriterFeature\n    var shouldAddToWriterFeatures = true\n    if (feature.isLegacyFeature) {\n      if (feature.isReaderWriterFeature) {\n        shouldAddToReaderFeatures = shouldAddRead\n      }\n      shouldAddToWriterFeatures = shouldAddWrite\n    }\n\n    val protocolWithDependencies = withFeatures(feature.requiredFeatures)\n    protocolWithDependencies.withFeature(\n      feature.name,\n      addToReaderFeatures = shouldAddToReaderFeatures,\n      addToWriterFeatures = shouldAddToWriterFeatures)\n  }\n\n  /**\n   * Get a new Protocol object with multiple features supported.\n   *\n   * See the documentation of [[withFeature]] for more information.\n   */\n  def withFeatures(features: Iterable[TableFeature]): Protocol = {\n    features.foldLeft(this)(_.withFeature(_))\n  }\n\n  /**\n   * Get a new Protocol object with an additional feature descriptor. If `addToReaderFeatures` is\n   * set to `true`, the descriptor will be added to the protocol's `readerFeatures` field. If\n   * `addToWriterFeatures` is set to `true`, the descriptor will be added to the protocol's\n   * `writerFeatures` field.\n   *\n   * The method does not require the feature to be recognized by the client, therefore will not\n   * try keeping the protocol's `readerFeatures` and `writerFeatures` in sync. Use with caution.\n   */\n  private[actions] def withFeature(\n      name: String,\n      addToReaderFeatures: Boolean,\n      addToWriterFeatures: Boolean): Protocol = {\n    if (addToReaderFeatures && !supportsReaderFeatures) {\n      throw DeltaErrors.tableFeatureRequiresHigherReaderProtocolVersion(\n        name,\n        currentVersion = minReaderVersion,\n        requiredVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION)\n    }\n    if (addToWriterFeatures && !supportsWriterFeatures) {\n      throw DeltaErrors.tableFeatureRequiresHigherWriterProtocolVersion(\n        name,\n        currentVersion = minWriterVersion,\n        requiredVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n    }\n\n    val addedReaderFeatureOpt = if (addToReaderFeatures) Some(name) else None\n    val addedWriterFeatureOpt = if (addToWriterFeatures) Some(name) else None\n\n    copy(\n      readerFeatures = this.readerFeatures.map(_ ++ addedReaderFeatureOpt),\n      writerFeatures = this.writerFeatures.map(_ ++ addedWriterFeatureOpt))\n  }\n\n  /**\n   * Get a new Protocol object with additional feature descriptors added to the protocol's\n   * `readerFeatures` field.\n   *\n   * The method does not require the features to be recognized by the client, therefore will not\n   * try keeping the protocol's `readerFeatures` and `writerFeatures` in sync. Use with caution.\n   */\n  private[delta] def withReaderFeatures(names: Iterable[String]): Protocol = {\n    names.foldLeft(this)(\n      _.withFeature(_, addToReaderFeatures = true, addToWriterFeatures = false))\n  }\n\n  /**\n   * Get a new Protocol object with additional feature descriptors added to the protocol's\n   * `writerFeatures` field.\n   *\n   * The method does not require the features to be recognized by the client, therefore will not\n   * try keeping the protocol's `readerFeatures` and `writerFeatures` in sync. Use with caution.\n   */\n  private[delta] def withWriterFeatures(names: Iterable[String]): Protocol = {\n    names.foldLeft(this)(\n      _.withFeature(_, addToReaderFeatures = false, addToWriterFeatures = true))\n  }\n\n  /**\n   * Get all feature names in this protocol's `readerFeatures` field. Returns an empty set when\n   * this protocol does not support reader features.\n   */\n  def readerFeatureNames: Set[String] = this.readerFeatures.getOrElse(Set())\n\n  /**\n   * Get a set of all feature names in this protocol's `writerFeatures` field. Returns an empty\n   * set when this protocol does not support writer features.\n   */\n  def writerFeatureNames: Set[String] = this.writerFeatures.getOrElse(Set())\n\n  /**\n   * Get a set of all feature names in this protocol's `readerFeatures` and `writerFeatures`\n   * field. Returns an empty set when this protocol supports none of reader and writer features.\n   */\n  @JsonIgnore\n  lazy val readerAndWriterFeatureNames: Set[String] = readerFeatureNames ++ writerFeatureNames\n\n  /**\n   * Same as above but returns a sequence of [[TableFeature]] instead of a set of feature names.\n   */\n  @JsonIgnore\n  lazy val readerAndWriterFeatures: Seq[TableFeature] =\n    readerAndWriterFeatureNames.toSeq.flatMap(TableFeature.featureNameToFeature)\n\n  /**\n   * A sequence of native [[TableFeature]]s. This is derived by filtering out all explicitly\n   * supported legacy features.\n   */\n  @JsonIgnore\n  lazy val nativeReaderAndWriterFeatures: Seq[TableFeature] =\n    readerAndWriterFeatures.filterNot(_.isLegacyFeature)\n\n  /**\n   * Get all features that are implicitly supported by this protocol, for example, `Protocol(1,2)`\n   * implicitly supports `appendOnly` and `invariants`. When this protocol is capable of requiring\n   * writer features, no feature can be implicitly supported.\n   */\n  @JsonIgnore\n  lazy val implicitlySupportedFeatures: Set[TableFeature] = {\n    if (supportsReaderFeatures && supportsWriterFeatures) {\n      // this protocol uses both reader and writer features, no feature can be implicitly supported\n      Set()\n    } else {\n      TableFeature.allSupportedFeaturesMap.values\n        .filter(_.isLegacyFeature)\n        .filterNot(supportsReaderFeatures || this.minReaderVersion < _.minReaderVersion)\n        .filterNot(supportsWriterFeatures || this.minWriterVersion < _.minWriterVersion)\n        .toSet\n    }\n  }\n\n  /**\n   * Get all features that are supported by this protocol, implicitly and explicitly. When the\n   * protocol supports table features, this method returns the same set of features as\n   * [[readerAndWriterFeatureNames]]; when the protocol does not support table features, this\n   * method becomes equivalent to [[implicitlySupportedFeatures]].\n   */\n  @JsonIgnore\n  lazy val implicitlyAndExplicitlySupportedFeatures: Set[TableFeature] = {\n    readerAndWriterFeatureNames.flatMap(TableFeature.featureNameToFeature) ++\n      implicitlySupportedFeatures\n  }\n\n  /**\n   * Determine whether this protocol can be safely upgraded to a new protocol `to`. This means:\n   *   - all features supported by this protocol are supported by `to`.\n   *\n   * Examples regarding feature status:\n   *   - from `[appendOnly]` to `[appendOnly]` => allowed.\n   *   - from `[appendOnly, changeDataFeed]` to `[appendOnly]` => not allowed.\n   *   - from `[appendOnly]` to `[appendOnly, changeDataFeed]` => allowed.\n   */\n  def canUpgradeTo(to: Protocol): Boolean =\n    // All features supported by `this` are supported by `to`.\n    implicitlyAndExplicitlySupportedFeatures.subsetOf(to.implicitlyAndExplicitlySupportedFeatures)\n\n  /**\n   * Determine whether this protocol can be safely downgraded to a new protocol `to`.\n   * All the implicit and explicit features between the two protocols need to match,\n   * excluding the dropped feature. We also need to take into account that in some cases\n   * the downgrade process may add the CheckpointProtectionTableFeature.\n   *\n   * Note, the conditions above also account for cases where we downgrade from table features\n   * to legacy protocol versions.\n   */\n  def canDowngradeTo(to: Protocol, droppedFeatureName: String): Boolean = {\n    val thisFeatures = this.implicitlyAndExplicitlySupportedFeatures\n    val toFeatures = to.implicitlyAndExplicitlySupportedFeatures\n    val allowedNewFeatures: Set[TableFeature] = Set(CheckpointProtectionTableFeature)\n    val droppedFeature = Seq(droppedFeatureName).flatMap(TableFeature.featureNameToFeature)\n    val newFeatures = toFeatures -- thisFeatures\n    newFeatures.subsetOf(allowedNewFeatures) &&\n      (thisFeatures -- droppedFeature == toFeatures -- newFeatures)\n  }\n\n  /**\n   * True if this protocol can be upgraded or downgraded to the 'to' protocol.\n   */\n  def canTransitionTo(to: Protocol, op: Operation): Boolean = {\n    op match {\n      case drop: DeltaOperations.DropTableFeature => canDowngradeTo(to, drop.featureName)\n      case _ => canUpgradeTo(to)\n    }\n  }\n\n  /**\n   * Merge this protocol with multiple `protocols` to have the highest reader and writer versions\n   * plus all explicitly and implicitly supported features.\n   */\n  def merge(others: Protocol*): Protocol = {\n    val protocols = this +: others\n    val mergedReaderVersion = protocols.map(_.minReaderVersion).max\n    val mergedWriterVersion = protocols.map(_.minWriterVersion).max\n    val mergedReaderFeatures = protocols.flatMap(_.readerFeatureNames)\n    val mergedWriterFeatures = protocols.flatMap(_.writerFeatureNames)\n    val mergedImplicitFeatures = protocols.flatMap(_.implicitlySupportedFeatures)\n\n    val mergedProtocol = Protocol(mergedReaderVersion, mergedWriterVersion)\n      .withReaderFeatures(mergedReaderFeatures)\n      .withWriterFeatures(mergedWriterFeatures)\n      .withFeatures(mergedImplicitFeatures)\n\n    // The merged protocol is always normalized in order to represent the protocol\n    // with the weakest possible form. This enables backward compatibility.\n    // This is preceded by a denormalization step. This allows to fix invalid legacy Protocols.\n    // For example, (2, 3) is normalized to (1, 3). This is because there is no legacy feature\n    // in the set with reader version 2 unless the writer version is at least 5.\n    mergedProtocol.denormalizedNormalized\n  }\n\n  /**\n   * Remove writer feature from protocol. To remove a writer feature we only need to\n   * remove it from the writerFeatures set.\n   */\n  private[delta] def removeWriterFeature(targetWriterFeature: TableFeature): Protocol = {\n    require(targetWriterFeature.isRemovable)\n    require(!targetWriterFeature.isReaderWriterFeature)\n    copy(writerFeatures = writerFeatures.map(_ - targetWriterFeature.name))\n  }\n\n  /**\n   * Remove reader+writer feature from protocol. To remove a reader+writer feature we need to\n   * remove it from the readerFeatures set and the writerFeatures set.\n   */\n  private[delta] def removeReaderWriterFeature(\n      targetReaderWriterFeature: TableFeature): Protocol = {\n    require(targetReaderWriterFeature.isRemovable)\n    require(targetReaderWriterFeature.isReaderWriterFeature)\n    val newReaderFeatures = readerFeatures.map(_ - targetReaderWriterFeature.name)\n    val newWriterFeatures = writerFeatures.map(_ - targetReaderWriterFeature.name)\n    copy(readerFeatures = newReaderFeatures, writerFeatures = newWriterFeatures)\n  }\n\n  /**\n   * Remove feature wrapper for removing either Reader/Writer or Writer features. We assume\n   * the feature exists in the protocol. There is a relevant validation at\n   * [[AlterTableDropFeatureDeltaCommand]]. We also require targetFeature is removable.\n   *\n   * After removing the feature we normalize the protocol.\n   */\n  def removeFeature(targetFeature: TableFeature): Protocol = {\n    require(targetFeature.isRemovable)\n    val currentProtocol = this.denormalized\n    val newProtocol = targetFeature match {\n      case f@(_: ReaderWriterFeature | _: LegacyReaderWriterFeature) =>\n        currentProtocol.removeReaderWriterFeature(f)\n      case f@(_: WriterFeature | _: LegacyWriterFeature) =>\n        currentProtocol.removeWriterFeature(f)\n      case f =>\n        throw DeltaErrors.dropTableFeatureNonRemovableFeature(f.name)\n    }\n    newProtocol.normalized\n  }\n\n\n  /**\n   * Protocol normalization is the process of converting a table features protocol to the weakest\n   * possible form. This primarily refers to converting a table features protocol to a legacy\n   * protocol. A Table Features protocol can be represented with the legacy representation only\n   * when the features set of the former exactly matches a legacy protocol.\n   *\n   * Normalization can also decrease the reader version of a table features protocol when it is\n   * higher than necessary.\n   *\n   * For example:\n   * (1, 7, AppendOnly, Invariants, CheckConstraints) -> (1, 3)\n   * (3, 7, RowTracking) -> (1, 7, RowTracking)\n   */\n  def normalized: Protocol = {\n    // Normalization can only be applied to table feature protocols.\n    if (!supportsWriterFeatures) return this\n\n    val (minReaderVersion, minWriterVersion) =\n      TableFeatureProtocolUtils.minimumRequiredVersions(readerAndWriterFeatures)\n    val newProtocol = Protocol(minReaderVersion, minWriterVersion)\n\n    if (this.implicitlyAndExplicitlySupportedFeatures ==\n      newProtocol.implicitlyAndExplicitlySupportedFeatures) {\n      newProtocol\n    } else {\n      Protocol(minReaderVersion, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(readerAndWriterFeatures)\n    }\n  }\n\n  /**\n   * Protocol denormalization is the process of converting a legacy protocol to the\n   * the equivalent table features protocol. This is the inverse of protocol normalization.\n   * It can be used to allow operations on legacy protocols that yield result which\n   * cannot be represented anymore by a legacy protocol.\n   */\n  def denormalized: Protocol = {\n    // Denormalization can only be applied to legacy protocols.\n    if (supportsWriterFeatures) return this\n\n    val (minReaderVersion, _) =\n      TableFeatureProtocolUtils.minimumRequiredVersions(implicitlySupportedFeatures.toSeq)\n\n    Protocol(minReaderVersion, TABLE_FEATURES_MIN_WRITER_VERSION)\n      .withFeatures(implicitlySupportedFeatures)\n  }\n\n  /**\n   * Helper method that applies both denormalization and normalization. This can be used to\n   * normalize invalid legacy protocols such as (2, 3), (1, 5). A legacy protocol is invalid\n   * when the version numbers are higher than required to support the implied feature set.\n   */\n  def denormalizedNormalized: Protocol = denormalized.normalized\n\n  /**\n   * Check if a `feature` is supported by this protocol. This means either (a) the protocol does\n   * not support table features and implicitly supports the feature, or (b) the protocol supports\n   * table features and references the feature.\n   */\n  def isFeatureSupported(feature: TableFeature): Boolean = {\n    // legacy feature + legacy protocol\n    (feature.isLegacyFeature && this.implicitlySupportedFeatures.contains(feature)) ||\n      // new protocol\n      readerAndWriterFeatureNames.contains(feature.name)\n  }\n\n  /** Returns whether this client supports writing in a table with this protocol. */\n  def supportedForWrite(): Boolean = {\n    val supportedWriterVersions = Action.supportedWriterVersionNumbers\n    val supportedWriterFeatures = Action.supportedProtocolVersion().writerFeatureNames\n    val testUnsupportedFeatures: Set[String] = TableFeature.testUnsupportedFeatures\n      .filterNot(_.isReaderWriterFeature)\n      .map(_.name)\n\n    supportedWriterVersions.contains(this.minWriterVersion) &&\n      this.writerFeatureNames.subsetOf(supportedWriterFeatures -- testUnsupportedFeatures)\n  }\n\n  /** Returns whether this client supports reading a table with this protocol. */\n  def supportedForRead(): Boolean = {\n    val supportedReaderVersions = Action.supportedReaderVersionNumbers\n    val supportedReaderFeatures = Action.supportedProtocolVersion().readerFeatureNames\n    val testUnsupportedFeatures: Set[String] = TableFeature.testUnsupportedFeatures\n      .filter(_.isReaderWriterFeature).map(_.name)\n\n    supportedReaderVersions.contains(this.minReaderVersion) &&\n      this.readerFeatureNames.subsetOf(supportedReaderFeatures -- testUnsupportedFeatures)\n  }\n}\n\nobject TableFeatureProtocolUtils {\n\n  /** Prop prefix in table properties. */\n  val FEATURE_PROP_PREFIX = \"delta.feature.\"\n\n  /** Prop prefix in Spark sessions configs. */\n  val DEFAULT_FEATURE_PROP_PREFIX = \"spark.databricks.delta.properties.defaults.feature.\"\n\n  /**\n   * The string constant \"enabled\" for uses in table properties.\n   * @deprecated\n   *   This value is deprecated to avoid confusion with features that are actually enabled by\n   *   table metadata. Use [[FEATURE_PROP_SUPPORTED]] instead.\n   */\n  val FEATURE_PROP_ENABLED = \"enabled\"\n\n  /** The string constant \"supported\" for uses in table properties. */\n  val FEATURE_PROP_SUPPORTED = \"supported\"\n\n  /** Min reader version that supports reader features. */\n  val TABLE_FEATURES_MIN_READER_VERSION = 3\n\n  /** Min reader version that supports writer features. */\n  val TABLE_FEATURES_MIN_WRITER_VERSION = 7\n\n  /** Get the table property config key for the `feature`. */\n  def propertyKey(feature: TableFeature): String = propertyKey(feature.name)\n\n  /** Get the table property config key for the `featureName`. */\n  def propertyKey(featureName: String): String =\n    s\"$FEATURE_PROP_PREFIX$featureName\"\n\n  /** Get the session default config key for the `feature`. */\n  def defaultPropertyKey(feature: TableFeature): String = defaultPropertyKey(feature.name)\n\n  /** Get the session default config key for the `featureName`. */\n  def defaultPropertyKey(featureName: String): String =\n    s\"$DEFAULT_FEATURE_PROP_PREFIX$featureName\"\n\n  /**\n   * Determine whether a [[Protocol]] with the given reader protocol version is capable of adding\n   * features into its `readerFeatures` field.\n   */\n  def supportsReaderFeatures(readerVersion: Int): Boolean = {\n    readerVersion >= TABLE_FEATURES_MIN_READER_VERSION\n  }\n\n  /**\n   * Determine whether a [[Protocol]] with the given writer protocol version is capable of adding\n   * features into its `writerFeatures` field.\n   */\n  def supportsWriterFeatures(writerVersion: Int): Boolean = {\n    writerVersion >= TABLE_FEATURES_MIN_WRITER_VERSION\n  }\n\n  /**\n   * Get a set of [[TableFeature]]s representing supported features set in a table properties map.\n   */\n  def getSupportedFeaturesFromTableConfigs(configs: Map[String, String]): Set[TableFeature] = {\n    val featureConfigs = configs.filterKeys(_.startsWith(FEATURE_PROP_PREFIX))\n    val unsupportedFeatureConfigs = mutable.Set.empty[String]\n    val collectedFeatures = featureConfigs.flatMap { case (key, value) =>\n      // Feature name is lower cased in table properties but not in Spark session configs.\n      // Feature status is not lower cased in any case.\n      val name = key.stripPrefix(FEATURE_PROP_PREFIX).toLowerCase(Locale.ROOT)\n      val status = value.toLowerCase(Locale.ROOT)\n      if (status != FEATURE_PROP_SUPPORTED && status != FEATURE_PROP_ENABLED) {\n        throw DeltaErrors.unsupportedTableFeatureStatusException(name, status)\n      }\n      val featureOpt = TableFeature.featureNameToFeature(name)\n      if (!featureOpt.isDefined) {\n        unsupportedFeatureConfigs += key\n      }\n      featureOpt\n    }.toSet\n    if (unsupportedFeatureConfigs.nonEmpty) {\n      throw DeltaErrors.unsupportedTableFeatureConfigsException(unsupportedFeatureConfigs)\n    }\n    collectedFeatures\n  }\n\n  /**\n   * Checks if the the given table property key is a Table Protocol property, i.e.,\n   * `delta.minReaderVersion`, `delta.minWriterVersion`, ``delta.ignoreProtocolDefaults``, or\n   * anything that starts with `delta.feature.`\n   */\n  def isTableProtocolProperty(key: String): Boolean = {\n    key == Protocol.MIN_READER_VERSION_PROP ||\n    key == Protocol.MIN_WRITER_VERSION_PROP ||\n    key == DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key ||\n    key.startsWith(TableFeatureProtocolUtils.FEATURE_PROP_PREFIX)\n  }\n\n  /**\n   * Returns the minimum reader/writer versions required to support all provided features.\n   */\n  def minimumRequiredVersions(features: Seq[TableFeature]): (Int, Int) =\n    ((features.map(_.minReaderVersion) :+ 1).max, (features.map(_.minWriterVersion) :+ 1).max)\n}\n\nobject DropTableFeatureUtils extends DeltaLogging {\n  private val MAX_CHECKPOINT_RETRIES = 3\n\n  // The number of barrier checkpoints to create before the version requiring checkpoint protection.\n  val NUMBER_OF_BARRIER_CHECKPOINTS = 3\n\n  /**\n   * Helper function for creating checkpoints. If checkpoint creation fails we retry up\n   * to [[MAX_CHECKPOINT_RETRIES]] times.\n   *\n   * @param snapshotRefreshStartTimeTs The timestamp to use as a starting point for refreshing\n   *                                   the snapshot. This value is used to improve the performance\n   *                                   of the snapshot refresh operation.\n   */\n  def createCheckpointWithRetries(\n      table: DeltaTableV2,\n      snapshotRefreshStartTimeTs: Long): Boolean = {\n    val log = table.deltaLog\n    val snapshot = table.update(checkIfUpdatedSinceTs = Some(snapshotRefreshStartTimeTs))\n\n    def checkpointAndVerify(snapshot: Snapshot): Boolean = {\n      try {\n        table.checkpoint(snapshot)\n        log.checkpointExistsAtVersion(snapshot.version)\n      } catch {\n        case NonFatal(e) =>\n          recordDeltaEvent(\n            deltaLog = log,\n            opType = \"dropFeature.checkpointAndVerify.error\",\n            data = Map(\n              \"message\" -> e.getMessage,\n              \"stackTrace\" -> e.getStackTrace().mkString(\"\\n\")))\n          false\n      }\n    }\n\n    (1 to MAX_CHECKPOINT_RETRIES).collectFirst {\n      case _ if checkpointAndVerify(snapshot) => true\n    }.getOrElse(false)\n  }\n\n  def createEmptyCommitAndCheckpoint(\n      table: DeltaTableV2,\n      snapshotRefreshStartTs: Long,\n      retryOnFailure: Boolean = false): Boolean = {\n    val log = table.deltaLog\n    val snapshot = table.update(checkIfUpdatedSinceTs = Some(snapshotRefreshStartTs))\n    val emptyCommitTS = table.deltaLog.clock.nanoTime()\n    log.startTransaction(table.catalogTable, Some(snapshot))\n      .commit(Nil, DeltaOperations.EmptyCommit)\n\n    // retryOnFailure is temporary to avoid affecting the behavior of the legacy Drop Feature\n    // command behavior.\n    if (retryOnFailure) {\n      createCheckpointWithRetries(table, emptyCommitTS)\n    } else {\n      table.checkpoint(table.update(checkIfUpdatedSinceTs = Some(emptyCommitTS)))\n      true\n    }\n  }\n\n  def truncateHistoryLogRetentionMillis(metadata: Metadata): Long = {\n    val truncateHistoryLogRetention = DeltaConfigs\n      .TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION\n      .fromMetaData(metadata)\n\n    DeltaConfigs.getMilliSeconds(truncateHistoryLogRetention)\n  }\n\n  /**\n   * Returns new metadata without `tablePropertiesToRemoveAtDowngradeCommit` table properties.\n   */\n  def getDowngradedProtocolMetadata(\n      feature: RemovableFeature,\n      metadata: Metadata): Metadata = {\n    val propKeys = feature.tablePropertiesToRemoveAtDowngradeCommit\n    val normalizedKeys = DeltaConfigs.normalizeConfigKeys(propKeys)\n    val newConfiguration = metadata.configuration.filterNot {\n      case (key, _) => normalizedKeys.contains(key)\n    }\n    metadata.copy(configuration = newConfiguration)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/actions/actions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nimport scala.annotation.tailrec\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.util.control.NonFatal\n\nimport com.databricks.spark.util.TagDefinition\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, JsonUtils, Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.delta.util.PartitionUtils\nimport com.fasterxml.jackson.annotation._\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.core.{JsonGenerator, JsonParser}\nimport com.fasterxml.jackson.databind._\nimport com.fasterxml.jackson.databind.annotation.{JsonDeserialize, JsonSerialize}\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport io.delta.storage.commit.actions.{\n  AbstractCommitInfo => StorageAbstractCommitInfo,\n  AbstractMetadata => StorageAbstractMetadata,\n  AbstractProtocol => StorageAbstractProtocol\n}\nimport org.apache.spark.sql.delta.v2.interop.{\n  AbstractCommitInfo => SparkAbstractCommitInfo,\n  AbstractMetadata => SparkAbstractMetadata,\n  AbstractProtocol => SparkAbstractProtocol\n}\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.{Column, Encoder, SparkSession}\nimport org.apache.spark.sql.catalyst.ScalaReflection\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.types.{DataType, StructField, StructType}\nimport org.apache.spark.util.Utils\n\nobject Action extends DeltaLogging {\n  /**\n   * The maximum version of the protocol that this version of Delta understands by default.\n   *\n   * Use [[supportedProtocolVersion()]] instead, except to define new feature-gated versions.\n   */\n  private[actions] val readerVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION\n  private[actions] val writerVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION\n  private[actions] val protocolVersion: Protocol = Protocol(readerVersion, writerVersion)\n\n  // We can't extend the [[Action]] class itself since it affects serialization.\n  // Instead, we add a wrapper here as a helper method for logging.\n  override def recordDeltaEvent(\n      deltaLog: DeltaLog,\n      opType: String,\n      tags: Map[TagDefinition, String] = Map.empty,\n      data: AnyRef = null,\n      path: Option[Path] = None): Unit = {\n    super.recordDeltaEvent(deltaLog, opType, tags, data, path)\n  }\n\n  /**\n   * The maximum protocol version we are currently allowed to use, with or without all recognized\n   * features. Optionally, some features can be excluded using `featuresToExclude`.\n   */\n  private[delta] def supportedProtocolVersion(\n      withAllFeatures: Boolean = true,\n      featuresToExclude: Seq[TableFeature] = Seq.empty): Protocol = {\n    if (withAllFeatures) {\n      val featuresToAdd = TableFeature.allSupportedFeaturesMap.values.toSet -- featuresToExclude\n      protocolVersion.withFeatures(featuresToAdd)\n    } else {\n      protocolVersion\n    }\n  }\n\n  /** All reader protocol version numbers supported by the system. */\n  private[delta] lazy val supportedReaderVersionNumbers: Set[Int] = {\n    val allVersions =\n      supportedProtocolVersion().implicitlyAndExplicitlySupportedFeatures.map(_.minReaderVersion) +\n      1 // Version 1 does not introduce new feature, it's always supported.\n    if (DeltaUtils.isTesting) {\n      allVersions + 0 // Allow Version 0 in tests\n    } else {\n      allVersions - 0 // Delete 0 produced by writer-only features\n    }\n  }\n\n  /** All writer protocol version numbers supported by the system. */\n  private[delta] lazy val supportedWriterVersionNumbers: Set[Int] = {\n    val allVersions =\n      supportedProtocolVersion().implicitlyAndExplicitlySupportedFeatures.map(_.minWriterVersion) +\n        1 // Version 1 does not introduce new feature, it's always supported.\n    if (DeltaUtils.isTesting) {\n      allVersions + 0 // Allow Version 0 in tests\n    } else {\n      allVersions - 0 // Delete 0 produced by reader-only features - we don't have any - for safety\n    }\n  }\n\n  def fromJson(json: String): Action = {\n    JsonUtils.mapper.readValue[SingleAction](json).unwrap\n  }\n\n  lazy val logSchema = ExpressionEncoder[SingleAction]().schema\n  lazy val addFileSchema = logSchema(\"add\").dataType.asInstanceOf[StructType]\n\n  /**\n   * Same as [[logSchema]], but with a user-specified add.stats_parsed column. This is useful for\n   * reading parquet checkpoint files that provide add.stats_parsed instead of add.stats.\n   */\n  def logSchemaWithAddStatsParsed(statsParsed: StructField): StructType = {\n    val logAddSchema = logSchema(\"add\").dataType.asInstanceOf[StructType]\n    val fields = logSchema.map { f =>\n      if (f.name == \"add\") f.copy(dataType = logAddSchema.add(statsParsed)) else f\n    }\n    StructType(fields)\n  }\n}\n\n/**\n * Represents a single change to the state of a Delta table. An order sequence\n * of actions can be replayed using [[InMemoryLogReplay]] to derive the state\n * of the table at a given point in time.\n */\nsealed trait Action {\n  def wrap: SingleAction\n  def json: String = JsonUtils.toJson(wrap)\n}\n\n/**\n * Used to block older clients from reading or writing the log when backwards incompatible changes\n * are made to the protocol. Readers and writers are responsible for checking that they meet the\n * minimum versions before performing any other operations.\n *\n * This action allows us to explicitly block older clients in the case of a breaking change to the\n * protocol. Absent a protocol change, Clients MUST silently ignore messages and fields that they\n * do not understand.\n *\n * Note: Please initialize this class using the companion object's `apply` method, which will\n * assign correct values (`Set()` vs `None`) to [[readerFeatures]] and [[writerFeatures]].\n */\ncase class Protocol private (\n    minReaderVersion: Int,\n    minWriterVersion: Int,\n    @JsonInclude(Include.NON_ABSENT) // write to JSON only when the field is not `None`\n    readerFeatures: Option[Set[String]],\n    @JsonInclude(Include.NON_ABSENT)\n    writerFeatures: Option[Set[String]])\n  extends Action\n  with SparkAbstractProtocol\n  with StorageAbstractProtocol\n  with TableFeatureSupport {\n  // Correctness check\n  // Reader and writer versions must match the status of reader and writer features\n  require(\n    supportsReaderFeatures == readerFeatures.isDefined,\n    \"Mismatched minReaderVersion and readerFeatures.\")\n  require(\n    supportsWriterFeatures == writerFeatures.isDefined,\n    \"Mismatched minWriterVersion and writerFeatures.\")\n\n  // When reader is on table features, writer must be on table features too\n  if (supportsReaderFeatures && !supportsWriterFeatures) {\n    throw DeltaErrors.tableFeatureReadRequiresWriteException(\n      TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n  }\n\n  override def wrap: SingleAction = SingleAction(protocol = this)\n\n  /**\n   * Return a reader-friendly string representation of this Protocol.\n   *\n   * Returns the protocol versions and referenced features when the protocol does support table\n   * features, such as `3,7,{},{appendOnly}` and `2,7,None,{appendOnly}`. Otherwise returns only\n   * the protocol version such as `2,6`.\n   */\n  @JsonIgnore\n  lazy val simpleString: String = {\n    if (!supportsReaderFeatures && !supportsWriterFeatures) {\n      s\"$minReaderVersion,$minWriterVersion\"\n    } else {\n      val readerFeaturesStr = readerFeatures\n        .map(_.toSeq.sorted.mkString(\"[\", \",\", \"]\"))\n        .getOrElse(\"None\")\n      val writerFeaturesStr = writerFeatures\n        .map(_.toSeq.sorted.mkString(\"[\", \",\", \"]\"))\n        .getOrElse(\"None\")\n      s\"$minReaderVersion,$minWriterVersion,$readerFeaturesStr,$writerFeaturesStr\"\n    }\n  }\n\n  /**\n   * Return a map that contains the protocol versions and supported features of this Protocol.\n   */\n  @JsonIgnore\n  private[delta] lazy val fieldsForLogging: Map[String, Any] = {\n    Map(\n      \"minReaderVersion\" -> minReaderVersion, // Number\n      \"minWriterVersion\" -> minWriterVersion, // Number\n      \"supportedFeatures\" ->\n        implicitlyAndExplicitlySupportedFeatures.map(_.name).toSeq.sorted // Array[String]\n    )\n  }\n\n  override def toString: String = s\"Protocol($simpleString)\"\n\n  override def getMinReaderVersion: Int = minReaderVersion\n\n  override def getMinWriterVersion: Int = minWriterVersion\n\n  override def getReaderFeatures: java.util.Set[String] = readerFeatures.map(_.asJava).orNull\n\n  override def getWriterFeatures: java.util.Set[String] = writerFeatures.map(_.asJava).orNull\n}\n\nobject Protocol {\n  import TableFeatureProtocolUtils._\n\n  val MIN_READER_VERSION_PROP = \"delta.minReaderVersion\"\n  val MIN_WRITER_VERSION_PROP = \"delta.minWriterVersion\"\n\n  /**\n   * Construct a [[Protocol]] case class of the given reader and writer versions. This method will\n   * initialize table features fields when reader and writer versions are capable.\n   */\n  def apply(\n      minReaderVersion: Int = Action.readerVersion,\n      minWriterVersion: Int = Action.writerVersion): Protocol = {\n    new Protocol(\n      minReaderVersion = minReaderVersion,\n      minWriterVersion = minWriterVersion,\n      readerFeatures = if (supportsReaderFeatures(minReaderVersion)) Some(Set()) else None,\n      writerFeatures = if (supportsWriterFeatures(minWriterVersion)) Some(Set()) else None)\n  }\n\n  /** Returns the required protocol for a given feature. Takes into account dependent features. */\n  def forTableFeature(tf: TableFeature): Protocol = {\n    // Every table feature is a writer feature.\n    val writerFeatures = tf.requiredFeatures + tf\n    val readerFeatures = writerFeatures.filter(f => f.isReaderWriterFeature && !f.isLegacyFeature)\n    val writerFeaturesNames = writerFeatures.map(_.name)\n    val readerFeaturesNames = readerFeatures.map(_.name)\n\n    val minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION\n    val minReaderVersion = (readerFeatures.map(_.minReaderVersion) + 1).max\n\n    new Protocol(\n      minReaderVersion,\n      minWriterVersion,\n      readerFeatures = Option(readerFeaturesNames).filter(_.nonEmpty),\n      writerFeatures = Some(writerFeaturesNames))\n  }\n\n  /**\n   * Picks the protocol version for a new table given the Delta table metadata. The result\n   * satisfies all active features in the metadata and protocol-related configs in table\n   * properties, i.e., configs with keys [[MIN_READER_VERSION_PROP]], [[MIN_WRITER_VERSION_PROP]],\n   * and [[FEATURE_PROP_PREFIX]]. This method will also consider protocol-related configs: default\n   * reader version, default writer version, and features enabled by\n   * [[DEFAULT_FEATURE_PROP_PREFIX]].\n   */\n  def forNewTable(spark: SparkSession, metadataOpt: Option[Metadata]): Protocol = {\n    // `minProtocolComponentsFromMetadata` does not consider sessions defaults,\n    // so we must copy sessions defaults to table metadata.\n    val conf = spark.sessionState.conf\n    val ignoreProtocolDefaults = DeltaConfigs.ignoreProtocolDefaultsIsSet(\n      sqlConfs = conf,\n      tableConf = metadataOpt.map(_.configuration).getOrElse(Map.empty))\n    val defaultGlobalConf = if (ignoreProtocolDefaults) {\n      Map(MIN_READER_VERSION_PROP -> 1.toString, MIN_WRITER_VERSION_PROP -> 1.toString)\n    } else {\n      Map(\n        MIN_READER_VERSION_PROP ->\n          conf.getConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION).toString,\n        MIN_WRITER_VERSION_PROP ->\n          conf.getConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION).toString)\n    }\n    val overrideGlobalConf = DeltaConfigs\n      .mergeGlobalConfigs(\n        sqlConfs = spark.sessionState.conf,\n        tableConf = Map.empty,\n        ignoreProtocolConfsOpt = Some(ignoreProtocolDefaults))\n      // We care only about protocol related stuff\n      .filter { case (k, _) => TableFeatureProtocolUtils.isTableProtocolProperty(k) }\n    var metadata = metadataOpt.getOrElse(Metadata())\n    // Priority: user-provided > override of session defaults > session defaults\n    metadata = metadata.copy(configuration =\n      defaultGlobalConf ++ overrideGlobalConf ++ metadata.configuration)\n\n    val (readerVersion, writerVersion, enabledFeatures) =\n      minProtocolComponentsFromMetadata(spark, metadata)\n    // New table protocols should always be denormalized and then normalized to convert the\n    // protocol to the weakest possible form. This means either converting a table features\n    // protocol to a legacy protocol or reducing the versions of a table features protocol.\n    // For example:\n    // 1) (3, 7, RowTracking) is normalized to (1, 7, RowTracking).\n    // 2) (3, 7, AppendOnly, Invariants) is normalized to (1, 2).\n    // 3) (2, 3) is normalized to (1, 3).\n    Protocol(readerVersion, writerVersion)\n      .withFeatures(enabledFeatures)\n      .denormalizedNormalized\n  }\n\n  /**\n   * Returns the smallest set of table features that contains `features` and that also contains\n   * all dependencies of all features in the returned set.\n   */\n  @tailrec\n  private def getDependencyClosure(features: Set[TableFeature]): Set[TableFeature] = {\n    val requiredFeatures = features ++ features.flatMap(_.requiredFeatures)\n    if (features == requiredFeatures) {\n      features\n    } else {\n      getDependencyClosure(requiredFeatures)\n    }\n  }\n\n  /**\n   * Extracts all table features that are enabled by the given metadata and the optional protocol.\n   * This includes all already enabled features (if a protocol is provided), the features enabled\n   * directly by metadata, and all of their (transitive) dependencies.\n   */\n  def extractAutomaticallyEnabledFeatures(\n      spark: SparkSession,\n      metadata: Metadata,\n      protocol: Protocol): Set[TableFeature] = {\n    val protocolEnabledFeatures = protocol\n      .writerFeatureNames\n      .flatMap(TableFeature.featureNameToFeature)\n    val metadataEnabledFeatures = TableFeature\n      .allSupportedFeaturesMap.values\n      .collect {\n        case f: TableFeature with FeatureAutomaticallyEnabledByMetadata\n          if f.metadataRequiresFeatureToBeEnabled(protocol, metadata, spark) => f\n      }\n      .toSet\n\n    getDependencyClosure(protocolEnabledFeatures ++ metadataEnabledFeatures)\n  }\n\n  /**\n   * Given the Delta table metadata, returns the minimum required reader and writer version that\n   * satisfies all enabled features in the metadata and protocol-related configs in table\n   * properties, i.e., configs with keys [[MIN_READER_VERSION_PROP]], [[MIN_WRITER_VERSION_PROP]],\n   * and [[FEATURE_PROP_PREFIX]].\n   *\n   * This function returns the protocol versions and features individually instead of a\n   * [[Protocol]], so the caller can identify the features that caused the protocol version. For\n   * example, if the return values are (2, 5, columnMapping + preceding features), the caller\n   * can safely ignore all other features required by the protocol with a reader and writer\n   * version of 2 and 5.\n   *\n   * Note that this method does not consider features configured in session defaults.\n   * To make them effective, copy them to `metadata` using [[DeltaConfigs.mergeGlobalConfigs]].\n   */\n  def minProtocolComponentsFromMetadata(\n      spark: SparkSession,\n      metadata: Metadata): (Int, Int, Set[TableFeature]) = {\n    val tableConf = metadata.configuration\n    // There might be features enabled by the table properties aka\n    // `CREATE TABLE ... TBLPROPERTIES ...`.\n    val tablePropEnabledFeatures = getSupportedFeaturesFromTableConfigs(tableConf)\n    // To enable features that are being dependent by `tablePropEnabledFeatures`, we pass it here to\n    // let [[getDependencyClosure]] collect them.\n    val metaEnabledFeatures =\n      extractAutomaticallyEnabledFeatures(\n        spark, metadata, Protocol().withFeatures(tablePropEnabledFeatures))\n    val allEnabledFeatures = tablePropEnabledFeatures ++ metaEnabledFeatures\n\n    // Protocol version provided in table properties can upgrade the protocol, but only when they\n    // are higher than which required by the enabled features.\n    val (readerVersionFromTableConfOpt, writerVersionFromTableConfOpt) =\n      getProtocolVersionsFromTableConf(tableConf)\n\n    // If the user explicitly sets the table versions, we need to take into account the\n    // relevant implicit features.\n    val implicitFeaturesFromTableConf =\n      (readerVersionFromTableConfOpt, writerVersionFromTableConfOpt) match {\n        case (Some(readerVersion), Some(writerVersion)) =>\n          // We cannot have a table features reader version if the protocol does not\n          // support writer features.\n          val sanitizedReaderVersion = if (supportsWriterFeatures(writerVersion)) {\n            readerVersion\n          } else {\n            Math.min(2, readerVersion)\n          }\n          Protocol(sanitizedReaderVersion, writerVersion).implicitlySupportedFeatures\n        case _ => Set.empty\n      }\n\n    // Construct the minimum required protocol for the enabled features.\n    val minProtocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n      .withFeatures(allEnabledFeatures ++ implicitFeaturesFromTableConf)\n      .normalized\n\n    // Return the minimum protocol components.\n    (minProtocol.minReaderVersion, minProtocol.minWriterVersion,\n      minProtocol.implicitlyAndExplicitlySupportedFeatures)\n  }\n\n  /**\n   * Given the Delta table metadata, returns the minimum required reader and writer version\n   * that satisfies all enabled table features in the metadata plus all enabled features as a set.\n   *\n   * This function returns the protocol versions and features individually instead of a\n   * [[Protocol]], so the caller can identify the features that caused the protocol version. For\n   * example, if the return values are (2, 5, columnMapping), the caller can safely ignore all\n   * other features required by the protocol with a reader and writer version of 2 and 5.\n   *\n   * This method does not process protocol-related configs in table properties or session\n   * defaults, i.e., configs with keys [[MIN_READER_VERSION_PROP]], [[MIN_WRITER_VERSION_PROP]],\n   * and [[FEATURE_PROP_PREFIX]].\n   */\n  def minProtocolComponentsFromAutomaticallyEnabledFeatures(\n      spark: SparkSession,\n      metadata: Metadata,\n      current: Protocol): (Int, Int, Set[TableFeature]) = {\n    val enabledFeatures = extractAutomaticallyEnabledFeatures(spark, metadata, current)\n    var (readerVersion, writerVersion) = (0, 0)\n    enabledFeatures.foreach { feature =>\n      readerVersion = math.max(readerVersion, feature.minReaderVersion)\n      writerVersion = math.max(writerVersion, feature.minWriterVersion)\n    }\n\n    (readerVersion, writerVersion, enabledFeatures)\n  }\n\n  /** Cast the table property for the protocol version to an integer. */\n  private def tryCastProtocolVersionToInt(key: String, value: String): Int = {\n    try value.toInt\n    catch {\n      case _: NumberFormatException =>\n        throw DeltaErrors.protocolPropNotIntException(key, value)\n    }\n  }\n\n  def getReaderVersionFromTableConf(conf: Map[String, String]): Option[Int] = {\n    conf.get(MIN_READER_VERSION_PROP).map(tryCastProtocolVersionToInt(MIN_READER_VERSION_PROP, _))\n  }\n\n  def getWriterVersionFromTableConf(conf: Map[String, String]): Option[Int] = {\n    conf.get(MIN_WRITER_VERSION_PROP).map(tryCastProtocolVersionToInt(MIN_WRITER_VERSION_PROP, _))\n  }\n\n  def getProtocolVersionsFromTableConf(conf: Map[String, String]): (Option[Int], Option[Int]) = {\n    (getReaderVersionFromTableConf(conf), getWriterVersionFromTableConf(conf))\n  }\n\n  def filterProtocolPropsFromTableProps(properties: Map[String, String]): Map[String, String] =\n    properties.filterNot {\n      case (k, _) => TableFeatureProtocolUtils.isTableProtocolProperty(k)\n    }\n\n  /** Assert a table metadata contains no protocol-related table properties. */\n  def assertMetadataContainsNoProtocolProps(metadata: Metadata): Unit = {\n    assert(\n      !metadata.configuration.contains(MIN_READER_VERSION_PROP),\n      \"Should not have the \" +\n        s\"protocol version ($MIN_READER_VERSION_PROP) as part of table properties\")\n    assert(\n      !metadata.configuration.contains(MIN_WRITER_VERSION_PROP),\n      \"Should not have the \" +\n        s\"protocol version ($MIN_WRITER_VERSION_PROP) as part of table properties\")\n    assert(\n      !metadata.configuration.keys.exists(_.startsWith(FEATURE_PROP_PREFIX)),\n      \"Should not have \" +\n        s\"table features (starts with '$FEATURE_PROP_PREFIX') as part of table properties\")\n    assert(\n      !metadata.configuration.contains(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key),\n      \"Should not have the table property \" +\n        s\"${DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key} stored in table metadata\")\n  }\n\n  /**\n   * Upgrade the current protocol to satisfy all auto-update capable features required by the table\n   * metadata. An Delta error will be thrown if a non-auto-update capable feature is required by\n   * the metadata and not in the resulting protocol, in such a case the user must run `ALTER TABLE`\n   * to add support for this feature beforehand using the `delta.feature.featureName` table\n   * property.\n   *\n   * Refer to [[FeatureAutomaticallyEnabledByMetadata.automaticallyUpdateProtocolOfExistingTables]]\n   * to know more about \"auto-update capable\" features.\n   *\n   * Note: this method only considers metadata-enabled features. To avoid confusion, the caller\n   * must apply and remove protocol-related table properties from the metadata before calling this\n   * method.\n   */\n  def upgradeProtocolFromMetadataForExistingTable(\n      spark: SparkSession,\n      metadata: Metadata,\n      current: Protocol): Option[Protocol] = {\n\n    val required =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(extractAutomaticallyEnabledFeatures(spark, metadata, current))\n        .normalized\n\n    if (!required.canUpgradeTo(current)) {\n      // When the current protocol does not satisfy metadata requirement, some additional features\n      // must be supported by the protocol. We assert those features can actually perform the\n      // auto-update.\n      assertMetadataTableFeaturesAutomaticallySupported(\n        current.implicitlyAndExplicitlySupportedFeatures,\n        required.implicitlyAndExplicitlySupportedFeatures)\n      Some(required.merge(current))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Ensure all features listed in `currentFeatures` are also listed in `requiredFeatures`, or, if\n   * one is not listed, it must be capable to auto-update a protocol.\n   *\n   * Refer to [[FeatureAutomaticallyEnabledByMetadata.automaticallyUpdateProtocolOfExistingTables]]\n   * to know more about \"auto-update capable\" features.\n   *\n   * Note: Caller must make sure `requiredFeatures` is obtained from a min protocol that satisfies\n   * a table metadata.\n   */\n  private def assertMetadataTableFeaturesAutomaticallySupported(\n      currentFeatures: Set[TableFeature],\n      requiredFeatures: Set[TableFeature]): Unit = {\n    val (autoUpdateCapableFeatures, nonAutoUpdateCapableFeatures) =\n      requiredFeatures.diff(currentFeatures)\n        .collect { case f: FeatureAutomaticallyEnabledByMetadata => f }\n        .partition(_.automaticallyUpdateProtocolOfExistingTables)\n    if (nonAutoUpdateCapableFeatures.nonEmpty) {\n      // The \"current features\" we give to the user are from the original protocol, plus\n      // features newly supported by table properties in the current transaction, plus\n      // metadata-enabled features that are auto-update capable. The first two are provided by\n      // `currentFeatures`.\n      throw DeltaErrors.tableFeaturesRequireManualEnablementException(\n        nonAutoUpdateCapableFeatures,\n        currentFeatures ++ autoUpdateCapableFeatures)\n    }\n  }\n\n  /**\n   * Verify that the table properties satisfy legality constraints. Throw an exception if not.\n   */\n  def assertTablePropertyConstraintsSatisfied(\n      spark: SparkSession,\n      metadata: Metadata,\n      snapshot: Snapshot): Unit = {\n    import DeltaTablePropertyValidationFailedSubClass._\n\n    val tableName = if (metadata.name != null) metadata.name else metadata.id\n\n    val configs = metadata.configuration.map { case (k, v) => k.toLowerCase(Locale.ROOT) -> v }\n    val dvsEnabled = {\n      val lowerCaseKey = DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key.toLowerCase(Locale.ROOT)\n      configs.get(lowerCaseKey).exists(_.toBoolean)\n    }\n    if (dvsEnabled && metadata.format.provider != \"parquet\") {\n      // DVs only work with parquet-based delta tables.\n      throw new DeltaTablePropertyValidationFailedException(\n        table = tableName,\n        subClass = PersistentDeletionVectorsInNonParquetTable)\n    }\n    val manifestGenerationEnabled = {\n      val lowerCaseKey = DeltaConfigs.SYMLINK_FORMAT_MANIFEST_ENABLED.key.toLowerCase(Locale.ROOT)\n      configs.get(lowerCaseKey).exists(_.toBoolean)\n    }\n    if (dvsEnabled && manifestGenerationEnabled) {\n      throw new DeltaTablePropertyValidationFailedException(\n        table = tableName,\n        subClass = PersistentDeletionVectorsWithIncrementalManifestGeneration)\n    }\n    if (manifestGenerationEnabled) {\n      // Only allow enabling this, if there are no DVs present.\n      if (!DeletionVectorUtils.isTableDVFree(snapshot)) {\n        throw new DeltaTablePropertyValidationFailedException(\n          table = tableName,\n          subClass = ExistingDeletionVectorsWithIncrementalManifestGeneration)\n      }\n    }\n  }\n}\n\n/**\n * Sets the committed version for a given application. Used to make operations\n * like streaming append idempotent.\n */\ncase class SetTransaction(\n    appId: String,\n    version: Long,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    lastUpdated: Option[Long]) extends Action {\n  override def wrap: SingleAction = SingleAction(txn = this)\n}\n\n/**\n * The domain metadata action contains a configuration (string-string map) for a named metadata\n * domain. Two overlapping transactions conflict if they both contain a domain metadata action for\n * the same metadata domain.\n *\n * [[domain]]: A string used to identify a specific feature.\n * [[configuration]]: A string containing configuration options for the conflict domain.\n * [[removed]]: If it is true it serves as a tombstone to logically delete a [[DomainMetadata]]\n *              action.\n */\ncase class DomainMetadata(\n    domain: String,\n    configuration: String,\n    removed: Boolean) extends Action {\n  override def wrap: SingleAction = SingleAction(domainMetadata = this)\n}\n\n/** Actions pertaining to the addition and removal of files. */\nsealed trait FileAction extends Action {\n  val path: String\n  val dataChange: Boolean\n  @JsonIgnore\n  val tags: Map[String, String]\n  @JsonIgnore\n  lazy val pathAsUri: URI = {\n    // Paths like http:example.com are opaque URIs that have schema and scheme-specific parts, but\n    // path is not defined. We do not support such paths, so we throw an exception.\n    val uri = new URI(path)\n    if (uri.getPath == null) {\n      throw DeltaErrors.cannotReconstructPathFromURI(path)\n    }\n    uri\n  }\n  @JsonIgnore\n  def numLogicalRecords: Option[Long]\n  @JsonIgnore\n  val partitionValues: Map[String, String]\n  @JsonIgnore\n  def getFileSize: Long\n  def stats: String\n  def deletionVector: DeletionVectorDescriptor\n\n  /** Returns the approx size of the remaining records after excluding the deleted ones. */\n  @JsonIgnore\n  def estLogicalFileSize: Option[Long]\n\n  /** Returns [[tags]] or an empty Map if null */\n  @JsonIgnore\n  def tagsOrEmpty: Map[String, String] = Option(tags).getOrElse(Map.empty[String, String])\n\n  /**\n   * Return tag value if tags is not null and the tag present.\n   */\n  @JsonIgnore\n  def getTag(tagName: String): Option[String] = Option(tags).flatMap(_.get(tagName))\n\n\n  /** Returns the [[SparkPath]] for this file action. */\n  def sparkPath: SparkPath = SparkPath.fromUrlString(path)\n\n  /** Returns the [[Path]] for this file action (not URL-encoded). */\n  def toPath: Path = sparkPath.toPath\n\n  /** Returns the absolute [[Path]] for this file action (not URL-encoded). */\n  def absolutePath(deltaLog: DeltaLog): Path = {\n    // dataPath is not URL-encoded.\n    val dataPath: Path = deltaLog.dataPath\n    // this.path is a URL-encoded String, that is either the relative or absolute path.\n    DeltaFileOperations.absolutePath(dataPath.toString, path)\n  }\n}\n\ncase class ParsedStatsFields(\n  numLogicalRecords: Option[Long],\n  tightBounds: Option[Boolean])\n\n/**\n * Common trait for AddFile and RemoveFile actions providing methods for the computation of\n * logical, physical and deleted number of records based on the statistics and the Deletion Vector\n * of the file.\n */\ntrait HasNumRecords {\n  this: FileAction =>\n\n  @JsonIgnore\n  @transient\n  protected lazy val parsedStatsFields: Option[ParsedStatsFields] = Option(stats).collect {\n    case stats if stats.nonEmpty =>\n      val node = new ObjectMapper().readTree(stats)\n      val numLogicalRecords = if (node.has(\"numRecords\")) {\n        Some(node.get(\"numRecords\")).filterNot(_.isNull).map(_.asLong())\n          .map(_ - numDeletedRecords)\n      } else None\n      val tightBounds = if (node.has(\"tightBounds\")) {\n        Some(node.get(\"tightBounds\")).filterNot(_.isNull).map(_.asBoolean())\n      } else None\n\n      ParsedStatsFields(numLogicalRecords, tightBounds)\n  }\n\n  /** Returns the number of logical records, which do not include those marked as deleted. */\n  @JsonIgnore\n  @transient\n  override lazy val numLogicalRecords: Option[Long] = parsedStatsFields.flatMap(_.numLogicalRecords)\n\n  /** Returns the number of records marked as deleted. */\n  @JsonIgnore\n  def numDeletedRecords: Long = deletionVector match {\n    case dv: DeletionVectorDescriptor => dv.cardinality\n    case _ => 0L\n  }\n\n  /** Returns the total number of records, including those marked as deleted. */\n  @JsonIgnore\n  def numPhysicalRecords: Option[Long] = numLogicalRecords.map(_ + numDeletedRecords)\n\n  /** Returns the estimated size of the logical records in the file. */\n  @JsonIgnore\n  override def estLogicalFileSize: Option[Long] =\n    logicalToPhysicalRecordsRatio.map(n => (n * getFileSize).toLong)\n\n  /** Returns the ratio of the logical number of records to the total number of records. */\n  @JsonIgnore\n  def logicalToPhysicalRecordsRatio: Option[Double] = numLogicalRecords.map { numLogicalRecords =>\n    numLogicalRecords.toDouble / (numLogicalRecords + numDeletedRecords)\n  }\n\n  /** Returns the ratio of number of deleted records to the total number of records. */\n  @JsonIgnore\n  def deletedToPhysicalRecordsRatio: Option[Double] = logicalToPhysicalRecordsRatio.map(1.0d - _)\n\n  /** Returns whether the statistics are tight or wide. */\n  @JsonIgnore\n  @transient\n  lazy val tightBounds: Option[Boolean] = parsedStatsFields.flatMap(_.tightBounds)\n}\n\n/**\n * Adds a new file to the table. When multiple [[AddFile]] file actions\n * are seen with the same `path` only the metadata from the last one is\n * kept.\n *\n * [[path]] is URL-encoded.\n */\ncase class AddFile(\n    override val path: String,\n    @JsonInclude(JsonInclude.Include.ALWAYS)\n    /**\n     * [[partitionValues]] can be a raw and not normalized string. This is critical for a certain\n     * data types such as timestamps. Use [[normalizedPartitionValues]] instead if you want a\n     * normalized value.\n     */\n    partitionValues: Map[String, String],\n    size: Long,\n    modificationTime: Long,\n    override val dataChange: Boolean,\n    override val stats: String = null,\n    override val tags: Map[String, String] = null,\n    override val deletionVector: DeletionVectorDescriptor = null,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    baseRowId: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    defaultRowCommitVersion: Option[Long] = None,\n    clusteringProvider: Option[String] = None\n) extends FileAction with HasNumRecords {\n  require(path.nonEmpty)\n\n  override def wrap: SingleAction = SingleAction(add = this)\n\n  def remove: RemoveFile = removeWithTimestamp()\n\n  def removeWithTimestamp(\n      timestamp: Long = System.currentTimeMillis(),\n      dataChange: Boolean = true\n    ): RemoveFile = {\n    var newTags = tags\n    // scalastyle:off\n    RemoveFile(\n      path, Some(timestamp), dataChange,\n      extendedFileMetadata = Some(true), partitionValues, Some(size), newTags,\n      deletionVector = deletionVector,\n      baseRowId = baseRowId,\n      defaultRowCommitVersion = defaultRowCommitVersion,\n      stats = stats\n    )\n    // scalastyle:on\n  }\n\n  /**\n   * Logically remove rows by associating a `deletionVector` with the file.\n   * @param deletionVector: The descriptor of the DV that marks rows as deleted.\n   * @param dataChange: When false, the actions are marked as no-data-change actions.\n   */\n  def removeRows(\n        deletionVector: DeletionVectorDescriptor,\n        updateStats: Boolean,\n        dataChange: Boolean = true): (AddFile, RemoveFile) = {\n    // Verify DV does not contain any invalid row indexes. Note, maxRowIndex is optional\n    // and not all commands may set it when updating DVs.\n    (numPhysicalRecords, deletionVector.maxRowIndex) match {\n      case (Some(numPhysicalRecords), Some(maxRowIndex))\n        if (maxRowIndex + 1 > numPhysicalRecords) =>\n          throw DeltaErrors.deletionVectorInvalidRowIndex()\n      case _ => // Nothing to check.\n    }\n    // We make sure maxRowIndex is not stored in the log.\n    val dvDescriptorWithoutMaxRowIndex = deletionVector.maxRowIndex match {\n      case Some(_) => deletionVector.copy(maxRowIndex = None)\n      case _ => deletionVector\n    }\n    var addFileWithNewDv =\n      this.copy(deletionVector = dvDescriptorWithoutMaxRowIndex, dataChange = dataChange)\n    if (updateStats) {\n      addFileWithNewDv = addFileWithNewDv.withoutTightBoundStats\n    }\n    val removeFileWithOldDv = this.removeWithTimestamp(dataChange = dataChange)\n\n    // Sanity check for incremental DV updates.\n    if (addFileWithNewDv.numDeletedRecords < removeFileWithOldDv.numDeletedRecords) {\n      throw DeltaErrors.deletionVectorSizeMismatch()\n    }\n\n    (addFileWithNewDv, removeFileWithOldDv)\n  }\n\n  /**\n   * Return the unique id of the deletion vector, if present, or `None` if there's no DV.\n   *\n   * The unique id differentiates DVs, even if there are multiple in the same file\n   * or the DV is stored inline.\n   */\n  @JsonIgnore\n  def getDeletionVectorUniqueId: Option[String] = Option(deletionVector).map(_.uniqueId)\n\n  /** Update stats to have tightBounds = false, if file has any stats. */\n  def withoutTightBoundStats: AddFile = {\n    if (stats == null || stats.isEmpty) {\n      this\n    } else {\n      val node = JsonUtils.mapper.readTree(stats).asInstanceOf[ObjectNode]\n      if (node.has(\"tightBounds\") &&\n          !node.get(\"tightBounds\").asBoolean(true)) {\n        this\n      } else {\n        node.put(\"tightBounds\", false)\n        val newStatsString = JsonUtils.mapper.writer.writeValueAsString(node)\n        this.copy(stats = newStatsString)\n      }\n    }\n  }\n\n  /**\n   * Return partition values as literals with the correct data type according to the partition\n   * schema. Typed literals are safe for comparison purposes as the value and not the string\n   * format is compared.\n   * @return Map of partition column names to literals with the correct data type.\n   */\n  def normalizedPartitionValues(\n      spark: SparkSession,\n      deltaTxn: OptimisticTransaction): Map[String, Literal] = {\n    normalizedPartitionValues(spark, deltaTxn.metadata.physicalPartitionSchema, Some(deltaTxn))\n  }\n\n  /**\n   * Return partition values as literals with the correct data type according to the partition\n   * schema. Typed literals are safe for comparison purposes as the value and not the string\n   * format is compared.\n   * @return Map of partition column names to literals with the correct data type.\n   */\n  def normalizedPartitionValues(\n      spark: SparkSession,\n      partitionSchema: StructType,\n      deltaTxn: Option[OptimisticTransaction] = None): Map[String, Literal] = {\n\n    def partitionValuesAsStringLiterals: Map[String, Literal] = {\n      // Convert all partition values to string literals\n      partitionValues.map { case (k, v) => (k, Literal(v)) }\n    }\n\n    val normalizePartitionValuesOnRead =\n      spark.conf.get(DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ)\n    if (normalizePartitionValuesOnRead) {\n      val timeZone = spark.sessionState.conf.sessionLocalTimeZone\n\n      try {\n        val typedPartitionValueLiterals = PartitionUtils.parsePartitionValues(\n          partitionValues,\n          partitionSchema,\n          java.util.TimeZone.getDefault.getID,\n          validatePartitionColumns = true)\n\n        val stringNormalizedPartitionValues = typedPartitionValueLiterals.map {\n          case (k, v) => (k, PartitionUtils.literalToNormalizedString(\n            v,\n            Some(timeZone),\n            useUtcNormalizedTimestamp = true))\n        }\n\n        if (stringNormalizedPartitionValues != partitionValues) {\n          Action.recordDeltaEvent(\n            deltaTxn.map(_.deltaLog).orNull,\n            opType = \"delta.normalizedPartitionValues.unnormalizedValuesExist\",\n            data = Map(\n              \"readSnapshotMetadata\" -> deltaTxn.map(_.snapshot.metadata).orNull,\n              \"txnMetadata\" -> deltaTxn.map(_.metadata).orNull,\n              \"commitInfo\" -> deltaTxn.map(_.getCommitInfo).orNull\n            )\n          )\n        }\n        typedPartitionValueLiterals\n      } catch {\n        case NonFatal(e) =>\n          val opTypeSuffix = PartitionUtils.classifyPartitionValueParsingError(e)\n          Action.recordDeltaEvent(\n            deltaTxn.map(_.deltaLog).orNull,\n            opType = \"delta.normalizedPartitionValues.partitionValueParsingError\" + opTypeSuffix,\n            data = Map(\n              \"exceptionMessage\" -> e.getMessage,\n              \"readSnapshotMetadata\" -> deltaTxn.map(_.snapshot.metadata).orNull,\n              \"txnMetadata\" -> deltaTxn.map(_.metadata).orNull,\n              \"commitInfo\" -> deltaTxn.map(_.getCommitInfo).orNull,\n              \"readSnapshotVersion\" -> deltaTxn.map(_.snapshot.version).getOrElse(-1L),\n              \"timeZone\" -> timeZone\n            )\n          )\n          partitionValuesAsStringLiterals\n      }\n    } else {\n        partitionValuesAsStringLiterals\n    }\n  }\n\n  // Don't use lazy val because we want to save memory.\n  @JsonIgnore\n  def insertionTime: Long = longTag(AddFile.Tags.INSERTION_TIME)\n    // From modification time in milliseconds to microseconds.\n    .getOrElse(TimeUnit.MICROSECONDS.convert(modificationTime, TimeUnit.MILLISECONDS))\n\n\n  def copyWithTags(newTags: Map[String, String]): AddFile =\n    copy(tags = tagsOrEmpty ++ newTags)\n\n  def tag(tag: AddFile.Tags.KeyType): Option[String] = getTag(tag.name)\n\n  def longTag(tagKey: AddFile.Tags.KeyType): Option[Long] =\n    tag(tagKey).map(_.toLong)\n\n  def copyWithTag(tag: AddFile.Tags.KeyType, value: String): AddFile =\n    copy(tags = tagsOrEmpty + (tag.name -> value))\n\n  def copyWithoutTag(tag: AddFile.Tags.KeyType): AddFile = {\n    if (tags == null) {\n      this\n    } else {\n      copy(tags = tags - tag.name)\n    }\n  }\n\n  @JsonIgnore\n  override def getFileSize: Long = size\n\n  /**\n   * Before serializing make sure deletionVector.maxRowIndex is not defined.\n   * This is only a transient property and it is not intended to be stored in the log.\n   */\n  override def json: String = {\n    if (deletionVector != null) assert(!deletionVector.maxRowIndex.isDefined)\n    super.json\n  }\n\n}\n\nobject AddFile {\n  /**\n   * Misc file-level metadata.\n   *\n   * The convention is that clients may safely ignore any/all of these tags and this should never\n   * have an impact on correctness.\n   *\n   * Otherwise, the information should go as a field of the AddFile action itself and the Delta\n   * protocol version should be bumped.\n   */\n  object Tags {\n    sealed abstract class KeyType(val name: String)\n\n    /** [[ZCUBE_ID]]: identifier of the OPTIMIZE ZORDER BY job that this file was produced by */\n    object ZCUBE_ID extends AddFile.Tags.KeyType(\"ZCUBE_ID\")\n\n    /** [[ZCUBE_ZORDER_BY]]: ZOrdering of the corresponding ZCube */\n    object ZCUBE_ZORDER_BY extends AddFile.Tags.KeyType(\"ZCUBE_ZORDER_BY\")\n\n    /** [[ZCUBE_ZORDER_CURVE]]: Clustering strategy of the corresponding ZCube */\n    object ZCUBE_ZORDER_CURVE extends AddFile.Tags.KeyType(\"ZCUBE_ZORDER_CURVE\")\n\n    /**\n     * [[INSERTION_TIME]]: the latest timestamp in micro seconds when the data in the file\n     * was inserted\n     */\n    object INSERTION_TIME extends AddFile.Tags.KeyType(\"INSERTION_TIME\")\n\n\n    /** [[PARTITION_ID]]: rdd partition id that has written the file, will not be stored in the\n     physical log, only used for communication  */\n    object PARTITION_ID extends AddFile.Tags.KeyType(\"PARTITION_ID\")\n\n    /** [[OPTIMIZE_TARGET_SIZE]]: target file size the file was optimized to. */\n    object OPTIMIZE_TARGET_SIZE extends AddFile.Tags.KeyType(\"OPTIMIZE_TARGET_SIZE\")\n\n\n    /** [[ICEBERG_COMPAT_VERSION]]: IcebergCompat version */\n    object ICEBERG_COMPAT_VERSION extends AddFile.Tags.KeyType(\"ICEBERG_COMPAT_VERSION\")\n  }\n\n  /** Convert a [[Tags.KeyType]] to a string to be used in the AddMap.tags Map[String, String]. */\n  def tag(tagKey: Tags.KeyType): String = tagKey.name\n}\n\n/**\n * Logical removal of a given file from the reservoir. Acts as a tombstone before a file is\n * deleted permanently.\n *\n * Note that for protocol compatibility reasons, the fields `partitionValues`, `size`, and `tags`\n * are only present when the extendedFileMetadata flag is true. New writers should generally be\n * setting this flag, but old writers (and FSCK) won't, so readers must check this flag before\n * attempting to consume those values.\n *\n * Since old tables would not have `extendedFileMetadata` and `size` field, we should make them\n * nullable by setting their type Option.\n *\n * [[path]] is URL-encoded.\n */\n// scalastyle:off\ncase class RemoveFile(\n    override val path: String,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    deletionTimestamp: Option[Long],\n    override val dataChange: Boolean = true,\n    extendedFileMetadata: Option[Boolean] = None,\n    partitionValues: Map[String, String] = null,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    size: Option[Long] = None,\n    override val tags: Map[String, String] = null,\n    override val deletionVector: DeletionVectorDescriptor = null,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    baseRowId: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    defaultRowCommitVersion: Option[Long] = None,\n    override val stats: String = null\n) extends FileAction with HasNumRecords {\n  override def wrap: SingleAction = SingleAction(remove = this)\n\n  @JsonIgnore\n  val delTimestamp: Long = deletionTimestamp.getOrElse(0L)\n\n  /**\n   * Return the unique id of the deletion vector, if present, or `None` if there's no DV.\n   *\n   * The unique id differentiates DVs, even if there are multiple in the same file\n   * or the DV is stored inline.\n   */\n  @JsonIgnore\n  def getDeletionVectorUniqueId: Option[String] = Option(deletionVector).map(_.uniqueId)\n\n  /**\n   * Create a copy with the new tag. `extendedFileMetadata` is copied unchanged.\n   */\n  def copyWithTag(tag: String, value: String): RemoveFile = copy(\n    tags = tagsOrEmpty + (tag -> value))\n\n  /**\n   * Create a copy without the tag.\n   */\n  def copyWithoutTag(tag: String): RemoveFile =\n    copy(tags = tagsOrEmpty - tag)\n\n  @JsonIgnore\n  override def getFileSize: Long = size.getOrElse(0L)\n\n  /** Only for testing. */\n  @JsonIgnore\n  private [delta] def isDVTombstone: Boolean = DeletionVectorDescriptor.isDeletionVectorPath(new Path(path))\n\n}\n// scalastyle:on\n\n/**\n * A change file containing CDC data for the Delta version it's within. Non-CDC readers should\n * ignore this, CDC readers should scan all ChangeFiles in a version rather than computing\n * changes from AddFile and RemoveFile actions.\n *\n * [[path]] is URL-encoded.\n */\n@JsonIgnoreProperties(Array(\"stats\"))\ncase class AddCDCFile(\n    override val path: String,\n    @JsonInclude(JsonInclude.Include.ALWAYS)\n    partitionValues: Map[String, String],\n    size: Long,\n    override val tags: Map[String, String] = null,\n    override val stats: String = null) extends FileAction with HasNumRecords {\n  override val dataChange = false\n  @JsonIgnore\n  override val deletionVector: DeletionVectorDescriptor = null\n\n  override def wrap: SingleAction = SingleAction(cdc = this)\n\n  @JsonIgnore\n  override def getFileSize: Long = size\n\n  @JsonIgnore\n  override def estLogicalFileSize: Option[Long] = None\n}\n\ncase class Format(\n    provider: String = \"parquet\",\n    // If we support `options` in future, we should not store any file system options since they may\n    // contain credentials.\n    options: Map[String, String] = Map.empty)\n\n/**\n * Updates the metadata of the table. Only the last update to the [[Metadata]]\n * of a table is kept. It is the responsibility of the writer to ensure that\n * any data already present in the table is still valid after any change.\n */\ncase class Metadata(\n    id: String = java.util.UUID.randomUUID().toString,\n    name: String = null,\n    description: String = null,\n    format: Format = Format(),\n    schemaString: String = null,\n    partitionColumns: Seq[String] = Nil,\n    configuration: Map[String, String] = Map.empty,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    createdTime: Option[Long] = None)\n  extends Action with SparkAbstractMetadata with StorageAbstractMetadata {\n\n  // The `schema` and `partitionSchema` methods should be vals or lazy vals, NOT\n  // defs, because parsing StructTypes from JSON is extremely expensive and has\n  // caused perf. problems here in the past:\n\n  /**\n   * Compare this metadata with other.\n   * Returns a set of field names that differ between the two metadata objects.\n   * Returns an empty set when there are no differences.\n   */\n  def diffFieldNames(other: Metadata): Set[String] = {\n    import scala.reflect.runtime.universe._\n\n    // In scala 2.13, we can directly use productElementName(n: Int) along with productArity.\n    val fieldNames = typeOf[Metadata].members.sorted.collect {\n      case m: MethodSymbol if m.isCaseAccessor => m.name.toString\n    }\n    // It relies on the fact that members.sorted outputs fields in declaration order.\n    fieldNames.zipWithIndex.collect {\n      case (name, i) if this.productElement(i) != other.productElement(i) => name\n    }.toSet\n  }\n\n  /**\n   * Column mapping mode for this table\n   */\n  @JsonIgnore\n  lazy val columnMappingMode: DeltaColumnMappingMode =\n    DeltaConfigs.COLUMN_MAPPING_MODE.fromMetaData(this)\n\n  /**\n   * Column mapping max id for this table\n   */\n  @JsonIgnore\n  lazy val columnMappingMaxId: Long =\n    DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMetaData(this)\n\n  /** Returns the schema as a [[StructType]] */\n  @JsonIgnore\n  lazy val schema: StructType = Option(schemaString)\n    .map(DataType.fromJson(_).asInstanceOf[StructType])\n    .getOrElse(StructType.apply(Nil))\n\n  /** Returns the partitionSchema as a [[StructType]] */\n  @JsonIgnore\n  lazy val partitionSchema: StructType =\n    new StructType(partitionColumns.map(c => schema(c)).toArray)\n\n  /** Partition value keys in the AddFile map. */\n  @JsonIgnore\n  lazy val physicalPartitionSchema: StructType =\n    DeltaColumnMapping.renameColumns(partitionSchema)\n\n  /** Columns written out to files. */\n  @JsonIgnore\n  lazy val dataSchema: StructType = {\n    val partitions = partitionColumns.toSet\n    StructType(schema.filterNot(f => partitions.contains(f.name)))\n  }\n\n  /** Partition value written out to files */\n  @JsonIgnore\n  lazy val physicalPartitionColumns: Seq[String] = physicalPartitionSchema.fieldNames.toSeq\n\n  /**\n   * Store non-partition columns and their corresponding [[OptimizablePartitionExpression]] which\n   * can be used to create partition filters from data filters of these non-partition columns.\n   */\n  @JsonIgnore\n  lazy val optimizablePartitionExpressions: Map[String, Seq[OptimizablePartitionExpression]] =\n    GeneratedColumn.getOptimizablePartitionExpressions(schema, partitionSchema)\n\n  /**\n   * The name of commit-coordinator which arbitrates the commits to the table. This must be\n   * available if this is a coordinated-commits table.\n   */\n  @JsonIgnore\n  lazy val coordinatedCommitsCoordinatorName: Option[String] =\n    DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.fromMetaData(this)\n\n  /** The configuration to uniquely identify the commit-coordinator for coordinated-commits. */\n  @JsonIgnore\n  lazy val coordinatedCommitsCoordinatorConf: Map[String, String] =\n    DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.fromMetaData(this)\n\n  /** The table specific configuration for coordinated-commits. */\n  @JsonIgnore\n  lazy val coordinatedCommitsTableConf: Map[String, String] =\n    DeltaConfigs.COORDINATED_COMMITS_TABLE_CONF.fromMetaData(this)\n\n  override def wrap: SingleAction = SingleAction(metaData = this)\n\n  override def getId: String = id\n\n  override def getName: String = name\n\n  override def getDescription: String = description\n\n  @JsonIgnore\n  override def getProvider: String = format.provider\n\n  @JsonIgnore\n  override def getFormatOptions: java.util.Map[String, String] = format.options.asJava\n\n  override def getSchemaString: String = schemaString\n\n  override def getPartitionColumns: java.util.List[String] = partitionColumns.asJava\n\n  override def getConfiguration: java.util.Map[String, String] = configuration.asJava\n\n  override def getCreatedTime: java.lang.Long = createdTime.map(Long.box).orNull\n}\n\n/**\n * Interface for objects that represents the information for a commit. Commits can be referred to\n * using a version and timestamp. The timestamp of a commit comes from the remote storage\n * `lastModifiedTime`, and can be adjusted for clock skew. Hence we have the method `withTimestamp`.\n */\ntrait CommitMarker {\n  /** Get the timestamp of the commit as millis after the epoch. */\n  def getTimestamp: Long\n  /** Return a copy object of this object with the given timestamp. */\n  def withTimestamp(timestamp: Long): CommitMarker\n  /** Get the version of the commit. */\n  def getVersion: Long\n}\n\n/**\n * Holds provenance information about changes to the table. This [[Action]]\n * is not stored in the checkpoint and has reduced compatibility guarantees.\n * Information stored in it is best effort (i.e. can be falsified by the writer).\n *\n * @param inCommitTimestamp A monotonically increasing timestamp that represents the time since\n *                          epoch in milliseconds when the commit write was started. This should\n *                          only be set when the feature inCommitTimestamps is enabled.\n * @param isBlindAppend Whether this commit has blindly appended without caring about existing files\n * @param engineInfo The information for the engine that makes the commit.\n *                   If a commit is made by Delta Lake 1.1.0 or above, it will be\n *                   `Apache-Spark/x.y.z Delta-Lake/x.y.z`.\n */\ncase class CommitInfo(\n    // The commit version should be left unfilled during commit(). When reading a delta file, we can\n    // infer the commit version from the file name and fill in this field then.\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    version: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    inCommitTimestamp: Option[Long],\n    timestamp: Timestamp,\n    userId: Option[String],\n    userName: Option[String],\n    operation: String,\n    @JsonSerialize(using = classOf[JsonMapSerializer])\n    @JsonDeserialize(using = classOf[JsonMapDeserializer])\n    operationParameters: Map[String, String],\n    job: Option[JobInfo],\n    notebook: Option[NotebookInfo],\n    clusterId: Option[String],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    readVersion: Option[Long],\n    isolationLevel: Option[String],\n    isBlindAppend: Option[Boolean],\n    operationMetrics: Option[Map[String, String]],\n    userMetadata: Option[String],\n    tags: Option[Map[String, String]],\n    engineInfo: Option[String],\n    txnId: Option[String])\n  extends Action with CommitMarker with SparkAbstractCommitInfo with StorageAbstractCommitInfo {\n  override def wrap: SingleAction = SingleAction(commitInfo = this)\n\n  override def withTimestamp(timestamp: Long): CommitInfo = {\n    this.copy(timestamp = new Timestamp(timestamp))\n  }\n\n  // We need to explicitly ignore this field during serialization as Jackson\n  // by default calls all public getters of an object, which would lead to\n  // either an exception or the inCommitTimestamp being serialized twice.\n  @JsonIgnore\n  override def getCommitTimestamp: Long = {\n    inCommitTimestamp.getOrElse {\n      throw DeltaErrors.missingCommitTimestamp(version.map(_.toString).getOrElse(\"unknown\"))\n    }\n  }\n\n  override def getTimestamp: Long = timestamp.getTime\n  @JsonIgnore\n  override def getVersion: Long = version.get\n\n}\n\ncase class JobInfo(\n    jobId: String,\n    jobName: String,\n    jobRunId: String,\n    runId: String,\n    jobOwnerId: String,\n    triggerType: String)\n\nobject JobInfo {\n  def fromContext(context: Map[String, String]): Option[JobInfo] = {\n    context.get(\"jobId\").map { jobId =>\n      JobInfo(\n        jobId,\n        context.get(\"jobName\").orNull,\n        context.get(\"multitaskParentRunId\").orNull,\n        context.get(\"runId\").orNull,\n        context.get(\"jobOwnerId\").orNull,\n        context.get(\"jobTriggerType\").orNull)\n    }\n  }\n}\n\ncase class NotebookInfo(notebookId: String)\n\nobject NotebookInfo {\n  def fromContext(context: Map[String, String]): Option[NotebookInfo] = {\n    context.get(\"notebookId\").orElse(context.get(\"notebook_id\")).map { nbId => NotebookInfo(nbId) }\n  }\n}\n\nobject CommitInfo {\n  def empty(version: Option[Long] = None): CommitInfo = {\n    CommitInfo(version, None, null, None, None, null, null, None, None,\n      None, None, None, None, None, None, None, None, None)\n  }\n\n  // scalastyle:off argcount\n\n  def apply(\n      time: Long,\n      operation: String,\n      inCommitTimestamp: Option[Long] = None,\n      operationParameters: Map[String, String],\n      commandContext: Map[String, String],\n      readVersion: Option[Long],\n      isolationLevel: Option[String],\n      isBlindAppend: Option[Boolean],\n      operationMetrics: Option[Map[String, String]],\n      userMetadata: Option[String],\n      tags: Option[Map[String, String]],\n      txnId: Option[String]): CommitInfo = {\n    apply(None, time, operation, inCommitTimestamp, operationParameters, commandContext,\n      readVersion, isolationLevel, isBlindAppend, operationMetrics, userMetadata, tags, txnId)\n  }\n\n  def apply(\n      version: Option[Long],\n      time: Long,\n      operation: String,\n      inCommitTimestamp: Option[Long],\n      operationParameters: Map[String, String],\n      commandContext: Map[String, String],\n      readVersion: Option[Long],\n      isolationLevel: Option[String],\n      isBlindAppend: Option[Boolean],\n      operationMetrics: Option[Map[String, String]],\n      userMetadata: Option[String],\n      tags: Option[Map[String, String]],\n      txnId: Option[String]): CommitInfo = {\n\n    val getUserName = commandContext.get(\"user\").flatMap {\n      case \"unknown\" => None\n      case other => Option(other)\n    }\n\n    CommitInfo(\n      version,\n      inCommitTimestamp,\n      new Timestamp(time),\n      commandContext.get(\"userId\"),\n      getUserName,\n      operation,\n      operationParameters,\n      JobInfo.fromContext(commandContext),\n      NotebookInfo.fromContext(commandContext),\n      commandContext.get(\"clusterId\"),\n      readVersion,\n      isolationLevel,\n      isBlindAppend,\n      operationMetrics,\n      userMetadata,\n      tags,\n      getEngineInfo,\n      txnId)\n  }\n  // scalastyle:on argcount\n\n  private def getEngineInfo: Option[String] = {\n    Some(s\"Apache-Spark/${org.apache.spark.SPARK_VERSION} Delta-Lake/${io.delta.VERSION}\")\n  }\n\n  /**\n   * Returns the `inCommitTimestamp` of the given `commitInfoOpt` if it is defined.\n   * Throws an exception if `commitInfoOpt` is empty or contains an empty `inCommitTimestamp`.\n   */\n  def getRequiredInCommitTimestamp(commitInfoOpt: Option[CommitInfo], version: String): Long = {\n    val commitInfo = commitInfoOpt.getOrElse {\n      throw DeltaErrors.missingCommitInfo(InCommitTimestampTableFeature.name, version)\n    }\n    commitInfo.inCommitTimestamp.getOrElse {\n      throw DeltaErrors.missingCommitTimestamp(version)\n    }\n  }\n\n  /**\n   * Returns the legacy value of operation parameters after deserialization.\n   * See [[CommitInfoOperationParametersOnly]] and [[JsonMapDeserializer]] for more\n   * details about how operation parameter deserialization was broken before. These\n   * legacy values are the same as the original broken version.\n   */\n  def getLegacyPostDeserializationOperationParameters(\n      operationParameters: Map[String, String]): Map[String, String] = {\n    // Use JsonMapSerializer to serialize the operation parameters\n    val serializedOperationParameters =\n      JsonUtils.toJson(CommitInfoOperationParametersOnly(operationParameters))\n    // Instead of using JsonMapDeserializer,\n    // we can use JsonUtils.fromJson to deserialize the operation parameters\n    JsonUtils.fromJson[CommitInfoOperationParametersOnly](serializedOperationParameters)\n      .operationParameters\n  }\n\n}\n\n/** A trait to represent actions which can only be part of Checkpoint */\nsealed trait CheckpointOnlyAction extends Action\n\n/**\n * An [[Action]] containing the information about a sidecar file.\n *\n * @param path - sidecar path relative to `_delta_log/_sidecar` directory\n * @param sizeInBytes - size in bytes for the sidecar file\n * @param modificationTime - modification time of the sidecar file\n * @param tags - attributes of the sidecar file, defaults to null (which is semantically same as an\n *               empty Map). This is kept null to ensure that the field is not present in the\n *               generated json.\n */\ncase class SidecarFile(\n    path: String,\n    sizeInBytes: Long,\n    modificationTime: Long,\n    tags: Map[String, String] = null)\n  extends CheckpointOnlyAction {\n\n  override def wrap: SingleAction = SingleAction(sidecar = this)\n\n  def toFileStatus(logPath: Path): FileStatus = {\n    val partFilePath = new Path(FileNames.sidecarDirPath(logPath), path)\n    new FileStatus(sizeInBytes, false, 0, 0, modificationTime, partFilePath)\n  }\n}\n\nobject SidecarFile {\n  def apply(fileStatus: SerializableFileStatus): SidecarFile = {\n    SidecarFile(fileStatus.getHadoopPath.getName, fileStatus.length, fileStatus.modificationTime)\n  }\n\n  def apply(fileStatus: FileStatus): SidecarFile = {\n    SidecarFile(fileStatus.getPath.getName, fileStatus.getLen, fileStatus.getModificationTime)\n  }\n}\n\n/**\n * Holds information about the Delta Checkpoint. This action will only be part of checkpoints.\n *\n * @param version version of the checkpoint\n * @param tags    attributes of the checkpoint, defaults to null (which is semantically same as an\n *                empty Map). This is kept null to ensure that the field is not present in the\n *                generated json.\n */\ncase class CheckpointMetadata(\n    version: Long,\n    tags: Map[String, String] = null)\n  extends CheckpointOnlyAction {\n\n  override def wrap: SingleAction = SingleAction(checkpointMetadata = this)\n}\n\n\n/** A serialization helper to create a common action envelope. */\ncase class SingleAction(\n    txn: SetTransaction = null,\n    add: AddFile = null,\n    remove: RemoveFile = null,\n    metaData: Metadata = null,\n    protocol: Protocol = null,\n    cdc: AddCDCFile = null,\n    checkpointMetadata: CheckpointMetadata = null,\n    sidecar: SidecarFile = null,\n    domainMetadata: DomainMetadata = null,\n    commitInfo: CommitInfo = null) {\n\n  def unwrap: Action = {\n    if (add != null) {\n      add\n    } else if (remove != null) {\n      remove\n    } else if (metaData != null) {\n      metaData\n    } else if (txn != null) {\n      txn\n    } else if (protocol != null) {\n      protocol\n    } else if (cdc != null) {\n      cdc\n    } else if (sidecar != null) {\n      sidecar\n    } else if (checkpointMetadata != null) {\n      checkpointMetadata\n    } else if (domainMetadata != null) {\n      domainMetadata\n    } else if (commitInfo != null) {\n      commitInfo\n    } else {\n      null\n    }\n  }\n}\n\nobject SingleAction extends Logging {\n  implicit def encoder: Encoder[SingleAction] =\n    org.apache.spark.sql.delta.implicits.singleActionEncoder\n\n  implicit def addFileEncoder: Encoder[AddFile] =\n    org.apache.spark.sql.delta.implicits.addFileEncoder\n\n  lazy val nullLitForRemoveFile: Column =\n    Column(Literal(null, ScalaReflection.schemaFor[RemoveFile].dataType))\n\n  lazy val nullLitForAddCDCFile: Column =\n    Column(Literal(null, ScalaReflection.schemaFor[AddCDCFile].dataType))\n\n  lazy val nullLitForMetadataAction: Column =\n    Column(Literal(null, ScalaReflection.schemaFor[Metadata].dataType))\n}\n\n/** Serializes Maps containing JSON strings without extra escaping. */\nclass JsonMapSerializer extends JsonSerializer[Map[String, String]] {\n  def serialize(\n      parameters: Map[String, String],\n      jgen: JsonGenerator,\n      provider: SerializerProvider): Unit = {\n    jgen.writeStartObject()\n    parameters.foreach { case (key, value) =>\n      if (value == null) {\n        jgen.writeNullField(key)\n      } else {\n        jgen.writeFieldName(key)\n        // Write value as raw data, since it's already JSON text\n        jgen.writeRawValue(value)\n      }\n    }\n    jgen.writeEndObject()\n  }\n}\n\n/**\n * This is effectively performs an inverse of [[JsonMapSerializer]].\n *\n * The in-memory representation of operation params of any Delta Operation can be\n * a combination of json encoded strings, simple strings, or primitives single-encoded\n * as strings. i.e. the values can be any of \"abc\", \"123\", \"true\", \"1.0\", \"\\\"true\\\"\",\n * \"\\\"1.0\\\"\" or more complex json encoded strings.\n * Due to how [[JsonMapSerializer]] strips one level of encoding for these values\n * during serialization, these can end up being written out in this form:\n * \"123\" -> 123\n * \"true\" -> true\n * \"1.0\" -> 1.0\n * \"\\\"true\\\"\" -> \"true\"\n * \"\\\"1.0\\\"\" -> \"1.0\"\n * Since operationParameters is a Map[String, String], during the deserialization phase, the\n * deserializer intelligently converts primitive types from above to simple strings.\n * i.e.\n * \"123\" -> 123 -> \"123\"\n * \"true\" -> true -> \"true\"\n * \"1.0\" -> 1.0 -> \"1.0\"\n * \"\\\"true\\\"\" -> \"true\" -> \"true\"\n * \"\\\"1.0\\\"\" -> \"1.0\" -> \"1.0\"\n * Since we stripped one level of encoding during serialization, we need to add it back to\n * get closer to the original in-memory representation.\n * i.e.\n * \"123\" -> 123 -> \"123\" -> \"\\\"123\\\"\"\n * \"true\" -> true -> \"true\" -> \"\\\"true\\\"\"\n * \"1.0\" -> 1.0 -> \"1.0\" -> \"\\\"1.0\\\"\"\n * \"\\\"true\\\"\" -> \"true\" -> \"true\" -> \"\\\"true\\\"\"\n * \"\\\"1.0\\\"\" -> \"1.0\" -> \"1.0\" -> \"\\\"1.0\\\"\"\n * Note how values that were single-encoded as strings originally are now double-encoded as\n * strings.\n * (i.e. \"true\" -> true -> \"true\" -> \"\\\"true\\\"\"). This is because the deserializer converted\n * the primitive values to strings as well as retained simple strings as strings. In this process,\n * we lost some information about the original values. To fix this, we first deserialize the\n * values as a java.util.HashMap[String, Any] i.e.:\n * \"123\" -> 123 -> 123 (type: Integer)\n * \"true\" -> true -> true (type: Boolean)\n * \"1.0\" -> 1.0 -> 1.0 (type: Double)\n * \"\\\"true\\\"\" -> \"true\" -> \"true\" (type: String)\n * \"\\\"1.0\\\"\" -> \"1.0\" -> \"1.0\" (type: String)\n * and then JsonEncode them once to get the original in-memory representation. i.e.\n * \"123\" -> 123 -> 123 -> \"123\"\n * \"true\" -> true -> true -> \"true\"\n * \"1.0\" -> 1.0 -> 1.0 -> \"1.0\"\n * \"\\\"true\\\"\" -> \"true\" -> \"true\" -> \"\\\"true\\\"\"\n * \"\\\"1.0\\\"\" -> \"1.0\" -> \"1.0\" -> \"\\\"1.0\\\"\"\n */\nclass JsonMapDeserializer extends JsonDeserializer[Map[String, String]] {\n  def deserialize(jp: JsonParser, ctxt: DeserializationContext): Map[String, String] = {\n    // First read the map as a Map[String, Any]. Then use JsonUtils.toJson to convert\n    // the values to JSON strings.\n    val map = ctxt.readValue(jp, classOf[Map[String, Any]])\n    map.mapValues(JsonUtils.toJson(_)).toMap\n  }\n}\n\n/**\n * This class is only used by [[CommitInfo.getLegacyPostDeserializationOperationParameters]]\n * to regenerate legacy operation parameters.\n * The legacy deserialization of operationParameters goes as follows.\n *   - The in-memory representation of a Delta operation's parameters was a (String -> Any) map.\n *   - When setting the operation parameters in a [[CommitInfo]], the map is transformed into\n *     (String -> JsonEncodedString(Any)).\n *   - A [[CommitInfo]] is serialized with a `JsonMapSerializer`, which wrote the values as raw\n *     strings.\n *   - However, the deserialization process used a default deserializer that was not an inverse of\n *     JsonMapSerializer. The default deserializer automatically decoded JSON strings into their\n *     inferred types, and then cast the values to strings. This meant the output of the\n *     deserialization process was a (String -> String(Any)).\n *   - Note that String() and JsonEncodedString() are not the same. For example, if the original\n *     value was a string \"abc\", then String(\"abc\") is still \"abc\", but JsonEncodedString(\"abc\") is\n *     \"\\\"abc\\\"\". In the new deserializer, we have changed it so that the deserialization process to\n *     recover the input of the serialization process, i.e. (String -> JsonEncodedString(Any)).\n */\ncase class CommitInfoOperationParametersOnly(\n  @JsonSerialize(using = classOf[JsonMapSerializer])\n  operationParameters: Map[String, String]\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/catalog/AbstractDeltaCatalog.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.catalog\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.sql.Timestamp\nimport java.util\nimport java.util.Locale\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.ListMap\nimport scala.collection.mutable\n\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient.UC_TABLE_ID_KEY\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient.UC_TABLE_ID_KEY_OLD\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.skipping.clustering.temp.{ClusterBy, ClusterBySpec}\nimport org.apache.spark.sql.delta.skipping.clustering.temp.{ClusterByTransform => TempClusterByTransform}\nimport org.apache.spark.sql.delta.{ColumnWithDefaultExprUtils, DeltaConfigs, DeltaErrors, DeltaTableUtils}\nimport org.apache.spark.sql.delta.{DeltaOptions, IdentityColumn}\nimport org.apache.spark.sql.delta.DeltaTableIdentifier.gluePermissionError\nimport org.apache.spark.sql.delta.commands._\nimport org.apache.spark.sql.delta.constraints.{AddConstraint, DropConstraint}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.redirect.RedirectFeature\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.serverSidePlanning.ServerSidePlannedTable\nimport org.apache.spark.sql.delta.sources.{DeltaDataSource, DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\nimport org.apache.spark.sql.delta.tablefeatures.DropFeature\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.PartitionUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{AnalysisException, DataFrame, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{NoSuchDatabaseException, NoSuchNamespaceException, NoSuchTableException, UnresolvedAttribute, UnresolvedFieldName, UnresolvedFieldPosition}\nimport org.apache.spark.sql.catalyst.catalog.{BucketSpec, CatalogTable, CatalogTableType, CatalogUtils, SessionCatalog}\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, QualifiedColType, QualifiedColTypeShims, SyncIdentity}\nimport org.apache.spark.sql.connector.catalog.{DelegatingCatalogExtension, Identifier, StagedTable, StagingTableCatalog, SupportsWrite, Table, TableCapability, TableCatalog, TableChange, V1Table}\nimport org.apache.spark.sql.connector.catalog.TableCapability._\nimport org.apache.spark.sql.connector.catalog.TableChange._\nimport org.apache.spark.sql.connector.expressions.{FieldReference, IdentityTransform, Literal, NamedReference, Transform}\nimport org.apache.spark.sql.connector.write.{LogicalWriteInfo, SupportsTruncate, V1Write, WriteBuilder}\nimport org.apache.spark.sql.execution.datasources.{DataSource, PartitioningUtils}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.sources.InsertableRelation\nimport org.apache.spark.sql.types.{IntegerType, StructField, StructType}\n\n\n/**\n * V1 legacy implementation. Use [[org.apache.spark.sql.delta.catalog.DeltaCatalog]] instead.\n * See spark-unified/src/main/java/org/apache/spark/sql/delta/catalog/DeltaCatalog.java\n */\nclass DeltaCatalogV1 extends AbstractDeltaCatalog\n\n/**\n * Base class for Dsv2 catalog implementation, it contains all dsv1 based connector logic.\n * Introduced for compatibility purpose in the implementation of dsv2 based connector\n */\nclass AbstractDeltaCatalog extends DelegatingCatalogExtension\n  with StagingTableCatalog\n  with SupportsPathIdentifier\n  with DeltaLogging {\n\n\n  val spark = SparkSession.active\n\n  private lazy val isUnityCatalog: Boolean = {\n    val delegateField = classOf[DelegatingCatalogExtension].getDeclaredField(\"delegate\")\n    delegateField.setAccessible(true)\n    delegateField.get(this).getClass.getCanonicalName.startsWith(\"io.unitycatalog.\")\n  }\n\n  /**\n   * Creates a Delta table\n   *\n   * @param ident The identifier of the table\n   * @param schema The schema of the table\n   * @param partitions The partition transforms for the table\n   * @param allTableProperties The table properties that configure the behavior of the table or\n   *                           provide information about the table\n   * @param writeOptions Options specific to the write during table creation or replacement\n   * @param sourceQuery A query if this CREATE request came from a CTAS or RTAS\n   * @param operation The specific table creation mode, whether this is a Create/Replace/Create or\n   *                  Replace\n   */\n  private def createDeltaTable(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      allTableProperties: util.Map[String, String],\n      writeOptions: Map[String, String],\n      sourceQuery: Option[DataFrame],\n      operation: TableCreationModes.CreationMode\n    ): Table = recordFrameProfile(\n        \"DeltaCatalog\", \"createDeltaTable\") {\n    // These two keys are tableProperties in data source v2 but not in v1, so we have to filter\n    // them out. Otherwise property consistency checks will fail.\n    val tableProperties = allTableProperties.asScala.filterKeys {\n      case TableCatalog.PROP_LOCATION => false\n      case TableCatalog.PROP_PROVIDER => false\n      case TableCatalog.PROP_COMMENT => false\n      case TableCatalog.PROP_OWNER => false\n      case TableCatalog.PROP_EXTERNAL => false\n      case \"path\" => false\n      case \"option.path\" => false\n      case _ => true\n    }.toMap\n    val (partitionColumns, maybeBucketSpec, maybeClusterBySpec) = convertTransforms(partitions)\n    validateClusterBySpec(maybeClusterBySpec, schema)\n    // Check partition columns are not IDENTITY columns.\n    partitionColumns.foreach { colName =>\n      if (ColumnWithDefaultExprUtils.isIdentityColumn(schema(colName))) {\n        throw DeltaErrors.identityColumnPartitionNotSupported(colName)\n      }\n    }\n    var newSchema = schema\n    var newPartitionColumns = partitionColumns\n    var newBucketSpec = maybeBucketSpec\n    val conf = spark.sessionState.conf\n    allTableProperties.asScala\n      .get(DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.key)\n      .foreach(StatisticsCollection.validateDeltaStatsColumns(schema, partitionColumns, _))\n    val isByPath = isPathIdentifier(ident)\n    if (isByPath && !conf.getConf(DeltaSQLConf.DELTA_LEGACY_ALLOW_AMBIGUOUS_PATHS)\n      && allTableProperties.containsKey(\"location\")\n      // The location property can be qualified and different from the path in the identifier, so\n      // we check `endsWith` here.\n      && Option(allTableProperties.get(\"location\")).exists(!_.endsWith(ident.name()))\n    ) {\n      throw DeltaErrors.ambiguousPathsInCreateTableException(\n        ident.name(), allTableProperties.get(\"location\"))\n    }\n    val location = if (isByPath) {\n      Option(ident.name())\n    } else {\n      Option(allTableProperties.get(\"location\"))\n    }\n    val id = {\n      // Preserve the catalog name in the V1 identifier for Unity Catalog because this `id`\n      // becomes `tableDesc.identifier` for the rest of the create/replace flow. Downstream\n      // catalog-update logic reads `table.identifier.catalog`; without it, the table looks like\n      // an unqualified V1 table and catalog updates route through the current/session catalog\n      // instead of the delegated Unity Catalog entry.\n      val base = TableIdentifier(ident.name(), ident.namespace().lastOption)\n      if (isUnityCatalog) {\n        base.copy(catalog = Some(name())) // `name()` here is the catalog name.\n      } else {\n        base\n      }\n    }\n    var locUriOpt = location.map(CatalogUtils.stringToURI)\n    val existingTableOpt = getExistingTableIfExists(id, Some(ident), operation)\n    // PROP_IS_MANAGED_LOCATION indicates that the table location is not user-specified but\n    // system-generated. The table should be created as managed table in this case.\n    val isManagedLocation = Option(allTableProperties.get(TableCatalog.PROP_IS_MANAGED_LOCATION))\n      .exists(_.equalsIgnoreCase(\"true\"))\n    // Note: Spark generates the table location for managed tables in\n    // `DeltaCatalog#delegate#createTable`, so `isManagedLocation` should never be true if\n    // Unity Catalog is not involved. For safety we also check `isUnityCatalog` here.\n    val respectManagedLoc = isUnityCatalog || DeltaUtils.isTesting\n    val tableType = if (location.isEmpty || (isManagedLocation && respectManagedLoc)) {\n      CatalogTableType.MANAGED\n    } else {\n      CatalogTableType.EXTERNAL\n    }\n    val loc = locUriOpt\n      .orElse(existingTableOpt.flatMap(_.storage.locationUri))\n      .getOrElse(spark.sessionState.catalog.defaultTablePath(id))\n    val storage = DataSource.buildStorageFormatFromOptions(writeOptions)\n      .copy(locationUri = Option(loc))\n    val commentOpt = Option(allTableProperties.get(\"comment\"))\n\n\n    var tableDesc = new CatalogTable(\n      identifier = id,\n      tableType = tableType,\n      storage = storage,\n      schema = newSchema,\n      provider = Some(DeltaSourceUtils.ALT_NAME),\n      partitionColumnNames = newPartitionColumns,\n      bucketSpec = newBucketSpec,\n      properties = tableProperties,\n      comment = commentOpt\n    )\n\n    val withDb =\n      verifyTableAndSolidify(\n        tableDesc,\n        None,\n        maybeClusterBySpec\n      )\n\n    val writer = sourceQuery.map { df =>\n      // For safety, only extract the file system options here, to create deltaLog.\n      val fileSystemOptions = writeOptions.filter { case (k, _) =>\n        DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n      }\n      WriteIntoDelta(\n        DeltaUtils.getDeltaLogFromTableOrPath(spark, existingTableOpt,\n          new Path(loc), fileSystemOptions),\n        operation.mode,\n        new DeltaOptions(withDb.storage.properties, spark.sessionState.conf),\n        withDb.partitionColumnNames,\n        withDb.properties ++ commentOpt.map(\"comment\" -> _),\n        df,\n        Some(tableDesc),\n        schemaInCatalog = if (newSchema != schema) Some(newSchema) else None)\n    }\n\n    CreateDeltaTableCommand(\n      withDb,\n      existingTableOpt,\n      operation.mode,\n      writer,\n      operation,\n      tableByPath = isByPath,\n      allowCatalogManaged = isUnityCatalog && tableType == CatalogTableType.MANAGED,\n      // We should invoke the Spark catalog plugin API to create the table, to\n      // respect third party catalogs.\n      // TODO: Spark `V2SessionCatalog` mistakenly treat tables with location as EXTERNAL table.\n      //       Before this bug is fixed, we should only call the catalog plugin API to create tables\n      //       if UC is enabled to replace `V2SessionCatalog`.\n      createTableFunc = Option.when(isUnityCatalog) {\n        v1Table => {\n          val t = V1Table(v1Table)\n          super.createTable(ident, t.columns(), t.partitioning, t.properties)\n        }\n      }).run(spark)\n\n    loadTable(ident)\n  }\n\n  override def loadTable(ident: Identifier): Table = recordFrameProfile(\n      \"DeltaCatalog\", \"loadTable\") {\n    try {\n      val table = super.loadTable(ident)\n\n      ServerSidePlannedTable.tryCreate(spark, ident, table, isUnityCatalog).foreach { sspt =>\n        return sspt\n      }\n\n      table match {\n        case v1: V1Table if DeltaTableUtils.isDeltaTable(v1.catalogTable) =>\n          loadCatalogTable(ident, v1.catalogTable)\n        case o => o\n      }\n    } catch {\n      case e @ (\n        _: NoSuchDatabaseException | _: NoSuchNamespaceException | _: NoSuchTableException) =>\n          if (isPathIdentifier(ident)) {\n            loadPathTable(ident)\n          } else if (isIcebergPathIdentifier(ident)) {\n            newIcebergPathTable(ident)\n          } else {\n            throw e\n          }\n      case e: AnalysisException if gluePermissionError(e) && isPathIdentifier(ident) =>\n        logWarning(log\"Received an access denied error from Glue. Assuming this \" +\n          log\"identifier (${MDC(DeltaLogKeys.TABLE_NAME, ident)}) is path based.\", e)\n        loadPathTable(ident)\n    }\n  }\n\n  override def loadTable(ident: Identifier, timestamp: Long): Table = {\n    loadTableWithTimeTravel(ident, version = None, Some(timestamp))\n  }\n\n  override def loadTable(ident: Identifier, version: String): Table = {\n    loadTableWithTimeTravel(ident, Some(version), timestamp = None)\n  }\n\n  /**\n   * Helper method which loads a Delta table with given time travel parameters.\n   * Exactly one of the timetravel parameters (version or timestamp) must be present.\n   *\n   * @param version The table version to load\n   * @param timestamp The timestamp for the table to load, in microseconds\n   */\n  private def loadTableWithTimeTravel(\n      ident: Identifier,\n      version: Option[String],\n      timestamp: Option[Long]): Table = {\n    assert(version.isEmpty ^ timestamp.isEmpty,\n      \"Either the version or timestamp should be provided for time travel\")\n    val table = loadTable(ident)\n    table match {\n      case deltaTable: DeltaTableV2 =>\n        val ttOpts = Map(DeltaDataSource.TIME_TRAVEL_SOURCE_KEY -> \"SQL\") ++\n          (if (version.isDefined) {\n            Map(DeltaDataSource.TIME_TRAVEL_VERSION_KEY -> version.get)\n          } else {\n            val timestampMs = timestamp.get / 1000\n            Map(DeltaDataSource.TIME_TRAVEL_TIMESTAMP_KEY -> new Timestamp(timestampMs).toString)\n          })\n\n        deltaTable.withOptions(ttOpts)\n      // punt this problem up to the parent\n      case _ if version.isDefined => super.loadTable(ident, version.get)\n      case _ if timestamp.isDefined => super.loadTable(ident, timestamp.get)\n    }\n  }\n\n  // Perform checks on ClusterBySpec.\n  def validateClusterBySpec(\n      maybeClusterBySpec: Option[ClusterBySpec], schema: StructType): Unit = {\n    maybeClusterBySpec.foreach { clusterBy =>\n      // Check if the specified cluster by columns exists in the table.\n      val resolver = spark.sessionState.conf.resolver\n      clusterBy.columnNames.foreach { column =>\n        // This is the same check as in rules.scala, to keep the behaviour consistent.\n        SchemaUtils.findColumnPosition(column.fieldNames(), schema, resolver)\n      }\n      // Check that columns are not duplicated in the cluster by statement.\n      PartitionUtils.checkColumnNameDuplication(\n        clusterBy.columnNames.map(_.toString), \"in CLUSTER BY\", resolver)\n      // Check number of clustering columns is within allowed range.\n      ClusteredTableUtils.validateNumClusteringColumns(\n        clusterBy.columnNames.map(_.fieldNames.toSeq))\n    }\n  }\n\n\n  /**\n   * Loads a Delta table that is registered in the catalog.\n   *\n   * @param ident The identifier of the table in the catalog.\n   * @param catalogTable The catalog table metadata containing table properties and location.\n   * @return A DeltaTableV2 instance with catalog metadata attached.\n   */\n  protected def loadCatalogTable(ident: Identifier, catalogTable: CatalogTable): Table = {\n    DeltaTableV2(\n      spark,\n      new Path(catalogTable.location),\n      catalogTable = Some(catalogTable),\n      tableIdentifier = Some(ident.toString))\n  }\n\n  /**\n   * Loads a Delta table directly from a path.\n   * This is used for path-based table access where the identifier name is the table path.\n   *\n   * @param ident The identifier whose name contains the path to the Delta table.\n   * @return A DeltaTableV2 instance loaded from the specified path.\n   */\n  protected def loadPathTable(ident: Identifier): Table = {\n    DeltaTableV2(spark, new Path(ident.name()))\n  }\n\n  private def getProvider(properties: util.Map[String, String]): String = {\n    Option(properties.get(\"provider\"))\n      .getOrElse(spark.sessionState.conf.getConf(SQLConf.DEFAULT_DATA_SOURCE_NAME))\n  }\n\n  private def createCatalogTable(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: util.Map[String, String]\n  ): Table = {\n      super.createTable(ident, schema, partitions, properties)\n  }\n\n\n  override def createTable(\n      ident: Identifier,\n      columns: Array[org.apache.spark.sql.connector.catalog.Column],\n      partitions: Array[Transform],\n      properties: util.Map[String, String]): Table = {\n    createTable(\n      ident,\n      org.apache.spark.sql.connector.catalog.CatalogV2Util.v2ColumnsToStructType(columns),\n      partitions,\n      properties)\n  }\n\n  override def createTable(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: util.Map[String, String]) : Table =\n    recordFrameProfile(\"DeltaCatalog\", \"createTable\") {\n      if (DeltaSourceUtils.isDeltaDataSourceName(getProvider(properties))) {\n        // TODO: we should extract write options from table properties for all the cases. We\n        //       can remove the UC check when we have confidence.\n        val isUC = isUnityCatalog || properties.containsKey(\"test.simulateUC\")\n        val (props, writeOptions) = if (isUC) {\n          val (props, writeOptions) = getTablePropsAndWriteOptions(properties)\n          expandTableProps(props, writeOptions, spark.sessionState.conf)\n          props.remove(\"test.simulateUC\")\n          translateUCTableIdProperty(props)\n          (props, writeOptions)\n        } else {\n          (properties, Map.empty[String, String])\n        }\n\n        createDeltaTable(\n          ident,\n          schema,\n          partitions,\n          props,\n          writeOptions,\n          sourceQuery = None,\n          TableCreationModes.Create\n        )\n      } else {\n        createCatalogTable(ident, schema, partitions, properties\n        )\n      }\n    }\n\n  override def stageReplace(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: util.Map[String, String]): StagedTable =\n    recordFrameProfile(\"DeltaCatalog\", \"stageReplace\") {\n      if (DeltaSourceUtils.isDeltaDataSourceName(getProvider(properties))) {\n        new StagedDeltaTableV2(\n          ident,\n          schema,\n          partitions,\n          properties,\n          TableCreationModes.Replace\n        )\n      } else {\n        super.dropTable(ident)\n        val table = createCatalogTable(ident, schema, partitions, properties\n        )\n        BestEffortStagedTable(ident, table, this)\n      }\n    }\n\n  override def stageCreateOrReplace(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: util.Map[String, String]): StagedTable =\n    recordFrameProfile(\"DeltaCatalog\", \"stageCreateOrReplace\") {\n      if (DeltaSourceUtils.isDeltaDataSourceName(getProvider(properties))) {\n        new StagedDeltaTableV2(\n          ident,\n          schema,\n          partitions,\n          properties,\n          TableCreationModes.CreateOrReplace\n        )\n      } else {\n        try super.dropTable(ident)\n        catch {\n          case _: NoSuchDatabaseException => // this is fine\n          case _: NoSuchTableException => // this is fine\n        }\n        val table = createCatalogTable(ident, schema, partitions, properties\n        )\n        BestEffortStagedTable(ident, table, this)\n      }\n    }\n\n  override def stageCreate(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: util.Map[String, String]): StagedTable =\n    recordFrameProfile(\"DeltaCatalog\", \"stageCreate\") {\n      if (DeltaSourceUtils.isDeltaDataSourceName(getProvider(properties))) {\n        new StagedDeltaTableV2(\n          ident,\n          schema,\n          partitions,\n          properties,\n          TableCreationModes.Create\n        )\n      } else {\n        val table = createCatalogTable(ident, schema, partitions, properties\n        )\n        BestEffortStagedTable(ident, table, this)\n      }\n    }\n\n  // Copy of V2SessionCatalog.convertTransforms, which is private.\n  private def convertTransforms(\n      partitions: Seq[Transform]): (Seq[String], Option[BucketSpec], Option[ClusterBySpec]) = {\n    val identityCols = new mutable.ArrayBuffer[String]\n    var bucketSpec = Option.empty[BucketSpec]\n    var clusterBySpec = Option.empty[ClusterBySpec]\n\n    partitions.map {\n      case IdentityTransform(FieldReference(Seq(col))) =>\n        identityCols += col\n\n      case BucketTransform(numBuckets, bucketCols, sortCols) =>\n        bucketSpec = Some(BucketSpec(\n          numBuckets, bucketCols.map(_.fieldNames.head), sortCols.map(_.fieldNames.head)))\n      case TempClusterByTransform(columnNames) =>\n        if (clusterBySpec.nonEmpty) {\n          // Parser guarantees that it only passes down one TempClusterByTransform.\n          throw SparkException.internalError(\"Cannot have multiple cluster by transforms.\")\n        }\n        clusterBySpec = Some(ClusterBySpec(columnNames))\n\n      case transform =>\n        throw DeltaErrors.operationNotSupportedException(s\"Partitioning by expressions\")\n    }\n    // Parser guarantees that partition and cluster by can't both exist.\n    assert(!(identityCols.toSeq.nonEmpty && clusterBySpec.nonEmpty))\n    // Parser guarantees that bucketing and cluster by can't both exist.\n    assert(!(bucketSpec.nonEmpty && clusterBySpec.nonEmpty))\n\n    (identityCols.toSeq, bucketSpec, clusterBySpec)\n  }\n\n  /** Performs checks on the parameters provided for table creation for a Delta table. */\n  def verifyTableAndSolidify(\n      tableDesc: CatalogTable,\n      query: Option[LogicalPlan],\n      maybeClusterBySpec: Option[ClusterBySpec] = None): CatalogTable = {\n    if (tableDesc.bucketSpec.isDefined) {\n      throw DeltaErrors.operationNotSupportedException(\"Bucketing\", tableDesc.identifier)\n    }\n\n    val schema = query.map { plan =>\n      assert(tableDesc.schema.isEmpty, \"Can't specify table schema in CTAS.\")\n      plan.schema.asNullable\n    }.getOrElse(tableDesc.schema)\n\n    PartitioningUtils.validatePartitionColumn(\n      schema,\n      tableDesc.partitionColumnNames,\n      caseSensitive = false) // Delta is case insensitive\n\n    var validatedConfigurations =\n      DeltaConfigs.validateConfigurations(tableDesc.properties)\n    ClusteredTableUtils.validateExistingTableFeatureProperties(validatedConfigurations)\n    // Add needed configs for Clustered table. Note that [[PROP_CLUSTERING_COLUMNS]] can only\n    // be added after [[DeltaConfigs.validateConfigurations]] to avoid non-user configurable check\n    // failure.\n    if (maybeClusterBySpec.nonEmpty) {\n      validatedConfigurations =\n        validatedConfigurations ++\n          ClusteredTableUtils.getClusteringColumnsAsProperty(maybeClusterBySpec) ++\n          ClusteredTableUtils.getTableFeatureProperties(validatedConfigurations)\n    }\n\n    val db = tableDesc.identifier.database.getOrElse(catalog.getCurrentDatabase)\n    val tableIdentWithDB = tableDesc.identifier.copy(database = Some(db))\n    tableDesc.copy(\n      identifier = tableIdentWithDB,\n      schema = schema,\n      properties = validatedConfigurations)\n  }\n\n  /**\n   * Checks if a Delta table already exists for the provided identifier.\n   *\n   * This first goes through the legacy V1 SessionCatalog/HMS lookup using [[TableIdentifier]].\n   * For operations that target an existing Unity Catalog table, it then falls back to the\n   * delegated V2 catalog plugin lookup (for example Unity Catalog) when the V1 lookup does not\n   * surface the table entry.\n   */\n  def getExistingTableIfExists(\n      table: TableIdentifier,\n      identOpt: Option[Identifier],\n      operation: TableCreationModes.CreationMode)\n      : Option[CatalogTable] = {\n    // If this is a path identifier, we cannot return an existing CatalogTable. The Create command\n    // will check the file system itself\n    if (isPathIdentifier(table)) return None\n    val tableExists = catalog.tableExists(table)\n    if (tableExists) {\n      val oldTable = catalog.getTableMetadata(table)\n      if (oldTable.tableType == CatalogTableType.VIEW) {\n        throw DeltaErrors.cannotWriteIntoView(table)\n      }\n      if (!DeltaSourceUtils.isDeltaTable(oldTable.provider)) {\n        throw DeltaErrors.notADeltaTable(table.table)\n      }\n      Some(oldTable)\n    } else if (operation != TableCreationModes.Create) {\n      identOpt match {\n        case Some(ident) => getExistingTableFromDelegatedCatalog(ident)\n        case None =>\n          logDebug(log\"Delegated catalog lookup skipped because no V2 identifier was provided \" +\n            log\"for ${MDC(DeltaLogKeys.TABLE_NAME, table)} during ${MDC(DeltaLogKeys.OPERATION,\n              operation.toString)}.\")\n          None\n      }\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Returns an existing Delta table by querying the delegated catalog for Unity Catalog paths\n   * where the V1 SessionCatalog lookup does not surface the existing table entry.\n   *\n   * [[getExistingTableIfExists]] first checks the V1 catalog using a [[TableIdentifier]]. For\n   * Unity Catalog, some staged create/replace paths need a delegated V2 catalog lookup on the\n   * original [[Identifier]] to recover the existing table metadata.\n   *\n   * Even though this goes through the delegated V2 catalog plugin, Spark can still surface the\n   * result as a [[V1Table]] wrapper when the delegated catalog is exposing V1-backed table\n   * metadata. In that case we unwrap the embedded [[CatalogTable]] and continue on the V1 Delta\n   * path.\n   */\n  private def getExistingTableFromDelegatedCatalog(ident: Identifier): Option[CatalogTable] = {\n    if (isUnityCatalog) {\n      try {\n        super.loadTable(ident) match {\n          case v1: V1Table if DeltaTableUtils.isDeltaTable(v1.catalogTable) =>\n            Some(v1.catalogTable)\n          case _ =>\n            logDebug(log\"Delegated catalog lookup for ${MDC(DeltaLogKeys.TABLE_NAME, ident)} \" +\n              log\"did not return a Delta table.\")\n            None\n        }\n      } catch {\n        case _: NoSuchTableException =>\n          logDebug(log\"Delegated catalog lookup did not find an existing table for \" +\n            log\"${MDC(DeltaLogKeys.TABLE_NAME, ident)}.\")\n          None\n      }\n    } else {\n      None\n    }\n  }\n\n  private def getTablePropsAndWriteOptions(properties: util.Map[String, String])\n  : (util.Map[String, String], Map[String, String]) = {\n    val props = new util.HashMap[String, String]()\n    // Options passed in through the SQL API will show up both with an \"option.\" prefix and\n    // without in Spark 3.1, so we need to remove those from the properties\n    val optionsThroughProperties = properties.asScala.collect {\n      case (k, _) if k.startsWith(TableCatalog.OPTION_PREFIX) =>\n        k.stripPrefix(TableCatalog.OPTION_PREFIX)\n    }.toSet\n    val writeOptions = new util.HashMap[String, String]()\n    properties.asScala.foreach { case (k, v) =>\n      if (!k.startsWith(TableCatalog.OPTION_PREFIX) && !optionsThroughProperties.contains(k)) {\n        // Add to properties\n        props.put(k, v)\n      } else if (optionsThroughProperties.contains(k)) {\n        writeOptions.put(k, v)\n      }\n    }\n    (props, writeOptions.asScala.toMap)\n  }\n\n  private def expandTableProps(\n      props: util.Map[String, String],\n      options: Map[String, String],\n      conf: SQLConf): Unit = {\n    if (conf.getConf(DeltaSQLConf.DELTA_LEGACY_STORE_WRITER_OPTIONS_AS_PROPS)) {\n      // Legacy behavior\n      options.foreach { case (k, v) => props.put(k, v) }\n    } else {\n      options.foreach { case (k, v) =>\n        // Continue putting in Delta prefixed options to avoid breaking workloads\n        if (k.toLowerCase(Locale.ROOT).startsWith(\"delta.\")) {\n          props.put(k, v)\n        }\n      }\n    }\n  }\n\n  /**\n   * Normalizes the deprecated UC table ID property key to the canonical key.\n   *\n   * This is temporary compatibility for callers that still send the old key during the rename\n   * transition. If both keys are present, we drop the old one and keep the canonical key only.\n   * TODO(issue #6296): remove once all callers stop sending the deprecated key.\n   */\n  private def translateUCTableIdProperty(props: util.Map[String, String]): Unit = {\n    val oldTableIdProperty = Option(props.remove(UC_TABLE_ID_KEY_OLD))\n    oldTableIdProperty.foreach(props.putIfAbsent(UC_TABLE_ID_KEY, _))\n  }\n\n  /**\n   * A staged delta table, which creates a HiveMetaStore entry and appends data if this was a\n   * CTAS/RTAS command. We have a ugly way of using this API right now, but it's the best way to\n   * maintain old behavior compatibility between Databricks Runtime and OSS Delta Lake.\n   */\n  private class StagedDeltaTableV2(\n      ident: Identifier,\n      override val schema: StructType,\n      val partitions: Array[Transform],\n      override val properties: util.Map[String, String],\n      operation: TableCreationModes.CreationMode\n    ) extends StagedTable with SupportsWrite {\n\n    private var asSelectQuery: Option[DataFrame] = None\n    private var writeOptions: Map[String, String] = Map.empty\n\n    override def partitioning(): Array[Transform] = partitions\n\n    override def commitStagedChanges(): Unit = recordFrameProfile(\n        \"DeltaCatalog\", \"commitStagedChanges\") {\n      val conf = spark.sessionState.conf\n      val (props, sqlWriteOptions) = getTablePropsAndWriteOptions(properties)\n      if (writeOptions.isEmpty && sqlWriteOptions.nonEmpty) {\n        writeOptions = sqlWriteOptions\n      }\n      expandTableProps(props, writeOptions, conf)\n      if (isUnityCatalog) {\n        // Unity Catalog callers may still send the deprecated `ucTableId` property key.\n        // Normalize it here to the canonical `io.unitycatalog.tableId` key before create/replace.\n        translateUCTableIdProperty(props)\n      }\n      createDeltaTable(\n        ident,\n        schema,\n        partitions,\n        props,\n        writeOptions,\n        asSelectQuery,\n        operation\n      )\n    }\n\n    override def name(): String = ident.name()\n\n    override def abortStagedChanges(): Unit = {}\n\n    override def capabilities(): util.Set[TableCapability] = {\n      Set(V1_BATCH_WRITE, TRUNCATE).asJava\n    }\n\n    override def newWriteBuilder(info: LogicalWriteInfo): WriteBuilder = {\n      writeOptions = info.options.asCaseSensitiveMap().asScala.toMap\n      new DeltaV1WriteBuilder\n    }\n\n    /*\n     * WriteBuilder for creating a Delta table.\n     */\n    private class DeltaV1WriteBuilder extends WriteBuilder with SupportsTruncate {\n      override def truncate(): this.type = this\n      override def build(): V1Write = new V1Write {\n        override def toInsertableRelation(): InsertableRelation = {\n          new InsertableRelation {\n            override def insert(data: DataFrame, overwrite: Boolean): Unit = {\n              asSelectQuery = Option(data)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  override def alterTable(ident: Identifier, changes: TableChange*): Table = recordFrameProfile(\n      \"DeltaCatalog\", \"alterTable\") {\n    // We group the table changes by their type, since Delta applies each in a separate action.\n    // We also must define an artificial type for SetLocation, since data source V2 considers\n    // location just another property but it's special in catalog tables.\n    class SetLocation {}\n    val grouped = ListMap(changes.groupBy {\n      case s: SetProperty if s.property() == \"location\" => classOf[SetLocation]\n      case c => c.getClass\n    }.toSeq.sortBy {\n      // force SetProperty first to handle if other TableChange requires table feature enabled\n      case (cls, _) if cls == classOf[SetProperty] => 0\n      case _ => 1\n    }: _*)\n    // Determines whether this DDL SET or UNSET the table redirect property. If it is, the table\n    // redirect feature should be disabled to ensure the DDL can be applied onto the source or\n    // destination table properly.\n    val isUpdateTableRedirectDDL = grouped.map {\n      case (t, s: Seq[RemoveProperty]) if t == classOf[RemoveProperty] =>\n        s.map { prop => prop.property() }.exists(RedirectFeature.isRedirectProperty)\n      case (t, s: Seq[SetProperty]) if t == classOf[SetProperty] =>\n        RedirectFeature.hasRedirectConfig(s.map(prop => prop.property() -> prop.value()).toMap)\n      case (_, _) => false\n    }.toSeq.exists(a => a)\n    RedirectFeature.withUpdateTableRedirectDDL(isUpdateTableRedirectDDL) {\n    val table = loadTable(ident) match {\n      case deltaTable: DeltaTableV2 => deltaTable\n      case _ if changes.exists(_.isInstanceOf[ClusterBy]) =>\n        throw DeltaErrors.alterClusterByNotOnDeltaTableException()\n      case _ if changes.exists(_.isInstanceOf[SyncIdentity]) =>\n        throw DeltaErrors.identityColumnAlterNonDeltaFormatError()\n      case _ => return super.alterTable(ident, changes: _*)\n    }\n\n    val columnUpdates = new mutable.HashMap[Seq[String], DeltaChangeColumnSpec]()\n    val isReplaceColumnsCommand = grouped.get(classOf[DeleteColumn]) match {\n      case Some(deletes) if grouped.contains(classOf[AddColumn]) =>\n        // Convert to Seq so that contains method works\n        val deleteSet = deletes.asInstanceOf[Seq[DeleteColumn]].map(_.fieldNames().toSeq).toSet\n        // Ensure that all the table top level columns are being deleted\n        table.schema().fieldNames.forall(f => deleteSet.contains(Seq(f)))\n      case _ =>\n        false\n    }\n\n    if (isReplaceColumnsCommand &&\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_REPLACE_COLUMNS_SAFE)) {\n      // The new schema is essentially the AddColumn operators\n      val tableToUpdate = table\n      val colsToAdd = grouped(classOf[AddColumn]).asInstanceOf[Seq[AddColumn]]\n      val structFields = colsToAdd.map { col =>\n        assert(\n          col.fieldNames().length == 1, \"We don't expect replace to provide nested column adds\")\n        var field = StructField(col.fieldNames().head, col.dataType, col.isNullable)\n        Option(col.comment()).foreach { comment =>\n          field = field.withComment(comment)\n        }\n        Option(col.defaultValue()).foreach { defValue =>\n          field = field.withCurrentDefaultValue(defValue.getSql)\n        }\n        field\n      }\n      AlterTableReplaceColumnsDeltaCommand(tableToUpdate, structFields).run(spark)\n      return loadTable(ident)\n    }\n\n    grouped.foreach {\n      case (t, newColumns) if t == classOf[AddColumn] =>\n        val tableToUpdate = table\n        AlterTableAddColumnsDeltaCommand(\n          tableToUpdate,\n          newColumns.asInstanceOf[Seq[AddColumn]].map { col =>\n            // Convert V2 `AddColumn` to V1 `QualifiedColType` as `AlterTableAddColumnsDeltaCommand`\n            // is a V1 command.\n            val name = col.fieldNames()\n            val path = if (name.length > 1) Some(UnresolvedFieldName(name.init)) else None\n            QualifiedColType(\n              path,\n              name.last,\n              col.dataType(),\n              col.isNullable,\n              Option(col.comment()),\n              Option(col.position()).map(UnresolvedFieldPosition),\n              QualifiedColTypeShims.getDefaultValueArgFromAddColumn(col)\n            )\n          }).run(spark)\n\n      case (t, deleteColumns) if t == classOf[DeleteColumn] =>\n        AlterTableDropColumnsDeltaCommand(\n          table, deleteColumns.asInstanceOf[Seq[DeleteColumn]].map(_.fieldNames().toSeq)).run(spark)\n\n      case (t, newProperties) if t == classOf[SetProperty] =>\n        AlterTableSetPropertiesDeltaCommand(\n          table,\n          DeltaConfigs.validateConfigurations(\n            newProperties.asInstanceOf[Seq[SetProperty]].map { prop =>\n              prop.property() -> prop.value()\n            }.toMap)\n        ).run(spark)\n\n      case (t, oldProperties) if t == classOf[RemoveProperty] =>\n        AlterTableUnsetPropertiesDeltaCommand(\n          table,\n          oldProperties.asInstanceOf[Seq[RemoveProperty]].map(_.property()),\n          // Data source V2 REMOVE PROPERTY is always IF EXISTS.\n          ifExists = true).run(spark)\n\n      case (t, columnChanges) if classOf[ColumnChange].isAssignableFrom(t) =>\n        // TODO: Theoretically we should be able to fetch the snapshot from a txn.\n        val schema = table.initialSnapshot.schema\n        def getColumn(fieldNames: Seq[String])\n            : DeltaChangeColumnSpec = {\n          columnUpdates.getOrElseUpdate(fieldNames, {\n            val colName = UnresolvedAttribute(fieldNames).name\n            val fieldOpt = schema.findNestedField(fieldNames, includeCollections = true,\n              spark.sessionState.conf.resolver)\n              .map(_._2)\n            val field = fieldOpt.getOrElse {\n              throw DeltaErrors.nonExistentColumnInSchema(colName, schema.treeString)\n            }\n            DeltaChangeColumnSpec(\n              fieldNames.init,\n              fieldNames.last,\n              field,\n              colPosition = None,\n              syncIdentity = false)\n          })\n        }\n\n        // Any ColumnChange not explicitly on the allowlist is blocked from making changes on\n        // Identity Columns\n        val disallowedColumnChangesOnIdentityColumns = columnChanges.filterNot {\n            case _: UpdateColumnComment | _: UpdateColumnPosition | _: RenameColumn\n                 | _: SyncIdentity => true\n            case _ => false\n        }\n        disallowedColumnChangesOnIdentityColumns.foreach {\n          case change: ColumnChange =>\n            val field = change.fieldNames()\n            val spec = getColumn(field)\n            if (ColumnWithDefaultExprUtils.isIdentityColumn(spec.newColumn)) {\n              throw DeltaErrors.identityColumnAlterColumnNotSupported()\n            }\n        }\n\n        columnChanges.foreach {\n          case comment: UpdateColumnComment =>\n            val field = comment.fieldNames()\n            val spec = getColumn(field)\n            columnUpdates(field) = spec.copy(\n              newColumn = spec.newColumn.withComment(comment.newComment()))\n\n          case dataType: UpdateColumnType =>\n            val field = dataType.fieldNames()\n            val spec = getColumn(field)\n            val newField = SchemaUtils.setFieldDataTypeCharVarcharSafe(\n              spec.newColumn, dataType.newDataType())\n            columnUpdates(field) = spec.copy(newColumn = newField)\n\n          case position: UpdateColumnPosition =>\n            val field = position.fieldNames()\n            val spec = getColumn(field)\n            columnUpdates(field) = spec.copy(colPosition = Option(position.position()))\n\n          case nullability: UpdateColumnNullability =>\n            val field = nullability.fieldNames()\n            val spec = getColumn(field)\n            columnUpdates(field) = spec.copy(\n              newColumn = spec.newColumn.copy(nullable = nullability.nullable()))\n\n          case rename: RenameColumn =>\n            val field = rename.fieldNames()\n            val spec = getColumn(field)\n            columnUpdates(field) = spec.copy(\n              newColumn = spec.newColumn.copy(name = rename.newName()))\n\n          case sync: SyncIdentity =>\n            val field = sync.fieldNames\n            val spec = getColumn(field).copy(syncIdentity = true)\n            columnUpdates(field) = spec\n            if (!ColumnWithDefaultExprUtils.isIdentityColumn(spec.newColumn)) {\n              throw DeltaErrors.identityColumnAlterNonIdentityColumnError()\n            }\n            // If the IDENTITY column does not allow explicit insert, high water mark should\n            // always be sync'ed and this is a no-op.\n            // TODO: This is redundant at the moment because columnUpdates is always set above.\n            // The original intention was to avoid running sync identity when the column does not\n            // allow explicit insert, so columnUpdates should only be set here, but doing so would\n            // fail a test related to DELTA_IDENTITY_ALLOW_SYNC_IDENTITY_TO_LOWER_HIGH_WATER_MARK.\n            // This should be fixed in the future.\n            if (IdentityColumn.allowExplicitInsert(spec.newColumn)) {\n              columnUpdates(field) = spec\n            }\n\n          case updateDefault: UpdateColumnDefaultValue =>\n            val field = updateDefault.fieldNames()\n            val spec = getColumn(field)\n            val updatedField = updateDefault.newDefaultValue() match {\n              case \"\" => spec.newColumn.clearCurrentDefaultValue()\n              case newDefault => spec.newColumn.withCurrentDefaultValue(newDefault)\n            }\n            columnUpdates(field) = spec.copy(newColumn = updatedField)\n\n          case other =>\n            throw DeltaErrors.unrecognizedColumnChange(s\"${other.getClass}\")\n        }\n\n      case (t, locations) if t == classOf[SetLocation] =>\n        if (locations.size != 1) {\n          throw DeltaErrors.cannotSetLocationMultipleTimes(\n            locations.asInstanceOf[Seq[SetProperty]].map(_.value()))\n        }\n        if (table.tableIdentifier.isEmpty) {\n          throw DeltaErrors.setLocationNotSupportedOnPathIdentifiers()\n        }\n        AlterTableSetLocationDeltaCommand(\n          table,\n          locations.head.asInstanceOf[SetProperty].value()).run(spark)\n\n      case (t, constraints) if t == classOf[AddConstraint] =>\n        constraints.foreach { constraint =>\n          val c = constraint.asInstanceOf[AddConstraint]\n          AlterTableAddConstraintDeltaCommand(table, c.constraintName, c.expr).run(spark)\n        }\n\n      case (t, constraints) if t == classOf[DropConstraint] =>\n        constraints.foreach { constraint =>\n          val c = constraint.asInstanceOf[DropConstraint]\n          AlterTableDropConstraintDeltaCommand(table, c.constraintName, c.ifExists).run(spark)\n        }\n\n      case (t, dropFeature) if t == classOf[DropFeature] =>\n        // Only single feature removal is supported.\n        val dropFeatureTableChange = dropFeature.head.asInstanceOf[DropFeature]\n        val featureName = dropFeatureTableChange.featureName\n        val truncateHistory = dropFeatureTableChange.truncateHistory\n        AlterTableDropFeatureDeltaCommand(\n          table, featureName, truncateHistory = truncateHistory).run(spark)\n\n      case (t, clusterBy) if t == classOf[ClusterBy] =>\n        clusterBy.asInstanceOf[Seq[ClusterBy]].foreach { c =>\n          if (c.clusteringColumns.nonEmpty) {\n            val clusterBySpec = ClusterBySpec(c.clusteringColumns.toSeq)\n            validateClusterBySpec(Some(clusterBySpec), table.schema())\n          }\n          if (table.initialSnapshot.metadata.partitionColumns.nonEmpty) {\n            throw DeltaErrors.alterTableClusterByOnPartitionedTableException()\n          }\n          AlterTableClusterByDeltaCommand(\n            table, c.clusteringColumns.map(_.fieldNames().toSeq).toSeq).run(spark)\n        }\n    }\n\n    if (columnUpdates.nonEmpty) {\n      AlterTableChangeColumnDeltaCommand(table, columnUpdates.values.toSeq).run(spark)\n    }\n\n    loadTable(ident)\n  }\n  }\n\n  // We want our catalog to handle Delta, therefore for other data sources that want to be\n  // created, we just have this wrapper StagedTable to only drop the table if the commit fails.\n  private case class BestEffortStagedTable(\n      ident: Identifier,\n      table: Table,\n      catalog: TableCatalog) extends StagedTable with SupportsWrite {\n    override def abortStagedChanges(): Unit = catalog.dropTable(ident)\n\n    override def commitStagedChanges(): Unit = {}\n\n    // Pass through\n    override def name(): String = table.name()\n    override def schema(): StructType = table.schema()\n    override def partitioning(): Array[Transform] = table.partitioning()\n    override def capabilities(): util.Set[TableCapability] = table.capabilities()\n    override def properties(): util.Map[String, String] = table.properties()\n\n    override def newWriteBuilder(info: LogicalWriteInfo): WriteBuilder = table match {\n      case supportsWrite: SupportsWrite => supportsWrite.newWriteBuilder(info)\n      case _ => throw DeltaErrors.unsupportedWriteStagedTable(name)\n    }\n  }\n}\n\n/**\n * A trait for handling table access through delta.`/some/path`. This is a stop-gap solution\n * until PathIdentifiers are implemented in Apache Spark.\n */\ntrait SupportsPathIdentifier extends TableCatalog { self: AbstractDeltaCatalog =>\n\n  private def supportSQLOnFile: Boolean = spark.sessionState.conf.runSQLonFile\n\n  protected lazy val catalog: SessionCatalog = spark.sessionState.catalog\n\n  private def hasDeltaNamespace(ident: Identifier): Boolean = {\n    ident.namespace().length == 1 && DeltaSourceUtils.isDeltaDataSourceName(ident.namespace().head)\n  }\n\n  private def hasIcebergNamespace(ident: Identifier): Boolean = {\n    ident.namespace().length == 1 && ident.namespace().head.equalsIgnoreCase(\"iceberg\")\n  }\n\n  protected def isIcebergPathIdentifier(ident: Identifier): Boolean = {\n    hasIcebergNamespace(ident) && new Path(ident.name()).isAbsolute\n  }\n\n  protected def newIcebergPathTable(ident: Identifier): IcebergTablePlaceHolder = {\n    IcebergTablePlaceHolder(TableIdentifier(ident.name(), Some(\"iceberg\")))\n  }\n\n  protected def isPathIdentifier(ident: Identifier): Boolean = {\n    // Should be a simple check of a special PathIdentifier class in the future\n    try {\n      supportSQLOnFile && hasDeltaNamespace(ident) && new Path(ident.name()).isAbsolute\n    } catch {\n      case _: IllegalArgumentException => false\n    }\n  }\n\n  protected def isPathIdentifier(table: CatalogTable): Boolean = {\n    isPathIdentifier(table.identifier)\n  }\n\n  protected def isPathIdentifier(tableIdentifier: TableIdentifier) : Boolean = {\n    isPathIdentifier(Identifier.of(tableIdentifier.database.toArray, tableIdentifier.table))\n  }\n\n  override def tableExists(ident: Identifier): Boolean = recordFrameProfile(\n      \"DeltaCatalog\", \"tableExists\") {\n    if (isPathIdentifier(ident)) {\n      val path = new Path(ident.name())\n      // scalastyle:off deltahadoopconfiguration\n      val fs = path.getFileSystem(spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      fs.exists(path) && fs.listStatus(path).nonEmpty\n    } else {\n      super.tableExists(ident)\n    }\n  }\n}\n\nobject BucketTransform {\n  def unapply(transform: Transform): Option[(Int, Seq[NamedReference], Seq[NamedReference])] = {\n    val arguments = transform.arguments()\n    if (transform.name() == \"sorted_bucket\") {\n      var posOfLit: Int = -1\n      var numOfBucket: Int = -1\n      arguments.zipWithIndex.foreach {\n        case (literal: Literal[_], i) if literal.dataType() == IntegerType =>\n          numOfBucket = literal.value().asInstanceOf[Integer]\n          posOfLit = i\n        case _ =>\n      }\n      Some(numOfBucket, arguments.take(posOfLit).map(_.asInstanceOf[NamedReference]),\n        arguments.drop(posOfLit + 1).map(_.asInstanceOf[NamedReference]))\n    } else if (transform.name() == \"bucket\") {\n      val numOfBucket = arguments(0) match {\n        case literal: Literal[_] if literal.dataType() == IntegerType =>\n          literal.value().asInstanceOf[Integer]\n        case _ => throw new IllegalStateException(\"invalid bucket transform\")\n      }\n      Some(numOfBucket, arguments.drop(1).map(_.asInstanceOf[NamedReference]),\n        Seq.empty[FieldReference])\n    } else {\n      None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/catalog/CatalogResolver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.catalog\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaTableIdentifier, DeltaTableUtils}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.connector.catalog.{CatalogNotFoundException, CatalogPlugin, Identifier, Table, V1Table}\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits.CatalogHelper\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper\n\n/**\n * Helper object for resolving Delta tables using a *non-session* catalog.\n */\nobject CatalogResolver {\n  def getDeltaTableFromCatalog(\n      spark: SparkSession,\n      catalog: CatalogPlugin,\n      ident: Identifier): DeltaTableV2 = {\n    catalog.asTableCatalog.loadTable(ident) match {\n      case v2: DeltaTableV2 => v2\n      case v1: V1Table if DeltaTableUtils.isDeltaTable(v1.v1Table) =>\n        DeltaTableV2(\n          spark,\n          path = new Path(v1.v1Table.location),\n          catalogTable = Some(v1.v1Table),\n          tableIdentifier = Some(ident.toString)\n        )\n      case table => throw DeltaErrors.notADeltaTableException(\n        DeltaTableIdentifier(table = Some(TableIdentifier(table.name()))))\n    }\n  }\n\n  /**\n   Returns a [[(CatalogPlugin, Identifier)]] if a catalog exists with the\n   input name, otherwise throws a [[CatalogNotFoundException]]\n  */\n  def getCatalogPluginAndIdentifier(\n      spark: SparkSession,\n      catalog: String,\n      ident: Seq[String]): (CatalogPlugin, Identifier) = {\n    (spark.sessionState.catalogManager.catalog(catalog), ident.asIdentifier)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/catalog/DeltaTableV2.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.catalog\n\nimport java.{util => ju}\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands.WriteIntoDelta\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.{DeltaDataSource, DeltaSourceUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.ENABLE_TABLE_REDIRECT_FEATURE\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{DataFrame, Dataset, SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{ResolvedTable, UnresolvedTable}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType, CatalogUtils}\nimport org.apache.spark.sql.catalyst.plans.logical.{AnalysisHelper, LogicalPlan, SubqueryAlias}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.connector.catalog.{SupportsWrite, Table, TableCapability, TableCatalog, V2TableWithV1Fallback}\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\nimport org.apache.spark.sql.connector.catalog.TableCapability._\nimport org.apache.spark.sql.connector.catalog.V1Table\nimport org.apache.spark.sql.connector.expressions._\nimport org.apache.spark.sql.connector.write.{LogicalWriteInfo, SupportsDynamicOverwrite, SupportsOverwrite, SupportsTruncate, V1Write, WriteBuilder}\nimport org.apache.spark.sql.errors.QueryCompilationErrors\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.sources.{BaseRelation, Filter, InsertableRelation}\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\nimport org.apache.spark.util.{Clock, SystemClock}\n\n/**\n * The data source V2 representation of a Delta table that exists.\n *\n * @param path The path to the table\n * @param tableIdentifier The table identifier for this table\n */\nclass DeltaTableV2 private(\n    val spark: SparkSession,\n    val path: Path,\n    val catalogTable: Option[CatalogTable],\n    val tableIdentifier: Option[String],\n    val timeTravelOpt: Option[DeltaTimeTravelSpec],\n    val options: Map[String, String])\n  extends Table\n  with SupportsWrite\n  with V2TableWithV1Fallback\n  with DeltaLogging {\n\n  case class PathInfo(\n      rootPath: Path,\n      private[delta] var partitionFilters: Seq[(String, String)],\n      private[delta] var timeTravelByPath: Option[DeltaTimeTravelSpec]\n  )\n\n  private lazy val pathInfo: PathInfo = {\n    if (catalogTable.isDefined) {\n      // Fast path for reducing path munging overhead\n      PathInfo(new Path(catalogTable.get.location), Seq.empty, None)\n    } else {\n      val (rootPath, filters, timeTravel) =\n        DeltaDataSource.parsePathIdentifier(spark, path.toString, options)\n      PathInfo(rootPath, filters, timeTravel)\n    }\n  }\n\n  private def rootPath = pathInfo.rootPath\n\n  private def partitionFilters = pathInfo.partitionFilters\n\n  private def timeTravelByPath = pathInfo.timeTravelByPath\n\n\n  def hasPartitionFilters: Boolean = partitionFilters.nonEmpty\n\n  // This MUST be initialized before the deltaLog object is created, in order to accurately\n  // bound the creation time of the table.\n  private val creationTimeMs = {\n      System.currentTimeMillis()\n  }\n\n  // The loading of the DeltaLog is lazy in order to reduce the amount of FileSystem calls,\n  // in cases where we will fallback to the V1 behavior.\n  lazy val deltaLog: DeltaLog = {\n    DeltaTableV2.withEnrichedUnsupportedTableException(catalogTable, tableIdentifier) {\n      // Ideally the table storage properties should always be the same as the options load from\n      // the Delta log, as Delta CREATE TABLE command guarantees it. However, custom catalogs such\n      // as Unity Catalog may add more table storage properties on the fly. We should respect it\n      // and merge the table storage properties and Delta options.\n      val dataSourceOptions = if (catalogTable.isDefined) {\n        // To be safe, here we only extract file system options from table storage properties and\n        // the original `options` has higher priority than the table storage properties.\n        val fileSystemOptions = catalogTable.get.storage.properties.filter { case (k, _) =>\n          DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n        }\n        fileSystemOptions ++ options\n      } else {\n        options\n      }\n      DeltaLog.forTable(\n        spark,\n        rootPath,\n        dataSourceOptions,\n        catalogTable)\n    }\n  }\n\n  /**\n   * Updates the delta log for this table and returns a new snapshot\n   */\n  def update(): Snapshot = deltaLog.update(catalogTableOpt = catalogTable)\n\n  def update(checkIfUpdatedSinceTs: Option[Long]): Snapshot =\n    deltaLog.update(checkIfUpdatedSinceTs = checkIfUpdatedSinceTs, catalogTableOpt = catalogTable)\n\n  /**\n   * Gets the snapshot at the given version of this table\n   */\n  def getSnapshotAt(version: Long): Snapshot =\n    deltaLog.getSnapshotAt(version, catalogTableOpt = catalogTable)\n\n  def getTableIdentifierIfExists: Option[TableIdentifier] = tableIdentifier.map { tableName =>\n    spark.sessionState.sqlParser.parseMultipartIdentifier(tableName).asTableIdentifier\n  }\n\n  override def name(): String = catalogTable.map(_.identifier.unquotedString)\n    .orElse(tableIdentifier)\n    .getOrElse(s\"delta.`${deltaLog.dataPath}`\")\n\n  private lazy val timeTravelSpec: Option[DeltaTimeTravelSpec] = {\n    if (timeTravelOpt.isDefined && timeTravelByPath.isDefined) {\n      throw DeltaErrors.multipleTimeTravelSyntaxUsed\n    }\n    timeTravelOpt.orElse(timeTravelByPath)\n  }\n\n  private lazy val caseInsensitiveOptions = new CaseInsensitiveStringMap(options.asJava)\n\n  /**\n   * The snapshot initially associated with this table. It is captured on first access, usually (but\n   * not always) shortly after the table was first created, and is immutable once captured.\n   *\n   * WARNING: This snapshot could be arbitrarily stale for long-lived [[DeltaTableV2]] instances,\n   * such as the ones [[DeltaTable]] uses internally. Callers who cannot tolerate this potential\n   * staleness should use [[getFreshSnapshot]] instead.\n   *\n   * WARNING: Because the snapshot is captured lazily, callers should explicitly access the snapshot\n   * if they want to be certain it has been captured.\n   */\n  lazy val initialSnapshot: Snapshot = DeltaTableV2.withEnrichedUnsupportedTableException(\n    catalogTable, tableIdentifier) {\n\n    timeTravelSpec.map { spec =>\n      // By default, block using CDF + time-travel\n      if (CDCReader.isCDCRead(caseInsensitiveOptions) &&\n          !spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CDF_ALLOW_TIME_TRAVEL_OPTIONS)) {\n        throw DeltaErrors.timeTravelNotSupportedException\n      }\n\n      val (version, accessType) = DeltaTableUtils.resolveTimeTravelVersion(\n        spark.sessionState.conf, deltaLog, catalogTable, spec)\n      val source = spec.creationSource.getOrElse(\"unknown\")\n      recordDeltaEvent(deltaLog, s\"delta.timeTravel.$source\", data = Map(\n        // Log the cached version of the table on the cluster\n        \"tableVersion\" -> deltaLog.unsafeVolatileSnapshot.version,\n        \"queriedVersion\" -> version,\n        \"accessType\" -> accessType\n      ))\n      deltaLog.getSnapshotAt(\n        version,\n        catalogTableOpt = catalogTable,\n        enforceTimeTravelWithinDeletedFileRetention = spec.enforceRetention)\n    }.getOrElse(\n      deltaLog.update(\n        stalenessAcceptable = true,\n        checkIfUpdatedSinceTs = Some(creationTimeMs),\n        catalogTableOpt = catalogTable\n      )\n    )\n  }\n\n  // We get the cdcRelation ahead of time if this is a CDC read to be able to return the correct\n  // schema. The schema for CDC reads are currently convoluted due to column mapping behavior\n  private lazy val cdcRelation: Option[BaseRelation] = {\n    if (CDCReader.isCDCRead(caseInsensitiveOptions)) {\n      recordDeltaEvent(deltaLog, \"delta.cdf.read\",\n        data = caseInsensitiveOptions.asCaseSensitiveMap())\n      Some(CDCReader.getCDCRelation(\n        spark, initialSnapshot, catalogTable, timeTravelSpec.nonEmpty, spark.sessionState.conf,\n        caseInsensitiveOptions))\n    } else {\n      None\n    }\n  }\n\n  private lazy val tableSchema: StructType = {\n    val baseSchema = cdcRelation.map(_.schema).getOrElse(initialSnapshot.schema)\n    DeltaTableUtils.removeInternalDeltaMetadata(\n      spark, DeltaTableUtils.removeInternalWriterMetadata(spark, baseSchema)\n    )\n  }\n\n  override def schema(): StructType = tableSchema\n\n  override def partitioning(): Array[Transform] = {\n    initialSnapshot.metadata.partitionColumns.map { col =>\n      new IdentityTransform(new FieldReference(Seq(col)))\n    }.toArray\n  }\n\n  override def properties(): ju.Map[String, String] = {\n    val base = initialSnapshot.getProperties\n    base.put(TableCatalog.PROP_PROVIDER, \"delta\")\n    base.put(TableCatalog.PROP_LOCATION, CatalogUtils.URIToString(path.toUri))\n    catalogTable.foreach { table =>\n      if (table.owner != null && table.owner.nonEmpty) {\n        base.put(TableCatalog.PROP_OWNER, table.owner)\n      }\n      v1Table.storage.properties.foreach { case (key, value) =>\n        if (!DeltaTableV2.HIDDEN_STORAGE_PROPERTY_PREFIXES.exists(key.startsWith)) {\n          base.put(TableCatalog.OPTION_PREFIX + key, value)\n        }\n      }\n      if (v1Table.tableType == CatalogTableType.EXTERNAL) {\n        base.put(TableCatalog.PROP_EXTERNAL, \"true\")\n      }\n    }\n    // Don't use [[PROP_CLUSTERING_COLUMNS]] from CatalogTable because it may be stale.\n    // Since ALTER TABLE updates it using an async post-commit hook.\n    clusterBySpec.foreach { clusterBy =>\n      ClusterBySpec.toProperties(clusterBy).foreach { case (key, value) =>\n        base.put(key, value)\n      }\n    }\n    Option(initialSnapshot.metadata.description).foreach(base.put(TableCatalog.PROP_COMMENT, _))\n    base.asJava\n  }\n\n  override def capabilities(): ju.Set[TableCapability] = Set(\n    ACCEPT_ANY_SCHEMA, BATCH_READ,\n    V1_BATCH_WRITE, OVERWRITE_BY_FILTER, TRUNCATE, OVERWRITE_DYNAMIC\n  ).asJava\n\n  def tableExists: Boolean = deltaLog.tableExists\n\n\n  override def newWriteBuilder(info: LogicalWriteInfo): WriteBuilder = {\n    new WriteIntoDeltaBuilder(\n      this, info.options, spark.sessionState.conf.useNullsForMissingDefaultColumnValues)\n  }\n\n  /**\n   * Starts a transaction for this table, using the snapshot captured during table resolution.\n   *\n   * WARNING: Caller is responsible to ensure that table resolution was recent (e.g. if working with\n   * [[DataFrame]] or [[DeltaTable]] API, where the table could have been resolved long ago).\n   */\n  def startTransactionWithInitialSnapshot(): OptimisticTransaction =\n    startTransaction(Some(initialSnapshot))\n\n  /**\n   * Starts a transaction for this table, using Some provided snapshot, or a fresh snapshot if None\n   * was provided.\n   */\n  def startTransaction(snapshotOpt: Option[Snapshot] = None): OptimisticTransaction = {\n    deltaLog.startTransaction(catalogTable, snapshotOpt)\n  }\n\n  /**\n   * Creates a checkpoint for this table.\n   * @param snapshotToCheckpoint The snapshot to checkpoint.\n   */\n  def checkpoint(snapshotToCheckpoint: Snapshot): Unit = {\n    deltaLog.checkpoint(snapshotToCheckpoint, catalogTable)\n  }\n\n  /**\n   * Creates a V1 BaseRelation from this Table to allow read APIs to go through V1 DataSource code\n   * paths.\n   */\n  lazy val toBaseRelation: BaseRelation = {\n    // force update() if necessary in DataFrameReader.load code\n    initialSnapshot\n    if (!tableExists) {\n      // special error handling for path based tables\n      if (catalogTable.isEmpty\n        && !rootPath.getFileSystem(deltaLog.newDeltaHadoopConf()).exists(rootPath)) {\n        throw QueryCompilationErrors.dataPathNotExistError(rootPath.toString)\n      }\n\n      val id = catalogTable.map(ct => DeltaTableIdentifier(table = Some(ct.identifier)))\n        .getOrElse(DeltaTableIdentifier(path = Some(path.toString)))\n      throw DeltaErrors.nonExistentDeltaTable(id)\n    }\n    val partitionPredicates = DeltaDataSource.verifyAndCreatePartitionFilters(\n      path.toString, initialSnapshot, partitionFilters)\n\n    cdcRelation.getOrElse {\n      deltaLog.createRelation(\n        partitionPredicates, Some(initialSnapshot), catalogTable, timeTravelSpec.isDefined)\n    }\n  }\n\n  /** Creates a [[LogicalRelation]] that represents this table */\n  lazy val toLogicalRelation: LogicalRelation = {\n    val relation = this.toBaseRelation\n    LogicalRelation(\n      relation,\n      toAttributes(relation.schema),\n      ttSafeCatalogTable,\n      isStreaming = false,\n      stream = None)\n  }\n\n  /** Creates a [[DataFrame]] that uses the requested spark session to read from this table */\n  def toDf(sparkSession: SparkSession): DataFrame = {\n    val plan = catalogTable.foldLeft[LogicalPlan](toLogicalRelation) { (child, ct) =>\n      // Catalog based tables need a SubqueryAlias that carries their fully-qualified name\n      SubqueryAlias(ct.identifier.nameParts, child)\n    }\n    DataFrameUtils.ofRows(sparkSession, plan)\n  }\n\n  /** Creates a [[DataFrame]] that reads from this table */\n  lazy val toDf: DataFrame = toDf(spark)\n\n  /**\n   * Check the passed in options and existing timeTravelOpt, set new time travel by options.\n   */\n  def withOptions(newOptions: Map[String, String]): DeltaTableV2 = {\n    val ttSpec = DeltaDataSource.getTimeTravelVersion(newOptions)\n\n    // Spark 4.0 and 3.5 handle time travel options differently.\n    // Validate that only one time travel spec is being used\n    (timeTravelOpt, ttSpec) match {\n      case (Some(currSpec), Some(newSpec))\n        if currSpec.version != newSpec.version  ||\n          currSpec.getTimestampOpt(spark.sessionState.conf).map(_.getTime) !=\n            newSpec.getTimestampOpt(spark.sessionState.conf).map(_.getTime) =>\n          throw DeltaErrors.multipleTimeTravelSyntaxUsed\n      case _ =>\n    }\n\n    val caseInsensitiveNewOptions = new CaseInsensitiveStringMap(newOptions.asJava)\n\n    if (timeTravelOpt.isEmpty && ttSpec.nonEmpty) {\n      copy(timeTravelOpt = ttSpec)\n    } else if (CDCReader.isCDCRead(caseInsensitiveNewOptions)) {\n      checkCDCOptionsValidity(caseInsensitiveNewOptions)\n      // Do not use statistics during CDF reads\n      this.copy(catalogTable = catalogTable.map(_.copy(stats = None)), options = newOptions)\n    } else {\n      this\n    }\n  }\n\n  private def checkCDCOptionsValidity(options: CaseInsensitiveStringMap): Unit = {\n    // check if we have both version and timestamp parameters\n    if (options.containsKey(DeltaDataSource.CDC_START_TIMESTAMP_KEY)\n      && options.containsKey(DeltaDataSource.CDC_START_VERSION_KEY)) {\n      throw DeltaErrors.multipleCDCBoundaryException(\"starting\")\n    }\n    if (options.containsKey(DeltaDataSource.CDC_END_VERSION_KEY)\n      && options.containsKey(DeltaDataSource.CDC_END_TIMESTAMP_KEY)) {\n      throw DeltaErrors.multipleCDCBoundaryException(\"ending\")\n    }\n    if (!options.containsKey(DeltaDataSource.CDC_START_VERSION_KEY)\n      && !options.containsKey(DeltaDataSource.CDC_START_TIMESTAMP_KEY)) {\n      throw DeltaErrors.noStartVersionForCDC()\n    }\n  }\n\n  /** A \"clean\" version of the catalog table, safe for use with or without time travel. */\n  lazy val ttSafeCatalogTable: Option[CatalogTable] = catalogTable match {\n    case Some(ct) if timeTravelSpec.isDefined => Some(ct.copy(stats = None))\n    case other => other\n  }\n\n  override def v1Table: CatalogTable = ttSafeCatalogTable.getOrElse {\n    throw DeltaErrors.invalidV1TableCall(\"v1Table\", \"DeltaTableV2\")\n  }\n\n  lazy val clusterBySpec: Option[ClusterBySpec] = {\n    // Always get the clustering columns from metadata domain in delta log.\n    if (ClusteredTableUtils.isSupported(initialSnapshot.protocol)) {\n      val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(\n        initialSnapshot)\n      Some(ClusterBySpec.fromColumnNames(clusteringColumns))\n    } else {\n      None\n    }\n  }\n\n  def copy(\n    spark: SparkSession = this.spark,\n    path: Path = this.path,\n    catalogTable: Option[CatalogTable] = this.catalogTable,\n    tableIdentifier: Option[String] = this.tableIdentifier,\n    timeTravelOpt: Option[DeltaTimeTravelSpec] = this.timeTravelOpt,\n    options: Map[String, String] = this.options\n  ): DeltaTableV2 = {\n    val deltaTableV2 =\n      new DeltaTableV2(spark, path, catalogTable, tableIdentifier, timeTravelOpt, options)\n    deltaTableV2.pathInfo.timeTravelByPath = timeTravelByPath\n    deltaTableV2.pathInfo.partitionFilters = partitionFilters\n    deltaTableV2\n  }\n\n  override def toString: String =\n    s\"DeltaTableV2($spark,$path,$catalogTable,$tableIdentifier,$timeTravelOpt,$options)\"\n}\n\nobject DeltaTableV2 {\n\n  /**\n   * Storage property key prefixes that should be excluded from the user-visible V2 table\n   * properties returned by [[DeltaTableV2.properties()]]. These are Hadoop filesystem\n   * configuration options that may contain sensitive credentials (access keys, session\n   * tokens, etc.) injected by catalogs at table-load time.\n   */\n  private[delta] val HIDDEN_STORAGE_PROPERTY_PREFIXES: Seq[String] = Seq(\"fs.\")\n\n  def unapply(deltaTable: DeltaTableV2): Option[(\n      SparkSession,\n      Path,\n      Option[CatalogTable],\n      Option[String],\n      Option[DeltaTimeTravelSpec],\n      Map[String, String])\n  ] = {\n    Some((\n      deltaTable.spark,\n      deltaTable.path,\n      deltaTable.catalogTable,\n      deltaTable.tableIdentifier,\n      deltaTable.timeTravelOpt,\n      deltaTable.options)\n    )\n  }\n\n  def apply(\n      spark: SparkSession,\n      path: Path,\n      catalogTable: Option[CatalogTable] = None,\n      tableIdentifier: Option[String] = None,\n      options: Map[String, String] = Map.empty[String, String],\n      timeTravelOpt: Option[DeltaTimeTravelSpec] = None\n  ): DeltaTableV2 = {\n    val deltaTable = new DeltaTableV2(\n      spark,\n      path,\n      catalogTable = catalogTable,\n      tableIdentifier = tableIdentifier,\n      timeTravelOpt = timeTravelOpt,\n      options = options\n    )\n    if (spark == null || spark.sessionState == null ||\n        !spark.sessionState.conf.getConf(ENABLE_TABLE_REDIRECT_FEATURE)) {\n      return deltaTable\n    }\n    // This following code ensure passing the path and catalogTable of the redirected table object.\n    // Note: the DeltaTableV2 can only be created using this method.\n    AnalysisHelper.allowInvokingTransformsInAnalyzer {\n      val deltaLog = deltaTable.deltaLog\n      val rootDeltaLogPath = DeltaLog.logPathFor(deltaTable.rootPath.toString)\n      val finalDeltaLogPath = DeltaLog.formalizeDeltaPath(spark, options, rootDeltaLogPath)\n      if (finalDeltaLogPath == deltaLog.logPath) {\n        // If there is no redirection, use existing delta table.\n        deltaTable\n      } else {\n        // If there is redirection, use the catalogTable of deltaLog.\n        val catalogTable = deltaLog.getInitialCatalogTable\n        val newPath = new Path(deltaLog.dataPath.toUri)\n        new DeltaTableV2(\n          spark,\n          path = newPath,\n          catalogTable = catalogTable,\n          tableIdentifier = catalogTable.map(_.identifier.identifier),\n          timeTravelOpt = timeTravelOpt,\n          options = options\n        )\n      }\n    }\n  }\n\n  /** Resolves a path into a DeltaTableV2, leveraging standard v2 table resolution. */\n  def apply(spark: SparkSession, tablePath: Path, options: Map[String, String], cmd: String)\n      : DeltaTableV2 = {\n    val unresolved = UnresolvedPathBasedDeltaTable(tablePath.toString, options, cmd)\n    extractFrom((new DeltaAnalysis(spark))(unresolved), cmd)\n  }\n\n  /** Resolves a table identifier into a DeltaTableV2, leveraging standard v2 table resolution. */\n  def apply(spark: SparkSession, tableId: TableIdentifier, cmd: String): DeltaTableV2 = {\n    val unresolved = UnresolvedTable(tableId.nameParts, cmd)\n    extractFrom(spark.sessionState.analyzer.ResolveRelations(unresolved), cmd)\n  }\n\n  /**\n   * Extracts the DeltaTableV2 from a resolved Delta table plan node, throwing \"table not found\" if\n   * the node does not actually represent a resolved Delta table.\n   */\n  def extractFrom(plan: LogicalPlan, cmd: String): DeltaTableV2 =\n    maybeExtractFrom(plan).getOrElse(throw DeltaErrors.notADeltaTableException(cmd))\n\n  /**\n   * Extracts the DeltaTableV2 from a resolved Delta table plan node, returning None if the node\n   * does not actually represent a resolved Delta table.\n   */\n  def maybeExtractFrom(plan: LogicalPlan): Option[DeltaTableV2] = plan match {\n    case ResolvedTable(_, _, d: DeltaTableV2, _) => Some(d)\n    case ResolvedTable(_, _, t: V1Table, _) if DeltaTableUtils.isDeltaTable(t.catalogTable) =>\n      Some(DeltaTableV2(SparkSession.active, new Path(t.v1Table.location), Some(t.v1Table)))\n    case _ => None\n  }\n\n  /**\n   * Creates a DeltaTableV2 instance with a custom DeltaLog object for testing purposes. This is\n   * useful because the DeltaTableV2 constructor is private and cannot be called from\n   * DeltaTestImplicit.\n   */\n  def testOnlyApplyWithCustomDeltaLog(\n    spark: SparkSession, path: Path, clock: Clock): DeltaTableV2 = {\n    new DeltaTableV2(\n      spark,\n      path,\n      catalogTable = None,\n      tableIdentifier = None,\n      timeTravelOpt = None,\n      options = Map.empty\n    ) {\n      override lazy val deltaLog: DeltaLog = DeltaLog.forTable(spark, path, clock)\n    }\n  }\n\n\n  /**\n   * When Delta Log throws InvalidProtocolVersionException it doesn't know the table name and uses\n   * the data path in the message, this wrapper throw a new InvalidProtocolVersionException with\n   * table name and sets its Cause to the original InvalidProtocolVersionException.\n   */\n  def withEnrichedUnsupportedTableException[T](\n      catalogTable: Option[CatalogTable],\n      tableName: Option[String] = None)(thunk: => T): T = {\n\n    lazy val tableNameToUse = catalogTable match {\n      case Some(ct) => Some(ct.identifier.copy(catalog = None).unquotedString)\n      case None => tableName\n    }\n\n    try thunk catch {\n      case e: InvalidProtocolVersionException if tableNameToUse.exists(_ != e.tableNameOrPath) =>\n        throw e.copy(tableNameOrPath = tableNameToUse.get).initCause(e)\n      case e: DeltaUnsupportedTableFeatureException if\n          tableNameToUse.exists(_ != e.tableNameOrPath) =>\n        throw e.copy(tableNameOrPath = tableNameToUse.get).initCause(e)\n    }\n  }\n}\n\nprivate class WriteIntoDeltaBuilder(\n    table: DeltaTableV2,\n    writeOptions: CaseInsensitiveStringMap,\n    nullAsDefault: Boolean)\n  extends WriteBuilder with SupportsOverwrite with SupportsTruncate with SupportsDynamicOverwrite {\n\n  private var forceOverwrite = false\n\n  private val options =\n    mutable.HashMap[String, String](writeOptions.asCaseSensitiveMap().asScala.toSeq: _*)\n\n  override def truncate(): WriteIntoDeltaBuilder = {\n    forceOverwrite = true\n    this\n  }\n\n  override def overwrite(filters: Array[Filter]): WriteBuilder = {\n    if (writeOptions.containsKey(\"replaceWhere\")) {\n      throw DeltaErrors.replaceWhereUsedInOverwrite()\n    }\n    options.put(\"replaceWhere\", DeltaSourceUtils.translateFilters(filters).sql)\n    forceOverwrite = true\n    this\n  }\n\n  override def overwriteDynamicPartitions(): WriteBuilder = {\n    options.put(\n      DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION,\n      DeltaOptions.PARTITION_OVERWRITE_MODE_DYNAMIC)\n    forceOverwrite = true\n    this\n  }\n\n  override def build(): V1Write = new V1Write {\n    override def toInsertableRelation(): InsertableRelation = {\n      new InsertableRelation {\n        override def insert(data: DataFrame, overwrite: Boolean): Unit = {\n          val session = data.sparkSession\n          // Normal table insertion should be the only place that can use null as the default\n          // column value. We put a special option here so that `TransactionalWrite#writeFiles`\n          // will recognize it and apply null-as-default.\n          if (nullAsDefault) {\n            options.put(\n              ColumnWithDefaultExprUtils.USE_NULL_AS_DEFAULT_DELTA_OPTION,\n              \"true\"\n            )\n          }\n          // TODO: Get the config from WriteIntoDelta's txn.\n          WriteIntoDelta(\n            table.deltaLog,\n            if (forceOverwrite) SaveMode.Overwrite else SaveMode.Append,\n            new DeltaOptions(options.toMap, session.sessionState.conf),\n            Nil,\n            table.deltaLog.unsafeVolatileSnapshot.metadata.configuration,\n            data,\n            table.catalogTable).run(session)\n\n          // TODO: Push this to Apache Spark\n          // Re-cache all cached plans(including this relation itself, if it's cached) that refer\n          // to this data source relation. This is the behavior for InsertInto\n          session.sharedState.cacheManager.recacheByPlan(session, table.toLogicalRelation)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/catalog/IcebergTablePlaceHolder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.catalog\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.connector.catalog.{Table, TableCapability}\nimport org.apache.spark.sql.types.StructType\n\n/** A place holder used to resolve Iceberg table as a relation during analysis */\ncase class IcebergTablePlaceHolder(tableIdentifier: TableIdentifier) extends Table {\n\n  override def name(): String = tableIdentifier.unquotedString\n\n  override def schema(): StructType = new StructType()\n\n  override def capabilities(): java.util.Set[TableCapability] = Set.empty[TableCapability].asJava\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/clustering/ClusteringMetadataDomain.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.clustering\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteringColumn\nimport org.apache.spark.sql.delta.{JsonMetadataDomain, JsonMetadataDomainUtils}\nimport org.apache.spark.sql.delta.actions.{Action, DomainMetadata}\n\n/**\n * Metadata domain for Clustered table which tracks clustering columns.\n */\ncase class ClusteringMetadataDomain(clusteringColumns: Seq[Seq[String]])\n  extends JsonMetadataDomain[ClusteringMetadataDomain] {\n  override val domainName: String = ClusteringMetadataDomain.domainName\n}\n\nobject ClusteringMetadataDomain extends JsonMetadataDomainUtils[ClusteringMetadataDomain] {\n  override val domainName = \"delta.clustering\"\n  // Extracts the ClusteringMetadataDomain and the removed field.\n  def unapply(action: Action): Option[(ClusteringMetadataDomain, Boolean)] = action match {\n    case d: DomainMetadata if d.domain == domainName => Some((fromJsonConfiguration(d), d.removed))\n    case _ => None\n  }\n\n  def fromClusteringColumns(clusteringColumns: Seq[ClusteringColumn]): ClusteringMetadataDomain = {\n    ClusteringMetadataDomain(clusteringColumns.map(_.physicalName))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/CloneTableBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.Closeable\nimport java.util.UUID\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CoordinatedCommitsUtils}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util._\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.{Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.SQLConfHelper\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.plans.logical.LeafCommand\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.{Clock, SerializableConfiguration}\n// scalastyle:on import.ordering.noEmptyLine\n\n/**\n * An interface of the source table to be cloned from.\n */\ntrait CloneSource extends Closeable {\n  /** The format of the source table */\n  def format: String\n\n  /** The source table's protocol */\n  def protocol: Protocol\n\n  /** A system clock */\n  def clock: Clock\n\n  /** The source table name */\n  def name: String\n\n  /** The path of the source table */\n  def dataPath: Path\n\n  /** The source table schema */\n  def schema: StructType\n\n  /** The catalog table of the source table, if exists */\n  def catalogTable: Option[CatalogTable]\n\n  /** The time travel spec of the source table, if exists */\n  def timeTravelOpt: Option[DeltaTimeTravelSpec]\n\n  /** A snapshot of the source table, if exists */\n  def snapshot: Option[Snapshot]\n\n  /** The metadata of the source table */\n  def metadata: Metadata\n\n  /** All of the files present in the source table */\n  def allFiles: Dataset[AddFile]\n\n  /** Total size of data files in bytes */\n  def sizeInBytes: Long\n\n  /** Total number of data files */\n  def numOfFiles: Long\n\n  /** Describe this clone source */\n  def description: String\n}\n\n// Clone source table formats\nobject CloneSourceFormat {\n  val DELTA = \"Delta\"\n  val ICEBERG = \"Iceberg\"\n  val PARQUET = \"Parquet\"\n  val UNKNOWN = \"Unknown\"\n}\n\ntrait CloneTableBaseUtils extends DeltaLogging\n{\n\n  import CloneTableCommand._\n\n  /** Make a map of operation metrics for the executed command for DeltaLog commits */\n  protected def getOperationMetricsForDeltaLog(\n      opMetrics: SnapshotOverwriteOperationMetrics): Map[String, Long] = {\n    Map(\n      SOURCE_TABLE_SIZE -> opMetrics.sourceSnapshotSizeInBytes,\n      SOURCE_NUM_OF_FILES -> opMetrics.sourceSnapshotFileCount,\n      NUM_REMOVED_FILES -> 0L,\n      NUM_COPIED_FILES -> 0L,\n      REMOVED_FILES_SIZE -> 0L,\n      COPIED_FILES_SIZE -> 0L\n    )\n  }\n\n  /**\n   * Make a map of operation metrics for the executed command for recording events.\n   * Any command can extend to overwrite or add new metrics\n   */\n  protected def getOperationMetricsForEventRecord(\n      opMetrics: SnapshotOverwriteOperationMetrics): Map[String, Long] =\n    getOperationMetricsForDeltaLog(opMetrics)\n\n  /** Make a output Seq[Row] of metrics for the executed command */\n  protected def getOutputSeq(operationMetrics: Map[String, Long]): Seq[Row]\n\n  protected def checkColumnMappingMode(beforeMetadata: Metadata, afterMetadata: Metadata): Unit = {\n    val beforeColumnMappingMode = beforeMetadata.columnMappingMode\n    val afterColumnMappingMode = afterMetadata.columnMappingMode\n    // can't switch column mapping mode\n    if (beforeColumnMappingMode != afterColumnMappingMode) {\n      throw DeltaErrors.changeColumnMappingModeNotSupported(\n        beforeColumnMappingMode.name, afterColumnMappingMode.name)\n    }\n  }\n\n  // Return a copy of the AddFiles with path being absolutized, indicating a SHALLOW CLONE\n  protected def handleNewDataFiles(\n      opName: String,\n      datasetOfNewFilesToAdd: Dataset[AddFile],\n      qualifiedSourceTableBasePath: String,\n      destTable: DeltaLog\n  ): Dataset[AddFile] = {\n    recordDeltaOperation(destTable, s\"delta.${opName.toLowerCase()}.makeAbsolute\") {\n      val absolutePaths = DeltaFileOperations.makePathsAbsolute(\n        qualifiedSourceTableBasePath,\n        datasetOfNewFilesToAdd)\n      absolutePaths\n    }\n  }\n}\n\nabstract class CloneTableBase(\n    sourceTable: CloneSource,\n    tablePropertyOverrides: Map[String, String],\n    targetPath: Path)\n  extends LeafCommand\n  with CloneTableBaseUtils\n  with SQLConfHelper\n{\n\n  import CloneTableBase._\n  def dataChangeInFileAction: Boolean = true\n\n  /** Returns whether the table exists at the given snapshot version. */\n  def tableExists(snapshot: SnapshotDescriptor): Boolean = snapshot.version >= 0\n\n  /**\n   * Handles the transaction logic for the CLONE command.\n   *\n   * @param spark [[SparkSession]] to use\n   * @param txn [[OptimisticTransaction]] to use for the commit to the target table.\n   * @param destinationTable [[DeltaLog]] of the destination table.\n   * @param deltaOperation [[DeltaOperations.Operation]] to use when commit changes to DeltaLog\n   * @return\n   */\n  protected def handleClone(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      destinationTable: DeltaLog,\n      hdpConf: Configuration,\n      deltaOperation: DeltaOperations.Operation,\n      commandMetrics: Option[Map[String, SQLMetric]]): Seq[Row] = {\n    val targetFs = targetPath.getFileSystem(hdpConf)\n    val qualifiedTarget = targetFs.makeQualified(targetPath).toString\n    val qualifiedSource = {\n      val sourcePath = sourceTable.dataPath\n      val sourceFs = sourcePath.getFileSystem(hdpConf)\n      sourceFs.makeQualified(sourcePath).toString\n    }\n\n    if (txn.readVersion < 0) {\n      destinationTable.createLogDirectoriesIfNotExists()\n    }\n\n    val metadataToUpdate = determineTargetMetadata(spark, txn.snapshot, deltaOperation.name)\n    // Don't merge in the default properties when cloning, or we'll end up with different sets of\n    // properties between source and target.\n    txn.updateMetadata(metadataToUpdate, ignoreDefaultProperties = true)\n    val (\n      addedFileList\n      ) = {\n      // Make sure target table is empty before running clone\n      if (txn.snapshot.allFiles.count() > 0) {\n        throw DeltaErrors.cloneReplaceNonEmptyTable\n      }\n      val toAdd = sourceTable.allFiles\n      // absolutize file paths\n      handleNewDataFiles(\n        deltaOperation.name,\n        toAdd,\n        qualifiedSource,\n        destinationTable).collectAsList()\n    }\n\n    val (addedFileCount, addedFilesSize) =\n        (addedFileList.size.toLong, totalDataSize(addedFileList.iterator))\n\n\n    val newProtocol = determineTargetProtocol(spark, txn, deltaOperation.name)\n    val addFileIter =\n        addedFileList.iterator.asScala\n\n    try {\n      var actions: Iterator[Action] =\n        addFileIter.map { fileToCopy =>\n          val copiedFile = fileToCopy.copy(dataChange = dataChangeInFileAction)\n          // CLONE does not preserve Row IDs and Commit Versions\n          copiedFile.copy(baseRowId = None, defaultRowCommitVersion = None)\n        }\n      sourceTable.snapshot.foreach { sourceSnapshot =>\n        // Handle DomainMetadata for cloning a table.\n        if (deltaOperation.name == DeltaOperations.OP_CLONE) {\n          actions ++=\n            DomainMetadataUtils.handleDomainMetadataForCloneTable(sourceSnapshot, txn.snapshot)\n        }\n      }\n      val sourceName = sourceTable.name\n      // Override source table metadata with user-defined table properties\n      val context = Map[String, String]()\n      val isReplaceDelta = txn.readVersion >= 0\n\n      val opMetrics = SnapshotOverwriteOperationMetrics(\n        sourceTable.sizeInBytes,\n        sourceTable.numOfFiles,\n        addedFileCount,\n        addedFilesSize)\n      val commitOpMetrics = getOperationMetricsForDeltaLog(opMetrics)\n\n      // Propagate the metrics back to the caller.\n      commandMetrics.foreach { commandMetrics =>\n        commitOpMetrics.foreach { kv =>\n          commandMetrics(kv._1).set(kv._2)\n        }\n      }\n\n      recordDeltaOperation(\n        destinationTable, s\"delta.${deltaOperation.name.toLowerCase()}.commit\") {\n        txn.commitLarge(\n          spark,\n          actions,\n          Some(newProtocol),\n          deltaOperation,\n          context,\n          commitOpMetrics.mapValues(_.toString()).toMap)\n      }\n\n      val cloneLogData = getOperationMetricsForEventRecord(opMetrics) ++ Map(\n        SOURCE -> sourceName,\n        SOURCE_FORMAT -> sourceTable.format,\n        SOURCE_PATH -> qualifiedSource,\n        TARGET -> qualifiedTarget,\n        PARTITION_BY -> sourceTable.metadata.partitionColumns,\n        IS_REPLACE_DELTA -> isReplaceDelta) ++\n        sourceTable.snapshot.map(s => SOURCE_VERSION -> s.version)\n      recordDeltaEvent(\n        destinationTable, s\"delta.${deltaOperation.name.toLowerCase()}\", data = cloneLogData)\n\n      getOutputSeq(commitOpMetrics)\n    } finally {\n      sourceTable.close()\n    }\n  }\n\n\n  /**\n   * Prepares the source metadata by making it compatible with the existing target metadata.\n   */\n  private def prepareSourceMetadata(\n      targetSnapshot: SnapshotDescriptor,\n      opName: String): Metadata = {\n    var clonedMetadata =\n      sourceTable.metadata.copy(\n        id = UUID.randomUUID().toString,\n        name = targetSnapshot.metadata.name,\n        description = targetSnapshot.metadata.description)\n    // Existing target table\n    if (tableExists(targetSnapshot)) {\n      // Set the ID equal to the target ID\n      clonedMetadata = clonedMetadata.copy(id = targetSnapshot.metadata.id)\n    }\n\n    val filteredConfiguration = clonedMetadata.configuration\n      // Coordinated Commit configurations are never copied over to the target table.\n      .filterKeys(!CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS.contains(_))\n      // Catalog-Owned enabled table's UC table ID property is never copied over to the\n      // target table.\n      .filterKeys(_ != UCCommitCoordinatorClient.UC_TABLE_ID_KEY)\n      .toMap\n\n    val clonedSchema =\n      IdentityColumn.copySchemaWithMergedHighWaterMarks(\n        deltaLog = targetSnapshot.deltaLog,\n        schemaToCopy = clonedMetadata.schema,\n        schemaWithHighWaterMarksToMerge = targetSnapshot.metadata.schema\n      )\n    clonedMetadata.copy(configuration = filteredConfiguration, schemaString = clonedSchema.json)\n  }\n\n  /**\n   * Verifies metadata invariants.\n   */\n  private def verifyMetadataInvariants(\n      targetSnapshot: SnapshotDescriptor,\n      updatedMetadataWithOverrides: Metadata): Unit = {\n    // TODO: we have not decided on how to implement switching column mapping modes\n    //  so we block this feature for now\n    // 1. Validate configuration overrides\n    //    this checks if columnMapping.maxId is unexpected set in the properties\n    DeltaConfigs.validateConfigurations(tablePropertyOverrides)\n    // 2. Check for column mapping mode conflict with the source metadata w/ tablePropertyOverrides\n    checkColumnMappingMode(sourceTable.metadata, updatedMetadataWithOverrides)\n    // 3. Checks for column mapping mode conflicts with existing metadata if there's any\n    if (tableExists(targetSnapshot)) {\n      checkColumnMappingMode(targetSnapshot.metadata, updatedMetadataWithOverrides)\n    }\n  }\n\n  /**\n   * Priority of Coordinated Commits configurations:\n   *   - When CLONE into a new table, explicit command specification takes precedence over default\n   *     SparkSession configurations.\n   *   - When CLONE into an existing table, use the existing table's configurations.\n   */\n  private def determineCoordinatedCommitsConfigurations(\n      spark: SparkSession,\n      targetSnapshot: SnapshotDescriptor,\n      validatedOverrides: Map[String, String]): Map[String, String] = {\n    if (tableExists(targetSnapshot)) {\n      assert(validatedOverrides.isEmpty,\n        \"Explicit overrides on Coordinated Commits configurations for existing tables\" +\n          \" are not supported, and should have been caught earlier.\")\n      CoordinatedCommitsUtils.getExplicitCCConfigurations(targetSnapshot.metadata.configuration)\n    } else {\n      if (validatedOverrides.nonEmpty) {\n        validatedOverrides\n      } else {\n        CoordinatedCommitsUtils.getDefaultCCConfigurations(spark)\n      }\n    }\n  }\n\n  /**\n   * Helper function to determine [[UCCommitCoordinatorClient.UC_TABLE_ID_KEY]]\n   * for the target table.\n   */\n  private def determineCatalogOwnedUCTableId(\n      targetSnapshot: SnapshotDescriptor): Map[String, String] = {\n    // For REPLACE TABLE command, extract the UC table ID from the target table\n    // if it exists.\n    if (tableExists(targetSnapshot)) {\n      targetSnapshot.metadata.configuration.filter { case (k, _) =>\n        k == UCCommitCoordinatorClient.UC_TABLE_ID_KEY\n      }\n    } else {\n      Map.empty\n    }\n  }\n\n  /**\n   * Determines the expected metadata of the target.\n   */\n  private def determineTargetMetadata(\n      spark: SparkSession,\n      targetSnapshot: SnapshotDescriptor,\n      opName: String) : Metadata = {\n    var metadata = prepareSourceMetadata(targetSnapshot, opName)\n    val validatedConfigurations = DeltaConfigs.validateConfigurations(tablePropertyOverrides)\n\n    // Finalize Coordinated Commits configurations for the target\n    val coordinatedCommitsConfigurationOverrides =\n      CoordinatedCommitsUtils.getExplicitCCConfigurations(validatedConfigurations)\n    val validatedConfigurationsWithoutCoordinatedCommits =\n      validatedConfigurations -- coordinatedCommitsConfigurationOverrides.keys\n    val finalCoordinatedCommitsConfigurations = determineCoordinatedCommitsConfigurations(\n      spark,\n      targetSnapshot,\n      coordinatedCommitsConfigurationOverrides)\n    val finalCatalogOwnedMetadata = finalCoordinatedCommitsConfigurations ++\n      determineCatalogOwnedUCTableId(targetSnapshot)\n\n    // Merge source configuration, table property overrides and coordinated-commits configurations.\n    metadata = metadata.copy(configuration =\n      metadata.configuration ++\n        validatedConfigurationsWithoutCoordinatedCommits ++\n        finalCatalogOwnedMetadata)\n\n    verifyMetadataInvariants(targetSnapshot, metadata)\n    metadata\n  }\n\n  /**\n   * Determines the final protocol of the target. The metadata of the `txn` must be updated before\n   * determining the protocol.\n   */\n  private def determineTargetProtocol(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      opName: String): Protocol = {\n    // Catalog-Owned: Do not copy over [[CatalogOwnedTableFeature]] from source table.\n    val sourceProtocol =\n      sourceTable.protocol.removeFeature(targetFeature = CatalogOwnedTableFeature)\n    // Pre-transaction version of the target table.\n    val targetProtocol = txn.snapshot.protocol\n    // Overriding properties during the CLONE can change the minimum required protocol for target.\n    // We need to look at the metadata of the transaction to see the entire set of table properties\n    // for the post-transaction state and decide a version based on that. We also need to re-add\n    // the table property overrides as table features set by it won't be in the transaction\n    // metadata anymore.\n    val validatedConfigurations = DeltaConfigs.validateConfigurations(tablePropertyOverrides)\n    // For CREATE CLONE, check the default spark configuration for Catalog-Owned.\n    val catalogOwnedEnabledByDefaultConf =\n      if (CatalogOwnedTableUtils.defaultCatalogOwnedEnabled(spark)\n          && !tableExists(txn.snapshot)) {\n        // Append [[CatalogOwnedTableFeature]] to the `configWithOverrides` below if table\n        // does not exist, to ensure the final target protocol contains CatalogOwned\n        // if enabled by default.\n        // Note: We need this because CatalogOwned is enabled through single protocol\n        //       without auxiliary metadata.\n        Map(s\"delta.feature.${CatalogOwnedTableFeature.name}\" -> \"supported\")\n      } else {\n        Map.empty\n      }\n    val configWithOverrides = txn.metadata.configuration ++ validatedConfigurations ++\n      catalogOwnedEnabledByDefaultConf\n    val metadataWithOverrides = txn.metadata.copy(configuration = configWithOverrides)\n    var (minReaderVersion, minWriterVersion, enabledFeatures) =\n      Protocol.minProtocolComponentsFromMetadata(spark, metadataWithOverrides)\n\n    // Only upgrade the protocol, never downgrade (unless allowed by flag), since that may break\n    // time travel.\n    val protocolDowngradeAllowed =\n    conf.getConf(DeltaSQLConf.RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED) ||\n      // It's not a real downgrade if the table doesn't exist before the CLONE.\n      !tableExists(txn.snapshot)\n\n    if (protocolDowngradeAllowed) {\n      minReaderVersion = minReaderVersion.max(sourceProtocol.minReaderVersion)\n      minWriterVersion = minWriterVersion.max(sourceProtocol.minWriterVersion)\n      val minProtocol = Protocol(minReaderVersion, minWriterVersion).withFeatures(enabledFeatures)\n      sourceProtocol.merge(minProtocol)\n    } else {\n      // Take the maximum of all protocol versions being merged to ensure that table features\n      // from table property overrides are correctly added to the table feature list or are only\n      // implicitly enabled\n      minReaderVersion =\n        Seq(targetProtocol.minReaderVersion, sourceProtocol.minReaderVersion, minReaderVersion).max\n      minWriterVersion = Seq(\n        targetProtocol.minWriterVersion, sourceProtocol.minWriterVersion, minWriterVersion).max\n      val minProtocol = Protocol(minReaderVersion, minWriterVersion).withFeatures(enabledFeatures)\n      targetProtocol.merge(sourceProtocol, minProtocol)\n    }\n  }\n}\n\nobject CloneTableBase extends Logging {\n\n  val SOURCE = \"source\"\n  val SOURCE_FORMAT = \"sourceFormat\"\n  val SOURCE_PATH = \"sourcePath\"\n  val SOURCE_VERSION = \"sourceVersion\"\n  val TARGET = \"target\"\n  val IS_REPLACE_DELTA = \"isReplaceDelta\"\n  val PARTITION_BY = \"partitionBy\"\n\n  /** Utility method returns the total size of all files in the given iterator */\n  private def totalDataSize(fileList: java.util.Iterator[AddFile]): Long = {\n    var totalSize = 0L\n    fileList.asScala.foreach { f =>\n      totalSize += f.size\n    }\n    totalSize\n  }\n}\n\n/**\n * Metrics of snapshot overwrite operation.\n * @param sourceSnapshotSizeInBytes Total size of the data in the source snapshot.\n * @param sourceSnapshotFileCount Number of data files in the source snapshot.\n * @param destSnapshotAddedFileCount Number of new data files added to the destination\n *                                   snapshot as part of the execution.\n * @param destSnapshotAddedFilesSizeInBytes Total size (in bytes) of the data files that were\n *                                          added to the destination snapshot.\n */\ncase class SnapshotOverwriteOperationMetrics(\n    sourceSnapshotSizeInBytes: Long,\n    sourceSnapshotFileCount: Long,\n    destSnapshotAddedFileCount: Long,\n    destSnapshotAddedFilesSizeInBytes: Long)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/CloneTableCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.FileNotFoundException\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, DeltaTimeTravelSpec, OptimisticTransaction, Snapshot}\nimport org.apache.spark.sql.delta.DeltaOperations.Clone\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol}\nimport org.apache.spark.sql.delta.actions.Protocol.extractAutomaticallyEnabledFeatures\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.convert.{ConvertTargetTable, ConvertUtils}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{Column, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference}\nimport org.apache.spark.sql.connector.catalog.Table\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{LongType, StructType}\nimport org.apache.spark.util.{Clock, SerializableConfiguration, SystemClock}\n// scalastyle:on import.ordering.noEmptyLine\n\n/**\n * Clones a Delta table to a new location with a new table id.\n * The clone can be performed as a shallow clone (i.e. shallow = true),\n * where we do not copy the files, but just point to them.\n * If a table exists at the given targetPath, that table will be replaced.\n *\n * @param sourceTable is the table to be cloned\n * @param targetIdent destination table identifier to clone to\n * @param tablePropertyOverrides user-defined table properties that should override any properties\n *                       with the same key from the source table\n * @param targetPath the actual destination\n */\ncase class CloneTableCommand(\n    sourceTable: CloneSource,\n    targetIdent: TableIdentifier,\n    tablePropertyOverrides: Map[String, String],\n    targetPath: Path\n) extends CloneTableBase(sourceTable, tablePropertyOverrides, targetPath) {\n\n  import CloneTableCommand._\n\n\n  /** Return the CLONE command output from the execution metrics */\n  override protected def getOutputSeq(operationMetrics: Map[String, Long]): Seq[Row] = {\n    Seq(Row(\n      operationMetrics.get(SOURCE_TABLE_SIZE),\n      operationMetrics.get(SOURCE_NUM_OF_FILES),\n      operationMetrics.get(NUM_REMOVED_FILES),\n      operationMetrics.get(NUM_COPIED_FILES),\n      operationMetrics.get(REMOVED_FILES_SIZE),\n      operationMetrics.get(COPIED_FILES_SIZE)\n    ))\n  }\n\n  /**\n   * Handles the transaction logic for the CLONE command.\n   * @param txn [[OptimisticTransaction]] to use for the commit to the target table.\n   * @param targetDeltaLog [[DeltaLog]] of the target table.\n   * @return\n   */\n  def handleClone(\n      sparkSession: SparkSession,\n      txn: OptimisticTransaction,\n      targetDeltaLog: DeltaLog,\n      commandMetrics: Option[Map[String, SQLMetric]] = None): Seq[Row] = {\n    if (!targetPath.isAbsolute) {\n      throw DeltaErrors.cloneOnRelativePath(targetIdent.toString)\n    }\n\n    /** Log clone command information */\n    logInfo(log\"Cloning ${MDC(DeltaLogKeys.CLONE_SOURCE_DESC, sourceTable.description)} to \" +\n      log\"${MDC(DeltaLogKeys.PATH, targetPath)}\")\n\n    // scalastyle:off deltahadoopconfiguration\n    val hdpConf = sparkSession.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    if (!sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_CLONE_REPLACE_ENABLED)) {\n      val targetFs = targetPath.getFileSystem(hdpConf)\n      try {\n        val subFiles = targetFs.listStatus(targetPath)\n        if (subFiles.nonEmpty) {\n          throw DeltaErrors.cloneReplaceUnsupported(targetIdent)\n        }\n      } catch {\n        case _: FileNotFoundException => // we want the path to not exist\n          targetFs.mkdirs(targetPath)\n      }\n    }\n\n    handleClone(\n      sparkSession,\n      txn,\n      targetDeltaLog,\n      hdpConf = hdpConf,\n      deltaOperation = Clone(\n        sourceTable.name, sourceTable.snapshot.map(_.version).getOrElse(-1)\n      ),\n      commandMetrics = commandMetrics)\n  }\n}\n\nobject CloneTableCommand {\n  // Names of the metrics - added to the Delta commit log as part of Clone transaction\n  val SOURCE_TABLE_SIZE = \"sourceTableSize\"\n  val SOURCE_NUM_OF_FILES = \"sourceNumOfFiles\"\n  val NUM_REMOVED_FILES = \"numRemovedFiles\"\n  val NUM_COPIED_FILES = \"numCopiedFiles\"\n  val REMOVED_FILES_SIZE = \"removedFilesSize\"\n  val COPIED_FILES_SIZE = \"copiedFilesSize\"\n\n  // SQL way column names for metrics in command execution output\n  private val COLUMN_SOURCE_TABLE_SIZE = \"source_table_size\"\n  private val COLUMN_SOURCE_NUM_OF_FILES = \"source_num_of_files\"\n  private val COLUMN_NUM_REMOVED_FILES = \"num_removed_files\"\n  private val COLUMN_NUM_COPIED_FILES = \"num_copied_files\"\n  private val COLUMN_REMOVED_FILES_SIZE = \"removed_files_size\"\n  private val COLUMN_COPIED_FILES_SIZE = \"copied_files_size\"\n\n  val output: Seq[Attribute] = Seq(\n    AttributeReference(COLUMN_SOURCE_TABLE_SIZE, LongType)(),\n    AttributeReference(COLUMN_SOURCE_NUM_OF_FILES, LongType)(),\n    AttributeReference(COLUMN_NUM_REMOVED_FILES, LongType)(),\n    AttributeReference(COLUMN_NUM_COPIED_FILES, LongType)(),\n    AttributeReference(COLUMN_REMOVED_FILES_SIZE, LongType)(),\n    AttributeReference(COLUMN_COPIED_FILES_SIZE, LongType)()\n  )\n}\n\n/** A delta table source to be cloned from */\nclass CloneDeltaSource(\n  sourceTable: DeltaTableV2) extends CloneSource {\n\n  private val deltaLog = sourceTable.deltaLog\n  private val sourceSnapshot = sourceTable.initialSnapshot\n\n  def format: String = CloneSourceFormat.DELTA\n\n  def protocol: Protocol = sourceSnapshot.protocol\n\n  def clock: Clock = deltaLog.clock\n\n  def name: String = sourceTable.name()\n\n  def dataPath: Path = deltaLog.dataPath\n\n  def schema: StructType = sourceTable.schema()\n\n  def catalogTable: Option[CatalogTable] = sourceTable.catalogTable\n\n  def timeTravelOpt: Option[DeltaTimeTravelSpec] = sourceTable.timeTravelOpt\n\n  def snapshot: Option[Snapshot] = Some(sourceSnapshot)\n\n  def metadata: Metadata = sourceSnapshot.metadata\n\n  def allFiles: Dataset[AddFile] = sourceSnapshot.allFiles\n\n  def sizeInBytes: Long = sourceSnapshot.sizeInBytes\n\n  def numOfFiles: Long = sourceSnapshot.numOfFiles\n\n  def description: String = s\"${format} table ${name} at version ${sourceSnapshot.version}\"\n\n  override def close(): Unit = {}\n}\n\n/** A convertible non-delta table source to be cloned from */\nabstract class CloneConvertedSource(spark: SparkSession) extends CloneSource {\n\n  // The converter which produces delta metadata from non-delta table, child class must implement\n  // this converter.\n  protected def convertTargetTable: ConvertTargetTable\n\n  def format: String = CloneSourceFormat.UNKNOWN\n\n  def protocol: Protocol = {\n    // This is quirky but necessary to add table features such as column mapping if the default\n    // protocol version supports table features.\n    Protocol().withFeatures(extractAutomaticallyEnabledFeatures(spark, metadata, Protocol()))\n  }\n\n  override val clock: Clock = new SystemClock()\n\n  def dataPath: Path = new Path(convertTargetTable.fileManifest.basePath)\n\n  def schema: StructType = convertTargetTable.tableSchema\n\n  def timeTravelOpt: Option[DeltaTimeTravelSpec] = None\n\n  def snapshot: Option[Snapshot] = None\n\n  override lazy val metadata: Metadata = {\n    val conf = catalogTable\n      // Hive adds some transient table properties which should be ignored\n      .map(_.properties.filterKeys(_ != \"transient_lastDdlTime\").toMap)\n      .foldRight(convertTargetTable.properties.toMap)(_ ++ _)\n\n    {\n      Metadata(\n        schemaString = convertTargetTable.tableSchema.json,\n        partitionColumns = convertTargetTable.partitionSchema.fieldNames,\n        configuration = conf,\n        createdTime = Some(System.currentTimeMillis()))\n    }\n  }\n\n  override lazy val allFiles: Dataset[AddFile] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    // scalastyle:off deltahadoopconfiguration\n    val serializableConf = new SerializableConfiguration(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    val baseDir = dataPath.toString\n    val conf = spark.sparkContext.broadcast(serializableConf)\n    val partitionSchema = convertTargetTable.partitionSchema\n    val enforceRelativePathCheck = enforceRelativePath\n\n    {\n      convertTargetTable.fileManifest.allFiles.mapPartitions { targetFile =>\n        val basePath = new Path(baseDir)\n        val fs = basePath.getFileSystem(conf.value.value)\n        targetFile.map(ConvertUtils.createAddFile(\n          _, basePath, fs, SQLConf.get, Some(partitionSchema), !enforceRelativePathCheck))\n      }\n    }\n  }\n\n  /**\n   * All data file paths are required to be relative to the table path when true\n   */\n  def enforceRelativePath: Boolean = true\n\n  def sizeInBytes: Long = convertTargetTable.sizeInBytes\n\n  def numOfFiles: Long = convertTargetTable.numFiles\n\n  def description: String = s\"${format} table ${name}\"\n\n  override def close(): Unit = convertTargetTable.fileManifest.close()\n}\n\n/**\n * A parquet table source to be cloned from\n */\ncase class CloneParquetSource(\n    tableIdentifier: TableIdentifier,\n    override val catalogTable: Option[CatalogTable],\n    spark: SparkSession) extends CloneConvertedSource(spark) {\n\n  override lazy val convertTargetTable: ConvertTargetTable = {\n    val baseDir = catalogTable.map(_.location.toString).getOrElse(tableIdentifier.table)\n    ConvertUtils.getParquetTable(spark, baseDir, catalogTable, None)\n  }\n\n  override def format: String = CloneSourceFormat.PARQUET\n\n  override def name: String = catalogTable.map(_.identifier.unquotedString)\n    .getOrElse(s\"parquet.`${tableIdentifier.table}`\")\n}\n\ncase class TablePolicies(\n    hasRowPolicies: Boolean,\n    hasColumnPolicies: Boolean\n)\n\n/**\n * A iceberg table source to be cloned from\n */\ncase class CloneIcebergSource(\n  metadataLocation: String,\n  tableNameOpt: Option[String],\n  tablePoliciesOpt: Option[TablePolicies],\n  deltaSnapshotOpt: Option[Snapshot],\n  spark: SparkSession) extends CloneConvertedSource(spark) {\n\n  override lazy val convertTargetTable: ConvertTargetTable =\n    ConvertUtils.getIcebergTable(spark, metadataLocation, deltaSnapshotOpt)\n\n  override def format: String = CloneSourceFormat.ICEBERG\n\n  override def name: String =\n    tableNameOpt.getOrElse(s\"iceberg.`$metadataLocation`\")\n\n  override def catalogTable: Option[CatalogTable] = None\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/ConvertToDeltaCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.Closeable\nimport java.lang.reflect.InvocationTargetException\nimport java.util.Locale\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.VacuumCommand.{generateCandidateFileMap, getTouchedFile}\nimport org.apache.spark.sql.delta.commands.convert.{ConvertTargetFileManifest, ConvertTargetTable, ConvertUtils}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.util._\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{AnalysisException, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{Analyzer, NoSuchTableException}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType, SessionCatalog}\nimport org.apache.spark.sql.connector.catalog.{Identifier, TableCatalog, V1Table}\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Convert an existing parquet table to a delta table by creating delta logs based on\n * existing files. Here are the main components:\n *\n *   - File Listing:      Launch a spark job to list files from a given directory in parallel.\n *\n *   - Schema Inference:  Given an iterator on the file list result, we group the iterator into\n *                        sequential batches and launch a spark job to infer schema for each batch,\n *                        and finally merge schemas from all batches.\n *\n *   - Stats collection:  Again, we group the iterator on file list results into sequential batches\n *                        and launch a spark job to collect stats for each batch.\n *\n *   - Commit the files:  We take the iterator of files with stats and write out a delta\n *                        log file as the first commit. This bypasses the transaction protocol, but\n *                        it's ok as this would be the very first commit.\n *\n * @param tableIdentifier the target parquet table.\n * @param partitionSchema the partition schema of the table, required when table is partitioned.\n * @param collectStats Should collect column stats per file on convert.\n * @param deltaPath if provided, the delta log will be written to this location.\n */\nabstract class ConvertToDeltaCommandBase(\n    tableIdentifier: TableIdentifier,\n    partitionSchema: Option[StructType],\n    collectStats: Boolean,\n    deltaPath: Option[String]) extends LeafRunnableCommand with DeltaCommand {\n\n  protected lazy val statsEnabled: Boolean = conf.getConf(DeltaSQLConf.DELTA_COLLECT_STATS)\n\n  protected lazy val icebergEnabled: Boolean =\n    conf.getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_ENABLED)\n\n  override lazy val metrics: Map[String, SQLMetric] = Map (\n    \"numConvertedFiles\" ->\n      SQLMetrics.createMetric(SparkContext.getOrCreate(), \"number of files converted\")\n  )\n\n  protected def isParquetPathProvider(provider: String): Boolean =\n    provider.equalsIgnoreCase(\"parquet\")\n\n  protected def isIcebergPathProvider(provider: String): Boolean =\n    icebergEnabled && provider.equalsIgnoreCase(\"iceberg\")\n\n  protected def isSupportedPathTableProvider(provider: String): Boolean = {\n    isParquetPathProvider(provider) || isIcebergPathProvider(provider)\n  }\n\n  override def run(spark: SparkSession): Seq[Row] = {\n    val convertProperties = resolveConvertTarget(spark, tableIdentifier) match {\n      case Some(props) if !DeltaSourceUtils.isDeltaTable(props.provider) => props\n      case _ =>\n        // Make convert to delta idempotent\n        logConsole(\"The table you are trying to convert is already a delta table\")\n        return Seq.empty[Row]\n    }\n\n    val targetTable = getTargetTable(spark, convertProperties)\n    val deltaPathToUse = new Path(deltaPath.getOrElse(convertProperties.targetDir))\n    val deltaLog = DeltaLog.forTable(spark, deltaPathToUse)\n    val txn = deltaLog.startTransaction(convertProperties.catalogTable)\n    if (txn.readVersion > -1) {\n      handleExistingTransactionLog(spark, txn, convertProperties, targetTable.format)\n      return Seq.empty[Row]\n    }\n\n    performConvert(spark, txn, convertProperties, targetTable)\n  }\n\n  /** Given the table identifier, figure out what our conversion target is. */\n  private def resolveConvertTarget(\n      spark: SparkSession,\n      tableIdentifier: TableIdentifier): Option[ConvertTarget] = {\n    val v2SessionCatalog =\n      spark.sessionState.catalogManager.v2SessionCatalog.asInstanceOf[TableCatalog]\n\n    // TODO: Leverage the analyzer for all this work\n    if (isCatalogTable(spark.sessionState.analyzer, tableIdentifier)) {\n      val namespace =\n          tableIdentifier.database.map(Array(_))\n            .getOrElse(spark.sessionState.catalogManager.currentNamespace)\n      val ident = Identifier.of(namespace, tableIdentifier.table)\n      v2SessionCatalog.loadTable(ident) match {\n        case v1: V1Table if v1.catalogTable.tableType == CatalogTableType.VIEW =>\n          throw DeltaErrors.operationNotSupportedException(\n            \"Converting a view to a Delta table\",\n            tableIdentifier)\n        case v1: V1Table =>\n          val table = v1.catalogTable\n          // Hive adds some transient table properties which should be ignored\n          val props = table.properties.filterKeys(_ != \"transient_lastDdlTime\").toMap\n          Some(ConvertTarget(Some(table), table.provider, new Path(table.location).toString, props))\n        case _: DeltaTableV2 =>\n          // Already a Delta table\n          None\n        case other =>\n          throw DeltaErrors.operationNotSupportedException(\n            s\"Converting an unsupported table type $other to a Delta table\",\n            tableIdentifier)\n      }\n    } else {\n      Some(ConvertTarget(\n        None,\n        tableIdentifier.database,\n        tableIdentifier.table,\n        Map.empty[String, String]))\n    }\n  }\n\n  /**\n   * When converting a table to delta using table name, we should also change the metadata in the\n   * catalog table because the delta log should be the source of truth for the metadata rather than\n   * the metastore.\n   *\n   * @param catalogTable metadata of the table to be converted\n   * @param sessionCatalog session catalog of the metastore used to update the metadata\n   */\n  private def convertMetadata(\n      catalogTable: CatalogTable,\n      sessionCatalog: SessionCatalog): Unit = {\n    var newCatalog = catalogTable.copy(\n      provider = Some(\"delta\"),\n      // TODO: Schema changes unfortunately doesn't get reflected in the HiveMetaStore. Should be\n      // fixed in Apache Spark\n      schema = new StructType(),\n      partitionColumnNames = Seq.empty,\n      properties = Map.empty,\n      // TODO: Serde information also doesn't get removed\n      storage = catalogTable.storage.copy(\n        inputFormat = None,\n        outputFormat = None,\n        serde = None)\n    )\n    sessionCatalog.alterTable(newCatalog)\n    logInfo(log\"Convert to Delta converted metadata\")\n  }\n\n  /**\n   * Calls DeltaCommand.isCatalogTable. With Convert, we may get a format check error in cases where\n   * the metastore and the underlying table don't align, e.g. external table where the underlying\n   * files are converted to delta but the metadata has not been converted yet. In these cases,\n   * catch the error and return based on whether the provided Table Identifier could reasonably be\n   * a path\n   *\n   * @param analyzer The session state analyzer to call\n   * @param tableIdent Table Identifier to determine whether is path based or not\n   * @return Boolean where true means that the table is a table in a metastore and false means the\n   *         table is a path based table\n   */\n  override def isCatalogTable(analyzer: Analyzer, tableIdent: TableIdentifier): Boolean = {\n    try {\n      super.isCatalogTable(analyzer, tableIdentifier)\n    } catch {\n      case e: AnalysisException if e.getMessage.contains(\"Incompatible format detected\") =>\n        !isPathIdentifier(tableIdentifier)\n      case e: AssertionError if e.getMessage.contains(\"Conflicting directory structures\") =>\n        !isPathIdentifier(tableIdentifier)\n      case _: NoSuchTableException\n          if tableIdent.database.isEmpty && new Path(tableIdent.table).isAbsolute =>\n        throw DeltaErrors.missingProviderForConvertException(tableIdent.table)\n    }\n  }\n\n  /**\n   * Override this method since parquet paths are valid for Convert\n   *\n   * @param tableIdent the provided table or path\n   * @return Whether or not the ident provided can refer to a table by path\n   */\n  override def isPathIdentifier(tableIdent: TableIdentifier): Boolean = {\n    val provider = tableIdent.database.getOrElse(\"\")\n    // If db doesnt exist or db is called delta/tahoe then check if path exists\n    (DeltaSourceUtils.isDeltaDataSourceName(provider) ||\n      isSupportedPathTableProvider(provider)) &&\n      new Path(tableIdent.table).isAbsolute\n  }\n\n  /**\n   * If there is already a transaction log we should handle what happens when convert to delta is\n   * run once again. It may be the case that the table is entirely converted i.e. the underlying\n   * files AND the catalog (if one exists) are updated. Or it may be the case that the table is\n   * partially converted i.e. underlying files are converted but catalog (if one exists)\n   * has not been updated.\n   *\n   * @param spark spark session to get session catalog\n   * @param txn existing transaction log\n   * @param target properties that contains: the provider and the catalogTable when\n   * converting using table name\n   */\n  private def handleExistingTransactionLog(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      target: ConvertTarget,\n      sourceFormat: String): Unit = {\n    // In the case that the table is a delta table but the provider has not been updated we should\n    // update table metadata to reflect that the table is a delta table and table properties should\n    // also be updated\n    if (isParquetCatalogTable(target)) {\n      val catalogTable = target.catalogTable\n      val tableProps = target.properties\n      val deltaLogConfig = txn.metadata.configuration\n      val mergedConfig = deltaLogConfig ++ tableProps\n\n      if (mergedConfig != deltaLogConfig) {\n        if (deltaLogConfig.nonEmpty &&\n            conf.getConf(DeltaSQLConf.DELTA_CONVERT_METADATA_CHECK_ENABLED)) {\n          throw DeltaErrors.convertMetastoreMetadataMismatchException(tableProps, deltaLogConfig)\n        }\n        val newMetadata = txn.metadata.copy(\n          configuration = mergedConfig\n        )\n        txn.commit(\n          newMetadata :: Nil,\n          DeltaOperations.Convert(\n            numFiles = 0L,\n            partitionSchema.map(_.fieldNames.toSeq).getOrElse(Nil),\n            collectStats = false,\n            catalogTable = catalogTable.map(t => t.identifier.toString),\n            sourceFormat = Some(sourceFormat)\n          ))\n      }\n      convertMetadata(\n        catalogTable.get,\n        spark.sessionState.catalog\n      )\n    } else {\n      logConsole(\"The table you are trying to convert is already a delta table\")\n    }\n  }\n\n  /** Is the target table a parquet table defined in an external catalog. */\n  private def isParquetCatalogTable(target: ConvertTarget): Boolean = {\n    target.catalogTable match {\n      case Some(ct) =>\n        ConvertToDeltaCommand.isHiveStyleParquetTable(ct) ||\n          target.provider.get.toLowerCase(Locale.ROOT) == \"parquet\"\n      case None => false\n    }\n  }\n\n  protected def performStatsCollection(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      addFiles: Seq[AddFile]): Iterator[AddFile] = {\n    val dummySnapshot = new DummySnapshot(txn.deltaLog.logPath, txn.deltaLog, txn.metadata)\n    ConvertToDeltaCommand.computeStats(txn.deltaLog, dummySnapshot, addFiles)\n  }\n\n  /**\n   * Given the file manifest, create corresponding AddFile actions for the entire list of files.\n   */\n  protected def createDeltaActions(\n      spark: SparkSession,\n      manifest: ConvertTargetFileManifest,\n      partitionSchema: StructType,\n      txn: OptimisticTransaction,\n      fs: FileSystem): Iterator[AddFile] = {\n    val shouldCollectStats = collectStats && statsEnabled\n    val statsBatchSize = conf.getConf(DeltaSQLConf.DELTA_IMPORT_BATCH_SIZE_STATS_COLLECTION)\n    var numFiles = 0L\n    manifest.getFiles.grouped(statsBatchSize).flatMap { batch =>\n      val adds = batch.map(\n        ConvertUtils.createAddFile(\n          _, txn.deltaLog.dataPath, fs, conf, Some(partitionSchema), deltaPath.isDefined))\n      if (shouldCollectStats) {\n        logInfo(\n          log\"Collecting stats for a batch of \" +\n          log\"${MDC(DeltaLogKeys.NUM_FILES, batch.size.toLong)} files; \" +\n          log\"finished ${MDC(DeltaLogKeys.NUM_FILES2, numFiles)} so far\"\n          )\n        numFiles += statsBatchSize\n        performStatsCollection(spark, txn, adds)\n      } else if (collectStats) {\n        logWarning(log\"collectStats is set to true but ${MDC(DeltaLogKeys.CONFIG_KEY,\n          DeltaSQLConf.DELTA_COLLECT_STATS.key)}\" +\n          log\" is false. Skip statistics collection\")\n        adds.toIterator\n      } else {\n        adds.toIterator\n      }\n    }\n  }\n\n  /** Get the instance of the convert target table, which provides file manifest and schema */\n  protected def getTargetTable(spark: SparkSession, target: ConvertTarget): ConvertTargetTable = {\n    target.provider match {\n      case Some(providerName) => providerName.toLowerCase(Locale.ROOT) match {\n        case checkProvider\n          if target.catalogTable.exists(ConvertToDeltaCommand.isHiveStyleParquetTable) ||\n            isParquetPathProvider(checkProvider) =>\n          ConvertUtils.getParquetTable(\n            spark, target.targetDir, target.catalogTable, partitionSchema)\n        case checkProvider if isIcebergPathProvider(checkProvider) =>\n          if (partitionSchema.isDefined) {\n            throw DeltaErrors.partitionSchemaInIcebergTables\n          }\n          ConvertUtils.getIcebergTable(\n            spark, target.targetDir, deltaSnapshotOpt = None, collectStats\n          )\n        case other =>\n          throw DeltaErrors.convertNonParquetTablesException(tableIdentifier, other)\n      }\n      case None =>\n        throw DeltaErrors.missingProviderForConvertException(target.targetDir)\n    }\n  }\n\n  /**\n   * Converts the given table to a Delta table. First gets the file manifest for the table. Then\n   * in the first pass, it infers the schema of the table. Then in the second pass, it generates\n   * the relevant Actions for Delta's transaction log, namely the AddFile actions for each file\n   * in the manifest. Once a commit is made, updates an external catalog, e.g. Hive MetaStore,\n   * if this table was referenced through a table in a catalog.\n   */\n  private def performConvert(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      convertProperties: ConvertTarget,\n      targetTable: ConvertTargetTable): Seq[Row] =\n    recordDeltaOperation(txn.deltaLog, \"delta.convert\") {\n    txn.deltaLog.createLogDirectoriesIfNotExists()\n    val targetPath = new Path(convertProperties.targetDir)\n    // scalastyle:off deltahadoopconfiguration\n    val sessionHadoopConf = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    val fs = targetPath.getFileSystem(sessionHadoopConf)\n    val manifest = targetTable.fileManifest\n    try {\n      if (!manifest.getFiles.hasNext) {\n        throw DeltaErrors.emptyDirectoryException(convertProperties.targetDir)\n      }\n\n      val partitionFields = targetTable.partitionSchema\n      val schema = targetTable.tableSchema\n      val metadata = Metadata(\n        schemaString = schema.json,\n        partitionColumns = partitionFields.fieldNames,\n        configuration = convertProperties.properties ++ targetTable.properties,\n        createdTime = Some(System.currentTimeMillis()))\n      txn.updateMetadataForNewTable(metadata)\n\n      checkConversionIsAllowed(txn, targetTable)\n\n      val numFiles = targetTable.numFiles\n      val addFilesIter = createDeltaActions(spark, manifest, partitionFields, txn, fs)\n      val transactionMetrics = Map[String, String](\n        \"numConvertedFiles\" -> numFiles.toString\n      )\n      metrics(\"numConvertedFiles\") += numFiles\n      sendDriverMetrics(spark, metrics)\n      txn.commitLarge(\n        spark,\n        addFilesIter,\n        Some(txn.protocol),\n        getOperation(numFiles, convertProperties, targetTable.format),\n        getContext,\n        transactionMetrics)\n    } finally {\n      manifest.close()\n    }\n\n    // If there is a catalog table, convert metadata\n    if (convertProperties.catalogTable.isDefined) {\n      convertMetadata(\n        convertProperties.catalogTable.get,\n        spark.sessionState.catalog\n      )\n    }\n\n    Seq.empty[Row]\n  }\n\n  protected def getContext: Map[String, String] = {\n    Map.empty\n  }\n\n  /** Get the operation to store in the commit message. */\n  protected def getOperation(\n      numFilesConverted: Long,\n      convertProperties: ConvertTarget,\n      sourceFormat: String): DeltaOperations.Operation = {\n    DeltaOperations.Convert(\n      numFilesConverted,\n      partitionSchema.map(_.fieldNames.toSeq).getOrElse(Nil),\n      collectStats = collectStats && statsEnabled,\n      convertProperties.catalogTable.map(t => t.identifier.toString),\n      sourceFormat = Some(sourceFormat))\n  }\n\n  protected case class ConvertTarget(\n      catalogTable: Option[CatalogTable],\n      provider: Option[String],\n      targetDir: String,\n      properties: Map[String, String])\n\n  private def checkColumnMapping(\n      txnMetadata: Metadata,\n      convertTargetTable: ConvertTargetTable): Unit = {\n    if (convertTargetTable.requiredColumnMappingMode != txnMetadata.columnMappingMode) {\n      throw DeltaErrors.convertToDeltaWithColumnMappingNotSupported(txnMetadata.columnMappingMode)\n    }\n  }\n\n  /** Check if the conversion is allowed. */\n  private def checkConversionIsAllowed(\n      txn: OptimisticTransaction,\n      targetTable: ConvertTargetTable): Unit = {\n    // TODO: we have not decided on how to implement CONVERT TO DELTA under column mapping modes\n    //  for some convert targets so we block this feature for them here\n    checkColumnMapping(txn.metadata, targetTable)\n    RowTracking.checkStatsCollectedIfRowTrackingSupported(\n      txn.protocol,\n      collectStats,\n      statsEnabled)\n  }\n}\n\ncase class ConvertToDeltaCommand(\n    tableIdentifier: TableIdentifier,\n    partitionSchema: Option[StructType],\n    collectStats: Boolean,\n    deltaPath: Option[String])\n  extends ConvertToDeltaCommandBase(tableIdentifier, partitionSchema, collectStats, deltaPath)\n\nobject ConvertToDeltaCommand extends DeltaLogging {\n\n  def isHiveStyleParquetTable(catalogTable: CatalogTable): Boolean = {\n    catalogTable.provider.contains(\"hive\") && catalogTable.storage.serde.contains(\n      \"org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe\")\n  }\n\n  def computeStats(\n      deltaLog: DeltaLog,\n      snapshot: Snapshot,\n      addFiles: Seq[AddFile]): Iterator[AddFile] = {\n    import org.apache.spark.sql.functions._\n    val filesWithStats = deltaLog.createDataFrame(snapshot, addFiles)\n      .groupBy(input_file_name()).agg(to_json(snapshot.statsCollector))\n\n    val pathToAddFileMap = generateCandidateFileMap(deltaLog.dataPath, addFiles)\n    filesWithStats.collect().iterator.map { row =>\n      val addFile = getTouchedFile(deltaLog.dataPath, row.getString(0), pathToAddFileMap)\n      addFile.copy(stats = row.getString(1))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/CreateDeltaTableCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.concurrent.TimeUnit\n\nimport scala.util.Try\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.constraints.Constraints\nimport org.apache.spark.sql.delta.DeltaColumnMapping.filterColumnMappingProperties\nimport org.apache.spark.sql.delta.actions.{Action, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.actions.DomainMetadata\nimport org.apache.spark.sql.delta.commands.DMLUtils.TaggedCommitData\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CoordinatedCommitsUtils}\nimport org.apache.spark.sql.delta.hooks.{HudiConverterHook, IcebergConverterHook}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.execution.command.{LeafRunnableCommand, RunnableCommand}\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.execution.metric.SQLMetrics.createMetric\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\n\n/**\n * Single entry point for all write or declaration operations for Delta tables accessed through\n * the table name.\n *\n * @param table `CatalogTable` object representing the table to create\n * @param existingTableOpt The existing table for the same identifier if exists\n * @param mode The save mode when writing data. Relevant when the query is empty or set to Ignore\n *             with `CREATE TABLE IF NOT EXISTS`.\n * @param query The query to commit into the Delta table if it exist. This can come from\n *                - CTAS\n *                - saveAsTable\n * @param operation The table creation mode\n * @param tableByPath Whether the table is identified by path\n * @param output SQL output of the command\n * @param protocol This is used to create a table with specific protocol version\n * @param allowCatalogManaged This is used to create UC managed table with catalogManaged feature\n * @param createTableFunc If specified, call this function to create the table, instead of\n *                        Spark `SessionCatalog#createTable` which is backed by Hive Metastore.\n */\ncase class CreateDeltaTableCommand(\n    override val table: CatalogTable,\n    override val existingTableOpt: Option[CatalogTable],\n    override val mode: SaveMode,\n    query: Option[LogicalPlan],\n    override val operation: TableCreationModes.CreationMode = TableCreationModes.Create,\n    override val tableByPath: Boolean = false,\n    override val output: Seq[Attribute] = Nil,\n    protocol: Option[Protocol] = None,\n    override val allowCatalogManaged: Boolean = false,\n    createTableFunc: Option[CatalogTable => Unit] = None)\n  extends LeafRunnableCommand\n  with DeltaCommand\n  with DeltaLogging\n  with CreateDeltaTableLike {\n\n  @transient\n  private lazy val sc: SparkContext = SparkContext.getOrCreate()\n\n  override lazy val metrics = Map[String, SQLMetric](\n    \"numCopiedFiles\" -> createMetric(sc, \"number of files copied\"),\n    \"copiedFilesSize\" -> createMetric(sc, \"size of files copied\"),\n    \"executionTimeMs\" -> createMetric(sc, \"time taken to execute the entire operation\"),\n    \"numRemovedBytes\" -> createMetric(sc, \"number of bytes removed\"),\n    \"removedFilesSize\" -> createMetric(sc, \"size of files removed\"),\n    \"sourceTableSize\" -> createMetric(sc, \"size of source table\"),\n    \"numOutputRows\" -> createMetric(sc, \"number of output rows\"),\n    \"numParts\" -> createMetric(sc, \"number of partitions\"),\n    \"numFiles\" -> createMetric(sc, \"number of written files\"),\n    \"sourceNumOfFiles\" -> createMetric(sc, \"number of files in source table\"),\n    \"numRemovedFiles\" -> createMetric(sc, \"number of files removed.\"),\n    \"numOutputBytes\" -> createMetric(sc, \"number of output bytes\"),\n    \"taskCommitTime\" -> createMetric(sc, \"task commit time\"),\n    \"jobCommitTime\" -> createMetric(sc, \"job commit time\"),\n    \"numOfSyncedTransactions\" -> createMetric(sc, \"number of synced transactions\")\n  )\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n\n    assert(table.tableType != CatalogTableType.VIEW)\n    assert(table.identifier.database.isDefined, \"Database should've been fixed at analysis\")\n    // There is a subtle race condition here, where the table can be created by someone else\n    // while this command is running. Nothing we can do about that though :(\n    val tableExistsInCatalog = existingTableOpt.isDefined\n    if (mode == SaveMode.Ignore && tableExistsInCatalog) {\n      // Early exit on ignore\n      return Nil\n    } else if (mode == SaveMode.ErrorIfExists && tableExistsInCatalog) {\n      throw DeltaErrors.tableAlreadyExists(table)\n    }\n    // This check should be relaxed once the UC client supports creating tables,\n    // It gets bypassed in UTs to allow tests that use InMemoryCommitCoordinator to create tables\n    val tableFeatures = TableFeatureProtocolUtils.\n      getSupportedFeaturesFromTableConfigs(table.properties)\n    if (!Utils.isTesting && !allowCatalogManaged &&\n      (tableFeatures.contains(CatalogOwnedTableFeature) ||\n      CatalogOwnedTableUtils.defaultCatalogOwnedEnabled(spark = sparkSession))) {\n      throw DeltaErrors.deltaCannotCreateCatalogManagedTable()\n    }\n\n    val tableWithLocation = getCatalogTableWithLocation(sparkSession)\n\n    val tableLocation = getDeltaTablePath(tableWithLocation)\n    // To be safe, here we only extract file system options from table storage properties, to create\n    // the DeltaLog.\n    val fileSystemOptions = table.storage.properties.filter { case (k, _) =>\n      DeltaTableUtils.validDeltaTableHadoopPrefixes.exists(k.startsWith)\n    }\n    val deltaLog = DeltaUtils.getDeltaLogFromTableOrPath(\n      sparkSession, existingTableOpt, tableLocation, fileSystemOptions)\n    CoordinatedCommitsUtils.validateConfigurationsForCreateDeltaTableCommand(\n      sparkSession, deltaLog.tableExists, query, tableWithLocation.properties)\n    CatalogOwnedTableUtils.validatePropertiesForCreateDeltaTableCommand(\n      spark = sparkSession,\n      tableExists = deltaLog.tableExists,\n      query = query,\n      catalogTableProperties = tableWithLocation.properties,\n      existingTableSnapshotOpt =\n        if (deltaLog.tableExists) Some(deltaLog.unsafeVolatileSnapshot) else None)\n\n    recordDeltaOperation(deltaLog, \"delta.ddl.createTable\") {\n      val result = handleCommit(sparkSession, deltaLog, tableWithLocation)\n      sendDriverMetrics(sparkSession, metrics)\n      result\n    }\n  }\n\n  /**\n   * Handles the transaction logic for the command. Returns the operation metrics in case of CLONE.\n   */\n  private def handleCommit(\n      sparkSession: SparkSession,\n      deltaLog: DeltaLog,\n      tableWithLocation: CatalogTable): Seq[Row] = {\n    val tableExistsInCatalog = existingTableOpt.isDefined\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n    val tableLocation = getDeltaTablePath(tableWithLocation)\n    val fs = tableLocation.getFileSystem(hadoopConf)\n\n    def checkPathEmpty(txn: OptimisticTransaction): Unit = {\n      // Verify the table does not exist.\n      if (mode == SaveMode.Ignore || mode == SaveMode.ErrorIfExists) {\n        // We should have returned earlier in Ignore and ErrorIfExists mode if the table\n        // is already registered in the catalog.\n        assert(!tableExistsInCatalog)\n        // Verify that the data path does not contain any data.\n        // We may have failed a previous write. The retry should still succeed even if we have\n        // garbage data\n        if (txn.readVersion > -1 || !fs.exists(deltaLog.logPath)) {\n          assertPathEmpty(hadoopConf, tableWithLocation)\n        }\n      }\n    }\n\n    var txn = startTxnForTableCreation(sparkSession, deltaLog, tableWithLocation)\n\n    OptimisticTransaction.withActive(txn) {\n      val result = query match {\n        // CLONE handled separately from other CREATE TABLE syntax\n        case Some(cmd: CloneTableCommand) =>\n          checkPathEmpty(txn)\n          cmd.handleClone(\n            sparkSession,\n            txn,\n            targetDeltaLog = deltaLog,\n            commandMetrics = Some(metrics))\n        case Some(deltaWriter: WriteIntoDeltaLike) =>\n          checkPathEmpty(txn)\n          txn = handleCreateTableAsSelect(\n            sparkSession, txn, deltaLog, deltaWriter, tableWithLocation)\n          Nil\n        case Some(query) =>\n          checkPathEmpty(txn)\n          require(!query.isInstanceOf[RunnableCommand])\n          // When using V1 APIs, the `query` plan is not yet optimized, therefore, it is safe\n          // to once again go through analysis\n          val data = DataFrameUtils.ofRows(sparkSession, query)\n          val options = new DeltaOptions(table.storage.properties, sparkSession.sessionState.conf)\n          val deltaWriter = WriteIntoDelta(\n            deltaLog = deltaLog,\n            mode = mode,\n            options,\n            partitionColumns = table.partitionColumnNames,\n            configuration = tableWithLocation.properties + (\"comment\" -> table.comment.orNull),\n            data = data,\n            Some(tableWithLocation))\n          txn = handleCreateTableAsSelect(\n            sparkSession, txn, deltaLog, deltaWriter, tableWithLocation)\n          Nil\n        case _ =>\n          handleCreateTable(sparkSession, txn, tableWithLocation, fs, hadoopConf)\n          Nil\n      }\n\n      runPostCommitUpdates(sparkSession, txn, deltaLog, tableWithLocation)\n\n      result\n    }\n  }\n\n  /**\n   * Runs updates post table creation commit, such as updating the catalog\n   * with relevant information.\n   */\n  private def runPostCommitUpdates(\n      sparkSession: SparkSession,\n      txnUsedForCommit: OptimisticTransaction,\n      deltaLog: DeltaLog,\n      tableWithLocation: CatalogTable): Unit = {\n    // Note that someone may have dropped and recreated the table in a separate location in the\n    // meantime... Unfortunately we can't do anything there at the moment, because Hive sucks.\n    logInfo(log\"Table is path-based table: ${MDC(DeltaLogKeys.IS_PATH_TABLE, tableByPath)}. \" +\n      log\"Update catalog with mode: ${MDC(DeltaLogKeys.OPERATION, operation)}\")\n    val opStartTs = TimeUnit.NANOSECONDS.toMillis(txnUsedForCommit.txnStartTimeNs)\n    val postCommitSnapshot = deltaLog.update(\n      checkIfUpdatedSinceTs = Some(opStartTs),\n      catalogTableOpt = Some(tableWithLocation))\n    val didNotChangeMetadata = txnUsedForCommit.metadata == txnUsedForCommit.snapshot.metadata\n    updateCatalog(\n      sparkSession,\n      tableWithLocation,\n      postCommitSnapshot,\n      query,\n      didNotChangeMetadata,\n      createTableFunc)\n\n\n\n    if (UniversalFormat.hudiEnabled(postCommitSnapshot.metadata) &&\n        !txnUsedForCommit.containsPostCommitHook(HudiConverterHook)) {\n      deltaLog.hudiConverter.convertSnapshot(postCommitSnapshot, tableWithLocation)\n    }\n  }\n\n  /**\n   * Handles the transaction logic for CTAS-like statements, i.e.:\n   * CREATE TABLE AS SELECT\n   * CREATE OR REPLACE TABLE AS SELECT\n   * .saveAsTable in DataframeWriter API\n   *\n   * @return the txn used to make Delta commit\n   */\n  private def handleCreateTableAsSelect(\n      sparkSession: SparkSession,\n      txn: OptimisticTransaction,\n      deltaLog: DeltaLog,\n      deltaWriter: WriteIntoDeltaLike,\n      tableWithLocation: CatalogTable): OptimisticTransaction = {\n    val isManagedTable = tableWithLocation.tableType == CatalogTableType.MANAGED\n    val options = new DeltaOptions(table.storage.properties, sparkSession.sessionState.conf)\n\n    // Execute write command for `deltaWriter` by\n    //   - replacing the metadata new target table for DataFrameWriterV2 writer if it is a\n    //     REPLACE or CREATE_OR_REPLACE command,\n    //   - running the write procedure of DataFrameWriter command and returning the\n    //     new created actions,\n    //   - returning the Delta Operation type of this DataFrameWriter\n    def doDeltaWrite(\n        deltaWriter: WriteIntoDeltaLike,\n        schema: StructType): (TaggedCommitData[Action], DeltaOperations.Operation) = {\n      // In the V2 Writer, methods like \"replace\" and \"createOrReplace\" implicitly mean that\n      // the metadata should be changed. This wasn't the behavior for DataFrameWriterV1.\n      if (!isV1WriterSaveAsTableOverwrite) {\n        replaceMetadataIfNecessary(\n          txn,\n          tableWithLocation,\n          options,\n          sparkSession,\n          schema)\n      }\n      var taggedCommitData = deltaWriter.writeAndReturnCommitData(\n        txn,\n        sparkSession,\n        ClusteredTableUtils.getClusterBySpecOptional(table),\n        // Pass this option to the writer so that it can differentiate between an INSERT and a\n        // REPLACE command. This is needed because the writer is shared between the two commands.\n        // But some options, such as dynamic partition overwrite, are only valid for INSERT.\n        // Only allow createOrReplace command which is not a V1 writer.\n        // saveAsTable() command uses this same code path and is marked as a V1 writer.\n        // We do not want saveAsTable() to be treated as a REPLACE command wrt dynamic partition\n        // overwrite.\n        isTableReplace = isReplace && !isV1WriterSaveAsTableOverwrite\n      )\n      // The 'deltaWriter' initialized the schema. Remove 'EXISTS_DEFAULT' metadata keys because\n      // they are not required on tables created by CTAS.\n      txn.removeExistsDefaultFromSchema()\n      // Metadata updates for creating table (with any writer) and replacing table\n      // (only with V1 writer) will be handled inside WriteIntoDelta.\n      // For createOrReplace operation, metadata updates are handled here if the table already\n      // exists (replacing table), otherwise it is handled inside WriteIntoDelta (creating table).\n      if (!isV1WriterSaveAsTableOverwrite && isReplace && txn.readVersion > -1L) {\n        val newDomainMetadata = Seq.empty[DomainMetadata] ++\n          ClusteredTableUtils.getDomainMetadataFromTransaction(\n            ClusteredTableUtils.getClusterBySpecOptional(table), txn)\n        // Ensure to remove any domain metadata for REPLACE TABLE.\n        val newActions = taggedCommitData.actions ++\n          DomainMetadataUtils.handleDomainMetadataForReplaceTable(\n            txn.snapshot.domainMetadata, newDomainMetadata)\n        taggedCommitData = taggedCommitData.copy(actions = newActions)\n      }\n      val op = getOperation(txn.metadata, isManagedTable, Some(options),\n        clusterBy = ClusteredTableUtils.getLogicalClusteringColumnNames(\n          txn, taggedCommitData.actions),\n        // Only recording \"true\" to reduce noise in DESCRIBE HISTORY when it doesn't apply.\n        isV1SaveAsTableOverwrite = if (isV1WriterSaveAsTableOverwrite) Some(true) else None\n      )\n      (taggedCommitData, op)\n    }\n    val updatedConfiguration = UniversalFormat.enforceDependenciesInConfiguration(\n      sparkSession,\n      tableWithLocation,\n      deltaWriter.configuration,\n      txn.snapshot\n    )\n    val updatedWriter = deltaWriter.withNewWriterConfiguration(updatedConfiguration)\n    var txnToReturn = txn\n    // We are either appending/overwriting with saveAsTable or creating a new table with CTAS\n    if (!hasBeenExecuted(txn, sparkSession, Some(options))) {\n      val (taggedCommitData, op) = doDeltaWrite(updatedWriter, updatedWriter.data.schema.asNullable)\n      txn.commit(taggedCommitData.actions, op, tags = taggedCommitData.stringTags)\n    }\n    txnToReturn\n  }\n\n  /**\n   * Handles the transaction logic for CREATE OR REPLACE TABLE statement\n   * without the AS [CLONE, SELECT] clause.\n   */\n  private def handleCreateTable(\n      sparkSession: SparkSession,\n      txn: OptimisticTransaction,\n      tableWithLocation: CatalogTable,\n      fs: FileSystem,\n      hadoopConf: Configuration): Unit = {\n\n    val isManagedTable = tableWithLocation.tableType == CatalogTableType.MANAGED\n    val tableLocation = getDeltaTablePath(tableWithLocation)\n    val tableExistsInCatalog = existingTableOpt.isDefined\n    val options = new DeltaOptions(table.storage.properties, sparkSession.sessionState.conf)\n\n    def createActionsForNewTableOrVerify(): Seq[Action] = {\n      if (isManagedTable) {\n        // When creating a managed table, the table path should not exist or is empty, or\n        // users would be surprised to see the data, or see the data directory being dropped\n        // after the table is dropped.\n        assertPathEmpty(hadoopConf, tableWithLocation)\n      }\n\n      // However, if we allow creating an empty schema table and indeed the table is new, we\n      // would need to make sure txn.readVersion <= 0 so we are either:\n      // 1) Creating a new empty schema table (version = -1) or\n      // 2) Restoring an existing empty schema table at version 0. An empty schema table should\n      //    not have versions > 0 because it must be written with schema changes after initial\n      //    creation.\n      val emptySchemaTableFlag = sparkSession.sessionState.conf\n        .getConf(DeltaSQLConf.DELTA_ALLOW_CREATE_EMPTY_SCHEMA_TABLE)\n      val allowRestoringExistingEmptySchemaTable =\n        emptySchemaTableFlag && txn.metadata.schema.isEmpty && txn.readVersion == 0\n      val allowCreatingNewEmptySchemaTable =\n        emptySchemaTableFlag && tableWithLocation.schema.isEmpty && txn.readVersion == -1\n\n      // This is either a new table, or, we never defined the schema of the table. While it is\n      // unexpected that `txn.metadata.schema` to be empty when txn.readVersion >= 0, we still\n      // guard against it, in case of checkpoint corruption bugs.\n      val noExistingMetadata = txn.readVersion == -1 || txn.metadata.schema.isEmpty\n      if (noExistingMetadata && !allowRestoringExistingEmptySchemaTable) {\n        assertTableSchemaDefined(\n          fs, tableLocation, tableWithLocation, sparkSession,\n          allowCreatingNewEmptySchemaTable\n        )\n        assertPathEmpty(hadoopConf, tableWithLocation)\n        // This is a user provided schema.\n        // Doesn't come from a query, Follow nullability invariants.\n        var newMetadata =\n          getProvidedMetadata(tableWithLocation, table.schema.json)\n        newMetadata = newMetadata.copy(configuration =\n          UniversalFormat.enforceDependenciesInConfiguration(\n            sparkSession,\n            tableWithLocation,\n            newMetadata.configuration,\n            txn.snapshot\n          ))\n\n        txn.updateMetadataForNewTable(newMetadata)\n        // Remove 'EXISTS_DEFAULT' because it is not required for tables created with CREATE TABLE.\n        txn.removeExistsDefaultFromSchema()\n        protocol.foreach { protocol =>\n          // For commands like CREATE LIKE, the `protocol` here may contain table features\n          // from source table. It will override the `newProtocol` being created in the above\n          // `txn.updateMetadataForNewTable`.\n          // In order to enable [[CatalogOwnedTableFeature]] for target table w/ default\n          // spark configuration of CatalogOwned enabled, we need to manually append\n          // [[CatalogOwnedTableFeature]] here to the existing source table protocol.\n          val finalizedProtocol = if (CatalogOwnedTableUtils.defaultCatalogOwnedEnabled(\n              spark = sparkSession)) {\n            val minCatalogOwnedProtocol = Protocol(\n              CatalogOwnedTableFeature.minReaderVersion,\n              CatalogOwnedTableFeature.minWriterVersion).withFeature(CatalogOwnedTableFeature)\n            protocol.merge(minCatalogOwnedProtocol)\n          } else {\n            protocol\n          }\n          txn.updateProtocol(finalizedProtocol)\n        }\n        ClusteredTableUtils.getDomainMetadataFromTransaction(\n          ClusteredTableUtils.getClusterBySpecOptional(table), txn).toSeq\n      } else {\n        verifyTableMetadata(sparkSession, txn, tableWithLocation)\n        Nil\n      }\n    }\n\n    // We are defining a table using the Create or Replace Table statements.\n    val actionsToCommit = operation match {\n      case TableCreationModes.Create =>\n        require(!tableExistsInCatalog, \"Can't recreate a table when it exists\")\n        createActionsForNewTableOrVerify()\n\n      case TableCreationModes.CreateOrReplace if !tableExistsInCatalog =>\n        // If the table doesn't exist, CREATE OR REPLACE must provide a schema\n        if (tableWithLocation.schema.isEmpty) {\n          throw DeltaErrors.schemaNotProvidedException\n        }\n        createActionsForNewTableOrVerify()\n      case _ =>\n        // When the operation is a REPLACE or CREATE OR REPLACE, then the schema shouldn't be\n        // empty, since we'll use the entry to replace the schema\n        if (tableWithLocation.schema.isEmpty) {\n          throw DeltaErrors.schemaNotProvidedException\n        }\n        // This can happen if someone deleted files from the filesystem but\n        // the table still exists in the catalog.\n        if (txn.readVersion == -1 && tableExistsInCatalog) {\n          throw DeltaErrors.metadataAbsentForExistingCatalogTable(\n            tableWithLocation.identifier.toString, txn.deltaLog.logPath.toString)\n        }\n        // We need to replace\n        replaceMetadataIfNecessary(\n          txn,\n          tableWithLocation,\n          options,\n          sparkSession,\n          tableWithLocation.schema)\n        // Remove 'EXISTS_DEFAULT' because it is not required for tables created with REPLACE TABLE.\n        txn.removeExistsDefaultFromSchema()\n        // Truncate the table\n        val operationTimestamp = System.currentTimeMillis()\n        var actionsToCommit = Seq.empty[Action]\n        val removes = txn.filterFiles().map(_.removeWithTimestamp(operationTimestamp))\n        actionsToCommit = removes ++\n          DomainMetadataUtils.handleDomainMetadataForReplaceTable(\n            txn.snapshot.domainMetadata,\n            ClusteredTableUtils.getDomainMetadataFromTransaction(\n              ClusteredTableUtils.getClusterBySpecOptional(table), txn).toSeq)\n        actionsToCommit\n    }\n\n    // Validate check constraints for CREATE/REPLACE TABLE\n    val checkConstraints = Constraints.getAll(txn.metadata, sparkSession)\n    Constraints.validateCheckConstraints(\n      sparkSession,\n      checkConstraints,\n      txn.deltaLog,\n      txn.metadata.schema\n    )\n    val changedMetadata = txn.metadata != txn.snapshot.metadata\n    val changedProtocol = txn.protocol != txn.snapshot.protocol\n    if (actionsToCommit.nonEmpty || changedMetadata || changedProtocol) {\n      val op = getOperation(txn.metadata, isManagedTable, None,\n        clusterBy = ClusteredTableUtils.getLogicalClusteringColumnNames(\n          txn, actionsToCommit)\n      )\n      txn.commit(actionsToCommit, op)\n    }\n  }\n\n  private def getProvidedMetadata(table: CatalogTable, schemaString: String): Metadata = {\n    Metadata(\n      description = table.comment.orNull,\n      schemaString = schemaString,\n      partitionColumns = table.partitionColumnNames,\n      // Filter out ephemeral clustering columns config because we don't want to persist\n      // it in delta log. This will be persisted in CatalogTable's table properties instead.\n      configuration = ClusteredTableUtils.removeClusteringColumnsProperty(table.properties),\n      createdTime = Some(System.currentTimeMillis()))\n  }\n\n  private def assertPathEmpty(\n      hadoopConf: Configuration,\n      tableWithLocation: CatalogTable): Unit = {\n    val path = getDeltaTablePath(tableWithLocation)\n    val fs = path.getFileSystem(hadoopConf)\n    // Verify that the table location associated with CREATE TABLE doesn't have any data. Note that\n    // we intentionally diverge from this behavior w.r.t regular datasource tables (that silently\n    // overwrite any previous data)\n    if (fs.exists(path) && fs.listStatus(path).nonEmpty) {\n      throw DeltaErrors.createTableWithNonEmptyLocation(\n        tableWithLocation.identifier.toString,\n        path.toString)\n    }\n  }\n\n  private def assertTableSchemaDefined(\n      fs: FileSystem,\n      path: Path,\n      table: CatalogTable,\n      sparkSession: SparkSession,\n      allowEmptyTableSchema: Boolean): Unit = {\n    // Users did not specify the schema. We expect the schema exists in Delta.\n    if (table.schema.isEmpty) {\n      if (table.tableType == CatalogTableType.EXTERNAL) {\n        if (fs.exists(path) && fs.listStatus(path).nonEmpty) {\n          throw DeltaErrors.createExternalTableWithoutLogException(\n            path, table.identifier.quotedString, sparkSession)\n        } else {\n          if (allowEmptyTableSchema) return\n          throw DeltaErrors.createExternalTableWithoutSchemaException(\n            path, table.identifier.quotedString, sparkSession)\n        }\n      } else {\n        if (allowEmptyTableSchema) return\n        throw DeltaErrors.createManagedTableWithoutSchemaException(\n          table.identifier.quotedString, sparkSession)\n      }\n    }\n  }\n\n\n  /**\n   * When creating an external table in a location where some table already existed, we make sure\n   * that the specified table properties match the existing table properties. Since Coordinated\n   * Commits is not designed to be overridden, we should not error out if the command omits these\n   * properties. If the existing table has Coordinated Commits enabled, we also do not error out if\n   * the command omits the ICT properties, which are the dependencies for Coordinated Commits.\n   */\n  private def filterCoordinatedCommitsProperties(\n      existingProperties: Map[String, String],\n      tableProperties: Map[String, String]): Map[String, String] = {\n    var filteredExistingProperties = existingProperties\n    val overridingCCConfs = CoordinatedCommitsUtils.getExplicitCCConfigurations(tableProperties)\n    val existingCCConfs = CoordinatedCommitsUtils.getExplicitCCConfigurations(existingProperties)\n    if (existingCCConfs.nonEmpty && overridingCCConfs.isEmpty) {\n      filteredExistingProperties --= CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS\n      val overridingICTConfs = CoordinatedCommitsUtils.getExplicitICTConfigurations(tableProperties)\n      val existingICTConfs = CoordinatedCommitsUtils.getExplicitICTConfigurations(\n        existingProperties)\n      if (existingICTConfs.nonEmpty && overridingICTConfs.isEmpty) {\n        filteredExistingProperties --= CoordinatedCommitsUtils.ICT_TABLE_PROPERTY_KEYS\n      }\n    }\n    filteredExistingProperties\n  }\n\n  /**\n   * Verify against our transaction metadata that the user specified the right metadata for the\n   * table.\n   */\n  private def verifyTableMetadata(\n      sparkSession: SparkSession,\n      txn: OptimisticTransaction,\n      tableDesc: CatalogTable): Unit = {\n    val existingMetadata = txn.metadata\n    val path = getDeltaTablePath(tableDesc)\n\n    // The delta log already exists. If they give any configuration, we'll make sure it all matches.\n    // Otherwise we'll just go with the metadata already present in the log.\n    // The schema compatibility checks will be made in `WriteIntoDelta` for CreateTable\n    // with a query\n    if (txn.readVersion > -1) {\n      if (tableDesc.schema.nonEmpty) {\n        // We check exact alignment on create table if everything is provided\n        // However, if in column mapping mode, we can safely ignore the related metadata fields in\n        // existing metadata because new table desc will not have related metadata assigned yet\n        val differences = SchemaUtils.reportDifferences(\n          DeltaTableUtils.removeInternalDeltaMetadata(sparkSession, existingMetadata.schema),\n          tableDesc.schema)\n        if (differences.nonEmpty) {\n          throw DeltaErrors.createTableWithDifferentSchemaException(\n            path, tableDesc.schema, existingMetadata.schema, differences)\n        }\n\n        // If schema is specified, we must make sure the partitioning matches, even the partitioning\n        // is not specified.\n        if (tableDesc.partitionColumnNames != existingMetadata.partitionColumns) {\n          throw DeltaErrors.createTableWithDifferentPartitioningException(\n            path, tableDesc.partitionColumnNames, existingMetadata.partitionColumns)\n        }\n        // If schema is specified, we must make sure the clustering column matches (includes when\n        // clustering is not specified).\n        val specifiedClusterBySpec = ClusteredTableUtils.getClusterBySpecOptional(tableDesc)\n        val existingClusterBySpec = ClusteredTableUtils.getClusterBySpecOptional(txn.snapshot)\n        if (specifiedClusterBySpec != existingClusterBySpec) {\n          throw DeltaErrors.createTableWithDifferentClusteringException(\n            path,\n            specifiedClusterBySpec,\n            existingClusterBySpec)\n        }\n      }\n\n      if (tableDesc.properties.nonEmpty) {\n        // When comparing properties of the existing table and the new table, remove some\n        // internal column mapping properties for the sake of comparison.\n        var filteredTableProperties = filterColumnMappingProperties(\n          tableDesc.properties)\n        // We also need to remove any protocol-related properties as we're filtering these\n        // from the metadata so they won't be present in the table properties.\n        filteredTableProperties =\n          Protocol.filterProtocolPropsFromTableProps(filteredTableProperties)\n        var filteredExistingProperties = filterColumnMappingProperties(\n          existingMetadata.configuration)\n        // Clustered table has internal table properties in Metadata configurations and they are\n        // never configured by the user so remove them before validation.\n        if (ClusteredTableUtils.isSupported(txn.protocol)) {\n          filteredExistingProperties =\n            ClusteredTableUtils.removeInternalTableProperties(filteredExistingProperties) ++\n              // Validate clustering columns in CatalogTable.PROP_CLUSTERING_COLUMNS\n              // are matched.\n              ClusteredTableUtils.getClusteringColumnsAsProperty(txn.snapshot)\n          // Note that clustering columns are already stored in the key\n          // CatalogTable.PROP_CLUSTERING_COLUMNS.\n          filteredTableProperties =\n            ClusteredTableUtils.removeInternalTableProperties(filteredTableProperties)\n        }\n        filteredExistingProperties =\n          filterCoordinatedCommitsProperties(filteredExistingProperties, filteredTableProperties)\n        if (filteredTableProperties != filteredExistingProperties) {\n          throw DeltaErrors.createTableWithDifferentPropertiesException(\n            path, filteredTableProperties, filteredExistingProperties)\n        }\n        // If column mapping properties are present in both configs, verify they're the same value.\n        if (!DeltaColumnMapping.verifyInternalProperties(\n            tableDesc.properties, existingMetadata.configuration)) {\n          throw DeltaErrors.createTableWithDifferentPropertiesException(\n            path, tableDesc.properties, existingMetadata.configuration)\n        }\n      }\n    }\n  }\n\n  /**\n   * Based on the table creation operation, and parameters, we can resolve to different operations.\n   * A lot of this is needed for legacy reasons in Databricks Runtime.\n   * @param metadata The table metadata, which we are creating or replacing\n   * @param isManagedTable Whether we are creating or replacing a managed table\n   * @param options Write options, if this was a CTAS/RTAS\n   */\n  private def getOperation(\n      metadata: Metadata,\n      isManagedTable: Boolean,\n      options: Option[DeltaOptions],\n      clusterBy: Option[Seq[String]],\n      isV1SaveAsTableOverwrite: Option[Boolean] = None\n  ): DeltaOperations.Operation = operation match {\n    // This is legacy saveAsTable behavior in Databricks Runtime\n    case TableCreationModes.Create if existingTableOpt.isDefined &&\n      query.isDefined && options.nonEmpty =>\n      DeltaOperations.Write(\n        mode = mode,\n        partitionBy = Option(table.partitionColumnNames),\n        predicate = options.get.replaceWhere,\n        userMetadata = options.flatMap(_.userMetadata),\n        isDynamicPartitionOverwrite = options.flatMap(\n          o => if (Try(o.isDynamicPartitionOverwriteMode).getOrElse(false)) Some(true) else None),\n        canOverwriteSchema = options.flatMap(o => if (o.canOverwriteSchema) Some(true) else None),\n        canMergeSchema = options.flatMap(o => if (o.canMergeSchema) Some(true) else None)\n      )\n\n    // DataSourceV2 table creation\n    // CREATE TABLE (non-DataFrameWriter API) doesn't have options syntax\n    // (userMetadata uses SQLConf in this case)\n    case TableCreationModes.Create =>\n      DeltaOperations.CreateTable(\n        metadata, isManagedTable, query.isDefined, clusterBy = clusterBy\n      )\n\n    // DataSourceV2 table replace\n    // REPLACE TABLE (non-DataFrameWriter API) doesn't have options syntax\n    // (userMetadata uses SQLConf in this case)\n    case TableCreationModes.Replace =>\n      DeltaOperations.ReplaceTable(\n        metadata = metadata,\n        isManaged = isManagedTable,\n        orCreate = false,\n        asSelect = query.isDefined,\n        clusterBy = clusterBy,\n        predicate = options.flatMap(_.replaceWhere),\n        isDynamicPartitionOverwrite = options.flatMap(\n          o => if (Try(o.isDynamicPartitionOverwriteMode).getOrElse(false)) Some(true) else None),\n        canOverwriteSchema = options.flatMap(o => if (o.canOverwriteSchema) Some(true) else None),\n        canMergeSchema = options.flatMap(o => if (o.canMergeSchema) Some(true) else None),\n        isV1SaveAsTableOverwrite = isV1SaveAsTableOverwrite\n      )\n\n    // Legacy saveAsTable with Overwrite mode\n    case TableCreationModes.CreateOrReplace if options.exists(_.replaceWhere.isDefined) =>\n      DeltaOperations.Write(\n        mode = mode,\n        partitionBy = Option(table.partitionColumnNames),\n        predicate = options.get.replaceWhere,\n        userMetadata = options.flatMap(_.userMetadata),\n        isDynamicPartitionOverwrite = options.flatMap(\n          o => if (Try(o.isDynamicPartitionOverwriteMode).getOrElse(false)) Some(true) else None),\n        canOverwriteSchema = options.flatMap(o => if (o.canOverwriteSchema) Some(true) else None),\n        canMergeSchema = options.flatMap(o => if (o.canMergeSchema) Some(true) else None)\n      )\n\n    // New DataSourceV2 saveAsTable with overwrite mode behavior\n    case TableCreationModes.CreateOrReplace =>\n      DeltaOperations.ReplaceTable(\n        metadata = metadata,\n        isManaged = isManagedTable,\n        orCreate = true,\n        asSelect = query.isDefined,\n        userMetadata = options.flatMap(_.userMetadata),\n        clusterBy = clusterBy,\n        predicate = options.flatMap(_.replaceWhere),\n        isDynamicPartitionOverwrite = options.flatMap(\n          o => if (Try(o.isDynamicPartitionOverwriteMode).getOrElse(false)) Some(true) else None),\n        canOverwriteSchema = options.flatMap(o => if (o.canOverwriteSchema) Some(true) else None),\n        canMergeSchema = options.flatMap(o => if (o.canMergeSchema) Some(true) else None),\n        isV1SaveAsTableOverwrite = isV1SaveAsTableOverwrite\n      )\n  }\n\n  private def getDeltaTablePath(table: CatalogTable): Path = {\n    new Path(table.location)\n  }\n\n  /**\n   * With DataFrameWriterV2, methods like `replace()` or `createOrReplace()` mean that the\n   * metadata of the table should be replaced. If overwriteSchema=false is provided with these\n   * methods, then we will verify that the metadata match exactly.\n   */\n  private def replaceMetadataIfNecessary(\n      txn: OptimisticTransaction,\n      tableDesc: CatalogTable,\n      options: DeltaOptions,\n      sparkSession: SparkSession,\n      schema: StructType): Unit = {\n    // If a user explicitly specifies not to overwrite the schema, during a replace, we should\n    // tell them that it's not supported\n    val dontOverwriteSchema = options.options.contains(DeltaOptions.OVERWRITE_SCHEMA_OPTION) &&\n      !options.canOverwriteSchema\n    if (isReplace && dontOverwriteSchema) {\n      throw DeltaErrors.illegalUsageException(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"replacing\")\n    }\n    if (txn.readVersion > -1L && isReplace && !dontOverwriteSchema) {\n      // When a table already exists, and we're using the DataFrameWriterV2 API to replace\n      // or createOrReplace a table, we blindly overwrite the metadata.\n      var newMetadata = getProvidedMetadata(table, schema.json)\n      val updatedConfig = UniversalFormat.enforceDependenciesInConfiguration(\n        sparkSession,\n        tableDesc,\n        newMetadata.configuration,\n        txn.snapshot)\n      newMetadata = newMetadata.copy(configuration = updatedConfig)\n      if (allowCatalogManaged && txn.snapshot.isCatalogOwned) {\n        // Preserve the existing Delta metadata id across REPLACE. This is distinct from the\n        // Unity Catalog table id stored in `io.unitycatalog.tableId`.\n        newMetadata = newMetadata.copy(id = txn.snapshot.metadata.id)\n      }\n      txn.updateMetadataForNewTableInReplace(newMetadata)\n    }\n  }\n\n  /** Returns true if the current operation could be replacing a table. */\n  private def isReplace: Boolean = {\n    operation == TableCreationModes.CreateOrReplace ||\n      operation == TableCreationModes.Replace\n  }\n\n  /** Returns the transaction that should be used for the CREATE/REPLACE commit. */\n  private def startTxnForTableCreation(\n      sparkSession: SparkSession,\n      deltaLog: DeltaLog,\n      tableWithLocation: CatalogTable,\n      snapshotOpt: Option[Snapshot] = None): OptimisticTransaction = {\n    val txn = deltaLog.startTransaction(existingTableOpt, snapshotOpt)\n    validatePrerequisitesForClusteredTable(txn.snapshot.protocol, txn.deltaLog)\n\n    // During CREATE (not REPLACE/overwrites), we synchronously run conversion\n    //  (if Uniform is enabled) so we always remove the post commit hook here.\n    if (!isReplace) {\n      txn.unregisterPostCommitHooksWhere(hook => hook.name == IcebergConverterHook.name)\n      txn.unregisterPostCommitHooksWhere(hook => hook.name == HudiConverterHook.name)\n    }\n    txn\n  }\n\n  /**\n   * Validate pre-requisites for clustered tables for CREATE/REPLACE operations.\n   * @param protocol Protocol used for validations. This protocol should\n   *                 be used during the CREATE/REPLACE commit.\n   * @param deltaLog Delta log used for logging purposes.\n   */\n  private def validatePrerequisitesForClusteredTable(\n      protocol: Protocol,\n      deltaLog: DeltaLog): Unit = {\n    // Validate a clustered table is not replaced by a partitioned table.\n    if (table.partitionColumnNames.nonEmpty &&\n      ClusteredTableUtils.isSupported(protocol)) {\n      throw DeltaErrors.replacingClusteredTableWithPartitionedTableNotAllowed()\n    }\n  }\n}\n\n// isCreate is true for Create and CreateOrReplace modes. It is false for Replace mode.\nobject TableCreationModes {\n  sealed trait CreationMode {\n    def mode: SaveMode\n    def isCreate: Boolean = true\n  }\n\n  case object Create extends CreationMode {\n    override def mode: SaveMode = SaveMode.ErrorIfExists\n  }\n\n  case object CreateOrReplace extends CreationMode {\n    override def mode: SaveMode = SaveMode.Overwrite\n  }\n\n  case object Replace extends CreationMode {\n    override def mode: SaveMode = SaveMode.Overwrite\n    override def isCreate: Boolean = false\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/CreateDeltaTableLike.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaOptions, Snapshot}\nimport org.apache.spark.sql.delta.hooks.{UpdateCatalog, UpdateCatalogFactory}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.SQLConfHelper\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.util.CharVarcharUtils\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A common trait implementing utility functions (e.g. catalog operations) for all commands that\n * create a Delta table.\n */\ntrait CreateDeltaTableLike extends SQLConfHelper {\n  // The table to create.\n  val table: CatalogTable\n\n  // The existing table for the same identifier if exists.\n  val existingTableOpt: Option[CatalogTable]\n\n  // The table creation mode.\n  val operation: TableCreationModes.CreationMode\n\n  // Whether the table is accessed by path.\n  val tableByPath: Boolean = false\n\n  // The save mode when writing data. Relevant when the query is empty or set to Ignore with `CREATE\n  // TABLE IF NOT EXISTS`.\n  val mode: SaveMode\n\n  // Whether the table is UC managed table with catalogManaged feature.\n  val allowCatalogManaged: Boolean\n\n  /**\n   * Generates a `CatalogTable` with its `locationUri` set appropriately, depending on whether the\n   * table already exists or is newly created.\n   */\n  protected def getCatalogTableWithLocation(sparkSession: SparkSession): CatalogTable = {\n    val tableExistsInCatalog = existingTableOpt.isDefined\n    if (tableExistsInCatalog) {\n      val existingTable = existingTableOpt.get\n      table.storage.locationUri match {\n        case Some(location) if location.getPath != existingTable.location.getPath =>\n          throw DeltaErrors.tableLocationMismatch(table, existingTable)\n        case _ =>\n      }\n      table.copy(\n        storage = existingTable.storage,\n        tableType = existingTable.tableType)\n    } else if (table.storage.locationUri.isEmpty) {\n      // We are defining a new managed table\n      assert(table.tableType == CatalogTableType.MANAGED)\n      val loc = sparkSession.sessionState.catalog.defaultTablePath(table.identifier)\n      table.copy(storage = table.storage.copy(locationUri = Some(loc)))\n    } else {\n      // 1. We are defining a new external table\n      // 2. It's a managed table which already has the location populated. This can happen in DSV2\n      //    CTAS flow.\n      table\n    }\n  }\n\n  /**\n   * Here we disambiguate the catalog alterations we need to do based on the table operation, and\n   * whether we have reached here through legacy code or DataSourceV2 code paths.\n   */\n  protected def updateCatalog(\n      spark: SparkSession,\n      table: CatalogTable,\n      snapshot: Snapshot,\n      query: Option[LogicalPlan],\n      didNotChangeMetadata: Boolean,\n      createTableFunc: Option[CatalogTable => Unit] = None\n  ): Unit = {\n    val cleaned = cleanupTableDefinition(spark, table, snapshot)\n    operation match {\n      case _ if tableByPath => // do nothing with the metastore if this is by path\n      case TableCreationModes.Create =>\n        if (createTableFunc.isDefined) {\n          createTableFunc.get.apply(cleaned)\n        } else {\n          spark.sessionState.catalog.createTable(\n            cleaned,\n            ignoreIfExists = existingTableOpt.isDefined || mode == SaveMode.Ignore,\n            validateLocation = false)\n        }\n      case TableCreationModes.Replace | TableCreationModes.CreateOrReplace\n        if existingTableOpt.isDefined =>\n        // Catalog-managed / CC tables are owned by the delegated V2 catalog plugin (for example\n        // Unity Catalog), so SessionCatalog's post-commit UpdateCatalogHook must not run.\n        if (!allowCatalogManaged) {\n          UpdateCatalogFactory.getUpdateCatalogHook(table, spark).updateSchema(spark, snapshot)\n        }\n      case TableCreationModes.Replace =>\n        val ident = Identifier.of(table.identifier.database.toArray, table.identifier.table)\n        throw DeltaErrors.cannotReplaceMissingTableException(ident)\n      case TableCreationModes.CreateOrReplace =>\n        createTableFunc match {\n          case Some(createFunc) =>\n            // This is the new missing-table path where creation is delegated through the V2\n            // catalog plugin (for example Unity Catalog) instead of SessionCatalog.createTable().\n            createFunc(cleaned)\n          case None =>\n            spark.sessionState.catalog.createTable(\n              cleaned,\n              ignoreIfExists = false,\n              validateLocation = false)\n        }\n    }\n    if (conf.getConf(DeltaSQLConf.HMS_FORCE_ALTER_TABLE_DATA_SCHEMA)) {\n      spark.sessionState.catalog.alterTableDataSchema(cleaned.identifier, cleaned.schema)\n    }\n  }\n\n  /** Clean up the information we pass on to store in the catalog. */\n  private def cleanupTableDefinition(spark: SparkSession, table: CatalogTable, snapshot: Snapshot)\n  : CatalogTable = {\n    // These actually have no effect on the usability of Delta, but feature flagging legacy\n    // behavior for now\n    val storageProps = if (conf.getConf(DeltaSQLConf.DELTA_LEGACY_STORE_WRITER_OPTIONS_AS_PROPS)) {\n      // Legacy behavior\n      table.storage\n    } else {\n      table.storage.copy(properties = Map.empty)\n    }\n\n    // If we have to update the catalog, use the correct schema and table properties, otherwise\n    // empty out the schema and property information\n    if (conf.getConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED)) {\n      val truncationThreshold = spark.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD)\n      val (truncatedSchema, additionalProperties) = UpdateCatalog.truncateSchemaIfNecessary(\n        snapshot.schema,\n        truncationThreshold)\n\n      table.copy(\n        schema = truncatedSchema,\n        // Hive does not allow for the removal of partition columns once stored.\n        // To avoid returning the incorrect schema when the partition columns change,\n        // we store the partition columns as regular data columns.\n        partitionColumnNames = Nil,\n        properties = UpdateCatalog.updatedProperties(snapshot)\n          ++ additionalProperties,\n        storage = storageProps,\n        tracksPartitionsInCatalog = true)\n    } else if (allowCatalogManaged) {\n      // Setting table properties is required for creating catalogManaged tables.\n      table.copy(\n        // Here we use snapshot.schema instead of table.schema because it reflects the actual\n        // committed state of the table.\n        // Delta does not have a distinct storage type for Char/Varchar; in snapshots, they are\n        // represented in String type with extra type metadata. We convert them to back to the\n        // original Char/Varchar types when storing them in the catalog.\n        schema = CharVarcharUtils.getRawSchema(snapshot.schema),\n        partitionColumnNames = snapshot.metadata.partitionColumns,\n        properties = UpdateCatalog.updatedProperties(snapshot),\n        storage = storageProps,\n        tracksPartitionsInCatalog = true)\n    } else {\n      table.copy(\n        schema = new StructType(),\n        properties = Map.empty,\n        partitionColumnNames = Nil,\n        // Remove write specific options when updating the catalog\n        storage = storageProps,\n        tracksPartitionsInCatalog = true)\n    }\n  }\n\n  /**\n   * Differentiate between DataFrameWriterV1 and V2 so that we can decide\n   * what to do with table metadata. In DataFrameWriterV1, mode(\"overwrite\").saveAsTable,\n   * behaves as a CreateOrReplace table, but we have asked for \"overwriteSchema\" as an\n   * explicit option to overwrite partitioning or schema information. With DataFrameWriterV2,\n   * the behavior asked for by the user is clearer: .createOrReplace(), which means that we\n   * should overwrite schema and/or partitioning. Therefore we have this hack.\n   */\n  protected def isV1WriterSaveAsTableOverwrite: Boolean = {\n    val options = new DeltaOptions(table.storage.properties, conf)\n    CreateDeltaTableLikeShims.isV1WriterSaveAsTableOverwrite(options, mode)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DMLUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.DeltaCommitTag\nimport org.apache.spark.sql.delta.actions.{Action, FileAction}\n\nobject DMLUtils {\n\n  /** Holder for some of the parameters for a `OptimisticTransaction.commit` */\n  case class TaggedCommitData[A <: Action](\n      actions: Seq[A],\n      tags: Map[DeltaCommitTag, String] = Map.empty) {\n\n    def withTag[T](key: DeltaCommitTag.TypedCommitTag[T], value: T): TaggedCommitData[A] = {\n      val mergedValue = key.mergeWithNewTypedValue(tags.get(key), value)\n      this.copy(tags = this.tags + (key -> mergedValue))\n    }\n\n    def stringTags: Map[String, String] = tags.map { case (k, v) => k.key -> v }\n  }\n\n  object TaggedCommitData {\n    def empty[A <: Action]: TaggedCommitData[A] = TaggedCommitData(Seq.empty[A])\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DMLWithDeletionVectorsHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.util.UUID\n\nimport scala.collection.generic.Sizing\n\nimport org.apache.spark.sql.catalyst.expressions.aggregation.BitmapAggregator\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{DataFrameUtils, DeltaLog, DeltaParquetFileFormat, OptimisticTransaction, Snapshot}\nimport org.apache.spark.sql.delta.DeltaParquetFileFormat._\nimport org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor, FileAction}\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat, StoredBitmap}\nimport org.apache.spark.sql.delta.files.{TahoeBatchFileIndex, TahoeFileIndex}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatsCollectionUtils\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.spark.sql.delta.util.{BinPackingIterator, DeltaEncoder, PathWithFileSystem, Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.DeltaFileOperations.absolutePath\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.expressions.{AttributeReference, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.execution.datasources.FileFormat.{FILE_PATH, METADATA_NAME}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.{SerializableConfiguration, Utils => SparkUtils}\n\n\n/**\n * Contains utility classes and method for performing DML operations with Deletion Vectors.\n */\nobject DMLWithDeletionVectorsHelper extends DeltaCommand {\n  val SUPPORTED_DML_COMMANDS: Seq[String] = Seq(\"DELETE\", \"UPDATE\")\n\n  /**\n   * Creates a DataFrame that can be used to scan for rows matching the condition in the given\n   * files. Generally the given file list is a pruned file list using the stats based pruning.\n   */\n  def createTargetDfForScanningForMatches(\n      spark: SparkSession,\n      target: LogicalPlan,\n      fileIndex: TahoeFileIndex): DataFrame = {\n    DataFrameUtils.ofRows(spark, replaceFileIndex(spark, target, fileIndex))\n  }\n\n  /**\n   * Replace the file index in a logical plan and return the updated plan.\n   * It's a common pattern that, in Delta commands, we use data skipping to determine a subset of\n   * files that can be affected by the command, so we replace the whole-table file index in the\n   * original logical plan with a new index of potentially affected files, while everything else in\n   * the original plan, e.g., resolved references, remain unchanged.\n   *\n   * In addition we also request a metadata column and a row index column from the Scan to help\n   * generate the Deletion Vectors. When predicate pushdown is enabled, we only request the\n   * metadata column. This is because we can utilize _metadata.row_index instead of generating a\n   * custom one.\n   *\n   * @param spark the active spark session\n   * @param target the logical plan in which we replace the file index\n   * @param fileIndex the new file index\n   */\n  private def replaceFileIndex(\n      spark: SparkSession,\n      target: LogicalPlan,\n      fileIndex: TahoeFileIndex): LogicalPlan = {\n    val useMetadataRowIndex =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX)\n    // This is only used when predicate pushdown is disabled.\n    val rowIndexCol = AttributeReference(ROW_INDEX_COLUMN_NAME, ROW_INDEX_STRUCT_FIELD.dataType)()\n\n    var fileMetadataCol: AttributeReference = null\n\n    val newTarget = target.transformUp {\n      case l @ LogicalRelationWithTable(\n        hfsr @ HadoopFsRelation(_, _, _, _, format: DeltaParquetFileFormat, _), _) =>\n        fileMetadataCol = format.createFileMetadataCol()\n        // Take the existing schema and add additional metadata columns\n        if (useMetadataRowIndex) {\n          l.copy(\n            relation = hfsr.copy(location = fileIndex)(hfsr.sparkSession),\n            output = l.output :+ fileMetadataCol)\n        } else {\n          val newDataSchema =\n            StructType(hfsr.dataSchema).add(ROW_INDEX_STRUCT_FIELD)\n          val finalOutput = l.output ++ Seq(rowIndexCol, fileMetadataCol)\n          // Disable splitting and filter pushdown in order to generate the row-indexes.\n          val newFormat = format.copy(optimizationsEnabled = false)\n          val newBaseRelation = hfsr.copy(\n            location = fileIndex,\n            dataSchema = newDataSchema,\n            fileFormat = newFormat)(hfsr.sparkSession)\n\n          l.copy(relation = newBaseRelation, output = finalOutput)\n        }\n      case p @ Project(projectList, _) =>\n        if (fileMetadataCol == null) {\n          throw new IllegalStateException(\"File metadata column is not yet created.\")\n        }\n        val rowIndexColOpt = if (useMetadataRowIndex) None else Some(rowIndexCol)\n        val additionalColumns = Seq(fileMetadataCol) ++ rowIndexColOpt\n        p.copy(projectList = projectList ++ additionalColumns)\n    }\n    newTarget\n  }\n\n  /**\n   * Find the target table files that contain rows that satisfy the condition and a DV attached\n   * to each file that indicates a the rows marked as deleted from the file\n   */\n  def findTouchedFiles(\n      sparkSession: SparkSession,\n      txn: OptimisticTransaction,\n      hasDVsEnabled: Boolean,\n      deltaLog: DeltaLog,\n      targetDf: DataFrame,\n      fileIndex: TahoeFileIndex,\n      condition: Expression,\n      opName: String): Seq[TouchedFileWithDV] = {\n    require(\n      SUPPORTED_DML_COMMANDS.contains(opName),\n      s\"Expecting opName to be one of ${SUPPORTED_DML_COMMANDS.mkString(\", \")}, \" +\n        s\"but got '$opName'.\")\n\n    recordDeltaOperation(deltaLog, opType = s\"$opName.findTouchedFiles\") {\n      val candidateFiles = fileIndex match {\n        case f: TahoeBatchFileIndex => f.addFiles\n        case _ => throw new IllegalArgumentException(\"Unexpected file index found!\")\n      }\n\n      val matchedRowIndexSets =\n        DeletionVectorBitmapGenerator.buildRowIndexSetsForFilesMatchingCondition(\n          sparkSession,\n          txn,\n          hasDVsEnabled,\n          targetDf,\n          candidateFiles,\n          condition)\n\n      val nameToAddFileMap = generateCandidateFileMap(txn.deltaLog.dataPath, candidateFiles)\n      findFilesWithMatchingRows(txn, nameToAddFileMap, matchedRowIndexSets)\n    }\n  }\n\n  /**\n   * Finds the files in nameToAddFileMap in which rows were deleted by checking the row index set.\n   */\n  def findFilesWithMatchingRows(\n      txn: OptimisticTransaction,\n      nameToAddFileMap: Map[String, AddFile],\n      matchedFileRowIndexSets: Seq[DeletionVectorResult]): Seq[TouchedFileWithDV] = {\n    // Get the AddFiles using the touched file names and group them together with other\n    // information we need for later phases.\n    val dataPath = txn.deltaLog.dataPath\n    val touchedFilesWithMatchedRowIndices = matchedFileRowIndexSets.map { fileRowIndex =>\n      val filePath = fileRowIndex.filePath\n      val addFile = getTouchedFile(dataPath, filePath, nameToAddFileMap)\n      TouchedFileWithDV(\n        filePath,\n        addFile,\n        fileRowIndex.deletionVector,\n        fileRowIndex.matchedRowCount)\n    }\n\n    logTrace(\"findTouchedFiles: matched files:\\n\\t\" +\n      s\"${touchedFilesWithMatchedRowIndices.map(_.inputFilePath).mkString(\"\\n\\t\")}\")\n\n    touchedFilesWithMatchedRowIndices.filterNot(_.isUnchanged)\n  }\n\n  def processUnmodifiedData(\n      spark: SparkSession,\n      touchedFiles: Seq[TouchedFileWithDV],\n      snapshot: Snapshot,\n      stringTruncateLength: Int): (Seq[FileAction], Map[String, Long]) = {\n    val numModifiedRows = touchedFiles.map(_.numberOfModifiedRows).sum.toLong\n    val numRemovedFiles = touchedFiles.count(_.isFullyReplaced()).toLong\n\n    val (fullyRemovedFiles, notFullyRemovedFiles) = touchedFiles.partition(_.isFullyReplaced())\n\n    val timestamp = System.currentTimeMillis()\n    val fullyRemoved = fullyRemovedFiles.map(_.fileLogEntry.removeWithTimestamp(timestamp))\n\n    val dvUpdates = notFullyRemovedFiles.map { fileWithDVInfo =>\n      fileWithDVInfo.fileLogEntry.removeRows(\n        deletionVector = fileWithDVInfo.newDeletionVector,\n        updateStats = false\n      )}\n    val (dvAddFiles, dvRemoveFiles) = dvUpdates.unzip\n    val dvAddFilesWithStats = getActionsWithStats(spark, dvAddFiles, snapshot, stringTruncateLength)\n\n    var (numDeletionVectorsAdded, numDeletionVectorsRemoved, numDeletionVectorsUpdated) =\n      dvUpdates.foldLeft((0L, 0L, 0L)) { case ((added, removed, updated), (addFile, removeFile)) =>\n        (Option(addFile.deletionVector), Option(removeFile.deletionVector)) match {\n          case (Some(_), Some(_)) => (added, removed, updated + 1)\n          case (None, Some(_)) => (added, removed + 1, updated)\n          case (Some(_), None) => (added + 1, removed, updated)\n          case _ => (added, removed, updated)\n        }\n      }\n    numDeletionVectorsRemoved += fullyRemoved.count(_.deletionVector != null)\n    val metricMap = Map(\n      \"numModifiedRows\" -> numModifiedRows,\n      \"numRemovedFiles\" -> numRemovedFiles,\n      \"numDeletionVectorsAdded\" -> numDeletionVectorsAdded,\n      \"numDeletionVectorsRemoved\" -> numDeletionVectorsRemoved,\n      \"numDeletionVectorsUpdated\" -> numDeletionVectorsUpdated)\n    (fullyRemoved ++ dvAddFilesWithStats ++ dvRemoveFiles, metricMap)\n  }\n\n  /** Fetch stats for `addFiles`. */\n  private def getActionsWithStats(\n      spark: SparkSession,\n      addFilesWithNewDvs: Seq[AddFile],\n      snapshot: Snapshot,\n      stringTruncateLength: Int): Seq[AddFile] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    if (addFilesWithNewDvs.isEmpty) return Seq.empty\n\n    val selectionPathAndStatsCols = Seq(col(\"path\"), col(\"stats\"))\n    val addFilesWithNewDvsDf = addFilesWithNewDvs.toDF(spark)\n\n    // These files originate from snapshot.filesForScan which resets column statistics.\n    // Since these object don't carry stats and tags, if we were to use them as result actions of\n    // the operation directly, we'd effectively be removing all stats and tags. To resolve this\n    // we join the list of files with DVs with the log (allFiles) to retrieve statistics. This is\n    // expected to have better performance than supporting full stats retrieval\n    // in snapshot.filesForScan because it only affects a subset of the scanned files.\n\n    // Find the current metadata with stats for all files with new DV\n    val addFileWithStatsDf = snapshot.withStats\n      .join(addFilesWithNewDvsDf.select(\"path\"), \"path\")\n\n    // Update the existing stats to set the tightBounds to false and also set the appropriate\n    // null count. We want to set the bounds before the AddFile has DV descriptor attached.\n    // Attaching the DV descriptor here, causes wrong logical records computation in\n    // `updateStatsToWideBounds`.\n    val statsColName = snapshot.getBaseStatsColumnName\n    val addFilesWithWideBoundsDf = snapshot\n      .updateStatsToWideBounds(addFileWithStatsDf, statsColName)\n\n    val (filesWithNoStats, filesWithExistingStats) = {\n      // numRecords is the only stat we really have to guarantee.\n      // If the others are missing, we do not need to fetch them.\n      addFilesWithWideBoundsDf.as[AddFile].collect().toSeq\n        .partition(_.numPhysicalRecords.isEmpty)\n    }\n\n    // If we encounter files with no stats we fetch the stats from the parquet footer.\n    // Files with persistent DVs *must* have (at least numRecords) stats according to the\n    // Delta spec.\n    val filesWithFetchedStats =\n      if (filesWithNoStats.nonEmpty) {\n        StatsCollectionUtils.computeStats(spark,\n          conf = snapshot.deltaLog.newDeltaHadoopConf(),\n          deltaLog = snapshot.deltaLog,\n          snapshot = snapshot,\n          addFiles = filesWithNoStats.toDS(spark),\n          numFilesOpt = Some(filesWithNoStats.size),\n          stringTruncateLength = stringTruncateLength,\n          setBoundsToWide = true)\n          .collect()\n          .toSeq\n      } else {\n        Seq.empty\n      }\n\n    val allAddFilesWithUpdatedStats =\n      (filesWithExistingStats ++ filesWithFetchedStats).toSeq.toDF(spark)\n\n    // Now join the allAddFilesWithUpdatedStats with addFilesWithNewDvs\n    // so that the updated stats are joined with the new DV info\n    addFilesWithNewDvsDf.drop(\"stats\")\n      .join(\n        allAddFilesWithUpdatedStats.select(selectionPathAndStatsCols: _*), \"path\")\n      .as[AddFile]\n      .collect()\n      .toSeq\n  }\n}\n\nobject DeletionVectorBitmapGenerator {\n  final val FILE_NAME_COL = \"filePath\"\n  final val FILE_DV_ID_COL = \"deletionVectorId\"\n  final val ROW_INDEX_COL = \"rowIndexCol\"\n  final val DELETED_ROW_INDEX_BITMAP = \"deletedRowIndexSet\"\n  final val DELETED_ROW_INDEX_COUNT = \"deletedRowIndexCount\"\n  final val MAX_ROW_INDEX_COL = \"maxRowIndexCol\"\n\n  private class DeletionVectorSet(\n    spark: SparkSession,\n    target: DataFrame,\n    targetDeltaLog: DeltaLog,\n    prefixLen: Int) {\n\n    case object CardinalityAndBitmapStruct {\n      val name: String = \"CardinalityAndBitmapStruct\"\n      def cardinality: String = s\"$name.cardinality\"\n      def bitmap: String = s\"$name.bitmap\"\n    }\n\n    def computeResult(): Seq[DeletionVectorResult] = {\n      val aggregated = target\n        .groupBy(col(FILE_NAME_COL), col(FILE_DV_ID_COL))\n        .agg(aggColumns.head, aggColumns.tail: _*)\n        .select(outputColumns: _*)\n\n      import DeletionVectorResult.encoder\n      val rowIndexData = aggregated.as[DeletionVectorData]\n      val storedResults = rowIndexData.mapPartitions(bitmapStorageMapper())\n      storedResults.as[DeletionVectorResult].collect()\n    }\n\n    protected def aggColumns: Seq[Column] = {\n      Seq(createBitmapSetAggregator(col(ROW_INDEX_COL)).as(CardinalityAndBitmapStruct.name))\n    }\n\n    /** Create a bitmap set aggregator over the given column */\n    private def createBitmapSetAggregator(indexColumn: Column): Column = {\n      val func = new BitmapAggregator(indexColumn.expr, RoaringBitmapArrayFormat.Portable)\n      Column(func.toAggregateExpression(isDistinct = false))\n    }\n\n    protected def outputColumns: Seq[Column] =\n      Seq(\n        col(FILE_NAME_COL),\n        col(FILE_DV_ID_COL),\n        col(CardinalityAndBitmapStruct.bitmap).as(DELETED_ROW_INDEX_BITMAP),\n        col(CardinalityAndBitmapStruct.cardinality).as(DELETED_ROW_INDEX_COUNT)\n      )\n\n    protected def bitmapStorageMapper()\n      : Iterator[DeletionVectorData] => Iterator[DeletionVectorResult] = {\n      DeletionVectorWriter.createMapperToStoreDeletionVectors(\n        spark,\n        targetDeltaLog.newDeltaHadoopConf(),\n        targetDeltaLog.dataPath,\n        prefixLen)\n    }\n  }\n\n  /**\n   * Build bitmap compressed sets of row indices for each file in [[target]] using\n   * [[ROW_INDEX_COL]].\n   * Write those sets out to temporary files and collect the file names,\n   * together with some encoded metadata about the contents.\n   *\n   * @param target  DataFrame with expected schema [[FILE_NAME_COL]], [[ROW_INDEX_COL]],\n   */\n  def buildDeletionVectors(\n      spark: SparkSession,\n      target: DataFrame,\n      targetDeltaLog: DeltaLog,\n      prefixLen: Int): Seq[DeletionVectorResult] = {\n    val rowIndexSet = new DeletionVectorSet(spark, target, targetDeltaLog, prefixLen)\n    rowIndexSet.computeResult()\n  }\n\n  /**\n   * Build a dataframe to find filtered rows with metadata (e.g., file name, row index and\n   * existing DV) from candidate files for DV update.\n   *\n   * @param sparkSession the active spark session\n   * @param tablePath table path of candidate files, used to absolutize path\n   * @param tableHasDVs whether table has DV enabled\n   * @param targetDf a scan of candidate files, whose attribute reference matches 'condition'\n   * @param candidateFiles candidate files to be scanned, used to get existing DVs.\n   * @param condition filter to be applied, whose attribute reference matches 'targetDf'\n   * @param fileNameColumnOpt optional overwrite of file name column name\n   * @param rowIndexColumnOpt optional overwrite of row index column name\n   * @return a dataframe containing filtered rows with corresponding metadata for DV update.\n   */\n  def buildRowIndexSetsForFilesMatchingCondition(\n      sparkSession: SparkSession,\n      tablePath: String,\n      tableHasDVs: Boolean,\n      targetDf: DataFrame,\n      candidateFiles: Seq[AddFile],\n      condition: Expression,\n      fileNameColumnOpt: Option[Column],\n      rowIndexColumnOpt: Option[Column]): DataFrame = {\n    val useMetadataRowIndexConf = DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX\n    val useMetadataRowIndex = sparkSession.sessionState.conf.getConf(useMetadataRowIndexConf)\n    val fileNameColumn = fileNameColumnOpt.getOrElse(col(s\"${METADATA_NAME}.${FILE_PATH}\"))\n    val rowIndexColumn = if (useMetadataRowIndex) {\n      rowIndexColumnOpt.getOrElse(col(s\"${METADATA_NAME}.${ParquetFileFormat.ROW_INDEX}\"))\n    } else {\n      rowIndexColumnOpt.getOrElse(col(ROW_INDEX_COLUMN_NAME))\n    }\n    val matchedRowsDf = targetDf\n      .withColumn(FILE_NAME_COL, fileNameColumn)\n      // Filter after getting input file name as the filter might introduce a join and we\n      // cannot get input file name on join's output.\n      .filter(Column(condition))\n      .withColumn(ROW_INDEX_COL, rowIndexColumn)\n\n    if (tableHasDVs) {\n      // When the table already has DVs, join the `matchedRowDf` above to attach for each matched\n      // file its existing DeletionVectorDescriptor\n      val filePathToDV = candidateFiles.map { add =>\n        val serializedDV = Option(add.deletionVector).map(_.serializeToBase64())\n        // Paths in the metadata column are canonicalized. Thus we must canonicalize the DV path.\n        FileToDvDescriptor(\n          SparkPath.fromPath(absolutePath(tablePath, add.path)).urlEncoded,\n          serializedDV)\n      }\n      val filePathToDVDf = sparkSession.createDataset(filePathToDV)\n\n      val joinExpr = filePathToDVDf(\"path\") === matchedRowsDf(FILE_NAME_COL)\n      // Perform leftOuter join to make sure we do not eliminate any rows because of path\n      // encoding issues. If there is such an issue we will detect it during the aggregation\n      // of the bitmaps.\n      val joinedDf = matchedRowsDf.join(filePathToDVDf, joinExpr, \"leftOuter\")\n        .drop(FILE_NAME_COL)\n        .withColumnRenamed(\"path\", FILE_NAME_COL)\n      joinedDf\n    } else {\n      // When the table has no DVs, just add a column to indicate that the existing dv is null\n      matchedRowsDf.withColumn(FILE_DV_ID_COL, lit(null))\n    }\n  }\n\n  /** The same as above, except it also updates DVs for the table using the dataframe. */\n  def buildRowIndexSetsForFilesMatchingCondition(\n      sparkSession: SparkSession,\n      txn: OptimisticTransaction,\n      tableHasDVs: Boolean,\n      targetDf: DataFrame,\n      candidateFiles: Seq[AddFile],\n      condition: Expression,\n      fileNameColumnOpt: Option[Column] = None,\n      rowIndexColumnOpt: Option[Column] = None): Seq[DeletionVectorResult] = {\n\n    val df = buildRowIndexSetsForFilesMatchingCondition(\n      sparkSession,\n      txn.deltaLog.dataPath.toString,\n      tableHasDVs,\n      targetDf,\n      candidateFiles,\n      condition,\n      fileNameColumnOpt,\n      rowIndexColumnOpt\n    )\n\n    DeletionVectorBitmapGenerator.buildDeletionVectors(\n      sparkSession, df, txn.deltaLog, DeltaUtils.getRandomPrefixLength(txn.metadata))\n  }\n}\n\n/**\n * Holds a mapping from a file path (url-encoded) to an (optional) serialized Deletion Vector\n * descriptor.\n */\ncase class FileToDvDescriptor(path: String, deletionVectorId: Option[String])\n\nobject FileToDvDescriptor {\n  private lazy val _encoder = new DeltaEncoder[FileToDvDescriptor]\n  implicit def encoder: Encoder[FileToDvDescriptor] = _encoder.get\n}\n\n/**\n * Row containing the file path and its new deletion vector bitmap in memory\n *\n * @param filePath             Absolute path of the data file this DV result is generated for.\n * @param deletionVectorId     Existing [[DeletionVectorDescriptor]] serialized in JSON format.\n *                             This info is used to load the existing DV with the new DV.\n * @param deletedRowIndexSet   In-memory Deletion vector bitmap generated containing the newly\n *                             deleted row indexes from data file.\n * @param deletedRowIndexCount Count of rows marked as deleted using the [[deletedRowIndexSet]].\n */\ncase class DeletionVectorData(\n    filePath: String,\n    deletionVectorId: Option[String],\n    deletedRowIndexSet: Array[Byte],\n    deletedRowIndexCount: Long) extends Sizing {\n\n  @transient\n  lazy val deletionVectorDescriptor: Option[DeletionVectorDescriptor] = deletionVectorId.map { id =>\n    DeletionVectorDescriptor.deserializeFromBase64(id)\n  }\n\n  /** The size of the bitmaps to use in [[BinPackingIterator]]. */\n  override def size: Int = {\n    val sizeWithoutExistingDV: Int = deletedRowIndexSet.length\n    // Add the size of the existing DV that we will eventually merge with, so that\n    // [[BinPackingIterator]] can get a better estimate. It's an estimate since the\n    // row indices are not merged and serialized. We add the size of the checksum\n    // and the size of the data size, which are fixed sizes added for every DV.\n    val sizeWithDV = sizeWithoutExistingDV +\n      deletionVectorDescriptor.map(_.sizeInBytes).getOrElse(0) +\n      DeletionVectorStore.CHECKSUM_LEN + DeletionVectorStore.DATA_SIZE_LEN\n    // If we have an int overflow, we can end up with a negative size. In that case,\n    // let's return the maximum value of Int and fail later when writing the DV writer.\n    if (sizeWithDV < 0) {\n      Int.MaxValue\n    } else {\n      sizeWithDV\n    }\n  }\n}\n\nobject DeletionVectorData {\n  private lazy val _encoder = new DeltaEncoder[DeletionVectorData]\n  implicit def encoder: Encoder[DeletionVectorData] = _encoder.get\n\n  def apply(filePath: String, rowIndexSet: Array[Byte], rowIndexCount: Long): DeletionVectorData = {\n    DeletionVectorData(\n      filePath = filePath,\n      deletionVectorId = None,\n      deletedRowIndexSet = rowIndexSet,\n      deletedRowIndexCount = rowIndexCount)\n  }\n}\n\n/** Final output for each file containing the file path, DeletionVectorDescriptor and how many\n * rows are marked as deleted in this file as part of the this operation (doesn't include rows that\n * are already marked as deleted).\n *\n * @param filePath        Absolute path of the data file this DV result is generated for.\n * @param deletionVector  Deletion vector generated containing the newly deleted row indices from\n *                        data file.\n * @param matchedRowCount Number of rows marked as deleted using the [[deletionVector]].\n */\ncase class DeletionVectorResult(\n    filePath: String,\n    deletionVector: DeletionVectorDescriptor,\n    matchedRowCount: Long) {\n}\n\nobject DeletionVectorResult {\n  private lazy val _encoder = new DeltaEncoder[DeletionVectorResult]\n  implicit def encoder: Encoder[DeletionVectorResult] = _encoder.get\n\n  def fromDeletionVectorData(\n      data: DeletionVectorData,\n      deletionVector: DeletionVectorDescriptor): DeletionVectorResult = {\n    DeletionVectorResult(\n      filePath = data.filePath,\n      deletionVector = deletionVector,\n      matchedRowCount = data.deletedRowIndexCount)\n  }\n}\n\ncase class TouchedFileWithDV(\n    inputFilePath: String,\n    fileLogEntry: AddFile,\n    newDeletionVector: DeletionVectorDescriptor,\n    deletedRows: Long) {\n  /**\n   * Checks the *sufficient* condition for a file being fully replaced by the current operation.\n   * (That is, all rows are either being updated or deleted.)\n   */\n  def isFullyReplaced(): Boolean = {\n    fileLogEntry.numLogicalRecords match {\n      case Some(numRecords) => numRecords == numberOfModifiedRows\n      case None => false // must make defensive assumption if no statistics are available\n    }\n  }\n\n  /**\n   * Checks if the file is unchanged by the current operation.\n   * (That is no row has been updated or deleted.)\n   */\n  def isUnchanged: Boolean = {\n    // If the bitmap is empty then no row would be removed during the rewrite,\n    // thus the file is unchanged.\n    numberOfModifiedRows == 0\n  }\n\n  /**\n   * The number of rows that are modified in this file.\n   */\n  def numberOfModifiedRows: Long = newDeletionVector.cardinality - fileLogEntry.numDeletedRecords\n}\n\n/**\n * Utility methods to write the deletion vector to storage. If a particular file already\n * has an existing DV, it will be merged with the new deletion vector and written to storage.\n */\nobject DeletionVectorWriter extends DeltaLogging {\n  /**\n   * The context for [[createDeletionVectorMapper]] callback functions. Contains the DV writer that\n   * is used by callback functions to write the new DVs.\n   */\n  case class DeletionVectorMapperContext(\n      dvStore: DeletionVectorStore,\n      writer: DeletionVectorStore.Writer,\n      tablePath: Path,\n      fileId: UUID,\n      prefix: String)\n\n  /**\n   * Prepare a mapper function for storing deletion vectors.\n   *\n   * For each DeletionVector the writer will create a [[DeletionVectorMapperContext]] that contains\n   * a DV writer that is used by to write the DV into a file.\n   *\n   * The result can be used with [[org.apache.spark.sql.Dataset.mapPartitions()]] and must thus be\n   * serialized.\n   */\n  def createDeletionVectorMapper[InputT <: Sizing, OutputT](\n      sparkSession: SparkSession,\n      hadoopConf: Configuration,\n      table: Path,\n      prefixLength: Int)\n      (callbackFn: (DeletionVectorMapperContext, InputT) => OutputT)\n    : Iterator[InputT] => Iterator[OutputT] = {\n    val broadcastHadoopConf = sparkSession.sparkContext.broadcast(\n      new SerializableConfiguration(hadoopConf))\n    // hadoop.fs.Path is not Serializable, so close over the String representation instead\n    val tablePathString = DeletionVectorStore.pathToEscapedString(table)\n    val packingTargetSize =\n      sparkSession.conf.get(DeltaSQLConf.DELETION_VECTOR_PACKING_TARGET_SIZE)\n\n    // This is the (partition) mapper function we are returning\n    (rowIterator: Iterator[InputT]) => {\n      val dvStore = DeletionVectorStore.createInstance(broadcastHadoopConf.value.value)\n      val tablePath = DeletionVectorStore.escapedStringToPath(tablePathString)\n      val tablePathWithFS = dvStore.pathWithFileSystem(tablePath)\n\n      val perBinFunction: Seq[InputT] => Seq[OutputT] = (rows: Seq[InputT]) => {\n        val prefix = DeltaUtils.getRandomPrefix(prefixLength)\n        val (writer, fileId) = createWriter(dvStore, tablePathWithFS, prefix)\n        val ctx = DeletionVectorMapperContext(\n          dvStore,\n          writer,\n          tablePath,\n          fileId,\n          prefix)\n        val result = SparkUtils.tryWithResource(writer) { writer =>\n          rows.map(r => callbackFn(ctx, r))\n        }\n        result\n      }\n\n      val binPackedRowIterator = new BinPackingIterator(rowIterator, packingTargetSize)\n      binPackedRowIterator.flatMap(perBinFunction)\n    }\n  }\n\n  /**\n   * Creates a writer for writing multiple DVs in the same file.\n   *\n   * Returns the writer and the UUID of the new file.\n   */\n  def createWriter(\n      dvStore: DeletionVectorStore,\n      tablePath: PathWithFileSystem,\n      prefix: String = \"\"): (DeletionVectorStore.Writer, UUID) = {\n    val fileId = UUID.randomUUID()\n    val writer = dvStore.createWriter(dvStore.generateFileNameInTable(tablePath, fileId, prefix))\n    (writer, fileId)\n  }\n\n  /** Store the `bitmapData` on cloud storage. */\n  def storeSerializedBitmap(\n      ctx: DeletionVectorMapperContext,\n      bitmapData: Array[Byte],\n      cardinality: Long): DeletionVectorDescriptor = {\n    if (cardinality == 0L) {\n      DeletionVectorDescriptor.EMPTY\n    } else {\n      val dvRange = ctx.writer.write(bitmapData)\n      DeletionVectorDescriptor.onDiskWithRelativePath(\n        id = ctx.fileId,\n        randomPrefix = ctx.prefix,\n        sizeInBytes = bitmapData.length,\n        cardinality = cardinality,\n        offset = Some(dvRange.offset))\n    }\n  }\n\n  /**\n   * Prepares a mapper function that can be used by DML commands to store the Deletion Vectors\n   * that are in described in [[DeletionVectorData]] and return their descriptors\n   * [[DeletionVectorResult]].\n   */\n  def createMapperToStoreDeletionVectors(\n      sparkSession: SparkSession,\n      hadoopConf: Configuration,\n      table: Path,\n      prefixLength: Int): Iterator[DeletionVectorData] => Iterator[DeletionVectorResult] =\n    createDeletionVectorMapper(sparkSession, hadoopConf, table, prefixLength) {\n      (ctx, row) => storeBitmapAndGenerateResult(ctx, row)\n    }\n\n  /**\n   * Helper to generate and store the deletion vector bitmap. The deletion vector is merged with\n   * the file's already existing deletion vector before being stored.\n   */\n  def storeBitmapAndGenerateResult(ctx: DeletionVectorMapperContext, row: DeletionVectorData)\n    : DeletionVectorResult = {\n    // If a group with null path exists it means there was an issue while joining with the log to\n    // fetch the DeletionVectorDescriptors.\n    assert(row.filePath != null,\n      s\"\"\"\n         |Encountered a non matched file path.\n         |It is likely that _metadata.file_path is not encoded by Spark as expected.\n         |\"\"\".stripMargin)\n\n    val fileDvDescriptor = row.deletionVectorDescriptor\n    val finalDvDescriptor = fileDvDescriptor match {\n      case Some(existingDvDescriptor) if row.deletedRowIndexCount > 0 =>\n        // Load the existing bit map\n        val existingBitmap =\n          StoredBitmap.create(existingDvDescriptor, ctx.tablePath).load(ctx.dvStore)\n        val newBitmap =\n          DeletionVectorUtils.deserialize(row.deletedRowIndexSet, Some(ctx.tablePath))\n        // Merge both the existing and new bitmaps into one, and finally persist on disk\n        existingBitmap.merge(newBitmap)\n\n        val serializedBitmap = DeletionVectorUtils.serialize(\n          existingBitmap,\n          RoaringBitmapArrayFormat.Portable,\n          Some(ctx.tablePath),\n          debugInfo = Map(\"existingDvDescriptor\" -> existingDvDescriptor))\n        storeSerializedBitmap(\n          ctx,\n          serializedBitmap,\n          existingBitmap.cardinality)\n      case Some(existingDvDescriptor) =>\n        existingDvDescriptor // This is already stored.\n      case None =>\n        // Persist the new bitmap\n        storeSerializedBitmap(ctx, row.deletedRowIndexSet, row.deletedRowIndexCount)\n    }\n    DeletionVectorResult.fromDeletionVectorData(row, deletionVector = finalDvDescriptor)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DeleteCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.util.concurrent.TimeUnit\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.metric.IncrementMetric\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile, FileAction}\nimport org.apache.spark.sql.delta.commands.DeleteCommand.{rewritingFilesMsg, FINDING_TOUCHED_FILES_MSG}\nimport org.apache.spark.sql.delta.commands.MergeIntoCommandBase.totalBytesAndDistinctPartitionValues\nimport org.apache.spark.sql.delta.files.TahoeBatchFileIndex\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatsCollectionUtils\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.sql.{Column, DataFrame, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.EliminateSubqueryAliases\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, EqualNullSafe, Expression, If, Literal, Not}\nimport org.apache.spark.sql.catalyst.expressions.Literal.TrueLiteral\nimport org.apache.spark.sql.catalyst.plans.QueryPlan\nimport org.apache.spark.sql.catalyst.plans.logical.{DeltaDelete, LogicalPlan}\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.execution.metric.SQLMetrics.{createMetric, createTimingMetric}\nimport org.apache.spark.sql.functions.input_file_name\nimport org.apache.spark.sql.types.LongType\n\ntrait DeleteCommandMetrics { self: LeafRunnableCommand =>\n  @transient private lazy val sc: SparkContext = SparkContext.getOrCreate()\n\n  def createMetrics: Map[String, SQLMetric] = Map[String, SQLMetric](\n    \"numRemovedFiles\" -> createMetric(sc, \"number of files removed.\"),\n    \"numAddedFiles\" -> createMetric(sc, \"number of files added.\"),\n    \"numDeletedRows\" -> createMetric(sc, \"number of rows deleted.\"),\n    \"numFilesBeforeSkipping\" -> createMetric(sc, \"number of files before skipping\"),\n    \"numBytesBeforeSkipping\" -> createMetric(sc, \"number of bytes before skipping\"),\n    \"numFilesAfterSkipping\" -> createMetric(sc, \"number of files after skipping\"),\n    \"numBytesAfterSkipping\" -> createMetric(sc, \"number of bytes after skipping\"),\n    \"numPartitionsAfterSkipping\" -> createMetric(sc, \"number of partitions after skipping\"),\n    \"numPartitionsAddedTo\" -> createMetric(sc, \"number of partitions added\"),\n    \"numPartitionsRemovedFrom\" -> createMetric(sc, \"number of partitions removed\"),\n    \"numCopiedRows\" -> createMetric(sc, \"number of rows copied\"),\n    \"numAddedBytes\" -> createMetric(sc, \"number of bytes added\"),\n    \"numRemovedBytes\" -> createMetric(sc, \"number of bytes removed\"),\n    \"executionTimeMs\" ->\n      createTimingMetric(sc, \"time taken to execute the entire operation\"),\n    \"scanTimeMs\" ->\n      createTimingMetric(sc, \"time taken to scan the files for matches\"),\n    \"rewriteTimeMs\" ->\n      createTimingMetric(sc, \"time taken to rewrite the matched files\"),\n    \"numAddedChangeFiles\" -> createMetric(sc, \"number of change data capture files generated\"),\n    \"changeFileBytes\" -> createMetric(sc, \"total size of change data capture files generated\"),\n    \"numTouchedRows\" -> createMetric(sc, \"number of rows touched\"),\n    \"numDeletionVectorsAdded\" -> createMetric(sc, \"number of deletion vectors added\"),\n    \"numDeletionVectorsRemoved\" -> createMetric(sc, \"number of deletion vectors removed\"),\n    \"numDeletionVectorsUpdated\" -> createMetric(sc, \"number of deletion vectors updated\")\n  )\n\n  def getDeletedRowsFromAddFilesAndUpdateMetrics(files: Seq[AddFile]) : Option[Long] = {\n    if (!conf.getConf(DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA)) {\n      return None;\n    }\n    // No file to get metadata, return none to be consistent with metadata stats disabled\n    if (files.isEmpty) {\n      return None\n    }\n    // Return None if any file does not contain numLogicalRecords status\n    var count: Long = 0\n    for (file <- files) {\n      if (file.numLogicalRecords.isEmpty) {\n        return None\n      }\n      count += file.numLogicalRecords.get\n    }\n    metrics(\"numDeletedRows\").set(count)\n    return Some(count)\n  }\n}\n\n/**\n * Performs a Delete based on the search condition\n *\n * Algorithm:\n *   1) Scan all the files and determine which files have\n *      the rows that need to be deleted.\n *   2) Traverse the affected files and rebuild the touched files.\n *   3) Use the Delta protocol to atomically write the remaining rows to new files and remove\n *      the affected files that are identified in step 1.\n */\ncase class DeleteCommand(\n    deltaLog: DeltaLog,\n    catalogTable: Option[CatalogTable],\n    target: LogicalPlan,\n    condition: Option[Expression])\n  extends LeafRunnableCommand with DeltaCommand with DeleteCommandMetrics {\n\n  override def innerChildren: Seq[QueryPlan[_]] = Seq(target)\n\n  override val output: Seq[Attribute] = Seq(AttributeReference(\"num_affected_rows\", LongType)())\n\n  override lazy val metrics = createMetrics\n\n  final override def run(sparkSession: SparkSession): Seq[Row] = {\n    recordDeltaOperation(deltaLog, \"delta.dml.delete\") {\n      deltaLog.withNewTransaction(catalogTable) { txn =>\n        DeltaLog.assertRemovable(txn.snapshot)\n        if (hasBeenExecuted(txn, sparkSession)) {\n          sendDriverMetrics(sparkSession, metrics)\n          return Seq.empty\n        }\n\n        val (deleteActions, deleteMetrics) = performDelete(sparkSession, deltaLog, txn)\n        val numRecordsStats = NumRecordsStats.fromActions(deleteActions)\n        val operation = DeltaOperations.Delete(condition.toSeq)\n        validateNumRecords(deleteActions, numRecordsStats, operation)\n        val commitVersion = txn.commitIfNeeded(\n          actions = deleteActions,\n          op = operation,\n          tags = RowTracking.addPreservedRowTrackingTagIfNotSet(txn.snapshot))\n        recordDeltaEvent(\n          deltaLog,\n          \"delta.dml.delete.stats\",\n          data = deleteMetrics.copy(commitVersion = commitVersion))\n      }\n      // Re-cache all cached plans(including this relation itself, if it's cached) that refer to\n      // this data source relation.\n      sparkSession.sharedState.cacheManager.recacheByPlan(sparkSession, target)\n    }\n\n    // Adjust for deletes at partition boundaries. Deletes at partition boundaries is a metadata\n    // operation, therefore we don't actually have any information around how many rows were deleted\n    // While this info may exist in the file statistics, it's not guaranteed that we have these\n    // statistics. To avoid any performance regressions, we currently just return a -1 in such cases\n    if (metrics(\"numRemovedFiles\").value > 0 && metrics(\"numDeletedRows\").value == 0) {\n      Seq(Row(-1L))\n    } else {\n      Seq(Row(metrics(\"numDeletedRows\").value))\n    }\n  }\n\n  def performDelete(\n      sparkSession: SparkSession,\n      deltaLog: DeltaLog,\n      txn: OptimisticTransaction): (Seq[Action], DeleteMetric) = {\n    import org.apache.spark.sql.delta.implicits._\n\n    var numRemovedFiles: Long = 0\n    var numAddedFiles: Long = 0\n    var numAddedChangeFiles: Long = 0\n    var scanTimeMs: Long = 0\n    var rewriteTimeMs: Long = 0\n    var numAddedBytes: Long = 0\n    var changeFileBytes: Long = 0\n    var numRemovedBytes: Long = 0\n    var numFilesBeforeSkipping: Long = 0\n    var numBytesBeforeSkipping: Long = 0\n    var numFilesAfterSkipping: Long = 0\n    var numBytesAfterSkipping: Long = 0\n    var numPartitionsAfterSkipping: Option[Long] = None\n    var numPartitionsRemovedFrom: Option[Long] = None\n    var numPartitionsAddedTo: Option[Long] = None\n    var numDeletedRows: Option[Long] = None\n    var numCopiedRows: Option[Long] = None\n    var numDeletionVectorsAdded: Long = 0\n    var numDeletionVectorsRemoved: Long = 0\n    var numDeletionVectorsUpdated: Long = 0\n\n    val startTime = System.nanoTime()\n    val numFilesTotal = txn.snapshot.numOfFiles\n\n    val deleteActions: Seq[Action] = condition match {\n      case None =>\n        // Case 1: Delete the whole table if the condition is true\n        val reportRowLevelMetrics = conf.getConf(DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA)\n        val allFiles = txn.filterFiles(Nil, keepNumRecords = reportRowLevelMetrics)\n\n        numRemovedFiles = allFiles.size\n        numDeletionVectorsRemoved = allFiles.count(_.deletionVector != null)\n        scanTimeMs = (System.nanoTime() - startTime) / 1000 / 1000\n        val (numBytes, numPartitions) = totalBytesAndDistinctPartitionValues(allFiles)\n        numRemovedBytes = numBytes\n        numFilesBeforeSkipping = numRemovedFiles\n        numBytesBeforeSkipping = numBytes\n        numFilesAfterSkipping = numRemovedFiles\n        numBytesAfterSkipping = numBytes\n        numDeletedRows = getDeletedRowsFromAddFilesAndUpdateMetrics(allFiles)\n\n        if (txn.metadata.partitionColumns.nonEmpty) {\n          numPartitionsAfterSkipping = Some(numPartitions)\n          numPartitionsRemovedFrom = Some(numPartitions)\n          numPartitionsAddedTo = Some(0)\n        }\n        val operationTimestamp = System.currentTimeMillis()\n        allFiles.map(_.removeWithTimestamp(operationTimestamp))\n      case Some(cond) =>\n        val (metadataPredicates, otherPredicates) =\n          DeltaTableUtils.splitMetadataAndDataPredicates(\n            cond, txn.metadata.partitionColumns, sparkSession)\n\n        numFilesBeforeSkipping = txn.snapshot.numOfFiles\n        numBytesBeforeSkipping = txn.snapshot.sizeInBytes\n\n        if (otherPredicates.isEmpty) {\n          // Case 2: The condition can be evaluated using metadata only.\n          //         Delete a set of files without the need of scanning any data files.\n          val operationTimestamp = System.currentTimeMillis()\n          val reportRowLevelMetrics = conf.getConf(DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA)\n          val candidateFiles =\n            txn.filterFiles(metadataPredicates, keepNumRecords = reportRowLevelMetrics)\n\n          scanTimeMs = (System.nanoTime() - startTime) / 1000 / 1000\n          numRemovedFiles = candidateFiles.size\n          numRemovedBytes = candidateFiles.map(_.size).sum\n          numFilesAfterSkipping = candidateFiles.size\n          numDeletionVectorsRemoved = candidateFiles.count(_.deletionVector != null)\n          val (numCandidateBytes, numCandidatePartitions) =\n            totalBytesAndDistinctPartitionValues(candidateFiles)\n          numBytesAfterSkipping = numCandidateBytes\n          numDeletedRows = getDeletedRowsFromAddFilesAndUpdateMetrics(candidateFiles)\n\n          if (txn.metadata.partitionColumns.nonEmpty) {\n            numPartitionsAfterSkipping = Some(numCandidatePartitions)\n            numPartitionsRemovedFrom = Some(numCandidatePartitions)\n            numPartitionsAddedTo = Some(0)\n          }\n          candidateFiles.map(_.removeWithTimestamp(operationTimestamp))\n        } else {\n          // Case 3: Delete the rows based on the condition.\n\n          // Should we write the DVs to represent the deleted rows?\n          val shouldWriteDVs = shouldWritePersistentDeletionVectors(sparkSession, txn)\n\n          val candidateFiles = txn.filterFiles(\n            metadataPredicates ++ otherPredicates,\n            keepNumRecords = shouldWriteDVs)\n          // `candidateFiles` contains the files filtered using statistics and delete condition\n          // They may or may not contains any rows that need to be deleted.\n\n          numFilesAfterSkipping = candidateFiles.size\n          val (numCandidateBytes, numCandidatePartitions) =\n            totalBytesAndDistinctPartitionValues(candidateFiles)\n          numBytesAfterSkipping = numCandidateBytes\n          if (txn.metadata.partitionColumns.nonEmpty) {\n            numPartitionsAfterSkipping = Some(numCandidatePartitions)\n          }\n\n          val nameToAddFileMap = generateCandidateFileMap(deltaLog.dataPath, candidateFiles)\n\n          val fileIndex = new TahoeBatchFileIndex(\n            sparkSession, \"delete\", candidateFiles, deltaLog, deltaLog.dataPath, txn.snapshot)\n          if (shouldWriteDVs) {\n            val targetDf = DMLWithDeletionVectorsHelper.createTargetDfForScanningForMatches(\n              sparkSession,\n              target,\n              fileIndex)\n\n            // Does the target table already has DVs enabled? If so, we need to read the table\n            // with deletion vectors.\n            val mustReadDeletionVectors = DeletionVectorUtils.deletionVectorsReadable(txn.snapshot)\n\n            val touchedFiles = DMLWithDeletionVectorsHelper.findTouchedFiles(\n              sparkSession,\n              txn,\n              mustReadDeletionVectors,\n              deltaLog,\n              targetDf,\n              fileIndex,\n              cond,\n              opName = \"DELETE\")\n\n            if (touchedFiles.nonEmpty) {\n              val stringTruncateLength = StatsCollectionUtils.getDataSkippingStringPrefixLength(\n                sparkSession, txn.metadata)\n              val (actions, metricMap) = DMLWithDeletionVectorsHelper.processUnmodifiedData(\n                sparkSession,\n                touchedFiles,\n                txn.snapshot,\n                stringTruncateLength)\n              metrics(\"numDeletedRows\").set(metricMap(\"numModifiedRows\"))\n              numDeletionVectorsAdded = metricMap(\"numDeletionVectorsAdded\")\n              numDeletionVectorsRemoved = metricMap(\"numDeletionVectorsRemoved\")\n              numDeletionVectorsUpdated = metricMap(\"numDeletionVectorsUpdated\")\n              numRemovedFiles = metricMap(\"numRemovedFiles\")\n              actions\n            } else {\n              Nil // Nothing to update\n            }\n          } else {\n            // Keep everything from the resolved target except a new TahoeFileIndex\n            // that only involves the affected files instead of all files.\n            val newTarget = DeltaTableUtils.replaceFileIndex(target, fileIndex)\n            val data = DataFrameUtils.ofRows(sparkSession, newTarget)\n            val incrDeletedCountExpr = IncrementMetric(TrueLiteral, metrics(\"numDeletedRows\"))\n            val filesToRewrite =\n              withStatusCode(\"DELTA\", FINDING_TOUCHED_FILES_MSG) {\n                if (candidateFiles.isEmpty) {\n                  Array.empty[String]\n                } else {\n                  data.filter(Column(cond))\n                    .select(input_file_name())\n                    .filter(Column(incrDeletedCountExpr))\n                    .distinct()\n                    .as[String]\n                    .collect()\n                }\n              }\n\n            numRemovedFiles = filesToRewrite.length\n            scanTimeMs = (System.nanoTime() - startTime) / 1000 / 1000\n            if (filesToRewrite.isEmpty) {\n              // Case 3.1: no row matches and no delete will be triggered\n              if (txn.metadata.partitionColumns.nonEmpty) {\n                numPartitionsRemovedFrom = Some(0)\n                numPartitionsAddedTo = Some(0)\n              }\n              Nil\n            } else {\n              // Case 3.2: some files need an update to remove the deleted files\n              // Do the second pass and just read the affected files\n              val baseRelation = buildBaseRelation(\n                sparkSession, txn, \"delete\", deltaLog.dataPath, filesToRewrite, nameToAddFileMap)\n              // Keep everything from the resolved target except a new TahoeFileIndex\n              // that only involves the affected files instead of all files.\n              val newTarget = DeltaTableUtils.replaceFileIndex(target, baseRelation.location)\n              val targetDF = RowTracking.preserveRowTrackingColumns(\n                dfWithoutRowTrackingColumns = DataFrameUtils.ofRows(sparkSession, newTarget),\n                snapshot = txn.snapshot)\n              val filterCond = Not(EqualNullSafe(cond, Literal.TrueLiteral))\n              val rewrittenActions = rewriteFiles(txn, targetDF, filterCond, filesToRewrite.length)\n              val (changeFiles, rewrittenFiles) = rewrittenActions\n                .partition(_.isInstanceOf[AddCDCFile])\n              numAddedFiles = rewrittenFiles.size\n              val removedFiles = filesToRewrite.map(f =>\n                getTouchedFile(deltaLog.dataPath, f, nameToAddFileMap))\n              val (removedBytes, removedPartitions) =\n                totalBytesAndDistinctPartitionValues(removedFiles)\n              numRemovedBytes = removedBytes\n              val (rewrittenBytes, rewrittenPartitions) =\n                totalBytesAndDistinctPartitionValues(rewrittenFiles)\n              numAddedBytes = rewrittenBytes\n              if (txn.metadata.partitionColumns.nonEmpty) {\n                numPartitionsRemovedFrom = Some(removedPartitions)\n                numPartitionsAddedTo = Some(rewrittenPartitions)\n              }\n              numAddedChangeFiles = changeFiles.size\n              changeFileBytes = changeFiles.collect { case f: AddCDCFile => f.size }.sum\n              rewriteTimeMs =\n                TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) - scanTimeMs\n              numDeletedRows = Some(metrics(\"numDeletedRows\").value)\n              numCopiedRows =\n                Some(metrics(\"numTouchedRows\").value - metrics(\"numDeletedRows\").value)\n              numDeletionVectorsRemoved = removedFiles.count(_.deletionVector != null)\n              val operationTimestamp = System.currentTimeMillis()\n              removeFilesFromPaths(\n                deltaLog, nameToAddFileMap, filesToRewrite, operationTimestamp) ++ rewrittenActions\n            }\n          }\n        }\n    }\n    metrics(\"numRemovedFiles\").set(numRemovedFiles)\n    metrics(\"numAddedFiles\").set(numAddedFiles)\n    val executionTimeMs = (System.nanoTime() - startTime) / 1000 / 1000\n    metrics(\"executionTimeMs\").set(executionTimeMs)\n    metrics(\"scanTimeMs\").set(scanTimeMs)\n    metrics(\"rewriteTimeMs\").set(rewriteTimeMs)\n    metrics(\"numAddedChangeFiles\").set(numAddedChangeFiles)\n    metrics(\"changeFileBytes\").set(changeFileBytes)\n    metrics(\"numAddedBytes\").set(numAddedBytes)\n    metrics(\"numRemovedBytes\").set(numRemovedBytes)\n    metrics(\"numFilesBeforeSkipping\").set(numFilesBeforeSkipping)\n    metrics(\"numBytesBeforeSkipping\").set(numBytesBeforeSkipping)\n    metrics(\"numFilesAfterSkipping\").set(numFilesAfterSkipping)\n    metrics(\"numBytesAfterSkipping\").set(numBytesAfterSkipping)\n    metrics(\"numDeletionVectorsAdded\").set(numDeletionVectorsAdded)\n    metrics(\"numDeletionVectorsRemoved\").set(numDeletionVectorsRemoved)\n    metrics(\"numDeletionVectorsUpdated\").set(numDeletionVectorsUpdated)\n    numPartitionsAfterSkipping.foreach(metrics(\"numPartitionsAfterSkipping\").set)\n    numPartitionsAddedTo.foreach(metrics(\"numPartitionsAddedTo\").set)\n    numPartitionsRemovedFrom.foreach(metrics(\"numPartitionsRemovedFrom\").set)\n    numCopiedRows.foreach(metrics(\"numCopiedRows\").set)\n    txn.registerSQLMetrics(sparkSession, metrics)\n    sendDriverMetrics(sparkSession, metrics)\n\n    val numRecordsStats = NumRecordsStats.fromActions(deleteActions)\n    val deleteMetric = DeleteMetric(\n        condition = condition.map(_.sql).getOrElse(\"true\"),\n        numFilesTotal,\n        numFilesAfterSkipping,\n        numAddedFiles,\n        numRemovedFiles,\n        numAddedFiles,\n        numAddedChangeFiles = numAddedChangeFiles,\n        numFilesBeforeSkipping,\n        numBytesBeforeSkipping,\n        numFilesAfterSkipping,\n        numBytesAfterSkipping,\n        numPartitionsAfterSkipping,\n        numPartitionsAddedTo,\n        numPartitionsRemovedFrom,\n        numCopiedRows,\n        numDeletedRows,\n        numAddedBytes,\n        numRemovedBytes,\n        changeFileBytes = changeFileBytes,\n        scanTimeMs,\n        rewriteTimeMs,\n        numDeletionVectorsAdded,\n        numDeletionVectorsRemoved,\n        numDeletionVectorsUpdated,\n        numLogicalRecordsAdded = numRecordsStats.numLogicalRecordsAdded,\n        numLogicalRecordsRemoved = numRecordsStats.numLogicalRecordsRemoved)\n\n    val actionsToCommit = if (deleteActions.nonEmpty) {\n      createSetTransaction(sparkSession, deltaLog).toSeq ++ deleteActions\n    } else {\n      Seq.empty\n    }\n    (actionsToCommit, deleteMetric)\n  }\n\n  /**\n   * Returns the list of [[AddFile]]s and [[AddCDCFile]]s that have been re-written.\n   */\n  private def rewriteFiles(\n      txn: OptimisticTransaction,\n      baseData: DataFrame,\n      filterCondition: Expression,\n      numFilesToRewrite: Long): Seq[FileAction] = {\n    val shouldWriteCdc = DeltaConfigs.CHANGE_DATA_FEED.fromMetaData(txn.metadata)\n\n    // number of total rows that we have seen / are either copying or deleting (sum of both).\n    val incrTouchedCountExpr = IncrementMetric(TrueLiteral, metrics(\"numTouchedRows\"))\n\n    withStatusCode(\n      \"DELTA\", rewritingFilesMsg(numFilesToRewrite)) {\n      val dfToWrite = if (shouldWriteCdc) {\n        import org.apache.spark.sql.delta.commands.cdc.CDCReader._\n        // The logic here ends up being surprisingly elegant, with all source rows ending up in\n        // the output. Recall that we flipped the user-provided delete condition earlier, before the\n        // call to `rewriteFiles`. All rows which match this latest `filterCondition` are retained\n        // as table data, while all rows which don't match are removed from the rewritten table data\n        // but do get included in the output as CDC events.\n        baseData\n          .filter(Column(incrTouchedCountExpr))\n          .withColumn(\n            CDC_TYPE_COLUMN_NAME,\n            Column(If(filterCondition, CDC_TYPE_NOT_CDC, CDC_TYPE_DELETE))\n          )\n      } else {\n        baseData\n          .filter(Column(incrTouchedCountExpr))\n          .filter(Column(filterCondition))\n      }\n\n      txn.writeFiles(dfToWrite)\n    }\n  }\n\n  def shouldWritePersistentDeletionVectors(\n      spark: SparkSession, txn: OptimisticTransaction): Boolean = {\n    spark.conf.get(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS) &&\n      DeletionVectorUtils.deletionVectorsWritable(txn.snapshot)\n  }\n\n  /**\n   * Validates that the number of records does not increase.\n   *\n   * Note: ideally we would also compare the number of added/removed rows in the statistics with the\n   * number of deleted/copied rows in the SQL metrics, but unfortunately this is not possible, as\n   * sql metrics are not reliable when there are task or stage retries.\n   */\n  private def validateNumRecords(\n      actions: Seq[Action],\n      numRecordsStats: NumRecordsStats,\n      op: Operation): Unit = {\n    (numRecordsStats.numLogicalRecordsAdded,\n      numRecordsStats.numLogicalRecordsRemoved,\n      numRecordsStats.numLogicalRecordsAddedInFilesWithDeletionVectors) match {\n      case (\n        Some(numAddedRecords),\n        Some(numRemovedRecords),\n        Some(numRecordsNotCopied)) =>\n        if (numAddedRecords > numRemovedRecords) {\n          logNumRecordsMismatch(deltaLog, actions, numRecordsStats, op)\n          if (conf.getConf(DeltaSQLConf.NUM_RECORDS_VALIDATION_ENABLED)) {\n            throw DeltaErrors.numRecordsMismatch(\n              operation = \"DELETE\",\n              numAddedRecords,\n              numRemovedRecords\n            )\n          }\n        }\n\n        if (conf.getConf(DeltaSQLConf.COMMAND_INVARIANT_CHECKS_USE_UNRELIABLE)) {\n          // and also using regular (unreliable) metrics for baseline\n          validateMetricBasedCommandInvariants(\n            numAddedRecords, numRemovedRecords, numRecordsNotCopied, op, deltaLog)\n        }\n\n      case _ =>\n        recordDeltaEvent(deltaLog, opType = \"delta.assertions.statsNotPresentForNumRecordsCheck\")\n        logWarning(log\"Could not validate number of records due to missing statistics.\")\n    }\n  }\n\n  private def validateMetricBasedCommandInvariants(\n      numAddedRecords: Long,\n      numRemovedRecords: Long,\n      numRecordsNotCopied: Long,\n      op: Operation,\n      deltaLog: DeltaLog): Unit = try {\n\n    val numRowsDeleted = CommandInvariantMetricValueFromSingle(metrics(\"numDeletedRows\"))\n    val numRowsCopied = CommandInvariantMetricValueFromSingle(metrics(\"numCopiedRows\"))\n\n    val recordMetricsFromMetadata = conf.getConf(DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA)\n    if (numRowsDeleted.getOrDummy == 0 && !recordMetricsFromMetadata) {\n      // If we don't record metrics we can't use them to perform invariant checks.\n      return\n    }\n\n    checkCommandInvariant(\n      invariant = () =>\n        numRowsDeleted.getOrThrow + numRowsCopied.getOrThrow + numRecordsNotCopied\n          == numRemovedRecords,\n      label = \"numRowsDeleted + numRowsCopied + numRecordsNotCopied + \" +\n        \"numRowsRemovedByMetadataOnlyDelete == numRemovedRecords\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsDeleted\" -> numRowsDeleted.getOrDummy,\n        \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n        \"numRemovedRecords\" -> numRemovedRecords,\n        \"numRecordsNotCopied\" -> numRecordsNotCopied\n      ),\n      additionalInfo = Map(\n        DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA.key -> recordMetricsFromMetadata.toString\n      )\n    )\n\n    checkCommandInvariant(\n      invariant = () => numRowsCopied.getOrThrow + numRecordsNotCopied == numAddedRecords,\n      label = \"numRowsCopied + numRecordsNotCopied == numAddedRecords\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n        \"numAddedRecords\" -> numAddedRecords,\n        \"numRecordsNotCopied\" -> numRecordsNotCopied\n      ),\n      additionalInfo = Map(\n        DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA.key -> recordMetricsFromMetadata.toString\n      )\n    )\n  } catch {\n    // Immediately re-throw actual command invariant violations, so we don't re-wrap them below.\n    case e: DeltaIllegalStateException if e.getErrorClass == \"DELTA_COMMAND_INVARIANT_VIOLATION\" =>\n      throw e\n    case NonFatal(e) =>\n      logWarning(log\"Unexpected error in validateMetricBasedCommandInvariants\", e)\n      checkCommandInvariant(\n        invariant = () => false,\n        label = \"Unexpected error in validateMetricBasedCommandInvariants\",\n        op = op,\n        deltaLog = deltaLog,\n        parameters = Map.empty\n      )\n  }\n}\n\nobject DeleteCommand {\n  def apply(delete: DeltaDelete): DeleteCommand = {\n    EliminateSubqueryAliases(delete.child) match {\n      case DeltaFullTable(relation, fileIndex) =>\n        DeleteCommand(fileIndex.deltaLog, relation.catalogTable, delete.child, delete.condition)\n      case o =>\n        throw DeltaErrors.notADeltaSourceException(\"DELETE\", Some(o))\n    }\n  }\n\n  val FILE_NAME_COLUMN: String = \"_input_file_name_\"\n  val FINDING_TOUCHED_FILES_MSG: String = \"Finding files to rewrite for DELETE operation\"\n\n  def rewritingFilesMsg(numFilesToRewrite: Long): String =\n    s\"Rewriting $numFilesToRewrite files for DELETE operation\"\n}\n\n/**\n * Used to report details about delete.\n *\n * @param condition: what was the delete condition\n * @param numFilesTotal: how big is the table\n * @param numTouchedFiles: how many files did we touch. Alias for `numFilesAfterSkipping`\n * @param numRewrittenFiles: how many files had to be rewritten. Alias for `numAddedFiles`\n * @param numRemovedFiles: how many files we removed. Alias for `numTouchedFiles`\n * @param numAddedFiles: how many files we added. Alias for `numRewrittenFiles`\n * @param numAddedChangeFiles: how many change files were generated\n * @param numFilesBeforeSkipping: how many candidate files before skipping\n * @param numBytesBeforeSkipping: how many candidate bytes before skipping\n * @param numFilesAfterSkipping: how many candidate files after skipping\n * @param numBytesAfterSkipping: how many candidate bytes after skipping\n * @param numPartitionsAfterSkipping: how many candidate partitions after skipping\n * @param numPartitionsAddedTo: how many new partitions were added\n * @param numPartitionsRemovedFrom: how many partitions were removed\n * @param numCopiedRows: how many rows were copied\n * @param numDeletedRows: how many rows were deleted\n * @param numBytesAdded: how many bytes were added\n * @param numBytesRemoved: how many bytes were removed\n * @param changeFileBytes: total size of change files generated\n * @param scanTimeMs: how long did finding take\n * @param rewriteTimeMs: how long did rewriting take\n * @param numDeletionVectorsAdded: how many deletion vectors were added\n * @param numDeletionVectorsRemoved: how many deletion vectors were removed\n * @param numDeletionVectorsUpdated: how many deletion vectors were updated\n *\n * @note All the time units are milliseconds.\n */\ncase class DeleteMetric(\n    condition: String,\n    numFilesTotal: Long,\n    numTouchedFiles: Long,\n    numRewrittenFiles: Long,\n    numRemovedFiles: Long,\n    numAddedFiles: Long,\n    numAddedChangeFiles: Long,\n    numFilesBeforeSkipping: Long,\n    numBytesBeforeSkipping: Long,\n    numFilesAfterSkipping: Long,\n    numBytesAfterSkipping: Long,\n    numPartitionsAfterSkipping: Option[Long],\n    numPartitionsAddedTo: Option[Long],\n    numPartitionsRemovedFrom: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numCopiedRows: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numDeletedRows: Option[Long],\n    numBytesAdded: Long,\n    numBytesRemoved: Long,\n    changeFileBytes: Long,\n    scanTimeMs: Long,\n    rewriteTimeMs: Long,\n    numDeletionVectorsAdded: Long,\n    numDeletionVectorsRemoved: Long,\n    numDeletionVectorsUpdated: Long,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    commitVersion: Option[Long] = None,\n    isWriteCommand: Boolean = false,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numLogicalRecordsAdded: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numLogicalRecordsRemoved: Option[Long] = None\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DeletionVectorUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.{DeletionVectorsTableFeature, DeltaConfigs, Snapshot, SnapshotDescriptor}\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\nimport org.apache.spark.sql.delta.files.SupportsRowIndexFilters\nimport org.apache.spark.sql.delta.files.TahoeFileIndex\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.datasources.FileIndex\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.util.Utils\n\ntrait DeletionVectorUtils extends DeltaLogging {\n\n  /**\n   * Run a query on the delta log to determine if the given snapshot contains no deletion vectors.\n   * Return `false` if it does contain deletion vectors.\n   */\n  def isTableDVFree(snapshot: Snapshot): Boolean = {\n    val dvsReadable = deletionVectorsReadable(snapshot)\n\n    if (dvsReadable) {\n      val dvCount = snapshot.allFiles\n        .filter(col(\"deletionVector\").isNotNull)\n        .limit(1)\n        .count()\n\n      dvCount == 0L\n    } else {\n      true\n    }\n  }\n\n  /**\n   * Returns true if persistent deletion vectors are enabled and\n   * readable with the current reader version.\n   */\n  def fileIndexSupportsReadingDVs(fileIndex: FileIndex): Boolean = fileIndex match {\n    case index: TahoeFileIndex => deletionVectorsReadable(index)\n    case _: SupportsRowIndexFilters => true\n    case _ => false\n  }\n\n  def deletionVectorsWritable(\n      snapshot: SnapshotDescriptor,\n      newProtocol: Option[Protocol] = None,\n      newMetadata: Option[Metadata] = None): Boolean =\n    deletionVectorsWritable(\n      protocol = newProtocol.getOrElse(snapshot.protocol),\n      metadata = newMetadata.getOrElse(snapshot.metadata))\n\n  def deletionVectorsWritable(protocol: Protocol, metadata: Metadata): Boolean =\n    protocol.isFeatureSupported(DeletionVectorsTableFeature) &&\n      DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(metadata)\n\n  def deletionVectorsReadable(\n      snapshot: SnapshotDescriptor,\n      newProtocol: Option[Protocol] = None,\n      newMetadata: Option[Metadata] = None): Boolean = {\n    deletionVectorsReadable(\n      newProtocol.getOrElse(snapshot.protocol),\n      newMetadata.getOrElse(snapshot.metadata))\n  }\n\n  def deletionVectorsReadable(\n      protocol: Protocol,\n      metadata: Metadata): Boolean = {\n    protocol.isFeatureSupported(DeletionVectorsTableFeature) &&\n      metadata.format.provider == \"parquet\" // DVs are only supported on parquet tables.\n  }\n\n  /**\n   * Serializes `bitmap` into a byte array using `serializationFormat`. If it fails, it records a\n   * delta event and re-throws the exception.\n   */\n  def serialize(\n      bitmap: RoaringBitmapArray,\n      serializationFormat: RoaringBitmapArrayFormat.Value,\n      tablePath: Option[Path] = None,\n      debugInfo: Map[String, Any] = Map.empty): Array[Byte] = {\n    try {\n      bitmap.serializeAsByteArray(serializationFormat)\n    } catch {\n      case e: Exception =>\n        recordDeltaEvent(\n          deltaLog = null,\n          opType = \"delta.assertions.deletionVectorSerializationError\",\n          data = debugInfo ++ Map(\n            \"serializationFormat\" -> serializationFormat,\n            \"cardinality\" -> bitmap.cardinality,\n            \"errorMsg\" -> e.getMessage,\n            \"errorStackTrace\" -> e.getStackTrace),\n          path = tablePath)\n        throw e\n    }\n  }\n\n  /**\n   * Deserializes a RoaringBitmapArray from `bytes`. If it fails, it records a delta event and\n   * re-throws the exception.\n   */\n  def deserialize(\n      bytes: Array[Byte],\n      tablePath: Option[Path] = None,\n      debugInfo: Map[String, Any] = Map.empty): RoaringBitmapArray = {\n    try {\n      RoaringBitmapArray.readFrom(bytes)\n    } catch {\n      case e: Exception =>\n        recordDeltaEvent(\n          deltaLog = null,\n          \"delta.assertions.deletionVectorDeserializationError\",\n          data = debugInfo ++ Map(\n            \"errorMsg\" -> e.getMessage,\n            \"errorStackTrace\" -> e.getStackTrace),\n          path = tablePath)\n        throw e\n    }\n  }\n}\n\n// To access utilities from places where mixing in a trait is inconvenient.\nobject DeletionVectorUtils extends DeletionVectorUtils\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DeltaCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.concurrent.TimeUnit.NANOSECONDS\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, DeltaErrors, DeltaLog, DeltaOptions, DeltaTableIdentifier, DeltaTableUtils, NumRecordsStats, OptimisticTransaction, ResolvedPathBasedNonDeltaTable}\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.catalog.{DeltaTableV2, IcebergTablePlaceHolder}\nimport org.apache.spark.sql.delta.files.TahoeBatchFileIndex\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{Analyzer, EliminateSubqueryAliases, NoSuchTableException, ResolvedTable, UnresolvedAttribute, UnresolvedRelation}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.catalyst.expressions.{Expression, SubqueryExpression}\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.connector.catalog.V1Table\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.DELTA_COLLECT_STATS\nimport org.apache.spark.sql.execution.SQLExecution\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.execution.datasources.v2.{DataSourceV2Relation, DataSourceV2RelationShim}\nimport org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}\n\n/**\n * Helper trait for all delta commands.\n */\ntrait DeltaCommand extends DeltaLogging with DeltaCommandInvariants {\n  /**\n   * Converts string predicates into [[Expression]]s relative to a transaction.\n   *\n   * @throws AnalysisException if a non-partition column is referenced.\n   */\n  protected def parsePredicates(\n      spark: SparkSession,\n      predicate: String): Seq[Expression] = {\n    try {\n      spark.sessionState.sqlParser.parseExpression(predicate) :: Nil\n    } catch {\n      case e: ParseException =>\n        throw DeltaErrors.failedRecognizePredicate(predicate, e)\n      case e: NullPointerException if predicate == null =>\n        throw DeltaErrors.failedRecognizePredicate(\"NULL\", e)\n    }\n  }\n\n  def verifyPartitionPredicates(\n      spark: SparkSession,\n      partitionColumns: Seq[String],\n      predicates: Seq[Expression]): Unit = {\n\n    predicates.foreach { pred =>\n      if (SubqueryExpression.hasSubquery(pred)) {\n        throw DeltaErrors.unsupportSubqueryInPartitionPredicates()\n      }\n\n      pred.references.foreach { col =>\n        val colName = col match {\n          case u: UnresolvedAttribute =>\n            // Note: `UnresolvedAttribute(Seq(\"a.b\"))` and `UnresolvedAttribute(Seq(\"a\", \"b\"))` will\n            // return the same name. We accidentally treated the latter as the same as the former.\n            // Because some users may already rely on it, we keep supporting both.\n            u.nameParts.mkString(\".\")\n          case _ => col.name\n        }\n        val nameEquality = spark.sessionState.conf.resolver\n        partitionColumns.find(f => nameEquality(f, colName)).getOrElse {\n          throw DeltaErrors.nonPartitionColumnReference(colName, partitionColumns)\n        }\n      }\n    }\n  }\n\n  /**\n   * Generates a map of file names to add file entries for operations where we will need to\n   * rewrite files such as delete, merge, update. We expect file names to be unique, because\n   * each file contains a UUID.\n   */\n  def generateCandidateFileMap(\n      basePath: Path,\n      candidateFiles: Seq[AddFile]): Map[String, AddFile] = {\n    val nameToAddFileMap = candidateFiles.map(add =>\n      DeltaFileOperations.absolutePath(basePath.toString, add.path).toString -> add).toMap\n    assert(nameToAddFileMap.size == candidateFiles.length,\n      s\"File name collisions found among:\\n${candidateFiles.map(_.path).mkString(\"\\n\")}\")\n    nameToAddFileMap\n  }\n\n  /**\n   * This method provides the RemoveFile actions that are necessary for files that are touched and\n   * need to be rewritten in methods like Delete, Update, and Merge.\n   *\n   * @param deltaLog The DeltaLog of the table that is being operated on\n   * @param nameToAddFileMap A map generated using `generateCandidateFileMap`.\n   * @param filesToRewrite Absolute paths of the files that were touched. We will search for these\n   *                       in `candidateFiles`. Obtained as the output of the `input_file_name`\n   *                       function.\n   * @param operationTimestamp The timestamp of the operation\n   */\n  protected def removeFilesFromPaths(\n      deltaLog: DeltaLog,\n      nameToAddFileMap: Map[String, AddFile],\n      filesToRewrite: Seq[String],\n      operationTimestamp: Long): Seq[RemoveFile] = {\n    filesToRewrite.map { absolutePath =>\n      val addFile = getTouchedFile(deltaLog.dataPath, absolutePath, nameToAddFileMap)\n      addFile.removeWithTimestamp(operationTimestamp)\n    }\n  }\n\n  /**\n   * Build a base relation of files that need to be rewritten as part of an update/delete/merge\n   * operation.\n   */\n  protected def buildBaseRelation(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      actionType: String,\n      rootPath: Path,\n      inputLeafFiles: Seq[String],\n      nameToAddFileMap: Map[String, AddFile]): HadoopFsRelation = {\n    val deltaLog = txn.deltaLog\n    val scannedFiles = inputLeafFiles.map(f => getTouchedFile(rootPath, f, nameToAddFileMap))\n    val fileIndex = new TahoeBatchFileIndex(\n      spark, actionType, scannedFiles, deltaLog, rootPath, txn.snapshot)\n    HadoopFsRelation(\n      fileIndex,\n      partitionSchema = txn.metadata.partitionSchema,\n      dataSchema = txn.metadata.schema,\n      bucketSpec = None,\n      deltaLog.fileFormat(txn.protocol, txn.metadata),\n      txn.metadata.format.options)(spark)\n  }\n\n  /**\n   * Find the AddFile record corresponding to the file that was read as part of a\n   * delete/update/merge operation.\n   *\n   * @param basePath The path of the table. Must not be escaped.\n   * @param escapedFilePath The path to a file that can be either absolute or relative. All special\n   *                        chars in this path must be already escaped by URI standards.\n   * @param nameToAddFileMap Map generated through `generateCandidateFileMap()`.\n   */\n  def getTouchedFile(\n      basePath: Path,\n      escapedFilePath: String,\n      nameToAddFileMap: Map[String, AddFile]): AddFile = {\n    val absolutePath =\n      DeltaFileOperations.absolutePath(basePath.toString, escapedFilePath).toString\n    nameToAddFileMap.getOrElse(absolutePath, {\n      throw DeltaErrors.notFoundFileToBeRewritten(absolutePath, nameToAddFileMap.keys)\n    })\n  }\n\n  /**\n   * Use the analyzer to resolve the identifier provided\n   * @param analyzer The session state analyzer to call\n   * @param identifier Table Identifier to determine whether is path based or not\n   * @return\n   */\n  protected def resolveIdentifier(analyzer: Analyzer, identifier: TableIdentifier): LogicalPlan = {\n    EliminateSubqueryAliases(analyzer.execute(UnresolvedRelation(identifier)))\n  }\n\n  /**\n   * Use the analyzer to see whether the provided TableIdentifier is for a path based table or not\n   * @param analyzer The session state analyzer to call\n   * @param tableIdent Table Identifier to determine whether is path based or not\n   * @return Boolean where true means that the table is a table in a metastore and false means the\n   *         table is a path based table\n   */\n  def isCatalogTable(analyzer: Analyzer, tableIdent: TableIdentifier): Boolean = {\n    try {\n      resolveIdentifier(analyzer, tableIdent) match {\n        // is path\n        case LogicalRelationWithTable(HadoopFsRelation(_, _, _, _, _, _), None) => false\n        // is table\n        case LogicalRelationWithTable(HadoopFsRelation(_, _, _, _, _, _), Some(_)) => true\n        // is iceberg table\n        case DataSourceV2RelationShim(_: IcebergTablePlaceHolder, _, _, _, _) => false\n        // could not resolve table/db\n        case _: UnresolvedRelation =>\n          throw new NoSuchTableException(tableIdent.database.getOrElse(\"\"), tableIdent.table)\n        // other e.g. view\n        case _ => true\n      }\n    } catch {\n      // Checking for table exists/database exists may throw an error in some cases in which case,\n      // see if the table is a path-based table, otherwise throw the original error\n      case _: AnalysisException if isPathIdentifier(tableIdent) => false\n    }\n  }\n\n  /**\n   * Checks if the given identifier can be for a delta table's path\n   * @param tableIdent Table Identifier for which to check\n   */\n  protected def isPathIdentifier(tableIdent: TableIdentifier): Boolean = {\n    val provider = tableIdent.database.getOrElse(\"\")\n    // If db doesnt exist or db is called delta/tahoe then check if path exists\n    DeltaSourceUtils.isDeltaDataSourceName(provider) && new Path(tableIdent.table).isAbsolute\n  }\n\n  /**\n   * Utility method to return the [[DeltaLog]] of an existing Delta table referred\n   * by either the given [[path]] or [[tableIdentifier]].\n   *\n   * @param spark [[SparkSession]] reference to use.\n   * @param path Table location. Expects a non-empty [[tableIdentifier]] or [[path]].\n   * @param tableIdentifier Table identifier. Expects a non-empty [[tableIdentifier]] or [[path]].\n   * @param operationName Operation that is getting the DeltaLog, used in error messages.\n   * @param hadoopConf Hadoop file system options used to build DeltaLog.\n   * @return DeltaLog of the table\n   * @throws AnalysisException If either no Delta table exists at the given path/identifier or\n   *                           there is neither [[path]] nor [[tableIdentifier]] is provided.\n   */\n  protected def getDeltaLog(\n      spark: SparkSession,\n      path: Option[String],\n      tableIdentifier: Option[TableIdentifier],\n      operationName: String,\n      hadoopConf: Map[String, String] = Map.empty): DeltaLog = {\n    val (deltaLog, catalogTable) =\n      if (path.nonEmpty) {\n        (DeltaLog.forTable(spark, new Path(path.get), hadoopConf), None)\n      } else if (tableIdentifier.nonEmpty) {\n        val sessionCatalog = spark.sessionState.catalog\n        lazy val metadata = sessionCatalog.getTableMetadata(tableIdentifier.get)\n\n        DeltaTableIdentifier(spark, tableIdentifier.get) match {\n          case Some(id) if id.path.nonEmpty =>\n            (DeltaLog.forTable(spark, new Path(id.path.get), hadoopConf), None)\n          case Some(id) if id.table.nonEmpty =>\n            (DeltaLog.forTable(spark, metadata, hadoopConf), Some(metadata))\n          case _ =>\n            if (metadata.tableType == CatalogTableType.VIEW) {\n              throw DeltaErrors.viewNotSupported(operationName)\n            }\n            throw DeltaErrors.notADeltaTableException(operationName)\n        }\n      } else {\n        throw DeltaErrors.missingTableIdentifierException(operationName)\n      }\n\n    val startTime = Some(System.currentTimeMillis)\n    if (deltaLog\n        .update(checkIfUpdatedSinceTs = startTime, catalogTableOpt = catalogTable)\n        .version < 0) {\n      throw DeltaErrors.notADeltaTableException(\n        operationName,\n        DeltaTableIdentifier(path, tableIdentifier))\n    }\n    deltaLog\n  }\n\n  /**\n   * Send the driver-side metrics.\n   *\n   * This is needed to make the SQL metrics visible in the Spark UI.\n   * All metrics are default initialized with 0 so that's what we're\n   * reporting in case we skip an already executed action.\n   */\n  protected def sendDriverMetrics(spark: SparkSession, metrics: Map[String, SQLMetric]): Unit = {\n    val executionId = spark.sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)\n    SQLMetrics.postDriverMetricUpdates(spark.sparkContext, executionId, metrics.values.toSeq)\n  }\n\n  /**\n   * Extracts the [[DeltaTableV2]] from a LogicalPlan iff the LogicalPlan is a [[ResolvedTable]]\n   * with either a [[DeltaTableV2]] or a [[V1Table]] that is referencing a Delta table. In all\n   * other cases this method will throw a \"Table not found\" exception.\n   */\n  def getDeltaTable(target: LogicalPlan, cmd: String): DeltaTableV2 = {\n    // TODO: Remove this wrapper and let former callers invoke DeltaTableV2.extractFrom directly.\n    DeltaTableV2.extractFrom(target, cmd)\n  }\n\n  /**\n   * Extracts [[CatalogTable]] metadata from a LogicalPlan if the plan is a [[ResolvedTable]]. The\n   * table can be a non delta table.\n   */\n  def getTableCatalogTable(target: LogicalPlan, cmd: String): Option[CatalogTable] = {\n    target match {\n      case ResolvedTable(_, _, d: DeltaTableV2, _) => d.catalogTable\n      case ResolvedTable(_, _, t: V1Table, _) => Some(t.catalogTable)\n      case _ => None\n    }\n  }\n\n  /**\n   * Helper method to extract the table id or path from a LogicalPlan representing\n   * a Delta table. This uses [[DeltaCommand.getDeltaTable]] to convert the LogicalPlan\n   * to a [[DeltaTableV2]] and then extracts either the path or identifier from it. If\n   * the [[DeltaTableV2]] has a [[CatalogTable]], the table identifier will be returned.\n   * Otherwise, the table's path will be returned. Throws an exception if the LogicalPlan\n   * does not represent a Delta table.\n   */\n  def getDeltaTablePathOrIdentifier(\n      target: LogicalPlan,\n      cmd: String): (Option[TableIdentifier], Option[String]) = {\n    val table = getDeltaTable(target, cmd)\n    table.catalogTable match {\n      case Some(catalogTable)\n        => (Some(catalogTable.identifier), None)\n      case _ => (None, Some(table.path.toString))\n    }\n  }\n\n  /**\n   * Helper method to extract the table id or path from a LogicalPlan representing a resolved table\n   * or path. This calls getDeltaTablePathOrIdentifier if the resolved table is a delta table. For\n   * non delta table with identifier, we extract its identifier. For non delta table with path, it\n   * expects the path to be wrapped in an ResolvedPathBasedNonDeltaTable and extracts it from there.\n   */\n  def getTablePathOrIdentifier(\n      target: LogicalPlan,\n      cmd: String): (Option[TableIdentifier], Option[String]) = {\n    target match {\n      case ResolvedTable(_, _, t: DeltaTableV2, _) => getDeltaTablePathOrIdentifier(target, cmd)\n      case ResolvedTable(_, _, t: V1Table, _) if DeltaTableUtils.isDeltaTable(t.catalogTable) =>\n        getDeltaTablePathOrIdentifier(target, cmd)\n      case ResolvedTable(_, _, t: V1Table, _) => (Some(t.catalogTable.identifier), None)\n      case p: ResolvedPathBasedNonDeltaTable => (None, Some(p.path))\n      case _ => (None, None)\n    }\n  }\n\n  /**\n   * Returns true if there is information in the spark session that indicates that this write\n   * has already been successfully written.\n   */\n  protected def hasBeenExecuted(txn: OptimisticTransaction, sparkSession: SparkSession,\n    options: Option[DeltaOptions] = None): Boolean = {\n    val (txnVersionOpt, txnAppIdOpt, isFromSessionConf) = getTxnVersionAndAppId(\n      sparkSession, options)\n    // only enter if both txnVersion and txnAppId are set\n    for (version <- txnVersionOpt; appId <- txnAppIdOpt) {\n      val currentVersion = txn.txnVersion(appId)\n      if (currentVersion >= version) {\n        logInfo(log\"Already completed batch ${MDC(DeltaLogKeys.VERSION, version)} in application \" +\n          log\"${MDC(DeltaLogKeys.APP_ID, appId)}. This will be skipped.\")\n        if (isFromSessionConf && sparkSession.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_IDEMPOTENT_DML_AUTO_RESET_ENABLED)) {\n          // if we got txnAppId and txnVersion from the session config, we reset the\n          // version here, after skipping the current transaction, as a safety measure to\n          // prevent data loss if the user forgets to manually reset txnVersion\n          sparkSession.sessionState.conf.unsetConf(DeltaSQLConf.DELTA_IDEMPOTENT_DML_TXN_VERSION)\n        }\n        return true\n      }\n    }\n    false\n  }\n\n  /**\n   * Returns SetTransaction if a valid app ID and version are present. Otherwise returns\n   * an empty list.\n   */\n  protected def createSetTransaction(\n    sparkSession: SparkSession,\n    deltaLog: DeltaLog,\n    options: Option[DeltaOptions] = None): Option[SetTransaction] = {\n    val (txnVersionOpt, txnAppIdOpt, isFromSessionConf) = getTxnVersionAndAppId(\n      sparkSession, options)\n    // only enter if both txnVersion and txnAppId are set\n    for (version <- txnVersionOpt; appId <- txnAppIdOpt) {\n      if (isFromSessionConf && sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_IDEMPOTENT_DML_AUTO_RESET_ENABLED)) {\n        // if we got txnAppID and txnVersion from the session config, we reset the\n        // version here as a safety measure to prevent data loss if the user forgets\n        // to manually reset txnVersion\n        sparkSession.sessionState.conf.unsetConf(DeltaSQLConf.DELTA_IDEMPOTENT_DML_TXN_VERSION)\n      }\n      return Some(SetTransaction(appId, version, Some(deltaLog.clock.getTimeMillis())))\n    }\n    None\n  }\n\n  /**\n   * Helper method to retrieve the current txn version and app ID. These are either\n   * retrieved from user-provided write options or from session configurations.\n   */\n  private def getTxnVersionAndAppId(\n    sparkSession: SparkSession,\n    options: Option[DeltaOptions]): (Option[Long], Option[String], Boolean) = {\n    var txnVersion: Option[Long] = None\n    var txnAppId: Option[String] = None\n    for (o <- options) {\n      txnVersion = o.txnVersion\n      txnAppId = o.txnAppId\n    }\n\n    var numOptions = txnVersion.size + txnAppId.size\n    // numOptions can only be 0 or 2, as enforced by\n    // DeltaWriteOptionsImpl.validateIdempotentWriteOptions so this\n    // assert should never be triggered\n    assert(numOptions == 0 || numOptions == 2, s\"Only one of txnVersion and txnAppId \" +\n      s\"has been set via dataframe writer options: txnVersion = $txnVersion txnAppId = $txnAppId\")\n    var fromSessionConf = false\n    if (numOptions == 0) {\n      txnVersion = sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_IDEMPOTENT_DML_TXN_VERSION)\n      // don't need to check for valid conversion to Long here as that\n      // is already enforced at set time\n      txnAppId = sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_IDEMPOTENT_DML_TXN_APP_ID)\n      // check that both session configs are set\n      numOptions = txnVersion.size + txnAppId.size\n      if (numOptions != 0 && numOptions != 2) {\n        throw DeltaErrors.invalidIdempotentWritesOptionsException(\n          \"Both spark.databricks.delta.write.txnAppId and \" +\n            \"spark.databricks.delta.write.txnVersion must be specified for \" +\n            \"idempotent Delta writes\")\n      }\n      fromSessionConf = true\n    }\n    (txnVersion, txnAppId, fromSessionConf)\n  }\n\n  protected def logNumRecordsMismatch(\n      deltaLog: DeltaLog,\n      actions: Seq[Action],\n      stats: NumRecordsStats,\n      op: Operation): Unit = {\n\n    var numRemove = 0\n    var numAdd = 0\n    actions.foreach {\n      case _: AddFile =>\n        numAdd += 1\n      case _: RemoveFile =>\n        numRemove += 1\n      case _ =>\n    }\n\n    val info = NumRecordsCheckInfo(\n      operation = op.name,\n      numAdd = numAdd,\n      numRemove = numRemove,\n      numRecordsRemoved = stats.numLogicalRecordsRemovedPartial,\n      numRecordsAdded = stats.numLogicalRecordsAddedPartial,\n      numDeletionVectorRecordsRemoved = stats.numDeletionVectorRecordsRemoved,\n      numDeletionVectorRecords = stats.numDeletionVectorRecordsAdded,\n      operationParameters = op.jsonEncodedValues,\n      statsCollectionEnabled = SparkSession.getActiveSession.get.conf.get(DELTA_COLLECT_STATS)\n    )\n    recordDeltaEvent(deltaLog, opType = \"delta.assertions.numRecordsChanged\", data = info)\n    logWarning(log\"Number of records validation failed. Number of added records\" +\n      log\" (${MDC(DeltaLogKeys.NUM_RECORDS, stats.numLogicalRecordsAddedPartial)})\" +\n      log\" does not match removed records\" +\n      log\" (${MDC(DeltaLogKeys.NUM_RECORDS2, stats.numLogicalRecordsRemovedPartial)})\")\n  }\n}\n\n// Recorded when number of records check for unchanged data fails.\ncase class NumRecordsCheckInfo(\n    operation: String,\n    numAdd: Int,\n    numRemove: Int,\n    numRecordsAdded: Long,\n    numRecordsRemoved: Long,\n    numDeletionVectorRecordsRemoved: Long = 0, // number of DV records removed by the RemoveFiles\n    numDeletionVectorRecords: Long = 0, // number of DV records present in all AddFiles\n    operationParameters: Map[String, String],\n    statsCollectionEnabled: Boolean\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DeltaCommandInvariants.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.util.UUID\n\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog}\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.internal.{LogEntry, Logging, MDC}\nimport org.apache.spark.sql.catalyst.SQLConfHelper\nimport org.apache.spark.sql.execution.metric.SQLMetric\n\ntrait DeltaCommandInvariants extends DeltaLogging with SQLConfHelper with Logging {\n  /**\n   * Evaluate that `invariant` \"holds\" (evaluates to `true`) or log the violation.\n   * @param invariant The invariant to evaluate.\n   * @param label  It's suggested to make `label` the same text as `invariant` so it's easy to\n   *               see in the logs what was evaluated.\n   * @param op Operation name.\n   * @param deltaLog Delta log of the table this invariant is evaluated on.\n   * @param parameters Parameters that were used to evaluate the invariant.\n   * @param additionalInfo Additional info to be included in usage logs.\n   */\n  protected def checkCommandInvariant(\n      invariant: () => Boolean,\n      label: String,\n      op: Operation,\n      deltaLog: DeltaLog,\n      parameters: => Map[String, Long],\n      additionalInfo: => Map[String, String] = Map.empty): Unit = {\n    val id = UUID.randomUUID()\n    val invariantResult = try {\n      invariant()\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Exception thrown while evaluating command invariant.\" +\n          log\" Reference: ${MDC(DeltaLogKeys.ERROR_ID, id.toString)}.\", e)\n        false\n    }\n    if (!invariantResult) {\n      val shouldFail = conf.getConf(DeltaSQLConf.COMMAND_INVARIANT_CHECKS_THROW)\n      try {\n        val opType =\n          \"delta.assertions.unreliable.commandInvariantViolated\"\n        val info = CommandInvariantCheckInfo(\n          exceptionThrown = shouldFail,\n          id = id,\n          invariantExpression = label,\n          invariantParameters = parameters,\n          operation = op.name,\n          operationParameters = op.jsonEncodedValues,\n          additionalInfo = additionalInfo\n        )\n        recordDeltaEvent(\n          deltaLog,\n          opType = opType,\n          data = info)\n\n        // Log this to Spark logs as well, so someone looking through there knows to look for the\n        // details in usage logs.\n        // FIXME: Needs inline type annotations because otherwise compiler gets confused on\n        //        implicit conversions.\n        val logEntry: LogEntry = log\"Delta Command Invariant violated.\" +\n          log\" Reference: ${MDC(DeltaLogKeys.ERROR_ID, id.toString)}.\" +\n          log\" Info: ${MDC(DeltaLogKeys.INVARIANT_CHECK_INFO, info)}.\"\n        logWarning(logEntry)\n      } catch {\n        case NonFatal(e) =>\n          logWarning(log\"Unexpected error while logging command invariant violation.\" +\n            log\" Reference: ${MDC(DeltaLogKeys.ERROR_ID, id.toString)}.\", e)\n      }\n\n      if (shouldFail) {\n        throw DeltaErrors.commandInvariantViolationException(operation = op.name, id = id)\n      }\n    }\n  }\n}\n\n// Recorded when a command invariant is violated.\ncase class CommandInvariantCheckInfo(\n    exceptionThrown: Boolean,\n    id: UUID,\n    invariantExpression: String,\n    invariantParameters: Map[String, Long],\n    operation: String,\n    operationParameters: Map[String, String],\n    additionalInfo: Map[String, String])\n\nabstract class CommandInvariantMetricValue extends Logging {\n\n  protected def value: Try[Long]\n\n  def getOrThrow: Long = value.get\n\n  def getOrDummy: Long = value.getOrElse(-1L)\n}\n\ncase class CommandInvariantMetricValueFromSingle(\n    metric: SQLMetric) extends CommandInvariantMetricValue  {\n  override protected val value: Try[Long] = Try {\n    metric.value\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DeltaGenerateCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, UnresolvedDeltaPathOrIdentifier}\nimport org.apache.spark.sql.delta.hooks.GenerateSymlinkManifest\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.execution.command.RunnableCommand\n\ncase class DeltaGenerateCommand(override val child: LogicalPlan, modeName: String)\n  extends RunnableCommand\n  with UnaryNode\n  with DeltaCommand {\n\n  import DeltaGenerateCommand._\n\n  override def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = copy(child = newChild)\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    if (!modeNameToGenerationFunc.contains(modeName)) {\n      throw DeltaErrors.unsupportedGenerateModeException(modeName)\n    }\n    val generationFunc = modeNameToGenerationFunc(modeName)\n    val table = getDeltaTable(child, COMMAND_NAME)\n    generationFunc(sparkSession, table.deltaLog, table.catalogTable)\n    Seq.empty\n  }\n}\n\nobject DeltaGenerateCommand {\n  val modeNameToGenerationFunc\n      : CaseInsensitiveMap[(SparkSession, DeltaLog, Option[CatalogTable]) => Unit] =\n    CaseInsensitiveMap(\n      Map[String, (SparkSession, DeltaLog, Option[CatalogTable]) => Unit](\n        \"symlink_format_manifest\" -> GenerateSymlinkManifest.generateFullManifest\n      )\n    )\n\n  val COMMAND_NAME = \"GENERATE\"\n\n  def apply(\n    path: Option[String],\n    tableIdentifier: Option[TableIdentifier],\n    modeName: String,\n    options: Map[String, String]\n  ): DeltaGenerateCommand = {\n    // Exactly one of path or tableIdentifier should be specified\n    val plan = UnresolvedDeltaPathOrIdentifier(\n      path.filter(_ => tableIdentifier.isEmpty),\n      tableIdentifier,\n      options,\n      COMMAND_NAME)\n    DeltaGenerateCommand(plan, modeName)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DeltaReorgTableCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaErrors, Snapshot}\nimport org.apache.spark.sql.delta.actions.AddFile\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.plans.logical.{IgnoreCachedData, LeafCommand, LogicalPlan, UnaryCommand}\n\nobject DeltaReorgTableMode extends Enumeration {\n  val PURGE, UNIFORM_ICEBERG, REWRITE_TYPE_WIDENING = Value\n}\n\ncase class DeltaReorgTableSpec(\n    reorgTableMode: DeltaReorgTableMode.Value,\n    icebergCompatVersionOpt: Option[Int]\n)\n\ncase class DeltaReorgTable(\n    target: LogicalPlan,\n    reorgTableSpec: DeltaReorgTableSpec = DeltaReorgTableSpec(DeltaReorgTableMode.PURGE, None))(\n    val predicates: Seq[String]) extends UnaryCommand {\n\n  def child: LogicalPlan = target\n\n  protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan =\n    copy(target = newChild)(predicates)\n\n  override val otherCopyArgs: Seq[AnyRef] = predicates :: Nil\n}\n\n/**\n * The REORG TABLE command.\n */\ncase class DeltaReorgTableCommand(\n    target: LogicalPlan,\n    reorgTableSpec: DeltaReorgTableSpec = DeltaReorgTableSpec(DeltaReorgTableMode.PURGE, None))(\n    val predicates: Seq[String])\n  extends OptimizeTableCommandBase\n  with ReorgTableForUpgradeUniformHelper\n  with LeafCommand\n  with IgnoreCachedData {\n\n  override val otherCopyArgs: Seq[AnyRef] = predicates :: Nil\n\n  override def optimizeByReorg(sparkSession: SparkSession): Seq[Row] = {\n    val command = OptimizeTableCommand(\n      target,\n      predicates,\n      optimizeContext = DeltaOptimizeContext(\n        reorg = Some(reorgOperation),\n        minFileSize = Some(0L),\n        maxDeletedRowsRatio = Some(0d))\n    )(zOrderBy = Nil)\n    command.run(sparkSession)\n  }\n\n  override def run(sparkSession: SparkSession): Seq[Row] = reorgTableSpec match {\n    case DeltaReorgTableSpec(\n        DeltaReorgTableMode.PURGE | DeltaReorgTableMode.REWRITE_TYPE_WIDENING, None) =>\n      optimizeByReorg(sparkSession)\n    case DeltaReorgTableSpec(DeltaReorgTableMode.UNIFORM_ICEBERG, Some(icebergCompatVersion)) =>\n      val table = getDeltaTable(target, \"REORG\")\n      if (table.update().isCatalogOwned) {\n        throw DeltaErrors.operationBlockedOnCatalogManagedTable(\"REORG\")\n      }\n      upgradeUniformIcebergCompatVersion(table, sparkSession, icebergCompatVersion)\n  }\n\n  protected def reorgOperation: DeltaReorgOperation = reorgTableSpec match {\n    case DeltaReorgTableSpec(DeltaReorgTableMode.PURGE, None) =>\n      new DeltaPurgeOperation()\n    case DeltaReorgTableSpec(DeltaReorgTableMode.UNIFORM_ICEBERG, Some(icebergCompatVersion)) =>\n      new DeltaUpgradeUniformOperation(icebergCompatVersion)\n    case DeltaReorgTableSpec(DeltaReorgTableMode.REWRITE_TYPE_WIDENING, None) =>\n      new DeltaRewriteTypeWideningOperation()\n  }\n}\n\n/**\n * Defines a Reorg operation to be applied during optimize.\n */\nsealed trait DeltaReorgOperation {\n  /**\n   * Collects files that need to be processed by the reorg operation from the list of candidate\n   * files.\n   */\n  def filterFilesToReorg(spark: SparkSession, snapshot: Snapshot, files: Seq[AddFile]): Seq[AddFile]\n}\n\n/**\n * Reorg operation to purge files with soft deleted rows.\n * This operation will also try finding and removing the dropped columns from parquet files,\n * if ever exists such column that does not present in the current table schema.\n */\nclass DeltaPurgeOperation extends DeltaReorgOperation with ReorgTableHelper {\n  override def filterFilesToReorg(spark: SparkSession, snapshot: Snapshot, files: Seq[AddFile])\n    : Seq[AddFile] = {\n    val physicalSchema = DeltaColumnMapping.renameColumns(snapshot.schema)\n    val protocol = snapshot.protocol\n    val metadata = snapshot.metadata\n    val filesWithDroppedColumns: Seq[AddFile] =\n      filterParquetFilesOnExecutors(spark, files, snapshot, ignoreCorruptFiles = false) {\n        schema => fileHasExtraColumns(schema, physicalSchema, protocol, metadata)\n      }\n    val filesWithDV: Seq[AddFile] = files.filter { file =>\n        (file.deletionVector != null && file.numPhysicalRecords.isEmpty) ||\n        file.numDeletedRecords > 0L\n    }\n    (filesWithDroppedColumns ++ filesWithDV).distinct\n  }\n}\n\n/**\n * Reorg operation to upgrade the iceberg compatibility version of a table.\n */\nclass DeltaUpgradeUniformOperation(icebergCompatVersion: Int) extends DeltaReorgOperation {\n  override def filterFilesToReorg(spark: SparkSession, snapshot: Snapshot, files: Seq[AddFile])\n    : Seq[AddFile] = {\n    def shouldRewriteToBeIcebergCompatible(file: AddFile): Boolean = {\n      if (file.tags == null) return true\n      val fileIcebergCompatVersion =\n        file.tags.getOrElse(AddFile.Tags.ICEBERG_COMPAT_VERSION.name, \"0\")\n      fileIcebergCompatVersion != icebergCompatVersion.toString\n    }\n    files.filter(shouldRewriteToBeIcebergCompatible)\n  }\n}\n\n/**\n * Internal reorg operation to rewrite files to conform to the current table schema when dropping\n * the type widening table feature.\n */\nclass DeltaRewriteTypeWideningOperation extends DeltaReorgOperation with ReorgTableHelper {\n  override def filterFilesToReorg(spark: SparkSession, snapshot: Snapshot, files: Seq[AddFile])\n    : Seq[AddFile] = {\n    val physicalSchema = DeltaColumnMapping.renameColumns(snapshot.schema)\n    filterParquetFilesOnExecutors(spark, files, snapshot, ignoreCorruptFiles = false) {\n      schema => fileHasDifferentTypes(schema, physicalSchema)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DescribeDeltaDetailsCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.sql.Timestamp\n\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, Snapshot, UnresolvedPathOrIdentifier}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.DeltaCommitFileProvider\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.{CatalystTypeConverters, ScalaReflection, TableIdentifier}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.execution.command.RunnableCommand\nimport org.apache.spark.sql.types.StructType\n\n/** The result returned by the `describe detail` command. */\ncase class TableDetail(\n    format: String,\n    id: String,\n    name: String,\n    description: String,\n    location: String,\n    createdAt: Timestamp,\n    lastModified: Timestamp,\n    partitionColumns: Seq[String],\n    clusteringColumns: Seq[String],\n    numFiles: java.lang.Long,\n    sizeInBytes: java.lang.Long,\n    properties: Map[String, String],\n    minReaderVersion: java.lang.Integer,\n    minWriterVersion: java.lang.Integer,\n    tableFeatures: Seq[String]\n    )\n\nobject TableDetail {\n  val schema = ScalaReflection.schemaFor[TableDetail].dataType.asInstanceOf[StructType]\n\n  private lazy val converter: TableDetail => Row = {\n    val toInternalRow = CatalystTypeConverters.createToCatalystConverter(schema)\n    val toExternalRow = CatalystTypeConverters.createToScalaConverter(schema)\n    toInternalRow.andThen(toExternalRow).asInstanceOf[TableDetail => Row]\n  }\n\n  def toRow(table: TableDetail): Row = converter(table)\n}\n\n/**\n * A command for describing the details of a table such as the format, name, and size.\n */\ncase class DescribeDeltaDetailCommand(\n    override val child: LogicalPlan,\n    hadoopConf: Map[String, String])\n  extends RunnableCommand\n    with UnaryNode\n    with DeltaLogging\n    with DeltaCommand\n{\n  override val output: Seq[Attribute] = toAttributes(TableDetail.schema)\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): DescribeDeltaDetailCommand =\n    copy(child = newChild)\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val tableMetadata = getTableCatalogTable(child, DescribeDeltaDetailCommand.CMD_NAME)\n    val (_, path) = getTablePathOrIdentifier(child, DescribeDeltaDetailCommand.CMD_NAME)\n    val deltaLog = (tableMetadata, path) match {\n      case (Some(metadata), _) => DeltaLog.forTable(sparkSession, metadata, hadoopConf)\n      case (_, Some(path)) => DeltaLog.forTable(sparkSession, new Path(path), hadoopConf)\n      case _ =>\n        throw DeltaErrors.missingTableIdentifierException(DescribeDeltaDetailCommand.CMD_NAME)\n    }\n    recordDeltaOperation(deltaLog, \"delta.ddl.describeDetails\") {\n      val snapshot = deltaLog.update(catalogTableOpt = tableMetadata)\n      if (snapshot.version == -1) {\n        if (path.nonEmpty) {\n          val fs = new Path(path.get).getFileSystem(deltaLog.newDeltaHadoopConf())\n          // Throw FileNotFoundException when the path doesn't exist since there may be a typo\n          if (!fs.exists(new Path(path.get))) {\n            throw DeltaErrors.fileNotFoundException(path.get)\n          }\n          describeNonDeltaPath(path.get)\n        } else {\n          describeNonDeltaTable(tableMetadata.get)\n        }\n      } else {\n        describeDeltaTable(sparkSession, deltaLog, snapshot, tableMetadata)\n      }\n    }\n  }\n\n  private def toRows(detail: TableDetail): Seq[Row] = TableDetail.toRow(detail) :: Nil\n\n  private def describeNonDeltaTable(table: CatalogTable): Seq[Row] = {\n    toRows(\n      TableDetail(\n        format = table.provider.orNull,\n        id = null,\n        name = table.qualifiedName,\n        description = table.comment.getOrElse(\"\"),\n        location = table.storage.locationUri.map(new Path(_).toString).orNull,\n        createdAt = new Timestamp(table.createTime),\n        lastModified = null,\n        partitionColumns = table.partitionColumnNames,\n        clusteringColumns = null,\n        numFiles = null,\n        sizeInBytes = null,\n        properties = table.properties,\n        minReaderVersion = null,\n        minWriterVersion = null,\n        tableFeatures = null\n      ))\n  }\n\n  private def describeNonDeltaPath(path: String): Seq[Row] = {\n    toRows(\n      TableDetail(\n        format = null,\n        id = null,\n        name = null,\n        description = null,\n        location = path,\n        createdAt = null,\n        lastModified = null,\n        partitionColumns = null,\n        clusteringColumns = null,\n        numFiles = null,\n        sizeInBytes = null,\n        properties = Map.empty,\n        minReaderVersion = null,\n        minWriterVersion = null,\n        tableFeatures = null))\n  }\n\n  private def describeDeltaTable(\n      sparkSession: SparkSession,\n      deltaLog: DeltaLog,\n      snapshot: Snapshot,\n      tableMetadata: Option[CatalogTable]): Seq[Row] = {\n    val currentVersionPath = DeltaCommitFileProvider(snapshot).deltaFile(snapshot.version)\n    val fs = currentVersionPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n    val tableName = tableMetadata.map(_.qualifiedName).getOrElse(snapshot.metadata.name)\n    val featureNames = (\n      snapshot.protocol.implicitlySupportedFeatures.map(_.name) ++\n        snapshot.protocol.readerAndWriterFeatureNames).toSeq.sorted\n    val clusteringColumns = if (ClusteredTableUtils.isSupported(snapshot.protocol)) {\n      ClusteringColumnInfo.extractLogicalNames(snapshot)\n    } else {\n      Nil\n    }\n    toRows(\n      TableDetail(\n        format = \"delta\",\n        id = snapshot.metadata.id,\n        name = tableName,\n        description = snapshot.metadata.description,\n        location = deltaLog.dataPath.toString,\n        createdAt = snapshot.metadata.createdTime.map(new Timestamp(_)).orNull,\n        lastModified = new Timestamp(fs.getFileStatus(currentVersionPath).getModificationTime),\n        partitionColumns = snapshot.metadata.partitionColumns,\n        clusteringColumns = clusteringColumns,\n        numFiles = snapshot.numOfFiles,\n        sizeInBytes = snapshot.sizeInBytes,\n        properties = snapshot.metadata.configuration,\n        minReaderVersion = snapshot.protocol.minReaderVersion,\n        minWriterVersion = snapshot.protocol.minWriterVersion,\n        tableFeatures = featureNames\n      ))\n  }\n}\n\nobject DescribeDeltaDetailCommand {\n  val CMD_NAME = \"DESCRIBE DETAIL\"\n  def apply(\n    path: Option[String],\n    tableIdentifier: Option[TableIdentifier],\n    hadoopConf: Map[String, String]\n  ): DescribeDeltaDetailCommand = {\n    val plan = UnresolvedPathOrIdentifier(\n      path,\n      tableIdentifier,\n      hadoopConf,\n      CMD_NAME\n    )\n    DescribeDeltaDetailCommand(plan, hadoopConf)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/DescribeDeltaHistoryCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaHistory, DeltaTableIdentifier, UnresolvedDeltaPathOrIdentifier, UnresolvedPathBasedDeltaTable}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{MultiInstanceRelation, UnresolvedTable}\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeSet}\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project, UnaryNode}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\n\nobject DescribeDeltaHistory {\n  /**\n   * Alternate constructor that converts a provided path or table identifier into the\n   * correct child LogicalPlan node. If both path and tableIdentifier are specified (or\n   * if both are None), this method will throw an exception. If a table identifier is\n   * specified, the child LogicalPlan will be an [[UnresolvedTable]] whereas if a path\n   * is specified, it will be an [[UnresolvedPathBasedDeltaTable]].\n   *\n   * Note that the returned command will have an *unresolved* child table and hence, the command\n   * needs to be analyzed before it can be executed.\n   */\n  def apply(\n      path: Option[String],\n      tableIdentifier: Option[TableIdentifier],\n      limit: Option[Int]): DescribeDeltaHistory = {\n    val plan = UnresolvedDeltaPathOrIdentifier(path, tableIdentifier, COMMAND_NAME)\n    DescribeDeltaHistory(plan, limit)\n  }\n\n  val COMMAND_NAME = \"DESCRIBE HISTORY\"\n}\n\n/**\n * A logical placeholder for describing a Delta table's history, so that the history can be\n * leveraged in subqueries. Replaced with `DescribeDeltaHistoryCommand` during planning.\n *\n * @param options: Hadoop file system options used for read and write.\n */\ncase class DescribeDeltaHistory(\n    override val child: LogicalPlan,\n    limit: Option[Int],\n    override val output: Seq[Attribute] = toAttributes(ExpressionEncoder[DeltaHistory]().schema))\n  extends UnaryNode\n    with MultiInstanceRelation\n    with DeltaCommand {\n\n  override def newInstance(): LogicalPlan = copy(output = output.map(_.newInstance()))\n\n  override def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = copy(child = newChild)\n\n  /**\n   * Define this operator as having no attributes provided by children in order to prevent column\n   * pruning from trying to insert projections above the source relation.\n   */\n  override lazy val references: AttributeSet = AttributeSet.empty\n  override def inputSet: AttributeSet = AttributeSet.empty\n  assert(!child.isInstanceOf[Project],\n    s\"The child operator of DescribeDeltaHistory must not contain any projection: $child\")\n\n  /** Converts this operator into an executable command. */\n  def toCommand: DescribeDeltaHistoryCommand = {\n    // Max array size\n    if (limit.exists(_ > Int.MaxValue - 8)) {\n      throw DeltaErrors.maxArraySizeExceeded()\n    }\n    val deltaTableV2: DeltaTableV2 = getDeltaTable(child, DescribeDeltaHistory.COMMAND_NAME)\n    DescribeDeltaHistoryCommand(table = deltaTableV2, limit = limit, output = output)\n  }\n}\n\n/**\n * A command for describing the history of a Delta table.\n */\ncase class DescribeDeltaHistoryCommand(\n    @transient table: DeltaTableV2,\n    limit: Option[Int],\n    override val output: Seq[Attribute] = toAttributes(ExpressionEncoder[DeltaHistory]().schema))\n  extends LeafRunnableCommand\n    with MultiInstanceRelation\n    with DeltaLogging {\n\n  override def newInstance(): LogicalPlan = copy(output = output.map(_.newInstance()))\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.describeHistory\") {\n      if (!deltaLog.tableExists) {\n        throw DeltaErrors.notADeltaTableException(\n          DescribeDeltaHistory.COMMAND_NAME,\n          DeltaTableIdentifier(path = Some(table.path.toString))\n        )\n      }\n      import org.apache.spark.sql.delta.implicits._\n      val commits = deltaLog.history.getHistory(limit, table.catalogTable)\n      sparkSession.implicits.localSeqToDatasetHolder(commits).toDF().collect().toSeq\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/MergeIntoCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.FileAction\nimport org.apache.spark.sql.delta.commands.merge.{ClassicMergeExecutor, InsertOnlyMergeExecutor, MergeIntoMaterializeSourceReason}\nimport org.apache.spark.sql.delta.files._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.types.{LongType, StructType}\n\n/**\n * Performs a merge of a source query/table into a Delta table.\n *\n * Issues an error message when the ON search_condition of the MERGE statement can match\n * a single row from the target table with multiple rows of the source table-reference.\n *\n * Algorithm:\n *\n * Phase 1: Find the input files in target that are touched by the rows that satisfy\n *    the condition and verify that no two source rows match with the same target row.\n *    This is implemented as an inner-join using the given condition. See [[ClassicMergeExecutor]]\n *    for more details.\n *\n * Phase 2: Read the touched files again and write new files with updated and/or inserted rows.\n *\n * Phase 3: Use the Delta protocol to atomically remove the touched files and add the new files.\n *\n * @param source                     Source data to merge from\n * @param target                     Target table to merge into\n * @param targetFileIndex            TahoeFileIndex of the target table\n * @param condition                  Condition for a source row to match with a target row\n * @param matchedClauses             All info related to matched clauses.\n * @param notMatchedClauses          All info related to not matched clauses.\n * @param notMatchedBySourceClauses  All info related to not matched by source clauses.\n * @param migratedSchema             The final schema of the target - may be changed by schema\n *                                   evolution.\n * @param trackHighWaterMarks        The column names for which we will track IDENTITY high water\n *                                   marks.\n */\ncase class MergeIntoCommand(\n    @transient source: LogicalPlan,\n    @transient target: LogicalPlan,\n    @transient catalogTable: Option[CatalogTable],\n    @transient targetFileIndex: TahoeFileIndex,\n    condition: Expression,\n    matchedClauses: Seq[DeltaMergeIntoMatchedClause],\n    notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause],\n    notMatchedBySourceClauses: Seq[DeltaMergeIntoNotMatchedBySourceClause],\n    migratedSchema: Option[StructType],\n    trackHighWaterMarks: Set[String] = Set.empty,\n    schemaEvolutionEnabled: Boolean = false)\n  extends MergeIntoCommandBase\n  with InsertOnlyMergeExecutor\n  with ClassicMergeExecutor {\n\n  override val output: Seq[Attribute] = Seq(\n    AttributeReference(\"num_affected_rows\", LongType)(),\n    AttributeReference(\"num_updated_rows\", LongType)(),\n    AttributeReference(\"num_deleted_rows\", LongType)(),\n    AttributeReference(\"num_inserted_rows\", LongType)())\n\n  protected def runMerge(spark: SparkSession): Seq[Row] = {\n    recordDeltaOperation(targetDeltaLog, \"delta.dml.merge\") {\n      val startTime = System.nanoTime()\n      targetDeltaLog.withNewTransaction(catalogTable) { deltaTxn =>\n        if (hasBeenExecuted(deltaTxn, spark)) {\n          sendDriverMetrics(spark, metrics)\n          return Seq.empty\n        }\n        if (target.schema.size != deltaTxn.metadata.schema.size) {\n          throw DeltaErrors.schemaChangedSinceAnalysis(\n            atAnalysis = target.schema, latestSchema = deltaTxn.metadata.schema)\n        }\n\n        // Check that type widening wasn't enabled/disabled between analysis and the start of the\n        // transaction.\n        TypeWidening.ensureFeatureConsistentlyEnabled(\n          protocol = targetFileIndex.protocol,\n          metadata = targetFileIndex.metadata,\n          otherProtocol = deltaTxn.protocol,\n          otherMetadata = deltaTxn.metadata\n        )\n\n        if (canMergeSchema) {\n          updateMetadata(\n            spark, deltaTxn, migratedSchema.getOrElse(target.schema),\n            deltaTxn.metadata.partitionColumns, deltaTxn.metadata.configuration,\n            isOverwriteMode = false, rearrangeOnly = false)\n        }\n\n        checkIdentityColumnHighWaterMarks(deltaTxn)\n        deltaTxn.setTrackHighWaterMarks(trackHighWaterMarks)\n\n        // Materialize the source if needed.\n        prepareMergeSource(\n          spark,\n          source,\n          condition,\n          matchedClauses,\n          notMatchedClauses,\n          isInsertOnly)\n\n        val mergeActions = {\n          if (isInsertOnly && spark.conf.get(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED)) {\n            // This is a single-job execution so there is no WriteChanges.\n            performedSecondSourceScan = false\n            writeOnlyInserts(\n              spark, deltaTxn, filterMatchedRows = true, numSourceRowsMetric = \"numSourceRows\")\n          } else {\n            val (filesToRewrite, deduplicateCDFDeletes) = findTouchedFiles(spark, deltaTxn)\n            if (filesToRewrite.nonEmpty) {\n              val shouldWriteDeletionVectors = shouldWritePersistentDeletionVectors(spark, deltaTxn)\n              if (shouldWriteDeletionVectors) {\n                val newWrittenFiles = withStatusCode(\"DELTA\", \"Writing modified data\") {\n                  writeAllChanges(\n                    spark,\n                    deltaTxn,\n                    filesToRewrite,\n                    deduplicateCDFDeletes,\n                    writeUnmodifiedRows = false)\n                }\n\n                val dvActions = withStatusCode(\n                   \"DELTA\",\n                   \"Writing Deletion Vectors for modified data\") {\n                  writeDVs(spark, deltaTxn, filesToRewrite)\n                }\n\n                newWrittenFiles ++ dvActions\n              } else {\n                val newWrittenFiles = withStatusCode(\"DELTA\", \"Writing modified data\") {\n                  writeAllChanges(\n                    spark,\n                    deltaTxn,\n                    filesToRewrite,\n                    deduplicateCDFDeletes,\n                    writeUnmodifiedRows = true)\n                }\n                newWrittenFiles ++ filesToRewrite.map(_.remove)\n              }\n            } else {\n              // Run an insert-only job instead of WriteChanges\n              writeOnlyInserts(\n                spark,\n                deltaTxn,\n                filterMatchedRows = false,\n                numSourceRowsMetric = \"numSourceRowsInSecondScan\")\n            }\n          }\n        }\n        commitAndRecordStats(\n          spark,\n          deltaTxn,\n          mergeActions,\n          startTime,\n          getMergeSource.materializeReason)\n      }\n      spark.sharedState.cacheManager.recacheByPlan(spark, target)\n    }\n    sendDriverMetrics(spark, metrics)\n    val num_affected_rows =\n      metrics(\"numTargetRowsUpdated\").value +\n        metrics(\"numTargetRowsDeleted\").value +\n        metrics(\"numTargetRowsInserted\").value\n    Seq(Row(\n      num_affected_rows,\n      metrics(\"numTargetRowsUpdated\").value,\n      metrics(\"numTargetRowsDeleted\").value,\n      metrics(\"numTargetRowsInserted\").value))\n  }\n\n  /**\n   * Finalizes the merge operation before committing it to the delta log and records merge metrics:\n   *   - Checks that the source table didn't change during the merge operation.\n   *   - Register SQL metrics to be updated during commit.\n   *   - Commit the operations.\n   *   - Collects final merge stats and record them with a Delta event.\n   */\n  private def commitAndRecordStats(\n      spark: SparkSession,\n      deltaTxn: OptimisticTransaction,\n      mergeActions: Seq[FileAction],\n      startTime: Long,\n      materializeSourceReason: MergeIntoMaterializeSourceReason.MergeIntoMaterializeSourceReason\n  ): Unit = {\n    checkNonDeterministicSource(spark)\n\n    // Metrics should be recorded before commit (where they are written to delta logs).\n    setOperationNumSourceRowsMetric()\n    metrics(\"executionTimeMs\").set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime))\n    deltaTxn.registerSQLMetrics(spark, metrics)\n\n    val finalActions = createSetTransaction(spark, targetDeltaLog).toSeq ++ mergeActions\n    val numRecordsStats = NumRecordsStats.fromActions(finalActions)\n    val operation = DeltaOperations.Merge(\n      predicate = Option(condition),\n      matchedPredicates = matchedClauses.map(DeltaOperations.MergePredicate(_)),\n      notMatchedPredicates = notMatchedClauses.map(DeltaOperations.MergePredicate(_)),\n      notMatchedBySourcePredicates =\n        notMatchedBySourceClauses.map(DeltaOperations.MergePredicate(_))\n    )\n    validateNumRecords(finalActions, numRecordsStats, operation, deltaTxn.deltaLog)\n    val commitVersion = deltaTxn.commitIfNeeded(\n      actions = finalActions,\n      op = operation,\n      tags = RowTracking.addPreservedRowTrackingTagIfNotSet(deltaTxn.snapshot))\n    val stats = collectMergeStats(deltaTxn, materializeSourceReason, commitVersion, numRecordsStats)\n    recordDeltaEvent(targetDeltaLog, \"delta.dml.merge.stats\", data = stats)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/MergeIntoCommandBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.mutable\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.metric.IncrementMetric\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, FileAction}\nimport org.apache.spark.sql.delta.commands.merge.{MergeIntoMaterializeSource, MergeIntoMaterializeSourceReason, MergeStats}\nimport org.apache.spark.sql.delta.files.{TahoeBatchFileIndex, TahoeFileIndex, TransactionalWrite}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.{ImplicitMetadataOperation, SchemaUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.SparkContext\nimport org.apache.spark.sql.{AnalysisException, DataFrame, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.StructType\n\ntrait MergeIntoCommandBase extends LeafRunnableCommand\n  with DeltaCommand\n  with DeltaLogging\n  with PredicateHelper\n  with ImplicitMetadataOperation\n  with MergeIntoMaterializeSource\n  with UpdateExpressionsSupport\n  with SupportsNonDeterministicExpression {\n\n  @transient val source: LogicalPlan\n  @transient val target: LogicalPlan\n  @transient val targetFileIndex: TahoeFileIndex\n  val condition: Expression\n  val matchedClauses: Seq[DeltaMergeIntoMatchedClause]\n  val notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause]\n  val notMatchedBySourceClauses: Seq[DeltaMergeIntoNotMatchedBySourceClause]\n  val migratedSchema: Option[StructType]\n  val schemaEvolutionEnabled: Boolean\n\n  protected def shouldWritePersistentDeletionVectors(\n      spark: SparkSession,\n      txn: OptimisticTransaction): Boolean = {\n    spark.conf.get(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS) &&\n      DeletionVectorUtils.deletionVectorsWritable(txn.snapshot)\n  }\n\n  override val (canMergeSchema, canOverwriteSchema) = {\n    // Delta options can't be passed to MERGE INTO currently, so they'll always be empty.\n    // The methods in options check if user has instructed to turn on schema evolution for this\n    // statement, or the auto migration DeltaSQLConf is on, in which case schema evolution\n    // will be allowed.\n    val options = new DeltaOptions(Map.empty[String, String], conf)\n    (schemaEvolutionEnabled || options.canMergeSchema, options.canOverwriteSchema)\n  }\n\n  @transient protected lazy val sc: SparkContext = SparkContext.getOrCreate()\n  @transient protected lazy val targetDeltaLog: DeltaLog = targetFileIndex.deltaLog\n\n  /**\n   * Map to get target read attributes by name. The case sensitivity of the map is set accordingly\n   * to Spark configuration.\n   */\n  @transient private lazy val preEvolutionTargetAttributesMap: Map[String, Attribute] = {\n    val attrMap: Map[String, Attribute] = target\n      .outputSet.view\n      .map(attr => attr.name -> attr).toMap\n    if (conf.caseSensitiveAnalysis) {\n      attrMap\n    } else {\n      CaseInsensitiveMap(attrMap)\n    }\n  }\n\n  /**\n   * Expressions to convert from a pre-evolution target row to the post-evolution target row. These\n   * expressions are used for columns that are not modified in updated rows or to copy rows that are\n   * not modified.\n   * There are two kinds of expressions here:\n   *  * References to existing columns in the target dataframe. Note that these references may have\n   *    a different data type than they originally did due to schema evolution so we add a cast that\n   *    supports schema evolution. The references will be marked as nullable if `makeNullable` is\n   *    set to true, which allows the attributes to reference the output of an outer join.\n   *  * Literal nulls, for new columns which are being added to the target table as part of\n   *    this transaction, since new columns will have a value of null for all existing rows.\n   */\n  protected def postEvolutionTargetExpressions(makeNullable: Boolean = false)\n    : Seq[NamedExpression] = {\n    val schema = if (makeNullable) {\n      migratedSchema.getOrElse(target.schema).asNullable\n    } else {\n      migratedSchema.getOrElse(target.schema)\n    }\n    schema.map { col =>\n      preEvolutionTargetAttributesMap\n        .get(col.name)\n        .map { attr =>\n          Alias(\n            castIfNeeded(\n              attr.withNullability(attr.nullable || makeNullable),\n              col.dataType,\n              castingBehavior = MergeOrUpdateCastingBehavior(canMergeSchema),\n              col.name),\n            col.name\n          )()\n        }\n        .getOrElse(Alias(Literal(null), col.name)())\n    }\n  }\n\n  /** Whether this merge statement only has MATCHED clauses. */\n  protected def isMatchedOnly: Boolean = notMatchedClauses.isEmpty && matchedClauses.nonEmpty &&\n    notMatchedBySourceClauses.isEmpty\n\n  /** Whether this merge statement only has insert (NOT MATCHED) clauses. */\n  protected def isInsertOnly: Boolean = matchedClauses.isEmpty && notMatchedClauses.nonEmpty &&\n    notMatchedBySourceClauses.isEmpty\n\n  /** Whether this merge statement only has delete clauses. */\n  protected lazy val isDeleteOnly: Boolean =\n    matchedClauses.forall(_.isInstanceOf[DeltaMergeIntoMatchedDeleteClause]) &&\n      notMatchedClauses.isEmpty &&\n      notMatchedBySourceClauses.forall(_.isInstanceOf[DeltaMergeIntoNotMatchedBySourceDeleteClause])\n\n  /** Whether this merge statement includes inserts statements. */\n  protected def includesInserts: Boolean = notMatchedClauses.nonEmpty\n\n  /** Whether this merge statement includes delete statements. */\n  protected def includesDeletes: Boolean = {\n    matchedClauses.exists(_.isInstanceOf[DeltaMergeIntoMatchedDeleteClause]) ||\n      notMatchedBySourceClauses.exists(_.isInstanceOf[DeltaMergeIntoNotMatchedBySourceDeleteClause])\n  }\n\n  protected def isCdcEnabled(deltaTxn: OptimisticTransaction): Boolean =\n    DeltaConfigs.CHANGE_DATA_FEED.fromMetaData(deltaTxn.metadata)\n\n  protected def runMerge(spark: SparkSession): Seq[Row]\n\n  override def run(spark: SparkSession): Seq[Row] = {\n    metrics(\"executionTimeMs\").set(0)\n    metrics(\"scanTimeMs\").set(0)\n    metrics(\"rewriteTimeMs\").set(0)\n    if (migratedSchema.isDefined) {\n      // Block writes of void columns in the Delta log. Currently void columns are not properly\n      // supported and are dropped on read, but this is not enough for merge command that is also\n      // reading the schema from the Delta log. Until proper support we prefer to fail merge\n      // queries that add void columns.\n      val newNullColumn = SchemaUtils.findNullTypeColumn(migratedSchema.get)\n      if (newNullColumn.isDefined) {\n        throw DeltaErrors.mergeAddVoidColumn(newNullColumn.get)\n      }\n    }\n\n    val (materializeSource, _) = shouldMaterializeSource(spark, source, isInsertOnly)\n    if (!materializeSource) {\n      runMerge(spark)\n    } else {\n      // If it is determined that source should be materialized, wrap the execution with retries,\n      // in case the data of the materialized source is lost.\n      runWithMaterializedSourceLostRetries(\n        spark, targetFileIndex.deltaLog, metrics, runMerge)\n    }\n  }\n\n  import SQLMetrics._\n\n  override lazy val metrics: Map[String, SQLMetric] = baseMetrics\n\n  lazy val baseMetrics: Map[String, SQLMetric] = Map(\n    \"numSourceRows\" -> createMetric(sc, \"number of source rows\"),\n    \"numSourceRowsInSecondScan\" ->\n      createMetric(sc, \"number of source rows (during repeated scan)\"),\n    \"operationNumSourceRows\" -> createMetric(sc, \"number of source rows (reported)\"),\n    \"numTargetRowsCopied\" -> createMetric(sc, \"number of target rows rewritten unmodified\"),\n    \"numTargetRowsInserted\" -> createMetric(sc, \"number of inserted rows\"),\n    \"numTargetRowsUpdated\" -> createMetric(sc, \"number of updated rows\"),\n    \"numTargetRowsMatchedUpdated\" -> createMetric(sc, \"number of rows updated by a matched clause\"),\n    \"numTargetRowsNotMatchedBySourceUpdated\" ->\n      createMetric(sc, \"number of rows updated by a not matched by source clause\"),\n    \"numTargetRowsDeleted\" -> createMetric(sc, \"number of deleted rows\"),\n    \"numTargetRowsMatchedDeleted\" -> createMetric(sc, \"number of rows deleted by a matched clause\"),\n    \"numTargetRowsNotMatchedBySourceDeleted\" ->\n      createMetric(sc, \"number of rows deleted by a not matched by source clause\"),\n    \"numTargetFilesBeforeSkipping\" -> createMetric(sc, \"number of target files before skipping\"),\n    \"numTargetFilesAfterSkipping\" -> createMetric(sc, \"number of target files after skipping\"),\n    \"numTargetFilesRemoved\" -> createMetric(sc, \"number of files removed to target\"),\n    \"numTargetFilesAdded\" -> createMetric(sc, \"number of files added to target\"),\n    \"numTargetChangeFilesAdded\" ->\n      createMetric(sc, \"number of change data capture files generated\"),\n    \"numTargetChangeFileBytes\" ->\n      createMetric(sc, \"total size of change data capture files generated\"),\n    \"numTargetBytesBeforeSkipping\" -> createMetric(sc, \"number of target bytes before skipping\"),\n    \"numTargetBytesAfterSkipping\" -> createMetric(sc, \"number of target bytes after skipping\"),\n    \"numTargetBytesRemoved\" -> createMetric(sc, \"number of target bytes removed\"),\n    \"numTargetBytesAdded\" -> createMetric(sc, \"number of target bytes added\"),\n    \"numTargetPartitionsAfterSkipping\" ->\n      createMetric(sc, \"number of target partitions after skipping\"),\n    \"numTargetPartitionsRemovedFrom\" ->\n      createMetric(sc, \"number of target partitions from which files were removed\"),\n    \"numTargetPartitionsAddedTo\" ->\n      createMetric(sc, \"number of target partitions to which files were added\"),\n    \"executionTimeMs\" ->\n      createTimingMetric(sc, \"time taken to execute the entire operation\"),\n    \"materializeSourceTimeMs\" ->\n      createTimingMetric(sc, \"time taken to materialize source (or determine it's not needed)\"),\n    \"scanTimeMs\" ->\n      createTimingMetric(sc, \"time taken to scan the files for matches\"),\n    \"rewriteTimeMs\" ->\n      createTimingMetric(sc, \"time taken to rewrite the matched files\"),\n    \"numTargetDeletionVectorsAdded\" -> createMetric(sc, \"number of deletion vectors added\"),\n    \"numTargetDeletionVectorsRemoved\" -> createMetric(sc, \"number of deletion vectors removed\"),\n    \"numTargetDeletionVectorsUpdated\" -> createMetric(sc, \"number of deletion vectors updated\")\n  )\n\n  /**\n   * Collects the merge operation stats and metrics into a [[MergeStats]] object that can be\n   * recorded with `recordDeltaEvent`. Merge stats should be collected after committing all new\n   * actions as metrics may still be updated during commit.\n   */\n  protected def collectMergeStats(\n      deltaTxn: OptimisticTransaction,\n      materializeSourceReason: MergeIntoMaterializeSourceReason.MergeIntoMaterializeSourceReason,\n      commitVersion: Option[Long],\n      numRecordsStats: NumRecordsStats): MergeStats = {\n    val stats = MergeStats.fromMergeSQLMetrics(\n      metrics,\n      condition,\n      matchedClauses,\n      notMatchedClauses,\n      notMatchedBySourceClauses,\n      isPartitioned = deltaTxn.metadata.partitionColumns.nonEmpty,\n      performedSecondSourceScan = performedSecondSourceScan,\n      commitVersion = commitVersion,\n      numRecordsStats = numRecordsStats\n    )\n    stats.copy(\n      materializeSourceReason = Some(materializeSourceReason.toString),\n      materializeSourceAttempts = Some(attempt))\n  }\n\n  protected def shouldOptimizeMatchedOnlyMerge(spark: SparkSession): Boolean = {\n    isMatchedOnly && spark.conf.get(DeltaSQLConf.MERGE_MATCHED_ONLY_ENABLED)\n  }\n\n  // There is only one when matched clause and it's a Delete and it does not have a condition.\n  protected val isOnlyOneUnconditionalDelete: Boolean =\n    matchedClauses == Seq(DeltaMergeIntoMatchedDeleteClause(None))\n\n  // We over-count numTargetRowsDeleted when there are multiple matches;\n  // this is the amount of the overcount, so we can subtract it to get a correct final metric.\n  protected var multipleMatchDeleteOnlyOvercount: Option[Long] = None\n\n  // Throw error if multiple matches are ambiguous or cannot be computed correctly.\n  protected def throwErrorOnMultipleMatches(\n      hasMultipleMatches: Boolean, spark: SparkSession): Unit = {\n    // Multiple matches are not ambiguous when there is only one unconditional delete as\n    // all the matched row pairs in the 2nd join in `writeAllChanges` will get deleted.\n    if (hasMultipleMatches && !isOnlyOneUnconditionalDelete) {\n      throw DeltaErrors.multipleSourceRowMatchingTargetRowInMergeException(spark)\n    }\n  }\n\n  /**\n   * Write the output data to files, repartitioning the output DataFrame by the partition columns\n   * if table is partitioned and `merge.repartitionBeforeWrite.enabled` is set to true.\n   */\n  protected def writeFiles(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      outputDF: DataFrame): Seq[FileAction] = {\n    val partitionColumns = txn.metadata.partitionColumns\n    // If the write will be an optimized write, which shuffles the data anyway, then don't\n    // repartition. Optimized writes can handle both splitting very large tasks and coalescing\n    // very small ones.\n    if (partitionColumns.nonEmpty && spark.conf.get(DeltaSQLConf.MERGE_REPARTITION_BEFORE_WRITE)\n      && !TransactionalWrite.shouldOptimizeWrite(txn.metadata, spark.sessionState.conf)) {\n      txn.writeFiles(outputDF.repartition(partitionColumns.map(col): _*))\n    } else {\n      txn.writeFiles(outputDF)\n    }\n  }\n\n  /**\n   * Builds a new logical plan to read the given `files` instead of the whole target table.\n   * The plan returned has the same output columns (exprIds) as the `target` logical plan, so that\n   * existing update/insert expressions can be applied on this new plan. Unneeded non-partition\n   * columns may be dropped.\n   */\n  protected def buildTargetPlanWithFiles(\n      spark: SparkSession,\n      deltaTxn: OptimisticTransaction,\n      files: Seq[AddFile],\n      columnsToDrop: Seq[String]): LogicalPlan = {\n    // Action type \"batch\" is a historical artifact; the original implementation used it.\n    val fileIndex = new TahoeBatchFileIndex(\n      spark,\n      actionType = \"batch\",\n      files,\n      deltaTxn.deltaLog,\n      targetFileIndex.path,\n      deltaTxn.snapshot)\n\n    buildTargetPlanWithIndex(\n      spark,\n      fileIndex,\n      columnsToDrop\n    )\n  }\n\n  /**\n   * Builds a new logical plan to read the target table using the given `fileIndex`.\n   * The plan returned has the same output columns (exprIds) as the `target` logical plan, so that\n   * existing update/insert expressions can be applied on this new plan.\n   *\n   * @param columnsToDrop unneeded non-partition columns to be dropped\n   */\n  protected def buildTargetPlanWithIndex(\n      spark: SparkSession,\n      fileIndex: TahoeFileIndex,\n      columnsToDrop: Seq[String]): LogicalPlan = {\n    var newTarget = DeltaTableUtils.replaceFileIndex(target, fileIndex)\n    newTarget = DeltaTableUtils.dropColumns(spark, newTarget, columnsToDrop)\n    newTarget\n  }\n\n  /** @return An `Expression` to increment a SQL metric */\n  protected def incrementMetricAndReturnBool(\n      name: String,\n      valueToReturn: Boolean): Expression = {\n    IncrementMetric(Literal(valueToReturn), metrics(name))\n  }\n\n  /** @return An `Expression` to increment SQL metrics */\n  protected def incrementMetricsAndReturnBool(\n      names: Seq[String],\n      valueToReturn: Boolean): Expression = {\n    val incExpr = incrementMetricAndReturnBool(names.head, valueToReturn)\n    names.tail.foldLeft(incExpr) { case (expr, name) =>\n      IncrementMetric(expr, metrics(name))\n    }\n  }\n\n  protected def getTargetOnlyPredicates(spark: SparkSession): Seq[Expression] = {\n    val targetOnlyPredicatesOnCondition =\n      splitConjunctivePredicates(condition).filter(_.references.subsetOf(target.outputSet))\n\n    if (!isMatchedOnly) {\n      targetOnlyPredicatesOnCondition\n    } else {\n      val targetOnlyMatchedPredicate = matchedClauses\n        .map(_.condition.getOrElse(Literal.TrueLiteral))\n        .map { condition =>\n          splitConjunctivePredicates(condition)\n            .filter(_.references.subsetOf(target.outputSet))\n            .reduceOption(And)\n            .getOrElse(Literal.TrueLiteral)\n        }\n        .reduceOption(Or)\n      targetOnlyPredicatesOnCondition ++ targetOnlyMatchedPredicate\n    }\n  }\n\n  protected def seqToString(exprs: Seq[Expression]): String = exprs.map(_.sql).mkString(\"\\n\\t\")\n\n  /**\n   * Execute the given `thunk` and return its result while recording the time taken to do it\n   * and setting additional local properties for better UI visibility.\n   *\n   * @param extraOpType extra operation name recorded in the logs\n   * @param status human readable status string describing what the thunk is doing\n   * @param sqlMetricName name of SQL metric to update with the time taken by the thunk\n   * @param thunk the code to execute\n   */\n  protected def recordMergeOperation[A](\n      extraOpType: String = \"\",\n      status: String = null,\n      sqlMetricName: String = null)(\n      thunk: => A): A = {\n    val changedOpType = if (extraOpType == \"\") {\n      \"delta.dml.merge\"\n    } else {\n      s\"delta.dml.merge.$extraOpType\"\n    }\n\n    val prevDesc = sc.getLocalProperty(SparkContext.SPARK_JOB_DESCRIPTION)\n    val newDesc = Option(status).map { s =>\n      // Append the status to existing description if any\n      val prefix = Option(prevDesc).filter(_.nonEmpty).map(_ + \" - \").getOrElse(\"\")\n      prefix + s\n    }\n\n    def executeThunk(): A = {\n      try {\n        val startTimeNs = System.nanoTime()\n        newDesc.foreach { d => sc.setJobDescription(d) }\n        val r = thunk\n        val timeTakenMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)\n        if (sqlMetricName != null) {\n          if (timeTakenMs > 0) {\n            metrics(sqlMetricName) += timeTakenMs\n          } else if (metrics(sqlMetricName).isZero) {\n            // Make it always at least 1ms if it ran, to distinguish whether it ran or not.\n            metrics(sqlMetricName) += 1\n          }\n        }\n        r\n      } finally {\n        if (newDesc.isDefined) {\n          sc.setJobDescription(prevDesc)\n        }\n      }\n    }\n\n    recordDeltaOperation(targetDeltaLog, changedOpType) {\n      if (status == null) {\n        executeThunk()\n      } else {\n        withStatusCode(\"DELTA\", status) { executeThunk() }\n      }\n    }\n  }\n\n  // Whether the source was scanned the second time, and it was guaranteed to be a scan without\n  // pruning.\n  protected var secondSourceScanWasFullScan: Boolean = false\n\n  /**\n   * Sets operationNumSourceRows as numSourceRowsInSecondScan if the source was scanned without\n   * possibility of pruning in the 2nd scan. Uses numSourceRows otherwise.\n   */\n  protected def setOperationNumSourceRowsMetric(): Unit = {\n    val operationNumSourceRows = if (secondSourceScanWasFullScan) {\n      metrics(\"numSourceRowsInSecondScan\").value\n    } else {\n      metrics(\"numSourceRows\").value\n    }\n    metrics(\"operationNumSourceRows\").set(operationNumSourceRows)\n  }\n\n  // Whether we actually scanned the source twice or the value in numSourceRowsInSecondScan is\n  // uninitialised.\n  protected var performedSecondSourceScan: Boolean = true\n\n  /**\n   * Throws an exception if merge metrics indicate that the source table changed between the first\n   * and the second source table scans.\n   */\n  protected def checkNonDeterministicSource(spark: SparkSession): Unit = {\n    // We only detect changes in the number of source rows. This is a best-effort detection; a\n    // more comprehensive solution would be to checksum the values for the columns that we read\n    // in both jobs.\n    // If numSourceRowsInSecondScan is < 0 then it hasn't run, e.g. for insert-only merges.\n    // In that case we have only read the source table once.\n    if (performedSecondSourceScan &&\n        metrics(\"numSourceRows\").value != metrics(\"numSourceRowsInSecondScan\").value) {\n      log.warn(s\"Merge source has ${metrics(\"numSourceRows\")} rows in initial scan but \" +\n        s\"${metrics(\"numSourceRowsInSecondScan\")} rows in second scan\")\n      if (conf.getConf(DeltaSQLConf.MERGE_FAIL_IF_SOURCE_CHANGED)) {\n        throw DeltaErrors.sourceNotDeterministicInMergeException(spark)\n      }\n    }\n  }\n\n  /**\n   * Check whether (part of) the give source dataframe is cached and logs an assertion or fails if\n   * it is. Query caching doesn't pin versions of delta tables and can lead to incorrect results so\n   * cached source plans must be materialized.\n   */\n  def checkSourcePlanIsNotCached(spark: SparkSession, source: LogicalPlan): Unit = {\n    val sourceIsCached = planContainsCachedRelation(DataFrameUtils.ofRows(spark, source))\n    if (sourceIsCached &&\n        spark.conf.get(DeltaSQLConf.MERGE_FAIL_SOURCE_CACHED_AFTER_MATERIALIZATION)) {\n      throw DeltaErrors.mergeConcurrentOperationCachedSourceException()\n    }\n\n    deltaAssert(\n      !sourceIsCached,\n      name = \"merge.sourceCachedAfterMaterializationStep\",\n      msg = \"Cached source plans must be materialized in MERGE but the source only got cached \" +\n        \"after the decision to materialize was taken.\",\n      deltaLog = targetDeltaLog\n    )\n  }\n\n  override protected def prepareMergeSource(\n      spark: SparkSession,\n      source: LogicalPlan,\n      condition: Expression,\n      matchedClauses: Seq[DeltaMergeIntoMatchedClause],\n      notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause],\n      isInsertOnly: Boolean\n    ): Unit = recordMergeOperation(\n      extraOpType = \"materializeSource\",\n      status = \"MERGE operation - materialize source\",\n      sqlMetricName = \"materializeSourceTimeMs\") {\n    super.prepareMergeSource(\n      spark, source, condition, matchedClauses, notMatchedClauses, isInsertOnly)\n  }\n\n  /**\n   * Verify that the high water marks used by the identity column generators still match the\n   * the high water marks in the version of the table read by the current transaction.\n   * These high water marks were determined during analysis in [[PreprocessTableMerge]],\n   * which runs outside of the current transaction, so they may no longer be valid.\n   */\n  protected def checkIdentityColumnHighWaterMarks(deltaTxn: OptimisticTransaction): Unit = {\n    notMatchedClauses.foreach { clause =>\n      if (deltaTxn.metadata.schema.length != clause.resolvedActions.length) {\n        throw new IllegalStateException\n      }\n      deltaTxn.metadata.schema.zip(clause.resolvedActions.map(_.expr)).foreach {\n        case (f, GenerateIdentityValues(gen)) =>\n          val info = IdentityColumn.getIdentityInfo(f)\n          if (info.highWaterMark != gen.highWaterMarkOpt) {\n            IdentityColumn.logTransactionAbort(deltaTxn.deltaLog)\n            throw DeltaErrors.metadataChangedException(conflictingCommit = None)\n          }\n\n        case (f, _) if ColumnWithDefaultExprUtils.isIdentityColumn(f) &&\n          !IdentityColumn.allowExplicitInsert(f) =>\n          throw new IllegalStateException\n\n        case _ => ()\n      }\n    }\n  }\n\n  /** Returns whether it allows non-deterministic expressions. */\n  override def allowNonDeterministicExpression: Boolean = {\n    def isConditionDeterministic(mergeClause: DeltaMergeIntoClause): Boolean = {\n      mergeClause.condition match {\n        case Some(c) => c.deterministic\n        case None => true\n      }\n    }\n    // Allow actions to be non-deterministic while all the conditions\n    // must be deterministic.\n    condition.deterministic &&\n      matchedClauses.forall(isConditionDeterministic) &&\n      notMatchedClauses.forall(isConditionDeterministic) &&\n      notMatchedBySourceClauses.forall(isConditionDeterministic)\n  }\n\n  /**\n   * Validates that the number of records does not increase if there are no insert clauses, and does\n   * not decrease if there are no delete clauses.\n   *\n   * Note: ideally we would also compare the number of added/removed rows in the statistics with the\n   * number of inserted/updated/deleted/copied rows in the SQL metrics, but unfortunately this is\n   * not possible, as sql metrics are not reliable when there are task or stage retries.\n   */\n  protected def validateNumRecords(\n      actions: Seq[Action],\n      numRecordsStats: NumRecordsStats,\n      op: DeltaOperations.Merge,\n      deltaLog: DeltaLog): Unit = {\n    (numRecordsStats.numLogicalRecordsAdded,\n      numRecordsStats.numLogicalRecordsRemoved,\n      numRecordsStats.numLogicalRecordsAddedInFilesWithDeletionVectors) match {\n      case (\n        Some(numAddedRecords),\n        Some(numRemovedRecords),\n        Some(numRecordsNotCopied)) =>\n        if (!includesInserts && numAddedRecords > numRemovedRecords) {\n          logNumRecordsMismatch(targetDeltaLog, actions, numRecordsStats, op)\n          if (conf.getConf(DeltaSQLConf.NUM_RECORDS_VALIDATION_ENABLED)) {\n            throw DeltaErrors.numRecordsMismatch(\n              operation = \"MERGE without INSERT clauses\",\n              numAddedRecords,\n              numRemovedRecords\n            )\n          }\n        }\n        if (!includesDeletes && numAddedRecords < numRemovedRecords) {\n          logNumRecordsMismatch(targetDeltaLog, actions, numRecordsStats, op)\n          if (conf.getConf(DeltaSQLConf.NUM_RECORDS_VALIDATION_ENABLED)) {\n            throw DeltaErrors.numRecordsMismatch(\n              operation = \"MERGE without DELETE clauses\",\n              numAddedRecords,\n              numRemovedRecords\n            )\n          }\n        }\n\n        if (conf.getConf(DeltaSQLConf.COMMAND_INVARIANT_CHECKS_USE_UNRELIABLE)) {\n          // and also using regular (unreliable) metrics for baseline\n          validateMetricBasedCommandInvariants(\n            numAddedRecords, numRemovedRecords, numRecordsNotCopied, op, deltaLog)\n        }\n\n      case _ =>\n        recordDeltaEvent(\n          targetDeltaLog, opType = \"delta.assertions.statsNotPresentForNumRecordsCheck\")\n        logWarning(log\"Could not validate number of records due to missing statistics.\")\n    }\n  }\n\n  private def validateMetricBasedCommandInvariants(\n      numAddedRecords: Long,\n      numRemovedRecords: Long,\n      numRecordsNotCopied: Long,\n      op: DeltaOperations.Merge,\n      deltaLog: DeltaLog): Unit = try {\n\n    val numRowsInserted = CommandInvariantMetricValueFromSingle(metrics(\"numTargetRowsInserted\"))\n    val numRowsUpdated = CommandInvariantMetricValueFromSingle(metrics(\"numTargetRowsUpdated\"))\n    val numRowsDeleted = CommandInvariantMetricValueFromSingle(metrics(\"numTargetRowsDeleted\"))\n    val numRowsCopied = CommandInvariantMetricValueFromSingle(metrics(\"numTargetRowsCopied\"))\n\n    checkCommandInvariant(\n      invariant = () => includesInserts || numRowsInserted.getOrThrow == 0,\n      label = \"includesInserts || numRowsInserted == 0\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsInserted\" -> numRowsInserted.getOrDummy\n      )\n    )\n\n    checkCommandInvariant(\n      invariant = () => includesDeletes || numRowsDeleted.getOrThrow == 0,\n      label = \"includesDeletes || numRowsDeleted == 0\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsDeleted\" -> numRowsDeleted.getOrDummy\n      )\n    )\n\n    checkCommandInvariant(\n      invariant = () => !isDeleteOnly ||\n        numRowsUpdated.getOrThrow + numRowsInserted.getOrThrow == 0,\n      label = \"!isDeleteOnlyMerge || numRowsUpdated + numRowsInserted == 0\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n        \"numRowsInserted\" -> numRowsInserted.getOrDummy\n      )\n    )\n\n    if (conf.getConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED)) {\n      checkCommandInvariant(\n        invariant = () => !isInsertOnly ||\n          numRowsUpdated.getOrThrow +\n            numRowsDeleted.getOrThrow +\n            numRowsCopied.getOrThrow +\n            numRecordsNotCopied == 0,\n        label = \"!isInsertOnly\" +\n          \" || numRowsUpdated + numRowsDeleted + numRowsCopied + numRecordsNotCopied == 0\",\n        op = op,\n        deltaLog = deltaLog,\n        parameters = Map(\n          \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n          \"numRowsDeleted\" -> numRowsDeleted.getOrDummy,\n          \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n          \"numRecordsNotCopied\" -> numRecordsNotCopied\n        )\n      )\n    } else {\n      // When the special insert-only codepath is disabled, we may end up copying some rows.\n      checkCommandInvariant(\n        invariant = () => !isInsertOnly ||\n          numRowsUpdated.getOrThrow + numRowsDeleted.getOrThrow == 0,\n        label = \"!isInsertOnly\" +\n          \" || numRowsUpdated + numRowsDeleted == 0\",\n        op = op,\n        deltaLog = deltaLog,\n        parameters = Map(\n          \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n          \"numRowsDeleted\" -> numRowsDeleted.getOrDummy\n        )\n      )\n    }\n\n    checkCommandInvariant(\n      invariant = () =>\n        numRowsUpdated.getOrThrow +\n          numRowsDeleted.getOrThrow +\n          numRowsCopied.getOrThrow +\n          numRecordsNotCopied == numRemovedRecords,\n      label = \"numRowsUpdated + numRowsDeleted + numRowsCopied + numRecordsNotCopied ==\" +\n        \" numRemovedRecords\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n        \"numRowsDeleted\" -> numRowsDeleted.getOrDummy,\n        \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n        \"numRemovedRecords\" -> numRemovedRecords,\n        \"numRecordsNotCopied\" -> numRecordsNotCopied\n      )\n    )\n\n    checkCommandInvariant(\n      invariant = () =>\n        numRowsInserted.getOrThrow +\n          numRowsUpdated.getOrThrow +\n          numRowsCopied.getOrThrow +\n          numRecordsNotCopied == numAddedRecords,\n      label = \"numRowsInserted + numRowsUpdated + numRowsCopied + numRecordsNotCopied ==\" +\n        \" numAddedRecords\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsInserted\" -> numRowsInserted.getOrDummy,\n        \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n        \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n        \"numAddedRecords\" -> numAddedRecords,\n        \"numRecordsNotCopied\" -> numRecordsNotCopied\n      )\n    )\n  } catch {\n    // Immediately re-throw actual command invariant violations, so we don't re-wrap them below.\n    case e: DeltaIllegalStateException if e.getErrorClass == \"DELTA_COMMAND_INVARIANT_VIOLATION\" =>\n      throw e\n    case NonFatal(e) =>\n      logWarning(log\"Unexpected error in validateMetricBasedCommandInvariants\", e)\n      checkCommandInvariant(\n        invariant = () => false,\n        label = \"Unexpected error in validateMetricBasedCommandInvariants\",\n        op = op,\n        deltaLog = deltaLog,\n        parameters = Map.empty\n      )\n  }\n}\n\nobject MergeIntoCommandBase {\n  val ROW_ID_COL = \"_row_id_\"\n  val FILE_NAME_COL = \"_file_name_\"\n  val SOURCE_ROW_PRESENT_COL = \"_source_row_present_\"\n  val TARGET_ROW_PRESENT_COL = \"_target_row_present_\"\n  val ROW_DROPPED_COL = \"_row_dropped_\"\n  val PRECOMPUTED_CONDITION_COL = \"_condition_\"\n\n  /**\n   * Spark UI will track all normal accumulators along with Spark tasks to show them on Web UI.\n   * However, the accumulator used by `MergeIntoCommand` can store a very large value since it\n   * tracks all files that need to be rewritten. We should ask Spark UI to not remember it,\n   * otherwise, the UI data may consume lots of memory. Hence, we use the prefix `internal.metrics.`\n   * to make this accumulator become an internal accumulator, so that it will not be tracked by\n   * Spark UI.\n   */\n  val TOUCHED_FILES_ACCUM_NAME = \"internal.metrics.MergeIntoDelta.touchedFiles\"\n\n\n  /** Count the number of distinct partition values among the AddFiles in the given set. */\n  def totalBytesAndDistinctPartitionValues(files: Seq[FileAction]): (Long, Int) = {\n    val distinctValues = new mutable.HashSet[Map[String, String]]()\n    var bytes = 0L\n    files.collect { case file: AddFile =>\n      distinctValues += file.partitionValues\n      bytes += file.size\n    }.toList\n    // If the only distinct value map is an empty map, then it must be an unpartitioned table.\n    // Return 0 in that case.\n    val numDistinctValues =\n      if (distinctValues.size == 1 && distinctValues.head.isEmpty) 0 else distinctValues.size\n    (bytes, numDistinctValues)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/OptimizeTableCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.util.ConcurrentModificationException\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.skipping.MultiDimClustering\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, DeletionVectorDescriptor, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.commands.optimize._\nimport org.apache.spark.sql.delta.files.SQLMetricsReporting\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.BinPackingUtils\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.SparkContext.SPARK_JOB_GROUP_ID\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{AnalysisException, Encoders, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{UnresolvedAttribute, UnresolvedTable}\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.execution.command.RunnableCommand\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.execution.metric.SQLMetrics.createMetric\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.{SystemClock, ThreadUtils}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.delta.actions.InMemoryLogReplay.UniqueFileActionTuple\n\n/** Base class defining abstract optimize command */\nabstract class OptimizeTableCommandBase extends RunnableCommand with DeltaCommand {\n\n  override val output: Seq[Attribute] = Seq(\n    AttributeReference(\"path\", StringType)(),\n    AttributeReference(\"metrics\", Encoders.product[OptimizeMetrics].schema)())\n\n  /**\n   * Validates ZOrderBy columns\n   * - validates that partitions columns are not used in `unresolvedZOrderByCols`\n   * - validates that we already collect stats for all the columns used in `unresolvedZOrderByCols`\n   *\n   * @param spark [[SparkSession]] to use\n   * @param snapshot the [[Snapshot]] being used to optimize from\n   * @param unresolvedZOrderByCols Seq of [[UnresolvedAttribute]] corresponding to zOrderBy columns\n   */\n  def validateZorderByColumns(\n      spark: SparkSession,\n      snapshot: Snapshot,\n      unresolvedZOrderByCols: Seq[UnresolvedAttribute]): Unit = {\n    if (unresolvedZOrderByCols.isEmpty) return\n    val metadata = snapshot.metadata\n    val partitionColumns = metadata.partitionColumns.toSet\n    val dataSchema =\n      StructType(metadata.schema.filterNot(c => partitionColumns.contains(c.name)))\n    val df = spark.createDataFrame(new java.util.ArrayList[Row](), dataSchema)\n    val checkColStat = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_OPTIMIZE_ZORDER_COL_STAT_CHECK)\n    val statCollectionSchema = snapshot.statCollectionLogicalSchema\n    val colsWithoutStats = ArrayBuffer[String]()\n\n    unresolvedZOrderByCols.foreach { colAttribute =>\n      val colName = colAttribute.name\n      if (checkColStat) {\n        try {\n          SchemaUtils.findColumnPosition(colAttribute.nameParts, statCollectionSchema)\n        } catch {\n          case e: AnalysisException if e.getMessage.contains(\"Couldn't find column\") =>\n            colsWithoutStats.append(colName)\n        }\n      }\n      val isNameEqual = spark.sessionState.conf.resolver\n      if (partitionColumns.find(isNameEqual(_, colName)).nonEmpty) {\n        throw DeltaErrors.zOrderingOnPartitionColumnException(colName)\n      }\n      if (df.queryExecution.analyzed.resolve(colAttribute.nameParts, isNameEqual).isEmpty) {\n        throw DeltaErrors.zOrderingColumnDoesNotExistException(colName)\n      }\n    }\n    if (checkColStat && colsWithoutStats.nonEmpty) {\n      throw DeltaErrors.zOrderingOnColumnWithNoStatsException(\n        colsWithoutStats.toSeq, spark)\n    }\n  }\n}\n\nobject OptimizeTableCommand {\n  /**\n   * Alternate constructor that converts a provided path or table identifier into the\n   * correct child LogicalPlan node. If both path and tableIdentifier are specified (or\n   * if both are None), this method will throw an exception. If a table identifier is\n   * specified, the child LogicalPlan will be an [[UnresolvedTable]] whereas if a path\n   * is specified, it will be an [[UnresolvedPathBasedDeltaTable]].\n   *\n   * Note that the returned OptimizeTableCommand will have an *unresolved* child table\n   * and hence, the command needs to be analyzed before it can be executed.\n   */\n  def apply(\n      path: Option[String],\n      tableIdentifier: Option[TableIdentifier],\n      userPartitionPredicates: Seq[String],\n      optimizeContext: DeltaOptimizeContext = DeltaOptimizeContext())(\n      zOrderBy: Seq[UnresolvedAttribute]): OptimizeTableCommand = {\n    val plan = UnresolvedDeltaPathOrIdentifier(path, tableIdentifier, \"OPTIMIZE\")\n    OptimizeTableCommand(plan, userPartitionPredicates, optimizeContext)(zOrderBy)\n  }\n}\n\n/**\n * The `optimize` command implementation for Spark SQL. Example SQL:\n * {{{\n *    OPTIMIZE ('/path/to/dir' | delta.table) [WHERE part = 25] [FULL];\n * }}}\n *\n * Note FULL and WHERE clauses are set exclusively.\n */\ncase class OptimizeTableCommand(\n    override val child: LogicalPlan,\n    userPartitionPredicates: Seq[String],\n    optimizeContext: DeltaOptimizeContext)(\n    val zOrderBy: Seq[UnresolvedAttribute])\n  extends OptimizeTableCommandBase\n  with UnaryNode {\n\n  override val otherCopyArgs: Seq[AnyRef] = zOrderBy :: Nil\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): OptimizeTableCommand =\n    copy(child = newChild)(zOrderBy)\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val table = getDeltaTable(child, \"OPTIMIZE\")\n    val snapshot = table.update()\n    if (snapshot.version == -1) {\n      throw DeltaErrors.notADeltaTableException(table.deltaLog.dataPath.toString)\n    }\n\n    if (snapshot.isCatalogOwned) {\n      throw DeltaErrors.operationBlockedOnCatalogManagedTable(\"OPTIMIZE\")\n    }\n\n    val isClusteredTable = ClusteredTableUtils.isSupported(snapshot.protocol)\n    if (isClusteredTable) {\n      if (userPartitionPredicates.nonEmpty) {\n        throw DeltaErrors.clusteringWithPartitionPredicatesException(userPartitionPredicates)\n      }\n      if (zOrderBy.nonEmpty) {\n        throw DeltaErrors.clusteringWithZOrderByException(zOrderBy)\n      }\n    }\n\n    lazy val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(snapshot)\n    if (optimizeContext.isFull && (!isClusteredTable || clusteringColumns.isEmpty)) {\n      throw DeltaErrors.optimizeFullNotSupportedException()\n    }\n\n    val partitionColumns = snapshot.metadata.partitionColumns\n    // Parse the predicate expression into Catalyst expression and verify only simple filters\n    // on partition columns are present\n\n    val partitionPredicates = userPartitionPredicates.flatMap { predicate =>\n        val predicates = parsePredicates(sparkSession, predicate)\n        verifyPartitionPredicates(\n          sparkSession,\n          partitionColumns,\n          predicates)\n        predicates\n    }\n\n    validateZorderByColumns(sparkSession, snapshot, zOrderBy)\n    val zOrderByColumns = zOrderBy.map(_.name).toSeq\n\n    new OptimizeExecutor(\n      sparkSession,\n      snapshot,\n      table.catalogTable,\n      partitionPredicates,\n      zOrderByColumns,\n      isAutoCompact = false,\n      optimizeContext\n    ).optimize()\n  }\n}\n\n/**\n * Stored all runtime context information that can control the execution of optimize.\n *\n * @param reorg The REORG operation that triggered the rewriting task, if any.\n * @param minFileSize Files which are smaller than this threshold will be selected for compaction.\n *                    If not specified, [[DeltaSQLConf.DELTA_OPTIMIZE_MIN_FILE_SIZE]] will be used.\n *                    This parameter must be set to `0` when [[reorg]] is set.\n * @param maxDeletedRowsRatio Files with a ratio of soft-deleted rows to the total rows larger than\n *                            this threshold will be rewritten by the OPTIMIZE command. If not\n *                            specified, [[DeltaSQLConf.DELTA_OPTIMIZE_MAX_DELETED_ROWS_RATIO]]\n *                            will be used. This parameter must be set to `0` when [[reorg]] is set.\n * @param isFull whether OPTIMIZE FULL is run. This is only for clustered tables.\n */\ncase class DeltaOptimizeContext(\n    reorg: Option[DeltaReorgOperation] = None,\n    minFileSize: Option[Long] = None,\n    maxFileSize: Option[Long] = None,\n    maxDeletedRowsRatio: Option[Double] = None,\n    isFull: Boolean = false) {\n  if (reorg.nonEmpty) {\n    require(\n      minFileSize.contains(0L) && maxDeletedRowsRatio.contains(0d),\n      \"minFileSize and maxDeletedRowsRatio must be 0 when running REORG TABLE.\")\n  }\n}\n\n/**\n * A bin represents a single set of files that are being re-written in a single Spark job.\n * For compaction, this represents a single file being written. For clustering, this is\n * an entire partition for Z-ordering, or an entire ZCube for liquid clustering.\n *\n * @param partitionValues The partition this set of files is in\n * @param files The list of files being re-written\n */\ncase class Bin(partitionValues: Map[String, String], files: Seq[AddFile])\n\n/**\n * A batch represents all the bins that will be processed and commited in a single transaction.\n *\n * @param bins The set of bins to process in this transaction\n */\ncase class Batch(bins: Seq[Bin])\n\n/**\n * Optimize job which compacts small files into larger files to reduce\n * the number of files and potentially allow more efficient reads.\n *\n * @param sparkSession Spark environment reference.\n * @param snapshot The snapshot of the table to optimize\n * @param partitionPredicate List of partition predicates to select subset of files to optimize.\n */\nclass OptimizeExecutor(\n    sparkSession: SparkSession,\n    snapshot: Snapshot,\n    catalogTable: Option[CatalogTable],\n    partitionPredicate: Seq[Expression],\n    zOrderByColumns: Seq[String],\n    isAutoCompact: Boolean,\n    optimizeContext: DeltaOptimizeContext)\n  extends DeltaCommand with SQLMetricsReporting with Serializable {\n\n  /**\n   * In which mode the Optimize command is running. There are three valid modes:\n   * 1. Compaction\n   * 2. ZOrder\n   * 3. Clustering\n   */\n  private val optimizeStrategy =\n    OptimizeTableStrategy(sparkSession, snapshot, optimizeContext, zOrderByColumns)\n\n  /** Timestamp to use in [[FileAction]] */\n  private val operationTimestamp = new SystemClock().getTimeMillis()\n\n  private val isClusteredTable = ClusteredTableUtils.isSupported(snapshot.protocol)\n\n  private val isMultiDimClustering =\n    optimizeStrategy.isInstanceOf[ClusteringStrategy] ||\n    optimizeStrategy.isInstanceOf[ZOrderStrategy]\n\n  private val clusteringColumns: Seq[String] = {\n    if (zOrderByColumns.nonEmpty) {\n      zOrderByColumns\n    } else if (isClusteredTable) {\n      ClusteringColumnInfo.extractLogicalNames(snapshot)\n    } else {\n      Nil\n    }\n  }\n\n  private val partitionSchema = snapshot.metadata.partitionSchema\n\n  def optimize(): Seq[Row] = {\n    recordDeltaOperation(snapshot.deltaLog, \"delta.optimize\") {\n      val minFileSize = optimizeContext.minFileSize.getOrElse(\n        sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_MIN_FILE_SIZE))\n      val maxFileSize = optimizeContext.maxFileSize.getOrElse(\n        sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE))\n      val maxDeletedRowsRatio = optimizeContext.maxDeletedRowsRatio.getOrElse(\n        sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_MAX_DELETED_ROWS_RATIO))\n      val batchSize = sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_BATCH_SIZE)\n\n      // Get all the files from the snapshot, we will register them with the individual\n      // transactions later\n      val candidateFiles = snapshot.filesForScan(partitionPredicate, keepNumRecords = true).files\n\n      val filesToProcess = optimizeContext.reorg match {\n        case Some(reorgOperation) =>\n          reorgOperation.filterFilesToReorg(sparkSession, snapshot, candidateFiles)\n        case None =>\n          filterCandidateFileList(minFileSize, maxDeletedRowsRatio, candidateFiles)\n      }\n      val partitionsToCompact = filesToProcess.groupBy(_.partitionValues).toSeq\n\n      val jobs = groupFilesIntoBins(partitionsToCompact)\n\n      val batchResults = batchSize match {\n        case Some(size) =>\n          val batches = BinPackingUtils.binPackBySize[Bin, Bin](\n            jobs,\n            bin => bin.files.map(_.size).sum,\n            bin => bin,\n            size)\n          batches.map(batch => runOptimizeBatch(Batch(batch), maxFileSize))\n        case None =>\n          Seq(runOptimizeBatch(Batch(jobs), maxFileSize))\n      }\n\n      val addedFiles = batchResults.map(_._1).flatten\n      val removedFiles = batchResults.map(_._2).flatten\n      val removedDVs = batchResults.map(_._3).flatten\n\n      val optimizeStats = OptimizeStats()\n      optimizeStats.addedFilesSizeStats.merge(addedFiles)\n      optimizeStats.removedFilesSizeStats.merge(removedFiles)\n      optimizeStats.numPartitionsOptimized = jobs.map(j => j.partitionValues).distinct.size\n      optimizeStats.numBins = jobs.size\n      optimizeStats.numBatches = batchResults.size\n      optimizeStats.totalConsideredFiles = candidateFiles.size\n      optimizeStats.totalFilesSkipped = optimizeStats.totalConsideredFiles - removedFiles.size\n      optimizeStats.totalClusterParallelism = sparkSession.sparkContext.defaultParallelism\n      val numTableColumns = snapshot.metadata.schema.size\n      optimizeStats.numTableColumns = numTableColumns\n      optimizeStats.numTableColumnsWithStats =\n        DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.fromMetaData(snapshot.metadata)\n          .min(numTableColumns)\n      if (removedDVs.size > 0) {\n        optimizeStats.deletionVectorStats = Some(DeletionVectorStats(\n          numDeletionVectorsRemoved = removedDVs.size,\n          numDeletionVectorRowsRemoved = removedDVs.map(_.cardinality).sum))\n      }\n\n      optimizeStrategy.updateOptimizeStats(optimizeStats, removedFiles, jobs)\n\n      return Seq(Row(snapshot.deltaLog.dataPath.toString, optimizeStats.toOptimizeMetrics))\n    }\n  }\n\n  /**\n   * Helper method to prune the list of selected files based on fileSize and ratio of\n   * deleted rows according to the deletion vector in [[AddFile]].\n   */\n  private def filterCandidateFileList(\n      minFileSize: Long, maxDeletedRowsRatio: Double, files: Seq[AddFile]): Seq[AddFile] = {\n\n    // Select all files in case of multi-dimensional clustering\n    if (isMultiDimClustering) return files\n\n    def shouldCompactBecauseOfDeletedRows(file: AddFile): Boolean = {\n      // Always compact files with DVs but without numRecords stats.\n      // This may be overly aggressive, but it fixes the problem in the long-term,\n      // as the compacted files will have stats.\n      (file.deletionVector != null && file.numPhysicalRecords.isEmpty) ||\n          file.deletedToPhysicalRecordsRatio.getOrElse(0d) > maxDeletedRowsRatio\n    }\n\n    // Select files that are small or have too many deleted rows\n    files.filter(\n      addFile => addFile.size < minFileSize || shouldCompactBecauseOfDeletedRows(addFile))\n  }\n\n  /**\n   * Utility methods to group files into bins for optimize.\n   *\n   * @param partitionsToCompact List of files to compact group by partition.\n   *                            Partition is defined by the partition values (partCol -> partValue)\n   * @return Sequence of bins. Each bin contains one or more files from the same\n   *         partition and targeted for one output file.\n   */\n  private def groupFilesIntoBins(\n      partitionsToCompact: Seq[(Map[String, String], Seq[AddFile])])\n  : Seq[Bin] = {\n    val maxBinSize = optimizeStrategy.maxBinSize\n    partitionsToCompact.flatMap {\n      case (partition, files) =>\n        val bins = new ArrayBuffer[Seq[AddFile]]()\n\n        val currentBin = new ArrayBuffer[AddFile]()\n        var currentBinSize = 0L\n\n        val preparedFiles = optimizeStrategy.prepareFilesPerPartition(files)\n        preparedFiles.foreach { file =>\n          // Generally, a bin is a group of existing files, whose total size does not exceed the\n          // desired maxBinSize. The output file size depends on the mode:\n          // 1. Compaction: Files in a bin will be coalesced into a single output file.\n          // 2. ZOrder:  all files in a partition will be read by the\n          //    same job, the data will be range-partitioned and\n          //    numFiles = totalFileSize / maxFileSize will be produced.\n          // 3. Clustering: Files in a bin belongs to one ZCUBE, the data will be\n          //    range-partitioned and numFiles = totalFileSize / maxFileSize.\n          if (file.size + currentBinSize > maxBinSize) {\n            bins += currentBin.toVector\n            currentBin.clear()\n            currentBin += file\n            currentBinSize = file.size\n          } else {\n            currentBin += file\n            currentBinSize += file.size\n          }\n        }\n\n        if (currentBin.nonEmpty) {\n          bins += currentBin.toVector\n        }\n\n        bins.filter { bin =>\n          bin.size > 1 || // bin has more than one file or\n          bin.size == 1 && optimizeContext.reorg.nonEmpty || // always rewrite files during reorg\n          isMultiDimClustering // multi-clustering\n        }.map(b => Bin(partition, b))\n    }\n  }\n\n  private def runOptimizeBatch(\n    batch: Batch,\n    maxFileSize: Long\n  ): (Seq[AddFile], Seq[RemoveFile], Seq[DeletionVectorDescriptor]) = {\n    val txn = snapshot.deltaLog.startTransaction(catalogTable, Some(snapshot))\n\n    val filesToProcess = batch.bins.flatMap(_.files)\n\n    txn.trackFilesRead(filesToProcess)\n    txn.trackReadPredicates(partitionPredicate)\n\n    val maxThreads =\n      sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_MAX_THREADS)\n    val updates = ThreadUtils.parmap(batch.bins, \"OptimizeJob\", maxThreads) { partitionBinGroup =>\n      runOptimizeBinJob(txn, partitionBinGroup.partitionValues, partitionBinGroup.files,\n        maxFileSize)\n    }.flatten\n\n    val addedFiles = updates.collect { case a: AddFile => a }\n    val removedFiles = updates.collect { case r: RemoveFile => r }\n    val removedDVs = filesToProcess.filter(_.deletionVector != null).map(_.deletionVector).toSeq\n    if (addedFiles.size > 0) {\n      val metrics = createMetrics(sparkSession.sparkContext, addedFiles, removedFiles, removedDVs)\n      commitAndRetry(txn, getOperation(), updates, metrics) { newTxn =>\n        val newPartitionSchema = newTxn.metadata.partitionSchema\n        // Note: When checking if the candidate set is the same, we need to consider (Path, DV)\n        //       as the key.\n        val candidateSetOld = filesToProcess.\n          map(f => UniqueFileActionTuple(f.pathAsUri, f.getDeletionVectorUniqueId)).toSet\n\n        // We specifically don't list the files through the transaction since we are potentially\n        // only processing a subset of them below. If the transaction is still valid, we will\n        // register the files and predicate below\n        val candidateSetNew =\n          newTxn.snapshot.filesForScan(partitionPredicate).files\n            .map(f => UniqueFileActionTuple(f.pathAsUri, f.getDeletionVectorUniqueId)).toSet\n\n        // As long as all of the files that we compacted are still part of the table,\n        // and the partitioning has not changed it is valid to continue to try\n        // and commit this checkpoint.\n        if (candidateSetOld.subsetOf(candidateSetNew) && partitionSchema == newPartitionSchema) {\n          // Make sure the files we are processing are registered with the transaction\n          newTxn.trackFilesRead(filesToProcess)\n          newTxn.trackReadPredicates(partitionPredicate)\n          true\n        } else {\n          val deleted = candidateSetOld -- candidateSetNew\n          logWarning(log\"The following compacted files were deleted \" +\n            log\"during checkpoint ${MDC(DeltaLogKeys.PATHS, deleted.mkString(\",\"))}. \" +\n            log\"Aborting the compaction.\")\n          false\n        }\n      }\n    }\n    (addedFiles, removedFiles, removedDVs)\n  }\n\n  /**\n   * Utility method to run a Spark job to compact the files in given bin\n   *\n   * @param txn [[OptimisticTransaction]] instance in use to commit the changes to DeltaLog.\n   * @param partition Partition values of the partition that files in [[bin]] belongs to.\n   * @param bin List of files to compact into one large file.\n   * @param maxFileSize Targeted output file size in bytes\n   */\n  private def runOptimizeBinJob(\n      txn: OptimisticTransaction,\n      partition: Map[String, String],\n      bin: Seq[AddFile],\n      maxFileSize: Long): Seq[FileAction] = {\n    val baseTablePath = txn.deltaLog.dataPath\n\n    var input = txn.deltaLog.createDataFrame(txn.snapshot, bin, actionTypeOpt = Some(\"Optimize\"))\n    input = RowTracking.preserveRowTrackingColumns(input, txn.snapshot)\n    val repartitionDF = if (isMultiDimClustering) {\n      val totalSize = bin.map(_.size).sum\n      val approxNumFiles = Math.max(1, totalSize / maxFileSize).toInt\n      MultiDimClustering.cluster(\n        input,\n        approxNumFiles,\n        clusteringColumns,\n        optimizeStrategy.curve)\n    } else {\n      val useRepartition = sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_REPARTITION_ENABLED)\n      if (useRepartition) {\n        input.repartition(numPartitions = 1)\n      } else {\n        input.coalesce(numPartitions = 1)\n      }\n    }\n\n    val partitionDesc = partition.toSeq.map(entry => entry._1 + \"=\" + entry._2).mkString(\",\")\n\n    val partitionName = if (partition.isEmpty) \"\" else s\" in partition ($partitionDesc)\"\n    val description = s\"$baseTablePath<br/>Optimizing ${bin.size} files\" + partitionName\n    sparkSession.sparkContext.setJobGroup(\n      sparkSession.sparkContext.getLocalProperty(SPARK_JOB_GROUP_ID),\n      description)\n\n    val binInfo = optimizeStrategy.initNewBin\n    val addFiles = txn.writeFiles(repartitionDF, None, isOptimize = true, Nil).collect {\n      case a: AddFile => optimizeStrategy.tagAddFile(a, binInfo)\n      case other =>\n        throw new IllegalStateException(\n          s\"Unexpected action $other with type ${other.getClass}. File compaction job output\" +\n              s\"should only have AddFiles\")\n    }\n    val removeFiles = bin.map(f => f.removeWithTimestamp(operationTimestamp, dataChange = false))\n    val updates = addFiles ++ removeFiles\n    updates\n  }\n\n  /**\n   * Attempts to commit the given actions to the log. In the case of a concurrent update,\n   * the given function will be invoked with a new transaction to allow custom conflict\n   * detection logic to indicate it is safe to try again, by returning `true`.\n   *\n   * This function will continue to try to commit to the log as long as `f` returns `true`,\n   * otherwise throws a subclass of [[ConcurrentModificationException]].\n   */\n  private def commitAndRetry(\n      txn: OptimisticTransaction,\n      optimizeOperation: Operation,\n      actions: Seq[Action],\n      metrics: Map[String, SQLMetric])(f: OptimisticTransaction => Boolean): Unit = {\n    try {\n      txn.registerSQLMetrics(sparkSession, metrics)\n      txn.commit(actions, optimizeOperation,\n        RowTracking.addPreservedRowTrackingTagIfNotSet(txn.snapshot))\n    } catch {\n      case e: ConcurrentModificationException =>\n        val newTxn = txn.deltaLog.startTransaction(txn.catalogTable)\n        if (f(newTxn)) {\n          logInfo(\n            log\"Retrying commit after checking for semantic conflicts with concurrent updates.\")\n          commitAndRetry(newTxn, optimizeOperation, actions, metrics)(f)\n        } else {\n          logWarning(log\"Semantic conflicts detected. Aborting operation.\")\n          throw e\n        }\n    }\n  }\n\n  /** Create the appropriate [[Operation]] object for txn commit history */\n  private def getOperation(): Operation = {\n    if (optimizeContext.reorg.nonEmpty) {\n      DeltaOperations.Reorg(partitionPredicate)\n    } else {\n      DeltaOperations.Optimize(\n        predicate = partitionPredicate,\n        zOrderBy = zOrderByColumns,\n        auto = isAutoCompact,\n        clusterBy = if (isClusteredTable) Option(clusteringColumns).filter(_.nonEmpty) else None,\n        isFull = optimizeContext.isFull)\n    }\n  }\n\n  /** Create a map of SQL metrics for adding to the commit history. */\n  private def createMetrics(\n      sparkContext: SparkContext,\n      addedFiles: Seq[AddFile],\n      removedFiles: Seq[RemoveFile],\n      removedDVs: Seq[DeletionVectorDescriptor]): Map[String, SQLMetric] = {\n\n    def setAndReturnMetric(description: String, value: Long) = {\n      val metric = createMetric(sparkContext, description)\n      metric.set(value)\n      metric\n    }\n\n    def totalSize(actions: Seq[FileAction]): Long = {\n      var totalSize = 0L\n      actions.foreach { file =>\n        val fileSize = file match {\n          case addFile: AddFile => addFile.size\n          case removeFile: RemoveFile => removeFile.size.getOrElse(0L)\n          case default =>\n            throw new IllegalArgumentException(s\"Unknown FileAction type: ${default.getClass}\")\n        }\n        totalSize += fileSize\n      }\n      totalSize\n    }\n\n    val (deletionVectorRowsRemoved, deletionVectorBytesRemoved) =\n      removedDVs.map(dv => (dv.cardinality, dv.sizeInBytes.toLong))\n        .reduceLeftOption((dv1, dv2) => (dv1._1 + dv2._1, dv1._2 + dv2._2))\n        .getOrElse((0L, 0L))\n\n    val dvMetrics: Map[String, SQLMetric] = Map(\n      \"numDeletionVectorsRemoved\" ->\n        setAndReturnMetric(\n          \"total number of deletion vectors removed\",\n          removedDVs.size),\n      \"numDeletionVectorRowsRemoved\" ->\n        setAndReturnMetric(\n          \"total number of deletion vector rows removed\",\n          deletionVectorRowsRemoved),\n      \"numDeletionVectorBytesRemoved\" ->\n        setAndReturnMetric(\n          \"total number of bytes of removed deletion vectors\",\n          deletionVectorBytesRemoved))\n\n    val sizeStats = FileSizeStatsWithHistogram.create(addedFiles.map(_.size).sorted)\n    Map[String, SQLMetric](\n      \"minFileSize\" -> setAndReturnMetric(\"minimum file size\", sizeStats.get.min),\n      \"p25FileSize\" -> setAndReturnMetric(\"25th percentile file size\", sizeStats.get.p25),\n      \"p50FileSize\" -> setAndReturnMetric(\"50th percentile file size\", sizeStats.get.p50),\n      \"p75FileSize\" -> setAndReturnMetric(\"75th percentile file size\", sizeStats.get.p75),\n      \"maxFileSize\" -> setAndReturnMetric(\"maximum file size\", sizeStats.get.max),\n      \"numAddedFiles\" -> setAndReturnMetric(\"total number of files added.\", addedFiles.size),\n      \"numRemovedFiles\" -> setAndReturnMetric(\"total number of files removed.\", removedFiles.size),\n      \"numAddedBytes\" -> setAndReturnMetric(\"total number of bytes added\", totalSize(addedFiles)),\n      \"numRemovedBytes\" ->\n        setAndReturnMetric(\"total number of bytes removed\", totalSize(removedFiles))\n    ) ++ dvMetrics\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/OptimizeTableStrategy.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo, ClusteringStatsCollector}\nimport org.apache.spark.sql.delta.skipping.clustering.ZCube\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaErrors, OptimisticTransaction, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.commands.OptimizeTableStrategy.DummyBinInfo\nimport org.apache.spark.sql.delta.commands.optimize.{AddFileWithNumRecords, DeletionVectorStats, OptimizeStats, ZOrderFileStats, ZOrderStats}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.{DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE, DELTA_OPTIMIZE_CLUSTERING_TARGET_CUBE_SIZE}\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo\n\nimport org.apache.spark.sql.SparkSession\n\nobject OptimizeTableMode extends Enumeration {\n  type OptimizeTableMode = Value\n  val COMPACTION, ZORDER, CLUSTERING = Value\n}\n\n/**\n * Defines set of utilities used in OptimizeTableCommand. The behavior of these utilities will\n * change based on the [[OptimizeTableMode]]: COMPACTION, ZORDER and CLUSTERING.\n */\ntrait OptimizeTableStrategy {\n  def sparkSession: SparkSession\n\n  /**\n   * Utility method to get max bin size in bytes to group files into.\n   */\n  def maxBinSize: Long\n\n  /**\n   * Utility method to prepare files in a partition for optimization.\n   *\n   * By default it sorts files on the size for the binpack.\n   *\n   * @return Prepared files for the subsequent optimization.\n   */\n  def prepareFilesPerPartition(inputFiles: Seq[AddFile]): Seq[AddFile] = inputFiles.sortBy(_.size)\n\n  /** The optimize mode the strategy instance is created for. */\n  def optimizeTableMode: OptimizeTableMode.Value\n\n  /**\n   * The clustering algorithm to be used by either by ZORDER or Liquid CLUSTERING.\n   * An error is thrown for COMPACTION.\n   */\n  def curve: String\n\n  /**\n   * Prepare a new Bin and returns its initialized [[BinInfo]].\n   *\n   * This function is expected to be called once for each bin\n   * before [[tagAddFile]] is called.\n   */\n  def initNewBin: OptimizeTableStrategy.BinInfo = DummyBinInfo()\n\n  /**\n   * Incorporate essential tags for optimized files based on the [[OptimizeTableMode]].\n   */\n  def tagAddFile(file: AddFile, binInfo: OptimizeTableStrategy.BinInfo): AddFile =\n    file.copy(dataChange = false)\n\n  /**\n   * Utility to update additional metrics after optimization.\n   *\n   * @param optimizeStats The input stats to update on.\n   * @param removedFiles Removed files.\n   * @param bins Sequence of bin-packed file groups,\n   *             where each group consists of a partition value\n   *             and its associated files.\n   */\n  def updateOptimizeStats(\n      optimizeStats: OptimizeStats,\n      removedFiles: Seq[RemoveFile],\n      bins: Seq[Bin]): Unit\n}\n\nobject OptimizeTableStrategy {\n  // A trait representing the context for a Bin.\n  sealed trait BinInfo\n\n  /** Default [[BinInfo]] implementation. */\n  case class DummyBinInfo() extends BinInfo\n\n  /** [[ClusteringStrategy]]'s [[BinInfo]]. */\n  case class ZCubeBinInfo(zCubeInfo: ZCubeInfo) extends BinInfo\n\n  def apply(\n      sparkSession: SparkSession,\n      snapshot: Snapshot,\n      optimizeContext: DeltaOptimizeContext,\n      zOrderBy: Seq[String]): OptimizeTableStrategy = getMode(snapshot, zOrderBy) match {\n    case OptimizeTableMode.CLUSTERING =>\n      ClusteringStrategy(\n        sparkSession, ClusteringColumnInfo.extractLogicalNames(snapshot), optimizeContext)\n    case OptimizeTableMode.ZORDER => ZOrderStrategy(sparkSession, zOrderBy)\n    case OptimizeTableMode.COMPACTION =>\n      CompactionStrategy(sparkSession, optimizeContext)\n    case other => throw new UnsupportedOperationException(s\"Unsupported mode $other\")\n  }\n\n  private def getMode(snapshot: Snapshot, zOrderBy: Seq[String]): OptimizeTableMode.Value = {\n    val isClusteredTable = ClusteredTableUtils.isSupported(snapshot.protocol)\n    val hasClusteringColumns = ClusteringColumnInfo.extractLogicalNames(snapshot).nonEmpty\n    val isZOrderBy = zOrderBy.nonEmpty\n    if (isClusteredTable && hasClusteringColumns) {\n      assert(!isZOrderBy)\n      OptimizeTableMode.CLUSTERING\n    } else if (isZOrderBy) {\n      OptimizeTableMode.ZORDER\n    } else {\n      OptimizeTableMode.COMPACTION\n    }\n  }\n}\n\n/** Implements compaction strategy */\ncase class CompactionStrategy(\n    override val sparkSession: SparkSession,\n    optimizeContext: DeltaOptimizeContext) extends OptimizeTableStrategy {\n\n  override val optimizeTableMode: OptimizeTableMode.Value = OptimizeTableMode.COMPACTION\n\n  // In COMPACTION, all files within a bin are written into single larger file.\n  override val maxBinSize: Long = {\n    optimizeContext.maxFileSize.getOrElse(\n      sparkSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE))\n  }\n\n  override def curve: String = {\n    throw new UnsupportedOperationException(\"Compaction doesn't support clustering.\")\n  }\n\n  override def updateOptimizeStats(\n      optimizeStats: OptimizeStats,\n      removedFiles: Seq[RemoveFile],\n      bins: Seq[Bin]): Unit = {}\n}\n\n/** Implements ZOrder strategy */\ncase class ZOrderStrategy(\n    override val sparkSession: SparkSession,\n    zOrderColumns: Seq[String]) extends OptimizeTableStrategy {\n\n  assert(zOrderColumns.nonEmpty)\n\n  override val optimizeTableMode: OptimizeTableMode.Value = OptimizeTableMode.ZORDER\n\n  override val curve: String = \"zorder\"\n\n  // For ZORDER, set maxBinSize the maximal LONG value to have single BIN for each partition.\n  override val maxBinSize: Long = Long.MaxValue\n\n  override def updateOptimizeStats(\n      optimizeStats: OptimizeStats,\n      removedFiles: Seq[RemoveFile],\n      bins: Seq[Bin]): Unit = {\n    val inputFileStats =\n      ZOrderFileStats(removedFiles.size, removedFiles.map(_.size.getOrElse(0L)).sum)\n    optimizeStats.zOrderStats = Some(ZOrderStats(\n      strategyName = \"all\", // means process all files in a partition\n      inputCubeFiles = ZOrderFileStats(0, 0),\n      inputOtherFiles = inputFileStats,\n      inputNumCubes = 0,\n      mergedFiles = inputFileStats,\n      // There will one z-cube for each partition\n      numOutputCubes = optimizeStats.numPartitionsOptimized))\n  }\n}\n\n/** Implements clustering strategy for clustered tables */\ncase class ClusteringStrategy(\n    override val sparkSession: SparkSession,\n    clusteringColumns: Seq[String],\n    optimizeContext: DeltaOptimizeContext) extends OptimizeTableStrategy {\n\n  override val optimizeTableMode: OptimizeTableMode.Value = OptimizeTableMode.CLUSTERING\n\n  override val curve: String = \"hilbert\"\n\n  /**\n   * In clustering, the bin size corresponds to a ZCube size that can be adjusted through\n   * configurations.\n   */\n  override val maxBinSize: Long = {\n    Math.max(\n      sparkSession.sessionState.conf.getConf(DELTA_OPTIMIZE_CLUSTERING_TARGET_CUBE_SIZE),\n      sparkSession.sessionState.conf.getConf(DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE))\n  }\n\n  // Prepare files for binpack.\n  override def prepareFilesPerPartition(inputFiles: Seq[AddFile]): Seq[AddFile] = {\n    // Un-clustered files don't have a ZCUBE_ID, and are sorted before clustered files (None is\n    // considered the smallest element). We also don't consider partitionValues because\n    // clustered tables should always be unpartitioned.\n    applyMinZCube(inputFiles.sortBy(_.tag(AddFile.Tags.ZCUBE_ID)))\n  }\n\n  // Upon a new ZCube, allocate a [[ZCubeInfo]] with a new ZCUBE ID.\n  override def initNewBin: OptimizeTableStrategy.BinInfo = {\n    OptimizeTableStrategy.ZCubeBinInfo(ZCubeInfo(clusteringColumns))\n  }\n\n  override def tagAddFile(file: AddFile, binInfo: OptimizeTableStrategy.BinInfo): AddFile = {\n    val taggedFile = super.tagAddFile(file, binInfo)\n    val zCubeInfo = binInfo.asInstanceOf[OptimizeTableStrategy.ZCubeBinInfo].zCubeInfo\n    ZCubeInfo.setForFile(\n      taggedFile.copy(clusteringProvider = Some(ClusteredTableUtils.clusteringProvider)), zCubeInfo)\n  }\n\n  override def updateOptimizeStats(\n      optimizeStats: OptimizeStats,\n      removedFiles: Seq[RemoveFile],\n      bins: Seq[Bin]): Unit = {\n    clusteringStatsCollector.numOutputZCubes = bins.size\n    optimizeStats.clusteringStats = Option(clusteringStatsCollector.getClusteringStats)\n  }\n\n  /**\n   * Given a sequence of files sorted by ZCubeId, return candidate files for\n   * clustering. The requirements to pick candidate files are:\n   *\n   * 1. Candidate files are either un-clustered (missing clusteringProvider) or the\n   * clusteringProvider is \"liquid\" when isFull is unset.\n   * 2. Clustered files with different clustering columns are handled differently based\n   * on isFull setting: If isFull is unset, existing clustered files with different columns are\n   * skipped. If isFull is set, all files are considered.\n   * 3. Files that belong to the partial ZCubes are picked. A ZCube is considered as a partial\n   * ZCube if its size is smaller than [[DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE]].\n   * 4. If there is only single ZCUBE with all files are clustered and if all clustered files\n   * belong to that ZCube, all files are filtered out.\n   */\n  private def applyMinZCube(files: Seq[AddFile]): Seq[AddFile] = {\n    val targetSize = sparkSession.sessionState.conf.getConf(DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE)\n    // Keep all files if isFull is set, otherwise skip files with different clusteringProviders\n    // or files clustered by a different set of clustering columns.\n    val (candidateFiles, skippedClusteredFiles) = files.iterator.map { f =>\n      // Note that updateStats is moved out of Iterator.partition lambda since\n      // scala2.13 doesn't call the lambda in the order of files which violates\n      // the updateStats' requirement which requires files are ordered in the\n      // ZCUBE id (files have been ordered before calling applyMinZCube).\n      clusteringStatsCollector.inputStats.updateStats(f)\n      f\n    }.partition { file =>\n      val sameOrMissingClusteringProvider =\n        file.clusteringProvider.forall(_ == ClusteredTableUtils.clusteringProvider)\n\n      // If clustered before, remove those with different clustering columns.\n      val zCubeInfo = ZCubeInfo.getForFile(file)\n      val unmatchedClusteringColumns = zCubeInfo.exists(_.zOrderBy != clusteringColumns)\n      sameOrMissingClusteringProvider && !unmatchedClusteringColumns\n    }\n    // Skip files that belong to a ZCUBE that is larger than target ZCUBE size.\n    // Note that ZCube.filterOutLargeZCubes requires clustered files have\n    // the same clustering columns, so skippedClusteredFiles are not included.\n    val smallZCubeFiles = ZCube.filterOutLargeZCubes(\n      candidateFiles.map(AddFileWithNumRecords.createFromFile), targetSize)\n\n    if (optimizeContext.isFull && skippedClusteredFiles.nonEmpty) {\n      // Clustered files with different clustering columns have to be re-clustered.\n      val finalFiles = (smallZCubeFiles.map(_.addFile) ++ skippedClusteredFiles).toSeq\n      finalFiles.map { f =>\n        clusteringStatsCollector.outputStats.updateStats(f)\n        f\n      }\n    } else {\n      // Skip smallZCubeFiles if they all belong to a single ZCUBE.\n      ZCube.filterOutSingleZCubes(smallZCubeFiles).map { file =>\n        clusteringStatsCollector.outputStats.updateStats(file.addFile)\n        file.addFile\n      }.toSeq\n    }\n  }\n\n  /** Metrics for clustering when [[isClusteredTable]] is true. */\n  private val clusteringStatsCollector: ClusteringStatsCollector =\n    ClusteringStatsCollector(clusteringColumns, optimizeContext)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/ReorgTableForUpgradeUniformHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeletionVectorsTableFeature, DeltaConfigs, DeltaErrors, DeltaOperations, IcebergCompatBase, Snapshot}\nimport org.apache.spark.sql.delta.IcebergCompat.{getEnabledVersion, getForVersion}\nimport org.apache.spark.sql.delta.UniversalFormat.{icebergEnabled, ICEBERG_FORMAT}\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.Utils.try_element_at\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.functions.col\n\n/**\n * Helper trait for ReorgTableCommand to rewrite the table to be Iceberg compatible.\n */\ntrait ReorgTableForUpgradeUniformHelper extends DeltaLogging {\n\n  private val rewriteCheckTable: Map[Int, Set[Int]] =\n    Map(0 -> Set(2, 3), 1 -> Set(2, 3), 2 -> Set(2, 3), 3 -> Set(2, 3))\n\n  /**\n   * Check if the given pair of (old_version, new_version) should trigger a rewrite check.\n   * NOTE: Actual rewrite only happens when not all addFiles has tags with newVersion.\n   */\n  private def shallCheckRewrite(oldVersion: Int, newVersion: Int): Boolean = {\n    rewriteCheckTable.getOrElse(oldVersion, Set.empty[Int]).contains(newVersion)\n  }\n\n  /**\n   * Helper function to rewrite the table. Implemented by Reorg Table Command.\n   */\n  def optimizeByReorg(sparkSession: SparkSession): Seq[Row]\n\n  /**\n   * Enable the new IcebergCompat on the table by updating table conf.\n   */\n  private def enableIcebergCompat(\n      table: DeltaTableV2,\n      currentCompatVersion: Option[Int],\n      compatToEnable: IcebergCompatBase): Unit = {\n    var newConf: Map[String, String] = Map(\n      compatToEnable.config.key -> \"true\",\n      DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\") ++\n      currentCompatVersion.map(getForVersion(_).config.key -> \"false\") // Disable old IcebergCompat\n\n    if (compatToEnable.incompatibleTableFeatures.contains(DeletionVectorsTableFeature)) {\n      newConf += DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key -> \"false\"\n    }\n\n    val alterConfTxn = table.startTransaction()\n\n    if (alterConfTxn.protocol.minWriterVersion < 7) {\n      newConf += Protocol.MIN_WRITER_VERSION_PROP -> \"7\"\n    }\n    if (alterConfTxn.protocol.minReaderVersion < 3) {\n      newConf += Protocol.MIN_READER_VERSION_PROP -> \"3\"\n    }\n\n    val metadata = alterConfTxn.metadata\n    val newMetadata = metadata.copy(\n      description = metadata.description,\n      configuration = metadata.configuration ++ newConf)\n    alterConfTxn.updateMetadata(newMetadata)\n    alterConfTxn.commit(\n      Nil,\n      DeltaOperations.UpgradeUniformProperties(newConf)\n    )\n  }\n\n  /**\n   * Helper function to get the num of addFiles as well as\n   * num of addFiles with ICEBERG_COMPAT_VERSION tag.\n   * @param icebergCompatVersion target iceberg compat version\n   * @param snapshot current snapshot\n   * @return (NumOfAddFiles, NumOfAddFilesWithIcebergCompatTag)\n   */\n  private def getNumOfAddFiles(\n      icebergCompatVersion: Int,\n      table: DeltaTableV2,\n      snapshot: Snapshot): (Long, Long) = {\n    val numOfAddFilesWithTag = snapshot.allFiles\n      .select(\"tags\")\n      .where(try_element_at(col(\"tags\"), AddFile.Tags.ICEBERG_COMPAT_VERSION.name)\n        === icebergCompatVersion.toString)\n      .count()\n    val numOfAddFiles = snapshot.numOfFiles\n    logInfo(log\"For table ${MDC(DeltaLogKeys.TABLE_NAME, table.tableIdentifier)} \" +\n      log\"at version ${MDC(DeltaLogKeys.VERSION, snapshot.version)}, there are \" +\n      log\"${MDC(DeltaLogKeys.NUM_FILES, numOfAddFiles)} addFiles, and \" +\n      log\"${MDC(DeltaLogKeys.NUM_FILES2, numOfAddFilesWithTag)} addFiles with \" +\n      log\"ICEBERG_COMPAT_VERSION=${MDC(DeltaLogKeys.VERSION2, icebergCompatVersion.toLong)} tag.\")\n    (numOfAddFiles, numOfAddFilesWithTag)\n  }\n\n  /**\n   * Helper function to rewrite the table data files in Iceberg compatible way.\n   * This method would do following things:\n   * 1. Update the table properties to enable the target iceberg compat version and disable the\n   *    existing iceberg compat version.\n   * 2. If target iceberg compat version require rewriting and not all addFiles has\n   *    ICEBERG_COMPAT_VERSION=version tag, rewrite the table data files to be iceberg compatible\n   *    and adding tag to all addFiles.\n   * 3. If universal format not enabled, alter the table properties to enable\n   *    universalFormat = Iceberg.\n   *\n   * * There are six possible write combinations:\n   * | CurrentIcebergCompatVersion | TargetIcebergCompatVersion | Required steps|\n   * | --------------------------- | -------------------------- | ------------- |\n   * |      None                   |         1                  |   1, 3        |\n   * |      None                   |         2+                 |   1, 2, 3     |\n   * |      1                      |         1                  |   3           |\n   * |      1                      |         2+                 |   1, 2, 3     |\n   * |      2+                     |         1                  |   1, 3        |\n   * |      2+                     |         2+                 |   2, 3        |\n   */\n  private def doRewrite(\n      target: DeltaTableV2,\n      sparkSession: SparkSession,\n      targetIcebergCompatVersion: Int): Seq[Row] = {\n\n    val snapshot = target.update()\n    val currIcebergCompatVersionOpt = getEnabledVersion(snapshot.metadata)\n    if (targetIcebergCompatVersion >= 3) {\n      throw DeltaErrors.icebergCompatVersionNotSupportedException(targetIcebergCompatVersion, 2)\n    }\n    val targetIcebergCompatObject = getForVersion(targetIcebergCompatVersion)\n    val mayNeedRewrite = shallCheckRewrite(\n      currIcebergCompatVersionOpt.getOrElse(0), targetIcebergCompatVersion)\n\n    // Step 1: Update the table properties to enable the target iceberg compat version\n    val didUpdateIcebergCompatVersion =\n      if (!currIcebergCompatVersionOpt.contains(targetIcebergCompatVersion)) {\n        enableIcebergCompat(target, currIcebergCompatVersionOpt, targetIcebergCompatObject)\n        logInfo(log\"Update table ${MDC(DeltaLogKeys.TABLE_NAME, target.tableIdentifier)} \" +\n          log\"to iceberg compat version = \" +\n          log\"${MDC(DeltaLogKeys.VERSION, targetIcebergCompatVersion)} successfully.\")\n        true\n      } else {\n        false\n      }\n\n    // Step 2: Rewrite the table data files to be Iceberg compatible.\n    val (numOfAddFilesBefore, numOfAddFilesWithTagBefore) = getNumOfAddFiles(\n      targetIcebergCompatVersion, target, snapshot)\n    val allAddFilesHaveTag = numOfAddFilesWithTagBefore == numOfAddFilesBefore\n    // The table needs to be rewritten if:\n    //   1. The target iceberg compat version requires rewrite.\n    //   2. Not all addFile have ICEBERG_COMPAT_VERSION=targetVersion tag\n    val (metricsOpt, didRewrite) = if (mayNeedRewrite && !allAddFilesHaveTag) {\n      logInfo(log\"Reorg Table ${MDC(DeltaLogKeys.TABLE_NAME, target.tableIdentifier)} to \" +\n        log\"iceberg compat version = ${MDC(DeltaLogKeys.VERSION, targetIcebergCompatVersion)} \" +\n        log\"need rewrite data files.\")\n      val metrics = try {\n        optimizeByReorg(sparkSession)\n      } catch {\n        case NonFatal(e) =>\n          throw DeltaErrors.icebergCompatDataFileRewriteFailedException(\n            targetIcebergCompatVersion, e)\n      }\n      logInfo(log\"Rewrite table ${MDC(DeltaLogKeys.TABLE_NAME, target.tableIdentifier)} \" +\n        log\"to iceberg compat version = ${MDC(DeltaLogKeys.VERSION,\n          targetIcebergCompatVersion)} successfully.\")\n      (Some(metrics), true)\n    } else {\n      (None, false)\n    }\n    val updatedSnapshot = target.update()\n    val (numOfAddFiles, numOfAddFilesWithIcebergCompatTag) = getNumOfAddFiles(\n      targetIcebergCompatVersion, target, updatedSnapshot)\n    if (mayNeedRewrite && numOfAddFilesWithIcebergCompatTag != numOfAddFiles) {\n      throw DeltaErrors.icebergCompatReorgAddFileTagsMissingException(\n        updatedSnapshot.version,\n        targetIcebergCompatVersion,\n        numOfAddFiles,\n        numOfAddFilesWithIcebergCompatTag\n      )\n    }\n\n    // Step 3: Update the table properties to enable the universalFormat = Iceberg.\n    if (!icebergEnabled(updatedSnapshot.metadata)) {\n      val enableUniformConf = Map(\n        DeltaConfigs.UNIVERSAL_FORMAT_ENABLED_FORMATS.key -> ICEBERG_FORMAT)\n      AlterTableSetPropertiesDeltaCommand(target, enableUniformConf).run(sparkSession)\n      logInfo(log\"Enabling universal format with iceberg compat version = \" +\n        log\"${MDC(DeltaLogKeys.VERSION, targetIcebergCompatVersion)} for table \" +\n        log\"${MDC(DeltaLogKeys.TABLE_NAME, target.tableIdentifier)} succeeded.\")\n    }\n\n    recordDeltaEvent(updatedSnapshot.deltaLog, \"delta.upgradeUniform.success\", data = Map(\n      \"currIcebergCompatVersion\" -> currIcebergCompatVersionOpt.toString,\n      \"targetIcebergCompatVersion\" -> targetIcebergCompatVersion.toString,\n      \"metrics\" -> metricsOpt.toString,\n      \"didUpdateIcebergCompatVersion\" -> didUpdateIcebergCompatVersion.toString,\n      \"needRewrite\" -> mayNeedRewrite.toString,\n      \"didRewrite\" -> didRewrite.toString,\n      \"numOfAddFilesBefore\" -> numOfAddFilesBefore.toString,\n      \"numOfAddFilesWithIcebergCompatTagBefore\" -> numOfAddFilesWithTagBefore.toString,\n      \"numOfAddFilesAfter\" -> numOfAddFiles.toString,\n      \"numOfAddFilesWithIcebergCompatTagAfter\" -> numOfAddFilesWithIcebergCompatTag.toString,\n      \"universalFormatIcebergEnabled\" -> icebergEnabled(target.update().metadata).toString\n    ))\n    metricsOpt.getOrElse(Seq.empty[Row])\n  }\n\n  /**\n   * Helper function to upgrade the table to uniform iceberg compat version.\n   */\n  protected def upgradeUniformIcebergCompatVersion(\n      target: DeltaTableV2,\n      sparkSession: SparkSession,\n      targetIcebergCompatVersion: Int): Seq[Row] = {\n    try {\n      doRewrite(target, sparkSession, targetIcebergCompatVersion)\n    } catch {\n      case NonFatal(e) =>\n        recordDeltaEvent(target.deltaLog, \"delta.upgradeUniform.exception\", data = Map(\n          \"targetIcebergCompatVersion\" -> targetIcebergCompatVersion.toString,\n          \"exception\" -> e.toString\n        ))\n        throw e\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/ReorgTableHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.{MaterializedRowCommitVersion, MaterializedRowId, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol}\nimport org.apache.spark.sql.delta.commands.VacuumCommand.generateCandidateFileMap\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.datasources.parquet.{ParquetFileFormat, ParquetToSparkSchemaConverter}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{AtomicType, StructField, StructType}\nimport org.apache.spark.util.SerializableConfiguration\n\ntrait ReorgTableHelper extends Serializable {\n  /**\n   * Determine whether `fileSchema` has any column that has a type that differs from\n   * `tablePhysicalSchema`.\n   *\n   * @param fileSchema the current parquet schema to be checked.\n   * @param tablePhysicalSchema the current table schema.\n   * @return whether the file has any column that has a different type from table column.\n   */\n  protected def fileHasDifferentTypes(\n      fileSchema: StructType,\n      tablePhysicalSchema: StructType): Boolean = {\n    SchemaMergingUtils.transformColumns(fileSchema, tablePhysicalSchema) {\n      case (_, StructField(_, fileType: AtomicType, _, _),\n        Some(StructField(_, tableType: AtomicType, _, _)), _) if fileType != tableType =>\n        return true\n      case (_, field, _, _) => field\n    }\n    false\n  }\n\n  /**\n   * Determine whether `fileSchema` has any column that does not exist in the\n   * `tablePhysicalSchema`, this is possible by running ALTER TABLE commands,\n   * e.g., ALTER TABLE DROP COLUMN.\n   *\n   * @param fileSchema the current parquet schema to be checked.\n   * @param tablePhysicalSchema the current table schema.\n   * @param protocol the protocol used to check `row_id` and `row_commit_version`.\n   * @param metadata the metadata used to check `row_id` and `row_commit_version`.\n   * @return whether the file has any dropped column.\n   */\n  protected def fileHasExtraColumns(\n      fileSchema: StructType,\n      tablePhysicalSchema: StructType,\n      protocol: Protocol,\n      metadata: Metadata): Boolean = {\n    // 0. get the materialized names for `row_id` and `row_commit_version`.\n    val materializedRowIdColumnNameOpt =\n      MaterializedRowId.getMaterializedColumnName(protocol, metadata)\n    val materializedRowCommitVersionColumnNameOpt =\n      MaterializedRowCommitVersion.getMaterializedColumnName(protocol, metadata)\n\n    SchemaMergingUtils.transformColumns(fileSchema) { (path, field, _) =>\n      // 1. check whether the field exists in the `tablePhysicalSchema`.\n      val fullName = path :+ field.name\n      val inTableFieldOpt = SchemaUtils.findNestedFieldIgnoreCase(\n        tablePhysicalSchema, fullName, includeCollections = true)\n\n      // 2. check whether the current `field` is `row_id` or `row_commit_version`\n      //    column; if so, we need to explicitly keep these columns since they are\n      //    not part of the table schema but exist in the parquet file.\n      val isRowIdOrRowCommitVersion = materializedRowIdColumnNameOpt.contains(field.name) ||\n        materializedRowCommitVersionColumnNameOpt.contains(field.name)\n\n      if (inTableFieldOpt.isEmpty && !isRowIdOrRowCommitVersion) {\n        return true\n      }\n      field\n    }\n    false\n  }\n\n  /**\n   * Apply a filter on the list of AddFile to only keep the files that have physical parquet schema\n   * that satisfies the given filter function.\n   *\n   * Note: Filtering happens on the executors: **any variable captured by `filterFileFn` must be\n   * Serializable**\n   */\n  protected def filterParquetFilesOnExecutors(\n      spark: SparkSession,\n      files: Seq[AddFile],\n      snapshot: Snapshot,\n      ignoreCorruptFiles: Boolean)(\n      filterFileFn: StructType => Boolean): Seq[AddFile] = {\n\n    val serializedConf = new SerializableConfiguration(snapshot.deltaLog.newDeltaHadoopConf())\n    val dataPath = new Path(snapshot.deltaLog.dataPath.toString)\n\n    import org.apache.spark.sql.delta.implicits._\n\n    files.toDF(spark).as[AddFile].mapPartitions { iter =>\n      val sqlConf = SparkSession.active.sessionState.conf\n      filterParquetFiles(\n        sqlConf,\n        iter.toList,\n        dataPath,\n        serializedConf.value,\n        ignoreCorruptFiles)(filterFileFn).toIterator\n    }.collect()\n  }\n\n  protected def filterParquetFiles(\n      sqlConf: SQLConf,\n      files: Seq[AddFile],\n      dataPath: Path,\n      configuration: Configuration,\n      ignoreCorruptFiles: Boolean)(\n      filterFileFn: StructType => Boolean): Seq[AddFile] = {\n    val nameToAddFileMap = generateCandidateFileMap(dataPath, files)\n\n    val fileStatuses = nameToAddFileMap.map { case (absPath, addFile) =>\n      new FileStatus(\n        /* length */ addFile.size,\n        /* isDir */ false,\n        /* blockReplication */ 0,\n        /* blockSize */ 1,\n        /* modificationTime */ addFile.modificationTime,\n        new Path(absPath)\n      )\n    }\n\n    val footers = DeltaFileOperations.readParquetFootersInParallel(\n      configuration,\n      fileStatuses.toList,\n      ignoreCorruptFiles)\n\n    // Spark 4.0.1 changed the primary ctor signature (added a param), which breaks binary\n    // compatibility for code compiled against Spark 4.0.0. Use the stable SQLConf-based ctor\n    // that takes the current SparkSession's SQLConf instead.\n    val converter = new ParquetToSparkSchemaConverter(sqlConf)\n\n    val filesNeedToRewrite = footers.filter { footer =>\n      val fileSchema = ParquetFileFormat.readSchemaFromFooter(footer, converter)\n      filterFileFn(fileSchema)\n    }.map(_.getFile.toString)\n    filesNeedToRewrite.map(absPath => nameToAddFileMap(absPath))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/RestoreTableCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport java.sql.Timestamp\n\nimport scala.collection.JavaConverters._\nimport scala.util.{Success, Try}\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, DeltaOperations, DomainMetadataUtils, IdentityColumn, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.DeltaFileOperations.absolutePath\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{Column, DataFrame, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, Literal}\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.functions.{column, lit}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.IGNORE_MISSING_FILES\nimport org.apache.spark.sql.types.LongType\nimport org.apache.spark.unsafe.types.UTF8String\nimport org.apache.spark.util.SerializableConfiguration\n\n/** Base trait class for RESTORE. Defines command output schema and metrics. */\ntrait RestoreTableCommandBase {\n\n  // RESTORE operation metrics\n  val TABLE_SIZE_AFTER_RESTORE = \"tableSizeAfterRestore\"\n  val NUM_OF_FILES_AFTER_RESTORE = \"numOfFilesAfterRestore\"\n  val NUM_REMOVED_FILES = \"numRemovedFiles\"\n  val NUM_RESTORED_FILES = \"numRestoredFiles\"\n  val REMOVED_FILES_SIZE = \"removedFilesSize\"\n  val RESTORED_FILES_SIZE = \"restoredFilesSize\"\n\n  // SQL way column names for RESTORE command output\n  private val COLUMN_TABLE_SIZE_AFTER_RESTORE = \"table_size_after_restore\"\n  private val COLUMN_NUM_OF_FILES_AFTER_RESTORE = \"num_of_files_after_restore\"\n  private val COLUMN_NUM_REMOVED_FILES = \"num_removed_files\"\n  private val COLUMN_NUM_RESTORED_FILES = \"num_restored_files\"\n  private val COLUMN_REMOVED_FILES_SIZE = \"removed_files_size\"\n  private val COLUMN_RESTORED_FILES_SIZE = \"restored_files_size\"\n\n  val outputSchema: Seq[Attribute] = Seq(\n    AttributeReference(COLUMN_TABLE_SIZE_AFTER_RESTORE, LongType)(),\n    AttributeReference(COLUMN_NUM_OF_FILES_AFTER_RESTORE, LongType)(),\n    AttributeReference(COLUMN_NUM_REMOVED_FILES, LongType)(),\n    AttributeReference(COLUMN_NUM_RESTORED_FILES, LongType)(),\n    AttributeReference(COLUMN_REMOVED_FILES_SIZE, LongType)(),\n    AttributeReference(COLUMN_RESTORED_FILES_SIZE, LongType)()\n  )\n}\n\n/**\n * Perform restore of delta table to a specified version or timestamp\n *\n * Algorithm:\n * 1) Read the latest snapshot of the table.\n * 2) Read snapshot for version or timestamp to restore\n * 3) Compute files available in snapshot for restoring (files were removed by some commit)\n * but missed in the latest. Add these files into commit as AddFile action.\n * 4) Compute files available in the latest snapshot (files were added after version to restore)\n * but missed in the snapshot to restore. Add these files into commit as RemoveFile action.\n * 5) If SQLConf.IGNORE_MISSING_FILES option is false (default value) check availability of AddFile\n * in file system.\n * 6) Commit metadata, Protocol, all RemoveFile and AddFile actions\n * into delta log using `commitLarge` (commit will be failed in case of parallel transaction)\n * 7) If table was modified in parallel then ignore restore and raise exception.\n *\n */\ncase class RestoreTableCommand(sourceTable: DeltaTableV2)\n  extends LeafRunnableCommand with DeltaCommand with RestoreTableCommandBase {\n\n  override val output: Seq[Attribute] = outputSchema\n\n  override def run(spark: SparkSession): Seq[Row] = {\n    val deltaLog = sourceTable.deltaLog\n    val catalogTableOpt = sourceTable.catalogTable\n    val version = sourceTable.timeTravelOpt.get.version\n    val timestamp = getTimestamp()\n    recordDeltaOperation(deltaLog, \"delta.restore\") {\n      require(version.isEmpty ^ timestamp.isEmpty,\n        \"Either the version or timestamp should be provided for restore\")\n\n      val versionToRestore = version.getOrElse {\n        deltaLog\n          .history\n          .getActiveCommitAtTime(\n            parseStringToTs(timestamp), catalogTableOpt, canReturnLastCommit = true)\n          .version\n      }\n\n      val latestVersion = deltaLog\n        .update(catalogTableOpt = catalogTableOpt)\n        .version\n\n      require(versionToRestore < latestVersion, s\"Version to restore ($versionToRestore)\" +\n        s\"should be less then last available version ($latestVersion)\")\n\n      deltaLog.withNewTransaction(catalogTableOpt) { txn =>\n        val latestSnapshot = txn.snapshot\n        val snapshotToRestore = deltaLog.getSnapshotAt(\n          versionToRestore,\n          catalogTableOpt = txn.catalogTable, enforceTimeTravelWithinDeletedFileRetention = true)\n        val latestSnapshotFiles = latestSnapshot.allFiles\n        val snapshotToRestoreFiles = snapshotToRestore.allFiles\n\n        import org.apache.spark.sql.delta.implicits._\n\n        // If either source version or destination version contains DVs,\n        // we have to take them into account during deduplication.\n        val targetMayHaveDVs = DeletionVectorUtils.deletionVectorsReadable(latestSnapshot)\n        val sourceMayHaveDVs = DeletionVectorUtils.deletionVectorsReadable(snapshotToRestore)\n\n        val normalizedSourceWithoutDVs = snapshotToRestoreFiles.mapPartitions { files =>\n          files.map(file => (file, file.path))\n        }.toDF(\"srcAddFile\", \"srcPath\")\n        val normalizedTargetWithoutDVs = latestSnapshotFiles.mapPartitions { files =>\n          files.map(file => (file, file.path))\n        }.toDF(\"tgtAddFile\", \"tgtPath\")\n\n        def addDVsToNormalizedDF(\n          mayHaveDVs: Boolean,\n          dvIdColumnName: String,\n          dvAccessColumn: Column,\n          normalizedDf: DataFrame): DataFrame = {\n          if (mayHaveDVs) {\n            normalizedDf.withColumn(\n              dvIdColumnName,\n              DeletionVectorDescriptor.uniqueIdExpression(dvAccessColumn))\n          } else {\n            normalizedDf.withColumn(dvIdColumnName, lit(null))\n          }\n        }\n\n        val normalizedSource = addDVsToNormalizedDF(\n          mayHaveDVs = sourceMayHaveDVs,\n          dvIdColumnName = \"srcDeletionVectorId\",\n          dvAccessColumn = column(\"srcAddFile.deletionVector\"),\n          normalizedDf = normalizedSourceWithoutDVs)\n\n        val normalizedTarget = addDVsToNormalizedDF(\n          mayHaveDVs = targetMayHaveDVs,\n          dvIdColumnName = \"tgtDeletionVectorId\",\n          dvAccessColumn = column(\"tgtAddFile.deletionVector\"),\n          normalizedDf = normalizedTargetWithoutDVs)\n\n        val joinExprs =\n          column(\"srcPath\") === column(\"tgtPath\") and\n            // Use comparison operator where NULL == NULL\n            column(\"srcDeletionVectorId\") <=> column(\"tgtDeletionVectorId\")\n\n        val filesToAdd = normalizedSource\n          .join(normalizedTarget, joinExprs, \"left_anti\")\n          .select(column(\"srcAddFile\").as[AddFile])\n          .map(_.copy(dataChange = true))\n\n        val filesToRemove = normalizedTarget\n          .join(normalizedSource, joinExprs, \"left_anti\")\n          .select(column(\"tgtAddFile\").as[AddFile])\n          .map(_.removeWithTimestamp())\n\n        val ignoreMissingFiles = spark\n          .sessionState\n          .conf\n          .getConf(IGNORE_MISSING_FILES)\n\n        if (!ignoreMissingFiles) {\n          checkSnapshotFilesAvailability(deltaLog, filesToAdd, versionToRestore)\n        }\n\n          // Commit files, metrics, protocol and metadata to delta log\n        val metrics = withDescription(\"metrics\") {\n          computeMetrics(filesToAdd, filesToRemove, snapshotToRestore)\n        }\n        val addActions = withDescription(\"add actions\") {\n          filesToAdd.toLocalIterator().asScala\n        }\n        val removeActions = withDescription(\"remove actions\") {\n          filesToRemove.toLocalIterator().asScala\n        }\n\n        // We need to merge the schema of the latest snapshot with the schema of the snapshot\n        // we're restoring to ensure that the high water mark is correct.\n        val mergedSchema = IdentityColumn.copySchemaWithMergedHighWaterMarks(\n          deltaLog = deltaLog,\n          schemaToCopy = snapshotToRestore.metadata.schema,\n          schemaWithHighWaterMarksToMerge = latestSnapshot.metadata.schema\n        )\n\n        txn.updateMetadata(snapshotToRestore.metadata.copy(schemaString = mergedSchema.json))\n\n        val sourceProtocol = snapshotToRestore.protocol\n        val targetProtocol = latestSnapshot.protocol\n        // Only upgrade the protocol, never downgrade (unless allowed by flag), since that may break\n        // time travel.\n        val protocolDowngradeAllowed =\n          conf.getConf(DeltaSQLConf.RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED)\n        val newProtocol = if (protocolDowngradeAllowed) {\n          sourceProtocol\n        } else {\n          sourceProtocol.merge(targetProtocol)\n        }\n\n        val actions = addActions ++ removeActions ++\n          DomainMetadataUtils.handleDomainMetadataForRestoreTable(snapshotToRestore, latestSnapshot)\n\n        txn.commitLarge(\n          spark,\n          actions,\n          Some(newProtocol),\n          DeltaOperations.Restore(version, timestamp),\n          Map.empty,\n          metrics.mapValues(_.toString).toMap)\n\n        Seq(Row(\n          metrics.get(TABLE_SIZE_AFTER_RESTORE),\n          metrics.get(NUM_OF_FILES_AFTER_RESTORE),\n          metrics.get(NUM_REMOVED_FILES),\n          metrics.get(NUM_RESTORED_FILES),\n          metrics.get(REMOVED_FILES_SIZE),\n          metrics.get(RESTORED_FILES_SIZE)))\n      }\n    }\n  }\n\n  private def withDescription[T](action: String)(f: => T): T =\n    withStatusCode(\"DELTA\",\n      s\"RestoreTableCommand: compute $action  (table path ${sourceTable.deltaLog.dataPath})\") {\n    f\n  }\n\n  private def parseStringToTs(timestamp: Option[String]): Timestamp = {\n    Try {\n      timestamp.flatMap { tsStr =>\n        val tz = DateTimeUtils.getZoneId(SQLConf.get.sessionLocalTimeZone)\n        val utfStr = UTF8String.fromString(tsStr)\n        DateTimeUtils.stringToTimestamp(utfStr, tz)\n      }\n    } match {\n      case Success(Some(tsMicroseconds)) => new Timestamp(tsMicroseconds / 1000)\n      case _ => throw DeltaErrors.timestampInvalid(Literal(timestamp.get))\n    }\n  }\n\n  private def computeMetrics(\n    toAdd: Dataset[AddFile],\n    toRemove: Dataset[RemoveFile],\n    snapshot: Snapshot\n  ): Map[String, Long] = {\n    // scalastyle:off sparkimplicits\n    import toAdd.sparkSession.implicits._\n    // scalastyle:on sparkimplicits\n\n    val (numRestoredFiles, restoredFilesSize) = toAdd\n      .agg(\"size\" -> \"count\", \"size\" -> \"sum\").as[(Long, Option[Long])].head()\n\n    val (numRemovedFiles, removedFilesSize) = toRemove\n      .agg(\"size\" -> \"count\", \"size\" -> \"sum\").as[(Long, Option[Long])].head()\n\n    Map(\n      NUM_RESTORED_FILES -> numRestoredFiles,\n      RESTORED_FILES_SIZE -> restoredFilesSize.getOrElse(0),\n      NUM_REMOVED_FILES -> numRemovedFiles,\n      REMOVED_FILES_SIZE -> removedFilesSize.getOrElse(0),\n      NUM_OF_FILES_AFTER_RESTORE -> snapshot.numOfFiles,\n      TABLE_SIZE_AFTER_RESTORE -> snapshot.sizeInBytes\n    )\n  }\n\n  /* Prevent users from running restore to table version with missed\n   * data files (manually deleted or vacuumed). Restoring to this version partially\n   * is still possible if spark.sql.files.ignoreMissingFiles is set to true\n   */\n  private def checkSnapshotFilesAvailability(\n    deltaLog: DeltaLog,\n    files: Dataset[AddFile],\n    version: Long): Unit = withDescription(\"missing files validation\") {\n\n    val spark: SparkSession = files.sparkSession\n\n    val pathString = deltaLog.dataPath.toString\n    val hadoopConf = spark.sparkContext.broadcast(\n      new SerializableConfiguration(deltaLog.newDeltaHadoopConf()))\n\n    import org.apache.spark.sql.delta.implicits._\n\n    val missedFiles = files\n      .mapPartitions { files =>\n        val path = new Path(pathString)\n        val fs = path.getFileSystem(hadoopConf.value.value)\n        files.filterNot(f => fs.exists(absolutePath(pathString, f.path)))\n      }\n      .map(_.path)\n      .head(100)\n\n    if (missedFiles.nonEmpty) {\n      throw DeltaErrors.restoreMissedDataFilesError(missedFiles, version)\n    }\n  }\n\n  /** If available get the timestamp referring to a snapshot in the source table timeline */\n  private def getTimestamp(): Option[String] = {\n    if (sourceTable.timeTravelOpt.get.timestamp.isDefined) {\n      Some(sourceTable.timeTravelOpt.get.getTimestamp(conf).toString)\n    } else {\n      None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/ShowDeltaTableColumnsCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.execution.command.RunnableCommand\n\n/**\n * The column format of the result returned by the `SHOW COLUMNS` command.\n */\ncase class TableColumns(col_name: String)\n\n/**\n * A command for listing all column names of a Delta table.\n *\n * @param child The resolved Delta table\n */\ncase class ShowDeltaTableColumnsCommand(child: LogicalPlan)\n  extends RunnableCommand with UnaryNode with DeltaCommand {\n\n  override val output: Seq[Attribute] = toAttributes(ExpressionEncoder[TableColumns]().schema)\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): ShowDeltaTableColumnsCommand =\n    copy(child = newChild)\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    // Return the schema from snapshot if it is an Delta table. Or raise\n    // `DeltaErrors.notADeltaTableException` if it is a non-Delta table.\n    val deltaTable = getDeltaTable(child, \"SHOW COLUMNS\")\n    recordDeltaOperation(deltaTable.deltaLog, \"delta.ddl.showColumns\") {\n      deltaTable.update().schema.fieldNames.map { x => Row(x) }.toSeq\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/UpdateCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.concurrent.TimeUnit\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.metric.IncrementMetric\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile, FileAction}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader.{CDC_TYPE_COLUMN_NAME, CDC_TYPE_NOT_CDC, CDC_TYPE_UPDATE_POSTIMAGE, CDC_TYPE_UPDATE_PREIMAGE}\nimport org.apache.spark.sql.delta.files.{TahoeBatchFileIndex, TahoeFileIndex}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatsCollectionUtils\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.sql.{Column, DataFrame, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, Expression, If, Literal}\nimport org.apache.spark.sql.catalyst.expressions.Literal.TrueLiteral\nimport org.apache.spark.sql.catalyst.plans.QueryPlan\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.execution.metric.SQLMetrics.{createMetric, createTimingMetric}\nimport org.apache.spark.sql.functions.{array, col, explode, input_file_name, lit, struct}\nimport org.apache.spark.sql.types.LongType\n\n/**\n * Performs an Update using `updateExpression` on the rows that match `condition`\n *\n * Algorithm:\n *   1) Identify the affected files, i.e., the files that may have the rows to be updated.\n *   2) Scan affected files, apply the updates, and generate a new DF with updated rows.\n *   3) Use the Delta protocol to atomically write the new DF as new files and remove\n *      the affected files that are identified in step 1.\n */\ncase class UpdateCommand(\n    tahoeFileIndex: TahoeFileIndex,\n    catalogTable: Option[CatalogTable],\n    target: LogicalPlan,\n    updateExpressions: Seq[Expression],\n    condition: Option[Expression])\n  extends LeafRunnableCommand with DeltaCommand {\n\n  override val output: Seq[Attribute] = {\n    Seq(AttributeReference(\"num_affected_rows\", LongType)())\n  }\n\n  override def innerChildren: Seq[QueryPlan[_]] = Seq(target)\n\n  @transient private lazy val sc: SparkContext = SparkContext.getOrCreate()\n\n  override lazy val metrics = Map[String, SQLMetric](\n    \"numAddedFiles\" -> createMetric(sc, \"number of files added.\"),\n    \"numAddedBytes\" -> createMetric(sc, \"number of bytes added\"),\n    \"numRemovedFiles\" -> createMetric(sc, \"number of files removed.\"),\n    \"numRemovedBytes\" -> createMetric(sc, \"number of bytes removed\"),\n    \"numUpdatedRows\" -> createMetric(sc, \"number of rows updated.\"),\n    \"numCopiedRows\" -> createMetric(sc, \"number of rows copied.\"),\n    \"executionTimeMs\" ->\n      createTimingMetric(sc, \"time taken to execute the entire operation\"),\n    \"scanTimeMs\" ->\n      createTimingMetric(sc, \"time taken to scan the files for matches\"),\n    \"rewriteTimeMs\" ->\n      createTimingMetric(sc, \"time taken to rewrite the matched files\"),\n    \"numAddedChangeFiles\" -> createMetric(sc, \"number of change data capture files generated\"),\n    \"changeFileBytes\" -> createMetric(sc, \"total size of change data capture files generated\"),\n    \"numTouchedRows\" -> createMetric(sc, \"number of rows touched (copied + updated)\"),\n    \"numDeletionVectorsAdded\" -> createMetric(sc, \"number of deletion vectors added\"),\n    \"numDeletionVectorsRemoved\" -> createMetric(sc, \"number of deletion vectors removed\"),\n    \"numDeletionVectorsUpdated\" -> createMetric(sc, \"number of deletion vectors updated\")\n  )\n\n  final override def run(sparkSession: SparkSession): Seq[Row] = {\n    recordDeltaOperation(tahoeFileIndex.deltaLog, \"delta.dml.update\") {\n      val deltaLog = tahoeFileIndex.deltaLog\n      deltaLog.withNewTransaction(catalogTable) { txn =>\n        DeltaLog.assertRemovable(txn.snapshot)\n        if (hasBeenExecuted(txn, sparkSession)) {\n          sendDriverMetrics(sparkSession, metrics)\n          return Seq.empty\n        }\n        performUpdate(sparkSession, deltaLog, txn)\n      }\n      // Re-cache all cached plans(including this relation itself, if it's cached) that refer to\n      // this data source relation.\n      sparkSession.sharedState.cacheManager.recacheByPlan(sparkSession, target)\n    }\n    Seq(Row(metrics(\"numUpdatedRows\").value))\n  }\n\n  private def performUpdate(\n      sparkSession: SparkSession, deltaLog: DeltaLog, txn: OptimisticTransaction): Unit = {\n    import org.apache.spark.sql.delta.implicits._\n\n    var numTouchedFiles: Long = 0\n    var numRewrittenFiles: Long = 0\n    var numAddedBytes: Long = 0\n    var numRemovedBytes: Long = 0\n    var numAddedChangeFiles: Long = 0\n    var changeFileBytes: Long = 0\n    var scanTimeMs: Long = 0\n    var rewriteTimeMs: Long = 0\n    var numDeletionVectorsAdded: Long = 0\n    var numDeletionVectorsRemoved: Long = 0\n    var numDeletionVectorsUpdated: Long = 0\n\n    val startTime = System.nanoTime()\n    val numFilesTotal = txn.snapshot.numOfFiles\n\n    val updateCondition = condition.getOrElse(Literal.TrueLiteral)\n    val (metadataPredicates, dataPredicates) =\n      DeltaTableUtils.splitMetadataAndDataPredicates(\n        updateCondition, txn.metadata.partitionColumns, sparkSession)\n\n    // Should we write the DVs to represent updated rows?\n    val shouldWriteDeletionVectors = shouldWritePersistentDeletionVectors(sparkSession, txn)\n    val candidateFiles = txn.filterFiles(\n      metadataPredicates ++ dataPredicates,\n      keepNumRecords = shouldWriteDeletionVectors)\n\n    val nameToAddFile = generateCandidateFileMap(deltaLog.dataPath, candidateFiles)\n\n    scanTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)\n\n    val filesToRewrite: Seq[TouchedFileWithDV] = if (candidateFiles.isEmpty) {\n      // Case 1: Do nothing if no row qualifies the partition predicates\n      // that are part of Update condition\n      Nil\n    } else if (dataPredicates.isEmpty) {\n      // Case 2: Update all the rows from the files that are in the specified partitions\n      // when the data filter is empty\n      candidateFiles\n        .map(f => TouchedFileWithDV(f.path, f, newDeletionVector = null, deletedRows = 0L))\n    } else {\n      // Case 3: Find all the affected files using the user-specified condition\n      val fileIndex = new TahoeBatchFileIndex(\n        sparkSession, \"update\", candidateFiles, deltaLog, tahoeFileIndex.path, txn.snapshot)\n\n      val touchedFilesWithDV = if (shouldWriteDeletionVectors) {\n        // Case 3.1: Find all the affected files via DV path\n        val targetDf = DMLWithDeletionVectorsHelper.createTargetDfForScanningForMatches(\n          sparkSession,\n          target,\n          fileIndex)\n\n        // Does the target table already has DVs enabled? If so, we need to read the table\n        // with deletion vectors.\n        val mustReadDeletionVectors = DeletionVectorUtils.deletionVectorsReadable(txn.snapshot)\n\n        DMLWithDeletionVectorsHelper.findTouchedFiles(\n          sparkSession,\n          txn,\n          mustReadDeletionVectors,\n          deltaLog,\n          targetDf,\n          fileIndex,\n          updateCondition,\n          opName = \"UPDATE\")\n      } else {\n        // Case 3.2: Find all the affected files using the non-DV path\n        // Keep everything from the resolved target except a new TahoeFileIndex\n        // that only involves the affected files instead of all files.\n        val newTarget = DeltaTableUtils.replaceFileIndex(target, fileIndex)\n        val data = DataFrameUtils.ofRows(sparkSession, newTarget)\n        val incrUpdatedCountExpr = IncrementMetric(TrueLiteral, metrics(\"numUpdatedRows\"))\n        val pathsToRewrite =\n          withStatusCode(\"DELTA\", UpdateCommand.FINDING_TOUCHED_FILES_MSG) {\n            data.filter(Column(updateCondition))\n              .select(input_file_name())\n              .filter(Column(incrUpdatedCountExpr))\n              .distinct()\n              .as[String]\n              .collect()\n          }\n\n        // Wrap AddFile into TouchedFileWithDV that has empty DV.\n        pathsToRewrite\n          .map(getTouchedFile(deltaLog.dataPath, _, nameToAddFile))\n          .map(f => TouchedFileWithDV(f.path, f, newDeletionVector = null, deletedRows = 0L))\n          .toSeq\n      }\n      // Refresh scan time for Case 3, since we performed scan here.\n      scanTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)\n      touchedFilesWithDV\n    }\n\n    val totalActions = {\n      // When DV is on, we first mask removed rows with DVs and generate (remove, add) pairs.\n      val actionsForExistingFiles = if (shouldWriteDeletionVectors) {\n        // When there's no data predicate, all matched files are removed.\n        if (dataPredicates.isEmpty) {\n          val operationTimestamp = System.currentTimeMillis()\n          filesToRewrite.map(_.fileLogEntry.removeWithTimestamp(operationTimestamp))\n        } else {\n          // When there is data predicate, we generate (remove, add) pairs.\n          val filesToRewriteWithDV = filesToRewrite.filter(_.newDeletionVector != null)\n          val stringTruncateLength = StatsCollectionUtils.getDataSkippingStringPrefixLength(\n            sparkSession, txn.metadata)\n          val (dvActions, metricMap) = DMLWithDeletionVectorsHelper.processUnmodifiedData(\n            sparkSession,\n            filesToRewriteWithDV,\n            txn.snapshot,\n            stringTruncateLength)\n          metrics(\"numUpdatedRows\").set(metricMap(\"numModifiedRows\"))\n          numDeletionVectorsAdded = metricMap(\"numDeletionVectorsAdded\")\n          numDeletionVectorsRemoved = metricMap(\"numDeletionVectorsRemoved\")\n          numDeletionVectorsUpdated = metricMap(\"numDeletionVectorsUpdated\")\n          numTouchedFiles = metricMap(\"numRemovedFiles\")\n          dvActions\n        }\n      } else {\n        // Without DV we'll leave the job to `rewriteFiles`.\n        Nil\n      }\n\n      // When DV is on, we write out updated rows only. The return value will be only `add` actions.\n      // When DV is off, we write out updated rows plus unmodified rows from the same file, then\n      // return `add` and `remove` actions.\n      val rewriteStartNs = System.nanoTime()\n      val actionsForNewFiles =\n        withStatusCode(\"DELTA\", UpdateCommand.rewritingFilesMsg(filesToRewrite.size)) {\n          if (filesToRewrite.nonEmpty) {\n            rewriteFiles(\n              sparkSession,\n              txn,\n              rootPath = tahoeFileIndex.path,\n              inputLeafFiles = filesToRewrite.map(_.fileLogEntry),\n              nameToAddFileMap = nameToAddFile,\n              condition = updateCondition,\n              generateRemoveFileActions = !shouldWriteDeletionVectors,\n              copyUnmodifiedRows = !shouldWriteDeletionVectors)\n          } else {\n            Nil\n          }\n        }\n      rewriteTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - rewriteStartNs)\n\n      numTouchedFiles = filesToRewrite.length\n      val (addActions, removeActions) = actionsForNewFiles.partition(_.isInstanceOf[AddFile])\n      numRewrittenFiles = addActions.size\n      numAddedBytes = addActions.map(_.getFileSize).sum\n      numRemovedBytes = removeActions.map(_.getFileSize).sum\n\n      actionsForExistingFiles ++ actionsForNewFiles\n    }\n\n    val changeActions = totalActions.collect { case f: AddCDCFile => f }\n    numAddedChangeFiles = changeActions.size\n    changeFileBytes = changeActions.map(_.size).sum\n\n    metrics(\"numAddedFiles\").set(numRewrittenFiles)\n    metrics(\"numAddedBytes\").set(numAddedBytes)\n    metrics(\"numAddedChangeFiles\").set(numAddedChangeFiles)\n    metrics(\"changeFileBytes\").set(changeFileBytes)\n    metrics(\"numRemovedFiles\").set(numTouchedFiles)\n    metrics(\"numRemovedBytes\").set(numRemovedBytes)\n    metrics(\"executionTimeMs\").set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime))\n    metrics(\"scanTimeMs\").set(scanTimeMs)\n    metrics(\"rewriteTimeMs\").set(rewriteTimeMs)\n    // In the case where the numUpdatedRows is not captured, we can siphon out the metrics from\n    // the BasicWriteStatsTracker. This is for case 2 where the update condition contains only\n    // metadata predicates and so the entire partition is re-written.\n    val outputRows = txn.getMetric(\"numOutputRows\").map(_.value).getOrElse(-1L)\n    if (metrics(\"numUpdatedRows\").value == 0 && outputRows != 0 &&\n      metrics(\"numCopiedRows\").value == 0) {\n      // We know that numTouchedRows = numCopiedRows + numUpdatedRows.\n      // Since an entire partition was re-written, no rows were copied.\n      // So numTouchedRows == numUpdateRows\n      metrics(\"numUpdatedRows\").set(metrics(\"numTouchedRows\").value)\n    } else {\n      // This is for case 3 where the update condition contains both metadata and data predicates\n      // so relevant files will have some rows updated and some rows copied. We don't need to\n      // consider case 1 here, where no files match the update condition, as we know that\n      // `totalActions` is empty.\n      metrics(\"numCopiedRows\").set(\n        metrics(\"numTouchedRows\").value - metrics(\"numUpdatedRows\").value)\n      metrics(\"numDeletionVectorsAdded\").set(numDeletionVectorsAdded)\n      metrics(\"numDeletionVectorsRemoved\").set(numDeletionVectorsRemoved)\n      metrics(\"numDeletionVectorsUpdated\").set(numDeletionVectorsUpdated)\n    }\n    txn.registerSQLMetrics(sparkSession, metrics)\n\n    val finalActions = createSetTransaction(sparkSession, deltaLog).toSeq ++ totalActions\n    val numRecordsStats = NumRecordsStats.fromActions(finalActions)\n    val operation = DeltaOperations.Update(condition)\n    validateNumRecords(finalActions, numRecordsStats, operation)\n    val commitVersion = txn.commitIfNeeded(\n      actions = finalActions,\n      op = operation,\n      tags = RowTracking.addPreservedRowTrackingTagIfNotSet(txn.snapshot))\n    sendDriverMetrics(sparkSession, metrics)\n\n    recordDeltaEvent(\n      deltaLog,\n      \"delta.dml.update.stats\",\n      data = UpdateMetric(\n        condition = condition.map(_.sql).getOrElse(\"true\"),\n        numFilesTotal,\n        numTouchedFiles,\n        numRewrittenFiles,\n        numAddedChangeFiles,\n        changeFileBytes,\n        scanTimeMs,\n        rewriteTimeMs,\n        numDeletionVectorsAdded,\n        numDeletionVectorsRemoved,\n        numDeletionVectorsUpdated,\n        commitVersion = commitVersion,\n        numLogicalRecordsAdded = numRecordsStats.numLogicalRecordsAdded,\n        numLogicalRecordsRemoved = numRecordsStats.numLogicalRecordsRemoved)\n    )\n  }\n\n  /**\n   * Scan all the affected files and write out the updated files.\n   *\n   * When CDF is enabled, includes the generation of CDC preimage and postimage columns for\n   * changed rows.\n   *\n   * @return a list of [[FileAction]]s, consisting of newly-written data and CDC files and old\n   *         files that have been removed.\n   */\n  private def rewriteFiles(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      rootPath: Path,\n      inputLeafFiles: Seq[AddFile],\n      nameToAddFileMap: Map[String, AddFile],\n      condition: Expression,\n      generateRemoveFileActions: Boolean,\n      copyUnmodifiedRows: Boolean): Seq[FileAction] = {\n    // Number of total rows that we have seen, i.e. are either copying or updating (sum of both).\n    // This will be used later, along with numUpdatedRows, to determine numCopiedRows.\n    val incrTouchedCountExpr = IncrementMetric(TrueLiteral, metrics(\"numTouchedRows\"))\n\n    // Containing the map from the relative file path to AddFile\n    val baseRelation = buildBaseRelation(\n      spark, txn, \"update\", rootPath, inputLeafFiles.map(_.path), nameToAddFileMap)\n    val newTarget = DeltaTableUtils.replaceFileIndex(target, baseRelation.location)\n    val (targetDf, finalOutput, finalUpdateExpressions) = UpdateCommand.preserveRowTrackingColumns(\n      targetDfWithoutRowTrackingColumns = DataFrameUtils.ofRows(spark, newTarget),\n      snapshot = txn.snapshot,\n      targetOutput = target.output,\n      updateExpressions)\n\n    val targetDfWithEvaluatedCondition = {\n      val evalDf = targetDf.withColumn(UpdateCommand.CONDITION_COLUMN_NAME, Column(condition))\n      val copyAndUpdateRowsDf = if (copyUnmodifiedRows) {\n        evalDf\n      } else {\n        evalDf.filter(Column(UpdateCommand.CONDITION_COLUMN_NAME))\n      }\n      copyAndUpdateRowsDf.filter(Column(incrTouchedCountExpr))\n    }\n\n    val updatedDataFrame = UpdateCommand.withUpdatedColumns(\n      finalOutput,\n      finalUpdateExpressions,\n      condition,\n      targetDfWithEvaluatedCondition,\n      UpdateCommand.shouldOutputCdc(txn))\n\n    val addFiles = txn.writeFiles(updatedDataFrame)\n\n    val removeFiles = if (generateRemoveFileActions) {\n      val operationTimestamp = System.currentTimeMillis()\n      inputLeafFiles.map(_.removeWithTimestamp(operationTimestamp))\n    } else {\n      Nil\n    }\n\n    addFiles ++ removeFiles\n  }\n\n  def shouldWritePersistentDeletionVectors(\n      spark: SparkSession, txn: OptimisticTransaction): Boolean = {\n    spark.conf.get(DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS) &&\n      DeletionVectorUtils.deletionVectorsWritable(txn.snapshot)\n  }\n\n  /**\n   * Validates that the number of records does not change.\n   */\n  private def validateNumRecords(\n      actions: Seq[Action],\n      numRecordsStats: NumRecordsStats,\n      op: Operation): Unit = {\n    val deltaLog = tahoeFileIndex.deltaLog\n\n    (numRecordsStats.numLogicalRecordsAdded,\n      numRecordsStats.numLogicalRecordsRemoved,\n      numRecordsStats.numLogicalRecordsAddedInFilesWithDeletionVectors) match {\n      case (\n        Some(numAddedRecords),\n        Some(numRemovedRecords),\n        Some(numRecordsNotCopied)) =>\n        if (numAddedRecords != numRemovedRecords) {\n          logNumRecordsMismatch(deltaLog, actions, numRecordsStats, op)\n          if (conf.getConf(DeltaSQLConf.NUM_RECORDS_VALIDATION_ENABLED)) {\n            throw DeltaErrors.numRecordsMismatch(\n              operation = \"UPDATE\",\n              numAddedRecords,\n              numRemovedRecords\n            )\n          }\n        }\n\n        if (conf.getConf(DeltaSQLConf.COMMAND_INVARIANT_CHECKS_USE_UNRELIABLE)) {\n          // and also using regular (unreliable) metrics for baseline\n          validateMetricBasedCommandInvariants(\n            numAddedRecords, numRemovedRecords, numRecordsNotCopied, op, deltaLog)\n        }\n\n      case _ =>\n        recordDeltaEvent(deltaLog, opType = \"delta.assertions.statsNotPresentForNumRecordsCheck\")\n        logWarning(log\"Could not validate number of records due to missing statistics.\")\n    }\n  }\n\n  private def validateMetricBasedCommandInvariants(\n      numAddedRecords: Long,\n      numRemovedRecords: Long,\n      numRecordsNotCopied: Long,\n      op: Operation,\n      deltaLog: DeltaLog): Unit = try {\n\n    // Note: These are redundant w.r.t. validateNumRecords, but they ensure correct metrics.\n    val numRowsUpdated = CommandInvariantMetricValueFromSingle(metrics(\"numUpdatedRows\"))\n    val numRowsCopied = CommandInvariantMetricValueFromSingle(metrics(\"numCopiedRows\"))\n\n    // There's a bug where Spark eliminates the entire plan and just rewrites the input files 1:1\n    // for no-op updates and in this case we don't record any metrics.\n    if (numRowsUpdated.getOrDummy == 0 && numRowsCopied.getOrDummy == 0) {\n      return\n    }\n\n    checkCommandInvariant(\n      invariant = () =>\n        numRowsUpdated.getOrThrow + numRowsCopied.getOrThrow + numRecordsNotCopied\n          == numRemovedRecords,\n      label = \"numRowsUpdated + numRowsCopied + numRecordsNotCopied == numRemovedRecords\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n        \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n        \"numRemovedRecords\" -> numRemovedRecords,\n        \"numRecordsNotCopied\" -> numRecordsNotCopied\n      )\n    )\n\n    checkCommandInvariant(\n      invariant = () =>\n        numRowsUpdated.getOrThrow + numRowsCopied.getOrDummy + numRecordsNotCopied\n          == numAddedRecords,\n      label = \"numRowsUpdated + numRowsCopied + numRecordsNotCopied == numAddedRecords\",\n      op = op,\n      deltaLog = deltaLog,\n      parameters = Map(\n        \"numRowsUpdated\" -> numRowsUpdated.getOrDummy,\n        \"numRowsCopied\" -> numRowsCopied.getOrDummy,\n        \"numAddedRecords\" -> numAddedRecords,\n        \"numRecordsNotCopied\" -> numRecordsNotCopied\n      )\n    )\n  } catch {\n    // Immediately re-throw actual command invariant violations, so we don't re-wrap them below.\n    case e: DeltaIllegalStateException if e.getErrorClass == \"DELTA_COMMAND_INVARIANT_VIOLATION\" =>\n      throw e\n    case NonFatal(e) =>\n      logWarning(log\"Unexpected error in validateMetricBasedCommandInvariants\", e)\n      checkCommandInvariant(\n        invariant = () => false,\n        label = \"Unexpected error in validateMetricBasedCommandInvariants\",\n        op = op,\n        deltaLog = deltaLog,\n        parameters = Map.empty\n      )\n  }\n}\n\nobject UpdateCommand {\n  val FILE_NAME_COLUMN = \"_input_file_name_\"\n  val CONDITION_COLUMN_NAME = \"__condition__\"\n  val FINDING_TOUCHED_FILES_MSG: String = \"Finding files to rewrite for UPDATE operation\"\n\n  def rewritingFilesMsg(numFilesToRewrite: Long): String =\n    s\"Rewriting $numFilesToRewrite files for UPDATE operation\"\n\n  /**\n   * Whether or not CDC is enabled on this table and, thus, if we should output CDC data during this\n   * UPDATE operation.\n   */\n  def shouldOutputCdc(txn: OptimisticTransaction): Boolean = {\n    DeltaConfigs.CHANGE_DATA_FEED.fromMetaData(txn.metadata)\n  }\n\n  /**\n   * Build the new columns. If the condition matches, generate the new value using\n   * the corresponding UPDATE EXPRESSION; otherwise, keep the original column value.\n   *\n   * When CDC is enabled, includes the generation of CDC pre-image and post-image columns for\n   * changed rows.\n   *\n   * @param originalExpressions the original column values\n   * @param updateExpressions the update transformation to perform on the input DataFrame\n   * @param dfWithEvaluatedCondition source DataFrame on which we will apply the update expressions\n   *                                 with an additional column CONDITION_COLUMN_NAME which is the\n   *                                 true/false value of if the update condition is satisfied\n   * @param condition update condition\n   * @param shouldOutputCdc if we should output CDC data during this UPDATE operation.\n   * @return the updated DataFrame, with extra CDC columns if CDC is enabled\n   */\n  def withUpdatedColumns(\n      originalExpressions: Seq[Attribute],\n      updateExpressions: Seq[Expression],\n      condition: Expression,\n      dfWithEvaluatedCondition: DataFrame,\n      shouldOutputCdc: Boolean): DataFrame = {\n    val resultDf = if (shouldOutputCdc) {\n      val namedUpdateCols = updateExpressions.zip(originalExpressions).map {\n        case (expr, targetCol) => Column(expr).as(targetCol.name, targetCol.metadata)\n      }\n\n      // Build an array of output rows to be unpacked later. If the condition is matched, we\n      // generate CDC pre and postimages in addition to the final output row; if the condition\n      // isn't matched, we just generate a rewritten no-op row without any CDC events.\n      val preimageCols = originalExpressions.map(Column(_)) :+\n        lit(CDC_TYPE_UPDATE_PREIMAGE).as(CDC_TYPE_COLUMN_NAME)\n      val postimageCols = namedUpdateCols :+\n        lit(CDC_TYPE_UPDATE_POSTIMAGE).as(CDC_TYPE_COLUMN_NAME)\n      val notCdcCol = Column(CDC_TYPE_NOT_CDC).as(CDC_TYPE_COLUMN_NAME)\n      val updatedDataCols = namedUpdateCols :+ notCdcCol\n      val noopRewriteCols = originalExpressions.map(Column(_)) :+ notCdcCol\n      val packedUpdates = array(\n        struct(preimageCols: _*),\n        struct(postimageCols: _*),\n        struct(updatedDataCols: _*)\n      ).expr\n\n      val packedData = if (condition == Literal.TrueLiteral) {\n        packedUpdates\n      } else {\n        If(\n          UnresolvedAttribute(CONDITION_COLUMN_NAME),\n          packedUpdates, // if it should be updated, then use `packagedUpdates`\n          array(struct(noopRewriteCols: _*)).expr) // else, this is a noop rewrite\n      }\n\n      // Explode the packed array, and project back out the final data columns.\n      val finalColumns = (originalExpressions :+ UnresolvedAttribute(CDC_TYPE_COLUMN_NAME)).map {\n        a => col(s\"packedData.`${a.name}`\").as(a.name, a.metadata)\n      }\n      dfWithEvaluatedCondition\n        .select(explode(Column(packedData)).as(\"packedData\"))\n        .select(finalColumns: _*)\n    } else {\n      val finalCols = updateExpressions.zip(originalExpressions).map { case (update, original) =>\n        val updated = if (condition == Literal.TrueLiteral) {\n          update\n        } else {\n          If(UnresolvedAttribute(CONDITION_COLUMN_NAME), update, original)\n        }\n        Column(updated).as(original.name, original.metadata)\n      }\n\n      dfWithEvaluatedCondition.select(finalCols: _*)\n    }\n\n    resultDf.drop(CONDITION_COLUMN_NAME)\n  }\n\n  /**\n   * Preserve the row tracking columns when performing an UPDATE.\n   *\n   * @param targetDfWithoutRowTrackingColumns The target DataFrame on which the UPDATE\n   *                                          operation is to be performed.\n   * @param snapshot                          Snapshot of the Delta table at the start of\n   *                                          the transaction.\n   * @param targetOutput                      The output schema of the target DataFrame.\n   * @param updateExpressions                 The update transformation to perform on the\n   *                                          target DataFrame.\n   * @return\n   * 1. targetDf: The target DataFrame that includes the preserved row tracking columns.\n   * 2. finalOutput: The final output schema, including the preserved row tracking columns.\n   * 3. finalUpdateExpressions: The final update expressions, including transformations\n   * for the preserved row tracking columns.\n   */\n  def preserveRowTrackingColumns(\n      targetDfWithoutRowTrackingColumns: DataFrame,\n      snapshot: Snapshot,\n      targetOutput: Seq[Attribute] = Seq.empty,\n      updateExpressions: Seq[Expression] = Seq.empty):\n    (DataFrame, Seq[Attribute], Seq[Expression]) = {\n    val targetDf = RowTracking.preserveRowTrackingColumns(\n      targetDfWithoutRowTrackingColumns, snapshot)\n\n    val rowIdAttributeOpt = MaterializedRowId.getAttribute(snapshot, targetDf)\n    val rowCommitVersionAttributeOpt =\n      MaterializedRowCommitVersion.getAttribute(snapshot, targetDf)\n    val finalOutput = targetOutput ++ rowIdAttributeOpt ++ rowCommitVersionAttributeOpt\n\n    val finalUpdateExpressions = updateExpressions ++\n      rowIdAttributeOpt ++\n      rowCommitVersionAttributeOpt.map(_ => Literal(null, LongType))\n    (targetDf, finalOutput, finalUpdateExpressions)\n  }\n}\n\n/**\n * Used to report details about update.\n *\n * @param condition: what was the update condition\n * @param numFilesTotal: how big is the table\n * @param numTouchedFiles: how many files did we touch\n * @param numRewrittenFiles: how many files had to be rewritten\n * @param numAddedChangeFiles: how many change files were generated\n * @param changeFileBytes: total size of change files generated\n * @param scanTimeMs: how long did finding take\n * @param rewriteTimeMs: how long did rewriting take\n *\n * @note All the time units are milliseconds.\n */\ncase class UpdateMetric(\n    condition: String,\n    numFilesTotal: Long,\n    numTouchedFiles: Long,\n    numRewrittenFiles: Long,\n    numAddedChangeFiles: Long,\n    changeFileBytes: Long,\n    scanTimeMs: Long,\n    rewriteTimeMs: Long,\n    numDeletionVectorsAdded: Long,\n    numDeletionVectorsRemoved: Long,\n    numDeletionVectorsUpdated: Long,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    commitVersion: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numLogicalRecordsAdded: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numLogicalRecordsRemoved: Option[Long] = None\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/VacuumCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.io.FileNotFoundException\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.util.Date\nimport java.util.concurrent.TimeUnit\nimport scala.collection.JavaConverters._\nimport scala.math.min\nimport scala.util.control.NonFatal\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, FileAction, RemoveFile, SingleAction}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, DeltaFileOperations, FileNames, JsonUtils, Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.DeltaFileOperations.tryDeleteNonRecursive\nimport org.apache.spark.sql.delta.util.FileNames._\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\nimport org.apache.spark.broadcast.Broadcast\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.{Column, DataFrame, Dataset, Encoder, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTableType\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.execution.metric.SQLMetrics.createMetric\nimport org.apache.spark.sql.functions.{col, count, lit, replace, startswith, substr, sum}\nimport org.apache.spark.sql.types.{BooleanType, LongType, StringType, StructField, StructType}\nimport org.apache.spark.util.{Clock, SerializableConfiguration, SystemClock, Utils}\n\n/**\n * Vacuums the table by clearing all untracked files and folders within this table.\n * First lists all the files and directories in the table, and gets the relative paths with\n * respect to the base of the table. Then it gets the list of all tracked files for this table,\n * which may or may not be within the table base path, and gets the relative paths of\n * all the tracked files with respect to the base of the table. Files outside of the table path\n * will be ignored. Then we take a diff of the files and delete directories that were already empty,\n * and all files that are within the table that are no longer tracked.\n */\nobject VacuumCommand extends VacuumCommandImpl with Serializable {\n\n  case class FileNameAndSize(path: String, length: Long)\n\n  /**\n   * path : fully qualified uri\n   * length: size in bytes\n   * isDir: boolean indicating if it is a directory\n   * modificationTime: file update time in milliseconds\n   */\n  val INVENTORY_SCHEMA = StructType(\n    Seq(\n      StructField(\"path\", StringType),\n      StructField(\"length\", LongType),\n      StructField(\"isDir\", BooleanType),\n      StructField(\"modificationTime\", LongType)\n    ))\n\n  object VacuumType extends Enumeration {\n    type VacuumType = Value\n    val LITE = Value(\"LITE\")\n    val FULL = Value(\"FULL\")\n  }\n\n\n  def getFilesFromInventory(\n      basePath: String,\n      partitionColumns: Seq[String],\n      inventory: DataFrame,\n      shouldIcebergMetadataDirBeHidden: Boolean): Dataset[SerializableFileStatus] = {\n    implicit val fileNameAndSizeEncoder: Encoder[SerializableFileStatus] =\n      org.apache.spark.sql.Encoders.product[SerializableFileStatus]\n\n    // filter out required fields from provided inventory DF\n    val inventorySchema = StructType(\n        inventory.schema.fields.filter(f => INVENTORY_SCHEMA.fields.map(_.name).contains(f.name))\n      )\n    if (inventorySchema != INVENTORY_SCHEMA) {\n      throw DeltaErrors.invalidInventorySchema(INVENTORY_SCHEMA.treeString)\n    }\n\n    inventory\n      .filter(startswith(col(\"path\"), lit(s\"$basePath/\")))\n      .select(\n        substr(col(\"path\"), lit(basePath.length + 2)).as(\"path\"),\n        col(\"length\"), col(\"isDir\"), col(\"modificationTime\")\n      )\n      .flatMap {\n        row =>\n          val path = row.getString(0)\n          if(!DeltaTableUtils.isHiddenDirectory(\n              partitionColumns, path, shouldIcebergMetadataDirBeHidden)\n          ) {\n            Seq(SerializableFileStatus(path,\n              row.getLong(1), row.getBoolean(2), row.getLong(3)))\n          } else {\n            None\n          }\n      }\n      .map { f =>\n        // Below logic will make paths url-encoded\n        SerializableFileStatus(pathStringtoUrlEncodedString(f.path), f.length, f.isDir,\n          f.modificationTime)\n      }\n  }\n\n  /**\n   * Clears all untracked files and folders within this table. If the inventory is not provided\n   * then the command first lists all the files and directories in the table, if inventory is\n   * provided then it will be used for identifying files and directories within the table and\n   * gets the relative paths with respect to the base of the table. Then the command gets the\n   * list of all tracked files for this table, which may or may not be within the table base path,\n   * and gets the relative paths of all the tracked files with respect to the base of the table.\n   * Files outside of the table path will be ignored. Then we take a diff of the files and delete\n   * directories that were already empty, and all files that are within the table that are no longer\n   * tracked.\n   *\n   * @param dryRun If set to true, no files will be deleted. Instead, we will list all files and\n   *               directories that will be cleared.\n   * @param retentionHours An optional parameter to override the default Delta tombstone retention\n   *                       period\n   * @param inventory An optional dataframe of files and directories within the table generated\n   *                  from sources like blob store inventory report\n   * @return A Dataset containing the paths of the files/folders to delete in dryRun mode. Otherwise\n   *         returns the base path of the table.\n   */\n  // scalastyle:off argcount\n  def gc(\n      spark: SparkSession,\n      table: DeltaTableV2,\n      dryRun: Boolean = true,\n      retentionHours: Option[Double] = None,\n      inventory: Option[DataFrame] = None,\n      vacuumTypeOpt: Option[String] = None,\n      commandMetrics: Map[String, SQLMetric] = Map.empty,\n      clock: Clock = new SystemClock): DataFrame = {\n    // scalastyle:on argcount\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.gc\") {\n\n      val vacuumStartTime = System.currentTimeMillis()\n      val path = deltaLog.dataPath\n      val deltaHadoopConf = deltaLog.newDeltaHadoopConf()\n      val fs = path.getFileSystem(deltaHadoopConf)\n\n      import org.apache.spark.sql.delta.implicits._\n\n      val snapshot = table.update()\n      deltaLog.protocolWrite(snapshot.protocol)\n\n      if (snapshot.isCatalogOwned) {\n        table.catalogTable.foreach { catalogTable =>\n          assert(\n            catalogTable.tableType == CatalogTableType.MANAGED,\n            s\"All Catalog Owned tables should be MANAGED tables, \" +\n              s\"but found ${catalogTable.tableType} for table ${catalogTable.identifier}.\"\n          )\n        }\n        throw DeltaErrors.operationBlockedOnCatalogManagedTable(\"VACUUM\")\n      }\n\n      // By default, we will do full vacuum unless LITE vacuum conf is set\n      val isLiteVacuumEnabled = spark.sessionState.conf.getConf(DeltaSQLConf.LITE_VACUUM_ENABLED)\n      val defaultType = if (isLiteVacuumEnabled) VacuumType.LITE else VacuumType.FULL\n      val vacuumType = vacuumTypeOpt.map(VacuumType.withName).getOrElse(defaultType)\n\n      val snapshotTombstoneRetentionMillis = DeltaLog.tombstoneRetentionMillis(snapshot.metadata)\n      val retentionMillis = retentionHours.flatMap { h =>\n        val retentionArgument = TimeUnit.HOURS.toMillis(math.round(h))\n        // We ignore retention window argument unless the specified value is 0 hours.\n        if (spark.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_VACUUM_RETENTION_WINDOW_IGNORE_ENABLED) &&\n          retentionArgument != 0L) {\n          logWarning(s\"Vacuum with retention threshold other than 0 hours is ignored.\" +\n            s\" Please set ${DeltaConfigs.TOMBSTONE_RETENTION.key} table property to configure\" +\n            s\" the retention period.\")\n          None\n        } else {\n          Some(retentionArgument)\n        }\n      }\n      val deleteBeforeTimestamp = retentionMillis match {\n        case Some(millis) => clock.getTimeMillis() - millis\n        case _ => snapshot.minFileRetentionTimestamp\n      }\n      logInfo(log\"Starting garbage collection (dryRun = \" +\n        log\"${MDC(DeltaLogKeys.IS_DRY_RUN, dryRun)}) of untracked \" +\n        log\"files older than ${MDC(DeltaLogKeys.DATE,\n          new Date(deleteBeforeTimestamp).toGMTString)} in \" +\n        log\"${MDC(DeltaLogKeys.PATH, path)}\")\n      val hadoopConf = spark.sparkContext.broadcast(\n        new SerializableConfiguration(deltaHadoopConf))\n      val basePath = fs.makeQualified(path).toString\n      val parallelDeleteEnabled =\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_PARALLEL_DELETE_ENABLED)\n      val parallelDeletePartitions =\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_PARALLEL_DELETE_PARALLELISM)\n        .getOrElse(spark.sessionState.conf.numShufflePartitions)\n      val startTimeToIdentifyEligibleFiles = System.currentTimeMillis()\n\n\n      val validFilesResult =\n        getValidFilesFromSnapshot(\n          spark,\n          basePath,\n          snapshot,\n          retentionMillis,\n          hadoopConf,\n          clock,\n          ValidFilesConfig(\n            checkAbsolutePathOnly = false,\n            performRetentionSafetyCheck = true,\n            relativizeIgnoreError = None,\n            dvDiscoveryDisabled = None\n          )\n        )\n\n      val validFiles = validFilesResult.validFiles // Extract DataFrame\n\n      val partitionColumns = snapshot.metadata.partitionSchema.fieldNames\n      val parallelism = spark.sessionState.conf.parallelPartitionDiscoveryParallelism\n      val shouldIcebergMetadataDirBeHidden = UniversalFormat.icebergEnabled(snapshot.metadata)\n      val latestCommitVersionOutsideOfRetentionWindowOpt: Option[Long] =\n        if (vacuumType == VacuumType.LITE) {\n          try {\n            val timestamp = new Timestamp(deleteBeforeTimestamp)\n            val commit = new DeltaHistoryManager(deltaLog).getActiveCommitAtTime(\n              timestamp, table.catalogTable, canReturnLastCommit = true, mustBeRecreatable = false)\n            Some(commit.version)\n          } catch {\n            case ex: DeltaErrors.TimestampEarlierThanCommitRetentionException => None\n          }\n        } else {\n          None\n        }\n\n      // eligibleStartCommitVersionOpt and eligibleEndCommitVersionOpt are valid in case of\n      // lite vacuum. They represent the range of commit versions(inclusive) which give us the\n      // eligible files to be deleted.\n      val\n      (allFilesAndDirsWithDuplicates, eligibleStartCommitVersionOpt, eligibleEndCommitVersionOpt) =\n        inventory match {\n        case Some(inventoryDF) =>\n          val files = getFilesFromInventory(\n            basePath, partitionColumns, inventoryDF, shouldIcebergMetadataDirBeHidden)\n          (files, None, None)\n        case _ if vacuumType == VacuumType.LITE =>\n          getFilesFromDeltaLog(spark, snapshot, basePath, hadoopConf,\n            latestCommitVersionOutsideOfRetentionWindowOpt)\n        case _ =>\n          val files = getFilesFromFilesystem(\n            spark,\n            deltaLog,\n            snapshot,\n            hadoopConf,\n            shouldIcebergMetadataDirBeHidden,\n            applyHiddenFilters = true,\n            parallelism = Option(parallelism)\n          )\n          (files, None, None)\n          }\n      val allFilesAndDirs = allFilesAndDirsWithDuplicates.groupByKey(_.path)\n        .mapGroups { (k, v) =>\n          val duplicates = v.toSeq\n          // of all the duplicates we can return the newest file.\n          duplicates.maxBy(_.modificationTime)\n        }\n\n      recordFrameProfile(\"Delta\", \"VacuumCommand.gc\") {\n        try {\n          allFilesAndDirs.cache()\n\n          implicit val fileNameAndSizeEncoder =\n            org.apache.spark.sql.Encoders.product[FileNameAndSize]\n\n          val dirCounts = allFilesAndDirs.where(col(\"isDir\")).count() + 1 // +1 for the base path\n          val filesAndDirsPresentBeforeDelete = allFilesAndDirs.count()\n\n          // The logic below is as follows:\n          //   1. We take all the files and directories listed in our reservoir\n          //   2. We filter all files older than our tombstone retention period and directories\n          //   3. We get the subdirectories of all files so that we can find non-empty directories\n          //   4. We groupBy each path, and count to get how many files are in each sub-directory\n          //   5. We subtract all the valid files and tombstones in our state\n          //   6. We filter all paths with a count of 1, which will correspond to files not in the\n          //      state, and empty directories. We can safely delete all of these\n          val diff = includeRespectiveDirectoriesWithFilesAndSafetyCheck(\n              allFilesAndDirs, basePath, Some(deleteBeforeTimestamp), hadoopConf)\n            .groupBy(col(\"path\")).agg(count(new Column(\"*\")).as(\"count\"),\n              sum(\"length\").as(\"length\"))\n            .join(validFiles, Seq(\"path\"), \"leftanti\")\n            .where(col(\"count\") === 1)\n\n\n          val sizeOfDataToDeleteRow = diff.agg(sum(\"length\").cast(\"long\")).first()\n          val sizeOfDataToDelete = if (sizeOfDataToDeleteRow.isNullAt(0)) {\n            0L\n          } else {\n            sizeOfDataToDeleteRow.getLong(0)\n          }\n\n          val diffFiles = diff\n            .select(col(\"path\"))\n            .as[String]\n            .map { relativePath =>\n              assert(!urlEncodedStringToPath(relativePath).isAbsolute,\n                \"Shouldn't have any absolute paths for deletion here.\")\n              pathToUrlEncodedString(DeltaFileOperations.absolutePath(basePath, relativePath))\n            }\n          val timeTakenToIdentifyEligibleFiles =\n            System.currentTimeMillis() - startTimeToIdentifyEligibleFiles\n\n\n          val numFiles = diffFiles.count()\n          if (dryRun) {\n            val stats = DeltaVacuumStats(\n              isDryRun = true,\n              specifiedRetentionMillis = retentionMillis,\n              defaultRetentionMillis = snapshotTombstoneRetentionMillis,\n              minRetainedTimestamp = deleteBeforeTimestamp,\n              dirsPresentBeforeDelete = dirCounts,\n              filesAndDirsPresentBeforeDelete = filesAndDirsPresentBeforeDelete,\n              objectsDeleted = numFiles,\n              sizeOfDataToDelete = sizeOfDataToDelete,\n              timeTakenToIdentifyEligibleFiles = timeTakenToIdentifyEligibleFiles,\n              timeTakenForDelete = 0L,\n              vacuumStartTime = vacuumStartTime,\n              vacuumEndTime = System.currentTimeMillis,\n              numPartitionColumns = partitionColumns.size,\n              latestCommitVersion = snapshot.version,\n              eligibleStartCommitVersion = eligibleStartCommitVersionOpt,\n              eligibleEndCommitVersion = eligibleEndCommitVersionOpt,\n              typeOfVacuum = vacuumType.toString\n            )\n\n            recordDeltaEvent(deltaLog, \"delta.gc.stats\", data = stats)\n            logInfo(log\"Found ${MDC(DeltaLogKeys.NUM_FILES, numFiles.toLong)} files \" +\n              log\"(${MDC(DeltaLogKeys.NUM_BYTES, sizeOfDataToDelete)} bytes) and directories in \" +\n              log\"a total of ${MDC(DeltaLogKeys.NUM_DIRS, dirCounts)} directories \" +\n              log\"that are safe to delete. Vacuum stats: ${MDC(DeltaLogKeys.VACUUM_STATS, stats)}\")\n\n            return diffFiles.map(f => urlEncodedStringToPath(f).toString).toDF(\"path\")\n          }\n          logVacuumStart(\n            spark,\n            table,\n            diffFiles,\n            sizeOfDataToDelete,\n            retentionMillis,\n            snapshotTombstoneRetentionMillis)\n\n          val deleteStartTime = System.currentTimeMillis()\n          val filesDeleted = try {\n            delete(diffFiles, spark, basePath,\n              hadoopConf, parallelDeleteEnabled, parallelDeletePartitions)\n          } catch {\n            case t: Throwable =>\n              logVacuumEnd(spark, table, commandMetrics = commandMetrics)\n              throw t\n          }\n          val timeTakenForDelete = System.currentTimeMillis() - deleteStartTime\n          val stats = DeltaVacuumStats(\n            isDryRun = false,\n            specifiedRetentionMillis = retentionMillis,\n            defaultRetentionMillis = snapshotTombstoneRetentionMillis,\n            minRetainedTimestamp = deleteBeforeTimestamp,\n            dirsPresentBeforeDelete = dirCounts,\n            filesAndDirsPresentBeforeDelete = filesAndDirsPresentBeforeDelete,\n            objectsDeleted = filesDeleted,\n            sizeOfDataToDelete = sizeOfDataToDelete,\n            timeTakenToIdentifyEligibleFiles = timeTakenToIdentifyEligibleFiles,\n            timeTakenForDelete = timeTakenForDelete,\n            vacuumStartTime = vacuumStartTime,\n            vacuumEndTime = System.currentTimeMillis,\n            numPartitionColumns = partitionColumns.size,\n            latestCommitVersion = snapshot.version,\n            eligibleStartCommitVersion = eligibleStartCommitVersionOpt,\n            eligibleEndCommitVersion = eligibleEndCommitVersionOpt,\n            typeOfVacuum = vacuumType.toString)\n          recordDeltaEvent(deltaLog, \"delta.gc.stats\", data = stats)\n          logVacuumEnd(\n            spark,\n            table,\n            commandMetrics = commandMetrics,\n            Some(filesDeleted),\n            Some(dirCounts))\n\n          LastVacuumInfo.persistLastVacuumInfo(\n            LastVacuumInfo(latestCommitVersionOutsideOfRetentionWindowOpt), deltaLog)\n\n          logInfo(log\"Deleted ${MDC(DeltaLogKeys.NUM_FILES, filesDeleted)} files \" +\n            log\"(${MDC(DeltaLogKeys.NUM_BYTES, sizeOfDataToDelete)} bytes) and directories in \" +\n            log\"a total of ${MDC(DeltaLogKeys.NUM_DIRS, dirCounts)} directories. \" +\n            log\"Vacuum stats: ${MDC(DeltaLogKeys.VACUUM_STATS, stats)}\")\n\n\n          spark.createDataset(Seq(basePath)).toDF(\"path\")\n        } finally {\n          allFilesAndDirs.unpersist()\n        }\n      }\n    }\n  }\n\n  /**\n   * Returns eligible files to be deleted by looking at the delta log. Additionally, it returns\n   * the start and the end commit versions(inclusive) which give us the eligible files to be\n   * deleted.\n   */\n  protected def getFilesFromDeltaLog(\n      spark: SparkSession,\n      snapshot: Snapshot,\n      basePath: String,\n      hadoopConf: Broadcast[SerializableConfiguration],\n      latestCommitVersionOutsideOfRetentionWindowOpt: Option[Long])\n    : (Dataset[SerializableFileStatus], Option[Long], Option[Long]) = {\n    import org.apache.spark.sql.delta.implicits._\n    val deltaLog = snapshot.deltaLog\n    val earliestCommitVersion = DeltaHistoryManager.getEarliestDeltaFile(deltaLog)\n    val latestCommitVersionOutsideOfRetentionWindowAsOfLastVacuumOpt =\n      LastVacuumInfo.getLastVacuumInfo(deltaLog)\n        .flatMap(_.latestCommitVersionOutsideOfRetentionWindow)\n\n    // If there are no commit versions outside of the retention window,\n    // then there is nothing to Vacuum.\n    val latestCommitVersionOutsideOfRetentionWindow =\n      latestCommitVersionOutsideOfRetentionWindowOpt.getOrElse {\n        return (spark.emptyDataset[SerializableFileStatus], None, None)\n      }\n\n    // In the following two conditions, we return error saying lite vacuum is not possible:\n    // 1. We are not able to locate the last vacuum info and we don't have commit files starting\n    //    from 0\n    // 2. Last vacuum info is there but metadata cleanup has cleaned up commit files since\n    // the last Vacuum's latest commit version outside of the retention window.\n    if (earliestCommitVersion != 0 &&\n      latestCommitVersionOutsideOfRetentionWindowAsOfLastVacuumOpt\n        .forall(_ < earliestCommitVersion)) {\n      throw DeltaErrors.deltaCannotVacuumLite()\n    }\n\n    // The start and the end commit versions give the range of commit files we want to look into\n    // to get the list of eligible files for deletion.\n    val eligibleStartCommitVersion = math.min(\n      snapshot.version,\n      latestCommitVersionOutsideOfRetentionWindowAsOfLastVacuumOpt\n        .map(_ + 1).getOrElse(earliestCommitVersion))\n    val eligibleEndCommitVersion = latestCommitVersionOutsideOfRetentionWindow\n\n    // If there are no additional commit files to look into, then\n    // there is nothing to vacuum.\n    if (eligibleStartCommitVersion > latestCommitVersionOutsideOfRetentionWindow) {\n      return (spark.emptyDataset[SerializableFileStatus], None, None)\n    }\n\n    (getFilesFromDeltaLog(spark, deltaLog, basePath, hadoopConf,\n        eligibleStartCommitVersion, eligibleEndCommitVersion, relativizeIgnoreError = None),\n      Some(eligibleStartCommitVersion),\n      Some(eligibleEndCommitVersion)\n    )\n  }\n\n  case class ValidFilesResult(\n    validFiles: DataFrame\n  )\n}\n\ntrait VacuumCommandImpl extends DeltaCommand {\n\n  private val supportedFsForLogging = Seq(\n    \"wasbs\", \"wasbss\", \"abfs\", \"abfss\", \"adl\", \"gs\", \"file\", \"hdfs\"\n  )\n\n  /**\n   * Returns whether we should record vacuum metrics in the delta log.\n   */\n  private def shouldLogVacuum(\n      spark: SparkSession,\n      table: DeltaTableV2,\n      hadoopConf: Configuration,\n      path: Path): Boolean = {\n    val logVacuumConf = spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_LOGGING_ENABLED)\n\n    if (logVacuumConf.nonEmpty) {\n      return logVacuumConf.get\n    }\n\n    val deltaLog = table.deltaLog\n    val logStore = deltaLog.store\n\n    try {\n      val rawResolvedUri: URI = logStore.resolvePathOnPhysicalStorage(path, hadoopConf).toUri\n      val scheme = rawResolvedUri.getScheme\n      supportedFsForLogging.contains(scheme)\n    } catch {\n      case _: UnsupportedOperationException =>\n        logWarning(log\"Vacuum event logging\" +\n          \" not enabled on this file system because we cannot detect your cloud storage type.\")\n        false\n    }\n  }\n\n  /**\n   * Record Vacuum specific metrics in the commit log at the START of vacuum.\n   *\n   * @param spark - spark session\n   * @param table - the delta table\n   * @param diff - the list of paths (files, directories) that are safe to delete\n   * @param sizeOfDataToDelete - the amount of data (bytes) to be deleted\n   * @param specifiedRetentionMillis - the optional override retention period (millis) to keep\n   *                                   logically removed files before deleting them\n   * @param defaultRetentionMillis - the default retention period (millis)\n   */\n  protected def logVacuumStart(\n      spark: SparkSession,\n      table: DeltaTableV2,\n      diff: Dataset[String],\n      sizeOfDataToDelete: Long,\n      specifiedRetentionMillis: Option[Long],\n      defaultRetentionMillis: Long): Unit = {\n    val deltaLog = table.deltaLog\n    logInfo(\n      log\"Deleting untracked files and empty directories in \" +\n      log\"${MDC(DeltaLogKeys.PATH, deltaLog.dataPath)}. The amount \" +\n      log\"of data to be deleted is ${MDC(DeltaLogKeys.NUM_BYTES, sizeOfDataToDelete)} (in bytes)\"\n      )\n\n    // We perform an empty commit in order to record information about the Vacuum\n    if (shouldLogVacuum(spark, table, deltaLog.newDeltaHadoopConf(), deltaLog.dataPath)) {\n      val checkEnabled =\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED)\n      val txn = table.startTransaction()\n      val metrics = Map[String, SQLMetric](\n        \"numFilesToDelete\" -> createMetric(spark.sparkContext, \"number of files to deleted\"),\n        \"sizeOfDataToDelete\" -> createMetric(spark.sparkContext,\n          \"The total amount of data to be deleted in bytes\")\n      )\n      metrics(\"numFilesToDelete\").set(diff.count())\n      metrics(\"sizeOfDataToDelete\").set(sizeOfDataToDelete)\n      txn.registerSQLMetrics(spark, metrics)\n      val version = txn.commit(actions = Seq(), DeltaOperations.VacuumStart(\n        checkEnabled,\n        specifiedRetentionMillis,\n        defaultRetentionMillis\n      ))\n      setCommitClock(deltaLog, version)\n    }\n  }\n\n  /**\n   * Record Vacuum specific metrics in the commit log at the END of vacuum.\n   *\n   * @param spark - spark session\n   * @param table - the delta table\n   * @param filesDeleted - if the vacuum completed this will contain the number of files deleted.\n   *                       if the vacuum failed, this will be None.\n   * @param dirCounts - if the vacuum completed this will contain the number of directories\n   *                    vacuumed. if the vacuum failed, this will be None.\n   */\n  protected def logVacuumEnd(\n      spark: SparkSession,\n      table: DeltaTableV2,\n      commandMetrics: Map[String, SQLMetric],\n      filesDeleted: Option[Long] = None,\n      dirCounts: Option[Long] = None): Unit = {\n    val deltaLog = table.deltaLog\n    if (shouldLogVacuum(spark, table, deltaLog.newDeltaHadoopConf(), deltaLog.dataPath)) {\n      val txn = table.startTransaction()\n      val status = if (filesDeleted.isEmpty && dirCounts.isEmpty) { \"FAILED\" } else { \"COMPLETED\" }\n      if (filesDeleted.nonEmpty && dirCounts.nonEmpty) {\n        // Populate top level metrics.\n        commandMetrics.get(\"numDeletedFiles\").foreach(_.set(filesDeleted.get))\n        commandMetrics.get(\"numVacuumedDirectories\").foreach(_.set(dirCounts.get))\n        // Additionally, create a separate metrics map in case the commandMetrics is empty.\n        val metrics = Map[String, SQLMetric](\n          \"numDeletedFiles\" -> createMetric(spark.sparkContext, \"number of files deleted.\"),\n          \"numVacuumedDirectories\" ->\n            createMetric(spark.sparkContext, \"num of directories vacuumed.\"),\n          \"status\" -> createMetric(spark.sparkContext, \"status of vacuum\")\n        )\n        metrics(\"numDeletedFiles\").set(filesDeleted.get)\n        metrics(\"numVacuumedDirectories\").set(dirCounts.get)\n        txn.registerSQLMetrics(spark, metrics)\n      }\n      val version = txn.commit(actions = Seq(), DeltaOperations.VacuumEnd(\n        status\n      ))\n      setCommitClock(deltaLog, version)\n    }\n\n    if (filesDeleted.nonEmpty) {\n      logConsole(s\"Deleted ${filesDeleted.get} files and directories in a total \" +\n        s\"of ${dirCounts.get} directories.\")\n    }\n  }\n  protected def setCommitClock(deltaLog: DeltaLog, version: Long) = {\n    // This is done to make sure that the commit timestamp reflects the one provided by the clock\n    // object.\n    if (DeltaUtils.isTesting) {\n      val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n      val filePath = DeltaCommitFileProvider(deltaLog.update()).deltaFile(version)\n      if (fs.exists(filePath)) {\n        fs.setTimes(filePath, deltaLog.clock.getTimeMillis(), deltaLog.clock.getTimeMillis())\n      }\n    }\n  }\n\n  /**\n   * Attempts to relativize the `path` with respect to the `reservoirBase` and converts the path to\n   * a string.\n   */\n  protected def relativize(\n      path: Path,\n      fs: FileSystem,\n      reservoirBase: Path,\n      isDir: Boolean): String = {\n    pathToUrlEncodedString(DeltaFileOperations.tryRelativizePath(fs, reservoirBase, path))\n  }\n\n  /**\n   * Wrapper function for DeltaFileOperations.getAllSubDirectories\n   * returns all subdirectories that `file` has with respect to `base`.\n   */\n  protected def getAllSubdirs(base: String, file: String, fs: FileSystem): Iterator[String] = {\n    DeltaFileOperations.getAllSubDirectories(base, file)._1\n  }\n\n  /**\n   * Attempts to delete the list of candidate files. Returns the number of files deleted.\n   */\n  protected def delete(\n      diff: Dataset[String],\n      spark: SparkSession,\n      basePath: String,\n      hadoopConf: Broadcast[SerializableConfiguration],\n      parallel: Boolean,\n      parallelPartitions: Int): Long = {\n    import org.apache.spark.sql.delta.implicits._\n\n    if (parallel) {\n      diff.repartition(parallelPartitions).mapPartitions { files =>\n        val fs = new Path(basePath).getFileSystem(hadoopConf.value.value)\n        val filesDeletedPerPartition =\n          files.map(p => urlEncodedStringToPath(p)).count(f => tryDeleteNonRecursive(fs, f))\n        Iterator(filesDeletedPerPartition)\n      }.collect().sum\n    } else {\n      val fs = new Path(basePath).getFileSystem(hadoopConf.value.value)\n      val fileResultSet = diff.toLocalIterator().asScala\n      fileResultSet.map(p => urlEncodedStringToPath(p)).count(f => tryDeleteNonRecursive(fs, f))\n    }\n  }\n\n  protected def urlEncodedStringToPath(path: String): Path = SparkPath.fromUrlString(path).toPath\n\n  protected def pathToUrlEncodedString(path: Path): String = SparkPath.fromPath(path).toString\n\n  protected def pathStringtoUrlEncodedString(path: String) =\n    SparkPath.fromPathString(path).toString\n\n  protected def getActionRelativePath(\n      action: FileAction,\n      fs: FileSystem,\n      basePath: Path,\n      relativizeIgnoreError: Boolean): Option[String] = {\n    getRelativePath(action.path, fs, basePath, relativizeIgnoreError)\n  }\n  /** Returns the relative path of a file or None if the file lives outside of the table. */\n  protected def getRelativePath(\n      path: String,\n      fs: FileSystem,\n      basePath: Path,\n      relativizeIgnoreError: Boolean): Option[String] = {\n    val filePath = urlEncodedStringToPath(path)\n    if (filePath.isAbsolute) {\n      val maybeRelative =\n        DeltaFileOperations.tryRelativizePath(fs, basePath, filePath, relativizeIgnoreError)\n      if (maybeRelative.isAbsolute) {\n        // This file lives outside the directory of the table.\n        None\n      } else {\n        Some(pathToUrlEncodedString(maybeRelative))\n      }\n    } else {\n      Some(pathToUrlEncodedString(filePath))\n    }\n  }\n\n\n  /**\n   * Returns the relative paths of all files and subdirectories for this action that must be\n   * retained during GC.\n   */\n  protected def getValidRelativePathsAndSubdirs(\n      action: FileAction,\n      fs: FileSystem,\n      basePath: Path,\n      relativizeIgnoreError: Boolean,\n      dvDiscoveryDisabled: Boolean\n  ): Seq[String] = {\n    val paths = getActionRelativePath(action, fs, basePath, relativizeIgnoreError)\n      .map {\n        relativePath =>\n        Seq(relativePath) ++ getAllSubdirs(\"/\", relativePath, fs)\n      }.getOrElse(Seq.empty)\n\n    val deletionVectorPath =\n      if (dvDiscoveryDisabled) None else getDeletionVectorRelativePathAndSize(action).map(_._1)\n\n    paths ++ deletionVectorPath.toSeq\n  }\n\n  /**\n   * Returns the path of the on-disk deletion vector if it is stored relative to the\n   * `basePath` and it's size otherwise `None`.\n   */\n  protected def getDeletionVectorRelativePathAndSize(action: FileAction): Option[(String, Long)] = {\n    val dv = action match {\n      case a: AddFile if a.deletionVector != null =>\n        Some(a.deletionVector)\n      case r: RemoveFile if r.deletionVector != null =>\n        Some(r.deletionVector)\n      case _ => None\n    }\n\n    dv match {\n      case Some(dv) if dv.isOnDisk =>\n        if (dv.isRelative) {\n          // We actually want a relative path here.\n          Some((pathToUrlEncodedString(dv.absolutePath(new Path(\".\"))), dv.sizeInBytes))\n        } else {\n          assert(dv.isAbsolute)\n          // This is never going to be a path relative to `basePath` for DVs.\n          None\n        }\n      case None => None\n    }\n  }\n\n  // Utility methods for use by VacuumCommand and other commands\n\n  /**\n   * Additional check on retention duration to prevent people from shooting themselves in the foot.\n   */\n  protected def checkRetentionPeriodSafety(\n      spark: SparkSession,\n      retentionMs: Option[Long],\n      configuredRetention: Long): Unit = {\n    if (retentionMs.exists(_ < 0)) {\n      throw DeltaErrors.vacuumRetentionPeriodNegative()\n    }\n    val checkEnabled =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED)\n    val retentionSafe = retentionMs.forall(_ >= configuredRetention)\n    var configuredRetentionHours = TimeUnit.MILLISECONDS.toHours(configuredRetention)\n    if (TimeUnit.HOURS.toMillis(configuredRetentionHours) < configuredRetention) {\n      configuredRetentionHours += 1\n    }\n    if (checkEnabled && !retentionSafe) {\n      throw DeltaErrors.vacuumRetentionPeriodTooShort(configuredRetentionHours)\n    }\n  }\n\n  /**\n   * Configuration for getValidFilesFromSnapshot behavior.\n   *\n   * @param checkAbsolutePathOnly If true, filters out files not in table path (for clones)\n   * @param performRetentionSafetyCheck If true, validates retention period is safe\n   * @param relativizeIgnoreError If None, reads from config; if Some(value), uses that value\n   * @param dvDiscoveryDisabled If None, reads from config+test; if Some(value), uses that value\n   */\n  case class ValidFilesConfig(\n    checkAbsolutePathOnly: Boolean,\n    performRetentionSafetyCheck: Boolean,\n    relativizeIgnoreError: Option[Boolean],\n    dvDiscoveryDisabled: Option[Boolean]\n  )\n\n  /**\n   * Returns eligible files (RemoveFile + CDC) from a range of commit versions.\n   *\n   * @param relativizeIgnoreError If None, reads from config; if Some(value), uses that value\n   */\n  protected def getFilesFromDeltaLog(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      basePath: String,\n      hadoopConf: Broadcast[SerializableConfiguration],\n      eligibleStartCommitVersion: Long,\n      eligibleEndCommitVersion: Long,\n      relativizeIgnoreError: Option[Boolean]): Dataset[SerializableFileStatus] = {\n    import org.apache.spark.sql.delta.implicits._\n    // When coordinated commits are enabled, commit files could be found in _delta_log directory\n    // as well as in commit directory. We get the delta log files outside of the retention window\n    // from both the places.\n    val prefix = listingPrefix(deltaLog.logPath, eligibleStartCommitVersion)\n    val eligibleDeltaLogFilesFromDeltaLogDirectory =\n      deltaLog.store.listFrom(prefix, deltaLog.newDeltaHadoopConf)\n        .collect { case DeltaFile(f, deltaFileVersion) => (f, deltaFileVersion) }\n        .takeWhile(_._2 <= eligibleEndCommitVersion)\n        .toSeq\n\n    val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n    val commitDirPath = FileNames.commitDirPath(deltaLog.logPath)\n    val updatedStartCommitVersion =\n      eligibleDeltaLogFilesFromDeltaLogDirectory.lastOption.map(_._2)\n        .getOrElse(eligibleStartCommitVersion)\n    val eligibleDeltaLogFilesFromCommitDirectory = if (fs.exists(commitDirPath)) {\n      deltaLog.store\n        .listFrom(listingPrefix(commitDirPath, updatedStartCommitVersion),\n          deltaLog.newDeltaHadoopConf)\n        .collect { case UnbackfilledDeltaFile(f, deltaFileVersion, _) => (f, deltaFileVersion) }\n        .takeWhile(_._2 <= eligibleEndCommitVersion)\n        .toSeq\n    } else {\n      Seq.empty\n    }\n\n    val allDeltaLogFilesOutsideTheRetentionWindow = eligibleDeltaLogFilesFromDeltaLogDirectory ++\n      eligibleDeltaLogFilesFromCommitDirectory\n    val deltaLogFileIndex = DeltaLogFileIndex(DeltaLogFileIndex.COMMIT_FILE_FORMAT,\n      allDeltaLogFilesOutsideTheRetentionWindow.map(_._1)).get\n\n    val allActions = deltaLog.loadIndex(deltaLogFileIndex).as[SingleAction]\n    val nonCDFFiles = allActions\n      .where(\"remove IS NOT NULL\")\n      .select(col(\"remove\")\n      .as[RemoveFile])\n      .mapPartitions { iter =>\n        iter.flatMap { r =>\n          val modificationTime = r.deletionTimestamp.getOrElse(0L)\n          val dv = getDeletionVectorRelativePathAndSize(r).map { case (path, length) =>\n            SerializableFileStatus(path, length, isDir = false, modificationTime)\n          }\n          dv.iterator ++ Iterator.single(SerializableFileStatus(\n            r.path, r.size.getOrElse(0L), isDir = false, modificationTime))\n        }\n      }\n      .as[SerializableFileStatus]\n    val cdfFiles = allActions\n      .where(\"cdc IS NOT NULL\")\n      .select(col(\"cdc\")\n      .as[AddCDCFile])\n      .map(cdc => SerializableFileStatus(cdc.path, cdc.size, isDir = false, modificationTime = 0L))\n\n    val relativizeIgnoreErrorValue = relativizeIgnoreError.getOrElse(\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_RELATIVIZE_IGNORE_ERROR)\n    )\n    nonCDFFiles.union(cdfFiles).mapPartitions { iter =>\n      val tableBasePath = new Path(basePath)\n      val fs = tableBasePath.getFileSystem(hadoopConf.value.value)\n      iter.flatMap { f =>\n        // if file path is outside of the table base path, those files are not considered as they\n        // are not part of this table. Shallow clone is one example where this happens.\n        getRelativePath(f.path, fs, tableBasePath, relativizeIgnoreErrorValue)\n          .map(SerializableFileStatus(_, f.length, f.isDir, f.modificationTime))\n      }\n    }\n  }\n\n  /**\n   * Returns files from filesystem via recursive directory listing.\n   *\n   * @param spark SparkSession\n   * @param deltaLog DeltaLog for the table\n   * @param snapshot Current snapshot\n   * @param hadoopConf Hadoop configuration\n   * @param shouldIcebergMetadataDirBeHidden Whether to hide Iceberg metadata directories\n   * @param applyHiddenFilters If true, excludes hidden directories (_delta_log, etc.).\n   * @param parallelism Optional parallelism for file listing\n   * @return Dataset of SerializableFileStatus with url-encoded relative paths\n   */\n  protected def getFilesFromFilesystem(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      snapshot: Snapshot,\n      hadoopConf: Broadcast[SerializableConfiguration],\n      shouldIcebergMetadataDirBeHidden: Boolean,\n      applyHiddenFilters: Boolean,\n      parallelism: Option[Int]): Dataset[SerializableFileStatus] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    val basePath = deltaLog.dataPath.toString\n    val partitionColumns = snapshot.metadata.partitionColumns\n\n    val (hiddenDirFilter, hiddenFileFilter) = if (applyHiddenFilters) {\n      // Apply filters to exclude _delta_log and other hidden directories\n      val filter = DeltaTableUtils.isHiddenDirectory(\n        partitionColumns, _: String, shouldIcebergMetadataDirBeHidden)\n      (filter, filter)\n    } else {\n      // Include all directories and files\n      ((_: String) => false, (_: String) => false)\n    }\n\n    // Use DeltaFileOperations.recursiveListDirs\n    val files = DeltaFileOperations.recursiveListDirs(\n      spark,\n      Seq(basePath),\n      hadoopConf,\n      hiddenDirNameFilter = hiddenDirFilter,\n      hiddenFileNameFilter = hiddenFileFilter,\n      fileListingParallelism = parallelism\n    )\n      .map { f =>\n        // Make paths url-encoded (same pattern as VacuumCommand)\n        val path = pathStringtoUrlEncodedString(f.path)\n        SerializableFileStatus(path, f.length, f.isDir, f.modificationTime)\n      }\n\n    files\n  }\n\n\n  /**\n   * Helper to compute all valid files based on basePath and Snapshot provided.\n   * Returns a DataFrame with a single column \"path\" containing all files that should be\n   * protected from vacuum (active files, tombstones in retention, DVs, subdirs, etc.)\n   *\n   * @param config Configuration for behavior customization\n   */\n  protected def getValidFilesFromSnapshot(\n      spark: SparkSession,\n      basePath: String,\n      snapshot: Snapshot,\n      retentionMillis: Option[Long],\n      hadoopConf: Broadcast[SerializableConfiguration],\n      clock: Clock,\n      config: ValidFilesConfig): VacuumCommand.ValidFilesResult = {\n    import org.apache.spark.sql.delta.implicits._\n    require(snapshot.version >= 0, \"No state defined for this table. Is this really \" +\n      \"a Delta table? Refusing to garbage collect.\")\n\n    val snapshotTombstoneRetentionMillis = DeltaLog.tombstoneRetentionMillis(snapshot.metadata)\n\n    // Safety check only for vacuum (not for read-only metrics)\n    if (config.performRetentionSafetyCheck) {\n      checkRetentionPeriodSafety(spark, retentionMillis, snapshotTombstoneRetentionMillis)\n    }\n\n    val deleteBeforeTimestamp = retentionMillis match {\n      case Some(millis) => clock.getTimeMillis() - millis\n      case _ => snapshot.minFileRetentionTimestamp\n    }\n\n    // Use provided values or read from config\n    val relativizeIgnoreErrorValue = config.relativizeIgnoreError.getOrElse(\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_VACUUM_RELATIVIZE_IGNORE_ERROR)\n    )\n    val dvDiscoveryDisabledValue = config.dvDiscoveryDisabled.getOrElse(\n      DeltaUtils.isTesting && spark.sessionState.conf.getConf(\n        DeltaSQLConf.FAST_DROP_FEATURE_DV_DISCOVERY_IN_VACUUM_DISABLED)\n    )\n\n    val canonicalizedBasePath = SparkPath.fromPathString(basePath).urlEncoded\n\n\n    val files = snapshot.stateDS.mapPartitions { actions =>\n      val reservoirBase = new Path(basePath)\n      val fs = reservoirBase.getFileSystem(hadoopConf.value.value)\n      actions.flatMap {\n        _.unwrap match {\n          // Existing tables may not store canonicalized paths, so we check both the canonicalized\n          // and non-canonicalized paths to ensure we don't accidentally delete wrong files.\n          case fa: FileAction if config.checkAbsolutePathOnly &&\n            !fa.path.contains(basePath) && !fa.path.contains(canonicalizedBasePath) => Nil\n          case tombstone: RemoveFile if tombstone.delTimestamp < deleteBeforeTimestamp => Nil\n          case fa: FileAction =>\n            getValidRelativePathsAndSubdirs(\n              fa,\n              fs,\n              reservoirBase,\n              relativizeIgnoreErrorValue,\n              dvDiscoveryDisabledValue\n            )\n          case _ => Nil\n        }\n      }\n    }\n\n    val validFiles = files\n    .toDF(\"path\")\n    VacuumCommand.ValidFilesResult(\n      validFiles\n      )\n  }\n\n  /**\n   * Expands files into their parent directories.\n   * Used by both VacuumCommand (for deletion).\n   *\n   * For each file, this creates entries for all parent directories.\n   * The caller must then aggregate and perform safety checks:\n   * 1. GroupBy path and aggregate (count, sum length, max modificationTime)\n   * 2. Join with validFiles (leftanti) to remove protected files\n   * 3. Filter where count === 1 (safety check - only unique paths are safe to delete)\n   *\n   * @param files Dataset of files to process\n   * @param basePath Base path of the table\n   * @param deleteBeforeTimestamp If Some(timestamp), filters files by modificationTime\n   *                              < timestamp (for VacuumCommand). If None, includes all files\n   *                              regardless of time.\n   * @param hadoopConf Hadoop configuration\n   *\n   * @return DataFrame with schema (path: String, length: Long,\n   *         isDir: Boolean, modificationTime: Long)\n   *         The caller should groupBy, aggregate, join with validFiles, then filter count === 1\n   */\n  protected def includeRespectiveDirectoriesWithFilesAndSafetyCheck(\n      files: Dataset[SerializableFileStatus],\n      basePath: String,\n      deleteBeforeTimestamp: Option[Long],\n      hadoopConf: Broadcast[SerializableConfiguration]\n  ): DataFrame = {\n    import org.apache.spark.sql.functions.col\n\n    implicit val serializableFileStatusEncoder =\n      org.apache.spark.sql.Encoders.product[SerializableFileStatus]\n\n    val canonicalizedBasePath = SparkPath.fromPathString(basePath).urlEncoded\n\n    val filteredFiles = deleteBeforeTimestamp match {\n      case Some(timestamp) =>\n        files.where(col(\"modificationTime\") < timestamp || col(\"isDir\"))\n      case None =>\n        files\n    }\n\n    filteredFiles\n      .mapPartitions { fileStatusIterator =>\n        val reservoirBase = new Path(basePath)\n        val fs = reservoirBase.getFileSystem(hadoopConf.value.value)\n        fileStatusIterator.flatMap { fileStatus =>\n          if (fileStatus.isDir) {\n            Iterator.single(SerializableFileStatus(\n              relativize(urlEncodedStringToPath(fileStatus.path), fs,\n                reservoirBase, isDir = true), 0L, isDir = true, fileStatus.modificationTime))\n          } else {\n            val dirs = getAllSubdirs(canonicalizedBasePath, fileStatus.path, fs)\n            val dirsWithSlash = dirs.map { p =>\n              val relativizedPath = relativize(urlEncodedStringToPath(p), fs,\n                reservoirBase, isDir = true)\n              SerializableFileStatus(relativizedPath, 0L, isDir = true, fileStatus.modificationTime)\n            }\n            dirsWithSlash ++ Iterator(\n              SerializableFileStatus(relativize(\n                urlEncodedStringToPath(fileStatus.path), fs, reservoirBase, isDir = false),\n                fileStatus.length, isDir = false, fileStatus.modificationTime))\n          }\n        }\n      }.toDF()\n  }\n}\n\ncase class DeltaVacuumStats(\n    isDryRun: Boolean,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    specifiedRetentionMillis: Option[Long],\n    defaultRetentionMillis: Long,\n    minRetainedTimestamp: Long,\n    dirsPresentBeforeDelete: Long,\n    filesAndDirsPresentBeforeDelete: Long,\n    objectsDeleted: Long,\n    sizeOfDataToDelete: Long,\n    timeTakenToIdentifyEligibleFiles: Long,\n    timeTakenForDelete: Long,\n    vacuumStartTime: Long,\n    vacuumEndTime: Long,\n    numPartitionColumns: Long,\n    latestCommitVersion: Long,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    eligibleStartCommitVersion: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    eligibleEndCommitVersion: Option[Long],\n    typeOfVacuum: String\n)\n\ncase class LastVacuumInfo(\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  latestCommitVersionOutsideOfRetentionWindow: Option[Long] = None\n)\n\nobject LastVacuumInfo extends DeltaCommand {\n  private val LAST_VACUUM_INFO_FILE_NAME = \"_last_vacuum_info\"\n\n  /** The path to the file that holds metadata about the most recent Vacuum. */\n  private def getLastVacuumInfoPath(logPath: Path): Path =\n    new Path(logPath, LAST_VACUUM_INFO_FILE_NAME)\n\n  def getLastVacuumInfo(deltaLog: DeltaLog): Option[LastVacuumInfo] = {\n    try {\n      val path = getLastVacuumInfoPath(deltaLog.logPath)\n      val json = deltaLog.store.read(path, deltaLog.newDeltaHadoopConf()).head\n      Some(JsonUtils.mapper.readValue[LastVacuumInfo](json))\n    } catch {\n      case _: FileNotFoundException =>\n        None\n      case NonFatal(e) =>\n        recordDeltaEvent(\n          deltaLog,\n          \"delta.lastVacuumInfo.read.corruptedJson\",\n          data = Map(\"exception\" -> Utils.exceptionString(e))\n        )\n        None\n    }\n  }\n\n  def persistLastVacuumInfo(lastVacuumInfo: LastVacuumInfo, deltaLog: DeltaLog): Unit = {\n    try {\n      val path = getLastVacuumInfoPath(deltaLog.logPath)\n      val json = Iterator.single(JsonUtils.toJson(lastVacuumInfo))\n      deltaLog.store.write(path, json, overwrite = true, deltaLog.newDeltaHadoopConf())\n    } catch {\n      case NonFatal(e) =>\n        recordDeltaEvent(\n          deltaLog,\n          \"delta.lastVacuumInfo.write.failure\",\n          data = Map(\"exception\" -> Utils.exceptionString(e))\n        )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/WriteIntoDelta.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport scala.collection.mutable\nimport scala.util.Try\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.DMLUtils.TaggedCommitData\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.constraints.Constraint\nimport org.apache.spark.sql.delta.constraints.Constraints.Check\nimport org.apache.spark.sql.delta.constraints.Invariants.ArbitraryExpression\nimport org.apache.spark.sql.delta.schema.{ImplicitMetadataOperation, InvariantViolationException, SchemaUtils}\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{And, Attribute, Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical.DeleteFromTable\nimport org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, CharVarcharUtils}\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.functions.{array, col, explode, lit, struct}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{StringType, StructType}\n\n/**\n * Used to write a [[DataFrame]] into a delta table.\n *\n * New Table Semantics\n *  - The schema of the [[DataFrame]] is used to initialize the table.\n *  - The partition columns will be used to partition the table.\n *\n * Existing Table Semantics\n *  - The save mode will control how existing data is handled (i.e. overwrite, append, etc)\n *  - The schema of the DataFrame will be checked and if there are new columns present\n *    they will be added to the tables schema. Conflicting columns (i.e. a INT, and a STRING)\n *    will result in an exception\n *  - The partition columns, if present are validated against the existing metadata. If not\n *    present, then the partitioning of the table is respected.\n *\n * In combination with `Overwrite`, a `replaceWhere` option can be used to transactionally\n * replace data that matches a predicate.\n *\n * In combination with `Overwrite` dynamic partition overwrite mode (option `partitionOverwriteMode`\n * set to `dynamic`, or in spark conf `spark.sql.sources.partitionOverwriteMode` set to `dynamic`)\n * is also supported.\n *\n * Dynamic partition overwrite mode conflicts with `replaceWhere`:\n *   - If a `replaceWhere` option is provided, and dynamic partition overwrite mode is enabled in\n *   the DataFrameWriter options, an error will be thrown.\n *   - If a `replaceWhere` option is provided, and dynamic partition overwrite mode is enabled in\n *   the spark conf, data will be overwritten according to the `replaceWhere` expression\n *\n * @param catalogTableOpt Should explicitly be set when table is accessed from catalog\n * @param schemaInCatalog The schema created in Catalog. We will use this schema to update metadata\n *                        when it is set (in CTAS code path), and otherwise use schema from `data`.\n */\ncase class WriteIntoDelta(\n    override val deltaLog: DeltaLog,\n    mode: SaveMode,\n    options: DeltaOptions,\n    partitionColumns: Seq[String],\n    override val configuration: Map[String, String],\n    override val data: DataFrame,\n    val catalogTableOpt: Option[CatalogTable] = None,\n    schemaInCatalog: Option[StructType] = None\n    )\n  extends LeafRunnableCommand\n  with ImplicitMetadataOperation\n  with DeltaCommand\n  with WriteIntoDeltaLike {\n\n  override protected val canMergeSchema: Boolean = options.canMergeSchema\n\n  private def isOverwriteOperation: Boolean = mode == SaveMode.Overwrite\n\n  override protected val canOverwriteSchema: Boolean =\n    options.canOverwriteSchema && isOverwriteOperation && options.replaceWhere.isEmpty\n\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    deltaLog.withNewTransaction(catalogTableOpt) { txn =>\n      if (hasBeenExecuted(txn, sparkSession, Some(options))) {\n        return Seq.empty\n      }\n\n      val taggedCommitData = writeAndReturnCommitData(\n        txn, sparkSession\n      )\n      val operation = DeltaOperations.Write(\n        mode = mode,\n        partitionBy = Option(partitionColumns),\n        predicate = options.replaceWhere,\n        userMetadata = options.userMetadata,\n        isDynamicPartitionOverwrite =\n          if (Try(options.isDynamicPartitionOverwriteMode).getOrElse(false)) Some(true) else None,\n        canOverwriteSchema = if (options.canOverwriteSchema) Some(true) else None,\n        canMergeSchema = if (options.canMergeSchema) Some(true) else None\n      )\n      txn.commitIfNeeded(taggedCommitData.actions, operation, tags = taggedCommitData.stringTags)\n    }\n    Seq.empty\n  }\n\n  override def writeAndReturnCommitData(\n      txn: OptimisticTransaction,\n      sparkSession: SparkSession,\n      clusterBySpecOpt: Option[ClusterBySpec] = None,\n      isTableReplace: Boolean = false): TaggedCommitData[Action] = {\n    import org.apache.spark.sql.delta.implicits._\n    if (txn.readVersion > -1) {\n      // This table already exists, check if the insert is valid.\n      if (mode == SaveMode.ErrorIfExists) {\n        throw DeltaErrors.pathAlreadyExistsException(deltaLog.dataPath)\n      } else if (mode == SaveMode.Ignore) {\n        return TaggedCommitData.empty\n      } else if (mode == SaveMode.Overwrite) {\n        DeltaLog.assertRemovable(txn.snapshot)\n      }\n    }\n    val isReplaceWhere = mode == SaveMode.Overwrite && options.replaceWhere.nonEmpty\n    val finalClusterBySpecOpt =\n      if (mode == SaveMode.Append || isReplaceWhere) {\n        clusterBySpecOpt.foreach { clusterBySpec =>\n          ClusteredTableUtils.validateClusteringColumnsInSnapshot(txn.snapshot, clusterBySpec)\n        }\n        // Append mode and replaceWhere cannot update the clustering columns.\n        None\n      } else {\n        clusterBySpecOpt\n      }\n    val rearrangeOnly = options.rearrangeOnly\n    val charPadding = sparkSession.conf.get(SQLConf.READ_SIDE_CHAR_PADDING)\n    val charAsVarchar = sparkSession.conf.get(SQLConf.CHAR_AS_VARCHAR)\n    val dataSchema = if (!charAsVarchar && charPadding) {\n      data.schema\n    } else {\n      // If READ_SIDE_CHAR_PADDING is not enabled, CHAR type is the same as VARCHAR. The change\n      // below makes DESC TABLE to show VARCHAR instead of CHAR.\n      CharVarcharUtils.replaceCharVarcharWithStringInSchema(\n        CharVarcharUtils.replaceCharWithVarchar(CharVarcharUtils.getRawSchema(data.schema))\n          .asInstanceOf[StructType])\n    }\n    val finalSchema = schemaInCatalog.getOrElse(dataSchema)\n    if (txn.metadata.schemaString != null) {\n      // In cases other than CTAS (INSERT INTO, DataFrame write), block if values are provided for\n      // GENERATED ALWAYS AS IDENTITY columns.\n      IdentityColumn.blockExplicitIdentityColumnInsert(\n        txn.metadata.schema,\n        data.queryExecution.analyzed)\n    }\n    // We need to cache this canUpdateMetadata before calling updateMetadata, as that will update\n    // it to true. This is unavoidable as getNewDomainMetadata uses information generated by\n    // updateMetadata, so it needs to be run after that.\n    val canUpdateMetadata = txn.canUpdateMetadata\n    updateMetadata(data.sparkSession, txn, finalSchema,\n      partitionColumns, configuration, isOverwriteOperation, rearrangeOnly\n    )\n    val newDomainMetadata = getNewDomainMetadata(\n      txn,\n      canUpdateMetadata,\n      isReplacingTable = isOverwriteOperation && options.replaceWhere.isEmpty,\n      finalClusterBySpecOpt\n    )\n\n    val replaceWhereOnDataColsEnabled =\n      sparkSession.conf.get(DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED)\n\n    val useDynamicPartitionOverwriteMode = {\n      if (txn.metadata.partitionColumns.isEmpty) {\n        // We ignore dynamic partition overwrite mode for non-partitioned tables\n        false\n      } else if (isTableReplace) {\n        // A replace table command should always replace the table, not just some partitions.\n        false\n      } else if (options.replaceWhere.nonEmpty) {\n        if (options.partitionOverwriteModeInOptions && options.isDynamicPartitionOverwriteMode) {\n          // replaceWhere and dynamic partition overwrite conflict because they both specify which\n          // data to overwrite. We throw an error when:\n          // 1. replaceWhere is provided in a DataFrameWriter option\n          // 2. partitionOverwriteMode is set to \"dynamic\" in a DataFrameWriter option\n          throw DeltaErrors.replaceWhereUsedWithDynamicPartitionOverwrite()\n        } else {\n          // If replaceWhere is provided, we do not use dynamic partition overwrite, even if it's\n          // enabled in the spark session configuration, since generally query-specific configs take\n          // precedence over session configs\n          false\n        }\n      } else {\n        options.isDynamicPartitionOverwriteMode\n      }\n    }\n\n    if (useDynamicPartitionOverwriteMode && canOverwriteSchema) {\n      throw DeltaErrors.overwriteSchemaUsedWithDynamicPartitionOverwrite()\n    }\n\n    // Validate partition predicates\n    var containsDataFilters = false\n    val replaceWhere = options.replaceWhere.flatMap { replace =>\n      val parsed = parsePredicates(sparkSession, replace)\n      if (replaceWhereOnDataColsEnabled) {\n        // Helps split the predicate into separate expressions\n        val (metadataPredicates, dataFilters) = DeltaTableUtils.splitMetadataAndDataPredicates(\n          parsed.head, txn.metadata.partitionColumns, sparkSession)\n        if (rearrangeOnly && dataFilters.nonEmpty) {\n          throw DeltaErrors.replaceWhereWithFilterDataChangeUnset(dataFilters.mkString(\",\"))\n        }\n        containsDataFilters = dataFilters.nonEmpty\n        Some(metadataPredicates ++ dataFilters)\n      } else if (mode == SaveMode.Overwrite) {\n        verifyPartitionPredicates(sparkSession, txn.metadata.partitionColumns, parsed)\n        Some(parsed)\n      } else {\n        None\n      }\n    }\n\n    if (txn.readVersion < 0) {\n      // Initialize the log path\n      deltaLog.createLogDirectoriesIfNotExists()\n    }\n\n    val (newFiles, addFiles, deletedFiles) = (mode, replaceWhere) match {\n      case (SaveMode.Overwrite, Some(predicates)) if !replaceWhereOnDataColsEnabled =>\n        // fall back to match on partition cols only when replaceArbitrary is disabled.\n        val newFiles = txn.writeFiles(data, Some(options))\n        val addFiles = newFiles.collect { case a: AddFile => a }\n        // Check to make sure the files we wrote out were actually valid.\n        val matchingFiles = DeltaLog.filterFileList(\n          txn.metadata.partitionSchema, addFiles.toDF(sparkSession), predicates).as[AddFile]\n          .collect()\n        val invalidFiles = addFiles.toSet -- matchingFiles\n        if (invalidFiles.nonEmpty) {\n          val badPartitions = invalidFiles\n            .map(_.partitionValues)\n            .map { _.map { case (k, v) => s\"$k=$v\" }.mkString(\"/\") }\n            .mkString(\", \")\n          throw DeltaErrors.replaceWhereMismatchException(options.replaceWhere.get, badPartitions)\n        }\n        (newFiles, addFiles, txn.filterFiles(predicates).map(_.remove))\n      case (SaveMode.Overwrite, Some(conditions)) if txn.snapshot.version >= 0 =>\n        val constraints = extractConstraints(sparkSession, conditions)\n\n        val removedFileActions = removeFiles(sparkSession, txn, conditions)\n        val cdcExistsInRemoveOp = removedFileActions.exists(_.isInstanceOf[AddCDCFile])\n\n        // The above REMOVE will not produce explicit CDF data when persistent DV is enabled.\n        // Therefore here we need to decide whether to produce explicit CDF for INSERTs, because\n        // the CDF protocol requires either (i) all CDF data are generated explicitly as AddCDCFile,\n        // or (ii) all CDF data can be deduced from [[AddFile]] and [[RemoveFile]].\n        val dataToWrite =\n          if (containsDataFilters &&\n              CDCReader.isCDCEnabledOnTable(txn.metadata, sparkSession) &&\n              sparkSession.conf.get(DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_WITH_CDF_ENABLED) &&\n              cdcExistsInRemoveOp) {\n            var dataWithDefaultExprs = data\n            // Add identity columns if they are not in `data`.\n            // Column names for which we will track identity column high water marks.\n            val trackHighWaterMarks = mutable.Set.empty[String]\n            val topLevelOutputNames = CaseInsensitiveMap(data.schema.map(f => f.name -> f).toMap)\n            val selectExprs = txn.metadata.schema.map { f =>\n              if (ColumnWithDefaultExprUtils.isIdentityColumn(f) &&\n                !topLevelOutputNames.contains(f.name)) {\n                // Track high water marks for generated IDENTITY values.\n                trackHighWaterMarks += f.name\n                IdentityColumn.createIdentityColumnGenerationExprAsColumn(f)\n              } else {\n                SchemaUtils.fieldToColumn(f).alias(f.name)\n              }\n            }\n            if (trackHighWaterMarks.nonEmpty) {\n              txn.setTrackHighWaterMarks(trackHighWaterMarks.toSet)\n              dataWithDefaultExprs = data.select(selectExprs: _*)\n            }\n\n            // pack new data and cdc data into an array of structs and unpack them into rows\n            // to share values in outputCols on both branches, avoiding re-evaluating\n            // non-deterministic expression twice.\n            val outputCols = dataWithDefaultExprs.schema.map(SchemaUtils.fieldToColumn(_))\n            val insertCols = outputCols :+\n              lit(CDCReader.CDC_TYPE_INSERT).as(CDCReader.CDC_TYPE_COLUMN_NAME)\n            val insertDataCols = outputCols :+\n              Column(CDCReader.CDC_TYPE_NOT_CDC)\n                .as(CDCReader.CDC_TYPE_COLUMN_NAME)\n            val packedInserts = array(\n              struct(insertCols: _*),\n              struct(insertDataCols: _*)\n            ).expr\n\n            dataWithDefaultExprs\n              .select(explode(Column(packedInserts)).as(\"packedData\"))\n              .select(\n                (dataWithDefaultExprs.schema.map(_.name) :+ CDCReader.CDC_TYPE_COLUMN_NAME)\n                  .map { n => col(s\"packedData.`$n`\").as(n) }: _*)\n          } else {\n            data\n          }\n        val newFiles = try txn.writeFiles(dataToWrite, Some(options), constraints) catch {\n          case e: InvariantViolationException =>\n            throw DeltaErrors.replaceWhereMismatchException(\n              options.replaceWhere.get,\n              e)\n        }\n        (newFiles,\n          newFiles.collect { case a: AddFile => a },\n          removedFileActions)\n      case (SaveMode.Overwrite, None) =>\n        val newFiles = writeFiles(\n          txn, data, options\n        )\n        val addFiles = newFiles.collect { case a: AddFile => a }\n        val deletedFiles = if (useDynamicPartitionOverwriteMode) {\n          // with dynamic partition overwrite for any partition that is being written to all\n          // existing data in that partition will be deleted.\n          // the selection what to delete is on the next two lines\n          val updatePartitions = addFiles.map(_.partitionValues).toSet\n          txn.filterFiles(updatePartitions).map(_.remove)\n        } else {\n          txn.filterFiles().map(_.remove)\n        }\n        (newFiles, addFiles, deletedFiles)\n      case _ =>\n        val newFiles = writeFiles(\n          txn, data, options\n        )\n        (newFiles, newFiles.collect { case a: AddFile => a }, Nil)\n    }\n\n    // Need to handle replace where metrics separately.\n    if (replaceWhere.nonEmpty && replaceWhereOnDataColsEnabled &&\n        sparkSession.conf.get(DeltaSQLConf.REPLACEWHERE_METRICS_ENABLED)) {\n      registerReplaceWhereMetrics(sparkSession, txn, newFiles, deletedFiles)\n    } else if (mode == SaveMode.Overwrite &&\n        sparkSession.conf.get(DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED)) {\n      registerOverwriteRemoveMetrics(sparkSession, txn, deletedFiles)\n    }\n\n    val fileActions = if (rearrangeOnly) {\n      val changeFiles = newFiles.collect { case c: AddCDCFile => c }\n      if (changeFiles.nonEmpty) {\n        throw DeltaErrors.unexpectedChangeFilesFound(changeFiles.mkString(\"\\n\"))\n      }\n      addFiles.map(_.copy(dataChange = !rearrangeOnly)) ++\n        deletedFiles.map {\n          case add: AddFile => add.copy(dataChange = !rearrangeOnly)\n          case remove: RemoveFile => remove.copy(dataChange = !rearrangeOnly)\n          case other => throw DeltaErrors.illegalFilesFound(other.toString)\n        }\n    } else {\n      newFiles ++ deletedFiles\n    }\n    val allActions =\n      newDomainMetadata ++\n        createSetTransaction(sparkSession, deltaLog, Some(options)).toSeq ++\n        fileActions\n    TaggedCommitData(allActions)\n  }\n\n  private def writeFiles(\n      txn: OptimisticTransaction,\n      data: DataFrame,\n      options: DeltaOptions\n    ): Seq[FileAction] = {\n    txn.writeFiles(data, Some(options))\n  }\n\n  private def removeFiles(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      conditions: Seq[Expression]): Seq[Action] = {\n    val relation = LogicalRelation(\n        txn.deltaLog.createRelation(snapshotToUseOpt = Some(txn.snapshot),\n          catalogTableOpt = txn.catalogTable))\n    val processedCondition = conditions.reduceOption(And)\n    val command = spark.sessionState.analyzer.execute(\n      DeleteFromTable(relation, processedCondition.getOrElse(Literal.TrueLiteral)))\n    spark.sessionState.analyzer.checkAnalysis(command)\n    val (deleteActions, deleteMetrics) =\n      command.asInstanceOf[DeleteCommand].performDelete(spark, txn.deltaLog, txn)\n    recordDeltaEvent(\n      deltaLog,\n      \"delta.dml.write.removeFiles.stats\",\n      data = deleteMetrics.copy(isWriteCommand = true)\n    )\n    deleteActions\n  }\n\n  override def withNewWriterConfiguration(updatedConfiguration: Map[String, String])\n    : WriteIntoDeltaLike = this.copy(configuration = updatedConfiguration)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/WriteIntoDeltaLike.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.OptimisticTransaction\nimport org.apache.spark.sql.delta.actions.Action\nimport org.apache.spark.sql.delta.actions.AddCDCFile\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.actions.RemoveFile\nimport org.apache.spark.sql.delta.commands.DMLUtils.TaggedCommitData\nimport org.apache.spark.sql.delta.constraints.Constraint\nimport org.apache.spark.sql.delta.constraints.Constraints.Check\nimport org.apache.spark.sql.delta.constraints.Invariants.ArbitraryExpression\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * An interface for writing [[data]] into Delta tables.\n */\ntrait WriteIntoDeltaLike {\n  /**\n   * A helper method to create a new instances of [[WriteIntoDeltaLike]] with\n   * updated [[configuration]].\n   */\n  def withNewWriterConfiguration(updatedConfiguration: Map[String, String]): WriteIntoDeltaLike\n\n  /**\n   * The configuration to be used for writing [[data]] into Delta table.\n   */\n  val configuration: Map[String, String]\n\n  /**\n   * Data to be written into Delta table.\n   */\n  val data: DataFrame\n\n  /**\n   * Write [[data]] into Delta table as part of [[txn]] and @return the actions to be committed.\n   */\n  def writeAndReturnCommitData(\n      txn: OptimisticTransaction,\n      sparkSession: SparkSession,\n      clusterBySpecOpt: Option[ClusterBySpec] = None,\n      isTableReplace: Boolean = false): TaggedCommitData[Action]\n\n  def write(\n      txn: OptimisticTransaction,\n      sparkSession: SparkSession,\n      clusterBySpecOpt: Option[ClusterBySpec] = None,\n      isTableReplace: Boolean = false): Seq[Action] = writeAndReturnCommitData(\n    txn, sparkSession, clusterBySpecOpt, isTableReplace).actions\n\n  val deltaLog: DeltaLog\n\n\n\n  // Helper for creating a SQLMetric and setting its value, since it isn't valid to create a\n  // SQLMetric with a positive `initValue`.\n  private def createSumMetricWithValue(\n      spark: SparkSession,\n      name: String,\n      value: Long): SQLMetric = {\n    val metric = new SQLMetric(\"sum\")\n    metric.register(spark.sparkContext, Some(name))\n    metric.set(value)\n    metric\n  }\n\n  /**\n   * Overwrite mode produces extra delete metrics that are registered here.\n   * @param deleteActions - RemoveFiles added by Delete job\n   */\n  protected def registerOverwriteRemoveMetrics(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      deleteActions: Seq[Action]): Unit = {\n    var numRemovedFiles = 0\n    var numRemovedBytes = 0L\n    deleteActions.foreach {\n      case action: RemoveFile =>\n        numRemovedFiles += 1\n        numRemovedBytes += action.getFileSize\n      case _ => () // do nothing\n    }\n    val sqlMetrics = Map(\n      \"numRemovedFiles\" -> createSumMetricWithValue(\n        spark, \"number of files removed\", numRemovedFiles),\n      \"numRemovedBytes\" -> createSumMetricWithValue(\n        spark, \"number of bytes removed\", numRemovedBytes)\n    )\n    txn.registerSQLMetrics(spark, sqlMetrics)\n  }\n\n  /**\n   * Replace where operationMetrics need to be recorded separately.\n   * @param newFiles - AddFile and AddCDCFile added by write job\n   * @param deleteActions - AddFile, RemoveFile, AddCDCFile added by Delete job\n   */\n  protected def registerReplaceWhereMetrics(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      newFiles: Seq[Action],\n      deleteActions: Seq[Action]): Unit = {\n    var numFiles = 0L\n    var numCopiedRows = 0L\n    var numOutputBytes = 0L\n    var numNewRows = 0L\n    var numAddedChangedFiles = 0L\n    var hasRowLevelMetrics = true\n\n    newFiles.foreach {\n      case a: AddFile =>\n        numFiles += 1\n        numOutputBytes += a.size\n        if (a.numLogicalRecords.isEmpty) {\n          hasRowLevelMetrics = false\n        } else {\n          numNewRows += a.numLogicalRecords.get\n        }\n      case cdc: AddCDCFile =>\n        numAddedChangedFiles += 1\n      case _ =>\n    }\n\n    deleteActions.foreach {\n      case a: AddFile =>\n        numFiles += 1\n        numOutputBytes += a.size\n        if (a.numLogicalRecords.isEmpty) {\n          hasRowLevelMetrics = false\n        } else {\n          numCopiedRows += a.numLogicalRecords.get\n        }\n      case _: AddCDCFile =>\n        numAddedChangedFiles += 1\n      // Remove metrics will be handled by the delete command.\n      case _ =>\n    }\n\n    var sqlMetrics = Map(\n      \"numFiles\" -> createSumMetricWithValue(spark, \"number of files written\", numFiles),\n      \"numOutputBytes\" -> createSumMetricWithValue(spark, \"number of output bytes\", numOutputBytes),\n      \"numAddedChangeFiles\" -> createSumMetricWithValue(\n        spark, \"number of change files added\", numAddedChangedFiles)\n    )\n    if (hasRowLevelMetrics) {\n      sqlMetrics ++= Map(\n        \"numOutputRows\" -> createSumMetricWithValue(\n          spark, \"number of rows added\", numNewRows + numCopiedRows),\n        \"numCopiedRows\" -> createSumMetricWithValue(spark, \"number of copied rows\", numCopiedRows)\n      )\n    } else {\n      // this will get filtered out in DeltaOperations.WRITE transformMetrics\n      sqlMetrics ++= Map(\n        \"numOutputRows\" -> createSumMetricWithValue(spark, \"number of rows added\", 0L),\n        \"numCopiedRows\" -> createSumMetricWithValue(spark, \"number of copied rows\", 0L)\n      )\n    }\n    txn.registerSQLMetrics(spark, sqlMetrics)\n  }\n\n  protected def extractConstraints(\n      sparkSession: SparkSession,\n      exprs: Seq[Expression]): Seq[Constraint] = {\n    if (!sparkSession.conf.get(DeltaSQLConf.REPLACEWHERE_CONSTRAINT_CHECK_ENABLED)) {\n      Seq.empty\n    } else {\n      exprs.flatMap { e =>\n        // While writing out the new data, we only want to enforce constraint on expressions\n        // with UnresolvedAttribute, that is, containing column name. Because we parse a\n        // predicate string without analyzing it, if there's a column name, it has to be\n        // unresolved.\n        e.collectFirst {\n          case _: UnresolvedAttribute =>\n            val arbitraryExpression = ArbitraryExpression(e)\n            Check(arbitraryExpression.name, arbitraryExpression.expression)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/alterDeltaTableCommands.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteringColumnInfo\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{DropTableFeatureUtils, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillCommand\nimport org.apache.spark.sql.delta.commands.columnmapping.RemoveColumnMappingCommand\nimport org.apache.spark.sql.delta.constraints.{CharVarcharConstraint, Constraints}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CoordinatedCommitsUtils}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.redirect.RedirectFeature\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.schema.SchemaUtils.transformSchema\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.sql.{AnalysisException, Column, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.{Resolver, UnresolvedAttribute}\nimport org.apache.spark.sql.catalyst.catalog.CatalogUtils\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical.{Filter, IgnoreCachedData, QualifiedColType, QualifiedColTypeShims}\nimport org.apache.spark.sql.catalyst.util.{CharVarcharUtils, SparkCharVarcharUtils}\nimport org.apache.spark.sql.connector.catalog.TableCatalog\nimport org.apache.spark.sql.connector.catalog.TableChange.{After, ColumnPosition, First}\nimport org.apache.spark.sql.connector.expressions.FieldReference\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\nimport org.apache.spark.sql.types._\n\n/**\n * A super trait for alter table commands that modify Delta tables.\n */\ntrait AlterDeltaTableCommand extends DeltaCommand {\n\n  def table: DeltaTableV2\n\n  protected def startTransaction(): OptimisticTransaction = {\n    // WARNING: It's not safe to use startTransactionWithInitialSnapshot here. Some commands call\n    // this method more than once, and some commands can be created with a stale table.\n    val txn = table.startTransaction()\n    if (txn.readVersion == -1) {\n      throw DeltaErrors.notADeltaTableException(table.name())\n    }\n    txn\n  }\n\n  /**\n   * Check if the column to change has any dependent expressions:\n   *   - generated column expressions\n   *   - check constraints\n   */\n  protected def checkDependentExpressions(\n      sparkSession: SparkSession,\n      columnParts: Seq[String],\n      oldMetadata: actions.Metadata,\n      protocol: Protocol): Unit = {\n    if (!sparkSession.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ALTER_TABLE_CHANGE_COLUMN_CHECK_EXPRESSIONS)) {\n      return\n    }\n    // check if the column to change is referenced by check constraints\n    val dependentConstraints =\n      Constraints.findDependentConstraints(sparkSession, columnParts, oldMetadata)\n    if (dependentConstraints.nonEmpty) {\n      throw DeltaErrors.foundViolatingConstraintsForColumnChange(\n        UnresolvedAttribute(columnParts).name, dependentConstraints)\n    }\n    // check if the column to change is referenced by any generated columns\n    val dependentGenCols = SchemaUtils.findDependentGeneratedColumns(\n      sparkSession, columnParts, protocol, oldMetadata.schema)\n    if (dependentGenCols.nonEmpty) {\n      throw DeltaErrors.foundViolatingGeneratedColumnsForColumnChange(\n        UnresolvedAttribute(columnParts).name, dependentGenCols)\n    }\n  }\n}\n\n/**\n * A command that sets Delta table configuration.\n *\n * The syntax of this command is:\n * {{{\n *   ALTER TABLE table1 SET TBLPROPERTIES ('key1' = 'val1', 'key2' = 'val2', ...);\n * }}}\n */\ncase class AlterTableSetPropertiesDeltaCommand(\n    table: DeltaTableV2,\n    configuration: Map[String, String])\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n\n    RowTrackingFeature.validateAndBackfill(sparkSession, table, configuration)\n\n    val columnMappingPropertyKey = DeltaConfigs.COLUMN_MAPPING_MODE.key\n    val disableColumnMapping = configuration.get(columnMappingPropertyKey).contains(\"none\")\n    val columnMappingRemovalAllowed = sparkSession.sessionState.conf.getConf(\n      DeltaSQLConf.ALLOW_COLUMN_MAPPING_REMOVAL)\n    if (disableColumnMapping && columnMappingRemovalAllowed) {\n      RemoveColumnMappingCommand(deltaLog, table.catalogTable)\n        .run(sparkSession, removeColumnMappingTableProperty = false)\n      // Not changing anything else, so we can return early.\n      if (configuration.size == 1) {\n        return Seq.empty[Row]\n      }\n    }\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.setProperties\") {\n      val txn = startTransaction()\n\n      val metadata = txn.metadata\n      val filteredConfs = configuration.filterKeys {\n        case k if k.toLowerCase(Locale.ROOT).startsWith(\"delta.constraints.\") =>\n          throw DeltaErrors.useAddConstraints\n        case k if k == TableCatalog.PROP_LOCATION =>\n          throw DeltaErrors.useSetLocation()\n        case k if k == TableCatalog.PROP_COMMENT =>\n          false\n        case k if k == TableCatalog.PROP_PROVIDER =>\n          throw DeltaErrors.cannotChangeProvider()\n        case k if k == TableFeatureProtocolUtils.propertyKey(ClusteringTableFeature) =>\n          throw DeltaErrors.alterTableSetClusteringTableFeatureException(\n            ClusteringTableFeature.name)\n        case k if k == ClusteredTableUtils.PROP_CLUSTERING_COLUMNS =>\n          throw DeltaErrors.cannotModifyTableProperty(k)\n        case _ =>\n          true\n      }.toMap\n\n      // For Coordinated Commits table validation\n      CoordinatedCommitsUtils.validateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n        existingConfs = metadata.configuration, propertyOverrides = filteredConfs)\n      // For Catalog Owned table validation\n      CatalogOwnedTableUtils.validatePropertiesForAlterTableSetPropertiesDeltaCommand(\n        txn.snapshot, propertyOverrides = filteredConfs)\n\n      // If table redirect feature is updated, validates its property.\n      RedirectFeature.validateTableRedirect(txn.snapshot, table.catalogTable, configuration)\n      val newMetadata = metadata.copy(\n        description = configuration.getOrElse(TableCatalog.PROP_COMMENT, metadata.description),\n        configuration = metadata.configuration ++ filteredConfs)\n\n      txn.updateMetadata(newMetadata)\n\n      // Tag if the metadata update is _only_ for enabling row tracking. This allows for\n      // an optimization where we can safely not fail concurrent txns from the metadata update.\n      var tags = Map.empty[String, String]\n      val enableRowTracking = configuration\n        .getOrElse(DeltaConfigs.ROW_TRACKING_ENABLED.key, \"false\")\n        .toBoolean\n      if (enableRowTracking) {\n        RowTrackingFeature.validateConfigurations(txn.metadata.configuration ++ configuration)\n        // In general, we should be able to detect any relevant state changes during backfill.\n        // Nevertheless, before enabling row tracking make sure the main invariants are satisfied.\n        // Note, Delta makes sure conflicting txns backfill their own files. However, this does\n        // not cover third party writers.\n        RowTracking.verifyInvariantsForTablePropertyEnablement(txn.snapshot)\n\n        if (configuration.size == 1) {\n          tags += (DeltaCommitTag.RowTrackingEnablementOnlyTag.key -> \"true\")\n        }\n      }\n      txn.commit(Nil, DeltaOperations.SetTableProperties(configuration), tags)\n\n      Seq.empty[Row]\n    }\n  }\n}\n\n/**\n * A command that unsets Delta table configuration.\n * If ifExists is false, each individual key will be checked if it exists or not, it's a\n * one-by-one operation, not an all or nothing check. Otherwise, non-existent keys will be ignored.\n *\n * The syntax of this command is:\n * {{{\n *   ALTER TABLE table1 UNSET TBLPROPERTIES [IF EXISTS] ('key1', 'key2', ...);\n * }}}\n */\ncase class AlterTableUnsetPropertiesDeltaCommand(\n    table: DeltaTableV2,\n    propKeys: Seq[String],\n    ifExists: Boolean,\n    fromDropFeatureCommand: Boolean = false)\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    val columnMappingPropertyKey = DeltaConfigs.COLUMN_MAPPING_MODE.key\n    val disableColumnMapping = propKeys.contains(columnMappingPropertyKey)\n    val columnMappingRemovalAllowed = sparkSession.sessionState.conf.getConf(\n      DeltaSQLConf.ALLOW_COLUMN_MAPPING_REMOVAL)\n    if (disableColumnMapping && columnMappingRemovalAllowed) {\n      RemoveColumnMappingCommand(deltaLog, table.catalogTable)\n        .run(sparkSession, removeColumnMappingTableProperty = true)\n      if (propKeys.size == 1) {\n        // Not unsetting anything else, so we can return early.\n        return Seq.empty[Row]\n      }\n    }\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.unsetProperties\") {\n      val txn = startTransaction()\n      val metadata = txn.metadata\n\n      val normalizedKeys = DeltaConfigs.normalizeConfigKeys(propKeys)\n      if (!ifExists) {\n        normalizedKeys.foreach { k =>\n          if (!metadata.configuration.contains(k)) {\n            throw DeltaErrors.unsetNonExistentProperty(k, table.name())\n          }\n        }\n      }\n\n      if (!fromDropFeatureCommand) {\n        CoordinatedCommitsUtils.validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n          existingConfs = metadata.configuration, propKeysToUnset = normalizedKeys)\n        CatalogOwnedTableUtils.validatePropertiesForAlterTableUnsetPropertiesDeltaCommand(\n          txn.snapshot, propKeysToUnset = normalizedKeys)\n      }\n      val newConfiguration = metadata.configuration.filterNot {\n        case (key, _) => normalizedKeys.contains(key)\n      }\n      val description = if (normalizedKeys.contains(TableCatalog.PROP_COMMENT)) null else {\n        metadata.description\n      }\n      val newMetadata = metadata.copy(\n        description = description,\n        configuration = newConfiguration)\n      txn.updateMetadata(newMetadata)\n      txn.commit(Nil, DeltaOperations.UnsetTableProperties(normalizedKeys, ifExists))\n\n      Seq.empty[Row]\n    }\n  }\n}\n\n/**\n * A command that removes an existing feature from the table. The feature needs to implement the\n * [[RemovableFeature]] trait.\n *\n * The syntax of the command is:\n * {{{\n *   ALTER TABLE t DROP FEATURE f [TRUNCATE HISTORY]\n * }}}\n *\n * When dropping a feature, remove the feature traces from the latest version. However, the table\n * history still contains feature traces. This creates two problems:\n *\n * 1) Reconstructing the state of the latest version may require replaying log records prior to\n *    feature removal. Log replay is based on checkpoints which is used by clients as a starting\n *    point for replaying history. Any actions before the checkpoint do not need to be replayed.\n *    However, checkpoints may be deleted at any time, which can then expose readers to older\n *    log records.\n * 2) Clients could create checkpoints in past versions. These could lead to incorrect behavior\n *    if the client that created the checkpoint did not support all features.\n *\n * To address these issues, we currently provide two implementations:\n *\n * 1) [[executeDropFeatureWithHistoryTruncation]]. We truncate history at the boundary of version\n *    of the dropped feature (when required). Requires two executions of the drop feature command\n *    with a waiting time in between the two executions.\n * 2) [[executeDropFeatureWithCheckpointProtection]], i.e. fast drop feature. We create barrier\n *    checkpoints to protect against log replay and checkpoint creation. The behavior is enforced\n *    with the aid of CheckpointProtectionTableFeature.\n *\n * Config tableFeatures.fastDropFeature.enabled can be used to control which implementation\n * is used. Furthermore, please note the option [TRUNCATE HISTORY] in the SQL syntax is only\n * relevant for [[executeDropFeatureWithHistoryTruncation]]. When used, we always fallback to that\n * implementation.\n *\n * At a high level, dropping a feature consists of two stages (see [[RemovableFeature]]):\n *\n * 1) preDowngradeCommand. This command is responsible for removing any data and metadata\n * related to the feature.\n * 2) Protocol downgrade. Removes the feature from the current version's protocol.\n *    During this stage we also validate whether all traces of the feature-to-be-removed are gone.\n *\n * For removing features with requiresHistoryProtection=false the two steps above are sufficient.\n * For features that require history protection, we follow a different approach for each of the\n * implementations listed above. Please see the corresponding functions for more details.\n *\n * Note, legacy features can be removed as well. When removing a legacy feature from a legacy\n * protocol, if the result cannot be represented with a legacy representation we use the\n * table features representation. For example, removing Invariants from (1, 3) results to\n * (1, 7, None, [AppendOnly, CheckConstraints]). Adding back Invariants to the protocol is\n * normalized back to (1, 3). This allows to consistently transition back and forth between legacy\n * protocols and table feature protocols.\n */\ncase class AlterTableDropFeatureDeltaCommand(\n    table: DeltaTableV2,\n    featureName: String,\n    truncateHistory: Boolean = false)\n  extends LeafRunnableCommand\n  with AlterDeltaTableCommand\n  with IgnoreCachedData {\n  import org.apache.spark.sql.delta.actions.DropTableFeatureUtils._\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    // Check whether the protocol contains the feature in either the writer features list or\n    // the reader+writer features list. Note, protocol needs to denormalized to allow dropping\n    // features from legacy protocols.\n    val protocol = table.update().protocol\n    val protocolContainsFeatureName =\n      protocol.implicitlyAndExplicitlySupportedFeatures.map(_.name).contains(featureName)\n    val featureInLowerCase = featureName.toLowerCase(Locale.ROOT)\n    val removableFeature = TableFeature.featureNameToFeature(featureName) match {\n      // Check if a property was passed instead of a feature, featureName has to\n      // start with \"delta.\" if that is the case.\n      case _ if !protocolContainsFeatureName && featureInLowerCase.startsWith(\"delta.\") &&\n        DeltaConfigs.getAllConfigs.contains(featureInLowerCase.stripPrefix(\"delta.\")) =>\n          throw DeltaErrors.dropTableFeatureFeatureIsADeltaProperty(featureName)\n      case Some(_) if !protocolContainsFeatureName =>\n        throw DeltaErrors.dropTableFeatureFeatureNotSupportedByProtocol(featureName)\n      case Some(feature: RemovableFeature) => feature\n      case Some(_) => throw DeltaErrors.dropTableFeatureNonRemovableFeature(featureName)\n      case None => throw DeltaErrors.dropTableFeatureFeatureNotSupportedByClient(featureName)\n    }\n\n    val historyTruncationEligibleFeature = removableFeature.requiresHistoryProtection ||\n      removableFeature == CheckpointProtectionTableFeature\n    if (truncateHistory && !historyTruncationEligibleFeature) {\n      throw DeltaErrors.tableFeatureDropHistoryTruncationNotAllowed()\n    }\n\n    if (removableFeature == CheckpointProtectionTableFeature && !truncateHistory) {\n      throw DeltaErrors.canOnlyDropCheckpointProtectionWithHistoryTruncationException\n    }\n\n    // Validate that the `removableFeature` is not a dependency of any other feature that is\n    // enabled on the table.\n    dependentFeatureCheck(removableFeature, protocol)\n\n    // If the user uses the truncate history option we always fallback to the old implementation.\n    if (!truncateHistory && conf.getConf(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED)) {\n      executeDropFeatureWithCheckpointProtection(sparkSession, removableFeature)\n    } else {\n      executeDropFeatureWithHistoryTruncation(sparkSession, removableFeature)\n    }\n  }\n\n  /**\n   *  Drop features with history truncation. When dropping a feature that\n   *  requiresHistoryProtection, we need to truncate history prior to feature removal to ensure\n   *  the history does not contain any traces of the removed feature. The user journey is the\n   *  following:\n   *\n   *  1) The user runs the remove feature command which removes any traces of the feature from\n   *     the latest version. The removal command throws a message that there was partial success\n   *     and the retention period must pass before a protocol downgrade is possible.\n   *  2) The user runs again the command after the retention period is over. The command checks the\n   *     current state again and the history. If everything is clean, it proceeds with the protocol\n   *     downgrade. The TRUNCATE HISTORY option may be used here to automatically set\n   *     the log retention period to a minimum of 24 hours before clearing the logs. The minimum\n   *     value is based on the expected duration of the longest running transaction. This is the\n   *     lowest retention period we can set without endangering concurrent transactions.\n   *     If transactions do run for longer than this period while this command is run, then this\n   *     can lead to data corruption.\n   */\n  private def executeDropFeatureWithHistoryTruncation(\n      sparkSession: SparkSession,\n      removableFeature: TableFeature with RemovableFeature): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.dropFeature\") {\n      // The removableFeature.preDowngradeCommand needs to adhere to the following requirements:\n      //\n      // a) Bring the table to a state the validation passes.\n      // b) To not allow concurrent commands to alter the table in a way the validation does not\n      //    pass. This can be done by first disabling the relevant metadata property.\n      // c) Undoing (b) should cause the preDowngrade command to fail.\n      //\n      // Note, for features that cannot be disabled we solely rely for correctness on\n      // isSnapshotClean.\n      val requiresHistoryValidation = removableFeature.requiresHistoryProtection\n      val startTimeNs = table.deltaLog.clock.nanoTime()\n      val status = removableFeature\n        .preDowngradeCommand(table)\n        .removeFeatureTracesIfNeeded(sparkSession)\n      val preDowngradeMadeChanges = status.performedChanges\n      if (requiresHistoryValidation) {\n        // Generate a checkpoint after the cleanup that is based on commits that do not use\n        // the feature. This intends to help slow-moving tables to qualify for history truncation\n        // asap. The checkpoint is based on a new commit to avoid creating a checkpoint\n        // on a commit that still contains traces of the removed feature.\n        // Note, the checkpoint is created in both executions of DROP FEATURE command.\n        createEmptyCommitAndCheckpoint(table, startTimeNs)\n\n        // If the pre-downgrade command made changes, then the table's historical versions\n        // certainly still contain traces of the feature. We don't have to run an expensive\n        // explicit check, but instead we fail straight away.\n        if (preDowngradeMadeChanges) {\n          throw DeltaErrors.dropTableFeatureWaitForRetentionPeriod(\n            featureName, table.initialSnapshot.metadata)\n        }\n      }\n\n      val startSnapshotOpt = status.lastCommitVersionOpt.map(table.getSnapshotAt(_))\n      val txn = table.startTransaction(snapshotOpt = startSnapshotOpt)\n      val snapshot = txn.snapshot\n\n      // Verify whether all requirements hold before performing the protocol downgrade.\n      // If any concurrent transactions interfere with the protocol downgrade txn we\n      // revalidate the requirements against the snapshot of the winning txn.\n      if (!removableFeature.validateDropInvariants(table, snapshot)) {\n        TransactionExecutionObserver.getObserver.transactionAborted()\n        throw DeltaErrors.dropTableFeatureConflictRevalidationFailed()\n      }\n\n      // For reader+writer features, before downgrading the protocol we need to ensure there are no\n      // traces of the feature in past versions. If traces are found, the user is advised to wait\n      // until the retention period is over. This is a slow operation.\n      // Note, if this txn conflicts, we check all winning commits for traces of the feature.\n      // Therefore, we do not need to check again for historical versions during conflict\n      // resolution.\n      if (requiresHistoryValidation) {\n        // Clean up expired logs before checking history. This also makes sure there is no\n        // concurrent metadataCleanup during findEarliestReliableCheckpoint. Note, this\n        // cleanUpExpiredLogs call truncates the cutoff at a minute granularity.\n        deltaLog.cleanUpExpiredLogs(\n          snapshotToCleanup = snapshot,\n          catalogTableOpt = table.catalogTable,\n          deltaRetentionMillisOpt =\n            if (truncateHistory) Some(truncateHistoryLogRetentionMillis(txn.metadata)) else None,\n          cutoffTruncationGranularity =\n            if (truncateHistory) TruncationGranularity.MINUTE else TruncationGranularity.DAY)\n\n        val historyContainsFeature = removableFeature.historyContainsFeature(\n          spark = sparkSession,\n          table = table,\n          downgradeTxnReadSnapshot = snapshot)\n        if (historyContainsFeature) {\n          throw DeltaErrors.dropTableFeatureHistoricalVersionsExist(featureName, snapshot.metadata)\n        }\n      }\n\n      val op = DeltaOperations.DropTableFeature(featureName, truncateHistory)\n      txn.updateProtocol(txn.protocol.removeFeature(removableFeature))\n      val metadataWithNewConfiguration = DropTableFeatureUtils\n        .getDowngradedProtocolMetadata(removableFeature, txn.metadata)\n      val commitActions = removableFeature.actionsToIncludeAtDowngradeCommit(txn.snapshot)\n      txn.updateMetadata(metadataWithNewConfiguration)\n      txn.commit(commitActions, op)\n      recordDeltaEvent(\n        deltaLog = deltaLog,\n        opType = \"dropFeatureCompleted.withHistoryTruncation\",\n        data = Map(\"droppedFeature\" -> removableFeature.name))\n      Nil\n    }\n  }\n\n  /**\n   * Drop [[removableFeature]] and enforce correctness with protected checkpoints. When dropping a\n   * feature that requiresHistoryProtection we need to make sure:\n   *\n   * 1) Clients will not process historical commits that contain traces of the dropped feature.\n   * 2) Clients will not create checkpoints in historical versions when they do not support the\n   *    required features.\n   *\n   * This can be achieved as follows:\n   * 1) Create DELTA_SNAPSHOT_LOADING_MAX_RETRIES + 1 checkpoints (3 checkpoints),\n   *    after the execution of the pre-downgrade command but before the protocol downgrade commit.\n   *    Clients should never attempt to read prior to these checkpoints. We create multiple\n   *    checkpoints because the Delta Spark client may try multiple earlier checkpoints when it\n   *    encounters a checkpoint that it cannot read. By adding multiple checkpoints, we make\n   *    sure it gives up before it proceeds to earlier checkpoints.\n   * 2) Protect checkpoints 1-3 from metadata cleanup operations. This is achieved with the aid of\n   *    the CheckpointProtectionTableFeature. Using a table property we store the version of\n   *    the last dropped feature, V. With CheckpointProtectionTableFeature we enforce clients\n   *    can only delete checkpoints before V if they clean up the corresponding commit history at\n   *    the same time and do not create any checkpoints prior to V. To create a checkpoint prior\n   *    to V, they must support all features for the versions they truncate.\n   * 3) In a single commit, drop the feature from the protocol, set V = the version the current\n   *    feature is dropped and add the CheckpointProtectionTableFeature.\n   * 4) Create a checkpoint after the protocol downgrade. This is optional and it is used to allow\n   *    log replay from checkpoint 3 to the protocol downgrade commit. This is for clients that do\n   *    not support the dropped feature and choose to validate against the protocols of all commits\n   *    used in the replay of a snapshot instead of only validating against the final resulting\n   *    protocol.\n   *\n   */\n  private def executeDropFeatureWithCheckpointProtection(\n      sparkSession: SparkSession,\n      removableFeature: TableFeature with RemovableFeature): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.dropFeatureWithCheckpointProtection\") {\n      var startTimeNs = System.nanoTime()\n      val status = removableFeature\n        .preDowngradeCommand(table)\n        .removeFeatureTracesIfNeeded(sparkSession)\n\n      // Create and validate the barrier checkpoints. The checkpoint are created on top of\n      // empty commits. However, this is not guaranteed. Other txns might interleave the empty\n      // commit. As a result the checkpoint could be created on top of an unrelated non-empty\n      // commit. This is not a problem. We only care about the checkpoint being created after the\n      // pre-downgrade process and before the protocol downgrade commit.\n      // Furthermore, each checkpoint validation requires a roundtrip to the object store.\n      // Could be optimized, if required, by performing a single list operation and checking\n      // whether NUMBER_OF_BARRIER_CHECKPOINTS exist after the pre-downgrade commit.\n      val historyBarrierIsRequired = removableFeature.requiresHistoryProtection\n      if (historyBarrierIsRequired) {\n        (1 to DropTableFeatureUtils.NUMBER_OF_BARRIER_CHECKPOINTS).foreach { _ =>\n          // This call also cleans up the logs. In most of the cases we should be able to truncate\n          // the history of a previous drop feature operation.\n          if (!createEmptyCommitAndCheckpoint(table, startTimeNs, retryOnFailure = true)) {\n            throw DeltaErrors.dropTableFeatureCheckpointFailedException(removableFeature.name)\n          }\n          startTimeNs = System.nanoTime()\n        }\n      }\n\n      val startSnapshotOpt = status.lastCommitVersionOpt.map(table.getSnapshotAt(_))\n      val txn = table.startTransaction(snapshotOpt = startSnapshotOpt)\n      val snapshot = txn.snapshot\n\n      // Verify whether all requirements hold before performing the protocol downgrade.\n      // If any concurrent transactions interfere with the protocol downgrade txn we\n      // revalidate the requirements against the snapshot of the winning txn.\n      if (!removableFeature.validateDropInvariants(table, snapshot)) {\n        TransactionExecutionObserver.getObserver.transactionAborted()\n        throw DeltaErrors.dropTableFeatureConflictRevalidationFailed()\n      }\n      val metadataWithNewConfiguration = DropTableFeatureUtils\n        .getDowngradedProtocolMetadata(removableFeature, txn.metadata)\n\n      if (historyBarrierIsRequired) {\n        val newProtocol = txn.protocol\n          .denormalized\n          .withFeature(CheckpointProtectionTableFeature)\n          .removeFeature(removableFeature)\n\n        txn.updateProtocol(newProtocol)\n\n        val newMetadata = CheckpointProtectionTableFeature.metadataWithCheckpointProtection(\n          metadataWithNewConfiguration, txn.readVersion + 1L)\n        txn.updateMetadata(newMetadata)\n      } else {\n        txn.updateProtocol(txn.protocol.removeFeature(removableFeature))\n        txn.updateMetadata(metadataWithNewConfiguration)\n      }\n\n      val commitActions = removableFeature.actionsToIncludeAtDowngradeCommit(txn.snapshot)\n      txn.commit(commitActions, DeltaOperations.DropTableFeature(featureName, false))\n\n      // This is a protected checkpoint.\n      if (historyBarrierIsRequired) createCheckpointWithRetries(table, System.nanoTime())\n      recordDeltaEvent(\n        deltaLog = deltaLog,\n        opType = \"dropFeatureCompleted.withCheckpointProtection\",\n        data = Map(\"droppedFeature\" -> removableFeature.name))\n      Nil\n    }\n  }\n\n  /**\n   * Checks if the `removableFeature` is a requirement for some other feature that is enabled on the\n   * table. In such a scenario, we need to fail the drop feature command. The dependent features\n   * needs to be dropped first before this `removableFeature` can be removed.\n   */\n  private def dependentFeatureCheck(\n      removableFeature: TableFeature,\n      protocol: Protocol): Unit = {\n    val dependentFeatures = TableFeature.getDependentFeatures(removableFeature)\n    if (dependentFeatures.nonEmpty) {\n      val dependentFeaturesInProtocol = dependentFeatures.filter(protocol.isFeatureSupported)\n      if (dependentFeaturesInProtocol.nonEmpty) {\n        val dependentFeatureNames = dependentFeaturesInProtocol.map(_.name)\n        throw DeltaErrors.dropTableFeatureFailedBecauseOfDependentFeatures(\n          removableFeature.name, dependentFeatureNames.toSeq)\n      }\n    }\n  }\n}\n\n/**\n * A command that add columns to a Delta table.\n * The syntax of using this command in SQL is:\n * {{{\n *   ALTER TABLE table_identifier\n *   ADD COLUMNS (col_name data_type [COMMENT col_comment], ...);\n * }}}\n*/\ncase class AlterTableAddColumnsDeltaCommand(\n    table: DeltaTableV2,\n    colsToAddWithPosition: Seq[QualifiedColType])\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.addColumns\") {\n      val txn = startTransaction()\n\n      if (SchemaUtils.filterRecursively(\n            StructType(colsToAddWithPosition.map {\n              case QualifiedColTypeWithPosition(_, column, _) => column\n            }), true)(!_.nullable).nonEmpty) {\n        throw DeltaErrors.operationNotSupportedException(\"NOT NULL in ALTER TABLE ADD COLUMNS\")\n      }\n\n      // TODO: remove this after auto cache refresh is merged.\n      table.tableIdentifier.foreach { identifier =>\n        try sparkSession.catalog.uncacheTable(identifier) catch {\n          case NonFatal(e) =>\n            log.warn(s\"Exception when attempting to uncache table $identifier\", e)\n        }\n      }\n\n      val metadata = txn.metadata\n      val oldSchema = metadata.schema\n\n      val resolver = sparkSession.sessionState.conf.resolver\n      val newSchema = colsToAddWithPosition.foldLeft(oldSchema) {\n        case (schema, QualifiedColTypeWithPosition(columnPath, column, None)) =>\n          val parentPosition = SchemaUtils.findColumnPosition(columnPath, schema, resolver)\n          val insertPosition = SchemaUtils.getNestedTypeFromPosition(schema, parentPosition) match {\n            case s: StructType => s.size\n            case other =>\n               throw DeltaErrors.addColumnParentNotStructException(column, other)\n          }\n          SchemaUtils.addColumn(schema, column, parentPosition :+ insertPosition)\n        case (schema, QualifiedColTypeWithPosition(columnPath, column, Some(_: First))) =>\n          val parentPosition = SchemaUtils.findColumnPosition(columnPath, schema, resolver)\n          SchemaUtils.addColumn(schema, column, parentPosition :+ 0)\n        case (schema,\n        QualifiedColTypeWithPosition(columnPath, column, Some(after: After))) =>\n          val prevPosition =\n            SchemaUtils.findColumnPosition(columnPath :+ after.column, schema, resolver)\n          val position = prevPosition.init :+ (prevPosition.last + 1)\n          SchemaUtils.addColumn(schema, column, position)\n      }\n\n      SchemaMergingUtils.checkColumnNameDuplication(newSchema, \"in adding columns\")\n      SchemaUtils.checkSchemaFieldNames(newSchema, metadata.columnMappingMode)\n\n      val newMetadata = metadata.copy(schemaString = newSchema.json)\n      txn.updateMetadata(newMetadata)\n      txn.commit(Nil, DeltaOperations.AddColumns(\n        colsToAddWithPosition.map {\n          case QualifiedColTypeWithPosition(path, col, colPosition) =>\n            DeltaOperations.QualifiedColTypeWithPositionForLog(\n              path, col, colPosition.map(_.toString))\n        }))\n\n      Seq.empty[Row]\n    }\n  }\n\n  object QualifiedColTypeWithPosition {\n\n    private def toV2Position(input: Any): ColumnPosition = {\n      input.asInstanceOf[org.apache.spark.sql.catalyst.analysis.FieldPosition].position\n    }\n\n    def unapply(\n        col: QualifiedColType): Option[(Seq[String], StructField, Option[ColumnPosition])] = {\n      val builder = new MetadataBuilder\n      col.comment.foreach(builder.putString(\"comment\", _))\n\n      val field = StructField(col.name.last, col.dataType, col.nullable, builder.build())\n\n      QualifiedColTypeShims.getDefaultValueStr(col).map { defaultStr =>\n        Some((col.name.init,\n          field.withCurrentDefaultValue(defaultStr),\n          col.position.map(toV2Position)))\n      }.getOrElse {\n        Some((col.name.init, field, col.position.map(toV2Position)))\n      }\n    }\n  }\n}\n\n/**\n * A command that drop columns from a Delta table.\n * The syntax of using this command in SQL is:\n * {{{\n *   ALTER TABLE table_identifier\n *   DROP COLUMN(S) (col_name_1, col_name_2, ...);\n * }}}\n */\ncase class AlterTableDropColumnsDeltaCommand(\n    table: DeltaTableV2,\n    columnsToDrop: Seq[Seq[String]])\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    if (!sparkSession.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ALTER_TABLE_DROP_COLUMN_ENABLED)) {\n      // this featue is still behind the flag and not ready for release.\n      throw DeltaErrors.dropColumnNotSupported(suggestUpgrade = false)\n    }\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.dropColumns\") {\n      val txn = startTransaction()\n      val metadata = txn.metadata\n      if (txn.metadata.columnMappingMode == NoMapping) {\n        throw DeltaErrors.dropColumnNotSupported(suggestUpgrade = true)\n      }\n      val newSchema = columnsToDrop.foldLeft(metadata.schema) { case (schema, columnPath) =>\n        val parentPosition =\n          SchemaUtils.findColumnPosition(\n            columnPath, schema, sparkSession.sessionState.conf.resolver)\n        SchemaUtils.dropColumn(schema, parentPosition)._1\n      }\n\n      // in case any of the dropped column is partition columns\n      val droppedColumnSet = columnsToDrop.map(UnresolvedAttribute(_).name).toSet\n      val droppingPartitionCols = metadata.partitionColumns.filter(droppedColumnSet.contains(_))\n      if (droppingPartitionCols.nonEmpty) {\n        throw DeltaErrors.dropPartitionColumnNotSupported(droppingPartitionCols)\n      }\n      // Disallow dropping clustering columns.\n      val clusteringCols = ClusteringColumnInfo.extractLogicalNames(txn.snapshot)\n      val droppingClusteringCols = clusteringCols.filter(droppedColumnSet.contains(_))\n      if (droppingClusteringCols.nonEmpty) {\n        throw DeltaErrors.dropClusteringColumnNotSupported(droppingClusteringCols)\n      }\n      // Updates the delta statistics column list by removing the dropped columns from it.\n      val newConfiguration = metadata.configuration ++\n        StatisticsCollection.dropDeltaStatsColumns(metadata, columnsToDrop)\n      val newMetadata = metadata.copy(\n        schemaString = newSchema.json,\n        configuration = newConfiguration\n      )\n      columnsToDrop.foreach { columnParts =>\n        checkDependentExpressions(sparkSession, columnParts, metadata, txn.protocol)\n      }\n\n      txn.updateMetadata(newMetadata)\n      txn.commit(Nil, DeltaOperations.DropColumns(columnsToDrop))\n\n      Seq.empty[Row]\n    }\n  }\n}\n\ncase class DeltaChangeColumnSpec(\n    columnPath: Seq[String],\n    columnName: String,\n    newColumn: StructField,\n    colPosition: Option[ColumnPosition],\n    syncIdentity: Boolean)\n\n/**\n * A command to change the column for a Delta table, support changing the comment of a column and\n * reordering columns.\n *\n * The syntax of using this command in SQL is:\n * {{{\n *   ALTER TABLE table_identifier\n *   CHANGE [COLUMN] column_old_name column_new_name column_dataType [COMMENT column_comment]\n *   [FIRST | AFTER column_name];\n * }}}\n */\ncase class AlterTableChangeColumnDeltaCommand(\n    table: DeltaTableV2,\n    columnChanges: Seq[DeltaChangeColumnSpec])\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.changeColumns\") {\n      val txn = startTransaction()\n      val metadata = txn.metadata\n      val bypassCharVarcharToStringFix =\n        sparkSession.conf.get(DeltaSQLConf.DELTA_BYPASS_CHARVARCHAR_TO_STRING_FIX)\n      val oldSchema = metadata.schema\n      val resolver = sparkSession.sessionState.conf.resolver\n\n      columnChanges.foreach(change => {\n        val columnName = change.columnName\n        val columnPath = change.columnPath\n        val newColumn = change.newColumn\n        if (newColumn.name != columnName) {\n          // need to validate the changes if the column is renamed\n          checkDependentExpressions(\n            sparkSession, columnPath :+ columnName, metadata, txn.protocol)\n        }\n        // Verify that the columnName provided actually exists in the schema\n        SchemaUtils.findColumnPosition(columnPath :+ columnName, oldSchema, resolver)\n      })\n\n      def transformSchemaOnce(prevSchema: StructType, change: DeltaChangeColumnSpec) = {\n        val columnPath = change.columnPath\n        val columnName = change.columnName\n        val newColumn = change.newColumn\n        transformSchema(prevSchema, Some(columnName)) {\n          case (`columnPath`, struct @ StructType(fields), _) =>\n            val oldColumn = struct(columnName)\n            verifyColumnChange(change, sparkSession, oldColumn, resolver, txn)\n\n            val newField = {\n              if (change.syncIdentity) {\n                assert(oldColumn == newColumn)\n                val df = txn.snapshot.deltaLog.createDataFrame(txn.snapshot, txn.filterFiles())\n                val allowLoweringHighWaterMarkForSyncIdentity = sparkSession.conf\n                  .get(DeltaSQLConf.DELTA_IDENTITY_ALLOW_SYNC_IDENTITY_TO_LOWER_HIGH_WATER_MARK)\n                val field = IdentityColumn.syncIdentity(\n                  deltaLog,\n                  newColumn,\n                  df,\n                  allowLoweringHighWaterMarkForSyncIdentity\n                )\n                txn.setSyncIdentity()\n                txn.readWholeTable()\n                field\n              } else {\n                // Take the name, comment, nullability and data type from newField\n                // It's crucial to keep the old column's metadata, which may contain column mapping\n                // metadata.\n                var result = newColumn.getComment().map(oldColumn.withComment).getOrElse(oldColumn)\n                // Apply the current default value as well, if any.\n                result = newColumn.getCurrentDefaultValue() match {\n                  case Some(newDefaultValue) => result.withCurrentDefaultValue(newDefaultValue)\n                  case None => result.clearCurrentDefaultValue()\n                }\n                result = SchemaUtils.changeFieldDataTypeCharVarcharSafe(result, newColumn, resolver)\n                result.copy(\n                  name = newColumn.name,\n                  nullable = newColumn.nullable)\n              }\n            }\n\n            // Replace existing field with new field\n            val newFieldList = fields.map { field =>\n              if (DeltaColumnMapping.getPhysicalName(field) ==\n                DeltaColumnMapping.getPhysicalName(newField)) {\n                newField\n              } else field\n            }\n\n            // Reorder new field to correct position if necessary\n            StructType(change.colPosition.map { position =>\n              reorderFieldList(columnName, struct, newFieldList, newField, position, resolver)\n            }.getOrElse(newFieldList.toSeq))\n\n          case (`columnPath`, m: MapType, _) if columnName == \"key\" =>\n            val originalField = StructField(columnName, m.keyType, nullable = false)\n            verifyMapArrayChange(change, sparkSession, originalField, resolver, txn)\n            val fieldWithNewDataType = SchemaUtils.changeFieldDataTypeCharVarcharSafe(\n              originalField, newColumn, resolver)\n            m.copy(keyType = fieldWithNewDataType.dataType)\n\n          case (`columnPath`, m: MapType, _) if columnName == \"value\" =>\n            val originalField = StructField(columnName, m.valueType, nullable = m.valueContainsNull)\n            verifyMapArrayChange(change, sparkSession, originalField, resolver, txn)\n            val fieldWithNewDataType = SchemaUtils.changeFieldDataTypeCharVarcharSafe(\n              originalField, newColumn, resolver)\n            m.copy(valueType = fieldWithNewDataType.dataType)\n\n          case (`columnPath`, a: ArrayType, _) if columnName == \"element\" =>\n            val originalField = StructField(columnName, a.elementType, nullable = a.containsNull)\n            verifyMapArrayChange(change, sparkSession, originalField, resolver, txn)\n            val fieldWithNewDataType = SchemaUtils.changeFieldDataTypeCharVarcharSafe(\n              originalField, newColumn, resolver)\n            a.copy(elementType = fieldWithNewDataType.dataType)\n\n          case (_, other @ (_: StructType | _: ArrayType | _: MapType), _) => other\n        }\n      }\n\n      val transformedSchema = columnChanges.foldLeft(oldSchema)(transformSchemaOnce)\n\n      // Validate clustering columns remain in stats schema after column reordering\n      validateClusteringColumnsAfterReordering(sparkSession, txn, columnChanges)\n\n      val newSchemaWithTypeWideningMetadata =\n        TypeWideningMetadata.addTypeWideningMetadata(\n          txn,\n          schema = transformedSchema,\n          oldSchema = metadata.schema)\n\n      val metadataWithNewSchema = metadata.copy(\n        schemaString = newSchemaWithTypeWideningMetadata.json)\n\n      def updateMetadataOnce(prevMetadata: actions.Metadata, change: DeltaChangeColumnSpec) = {\n        val columnPath = change.columnPath\n        val columnName = change.columnName\n        val newColumn = change.newColumn\n        // update `partitionColumns` if the changed column is a partition column\n        val newPartitionColumns = if (columnPath.isEmpty) {\n          metadata.partitionColumns.map { partCol =>\n            if (partCol == columnName) newColumn.name else partCol\n          }\n        } else metadata.partitionColumns\n\n        val oldColumnPath = columnPath :+ columnName\n        val newColumnPath = columnPath :+ newColumn.name\n        // Rename the column in the delta statistics columns configuration, if present.\n        val newConfiguration = metadata.configuration ++\n          StatisticsCollection.renameDeltaStatsColumn(metadata, oldColumnPath, newColumnPath)\n\n        val updatedMetadata = prevMetadata.copy(\n          partitionColumns = newPartitionColumns,\n          configuration = newConfiguration\n        )\n\n        updatedMetadata\n      }\n\n      val newMetadata = columnChanges.foldLeft(metadataWithNewSchema)(updateMetadataOnce)\n\n\n      txn.updateMetadata(newMetadata)\n\n      def getDeltaChangeColumnOperation(change: DeltaChangeColumnSpec) =\n        DeltaOperations.ChangeColumn(\n          change.columnPath,\n          change.columnName,\n          change.newColumn,\n          change.colPosition.map(_.toString))\n\n      val operation = if (columnChanges.size == 1) {\n        val change = columnChanges.head\n        val columnName = change.columnName\n        val newColumn = change.newColumn\n        if (newColumn.name != columnName) {\n          val columnPath = change.columnPath\n          val oldColumnPath = columnPath :+ columnName\n          val newColumnPath = columnPath :+ newColumn.name\n          // record column rename separately\n          DeltaOperations.RenameColumn(oldColumnPath, newColumnPath)\n        } else {\n          getDeltaChangeColumnOperation(change)\n        }\n      } else {\n        val changes = columnChanges.map(getDeltaChangeColumnOperation)\n        DeltaOperations.ChangeColumns(changes)\n      }\n      txn.commit(Nil, operation)\n\n      Seq.empty[Row]\n    }\n  }\n\n  /**\n   * Reorder the given fieldList to place `field` at the given `position` in `fieldList`\n   *\n   * @param columnName Name of the column being reordered\n   * @param struct The initial StructType with the original field at its original position\n   * @param fieldList List of fields with the changed field in the original position\n   * @param field The field that is to be added\n   * @param position Position where the field is to be placed\n   * @return Returns a new list of fields with the changed field in the new position\n   */\n  private def reorderFieldList(\n      columnName: String,\n      struct: StructType,\n      fieldList: Array[StructField],\n      field: StructField,\n      position: ColumnPosition,\n      resolver: Resolver): Seq[StructField] = {\n    val startIndex = struct.fieldIndex(columnName)\n    val filtered = fieldList.filterNot(_.name == columnName)\n    val newFieldList = position match {\n      case _: First =>\n        field +: filtered\n\n      case after: After if after.column() == columnName =>\n        filtered.slice(0, startIndex)++\n          Seq(field) ++\n          filtered.slice(startIndex, filtered.length)\n\n      case after: After =>\n        val endIndex = filtered.indexWhere(i => resolver(i.name, after.column()))\n        if (endIndex < 0) {\n          throw DeltaErrors.columnNotInSchemaException(after.column(), struct)\n        }\n\n        filtered.slice(0, endIndex + 1) ++\n          Seq(field) ++\n          filtered.slice(endIndex + 1, filtered.length)\n    }\n    newFieldList.toSeq\n  }\n\n  /**\n   * Validates that clustering columns remain in the stats schema after column reordering.\n   *\n   * This validation ensures that when a user executes `ALTER TABLE ALTER COLUMN col1 AFTER col2`,\n   * all clustering columns that were in the stats schema before the reordering remain in the\n   * stats schema after the operation. When DELTA_LIQUID_ALTER_COLUMN_AFTER_STATS_SCHEMA_CHECK\n   * is enabled, the validation runs and throws an error if any clustering column would lose\n   * stats collection due to position-based indexing. When disabled (default), no validation\n   * is performed and stats collection may follow position-based indexing rules.\n   *\n   * @param spark The SparkSession\n   * @param txn The transaction\n   * @param columnChanges The column changes being applied\n   */\n  private def validateClusteringColumnsAfterReordering(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      columnChanges: Seq[DeltaChangeColumnSpec]): Unit = {\n    if (!spark.conf.get(\n      DeltaSQLConf.DELTA_LIQUID_ALTER_COLUMN_AFTER_STATS_SCHEMA_CHECK)) {\n      return\n    }\n    // Only validate if table supports clustering and check is enabled\n    if (ClusteredTableUtils.isSupported(txn.snapshot.protocol) &&\n        columnChanges.exists(_.colPosition.isDefined)) {\n      val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(txn.snapshot)\n      if (clusteringColumns.nonEmpty) {\n        // Validate that prior stats schema is preserved (clustering columns remain in stats)\n        ClusteredTableUtils.validateClusteringColumnsInStatsSchema(\n          txn.snapshot, clusteringColumns)\n      }\n    }\n  }\n\n  /**\n   * Given two columns, verify whether replacing the original column with the new column is a valid\n   * operation.\n   *\n   * Note that this requires a full table scan in the case of SET NOT NULL to verify that all\n   * existing values are valid.\n   *\n   * @param change Information about the column change\n   * @param originalField The existing column\n   */\n  private def verifyColumnChange(\n      change: DeltaChangeColumnSpec,\n      spark: SparkSession,\n      originalField: StructField,\n      resolver: Resolver,\n      txn: OptimisticTransaction): Unit = {\n    val columnPath = change.columnPath\n    val columnName = change.columnName\n    val newColumn = change.newColumn\n    originalField.dataType match {\n      case same if same == newColumn.dataType =>\n      // just changing comment or position so this is fine\n      case s: StructType if s != newColumn.dataType =>\n        val fieldName = UnresolvedAttribute(columnPath :+ columnName).name\n        throw DeltaErrors.cannotUpdateStructField(table.name(), fieldName)\n      case m: MapType if m != newColumn.dataType =>\n        val fieldName = UnresolvedAttribute(columnPath :+ columnName).name\n        throw DeltaErrors.cannotUpdateMapField(table.name(), fieldName)\n      case a: ArrayType if a != newColumn.dataType =>\n        val fieldName = UnresolvedAttribute(columnPath :+ columnName).name\n        throw DeltaErrors.cannotUpdateArrayField(table.name(), fieldName)\n      case _: AtomicType =>\n      // update is okay\n      case o =>\n        throw DeltaErrors.cannotUpdateOtherField(table.name(), o)\n    }\n\n    // Analyzer already validates the char/varchar type change of ALTER COLUMN in\n    // `CheckAnalysis.checkAlterTableCommand`. We should normalize char/varchar type to string type\n    // first (original data type is already normalized as we store char/varchar as string type with\n    // special metadata in the Delta log), then apply Delta-specific checks.\n    val newType = CharVarcharUtils.replaceCharVarcharWithString(newColumn.dataType)\n    if (SchemaUtils.canChangeDataType(\n        originalField.dataType,\n        newType,\n        resolver,\n        txn.metadata.columnMappingMode,\n        columnPath :+ originalField.name,\n        allowTypeWidening = TypeWidening.isEnabled(txn.protocol, txn.metadata)\n      ).nonEmpty) {\n      throw DeltaErrors.alterTableChangeColumnException(\n        fieldPath = UnresolvedAttribute(columnPath :+ originalField.name).name,\n        oldField = originalField,\n        newField = newColumn\n      )\n    }\n\n    if (columnName != newColumn.name) {\n      if (txn.metadata.columnMappingMode == NoMapping) {\n        throw DeltaErrors.columnRenameNotSupported\n      }\n    }\n\n    if (originalField.dataType != newType) {\n      checkDependentExpressions(\n        spark, columnPath :+ columnName, txn.metadata, txn.protocol)\n    }\n\n    if (originalField.nullable && !newColumn.nullable) {\n      throw DeltaErrors.alterTableChangeColumnException(\n        fieldPath = UnresolvedAttribute(columnPath :+ originalField.name).name,\n        oldField = originalField,\n        newField = newColumn\n      )\n    }\n  }\n\n  /**\n   * Verify whether replacing the original map key/value or array element with a new data type is a\n   * valid operation.\n   *\n   * @param change Information about the column change\n   * @param originalField the original map key/value or array element to update.\n   */\n  private def verifyMapArrayChange(\n      change: DeltaChangeColumnSpec,\n      spark: SparkSession,\n      originalField: StructField,\n      resolver: Resolver,\n      txn: OptimisticTransaction): Unit = {\n    val columnPath = change.columnPath\n    val columnName = change.columnName\n    val newColumn = change.newColumn\n    // Map key/value and array element can't have comments.\n    if (newColumn.getComment().nonEmpty) {\n      throw DeltaErrors.addCommentToMapArrayException(\n        fieldPath = UnresolvedAttribute(columnPath :+ columnName).name\n      )\n    }\n    // Changing the nullability of map key/value or array element isn't supported.\n    if (originalField.nullable != newColumn.nullable) {\n      throw DeltaErrors.alterTableChangeColumnException(\n        fieldPath = UnresolvedAttribute(columnPath :+ originalField.name).name,\n        oldField = originalField,\n        newField = newColumn\n      )\n    }\n    verifyColumnChange(change, spark, originalField, resolver, txn)\n  }\n}\n\n/**\n * A command to replace columns for a Delta table, support changing the comment of a column,\n * reordering columns, and loosening nullabilities.\n *\n * The syntax of using this command in SQL is:\n * {{{\n *   ALTER TABLE table_identifier REPLACE COLUMNS (col_spec[, col_spec ...]);\n * }}}\n */\ncase class AlterTableReplaceColumnsDeltaCommand(\n    table: DeltaTableV2,\n    columns: Seq[StructField])\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    recordDeltaOperation(table.deltaLog, \"delta.ddl.alter.replaceColumns\") {\n      val txn = startTransaction()\n\n      val metadata = txn.metadata\n      val existingSchema = metadata.schema\n\n      if (ColumnWithDefaultExprUtils.hasIdentityColumn(table.initialSnapshot.schema)) {\n        throw DeltaErrors.identityColumnReplaceColumnsNotSupported()\n      }\n\n      val resolver = sparkSession.sessionState.conf.resolver\n      val changingSchema = StructType(columns)\n\n      SchemaUtils.canChangeDataType(\n        existingSchema,\n        changingSchema,\n        resolver,\n        txn.metadata.columnMappingMode,\n        allowTypeWidening = TypeWidening.isEnabled(txn.protocol, txn.metadata),\n        failOnAmbiguousChanges = true\n      ).foreach { operation =>\n        throw DeltaErrors.alterTableReplaceColumnsException(\n          existingSchema, changingSchema, operation)\n      }\n\n      val newSchema = SchemaUtils.changeDataType(existingSchema, changingSchema, resolver)\n        .asInstanceOf[StructType]\n\n      SchemaMergingUtils.checkColumnNameDuplication(newSchema, \"in replacing columns\")\n      SchemaUtils.checkSchemaFieldNames(newSchema, metadata.columnMappingMode)\n\n      val newSchemaWithTypeWideningMetadata = TypeWideningMetadata.addTypeWideningMetadata(\n        txn,\n        schema = newSchema,\n        oldSchema = existingSchema\n      )\n\n      val newMetadata = metadata.copy(schemaString = newSchemaWithTypeWideningMetadata.json)\n      txn.updateMetadata(newMetadata)\n      txn.commit(Nil, DeltaOperations.ReplaceColumns(columns))\n\n      Nil\n    }\n  }\n}\n\n/**\n * A command to change the location of a Delta table. Effectively, this only changes the symlink\n * in the Hive MetaStore from one Delta table to another.\n *\n * This command errors out if the new location is not a Delta table. By default, the new Delta\n * table must have the same schema as the old table, but we have a SQL conf that allows users\n * to bypass this schema check.\n *\n * The syntax of using this command in SQL is:\n * {{{\n *   ALTER TABLE table_identifier SET LOCATION 'path/to/new/delta/table';\n * }}}\n */\ncase class AlterTableSetLocationDeltaCommand(\n    table: DeltaTableV2,\n    location: String)\n  extends LeafRunnableCommand\n    with AlterDeltaTableCommand\n    with IgnoreCachedData {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val catalog = sparkSession.sessionState.catalog\n    if (table.catalogTable.isEmpty) {\n      throw DeltaErrors.setLocationNotSupportedOnPathIdentifiers()\n    }\n    val catalogTable = table.catalogTable.get\n    val locUri = CatalogUtils.stringToURI(location)\n\n    val oldTable = table.update()\n    if (oldTable.version == -1) {\n      throw DeltaErrors.notADeltaTableException(table.name())\n    }\n    val oldMetadata = oldTable.metadata\n\n    var updatedTable = catalogTable.withNewStorage(locationUri = Some(locUri))\n\n    val (_, newTable) =\n      DeltaLog.forTableWithSnapshot(sparkSession, updatedTable, options = Map.empty[String, String])\n    if (newTable.version == -1) {\n      throw DeltaErrors.notADeltaTableException(DeltaTableIdentifier(path = Some(location)))\n    }\n    val newMetadata = newTable.metadata\n    val bypassSchemaCheck = sparkSession.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ALTER_LOCATION_BYPASS_SCHEMA_CHECK)\n\n    if (!bypassSchemaCheck && !schemasEqual(sparkSession, oldMetadata, newMetadata)) {\n      throw DeltaErrors.alterTableSetLocationSchemaMismatchException(\n        oldMetadata.schema, newMetadata.schema)\n    }\n    catalog.alterTable(updatedTable)\n\n    Seq.empty[Row]\n  }\n\n  private def schemasEqual(\n      sparkSession: SparkSession,\n      oldMetadata: actions.Metadata, newMetadata: actions.Metadata): Boolean = {\n    DeltaTableUtils.removeInternalDeltaMetadata(sparkSession, oldMetadata.schema) ==\n      DeltaTableUtils.removeInternalDeltaMetadata(sparkSession, newMetadata.schema) &&\n      DeltaTableUtils.removeInternalDeltaMetadata(sparkSession, oldMetadata.partitionSchema) ==\n        DeltaTableUtils.removeInternalDeltaMetadata(sparkSession, newMetadata.partitionSchema)\n  }\n}\n\ntrait AlterTableConstraintDeltaCommand\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData  {\n\n  def getConstraintWithName(\n      table: DeltaTableV2,\n      name: String,\n      metadata: actions.Metadata,\n      sparkSession: SparkSession): Option[String] = {\n    val expr = Constraints.getExprTextByName(name, metadata, sparkSession)\n    if (expr.nonEmpty) {\n      return expr\n    }\n    None\n  }\n}\n\n/**\n * Command to add a constraint to a Delta table. Currently only CHECK constraints are supported.\n *\n * Adding a constraint will scan all data in the table to verify the constraint currently holds.\n *\n * @param table The table to which the constraint should be added.\n * @param name The name of the new constraint.\n * @param exprText The contents of the new CHECK constraint, to be parsed and evaluated.\n */\ncase class AlterTableAddConstraintDeltaCommand(\n    table: DeltaTableV2,\n    name: String,\n    exprText: String)\n  extends AlterTableConstraintDeltaCommand {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    if (name == CharVarcharConstraint.INVARIANT_NAME) {\n      throw DeltaErrors.invalidConstraintName(name)\n    }\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.addConstraint\") {\n      val txn = startTransaction()\n\n      getConstraintWithName(table, name, txn.metadata, sparkSession).foreach { oldExpr =>\n        throw DeltaErrors.constraintAlreadyExists(name, oldExpr)\n      }\n\n      val newMetadata = txn.metadata.copy(\n        configuration = txn.metadata.configuration +\n          (Constraints.checkConstraintPropertyName(name) -> exprText)\n      )\n\n      val df = txn.snapshot.deltaLog.createDataFrame(txn.snapshot, txn.filterFiles())\n      val unresolvedExpr = sparkSession.sessionState.sqlParser.parseExpression(exprText)\n\n      try {\n        df.where(Column(unresolvedExpr)).queryExecution.analyzed\n      } catch {\n        case a: AnalysisException\n            if a.errorClass.contains(\"DATATYPE_MISMATCH.FILTER_NOT_BOOLEAN\") =>\n          throw DeltaErrors.checkConstraintNotBoolean(name, exprText)\n        case a: AnalysisException =>\n          // Strip out the context of the DataFrame that was used to analyze the expression.\n          throw a.copy(context = Array.empty)\n      }\n\n      Constraints.validateCheckConstraints(\n        sparkSession,\n        Seq(Constraints.Check(name, unresolvedExpr)),\n        deltaLog,\n        txn.metadata.schema\n      )\n\n      logInfo(log\"Checking that ${MDC(DeltaLogKeys.EXPR, exprText)} \" +\n        log\"is satisfied for existing data. This will require a full table scan.\")\n      recordDeltaOperation(\n          txn.snapshot.deltaLog,\n          \"delta.ddl.alter.addConstraint.checkExisting\") {\n        val n = df.where(Column(Or(Not(unresolvedExpr), IsUnknown(unresolvedExpr)))).count()\n\n        if (n > 0) {\n          throw DeltaErrors.newCheckConstraintViolated(n, table.name(), exprText)\n        }\n      }\n\n      txn.commit(newMetadata :: Nil, DeltaOperations.AddConstraint(name, exprText))\n    }\n    Seq()\n  }\n}\n\n/**\n * Command to drop a constraint from a Delta table. No-op if a constraint with the given name\n * doesn't exist.\n *\n * Currently only CHECK constraints are supported.\n *\n * @param table The table from which the constraint should be dropped\n * @param name The name of the constraint to drop\n */\ncase class AlterTableDropConstraintDeltaCommand(\n    table: DeltaTableV2,\n    name: String,\n    ifExists: Boolean)\n  extends AlterTableConstraintDeltaCommand {\n\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.dropConstraint\") {\n      val txn = startTransaction()\n\n      val oldExprText = Constraints.getExprTextByName(name, txn.metadata, sparkSession)\n      if (oldExprText.isEmpty && !ifExists && !sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_ASSUMES_DROP_CONSTRAINT_IF_EXISTS)) {\n        val quotedTableName = table.getTableIdentifierIfExists.map(_.quotedString)\n          .orElse(table.catalogTable.map(_.identifier.quotedString))\n          .getOrElse(table.name())\n        throw DeltaErrors.nonexistentConstraint(name, quotedTableName)\n      }\n\n      val newMetadata = txn.metadata.copy(\n        configuration = txn.metadata.configuration - Constraints.checkConstraintPropertyName(name))\n\n      txn.commit(newMetadata :: Nil, DeltaOperations.DropConstraint(name, oldExprText))\n    }\n\n    Seq()\n  }\n}\n\n/**\n * Command for altering clustering columns for clustered tables.\n * - ALTER TABLE .. CLUSTER BY (col1, col2, ...)\n * - ALTER TABLE .. CLUSTER BY NONE\n *\n * Note that the given `clusteringColumns` are empty when CLUSTER BY NONE is specified.\n * Also, `clusteringColumns` are validated (e.g., duplication / existence check) in\n * DeltaCatalog.alterTable().\n */\ncase class AlterTableClusterByDeltaCommand(\n    table: DeltaTableV2,\n    clusteringColumns: Seq[Seq[String]])\n  extends LeafRunnableCommand with AlterDeltaTableCommand with IgnoreCachedData {\n  override def run(sparkSession: SparkSession): Seq[Row] = {\n    val deltaLog = table.deltaLog\n    ClusteredTableUtils.validateNumClusteringColumns(clusteringColumns, Some(deltaLog))\n    // If the target table is not a clustered table and there are no clustering columns being added\n    // (CLUSTER BY NONE), do not convert the table into a clustered table.\n    val snapshot = table.update()\n    if (clusteringColumns.isEmpty &&\n      !ClusteredTableUtils.isSupported(snapshot.protocol)) {\n      logInfo(log\"Skipping ALTER TABLE CLUSTER BY NONE on a non-clustered table: \" +\n        log\"${MDC(DeltaLogKeys.TABLE_NAME, table.name())}.\")\n      recordDeltaEvent(\n        deltaLog,\n        \"delta.ddl.alter.clusterBy\",\n        data = Map(\n          \"isClusterByNoneSkipped\" -> true,\n          \"isNewClusteredTable\" -> false,\n          \"oldColumnsCount\" -> 0,\n          \"newColumnsCount\" -> 0))\n      return Seq.empty\n    }\n    recordDeltaOperation(deltaLog, \"delta.ddl.alter.clusterBy\") {\n      val txn = startTransaction()\n\n      val clusteringColsLogicalNames = ClusteringColumnInfo.extractLogicalNames(txn.snapshot)\n      val oldLogicalClusteringColumnsString = clusteringColsLogicalNames.mkString(\",\")\n      val oldColumnsCount = clusteringColsLogicalNames.size\n\n      val newLogicalClusteringColumns = clusteringColumns.map(FieldReference(_).toString)\n      ClusteredTableUtils.validateClusteringColumnsInStatsSchema(\n        txn.snapshot, newLogicalClusteringColumns)\n\n      val newDomainMetadata =\n        ClusteredTableUtils\n          .getClusteringDomainMetadataForAlterTableClusterBy(newLogicalClusteringColumns, txn)\n\n      recordDeltaEvent(\n        deltaLog,\n        \"delta.ddl.alter.clusterBy\",\n        data = Map(\n          \"isClusterByNoneSkipped\" -> false,\n          \"isNewClusteredTable\" -> !ClusteredTableUtils.isSupported(txn.protocol),\n          \"oldColumnsCount\" -> oldColumnsCount, \"newColumnsCount\" -> clusteringColumns.size))\n      // Add clustered table properties if the current table is not clustered.\n      // [[DeltaCatalog.alterTable]] already ensures that the table is not partitioned.\n      if (!ClusteredTableUtils.isSupported(txn.protocol)) {\n        txn.updateMetadata(\n          txn.metadata.copy(\n            configuration = txn.metadata.configuration ++\n              ClusteredTableUtils.getTableFeatureProperties(txn.metadata.configuration)\n          ))\n      }\n      txn.commit(\n        newDomainMetadata,\n        DeltaOperations.ClusterBy(\n          oldLogicalClusteringColumnsString,\n          newLogicalClusteringColumns.mkString(\",\")))\n    }\n    Seq.empty[Row]\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/BackfillBatch.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.spark.sql.delta.OptimisticTransaction\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\n\ntrait BackfillBatch extends DeltaLogging {\n  /** The files in this batch. */\n  def filesInBatch: Seq[AddFile]\n  def backfillBatchStatsOpType: String\n\n  protected def prepareFilesAndCommit(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      batchId: Int): Long\n\n  /**\n   * The main method of this trait. This method commits the backfill batch, records metrics and\n   * updates the two atomic counters passed in.\n   *\n   * @param spark The Spark session.\n   * @param backfillTxnId the transaction id associated with the parent command.\n   * @param batchId an integer identifier of the batch within a parent [[BackfillCommand]].\n   * @param txn transaction used to construct the current batch.\n   * @param numSuccessfulBatch an AtomicInteger which serves as a counter for the total number of\n   *                           batches that were successful.\n   * @param numFailedBatch an AtomicInteger which serves as a counter for the total number of\n   *                       batches that failed.\n   */\n  def execute(\n      spark: SparkSession,\n      backfillTxnId: String,\n      batchId: Int,\n      txn: OptimisticTransaction,\n      numSuccessfulBatch: AtomicInteger,\n      numFailedBatch: AtomicInteger): Long = {\n    val startTimeNs = System.nanoTime()\n\n    def recordBackfillBatchStats(txnId: String, wasSuccessful: Boolean): Unit = {\n      if (wasSuccessful) {\n        numSuccessfulBatch.incrementAndGet()\n      } else {\n        numFailedBatch.incrementAndGet()\n      }\n      val totalExecutionTimeInMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)\n      val batchStats = BackfillBatchStats(\n        backfillTxnId, txnId, batchId, filesInBatch.size, totalExecutionTimeInMs, wasSuccessful)\n      recordDeltaEvent(\n        txn.deltaLog,\n        opType = backfillBatchStatsOpType,\n        data = batchStats\n      )\n    }\n\n    logInfo(log\"Batch ${MDC(DeltaLogKeys.BATCH_ID, batchId.toLong)} starting, committing \" +\n      log\"${MDC(DeltaLogKeys.NUM_FILES, filesInBatch.size.toLong)} candidate files\")\n    val txnId = txn.txnId\n    try {\n      val commitVersion = prepareFilesAndCommit(spark, txn, batchId)\n      recordBackfillBatchStats(txnId, wasSuccessful = true)\n      commitVersion\n    } catch {\n      case t: Throwable =>\n        recordBackfillBatchStats(txnId, wasSuccessful = false)\n        throw t\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/BackfillBatchStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\n/**\n * Metrics for each BackfillBatch.\n *\n * @param parentTransactionId The transaction id associated with the parent command.\n * @param transactionId The transaction id used in this batch.\n * @param batchId An integer identifier of the batch within a parent BackfillCommand.\n * @param initialNumFiles The number of files in BackfillBatch prior to conflict\n *                        resolution.\n * @param totalExecutionTimeInMs The total execution time in milliseconds.\n * @param wasSuccessful Boolean indicating whether the batch was successfully committed.\n */\ncase class BackfillBatchStats(\n    parentTransactionId: String,\n    transactionId: String,\n    batchId: Int,\n    initialNumFiles: Long,\n    totalExecutionTimeInMs: Long,\n    wasSuccessful: Boolean\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/BackfillCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport java.util.UUID\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands.DeltaCommand\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.execution.command.LeafRunnableCommand\n\n/**\n * This command will lazily materialize AllFiles and split them into multiple backfill commits\n * if the number of files exceeds the threshold set by\n * [[DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT]].\n */\ntrait BackfillCommand extends LeafRunnableCommand with DeltaCommand {\n  def deltaLog: DeltaLog\n  def nameOfTriggeringOperation: String\n  def catalogTableOpt: Option[CatalogTable]\n\n  def getBackfillExecutor(\n    spark: SparkSession,\n    deltaLog: DeltaLog,\n    catalogTableOpt: Option[CatalogTable],\n    backfillId: String,\n    backfillStats: BackfillCommandStats): BackfillExecutor\n\n  def opType: String\n\n  override def run(spark: SparkSession): Seq[Row] = {\n    recordDeltaOperation(deltaLog, opType) {\n      val backfillId = UUID.randomUUID().toString\n      val maxNumFilesPerCommit =\n        spark.conf.get(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT)\n      val startTimeNs = System.nanoTime()\n      val backfillStats = BackfillCommandStats(\n        transactionId = backfillId,\n        nameOfTriggeringOperation\n      )\n      try {\n        val backfillExecutor = getBackfillExecutor(\n          spark, deltaLog, catalogTableOpt, backfillId, backfillStats)\n        val lastCommitOpt = backfillExecutor.run(maxNumFilesPerCommit)\n        backfillStats.wasSuccessful = true\n        Array.empty[Row] ++ lastCommitOpt.map(Row(_))\n      } finally {\n        val totalExecutionTimeMs =\n          TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs)\n        backfillStats.totalExecutionTimeMs = totalExecutionTimeMs\n\n        recordDeltaEvent(\n          deltaLog,\n          opType = opType + \".stats\",\n          data = backfillStats\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/BackfillCommandStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\n/**\n * Metrics for the BackfillCommand.\n *\n * @param transactionId: The transaction id associated with this BackfillCommand that\n *                       is the parent transaction for BackfillBatch commits.\n * @param nameOfTriggeringOperation: The name of the operation that triggered backfill. For now,\n *                                   this can be ALTER TABLE SET TBLPROPERTIES\n * @param totalExecutionTimeMs The total execution time in milliseconds.\n * @param numSuccessfulBatches The number of BackfillBatch's that was successfully committed.\n * @param numFailedBatches The number of BackfillBatch's that failed.\n * @param wasSuccessful Boolean indicating whether this BackfillCommand didn't have any error.\n */\ncase class BackfillCommandStats(\n    transactionId: String,\n    nameOfTriggeringOperation: String,\n    var totalExecutionTimeMs: Long = 0,\n    var numSuccessfulBatches: Int = 0,\n    var numFailedBatches: Int = 0,\n    var wasSuccessful: Boolean = false\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/BackfillExecutionObserver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta.{ChainableExecutionObserver, NoOpTransactionExecutionObserver, ThreadStorageExecutionObserver, TransactionExecutionObserver}\n\ntrait BackfillExecutionObserver extends ChainableExecutionObserver[BackfillExecutionObserver] {\n  def executeBatch[T](f: => T): T\n\n  override def advanceToNextThreadObserver(): Unit = {\n    BackfillExecutionObserver.setObserver(nextObserver.getOrElse(NoOpBackfillExecutionObserver))\n  }\n}\n\nobject BackfillExecutionObserver\n  extends ThreadStorageExecutionObserver[BackfillExecutionObserver] {\n\n  override protected val threadObserver: ThreadLocal[BackfillExecutionObserver] =\n    new InheritableThreadLocal[BackfillExecutionObserver] {\n      override def initialValue(): BackfillExecutionObserver = NoOpBackfillExecutionObserver\n    }\n\n  override protected def initialValue: BackfillExecutionObserver = NoOpBackfillExecutionObserver\n}\n\nobject NoOpBackfillExecutionObserver extends BackfillExecutionObserver {\n  def executeBatch[T](f: => T): T = f\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/BackfillExecutor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\ntrait BackfillExecutor extends DeltaLogging {\n  def spark: SparkSession\n  def deltaLog: DeltaLog\n  def catalogTableOpt: Option[CatalogTable]\n  def backfillTxnId: String\n  def backfillStats: BackfillCommandStats\n\n  def backFillBatchOpType: String\n  def filesToBackfill(snapshot: Snapshot): Dataset[AddFile]\n  def constructBatch(files: Seq[AddFile]): BackfillBatch\n\n  /**\n   * Execute the command by consuming a sequence of [[BackfillBatch]].\n   * Returns an option with the last commit version when available. Otherwise, it returns None.\n   */\n  def run(maxNumFilesPerCommit: Int): Option[Long] = {\n    executeBackfillBatches(maxNumFilesPerCommit)\n  }\n\n  /**\n   * Execute all available [[BackfillBatch]].\n   * Returns an option with the last commit version when available. Otherwise, it returns None.\n   *\n   * Note, in the case of competing concurrent transactions, this method will exit after processing\n   * a maximum of `backfill.maxNumFilesFactor` times the total number of files in the table.\n   */\n  private def executeBackfillBatches(maxNumFilesPerCommit: Int): Option[Long] = {\n    val observer = BackfillExecutionObserver.getObserver\n    val numSuccessfulBatch = new AtomicInteger(0)\n    val numFailedBatch = new AtomicInteger(0)\n    val totalFileCount = deltaLog.update(catalogTableOpt = catalogTableOpt).numOfFiles\n    val factor = spark.conf.get(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_FACTOR)\n    val maxFilesToProcess = totalFileCount * factor\n\n    var batchId = 0\n    var totalFilesProcessed = 0L\n    var filesInBatch = Seq.empty[AddFile]\n    var lastCommitOpt: Option[Long] = None\n\n    // If the last batch contained fewer files than the maxNumFilesPerCommit we exit.\n    // This protects against live-locking with fast concurrent txns that only commit\n    // a few files.\n    // Having excluded this option the backfill can only live-lock with an equally fast\n    // concurrent txn, i.e a competing un-backfill that only commits logs files.\n    // To protect against this we set a maximum backfill limit equal to a factor of the\n    // total table file count.\n    def moreFilesToProcess(): Boolean = {\n      filesInBatch.length == maxNumFilesPerCommit && totalFilesProcessed < maxFilesToProcess\n    }\n\n    try {\n      do {\n        val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n        filesInBatch = filesToBackfill(snapshot).limit(maxNumFilesPerCommit).collect()\n        if (filesInBatch.isEmpty) {\n          return lastCommitOpt\n        }\n\n        val batch = constructBatch(filesInBatch)\n        observer.executeBatch {\n          val txn = deltaLog.startTransaction(catalogTableOpt, Some(snapshot))\n          txn.trackFilesRead(filesInBatch)\n          recordDeltaOperation(deltaLog, backFillBatchOpType) {\n            lastCommitOpt = Some(batch.execute(\n              spark, backfillTxnId, batchId, txn, numSuccessfulBatch, numFailedBatch))\n          }\n          batchId += 1\n          totalFilesProcessed += filesInBatch.length\n        }\n      } while (moreFilesToProcess())\n\n      if (totalFilesProcessed >= maxFilesToProcess) {\n        recordDeltaEvent(\n          deltaLog,\n          opType = \"delta.backfillExceededMaxFilesToProcess\",\n          data = Map(\"maxFilesProcessed\" -> maxFilesToProcess))\n      }\n      lastCommitOpt\n    } finally {\n      backfillStats.numSuccessfulBatches = numSuccessfulBatch.get()\n      backfillStats.numFailedBatches = numFailedBatch.get()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingBackfillBatch.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaOperations, OptimisticTransaction, RowTrackingFeature}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\n\nimport org.apache.spark.sql.SparkSession\n\ncase class RowTrackingBackfillBatch(filesInBatch: Seq[AddFile]) extends BackfillBatch {\n\n  override val backfillBatchStatsOpType = \"delta.rowTracking.backfill.batch.stats\"\n\n  /** Mark all files as dataChange = false and commit. */\n  override protected def prepareFilesAndCommit(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      batchId: Int): Long = {\n    val protocol = txn.snapshot.protocol\n    val metadata = txn.snapshot.metadata\n    val isRowTrackingSupported = protocol.isFeatureSupported(RowTrackingFeature)\n    val ignoreProperty = DeltaSQLConf.DELTA_ROW_TRACKING_IGNORE_SUSPENSION.key\n    val ignoreSuspension = DeltaUtils.isTesting && spark.conf.get(ignoreProperty).toBoolean\n    val suspendRowTracking =\n      DeltaConfigs.ROW_TRACKING_SUSPENDED.fromMetaData(metadata) && !ignoreSuspension\n    if (!isRowTrackingSupported || suspendRowTracking) {\n      throw new IllegalStateException(\n        \"\"\"\n          |Cannot run backfill command if row tracking is not supported or\n          |row ID generation is suspended.\"\"\".stripMargin)\n    }\n\n    val filesToCommit = filesInBatch.map(_.copy(dataChange = false))\n    // Base Row IDs are added as part of the OptimisticTransaction.prepareCommit(), so we don't\n    // need to do anything here other than recommit the files.\n    // Note: A backfill commit can be empty ,i.e. have no file actions, at commit time due to\n    // files being removed by concurrent conflict resolution.\n    txn.commit(filesToCommit, DeltaOperations.RowTrackingBackfill(batchId))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingBackfillCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\n\nimport org.apache.spark.sql.{Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * This command re-commits all AddFiles in the current snapshot that do not have a base row IDs.\n * After the backfill command finishes, the snapshot has row IDs for all files. All commits\n * afterwards must include row IDs.\n *\n * First, we will add the table feature support, if necessary.\n * Then, the command will lazily materialize AllFiles and split them into multiple backfill commits\n * if the number of files exceeds the threshold set by\n * [[DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT]].\n *\n * Note: We expect Backfill to be called before the table property is set. Furthermore, we do not\n * set the table property [[DeltaConfigs.ROW_TRACKING_ENABLED]] as part of backfill. The metadata\n * update needs to be handled by the caller.\n */\ncase class RowTrackingBackfillCommand(\n    override val deltaLog: DeltaLog,\n    override val nameOfTriggeringOperation: String,\n    override val catalogTableOpt: Option[CatalogTable])\n  extends BackfillCommand {\n\n  override def getBackfillExecutor(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      backfillId: String,\n      backfillStats: BackfillCommandStats): BackfillExecutor =\n    new RowTrackingBackfillExecutor(spark, deltaLog, catalogTableOpt, backfillId, backfillStats)\n\n  override def opType: String = \"delta.rowTracking.backfill\"\n\n /**\n  * Add Row tracking table feature support. This will also upgrade the minWriterVersion if\n  * the current protocol cannot support write table feature.\n  */\n  private def upgradeProtocolIfRequired(): Unit = {\n    val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n    if (!snapshot.protocol.isFeatureSupported(RowTrackingFeature)) {\n      val minProtocolAllowingWriteTableFeature = Protocol(\n        snapshot.protocol.minReaderVersion,\n        TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n      val newProtocol = snapshot.protocol\n        .merge(minProtocolAllowingWriteTableFeature.withFeature(RowTrackingFeature))\n      deltaLog.upgradeProtocol(catalogTableOpt, snapshot, newProtocol)\n    }\n  }\n\n  override def run(spark: SparkSession): Seq[Row] = {\n    if (!spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED)) {\n      throw new UnsupportedOperationException(\"Cannot enable Row IDs on an existing table.\")\n    }\n\n    // Upgrade the protocol to support the table feature if it isn't already supported.\n    // This steps bounds the number of files the command must commit, since all actions after\n    // we support the table feature must have base row IDs.\n    upgradeProtocolIfRequired()\n\n    super.run(spark)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingBackfillExecutor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\nclass RowTrackingBackfillExecutor(\n    override val spark: SparkSession,\n    override val deltaLog: DeltaLog,\n    override val catalogTableOpt: Option[CatalogTable],\n    override val backfillTxnId: String,\n    override val backfillStats: BackfillCommandStats) extends BackfillExecutor {\n  override val backFillBatchOpType = \"delta.rowTracking.backfill.batch\"\n\n  override def filesToBackfill(snapshot: Snapshot): Dataset[AddFile] =\n    RowTrackingBackfillExecutor.getCandidateFilesToBackfill(snapshot)\n\n  override def constructBatch(files: Seq[AddFile]): BackfillBatch =\n    RowTrackingBackfillBatch(files)\n}\n\nprivate[delta] object RowTrackingBackfillExecutor extends DeltaLogging {\n  /**\n   * Returns the dataset with the list of candidate files to backfill.\n   */\n  def getCandidateFilesToBackfill(snapshot: Snapshot): Dataset[AddFile] = {\n    // Note: We can't use txn.filterFiles() because it drops the file statistics.\n    snapshot\n      .allFiles\n      .filter(_.baseRowId.isEmpty)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingUnBackfillBatch.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaOperations, OptimisticTransaction}\nimport org.apache.spark.sql.delta.actions.AddFile\n\nimport org.apache.spark.sql.SparkSession\n\ncase class RowTrackingUnBackfillBatch(filesInBatch: Seq[AddFile]) extends BackfillBatch {\n  override val backfillBatchStatsOpType = \"delta.rowTracking.unbackfill.batch.stats\"\n\n  /** Remove relevant metadata from addFiles. */\n  override protected def prepareFilesAndCommit(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      batchId: Int): Long = {\n    val metadata = txn.snapshot.metadata\n    val isEnabled = DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(metadata)\n    val suspendIdGeneration = DeltaConfigs.ROW_TRACKING_SUSPENDED.fromMetaData(metadata)\n    if (isEnabled || !suspendIdGeneration) {\n      throw new IllegalStateException(\n        \"Cannot run unbackfill when row tracking is enabled or not suspended.\")\n    }\n\n    val filesToCommit = filesInBatch.map(_.copy(\n      baseRowId = None,\n      defaultRowCommitVersion = None,\n      dataChange = false))\n    txn.commit(filesToCommit, DeltaOperations.RowTrackingUnBackfill(batchId))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingUnBackfillCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * This command cleans up tracking metadata from a delta table. In particular, it removes\n * `baseRowId` and `defaultRowCommitVersion`. This is achieved by re-commiting addFiles with\n * `dataChance = false`. This requires to commit the AddFiles of the entire table.\n *\n * Similarly to all backfilling operations, the relevant files are commited in multiple batches.\n * Each batch contains [[DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT]]. This is to avoid\n * generating large commits in big tables.\n */\ncase class RowTrackingUnBackfillCommand(\n    override val deltaLog: DeltaLog,\n    override val nameOfTriggeringOperation: String,\n    override val catalogTableOpt: Option[CatalogTable])\n  extends BackfillCommand {\n\n  override def getBackfillExecutor(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      backfillId: String,\n      backfillStats: BackfillCommandStats): BackfillExecutor = {\n    new RowTrackingUnBackfillExecutor(spark, deltaLog, catalogTableOpt, backfillId, backfillStats)\n  }\n\n  override def opType: String = \"delta.rowTracking.unbackfill\"\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingUnBackfillExecutor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\nclass RowTrackingUnBackfillExecutor(\n    override val spark: SparkSession,\n    override val deltaLog: DeltaLog,\n    override val catalogTableOpt: Option[CatalogTable],\n    override val backfillTxnId: String,\n    override val backfillStats: BackfillCommandStats) extends BackfillExecutor {\n  override val backFillBatchOpType = \"delta.rowTracking.unbackfill.batch\"\n\n  override def filesToBackfill(snapshot: Snapshot): Dataset[AddFile] = {\n    RowTrackingUnBackfillExecutor.getCandidateFilesToUnBackfill(snapshot)\n  }\n\n  override def constructBatch(files: Seq[AddFile]): BackfillBatch =\n    RowTrackingUnBackfillBatch(files)\n}\n\nprivate[delta] object RowTrackingUnBackfillExecutor extends DeltaLogging {\n  /** Returns the dataset with the list of candidate files to unbackfill. */\n  def getCandidateFilesToUnBackfill(\n      snapshot: Snapshot): Dataset[AddFile] = {\n    // Note: We can't use txn.filterFiles() because it drops the file statistics.\n    snapshot\n      .allFiles\n      .filter(a => a.baseRowId.nonEmpty || a.defaultRowCommitVersion.nonEmpty)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/cdc/CDCReader.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.cdc\n\nimport java.sql.Timestamp\n\nimport scala.collection.mutable.{ListBuffer, Map => MutableMap}\nimport scala.util.Try\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\nimport org.apache.spark.sql.delta.files.{CdcAddFileIndex, TahoeChangeFileIndex, TahoeFileIndexWithSnapshotDescriptor, TahoeRemoveFileIndex}\nimport org.apache.spark.sql.delta.sources.DeltaDataSource\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.spark.sql.util.ScalaExtensions.OptionExt\n\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.sql.{Column, DataFrame, Row, SparkSession, SQLContext}\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical.Statistics\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.execution.LogicalRDD\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelation}\nimport org.apache.spark.sql.sources.BaseRelation\nimport org.apache.spark.sql.types.{LongType, StringType, StructType, TimestampType}\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * The API that allows reading Change data between two versions of a table.\n *\n * The basic abstraction here is the CDC type column defined by [[CDCReader.CDC_TYPE_COLUMN_NAME]].\n * When CDC is enabled, our writer will treat this column as a special partition column even though\n * it's not part of the table. Writers should generate a query that has two types of rows in it:\n * the main data in partition CDC_TYPE_NOT_CDC and the CDC data with the appropriate CDC type value.\n *\n * [[org.apache.spark.sql.delta.files.DelayedCommitProtocol]]\n * does special handling for this column, dispatching the main data to its normal location while the\n * CDC data is sent to [[AddCDCFile]] entries.\n */\nobject CDCReader extends CDCReaderImpl\n{\n  // Definitions for the CDC type column. Delta writers will write data with a non-null value for\n  // this column into [[AddCDCFile]] actions separate from the main table, and the CDC reader will\n  // read this column to determine what type of change it was.\n  val CDC_TYPE_COLUMN_NAME = \"_change_type\" // emitted from data\n  val CDC_COMMIT_VERSION = \"_commit_version\" // inferred by reader\n  val CDC_COMMIT_TIMESTAMP = \"_commit_timestamp\" // inferred by reader\n  val CDC_TYPE_DELETE_STRING = \"delete\"\n  val CDC_TYPE_DELETE = Literal(CDC_TYPE_DELETE_STRING)\n  val CDC_TYPE_INSERT = \"insert\"\n  val CDC_TYPE_UPDATE_PREIMAGE = \"update_preimage\"\n  val CDC_TYPE_UPDATE_POSTIMAGE = \"update_postimage\"\n\n  /**\n   * Append CDC metadata columns to the provided schema.\n   */\n  def cdcAttributes: Seq[Attribute] = Seq(\n    AttributeReference(CDC_TYPE_COLUMN_NAME, StringType)(),\n    AttributeReference(CDC_COMMIT_VERSION, LongType)(),\n    AttributeReference(CDC_COMMIT_TIMESTAMP, TimestampType)())\n\n  // A special sentinel value indicating rows which are part of the main table rather than change\n  // data. Delta writers will partition rows with this value away from the CDC data and\n  // write them as normal to the main table.\n  // Note that we specifically avoid using `null` here, because partition values of `null` are in\n  // some scenarios mapped to a special string for Hive compatibility.\n  val CDC_TYPE_NOT_CDC: Literal = Literal(null, StringType)\n\n  // The virtual column name used for dividing CDC data from main table data. Delta writers should\n  // permit this column through even though it's not part of the main table, and the\n  // [[DelayedCommitProtocol]] will apply some special handling, ensuring there's only a\n  // subfolder with __is_cdc = true and writing data with __is_cdc = false to the base location\n  // as it would with CDC output off.\n  // This is a bit redundant with CDC_TYPE_COL, but partitioning directly on the type would mean\n  // that CDC of each type is partitioned away separately, exacerbating small file problems.\n  val CDC_PARTITION_COL = \"__is_cdc\" // emitted by data\n\n  // The top-level folder within the Delta table containing change data. This folder may contain\n  // partitions within itself.\n  val CDC_LOCATION = \"_change_data\"\n\n  // CDC specific columns in data written by operations\n  val CDC_COLUMNS_IN_DATA = Seq(CDC_PARTITION_COL, CDC_TYPE_COLUMN_NAME)\n\n  /**\n   * A special BaseRelation wrapper for CDF reads.\n   */\n  case class DeltaCDFRelation(\n      snapshotWithSchemaMode: SnapshotWithSchemaMode,\n      sqlContext: SQLContext,\n      catalogTableOpt: Option[CatalogTable],\n      startingVersion: Option[Long],\n      endingVersion: Option[Long]) extends\n    DeltaCDFRelationBase(\n      snapshotWithSchemaMode,\n      sqlContext,\n      catalogTableOpt,\n      startingVersion,\n      endingVersion) {\n\n    override def buildScan(requiredColumns: Seq[Attribute], filters: Seq[Expression]): RDD[Row] = {\n      val df = changesToBatchDF(\n        deltaLog,\n        startingVersion.get,\n        // The actual ending version we should scan until during execution, as it might have changed\n        endingVersion.getOrElse {\n          deltaLog.update(catalogTableOpt = catalogTableOpt).version\n        },\n        sqlContext.sparkSession,\n        catalogTableOpt,\n        readSchemaSnapshot = Some(snapshotForBatchSchema))\n      constructRDD(df, requiredColumns, filters)\n    }\n  }\n\n  case class CDCDataSpec[T <: FileAction](\n      version: Long,\n      timestamp: Timestamp,\n      actions: Seq[T],\n      commitInfo: Option[CommitInfo]) {\n    def this(\n        tableVersion: TableVersion,\n        actions: Seq[T],\n        commitInfo: Option[CommitInfo]) = {\n      this(\n        tableVersion.version,\n        tableVersion.timestamp,\n        actions,\n        commitInfo)\n    }\n  }\n\n  /** A version number of a Delta table, with the version's timestamp. */\n  case class TableVersion(version: Long, timestamp: Timestamp) {\n    def this(wp: FilePathWithTableVersion) = this(wp.version, wp.timestamp)\n  }\n\n  /** Path of a file of a Delta table, together with it's origin table version & timestamp. */\n  case class FilePathWithTableVersion(\n      path: String,\n      commitInfo: Option[CommitInfo],\n      version: Long,\n      timestamp: Timestamp)\n}\n\ntrait CDCReaderImpl extends CDCReaderBase {\n\n  import org.apache.spark.sql.delta.commands.cdc.CDCReader._\n\n  /**\n   * Function to check if file actions should be skipped for no-op merges based on\n   * CommitInfo metrics.\n   * MERGE will sometimes rewrite files in a way which *could* have changed data\n   * (so dataChange = true) but did not actually do so (so no CDC will be produced).\n   * In this case the correct CDC output is empty - we shouldn't serve it from\n   * those files. This should be handled within the command, but as a hotfix-safe fix, we check\n   * the metrics. If the command reported 0 rows inserted, updated, or deleted, then CDC\n   * shouldn't be produced.\n   */\n  def shouldSkipFileActionsInCommit(commitInfo: CommitInfo): Boolean = {\n    val isMerge = commitInfo.operation == DeltaOperations.OP_MERGE\n    val knownToHaveNoChangedRows = {\n      val metrics = commitInfo.operationMetrics.getOrElse(Map.empty)\n      // Note that if any metrics are missing, this condition will be false and we won't skip.\n      // Unfortunately there are no predefined constants for these metric values.\n      Seq(\"numTargetRowsInserted\", \"numTargetRowsUpdated\", \"numTargetRowsDeleted\").forall {\n        metrics.get(_).contains(\"0\")\n      }\n    }\n    isMerge && knownToHaveNoChangedRows\n  }\n\n\n  /**\n   * For a sequence of changes(AddFile, RemoveFile, AddCDCFile) create a DataFrame that represents\n   * that captured change data between start and end inclusive.\n   *\n   * Builds the DataFrame using the following logic: Per each change of type (Long, Seq[Action]) in\n   * `changes`, iterates over the actions and handles two cases.\n   * - If there are any CDC actions, then we ignore the AddFile and RemoveFile actions in that\n   *   version and create an AddCDCFile instead.\n   * - If there are no CDC actions, then we must infer the CDC data from the AddFile and RemoveFile\n   *   actions, taking only those with `dataChange = true`.\n   *\n   * These buffers of AddFile, RemoveFile, and AddCDCFile actions are then used to create\n   * corresponding FileIndexes (e.g. [[TahoeChangeFileIndex]]), where each is suited to use the\n   * given action type to read CDC data. These FileIndexes are then unioned to produce the final\n   * DataFrame.\n   *\n   * @param readSchemaSnapshot - Snapshot for the table for which we are creating a CDF\n   *                             Dataframe, the schema of the snapshot is expected to be\n   *                             the change DF's schema. We have already adjusted this\n   *                             snapshot with the schema mode if there's any. We don't use\n   *                             its data actually.\n   * @param start - startingVersion of the changes\n   * @param end - endingVersion of the changes\n   * @param changes - changes is an iterator of all FileActions for a particular commit version.\n   *                Note that for log files where InCommitTimestamps are enabled, the iterator\n   *                must also contain the [[CommitInfo]] action.\n   * @param spark - SparkSession\n   * @param catalogTableOpt - The catalog table for the Delta table\n   * @param isStreaming - indicates whether the DataFrame returned is a streaming DataFrame\n   * @param useCoarseGrainedCDC - ignores checks related to CDC being disabled in any of the\n   *         versions and computes CDC entirely from AddFiles/RemoveFiles (ignoring\n   *         AddCDCFile actions)\n   * @param startVersionSnapshot - The snapshot of the starting version.\n   * @return CDCInfo which contains the DataFrame of the changes as well as the statistics\n   *         related to the changes\n   */\n  // scalastyle:off argcount\n  def changesToDF(\n      readSchemaSnapshot: SnapshotDescriptor,\n      start: Long,\n      end: Long,\n      changes: Iterator[(Long, Seq[Action])],\n      spark: SparkSession,\n      catalogTableOpt: Option[CatalogTable],\n      isStreaming: Boolean = false,\n      useCoarseGrainedCDC: Boolean = false,\n      startVersionSnapshot: Option[SnapshotDescriptor] = None): CDCVersionDiffInfo = {\n  // scalastyle:on argcount\n    val deltaLog = readSchemaSnapshot.deltaLog\n\n    if (end < start) {\n      throw DeltaErrors.endBeforeStartVersionInCDC(start, end)\n    }\n\n    // A map from change version to associated file modification timestamps.\n    // We only need these for non-InCommitTimestamp commits because for InCommitTimestamp commits,\n    // the timestamps are already stored in the commit info.\n    val nonICTTimestampsByVersion: Map[Long, Timestamp] =\n      getNonICTTimestampsByVersion(deltaLog, start, end)\n\n    val changeFiles = ListBuffer[CDCDataSpec[AddCDCFile]]()\n    val addFiles = ListBuffer[CDCDataSpec[AddFile]]()\n    val removeFiles = ListBuffer[CDCDataSpec[RemoveFile]]()\n\n    val startVersionMetadata = startVersionSnapshot.map(_.metadata).getOrElse {\n      deltaLog.getSnapshotAt(start, catalogTableOpt = catalogTableOpt).metadata\n    }\n    if (!useCoarseGrainedCDC && !isCDCEnabledOnTable(startVersionMetadata, spark)) {\n      throw DeltaErrors.changeDataNotRecordedException(start, start, end)\n    }\n\n    val checkSchemaToBlockRead = shouldCheckSchemaToBlockBatchRead(\n      spark,\n      deltaLog,\n      isStreaming\n    )\n\n    var totalBytes = 0L\n    var numAddFiles, numRemoveFiles, numAddCRCFiles = 0L\n\n    changes.foreach {\n      case (v, actions) =>\n        // Check whether CDC was newly disabled in this version. (We should have already checked\n        // that it's enabled for the starting version, so checking this for each version\n        // incrementally is sufficient to ensure that it's enabled for the entire range.)\n        val cdcDisabled = actions.exists {\n          case m: Metadata => !isCDCEnabledOnTable(m, spark)\n          case _ => false\n        }\n\n        if (cdcDisabled && !useCoarseGrainedCDC) {\n          throw DeltaErrors.changeDataNotRecordedException(v, start, end)\n        }\n\n        // Check all intermediary metadata schema changes, this guarantees that there will be no\n        // read-incompatible schema changes across the querying range.\n        // Note that we don't have to check the schema change if it's at the start version, because:\n        // 1. If it's an initialization, e.g. CREATE AS SELECT, we don't have to consider this\n        //    as a schema change and report weird error messages.\n        // 2. If it's indeed a schema change, as we won't be reading any data prior to it that\n        //    falls back to the previous (possibly incorrect) schema, we will be safe. Also if there\n        //    are any data file residing in the same commit, it will follow the new schema as well.\n        if (v > start) {\n          actions.collect { case a: Metadata => a }.foreach { metadata =>\n            // Verify with start snapshot to check for any read-incompatible changes\n            // This also detects the corner case in that there's only one schema change between\n            // start and end, which looks exactly like the end schema.\n            checkBatchCdfReadSchemaIncompatibility(\n              readSchemaSnapshot, start, end, checkSchemaToBlockRead,\n              metadata, v, isSchemaChange = true)\n          }\n        }\n\n        // Set up buffers for all action types to avoid multiple passes.\n        val cdcActions = ListBuffer[AddCDCFile]()\n\n        // Note that the CommitInfo is *not* guaranteed to be generated in 100% of cases.\n        // We are using it only for a hotfix-safe mitigation/defense-in-depth - the value\n        // extracted here cannot be relied on for correctness.\n        var commitInfo: Option[CommitInfo] = None\n        actions.foreach {\n          case c: AddCDCFile =>\n            cdcActions.append(c)\n            numAddCRCFiles += 1L\n            totalBytes += c.size\n          case a: AddFile =>\n            numAddFiles += 1L\n            totalBytes += a.size\n          case r: RemoveFile =>\n            numRemoveFiles += 1L\n            totalBytes += r.size.getOrElse(0L)\n          case i: CommitInfo => commitInfo = Some(i)\n          case _ => // do nothing\n        }\n        // If the commit has an In-Commit Timestamp, we should use that as the commit timestamp.\n        // Note that it is technically possible for a commit range to begin with ICT commits\n        // followed by non-ICT commits, and end with ICT commits again. Ideally, for these commits\n        // we should use the file modification time for the first two ranges. However, this\n        // scenario is an edge case not worth optimizing for.\n        val ts = commitInfo\n          .flatMap(_.inCommitTimestamp)\n          .map(ict => new Timestamp(ict))\n          .getOrElse(nonICTTimestampsByVersion.get(v).orNull)\n        // When `isStreaming` = `true` the [CommitInfo] action is only used for passing the\n        // in-commit timestamp to this method. We should filter them out.\n        commitInfo = if (isStreaming) None else commitInfo\n\n        // If there are CDC actions, we read them exclusively if we should not use the\n        // Add and RemoveFiles.\n        if (cdcActions.nonEmpty && !useCoarseGrainedCDC) {\n          changeFiles.append(CDCDataSpec(v, ts, cdcActions.toSeq, commitInfo))\n        } else {\n          val shouldSkipIndexedFile = commitInfo.exists(CDCReader.shouldSkipFileActionsInCommit)\n          if (shouldSkipIndexedFile) {\n            // This was introduced for a hotfix, so we're mirroring the existing logic as closely\n            // as possible - it'd likely be safe to just return an empty dataframe here.\n            addFiles.append(CDCDataSpec(v, ts, Nil, commitInfo))\n            removeFiles.append(CDCDataSpec(v, ts, Nil, commitInfo))\n          } else {\n            // Otherwise, we take the AddFile and RemoveFile actions with dataChange = true and\n            // infer CDC from them.\n            val addActions = actions.collect { case a: AddFile if a.dataChange => a }\n            val removeActions = actions.collect { case r: RemoveFile if r.dataChange => r }\n            addFiles.append(\n              CDCDataSpec(\n                version = v,\n                timestamp = ts,\n                actions = addActions,\n                commitInfo = commitInfo)\n            )\n            removeFiles.append(\n              CDCDataSpec(\n                version = v,\n                timestamp = ts,\n                actions = removeActions,\n                commitInfo = commitInfo)\n            )\n          }\n        }\n    }\n\n    // Verify the final read schema with the start snapshot version once again\n    // This is needed to:\n    // 1. Handle the case in that there are no read-incompatible schema change with the range, BUT\n    //    the latest schema may still be incompatible as it COULD be arbitrary.\n    // 2. Similarly, handle the corner case when there are no read-incompatible schema change with\n    //    the range, BUT time-travel is used so the read schema could also be arbitrary.\n    // It is sufficient to just verify with the start version schema because we have already\n    // verified that all data being queried is read-compatible with start schema.\n    checkBatchCdfReadSchemaIncompatibility(\n      readSchemaSnapshot, start, end, checkSchemaToBlockRead,\n      startVersionMetadata, start, isSchemaChange = false)\n\n    val dfs = ListBuffer[DataFrame]()\n    if (changeFiles.nonEmpty) {\n      dfs.append(scanIndex(\n        spark,\n        new TahoeChangeFileIndex(\n          spark, changeFiles.toSeq, deltaLog, deltaLog.dataPath, readSchemaSnapshot),\n        isStreaming))\n    }\n\n    val deletedAndAddedRows = getDeletedAndAddedRows(\n      addFiles.toSeq, removeFiles.toSeq, deltaLog,\n      readSchemaSnapshot, isStreaming, spark)\n    dfs.append(deletedAndAddedRows: _*)\n\n    val readSchema = cdcReadSchema(readSchemaSnapshot.metadata.schema)\n    // build an empty DS. This DS retains the table schema and the isStreaming property\n    // NOTE: We need to manually set the stats to 0 otherwise we will use default stats of INT_MAX,\n    // which causes lots of optimizations to be applied wrong.\n    val emptyRdd = LogicalRDD(\n      toAttributes(readSchema),\n      spark.sparkContext.emptyRDD[InternalRow],\n      isStreaming = isStreaming\n    )(spark.sqlContext.sparkSession, Some(Statistics(0, Some(0))))\n    val emptyDf =\n      DataFrameUtils.ofRows(spark.sqlContext.sparkSession, emptyRdd)\n\n    recordDeltaEvent(\n      deltaLog,\n      \"delta.changeDataFeed.changesToDF\",\n      data = Map(\n        \"startVersion\" -> start,\n        \"endVersion\" -> end,\n        \"useCoarseGrainedCDC\" -> useCoarseGrainedCDC,\n        \"numAddFiles\" -> numAddFiles,\n        \"numRemoveFiles\" -> numRemoveFiles,\n        \"numAddCRCFiles\" -> numAddCRCFiles,\n        \"totalBytes\" -> totalBytes,\n        \"isStreaming\" -> isStreaming\n      )\n    )\n    val totalFiles = numAddFiles + numRemoveFiles + numAddCRCFiles\n    CDCVersionDiffInfo(\n      (emptyDf +: dfs).reduce((df1, df2) => df1.union(\n        df2\n      )),\n      totalFiles,\n      totalBytes)\n  }\n\n  /**\n   * Generate CDC rows by looking at added and removed files, together with Deletion Vectors they\n   * may have.\n   *\n   * When DV is used, the same file can be removed then added in the same version, and the only\n   * difference is the assigned DVs. The base method does not consider DVs in this case, thus will\n   * produce CDC that *all* rows in file being removed then *some* re-added. The correct answer,\n   * however, is to compare two DVs and apply the diff to the file to get removed and re-added rows.\n   *\n   * Currently it is always the case that in the log \"remove\" comes first, followed by \"add\" --\n   * which means that the file stays alive with a new DV. There's another possibility, though not\n   * make many senses, that a file is \"added\" to log then \"removed\" in the same version. If this\n   * becomes possible in future, we have to reconstruct the timeline considering the order of\n   * actions rather than simply matching files by path.\n   */\n  protected def getDeletedAndAddedRows(\n      addFileSpecs: Seq[CDCDataSpec[AddFile]],\n      removeFileSpecs: Seq[CDCDataSpec[RemoveFile]],\n      deltaLog: DeltaLog,\n      snapshot: SnapshotDescriptor,\n      isStreaming: Boolean,\n      spark: SparkSession): Seq[DataFrame] = {\n    // Transform inputs to maps indexed by version and path and map each version to a CommitInfo\n    // object.\n    val versionToCommitInfo = MutableMap.empty[Long, CommitInfo]\n    val addFilesMap = addFileSpecs.flatMap { spec =>\n      spec.commitInfo.ifDefined { ci => versionToCommitInfo(spec.version) = ci }\n      spec.actions.map { action =>\n        val key =\n          FilePathWithTableVersion(action.path, spec.commitInfo, spec.version, spec.timestamp)\n        key -> action\n      }\n    }.toMap\n    val removeFilesMap = removeFileSpecs.flatMap { spec =>\n      spec.commitInfo.ifDefined { ci => versionToCommitInfo(spec.version) = ci }\n      spec.actions.map { action =>\n        val key =\n          FilePathWithTableVersion(action.path, spec.commitInfo, spec.version, spec.timestamp)\n        key -> action\n      }\n    }.toMap\n\n    val finalAddFiles = MutableMap[TableVersion, ListBuffer[AddFile]]()\n    val finalRemoveFiles = MutableMap[TableVersion, ListBuffer[RemoveFile]]()\n\n    // If a path is only being added, then scan it normally as inserted rows\n    (addFilesMap.keySet -- removeFilesMap.keySet).foreach { addKey =>\n      finalAddFiles\n        .getOrElseUpdate(new TableVersion(addKey), ListBuffer())\n        .append(addFilesMap(addKey))\n    }\n\n    // If a path is only being removed, then scan it normally as removed rows\n    (removeFilesMap.keySet -- addFilesMap.keySet).foreach { removeKey =>\n      finalRemoveFiles\n        .getOrElseUpdate(new TableVersion(removeKey), ListBuffer())\n        .append(removeFilesMap(removeKey))\n    }\n\n    // Convert maps back into Seq[CDCDataSpec] and feed it into a single scan. This will greatly\n    // reduce the number of tasks.\n    val finalAddFilesSpecs = buildCDCDataSpecSeq(finalAddFiles, versionToCommitInfo)\n    val finalRemoveFilesSpecs = buildCDCDataSpecSeq(finalRemoveFiles, versionToCommitInfo)\n\n    val dfAddsAndRemoves = ListBuffer[DataFrame]()\n\n    if (finalAddFilesSpecs.nonEmpty) {\n      dfAddsAndRemoves.append(\n        scanIndex(\n          spark,\n          new CdcAddFileIndex(spark, finalAddFilesSpecs, deltaLog, deltaLog.dataPath, snapshot),\n          isStreaming))\n    }\n\n    if (finalRemoveFilesSpecs.nonEmpty) {\n      dfAddsAndRemoves.append(\n        scanIndex(\n          spark,\n          new TahoeRemoveFileIndex(\n            spark,\n            finalRemoveFilesSpecs,\n            deltaLog,\n            deltaLog.dataPath,\n            snapshot),\n          isStreaming))\n    }\n\n    val dfGeneratedDvScanActions = processDeletionVectorActions(\n      addFilesMap,\n      removeFilesMap,\n      versionToCommitInfo.toMap,\n      deltaLog,\n      snapshot,\n      isStreaming,\n      spark)\n\n    dfAddsAndRemoves.toSeq ++ dfGeneratedDvScanActions\n  }\n\n  def processDeletionVectorActions(\n      addFilesMap: Map[FilePathWithTableVersion, AddFile],\n      removeFilesMap: Map[FilePathWithTableVersion, RemoveFile],\n      versionToCommitInfo: Map[Long, CommitInfo],\n      deltaLog: DeltaLog,\n      snapshot: SnapshotDescriptor,\n      isStreaming: Boolean,\n      spark: SparkSession): Seq[DataFrame] = {\n    val finalReplaceAddFiles = MutableMap[TableVersion, ListBuffer[AddFile]]()\n    val finalReplaceRemoveFiles = MutableMap[TableVersion, ListBuffer[RemoveFile]]()\n\n    val dvStore = DeletionVectorStore.createInstance(deltaLog.newDeltaHadoopConf())\n    (addFilesMap.keySet intersect removeFilesMap.keySet).foreach { key =>\n      val add = addFilesMap(key)\n      val remove = removeFilesMap(key)\n      val generatedActions = generateFileActionsWithInlineDv(add, remove, dvStore, deltaLog)\n      generatedActions.foreach {\n        case action: AddFile =>\n          finalReplaceAddFiles\n            .getOrElseUpdate(new TableVersion(key), ListBuffer())\n            .append(action)\n        case action: RemoveFile =>\n          finalReplaceRemoveFiles\n            .getOrElseUpdate(new TableVersion(key), ListBuffer())\n            .append(action)\n        case _ =>\n          throw new Exception(\"Expecting AddFile or RemoveFile.\")\n      }\n    }\n\n    // We have to build one scan for each version because DVs attached to actions will be\n    // broadcasted in [[ScanWithDeletionVectors.createBroadcastDVMap]] which is not version-aware.\n    // Here, one file can have different row index filters in different versions.\n    val dfs = ListBuffer[DataFrame]()\n    // Scan for masked rows as change_type = \"insert\",\n    // see explanation in [[generateFileActionsWithInlineDv]].\n    finalReplaceAddFiles.foreach { case (tableVersion, addFiles) =>\n      val commitInfo = versionToCommitInfo.get(tableVersion.version)\n      dfs.append(\n        scanIndex(\n          spark,\n          new CdcAddFileIndex(\n            spark,\n            Seq(new CDCDataSpec(tableVersion, addFiles.toSeq, commitInfo)),\n            deltaLog,\n            deltaLog.dataPath,\n            snapshot,\n            rowIndexFilters =\n              Some(fileActionsToIfNotContainedRowIndexFilters(addFiles.toSeq))),\n          isStreaming))\n    }\n\n    // Scan for masked rows as change_type = \"delete\",\n    // see explanation in [[generateFileActionsWithInlineDv]].\n    finalReplaceRemoveFiles.foreach { case (tableVersion, removeFiles) =>\n      val commitInfo = versionToCommitInfo.get(tableVersion.version)\n      dfs.append(\n        scanIndex(\n          spark,\n          new TahoeRemoveFileIndex(\n            spark,\n            Seq(new CDCDataSpec(tableVersion, removeFiles.toSeq, commitInfo)),\n            deltaLog,\n            deltaLog.dataPath,\n            snapshot,\n            rowIndexFilters =\n              Some(fileActionsToIfNotContainedRowIndexFilters(removeFiles.toSeq))),\n          isStreaming))\n    }\n\n    dfs.toSeq\n  }\n\n  /**\n   * Get the block of change data from start to end Delta log versions (both sides inclusive).\n   * The returned DataFrame has isStreaming set to false.\n   *\n   * @param readSchemaSnapshot The snapshot with the desired schema that will be used to\n   *                           serve this CDF batch. It is usually passed upstream from\n   *                           e.g. DeltaTableV2 as an effort to stablize the schema used for the\n   *                           batch DF. We don't actually use its data.\n   *                           If not set, it will fallback to the legacy behavior of using\n   *                           whatever deltaLog.unsafeVolatileSnapshot is. This should be\n   *                           avoided in production.\n   */\n  def changesToBatchDF(\n      deltaLog: DeltaLog,\n      start: Long,\n      end: Long,\n      spark: SparkSession,\n      catalogTableOpt: Option[CatalogTable] = None,\n      readSchemaSnapshot: Option[Snapshot] = None,\n      useCoarseGrainedCDC: Boolean = false,\n      startVersionSnapshot: Option[SnapshotDescriptor] = None): DataFrame = {\n\n    val changesWithinRange = deltaLog.getChanges(\n      start, end, catalogTableOpt, failOnDataLoss = false)\n    changesToDF(\n      readSchemaSnapshot.getOrElse(deltaLog.unsafeVolatileSnapshot),\n      start,\n      end,\n      changesWithinRange,\n      spark,\n      catalogTableOpt,\n      isStreaming = false,\n      useCoarseGrainedCDC = useCoarseGrainedCDC,\n      startVersionSnapshot = startVersionSnapshot)\n      .fileChangeDf\n  }\n\n  /**\n   * Build a dataframe from the specified file index. We can't use a DataFrame scan directly on the\n   * file names because that scan wouldn't include partition columns.\n   *\n   * It can optionally take a customReadSchema for the dataframe generated.\n   */\n  protected def scanIndex(\n      spark: SparkSession,\n      index: TahoeFileIndexWithSnapshotDescriptor,\n      isStreaming: Boolean = false): DataFrame = {\n\n    val relation = HadoopFsRelation(\n      location = index,\n      partitionSchema = index.partitionSchema,\n      dataSchema = cdcReadSchema(index.schema),\n      bucketSpec = None,\n      new DeltaParquetFileFormat(index.protocol, index.metadata, isCDCRead = true),\n      options = index.deltaLog.options)(spark)\n    val plan = LogicalRelation(relation, isStreaming = isStreaming)\n    DataFrameUtils.ofRows(spark, plan)\n  }\n\n  /**\n   * Based on the read options passed it indicates whether the read was a cdc read or not.\n   */\n  def isCDCRead(options: CaseInsensitiveStringMap): Boolean = {\n    // Consistent with DeltaOptions.readChangeFeed,\n    // but CDCReader use CaseInsensitiveStringMap vs. CaseInsensitiveMap used by DataFrameReader.\n    def toBoolean(input: String, name: String): Boolean = {\n      Try(input.toBoolean).toOption.getOrElse {\n        throw DeltaErrors.illegalDeltaOptionException(name, input, \"must be 'true' or 'false'\")\n      }\n    }\n\n    val cdcEnabled = options.containsKey(DeltaDataSource.CDC_ENABLED_KEY) &&\n      toBoolean(options.get(DeltaDataSource.CDC_ENABLED_KEY), DeltaDataSource.CDC_ENABLED_KEY)\n\n    val cdcLegacyConfEnabled = options.containsKey(DeltaDataSource.CDC_ENABLED_KEY_LEGACY) &&\n      toBoolean(\n        options.get(DeltaDataSource.CDC_ENABLED_KEY_LEGACY), DeltaDataSource.CDC_ENABLED_KEY_LEGACY)\n\n    cdcEnabled || cdcLegacyConfEnabled\n  }\n\n  /**\n   * Determine if the metadata provided has cdc enabled or not.\n   */\n  def isCDCEnabledOnTable(metadata: Metadata, spark: SparkSession): Boolean = {\n    ChangeDataFeedTableFeature.metadataRequiresFeatureToBeEnabled(\n      protocol = Protocol(), metadata, spark)\n  }\n\n  /**\n   * Check metadata changes in CDC enabled tables.\n   *\n   * - Check that CDC is not enabled in a table that contains columns reserved by CDC.\n   * - Check that columns reserved by CDC are not added to a table with CDC enabled.\n   */\n  def checkMetadataChange(\n      spark: SparkSession,\n      newMetadata: Metadata,\n      oldMetadata: Metadata): Unit = {\n    if (!isCDCEnabledOnTable(newMetadata, spark)) {\n      return\n    }\n\n    val newSchema = newMetadata.schema.fieldNames\n    val reservedColumnsUsed =\n      CDCReader.cdcReadSchema(new StructType()).fieldNames.intersect(newSchema)\n    if (reservedColumnsUsed.length > 0) {\n      if (!isCDCEnabledOnTable(oldMetadata, spark)) {\n        // cdc was not enabled previously but reserved columns are present in the new schema.\n        throw DeltaErrors.tableAlreadyContainsCDCColumns(reservedColumnsUsed)\n      } else {\n        // cdc was enabled but reserved columns are present in the new metadata.\n        throw DeltaErrors.cdcColumnsInData(reservedColumnsUsed)\n      }\n    }\n  }\n\n  /**\n   * Given `add` and `remove` actions of the same file, manipulate DVs to get rows that are deleted\n   * and re-added from `add` to `remove`.\n   *\n   * @return One or more [[AddFile]] and [[RemoveFile]], corresponding to CDC change_type \"insert\"\n   *         and \"delete\". Rows masked by inline DVs are changed rows.\n   */\n  private def generateFileActionsWithInlineDv(\n      add: AddFile,\n      remove: RemoveFile,\n      dvStore: DeletionVectorStore,\n      deltaLog: DeltaLog): Seq[FileAction] = {\n\n    val removeDvOpt = Option(remove.deletionVector)\n    val addDvOpt = Option(add.deletionVector)\n\n    val newActions = ListBuffer[FileAction]()\n\n    // Four cases:\n    // 1) Remove without DV, add without DV:\n    //    Not possible. This case has been handled before.\n    // 2) Remove without DV, add with DV1:\n    //    Rows masked by DV1 are deleted.\n    // 3) Remove with DV1, add without DV:\n    //    Rows masked by DV1 are added. May happen when restoring a table.\n    // 4) Remove with DV1, add with DV2:\n    //   a) Rows masked by DV2 but not DV1 are deleted.\n    //   b) Rows masked by DV1 but not DV2 are re-added. May happen when restoring a table.\n    (removeDvOpt, addDvOpt) match {\n      case (None, None) =>\n        throw new Exception(\"Expecting one or both of add and remove contain DV.\")\n      case (None, Some(addDv)) =>\n        newActions += remove.copy(deletionVector = addDv)\n      case (Some(removeDv), None) =>\n        newActions += add.copy(deletionVector = removeDv)\n      case (Some(removeDv), Some(addDv)) =>\n        val removeBitmap = dvStore.read(removeDv, deltaLog.dataPath)\n        val addBitmap = dvStore.read(addDv, deltaLog.dataPath)\n\n        // Case 4a\n        val finalRemovedRowsBitmap = getDeletionVectorsDiff(addBitmap, removeBitmap)\n        // Case 4b\n        val finalReAddedRowsBitmap = getDeletionVectorsDiff(removeBitmap, addBitmap)\n\n        val finalRemovedRowsDv = DeletionVectorDescriptor.inlineInLog(\n          DeletionVectorUtils.serialize(\n            finalRemovedRowsBitmap, RoaringBitmapArrayFormat.Portable, Some(deltaLog.dataPath)),\n          finalRemovedRowsBitmap.cardinality)\n        val finalReAddedRowsDv = DeletionVectorDescriptor.inlineInLog(\n          DeletionVectorUtils.serialize(\n            finalReAddedRowsBitmap, RoaringBitmapArrayFormat.Portable, Some(deltaLog.dataPath)),\n          finalReAddedRowsBitmap.cardinality)\n\n        newActions += remove.copy(deletionVector = finalRemovedRowsDv)\n        newActions += add.copy(deletionVector = finalReAddedRowsDv)\n    }\n\n    newActions.toSeq\n  }\n\n  /**\n   * Return a map of file paths to IfNotContained row index filters, to keep only the marked rows.\n   */\n  private def fileActionsToIfNotContainedRowIndexFilters(\n      actions: Seq[FileAction]): Map[String, RowIndexFilterType] = {\n    actions.map(f => f.path -> RowIndexFilterType.IF_NOT_CONTAINED).toMap\n  }\n\n  /**\n   * Get a new [[RoaringBitmapArray]] copy storing values that are in `left` but not in `right`.\n   */\n  private def getDeletionVectorsDiff(\n      left: RoaringBitmapArray,\n      right: RoaringBitmapArray): RoaringBitmapArray = {\n    val leftCopy = left.copy()\n    leftCopy.diff(right)\n    leftCopy\n  }\n\n  private def buildCDCDataSpecSeq[T <: FileAction](\n      actionsByVersion: MutableMap[TableVersion, ListBuffer[T]],\n      versionToCommitInfo: MutableMap[Long, CommitInfo]\n  ): Seq[CDCDataSpec[T]] = actionsByVersion.map { case (fileVersion, addFiles) =>\n    val commitInfo = versionToCommitInfo.get(fileVersion.version)\n    new CDCDataSpec(fileVersion, addFiles.toSeq, commitInfo)\n  }.toSeq\n\n  override def getConstructedCDCRelation(\n    snapshotWithSchema: SnapshotWithSchemaMode,\n    sqlContext: SQLContext,\n    catalogTableOpt: Option[CatalogTable],\n    startingVersion: Option[Long],\n    endingVersion: Option[Long]): BaseRelation = {\n    DeltaCDFRelation(\n      snapshotWithSchema,\n      sqlContext,\n      catalogTableOpt,\n      startingVersion,\n      endingVersion\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/cdc/CDCReaderBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.cdc\n\nimport java.sql.Timestamp\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader.{cdcReadSchema, DeltaCDFRelation}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.{DeltaDataSource, DeltaSource, DeltaSQLConf}\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.sql.{Column, DataFrame, Row, SparkSession, SQLContext}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{And, Attribute, Expression, Literal}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.sources.{BaseRelation, CatalystScan, Filter}\nimport org.apache.spark.sql.types.{LongType, StringType, StructType, TimestampType}\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Represents a Delta log version, and how the version is determined.\n * @param version the determined version.\n * @param timestamp the commit timestamp of the determined version. Will be filled when the\n *                  version is determined by timestamp.\n */\ncase class ResolvedCDFVersion(version: Long, timestamp: Option[Timestamp]) {\n  /** Whether this version is resolved by timestamp. */\n  def resolvedByTimestamp: Boolean = timestamp.isDefined\n}\n\n// A snapshot coupled with a schema mode that user specified\ncase class SnapshotWithSchemaMode(snapshot: Snapshot, schemaMode: DeltaBatchCDFSchemaMode)\n\n/**\n * A special BaseRelation wrapper for CDF reads.\n */\nabstract class DeltaCDFRelationBase(\n    snapshotWithSchemaMode: SnapshotWithSchemaMode,\n    sqlContext: SQLContext,\n    catalogTableOpt: Option[CatalogTable],\n    startingVersion: Option[Long],\n    endingVersion: Option[Long]) extends BaseRelation with CatalystScan {\n\n  protected val deltaLog: DeltaLog = snapshotWithSchemaMode.snapshot.deltaLog\n\n  protected lazy val latestVersionOfTableDuringAnalysis: Long =\n    deltaLog.update(catalogTableOpt = catalogTableOpt).version\n\n  /**\n   * There may be a slight divergence here in terms of what schema is in the latest data vs what\n   * schema we have captured during analysis, but this is an inherent limitation of Spark.\n   *\n   * However, if there are schema changes between analysis and execution, since we froze this\n   * schema, our schema incompatibility checks will kick in during the scan so we will always\n   * be safe - Although it is a notable caveat that user should be aware of because the CDC query\n   * may break.\n   */\n  protected lazy val endingVersionForBatchSchema: Long = endingVersion.map { v =>\n    // As defined in the method doc, if ending version is greater than the latest version, we will\n    // just use the latest version to find the schema.\n    latestVersionOfTableDuringAnalysis min v\n  }.getOrElse {\n    // Or if endingVersion is not specified, we just use the latest schema.\n    latestVersionOfTableDuringAnalysis\n  }\n\n  // The final snapshot whose schema is going to be used as this CDF relation's schema\n  protected val snapshotForBatchSchema: Snapshot = snapshotWithSchemaMode.schemaMode match {\n    case BatchCDFSchemaEndVersion =>\n      // Fetch the ending version and its schema\n      deltaLog.getSnapshotAt(endingVersionForBatchSchema, catalogTableOpt = catalogTableOpt)\n    case _ =>\n      // Apply the default, either latest generated by DeltaTableV2 or specified by Time-travel\n      // options.\n      snapshotWithSchemaMode.snapshot\n  }\n\n  override val schema: StructType = {\n    cdcReadSchema(\n      DeltaTableUtils.removeInternalDeltaMetadata(\n        sqlContext.sparkSession,\n        DeltaTableUtils.removeInternalWriterMetadata(\n          sqlContext.sparkSession, snapshotForBatchSchema.metadata.schema\n        )\n      )\n    )\n  }\n\n  override def unhandledFilters(filters: Array[Filter]): Array[Filter] = Array.empty\n\n  protected def constructRDD(\n      df: DataFrame,\n      requiredColumns: Seq[Attribute],\n      filters: Seq[Expression]): RDD[Row] = {\n    // Rewrite the attributes in the required columns and\n    // pushed down filters to match the output of the internal DataFrame.\n    val outputMap = df.queryExecution.analyzed.output.map(a => a.name -> a).toMap\n    val projections =\n      requiredColumns.map(a => Column(outputMap(a.name)))\n    val filter = Column(\n      filters\n        .map(_.transform { case a: Attribute => outputMap(a.name) })\n        .reduceOption(And)\n        .getOrElse(Literal.TrueLiteral)\n    )\n\n    df.filter(filter).select(projections: _*).rdd\n  }\n}\n\n/**\n * Base trait for CDC readers that contains common functionality\n * shared across different CDC reader implementations.\n */\ntrait CDCReaderBase extends DeltaLogging {\n  /**\n   * Given timestamp or version, this method returns the corresponding version for that timestamp\n   * or the version itself, as well as how the return version is obtained: by `version` or\n   * `timestamp`.\n   */\n  private def getVersionForCDC(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      conf: SQLConf,\n      options: CaseInsensitiveStringMap,\n      versionKey: String,\n      timestampKey: String): Option[ResolvedCDFVersion] = {\n    if (options.containsKey(versionKey)) {\n      val version = options.get(versionKey)\n      try {\n        Some(ResolvedCDFVersion(version.toLong, timestamp = None))\n      } catch {\n        case _: NumberFormatException => throw DeltaErrors.versionInvalid(version)\n      }\n    } else if (options.containsKey(timestampKey)) {\n      val ts = options.get(timestampKey)\n      val spec = DeltaTimeTravelSpec(Some(Literal(ts)), None, Some(\"cdcReader\"))\n      val timestamp = spec.getTimestamp(spark.sessionState.conf)\n      val allowOutOfRange = conf.getConf(DeltaSQLConf.DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP)\n      val resolvedVersion = if (timestampKey == DeltaDataSource.CDC_START_TIMESTAMP_KEY) {\n        // For the starting timestamp we need to find a version after the provided timestamp\n        // we can use the same semantics as streaming.\n        DeltaSource.getStartingVersionFromTimestamp(\n          spark, deltaLog, catalogTableOpt, timestamp, allowOutOfRange)\n      } else {\n        // For ending timestamp the version should be before the provided timestamp.\n        DeltaTableUtils.resolveTimeTravelVersion(\n          conf, deltaLog, catalogTableOpt, spec, allowOutOfRange)._1\n      }\n      Some(ResolvedCDFVersion(resolvedVersion, Some(timestamp)))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Get the batch cdf schema mode for a table, considering whether it has column mapping enabled\n   * or not.\n   */\n  def getBatchSchemaModeForTable(\n      spark: SparkSession,\n      columnMappingEnabled: Boolean): DeltaBatchCDFSchemaMode = {\n    if (columnMappingEnabled) {\n      // Tables with column-mapping enabled can specify which schema version to use with this\n      // config.\n      DeltaBatchCDFSchemaMode(spark.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE))\n    } else {\n      // Non column-mapping table uses the current default, which is typically `legacy` - usually\n      // the latest schema is used, but it can depend on time-travel arguments as well.\n      BatchCDFSchemaLegacy\n    }\n  }\n\n  /**\n   * Get a Relation that represents change data between two snapshots of the table.\n   *\n   * @param spark Spark session\n   * @param snapshotToUse Snapshot to use to provide read schema and version\n   * @param isTimeTravelQuery Whether this CDC scan is used in conjunction with time-travel args\n   * @param conf SQL conf\n   * @param options CDC specific options\n   */\n  def getCDCRelation(\n      spark: SparkSession,\n      snapshotToUse: Snapshot,\n      catalogTableOpt: Option[CatalogTable],\n      isTimeTravelQuery: Boolean,\n      conf: SQLConf,\n      options: CaseInsensitiveStringMap): BaseRelation = {\n    val startingVersion = getVersionForCDC(\n      spark,\n      snapshotToUse.deltaLog,\n      catalogTableOpt,\n      conf,\n      options,\n      DeltaDataSource.CDC_START_VERSION_KEY,\n      DeltaDataSource.CDC_START_TIMESTAMP_KEY).getOrElse {\n      throw DeltaErrors.noStartVersionForCDC()\n    }\n\n    val endingVersionOpt = getVersionForCDC(\n      spark,\n      snapshotToUse.deltaLog,\n      catalogTableOpt,\n      conf,\n      options,\n      DeltaDataSource.CDC_END_VERSION_KEY,\n      DeltaDataSource.CDC_END_TIMESTAMP_KEY\n    )\n\n    verifyStartingVersion(spark, snapshotToUse, catalogTableOpt, conf, startingVersion) match {\n      case Some(toReturn) =>\n        return toReturn\n      case None =>\n    }\n\n    verifyEndingVersion(\n      spark, snapshotToUse, catalogTableOpt, startingVersion, endingVersionOpt) match {\n      case Some(toReturn) =>\n        return toReturn\n      case None =>\n    }\n\n    logInfo(\n      log\"startingVersion: ${MDC(DeltaLogKeys.START_VERSION, startingVersion.version)}, \" +\n        log\"endingVersion: ${MDC(DeltaLogKeys.END_VERSION, endingVersionOpt.map(_.version))}\")\n\n    val startingSnapshot = snapshotToUse.deltaLog.getSnapshotAt(\n      startingVersion.version,\n      catalogTableOpt = catalogTableOpt,\n      enforceTimeTravelWithinDeletedFileRetention = true)\n    val columnMappingEnabledAtStartingVersion =\n      startingSnapshot.metadata.columnMappingMode != NoMapping\n\n    val columnMappingEnabledAtEndVersion = endingVersionOpt.exists { endingVersion =>\n      // End version could be after the snapshot to use version, in which case it might not exist.\n      if (endingVersion.version > snapshotToUse.version) {\n        false\n      } else {\n        val endingSnapshot = snapshotToUse.deltaLog.getSnapshotAt(endingVersion.version,\n          catalogTableOpt = catalogTableOpt)\n        endingSnapshot.metadata.columnMappingMode != NoMapping &&\n          endingVersion.version <= snapshotToUse.version\n      }\n    }\n\n    val columnMappingEnabledAtSnapshotToUseVersion =\n      snapshotToUse.metadata.columnMappingMode != NoMapping\n\n    // Special handling for tables with column mapping mode enabled in any of the versions.\n    val columnMappingEnabled = columnMappingEnabledAtSnapshotToUseVersion ||\n      columnMappingEnabledAtEndVersion || columnMappingEnabledAtStartingVersion\n    val schemaMode = getBatchSchemaModeForTable(spark, columnMappingEnabled = columnMappingEnabled)\n\n    // Non-legacy schema mode options cannot be used with time-travel because the schema to use\n    // will be confusing.\n    if (isTimeTravelQuery && schemaMode != BatchCDFSchemaLegacy) {\n      throw DeltaErrors.illegalDeltaOptionException(\n        DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key,\n        schemaMode.name,\n        s\"${DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key} \" +\n          s\"cannot be used with time travel options.\")\n    }\n\n    getConstructedCDCRelation(\n      SnapshotWithSchemaMode(snapshotToUse, schemaMode),\n      spark.sqlContext,\n      catalogTableOpt,\n      Some(startingVersion.version),\n      endingVersionOpt.map(_.version)\n    )\n  }\n\n  private def verifyStartingVersion(\n      spark: SparkSession,\n      snapshotToUse: Snapshot,\n      catalogTableOpt: Option[CatalogTable],\n      conf: SQLConf,\n      startingVersion: ResolvedCDFVersion): Option[BaseRelation] = {\n    // add a version check here that is cheap instead of after trying to list a large version\n    // that doesn't exist\n    if (startingVersion.version > snapshotToUse.version) {\n      val allowOutOfRange = conf.getConf(DeltaSQLConf.DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP)\n      if (allowOutOfRange) {\n        return Some(emptyCDFRelation(spark, snapshotToUse, catalogTableOpt, BatchCDFSchemaLegacy))\n      }\n      throw DeltaErrors.startVersionAfterLatestVersion(\n        startingVersion.version, snapshotToUse.version)\n    }\n    None\n  }\n\n  private def verifyEndingVersion(\n      spark: SparkSession,\n      snapshotToUse: Snapshot,\n      catalogTableOpt: Option[CatalogTable],\n      startingVersion: ResolvedCDFVersion,\n      endingVersionOpt: Option[ResolvedCDFVersion]): Option[BaseRelation] = {\n    // Given two timestamps, there is a case when both of them lay closely between two versions:\n    // version:          4                                                 5\n    //          ---------|-------------------------------------------------|--------\n    //                           ^ start timestamp        ^ end timestamp\n    // In this case the starting version will be 5 and ending version will be 4. We must not\n    // throw `endBeforeStartVersionInCDC` but return empty result.\n    endingVersionOpt.foreach { endingVersion =>\n      if (startingVersion.resolvedByTimestamp && endingVersion.resolvedByTimestamp) {\n        // The next `if` is true when end is less than start but no commit is in between.\n        // We need to capture such a case and throw early.\n        if (startingVersion.timestamp.get.after(endingVersion.timestamp.get)) {\n          throw DeltaErrors.endBeforeStartVersionInCDC(\n            startingVersion.version,\n            endingVersion.version)\n        }\n        if (endingVersion.version == startingVersion.version - 1) {\n          return Some(emptyCDFRelation(spark, snapshotToUse, catalogTableOpt, BatchCDFSchemaLegacy))\n        }\n      }\n      if (endingVersionOpt.exists(_.version < startingVersion.version)) {\n        throw DeltaErrors.endBeforeStartVersionInCDC(\n          startingVersion.version,\n          endingVersionOpt.get.version)\n      }\n    }\n    None\n  }\n\n  private def emptyCDFRelation(\n      spark: SparkSession,\n      snapshot: Snapshot,\n      catalogTableOpt: Option[CatalogTable],\n      schemaMode: DeltaBatchCDFSchemaMode) = {\n    new DeltaCDFRelation(\n      SnapshotWithSchemaMode(snapshot, schemaMode),\n      spark.sqlContext,\n      catalogTableOpt,\n      startingVersion = None,\n      endingVersion = None) {\n      override def buildScan(requiredColumns: Seq[Attribute], filters: Seq[Expression]): RDD[Row] =\n        sqlContext.sparkSession.sparkContext.emptyRDD[Row]\n    }\n  }\n\n  /**\n   * Append CDC metadata columns to the provided schema.\n   */\n  def cdcReadSchema(deltaSchema: StructType): StructType = {\n    deltaSchema\n      .add(CDCReader.CDC_TYPE_COLUMN_NAME, StringType)\n      .add(CDCReader.CDC_COMMIT_VERSION, LongType)\n      .add(CDCReader.CDC_COMMIT_TIMESTAMP, TimestampType)\n  }\n\n  /**\n   * Check metadata (which may contain schema change)'s read compatibility with read schema.\n   */\n  protected def checkBatchCdfReadSchemaIncompatibility(\n      readSchemaSnapshot: SnapshotDescriptor,\n      start: Long,\n      end: Long,\n      shouldCheckSchemaToBlockBatchRead: Boolean,\n      metadata: Metadata,\n      metadataVer: Long,\n      isSchemaChange: Boolean): Unit = {\n    // We do not check for any incompatibility if the global \"I don't care\" flag is turned on\n    if (shouldCheckSchemaToBlockBatchRead) {\n      // Column mapping incompatibilities\n      val compatible = {\n        // For column mapping schema change, the order matters because we don't want to treat\n        // an ADD COLUMN as an inverse DROP COLUMN.\n        if (metadataVer <= readSchemaSnapshot.version) {\n          DeltaColumnMapping.hasNoColumnMappingSchemaChanges(\n            newMetadata = readSchemaSnapshot.metadata, oldMetadata = metadata)\n        } else {\n          DeltaColumnMapping.hasNoColumnMappingSchemaChanges(\n            newMetadata = metadata, oldMetadata = readSchemaSnapshot.metadata)\n        }\n      } && {\n        // Other standard read incompatibilities\n        if (metadataVer <= readSchemaSnapshot.version) {\n          // If the metadata is before the read schema version, make sure:\n          // a) metadata schema is a part of the read schema, i.e. only ADD COLUMN can evolve\n          //    metadata schema into read schema\n          // b) data type for common fields remain the same\n          // c) metadata schema should not contain field that is nullable=true but the read schema\n          //    is nullable=false.\n          SchemaUtils.isReadCompatible(\n            existingSchema = metadata.schema,\n            readSchema = readSchemaSnapshot.schema,\n            forbidTightenNullability = true)\n        } else {\n          // If the metadata is POST the read schema version, which can happen due to time-travel\n          // or simply a divergence between analyzed version and the actual latest\n          // version during scan, we will make sure the other way around:\n          // a) the metadata must be a super set of the read schema, i.e. only ADD COLUMN can\n          //    evolve read schema into metadata schema\n          // b) data type for common fields remain the same\n          // c) read schema should not contain field that is nullable=false but the metadata\n          //    schema has nullable=true.\n          SchemaUtils.isReadCompatible(\n            existingSchema = readSchemaSnapshot.schema,\n            readSchema = metadata.schema,\n            forbidTightenNullability = false)\n        }\n      }\n\n      if (!compatible) {\n        throw DeltaErrors.blockBatchCdfReadWithIncompatibleSchemaChange(\n          start, end,\n          // The consistent read schema\n          readSchemaSnapshot.metadata.schema, readSchemaSnapshot.version,\n          // The conflicting schema or schema change version\n          metadataVer,\n          isSchemaChange\n        )\n      }\n    }\n  }\n\n  def shouldCheckSchemaToBlockBatchRead(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      isStreaming: Boolean): Boolean = {\n    // Check schema read-compatibility\n    val allowUnsafeBatchReadOnIncompatibleSchemaChanges =\n      spark.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_CDF_UNSAFE_BATCH_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES)\n\n    if (allowUnsafeBatchReadOnIncompatibleSchemaChanges) {\n      recordDeltaEvent(deltaLog, \"delta.unsafe.cdf.readOnColumnMappingSchemaChanges\")\n    }\n\n    !isStreaming && !allowUnsafeBatchReadOnIncompatibleSchemaChanges\n  }\n\n  def getConstructedCDCRelation(\n      snapshotWithSchemaMode: SnapshotWithSchemaMode,\n      sqlContext: SQLContext,\n      catalogTableOpt: Option[CatalogTable],\n      startingVersion: Option[Long],\n      endingVersion: Option[Long]): BaseRelation\n\n  /**\n   * Builds a map from commit versions to associated commit timestamps where the timestamp\n   * is the modification time of the commit file. Note that this function will not return\n   * InCommitTimestamps, it is up to the consumer of this function to decide whether the\n   * file modification time is the correct commit timestamp or whether they need to read the ICT.\n   *\n   * @param start  start commit version\n   * @param end  end commit version (inclusive)\n   */\n  def getNonICTTimestampsByVersion(\n      deltaLog: DeltaLog,\n      start: Long,\n      end: Long): Map[Long, Timestamp] = {\n    // Correct timestamp values are only available through DeltaHistoryManager.getCommits(). Commit\n    // info timestamps are wrong, and file modification times are wrong because they need to be\n    // monotonized first. This just performs a list (we don't read the contents of the files in\n    // getCommits()) so the performance overhead is minimal.\n    val monotonizationStart =\n      math.max(start - DeltaHistoryManager.POTENTIALLY_UNMONOTONIZED_TIMESTAMPS, 0)\n    val commits = DeltaHistoryManager.getCommitsWithNonIctTimestamps(\n      deltaLog.store,\n      deltaLog.logPath,\n      monotonizationStart,\n      Some(end + 1),\n      deltaLog.newDeltaHadoopConf())\n\n    // Note that the timestamps come from filesystem modification timestamps, so they're\n    // milliseconds since epoch and we don't need to deal with timezones.\n    commits.map(f => (f.version -> new Timestamp(f.timestamp))).toMap\n  }\n\n  /**\n   * Represents the changes between some start and end version of a Delta table\n   * @param fileChangeDf contains all of the file changes (AddFile, RemoveFile, AddCDCFile)\n   * @param numFiles the number of AddFile + RemoveFile + AddCDCFiles that are in the df\n   * @param numBytes the total size of the AddFile + RemoveFile + AddCDCFiles that are in the df\n   */\n  case class CDCVersionDiffInfo(fileChangeDf: DataFrame, numFiles: Long, numBytes: Long)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/columnmapping/RemoveColumnMappingCommand.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.columnmapping\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.schema.{ImplicitMetadataOperation, SchemaUtils}\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A command to remove the column mapping from a table.\n */\nclass RemoveColumnMappingCommand(\n    val deltaLog: DeltaLog,\n    val catalogOpt: Option[CatalogTable])\n  extends ImplicitMetadataOperation {\n  override protected val canMergeSchema: Boolean = false\n  override protected val canOverwriteSchema: Boolean = true\n\n  /**\n   * Remove the column mapping from the table.\n   * @param removeColumnMappingTableProperty - whether to remove the column mapping property from\n   *                                         the table instead of setting it to 'none'\n   */\n  def run(spark: SparkSession, removeColumnMappingTableProperty: Boolean): Unit = {\n    deltaLog.withNewTransaction(catalogOpt) { txn =>\n      val originalFiles = txn.filterFiles()\n      val originalData = buildDataFrame(txn, originalFiles)\n      val originalSchema = txn.snapshot.schema\n      val newSchema = DeltaColumnMapping.dropColumnMappingMetadata(originalSchema)\n      verifySchemaFieldNames(newSchema)\n\n      updateMetadata(removeColumnMappingTableProperty, txn, newSchema)\n\n      val deltaOptions = getDeltaOptionsForWrite(spark)\n      val addedFiles = writeData(txn, originalData, deltaOptions)\n      val removeFileActions = originalFiles.map(_.removeWithTimestamp(dataChange = false))\n\n      txn.commit(actions = removeFileActions ++ addedFiles,\n        op = DeltaOperations.RemoveColumnMapping(),\n        tags = RowTracking.addPreservedRowTrackingTagIfNotSet(txn.snapshot)\n      )\n    }\n  }\n\n  /**\n   * Verify none of the schema fields contain invalid column names.\n   */\n  def verifySchemaFieldNames(schema: StructType): Unit = {\n    val invalidColumnNames =\n      SchemaUtils.findInvalidColumnNamesInSchema(schema)\n    if (invalidColumnNames.nonEmpty) {\n      throw DeltaErrors\n        .foundInvalidColumnNamesWhenRemovingColumnMapping(invalidColumnNames)\n    }\n  }\n\n  /**\n   * Update the metadata to remove the column mapping table properties and\n   * update the schema to remove the column mapping metadata.\n   */\n  def updateMetadata(\n      removeColumnMappingTableProperty: Boolean,\n      txn: OptimisticTransaction,\n      newSchema: StructType): Unit = {\n    val newConfiguration =\n      getConfigurationWithoutColumnMapping(txn, removeColumnMappingTableProperty)\n    val newMetadata = txn.metadata.copy(\n      schemaString = newSchema.json,\n      configuration = newConfiguration)\n    txn.updateMetadata(newMetadata)\n  }\n\n  def getConfigurationWithoutColumnMapping(\n      txn: OptimisticTransaction,\n      removeColumnMappingTableProperty: Boolean): Map[String, String] = {\n    // Scanned schema does not include the column mapping metadata and can be reused as is.\n    val columnMappingPropertyKey = DeltaConfigs.COLUMN_MAPPING_MODE.key\n    val columnMappingMaxIdPropertyKey = DeltaConfigs.COLUMN_MAPPING_MAX_ID.key\n    // Unset or overwrite the column mapping mode to none and remove max id property\n    // while keeping other properties.\n    (if (removeColumnMappingTableProperty) {\n      txn.metadata.configuration - columnMappingPropertyKey\n    } else {\n      txn.metadata.configuration + (columnMappingPropertyKey -> \"none\")\n    }) - columnMappingMaxIdPropertyKey\n  }\n\n  def getDeltaOptionsForWrite(spark: SparkSession): DeltaOptions = {\n    new DeltaOptions(\n      // Prevent files from being split by writers.\n      Map(DeltaOptions.MAX_RECORDS_PER_FILE -> \"0\"),\n      spark.sessionState.conf)\n  }\n\n  def buildDataFrame(\n      txn: OptimisticTransaction,\n      originalFiles: Seq[AddFile]): DataFrame =\n    recordDeltaOperation(txn.deltaLog, \"delta.removeColumnMapping.setupDataFrame\") {\n      txn.deltaLog.createDataFrame(txn.snapshot, originalFiles)\n    }\n\n  def writeData(\n      txn: OptimisticTransaction,\n      data: DataFrame,\n      deltaOptions: DeltaOptions): Seq[AddFile] = {\n    txn.writeFiles(\n      inputData = RowTracking.preserveRowTrackingColumns(data, txn.snapshot),\n      writeOptions = Some(deltaOptions),\n      isOptimize = true,\n      additionalConstraints = Seq.empty)\n      .asInstanceOf[Seq[AddFile]]\n      // Mark as no data change to not generate CDC data. We are only removing column mapping.\n      .map(_.copy(dataChange = false))\n  }\n}\n\nobject RemoveColumnMappingCommand {\n  def apply(\n      deltaLog: DeltaLog,\n      catalogOpt: Option[CatalogTable]): RemoveColumnMappingCommand = {\n    new RemoveColumnMappingCommand(deltaLog, catalogOpt)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/convert/ConvertUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.lang.reflect.InvocationTargetException\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaErrors, SerializableFileStatus, Snapshot}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{DateFormatter, DeltaFileOperations, PartitionUtils, TimestampFormatter}\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.sql.{AnalysisException, Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.Cast\nimport org.apache.spark.sql.connector.catalog.Table\nimport org.apache.spark.sql.execution.datasources.PartitioningUtils\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{StringType, StructType}\nimport org.apache.spark.util.{SerializableConfiguration, Utils}\n\nobject ConvertUtils extends ConvertUtilsBase\n\ntrait ConvertUtilsBase extends DeltaLogging {\n\n  val timestampPartitionPattern = \"yyyy-MM-dd HH:mm:ss[.S]\"\n\n  var icebergSparkTableClassPath =\n    \"org.apache.spark.sql.delta.commands.convert.IcebergTable\"\n  var icebergLibTableClassPath = \"shadedForDelta.org.apache.iceberg.Table\"\n\n  /**\n   * Creates a source Parquet table for conversion.\n   *\n   * @param spark: the spark session to use.\n   * @param targetDir: the target directory of the Parquet table.\n   * @param catalogTable: the optional catalog table of the Parquet table.\n   * @param partitionSchema: the user provided partition schema (if exists) of the Parquet table.\n   * @return a target Parquet table.\n   */\n  def getParquetTable(\n      spark: SparkSession,\n      targetDir: String,\n      catalogTable: Option[CatalogTable],\n      partitionSchema: Option[StructType]): ConvertTargetTable = {\n    val qualifiedDir = getQualifiedPath(spark, new Path(targetDir)).toString\n    new ParquetTable(spark, qualifiedDir, catalogTable, partitionSchema)\n  }\n\n  /**\n   * Creates a source Iceberg table for conversion.\n   *\n   * @param spark: the spark session to use.\n   * @param targetDir: the target directory of the Iceberg table.\n   * @param sparkTable: the optional V2 table interface of the Iceberg table.\n   * @param deltaTable: the existing converted Delta table (if exists) of the Iceberg table.\n   * @param collectStats: collect column stats on convert\n   * @return a target Iceberg table.\n   */\n  def getIcebergTable(\n      spark: SparkSession,\n      targetDir: String,\n      deltaSnapshotOpt: Option[Snapshot],\n      collectStats: Boolean = true): ConvertTargetTable = {\n    try {\n      val convertIcebergStats = collectStats &&\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_ICEBERG_STATS)\n      val clazz = Utils.classForName(icebergSparkTableClassPath)\n      val baseDir = getQualifiedPath(spark, new Path(targetDir)).toString\n      val constFromPath = clazz.getConstructor(\n        classOf[SparkSession], classOf[String], classOf[Option[Snapshot]],\n        java.lang.Boolean.TYPE)\n      constFromPath.newInstance(spark, baseDir, deltaSnapshotOpt,\n        java.lang.Boolean.valueOf(convertIcebergStats))\n    } catch {\n      case e: ClassNotFoundException =>\n        logError(log\"Failed to find Iceberg class\", e)\n        throw DeltaErrors.icebergClassMissing(spark.sparkContext.getConf, e)\n      case e: InvocationTargetException =>\n        logError(log\"Got error when creating an Iceberg Converter\", e)\n        // The better error is within the cause\n        throw ExceptionUtils.getRootCause(e)\n    }\n  }\n\n  /**\n   * Get Iceberg metadata location from spark catalog resolved Iceberg table,\n   * which means it is a SparkTable\n   * Needs to use reflection because shaded Iceberg classes are not accessible here\n   * It is equivalent to call\n   *  table.asInstanceOf[SparkTable].table().operations().current().metadataFileLocation()\n   * @param table the iceberg table resolved spark catalog\n   * @return metadata location corresponding to table's latest snapshot\n   */\n  def getIcebergMetadataLocationFromSparkTable(table: Table): String = {\n    val tableMethod = table.getClass.getMethod(\"table\")\n    val icebergTable = tableMethod.invoke(table)\n    val operationsMethod = icebergTable.getClass.getMethod(\"operations\")\n    val operations = operationsMethod.invoke(icebergTable)\n    val currentMethod = operations.getClass.getMethod(\"current\")\n    val currentMetadata = currentMethod.invoke(operations)\n    val metadataFileLocationMethod =\n      currentMetadata.getClass.getMethod(\"metadataFileLocation\")\n    metadataFileLocationMethod.invoke(currentMetadata).asInstanceOf[String]\n  }\n\n  /**\n   * Generates a qualified Hadoop path from a given path.\n   *\n   * @param spark: the spark session to use\n   * @param path: the raw path used to generate the qualified path.\n   * @return the qualified path of the provided raw path.\n   */\n  def getQualifiedPath(spark: SparkSession, path: Path): Path = {\n    // scalastyle:off deltahadoopconfiguration\n    val sessionHadoopConf = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    val fs = path.getFileSystem(sessionHadoopConf)\n    val qualifiedPath = fs.makeQualified(path)\n    if (!fs.exists(qualifiedPath)) {\n      throw DeltaErrors.directoryNotFoundException(qualifiedPath.toString)\n    }\n    qualifiedPath\n  }\n\n  /**\n   * Generates AddFile from ConvertTargetFile for conversion.\n   *\n   * @param targetFile: the target file to convert.\n   * @param basePath: the table directory of the target file.\n   * @param fs: the file system to access the target file.\n   * @param conf: the SQL configures use to convert.\n   * @param partitionSchema: the partition schema of the target file if exists.\n   * @param useAbsolutePath: whether to use absolute path instead of relative path in the AddFile.\n   * @return an AddFile corresponding to the provided ConvertTargetFile.\n   */\n  def createAddFile(\n      targetFile: ConvertTargetFile,\n      basePath: Path,\n      fs: FileSystem,\n      conf: SQLConf,\n      partitionSchema: Option[StructType],\n      useAbsolutePath: Boolean = false): AddFile = {\n    val partitionFields = partitionSchema.map(_.fields.toSeq).getOrElse(Nil)\n    val partitionColNames = partitionSchema.map(_.fieldNames.toSeq).getOrElse(Nil)\n    val physicalPartitionColNames = partitionSchema.map(_.map { f =>\n      DeltaColumnMapping.getPhysicalName(f)\n    }).getOrElse(Nil)\n    val file = targetFile.fileStatus\n    val path = file.getHadoopPath\n    val partition = targetFile.partitionValues.getOrElse {\n      // partition values are not provided by the source table format, so infer from the file path\n      val pathStr = file.getHadoopPath.toUri.toString\n      val dateFormatter = DateFormatter()\n      val timestampFormatter =\n        TimestampFormatter(timestampPartitionPattern, java.util.TimeZone.getDefault)\n      val resolver = conf.resolver\n      val dir = if (file.isDir) file.getHadoopPath else file.getHadoopPath.getParent\n      val (partitionOpt, _) = PartitionUtils.parsePartition(\n        dir,\n        typeInference = false,\n        basePaths = Set(basePath),\n        userSpecifiedDataTypes = Map.empty,\n        validatePartitionColumns = false,\n        java.util.TimeZone.getDefault,\n        dateFormatter,\n        timestampFormatter)\n\n      partitionOpt.map { partValues =>\n        if (partitionColNames.size != partValues.columnNames.size) {\n          throw DeltaErrors.unexpectedNumPartitionColumnsFromFileNameException(\n            pathStr, partValues.columnNames, partitionColNames)\n        }\n\n        val tz = Option(conf.sessionLocalTimeZone)\n        // Check if the partition value can be casted to the provided type\n        if (!conf.getConf(DeltaSQLConf.DELTA_CONVERT_PARTITION_VALUES_IGNORE_CAST_FAILURE)) {\n          partValues.literals.zip(partitionFields).foreach { case (literal, field) =>\n            if (literal.eval() != null &&\n              Cast(literal, field.dataType, tz, ansiEnabled = false).eval() == null) {\n              val partitionValue = Cast(literal, StringType, tz, ansiEnabled = false).eval()\n              val partitionValueStr = Option(partitionValue).map(_.toString).orNull\n              throw DeltaErrors.castPartitionValueException(partitionValueStr, field.dataType)\n            }\n          }\n        }\n\n        val values = partValues\n          .literals\n          .map(PartitionUtils.literalToNormalizedString(_, tz))\n\n        partitionColNames.zip(partValues.columnNames).foreach { case (expected, parsed) =>\n          if (!resolver(expected, parsed)) {\n            throw DeltaErrors.unexpectedPartitionColumnFromFileNameException(\n              pathStr, parsed, expected)\n          }\n        }\n        physicalPartitionColNames.zip(values).toMap\n      }.getOrElse {\n        if (partitionColNames.nonEmpty) {\n          throw DeltaErrors.unexpectedNumPartitionColumnsFromFileNameException(\n            pathStr, Seq.empty, partitionColNames)\n        }\n        Map[String, String]()\n      }\n    }\n\n    val pathStrForAddFile = if (!useAbsolutePath) {\n      val relativePath = DeltaFileOperations.tryRelativizePath(fs, basePath, path)\n      assert(!relativePath.isAbsolute,\n        s\"Fail to relativize path $path against base path $basePath.\")\n      relativePath.toUri.toString\n    } else {\n      fs.makeQualified(path).toUri.toString\n    }\n\n\n    AddFile(\n      pathStrForAddFile,\n      partition,\n      file.length,\n      file.modificationTime,\n      dataChange = true,\n      stats = targetFile.stats.orNull\n    )\n  }\n\n  /**\n   * A helper function to check whether a directory should be skipped during conversion.\n   *\n   * @param dirName: the directory name to check.\n   * @return true if directory should be skipped for conversion, otherwise false.\n   */\n  def dirNameFilter(dirName: String): Boolean = {\n    // Allow partition column name starting with underscore and dot\n    DeltaFileOperations.defaultHiddenFileFilter(dirName) && !dirName.contains(\"=\")\n  }\n\n  /**\n   * Lists directories non-recursively in the distributed manner.\n   *\n   * @param spark: the spark session to use.\n   * @param rootDir: the root directory of all directories to list\n   * @param dirs: the list of directories to list.\n   * @param serializableConf: the hadoop configure to use.\n   * @return a dataset of files from the listing.\n   */\n  def listDirsInParallel(\n      spark: SparkSession,\n      rootDir: String,\n      dirs: Seq[String],\n      serializableConf: SerializableConfiguration): Dataset[SerializableFileStatus] = {\n\n    import org.apache.spark.sql.delta.implicits._\n\n    val conf = spark.sparkContext.broadcast(serializableConf)\n    val parallelism = spark.sessionState.conf.parallelPartitionDiscoveryParallelism\n\n    val rdd = spark.sparkContext.parallelize(dirs, math.min(parallelism, dirs.length))\n      .mapPartitions { batch =>\n        batch.flatMap { dir =>\n          DeltaFileOperations\n            .localListDirs(conf.value.value, Seq(dir), recursive = false)\n            .filter(!_.isDir)\n        }\n      }\n    spark.createDataset(rdd)\n  }\n\n  /**\n   * Merges the schemas of the ConvertTargetFiles.\n   *\n   * @param spark: the SparkSession used for schema merging.\n   * @param partitionSchema: the partition schema to be merged with the data schema.\n   * @param convertTargetFiles: the Dataset of ConvertTargetFiles to be merged.\n   * @return the merged StructType representing the combined schema of the Parquet files.\n   * @throws DeltaErrors.failedInferSchema If no schemas are found for merging.\n   */\n  def mergeSchemasInParallel(\n      spark: SparkSession,\n      partitionSchema: StructType,\n      convertTargetFiles: Dataset[ConvertTargetFile]): StructType = {\n    import org.apache.spark.sql.delta.implicits._\n    val partiallyMergedSchemas =\n      recordFrameProfile(\"Delta\", \"ConvertUtils.mergeSchemasInParallel\") {\n        convertTargetFiles.mapPartitions { iterator =>\n          var dataSchema: StructType = StructType(Seq())\n          iterator.foreach { file =>\n            try {\n              dataSchema = SchemaMergingUtils.mergeSchemas(dataSchema,\n                StructType.fromDDL(file.parquetSchemaDDL.get).asNullable)\n            } catch {\n              case cause: AnalysisException =>\n                throw DeltaErrors.failedMergeSchemaFile(\n                  file.fileStatus.path, StructType.fromDDL(file.parquetSchemaDDL.get).treeString,\n                  cause)\n            }\n          }\n          Iterator.single(dataSchema.toDDL)\n        }.collect().filter(_.nonEmpty)\n      }\n\n    if (partiallyMergedSchemas.isEmpty) {\n      throw DeltaErrors.failedInferSchema\n    }\n    var mergedSchema: StructType = StructType(Seq())\n    partiallyMergedSchemas.foreach { schema =>\n      mergedSchema = SchemaMergingUtils.mergeSchemas(mergedSchema, StructType.fromDDL(schema))\n    }\n    PartitioningUtils.mergeDataAndPartitionSchema(\n      mergedSchema,\n      StructType(partitionSchema.fields.toSeq),\n      spark.sessionState.conf.caseSensitiveAnalysis)._1\n  }\n}\n\n/**\n * Configuration for fetching Parquet schema.\n *\n * @param assumeBinaryIsString: whether unannotated BINARY fields should be assumed to be Spark\n *                              SQL [[StringType]] fields.\n * @param assumeInt96IsTimestamp: whether unannotated INT96 fields should be assumed to be Spark\n *                                SQL [[TimestampType]] fields.\n * @param ignoreCorruptFiles: a boolean indicating whether corrupt files should be ignored during\n *                            schema retrieval.\n */\ncase class ParquetSchemaFetchConfig(\n  assumeBinaryIsString: Boolean,\n  assumeInt96IsTimestamp: Boolean,\n  ignoreCorruptFiles: Boolean)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/convert/ParquetFileManifest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.{DeltaErrors, SerializableFileStatus}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, PartitionUtils}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.execution.datasources.parquet.{ParquetFileFormat, ParquetToSparkSchemaConverter}\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.SerializableConfiguration\n\n/** A file manifest generated through recursively listing a base path. */\nclass ManualListingFileManifest(\n    spark: SparkSession,\n    override val basePath: String,\n    partitionSchema: StructType,\n    parquetSchemaFetchConfig: ParquetSchemaFetchConfig,\n    serializableConf: SerializableConfiguration)\n  extends ConvertTargetFileManifest with DeltaLogging {\n\n  protected def doList(): Dataset[SerializableFileStatus] = {\n    val conf = spark.sparkContext.broadcast(serializableConf)\n    DeltaFileOperations\n      .recursiveListDirs(spark, Seq(basePath), conf, ConvertUtils.dirNameFilter)\n      .where(\"!isDir\")\n  }\n\n  override lazy val allFiles: Dataset[ConvertTargetFile] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    val conf = spark.sparkContext.broadcast(serializableConf)\n    val fetchConfig = parquetSchemaFetchConfig\n    val files = doList().mapPartitions { iter =>\n      val fileStatuses = iter.toSeq\n      val pathToStatusMapping = fileStatuses.map { fileStatus =>\n        fileStatus.path -> fileStatus\n      }.toMap\n      val footerSeq = DeltaFileOperations.readParquetFootersInParallel(\n        conf.value.value, fileStatuses.map(_.toFileStatus), fetchConfig.ignoreCorruptFiles)\n      val schemaConverter = new ParquetToSparkSchemaConverter(\n        assumeBinaryIsString = fetchConfig.assumeBinaryIsString,\n        assumeInt96IsTimestamp = fetchConfig.assumeInt96IsTimestamp\n      )\n      footerSeq.map { footer =>\n        val fileStatus = pathToStatusMapping(footer.getFile.toString)\n        val schema = ParquetFileFormat.readSchemaFromFooter(footer, schemaConverter)\n        ConvertTargetFile(fileStatus, None, Some(schema.toDDL))\n      }.toIterator\n    }\n    files.cache()\n    files\n  }\n\n  override lazy val parquetSchema: Option[StructType] = {\n    recordDeltaOperationForTablePath(basePath, \"delta.convert.schemaInference\") {\n      Some(ConvertUtils.mergeSchemasInParallel(spark, partitionSchema, allFiles))\n    }\n  }\n\n  override def close(): Unit = allFiles.unpersist()\n}\n\n/** A file manifest generated through listing partition paths from Metastore catalog. */\nclass CatalogFileManifest(\n    spark: SparkSession,\n    override val basePath: String,\n    catalogTable: CatalogTable,\n    partitionSchema: StructType,\n    parquetSchemaFetchConfig: ParquetSchemaFetchConfig,\n    serializableConf: SerializableConfiguration)\n  extends ConvertTargetFileManifest with DeltaLogging {\n\n  private val useCatalogSchema =\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_USE_CATALOG_SCHEMA)\n\n  // List of partition directories and corresponding partition values.\n  private lazy val partitionList = {\n    if (catalogTable.partitionSchema.isEmpty) {\n      // Not a partitioned table.\n      Seq(basePath -> Map.empty[String, String])\n    } else {\n      val partitions = spark.sessionState.catalog.listPartitions(catalogTable.identifier)\n      partitions.map { partition =>\n        // Convert URI into Path first to decode special characters.\n        val partitionDir = partition.storage.locationUri.map(new Path(_).toString())\n          .getOrElse {\n            val partitionDir =\n              PartitionUtils.getPathFragment(partition.spec, catalogTable.partitionSchema)\n            basePath.stripSuffix(\"/\") + \"/\" + partitionDir\n          }\n        partitionDir -> partition.spec\n      }\n    }\n  }\n\n  protected def doList(): Dataset[SerializableFileStatus] = {\n    if (partitionList.isEmpty) {\n      throw DeltaErrors.convertToDeltaNoPartitionFound(catalogTable.identifier.unquotedString)\n    }\n\n    ConvertUtils.listDirsInParallel(spark, basePath, partitionList.map(_._1), serializableConf)\n  }\n\n  override lazy val allFiles: Dataset[ConvertTargetFile] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    // Avoid the serialization of this CatalogFileManifest during distributed execution.\n    val conf = spark.sparkContext.broadcast(serializableConf)\n    val useParquetSchema = !useCatalogSchema\n    val dirToPartitionSpec = partitionList.toMap\n    val fetchConfig = parquetSchemaFetchConfig\n\n    val files = doList().mapPartitions { iter =>\n      val fileStatuses = iter.toSeq\n      if (useParquetSchema) {\n        val pathToFile = fileStatuses.map { fileStatus => fileStatus.path -> fileStatus }.toMap\n        val footerSeq = DeltaFileOperations.readParquetFootersInParallel(\n          conf.value.value,\n          fileStatuses.map(_.toFileStatus),\n          fetchConfig.ignoreCorruptFiles)\n        val schemaConverter = new ParquetToSparkSchemaConverter(\n          assumeBinaryIsString = fetchConfig.assumeBinaryIsString,\n          assumeInt96IsTimestamp = fetchConfig.assumeInt96IsTimestamp\n        )\n        footerSeq.map { footer =>\n          val schema = ParquetFileFormat.readSchemaFromFooter(footer, schemaConverter)\n          val fileStatus = pathToFile(footer.getFile.toString)\n          ConvertTargetFile(\n            fileStatus,\n            dirToPartitionSpec.get(footer.getFile.getParent.toString),\n            Some(schema.toDDL))\n        }.toIterator\n      } else {\n        // TODO: Currently \"spark.sql.files.ignoreCorruptFiles\" is not respected for\n        //  CatalogFileManifest when catalog schema is used to avoid performance regression.\n        fileStatuses.map { fileStatus =>\n            ConvertTargetFile(\n              fileStatus,\n              dirToPartitionSpec.get(fileStatus.getHadoopPath.getParent.toString),\n              None)\n        }.toIterator\n      }\n    }\n    files.cache()\n    files\n  }\n\n  override lazy val parquetSchema: Option[StructType] = {\n    if (useCatalogSchema) {\n      Some(catalogTable.schema)\n    } else {\n      recordDeltaOperationForTablePath(basePath, \"delta.convert.schemaInference\") {\n        Some(ConvertUtils.mergeSchemasInParallel(spark, partitionSchema, allFiles))\n      }\n    }\n  }\n\n  override def close(): Unit = allFiles.unpersist()\n}\n\n/** A file manifest generated from pre-existing parquet MetadataLog. */\nclass MetadataLogFileManifest(\n    spark: SparkSession,\n    override val basePath: String,\n    partitionSchema: StructType,\n    parquetSchemaFetchConfig: ParquetSchemaFetchConfig,\n    serializableConf: SerializableConfiguration)\n  extends ConvertTargetFileManifest with DeltaLogging {\n\n  val index = createMetadataLogFileIndex(spark, new Path(basePath), Map.empty, None)\n\n  protected def doList(): Dataset[SerializableFileStatus] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    val rdd = spark.sparkContext.parallelize(index.allFiles()).mapPartitions { _\n        .map(SerializableFileStatus.fromStatus)\n    }\n    spark.createDataset(rdd)\n  }\n\n  override lazy val allFiles: Dataset[ConvertTargetFile] = {\n    import org.apache.spark.sql.delta.implicits._\n\n    val conf = spark.sparkContext.broadcast(serializableConf)\n    val fetchConfig = parquetSchemaFetchConfig\n\n    val files = doList().mapPartitions { iter =>\n      val fileStatuses = iter.toSeq\n      val pathToStatusMapping = fileStatuses.map { fileStatus =>\n        fileStatus.path -> fileStatus\n      }.toMap\n      val footerSeq = DeltaFileOperations.readParquetFootersInParallel(\n        conf.value.value, fileStatuses.map(_.toFileStatus), fetchConfig.ignoreCorruptFiles)\n      val schemaConverter = new ParquetToSparkSchemaConverter(\n        assumeBinaryIsString = fetchConfig.assumeBinaryIsString,\n        assumeInt96IsTimestamp = fetchConfig.assumeInt96IsTimestamp\n      )\n      footerSeq.map { footer =>\n        val fileStatus = pathToStatusMapping(footer.getFile.toString)\n        val schema = ParquetFileFormat.readSchemaFromFooter(footer, schemaConverter)\n        ConvertTargetFile(fileStatus, None, Some(schema.toDDL))\n      }.toIterator\n    }\n    files.cache()\n    files\n  }\n\n  override lazy val parquetSchema: Option[StructType] = {\n    recordDeltaOperationForTablePath(basePath, \"delta.convert.schemaInference\") {\n      Some(ConvertUtils.mergeSchemasInParallel(spark, partitionSchema, allFiles))\n    }\n  }\n\n  override def close(): Unit = allFiles.unpersist()\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/convert/ParquetTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.{DeltaErrors, SerializableFileStatus}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.execution.datasources.PartitioningUtils\nimport org.apache.spark.sql.execution.datasources.parquet.{ParquetFileFormat, ParquetToSparkSchemaConverter}\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.SerializableConfiguration\n\n/**\n * A target Parquet table for conversion to a Delta table.\n *\n * @param spark: spark session to use.\n * @param basePath: the root directory of the Parquet table.\n * @param catalogTable: optional catalog table (if exists) of the Parquet table.\n * @param userPartitionSchema: user provided partition schema of the Parquet table.\n */\nclass ParquetTable(\n    val spark: SparkSession,\n    val basePath: String,\n    val catalogTable: Option[CatalogTable],\n    val userPartitionSchema: Option[StructType]) extends ConvertTargetTable with DeltaLogging {\n\n  // Validate user provided partition schema if catalogTable is available.\n  if (catalogTable.isDefined && userPartitionSchema.isDefined\n    && !catalogTable.get.partitionSchema.equals(userPartitionSchema.get)) {\n    throw DeltaErrors.unexpectedPartitionSchemaFromUserException(\n      catalogTable.get.partitionSchema, userPartitionSchema.get)\n  }\n\n  protected lazy val serializableConf: SerializableConfiguration = {\n    // scalastyle:off deltahadoopconfiguration\n    new SerializableConfiguration(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  override val partitionSchema: StructType = {\n    userPartitionSchema.orElse(catalogTable.map(_.partitionSchema)).getOrElse(new StructType())\n  }\n\n  override lazy val numFiles: Long = fileManifest.numFiles\n\n  override lazy val sizeInBytes: Long = fileManifest.sizeInBytes\n\n  def tableSchema: StructType = fileManifest.parquetSchema.get\n\n  override val format: String = \"parquet\"\n\n  val fileManifest: ConvertTargetFileManifest = {\n    val fetchConfig = ParquetSchemaFetchConfig(\n      spark.sessionState.conf.isParquetBinaryAsString,\n      spark.sessionState.conf.isParquetINT96AsTimestamp,\n      spark.sessionState.conf.ignoreCorruptFiles\n    )\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_USE_METADATA_LOG) &&\n      FileStreamSink.hasMetadata(Seq(basePath), serializableConf.value, spark.sessionState.conf)) {\n      new MetadataLogFileManifest(spark, basePath, partitionSchema, fetchConfig, serializableConf)\n    } else if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CONVERT_USE_CATALOG_PARTITIONS) &&\n      catalogTable.isDefined) {\n      new CatalogFileManifest(\n        spark, basePath, catalogTable.get, partitionSchema, fetchConfig, serializableConf)\n    } else {\n      new ManualListingFileManifest(\n        spark,\n        basePath,\n        partitionSchema,\n        fetchConfig,\n        serializableConf)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/convert/interfaces.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.convert\n\nimport java.io.Closeable\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaColumnMappingMode, DeltaLog, NoMapping, SerializableFileStatus}\n\nimport org.apache.spark.sql.Dataset\nimport org.apache.spark.sql.functions.sum\nimport org.apache.spark.sql.types.StructType\n\n/**\n * An interface for the table to be converted to Delta.\n */\ntrait ConvertTargetTable {\n  /** The table schema of the target table */\n  def tableSchema: StructType\n\n  /** The table properties of the target table */\n  def properties: Map[String, String] = Map.empty\n\n  /** The partition schema of the target table */\n  def partitionSchema: StructType\n\n  /** The file manifest of the target table */\n  def fileManifest: ConvertTargetFileManifest\n\n  /** The number of files from the target table */\n  def numFiles: Long\n\n  /** The number of bytes from the target table */\n  def sizeInBytes: Long\n\n  /** Whether this table requires column mapping to be converted */\n  def requiredColumnMappingMode: DeltaColumnMappingMode = NoMapping\n\n  /* The format of the table */\n  def format: String\n\n}\n\n/** An interface for providing an iterator of files for a table. */\ntrait ConvertTargetFileManifest extends Closeable {\n  /** The base path of a table. Should be a qualified, normalized path. */\n  val basePath: String\n\n  /** Return all files as a Dataset for parallelized processing. */\n  def allFiles: Dataset[ConvertTargetFile]\n\n  /** Return the active files for a table in sequence */\n  def getFiles: Iterator[ConvertTargetFile] = allFiles.toLocalIterator().asScala\n\n  /** Return the number of files for the table */\n  def numFiles: Long = allFiles.count()\n\n  /** Return the number of bytes for the table */\n  def sizeInBytes: Long = {\n    allFiles.select(\"fileStatus.*\").select(sum(\"length\")).collect().head.getLong(0)\n  }\n\n  /** Return the parquet schema for the table.\n   *  Defined only when the schema cannot be inferred from CatalogTable.\n   */\n  def parquetSchema: Option[StructType] = None\n}\n\n/**\n * An interface for the file to be included during conversion.\n *\n * @param fileStatus the file info\n * @param partitionValues partition values of this file that may be available from the source\n *                        table format. If none, the converter will infer partition values from the\n *                        file path, assuming the Hive directory format.\n * @param parquetSchemaDDL the Parquet schema DDL associated with the file.\n * @param stats           Stats information extracted from the source file.\n */\ncase class ConvertTargetFile(\n  fileStatus: SerializableFileStatus,\n  partitionValues: Option[Map[String, String]] = None,\n  parquetSchemaDDL: Option[String] = None,\n  stats: Option[String] = None\n) extends Serializable\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/merge/ClassicMergeExecutor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.merge\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, FileAction}\nimport org.apache.spark.sql.delta.commands.{DeletionVectorBitmapGenerator, DMLWithDeletionVectorsHelper, MergeIntoCommandBase}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader.{CDC_TYPE_COLUMN_NAME, CDC_TYPE_NOT_CDC}\nimport org.apache.spark.sql.delta.commands.merge.MergeOutputGeneration.{SOURCE_ROW_INDEX_COL, TARGET_ROW_INDEX_COL}\nimport org.apache.spark.sql.delta.files.TahoeBatchFileIndex\nimport org.apache.spark.sql.delta.stats.StatsCollectionUtils\nimport org.apache.spark.sql.delta.util.SetAccumulator\n\nimport org.apache.spark.sql.{Column, Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.{And, Expression, Literal, Or}\nimport org.apache.spark.sql.catalyst.plans.logical.DeltaMergeIntoClause\nimport org.apache.spark.sql.functions.{coalesce, col, count, input_file_name, lit, monotonically_increasing_id, sum}\n\n/**\n * Trait with merge execution in two phases:\n *\n * Phase 1: Find the input files in target that are touched by the rows that satisfy\n *    the condition and verify that no two source rows match with the same target row.\n *    This is implemented as an inner-join using the given condition (see [[findTouchedFiles]]).\n *    In the special case that there is no update clause we write all the non-matching\n *    source data as new files and skip phase 2.\n *    Issues an error message when the ON search_condition of the MERGE statement can match\n *    a single row from the target table with multiple rows of the source table-reference.\n *\n * Phase 2: Read the touched files again and write new files with updated and/or inserted rows.\n *    If there are updates, then use an outer join using the given condition to write the\n *    updates and inserts (see [[writeAllChanges()]]). If there are no matches for updates,\n *    only inserts, then write them directly (see [[writeInsertsOnlyWhenNoMatches()]]).\n *\n *    Note, when deletion vectors are enabled, phase 2 is split into two parts:\n *    2.a. Read the touched files again and only write modified and new\n *         rows (see [[writeAllChanges()]]).\n *    2.b. Read the touched files and generate deletion vectors for the modified\n *         rows (see [[writeDVs()]]).\n *\n * If there are no matches for updates, only inserts, then write them directly\n * (see [[writeInsertsOnlyWhenNoMatches()]]). This remains the same when DVs are enabled since there\n * are no modified rows. Furthermore, eee [[InsertOnlyMergeExecutor]] for the optimized executor\n * used in case there are only inserts.\n */\ntrait ClassicMergeExecutor extends MergeOutputGeneration {\n  self: MergeIntoCommandBase =>\n  import MergeIntoCommandBase._\n\n  /**\n   * Find the target table files that contain the rows that satisfy the merge condition. This is\n   * implemented as an inner-join between the source query/table and the target table using\n   * the merge condition.\n   */\n  protected def findTouchedFiles(\n      spark: SparkSession,\n      deltaTxn: OptimisticTransaction\n    ): (Seq[AddFile], DeduplicateCDFDeletes) = recordMergeOperation(\n      extraOpType = \"findTouchedFiles\",\n      status = \"MERGE operation - scanning files for matches\",\n      sqlMetricName = \"scanTimeMs\") {\n\n    val columnComparator = spark.sessionState.analyzer.resolver\n\n    // Accumulator to collect all the distinct touched files\n    val touchedFilesAccum = new SetAccumulator[String]()\n    spark.sparkContext.register(touchedFilesAccum, TOUCHED_FILES_ACCUM_NAME)\n\n    // Prune non-matching files if we don't need to collect them for NOT MATCHED BY SOURCE clauses.\n    val dataSkippedFiles =\n      if (notMatchedBySourceClauses.isEmpty) {\n        deltaTxn.filterFiles(getTargetOnlyPredicates(spark), keepNumRecords = true)\n      } else {\n        deltaTxn.filterFiles(filters = Seq(Literal.TrueLiteral), keepNumRecords = true)\n      }\n\n    // Join the source and target table using the merge condition to find touched files. An inner\n    // join collects all candidate files for MATCHED clauses, a right outer join also includes\n    // candidates for NOT MATCHED BY SOURCE clauses.\n    // In addition, we attach two columns\n    // - a monotonically increasing row id for target rows to later identify whether the same\n    //     target row is modified by multiple user or not\n    // - the target file name the row is from to later identify the files touched by matched rows\n    val joinType = if (notMatchedBySourceClauses.isEmpty) \"inner\" else \"right_outer\"\n\n    // When they are only MATCHED clauses, after the join we prune files that have no rows that\n    // satisfy any of the clause conditions.\n    val matchedPredicate =\n      if (isMatchedOnly) {\n        matchedClauses\n          // An undefined condition (None) is implicitly true\n          .map(_.condition.getOrElse(Literal.TrueLiteral))\n          .reduce((a, b) => Or(a, b))\n      } else Literal.TrueLiteral\n\n    // Compute the columns needed for the inner join.\n    val targetColsNeeded = {\n      condition.references.map(_.name) ++ deltaTxn.snapshot.metadata.partitionColumns ++\n        matchedPredicate.references.map(_.name)\n    }\n\n    val columnsToDrop = deltaTxn.snapshot.metadata.schema.map(_.name)\n      .filterNot { field =>\n        targetColsNeeded.exists { name => columnComparator(name, field) }\n      }\n    val incrSourceRowCountExpr = incrementMetricAndReturnBool(\"numSourceRows\", valueToReturn = true)\n    // We can't use filter() directly on the expression because that will prevent\n    // column pruning. We don't need the SOURCE_ROW_PRESENT_COL so we immediately drop it.\n    val sourceDF = getMergeSource.df\n      .withColumn(SOURCE_ROW_PRESENT_COL, Column(incrSourceRowCountExpr))\n      .filter(SOURCE_ROW_PRESENT_COL)\n      .drop(SOURCE_ROW_PRESENT_COL)\n    val targetPlan =\n      buildTargetPlanWithFiles(\n        spark,\n        deltaTxn,\n        dataSkippedFiles,\n        columnsToDrop)\n    val targetDF = DataFrameUtils.ofRows(spark, targetPlan)\n      .withColumn(ROW_ID_COL, monotonically_increasing_id())\n      .withColumn(FILE_NAME_COL, input_file_name())\n\n    val joinToFindTouchedFiles =\n      sourceDF.join(targetDF, Column(condition), joinType)\n\n    // UDFs to records touched files names and add them to the accumulator\n    val recordTouchedFileName =\n      DeltaUDF.intFromStringBoolean { (fileName, shouldRecord) =>\n        if (shouldRecord) {\n          touchedFilesAccum.add(fileName)\n        }\n        1\n      }.asNondeterministic()\n\n    // Process the matches from the inner join to record touched files and find multiple matches\n    val collectTouchedFiles = joinToFindTouchedFiles\n      .select(col(ROW_ID_COL),\n        recordTouchedFileName(col(FILE_NAME_COL), Column(matchedPredicate)).as(\"one\"))\n\n    // Calculate frequency of matches per source row\n    val matchedRowCounts = collectTouchedFiles.groupBy(ROW_ID_COL).agg(sum(\"one\").as(\"count\"))\n\n    // Get multiple matches and simultaneously collect (using touchedFilesAccum) the file names\n    import org.apache.spark.sql.delta.implicits._\n    val (multipleMatchCount, multipleMatchSum) = matchedRowCounts\n      .filter(\"count > 1\")\n      .select(coalesce(count(Column(\"*\")), lit(0)), coalesce(sum(\"count\"), lit(0)))\n      .as[(Long, Long)]\n      .collect()\n      .head\n\n    checkSourcePlanIsNotCached(spark, getMergeSource.df.queryExecution.logical)\n\n    val hasMultipleMatches = multipleMatchCount > 0\n    throwErrorOnMultipleMatches(hasMultipleMatches, spark)\n    if (hasMultipleMatches) {\n      // This is only allowed for delete-only queries.\n      // This query will count the duplicates for numTargetRowsDeleted in Job 2,\n      // because we count matches after the join and not just the target rows.\n      // We have to compensate for this by subtracting the duplicates later,\n      // so we need to record them here.\n      val duplicateCount = multipleMatchSum - multipleMatchCount\n      multipleMatchDeleteOnlyOvercount = Some(duplicateCount)\n    }\n\n    // Get the AddFiles using the touched file names.\n    val touchedFileNames = touchedFilesAccum.value.iterator().asScala.toSeq\n    logTrace(s\"findTouchedFiles: matched files:\\n\\t${touchedFileNames.mkString(\"\\n\\t\")}\")\n\n    val nameToAddFileMap = generateCandidateFileMap(targetDeltaLog.dataPath, dataSkippedFiles)\n    val touchedAddFiles = touchedFileNames.map(\n      getTouchedFile(targetDeltaLog.dataPath, _, nameToAddFileMap))\n\n    if (metrics(\"numSourceRows\").value == 0 && (dataSkippedFiles.isEmpty ||\n      dataSkippedFiles.forall(_.numLogicalRecords.getOrElse(0) == 0))) {\n      // The target table is empty, and the optimizer optimized away the join entirely OR the\n      // source table is truly empty. In that case, scanning the source table once is the only\n      // way to get the correct metric.\n      val numSourceRows = sourceDF.count()\n      metrics(\"numSourceRows\").set(numSourceRows)\n    }\n\n    metrics(\"numTargetFilesBeforeSkipping\") += deltaTxn.snapshot.numOfFiles\n    metrics(\"numTargetBytesBeforeSkipping\") += deltaTxn.snapshot.sizeInBytes\n    val (afterSkippingBytes, afterSkippingPartitions) =\n      totalBytesAndDistinctPartitionValues(dataSkippedFiles)\n    metrics(\"numTargetFilesAfterSkipping\") += dataSkippedFiles.size\n    metrics(\"numTargetBytesAfterSkipping\") += afterSkippingBytes\n    metrics(\"numTargetPartitionsAfterSkipping\") += afterSkippingPartitions\n    val (removedBytes, removedPartitions) = totalBytesAndDistinctPartitionValues(touchedAddFiles)\n    metrics(\"numTargetFilesRemoved\") += touchedAddFiles.size\n    metrics(\"numTargetBytesRemoved\") += removedBytes\n    metrics(\"numTargetPartitionsRemovedFrom\") += removedPartitions\n    val dedupe = DeduplicateCDFDeletes(\n      hasMultipleMatches && isCdcEnabled(deltaTxn),\n      includesInserts)\n    (touchedAddFiles, dedupe)\n  }\n\n  /**\n   * Helper function that produces an expression by combining a sequence of clauses with OR.\n   * Requires the sequence to be non-empty.\n   */\n  protected def clauseDisjunction(clauses: Seq[DeltaMergeIntoClause]): Expression = {\n    require(clauses.nonEmpty)\n    clauses\n      .map(_.condition.getOrElse(Literal.TrueLiteral))\n      .reduceLeft(Or)\n  }\n\n  /**\n   * Returns the expression that can be used for selecting the modified rows generated\n   * by the merge operation. The expression is to designed to work irrespectively\n   * of the join type used between the source and target tables.\n   *\n   * The expression consists of two parts, one for each of the action clause types that produce\n   * row modifications: MATCHED, NOT MATCHED BY SOURCE. All actions of the same clause type form\n   * a disjunctive clause. The result is then conjucted to an expression that filters the rows\n   * of the particular action clause type. For example:\n   *\n   * MERGE INTO t\n   * USING s\n   * ON s.id = t.id\n   * WHEN MATCHED AND id < 5 THEN ...\n   * WHEN MATCHED AND id > 10 THEN ...\n   * WHEN NOT MATCHED BY SOURCE AND id > 20 THEN ...\n   *\n   * Produces the following expression:\n   *\n   * ((as.id = t.id) AND (id < 5 OR id > 10))\n   * OR\n   * ((SOURCE TABLE IS NULL) AND (id > 20))\n   */\n  protected def generateFilterForModifiedRows(): Expression = {\n    val matchedExpression = if (matchedClauses.nonEmpty) {\n      And(condition, clauseDisjunction(matchedClauses))\n    } else {\n      Literal.FalseLiteral\n    }\n\n    val notMatchedBySourceExpression = if (notMatchedBySourceClauses.nonEmpty) {\n      val combinedClauses = clauseDisjunction(notMatchedBySourceClauses)\n      And(col(SOURCE_ROW_PRESENT_COL).isNull.expr, combinedClauses)\n    } else {\n      Literal.FalseLiteral\n    }\n\n    Or(matchedExpression, notMatchedBySourceExpression)\n  }\n\n  /**\n   * Returns the expression that can be used for selecting the new rows generated\n   * by the merge operation.\n   */\n  protected def generateFilterForNewRows(): Expression = {\n    if (notMatchedClauses.nonEmpty) {\n      val combinedClauses = clauseDisjunction(notMatchedClauses)\n      And(col(TARGET_ROW_PRESENT_COL).isNull.expr, combinedClauses)\n    } else {\n      Literal.FalseLiteral\n    }\n  }\n\n  /**\n   * Write new files by reading the touched files and updating/inserting data using the source\n   * query/table. This is implemented using a full-outer-join using the merge condition.\n   *\n   * Note that unlike the insert-only code paths with just one control column ROW_DROPPED_COL, this\n   * method has a second control column CDC_TYPE_COL_NAME used for handling CDC when enabled.\n   */\n  protected def writeAllChanges(\n      spark: SparkSession,\n      deltaTxn: OptimisticTransaction,\n      filesToRewrite: Seq[AddFile],\n      deduplicateCDFDeletes: DeduplicateCDFDeletes,\n      writeUnmodifiedRows: Boolean): Seq[FileAction] = recordMergeOperation(\n        extraOpType = if (!writeUnmodifiedRows) {\n            \"writeModifiedRowsOnly\"\n          } else if (shouldOptimizeMatchedOnlyMerge(spark)) {\n            \"writeAllUpdatesAndDeletes\"\n          } else {\n            \"writeAllChanges\"\n          },\n        status = s\"MERGE operation - Rewriting ${filesToRewrite.size} files\",\n        sqlMetricName = \"rewriteTimeMs\") {\n\n      val cdcEnabled = isCdcEnabled(deltaTxn)\n\n      require(\n        !deduplicateCDFDeletes.enabled || cdcEnabled,\n        \"CDF delete duplication is enabled but overall the CDF generation is disabled\")\n\n    // Generate a new target dataframe that has same output attributes exprIds as the target plan.\n    // This allows us to apply the existing resolved update/insert expressions.\n    val targetPlan = buildTargetPlanWithFiles(\n      spark,\n      deltaTxn,\n      filesToRewrite,\n      columnsToDrop = Nil)\n    val baseTargetDF = RowTracking.preserveRowTrackingColumns(\n      dfWithoutRowTrackingColumns = DataFrameUtils.ofRows(spark, targetPlan),\n      snapshot = deltaTxn.snapshot)\n\n    val joinType = if (writeUnmodifiedRows) {\n      if (shouldOptimizeMatchedOnlyMerge(spark)) {\n        \"rightOuter\"\n      } else {\n        \"fullOuter\"\n      }\n    } else {\n      // Since we do not need to write unmodified rows, we can perform stricter joins.\n      if (isMatchedOnly) {\n        \"inner\"\n      } else if (notMatchedBySourceClauses.isEmpty) {\n        \"leftOuter\"\n      } else if (notMatchedClauses.isEmpty) {\n        \"rightOuter\"\n      } else {\n        \"fullOuter\"\n      }\n    }\n\n    if (joinType == \"fullOuter\" || joinType == \"leftOuter\") {\n      secondSourceScanWasFullScan = true\n    }\n\n    logDebug(s\"\"\"writeAllChanges using $joinType join:\n       |  source.output: ${source.outputSet}\n       |  target.output: ${target.outputSet}\n       |  condition: $condition\n       |  newTarget.output: ${baseTargetDF.queryExecution.logical.outputSet}\n       \"\"\".stripMargin)\n\n    // Expressions to update metrics\n    val incrSourceRowCountExpr = incrementMetricAndReturnBool(\n      \"numSourceRowsInSecondScan\", valueToReturn = true)\n    val incrNoopCountExpr = incrementMetricAndReturnBool(\n      \"numTargetRowsCopied\", valueToReturn = false)\n\n    // Apply an outer join to find both, matches and non-matches. We are adding two boolean fields\n    // with value `true`, one to each side of the join. Whether this field is null or not after\n    // the outer join, will allow us to identify whether the joined row was a\n    // matched inner result or an unmatched result with null on one side.\n    val joinedBaseDF = {\n      var sourceDF = getMergeSource.df\n      if (deduplicateCDFDeletes.enabled && deduplicateCDFDeletes.includesInserts) {\n        // Add row index for the source rows to identify inserted rows during the cdf deleted rows\n        // deduplication. See [[deduplicateCDFDeletes()]]\n        sourceDF = sourceDF.withColumn(SOURCE_ROW_INDEX_COL, monotonically_increasing_id())\n      }\n      val left = sourceDF\n          .withColumn(SOURCE_ROW_PRESENT_COL, Column(incrSourceRowCountExpr))\n          // In some cases, the optimizer (incorrectly) decides to omit the metrics column.\n          // This causes issues in the source determinism validation. We work around the issue by\n          // adding a redundant dummy filter to make sure the column is not pruned.\n          .filter(SOURCE_ROW_PRESENT_COL)\n\n      val targetDF = baseTargetDF\n        .withColumn(TARGET_ROW_PRESENT_COL, lit(true))\n      val right = if (deduplicateCDFDeletes.enabled) {\n        targetDF.withColumn(TARGET_ROW_INDEX_COL, monotonically_increasing_id())\n      } else {\n        targetDF\n      }\n      left.join(right, Column(condition), joinType)\n    }\n\n    val joinedDF =\n      if (writeUnmodifiedRows) {\n        joinedBaseDF\n      } else {\n        val filter = Or(generateFilterForModifiedRows(), generateFilterForNewRows())\n        joinedBaseDF.filter(Column(filter))\n      }\n\n    // Precompute conditions in matched and not matched clauses and generate\n    // the joinedDF with precomputed columns and clauses with rewritten conditions.\n    val (joinedAndPrecomputedConditionsDF, clausesWithPrecompConditions) =\n        generatePrecomputedConditionsAndDF(\n          joinedDF,\n          clauses = matchedClauses ++ notMatchedClauses ++ notMatchedBySourceClauses)\n\n    // In case Row IDs are preserved, get the attribute expression of the Row ID column.\n    val rowIdColumnExpressionOpt =\n      MaterializedRowId.getAttribute(deltaTxn.snapshot, joinedAndPrecomputedConditionsDF)\n\n    val rowCommitVersionColumnExpressionOpt =\n      MaterializedRowCommitVersion.getAttribute(deltaTxn.snapshot, joinedAndPrecomputedConditionsDF)\n\n    // The target output columns need to be marked as nullable here, as they are going to be used\n    // to reference the output of an outer join.\n    val targetWriteCols = postEvolutionTargetExpressions(makeNullable = true)\n\n    // If there are N columns in the target table, the full outer join output will have:\n    // - N columns for target table\n    // - Two optional Row ID / Row commit version preservation columns with their physical name.\n    // - ROW_DROPPED_COL to define whether the generated row should be dropped or written\n    // - if CDC is enabled, also CDC_TYPE_COLUMN_NAME with the type of change being performed\n    //   in a particular row\n    // (N+1 or N+2 columns depending on CDC disabled / enabled)\n    val outputColNames =\n      targetWriteCols.map(_.name) ++\n        rowIdColumnExpressionOpt.map(_.name) ++\n        rowCommitVersionColumnExpressionOpt.map(_.name) ++\n        Seq(ROW_DROPPED_COL) ++\n        (if (cdcEnabled) Some(CDC_TYPE_COLUMN_NAME) else None)\n\n    // Copy expressions to copy the existing target row and not drop it (ROW_DROPPED_COL=false),\n    // and in case CDC is enabled, set it to CDC_TYPE_NOT_CDC.\n    // (N+1 or N+2 or N+3 columns depending on CDC disabled / enabled and if Row IDs are preserved)\n    val noopCopyExprs =\n      targetWriteCols ++\n        rowIdColumnExpressionOpt ++\n        rowCommitVersionColumnExpressionOpt ++\n        Seq(incrNoopCountExpr) ++\n        (if (cdcEnabled) Seq(CDC_TYPE_NOT_CDC) else Seq())\n\n    // Generate output columns.\n    val needSetRowTrackingFieldIdForUniform = IcebergCompat.isGeqEnabled(deltaTxn.metadata, 3)\n    val outputCols = generateWriteAllChangesOutputCols(\n      targetWriteCols,\n      rowIdColumnExpressionOpt,\n      rowCommitVersionColumnExpressionOpt,\n      outputColNames,\n      noopCopyExprs,\n      writeUnmodifiedRows,\n      clausesWithPrecompConditions,\n      cdcEnabled,\n      needSetRowTrackingFieldIdForUniform\n    )\n\n    val preOutputDF = if (cdcEnabled) {\n      generateCdcAndOutputRows(\n          joinedAndPrecomputedConditionsDF,\n          outputCols,\n          outputColNames,\n          noopCopyExprs,\n          rowIdColumnExpressionOpt.map(_.name),\n          rowCommitVersionColumnExpressionOpt.map(_.name),\n          deduplicateCDFDeletes,\n          needSetRowTrackingFieldIdForUniform = needSetRowTrackingFieldIdForUniform\n      )\n    } else {\n      // change data capture is off, just output the normal data\n      joinedAndPrecomputedConditionsDF\n        .select(outputCols: _*)\n    }\n    // The filter ensures we only consider rows that are not dropped.\n    // The drop ensures that the dropped flag does not leak out to the output.\n    val outputDF = preOutputDF\n      .filter(s\"$ROW_DROPPED_COL = false\")\n      .drop(ROW_DROPPED_COL)\n\n    logDebug(\"writeAllChanges: join output plan:\\n\" + outputDF.queryExecution)\n\n    // Write to Delta\n    val newFiles = writeFiles(spark, deltaTxn, outputDF)\n\n    checkSourcePlanIsNotCached(spark, getMergeSource.df.queryExecution.logical)\n\n    // Update metrics\n    val (addedBytes, addedPartitions) = totalBytesAndDistinctPartitionValues(newFiles)\n    metrics(\"numTargetFilesAdded\") += newFiles.count(_.isInstanceOf[AddFile])\n    metrics(\"numTargetChangeFilesAdded\") += newFiles.count(_.isInstanceOf[AddCDCFile])\n    metrics(\"numTargetChangeFileBytes\") += newFiles.collect{ case f: AddCDCFile => f.size }.sum\n    metrics(\"numTargetBytesAdded\") += addedBytes\n    metrics(\"numTargetPartitionsAddedTo\") += addedPartitions\n    if (multipleMatchDeleteOnlyOvercount.isDefined) {\n      // Compensate for counting duplicates during the query.\n      val actualRowsDeleted =\n        metrics(\"numTargetRowsDeleted\").value - multipleMatchDeleteOnlyOvercount.get\n      assert(actualRowsDeleted >= 0)\n      metrics(\"numTargetRowsDeleted\").set(actualRowsDeleted)\n      val actualRowsMatchedDeleted =\n        metrics(\"numTargetRowsMatchedDeleted\").value - multipleMatchDeleteOnlyOvercount.get\n      assert(actualRowsMatchedDeleted >= 0)\n      metrics(\"numTargetRowsMatchedDeleted\").set(actualRowsMatchedDeleted)\n    }\n\n    newFiles\n  }\n\n  /**\n   * Writes Deletion Vectors for rows modified by the merge operation.\n   */\n  protected def writeDVs(\n    spark: SparkSession,\n    deltaTxn: OptimisticTransaction,\n    filesToRewrite: Seq[AddFile]): Seq[FileAction] = recordMergeOperation(\n      extraOpType = \"writeDeletionVectors\",\n      status = s\"MERGE operation - Rewriting Deletion Vectors to ${filesToRewrite.size} files\",\n      sqlMetricName = \"rewriteTimeMs\") {\n\n    val fileIndex = new TahoeBatchFileIndex(\n      spark,\n      actionType = \"merge\",\n      addFiles = filesToRewrite,\n      deltaLog = deltaTxn.deltaLog,\n      path = deltaTxn.deltaLog.dataPath,\n      snapshot = deltaTxn.snapshot)\n\n    val targetDF = DMLWithDeletionVectorsHelper.createTargetDfForScanningForMatches(\n      spark,\n      target,\n      fileIndex)\n\n    // For writing DVs we are only interested in the target table. When there are no\n    // notMatchedBySource clauses an inner join is sufficient. Otherwise, we need an rightOuter\n    // join to include target rows that are not matched.\n    val joinType = if (notMatchedBySourceClauses.isEmpty) {\n      \"inner\"\n    } else {\n      \"rightOuter\"\n    }\n\n    val joinedDF = getMergeSource.df\n      .withColumn(SOURCE_ROW_PRESENT_COL, lit(true))\n      .join(targetDF, Column(condition), joinType)\n\n    val modifiedRowsFilter = generateFilterForModifiedRows()\n    val matchedDVResult =\n      DeletionVectorBitmapGenerator.buildRowIndexSetsForFilesMatchingCondition(\n        spark,\n        deltaTxn,\n        tableHasDVs = true,\n        targetDf = joinedDF,\n        candidateFiles = filesToRewrite,\n        condition = modifiedRowsFilter\n      )\n\n    val nameToAddFileMap = generateCandidateFileMap(targetDeltaLog.dataPath, filesToRewrite)\n\n    val touchedFilesWithDVs = DMLWithDeletionVectorsHelper\n      .findFilesWithMatchingRows(deltaTxn, nameToAddFileMap, matchedDVResult)\n\n    val (dvActions, metricsMap) = DMLWithDeletionVectorsHelper.processUnmodifiedData(\n      spark,\n      touchedFilesWithDVs,\n      deltaTxn.snapshot,\n      StatsCollectionUtils.getDataSkippingStringPrefixLength(spark, deltaTxn.metadata))\n\n    metrics(\"numTargetDeletionVectorsAdded\")\n      .set(metricsMap.getOrElse(\"numDeletionVectorsAdded\", 0L))\n    metrics(\"numTargetDeletionVectorsRemoved\")\n      .set(metricsMap.getOrElse(\"numDeletionVectorsRemoved\", 0L))\n    metrics(\"numTargetDeletionVectorsUpdated\")\n      .set(metricsMap.getOrElse(\"numDeletionVectorsUpdated\", 0L))\n\n    // When DVs are enabled we override metrics related to removed files.\n    metrics(\"numTargetFilesRemoved\").set(metricsMap.getOrElse(\"numRemovedFiles\", 0L))\n\n    val fullyRemovedFiles = touchedFilesWithDVs.filter(_.isFullyReplaced()).map(_.fileLogEntry)\n    val (removedBytes, removedPartitions) = totalBytesAndDistinctPartitionValues(fullyRemovedFiles)\n    metrics(\"numTargetBytesRemoved\").set(removedBytes)\n    metrics(\"numTargetPartitionsRemovedFrom\").set(removedPartitions)\n\n    dvActions\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/merge/InsertOnlyMergeExecutor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.merge\n\nimport org.apache.spark.sql.delta.metric.IncrementMetric\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction}\nimport org.apache.spark.sql.delta.commands.MergeIntoCommandBase\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.expressions.{Alias, CaseWhen, Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical._\n\n/**\n * Trait with optimized execution for merges that only inserts new data.\n * There are two cases for inserts only: when there are no matched clauses for the merge command\n * and when there is nothing matched for the merge command even if there are matched clauses.\n */\ntrait InsertOnlyMergeExecutor extends MergeOutputGeneration {\n  self: MergeIntoCommandBase =>\n  import MergeIntoCommandBase._\n\n  /**\n   * Optimization to write new files by inserting only new data.\n   *\n   * When there are no matched clauses for the merge command, data is skipped\n   * based on the merge condition and left anti join is performed on the source\n   * data to find the rows to be inserted.\n   *\n   * When there is nothing matched for the merge command even if there are matched clauses,\n   * the source table is used to perform inserting.\n   *\n   * @param spark The spark session.\n   * @param deltaTxn The existing transaction.\n   * @param filterMatchedRows Whether to filter away matched data or not.\n   * @param numSourceRowsMetric The name of the metric in which to record the number of source rows\n   */\n  protected def writeOnlyInserts(\n      spark: SparkSession,\n      deltaTxn: OptimisticTransaction,\n      filterMatchedRows: Boolean,\n      numSourceRowsMetric: String): Seq[FileAction] = {\n    val extraOpType = if (filterMatchedRows) {\n      \"writeInsertsOnlyWhenNoMatchedClauses\"\n    } else \"writeInsertsOnlyWhenNoMatches\"\n    recordMergeOperation(\n      extraOpType = extraOpType,\n      status = \"MERGE operation - writing new files for only inserts\",\n      sqlMetricName = \"rewriteTimeMs\") {\n\n      // If nothing to do when not matched, then nothing to insert, that is, no new files to write\n      if (!includesInserts && !filterMatchedRows) {\n        performedSecondSourceScan = false\n        return Seq.empty\n      }\n\n      // source DataFrame\n      val mergeSource = getMergeSource\n      // Expression to update metrics.\n      val incrSourceRowCountExpr = incrementMetricAndReturnBool(numSourceRowsMetric, true)\n      val sourceDF = filterSource(mergeSource.df.filter(Column(incrSourceRowCountExpr)))\n\n      var dataSkippedFiles: Option[Seq[AddFile]] = None\n      val preparedSourceDF = if (filterMatchedRows) {\n        // This is an optimization of the case when there is no update clause for the merge.\n        // We perform an left anti join on the source data to find the rows to be inserted.\n\n        // Skip data based on the merge condition\n        val conjunctivePredicates = splitConjunctivePredicates(condition)\n        val targetOnlyPredicates =\n          conjunctivePredicates.filter(_.references.subsetOf(target.outputSet))\n        dataSkippedFiles = Some(deltaTxn.filterFiles(targetOnlyPredicates))\n\n        val targetPlan = buildTargetPlanWithFiles(\n          spark,\n          deltaTxn,\n          dataSkippedFiles.get,\n          columnsToDrop = Nil)\n        val targetDF = DataFrameUtils.ofRows(spark, targetPlan)\n        sourceDF.join(targetDF, Column(condition), \"leftanti\")\n      } else {\n        sourceDF\n      }\n\n      val outputDF = generateInsertsOnlyOutputDF(spark, preparedSourceDF, deltaTxn)\n      logDebug(s\"$extraOpType: output plan:\\n\" + outputDF.queryExecution)\n\n      val newFiles = writeFiles(spark, deltaTxn, outputDF)\n\n      // Update metrics\n      if (filterMatchedRows) {\n        metrics(\"numTargetFilesBeforeSkipping\") += deltaTxn.snapshot.numOfFiles\n        metrics(\"numTargetBytesBeforeSkipping\") += deltaTxn.snapshot.sizeInBytes\n        if (dataSkippedFiles.nonEmpty) {\n          val (afterSkippingBytes, afterSkippingPartitions) =\n            totalBytesAndDistinctPartitionValues(dataSkippedFiles.get)\n          metrics(\"numTargetFilesAfterSkipping\") += dataSkippedFiles.get.size\n          metrics(\"numTargetBytesAfterSkipping\") += afterSkippingBytes\n          metrics(\"numTargetPartitionsAfterSkipping\") += afterSkippingPartitions\n        }\n        metrics(\"numTargetFilesRemoved\") += 0\n        metrics(\"numTargetBytesRemoved\") += 0\n        metrics(\"numTargetPartitionsRemovedFrom\") += 0\n      }\n      metrics(\"numTargetFilesAdded\") += newFiles.count(_.isInstanceOf[AddFile])\n      val (addedBytes, addedPartitions) = totalBytesAndDistinctPartitionValues(newFiles)\n      metrics(\"numTargetBytesAdded\") += addedBytes\n      metrics(\"numTargetPartitionsAddedTo\") += addedPartitions\n      newFiles\n    }\n  }\n\n  private def filterSource(source: DataFrame): DataFrame = {\n    // If there is only one insert clause, then filter out the source rows that do not\n    // satisfy the clause condition because those rows will not be written out.\n    if (notMatchedClauses.size == 1 && notMatchedClauses.head.condition.isDefined) {\n      source.filter(Column(notMatchedClauses.head.condition.get))\n    } else {\n      source\n    }\n  }\n\n  /**\n   * Generate the DataFrame to write out for merges that contains only inserts - either, insert-only\n   * clauses or inserts when no matches were found.\n   *\n   * Specifically, it handles insert clauses in two cases: when there is only one insert clause,\n   * and when there are multiple insert clauses.\n   */\n  private def generateInsertsOnlyOutputDF(\n      spark: SparkSession,\n      preparedSourceDF: DataFrame,\n      deltaTxn: OptimisticTransaction): DataFrame = {\n\n    val targetWriteColNames = deltaTxn.metadata.schema.map(_.name)\n\n    // When there is only one insert clause, there is no need for ROW_DROPPED_COL and\n    // output df can be generated without CaseWhen.\n    if (notMatchedClauses.size == 1) {\n      val outputCols = generateOneInsertOutputCols(targetWriteColNames)\n      return preparedSourceDF\n        .filter(Column(incrementMetricAndReturnBool(\"numTargetRowsInserted\", valueToReturn = true)))\n        .select(outputCols: _*)\n    }\n\n    // Precompute conditions in insert clauses and generate source data frame with precomputed\n    // boolean columns and insert clauses with rewritten conditions.\n    val (sourceWithPrecompConditions, insertClausesWithPrecompConditions) =\n      generatePrecomputedConditionsAndDF(preparedSourceDF, notMatchedClauses)\n\n    // Generate output cols.\n    val outputCols = generateInsertsOnlyOutputCols(\n      targetWriteColNames,\n      insertClausesWithPrecompConditions\n        .collect { case c: DeltaMergeIntoNotMatchedInsertClause => c })\n\n    sourceWithPrecompConditions\n      .select(outputCols: _*)\n      .filter(s\"$ROW_DROPPED_COL = false\")\n      .drop(ROW_DROPPED_COL)\n  }\n\n  /**\n   * Generate output columns when there is only one insert clause.\n   *\n   * It assumes that the caller has already filtered out the source rows (`preparedSourceDF`)\n   * that do not satisfy the insert clause condition (if any).\n   * Then it simply applies the insertion action expression to generate\n   * the output target table rows.\n   */\n  private def generateOneInsertOutputCols(\n      targetWriteColNames: Seq[String]\n    ): Seq[Column] = {\n\n    val outputExprs = notMatchedClauses.head.resolvedActions.map(_.expr)\n    assert(outputExprs.nonEmpty)\n    // generate the outputDF without `CaseWhen` expressions.\n    outputExprs.zip(targetWriteColNames).map { case (expr, name) =>\n      Column(Alias(expr, name)())\n    }\n  }\n\n  /**\n   * Generate the output columns for inserts only when there are multiple insert clauses.\n   *\n   * It combines all the conditions and corresponding actions expressions\n   * into complicated CaseWhen expressions - one CaseWhen expression for\n   * each column in the target row. If a source row does not match any of the clause conditions,\n   * then the row will be dropped. These CaseWhen expressions basically look like this.\n   *\n   *    For the i-th output column,\n   *    CASE\n   *        WHEN [insert condition 1] THEN [execute i-th expression of insert action 1]\n   *        WHEN [insert condition 2] THEN [execute i-th expression of insert action 2]\n   *        ELSE [mark the source row to be dropped]\n   */\n  private def generateInsertsOnlyOutputCols(\n      targetWriteColNames: Seq[String],\n      insertClausesWithPrecompConditions: Seq[DeltaMergeIntoNotMatchedClause]\n    ): Seq[Column] = {\n    // ==== Generate the expressions to generate the target rows from the source rows ====\n    // If there are N columns in the target table, there will be N + 1 columns generated\n    // - N columns for target table\n    // - ROW_DROPPED_COL to define whether the generated row should be dropped or written out\n    // To generate these N + 1 columns, we will generate N + 1 expressions\n\n    val outputColNames = targetWriteColNames :+ ROW_DROPPED_COL\n    val numOutputCols = outputColNames.size\n\n    // Generate the sequence of N + 1 expressions from the sequence of INSERT clauses\n    val allInsertExprs: Seq[Seq[Expression]] =\n      insertClausesWithPrecompConditions.map { clause =>\n        clause.resolvedActions.map(_.expr) :+ incrementMetricAndReturnBool(\n          \"numTargetRowsInserted\", false)\n      }\n\n    // Expressions to drop the source row when it does not match any of the insert clause\n    // conditions. Note that it sets the N+1-th column ROW_DROPPED_COL to true.\n    val dropSourceRowExprs =\n      targetWriteColNames.map { _ => Literal(null)} :+ Literal.TrueLiteral\n\n    // Generate the final N + 1 expressions to generate the final target output rows.\n    // There are multiple not match clauses. Use `CaseWhen` to conditionally evaluate the right\n    // action expressions to output columns.\n    val outputExprs: Seq[Expression] = {\n      val allInsertConditions =\n        insertClausesWithPrecompConditions.map(_.condition.getOrElse(Literal.TrueLiteral))\n\n      (0 until numOutputCols).map { i =>\n        // For the i-th output column, generate\n        // CASE\n        //     WHEN <not match condition 1> THEN <execute i-th expression of action 1>\n        //     WHEN <not match condition 2> THEN <execute i-th expression of action 2>\n        //     ...\n        //\n        val conditionalBranches = allInsertConditions.zip(allInsertExprs).map {\n          case (notMatchCond, notMatchActionExprs) => notMatchCond -> notMatchActionExprs(i)\n        }\n        CaseWhen(conditionalBranches, dropSourceRowExprs(i))\n      }\n    }\n\n    assert(outputExprs.size == numOutputCols,\n      s\"incorrect # not matched expressions:\\n\\t\" + seqToString(outputExprs))\n    logDebug(\"prepareInsertsOnlyOutputDF: not matched expressions\\n\\t\" +\n      seqToString(outputExprs))\n\n    outputExprs.zip(outputColNames).map { case (expr, name) =>\n      Column(Alias(expr, name)())\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/merge/MergeIntoMaterializeSource.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.merge\n\nimport scala.annotation.tailrec\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DataFrameUtils, DeltaErrors, DeltaLog}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.DeltaSparkPlanUtils\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.FileSourceOptions\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{AttributeSet, Expression}\nimport org.apache.spark.sql.catalyst.optimizer.EliminateResolvedHint\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.execution.LogicalRDD\nimport org.apache.spark.sql.execution.datasources.HadoopFsRelation\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.internal.SQLConf._\nimport org.apache.spark.sql.sources.BaseRelation\nimport org.apache.spark.storage.StorageLevel\n\n/**\n * Trait with logic and utilities used for materializing a snapshot of MERGE source\n * in case we can't guarantee deterministic repeated reads from it.\n *\n * We materialize source if it is not safe to assume that it's deterministic\n * (override with MERGE_SOURCE_MATERIALIZATION).\n * Otherwise, if source changes between the phases of the MERGE, it can produce wrong results.\n * We use local checkpointing for the materialization, which saves the source as a\n * materialized RDD[InternalRow] on the executor local disks.\n *\n * 1st concern is that if an executor is lost, this data can be lost.\n * When Spark executor decommissioning API is used, it should attempt to move this\n * materialized data safely out before removing the executor.\n *\n * 2nd concern is that if an executor is lost for another reason (e.g. spot kill), we will\n * still lose that data. To mitigate that, we implement a retry loop.\n * The whole Merge operation needs to be restarted from the beginning in this case.\n * When we retry, we increase the replication level of the materialized data from 1 to 2.\n * (override with MERGE_SOURCE_MATERIALIZATION_RDD_STORAGE_LEVEL_RETRY).\n * If it still fails after the maximum number of attempts (MERGE_MATERIALIZE_SOURCE_MAX_ATTEMPTS),\n * we record the failure for tracking purposes.\n *\n * 3rd concern is that executors run out of disk space with the extra materialization.\n * We record such failures for tracking purposes.\n */\ntrait MergeIntoMaterializeSource extends DeltaLogging with DeltaSparkPlanUtils {\n\n  import MergeIntoMaterializeSource._\n\n  protected def operation: String = \"MERGE\"\n\n  protected def enableColumnPruningBeforeMaterialize: Boolean = true\n\n  protected def materializeSourceErrorOpType: String =\n    MergeIntoMaterializeSourceError.OP_TYPE\n\n  protected def getMaterializeSourceMode(spark: SparkSession): String =\n    spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE)\n\n  /**\n   * Prepared Dataframe with source data.\n   * If needed, it is materialized, @see prepareMergeSource\n   */\n  private var mergeSource: Option[MergeSource] = None\n\n  /**\n   * If the source was materialized, reference to the checkpointed RDD.\n   */\n  protected var materializedSourceRDD: Option[RDD[InternalRow]] = None\n\n  /**\n   * True if source materialization is used.\n   * It is set when materializedSourceRDD may not yet be initialized.\n   */\n  private var materializeSource = false\n\n  /**\n   * StorageLevel used for source materialization.\n   * It is set when materializedSourceRDD may not yet be initialized.\n   */\n  private var materializeSourceStorageLevel = StorageLevel.NONE\n\n  /**\n   * Track which attempt or retry it is in runWithMaterializedSourceAndRetries\n   */\n  protected var attempt: Int = 0\n\n  /**\n   * Run the Merge with retries in case it detects an RDD block lost error of the\n   * materialized source RDD.\n   * It will also record out of disk error, if such happens - possibly because of increased disk\n   * pressure from the materialized source RDD.\n   */\n  protected def runWithMaterializedSourceLostRetries(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      metrics: Map[String, SQLMetric],\n      runOperationFunc: SparkSession => Seq[Row]): Seq[Row] = {\n    var doRetry = false\n    var runResult: Seq[Row] = null\n    attempt = 1\n    do {\n      doRetry = false\n      metrics.values.foreach(_.reset())\n      try {\n        runResult = runOperationFunc(spark)\n      } catch {\n        case NonFatal(ex) =>\n          val isLastAttempt =\n            attempt == spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_MAX_ATTEMPTS)\n          handleExceptionDuringAttempt(ex, isLastAttempt, deltaLog) match {\n            case RetryHandling.Retry =>\n              logInfo(log\"Retrying ${MDC(DeltaLogKeys.OPERATION, operation)} \" +\n                log\"with materialized source.\" +\n                log\"Attempt ${MDC(DeltaLogKeys.NUM_ATTEMPT, attempt)} failed.\")\n              doRetry = true\n              attempt += 1\n            case RetryHandling.ExhaustedRetries =>\n              logError(log\"Exhausted retries after ${MDC(DeltaLogKeys.NUM_ATTEMPT, attempt)}\" +\n                log\" attempts in ${MDC(DeltaLogKeys.OPERATION, operation)} \" +\n                log\"with materialized source. Logging latest exception.\", ex)\n              throw DeltaErrors.sourceMaterializationFailedRepeatedlyInMerge\n            case RetryHandling.RethrowException =>\n              logError(log\"Fatal error in ${MDC(DeltaLogKeys.OPERATION, operation)} \" +\n                log\"with materialized source in \" +\n                log\"attempt ${MDC(DeltaLogKeys.NUM_ATTEMPT, attempt)}\", ex)\n              throw ex\n          }\n      } finally {\n        // Remove source from RDD cache (noop if wasn't cached)\n        materializedSourceRDD.foreach { rdd =>\n          rdd.unpersist()\n        }\n        materializedSourceRDD = None\n        mergeSource = None\n        materializeSource = false\n        materializeSourceStorageLevel = StorageLevel.NONE\n      }\n    } while (doRetry)\n\n    runResult\n  }\n\n  object RetryHandling extends Enumeration {\n    type Result = Value\n\n    val Retry, RethrowException, ExhaustedRetries = Value\n  }\n\n  /**\n   * Handle exception that was thrown from runMerge().\n   * Search for errors to log, or that can be handled by retry.\n   * It may need to descend into ex.getCause() to find the errors, as Spark may have wrapped them.\n   * @param isLastAttempt indicates that it's the last allowed attempt and there shall be no retry.\n   * @return true if the exception is handled and merge should retry\n   *         false if the caller should rethrow the error\n   */\n  @tailrec\n  private def handleExceptionDuringAttempt(\n      ex: Throwable,\n      isLastAttempt: Boolean,\n      deltaLog: DeltaLog): RetryHandling.Result = ex match {\n    // If Merge failed because the materialized source lost blocks from the\n    // locally checkpointed RDD, we want to retry the whole operation.\n    // If a checkpointed RDD block is lost, it throws\n    // SparkCoreErrors.checkpointRDDBlockIdNotFoundError from LocalCheckpointRDD.compute.\n    case s: SparkException\n      if materializedSourceRDD.nonEmpty &&\n        s.getErrorClass == \"CHECKPOINT_RDD_BLOCK_ID_NOT_FOUND\" &&\n        s.getMessageParameters.get(\"rddBlockId\").contains(s\"rdd_${materializedSourceRDD.get.id}\") =>\n      logWarning(log\"Materialized ${MDC(DeltaLogKeys.OPERATION, operation)} source RDD block \" +\n        log\"lost. ${MDC(DeltaLogKeys.OPERATION, operation)} needs to be restarted. \" +\n        log\"This was attempt number ${MDC(DeltaLogKeys.ATTEMPT, attempt)}.\")\n      if (!isLastAttempt) {\n        RetryHandling.Retry\n      } else {\n        // Record situations where we lost RDD materialized source blocks, despite retries.\n        recordDeltaEvent(\n          deltaLog,\n          materializeSourceErrorOpType,\n          data = MergeIntoMaterializeSourceError(\n            errorType = MergeIntoMaterializeSourceErrorType.RDD_BLOCK_LOST.toString,\n            attempt = attempt,\n            materializedSourceRDDStorageLevel =\n              materializedSourceRDD.get.getStorageLevel.toString\n          )\n        )\n        RetryHandling.ExhaustedRetries\n      }\n\n    // Record if we ran out of executor disk space when we materialized the source.\n    case s: SparkException\n      if materializeSource &&\n        s.getMessage.contains(\"java.io.IOException: No space left on device\") =>\n      // Record situations where we ran out of disk space, possibly because of the space took\n      // by the materialized RDD.\n      recordDeltaEvent(\n        deltaLog,\n        materializeSourceErrorOpType,\n        data = MergeIntoMaterializeSourceError(\n          errorType = MergeIntoMaterializeSourceErrorType.OUT_OF_DISK.toString,\n          attempt = attempt,\n          materializedSourceRDDStorageLevel = materializeSourceStorageLevel.toString\n        )\n      )\n      RetryHandling.RethrowException\n\n    // Descend into ex.getCause.\n    // The errors that we are looking for above might have been wrapped inside another exception.\n    case NonFatal(ex) if ex.getCause() != null =>\n      handleExceptionDuringAttempt(ex.getCause(), isLastAttempt, deltaLog)\n\n    // Descended to the bottom of the causes without finding a retryable error\n    case _ => RetryHandling.RethrowException\n  }\n\n  private def planContainsIgnoreUnreadableFilesReadOptions(plan: LogicalPlan): Boolean = {\n    def relationContainsOptions(relation: BaseRelation): Boolean = {\n      relation match {\n        case hdpRelation: HadoopFsRelation =>\n          hdpRelation.options.get(FileSourceOptions.IGNORE_CORRUPT_FILES).contains(\"true\") ||\n            hdpRelation.options.get(FileSourceOptions.IGNORE_MISSING_FILES).contains(\"true\")\n        case _ => false\n      }\n    }\n\n    val res = plan.collectFirst {\n      case lr: LogicalRelation if relationContainsOptions(lr.relation) => lr\n    }\n    res.nonEmpty\n  }\n\n  private def ignoreUnreadableFilesConfigsAreSet(plan: LogicalPlan, spark: SparkSession)\n    : Boolean = {\n    spark.conf.get(IGNORE_MISSING_FILES) || spark.conf.get(IGNORE_CORRUPT_FILES) ||\n      planContainsIgnoreUnreadableFilesReadOptions(plan)\n  }\n\n  /**\n   * @return pair of boolean whether source should be materialized\n   *         and the source materialization reason\n   */\n  protected def shouldMaterializeSource(\n    spark: SparkSession, source: LogicalPlan, isInsertOnly: Boolean\n  ): (Boolean, MergeIntoMaterializeSourceReason.MergeIntoMaterializeSourceReason) = {\n    val materializeType = getMaterializeSourceMode(spark)\n    val forceMaterializationWithUnreadableFiles =\n      spark.conf.get(DeltaSQLConf.MERGE_FORCE_SOURCE_MATERIALIZATION_WITH_UNREADABLE_FILES)\n    import DeltaSQLConf.MergeMaterializeSource._\n    val checkDeterministicOptions =\n      DeltaSparkPlanUtils.CheckDeterministicOptions(allowDeterministicUdf = true)\n    val materializeCachedSource = spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_CACHED_SOURCE)\n\n    materializeType match {\n      case ALL =>\n        (true, MergeIntoMaterializeSourceReason.MATERIALIZE_ALL)\n      case NONE =>\n        (false, MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_NONE)\n      case AUTO =>\n        if (isInsertOnly && spark.conf.get(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED)) {\n          (false, MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO_INSERT_ONLY)\n        } else if (!planContainsOnlyDeltaScans(source)) {\n          (true, MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA)\n        } else if (!planIsDeterministic(source, checkDeterministicOptions)) {\n          (true, MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_OPERATORS)\n          // Force source materialization if Spark configs IGNORE_CORRUPT_FILES,\n          // IGNORE_MISSING_FILES or file source read options FileSourceOptions.IGNORE_CORRUPT_FILES\n          // FileSourceOptions.IGNORE_MISSING_FILES are enabled on the source.\n          // This is done so to prevent irrecoverable data loss or unexpected results.\n        } else if (forceMaterializationWithUnreadableFiles &&\n            ignoreUnreadableFilesConfigsAreSet(source, spark)) {\n          (true, MergeIntoMaterializeSourceReason.IGNORE_UNREADABLE_FILES_CONFIGS_ARE_SET)\n        } else if (planContainsUdf(source)) {\n          // Force source materialization if the source contains a User Defined Function, even if\n          // the user defined function is marked as deterministic, as it is often incorrectly marked\n          // as such.\n          (true, MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_WITH_DETERMINISTIC_UDF)\n        } else if (materializeCachedSource &&\n            planContainsCachedRelation(DataFrameUtils.ofRows(spark, source))) {\n          // The query cache doesn't pin the version of cached Delta tables, cache can get\n          // concurrently updated in the middle of MERGE execution. We materialize the source in\n          // that case to avoid this issue.\n          (true, MergeIntoMaterializeSourceReason.SOURCE_CACHED)\n        } else {\n          (false, MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO)\n        }\n      case _ =>\n        // If the config is invalidly set, also materialize.\n        (true, MergeIntoMaterializeSourceReason.INVALID_CONFIG)\n    }\n  }\n  /**\n   * If source needs to be materialized, prepare the materialized dataframe in sourceDF\n   * Otherwise, prepare regular dataframe.\n   * @return the source materialization reason\n   */\n  protected def prepareMergeSource(\n      spark: SparkSession,\n      source: LogicalPlan,\n      condition: Expression,\n      matchedClauses: Seq[DeltaMergeIntoMatchedClause],\n      notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause],\n      isInsertOnly: Boolean): Unit = {\n    val (materialize, materializeReason) =\n      shouldMaterializeSource(spark, source, isInsertOnly)\n    materializeSource = materialize\n    if (!materialize) {\n      // Does not materialize, simply return the dataframe from source plan\n      mergeSource = Some(\n        MergeSource(\n          df = DataFrameUtils.ofRows(spark, source),\n          isMaterialized = false,\n          materializeReason = materializeReason\n        )\n      )\n      return\n    }\n\n    val referencedSourceColumns = if (enableColumnPruningBeforeMaterialize) {\n      getReferencedSourceColumns(source, condition, matchedClauses, notMatchedClauses)\n    } else {\n      assert(matchedClauses.isEmpty && notMatchedClauses.isEmpty,\n        \"If column pruning is disabled, then there should be no MERGE clauses.\")\n      assert(operation != \"MERGE\",\n        \"Column pruning before materialization must be done for MERGE.\")\n      source.output\n    }\n\n    val baseSourcePlanDF = if (enableColumnPruningBeforeMaterialize) {\n      // When we materialize the source, we want to make sure that columns got pruned\n      // before caching.\n      val sourceWithSelectedColumns = Project(referencedSourceColumns, source)\n      DataFrameUtils.ofRows(spark, sourceWithSelectedColumns)\n    } else {\n      DataFrameUtils.ofRows(spark, source)\n    }\n\n    // Select appropriate StorageLevel\n    materializeSourceStorageLevel = StorageLevel.fromString(\n      if (attempt == 1) {\n        spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL)\n      } else if (attempt == 2) {\n        // If it failed the first time, potentially use a different storage level on retry. The\n        // first retry has its own conf to allow gradually increasing the replication level.\n        spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL_FIRST_RETRY)\n      } else {\n        spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL_RETRY)\n      }\n    )\n\n    // Caches the source in RDD cache using localCheckpoint, which cuts away the RDD lineage,\n    // which shall ensure that the source cannot be recomputed and thus become inconsistent.\n    //\n    // WARNING: if eager == false, the source used during the first Spark Job that uses this may\n    // still be inconsistent with source materialized afterwards.\n    // This is because doCheckpoint that finalizes the lazy checkpoint is called after the Job\n    // that triggered the lazy checkpointing finished.\n    // If blocks were lost during that job, they may still get recomputed and changed compared\n    // to how they were used during the execution of the job.\n    val checkpointedSourcePlanDF =\n      baseSourcePlanDF.localCheckpoint(\n        eager = true, storageLevel = materializeSourceStorageLevel)\n\n    // We have to reach through the crust and into the plan of the checkpointed DF\n    // to get the RDD that was actually checkpointed, to be able to unpersist it later...\n    var checkpointedPlan = checkpointedSourcePlanDF.queryExecution.analyzed\n    val rdd = checkpointedPlan.asInstanceOf[LogicalRDD].rdd\n    assert(rdd.isCheckpointed)\n    materializedSourceRDD = Some(rdd)\n    rdd.setName(\"mergeMaterializedSource\")\n\n    // We should still keep the hints from the input plan.\n    checkpointedPlan = addHintsToPlan(source, checkpointedPlan)\n\n    mergeSource = Some(\n      MergeSource(\n        df = DataFrameUtils.ofRows(spark, checkpointedPlan),\n        isMaterialized = true,\n        materializeReason = materializeReason\n      )\n    )\n\n\n    logDebug(s\"Materializing $operation with pruned columns $referencedSourceColumns.\")\n    logDebug(s\"Materialized $operation source plan:\\n${getMergeSource.df.queryExecution}\")\n  }\n\n  /** Returns the prepared merge source. */\n  protected def getMergeSource: MergeSource = mergeSource match {\n    case Some(source) => source\n    case None => throw new IllegalStateException(\n      \"mergeSource was not initialized! Call prepareMergeSource before.\")\n  }\n\n  private def addHintsToPlan(sourcePlan: LogicalPlan, plan: LogicalPlan): LogicalPlan = {\n    val hints = EliminateResolvedHint.extractHintsFromPlan(sourcePlan)._2\n    // This follows similar code in CacheManager from https://github.com/apache/spark/pull/24580\n    if (hints.nonEmpty) {\n      // The returned hint list is in top-down order, we should create the hint nodes from\n      // right to left.\n      val planWithHints =\n      hints.foldRight[LogicalPlan](plan) { case (hint, p) =>\n        ResolvedHint(p, hint)\n      }\n      planWithHints\n    } else {\n      plan\n    }\n  }\n}\n\nobject MergeIntoMaterializeSource {\n  case class MergeSource(\n      df: DataFrame,\n      isMaterialized: Boolean,\n      materializeReason: MergeIntoMaterializeSourceReason.MergeIntoMaterializeSourceReason) {\n    assert(!isMaterialized ||\n      MergeIntoMaterializeSourceReason.MATERIALIZED_REASONS.contains(materializeReason))\n  }\n\n  /**\n   * @return The columns of the source plan that are used in this MERGE\n   */\n  private def getReferencedSourceColumns(\n      source: LogicalPlan,\n      condition: Expression,\n      matchedClauses: Seq[DeltaMergeIntoMatchedClause],\n      notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause]) = {\n    val conditionCols = condition.references\n    val matchedCondCols = matchedClauses.flatMap(_.condition).flatMap(_.references)\n    val notMatchedCondCols = notMatchedClauses.flatMap(_.condition).flatMap(_.references)\n    val matchedActionsCols = matchedClauses\n      .flatMap(_.resolvedActions)\n      .flatMap(_.expr.references)\n    val notMatchedActionsCols = notMatchedClauses\n      .flatMap(_.resolvedActions)\n      .flatMap(_.expr.references)\n    val allCols = AttributeSet(\n      conditionCols ++\n        matchedCondCols ++\n        notMatchedCondCols ++\n        matchedActionsCols ++\n        notMatchedActionsCols)\n\n    source.output.filter(allCols.contains)\n  }\n}\n\n/**\n * Enumeration with possible reasons that source may be materialized in a MERGE command.\n */\nobject MergeIntoMaterializeSourceReason extends Enumeration {\n  type MergeIntoMaterializeSourceReason = Value\n  // It was determined to not materialize on auto config.\n  val NOT_MATERIALIZED_AUTO = Value(\"notMaterializedAuto\")\n  // Config was set to never materialize source.\n  val NOT_MATERIALIZED_NONE = Value(\"notMaterializedNone\")\n  // Insert only merge is single pass, no need for materialization\n  val NOT_MATERIALIZED_AUTO_INSERT_ONLY = Value(\"notMaterializedAutoInsertOnly\")\n  // Config was set to always materialize source.\n  val MATERIALIZE_ALL = Value(\"materializeAll\")\n  // The source query is considered non-deterministic, because it contains a non-delta scan.\n  val NON_DETERMINISTIC_SOURCE_NON_DELTA = Value(\"materializeNonDeterministicSourceNonDelta\")\n  // The source query is considered non-deterministic, because it contains non-deterministic\n  // operators.\n  val NON_DETERMINISTIC_SOURCE_OPERATORS = Value(\"materializeNonDeterministicSourceOperators\")\n  // Either spark configs to ignore unreadable files are set or the source plan contains relations\n  // with ignore unreadable files options.\n  val IGNORE_UNREADABLE_FILES_CONFIGS_ARE_SET =\n    Value(\"materializeIgnoreUnreadableFilesConfigsAreSet\")\n  // The source query is considered non-determistic because it contains a User Defined Function.\n  val NON_DETERMINISTIC_SOURCE_WITH_DETERMINISTIC_UDF =\n    Value(\"materializeNonDeterministicSourceWithDeterministicUdf\")\n  // Materialize when the configuration is invalid\n  val INVALID_CONFIG = Value(\"invalidConfigurationFailsafe\")\n  // Materialize when the source is cached.\n  val SOURCE_CACHED = Value(\"materializeCachedSource\")\n  // Catch-all case.\n  val UNKNOWN = Value(\"unknown\")\n\n  // Set of reasons that result in source materialization.\n  final val MATERIALIZED_REASONS: Set[MergeIntoMaterializeSourceReason] = Set(\n    MATERIALIZE_ALL,\n    NON_DETERMINISTIC_SOURCE_NON_DELTA,\n    NON_DETERMINISTIC_SOURCE_OPERATORS,\n    IGNORE_UNREADABLE_FILES_CONFIGS_ARE_SET,\n    NON_DETERMINISTIC_SOURCE_WITH_DETERMINISTIC_UDF,\n    INVALID_CONFIG,\n    SOURCE_CACHED\n  )\n}\n\n/**\n * Structure with data for \"delta.dml.merge.materializeSourceError\" event.\n * Note: We log only errors that we want to track (out of disk or lost RDD blocks).\n */\ncase class MergeIntoMaterializeSourceError(\n  errorType: String,\n  attempt: Int,\n  materializedSourceRDDStorageLevel: String\n)\n\nobject MergeIntoMaterializeSourceError {\n  val OP_TYPE = \"delta.dml.merge.materializeSourceError\"\n}\n\nobject MergeIntoMaterializeSourceErrorType extends Enumeration {\n  type MergeIntoMaterializeSourceError = Value\n  val RDD_BLOCK_LOST = Value(\"materializeSourceRDDBlockLostRetriesFailure\")\n  val OUT_OF_DISK = Value(\"materializeSourceOutOfDiskFailure\")\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/merge/MergeOutputGeneration.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.merge\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{RowCommitVersion, RowId}\nimport org.apache.spark.sql.delta.commands.MergeIntoCommandBase\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.functions._\n\n/**\n * Contains logic to transform the merge clauses into expressions that can be evaluated to obtain\n * the output of the merge operation.\n */\ntrait MergeOutputGeneration { self: MergeIntoCommandBase =>\n  import CDCReader._\n  import MergeIntoCommandBase._\n  import MergeOutputGeneration._\n\n  /**\n   * Precompute conditions in MATCHED and NOT MATCHED clauses and generate the source\n   * data frame with precomputed boolean columns.\n   * @param sourceDF the source DataFrame.\n   * @param clauses the merge clauses to precompute.\n   * @return Generated sourceDF with precomputed boolean columns, matched clauses with\n   *         possible rewritten clause conditions, insert clauses with possible rewritten\n   *         clause conditions\n   */\n  protected def generatePrecomputedConditionsAndDF(\n      sourceDF: DataFrame,\n      clauses: Seq[DeltaMergeIntoClause]): (DataFrame, Seq[DeltaMergeIntoClause]) = {\n    //\n    // ==== Precompute conditions in MATCHED and NOT MATCHED clauses ====\n    // If there are conditions in the clauses, each condition will be computed once for every\n    // column (and obviously for every row) within the per-column CaseWhen expressions. Since, the\n    // conditions can be arbitrarily expensive, it is likely to be more efficient to\n    // precompute them into boolean columns and use these new columns in the CaseWhen exprs.\n    // Then each condition will be computed only once per row, and the resultant boolean reused\n    // for all the columns in the row.\n    //\n    val preComputedClauseConditions = new mutable.ArrayBuffer[(String, Expression)]()\n\n    // Rewrite clause condition into a simple lookup of precomputed column\n    def rewriteCondition[T <: DeltaMergeIntoClause](clause: T): T = {\n      clause.condition match {\n        case Some(conditionExpr) =>\n          val colName =\n            s\"\"\"_${clause.clauseType}${PRECOMPUTED_CONDITION_COL}\n            |${preComputedClauseConditions.length}_\n            |\"\"\".stripMargin.replaceAll(\"\\n\", \"\") // ex: _update_condition_0_\n          preComputedClauseConditions += ((colName, conditionExpr))\n          clause.makeCopy(Array(Some(UnresolvedAttribute(colName)), clause.actions)).asInstanceOf[T]\n        case None => clause\n      }\n    }\n\n    // Get the clauses with possibly rewritten clause conditions.\n    // This will automatically populate the `preComputedClauseConditions`\n    // (as part of `rewriteCondition`)\n    val clausesWithPrecompConditions = clauses.map(rewriteCondition)\n\n    // Add the columns to the given `sourceDF` to precompute clause conditions\n    val sourceWithPrecompConditions = {\n      val newCols = preComputedClauseConditions.map { case (colName, conditionExpr) =>\n        Column(conditionExpr).as(colName)\n      }.toSeq\n      sourceDF.select(col(\"*\") +: newCols: _*)\n    }\n    (sourceWithPrecompConditions, clausesWithPrecompConditions)\n  }\n\n  /**\n   * Generate the expressions to process full-outer join output and generate target rows.\n   *\n   * To generate these N + 2 columns, we generate N + 2 expressions and apply them\n   * on the joinedDF. The CDC column will be either used for CDC generation or dropped before\n   * performing the final write, and the other column will always be dropped after executing the\n   * increment metric expression and filtering on ROW_DROPPED_COL.\n   */\n  protected def generateWriteAllChangesOutputCols(\n      targetWriteCols: Seq[Expression],\n      rowIdColumnExpressionOpt: Option[NamedExpression],\n      rowCommitVersionColumnExpressionOpt: Option[NamedExpression],\n      targetWriteColNames: Seq[String],\n      noopCopyExprs: Seq[Expression],\n      writeUnmodifiedRows: Boolean,\n      clausesWithPrecompConditions: Seq[DeltaMergeIntoClause],\n      cdcEnabled: Boolean,\n      needSetRowTrackingFieldIdForUniform: Boolean,\n      shouldCountDeletedRows: Boolean = true): IndexedSeq[Column] = {\n\n    val numOutputCols = targetWriteColNames.size\n\n    // ==== Generate N + 2 (N + 4 preserving Row Tracking) expressions for MATCHED clauses ====\n    val processedMatchClauses: Seq[ProcessedClause] = generateAllActionExprs(\n      targetWriteCols,\n      rowIdColumnExpressionOpt,\n      rowCommitVersionColumnExpressionOpt,\n      clausesWithPrecompConditions.collect { case c: DeltaMergeIntoMatchedClause => c },\n      cdcEnabled,\n      shouldCountDeletedRows)\n    val matchedExprs: Seq[Expression] =\n      generateClauseOutputExprs(\n        numOutputCols,\n        processedMatchClauses,\n        noopCopyExprs,\n        writeUnmodifiedRows)\n\n    // N + 1 (or N + 2 with CDC, N + 4 preserving Row Tracking and CDC) expressions to delete the\n    // unmatched source row when it should not be inserted. `target.output` will produce NULLs\n    // which will get deleted eventually.\n\n    val deletedColsForUnmatchedTarget =\n      if (cdcEnabled) targetWriteCols\n      else targetWriteCols.map(e => Cast(Literal(null), e.dataType))\n\n    val deleteSourceRowExprs =\n      (deletedColsForUnmatchedTarget ++\n        rowIdColumnExpressionOpt.map(_ => Literal(null)) ++\n        rowCommitVersionColumnExpressionOpt.map(_ => Literal(null)) ++\n        Seq(Literal(true))) ++\n        (if (cdcEnabled) Seq(CDC_TYPE_NOT_CDC) else Seq())\n\n    // ==== Generate N + 2 (N + 4 preserving Row Tracking) expressions for NOT MATCHED clause ====\n    val processedNotMatchClauses: Seq[ProcessedClause] = generateAllActionExprs(\n      targetWriteCols,\n      rowIdColumnExpressionOpt,\n      rowCommitVersionColumnExpressionOpt,\n      clausesWithPrecompConditions.collect { case c: DeltaMergeIntoNotMatchedClause => c },\n      cdcEnabled,\n      shouldCountDeletedRows)\n    val notMatchedExprs: Seq[Expression] =\n      generateClauseOutputExprs(\n        numOutputCols,\n        processedNotMatchClauses,\n        deleteSourceRowExprs,\n        writeUnmodifiedRows)\n\n    // === Generate N + 2 (N + 4 with Row Tracking) expressions for NOT MATCHED BY SOURCE clause ===\n    val processedNotMatchBySourceClauses: Seq[ProcessedClause] = generateAllActionExprs(\n      targetWriteCols,\n      rowIdColumnExpressionOpt,\n      rowCommitVersionColumnExpressionOpt,\n      clausesWithPrecompConditions.collect { case c: DeltaMergeIntoNotMatchedBySourceClause => c },\n      cdcEnabled,\n      shouldCountDeletedRows)\n    val notMatchedBySourceExprs: Seq[Expression] =\n      generateClauseOutputExprs(\n        numOutputCols,\n        processedNotMatchBySourceClauses,\n        noopCopyExprs,\n        writeUnmodifiedRows)\n\n    // ==== Generate N + 2 (N + 4 preserving Row Tracking) expressions that invokes the MATCHED,\n    // NOT MATCHED and NOT MATCHED BY SOURCE expressions ====\n    // That is, conditionally invokes them based on whether there was a match in the outer join.\n\n    // Predicates to check whether there was a match in the full outer join.\n    val ifSourceRowNull = expression(col(SOURCE_ROW_PRESENT_COL).isNull)\n    val ifTargetRowNull = expression(col(TARGET_ROW_PRESENT_COL).isNull)\n\n    val outputCols = targetWriteColNames.zipWithIndex.map { case (name, i) =>\n      // Coupled with the clause conditions, the resultant possibly-nested CaseWhens can\n      // be the following for every i-th column. (In the case with single matched/not-matched\n      // clauses, instead of nested CaseWhens, there will be If/Else.)\n      //\n      // CASE WHEN <target row not matched>           (source row is null)\n      //          CASE WHEN <not-matched-by-source condition 1>\n      //               THEN <execute i-th expression of not-matched-by-source action 1>\n      //               WHEN <not-matched-by-source condition 2>\n      //               THEN <execute i-th expression of not-matched-by-source action 2>\n      //               ...\n      //               ELSE <execute i-th expression to noop-copy or RaiseError>\n      //\n      //      WHEN <source row not matched>           (target row is null)\n      //      THEN\n      //          CASE WHEN <not-matched condition 1>\n      //               THEN <execute i-th expression of not-matched (insert) action 1>\n      //               WHEN <not-matched condition 2>\n      //               THEN <execute i-th expression of not-matched (insert) action 2>\n      //               ...\n      //               ELSE <execute i-th expression to delete or RaiseError>\n      //\n      //      ELSE                                    (both source and target row are not null)\n      //          CASE WHEN <match condition 1>\n      //               THEN <execute i-th expression of match action 1>\n      //               WHEN <match condition 2>\n      //               THEN <execute i-th expression of match action 2>\n      //               ...\n      //               ELSE <execute i-th expression to noop-copy or RaiseError>\n      //\n      val caseWhen = CaseWhen(Seq(\n        ifSourceRowNull -> notMatchedBySourceExprs(i),\n        ifTargetRowNull -> notMatchedExprs(i)),\n        /*  otherwise  */ matchedExprs(i))\n      if (rowIdColumnExpressionOpt.exists(_.name == name)) {\n        // Add Row ID metadata to allow writing the column.\n        Column(Alias(caseWhen, name)(\n          explicitMetadata = Some(RowId.columnMetadata(name, needSetRowTrackingFieldIdForUniform))))\n      } else if (rowCommitVersionColumnExpressionOpt.exists(_.name == name)) {\n        // Add Row Commit Versions metadata to allow writing the column.\n        Column(Alias(caseWhen, name)(\n          explicitMetadata = Some(\n            RowCommitVersion.columnMetadata(name, needSetRowTrackingFieldIdForUniform))))\n      } else {\n        Column(Alias(caseWhen, name)())\n      }\n    }\n    logDebug(\"writeAllChanges: join output expressions\\n\\t\" + seqToString(\n      outputCols.map(expression)))\n    outputCols\n  }.toIndexedSeq\n\n  /**\n   * Represents a merge clause after its condition and action expressions have been processed before\n   * generating the final output expression.\n   * @param condition Optional precomputed condition.\n   * @param actions List of output expressions generated from every action of the clause.\n   */\n  protected case class ProcessedClause(condition: Option[Expression], actions: Seq[Expression])\n\n  /**\n   * Generate expressions for every output column and every merge clause based on the corresponding\n   * UPDATE, DELETE and/or INSERT action(s).\n   * @param targetWriteCols List of output column expressions from the target table. Used to\n   *                        generate CDC data for DELETE.\n   * @param rowIdColumnExpressionOpt The optional Row ID preservation column with the physical\n   *                                 Row ID name, it stores stable Row IDs of the table.\n   * @param rowCommitVersionColumnExpressionOpt The optional Row Commit Version preservation\n   *                                 column with the physical Row Commit Version name, it stores\n   *                                 stable Row Commit Versions.\n   * @param clausesWithPrecompConditions List of merge clauses with precomputed conditions. Action\n   *                                     expressions are generated for each of these clauses.\n   * @param cdcEnabled Whether the generated expressions should include CDC information.\n   * @param shouldCountDeletedRows Whether metrics for number of deleted rows should be incremented\n   *                               here.\n   * @return For each merge clause, a list of [[ProcessedClause]] each with a precomputed\n   *         condition and N+2 action expressions (N output columns + [[ROW_DROPPED_COL]] +\n   *         [[CDC_TYPE_COLUMN_NAME]]) to apply on a row when that clause matches.\n   */\n  protected def generateAllActionExprs(\n      targetWriteCols: Seq[Expression],\n      rowIdColumnExpressionOpt: Option[NamedExpression],\n      rowCommitVersionColumnExpressionOpt: Option[NamedExpression],\n      clausesWithPrecompConditions: Seq[DeltaMergeIntoClause],\n      cdcEnabled: Boolean,\n      shouldCountDeletedRows: Boolean): Seq[ProcessedClause] = {\n    clausesWithPrecompConditions.map { clause =>\n      val actions = clause match {\n        // Seq of up to N+3 expressions to generate output rows based on the UPDATE, DELETE and/or\n        // INSERT action(s)\n        case u: DeltaMergeIntoMatchedUpdateClause =>\n          val incrCountExpr = incrementMetricsAndReturnBool(\n            names = Seq(\"numTargetRowsUpdated\", \"numTargetRowsMatchedUpdated\"),\n            valueToReturn = false)\n          // Generate update expressions and set ROW_DROPPED_COL = false\n          u.resolvedActions.map(_.expr) ++\n            rowIdColumnExpressionOpt ++\n            rowCommitVersionColumnExpressionOpt.map(_ => Literal(null)) ++\n            Seq(incrCountExpr) ++\n            (if (cdcEnabled) Some(Literal(CDC_TYPE_UPDATE_POSTIMAGE)) else None)\n        case u: DeltaMergeIntoNotMatchedBySourceUpdateClause =>\n          val incrCountExpr = incrementMetricsAndReturnBool(\n            names = Seq(\"numTargetRowsUpdated\", \"numTargetRowsNotMatchedBySourceUpdated\"),\n            valueToReturn = false)\n          // Generate update expressions and set ROW_DROPPED_COL = false\n          u.resolvedActions.map(_.expr) ++\n            rowIdColumnExpressionOpt ++\n            rowCommitVersionColumnExpressionOpt.map(_ => Literal(null)) ++\n            Seq(incrCountExpr) ++\n            (if (cdcEnabled) Some(Literal(CDC_TYPE_UPDATE_POSTIMAGE)) else None)\n        case _: DeltaMergeIntoMatchedDeleteClause =>\n          val incrCountExpr = {\n            if (shouldCountDeletedRows) {\n              incrementMetricsAndReturnBool(\n                names = Seq(\"numTargetRowsDeleted\", \"numTargetRowsMatchedDeleted\"),\n                valueToReturn = true)\n            } else {\n              Literal.TrueLiteral\n            }\n          }\n          // Generate expressions to set the ROW_DROPPED_COL = true and mark as a DELETE\n          // Only read full target columns if CDC is enabled\n          val deletedDataExprs =\n            if (cdcEnabled) {\n              targetWriteCols\n            } else {\n              targetWriteCols.map(e => Cast(Literal(null), e.dataType))\n            }\n\n          deletedDataExprs ++\n            rowIdColumnExpressionOpt ++\n            rowCommitVersionColumnExpressionOpt ++\n            Seq(incrCountExpr) ++\n            (if (cdcEnabled) Some(CDC_TYPE_DELETE) else None)\n        case _: DeltaMergeIntoNotMatchedBySourceDeleteClause =>\n          val incrCountExpr = {\n            if (shouldCountDeletedRows) {\n              incrementMetricsAndReturnBool(\n                names = Seq(\"numTargetRowsDeleted\", \"numTargetRowsNotMatchedBySourceDeleted\"),\n                valueToReturn = true)\n            } else {\n              Literal.TrueLiteral\n            }\n          }\n          // Generate expressions to set the ROW_DROPPED_COL = true and mark as a DELETE\n          // Only read full target columns if CDC is enabled\n          val deletedColsForUnmatchedTarget =\n            if (cdcEnabled) targetWriteCols\n            else targetWriteCols.map(e => Cast(Literal(null), e.dataType))\n\n          deletedColsForUnmatchedTarget ++\n            rowIdColumnExpressionOpt ++\n            rowCommitVersionColumnExpressionOpt ++\n            Seq(incrCountExpr) ++\n            (if (cdcEnabled) Some(CDC_TYPE_DELETE) else None)\n        case i: DeltaMergeIntoNotMatchedInsertClause =>\n          val incrInsertedCountExpr = incrementMetricsAndReturnBool(\n            names = Seq(\"numTargetRowsInserted\"),\n            valueToReturn = false)\n          i.resolvedActions.map(_.expr) ++\n            rowIdColumnExpressionOpt.map(_ => Literal(null)) ++\n            rowCommitVersionColumnExpressionOpt.map(_ => Literal(null)) ++\n            Seq(incrInsertedCountExpr) ++\n            (if (cdcEnabled) Some(Literal(CDC_TYPE_INSERT)) else None)\n      }\n      ProcessedClause(clause.condition, actions)\n    }\n  }\n\n  /**\n   * Generate the output expression for each output column to apply the correct action for a type of\n   * merge clause. For each output column, the resulting expression dispatches the correct action\n   * based on all clause conditions.\n   * @param numOutputCols Number of output columns.\n   * @param clauses List of preprocessed merge clauses to bind together.\n   * @param noopCopyExprs The expressions to use for unmatched rows if writeUnmodifiedRows is true.\n   * @param writeUnmodifiedRows Whether to fallback to noopCopyExprs for unmatched rows.\n   * @return A list of one expression per output column to apply for a type of merge clause.\n   */\n  protected def generateClauseOutputExprs(\n      numOutputCols: Int,\n      clauses: Seq[ProcessedClause],\n      noopCopyExprs: Seq[Expression],\n      writeUnmodifiedRows: Boolean\n      ): Seq[Expression] = {\n    val clauseExprs = {\n      if (clauses.isEmpty) {\n        if (writeUnmodifiedRows) {\n          noopCopyExprs\n        } else {\n          // In this case, merge-on-read is enabled *and* there is no action defined for\n          // the MATCHED, NOT MATCHED or NOT MATCHED BY SOURCE cases.\n          // In this case, these expressions will never be evaluated, because\n          // we filtered to rows that match at least one merge clause.\n          // Returning RaiseError here is a sanity check to ensure that the code is correct.\n          val errExpr = RaiseError(\n            Literal(\"Unexpected row: did not match any merge clause\")\n          )\n          Seq.fill(numOutputCols)(errExpr)\n        }\n      } else if (clauses.head.condition.isEmpty) {\n        // Only one clause without any condition, so the corresponding action expressions\n        // can be evaluated directly to generate the output columns.\n        clauses.head.actions\n      } else if (clauses.length == 1) {\n        // Only one clause _with_ a condition, so generate IF/THEN instead of CASE WHEN.\n        // For the i-th output column, generate\n        // IF <condition> THEN <execute i-th expression of action>\n        //                ELSE fallback (noopCopyExprs or RaiseError)\n        val condition = clauses.head.condition.get\n        clauses.head.actions.zipWithIndex.map { case (a, i) =>\n          if (writeUnmodifiedRows) {\n            If(condition, a, noopCopyExprs(i))\n          } else {\n            // Since writeUnmodifiedRows is false, we know we will never reach the else branch\n            // because we filtered to rows that match at least one merge clause.\n            If(condition, a, RaiseError(Literal(\"Unexpected row: did not match any merge clause\")))\n          }\n        }\n      } else {\n        // There are multiple clauses. Use `CaseWhen` to conditionally evaluate the right\n        // action expressions to output columns\n        Seq.range(0, numOutputCols).map { i =>\n          // For the i-th output column, generate\n          // CASE\n          //     WHEN <condition 1> THEN <execute i-th expression of action 1>\n          //     WHEN <condition 2> THEN <execute i-th expression of action 2>\n          //                        ...\n          //                        ELSE fallback (noopCopyExprs or RaiseError)\n          //\n          //\n          val conditionalBranches = clauses.map { precomp =>\n            precomp.condition.getOrElse(Literal.TrueLiteral) -> precomp.actions(i)\n          }\n          val elseBranch =\n            if (writeUnmodifiedRows) Some(noopCopyExprs(i))\n            // Since writeUnmodifiedRows is false, we know we will never reach the else branch\n            // because we filtered to rows that match at least one merge clause.\n            else Some(\n              RaiseError(\n                Literal(\"Unexpected row: did not match any merge clause\")\n              )\n            )\n          CaseWhen(conditionalBranches, elseBranch)\n        }\n      }\n    }\n    // If there are clauses, we should have the correct number of expressions.\n    assert(clauseExprs.size == numOutputCols,\n      s\"incorrect # expressions:\\n\\t\" + seqToString(clauseExprs))\n    logDebug(s\"writeAllChanges: expressions\\n\\t\" + seqToString(clauseExprs))\n    clauseExprs\n  }\n\n  /**\n   * Build the full output as an array of packed rows, then explode into the final result. Based\n   * on the CDC type as originally marked, we produce both rows for the CDC_TYPE_NOT_CDC partition\n   * to be written to the main table and rows for the CDC partitions to be written as CDC files.\n   *\n   * See [[CDCReader]] for general details on how partitioning on the CDC type column works.\n   */\n  protected def generateCdcAndOutputRows(\n      sourceDf: DataFrame,\n      outputCols: Seq[Column],\n      outputColNames: Seq[String],\n      noopCopyExprs: Seq[Expression],\n      rowIdColumnNameOpt: Option[String],\n      rowCommitVersionColumnNameOpt: Option[String],\n      deduplicateDeletes: DeduplicateCDFDeletes,\n      needSetRowTrackingFieldIdForUniform: Boolean): DataFrame = {\n    import org.apache.spark.sql.delta.commands.cdc.CDCReader._\n    // The main partition just needs to swap in the CDC_TYPE_NOT_CDC value.\n    val mainDataOutput =\n      outputCols.dropRight(1) :+ Column(CDC_TYPE_NOT_CDC).as(CDC_TYPE_COLUMN_NAME)\n\n    // Deleted rows are sent to the CDC partition instead of the main partition. These rows are\n    // marked as dropped, we need to retain them while incrementing the original metric column\n    // ourselves.\n    val keepRowAndIncrDeletedCountExpr = !outputCols(outputCols.length - 2)\n    val deleteCdcOutput = outputCols\n      .updated(outputCols.length - 2, keepRowAndIncrDeletedCountExpr.as(ROW_DROPPED_COL))\n\n    // Update preimages need special handling. This is conceptually the same as the\n    // transformation for cdcOutputCols, but we have to transform the noop exprs to columns\n    // ourselves because it hasn't already been done.\n    val cdcNoopExprs = noopCopyExprs.dropRight(2) :+\n      Literal.FalseLiteral :+ Literal(CDC_TYPE_UPDATE_PREIMAGE)\n    val updatePreimageCdcOutput = cdcNoopExprs.zipWithIndex.map {\n      case (e, i) => Column(Alias(e, outputColNames(i))())\n    }\n\n    // To avoid duplicate evaluation of nondeterministic column values such as\n    // [[GenerateIdentityValues]], we EXPLODE CDC rows first, from which we EXPLODE again,\n    // and for each of \"insert\" and \"update_postimage\" rows, generate main data rows.\n    // The first EXPLODE will force evaluation all nondeterministic expressions,\n    // and the second EXPLODE will just copy the generated value from CDC rows\n    // to main data. By doing so we ensure nondeterministic column values in CDC and\n    // main data rows stay the same.\n\n    val cdcTypeCol = outputCols.last\n    val cdcArray = Column(CaseWhen(Seq(\n      EqualNullSafe(expression(cdcTypeCol), Literal(CDC_TYPE_INSERT)) -> expression(array(\n        struct(outputCols: _*))),\n\n      EqualNullSafe(expression(cdcTypeCol), Literal(CDC_TYPE_UPDATE_POSTIMAGE)) -> expression(array(\n        struct(updatePreimageCdcOutput: _*),\n        struct(outputCols: _*))),\n\n      EqualNullSafe(expression(cdcTypeCol), CDC_TYPE_DELETE) -> expression(array(\n        struct(deleteCdcOutput: _*)))),\n\n      // If none of the CDC cases apply (true for purely rewritten target rows, dropped source\n      // rows, etc.) just stick to the normal output.\n      expression(array(struct(mainDataOutput: _*)))\n    ))\n\n    val cdcToMainDataArray = Column(If(\n      Or(\n        EqualNullSafe(expression(col(s\"packedCdc.$CDC_TYPE_COLUMN_NAME\")),\n          Literal(CDC_TYPE_INSERT)),\n        EqualNullSafe(expression(col(s\"packedCdc.$CDC_TYPE_COLUMN_NAME\")),\n          Literal(CDC_TYPE_UPDATE_POSTIMAGE))),\n      expression(array(\n        col(\"packedCdc\"),\n        struct(\n          outputColNames\n            .dropRight(1)\n            .map { n => col(s\"packedCdc.`$n`\") }\n            :+ Column(CDC_TYPE_NOT_CDC).as(CDC_TYPE_COLUMN_NAME): _*)\n      )),\n      expression(array(col(\"packedCdc\")))\n    ))\n\n    if (deduplicateDeletes.enabled) {\n      deduplicateCDFDeletes(\n        deduplicateDeletes,\n        sourceDf,\n        cdcArray,\n        cdcToMainDataArray,\n        rowIdColumnNameOpt,\n        rowCommitVersionColumnNameOpt,\n        outputColNames,\n        needSetRowTrackingFieldIdForUniform\n      )\n    } else {\n      packAndExplodeCDCOutput(\n        sourceDf,\n        cdcArray,\n        cdcToMainDataArray,\n        rowIdColumnNameOpt,\n        rowCommitVersionColumnNameOpt,\n        outputColNames,\n        dedupColumns = Nil,\n        needSetRowTrackingFieldIdForUniform)\n    }\n  }\n\n  /**\n   * Applies the transformations to generate the CDC output:\n   *  1. Transform each input row into its corresponding array of CDC rows, e.g. an updated row\n   *     yields: array(update_preimage, update_postimage).\n   *  2. Add the main data output for inserted/updated rows to the previously packed CDC data.\n   *  3. Explode the result to flatten the packed arrays.\n   *\n   * @param sourceDf The dataframe generated after processing the merge output.\n   * @param cdcArray Transforms the merge output into the corresponding packed CDC data that will be\n   *                 written to the CDC partition.\n   * @param cdcToMainDataArray Transforms the packed CDC data to add the main data output, i.e. rows\n   *                           that are inserted or updated and will be written to the main\n   *                           partition.\n   * @param rowIdColumnNameOpt The optional Row ID preservation column with the physical Row ID\n   *                           name, it stores stable Row IDs.\n   * @param rowCommitVersionColumnNameOpt The optional Row Commit Version preservation column\n   *                                      with the physical Row Commit Version name, it stores\n   *                                      stable Row Commit Versions.\n   * @param outputColNames All the main and CDC columns to use in the output.\n   * @param dedupColumns Additional columns to add to enable deduplication.\n   */\n  private def packAndExplodeCDCOutput(\n      sourceDf: DataFrame,\n      cdcArray: Column,\n      cdcToMainDataArray: Column,\n      rowIdColumnNameOpt: Option[String],\n      rowCommitVersionColumnNameOpt: Option[String],\n      outputColNames: Seq[String],\n      dedupColumns: Seq[Column],\n      needSetRowTrackingFieldIdForUniform: Boolean): DataFrame = {\n    val unpackedCols = outputColNames.map { name =>\n      if (rowIdColumnNameOpt.contains(name)) {\n        // Add metadata to allow writing the column although it is not part of the schema.\n        col(s\"packedData.`$name`\").as(name,\n          RowId.columnMetadata(name, needSetRowTrackingFieldIdForUniform))\n      } else if (rowCommitVersionColumnNameOpt.contains(name)) {\n        col(s\"packedData.`$name`\").as(name,\n          RowCommitVersion.columnMetadata(name, needSetRowTrackingFieldIdForUniform))\n      } else {\n        col(s\"packedData.`$name`\").as(name)\n      }\n    }\n\n    sourceDf\n      // `explode()` creates a [[Generator]] which can't handle non-deterministic expressions that\n      // we use to increment metric counters. We first project the CDC array so that the expressions\n      // are evaluated before we explode the array.\n      .select(cdcArray.as(\"projectedCDC\") +: dedupColumns: _*)\n      .select(explode(col(\"projectedCDC\")).as(\"packedCdc\") +: dedupColumns: _*)\n      .select(explode(cdcToMainDataArray).as(\"packedData\") +: dedupColumns: _*)\n      .select(unpackedCols ++ dedupColumns: _*)\n  }\n\n  /**\n   * This method deduplicates CDF deletes where a target row has potentially multiple matches. It\n   * assumes that the input dataframe contains the [[TARGET_ROW_INDEX_COL]] and\n   * to detect inserts the [[SOURCE_ROW_INDEX_COL]] column to track the origin of the row.\n   *\n   * All duplicates of deleted rows have the same [[TARGET_ROW_INDEX_COL]] and\n   * [[CDC_TYPE_COLUMN_NAME]] therefore we use both columns as compound deduplication key.\n   * In case the input data frame contains additional insert rows we leave them untouched by using\n   * the [[SOURCE_ROW_INDEX_COL]] to fill the null values of the [[TARGET_ROW_INDEX_COL]]. This\n   * may lead to duplicates as part of the final row index but this is not a problem since if\n   * an insert and a delete have the same [[TARGET_ROW_INDEX_COL]] they definitely have a\n   * different [[CDC_TYPE_COLUMN_NAME]].\n   */\n  private def deduplicateCDFDeletes(\n      deduplicateDeletes: DeduplicateCDFDeletes,\n      df: DataFrame,\n      cdcArray: Column,\n      cdcToMainDataArray: Column,\n      rowIdColumnNameOpt: Option[String],\n      rowCommitVersionColumnNameOpt: Option[String],\n      outputColNames: Seq[String],\n      needSetRowTrackingFieldIdForUniform: Boolean): DataFrame = {\n    val dedupColumns = if (deduplicateDeletes.includesInserts) {\n      Seq(col(TARGET_ROW_INDEX_COL), col(SOURCE_ROW_INDEX_COL))\n    } else {\n      Seq(col(TARGET_ROW_INDEX_COL))\n    }\n\n    val cdcDf = packAndExplodeCDCOutput(\n      df,\n      cdcArray,\n      cdcToMainDataArray,\n      rowIdColumnNameOpt,\n      rowCommitVersionColumnNameOpt,\n      outputColNames,\n      dedupColumns,\n      needSetRowTrackingFieldIdForUniform\n    )\n\n    val cdcDfWithIncreasingIds = if (deduplicateDeletes.includesInserts) {\n      cdcDf.withColumn(\n        TARGET_ROW_INDEX_COL,\n        coalesce(col(TARGET_ROW_INDEX_COL), col(SOURCE_ROW_INDEX_COL)))\n    } else {\n      cdcDf\n    }\n    cdcDfWithIncreasingIds\n      .dropDuplicates(TARGET_ROW_INDEX_COL, CDC_TYPE_COLUMN_NAME)\n      .drop(TARGET_ROW_INDEX_COL, SOURCE_ROW_INDEX_COL)\n  }\n}\n\n/**\n * This class enables and configures the deduplication of CDF deletes in case the merge statement\n * contains an unconditional delete statement that matches multiple target rows.\n *\n * @param enabled CDF generation should be enabled and duplicate target matches are detected\n * @param includesInserts in addition to the unconditional deletes the merge also inserts rows\n */\ncase class DeduplicateCDFDeletes(\n  enabled: Boolean,\n  includesInserts: Boolean)\n\nobject MergeOutputGeneration {\n  final val TARGET_ROW_INDEX_COL = \"_target_row_index_\"\n  final val SOURCE_ROW_INDEX_COL = \"_source_row_index\"\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/merge/MergeStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.merge\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.NumRecordsStats\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.commons.lang3.StringUtils\n\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.logical.{DeltaMergeIntoClause, DeltaMergeIntoMatchedClause, DeltaMergeIntoNotMatchedBySourceClause, DeltaMergeIntoNotMatchedClause}\nimport org.apache.spark.sql.execution.metric.SQLMetric\n\ncase class MergeDataSizes(\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  rows: Option[Long] = None,\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  files: Option[Long] = None,\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  bytes: Option[Long] = None,\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  partitions: Option[Long] = None)\n\n/**\n * Represents the state of a single merge clause:\n * - merge clause's (optional) predicate\n * - action type (insert, update, delete)\n * - action's expressions\n */\ncase class MergeClauseStats(\n    condition: Option[String],\n    actionType: String,\n    actionExpr: Seq[String])\n\nobject MergeClauseStats {\n  def apply(mergeClause: DeltaMergeIntoClause): MergeClauseStats = {\n    MergeClauseStats(\n      condition = mergeClause.condition.map(c => StringUtils.abbreviate(c.sql, 256)),\n      mergeClause.clauseType.toLowerCase(),\n      actionExpr = truncateSeq(\n        mergeClause.actions.map(a => StringUtils.abbreviate(a.sql, 256)),\n        maxLength = 512)\n    )\n  }\n\n  /**\n   * Truncate a list of items to be serialized to around 'maxLength' characters.\n   * Always include at least on item.\n   */\n  private def truncateSeq(seq: Seq[String], maxLength: Long): Seq[String] = {\n    val buffer = ArrayBuffer.empty[String]\n    var length = 0L\n    for (x <- seq if length + x.length <= maxLength || buffer.isEmpty) {\n      length += x.length + 3 // quotes and comma\n      buffer.append(x)\n    }\n    val numTruncatedItems = seq.length - buffer.length\n    if (numTruncatedItems > 0) {\n      buffer.append(\"... \" + numTruncatedItems + \" more fields\")\n    }\n    buffer.toSeq\n  }\n}\n\n/** State for a merge operation */\ncase class MergeStats(\n    // Merge condition expression\n    conditionExpr: String,\n\n    // Expressions used in old MERGE stats, now always Null\n    updateConditionExpr: String,\n    updateExprs: Seq[String],\n    insertConditionExpr: String,\n    insertExprs: Seq[String],\n    deleteConditionExpr: String,\n\n    // Newer expressions used in MERGE with any number of MATCHED/NOT MATCHED/NOT MATCHED BY SOURCE\n    matchedStats: Seq[MergeClauseStats],\n    notMatchedStats: Seq[MergeClauseStats],\n    notMatchedBySourceStats: Seq[MergeClauseStats],\n\n    // Timings\n    executionTimeMs: Long,\n    materializeSourceTimeMs: Long,\n    scanTimeMs: Long,\n    rewriteTimeMs: Long,\n\n    // Data sizes of source and target at different stages of processing\n    source: MergeDataSizes,\n    targetBeforeSkipping: MergeDataSizes,\n    targetAfterSkipping: MergeDataSizes,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    sourceRowsInSecondScan: Option[Long],\n\n    // Data change sizes\n    targetFilesRemoved: Long,\n    targetFilesAdded: Long,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    targetChangeFilesAdded: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    targetChangeFileBytes: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    targetBytesRemoved: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    targetBytesAdded: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    targetPartitionsRemovedFrom: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    targetPartitionsAddedTo: Option[Long],\n    targetRowsCopied: Long,\n    targetRowsUpdated: Long,\n    targetRowsMatchedUpdated: Long,\n    targetRowsNotMatchedBySourceUpdated: Long,\n    targetRowsInserted: Long,\n    targetRowsDeleted: Long,\n    targetRowsMatchedDeleted: Long,\n    targetRowsNotMatchedBySourceDeleted: Long,\n    numTargetDeletionVectorsAdded: Long,\n    numTargetDeletionVectorsRemoved: Long,\n    numTargetDeletionVectorsUpdated: Long,\n\n    // MergeMaterializeSource stats\n    materializeSourceReason: Option[String] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    materializeSourceAttempts: Option[Long] = None,\n\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numLogicalRecordsAdded: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    numLogicalRecordsRemoved: Option[Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    commitVersion: Option[Long] = None\n)\n\nobject MergeStats {\n\n  def fromMergeSQLMetrics(\n      metrics: Map[String, SQLMetric],\n      condition: Expression,\n      matchedClauses: Seq[DeltaMergeIntoMatchedClause],\n      notMatchedClauses: Seq[DeltaMergeIntoNotMatchedClause],\n      notMatchedBySourceClauses: Seq[DeltaMergeIntoNotMatchedBySourceClause],\n      isPartitioned: Boolean,\n      performedSecondSourceScan: Boolean,\n      commitVersion: Option[Long],\n      numRecordsStats: NumRecordsStats\n    ): MergeStats = {\n\n    def metricValueIfPartitioned(metricName: String): Option[Long] = {\n      if (isPartitioned) Some(metrics(metricName).value) else None\n    }\n\n    MergeStats(\n      // Merge condition expression\n      conditionExpr = StringUtils.abbreviate(condition.sql, 2048),\n\n      // Newer expressions used in MERGE with any number of MATCHED/NOT MATCHED/\n      // NOT MATCHED BY SOURCE\n      matchedStats = matchedClauses.map(MergeClauseStats(_)),\n      notMatchedStats = notMatchedClauses.map(MergeClauseStats(_)),\n      notMatchedBySourceStats = notMatchedBySourceClauses.map(MergeClauseStats(_)),\n\n      // Timings\n      executionTimeMs = metrics(\"executionTimeMs\").value,\n      materializeSourceTimeMs = metrics(\"materializeSourceTimeMs\").value,\n      scanTimeMs = metrics(\"scanTimeMs\").value,\n      rewriteTimeMs = metrics(\"rewriteTimeMs\").value,\n\n      // Data sizes of source and target at different stages of processing\n      source = MergeDataSizes(rows = Some(metrics(\"numSourceRows\").value)),\n      targetBeforeSkipping =\n        MergeDataSizes(\n          files = Some(metrics(\"numTargetFilesBeforeSkipping\").value),\n          bytes = Some(metrics(\"numTargetBytesBeforeSkipping\").value)),\n      targetAfterSkipping =\n        MergeDataSizes(\n          files = Some(metrics(\"numTargetFilesAfterSkipping\").value),\n          bytes = Some(metrics(\"numTargetBytesAfterSkipping\").value),\n          partitions = metricValueIfPartitioned(\"numTargetPartitionsAfterSkipping\")),\n      sourceRowsInSecondScan =\n        Option.when(performedSecondSourceScan)(metrics(\"numSourceRowsInSecondScan\").value),\n\n      // Data change sizes\n      targetFilesAdded = metrics(\"numTargetFilesAdded\").value,\n      targetChangeFilesAdded = metrics.get(\"numTargetChangeFilesAdded\").map(_.value),\n      targetChangeFileBytes = metrics.get(\"numTargetChangeFileBytes\").map(_.value),\n      targetFilesRemoved = metrics(\"numTargetFilesRemoved\").value,\n      targetBytesAdded = Some(metrics(\"numTargetBytesAdded\").value),\n      targetBytesRemoved = Some(metrics(\"numTargetBytesRemoved\").value),\n      targetPartitionsRemovedFrom = metricValueIfPartitioned(\"numTargetPartitionsRemovedFrom\"),\n      targetPartitionsAddedTo = metricValueIfPartitioned(\"numTargetPartitionsAddedTo\"),\n      targetRowsCopied = metrics(\"numTargetRowsCopied\").value,\n      targetRowsUpdated = metrics(\"numTargetRowsUpdated\").value,\n      targetRowsMatchedUpdated = metrics(\"numTargetRowsMatchedUpdated\").value,\n      targetRowsNotMatchedBySourceUpdated = metrics(\"numTargetRowsNotMatchedBySourceUpdated\").value,\n      targetRowsInserted = metrics(\"numTargetRowsInserted\").value,\n      targetRowsDeleted = metrics(\"numTargetRowsDeleted\").value,\n      targetRowsMatchedDeleted = metrics(\"numTargetRowsMatchedDeleted\").value,\n      targetRowsNotMatchedBySourceDeleted = metrics(\"numTargetRowsNotMatchedBySourceDeleted\").value,\n\n      // Deletion Vector metrics.\n      numTargetDeletionVectorsAdded = metrics(\"numTargetDeletionVectorsAdded\").value,\n      numTargetDeletionVectorsRemoved = metrics(\"numTargetDeletionVectorsRemoved\").value,\n      numTargetDeletionVectorsUpdated = metrics(\"numTargetDeletionVectorsUpdated\").value,\n\n      commitVersion = commitVersion,\n      numLogicalRecordsAdded = numRecordsStats.numLogicalRecordsAdded,\n      numLogicalRecordsRemoved = numRecordsStats.numLogicalRecordsRemoved,\n\n      // Deprecated fields\n      updateConditionExpr = null,\n      updateExprs = null,\n      insertConditionExpr = null,\n      insertExprs = null,\n      deleteConditionExpr = null)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/optimize/AddFileWithNumRecords.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.optimize\n\nimport org.apache.spark.sql.delta.actions.AddFile\n\n/**\n * Wrapper over an [AddFile] and its stats:\n * @param numPhysicalRecords The number of records physically present in the file.\n *                           Equivalent to `addFile.numTotalRecords`.\n * @param numLogicalRecords The physical number of records minus the Deletion Vector cardinality.\n *                          Equivalent to `addFile.numRecords`.\n */\ncase class AddFileWithNumRecords(\n  addFile: AddFile,\n  numPhysicalRecords: java.lang.Long,\n  numLogicalRecords: java.lang.Long) {\n\n  /** Returns the approx size of the remaining records after excluding the deleted ones. */\n  def estLogicalFileSize: Long = {\n    (addFile.size * logicalToPhysicalRecordsRatio).toLong\n  }\n\n  /** Returns the ratio of the logical number of records to the total number of records. */\n  def logicalToPhysicalRecordsRatio: Double = {\n    if (numLogicalRecords == null || numPhysicalRecords == null || numPhysicalRecords == 0) {\n      1.0d\n    } else {\n      numLogicalRecords.toDouble / numPhysicalRecords.toDouble\n    }\n  }\n\n  /** Returns the ratio of the deleted number of records to the total number of records */\n  def deletedToPhysicalRecordsRatio: Double = {\n    1.0d - logicalToPhysicalRecordsRatio\n  }\n}\n\nobject AddFileWithNumRecords {\n  def createFromFile(file: AddFile): AddFileWithNumRecords = {\n    val numPhysicalRecords = file.numPhysicalRecords.getOrElse(0L)\n    val numLogicalRecords = file.numLogicalRecords.getOrElse(0L)\n    AddFileWithNumRecords(file, numPhysicalRecords, numLogicalRecords)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/optimize/OptimizeStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.optimize\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteringStats\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction, RemoveFile}\n\n// scalastyle:off import.ordering.noEmptyLine\n\n/**\n * Stats for an OPTIMIZE operation accumulated across all batches.\n */\ncase class OptimizeStats(\n    var addedFilesSizeStats: FileSizeStats = FileSizeStats(),\n    var removedFilesSizeStats: FileSizeStats = FileSizeStats(),\n    var numPartitionsOptimized: Long = 0,\n    var zOrderStats: Option[ZOrderStats] = None,\n    var clusteringStats: Option[ClusteringStats] = None,\n    var numBins: Long = 0,\n    var numBatches: Long = 0,\n    var totalConsideredFiles: Long = 0,\n    var totalFilesSkipped: Long = 0,\n    var preserveInsertionOrder: Boolean = false,\n    var numFilesSkippedToReduceWriteAmplification: Long = 0,\n    var numBytesSkippedToReduceWriteAmplification: Long = 0,\n    startTimeMs: Long = System.currentTimeMillis(),\n    var endTimeMs: Long = 0,\n    var totalClusterParallelism: Long = 0,\n    var totalScheduledTasks: Long = 0,\n    var deletionVectorStats: Option[DeletionVectorStats] = None,\n    var numTableColumns: Long = 0,\n    var numTableColumnsWithStats: Long = 0,\n    var autoCompactParallelismStats: AutoCompactParallelismStats = AutoCompactParallelismStats()) {\n\n  def toOptimizeMetrics: OptimizeMetrics = {\n    OptimizeMetrics(\n      numFilesAdded = addedFilesSizeStats.totalFiles,\n      numFilesRemoved = removedFilesSizeStats.totalFiles,\n      filesAdded = addedFilesSizeStats.toFileSizeMetrics,\n      filesRemoved = removedFilesSizeStats.toFileSizeMetrics,\n      partitionsOptimized = numPartitionsOptimized,\n      zOrderStats = zOrderStats,\n      clusteringStats = clusteringStats,\n      numBins = numBins,\n      numBatches = numBatches,\n      totalConsideredFiles = totalConsideredFiles,\n      totalFilesSkipped = totalFilesSkipped,\n      preserveInsertionOrder = preserveInsertionOrder,\n      numFilesSkippedToReduceWriteAmplification = numFilesSkippedToReduceWriteAmplification,\n      numBytesSkippedToReduceWriteAmplification = numBytesSkippedToReduceWriteAmplification,\n      startTimeMs = startTimeMs,\n      endTimeMs = endTimeMs,\n      totalClusterParallelism = totalClusterParallelism,\n      totalScheduledTasks = totalScheduledTasks,\n      deletionVectorStats = deletionVectorStats,\n      numTableColumns = numTableColumns,\n      numTableColumnsWithStats = numTableColumnsWithStats,\n      autoCompactParallelismStats = autoCompactParallelismStats.toMetrics)\n  }\n}\n\n/**\n * This statistics class keeps tracking the parallelism usage of Auto Compaction.\n * It collects following metrics:\n *   -- the min/max parallelism among the whole cluster are used for Auto Compact,\n *   -- the min/max parallelism occupied by current Auto Compact session,\n */\ncase class AutoCompactParallelismStats(\n    var maxClusterUsedParallelism: Long = 0,\n    var minClusterUsedParallelism: Long = 0,\n    var maxSessionUsedParallelism: Long = 0,\n    var minSessionUsedParallelism: Long = 0) {\n  def toMetrics: Option[ParallelismMetrics] = {\n    if (maxSessionUsedParallelism == 0) {\n      return None\n    }\n    Some(ParallelismMetrics(\n      Some(maxClusterUsedParallelism),\n      Some(minClusterUsedParallelism),\n      Some(maxSessionUsedParallelism),\n      Some(minSessionUsedParallelism)))\n  }\n\n  /** Update the statistics of parallelism of current Auto Compact command. */\n  def update(clusterUsedParallelism: Long, sessionUsedParallelism: Long): Unit = {\n    maxClusterUsedParallelism = Math.max(maxClusterUsedParallelism, clusterUsedParallelism)\n    minClusterUsedParallelism = if (minClusterUsedParallelism == 0) {\n        clusterUsedParallelism\n      } else {\n        Math.min(minClusterUsedParallelism, clusterUsedParallelism)\n      }\n    maxSessionUsedParallelism = Math.max(maxSessionUsedParallelism, sessionUsedParallelism)\n    minSessionUsedParallelism = if (minSessionUsedParallelism == 0) {\n        sessionUsedParallelism\n      } else {\n        Math.min(minSessionUsedParallelism, sessionUsedParallelism)\n      }\n  }\n}\n\ncase class FileSizeStats(\n    var minFileSize: Long = 0,\n    var maxFileSize: Long = 0,\n    var totalFiles: Long = 0,\n    var totalSize: Long = 0) {\n\n  def avgFileSize: Double = if (totalFiles > 0) {\n      totalSize * 1.0 / totalFiles\n    } else {\n      0.0\n    }\n\n  def merge(candidateFiles: Seq[FileAction]): Unit = {\n    if (totalFiles == 0 && candidateFiles.nonEmpty) {\n      minFileSize = Long.MaxValue\n      maxFileSize = Long.MinValue\n    }\n    candidateFiles.foreach { file =>\n      val fileSize = file match {\n        case addFile: AddFile => addFile.size\n        case removeFile: RemoveFile => removeFile.size.getOrElse(0L)\n        case default =>\n          throw new IllegalArgumentException(s\"Unknown FileAction type: ${default.getClass}\")\n      }\n      minFileSize = math.min(fileSize, minFileSize)\n      maxFileSize = math.max(fileSize, maxFileSize)\n      totalSize += fileSize\n    }\n    totalFiles += candidateFiles.length\n  }\n\n\n  def toFileSizeMetrics: FileSizeMetrics = {\n    if (totalFiles == 0) {\n      return FileSizeMetrics(min = None, max = None, avg = 0, totalFiles = 0, totalSize = 0)\n    }\n    FileSizeMetrics(\n      min = Some(minFileSize),\n      max = Some(maxFileSize),\n      avg = avgFileSize,\n      totalFiles = totalFiles,\n      totalSize = totalSize)\n  }\n}\n/**\n * Percentiles on the file sizes in this batch.\n * @param min Size of the smallest file\n * @param p25 Size of the 25th percentile file\n * @param p50 Size of the 50th percentile file\n * @param p75 Size of the 75th percentile file\n * @param max Size of the largest file\n */\ncase class FileSizeStatsWithHistogram(\n     min: Long,\n     p25: Long,\n     p50: Long,\n     p75: Long,\n     max: Long)\n\nobject FileSizeStatsWithHistogram {\n\n  /**\n   * Creates a [[FileSizeStatsWithHistogram]] based on the passed sorted file sizes\n   * @return Some(fileSizeStatsWithHistogram) if sizes are non-empty, else returns None\n   */\n  def create(sizes: Seq[Long]): Option[FileSizeStatsWithHistogram] = {\n    if (sizes.isEmpty) {\n      return None\n    }\n    val count = sizes.length\n    Some(FileSizeStatsWithHistogram(\n      min = sizes.head,\n      // we do not need to ceil the computed index as arrays start at 0\n      p25 = sizes(count / 4),\n      p50 = sizes(count / 2),\n      p75 = sizes(count * 3 / 4),\n      max = sizes.last))\n  }\n}\n\n/**\n * Metrics returned by the optimize command.\n *\n * @param numFilesAdded number of files added by optimize\n * @param numFilesRemoved number of files removed by optimize\n * @param filesAdded Stats for the files added\n * @param filesRemoved Stats for the files removed\n * @param partitionsOptimized Number of partitions optimized\n * @param zOrderStats Z-Order stats\n * @param clusteringStats Clustering stats\n * @param numBins Number of bins\n * @param numBatches Number of batches\n * @param totalConsideredFiles Number of files considered for the Optimize operation.\n * @param totalFilesSkipped Number of files that are skipped from being Optimized.\n * @param preserveInsertionOrder If optimize was run with insertion preservation enabled.\n * @param numFilesSkippedToReduceWriteAmplification Number of files skipped for reducing write\n *                                                  amplification.\n * @param numBytesSkippedToReduceWriteAmplification Number of bytes skipped for reducing write\n *                                                  amplification.\n * @param startTimeMs The start time of Optimize command.\n * @param endTimeMs The end time of Optimize command.\n * @param totalClusterParallelism The total number of parallelism of this cluster.\n * @param totalScheduledTasks The total number of optimize task scheduled.\n * @param autoCompactParallelismStats The metrics of cluster and session parallelism.\n * @param deletionVectorStats Statistics related with Deletion Vectors.\n * @param numTableColumns Number of columns in the table.\n * @param numTableColumnsWithStats Number of table columns to collect data skipping stats.\n */\ncase class OptimizeMetrics(\n    numFilesAdded: Long,\n    numFilesRemoved: Long,\n    filesAdded: FileSizeMetrics =\n      FileSizeMetrics(min = None, max = None, avg = 0, totalFiles = 0, totalSize = 0),\n    filesRemoved: FileSizeMetrics =\n      FileSizeMetrics(min = None, max = None, avg = 0, totalFiles = 0, totalSize = 0),\n    partitionsOptimized: Long = 0,\n    zOrderStats: Option[ZOrderStats] = None,\n    clusteringStats: Option[ClusteringStats] = None,\n    numBins: Long,\n    numBatches: Long,\n    totalConsideredFiles: Long,\n    totalFilesSkipped: Long = 0,\n    preserveInsertionOrder: Boolean = false,\n    numFilesSkippedToReduceWriteAmplification: Long = 0,\n    numBytesSkippedToReduceWriteAmplification: Long = 0,\n    startTimeMs: Long = 0,\n    endTimeMs: Long = 0,\n    totalClusterParallelism: Long = 0,\n    totalScheduledTasks: Long = 0,\n    autoCompactParallelismStats: Option[ParallelismMetrics] = None,\n    deletionVectorStats: Option[DeletionVectorStats] = None,\n    numTableColumns: Long = 0,\n    numTableColumnsWithStats: Long = 0\n  )\n\n/**\n * Basic Stats on file sizes.\n *\n * @param min Minimum file size\n * @param max Maximum file size\n * @param avg Average of the file size\n * @param totalFiles Total number of files\n * @param totalSize Total size of the files\n */\ncase class FileSizeMetrics(\n    min: Option[Long],\n    max: Option[Long],\n    avg: Double,\n    totalFiles: Long,\n    totalSize: Long)\n\n/**\n * This statistics contains following metrics:\n *   -- the min/max parallelism among the whole cluster are used,\n *   -- the min/max parallelism occupied by current session,\n */\ncase class ParallelismMetrics(\n     maxClusterActiveParallelism: Option[Long] = None,\n     minClusterActiveParallelism: Option[Long] = None,\n     maxSessionActiveParallelism: Option[Long] = None,\n     minSessionActiveParallelism: Option[Long] = None)\n\n/**\n * Accumulator for statistics related with Deletion Vectors.\n * Note that this case class contains mutable variables and cannot be used in places where immutable\n * case classes can be used (e.g. map/set keys).\n */\ncase class DeletionVectorStats(\n  var numDeletionVectorsRemoved: Long = 0,\n  var numDeletionVectorRowsRemoved: Long = 0)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/optimize/ZCubeFileStatsCollector.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.optimize\n\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo.ZCubeID\n\n/**\n * ZCube file statistics collector. An object of this class can be used to collect ZCube statistics.\n * The file statistics collection can be started by initializing an object of this class and\n * calling updateStats on every new file seen.\n * The number of ZCubes, number of files from matching cubes and number of unoptimized files are\n * captured here.\n *\n * @param zOrderBy zOrder or clustering columns.\n * @param isFull whether OPTIMIZE FULL is run. This is only for clustered tables.\n */\nclass ZCubeFileStatsCollector(zOrderBy: Seq[String], isFull: Boolean) {\n\n  /** map that holds the file statistics Map(\"element\" -> (number of files, total file size)) */\n  private var processedZCube: ZCubeID = _\n\n  /** number of distinct zCubes seen so far */\n  var numZCubes = 0\n\n  private var matchingCubeCnt = 0\n  private var matchingCubeSize = 0L\n  private var otherFilesCnt = 0\n  private var otherFilesSize = 0L\n  def fileStats: Map[String, (Int, Long)] = Map(\n    \"matchingCube\" -> ((matchingCubeCnt, matchingCubeSize)),\n    \"otherFiles\" -> ((otherFilesCnt, otherFilesSize))\n  )\n\n  /** method to update the zCubeFileStats incrementally by file */\n  def updateStats(file: AddFile): AddFile = {\n    val zCubeInfo = ZCubeInfo.getForFile(file)\n    // Note that clustered files with different clustering columns are considered candidate\n    // files when isFull is set.\n    if (zCubeInfo.isDefined && (isFull || zCubeInfo.get.zOrderBy == zOrderBy)) {\n      if (processedZCube != zCubeInfo.get.zCubeID) {\n        processedZCube = zCubeInfo.get.zCubeID\n        numZCubes += 1\n      }\n      matchingCubeCnt += 1\n      matchingCubeSize += file.size\n    } else {\n      otherFilesCnt += 1\n      otherFilesSize += file.size\n    }\n    file\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/commands/optimize/ZOrderMetrics.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.optimize\n\n/**\n * A class to create blob structure for zorder metrics and events.\n */\nclass ZOrderMetrics(zOrderBy: Seq[String]) {\n\n  var strategyName: String = _\n  val inputStats = new ZCubeFileStatsCollector(zOrderBy, isFull = false)\n  val outputStats = new ZCubeFileStatsCollector(zOrderBy, isFull = false)\n  var numOutputCubes = 0\n\n  def getZOrderStats(): ZOrderStats = {\n    ZOrderStats(\n      strategyName,\n      inputNumCubes = inputStats.numZCubes,\n      inputCubeFiles = ZOrderFileStats(inputStats.fileStats.get(\"matchingCube\")),\n      inputOtherFiles = ZOrderFileStats(inputStats.fileStats.get(\"otherFiles\")),\n      mergedFiles = ZOrderFileStats(outputStats.fileStats.values),\n      numOutputCubes = numOutputCubes\n    )\n  }\n}\n\n/**\n * Aggregated file stats for a category of ZCube files.\n * @param num Total number of files.\n * @param size Total size of files in bytes.\n */\ncase class ZOrderFileStats(num: Long, size: Long)\n\nobject ZOrderFileStats {\n  def apply(v: Iterable[(Int, Long)]): ZOrderFileStats = {\n    v.foldLeft(ZOrderFileStats(0, 0)) { (a, b) =>\n      ZOrderFileStats(a.num + b._1, a.size + b._2)\n    }\n  }\n}\n\n/**\n * Aggregated stats for OPTIMIZE ZORDERBY command.\n * This is a public facing API, consider any change carefully.\n *\n * @param strategyName ZCubeMergeStrategy used.\n * @param inputCubeFiles Files in the ZCube matching the current OPTIMIZE operation.\n * @param inputOtherFiles Files not in any ZCube or in other ZCube orderings.\n * @param inputNumCubes Number of different cubes among input files.\n * @param mergedFiles Subset of input files merged by the current operation\n * @param numOutputCubes Number of output ZCubes written out\n * @param mergedNumCubes Number of different cubes among merged files.\n */\ncase class ZOrderStats(\n  strategyName: String,\n  inputCubeFiles: ZOrderFileStats,\n  inputOtherFiles: ZOrderFileStats,\n  inputNumCubes: Long,\n  mergedFiles: ZOrderFileStats,\n  numOutputCubes: Long,\n  mergedNumCubes: Option[Long] = None\n)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/CharVarcharConstraint.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.util.CharVarcharUtils\nimport org.apache.spark.sql.types._\n\n// Delta implements char/varchar length check with CONSTRAINTS, and needs to generate predicate\n// expression which is different from the OSS version.\nobject CharVarcharConstraint {\n  final val INVARIANT_NAME = \"__CHAR_VARCHAR_STRING_LENGTH_CHECK__\"\n\n  def stringConstraints(schema: StructType): Seq[Constraint] = {\n    schema.flatMap { f =>\n      val targetType = CharVarcharUtils.getRawType(f.metadata).getOrElse(f.dataType)\n      val col = UnresolvedAttribute(Seq(f.name))\n      checkStringLength(col, targetType).map { lengthCheckExpr =>\n        Constraints.Check(INVARIANT_NAME, lengthCheckExpr)\n      }\n    }\n  }\n\n  private def checkStringLength(expr: Expression, dt: DataType): Option[Expression] = dt match {\n    case v: VarcharType =>\n      Some(Or(IsNull(expr), LessThanOrEqual(Length(expr), Literal(v.length))))\n\n    case c: CharType =>\n      checkStringLength(expr, VarcharType(c.length))\n\n    case StructType(fields) =>\n      fields.zipWithIndex.flatMap { case (f, i) =>\n        checkStringLength(GetStructField(expr, i, Some(f.name)), f.dataType)\n      }.reduceOption(And(_, _))\n\n    case ArrayType(et, containsNull) =>\n      checkStringLengthInArray(expr, et, containsNull)\n\n    case MapType(kt, vt, valueContainsNull) =>\n      (checkStringLengthInArray(MapKeys(expr), kt, false) ++\n        checkStringLengthInArray(MapValues(expr), vt, valueContainsNull))\n        .reduceOption(And(_, _))\n\n    case _ => None\n  }\n\n  private def checkStringLengthInArray(\n      arr: Expression, et: DataType, containsNull: Boolean): Option[Expression] = {\n    val cleanedType = CharVarcharUtils.replaceCharVarcharWithString(et)\n    val param = NamedLambdaVariable(\"x\", cleanedType, containsNull)\n    checkStringLength(param, et).map { checkExpr =>\n      Or(IsNull(arr), ArrayForAll(arr, LambdaFunction(checkExpr, Seq(param))))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/CheckDeltaInvariant.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport org.apache.spark.sql.delta.constraints.Constraints.{Check, NotNull}\nimport org.apache.spark.sql.delta.schema.DeltaInvariantViolationException\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{Expression, NonSQLExpression}\nimport org.apache.spark.sql.catalyst.expressions.codegen._\nimport org.apache.spark.sql.catalyst.expressions.codegen.Block._\nimport org.apache.spark.sql.types.{DataType, NullType}\n\n/**\n * An expression that validates a specific invariant on a column, before writing into Delta.\n *\n * @param child The fully resolved expression to be evaluated to check the constraint.\n * @param columnExtractors Extractors for each referenced column. Used to generate readable errors.\n * @param constraint The original constraint definition.\n */\ncase class CheckDeltaInvariant(\n    child: Expression,\n    columnExtractors: Seq[(String, Expression)],\n    constraint: Constraint)\n  extends Expression with NonSQLExpression with CodegenFallback {\n\n  override def children: Seq[Expression] = child +: columnExtractors.map(_._2)\n  override def dataType: DataType = NullType\n  override def foldable: Boolean = false\n  override def nullable: Boolean = true\n\n  private def assertRule(input: InternalRow): Unit = constraint match {\n    case n: NotNull =>\n      if (child.eval(input) == null) {\n        throw DeltaInvariantViolationException(n)\n      }\n    case c: Check =>\n      val result = child.eval(input)\n      if (result == null || result == false) {\n        throw DeltaInvariantViolationException(\n          c,\n          columnExtractors.map {\n            case (column, extractor) => column -> extractor.eval(input)\n          }.toMap\n        )\n      }\n  }\n\n  override def eval(input: InternalRow): Any = {\n    assertRule(input)\n    null\n  }\n\n  private def generateNotNullCode(ctx: CodegenContext): Block = {\n    val childGen = child.genCode(ctx)\n    val invariantField = ctx.addReferenceObj(\"errMsg\", constraint)\n    code\"\"\"${childGen.code}\n       |\n       |if (${childGen.isNull}) {\n       |  throw org.apache.spark.sql.delta.schema.DeltaInvariantViolationException.apply(\n       |    $invariantField);\n       |}\n     \"\"\".stripMargin\n  }\n\n  /**\n   * Generate the code to extract values for the columns referenced in a violated CHECK constraint.\n   * We build parallel lists of full column names and their extracted values in the row which\n   * violates the constraint, to be passed to the [[InvariantViolationException]] constructor\n   * in [[generateExpressionValidationCode()]].\n   *\n   * Note that this code is a bit expensive, so it shouldn't be run until we already\n   * know the constraint has been violated.\n   */\n  private def generateColumnValuesCode(\n      colList: String, valList: String, ctx: CodegenContext): Block = {\n    val start =\n      code\"\"\"\n        |java.util.List<String> $colList = new java.util.ArrayList<String>();\n        |java.util.List<Object> $valList = new java.util.ArrayList<Object>();\n        |\"\"\".stripMargin\n    columnExtractors.map {\n      case (name, extractor) =>\n        val colValue = extractor.genCode(ctx)\n        code\"\"\"\n          |$colList.add(\"$name\");\n          |${colValue.code}\n          |if (${colValue.isNull}) {\n          |  $valList.add(null);\n          |} else {\n          |  $valList.add(${colValue.value});\n          |}\n          |\"\"\".stripMargin\n    }.fold(start)(_ + _)\n  }\n\n  private def generateExpressionValidationCode(ctx: CodegenContext): Block = {\n    val elementValue = child.genCode(ctx)\n    val invariantField = ctx.addReferenceObj(\"errMsg\", constraint)\n    val colListName = ctx.freshName(\"colList\")\n    val valListName = ctx.freshName(\"valList\")\n    code\"\"\"${elementValue.code}\n       |\n       |if (${elementValue.isNull} || ${elementValue.value} == false) {\n       |  ${generateColumnValuesCode(colListName, valListName, ctx)}\n       |  throw org.apache.spark.sql.delta.schema.DeltaInvariantViolationException.apply(\n       |     $invariantField, $colListName, $valListName);\n       |}\n     \"\"\".stripMargin\n  }\n\n  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {\n    val code = constraint match {\n      case _: NotNull => generateNotNullCode(ctx)\n      case _: Check => generateExpressionValidationCode(ctx)\n    }\n    ev.copy(code = code, isNull = TrueLiteral, value = JavaCode.literal(\"null\", NullType))\n  }\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): Expression = {\n    copy(\n      child = newChildren.head,\n      columnExtractors = columnExtractors.map(_._1).zip(newChildren.tail)\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/Constraints.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport java.util.Locale\n\nimport scala.concurrent.duration\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{AllowedUserProvidedExpressions, DeltaErrors, DeltaLog}\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.ValidateCheckConstraintsMode\n\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.{Alias, Expression, GetArrayItem, GetMapValue, GetStructField, IsNotNull, UserDefinedExpression}\nimport org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression\nimport org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, Project}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.catalyst.util.CharVarcharUtils\nimport org.apache.spark.sql.types.{BooleanType, StructType}\n\n/**\n * A constraint defined on a Delta table, which writers must verify before writing.\n */\nsealed trait Constraint {\n  val name: String\n}\n\n/**\n * Utilities for handling constraints. Right now this includes:\n * - Column-level invariants delegated to [[Invariants]], including both NOT NULL constraints and\n *   an old style of CHECK constraint specified in the column metadata\n * - Table-level CHECK constraints\n */\nobject Constraints extends DeltaLogging {\n  /**\n   * A constraint that the specified column must not be NULL. Note that when the column is nested,\n   * this implies its parents must also not be NULL.\n   */\n  case class NotNull(column: Seq[String]) extends Constraint {\n    override val name: String = \"NOT NULL\"\n  }\n\n  /** A SQL expression to check for when writing out data. */\n  case class Check(name: String, expression: Expression) extends Constraint\n\n  def getCheckConstraintNames(metadata: Metadata): Seq[String] = {\n    metadata.configuration.keys.collect {\n      case key if key.toLowerCase(Locale.ROOT).startsWith(\"delta.constraints.\") =>\n        key.stripPrefix(\"delta.constraints.\")\n    }.toSeq\n  }\n\n  /**\n   * Extract CHECK constraints from the table properties. Note that some CHECK constraints may also\n   * come from schema metadata; these constraints were never released in a public API but are\n   * maintained for protocol compatibility.\n   */\n  def getCheckConstraints(metadata: Metadata, spark: SparkSession): Seq[Constraint] = {\n    metadata.configuration.collect {\n      case (key, constraintText) if key.toLowerCase(Locale.ROOT).startsWith(\"delta.constraints.\") =>\n        val name = key.stripPrefix(\"delta.constraints.\")\n        val expression = spark.sessionState.sqlParser.parseExpression(constraintText)\n        Check(name, expression)\n    }.toSeq\n  }\n\n  /** Extract all constraints from the given Delta table metadata. */\n  def getAll(metadata: Metadata, spark: SparkSession): Seq[Constraint] = {\n    val checkConstraints = getCheckConstraints(metadata, spark)\n    val constraintsFromSchema = Invariants.getFromSchema(metadata.schema, spark)\n    val charVarcharLengthChecks = if (spark.sessionState.conf.charVarcharAsString) {\n      Nil\n    } else {\n      CharVarcharConstraint.stringConstraints(metadata.schema)\n    }\n\n    (checkConstraints ++ constraintsFromSchema ++ charVarcharLengthChecks).toSeq\n  }\n\n  /** Get the expression text for a constraint with the given name, if present. */\n  def getExprTextByName(\n      name: String,\n      metadata: Metadata,\n      spark: SparkSession): Option[String] = {\n    metadata.configuration.get(checkConstraintPropertyName(name))\n  }\n\n  def checkConstraintPropertyName(constraintName: String): String = {\n    \"delta.constraints.\" + constraintName.toLowerCase(Locale.ROOT)\n  }\n\n  /**\n   * Find all the check constraints that reference the given column name. Returns a map of\n   * constraint names to their corresponding expression.\n   */\n  def findDependentConstraints(\n      sparkSession: SparkSession,\n      columnName: Seq[String],\n      metadata: Metadata): Map[String, String] = {\n    metadata.configuration.filter {\n      case (key, constraint) if key.toLowerCase(Locale.ROOT).startsWith(\"delta.constraints.\") =>\n        SchemaUtils.containsDependentExpression(\n          sparkSession,\n          columnName,\n          constraint,\n          metadata.schema,\n          sparkSession.sessionState.conf.resolver)\n      case _ => false\n    }\n  }\n\n  /**\n   * Validates check constraints with rollout logic for safe deployment.\n   * This wrapper handles feature flag checks, error logging, and mode-based error handling.\n   */\n  def validateCheckConstraints(\n      spark: SparkSession,\n      constraints: Seq[Constraint],\n      deltaLog: DeltaLog,\n      schema: StructType): Unit = {\n    val validateCheckConstraints = ValidateCheckConstraintsMode.fromConf(spark.sessionState.conf)\n    if (validateCheckConstraints == ValidateCheckConstraintsMode.OFF) return\n    if (constraints.isEmpty) return\n\n    try {\n      val startTime = System.nanoTime()\n      validateCheckConstraintsInternal(spark, constraints, schema)\n      val durationMs = duration.NANOSECONDS.toMillis(System.nanoTime() - startTime)\n      logInfo(\n        log\"Validated CHECK constraints on table \" +\n        log\"${MDC(DeltaLogKeys.TABLE_ID, deltaLog.unsafeVolatileTableId)} \" +\n        log\"in ${MDC(DeltaLogKeys.TIME_MS, durationMs)} ms and processed \" +\n        log\"${MDC(DeltaLogKeys.NUM_PREDICATES, constraints.size)} constraints\"\n      )\n    } catch {\n      case NonFatal(e) =>\n        val errorClassName = e match {\n          case sparkEx: SparkThrowable => sparkEx.getErrorClass\n          case _ => e.getClass\n        }\n        recordDeltaEvent(\n          deltaLog,\n          \"delta.checkConstraints.validationFailure\",\n          data = Map(\n            \"errorClassName\" -> errorClassName,\n            \"errorMessage\" -> e.getMessage\n          )\n        )\n        if (validateCheckConstraints == ValidateCheckConstraintsMode.ASSERT) {\n          throw e\n        }\n    }\n  }\n\n  /**\n   * Internal validation logic for check constraints.\n   */\n  private def validateCheckConstraintsInternal(\n      spark: SparkSession,\n      constraints: Seq[Constraint],\n      schema: StructType): Unit = {\n    // Create NamedExpressions for analysis (type checking will happen after resolution)\n    // We unresolve expressions to validate against the `LocalRelation` we later create\n    val selectExprs = constraints.map {\n      case Check(name, expression) =>\n        Alias(expression, name)()\n      case NotNull(columnPath) =>\n        // Create an IsNotNull expression to validate the column exists\n        val columnRef = UnresolvedAttribute(columnPath)\n        val isNotNullExpr = IsNotNull(columnRef)\n        Alias(isNotNullExpr, s\"NOT NULL ${columnPath.mkString(\".\")}\")()\n    }\n\n    // Analyze all constraint expressions to ensure they can be properly resolved\n    // Use LocalRelation with the table schema to ensure column references can be validated\n    val analyzed = try {\n      val analyzer = spark.sessionState.analyzer\n      val relation = LocalRelation(\n        DataTypeUtils.toAttributes(CharVarcharUtils.replaceCharVarcharWithStringInSchema(schema))\n      )\n      val plan = analyzer.execute(Project(selectExprs, relation))\n      analyzer.checkAnalysis(plan)\n      plan\n    } catch {\n      case e: SparkThrowable\n        if e.getErrorClass != null && e.getErrorClass.startsWith(\"UNRESOLVED_COLUMN\") =>\n          // Check if this is an unresolved column/field error by examining the error class\n          throw DeltaErrors.checkConstraintReferToWrongColumns(\n            e.getMessageParameters.getOrDefault(\"objectName\", \"\")\n          )\n      case e => throw e\n    }\n\n    // Check that all Check constraints return boolean type\n    analyzed match {\n      case Project(projectList, _) =>\n        projectList.foreach {\n          case a: Alias =>\n            if (a.dataType != BooleanType) {\n              throw DeltaErrors.checkConstraintNotBoolean(a.name, a.child.sql)\n            }\n          case _ => // We should only the Aliases we previously created.\n        }\n      case _ => // We should only have a single projection\n    }\n\n    // Validate the analyzed expressions\n    analyzed.transformAllExpressions {\n      case expr: Alias =>\n        // Alias will be non deterministic if it points to a non deterministic expression.\n        // Skip `Alias` to provide a better error for a non deterministic expression.\n        expr\n      case expr@(_: GetStructField | _: GetArrayItem | _: GetMapValue) =>\n        // The complex type extractors don't have a function name, so we need to check them\n        // separately. Unlike generated columns we do allow `GetMapValue`.\n        expr\n      case expr: UserDefinedExpression =>\n        throw DeltaErrors.checkConstraintUDF(expr)\n      case expr if !expr.deterministic =>\n        throw DeltaErrors.checkConstraintNonDeterministicExpression(expr)\n      case expr if expr.isInstanceOf[AggregateExpression] =>\n        throw DeltaErrors.checkConstraintAggregateExpression(expr)\n      case expr if\n        !AllowedUserProvidedExpressions.expressions.contains(expr.getClass) &&\n          !AllowedUserProvidedExpressions\n            .checkConstraintExpressions.contains(expr.getClass) =>\n        throw DeltaErrors.checkConstraintUnsupportedExpression(expr)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/DeltaInvariantCheckerExec.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaIllegalStateException}\nimport org.apache.spark.sql.delta.constraints.Constraints.{Check, NotNull}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.util.AnalysisHelper\n\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.optimizer\nimport org.apache.spark.sql.catalyst.optimizer.ReplaceExpressions\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}\nimport org.apache.spark.sql.catalyst.plans.physical.Partitioning\nimport org.apache.spark.sql.catalyst.rules.RuleExecutor\nimport org.apache.spark.sql.execution.{SparkPlan, SparkStrategy, UnaryExecNode}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Operator that validates that records satisfy provided constraints before they are written into\n * Delta. Each row is left unchanged after validations.\n */\ncase class DeltaInvariantChecker(\n    child: LogicalPlan,\n    deltaConstraints: Seq[CheckDeltaInvariant]) extends UnaryNode {\n  assert(deltaConstraints.nonEmpty)\n\n  override def output: Seq[Attribute] = child.output\n\n  override protected def withNewChildInternal(newChild: LogicalPlan): DeltaInvariantChecker =\n    copy(child = newChild)\n}\n\nobject DeltaInvariantChecker {\n  def apply(\n      spark: SparkSession,\n      child: LogicalPlan,\n      constraints: Seq[Constraint]): DeltaInvariantChecker = {\n    val invariantChecks =\n      DeltaInvariantCheckerExec.buildInvariantChecks(child.output, constraints, spark)\n    DeltaInvariantChecker(child, invariantChecks)\n  }\n}\n\nobject DeltaInvariantCheckerStrategy extends SparkStrategy {\n  override def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {\n    case DeltaInvariantChecker(child, constraints) =>\n      DeltaInvariantCheckerExec(planLater(child), constraints) :: Nil\n    case _ => Nil\n  }\n}\n\n/**\n * A physical operator that validates records, before they are written into Delta. Each row\n * is left unchanged after validations.\n */\ncase class DeltaInvariantCheckerExec(\n    child: SparkPlan,\n    constraints: Seq[CheckDeltaInvariant]) extends UnaryExecNode {\n\n  override def output: Seq[Attribute] = child.output\n\n  override protected def doExecute(): RDD[InternalRow] = {\n    if (constraints.isEmpty) return child.execute()\n\n    // Resolve current_date()/current_time() expressions.\n    // We resolve currentTime for all invariants together to make sure we use the same timestamp.\n    val invariantsFakePlan = AnalysisHelper.FakeLogicalPlan(constraints, Nil)\n    val newInvariantsPlan = optimizer.ComputeCurrentTime(invariantsFakePlan)\n    val constraintsWithFixedTime = newInvariantsPlan.expressions.toArray\n\n    val childOutput = child.output\n    child.execute().mapPartitionsInternal { rows =>\n      val assertions = UnsafeProjection.create(constraintsWithFixedTime, childOutput)\n      rows.map { row =>\n        assertions(row)\n        row\n      }\n    }\n  }\n\n  override def outputOrdering: Seq[SortOrder] = child.outputOrdering\n\n  override def outputPartitioning: Partitioning = child.outputPartitioning\n\n  override protected def withNewChildInternal(newChild: SparkPlan): DeltaInvariantCheckerExec =\n    copy(child = newChild)\n}\n\nobject DeltaInvariantCheckerExec extends DeltaLogging {\n  def apply(\n      spark: SparkSession,\n      child: SparkPlan,\n      constraints: Seq[Constraint]): DeltaInvariantCheckerExec = {\n    val invariantChecks =\n      DeltaInvariantCheckerExec.buildInvariantChecks(child.output, constraints, spark)\n    DeltaInvariantCheckerExec(child, invariantChecks)\n  }\n\n  // Specialized optimizer to run necessary rules so that the check expressions can be evaluated.\n  object DeltaInvariantCheckerOptimizer extends RuleExecutor[LogicalPlan] {\n    import org.apache.spark.sql.catalyst.optimizer.{ReplaceExpressions, RewriteWithExpression}\n\n    final override protected def batches = Seq(\n      Batch(\"Finish Analysis\", Once, ReplaceExpressions),\n      Batch(\"Rewrite With expression\", Once, RewriteWithExpression)\n    )\n  }\n\n  /** Build the extractor for a particular column. */\n  private def buildExtractor(output: Seq[Attribute], column: Seq[String]): Option[Expression] = {\n    assert(column.nonEmpty)\n    val topLevelColumn = column.head\n    val topLevelRefOpt = output.collectFirst {\n      case a: AttributeReference if SchemaUtils.DELTA_COL_RESOLVER(a.name, topLevelColumn) => a\n    }\n\n    if (column.length == 1) {\n      topLevelRefOpt\n    } else {\n      topLevelRefOpt.flatMap { topLevelRef =>\n        try {\n          val nested = column.tail.foldLeft[Expression](topLevelRef) { case (e, fieldName) =>\n            e.dataType match {\n              case StructType(fields) =>\n                val ordinal = fields.indexWhere(f =>\n                  SchemaUtils.DELTA_COL_RESOLVER(f.name, fieldName))\n                if (ordinal == -1) {\n                  throw DeltaErrors.notNullColumnNotFoundInStruct(\n                    s\"${fields.map(_.name).mkString(\"[\", \",\", \"]\")}\")\n                }\n                GetStructField(e, ordinal, Some(fieldName))\n              case _ =>\n                // NOTE: We should also update `GeneratedColumn.validateGeneratedColumns` to enable\n                // `GetMapValue` and `GetArrayStructFields` expressions when this is supported.\n                throw DeltaErrors.unSupportedInvariantNonStructType\n            }\n          }\n          Some(nested)\n        } catch {\n          case _: IndexOutOfBoundsException => None\n        }\n      }\n    }\n  }\n\n  def buildInvariantChecks(\n      output: Seq[Attribute],\n      constraints: Seq[Constraint],\n      spark: SparkSession): Seq[CheckDeltaInvariant] = {\n    constraints.map { constraint =>\n      val columnExtractors = mutable.Map[String, Expression]()\n      val executableExpr = constraint match {\n        case n @ NotNull(column) =>\n          buildExtractor(output, column).getOrElse {\n            throw DeltaErrors.notNullColumnMissingException(n)\n          }\n        case Check(name, expr) =>\n          // We need to do two stages of resolution here:\n          //  * Build the extractors to evaluate attribute references against input InternalRows.\n          //  * Do logical analysis to handle nested field extractions, functions, etc.\n\n          val attributesExtracted = expr.transformUp {\n            case a: UnresolvedAttribute =>\n              val ex = buildExtractor(output, a.nameParts).getOrElse(Literal(null))\n              columnExtractors(a.name) = ex\n              ex\n          }\n\n          val wrappedPlan: LogicalPlan = ExpressionLogicalPlanWrapper(attributesExtracted)\n          val analyzedLogicalPlan = spark.sessionState.analyzer.execute(wrappedPlan)\n          val optimizedLogicalPlan = DeltaInvariantCheckerOptimizer.execute(analyzedLogicalPlan)\n          val resolvedExpr = optimizedLogicalPlan match {\n            case ExpressionLogicalPlanWrapper(e) => e\n            // This should never happen.\n            case plan => throw new DeltaIllegalStateException(\n              errorClass = \"INTERNAL_ERROR\",\n              messageParameters = Array(\n                \"Applying type casting resulted in a bad plan rather than a simple expression.\\n\" +\n               s\"Plan:${plan.prettyJson}\\n\"))\n          }\n          // Cap the maximum length when logging an unresolved expression to avoid issues. This is a\n          // CHECK constraint expression and should be relatively simple.\n          val MAX_OUTPUT_LENGTH = 10 * 1024\n          if (!resolvedExpr.resolved) {\n            // If the plan is not resolved, check the plan so that a user-facing exception is\n            // thrown.\n            spark.sessionState.analyzer.checkAnalysis(wrappedPlan)\n            deltaAssert(\n              check = false,\n              name = \"invariant.unresolvedExpression\",\n              msg = s\"CHECK constraint child expression was not properly resolved\",\n              data = Map(\n                \"name\" -> name,\n                \"checkExpr\" -> expr.treeString.take(MAX_OUTPUT_LENGTH),\n                \"attributesExtracted\" -> attributesExtracted.treeString.take(MAX_OUTPUT_LENGTH),\n                \"analyzedLogicalPlan\" -> analyzedLogicalPlan.treeString.take(MAX_OUTPUT_LENGTH),\n                \"optimizedLogicalPlan\" -> optimizedLogicalPlan.treeString.take(MAX_OUTPUT_LENGTH),\n                \"resolvedExpr\" -> resolvedExpr.treeString.take(MAX_OUTPUT_LENGTH)\n              )\n            )\n          }\n          resolvedExpr\n      }\n\n      CheckDeltaInvariant(executableExpr, columnExtractors.toSeq, constraint)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/ExpressionLogicalPlanWrapper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.LeafNode\n\n/**\n * A dummy wrapper for expressions so we can pass them to the [[Analyzer]].\n */\nprivate[constraints] case class ExpressionLogicalPlanWrapper(e: Expression) extends LeafNode {\n  override def output: Seq[Attribute] = Seq.empty\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/Invariants.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.types.StructType\n\n/**\n * List of invariants that can be defined on a Delta table that will allow us to perform\n * validation checks during changes to the table.\n */\nobject Invariants {\n  sealed trait Rule {\n    val name: String\n  }\n\n  /** Used for columns that should never be null. */\n  case object NotNull extends Rule { override val name: String = \"NOT NULL\" }\n\n  sealed trait RulePersistedInMetadata {\n    def wrap: PersistedRule\n    def json: String = JsonUtils.toJson(wrap)\n  }\n\n  /** Rules that are persisted in the metadata field of a schema. */\n  case class PersistedRule(expression: PersistedExpression = null) {\n    def unwrap: RulePersistedInMetadata = {\n      if (expression != null) {\n        expression\n      } else {\n        null\n      }\n    }\n  }\n\n  /** A SQL expression to check for when writing out data. */\n  case class ArbitraryExpression(expression: Expression) extends Rule {\n    override val name: String = s\"EXPRESSION($expression)\"\n  }\n\n  object ArbitraryExpression {\n    def apply(sparkSession: SparkSession, exprString: String): ArbitraryExpression = {\n      val expr = sparkSession.sessionState.sqlParser.parseExpression(exprString)\n      ArbitraryExpression(expr)\n    }\n  }\n\n  /** Persisted companion of the ArbitraryExpression rule. */\n  case class PersistedExpression(expression: String) extends RulePersistedInMetadata {\n    override def wrap: PersistedRule = PersistedRule(expression = this)\n  }\n\n  /** Extract invariants from the given schema */\n  def getFromSchema(schema: StructType, spark: SparkSession): Seq[Constraint] = {\n    val columns = SchemaUtils.filterRecursively(schema, checkComplexTypes = false) { field =>\n      !field.nullable || field.metadata.contains(INVARIANTS_FIELD)\n    }\n    columns.map {\n      case (parents, field) if !field.nullable =>\n        Constraints.NotNull(parents :+ field.name)\n      case (parents, field) =>\n        val rule = field.metadata.getString(INVARIANTS_FIELD)\n        val invariant = Option(JsonUtils.mapper.readValue[PersistedRule](rule).unwrap) match {\n          case Some(PersistedExpression(exprString)) =>\n            ArbitraryExpression(spark, exprString)\n          case _ =>\n            throw DeltaErrors.unrecognizedInvariant()\n        }\n        Constraints.Check(invariant.name, invariant.expression)\n    }\n  }\n\n  val INVARIANTS_FIELD = \"delta.invariants\"\n}\n\n/** A rule applied on a column to ensure data hygiene. */\ncase class Invariant(column: Seq[String], rule: Invariants.Rule)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/constraints/tableChanges.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.constraints\n\nimport org.apache.spark.sql.connector.catalog.TableChange\n\n/**\n * Change to add a CHECK constraint to a table.\n *\n * @param constraintName The name of the new constraint. Note that constraint names are\n *                       case insensitive.\n * @param expr The expression to add, as a SQL parseable string.\n */\ncase class AddConstraint(constraintName: String, expr: String) extends TableChange {}\n\n/**\n * Change to drop a constraint from a table. Note that this is always idempotent - no error\n * will be thrown if the constraint doesn't exist.\n *\n * @param constraintName the name of the constraint to drop - case insensitive\n * @param ifExists if false, throws an error if the constraint to be dropped does not exist\n */\ncase class DropConstraint(constraintName: String, ifExists: Boolean) extends TableChange {}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/AbstractBatchBackfillingCommitCoordinatorClient.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.{Optional, UUID}\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.TransactionExecutionObserver\nimport org.apache.spark.sql.delta.actions.CommitInfo\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.util.FileNames\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{CommitCoordinatorClient, CommitFailedException => JCommitFailedException, CommitResponse, CoordinatedCommitsUtils => JCoordinatedCommitsUtils, TableDescriptor, TableIdentifier, UpdatedActions}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.internal.{Logging, MDC}\n\n/**\n * An abstract [[CommitCoordinatorClient]] which triggers backfills every n commits.\n * - every commit version which satisfies `commitVersion % batchSize == 0` will trigger a backfill.\n */\ntrait AbstractBatchBackfillingCommitCoordinatorClient\n  extends CommitCoordinatorClient\n    with Logging {\n\n  /**\n   * Size of batch that should be backfilled. So every commit version which satisfies\n   * `commitVersion % batchSize == 0` will trigger a backfill.\n   */\n  val batchSize: Long\n\n  /**\n   * Commit a given `commitFile` to the table represented by given `logPath` at the\n   * given `commitVersion`\n   */\n  private[delta] def commitImpl(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      logPath: Path,\n      coordinatedCommitsTableConf: Map[String, String],\n      commitVersion: Long,\n      commitFile: FileStatus,\n      commitTimestamp: Long): CommitResponse\n\n  override def commit(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      tableDesc: TableDescriptor,\n      commitVersion: Long,\n      actions: java.util.Iterator[String],\n      updatedActions: UpdatedActions): CommitResponse = {\n    val logPath = tableDesc.getLogPath\n    val executionObserver = TransactionExecutionObserver.getObserver\n    val tablePath = JCoordinatedCommitsUtils.getTablePath(logPath)\n    if (commitVersion == 0) {\n      throw new JCommitFailedException(false, false, \"Commit version 0 must go via filesystem.\")\n    }\n    logInfo(log\"Attempting to commit version \" +\n      log\"${MDC(DeltaLogKeys.VERSION, commitVersion)} on table \" +\n      log\"${MDC(DeltaLogKeys.PATH, tablePath)}\")\n    val fs = logPath.getFileSystem(hadoopConf)\n    if (batchSize <= 1) {\n      // Backfill until `commitVersion - 1`\n      logInfo(log\"Making sure commits are backfilled until \" +\n        log\"${MDC(DeltaLogKeys.VERSION, commitVersion - 1)} version for\" +\n        log\" table ${MDC(DeltaLogKeys.PATH, tablePath.toString)}\")\n      backfillToVersion(\n        logStore,\n        hadoopConf,\n        tableDesc,\n        commitVersion - 1,\n        null)\n    }\n\n    // Write new commit file in `_staged_commits` directory\n    val fileStatus = JCoordinatedCommitsUtils.writeUnbackfilledCommitFile(\n      logStore, hadoopConf, logPath.toString, commitVersion, actions, generateUUID())\n\n    // Do the actual commit\n    val commitTimestamp = updatedActions.getCommitInfo.getCommitTimestamp\n    var commitResponse =\n      commitImpl(\n        logStore,\n        hadoopConf,\n        logPath,\n        tableDesc.getTableConf.asScala.toMap,\n        commitVersion,\n        fileStatus,\n        commitTimestamp)\n\n    val mcToFsConversion = isCoordinatedCommitsToFSConversion(commitVersion, updatedActions)\n    // Backfill if needed\n    executionObserver.beginBackfill()\n    if (batchSize <= 1) {\n      // Always backfill when batch size is configured as 1\n      backfill(logStore, hadoopConf, logPath, commitVersion, fileStatus)\n      val targetFile = FileNames.unsafeDeltaFile(logPath, commitVersion)\n      val targetFileStatus = fs.getFileStatus(targetFile)\n      val newCommit = commitResponse.getCommit.withFileStatus(targetFileStatus)\n      commitResponse = new CommitResponse(newCommit)\n    } else if (commitVersion % batchSize == 0 || mcToFsConversion) {\n      logInfo(log\"Making sure commits are backfilled till \" +\n        log\"${MDC(DeltaLogKeys.VERSION, commitVersion)} \" +\n        log\"version for table ${MDC(DeltaLogKeys.PATH, tablePath.toString)}\")\n      backfillToVersion(logStore, hadoopConf, tableDesc, commitVersion, null)\n    }\n    logInfo(log\"Commit ${MDC(DeltaLogKeys.VERSION, commitVersion)} done successfully on table \" +\n      log\"${MDC(DeltaLogKeys.PATH, tablePath)}\")\n    commitResponse\n  }\n\n  private def isCoordinatedCommitsToFSConversion(\n      commitVersion: Long,\n      updatedActions: UpdatedActions): Boolean = {\n    val oldMetadataHasCoordinatedCommits =\n      JCoordinatedCommitsUtils.getCoordinatorName(updatedActions.getOldMetadata).isPresent\n    val newMetadataHasCoordinatedCommits =\n      JCoordinatedCommitsUtils.getCoordinatorName(updatedActions.getNewMetadata).isPresent\n    oldMetadataHasCoordinatedCommits && !newMetadataHasCoordinatedCommits && commitVersion > 0\n  }\n\n  protected def generateUUID(): String = UUID.randomUUID().toString\n\n  override def backfillToVersion(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      tableDesc: TableDescriptor,\n      version: Long,\n      lastKnownBackfilledVersionOpt: java.lang.Long): Unit = {\n    val logPath = tableDesc.getLogPath\n    // Confirm the last backfilled version by checking the backfilled delta file's existence.\n    val validLastKnownBackfilledVersionOpt = Option(lastKnownBackfilledVersionOpt)\n        .filter { version =>\n      val fs = logPath.getFileSystem(hadoopConf)\n      fs.exists(FileNames.unsafeDeltaFile(logPath, version))\n    }\n    val startVersionOpt: Long = validLastKnownBackfilledVersionOpt.map(_ + 1).map(Long.box).orNull\n    getCommits(tableDesc, startVersionOpt, version)\n      .getCommits.asScala\n      .foreach { commit =>\n        backfill(logStore, hadoopConf, logPath, commit.getVersion, commit.getFileStatus)\n    }\n  }\n\n  /** Backfills a given `fileStatus` to `version`.json */\n  protected def backfill(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      logPath: Path,\n      version: Long,\n      fileStatus: FileStatus): Unit = {\n    val targetFile = FileNames.unsafeDeltaFile(logPath, version)\n    logInfo(log\"Backfilling commit ${MDC(DeltaLogKeys.PATH, fileStatus.getPath)} to \" +\n      log\"${MDC(DeltaLogKeys.PATH2, targetFile.toString)}\")\n    val commitContentIterator = logStore.read(fileStatus.getPath, hadoopConf)\n    try {\n      logStore.write(\n        targetFile,\n        commitContentIterator,\n        false,\n        hadoopConf)\n      registerBackfill(logPath, version)\n    } catch {\n      case _: FileAlreadyExistsException =>\n        logInfo(log\"The backfilled file ${MDC(DeltaLogKeys.FILE_NAME, targetFile)} already exists.\")\n    } finally {\n      commitContentIterator.close()\n    }\n  }\n\n  /**\n   * Callback to tell the CommitCoordinator that all commits <= `backfilledVersion` are backfilled.\n   */\n  protected[delta] def registerBackfill(\n      logPath: Path,\n      backfilledVersion: Long): Unit\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/CommitCoordinatorClient.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport scala.collection.mutable\n\nimport io.delta.dynamodbcommitcoordinator.DynamoDBCommitCoordinatorClientBuilder\nimport io.delta.storage.commit.CommitCoordinatorClient\n\nimport org.apache.spark.sql.SparkSession\n\nobject CommitCoordinatorClient {\n  def semanticEquals(\n      commitCoordinatorClientOpt1: Option[CommitCoordinatorClient],\n      commitCoordinatorClientOpt2: Option[CommitCoordinatorClient]): Boolean = {\n    (commitCoordinatorClientOpt1, commitCoordinatorClientOpt2) match {\n      case (Some(commitCoordinatorClient1), Some(commitCoordinatorClient2)) =>\n        commitCoordinatorClient1.semanticEquals(commitCoordinatorClient2)\n      case (None, None) =>\n        true\n      case _ =>\n        false\n    }\n  }\n}\n\n/** A builder interface for [[CommitCoordinatorClient]] */\ntrait CommitCoordinatorBuilder {\n\n  /** Name of the commit-coordinator */\n  def getName: String\n\n  /** Returns a commit-coordinator client based on the given conf */\n  def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient\n}\n\n/** An extended builder interface for [[CommitCoordinatorClient]] with CatalogOwned table feature */\ntrait CatalogOwnedCommitCoordinatorBuilder extends CommitCoordinatorBuilder {\n  /** Returns a catalog-owned commit-coordinator client based for the given catalog. */\n  def buildForCatalog(spark: SparkSession, catalogName: String): CommitCoordinatorClient\n}\n\n/** Factory to get the correct [[CommitCoordinatorClient]] for a table */\nobject CommitCoordinatorProvider {\n  // mapping from different commit-coordinator names to the corresponding\n  // [[CommitCoordinatorBuilder]]s.\n  private val nameToBuilderMapping = mutable.Map.empty[String, CommitCoordinatorBuilder]\n\n  /** Registers a new [[CommitCoordinatorBuilder]] with the [[CommitCoordinatorProvider]] */\n  def registerBuilder(commitCoordinatorBuilder: CommitCoordinatorBuilder): Unit = synchronized {\n    nameToBuilderMapping.get(commitCoordinatorBuilder.getName) match {\n      case Some(existingBuilder: CommitCoordinatorBuilder) =>\n        throw new IllegalArgumentException(\n          s\"commit-coordinator: ${existingBuilder.getName} already\" +\n          s\" registered with builder ${existingBuilder.getClass.getName}\")\n      case None =>\n        nameToBuilderMapping.put(commitCoordinatorBuilder.getName, commitCoordinatorBuilder)\n    }\n  }\n\n  /**\n   * Returns a [[CommitCoordinatorClient]] for the given `name`, `conf`, and `spark`.\n   * If the commit-coordinator with the given name is not registered, an exception is thrown.\n   */\n  def getCommitCoordinatorClient(\n      name: String,\n      conf: Map[String, String],\n      spark: SparkSession): CommitCoordinatorClient = synchronized {\n    getCommitCoordinatorClientOpt(name, conf, spark).getOrElse {\n      throw new IllegalArgumentException(s\"Unknown commit-coordinator: $name\")\n    }\n  }\n\n  /**\n   * Returns a [[CommitCoordinatorClient]] for the given `name`, `conf`, and `spark`.\n   * Returns None if the commit-coordinator with the given name is not registered.\n   */\n  def getCommitCoordinatorClientOpt(\n      name: String,\n      conf: Map[String, String],\n      spark: SparkSession): Option[CommitCoordinatorClient] = synchronized {\n    nameToBuilderMapping.get(name).map(_.build(spark, conf))\n  }\n\n  def getRegisteredCoordinatorNames: Seq[String] = synchronized {\n    nameToBuilderMapping.keys.toSeq\n  }\n\n  // Visible only for UTs\n  private[delta] def clearNonDefaultBuilders(): Unit = synchronized {\n    val initialCommitCoordinatorNames = initialCommitCoordinatorBuilders.map(_.getName).toSet\n    nameToBuilderMapping.retain((k, _) => initialCommitCoordinatorNames.contains(k))\n  }\n\n  private[delta] def clearAllBuilders(): Unit = synchronized {\n    nameToBuilderMapping.clear()\n  }\n\n  private val initialCommitCoordinatorBuilders = Seq[CommitCoordinatorBuilder](\n    UCCommitCoordinatorBuilder,\n    new DynamoDBCommitCoordinatorClientBuilder()\n  )\n  initialCommitCoordinatorBuilders.foreach(registerBuilder)\n}\n\n/** Factory to get the correct [[CatalogOwnedCommitCoordinatorBuilder]] for a catalog-owned table */\nobject CatalogOwnedCommitCoordinatorProvider {\n  // mapping from catalog names to the corresponding [[CatalogOwnedCommitCoordinatorBuilder]]s.\n  private val catalogNameToBuilderMapping =\n    mutable.Map.empty[String, CatalogOwnedCommitCoordinatorBuilder]\n\n  // Visible only for UTs\n  private[delta] def clearBuilders(): Unit = synchronized {\n    catalogNameToBuilderMapping.clear()\n  }\n\n  /** Registers a new [[CommitCoordinatorBuilder]] with the [[CommitCoordinatorProvider]] */\n  def registerBuilder(\n      catalogName: String, commitCoordinatorBuilder: CatalogOwnedCommitCoordinatorBuilder): Unit =\n    synchronized {\n      catalogNameToBuilderMapping.get(catalogName) match {\n        case Some(existingBuilder: CommitCoordinatorBuilder) =>\n          throw new IllegalArgumentException(\n            s\"commit-coordinator for catalog: $catalogName already\" +\n              s\" registered with builder ${existingBuilder.getClass.getName}\")\n        case None =>\n          catalogNameToBuilderMapping.put(catalogName, commitCoordinatorBuilder)\n      }\n    }\n\n  def getBuilder(catalogName: String): Option[CatalogOwnedCommitCoordinatorBuilder] =\n    catalogNameToBuilderMapping.get(catalogName)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsUsageLogs.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\n/** Class containing usage logs emitted by Catalog Owned. */\nobject CatalogOwnedUsageLogs {\n  /** Common prefix for all catalog-owned usage logs. */\n  val PREFIX = \"delta.catalogOwned\"\n\n  /**\n   * Usage log emitted when we populate the commit coordinator via invalid path-based access.\n   * I.e., No catalog table is present when trying to populate commit coordinator, which is\n   *       almost a bug case for CC tables.\n   */\n  val COMMIT_COORDINATOR_POPULATION_INVALID_PATH_BASED_ACCESS =\n    s\"$PREFIX.commitCoordinatorPopulation.invalidPathBasedAccess\"\n}\n\n/** Class containing usage logs emitted by Coordinated Commits. */\nobject CoordinatedCommitsUsageLogs {\n\n  // Common prefix for all coordinated-commits usage logs.\n  private val PREFIX = \"delta.coordinatedCommits\"\n\n  // Usage log emitted as part of [[CommitCoordinatorClient.getCommits]] call.\n  val COMMIT_COORDINATOR_CLIENT_GET_COMMITS = s\"$PREFIX.commitCoordinatorClient.getCommits\"\n\n  // Usage log emitted when listing files in CommitCoordinatorClient (i.e. getCommits) can't be done\n  // in separate thread because the thread pool is full.\n  val COMMIT_COORDINATOR_LISTING_THREADPOOL_FULL =\n    s\"$PREFIX.listDeltaAndCheckpointFiles.GetCommitsThreadpoolFull\"\n\n  // Usage log emitted when we need a 2nd roundtrip to list files in FileSystem.\n  // This happens when:\n  // 1. FileSystem returns File 1/2/3\n  // 2. CommitCoordinatorClient returns File 5/6 -- 4 got backfilled by the time our request reached\n  //    CommitCoordinatorClient\n  // 3. We need to list again in FileSystem to get File 4.\n  val COMMIT_COORDINATOR_ADDITIONAL_LISTING_REQUIRED =\n    s\"$PREFIX.listDeltaAndCheckpointFiles.requiresAdditionalFsListing\"\n\n  // Usage log emitted when listing files via FileSystem and CommitCoordinatorClient\n  // (i.e. getCommits) shows an unexpected gap.\n  val FS_COMMIT_COORDINATOR_LISTING_UNEXPECTED_GAPS =\n    s\"$PREFIX.listDeltaAndCheckpointFiles.unexpectedGapsInResults\"\n\n  // Usage log emitted when a requested Commit Coordinator implementation is missing.\n  val COMMIT_COORDINATOR_MISSING_IMPLEMENTATION =\n    s\"$PREFIX.commitCoordinator.missingImplementation\"\n\n  // Usage log emitted when a client attempts to write to a CC table even though the\n  // commit coordinator implementation is missing.\n  val COMMIT_COORDINATOR_MISSING_IMPLEMENTATION_WRITE =\n    s\"$PREFIX.commitCoordinator.missingImplementationWrite\"\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, CheckpointPolicy, CoordinatedCommitsTableFeature, DeletionVectorsTableFeature, DeltaConfig, DeltaConfigs, DeltaErrors, DeltaIllegalArgumentException, DeltaLog, NameMapping, OptimisticTransaction, RowTrackingFeature, Snapshot, SnapshotDescriptor, TableFeature, V2CheckpointTableFeature}\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.commands.CloneTableCommand\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.spark.sql.delta.util.FileNames.{BackfilledDeltaFile, CompactedDeltaFile, DeltaFile, UnbackfilledDeltaFile}\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{CommitCoordinatorClient, GetCommitsResponse => JGetCommitsResponse, TableIdentifier}\nimport io.delta.storage.commit.actions.AbstractMetadata\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.{TableIdentifier => CatalystTableIdentifier}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.connector.catalog.CatalogPlugin\nimport org.apache.spark.util.Utils\n\nobject CatalogOwnedTableUtils extends DeltaLogging {\n  /** The default catalog name only used for testing. */\n  val DEFAULT_CATALOG_NAME_FOR_TESTING: String = \"spark_catalog\"\n\n  /**\n   * Sets or removes CatalogOwnedTableFeature from a transaction's protocol.\n   *\n   * This helper method is used to control whether a transaction should use CC.\n   *\n   * @param txn The OptimisticTransaction to modify protocol and metadata on\n   * @param withCatalogOwnedTableFeature If true, adds CatalogOwnedTableFeature to the protocol\n   *                                      and ensures ICT enablement. If false, removes it.\n   *\n   * @note When adding CatalogOwnedTableFeature, this method also forces a metadata update\n   *       to ensure ICT (In-Commit Timestamp) is properly enabled. See the below comment\n   *       for details.\n   */\n  def setTxnProtocol(txn: OptimisticTransaction, withCatalogOwnedTableFeature: Boolean): Unit = {\n    if (withCatalogOwnedTableFeature) {\n      val p = txn.protocol.withFeature(feature = CatalogOwnedTableFeature)\n      txn.updateProtocol(protocol = p)\n      // Force a metadata update to trigger ICT (In-Commit Timestamp) enablement.\n      // CatalogOwnedTableFeature requires ICT to be enabled in the metadata, but\n      // updateMetadataAndProtocolWithRequiredFeatures in prepareCommit only runs\n      // when there's an explicit metadata change (metadataChanges.headOption is non-empty).\n      // Since we're only updating the protocol here without changing metadata content,\n      // we need to explicitly call updateMetadata to ensure metadataChanges is non-empty\n      // during prepareCommit, which will then enable ICT in the metadata.\n      // Without this, generateInCommitTimestampForFirstCommitAttempt would return None,\n      // causing UCCommitCoordinatorClient to fail with DELTA_MISSING_COMMIT_TIMESTAMP.\n      txn.updateMetadata(proposedNewMetadata = txn.metadata)\n    } else {\n      val p = txn.protocol.removeFeature(targetFeature = CatalogOwnedTableFeature)\n      txn.updateProtocol(protocol = p)\n    }\n  }\n\n  // Populate table commit coordinator using table identifier inside CatalogTable.\n  def populateTableCommitCoordinatorFromCatalog(\n      spark: SparkSession,\n      catalogTableOpt: Option[CatalogTable],\n      snapshot: Snapshot): Option[TableCommitCoordinatorClient] = {\n    if (!snapshot.isCatalogOwned) {\n      return None\n    }\n    catalogTableOpt.map { catalogTable =>\n      // Resolve commit coordinator name by contacting catalog.\n      val cc = getCommitCoordinator(\n          spark,\n          catalogTable.identifier).getOrElse {\n        throw new IllegalStateException(\n          \"Couldn't locate commit coordinator for: \" + catalogTable.identifier)\n      }\n      val tableConf =\n        snapshot.metadata.configuration\n      TableCommitCoordinatorClient(\n        commitCoordinatorClient = cc,\n        logPath = snapshot.deltaLog.logPath,\n        tableConf = tableConf,\n        hadoopConf = snapshot.deltaLog.newDeltaHadoopConf(),\n        logStore = snapshot.deltaLog.store\n      )\n    }\n    .orElse {\n      if (Utils.isTesting) {\n        // In unit test with a path based access, it is possible to enable CatalogOwned with\n        // in-memory commit coordinator. In this case, we return table commit coordinator\n        // registered in the provider so that it can still test CatalogOwned table feature\n        // capability.\n        CatalogOwnedCommitCoordinatorProvider.getBuilder(DEFAULT_CATALOG_NAME_FOR_TESTING)\n          .flatMap{ builder =>\n            Some(builder.buildForCatalog(spark, DEFAULT_CATALOG_NAME_FOR_TESTING))\n          }\n          .map { cc =>\n            return Some(TableCommitCoordinatorClient(\n              cc,\n              logPath = snapshot.deltaLog.logPath,\n              tableConf = snapshot.metadata.configuration,\n              hadoopConf = snapshot.deltaLog.newDeltaHadoopConf(),\n              logStore = snapshot.deltaLog.store)\n            )\n          }\n      }\n      // This table is catalog owned table but catalogTableOpt is not defined. This means\n      // that the caller is accessing this table by path-based or the calling code path is missing\n      // the CatalogTable.\n      // TODO: Better error message with proper error code.\n      logAndThrowPathBasedAccessNotAllowed(snapshot)\n    }\n  }\n\n  // Directly returns the commit coordinator client for the given catalog table.\n  def getCommitCoordinator(\n      spark: SparkSession,\n      identifier: CatalystTableIdentifier): Option[CommitCoordinatorClient] = {\n    identifier.nameParts match {\n      case spark.sessionState.analyzer.CatalogAndIdentifier(catalog, _) =>\n        CatalogOwnedCommitCoordinatorProvider.getBuilder(catalog.name)\n          .map(_.buildForCatalog(spark, catalog.name)).orElse {\n            if (catalog.getClass.getName ==\n                UCCommitCoordinatorBuilder.UNITY_CATALOG_CONNECTOR_CLASS) {\n              Some(UCCommitCoordinatorBuilder.buildForCatalog(spark, catalog.name))\n            } else {\n              None\n            }\n          }\n      case _ =>\n        throw new IllegalStateException(\n          \"Failed to resolve the catalog: \" + identifier)\n    }\n  }\n\n  /**\n   * Returns the catalog name from the given catalog table identifier.\n   * If the catalog table is not present, returns None.\n   */\n  def getCatalogName(\n      spark: SparkSession,\n      identifier: CatalystTableIdentifier): Option[String] = {\n    identifier.nameParts match {\n      case spark.sessionState.analyzer.CatalogAndIdentifier(catalog, _) =>\n        if (catalog.getClass.getName == UCCommitCoordinatorBuilder.UNITY_CATALOG_CONNECTOR_CLASS) {\n          // UC is the current commit coordinator.\n          Some(UCCommitCoordinatorBuilder.COORDINATOR_NAME)\n        } else {\n          // Other catalog (e.g., `spark_catalog`) is the commit coordinator.\n          Some(catalog.name)\n        }\n      case _ =>\n        None\n    }\n  }\n\n  /**\n   * The \"Quality of Life\" table features that will be enabled automatically\n   * when creating CatalogOwned tables.\n   * Note that we also include the properties (i.e., DeltaConfig and target value)\n   * used to determine whether the table features and the corresponding\n   * properties/metadata have been enabled or not.\n   */\n  val QOL_TABLE_FEATURES_AND_PROPERTIES: Seq[(TableFeature, DeltaConfig[_], String)] =\n    qolTableFeatureAndProperties\n\n  def qolTableFeatureAndProperties: Seq[(TableFeature, DeltaConfig[_], String)] =\n    Seq(\n      (DeletionVectorsTableFeature, DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION, \"true\"),\n      (V2CheckpointTableFeature, DeltaConfigs.CHECKPOINT_POLICY, CheckpointPolicy.V2.name),\n      (RowTrackingFeature, DeltaConfigs.ROW_TRACKING_ENABLED, \"true\")\n    )\n\n  /**\n   * Return true if we should enable CatalogOwned either via default spark\n   * session configuration during creating a new table,\n   * or via the explicit table property overrides.\n   */\n  def shouldEnableCatalogOwned(\n      spark: SparkSession,\n      propertyOverrides: Map[String, String],\n      isCreatingNew: Boolean = true): Boolean = {\n    // Check explicit property overrides when creating a new or upgrading an existing table.\n    val isExplicitlyEnablingCO = TableFeatureProtocolUtils.getSupportedFeaturesFromTableConfigs(\n      configs = propertyOverrides).contains(CatalogOwnedTableFeature)\n\n    // Check default spark session configuration only when creating a new table.\n    val isEnablingCOByDefault =\n      isCreatingNew && CatalogOwnedTableUtils.defaultCatalogOwnedEnabled(spark)\n\n    isExplicitlyEnablingCO || isEnablingCOByDefault\n  }\n\n  /**\n   * Checks if a configuration is already set in metadata or Spark defaults.\n   * Ensures we don't override user preferences.\n   */\n  private def isAlreadyConfigured(\n      config: DeltaConfig[_],\n      configuration: Map[String, String],\n      spark: SparkSession): Boolean = {\n    configuration.contains(config.key) ||\n      spark.sessionState.conf.contains(config.defaultTablePropertyKey)\n  }\n\n  /**\n   * Updates table metadata with appropriate QoL features for CatalogManaged tables.\n   *\n   * Main entry point for QoL feature enablement during table creation.\n   * See [[getQoLConfigsToAdd]] for the logic that determines which features are added.\n   *\n   * @param spark SparkSession for configuration\n   * @param metadata Table metadata to update\n   * @return Updated metadata with QoL features\n   */\n  def updateMetadataForQoLFeatures(\n      spark: SparkSession,\n      metadata: Metadata): Metadata = {\n    val qoLConfigsToAdd = QOL_TABLE_FEATURES_AND_PROPERTIES.collect {\n      case (feature, config, targetValue) if\n          !isAlreadyConfigured(config, metadata.configuration, spark) =>\n        config.key -> targetValue\n    }.toMap\n    metadata.copy(configuration = metadata.configuration ++ qoLConfigsToAdd)\n  }\n\n  val ICT_TABLE_PROPERTY_CONFS = Seq(\n    DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED,\n    DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n    DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP)\n\n  /**\n   * The main ICT table properties used as dependencies for Catalog-Owned enabled table.\n   */\n  val ICT_TABLE_PROPERTY_KEYS: Seq[String] = ICT_TABLE_PROPERTY_CONFS.map(_.key)\n\n  /**\n   * Verifies that the property keys do not contain any ICT dependencies for Catalog-Owned.\n   */\n  private[delta] def verifyNotContainsICTConfigurations(propKeys: Seq[String]): Unit = {\n    ICT_TABLE_PROPERTY_KEYS.foreach { key =>\n      if (propKeys.contains(key)) {\n        throw new DeltaIllegalArgumentException(\n          \"DELTA_CANNOT_MODIFY_CATALOG_MANAGED_DEPENDENCIES\",\n          messageParameters = Array.empty)\n      }\n    }\n  }\n\n  /**\n   * Validates the Catalog-Owned configurations in explicit command overrides for\n   * `AlterTableSetPropertiesDeltaCommand`.\n   *\n   * If [[CatalogOwnedTableFeature]] presents, we do NOT allow users to\n   * modify any ICT properties that Catalog-Owned depends on.\n   */\n  def validatePropertiesForAlterTableSetPropertiesDeltaCommand(\n      snapshot: Snapshot,\n      propertyOverrides: Map[String, String]): Unit = {\n    if (snapshot.isCatalogOwned) {\n      // For Catalog-Owned enabled tables, check the dependent ICT properties.\n      // Note: Upgrade/Downgrade have been blocked earlier, which do not need to be\n      //       checked here.\n      verifyNotContainsICTConfigurations(propKeys = propertyOverrides.keys.toSeq)\n    }\n  }\n\n  /**\n   * Validates the configurations to unset for `AlterTableUnsetPropertiesDeltaCommand`.\n   *\n   * If the table already has [[CatalogOwnedTableFeature]] present,\n   * we do not allow users to unset any of the ICT properties that Catalog-Owned depends on.\n   */\n  def validatePropertiesForAlterTableUnsetPropertiesDeltaCommand(\n      snapshot: Snapshot,\n      propKeysToUnset: Seq[String]): Unit = {\n    if (snapshot.isCatalogOwned) {\n      verifyNotContainsICTConfigurations(propKeys = propKeysToUnset)\n    }\n  }\n\n  /**\n   * Validates the CatalogManaged properties in explicit command overrides and default\n   * SparkSession properties for `CreateDeltaTableCommand`.\n   *\n   * @param spark The SparkSession.\n   * @param tableExists Whether the table already exists.\n   * @param query The query to be executed (e.g., CloneTableCommand).\n   * @param catalogTableProperties The table properties from the catalog table.\n   * @param existingTableSnapshotOpt The snapshot of the existing table, if it exists.\n   */\n  def validatePropertiesForCreateDeltaTableCommand(\n      spark: SparkSession,\n      tableExists: Boolean,\n      query: Option[LogicalPlan],\n      catalogTableProperties: Map[String, String],\n      existingTableSnapshotOpt: Option[Snapshot] = None): Unit = {\n    val (command, propertyOverrides) = query match {\n      // For CLONE, we cannot use the properties from the catalog table, because they are already\n      // the result of merging the source table properties with the overrides, but we do not\n      // consider the source table properties for CatalogManaged tables.\n      case Some(cmd: CloneTableCommand) =>\n        (if (tableExists) \"REPLACE with CLONE\" else \"CREATE with CLONE\",\n          cmd.tablePropertyOverrides)\n      case _ => (if (tableExists) \"REPLACE\" else \"CREATE\", catalogTableProperties)\n    }\n    // We do not allow users to modify [[UCCommitCoordinatorClient.UC_TABLE_ID_KEY]] and\n    // [[CatalogOwnedTableFeature.name]] in any explicit overrides for REPLACE command.\n    if (tableExists) {\n      // Must be \"REPLACE\" or \"REPLACE with CLONE\" if the table already exists.\n      assert(command == \"REPLACE with CLONE\" || command == \"REPLACE\",\n        s\"Unexpected command: $command\")\n      validateUCTableIdNotPresent(property = propertyOverrides)\n\n      val isSpecifyingCatalogManaged = TableFeatureProtocolUtils\n        .getSupportedFeaturesFromTableConfigs(propertyOverrides)\n        .contains(CatalogOwnedTableFeature)\n      val existingTableIsCatalogManaged = existingTableSnapshotOpt.exists(_.isCatalogOwned)\n\n      // Allow specifying CatalogManaged in REPLACE TABLE if the existing table is already\n      // CatalogManaged. In this case, the commit coordinator properties are treated as a no-op.\n      // Block if trying to enable CatalogManaged on a non-CatalogManaged table via REPLACE.\n      // Users should either upgrade the existing table or create a fresh CatalogManaged table.\n      //\n      // Note: We intentionally use `&& !` instead of `!=` here. Using `!=` would also block the\n      // case where a CatalogManaged table is replaced without explicitly specifying CatalogManaged\n      // properties, which would hurt customer experience by forcing them to always specify CC\n      // properties on every REPLACE command. Since the existing Delta behavior already preserves\n      // the CatalogManaged status during REPLACE (the table type won't change), there's no need\n      // to block that case.\n      if (isSpecifyingCatalogManaged && !existingTableIsCatalogManaged) {\n        throw new IllegalStateException(\n          \"Specifying CatalogManaged in REPLACE TABLE command is not supported \" +\n          \"for tables that are not already CatalogManaged. \" +\n          \"Please either upgrade the existing table or create a fresh CatalogManaged table.\")\n      }\n    }\n  }\n\n  /**\n   * Validates that the UC table ID is not present in the provided property (overrides).\n   * Errors out if it is present.\n   *\n   * @param property The property to validate.\n   */\n  def validateUCTableIdNotPresent(property: Map[String, String]): Unit = {\n    if (property.contains(UCCommitCoordinatorClient.UC_TABLE_ID_KEY)) {\n      throw DeltaErrors.cannotModifyTableProperty(\n        prop = UCCommitCoordinatorClient.UC_TABLE_ID_KEY)\n    }\n  }\n\n  /**\n   * Whether Catalog-Owned is enabled via default SparkSession configuration.\n   *\n   * @param spark The SparkSession to check.\n   * @return True if Catalog-Owned is enabled by default, false otherwise.\n   */\n  def defaultCatalogOwnedEnabled(spark: SparkSession): Boolean = {\n    spark.conf\n      .getOption(TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature))\n      .contains(\"supported\")\n  }\n\n  /**\n   * Helper function to log invalid path-based access and throw the appropriate error.\n   *\n   * @param snapshot The snapshot being processed\n   */\n  private def logAndThrowPathBasedAccessNotAllowed(snapshot: Snapshot): Nothing = {\n    recordCommitCoordinatorPopulationUsageLog(\n      snapshot.deltaLog,\n      opType = CatalogOwnedUsageLogs.COMMIT_COORDINATOR_POPULATION_INVALID_PATH_BASED_ACCESS,\n      snapshot,\n      catalogTableOpt = None,\n      commitCoordinatorOpt = None,\n      includeStackTrace = true,\n      includeAdditionalDiagnostics = true\n    )\n    throw DeltaErrors.catalogManagedTablePathBasedAccessNotAllowed(snapshot.path)\n  }\n\n  /**\n   * Records usage logs for commit coordinator population with common fields.\n   *\n   * @param deltaLog The delta log instance\n   * @param opType The operation type for the usage log\n   * @param snapshot The table snapshot\n   * @param catalogTableOpt Optional catalog table information\n   * @param commitCoordinatorOpt Optional commit coordinator instance\n   * @param includeStackTrace Whether to include stack trace in the log\n   * @param includeAdditionalDiagnostics Whether to include additional diagnostic information\n   */\n  private def recordCommitCoordinatorPopulationUsageLog(\n      deltaLog: DeltaLog,\n      opType: String,\n      snapshot: Snapshot,\n      catalogTableOpt: Option[CatalogTable],\n      commitCoordinatorOpt: Option[CommitCoordinatorClient] = None,\n      includeStackTrace: Boolean = false,\n      includeAdditionalDiagnostics: Boolean = false): Unit = {\n    // Base data that's common to *all* usage logs for tccc population.\n    val baseData = Map(\n      \"version\" -> snapshot.version.toString,\n      \"path\" -> snapshot.path.toString\n    )\n\n    val catalogData = catalogTableOpt.map { catalogTable =>\n      Map(\n        \"catalogTable.identifier\" -> catalogTable.identifier.toString,\n        \"catalogTable.tableType\" -> catalogTable.tableType.toString\n      )\n    }.getOrElse(Map.empty[String, String])\n\n    val coordinatorData = commitCoordinatorOpt.map { cc =>\n      Map(\"commitCoordinator.getClass\" -> cc.getClass.getName)\n    }.getOrElse(Map.empty[String, String])\n\n    val stackTraceData = if (includeStackTrace) {\n      Map(\"stackTrace\" -> Thread.currentThread().getStackTrace.tail.mkString(\"\\n\\t\"))\n    } else {\n      Map.empty[String, String]\n    }\n\n    val diagnosticData = if (includeAdditionalDiagnostics) {\n      Map(\n        \"latestCheckpointVersion\" -> snapshot.checkpointProvider.version,\n        \"checksumOpt\" -> snapshot.checksumOpt,\n        \"properties\" -> snapshot.getProperties,\n        \"logStore\" -> snapshot.deltaLog.store.getClass.getName\n      )\n    } else {\n      Map.empty[String, Any]\n    }\n\n    val allData = baseData ++ catalogData ++ coordinatorData ++ stackTraceData ++ diagnosticData\n\n    recordDeltaEvent(\n      deltaLog = deltaLog,\n      opType = opType,\n      data = allData\n    )\n  }\n}\n\nobject CoordinatedCommitsUtils extends DeltaLogging {\n\n  /**\n   * Returns the [[CommitCoordinatorClient.getCommits]] response for the given startVersion and\n   * versionToLoad.\n   */\n  def getCommitsFromCommitCoordinatorWithUsageLogs(\n      deltaLog: DeltaLog,\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      catalogTableOpt: Option[CatalogTable],\n      startVersion: Long,\n      versionToLoad: Option[Long],\n      isAsyncRequest: Boolean): JGetCommitsResponse = {\n    recordFrameProfile(\"DeltaLog\", s\"CommitCoordinatorClient.getCommits.async=$isAsyncRequest\") {\n      val startTimeMs = System.currentTimeMillis()\n      def recordEvent(additionalData: Map[String, Any]): Unit = {\n        recordDeltaEvent(\n          deltaLog,\n          opType = CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_CLIENT_GET_COMMITS,\n          data = Map(\n            \"startVersion\" -> startVersion,\n            \"versionToLoad\" -> versionToLoad.getOrElse(-1L),\n            \"async\" -> isAsyncRequest.toString,\n            \"durationMs\" -> (System.currentTimeMillis() - startTimeMs).toString\n          ) ++ additionalData\n        )\n      }\n\n      try {\n        val response =\n          tableCommitCoordinatorClient.getCommits(\n            catalogTableOpt.map(_.identifier),\n            Some(startVersion),\n            endVersion = versionToLoad\n          )\n        val additionalEventData = Map(\n          \"responseCommitsSize\" -> response.getCommits.size,\n          \"responseLatestTableVersion\" -> response.getLatestTableVersion)\n        recordEvent(additionalEventData)\n        response\n      } catch {\n        case NonFatal(e) =>\n          recordEvent(\n            Map(\n              \"exceptionClass\" -> e.getClass.getName,\n              \"exceptionString\" -> Utils.exceptionString(e)\n            )\n          )\n          throw e\n      }\n    }\n  }\n\n  /**\n   * Returns an iterator of commit files starting from startVersion.\n   * If the iterator is consumed beyond what the file system listing shows, this method do a\n   * deltaLog.update() to find the latest version and returns listing results upto that version.\n   *\n   * @return an iterator of (file status, version) pair corresponding to commit files\n   */\n  def commitFilesIterator(\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      startVersion: Long): Iterator[(FileStatus, Long)] = {\n\n    def listDeltas(startVersion: Long, endVersion: Option[Long]): Iterator[(FileStatus, Long)] = {\n      deltaLog\n        .listFrom(startVersion)\n        .collect { case DeltaFile(fileStatus, version) => (fileStatus, version) }\n        .takeWhile { case (_, version) => endVersion.forall(version <= _) }\n    }\n\n    var maxVersionSeen = startVersion - 1\n    val listedDeltas = listDeltas(startVersion, endVersion = None).filter { case (_, version) =>\n      maxVersionSeen = math.max(maxVersionSeen, version)\n      true\n    }\n\n    def tailFromSnapshot(): Iterator[(FileStatus, Long)] = {\n      val currentSnapshotInDeltaLog = deltaLog.unsafeVolatileSnapshot\n      if (currentSnapshotInDeltaLog.version == maxVersionSeen &&\n           (currentSnapshotInDeltaLog.tableCommitCoordinatorClientOpt.isEmpty &&\n           !currentSnapshotInDeltaLog.isCatalogOwned)) {\n        // If the last version in listing is same as the `unsafeVolatileSnapshot` in deltaLog and\n        // if that snapshot doesn't have a commit-coordinator => this table was not a\n        // coordinated-commits table at the time of listing. This is because the commit which\n        // converts the file-system table to a coordinated-commits table must be a file-system\n        // commit as per the spec.\n        return Iterator.empty\n      }\n\n      val endSnapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n      // No need to worry if we already reached the end\n      if (maxVersionSeen >= endSnapshot.version) {\n        return Iterator.empty\n      }\n      val unbackfilledDeltas = endSnapshot.logSegment.deltas.collect {\n        case UnbackfilledDeltaFile(fileStatus, version, _) if version > maxVersionSeen =>\n          (fileStatus, version)\n      }\n      // Check for a gap between listing and commit files in the logsegment\n      val gapListing = unbackfilledDeltas.headOption match {\n        case Some((_, version)) if maxVersionSeen + 1 < version =>\n          listDeltas(maxVersionSeen + 1, Some(version))\n        // no gap before\n        case _ => Iterator.empty\n      }\n      gapListing ++ unbackfilledDeltas\n    }\n\n    // We want to avoid invoking `tailFromSnapshot()` as it internally calls deltaLog.update()\n    // So we append the two iterators and the second iterator will be created only if the first one\n    // is exhausted.\n    Iterator(1, 2).flatMap {\n      case 1 => listedDeltas\n      case 2 => tailFromSnapshot()\n    }\n  }\n\n  def getCommitCoordinatorClient(\n      spark: SparkSession,\n      deltaLog: DeltaLog, // Used for logging\n      metadata: Metadata,\n      protocol: Protocol,\n      failIfImplUnavailable: Boolean): Option[CommitCoordinatorClient] = {\n    metadata.coordinatedCommitsCoordinatorName.flatMap { commitCoordinatorStr =>\n      assert(protocol.isFeatureSupported(CoordinatedCommitsTableFeature),\n        \"coordinated commits table feature is not supported\")\n      val coordinatorConf = metadata.coordinatedCommitsCoordinatorConf\n      val coordinatorOpt = CommitCoordinatorProvider.getCommitCoordinatorClientOpt(\n        commitCoordinatorStr, coordinatorConf, spark)\n      if (coordinatorOpt.isEmpty) {\n        recordDeltaEvent(\n          deltaLog,\n          CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_MISSING_IMPLEMENTATION,\n          data = Map(\n            \"commitCoordinatorName\" -> commitCoordinatorStr,\n            \"registeredCommitCoordinators\" ->\n              CommitCoordinatorProvider.getRegisteredCoordinatorNames.mkString(\", \"),\n            \"commitCoordinatorConf\" -> coordinatorConf,\n            \"failIfImplUnavailable\" -> failIfImplUnavailable.toString\n          )\n        )\n        if (failIfImplUnavailable) {\n          throw new IllegalArgumentException(\n            s\"Unknown commit-coordinator: $commitCoordinatorStr\")\n        }\n      }\n      coordinatorOpt\n    }\n  }\n\n  /**\n   * Get the table commit coordinator client from the provided snapshot descriptor.\n   * Returns None if either this is not a coordinated-commits table. Also returns None when\n   * `failIfImplUnavailable` is false and the commit-coordinator implementation is not available.\n   */\n  def getTableCommitCoordinator(\n      spark: SparkSession,\n      deltaLog: DeltaLog, // Used for logging\n      snapshotDescriptor: SnapshotDescriptor,\n      failIfImplUnavailable: Boolean): Option[TableCommitCoordinatorClient] = {\n    getCommitCoordinatorClient(\n      spark,\n      deltaLog,\n      snapshotDescriptor.metadata,\n      snapshotDescriptor.protocol,\n      failIfImplUnavailable).map {\n      commitCoordinator =>\n        TableCommitCoordinatorClient(\n          commitCoordinator,\n          snapshotDescriptor.deltaLog.logPath,\n          snapshotDescriptor.metadata.coordinatedCommitsTableConf,\n          snapshotDescriptor.deltaLog.newDeltaHadoopConf(),\n          snapshotDescriptor.deltaLog.store\n        )\n    }\n  }\n\n  def getCoordinatedCommitsConfs(metadata: Metadata): (Option[String], Map[String, String]) = {\n    metadata.coordinatedCommitsCoordinatorName match {\n      case Some(name) => (Some(name), metadata.coordinatedCommitsCoordinatorConf)\n      case None => (None, Map.empty)\n    }\n  }\n\n  val TABLE_PROPERTY_CONFS = Seq(\n    DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME,\n    DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF,\n    DeltaConfigs.COORDINATED_COMMITS_TABLE_CONF)\n\n  val ICT_TABLE_PROPERTY_CONFS = Seq(\n    DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED,\n    DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n    DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP)\n\n  /**\n   * The main table properties used to instantiate a TableCommitCoordinatorClient.\n   */\n  val TABLE_PROPERTY_KEYS: Seq[String] = TABLE_PROPERTY_CONFS.map(_.key)\n\n  /**\n   * The main ICT table properties used as dependencies for Coordinated Commits.\n   */\n  val ICT_TABLE_PROPERTY_KEYS: Seq[String] = ICT_TABLE_PROPERTY_CONFS.map(_.key)\n\n  /**\n   * Returns true if any CoordinatedCommits-related table properties is present in the metadata.\n   */\n  def tablePropertiesPresent(metadata: Metadata): Boolean = {\n    TABLE_PROPERTY_KEYS.exists(metadata.configuration.contains)\n  }\n\n  /**\n   * Returns true if the snapshot is backed by unbackfilled commits.\n   */\n  def unbackfilledCommitsPresent(snapshot: Snapshot): Boolean = {\n    snapshot.logSegment.deltas.exists {\n      case FileNames.UnbackfilledDeltaFile(_, _, _) => true\n      case _ => false\n    } && !snapshot.allCommitsBackfilled\n  }\n\n  /**\n   * This method takes care of backfilling any unbackfilled delta files when coordinated commits is\n   * not enabled on the table (i.e. commit-coordinator is not present) but there are still\n   * unbackfilled delta files in the table. This can happen if an error occurred during the CC -> FS\n   * commit where the commit-coordinator was able to register the downgrade commit but it failed to\n   * backfill it. This method must be invoked before doing the next commit as otherwise there will\n   * be a gap in the backfilled commit sequence.\n   */\n  def backfillWhenCoordinatedCommitsDisabled(snapshot: Snapshot): Unit = {\n    if (snapshot.getTableCommitCoordinatorForWrites.nonEmpty || snapshot.isCatalogOwned) {\n      // Coordinated commits or Catalog-owned is enabled on the table. Don't backfill\n      // as backfills are managed by commit-coordinators.\n      return\n    }\n    val unbackfilledFilesAndVersions = snapshot.logSegment.deltas.collect {\n      case UnbackfilledDeltaFile(unbackfilledDeltaFile, version, _) =>\n        (unbackfilledDeltaFile, version)\n    }\n    if (unbackfilledFilesAndVersions.isEmpty) return\n    // Coordinated commits are disabled on the table but the table still has un-backfilled files.\n    val deltaLog = snapshot.deltaLog\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n    val fs = deltaLog.logPath.getFileSystem(hadoopConf)\n    val overwrite = !deltaLog.store.isPartialWriteVisible(deltaLog.logPath, hadoopConf)\n    var numAlreadyBackfilledFiles = 0L\n    unbackfilledFilesAndVersions.foreach { case (unbackfilledDeltaFile, version) =>\n      val backfilledFilePath = FileNames.unsafeDeltaFile(deltaLog.logPath, version)\n      if (!fs.exists(backfilledFilePath)) {\n        val actionsIter = deltaLog.store.readAsIterator(unbackfilledDeltaFile.getPath, hadoopConf)\n        deltaLog.store.write(\n          backfilledFilePath,\n          actionsIter,\n          overwrite,\n          hadoopConf)\n        logInfo(log\"Delta file ${MDC(DeltaLogKeys.PATH, unbackfilledDeltaFile.getPath.toString)} \" +\n          log\"backfilled to path ${MDC(DeltaLogKeys.PATH2, backfilledFilePath.toString)}.\")\n      } else {\n        numAlreadyBackfilledFiles += 1\n        logInfo(log\"Delta file ${MDC(DeltaLogKeys.PATH, unbackfilledDeltaFile.getPath.toString)} \" +\n          log\"already backfilled.\")\n      }\n    }\n    recordDeltaEvent(\n      deltaLog,\n      opType = \"delta.coordinatedCommits.backfillWhenCoordinatedCommitsSupportedAndDisabled\",\n      data = Map(\n        \"numUnbackfilledFiles\" -> unbackfilledFilesAndVersions.size,\n        \"unbackfilledFiles\" -> unbackfilledFilesAndVersions.map(_._1.getPath.toString),\n        \"numAlreadyBackfilledFiles\" -> numAlreadyBackfilledFiles\n      )\n    )\n  }\n\n  /**\n   * Returns the last backfilled file in the given list of `deltas` if it exists. This could be\n   * 1. A backfilled delta\n   * 2. A minor compaction\n   */\n  def getLastBackfilledFile(deltas: Seq[FileStatus]): Option[FileStatus] = {\n    var maxFile: Option[FileStatus] = None\n    deltas.foreach {\n      case BackfilledDeltaFile(f, _) => maxFile = Some(f)\n      case CompactedDeltaFile(f, _, _) => maxFile = Some(f)\n      case _ => // do nothing\n    }\n    maxFile\n  }\n\n  /**\n   * Extracts the Coordinated Commits configurations from the provided properties.\n   */\n  def getExplicitCCConfigurations(\n      properties: Map[String, String]): Map[String, String] = {\n    properties.filter { case (k, _) => TABLE_PROPERTY_KEYS.contains(k) }\n  }\n\n  /**\n   * Extracts the ICT configurations from the provided properties.\n   */\n  def getExplicitICTConfigurations(properties: Map[String, String]): Map[String, String] = {\n    properties.filter { case (k, _) => ICT_TABLE_PROPERTY_KEYS.contains(k) }\n  }\n\n  /**\n   * Extracts the explicit QoL configurations from the provided properties.\n   *\n   * These are preserved across catalog-managed REPLACE when the existing table already has the\n   * QoL defaults materialized in metadata, so a no-op REPLACE does not accidentally drop them\n   * while rebuilding the configuration map.\n   */\n  def getExplicitQoLConfigurations(properties: Map[String, String]): Map[String, String] = {\n    val qolKeys = CatalogOwnedTableUtils.QOL_TABLE_FEATURES_AND_PROPERTIES.map(_._2.key).toSet\n    properties.filter { case (k, _) => qolKeys.contains(k) }\n  }\n\n  /**\n   * Fetches the SparkSession default configurations for ICT. The `withDefaultKey`\n   * flag controls whether the keys in the returned map should have the default prefix or not.\n   */\n  def getDefaultICTConfigurations(\n      spark: SparkSession, withDefaultKey: Boolean = false): Map[String, String] = {\n    ICT_TABLE_PROPERTY_CONFS.flatMap { conf =>\n      spark.conf.getOption(conf.defaultTablePropertyKey).map { value =>\n        val finalKey = if (withDefaultKey) conf.defaultTablePropertyKey else conf.key\n        finalKey -> value\n      }\n    }.toMap\n  }\n\n  /**\n   * Fetches the SparkSession default configurations for Coordinated Commits. The `withDefaultKey`\n   * flag controls whether the keys in the returned map should have the default prefix or not.\n   * For example, if property 'coordinatedCommits.commitCoordinator-preview' is set to 'dynamodb'\n   * in SparkSession default, then\n   *\n   *   - fetchDefaultCoordinatedCommitsConfigurations(spark) =>\n   *       Map(\"delta.coordinatedCommits.commitCoordinator-preview\" -> \"dynamodb\")\n   *\n   *   - fetchDefaultCoordinatedCommitsConfigurations(spark, withDefaultKey = true) =>\n   *       Map(\"spark.databricks.delta.properties.defaults\n   *            .coordinatedCommits.commitCoordinator-preview\" -> \"dynamodb\")\n   */\n  def getDefaultCCConfigurations(\n      spark: SparkSession, withDefaultKey: Boolean = false): Map[String, String] = {\n    TABLE_PROPERTY_CONFS.flatMap { conf =>\n      spark.conf.getOption(conf.defaultTablePropertyKey).map { value =>\n        val finalKey = if (withDefaultKey) conf.defaultTablePropertyKey else conf.key\n        finalKey -> value\n      }\n    }.toMap\n  }\n\n  /**\n   * Verifies that the properties contain exactly the Coordinator Name and Coordinator Conf.\n   * If `fromDefault` is true, then the properties have keys with the default prefix.\n   */\n  private def verifyContainsOnlyCoordinatorNameAndConf(\n      properties: Map[String, String],\n      command: String,\n      fromDefault: Boolean): Unit = {\n    Seq(DeltaConfigs.COORDINATED_COMMITS_TABLE_CONF).foreach { conf =>\n      if (fromDefault) {\n        if (properties.contains(conf.defaultTablePropertyKey)) {\n          throw new DeltaIllegalArgumentException(\n            errorClass = \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_SESSION\",\n            messageParameters = Array(\n              command, conf.defaultTablePropertyKey, conf.defaultTablePropertyKey))\n        }\n      } else {\n        if (properties.contains(conf.key)) {\n          throw new DeltaIllegalArgumentException(\n            errorClass = \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\",\n            messageParameters = Array(command, conf.key))\n        }\n      }\n    }\n    Seq(\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME,\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF).foreach { conf =>\n      if (fromDefault) {\n        if (!properties.contains(conf.defaultTablePropertyKey)) {\n          throw new DeltaIllegalArgumentException(\n            errorClass = \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_SESSION\",\n            messageParameters = Array(command, conf.defaultTablePropertyKey))\n        }\n      } else {\n        if (!properties.contains(conf.key)) {\n          throw new DeltaIllegalArgumentException(\n            errorClass = \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_COMMAND\",\n            messageParameters = Array(command, conf.key))\n        }\n      }\n    }\n  }\n\n  /**\n   * Verifies that the property keys do not contain any ICT dependencies for Coordinated Commits.\n   */\n  private def verifyNotContainsICTConfigurations(\n      propKeys: Seq[String], command: String, errorClass: String): Unit = {\n    ICT_TABLE_PROPERTY_KEYS.foreach { key =>\n      if (propKeys.contains(key)) {\n        throw new DeltaIllegalArgumentException(\n          errorClass,\n          messageParameters = Array(command))\n      }\n    }\n  }\n\n  /**\n   * Validates the Coordinated Commits configurations in explicit command overrides for\n   * `AlterTableSetPropertiesDeltaCommand`.\n   *\n   * If the table already has Coordinated Commits configurations present, then we do not allow\n   * users to override them via `ALTER TABLE t SET TBLPROPERTIES ...`. Users must downgrade the\n   * table and then upgrade it with the new Coordinated Commits configurations.\n   * If the table is a Coordinated Commits table or will be one via this ALTER command, then we\n   * do not allow users to disable any ICT properties that Coordinated Commits depends on.\n   */\n  def validateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n      existingConfs: Map[String, String],\n      propertyOverrides: Map[String, String]): Unit = {\n    val existingCoordinatedCommitsConfs = getExplicitCCConfigurations(existingConfs)\n    val coordinatedCommitsOverrides = getExplicitCCConfigurations(propertyOverrides)\n    if (coordinatedCommitsOverrides.nonEmpty) {\n      if (existingCoordinatedCommitsConfs.nonEmpty) {\n        throw new DeltaIllegalArgumentException(\n          \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\",\n          Array(\"ALTER\"))\n      }\n      verifyNotContainsICTConfigurations(propertyOverrides.keys.toSeq, command = \"ALTER\",\n        errorClass = \"DELTA_CANNOT_SET_COORDINATED_COMMITS_DEPENDENCIES\")\n      verifyContainsOnlyCoordinatorNameAndConf(\n        coordinatedCommitsOverrides, command = \"ALTER\", fromDefault = false)\n    }\n    if (existingCoordinatedCommitsConfs.nonEmpty) {\n      verifyNotContainsICTConfigurations(propertyOverrides.keys.toSeq, command = \"ALTER\",\n        errorClass = \"DELTA_CANNOT_MODIFY_COORDINATED_COMMITS_DEPENDENCIES\")\n    }\n  }\n\n  /**\n   * Validates the configurations to unset for `AlterTableUnsetPropertiesDeltaCommand`.\n   *\n   * If the table already has Coordinated Commits configurations present, then we do not allow users\n   * to unset them via `ALTER TABLE t UNSET TBLPROPERTIES ...`. Users could only downgrade the table\n   * via `ALTER TABLE t DROP FEATURE ...`. We also do not allow users to unset any ICT properties\n   * that Coordinated Commits depends on.\n   */\n  def validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n      existingConfs: Map[String, String],\n      propKeysToUnset: Seq[String]): Unit = {\n    // If the table does not have any Coordinated Commits configurations, then we do not check the\n    // properties to unset. This is because unsetting non-existent entries would either be caught\n    // earlier (without `IF EXISTS`) or simply be a no-op (with `IF EXISTS`). Thus, we ignore them\n    // instead of throwing an exception.\n    if (getExplicitCCConfigurations(existingConfs).nonEmpty) {\n      if (propKeysToUnset.exists(TABLE_PROPERTY_KEYS.contains)) {\n        throw new DeltaIllegalArgumentException(\n          \"DELTA_CANNOT_UNSET_COORDINATED_COMMITS_CONFS\",\n          Array.empty)\n      }\n      verifyNotContainsICTConfigurations(propKeysToUnset, command = \"ALTER\",\n        errorClass = \"DELTA_CANNOT_MODIFY_COORDINATED_COMMITS_DEPENDENCIES\")\n    }\n  }\n\n  /**\n   * Validates the Coordinated Commits configurations in explicit command overrides and default\n   * SparkSession properties for `CreateDeltaTableCommand`.\n   * See `validateConfigurationsForCreateDeltaTableCommandImpl` for details.\n   */\n  def validateConfigurationsForCreateDeltaTableCommand(\n      spark: SparkSession,\n      tableExists: Boolean,\n      query: Option[LogicalPlan],\n      catalogTableProperties: Map[String, String]): Unit = {\n    val (command, propertyOverrides) = query match {\n      // For CLONE, we cannot use the properties from the catalog table, because they are already\n      // the result of merging the source table properties with the overrides, but we do not\n      // consider the source table properties for Coordinated Commits.\n      case Some(cmd: CloneTableCommand) =>\n        (if (tableExists) \"REPLACE with CLONE\" else \"CREATE with CLONE\",\n          cmd.tablePropertyOverrides)\n      case _ => (if (tableExists) \"REPLACE\" else \"CREATE\", catalogTableProperties)\n    }\n    validateConfigurationsForCreateDeltaTableCommandImpl(\n      spark, propertyOverrides, tableExists, command)\n  }\n\n  /**\n   * Validates the Coordinated Commits configurations for the table.\n   *   - If the table already exists, the explicit command property overrides must not contain any\n   *     Coordinated Commits configurations.\n   *   - If the table does not exist, the explicit command property overrides must contain exactly\n   *     the Coordinator Name and Coordinator Conf, and no Table Conf. Default configurations are\n   *     checked similarly if none of the three properties is present in explicit overrides.\n   */\n  private[delta] def validateConfigurationsForCreateDeltaTableCommandImpl(\n      spark: SparkSession,\n      propertyOverrides: Map[String, String],\n      tableExists: Boolean,\n      command: String): Unit = {\n    val coordinatedCommitsConfs = getExplicitCCConfigurations(propertyOverrides)\n    if (tableExists) {\n      if (coordinatedCommitsConfs.nonEmpty) {\n        throw new DeltaIllegalArgumentException(\n          \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\",\n          Array(command))\n      }\n    } else {\n      if (coordinatedCommitsConfs.nonEmpty) {\n        verifyContainsOnlyCoordinatorNameAndConf(\n          coordinatedCommitsConfs, command, fromDefault = false)\n      } else {\n        val defaultCoordinatedCommitsConfs = getDefaultCCConfigurations(\n          spark, withDefaultKey = true)\n        if (defaultCoordinatedCommitsConfs.nonEmpty) {\n          verifyContainsOnlyCoordinatorNameAndConf(\n            defaultCoordinatedCommitsConfs, command, fromDefault = true)\n        }\n      }\n    }\n  }\n\n  /**\n   * Converts a given Spark [[CatalystTableIdentifier]] to Coordinated Commits [[TableIdentifier]]\n   */\n  def toCCTableIdentifier(\n      catalystTableIdentifierOpt: Option[CatalystTableIdentifier]): Optional[TableIdentifier] = {\n    catalystTableIdentifierOpt.map { catalystTableIdentifier =>\n      val namespace =\n        catalystTableIdentifier.catalog.toSeq ++\n          catalystTableIdentifier.database.toSeq\n      new TableIdentifier(namespace.toArray, catalystTableIdentifier.table)\n    }.map(Optional.of[TableIdentifier]).getOrElse(Optional.empty[TableIdentifier])\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/InMemoryCommitCoordinator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.util.{Map => JMap, Optional}\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.locks.ReentrantReadWriteLock\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{\n  Commit => JCommit,\n  CommitCoordinatorClient,\n  CommitFailedException => JCommitFailedException,\n  CommitResponse,\n  GetCommitsResponse => JGetCommitsResponse,\n  TableDescriptor,\n  TableIdentifier\n}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\n\nclass InMemoryCommitCoordinator(val batchSize: Long)\n  extends AbstractBatchBackfillingCommitCoordinatorClient {\n\n  /**\n   * @param maxCommitVersion represents the max commit version known for the table. This is\n   *                         initialized at the time of pre-registration and updated whenever a\n   *                         commit is successfully added to the commit-coordinator.\n   * @param active represents whether this commit-coordinator has ratified any commit or not.\n   * |----------------------------|------------------|---------------------------|\n   * |        State               | maxCommitVersion |          active           |\n   * |----------------------------|------------------|---------------------------|\n   * | Table is pre-registered    | currentVersion+1 |          false            |\n   * |----------------------------|------------------|---------------------------|\n   * | Table is pre-registered    |       X          |          true             |\n   * | and more commits are done  |                  |                           |\n   * |----------------------------|------------------|---------------------------|\n   */\n  private[coordinatedcommits] class PerTableData(\n    var maxCommitVersion: Long = -1,\n    var active: Boolean = false\n  ) {\n    def updateLastRatifiedCommit(commitVersion: Long): Unit = {\n      active = true\n      maxCommitVersion = commitVersion\n    }\n\n    /**\n     * Returns the last ratified commit version for the table. If no commits have been done from\n     * commit-coordinator yet, returns -1.\n     */\n    def lastRatifiedCommitVersion: Long = if (!active) -1 else maxCommitVersion\n\n    // Map from version to Commit data\n    val commitsMap: mutable.SortedMap[Long, JCommit] = mutable.SortedMap.empty\n    // We maintain maxCommitVersion explicitly since commitsMap might be empty\n    // if all commits for a table have been backfilled.\n    val lock: ReentrantReadWriteLock = new ReentrantReadWriteLock()\n  }\n\n  private[coordinatedcommits] val perTableMap = new ConcurrentHashMap[Path, PerTableData]()\n\n  private[coordinatedcommits] def withWriteLock[T](logPath: Path)(operation: => T): T = {\n    val tableData = perTableMap.computeIfAbsent(logPath, _ => new PerTableData())\n    val lock = tableData.lock.writeLock()\n    lock.lock()\n    try {\n      operation\n    } finally {\n      lock.unlock()\n    }\n  }\n\n  private[coordinatedcommits] def withReadLock[T](logPath: Path)(operation: => T): T = {\n    val tableData = perTableMap.computeIfAbsent(logPath, _ => new PerTableData())\n    val lock = tableData.lock.readLock()\n    lock.lock()\n    try {\n      operation\n    } finally {\n      lock.unlock()\n    }\n  }\n\n  /**\n   * This method acquires a write lock, validates the commit version is next in line,\n   * updates commit maps, and releases the lock.\n   *\n   * @throws CommitFailedException if the commit version is not the expected next version,\n   *                               indicating a version conflict.\n   */\n  private[delta] def commitImpl(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      logPath: Path,\n      coordinatedCommitsTableConf: Map[String, String],\n      commitVersion: Long,\n      commitFile: FileStatus,\n      commitTimestamp: Long): CommitResponse = {\n    addToMap(logPath, commitVersion, commitFile, commitTimestamp)\n  }\n\n  private[sql] def addToMap(\n      logPath: Path,\n      commitVersion: Long,\n      commitFile: FileStatus,\n      commitTimestamp: Long): CommitResponse = {\n    withWriteLock[CommitResponse](logPath) {\n      val tableData = perTableMap.get(logPath)\n      val expectedVersion = tableData.maxCommitVersion + 1\n      if (commitVersion != expectedVersion && tableData.maxCommitVersion != -1) {\n        throw new JCommitFailedException(\n          commitVersion < expectedVersion,\n          commitVersion < expectedVersion,\n          s\"Commit version $commitVersion is not valid. Expected version: $expectedVersion.\")\n      }\n\n      val commit = new JCommit(commitVersion, commitFile, commitTimestamp)\n      tableData.commitsMap(commitVersion) = commit\n      tableData.updateLastRatifiedCommit(commitVersion)\n\n      logInfo(log\"Added commit file ${MDC(DeltaLogKeys.PATH, commitFile.getPath)} \" +\n        log\"to commit-coordinator.\")\n      new CommitResponse(commit)\n    }\n  }\n\n  override def getCommits(\n      tableDesc: TableDescriptor,\n      startVersion: java.lang.Long,\n      endVersion: java.lang.Long): JGetCommitsResponse = {\n    withReadLock[JGetCommitsResponse](tableDesc.getLogPath) {\n      val startVersionOpt: Option[Long] = Option(startVersion).map(_.toLong)\n      val endVersionOpt: Option[Long] = Option(endVersion).map(_.toLong)\n      val tableData = perTableMap.get(tableDesc.getLogPath)\n      val effectiveStartVersion = startVersionOpt.getOrElse(0L)\n      // Calculate the end version for the range, or use the last key if endVersion is not provided\n      val effectiveEndVersion = endVersionOpt.getOrElse(\n        tableData.commitsMap.lastOption.map(_._1).getOrElse(effectiveStartVersion))\n      val commitsInRange = tableData.commitsMap.range(\n        effectiveStartVersion, effectiveEndVersion + 1)\n      new JGetCommitsResponse(\n        commitsInRange.values.toSeq.asJava, tableData.lastRatifiedCommitVersion)\n    }\n  }\n\n  override protected[sql] def registerBackfill(\n      logPath: Path,\n      backfilledVersion: Long): Unit = {\n    withWriteLock(logPath) {\n      val tableData = perTableMap.get(logPath)\n      if (backfilledVersion > tableData.lastRatifiedCommitVersion) {\n        throw new IllegalArgumentException(\n          s\"Unexpected backfill version: $backfilledVersion. \" +\n            s\"Max backfill version: ${tableData.maxCommitVersion}\")\n      }\n      // Remove keys with versions less than or equal to 'untilVersion'\n      val versionsToRemove = tableData.commitsMap.keys.takeWhile(_ <= backfilledVersion).toList\n      versionsToRemove.foreach(tableData.commitsMap.remove)\n    }\n  }\n\n  override def registerTable(\n      logPath: Path,\n      tableIdentifier: Optional[TableIdentifier],\n      currentVersion: Long,\n      currentMetadata: AbstractMetadata,\n      currentProtocol: AbstractProtocol): JMap[String, String] = {\n    val newPerTableData = new PerTableData(currentVersion + 1)\n    perTableMap.compute(logPath, (_, existingData) => {\n      if (existingData != null) {\n        if (existingData.lastRatifiedCommitVersion != -1) {\n          throw new IllegalStateException(\n            s\"Table $logPath already exists in the commit-coordinator.\")\n        }\n        // If lastRatifiedCommitVersion is -1 i.e. the commit-coordinator has never attempted any\n        // commit for this table => this table was just pre-registered. If there is another\n        // pre-registration request for an older version, we reject it and table can't go backward.\n        if (currentVersion < existingData.maxCommitVersion) {\n          throw new IllegalStateException(\n            s\"Table $logPath already registered with commit-coordinator\")\n        }\n      }\n      newPerTableData\n    })\n    Map.empty[String, String].asJava\n  }\n\n  def dropTable(logPath: Path): Unit = {\n    withWriteLock(logPath) {\n      perTableMap.remove(logPath)\n    }\n  }\n\n  override def semanticEquals(other: CommitCoordinatorClient): Boolean = this == other\n\n  private[delta] def removeCommitTestOnly(\n      logPath: Path,\n      commitVersion: Long\n  ): Unit = {\n    val tableData = perTableMap.get(logPath)\n    tableData.commitsMap.remove(commitVersion)\n    if (commitVersion == tableData.maxCommitVersion) {\n      tableData.maxCommitVersion -= 1\n    }\n  }\n}\n\n/**\n * The [[InMemoryCommitCoordinatorBuilder]] class is responsible for creating singleton instances of\n * [[InMemoryCommitCoordinator]] with the specified batchSize.\n */\ncase class InMemoryCommitCoordinatorBuilder(batchSize: Long)\n    extends CatalogOwnedCommitCoordinatorBuilder {\n  private lazy val inMemoryStore = new InMemoryCommitCoordinator(batchSize)\n\n  /** Name of the commit-coordinator */\n  def getName: String = \"in-memory\"\n\n  /** Returns a commit-coordinator based on the given conf */\n  def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n    inMemoryStore\n  }\n\n  /** Returns a commit-coordinator based on the given catalog name */\n  def buildForCatalog(spark: SparkSession, catalogName: String): CommitCoordinatorClient = {\n    inMemoryStore\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/InMemoryUCClient.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.lang.{Long => JLong}\nimport java.net.URI\nimport java.util.Optional\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport io.delta.storage.commit.{Commit => JCommit, GetCommitsResponse => JGetCommitsResponse}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport io.delta.storage.commit.uccommitcoordinator.UCClient\nimport io.delta.storage.commit.uniform.UniformMetadata\n\n/**\n * An in-memory implementation of [[UCClient]] for testing purposes.\n * This implementation simulates Unity Catalog operations without actually connecting to a remote\n * service. It maintains all state in memory in [[InMemoryUCCommitCoordinator]]\n *\n * This class provides a lightweight way to test Delta table operations that would\n * normally require interaction with the Unity Catalog.\n *\n * Example usage:\n * {{{\n * val metastoreId = \"test-metastore\"\n * val ucCommitCoordinator = new InMemoryUCCommitCoordinator()\n * val client = new InMemoryUCClient(metastoreId, ucCommitCoordinator)\n *\n * // Use the client for testing\n * val getCommitsResponse = client.getCommits(\n *     \"tableId\",\n *     new URI(\"tableUri\"),\n *     Optional.empty(),\n *     Optional.empty())\n * }}}\n *\n * @param metastoreId The identifier for the simulated metastore\n * @param ucCommitCoordinator The in-memory coordinator to handle commit operations\n */\nclass InMemoryUCClient(\n    metastoreId: String,\n    ucCommitCoordinator: InMemoryUCCommitCoordinator) extends UCClient {\n\n  override def getMetastoreId: String = metastoreId\n\n  override def commit(\n      tableId: String,\n      tableUri: URI,\n      commit: Optional[JCommit],\n      lastKnownBackfilledVersion: Optional[JLong],\n      disown: Boolean,\n      newMetadata: Optional[AbstractMetadata],\n      newProtocol: Optional[AbstractProtocol],\n      uniform: Optional[UniformMetadata] = Optional.empty()): Unit = {\n    ucCommitCoordinator.commitToCoordinator(\n      tableId,\n      tableUri,\n      Option(commit.orElse(null)).map(_.getFileStatus.getPath.getName),\n      Option(commit.orElse(null)).map(_.getVersion),\n      Option(commit.orElse(null)).map(_.getFileStatus.getLen),\n      Option(commit.orElse(null)).map(_.getFileStatus.getModificationTime),\n      Option(commit.orElse(null)).map(_.getCommitTimestamp),\n      Option(lastKnownBackfilledVersion.orElse(null)).map(_.toLong),\n      disown,\n      Option(newProtocol.orElse(null)).map(_.asInstanceOf[Protocol]),\n      Option(newMetadata.orElse(null)).map(_.asInstanceOf[Metadata]))\n  }\n\n  override def getCommits(\n      tableId: String,\n      tableUri: URI,\n      startVersion: Optional[JLong],\n      endVersion: Optional[JLong]): JGetCommitsResponse = {\n    ucCommitCoordinator.getCommitsFromCoordinator(\n      tableId,\n      tableUri,\n      Option(startVersion.orElse(null)).map(_.toLong),\n      Option(endVersion.orElse(null)).map(_.toLong))\n  }\n\n  override def close(): Unit = {}\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/InMemoryUCCommitCoordinator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.io.IOException\nimport java.net.URI\nimport java.util.UUID\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.locks.ReentrantReadWriteLock\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DeltaTableUtils\nimport org.apache.spark.sql.delta.util.FileNames\nimport io.delta.storage.commit.{\n  Commit => JCommit,\n  CommitFailedException => JCommitFailedException,\n  GetCommitsResponse => JGetCommitsResponse\n}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport io.delta.storage.commit.uccommitcoordinator.{CommitLimitReachedException => JCommitLimitReachedException, InvalidTargetTableException => JInvalidTargetTableException}\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\n\nfinal object UCCoordinatedCommitsRequestType extends Enumeration {\n  type UCCoordinatedCommitsRequestType = Value\n  val COMMIT = Value\n  val GET_COMMITS = Value\n}\n\n/**\n * A mock UC commit coordinator for testing purposes.\n */\nclass InMemoryUCCommitCoordinator {\n\n  /**\n   * Represents the data associated with a table.\n   * `ucCommits` mimics the underlying list for the commit files.\n   */\n  private class PerTableData(val path: URI) {\n\n    /**\n     * Represents a UC commit record.\n     * @param commit represents the commit itself.\n     * @param isDisownCommit represents whether the commit is a disown commit or not.\n     * @param isBackfilled represents whether the commit is backfilled or not.\n     */\n    private case class UCCommit(\n        commit: JCommit,\n        isDisownCommit: Boolean,\n        isBackfilled: Boolean = false) {\n      /** Version of the underlying commit file */\n      val version: Long = commit.getVersion\n    }\n\n    /** Underlying storage of UC commit records */\n    private val ucCommits: mutable.ArrayBuffer[UCCommit] = mutable.ArrayBuffer.empty\n\n    /** RWLock to protect the commitsMap */\n    val lock: ReentrantReadWriteLock = new ReentrantReadWriteLock()\n\n    /**\n     * Returns the last ratified commit version for the table.\n     * If no commits have been done from commit-coordinator yet, returns -1.\n     */\n    def lastRatifiedCommitVersion: Long = ucCommits.lastOption.map(_.version).getOrElse(-1L)\n\n    /**\n     * Returns true if:\n     * - the table has ratified any commit, and\n     * - the last one is not a disown commit.\n     */\n    def isActive: Boolean = ucCommits.lastOption.exists(!_.isDisownCommit)\n\n    /**\n     * Returns true if:\n     * - the table has ratified any commit, and\n     * - the last one is a disown commit.\n     */\n    def isDisowned: Boolean = ucCommits.lastOption.exists(_.isDisownCommit)\n\n    /** Appends a commit to the table's commit history */\n    def appendCommit(commit: JCommit, isDisownCommit: Boolean): Unit = {\n      ucCommits += UCCommit(commit, isDisownCommit)\n    }\n\n    /** Removes all commits until the given version (inclusive) */\n    def removeCommitsUntilVersion(version: Long): Unit = {\n      val toRemove = ucCommits.takeWhile(_.version <= version)\n      ucCommits --= toRemove\n    }\n\n    /** Marks the last commit as backfilled */\n    def markLastCommitBackfilled(): Unit = {\n      ucCommits.lastOption.foreach { lastUCCommit =>\n        ucCommits.update(ucCommits.size - 1, lastUCCommit.copy(isBackfilled = true))\n      }\n    }\n\n    /**\n     * Returns the unbackfilled commits in the given range.\n     * If `startVersion` is not provided, the first commit is used.\n     * If `endVersion` is not provided, the last commit is used.\n     */\n    def getCommits(startVersion: Option[Long], endVersion: Option[Long]): Seq[JCommit] = {\n      val effectiveStartVersion = startVersion.getOrElse(0L)\n      val effectiveEndVersion = endVersion.getOrElse(\n        ucCommits.lastOption.map(_.version).getOrElse(return Seq.empty))\n      // Collect unbackfilled `Commit`s from the `UCCommit`s in the range.\n      ucCommits.filter(c =>\n        effectiveStartVersion <= c.version && c.version <= effectiveEndVersion && !c.isBackfilled\n      ).map(_.commit).toSeq\n    }\n  }\n\n  /**\n   * Variable to allow to control the behavior of the InMemoryUCCommitCoordinator\n   * externally. If set to true, the coordinator will throw an IOException after\n   * a successful commit. This will be reset to false once the exception has been\n   * thrown.\n   */\n  var throwIOExceptionAfterCommit: Boolean = false\n\n  /**\n   * Variable to allow to control the behavior of the InMemoryUCCommitCoordinator\n   * externally. If set to true, the coordinator will throw an IOException before\n   * persisting a commit to the in memory map. This will be reset to false once the\n   * exception has been thrown.\n   */\n  var throwIOExceptionBeforeCommit: Boolean = false\n\n  /** The maximum number of unbackfilled commits this commit coordinator can store at a time */\n  private val MAX_NUM_COMMITS = 10\n\n  /**\n   * Map from table UUID to the data associated with the table.\n   * Mimics the underlying storage for the commit files of different tables.\n   */\n  private val perTableMap = new ConcurrentHashMap[UUID, PerTableData]()\n\n  /** Performs the given operation with lock acquired on the table entry */\n  private def withLock[T](tableUUID: UUID, writeLock: Boolean = false)(operation: => T): T = {\n    val tableData = Option(perTableMap.get(tableUUID)).getOrElse {\n      throw new IllegalArgumentException(s\"Unknown table $tableUUID.\")\n    }\n    val lock = if (writeLock) tableData.lock.writeLock() else tableData.lock.readLock()\n    lock.lock()\n    try {\n      operation\n    } finally {\n      lock.unlock()\n    }\n  }\n\n  private def validateTableURI(\n      srcTable: URI,\n      targetTable: URI,\n      request: UCCoordinatedCommitsRequestType.UCCoordinatedCommitsRequestType): Unit = {\n    if (srcTable != targetTable) {\n      val errorMsg = s\"Source table $srcTable and targetTable $targetTable do not match for \" +\n        s\"$request\"\n      throw new JInvalidTargetTableException(errorMsg)\n    }\n  }\n\n  // scalastyle:off argcount\n  /**\n   * Validates the commit and backfill parameters.\n   *  - Ensures that all the fields are provided.\n   *  - Makes sure that `lastKnownBackfilledVersion` is not more than the latest table version.\n   *  - Ensures that the commit version is the next expected version.\n   *  - Blocks committing to the table if the number of unbackfilled commits exceeds the limit.\n   *  This function does not mutate any state.\n   */\n  private def getValidatedCommit(\n      tableId: String,\n      tableUri: URI,\n      commitFileName: Option[String] = None,\n      commitVersion: Option[Long] = None,\n      commitFileSize: Option[Long] = None,\n      commitFileModTime: Option[Long] = None,\n      commitTimestamp: Option[Long] = None,\n      lastKnownBackfilledVersion: Option[Long] = None,\n      isDisownCommit: Boolean = false): Option[JCommit] = {\n    val tableUUID = UUID.fromString(tableId)\n    val path = tableUri\n\n    val tableData = perTableMap.get(tableUUID)\n\n    lastKnownBackfilledVersion.foreach { backfilledUntil =>\n      val maxBackfillVersion = commitVersion.getOrElse(0L).max(tableData.lastRatifiedCommitVersion)\n      if (backfilledUntil > maxBackfillVersion) {\n        throw new IllegalArgumentException(\n          s\"Unexpected backfill version: $backfilledUntil. \" +\n            s\"Max backfill version: ${maxBackfillVersion}\")\n      }\n    }\n    commitFileName.map { fileName =>\n      // ensure that all other necessary parameters are provided\n      require(commitVersion.nonEmpty)\n      require(commitFileSize.nonEmpty)\n      require(commitFileModTime.nonEmpty)\n      require(commitTimestamp.nonEmpty)\n      validateTableURI(path, tableUri, UCCoordinatedCommitsRequestType.COMMIT)\n      // Check that there is still space in the commit coordinator.\n      val tableIdStr = tableUUID.toString\n      val currentNumCommits = getCommitsFromCoordinator(\n        tableIdStr, tableUri, startVersion = None, endVersion = None).getCommits.size\n      if (currentNumCommits == MAX_NUM_COMMITS) {\n        val errorMsg = s\"Too many unbackfilled commits for $tableIdStr. Cannot \" +\n          s\"store more than $MAX_NUM_COMMITS commits\"\n        throw new JCommitLimitReachedException(errorMsg)\n      }\n\n      if (throwIOExceptionBeforeCommit) {\n        throwIOExceptionBeforeCommit = false\n        throw new IOException(\"Problem before comitting\")\n      }\n      // Store the commit. For the InMemoryUCCommit coordinator, we concatenate the full commit path\n      // here already so that we don't have to do it during getCommits.\n      val basePath = FileNames.commitDirPath(\n        DeltaTableUtils.safeConcatPaths(new Path(tableUri), \"_delta_log\"))\n      val commitFilePath = new Path(basePath, fileName)\n      val fileStatus = new FileStatus(\n        commitFileSize.get, false, 0, 0, commitFileModTime.get, commitFilePath)\n      // We only check the expected version matches the commit version if the table is active.\n      // If the table is disowned, the check was already done above.\n      // If the table was just registered, the check is not necessary.\n      if (tableData.isActive) {\n        val expectedVersion = tableData.lastRatifiedCommitVersion + 1\n        if (commitVersion.get != expectedVersion) {\n          throw new JCommitFailedException(\n            commitVersion.get < expectedVersion,\n            commitVersion.get < expectedVersion,\n            s\"Commit version ${commitVersion.get} is not valid. \" +\n              s\"Expected version: $expectedVersion.\")\n        }\n      }\n      new JCommit(commitVersion.get, fileStatus, commitTimestamp.get)\n    }\n  }\n\n  private def backfillAfterCommitToCoordinatorInternal(\n      tableId: String,\n      lastKnownBackfilledVersion: Option[Long] = None): Unit = {\n    val tableUUID = UUID.fromString(tableId)\n    // Register any backfills.\n    lastKnownBackfilledVersion.foreach { backfilledUntil =>\n      val tableData = perTableMap.get(tableUUID)\n      val maxVersionToRemove = if (backfilledUntil == tableData.lastRatifiedCommitVersion) {\n        // If the backfill version is the last ratified commit version, we remove all but the\n        // last commit, and mark the last commit as backfilled. This is to ensure that every\n        // active table keeps track of at least one commit record.\n        tableData.markLastCommitBackfilled()\n        backfilledUntil - 1\n      } else {\n        // We have already validated that the backfill version is not more than the last\n        // ratified version in getValidatedCommit.\n        backfilledUntil\n      }\n      tableData.removeCommitsUntilVersion(maxVersionToRemove)\n    }\n  }\n\n  def commitToCoordinator(\n       tableId: String,\n       tableUri: URI,\n       commitFileName: Option[String] = None,\n       commitVersion: Option[Long] = None,\n       commitFileSize: Option[Long] = None,\n       commitFileModTime: Option[Long] = None,\n       commitTimestamp: Option[Long] = None,\n       lastKnownBackfilledVersion: Option[Long] = None,\n       isDisownCommit: Boolean = false,\n       protocolOpt: Option[AbstractProtocol] = None,\n       metadataOpt: Option[AbstractMetadata] = None): Unit = {\n    // either commitFileName or backfilledUntil (or both) need to be set\n    require(commitFileName.nonEmpty || lastKnownBackfilledVersion.nonEmpty)\n    // Onboard the table if it is not already present in the perTableMap.\n    if (commitVersion.nonEmpty) {\n      val tableUuid = UUID.fromString(tableId)\n      perTableMap.putIfAbsent(tableUuid, new PerTableData(tableUri))\n    }\n    withLock(\n      UUID.fromString(tableId),\n      writeLock = true\n    ) {\n      val commitToAppendOpt = getValidatedCommit(\n        tableId,\n        tableUri,\n        commitFileName,\n        commitVersion,\n        commitFileSize,\n        commitFileModTime,\n        commitTimestamp,\n        lastKnownBackfilledVersion,\n        isDisownCommit\n      )\n      commitToAppendOpt.foreach { commitToAppend =>\n        val tableData = perTableMap.get(UUID.fromString(tableId))\n        tableData.appendCommit(commitToAppend, isDisownCommit)\n      }\n      if (throwIOExceptionAfterCommit) {\n        throwIOExceptionAfterCommit = false\n        throw new IOException(\"Problem after comitting\")\n      }\n      backfillAfterCommitToCoordinatorInternal(\n        tableId,\n        lastKnownBackfilledVersion\n      )\n    }\n  }\n\n  def getCommitsFromCoordinator(\n      tableId: String,\n      tableUri: URI,\n      startVersion: Option[Long],\n      endVersion: Option[Long]): JGetCommitsResponse = {\n    val tableUUID = UUID.fromString(tableId)\n    val path = Option(perTableMap.get(tableUUID)).map(_.path).getOrElse {\n      return new JGetCommitsResponse(Seq.empty.asJava, -1)\n    }\n    validateTableURI(path, tableUri, UCCoordinatedCommitsRequestType.GET_COMMITS)\n    withLock[JGetCommitsResponse](tableUUID) {\n      val tableData = perTableMap.get(tableUUID)\n      val commits = tableData.getCommits(startVersion, endVersion)\n      new JGetCommitsResponse(commits.asJava, tableData.lastRatifiedCommitVersion)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/TableCommitCoordinatorClient.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.storage.{LogStore, LogStoreInverseAdaptor}\nimport io.delta.storage.commit.{\n  CommitCoordinatorClient => JCommitCoordinatorClient,\n  CommitResponse,\n  GetCommitsResponse => JGetCommitsResponse,\n  TableDescriptor,\n  UpdatedActions\n}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.catalyst.{TableIdentifier => CatalystTableIdentifier}\n\n/**\n * A wrapper around [[CommitCoordinatorClient]] that provides a more user-friendly API for\n * committing/ accessing commits to a specific table. This class takes care of passing the\n * table specific configuration to the underlying [[CommitCoordinatorClient]] e.g. logPath /\n * logStore / coordinatedCommitsTableConf / hadoopConf.\n *\n * @param commitCoordinatorClient the underlying [[CommitCoordinatorClient]]\n * @param logPath the path to the log directory\n * @param tableConf the table specific coordinated-commits configuration\n * @param hadoopConf hadoop configuration\n * @param logStore the log store\n */\ncase class TableCommitCoordinatorClient(\n    commitCoordinatorClient: JCommitCoordinatorClient,\n    logPath: Path,\n    tableConf: Map[String, String],\n    hadoopConf: Configuration,\n    logStore: LogStore) {\n\n  private def makeTableDesc(\n      tableIdentifierOpt: Option[CatalystTableIdentifier]): TableDescriptor = {\n    val ccTableIdentifier = CoordinatedCommitsUtils.toCCTableIdentifier(tableIdentifierOpt)\n    new TableDescriptor(logPath, ccTableIdentifier, tableConf.asJava)\n  }\n\n  def commit(\n      commitVersion: Long,\n      actions: Iterator[String],\n      updatedActions: UpdatedActions,\n      tableIdentifierOpt: Option[CatalystTableIdentifier]): CommitResponse = {\n    commitCoordinatorClient.commit(\n      LogStoreInverseAdaptor(logStore, hadoopConf),\n      hadoopConf,\n      makeTableDesc(tableIdentifierOpt),\n      commitVersion,\n      actions.asJava,\n      updatedActions)\n  }\n\n  def getCommits(\n      tableIdentifierOpt: Option[CatalystTableIdentifier],\n      startVersion: Option[Long] = None,\n      endVersion: Option[Long] = None): JGetCommitsResponse = {\n    commitCoordinatorClient.getCommits(\n      makeTableDesc(tableIdentifierOpt),\n      startVersion.map(Long.box).orNull,\n      endVersion.map(Long.box).orNull)\n  }\n\n  def backfillToVersion(\n      tableIdentifierOpt: Option[CatalystTableIdentifier],\n      version: Long,\n      lastKnownBackfilledVersion: Option[Long] = None): Unit = {\n    commitCoordinatorClient.backfillToVersion(\n      LogStoreInverseAdaptor(logStore, hadoopConf),\n      hadoopConf,\n      makeTableDesc(tableIdentifierOpt),\n      version,\n      lastKnownBackfilledVersion.map(Long.box).orNull)\n  }\n\n  /**\n   * Checks whether the signature of the underlying backing [[CommitCoordinatorClient]] is the same\n   * as the given `otherCommitCoordinatorClient`\n   */\n  def semanticsEquals(otherCommitCoordinatorClient: JCommitCoordinatorClient): Boolean = {\n    CommitCoordinatorClient.semanticEquals(\n      Some(commitCoordinatorClient), Some(otherCommitCoordinatorClient))\n  }\n\n  /**\n   * Checks whether the signature of the underlying backing [[CommitCoordinatorClient]] is the same\n   * as the given `otherCommitCoordinatorClient`\n   */\n  def semanticsEquals(otherCommitCoordinatorClient: TableCommitCoordinatorClient): Boolean = {\n    semanticsEquals(otherCommitCoordinatorClient.commitCoordinatorClient)\n  }\n}\n\nobject TableCommitCoordinatorClient {\n    def apply(\n      commitCoordinatorClient: JCommitCoordinatorClient,\n      deltaLog: DeltaLog,\n      coordinatedCommitsTableConf: Map[String, String]): TableCommitCoordinatorClient = {\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n    new TableCommitCoordinatorClient(\n      commitCoordinatorClient,\n      deltaLog.logPath,\n      coordinatedCommitsTableConf,\n      hadoopConf,\n      deltaLog.store)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/coordinatedcommits/UCCommitCoordinatorBuilder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.net.{URI, URISyntaxException}\nimport java.util.concurrent.ConcurrentHashMap\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport io.delta.storage.commit.CommitCoordinatorClient\nimport io.delta.storage.commit.uccommitcoordinator.{UCClient, UCCommitCoordinatorClient, UCTokenBasedRestClient}\n\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport io.unitycatalog.client.auth.TokenProvider\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\n\n/**\n * Builder for Unity Catalog Commit Coordinator Clients.\n *\n * This builder is responsible for creating and caching UCCommitCoordinatorClient instances\n * based on the provided metastore IDs and catalog configurations.\n *\n * It caches the UCCommitCoordinatorClient instance for a given metastore ID upon its first access.\n */\nobject UCCommitCoordinatorBuilder\n    extends CatalogOwnedCommitCoordinatorBuilder with DeltaLogging {\n\n  /** The coordinator name used in table metadata to identify UC-managed tables. */\n  final val COORDINATOR_NAME: String = \"unity-catalog\"\n\n  /** Prefix for Spark SQL catalog configurations. */\n  final private val SPARK_SQL_CATALOG_PREFIX = \"spark.sql.catalog.\"\n\n  /** Connector class name for filtering relevant Unity Catalog catalogs. */\n  final private[delta] val UNITY_CATALOG_CONNECTOR_CLASS: String =\n    \"io.unitycatalog.spark.UCSingleCatalog\"\n\n  /** Suffix for the URI configuration of a catalog. */\n  final private val URI_SUFFIX = \"uri\"\n\n  /** Cache for UCCommitCoordinatorClient instances. */\n  private val commitCoordinatorClientCache =\n    new ConcurrentHashMap[String, UCCommitCoordinatorClient]()\n\n  // Helper cache for (uri, authConfig) to metastoreId to avoid redundant calls to getMetastoreId\n  private val uriAuthConfigToMetastoreIdCache =\n    new ConcurrentHashMap[(String, Map[String, String]), String]()\n\n  // Use a var instead of val for ease of testing by injecting different UCClientFactory.\n  private[delta] var ucClientFactory: UCClientFactory = UCTokenBasedRestClientFactory\n\n  override def getName: String = COORDINATOR_NAME\n\n  override def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n    val metastoreId = conf.getOrElse(\n      UCCommitCoordinatorClient.UC_METASTORE_ID_KEY,\n      throw new IllegalArgumentException(\n        s\"UC metastore ID not found in the provided coordinator conf: $conf\"))\n\n    commitCoordinatorClientCache.computeIfAbsent(\n      metastoreId,\n      _ => new UCCommitCoordinatorClient(conf.asJava, getMatchingUCClient(spark, metastoreId)))\n  }\n\n  override def buildForCatalog(\n      spark: SparkSession,\n      catalogName: String): CommitCoordinatorClient = {\n    val client = getCatalogConfigs(spark).find(_._1 == catalogName) match {\n      case Some((_, uri, authConfig)) => ucClientFactory.createUCClient(uri, authConfig)\n      case None =>\n        throw new IllegalArgumentException(\n          s\"Catalog $catalogName not found in the provided SparkSession configurations.\")\n    }\n    val conf = Map.empty[String, String]\n    new UCCommitCoordinatorClient(conf.asJava, client)\n  }\n\n  /**\n   * Finds and returns a UCClient that matches the given metastore ID.\n   *\n   * This method iterates through all configured catalogs in SparkSession, creates UCClients for\n   * each, gets their metastore ID and returns the one that matches the provided metastore ID.\n   * If no matching catalog is found or if multiple matching catalogs are found, it throws an\n   * appropriate exception.\n   */\n  private def getMatchingUCClient(spark: SparkSession, metastoreId: String): UCClient = {\n    val matchingClients: List[(String, Map[String, String])] = getCatalogConfigs(spark)\n      .map { case (name, uri, authConfig) => (uri, authConfig) }\n      .distinct // Remove duplicates since multiple catalogs can have the same uri and config\n      .filter { case (uri, authConfig) => getMetastoreId(uri, authConfig).contains(metastoreId) }\n\n    matchingClients match {\n      case Nil => throw noMatchingCatalogException(metastoreId)\n      case (uri, authConfig) :: Nil => ucClientFactory.createUCClient(uri, authConfig)\n      case multiple => throw multipleMatchingCatalogs(metastoreId, multiple.map(_._1))\n    }\n  }\n\n  /**\n   * Retrieves the metastore ID for a given URI and auth configuration map.\n   *\n   * This method creates a UCClient using the provided URI and auth configuration map, then\n   * retrieves its metastore ID. The result is cached to avoid unnecessary getMetastoreId requests\n   * in future calls. If there's an error, it returns None and logs a warning.\n   */\n  private def getMetastoreId(uri: String, authConfig: Map[String, String]): Option[String] = {\n    try {\n      val metastoreId = uriAuthConfigToMetastoreIdCache.computeIfAbsent(\n        (uri, authConfig),\n        _ => {\n          val ucClient = ucClientFactory.createUCClient(uri, authConfig)\n          try {\n            ucClient.getMetastoreId\n          } finally {\n            safeClose(ucClient, uri)\n          }\n        })\n      Some(metastoreId)\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Failed to getMetastoreSummary with ${MDC(DeltaLogKeys.URI, uri)}\", e)\n        None\n    }\n  }\n\n  private def noMatchingCatalogException(metastoreId: String) = {\n    new IllegalStateException(\n      s\"No matching catalog found for UC metastore ID $metastoreId. \" +\n        \"Please ensure the catalog is configured correctly by setting \" +\n        \"`spark.sql.catalog.<catalog-name>`, `spark.sql.catalog.<catalog-name>.uri` and \" +\n        \"any required Unity Catalog authentication configurations. \" +\n        \"Note that the matching process involves retrieving the metastoreId using the \" +\n        \"provided configuration in Spark Session configs.\")\n  }\n\n  private def multipleMatchingCatalogs(metastoreId: String, uris: List[String]) = {\n    new IllegalStateException(\n      s\"Found multiple catalogs for UC metastore ID $metastoreId at $uris. \" +\n        \"Please ensure the catalog is configured correctly by setting \" +\n        \"`spark.sql.catalog.<catalog-name>`, `spark.sql.catalog.<catalog-name>.uri` and \" +\n        \"any required Unity Catalog authentication configurations. \" +\n        \"Note that the matching process involves retrieving the metastoreId using the \" +\n        \"provided configuration in Spark Session configs.\")\n  }\n\n  /**\n   * Retrieves the catalog configurations from the SparkSession.\n   *\n   * This method supports both the new auth.* format and the legacy token format for backward\n   * compatibility:\n   *\n   * New format:\n   *   spark.sql.catalog.catalog1.uri = \"https://dbc-123abc.databricks.com\"\n   *   spark.sql.catalog.catalog1.auth.type = \"static\"\n   *   spark.sql.catalog.catalog1.auth.token = \"dapi1234567890\"\n   *\n   * Legacy format (for backward compatibility):\n   *   spark.sql.catalog.catalog1.uri = \"https://dbc-123abc.databricks.com\"\n   *   spark.sql.catalog.catalog1.token = \"dapi1234567890\"\n   *\n   * When the legacy format is detected (token without auth. prefix), it is automatically\n   * converted to the new format (type=static, token=value) for TokenProvider.\n   *\n   * @return\n   *   A list of tuples containing (catalogName, uri, authConfigMap) for each properly configured\n   *   catalog. The authConfigMap contains authentication configurations ready to be passed to\n   *   TokenProvider.create().\n   */\n  private[delta] def getCatalogConfigs(\n      spark: SparkSession): List[(String, String, Map[String, String])] = {\n    val catalogConfigs = spark.conf.getAll.filterKeys(_.startsWith(SPARK_SQL_CATALOG_PREFIX))\n\n    // First, identify all Unity Catalog catalogs\n    val ucCatalogNames = catalogConfigs\n      .keys\n      .map(_.split(\"\\\\.\"))\n      .filter(_.length == 4)\n      .map(_(3))\n      .filter { catalogName: String =>\n        val connector = catalogConfigs.get(s\"$SPARK_SQL_CATALOG_PREFIX$catalogName\")\n        connector.contains(UNITY_CATALOG_CONNECTOR_CLASS)\n      }\n\n    // For each UC catalog, extract its URI and auth configurations\n    ucCatalogNames\n      .flatMap { catalogName: String =>\n        val catalogPrefix = s\"$SPARK_SQL_CATALOG_PREFIX$catalogName.\"\n        val authPrefix = s\"${catalogPrefix}auth.\"\n        val uriOpt = catalogConfigs.get(s\"$catalogPrefix$URI_SUFFIX\")\n\n        uriOpt match {\n          case Some(uri) =>\n            try {\n              new URI(uri) // Validate the URI\n\n              // Extract all auth.* configuration keys for this catalog\n              // and strip the \"spark.sql.catalog.<catalog-name>.auth.\" prefix\n              var authConfigMap = catalogConfigs\n                .filterKeys(_.startsWith(authPrefix))\n                .map { case (fullKey, value) =>\n                  // Remove the auth prefix to get just the auth config key\n                  // e.g., \"spark.sql.catalog.catalog1.auth.type\" -> \"type\"\n                  // e.g., \"spark.sql.catalog.catalog1.auth.oauth.uri\" -> \"oauth.uri\"\n                  val authKey = fullKey.stripPrefix(authPrefix)\n                  (authKey, value)\n                }\n                .toMap\n\n              // Support legacy format: if no auth.* configs but token exists,\n              // convert to new format (type=static, token=value)\n              if (authConfigMap.isEmpty) {\n                val legacyTokenOpt = catalogConfigs.get(s\"${catalogPrefix}token\")\n                legacyTokenOpt match {\n                  case Some(token) =>\n                    authConfigMap = Map(\"type\" -> \"static\", \"token\" -> token)\n                  case None =>\n                  // No auth configs found\n                }\n              }\n\n              if (authConfigMap.isEmpty) {\n                logWarning(\n                  log\"Skipping catalog ${MDC(DeltaLogKeys.CATALOG, catalogName)} as it \" +\n                    \"does not have any authentication configurations in Spark Session.\")\n                None\n              } else {\n                Some((catalogName, uri, authConfigMap))\n              }\n            } catch {\n              case _: URISyntaxException =>\n                logWarning(\n                  log\"Skipping catalog ${MDC(DeltaLogKeys.CATALOG, catalogName)} as it \" +\n                    log\"does not have a valid URI ${MDC(DeltaLogKeys.URI, uri)}.\")\n                None\n            }\n          case None =>\n            logWarning(\n              log\"Skipping catalog ${MDC(DeltaLogKeys.CATALOG, catalogName)} as it does \" +\n                \"not have uri configured in Spark Session.\")\n            None\n        }\n      }\n      .toList\n  }\n\n  /**\n   * Returns catalog configurations as a Map for O(1) lookup by catalog name.\n   * Wraps [[getCatalogConfigs]] results in [[UCCatalogConfig]] for better readability.\n   */\n  private[delta] def getCatalogConfigMap(spark: SparkSession): Map[String, UCCatalogConfig] = {\n    getCatalogConfigs(spark).map {\n      case (name, uri, authConfig) => name -> UCCatalogConfig(name, uri, authConfig)\n    }.toMap\n  }\n\n  private def safeClose(ucClient: UCClient, uri: String): Unit = {\n    try {\n      ucClient.close()\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Failed to close UCClient for uri ${MDC(DeltaLogKeys.URI, uri)}\", e)\n    }\n  }\n\n  def clearCache(): Unit = {\n    commitCoordinatorClientCache.clear()\n    uriAuthConfigToMetastoreIdCache.clear()\n  }\n}\n\ntrait UCClientFactory {\n  def createUCClient(uri: String, authConfig: Map[String, String]): UCClient\n}\n\nobject UCTokenBasedRestClientFactory extends UCClientFactory {\n  override def createUCClient(uri: String, authConfig: Map[String, String]): UCClient = {\n    createUCClientWithVersions(uri, authConfig, defaultAppVersions)\n  }\n\n  /**\n   * Creates a UC client with the given application versions for telemetry.\n   * The provided `appVersions` map is used as-is; callers are responsible for\n   * including all desired version entries.\n   */\n  def createUCClientWithVersions(\n      uri: String,\n      authConfig: Map[String, String],\n      appVersions: Map[String, String]): UCClient = {\n    // Create TokenProvider from the authentication configuration map\n    // We pass the configuration through without interpreting any specific keys,\n    // as those are managed by the Unity Catalog client library\n    val tokenProvider = TokenProvider.create(authConfig.asJava)\n    new UCTokenBasedRestClient(uri, tokenProvider, appVersions.asJava)\n  }\n\n  private[coordinatedcommits] def defaultAppVersions: Map[String, String] = {\n    Map(\n      \"Delta\" -> io.delta.VERSION,\n      \"Spark\" -> org.apache.spark.SPARK_VERSION,\n      \"Scala\" -> scala.util.Properties.versionNumberString,\n      \"Java\" -> System.getProperty(\"java.version\")\n    )\n  }\n\n  /** Returns the default app versions as a mutable Java map for easy extension. */\n  def defaultAppVersionsAsJava: java.util.Map[String, String] = {\n    new java.util.HashMap(defaultAppVersions.asJava)\n  }\n\n  /** Java-friendly overload that accepts a java.util.Map */\n  def createUCClient(uri: String, authConfig: java.util.Map[String, String]): UCClient = {\n    createUCClient(uri, authConfig.asScala.toMap)\n  }\n\n  /** Java-friendly overload that accepts application versions for telemetry. */\n  def createUCClientWithVersions(\n      uri: String,\n      authConfig: java.util.Map[String, String],\n      appVersions: java.util.Map[String, String]): UCClient = {\n    createUCClientWithVersions(uri, authConfig.asScala.toMap, appVersions.asScala.toMap)\n  }\n}\n\n/**\n * Holder for Unity Catalog configuration extracted from Spark configs.\n * Used by [[UCCommitCoordinatorBuilder.getCatalogConfigMap]].\n */\ncase class UCCatalogConfig(catalogName: String, uri: String, authConfig: Map[String, String])\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/deletionvectors/RoaringBitmapArray.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.deletionvectors\n\nimport java.io.IOException\nimport java.nio.{ByteBuffer, ByteOrder}\n\nimport scala.collection.immutable.NumericRange\n\nimport com.google.common.primitives.{Ints, UnsignedInts}\nimport org.roaringbitmap.{RelativeRangeConsumer, RoaringBitmap}\n\n/**\n * A 64-bit extension of [[RoaringBitmap]] that is optimized for cases that usually fit within\n * a 32-bit bitmap, but may run over by a few bits on occasion.\n *\n * This focus makes it different from [[org.roaringbitmap.longlong.Roaring64NavigableMap]] and\n * [[org.roaringbitmap.longlong.Roaring64Bitmap]] which focus on sparse bitmaps over the whole\n * 64-bit range.\n *\n * Structurally, this implementation simply uses the most-significant 4 bytes to index into\n * an array of 32-bit [[RoaringBitmap]] instances.\n * The array is grown as necessary to accommodate the largest value in the bitmap.\n *\n * *Note:* As opposed to the other two 64-bit bitmap implementations mentioned above,\n *         this implementation cannot accommodate `Long` values where the most significant\n *         bit is non-zero (i.e., negative `Long` values).\n *         It cannot even accommodate values where the 4 high-order bytes are `Int.MaxValue`,\n *         because then the length of the `bitmaps` array would be a negative number\n *         (`Int.MaxValue + 1`).\n */\nfinal class RoaringBitmapArray extends Equals {\n  import RoaringBitmapArray._\n\n  private var bitmaps: Array[RoaringBitmap] = Array.empty\n\n  /**\n   * Add the value to the container (set the value to `true`),\n   * whether it already appears or not.\n   */\n  def add(value: Long): Unit = {\n    require(value >= 0 && value <= MAX_REPRESENTABLE_VALUE)\n    val (high, low) = decomposeHighLowBytes(value)\n    if (high >= bitmaps.length) {\n      extendBitmaps(newLength = high + 1)\n    }\n    val highBitmap = bitmaps(high)\n    highBitmap.add(low)\n  }\n\n  /** Add all `values` to the container. For testing purposes only. */\n  protected[delta] def addAll(values: Long*): Unit = values.foreach(add)\n\n  /** Add all values in `range` to the container. */\n  protected[delta] def addRange(range: Range): Unit = {\n    require(0 <= range.start && range.start <= range.end)\n    if (range.isEmpty) return // Nothing to do.\n    if (range.step != 1) {\n      // Can't optimize in this case.\n      range.foreach(i => add(UnsignedInts.toLong(i)))\n      return\n    }\n    // This is an Int range, so it must fit completely into the first bitmap.\n    if (bitmaps.isEmpty) {\n      extendBitmaps(newLength = 1)\n    }\n    val end = if (range.isInclusive) range.end + 1 else range.end\n    bitmaps.head.add(range.start, end)\n  }\n\n  /** Add all values in `range` to the container. */\n  protected[delta] def addRange(range: NumericRange[Long]): Unit = {\n    require(0L <= range.start && range.start <= range.end && range.end <= MAX_REPRESENTABLE_VALUE)\n    if (range.isEmpty) return // Nothing to do.\n    if (range.step != 1L) {\n      // Can't optimize in this case.\n      range.foreach(add)\n      return\n    }\n    // Decompose into sub-ranges that target a single bitmap,\n    // to use the range adds within a bitmap for efficiency.\n    val (startHigh, startLow) = decomposeHighLowBytes(range.start)\n    val (endHigh, endLow) = decomposeHighLowBytes(range.end)\n    val lastHigh = if (endLow == 0 && !range.isInclusive) endHigh - 1 else endHigh\n    if (lastHigh >= bitmaps.length) {\n      extendBitmaps(newLength = lastHigh + 1)\n    }\n    var currentHigh = startHigh\n    while (currentHigh <= lastHigh) {\n      val start = if (currentHigh == startHigh) UnsignedInts.toLong(startLow) else 0L\n      // RoaringBitmap.add is exclusive the end boundary.\n      val end = if (currentHigh == endHigh) {\n        if (range.isInclusive) UnsignedInts.toLong(endLow) + 1L else UnsignedInts.toLong(endLow)\n      } else {\n        0xFFFFFFFFL + 1L\n      }\n      bitmaps(currentHigh).add(start, end)\n      currentHigh += 1\n    }\n  }\n\n  /**\n   * If present, remove the `value` (effectively, sets its bit value to false).\n   *\n   * @param value The index in a bitmap.\n   */\n  protected[deletionvectors] def remove(value: Long): Unit = {\n    require(value >= 0 && value <= MAX_REPRESENTABLE_VALUE)\n    val (high, low) = decomposeHighLowBytes(value)\n    if (high < bitmaps.length) {\n      val highBitmap = bitmaps(high)\n      highBitmap.remove(low)\n      if (highBitmap.isEmpty) {\n        // Clean up all bitmaps that are now empty (from the end).\n        var latestNonEmpty = bitmaps.length - 1\n        var done = false\n        while (!done && latestNonEmpty >= 0) {\n          if (bitmaps(latestNonEmpty).isEmpty) {\n            latestNonEmpty -= 1\n          } else {\n            done = true\n          }\n        }\n        shrinkBitmaps(latestNonEmpty + 1)\n      }\n    }\n  }\n\n  /** Remove all values from the bitmap. */\n  def clear(): Unit = {\n    bitmaps = Array.empty\n  }\n\n  /**\n   * Checks whether the value is included,\n   * which is equivalent to checking if the corresponding bit is set.\n   */\n  def contains(value: Long): Boolean = {\n    require(value >= 0 && value <= MAX_REPRESENTABLE_VALUE)\n    val high = highBytes(value)\n    if (high >= bitmaps.length) {\n      false\n    } else {\n      val highBitmap = bitmaps(high)\n      val low = lowBytes(value)\n      highBitmap.contains(low)\n    }\n  }\n\n  /**\n   * Return the set values as an array, if the cardinality is smaller than 2147483648.\n   *\n   * The integer values are in sorted order.\n   */\n  def toArray: Array[Long] = {\n    val cardinality = this.cardinality\n    require(cardinality <= Int.MaxValue)\n    val values = Array.ofDim[Long](cardinality.toInt)\n    var valuesIndex = 0\n    for ((bitmap, bitmapIndex) <- bitmaps.zipWithIndex) {\n      bitmap.forEach((value: Int) => {\n        values(valuesIndex) = composeFromHighLowBytes(bitmapIndex, value)\n        valuesIndex += 1\n      })\n    }\n    values\n  }\n\n  /** Materialise the whole set into an array */\n  def values: Array[Long] = toArray\n\n  /** Returns the number of distinct integers added to the bitmap (e.g., number of bits set). */\n  def cardinality: Long = bitmaps.foldLeft(0L)((sum, bitmap) => sum + bitmap.getLongCardinality)\n\n  /** Tests whether the bitmap is empty. */\n  def isEmpty: Boolean = bitmaps.forall(_.isEmpty)\n\n  /**\n   * Use a run-length encoding where it is more space efficient.\n   *\n   * @return `true` if a change was applied\n   */\n  def runOptimize(): Boolean = {\n    var changeApplied = false\n    for (bitmap <- bitmaps) {\n      changeApplied |= bitmap.runOptimize()\n    }\n    changeApplied\n  }\n\n  /**\n   * Remove run-length encoding even when it is more space efficient.\n   *\n   * @return `true` if a change was applied\n   */\n  def removeRunCompression(): Boolean = {\n    var changeApplied = false\n    for (bitmap <- bitmaps) {\n      changeApplied |= bitmap.removeRunCompression()\n    }\n    changeApplied\n  }\n\n  /**\n   * In-place bitwise OR (union) operation.\n   *\n   * The current bitmap is modified.\n   */\n  def or(that: RoaringBitmapArray): Unit = {\n    if (this.bitmaps.length < that.bitmaps.length) {\n      extendBitmaps(newLength = that.bitmaps.length)\n    }\n    for (index <- that.bitmaps.indices) {\n      val thisBitmap = this.bitmaps(index)\n      val thatBitmap = that.bitmaps(index)\n      thisBitmap.or(thatBitmap)\n    }\n  }\n\n  /** Merges the `other` set into this one. */\n  def merge(other: RoaringBitmapArray): Unit = this.or(other)\n\n  /** Get values in `this` but not `that`. */\n  def diff(other: RoaringBitmapArray): Unit = this.andNot(other)\n\n  /** Copy `this` along with underlying bitmaps to a new instance. */\n  def copy(): RoaringBitmapArray = {\n    val newBitmap = new RoaringBitmapArray()\n    newBitmap.merge(this)\n    newBitmap\n  }\n\n  /**\n   * In-place bitwise AND (this & that) operation.\n   *\n   * The current bitmap is modified.\n   */\n  def and(that: RoaringBitmapArray): Unit = {\n    for (index <- 0 until this.bitmaps.length) {\n      val thisBitmap = this.bitmaps(index)\n      if (index < that.bitmaps.length) {\n        val thatBitmap = that.bitmaps(index)\n        thisBitmap.and(thatBitmap)\n      } else {\n        thisBitmap.clear()\n      }\n    }\n  }\n\n  /**\n   * In-place bitwise AND-NOT (this & ~that) operation.\n   *\n   * The current bitmap is modified.\n   */\n  def andNot(that: RoaringBitmapArray): Unit = {\n    val validLength = math.min(this.bitmaps.length, that.bitmaps.length)\n    for (index <- 0 until validLength) {\n      val thisBitmap = this.bitmaps(index)\n      val thatBitmap = that.bitmaps(index)\n      thisBitmap.andNot(thatBitmap)\n    }\n  }\n\n  /**\n   * Report the number of bytes required to serialize this bitmap.\n   *\n   * This is the number of bytes written out when using the [[serialize]] method.\n   */\n  def serializedSizeInBytes(format: RoaringBitmapArrayFormat.Value): Long = {\n    val magicNumberSize = 4\n\n    val serializedBitmapsSize = format.formatImpl.serializedSizeInBytes(bitmaps)\n\n    magicNumberSize + serializedBitmapsSize\n  }\n\n  /**\n   * Serialize this [[RoaringBitmapArray]] into the `buffer`.\n   *\n   * == Format ==\n   * - A Magic Number indicating the format used (4 bytes)\n   * - The actual data as specified by the format.\n   *\n   */\n  def serialize(buffer: ByteBuffer, format: RoaringBitmapArrayFormat.Value): Unit = {\n    require(ByteOrder.LITTLE_ENDIAN == buffer.order(),\n      \"RoaringBitmapArray has to be serialized using a little endian buffer\")\n    // Magic number to make sure we don't try to deserialize a simple RoaringBitmap or the wrong\n    // format later.\n    buffer.putInt(format.formatImpl.MAGIC_NUMBER)\n    format.formatImpl.serialize(bitmaps, buffer)\n  }\n\n  /** Serializes this [[RoaringBitmapArray]] and returns the serialized form as a byte array. */\n  def serializeAsByteArray(format: RoaringBitmapArrayFormat.Value): Array[Byte] = {\n    val size = serializedSizeInBytes(format)\n    if (!size.isValidInt) {\n      throw new IOException(\n        s\"A bitmap was too big to be serialized into an array ($size bytes)\")\n    }\n    val buffer = ByteBuffer.allocate(size.toInt)\n    buffer.order(ByteOrder.LITTLE_ENDIAN)\n    // This is faster than Java serialization.\n    // See: https://richardstartin.github.io/posts/roaringbitmap-performance-tricks#serialisation\n    serialize(buffer, format)\n    buffer.array()\n  }\n\n  /**\n   * Deserialize the contents of `buffer` into this [[RoaringBitmapArray]].\n   *\n   * All existing content will be discarded!\n   *\n   * See [[serialize]] for the expected serialization format.\n   */\n  def deserialize(buffer: ByteBuffer): Unit = {\n    require(ByteOrder.LITTLE_ENDIAN == buffer.order(),\n      \"RoaringBitmapArray has to be deserialized using a little endian buffer\")\n\n    val magicNumber = buffer.getInt\n    val serializationFormat = magicNumber match {\n      case NativeRoaringBitmapArraySerializationFormat.MAGIC_NUMBER =>\n        NativeRoaringBitmapArraySerializationFormat\n      case PortableRoaringBitmapArraySerializationFormat.MAGIC_NUMBER =>\n        PortableRoaringBitmapArraySerializationFormat\n      case _ =>\n        throw new IOException(s\"Unexpected RoaringBitmapArray magic number $magicNumber\")\n    }\n    bitmaps = serializationFormat.deserialize(buffer)\n  }\n\n  /**\n   * Consume presence information for all values in the range `[start, start + length)`.\n   *\n   * @param start Lower bound of values to consume.\n   * @param length Maximum number of values to consume.\n   * @param rrc Code to be executed for each present or absent value.\n   */\n  def forAllInRange(start: Long, length: Int, consumer: RelativeRangeConsumer): Unit = {\n    // This one is complicated and deserves its own PR,\n    // when we actually want to enable it.\n    throw new UnsupportedOperationException\n  }\n\n  /** Execute the `consume` function for every value in the set represented by this bitmap. */\n  def forEach(consume: Long => Unit): Unit = {\n    for ((bitmap, high) <- bitmaps.zipWithIndex) {\n      bitmap.forEach { low: Int =>\n        val value = composeFromHighLowBytes(high, low)\n        consume(value)\n      }\n    }\n  }\n\n  override def canEqual(that: Any): Boolean = that.isInstanceOf[RoaringBitmapArray]\n\n  override def equals(other: Any): Boolean = {\n    other match {\n      case that: RoaringBitmapArray =>\n        (this eq that) || // don't need to check canEqual because class is final\n          java.util.Arrays.deepEquals(\n            this.bitmaps.asInstanceOf[Array[AnyRef]],\n            that.bitmaps.asInstanceOf[Array[AnyRef]])\n      case _ => false\n    }\n  }\n\n  override def hashCode: Int = 131 * java.util.Arrays.deepHashCode(\n    bitmaps.asInstanceOf[Array[AnyRef]])\n\n  def mkString(start: String = \"\", sep: String = \"\", end: String = \"\"): String =\n    toArray.mkString(start, sep, end)\n\n  def first: Option[Long] = {\n    for ((bitmap, high) <- bitmaps.zipWithIndex) {\n      if (!bitmap.isEmpty) {\n        val low = bitmap.first()\n        return Some(composeFromHighLowBytes(high, low))\n      }\n    }\n    None\n  }\n\n  def last: Option[Long] = {\n    for ((bitmap, high) <- bitmaps.zipWithIndex.reverse) {\n      if (!bitmap.isEmpty) {\n        val low = bitmap.last()\n        return Some(composeFromHighLowBytes(high, low))\n      }\n    }\n    None\n  }\n\n  /**\n   * Utility method to extend the array of [[RoaringBitmap]] to given length, keeping\n   * the existing elements in place.\n   */\n  private def extendBitmaps(newLength: Int): Unit = {\n    // Optimization for the most common case\n    if (bitmaps.isEmpty && newLength == 1) {\n      bitmaps = Array(new RoaringBitmap())\n      return\n    }\n    val newBitmaps = Array.ofDim[RoaringBitmap](newLength)\n    System.arraycopy(\n      bitmaps, // source\n      0, // source start pos\n      newBitmaps, // dest\n      0, // dest start pos\n      bitmaps.length) // number of entries to copy\n    for (i <- bitmaps.length until newLength) {\n      newBitmaps(i) = new RoaringBitmap()\n    }\n    bitmaps = newBitmaps\n  }\n\n  /** Utility method to shrink the array of [[RoaringBitmap]] to given length. */\n  private def shrinkBitmaps(newLength: Int): Unit = {\n    if (newLength == 0) {\n      bitmaps = Array.empty\n    } else {\n      val newBitmaps = Array.ofDim[RoaringBitmap](newLength)\n      System.arraycopy(\n        bitmaps, // source\n        0, // source start pos\n        newBitmaps, // dest\n        0, // dest start pos\n        newLength) // number of entries to copy\n      bitmaps = newBitmaps\n    }\n  }\n\n  // For testing purposes\n  protected[delta] def toBitmap32Bit(): RoaringBitmap = {\n    val bitmap32 = new RoaringBitmap()\n    forEach { value =>\n      val value32 = Ints.checkedCast(value)\n      bitmap32.add(value32)\n    }\n    bitmap32.runOptimize()\n    bitmap32\n  }\n}\n\nobject RoaringBitmapArray {\n\n  /** The largest value a [[RoaringBitmapArray]] can possibly represent. */\n  final val MAX_REPRESENTABLE_VALUE: Long = composeFromHighLowBytes(Int.MaxValue - 1, Int.MinValue)\n  final val MAX_BITMAP_CARDINALITY: Long = 1L << 32\n\n  /** Create a new [[RoaringBitmapArray]] with the given `values`. */\n  def apply(values: Long*): RoaringBitmapArray = {\n    val bitmap = new RoaringBitmapArray\n    bitmap.addAll(values: _*)\n    bitmap\n  }\n\n  /**\n   *\n   * @param value Any `Long`; positive or negative.\n   * @return An `Int` holding the 4 high-order bytes of information of the input `value`.\n   */\n  def highBytes(value: Long): Int = (value >> 32).toInt\n\n  /**\n   *\n   * @param value Any `Long`; positive or negative.\n   * @return An `Int` holding the 4 low-order bytes of information of the input `value`.\n   */\n  def lowBytes(value: Long): Int = value.toInt\n\n  /** Separate high and low 4 bytes into a pair of `Int`s (high, low). */\n  def decomposeHighLowBytes(value: Long): (Int, Int) = (highBytes(value), lowBytes(value))\n\n  /**\n   * Combine high and low 4 bytes of a pair of `Int`s into a `Long`.\n   *\n   * This is essentially the inverse of [[decomposeHighLowBytes()]].\n   *\n   * @param high An `Int` representing the 4 high-order bytes of the output `Long`\n   * @param low An `Int` representing the 4 low-order bytes of the output `Long`\n   * @return A `Long` composing the `high` and `low` bytes.\n   */\n  def composeFromHighLowBytes(high: Int, low: Int): Long =\n    (high.toLong << 32) | (low.toLong & 0xFFFFFFFFL) // Must bitmask to avoid sign extension.\n\n  /** Deserialize the right instance from the given bytes */\n  def readFrom(bytes: Array[Byte]): RoaringBitmapArray = {\n    val buffer = ByteBuffer.wrap(bytes)\n    buffer.order(ByteOrder.LITTLE_ENDIAN)\n    val bitmap = new RoaringBitmapArray()\n    bitmap.deserialize(buffer)\n    bitmap\n  }\n}\n\n/**\n * Abstracts out how to (de-)serialize the array.\n *\n * All formats are indicated by a magic number in the first 4-bytes,\n * which must be add/stripped by the *caller*.\n */\nprivate[deletionvectors] sealed trait RoaringBitmapArraySerializationFormat {\n  /** Magic number prefix for serialization with this format. */\n  val MAGIC_NUMBER: Int\n  /** The number of bytes written out when using the [[serialize]] method. */\n  def serializedSizeInBytes(bitmaps: Array[RoaringBitmap]): Long\n  /** Serialize `bitmaps` into `buffer`. */\n  def serialize(bitmaps: Array[RoaringBitmap], buffer: ByteBuffer): Unit\n  /** Deserialize all bitmaps from the `buffer` into a fresh array. */\n  def deserialize(buffer: ByteBuffer): Array[RoaringBitmap]\n}\n\n/** Legal values for the serialization format for [[RoaringBitmapArray]]. */\nobject RoaringBitmapArrayFormat extends Enumeration {\n  protected case class Format(formatImpl: RoaringBitmapArraySerializationFormat)\n    extends super.Val\n\n  import scala.language.implicitConversions\n  implicit def valueToFormat(x: Value): Format = x.asInstanceOf[Format]\n\n  val Native = Format(NativeRoaringBitmapArraySerializationFormat)\n  val Portable = Format(PortableRoaringBitmapArraySerializationFormat)\n}\n\nprivate[deletionvectors] object NativeRoaringBitmapArraySerializationFormat\n  extends RoaringBitmapArraySerializationFormat {\n\n  override val MAGIC_NUMBER: Int = 1681511376\n\n  override def serializedSizeInBytes(bitmaps: Array[RoaringBitmap]): Long = {\n    val roaringBitmapsCountSize = 4\n\n    val roaringBitmapLengthSize = 4\n    val roaringBitmapsSize = bitmaps.foldLeft(0L) { (sum, bitmap) =>\n      sum + bitmap.serializedSizeInBytes() + roaringBitmapLengthSize\n    }\n\n    roaringBitmapsCountSize + roaringBitmapsSize\n  }\n\n  /**\n   * Serialize `bitmaps` into the `buffer`.\n   *\n   * == Format ==\n   * - Number of bitmaps (4 bytes)\n   * - For each individual bitmap:\n   *    - Length of the serialized bitmap\n   *    - Serialized bitmap data using the standard format\n   *      (see https://github.com/RoaringBitmap/RoaringFormatSpec)\n   */\n  override def serialize(bitmaps: Array[RoaringBitmap], buffer: ByteBuffer): Unit = {\n    buffer.putInt(bitmaps.length)\n    for (bitmap <- bitmaps) {\n      val placeholderPos = buffer.position()\n      buffer.putInt(-1) // Placeholder for the serialized size\n      val startPos = placeholderPos + 4\n      bitmap.serialize(buffer)\n      val endPos = buffer.position()\n      val writtenBytes = endPos - startPos\n      buffer.putInt(placeholderPos, writtenBytes)\n    }\n  }\n\n  override def deserialize(buffer: ByteBuffer): Array[RoaringBitmap] = {\n    val numberOfBitmaps = buffer.getInt\n    if (numberOfBitmaps < 0) {\n      throw new IOException(s\"Invalid RoaringBitmapArray length\" +\n        s\" ($numberOfBitmaps < 0)\")\n    }\n    val bitmaps = Array.fill(numberOfBitmaps)(new RoaringBitmap())\n    for (index <- 0 until numberOfBitmaps) {\n      val bitmapSize = buffer.getInt\n      bitmaps(index).deserialize(buffer)\n      // RoaringBitmap.deserialize doesn't move the buffer's pointer\n      buffer.position(buffer.position() + bitmapSize)\n    }\n    bitmaps\n  }\n}\n\n/**\n * This is the \"official\" portable format defined in the spec.\n *\n * See [[https://github.com/RoaringBitmap/RoaringFormatSpec#extention-for-64-bit-implementations]]\n */\nprivate[sql] object PortableRoaringBitmapArraySerializationFormat\n  extends RoaringBitmapArraySerializationFormat {\n\n  override val MAGIC_NUMBER: Int = 1681511377\n\n  override def serializedSizeInBytes(bitmaps: Array[RoaringBitmap]): Long = {\n    val bitmapCountSize = 8\n\n    val individualBitmapKeySize = 4\n    val bitmapSizes = bitmaps.foldLeft(0L) { (sum, bitmap) =>\n      sum + bitmap.serializedSizeInBytes() + individualBitmapKeySize\n    }\n\n    bitmapCountSize + bitmapSizes\n  }\n\n  /**\n   * Serialize `bitmaps` into the `buffer`.\n   *\n   * ==Format==\n   *   - Number of bitmaps (8 bytes, upper 4 are basically padding)\n   *   - For each individual bitmap, in increasing key order (unsigned, technically, but\n   *     RoaringBitmapArray doesn't support negative keys anyway.):\n   *     - key of the bitmap (upper 32 bit)\n   *     - Serialized bitmap data using the standard format (see\n   *       https://github.com/RoaringBitmap/RoaringFormatSpec)\n   */\n  override def serialize(bitmaps: Array[RoaringBitmap], buffer: ByteBuffer): Unit = {\n    buffer.putLong(bitmaps.length.toLong)\n    // Iterate in index-order, so that the keys are ascending as required by spec.\n    for ((bitmap, index) <- bitmaps.zipWithIndex) {\n      // In our array-based implementation the index is the key.\n      buffer.putInt(index)\n      bitmap.serialize(buffer)\n    }\n  }\n  override def deserialize(buffer: ByteBuffer): Array[RoaringBitmap] = {\n    val numberOfBitmaps = buffer.getLong\n    // These cases are allowed by the format, but out implementation doesn't support them.\n    if (numberOfBitmaps < 0L) {\n      throw new IOException(s\"Invalid RoaringBitmapArray length ($numberOfBitmaps < 0)\")\n    }\n    if (numberOfBitmaps > Int.MaxValue) {\n      throw new IOException(\n        s\"Invalid RoaringBitmapArray length ($numberOfBitmaps > ${Int.MaxValue})\")\n    }\n    // This format is designed for sparse bitmaps, so numberOfBitmaps is only a lower bound for the\n    // actual size of the array.\n    val minimumArraySize = numberOfBitmaps.toInt\n    val bitmaps = Array.newBuilder[RoaringBitmap]\n    bitmaps.sizeHint(minimumArraySize)\n    var lastIndex = 0\n    for (_ <- 0L until numberOfBitmaps) {\n      val key = buffer.getInt\n      if (key < 0L) {\n        throw new IOException(s\"Invalid unsigned entry in RoaringBitmapArray ($key)\")\n      }\n      assert(key >= lastIndex, \"Keys are required to be sorted in ascending order.\")\n      // Fill gaps in sparse data.\n      while (lastIndex < key) {\n        bitmaps += new RoaringBitmap()\n        lastIndex += 1\n      }\n      val bitmap = new RoaringBitmap()\n      bitmap.deserialize(buffer)\n      bitmaps += bitmap\n      lastIndex += 1\n      // RoaringBitmap.deserialize doesn't move the buffer's pointer\n      buffer.position(buffer.position() + bitmap.serializedSizeInBytes())\n    }\n    bitmaps.result()\n  }\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/deletionvectors/RowIndexMarkingFilters.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.deletionvectors\n\nimport org.apache.spark.sql.delta.RowIndexFilter\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.execution.vectorized.WritableColumnVector\nimport org.apache.spark.sql.vectorized.ColumnVector\n\n/**\n * Base class for row index filters.\n * @param bitmap Represents the deletion vector.\n */\nabstract sealed class RowIndexMarkingFilters(bitmap: RoaringBitmapArray) extends RowIndexFilter {\n  val valueWhenContained: Byte\n  val valueWhenNotContained: Byte\n\n  private def isContainedInBitmap(rowIndex: Long): Byte = {\n    val isContained = bitmap.contains(rowIndex)\n    if (isContained) {\n      valueWhenContained\n    } else {\n      valueWhenNotContained\n    }\n  }\n\n  override def materializeIntoVector(start: Long, end: Long, batch: WritableColumnVector): Unit = {\n    val batchSize = (end - start).toInt\n    var rowId = 0\n    while (rowId < batchSize) {\n      val isContained = isContainedInBitmap(start + rowId.toLong)\n      batch.putByte(rowId, isContained)\n      rowId += 1\n    }\n  }\n\n  override def materializeIntoVectorWithRowIndex(\n      batchSize: Int,\n      rowIndexColumn: ColumnVector,\n      batch: WritableColumnVector): Unit = {\n    for (rowNumber <- 0 until batchSize) {\n      val rowIndex = rowIndexColumn.getLong(rowNumber)\n      val isContained = isContainedInBitmap(rowIndex)\n      batch.putByte(rowNumber, isContained)\n    }\n  }\n\n  override def materializeSingleRowWithRowIndex(\n      rowIndex: Long,\n      batch: WritableColumnVector): Unit = {\n    val isContained = isContainedInBitmap(rowIndex)\n    // Assumes the batch has only one element.\n    batch.putByte(0, isContained)\n  }\n}\n\nsealed trait RowIndexMarkingFiltersBuilder {\n  def getFilterForEmptyDeletionVector(): RowIndexFilter\n  def getFilterForNonEmptyDeletionVector(bitmap: RoaringBitmapArray): RowIndexFilter\n\n  def createInstance(\n      deletionVector: DeletionVectorDescriptor,\n      hadoopConf: Configuration,\n      tablePath: Option[Path]): RowIndexFilter = {\n    if (deletionVector.cardinality == 0) {\n      getFilterForEmptyDeletionVector()\n    } else {\n      require(tablePath.nonEmpty, \"Table path is required for non-empty deletion vectors\")\n      val dvStore = DeletionVectorStore.createInstance(hadoopConf)\n      val storedBitmap = StoredBitmap.create(deletionVector, tablePath.get)\n      val bitmap = storedBitmap.load(dvStore)\n      getFilterForNonEmptyDeletionVector(bitmap)\n    }\n  }\n}\n\n/**\n * Implementation of [[RowIndexFilter]] which checks, for a given row index and deletion vector,\n * whether the row index is present in the deletion vector. If present, the row is marked for\n * skipping.\n * @param bitmap Represents the deletion vector\n */\nfinal class DropMarkedRowsFilter(bitmap: RoaringBitmapArray)\n  extends RowIndexMarkingFilters(bitmap) {\n  override val valueWhenContained: Byte = RowIndexFilter.DROP_ROW_VALUE\n  override val valueWhenNotContained: Byte = RowIndexFilter.KEEP_ROW_VALUE\n}\n\n/**\n * Utility methods that creates [[DropMarkedRowsFilter]] to filter out row indices that are present\n * in the given deletion vector.\n */\nobject DropMarkedRowsFilter extends RowIndexMarkingFiltersBuilder {\n  override def getFilterForEmptyDeletionVector(): RowIndexFilter = KeepAllRowsFilter\n\n  override def getFilterForNonEmptyDeletionVector(bitmap: RoaringBitmapArray): RowIndexFilter =\n    new DropMarkedRowsFilter(bitmap)\n}\n\n/**\n * Implementation of [[RowIndexFilter]] which checks, for a given row index and deletion vector,\n * whether the row index is present in the deletion vector. If not present, the row is marked for\n * skipping.\n * @param bitmap Represents the deletion vector\n */\nfinal class KeepMarkedRowsFilter(bitmap: RoaringBitmapArray)\n  extends RowIndexMarkingFilters(bitmap) {\n  override val valueWhenContained: Byte = RowIndexFilter.KEEP_ROW_VALUE\n  override val valueWhenNotContained: Byte = RowIndexFilter.DROP_ROW_VALUE\n}\n\n/**\n * Utility methods that creates [[KeepMarkedRowsFilter]] to filter out row indices that are present\n * in the given deletion vector.\n */\nobject KeepMarkedRowsFilter extends RowIndexMarkingFiltersBuilder {\n  override def getFilterForEmptyDeletionVector(): RowIndexFilter = DropAllRowsFilter\n\n  override def getFilterForNonEmptyDeletionVector(bitmap: RoaringBitmapArray): RowIndexFilter =\n    new KeepMarkedRowsFilter(bitmap)\n}\n\ncase object DropAllRowsFilter extends RowIndexFilter {\n  override def materializeIntoVector(start: Long, end: Long, batch: WritableColumnVector): Unit = {\n    val batchSize = (end - start).toInt\n    var rowId = 0\n    while (rowId < batchSize) {\n      batch.putByte(rowId, RowIndexFilter.DROP_ROW_VALUE)\n      rowId += 1\n    }\n  }\n\n  override def materializeIntoVectorWithRowIndex(\n      batchSize: Int,\n      rowIndexColumn: ColumnVector,\n      batch: WritableColumnVector): Unit = {\n    for (rowId <- 0 until batchSize) {\n      batch.putByte(rowId, RowIndexFilter.DROP_ROW_VALUE)\n    }\n  }\n\n  override def materializeSingleRowWithRowIndex(\n      rowIndex: Long,\n      batch: WritableColumnVector): Unit =\n    // Assumes the batch has only one element.\n    batch.putByte(0, RowIndexFilter.DROP_ROW_VALUE)\n}\n\ncase object KeepAllRowsFilter extends RowIndexFilter {\n  override def materializeIntoVector(start: Long, end: Long, batch: WritableColumnVector): Unit = {\n    val batchSize = (end - start).toInt\n    var rowId = 0\n    while (rowId < batchSize) {\n      batch.putByte(rowId, RowIndexFilter.KEEP_ROW_VALUE)\n      rowId += 1\n    }\n  }\n\n  override def materializeIntoVectorWithRowIndex(\n      batchSize: Int,\n      rowIndexColumn: ColumnVector,\n      batch: WritableColumnVector): Unit = {\n    for (rowId <- 0 until batchSize) {\n      batch.putByte(rowId, RowIndexFilter.KEEP_ROW_VALUE)\n    }\n  }\n\n  override def materializeSingleRowWithRowIndex(\n      rowIndex: Long,\n      batch: WritableColumnVector): Unit =\n    // Assumes the batch has only one element.\n    batch.putByte(0, RowIndexFilter.KEEP_ROW_VALUE)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/deletionvectors/StoredBitmap.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.deletionvectors\n\nimport java.io.{IOException, ObjectInputStream}\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.util.Utils\n\n/**\n * Interface for bitmaps that are stored as Deletion Vectors.\n */\ntrait StoredBitmap {\n  /**\n   * Read the bitmap into memory.\n   * Use `dvStore` if this variant is in cloud storage, otherwise just deserialize.\n   */\n  def load(dvStore: DeletionVectorStore): RoaringBitmapArray\n\n  /**\n   * The serialized size of the stored bitmap in bytes.\n   * Can be used for planning memory management without a round-trip to cloud storage.\n   */\n  def size: Int\n\n  /**\n   * The number of entries in the bitmap.\n   */\n  def cardinality: Long\n\n  /**\n   * Returns a unique identifier for this bitmap (Deletion Vector serialized as a JSON object).\n   */\n  def getUniqueId: String\n}\n\n/**\n * Bitmap for a Deletion Vector, implemented as a thin wrapper around a Deletion Vector\n * Descriptor. The bitmap can be empty, inline or on-disk. In case of on-disk deletion\n * vectors, `tableDataPath` must be set to the data path of the Delta table, which is where\n * deletion vectors are stored.\n */\ncase class DeletionVectorStoredBitmap(\n    dvDescriptor: DeletionVectorDescriptor,\n    tableDataPath: Option[Path] = None\n) extends StoredBitmap with DeltaLogging {\n  require(tableDataPath.isDefined || !dvDescriptor.isOnDisk,\n    \"Table path is required for on-disk deletion vectors\")\n\n  override def load(dvStore: DeletionVectorStore): RoaringBitmapArray = {\n    val bitmap = if (isEmpty) {\n      new RoaringBitmapArray()\n    } else if (isInline) {\n      DeletionVectorUtils.deserialize(\n        dvDescriptor.inlineData,\n        tableDataPath,\n        debugInfo = Map(\"dvDescriptor\" -> dvDescriptor))\n    } else {\n      assert(isOnDisk)\n      dvStore.read(onDiskPath.get, dvDescriptor.offset.getOrElse(0), dvDescriptor.sizeInBytes)\n    }\n\n    // Verify that the cardinality in the bitmap matches the DV descriptor.\n    if (bitmap.cardinality != dvDescriptor.cardinality) {\n      recordDeltaEvent(\n        deltaLog = null,\n        opType = \"delta.assertions.deletionVectorReadCardinalityMismatch\",\n        data = Map(\n          \"deletionVectorPath\" -> onDiskPath,\n          \"deletionVectorCardinality\" -> bitmap.cardinality,\n          \"deletionVectorDescriptor\" -> dvDescriptor),\n        path = tableDataPath)\n      throw DeltaErrors.deletionVectorCardinalityMismatch()\n    }\n\n    bitmap\n  }\n\n  override def size: Int = dvDescriptor.sizeInBytes\n\n  override def cardinality: Long = dvDescriptor.cardinality\n\n  override lazy val getUniqueId: String = dvDescriptor.serializeToBase64()\n\n  private def isEmpty: Boolean = dvDescriptor.isEmpty\n\n  private def isInline: Boolean = dvDescriptor.isInline\n\n  private def isOnDisk: Boolean = dvDescriptor.isOnDisk\n\n  /** The absolute path for on-disk deletion vectors. */\n  private lazy val onDiskPath: Option[Path] = tableDataPath.map(dvDescriptor.absolutePath)\n}\n\nobject StoredBitmap {\n  /** The stored bitmap of an empty deletion vector. */\n  final val EMPTY = DeletionVectorStoredBitmap(DeletionVectorDescriptor.EMPTY, None)\n\n\n  /** Factory for inline deletion vectors. */\n  def inline(dvDescriptor: DeletionVectorDescriptor): StoredBitmap = {\n    require(dvDescriptor.isInline)\n    DeletionVectorStoredBitmap(dvDescriptor, None)\n  }\n\n  /** Factory for deletion vectors. */\n  def create(dvDescriptor: DeletionVectorDescriptor, tablePath: Path): StoredBitmap = {\n    if (dvDescriptor.isOnDisk) {\n      DeletionVectorStoredBitmap(dvDescriptor, Some(tablePath))\n    } else {\n      DeletionVectorStoredBitmap(dvDescriptor, None)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/DecodeNestedZ85EncodedVariant.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis.TypeCheckResult\nimport org.apache.spark.sql.catalyst.expressions.{Expression, UnaryExpression}\nimport org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback\nimport org.apache.spark.sql.delta.util.DeltaStatsJsonUtils\nimport org.apache.spark.sql.types._\nimport org.apache.spark.types.variant.{Variant, VariantUtil}\nimport org.apache.spark.unsafe.types.VariantVal\n\n/**\n * An expression that replaces Z85-encoded variant strings with decoded VariantVals.\n *\n * When parsing JSON stats with variant fields, the variants are initially encoded as Z85 strings.\n * The standard from_json treats these as regular strings and creates VariantVal objects that\n * contain the Z85 string representation. This expression walks through the result and decodes\n * any Z85-encoded variants to their proper binary representation.\n *\n * @param child The expression producing the row with Z85-encoded variants.\n */\ncase class DecodeNestedZ85EncodedVariant(child: Expression)\n  extends UnaryExpression with CodegenFallback {\n\n  override def dataType: DataType = child.dataType\n\n  override def nullable: Boolean = child.nullable\n\n  override def checkInputDataTypes(): TypeCheckResult = {\n    if (!child.dataType.isInstanceOf[StructType]) {\n      TypeCheckResult.TypeCheckFailure(s\"The top-level data type for the input to \" +\n        s\"DecodeNestedZ85EncodedVariant must be StructType but this is not true \" +\n        s\"in: ${child.dataType}.\")\n    } else if (!isValidType(child.dataType)) {\n      TypeCheckResult.TypeCheckFailure(\n        s\"DecodeNestedZ85EncodedVariant does not support arrays or maps in schema. \" +\n        s\"Found: ${child.dataType}\")\n    } else {\n      TypeCheckResult.TypeCheckSuccess\n    }\n  }\n\n  // The data type cannot contain arrays or maps since stats structs do not have arrays or maps yet.\n  private def isValidType(dataType: DataType): Boolean = {\n    dataType match {\n      case _: ArrayType => false\n      case _: MapType => false\n      case st: StructType =>\n        st.fields.forall(field => isValidType(field.dataType))\n      case _ => true\n    }\n  }\n\n  override protected def nullSafeEval(input: Any): Any = {\n    transformValue(input, child.dataType)\n  }\n\n  private def transformValue(value: Any, dataType: DataType): Any = {\n    if (value == null) {\n      return null\n    }\n\n    dataType match {\n      case VariantType =>\n        val variantVal = value.asInstanceOf[VariantVal]\n        val variant = new Variant(variantVal.getValue, variantVal.getMetadata)\n        if (VariantUtil.getType(variant.getValue, 0) == VariantUtil.Type.STRING) {\n          val z85String = variant.getString()\n          DeltaStatsJsonUtils.decodeVariantFromZ85(z85String)\n        } else {\n          throw new IllegalStateException(\n            s\"Expected Z85-encoded variant string but got type \" +\n              s\"${VariantUtil.getType(variant.getValue, 0)}\")\n        }\n\n      case st: StructType =>\n        val row = value.asInstanceOf[InternalRow]\n        val newValues = st.fields.zipWithIndex.map { case (field, i) =>\n          val fieldValue = row.get(i, field.dataType)\n          transformValue(fieldValue, field.dataType)\n        }\n        InternalRow.fromSeq(newValues)\n\n      case _ =>\n        value\n    }\n  }\n\n  override def prettyName: String = \"replace_variant_z85_with_variant_val\"\n\n  override protected def withNewChildInternal(newChild: Expression)\n      : DecodeNestedZ85EncodedVariant = copy(child = newChild)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/EncodeNestedVariantAsZ85String.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis.TypeCheckResult\nimport org.apache.spark.sql.catalyst.expressions.{Expression, UnaryExpression}\nimport org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback\nimport org.apache.spark.sql.delta.util.DeltaStatsJsonUtils\nimport org.apache.spark.sql.types._\nimport org.apache.spark.types.variant.Variant\nimport org.apache.spark.unsafe.types.{UTF8String, VariantVal}\n\n/**\n * An expression that encodes VariantVal fields in a struct as Z85 strings.\n *\n * When converting stats structs to JSON for state reconstruction, variants need to be\n * encoded as Z85 strings to preserve their binary representation. This expression walks\n * through the struct and replaces any VariantVal fields with their Z85 string encoding.\n *\n * The output schema has VariantType fields replaced with StringType.\n *\n * @param child The expression producing the row with VariantVal fields.\n */\ncase class EncodeNestedVariantAsZ85String(child: Expression)\n  extends UnaryExpression with CodegenFallback {\n\n  override def dataType: DataType = transformDataType(child.dataType)\n\n  override def nullable: Boolean = child.nullable\n\n  override def checkInputDataTypes(): TypeCheckResult = {\n    if (!child.dataType.isInstanceOf[StructType]) {\n      TypeCheckResult.TypeCheckFailure(s\"The top-level data type for the input to \" +\n        s\"EncodeNestedVariantAsZ85String must be StructType but this is not true \" +\n        s\"in: ${child.dataType}.\")\n    } else if (!isValidType(child.dataType)) {\n      TypeCheckResult.TypeCheckFailure(\n        s\"EncodeNestedVariantAsZ85String does not support arrays or maps in schema. \" +\n        s\"Found: ${child.dataType}\")\n    } else {\n      TypeCheckResult.TypeCheckSuccess\n    }\n  }\n\n  // The data type cannot contain arrays or maps since stats structs do not have arrays or maps yet.\n  private def isValidType(dataType: DataType): Boolean = {\n    dataType match {\n      case _: ArrayType => false\n      case _: MapType => false\n      case st: StructType =>\n        st.fields.forall(field => isValidType(field.dataType))\n      case _ => true\n    }\n  }\n\n  /**\n   * Transform the data type by replacing VariantType with StringType.\n   */\n  private def transformDataType(dataType: DataType): DataType = {\n    dataType match {\n      case VariantType => StringType\n      case st: StructType =>\n        StructType(st.fields.map { field =>\n          field.copy(dataType = transformDataType(field.dataType))\n        })\n      case other => other\n    }\n  }\n\n  override protected def nullSafeEval(input: Any): Any = {\n    transformValue(input, child.dataType)\n  }\n\n  private def transformValue(value: Any, dataType: DataType): Any = {\n    if (value == null) {\n      return null\n    }\n\n    dataType match {\n      case VariantType =>\n        val variantVal = value.asInstanceOf[VariantVal]\n        val variant = new Variant(variantVal.getValue, variantVal.getMetadata)\n        val z85String = DeltaStatsJsonUtils.encodeVariantAsZ85(variant)\n        UTF8String.fromString(z85String)\n\n      case st: StructType =>\n        val row = value.asInstanceOf[InternalRow]\n        val newValues = st.fields.zipWithIndex.map { case (field, i) =>\n          val fieldValue = row.get(i, field.dataType)\n          transformValue(fieldValue, field.dataType)\n        }\n        InternalRow.fromSeq(newValues)\n\n      case _ =>\n        value\n    }\n  }\n\n  override def prettyName: String = \"encode_variant_as_z85_string\"\n\n  override protected def withNewChildInternal(newChild: Expression)\n      : EncodeNestedVariantAsZ85String = copy(child = newChild)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/HilbertIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport java.util\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.expressions.HilbertUtils._\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes, Expression}\nimport org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback\nimport org.apache.spark.sql.types.{AbstractDataType, DataType, DataTypes}\n\n/**\n * Represents a hilbert index built from the provided columns.\n * The columns are expected to all be Ints and to have at most numBits individually.\n * The points along the hilbert curve are represented by Longs.\n */\nprivate[sql] case class HilbertLongIndex(numBits: Int, children: Seq[Expression])\n    extends Expression with ExpectsInputTypes with CodegenFallback {\n\n  private val n: Int = children.size\n  private val nullValue: Int = 0\n\n  override def nullable: Boolean = false\n\n  // pre-initialize working set array\n  private val ints = new Array[Int](n)\n\n  override def eval(input: InternalRow): Any = {\n    var i = 0\n    while (i < n) {\n      ints(i) = children(i).eval(input) match {\n        case null => nullValue\n        case int: Integer => int\n        case any => throw new IllegalArgumentException(\n          s\"${this.getClass.getSimpleName} expects only inputs of type Int, but got: \" +\n            s\"$any of type${any.getClass.getSimpleName}\")\n      }\n      i += 1\n    }\n\n    HilbertStates.getStateList(n).translateNPointToDKey(ints, numBits)\n  }\n\n  override def dataType: DataType = DataTypes.LongType\n\n  override def inputTypes: Seq[AbstractDataType] = Seq.fill(n)(DataTypes.IntegerType)\n\n  override protected def withNewChildrenInternal(\n    newChildren: IndexedSeq[Expression]): HilbertLongIndex = copy(children = newChildren)\n}\n\n/**\n * Represents a hilbert index built from the provided columns.\n * The columns are expected to all be Ints and to have at most numBits.\n * The points along the hilbert curve are represented by Byte arrays.\n */\nprivate[sql] case class HilbertByteArrayIndex(numBits: Int, children: Seq[Expression])\n    extends Expression with ExpectsInputTypes with CodegenFallback {\n\n  private val n: Int = children.size\n  private val nullValue: Int = 0\n\n  override def nullable: Boolean = false\n\n  // pre-initialize working set array\n  private val ints = new Array[Int](n)\n\n  override def eval(input: InternalRow): Any = {\n    var i = 0\n    while (i < n) {\n      ints(i) = children(i).eval(input) match {\n        case null => nullValue\n        case int: Integer => int\n        case any => throw new IllegalArgumentException(\n          s\"${this.getClass.getSimpleName} expects only inputs of type Int, but got: \" +\n            s\"$any of type${any.getClass.getSimpleName}\")\n      }\n      i += 1\n    }\n\n    HilbertStates.getStateList(n).translateNPointToDKeyArray(ints, numBits)\n  }\n\n  override def dataType: DataType = DataTypes.BinaryType\n\n  override def inputTypes: Seq[AbstractDataType] = Seq.fill(n)(DataTypes.IntegerType)\n\n  override protected def withNewChildrenInternal(\n    newChildren: IndexedSeq[Expression]): HilbertByteArrayIndex = copy(children = newChildren)\n}\n\n// scalastyle:off line.size.limit\n/**\n * The following code is based on this paper:\n *   https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=bfd6d94c98627756989b0147a68b7ab1f881a0d6\n * with optimizations around matrix manipulation taken from this one:\n *   https://pdfs.semanticscholar.org/4043/1c5c43a2121e1bc071fc035e90b8f4bb7164.pdf\n *\n * At a high level you construct a GeneratorTable with the getStateGenerator method.\n * That represents the information necessary to construct a state list for a given number\n * of dimension, N.\n * Once you have the generator table for your dimension you can construct a state list.\n * You can then turn those state lists into compact state lists that store all the information\n * in one large array of longs.\n */\n// scalastyle:on line.size.limit\nobject HilbertIndex {\n\n  private type CompactStateList = HilbertCompactStateList\n\n  val SIZE_OF_INT = 32\n\n  /**\n   * Construct the generator table for a space of dimension n.\n   * This table consists of 2^n rows, each row containing Y, X1, and TY.\n   *   Y    The index in the array representing the table. (0 to (2^n - 1))\n   *   X1   A coordinate representing points on the curve expressed as an n-point.\n   *        These are arranged such that if two rows differ by 1 in Y then the binary\n   *        representation of their X1 values differ by exactly one bit.\n   *        These are the \"Gray-codes\" of their Y value.\n   *   TY   A transformation matrix that transforms X2(1) to the X1 value where Y is zero and\n   *        transforms X2(2) to the X1 value where Y is (2^n - 1)\n   */\n  def getStateGenerator(n: Int): GeneratorTable = {\n    val x2s = getX2GrayCodes(n)\n\n    val len = 1 << n\n    val rows = (0 until len).map { i =>\n      // A pair of n-points corresponding to the first and last points on the first order curve to\n      // which X1 transforms in the construction of a second order curve.\n      val x21 = x2s(i << 1)\n      val x22 = x2s((i << 1) + 1)\n      // Represents the magnitude of difference between X2 values in this row.\n      val dy = x21 ^ x22\n\n      Row(\n        y = i,\n        x1 = i ^ (i >>> 1),\n        m = HilbertMatrix(n, x21, getSetColumn(n, dy))\n      )\n    }\n\n    new GeneratorTable(n, rows)\n  }\n\n  // scalastyle:off line.size.limit\n  /**\n   * This will construct an x2-gray-codes sequence of order n as described in\n   *  https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=bfd6d94c98627756989b0147a68b7ab1f881a0d6\n   *\n   *   Each pair of values corresponds to the first and last coordinates of points on a first\n   *   order curve to which a point taken from column X1 transforms to at the second order.\n   */\n  // scalastyle:on line.size.limit\n  private[this] def getX2GrayCodes(n: Int) : Array[Int] = {\n    if (n == 1) {\n      // hard code the base case\n      return Array(0, 1, 0, 1)\n    }\n    val mask = 1 << (n - 1)\n    val base = getX2GrayCodes(n - 1)\n    base(base.length - 1) = base(base.length - 2) + mask\n    val result = Array.fill(base.length * 2)(0)\n    base.indices.foreach { i =>\n      result(i) = base(i)\n      result(result.length - 1 - i) = base(i) ^ mask\n    }\n    result\n  }\n\n  private[this] case class Row(y: Int, x1: Int, m: HilbertMatrix)\n\n  private[this] case class PointState(y: Int, var x1: Int = 0, var state: Int = 0)\n\n  private[this] case class State(id: Int, matrix: HilbertMatrix, var pointStates: Seq[PointState])\n\n  private[sql] class StateList(n: Int, states: Map[Int, State]) {\n    def getNPointToDKeyStateMap: CompactStateList = {\n      val numNPoints = 1 << n\n      val array = new Array[Long](numNPoints * states.size)\n\n      states.foreach { case (stateIdx, state) =>\n        val stateStartIdx = stateIdx * numNPoints\n\n        state.pointStates.foreach { ps =>\n          val psLong = (ps.y.toLong << SIZE_OF_INT) | ps.state.toLong\n          array(stateStartIdx + ps.x1) = psLong\n        }\n      }\n      new CompactStateList(n, array)\n    }\n    def getDKeyToNPointStateMap: CompactStateList = {\n      val numNPoints = 1 << n\n      val array = new Array[Long](numNPoints * states.size)\n\n      states.foreach { case (stateIdx, state) =>\n        val stateStartIdx = stateIdx * numNPoints\n\n        state.pointStates.foreach { ps =>\n          val psLong = (ps.x1.toLong << SIZE_OF_INT) | ps.state.toLong\n          array(stateStartIdx + ps.y) = psLong\n        }\n      }\n      new CompactStateList(n, array)\n    }\n  }\n\n  private[sql] class GeneratorTable(n: Int, rows: Seq[Row]) {\n    def generateStateList(): StateList = {\n      val result = mutable.Map[Int, State]()\n      val list = new util.LinkedList[State]()\n\n      var nextStateNum = 1\n\n      val initialState = State(0, HilbertMatrix.identity(n), rows.map(r => PointState(r.y, r.x1)))\n      result.put(0, initialState)\n\n      rows.foreach { row =>\n        val matrix = row.m\n        result.find { case (_, s) => s.matrix == matrix } match {\n          case Some((_, s)) =>\n            initialState.pointStates(row.y).state = s.id\n          case _ =>\n            initialState.pointStates(row.y).state = nextStateNum\n            val newState = State(nextStateNum, matrix, Seq())\n            result.put(nextStateNum, newState)\n            list.addLast(newState)\n            nextStateNum += 1\n        }\n      }\n\n      while (!list.isEmpty) {\n        val currentState = list.removeFirst()\n        currentState.pointStates = rows.indices.map(r => PointState(r))\n\n        rows.indices.foreach { i =>\n          val j = currentState.matrix.transform(i)\n          val p = initialState.pointStates.find(_.x1 == j).get\n          val currentPointState = currentState.pointStates(p.y)\n          currentPointState.x1 = i\n          val tm = result(p.state).matrix.multiply(currentState.matrix)\n\n          result.find { case (_, s) => s.matrix == tm } match {\n            case Some((_, s)) =>\n              currentPointState.state = s.id\n            case _ =>\n              currentPointState.state = nextStateNum\n              val newState = State(nextStateNum, tm, Seq())\n              result.put(nextStateNum, newState)\n              list.addLast(newState)\n              nextStateNum += 1\n          }\n        }\n      }\n\n      new StateList(n, result.toMap)\n    }\n  }\n}\n\n/**\n * Represents a compact state map. This is used in the mapping between n-points and d-keys.\n * [[array]] is treated as a Map(Int -> Map(Int -> (Int, Int)))\n *\n * Each values in the array will be a combination of two things, a point and the index of the\n * next state, in the most- and least- significant bits, respectively.\n *   state -> coord -> [point + nextState]\n */\nprivate[sql] class HilbertCompactStateList(n: Int, array: Array[Long]) {\n    private val maxNumN = 1 << n\n    private val mask = maxNumN - 1\n    private val intMask = (1L << HilbertIndex.SIZE_OF_INT) - 1\n\n    // point and nextState\n    @inline def transform(nPoint: Int, state: Int): (Int, Int) = {\n      val value = array(state * maxNumN + nPoint)\n      (\n        (value >>> HilbertIndex.SIZE_OF_INT).toInt,\n        (value & intMask).toInt\n      )\n    }\n\n    // These while loops are to minimize overhead.\n    // This method exists only for testing\n    private[expressions] def translateDKeyToNPoint(key: Long, k: Int): Array[Int] = {\n      val result = new Array[Int](n)\n      var currentState = 0\n      var i = 0\n      while (i < k) {\n        val h = (key >> ((k - 1 - i) * n)) & mask\n\n        val (z, nextState) = transform(h.toInt, currentState)\n\n        var j = 0\n        while (j < n) {\n          val v = (z >> (n - 1 - j)) & 1\n          result(j) = (result(j) << 1) | v\n          j += 1\n        }\n\n        currentState = nextState\n        i += 1\n      }\n      result\n    }\n\n    // These while loops are to minimize overhead.\n    // This method exists only for testing\n    private[expressions] def translateDKeyArrayToNPoint(key: Array[Byte], k: Int): Array[Int] = {\n      val result = new Array[Int](n)\n      val initialOffset = (key.length * 8) - (k * n)\n      var currentState = 0\n      var i = 0\n      while (i < k) {\n        val offset = initialOffset + (i * n)\n        val h = getBits(key, offset, n)\n\n        val (z, nextState) = transform(h, currentState)\n\n        var j = 0\n        while (j < n) {\n          val v = (z >> (n - 1 - j)) & 1\n          result(j) = (result(j) << 1) | v\n          j += 1\n        }\n\n        currentState = nextState\n        i += 1\n      }\n      result\n    }\n\n    /**\n     * Translate an n-dimensional point into it's corresponding position on the n-dimensional\n     * hilbert curve.\n     * @param point An n-dimensional point. (assumed to have n elements)\n     * @param k     The number of meaningful bits in each value of the point.\n     */\n    def translateNPointToDKey(point: Array[Int], k: Int): Long = {\n      var result = 0L\n      var currentState = 0\n      var i = 0\n      while (i < k) {\n        var z = 0\n        var j = 0\n        while (j < n) {\n          z = (z << 1) | ((point(j) >> (k - 1 - i)) & 1)\n          j += 1\n        }\n        val (h, nextState) = transform(z, currentState)\n        result = (result << n) | h\n        currentState = nextState\n        i += 1\n      }\n      result\n    }\n\n    /**\n     * Translate an n-dimensional point into it's corresponding position on the n-dimensional\n     * hilbert curve. Returns the resulting integer as an array of bytes.\n     * @param point An n-dimensional point. (assumed to have n elements)\n     * @param k     The number of meaningful bits in each value of the point.\n     */\n    def translateNPointToDKeyArray(point: Array[Int], k: Int): Array[Byte] = {\n      val numBits = k * n\n      val numBytes = (numBits + 7) / 8\n      val result = new Array[Byte](numBytes)\n      val initialOffset = (numBytes * 8) - numBits\n      var currentState = 0\n      var i = 0\n      while (i < k) {\n        var z = 0\n        var j = 0\n        while (j < n) {\n          z = (z << 1) | ((point(j) >> (k - 1 - i)) & 1)\n          j += 1\n        }\n        val (h, nextState) = transform(z, currentState)\n        setBits(result, initialOffset + (i * n), h, n)\n        currentState = nextState\n        i += 1\n      }\n      result\n    }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/HilbertStates.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions;\n\nimport org.apache.spark.SparkException;\n\npublic class HilbertStates {\n\n    /**\n     * Constructs a hilbert state for the given arity, [[n]].\n     * This state list can be used to map n-points to their corresponding d-key value.\n     *\n     * @param n The number of bits in this space (we assert 2 <= n <= 9 for simplicity)\n     * @return The CompactStateList for mapping from n-point to hilbert distance key.\n     */\n    private static HilbertCompactStateList constructHilbertState(int n) {\n        HilbertIndex.GeneratorTable generator = HilbertIndex.getStateGenerator(n);\n        return generator.generateStateList().getNPointToDKeyStateMap();\n    }\n\n    private HilbertStates() { }\n\n    private static class HilbertIndex2 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(2);\n    }\n\n    private static class HilbertIndex3 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(3);\n    }\n\n    private static class HilbertIndex4 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(4);\n    }\n\n    private static class HilbertIndex5 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(5);\n    }\n\n    private static class HilbertIndex6 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(6);\n    }\n\n    private static class HilbertIndex7 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(7);\n    }\n\n    private static class HilbertIndex8 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(8);\n    }\n\n    private static class HilbertIndex9 {\n        static final HilbertCompactStateList STATE_LIST = constructHilbertState(9);\n    }\n\n    public static HilbertCompactStateList getStateList(int n) throws SparkException {\n        switch (n) {\n            case 2:\n                return HilbertIndex2.STATE_LIST;\n            case 3:\n                return HilbertIndex3.STATE_LIST;\n            case 4:\n                return HilbertIndex4.STATE_LIST;\n            case 5:\n                return HilbertIndex5.STATE_LIST;\n            case 6:\n                return HilbertIndex6.STATE_LIST;\n            case 7:\n                return HilbertIndex7.STATE_LIST;\n            case 8:\n                return HilbertIndex8.STATE_LIST;\n            case 9:\n                return HilbertIndex9.STATE_LIST;\n            default:\n                throw new SparkException(String.format(\"Cannot perform hilbert clustering on \" +\n                    \"fewer than 2 or more than 9 dimensions; got %d dimensions\", n));\n        }\n    }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/HilbertUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nobject HilbertUtils {\n\n  /**\n   * Returns the column number that is set. We assume that a bit is set.\n   */\n  @inline def getSetColumn(n: Int, i: Int): Int = {\n    n - 1 - Integer.numberOfTrailingZeros(i)\n  }\n\n  @inline def circularLeftShift(n: Int, i: Int, shift: Int): Int = {\n    ((i << shift) | (i >>> (n - shift))) & ((1 << n) - 1)\n  }\n\n  @inline def circularRightShift(n: Int, i: Int, shift: Int): Int = {\n    ((i >>> shift) | (i << (n - shift))) & ((1 << n) - 1)\n  }\n\n  @inline\n  private[expressions] def getBits(key: Array[Byte], offset: Int, n: Int): Int = {\n    // [    ][    ][    ][    ][    ]\n    // <---offset---> [  n-bits  ]      <- this is the result\n    var result = 0\n\n    var remainingBits = n\n    var keyIndex = offset / 8\n    // initial key offset\n    var keyOffset = offset - (keyIndex * 8)\n    while (remainingBits > 0) {\n      val bitsFromIdx = math.min(remainingBits, 8 - keyOffset)\n      val newInt = if (remainingBits >= 8) {\n        java.lang.Byte.toUnsignedInt(key(keyIndex))\n      } else {\n        java.lang.Byte.toUnsignedInt(key(keyIndex)) >>> (8 - keyOffset - bitsFromIdx)\n      }\n      result = (result << bitsFromIdx) | (newInt & ((1 << bitsFromIdx) - 1))\n\n      remainingBits -= (8 - keyOffset)\n      keyOffset = 0\n      keyIndex += 1\n    }\n\n    result\n  }\n\n  @inline\n  private[expressions] def setBits(\n      key: Array[Byte],\n      offset: Int,\n      newBits: Int,\n      n: Int): Array[Byte] = {\n    // bits: [   meaningless bits   ][  n meaningful bits  ]\n    //\n    // [    ][    ][    ][    ][    ]\n    // <---offset---> [  n-bits  ]\n\n    // move meaningful bits to the far left\n    var bits = newBits << (32 - n)\n    var remainingBits = n\n\n    // initial key index\n    var keyIndex = offset / 8\n    // initial key offset\n    var keyOffset = offset - (keyIndex * 8)\n    while (remainingBits > 0) {\n      key(keyIndex) = (key(keyIndex) | (bits >>> (24 + keyOffset))).toByte\n      remainingBits -= (8 - keyOffset)\n      bits = bits << (8 - keyOffset)\n      keyOffset = 0\n      keyIndex += 1\n    }\n    key\n  }\n\n  /**\n   * treats `key` as an Integer and adds 1\n   */\n  @inline def addOne(key: Array[Byte]): Array[Byte] = {\n    var idx = key.length - 1\n    var overflow = true\n    while (overflow && idx >= 0) {\n      key(idx) = (key(idx) + 1.toByte).toByte\n      overflow = key(idx) == 0\n      idx -= 1\n    }\n    key\n  }\n\n  def manhattanDist(p1: Array[Int], p2: Array[Int]): Int = {\n    assert(p1.length == p2.length)\n    p1.zip(p2).map { case (a, b) => math.abs(a - b) }.sum\n  }\n\n\n  /**\n   * This is not really a matrix, but a representation of one. Due to the constraints of this\n   * system the necessary matrices can be defined by two values: dY and X2. DY is the amount\n   * of right shifting of the identity matrix, and X2 is a bitmask for which column values are\n   * negative. The [[toString]] method is overridden to construct and print the matrix to aid\n   * in debugging.\n   * Instead of constructing the matrix directly we store and manipulate these values.\n   */\n  case class HilbertMatrix(n: Int, x2: Int, dy: Int) {\n    override def toString(): String = {\n      val sb = new StringBuilder()\n\n      val base = 1 << (n - 1 - dy)\n      (0 until n).foreach { i =>\n        sb.append('\\n')\n        val row = circularRightShift(n, base, i)\n        (0 until n).foreach { j =>\n          if (isColumnSet(row, j)) {\n            if (isColumnSet(x2, j)) {\n              sb.append('-')\n            } else {\n              sb.append(' ')\n            }\n            sb.append('1')\n          } else {\n            sb.append(\" 0\")\n          }\n        }\n      }\n      sb.append('\\n')\n      sb.toString\n    }\n\n    // columns count from the left: 0, 1, 2 ... , n\n    @inline def isColumnSet(i: Int, column: Int): Boolean = {\n      val mask = 1 << (n - 1 - column)\n      (i & mask) > 0\n    }\n\n    def transform(e: Int): Int = {\n      circularLeftShift(n, e ^ x2, dy)\n    }\n\n    def multiply(other: HilbertMatrix): HilbertMatrix = {\n      HilbertMatrix(n, circularRightShift(n, x2, other.dy) ^ other.x2, (dy + other.dy) % n)\n    }\n  }\n\n  object HilbertMatrix {\n    def identity(n: Int): HilbertMatrix = {\n      HilbertMatrix(n, 0, 0)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/InterleaveBits.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.catalyst.{InternalRow, SQLConfHelper}\nimport org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes, Expression}\nimport org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback\nimport org.apache.spark.sql.types.{BinaryType, DataType, IntegerType}\n\n\n/**\n * Interleaves the bits of its input data in a round-robin fashion.\n *\n * If the input data is seen as a series of multidimensional points, this function computes the\n * corresponding Z-values, in a way that's preserving data locality: input points that are close\n * in the multidimensional space will be mapped to points that are close on the Z-order curve.\n *\n * The returned value is a byte array where the size of the array is 4 * num of input columns.\n *\n * @see https://en.wikipedia.org/wiki/Z-order_curve\n *\n * @note Only supports input expressions of type Int for now.\n */\ncase class InterleaveBits(children: Seq[Expression])\n  extends Expression with ExpectsInputTypes with SQLConfHelper\n    with CodegenFallback /* TODO: implement doGenCode() */ {\n\n  private val n: Int = children.size\n\n  override def inputTypes: Seq[DataType] = Seq.fill(n)(IntegerType)\n\n  override def dataType: DataType = BinaryType\n\n  override def nullable: Boolean = false\n\n  /** Nulls in the input will be treated like this value */\n  val nullValue: Int = 0\n\n  private val childrenArray: Array[Expression] = children.toArray\n\n  private val fastInterleaveBitsEnabled = conf.getConf(DeltaSQLConf.FAST_INTERLEAVE_BITS_ENABLED)\n\n  private val ints = new Array[Int](n)\n\n  override def eval(input: InternalRow): Any = {\n    var i = 0\n    while (i < n) {\n      val int = childrenArray(i).eval(input) match {\n        case null => nullValue\n        case int: Int => int\n        case any => throw new IllegalArgumentException(\n          s\"${this.getClass.getSimpleName} expects only inputs of type Int, but got: \" +\n            s\"$any of type${any.getClass.getSimpleName}\")\n      }\n      ints.update(i, int)\n      i += 1\n    }\n    InterleaveBits.interleaveBits(ints, fastInterleaveBitsEnabled)\n  }\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): InterleaveBits = copy(children = newChildren)\n}\n\nobject InterleaveBits {\n\n  private[expressions] def interleaveBits(\n      inputs: Array[Int],\n      fastInterleaveBitsEnabled: Boolean): Array[Byte] = {\n    if (fastInterleaveBitsEnabled) {\n      inputs.length match {\n        // The default algorithm has the complexity O(32 * n) (n is the number of input columns)\n        // The new algorithm has O(4 * 8) complexity when the number of Z-Order by columns is\n        // less than 9. It uses the algorithm described here\n        // http://graphics.stanford.edu/~seander/bithacks.html#InterleaveTableObvious\n        case 0 => Array.empty\n        case 1 => intToByte(inputs(0))\n        case 2 => interleave2Ints(inputs(1), inputs(0))\n        case 3 => interleave3Ints(inputs(2), inputs(1), inputs(0))\n        case 4 => interleave4Ints(inputs(3), inputs(2), inputs(1), inputs(0))\n        case 5 => interleave5Ints(inputs(4), inputs(3), inputs(2), inputs(1), inputs(0))\n        case 6 => interleave6Ints(inputs(5), inputs(4), inputs(3), inputs(2), inputs(1), inputs(0))\n        case 7 => interleave7Ints(inputs(6), inputs(5), inputs(4), inputs(3), inputs(2), inputs(1),\n          inputs(0))\n        case 8 => interleave8Ints(inputs(7), inputs(6), inputs(5), inputs(4), inputs(3), inputs(2),\n          inputs(1), inputs(0))\n        case _ => defaultInterleaveBits(inputs, inputs.length)\n      }\n    } else {\n      defaultInterleaveBits(inputs, inputs.length)\n    }\n  }\n\n  private def defaultInterleaveBits(inputs: Array[Int], numCols: Int): Array[Byte] = {\n    val ret = new Array[Byte](numCols * 4)\n    var ret_idx: Int = 0\n    var ret_bit: Int = 7\n    var ret_byte: Byte = 0\n\n    var bit = 31 /* going from most to least significant bit */\n    while (bit >= 0) {\n      var idx = 0\n      while (idx < numCols) {\n        ret_byte = (ret_byte | (((inputs(idx) >> bit) & 1) << ret_bit)).toByte\n        ret_bit -= 1\n        if (ret_bit == -1) {\n          // finished processing a byte\n          ret.update(ret_idx, ret_byte)\n          ret_byte = 0\n          ret_idx += 1\n          ret_bit = 7\n        }\n        idx += 1\n      }\n      bit -= 1\n    }\n    assert(ret_idx == numCols * 4)\n    assert(ret_bit == 7)\n    ret\n  }\n\n  private def interleave2Ints(i1: Int, i2: Int): Array[Byte] = {\n    val result = new Array[Byte](8)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0\n      var j = 0\n      while (j < 8) {\n        val x_masked = tmp1 & (1 << j)\n        val y_masked = tmp2 & (1 << j)\n        z |= (x_masked << j)\n        z |= (y_masked << (j + 1))\n        j = j + 1\n      }\n      result((3 - i) * 2 + 1) = (z & 0xFF).toByte\n      result((3 - i) * 2) = ((z >> 8) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n\n  private def intToByte(input: Int): Array[Byte] = {\n    val result = new Array[Byte](4)\n    var i = 0\n    while (i <= 3) {\n      val offset = i * 8\n      result(3 - i) = ((input >> offset) & 0xFF).toByte\n      i += 1\n    }\n    result\n  }\n\n  private def interleave3Ints(i1: Int, i2: Int, i3: Int): Array[Byte] = {\n    val result = new Array[Byte](12)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n      val tmp3 = ((i3 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0\n      var j = 0\n      while (j < 8) {\n        val r1_mask = tmp1 & (1 << j)\n        val r2_mask = tmp2 & (1 << j)\n        val r3_mask = tmp3 & (1 << j)\n        z |= (r1_mask << (2 * j)) | (r2_mask << (2 * j + 1)) | (r3_mask << (2 * j + 2))\n        j = j + 1\n      }\n      result((3 - i) * 3 + 2) = (z & 0xFF).toByte\n      result((3 - i) * 3 + 1) = ((z >> 8) & 0xFF).toByte\n      result((3 - i) * 3) = ((z >> 16) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n\n  private def interleave4Ints(i1: Int, i2: Int, i3: Int, i4: Int): Array[Byte] = {\n    val result = new Array[Byte](16)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n      val tmp3 = ((i3 >> (i * 8)) & 0xFF).toByte\n      val tmp4 = ((i4 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0\n      var j = 0\n      while (j < 8) {\n        val r1_mask = tmp1 & (1 << j)\n        val r2_mask = tmp2 & (1 << j)\n        val r3_mask = tmp3 & (1 << j)\n        val r4_mask = tmp4 & (1 << j)\n        z |= (r1_mask << (3 * j)) | (r2_mask << (3 * j + 1)) | (r3_mask << (3 * j + 2)) |\n          (r4_mask << (3 * j + 3))\n        j = j + 1\n      }\n      result((3 - i) * 4 + 3) = (z & 0xFF).toByte\n      result((3 - i) * 4 + 2) = ((z >> 8) & 0xFF).toByte\n      result((3 - i) * 4 + 1) = ((z >> 16) & 0xFF).toByte\n      result((3 - i) * 4) = ((z >> 24) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n\n  private def interleave5Ints(\n      i1: Int,\n      i2: Int,\n      i3: Int,\n      i4: Int,\n      i5: Int): Array[Byte] = {\n    val result = new Array[Byte](20)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n      val tmp3 = ((i3 >> (i * 8)) & 0xFF).toByte\n      val tmp4 = ((i4 >> (i * 8)) & 0xFF).toByte\n      val tmp5 = ((i5 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0L\n      var j = 0\n      while (j < 8) {\n        val r1_mask = tmp1 & (1 << j).toLong\n        val r2_mask = tmp2 & (1 << j).toLong\n        val r3_mask = tmp3 & (1 << j).toLong\n        val r4_mask = tmp4 & (1 << j).toLong\n        val r5_mask = tmp5 & (1 << j).toLong\n        z |= (r1_mask << (4 * j)) | (r2_mask << (4 * j + 1)) | (r3_mask << (4 * j + 2)) |\n          (r4_mask << (4 * j + 3)) | (r5_mask << (4 * j + 4))\n        j = j + 1\n      }\n      result((3 - i) * 5 + 4) = (z & 0xFF).toByte\n      result((3 - i) * 5 + 3) = ((z >> 8) & 0xFF).toByte\n      result((3 - i) * 5 + 2) = ((z >> 16) & 0xFF).toByte\n      result((3 - i) * 5 + 1) = ((z >> 24) & 0xFF).toByte\n      result((3 - i) * 5) = ((z >> 32) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n\n  private def interleave6Ints(\n      i1: Int,\n      i2: Int,\n      i3: Int,\n      i4: Int,\n      i5: Int,\n      i6: Int): Array[Byte] = {\n    val result = new Array[Byte](24)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n      val tmp3 = ((i3 >> (i * 8)) & 0xFF).toByte\n      val tmp4 = ((i4 >> (i * 8)) & 0xFF).toByte\n      val tmp5 = ((i5 >> (i * 8)) & 0xFF).toByte\n      val tmp6 = ((i6 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0L\n      var j = 0\n      while (j < 8) {\n        val r1_mask = tmp1 & (1 << j).toLong\n        val r2_mask = tmp2 & (1 << j).toLong\n        val r3_mask = tmp3 & (1 << j).toLong\n        val r4_mask = tmp4 & (1 << j).toLong\n        val r5_mask = tmp5 & (1 << j).toLong\n        val r6_mask = tmp6 & (1 << j).toLong\n        z |= (r1_mask << (5 * j)) | (r2_mask << (5 * j + 1)) | (r3_mask << (5 * j + 2)) |\n          (r4_mask << (5 * j + 3)) | (r5_mask << (5 * j + 4)) | (r6_mask << (5 * j + 5))\n        j = j + 1\n      }\n      result((3 - i) * 6 + 5) = (z & 0xFF).toByte\n      result((3 - i) * 6 + 4) = ((z >> 8) & 0xFF).toByte\n      result((3 - i) * 6 + 3) = ((z >> 16) & 0xFF).toByte\n      result((3 - i) * 6 + 2) = ((z >> 24) & 0xFF).toByte\n      result((3 - i) * 6 + 1) = ((z >> 32) & 0xFF).toByte\n      result((3 - i) * 6) = ((z >> 40) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n\n  private def interleave7Ints(\n      i1: Int,\n      i2: Int,\n      i3: Int,\n      i4: Int,\n      i5: Int,\n      i6: Int,\n      i7: Int): Array[Byte] = {\n    val result = new Array[Byte](28)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n      val tmp3 = ((i3 >> (i * 8)) & 0xFF).toByte\n      val tmp4 = ((i4 >> (i * 8)) & 0xFF).toByte\n      val tmp5 = ((i5 >> (i * 8)) & 0xFF).toByte\n      val tmp6 = ((i6 >> (i * 8)) & 0xFF).toByte\n      val tmp7 = ((i7 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0L\n      var j = 0\n      while (j < 8) {\n        val r1_mask = tmp1 & (1 << j).toLong\n        val r2_mask = tmp2 & (1 << j).toLong\n        val r3_mask = tmp3 & (1 << j).toLong\n        val r4_mask = tmp4 & (1 << j).toLong\n        val r5_mask = tmp5 & (1 << j).toLong\n        val r6_mask = tmp6 & (1 << j).toLong\n        val r7_mask = tmp7 & (1 << j).toLong\n        z |= (r1_mask << (6 * j)) | (r2_mask << (6 * j + 1)) | (r3_mask << (6 * j + 2)) |\n          (r4_mask << (6 * j + 3)) | (r5_mask << (6 * j + 4)) | (r6_mask << (6 * j + 5)) |\n          (r7_mask << (6 * j + 6))\n        j = j + 1\n      }\n      result((3 - i) * 7 + 6) = (z & 0xFF).toByte\n      result((3 - i) * 7 + 5) = ((z >> 8) & 0xFF).toByte\n      result((3 - i) * 7 + 4) = ((z >> 16) & 0xFF).toByte\n      result((3 - i) * 7 + 3) = ((z >> 24) & 0xFF).toByte\n      result((3 - i) * 7 + 2) = ((z >> 32) & 0xFF).toByte\n      result((3 - i) * 7 + 1) = ((z >> 40) & 0xFF).toByte\n      result((3 - i) * 7) = ((z >> 48) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n\n  private def interleave8Ints(\n      i1: Int,\n      i2: Int,\n      i3: Int,\n      i4: Int,\n      i5: Int,\n      i6: Int,\n      i7: Int,\n      i8: Int): Array[Byte] = {\n    val result = new Array[Byte](32)\n    var i = 0\n    while (i < 4) {\n      val tmp1 = ((i1 >> (i * 8)) & 0xFF).toByte\n      val tmp2 = ((i2 >> (i * 8)) & 0xFF).toByte\n      val tmp3 = ((i3 >> (i * 8)) & 0xFF).toByte\n      val tmp4 = ((i4 >> (i * 8)) & 0xFF).toByte\n      val tmp5 = ((i5 >> (i * 8)) & 0xFF).toByte\n      val tmp6 = ((i6 >> (i * 8)) & 0xFF).toByte\n      val tmp7 = ((i7 >> (i * 8)) & 0xFF).toByte\n      val tmp8 = ((i8 >> (i * 8)) & 0xFF).toByte\n\n      var z = 0L\n      var j = 0\n      while (j < 8) {\n        val r1_mask = tmp1 & (1 << j).toLong\n        val r2_mask = tmp2 & (1 << j).toLong\n        val r3_mask = tmp3 & (1 << j).toLong\n        val r4_mask = tmp4 & (1 << j).toLong\n        val r5_mask = tmp5 & (1 << j).toLong\n        val r6_mask = tmp6 & (1 << j).toLong\n        val r7_mask = tmp7 & (1 << j).toLong\n        val r8_mask = tmp8 & (1 << j).toLong\n        z |= (r1_mask << (7 * j)) | (r2_mask << (7 * j + 1)) | (r3_mask << (7 * j + 2)) |\n          (r4_mask << (7 * j + 3)) | (r5_mask << (7 * j + 4)) | (r6_mask << (7 * j + 5)) |\n          (r7_mask << (7 * j + 6)) | (r8_mask << (7 * j + 7))\n        j = j + 1\n      }\n      result((3 - i) * 8 + 7) = (z & 0xFF).toByte\n      result((3 - i) * 8 + 6) = ((z >> 8) & 0xFF).toByte\n      result((3 - i) * 8 + 5) = ((z >> 16) & 0xFF).toByte\n      result((3 - i) * 8 + 4) = ((z >> 24) & 0xFF).toByte\n      result((3 - i) * 8 + 3) = ((z >> 32) & 0xFF).toByte\n      result((3 - i) * 8 + 2) = ((z >> 40) & 0xFF).toByte\n      result((3 - i) * 8 + 1) = ((z >> 48) & 0xFF).toByte\n      result((3 - i) * 8) = ((z >> 56) & 0xFF).toByte\n      i = i + 1\n    }\n    result\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/JoinedProjection.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeMap, BoundReference, Expression, GetStructField}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Helper class for generating a joined projection.\n *\n *\n * This class is used to instantiate a \"Joined Row\" - a wrapper that makes two rows appear to be a\n * single concatenated row, by using nested access. It is primarily used during statistics\n * collection to update a buffer of per-column aggregates (i.e. the left-hand side row) with stats\n * from the latest row processed (i.e. the right-hand side row).\n *\n * Implementation Note: If we instead stored `leftRow` and `rightRow` we would have to perform size\n * checks on `leftRow` during every access, which is slow.\n */\nobject JoinedProjection {\n  /**\n   * Bind attributes for a joined projection. This resulting project list expects an input row\n   * that has two nested struct fields, the struct at position 0 must be the left hand row of the\n   * join, and the struct at position 1 must be the right hand row of the join.\n   *\n   * The following shows example shows how this can be used for updating an aggregation buffer:\n   * {{{\n   *   val buffer = new GenericInternalRow()\n   *\n   *  val update = GenerateMutableProjection.generate(\n   *     expressions = JoinedProjection(\n   *       leftAttributes = bufferAttrs,\n   *       rightAttributes = dataCols,\n   *       projectList = aggregates.flatMap(_.updateExpressions)),\n   *     inputSchema = Nil,\n   *     useSubexprElimination = true\n   *   ).target(buffer)\n   *\n   *   val joinedRow = new GenericInternalRow(2)\n   *   joinedRow.update(0, input)\n   *\n   *   def updateBuffer(input: InternalRow): Unit = {\n   *     joinedRow.update(1, input)\n   *     update(joinedRow)\n   *   }\n   * }}}\n   */\n  def bind(\n      leftAttributes: Seq[Attribute],\n      rightAttributes: Seq[Attribute],\n      projectList: Seq[Expression],\n      leftCanBeNull: Boolean = false,\n      rightCanBeNull: Boolean = false): Seq[Expression] = {\n    val mapping = AttributeMap(\n      createMapping(0, leftCanBeNull, leftAttributes)\n        ++ createMapping(1, rightCanBeNull, rightAttributes))\n    projectList.map { expr =>\n      expr.transformUp {\n        case a: Attribute => mapping(a)\n      }\n    }\n  }\n\n  /**\n   * Helper method to create a nested struct field with efficient value extraction.\n   */\n  private def createMapping(\n      index: Int,\n      nullable: Boolean,\n      attributes: Seq[Attribute]): Seq[(Attribute, Expression)] = {\n    val ref = BoundReference(\n      index,\n      DataTypeUtils.fromAttributes(attributes),\n      nullable)\n    attributes.zipWithIndex.map {\n      case (a, ordinal) => a -> GetStructField(ref, ordinal, Option(a.name))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/expressions/RangePartitionId.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport org.apache.spark.Partitioner\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis.TypeCheckResult\nimport org.apache.spark.sql.catalyst.expressions.{Expression, GenericInternalRow, RowOrdering, UnaryExpression, Unevaluable}\nimport org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode}\nimport org.apache.spark.sql.types._\n\n\n/**\n * Unevaluable placeholder expression to be rewritten by the optimizer into [[PartitionerExpr]]\n *\n * This is just a convenient way to introduce the former, without the need to manually construct the\n * [[RangePartitioner]] beforehand, which requires an RDD to be sampled in order to determine range\n * partition boundaries. The optimizer rule will take care of all that.\n *\n * @see [[org.apache.spark.sql.delta.optimizer.RangeRepartitionIdRewrite]]\n */\ncase class RangePartitionId(child: Expression, numPartitions: Int)\n  extends UnaryExpression with Unevaluable {\n\n  require(numPartitions > 0, \"expected the number partitions to be greater than zero\")\n\n  override def checkInputDataTypes(): TypeCheckResult = {\n    if (RowOrdering.isOrderable(child.dataType)) {\n      TypeCheckResult.TypeCheckSuccess\n    } else {\n      TypeCheckResult.TypeCheckFailure(s\"cannot sort data type ${child.dataType.simpleString}\")\n    }\n  }\n\n  override def dataType: DataType = IntegerType\n\n  override def nullable: Boolean = false\n\n  override protected def withNewChildInternal(newChild: Expression): RangePartitionId =\n    copy(child = newChild)\n}\n\n/**\n * Thin wrapper around [[Partitioner]] instances that are used in Shuffle operations.\n * TODO: If needed elsewhere, consider moving it into its own file.\n */\ncase class PartitionerExpr(child: Expression, partitioner: Partitioner)\n  extends UnaryExpression {\n\n  override def dataType: DataType = IntegerType\n\n  override def nullable: Boolean = false\n\n  private lazy val row = new GenericInternalRow(Array[Any](null))\n\n  override def eval(input: InternalRow): Any = {\n    val value: Any = child.eval(input)\n    row.update(0, value)\n    partitioner.getPartition(row)\n  }\n\n  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {\n    val partitionerReference = ctx.addReferenceObj(\"partitioner\", partitioner)\n    val rowReference = ctx.addReferenceObj(\"row\", row)\n\n    nullSafeCodeGen(ctx, ev, input =>\n      s\"\"\"$rowReference.update(0, $input);\n         |${ev.value} = $partitionerReference.getPartition($rowReference);\n       \"\"\".stripMargin)\n  }\n\n  override protected def withNewChildInternal(newChild: Expression): PartitionerExpr =\n    copy(child = newChild)\n}\n\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/CdcAddFileIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport java.text.SimpleDateFormat\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader._\nimport org.apache.spark.sql.delta.implicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A [[TahoeFileIndex]] for scanning a sequence of added files as CDC. Similar to\n * [[TahoeBatchFileIndex]], with a bit of special handling to attach the log version\n * and CDC type on a per-file basis.\n * @param spark The Spark session.\n * @param filesByVersion Grouped FileActions, one per table version.\n * @param deltaLog The delta log instance.\n * @param path The table's data path.\n * @param snapshot The snapshot where we read CDC from.\n * @param rowIndexFilters Map from <b>URI-encoded</b> file path to a row index filter type.\n *\n * Note: Please also consider other CDC-related file indexes like [[TahoeChangeFileIndex]]\n * and [[TahoeRemoveFileIndex]] when modifying this file index.\n */\nclass CdcAddFileIndex(\n    spark: SparkSession,\n    filesByVersion: Seq[CDCDataSpec[AddFile]],\n    deltaLog: DeltaLog,\n    path: Path,\n    snapshot: SnapshotDescriptor,\n    override val rowIndexFilters: Option[Map[String, RowIndexFilterType]] = None\n  ) extends TahoeBatchFileIndex(\n    spark, \"cdcRead\", filesByVersion.flatMap(_.actions), deltaLog, path, snapshot) {\n\n  override def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile] = {\n    val addFiles = filesByVersion.flatMap {\n      case CDCDataSpec(version, ts, files, ci) =>\n        files.map { f =>\n          // We add the metadata as faked partition columns in order to attach it on a per-file\n          // basis.\n          val tsOpt = Option(ts)\n            .map(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS Z\").format(_)).orNull\n          val newPartitionVals = f.partitionValues +\n            (CDC_COMMIT_VERSION -> version.toString) +\n            (CDC_COMMIT_TIMESTAMP -> tsOpt) +\n            (CDC_TYPE_COLUMN_NAME -> CDC_TYPE_INSERT)\n          f.copy(partitionValues = newPartitionVals)\n        }\n    }\n    DeltaLog.filterFileList(partitionSchema, addFiles.toDF(spark), partitionFilters)\n      .as[AddFile]\n      .collect()\n  }\n\n  override def inputFiles: Array[String] = {\n    filesByVersion.flatMap(_.actions).map(f => absolutePath(f.path).toString).toArray\n  }\n\n  override val partitionSchema: StructType =\n    CDCReader.cdcReadSchema(snapshot.metadata.partitionSchema)\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/DelayedCommitProtocol.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.net.URI\nimport java.util.UUID\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, FileAction}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader.{CDC_LOCATION, CDC_PARTITION_COL}\nimport org.apache.spark.sql.delta.util.{DateFormatter, PartitionUtils, TimestampFormatter, Utils => DeltaUtils}\nimport org.apache.hadoop.fs.{FileStatus, Path}\nimport org.apache.hadoop.mapreduce.{JobContext, TaskAttemptContext}\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.internal.io.FileCommitProtocol\nimport org.apache.spark.internal.io.FileCommitProtocol.TaskCommitMessage\nimport org.apache.spark.sql.catalyst.expressions.Cast\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.delta.files.DeltaFileFormatWriter.PartitionedTaskAttemptContextImpl\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{DataType, StringType, TimestampType}\nimport org.apache.spark.util.Utils\n\n/**\n * Writes out the files to `path` and returns a list of them in `addedStatuses`. Includes\n * special handling for partitioning on [[CDC_PARTITION_COL]] for\n * compatibility between enabled and disabled CDC; partitions with a value of false in this\n * column produce no corresponding partitioning directory.\n * @param path The base path files will be written\n * @param randomPrefixLength The length of random subdir name under 'path' that files been written\n * @param subdir The immediate subdir under path; If randomPrefixLength and subdir both exist, file\n *               path will be path/subdir/[rand str of randomPrefixLength]/file\n */\nclass DelayedCommitProtocol(\n      jobId: String,\n      path: String,\n      randomPrefixLength: Option[Int],\n      subdir: Option[String])\n  extends FileCommitProtocol with Serializable with Logging {\n  // Track the list of files added by a task, only used on the executors.\n  @transient protected var addedFiles: ArrayBuffer[(Map[String, String], String)] = _\n\n  // Track the change files added, only used on the driver. Files are sorted between this buffer\n  // and addedStatuses based on the value of the [[CDC_TYPE_COLUMN_NAME]] partition column - a\n  // file goes to addedStatuses if the value is CDC_TYPE_NOT_CDC and changeFiles otherwise.\n  @transient val changeFiles = new ArrayBuffer[AddCDCFile]\n\n  // Track the overall files added, only used on the driver.\n  //\n  // In rare cases, some of these AddFiles can be empty (i.e. contain no logical records).\n  // If the caller wishes to have only non-empty AddFiles, they must collect stats and perform\n  // the filter themselves. See TransactionalWrite::writeFiles. This filter will be best-effort,\n  // since there's no guarantee the stats will exist.\n  @transient val addedStatuses = new ArrayBuffer[AddFile]\n\n  // Constants for CDC partition manipulation. Used only in newTaskTempFile(), but we define them\n  // here to avoid building a new redundant regex for every file.\n  protected val cdcPartitionFalse = s\"${CDC_PARTITION_COL}=false\"\n  protected val cdcPartitionTrue = s\"${CDC_PARTITION_COL}=true\"\n  protected val cdcPartitionTrueRegex = cdcPartitionTrue.r\n\n  override def setupJob(jobContext: JobContext): Unit = {\n\n  }\n\n  /**\n   * Commits a job after the writes succeed. Must be called on the driver. Partitions the written\n   * files into [[AddFile]]s and [[AddCDCFile]]s as these metadata actions are treated differently\n   * by [[TransactionalWrite]] (i.e. AddFile's may have additional statistics injected)\n   */\n  override def commitJob(jobContext: JobContext, taskCommits: Seq[TaskCommitMessage]): Unit = {\n    val (addFiles, changeFiles) = taskCommits.flatMap(_.obj.asInstanceOf[Seq[_]])\n      .partition {\n        case _: AddFile => true\n        case _: AddCDCFile => false\n        case other =>\n          throw DeltaErrors.unrecognizedFileAction(s\"$other\", s\"${other.getClass}\")\n      }\n\n    // we cannot add type information above because of type erasure\n    addedStatuses ++= addFiles.map(_.asInstanceOf[AddFile])\n    this.changeFiles ++= changeFiles.map(_.asInstanceOf[AddCDCFile]).toArray[AddCDCFile]\n  }\n\n  override def abortJob(jobContext: JobContext): Unit = {\n    // TODO: Best effort cleanup\n  }\n\n  override def setupTask(taskContext: TaskAttemptContext): Unit = {\n    addedFiles = new ArrayBuffer[(Map[String, String], String)]\n  }\n\n  /** Prefix added in testing mode to all filenames to test special chars that need URL-encoding. */\n  val FILE_NAME_PREFIX = SQLConf.get.getConf(DeltaSQLConf.TEST_FILE_NAME_PREFIX)\n\n  protected def getFileName(\n      taskContext: TaskAttemptContext,\n      ext: String,\n      partitionValues: Map[String, String]): String = {\n    // The file name looks like part-r-00000-2dd664f9-d2c4-4ffe-878f-c6c70c1fb0cb_00003.gz.parquet\n    // Note that %05d does not truncate the split number, so if we have more than 100000 tasks,\n    // the file name is fine and won't overflow.\n    val split = taskContext.getTaskAttemptID.getTaskID.getId\n    val uuid = UUID.randomUUID.toString\n    // CDC files (CDC_PARTITION_COL = true) are named with \"cdc-...\" instead of \"part-...\".\n    val typePrefix =\n      if (partitionValues.get(CDC_PARTITION_COL).contains(\"true\")) \"cdc-\" else \"part-\"\n    f\"${FILE_NAME_PREFIX}${typePrefix}${split}%05d-${uuid}${ext}\"\n  }\n\n  protected def parsePartitions(\n      dir: String,\n      taskContext: TaskAttemptContext): Map[String, String] = {\n    // TODO: enable validatePartitionColumns?\n    val useUtcNormalizedTimestamps = taskContext match {\n      case _: PartitionedTaskAttemptContextImpl => taskContext.getConfiguration.getBoolean(\n        DeltaSQLConf.UTC_TIMESTAMP_PARTITION_VALUES.key, true)\n      case _ => false\n    }\n\n    val partitionColumnToDataType: Map[String, DataType] =\n      taskContext.asInstanceOf[PartitionedTaskAttemptContextImpl]\n        .partitionColToDataType\n        .filter(partitionCol => partitionCol._2 == TimestampType)\n\n    val dateFormatter = DateFormatter()\n    // if adjusting to UTC make sure to interpret timezones using Spark\n    // config, otherwise fallback to JVM timezone\n    val timezone = {\n      if (useUtcNormalizedTimestamps) {\n        DateTimeUtils.getTimeZone(SQLConf.get.sessionLocalTimeZone)\n      } else {\n        java.util.TimeZone.getDefault\n      }\n    }\n\n    val timestampFormatter = TimestampFormatter(PartitionUtils.timestampPartitionPattern, timezone)\n\n    /**\n     * ToDo: Remove the use of this PartitionUtils API with type inference logic\n     * since the types are already known from the Delta metadata!\n     *\n     * Currently types are passed to the PartitionUtils.parsePartition API to facilitate\n     * timestamp conversion to UTC. In all other cases, the type is just inferred as a String.\n     * Note: the passed in timestampFormatter and timezone detail\n     * is used for parsing from the string timestamp.\n     * If utc normalization is enabled the parsed partition value will be adjusted to UTC\n     * and output in iso8601 format.\n     */\n    val parsedPartition =\n      PartitionUtils\n        .parsePartition(\n          new Path(dir),\n          typeInference = false,\n          Set.empty,\n          userSpecifiedDataTypes = partitionColumnToDataType,\n          validatePartitionColumns = false,\n          java.util.TimeZone.getDefault,\n          dateFormatter,\n          timestampFormatter,\n          useUtcNormalizedTimestamps)\n        ._1\n        .get\n    parsedPartition\n        .columnNames\n        .zip(\n          parsedPartition\n            .literals\n            .map(PartitionUtils.literalToNormalizedString(\n              _,\n              Some(timezone.getID),\n              useUtcNormalizedTimestamps)))\n        .toMap\n  }\n\n  /**\n   * Notifies the commit protocol to add a new file, and gets back the full path that should be\n   * used.\n   *\n   * Includes special logic for CDC files and paths. Specifically, if the directory `dir` contains\n   * the CDC partition `__is_cdc=true` then\n   * - the file name begins with `cdc-` instead of `part-`\n   * - the directory has the `__is_cdc=true` partition removed and is placed in the `_changed_data`\n   *   folder\n   */\n  override def newTaskTempFile(\n      taskContext: TaskAttemptContext, dir: Option[String], ext: String): String = {\n    val partitionValues = dir.map(dir => parsePartitions(dir, taskContext))\n      .getOrElse(Map.empty[String, String])\n    val filename = getFileName(taskContext, ext, partitionValues)\n    val relativePath = randomPrefixLength.map { prefixLength =>\n      DeltaUtils.getRandomPrefix(prefixLength) // Generate a random prefix as a first choice\n    }.orElse {\n      dir // or else write into the partition directory if it is partitioned\n    }.map { subDir =>\n      // Do some surgery on the paths we write out to eliminate the CDC_PARTITION_COL. Non-CDC\n      // data is written to the base location, while CDC data is written to a special folder\n      // _change_data.\n      // The code here gets a bit complicated to accommodate two corner cases: an empty subdir\n      // can't be passed to new Path() at all, and a single-level subdir won't have a trailing\n      // slash.\n      if (subDir == cdcPartitionFalse) {\n        new Path(filename)\n      } else if (subDir.startsWith(cdcPartitionTrue)) {\n        val cleanedSubDir = cdcPartitionTrueRegex.replaceFirstIn(subDir, CDC_LOCATION)\n        new Path(cleanedSubDir, filename)\n      } else if (subDir.startsWith(cdcPartitionFalse)) {\n        // We need to remove the trailing slash in addition to the directory - otherwise\n        // it'll be interpreted as an absolute path and fail.\n        val cleanedSubDir = subDir.stripPrefix(cdcPartitionFalse + \"/\")\n        new Path(cleanedSubDir, filename)\n      } else {\n        new Path(subDir, filename)\n      }\n    }.getOrElse(new Path(filename)) // or directly write out to the output path\n\n    val relativePathWithSubdir = subdir.map(new Path(_, relativePath)).getOrElse(relativePath)\n    addedFiles.append((partitionValues, relativePathWithSubdir.toUri.toString))\n    new Path(path, relativePathWithSubdir).toString\n  }\n\n  override def newTaskTempFileAbsPath(\n      taskContext: TaskAttemptContext, absoluteDir: String, ext: String): String = {\n    throw DeltaErrors.unsupportedAbsPathAddFile(s\"$this\")\n  }\n\n  protected def buildActionFromAddedFile(\n      f: (Map[String, String], String),\n      stat: FileStatus,\n      taskContext: TaskAttemptContext): FileAction = {\n    // The partitioning in the Delta log action will be read back as part of the data, so our\n    // virtual CDC_PARTITION_COL needs to be stripped out.\n    val partitioning = f._1.filter { case (k, v) => k != CDC_PARTITION_COL }\n    f._1.get(CDC_PARTITION_COL) match {\n      case Some(\"true\") =>\n        val partitioning = f._1.filter { case (k, v) => k != CDC_PARTITION_COL }\n        AddCDCFile(f._2, partitioning, stat.getLen)\n      case _ =>\n        val addFile = AddFile(f._2, partitioning, stat.getLen, stat.getModificationTime, true)\n        addFile\n    }\n  }\n\n  override def commitTask(taskContext: TaskAttemptContext): TaskCommitMessage = {\n    if (addedFiles.nonEmpty) {\n      val fs = new Path(path, addedFiles.head._2).getFileSystem(taskContext.getConfiguration)\n      val statuses: Seq[FileAction] = addedFiles.map { f =>\n        // scalastyle:off pathfromuri\n        val filePath = new Path(path, new Path(new URI(f._2)))\n        // scalastyle:on pathfromuri\n        val stat = fs.getFileStatus(filePath)\n\n        buildActionFromAddedFile(f, stat, taskContext)\n      }.toSeq\n\n      new TaskCommitMessage(statuses)\n    } else {\n      new TaskCommitMessage(Nil)\n    }\n  }\n\n  override def abortTask(taskContext: TaskAttemptContext): Unit = {\n    // TODO: we can also try delete the addedFiles as a best-effort cleanup.\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/DeltaFileFormatWriter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport java.util.{Date, UUID}\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaOptions\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileAlreadyExistsException, Path}\nimport org.apache.hadoop.mapreduce._\nimport org.apache.hadoop.mapreduce.lib.output.FileOutputFormat\nimport org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl\n\nimport org.apache.spark._\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.internal.io.{FileCommitProtocol, SparkHadoopWriterUtils}\nimport org.apache.spark.shuffle.FetchFailedException\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.catalog.BucketSpec\nimport org.apache.spark.sql.catalyst.catalog.CatalogTypes.TablePartitionSpec\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.BindReferences.bindReferences\nimport org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, DateTimeUtils}\nimport org.apache.spark.sql.connector.write.WriterCommitMessage\nimport org.apache.spark.sql.errors.QueryExecutionErrors\nimport org.apache.spark.sql.execution.{ProjectExec, SortExec, SparkPlan, SQLExecution, UnsafeExternalRowSorter}\nimport org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanExec\nimport org.apache.spark.sql.execution.datasources._\nimport org.apache.spark.sql.execution.datasources.FileFormatWriter._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.DataType\nimport org.apache.spark.util.{SerializableConfiguration, Utils}\n\n/**\n *  A helper object for writing FileFormat data out to a location.\n *  Logic is copied from FileFormatWriter from Spark 3.5 with added functionality to write partition\n *  values to data files. Specifically L123-126, L132, and L140 where it adds option\n *  WRITE_PARTITION_COLUMNS\n */\nobject DeltaFileFormatWriter extends Logging {\n\n  /**\n   * A variable used in tests to check whether the output ordering of the query matches the\n   * required ordering of the write command.\n   */\n  private var outputOrderingMatched: Boolean = false\n\n  /**\n   * A variable used in tests to check the final executed plan.\n   */\n  private var executedPlan: Option[SparkPlan] = None\n\n  // scalastyle:off argcount\n  /**\n   * Basic work flow of this command is:\n   * 1. Driver side setup, including output committer initialization and data source specific\n   *    preparation work for the write job to be issued.\n   * 2. Issues a write job consists of one or more executor side tasks, each of which writes all\n   *    rows within an RDD partition.\n   * 3. If no exception is thrown in a task, commits that task, otherwise aborts that task;  If any\n   *    exception is thrown during task commitment, also aborts that task.\n   * 4. If all tasks are committed, commit the job, otherwise aborts the job;  If any exception is\n   *    thrown during job commitment, also aborts the job.\n   * 5. If the job is successfully committed, perform post-commit operations such as\n   *    processing statistics.\n   * @return The set of all partition paths that were updated during this write job.\n   */\n  def write(\n      sparkSession: SparkSession,\n      plan: SparkPlan,\n      fileFormat: FileFormat,\n      committer: FileCommitProtocol,\n      outputSpec: OutputSpec,\n      hadoopConf: Configuration,\n      partitionColumns: Seq[Attribute],\n      bucketSpec: Option[BucketSpec],\n      statsTrackers: Seq[WriteJobStatsTracker],\n      options: Map[String, String],\n      numStaticPartitionCols: Int = 0): Set[String] = {\n    require(partitionColumns.size >= numStaticPartitionCols)\n\n    val job = Job.getInstance(hadoopConf)\n    job.setOutputKeyClass(classOf[Void])\n    job.setOutputValueClass(classOf[InternalRow])\n    FileOutputFormat.setOutputPath(job, new Path(outputSpec.outputPath))\n\n    val partitionSet = AttributeSet(partitionColumns)\n    // cleanup the internal metadata information of\n    // the file source metadata attribute if any before write out\n    val finalOutputSpec = outputSpec.copy(\n      outputColumns = outputSpec.outputColumns\n        .map(FileSourceMetadataAttribute.cleanupFileSourceMetadataInformation)\n    )\n    val dataColumns = finalOutputSpec.outputColumns.filterNot(partitionSet.contains)\n\n    val writerBucketSpec = V1WritesUtils.getWriterBucketSpec(bucketSpec, dataColumns, options)\n    val sortColumns = V1WritesUtils.getBucketSortColumns(bucketSpec, dataColumns)\n\n    val caseInsensitiveOptions = CaseInsensitiveMap(options)\n\n    val dataSchema = dataColumns.toStructType\n    DataSourceUtils.verifySchema(fileFormat, dataSchema)\n    DataSourceUtils.checkFieldNames(fileFormat, dataSchema)\n    // Note: prepareWrite has side effect. It sets \"job\".\n\n    val outputDataColumns =\n      if (caseInsensitiveOptions.get(DeltaOptions.WRITE_PARTITION_COLUMNS).contains(\"true\")) {\n        dataColumns ++ partitionColumns\n      } else dataColumns\n\n    val outputWriterFactory =\n      fileFormat.prepareWrite(\n        sparkSession,\n        job,\n        caseInsensitiveOptions,\n        outputDataColumns.toStructType\n      )\n\n    val description = new WriteJobDescription(\n      uuid = UUID.randomUUID.toString,\n      serializableHadoopConf = new SerializableConfiguration(job.getConfiguration),\n      outputWriterFactory = outputWriterFactory,\n      allColumns = finalOutputSpec.outputColumns,\n      dataColumns = outputDataColumns,\n      partitionColumns = partitionColumns,\n      bucketSpec = writerBucketSpec,\n      path = finalOutputSpec.outputPath,\n      customPartitionLocations = finalOutputSpec.customPartitionLocations,\n      maxRecordsPerFile = caseInsensitiveOptions\n        .get(\"maxRecordsPerFile\")\n        .map(_.toLong)\n        .getOrElse(sparkSession.sessionState.conf.maxRecordsPerFile),\n      timeZoneId = caseInsensitiveOptions\n        .get(DateTimeUtils.TIMEZONE_OPTION)\n        .getOrElse(sparkSession.sessionState.conf.sessionLocalTimeZone),\n      statsTrackers = statsTrackers\n    )\n\n    // We should first sort by dynamic partition columns, then bucket id, and finally sorting\n    // columns.\n    val requiredOrdering = partitionColumns.drop(numStaticPartitionCols) ++\n      writerBucketSpec.map(_.bucketIdExpression) ++ sortColumns\n    val writeFilesOpt = V1WritesUtils.getWriteFilesOpt(plan)\n\n    // SPARK-40588: when planned writing is disabled and AQE is enabled,\n    // plan contains an AdaptiveSparkPlanExec, which does not know\n    // its final plan's ordering, so we have to materialize that plan first\n    // it is fine to use plan further down as the final plan is cached in that plan\n    def materializeAdaptiveSparkPlan(plan: SparkPlan): SparkPlan = plan match {\n      case a: AdaptiveSparkPlanExec => a.finalPhysicalPlan\n      case p: SparkPlan => p.withNewChildren(p.children.map(materializeAdaptiveSparkPlan))\n    }\n\n    // the sort order doesn't matter\n    val actualOrdering = writeFilesOpt\n      .map(_.child)\n      .getOrElse(materializeAdaptiveSparkPlan(plan))\n      .outputOrdering\n    val orderingMatched = V1WritesUtils.isOrderingMatched(requiredOrdering, actualOrdering)\n\n    SQLExecution.checkSQLExecutionId(sparkSession)\n\n    // propagate the description UUID into the jobs, so that committers\n    // get an ID guaranteed to be unique.\n    job.getConfiguration.set(\"spark.sql.sources.writeJobUUID\", description.uuid)\n\n    // When `PLANNED_WRITE_ENABLED` is true, the optimizer rule V1Writes will add logical sort\n    // operator based on the required ordering of the V1 write command. So the output\n    // ordering of the physical plan should always match the required ordering. Here\n    // we set the variable to verify this behavior in tests.\n    // There are two cases where FileFormatWriter still needs to add physical sort:\n    // 1) When the planned write config is disabled.\n    // 2) When the concurrent writers are enabled (in this case the required ordering of a\n    //    V1 write command will be empty).\n    if (DeltaUtils.isTesting) outputOrderingMatched = orderingMatched\n\n    if (writeFilesOpt.isDefined) {\n      // build `WriteFilesSpec` for `WriteFiles`\n      val concurrentOutputWriterSpecFunc = (plan: SparkPlan) => {\n        val sortPlan = createSortPlan(plan, requiredOrdering, outputSpec)\n        createConcurrentOutputWriterSpec(sparkSession, sortPlan, sortColumns)\n      }\n      val writeSpec = WriteFilesSpec(\n        description = description,\n        committer = committer,\n        concurrentOutputWriterSpecFunc = concurrentOutputWriterSpecFunc\n      )\n      executeWrite(sparkSession, plan, writeSpec, job)\n    } else {\n      executeWrite(\n        sparkSession,\n        plan,\n        job,\n        description,\n        committer,\n        outputSpec,\n        requiredOrdering,\n        partitionColumns,\n        sortColumns,\n        orderingMatched\n      )\n    }\n  }\n  // scalastyle:on argcount\n\n  private def executeWrite(\n      sparkSession: SparkSession,\n      plan: SparkPlan,\n      job: Job,\n      description: WriteJobDescription,\n      committer: FileCommitProtocol,\n      outputSpec: OutputSpec,\n      requiredOrdering: Seq[Expression],\n      partitionColumns: Seq[Attribute],\n      sortColumns: Seq[Attribute],\n      orderingMatched: Boolean): Set[String] = {\n    val projectList = V1WritesUtils.convertEmptyToNull(plan.output, partitionColumns)\n    val empty2NullPlan = if (projectList.nonEmpty) ProjectExec(projectList, plan) else plan\n\n    writeAndCommit(job, description, committer) {\n      val (planToExecute, concurrentOutputWriterSpec) = if (orderingMatched) {\n        (empty2NullPlan, None)\n      } else {\n        val sortPlan = createSortPlan(empty2NullPlan, requiredOrdering, outputSpec)\n        val concurrentOutputWriterSpec =\n          createConcurrentOutputWriterSpec(sparkSession, sortPlan, sortColumns)\n        if (concurrentOutputWriterSpec.isDefined) {\n          (empty2NullPlan, concurrentOutputWriterSpec)\n        } else {\n          (sortPlan, concurrentOutputWriterSpec)\n        }\n      }\n\n      // In testing, this is the only way to get hold of the actually executed plan written to file\n      if (DeltaUtils.isTesting) executedPlan = Some(planToExecute)\n\n      val rdd = planToExecute.execute()\n\n      // SPARK-23271 If we are attempting to write a zero partition rdd, create a dummy single\n      // partition rdd to make sure we at least set up one write task to write the metadata.\n      val rddWithNonEmptyPartitions = if (rdd.partitions.length == 0) {\n        sparkSession.sparkContext.parallelize(Array.empty[InternalRow], 1)\n      } else {\n        rdd\n      }\n\n      val jobTrackerID = SparkHadoopWriterUtils.createJobTrackerID(new Date())\n      val ret = new Array[WriteTaskResult](rddWithNonEmptyPartitions.partitions.length)\n      val partitionColumnToDataType = description.partitionColumns\n        .map(attr => (attr.name, attr.dataType)).toMap\n      sparkSession.sparkContext.runJob(\n        rddWithNonEmptyPartitions,\n        (taskContext: TaskContext, iter: Iterator[InternalRow]) => {\n          executeTask(\n            description = description,\n            jobTrackerID = jobTrackerID,\n            sparkStageId = taskContext.stageId(),\n            sparkPartitionId = taskContext.partitionId(),\n            sparkAttemptNumber = taskContext.taskAttemptId().toInt & Integer.MAX_VALUE,\n            committer,\n            iterator = iter,\n            concurrentOutputWriterSpec = concurrentOutputWriterSpec,\n            partitionColumnToDataType\n          )\n        },\n        rddWithNonEmptyPartitions.partitions.indices,\n        (index, res: WriteTaskResult) => {\n          committer.onTaskCommit(res.commitMsg)\n          ret(index) = res\n        }\n      )\n      ret\n    }\n  }\n\n  private def writeAndCommit(\n      job: Job,\n      description: WriteJobDescription,\n      committer: FileCommitProtocol)(f: => Array[WriteTaskResult]): Set[String] = {\n    // This call shouldn't be put into the `try` block below because it only initializes and\n    // prepares the job, any exception thrown from here shouldn't cause abortJob() to be called.\n    committer.setupJob(job)\n    try {\n      val ret = f\n      val commitMsgs = ret.map(_.commitMsg)\n\n      logInfo(log\"Start to commit write Job ${MDC(DeltaLogKeys.JOB_ID, description.uuid)}.\")\n      val (_, duration) = Utils.timeTakenMs { committer.commitJob(job, commitMsgs) }\n      logInfo(log\"Write Job ${MDC(DeltaLogKeys.JOB_ID, description.uuid)} committed. \" +\n        log\"Elapsed time: ${MDC(DeltaLogKeys.DURATION, duration)} ms.\")\n\n      processStats(description.statsTrackers, ret.map(_.summary.stats), duration)\n      logInfo(log\"Finished processing stats for write job \" +\n        log\"${MDC(DeltaLogKeys.JOB_ID, description.uuid)}.\")\n\n      // return a set of all the partition paths that were updated during this job\n      ret.map(_.summary.updatedPartitions).reduceOption(_ ++ _).getOrElse(Set.empty)\n    } catch {\n      case cause: Throwable =>\n        logError(log\"Aborting job ${MDC(DeltaLogKeys.JOB_ID, description.uuid)}\", cause)\n        committer.abortJob(job)\n        throw cause\n    }\n  }\n\n  /**\n   * Write files using [[SparkPlan.executeWrite]]\n   */\n  private def executeWrite(\n      session: SparkSession,\n      planForWrites: SparkPlan,\n      writeFilesSpec: WriteFilesSpec,\n      job: Job): Set[String] = {\n    val committer = writeFilesSpec.committer\n    val description = writeFilesSpec.description\n\n    // In testing, this is the only way to get hold of the actually executed plan written to file\n    if (DeltaUtils.isTesting) executedPlan = Some(planForWrites)\n\n    writeAndCommit(job, description, committer) {\n      val rdd = planForWrites.executeWrite(writeFilesSpec)\n      val ret = new Array[WriteTaskResult](rdd.partitions.length)\n      session.sparkContext.runJob(\n        rdd,\n        (context: TaskContext, iter: Iterator[WriterCommitMessage]) => {\n          assert(iter.hasNext)\n          val commitMessage = iter.next()\n          assert(!iter.hasNext)\n          commitMessage\n        },\n        rdd.partitions.indices,\n        (index, res: WriterCommitMessage) => {\n          assert(res.isInstanceOf[WriteTaskResult])\n          val writeTaskResult = res.asInstanceOf[WriteTaskResult]\n          committer.onTaskCommit(writeTaskResult.commitMsg)\n          ret(index) = writeTaskResult\n        }\n      )\n      ret\n    }\n  }\n\n  private def createSortPlan(\n      plan: SparkPlan,\n      requiredOrdering: Seq[Expression],\n      outputSpec: OutputSpec): SortExec = {\n    // SPARK-21165: the `requiredOrdering` is based on the attributes from analyzed plan, and\n    // the physical plan may have different attribute ids due to optimizer removing some\n    // aliases. Here we bind the expression ahead to avoid potential attribute ids mismatch.\n    val orderingExpr =\n      bindReferences(requiredOrdering.map(SortOrder(_, Ascending)), outputSpec.outputColumns)\n    SortExec(orderingExpr, global = false, child = plan)\n  }\n\n  private def createConcurrentOutputWriterSpec(\n      sparkSession: SparkSession,\n      sortPlan: SortExec,\n      sortColumns: Seq[Attribute]): Option[ConcurrentOutputWriterSpec] = {\n    val maxWriters = sparkSession.sessionState.conf.maxConcurrentOutputFileWriters\n    val concurrentWritersEnabled = maxWriters > 0 && sortColumns.isEmpty\n    if (concurrentWritersEnabled) {\n      Some(ConcurrentOutputWriterSpec(maxWriters, () => sortPlan.createSorter()))\n    } else {\n      None\n    }\n  }\n\n   class PartitionedTaskAttemptContextImpl(\n       conf: Configuration,\n       taskId: TaskAttemptID,\n       partitionColumnToDataType: Map[String, DataType])\n     extends TaskAttemptContextImpl(conf, taskId) {\n     val partitionColToDataType: Map[String, DataType] = partitionColumnToDataType\n  }\n\n  /** Writes data out in a single Spark task. */\n  private def executeTask(\n      description: WriteJobDescription,\n      jobTrackerID: String,\n      sparkStageId: Int,\n      sparkPartitionId: Int,\n      sparkAttemptNumber: Int,\n      committer: FileCommitProtocol,\n      iterator: Iterator[InternalRow],\n      concurrentOutputWriterSpec: Option[ConcurrentOutputWriterSpec],\n      partitionColumnToDataType: Map[String, DataType]): WriteTaskResult = {\n\n    val jobId = SparkHadoopWriterUtils.createJobID(jobTrackerID, sparkStageId)\n    val taskId = new TaskID(jobId, TaskType.MAP, sparkPartitionId)\n    val taskAttemptId = new TaskAttemptID(taskId, sparkAttemptNumber)\n\n    // Set up the attempt context required to use in the output committer.\n    val taskAttemptContext: TaskAttemptContext = {\n      // Set up the configuration object\n      val hadoopConf = description.serializableHadoopConf.value\n      hadoopConf.set(\"mapreduce.job.id\", jobId.toString)\n      hadoopConf.set(\"mapreduce.task.id\", taskAttemptId.getTaskID.toString)\n      hadoopConf.set(\"mapreduce.task.attempt.id\", taskAttemptId.toString)\n      hadoopConf.setBoolean(\"mapreduce.task.ismap\", true)\n      hadoopConf.setInt(\"mapreduce.task.partition\", 0)\n\n      if (partitionColumnToDataType.isEmpty) {\n        new TaskAttemptContextImpl(hadoopConf, taskAttemptId)\n      } else {\n        new PartitionedTaskAttemptContextImpl(hadoopConf, taskAttemptId, partitionColumnToDataType)\n      }\n    }\n\n    committer.setupTask(taskAttemptContext)\n\n    val dataWriter =\n      if (sparkPartitionId != 0 && !iterator.hasNext) {\n        // In case of empty job, leave first partition to save meta for file format like parquet.\n        new EmptyDirectoryDataWriter(description, taskAttemptContext, committer)\n      } else if (description.partitionColumns.isEmpty && description.bucketSpec.isEmpty) {\n        new SingleDirectoryDataWriter(description, taskAttemptContext, committer)\n      } else {\n        concurrentOutputWriterSpec match {\n          case Some(spec) =>\n            new DynamicPartitionDataConcurrentWriter(\n              description,\n              taskAttemptContext,\n              committer,\n              spec\n            )\n          case _ =>\n            new DynamicPartitionDataSingleWriter(description, taskAttemptContext, committer)\n        }\n      }\n\n    try {\n      Utils.tryWithSafeFinallyAndFailureCallbacks(block = {\n        // Execute the task to write rows out and commit the task.\n        dataWriter.writeWithIterator(iterator)\n        dataWriter.commit()\n      })(catchBlock = {\n        // If there is an error, abort the task\n        dataWriter.abort()\n        logError(log\"Job ${MDC(DeltaLogKeys.JOB_ID, jobId)} aborted.\")\n      }, finallyBlock = {\n        dataWriter.close()\n      })\n    } catch {\n      case e: FetchFailedException =>\n        throw e\n      case f: FileAlreadyExistsException if SQLConf.get.fastFailFileFormatOutput =>\n        // If any output file to write already exists, it does not make sense to re-run this task.\n        // We throw the exception and let Executor throw ExceptionFailure to abort the job.\n        throw new TaskOutputFileAlreadyExistException(f)\n      case t: Throwable =>\n        throw QueryExecutionErrors.taskFailedWhileWritingRowsError(description.path, t)\n    }\n  }\n\n  /**\n   * For every registered [[WriteJobStatsTracker]], call `processStats()` on it, passing it\n   * the corresponding [[WriteTaskStats]] from all executors.\n   */\n  private def processStats(\n      statsTrackers: Seq[WriteJobStatsTracker],\n      statsPerTask: Seq[Seq[WriteTaskStats]],\n      jobCommitDuration: Long): Unit = {\n\n    val numStatsTrackers = statsTrackers.length\n    assert(\n      statsPerTask.forall(_.length == numStatsTrackers),\n      s\"\"\"Every WriteTask should have produced one `WriteTaskStats` object for every tracker.\n         |There are $numStatsTrackers statsTrackers, but some task returned\n         |${statsPerTask.find(_.length != numStatsTrackers).get.length} results instead.\n       \"\"\".stripMargin\n    )\n\n    val statsPerTracker = if (statsPerTask.nonEmpty) {\n      statsPerTask.transpose\n    } else {\n      statsTrackers.map(_ => Seq.empty)\n    }\n\n    statsTrackers.zip(statsPerTracker).foreach {\n      case (statsTracker, stats) => statsTracker.processStats(stats, jobCommitDuration)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/DeltaSourceSnapshot.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaLog, DeltaTableUtils, Snapshot}\nimport org.apache.spark.sql.delta.actions.SingleAction\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.sources.IndexedFile\nimport org.apache.spark.sql.delta.stats.DataSkippingReader\nimport org.apache.spark.sql.delta.util.StateCache\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.functions._\n\n/**\n * Converts a `Snapshot` into the initial set of files read when starting a new streaming query.\n * The list of files that represent the table at the time the query starts are selected by:\n * - Adding `version` and `index` to each file to enable splitting of the initial state into\n *   multiple batches.\n * - Filtering files that don't match partition predicates, while preserving the aforementioned\n *   indexing.\n */\nclass DeltaSourceSnapshot(\n    val spark: SparkSession,\n    val snapshot: Snapshot,\n    val filters: Seq[Expression])\n  extends StateCache {\n\n  protected val version = snapshot.version\n  protected val path = snapshot.path\n\n  protected lazy val (partitionFilters, dataFilters) = {\n    val partitionCols = snapshot.metadata.partitionColumns\n    val (part, data) = filters.partition { e =>\n      DeltaTableUtils.isPredicatePartitionColumnsOnly(e, partitionCols, spark)\n    }\n    logInfo(log\"Classified filters: partition: ${MDC(DeltaLogKeys.PARTITION_FILTER, part)}, \" +\n      log\"data: ${MDC(DeltaLogKeys.DATA_FILTER, data)}\")\n    (part, data)\n  }\n\n  private[delta] def filteredFiles: Dataset[IndexedFile] = {\n    import spark.implicits.rddToDatasetHolder\n    import org.apache.spark.sql.delta.implicits._\n\n    val initialFiles = snapshot.allFiles\n        // This allows us to control the number of partitions created from the sort instead of\n        // using the shufflePartitions setting\n        .repartitionByRange(snapshot.getNumPartitions, col(\"modificationTime\"), col(\"path\"))\n        .sort(\"modificationTime\", \"path\")\n        .rdd.zipWithIndex()\n        .toDF(\"add\", \"index\")\n        // Stats aren't used for streaming reads right now, so decrease\n        // the size of the files by nulling out the stats if they exist\n        .withColumn(\"add\", col(\"add\").withField(\"stats\", DataSkippingReader.nullStringLiteral))\n        .withColumn(\"remove\", SingleAction.nullLitForRemoveFile)\n        .withColumn(\"cdc\", SingleAction.nullLitForAddCDCFile)\n        .withColumn(\"version\", lit(version))\n        .withColumn(\"isLast\", lit(false))\n        .withColumn(\"shouldSkip\", lit(false))\n\n    DeltaLog.filterFileList(\n      snapshot.metadata.partitionSchema,\n      initialFiles,\n      partitionFilters,\n      Seq(\"add\")).as[IndexedFile]\n  }\n\n  private lazy val cachedState = {\n    cacheDS(filteredFiles, s\"Delta Source Snapshot #$version - ${snapshot.redactedPath}\")\n  }\n\n  def iterator(): Iterator[IndexedFile] = {\n    cachedState.getDS.toLocalIterator().asScala\n  }\n\n  def close(unpersistSnapshot: Boolean): Unit = {\n    uncache()\n    if (unpersistSnapshot) {\n      snapshot.uncache()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/SQLMetricsReporting.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.metric.SQLMetric\n\n/**\n * This trait is used to register SQL metrics for a Delta Operation.\n * Registering will allow the metrics to be instrumented via the CommitInfo and is accessible via\n * DescribeHistory\n */\ntrait SQLMetricsReporting {\n\n  // Map of SQL Metrics\n  private var operationSQLMetrics = Map[String, SQLMetric]()\n\n  /**\n   * Register SQL metrics for an operation by appending the supplied metrics map to the\n   * operationSQLMetrics map.\n   */\n  def registerSQLMetrics(spark: SparkSession, metrics: Map[String, SQLMetric]): Unit = {\n    if (spark.conf.get(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED)) {\n      operationSQLMetrics = operationSQLMetrics ++ metrics\n    }\n  }\n\n  /**\n   * Get the metrics for an operation based on collected SQL Metrics and filtering out\n   * the ones based on the metric parameters for that operation.\n   */\n  def getMetricsForOperation(operation: Operation): Map[String, String] = {\n    operation.transformMetrics(operationSQLMetrics)\n  }\n\n  /** Returns the metric with `name` registered for the given transaction if it exists. */\n  def getMetric(name: String): Option[SQLMetric] = {\n    operationSQLMetrics.get(name)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/TahoeChangeFileIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport java.text.SimpleDateFormat\n\nimport org.apache.spark.sql.delta.{DeltaLog, Snapshot, SnapshotDescriptor}\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader.{CDC_COMMIT_TIMESTAMP, CDC_COMMIT_VERSION, CDCDataSpec}\nimport org.apache.spark.sql.delta.implicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.types.{LongType, StructType, TimestampType}\n\n/**\n * A [[TahoeFileIndex]] for scanning a sequence of CDC files. Similar to [[TahoeBatchFileIndex]],\n * the equivalent for reading [[AddFile]] actions.\n *\n * Note: Please also consider other CDC-related file indexes like [[CdcAddFileIndex]]\n * and [[TahoeRemoveFileIndex]] when modifying this file index.\n */\nclass TahoeChangeFileIndex(\n    spark: SparkSession,\n    val filesByVersion: Seq[CDCDataSpec[AddCDCFile]],\n    deltaLog: DeltaLog,\n    path: Path,\n    snapshot: SnapshotDescriptor)\n  extends TahoeFileIndexWithSnapshotDescriptor(spark, deltaLog, path, snapshot) {\n\n  override def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile] = {\n    // Make some fake AddFiles to satisfy the interface.\n    val addFiles = filesByVersion.flatMap {\n      case CDCDataSpec(version, ts, files, ci) =>\n        files.map { f =>\n          // We add the metadata as faked partition columns in order to attach it on a per-file\n          // basis.\n          val tsOpt = Option(ts)\n            .map(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS Z\").format(_)).orNull\n          val newPartitionVals = f.partitionValues +\n            (CDC_COMMIT_VERSION -> version.toString) +\n            (CDC_COMMIT_TIMESTAMP -> tsOpt)\n          AddFile(f.path, newPartitionVals, f.size, 0, dataChange = false, tags = f.tags)\n        }\n    }\n    DeltaLog.filterFileList(partitionSchema, addFiles.toDF(spark), partitionFilters)\n      .as[AddFile]\n      .collect()\n  }\n\n  override def inputFiles: Array[String] = {\n    filesByVersion.flatMap(_.actions).map(f => absolutePath(f.path).toString).toArray\n  }\n\n  override val partitionSchema: StructType = super.partitionSchema\n    .add(CDC_COMMIT_VERSION, LongType)\n    .add(CDC_COMMIT_TIMESTAMP, TimestampType)\n\n  override def refresh(): Unit = {}\n\n  override val sizeInBytes: Long = filesByVersion.flatMap(_.actions).map(_.size).sum\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/TahoeFileIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.net.URI\nimport java.util.Objects\n\nimport scala.collection.mutable\nimport org.apache.spark.sql.delta.RowIndexFilterType\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaErrors, DeltaLog, DeltaParquetFileFormat, Snapshot, SnapshotDescriptor}\nimport org.apache.spark.sql.delta.DefaultRowCommitVersion\nimport org.apache.spark.sql.delta.RowId\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol}\nimport org.apache.spark.sql.delta.implicits._\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.{InternalRow, TableIdentifier}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Cast, Expression, GenericInternalRow, Literal}\nimport org.apache.spark.sql.execution.datasources._\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Similar to [[FileListingResult]], but maintains the partitions as [[AddFile]].\n */\ncase class DeltaFileListingResult(\n    partitions: Seq[(InternalRow, Seq[AddFile])],\n    addFiles: Seq[AddFile],\n    sortTime: Long = 0L)\n\n/**\n * A [[FileIndex]] that generates the list of files managed by the Tahoe protocol.\n */\nabstract class TahoeFileIndex(\n    val spark: SparkSession,\n    override val deltaLog: DeltaLog,\n    val path: Path)\n  extends FileIndex\n  with SupportsRowIndexFilters\n  with SnapshotDescriptor {\n\n  override def rootPaths: Seq[Path] = path :: Nil\n\n  /**\n   * Returns all matching/valid files by the given `partitionFilters` and `dataFilters`.\n   * Implementations may avoid evaluating data filters when doing so would be expensive, but\n   * *must* evaluate the partition filters; wrong results will be produced if AddFile entries\n   * which don't match the partition filters are returned.\n   */\n  def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile]\n\n  /**\n   * Utility method to convert a sequence of partition values (represented as a Map) and AddFiles\n   * to a sequence of (partitionValuesRow, files) tuples. The partitionValuesRow is a\n   * [[InternalRow]] representing the partition values. The files are represented as a\n   * sequence of [[AddFile]].\n   */\n  private def convertPartitionsToInternalRow(\n      partitions: Seq[(Map[String, String], Seq[AddFile])]): Seq[(InternalRow, Seq[AddFile])] = {\n    partitions.map { case (partitionValues, addFiles) =>\n        (getPartitionValuesRow(partitionValues), addFiles)\n    }\n  }\n\n  /**\n   * Returns (i) tuples of partition directories to their respective AddFile actions and\n   * (ii) a collection of matched AddFiles. The matched AddFiles are those that meet the criteria\n   * set by the partition and data filters. Essentially, this is a collection of all the files\n   * associated with the identified partitions.\n   */\n  def listPartitionsAsAddFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): (Seq[(InternalRow, Seq[AddFile])], Seq[AddFile]) = {\n    val matchedFiles = matchingFiles(partitionFilters, dataFilters)\n    val partitionValuesToFiles = matchedFiles.groupBy(_.partitionValues)\n    (convertPartitionsToInternalRow(partitionValuesToFiles.toSeq), matchedFiles)\n  }\n\n  override def listFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[PartitionDirectory] = {\n    val partitionValuesToFiles = listAddFiles(partitionFilters, dataFilters)\n    makePartitionDirectories(convertPartitionsToInternalRow(partitionValuesToFiles.toSeq))\n  }\n\n\n  private def listAddFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Map[Map[String, String], Seq[AddFile]] = {\n    matchingFiles(partitionFilters, dataFilters).groupBy(_.partitionValues)\n  }\n\n  /**\n   * Generates a FileStatusWithMetadata using data extracted from a given AddFile.\n   */\n  def fileStatusWithMetadataFromAddFile(addFile: AddFile): FileStatusWithMetadata = {\n    val fs = new FileStatus(\n      /* length */ addFile.size,\n      /* isDir */ false,\n      /* blockReplication */ 0,\n      /* blockSize */ 1,\n      /* modificationTime */ addFile.modificationTime,\n      /* path */ absolutePath(addFile.path))\n    val metadata = mutable.Map.empty[String, Any]\n    addFile.baseRowId.foreach(baseRowId => metadata.put(RowId.BASE_ROW_ID, baseRowId))\n    addFile.defaultRowCommitVersion.foreach(defaultRowCommitVersion =>\n      metadata.put(DefaultRowCommitVersion.METADATA_STRUCT_FIELD_NAME, defaultRowCommitVersion))\n\n    if (addFile.deletionVector != null) {\n      metadata.put(DeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_ID_ENCODED,\n        addFile.deletionVector.serializeToBase64())\n\n      // Set the filter type to IF_CONTAINED by default to let [[DeltaParquetFileFormat]] filter\n      // out rows unless a filter type was explicitly provided in rowIndexFilters. This can happen\n      // e.g. when reading CDC data to keep deleted rows instead of filtering them out.\n      val filterType = rowIndexFilters.getOrElse(Map.empty)\n        .getOrElse(addFile.path, RowIndexFilterType.IF_CONTAINED)\n      metadata.put(DeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_TYPE, filterType)\n    }\n    FileStatusWithMetadata(fs, metadata.toMap)\n  }\n\n  def makePartitionDirectories(\n      partitionValuesToFiles: Seq[(InternalRow, Seq[AddFile])]): Seq[PartitionDirectory] = {\n    val timeZone = spark.sessionState.conf.sessionLocalTimeZone\n    partitionValuesToFiles.map {\n      case (partitionValues, files) =>\n        val fileStatuses = files.map(f => fileStatusWithMetadataFromAddFile(f)).toArray\n        PartitionDirectory(partitionValues, fileStatuses)\n    }\n  }\n\n  protected def getPartitionValuesRow(partitionValues: Map[String, String]): GenericInternalRow = {\n    val timeZone = spark.sessionState.conf.sessionLocalTimeZone\n    val partitionRowValues = partitionSchema.map { p =>\n      val colName = DeltaColumnMapping.getPhysicalName(p)\n      val partValue = Literal(partitionValues.get(colName).orNull)\n      Cast(partValue, p.dataType, Option(timeZone), ansiEnabled = false).eval()\n    }.toArray\n    new GenericInternalRow(partitionRowValues)\n  }\n\n  override def partitionSchema: StructType = metadata.partitionSchema\n\n  def absolutePath(child: String): Path = {\n    // scalastyle:off pathfromuri\n    val p = new Path(new URI(child))\n    // scalastyle:on pathfromuri\n    if (p.isAbsolute) {\n      p\n    } else {\n      new Path(path, p)\n    }\n  }\n\n  override def toString: String = {\n    // the rightmost 100 characters of the path\n    val truncatedPath = truncateRight(path.toString, len = 100)\n    s\"Delta[version=$version, $truncatedPath]\"\n  }\n\n  /**\n   * Gets the rightmost {@code len} characters of a String.\n   *\n   * @return the trimmed and formatted string.\n   */\n  private def truncateRight(input: String, len: Int): String = {\n    if (input.length > len) {\n      \"... \" + input.takeRight(len)\n    } else {\n      input\n    }\n  }\n\n  /**\n   * Returns the path of the base directory of the given file path (i.e. its parent directory with\n   * all the partition directories stripped off).\n   */\n  def getBasePath(filePath: Path): Option[Path] = Some(path)\n\n}\n\n/** A [[TahoeFileIndex]] that works with a specific [[SnapshotDescriptor]]. */\nabstract class TahoeFileIndexWithSnapshotDescriptor(\n    spark: SparkSession,\n    deltaLog: DeltaLog,\n    path: Path,\n    snapshot: SnapshotDescriptor) extends TahoeFileIndex(spark, deltaLog, path) {\n\n  override def version: Long = snapshot.version\n  override def metadata: Metadata = snapshot.metadata\n  override def protocol: Protocol = snapshot.protocol\n\n\n  protected[delta] def numOfFilesIfKnown: Option[Long] = snapshot.numOfFilesIfKnown\n  protected[delta] def sizeInBytesIfKnown: Option[Long] = snapshot.sizeInBytesIfKnown\n}\n\n/**\n * A lightweight [[SnapshotDescriptor]] implementation that points to an actual [[Snapshot]].\n *\n * @param snapshot the [[Snapshot]] this pointer points to\n */\nclass ShallowSnapshotDescriptor(\n    snapshot: Snapshot,\n    catalogTableOpt: Option[CatalogTable]) extends SnapshotDescriptor {\n  override val deltaLog: DeltaLog = snapshot.deltaLog\n  override val version: Long = snapshot.version\n  override val metadata: Metadata = snapshot.metadata\n  override val protocol: Protocol = snapshot.protocol\n  // Avoid eager state reconstruction\n  override protected[delta] def numOfFilesIfKnown: Option[Long] =\n    deltaLog.getSnapshotAt(version, catalogTableOpt = catalogTableOpt).numOfFilesIfKnown\n  override protected[delta] def sizeInBytesIfKnown: Option[Long] =\n    deltaLog.getSnapshotAt(version, catalogTableOpt = catalogTableOpt).sizeInBytesIfKnown\n}\n\n/**\n * A [[TahoeFileIndex]] that generates the list of files from DeltaLog with given partition filters.\n *\n * NOTE: This is NOT a [[TahoeFileIndexWithSnapshotDescriptor]] because we only use\n * [[snapshotAtAnalysis]] for actual data skipping if this is a time travel query.\n */\ncase class TahoeLogFileIndex(\n    override val spark: SparkSession,\n    override val deltaLog: DeltaLog,\n    override val path: Path,\n    snapshotAtAnalysis: SnapshotDescriptor,\n    catalogTableOpt: Option[CatalogTable],\n    partitionFilters: Seq[Expression],\n    isTimeTravelQuery: Boolean)\n  extends TahoeFileIndex(spark, deltaLog, path) {\n\n  def this(\n    spark: SparkSession,\n    deltaLog: DeltaLog,\n    path: Path,\n    snapshotAtAnalysis: Snapshot,\n    catalogTableOpt: Option[CatalogTable],\n    partitionFilters: Seq[Expression] = Nil,\n    isTimeTravelQuery: Boolean = false\n  ) = this (\n    spark,\n    deltaLog,\n    path,\n    if (isTimeTravelQuery) snapshotAtAnalysis\n    else new ShallowSnapshotDescriptor(snapshotAtAnalysis, catalogTableOpt),\n    catalogTableOpt,\n    partitionFilters,\n    isTimeTravelQuery)\n\n  require(!isTimeTravelQuery || snapshotAtAnalysis.isInstanceOf[Snapshot])\n\n\n  // WARNING: Stability of this method is _NOT_ guaranteed!\n  override def version: Long = {\n    if (isTimeTravelQuery) snapshotAtAnalysis.version else deltaLog.unsafeVolatileSnapshot.version\n  }\n\n  // WARNING: These methods are intentionally pinned to the analysis-time snapshot, which may differ\n  // from the one returned by [[getSnapshot]] that we will eventually scan.\n  override def metadata: Metadata = snapshotAtAnalysis.metadata\n  override def protocol: Protocol = snapshotAtAnalysis.protocol\n\n  private def checkSchemaOnRead: Boolean = {\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SCHEMA_ON_READ_CHECK_ENABLED)\n  }\n\n  private def includeTableIdInComparisons: Boolean =\n    spark.conf.get(DeltaSQLConf.DELTA_INCLUDE_TABLE_ID_IN_FILE_INDEX_COMPARISON)\n\n  protected def getSnapshotToScan: Snapshot = {\n    if (isTimeTravelQuery) {\n      snapshotAtAnalysis.asInstanceOf[Snapshot]\n    } else {\n      deltaLog.update(stalenessAcceptable = true, catalogTableOpt = catalogTableOpt)\n    }\n  }\n\n  /** Provides the version that's being used as part of the scan if this is a time travel query. */\n  def versionToUse: Option[Long] = if (isTimeTravelQuery) Some(snapshotAtAnalysis.version) else None\n\n  def getSnapshot: Snapshot = {\n    val snapshotToScan = getSnapshotToScan\n    // Always check read compatibility with column mapping tables\n    if (checkSchemaOnRead) {\n      // Ensure that the schema hasn't changed in an incompatible manner since analysis time:\n      // 1. Check logical schema incompatibility\n      // 2. Check column mapping read compatibility. The above check is not sufficient\n      //    when the schema's logical names are not changing but the underlying physical name has\n      //    changed. In this case, the data files cannot be read using the old schema any more.\n      val snapshotSchema = snapshotToScan.metadata.schema\n      if (!SchemaUtils.isReadCompatible(snapshotAtAnalysis.schema, snapshotSchema) ||\n          !DeltaColumnMapping.hasNoColumnMappingSchemaChanges(\n            snapshotToScan.metadata, snapshotAtAnalysis.metadata)) {\n        throw DeltaErrors.schemaChangedSinceAnalysis(snapshotAtAnalysis.schema, snapshotSchema)\n      }\n    }\n\n    // disallow reading table with empty schema, which we support creating now\n    if (snapshotToScan.schema.isEmpty) {\n      // print the catalog identifier or delta.`/path/to/table`\n      var message = TableIdentifier(deltaLog.dataPath.toString, Some(\"delta\")).quotedString\n      throw DeltaErrors.readTableWithoutSchemaException(message)\n    }\n\n    snapshotToScan\n  }\n\n  override def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile] = {\n    getSnapshot.filesForScan(this.partitionFilters ++ partitionFilters ++ dataFilters).files\n  }\n\n  override def inputFiles: Array[String] = {\n    getSnapshot\n      .filesForScan(partitionFilters).files\n      .map(f => absolutePath(f.path).toString)\n      .toArray\n  }\n\n  override def refresh(): Unit = {}\n  override def sizeInBytes: Long = deltaLog.unsafeVolatileSnapshot.sizeInBytes\n\n  override def equals(that: Any): Boolean = that match {\n    case t: TahoeLogFileIndex =>\n      t.path == path &&\n        (if (includeTableIdInComparisons) {\n          t.deltaLog.isSameLogAs(deltaLog)\n        } else {\n          t.deltaLog.dataPath == deltaLog.dataPath\n        }) &&\n        t.versionToUse == versionToUse && t.partitionFilters == partitionFilters\n    case _ => false\n  }\n\n  override def hashCode: scala.Int = {\n    if (includeTableIdInComparisons) {\n      Objects.hashCode(path, deltaLog.compositeId, versionToUse, partitionFilters)\n    } else {\n      Objects.hashCode(path, deltaLog.dataPath, versionToUse, partitionFilters)\n    }\n  }\n\n  protected[delta] def numOfFilesIfKnown: Option[Long] =\n    deltaLog.unsafeVolatileSnapshot.numOfFilesIfKnown\n\n  protected[delta] def sizeInBytesIfKnown: Option[Long] =\n    deltaLog.unsafeVolatileSnapshot.sizeInBytesIfKnown\n}\n\nobject TahoeLogFileIndex {\n  def apply(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable]): TahoeLogFileIndex =\n    new TahoeLogFileIndex(\n      spark, deltaLog, deltaLog.dataPath, deltaLog.unsafeVolatileSnapshot, catalogTableOpt)\n\n  def apply(\n    spark: SparkSession,\n    deltaLog: DeltaLog,\n    path: Path,\n    snapshotAtAnalysis: Snapshot,\n    catalogTableOpt: Option[CatalogTable],\n    partitionFilters: Seq[Expression] = Nil,\n    isTimeTravelQuery: Boolean = false): TahoeLogFileIndex\n  = new TahoeLogFileIndex(\n    spark, deltaLog, path, snapshotAtAnalysis, catalogTableOpt, partitionFilters, isTimeTravelQuery)\n}\n\n/**\n * A [[TahoeFileIndex]] that generates the list of files from a given list of files\n * that are within a version range of DeltaLog.\n */\nclass TahoeBatchFileIndex(\n    spark: SparkSession,\n    val actionType: String,\n    val addFiles: Seq[AddFile],\n    deltaLog: DeltaLog,\n    path: Path,\n    val snapshot: SnapshotDescriptor,\n    val partitionFiltersGenerated: Boolean = false)\n  extends TahoeFileIndexWithSnapshotDescriptor(spark, deltaLog, path, snapshot) {\n\n  override def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile] = {\n    DeltaLog.filterFileList(partitionSchema, addFiles.toDF(spark), partitionFilters)\n      .as[AddFile]\n      .collect()\n  }\n\n  override def inputFiles: Array[String] = {\n    addFiles.map(a => absolutePath(a.path).toString).toArray\n  }\n\n  override def refresh(): Unit = {}\n  override lazy val sizeInBytes: Long = addFiles.map(_.size).sum\n}\n\ntrait SupportsRowIndexFilters {\n  /**\n   * If we know a-priori which exact rows we want to read (e.g., from a previous scan)\n   * find the per-file filter here, which must be passed down to the appropriate reader.\n   *\n   * @return a mapping from file names to the row index filter for that file.\n   */\n  def rowIndexFilters: Option[Map[String, RowIndexFilterType]] = None\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/TahoeRemoveFileIndex.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport java.text.SimpleDateFormat\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddFile, RemoveFile}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader._\nimport org.apache.spark.sql.delta.implicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A [[TahoeFileIndex]] for scanning a sequence of removed files as CDC. Similar to\n * [[TahoeBatchFileIndex]], the equivalent for reading [[AddFile]] actions.\n * @param spark The Spark session.\n * @param filesByVersion Grouped FileActions, one per table version.\n * @param deltaLog The delta log instance.\n * @param path The table's data path.\n * @param snapshot The snapshot where we read CDC from.\n * @param rowIndexFilters Map from <b>URI-encoded</b> file path to a row index filter type.\n *\n * Note: Please also consider other CDC-related file indexes like [[CdcAddFileIndex]]\n * and [[TahoeChangeFileIndex]] when modifying this file index.\n */\nclass TahoeRemoveFileIndex(\n    spark: SparkSession,\n    val filesByVersion: Seq[CDCDataSpec[RemoveFile]],\n    deltaLog: DeltaLog,\n    path: Path,\n    snapshot: SnapshotDescriptor,\n    override val rowIndexFilters: Option[Map[String, RowIndexFilterType]] = None\n  ) extends TahoeFileIndexWithSnapshotDescriptor(spark, deltaLog, path, snapshot) {\n\n  override def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile] = {\n    // Make some fake AddFiles to satisfy the interface.\n    val addFiles = filesByVersion.flatMap {\n      case CDCDataSpec(version, ts, files, ci) =>\n        files.map { r =>\n          if (!r.extendedFileMetadata.getOrElse(false)) {\n            // This shouldn't happen in user queries - the CDC flag was added at the same time as\n            // extended metadata, so all removes in a table with CDC enabled should have it. (The\n            // only exception is FSCK removes, which we screen out separately because they have\n            // dataChange set to false.)\n            throw DeltaErrors.removeFileCDCMissingExtendedMetadata(r.toString)\n          }\n          // We add the metadata as faked partition columns in order to attach it on a per-file\n          // basis.\n          val tsOpt = Option(ts)\n            .map(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS Z\").format(_)).orNull\n          val newPartitionVals = r.partitionValues +\n            (CDC_COMMIT_VERSION -> version.toString) +\n            (CDC_COMMIT_TIMESTAMP -> tsOpt) +\n            (CDC_TYPE_COLUMN_NAME -> CDC_TYPE_DELETE_STRING)\n          AddFile(\n            path = r.path,\n            partitionValues = newPartitionVals,\n            size = r.size.getOrElse(0L),\n            modificationTime = 0,\n            dataChange = r.dataChange,\n            tags = r.tags,\n            deletionVector = r.deletionVector,\n            baseRowId = r.baseRowId\n          )\n        }\n    }\n    DeltaLog.filterFileList(partitionSchema, addFiles.toDF(spark), partitionFilters)\n      .as[AddFile]\n      .collect()\n  }\n\n  override def inputFiles: Array[String] = {\n    filesByVersion.flatMap(_.actions).map(f => absolutePath(f.path).toString).toArray\n  }\n\n  override def partitionSchema: StructType = CDCReader.cdcReadSchema(super.partitionSchema)\n\n  override def refresh(): Unit = {}\n\n  override val sizeInBytes: Long = filesByVersion.flatMap(_.actions).map(_.size.getOrElse(0L)).sum\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/files/TransactionalWrite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport scala.collection.mutable.ListBuffer\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.constraints.{Constraint, Constraints, DeltaInvariantCheckerExec}\nimport org.apache.spark.sql.delta.hooks.AutoCompact\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.perf.DeltaOptimizedWriterExec\nimport org.apache.spark.sql.delta.schema._\nimport org.apache.spark.sql.delta.shims.VariantShreddingShims\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.DELTA_COLLECT_STATS_USING_TABLE_SCHEMA\nimport org.apache.spark.sql.delta.stats.{\n  DeltaJobStatisticsTracker,\n  StatisticsCollection,\n  StatsCollectionUtils\n}\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{DataFrame, Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.plans.logical.LocalRelation\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.connector.catalog._\nimport org.apache.spark.sql.execution._\nimport org.apache.spark.sql.execution.datasources.{BasicWriteJobStatsTracker, FileFormatWriter, WriteJobStatsTracker}\nimport org.apache.spark.sql.functions.{col, to_json}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{StringType, StructField, StructType}\nimport org.apache.spark.util.SerializableConfiguration\n\n/**\n * Adds the ability to write files out as part of a transaction. Checks\n * are performed to ensure that the data being written matches either the\n * current metadata or the new metadata being set by this transaction.\n */\ntrait TransactionalWrite extends DeltaLogging { self: OptimisticTransactionImpl =>\n\n  protected var hasWritten = false\n\n  private[delta] val deltaDataSubdir =\n    if (spark.sessionState.conf.getConf(DeltaSQLConf.WRITE_DATA_FILES_TO_SUBDIR)) {\n      Some(\"data\")\n    } else None\n\n  // It's okay to make this a lazy val. Once this is read, the metadata will be marked as read\n  // and can't be changed again within the transaction, otherwise it will throw an exception.\n  private lazy val randomizeFilePrefixes =\n    DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(metadata)\n  private lazy val randomPrefixLength = DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(metadata)\n\n  protected def getCommitter(outputPath: Path): DelayedCommitProtocol = {\n    // We force the use of random prefixes in column mapping modes.\n    // Note that here we need to use the txn metadata instead of the snapshot's metadata\n    val prefixLengthOpt = if (randomizeFilePrefixes || metadata.columnMappingMode != NoMapping) {\n      Some(randomPrefixLength)\n    } else None\n    new DelayedCommitProtocol(\"delta\", outputPath.toString, prefixLengthOpt, deltaDataSubdir)\n  }\n\n  /** Makes the output attributes nullable, so that we don't write unreadable parquet files. */\n  protected def makeOutputNullable(output: Seq[Attribute]): Seq[Attribute] = {\n    output.map {\n      case ref: AttributeReference =>\n        val nullableDataType = SchemaUtils.typeAsNullable(ref.dataType)\n        ref.copy(dataType = nullableDataType, nullable = true)(ref.exprId, ref.qualifier)\n      case attr => attr.withNullability(true)\n    }\n  }\n\n  /** Replace the output attributes with the physical mapping information. */\n  protected def mapColumnAttributes(\n      output: Seq[Attribute],\n      mappingMode: DeltaColumnMappingMode): Seq[Attribute] = {\n    DeltaColumnMapping.createPhysicalAttributes(output, metadata.schema, mappingMode)\n  }\n\n  /**\n   * Used to perform all required normalizations before writing out the data.\n   * Returns the QueryExecution to execute.\n   */\n  protected def normalizeData(\n      deltaLog: DeltaLog,\n      options: Option[DeltaOptions],\n      data: DataFrame): (QueryExecution, Seq[Attribute], Seq[Constraint], Set[String]) = {\n    val (normalizedSchema, output, constraints, trackHighWaterMarks) = normalizeSchema(\n      deltaLog, options, data)\n\n    (normalizedSchema.queryExecution, output, constraints, trackHighWaterMarks)\n  }\n\n  /**\n   * Normalize the schema of the query, and returns the updated DataFrame. If the table has\n   * generated columns and users provide these columns in the output, we will also return\n   * constraints that should be respected. If any constraints are returned, the caller should apply\n   * these constraints when writing data.\n   *\n   * Note: The schema of the DataFrame may not match the attributes we return as the\n   * output schema. This is because streaming queries create `IncrementalExecution`, which cannot be\n   * further modified. We can however have the Parquet writer use the physical plan from\n   * `IncrementalExecution` and the output schema provided through the attributes.\n   */\n  protected def normalizeSchema(\n      deltaLog: DeltaLog,\n      options: Option[DeltaOptions],\n      data: DataFrame): (DataFrame, Seq[Attribute], Seq[Constraint], Set[String]) = {\n    val normalizedData = SchemaUtils.normalizeColumnNames(\n      deltaLog, metadata.schema, data\n    )\n\n    // Validate that write columns for Row IDs have the correct name.\n    RowId.throwIfMaterializedRowIdColumnNameIsInvalid(\n      normalizedData, metadata, protocol, deltaLog.unsafeVolatileTableId)\n\n    val nullAsDefault = options.isDefined &&\n      options.get.options.contains(ColumnWithDefaultExprUtils.USE_NULL_AS_DEFAULT_DELTA_OPTION)\n    val enforcesDefaultExprs = ColumnWithDefaultExprUtils.tableHasDefaultExpr(\n      protocol, metadata, nullAsDefault)\n    val (dataWithDefaultExprs, generatedColumnConstraints, trackHighWaterMarks) =\n      if (enforcesDefaultExprs) {\n        ColumnWithDefaultExprUtils.addDefaultExprsOrReturnConstraints(\n          deltaLog,\n          protocol,\n          // We need the original query execution if this is a streaming query, because\n          // `normalizedData` may add a new projection and change its type.\n          data.queryExecution,\n          metadata.schema,\n          normalizedData,\n          nullAsDefault)\n      } else {\n        (normalizedData, Nil, Set[String]())\n      }\n    val cleanedData = SchemaUtils.dropNullTypeColumns(dataWithDefaultExprs)\n    val finalData = if (cleanedData.schema != dataWithDefaultExprs.schema) {\n      // This must be batch execution as DeltaSink doesn't accept NullType in micro batch DataFrame.\n      // For batch executions, we need to use the latest DataFrame query execution\n      cleanedData\n    } else if (enforcesDefaultExprs) {\n      dataWithDefaultExprs\n    } else {\n      assert(\n        normalizedData == dataWithDefaultExprs,\n        \"should not change data when there is no generate column\")\n      normalizedData\n    }\n    val nullableOutput = makeOutputNullable(cleanedData.queryExecution.analyzed.output)\n    val columnMapping = metadata.columnMappingMode\n    // Check partition column errors\n    checkPartitionColumns(\n      metadata.partitionSchema, nullableOutput, nullableOutput.length < data.schema.size\n    )\n    // Rewrite column physical names if using a mapping mode\n    val mappedOutput = if (columnMapping == NoMapping) nullableOutput else {\n      mapColumnAttributes(nullableOutput, columnMapping)\n    }\n    (finalData, mappedOutput, generatedColumnConstraints, trackHighWaterMarks)\n  }\n\n  protected def checkPartitionColumns(\n      partitionSchema: StructType,\n      output: Seq[Attribute],\n      colsDropped: Boolean): Unit = {\n    val partitionColumns: Seq[Attribute] = partitionSchema.map { col =>\n      // schema is already normalized, therefore we can do an equality check\n      output.find(f => f.name == col.name).getOrElse(\n        throw DeltaErrors.partitionColumnNotFoundException(col.name, output)\n      )\n    }\n    if (partitionColumns.nonEmpty && partitionColumns.length == output.length) {\n      throw DeltaErrors.nonPartitionColumnAbsentException(colsDropped)\n    }\n  }\n\n  protected def getPartitioningColumns(\n      partitionSchema: StructType,\n      output: Seq[Attribute]): Seq[Attribute] = {\n    val partitionColumns: Seq[Attribute] = partitionSchema.map { col =>\n      // schema is already normalized, therefore we can do an equality check\n      // we have already checked for missing columns, so the fields must exist\n      output.find(f => f.name == col.name).get\n    }\n    partitionColumns\n  }\n\n  /**\n   * If there is any string partition column and there are constraints defined, add a projection to\n   * convert empty string to null for that column. The empty strings will be converted to null\n   * eventually even without this convert, but we want to do this earlier before check constraints\n   * so that empty strings are correctly rejected. Note that this should not cause the downstream\n   * logic in `FileFormatWriter` to add duplicate conversions because the logic there checks the\n   * partition column using the original plan's output. When the plan is modified with additional\n   * projections, the partition column check won't match and will not add more conversion.\n   *\n   * @param plan The original SparkPlan.\n   * @param partCols The partition columns.\n   * @param constraints The defined constraints.\n   * @return A SparkPlan potentially modified with an additional projection on top of `plan`\n   */\n  protected def convertEmptyToNullIfNeeded(\n      plan: SparkPlan,\n      partCols: Seq[Attribute],\n      constraints: Seq[Constraint]): SparkPlan = {\n    if (!spark.conf.get(DeltaSQLConf.CONVERT_EMPTY_TO_NULL_FOR_STRING_PARTITION_COL)) {\n      return plan\n    }\n    // No need to convert if there are no constraints. The empty strings will be converted later by\n    // FileFormatWriter and FileFormatDataWriter. Note that we might still do unnecessary convert\n    // here as the constraints might not be related to the string partition columns. A precise\n    // check will need to walk the constraints to see if such columns are really involved. It\n    // doesn't seem to worth the effort.\n    if (constraints.isEmpty) return plan\n\n    val partSet = AttributeSet(partCols)\n    var needConvert = false\n    val projectList: Seq[NamedExpression] = plan.output.map {\n      case p if partSet.contains(p) && p.dataType == StringType =>\n        needConvert = true\n        Alias(org.apache.spark.sql.catalyst.expressions.Empty2Null(p), p.name)()\n      case attr => attr\n    }\n    if (needConvert) ProjectExec(projectList, plan) else plan\n  }\n\n  def writeFiles(\n      data: Dataset[_],\n      additionalConstraints: Seq[Constraint]): Seq[FileAction] = {\n    writeFiles(data, None, additionalConstraints)\n  }\n\n  def writeFiles(\n      data: Dataset[_],\n      writeOptions: Option[DeltaOptions]): Seq[FileAction] = {\n    writeFiles(data, writeOptions, Nil)\n  }\n\n  def writeFiles(data: Dataset[_]): Seq[FileAction] = {\n    writeFiles(data, Nil)\n  }\n\n  def writeFiles(\n      data: Dataset[_],\n      deltaOptions: Option[DeltaOptions],\n      additionalConstraints: Seq[Constraint]): Seq[FileAction] = {\n    writeFiles(data, deltaOptions, isOptimize = false, additionalConstraints)\n  }\n\n  /**\n   * Returns a tuple of (data, partition schema). For CDC writes, a `__is_cdc` column is added to\n   * the data and `__is_cdc=true/false` is added to the front of the partition schema.\n   */\n  protected def performCDCPartition(inputData: Dataset[_]): (DataFrame, StructType) = {\n    // If this is a CDC write, we need to generate the CDC_PARTITION_COL in order to properly\n    // dispatch rows between the main table and CDC event records. This is a virtual partition\n    // and will be stripped out later in [[DelayedCommitProtocolEdge]].\n    // Note that the ordering of the partition schema is relevant - CDC_PARTITION_COL must\n    // come first in order to ensure CDC data lands in the right place.\n    if (CDCReader.isCDCEnabledOnTable(metadata, spark) &&\n      inputData.schema.fieldNames.contains(CDCReader.CDC_TYPE_COLUMN_NAME)) {\n      val augmentedData = inputData.withColumn(\n        CDCReader.CDC_PARTITION_COL, col(CDCReader.CDC_TYPE_COLUMN_NAME).isNotNull)\n      val partitionSchema = StructType(\n        StructField(CDCReader.CDC_PARTITION_COL, StringType) +: metadata.physicalPartitionSchema)\n      (augmentedData, partitionSchema)\n    } else {\n      (inputData.toDF(), metadata.physicalPartitionSchema)\n    }\n  }\n\n  /**\n   * Return a tuple of (outputStatsCollectionSchema, statsCollectionSchema).\n   * outputStatsCollectionSchema is the data source schema from DataFrame used for stats collection.\n   * It contains the columns in the DataFrame output, excluding the partition columns.\n   * tableStatsCollectionSchema is the schema to collect stats for. It contains the columns in the\n   * table schema, excluding the partition columns.\n   * Note: We only collect NULL_COUNT stats (as the number of rows) for the columns in\n   * statsCollectionSchema but missing in outputStatsCollectionSchema\n   */\n  protected def getStatsSchema(\n    dataFrameOutput: Seq[Attribute],\n    partitionSchema: StructType): (Seq[Attribute], Seq[Attribute]) = {\n    val partitionColNames = partitionSchema.map(_.name).toSet\n\n    // The outputStatsCollectionSchema comes from DataFrame output\n    // schema should be normalized, therefore we can do an equality check\n    val outputStatsCollectionSchema = dataFrameOutput\n      .filterNot(c => partitionColNames.contains(c.name))\n\n    // The tableStatsCollectionSchema comes from table schema\n    val statsTableSchema = toAttributes(metadata.schema)\n    val mappedStatsTableSchema = if (metadata.columnMappingMode == NoMapping) {\n      statsTableSchema\n    } else {\n      mapColumnAttributes(statsTableSchema, metadata.columnMappingMode)\n    }\n\n    // It's important to first do the column mapping and then drop the partition columns\n    val tableStatsCollectionSchema = mappedStatsTableSchema\n      .filterNot(c => partitionColNames.contains(c.name))\n\n    (outputStatsCollectionSchema, tableStatsCollectionSchema)\n  }\n\n  /**\n   * Returns a resolved `statsCollection.statsCollector` expression with `statsDataSchema`\n   * attributes re-resolved to be used for writing Delta file stats.\n   */\n  protected def getStatsColExpr(\n      statsDataSchema: Seq[Attribute],\n      statsCollection: StatisticsCollection): (Expression, Seq[Attribute]) = {\n    val resolvedPlan = DataFrameUtils.ofRows(spark, LocalRelation(statsDataSchema))\n      .select(to_json(statsCollection.statsCollector))\n      .queryExecution.analyzed\n\n    // We have to use the new attributes with regenerated attribute IDs, because the Analyzer\n    // doesn't guarantee that attributes IDs will stay the same\n    val newStatsDataSchema = resolvedPlan.children.head.output\n\n    resolvedPlan.expressions.head -> newStatsDataSchema\n  }\n\n\n  /** Return the pair of optional stats tracker and stats collection class */\n  protected def getOptionalStatsTrackerAndStatsCollection(\n      output: Seq[Attribute],\n      outputPath: Path,\n      partitionSchema: StructType, data: DataFrame): (\n        Option[DeltaJobStatisticsTracker],\n        Option[StatisticsCollection]) = {\n    // check whether we should collect Delta stats\n    val collectStats =\n      (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_COLLECT_STATS)\n      )\n\n    if (collectStats) {\n      val (outputStatsCollectionSchema, tableStatsCollectionSchema) =\n        getStatsSchema(output, partitionSchema)\n\n      val statsCollection = new StatisticsCollection {\n        override val columnMappingMode: DeltaColumnMappingMode = metadata.columnMappingMode\n        override def tableSchema: StructType = metadata.schema\n        override def outputTableStatsSchema: StructType = {\n          // If collecting stats uses the table schema, then we pass in tableStatsCollectionSchema;\n          // otherwise, pass in outputStatsCollectionSchema to collect stats using the DataFrame\n          // schema.\n          if (spark.sessionState.conf.getConf(DELTA_COLLECT_STATS_USING_TABLE_SCHEMA)) {\n            tableStatsCollectionSchema.toStructType\n          } else {\n            outputStatsCollectionSchema.toStructType\n          }\n        }\n        override def outputAttributeSchema: StructType = outputStatsCollectionSchema.toStructType\n        override val spark: SparkSession = data.sparkSession\n        override val statsColumnSpec = StatisticsCollection.configuredDeltaStatsColumnSpec(metadata)\n        override val protocol: Protocol = newProtocol.getOrElse(snapshot.protocol)\n        override def getDataSkippingStringPrefixLength: Int =\n          StatsCollectionUtils.getDataSkippingStringPrefixLength(spark, metadata)\n      }\n      val (statsColExpr, newOutputStatsCollectionSchema) =\n        getStatsColExpr(outputStatsCollectionSchema, statsCollection)\n\n      (Some(new DeltaJobStatisticsTracker(deltaLog.newDeltaHadoopConf(),\n                                          outputPath,\n                                          newOutputStatsCollectionSchema,\n                                          statsColExpr\n        )),\n       Some(statsCollection))\n    } else {\n      (None, None)\n    }\n  }\n\n\n  /**\n   * Writes out the dataframe after performing schema validation. Returns a list of\n   * actions to append these files to the reservoir.\n   *\n   * @param inputData Data to write out.\n   * @param writeOptions Options to decide how to write out the data.\n   * @param isOptimize Whether the operation writing this is Optimize or not.\n   * @param additionalConstraints Additional constraints on the write.\n   */\n  def writeFiles(\n      inputData: Dataset[_],\n      writeOptions: Option[DeltaOptions],\n      isOptimize: Boolean,\n      additionalConstraints: Seq[Constraint]): Seq[FileAction] = {\n    hasWritten = true\n\n    val spark = inputData.sparkSession\n    val (data, partitionSchema) = performCDCPartition(inputData)\n    val outputPath = deltaLog.dataPath\n\n    val (queryExecution, output, generatedColumnConstraints, trackFromData) =\n      normalizeData(deltaLog, writeOptions, data)\n    // Use the track set from the transaction if set,\n    // otherwise use the track set from `normalizeData()`.\n    val trackIdentityHighWaterMarks = trackHighWaterMarks.getOrElse(trackFromData)\n\n    val partitioningColumns = getPartitioningColumns(partitionSchema, output)\n\n    val committer = getCommitter(outputPath)\n\n    val (statsDataSchema, _) = getStatsSchema(output, partitionSchema)\n\n    // If Statistics Collection is enabled, then create a stats tracker that will be injected during\n    // the FileFormatWriter.write call below and will collect per-file stats using\n    // StatisticsCollection\n    val (optionalStatsTracker, _) = getOptionalStatsTrackerAndStatsCollection(output, outputPath,\n      partitionSchema, data)\n\n\n    val constraints =\n      Constraints.getAll(metadata, spark) ++ generatedColumnConstraints ++ additionalConstraints\n    Constraints.validateCheckConstraints(spark, constraints, deltaLog, metadata.schema)\n\n    val identityTrackerOpt = IdentityColumn.createIdentityColumnStatsTracker(\n      spark,\n      deltaLog.newDeltaHadoopConf(),\n      outputPath,\n      metadata.schema,\n      statsDataSchema,\n      trackIdentityHighWaterMarks\n    )\n\n    SQLExecution.withNewExecutionId(queryExecution, Option(\"deltaTransactionalWrite\")) {\n      val outputSpec = FileFormatWriter.OutputSpec(\n        outputPath.toString,\n        Map.empty,\n        output)\n\n      val empty2NullPlan = convertEmptyToNullIfNeeded(queryExecution.executedPlan,\n        partitioningColumns, constraints)\n      val checkInvariants = DeltaInvariantCheckerExec(spark, empty2NullPlan, constraints)\n      // No need to plan optimized write if the write command is OPTIMIZE, which aims to produce\n      // evenly-balanced data files already.\n      val physicalPlan = if (!isOptimize &&\n        shouldOptimizeWrite(writeOptions, spark.sessionState.conf)) {\n        DeltaOptimizedWriterExec(checkInvariants, metadata.partitionColumns, deltaLog)\n      } else {\n        checkInvariants\n      }\n\n      val statsTrackers: ListBuffer[WriteJobStatsTracker] = ListBuffer()\n\n      if (spark.conf.get(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED)) {\n        val basicWriteJobStatsTracker = new BasicWriteJobStatsTracker(\n          new SerializableConfiguration(deltaLog.newDeltaHadoopConf()),\n          BasicWriteJobStatsTracker.metrics)\n        registerSQLMetrics(spark, basicWriteJobStatsTracker.driverSideMetrics)\n        statsTrackers.append(basicWriteJobStatsTracker)\n      }\n\n      // Iceberg spec requires partition columns in data files\n      val writePartitionColumns = IcebergCompat.isAnyEnabled(metadata) ||\n        protocol.isFeatureSupported(MaterializePartitionColumnsTableFeature)\n      // Retain only a minimal selection of Spark writer options to avoid any potential\n      // compatibility issues\n      val options = (writeOptions match {\n        case None => Map.empty[String, String]\n        case Some(writeOptions) =>\n          writeOptions.options.filterKeys { key =>\n            key.equalsIgnoreCase(DeltaOptions.MAX_RECORDS_PER_FILE) ||\n              key.equalsIgnoreCase(DeltaOptions.COMPRESSION)\n          }.toMap\n      }) + (DeltaOptions.WRITE_PARTITION_COLUMNS -> writePartitionColumns.toString) ++\n        VariantShreddingShims.getVariantInferShreddingSchemaOptions(\n          DeltaConfigs.ENABLE_VARIANT_SHREDDING.fromMetaData(metadata))\n\n      try {\n        DeltaFileFormatWriter.write(\n          sparkSession = spark,\n          plan = physicalPlan,\n          fileFormat = deltaLog.fileFormat(protocol, metadata), // TODO support changing formats.\n          committer = committer,\n          outputSpec = outputSpec,\n          // scalastyle:off deltahadoopconfiguration\n          hadoopConf =\n            spark.sessionState.newHadoopConfWithOptions(metadata.configuration ++ deltaLog.options),\n          // scalastyle:on deltahadoopconfiguration\n          partitionColumns = partitioningColumns,\n          bucketSpec = None,\n          statsTrackers = optionalStatsTracker.toSeq\n            ++ statsTrackers\n            ++ identityTrackerOpt.toSeq,\n          options = options)\n      } catch {\n        case InnerInvariantViolationException(violationException) =>\n          // Pull an InvariantViolationException up to the top level if it was the root cause.\n          throw violationException\n      }\n      statsTrackers.foreach {\n        case tracker: BasicWriteJobStatsTracker =>\n          val numOutputRowsOpt = tracker.driverSideMetrics.get(\"numOutputRows\").map(_.value)\n          IdentityColumn.logTableWrite(snapshot, trackIdentityHighWaterMarks, numOutputRowsOpt)\n        case _ => ()\n      }\n    }\n\n    var resultFiles =\n      (if (optionalStatsTracker.isDefined) {\n        committer.addedStatuses.map { a =>\n          a.copy(stats = optionalStatsTracker.map(\n            _.recordedStats(a.toPath.getName)).getOrElse(a.stats))\n        }\n      }\n      else {\n        committer.addedStatuses\n      })\n      .filter {\n      // In some cases, we can write out an empty `inputData`. Some examples of this (though, they\n      // may be fixed in the future) are the MERGE command when you delete with empty source, or\n      // empty target, or on disjoint tables. This is hard to catch before the write without\n      // collecting the DF ahead of time. Instead, we can return only the AddFiles that\n      // a) actually add rows, or\n      // b) don't have any stats so we don't know the number of rows at all\n      case a: AddFile => a.numLogicalRecords.forall(_ > 0)\n      case _ => true\n    }\n\n    // add [[AddFile.Tags.ICEBERG_COMPAT_VERSION.name]] tags to addFiles\n    // starting from IcebergCompatV2\n    val enabledCompat = IcebergCompat.anyEnabled(metadata)\n    if (enabledCompat.exists(_.version >= 2)) {\n      resultFiles = resultFiles.map { addFile =>\n        addFile.copy(tags = Option(addFile.tags).getOrElse(Map.empty[String, String]) +\n          (AddFile.Tags.ICEBERG_COMPAT_VERSION.name -> enabledCompat.get.version.toString)\n        )\n      }\n    }\n\n\n    if (resultFiles.nonEmpty && !isOptimize) registerPostCommitHook(AutoCompact)\n    // Record the updated high water marks to be used during transaction commit.\n    identityTrackerOpt.ifDefined { tracker =>\n      updatedIdentityHighWaterMarks.appendAll(tracker.highWaterMarks.toSeq)\n    }\n\n    resultFiles.toSeq ++ committer.changeFiles\n  }\n\n  /**\n   * Optimized writes can be enabled/disabled through the following order:\n   *  - Through DataFrameWriter options\n   *  - Through SQL configuration\n   *  - Through the table parameter\n   */\n  private def shouldOptimizeWrite(\n      writeOptions: Option[DeltaOptions], sessionConf: SQLConf): Boolean = {\n    writeOptions.flatMap(_.optimizeWrite)\n      .getOrElse(TransactionalWrite.shouldOptimizeWrite(metadata, sessionConf))\n  }\n}\n\nobject TransactionalWrite {\n  def shouldOptimizeWrite(metadata: Metadata, sessionConf: SQLConf): Boolean = {\n    sessionConf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED)\n      .orElse(DeltaConfigs.OPTIMIZE_WRITE.fromMetaData(metadata))\n      .getOrElse(false)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/fuzzer/AtomicBarrier.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.fuzzer\n\nimport java.util.concurrent.atomic.AtomicInteger\n\n/**\n * An atomic barrier is similar to a countdown latch,\n * except that the content is a state transition system with semantic meaning\n * instead of a simple counter.\n *\n * It is designed with a single writer (\"unblocker\") thread and a single reader (\"waiter\") thread\n * in mind. It is concurrency safe with more writers and readers, but using more is likely to cause\n * race conditions for legal transitions. That is to say, trying to perform an otherwise\n * legal transition twice is illegal and may occur if there is more than one unblocker or\n * waiter thread.\n * Having additional passive state observers that only call [[load()]] is never an issue.\n *\n * Legal transitions are:\n * - BLOCKED -> UNBLOCKED\n * - BLOCKED -> REQUESTED\n * - REQUESTED -> UNBLOCKED\n * - UNBLOCKED -> PASSED\n */\nclass AtomicBarrier {\n\n  import AtomicBarrier._\n\n  private final val state: AtomicInteger = new AtomicInteger(State.Blocked.ordinal)\n\n  /** Get the current state. */\n  def load(): State = {\n    val ordinal = state.get()\n    // We should never be putting illegal state ordinals into `state`,\n    // so this should always succeed.\n    stateIndex(ordinal)\n  }\n\n  /** Transition to the Unblocked state. */\n  def unblock(): Unit = {\n    // Just hot-retry this, since it never needs to wait to make progress.\n    var successful = false\n    while(!successful) {\n      val currentValue = state.get()\n      if (currentValue == State.Blocked.ordinal || currentValue == State.Requested.ordinal) {\n        this.synchronized {\n          successful = state.compareAndSet(currentValue, State.Unblocked.ordinal)\n          if (successful) {\n            this.notifyAll()\n          }\n        }\n      } else {\n        // if it's in any other state we will never make progress\n        throw new IllegalStateTransitionException(stateIndex(currentValue), State.Unblocked)\n      }\n    }\n  }\n\n  /** Wait until this barrier can be passed and then mark it as Passed. */\n  def waitToPass(): Unit = {\n    while (true) {\n      val currentState = load()\n      currentState match {\n        case State.Unblocked =>\n          val updated = state.compareAndSet(currentState.ordinal, State.Passed.ordinal)\n          if (updated) {\n            return\n          }\n        case State.Passed =>\n          throw new IllegalStateTransitionException(State.Passed, State.Passed)\n        case State.Requested =>\n          this.synchronized {\n            if (load().ordinal == State.Requested.ordinal) {\n              this.wait()\n            }\n          }\n        case State.Blocked =>\n          this.synchronized {\n            val updated = state.compareAndSet(currentState.ordinal, State.Requested.ordinal)\n            if (updated) {\n              this.wait()\n            }\n          } // else (if we didn't succeed) just hot-retry until we do\n            // (or more likely pass, since unblocking is the only legal concurrent\n            // update with a single concurrent \"waiter\")\n      }\n    }\n  }\n\n  override def toString: String = s\"AtomicBarrier(state=${load()})\"\n}\n\nobject AtomicBarrier {\n\n  sealed trait State {\n    def ordinal: Int\n  }\n\n  object State {\n    case object Blocked extends State {\n      override final val ordinal = 0\n    }\n    case object Unblocked extends State {\n      override final val ordinal = 1\n    }\n    case object Requested extends State {\n      override final val ordinal = 2\n    }\n    case object Passed extends State {\n      override final val ordinal = 3\n    }\n  }\n\n  final val stateIndex: Map[Int, State] =\n    List(State.Blocked, State.Unblocked, State.Requested, State.Passed)\n      .map(state => state.ordinal -> state)\n      .toMap\n}\n\nclass IllegalStateTransitionException(fromState: AtomicBarrier.State, toState: AtomicBarrier.State)\n  extends RuntimeException(s\"State transition from $fromState to $toState is illegal.\")\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/fuzzer/ExecutionPhaseLock.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.fuzzer\n\n/**\n * An ExecutionPhaseLock is an abstraction to keep multiple transactions moving in\n * a pre-selected lock-step sequence.\n *\n * In order to pass a phase, we first wait on the `entryBarrier`. Once we are allowed to pass there,\n * we can execute the code that belongs to this phase, and then we unblock the `exitBarrier`.\n *\n * @param name human readable name for debugging\n */\ncase class ExecutionPhaseLock(\n    name: String,\n    entryBarrier: AtomicBarrier = new AtomicBarrier(),\n    exitBarrier: AtomicBarrier = new AtomicBarrier()) {\n\n  def hasEntered: Boolean = entryBarrier.load() == AtomicBarrier.State.Passed\n\n  def hasLeft: Boolean = {\n    val current = exitBarrier.load()\n    current == AtomicBarrier.State.Unblocked || current == AtomicBarrier.State.Passed\n  }\n\n  /** Blocks at this point until the phase has been entered. */\n  def waitToEnter(): Unit = entryBarrier.waitToPass()\n\n  /** Unblock the next dependent phase. */\n  def leave(): Unit = exitBarrier.unblock()\n\n  /**\n   * Wait to enter this phase, then execute `f`, and leave before returning the result of `f`.\n   *\n   * @return the result of evaluating `f`\n   */\n  def execute[T](f: => T): T = {\n    waitToEnter()\n    try {\n      f\n    } finally {\n      leave()\n    }\n  }\n\n  /**\n   * If there is nothing that needs to be done in this phase,\n   * we can leave immediately after entering.\n   */\n  def passThrough(): Unit = {\n    waitToEnter()\n    leave()\n  }\n\n  def hasReached: Boolean = {\n    val current = entryBarrier.load()\n    current == AtomicBarrier.State.Requested || current == AtomicBarrier.State.Passed\n  }\n\n  /** Blocks at this point until the phase has been left. */\n  def waitToLeave(): Unit = exitBarrier.waitToPass()\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/fuzzer/OptimisticTransactionPhases.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.fuzzer\n\ncase class OptimisticTransactionPhases(\n    initialPhase: ExecutionPhaseLock,\n    preparePhase: ExecutionPhaseLock,\n    commitPhase: ExecutionPhaseLock,\n    backfillPhase: ExecutionPhaseLock,\n    postCommitPhase: ExecutionPhaseLock)\n\nobject OptimisticTransactionPhases {\n\n  private final val PREFIX = \"TXN_\"\n\n  final val INITIAL_PHASE_LABEL = PREFIX + \"INIT\"\n  final val PREPARE_PHASE_LABEL = PREFIX + \"PREPARE\"\n  final val COMMIT_PHASE_LABEL = PREFIX + \"COMMIT\"\n  final val BACKFILL_PHASE_LABEL = PREFIX + \"BACKFILL\"\n  final val POST_COMMIT_PHASE_LABEL = PREFIX + \"POST_COMMIT\"\n\n  def forName(txnName: String): OptimisticTransactionPhases = {\n\n    def toTxnPhaseLabel(phaseLabel: String): String =\n      txnName + \"-\" + phaseLabel\n\n    OptimisticTransactionPhases(\n      initialPhase = ExecutionPhaseLock(toTxnPhaseLabel(INITIAL_PHASE_LABEL)),\n      preparePhase = ExecutionPhaseLock(toTxnPhaseLabel(PREPARE_PHASE_LABEL)),\n      commitPhase = ExecutionPhaseLock(toTxnPhaseLabel(COMMIT_PHASE_LABEL)),\n      backfillPhase = ExecutionPhaseLock(toTxnPhaseLabel(BACKFILL_PHASE_LABEL)),\n      postCommitPhase = ExecutionPhaseLock(toTxnPhaseLabel(POST_COMMIT_PHASE_LABEL)))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/fuzzer/PhaseLockingExecutionObserver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.fuzzer\n\n/**\n * Trait representing execution observers that rely on phases with entry and exit barriers to\n * control the order of execution of the observed code paths. See [[ExecutionPhaseLock]].\n */\ntrait PhaseLockingExecutionObserver {\n\n  val phaseLocks: Seq[ExecutionPhaseLock]\n\n  /** Return `true` if we have left all phases, `false` otherwise. */\n  def allPhasesHavePassed: Boolean = phaseLocks.forall(_.hasLeft)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/fuzzer/PhaseLockingTransactionExecutionObserver.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.fuzzer\n\nimport org.apache.spark.sql.delta.{OptimisticTransaction, TransactionExecutionObserver}\n\nprivate[delta] class PhaseLockingTransactionExecutionObserver(\n    val phases: OptimisticTransactionPhases)\n  extends TransactionExecutionObserver\n  with PhaseLockingExecutionObserver {\n\n  override val phaseLocks: Seq[ExecutionPhaseLock] = Seq(\n    phases.initialPhase,\n    phases.preparePhase,\n    phases.commitPhase,\n    phases.backfillPhase,\n    phases.postCommitPhase)\n\n  override def createChild(): TransactionExecutionObserver = {\n    // Just return the current thread observer.\n    // This is equivalent to the behaviour of the use-site before introduction of\n    // `createChild`.\n    TransactionExecutionObserver.getObserver\n  }\n\n  /**\n   * When set to true this observer will automatically update the thread's current observer to\n   * the next one. Also, it will not unblock the exit barrier of the commit phase automatically.\n   * Instead, the caller will have to automatically unblock it. This allows writing tests that\n   * can capture errors caused by code written between the end of the last txn and the start of\n   * the next txn.\n   */\n  @volatile protected var autoAdvanceNextObserver: Boolean = false\n\n  override def startingTransaction(f: => OptimisticTransaction): OptimisticTransaction =\n    phases.initialPhase.execute(f)\n\n  override def preparingCommit[T](f: => T): T = phases.preparePhase.execute(f)\n\n  override def beginDoCommit(): Unit = {\n    phases.commitPhase.waitToEnter()\n  }\n\n  override def beginBackfill(): Unit = {\n    phases.commitPhase.leave()\n    phases.backfillPhase.waitToEnter()\n  }\n\n  override def beginPostCommit(): Unit = {\n    phases.backfillPhase.leave()\n    phases.postCommitPhase.waitToEnter()\n  }\n\n  override def transactionCommitted(): Unit = {\n    if (nextObserver.nonEmpty && autoAdvanceNextObserver) {\n      waitForLastPhaseAndAdvanceToNextObserver()\n    } else {\n      phases.postCommitPhase.leave()\n    }\n  }\n\n  override def transactionAborted(): Unit = {\n    if (!phases.commitPhase.hasLeft) {\n      if (!phases.commitPhase.hasEntered) {\n        phases.commitPhase.waitToEnter()\n      }\n      phases.commitPhase.leave()\n    }\n    if (!phases.backfillPhase.hasLeft) {\n      if (!phases.backfillPhase.hasEntered) {\n        phases.backfillPhase.waitToEnter()\n      }\n      phases.backfillPhase.leave()\n    }\n    if (!phases.postCommitPhase.hasEntered) {\n      phases.postCommitPhase.waitToEnter()\n    }\n    if (nextObserver.nonEmpty && autoAdvanceNextObserver) {\n      waitForLastPhaseAndAdvanceToNextObserver()\n    } else {\n      phases.postCommitPhase.leave()\n    }\n  }\n\n  /*\n   * Wait for the last phase to pass but do not unblock it so that callers can write tests\n   * that capture errors caused by code between the end of the last txn and the start of the\n   * new txn. After the commit phase is passed, update the thread observer of the thread to\n   * the next observer.\n   */\n  def waitForLastPhaseAndAdvanceToNextObserver(): Unit = {\n    require(nextObserver.nonEmpty)\n    phases.postCommitPhase.waitToLeave()\n    advanceToNextThreadObserver()\n  }\n\n  /**\n   * Set the next observer, which will replace the txn observer on the thread after a successful\n   * commit. This method only works as expected if we haven't entered the commit phase yet.\n   *\n   * Note that when a next observer is set, the caller needs to manually unblock the exit barrier\n   * of the commit phase.\n   *\n   * For example, see [[waitForLastPhaseAndAdvanceToNextObserver]].\n   */\n  def setNextObserver(\n      nextTxnObserver: TransactionExecutionObserver,\n      autoAdvance: Boolean): Unit = {\n    setNextObserver(nextTxnObserver)\n    autoAdvanceNextObserver = autoAdvance\n  }\n\n  override def advanceToNextThreadObserver(): Unit = super.advanceToNextThreadObserver()\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/AutoCompact.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.{DeltaOptimizeContext, OptimizeExecutor}\nimport org.apache.spark.sql.delta.commands.optimize._\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.AutoCompactPartitionStats\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * A trait for post commit hook which compacts files in a Delta table. This hook acts as a cheaper\n * version of the OPTIMIZE command, by attempting to compact small files together into fewer bigger\n * files.\n *\n * Auto Compact chooses files to compact greedily by looking at partition directories which\n * have the largest number of files that are under a certain size threshold and launches a bounded\n * number of optimize tasks based on the capacity of the cluster.\n */\ntrait AutoCompactBase extends PostCommitHook with DeltaLogging {\n\n  override val name: String = \"Auto Compact\"\n\n  private[delta] val OP_TYPE = \"delta.commit.hooks.autoOptimize\"\n\n  /**\n   * This method returns the type of Auto Compaction to use on a delta table or returns None\n   * if Auto Compaction is disabled.\n   * Prioritization:\n   *   1. The highest priority is given to [[DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED]] config.\n   *   2. Then we check if the deprecated property `DeltaConfigs.AUTO_OPTIMIZE` is set. If yes, then\n   *      we return [[AutoCompactType.Enabled]] type.\n   *   3. Then we check the table property [[DeltaConfigs.AUTO_COMPACT]].\n   *   4. If none of 1/2/3 are set explicitly, then we return None\n   */\n  def getAutoCompactType(conf: SQLConf, metadata: Metadata): Option[AutoCompactType] = {\n    // If user-facing conf is set to something, use that value.\n    val autoCompactTypeFromConf =\n      conf.getConf(DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED).map(AutoCompactType(_))\n    if (autoCompactTypeFromConf.nonEmpty) return autoCompactTypeFromConf.get\n\n    // If user-facing conf is not set, use what table property says.\n    val deprecatedFlag = DeltaConfigs.AUTO_OPTIMIZE.fromMetaData(metadata)\n    val autoCompactTypeFromPropertyOrDefaultValue = deprecatedFlag match {\n      case Some(true) =>\n        Some(AutoCompactType.Enabled)\n      case _ =>\n        // If the legacy property `DeltaConfigs.AUTO_OPTIMIZE` is false or not set, then check\n        // the new table property `DeltaConfigs.AUTO_COMPACT`.\n        val confValueFromTableProperty = DeltaConfigs.AUTO_COMPACT.fromMetaData(metadata)\n        confValueFromTableProperty match {\n          case Some(v) =>\n            // Table property is set to something explicitly by user.\n            AutoCompactType(v)\n          case None =>\n            AutoCompactType(AutoCompactType.DISABLED) // Default to disabled\n        }\n    }\n    autoCompactTypeFromPropertyOrDefaultValue\n  }\n\n  private[hooks] def shouldSkipAutoCompact(\n      autoCompactTypeOpt: Option[AutoCompactType],\n      spark: SparkSession,\n      txn: CommittedTransaction): Boolean = {\n    // If auto compact type is empty, then skip compaction\n    if (autoCompactTypeOpt.isEmpty) return true\n\n    // Skip Auto Compaction, if one of the following conditions is satisfied:\n    // -- Auto Compaction is not enabled.\n    // -- Transaction execution time is empty, which means the parent transaction is not committed.\n      !AutoCompactUtils.isQualifiedForAutoCompact(spark, txn)\n\n  }\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    val conf = spark.sessionState.conf\n    val autoCompactTypeOpt = getAutoCompactType(conf, txn.postCommitSnapshot.metadata)\n    // Skip Auto Compact if current transaction is not qualified or the table is not qualified\n    // based on the value of autoCompactTypeOpt.\n    if (shouldSkipAutoCompact(autoCompactTypeOpt, spark, txn)) return\n    compactIfNecessary(\n        spark,\n        txn,\n        OP_TYPE,\n        maxDeletedRowsRatio = None)\n  }\n\n  /**\n   * Compact the target table of write transaction `txn` only when there are sufficient amount of\n   * small size files.\n   */\n  private[delta] def compactIfNecessary(\n      spark: SparkSession,\n      txn: CommittedTransaction,\n      opType: String,\n      maxDeletedRowsRatio: Option[Double]\n  ): Seq[OptimizeMetrics] = {\n    val tableId = txn.deltaLog.unsafeVolatileTableId\n    val autoCompactRequest = AutoCompactUtils.prepareAutoCompactRequest(\n      spark,\n      txn,\n      opType,\n      maxDeletedRowsRatio)\n    if (autoCompactRequest.shouldCompact) {\n      try {\n        val metrics = AutoCompact\n          .compact(\n            spark,\n            txn.deltaLog,\n            txn.catalogTable,\n            autoCompactRequest.targetPartitionsPredicate,\n            opType,\n            maxDeletedRowsRatio\n          )\n        val partitionsStats = AutoCompactPartitionStats.instance(spark)\n        // Mark partitions as compacted before releasing them.\n        // Otherwise an already compacted partition might get picked up by a concurrent thread.\n        // But only marks it as compacted, if no exception was thrown by auto compaction so that the\n        // partitions stay eligible for subsequent auto compactions.\n        partitionsStats.markPartitionsAsCompacted(\n          tableId,\n          autoCompactRequest.allowedPartitions\n        )\n        metrics\n      } catch {\n        case e: Throwable =>\n          logError(log\"Auto Compaction failed with: ${MDC(DeltaLogKeys.ERROR, e.getMessage)}\")\n          recordDeltaEvent(\n            txn.deltaLog,\n            opType = \"delta.autoCompaction.error\",\n            data = getErrorData(e))\n          throw e\n      } finally {\n        if (AutoCompactUtils.reservePartitionEnabled(spark)) {\n          AutoCompactPartitionReserve.releasePartitions(\n            tableId,\n            autoCompactRequest.allowedPartitions\n          )\n        }\n      }\n    } else {\n      Seq.empty[OptimizeMetrics]\n    }\n  }\n\n\n  /**\n   * Launch Auto Compaction jobs if there is sufficient capacity.\n   * @param spark The spark session of the parent transaction that triggers this Auto Compaction.\n   * @param deltaLog The delta log of the parent transaction.\n   * @return the optimize metrics of this compaction job.\n   */\n  private[delta] def compact(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTable: Option[CatalogTable],\n      partitionPredicates: Seq[Expression] = Nil,\n      opType: String = OP_TYPE,\n      maxDeletedRowsRatio: Option[Double] = None)\n  : Seq[OptimizeMetrics] = recordDeltaOperation(deltaLog, opType) {\n    val maxFileSize = spark.conf.get(DeltaSQLConf.DELTA_AUTO_COMPACT_MAX_FILE_SIZE)\n    val minFileSizeOpt = Some(spark.conf.get(DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_FILE_SIZE)\n      .getOrElse(maxFileSize / 2))\n    val maxFileSizeOpt = Some(maxFileSize)\n    recordDeltaOperation(deltaLog, s\"$opType.execute\") {\n      val optimizeContext = DeltaOptimizeContext(\n        reorg = None,\n        minFileSizeOpt,\n        maxFileSizeOpt,\n        maxDeletedRowsRatio = maxDeletedRowsRatio\n      )\n      val rows = new OptimizeExecutor(\n        spark,\n        deltaLog.update(catalogTableOpt = catalogTable),\n        catalogTable,\n        partitionPredicates,\n        zOrderByColumns = Seq(),\n        isAutoCompact = true,\n        optimizeContext\n      )\n      .optimize()\n      val metrics = rows.map(_.getAs[OptimizeMetrics](1))\n      recordDeltaEvent(deltaLog, s\"$opType.execute.metrics\", data = metrics.head)\n      metrics\n    }\n  }\n\n}\n\n/**\n * Post commit hook for Auto Compaction.\n */\ncase object AutoCompact extends AutoCompactBase\n/**\n * A trait describing the type of Auto Compaction.\n */\nsealed trait AutoCompactType {\n  val configValueStrings: Seq[String]\n}\n\nobject AutoCompactType {\n\n  private[hooks] val DISABLED = \"false\"\n\n  /**\n   * Enable auto compact.\n   * 1. MAX_FILE_SIZE is configurable and defaults to 128 MB unless overridden.\n   * 2. MIN_FILE_SIZE is configurable and defaults to MAX_FILE_SIZE / 2 unless overridden.\n   * Note: User can use DELTA_AUTO_COMPACT_MAX_FILE_SIZE to override this value.\n   */\n  case object Enabled extends AutoCompactType {\n    override val configValueStrings = Seq(\n      \"true\"\n    )\n  }\n\n\n  /**\n   * Converts the config value String (coming from [[DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED]] conf\n   * or [[DeltaConfigs.AUTO_COMPACT]] table property) and translates into the [[AutoCompactType]].\n   */\n  def apply(value: String): Option[AutoCompactType] = {\n    if (Enabled.configValueStrings.contains(value)) return Some(Enabled)\n    if (value == DISABLED) return None\n    throw DeltaErrors.invalidAutoCompactType(value)\n  }\n\n  // All allowed values for [[DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED]] and\n  // [[DeltaConfigs.AUTO_COMPACT]].\n  val ALLOWED_VALUES =\n    Enabled.configValueStrings ++\n    Seq(DISABLED)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/AutoCompactUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport scala.collection.mutable\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DeltaLog, Snapshot}\nimport org.apache.spark.sql.delta.CommittedTransaction\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\nimport org.apache.spark.sql.delta.stats.AutoCompactPartitionStats\n\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.{And, Cast, EqualNullSafe, Expression, Literal, Or}\nimport org.apache.spark.sql.functions.collect_list\n\n/**\n * The request class that contains all information needed for Auto Compaction.\n * @param shouldCompact True if Auto Compact should start.\n * @param optimizeContext The context that control execution of optimize command.\n * @param targetPartitionsPredicate The predicate of the target partitions of this Auto Compact\n *                                 request.\n */\ncase class AutoCompactRequest(\n    shouldCompact: Boolean,\n    allowedPartitions: AutoCompactUtils.PartitionKeySet,\n    targetPartitionsPredicate: Seq[Expression] = Nil) {\n}\n\nobject AutoCompactRequest {\n  /** Return a default AutoCompactRequest object that doesn't trigger Auto Compact. */\n  def noopRequest: AutoCompactRequest =\n    AutoCompactRequest(\n      shouldCompact = false,\n      allowedPartitions = Set.empty\n    )\n}\n\nobject AutoCompactUtils extends DeltaLogging {\n  type PartitionKey = Map[String, String]\n  type PartitionKeySet = Set[PartitionKey]\n\n  val STATUS_NAME = {\n    \"status\"\n  }\n\n  /** Create partition predicate from a partition key. */\n  private def createPartitionPredicate(\n      postCommitSnapshot: Snapshot,\n      partitions: PartitionKeySet): Seq[Expression] = {\n    val schema = postCommitSnapshot.metadata.physicalPartitionSchema\n    val partitionBranches = partitions.filterNot(_.isEmpty).map { partition =>\n      partition\n        .toSeq\n        .map { case (key, value) =>\n          val field = schema(key)\n          EqualNullSafe(UnresolvedAttribute.quoted(key), Cast(Literal(value), field.dataType))\n        }\n        .reduceLeft[Expression](And.apply)\n    }\n    if (partitionBranches.size > 1) {\n      Seq(partitionBranches.reduceLeft[Expression](Or.apply))\n    } else if (partitionBranches.size == 1) {\n      partitionBranches.toList\n    } else {\n      Seq.empty\n    }\n  }\n\n  /** True if Auto Compaction only runs on modified partitions. */\n  def isModifiedPartitionsOnlyAutoCompactEnabled(spark: SparkSession): Boolean =\n    spark.sessionState.conf.getConf(DELTA_AUTO_COMPACT_MODIFIED_PARTITIONS_ONLY_ENABLED)\n\n  def isNonBlindAppendAutoCompactEnabled(spark: SparkSession): Boolean =\n    spark.sessionState.conf.getConf(DELTA_AUTO_COMPACT_NON_BLIND_APPEND_ENABLED)\n\n  def reservePartitionEnabled(spark: SparkSession): Boolean =\n    spark.sessionState.conf.getConf(DELTA_AUTO_COMPACT_RESERVE_PARTITIONS_ENABLED)\n\n  /**\n   * Get the minimum number of files to trigger Auto Compact.\n   */\n  def minNumFilesForAutoCompact(spark: SparkSession): Int = {\n    spark.sessionState.conf.getConf(DELTA_AUTO_COMPACT_MIN_NUM_FILES)\n  }\n\n\n  /**\n   * Try to reserve partitions inside `partitionsAddedToOpt` for Auto Compaction.\n   * @return (shouldCompact, finalPartitions) The value of needCompaction is True if Auto\n   *         Compaction needs to run. `finalPartitions` is the set of target partitions that were\n   *         reserved for compaction. If finalPartitions is empty, then all partitions need to be\n   *         considered.\n   */\n  private def reserveTablePartitions(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      postCommitSnapshot: Snapshot,\n      partitionsAddedToOpt: Option[PartitionKeySet],\n      opType: String,\n      maxDeletedRowsRatio: Option[Double]): (Boolean, PartitionKeySet) = {\n    import AutoCompactPartitionReserve._\n    if (partitionsAddedToOpt.isEmpty) {\n      recordDeltaEvent(deltaLog, opType, data = Map(STATUS_NAME -> \"skipEmptyIngestion\"))\n      // If partitionsAddedToOpt is empty, then just skip compact since it means there is no file\n      // added in parent transaction and we do not want to hook AC on empty commits.\n      return (false, Set.empty[PartitionKey])\n    }\n\n    // Reserve partitions as following:\n    // 1) First check if any partitions are free, i.e. no concurrent auto-compact thread is running.\n    // 2) From free partitions check if any are eligible based on the number of small files.\n    // 3) From free partitions check if any are eligible based on the deletion vectors.\n    // 4) Try and reserve the union of the two lists.\n    // All concurrent accesses to partitions reservation and partition stats are managed by the\n    // [[AutoCompactPartitionReserve]] and [[AutoCompactPartitionStats]] singletons.\n    val shouldReservePartitions =\n      isModifiedPartitionsOnlyAutoCompactEnabled(spark) && reservePartitionEnabled(spark)\n    val freePartitions =\n      if (shouldReservePartitions) {\n        filterFreePartitions(deltaLog.unsafeVolatileTableId, partitionsAddedToOpt.get)\n      } else {\n        partitionsAddedToOpt.get\n      }\n\n    // Early abort if all partitions are reserved.\n    if (freePartitions.isEmpty) {\n      recordDeltaEvent(deltaLog, opType,\n        data = Map(STATUS_NAME -> \"skipAllPartitionsAlreadyReserved\"))\n      return (false, Set.empty[PartitionKey])\n    }\n\n    // Check min number of files criteria.\n    val ChosenPartitionsResult(shouldCompactBasedOnNumFiles,\n    chosenPartitionsBasedOnNumFiles, minNumFilesLogMsg) =\n      choosePartitionsBasedOnMinNumSmallFiles(\n        spark,\n        deltaLog,\n        postCommitSnapshot,\n        freePartitions\n      )\n    if (shouldCompactBasedOnNumFiles && chosenPartitionsBasedOnNumFiles.isEmpty) {\n      // Run on all partitions, no need to check other criteria.\n      // Note: this outcome of [choosePartitionsBasedOnMinNumSmallFiles]\n      // is also only possible if partitions reservation is turned off,\n      // so we do not need to reserve partitions.\n      recordDeltaEvent(deltaLog, opType, data = Map(STATUS_NAME -> \"runOnAllPartitions\"))\n      return (shouldCompactBasedOnNumFiles, chosenPartitionsBasedOnNumFiles)\n    }\n\n    // Check files with DVs criteria.\n    val (shouldCompactBasedOnDVs, chosenPartitionsBasedOnDVs) =\n      choosePartitionsBasedOnDVs(freePartitions, postCommitSnapshot, maxDeletedRowsRatio)\n\n    var finalPartitions = chosenPartitionsBasedOnNumFiles ++ chosenPartitionsBasedOnDVs\n    if (isModifiedPartitionsOnlyAutoCompactEnabled(spark)) {\n      val maxNumPartitions = spark.conf.get(DELTA_AUTO_COMPACT_MAX_NUM_MODIFIED_PARTITIONS)\n      finalPartitions = if (finalPartitions.size > maxNumPartitions) {\n        // Choose maxNumPartitions at random.\n        scala.util.Random.shuffle(finalPartitions.toIndexedSeq).take(maxNumPartitions).toSet\n      } else {\n        finalPartitions\n      }\n    }\n\n    val numChosenPartitions = finalPartitions.size\n    if (shouldReservePartitions) {\n      finalPartitions = tryReservePartitions(\n        deltaLog.unsafeVolatileTableId, finalPartitions)\n    }\n    // Abort if all chosen partitions were reserved by a concurrent thread.\n    if (numChosenPartitions > 0 && finalPartitions.isEmpty) {\n      recordDeltaEvent(deltaLog, opType,\n        data = Map(STATUS_NAME -> \"skipAllPartitionsAlreadyReserved\"))\n      return (false, Set.empty[PartitionKey])\n    }\n\n    val shouldCompact = shouldCompactBasedOnNumFiles || shouldCompactBasedOnDVs\n    val statusLogMessage =\n      if (!shouldCompact) {\n        \"skip\" + minNumFilesLogMsg\n      } else if (shouldCompactBasedOnNumFiles && !shouldCompactBasedOnDVs) {\n        \"run\" + minNumFilesLogMsg\n      } else if (shouldCompactBasedOnNumFiles && shouldCompactBasedOnDVs) {\n        \"run\" + minNumFilesLogMsg + \"AndPartitionsWithDVs\"\n      } else if (!shouldCompactBasedOnNumFiles && shouldCompactBasedOnDVs) {\n        \"runOnPartitionsWithDVs\"\n      }\n    val logData = scala.collection.mutable.Map(STATUS_NAME -> statusLogMessage)\n    if (finalPartitions.nonEmpty) {\n      logData += (\"partitions\" -> finalPartitions.size.toString)\n    }\n    recordDeltaEvent(deltaLog, opType, data = logData)\n\n    (shouldCompactBasedOnNumFiles || shouldCompactBasedOnDVs, finalPartitions)\n  }\n\n  private case class ChosenPartitionsResult(\n    shouldRunAC: Boolean,\n    chosenPartitions: PartitionKeySet,\n    logMessage: String)\n\n  private def choosePartitionsBasedOnMinNumSmallFiles(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      postCommitSnapshot: Snapshot,\n      freePartitionsAddedTo: PartitionKeySet): ChosenPartitionsResult = {\n    def getConf[T](entry: ConfigEntry[T]): T = spark.sessionState.conf.getConf(entry)\n\n    val minNumFiles = minNumFilesForAutoCompact(spark)\n      val partitionEarlySkippingEnabled =\n        getConf(DELTA_AUTO_COMPACT_EARLY_SKIP_PARTITION_TABLE_ENABLED)\n      val tablePartitionStats = AutoCompactPartitionStats.instance(spark)\n      if (isModifiedPartitionsOnlyAutoCompactEnabled(spark)) {\n        // If modified partition only Auto Compact is enabled, pick the partitions that have more\n        // number of files than minNumFiles.\n        // If table partition early skipping feature is enabled, use the current minimum number of\n        // files threshold; otherwise, use 0 to indicate that any partition is qualified.\n        val minNumFilesPerPartition = if (partitionEarlySkippingEnabled) minNumFiles else 0L\n        val pickedPartitions = tablePartitionStats.filterPartitionsWithSmallFiles(\n          deltaLog.unsafeVolatileTableId,\n          freePartitionsAddedTo,\n          minNumFilesPerPartition)\n        if (pickedPartitions.isEmpty) {\n          ChosenPartitionsResult(shouldRunAC = false,\n            chosenPartitions = pickedPartitions,\n            logMessage = \"InsufficientFilesInModifiedPartitions\")\n        } else {\n          ChosenPartitionsResult(shouldRunAC = true,\n            chosenPartitions = pickedPartitions,\n            logMessage = \"OnModifiedPartitions\")\n        }\n      } else if (partitionEarlySkippingEnabled) {\n        // If only early skipping is enabled, then check whether there is any partition with more\n        // files than minNumFiles.\n        val maxNumFiles = tablePartitionStats.maxNumFilesInTable(deltaLog.unsafeVolatileTableId)\n        val shouldCompact = maxNumFiles >= minNumFiles\n        if (shouldCompact) {\n          ChosenPartitionsResult(shouldRunAC = true,\n            chosenPartitions = Set.empty[PartitionKey],\n            logMessage = \"OnAllPartitions\")\n        } else {\n          ChosenPartitionsResult(shouldRunAC = false,\n            chosenPartitions = Set.empty[PartitionKey],\n            logMessage = \"InsufficientInAllPartitions\")\n        }\n      } else {\n        // If both are disabled, then Auto Compaction should search all partitions of the target\n        // table.\n        ChosenPartitionsResult(\n          shouldRunAC = true,\n          chosenPartitions = Set.empty[PartitionKey],\n          logMessage = \"OnAllPartitions\")\n      }\n  }\n\n  private def choosePartitionsBasedOnDVs(\n      freePartitionsAddedTo: PartitionKeySet,\n      postCommitSnapshot: Snapshot,\n      maxDeletedRowsRatio: Option[Double]) = {\n    var partitionsWithDVs = if (maxDeletedRowsRatio.nonEmpty) {\n      postCommitSnapshot\n        .allFiles\n        .where(\"deletionVector IS NOT NULL\")\n        .where(\n          s\"\"\"\n           |(deletionVector.cardinality / stats:`numRecords`) > ${maxDeletedRowsRatio.get}\n           |\"\"\".stripMargin)\n        // Cast map to string so we can group by it.\n        // The string representation might not be deterministic.\n        // Still, there is only a limited number of representations we could get for a given map,\n        // Which should sufficiently reduce the data collected on the driver.\n        // We then make sure the partitions are distinct on the driver.\n        .selectExpr(\"CAST(partitionValues AS STRING) as partitionValuesStr\", \"partitionValues\")\n        .groupBy(\"partitionValuesStr\")\n        .agg(collect_list(\"partitionValues\").as(\"partitionValues\"))\n        .selectExpr(\"partitionValues[0] as partitionValues\")\n        .collect()\n        .map(_.getAs[Map[String, String]](\"partitionValues\")).toSet\n    } else {\n      Set.empty[PartitionKey]\n    }\n    partitionsWithDVs = partitionsWithDVs.intersect(freePartitionsAddedTo)\n    (partitionsWithDVs.nonEmpty, partitionsWithDVs)\n  }\n\n  /**\n   * Prepare an [[AutoCompactRequest]] object based on the statistics of partitions inside\n   * `partitionsAddedToOpt` in `txn`.\n   *\n   * @param maxDeletedRowsRatio  If set, signals to Auto Compaction to rewrite files with\n   *                             DVs with maxDeletedRowsRatio above this threshold.\n   */\n  def prepareAutoCompactRequest(\n      spark: SparkSession,\n      txn: CommittedTransaction,\n      opType: String,\n      maxDeletedRowsRatio: Option[Double]): AutoCompactRequest = {\n    val partitionsAddedToOpt = txn.partitionsAddedToOpt.map(_.toSet)\n    val (needAutoCompact, reservedPartitions) = reserveTablePartitions(\n      spark,\n      txn.deltaLog,\n      txn.postCommitSnapshot,\n      partitionsAddedToOpt,\n      opType,\n      maxDeletedRowsRatio)\n    AutoCompactRequest(\n      needAutoCompact,\n      reservedPartitions,\n      createPartitionPredicate(txn.postCommitSnapshot, reservedPartitions))\n  }\n\n  /**\n   * True if this transaction is qualified for Auto Compaction.\n   * - When current transaction is not blind append, it is safe to enable Auto Compaction when\n   *   DELTA_AUTO_COMPACT_MODIFIED_PARTITIONS_ONLY_ENABLED is true, or it's an un-partitioned table,\n   *   because then we cannot introduce _additional_ conflicts with concurrent write transactions.\n   */\n  def isQualifiedForAutoCompact(\n      spark: SparkSession,\n      txn: CommittedTransaction): Boolean = {\n    // If modified partitions only mode is not enabled, return true to avoid subsequent checking.\n    if (!isModifiedPartitionsOnlyAutoCompactEnabled(spark)) return true\n\n    !(isNonBlindAppendAutoCompactEnabled(spark) && txn.isBlindAppend)\n  }\n\n}\n\n/**\n * Thread-safe singleton to keep track of partitions reserved for auto-compaction.\n */\nobject AutoCompactPartitionReserve {\n\n  import org.apache.spark.sql.delta.hooks.AutoCompactUtils.PartitionKey\n\n  // Key is table id and the value the set of currently reserved partition hashes.\n  private val reservedTablesPartitions = new mutable.LinkedHashMap[String, Set[Int]]\n\n  /**\n   * @return Partitions from targetPartitions that are not reserved.\n   */\n  def filterFreePartitions(tableId: String, targetPartitions: Set[PartitionKey])\n  : Set[PartitionKey] = synchronized {\n    val reservedPartitionKeys = reservedTablesPartitions.getOrElse(tableId, Set.empty)\n    targetPartitions.filter(partition => !reservedPartitionKeys.contains(partition.##))\n  }\n\n  /**\n   * Try to reserve partitions from [[targetPartitions]] which are not yet reserved.\n   * @return partitions from targetPartitions which were not previously reserved.\n   */\n  def tryReservePartitions(tableId: String, targetPartitions: Set[PartitionKey])\n  : Set[PartitionKey] = synchronized {\n    val allReservedPartitions = reservedTablesPartitions.getOrElse(tableId, Set.empty)\n    val unReservedPartitionsFromTarget = targetPartitions\n      .filter(targetPartition => !allReservedPartitions.contains(targetPartition.##))\n    val newAllReservedPartitions = allReservedPartitions ++ unReservedPartitionsFromTarget.map(_.##)\n    reservedTablesPartitions.update(tableId, newAllReservedPartitions)\n    unReservedPartitionsFromTarget\n  }\n\n\n  /**\n   * Releases the reserved table partitions to allow other threads to reserve them.\n   * @param tableId The identity of the target table of Auto Compaction.\n   * @param reservedPartitions The set of partitions, which were reserved and which need releasing.\n   */\n  def releasePartitions(\n      tableId: String,\n      reservedPartitions: Set[PartitionKey]): Unit = synchronized {\n    val allReservedPartitions = reservedTablesPartitions.getOrElse(tableId, Set.empty)\n    val newPartitions = allReservedPartitions -- reservedPartitions.map(_.##)\n    reservedTablesPartitions.update(tableId, newPartitions)\n  }\n\n  /** This is test only code to reset the state of table partition reservations. */\n  private[delta] def resetTestOnly(): Unit = synchronized {\n    reservedTablesPartitions.clear()\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/CheckpointHook.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport org.apache.spark.sql.delta.CommittedTransaction\n\nimport org.apache.spark.sql.SparkSession\n\n/** Write a new checkpoint at the version committed by the txn if required. */\nobject CheckpointHook extends PostCommitHook {\n  override val name: String = \"Post commit checkpoint trigger\"\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    if (!txn.needsCheckpoint) return\n\n    // Since the postCommitSnapshot isn't guaranteed to match committedVersion, we have to\n    // explicitly checkpoint the snapshot at the committedVersion.\n    val cp = txn.postCommitSnapshot.checkpointProvider\n    val snapshotToCheckpoint = txn.deltaLog.getSnapshotAt(\n      txn.committedVersion,\n      lastCheckpointHint = None,\n      lastCheckpointProvider = Some(cp),\n      catalogTableOpt = txn.catalogTable,\n      enforceTimeTravelWithinDeletedFileRetention = false)\n    txn.deltaLog.checkpoint(snapshotToCheckpoint, txn.catalogTable)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/ChecksumHook.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{CommittedTransaction, DeltaLog, RecordChecksum, Snapshot}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\n\n/** Write a new checksum at the version committed by the txn if possible. */\nobject ChecksumHook extends PostCommitHook with DeltaLogging {\n  // Helper that creates a RecordChecksum and uses it to write a checksum file\n  case class WriteChecksum(\n      override val spark: SparkSession,\n      override val deltaLog: DeltaLog,\n      txnId: String,\n      snapshot: Snapshot) extends RecordChecksum {\n    writeChecksumFile(txnId, snapshot)\n  }\n\n  override val name: String = \"Post commit checksum trigger\"\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    // Only write the checksum if the postCommitSnapshot matches the version that was committed.\n    if (txn.postCommitSnapshot.version != txn.committedVersion) return\n    logInfo(\n      log\"Writing checksum file for table path ${MDC(DeltaLogKeys.PATH, txn.deltaLog.logPath)} \" +\n      log\"version ${MDC(DeltaLogKeys.VERSION, txn.committedVersion)}\")\n\n    writeChecksum(spark, txn)\n  }\n\n  private def writeChecksum(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    WriteChecksum(spark, txn.deltaLog, txn.txnId, txn.postCommitSnapshot)\n  }\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/GenerateSymlinkManifest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.net.URI\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils.isTableDVFree\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.storage.LogStore\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, ExternalCatalogUtils}\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Cast, Concat, Expression, Literal, ScalaUDF}\nimport org.apache.spark.sql.execution.datasources.InMemoryFileIndex\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.StringType\nimport org.apache.spark.util.SerializableConfiguration\n\n/**\n * Post commit hook to generate hive-style manifests for Delta table. This is useful for\n * compatibility with Presto / Athena.\n */\nobject GenerateSymlinkManifest extends GenerateSymlinkManifestImpl\n\n// A separate singleton to avoid creating encoders from scratch every time\nobject GenerateSymlinkManifestUtils extends DeltaLogging {\n  private[hooks] lazy val mapEncoder = try {\n    ExpressionEncoder[Map[String, String]]()\n  } catch {\n    case e: Throwable =>\n      logError(e.getMessage, e)\n      throw e\n  }\n}\n\ntrait GenerateSymlinkManifestImpl extends PostCommitHook with DeltaLogging with Serializable {\n  val CONFIG_NAME_ROOT = \"compatibility.symlinkFormatManifest\"\n\n  val MANIFEST_LOCATION = \"_symlink_format_manifest\"\n\n  val OP_TYPE_ROOT = \"delta.compatibility.symlinkFormatManifest\"\n  val FULL_MANIFEST_OP_TYPE = s\"$OP_TYPE_ROOT.full\"\n  val INCREMENTAL_MANIFEST_OP_TYPE = s\"$OP_TYPE_ROOT.incremental\"\n\n  override val name: String = \"Generate Symlink Format Manifest\"\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    generateIncrementalManifest(spark, txn, txn.postCommitSnapshot)\n  }\n\n  override def handleError(spark: SparkSession, error: Throwable, version: Long): Unit = {\n    error match {\n      case e: ColumnMappingUnsupportedException => throw e\n      case e: DeltaCommandUnsupportedWithDeletionVectorsException => throw e\n      case _ =>\n        throw DeltaErrors.postCommitHookFailedException(this, version, name, error)\n    }\n  }\n\n  /**\n   * Generate manifest files incrementally, that is, only for the table partitions touched by the\n   * given actions.\n   */\n  protected def generateIncrementalManifest(\n      spark: SparkSession,\n      txn: CommittedTransaction,\n      currentSnapshot: Snapshot): Unit = recordManifestGeneration(txn.deltaLog, full = false) {\n\n    import org.apache.spark.sql.delta.implicits._\n\n    checkColumnMappingMode(currentSnapshot.metadata)\n\n    val deltaLog = txn.deltaLog\n    val partitionCols = currentSnapshot.metadata.partitionColumns\n    val manifestRootDirPath = new Path(deltaLog.dataPath, MANIFEST_LOCATION)\n    val hadoopConf = new SerializableConfiguration(deltaLog.newDeltaHadoopConf())\n    val fs = deltaLog.dataPath.getFileSystem(hadoopConf.value)\n    if (!fs.exists(manifestRootDirPath)) {\n      generateFullManifest(spark, deltaLog, txn.catalogTable)\n      return\n    }\n\n    // Find all the manifest partitions that need to updated or deleted\n    val (allFilesInUpdatedPartitions, nowEmptyPartitions) = if (partitionCols.nonEmpty) {\n      val actions = txn.committedActions\n      val (addFiles, otherActions) = actions.partition(_.isInstanceOf[AddFile])\n      val (removeFiles, _) = otherActions.partition(_.isInstanceOf[RemoveFile])\n\n      // Get the partitions where files were added\n      val partitionsOfAddedFiles = addFiles.collect { case a: AddFile => a.partitionValues }.toSet\n\n      // Get the partitions where files were deleted\n      val removedFileNames =\n        spark.createDataset(removeFiles.collect { case r: RemoveFile => r.path }.toSeq).toDF(\"path\")\n      val partitionValuesOfRemovedFiles =\n        txn.readSnapshot.allFiles.join(removedFileNames, \"path\").select(\"partitionValues\").persist()\n      try {\n        val partitionsOfRemovedFiles = partitionValuesOfRemovedFiles\n          .as[Map[String, String]](GenerateSymlinkManifestUtils.mapEncoder).collect().toSet\n\n        // Get the files present in the updated partitions\n        val partitionsUpdated: Set[Map[String, String]] =\n          partitionsOfAddedFiles ++ partitionsOfRemovedFiles\n        val filesInUpdatedPartitions = currentSnapshot.allFiles.filter { a =>\n          partitionsUpdated.contains(a.partitionValues)\n        }\n\n        // Find the current partitions\n        val currentPartitionRelativeDirs =\n          withRelativePartitionDir(spark, partitionCols, currentSnapshot.allFiles)\n            .select(\"relativePartitionDir\").distinct()\n\n        // Find the partitions that became empty and delete their manifests\n        val partitionRelativeDirsOfRemovedFiles =\n          withRelativePartitionDir(spark, partitionCols, partitionValuesOfRemovedFiles)\n            .select(\"relativePartitionDir\").distinct()\n\n        val partitionsThatBecameEmpty =\n          partitionRelativeDirsOfRemovedFiles.join(\n            currentPartitionRelativeDirs, Seq(\"relativePartitionDir\"), \"leftanti\")\n            .as[String].collect()\n\n        (filesInUpdatedPartitions, partitionsThatBecameEmpty)\n      } finally {\n        partitionValuesOfRemovedFiles.unpersist()\n      }\n    } else {\n      (currentSnapshot.allFiles, Array.empty[String])\n    }\n\n    val manifestFilePartitionsWritten = writeManifestFiles(\n      deltaLog.dataPath,\n      manifestRootDirPath.toString,\n      allFilesInUpdatedPartitions,\n      partitionCols,\n      hadoopConf)\n\n    if (nowEmptyPartitions.nonEmpty) {\n      deleteManifestFiles(manifestRootDirPath.toString, nowEmptyPartitions, hadoopConf)\n    }\n\n    // Post stats\n    val stats = SymlinkManifestStats(\n      filesWritten = manifestFilePartitionsWritten.size,\n      filesDeleted = nowEmptyPartitions.length,\n      partitioned = partitionCols.nonEmpty)\n    recordDeltaEvent(deltaLog, s\"$INCREMENTAL_MANIFEST_OP_TYPE.stats\", data = stats)\n  }\n\n  /**\n   * Generate manifest files for all the partitions in the table. Note, this will ensure that\n   * that stale and unnecessary files will be vacuumed.\n   */\n  def generateFullManifest(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable]): Unit = {\n    val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n    assertTableIsDVFree(spark, snapshot)\n    generateFullManifestWithSnapshot(spark, deltaLog, snapshot)\n  }\n\n  // Separated out to allow overriding with a specific snapshot.\n  protected def generateFullManifestWithSnapshot(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      snapshot: Snapshot): Unit = recordManifestGeneration(deltaLog, full = true) {\n    val partitionCols = snapshot.metadata.partitionColumns\n    val manifestRootDirPath = new Path(deltaLog.dataPath, MANIFEST_LOCATION).toString\n    val hadoopConf = new SerializableConfiguration(deltaLog.newDeltaHadoopConf())\n\n    checkColumnMappingMode(snapshot.metadata)\n\n    // Update manifest files of the current partitions\n    val newManifestPartitionRelativePaths = writeManifestFiles(\n      deltaLog.dataPath,\n      manifestRootDirPath,\n      snapshot.allFiles,\n      partitionCols,\n      hadoopConf)\n\n    // Get the existing manifest files as relative partition paths, that is,\n    // [ \"col1=0/col2=0\", \"col1=1/col2=1\", \"col1=2/col2=2\" ]\n    val fs = deltaLog.dataPath.getFileSystem(hadoopConf.value)\n    val existingManifestPartitionRelativePaths = {\n      val manifestRootDirAbsPath = fs.makeQualified(new Path(manifestRootDirPath))\n      if (fs.exists(manifestRootDirAbsPath)) {\n        val index = new InMemoryFileIndex(\n          spark,\n          Seq(manifestRootDirAbsPath),\n          deltaLog.options,\n          None)\n        val prefixToStrip = manifestRootDirAbsPath.toUri.getPath\n        index.inputFiles.map { p =>\n          // Remove root directory \"rootDir\" path from the manifest file paths like\n          // \"rootDir/col1=0/col2=0/manifest\" to get the relative partition dir \"col1=0/col2=0\".\n          // Note: It important to compare only the \"path\" in the URI and not the user info in it.\n          // In s3a://access-key:secret-key@host/path, the access-key and secret-key may change\n          // unknowingly to `\\` and `%` encoding between the root dir and file names generated\n          // by listing.\n          val relativeManifestFilePath =\n            new URI(p).getPath.stripPrefix(prefixToStrip).stripPrefix(Path.SEPARATOR)\n          new Path(relativeManifestFilePath).getParent.toString // returns \"col1=0/col2=0\"\n        }.filterNot(_.trim.isEmpty).toSet\n      } else Set.empty[String]\n    }\n    // paths returned from inputFiles are URI encoded so we need to convert them back to string.\n    // So that they can compared with newManifestPartitionRelativePaths in the next step.\n\n    // Delete manifest files for partitions that are not in current and so weren't overwritten\n    val manifestFilePartitionsToDelete =\n      existingManifestPartitionRelativePaths.diff(newManifestPartitionRelativePaths)\n    deleteManifestFiles(manifestRootDirPath, manifestFilePartitionsToDelete, hadoopConf)\n\n    // Post stats\n    val stats = SymlinkManifestStats(\n      filesWritten = newManifestPartitionRelativePaths.size,\n      filesDeleted = manifestFilePartitionsToDelete.size,\n      partitioned = partitionCols.nonEmpty)\n    recordDeltaEvent(deltaLog, s\"$FULL_MANIFEST_OP_TYPE.stats\", data = stats)\n  }\n\n  protected def assertTableIsDVFree(spark: SparkSession, snapshot: Snapshot): Unit = {\n    if (!isTableDVFree(snapshot)) {\n      throw DeltaErrors.generateNotSupportedWithDeletionVectors()\n    }\n  }\n\n  /**\n   * Write the manifest files and return the partition relative paths of the manifests written.\n   *\n   * @param deltaLogDataPath     path of the table data (e.g., tablePath which has _delta_log in it)\n   * @param manifestRootDirPath  root directory of the manifest files (e.g., tablePath/_manifest/)\n   * @param fileNamesForManifest relative paths or file names of data files for being written into\n   *                             the manifest (e.g., partition=1/xyz.parquet)\n   * @param partitionCols        Table partition columns\n   * @param hadoopConf           Hadoop configuration to use\n   * @return Set of partition relative paths of the written manifest files (e.g., part1=1/part2=2)\n   */\n  private def writeManifestFiles(\n      deltaLogDataPath: Path,\n      manifestRootDirPath: String,\n      fileNamesForManifest: Dataset[AddFile],\n      partitionCols: Seq[String],\n      hadoopConf: SerializableConfiguration): Set[String] = {\n\n    val spark = fileNamesForManifest.sparkSession\n    import org.apache.spark.sql.delta.implicits._\n\n    val tableAbsPathForManifest = LogStore(spark)\n      .resolvePathOnPhysicalStorage(deltaLogDataPath, hadoopConf.value).toString\n\n    /** Write the data file relative paths to manifestDirAbsPath/manifest as absolute paths */\n    def writeSingleManifestFile(\n      manifestDirAbsPath: String,\n      dataFileRelativePaths: Iterator[String]): Unit = {\n\n      val manifestFilePath = new Path(manifestDirAbsPath, \"manifest\")\n      val fs = manifestFilePath.getFileSystem(hadoopConf.value)\n      fs.mkdirs(manifestFilePath.getParent())\n\n      val manifestContent = dataFileRelativePaths.map { relativePath =>\n        DeltaFileOperations.absolutePath(tableAbsPathForManifest, relativePath).toString\n      }\n      val logStore = LogStore(SparkEnv.get.conf, hadoopConf.value)\n      logStore.write(manifestFilePath, manifestContent, overwrite = true, hadoopConf.value)\n    }\n\n    val newManifestPartitionRelativePaths =\n      if (fileNamesForManifest.isEmpty && partitionCols.isEmpty) {\n        writeSingleManifestFile(manifestRootDirPath, Iterator())\n        Set.empty[String]\n      } else {\n        withRelativePartitionDir(spark, partitionCols, fileNamesForManifest)\n          .select(\"relativePartitionDir\", \"path\").as[(String, String)]\n          .groupByKey(_._1).mapGroups {\n          (relativePartitionDir: String, relativeDataFilePath: Iterator[(String, String)]) =>\n            val manifestPartitionDirAbsPath = {\n              if (relativePartitionDir == null || relativePartitionDir.isEmpty) manifestRootDirPath\n              else new Path(manifestRootDirPath, relativePartitionDir).toString\n            }\n            writeSingleManifestFile(manifestPartitionDirAbsPath, relativeDataFilePath.map(_._2))\n            relativePartitionDir\n        }.collect().toSet\n      }\n\n    logInfo(log\"Generated manifest partitions for ${MDC(DeltaLogKeys.PATH, deltaLogDataPath)} \" +\n      log\"[${MDC(DeltaLogKeys.NUM_PARTITIONS, newManifestPartitionRelativePaths.size)}]:\\n\\t\" +\n      log\"${MDC(DeltaLogKeys.PATHS, newManifestPartitionRelativePaths.mkString(\"\\n\\t\"))}\")\n\n    newManifestPartitionRelativePaths\n  }\n\n  /**\n   * Delete manifest files in the given paths.\n   *\n   * @param manifestRootDirPath root directory of the manifest files (e.g., tablePath/_manifest/)\n   * @param partitionRelativePathsToDelete partitions to delete manifest files from\n   *                                       (e.g., part1=1/part2=2/)\n   * @param hadoopConf Hadoop configuration to use\n   */\n  private def deleteManifestFiles(\n      manifestRootDirPath: String,\n      partitionRelativePathsToDelete: Iterable[String],\n      hadoopConf: SerializableConfiguration): Unit = {\n\n    val fs = new Path(manifestRootDirPath).getFileSystem(hadoopConf.value)\n    partitionRelativePathsToDelete.foreach { path =>\n      val absPathToDelete = new Path(manifestRootDirPath, path)\n      fs.delete(absPathToDelete, true)\n    }\n\n    logInfo(log\"Deleted manifest partitions [\" +\n      log\"${MDC(DeltaLogKeys.NUM_FILES, partitionRelativePathsToDelete.size.toLong)}]:\\n\\t\" +\n      log\"${MDC(DeltaLogKeys.PATHS, partitionRelativePathsToDelete.mkString(\"\\n\\t\"))}\")\n  }\n\n  /**\n   * Append a column `relativePartitionDir` to the given Dataset which has `partitionValues` as\n   * one of the columns. `partitionValues` is a map-type column that contains values of the\n   * given `partitionCols`.\n   */\n  private def withRelativePartitionDir(\n      spark: SparkSession,\n      partitionCols: Seq[String],\n      datasetWithPartitionValues: Dataset[_]) = {\n\n    require(datasetWithPartitionValues.schema.fieldNames.contains(\"partitionValues\"))\n    val colNamePrefix = \"_col_\"\n\n    // Flatten out nested partition value columns while renaming them, so that the new columns do\n    // not conflict with existing columns in DF `pathsWithPartitionValues.\n    val colToRenamedCols = partitionCols.map { column => column -> s\"$colNamePrefix$column\" }\n\n    val df = colToRenamedCols.foldLeft(datasetWithPartitionValues.toDF()) {\n      case(currentDs, (column, renamedColumn)) =>\n        currentDs.withColumn(renamedColumn, col(s\"partitionValues.`$column`\"))\n    }\n\n    // Mapping between original column names to use for generating partition path and\n    // attributes referring to corresponding columns added to DF `pathsWithPartitionValues`.\n    val colNameToAttribs =\n      colToRenamedCols.map { case (col, renamed) => col -> UnresolvedAttribute.quoted(renamed) }\n\n    // Build an expression that can generate the path fragment col1=value/col2=value/ from the\n    // partition columns. Note: The session time zone maybe different from the time zone that was\n    // used to write the partition structure of the actual data files. This may lead to\n    // inconsistencies between the partition structure of metadata files and data files.\n    val relativePartitionDirExpression = generatePartitionPathExpression(\n      colNameToAttribs,\n      spark.sessionState.conf.sessionLocalTimeZone)\n\n    df.withColumn(\"relativePartitionDir\", Column(relativePartitionDirExpression))\n      .drop(colToRenamedCols.map(_._2): _*)\n  }\n\n  /** Expression that given partition columns builds a path string like: col1=val/col2=val/... */\n  protected def generatePartitionPathExpression(\n      partitionColNameToAttrib: Seq[(String, Attribute)],\n      timeZoneId: String): Expression = Concat(\n\n    partitionColNameToAttrib.zipWithIndex.flatMap { case ((colName, col), i) =>\n      val partitionName = ScalaUDF(\n        ExternalCatalogUtils.getPartitionPathString _,\n        StringType,\n        Seq(Literal(colName), Cast(col, StringType, Option(timeZoneId))))\n      if (i == 0) Seq(partitionName) else Seq(Literal(Path.SEPARATOR), partitionName)\n    }\n  )\n\n\n  private def recordManifestGeneration(deltaLog: DeltaLog, full: Boolean)(thunk: => Unit): Unit = {\n    val (opType, manifestType) =\n      if (full) FULL_MANIFEST_OP_TYPE -> \"full\"\n      else INCREMENTAL_MANIFEST_OP_TYPE -> \"incremental\"\n    recordDeltaOperation(deltaLog, opType) {\n      withStatusCode(\"DELTA\", s\"Updating $manifestType Hive manifest for the Delta table\") {\n        thunk\n      }\n    }\n  }\n\n  /**\n   * Generating manifests, when column mapping used is not supported,\n   * because external systems will not be able to read Delta tables that leverage\n   * column mapping correctly.\n   */\n  private def checkColumnMappingMode(metadata: Metadata): Unit = {\n    if (metadata.columnMappingMode != NoMapping) {\n      throw DeltaErrors.generateManifestWithColumnMappingNotSupported\n    }\n  }\n\n  case class SymlinkManifestStats(\n      filesWritten: Int,\n      filesDeleted: Int,\n      partitioned: Boolean)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/HudiConverterHook.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport org.apache.spark.sql.delta.{CommittedTransaction, UniversalFormat}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.DELTA_UNIFORM_HUDI_SYNC_CONVERT_ENABLED\n\nimport org.apache.spark.sql.SparkSession\n\n/** Write a new Hudi commit for the version committed by the txn, if required. */\nobject HudiConverterHook extends PostCommitHook with DeltaLogging {\n  override val name: String = \"Post-commit Hudi metadata conversion\"\n\n  val ASYNC_HUDI_CONVERTER_THREAD_NAME = \"async-hudi-converter\"\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    val postCommitSnapshot = txn.postCommitSnapshot\n    // Only convert to Hudi if the snapshot matches the version committed.\n    // This is to skip converting the same actions multiple times - they'll be written out\n    // by another commit anyways.\n    if (txn.committedVersion != postCommitSnapshot.version ||\n        !UniversalFormat.hudiEnabled(postCommitSnapshot.metadata)) {\n      return\n    }\n    val converter = postCommitSnapshot.deltaLog.hudiConverter\n    if (spark.sessionState.conf.getConf(DELTA_UNIFORM_HUDI_SYNC_CONVERT_ENABLED)) {\n      converter.convertSnapshot(postCommitSnapshot, txn)\n    } else {\n      converter.enqueueSnapshotForConversion(postCommitSnapshot, txn)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/IcebergConverterHook.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport org.apache.spark.sql.delta.{CommittedTransaction, DeltaErrors, UniversalFormat, UniversalFormatConverter}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.DELTA_UNIFORM_ICEBERG_SYNC_CONVERT_ENABLED\nimport org.apache.commons.lang3.exception.ExceptionUtils\n\nimport org.apache.spark.sql.SparkSession\n\n/** Write a new Iceberg metadata file at the version committed by the txn, if required. */\ntrait IcebergConverterHook extends PostCommitHook with DeltaLogging {\n  override val name: String = \"Post-commit Iceberg metadata conversion\"\n\n  val ASYNC_ICEBERG_CONVERTER_THREAD_NAME = \"async-iceberg-converter\"\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    val postCommitSnapshot = txn.postCommitSnapshot\n    // Only convert to Iceberg if the snapshot matches the version committed.\n    // This is to skip converting the same actions multiple times - they'll be written out\n    // by another commit anyways.\n    if (txn.committedVersion != postCommitSnapshot.version ||\n        !UniversalFormat.icebergEnabled(postCommitSnapshot.metadata)) {\n      return\n    }\n\n    val converter = postCommitSnapshot.deltaLog.icebergConverter\n    triggerIcebergConversion(converter, spark, txn)\n  }\n\n  // Always throw when sync Iceberg conversion fails. Async conversion exception\n  // is handled in the async thread.\n  override def handleError(spark: SparkSession, error: Throwable, version: Long): Unit = {\n    logError(error.getMessage, error)\n    throw DeltaErrors.universalFormatConversionFailedException(\n      version, \"iceberg\", ExceptionUtils.getMessage(error))\n  }\n\n  def triggerIcebergConversion(\n      converter: UniversalFormatConverter,\n      spark: SparkSession,\n      txn: CommittedTransaction): Unit = {\n    val postCommitSnapshot = txn.postCommitSnapshot\n    if (spark.sessionState.conf.getConf(DELTA_UNIFORM_ICEBERG_SYNC_CONVERT_ENABLED) ||\n      !UniversalFormat.icebergEnabled(txn.readSnapshot.metadata)) { // UniForm was not enabled\n      converter.convertSnapshot(postCommitSnapshot, txn)\n    } else {\n      converter.enqueueSnapshotForConversion(postCommitSnapshot, txn)\n    }\n  }\n}\n\nobject IcebergConverterHook extends IcebergConverterHook\n\nobject IcebergSyncConverterHook extends IcebergConverterHook {\n  override def triggerIcebergConversion(\n      converter: UniversalFormatConverter,\n      spark: SparkSession,\n      txn: CommittedTransaction): Unit = {\n    converter.convertSnapshot(txn.postCommitSnapshot, txn)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/PostCommitHook.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.SparkSession\n\n/**\n * A hook which can be executed after a transaction. These hooks are registered to a\n * [[OptimisticTransaction]], and are executed after a *successful* commit takes place.\n */\ntrait PostCommitHook {\n\n  /** A user-friendly name for the hook for error reporting purposes. */\n  val name: String\n\n  /**\n   * Executes the hook.\n   * @param txn The txn that made the commit, after which this PostCommitHook was run\n   */\n  def run(spark: SparkSession, txn: CommittedTransaction): Unit\n\n  /**\n   * Handle any error caused while running the hook. By default, all errors are ignored as\n   * default policy should be to not let post-commit hooks to cause failures in the operation.\n   */\n  def handleError(spark: SparkSession, error: Throwable, version: Long): Unit = {\n    if (spark.conf.get(DeltaSQLConf.DELTA_POST_COMMIT_HOOK_THROW_ON_ERROR)) {\n      throw DeltaErrors.postCommitHookFailedException(this, version, name, error)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/hooks/UpdateCatalog.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.hooks\n\nimport java.nio.charset.Charset\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.{ExecutionContext, Future, TimeoutException}\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta.{CommittedTransaction, DeltaConfigs, DeltaTableIdentifier, Snapshot}\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.threads.DeltaThreadPool\nimport org.apache.commons.lang3.exception.ExceptionUtils\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.connector.catalog.CatalogManager.SESSION_CATALOG_NAME\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.ThreadUtils\n\n/**\n * Factory object to create an UpdateCatalog post commit hook. This should always be used\n * instead of directly creating a specific hook.\n */\nobject UpdateCatalogFactory {\n  def getUpdateCatalogHook(table: CatalogTable, spark: SparkSession): UpdateCatalogBase = {\n    UpdateCatalog(table)\n  }\n}\n\n/**\n * Base trait for post commit hooks that want to update the catalog with the\n * latest table schema and properties.\n */\ntrait UpdateCatalogBase extends PostCommitHook with DeltaLogging {\n\n  protected val table: CatalogTable\n\n  override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {\n    // There's a potential race condition here, where a newer commit has already triggered\n    // this to run. That's fine.\n    executeOnWrite(spark, txn.postCommitSnapshot)\n  }\n\n  /**\n   * Used to manually execute an UpdateCatalog hook during a write.\n   */\n  def executeOnWrite(\n    spark: SparkSession,\n    snapshot: Snapshot\n    ): Unit\n\n\n  /**\n   * Update the schema in the catalog based on the provided snapshot.\n   */\n  def updateSchema(spark: SparkSession, snapshot: Snapshot): Unit\n\n  /**\n   * Update the properties in the catalog based on the provided snapshot.\n   */\n  protected def updateProperties(spark: SparkSession, snapshot: Snapshot): Unit\n\n  /**\n   * Checks if the table schema has changed in the Snapshot with respect to what's stored in\n   * the catalog.\n   */\n  protected def schemaHasChanged(snapshot: Snapshot, spark: SparkSession): Boolean\n\n  /**\n   * Checks if the table properties have changed in the Snapshot with respect to what's stored in\n   * the catalog.\n   *\n   * Visible for testing.\n   */\n  protected[sql] def propertiesHaveChanged(\n    properties: Map[String, String],\n    metadata: Metadata,\n    spark: SparkSession): Boolean\n\n  protected def shouldRun(\n      spark: SparkSession,\n      snapshot: Snapshot\n      ): Boolean = {\n    if (!spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED)) {\n      return false\n    }\n    // Do not execute for path based tables, because they don't exist in the MetaStore\n    if (isPathBasedDeltaTable(table, spark)) return false\n    // Only execute if this is a Delta table\n    if (snapshot.version < 0) return false\n    true\n  }\n\n  private def isPathBasedDeltaTable(table: CatalogTable, spark: SparkSession): Boolean = {\n    return DeltaTableIdentifier.isDeltaPath(spark, table.identifier)\n  }\n\n  /** Check if the clustering columns from snapshot doesn't match what's in the table properties. */\n  protected def clusteringColumnsChanged(snapshot: Snapshot): Boolean = {\n    if (!ClusteredTableUtils.isSupported(snapshot.protocol)) {\n      return false\n    }\n    val currentLogicalClusteringNames =\n      ClusteringColumnInfo.extractLogicalNames(snapshot).mkString(\",\")\n    val clusterBySpecOpt = ClusterBySpec.fromProperties(table.properties)\n\n    // Since we don't remove the clustering columns table property, this can't happen.\n    assert(!(currentLogicalClusteringNames.nonEmpty && clusterBySpecOpt.isEmpty))\n    clusterBySpecOpt.exists(_.columnNames.map(_.toString).mkString(\",\") !=\n      currentLogicalClusteringNames)\n  }\n\n  /** Update the entry in the Catalog to reflect the latest schema and table properties. */\n  protected def execute(\n      spark: SparkSession,\n      snapshot: Snapshot): Unit = {\n    recordDeltaOperation(snapshot.deltaLog, \"delta.catalog.update\") {\n      val properties = snapshot.getProperties.toMap\n      val v = table.properties.get(DeltaConfigs.METASTORE_LAST_UPDATE_VERSION)\n        .flatMap(v => Try(v.toLong).toOption)\n        .getOrElse(-1L)\n      val lastCommitTimestamp = table.properties.get(DeltaConfigs.METASTORE_LAST_COMMIT_TIMESTAMP)\n        .flatMap(v => Try(v.toLong).toOption)\n        .getOrElse(-1L)\n      // If the metastore entry is at an older version and not the timestamp of that version, e.g.\n      // a table can be rm -rf'd and get the same version number with a different timestamp\n      if (v <= snapshot.version || lastCommitTimestamp < snapshot.timestamp) {\n        try {\n          val loggingData = Map(\n            \"identifier\" -> table.identifier,\n            \"snapshotVersion\" -> snapshot.version,\n            \"snapshotTimestamp\" -> snapshot.timestamp,\n            \"catalogVersion\" -> v,\n            \"catalogTimestamp\" -> lastCommitTimestamp\n          )\n          if (schemaHasChanged(snapshot, spark)) {\n            updateSchema(spark, snapshot)\n            recordDeltaEvent(\n              snapshot.deltaLog,\n              \"delta.catalog.update.schema\",\n              data = loggingData\n            )\n          } else if (propertiesHaveChanged(properties, snapshot.metadata, spark)) {\n            updateProperties(spark, snapshot)\n            recordDeltaEvent(\n              snapshot.deltaLog,\n              \"delta.catalog.update.properties\",\n              data = loggingData\n            )\n          } else if (clusteringColumnsChanged(snapshot)) {\n            // If the clustering columns changed, we'll update the catalog with the new\n            // table properties.\n            updateProperties(spark, snapshot)\n            recordDeltaEvent(\n              snapshot.deltaLog,\n              \"delta.catalog.update.clusteringColumns\",\n              data = loggingData\n            )\n          }\n        } catch {\n          case NonFatal(e) =>\n            recordDeltaEvent(\n              snapshot.deltaLog,\n              \"delta.catalog.update.error\",\n              data = Map(\n                \"exceptionMsg\" -> ExceptionUtils.getMessage(e),\n                \"stackTrace\" -> ExceptionUtils.getStackTrace(e))\n            )\n            logWarning(log\"Failed to update the catalog for \" +\n              log\"${MDC(DeltaLogKeys.TABLE_NAME, table.identifier)} with the latest \" +\n              log\"table information.\", e)\n        }\n      }\n    }\n  }\n}\n\n/**\n * A post-commit hook that allows us to cache the most recent schema and table properties of a Delta\n * table in an External Catalog. In addition to the schema and table properties, we also store the\n * last commit timestamp and version for which we updated the catalog. This prevents us from\n * updating the MetaStore with potentially stale information.\n */\ncase class UpdateCatalog(table: CatalogTable) extends UpdateCatalogBase {\n\n  override val name: String = \"Update Catalog\"\n\n  override def executeOnWrite(\n      spark: SparkSession,\n      snapshot: Snapshot\n     ): Unit = {\n    executeAsync(spark, snapshot)\n  }\n\n\n  override protected def schemaHasChanged(snapshot: Snapshot, spark: SparkSession): Boolean = {\n    // We need to check whether the schema in the catalog matches the current schema.\n    // Depending on the schema validation policy, the schema might need to be truncated.\n    // Therefore, we should use what we want to store in the catalog for comparison.\n    val truncationThreshold = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD)\n    val schemaChanged = table.schema != UpdateCatalog.truncateSchemaIfNecessary(\n      snapshot.schema,\n      truncationThreshold)._1\n    // The table may have been dropped as we're just about to update the information. There is\n    // unfortunately no great way to avoid a race condition, but we do one last check here as\n    // updates may have been queued for some time.\n    schemaChanged && spark.sessionState.catalog.tableExists(table.identifier)\n  }\n\n  /**\n   * Checks if the table properties have changed in the Snapshot with respect to what's stored in\n   * the catalog. We check to see if our table properties are a subset of what is in the MetaStore\n   * to avoid flip-flopping the information between older and newer versions of Delta. The\n   * assumption here is that newer Delta releases will only add newer table properties and not\n   * remove them.\n   */\n  override protected[sql] def propertiesHaveChanged(\n      properties: Map[String, String],\n      metadata: Metadata,\n      spark: SparkSession): Boolean = {\n    val propertiesChanged = !properties.forall { case (k, v) =>\n      table.properties.get(k) == Some(v)\n    }\n    // The table may have been dropped as we're just about to update the information. There is\n    // unfortunately no great way to avoid a race condition, but we do one last check here as\n    // updates may have been queued for some time.\n    propertiesChanged && spark.sessionState.catalog.tableExists(table.identifier)\n  }\n\n  override def updateSchema(spark: SparkSession, snapshot: Snapshot): Unit = {\n    UpdateCatalog.replaceTable(spark, snapshot, table)\n  }\n\n  override protected def updateProperties(spark: SparkSession, snapshot: Snapshot): Unit = {\n    spark.sessionState.catalog.alterTable(\n      table.copy(properties = UpdateCatalog.updatedProperties(snapshot)))\n  }\n\n  /**\n   * Update the entry in the Catalog to reflect the latest schema and table properties\n   * asynchronously.\n   */\n  private def executeAsync(\n      spark: SparkSession,\n      snapshot: Snapshot): Unit = {\n    if (!shouldRun(spark, snapshot)) return\n    UpdateCatalog.activeAsyncRequests.incrementAndGet()\n    Future[Unit] {\n      execute(spark, snapshot)\n    }(UpdateCatalog.getOrCreateExecutionContext(spark.sessionState.conf)).onComplete { _ =>\n      UpdateCatalog.activeAsyncRequests.decrementAndGet()\n    }(UpdateCatalog.getOrCreateExecutionContext(spark.sessionState.conf))\n  }\n}\n\nobject UpdateCatalog {\n  // Exposed for testing.\n  private[delta] var tp: ExecutionContext = _\n\n  // This is the encoding of the database for the Hive MetaStore\n  private val latin1 = Charset.forName(\"ISO-8859-1\")\n\n  val ERROR_KEY = \"delta.catalogUpdateError\"\n  val LONG_SCHEMA_ERROR: String = \"The schema contains a very long nested field and cannot be \" +\n    \"stored in the catalog.\"\n  val NON_LATIN_CHARS_ERROR: String = \"The schema contains non-latin encoding characters and \" +\n    \"cannot be stored in the catalog.\"\n  val HIVE_METASTORE_NAME = \"hive_metastore\"\n\n  private def getOrCreateExecutionContext(conf: SQLConf): ExecutionContext = synchronized {\n    if (tp == null) {\n      tp = ExecutionContext.fromExecutorService(DeltaThreadPool.newDaemonCachedThreadPool(\n        \"delta-catalog-update\",\n        conf.getConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_THREAD_POOL_SIZE)\n        )\n      )\n    }\n    tp\n  }\n\n  /** Keeps track of active or queued async requests. */\n  private val activeAsyncRequests = new AtomicInteger(0)\n\n  /**\n   * Waits for all active and queued updates to finish until the given timeout. Will return true\n   * if all async threads have completed execution. Will return false if not. Exposed for tests.\n   */\n  def awaitCompletion(timeoutMillis: Long): Boolean = {\n    try {\n      ThreadUtils.runInNewThread(\"UpdateCatalog-awaitCompletion\") {\n        val startTime = System.currentTimeMillis()\n        while (activeAsyncRequests.get() > 0) {\n          Thread.sleep(100)\n          val currentTime = System.currentTimeMillis()\n          if (currentTime - startTime > timeoutMillis) {\n            throw new TimeoutException(\n              s\"Timed out waiting for catalog updates to complete after $currentTime ms\")\n          }\n        }\n      }\n      true\n    } catch {\n      case _: TimeoutException =>\n        false\n    }\n  }\n\n  /** Replace the table definition in the MetaStore. */\n  private def replaceTable(spark: SparkSession, snapshot: Snapshot, table: CatalogTable): Unit = {\n    val catalog = spark.sessionState.catalog\n    val qualifiedIdentifier =\n      catalog.qualifyIdentifier(TableIdentifier(table.identifier.table, Some(table.database)))\n    val db = qualifiedIdentifier.database.get\n    val tblName = qualifiedIdentifier.table\n    val truncationThreshold = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD)\n    val (schema, additionalProperties) = truncateSchemaIfNecessary(\n      snapshot.schema,\n      truncationThreshold)\n\n    // We call the lower level API so that we can actually drop columns. We also assume that\n    // all columns are data columns so that we don't have to deal with partition columns\n    // having to be at the end of the schema, which Hive follows.\n    val catalogName = table.identifier.catalog.getOrElse(\n      spark.sessionState.catalogManager.currentCatalog.name())\n    if (\n      (catalogName == UpdateCatalog.HIVE_METASTORE_NAME\n        || catalogName == SESSION_CATALOG_NAME) &&\n      catalog.externalCatalog.tableExists(db, tblName)) {\n      catalog.externalCatalog.alterTableDataSchema(db, tblName, schema)\n    }\n\n    // We have to update the properties anyway with the latest version/timestamp information\n    catalog.alterTable(table.copy(properties = updatedProperties(snapshot) ++ additionalProperties))\n  }\n\n  /** Updates our properties map with the version and timestamp information of the snapshot. */\n  def updatedProperties(snapshot: Snapshot): Map[String, String] = {\n    var newProperties =\n      snapshot.getProperties.toMap ++ Map(\n        DeltaConfigs.METASTORE_LAST_UPDATE_VERSION -> snapshot.version.toString,\n        DeltaConfigs.METASTORE_LAST_COMMIT_TIMESTAMP -> snapshot.timestamp.toString)\n    if (ClusteredTableUtils.isSupported(snapshot.protocol)) {\n      val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(snapshot)\n      val properties = ClusterBySpec.toProperties(\n        ClusterBySpec.fromColumnNames(clusteringColumns))\n      properties.foreach { case (key, value) =>\n        newProperties += (key -> value)\n      }\n    }\n    newProperties\n  }\n\n  /**\n   * If the schema contains non-latin encoding characters, the schema can become garbled.\n   * We need to truncate the schema in that case.\n   * Also, if any of the fields is longer than `truncationThreshold`, then the schema will be\n   * truncated to an empty schema to avoid corruption.\n   *\n   * @return a tuple of the truncated schema and a map of error messages if any.\n   *         The error message is only set if the schema is truncated. Truncation\n   *         can happen if the schema is too long or if it contains non-latin characters.\n   */\n  def truncateSchemaIfNecessary(\n      schema: StructType,\n      truncationThreshold: Long): (StructType, Map[String, String]) = {\n    // Encoders are not threadsafe\n    val encoder = latin1.newEncoder()\n    schema.foreach { f =>\n      if (f.dataType.catalogString.length > truncationThreshold) {\n        return (new StructType(), Map(UpdateCatalog.ERROR_KEY -> LONG_SCHEMA_ERROR))\n      }\n      if (!encoder.canEncode(f.name) || !encoder.canEncode(f.dataType.catalogString)) {\n        return (new StructType(), Map(UpdateCatalog.ERROR_KEY -> NON_LATIN_CHARS_ERROR))\n      }\n    }\n    (schema, Map.empty)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/implicits/RichSparkClasses.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.implicits\n\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.QueryPlan\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.{RuleId, UnknownRuleId}\nimport org.apache.spark.sql.catalyst.trees.{AlwaysProcess, TreePatternBits}\nimport org.apache.spark.sql.delta.util.DeltaEncoders\nimport org.apache.spark.sql.types.{ArrayType, MapType, StructField, StructType}\n\ntrait RichSparkClasses {\n\n  /**\n   * This implicit class is used to provide helpful methods used throughout the code that are not\n   * provided by Spark-Catalyst's StructType.\n   */\n  implicit class RichStructType(structType: StructType) {\n\n    /**\n     * Returns a field in this struct and its child structs, case insensitively.\n     *\n     * If includeCollections is true, this will return fields that are nested in maps and arrays.\n     *\n     * @param fieldNames The path to the field, in order from the root. For example, the column\n     *                   nested.a.b.c would be Seq(\"nested\", \"a\", \"b\", \"c\").\n     */\n    def findNestedFieldIgnoreCase(\n        fieldNames: Seq[String],\n        includeCollections: Boolean = false): Option[StructField] = {\n      val fieldOption = fieldNames.headOption.flatMap {\n        fieldName => structType.find(_.name.equalsIgnoreCase(fieldName))\n      }\n      fieldOption match {\n        case Some(field) =>\n          (fieldNames.tail, field.dataType, includeCollections) match {\n            case (Seq(), _, _) =>\n              Some(field)\n\n            case (names, struct: StructType, _) =>\n              struct.findNestedFieldIgnoreCase(names, includeCollections)\n\n            case (_, _, false) =>\n              None // types nested in maps and arrays are not used\n\n            case (Seq(\"key\"), MapType(keyType, _, _), true) =>\n              // return the key type as a struct field to include nullability\n              Some(StructField(\"key\", keyType, nullable = false))\n\n            case (Seq(\"key\", names @ _*), MapType(struct: StructType, _, _), true) =>\n              struct.findNestedFieldIgnoreCase(names, includeCollections)\n\n            case (Seq(\"value\"), MapType(_, valueType, isNullable), true) =>\n              // return the value type as a struct field to include nullability\n              Some(StructField(\"value\", valueType, nullable = isNullable))\n\n            case (Seq(\"value\", names @ _*), MapType(_, struct: StructType, _), true) =>\n              struct.findNestedFieldIgnoreCase(names, includeCollections)\n\n            case (Seq(\"element\"), ArrayType(elementType, isNullable), true) =>\n              // return the element type as a struct field to include nullability\n              Some(StructField(\"element\", elementType, nullable = isNullable))\n\n            case (Seq(\"element\", names @ _*), ArrayType(struct: StructType, _), true) =>\n              struct.findNestedFieldIgnoreCase(names, includeCollections)\n\n            case _ =>\n              None\n          }\n        case _ =>\n          None\n      }\n    }\n  }\n\n  /**\n   * This implicit class is used to provide helpful methods used throughout the code that are not\n   * provided by Spark-Catalyst's LogicalPlan.\n   */\n  implicit class RichLogicalPlan(plan: LogicalPlan) {\n    /**\n     * Returns the result of running QueryPlan.transformExpressionsUpWithPruning on this node\n     * and all its children.\n     */\n    def transformAllExpressionsUpWithPruning(\n        cond: TreePatternBits => Boolean,\n        ruleId: RuleId = UnknownRuleId)(\n        rule: PartialFunction[Expression, Expression]\n      ): LogicalPlan = {\n      plan.transformUpWithPruning(cond, ruleId) {\n        case q: QueryPlan[_] =>\n          q.transformExpressionsUpWithPruning(cond, ruleId)(rule)\n      }\n    }\n\n    /**\n     * Returns the result of running QueryPlan.transformExpressionsUp on this node\n     * and all its children.\n     */\n    def transformAllExpressionsUp(\n        rule: PartialFunction[Expression, Expression]): LogicalPlan = {\n      transformAllExpressionsUpWithPruning(AlwaysProcess.fn, UnknownRuleId)(rule)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/implicits/package.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.util.DeltaEncoders\n\nimport org.apache.spark.sql.{DataFrame, Dataset, SparkSession}\n\npackage object implicits extends DeltaEncoders with RichSparkClasses {\n  // Define a few implicit classes to provide the `toDF` method. These classes are not using generic\n  // types to avoid touching Scala reflection.\n  implicit class RichAddFileSeq(files: Seq[AddFile]) {\n    def toDF(spark: SparkSession): DataFrame = spark.implicits.localSeqToDatasetHolder(files).toDF()\n\n    def toDS(spark: SparkSession): Dataset[AddFile] =\n      spark.implicits.localSeqToDatasetHolder(files).toDS()\n  }\n\n  implicit class RichStringSeq(strings: Seq[String]) {\n    def toDF(spark: SparkSession): DataFrame =\n      spark.implicits.localSeqToDatasetHolder(strings).toDF()\n\n    def toDF(spark: SparkSession, colNames: String*): DataFrame =\n      spark.implicits.localSeqToDatasetHolder(strings).toDF(colNames: _*)\n  }\n\n  implicit class RichIntSeq(ints: Seq[Int]) {\n    def toDF(spark: SparkSession): DataFrame = spark.implicits.localSeqToDatasetHolder(ints).toDF()\n\n    def toDF(spark: SparkSession, colNames: String*): DataFrame =\n      spark.implicits.localSeqToDatasetHolder(ints).toDF(colNames: _*)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/isolationLevels.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n/**\n * Trait that defines the level consistency guarantee is going to be provided by\n * `OptimisticTransaction.commit()`. [[Serializable]] is the most\n * strict level and [[SnapshotIsolation]] is the least strict one.\n *\n * @see [[IsolationLevel.allLevelsInDescOrder]] for all the levels in the descending order\n *       of strictness and [[IsolationLevel.DEFAULT]] for the default table isolation level.\n */\nsealed trait IsolationLevel {\n  override def toString: String = this.getClass.getSimpleName.stripSuffix(\"$\")\n}\n\n/**\n * This isolation level will ensure serializability between all read and write operations.\n * Specifically, for write operations, this mode will ensure that the result of\n * the table will be perfectly consistent with the visible history of operations, that is,\n * as if all the operations were executed sequentially one by one.\n */\ncase object Serializable extends IsolationLevel\n\n/**\n * This isolation level will ensure snapshot isolation consistency guarantee between write\n * operations only. In other words, if only the write operations are considered, then\n * there exists a serializable sequence between them that would produce the same result\n * as seen in the table. However, if both read and write operations are considered, then\n * there may not exist a serializable sequence that would explain all the observed reads.\n *\n * This provides a lower consistency guarantee than [[Serializable]] but a higher\n * availability than that. For example, unlike [[Serializable]], this level allows an UPDATE\n * operation to be committed even if there was a concurrent INSERT operation that has already\n * added data that should have been read by the UPDATE. It will be as if the UPDATE was executed\n * before the INSERT even if the former was committed after the latter. As a side effect,\n * the visible history of operations may not be consistent with the\n * result expected if these operations were executed sequentially one by one.\n */\ncase object WriteSerializable extends IsolationLevel\n\n/**\n * This isolation level will ensure that all reads will see a consistent\n * snapshot of the table and any transactional write will successfully commit only\n * if the values updated by the transaction have not been changed externally since\n * the snapshot was read by the transaction.\n *\n * This provides a lower consistency guarantee than [[WriteSerializable]] but a higher\n * availability than that. For example, unlike [[WriteSerializable]], this level allows two\n * concurrent UPDATE operations reading the same data to be committed successfully as long as\n * they don't modify the same data.\n *\n * Note that for operations that do not modify data in the table, Snapshot isolation is same\n * as Serializablity. Hence such operations can be safely committed with Snapshot isolation level.\n */\ncase object SnapshotIsolation extends IsolationLevel\n\n\nobject IsolationLevel {\n\n  val DEFAULT = WriteSerializable\n\n  /** All possible isolation levels in descending order of guarantees provided */\n  val allLevelsInDescOrder: Seq[IsolationLevel] = Seq(\n    Serializable,\n    WriteSerializable,\n    SnapshotIsolation)\n\n  /** All the valid isolation levels that can be specified as the table isolation level */\n  val validTableIsolationLevels = Set[IsolationLevel](Serializable, WriteSerializable)\n\n  def fromString(s: String): IsolationLevel = {\n    allLevelsInDescOrder.find(_.toString.equalsIgnoreCase(s)).getOrElse {\n      throw DeltaErrors.invalidIsolationLevelException(s)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/logging/DeltaLogKeys.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.logging\n\n// DeltaLogKey is provided by LogKeyShims (see scala-shims/<SparkShortVersion>/LogKeyShims.scala)\n// to handle the difference between Spark versions:\n// - Spark 4.0: LogKey is a Scala trait with a default `name` implementation\n// - Spark 4.1: LogKey is a Java interface requiring explicit `name()` implementation\n\n/**\n * Various keys used for mapped diagnostic contexts(MDC) in logging. All structured logging keys\n * should be defined here for standardization.\n */\ntrait DeltaLogKeysBase {\n  case object APP_ID extends DeltaLogKey\n  case object ATTEMPT extends DeltaLogKey\n  case object BATCH_ID extends DeltaLogKey\n  case object BATCH_SIZE extends DeltaLogKey\n  case object CATALOG extends DeltaLogKey\n  case object CLONE_SOURCE_DESC extends DeltaLogKey\n  case object CONFIG extends DeltaLogKey\n  case object CONFIG_KEY extends DeltaLogKey\n  case object COORDINATOR_CONF extends DeltaLogKey\n  case object COORDINATOR_NAME extends DeltaLogKey\n  case object COUNT extends DeltaLogKey\n  case object DATA_FILTER extends DeltaLogKey\n  case object DATE extends DeltaLogKey\n  case object DELTA_COMMIT_INFO extends DeltaLogKey\n  case object DELTA_METADATA extends DeltaLogKey\n  case object DIR extends DeltaLogKey\n  case object DURATION extends DeltaLogKey\n  case object ERROR_ID extends DeltaLogKey\n  case object END_INDEX extends DeltaLogKey\n  case object END_OFFSET extends DeltaLogKey\n  case object END_VERSION extends DeltaLogKey\n  case object ERROR extends DeltaLogKey\n  case object EXCEPTION extends DeltaLogKey\n  case object EXECUTOR_ID extends DeltaLogKey\n  case object EXPR extends DeltaLogKey\n  case object FILE_INDEX extends DeltaLogKey\n  case object FILE_NAME extends DeltaLogKey\n  case object FILE_STATUS extends DeltaLogKey\n  case object FILE_SYSTEM_SCHEME extends DeltaLogKey\n  case object FILTER extends DeltaLogKey\n  case object FILTER2 extends DeltaLogKey\n  case object HOOK_NAME extends DeltaLogKey\n  case object INVARIANT_CHECK_INFO extends DeltaLogKey\n  case object ISOLATION_LEVEL extends DeltaLogKey\n  case object IS_DRY_RUN extends DeltaLogKey\n  case object IS_INIT_SNAPSHOT extends DeltaLogKey\n  case object IS_PATH_TABLE extends DeltaLogKey\n  case object JOB_ID extends DeltaLogKey\n  case object LOG_SEGMENT extends DeltaLogKey\n  case object MAX_SIZE extends DeltaLogKey\n  case object METADATA_ID extends DeltaLogKey\n  case object METADATA_NEW extends DeltaLogKey\n  case object METADATA_OLD extends DeltaLogKey\n  case object METRICS extends DeltaLogKey\n  case object METRIC_NAME extends DeltaLogKey\n  case object MIN_SIZE extends DeltaLogKey\n  case object NUM_ACTIONS extends DeltaLogKey\n  case object NUM_ACTIONS2 extends DeltaLogKey\n  case object NUM_ATTEMPT extends DeltaLogKey\n  case object NUM_BYTES extends DeltaLogKey\n  case object NUM_DIRS extends DeltaLogKey\n  case object NUM_FILES extends DeltaLogKey\n  case object NUM_FILES2 extends DeltaLogKey\n  case object NUM_PARTITIONS extends DeltaLogKey\n  case object NUM_PREDICATES extends DeltaLogKey\n  case object NUM_RECORDS extends DeltaLogKey\n  case object NUM_RECORDS2 extends DeltaLogKey\n  case object NUM_SKIPPED extends DeltaLogKey\n  case object OFFSET extends DeltaLogKey\n  case object OPERATION extends DeltaLogKey\n  case object OP_NAME extends DeltaLogKey\n  case object PARTITION_FILTER extends DeltaLogKey\n  case object PATH extends DeltaLogKey\n  case object PATH2 extends DeltaLogKey\n  case object PATHS extends DeltaLogKey\n  case object PATHS2 extends DeltaLogKey\n  case object PATHS3 extends DeltaLogKey\n  case object PATHS4 extends DeltaLogKey\n  case object PROTOCOL extends DeltaLogKey\n  case object QUERY_ID extends DeltaLogKey\n  case object SCHEMA extends DeltaLogKey\n  case object SCHEMA_DIFF extends DeltaLogKey\n  case object SNAPSHOT extends DeltaLogKey\n  case object START_INDEX extends DeltaLogKey\n  case object START_VERSION extends DeltaLogKey\n  case object STATS extends DeltaLogKey\n  case object STATUS extends DeltaLogKey\n  case object STATUS_MESSAGE extends DeltaLogKey\n  case object SYSTEM_CLASS_NAME extends DeltaLogKey\n  case object TABLE_FEATURES extends DeltaLogKey\n  case object TABLE_ID extends DeltaLogKey\n  case object TABLE_NAME extends DeltaLogKey\n  case object TBL_PROPERTIES extends DeltaLogKey\n  case object THREAD_NAME extends DeltaLogKey\n  case object TIMESTAMP extends DeltaLogKey\n  case object TIMESTAMP2 extends DeltaLogKey\n  case object TIME_MS extends DeltaLogKey\n  case object TIME_STATS extends DeltaLogKey\n  case object TXN_ID extends DeltaLogKey\n  case object URI extends DeltaLogKey\n  case object VACUUM_STATS extends DeltaLogKey\n  case object VERSION extends DeltaLogKey\n  case object VERSION2 extends DeltaLogKey\n}\n\nobject DeltaLogKeys extends DeltaLogKeysBase\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/metering/DeltaLogging.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.metering\n\nimport scala.concurrent.duration._\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.{DatabricksLogging, OpType, TagDefinition}\nimport com.databricks.spark.util.MetricDefinitions.{EVENT_LOGGING_FAILURE, EVENT_TAHOE}\nimport com.databricks.spark.util.TagDefinitions.{\n  TAG_OP_TYPE,\n  TAG_TAHOE_ID,\n  TAG_TAHOE_PATH\n}\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.util.DeltaProgressReporter\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.internal.{Logging, MDC, MessageWithContext}\n\n/**\n * Convenience wrappers for logging that include delta specific options and\n * avoids the need to predeclare all operations. Metrics in Delta should respect the following\n * conventions:\n *  - Tags should identify the context of the event (which shard, user, table, machine, etc).\n *  - All actions initiated by a user should be wrapped in a recordOperation so we can track usage\n *    latency and failures. If there is a significant (more than a few seconds) subaction like\n *    identifying candidate files, consider nested recordOperation.\n *  - Events should be used to return detailed statistics about usage. Generally these should be\n *    defined with a case class to ease analysis later.\n *  - Events can also be used to record that a particular codepath was hit (i.e. a checkpoint\n *    failure, a conflict, or a specific optimization).\n *  - Both events and operations should be named hierarchically to allow for analysis at different\n *    levels. For example, to look at the latency of all DDL operations we could scan for operations\n *    that match \"delta.ddl.%\".\n *\n *  Underneath these functions use the standard usage log reporting defined in\n *  [[com.databricks.spark.util.DatabricksLogging]].\n */\ntrait DeltaLogging\n  extends DeltaProgressReporter\n  with DatabricksLogging {\n\n  /**\n   * Used to record the occurrence of a single event or report detailed, operation specific\n   * statistics.\n   *\n   * @param path Used to log the path of the delta table when `deltaLog` is null.\n   */\n  protected def recordDeltaEvent(\n      deltaLog: DeltaLog,\n      opType: String,\n      tags: Map[TagDefinition, String] = Map.empty,\n      data: AnyRef = null,\n      path: Option[Path] = None): Unit = recordFrameProfile(\"Delta\", \"recordDeltaEvent\") {\n    try {\n      val json = if (data != null) JsonUtils.toJson(data) else \"\"\n      val tableTags = if (deltaLog != null) {\n        getCommonTags(deltaLog, Try(deltaLog.unsafeVolatileSnapshot.metadata.id).getOrElse(null))\n      } else if (path.isDefined) {\n        Map(TAG_TAHOE_PATH -> path.get.toString)\n      } else {\n        Map.empty[TagDefinition, String]\n      }\n      recordProductEvent(\n        EVENT_TAHOE,\n        Map((TAG_OP_TYPE: TagDefinition) -> opType) ++ tableTags ++ tags,\n        blob = json)\n    } catch {\n      case NonFatal(e) =>\n        recordEvent(\n          EVENT_LOGGING_FAILURE,\n          blob = JsonUtils.toJson(\n            Map(\"exception\" -> e.getMessage,\n              \"opType\" -> opType,\n              \"method\" -> \"recordDeltaEvent\"))\n        )\n    }\n  }\n\n  /**\n   * Used to report the duration as well as the success or failure of an operation on a `tahoePath`.\n   */\n  protected def recordDeltaOperationForTablePath[A](\n      tablePath: String,\n      opType: String,\n      tags: Map[TagDefinition, String] = Map.empty)(\n      thunk: => A): A = {\n    recordDeltaOperationInternal(Map(TAG_TAHOE_PATH -> tablePath), opType, tags)(thunk)\n  }\n\n  /**\n   * Used to report the duration as well as the success or failure of an operation on a `deltaLog`.\n   */\n  protected def recordDeltaOperation[A](\n      deltaLog: DeltaLog,\n      opType: String,\n      tags: Map[TagDefinition, String] = Map.empty)(\n      thunk: => A): A = {\n    val tableTags: Map[TagDefinition, String] = if (deltaLog != null) {\n      getCommonTags(deltaLog, Try(deltaLog.unsafeVolatileSnapshot.metadata.id).getOrElse(null))\n    } else {\n      Map.empty\n    }\n    recordDeltaOperationInternal(tableTags, opType, tags)(thunk)\n  }\n\n  private def recordDeltaOperationInternal[A](\n      tableTags: Map[TagDefinition, String],\n      opType: String,\n      tags: Map[TagDefinition, String])(thunk: => A): A = {\n    recordOperation(\n      new OpType(opType, \"\"),\n      extraTags = tableTags ++ tags) {\n        recordFrameProfile(\"Delta\", opType) {\n            thunk\n        }\n    }\n  }\n\n  /**\n   * Helper method to check invariants in Delta code. Fails when running in tests, records a delta\n   * assertion event and logs a warning otherwise.\n   */\n  protected def deltaAssert(\n      check: => Boolean,\n      name: String,\n      msg: String,\n      deltaLog: DeltaLog = null,\n      data: AnyRef = null,\n      path: Option[Path] = None)\n    : Unit = {\n    if (DeltaUtils.isTesting) {\n      assert(check, msg)\n    } else if (!check) {\n      recordDeltaEvent(\n        deltaLog = deltaLog,\n        opType = s\"delta.assertions.$name\",\n        data = data,\n        path = path\n      )\n      logWarning(msg)\n    }\n  }\n\n  protected def recordFrameProfile[T](group: String, name: String)(thunk: => T): T = {\n    // future work to capture runtime information ...\n    thunk\n  }\n\n  private def withDmqTag[T](thunk: => T): T = {\n    thunk\n  }\n\n  // Extract common tags from the delta log and snapshot.\n  def getCommonTags(deltaLog: DeltaLog, tahoeId: String): Map[TagDefinition, String] = {\n    (\n      Map(\n        TAG_TAHOE_ID -> tahoeId,\n        TAG_TAHOE_PATH -> Try(deltaLog.dataPath.toString).getOrElse(null)\n      )\n    )\n  }\n\n  /*\n   * Returns error data suitable for logging.\n   *\n   * It will recursively look for the error class and sql state in the cause of the exception.\n   */\n  def getErrorData(e: Throwable): Map[String, Any] = {\n    var data = Map[String, Any](\"exceptionMessage\" -> e.getMessage)\n    e condDo {\n      case sparkEx: SparkThrowable\n        if sparkEx.getErrorClass != null && sparkEx.getErrorClass.nonEmpty =>\n        data ++= Map(\n          \"errorClass\" -> sparkEx.getErrorClass,\n          \"sqlState\" -> sparkEx.getSqlState\n        )\n      case NonFatal(e) if e.getCause != null =>\n        data = getErrorData(e.getCause)\n    }\n    data\n  }\n}\n\nobject DeltaLogging {\n\n  // The opType for delta commit stats.\n  final val DELTA_COMMIT_STATS_OPTYPE = \"delta.commit.stats\"\n}\n\n/**\n * A thread-safe token bucket-based throttler implementation with nanosecond accuracy.\n *\n * Each instance must be shared across all scopes it should throttle.\n * For global throttling that means either by extending this class in an `object` or\n * by creating the instance as a field of an `object`.\n *\n * @param bucketSize This corresponds to the largest possible burst without throttling,\n *                   in number of executions.\n * @param tokenRecoveryInterval Time between two tokens being added back to the bucket.\n *                              This is reciprocal of the long-term average unthrottled rate.\n *\n * Example: With a bucket size of 100 and a recovery interval of 1s, we could log up to 100 events\n * in under a second without throttling, but at that point the bucket is exhausted and we only\n * regain the ability to log more events at 1 event per second. If we log less than 1 event/s\n * the bucket will slowly refill until it's back at 100.\n * Either way, we can always log at least 1 event/s.\n */\nclass LogThrottler(\n    val bucketSize: Int = 100,\n    val tokenRecoveryInterval: FiniteDuration = 1.second,\n    val timeSource: NanoTimeTimeSource = SystemNanoTimeSource) extends Logging {\n\n  private var remainingTokens = bucketSize\n  private var nextRecovery: DeadlineWithTimeSource =\n    DeadlineWithTimeSource.now(timeSource) + tokenRecoveryInterval\n  private var numSkipped: Long = 0\n\n  /**\n   * Run `thunk` as long as there are tokens remaining in the bucket,\n   * otherwise skip and remember number of skips.\n   *\n   * The argument to `thunk` is how many previous invocations have been skipped since the last time\n   * an invocation actually ran.\n   *\n   * Note: This method is `synchronized`, so it is concurrency safe.\n   * However, that also means no heavy-lifting should be done as part of this\n   * if the throttler is shared between concurrent threads.\n   * This also means that the synchronized block of the `thunk` that *does* execute will still\n   * hold up concurrent `thunk`s that will actually get rejected once they hold the lock.\n   * This is fine at low concurrency/low recovery rates. But if we need this to be more efficient at\n   * some point, we will need to decouple the check from the `thunk` execution.\n   */\n  def throttled(thunk: Long => Unit): Unit = this.synchronized {\n    tryRecoverTokens()\n    if (remainingTokens > 0) {\n      thunk(numSkipped)\n      numSkipped = 0\n      remainingTokens -= 1\n    } else {\n      numSkipped += 1L\n    }\n  }\n\n  /**\n   * Same as [[throttled]] but turns the number of skipped invocations into a logging message\n   * that can be appended to item being logged in `thunk`.\n   */\n  def throttledWithSkippedLogMessage(thunk: MessageWithContext => Unit): Unit = {\n    this.throttled { numSkipped =>\n      val skippedStr = if (numSkipped != 0L) {\n        log\" [${MDC(DeltaLogKeys.NUM_SKIPPED, numSkipped)} similar messages were skipped.]\"\n      } else {\n        log\"\"\n      }\n      thunk(skippedStr)\n    }\n  }\n\n  /**\n   * Try to recover tokens, if the rate allows.\n   *\n   * Only call from within a `this.synchronized` block!\n   */\n  private def tryRecoverTokens(): Unit = {\n    try {\n      // Doing it one-by-one is a bit inefficient for long periods, but it's easy to avoid jumps\n      // and rounding errors this way. The inefficiency shouldn't matter as long as the bucketSize\n      // isn't huge.\n      while (remainingTokens < bucketSize && nextRecovery.isOverdue()) {\n        remainingTokens += 1\n        nextRecovery += tokenRecoveryInterval\n      }\n      if (remainingTokens == bucketSize &&\n        (DeadlineWithTimeSource.now(timeSource) - nextRecovery) > tokenRecoveryInterval) {\n        // Reset the recovery time, so we don't accumulate infinite recovery while nothing is\n        // going on.\n        nextRecovery = DeadlineWithTimeSource.now(timeSource) + tokenRecoveryInterval\n      }\n    } catch {\n      case _: IllegalArgumentException =>\n        // Adding FiniteDuration throws IllegalArgumentException instead of wrapping on overflow.\n        // Given that this happens every ~300 years, we can afford some non-linearity here,\n        // rather than taking the effort to properly work around that.\n        nextRecovery = DeadlineWithTimeSource(Duration(-Long.MaxValue, NANOSECONDS), timeSource)\n    }\n  }\n}\n\n/**\n * This is essentially the same as Scala's [[Deadline]],\n * just with a custom source of nanoTime so it can actually be tested properly.\n */\ncase class DeadlineWithTimeSource(\n    time: FiniteDuration,\n    timeSource: NanoTimeTimeSource = SystemNanoTimeSource) {\n  // Only implemented the methods LogThrottler actually needs for now.\n\n  /**\n   * Return a deadline advanced (i.e., moved into the future) by the given duration.\n   */\n  def +(other: FiniteDuration): DeadlineWithTimeSource = copy(time = time + other)\n\n  /**\n   * Calculate time difference between this and the other deadline, where the result is directed\n   * (i.e., may be negative).\n   */\n  def -(other: DeadlineWithTimeSource): FiniteDuration = time - other.time\n\n  /**\n   * Determine whether the deadline lies in the past at the point where this method is called.\n   */\n  def isOverdue(): Boolean = (time.toNanos - timeSource.nanoTime()) <= 0\n}\n\nobject DeadlineWithTimeSource {\n  /**\n   * Construct a deadline due exactly at the point where this method is called. Useful for then\n   * advancing it to obtain a future deadline, or for sampling the current time exactly once and\n   * then comparing it to multiple deadlines (using subtraction).\n   */\n  def now(timeSource: NanoTimeTimeSource = SystemNanoTimeSource): DeadlineWithTimeSource =\n    DeadlineWithTimeSource(Duration(timeSource.nanoTime(), NANOSECONDS), timeSource)\n}\n\n/** Generalisation of [[System.nanoTime()]]. */\nprivate[delta] trait NanoTimeTimeSource {\n  def nanoTime(): Long\n}\nprivate[delta] object SystemNanoTimeSource extends NanoTimeTimeSource {\n  override def nanoTime(): Long = System.nanoTime()\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/metering/ScanReport.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.metering\n\nimport org.apache.spark.sql.delta.stats.DataSize\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\n\ncase class ScanReport(\n    tableId: String,\n    path: String,\n    scanType: String,\n    deltaDataSkippingType: String,\n    partitionFilters: Seq[String],\n    dataFilters: Seq[String],\n    partitionLikeDataFilters: Seq[String],\n    rewrittenPartitionLikeDataFilters: Seq[String],\n    unusedFilters: Seq[String],\n    size: Map[String, DataSize],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    metrics: Map[String, Long],\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    versionScanned: Option[Long],\n    annotations: Map[String, Long],\n    usedPartitionColumns: Seq[String],\n    numUsedPartitionColumns: Long,\n    allPartitionColumns: Seq[String],\n    numAllPartitionColumns: Long,\n    // Number of output rows from parent filter node if it is available and has the same\n    // predicates as dataFilters.\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    parentFilterOutputRows: Option[Long])\n\nobject ScanReport {\n  // Several of the ScanReport fields are only relevant for certain types of delta scans.\n  // Provide an alternative constructor for callers that don't need to set those fields.\n  // scalastyle:off argcount\n  def apply(\n      tableId: String,\n      path: String,\n      scanType: String,\n      partitionFilters: Seq[String],\n      partitionLikeDataFilters: Seq[String],\n      rewrittenPartitionLikeDataFilters: Seq[String],\n      dataFilters: Seq[String],\n      unusedFilters: Seq[String],\n      size: Map[String, DataSize],\n      metrics: Map[String, Long],\n      versionScanned: Option[Long],\n      annotations: Map[String, Long],\n      parentFilterOutputRows: Option[Long]\n      ): ScanReport = {\n    // scalastyle:on\n    ScanReport(\n      tableId = tableId,\n      path = path,\n      scanType = scanType,\n      deltaDataSkippingType = \"\",\n      partitionFilters = partitionFilters,\n      dataFilters = dataFilters,\n      partitionLikeDataFilters = partitionLikeDataFilters,\n      rewrittenPartitionLikeDataFilters = rewrittenPartitionLikeDataFilters,\n      unusedFilters = unusedFilters,\n      size = size,\n      metrics = metrics,\n      versionScanned = versionScanned,\n      annotations = annotations,\n      usedPartitionColumns = Nil,\n      numUsedPartitionColumns = 0L,\n      allPartitionColumns = Nil,\n      numAllPartitionColumns = 0L,\n      parentFilterOutputRows = parentFilterOutputRows)\n  }\n\n  // Similar as above, but without parentFilterOutputRows\n  def apply(\n      tableId: String,\n      path: String,\n      scanType: String,\n      partitionFilters: Seq[String],\n      dataFilters: Seq[String],\n      partitionLikeDataFilters: Seq[String],\n      rewrittenPartitionLikeDataFilters: Seq[String],\n      unusedFilters: Seq[String],\n      size: Map[String, DataSize],\n      metrics: Map[String, Long],\n      versionScanned: Option[Long],\n      annotations: Map[String, Long]): ScanReport = {\n    ScanReport(\n      tableId = tableId,\n      path = path,\n      scanType = scanType,\n      partitionFilters = partitionFilters,\n      dataFilters = dataFilters,\n      partitionLikeDataFilters = partitionLikeDataFilters,\n      rewrittenPartitionLikeDataFilters = rewrittenPartitionLikeDataFilters,\n      unusedFilters = unusedFilters,\n      size = size,\n      metrics = metrics,\n      versionScanned = versionScanned,\n      annotations = annotations,\n      parentFilterOutputRows = None)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/metric/IncrementMetric.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.metric\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis.TypeCheckResult\nimport org.apache.spark.sql.catalyst.expressions.{Expression, ExpressionDescription, Literal, Nondeterministic, UnaryExpression}\nimport org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode}\nimport org.apache.spark.sql.catalyst.expressions.codegen.Block._\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.catalyst.util.TypeUtils\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{BooleanType, DataType}\n\n/**\n * IncrementMetric is used to count the number of rows passing through it. It can be used to\n * wrap a child expression to count the number of rows. Its currently only accessible via the Scala\n * DSL.\n *\n * For example, consider the following expression returning a string literal:\n * If(SomeCondition,\n *  IncrementMetric(Literal(\"ValueIfTrue\"), countTrueMetric),\n *  IncrementMetric(Literal(\"ValueIfFalse\"), countFalseMetric))\n *\n * The SQLMetric `countTrueMetric` would be incremented whenever the condition `SomeCondition` is\n * true, and conversely `countFalseMetric` would be incremented whenever the condition is false.\n *\n * The expression does not really compute anything, and merely forwards the value computed by the\n * child expression.\n *\n * It is marked as non deterministic to ensure that it retains strong affinity with the `child`\n * expression, so as to accurately update the `metric`.\n *\n * It takes the following parameters:\n * @param child is the actual expression to call.\n * @param metric is the SQLMetric to increment.\n */\n@ExpressionDescription(\n  usage = \"_FUNC_(expr, metric) - Returns `expr` as is, while incrementing metric.\")\ncase class IncrementMetric(child: Expression, metric: SQLMetric)\n  extends UnaryExpression with Nondeterministic {\n  override def nullable: Boolean = child.nullable\n\n  override def dataType: DataType = child.dataType\n\n  override protected def initializeInternal(partitionIndex: Int): Unit = {}\n\n  override def toString: String = child.toString\n\n  override def prettyName: String = \"increment_metric\"\n\n  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {\n    // codegen for children expressions\n    val eval = child.genCode(ctx)\n    val metricRef = ctx.addReferenceObj(metric.name.getOrElse(\"metric\"), metric)\n    eval.copy(code = code\"\"\"$metricRef.add(1L);\"\"\" + eval.code)\n  }\n\n  override def evalInternal(input: InternalRow): Any = {\n    metric.add(1L)\n    child.eval(input)\n  }\n\n  override protected def withNewChildInternal(newChild: Expression): IncrementMetric =\n    copy(child = newChild)\n}\n\n/**\n * ConditionalIncrementMetric is used to count the number of rows passing through it based on\n * a condition. It can be used to wrap a child expression to count the number of rows only when\n * the condition is true. Its currently only accessible via the Scala DSL.\n *\n * For example, consider the following expression:\n * ConditionalIncrementMetric(Literal(\"SomeValue\"), GreaterThan(col(\"count\"), Literal(10)),\n *   countMetric)\n *\n * The SQLMetric `countMetric` would be incremented whenever the condition is true.\n * Note: Be careful about nullability! The metric will not be incremented if the condition\n *       evaluates to NULL.\n *       When trying to invert a condition, use `condition = false` instead of `not condition`,\n *       if the metrics is supposed to be incremented if condition was NULL.\n *\n * The Expression returns the value computed by the child expression.\n *\n * It is marked as non deterministic to ensure that it retains strong affinity with the `child`\n * expression, and is not optimized out, so as to accurately update the `metric`.\n *\n * It takes the following parameters:\n * @param child is the actual expression to call.\n * @param condition is the Boolean expression that determines whether to increment the metric.\n * @param metric is the SQLMetric to increment.\n */\n@ExpressionDescription(\n  usage = \"_FUNC_(expr, condition, metric) \" +\n    \"- Returns `expr` as is, while incrementing metric when condition is true.\")\ncase class ConditionalIncrementMetric(child: Expression, condition: Expression, metric: SQLMetric)\n  extends Expression with Nondeterministic {\n\n  override def checkInputDataTypes(): TypeCheckResult = {\n    if (condition.dataType != BooleanType) {\n      TypeCheckResult.DataTypeMismatch(\n        errorSubClass = \"UNEXPECTED_INPUT_TYPE\",\n        messageParameters = Map(\n          \"paramIndex\" -> \"second\",\n          \"requiredType\" -> TypeUtils.toSQLType(BooleanType),\n          \"inputSql\" -> TypeUtils.toSQLExpr(condition),\n          \"inputType\" -> TypeUtils.toSQLType(condition.dataType)\n        )\n      )\n    } else {\n      TypeCheckResult.TypeCheckSuccess\n    }\n  }\n\n  override def nullable: Boolean = child.nullable\n\n  override def dataType: DataType = child.dataType\n\n  override protected def initializeInternal(partitionIndex: Int): Unit = {}\n\n  override def toString: String = s\"conditional_increment_metric($child, $condition)\"\n\n  override def prettyName: String = \"conditional_increment_metric\"\n\n  override def children: Seq[Expression] = Seq(child, condition)\n\n  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {\n    val childEval = child.genCode(ctx)\n    val conditionEval = condition.genCode(ctx)\n    val metricRef = ctx.addReferenceObj(metric.name.getOrElse(\"metric\"), metric)\n\n    val incrementCode = code\"\"\"\n      |${conditionEval.code}\n      |if (!${conditionEval.isNull} && ${conditionEval.value}) {\n      |  $metricRef.add(1L);\n      |}\n      |\"\"\".stripMargin\n\n    childEval.copy(code = incrementCode + childEval.code)\n  }\n\n  override def evalInternal(input: InternalRow): Any = {\n    val conditionResult = condition.eval(input)\n    if (conditionResult != null && conditionResult.asInstanceOf[Boolean]) {\n      metric.add(1L)\n    }\n    child.eval(input)\n  }\n\n  override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[Expression]): ConditionalIncrementMetric = {\n    require(newChildren.length == 2, \"ConditionalIncrementMetric requires exactly 2 children\")\n    copy(child = newChildren(0), condition = newChildren(1))\n  }\n}\n\n/**\n * Optimization rule that simplifies ConditionalIncrementMetric expressions with constant\n * conditions.\n */\nobject OptimizeConditionalIncrementMetric extends Rule[LogicalPlan] {\n  private def isEnabled: Boolean =\n    SQLConf.get.getConf(DeltaSQLConf.DELTA_OPTIMIZE_CONDITIONAL_INCREMENT_METRIC_ENABLED)\n\n  override def apply(plan: LogicalPlan): LogicalPlan = if (isEnabled) {\n    plan.transformAllExpressionsWithSubqueries {\n      case ConditionalIncrementMetric(child, Literal(true, BooleanType), metric) =>\n        // Always true condition: convert to regular IncrementMetric\n        IncrementMetric(child, metric)\n\n      case ConditionalIncrementMetric(child, Literal(false, BooleanType), metric) =>\n        // Always false condition: remove metric logic, keep only child\n        child\n\n      case ConditionalIncrementMetric(child, Literal(null, BooleanType), metric) =>\n        // Null condition: remove metric logic, keep only child\n        child\n    }\n  } else {\n    plan\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/optimizablePartitionExpressions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.OptimizablePartitionExpression._\n\nimport org.apache.spark.sql.Column\nimport org.apache.spark.sql.catalyst.dsl.expressions._\nimport org.apache.spark.sql.catalyst.expressions.{Cast, DateFormatClass, DayOfMonth, Expression, Hour, IsNull, Literal, Month, Or, Substring, TruncDate, TruncTimestamp, UnixTimestamp, Year}\nimport org.apache.spark.sql.catalyst.util.quoteIfNeeded\nimport org.apache.spark.sql.types.{DateType, StringType, TimestampType}\n\n/**\n * Defines rules to convert a data filter to a partition filter for a special generation expression\n * of a partition column.\n *\n * Note:\n * - This may be shared cross multiple `SparkSession`s, implementations should not store any\n * state (such as expressions) referring to a specific `SparkSession`.\n * - Partition columns may have different behaviors than data columns. For example, writing an empty\n *   string to a partition column would become `null` (SPARK-24438). We need to pay attention to\n *   these slight behavior differences and make sure applying the auto generated partition filters\n *   would still return the same result as if they were not applied.\n */\nsealed trait OptimizablePartitionExpression {\n  /**\n   * Assume we have a partition column `part`, and a data column `col`. Return a partition filter\n   * based on `part` for a data filter `col < lit`.\n   */\n  def lessThan(lit: Literal): Option[Expression] = None\n\n  /**\n   * Assume we have a partition column `part`, and a data column `col`. Return a partition filter\n   * based on `part` for a data filter `col <= lit`.\n   */\n  def lessThanOrEqual(lit: Literal): Option[Expression] = None\n\n  /**\n   * Assume we have a partition column `part`, and a data column `col`. Return a partition filter\n   * based on `part` for a data filter `col = lit`.\n   */\n  def equalTo(lit: Literal): Option[Expression] = None\n\n  /**\n   * Assume we have a partition column `part`, and a data column `col`. Return a partition filter\n   * based on `part` for a data filter `col > lit`.\n   */\n  def greaterThan(lit: Literal): Option[Expression] = None\n\n  /**\n   * Assume we have a partition column `part`, and a data column `col`. Return a partition filter\n   * based on `part` for a data filter `col >= lit`.\n   */\n  def greaterThanOrEqual(lit: Literal): Option[Expression] = None\n\n  /**\n   * Assume we have a partition column `part`, and a data column `col`. Return a partition filter\n   * based on `part` for a data filter `col IS NULL`.\n   */\n  def isNull(): Option[Expression] = None\n}\n\nobject OptimizablePartitionExpression {\n  /** Provide a convenient method to convert a string to a column expression */\n  implicit class ColumnExpression(val colName: String) extends AnyVal {\n    // This will always be a top level column so quote it if necessary\n    def toPartCol: Expression = Column(quoteIfNeeded(colName)).expr\n  }\n}\n\n/** The rules for the generation expression `CAST(col AS DATE)`. */\ncase class DatePartitionExpr(partitionColumn: String) extends OptimizablePartitionExpression {\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType => Some(partitionColumn.toPartCol <= Cast(lit, DateType))\n      case DateType => Some(partitionColumn.toPartCol <= lit)\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType => Some(partitionColumn.toPartCol === Cast(lit, DateType))\n      case DateType => Some(partitionColumn.toPartCol === lit)\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType => Some(partitionColumn.toPartCol >= Cast(lit, DateType))\n      case DateType => Some(partitionColumn.toPartCol >= lit)\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def isNull(): Option[Expression] = Some(partitionColumn.toPartCol.isNull)\n}\n\n/**\n * The rules for the generation expression `YEAR(col)`.\n *\n * @param yearPart the year partition column name.\n */\ncase class YearPartitionExpr(yearPart: String) extends OptimizablePartitionExpression {\n\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType => Some(yearPart.toPartCol <= Year(lit))\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType => Some(yearPart.toPartCol.expr === Year(lit))\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType => Some(yearPart.toPartCol >= Year(lit))\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def isNull(): Option[Expression] = Some(yearPart.toPartCol.isNull)\n}\n\n/**\n * This is a placeholder to catch `month(col)` so that we can merge [[YearPartitionExpr]] and\n * [[MonthPartitionExpr]]to [[YearMonthDayPartitionExpr]].\n *\n * @param monthPart the month partition column name.\n */\ncase class MonthPartitionExpr(monthPart: String) extends OptimizablePartitionExpression\n\n/**\n * This is a placeholder to catch `day(col)` so that we can merge [[YearPartitionExpr]],\n * [[MonthPartitionExpr]] and [[DayPartitionExpr]] to [[YearMonthDayPartitionExpr]].\n *\n * @param dayPart the day partition column name.\n */\ncase class DayPartitionExpr(dayPart: String) extends OptimizablePartitionExpression\n\n/**\n * This is a placeholder to catch `hour(col)` so that we can merge [[YearPartitionExpr]],\n * [[MonthPartitionExpr]], [[DayPartitionExpr]] and [[HourPartitionExpr]] to\n * [[YearMonthDayHourPartitionExpr]].\n */\ncase class HourPartitionExpr(hourPart: String) extends OptimizablePartitionExpression\n\n/**\n * Optimize the case that two partition columns uses YEAR and MONTH using the same column, such\n * as `YEAR(eventTime)` and `MONTH(eventTime)`.\n *\n * @param yearPart the year partition column name\n * @param monthPart the month partition column name\n */\ncase class YearMonthPartitionExpr(\n    yearPart: String,\n    monthPart: String) extends OptimizablePartitionExpression {\n\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          (yearPart.toPartCol < Year(lit)) ||\n            (yearPart.toPartCol === Year(lit) && monthPart.toPartCol <= Month(lit))\n        )\n      case _ => None\n    }\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit)\n        )\n      case _ => None\n    }\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          (yearPart.toPartCol > Year(lit)) ||\n            (yearPart.toPartCol === Year(lit) && monthPart.toPartCol >= Month(lit))\n        )\n      case _ => None\n    }\n  }\n\n  override def isNull(): Option[Expression] = {\n    // `yearPart` and `monthPart` are derived columns, so they must be `null` when the input column\n    // is `null`.\n    Some(yearPart.toPartCol.isNull && monthPart.toPartCol.isNull)\n  }\n}\n\n/**\n * Optimize the case that three partition columns uses YEAR, MONTH and DAY using the same column,\n * such as `YEAR(eventTime)`, `MONTH(eventTime)` and `DAY(eventTime)`.\n *\n * @param yearPart the year partition column name\n * @param monthPart the month partition column name\n * @param dayPart the day partition column name\n */\ncase class YearMonthDayPartitionExpr(\n    yearPart: String,\n    monthPart: String,\n    dayPart: String) extends OptimizablePartitionExpression {\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          (yearPart.toPartCol < Year(lit)) ||\n            (yearPart.toPartCol === Year(lit) && monthPart.toPartCol < Month(lit)) ||\n              (\n                yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n                  dayPart.toPartCol <= DayOfMonth(lit)\n              )\n        )\n      case _ => None\n    }\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n            dayPart.toPartCol === DayOfMonth(lit))\n      case _ => None\n    }\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          (yearPart.toPartCol > Year(lit)) ||\n            (yearPart.toPartCol === Year(lit) && monthPart.toPartCol > Month(lit)) ||\n            (\n              yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n                dayPart.toPartCol >= DayOfMonth(lit)\n            )\n        )\n      case _ => None\n    }\n  }\n\n  override def isNull(): Option[Expression] = {\n    // `yearPart`, `monthPart` and `dayPart` are derived columns, so they must be `null` when the\n    // input column is `null`.\n    Some(yearPart.toPartCol.isNull && monthPart.toPartCol.isNull && dayPart.toPartCol.isNull)\n  }\n}\n\n/**\n * Optimize the case that four partition columns uses YEAR, MONTH, DAY and HOUR using the same\n * column, such as `YEAR(eventTime)`, `MONTH(eventTime)`, `DAY(eventTime)`, `HOUR(eventTime)`.\n *\n * @param yearPart the year partition column name\n * @param monthPart the month partition column name\n * @param dayPart the day partition column name\n * @param hourPart the hour partition column name\n */\ncase class YearMonthDayHourPartitionExpr(\n    yearPart: String,\n    monthPart: String,\n    dayPart: String,\n    hourPart: String) extends OptimizablePartitionExpression {\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          (yearPart.toPartCol < Year(lit)) ||\n            (yearPart.toPartCol === Year(lit) && monthPart.toPartCol < Month(lit)) ||\n            (\n              yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n                dayPart.toPartCol < DayOfMonth(lit)\n              ) ||\n            (\n              yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n                dayPart.toPartCol === DayOfMonth(lit) && hourPart.toPartCol <= Hour(lit)\n              )\n        )\n      case _ => None\n    }\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n            dayPart.toPartCol === DayOfMonth(lit) && hourPart.toPartCol === Hour(lit))\n      case _ => None\n    }\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case TimestampType =>\n        Some(\n          (yearPart.toPartCol > Year(lit)) ||\n            (yearPart.toPartCol === Year(lit) && monthPart.toPartCol > Month(lit)) ||\n            (\n              yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n                dayPart.toPartCol > DayOfMonth(lit)\n              ) ||\n            (\n              yearPart.toPartCol === Year(lit) && monthPart.toPartCol === Month(lit) &&\n                dayPart.toPartCol === DayOfMonth(lit) && hourPart.toPartCol >= Hour(lit)\n              )\n        )\n      case _ => None\n    }\n  }\n\n  override def isNull(): Option[Expression] = {\n    // `yearPart`, `monthPart`, `dayPart` and `hourPart` are derived columns, so they must be `null`\n    // when the input column is `null`.\n    Some(yearPart.toPartCol.isNull && monthPart.toPartCol.isNull &&\n      dayPart.toPartCol.isNull && hourPart.toPartCol.isNull)\n  }\n}\n\n/**\n * The rules for the generation expression `SUBSTRING(col, pos, len)`. Note:\n * - Writing an empty string to a partition column would become `null` (SPARK-24438) so generated\n *   partition filters always pick up the `null` partition for safety.\n * - When `pos` is 0, we also support optimizations for comparison operators. When `pos` is not 0,\n *   we only support optimizations for EqualTo.\n *\n * @param partitionColumn the partition column name using SUBSTRING in its generation expression.\n * @param substringPos the `pos` parameter of SUBSTRING  in the generation expression.\n * @param substringLen the `len` parameter of SUBSTRING  in the generation expression.\n */\ncase class SubstringPartitionExpr(\n    partitionColumn: String,\n    substringPos: Int,\n    substringLen: Int) extends OptimizablePartitionExpression {\n\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    // Both `pos == 0` and `pos == 1` start from the first char. See UTF8String.substringSQL.\n    if (substringPos == 0 || substringPos == 1) {\n      lit.dataType match {\n        case StringType =>\n          Some(\n            partitionColumn.toPartCol.isNull ||\n              partitionColumn.toPartCol <= Substring(lit, substringPos, substringLen))\n        case _ => None\n      }\n    } else {\n      None\n    }\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    lit.dataType match {\n      case StringType =>\n        Some(\n          partitionColumn.toPartCol.isNull ||\n            partitionColumn.toPartCol === Substring(lit, substringPos, substringLen))\n      case _ => None\n    }\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    // Both `pos == 0` and `pos == 1` start from the first char. See UTF8String.substringSQL.\n    if (substringPos == 0 || substringPos == 1) {\n      lit.dataType match {\n        case StringType =>\n          Some(\n            partitionColumn.toPartCol.isNull ||\n              partitionColumn.toPartCol >= Substring(lit, substringPos, substringLen))\n        case _ => None\n      }\n    } else {\n      None\n    }\n  }\n\n  override def isNull(): Option[Expression] = Some(partitionColumn.toPartCol.isNull)\n}\n\n/**\n * The rules for the generation expression `DATE_FORMAT(col, format)`, such as:\n * DATE_FORMAT(timestamp, 'yyyy-MM'), DATE_FORMAT(timestamp, 'yyyy-MM-dd-HH')\n *\n * @param partitionColumn the partition column name using DATE_FORMAT in its generation expression.\n * @param format the `format` parameter of DATE_FORMAT in the generation expression.\n *\n *            unix_timestamp('12345-12', 'yyyy-MM') | unix_timestamp('+12345-12', 'yyyy-MM')\n * EXCEPTION               fail                     |           327432240000\n * CORRECTED               null                     |           327432240000\n * LEGACY               327432240000                |               null\n */\ncase class DateFormatPartitionExpr(\n    partitionColumn: String, format: String) extends OptimizablePartitionExpression {\n\n  private val partitionColumnUnixTimestamp = UnixTimestamp(partitionColumn.toPartCol, format)\n\n  private def litUnixTimestamp(lit: Literal): UnixTimestamp =\n    UnixTimestamp(DateFormatClass(lit, format), format)\n\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    // timestamp + date are truncated to yyyy-MM\n    // timestamp are truncated to yyyy-MM-dd-HH\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType =>\n        Some(partitionColumnUnixTimestamp <= litUnixTimestamp(lit))\n      case _ => None\n    }\n    // when write and read timeParserPolicy-s are different, UnixTimestamp will yield null\n    // thus e would be null if either of two operands is null, we should not drop the data\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType =>\n        Some(partitionColumnUnixTimestamp === litUnixTimestamp(lit))\n      case _ => None\n    }\n    // when write and read timeParserPolicy-s are different, UnixTimestamp will yield null\n    // thus e would be null if either of two operands is null, we should not drop the data\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    // timestamp + date are truncated to yyyy-MM\n    // timestamp are truncated to yyyy-MM-dd-HH\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType =>\n        Some(partitionColumnUnixTimestamp >= litUnixTimestamp(lit))\n      case _ => None\n    }\n    // when write and read timeParserPolicy-s are different, UnixTimestamp will yield null\n    // thus e would be null if either of two operands is null, we should not drop the data\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def isNull(): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull)\n  }\n}\n\n/** The rules for the generation expression `date_trunc(field, col)`. */\ncase class TimestampTruncPartitionExpr(format: String, partitionColumn: String)\n  extends OptimizablePartitionExpression {\n  override def lessThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \"<\" to \"<=\".\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType => Some(partitionColumn.toPartCol <= TruncTimestamp(format, lit))\n      case DateType => Some(\n        partitionColumn.toPartCol <= TruncTimestamp(format, Cast(lit, TimestampType)))\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType => Some(partitionColumn.toPartCol === TruncTimestamp(format, lit))\n      case DateType => Some(\n        partitionColumn.toPartCol === TruncTimestamp(format, Cast(lit, TimestampType)))\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    // As the partition column has truncated information, we need to turn \">\" to \">=\".\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType => Some(partitionColumn.toPartCol >= TruncTimestamp(format, lit))\n      case DateType => Some(\n        partitionColumn.toPartCol >= TruncTimestamp(format, Cast(lit, TimestampType)))\n      case _ => None\n    }\n    // to avoid any expression which yields null\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def isNull(): Option[Expression] = Some(partitionColumn.toPartCol.isNull)\n}\n\n/**\n * The rules for the generation of identity expressions, used for partitioning on a nested column.\n * Note:\n * - Writing an empty string to a partition column would become `null` (SPARK-24438) so generated\n *   partition filters always pick up the `null` partition for safety.\n *\n * @param partitionColumn the partition column name used in the generation expression.\n */\ncase class IdentityPartitionExpr(partitionColumn: String)\n    extends OptimizablePartitionExpression {\n\n  override def lessThan(lit: Literal): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull || partitionColumn.toPartCol < lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull || partitionColumn.toPartCol <= lit)\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull || partitionColumn.toPartCol === lit)\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull || partitionColumn.toPartCol > lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull || partitionColumn.toPartCol >= lit)\n  }\n\n  override def isNull(): Option[Expression] = Some(partitionColumn.toPartCol.isNull)\n}\n\n/**\n * The rules for generation expression that use the function trunc(col, format) such as\n * trunc(timestamp, 'year'), trunc(date, 'week') and trunc(timestampStr, 'hour')\n * @param partitionColumn partition column using trunc function in the generation expression\n * @param format the format that specifies the unit of truncation applied to the partitionColumn\n */\ncase class TruncDatePartitionExpr(partitionColumn: String, format: String)\n  extends OptimizablePartitionExpression {\n\n  override def lessThan(lit: Literal): Option[Expression] = {\n    lessThanOrEqual(lit)\n  }\n\n  override def lessThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType | StringType =>\n        Some(partitionColumn.toPartCol <= TruncDate(lit, Literal(format)))\n      case _ => None\n    }\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def equalTo(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType | StringType =>\n        Some(partitionColumn.toPartCol === TruncDate(lit, Literal(format)))\n      case _ => None\n    }\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def greaterThan(lit: Literal): Option[Expression] = {\n    greaterThanOrEqual(lit)\n  }\n\n  override def greaterThanOrEqual(lit: Literal): Option[Expression] = {\n    val expr = lit.dataType match {\n      case TimestampType | DateType | StringType =>\n        Some(partitionColumn.toPartCol >= TruncDate(lit, Literal(format)))\n      case _ => None\n    }\n    expr.map(e => Or(e, IsNull(e)))\n  }\n\n  override def isNull(): Option[Expression] = {\n    Some(partitionColumn.toPartCol.isNull)\n  }\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/optimizer/RangePartitionIdRewrite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.optimizer\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.expressions.{PartitionerExpr, RangePartitionId}\n\nimport org.apache.spark.{RangePartitioner, SparkContext}\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{Alias, Ascending, IsNotNull, SortOrder}\nimport org.apache.spark.sql.catalyst.expressions.codegen.LazilyGeneratedOrdering\nimport org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan, Project, UnaryNode}\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.execution.{QueryExecution, SQLExecution}\nimport org.apache.spark.util.MutablePair\n\n\n/**\n * Rewrites all [[RangePartitionId]] into [[PartitionerExpr]] by running sampling jobs\n * on the child RDD in order to determine the range boundaries.\n */\ncase class RangePartitionIdRewrite(session: SparkSession)\n  extends Rule[LogicalPlan] {\n  import RangePartitionIdRewrite._\n\n  private def sampleSizeHint: Int = conf.rangeExchangeSampleSizePerPartition\n\n  def apply(plan: LogicalPlan): LogicalPlan = plan transformUp {\n    case node: UnaryNode => node.transformExpressionsUp {\n      case RangePartitionId(expr, n) =>\n        val aliasedExpr = Alias(expr, \"__RPI_child_col__\")()\n        val exprAttr = aliasedExpr.toAttribute\n\n        val planForSampling = Filter(IsNotNull(exprAttr), Project(Seq(aliasedExpr), node.child))\n        val qeForSampling = new QueryExecution(session, planForSampling)\n\n        val desc = s\"RangePartitionId($expr, $n) sampling\"\n        val jobGroupId = session.sparkContext.getLocalProperty(SparkContext.SPARK_JOB_GROUP_ID)\n        withCallSite(session.sparkContext, desc) {\n          SQLExecution.withNewExecutionId(qeForSampling) {\n            withJobGroup(session.sparkContext, jobGroupId, desc) {\n              // The code below is inspired from ShuffleExchangeExec.prepareShuffleDependency()\n\n              // Internally, RangePartitioner runs a job on the RDD that samples keys to compute\n              // partition bounds. To get accurate samples, we need to copy the mutable keys.\n              val rddForSampling = qeForSampling.toRdd.mapPartitionsInternal { iter =>\n                val mutablePair = new MutablePair[InternalRow, Null]()\n                iter.map(row => mutablePair.update(row.copy(), null))\n              }\n\n              val sortOrder = SortOrder(exprAttr, Ascending)\n              implicit val ordering = new LazilyGeneratedOrdering(Seq(sortOrder), Seq(exprAttr))\n              val partitioner = new RangePartitioner(n, rddForSampling, true, sampleSizeHint)\n\n              PartitionerExpr(expr, partitioner)\n            }\n          }\n        }\n    }\n  }\n}\n\nobject RangePartitionIdRewrite {\n  /**\n   * Executes the equivalent [[SparkContext.setJobGroup()]] call, runs the given `body`,\n   * then restores the original jobGroup.\n   */\n  private def withJobGroup[T](\n      sparkContext: SparkContext,\n      groupId: String,\n      description: String)\n      (body: => T): T = {\n    val oldJobDesc = sparkContext.getLocalProperty(\"spark.job.description\")\n    val oldGroupId = sparkContext.getLocalProperty(\"spark.jobGroup.id\")\n    val oldJobInterrupt = sparkContext.getLocalProperty(\"spark.job.interruptOnCancel\")\n    sparkContext.setJobGroup(groupId, description, interruptOnCancel = true)\n    try body finally {\n      sparkContext.setJobGroup(\n        oldGroupId, oldJobDesc, Option(oldJobInterrupt).map(_.toBoolean).getOrElse(false))\n    }\n  }\n\n  /**\n   * Executes the equivalent setCallSite() call, runs the given `body`,\n   * then restores the original call site.\n   */\n  private def withCallSite[T](sparkContext: SparkContext, shortCallSite: String)(body: => T): T = {\n    val oldCallSiteShortForm = sparkContext.getLocalProperty(\"callSite.short\")\n    val oldCallSiteLongForm = sparkContext.getLocalProperty(\"callSite.long\")\n    sparkContext.setCallSite(shortCallSite)\n    try body finally {\n      sparkContext.setLocalProperty(\"callSite.short\", oldCallSiteShortForm)\n      sparkContext.setLocalProperty(\"callSite.long\", oldCallSiteLongForm)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/perf/DeltaOptimizedWriterExec.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.perf\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.duration.Duration\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.BinPackingUtils\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark._\nimport org.apache.spark.internal.config\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.network.util.ByteUnit\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.shuffle._\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.catalyst.plans.physical.HashPartitioning\nimport org.apache.spark.sql.execution.{ShuffledRowRDD, SparkPlan, UnaryExecNode}\nimport org.apache.spark.sql.execution.exchange.ShuffleExchangeExec\nimport org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics, SQLShuffleReadMetricsReporter, SQLShuffleWriteMetricsReporter}\nimport org.apache.spark.storage._\nimport org.apache.spark.util.ThreadUtils\n\n\n/**\n * An execution node which shuffles data to a target output of `DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS`\n * blocks, hash partitioned on the table partition columns. We group all blocks by their\n * reducer_id's and bin-pack into `DELTA_OPTIMIZE_WRITE_BIN_SIZE` bins. Then we launch a Spark task\n * per bin to write out a single file for each bin.\n *\n * @param child The execution plan\n * @param partitionColumns The partition columns of the table. Used for hash partitioning the write\n * @param deltaLog The DeltaLog for the table. Used for logging only\n */\ncase class DeltaOptimizedWriterExec(\n    child: SparkPlan,\n    partitionColumns: Seq[String],\n    @transient deltaLog: DeltaLog\n  ) extends UnaryExecNode with DeltaLogging {\n\n  override def output: Seq[Attribute] = child.output\n\n  private lazy val writeMetrics =\n    SQLShuffleWriteMetricsReporter.createShuffleWriteMetrics(sparkContext)\n  private lazy val readMetrics =\n    SQLShuffleReadMetricsReporter.createShuffleReadMetrics(sparkContext)\n  override lazy val metrics: Map[String, SQLMetric] = Map(\n    \"dataSize\" -> SQLMetrics.createSizeMetric(sparkContext, \"data size\")\n  ) ++ readMetrics ++ writeMetrics\n\n  private lazy val childNumPartitions = child.execute().getNumPartitions\n\n  private lazy val numPartitions: Int = {\n    val targetShuffleBlocks = getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS)\n    math.min(\n      math.max(targetShuffleBlocks / childNumPartitions, 1),\n      getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_MAX_SHUFFLE_PARTITIONS))\n  }\n\n  @transient private var cachedShuffleRDD: ShuffledRowRDD = _\n\n  @transient private lazy val mapTracker = SparkEnv.get.mapOutputTracker\n\n  /** Creates a ShuffledRowRDD for facilitating the shuffle in the map side. */\n  private def getShuffleRDD: ShuffledRowRDD = {\n    if (cachedShuffleRDD == null) {\n      val resolver = org.apache.spark.sql.catalyst.analysis.caseInsensitiveResolution\n      val saltedPartitioning = HashPartitioning(\n        partitionColumns.map(p => output.find(o => resolver(p, o.name)).getOrElse(\n          throw DeltaErrors.failedFindPartitionColumnInOutputPlan(p))),\n        numPartitions)\n\n      val shuffledRDD =\n        ShuffleExchangeExec(saltedPartitioning, child).execute().asInstanceOf[ShuffledRowRDD]\n\n      cachedShuffleRDD = shuffledRDD\n    }\n    cachedShuffleRDD\n  }\n\n  private def computeBins(): Array[List[(BlockManagerId, ArrayBuffer[(BlockId, Long, Int)])]] = {\n    // Get all shuffle information\n    val shuffleStats = getShuffleStats()\n\n    // Group by blockId instead of block manager\n    val blockInfo = shuffleStats.flatMap { case (bmId, blocks) =>\n      blocks.map { case (blockId, size, index) =>\n        (blockId, (bmId, size, index))\n      }\n    }.toMap\n\n    val maxBinSize =\n      ByteUnit.BYTE.convertFrom(getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_BIN_SIZE), ByteUnit.MiB)\n\n    val bins = shuffleStats.toSeq.flatMap(_._2).groupBy(_._1.asInstanceOf[ShuffleBlockId].reduceId)\n      .flatMap { case (_, blocks) =>\n        BinPackingUtils.binPackBySize[(BlockId, Long, Int), BlockId](\n          blocks,\n          _._2, // size\n          _._1, // blockId\n          maxBinSize)\n      }\n\n    bins\n      .map { bin =>\n        var binSize = 0L\n        val blockLocations =\n          new mutable.HashMap[BlockManagerId, ArrayBuffer[(BlockId, Long, Int)]]()\n        for (blockId <- bin) {\n          val (bmId, size, index) = blockInfo(blockId)\n          binSize += size\n          val blocksAtBM = blockLocations.getOrElseUpdate(\n            bmId, new ArrayBuffer[(BlockId, Long, Int)]())\n          blocksAtBM.append((blockId, size, index))\n        }\n        (binSize, blockLocations.toList)\n      }\n      .toArray\n      .sortBy(_._1)(Ordering[Long].reverse) // submit largest blocks first\n      .map(_._2)\n  }\n\n  /** Performs the shuffle before the write, so that we can bin-pack output data. */\n  private def getShuffleStats(): Array[(BlockManagerId, collection.Seq[(BlockId, Long, Int)])] = {\n    val dep = getShuffleRDD.dependency\n    // Gets the shuffle output stats\n    def getStats() = mapTracker.getMapSizesByExecutorId(\n      dep.shuffleId, 0, Int.MaxValue, 0, numPartitions).toArray\n\n    // Executes the shuffle map stage in case we are missing output stats\n    def awaitShuffleMapStage(): Unit = {\n      assert(dep != null, \"Shuffle dependency should not be null\")\n      // hack to materialize the shuffle files in a fault tolerant way\n      ThreadUtils.awaitResult(sparkContext.submitMapStage(dep), Duration.Inf)\n    }\n\n    try {\n      val res = getStats()\n      if (res.isEmpty) awaitShuffleMapStage()\n      getStats()\n    } catch {\n      case e: FetchFailedException =>\n        logWarning(log\"Failed to fetch shuffle blocks for the optimized writer. Retrying\", e)\n        awaitShuffleMapStage()\n        getStats()\n    }\n  }\n\n  override def doExecute(): RDD[InternalRow] = {\n    // Single partitioned tasks can simply be written\n    if (childNumPartitions <= 1) return child.execute()\n\n    val shuffledRDD = getShuffleRDD\n\n    val partitions = computeBins()\n\n    recordDeltaEvent(deltaLog,\n      \"delta.optimizeWrite.planned\",\n      data = Map(\n        \"originalPartitions\" -> childNumPartitions,\n        \"outputPartitions\" -> partitions.length,\n        \"shufflePartitions\" -> numPartitions,\n        \"numShuffleBlocks\" -> getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS),\n        \"binSize\" -> getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_BIN_SIZE),\n        \"maxShufflePartitions\" ->\n          getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_MAX_SHUFFLE_PARTITIONS)\n      )\n    )\n\n    new DeltaOptimizedWriterRDD(\n      sparkContext,\n      shuffledRDD.dependency,\n      readMetrics,\n      new OptimizedWriterBlocks(partitions))\n  }\n\n  private def getConf[T](entry: ConfigEntry[T]): T = {\n    conf.getConf(entry)\n  }\n\n  override protected def withNewChildInternal(newChild: SparkPlan): DeltaOptimizedWriterExec =\n    copy(child = newChild)\n}\n\n/**\n * A wrapper class to make the blocks non-serializable. If we serialize the blocks and send them to\n * the executors, it may cause memory problems.\n * NOTE!!!: By wrapping the Array in a non-serializable class we enforce that the field needs to\n *          be transient, and gives us extra security against a developer making a mistake.\n */\nclass OptimizedWriterBlocks(\n    val bins: Array[List[(BlockManagerId, ArrayBuffer[(BlockId, Long, Int)])]])\n\n/**\n * A specialized implementation similar to `ShuffledRowRDD`, where a partition reads a prepared\n * set of shuffle blocks.\n */\nprivate class DeltaOptimizedWriterRDD(\n    @transient sparkContext: SparkContext,\n    var dep: ShuffleDependency[Int, _, InternalRow],\n    metrics: Map[String, SQLMetric],\n    @transient blocks: OptimizedWriterBlocks)\n  extends RDD[InternalRow](sparkContext, Seq(dep)) with DeltaLogging {\n\n  override def getPartitions: Array[Partition] = Array.tabulate(blocks.bins.length) { i =>\n    ShuffleBlockRDDPartition(i, blocks.bins(i))\n  }\n\n  override def compute(split: Partition, context: TaskContext): Iterator[InternalRow] = {\n    val tempMetrics = context.taskMetrics().createTempShuffleReadMetrics()\n    val sqlMetricsReporter = new SQLShuffleReadMetricsReporter(tempMetrics, metrics)\n\n    val blocks = if (context.stageAttemptNumber() > 0) {\n      // We lost shuffle blocks, so we need to now get new manager addresses\n      val executorTracker = SparkEnv.get.mapOutputTracker\n      val oldBlockLocations = split.asInstanceOf[ShuffleBlockRDDPartition].blocks\n\n      // assumes we bin-pack by reducerId\n      val reducerId = oldBlockLocations.head._2.head._1.asInstanceOf[ShuffleBlockId].reduceId\n      // Get block addresses\n      val newLocations = executorTracker.getMapSizesByExecutorId(dep.shuffleId, reducerId)\n        .flatMap { case (bmId, newBlocks) =>\n          newBlocks.map { blockInfo =>\n            (blockInfo._3, (bmId, blockInfo))\n          }\n        }.toMap\n\n      val blockLocations = new mutable.HashMap[BlockManagerId, ArrayBuffer[(BlockId, Long, Int)]]()\n      oldBlockLocations.foreach { case (_, oldBlocks) =>\n        oldBlocks.foreach { oldBlock =>\n          val (bmId, blockInfo) = newLocations(oldBlock._3)\n          val blocksAtBM = blockLocations.getOrElseUpdate(bmId,\n            new ArrayBuffer[(BlockId, Long, Int)]())\n          blocksAtBM.append(blockInfo)\n        }\n      }\n\n      blockLocations.iterator\n    } else {\n      split.asInstanceOf[ShuffleBlockRDDPartition].blocks.iterator\n    }\n\n    val reader = new OptimizedWriterShuffleReader(\n      dep,\n      context,\n      blocks,\n      sqlMetricsReporter)\n    reader.read().map(_._2)\n  }\n\n  override def clearDependencies(): Unit = {\n    super.clearDependencies()\n    dep = null\n  }\n}\n\n/** The list of blocks that need to be read by a partition of the ShuffleBlockRDD. */\nprivate case class ShuffleBlockRDDPartition(\n    index: Int,\n    blocks: List[(BlockManagerId, ArrayBuffer[(BlockId, Long, Int)])]) extends Partition\n\n/** A simplified implementation of the `BlockStoreShuffleReader` for reading shuffle blocks. */\nprivate class OptimizedWriterShuffleReader(\n    dep: ShuffleDependency[Int, _, InternalRow],\n    context: TaskContext,\n    blocks: Iterator[(BlockManagerId, ArrayBuffer[(BlockId, Long, Int)])],\n    readMetrics: ShuffleReadMetricsReporter) extends ShuffleReader[Int, InternalRow] {\n\n  /** Read the combined key-values for this reduce task */\n  override def read(): Iterator[Product2[Int, InternalRow]] = {\n    val wrappedStreams = new ShuffleBlockFetcherIterator(\n      context,\n      SparkEnv.get.blockManager.blockStoreClient,\n      SparkEnv.get.blockManager,\n      SparkEnv.get.mapOutputTracker,\n      blocks,\n      SparkEnv.get.serializerManager.wrapStream,\n      // Note: we use getSizeAsMb when no suffix is provided for backwards compatibility\n      SparkEnv.get.conf.getSizeAsMb(\"spark.reducer.maxSizeInFlight\", \"48m\") * 1024 * 1024,\n      SparkEnv.get.conf.getInt(\"spark.reducer.maxReqsInFlight\", Int.MaxValue),\n      SparkEnv.get.conf.get(config.REDUCER_MAX_BLOCKS_IN_FLIGHT_PER_ADDRESS),\n      SparkEnv.get.conf.get(config.MAX_REMOTE_BLOCK_SIZE_FETCH_TO_MEM),\n      SparkEnv.get.conf.get(config.SHUFFLE_MAX_ATTEMPTS_ON_NETTY_OOM),\n      SparkEnv.get.conf.getBoolean(\"spark.shuffle.detectCorrupt\", true),\n      SparkEnv.get.conf.getBoolean(\"spark.shuffle.detectCorrupt.useExtraMemory\", false),\n      SparkEnv.get.conf.getBoolean(\"spark.shuffle.checksum.enabled\", true),\n      SparkEnv.get.conf.get(\"spark.shuffle.checksum.algorithm\", \"ADLER32\"),\n      readMetrics,\n      false)\n\n    val serializerInstance = dep.serializer.newInstance()\n\n    // Create a key/value iterator for each stream\n    val recordIter = wrappedStreams.flatMap { case (_, wrappedStream) =>\n      // Note: the asKeyValueIterator below wraps a key/value iterator inside of a\n      // NextIterator. The NextIterator makes sure that close() is called on the\n      // underlying InputStream when all records have been read.\n      serializerInstance.deserializeStream(wrappedStream).asKeyValueIterator\n    }.asInstanceOf[Iterator[Product2[Int, InternalRow]]]\n\n    new InterruptibleIterator[Product2[Int, InternalRow]](context, recordIter)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/perf/OptimizeMetadataOnlyDeltaQuery.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.perf\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.aggregate._\nimport org.apache.spark.sql.catalyst.planning.PhysicalOperation\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, DateTimeUtils}\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaTable, Snapshot}\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils.isTableDVFree\nimport org.apache.spark.sql.delta.files.TahoeLogFileIndex\nimport org.apache.spark.sql.delta.stats.DeltaScanGenerator\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types._\n\nimport java.sql.Date\nimport java.util.Locale\n\n/** Optimize COUNT, MIN and MAX expressions on Delta tables.\n * This optimization is only applied when the following conditions are met:\n * - The MIN/MAX columns are not nested and data type is supported by the optimization (ByteType,\n * ShortType, IntegerType, LongType, FloatType, DoubleType, DateType).\n * - All AddFiles in the Delta Log must have stats on columns used in MIN/MAX expressions,\n * or the columns must be partitioned, in the latter case it uses partitionValues, a required field.\n * - Table has no deletion vectors, or query has no MIN/MAX expressions.\n * - COUNT has no DISTINCT.\n * - Query has no filters.\n * - Query has no GROUP BY.\n * Example of valid query: SELECT COUNT(*), MIN(id), MAX(partition_col) FROM MyDeltaTable\n */\ntrait OptimizeMetadataOnlyDeltaQuery extends Logging {\n  def optimizeQueryWithMetadata(plan: LogicalPlan): LogicalPlan = {\n    plan.transformUpWithSubqueries {\n      case agg@MetadataOptimizableAggregate(tahoeLogFileIndex) =>\n        createLocalRelationPlan(agg, tahoeLogFileIndex)\n    }\n  }\n\n  protected def getDeltaScanGenerator(index: TahoeLogFileIndex): DeltaScanGenerator\n\n  private def createLocalRelationPlan(\n      plan: Aggregate,\n      tahoeLogFileIndex: TahoeLogFileIndex): LogicalPlan = {\n\n    val aggColumnsNames = Set(extractMinMaxFieldNames(plan).map(_.toLowerCase(Locale.ROOT)) : _*)\n    val (rowCount, columnStats) = extractCountMinMaxFromDeltaLog(tahoeLogFileIndex, aggColumnsNames)\n\n    def checkStatsExists(attrRef: AttributeReference): Boolean = {\n      columnStats.contains(attrRef.name) &&\n        // Avoid StructType, it is not supported by this optimization.\n        // Sanity check only. If reference is nested column it would be GetStructType\n        // instead of AttributeReference.\n        attrRef.references.size == 1 && attrRef.references.head.dataType != StructType\n    }\n\n    def convertValueIfRequired(attrRef: AttributeReference, value: Any): Any = {\n      if (attrRef.dataType == DateType && value != null) {\n        DateTimeUtils.fromJavaDate(value.asInstanceOf[Date])\n      } else {\n        value\n      }\n    }\n\n    val rewrittenAggregationValues = plan.aggregateExpressions.collect {\n      case Alias(AggregateExpression(\n      Count(Seq(Literal(1, _))), Complete, false, None, _), _) if rowCount.isDefined =>\n        rowCount.get\n      case Alias(tps@ToPrettyString(AggregateExpression(\n      Count(Seq(Literal(1, _))), Complete, false, None, _), _), _) if rowCount.isDefined =>\n        tps.copy(child = Literal(rowCount.get)).eval()\n      case Alias(AggregateExpression(\n      Min(minReference: AttributeReference), Complete, false, None, _), _)\n        if checkStatsExists(minReference) =>\n        convertValueIfRequired(minReference, columnStats(minReference.name).min)\n      case Alias(tps@ToPrettyString(AggregateExpression(\n      Min(minReference: AttributeReference), Complete, false, None, _), _), _)\n        if checkStatsExists(minReference) =>\n          val v = columnStats(minReference.name).min\n          tps.copy(child = Literal(v)).eval()\n      case Alias(AggregateExpression(\n      Max(maxReference: AttributeReference), Complete, false, None, _), _)\n        if checkStatsExists(maxReference) =>\n        convertValueIfRequired(maxReference, columnStats(maxReference.name).max)\n      case Alias(tps@ToPrettyString(AggregateExpression(\n      Max(maxReference: AttributeReference), Complete, false, None, _), _), _)\n        if checkStatsExists(maxReference) =>\n          val v = columnStats(maxReference.name).max\n          tps.copy(child = Literal(v)).eval()\n    }\n\n    if (plan.aggregateExpressions.size == rewrittenAggregationValues.size) {\n      val r = LocalRelation(\n        plan.output,\n        Seq(InternalRow.fromSeq(rewrittenAggregationValues)))\n      r\n    } else {\n      logInfo(log\"Query can't be optimized using metadata because stats are missing\")\n      plan\n    }\n  }\n\n  private def extractMinMaxFieldNames(plan: Aggregate): Seq[String] = {\n    plan.aggregateExpressions.collect {\n      case Alias(AggregateExpression(\n        Min(minReference: AttributeReference), _, _, _, _), _) =>\n        minReference.name\n      case Alias(AggregateExpression(\n        Max(maxReference: AttributeReference), _, _, _, _), _) =>\n        maxReference.name\n      case Alias(ToPrettyString(AggregateExpression(\n        Min(minReference: AttributeReference), _, _, _, _), _), _) =>\n        minReference.name\n      case Alias(ToPrettyString(AggregateExpression(\n        Max(maxReference: AttributeReference), _, _, _, _), _), _) =>\n        maxReference.name\n    }\n  }\n\n  /**\n   * Min and max values from Delta Log stats or partitionValues.\n  */\n  case class DeltaColumnStat(min: Any, max: Any)\n\n  private def extractCountMinMaxFromStats(\n      deltaScanGenerator: DeltaScanGenerator,\n      lowerCaseColumnNames: Set[String]): (Option[Long], Map[String, DeltaColumnStat]) = {\n    val snapshot = deltaScanGenerator.snapshotToScan\n\n    // Count - account for deleted rows according to deletion vectors\n    val dvCardinality = coalesce(col(\"deletionVector.cardinality\"), lit(0))\n    val numLogicalRecords = (col(\"stats.numRecords\") - dvCardinality).as(\"numLogicalRecords\")\n\n    val filesWithStatsForScan = deltaScanGenerator.filesWithStatsForScan(Nil)\n    // Validate all the files has stats\n    val filesStatsCount = filesWithStatsForScan.select(\n      sum(numLogicalRecords).as(\"numLogicalRecords\"),\n      count(when(col(\"stats.numRecords\").isNull, 1)).as(\"missingNumRecords\"),\n      count(when(col(\"stats.numRecords\") > 0, 1)).as(\"countNonEmptyFiles\")).head\n\n    // If any numRecords is null, we have incomplete stats;\n    val allRecordsHasStats = filesStatsCount.getAs[Long](\"missingNumRecords\") == 0\n    if (!allRecordsHasStats) {\n      return (None, Map.empty)\n    }\n    // the sum agg is either null (for an empty table) or gives an accurate record count.\n    val numRecords = if (filesStatsCount.isNullAt(0)) 0 else filesStatsCount.getLong(0)\n    lazy val numFiles: Long = filesStatsCount.getAs[Long](\"countNonEmptyFiles\")\n\n    val dataColumns = snapshot.statCollectionPhysicalSchema.filter(col =>\n      lowerCaseColumnNames.contains(col.name.toLowerCase(Locale.ROOT)))\n\n    // DELETE operations creates AddFile records with 0 rows, and no column stats.\n    // We can safely ignore it since there is no data.\n    lazy val files = filesWithStatsForScan.filter(col(\"stats.numRecords\") > 0)\n    lazy val statsMinMaxNullColumns = files.select(col(\"stats.*\"))\n\n    val minColName = \"minValues\"\n    val maxColName = \"maxValues\"\n    val nullColName = \"nullCount\"\n\n    if (dataColumns.isEmpty\n      || dataColumns.size != lowerCaseColumnNames.size\n      || !isTableDVFree(snapshot) // When DV enabled we can't rely on stats values easily\n      || numFiles == 0\n      || !statsMinMaxNullColumns.columns.contains(minColName)\n      || !statsMinMaxNullColumns.columns.contains(maxColName)\n      || !statsMinMaxNullColumns.columns.contains(nullColName)) {\n      return (Some(numRecords), Map.empty)\n    }\n\n    // dataColumns can contain columns without stats if dataSkippingNumIndexedCols\n    // has been increased\n    val columnsWithStats = files.select(\n      col(s\"stats.$minColName.*\"),\n      col(s\"stats.$maxColName.*\"),\n      col(s\"stats.$nullColName.*\"))\n      .columns.groupBy(identity).mapValues(_.size)\n      .filter(x => x._2 == 3) // 3: minValues, maxValues, nullCount\n      .map(x => x._1).toSet\n\n    // Creates a tuple with physical name to avoid recalculating it multiple times\n    val dataColumnsWithStats = dataColumns.map(x => (x, DeltaColumnMapping.getPhysicalName(x)))\n      .filter(x => columnsWithStats.contains(x._2))\n\n    val columnsToQuery = dataColumnsWithStats.flatMap { columnAndPhysicalName =>\n      val dataType = columnAndPhysicalName._1.dataType\n      val physicalName = columnAndPhysicalName._2\n\n      Seq(col(s\"stats.$minColName.`$physicalName`\").cast(dataType).as(s\"min.$physicalName\"),\n        col(s\"stats.$maxColName.`$physicalName`\").cast(dataType).as(s\"max.$physicalName\"),\n        col(s\"stats.$nullColName.`$physicalName`\").as(s\"null_count.$physicalName\"))\n    } ++ Seq(col(s\"stats.numRecords\").as(s\"numRecords\"))\n\n    val minMaxExpr = dataColumnsWithStats.flatMap { columnAndPhysicalName =>\n      val physicalName = columnAndPhysicalName._2\n\n      // To validate if the column has stats we do two validation:\n      // 1-) COUNT(null_count.columnName) should be equals to numFiles,\n      // since null_count is always non-null.\n      // 2-) The number of files with non-null min/max:\n      // a. count(min.columnName)|count(max.columnName) +\n      // the number of files where all rows are NULL:\n      // b. count of (ISNULL(min.columnName) and null_count.columnName == numRecords)\n      // should be equals to numFiles\n      Seq(\n        s\"\"\"case when $numFiles = count(`null_count.$physicalName`)\n            | AND $numFiles = (count(`min.$physicalName`) + sum(case when\n            |  ISNULL(`min.$physicalName`) and `null_count.$physicalName` = numRecords\n            |   then 1 else 0 end))\n            | AND $numFiles = (count(`max.$physicalName`) + sum(case when\n            |  ISNULL(`max.$physicalName`) AND `null_count.$physicalName` = numRecords\n            |   then 1 else 0 end))\n            | then TRUE else FALSE end as `complete_$physicalName`\"\"\".stripMargin,\n        s\"min(`min.$physicalName`) as `min_$physicalName`\",\n        s\"max(`max.$physicalName`) as `max_$physicalName`\")\n    }\n\n    val statsResults = files.select(columnsToQuery: _*).selectExpr(minMaxExpr: _*).head\n\n    (Some(numRecords), dataColumnsWithStats\n      .filter(x => statsResults.getAs[Boolean](s\"complete_${x._2}\"))\n      .map { columnAndPhysicalName =>\n        val column = columnAndPhysicalName._1\n        val physicalName = columnAndPhysicalName._2\n        column.name ->\n          DeltaColumnStat(\n            statsResults.getAs(s\"min_$physicalName\"),\n            statsResults.getAs(s\"max_$physicalName\"))\n      }.toMap)\n  }\n\n  private def extractMinMaxFromPartitionValue(\n      snapshot: Snapshot,\n      lowerCaseColumnNames: Set[String]): Map[String, DeltaColumnStat] = {\n\n    val partitionedColumns = snapshot.metadata.partitionSchema\n      .filter(col => lowerCaseColumnNames.contains(col.name.toLowerCase(Locale.ROOT)))\n      .map(col => (col, DeltaColumnMapping.getPhysicalName(col)))\n\n    if (partitionedColumns.isEmpty) {\n      Map.empty\n    } else {\n      val partitionedColumnsValues = partitionedColumns.map { partitionedColumn =>\n        val physicalName = partitionedColumn._2\n        col(s\"partitionValues.`$physicalName`\")\n          .cast(partitionedColumn._1.dataType).as(physicalName)\n      }\n\n      val partitionedColumnsAgg = partitionedColumns.flatMap { partitionedColumn =>\n        val physicalName = partitionedColumn._2\n\n        Seq(min(s\"`$physicalName`\").as(s\"min_$physicalName\"),\n          max(s\"`$physicalName`\").as(s\"max_$physicalName\"))\n      }\n\n      val partitionedColumnsQuery = snapshot.allFiles\n        .select(partitionedColumnsValues: _*)\n        .agg(partitionedColumnsAgg.head, partitionedColumnsAgg.tail: _*)\n        .head()\n\n      partitionedColumns.map { partitionedColumn =>\n        val physicalName = partitionedColumn._2\n\n        partitionedColumn._1.name ->\n          DeltaColumnStat(\n            partitionedColumnsQuery.getAs(s\"min_$physicalName\"),\n            partitionedColumnsQuery.getAs(s\"max_$physicalName\"))\n      }.toMap\n    }\n  }\n\n  /**\n  * Extract the Count, Min and Max values from Delta Log stats and partitionValues.\n  * The first field is the rows count in the table or `None` if we cannot calculate it from stats\n  * If the column is not partitioned, the values are extracted from stats when it exists.\n  * If the column is partitioned, the values are extracted from partitionValues.\n  */\n  private def extractCountMinMaxFromDeltaLog(\n      tahoeLogFileIndex: TahoeLogFileIndex,\n      lowerCaseColumnNames: Set[String]):\n  (Option[Long], CaseInsensitiveMap[DeltaColumnStat]) = {\n    val deltaScanGen = getDeltaScanGenerator(tahoeLogFileIndex)\n\n    val partitionedValues = extractMinMaxFromPartitionValue(\n      deltaScanGen.snapshotToScan,\n      lowerCaseColumnNames)\n\n    val partitionedColNames = partitionedValues.keySet.map(_.toLowerCase(Locale.ROOT))\n    val dataColumnNames = lowerCaseColumnNames -- partitionedColNames\n    val (rowCount, columnStats) = extractCountMinMaxFromStats(deltaScanGen, dataColumnNames)\n\n    (rowCount, CaseInsensitiveMap(columnStats ++ partitionedValues))\n  }\n\n  object MetadataOptimizableAggregate {\n\n    /** Only data type that are stored in stats without any loss of precision are supported. */\n    def isSupportedDataType(dataType: DataType): Boolean = {\n      // DecimalType is not supported because not all the values are correctly stored\n      // For example -99999999999999999999999999999999999999 in stats is -1e38\n      (dataType.isInstanceOf[NumericType] && !dataType.isInstanceOf[DecimalType]) ||\n      dataType.isInstanceOf[DateType]\n    }\n\n    private def getAggFunctionOptimizable(\n        aggExpr: AggregateExpression): Option[DeclarativeAggregate] = {\n\n      aggExpr match {\n        case AggregateExpression(\n          c@Count(Seq(Literal(1, _))), Complete, false, None, _) =>\n            Some(c)\n        case AggregateExpression(\n          min@Min(minExpr), Complete, false, None, _) if isSupportedDataType(minExpr.dataType) =>\n            Some(min)\n        case AggregateExpression(\n          max@Max(maxExpr), Complete, false, None, _) if isSupportedDataType(maxExpr.dataType) =>\n            Some(max)\n        case _ => None\n      }\n    }\n\n    private def isStatsOptimizable(aggExprs: Seq[Expression]): Boolean = aggExprs.forall {\n      case Alias(aggExpr: AggregateExpression, _) => getAggFunctionOptimizable(aggExpr).isDefined\n      case Alias(ToPrettyString(aggExpr: AggregateExpression, _), _) =>\n        getAggFunctionOptimizable(aggExpr).isDefined\n      case _ => false\n    }\n\n    private def fieldsAreAttributeReference(fields: Seq[NamedExpression]): Boolean = fields.forall {\n      // Fields should be AttributeReference to avoid getting the incorrect column name\n      // from stats when we create the Local Relation, example\n      // SELECT MAX(Column2) FROM (SELECT Column1 AS Column2 FROM TableName)\n      // the AggregateExpression contains a reference to Column2, instead of Column1\n      case _: AttributeReference => true\n      case _ => false\n    }\n\n    def unapply(plan: Aggregate): Option[TahoeLogFileIndex] = {\n      // GROUP BY is not supports. All AggregateExpression must be stats optimizable.\n      if (plan.groupingExpressions.nonEmpty ||\n        plan.aggregateExpressions.isEmpty ||\n        !isStatsOptimizable(plan.aggregateExpressions)) {\n        return None\n      }\n      plan.child match {\n        case PhysicalOperation(fields, Nil, DeltaTable(fileIndex: TahoeLogFileIndex))\n          if fileIndex.partitionFilters.isEmpty && fieldsAreAttributeReference(fields) =>\n          Some(fileIndex)\n        case DeltaTable(fileIndex: TahoeLogFileIndex) if fileIndex.partitionFilters.isEmpty =>\n          // When all columns are selected, there are no Project/PhysicalOperation\n          Some(fileIndex)\n        case _ =>\n          None\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/redirect/TableRedirect.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.redirect\n\nimport java.util.{Locale, UUID}\n\nimport scala.reflect.ClassTag\nimport scala.util.DynamicVariable\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{\n  DeltaConfig,\n  DeltaConfigs,\n  DeltaErrors,\n  DeltaLog,\n  DeltaOperations,\n  RedirectReaderWriterFeature,\n  RedirectWriterOnlyFeature,\n  Snapshot\n}\nimport org.apache.spark.sql.delta.DeltaLog.logPathFor\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.ENABLE_TABLE_REDIRECT_FEATURE\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.catalog.SessionCatalog\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\n\n/**\n * The table redirection feature includes specific states that manage the behavior of Delta clients\n * during various stages of redirection. These states ensure query result consistency and prevent\n * data loss. There are four states:\n *   0. NO-REDIRECT: Indicates that table redirection is not enabled.\n *   1. ENABLE-REDIRECT-IN-PROGRESS: Table redirection is being enabled. Only read-only queries are\n *                                   allowed on the source table, while all write and metadata\n *                                   transactions are aborted.\n *   2. REDIRECT-READY: The redirection setup is complete, and all queries on the source table are\n *                      routed to the destination table.\n *   3. DROP-REDIRECT-IN-PROGRESS: Table redirection is being disabled. Only read-only queries are\n *                                 allowed on the destination table, with all write and metadata\n *                                 transactions aborted.\n * The valid procedures of state transition are:\n *   0. NO-REDIRECT -> ENABLE-REDIRECT-IN-PROGRESS: Begins the table redirection process by\n *                                                  transitioning the table to\n *                                                  'ENABLE-REDIRECT-IN-PROGRESS.' During this setup\n *                                                  phase, all concurrent DML and DDL operations are\n *                                                  temporarily blocked..\n *   1. ENABLE-REDIRECT-IN-PROGRESS -> REDIRECT-READY: Completes the setup for the table redirection\n *                                                     feature. The table starts redirecting all\n *                                                     queries to the destination location.\n *   2. REDIRECT-READY -> DROP-REDIRECT-IN-PROGRESS: Initiates the process of removing table\n *                                                   redirection by setting the table to\n *                                                   'DROP-REDIRECT-IN-PROGRESS.' This ensures that\n *                                                   concurrent DML/DDL operations do not interfere\n *                                                   with the cancellation process.\n *   3. DROP-REDIRECT-IN-PROGRESS -> NO-REDIRECT: Completes the removal of table redirection. As a\n *                                                result, all DML, DDL, and read-only queries are no\n *                                                longer redirected to the previous destination.\n *   4. ENABLE-REDIRECT-IN-PROGRESS -> NO-REDIRECT: This transition involves canceling table\n *                                                  redirection while it is still in the process of\n *                                                  being enabled.\n */\nsealed trait RedirectState {\n  val name: String\n}\n\n/** This state indicates that redirect is not enabled on the table. */\ncase object NoRedirect extends RedirectState {\n  override val name = \"NO-REDIRECT\"\n}\n\n/** This state indicates that the redirect process is still going on. */\ncase object EnableRedirectInProgress extends RedirectState {\n  override val name = \"ENABLE-REDIRECT-IN-PROGRESS\"\n}\n\n/**\n * This state indicates that the redirect process is completed. All types of queries would be\n * redirected to the table specified inside RedirectSpec object.\n */\ncase object RedirectReady extends RedirectState { override val name = \"REDIRECT-READY\" }\n\n/**\n * The table redirection is under withdrawal and the redirection property is going to be removed\n * from the delta table. In this state, the delta client stops redirecting new queries to redirect\n * destination tables, and only accepts read-only queries to access the redirect source table.\n * The on-going redirected write or metadata transactions, which are visiting redirect\n * destinations, can not commit.\n */\ncase object DropRedirectInProgress extends RedirectState {\n  override val name = \"DROP-REDIRECT-IN-PROGRESS\"\n}\n\n/**\n * This is the abstract class of the redirect specification, which stores the information\n * of accessing the redirect destination table.\n */\nabstract class RedirectSpec() {\n  /** Determine whether `dataPath` is the redirect destination location. */\n  def isRedirectDest(catalog: SessionCatalog, config: Configuration, dataPath: String): Boolean\n  /** Determine whether `dataPath` is the redirect source location. */\n  def isRedirectSource(dataPath: String): Boolean\n}\n\n/**\n * The default redirect spec that is used for OSS delta.\n * This is the specification about how to access the redirect destination table.\n * One example of its JSON presentation is:\n *   {\n *     ......\n *     \"spec\": {\n *       \"redirectSrc\": \"s3://<bucket-1>/tables/<table-name-src>\"\n *       \"redirectDest\": \"s3://<bucket-1>/tables/<table-name-dest>\"\n *     }\n *   }\n *\n * @param sourcePath this is the path where stores the redirect source table's location.\n * @param destPath: this is the path where stores the redirect destination table's location.\n */\nclass PathBasedRedirectSpec(\n     val sourcePath: String,\n     val destPath: String\n) extends RedirectSpec {\n  def isRedirectDest(catalog: SessionCatalog, config: Configuration, dataPath: String): Boolean = {\n    destPath == dataPath\n  }\n\n  def isRedirectSource(dataPath: String): Boolean = sourcePath == dataPath\n}\n\nobject PathBasedRedirectSpec {\n  /**\n   * This is the path based redirection. Delta client uses the `tablePath` of PathBasedRedirectSpec\n   * to access the delta log files on the redirect destination location.\n   */\n  final val REDIRECT_TYPE = \"PathBasedRedirect\"\n}\n\n/**\n * The customized JSON deserializer that parses the redirect specification's content into\n * RedirectSpec object. This class is passed to the JSON execution time object mapper.\n */\nclass RedirectSpecDeserializer[T <: RedirectSpec : ClassTag] {\n  def deserialize(specValue: String): T = {\n    val mapper = new ObjectMapper()\n    mapper.registerModule(DefaultScalaModule)\n    val clazz = implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]]\n    mapper.readValue(specValue, clazz)\n  }\n}\n\nobject RedirectSpec {\n  def getDeserializeModule(redirectType: String): RedirectSpecDeserializer[_ <: RedirectSpec] = {\n      new RedirectSpecDeserializer[PathBasedRedirectSpec]()\n  }\n}\n\n/**\n * This class defines the rule of allowing transaction to access redirect source table.\n * @param appName The application name that is allowed to commit transaction defined inside\n *                the `allowedOperations` set. If a rules' appName is empty, then all application\n *                should fulfill its `allowedOperations`.\n * @param allowedOperations The set of operation names that are allowed to commit on the\n *                          redirect source table.\n * The example of usage of NoRedirectRule.\n *   {\n *     \"type\": \"PathBasedRedirect\",\n *     \"state\": \"REDIRECT-READY\",\n *     \"spec\": {\n *       \"tablePath\": \"s3://<bucket-1>/tables/<table-name>\"\n *     },\n *     \"noRedirectRules\": [\n *       {\"allowedOperations\": [\"Write\", \"Delete\", \"Refresh\"] },\n *       {\"appName\": \"maintenance-job\", \"allowedOperations\": [\"Refresh\"] }\n *     ]\n *   }\n */\ncase class NoRedirectRule(\n    @JsonProperty(\"appName\")\n    appName: Option[String],\n    @JsonProperty(\"allowedOperations\")\n    allowedOperations: Set[String]\n)\n\n/**\n * This class stores all values defined inside table redirection property.\n * @param type: The type of redirection.\n * @param state: The current state of the redirection:\n *               ENABLE-REDIRECT-IN-PROGRESS, REDIRECT-READY, DROP-REDIRECT-IN-PROGRESS.\n * @param specValue: The specification of accessing redirect destination table.\n * @param noRedirectRules: The set of rules that applications should fulfill to access\n *                         redirect source table.\n * This class would be serialized into a JSON string during commit. One example of its JSON\n * presentation is:\n * PathBasedRedirect:\n *   {\n *     \"type\": \"PathBasedRedirect\",\n *     \"state\": \"DROP-REDIRECT-IN-PROGRESS\",\n *     \"spec\": {\n *       \"tablePath\": \"s3://<bucket-1>/tables/<table-name>\"\n *     },\n *     \"noRedirectRules\": [\n *       {\"allowedOperations\": [\"Write\", \"Refresh\"] },\n *       {\"appName\": \"maintenance-job\", \"allowedOperations\": [\"Refresh\"] }\n *     ]\n *   }\n */\ncase class TableRedirectConfiguration(\n    `type`: String,\n    state: String,\n    @JsonProperty(\"spec\")\n    specValue: String,\n    @JsonProperty(\"noRedirectRules\")\n    noRedirectRules: Set[NoRedirectRule] = Set.empty) {\n  @JsonIgnore\n  val spec: RedirectSpec = RedirectSpec.getDeserializeModule(`type`).deserialize(specValue)\n\n  @JsonIgnore\n  val redirectState: RedirectState = state match {\n    case EnableRedirectInProgress.name => EnableRedirectInProgress\n    case RedirectReady.name => RedirectReady\n    case DropRedirectInProgress.name => DropRedirectInProgress\n    case _ => throw new IllegalArgumentException(s\"Unrecognizable Table Redirect State: $state\")\n  }\n\n  @JsonIgnore\n  val isInProgressState: Boolean = {\n    redirectState == EnableRedirectInProgress || redirectState == DropRedirectInProgress\n  }\n\n  /** Determines whether the current application fulfills the no-redirect rules. */\n  private def isNoRedirectApp(spark: SparkSession): Boolean = {\n    noRedirectRules.exists { rule =>\n      // If rule.appName is empty, then it applied to \"spark.app.name\"\n      rule.appName.forall(_.equalsIgnoreCase(spark.conf.get(\"spark.app.name\")))\n    }\n  }\n\n  /** Determines whether the current session needs to access the redirect dest location. */\n  def needRedirect(spark: SparkSession, logPath: Path): Boolean = {\n    !isNoRedirectApp(spark) &&\n      redirectState == RedirectReady &&\n      spec.isRedirectSource(logPath.toUri.getPath)\n  }\n\n  /**\n   * Get the redirect destination location from `deltaLog` object.\n   */\n  def getRedirectLocation(deltaLog: DeltaLog, spark: SparkSession): Path = {\n    spec match {\n      case spec: PathBasedRedirectSpec =>\n        val location = new Path(spec.destPath)\n        val fs = location.getFileSystem(deltaLog.newDeltaHadoopConf())\n        fs.makeQualified(logPathFor(location))\n      case other => throw DeltaErrors.unrecognizedRedirectSpec(other)\n    }\n  }\n}\n\n/**\n * This is the main class of the table redirect that interacts with other components.\n */\nclass TableRedirect(val config: DeltaConfig[Option[String]]) {\n  /**\n   * Determine whether the property of table redirect feature is set.\n   */\n  def isFeatureSet(metadata: Metadata): Boolean = config.fromMetaData(metadata).nonEmpty\n\n  /**\n   * Parse the property of table redirect feature to be an in-memory object of\n   * TableRedirectConfiguration.\n   */\n  def getRedirectConfiguration(deltaLogMetadata: Metadata): Option[TableRedirectConfiguration] = {\n    config.fromMetaData(deltaLogMetadata).map { propertyValue =>\n      RedirectFeature.parseRedirectConfiguration(propertyValue)\n    }\n  }\n\n  /**\n   * Generate the key-value pair of the table redirect property. Its key is the table redirect\n   * property name and its name is the JSON string of TableRedirectConfiguration.\n   */\n  def generateRedirectMetadata(\n    redirectType: String,\n    state: RedirectState,\n    redirectSpec: RedirectSpec,\n    noRedirectRules: Set[NoRedirectRule]\n  ): Map[String, String] = {\n    val redirectConfiguration = TableRedirectConfiguration(\n      redirectType,\n      state.name,\n      JsonUtils.toJson(redirectSpec),\n      noRedirectRules\n    )\n    val redirectJson = JsonUtils.toJson(redirectConfiguration)\n    Map(config.key -> redirectJson)\n  }\n\n  /**\n   * Issues a commit to update the table redirect property on the `catalogTableOpt`.\n   * For the commits update the `state`, a validation is applied to ensure the state\n   * transition is valid.\n   * @param deltaLog The deltaLog object of the table to be redirected.\n   * @param catalogTableOpt The CatalogTable object of the table to be redirected.\n   * @param state The new state of redirection.\n   * @param spec The specification of redirection contains all necessary detail of looking up the\n   *             redirect destination table.\n   */\n  def update(\n    deltaLog: DeltaLog,\n    catalogTableOpt: Option[CatalogTable],\n    state: RedirectState,\n    spec: RedirectSpec,\n    noRedirectRules: Set[NoRedirectRule] = Set.empty[NoRedirectRule]\n  ): Unit = {\n    val txn = deltaLog.startTransaction(catalogTableOpt)\n    val deltaMetadata = txn.snapshot.metadata\n    val currentConfigOpt = getRedirectConfiguration(deltaMetadata)\n    val tableIdent = catalogTableOpt.map(_.identifier.quotedString).getOrElse {\n      s\"delta.`${deltaLog.dataPath.toString}`\"\n    }\n    // There should be an existing table redirect configuration.\n    if (currentConfigOpt.isEmpty) {\n      throw DeltaErrors.invalidRedirectStateTransition(tableIdent, NoRedirect, state)\n    }\n\n    val currentConfig = currentConfigOpt.get\n    val redirectState = currentConfig.redirectState\n    RedirectFeature.validateStateTransition(tableIdent, redirectState, state)\n    val properties = generateRedirectMetadata(currentConfig.`type`, state, spec, noRedirectRules)\n    val newConfigs = txn.metadata.configuration ++ properties\n    val newMetadata = txn.metadata.copy(configuration = newConfigs)\n    txn.updateMetadata(newMetadata)\n    txn.commit(Nil, DeltaOperations.SetTableProperties(properties))\n  }\n\n  /**\n   * Issues a commit to add the redirect property with state `EnableRedirectInProgress`\n   * to the `catalogTableOpt`.\n   * @param deltaLog The deltaLog object of the table to be redirected.\n   * @param catalogTableOpt The CatalogTable object of the table to be redirected.\n   * @param redirectType The type of redirection is used as an identifier to deserialize the content\n   *                     of `spec`.\n   * @param spec The specification of redirection contains all necessary detail of looking up the\n   *             redirect destination table.\n   */\n  def add(\n     deltaLog: DeltaLog,\n     catalogTableOpt: Option[CatalogTable],\n     redirectType: String,\n     spec: RedirectSpec,\n     noRedirectRules: Set[NoRedirectRule] = Set.empty[NoRedirectRule]\n  ): Unit = {\n    val txn = deltaLog.startTransaction(catalogTableOpt)\n    val snapshot = txn.snapshot\n    getRedirectConfiguration(snapshot.metadata).foreach { currentConfig =>\n      throw DeltaErrors.invalidRedirectStateTransition(\n        catalogTableOpt.map(_.identifier.quotedString).getOrElse {\n          s\"delta.`${deltaLog.dataPath.toString}`\"\n        },\n        currentConfig.redirectState,\n        EnableRedirectInProgress\n      )\n    }\n    val properties = generateRedirectMetadata(\n      redirectType,\n      EnableRedirectInProgress,\n      spec,\n      noRedirectRules\n    )\n    val newConfigs = txn.metadata.configuration ++ properties\n    val newMetadata = txn.metadata.copy(configuration = newConfigs)\n    txn.updateMetadata(newMetadata)\n    txn.commit(Nil, DeltaOperations.SetTableProperties(properties))\n  }\n\n  /** Issues a commit to remove the redirect property from the `catalogTableOpt`. */\n  def remove(deltaLog: DeltaLog, catalogTableOpt: Option[CatalogTable]): Unit = {\n    val txn = deltaLog.startTransaction(catalogTableOpt)\n    val currentConfigOpt = getRedirectConfiguration(txn.snapshot.metadata)\n    val tableIdent = catalogTableOpt.map(_.identifier.quotedString).getOrElse {\n      s\"delta.`${deltaLog.dataPath.toString}`\"\n    }\n    if (currentConfigOpt.isEmpty) {\n      DeltaErrors.invalidRemoveTableRedirect(tableIdent, NoRedirect)\n    }\n    val redirectState = currentConfigOpt.get.redirectState\n    if (redirectState != DropRedirectInProgress && redirectState != EnableRedirectInProgress) {\n      DeltaErrors.invalidRemoveTableRedirect(tableIdent, redirectState)\n    }\n    val newConfigs = txn.metadata.configuration.filterNot { case (key, _) => key == config.key }\n    txn.updateMetadata(txn.metadata.copy(configuration = newConfigs))\n    txn.commit(Nil, DeltaOperations.UnsetTableProperties(Seq(config.key), ifExists = true))\n  }\n}\n\nobject RedirectReaderWriter extends TableRedirect(config = DeltaConfigs.REDIRECT_READER_WRITER) {\n  /** True if `snapshot` enables redirect-reader-writer feature. */\n  def isFeatureSupported(snapshot: Snapshot): Boolean = {\n    snapshot.protocol.isFeatureSupported(RedirectReaderWriterFeature)\n  }\n\n  /** True if the update property command tries to set/unset redirect-reader-writer feature. */\n  def isUpdateProperty(snapshot: Snapshot, propKeys: Seq[String]): Boolean = {\n    propKeys.contains(DeltaConfigs.REDIRECT_READER_WRITER.key) && isFeatureSupported(snapshot)\n  }\n}\n\nobject RedirectWriterOnly extends TableRedirect(config = DeltaConfigs.REDIRECT_WRITER_ONLY) {\n  /** True if `snapshot` enables redirect-writer-only feature. */\n  def isFeatureSupported(snapshot: Snapshot): Boolean = {\n    snapshot.protocol.isFeatureSupported(RedirectWriterOnlyFeature)\n  }\n\n  /** True if the update property command tries to set/unset redirect-writer-only feature. */\n  def isUpdateProperty(snapshot: Snapshot, propKeys: Seq[String]): Boolean = {\n    propKeys.contains(DeltaConfigs.REDIRECT_WRITER_ONLY.key) && isFeatureSupported(snapshot)\n  }\n}\n\nobject RedirectFeature {\n  /**\n   * Determine whether the redirect-reader-writer or the redirect-writer-only feature is supported.\n   */\n  def isFeatureSupported(snapshot: Snapshot): Boolean = {\n    RedirectReaderWriter.isFeatureSupported(snapshot) ||\n    RedirectWriterOnly.isFeatureSupported(snapshot)\n  }\n\n  private def getRedirectConfigurationFromDeltaLog(\n     spark: SparkSession,\n     deltaLog: DeltaLog,\n     initialCatalogTable: Option[CatalogTable]\n   ): Option[TableRedirectConfiguration] = {\n      val snapshot = deltaLog.update(\n        catalogTableOpt = initialCatalogTable\n      )\n      getRedirectConfiguration(snapshot.getProperties.toMap)\n  }\n\n  /**\n   * This is the main method that redirect `deltaLog` to the destination location.\n   */\n  def getRedirectLocationAndTable(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      redirectConfig: TableRedirectConfiguration\n  ): (Path, Option[CatalogTable]) = {\n    // Try to get the catalogTable object for the redirect destination table.\n    val catalogTableOpt = redirectConfig.spec match {\n      case pathRedirect: PathBasedRedirectSpec =>\n        withUpdateTableRedirectDDL(updateTableRedirectDDL = true) {\n          val analyzer = spark.sessionState.analyzer\n          import analyzer.CatalogAndIdentifier\n          val CatalogAndIdentifier(catalog, ident) = Seq(\"delta\", pathRedirect.destPath)\n          catalog.asTableCatalog.loadTable(ident).asInstanceOf[DeltaTableV2].catalogTable\n        }\n    }\n    // Get the redirect destination location.\n    val redirectLocation = redirectConfig.getRedirectLocation(deltaLog, spark)\n    (redirectLocation, catalogTableOpt)\n  }\n\n  def parseRedirectConfiguration(configString: String): TableRedirectConfiguration = {\n    val mapper = new ObjectMapper()\n    mapper.registerModule(DefaultScalaModule)\n    mapper.readValue(configString, classOf[TableRedirectConfiguration])\n  }\n\n  /**\n   * Get the current `TableRedirectConfiguration` object from the table properties.\n   * Note that the redirect-reader-writer takes precedence over redirect-writer-only.\n   */\n  def getRedirectConfiguration(\n      properties: Map[String, String]): Option[TableRedirectConfiguration] = {\n    properties.get(DeltaConfigs.REDIRECT_READER_WRITER.key)\n      .orElse(properties.get(DeltaConfigs.REDIRECT_WRITER_ONLY.key))\n      .map(parseRedirectConfiguration)\n  }\n\n  /**\n   * Determine whether the operation `op` updates the existing redirect-reader-writer or\n   * redirect-writer-only table property of a table with `snapshot`.\n   */\n  def isUpdateProperty(snapshot: Snapshot, op: DeltaOperations.Operation): Boolean = {\n    op match {\n      case _ @ DeltaOperations.SetTableProperties(properties) =>\n        val propertyKeys = properties.keySet.toSeq\n        RedirectReaderWriter.isUpdateProperty(snapshot, propertyKeys) ||\n          RedirectWriterOnly.isUpdateProperty(snapshot, propertyKeys)\n      case _ @ DeltaOperations.UnsetTableProperties(propertyKeys, _) =>\n        RedirectReaderWriter.isUpdateProperty(snapshot, propertyKeys) ||\n        RedirectWriterOnly.isUpdateProperty(snapshot, propertyKeys)\n      case _ => false\n    }\n  }\n\n  /**\n   * Determine whether the operation `op` is dropping either the redirect-reader-writer or\n   * redirect-writer-only table feature.\n   */\n  def isDropFeature(op: DeltaOperations.Operation): Boolean = op match {\n    case DeltaOperations.DropTableFeature(featureName, _) => isRedirectFeature(featureName)\n    case _ => false\n  }\n\n  def isRedirectFeature(name: String): Boolean = {\n    name.toLowerCase(Locale.ROOT) == RedirectReaderWriterFeature.name.toLowerCase(Locale.ROOT) ||\n    name.toLowerCase(Locale.ROOT) == RedirectWriterOnlyFeature.name.toLowerCase(Locale.ROOT)\n  }\n\n  /**\n   * Get the current `TableRedirectConfiguration` object from the snapshot.\n   * Note that the redirect-reader-writer takes precedence over redirect-writer-only.\n   */\n  def getRedirectConfiguration(snapshot: Snapshot): Option[TableRedirectConfiguration] = {\n    getRedirectConfiguration(snapshot.metadata.configuration)\n  }\n\n  /** Determines whether `configs` contains redirect configuration. */\n  def hasRedirectConfig(configs: Map[String, String]): Boolean =\n    getRedirectConfiguration(configs).isDefined\n\n  /** Determines whether the property `name` is redirect property. */\n  def isRedirectProperty(name: String): Boolean = {\n    name == DeltaConfigs.REDIRECT_READER_WRITER.key || name == DeltaConfigs.REDIRECT_WRITER_ONLY.key\n  }\n\n  // Helper method to validate state transitions\n  def validateStateTransition(\n      identifier: String,\n      currentState: RedirectState,\n      newState: RedirectState\n  ): Unit = {\n    (currentState, newState) match {\n      case (state, RedirectReady) =>\n        if (state == DropRedirectInProgress) {\n          throw DeltaErrors.invalidRedirectStateTransition(identifier, state, newState)\n        }\n      case (state, DropRedirectInProgress) =>\n        if (state != RedirectReady) {\n          throw DeltaErrors.invalidRedirectStateTransition(identifier, state, newState)\n        }\n      case (state, _) =>\n        throw DeltaErrors.invalidRedirectStateTransition(identifier, state, newState)\n    }\n  }\n\n  /** Determine whether the current `deltaLog` needs to skip redirect feature. */\n  def needDeltaLogRedirect(\n    spark: SparkSession,\n    deltaLog: DeltaLog,\n    initialCatalogTable: Option[CatalogTable]\n  ): Option[TableRedirectConfiguration] = {\n    // It can skip redirect, if the table fulfills any of the following conditions:\n    // - redirect feature is not enable,\n    // - current command is an DDL that updates table redirect property, or\n    // - deltaLog doesn't have valid table.\n    val canSkipTableRedirect = !spark.conf.get(ENABLE_TABLE_REDIRECT_FEATURE) ||\n      isUpdateTableRedirectDDL.value ||\n      !deltaLog.tableExists\n    if (canSkipTableRedirect) return None\n\n    val redirectConfigOpt = getRedirectConfigurationFromDeltaLog(\n      spark,\n      deltaLog,\n      initialCatalogTable\n    )\n    val needRedirectToDest = redirectConfigOpt.exists { redirectConfig =>\n      // If the current deltaLog already points to destination, early returns since\n      // no need to redirect deltaLog.\n      redirectConfig.needRedirect(spark, deltaLog.dataPath)\n    }\n    if (needRedirectToDest) redirectConfigOpt else None\n  }\n\n  def validateTableRedirect(\n      snapshot: Snapshot,\n      catalogTable: Option[CatalogTable],\n      configs: Map[String, String]\n  ): Unit = {\n    val identifier = catalogTable\n      .map(_.identifier.quotedString)\n      .getOrElse(s\"delta.`${snapshot.deltaLog.logPath.toString}`\")\n    if (configs.contains(DeltaConfigs.REDIRECT_READER_WRITER.key)) {\n      if (RedirectWriterOnly.isFeatureSet(snapshot.metadata)) {\n        throw DeltaErrors.invalidSetUnSetRedirectCommand(\n          identifier,\n          DeltaConfigs.REDIRECT_READER_WRITER.key,\n          DeltaConfigs.REDIRECT_WRITER_ONLY.key\n        )\n      }\n    } else if (configs.contains(DeltaConfigs.REDIRECT_WRITER_ONLY.key)) {\n      if (RedirectReaderWriter.isFeatureSet(snapshot.metadata)) {\n        throw DeltaErrors.invalidSetUnSetRedirectCommand(\n          identifier,\n          DeltaConfigs.REDIRECT_WRITER_ONLY.key,\n          DeltaConfigs.REDIRECT_READER_WRITER.key\n        )\n      }\n    } else {\n      return\n    }\n    val currentRedirectConfigOpt = getRedirectConfiguration(snapshot)\n    val newRedirectConfigOpt = getRedirectConfiguration(configs)\n    newRedirectConfigOpt.foreach { newRedirectConfig =>\n      val newState = newRedirectConfig.redirectState\n      // Validate state transitions based on current and new states\n      currentRedirectConfigOpt match {\n        case Some(currentRedirectConfig) =>\n          validateStateTransition(identifier, currentRedirectConfig.redirectState, newState)\n        case None if newState == DropRedirectInProgress =>\n          throw DeltaErrors.invalidRedirectStateTransition(\n            identifier, newState, DropRedirectInProgress\n          )\n        case _ => // No action required for valid transitions\n      }\n    }\n  }\n\n  val DELTALOG_PREFIX = \"redirect-delta-log://\"\n  /**\n   * The thread local variable for indicating whether the current session is an\n   * DDL that updates redirect table property.\n   */\n  @SuppressWarnings(\n    Array(\n      \"BadMethodCall-DynamicVariable\",\n      \"\"\"\n        Reason: The redirect feature implementation requires a thread-local variable to control\n        enable/disable states during SET and UNSET operations. This approach is necessary because:\n        - Parameter Passing Limitation: The call stack cannot propagate this state via method\n          parameters, as the feature is triggered through an external open-source API interface\n          that does not expose this configurability.\n        - Concurrency Constraints: A global variable (without thread-local isolation) would allow\n          unintended cross-thread interference, risking undefined behavior in concurrent\n          transactions. We can not use lock because the lock would introduce big critical session\n          and create performance issue.\n        By using thread-local storage, the feature ensures transaction-specific state isolation\n        while maintaining compatibility with the third-party API's design.\"\"\"\n    )\n  )\n  private val isUpdateTableRedirectDDL = new DynamicVariable[Boolean](false)\n\n  /**\n   * Execute `thunk` while `isUpdateTableRedirectDDL` is set to `updateTableRedirectDDL`.\n   */\n  def withUpdateTableRedirectDDL[T](updateTableRedirectDDL: Boolean)(thunk: => T): T = {\n    isUpdateTableRedirectDDL.withValue(updateTableRedirectDDL) { thunk }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/schema/ImplicitMetadataOperation.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{DomainMetadata, Metadata, Protocol}\nimport org.apache.spark.sql.delta.constraints.Constraints\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.AllowAutomaticWideningMode\nimport org.apache.spark.sql.delta.util.PartitionUtils\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.expressions.FileSourceGeneratedMetadataStructField\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.types.{ArrayType, AtomicType, DataType, MapType, StructType}\n\n/**\n * A trait that writers into Delta can extend to update the schema and/or partitioning of the table.\n */\ntrait ImplicitMetadataOperation extends DeltaLogging {\n\n  import ImplicitMetadataOperation._\n\n  protected val canMergeSchema: Boolean\n  protected val canOverwriteSchema: Boolean\n\n  private def normalizePartitionColumns(\n      spark: SparkSession,\n      partitionCols: Seq[String],\n      schema: StructType): Seq[String] = {\n    partitionCols.map { columnName =>\n      val colMatches = schema.filter(s => SchemaUtils.DELTA_COL_RESOLVER(s.name, columnName))\n      if (colMatches.length > 1) {\n        throw DeltaErrors.ambiguousPartitionColumnException(columnName, colMatches)\n      } else if (colMatches.isEmpty) {\n        throw DeltaErrors.partitionColumnNotFoundException(columnName, toAttributes(schema))\n      }\n      colMatches.head.name\n    }\n  }\n\n  /** Remove all file source generated metadata columns from the schema. */\n  private def dropGeneratedMetadataColumns(structType: StructType): StructType = {\n    val fields = structType.filter {\n      case FileSourceGeneratedMetadataStructField(_, _) => false\n      case _ => true\n    }\n    StructType(fields)\n  }\n\n  protected final def updateMetadata(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      schema: StructType,\n      partitionColumns: Seq[String],\n      configuration: Map[String, String],\n      isOverwriteMode: Boolean,\n      rearrangeOnly: Boolean\n      ): Unit = {\n    // To support the new column mapping mode, we drop existing metadata on data schema\n    // so that all the column mapping related properties can be reinitialized in\n    // OptimisticTransaction.updateMetadata\n    var dataSchema =\n      DeltaColumnMapping.dropColumnMappingMetadata(schema.asNullable)\n\n    // File Source generated columns are not added to the stored schema.\n    dataSchema = dropGeneratedMetadataColumns(dataSchema)\n\n    val mergedSchema = mergeSchema(spark, txn, dataSchema, isOverwriteMode, canOverwriteSchema)\n    val normalizedPartitionCols =\n      normalizePartitionColumns(spark, partitionColumns, dataSchema)\n    // Merged schema will contain additional columns at the end\n    def isNewSchema: Boolean = txn.metadata.schema != mergedSchema\n    // We need to make sure that the partitioning order and naming is consistent\n    // if provided. Otherwise we follow existing partitioning\n    def isNewPartitioning: Boolean = normalizedPartitionCols.nonEmpty &&\n      txn.metadata.partitionColumns != normalizedPartitionCols\n    def isPartitioningChanged: Boolean = txn.metadata.partitionColumns != normalizedPartitionCols\n    PartitionUtils.validatePartitionColumn(\n      mergedSchema,\n      normalizedPartitionCols,\n      // Delta is case insensitive regarding internal column naming\n      caseSensitive = false)\n\n    if (!txn.deltaLog.tableExists) {\n      if (dataSchema.isEmpty) {\n        throw DeltaErrors.emptyDataException\n      }\n      recordDeltaEvent(txn.deltaLog, \"delta.ddl.initializeSchema\")\n      // If this is the first write, configure the metadata of the table.\n      if (rearrangeOnly) {\n        throw DeltaErrors.unexpectedDataChangeException(\"Create a Delta table\")\n      }\n      val description = configuration.get(\"comment\").orNull\n      // Filter out the property for clustering columns from Metadata action.\n      val cleanedConfs = ClusteredTableUtils.removeClusteringColumnsProperty(\n        configuration.filterKeys(_ != \"comment\").toMap)\n      txn.updateMetadata(\n        Metadata(\n          description = description,\n          schemaString = dataSchema.json,\n          partitionColumns = normalizedPartitionCols,\n          configuration = cleanedConfs\n          ,\n          createdTime = Some(System.currentTimeMillis())))\n    } else if (isOverwriteMode && canOverwriteSchema && (isNewSchema || isPartitioningChanged\n        )) {\n      // Can define new partitioning in overwrite mode\n      val newMetadata = txn.metadata.copy(\n        schemaString = dataSchema.json,\n        partitionColumns = normalizedPartitionCols\n      )\n      recordDeltaEvent(txn.deltaLog, \"delta.ddl.overwriteSchema\")\n      if (rearrangeOnly) {\n        throw DeltaErrors.unexpectedDataChangeException(\"Overwrite the Delta table schema or \" +\n          \"change the partition schema\")\n      }\n      txn.updateMetadataForTableOverwrite(newMetadata)\n    } else if (isNewSchema && canMergeSchema && !isNewPartitioning\n        ) {\n      logInfo(log\"New merged schema: ${MDC(DeltaLogKeys.SCHEMA, mergedSchema.treeString)}\")\n      recordDeltaEvent(txn.deltaLog, \"delta.ddl.mergeSchema\")\n      if (rearrangeOnly) {\n        throw DeltaErrors.unexpectedDataChangeException(\"Change the Delta table schema\")\n      }\n\n      val schemaWithTypeWideningMetadata = TypeWideningMetadata.addTypeWideningMetadata(\n        txn,\n        schema = mergedSchema,\n        oldSchema = txn.metadata.schema\n      )\n\n      txn.updateMetadata(txn.metadata.copy(schemaString = schemaWithTypeWideningMetadata.json\n      ))\n    } else if (isNewSchema || isNewPartitioning\n        ) {\n      recordDeltaEvent(txn.deltaLog, \"delta.schemaValidation.failure\")\n      val errorBuilder = new MetadataMismatchErrorBuilder\n      if (isNewSchema) {\n        errorBuilder.addSchemaMismatch(txn.metadata.schema, dataSchema, txn.metadata.id)\n      }\n      if (isNewPartitioning) {\n        errorBuilder.addPartitioningMismatch(txn.metadata.partitionColumns, normalizedPartitionCols)\n      }\n      if (isOverwriteMode) {\n        errorBuilder.addOverwriteBit()\n      }\n      errorBuilder.finalizeAndThrow(spark.sessionState.conf)\n    }\n  }\n\n  /**\n   * Returns a sequence of new DomainMetadata if canUpdateMetadata is true and the operation is\n   * either create table or replace the whole table (not replaceWhere operation). This is because\n   * we only update Domain Metadata when creating or replacing table, and replace table for DDL\n   * and DataFrameWriterV2 are already handled in CreateDeltaTableCommand. In that case,\n   * canUpdateMetadata is false, so we don't update again.\n   *\n   * @param txn [[OptimisticTransaction]] being used to create or replace table.\n   * @param canUpdateMetadata true if the metadata is not updated yet.\n   * @param isReplacingTable true if the operation is replace table without replaceWhere option.\n   * @param clusterBySpecOpt optional ClusterBySpec containing user-specified clustering columns.\n   */\n  protected final def getNewDomainMetadata(\n      txn: OptimisticTransaction,\n      canUpdateMetadata: Boolean,\n      isReplacingTable: Boolean,\n      clusterBySpecOpt: Option[ClusterBySpec] = None): Seq[DomainMetadata] = {\n    if (canUpdateMetadata && (!txn.deltaLog.tableExists || isReplacingTable)) {\n      val newDomainMetadata = Seq.empty[DomainMetadata] ++\n        ClusteredTableUtils.getDomainMetadataFromTransaction(clusterBySpecOpt, txn)\n      if (!txn.deltaLog.tableExists) {\n        newDomainMetadata\n      } else {\n        // Handle domain metadata for replacing a table.\n        DomainMetadataUtils.handleDomainMetadataForReplaceTable(\n          txn.snapshot.domainMetadata, newDomainMetadata)\n      }\n    } else {\n      Seq.empty\n    }\n  }\n}\n\nobject ImplicitMetadataOperation {\n\n  /**\n   * Merge schemas based on transaction state and delta options\n   * @param txn Target transaction\n   * @param dataSchema New data schema\n   * @param isOverwriteMode Whether we are overwriting\n   * @param canOverwriteSchema Whether we can overwrite\n   * @return Merged schema\n   */\n  private[delta] def mergeSchema(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      dataSchema: StructType,\n      isOverwriteMode: Boolean,\n      canOverwriteSchema: Boolean): StructType = {\n    if (isOverwriteMode && canOverwriteSchema) {\n      dataSchema\n    } else {\n      checkDependentExpressions(spark, txn.protocol, txn.metadata, dataSchema)\n\n      val typeWideningMode = if (TypeWidening.isEnabled(txn.protocol, txn.metadata)) {\n        TypeWideningMode.TypeEvolution(\n          uniformIcebergCompatibleOnly = UniversalFormat.icebergEnabled(txn.metadata),\n          allowAutomaticWidening = AllowAutomaticWideningMode.fromConf(spark.sessionState.conf))\n      } else {\n        TypeWideningMode.NoTypeWidening\n      }\n\n      SchemaMergingUtils.mergeSchemas(\n        txn.metadata.schema,\n        dataSchema,\n        typeWideningMode = typeWideningMode)\n    }\n  }\n\n  /**\n   * Check whether there are dependant (CHECK) constraints for\n   * the provided `currentDt`; if so, throw an error indicating\n   * the constraint data type mismatch.\n   *\n   * @param spark the spark session used.\n   * @param path the full column path for the current field.\n   * @param metadata the metadata used for checking dependant (CHECK) constraints.\n   * @param currentDt the current data type.\n   * @param updateDt the updated data type.\n   */\n  private def checkDependentConstraints(\n      spark: SparkSession,\n      path: Seq[String],\n      metadata: Metadata,\n      currentDt: DataType,\n      updateDt: DataType): Unit = {\n    val dependentConstraints =\n      Constraints.findDependentConstraints(spark, path, metadata)\n    if (dependentConstraints.nonEmpty) {\n      throw DeltaErrors.constraintDataTypeMismatch(\n        path,\n        currentDt,\n        updateDt,\n        dependentConstraints\n      )\n    }\n  }\n\n  /**\n   * Check whether there are dependant generated columns for\n   * the provided `currentDt`; if so, throw an error indicating\n   * the generated columns data type mismatch.\n   *\n   * @param spark the spark session used.\n   * @param path the full column path for the current field.\n   * @param protocol the protocol used.\n   * @param metadata the metadata used for checking dependant generated columns.\n   * @param currentDt the current data type.\n   * @param updateDt the updated data type.\n   */\n  private def checkDependentGeneratedColumns(\n      spark: SparkSession,\n      path: Seq[String],\n      protocol: Protocol,\n      metadata: Metadata,\n      currentDt: DataType,\n      updateDt: DataType): Unit = {\n    val dependentGeneratedColumns = SchemaUtils.findDependentGeneratedColumns(\n      spark, path, protocol, metadata.schema)\n    if (dependentGeneratedColumns.nonEmpty) {\n      throw DeltaErrors.generatedColumnsDataTypeMismatch(\n        path,\n        currentDt,\n        updateDt,\n        dependentGeneratedColumns\n      )\n    }\n  }\n\n  /**\n   * Check whether the provided field is currently being referenced\n   * by CHECK constraints or generated columns.\n   * Note that we explicitly ignore the check for `StructType` in this\n   * function by only inspecting its inner fields to relax the check;\n   * plus, any `StructType` will be traversed in [[checkDependentExpressions]].\n   *\n   * @param spark the spark session used.\n   * @param path the full column path for the current field.\n   * @param protocol the protocol used.\n   * @param metadata the metadata used for checking constraints and generated columns.\n   * @param currentDt the current data type.\n   * @param updateDt the updated data type.\n   */\n  private def checkConstraintsOrGeneratedColumnsOnStructField(\n      spark: SparkSession,\n      path: Seq[String],\n      protocol: Protocol,\n      metadata: Metadata,\n      currentDt: DataType,\n      updateDt: DataType): Unit = (currentDt, updateDt) match {\n    // we explicitly ignore the check for `StructType` here.\n    case (_: StructType, _: StructType) =>\n    case (current: ArrayType, update: ArrayType) =>\n      checkConstraintsOrGeneratedColumnsOnStructField(\n        spark, path :+ \"element\", protocol, metadata, current.elementType, update.elementType)\n    case (current: MapType, update: MapType) =>\n      checkConstraintsOrGeneratedColumnsOnStructField(\n        spark, path :+ \"key\", protocol, metadata, current.keyType, update.keyType)\n      checkConstraintsOrGeneratedColumnsOnStructField(\n        spark, path :+ \"value\", protocol, metadata, current.valueType, update.valueType)\n    case (_, _) =>\n      if (currentDt != updateDt) {\n        checkDependentConstraints(spark, path, metadata, currentDt, updateDt)\n        checkDependentGeneratedColumns(spark, path, protocol, metadata, currentDt, updateDt)\n      }\n  }\n\n  /**\n   * Finds all fields that change between the current schema and the new data schema and fail if any\n   * of them are referenced by check constraints or generated columns.\n   */\n  private def checkDependentExpressions(\n      sparkSession: SparkSession,\n      protocol: Protocol,\n      metadata: actions.Metadata,\n      dataSchema: StructType): Unit =\n    SchemaMergingUtils.transformColumns(metadata.schema, dataSchema) {\n      case (fieldPath, currentField, Some(updateField), _)\n        if !SchemaMergingUtils.equalsIgnoreCaseAndCompatibleNullability(\n          currentField.dataType,\n          updateField.dataType\n        ) =>\n        checkConstraintsOrGeneratedColumnsOnStructField(\n          spark = sparkSession,\n          path = fieldPath :+ currentField.name,\n          protocol = protocol,\n          metadata = metadata,\n          currentDt = currentField.dataType,\n          updateDt = updateField.dataType\n        )\n        // We don't transform the schema but just perform checks,\n        // the returned field won't be used anyway.\n        updateField\n      case (_, field, _, _) => field\n    }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/schema/InvariantViolationException.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaThrowable, DeltaThrowableHelper}\nimport org.apache.spark.sql.delta.constraints.{CharVarcharConstraint, Constraints}\nimport org.apache.commons.lang3.exception.ExceptionUtils\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\n\n/** Thrown when the given data doesn't match the rules defined on the table. */\ncase class InvariantViolationException(message: String) extends RuntimeException(message)\n\n/**\n * Match a [[SparkException]] and return the root cause Exception if it is a\n * InvariantViolationException.\n */\nobject InnerInvariantViolationException {\n  def unapply(t: Throwable): Option[InvariantViolationException] = t match {\n    case s: SparkException =>\n      Option(ExceptionUtils.getRootCause(s)) match {\n        case Some(i: InvariantViolationException) => Some(i)\n        case _ => None\n      }\n    case _ => None\n  }\n}\n\nobject DeltaInvariantViolationException {\n  def getNotNullInvariantViolationException(colName: String): DeltaInvariantViolationException = {\n    new DeltaInvariantViolationException(\n      errorClass = \"DELTA_NOT_NULL_CONSTRAINT_VIOLATED\",\n      messageParameters = Array(colName)\n    )\n  }\n\n  def apply(constraint: Constraints.NotNull): DeltaInvariantViolationException = {\n    getNotNullInvariantViolationException(UnresolvedAttribute(constraint.column).name)\n  }\n\n  def getCharVarcharLengthInvariantViolationException(\n      exprStr: String,\n      valueStr: String\n  ): DeltaInvariantViolationException = {\n    new DeltaInvariantViolationException(\n      errorClass = \"DELTA_EXCEED_CHAR_VARCHAR_LIMIT\",\n      messageParameters = Array(valueStr, exprStr)\n    )\n  }\n\n  def getConstraintViolationWithValuesException(\n      constraintName: String,\n      sqlStr: String,\n      valueLines: String\n  ): DeltaInvariantViolationException = {\n    new DeltaInvariantViolationException(\n      errorClass = \"DELTA_VIOLATE_CONSTRAINT_WITH_VALUES\",\n      messageParameters = Array(constraintName, sqlStr, valueLines)\n    )\n  }\n\n  /**\n   * Build an exception to report the current row failed a CHECK constraint.\n   *\n   * @param constraint the constraint definition\n   * @param values a map of full column names to their evaluated values in the failed row\n   */\n  def apply(\n      constraint: Constraints.Check,\n      values: Map[String, Any]): DeltaInvariantViolationException = {\n    if (constraint.name == CharVarcharConstraint.INVARIANT_NAME) {\n      return getCharVarcharLengthInvariantViolationException(\n        exprStr = constraint.expression.sql,\n        valueStr = values.head._2.toString)\n    }\n\n    // Sort by the column name to generate consistent error messages in Scala 2.12 and 2.13.\n    val valueLines = values.toSeq.sortBy(_._1).map {\n      case (column, value) =>\n        s\" - $column : $value\"\n    }.mkString(\"\\n\")\n\n    getConstraintViolationWithValuesException(\n      constraint.name,\n      constraint.expression.sql,\n      valueLines\n    )\n  }\n\n  /**\n   * Columns and values in parallel lists as a shim for Java codegen compatibility.\n   */\n  def apply(\n      constraint: Constraints.Check,\n      columns: java.util.List[String],\n      values: java.util.List[Any]): DeltaInvariantViolationException = {\n    apply(constraint, columns.asScala.zip(values.asScala).toMap)\n  }\n}\n\nclass DeltaInvariantViolationException(\n    errorClass: String,\n    messageParameters: Array[String])\n  extends InvariantViolationException(\n    DeltaThrowableHelper.getMessage(errorClass, messageParameters)) with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n\n  override def getMessageParameters: util.Map[String, String] = {\n    DeltaThrowableHelper.getMessageParameters(errorClass, errorSubClass = null, messageParameters)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/schema/SchemaMergingUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, TypeWideningMode}\n\nimport org.apache.spark.sql.catalyst.analysis.{Resolver, TypeCoercion, UnresolvedAttribute}\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.types._\n\n/**\n * Utils to merge table schema with data schema.\n * This is split from SchemaUtils, because finalSchema is introduced into DeltaMergeInto,\n * and resolving the final schema is now part of\n * [[ResolveDeltaMergeInto.resolveReferencesAndSchema]].\n */\nobject SchemaMergingUtils {\n\n  val DELTA_COL_RESOLVER: (String, String) => Boolean =\n    org.apache.spark.sql.catalyst.analysis.caseInsensitiveResolution\n\n  /**\n   * Returns pairs of (full column name path, field) in this schema as a list. For example, a schema\n   * like:\n   *   <field a>          | - a\n   *   <field 1>          | | - 1\n   *   <field 2>          | | - 2\n   *   <field b>          | - b\n   *   <field c>          | - c\n   *   <field `foo.bar`>  | | - `foo.bar`\n   *   <field 3>          |   | - 3\n   *   will return [\n   *     ([a], <field a>), ([a, 1], <field 1>), ([a, 2], <field 2>), ([b], <field b>),\n   *     ([c], <field c>), ([c, foo.bar], <field foo.bar>), ([c, foo.bar, 3], <field 3>)\n   *   ]\n   */\n  def explode(schema: StructType): Seq[(Seq[String], StructField)] = {\n    def recurseIntoComplexTypes(complexType: DataType): Seq[(Seq[String], StructField)] = {\n      complexType match {\n        case s: StructType => explode(s)\n        case a: ArrayType => recurseIntoComplexTypes(a.elementType)\n          .map { case (path, field) => (Seq(\"element\") ++ path, field) }\n        case m: MapType =>\n          recurseIntoComplexTypes(m.keyType)\n            .map { case (path, field) => (Seq(\"key\") ++ path, field) } ++\n          recurseIntoComplexTypes(m.valueType)\n            .map { case (path, field) => (Seq(\"value\") ++ path, field) }\n        case _ => Nil\n      }\n    }\n\n    schema.flatMap {\n      case f @ StructField(name, s: StructType, _, _) =>\n        Seq((Seq(name), f)) ++\n          explode(s).map { case (path, field) => (Seq(name) ++ path, field) }\n      case f @ StructField(name, a: ArrayType, _, _) =>\n        Seq((Seq(name), f)) ++\n          recurseIntoComplexTypes(a).map { case (path, field) => (Seq(name) ++ path, field) }\n      case f @ StructField(name, m: MapType, _, _) =>\n        Seq((Seq(name), f)) ++\n          recurseIntoComplexTypes(m).map { case (path, field) => (Seq(name) ++ path, field) }\n      case f => (Seq(f.name), f) :: Nil\n    }\n  }\n\n  /**\n   * Returns all column names in this schema as a flat list. For example, a schema like:\n   *   | - a\n   *   | | - 1\n   *   | | - 2\n   *   | - b\n   *   | - c\n   *   | | - nest\n   *   |   | - 3\n   *   will get flattened to: \"a\", \"a.1\", \"a.2\", \"b\", \"c\", \"c.nest\", \"c.nest.3\"\n   */\n  def explodeNestedFieldNames(schema: StructType): Seq[String] = {\n    explode(schema).map { case (path, _) => path }.map(UnresolvedAttribute.apply(_).name)\n  }\n\n  /**\n   * Checks if input column names have duplicate identifiers. This throws an exception if\n   * the duplication exists.\n   *\n   * @param schema the schema to check for duplicates\n   * @param colType column type name, used in an exception message\n   * @param caseSensitive Whether we should exception if two columns have casing conflicts. This\n   *                      should default to false for Delta.\n   */\n  def checkColumnNameDuplication(\n      schema: StructType,\n      colType: String,\n      caseSensitive: Boolean = false): Unit = {\n    val columnNames = explodeNestedFieldNames(schema)\n    // scalastyle:off caselocale\n    val names = if (caseSensitive) {\n      columnNames\n    } else {\n      columnNames.map(_.toLowerCase)\n    }\n    // scalastyle:on caselocale\n    if (names.distinct.length != names.length) {\n      val duplicateColumns = names.groupBy(identity).collect {\n        case (x, ys) if ys.length > 1 => s\"$x\"\n      }\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_DUPLICATE_COLUMNS_FOUND\",\n        messageParameters = Array(colType, duplicateColumns.mkString(\", \")))\n    }\n  }\n\n  /**\n   * A variant of [[mergeDataTypes]] with common default values and enforce struct type\n   * as inputs for Delta table operation.\n   *\n   * Check whether we can write to the Delta table, which has `tableSchema`, using a query that has\n   * `dataSchema`. Our rules are that:\n   *   - `dataSchema` may be missing columns or have additional columns\n   *   - We don't trust the nullability in `dataSchema`. Assume fields are nullable.\n   *   - We only allow nested StructType expansions. For all other complex types, we check for\n   *     strict equality\n   *   - `dataSchema` can't have duplicate column names. Columns that only differ by case are also\n   *     not allowed.\n   * The following merging strategy is\n   * applied:\n   *  - The name of the current field is used.\n   *  - The data types are merged by calling this function.\n   *  - We respect the current field's nullability.\n   *  - The metadata is current field's metadata.\n   *\n   * Schema merging occurs in a case insensitive manner. Hence, column names that only differ\n   * by case are not accepted in the `dataSchema`.\n   */\n  def mergeSchemas(\n      tableSchema: StructType,\n      dataSchema: StructType,\n      allowImplicitConversions: Boolean = false,\n      keepExistingType: Boolean = false,\n      typeWideningMode: TypeWideningMode = TypeWideningMode.NoTypeWidening,\n      caseSensitive: Boolean = false): StructType = {\n    checkColumnNameDuplication(dataSchema, \"in the data to save\", caseSensitive)\n    mergeDataTypes(\n      tableSchema,\n      dataSchema,\n      allowImplicitConversions,\n      keepExistingType,\n      typeWideningMode,\n      caseSensitive,\n      allowOverride = false,\n      overrideMetadata = false\n    ).asInstanceOf[StructType]\n  }\n\n  /**\n   * @param current The current data type.\n   * @param update The data type of the new data being written.\n   * @param allowImplicitConversions Whether to allow Spark SQL implicit conversions. By default,\n   *                                 we merge according to Parquet write compatibility - for\n   *                                 example, an integer type data field will throw when merged to a\n   *                                 string type table field, because int and string aren't stored\n   *                                 the same way in Parquet files. With this flag enabled, the\n   *                                 merge will succeed, because once we get to write time Spark SQL\n   *                                 will support implicitly converting the int to a string.\n   * @param keepExistingType Whether to keep existing types instead of trying to merge types.\n   * @param typeWideningMode Identifies the (current, update) type tuples where `current` can be\n   *                        widened to `update`, in which case `update` is used. See\n   *                        [[TypeWideningMode]].\n   * @param caseSensitive Whether we should keep field mapping case-sensitively.\n   *                      This should default to false for Delta, which is case insensitive.\n   * @param allowOverride Whether to let incoming type override the existing type if unmatched.\n   * @param overrideMetadata Whether to let metadata of new fields override the existing\n   *                         metadata of matching fields\n   */\n  def mergeDataTypes(\n      current: DataType,\n      update: DataType,\n      allowImplicitConversions: Boolean,\n      keepExistingType: Boolean,\n      typeWideningMode: TypeWideningMode,\n      caseSensitive: Boolean,\n      allowOverride: Boolean,\n      overrideMetadata: Boolean): DataType = {\n    def merge(current: DataType, update: DataType): DataType = {\n      (current, update) match {\n        case (StructType(currentFields), StructType(updateFields)) =>\n          // Merge existing fields.\n          val updateFieldMap = toFieldMap(updateFields, caseSensitive)\n          val updatedCurrentFields = currentFields.map { currentField =>\n            updateFieldMap.get(currentField.name) match {\n              case Some(updateField) =>\n                try {\n                  val updatedCurrentFieldMetadata =\n                    if (overrideMetadata) updateField.metadata\n                    else currentField.metadata\n                  StructField(\n                    currentField.name,\n                    merge(currentField.dataType, updateField.dataType),\n                    currentField.nullable,\n                    updatedCurrentFieldMetadata)\n                } catch {\n                  case NonFatal(e) =>\n                    throw new DeltaAnalysisException(\n                      errorClass = \"DELTA_FAILED_TO_MERGE_FIELDS\",\n                      messageParameters = Array(currentField.name, updateField.name),\n                      cause = Some(e)\n                    )\n                }\n              case None =>\n                // Retain the old field.\n                currentField\n            }\n          }\n\n          // Identify the newly added fields.\n          val nameToFieldMap = toFieldMap(currentFields, caseSensitive)\n          val newFields = updateFields.filterNot(f => nameToFieldMap.contains(f.name))\n\n          // Create the merged struct, the new fields are appended at the end of the struct.\n          StructType(updatedCurrentFields ++ newFields)\n        case (ArrayType(currentElementType, currentContainsNull),\n        ArrayType(updateElementType, _)) =>\n          ArrayType(\n            merge(currentElementType, updateElementType),\n            currentContainsNull)\n        case (MapType(currentKeyType, currentElementType, currentContainsNull),\n        MapType(updateKeyType, updateElementType, _)) =>\n          MapType(\n            merge(currentKeyType, updateKeyType),\n            merge(currentElementType, updateElementType),\n            currentContainsNull)\n\n        // If type widening is enabled and the type can be widened, it takes precedence over\n        // keepExistingType.\n        case (current: AtomicType, update: AtomicType)\n          if typeWideningMode.getWidenedType(fromType = current, toType = update).isDefined =>\n            typeWideningMode.getWidenedType(fromType = current, toType = update).get\n\n        // Simply keeps the existing type for primitive types\n        case (current, _) if keepExistingType => current\n        case (_, update) if allowOverride => update\n\n        // If implicit conversions are allowed, that means we can use any valid implicit cast to\n        // perform the merge.\n        case (current, update)\n          if allowImplicitConversions && typeForImplicitCast(update, current).isDefined =>\n          typeForImplicitCast(update, current).get\n\n        case (DecimalType.Fixed(leftPrecision, leftScale),\n        DecimalType.Fixed(rightPrecision, rightScale)) =>\n          if ((leftPrecision == rightPrecision) && (leftScale == rightScale)) {\n            current\n          } else if ((leftPrecision != rightPrecision) && (leftScale != rightScale)) {\n            throw new DeltaAnalysisException(\n              errorClass = \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n              messageParameters = Array(\n                s\"precision $leftPrecision and $rightPrecision & scale $leftScale and $rightScale\"))\n          } else if (leftPrecision != rightPrecision) {\n            throw new DeltaAnalysisException(\n              errorClass = \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n              messageParameters = Array(s\"precision $leftPrecision and $rightPrecision\"))\n          } else {\n            throw new DeltaAnalysisException(\n              errorClass = \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n              messageParameters = Array(s\"scale $leftScale and $rightScale\"))\n          }\n        case _ if current == update =>\n          current\n\n        // Parquet physically stores ByteType, ShortType and IntType as IntType, so when a parquet\n        // column is of one of these three types, you can read this column as any of these three\n        // types. Since Parquet doesn't complain, we should also allow upcasting among these\n        // three types when merging schemas.\n        case (ByteType, ShortType) => ShortType\n        case (ByteType, IntegerType) => IntegerType\n\n        case (ShortType, ByteType) => ShortType\n        case (ShortType, IntegerType) => IntegerType\n\n        case (IntegerType, ShortType) => IntegerType\n        case (IntegerType, ByteType) => IntegerType\n\n        case (NullType, _) =>\n          update\n        case (_, NullType) =>\n          current\n        case _ =>\n          throw new DeltaAnalysisException(errorClass = \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n            messageParameters = Array(current.toString, update.toString))\n      }\n    }\n    merge(current, update)\n  }\n\n  /**\n   * Try to cast the source data type to the target type, returning the final type or None if\n   * there's no valid cast.\n   */\n  private def typeForImplicitCast(sourceType: DataType, targetType: DataType): Option[DataType] = {\n    TypeCoercion.implicitCast(Literal.default(sourceType), targetType).map(_.dataType)\n  }\n\n  def toFieldMap(\n      fields: Seq[StructField],\n      caseSensitive: Boolean = false): Map[String, StructField] = {\n    val fieldMap = fields.map(field => field.name -> field).toMap\n    if (caseSensitive) {\n      fieldMap\n    } else {\n      CaseInsensitiveMap(fieldMap)\n    }\n  }\n\n  /**\n   * Transform (nested) columns in a schema.\n   *\n   * @param schema to transform.\n   * @param tf function to apply.\n   * @return the transformed schema.\n   */\n  def transformColumns[T <: DataType](\n      schema: T)(\n      tf: (Seq[String], StructField, Resolver) => StructField): T = {\n    def transform[E <: DataType](path: Seq[String], dt: E): E = {\n      val newDt = dt match {\n        case s: StructType\n          if org.apache.spark.sql.execution.datasources.VariantMetadata.isVariantStruct(s) =>\n          // A variant struct is logically still a variant, so we should not recurse into its\n          // fields like a normal struct.\n          s\n        case StructType(fields) =>\n          StructType(fields.map { field =>\n            val newField = tf(path, field, DELTA_COL_RESOLVER)\n            // maintain the old name as we recurse into the subfields\n            newField.copy(dataType = transform(path :+ field.name, newField.dataType))\n          })\n        case ArrayType(elementType, containsNull) =>\n          ArrayType(transform(path :+ \"element\", elementType), containsNull)\n        case MapType(keyType, valueType, valueContainsNull) =>\n          MapType(\n            transform(path :+ \"key\", keyType),\n            transform(path :+ \"value\", valueType),\n            valueContainsNull)\n        case other => other\n      }\n      newDt.asInstanceOf[E]\n    }\n    transform(Seq.empty, schema)\n  }\n\n  /**\n   * Prune all nested empty structs from the schema. Return None if top level struct is also empty.\n   * @param dataType the data type to prune.\n   */\n  def pruneEmptyStructs(dataType: DataType): Option[DataType] = {\n    dataType match {\n      case StructType(fields) =>\n        val newFields = fields.flatMap { f =>\n          pruneEmptyStructs(f.dataType).map { newType =>\n            StructField(f.name, newType, f.nullable, f.metadata)\n          }\n        }\n        // when there is no fields, i.e., the struct is empty, we will return None to indicate\n        // we don't want to include that field.\n        if (newFields.isEmpty) {\n          None\n        } else {\n          Option(StructType(newFields))\n        }\n      case ArrayType(currentElementType, containsNull) =>\n        // if the array element type is from from_json, we will exclude the array.\n        pruneEmptyStructs(currentElementType).map { newType =>\n          ArrayType(newType, containsNull)\n        }\n      case MapType(keyType, elementType, containsNull) =>\n        // if the map key/element type is from from_json, we will exclude the map.\n        val filtertedKeyType = pruneEmptyStructs(keyType)\n        val filtertedValueType = pruneEmptyStructs(elementType)\n        if (filtertedKeyType.isEmpty || filtertedValueType.isEmpty) {\n          None\n        } else {\n          Option(MapType(filtertedKeyType.get, filtertedValueType.get, containsNull))\n        }\n      case _ => Option(dataType)\n    }\n  }\n\n  /**\n   * Transform (nested) columns in `schema` by walking down `schema` and `other` simultaneously.\n   * This allows comparing the two schemas and transforming `schema` based on the comparison.\n   * Columns or fields present only in `other` are ignored while `None` is passed to the transform\n   * function for columns or fields missing in `other`.\n   * @param schema Schema to transform.\n   * @param other Schema to compare with.\n   * @param tf Function to apply. The function arguments are the full path of the current field to\n   *           transform, the current field in `schema` and, if present, the corresponding field in\n   *           `other`.\n   */\n  def transformColumns(\n      schema: StructType,\n      other: StructType)(\n    tf: (Seq[String], StructField, Option[StructField], Resolver) => StructField): StructType = {\n    def transform[E <: DataType](path: Seq[String], dt: E, otherDt: E): E = {\n      val newDt = (dt, otherDt) match {\n        case (struct: StructType, otherStruct: StructType) =>\n          val otherFields = SchemaMergingUtils.toFieldMap(otherStruct.fields, caseSensitive = true)\n          StructType(struct.map { field =>\n            val otherField = otherFields.get(field.name)\n            val newField = tf(path, field, otherField, DELTA_COL_RESOLVER)\n            otherField match {\n              case Some(other) =>\n                newField.copy(\n                  dataType = transform(path :+ field.name, field.dataType, other.dataType)\n                )\n              case None => newField\n            }\n          })\n        case (map: MapType, otherMap: MapType) =>\n          map.copy(\n            keyType = transform(path :+ \"key\", map.keyType, otherMap.keyType),\n            valueType = transform(path :+ \"value\", map.valueType, otherMap.valueType)\n          )\n        case (array: ArrayType, otherArray: ArrayType) =>\n          array.copy(\n            elementType = transform(path :+ \"element\", array.elementType, otherArray.elementType)\n          )\n        case _ => dt\n      }\n      newDt.asInstanceOf[E]\n    }\n    transform(Seq.empty, schema, other)\n  }\n\n  /**\n   *\n   * Taken from DataType\n   *\n   * Compares two types, ignoring compatible nullability of ArrayType, MapType, StructType, and\n   * ignoring case sensitivity of field names in StructType.\n   *\n   * Compatible nullability is defined as follows:\n   *   - If `from` and `to` are ArrayTypes, `from` has a compatible nullability with `to`\n   *   if and only if `to.containsNull` is true, or both of `from.containsNull` and\n   *   `to.containsNull` are false.\n   *   - If `from` and `to` are MapTypes, `from` has a compatible nullability with `to`\n   *   if and only if `to.valueContainsNull` is true, or both of `from.valueContainsNull` and\n   *   `to.valueContainsNull` are false.\n   *   - If `from` and `to` are StructTypes, `from` has a compatible nullability with `to`\n   *   if and only if for all every pair of fields, `to.nullable` is true, or both\n   *   of `fromField.nullable` and `toField.nullable` are false.\n   */\n  def equalsIgnoreCaseAndCompatibleNullability(from: DataType, to: DataType): Boolean = {\n    (from, to) match {\n      case (ArrayType(fromElement, fn), ArrayType(toElement, tn)) =>\n        (tn || !fn) && equalsIgnoreCaseAndCompatibleNullability(fromElement, toElement)\n\n      case (MapType(fromKey, fromValue, fn), MapType(toKey, toValue, tn)) =>\n        (tn || !fn) &&\n          equalsIgnoreCaseAndCompatibleNullability(fromKey, toKey) &&\n          equalsIgnoreCaseAndCompatibleNullability(fromValue, toValue)\n\n      case (StructType(fromFields), StructType(toFields)) =>\n        fromFields.length == toFields.length &&\n          fromFields.zip(toFields).forall { case (fromField, toField) =>\n            fromField.name.equalsIgnoreCase(toField.name) &&\n              (toField.nullable || !fromField.nullable) &&\n              equalsIgnoreCaseAndCompatibleNullability(fromField.dataType, toField.dataType)\n          }\n\n      case (fromDataType, toDataType) => fromDataType == toDataType\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/schema/SchemaUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, DeltaColumnMappingMode, DeltaErrors, DeltaLog, GeneratedColumn, NoMapping, TypeWidening, TypeWideningMode}\nimport org.apache.spark.sql.delta.{RowCommitVersion, RowId}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils._\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.sources.{DeltaSQLConf, DeltaStreamUtils}\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.analysis.{Resolver, UnresolvedAttribute}\nimport org.apache.spark.sql.catalyst.expressions.{Alias, AttributeReference, Expression, GetArrayItem, GetArrayStructFields, GetMapValue, GetStructField}\nimport org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, Project}\nimport org.apache.spark.sql.catalyst.util.{CharVarcharUtils, ResolveDefaultColumnsUtils}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\nobject SchemaUtils extends DeltaLogging {\n  // We use case insensitive resolution while writing into Delta\n  val DELTA_COL_RESOLVER: (String, String) => Boolean =\n    org.apache.spark.sql.catalyst.analysis.caseInsensitiveResolution\n  private val ARRAY_ELEMENT_INDEX = 0\n  private val MAP_KEY_INDEX = 0\n  private val MAP_VALUE_INDEX = 1\n\n  /**\n   * Finds `StructField`s that match a given check `f`. Returns the path to the column, and the\n   * field.\n   *\n   * @param checkComplexTypes While `StructType` is also a complex type, since we're returning\n   *                          StructFields, we definitely recurse into StructTypes. This flag\n   *                          defines whether we should recurse into ArrayType and MapType.\n   */\n  def filterRecursively(\n      schema: DataType,\n      checkComplexTypes: Boolean)(f: StructField => Boolean): Seq[(Seq[String], StructField)] = {\n    def recurseIntoComplexTypes(\n        complexType: DataType,\n        columnStack: Seq[String]): Seq[(Seq[String], StructField)] = complexType match {\n      case s: StructType =>\n        s.fields.flatMap { sf =>\n          val includeLevel = if (f(sf)) Seq((columnStack, sf)) else Nil\n          includeLevel ++ recurseIntoComplexTypes(sf.dataType, columnStack :+ sf.name)\n        }\n      case a: ArrayType if checkComplexTypes =>\n        recurseIntoComplexTypes(a.elementType, columnStack :+ \"element\")\n      case m: MapType if checkComplexTypes =>\n        recurseIntoComplexTypes(m.keyType, columnStack :+ \"key\") ++\n          recurseIntoComplexTypes(m.valueType, columnStack :+ \"value\")\n      case _ => Nil\n    }\n\n    recurseIntoComplexTypes(schema, Nil)\n  }\n\n  /** Copied over from DataType for visibility reasons. */\n  def typeExistsRecursively(dt: DataType)(f: DataType => Boolean): Boolean = dt match {\n    case s: StructType =>\n      f(s) || s.fields.exists(field => typeExistsRecursively(field.dataType)(f))\n    case a: ArrayType =>\n      f(a) || typeExistsRecursively(a.elementType)(f)\n    case m: MapType =>\n      f(m) || typeExistsRecursively(m.keyType)(f) || typeExistsRecursively(m.valueType)(f)\n    case other =>\n      f(other)\n  }\n\n  def findAnyTypeRecursively(dt: DataType)(f: DataType => Boolean): Option[DataType] = dt match {\n    case s: StructType =>\n      Some(s).filter(f).orElse(s.fields\n          .flatMap(field => findAnyTypeRecursively(field.dataType)(f)).find(_ => true))\n    case a: ArrayType =>\n      Some(a).filter(f).orElse(findAnyTypeRecursively(a.elementType)(f))\n    case m: MapType =>\n      Some(m).filter(f).orElse(findAnyTypeRecursively(m.keyType)(f))\n        .orElse(findAnyTypeRecursively(m.valueType)(f))\n    case other =>\n      Some(other).filter(f)\n  }\n\n  /**\n   * Checks if a given data type contains a NullType, including inside UDTs.\n   * `typeExistsRecursively` does not recurse into UDT sqlTypes, so this method\n   * explicitly handles that case.\n   */\n  def nullTypeExistsRecursively(\n      t: DataType\n  ): Boolean = {\n    typeExistsRecursively(t) {\n      case _: NullType =>\n        true\n      case udt: UserDefinedType[_] =>\n        nullTypeExistsRecursively(udt.sqlType)\n      case _ =>\n        false\n    }\n  }\n\n  /** Turns the data types to nullable in a recursive manner for nested columns. */\n  def typeAsNullable(dt: DataType): DataType = dt match {\n    case s: StructType => s.asNullable\n    case a @ ArrayType(s: StructType, _) => a.copy(s.asNullable, containsNull = true)\n    case a: ArrayType => a.copy(containsNull = true)\n    case m @ MapType(s1: StructType, s2: StructType, _) =>\n      m.copy(s1.asNullable, s2.asNullable, valueContainsNull = true)\n    case m @ MapType(s1: StructType, _, _) =>\n      m.copy(keyType = s1.asNullable, valueContainsNull = true)\n    case m @ MapType(_, s2: StructType, _) =>\n      m.copy(valueType = s2.asNullable, valueContainsNull = true)\n    case other => other\n  }\n\n  /**\n   * Drops null types from the DataFrame if they exist. We don't have easy ways of generating types\n   * such as MapType and ArrayType, therefore if these types contain NullType in their elements,\n   * we will throw an AnalysisException.\n   */\n  def dropNullTypeColumns(df: DataFrame): DataFrame = {\n    val schema = df.schema\n    if (!nullTypeExistsRecursively(schema)) return df\n\n    def generateSelectExpr(sf: StructField, nameStack: Seq[String]): Column = sf.dataType match {\n      case st: StructType =>\n        val nested = st.fields.flatMap { f =>\n          if (f.dataType.isInstanceOf[NullType]) {\n            None\n          } else {\n            Some(generateSelectExpr(f, nameStack :+ sf.name))\n          }\n        }\n        val colName = UnresolvedAttribute.apply(nameStack :+ sf.name).sql\n        when(col(colName).isNull, null)\n          .otherwise(struct(nested: _*))\n          .alias(sf.name)\n      case a: ArrayType if nullTypeExistsRecursively(a) =>\n        val colName = UnresolvedAttribute.apply(nameStack :+ sf.name).sql\n        throw new DeltaAnalysisException(\n          errorClass = \"DELTA_COMPLEX_TYPE_COLUMN_CONTAINS_NULL_TYPE\",\n          messageParameters = Array(colName, \"ArrayType\"))\n      case m: MapType if nullTypeExistsRecursively(m) =>\n        val colName = UnresolvedAttribute.apply(nameStack :+ sf.name).sql\n        throw new DeltaAnalysisException(\n          errorClass = \"DELTA_COMPLEX_TYPE_COLUMN_CONTAINS_NULL_TYPE\",\n          messageParameters = Array(colName, \"MapType\"))\n        case udt: UserDefinedType[_] if nullTypeExistsRecursively(udt.sqlType) =>\n          val colName = UnresolvedAttribute.apply(nameStack :+ sf.name).sql\n        throw new DeltaAnalysisException(\n          errorClass = \"DELTA_USER_DEFINED_TYPE_COLUMN_CONTAINS_NULL_TYPE\",\n          messageParameters = Array(colName, udt.userClass.getName))\n      case _ =>\n        val colName = UnresolvedAttribute.apply(nameStack :+ sf.name).sql\n        col(colName).alias(sf.name)\n    }\n\n    val selectExprs = schema.flatMap { f =>\n      if (f.dataType.isInstanceOf[NullType]) None else Some(generateSelectExpr(f, Nil))\n    }\n    df.select(selectExprs: _*)\n  }\n\n  /**\n   * A char(x)/varchar(x) related types are internally stored as string type with the constraint\n   * information stored in the metadata. For example:\n   *  + char(10) is (string, char_varchar_metadata = \"char(10)\")\n   *  + array[varchar(10)] is (array[string], char_varchar_metadata = \"array[varchar(10)]\")\n   * This method converts the string + metadata representation to the actual type.\n   *  + (string, char_varchar_metadata = \"char(10)\") -> (char(10), char_varchar_metadata = \"\")\n   *  + (array[string], char_varchar_metadata = \"array[varchar(10)]\")\n   *    -> (array[varchar(10)], char_varchar_metadata = \"\")\n   */\n  private def getRawFieldWithoutCharVarcharMetadata(field: StructField): StructField = {\n    val rawField = CharVarcharUtils.getRawType(field.metadata)\n      .map(dt => field.copy(dataType = dt))\n      .getOrElse(field)\n    val throwAwayAttrRef = AttributeReference(\n      rawField.name,\n      rawField.dataType,\n      nullable = rawField.nullable,\n      rawField.metadata)()\n    val cleanedMetadata = CharVarcharUtils.cleanAttrMetadata(throwAwayAttrRef).metadata\n    rawField.copy(metadata = cleanedMetadata)\n  }\n\n  /**\n   * Sets a data type to a field in a char/varchar-safe manner. A char(x)/varchar(x) related types\n   * consists of two parts: a string-based type and the constraint information stored in the\n   * metadata. Simply changing the data type will lead to unexpected results.\n   *\n   * For example, an array[varchar(10)] type is internally represented as\n   * (array[string], char_varchar_metadata = \"array[varchar(10)]\"). If we convert it into an\n   * array[string] simply by setting the data type part, the metadata part will still be there, and\n   * the type will still stay as array[varchar(10)].\n   *\n   * This method first converts the field into its raw type without the metadata part, then sets the\n   * data type part to the new data type, and finally converts the whole thing back to the original\n   * representation.\n   *\n   * In the above example, this methods will convert the array[varchar(10)] representation into\n   * (array[varchar(10)], char_varchar_metadata = \"\"), set the data type to array[string]\n   * (array[string], char_varchar_metadata = \"\"), and finally convert it back to\n   * (array[string], char_varchar_metadata = \"\"), which happens to be the same.\n   */\n  def setFieldDataTypeCharVarcharSafe(field: StructField, newDataType: DataType): StructField = {\n    val byPassCharVarcharToStringFix =\n      SparkSession.active.conf.get(DeltaSQLConf.DELTA_BYPASS_CHARVARCHAR_TO_STRING_FIX)\n    // Convert the field into its raw type without the metadata part\n    val rawField = if (byPassCharVarcharToStringFix) {\n      field\n    } else {\n      getRawFieldWithoutCharVarcharMetadata(field)\n    }\n\n    // Set the new data type\n    val rawFieldWithNewDataType = rawField.copy(dataType = newDataType)\n\n    // Convert it back to the original representation\n    if (byPassCharVarcharToStringFix) {\n      rawFieldWithNewDataType\n    } else {\n      val throwAwayStructType = StructType(Seq(rawFieldWithNewDataType))\n      CharVarcharUtils.replaceCharVarcharWithStringInSchema(throwAwayStructType)\n        .head\n    }\n  }\n\n  /**\n   * Drops null types from the schema if they exist. We do not recurse into Array and Map types,\n   * because we do not expect null types to exist in those columns, as Delta doesn't allow it during\n   * writes.\n   */\n  def dropNullTypeColumns(schema: StructType): StructType = {\n    def recurseAndRemove(struct: StructType): Seq[StructField] = {\n      struct.flatMap {\n        case sf @ StructField(_, s: StructType, _, _) =>\n          Some(sf.copy(dataType = StructType(recurseAndRemove(s))))\n        case StructField(_, n: NullType, _, _) => None\n        case other => Some(other)\n      }\n    }\n    StructType(recurseAndRemove(schema))\n  }\n\n  /**\n   * Returns the name of the first column/field that has null type (void).\n   */\n  def findNullTypeColumn(schema: StructType): Option[String] = {\n    // Helper method to recursively check nested structs.\n    def findNullTypeColumnRec(s: StructType, nameStack: Seq[String]): Option[String] = {\n      val nullFields = s.flatMap {\n        case StructField(name, n: NullType, _, _) => Some((nameStack :+ name).mkString(\".\"))\n        case StructField(name, s: StructType, _, _) => findNullTypeColumnRec(s, nameStack :+ name)\n        // Note that we don't recursively check Array and Map types because NullTypes are already\n        // not allowed (see 'dropNullTypeColumns').\n        case _ => None\n      }\n      return nullFields.headOption\n    }\n\n    if (nullTypeExistsRecursively(schema)) {\n      findNullTypeColumnRec(schema, Seq.empty)\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Recursively rewrite the query field names according to the table schema within nested\n   * data types.\n   *\n   * The same assumptions as in [[normalizeColumnNames]] are made.\n   *\n   * @param sourceDataType The data type that needs normalizing.\n   * @param tableDataType The normalization template from the table's schema.\n   * @param sourceParentFields The path (starting from the top level) to the nested field\n   *                           with `sourceDataType`.\n   * @param tableSchema The entire schema of the table.\n   *\n   * @return A normalized version of `sourceDataType`.\n   */\ndef normalizeColumnNamesInDataType(\n      deltaLog: DeltaLog,\n      sourceDataType: DataType,\n      tableDataType: DataType,\n      sourceParentFields: Seq[String],\n      tableSchema: StructType\n    ): DataType = {\n\n    def getMatchingTableField(\n        sourceField: StructField,\n        tableFields: Map[String, StructField]): StructField = {\n      tableFields.get(sourceField.name) match {\n        case Some(tableField) => tableField\n        case None =>\n          val columnPath = (sourceParentFields ++ Seq(sourceField.name)).mkString(\".\")\n          throw DeltaErrors.cannotResolveColumn(columnPath, tableSchema)\n      }\n    }\n\n    (sourceDataType, tableDataType) match {\n      case (sourceStruct: StructType, tableStruct: StructType) =>\n        val tableFields = toFieldMap(tableStruct.fields, caseSensitive = false)\n        val normalizedFields = sourceStruct.fields.map { sourceField =>\n          val tableField = getMatchingTableField(sourceField, tableFields)\n          val normalizedDataType =\n            normalizeColumnNamesInDataType(deltaLog, sourceField.dataType, tableField.dataType,\n              sourceParentFields :+ sourceField.name, tableSchema)\n          val normalizedName = tableField.name\n          sourceField.copy(\n            name = normalizedName,\n            dataType = normalizedDataType\n          )\n        }\n        sourceStruct.copy(fields = normalizedFields)\n      case (sourceArray: ArrayType, tableArray: ArrayType) =>\n        val normalizedElementType = normalizeColumnNamesInDataType(deltaLog,\n          sourceArray.elementType, tableArray.elementType, sourceParentFields, tableSchema)\n        sourceArray.copy(elementType = normalizedElementType)\n      case (sourceMap: MapType, tableMap: MapType) =>\n        val normalizedKeyType = normalizeColumnNamesInDataType(deltaLog, sourceMap.keyType,\n          tableMap.keyType, sourceParentFields, tableSchema)\n        val normalizedValueType = normalizeColumnNamesInDataType(deltaLog, sourceMap.valueType,\n          tableMap.valueType, sourceParentFields, tableSchema)\n        sourceMap.copy(\n          keyType = normalizedKeyType,\n          valueType = normalizedValueType\n        )\n      case (_: NullType, _) =>\n        // When schema evolution adds a new column during MERGE, it can be represented with\n        // a NullType in the schema of the data written by the MERGE.\n        sourceDataType\n      case (_: AtomicType, _: AtomicType) =>\n        // Some atomic types (e.g. integral types) can be cast to each other later on. For now,\n        // it's enough to know that there are no nested fields inside the atomic types that might\n        // require normalization.\n        sourceDataType\n      case _ =>\n        if (DeltaUtils.isTesting) {\n          assert(sourceDataType == tableDataType,\n            s\"Types without nesting should match but $sourceDataType != $tableDataType\")\n        } else if (sourceDataType != tableDataType) {\n          recordDeltaEvent(\n            deltaLog = deltaLog,\n            opType = \"delta.assertions.schemaNormalization.nonNestedTypeMismatch\",\n            tags = Map.empty,\n            data = Map(\n              \"sourceDataType\" -> sourceDataType.json,\n              \"tableDataType\" -> tableDataType.json\n            ),\n            path = None)\n        }\n        // The data types are compatible.\n        sourceDataType\n    }\n  }\n\n  /**\n   * Rewrite the query field names according to the table schema. This method assumes that all\n   * schema validation checks have been made and this is the last operation before writing into\n   * Delta.\n   */\n  def normalizeColumnNames(\n      deltaLog: DeltaLog,\n      baseSchema: StructType,\n      data: Dataset[_]\n    ): DataFrame = {\n    val dataSchema = data.schema\n    val dataFields = explodeNestedFieldNames(dataSchema).toSet\n    val tableFields = explodeNestedFieldNames(baseSchema).toSet\n    if (dataFields.subsetOf(tableFields)) {\n      data.toDF()\n    } else {\n      // Allow the same shortcut logic (as the above `if` stmt) if the only extra fields are CDC\n      // metadata fields.\n      val nonCdcFields = dataFields.filterNot { f =>\n        f == CDCReader.CDC_PARTITION_COL || f == CDCReader.CDC_TYPE_COLUMN_NAME\n      }\n      if (nonCdcFields.subsetOf(tableFields)) {\n        return data.toDF()\n      }\n\n      val baseFields = toFieldMap(baseSchema, caseSensitive = false)\n      val aliasExpressions = dataSchema.map { field =>\n        val (originalCase, castDataType): (String, Option[DataType]) =\n          baseFields.get(field.name) match {\n            case Some(original) =>\n              val normalizedDataType = normalizeColumnNamesInDataType(deltaLog,\n                field.dataType, original.dataType, Seq(field.name), baseSchema)\n              (original.name, Option.when(field.dataType != normalizedDataType)(normalizedDataType))\n            // This is a virtual partition column used for doing CDC writes. It's not actually\n            // in the table schema.\n            case None if field.name == CDCReader.CDC_TYPE_COLUMN_NAME ||\n              field.name == CDCReader.CDC_PARTITION_COL => (field.name, None)\n            // Consider Row Id columns internal if Row Ids are enabled.\n            case None if RowId.RowIdMetadataStructField.isRowIdColumn(field) =>\n              (field.name, None)\n            case None if RowCommitVersion.MetadataStructField.isRowCommitVersionColumn(field) =>\n              (field.name, None)\n            case None =>\n              throw DeltaErrors.cannotResolveColumn(field.name, baseSchema)\n          }\n        var expression = fieldToColumn(field)\n        castDataType.foreach { castType =>\n          expression = expression.cast(castType)\n        }\n        if (originalCase != field.name) {\n          expression = expression.as(originalCase)\n        }\n        expression\n      }\n      data.queryExecution match {\n        case incrementalExecution: IncrementalExecution =>\n          DeltaStreamUtils.selectFromStreamingDataFrame(\n            incrementalExecution, data.toDF(), aliasExpressions: _*)\n        case _ => data.select(aliasExpressions: _*)\n      }\n    }\n  }\n\n  /**\n   * A helper function to check if partition columns are the same.\n   * This function only checks for partition column names.\n   * Please use with other schema check functions for detecting type change etc.\n   */\n  def isPartitionCompatible(\n      newPartitionColumns: Seq[String] = Seq.empty,\n      oldPartitionColumns: Seq[String] = Seq.empty): Boolean = {\n    newPartitionColumns == oldPartitionColumns\n  }\n\n  /**\n   * As the Delta snapshots update, the schema may change as well. This method defines whether the\n   * new schema of a Delta table can be used with a previously analyzed LogicalPlan. Our\n   * rules are to return false if:\n   *   - Dropping any column or struct field that was present in the existing schema, if not\n   *     allowMissingColumns\n   *   - Any change of datatype, unless eligible for widening. The caller specifies eligible type\n   *     changes via `typeWideningMode`.\n   *   - Change of partition columns. Although analyzed LogicalPlan is not changed,\n   *     physical structure of data is changed and thus is considered not read compatible.\n   *   - If `forbidTightenNullability` = true:\n   *      - Forbids tightening the nullability (existing nullable=true -> read nullable=false)\n   *      - Typically Used when the existing schema refers to the schema of written data, such as\n   *        when a Delta streaming source reads a schema change (existingSchema) which\n   *        has nullable=true, using the latest schema which has nullable=false, so we should not\n   *        project nulls from the data into the non-nullable read schema.\n   *   - Otherwise:\n   *      - Forbids relaxing the nullability (existing nullable=false -> read nullable=true)\n   *      - Typically Used when the read schema refers to the schema of written data, such as during\n   *        Delta scan, the latest schema during execution (readSchema) has nullable=true but during\n   *        analysis phase the schema (existingSchema) was nullable=false, so we should not project\n   *        nulls from the later data onto a non-nullable schema analyzed in the past.\n   */\n  def isReadCompatible(\n      existingSchema: StructType,\n      readSchema: StructType,\n      forbidTightenNullability: Boolean = false,\n      allowMissingColumns: Boolean = false,\n      typeWideningMode: TypeWideningMode = TypeWideningMode.NoTypeWidening,\n      newPartitionColumns: Seq[String] = Seq.empty,\n      oldPartitionColumns: Seq[String] = Seq.empty): Boolean = {\n\n    def isNullabilityCompatible(existingNullable: Boolean, readNullable: Boolean): Boolean = {\n      if (forbidTightenNullability) {\n        readNullable || !existingNullable\n      } else {\n        existingNullable || !readNullable\n      }\n    }\n\n    def isDatatypeReadCompatible(existing: DataType, newtype: DataType): Boolean = {\n      (existing, newtype) match {\n        case (e: StructType, n: StructType) =>\n          isReadCompatible(e, n,\n            forbidTightenNullability,\n            typeWideningMode = typeWideningMode,\n            allowMissingColumns = allowMissingColumns\n          )\n        case (e: ArrayType, n: ArrayType) =>\n          // if existing elements are non-nullable, so should be the new element\n          isNullabilityCompatible(e.containsNull, n.containsNull) &&\n            isDatatypeReadCompatible(e.elementType, n.elementType)\n        case (e: MapType, n: MapType) =>\n          // if existing value is non-nullable, so should be the new value\n          isNullabilityCompatible(e.valueContainsNull, n.valueContainsNull) &&\n            isDatatypeReadCompatible(e.keyType, n.keyType) &&\n            isDatatypeReadCompatible(e.valueType, n.valueType)\n        case (e: AtomicType, n: AtomicType)\n          if typeWideningMode.shouldWidenTo(fromType = e, toType = n) => true\n        case (a, b) => a == b\n      }\n    }\n\n    def isStructReadCompatible(existing: StructType, newtype: StructType): Boolean = {\n      val existingFields = toFieldMap(existing)\n      // scalastyle:off caselocale\n      val existingFieldNames = existing.fieldNames.map(_.toLowerCase).toSet\n      assert(existingFieldNames.size == existing.length,\n        \"Delta tables don't allow field names that only differ by case\")\n      val newFields = newtype.fieldNames.map(_.toLowerCase).toSet\n      assert(newFields.size == newtype.length,\n        \"Delta tables don't allow field names that only differ by case\")\n      // scalastyle:on caselocale\n\n      if (!allowMissingColumns &&\n        !(existingFieldNames.subsetOf(newFields) &&\n          isPartitionCompatible(newPartitionColumns, oldPartitionColumns))) {\n        // Dropped a column that was present in the DataFrame schema\n        return false\n      }\n      newtype.forall { newField =>\n        // new fields are fine, they just won't be returned\n        existingFields.get(newField.name).forall { existingField =>\n          // we know the name matches modulo case - now verify exact match\n          (existingField.name == newField.name\n            // if existing value is non-nullable, so should be the new value\n            && isNullabilityCompatible(existingField.nullable, newField.nullable)\n            // and the type of the field must be compatible, too\n            && isDatatypeReadCompatible(existingField.dataType, newField.dataType))\n        }\n      }\n    }\n\n    isStructReadCompatible(existingSchema, readSchema)\n  }\n\n  /**\n   * Compare an existing schema to a specified new schema and\n   * return a message describing the first difference found, if any:\n   *   - different field name or datatype\n   *   - different metadata\n   */\n  def reportDifferences(existingSchema: StructType, specifiedSchema: StructType): Seq[String] = {\n\n    def canOrNot(can: Boolean) = if (can) \"can\" else \"can not\"\n    def isOrNon(b: Boolean) = if (b) \"\" else \"non-\"\n\n    def missingFieldsMessage(fields: Set[String]) : String = {\n      s\"Specified schema is missing field(s): ${fields.mkString(\", \")}\"\n    }\n    def additionalFieldsMessage(fields: Set[String]) : String = {\n      s\"Specified schema has additional field(s): ${fields.mkString(\", \")}\"\n    }\n    def fieldNullabilityMessage(field: String, specified: Boolean, existing: Boolean) : String = {\n      s\"Field $field is ${isOrNon(specified)}nullable in specified \" +\n        s\"schema but ${isOrNon(existing)}nullable in existing schema.\"\n    }\n    def arrayNullabilityMessage(field: String, specified: Boolean, existing: Boolean) : String = {\n      s\"Array field $field ${canOrNot(specified)} contain null in specified schema \" +\n        s\"but ${canOrNot(existing)} in existing schema\"\n    }\n    def valueNullabilityMessage(field: String, specified: Boolean, existing: Boolean) : String = {\n      s\"Map field $field ${canOrNot(specified)} contain null values in specified schema \" +\n        s\"but ${canOrNot(existing)} in existing schema\"\n    }\n    def removeGenerationExpressionMetadata(metadata: Metadata): Metadata = {\n      new MetadataBuilder()\n        .withMetadata(metadata)\n        .remove(GENERATION_EXPRESSION_METADATA_KEY)\n        .build()\n    }\n    def metadataDifferentMessage(field: String, specified: Metadata, existing: Metadata)\n      : String = {\n      val specifiedGenerationExpr = GeneratedColumn.getGenerationExpressionStr(specified)\n      val existingGenerationExpr = GeneratedColumn.getGenerationExpressionStr(existing)\n      var metadataDiffMessage = \"\"\n      if (specifiedGenerationExpr != existingGenerationExpr) {\n        metadataDiffMessage +=\n          s\"\"\"Specified generation expression for field $field is different from existing schema:\n             |Specified: ${specifiedGenerationExpr.getOrElse(\"\")}\n             |Existing:  ${existingGenerationExpr.getOrElse(\"\")}\"\"\".stripMargin\n      }\n      val specifiedMetadataWithoutGenerationExpr = removeGenerationExpressionMetadata(specified)\n      val existingMetadataWithoutGenerationExpr = removeGenerationExpressionMetadata(existing)\n      if (specifiedMetadataWithoutGenerationExpr != existingMetadataWithoutGenerationExpr) {\n        if (metadataDiffMessage.nonEmpty) metadataDiffMessage += \"\\n\"\n        metadataDiffMessage +=\n          s\"\"\"Specified metadata for field $field is different from existing schema:\n             |Specified: $specifiedMetadataWithoutGenerationExpr\n             |Existing:  $existingMetadataWithoutGenerationExpr\"\"\".stripMargin\n      }\n      metadataDiffMessage\n    }\n    def typeDifferenceMessage(field: String, specified: DataType, existing: DataType)\n      : String = {\n      s\"\"\"Specified type for $field is different from existing schema:\n         |Specified: ${specified.typeName}\n         |Existing:  ${existing.typeName}\"\"\".stripMargin\n    }\n\n    // prefix represents the nested field(s) containing this schema\n    def structDifference(existing: StructType, specified: StructType, prefix: String)\n      : Seq[String] = {\n\n      // 1. ensure set of fields is the same\n      val existingFieldNames = existing.fieldNames.toSet\n      val specifiedFieldNames = specified.fieldNames.toSet\n\n      val missingFields = existingFieldNames diff specifiedFieldNames\n      val missingFieldsDiffs =\n        if (missingFields.isEmpty) Nil\n        else Seq(missingFieldsMessage(missingFields.map(prefix + _)))\n\n      val extraFields = specifiedFieldNames diff existingFieldNames\n      val extraFieldsDiffs =\n        if (extraFields.isEmpty) Nil\n        else Seq(additionalFieldsMessage(extraFields.map(prefix + _)))\n\n      // 2. for each common field, ensure it has the same type and metadata\n      val existingFields = toFieldMap(existing)\n      val specifiedFields = toFieldMap(specified)\n      val fieldsDiffs = (existingFieldNames intersect specifiedFieldNames).flatMap(\n        (name: String) => fieldDifference(existingFields(name), specifiedFields(name), prefix))\n\n      missingFieldsDiffs ++ extraFieldsDiffs ++ fieldsDiffs\n    }\n\n    def fieldDifference(existing: StructField, specified: StructField, prefix: String)\n      : Seq[String] = {\n\n      val name = s\"$prefix${existing.name}\"\n      val nullabilityDiffs =\n        if (existing.nullable == specified.nullable) Nil\n        else Seq(fieldNullabilityMessage(s\"$name\", specified.nullable, existing.nullable))\n      val metadataDiffs =\n        if (existing.metadata == specified.metadata) Nil\n        else Seq(metadataDifferentMessage(s\"$name\", specified.metadata, existing.metadata))\n      val typeDiffs =\n        typeDifference(existing.dataType, specified.dataType, name)\n\n      nullabilityDiffs ++ metadataDiffs ++ typeDiffs\n    }\n\n    def typeDifference(existing: DataType, specified: DataType, field: String)\n      : Seq[String] = {\n\n      (existing, specified) match {\n        case (e: StructType, s: StructType) => structDifference(e, s, s\"$field.\")\n        case (e: ArrayType, s: ArrayType) => arrayDifference(e, s, s\"$field[]\")\n        case (e: MapType, s: MapType) => mapDifference(e, s, s\"$field\")\n        case (e, s) if e != s => Seq(typeDifferenceMessage(field, s, e))\n        case _ => Nil\n      }\n    }\n\n    def arrayDifference(existing: ArrayType, specified: ArrayType, field: String): Seq[String] = {\n\n      val elementDiffs =\n        typeDifference(existing.elementType, specified.elementType, field)\n      val nullabilityDiffs =\n        if (existing.containsNull == specified.containsNull) Nil\n        else Seq(arrayNullabilityMessage(field, specified.containsNull, existing.containsNull))\n\n      elementDiffs ++ nullabilityDiffs\n    }\n\n    def mapDifference(existing: MapType, specified: MapType, field: String) : Seq[String] = {\n\n      val keyDiffs =\n        typeDifference(existing.keyType, specified.keyType, s\"$field[key]\")\n      val valueDiffs =\n        typeDifference(existing.valueType, specified.valueType, s\"$field[value]\")\n      val nullabilityDiffs =\n        if (existing.valueContainsNull == specified.valueContainsNull) Nil\n        else Seq(\n          valueNullabilityMessage(field, specified.valueContainsNull, existing.valueContainsNull))\n\n      keyDiffs ++ valueDiffs ++ nullabilityDiffs\n    }\n\n    structDifference(\n      existingSchema,\n      CharVarcharUtils.replaceCharVarcharWithStringInSchema(specifiedSchema),\n      \"\"\n    )\n  }\n\n  /**\n   * Copied verbatim from Apache Spark.\n   *\n   * Returns a field in this struct and its child structs, case insensitively. This is slightly less\n   * performant than the case sensitive version.\n   *\n   * If includeCollections is true, this will return fields that are nested in maps and arrays.\n   *\n   * @param fieldNames The path to the field, in order from the root. For example, the column\n   *                   nested.a.b.c would be Seq(\"nested\", \"a\", \"b\", \"c\").\n   */\n  def findNestedFieldIgnoreCase(\n      schema: StructType,\n      fieldNames: Seq[String],\n      includeCollections: Boolean = false): Option[StructField] = {\n\n    @scala.annotation.tailrec\n    def findRecursively(\n      dataType: DataType,\n      fieldNames: Seq[String],\n      includeCollections: Boolean): Option[StructField] = {\n\n      (fieldNames, dataType, includeCollections) match {\n        case (Seq(fieldName, names @ _*), struct: StructType, _) =>\n          val field = struct.find(_.name.equalsIgnoreCase(fieldName))\n          if (names.isEmpty || field.isEmpty) {\n            field\n          } else {\n            findRecursively(field.get.dataType, names, includeCollections)\n          }\n\n        case (_, _, false) => None // types nested in maps and arrays are not used\n\n        case (Seq(\"key\"), MapType(keyType, _, _), true) =>\n          // return the key type as a struct field to include nullability\n          Some(StructField(\"key\", keyType, nullable = false))\n\n        case (Seq(\"key\", names @ _*), MapType(keyType, _, _), true) =>\n          findRecursively(keyType, names, includeCollections)\n\n        case (Seq(\"value\"), MapType(_, valueType, isNullable), true) =>\n          // return the value type as a struct field to include nullability\n          Some(StructField(\"value\", valueType, nullable = isNullable))\n\n        case (Seq(\"value\", names @ _*), MapType(_, valueType, _), true) =>\n          findRecursively(valueType, names, includeCollections)\n\n        case (Seq(\"element\"), ArrayType(elementType, isNullable), true) =>\n          // return the element type as a struct field to include nullability\n          Some(StructField(\"element\", elementType, nullable = isNullable))\n\n        case (Seq(\"element\", names @ _*), ArrayType(elementType, _), true) =>\n          findRecursively(elementType, names, includeCollections)\n\n        case _ =>\n          None\n      }\n    }\n\n    findRecursively(schema, fieldNames, includeCollections)\n  }\n\n  /**\n   * Returns the path of the given column in `schema` as a list of ordinals (0-based), each value\n   * representing the position at the current nesting level starting from the root.\n   *\n   * For ArrayType: accessing the array's element adds a position 0 to the position list.\n   * e.g. accessing a.element.y would have the result -> Seq(..., positionOfA, 0, positionOfY)\n   *\n   * For MapType: accessing the map's key adds a position 0 to the position list.\n   * e.g. accessing m.key.y would have the result -> Seq(..., positionOfM, 0, positionOfY)\n   *\n   * For MapType: accessing the map's value adds a position 1 to the position list.\n   * e.g. accessing m.key.y would have the result -> Seq(..., positionOfM, 1, positionOfY)\n   *\n   * @param column The column to search for in the given struct. If the length of `column` is\n   *               greater than 1, we expect to enter a nested field.\n   * @param schema The current struct we are looking at.\n   * @param resolver The resolver to find the column.\n   */\n  def findColumnPosition(\n      column: Seq[String],\n      schema: DataType,\n      resolver: Resolver = DELTA_COL_RESOLVER): Seq[Int] = {\n    def findRecursively(\n        searchPath: Seq[String],\n        currentType: DataType,\n        currentPath: Seq[String] = Nil): Seq[Int] = {\n      if (searchPath.isEmpty) return Nil\n\n      val currentFieldName = searchPath.head\n      val currentPathWithNestedField = currentPath :+ currentFieldName\n      (currentType, currentFieldName) match {\n        case (struct: StructType, _) =>\n          lazy val columnPath = UnresolvedAttribute(currentPathWithNestedField).name\n          val pos = struct.indexWhere(f => resolver(f.name, currentFieldName))\n          if (pos == -1) {\n            throw DeltaErrors.columnNotInSchemaException(columnPath, schema)\n          }\n          val childPosition = findRecursively(\n            searchPath = searchPath.tail,\n            currentType = struct(pos).dataType,\n            currentPath = currentPathWithNestedField)\n          pos +: childPosition\n\n        case (map: MapType, \"key\") =>\n          val childPosition = findRecursively(\n            searchPath = searchPath.tail,\n            currentType = map.keyType,\n            currentPath = currentPathWithNestedField)\n          MAP_KEY_INDEX +: childPosition\n\n        case (map: MapType, \"value\") =>\n          val childPosition = findRecursively(\n            searchPath = searchPath.tail,\n            currentType = map.valueType,\n            currentPath = currentPathWithNestedField)\n          MAP_VALUE_INDEX +: childPosition\n\n        case (_: MapType, _) =>\n          throw DeltaErrors.foundMapTypeColumnException(\n            prettyFieldName(currentPath :+ \"key\"),\n            prettyFieldName(currentPath :+ \"value\"),\n            schema)\n\n        case (array: ArrayType, \"element\") =>\n          val childPosition = findRecursively(\n            searchPath = searchPath.tail,\n            currentType = array.elementType,\n            currentPath = currentPathWithNestedField)\n          ARRAY_ELEMENT_INDEX +: childPosition\n\n        case (_: ArrayType, _) =>\n          throw DeltaErrors.incorrectArrayAccessByName(\n            prettyFieldName(currentPath :+ \"element\"),\n            prettyFieldName(currentPath),\n            schema)\n        case _ =>\n          throw DeltaErrors.columnPathNotNested(currentFieldName, currentType, currentPath, schema)\n      }\n    }\n\n    try {\n      findRecursively(column, schema)\n    } catch {\n      case e: DeltaAnalysisException => throw e\n      case e: AnalysisException =>\n        throw DeltaErrors.errorFindingColumnPosition(column, schema, e.getMessage)\n    }\n  }\n\n  /**\n   * Returns the nested field at the given position in `parent`. See [[findColumnPosition]] for the\n   * representation used for `position`.\n   * @param parent The field used for the lookup.\n   * @param position A list of ordinals (0-based) representing the path to the nested field in\n   *                 `parent`.\n   */\n  def getNestedFieldFromPosition(parent: StructField, position: Seq[Int]): StructField = {\n    if (position.isEmpty) return parent\n\n    val fieldPos = position.head\n    parent.dataType match {\n      case struct: StructType if fieldPos >= 0 && fieldPos < struct.size =>\n        getNestedFieldFromPosition(struct(fieldPos), position.tail)\n      case map: MapType if fieldPos == MAP_KEY_INDEX =>\n        getNestedFieldFromPosition(StructField(\"key\", map.keyType), position.tail)\n      case map: MapType if fieldPos == MAP_VALUE_INDEX =>\n        getNestedFieldFromPosition(StructField(\"value\", map.valueType), position.tail)\n      case array: ArrayType if fieldPos == ARRAY_ELEMENT_INDEX =>\n        getNestedFieldFromPosition(StructField(\"element\", array.elementType), position.tail)\n      case _: StructType | _: ArrayType | _: MapType =>\n        throw new IllegalArgumentException(\n          s\"Invalid child position $fieldPos in ${parent.dataType}\")\n      case other =>\n        throw new IllegalArgumentException(s\"Invalid indexing into non-nested type $other\")\n    }\n  }\n\n  /**\n   * Returns the nested type at the given position in `schema`. See [[findColumnPosition]] for the\n   * representation used for `position`.\n   * @param parent The root schema used for the lookup.\n   * @param position A list of ordinals (0-based) representing the path to the nested field in\n   *                 `parent`.\n   */\n  def getNestedTypeFromPosition(schema: DataType, position: Seq[Int]): DataType =\n    getNestedFieldFromPosition(StructField(\"schema\", schema), position).dataType\n\n  /**\n   * Pretty print the column path passed in.\n   */\n  def prettyFieldName(columnPath: Seq[String]): String = {\n    UnresolvedAttribute(columnPath).name\n  }\n\n  /**\n   * Add a column to its child.\n   * @param parent The parent data type.\n   * @param column The column to add.\n   * @param position The position to add the column.\n   */\n  def addColumn[T <: DataType](parent: T, column: StructField, position: Seq[Int]): T = {\n    if (position.isEmpty) {\n      throw DeltaErrors.addColumnParentNotStructException(column, parent)\n    }\n    parent match {\n      case struct: StructType =>\n        addColumnToStruct(struct, column, position).asInstanceOf[T]\n      case map: MapType if position.head == MAP_KEY_INDEX =>\n        map.copy(keyType = addColumn(map.keyType, column, position.tail)).asInstanceOf[T]\n      case map: MapType if position.head == MAP_VALUE_INDEX =>\n        map.copy(valueType = addColumn(map.valueType, column, position.tail)).asInstanceOf[T]\n      case array: ArrayType if position.head == ARRAY_ELEMENT_INDEX =>\n        array.copy(elementType =\n          addColumn(array.elementType, column, position.tail)).asInstanceOf[T]\n      case _: ArrayType =>\n        throw DeltaErrors.incorrectArrayAccess()\n      case other =>\n        throw DeltaErrors.addColumnParentNotStructException(column, other)\n    }\n  }\n\n  /**\n   * Add `column` to the specified `position` in a struct `schema`.\n   * @param position A Seq of ordinals on where this column should go. It is a Seq to denote\n   *                 positions in nested columns (0-based). For example:\n   *\n   *                 tableSchema: <a:STRUCT<a1,a2,a3>, b,c:STRUCT<c1,c3>>\n   *                 column: c2\n   *                 position: Seq(2, 1)\n   *                 will return\n   *                 result: <a:STRUCT<a1,a2,a3>, b,c:STRUCT<c1,**c2**,c3>>\n   */\n  private def addColumnToStruct(\n      schema: StructType,\n      column: StructField,\n      position: Seq[Int]): StructType = {\n    // If the proposed new column includes a default value, return a specific \"not supported\" error.\n    // The rationale is that such operations require the data source scan operator to implement\n    // support for filling in the specified default value when the corresponding field is not\n    // present in storage. That is not implemented yet for Delta, so we return this error instead.\n    // The error message is descriptive and provides an easy workaround for the user.\n    if (column.metadata.contains(\"CURRENT_DEFAULT\")) {\n      throw new DeltaAnalysisException(\n        errorClass = \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_ALTER_TABLE_ADD_COLUMN_NOT_SUPPORTED\",\n        messageParameters = Array.empty)\n    }\n\n    require(position.nonEmpty, s\"Don't know where to add the column $column\")\n    val slicePosition = position.head\n    if (slicePosition < 0) {\n      throw DeltaErrors.addColumnAtIndexLessThanZeroException(\n        slicePosition.toString, column.toString)\n    }\n    val length = schema.length\n    if (slicePosition > length) {\n      throw DeltaErrors.indexLargerThanStruct(slicePosition, column, length)\n    }\n    if (slicePosition == length) {\n      if (position.length > 1) {\n        throw DeltaErrors.addColumnStructNotFoundException(slicePosition.toString)\n      }\n      return StructType(schema :+ column)\n    }\n    val (pre, post) = schema.splitAt(slicePosition)\n    if (position.length > 1) {\n      val field = post.head\n      if (!column.nullable && field.nullable) {\n        throw DeltaErrors.nullableParentWithNotNullNestedField\n      }\n      val mid = field.copy(dataType = addColumn(field.dataType, column, position.tail))\n      StructType(pre ++ Seq(mid) ++ post.tail)\n    } else {\n      StructType(pre ++ Seq(column) ++ post)\n    }\n  }\n\n  /**\n   * Drop a column from its child.\n   * @param parent The parent data type.\n   * @param position The position to drop the column.\n   */\n  def dropColumn[T <: DataType](parent: T, position: Seq[Int]): (T, StructField) = {\n    if (position.isEmpty) {\n      throw DeltaErrors.dropNestedColumnsFromNonStructTypeException(parent)\n    }\n    parent match {\n      case struct: StructType =>\n        val (t, s) = dropColumnInStruct(struct, position)\n        (t.asInstanceOf[T], s)\n      case map: MapType if position.head == MAP_KEY_INDEX =>\n        val (newKeyType, droppedColumn) = dropColumn(map.keyType, position.tail)\n        map.copy(keyType = newKeyType).asInstanceOf[T] -> droppedColumn\n      case map: MapType if position.head == MAP_VALUE_INDEX =>\n        val (newValueType, droppedColumn) = dropColumn(map.valueType, position.tail)\n        map.copy(valueType = newValueType).asInstanceOf[T] -> droppedColumn\n      case array: ArrayType if position.head == ARRAY_ELEMENT_INDEX =>\n        val (newElementType, droppedColumn) = dropColumn(array.elementType, position.tail)\n        array.copy(elementType = newElementType).asInstanceOf[T] -> droppedColumn\n      case _: ArrayType =>\n        throw DeltaErrors.incorrectArrayAccess()\n      case other =>\n        throw DeltaErrors.dropNestedColumnsFromNonStructTypeException(other)\n    }\n  }\n\n  /**\n   * Drop from the specified `position` in `schema` and return with the original column.\n   * @param position A Seq of ordinals on where this column should go. It is a Seq to denote\n   *                 positions in nested columns (0-based). For example:\n   *\n   *                 tableSchema: <a:STRUCT<a1,a2,a3>, b,c:STRUCT<c1,c2,c3>>\n   *                 position: Seq(2, 1)\n   *                 will return\n   *                 result: <a:STRUCT<a1,a2,a3>, b,c:STRUCT<c1,c3>>\n   */\n  private def dropColumnInStruct(\n      schema: StructType,\n      position: Seq[Int]): (StructType, StructField) = {\n    require(position.nonEmpty, \"Don't know where to drop the column\")\n    val slicePosition = position.head\n    if (slicePosition < 0) {\n      throw DeltaErrors.dropColumnAtIndexLessThanZeroException(slicePosition)\n    }\n    val length = schema.length\n    if (slicePosition >= length) {\n      throw DeltaErrors.indexLargerOrEqualThanStruct(slicePosition, length)\n    }\n    val (pre, post) = schema.splitAt(slicePosition)\n    val field = post.head\n    if (position.length > 1) {\n      val (newType, droppedColumn) = dropColumn(field.dataType, position.tail)\n      val mid = field.copy(dataType = newType)\n\n      StructType(pre ++ Seq(mid) ++ post.tail) -> droppedColumn\n    } else {\n      if (length == 1) {\n        throw DeltaErrors.dropColumnOnSingleFieldSchema(schema)\n      }\n      StructType(pre ++ post.tail) -> field\n    }\n  }\n\n  /**\n   * Check if the two data types can be changed.\n   *\n   * @param failOnAmbiguousChanges Throw an error if a StructField both has columns dropped and new\n   *                               columns added. These are ambiguous changes, because we don't\n   *                               know if a column needs to be renamed, dropped, or added.\n   * @param allowTypeWidening      Whether widening type changes as defined in [[TypeWidening]]\n   *                               can be applied.\n   * @return None if the data types can be changed, otherwise Some(err) containing the reason.\n   */\n  def canChangeDataType(\n      from: DataType,\n      to: DataType,\n      resolver: Resolver,\n      columnMappingMode: DeltaColumnMappingMode,\n      columnPath: Seq[String] = Nil,\n      failOnAmbiguousChanges: Boolean = false,\n      allowTypeWidening: Boolean = false): Option[String] = {\n    def verify(cond: Boolean, err: => String): Unit = {\n      if (!cond) {\n        throw DeltaErrors.cannotChangeDataType(err)\n      }\n    }\n\n    def verifyNullability(fn: Boolean, tn: Boolean, columnPath: Seq[String]): Unit = {\n      verify(tn || !fn, s\"tightening nullability of ${UnresolvedAttribute(columnPath).name}\")\n    }\n\n    def check(fromDt: DataType, toDt: DataType, columnPath: Seq[String]): Unit = {\n      (fromDt, toDt) match {\n        case (ArrayType(fromElement, fn), ArrayType(toElement, tn)) =>\n          verifyNullability(fn, tn, columnPath)\n          check(fromElement, toElement, columnPath :+ \"element\")\n\n        case (MapType(fromKey, fromValue, fn), MapType(toKey, toValue, tn)) =>\n          verifyNullability(fn, tn, columnPath)\n          check(fromKey, toKey, columnPath :+ \"key\")\n          check(fromValue, toValue, columnPath :+ \"value\")\n\n        case (f @ StructType(fromFields), t @ StructType(toFields)) =>\n          val remainingFields = mutable.Set[StructField]()\n          remainingFields ++= fromFields\n          var addingColumns = false\n          toFields.foreach { toField =>\n            fromFields.find(field => resolver(field.name, toField.name)) match {\n              case Some(fromField) =>\n                remainingFields -= fromField\n\n                val newPath = columnPath :+ fromField.name\n                verifyNullability(fromField.nullable, toField.nullable, newPath)\n                check(fromField.dataType, toField.dataType, newPath)\n              case None =>\n                addingColumns = true\n                verify(toField.nullable,\n                  \"adding non-nullable column \" +\n                  UnresolvedAttribute(columnPath :+ toField.name).name)\n            }\n          }\n          val columnName = UnresolvedAttribute(columnPath).name\n          if (failOnAmbiguousChanges && remainingFields.nonEmpty && addingColumns) {\n            throw DeltaErrors.ambiguousDataTypeChange(columnName, f, t)\n          }\n          if (columnMappingMode == NoMapping) {\n            verify(remainingFields.isEmpty,\n              s\"dropping column(s) [${remainingFields.map(_.name).mkString(\", \")}]\" +\n                (if (columnPath.nonEmpty) s\" from $columnName\" else \"\"))\n          }\n\n        case (fromDataType: AtomicType, toDataType: AtomicType) if allowTypeWidening =>\n          verify(TypeWidening.isTypeChangeSupported(fromDataType, toDataType),\n            s\"changing data type of ${UnresolvedAttribute(columnPath).name} \" +\n              s\"from $fromDataType to $toDataType\")\n\n        case (fromDataType, toDataType) =>\n          verify(fromDataType == toDataType,\n            s\"changing data type of ${UnresolvedAttribute(columnPath).name} \" +\n              s\"from $fromDataType to $toDataType\")\n      }\n    }\n\n    try {\n      check(from, to, columnPath)\n      None\n    } catch {\n      case e: AnalysisException =>\n        Some(e.message)\n    }\n  }\n\n  /**\n   * Copy the nested data type between two data types in a char/varchar safe manner.\n   * See documentation of [[getRawFieldWithoutCharVarcharMetadata]] and\n   * [[setFieldDataTypeCharVarcharSafe]] for more context.\n   *\n   * This method uses [[getRawFieldWithoutCharVarcharMetadata]] on both the source and\n   * target fields to ensure that the metadata information is included in the data type\n   * before changing the data type. For example, to convert from a varchar(1) to varchar(10),\n   * we first change their representation:\n   *\n   * Source: (string, char_varchar_metadata = \"varchar(1)\")\n   *  -> (varchar(1), char_varchar_metadata = \"\")\n   * Target: (string, char_varchar_metadata = \"varchar(10)\")\n   *  -> (varchar(10), char_varchar_metadata = \"\")\n   *\n   * Then, we change the data type of the target to that of the source:\n   * (varchar(1), char_varchar_metadata = \"\") -> (varchar(10), char_varchar_metadata = \"\")\n   *\n   * Finally, we set the metadata back to the target:\n   * (varchar(10), char_varchar_metadata = \"\") -> (string, char_varchar_metadata = \"varchar(10)\")\n   */\n  def changeFieldDataTypeCharVarcharSafe(\n      fromField: StructField,\n      toField: StructField,\n      resolver: Resolver): StructField = {\n    val (safeFromField, safeToField) =\n      if (SparkSession.active.conf.get(DeltaSQLConf.DELTA_BYPASS_CHARVARCHAR_TO_STRING_FIX)) {\n        (fromField, toField)\n      } else {\n        (getRawFieldWithoutCharVarcharMetadata(fromField),\n         getRawFieldWithoutCharVarcharMetadata(toField))\n      }\n    val newDataType = SchemaUtils.changeDataType(\n      safeFromField.dataType, safeToField.dataType, resolver)\n    setFieldDataTypeCharVarcharSafe(fromField, newDataType)\n  }\n\n  /**\n   * Copy the nested data type between two data types.\n   */\n  def changeDataType(from: DataType, to: DataType, resolver: Resolver): DataType = {\n    (from, to) match {\n      case (ArrayType(fromElement, fn), ArrayType(toElement, _)) =>\n        ArrayType(changeDataType(fromElement, toElement, resolver), fn)\n\n      case (MapType(fromKey, fromValue, fn), MapType(toKey, toValue, _)) =>\n        MapType(\n          changeDataType(fromKey, toKey, resolver),\n          changeDataType(fromValue, toValue, resolver),\n          fn)\n\n      case (StructType(fromFields), StructType(toFields)) =>\n        StructType(\n          toFields.map { toField =>\n            fromFields.find(field => resolver(field.name, toField.name)).map { fromField =>\n              toField.getComment().map(fromField.withComment).getOrElse(fromField)\n                .copy(\n                  dataType = changeDataType(fromField.dataType, toField.dataType, resolver),\n                  nullable = toField.nullable)\n            }.getOrElse(toField)\n          }\n        )\n\n      case (_, toDataType) => toDataType\n    }\n  }\n\n  /**\n   * Runs the transform function `tf` on all nested StructTypes, MapTypes and ArrayTypes in the\n   * schema.\n   * If `colName` is defined, the transform function is only applied to all the fields with the\n   * given name. There may be multiple matches if nested fields with the same name exist in the\n   * schema, it is the responsibility of the caller to check the full field path before transforming\n   * a field.\n   * @param schema to transform.\n   * @param colName Optional name to match for\n   * @param tf function to apply on the StructType.\n   * @return the transformed schema.\n   */\n  def transformSchema(\n      schema: StructType,\n      colName: Option[String] = None)(\n      tf: (Seq[String], DataType, Resolver) => DataType): StructType = {\n    def transform[E <: DataType](path: Seq[String], dt: E): E = {\n      val newDt = dt match {\n        case struct @ StructType(fields) =>\n          val newStruct = if (colName.isEmpty || fields.exists(f => colName.contains(f.name))) {\n            tf(path, struct, DELTA_COL_RESOLVER).asInstanceOf[StructType]\n          } else {\n            struct\n          }\n\n          StructType(newStruct.fields.map { field =>\n            field.copy(dataType = transform(path :+ field.name, field.dataType))\n          })\n        case array: ArrayType =>\n          val newArray =\n            if (colName.isEmpty || colName.contains(\"element\")) {\n              tf(path, array, DELTA_COL_RESOLVER).asInstanceOf[ArrayType]\n            } else {\n              array\n            }\n          newArray.copy(elementType = transform(path :+ \"element\", newArray.elementType))\n        case map: MapType =>\n          val newMap =\n            if (colName.isEmpty || colName.contains(\"key\") || colName.contains(\"value\")) {\n              tf(path, map, DELTA_COL_RESOLVER).asInstanceOf[MapType]\n            } else {\n              map\n            }\n          newMap.copy(\n            keyType = transform(path :+ \"key\", newMap.keyType),\n            valueType = transform(path :+ \"value\", newMap.valueType))\n        case other => other\n      }\n      newDt.asInstanceOf[E]\n    }\n    transform(Seq.empty, schema)\n  }\n\n  /**\n   * Transform (nested) columns in a schema using the given path and parameter pairs. The transform\n   * function is only invoked when a field's path matches one of the input paths.\n   *\n   * @param schema to transform\n   * @param input paths and parameter pairs. The paths point to fields we want to transform. The\n   *              parameters will be passed to the transform function for a matching field.\n   * @param tf function to apply per matched field. This function takes the field path, the field\n   *           itself and the input names and payload pairs that matched the field name. It should\n   *           return a new field.\n   * @tparam E the type of the payload used for transforming fields.\n   * @return the transformed schema.\n   */\n  def transformColumns[E](\n      schema: StructType,\n      input: Seq[(Seq[String], E)])(\n      tf: (Seq[String], StructField, Seq[(Seq[String], E)]) => StructField): StructType = {\n    // scalastyle:off caselocale\n    val inputLookup = input.groupBy(_._1.map(_.toLowerCase))\n    SchemaMergingUtils.transformColumns(schema) { (path, field, resolver) =>\n      // Find the parameters that match this field name.\n      val fullPath = path :+ field.name\n      val normalizedFullPath = fullPath.map(_.toLowerCase)\n      val matches = inputLookup.get(normalizedFullPath).toSeq.flatMap {\n        // Keep only the input name(s) that actually match the field name(s). Note\n        // that the Map guarantees that the zipped sequences have the same size.\n        _.filter(_._1.zip(fullPath).forall(resolver.tupled))\n      }\n      if (matches.nonEmpty) {\n        tf(path, field, matches)\n      } else {\n        field\n      }\n    }\n    // scalastyle:on caselocale\n  }\n\n  /**\n   * Check if the schema contains invalid char in the column names depending on the mode.\n   */\n  def checkSchemaFieldNames(schema: StructType, columnMappingMode: DeltaColumnMappingMode): Unit = {\n    if (columnMappingMode != NoMapping) {\n      return\n    }\n    val invalidColumnNames =\n      findInvalidColumnNames(SchemaMergingUtils.explodeNestedFieldNames(schema))\n    if (invalidColumnNames.nonEmpty) {\n      throw DeltaErrors.foundInvalidCharsInColumnNames(invalidColumnNames)\n    }\n  }\n\n  /**\n   * Verifies that the column names are acceptable by Parquet and henceforth Delta. Parquet doesn't\n   * accept the characters ' ,;{}()\\n\\t='. We ensure that neither the data columns nor the partition\n   * columns have these characters.\n   */\n  def checkFieldNames(names: Seq[String]): Unit = {\n    val invalidColumnNames = findInvalidColumnNames(names)\n    if (invalidColumnNames.nonEmpty) {\n      throw DeltaErrors.invalidColumnName(invalidColumnNames.head)\n    }\n  }\n\n  /**\n   * Finds columns with invalid names, i.e. names containing any of the ' ,;{}()\\n\\t=' characters.\n   */\n  def findInvalidColumnNamesInSchema(schema: StructType): Seq[String] = {\n    findInvalidColumnNames(SchemaMergingUtils.explodeNestedFieldNames(schema))\n  }\n\n  private def findInvalidColumnNames(columnNames: Seq[String]): Seq[String] = {\n    val badChars = Seq(' ', ',', ';', '{', '}', '(', ')', '\\n', '\\t', '=')\n    columnNames.filter(colName => badChars.map(_.toString).exists(colName.contains))\n  }\n\n  /**\n   * Go through the schema to look for unenforceable NOT NULL constraints. By default we'll throw\n   * when they're encountered, but if this is suppressed through SQLConf they'll just be silently\n   * removed.\n   *\n   * Note that this should only be applied to schemas created from explicit user DDL - in other\n   * scenarios, the nullability information may be inaccurate and Delta should always coerce the\n   * nullability flag to true.\n   */\n  def removeUnenforceableNotNullConstraints(schema: StructType, conf: SQLConf): StructType = {\n    val allowUnenforceableNotNulls =\n      conf.getConf(DeltaSQLConf.ALLOW_UNENFORCED_NOT_NULL_CONSTRAINTS)\n\n    def checkField(path: Seq[String], f: StructField, r: Resolver): StructField = f match {\n      case StructField(name, ArrayType(elementType, containsNull), nullable, metadata) =>\n        val nullableElementType = SchemaUtils.typeAsNullable(elementType)\n        if (elementType != nullableElementType && !allowUnenforceableNotNulls) {\n          throw DeltaErrors.nestedNotNullConstraint(\n            prettyFieldName(path :+ f.name), elementType, nestType = \"element\")\n        }\n        StructField(\n          name, ArrayType(nullableElementType, containsNull), nullable, metadata)\n\n      case f @ StructField(\n          name, MapType(keyType, valueType, containsNull), nullable, metadata) =>\n        val nullableKeyType = SchemaUtils.typeAsNullable(keyType)\n        val nullableValueType = SchemaUtils.typeAsNullable(valueType)\n\n        if (keyType != nullableKeyType && !allowUnenforceableNotNulls) {\n          throw DeltaErrors.nestedNotNullConstraint(\n            prettyFieldName(path :+ f.name), keyType, nestType = \"key\")\n        }\n        if (valueType != nullableValueType && !allowUnenforceableNotNulls) {\n          throw DeltaErrors.nestedNotNullConstraint(\n            prettyFieldName(path :+ f.name), valueType, nestType = \"value\")\n        }\n\n        StructField(\n          name,\n          MapType(nullableKeyType, nullableValueType, containsNull),\n          nullable,\n          metadata)\n\n      case s: StructField => s\n    }\n\n    SchemaMergingUtils.transformColumns(schema)(checkField)\n  }\n\n  def fieldToColumn(field: StructField): Column = {\n    Column(UnresolvedAttribute.quoted(field.name))\n  }\n\n  /**  converting field name to column type with quoted back-ticks */\n  def fieldNameToColumn(field: String): Column = {\n    col(quoteIdentifier(field))\n  }\n  // Escapes back-ticks within the identifier name with double-back-ticks, and then quote the\n  // identifier with back-ticks.\n  def quoteIdentifier(part: String): String = s\"`${part.replace(\"`\", \"``\")}`\"\n\n  private def analyzeExpression(\n      spark: SparkSession,\n      expr: Expression,\n      schema: StructType): Expression = {\n    // Workaround for `exp` analyze\n    val relation = LocalRelation(schema)\n    val relationWithExp = Project(Seq(Alias(expr, \"validate_column\")()), relation)\n    val analyzedPlan = spark.sessionState.analyzer.execute(relationWithExp)\n    analyzedPlan.collectFirst {\n      case Project(Seq(a: Alias), _: LocalRelation) => a.child\n    }.get\n  }\n\n  /**\n   * Collects all attribute references in the given expression tree as a list of paths.\n   * In particular, generates paths for nested fields accessed using extraction expressions.\n   * For example:\n   * - GetStructField(AttributeReference(\"struct\"), \"a\") -> [\"struct.a\"]\n   * - Size(AttributeReference(\"array\")) -> [\"array\"]\n   */\n  private def collectUsedColumns(expression: Expression): Seq[Seq[String]] = {\n    val result = new collection.mutable.ArrayBuffer[Seq[String]]()\n\n    // Firstly, try to get referenced column for a child's expression.\n    // If it exists then we try to extend it by current expression.\n    // In case if we cannot extend one, we save the received column path (it's as long as possible).\n    def traverseAllPaths(exp: Expression): Option[Seq[String]] = exp match {\n      case GetStructField(child, _, Some(name)) => traverseAllPaths(child).map(_ :+ name)\n      case GetMapValue(child, key) =>\n        traverseAllPaths(key).foreach(result += _)\n        traverseAllPaths(child).map { childPath =>\n          result += childPath :+ \"key\"\n          childPath :+ \"value\"\n        }\n      case arrayExtract: GetArrayItem => traverseAllPaths(arrayExtract.child).map(_ :+ \"element\")\n      case arrayExtract: GetArrayStructFields =>\n        traverseAllPaths(arrayExtract.child).map(_ :+ \"element\" :+ arrayExtract.field.name)\n      case refCol: AttributeReference => Some(Seq(refCol.name))\n      case _ =>\n        exp.children.foreach(child => traverseAllPaths(child).foreach(result += _))\n        None\n    }\n\n    traverseAllPaths(expression).foreach(result += _)\n\n    result.toSeq\n  }\n\n  private def fallbackContainsDependentExpression(\n      expression: Expression,\n      columnToChange: Seq[String],\n      resolver: Resolver): Boolean = {\n    expression.foreach {\n      case refCol: UnresolvedAttribute =>\n        // columnToChange is the referenced column or its prefix\n        val prefixMatched = columnToChange.size <= refCol.nameParts.size &&\n          refCol.nameParts.zip(columnToChange).forall(pair => resolver(pair._1, pair._2))\n        if (prefixMatched) return true\n      case _ =>\n    }\n    false\n  }\n\n  /**\n   * Will a column change, e.g., rename, need to be populated to the expression. This is true when\n   * the column to change itself or any of its descendent column is referenced by expression.\n   * For example:\n   *  - a, length(a) -> true\n   *  - b, (b.c + 1) -> true, because renaming b1 will need to change the expr to (b1.c + 1).\n   *  - b.c, (cast b as string) -> true, because change b.c to b.c1 affects (b as string) result.\n   */\n  def containsDependentExpression(\n      spark: SparkSession,\n      columnToChange: Seq[String],\n      exprString: String,\n      schema: StructType,\n      resolver: Resolver): Boolean = {\n    val expression = spark.sessionState.sqlParser.parseExpression(exprString)\n    if (spark.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_CHANGE_COLUMN_CHECK_DEPENDENT_EXPRESSIONS_USE_V2)) {\n      try {\n        val analyzedExpr = analyzeExpression(spark, expression, schema)\n        val exprColumns = collectUsedColumns(analyzedExpr)\n        exprColumns.exists { exprColumn =>\n          // Changed column violates expression's column only when:\n          // 1) the changed column is a prefix of the referenced column,\n          // for example changing type of `col` affects `hash(col[0]) == 0`;\n          // 2) or the referenced column is a prefix of the changed column,\n          // for example changing type of `col.element` affects `concat_ws('', col) == 'abc'`;\n          // 3) or they are equal.\n          exprColumn.zip(columnToChange).forall {\n            case (exprFieldName, changedFieldName) => resolver(exprFieldName, changedFieldName)\n          }\n        }\n      } catch {\n        case NonFatal(e) =>\n          deltaAssert(\n            check = false,\n            name = \"containsDependentExpression.checkV2Error\",\n            msg = \"Exception during dependent expression V2 checking: \" + e.getMessage\n          )\n          fallbackContainsDependentExpression(expression, columnToChange, resolver)\n      }\n    } else {\n      fallbackContainsDependentExpression(expression, columnToChange, resolver)\n    }\n  }\n\n  /**\n   * Find the unsupported data type in a table schema. Return all columns that are using unsupported\n   * data types. For example,\n   * `findUnsupportedDataType(struct&lt;a: struct&lt;b: unsupported_type&gt;&gt;)` will return\n   * `Some(unsupported_type, Some(\"a.b\"))`.\n   */\n  def findUnsupportedDataTypes(schema: StructType): Seq[UnsupportedDataTypeInfo] = {\n    val unsupportedDataTypes = mutable.ArrayBuffer[UnsupportedDataTypeInfo]()\n    findUnsupportedDataTypesRecursively(unsupportedDataTypes, schema)\n    unsupportedDataTypes.toSeq\n  }\n\n  /**\n   * Find TimestampNTZ columns in the table schema.\n   */\n  def checkForTimestampNTZColumnsRecursively(schema: StructType): Boolean = {\n    SchemaUtils.typeExistsRecursively(schema)(_.isInstanceOf[TimestampNTZType])\n  }\n\n\n  /**\n   * Returns 'true' if any VariantType exists in the table schema.\n   */\n  def checkForVariantTypeColumnsRecursively(schema: StructType): Boolean = {\n    SchemaUtils.typeExistsRecursively(schema)(_.isInstanceOf[VariantType])\n  }\n\n  /**\n   * Find the unsupported data types in a `DataType` recursively. Add the unsupported data types to\n   * the provided `unsupportedDataTypes` buffer.\n   *\n   * @param unsupportedDataTypes the buffer to store the found unsupport data types and the column\n   *                             paths.\n   * @param dataType the data type to search.\n   * @param columnPath the column path to access the given data type. The callder should make sure\n   *                   `columnPath` is not empty when `dataType` is not `StructType`.\n   */\n  private def findUnsupportedDataTypesRecursively(\n      unsupportedDataTypes: mutable.ArrayBuffer[UnsupportedDataTypeInfo],\n      dataType: DataType,\n      columnPath: Seq[String] = Nil): Unit = dataType match {\n    case NullType =>\n    case BooleanType =>\n    case ByteType =>\n    case ShortType =>\n    case IntegerType =>\n    case dt: YearMonthIntervalType =>\n      assert(columnPath.nonEmpty, \"'columnPath' must not be empty\")\n      unsupportedDataTypes += UnsupportedDataTypeInfo(prettyFieldName(columnPath), dt)\n    case LongType =>\n    case dt: DayTimeIntervalType =>\n      assert(columnPath.nonEmpty, \"'columnPath' must not be empty\")\n      unsupportedDataTypes += UnsupportedDataTypeInfo(prettyFieldName(columnPath), dt)\n    case FloatType =>\n    case DoubleType =>\n    case StringType =>\n    case DateType =>\n    case TimestampType =>\n    case TimestampNTZType =>\n    case dt if dt.isInstanceOf[VariantType] =>\n    case BinaryType =>\n    case _: DecimalType =>\n    case a: ArrayType =>\n      assert(columnPath.nonEmpty, \"'columnPath' must not be empty\")\n      findUnsupportedDataTypesRecursively(\n        unsupportedDataTypes,\n        a.elementType,\n        columnPath.dropRight(1) :+ columnPath.last + \"[]\")\n    case m: MapType =>\n      assert(columnPath.nonEmpty, \"'columnPath' must not be empty\")\n      findUnsupportedDataTypesRecursively(\n        unsupportedDataTypes,\n        m.keyType,\n        columnPath.dropRight(1) :+ columnPath.last + \"[key]\")\n      findUnsupportedDataTypesRecursively(\n        unsupportedDataTypes,\n        m.valueType,\n        columnPath.dropRight(1) :+ columnPath.last + \"[value]\")\n    case s: StructType =>\n      s.fields.foreach { f =>\n        findUnsupportedDataTypesRecursively(\n          unsupportedDataTypes,\n          f.dataType,\n          columnPath :+ f.name)\n      }\n    case udt: UserDefinedType[_] =>\n      findUnsupportedDataTypesRecursively(unsupportedDataTypes, udt.sqlType, columnPath)\n    case dt: DataType =>\n      assert(columnPath.nonEmpty, \"'columnPath' must not be empty\")\n      unsupportedDataTypes += UnsupportedDataTypeInfo(prettyFieldName(columnPath), dt)\n  }\n\n  /**\n   * Find all the generated columns that depend on the given target column. Returns a map of\n   * generated names to their corresponding expression.\n   */\n  def findDependentGeneratedColumns(\n      sparkSession: SparkSession,\n      targetColumn: Seq[String],\n      protocol: Protocol,\n      schema: StructType): Map[String, String] = {\n    if (GeneratedColumn.satisfyGeneratedColumnProtocol(protocol) &&\n        GeneratedColumn.hasGeneratedColumns(schema)) {\n\n      val dependentGenCols = mutable.Map[String, String]()\n      SchemaMergingUtils.transformColumns(schema) { (_, field, _) =>\n        GeneratedColumn.getGenerationExpressionStr(field.metadata).foreach { exprStr =>\n          val needsToChangeExpr = SchemaUtils.containsDependentExpression(\n            sparkSession, targetColumn, exprStr, schema, sparkSession.sessionState.conf.resolver)\n          if (needsToChangeExpr) dependentGenCols += field.name -> exprStr\n        }\n        field\n      }\n      dependentGenCols.toMap\n    } else {\n      Map.empty\n    }\n  }\n\n  /** Recursively find all types not defined in Delta protocol but used in `dt` */\n  def findUndefinedTypes(dt: DataType): Seq[DataType] = dt match {\n    // Types defined in Delta protocol\n    case NullType => Nil\n    case BooleanType => Nil\n    case ByteType | ShortType | IntegerType | LongType => Nil\n    case FloatType | DoubleType | _: DecimalType => Nil\n    case StringType | BinaryType => Nil\n    case DateType | TimestampType => Nil\n    // Recursively search complex data types\n    case s: StructType => s.fields.flatMap(f => findUndefinedTypes(f.dataType))\n    case a: ArrayType => findUndefinedTypes(a.elementType)\n    case m: MapType => findUndefinedTypes(m.keyType) ++ findUndefinedTypes(m.valueType)\n    // Other types are not defined in Delta protocol\n    case undefinedType => Seq(undefinedType)\n  }\n\n  /** Record all types not defined in Delta protocol but used in the `schema`. */\n  def recordUndefinedTypes(deltaLog: DeltaLog, schema: StructType): Unit = {\n    try {\n      findUndefinedTypes(schema).map(_.getClass.getName).toSet.foreach { className: String =>\n        recordDeltaEvent(deltaLog, \"delta.undefined.type\", data = Map(\"className\" -> className))\n      }\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Failed to log undefined types for table \" +\n          log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)}\", e)\n    }\n  }\n\n  // Helper method to validate that two logical column names are equal using the Delta column\n  // resolver (case insensitive comparison).\n  def areLogicalNamesEqual(col1: Seq[String], col2: Seq[String]): Boolean = {\n    col1.length == col2.length && col1.zip(col2).forall(DELTA_COL_RESOLVER.tupled)\n  }\n\n  def removeExistsDefaultMetadata(schema: StructType): StructType = {\n    // 'EXISTS_DEFAULT' is not used in Delta because it is not allowed to add a column with a\n    // default value. Spark does though still add the metadata key when a column with a default\n    // value is added at table creation.\n    // We remove the metadata field here because it is not part of the Delta protocol and\n    // having it in the schema prohibits CTAS from a table with a dropped default value.\n    // @TODO: Clarify if active default values should be propagated to the target table in CTAS or\n    //        not and if not also remove 'CURRENT_DEFAULT' in CTAS.\n    SchemaUtils.transformSchema(schema) {\n      case (_, StructType(fields), _)\n        if fields.exists(_.metadata.contains(\n          ResolveDefaultColumnsUtils.EXISTS_DEFAULT_COLUMN_METADATA_KEY)) =>\n        val newFields = fields.map { field =>\n          val builder = new MetadataBuilder()\n            .withMetadata(field.metadata)\n            .remove(ResolveDefaultColumnsUtils.EXISTS_DEFAULT_COLUMN_METADATA_KEY)\n\n          field.copy(metadata = builder.build())\n        }\n        StructType(newFields)\n      case (_, other, _) => other\n    }\n  }\n\n  /**\n   * Renames a column in the metadata, given the old column path, new column path, and an optional\n   * list of column names. If the column names are provided, they will be updated to reflect the\n   * new path.\n   *\n   * @param oldColumnPath The original physical name path of the column to be renamed.\n   * @param newColumnPath The new physical name path for the column.\n   * @param columnNameOpt An optional sequence of unresolved attributes representing the column\n   *                      logical name.\n   * @param deltaConfig   The configuration key for columns that need to be renamed from metadata.\n   * @return              A map containing the updated Delta configuration with new column paths.\n   */\n  def renameColumnForConfig(\n      oldColumnPath: Seq[String],\n      newColumnPath: Seq[String],\n      columnNameOpt: Option[Seq[UnresolvedAttribute]],\n      deltaConfig: String): Map[String, String] = {\n    columnNameOpt.map { deltaColumnsNames =>\n      val deltaColumnsPath = deltaColumnsNames\n        .map(_.nameParts)\n        .map { attributeNameParts =>\n          val commonPrefix = oldColumnPath.zip(attributeNameParts)\n            .takeWhile { case (left, right) => left == right }\n            .size\n          if (commonPrefix == oldColumnPath.size) {\n            newColumnPath ++ attributeNameParts.takeRight(attributeNameParts.size - commonPrefix)\n          } else {\n            attributeNameParts\n          }\n        }\n        .map(columnParts =>\n          if (SparkSession.active.conf.get(DeltaSQLConf.DELTA_RENAME_COLUMN_ESCAPE_NAME)) {\n            UnresolvedAttribute(columnParts).sql\n          } else {\n            UnresolvedAttribute(columnParts).name\n          }\n        )\n      Map(deltaConfig -> deltaColumnsPath.mkString(\",\"))\n    }.getOrElse(Map.empty[String, String])\n  }\n}\n\n/**\n * The information of unsupported data type returned by [[SchemaUtils.findUnsupportedDataTypes]].\n *\n * @param column the column path to access the column using an unsupported data type, such as `a.b`.\n * @param dataType the unsupported data type.\n */\ncase class UnsupportedDataTypeInfo(column: String, dataType: DataType)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlannedTable.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport java.util\nimport java.util.Locale\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.connector.catalog.{SupportsRead, Table, TableCapability}\nimport org.apache.spark.sql.connector.read._\nimport org.apache.spark.sql.execution.datasources.{FileFormat, PartitionedFile}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat\nimport org.apache.spark.sql.sources.{And, Filter}\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.sql.util.{CaseInsensitiveStringMap, SchemaUtils}\n\n/**\n * Companion object for ServerSidePlannedTable with factory methods.\n */\nobject ServerSidePlannedTable extends DeltaLogging {\n  /**\n   * Determine if server-side planning should be used based on catalog type,\n   * credential availability, and configuration.\n   *\n   * Decision logic:\n   * - Requires enableServerSidePlanning flag to be enabled (prevents accidental enablement)\n   * - In production: Also requires Unity Catalog table that lacks credentials\n   * - In test mode: Only requires the enable flag (allows testing without UC setup)\n   * - Otherwise use normal table loading path\n   *\n   * The logic is: ((isUnityCatalog && !hasCredentials) || skipUCRequirementForTests) && enableFlag\n   *\n   * @param isUnityCatalog Whether this is a Unity Catalog instance\n   * @param hasCredentials Whether the table has credentials available\n   * @param enableServerSidePlanning Whether to enable server-side planning (config flag)\n   * @param skipUCRequirementForTests Whether to skip Unity Catalog requirement for testing\n   *                                   with non-UC tables\n   * @return true if server-side planning should be used\n   */\n  private[serverSidePlanning] def shouldUseServerSidePlanning(\n      isUnityCatalog: Boolean,\n      hasCredentials: Boolean,\n      enableServerSidePlanning: Boolean,\n      skipUCRequirementForTests: Boolean): Boolean = {\n    ((isUnityCatalog && !hasCredentials) || skipUCRequirementForTests) && enableServerSidePlanning\n  }\n\n  /**\n   * Try to create a ServerSidePlannedTable if server-side planning is needed.\n   * Returns None if not needed or if the planning client factory is not available.\n   *\n   * This method encapsulates all the logic to decide whether to use server-side planning:\n   * - Checks if Unity Catalog table lacks credentials\n   * - Checks if server-side planning is enabled via config (required for all cases)\n   * - In test mode, Unity Catalog check is bypassed to allow testing\n   * - Extracts catalog name and table identifiers\n   * - Attempts to create the planning client\n   *\n   * Test coverage: ServerSidePlanningSuite tests verify the decision logic through\n   * shouldUseServerSidePlanning() method with different input combinations.\n   *\n   * @param spark The SparkSession\n   * @param ident The table identifier\n   * @param table The loaded table from the delegate catalog\n   * @param isUnityCatalog Whether this is a Unity Catalog instance\n   * @return Some(ServerSidePlannedTable) if server-side planning should be used, None otherwise\n   */\n  def tryCreate(\n      spark: SparkSession,\n      ident: Identifier,\n      table: Table,\n      isUnityCatalog: Boolean): Option[ServerSidePlannedTable] = {\n    // Check if we should enable server-side planning (for testing)\n    val enableServerSidePlanning =\n      spark.conf.get(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key, \"false\").toBoolean\n    val hasTableCredentials = hasCredentials(table)\n\n    // Check if we should use server-side planning\n    if (shouldUseServerSidePlanning(\n        isUnityCatalog, hasTableCredentials, enableServerSidePlanning,\n        skipUCRequirementForTests = DeltaUtils.isTesting)) {\n      val namespace = ident.namespace().mkString(\".\")\n      val tableName = ident.name()\n\n      // Create metadata from table\n      val metadata = ServerSidePlanningMetadata.fromTable(table, spark, ident, isUnityCatalog)\n\n      // Try to create ServerSidePlannedTable with server-side planning\n      val plannedTable = tryCreate(spark, namespace, tableName, table.schema(), metadata)\n      if (plannedTable.isEmpty) {\n        logWarning(\n          s\"Server-side planning not available for catalog ${metadata.catalogName}. \" +\n            \"Falling back to normal table loading.\")\n      }\n      plannedTable\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Try to create a ServerSidePlannedTable with server-side planning.\n   * Returns None if the planning client factory is not available.\n   *\n   * @param spark The SparkSession\n   * @param databaseName The database name (may include catalog prefix)\n   * @param tableName The table name\n   * @param tableSchema The table schema\n   * @param metadata Metadata extracted from loadTable response\n   * @return Some(ServerSidePlannedTable) if successful, None if factory not registered\n   */\n  private def tryCreate(\n      spark: SparkSession,\n      databaseName: String,\n      tableName: String,\n      tableSchema: StructType,\n      metadata: ServerSidePlanningMetadata): Option[ServerSidePlannedTable] = {\n    try {\n      val client = ServerSidePlanningClientFactory.buildClient(spark, metadata)\n      Some(new ServerSidePlannedTable(spark, databaseName, tableName, tableSchema, client))\n    } catch {\n      case _: IllegalStateException =>\n        // Factory not registered - this shouldn't happen in production but could during testing\n        None\n    }\n  }\n\n  /**\n   * Check if a table has credentials available.\n   * UC injects credentials as table properties with \"option.fs.*\" prefix for filesystem configs.\n   * See: CredPropsUtil in UCSingleCatalog.\n   */\n  private def hasCredentials(table: Table): Boolean = {\n    val properties = table.properties()\n    val keys = properties.keySet()\n    val iter = keys.iterator()\n    while (iter.hasNext) {\n      if (iter.next().startsWith(\"option.fs.\")) {\n        return true\n      }\n    }\n    false\n  }\n}\n\n/**\n * A Spark Table implementation that uses server-side scan planning\n * to get the list of files to read. Used as a fallback when Unity Catalog\n * doesn't provide credentials.\n *\n * Similar to DeltaTableV2, we accept SparkSession as a constructor parameter\n * since Tables are created on the driver and are not serialized to executors.\n */\nclass ServerSidePlannedTable(\n    spark: SparkSession,\n    databaseName: String,\n    tableName: String,\n    tableSchema: StructType,\n    planningClient: ServerSidePlanningClient)\n    extends Table with SupportsRead with AutoCloseable with DeltaLogging {\n\n  // Returns fully qualified name (e.g., \"catalog.database.table\").\n  // The databaseName parameter receives ident.namespace().mkString(\".\") from DeltaCatalog,\n  // which includes the catalog name when present, similar to DeltaTableV2's name() method.\n  override def name(): String = s\"$databaseName.$tableName\"\n\n  override def schema(): StructType = tableSchema\n\n  override def capabilities(): util.Set[TableCapability] = {\n    Set(TableCapability.BATCH_READ).asJava\n  }\n\n  override def newScanBuilder(options: CaseInsensitiveStringMap): ScanBuilder = {\n    new ServerSidePlannedScanBuilder(spark, databaseName, tableName, tableSchema, planningClient)\n  }\n\n  override def close(): Unit = {\n    planningClient.close()\n  }\n}\n\n/**\n * ScanBuilder that uses ServerSidePlanningClient to plan the scan.\n * Implements SupportsPushDownFilters to enable WHERE clause pushdown to the server.\n * Implements SupportsPushDownRequiredColumns to enable column pruning pushdown to the server.\n * Implements SupportsPushDownLimit to enable LIMIT pushdown to the server.\n */\nclass ServerSidePlannedScanBuilder(\n    spark: SparkSession,\n    databaseName: String,\n    tableName: String,\n    tableSchema: StructType,\n    planningClient: ServerSidePlanningClient)\n  extends ScanBuilder\n  with SupportsPushDownFilters\n  with SupportsPushDownRequiredColumns\n  with SupportsPushDownLimit\n  with DeltaLogging {\n\n  // Filters that have been pushed down and will be sent to the server\n  private var _pushedFilters: Array[Filter] = Array.empty\n\n  // Required schema (columns to read). Defaults to full table schema.\n  private var _requiredSchema: StructType = tableSchema\n\n  // Limit that has been pushed down. None means no limit.\n  private var _limit: Option[Int] = None\n\n  /**\n   * Push filters to the server-side planning client.\n   *\n   * Strategy:\n   * - If ALL filters convert to server's native format: Returns empty array (no residuals)\n   *   This enables Spark to push down LIMIT in addition to filters\n   * - If ANY filter fails conversion: Returns all filters as residuals\n   *   This falls back to safety mode where Spark re-applies all filters locally\n   *\n   * The server receives converted filters in both cases, but residuals provide a safety net\n   * for correctness if the server silently ignores unsupported filters.\n   */\n  override def pushFilters(filters: Array[Filter]): Array[Filter] = {\n    // Store filters to send to IRC server\n    _pushedFilters = filters\n\n    // Strategy: Check if all filters can be converted upfront\n    // Case 1: ALL convert -> return empty residuals -> enables filter+limit pushdown\n    // Case 2: ANY fails -> return all residuals -> only filter pushdown (safety mode)\n\n    if (filters.isEmpty) {\n      // No filters to push\n      return Array.empty\n    }\n\n    // Check if all filters are convertible\n    val allConvertible = planningClient.canConvertFilters(filters)\n\n    if (allConvertible) {\n      // All filters successfully converted to server's native format\n      // Trust that the server can handle them - return no residuals\n      // This enables Spark to call pushLimit() for combined filter+limit pushdown\n      logInfo(s\"All ${filters.length} filters convertible, \" +\n              \"returning empty residuals to enable limit pushdown\")\n      Array.empty\n    } else {\n      // At least one filter failed to convert\n      // Return all filters as residuals for safety (Spark will re-apply)\n      // Note: Server will still receive converted filters, but Spark provides safety net\n      logWarning(s\"Some filters failed to convert, \" +\n                 \"returning all as residuals (limit pushdown disabled)\")\n      filters\n    }\n  }\n\n  override def pushedFilters(): Array[Filter] = _pushedFilters\n\n  override def pruneColumns(requiredSchema: StructType): Unit = {\n    _requiredSchema = requiredSchema\n  }\n\n  override def pushLimit(limit: Int): Boolean = {\n    _limit = Some(limit)\n    true  // Return true to indicate the limit is fully pushed down to the server\n  }\n\n  override def isPartiallyPushed(): Boolean = {\n    // Return true if we have a limit - indicates partial pushdown so Spark applies it too\n    _limit.isDefined\n  }\n\n  override def build(): Scan = {\n    new ServerSidePlannedScan(\n      spark, databaseName, tableName, tableSchema, planningClient, _pushedFilters, _requiredSchema,\n      _limit)\n  }\n}\n\n/**\n * Scan implementation that calls the server-side planning API to get file list.\n */\nclass ServerSidePlannedScan(\n    spark: SparkSession,\n    databaseName: String,\n    tableName: String,\n    tableSchema: StructType,\n    planningClient: ServerSidePlanningClient,\n    pushedFilters: Array[Filter],\n    requiredSchema: StructType,\n    limit: Option[Int]) extends Scan with Batch {\n\n  override def readSchema(): StructType = requiredSchema\n\n  override def toBatch: Batch = this\n\n  // Convert pushed filters to a single Spark Filter for the API call.\n  // If no filters, pass None. If filters exist, combine them into a single filter.\n  private val combinedFilter: Option[Filter] = {\n    if (pushedFilters.isEmpty) {\n      None\n    } else if (pushedFilters.length == 1) {\n      Some(pushedFilters.head)\n    } else {\n      // Combine multiple filters with And\n      Some(pushedFilters.reduce((left, right) => And(left, right)))\n    }\n  }\n\n  // Only pass projection if columns are actually pruned (not SELECT *)\n  // Extract field names for planning client (server only needs names, not types)\n  // Use Spark's SchemaUtils.explodeNestedFieldNames to flatten and escape field names,\n  // then filter out parent structs by keeping only fields that have no children.\n  // For example, for schema STRUCT<a: STRUCT<b.c: STRING>>:\n  //   - explodeNestedFieldNames returns: [\"a\", \"a.`b.c`\"]\n  //   - We filter to leaf fields only: [\"a.`b.c`\"]\n  // This ensures projections only include actual data columns, not parent containers.\n  private val projectionColumnNames: Option[Seq[String]] = {\n    if (requiredSchema.fieldNames.toSet == tableSchema.fieldNames.toSet) {\n      None\n    } else {\n      val allFields = SchemaUtils.explodeNestedFieldNames(requiredSchema)\n      Some(allFields.filter { field =>\n        !allFields.exists(other => other.startsWith(field + \".\"))\n      })\n    }\n  }\n\n  // Call the server-side planning API to get the scan plan with files AND credentials.\n  // Close the client after planning - the scan plan contains all data needed for partition\n  // creation and reading, so the client (and its HTTP connection) is no longer needed.\n  private lazy val scanPlan: ScanPlan = {\n    val plan = planningClient.planScan(\n      databaseName,\n      tableName,\n      combinedFilter,\n      projectionColumnNames,\n      limit)\n    planningClient.close()\n    plan\n  }\n\n  // Explicitly signal that columnar is unsupported to prevent early enumeration of the partitions\n  override def columnarSupportMode(): Scan.ColumnarSupportMode =\n    Scan.ColumnarSupportMode.UNSUPPORTED\n\n  override def planInputPartitions(): Array[InputPartition] = {\n    // Convert each file to an InputPartition\n    scanPlan.files.map { file =>\n      ServerSidePlannedFileInputPartition(file.filePath, file.fileSizeInBytes, file.fileFormat)\n    }.toArray\n  }\n\n  override def createReaderFactory(): PartitionReaderFactory = {\n    new ServerSidePlannedFilePartitionReaderFactory(\n      spark, tableSchema, requiredSchema, scanPlan.credentials)\n  }\n}\n\n/**\n * InputPartition representing a single file from the server-side scan plan.\n */\ncase class ServerSidePlannedFileInputPartition(\n    filePath: String,\n    fileSizeInBytes: Long,\n    fileFormat: String) extends InputPartition\n\n/**\n * Factory for creating PartitionReaders that read server-side planned files.\n * Builds reader functions on the driver for Parquet files.\n *\n * @param tableSchema The full table schema (all columns in the file)\n * @param requiredSchema The required schema (columns to read after projection pushdown)\n * @param credentials Optional storage credentials from server-side planning response\n */\nclass ServerSidePlannedFilePartitionReaderFactory(\n    spark: SparkSession,\n    tableSchema: StructType,\n    requiredSchema: StructType,\n    credentials: Option[ScanPlanStorageCredentials])\n    extends PartitionReaderFactory {\n\n  import org.apache.spark.util.SerializableConfiguration\n\n  // scalastyle:off deltahadoopconfiguration\n  // We use sessionState.newHadoopConf() here instead of deltaLog.newDeltaHadoopConf().\n  // This means DataFrame options (like custom S3 credentials) passed by users will NOT be\n  // included in the Hadoop configuration. This is intentional:\n  // - Server-side planning uses server-provided credentials, not user-specified credentials\n  // - ServerSidePlannedTable is NOT a Delta table, so we don't want Delta-specific options\n  //   from deltaLog.newDeltaHadoopConf()\n  // - General Spark options from spark.hadoop.* are included and work for all tables\n  private val hadoopConf = {\n    val conf = spark.sessionState.newHadoopConf()\n\n    // Inject temporary credentials from IRC server response.\n    // Disable FileSystem cache for S3, Azure, and GCS so each scan uses fresh credentials\n    // (avoids AccessDenied when temp creds expire and a cached FS is reused).\n    // Aligns with CredPropsUtil in the Unity Catalog connector.\n    credentials.foreach(_.configure(conf))\n\n    new SerializableConfiguration(conf)\n  }\n  // scalastyle:on deltahadoopconfiguration\n\n  // Pre-build reader function for Parquet on the driver\n  // This function will be serialized and sent to executors\n  // tableSchema: All columns in the file (full table schema)\n  // requiredSchema: Columns to actually read (after projection pushdown)\n  private val parquetReaderBuilder = new ParquetFileFormat().buildReaderWithPartitionValues(\n    sparkSession = spark,\n    dataSchema = tableSchema,\n    partitionSchema = StructType(Nil),\n    requiredSchema = requiredSchema,\n    filters = Seq.empty,\n    options = Map(\n      FileFormat.OPTION_RETURNING_BATCH -> \"false\"\n    ),\n    hadoopConf = hadoopConf.value\n  )\n\n  override def createReader(partition: InputPartition): PartitionReader[InternalRow] = {\n    val filePartition = partition.asInstanceOf[ServerSidePlannedFileInputPartition]\n\n    // Verify file format is Parquet\n    // Scalastyle suppression needed: the caselocale regex incorrectly flags even correct usage\n    // of toLowerCase(Locale.ROOT). Similar to PartitionUtils.scala and SchemaUtils.scala.\n    // scalastyle:off caselocale\n    if (filePartition.fileFormat.toLowerCase(Locale.ROOT) != \"parquet\") {\n    // scalastyle:on caselocale\n      throw new UnsupportedOperationException(\n        s\"File format '${filePartition.fileFormat}' is not supported. Only Parquet is supported.\")\n    }\n\n    new ServerSidePlannedFilePartitionReader(filePartition, parquetReaderBuilder)\n  }\n}\n\n/**\n * PartitionReader that reads a single file using a pre-built reader function.\n * The reader function was created on the driver and is executed on the executor.\n */\nclass ServerSidePlannedFilePartitionReader(\n    partition: ServerSidePlannedFileInputPartition,\n    readerBuilder: PartitionedFile => Iterator[InternalRow])\n    extends PartitionReader[InternalRow] {\n\n  // Create PartitionedFile for this file\n  private val partitionedFile = PartitionedFile(\n    partitionValues = InternalRow.empty,\n    filePath = SparkPath.fromPathString(partition.filePath),\n    start = 0,\n    length = partition.fileSizeInBytes\n  )\n\n  // Track the iterator so we can close it properly\n  // Using Option to avoid initializing the iterator if close() is called before next()\n  private var readerIterator: Option[Iterator[InternalRow]] = None\n\n  // Get or create the reader iterator\n  private def getIterator: Iterator[InternalRow] = {\n    readerIterator.getOrElse {\n      val iter = readerBuilder(partitionedFile)\n      readerIterator = Some(iter)\n      iter\n    }\n  }\n\n  override def next(): Boolean = {\n    getIterator.hasNext\n  }\n\n  override def get(): InternalRow = {\n    getIterator.next()\n  }\n\n  override def close(): Unit = {\n    // Close the iterator if it implements AutoCloseable (which Parquet iterators do)\n    readerIterator.foreach {\n      case closeable: AutoCloseable => closeable.close()\n      case _ => // No cleanup needed\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlanningClient.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.sources.Filter\n\n/**\n * Simple data class representing a file to scan.\n * No dependencies on Iceberg types.\n */\ncase class ScanFile(\n  filePath: String,\n  fileSizeInBytes: Long,\n  fileFormat: String  // \"parquet\", \"orc\", etc.\n)\n\n/**\n * Interface for planning table scans via server-side planning.\n * This interface uses Spark's standard `org.apache.spark.sql.sources.Filter` as the universal\n * representation for filter pushdown. This keeps the interface catalog-agnostic while allowing\n * each server-side planning catalog implementation to convert filters to their own native format.\n */\ntrait ServerSidePlanningClient extends AutoCloseable {\n  /**\n   * Plan a table scan and return the list of files to read.\n   *\n   * @param databaseName The database or schema name\n   * @param table The table name\n   * @param filterOption Optional filter expression to push down to server (Spark Filter format)\n   * @param projectionOption Optional projection (column names) to push down to server\n   * @param limitOption Optional limit to push down to server\n   * @return ScanPlan containing files to read\n   */\n  def planScan(\n      databaseName: String,\n      table: String,\n      filterOption: Option[Filter] = None,\n      projectionOption: Option[Seq[String]] = None,\n      limitOption: Option[Int] = None): ScanPlan\n\n  /**\n   * Check if all given filters can be converted to the server's native filter format.\n   * This is used during filter pushdown to determine whether to return residuals to Spark.\n   *\n   * @param filters Array of Spark filters to check\n   * @return true if ALL filters can be converted, false if ANY filter cannot be converted\n   */\n  def canConvertFilters(filters: Array[Filter]): Boolean\n\n  /**\n   * Close any resources held by this client.\n   * Default implementation is a no-op for clients that don't hold resources.\n   */\n  override def close(): Unit = {}\n}\n\n/**\n * Factory for creating ServerSidePlanningClient instances.\n * This allows for configurable implementations (REST, mock, Spark-based, etc.)\n */\nprivate[serverSidePlanning] trait ServerSidePlanningClientFactory {\n  /**\n   * Create a client using metadata necessary for server-side planning.\n   *\n   * @param spark The SparkSession\n   * @param metadata Metadata necessary for server-side planning\n   * @return A ServerSidePlanningClient configured with the metadata\n   */\n  def buildClient(\n      spark: SparkSession,\n      metadata: ServerSidePlanningMetadata): ServerSidePlanningClient\n}\n\n/**\n * Registry for client factories. Automatically discovers and registers implementations\n * using reflection-based auto-discovery on first access to the factory. Manual registration\n * using setFactory() is only needed for testing or to override the auto-discovered factory.\n */\nprivate[serverSidePlanning] object ServerSidePlanningClientFactory {\n  // Fully qualified class name for auto-registration via reflection\n  private val ICEBERG_FACTORY_CLASS_NAME =\n    \"org.apache.spark.sql.delta.serverSidePlanning.IcebergRESTCatalogPlanningClientFactory\"\n\n  @volatile private var registeredFactory: Option[ServerSidePlanningClientFactory] = None\n  @volatile private var autoRegistrationAttempted: Boolean = false\n\n  // Lazy initialization - only runs when getFactory() is called and no factory is set.\n  // Uses reflection to load the hardcoded IcebergRESTCatalogPlanningClientFactory class.\n  private def tryAutoRegisterFactory(): Unit = {\n    // Double-checked locking pattern to ensure initialization happens only once\n    if (!autoRegistrationAttempted) {\n      synchronized {\n        if (!autoRegistrationAttempted) {\n          autoRegistrationAttempted = true\n\n          try {\n            // Use reflection to load the Iceberg factory class\n            // scalastyle:off classforname\n            val clazz = Class.forName(ICEBERG_FACTORY_CLASS_NAME)\n            // scalastyle:on classforname\n            val factory = clazz.getConstructor().newInstance()\n              .asInstanceOf[ServerSidePlanningClientFactory]\n            registeredFactory = Some(factory)\n          } catch {\n            case e: Exception =>\n              throw new IllegalStateException(\n                \"No ServerSidePlanningClientFactory has been registered. \" +\n                \"Ensure delta-iceberg JAR is on the classpath for auto-registration, \" +\n                \"or call ServerSidePlanningClientFactory.setFactory() to register manually.\",\n                e)\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Set a factory, overriding any auto-registered factory.\n   * Synchronized to prevent race conditions with auto-registration.\n   */\n  private[serverSidePlanning] def setFactory(factory: ServerSidePlanningClientFactory): Unit = {\n    synchronized {\n      registeredFactory = Some(factory)\n    }\n  }\n\n  /**\n   * Clear the registered factory.\n   * Synchronized to ensure atomic reset of both flags.\n   */\n  private[serverSidePlanning] def clearFactory(): Unit = {\n    synchronized {\n      registeredFactory = None\n      autoRegistrationAttempted = false\n    }\n  }\n\n  /**\n   * Get the currently registered factory.\n   * Throws IllegalStateException if no factory has been registered (either via reflection-based\n   * auto-discovery or explicit setFactory() call).\n   */\n  def getFactory(): ServerSidePlanningClientFactory = {\n    // Try auto-registration if not already attempted and no factory is manually set\n    if (registeredFactory.isEmpty) {\n      tryAutoRegisterFactory()\n    }\n\n    registeredFactory.getOrElse {\n      throw new IllegalStateException(\n        \"No ServerSidePlanningClientFactory has been registered. \" +\n        \"Ensure delta-iceberg JAR is on the classpath for auto-registration, \" +\n        \"or call ServerSidePlanningClientFactory.setFactory() to register manually.\")\n    }\n  }\n\n  /**\n   * Convenience method to create a client from metadata using the registered factory.\n   */\n  def buildClient(\n      spark: SparkSession,\n      metadata: ServerSidePlanningMetadata): ServerSidePlanningClient = {\n    getFactory().buildClient(spark, metadata)\n  }\n}\n\n/**\n * Functional interface for applying storage credentials to a Hadoop configuration.\n * Implementations are responsible for setting the appropriate Hadoop config keys\n * for their respective cloud provider.\n */\ntrait ScanPlanStorageCredentials {\n  def configure(conf: org.apache.hadoop.conf.Configuration): Unit\n}\n\n/**\n * Result of a table scan plan operation.\n */\ncase class ScanPlan(\n    files: Seq[ScanFile],\n    credentials: Option[ScanPlanStorageCredentials] = None)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlanningMetadata.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.connector.catalog.{Identifier, Table}\n\n/**\n * Metadata required for creating a server-side planning client.\n *\n * This interface captures all information from the catalog's loadTable response\n * that is needed to create and configure a ServerSidePlanningClient.\n */\nprivate[serverSidePlanning] trait ServerSidePlanningMetadata {\n  /**\n   * The base URI for the planning endpoint.\n   */\n  def planningEndpointUri: String\n\n  /**\n   * Authentication token for the planning endpoint.\n   */\n  def authToken: Option[String]\n\n  /**\n   * Catalog name for configuration lookups.\n   */\n  def catalogName: String\n\n  /**\n   * Additional table properties that may be needed.\n   * For example, table UUID, credential hints, etc.\n   */\n  def tableProperties: Map[String, String]\n}\n\n/**\n * Default metadata for non-UC catalogs.\n * Used when server-side planning is force-enabled for testing/development.\n */\nprivate[serverSidePlanning] case class DefaultMetadata(\n    catalogName: String,\n    tableProps: Map[String, String] = Map.empty) extends ServerSidePlanningMetadata {\n  override def planningEndpointUri: String = \"\"\n  override def authToken: Option[String] = None\n  override def tableProperties: Map[String, String] = tableProps\n}\n\nobject ServerSidePlanningMetadata {\n  /**\n   * Create metadata from a loaded table.\n   *\n   * Returns UnityCatalogMetadata for Unity Catalog tables, or DefaultMetadata otherwise.\n   */\n  def fromTable(\n      table: Table,\n      spark: SparkSession,\n      ident: Identifier,\n      isUnityCatalog: Boolean): ServerSidePlanningMetadata = {\n\n    if (isUnityCatalog) {\n      UnityCatalogMetadata.fromTable(table, spark, ident)\n    } else {\n      val catalogName = extractCatalogName(ident)\n      DefaultMetadata(catalogName, Map.empty)\n    }\n  }\n\n  private def extractCatalogName(ident: Identifier): String = {\n    if (ident.namespace().length > 1) {\n      ident.namespace().head\n    } else {\n      \"spark_catalog\"\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/UnityCatalogMetadata.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.connector.catalog.{Identifier, Table}\n\n/**\n * Metadata for Unity Catalog tables.\n * Provides base Iceberg REST endpoint for server-side planning.\n */\ncase class UnityCatalogMetadata(\n    catalogName: String,\n    ucUri: String,\n    ucToken: String,\n    tableProps: Map[String, String]) extends ServerSidePlanningMetadata {\n\n  override def planningEndpointUri: String = {\n    // Return base Iceberg REST path up to /v1/\n    // The IcebergRESTCatalogPlanningClient will call /v1/config to get the prefix\n    // and construct the full URL according to the Iceberg REST catalog spec\n    val base = if (ucUri.endsWith(\"/\")) ucUri.dropRight(1) else ucUri\n    s\"$base/api/2.1/unity-catalog/iceberg-rest/v1\"\n  }\n\n  override def authToken: Option[String] = Some(ucToken)\n\n  override def tableProperties: Map[String, String] = tableProps\n}\n\nobject UnityCatalogMetadata {\n  def fromTable(\n      table: Table,\n      spark: SparkSession,\n      ident: Identifier): UnityCatalogMetadata = {\n\n    val catalogName = if (ident.namespace().length > 1) {\n      ident.namespace().head\n    } else {\n      // Use current catalog from session\n      // This allows queries with 2-part names (schema.table) to work with Unity Catalog\n      spark.sessionState.catalogManager.currentCatalog.name()\n    }\n\n    // Read UC configuration from Spark conf\n    val ucUri = spark.conf.get(s\"spark.sql.catalog.$catalogName.uri\", \"\")\n    val ucToken = spark.conf.get(s\"spark.sql.catalog.$catalogName.token\", \"\")\n\n    // Table properties currently unused, may be needed in future\n    val tableProps = Map.empty[String, String]\n\n    UnityCatalogMetadata(catalogName, ucUri, ucToken, tableProps)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/MultiDimClustering.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.skipping.MultiDimClusteringFunctions._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types._\n\n/** Trait for changing the data layout using a multi-dimensional clustering algorithm */\ntrait MultiDimClustering extends Logging {\n  /** Repartition the given `df` into `approxNumPartitions` based on the provided `colNames`. */\n  def cluster(\n      df: DataFrame,\n      colNames: Seq[String],\n      approxNumPartitions: Int,\n      randomizationExpressionOpt: Option[Column]\n  ): DataFrame\n}\n\nobject MultiDimClustering {\n  /**\n   * Repartition the given dataframe `df` based on the given `curve` type into\n   * `approxNumPartitions` on the given `colNames`.\n   */\n  def cluster(\n      df: DataFrame,\n      approxNumPartitions: Int,\n      colNames: Seq[String],\n      curve: String): DataFrame = {\n    assert(colNames.nonEmpty, \"Cannot cluster by zero columns!\")\n    val clusteringImpl = curve match {\n      case \"hilbert\" if colNames.size == 1 => ZOrderClustering\n      case \"hilbert\" => HilbertClustering\n      case \"zorder\" => ZOrderClustering\n      case unknownCurve =>\n        throw new SparkException(s\"Unknown curve ($unknownCurve), unable to perform multi \" +\n          \"dimensional clustering.\")\n    }\n    clusteringImpl.cluster(df, colNames, approxNumPartitions, randomizationExpressionOpt = None)\n  }\n}\n\n/** Base class for space filling curve based clustering e.g. ZOrder */\ntrait SpaceFillingCurveClustering extends MultiDimClustering {\n\n  protected def getClusteringExpression(cols: Seq[Column], numRanges: Int): Column\n\n  override def cluster(\n      df: DataFrame,\n      colNames: Seq[String],\n      approxNumPartitions: Int,\n      randomizationExpressionOpt: Option[Column]): DataFrame = {\n    val conf = df.sparkSession.sessionState.conf\n    val numRanges = conf.getConf(DeltaSQLConf.MDC_NUM_RANGE_IDS)\n    val addNoise = conf.getConf(DeltaSQLConf.MDC_ADD_NOISE)\n    val sortWithinFiles = conf.getConf(DeltaSQLConf.MDC_SORT_WITHIN_FILES)\n\n    val cols = colNames.map(df(_))\n    val mdcCol = getClusteringExpression(cols, numRanges)\n    val repartitionKeyColName = s\"${UUID.randomUUID().toString}-rpKey1\"\n\n    var repartitionedDf = if (addNoise) {\n      val randByteColName = s\"${UUID.randomUUID().toString}-rpKey2\"\n      val randByteCol = randomizationExpressionOpt.getOrElse((rand() * 255 - 128).cast(ByteType))\n      df.withColumn(repartitionKeyColName, mdcCol).withColumn(randByteColName, randByteCol)\n        .repartitionByRange(approxNumPartitions, col(repartitionKeyColName), col(randByteColName))\n        .drop(randByteColName)\n    } else {\n      df.withColumn(repartitionKeyColName, mdcCol)\n        .repartitionByRange(approxNumPartitions, col(repartitionKeyColName))\n    }\n\n    if (sortWithinFiles) {\n      repartitionedDf = repartitionedDf.sortWithinPartitions(repartitionKeyColName)\n    }\n\n    repartitionedDf.drop(repartitionKeyColName)\n  }\n}\n\n/** Implement Z-Order clustering */\nobject ZOrderClustering extends SpaceFillingCurveClustering {\n  override protected[skipping] def getClusteringExpression(\n      cols: Seq[Column], numRanges: Int): Column = {\n    assert(cols.size >= 1, \"Cannot do Z-Order clustering by zero columns!\")\n    val rangeIdCols = cols.map(range_partition_id(_, numRanges))\n    interleave_bits(rangeIdCols: _*).cast(StringType)\n  }\n}\n\nobject HilbertClustering extends SpaceFillingCurveClustering with Logging {\n  override protected def getClusteringExpression(cols: Seq[Column], numRanges: Int): Column = {\n    assert(cols.size > 1, \"Cannot do Hilbert clustering by zero or one column!\")\n    val rangeIdCols = cols.map(range_partition_id(_, numRanges))\n    val numBits = Integer.numberOfTrailingZeros(Integer.highestOneBit(numRanges)) + 1\n    hilbert_index(numBits, rangeIdCols: _*)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/MultiDimClusteringFunctions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.expressions.{HilbertByteArrayIndex, HilbertLongIndex, InterleaveBits, RangePartitionId}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.Column\nimport org.apache.spark.sql.catalyst.expressions.{Cast, Expression}\nimport org.apache.spark.sql.types.StringType\n\n/** Functions for multi-dimensional clustering of the data */\nobject MultiDimClusteringFunctions {\n  private def withExpr(expr: Expression): Column = Column(expr)\n\n  /**\n   * Conceptually range-partitions the domain of values of the given column into `numPartitions`\n   * partitions and computes the partition number that every value of that column corresponds to.\n   * One can think of this as an approximate rank() function.\n   *\n   * Ex. For a column with values (0, 1, 3, 15, 36, 99) and numPartitions = 3 returns\n   * partition range ids as (0, 0, 1, 1, 2, 2).\n   */\n  def range_partition_id(col: Column, numPartitions: Int): Column = withExpr {\n    RangePartitionId(expression(col), numPartitions)\n  }\n\n  /**\n   * Interleaves the bits of its input data in a round-robin fashion.\n   *\n   * If the input data is seen as a series of multidimensional points, this function computes the\n   * corresponding Z-values, in a way that's preserving data locality: input points that are close\n   * in the multidimensional space will be mapped to points that are close on the Z-order curve.\n   *\n   * The returned value is a byte array where the size of the array is 4 * num of input columns.\n   *\n   * @see https://en.wikipedia.org/wiki/Z-order_curve\n   *\n   * @note Only supports input expressions of type Int for now.\n   */\n  def interleave_bits(cols: Column*): Column = withExpr {\n    InterleaveBits(cols.map(expression))\n  }\n\n  // scalastyle:off line.size.limit\n  /**\n   * Transforms the provided integer columns into their corresponding position in the hilbert\n   * curve for the given dimension.\n   * @see https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=bfd6d94c98627756989b0147a68b7ab1f881a0d6\n   * @see https://en.wikipedia.org/wiki/Hilbert_curve\n   * @param numBits The number of bits to consider in each column.\n   * @param cols The integer columns to map to the curve.\n   */\n  // scalastyle:on line.size.limit\n  def hilbert_index(numBits: Int, cols: Column*): Column = withExpr {\n    if (cols.size > 9) {\n      throw new SparkException(\"Hilbert indexing can only be used on 9 or fewer columns.\")\n    }\n    val hilbertBits = cols.length * numBits\n    if (hilbertBits < 64) {\n      HilbertLongIndex(numBits, cols.map(expression))\n    } else {\n      Cast(HilbertByteArrayIndex(numBits, cols.map(expression)), StringType)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/ClusteredTableUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta.{ClusteringTableFeature, DeltaColumnMappingMode, DeltaErrors, DeltaLog, OptimisticTransaction, Snapshot}\nimport org.apache.spark.sql.delta.actions.{Action, DomainMetadata, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.clustering.ClusteringMetadataDomain\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.{DeltaStatistics, SkippingEligibleDataType, StatisticsCollection, StatsCollectionUtils}\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\n\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{StructField, StructType}\n\ncase class MatchingMetadataDomain(\n    clusteringDomainOpt: Option[DomainMetadata]\n)\n\n/**\n * Clustered table utility functions.\n */\ntrait ClusteredTableUtilsBase extends DeltaLogging {\n  // Clustering columns property key. The column names are logical and separated by comma.\n  // This will be removed when we integrate with OSS Spark and use\n  // [[CatalogTable.PROP_CLUSTERING_COLUMNS]] directly.\n  val PROP_CLUSTERING_COLUMNS: String = \"clusteringColumns\"\n\n /**\n  * Returns whether the protocol version supports the Liquid table feature.\n  */\n  def isSupported(protocol: Protocol): Boolean = protocol.isFeatureSupported(ClusteringTableFeature)\n\n  /** The clustering implementation name for [[AddFile.clusteringProvider]] */\n  def clusteringProvider: String = \"liquid\"\n\n  /**\n   * Returns an optional [[ClusterBySpec]] from the given CatalogTable.\n   */\n  def getClusterBySpecOptional(table: CatalogTable): Option[ClusterBySpec] = {\n    table.properties.get(PROP_CLUSTERING_COLUMNS).map(ClusterBySpec.fromProperty)\n  }\n\n  /**\n   * Returns an optional [[ClusterBySpec]] from the given Snapshot.\n   */\n  def getClusterBySpecOptional(snapshot: Snapshot): Option[ClusterBySpec] = {\n    if (isSupported(snapshot.protocol)) {\n      val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(snapshot)\n      Some(ClusterBySpec.fromColumnNames(clusteringColumns))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Extract clustering columns from ClusterBySpec.\n   *\n   * @param maybeClusterBySpec optional ClusterBySpec. If it's empty, will return the\n   *                             original properties.\n   * @return an optional pair with clustering columns.\n   */\n  def getClusteringColumnsAsProperty(\n      maybeClusterBySpec: Option[ClusterBySpec]): Option[(String, String)] = {\n    maybeClusterBySpec.map(ClusterBySpec.toProperty)\n  }\n\n  /**\n   * Extract clustering columns from a given snapshot.\n   */\n  def getClusteringColumnsAsProperty(snapshot: Snapshot): Option[(String, String)] = {\n    val clusterBySpec = getClusterBySpecOptional(snapshot)\n    getClusteringColumnsAsProperty(clusterBySpec)\n  }\n\n  /**\n   * Returns table feature properties that's required to create a clustered table.\n   *\n   * @param existingProperties Table properties set by the user when creating a clustered table.\n   */\n  def getTableFeatureProperties(existingProperties: Map[String, String]): Map[String, String] = {\n    val properties = collection.mutable.Map.empty[String, String]\n    properties += TableFeatureProtocolUtils.propertyKey(ClusteringTableFeature) ->\n      TableFeatureProtocolUtils.FEATURE_PROP_SUPPORTED\n\n    properties.toMap\n  }\n\n  /**\n   * Verify user didn't set clustering table feature in table properties.\n   *\n   * @param existingProperties Table properties set by the user when creating a clustered table.\n   */\n  def validateExistingTableFeatureProperties(existingProperties: Map[String, String]): Unit = {\n    if (existingProperties.contains(\n        TableFeatureProtocolUtils.propertyKey(ClusteringTableFeature))) {\n      throw DeltaErrors.createTableSetClusteringTableFeatureException(ClusteringTableFeature.name)\n    }\n  }\n\n  /**\n   * Validate the number of clustering columns doesn't exceed the limit.\n   *\n   * @param clusteringColumns clustering columns for the table.\n   * @param deltaLogOpt optional delta log. If present, will be used to record a delta event.\n   */\n  def validateNumClusteringColumns(\n      clusteringColumns: Seq[Seq[String]],\n      deltaLogOpt: Option[DeltaLog] = None): Unit = {\n    val numColumnsLimit =\n      SQLConf.get.getConf(DeltaSQLConf.DELTA_NUM_CLUSTERING_COLUMNS_LIMIT)\n    val actualNumColumns = clusteringColumns.size\n    if (actualNumColumns > numColumnsLimit) {\n      deltaLogOpt.foreach { deltaLog =>\n        recordDeltaEvent(\n          deltaLog,\n          opType = \"delta.clusteredTable.invalidNumClusteringColumns\",\n          data = Map(\n            \"numCols\" -> clusteringColumns.size,\n            \"numColsLimit\" -> numColumnsLimit))\n      }\n      throw DeltaErrors.clusterByInvalidNumColumnsException(numColumnsLimit, actualNumColumns)\n    }\n  }\n\n  /**\n   * Remove clustered table internal table properties. These properties are never stored into\n   * [[Metadata.configuration]] such as table features.\n   */\n  def removeInternalTableProperties(\n      props: scala.collection.Map[String, String]): Map[String, String] = {\n    props.toMap --\n      // Clustering table feature and dependent table features\n      Seq(ClusteringTableFeature).flatMap { feature =>\n        (feature +: feature.requiredFeatures.toSeq).map(TableFeatureProtocolUtils.propertyKey)\n      }\n  }\n\n  /**\n   * Remove PROP_CLUSTERING_COLUMNS from metadata action.\n   * Clustering columns should only exist in:\n   * 1. CatalogTable.properties(PROP_CLUSTERING_COLUMNS)\n   * 2. Clustering metadata domain.\n   * @param configuration original configuration.\n   * @return new configuration without clustering columns property\n   */\n  def removeClusteringColumnsProperty(configuration: Map[String, String]): Map[String, String] = {\n    configuration - PROP_CLUSTERING_COLUMNS\n  }\n\n  /**\n   * Returns [[DomainMetadata]] action to store clustering columns.\n   * If clusterBySpecOpt is not empty (clustering columns are specified by CLUSTER BY), it creates\n   * the domain metadata based on the clustering columns.\n   * Otherwise (CLUSTER BY is not specified for REPLACE TABLE), it creates the domain metadata\n   * with empty clustering columns if a clustering domain exists.\n   *\n   * This is used for CREATE TABLE and REPLACE TABLE.\n   */\n  def getDomainMetadataFromTransaction(\n      clusterBySpecOpt: Option[ClusterBySpec],\n      txn: OptimisticTransaction): Seq[DomainMetadata] = {\n    clusterBySpecOpt.map { clusterBy =>\n      ClusteredTableUtils.validateClusteringColumnsInStatsSchema(\n        txn.protocol, txn.metadata, clusterBy)\n      val clusteringColumns =\n        clusterBy.columnNames.map(_.toString).map(ClusteringColumn(txn.metadata.schema, _))\n      Seq(createDomainMetadata(clusteringColumns))\n    }.getOrElse {\n      getMatchingMetadataDomain(\n        clusteringColumns = Seq.empty,\n        txn.snapshot.domainMetadata).clusteringDomainOpt.toSeq\n    }\n  }\n\n  /**\n   * Returns a sequence of [[DomainMetadata]] actions to update the existing domain metadata with\n   * the given clustering columns.\n   *\n   * This is mainly used for REPLACE TABLE and RESTORE TABLE.\n   */\n  def getMatchingMetadataDomain(\n      clusteringColumns: Seq[ClusteringColumn],\n      existingDomainMetadata: Seq[DomainMetadata]): MatchingMetadataDomain = {\n    val clusteringMetadataDomainOpt =\n      if (existingDomainMetadata.exists(_.domain == ClusteringMetadataDomain.domainName)) {\n        Some(ClusteringMetadataDomain.fromClusteringColumns(clusteringColumns).toDomainMetadata)\n      } else {\n        None\n      }\n\n    MatchingMetadataDomain(\n      clusteringMetadataDomainOpt\n    )\n  }\n\n  /**\n   * Create a [[DomainMetadata]] action to store clustering columns.\n   */\n  def createDomainMetadata(clusteringColumns: Seq[ClusteringColumn]): DomainMetadata = {\n    ClusteringMetadataDomain.fromClusteringColumns(clusteringColumns).toDomainMetadata\n  }\n\n  /**\n   * Extract [[ClusteringColumn]]s from a given snapshot. Return None if the clustering domain\n   * metadata is missing.\n   */\n  def getClusteringColumnsOptional(snapshot: Snapshot): Option[Seq[ClusteringColumn]] = {\n    ClusteringMetadataDomain\n      .fromSnapshot(snapshot)\n      .map(_.clusteringColumns.map(ClusteringColumn.apply))\n  }\n\n  /**\n   * Extract [[DomainMetadata]] for storing clustering columns from a given snapshot.\n   * It returns clustering domain metadata if exists.\n   * Return empty if the clustering domain metadata is missing.\n   */\n  def getClusteringDomainMetadata(snapshot: Snapshot): Seq[DomainMetadata] = {\n    ClusteringMetadataDomain.fromSnapshot(snapshot).map(_.toDomainMetadata).toSeq\n  }\n\n  /**\n   * Create new clustering [[DomainMetadata]] actions given updated column names for\n   * 'ALTER TABLE ... CLUSTER BY'.\n   */\n  def getClusteringDomainMetadataForAlterTableClusterBy(\n      newLogicalClusteringColumns: Seq[String],\n      txn: OptimisticTransaction): Seq[DomainMetadata] = {\n    val newClusteringColumns =\n      newLogicalClusteringColumns.map(ClusteringColumn(txn.metadata.schema, _))\n    val clusteringMetadataDomainOpt =\n      Some(ClusteringMetadataDomain.fromClusteringColumns(newClusteringColumns).toDomainMetadata)\n    clusteringMetadataDomainOpt.toSeq\n  }\n\n  /**\n   * Extract the logical clustering column names from the to-be committed domain metadata action.\n   *\n   * @param txn the transaction being used to commit the actions.\n   * @param actionsToCommit the actions to be committed.\n   * @return optional logical clustering column names.\n   */\n  def getLogicalClusteringColumnNames(\n      txn: OptimisticTransaction,\n      actionsToCommit: Seq[Action]): Option[Seq[String]] = {\n    def getLogicalColumnNames(clusteringColumns: Seq[ClusteringColumn]): Seq[String] = {\n      clusteringColumns.map(ClusteringColumnInfo(txn.metadata.schema, _).logicalName)\n    }\n\n    actionsToCommit.collectFirst {\n      // Only consider clustering domain metadata actions that are getting added\n      // (removed = false).\n      case ClusteringMetadataDomain(domain, removed) if !removed =>\n        getLogicalColumnNames(domain.clusteringColumns.map(ClusteringColumn.apply))\n    }\n  }\n\n  /**\n   * Validate stats will be collected for all clustering columns.\n   */\n  def validateClusteringColumnsInStatsSchema(\n      snapshot: Snapshot,\n      logicalClusteringColumns: Seq[String]): Unit = {\n    validateClusteringColumnsInStatsSchema(\n      snapshot,\n      logicalClusteringColumns.map { name =>\n        ClusteringColumnInfo(snapshot.schema, ClusteringColumn(snapshot.schema, name))\n      })\n  }\n\n  /**\n   * Returns true if stats will be collected for all clustering columns.\n   */\n  def areClusteringColumnsInStatsSchema(\n      snapshot: Snapshot,\n      logicalClusteringColumns: Seq[String]): Boolean = {\n    getClusteringColumnsNotInStatsSchema(\n      snapshot,\n      logicalClusteringColumns.map { name =>\n        ClusteringColumnInfo(snapshot.schema, ClusteringColumn(snapshot.schema, name))\n      }).isEmpty\n  }\n\n  /**\n   * Validate stats will be collected for all clustering columns.\n   *\n   * This version is used when [[Snapshot]] doesn't have latest stats column information such as\n   * `CREATE TABLE...` where the initial snapshot doesn't have updated metadata / protocol yet.\n   */\n  def validateClusteringColumnsInStatsSchema(\n      protocol: Protocol,\n      metadata: Metadata,\n      clusterBy: ClusterBySpec): Unit = {\n    validateClusteringColumnsInStatsSchema(\n      statisticsCollectionFromMetadata(protocol, metadata),\n      clusterBy.columnNames.map { column =>\n        ClusteringColumnInfo(metadata.schema, ClusteringColumn(metadata.schema, column.toString))\n      })\n  }\n\n  /**\n   * Build a [[StatisticsCollection]] with minimal requirements that can be used to find stats\n   * columns.\n   *\n   * We can not use [[Snapshot]] as in a normal case during table creation such as `CREATE TABLE`\n   * because the initial snapshot doesn't have the updated metadata / protocol to find latest stats\n   * columns.\n   */\n  private def statisticsCollectionFromMetadata(\n      p: Protocol,\n      metadata: Metadata): StatisticsCollection = {\n    new StatisticsCollection {\n      override val tableSchema: StructType = metadata.schema\n      override val outputAttributeSchema: StructType = tableSchema\n      // [[outputTableStatsSchema]] is the candidate schema to find statistics columns.\n      override val outputTableStatsSchema: StructType = tableSchema\n      override val statsColumnSpec = StatisticsCollection.configuredDeltaStatsColumnSpec(metadata)\n      override val columnMappingMode: DeltaColumnMappingMode = metadata.columnMappingMode\n      override val protocol: Protocol = p\n      override def getDataSkippingStringPrefixLength: Int =\n        StatsCollectionUtils.getDataSkippingStringPrefixLength(spark, metadata)\n\n      override def spark: SparkSession = {\n        throw new Exception(\"Method not used in statisticsCollectionFromMetadata\")\n      }\n    }\n  }\n\n  /**\n   * Validate physical clustering columns can be found in the latest stats columns.\n   *\n   * @param statsCollection Provides latest stats columns.\n   * @param clusteringColumnInfos Clustering columns in physical names.\n   *\n   * A [[AnalysisException]] is thrown if the clustering column can not be found in the latest\n   * stats columns. The error message contains logical names only for better user experience.\n   */\n  private def validateClusteringColumnsInStatsSchema(\n      statsCollection: StatisticsCollection,\n      clusteringColumnInfos: Seq[ClusteringColumnInfo]): Unit = {\n    val missingColumns =\n      getClusteringColumnsNotInStatsSchema(statsCollection, clusteringColumnInfos)\n    if (missingColumns.nonEmpty) {\n      // Check DataType eligibility.\n      val missingColumnInfos = clusteringColumnInfos.filter(\n        info => missingColumns.contains(info.logicalName))\n      // This assertion must hold since missingColumns are subset of clusteringColumnInfos.\n      assert(missingColumnInfos.length == missingColumns.length)\n      val nonSkippingEligibleMissingColumnInfos =\n        missingColumnInfos.filter(info => !SkippingEligibleDataType(info.dataType))\n      if (nonSkippingEligibleMissingColumnInfos.nonEmpty) {\n        val columnNameWithDataTypes = nonSkippingEligibleMissingColumnInfos\n          .map(info => s\"${info.logicalName} : ${info.dataType.sql}\")\n          .mkString(\", \")\n        throw DeltaErrors.clusteringColumnUnsupportedDataTypes(columnNameWithDataTypes)\n      }\n\n      throw DeltaErrors.clusteringColumnMissingStats(\n        missingColumns.mkString(\", \"),\n        statsCollection.statCollectionLogicalSchema.treeString)\n    }\n  }\n\n  /**\n   * Validate that the given clusterBySpec matches the existing table's in the given snapshot.\n   * This is used for append mode and replaceWhere.\n   */\n  def validateClusteringColumnsInSnapshot(\n      snapshot: Snapshot,\n      clusterBySpec: ClusterBySpec): Unit = {\n    // This uses physical column names to compare.\n    val providedClusteringColumns =\n      Some(clusterBySpec.columnNames.map(col => ClusteringColumn(snapshot.schema, col.toString)))\n    val existingClusteringColumns = ClusteredTableUtils.getClusteringColumnsOptional(snapshot)\n    if (providedClusteringColumns != existingClusteringColumns) {\n      throw DeltaErrors.clusteringColumnsMismatchException(\n        clusterBySpec.columnNames.map(_.toString).mkString(\",\"),\n        existingClusteringColumns.map(_.map(\n          ClusteringColumnInfo(snapshot.schema, _).logicalName).mkString(\",\")).getOrElse(\"\")\n      )\n    }\n  }\n\n  /**\n   * Returns empty if all physical clustering columns can be found in the latest stats columns.\n   * Otherwise, returns the logical names of the all clustering columns that are not found.\n   *\n   * [[StatisticsCollection.statsSchema]] has converted field's name to physical name and also it\n   * filters out any columns that are NOT qualified as a stats data type\n   * through [[SkippingEligibleDataType]].\n   *\n   * @param statsCollection       Provides latest stats columns.\n   * @param clusteringColumnInfos Clustering columns in physical names.\n   */\n  private def getClusteringColumnsNotInStatsSchema(\n      statsCollection: StatisticsCollection,\n      clusteringColumnInfos: Seq[ClusteringColumnInfo]): Seq[String] = {\n    clusteringColumnInfos.flatMap { info =>\n      val path = DeltaStatistics.MIN +: info.physicalName\n      SchemaUtils.findNestedFieldIgnoreCase(statsCollection.statsSchema, path) match {\n        // Validate that the column exists in the stats schema and is not a struct\n        // in the stats schema (to catch CLUSTER BY an entire struct).\n        case None | Some(StructField(_, _: StructType, _, _)) =>\n          Some(info.logicalName)\n        case _ => None\n      }\n    }\n  }\n}\n\nobject ClusteredTableUtils extends ClusteredTableUtilsBase\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/ClusteringColumn.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaErrors, Snapshot}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\n\nimport org.apache.spark.sql.catalyst.expressions.variant.VariantExpressionEvalUtils\nimport org.apache.spark.sql.connector.expressions.FieldReference\nimport org.apache.spark.sql.types.{DataType, StructType}\n\n/**\n * A wrapper class that stores a clustering column's physical name parts.\n */\ncase class ClusteringColumn(physicalName: Seq[String])\n\nobject ClusteringColumn {\n  /**\n   * Note: `logicalName` must be validated to exist in the given `schema`.\n   */\n  def apply(schema: StructType, logicalName: String): ClusteringColumn = {\n    val resolver = SchemaUtils.DELTA_COL_RESOLVER\n    // Note that we use AttributeNameParser instead of CatalystSqlParser to account for the case\n    // where the column name is a backquoted string with spaces.\n    val logicalNameParts = FieldReference(logicalName).fieldNames\n    val physicalNameParts = logicalNameParts.foldLeft[(DataType, Seq[String])]((schema, Nil)) {\n      (partial, namePart) =>\n        val (currStructType, currPhysicalNameSeq) = partial\n        val field = currStructType match {\n          case fieldType: StructType =>\n            fieldType.find(field => resolver(field.name, namePart)) match {\n              case Some(f) => f\n              case None =>\n                throw DeltaErrors.columnNotInSchemaException(logicalName, schema)\n            }\n          case _ =>\n            throw DeltaErrors.columnNotInSchemaException(logicalName, schema)\n        }\n        // Variant columns cannot be used as clustering columns because they are not orderable.\n        if (VariantExpressionEvalUtils.typeContainsVariant(field.dataType)) {\n          throw DeltaErrors.clusteringColumnUnsupportedDataTypes(\n            s\"$logicalName : ${field.dataType.sql}\")\n        }\n        (field.dataType, currPhysicalNameSeq :+ DeltaColumnMapping.getPhysicalName(field))\n    }._2\n    ClusteringColumn(physicalNameParts)\n  }\n}\n\n/**\n * A wrapper class that stores a clustering column's physical name parts and data type.\n */\ncase class ClusteringColumnInfo(\n    physicalName: Seq[String], dataType: DataType, schema: StructType) {\n  lazy val logicalName: String = {\n    val reversePhysicalNameParts = physicalName.reverse\n    val resolver = SchemaUtils.DELTA_COL_RESOLVER\n    val logicalNameParts =\n      reversePhysicalNameParts\n        .foldRight[(Seq[String], DataType)]((Nil, schema)) {\n          (namePart, state) =>\n            val (logicalNameParts, parentRawDataType) = state\n            val parentDataType = parentRawDataType.asInstanceOf[StructType]\n            val nextField =\n              parentDataType\n                .find(field => resolver(DeltaColumnMapping.getPhysicalName(field), namePart))\n                .get\n            (nextField.name +: logicalNameParts, nextField.dataType)\n        }._1.reverse\n    FieldReference(logicalNameParts).toString\n  }\n}\n\nobject ClusteringColumnInfo extends DeltaLogging {\n  def apply(schema: StructType, clusteringColumn: ClusteringColumn): ClusteringColumnInfo =\n    apply(schema, clusteringColumn.physicalName)\n\n  def apply(schema: StructType, physicalName: Seq[String]): ClusteringColumnInfo = {\n    val resolver = SchemaUtils.DELTA_COL_RESOLVER\n    val dataType = physicalName.foldLeft[DataType](schema) {\n      (currStructType, namePart) =>\n        currStructType.asInstanceOf[StructType].find { field =>\n          resolver(DeltaColumnMapping.getPhysicalName(field), namePart)\n        }.get.dataType\n    }\n    ClusteringColumnInfo(physicalName, dataType, schema)\n  }\n\n  def extractLogicalNames(snapshot: Snapshot): Seq[String] = {\n    ClusteredTableUtils.getClusteringColumnsOptional(snapshot).map { clusteringColumns =>\n      clusteringColumns.map(ClusteringColumnInfo(snapshot.schema, _).logicalName)\n    }.getOrElse(Seq.empty)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/ClusteringStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\nimport org.apache.spark.sql.delta.commands.DeltaOptimizeContext\nimport org.apache.spark.sql.delta.commands.optimize.ZCubeFileStatsCollector\n\n/**\n * Aggregated file stats for a category of ZCube files.\n *\n * @param numFiles Total number of files.\n * @param size Total physical size of files in bytes.\n */\ncase class ClusteringFileStats(numFiles: Long, size: Long)\n\nobject ClusteringFileStats {\n  def apply(v: Iterable[(Int, Long)]): ClusteringFileStats = {\n    v.foldLeft(ClusteringFileStats(0, 0)) { (a, b) =>\n      ClusteringFileStats(a.numFiles + b._1, a.size + b._2)\n    }\n  }\n}\n\n/**\n * Aggregated stats for OPTIMIZE command on clustered tables.\n *\n * @param inputZCubeFiles Files in the ZCubes matching the current OPTIMIZE operation.\n * @param inputOtherFiles Files not in any ZCubes or in other ZCubes with different\n *                        clustering columns.\n * @param inputNumZCubes Number of different cubes among input files.\n * @param mergedFiles Subset of input files merged by the current operation\n * @param numOutputZCubes Number of output ZCubes written out\n */\ncase class ClusteringStats(\n    inputZCubeFiles: ClusteringFileStats,\n    inputOtherFiles: ClusteringFileStats,\n    inputNumZCubes: Long,\n    mergedFiles: ClusteringFileStats,\n    numOutputZCubes: Long)\n\n/**\n * A class help collecting ClusteringStats.\n */\ncase class ClusteringStatsCollector(zOrderBy: Seq[String], optimizeContext: DeltaOptimizeContext) {\n\n  val inputStats = new ZCubeFileStatsCollector(zOrderBy, optimizeContext.isFull)\n  val outputStats = new ZCubeFileStatsCollector(zOrderBy, optimizeContext.isFull)\n  var numOutputZCubes = 0\n\n  def getClusteringStats: ClusteringStats = {\n    ClusteringStats(\n      inputNumZCubes = inputStats.numZCubes,\n      inputZCubeFiles = ClusteringFileStats(inputStats.fileStats.get(\"matchingCube\")),\n      inputOtherFiles = ClusteringFileStats(inputStats.fileStats.get(\"otherFiles\")),\n      mergedFiles = ClusteringFileStats(outputStats.fileStats.values),\n      numOutputZCubes = numOutputZCubes)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/ZCube.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.commands.optimize.AddFileWithNumRecords\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo.{getForFile => getZCubeInfo}\n\n/**\n * Collection of files that were produced by the same job in a run of the clustering command.\n */\ncase class ZCube(files: Seq[AddFile]) {\n  require(files.nonEmpty)\n\n  if (DeltaUtils.isTesting) {\n    assert(files.forall(getZCubeInfo(_) == Some(zCubeInfo)))\n  }\n\n  lazy val zCubeInfo: ZCubeInfo = getZCubeInfo(files.head).get\n  lazy val totalFileSize: Long = files.foldLeft(0L)(_ + _.size)\n}\n\nobject ZCube {\n  /**\n   * Given an iterator of files sorted by ZCubeId, returns a filtered iterator of files,\n   * where files belonging to large ZCubes (ZCube size >= target ZCube size ) are filtered\n   * out.\n   *\n   * @param files - Files sorted by ZCubeId, unoptimized files first.\n   */\n  def filterOutLargeZCubes(\n      files: Iterator[AddFileWithNumRecords],\n      targetCubeSize: Long): Iterator[AddFileWithNumRecords] = {\n    val currentZCube = new ArrayBuffer[AddFileWithNumRecords]()\n    var currentZCubeSize = 0L\n    var currentZCubeId: String = null\n\n    def appendZCube(file: AddFileWithNumRecords): Unit = {\n      currentZCube.append(file)\n      currentZCubeSize += file.addFile.estLogicalFileSize.getOrElse(file.addFile.size)\n    }\n\n    def resetZCube(): Unit = {\n      currentZCube.clear()\n      currentZCubeSize = 0\n    }\n\n    def returnAndResetCurrentZCube(): Seq[AddFileWithNumRecords] = {\n      val res = if (currentZCubeSize >= targetCubeSize) {\n        // Drop the current ZCube.\n        Seq.empty\n      } else {\n        // Return a copy of current.\n        currentZCube.toVector\n      }\n      resetZCube()\n      res\n    }\n\n    files.flatMap { addFileWithNumRecords =>\n      val file = addFileWithNumRecords.addFile\n      val res = ZCubeInfo.getForFile(file) match {\n        case Some(ZCubeInfo(zCubeID, _)) =>\n          // Note: check for ZCubes' ids to group files from the same ZCube.\n          if (zCubeID == currentZCubeId) {\n            // Add to the same ZCube.\n            appendZCube(addFileWithNumRecords)\n            // Skip to next file.\n            Nil\n          } else {\n            // New ZCube.\n            val currentZCubeResult = returnAndResetCurrentZCube()\n            // Start a new ZCube.\n            appendZCube(addFileWithNumRecords)\n            currentZCubeId = zCubeID\n            currentZCubeResult\n          }\n        case None =>\n          // Return current ZCube and this file.\n          returnAndResetCurrentZCube() :+ addFileWithNumRecords\n      }\n      if (!files.hasNext) {\n        // Last file, return the current ZCube and the result.\n        returnAndResetCurrentZCube() ++ res\n      } else {\n        res\n      }\n    }\n  }\n\n  /**\n   * Filter out files belonging to single ZCube.\n   *\n   * @param files - Files sorted by ZCubeId, unoptimized files first.\n   */\n  def filterOutSingleZCubes(\n      files: Iterator[AddFileWithNumRecords]): Iterator[AddFileWithNumRecords] = {\n    val currentZCube = new ArrayBuffer[AddFileWithNumRecords]()\n    var singleZCube = true\n    var currentZCubeId: String = null\n\n    def appendZCube(file: AddFileWithNumRecords): Unit = {\n      currentZCube.append(file)\n    }\n\n    def returnAndResetCurrentZCube(): Seq[AddFileWithNumRecords] = {\n      val res = if (singleZCube) {\n        // Drop the current ZCube.\n        Seq.empty\n      } else {\n        // Return a copy of current.\n        currentZCube.toVector\n      }\n      resetZCube()\n      res\n    }\n\n    def resetZCube(): Unit = {\n      currentZCube.clear()\n    }\n\n    files.flatMap { addFileWithNumRecords =>\n      val file = addFileWithNumRecords.addFile\n      val res = ZCubeInfo.getForFile(file) match {\n        case Some(ZCubeInfo(zCubeID, _)) =>\n          if (zCubeID == currentZCubeId || currentZCubeId == null) {\n            if (currentZCubeId == null) {\n              currentZCubeId = zCubeID\n            }\n            // Same ZCube.\n            appendZCube(addFileWithNumRecords)\n            Nil\n          } else {\n            // New ZCube.\n            currentZCubeId = zCubeID\n            // Return the current ZCube and start a new ZCube.\n            singleZCube = false\n            val currentZCubeResult = returnAndResetCurrentZCube()\n            appendZCube(addFileWithNumRecords)\n            currentZCubeResult\n          }\n        case None =>\n          val resZCube = returnAndResetCurrentZCube()\n          // Unoptimized file means the following ZCube is not alone.\n          singleZCube = false\n          resZCube :+ addFileWithNumRecords\n      }\n      if (!files.hasNext) {\n        // Last file, return the current ZCube.\n        returnAndResetCurrentZCube() ++ res\n      } else {\n        res\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/temp/AlterTableClusterBy.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering.temp\n\nimport org.apache.spark.sql.catalyst.plans.logical.{AlterTableCommand, LogicalPlan}\nimport org.apache.spark.sql.connector.catalog.TableChange\nimport org.apache.spark.sql.connector.expressions.NamedReference\n\n/**\n * The logical plan of the following commands:\n *  - ALTER TABLE ... CLUSTER BY (col1, col2, ...)\n *  - ALTER TABLE ... CLUSTER BY NONE\n */\ncase class AlterTableClusterBy(\n    table: LogicalPlan, clusterBySpec: Option[ClusterBySpec]) extends AlterTableCommand {\n  override def changes: Seq[TableChange] =\n    Seq(ClusterBy(clusterBySpec\n      .map(_.columnNames) // CLUSTER BY (col1, col2, ...)\n      .getOrElse(Seq.empty))) // CLUSTER BY NONE\n\n  protected def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = copy(table = newChild)\n}\n\n/** A TableChange to alter clustering columns for a table. */\ncase class ClusterBy(clusteringColumns: Seq[NamedReference]) extends TableChange {}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/temp/ClusterBySpec.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering.temp\n\nimport scala.reflect.ClassTag\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}\nimport com.fasterxml.jackson.module.scala.{ClassTagExtensions, DefaultScalaModule}\nimport org.antlr.v4.runtime.ParserRuleContext\n\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.catalyst.parser.{ParseException, ParserInterface, ParserUtils}\nimport org.apache.spark.sql.catalyst.plans.logical.{CreateTable, CreateTableAsSelect, LeafNode, LogicalPlan, ReplaceTable, ReplaceTableAsSelect}\nimport org.apache.spark.sql.connector.expressions.{BucketTransform, FieldReference, NamedReference, Transform}\n\n/**\n * A container for clustering information. Copied from OSS Spark.\n *\n * This class will be removed when we integrate with OSS Spark's CLUSTER BY implementation.\n * @see https://github.com/apache/spark/pull/42577\n *\n * @param columnNames the names of the columns used for clustering.\n */\ncase class ClusterBySpec(columnNames: Seq[NamedReference]) {\n  override def toString: String = toJson\n\n  def toJson: String =\n    ClusterBySpec.mapper.writeValueAsString(columnNames.map(_.fieldNames))\n}\n\nobject ClusterBySpec {\n  private val mapper = {\n    val ret = new ObjectMapper() with ClassTagExtensions\n    ret.setSerializationInclusion(Include.NON_ABSENT)\n    ret.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n    ret.registerModule(DefaultScalaModule)\n    ret\n  }\n\n  // ClassTag is added to avoid the \"same type after erasure\" issue with the case class.\n  def apply[_: ClassTag](columnNames: Seq[Seq[String]]): ClusterBySpec = {\n    ClusterBySpec(columnNames.map(FieldReference(_)))\n  }\n\n  // Convert from table property back to ClusterBySpec.\n  def fromProperty(columns: String): ClusterBySpec = {\n    ClusterBySpec(mapper.readValue[Seq[Seq[String]]](columns).map(FieldReference(_)))\n  }\n\n  def toProperty(clusterBySpec: ClusterBySpec): (String, String) = {\n    ClusteredTableUtils.PROP_CLUSTERING_COLUMNS -> clusterBySpec.toJson\n  }\n\n  def fromProperties(properties: Map[String, String]): Option[ClusterBySpec] = {\n    properties.get(ClusteredTableUtils.PROP_CLUSTERING_COLUMNS).map { clusteringColumns =>\n      fromProperty(clusteringColumns)\n    }\n  }\n\n  def toProperties(clusterBySpec: ClusterBySpec): Map[String, String] = {\n    val columnValue = mapper.writeValueAsString(clusterBySpec.columnNames.map(_.fieldNames))\n    Map(ClusteredTableUtils.PROP_CLUSTERING_COLUMNS -> columnValue)\n  }\n\n  def fromColumnNames(names: Seq[String]): ClusterBySpec = {\n    ClusterBySpec(names.map(FieldReference(_)))\n  }\n}\n\n/**\n * A [[LogicalPlan]] representing a CLUSTER BY clause.\n *\n * This class will be removed when we integrate with OSS Spark's CLUSTER BY implementation.\n * @see https://github.com/apache/spark/pull/42577\n *\n * @param clusterBySpec: clusterBySpec which contains the clustering columns.\n * @param startIndex: start index of CLUSTER BY clause.\n * @param stopIndex: stop index of CLUSTER BY clause.\n * @param parenStartIndex: start index of the left parenthesis in CLUSTER BY clause.\n * @param parenStopIndex: stop index of the right parenthesis in CLUSTER BY clause.\n * @param ctx: parser rule context of the CLUSTER BY clause.\n */\ncase class ClusterByPlan(\n    clusterBySpec: ClusterBySpec,\n    startIndex: Int,\n    stopIndex: Int,\n    parenStartIndex: Int,\n    parenStopIndex: Int,\n    ctx: ParserRuleContext)\n    extends LeafNode {\n  override def withNewChildrenInternal(newChildren: IndexedSeq[LogicalPlan]): LogicalPlan = this\n  override def output: Seq[Attribute] = Seq.empty\n}\n\n/**\n * Parser utils for parsing a [[ClusterByPlan]] and converts it to table properties.\n *\n * This class will be removed when we integrate with OSS Spark's CLUSTER BY implementation.\n * @see https://github.com/apache/spark/pull/42577\n *\n * @param clusterByPlan: the ClusterByPlan to parse.\n * @param delegate: delegate parser.\n */\ncase class ClusterByParserUtils(clusterByPlan: ClusterByPlan, delegate: ParserInterface) {\n  // Update partitioning to include clustering columns as transforms.\n  private def updatePartitioning(partitioning: Seq[Transform]): Seq[Transform] = {\n    // Validate no bucketing is specified.\n    if (partitioning.exists(t => t.isInstanceOf[BucketTransform])) {\n      ParserUtils.operationNotAllowed(\n        \"Clustering and bucketing cannot both be specified. \" +\n          \"Please remove CLUSTERED BY INTO BUCKETS if you \" +\n          \"want to create a Delta table with clustering\",\n        clusterByPlan.ctx)\n    }\n    Seq(ClusterByTransform(clusterByPlan.clusterBySpec.columnNames))\n  }\n\n  /**\n   * Parse the [[ClusterByPlan]] by replacing CLUSTER BY with PARTITIONED BY and\n   * leverage Spark SQL parser to perform the validation. After parsing, store the\n   * clustering columns in the logical plan's partitioning transforms.\n   *\n   * @param sqlText: original SQL text.\n   * @return the logical plan after parsing.\n   */\n  def parsePlan(sqlText: String): LogicalPlan = {\n    val colText =\n      sqlText.substring(clusterByPlan.parenStartIndex, clusterByPlan.parenStopIndex + 1)\n    // Replace CLUSTER BY with PARTITIONED BY to let SparkSqlParser do the validation for us.\n    // This serves as a short-term workaround until Spark incorporates CREATE TABLE ... CLUSTER BY\n    // syntax.\n    val partitionedByText = \"PARTITIONED BY \" + colText\n    val newSqlText =\n      sqlText.substring(0, clusterByPlan.startIndex) +\n        partitionedByText +\n        sqlText.substring(clusterByPlan.stopIndex + 1)\n    try {\n      delegate.parsePlan(newSqlText) match {\n        case create: CreateTable =>\n          create.copy(partitioning = updatePartitioning(create.partitioning))\n        case ctas: CreateTableAsSelect =>\n          ctas.copy(partitioning = updatePartitioning(ctas.partitioning))\n        case replace: ReplaceTable =>\n          replace.copy(partitioning = updatePartitioning(replace.partitioning))\n        case rtas: ReplaceTableAsSelect =>\n          rtas.copy(partitioning = updatePartitioning(rtas.partitioning))\n        case plan => plan\n      }\n    } catch {\n      case e: ParseException if (e.errorClass.contains(\"DUPLICATE_CLAUSES\")) =>\n        // Since we replace CLUSTER BY with PARTITIONED BY, duplicated clauses means we\n        // encountered CLUSTER BY with PARTITIONED BY.\n        ParserUtils.operationNotAllowed(\n          \"Clustering and partitioning cannot both be specified. \" +\n            \"Please remove PARTITIONED BY if you want to create a Delta table with clustering\",\n          clusterByPlan.ctx)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/skipping/clustering/temp/ClusterByTransform.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering.temp\n\nimport org.apache.spark.sql.connector.expressions.{Expression, NamedReference, Transform}\n\n/**\n * Minimal version of Spark's ClusterByTransform. We'll remove this when we integrate with OSS\n * Spark's CLUSTER BY implementation.\n *\n * This class represents a transform for `ClusterBySpec`. This is used to bundle\n * ClusterBySpec in CreateTable's partitioning transforms to pass it down to analyzer/delta.\n */\nfinal case class ClusterByTransform(\n    columnNames: Seq[NamedReference]) extends Transform {\n\n  override val name: String = \"temp_cluster_by\"\n\n  override def arguments: Array[Expression] = columnNames.toArray\n\n  override def toString: String = s\"$name(${arguments.map(_.describe).mkString(\", \")})\"\n}\n\n/**\n * Convenience extractor for ClusterByTransform.\n */\nobject ClusterByTransform {\n  def unapply(transform: Transform): Option[Seq[NamedReference]] =\n    transform match {\n      case NamedTransform(\"temp_cluster_by\", arguments) =>\n        Some(arguments.map(_.asInstanceOf[NamedReference]))\n      case _ =>\n        None\n    }\n}\n\n/**\n * Copied from OSS Spark. We'll remove this when we integrate with OSS Spark's CLUSTER BY.\n * Convenience extractor for any Transform.\n */\nprivate object NamedTransform {\n  def unapply(transform: Transform): Some[(String, Seq[Expression])] = {\n    Some((transform.name, transform.arguments))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaDataSource.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.util.{Failure, Success, Try}\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.DatabricksLogging\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.WriteIntoDelta\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.{PartitionUtils, Utils}\nimport org.apache.hadoop.fs.Path\nimport org.json4s.{Formats, NoTypeHints}\nimport org.json4s.jackson.Serialization\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{EqualTo, Expression, Literal}\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.connector.catalog.{SupportsV1OverwriteWithSaveAsTable, Table, TableProvider}\nimport org.apache.spark.sql.connector.expressions.Transform\nimport org.apache.spark.sql.execution.streaming.{Sink, Source}\nimport org.apache.spark.sql.sources._\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types.{DataType, StructType, VariantType}\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n\n/** A DataSource V1 for integrating Delta into Spark SQL batch and Streaming APIs. */\nclass DeltaDataSource\n  extends RelationProvider\n  with StreamSourceProvider\n  with StreamSinkProvider\n  with CreatableRelationProvider\n  with DataSourceRegister\n  with TableProvider\n  with SupportsV1OverwriteWithSaveAsTable\n  with DeltaLogging {\n\n  /**\n   * WARNING: This field has complex initialization timing.\n   *\n   * This field is not initialized in the constructor because the DataSource V1 API does not allow\n   * for passing a catalog table. As a work around, we set this field immediately after the\n   * `DeltaDataSource` is constructed in `DataSource::providingInstance()`.\n   */\n  private var catalogTableOpt: Option[CatalogTable] = None\n\n  /**\n   * Internal method used only by `DataSource.providingInstance()` right after `DeltaDataSource`\n   * construction to plumb the catalog table. This is intended to be set once per instance;\n   * subsequent sets are ignored by a guard.\n   */\n  def setCatalogTableOpt(newCatalogTableOpt: Option[CatalogTable]): Unit = {\n    if (catalogTableOpt.isEmpty) {\n      catalogTableOpt = newCatalogTableOpt\n    }\n  }\n\n  /**\n   * Construct a snapshot from either the catalog table or a path.\n   *\n   * If catalogTableOpt is defined, use it to construct the snapshot; otherwise, fall back to use\n   * path-based snapshot construction.\n   */\n  private def getSnapshotFromTableOrPath(\n      sparkSession: SparkSession,\n      path: Path,\n      options: Map[String, String]): Snapshot = {\n    catalogTableOpt\n      .map(catalogTable => DeltaLog.forTableWithSnapshot(\n        sparkSession, catalogTable, options))\n      .getOrElse(DeltaLog.forTableWithSnapshot(sparkSession, path, options))._2\n  }\n\n  def inferSchema: StructType = new StructType() // empty\n\n  override def inferSchema(options: CaseInsensitiveStringMap): StructType = inferSchema\n\n  override def getTable(\n      schema: StructType,\n      partitioning: Array[Transform],\n      properties: java.util.Map[String, String]): Table = {\n    val options = new CaseInsensitiveStringMap(properties)\n    val path = options.get(\"path\")\n    if (path == null) throw DeltaErrors.pathNotSpecifiedException\n    DeltaTableV2(SparkSession.active, new Path(path), options = options.asScala.toMap)\n  }\n\n  override def sourceSchema(\n      sqlContext: SQLContext,\n      schema: Option[StructType],\n      providerName: String,\n      parameters: Map[String, String]): (String, StructType) = {\n    val options = new CaseInsensitiveStringMap(parameters.asJava)\n    // Check if we should bypass DeltaLog schema loading for UC-managed tables.\n    // DeltaV2Mode checks the parameters map for UC markers and returns true for\n    // AUTO/STRICT modes with UC tables.\n    val deltaV2Mode = new DeltaV2Mode(sqlContext.sparkSession.sessionState.conf)\n    if (schema.isDefined &&\n        deltaV2Mode.shouldBypassSchemaValidationForStreaming(parameters.asJava)) {\n      require(!CDCReader.isCDCRead(options), \"CDC read is not supported for schema bypass.\")\n      return (shortName(), schema.get)\n    }\n    val path = parameters.getOrElse(\"path\", {\n      throw DeltaErrors.pathNotSpecifiedException\n    })\n\n    val (_, maybeTimeTravel) = DeltaTableUtils.extractIfPathContainsTimeTravel(\n      sqlContext.sparkSession, path, Map.empty)\n    if (maybeTimeTravel.isDefined) throw DeltaErrors.timeTravelNotSupportedException\n    if (DeltaDataSource.getTimeTravelVersion(parameters).isDefined) {\n      throw DeltaErrors.timeTravelNotSupportedException\n    }\n\n    val snapshot =\n      getSnapshotFromTableOrPath(sqlContext.sparkSession, new Path(path), parameters)\n    // This is the analyzed schema for Delta streaming\n    val readSchema = {\n      // Check if we would like to merge consecutive schema changes, this would allow customers\n      // to write queries based on their latest changes instead of an arbitrary schema in the past.\n      val shouldMergeConsecutiveSchemas = sqlContext.sparkSession.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING_MERGE_CONSECUTIVE_CHANGES\n      )\n      // This method is invoked during the analysis phase and would determine the schema for the\n      // streaming dataframe. We only need to merge consecutive schema changes here because the\n      // process would create a new entry in the schema log such that when the schema log is\n      // looked up again in the execution phase, we would use the correct schema.\n      DeltaDataSource.getMetadataTrackingLogForDeltaSource(\n          sqlContext.sparkSession, snapshot, catalogTableOpt, parameters,\n          mergeConsecutiveSchemaChanges = shouldMergeConsecutiveSchemas)\n        .flatMap(_.getCurrentTrackedMetadata.map(_.dataSchema))\n        .getOrElse(snapshot.schema)\n    }\n\n    DeltaDataSource.verifyReadSchemaMatchesTheTableSchema(schema, readSchema)\n\n    val schemaToUse = DeltaTableUtils.removeInternalDeltaMetadata(\n      sqlContext.sparkSession,\n      DeltaTableUtils.removeInternalWriterMetadata(sqlContext.sparkSession, readSchema)\n    )\n    if (schemaToUse.isEmpty) {\n      throw DeltaErrors.schemaNotSetException\n    }\n    if (CDCReader.isCDCRead(options)) {\n      (shortName(), CDCReader.cdcReadSchema(schemaToUse))\n    } else {\n      (shortName(), schemaToUse)\n    }\n  }\n\n  override def createSource(\n      sqlContext: SQLContext,\n      metadataPath: String,\n      schema: Option[StructType],\n      providerName: String,\n      parameters: Map[String, String]): Source = {\n    val path = parameters.getOrElse(\"path\", {\n      throw DeltaErrors.pathNotSpecifiedException\n    })\n    val options = new DeltaOptions(parameters, sqlContext.sparkSession.sessionState.conf)\n    val snapshot =\n      getSnapshotFromTableOrPath(sqlContext.sparkSession, new Path(path), parameters)\n    val schemaTrackingLogOpt =\n      DeltaDataSource.getMetadataTrackingLogForDeltaSource(\n        sqlContext.sparkSession, snapshot, catalogTableOpt, parameters,\n        // Pass in the metadata path opt so we can use it for validation\n        sourceMetadataPathOpt = Some(metadataPath))\n\n    val readSchema = schemaTrackingLogOpt.flatMap(_.getCurrentTrackedMetadata).map { metadata =>\n      logInfo(log\"Delta source schema fetched from tracking log version \" +\n        log\"${MDC(DeltaLogKeys.VERSION2, schemaTrackingLogOpt.get.getCurrentTrackedSeqNum)}\" +\n        log\" with Delta commit version \" +\n        log\"${MDC(DeltaLogKeys.VERSION2, metadata.deltaCommitVersion)}\")\n      metadata.dataSchema\n    }.getOrElse {\n      logInfo(log\"Delta source schema fetched from Delta snapshot version \" +\n        log\"${MDC(DeltaLogKeys.VERSION2, snapshot.version)}\")\n      snapshot.schema\n    }\n\n    DeltaDataSource.verifyReadSchemaMatchesTheTableSchema(schema, readSchema)\n\n    if (readSchema.isEmpty) {\n      throw DeltaErrors.schemaNotSetException\n    }\n    DeltaSource(\n      sqlContext.sparkSession,\n      snapshot.deltaLog,\n      catalogTableOpt,\n      options,\n      snapshot,\n      metadataPath,\n      schemaTrackingLogOpt\n    )\n  }\n\n  override def createSink(\n      sqlContext: SQLContext,\n      parameters: Map[String, String],\n      partitionColumns: Seq[String],\n      outputMode: OutputMode): Sink = {\n    val path = parameters.getOrElse(\"path\", {\n      throw DeltaErrors.pathNotSpecifiedException\n    })\n    if (outputMode != OutputMode.Append && outputMode != OutputMode.Complete) {\n      throw DeltaErrors.outputModeNotSupportedException(getClass.getName, outputMode.toString)\n    }\n    val deltaOptions = new DeltaOptions(parameters, sqlContext.sparkSession.sessionState.conf)\n    // NOTE: Spark API doesn't give access to the CatalogTable here, but DeltaAnalysis will pick\n    // that info out of the containing WriteToStream (if present), and update the sink there.\n    new DeltaSink(sqlContext, new Path(path), partitionColumns, outputMode, deltaOptions)\n  }\n\n  override def createRelation(\n      sqlContext: SQLContext,\n      mode: SaveMode,\n      parameters: Map[String, String],\n      data: DataFrame): BaseRelation = {\n    val path = parameters.getOrElse(\"path\", {\n      throw DeltaErrors.pathNotSpecifiedException\n    })\n    val partitionColumns = parameters.get(DeltaSourceUtils.PARTITIONING_COLUMNS_KEY)\n      .map(DeltaDataSource.decodePartitioningColumns)\n      .getOrElse(Nil)\n\n    val deltaLog = Utils.getDeltaLogFromTableOrPath(\n      sqlContext.sparkSession, catalogTableOpt, new Path(path), parameters)\n    WriteIntoDelta(\n      deltaLog = deltaLog,\n      mode = mode,\n      new DeltaOptions(parameters, sqlContext.sparkSession.sessionState.conf),\n      partitionColumns = partitionColumns,\n      configuration = DeltaConfigs.validateConfigurations(\n        parameters.filterKeys(_.startsWith(\"delta.\")).toMap),\n      data = data,\n      // empty catalogTable is acceptable as the code path is only for path based writes\n      // (df.write.save(\"path\")) which does not need to use/update catalog\n      catalogTableOpt = None\n      ).run(sqlContext.sparkSession)\n\n    deltaLog.createRelation(catalogTableOpt = catalogTableOpt)\n  }\n\n  override def createRelation(\n      sqlContext: SQLContext,\n      parameters: Map[String, String]): BaseRelation = {\n    recordFrameProfile(\"Delta\", \"DeltaDataSource.createRelation\") {\n      val maybePath = parameters.getOrElse(\"path\", {\n        throw DeltaErrors.pathNotSpecifiedException\n      })\n\n      // Log any invalid options that are being passed in\n      DeltaOptions.verifyOptions(CaseInsensitiveMap(parameters))\n\n      val timeTravelByParams = DeltaDataSource.getTimeTravelVersion(parameters)\n      var cdcOptions: mutable.Map[String, String] = mutable.Map.empty\n      val caseInsensitiveParams = new CaseInsensitiveStringMap(parameters.asJava)\n      if (CDCReader.isCDCRead(caseInsensitiveParams)) {\n        cdcOptions = mutable.Map[String, String](DeltaDataSource.CDC_ENABLED_KEY -> \"true\")\n        if (caseInsensitiveParams.containsKey(DeltaDataSource.CDC_START_VERSION_KEY)) {\n          cdcOptions(DeltaDataSource.CDC_START_VERSION_KEY) = caseInsensitiveParams.get(\n            DeltaDataSource.CDC_START_VERSION_KEY)\n        }\n        if (caseInsensitiveParams.containsKey(DeltaDataSource.CDC_START_TIMESTAMP_KEY)) {\n          cdcOptions(DeltaDataSource.CDC_START_TIMESTAMP_KEY) = caseInsensitiveParams.get(\n            DeltaDataSource.CDC_START_TIMESTAMP_KEY)\n        }\n        if (caseInsensitiveParams.containsKey(DeltaDataSource.CDC_END_VERSION_KEY)) {\n          cdcOptions(DeltaDataSource.CDC_END_VERSION_KEY) = caseInsensitiveParams.get(\n            DeltaDataSource.CDC_END_VERSION_KEY)\n        }\n        if (caseInsensitiveParams.containsKey(DeltaDataSource.CDC_END_TIMESTAMP_KEY)) {\n          cdcOptions(DeltaDataSource.CDC_END_TIMESTAMP_KEY) = caseInsensitiveParams.get(\n            DeltaDataSource.CDC_END_TIMESTAMP_KEY)\n        }\n      }\n      val dfOptions: Map[String, String] =\n        if (sqlContext.sparkSession.sessionState.conf.getConf(\n            DeltaSQLConf.LOAD_FILE_SYSTEM_CONFIGS_FROM_DATAFRAME_OPTIONS)) {\n          parameters ++ cdcOptions\n        } else {\n          cdcOptions.toMap\n        }\n      DeltaTableV2(\n        sqlContext.sparkSession,\n        new Path(maybePath),\n        timeTravelOpt = timeTravelByParams,\n        options = dfOptions\n      ).toBaseRelation\n    }\n  }\n\n  /**\n   * Extend the default `supportsDataType` to allow VariantType.\n   */\n  override def supportsDataType(dt: DataType): Boolean = {\n    dt.isInstanceOf[VariantType] || super.supportsDataType(dt)\n  }\n\n  override def shortName(): String = {\n    DeltaSourceUtils.ALT_NAME\n  }\n\n}\n\nobject DeltaDataSource extends DatabricksLogging {\n  private implicit val formats: Formats = Serialization.formats(NoTypeHints)\n\n  final val TIME_TRAVEL_SOURCE_KEY = \"__time_travel_source__\"\n\n  /**\n   * The option key for time traveling using a timestamp. The timestamp should be a valid\n   * timestamp string which can be cast to a timestamp type.\n   */\n  final val TIME_TRAVEL_TIMESTAMP_KEY = \"timestampAsOf\"\n\n  /**\n   * The option key for time traveling using a version of a table. This value should be\n   * castable to a long.\n   */\n  final val TIME_TRAVEL_VERSION_KEY = \"versionAsOf\"\n\n  final val CDC_START_VERSION_KEY = \"startingVersion\"\n\n  final val CDC_START_TIMESTAMP_KEY = \"startingTimestamp\"\n\n  final val CDC_END_VERSION_KEY = \"endingVersion\"\n\n  final val CDC_END_TIMESTAMP_KEY = \"endingTimestamp\"\n\n  final val CDC_ENABLED_KEY = \"readChangeFeed\"\n\n  final val CDC_ENABLED_KEY_LEGACY = \"readChangeData\"\n\n  def encodePartitioningColumns(columns: Seq[String]): String = {\n    Serialization.write(columns)\n  }\n\n  def decodePartitioningColumns(str: String): Seq[String] = {\n    Serialization.read[Seq[String]](str)\n  }\n\n  /**\n   * For Delta, we allow certain magic to be performed through the paths that are provided by users.\n   * Normally, a user specified path should point to the root of a Delta table. However, some users\n   * are used to providing specific partition values through the path, because of how expensive it\n   * was to perform partition discovery before. We treat these partition values as logical partition\n   * filters, if a table does not exist at the provided path.\n   *\n   * In addition, we allow users to provide time travel specifications through the path. This is\n   * provided after an `@` symbol after a path followed by a time specification in\n   * `yyyyMMddHHmmssSSS` format, or a version number preceded by a `v`.\n   *\n   * This method parses these specifications and returns these modifiers only if a path does not\n   * really exist at the provided path. We first parse out the time travel specification, and then\n   * the partition filters. For example, a path specified as:\n   *      /some/path/partition=1@v1234\n   * will be parsed into `/some/path` with filters `partition=1` and a time travel spec of version\n   * 1234.\n   *\n   * @return A tuple of the root path of the Delta table, partition filters, and time travel options\n   */\n  def parsePathIdentifier(\n      spark: SparkSession,\n      userPath: String,\n      options: Map[String, String]): (Path, Seq[(String, String)], Option[DeltaTimeTravelSpec]) = {\n    // Handle time travel\n    val (path, timeTravelByPath) =\n      DeltaTableUtils.extractIfPathContainsTimeTravel(spark, userPath, options)\n\n    val hadoopPath = new Path(path)\n    val rootPath =\n      DeltaTableUtils.findDeltaTableRoot(spark, hadoopPath, options).getOrElse(hadoopPath)\n\n    val partitionFilters = if (rootPath != hadoopPath) {\n      logConsole(\n        \"\"\"\n          |WARNING: loading partitions directly with delta is not recommended.\n          |If you are trying to read a specific partition, use a where predicate.\n          |\n          |CORRECT: spark.read.format(\"delta\").load(\"/data\").where(\"part=1\")\n          |INCORRECT: spark.read.format(\"delta\").load(\"/data/part=1\")\n        \"\"\".stripMargin)\n\n      val fragment = hadoopPath.toString.substring(rootPath.toString.length() + 1)\n      try {\n        PartitionUtils.parsePathFragmentAsSeq(fragment)\n      } catch {\n        case _: ArrayIndexOutOfBoundsException =>\n          throw DeltaErrors.partitionPathParseException(fragment)\n      }\n    } else {\n      Nil\n    }\n\n    (rootPath, partitionFilters, timeTravelByPath)\n  }\n\n  /**\n   * Verifies that the provided partition filters are valid and returns the corresponding\n   * expressions.\n   */\n  def verifyAndCreatePartitionFilters(\n      userPath: String,\n      snapshot: Snapshot,\n      partitionFilters: Seq[(String, String)]): Seq[Expression] = {\n    if (partitionFilters.nonEmpty) {\n      val metadata = snapshot.metadata\n\n      val badColumns = partitionFilters.map(_._1).filterNot(metadata.partitionColumns.contains)\n      if (badColumns.nonEmpty) {\n        val fragment = partitionFilters.map(f => s\"${f._1}=${f._2}\").mkString(\"/\")\n        throw DeltaErrors.partitionPathInvolvesNonPartitionColumnException(badColumns, fragment)\n      }\n\n      val filters = partitionFilters.map { case (key, value) =>\n        // Nested fields cannot be partitions, so we pass the key as a identifier\n        EqualTo(UnresolvedAttribute(Seq(key)), Literal(value))\n      }\n      val files = DeltaLog.filterFileList(\n        metadata.partitionSchema, snapshot.allFiles.toDF(), filters)\n      if (files.count() == 0) {\n        throw DeltaErrors.pathNotExistsException(userPath)\n      }\n      filters\n    } else {\n      Nil\n    }\n  }\n\n  /** Extracts whether users provided the option to time travel a relation. */\n  def getTimeTravelVersion(parameters: Map[String, String]): Option[DeltaTimeTravelSpec] = {\n    val caseInsensitive = CaseInsensitiveMap[String](parameters)\n    val tsOpt = caseInsensitive.get(DeltaDataSource.TIME_TRAVEL_TIMESTAMP_KEY)\n    val versionOpt = caseInsensitive.get(DeltaDataSource.TIME_TRAVEL_VERSION_KEY)\n    val sourceOpt = caseInsensitive.get(DeltaDataSource.TIME_TRAVEL_SOURCE_KEY)\n\n    if (tsOpt.isDefined && versionOpt.isDefined) {\n      throw DeltaErrors.provideOneOfInTimeTravel\n    } else if (tsOpt.isDefined) {\n      Some(DeltaTimeTravelSpec(Some(Literal(tsOpt.get)), None, sourceOpt.orElse(Some(\"dfReader\"))))\n    } else if (versionOpt.isDefined) {\n      val version = Try(versionOpt.get.toLong) match {\n        case Success(v) => v\n        case Failure(t) =>\n          throw DeltaErrors.timeTravelInvalidBeginValue(DeltaDataSource.TIME_TRAVEL_VERSION_KEY, t)\n      }\n      Some(DeltaTimeTravelSpec(None, Some(version), sourceOpt.orElse(Some(\"dfReader\"))))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Extract the schema tracking location from options.\n   */\n  def extractSchemaTrackingLocationConfig(\n      spark: SparkSession, parameters: Map[String, String]): Option[String] = {\n    val options = new CaseInsensitiveStringMap(parameters.asJava)\n\n    Option(options.get(DeltaOptions.SCHEMA_TRACKING_LOCATION))\n      .orElse(Option(options.get(DeltaOptions.SCHEMA_TRACKING_LOCATION_ALIAS)))\n  }\n\n  /**\n   * Create a schema log for Delta streaming source if possible\n   */\n  def getMetadataTrackingLogForDeltaSource(\n      spark: SparkSession,\n      sourceSnapshot: SnapshotDescriptor,\n      catalogTableOpt: Option[CatalogTable],\n      parameters: Map[String, String],\n      sourceMetadataPathOpt: Option[String] = None,\n      mergeConsecutiveSchemaChanges: Boolean = false): Option[DeltaSourceMetadataTrackingLog] = {\n\n    DeltaDataSource.extractSchemaTrackingLocationConfig(spark, parameters)\n      .map { schemaTrackingLocation =>\n        if (!spark.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING)) {\n          throw new UnsupportedOperationException(\n            \"Schema tracking location is not supported for Delta streaming source\")\n        }\n\n        DeltaSourceMetadataTrackingLog.create(\n          spark,\n          schemaTrackingLocation,\n          sourceSnapshot,\n          catalogTableOpt,\n          parameters,\n          sourceMetadataPathOpt,\n          mergeConsecutiveSchemaChanges\n        )\n      }\n  }\n\n  private def verifyReadSchemaMatchesTheTableSchema(\n                                                     schema: Option[StructType],\n                                                     readSchema: StructType): Unit = {\n    if (schema.nonEmpty && schema.get.nonEmpty &&\n      !DataType.equalsIgnoreCompatibleNullability(readSchema, schema.get)) {\n      throw DeltaErrors.readSourceSchemaConflictException\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSQLConf.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.internal.config.ConfigBuilder\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.network.util.ByteUnit\nimport org.apache.spark.sql.catalyst.FileSourceOptions\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.storage.StorageLevel\n\n/**\n * Utility trait providing common configuration building methods for Delta SQL configs.\n *\n * This trait contains only utility methods and constants, no actual config entries.\n * It is designed to be extended by multiple configuration objects without causing\n * duplicate config registration.\n */\ntrait DeltaSQLConfUtils {\n  val SQL_CONF_PREFIX = \"spark.databricks.delta\"\n\n  def buildConf(key: String): ConfigBuilder = SQLConf.buildConf(s\"$SQL_CONF_PREFIX.$key\")\n  def buildStaticConf(key: String): ConfigBuilder =\n    SQLConf.buildStaticConf(s\"spark.databricks.delta.$key\")\n}\n\n/**\n * [[SQLConf]] entries for Delta features.\n */\ntrait DeltaSQLConfBase extends DeltaSQLConfUtils {\n\n  // Values for flags that also support log-only mode.\n  final object BooleanStringOrLogOnly {\n    final val FALSE = \"false\"\n    final val TRUE = \"true\"\n    final val LOG_ONLY = \"log-only\"\n\n    final val VALUES = Set(FALSE, TRUE, LOG_ONLY)\n  }\n\n  object DeltaBreakingChangeEnum {\n    val OFF = \"OFF\"\n    val LOG_ONLY = \"LOG_ONLY\"\n    val ASSERT = \"ASSERT\"\n    val validValues: Set[String] = Set(OFF, LOG_ONLY, ASSERT)\n  }\n\n  abstract class DeltaBreakingChangeEnum(configEntry: ConfigEntry[String])\n    extends Enumeration {\n    val OFF = Value(\"OFF\")\n    val LOG_ONLY = Value(\"LOG_ONLY\")\n    val ASSERT = Value(\"ASSERT\")\n\n    def fromConf(conf: SQLConf): Value =\n      withName(conf.getConf(configEntry))\n\n    def default: Value =\n      withName(configEntry.defaultValueString)\n\n    def confName: String = configEntry.key\n  }\n\n  val RESOLVE_TIME_TRAVEL_ON_IDENTIFIER =\n    buildConf(\"timeTravel.resolveOnIdentifier.enabled\")\n      .internal()\n      .doc(\"When true, we will try to resolve patterns as `@v123` in identifiers as time \" +\n        \"travel nodes.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_COMMIT_LOCK_ENABLED =\n    buildConf(\"commitLock.enabled\")\n      .internal()\n      .doc(\"Whether to lock a Delta table when doing a commit.\")\n      .booleanConf\n      .createOptional\n\n  val DELTA_COLLECT_STATS =\n    buildConf(\"stats.collect\")\n      .internal()\n      .doc(\"When true, statistics are collected while writing files into a Delta table.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_DML_METRICS_FROM_METADATA =\n    buildConf(\"dmlMetricsFromMetadata.enabled\")\n      .internal()\n      .doc(\n        \"\"\" When enabled, metadata only Delete, ReplaceWhere and Truncate operations will report row\n        | level operation metrics by reading the file statistics for number of rows.\n        | \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_COLLECT_STATS_USING_TABLE_SCHEMA =\n    buildConf(\"stats.collect.using.tableSchema\")\n      .internal()\n      .doc(\"When collecting stats while writing files into Delta table\" +\n        s\" (${DELTA_COLLECT_STATS.key} needs to be true), whether to use the table schema (true)\" +\n        \" or the DataFrame schema (false) as the stats collection schema.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_USER_METADATA =\n    buildConf(\"commitInfo.userMetadata\")\n      .doc(\"Arbitrary user-defined metadata to include in CommitInfo.\")\n      .stringConf\n      .createOptional\n\n  val DELTA_FORCE_ALL_COMMIT_STATS =\n    buildConf(\"commitStats.force\")\n      .internal()\n      .doc(\n        \"\"\"When true, forces commit statistics to be collected for logging purposes.\n        | Enabling this feature requires the Snapshot State to be computed, which is\n        | potentially expensive.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CONVERT_USE_METADATA_LOG =\n    buildConf(\"convert.useMetadataLog\")\n      .doc(\n        \"\"\" When converting to a Parquet table that was created by Structured Streaming, whether\n        |  to use the transaction log under `_spark_metadata` as the source of truth for files\n        | contained in the table.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CONVERT_USE_CATALOG_PARTITIONS =\n    buildConf(\"convert.useCatalogPartitions\")\n      .internal()\n      .doc(\n        \"\"\" When converting a catalog Parquet table, whether to use the partition information from\n          | the Metastore catalog and only commit files under the directories of active partitions.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CONVERT_USE_CATALOG_SCHEMA =\n    buildConf(\"convert.useCatalogSchema\")\n      .doc(\n        \"\"\" When converting to a catalog Parquet table, whether to use the catalog schema as the\n          | source of truth.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CONVERT_PARTITION_VALUES_IGNORE_CAST_FAILURE =\n    buildConf(\"convert.partitionValues.ignoreCastFailure\")\n      .doc(\n        \"\"\" When converting to Delta, ignore the failure when casting a partition value to\n        | the specified data type, in which case the partition column will be filled with null.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CONVERT_ICEBERG_USE_NATIVE_PARTITION_VALUES =\n    buildConf(\"convert.iceberg.useNativePartitionValues\")\n      .doc(\n        \"\"\" When enabled, obtain the partition values from Iceberg table's metadata, instead\n          | of inferring from file paths.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_SNAPSHOT_PARTITIONS =\n    buildConf(\"snapshotPartitions\")\n      .internal()\n      .doc(\"Number of partitions to use when building a Delta Lake snapshot.\")\n      .intConf\n      .checkValue(n => n > 0, \"Delta snapshot partition number must be positive.\")\n      .createOptional\n\n  val DELTA_SNAPSHOT_LOADING_MAX_RETRIES =\n    buildConf(\"snapshotLoading.maxRetries\")\n      .internal()\n      .doc(\"How many times to retry when failing to load a snapshot. Each retry will try to use \" +\n        \"a different checkpoint in order to skip potential corrupt checkpoints.\")\n      .intConf\n      .checkValue(n => n >= 0, \"must not be negative.\")\n      .createWithDefault(2)\n\n  val DELTA_SNAPSHOT_CACHE_STORAGE_LEVEL =\n    buildConf(\"snapshotCache.storageLevel\")\n      .internal()\n      .doc(\"StorageLevel to use for caching the DeltaLog Snapshot. In general, this should not \" +\n        \"be used unless you are pretty sure that caching has a negative impact.\")\n      .stringConf\n      .createWithDefault(\"MEMORY_AND_DISK_SER\")\n\n  val DELTA_SNAPSHOT_LOGGING_MAX_FILES_THRESHOLD =\n    buildConf(\"snapshot.logging.maxFilesThreshold\")\n      .internal()\n      .doc(\"Threshold for number of files in a snapshot. When exceeded, emits a warning with \" +\n        \"remediation hints. Set to 0 to disable snapshot logging completely.\")\n      .longConf\n      .checkValue(_ >= 0, \"must be non-negative\")\n      .createWithDefault(500000L)\n\n  val DELTA_PARTITION_COLUMN_CHECK_ENABLED =\n    buildConf(\"partitionColumnValidity.enabled\")\n      .internal()\n      .doc(\"Whether to check whether the partition column names have valid names, just like \" +\n        \"the data columns.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_PARTITION_COLUMN_CHANGE_CHECK = buildConf(\"partitionColumnChangeCheck\")\n    .internal()\n    .doc(\"\"\"Controls the validation behavior when changes to partition columns are detected.\n           |Possible values:\n           |  - \"false\": Disables validation for partition column changes.\n           |  - \"true\": Enables validation and throws an error if an illegal change is detected.\n           |  - \"log-only\": Logs the detected illegal change but does not block the operation.\n           |\"\"\".stripMargin)\n    .stringConf\n    .transform(_.toLowerCase(Locale.ROOT))\n    .checkValues(BooleanStringOrLogOnly.VALUES)\n    .createWithDefault(\n      if (DeltaUtils.isTesting) {\n        BooleanStringOrLogOnly.TRUE\n      } else {\n        BooleanStringOrLogOnly.LOG_ONLY\n      }\n    )\n\n  val DELTA_COMMIT_VALIDATION_ENABLED =\n    buildConf(\"commitValidation.enabled\")\n      .internal()\n      .doc(\"Whether to perform validation checks before commit or not.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_EMPTY_FILE_CHECK_THROW_ENABLED =\n    buildConf(\"emptyFileCheck.throwEnabled\")\n      .internal()\n      .doc(\"When true, throws IllegalStateException if a commit contains AddFile actions \" +\n        \"referencing parquet files with size 0 bytes. \" +\n        \"When false, only logs. Logging always occurs regardless of this setting.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_NULL_PARTITION_CHECK_THROW_ENABLED =\n    buildConf(\"nullPartitionCheck.throwEnabled\")\n      .internal()\n      .doc(\"When true, throws IllegalStateException if a commit contains AddFile actions with \" +\n        \"null partition values for columns that have NOT NULL constraints. \" +\n        \"When false, only logs. Logging always occurs regardless of this setting.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_SCHEMA_ON_READ_CHECK_ENABLED =\n    buildConf(\"checkLatestSchemaOnRead\")\n      .doc(\"In Delta, we always try to give users the latest version of their data without \" +\n        \"having to call REFRESH TABLE or redefine their DataFrames when used in the context of \" +\n        \"streaming. There is a possibility that the schema of the latest version of the table \" +\n        \"may be incompatible with the schema at the time of DataFrame creation. This flag \" +\n        \"enables a check that ensures that users won't read corrupt data if the source schema \" +\n        \"changes in an incompatible way.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_INCLUDE_TABLE_ID_IN_FILE_INDEX_COMPARISON =\n    buildConf(\"includeTableIdInFileIndexComparison\")\n      .internal()\n      .doc(\n        \"\"\"\n          |Include the deltaLog.unsafeVolatileTableId field in equals and hashCode for\n          |TahoeLogFileIndex. The field is unstable, so including it can lead semantic violations\n          |for equals and hashCode.\"\"\".stripMargin)\n      .booleanConf\n      // TODO: Phase this out towards `false` eventually remove the flag altogether again.\n      .createWithDefault(true)\n\n  val DELTA_ALLOW_CREATE_EMPTY_SCHEMA_TABLE =\n    buildConf(\"createEmptySchemaTable.enabled\")\n      .internal()\n      .doc(\n        s\"\"\"If enabled, creating a Delta table with an empty schema will be allowed through SQL API\n           |`CREATE TABLE table () USING delta ...`, or Delta table APIs.\n           |Creating a Delta table with empty schema table using dataframe operations or\n           |`CREATE OR REPLACE` syntax are not supported.\n           |The result Delta table can be updated using schema evolution operations such as\n           |`df.save()` with `mergeSchema = true`.\n           |Reading the empty schema table using DataframeReader or `SELECT` is not allowed.\n           |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val AUTO_COMPACT_ALLOWED_VALUES = Seq(\n    \"false\",\n    \"true\"\n  )\n\n  val DELTA_AUTO_COMPACT_ENABLED =\n    buildConf(\"autoCompact.enabled\")\n      .doc(s\"\"\"Whether to compact files after writes made into Delta tables from this session. This\n        | conf can be set to \"true\" to enable Auto Compaction, OR \"false\" to disable Auto Compaction\n        | on all writes across all delta tables in this session.\n        | \"\"\".stripMargin)\n      .stringConf\n      .transform(_.toLowerCase(Locale.ROOT))\n      .checkValue(AUTO_COMPACT_ALLOWED_VALUES.contains(_),\n        \"\"\"\"spark.databricks.delta.autoCompact.enabled\" must be one of: \"\"\" +\n          s\"\"\"${AUTO_COMPACT_ALLOWED_VALUES.mkString(\"(\", \",\", \")\")}\"\"\")\n      .createOptional\n\n  val DELTA_AUTO_COMPACT_RECORD_PARTITION_STATS_ENABLED =\n    buildConf(\"autoCompact.recordPartitionStats.enabled\")\n      .internal()\n      .doc(s\"\"\"When enabled, each committed write delta transaction records the number of qualified\n              |files of each partition of the target table for Auto Compact in driver's\n              |memory.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_AUTO_COMPACT_EARLY_SKIP_PARTITION_TABLE_ENABLED =\n    buildConf(\"autoCompact.earlySkipPartitionTable.enabled\")\n      .internal()\n      .doc(s\"\"\"Auto Compaction will be skipped if there is no partition with\n              |sufficient number of small files.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_AUTO_COMPACT_MAX_TABLE_PARTITION_STATS =\n    buildConf(\"autoCompact.maxTablePartitionStats\")\n      .internal()\n      .doc(\n        s\"\"\"The maximum number of Auto Compaction partition statistics of each table. This controls\n           |the maximum number of partitions statistics each delta table can have. Increasing\n           |this value reduces the hash conflict and makes partitions statistics more accurate with\n           |the cost of more memory consumption.\n           |\"\"\".stripMargin)\n      .intConf\n      .checkValue(_ > 0, \"The value of maxTablePartitionStats should be positive.\")\n      .createWithDefault(16 * 1024)\n\n  val DELTA_AUTO_COMPACT_PARTITION_STATS_SIZE =\n    buildConf(\"autoCompact.partitionStatsSize\")\n      .internal()\n      .doc(\n        s\"\"\"The total number of partitions statistics entries can be kept in memory for all\n           |tables in each driver. If this threshold is reached, the partitions statistics of\n           |least recently accessed tables will be evicted out.\"\"\".stripMargin)\n      .intConf\n      .checkValue(_ > 0, \"The value of partitionStatsSize should be positive.\")\n      .createWithDefault(64 * 1024)\n\n  val DELTA_AUTO_COMPACT_MAX_FILE_SIZE =\n    buildConf(\"autoCompact.maxFileSize\")\n      .internal()\n      .doc(s\"Target file size produced by auto compaction. The default value of this config\" +\n        \" is 128 MB.\")\n      .longConf\n      .checkValue(_ >= 0, \"maxFileSize has to be positive\")\n      .createWithDefault(128 * 1024 * 1024)\n\n  val DELTA_AUTO_COMPACT_MIN_NUM_FILES =\n    buildConf(\"autoCompact.minNumFiles\")\n      .internal()\n      .doc(\"Number of small files that need to be in a directory before it can be optimized.\")\n      .intConf\n      .checkValue(_ >= 0, \"minNumFiles has to be positive\")\n      .createWithDefault(50)\n\n  val DELTA_AUTO_COMPACT_MIN_FILE_SIZE =\n    buildConf(\"autoCompact.minFileSize\")\n      .internal()\n      .doc(\"Files which are smaller than this threshold (in bytes) will be grouped together and \" +\n        \"rewritten as larger files by the Auto Compaction. The default value of this config \" +\n        s\"is set to half of the config ${DELTA_AUTO_COMPACT_MAX_FILE_SIZE.key}\")\n      .longConf\n      .checkValue(_ >= 0, \"minFileSize has to be positive\")\n      .createOptional\n\n  val DELTA_AUTO_COMPACT_MODIFIED_PARTITIONS_ONLY_ENABLED =\n    buildConf(\"autoCompact.modifiedPartitionsOnly.enabled\")\n      .internal()\n      .doc(\n        s\"\"\"When enabled, Auto Compaction only works on the modified partitions of the delta\n           |transaction that triggers compaction.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_AUTO_COMPACT_NON_BLIND_APPEND_ENABLED =\n    buildConf(\"autoCompact.nonBlindAppend.enabled\")\n      .internal()\n      .doc(\n        s\"\"\"When enabled, Auto Compaction is only triggered by non-blind-append write\n           |transaction.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_AUTO_COMPACT_MAX_NUM_MODIFIED_PARTITIONS =\n    buildConf(\"autoCompact.maxNumModifiedPartitions\")\n      .internal()\n      .doc(\n        s\"\"\"The maximum number of partition can be selected for Auto Compaction when\n           | Auto Compaction runs on modified partition is enabled.\"\"\".stripMargin)\n      .intConf\n      .checkValue(_ > 0, \"The value of maxNumModifiedPartitions should be positive.\")\n      .createWithDefault(128)\n\n  val DELTA_AUTO_COMPACT_RESERVE_PARTITIONS_ENABLED =\n    buildConf(\"autoCompact.reservePartitions.enabled\")\n      .internal()\n      .doc(\n        s\"\"\"When enabled, each Auto Compact thread reserves its target partitions and skips the\n           |partitions that are under Auto Compaction by another thread\n           |concurrently.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_IMPORT_BATCH_SIZE_STATS_COLLECTION =\n    buildConf(\"import.batchSize.statsCollection\")\n      .internal()\n      .doc(\"The number of files per batch for stats collection during import.\")\n      .intConf\n      .createWithDefault(50000)\n\n  val DELTA_IMPORT_BATCH_SIZE_SCHEMA_INFERENCE =\n    buildConf(\"import.batchSize.schemaInference\")\n      .internal()\n      .doc(\"The number of files per batch for schema inference during import.\")\n      .intConf\n      .createWithDefault(1000000)\n\n  val DELTA_SAMPLE_ESTIMATOR_ENABLED =\n    buildConf(\"sampling.enabled\")\n      .internal()\n      .doc(\"Enable sample based estimation.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CONVERT_METADATA_CHECK_ENABLED =\n    buildConf(\"convert.metadataCheck.enabled\")\n      .doc(\n        \"\"\"\n          |If enabled, during convert to delta, if there is a difference between the catalog table's\n          |properties and the Delta table's configuration, we should error. If disabled, merge\n          |the two configurations with the same semantics as update and merge.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STATS_SKIPPING =\n    buildConf(\"stats.skipping\")\n      .internal()\n      .doc(\"When true, statistics are used for skipping\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_ALWAYS_COLLECT_STATS =\n    buildConf(\"alwaysCollectStats.enabled\")\n      .internal()\n      .doc(\"When true, row counts are collected from file statistics even when there are no \" +\n        \"data filters. This is useful for ensuring PreparedDeltaFileIndex always has row count \" +\n        \"information available. Note: this may have a small performance overhead as it requires \" +\n        \"summing numRecords from all files.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_LIMIT_PUSHDOWN_ENABLED =\n    buildConf(\"stats.limitPushdown.enabled\")\n      .internal()\n      .doc(\"If true, use the limit clause and file statistics to prune files before \" +\n        \"they are collected to the driver. \")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_MAX_RETRY_COMMIT_ATTEMPTS =\n    buildConf(\"maxCommitAttempts\")\n      .internal()\n      .doc(\"The maximum number of commit attempts we will try for a single commit before failing\")\n      .intConf\n      .checkValue(_ >= 0, \"maxCommitAttempts has to be positive\")\n      .createWithDefault(10000000)\n\n  val DELTA_MAX_NON_CONFLICT_RETRY_COMMIT_ATTEMPTS =\n    buildConf(\"maxNonConflictCommitAttempts\")\n      .internal()\n      .doc(\"The maximum number of non-conflict commit attempts we will try for a single commit \" +\n        \"before failing\")\n      .intConf\n      .checkValue(_ >= 0, \"maxNonConflictCommitAttempts has to be positive\")\n      .createWithDefault(10)\n\n  val DELTA_CONFLICT_CHECKER_ENFORCE_FEATURE_ENABLEMENT_VALIDATION =\n    buildConf(\"conflictChecker.enforceConcurrentFeatureEnablement.enabled\")\n      .internal()\n      .doc(\"When enabled, the conflict checker will enforce that features that are marked \" +\n        \"as failing concurrent transactions at upgrade, will fail any conflicting commits with \" +\n        \"their enablement protocol changes.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val FEATURE_ENABLEMENT_CONFLICT_RESOLUTION_ENABLED =\n    buildConf(\"featureEnablement.conflictResolution.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Controls whether we attempt to resolve feature enablement with allowlist.\n          |This is only intended to be used as a kill switch.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_PROTOCOL_DEFAULT_WRITER_VERSION =\n    buildConf(\"properties.defaults.minWriterVersion\")\n      .doc(\"The default writer protocol version to create new tables with, unless a feature \" +\n        \"that requires a higher version for correctness is enabled.\")\n      .intConf\n      .checkValues(Set(1, 2, 3, 4, 5, 6, 7))\n      .createWithDefault(2)\n\n  val DELTA_PROTOCOL_DEFAULT_READER_VERSION =\n    buildConf(\"properties.defaults.minReaderVersion\")\n      .doc(\"The default reader protocol version to create new tables with, unless a feature \" +\n        \"that requires a higher version for correctness is enabled.\")\n      .intConf\n      .checkValues(Set(1, 2, 3))\n      .createWithDefault(1)\n\n  val TABLE_FEATURES_TEST_FEATURES_ENABLED =\n    buildConf(\"tableFeatures.testFeatures.enabled\")\n      .internal()\n      .doc(\"Controls whether test features are enabled in testing mode. \" +\n        \"This config is only used for testing purposes. \")\n      .booleanConf\n      .createWithDefault(true)\n\n  val UNSUPPORTED_TESTING_FEATURES_ENABLED =\n    buildConf(\"tableFeatures.dev.unsupportedTableFeatures.enabled\")\n      .internal()\n      .doc(\n        \"\"\"When turned on, it emulates the existence of unsupported features by the client.\n          |This config is only used for testing purposes.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val ALLOW_METADATA_CLEANUP_WHEN_ALL_PROTOCOLS_SUPPORTED =\n    buildConf(\"tableFeatures.allowMetadataCleanupWhenAllProtocolsSupported\")\n      .internal()\n      .doc(\n        \"\"\"Whether to perform protocol validation when the client is unable to clean\n          |up to 'delta.requireCheckpointProtectionBeforeVersion'.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val ALLOW_METADATA_CLEANUP_CHECKPOINT_EXISTENCE_CHECK_DISABLED =\n    buildConf(\"tableFeatures.dev.allowMetadataCleanupCheckpointExistenceCheck.disabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether to disable the checkpoint check at the cleanup boundary when performing\n          |the CheckpointProtectionTableFeature validations.\n          |This is only used for testing purposes.'.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val FAST_DROP_FEATURE_ENABLED =\n    buildConf(\"tableFeatures.fastDropFeature.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether to allow dropping features with the fast drop feature feature\n          |functionality.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val FAST_DROP_FEATURE_DV_DISCOVERY_IN_VACUUM_DISABLED =\n    buildConf(\"tableFeatures.dev.fastDropFeature.DVDiscoveryInVacuum.disabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether to allow DV discovery in Vacuum.\n          |This is config is only intended for testing purposes.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val FAST_DROP_FEATURE_GENERATE_DV_TOMBSTONES =\n    buildConf(\"tableFeatures.fastDropFeature.generateDVTombstones.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether to generate DV tombstones when dropping deletion vectors.\n          |These make sure deletion vector files won't accidentally be vacuumed by clients\n          |that do not support DVs.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefaultFunction(() => SQLConf.get.getConf(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED))\n\n  val FAST_DROP_FEATURE_DV_TOMBSTONE_COUNT_THRESHOLD =\n    buildConf(\"tableFeatures.fastDropFeature.dvTombstoneCountThreshold\")\n      .doc(\n        \"\"\"The maximum number of DV tombstones we are allowed store to memory when dropping\n          |deletion vectors. When the resulting number of DV tombstones is higher, we use\n          |a special commit for large outputs. This does not materialize results to memory\n          |but does not retry in case of a conflict.\"\"\".stripMargin)\n      .intConf\n      .checkValue(_ >= 0, \"DVTombstoneCountThreshold must not be negative.\")\n      .createWithDefault(10000)\n\n  val FAST_DROP_FEATURE_STREAMING_ALWAYS_VALIDATE_PROTOCOL =\n    buildConf(\"tableFeatures.fastDropFeature.alwaysValidateProtocolInStreaming.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether to validate the protocol when starting a stream from arbitrary\n          |versions.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_MAX_SNAPSHOT_LINEAGE_LENGTH =\n    buildConf(\"maxSnapshotLineageLength\")\n      .internal()\n      .doc(\"The max lineage length of a Snapshot before Delta forces to build a Snapshot from \" +\n        \"scratch.\")\n      .intConf\n      .checkValue(_ > 0, \"maxSnapshotLineageLength must be positive.\")\n      .createWithDefault(50)\n\n  val DELTA_REPLACE_COLUMNS_SAFE =\n    buildConf(\"alter.replaceColumns.safe.enabled\")\n      .internal()\n      .doc(\"Prevents an ALTER TABLE REPLACE COLUMNS method from dropping all columns, which \" +\n        \"leads to losing all data. It will only allow safe, unambiguous column changes.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_HISTORY_PAR_SEARCH_THRESHOLD =\n    buildConf(\"history.maxKeysPerList\")\n      .internal()\n      .doc(\"How many commits to list when performing a parallel search. Currently set to 1000, \" +\n        \"which is the maximum keys returned by S3 per list call. Azure can return 5000, \" +\n        \"therefore we choose 1000.\")\n      .intConf\n      .createWithDefault(1000)\n\n  val DELTA_HISTORY_METRICS_ENABLED =\n    buildConf(\"history.metricsEnabled\")\n      .doc(\"Enables Metrics reporting in Describe History. CommitInfo will now record the \" +\n        \"Operation Metrics.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_VACUUM_RETENTION_WINDOW_IGNORE_ENABLED =\n    buildConf(\"vacuum.retentionWindowIgnore.enabled\")\n      .internal()\n      .doc(\"When set, retention window as part of Vacuum will be ignored unless the value is 0\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_HISTORY_MANAGER_THREAD_POOL_SIZE =\n    buildConf(\"history.threadPoolSize\")\n      .internal()\n      .doc(\"The size of the thread pool used for search during DeltaHistory operations. \" +\n        \"This configuration is only used when the feature inCommitTimestamps is enabled.\")\n      .intConf\n      .checkValue(_ > 0, \"history.threadPoolSize must be positive\")\n      .createWithDefault(10)\n\n  val ENFORCE_TIME_TRAVEL_WITHIN_DELETED_FILE_RETENTION_DURATION =\n    buildConf(\"vacuum.enforceTimeTravelWithinDeletedFileRetentionDuration\")\n      .internal()\n      .doc(\"Enforces time travel within delta.deletedFileRetentionDuration.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_VACUUM_LOGGING_ENABLED =\n    buildConf(\"vacuum.logging.enabled\")\n      .doc(\"Whether to log vacuum information into the Delta transaction log.\" +\n        \" Users should only set this config to 'true' when the underlying file system safely\" +\n        \" supports concurrent writes.\")\n      .booleanConf\n      .createOptional\n\n  val LITE_VACUUM_ENABLED =\n    buildConf(\"vacuum.lite.enabled\")\n      .doc(\"Allows Vacuum to be run in Lite mode\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_VACUUM_RETENTION_CHECK_ENABLED =\n    buildConf(\"retentionDurationCheck.enabled\")\n      .doc(\"Adds a check preventing users from running vacuum with a very short retention \" +\n        \"period, which may end up corrupting the Delta Log.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_VACUUM_PARALLEL_DELETE_ENABLED =\n    buildConf(\"vacuum.parallelDelete.enabled\")\n      .doc(\"Enables parallelizing the deletion of files during a vacuum command. Enabling \" +\n        \"may result hitting rate limits on some storage backends. When enabled, parallelization \" +\n        \"is controlled 'spark.databricks.delta.vacuum.parallelDelete.parallelism'.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_VACUUM_PARALLEL_DELETE_PARALLELISM =\n    buildConf(\"vacuum.parallelDelete.parallelism\")\n      .doc(\"Sets the number of partitions to use for parallel deletes. If not set, defaults to \" +\n        \"spark.sql.shuffle.partitions.\")\n      .intConf\n      .checkValue(_ > 0, \"parallelDelete.parallelism must be positive\")\n      .createOptional\n\n  val ENFORCE_DELETED_FILE_AND_LOG_RETENTION_DURATION_COMPATIBILITY =\n    buildConf(\"vacuum.enforceDeletedFileAndLogRetentionDurationCompatibility\")\n      .internal()\n      .doc(\"Throws an error if log retention duration is less than deletedFileRetentionDuration\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_SCHEMA_AUTO_MIGRATE =\n    buildConf(\"schema.autoMerge.enabled\")\n      .doc(\"If true, enables schema merging on appends and on overwrites.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_MERGE_SCHEMA_EVOLUTION_FIX_NESTED_STRUCT_ALIGNMENT =\n    buildConf(\"schemaEvolution.merge.fixNestedStructAlignment\")\n      .internal()\n      .doc(\"Internal flag covering a fix for a regression in schema evolution inside nested \" +\n        \"structs in MERGE. Disabling this fix may cause MERGE operations to fail when a new \" +\n        \"field is added to a struct that is omitted in at least one MATCHED clause.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS =\n    buildConf(\"merge.preserveNullSourceStructs\")\n      .internal()\n      .doc(\n        \"\"\"Fixes the null expansion issue by preserving NULL structs in MERGE operations. When set\n          |to true, a NULL struct in the source will be preserved as NULL in the target after MERGE,\n          |rather than being incorrectly expanded to a struct with NULL fields. When set to false,\n          |NULL structs are expanded. This fix addresses null expansion caused by (1) struct type\n          |cast, and (2) expanding UPDATE SET * to leaf-level actions in schema evolution (when\n          |`spark.databricks.delta.merge.preserveNullSourceStructs.updateStar` is also enabled).\n          |Note: The fix for struct type cast also fixes the null expansion issue in UPDATE queries\n          |and streaming inserts with struct type cast.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS_UPDATE_STAR =\n    buildConf(\"merge.preserveNullSourceStructs.updateStar\")\n      .internal()\n      .doc(\"\"\"Fixes the null expansion issue in MERGE with UPDATE SET * actions in schema evolution.\n             |When set to true, and `spark.databricks.delta.merge.preserveNullSourceStructs` is also\n             |true, a NULL struct in the source will be preserved as NULL in the target after MERGE,\n             |rather than being incorrectly expanded to a struct with NULL fields. Otherwise, NULL\n             |structs are expanded.\"\"\".stripMargin)\n      .fallbackConf(DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS)\n\n  val DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS =\n    buildConf(\"insert.preserveNullSourceStructs\")\n      .internal()\n      .doc(\n        \"\"\"Fixes the null expansion issue by preserving NULL structs in INSERT operations. When set\n          |to true, a NULL struct in the source will be preserved as NULL in the target after\n          |INSERT, rather than being incorrectly expanded to a struct with NULL fields. When set to\n          |false, NULL structs are expanded. This fix addresses null expansion caused by struct\n          |type cast during INSERT operations.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_INSERT_BY_NAME_SCHEMA_EVOLUTION_ENABLED =\n    buildConf(\"insert.byName.schemaEvolution.enabled\")\n      .internal()\n      .doc(\n        \"\"\"When enabled, SQL INSERT INTO BY NAME operations allow schema evolution: extra columns in\n          |the source that are not in the target table schema are added to the target schema when\n          |schema evolution (mergeSchema) is also enabled. Disable this flag to revert to the old\n          |behavior where extra columns always cause an error, regardless of schema evolution\n          |settings.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_SCHEMA_TYPE_CHECK =\n    buildConf(\"schema.typeCheck.enabled\")\n      .doc(\n        \"\"\"Enable the data type check when updating the table schema. Disabling this flag may\n          | allow users to create unsupported Delta tables and should only be used when trying to\n          | read/write legacy tables.\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_SCHEMA_REMOVE_SPARK_INTERNAL_METADATA =\n    buildConf(\"schema.removeSparkInternalMetadata\")\n      .doc(\n        \"\"\"Whether to remove leaked Spark's internal metadata from the table schema before returning\n          |to Spark. These internal metadata might be stored unintentionally in tables created by\n          |old Spark versions\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_UPDATE_CATALOG_ENABLED =\n    buildConf(\"catalog.update.enabled\")\n      .internal()\n      .doc(\"When enabled, we will cache the schema of the Delta table and the table properties \" +\n        \"in the external catalog, e.g. the Hive MetaStore.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_UPDATE_CATALOG_THREAD_POOL_SIZE =\n    buildStaticConf(\"catalog.update.threadPoolSize\")\n      .internal()\n      .doc(\"The size of the thread pool for updating the external catalog.\")\n      .intConf\n      .checkValue(_ > 0, \"threadPoolSize must be positive\")\n      .createWithDefault(20)\n\n  val COORDINATED_COMMITS_GET_COMMITS_THREAD_POOL_SIZE =\n    buildStaticConf(\"coordinatedCommits.getCommits.threadPoolSize\")\n      .internal()\n      .doc(\"The size of the thread pool for listing files from the commit-coordinator.\")\n      .intConf\n      .checkValue(_ > 0, \"threadPoolSize must be positive\")\n      .createWithDefault(5)\n\n  val COORDINATED_COMMITS_IGNORE_MISSING_COORDINATOR_IMPLEMENTATION =\n    buildConf(\"coordinatedCommits.ignoreMissingCoordinatorImplementation\")\n      .internal()\n      .doc(\"When enabled, reads will not fail if the commit coordinator implementation \" +\n        \"is missing. Writes will still fail and reads will just rely on backfilled commits. \" +\n        \"This also means that reads can be stale.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val REMOVE_EXISTS_DEFAULT_FROM_SCHEMA =\n    buildConf(\"schema.removeExistsDefault\")\n      .internal()\n      .doc(\"When enabled, do not store the 'EXISTS_DEFAULT' metadata key when a table with a \" +\n        \"default value is created and this table does not re-use existing data files.\" +\n        \"'EXISTS_DEFAULT' holds values that are used in Spark for existing rows when a new column\" +\n        \"with a default value is added to a table. Since we do not support adding columns with a\" +\n        \"default value in Delta, this metadata key can be omitted, except in cases like when\" +\n        \"we convert a table to Delta that does actually require 'EXISTS_DEFAULT'.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val HMS_FORCE_ALTER_TABLE_DATA_SCHEMA =\n    buildConf(\"hms.schema.forceAlterTableDataSchema\")\n      .internal()\n      .doc(\n        \"\"\"\n          | This conf fixes the schema in tableCatalog object and force an alter table\n          | schema command after upload the schema. As in spark project the schema is removed\n          | because delta is not a valid serDe configuration. This is a problem known only to HMS.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  //////////////////////////////////////////////\n  // DynamoDB Commit Coordinator-specific configs\n  /////////////////////////////////////////////\n\n  val COORDINATED_COMMITS_DDB_AWS_CREDENTIALS_PROVIDER_NAME =\n    buildConf(\"coordinatedCommits.commitCoordinator.dynamodb.awsCredentialsProviderName\")\n      .internal()\n      .doc(\"The fully qualified class name of the AWS credentials provider to use for \" +\n        \"interacting with DynamoDB in the DynamoDB Commit Coordinator Client. e.g. \" +\n        \"com.amazonaws.auth.DefaultAWSCredentialsProviderChain.\")\n      .stringConf\n      .createWithDefault(\"com.amazonaws.auth.DefaultAWSCredentialsProviderChain\")\n\n  val COORDINATED_COMMITS_DDB_SKIP_PATH_CHECK =\n    buildConf(\"coordinatedCommits.commitCoordinator.dynamodb.skipPathCheckEnabled\")\n      .internal()\n      .doc(\"When enabled, the DynamoDB Commit Coordinator will not enforce that the table path \" +\n        \"of the current Delta table matches the stored in the corresponding DynamoDB table. This \" +\n        \"should only be used when the observed table path for the same physical table varies \" +\n        \"depending on how it is accessed (e.g. abfs://path1 vs abfss://path1). Leaving this \" +\n        \"enabled can be dangerous as every physical copy of a Delta table with try to write to\" +\n        \" the same DynamoDB table.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val COORDINATED_COMMITS_DDB_READ_CAPACITY_UNITS =\n    buildConf(\"coordinatedCommits.commitCoordinator.dynamodb.readCapacityUnits\")\n      .internal()\n      .doc(\"Controls the provisioned read capacity units for the DynamoDB table backing the \" +\n        \"DynamoDB Commit Coordinator. This configuration is only used when the DynamoDB table \" +\n        \"is first provisioned and cannot be used configure an existing table.\")\n      .intConf\n      .createWithDefault(5)\n\n  val COORDINATED_COMMITS_DDB_WRITE_CAPACITY_UNITS =\n    buildConf(\"coordinatedCommits.commitCoordinator.dynamodb.writeCapacityUnits\")\n      .internal()\n      .doc(\"Controls the provisioned write capacity units for the DynamoDB table backing the \" +\n        \"DynamoDB Commit Coordinator. This configuration is only used when the DynamoDB table \" +\n        \"is first provisioned and cannot be used configure an existing table.\")\n      .intConf\n      .createWithDefault(5)\n\n  //////////////////////////////////////////////\n  // DynamoDB Commit Coordinator-specific configs end\n  /////////////////////////////////////////////\n\n  val IN_COMMIT_TIMESTAMP_RETAIN_ENABLEMENT_INFO_FIX_ENABLED =\n    buildConf(\"inCommitTimestamp.retainEnablementInfoFix.enabled\")\n      .internal()\n      .doc(\"When disabled, Delta can end up dropping \" +\n        s\"inCommitTimestampEnablementVersion and inCommitTimestampEnablementTimestamp \" +\n        s\"during a REPLACE or CLONE command. This accidental removal of these \" +\n        s\"properties can result in failures on time travel queries.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD =\n    buildConf(\"catalog.update.longFieldTruncationThreshold\")\n      .internal()\n      .doc(\n        \"When syncing table schema to the catalog, Delta will truncate the whole schema \" +\n        \"if any field is longer than this threshold.\")\n      .longConf\n      .createWithDefault(4000)\n\n  val DELTA_ASSUMES_DROP_CONSTRAINT_IF_EXISTS =\n    buildConf(\"constraints.assumesDropIfExists.enabled\")\n      .doc(\"\"\"If true, DROP CONSTRAINT quietly drops nonexistent constraints even without\n             |IF EXISTS.\n           \"\"\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_ASYNC_UPDATE_STALENESS_TIME_LIMIT =\n    buildConf(\"stalenessLimit\")\n      .doc(\n        \"\"\"Setting a non-zero time limit will allow you to query the last loaded state of the Delta\n          |table without blocking on a table update. You can use this configuration to reduce the\n          |latency on queries when up-to-date results are not a requirement. Table updates will be\n          |scheduled on a separate scheduler pool in a FIFO queue, and will share cluster resources\n          |fairly with your query. If a table hasn't updated past this time limit, we will block\n          |on a synchronous state update before running the query.\n        \"\"\".stripMargin)\n      .timeConf(TimeUnit.MILLISECONDS)\n      .checkValue(_ >= 0, \"Staleness limit cannot be negative\")\n      .createWithDefault(0L) // Don't let tables go stale\n\n  val DELTA_ALTER_LOCATION_BYPASS_SCHEMA_CHECK =\n    buildConf(\"alterLocation.bypassSchemaCheck\")\n      .doc(\"If true, Alter Table Set Location on Delta will go through even if the Delta table \" +\n        \"in the new location has a different schema from the original Delta table.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DUMMY_FILE_MANAGER_NUM_OF_FILES =\n    buildConf(\"dummyFileManager.numOfFiles\")\n      .internal()\n      .doc(\"How many dummy files to write in DummyFileManager\")\n      .intConf\n      .checkValue(_ >= 0, \"numOfFiles can not be negative.\")\n      .createWithDefault(3)\n\n  val DUMMY_FILE_MANAGER_PREFIX =\n    buildConf(\"dummyFileManager.prefix\")\n      .internal()\n      .doc(\"The file prefix to use in DummyFileManager\")\n      .stringConf\n      .createWithDefault(\".s3-optimization-\")\n\n  val DELTA_MERGE_ANALYSIS_BATCH_RESOLUTION =\n    buildConf(\"merge.analysis.batchActionResolution.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          | Whether to batch the analysis of all DeltaMergeActions within a clause\n          | during merge's analysis resolution.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_INSERT_ONLY_ENABLED =\n    buildConf(\"merge.optimizeInsertOnlyMerge.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |If enabled, merge without any matched clause (i.e., insert-only merge) will be optimized\n          |by avoiding rewriting old files and just inserting new files.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_REPARTITION_BEFORE_WRITE =\n    buildConf(\"merge.repartitionBeforeWrite.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, merge will repartition the output by the table's partition columns before\n          |writing the files.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_MATCHED_ONLY_ENABLED =\n    buildConf(\"merge.optimizeMatchedOnlyMerge.enabled\")\n      .internal()\n      .doc(\n        \"\"\"If enabled, merge without 'when not matched' clause will be optimized to use a\n          |right outer join instead of a full outer join.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_SKIP_OSS_RESOLUTION_WITH_STAR =\n    buildConf(\"merge.skipOssResolutionWithStar\")\n      .internal()\n      .doc(\n        \"\"\"\n          |If enabled, then any MERGE operation having UPDATE * / INSERT * will skip Apache\n          |Spark's resolution logic and use Delta's specific resolution logic. This is to avoid\n          |bug with star and temp views. See SC-72276 for details.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_FAIL_IF_SOURCE_CHANGED =\n    buildConf(\"merge.failIfSourceChanged\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, MERGE will fail if it detects that the source dataframe was changed.\n          |This can be triggered as a result of modified input data or the use of nondeterministic\n          |query plans. The detection is best-effort.\n      \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  final object MergeMaterializeSource {\n    // See value explanations in the doc below.\n    final val NONE = \"none\"\n    final val ALL = \"all\"\n    final val AUTO = \"auto\"\n\n    final val list = Set(NONE, ALL, AUTO)\n  }\n\n  val MERGE_MATERIALIZE_SOURCE =\n    buildConf(\"merge.materializeSource\")\n      .internal()\n      .doc(\"When to materialize the source plan during MERGE execution. \" +\n        \"The value 'none' means source will never be materialized. \" +\n        \"The value 'all' means source will always be materialized. \" +\n        \"The value 'auto' means sources will not be materialized when they are certain to be \" +\n        \"deterministic.\"\n      )\n      .stringConf\n      .transform(_.toLowerCase(Locale.ROOT))\n      .checkValues(MergeMaterializeSource.list)\n      .createWithDefault(MergeMaterializeSource.AUTO)\n\n  val MERGE_FORCE_SOURCE_MATERIALIZATION_WITH_UNREADABLE_FILES =\n    buildConf(\"merge.forceSourceMaterializationWithUnreadableFilesConfig\")\n      .internal()\n      .doc(\n        s\"\"\"\n           |When set to true, merge command will force source materialization if Spark configs\n           |${SQLConf.IGNORE_CORRUPT_FILES.key}, ${SQLConf.IGNORE_MISSING_FILES.key} or\n           |file source read options ${FileSourceOptions.IGNORE_CORRUPT_FILES}\n           |${FileSourceOptions.IGNORE_MISSING_FILES} are enabled on the source.\n           |This is done so to prevent irrecoverable data loss or unexpected results.\n           |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_MATERIALIZE_CACHED_SOURCE =\n    buildConf(\"merge.materializeCachedSource\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, materialize the source in MERGE if it is cached (e.g. via df.cache()). This\n          |prevents incorrect results due to query caching not pinning the version of cached Delta\n          |tables.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_FAIL_SOURCE_CACHED_AFTER_MATERIALIZATION =\n    buildConf(\"merge.failSourceCachedAfterMaterialization\")\n      .internal()\n      .doc(\n        \"\"\"\n          |Enables a check that fails the MERGE operation if the source was cached (using\n          |df.cache()) after the source materialization phase. Query caching doesn't pin the version\n          |of Delta tables and we should materialize cached source plans. In rare cases, the source\n          |might get cached after the decision to materialize, which could lead to incorrect results\n          |if we let the operation succeed.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL =\n    buildConf(\"merge.materializeSource.rddStorageLevel\")\n      .internal()\n      .doc(\"What StorageLevel to use to persist the source RDD. Note: will always use disk.\")\n      .stringConf\n      .transform(_.toUpperCase(Locale.ROOT))\n      .checkValue( v =>\n        try {\n          StorageLevel.fromString(v).isInstanceOf[StorageLevel]\n        } catch {\n          case _: IllegalArgumentException => true\n        },\n        \"\"\"\"spark.databricks.delta.merge.materializeSource.rddStorageLevel\" \"\"\" +\n          \"must be a valid StorageLevel\")\n      .createWithDefault(\"DISK_ONLY\")\n\n  val MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL_FIRST_RETRY =\n    buildConf(\"merge.materializeSource.rddStorageLevelFirstRetry\")\n      .internal()\n      .doc(\"What StorageLevel to use to persist the source RDD when MERGE is retried the first\" +\n        \"time. Note: will always use disk.\")\n      .stringConf\n      .transform(_.toUpperCase(Locale.ROOT))\n      .checkValue( v =>\n        try {\n          StorageLevel.fromString(v).isInstanceOf[StorageLevel]\n        } catch {\n          case _: IllegalArgumentException => true\n        },\n        \"\"\"\"spark.databricks.delta.merge.materializeSource.rddStorageLevelFirstRetry\" \"\"\" +\n          \"must be a valid StorageLevel\")\n      .createWithDefault(\"DISK_ONLY_2\")\n\n  val MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL_RETRY =\n    buildConf(\"merge.materializeSource.rddStorageLevelRetry\")\n      .internal()\n      .doc(\"What StorageLevel to use to persist the source RDD when MERGE is retried after the \" +\n        \"first retry. The storage level to use for the first retry can be configured using\" +\n        \"\"\"\"spark.databricks.delta.merge.materializeSource.rddStorageLevelFirstRetry\" \"\"\" +\n        \"Note: will always use disk.\")\n      .stringConf\n      .transform(_.toUpperCase(Locale.ROOT))\n      .checkValue( v =>\n        try {\n          StorageLevel.fromString(v).isInstanceOf[StorageLevel]\n        } catch {\n          case _: IllegalArgumentException => true\n        },\n        \"\"\"\"spark.databricks.delta.merge.materializeSource.rddStorageLevelRetry\" \"\"\" +\n          \"must be a valid StorageLevel\")\n      .createWithDefault(\"DISK_ONLY_3\")\n\n  val MERGE_MATERIALIZE_SOURCE_MAX_ATTEMPTS =\n    buildStaticConf(\"merge.materializeSource.maxAttempts\")\n      .doc(\"How many times to try MERGE in case of lost RDD materialized source data\")\n      .intConf\n      .createWithDefault(4)\n\n  val DELTA_LAST_COMMIT_VERSION_IN_SESSION =\n    buildConf(\"lastCommitVersionInSession\")\n      .doc(\"The version of the last commit made in the SparkSession for any table.\")\n      .longConf\n      .checkValue(_ >= 0, \"the version must be >= 0\")\n      .createOptional\n\n  val ALLOW_UNENFORCED_NOT_NULL_CONSTRAINTS =\n    buildConf(\"constraints.allowUnenforcedNotNull.enabled\")\n      .internal()\n      .doc(\"If enabled, NOT NULL constraints within array and map types will be permitted in \" +\n        \"Delta table creation, even though Delta can't enforce them.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val CHECKPOINT_SCHEMA_WRITE_THRESHOLD_LENGTH =\n    buildConf(\"checkpointSchema.writeThresholdLength\")\n      .internal()\n      .doc(\"Checkpoint schema larger than this threshold won't be written to the last checkpoint\" +\n        \" file\")\n      .intConf\n      .createWithDefault(20000)\n\n  val LAST_CHECKPOINT_CHECKSUM_ENABLED =\n    buildConf(\"lastCheckpoint.checksum.enabled\")\n      .internal()\n      .doc(\"Controls whether to write the checksum while writing the LAST_CHECKPOINT file and\" +\n        \" whether to validate it while reading the LAST_CHECKPOINT file\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val SUPPRESS_OPTIONAL_LAST_CHECKPOINT_FIELDS =\n      buildConf(\"lastCheckpoint.suppressOptionalFields\")\n      .internal()\n      .doc(\"If set, the LAST_CHECKPOINT file will contain only version, size, and parts fields. \" +\n          \"For compatibility with broken third-party connectors that choke on unrecognized fields.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CHECKPOINT_PART_SIZE =\n    buildConf(\"checkpoint.partSize\")\n        .internal()\n        .doc(\"The limit at which we will start parallelizing the checkpoint. We will attempt to \" +\n                 \"write a maximum of this many actions per checkpoint file.\")\n        .longConf\n        .checkValue(_ > 0, \"partSize has to be positive\")\n        .createOptional\n\n  /////////////////////////////////\n  // File Materialization Tracker\n  /////////////////////////////////\n\n  val DELTA_COMMAND_FILE_MATERIALIZATION_TRACKING_ENABLED =\n    buildConf(\"command.fileMaterializationLimit.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, tracks the file metadata materialized on the driver and restricts the\n          |number of files materialized on the driver to be within the global file\n          |materialization limit.\n       \"\"\".stripMargin\n      )\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_COMMAND_FILE_MATERIALIZATION_LIMIT =\n    buildStaticConf(\"command.fileMaterializationLimit.softMax\")\n      .internal()\n      .doc(\n        s\"\"\"\n           |The soft limit for the total number of file metadata that can be materialized at once on\n           |the driver. This config will take effect only when\n           |${DELTA_COMMAND_FILE_MATERIALIZATION_TRACKING_ENABLED.key} is enabled.\n        \"\"\".stripMargin\n      )\n      .intConf\n      .checkValue(_ >= 0, \"'command.fileMaterializationLimit.softMax' must be positive\")\n      .createWithDefault(10000000)\n\n  ////////////////////////\n  // BACKFILL\n  ////////////////////////\n\n  val DELTA_ROW_TRACKING_BACKFILL_ENABLED =\n    buildConf(\"rowTracking.backfill.enabled\")\n      .internal()\n      .doc(\"Whether Row Tracking backfill can be performed.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_ROW_TRACKING_BACKFILL_MAX_NUM_FILES_PER_COMMIT =\n    buildConf(\"rowTracking.backfill.maxNumFiles\")\n      .internal()\n      .doc(\"The maximum number of files to include in a single commit when running \" +\n        \"RowTrackingBackfillCommand. The default maximum aims to keep every \" +\n        \"delta log entry below 100mb.\")\n      .intConf\n      .checkValue(_ > 0, \"'backfill.maxNumFiles' must be positive.\")\n      .createWithDefault(22000)\n\n  val DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT =\n    buildConf(\"backfill.maxNumFiles\")\n      .internal()\n      .doc(\"The maximum number of files to include in a single commit when running \" +\n        \"BackfillCommand. The default maximum aims to keep every \" +\n        \"delta log entry below 100mb.\")\n      .fallbackConf(DELTA_ROW_TRACKING_BACKFILL_MAX_NUM_FILES_PER_COMMIT)\n\n  val DELTA_BACKFILL_MAX_NUM_FILES_FACTOR =\n    buildConf(\"backfill.maxNumFilesFactor\")\n      .internal()\n      .doc(\n        \"\"\"The factor used to compute the maximum number of files to backfill.\n          |The maximum number of files to compute in backfill is computed as\n          |number of files in table * factor.\"\"\".stripMargin)\n      .doubleConf\n      .checkValue(_ > 0, \"'backfill.maxNumFilesFactor' must be greater than zero.\")\n      .createWithDefault(3)\n\n  val DELTA_ROW_TRACKING_IGNORE_SUSPENSION =\n    buildConf(\"rowTracking.ignoreSuspension\")\n      .internal()\n      .doc(\n        \"\"\"Controls whether to ignore `delta.rowTrackingSuspended` property.\n          |This is a testing only config.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  ////////////////////////////////////\n  // Checkpoint V2 Specific Configs\n  ////////////////////////////////////\n\n  val CHECKPOINT_V2_DRIVER_THREADPOOL_PARALLELISM =\n    buildStaticConf(\"checkpointV2.threadpool.size\")\n      .doc(\"The size of the threadpool for fetching CheckpointMetadata and SidecarFiles from a\" +\n        \" checkpoint.\")\n      .internal()\n      .intConf\n      .createWithDefault(32)\n\n  val CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT =\n    buildConf(\"checkpointV2.topLevelFileFormat\")\n      .internal()\n      .doc(\n        \"\"\"\n          |The file format to use for the top level checkpoint file in V2 Checkpoints.\n          | This can be set to either json or parquet. The appropriate format will be\n          | picked automatically if this config is not specified.\n          |\"\"\".stripMargin)\n      .stringConf\n      .checkValues(Set(\"json\", \"parquet\"))\n      .createOptional\n\n  // This is temporary conf to make sure v2 checkpoints are not used by anyone other than devs as\n  // the feature is not fully ready.\n  val EXPOSE_CHECKPOINT_V2_TABLE_FEATURE_FOR_TESTING =\n    buildConf(\"checkpointV2.exposeTableFeatureForTesting\")\n      .internal()\n      .doc(\n        \"\"\"\n          |This conf controls whether v2 checkpoints table feature is exposed or not. Note that\n          | v2 checkpoints are in development and this should config should be used only for\n          | testing/benchmarking.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val LAST_CHECKPOINT_NON_FILE_ACTIONS_THRESHOLD =\n    buildConf(\"lastCheckpoint.nonFileActions.threshold\")\n      .internal()\n      .doc(\"\"\"\n          |Threshold for total number of non file-actions to store in the last_checkpoint\n          | corresponding to the checkpoint v2.\n          |\"\"\".stripMargin)\n      .intConf\n      .createWithDefault(30)\n\n  val STATS_AS_STRUCT_IN_CHECKPOINT_FORCE_DISABLED =\n    buildConf(\"statsAsStructInCheckpoint.forcedDisabled\")\n      .internal()\n      .doc(\"\"\"\n          |Force disables storing statistics as struct in the checkpoint.\n          |Note that should only be used as a kill switch.\n          |This functionality should normally be controlled using the delta config\n          |'checkpoint.writeStatsAsStruct'.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createOptional\n\n  val LAST_CHECKPOINT_SIDECARS_THRESHOLD =\n    buildConf(\"lastCheckpoint.sidecars.threshold\")\n      .internal()\n      .doc(\"\"\"\n          |Threshold for total number of sidecar files to store in the last_checkpoint\n          | corresponding to the checkpoint v2.\n          |\"\"\".stripMargin)\n      .intConf\n      .createWithDefault(30)\n\n  val USE_CHECKPOINT_SCHEMA_FROM_CHECKPOINT_METADATA =\n    buildConf(\"checkpointSchema.useFromCheckpointMetadata\")\n      .internal()\n      .doc(\"If enabled, use checkpoint schema from checkpoint metadata file instead of reading it\" +\n        \" from the checkpoint file\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_WRITE_CHECKSUM_ENABLED =\n    buildConf(\"writeChecksumFile.enabled\")\n      .doc(\"Whether the checksum file can be written.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CHECKSUM_HISTOGRAM_FIELD_FOLLOWS_PROTOCOL =\n    buildConf(\"writeChecksumFile.histogramFollowsProtocol\")\n      .internal()\n      .doc(\"\"\"When true, writes the file size histogram to CRC files using the Delta spec field\n             |name \"fileSizeHistogram\". When false, uses the legacy Delta-Spark field name\n             |\"histogramOpt\".\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  private val FORCED_CHECKSUM_VALIDATION_INTERVAL_DEFAULT = 400\n  val FORCED_CHECKSUM_VALIDATION_INTERVAL =\n    buildConf(\"versionChecksum.forcedValidationInterval\")\n      .internal()\n      .doc(\"The number of commits since the last checkpoint at which we \" +\n        \"should force validation of the version checksum. This is done before \" +\n        \"a commit to block further writes in case of checksum mismatch.\" +\n        \"Set to -1 to disable, set to 0 to validate on every commit. \" +\n        \"The validation will be skipped if the checkpoint was created \" +\n        \"within the time gap specified by versionChecksum.forcedValidationMinTimeIntevalMinutes.\")\n      .intConf\n      .createWithDefault(FORCED_CHECKSUM_VALIDATION_INTERVAL_DEFAULT)\n\n  val FORCED_CHECKSUM_VALIDATION_MIN_TIME_INTERVAL_MINUTES =\n    buildConf(\"versionChecksum.forcedValidationMinTimeIntevalMinutes\")\n      .internal()\n      .doc(\"The minimum time gap in minutes between the checkpoint creation time and \" +\n        \"current time for forced checksum validation. If the checkpoint was created \" +\n        \"within this time gap, forced validation is skipped even if the number of \" +\n        \"commits since the checkpoint exceeds the forcedValidationInterval threshold. \" +\n        \"For fast moving tables, the checkpoint can lag much behind \" +\n        \"versionChecksum.forcedValidationInterval. This helps us avoid slowing \" +\n        \"them down. Set to 0 to disable this optimization.\")\n      .intConf\n      .checkValue(_ >= 0,\n        \"'versionChecksum.forcedValidationMinTimeIntevalMinutes' must be non-negative.\")\n      .createWithDefault(12*60) // 12 hours\n\n  val INCREMENTAL_COMMIT_ENABLED =\n    buildConf(\"incremental.commit.enabled\")\n      .internal()\n      .doc(\"If true, Delta will incrementally compute the content of the commit checksum \" +\n        \"file, which avoids the full state reconstruction that would otherwise be required.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CHECKSUM_MISMATCH_IS_FATAL =\n    buildConf(\"checksum.mismatch.fatal\")\n      .internal()\n      .doc(\n        \"\"\"If true, throws a fatal error when the recreated Delta State doesn't\n          |match committed checksum file.\n        \"\"\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val INCREMENTAL_COMMIT_VERIFY =\n    buildConf(\"incremental.commit.verify\")\n      .internal()\n      .doc(\"If true, Delta commit will validate the commit checksum file content before and \" +\n        \"after each incremental commit. Note that this requires two full state reconstructions.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  // This config is effective only in unit tests.\n  val INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS =\n    buildConf(\"incremental.commit.forceVerifyInTests\")\n      .internal()\n      .doc(\"If true, Delta commit will validate the commit checksum file content before and \" +\n        \"after each incremental commit as part of Unit Tests. Note that this overrides any \" +\n        s\"behaviour from ${INCREMENTAL_COMMIT_VERIFY.key} config.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_WRITE_SET_TRANSACTIONS_IN_CRC =\n    buildConf(\"setTransactionsInCrc.writeOnCommit\")\n      .internal()\n      .doc(\"When enabled, each commit will incrementally compute and cache all SetTransaction\" +\n        \" actions in the .crc file. Note that this only happens when incremental commits\" +\n        s\" are enabled (${INCREMENTAL_COMMIT_ENABLED.key})\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_MAX_SET_TRANSACTIONS_IN_CRC =\n    buildConf(\"setTransactionsInCrc.maxAllowed\")\n      .internal()\n      .doc(\"Threshold of the number of SetTransaction actions below which this optimization\" +\n        \" should be enabled\")\n      .longConf\n      .createWithDefault(100)\n\n  val DELTA_MAX_DOMAIN_METADATAS_IN_CRC =\n    buildConf(\"domainMetadatasInCrc.maxAllowed\")\n      .internal()\n      .doc(\"Threshold of the number of DomainMetadata actions below which this optimization\" +\n        \" should be enabled\")\n      .longConf\n      .createWithDefault(10)\n\n  val DELTA_ALL_FILES_IN_CRC_THRESHOLD_FILES =\n    buildConf(\"allFilesInCrc.thresholdNumFiles\")\n      .internal()\n      .doc(\"Threshold of the number of AddFiles below which AddFiles will be added to CRC.\")\n      .intConf\n      .createWithDefault(50)\n\n  val DELTA_ALL_FILES_IN_CRC_ENABLED =\n    buildConf(\"allFilesInCrc.enabled\")\n      .internal()\n      .doc(\"When enabled, [[Snapshot.allFiles]] will be stored in the .crc file when the \" +\n        \"length is less than the threshold specified by \" +\n        s\"${DELTA_ALL_FILES_IN_CRC_THRESHOLD_FILES.key}. \" +\n        \"Note that this config only takes effect when incremental commits are enabled \" +\n        s\"(${INCREMENTAL_COMMIT_ENABLED.key}).\"\n      )\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED =\n    buildConf(\"allFilesInCrc.verificationMode.enabled\")\n      .internal()\n      .doc(s\"This will be effective only if ${DELTA_ALL_FILES_IN_CRC_ENABLED.key} is set. When\" +\n        \" enabled, We will have additional verification of the incrementally computed state by\" +\n        \" doing an actual state reconstruction on every commit.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED =\n    buildConf(\"allFilesInCrc.verificationMode.forceOnNonUTC.enabled\")\n      .internal()\n      .doc(s\"This will be effective only if \" +\n        s\"${DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key} is not set. When enabled, we \" +\n        s\"will force verification of the incrementally computed state by doing an actual state \" +\n        s\"reconstruction on every commit for tables that are not using UTC timezone.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_ALL_FILES_IN_CRC_THRESHOLD_INDEXED_COLS =\n    buildConf(\"allFilesInCrc.thresholdIndexedCols\")\n      .internal()\n      .doc(\"If the delta table is configured to collect stats on more columns than this\" +\n        \" threshold, then disable storage of `[[Snapshot.allFiles]]` in the .crc file.\")\n      .intConf\n      .createOptional\n\n  val USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED =\n    buildConf(\"readProtocolAndMetadataFromChecksum.enabled\")\n      .internal()\n      .doc(\"If enabled, delta log snapshot will read the protocol, metadata, and ICT \" +\n        \"(if applicable) from the checksum file and use those to avoid a spark job over the \" +\n        \"checkpoint for the two rows of protocol and metadata\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CHECKSUM_DV_METRICS_ENABLED =\n    buildConf(\"checksumDVMetrics.enabled\")\n      .internal()\n      .doc(s\"\"\"When enabled, each delta transaction includes vector metrics in the checksum.\n              |Only applies to tables that use Deletion Vectors.\"\"\"\n        .stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_DELETED_RECORD_COUNTS_HISTOGRAM_ENABLED =\n    buildConf(\"checksumDeletedRecordCountsHistogramMetrics.enabled\")\n      .internal()\n      .doc(s\"\"\"When enabled, each delta transaction includes in the checksum the deleted\n              |record count distribution histogram for all the files. To enable this feature\n              |${DELTA_CHECKSUM_DV_METRICS_ENABLED.key} needs to be enabled as well. Only\n              |applies to tables that use Deletion Vectors.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CHECKPOINT_THROW_EXCEPTION_WHEN_FAILED =\n      buildConf(\"checkpoint.exceptionThrowing.enabled\")\n        .internal()\n      .doc(\"Throw an error if checkpoint is failed. This flag is intentionally used for \" +\n          \"testing purpose to catch the checkpoint issues proactively. In production, we \" +\n          \"should not set this flag to be true because successful commit should return \" +\n          \"success to client regardless of the checkpoint result without throwing.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME =\n    buildConf(\"resolveMergeUpdateStructsByName.enabled\")\n      .internal()\n      .doc(\"Whether to resolve structs by name in UPDATE operations of UPDATE and MERGE INTO \" +\n        \"commands. If disabled, Delta will revert to the legacy behavior of resolving by position.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_TIME_TRAVEL_STRICT_TIMESTAMP_PARSING =\n    buildConf(\"timeTravel.parsing.strict\")\n      .internal()\n      .doc(\"Whether to require time travel timestamps to parse to a valid timestamp. If \" +\n        \"disabled, Delta will revert to the legacy behavior of treating invalid timestamps as \" +\n        \"equivalent to unix time 0 (1970-01-01 00:00:00).\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STRICT_CHECK_DELTA_TABLE =\n    buildConf(\"isDeltaTable.strictCheck\")\n      .internal()\n      .doc(\"\"\"\n           | When enabled, io.delta.tables.DeltaTable.isDeltaTable\n           | should return false when the _delta_log directory doesn't\n           | contain any transaction logs.\n           |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  /**\n   * Internal config to bypass the check that ensures a table doesn't contain any unsupported type\n   * change when reading it. Meant as a mitigation in case the check incorrectly flags valid cases.\n   */\n  val DELTA_TYPE_WIDENING_BYPASS_UNSUPPORTED_TYPE_CHANGE_CHECK =\n    buildConf(\"typeWidening.bypassUnsupportedTypeChangeCheck\")\n      .internal()\n      .doc(\"\"\"\n           | Disables check that ensures a table doesn't contain any unsupported type change when\n           | reading it.\n           |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_ALLOW_TYPE_WIDENING_STREAMING_SOURCE =\n    buildConf(\"typeWidening.allowTypeChangeStreamingDeltaSource\")\n      .doc(\"Accept incoming widening type changes when streaming from a Delta source.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  object AllowAutomaticWideningMode extends Enumeration {\n    val NEVER, SAME_FAMILY_TYPE, ALWAYS = Value\n\n    def fromConf(conf: SQLConf): Value =\n      withName(conf.getConf(DELTA_ALLOW_AUTOMATIC_WIDENING))\n\n    def default: Value =\n      withName(DELTA_ALLOW_AUTOMATIC_WIDENING.defaultValueString)\n  }\n\n  val DELTA_ALLOW_AUTOMATIC_WIDENING =\n    buildConf(\"typeWidening.allowAutomaticWidening\")\n      .doc(\"Controls the scope of enabled widening conversions in automatic schema widening \" +\n        \"during schema evolution. This flag is guarded by the flag 'delta.enableTypeWidening'\" +\n        \"All supported widenings are enabled with 'always' selected, which allows some \" +\n        \"conversions between integer types and floating numbers. The value 'same_family_type' \" +\n        \"was the historical behavior. 'never' allows no widenings.\")\n      .internal()\n      .stringConf\n      .transform(_.toUpperCase(Locale.ROOT))\n      .checkValues(AllowAutomaticWideningMode.values.map(_.toString))\n      .createWithDefault(AllowAutomaticWideningMode.ALWAYS.toString)\n\n  val DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING =\n    buildConf(\"typeWidening.enableStreamingSchemaTracking\")\n      .doc(\"Whether to enable schema tracking when streaming from a Delta source that had a \" +\n        \"widening type change applied. This allows blocking the stream on restart until the user \" +\n        \"acknowledges the type change. When disabled, we will not initialize a schema tracking \" +\n        \"log when first detecting a type change and will automatically accept the type change \" +\n        \"instead.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK =\n    buildConf(\"typeWidening.bypassStreamingTypeChangeCheck\")\n      .doc(\"Controls the check performed when a type change is detected when streaming from a \" +\n        \"Delta source. This check fails the streaming query in case a type change may impact the \" +\n        \"semantics of the query and requests user intervention.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  /**\n   * Internal config to bypass check that prevents applying type changes that are not supported by\n   * Iceberg when Uniform is enabled with Iceberg compatibility.\n   */\n  val DELTA_TYPE_WIDENING_ALLOW_UNSUPPORTED_ICEBERG_TYPE_CHANGES =\n    buildConf(\"typeWidening.allowUnsupportedIcebergTypeChanges\")\n      .internal()\n      .doc(\n        \"\"\"\n          |By default, type changes that aren't supported by Iceberg are rejected when Uniform is\n          |enabled with Iceberg compatibility. This config allows bypassing this restriction, but\n          |reading the affected column with Iceberg clients will likely fail or behave erratically.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n    val DELTA_TYPE_WIDENING_REMOVE_SCHEMA_METADATA =\n    buildConf(\"typeWidening.removeSchemaMetadata\")\n      .doc(\"When true, type widening metadata is removed from schemas that are surfaced outside \" +\n        \"of Delta or used for schema comparisons\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_TYPE_WIDENING_ALLOW_INTEGRAL_DECIMAL_COERCION =\n    buildConf(\"typeWidening.allowIntegralDecimalCoercion\")\n      .doc(\"When true, the type widening mode `AllTypeWideningToCommonWiderType` \" +\n        \"should allow converting integral types to DecimalType and use decimal \" +\n        \"coercion to find a common wider type with another DecimalType\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_IS_DELTA_TABLE_THROW_ON_ERROR =\n    buildConf(\"isDeltaTable.throwOnError\")\n      .internal()\n      .doc(\"\"\"\n        | If checking the path provided to isDeltaTable (or findDeltaTableRoot) throws an exception,\n        | then propagate this exception unless a _delta_log directory is found in an\n        | accessible parent.\n        | When disabled, such any exception leads to a result indicating that this is not a\n        | Delta table.\n        |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_LEGACY_STORE_WRITER_OPTIONS_AS_PROPS =\n    buildConf(\"legacy.storeOptionsAsProperties\")\n      .internal()\n      .doc(\"\"\"\n             |Delta was unintentionally storing options provided by the DataFrameWriter in the\n             |saveAsTable method as table properties in the transaction log. This was unsupported\n             |behavior (it was a bug), and it has security implications (accidental storage of\n             |credentials). This flag prevents the storage of arbitrary options as table properties.\n             |Set this flag to true to continue setting non-delta prefixed table properties through\n             |table options.\n             |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_VACUUM_RELATIVIZE_IGNORE_ERROR =\n    buildConf(\"vacuum.relativize.ignoreError\")\n      .internal()\n      .doc(\"\"\"\n             |When enabled, the error when trying to relativize an absolute path when\n             |vacuuming a delta table will be ignored. This usually happens when a table is\n             |shallow cloned across FileSystems, such as across buckets or across cloud storage\n             |systems. We do not recommend enabling this configuration in production or using it\n             |with production datasets.\n             |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n  val DELTA_LEGACY_ALLOW_AMBIGUOUS_PATHS =\n    buildConf(\"legacy.allowAmbiguousPathsInCreateTable\")\n      .internal()\n      .doc(\"\"\"\n             |Delta was unintentionally allowing CREATE TABLE queries with both 'delta.`path`'\n             |and 'LOCATION path' clauses. In the new version, we will raise an error\n             |for this case. This flag is added to allow users to skip the check. When it's set to\n             |true and there are two paths in CREATE TABLE, the LOCATION path clause will be\n             |ignored like what the old version does.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_WORK_AROUND_COLONS_IN_HADOOP_PATHS =\n    buildConf(\"workAroundColonsInHadoopPaths.enabled\")\n      .internal()\n      .doc(\"\"\"\n             |When enabled, Delta will work around to allow colons in file paths. Normally Hadoop\n             |does not support colons in file paths due to ambiguity, but some file systems like\n             |S3 allow them.\n             |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val REPLACEWHERE_DATACOLUMNS_ENABLED =\n    buildConf(\"replaceWhere.dataColumns.enabled\")\n      .doc(\n        \"\"\"\n          |When enabled, replaceWhere on arbitrary expression and arbitrary columns is enabled.\n          |If disabled, it falls back to the old behavior\n          |to replace on partition columns only.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val REPLACEWHERE_METRICS_ENABLED =\n    buildConf(\"replaceWhere.dataColumns.metrics.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, replaceWhere operations metrics on arbitrary expression and\n          |arbitrary columns is enabled. This will not report row level metrics for partitioned\n          |tables and tables with no stats.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n  val REPLACEWHERE_CONSTRAINT_CHECK_ENABLED =\n    buildConf(\"replaceWhere.constraintCheck.enabled\")\n      .doc(\n        \"\"\"\n          |When enabled, replaceWhere on arbitrary expression and arbitrary columns will\n          |enforce the constraint check to replace the target table only when all the\n          |rows in the source dataframe match that constraint.\n          |If disabled, it will skip the constraint check and replace with all the rows\n          |from the new dataframe.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val REPLACEWHERE_DATACOLUMNS_WITH_CDF_ENABLED =\n    buildConf(\"replaceWhere.dataColumnsWithCDF.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, replaceWhere on arbitrary expression and arbitrary columns will produce\n          |results for CDF. If disabled, it will fall back to the old behavior.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val OVERWRITE_REMOVE_METRICS_ENABLED =\n    buildConf(\"insertOverwrite.removeMetrics.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, insert operations in overwrite mode will add metrics describing\n          |removed data to table's history\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val LOG_SIZE_IN_MEMORY_THRESHOLD =\n    buildConf(\"streaming.logSizeInMemoryThreshold\")\n      .internal()\n      .doc(\n        \"\"\"\n          |The threshold of transaction log file size to read into the memory. When a file is larger\n          |than this, we will read the log file in multiple passes rather than loading it into\n          |the memory entirely.\"\"\".stripMargin)\n      .longConf\n      .createWithDefault(128L * 1024 * 1024) // 128MB\n\n  val STREAMING_OFFSET_VALIDATION =\n    buildConf(\"streaming.offsetValidation.enabled\")\n      .internal()\n      .doc(\"Whether to validate whether delta streaming source generates a smaller offset and \" +\n        \"moves backward.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val LOAD_FILE_SYSTEM_CONFIGS_FROM_DATAFRAME_OPTIONS =\n    buildConf(\"loadFileSystemConfigsFromDataFrameOptions\")\n      .internal()\n      .doc(\n        \"\"\"Whether to load file systems configs provided in DataFrameReader/Writer options when\n          |calling `DataFrameReader.load/DataFrameWriter.save` using a Delta table path.\n          |`DataFrameReader.table/DataFrameWriter.saveAsTable` doesn't support this.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val CONVERT_EMPTY_TO_NULL_FOR_STRING_PARTITION_COL =\n    buildConf(\"convertEmptyToNullForStringPartitionCol\")\n      .internal()\n      .doc(\n        \"\"\"\n          |If true, always convert empty string to null for string partition columns before\n          |constraint checks.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ENABLED =\n    buildConf(\"skipping.partitionLikeFilters.enabled\")\n      .doc(\n        \"\"\"\n           |If true, during data skipping, apply arbitrary data filters to \"partition-like\"\n           |files (files with the same min-max values and no nulls on all referenced attributes).\n           |\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_THRESHOLD =\n    buildConf(\"skipping.partitionLikeDataSkippingFilesThreshold\")\n      .internal()\n      .doc(\"Partition-like data skipping on files with the same min-max values will only be\" +\n        \"attempted when a Delta table has a number of files larger than this threshold.\")\n      .intConf\n      .createWithDefault(100)\n\n  val DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_CLUSTERING_COLUMNS_ONLY =\n    buildConf(\"skipping.partitionLikeDataSkipping.limitToClusteringColumns\")\n      .internal()\n      .doc(\"Limits partition-like data skipping to filters referencing only clustering columns\" +\n        \"In general, clustering columns will be most likely to produce files with the same\" +\n        \"min-max values, though this restriction might exclude filters on columns highly \" +\n        \"correlated with the clustering columns.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ADDITIONAL_SUPPORTED_EXPRESSIONS =\n    buildConf(\"skipping.partitionLikeDataSkipping.additionalSupportedExpressions\")\n      .internal()\n      .doc(\"Comma-separated list of the canonical class names of additional expressions for which\" +\n        \"partition-like data skipping can be safely applied.\")\n      .stringConf\n      .createOptional\n\n  val DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_ENABLED =\n    buildConf(\"skipping.enhancedIsNullPushdownExprs.enabled\")\n      .doc(\"If true, support pushing down IsNull on additional null-intolerant expressions for \" +\n        \"data skipping.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_MAX_DEPTH =\n    buildConf(\"skipping.enhancedIsNullPushdownExprs.maxDepth\")\n      .doc(\"The maximum number of times a complex expression like Or or And would have an IsNull \" +\n        \"pushed down in it for data skipping.\")\n      .internal()\n      .intConf\n      .createWithDefault(8)\n\n  /**\n   * The below confs have a special prefix `spark.databricks.io` because this is the conf value\n   * already used by Databricks' data skipping implementation. There's no benefit to making OSS\n   * users, some of whom are Databricks customers, have to keep track of two different conf\n   * values for the same data skipping parameter.\n   */\n  val DATA_SKIPPING_STRING_PREFIX_LENGTH =\n    SQLConf.buildConf(\"spark.databricks.io.skipping.stringPrefixLength\")\n      .internal()\n      .doc(\"For string columns, how long prefix to store in the data skipping index.\")\n      .intConf\n      .createWithDefault(32)\n\n  val MDC_NUM_RANGE_IDS =\n    SQLConf.buildConf(\"spark.databricks.io.skipping.mdc.rangeId.max\")\n      .internal()\n      .doc(\"This controls the domain of rangeId values to be interleaved. The bigger, the better \" +\n         \"granularity, but at the expense of performance (more data gets sampled).\")\n      .intConf\n      .checkValue(_ > 1, \"'spark.databricks.io.skipping.mdc.rangeId.max' must be greater than 1\")\n      .createWithDefault(1000)\n\n  val MDC_ADD_NOISE =\n    SQLConf.buildConf(\"spark.databricks.io.skipping.mdc.addNoise\")\n      .internal()\n      .doc(\"Whether or not a random byte should be added as a suffix to the interleaved bits \" +\n         \"when computing the Z-order values for MDC. This can help deal with skew, but may \" +\n         \"have a negative impact on overall min/max skipping effectiveness.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val MDC_SORT_WITHIN_FILES =\n    SQLConf.buildConf(\"spark.databricks.io.skipping.mdc.sortWithinFiles\")\n      .internal()\n      .doc(\"If enabled, sort within files by the specified MDC curve. \" +\n         \"This might improve row-group skipping and data compression, at \" +\n         \"the cost of additional overhead for sorting.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_OPTIMIZE_ZORDER_COL_STAT_CHECK =\n    buildConf(\"optimize.zorder.checkStatsCollection.enabled\")\n      .internal()\n      .doc(s\"When enabled, we will check if the column we're actually collecting stats \" +\n        \"on the columns we are z-ordering on.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val FAST_INTERLEAVE_BITS_ENABLED =\n    buildConf(\"optimize.zorder.fastInterleaveBits.enabled\")\n      .internal()\n      .doc(\"When true, a faster version of the bit interleaving algorithm is used.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val INTERNAL_UDF_OPTIMIZATION_ENABLED =\n    buildConf(\"internalUdfOptimization.enabled\")\n      .internal()\n      .doc(\n        \"\"\"If true, create udfs used by Delta internally from templates to reduce lock contention\n          |caused by Scala Reflection.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_OPTIMIZE_CONDITIONAL_INCREMENT_METRIC_ENABLED =\n    buildConf(\"optimize.conditionalIncrementMetric.enabled\")\n      .internal()\n      .doc(\"Whether to enable optimization of ConditionalIncrementMetric expressions with \" +\n        \"constant conditions.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED =\n    buildConf(\"generatedColumn.partitionFilterOptimization.enabled\")\n      .internal()\n      .doc(\n      \"Whether to extract partition filters automatically from data filters for a partition\" +\n        \" generated column if possible\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val GENERATED_COLUMN_ALLOW_NULLABLE =\n    buildConf(\"generatedColumn.allowNullableIngest.enabled\")\n      .internal()\n      .doc(\"When enabled this will allow tables with generated columns enabled to be able \" +\n        \"to write data without providing values for a nullable column via DataFrame.write\")\n      .booleanConf\n      .createWithDefault(true)\n\n  object GeneratedColumnValidateOnWriteMode extends Enumeration {\n    val OFF, LOG_ONLY, ASSERT = Value\n\n    def fromConf(conf: SQLConf): Value =\n      withName(conf.getConf(GENERATED_COLUMN_VALIDATE_ON_WRITE))\n\n    def default: Value =\n      withName(GENERATED_COLUMN_VALIDATE_ON_WRITE.defaultValueString)\n  }\n\n  val GENERATED_COLUMN_VALIDATE_ON_WRITE =\n    buildConf(\"generatedColumn.validateOnWrite.enabled\")\n      .internal()\n      .doc(\"When enabled, validates generated column expressions during write operations to \" +\n        \"protect against disallowed expressions.\")\n      .stringConf\n      .transform(_.toUpperCase(Locale.ROOT))\n      .checkValues(GeneratedColumnValidateOnWriteMode.values.map(_.toString))\n      .createWithDefault(GeneratedColumnValidateOnWriteMode.LOG_ONLY.toString)\n\n  object ValidateCheckConstraintsMode extends Enumeration {\n    val OFF, LOG_ONLY, ASSERT = Value\n\n    def fromConf(conf: SQLConf): Value =\n      withName(conf.getConf(VALIDATE_CHECK_CONSTRAINTS))\n\n    def default: Value =\n      withName(VALIDATE_CHECK_CONSTRAINTS.defaultValueString)\n  }\n\n  val VALIDATE_CHECK_CONSTRAINTS =\n    buildConf(\"checkConstraints.validation.enabled\")\n      .internal()\n      .doc(\"When enabled, validates check constraints expressions during both creation and write\" +\n        \" paths to protect against disallowed expressions.\")\n      .stringConf\n      .transform(_.toUpperCase(Locale.ROOT))\n      .checkValues(ValidateCheckConstraintsMode.values.map(_.toString))\n      .createWithDefault(ValidateCheckConstraintsMode.LOG_ONLY.toString)\n\n  val DELTA_CONVERT_ICEBERG_ENABLED =\n    buildConf(\"convert.iceberg.enabled\")\n      .internal()\n      .doc(\"If enabled, Iceberg tables can be converted into a Delta table.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CONVERT_ICEBERG_PARTITION_EVOLUTION_ENABLED =\n    buildConf(\"convert.iceberg.partitionEvolution.enabled\")\n      .doc(\"If enabled, support conversion of iceberg tables experienced partition evolution.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CONVERT_ICEBERG_BUCKET_PARTITION_ENABLED =\n    buildConf(\"convert.iceberg.bucketPartition.enabled\")\n      .doc(\"If enabled, convert iceberg table with bucket partition to unpartitioned delta table.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CONVERT_ICEBERG_CAST_TIME_TYPE = {\n    buildConf(\"convert.iceberg.castTimeType\")\n      .internal()\n      .doc(\"Cast Iceberg TIME type to Spark Long when converting to Delta\")\n      .booleanConf\n      .createWithDefault(false)\n  }\n\n  final object NonDeterministicPredicateWidening {\n    final val OFF = \"off\"\n    final val LOGGING = \"logging\"\n    final val ON = \"on\"\n\n    final val list = Set(OFF, LOGGING, ON)\n  }\n\n  val DELTA_CONFLICT_DETECTION_WIDEN_NONDETERMINISTIC_PREDICATES =\n    buildConf(\"conflictDetection.partitionLevelConcurrency.widenNonDeterministicPredicates\")\n      .doc(\"Whether to widen non-deterministic predicates during partition-level concurrency. \" +\n        \"Widening can lead to additional conflicts.\" +\n        \"When the value is 'off', non-deterministic predicates are not widened during conflict \" +\n        \"resolution.\" +\n        \"The value 'logging' will log whether the widening of non-deterministic predicates lead \" +\n        \"to additional conflicts. The conflict resolution is still done without widening. \" +\n        \"When the value is 'on', non-deterministic predicates are widened during conflict \" +\n        \"resolution.\")\n      .internal()\n      .stringConf\n      .transform(_.toLowerCase(Locale.ROOT))\n      .checkValues(NonDeterministicPredicateWidening.list)\n      .createWithDefault(NonDeterministicPredicateWidening.ON)\n\n  val DELTA_CONFLICT_DETECTION_ALLOW_REPLACE_TABLE_TO_REMOVE_NEW_DOMAIN_METADATA =\n    buildConf(\"conflictDetection.allowReplaceTableToRemoveNewDomainMetadata\")\n      .doc(\"Whether to allow removing new domain metadatas from concurrent transactions during \" +\n        \"conflict resolution for a REPLACE TABLE operation. Note that this flag applies only \" +\n        \"to metadata domains where the table snapshot read by the REPLACE TABLE command did \" +\n        \"not contain a domain metadata of the same domain.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_UNIFORM_ICEBERG_SYNC_CONVERT_ENABLED =\n    buildConf(\"uniform.iceberg.sync.convert.enabled\")\n      .doc(\"If enabled, iceberg conversion will be done synchronously. \" +\n        \"This can cause slow down in Delta commits and should only be used \" +\n        \"for debugging or in test suites.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_UNIFORM_HUDI_SYNC_CONVERT_ENABLED =\n    buildConf(\"uniform.hudi.sync.convert.enabled\")\n      .doc(\"If enabled, Hudi conversion will be done synchronously.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_UNIFORM_ICEBERG_RETRY_TIMES =\n    buildConf(\"uniform.iceberg.retry.times\")\n      .doc(\"The number of retries iceberg conversions should have in case \" +\n        \"of failures\")\n      .internal()\n      .intConf\n      .createWithDefault(3)\n\n  val DELTA_UNIFORM_ICEBERG_INCLUDE_BASE_CONVERTED_VERSION =\n    buildConf(\"uniform.iceberg.include.base.converted.version\")\n      .doc(\"If true, include the base converted delta version as a tbl property in Iceberg \" +\n        \"metadata to indicate the delta version that the conversion started from\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_OPTIMIZE_MIN_FILE_SIZE =\n    buildConf(\"optimize.minFileSize\")\n        .internal()\n        .doc(\n          \"\"\"Files which are smaller than this threshold (in bytes) will be grouped together\n             | and rewritten as larger files by the OPTIMIZE command.\n             |\"\"\".stripMargin)\n        .longConf\n        .checkValue(_ >= 0, \"minFileSize has to be positive\")\n        .createWithDefault(1024 * 1024 * 1024)\n\n  val DELTA_OPTIMIZE_MAX_FILE_SIZE =\n    buildConf(\"optimize.maxFileSize\")\n        .internal()\n        .doc(\"Target file size produced by the OPTIMIZE command.\")\n        .longConf\n        .checkValue(_ >= 0, \"maxFileSize has to be positive\")\n        .createWithDefault(1024 * 1024 * 1024)\n\n  val DELTA_OPTIMIZE_MAX_THREADS =\n    buildConf(\"optimize.maxThreads\")\n        .internal()\n        .doc(\n          \"\"\"\n            |Maximum number of parallel jobs allowed in OPTIMIZE command. Increasing the maximum\n            | parallel jobs allows the OPTIMIZE command to run faster, but increases the job\n            | management on the Spark driver side.\n            |\"\"\".stripMargin)\n        .intConf\n        .checkValue(_ > 0, \"'optimize.maxThreads' must be positive.\")\n        .createWithDefault(15)\n\n  val DELTA_OPTIMIZE_BATCH_SIZE =\n    buildConf(\"optimize.batchSize\")\n        .internal()\n        .doc(\n          \"\"\"\n            |The size of a batch within an OPTIMIZE JOB. After a batch is complete, its\n            | progress will be committed to the transaction log, allowing for incremental\n            | progress.\n            |\"\"\".stripMargin)\n        .bytesConf(ByteUnit.BYTE)\n        .checkValue(_ > 0, \"batchSize has to be positive\")\n        .createOptional\n\n  val DELTA_OPTIMIZE_REPARTITION_ENABLED =\n    buildConf(\"optimize.repartition.enabled\")\n      .internal()\n      .doc(\"Use repartition(1) instead of coalesce(1) to merge small files. \" +\n        \"coalesce(1) is executed with only one task, if there are many tiny files \" +\n        \"within a bin (e.g. 1000 files of 50MB), it cannot be optimized with more executors. \" +\n        \"repartition(1) incurs a shuffle stage, but the job can be distributed.\"\n      )\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_ALTER_TABLE_CHANGE_COLUMN_CHECK_EXPRESSIONS =\n    buildConf(\"alterTable.changeColumn.checkExpressions\")\n      .internal()\n      .doc(\n        \"\"\"\n          |Given an ALTER TABLE command that changes columns, check if there are expressions used\n          | in Check Constraints and Generated Columns that reference this column and thus will\n          | be affected by this change.\n          |\n          |This is a safety switch - we should only turn this off when there is an issue with\n          |expression checking logic that prevents a valid column change from going through.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_LIQUID_ALTER_COLUMN_AFTER_STATS_SCHEMA_CHECK =\n    buildConf(\"liquid.alterColumnAfter.statsSchemaCheck\")\n      .internal()\n      .doc(\n         \"\"\"\n           |When enabled, validates that clustering columns remain in the stats schema after\n           | a user executes `ALTER TABLE ALTER COLUMN col1 AFTER col2`. The validation checks\n           | that all clustering columns that were in the stats schema before the column reordering\n           | remain in the stats schema after the operation. This ensures that clustering columns\n           | continue to have statistics collected even if their position in the table schema\n           | changes. When disabled, no validation is performed and stats collection may follow\n           | position-based indexing rules (e.g., `dataSkippingNumIndexedCols`), potentially\n           | causing clustering columns to lose stats collection if they move outside the indexed\n           | range.\n        \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CHANGE_COLUMN_CHECK_DEPENDENT_EXPRESSIONS_USE_V2 =\n    buildConf(\"changeColumn.checkDependentExpressionsUseV2\")\n      .internal()\n      .doc(\n        \"\"\"\n          |More accurate implementation of checker for altering/renaming/dropping columns\n          |that might be referenced by constraints or generation rules.\n          |It respects nested arrays and maps, unlike the V1 checker.\n          |\n          |This is a safety switch - we should only turn this off when there is an issue with\n          |expression checking logic that prevents a valid column change from going through.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_RENAME_COLUMN_ESCAPE_NAME =\n    buildConf(\"changeColumn.renameColumnEscapeName\")\n      .internal()\n      .doc(\n        \"\"\"\n          |Properly escape column names when renaming a column in the metadata.\n          |\n          |This is a safety switch - we should only set this to false if the fix introduces some\n          |regression.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_ALTER_TABLE_DROP_COLUMN_ENABLED =\n    buildConf(\"alterTable.dropColumn.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether to enable the drop column feature for Delta.\n          |This is a safety switch - we should only turn this off when there is an issue.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_BYPASS_CHARVARCHAR_TO_STRING_FIX =\n    buildConf(\"alterTable.bypassCharVarcharToStringFix\")\n      .internal()\n      .doc(\n        \"\"\"Whether to bypass the fix for CHAR/VARCHAR to STRING type conversion in ALTER TABLE.\n          |This is a safety switch - we should only set this to true if the fix introduces some\n          |regression.\n          |The fix in question strips CHAR/VARCHAR metadata from columns and converts\n          |StringType to CHAR/VARCHAR Type temporarily during alter table column commands.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP = {\n    buildConf(\"changeDataFeed.timestampOutOfRange.enabled\")\n      .doc(\n        \"\"\"When enabled, Change Data Feed queries with starting and ending timestamps\n           | exceeding the newest delta commit timestamp will not error out. For starting timestamp\n           | out of range we will return an empty DataFrame, for ending timestamps out of range we\n           | will consider the latest Delta version as the ending version.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n  }\n\n  val DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES =\n    buildConf(\"streaming.unsafeReadOnIncompatibleColumnMappingSchemaChanges.enabled\")\n      .doc(\n        \"Streaming read on Delta table with column mapping schema operations \" +\n          \"(e.g. rename or drop column) is currently blocked due to potential data loss and \" +\n        \"schema confusion. However, existing users may use this flag to force unblock \" +\n          \"if they'd like to take the risk.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES_DURING_STREAM_START =\n    buildConf(\"streaming.unsafeReadOnIncompatibleSchemaChangesDuringStreamStart.enabled\")\n      .doc(\n        \"\"\"A legacy config to disable schema read-compatibility check on the start version schema\n          |when starting a streaming query. The config is added to allow legacy problematic queries\n          |disabling the check to keep running if users accept the potential risks of incompatible\n          |schema reading.\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_STREAMING_UNSAFE_READ_ON_PARTITION_COLUMN_CHANGE =\n    buildConf(\"streaming.unsafeReadOnPartitionColumnChanges.enabled\")\n      .doc(\n        \"Streaming read on Delta table with partition column overwrite \" +\n          \"(e.g. changing partition column) is currently blocked due to potential data loss. \" +\n          \"However, existing users may use this flag to force unblock \" +\n          \"if they'd like to take the risk.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_STREAMING_IGNORE_INTERNAL_METADATA_FOR_SCHEMA_CHANGE =\n    buildConf(\"streaming.ignoreInternalMetadataForSchemaChange.enabled\")\n      .doc(\n        \"Whether to ignore internal metadata attached to struct fields when detecting schema \" +\n        \"changes in Delta sources, e.g. identity columns internal high-water mark tracking.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAMING_ENABLE_SCHEMA_TRACKING =\n    buildConf(\"streaming.schemaTracking.enabled\")\n      .doc(\n        \"\"\"If enabled, Delta streaming source can support non-additive schema evolution for\n          |operations such as rename or drop column on column mapping enabled tables.\n          |\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAMING_ENABLE_SCHEMA_TRACKING_MERGE_CONSECUTIVE_CHANGES =\n    buildConf(\"streaming.schemaTracking.mergeConsecutiveSchemaChanges.enabled\")\n      .doc(\n        \"When enabled, schema tracking in Delta streaming would consider multiple consecutive \" +\n          \"schema changes as one.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAMING_ALLOW_SCHEMA_LOCATION_OUTSIDE_CHECKPOINT_LOCATION =\n    buildConf(\"streaming.allowSchemaLocationOutsideCheckpointLocation\")\n      .doc(\n        \"When enabled, Delta streaming can set a schema location outside of the \" +\n        \"query's checkpoint location. This is not recommended.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_STREAMING_SCHEMA_TRACKING_METADATA_PATH_CHECK_ENABLED =\n    buildConf(\"streaming.schemaTracking.metadataPathCheck.enabled\")\n      .doc(\n        \"When enabled, Delta streaming with schema tracking will ensure the schema log entry \" +\n          \"must match the source's unique checkpoint metadata location.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAM_UNSAFE_READ_ON_NULLABILITY_CHANGE =\n    buildConf(\"streaming.unsafeReadOnNullabilityChange.enabled\")\n      .doc(\n        \"\"\"A legacy config to disable unsafe nullability check. The config is added to allow legacy\n          |problematic queries disabling the check to keep running if users accept the potential\n          |risks of incompatible schema reading.\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_STREAMING_CREATE_DATAFRAME_DROP_NULL_COLUMNS =\n    buildConf(\"streaming.createDataFrame.dropNullColumns\")\n      .internal()\n      .doc(\"Whether to drop columns with NullType in DeltaLog.createDataFrame.\")\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CREATE_DATAFRAME_DROP_NULL_COLUMNS =\n    buildConf(\"createDataFrame.dropNullColumns\")\n      .internal()\n      .doc(\"Whether to drop columns with NullType in DeltaLog.createDataFrame.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS =\n    buildConf(\"streaming.sink.allowImplicitCasts\")\n      .internal()\n      .doc(\n        \"\"\"Whether to accept writing data to a Delta streaming sink when the data type doesn't\n          |match the type in the underlying Delta table. When true, data is cast to the expected\n          |type before the write. When false, the write fails.\n          |The casting behavior is governed by 'spark.sql.storeAssignmentPolicy'.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAMING_SINK_IMPLICIT_CAST_FOR_TYPE_MISMATCH_ONLY =\n    buildConf(\"streaming.sink.implicitCastForTypeMismatchOnly\")\n      .internal()\n      .doc(\n        \"\"\"Controls when an implicit cast is added when writing data to a Delta table using\n          |streaming.\n          |When true, a cast is added only when there is a type mismatch between a column or\n          |nested field in the data and table schema.\n          |When false, missing, extra or reordered columns or nested fields also trigger adding an\n          |implicit cast.\n          |Only takes effect when implicit casting is enabled in streaming writes to a Delta table\n          |via `spark.databricks.delta.streaming.sink.allowImplicitCasts`.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STREAMING_SINK_IMPLICIT_CAST_ESCAPE_COLUMN_NAMES =\n    buildConf(\"streaming.sink.implicitCastEscapeColumnNames\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When true, the code paths handling implicit casting in streaming will escape column names\n          |to properly handle e.g. dots in column names.\n          |This is a kill-switch and shouldn't be disabled unless necessary to mitigate an issue.\n          |Only takes effect when implicit casting is enabled in streaming writes to a Delta table\n          |via `spark.databricks.delta.streaming.sink.allowImplicitCasts`.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CDF_UNSAFE_BATCH_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES =\n    buildConf(\"changeDataFeed.unsafeBatchReadOnIncompatibleSchemaChanges.enabled\")\n      .doc(\n        \"Reading change data in batch (e.g. using `table_changes()`) on Delta table with \" +\n          \"column mapping schema operations is currently blocked due to potential data loss and \" +\n          \"schema confusion. However, existing users may use this flag to force unblock \" +\n          \"if they'd like to take the risk.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE =\n    buildConf(\"changeDataFeed.defaultSchemaModeForColumnMappingTable\")\n      .doc(\n        \"\"\"Reading batch CDF on column mapping enabled table requires schema mode to be set to\n           |`endVersion` so the ending version's schema will be used.\n           |Set this to `latest` to use the schema of the latest available table version,\n           |or to `legacy` to fallback to the non column-mapping default behavior, in which\n           |the time travel option can be used to select the version of the schema.\"\"\".stripMargin)\n      .internal()\n      .stringConf\n      .createWithDefault(\"endVersion\")\n\n  val DELTA_CDF_ALLOW_TIME_TRAVEL_OPTIONS =\n    buildConf(\"changeDataFeed.allowTimeTravelOptionsForSchema\")\n      .doc(\n        s\"\"\"If allowed, user can specify time-travel reader options such as\n           |'versionAsOf' or 'timestampAsOf' to specify the read schema while\n           |reading change data feed.\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_COLUMN_MAPPING_CHECK_MAX_COLUMN_ID =\n    buildConf(\"columnMapping.checkMaxColumnId\")\n      .doc(\n        s\"\"\"If enabled, check if delta.columnMapping.maxColumnId is correctly assigned at each\n           |Delta transaction commit.\n           |\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_COLUMN_MAPPING_STRIP_METADATA =\n    buildConf(\"columnMapping.stripMetadata\")\n      .doc(\n        \"\"\"\n          |Transactions might try to update the schema of a table with columns that contain\n          |column mapping metadata, even when column mapping is not enabled. For example, this\n          |can happen when transactions copy the schema from another table. When this setting is\n          |enabled, we will strip the column mapping metadata from the schema before applying it.\n          |Note that this config applies only when the existing schema of the table does not\n          |contain any column mapping metadata.\n          |\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_COLUMN_MAPPING_DISALLOW_ENABLING_WHEN_METADATA_ALREADY_EXISTS =\n    buildConf(\"columnMapping.disallowEnablingWhenColumnMappingMetadataAlreadyExists\")\n      .doc(\n        \"\"\"\n          |If Delta table already has column mapping metadata before the feature is enabled, it is\n          |as a result of a corruption or a bug. Enabling column mapping in such a case can lead to\n          |further corruption of the table and should be disallowed.\n          |\"\"\".stripMargin)\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DYNAMIC_PARTITION_OVERWRITE_ENABLED =\n    buildConf(\"dynamicPartitionOverwrite.enabled\")\n      .doc(\"Whether to overwrite partitions dynamically when 'partitionOverwriteMode' is set to \" +\n        \"'dynamic' in either the SQL conf, or a DataFrameWriter option. When this is disabled \" +\n        \"'partitionOverwriteMode' will be ignored.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val ALLOW_ARBITRARY_TABLE_PROPERTIES =\n    buildConf(\"allowArbitraryProperties.enabled\")\n      .doc(\n      \"\"\"Whether we allow arbitrary Delta table properties. When this is enabled, table properties\n          |with the prefix 'delta.' are not checked for validity. Table property validity is based\n          |on the current Delta version being used and feature support in that version. Arbitrary\n          |properties without the 'delta.' prefix are always allowed regardless of this config.\n          |\n          |Please use with caution. When enabled, there will be no warning when unsupported table\n          |properties for the Delta version being used are set, or when properties are set\n          |incorrectly (for example, misspelled).\"\"\".stripMargin\n      )\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val TABLE_BUILDER_FORCE_TABLEPROPERTY_LOWERCASE =\n    buildConf(\"deltaTableBuilder.forceTablePropertyLowerCase.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Whether the keys of table properties should be set to lower case.\n          | Turn on this flag if you want keys of table properties not starting with delta\n          | to be backward compatible when the table is created via DeltaTableBuilder\n          | Please note that if you set this to true, the lower case of the\n          | key will be used for non delta prefix table properties.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_REQUIRED_SPARK_CONFS_CHECK =\n    buildConf(\"requiredSparkConfsCheck.enabled\")\n      .doc(\"Whether to verify SparkSession is initialized with required configurations.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED =\n    buildConf(\"restore.protocolDowngradeAllowed\")\n      .doc(\"\"\"\n        | Whether a table RESTORE or CLONE operation may downgrade the protocol of the table.\n        | Note that depending on the protocol and the enabled table features, downgrading the\n        | protocol may break snapshot reconstruction and make the table unreadable. Protocol\n        | downgrades may also make the history unreadable.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_CLONE_REPLACE_ENABLED =\n    buildConf(\"clone.replaceEnabled\")\n      .internal()\n      .doc(\"If enabled, the table will be replaced when cloning over an existing Delta table.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_OPTIMIZE_METADATA_QUERY_ENABLED =\n    buildConf(\"optimizeMetadataQuery.enabled\")\n      .internal()\n      .doc(\"Whether we can use the metadata in the DeltaLog to\" +\n        \" optimize queries that can be run purely on metadata.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_SKIP_RECORDING_EMPTY_COMMITS =\n    buildConf(\"skipRecordingEmptyCommits\")\n      .internal()\n      .doc(\n        \"\"\"\n          | Whether to skip recording an empty commit in the Delta Log. This only works when table\n          | is using SnapshotIsolation or Serializable Isolation Mode.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val REPLACE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED =\n  buildConf(\"replace.protocolDowngradeAllowed\")\n    .internal()\n    .doc(\"\"\"\n       | Whether a REPLACE operation may downgrade the protocol of the table.\n       | Note that depending on the protocol and the enabled table features, downgrading the\n       | protocol may break snapshot reconstruction and make the table unreadable. Protocol\n       | downgrades may also make the history unreadable.\"\"\".stripMargin)\n    .booleanConf\n    .createWithDefault(false)\n\n  //////////////////\n  // Idempotent DML\n  //////////////////\n\n  val DELTA_IDEMPOTENT_DML_TXN_APP_ID =\n    buildConf(\"write.txnAppId\")\n      .internal()\n      .doc(\"\"\"\n             |The application ID under which this write will be committed.\n             | If specified, spark.databricks.delta.write.txnVersion also needs to\n             | be set.\n             |\"\"\".stripMargin)\n      .stringConf\n      .createOptional\n\n  val DELTA_IDEMPOTENT_DML_TXN_VERSION =\n    buildConf(\"write.txnVersion\")\n      .internal()\n      .doc(\"\"\"\n             |The user-defined version under which this write will be committed.\n             | If specified, spark.databricks.delta.write.txnAppId also needs to\n             | be set. To ensure idempotency, txnVersions across different writes\n             | need to be monotonically increasing.\n             |\"\"\".stripMargin)\n      .longConf\n      .createOptional\n\n  val DELTA_IDEMPOTENT_DML_AUTO_RESET_ENABLED =\n    buildConf(\"write.txnVersion.autoReset.enabled\")\n      .internal()\n      .doc(\"\"\"\n             |If true, will automatically reset spark.databricks.delta.write.txnVersion\n             |after every write. This is false by default.\n             |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_OPTIMIZE_MAX_DELETED_ROWS_RATIO =\n    buildConf(\"optimize.maxDeletedRowsRatio\")\n      .internal()\n      .doc(\"Files with a ratio of deleted rows to the total rows larger than this threshold \" +\n        \"will be rewritten by the OPTIMIZE command.\")\n      .doubleConf\n      .checkValue(_ >= 0, \"maxDeletedRowsRatio must be in range [0.0, 1.0]\")\n      .checkValue(_ <= 1, \"maxDeletedRowsRatio must be in range [0.0, 1.0]\")\n      .createWithDefault(0.05d)\n\n  val DELTA_TABLE_PROPERTY_CONSTRAINTS_CHECK_ENABLED =\n    buildConf(\"tablePropertyConstraintsCheck.enabled\")\n      .internal()\n      .doc(\n        \"\"\"Check that all table-properties satisfy validity constraints.\n          |Only change this for testing!\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_DUPLICATE_ACTION_CHECK_ENABLED =\n    buildConf(\"duplicateActionCheck.enabled\")\n      .internal()\n      .doc(\"\"\"\n             |Verify only one action is specified for each file path in one commit.\n             |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELETE_USE_PERSISTENT_DELETION_VECTORS =\n    buildConf(\"delete.deletionVectors.persistent\")\n      .internal()\n      .doc(\"Enable persistent Deletion Vectors in the Delete command.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val MERGE_USE_PERSISTENT_DELETION_VECTORS =\n    buildConf(\"merge.deletionVectors.persistent\")\n      .internal()\n      .doc(\"Enable persistent Deletion Vectors in Merge command.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val UPDATE_USE_PERSISTENT_DELETION_VECTORS =\n    buildConf(\"update.deletionVectors.persistent\")\n      .internal()\n      .doc(\"Enable persistent Deletion Vectors in the Update command.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELETION_VECTOR_PACKING_TARGET_SIZE =\n    buildConf(\"deletionVectors.packing.targetSize\")\n      .internal()\n      .doc(\"Controls the target file deletion vector file size when packing multiple\" +\n        \"deletion vectors in a single file.\")\n      .bytesConf(ByteUnit.BYTE)\n      /**\n       * A [[DeletionVectorDescriptor]] stores an offset as a 32-bit integer into the file where the\n       * deletion vector is stored. There is a hard limit of ~2.1GB for this file before the offset\n       * integer overflows. Since we do bin packing with estimates, we set a lower internal\n       * limit to be safe.\n       */\n      .checkValue(_ >= 0, \"deletionVectors.packing.targetSize must be non-negative\")\n      .checkValue(_ < 3L * 1024L * 1024L * 1024L / 2L,\n         \"deletionVectors.packing.targetSize must be less than 1.5GB\")\n      .createWithDefault(2L * 1024L * 1024L)\n\n  val TIGHT_BOUND_COLUMN_ON_FILE_INIT_DISABLED =\n    buildConf(\"deletionVectors.disableTightBoundOnFileCreationForDevOnly\")\n      .internal()\n      .doc(\"\"\"Controls whether we generate a tightBounds column in statistics on file creation.\n             |The tightBounds column annotates whether the statistics of the file are tight or wide.\n             |This flag is only used for testing purposes.\n                \"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELETION_VECTORS_USE_METADATA_ROW_INDEX =\n    buildConf(\"deletionVectors.useMetadataRowIndex\")\n      .internal()\n      .doc(\n        \"\"\"Controls whether we use the Parquet reader generated row_index column for\n          | filtering deleted rows with deletion vectors. When enabled, it allows\n          | predicate pushdown and file splitting in scans.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val WRITE_DATA_FILES_TO_SUBDIR = buildConf(\"write.dataFilesToSubdir\")\n    .internal()\n    .doc(\"Delta will write all data files to subdir 'data/' under table dir if enabled\")\n    .booleanConf\n    .createWithDefault(false)\n\n  val DELETION_VECTORS_COMMIT_CHECK_ENABLED =\n    buildConf(\"deletionVectors.skipCommitCheck\")\n      .internal()\n      .doc(\n        \"\"\"Check the table-property and verify that deletion vectors may be added\n          |to this table.\n          |Only change this for testing!\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val REUSE_COLUMN_MAPPING_METADATA_DURING_OVERWRITE =\n    buildConf(\"columnMapping.reuseColumnMetadataDuringOverwrite\")\n      .internal()\n      .doc(\n        \"\"\"\n          |If enabled, when a column mapping table is overwritten, the new schema will reuse as many\n          |old schema's column mapping metadata (field id and physical name) as possible.\n          |This allows the analyzed schema from prior to the overwrite to be still read-compatible\n          |with the data post the overwrite, enabling better user experience when, for example,\n          |the column mapping table is being continuously scanned in a streaming query, the analyzed\n          |table schema will still be readable after the table is overwritten.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE =\n    buildConf(\"columnMapping.reuseColumnMetadataDuringReplace\")\n      .internal()\n      .doc(\n        \"\"\"\n          |If enabled, when a column mapping table is replaced, the new schema will reuse as many\n          |old schema's column mapping metadata (field id and physical name) as possible.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val ALLOW_COLUMN_MAPPING_REMOVAL =\n    buildConf(\"columnMapping.allowRemoval\")\n      .internal()\n      .doc(\n        \"\"\"\n          |If enabled, allow the column mapping to be removed from a table.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTALOG_MINOR_COMPACTION_USE_FOR_READS =\n    buildConf(\"deltaLog.minorCompaction.useForReads\")\n      .doc(\"If true, minor compacted delta log files will be used for creating Snapshots\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val ICEBERG_MAX_COMMITS_TO_CONVERT = buildConf(\"iceberg.maxPendingCommits\")\n    .doc(\"\"\"\n        |The maximum number of pending Delta commits to convert to Iceberg incrementally.\n        |If the table hasn't been converted to Iceberg in longer than this number of commits,\n        |we start from scratch, replacing the previously converted Iceberg table contents.\n        |\"\"\".stripMargin)\n    .intConf\n    .createWithDefault(100)\n\n  val HUDI_MAX_COMMITS_TO_CONVERT = buildConf(\"hudi.maxPendingCommits\")\n    .doc(\"\"\"\n           |The maximum number of pending Delta commits to convert to Hudi incrementally.\n           |If the table hasn't been converted to Hudi in longer than this number of commits,\n           |we start from scratch, replacing the previously converted Hudi table contents.\n           |\"\"\".stripMargin)\n    .intConf\n    .createWithDefault(100)\n\n  val ICEBERG_MAX_ACTIONS_TO_CONVERT = buildConf(\"iceberg.maxPendingActions\")\n    .doc(\"\"\"\n        |[Deprecated]\n        |The maximum number of pending Delta actions to convert to Iceberg incrementally.\n        |If there are more than this number of outstanding actions, chunk them into separate\n        |Iceberg commits.\n        |\"\"\".stripMargin)\n    .intConf\n    .createWithDefault(100 * 1000)\n\n  val UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG =\n    buildConf(\"updateAndMergeCastingFollowsAnsiEnabledFlag\")\n      .internal()\n      .doc(\"\"\"If false, casting behaviour in implicit casts in UPDATE and MERGE follows\n             |'spark.sql.storeAssignmentPolicy'. If true, these casts follow 'ansi.enabled'.\n             |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_USE_MULTI_THREADED_STATS_COLLECTION =\n    buildConf(\"collectStats.useMultiThreadedStatsCollection\")\n      .internal()\n      .doc(\"Whether to use multi-threaded statistics collection. If false, statistics will be \" +\n        \"collected sequentially within each partition.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_STATS_COLLECTION_NUM_FILES_PARTITION =\n    buildConf(\"collectStats.numFilesPerPartition\")\n      .internal()\n      .doc(\"Controls the number of files that should be within a RDD partition \" +\n        \"during multi-threaded optimized statistics collection. A larger number will lead to \" +\n        \"less parallelism, but can reduce scheduling overhead.\")\n      .intConf\n      .checkValue(v => v >= 1, \"Must be at least 1.\")\n      .createWithDefault(100)\n\n  val DELTA_STATS_COLLECTION_FALLBACK_TO_INTERPRETED_PROJECTION =\n    buildConf(\"collectStats.fallbackToInterpretedProjection\")\n      .internal()\n      .doc(\"When enabled, the updateStats expression will use the standard code path\" +\n        \" that falls back to an interpreted expression if codegen fails. This should\" +\n        \" always be true. The config only exists to force the old behavior, which was\" +\n        \" to always use codegen.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_CONVERT_ICEBERG_STATS = buildConf(\"collectStats.convertIceberg\")\n    .internal()\n    .doc(\"When enabled, attempts to convert Iceberg stats to Delta stats when cloning from \" +\n      \"an Iceberg source.\")\n    .booleanConf\n    .createWithDefault(true)\n\n  val DELTA_CONVERT_ICEBERG_DECIMAL_STATS = buildConf(\"collectStats.convertIceberg.decimal\")\n    .internal()\n    .doc(\"When enabled, attempts to convert Iceberg stats for DECIMAL to Delta stats\" +\n      \"when cloning from an Iceberg source.\")\n    .booleanConf\n    .createWithDefault(true)\n\n  val DELTA_CONVERT_ICEBERG_DATE_STATS = buildConf(\"collectStats.convertIceberg.date\")\n    .internal()\n    .doc(\"When enabled, attempts to convert Iceberg stats for DATE to Delta stats\" +\n      \"when cloning from an Iceberg source.\")\n    .booleanConf\n    .createWithDefault(true)\n\n  val DELTA_CONVERT_ICEBERG_TIMESTAMP_STATS = buildConf(\"collectStats.convertIceberg.timestamp\")\n    .internal()\n    .doc(\"When enabled, attempts to convert Iceberg stats for TIMESTAMP to Delta stats\" +\n      \"when cloning from an Iceberg source.\")\n    .booleanConf\n    .createWithDefault(true)\n\n  /**\n   * For iceberg clone,\n   * When stats conversion from iceberg off, fallback to slow stats conversion enabled\n   * When stats conversion from iceberg on,\n   *  fallback to slow stats conversion will not happen if partial stats conversion enabled\n   *  fallback only happens if partial stats conversion disabled and iceberg has partial stats\n   *  - either minValues or maxValues is missing\n   */\n  val DELTA_CLONE_ICEBERG_ALLOW_PARTIAL_STATS =\n    buildConf(\"clone.iceberg.allowPartialStats\")\n      .internal()\n      .doc(\"If true, allow converting partial stats from iceberg stats \" +\n        \"to delta stats during clone.\"\n      )\n      .booleanConf\n      .createWithDefault(true)\n\n  /////////////////////\n  // Optimized Write\n  /////////////////////\n\n  val DELTA_OPTIMIZE_WRITE_ENABLED =\n    buildConf(\"optimizeWrite.enabled\")\n      .doc(\"Whether to optimize writes made into Delta tables from this session.\")\n      .booleanConf\n      .createOptional\n\n  val DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS =\n    buildConf(\"optimizeWrite.numShuffleBlocks\")\n      .internal()\n      .doc(\"Maximum number of shuffle blocks to target for the adaptive shuffle \" +\n        \"in optimized writes.\")\n      .intConf\n      .createWithDefault(50000000)\n\n  val SKIP_REDIRECT_FEATURE =\n    buildConf(\"skipRedirectFeature\")\n      .doc(\"True if skipping the redirect feature.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val ENABLE_TABLE_REDIRECT_FEATURE =\n    buildConf(\"enableTableRedirectFeature\")\n      .doc(\"True if enabling the table redirect feature.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_OPTIMIZE_WRITE_MAX_SHUFFLE_PARTITIONS =\n    buildConf(\"optimizeWrite.maxShufflePartitions\")\n      .internal()\n      .doc(\"Max number of output buckets (reducers) that can be used by optimized writes. This \" +\n        \"can be thought of as: 'how many target partitions are we going to write to in our \" +\n        \"table in one write'. This should not be larger than \" +\n        \"spark.shuffle.minNumPartitionsToHighlyCompress. Otherwise, partition coalescing and \" +\n        \"skew split may not work due to incomplete stats from HighlyCompressedMapStatus\")\n      .intConf\n      .createWithDefault(2000)\n\n  val DELTA_OPTIMIZE_WRITE_BIN_SIZE =\n    buildConf(\"optimizeWrite.binSize\")\n      .internal()\n      .doc(\"Bin size for the adaptive shuffle in optimized writes in megabytes.\")\n      .bytesConf(ByteUnit.MiB)\n      .createWithDefault(512)\n\n  val DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE =\n  buildConf(\"optimize.clustering.mergeStrategy.minCubeSize.threshold\")\n    .internal()\n    .doc(\n      \"Z-cube size at which new data will no longer be merged with it during incremental \" +\n        \"OPTIMIZE.\"\n    )\n    .longConf\n    .checkValue(_ >= 0, \"the threshold must be >= 0\")\n    .createWithDefault(100 * DELTA_OPTIMIZE_MAX_FILE_SIZE.defaultValue.get)\n\n  val DELTA_OPTIMIZE_CLUSTERING_TARGET_CUBE_SIZE =\n  buildConf(\"optimize.clustering.mergeStrategy.minCubeSize.targetCubeSize\")\n    .internal()\n    .doc(\n      \"Target size of the Z-cubes we will create. This is not a hard max; we will continue \" +\n        \"adding files to a Z-cube until their combined size exceeds this value. This value \" +\n        s\"must be greater than or equal to ${DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key}. \"\n    )\n    .longConf\n    .checkValue(_ >= 0, \"the target must be >= 0\")\n    .createWithDefault((DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.defaultValue.get * 1.5).toLong)\n\n  //////////////////\n  // Clustered Table\n  //////////////////\n\n  val DELTA_NUM_CLUSTERING_COLUMNS_LIMIT =\n    buildStaticConf(\"clusteredTable.numClusteringColumnsLimit\")\n      .internal()\n      .doc(\"\"\"The maximum number of clustering columns allowed for a clustered table.\n        \"\"\".stripMargin)\n      .intConf\n      .checkValue(\n        _ > 0,\n        \"'clusteredTable.numClusteringColumnsLimit' must be positive.\"\n      )\n    .createWithDefault(4)\n\n  val DELTA_LOG_CACHE_SIZE = buildConf(\"delta.log.cacheSize\")\n    .internal()\n    .doc(\"The maximum number of DeltaLog instances to cache in memory.\")\n    .longConf\n    .createWithDefault(10000)\n\n  val DELTA_LOG_CACHE_RETENTION_MINUTES = buildConf(\"delta.log.cacheRetentionMinutes\")\n    .internal()\n    .doc(\"The rentention duration of DeltaLog instances in the cache\")\n    .timeConf(TimeUnit.MINUTES)\n    .createWithDefault(60)\n\n  //////////////////\n  // Delta Sharing\n  //////////////////\n\n  val DELTA_SHARING_ENABLE_DELTA_FORMAT_BATCH =\n    buildConf(\"spark.sql.delta.sharing.enableDeltaFormatBatch\")\n      .doc(\"Enable delta format sharing in case of issues.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_SHARING_FORCE_DELTA_FORMAT =\n    buildConf(\"spark.sql.delta.sharing.forceDeltaFormat\")\n      .doc(\"Force queries to use delta format when no responseFormat is specified.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  val DELTA_SHARING_STREAMING_AUTO_RESOLVE_RESPONSE_FORMAT =\n    buildConf(\"spark.sql.delta.sharing.streamingAutoResolveResponseFormat\")\n      .doc(\"When true, auto-resolve Delta Sharing streaming source format by calling getMetadata \" +\n        \"on the table and using the server's responded format (parquet or delta). When false, \" +\n        \"use the responseFormat option from the user.\")\n      .internal()\n      .booleanConf\n      .createWithDefault(false)\n\n  ///////////////////\n  // IDENTITY COLUMN\n  ///////////////////\n\n  val DELTA_IDENTITY_COLUMN_ENABLED =\n    buildConf(\"identityColumn.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          | The umbrella config to turn on/off the IDENTITY column support.\n          | If true, enable Delta IDENTITY column write support. If a table has an IDENTITY column,\n          | it is not writable but still readable if this config is set to false.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(true)\n\n  val DELTA_IDENTITY_ALLOW_SYNC_IDENTITY_TO_LOWER_HIGH_WATER_MARK =\n    buildConf(\"identityColumn.allowSyncIdentityToLowerHighWaterMark.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          | If true, the SYNC IDENTITY command can reduce the high water mark in a Delta IDENTITY\n          | column. If false, the high water mark will only be updated if it\n          | respects the column's specified start, step, and existing high watermark value.\n          |\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  ///////////\n  // VARIANT\n  ///////////////////\n  val FORCE_USE_PREVIEW_VARIANT_FEATURE = buildConf(\"variant.forceUsePreviewTableFeature\")\n    .internal()\n    .doc(\n      \"\"\"\n        | If true, creating new tables with variant columns only attaches the 'variantType-preview'\n        | table feature. Attempting to operate on existing tables created with the stable feature\n        | does not require that the preview table feature be present.\n        |\"\"\".stripMargin)\n    .booleanConf\n    .createWithDefault(false)\n\n  val FORCE_USE_PREVIEW_SHREDDING_FEATURE =\n    buildConf(\"variantShredding.forceUsePreviewTableFeature\")\n    .internal()\n    .doc(\n      \"\"\"\n        | If true, attach the 'variantShredding-preview' table feature when enabling shredding\n        | on a table. When false, the 'variantShredding' feature is used instead.\"\"\".stripMargin)\n    .booleanConf\n    .createWithDefault(true)\n\n  val COLLECT_VARIANT_DATA_SKIPPING_STATS =\n    buildConf(\"variantShredding.collectVariantDataSkippingStats\")\n    .internal()\n    .doc(\n      \"\"\"\n        | If enabled, Spark writes to Delta could collect data skipping stats for Variant\n        | columns. Currently, this config is used to ensure that new checkpoints preserve previous\n        | Variant stats.\"\"\"\n        .stripMargin)\n    .booleanConf\n    .createWithDefault(true)\n\n  ///////////\n  // TESTING\n  ///////////\n  val DELTA_POST_COMMIT_HOOK_THROW_ON_ERROR =\n    buildConf(\"postCommitHook.throwOnError\")\n      .internal()\n      .doc(\"If true, post-commit hooks will by default throw an exception when they fail.\")\n      .booleanConf\n      .createWithDefault(DeltaUtils.isTesting)\n\n  val TEST_FILE_NAME_PREFIX =\n    buildStaticConf(\"testOnly.dataFileNamePrefix\")\n      .internal()\n      .doc(\"[TEST_ONLY]: The prefix to use for the names of all Parquet data files.\")\n      .stringConf\n      .createWithDefault(if (DeltaUtils.isTesting) \"test%file%prefix-\" else \"\")\n\n  val TEST_DV_NAME_PREFIX =\n    buildStaticConf(\"testOnly.dvFileNamePrefix\")\n      .internal()\n      .doc(\"[TEST_ONLY]: The prefix to use for the names of all Deletion Vector files.\")\n      .stringConf\n      .createWithDefault(if (DeltaUtils.isTesting) \"test%dv%prefix-\" else \"\")\n\n  ///////////\n  // UTC TIMESTAMP PARTITION VALUES\n  ///////////////////\n  val UTC_TIMESTAMP_PARTITION_VALUES = buildConf(\"write.utcTimestampPartitionValues\")\n    .internal()\n    .doc(\n      \"\"\"\n        | If true, write UTC normalized timestamp partition values to Delta Log.\n        |\"\"\".stripMargin)\n    .booleanConf\n    .createWithDefault(true)\n\n  /////////////////////////////////////\n  // NORMALIZE PARTITION VALUES ON READ\n  ////////////////////////////////////\n\n  val DELTA_NORMALIZE_PARTITION_VALUES_ON_READ =\n    buildConf(\"normalizePartitionValuesOnRead\")\n      .internal()\n      .doc(\n        \"When true, we will normalize partition values on read by parsing them \" +\n        \"to their actual types for comparison instead of using raw strings. This helps prevent \" +\n        \"issues with inconsistently formatted partition values. \" +\n        \"UTC_TIMESTAMP_PARTITION_VALUES normalized timestamp partition values on write. However, \" +\n        \"data written before this flag existed may not be normalized and needs to be normalized \" +\n        \"on read.\"\n      )\n      .booleanConf\n      .createWithDefault(true)\n\n  //////////////////\n  // CORRECTNESS\n  //////////////////\n\n  val NUM_RECORDS_VALIDATION_ENABLED =\n    buildConf(\"numRecordsValidation.enabled\")\n      .internal()\n      .doc(\n        \"\"\"\n          |When enabled, adds a check to MERGE, UPDATE and DELETE that validates the number of\n          |records that were added and removed.\n          |\n          |- For MERGE without INSERT statements it checks that the number of records does not\n          |  increase.\n          |- For MERGE without DELETE statements it checks that the number of records does not\n          |  decrease.\n          |- For UPDATE statements it checks that the number of records does not change.\n          |- For DELETE statements it checks that the number of records does not increase.\n          |\n          |When disabled, we only log a warning.\n          |\"\"\".stripMargin\n      )\n      .booleanConf\n      .createWithDefault(true)\n\n\n  val COMMAND_INVARIANT_CHECKS_USE_UNRELIABLE =\n    buildConf(\"commandInvariantChecksUseUnreliable\")\n      .internal()\n      .doc(\"When enabled all DML commands will check and log invariants using unreliable metrics.\")\n      .booleanConf\n      .createWithDefault(true)\n\n  val COMMAND_INVARIANT_CHECKS_THROW =\n    buildConf(\"commandInvariantChecksThrow\")\n      .internal()\n      .doc(\n        \"\"\"When disabled all DML commands using reliable metrics just log a warning on command\n          |invariant violation and proceed to commit.\n          |When enabled, it's decided by a per-command flag.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  val ENABLE_SERVER_SIDE_PLANNING =\n    buildConf(\"catalog.enableServerSidePlanning\")\n      .internal()\n      .doc(\n        \"\"\"When enabled, DeltaCatalog will use server-side scan planning path\n          |instead of normal table loading.\"\"\".stripMargin)\n      .booleanConf\n      .createWithDefault(false)\n\n  /**\n   * Controls which connector implementation to use for Delta table operations.\n   *\n   * Valid values:\n   * - NONE: sparkV2 connector is disabled, always use sparkV1 connector (DeltaTableV2) - default\n   * - AUTO: Automatically use sparkV2 connector (SparkTable) for Unity Catalog managed tables\n   *         in streaming queries and sparkV1 connector (DeltaTableV2) for all other tables\n   * - STRICT: sparkV2 connector is strictly enforced, always use sparkV2 connector (SparkTable).\n   *           Intended for testing sparkV2 connector capabilities\n   *\n   * sparkV1 vs sparkV2 Connectors:\n   * - sparkV1 Connector (DeltaTableV2): Legacy Delta connector with full read/write support,\n   *   uses DeltaLog for metadata management\n   * - sparkV2 Connector (SparkTable): New kernel-based connector with read-only support,\n   *   uses Kernel's Table API for metadata management\n   *\n   * See [[org.apache.spark.sql.delta.DeltaV2Mode]] for the centralized logic that interprets\n   * this configuration.\n   */\n  val V2_ENABLE_MODE =\n    buildConf(\"v2.enableMode\")\n      .doc(\n        \"Controls the Delta connector enable mode. \" +\n          \"NONE (use v1 connector for all cases), AUTO (use v2 only for v2 \" +\n          \"supported operations, default), STRICT (should ONLY be enabled for testing).\")\n      .stringConf\n      .checkValues(Set(\"AUTO\", \"NONE\", \"STRICT\"))\n      .createWithDefault(\"AUTO\")\n\n  val DELTA_STREAMING_INITIAL_SNAPSHOT_MAX_FILES =\n    buildConf(\"streaming.initialSnapshotMaxFiles\")\n      .internal()\n      .doc(\"Maximum number of files allowed in initial snapshot for V2 streaming.\")\n      .intConf\n      .createWithDefault(100000)\n}\n\nobject DeltaSQLConf extends DeltaSQLConfBase\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSink.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport java.util.concurrent.ConcurrentHashMap\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaOperations.StreamingUpdate\nimport org.apache.spark.sql.delta.actions.{FileAction, Metadata, Protocol, SetTransaction}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.{ImplicitMetadataOperation, SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.AllowAutomaticWideningMode\nimport org.apache.spark.sql.delta.util.{Utils => DeltaUtils}\nimport org.apache.hadoop.fs.Path\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Alias, Expression}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, QuotingUtils}\nimport org.apache.spark.sql.execution.{QueryExecution, SQLExecution}\nimport org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}\nimport org.apache.spark.sql.execution.metric.SQLMetrics.createMetric\nimport org.apache.spark.sql.execution.streaming.Sink\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types.{ArrayType, DataType, MapType, StructType}\nimport org.apache.spark.util.Utils\n\n/**\n * A streaming sink that writes data into a Delta Table.\n */\ncase class DeltaSink(\n    sqlContext: SQLContext,\n    path: Path,\n    partitionColumns: Seq[String],\n    outputMode: OutputMode,\n    options: DeltaOptions,\n    catalogTable: Option[CatalogTable] = None)\n  extends Sink\n    with ImplicitMetadataOperation\n    with UpdateExpressionsSupport\n    with DeltaLogging {\n\n  private lazy val deltaLog = DeltaUtils.getDeltaLogFromTableOrPath(\n    sqlContext.sparkSession, catalogTable, path)\n\n  private val sqlConf = sqlContext.sparkSession.sessionState.conf\n\n  // This have to be lazy because queryId is a thread local property that is not available\n  // when the Sink object is created.\n  lazy val queryId = sqlContext.sparkContext.getLocalProperty(StreamExecution.QUERY_ID_KEY)\n\n  override protected val canOverwriteSchema: Boolean =\n    outputMode == OutputMode.Complete() && options.canOverwriteSchema\n\n  override protected val canMergeSchema: Boolean = options.canMergeSchema\n\n  case class PendingTxn(batchId: Long,\n                        optimisticTransaction: OptimisticTransaction,\n                        streamingUpdate: StreamingUpdate,\n                        newFiles: Seq[FileAction],\n                        deletedFiles: Seq[FileAction]) {\n    def commit(): Unit = {\n      val sc = sqlContext.sparkContext\n      val metrics = Map[String, SQLMetric](\n        \"numAddedFiles\" -> createMetric(sc, \"number of files added\"),\n        \"numRemovedFiles\" -> createMetric(sc, \"number of files removed\")\n      )\n      metrics(\"numRemovedFiles\").set(deletedFiles.size)\n      metrics(\"numAddedFiles\").set(newFiles.size)\n      optimisticTransaction.registerSQLMetrics(sqlContext.sparkSession, metrics)\n      val setTxn = SetTransaction(appId = queryId, version = batchId,\n        lastUpdated = Some(deltaLog.clock.getTimeMillis())) :: Nil\n      val (_, durationMs) = Utils.timeTakenMs {\n        optimisticTransaction\n          .commit(actions = setTxn ++ newFiles ++ deletedFiles\n            , op = streamingUpdate)\n      }\n      logInfo(\n        log\"Committed transaction, batchId=${MDC(DeltaLogKeys.BATCH_ID, batchId)}, \" +\n        log\"duration=${MDC(DeltaLogKeys.DURATION, durationMs)} ms, \" +\n        log\"added ${MDC(DeltaLogKeys.NUM_FILES, newFiles.size.toLong)} files, \" +\n        log\"removed ${MDC(DeltaLogKeys.NUM_FILES2, deletedFiles.size.toLong)} files.\")\n      val executionId = sc.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)\n      SQLMetrics.postDriverMetricUpdates(sc, executionId, metrics.values.toSeq)\n    }\n  }\n\n  override def addBatch(batchId: Long, data: DataFrame): Unit = {\n    addBatchWithStatusImpl(batchId, data)\n  }\n\n\n  private def addBatchWithStatusImpl(batchId: Long, data: DataFrame): Boolean = {\n    val txn = deltaLog.startTransaction(catalogTable)\n    assert(queryId != null)\n\n    if (SchemaUtils.nullTypeExistsRecursively(data.schema)) {\n      throw DeltaErrors.streamWriteNullTypeException\n    }\n\n    IdentityColumn.blockExplicitIdentityColumnInsert(\n      txn.snapshot.schema,\n      data.queryExecution.analyzed)\n\n    // If the batch reads the same Delta table as this sink is going to write to, then this\n    // write has dependencies. Then make sure that this commit set hasDependencies to true\n    // by injecting a read on the whole table. This needs to be done explicitly because\n    // MicroBatchExecution has already enforced all the data skipping (by forcing the generation\n    // of the executed plan) even before the transaction was started.\n    val selfScan = data.queryExecution.analyzed.collectFirst {\n      case DeltaTable(index) if index.deltaLog.isSameLogAs(txn.deltaLog) => true\n    }.nonEmpty\n    if (selfScan) {\n      txn.readWholeTable()\n    }\n\n    val writeSchema = getWriteSchema(txn.protocol, txn.metadata, data.schema)\n    // Streaming sinks can't blindly overwrite schema. See Schema Management design doc for details\n    updateMetadata(data.sparkSession, txn, writeSchema, partitionColumns, Map.empty,\n      outputMode == OutputMode.Complete(), rearrangeOnly = false)\n\n    val currentVersion = txn.txnVersion(queryId)\n    if (currentVersion >= batchId) {\n      logInfo(log\"Skipping already complete epoch ${MDC(DeltaLogKeys.BATCH_ID, batchId)}, \" +\n        log\"in query ${MDC(DeltaLogKeys.QUERY_ID, queryId)}\")\n      return false\n    }\n\n    val deletedFiles = outputMode match {\n      case o if o == OutputMode.Complete() =>\n        DeltaLog.assertRemovable(txn.snapshot)\n        txn.filterFiles().map(_.remove)\n      case _ => Nil\n    }\n    val (newFiles, writeFilesTimeMs) = Utils.timeTakenMs{\n      txn.writeFiles(castDataIfNeeded(data, writeSchema), Some(options))\n    }\n    val totalSize = newFiles.map(_.getFileSize).sum\n    val totalLogicalRecords = newFiles.map(_.numLogicalRecords.getOrElse(0L)).sum\n    logInfo(\n      log\"Wrote ${MDC(DeltaLogKeys.NUM_FILES, newFiles.size.toLong)} files, \" +\n        log\"with total size ${MDC(DeltaLogKeys.NUM_BYTES, totalSize)}, \" +\n      log\"${MDC(DeltaLogKeys.NUM_RECORDS, totalLogicalRecords)} logical records, \" +\n      log\"duration=${MDC(DeltaLogKeys.DURATION, writeFilesTimeMs)} ms.\")\n\n    val info = DeltaOperations.StreamingUpdate(outputMode, queryId, batchId, options.userMetadata\n                                               )\n    val pendingTxn = PendingTxn(batchId, txn, info, newFiles, deletedFiles)\n    pendingTxn.commit()\n    return true\n  }\n\n  /**\n   * Returns the schema to use to write data to this delta table. The write schema includes new\n   * columns to add with schema evolution and reconciles types to match the table types when\n   * possible or apply type widening if enabled.\n   */\n  private def getWriteSchema(\n      protocol: Protocol, metadata: Metadata, dataSchema: StructType): StructType = {\n    if (!sqlConf.getConf(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS)) return dataSchema\n\n    if (canOverwriteSchema) return dataSchema\n\n    val typeWideningMode = if (canMergeSchema && TypeWidening.isEnabled(protocol, metadata)) {\n        TypeWideningMode.TypeEvolution(\n          uniformIcebergCompatibleOnly = UniversalFormat.icebergEnabled(metadata),\n          allowAutomaticWidening = AllowAutomaticWideningMode.fromConf(sqlConf))\n      } else {\n        TypeWideningMode.NoTypeWidening\n      }\n    SchemaMergingUtils.mergeSchemas(\n      metadata.schema,\n      dataSchema,\n      allowImplicitConversions = true,\n      typeWideningMode = typeWideningMode\n    )\n  }\n\n  /** Casts columns in the given dataframe to match the target schema. */\n  private def castDataIfNeeded(data: DataFrame, targetSchema: StructType): DataFrame = {\n    if (!sqlConf.getConf(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS)) return data\n\n    // We should respect 'spark.sql.caseSensitive' here but writing to a Delta sink is currently\n    // case insensitive so we align with that.\n    val targetTypes =\n      CaseInsensitiveMap[DataType](targetSchema.map(field => field.name -> field.dataType).toMap)\n\n    val needCast =\n      if (sqlConf.getConf(DeltaSQLConf.DELTA_STREAMING_SINK_IMPLICIT_CAST_FOR_TYPE_MISMATCH_ONLY)) {\n        def hasTypeMismatch(from: DataType, to: DataType): Boolean = (from, to) match {\n          case (from: StructType, to: StructType) =>\n            val otherFields = SchemaMergingUtils.toFieldMap(to.fields, caseSensitive = false)\n            from.exists { field =>\n              otherFields.get(field.name) match {\n                case Some(other) => hasTypeMismatch(field.dataType, other.dataType)\n                // Ignore extra fields.\n                case None => false\n              }\n            }\n          case (from: MapType, to: MapType) =>\n            hasTypeMismatch(from.keyType, to.keyType) ||\n              hasTypeMismatch(from.valueType, to.valueType)\n          case (from: ArrayType, to: ArrayType) =>\n            hasTypeMismatch(from.elementType, to.elementType)\n          case (from, to) => from != to\n        }\n\n        hasTypeMismatch(data.schema, targetSchema)\n      } else {\n        // This will also return true if there are missing/extra nested fields or if nested fields\n        // are in a different order than in the table schema. We don't actually need implicit\n        // casting in these cases since Parquet will automatically fill missing fields with nulls\n        // and resolves fields by name.\n        data.schema.exists { field =>\n          !DataTypeUtils.equalsIgnoreCaseAndNullability(field.dataType, targetTypes(field.name))\n        }\n      }\n\n    if (!needCast) return data\n\n    def exprForColumn(df: DataFrame, columnName: String): Expression =\n      if (sqlConf.getConf(DeltaSQLConf.DELTA_STREAMING_SINK_IMPLICIT_CAST_ESCAPE_COLUMN_NAMES)) {\n        df.col(QuotingUtils.quoteIdentifier(columnName)).expr\n      } else {\n        df.col(columnName).expr\n      }\n\n    val castColumns = data.columns.map { columnName =>\n      val castExpr = castIfNeeded(\n        fromExpression = exprForColumn(data, columnName),\n        dataType = targetTypes(columnName),\n        castingBehavior = CastByName(allowMissingStructField = true),\n        columnName = columnName\n      )\n      Column(Alias(castExpr, columnName)())\n    }\n\n    data.queryExecution match {\n      case i: IncrementalExecution =>\n        DeltaStreamUtils.selectFromStreamingDataFrame(i, data, castColumns: _*)\n      case _: QueryExecution =>\n        data.select(castColumns: _*)\n    }\n  }\n\n  override def toString(): String = s\"DeltaSink[$path]\"\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSource.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.FileNotFoundException\nimport java.sql.Timestamp\n\nimport scala.util.{Failure, Success, Try}\nimport scala.util.control.NonFatal\nimport scala.util.matching.Regex\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.files.DeltaSourceSnapshot\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.storage.{ClosableIterator, SupportsRewinding}\nimport org.apache.spark.sql.delta.storage.ClosableIterator._\nimport org.apache.spark.sql.delta.util.{DateTimeUtils, TimestampFormatter}\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.fs.FileStatus\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{DataFrame, SparkSession}\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical.LocalRelation\nimport org.apache.spark.sql.connector.read.streaming\nimport org.apache.spark.sql.connector.read.streaming.{ReadAllAvailable, ReadLimit, ReadMaxFiles, SupportsAdmissionControl, SupportsTriggerAvailableNow}\nimport org.apache.spark.sql.execution.streaming._\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\n\n/**\n * A case class to help with `Dataset` operations regarding Offset indexing, representing AddFile\n * actions in a Delta log. For proper offset tracking (SC-19523), there are also special sentinel\n * values with negative index = [[DeltaSourceOffset.BASE_INDEX]] and add = null.\n *\n * This class is not designed to be persisted in offset logs or such.\n *\n * @param version The version of the Delta log containing this AddFile.\n * @param index The index of this AddFile in the Delta log.\n * @param add The AddFile.\n * @param remove The RemoveFile if any.\n * @param cdc the CDC File if any.\n * @param isLast A flag to indicate whether this is the last AddFile in the version. This is used\n *               to resolve an off-by-one issue in the streaming offset interface; once we've read\n *               to the end of a log version file, we check this flag to advance immediately to the\n *               next one in the persisted offset. Without this special case we would re-read the\n *               already completed log file.\n * @param shouldSkip A flag to indicate whether this IndexedFile should be skipped. Currently, we\n *                   skip processing an IndexedFile on no-op merges to avoid producing redundant\n *                   records.\n */\nprivate[delta] case class IndexedFile(\n    version: Long,\n    index: Long,\n    add: AddFile,\n    remove: RemoveFile = null,\n    cdc: AddCDCFile = null,\n    shouldSkip: Boolean = false) extends AdmittableFile {\n\n  require(Option(add).size + Option(remove).size + Option(cdc).size <= 1,\n    \"IndexedFile must have at most one of add, remove, or cdc\")\n\n  def getFileAction: FileAction = {\n    if (add != null) {\n      add\n    } else if (remove != null) {\n      remove\n    } else {\n      cdc\n    }\n  }\n\n  override def hasFileAction(): Boolean = {\n    getFileAction != null\n  }\n\n  override def getFileSize(): Long = {\n    if (add != null) {\n      add.size\n    } else if (remove != null) {\n      remove.size.getOrElse(0)\n    } else {\n      cdc.size\n    }\n  }\n}\n\n/**\n * Base trait for the Delta Source, that contains methods that deal with\n * getting changes from the delta log.\n */\ntrait DeltaSourceBase extends Source\n    with SupportsAdmissionControl\n    with SupportsTriggerAvailableNow\n    with DeltaLogging { self: DeltaSource =>\n\n  /**\n   * Configuration options for handling schema changes behavior. Controls unsafe operations like\n   * column mapping changes, partition column changes, nullability changes, and type widening.\n   */\n  protected lazy val schemaReadOptions: DeltaStreamUtils.SchemaReadOptions = {\n    val schemaReadOptions = DeltaStreamUtils.SchemaReadOptions.fromSparkSession(\n      spark = spark,\n      isStreamingFromColumnMappingTable =\n        snapshotAtSourceInit.metadata.columnMappingMode != NoMapping,\n      isTypeWideningSupportedInProtocol = TypeWidening.isSupported(snapshotAtSourceInit.protocol))\n    if (schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges) recordDeltaEvent(\n      deltaLog,\n      \"delta.unsafe.streaming.readOnColumnMappingSchemaChanges\"\n    )\n    schemaReadOptions\n  }\n\n  /**\n   * The persisted schema from the schema log that must be used to read data files in this Delta\n   * streaming source.\n   */\n  protected val persistedMetadataAtSourceInit: Option[PersistedMetadata] =\n    metadataTrackingLog.flatMap(_.getCurrentTrackedMetadata)\n\n  /**\n   * The read schema for this source during initialization, taking in account of SchemaLog.\n   */\n  protected val readSchemaAtSourceInit: StructType = readSnapshotDescriptor.metadata.schema\n\n  protected val readPartitionSchemaAtSourceInit: StructType =\n    readSnapshotDescriptor.metadata.partitionSchema\n\n  protected val readProtocolAtSourceInit: Protocol = readSnapshotDescriptor.protocol\n\n  protected val readConfigurationsAtSourceInit: Map[String, String] =\n    readSnapshotDescriptor.metadata.configuration\n\n  /**\n   * Create a snapshot descriptor, customizing its metadata using metadata tracking if necessary\n   */\n  protected lazy val readSnapshotDescriptor: SnapshotDescriptor =\n    persistedMetadataAtSourceInit.map { customMetadata =>\n      // Construct a snapshot descriptor with custom schema inline\n      new SnapshotDescriptor {\n        val deltaLog: DeltaLog = snapshotAtSourceInit.deltaLog\n        val metadata: Metadata =\n          snapshotAtSourceInit.metadata.copy(\n            schemaString = customMetadata.dataSchemaJson,\n            partitionColumns = customMetadata.partitionSchema.fieldNames,\n            // Copy the configurations so the correct file format can be constructed\n            configuration = customMetadata.tableConfigurations\n              // Fallback for backward compat only, this should technically not be triggered\n              .getOrElse {\n                val config = snapshotAtSourceInit.metadata.configuration\n                logWarning(log\"Using snapshot's table configuration: \" +\n                  log\"${MDC(DeltaLogKeys.CONFIG, config)}\")\n                config\n              }\n          )\n        val protocol: Protocol = customMetadata.protocol.getOrElse {\n          val protocol = snapshotAtSourceInit.protocol\n          logWarning(log\"Using snapshot's protocol: ${MDC(DeltaLogKeys.PROTOCOL, protocol)}\")\n          protocol\n        }\n        // The following are not important in stream reading\n        val version: Long = customMetadata.deltaCommitVersion\n        val numOfFilesIfKnown = snapshotAtSourceInit.numOfFilesIfKnown\n        val sizeInBytesIfKnown = snapshotAtSourceInit.sizeInBytesIfKnown\n      }\n    }.getOrElse(snapshotAtSourceInit)\n\n  /**\n   * A global flag to mark whether we have done a per-stream start check for column mapping\n   * schema changes (rename / drop).\n   */\n  @volatile protected var hasCheckedReadIncompatibleSchemaChangesOnStreamStart: Boolean = false\n\n  override val schema: StructType = {\n    val readSchemaWithCdc = if (options.readChangeFeed) {\n      CDCReader.cdcReadSchema(readSchemaAtSourceInit)\n    } else {\n      readSchemaAtSourceInit\n    }\n    DeltaTableUtils.removeInternalDeltaMetadata(\n      spark, DeltaTableUtils.removeInternalWriterMetadata(spark, readSchemaWithCdc))\n  }\n\n  // A dummy empty dataframe that can be returned at various point during streaming\n  protected val emptyDataFrame: DataFrame =\n    DataFrameUtils.ofRows(spark, LocalRelation(schema).copy(isStreaming = true))\n\n  /**\n   * When `AvailableNow` is used, this offset will be the upper bound where this run of the query\n   * will process up. We may run multiple micro batches, but the query will stop itself when it\n   * reaches this offset.\n   */\n  protected var lastOffsetForTriggerAvailableNow: Option[DeltaSourceOffset] = None\n\n  private var isLastOffsetForTriggerAvailableNowInitialized = false\n\n  private var isTriggerAvailableNow = false\n\n  override def prepareForTriggerAvailableNow(): Unit = {\n    logInfo(log\"The streaming query reports to use Trigger.AvailableNow.\")\n    isTriggerAvailableNow = true\n  }\n\n  /**\n   * initialize the internal states for AvailableNow if this method is called first time after\n   * `prepareForTriggerAvailableNow`.\n   */\n  protected def initForTriggerAvailableNowIfNeeded(\n    startOffsetOpt: Option[DeltaSourceOffset]): Unit = {\n    if (isTriggerAvailableNow && !isLastOffsetForTriggerAvailableNowInitialized) {\n      isLastOffsetForTriggerAvailableNowInitialized = true\n      initLastOffsetForTriggerAvailableNow(startOffsetOpt)\n    }\n  }\n\n  protected def initLastOffsetForTriggerAvailableNow(\n    startOffsetOpt: Option[DeltaSourceOffset]): Unit = {\n    val offset = latestOffsetInternal(startOffsetOpt, ReadLimit.allAvailable())\n    lastOffsetForTriggerAvailableNow = offset\n    lastOffsetForTriggerAvailableNow.foreach { lastOffset =>\n\n    logInfo(log\"lastOffset for Trigger.AvailableNow has set to \" +\n      log\"${MDC(DeltaLogKeys.OFFSET, lastOffset.json)}\")\n    }\n  }\n\n  /** An internal `latestOffsetInternal` to get the latest offset. */\n  protected def latestOffsetInternal(\n    startOffset: Option[DeltaSourceOffset], limit: ReadLimit): Option[DeltaSourceOffset]\n\n  protected def getFileChangesWithRateLimit(\n      fromVersion: Long,\n      fromIndex: Long,\n      isInitialSnapshot: Boolean,\n      limits: Option[DeltaSource.AdmissionLimits] = Some(DeltaSource.AdmissionLimits(options)))\n    : ClosableIterator[IndexedFile] = {\n    val iter = if (options.readChangeFeed) {\n      // In this CDC use case, we need to consider RemoveFile and AddCDCFiles when getting the\n      // offset.\n\n      // This method is only used to get the offset so we need to return an iterator of IndexedFile.\n      getFileChangesForCDC(fromVersion, fromIndex, isInitialSnapshot, limits, None).flatMap(_._2)\n        .toClosable\n    } else {\n      val changes = getFileChanges(fromVersion, fromIndex, isInitialSnapshot)\n\n      // Take each change until we've seen the configured number of addFiles. Some changes don't\n      // represent file additions; we retain them for offset tracking, but they don't count towards\n      // the maxFilesPerTrigger conf.\n      if (limits.isEmpty) {\n        changes\n      } else {\n        val admissionControl = limits.get\n        changes.withClose { it => it.takeWhile { admissionControl.admit(_) }\n        }\n      }\n    }\n    // Stop before any schema change barrier if detected.\n    stopIndexedFileIteratorAtSchemaChangeBarrier(iter)\n  }\n\n  /**\n   * get the changes from startVersion, startIndex to the end\n   * @param startVersion - calculated starting version\n   * @param startIndex - calculated starting index\n   * @param isInitialSnapshot - whether the stream has to return the initial snapshot or not\n   * @param endOffset - Offset that signifies the end of the stream.\n   * @return\n   */\n  protected def getFileChangesAndCreateDataFrame(\n      startVersion: Long,\n      startIndex: Long,\n      isInitialSnapshot: Boolean,\n      endOffset: DeltaSourceOffset): DataFrame = {\n    if (options.readChangeFeed) {\n      getCDCFileChangesAndCreateDataFrame(startVersion, startIndex, isInitialSnapshot, endOffset)\n    } else {\n      val fileActionsIter = getFileChanges(\n        startVersion,\n        startIndex,\n        isInitialSnapshot,\n        endOffset = Some(endOffset)\n      )\n      try {\n        val filteredIndexedFiles = fileActionsIter.filter { indexedFile =>\n          indexedFile.getFileAction != null &&\n            excludeRegex.forall(_.findFirstIn(indexedFile.getFileAction.path).isEmpty)\n        }\n\n        val (result, duration) = Utils.timeTakenMs {\n          createDataFrame(filteredIndexedFiles)\n        }\n        logInfo(log\"Getting dataFrame for delta_log_path=\" +\n          log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)} with \" +\n          log\"startVersion=${MDC(DeltaLogKeys.START_VERSION, startVersion)}, \" +\n          log\"startIndex=${MDC(DeltaLogKeys.START_INDEX, startIndex)}, \" +\n          log\"isInitialSnapshot=${MDC(DeltaLogKeys.IS_INIT_SNAPSHOT, isInitialSnapshot)}, \" +\n          log\"endOffset=${MDC(DeltaLogKeys.END_INDEX, endOffset)} took timeMs=\" +\n          log\"${MDC(DeltaLogKeys.DURATION, duration)} ms\")\n        result\n      } finally {\n        fileActionsIter.close()\n      }\n    }\n  }\n\n  /**\n   * Given an iterator of file actions, create a DataFrame representing the files added to a table\n   * Only AddFile actions will be used to create the DataFrame.\n   * @param indexedFiles actions iterator from which to generate the DataFrame.\n   */\n  protected def createDataFrame(indexedFiles: Iterator[IndexedFile]): DataFrame = {\n    val addFiles = indexedFiles\n      .filter(_.getFileAction.isInstanceOf[AddFile])\n      .toSeq\n    val hasDeletionVectors =\n      addFiles.exists(_.getFileAction.asInstanceOf[AddFile].deletionVector != null)\n    if (hasDeletionVectors) {\n      // Read AddFiles from different versions in different scans.\n      // This avoids an issue where we might read the same file with different deletion vectors in\n      // the same scan, which we cannot support as long we broadcast a map of DVs for lookup.\n      // This code can be removed once we can pass the DVs into the scan directly together with the\n      // AddFile/PartitionedFile entry.\n      addFiles\n        .groupBy(_.version)\n        .values\n        .map { addFilesList =>\n          deltaLog.createDataFrame(\n            readSnapshotDescriptor,\n            addFilesList.map(_.getFileAction.asInstanceOf[AddFile]),\n            isStreaming = true)\n        }\n        .reduceOption(_ union _)\n        .getOrElse {\n          // If we filtered out all the values before the groupBy, just return an empty DataFrame.\n          deltaLog.createDataFrame(\n            readSnapshotDescriptor,\n            Seq.empty[AddFile],\n            isStreaming = true)\n        }\n    } else {\n      deltaLog.createDataFrame(\n        readSnapshotDescriptor,\n        addFiles.map(_.getFileAction.asInstanceOf[AddFile]),\n        isStreaming = true)\n    }\n  }\n\n  /**\n   * Returns the offset that starts from a specific delta table version. This function is\n   * called when starting a new stream query.\n   *\n   * @param fromVersion The version of the delta table to calculate the offset from.\n   * @param isInitialSnapshot Whether the delta version is for the initial snapshot or not.\n   * @param limits Indicates how much data can be processed by a micro batch.\n   */\n  protected def getStartingOffsetFromSpecificDeltaVersion(\n      fromVersion: Long,\n      isInitialSnapshot: Boolean,\n      limits: Option[DeltaSource.AdmissionLimits]): Option[DeltaSourceOffset] = {\n    // Initialize schema tracking log if possible, no-op if already initialized\n    // This is one of the two places can initialize schema tracking.\n    // This case specifically handles when we have a fresh stream.\n    if (readyToInitializeMetadataTrackingEagerly) {\n      initializeMetadataTrackingAndExitStream(fromVersion)\n    }\n\n    val changes = getFileChangesWithRateLimit(\n      fromVersion,\n      fromIndex = DeltaSourceOffset.BASE_INDEX,\n      isInitialSnapshot = isInitialSnapshot,\n      limits)\n\n    val lastFileChange = DeltaSource.iteratorLast(changes)\n\n    if (lastFileChange.isEmpty) {\n      None\n    } else {\n      // Block latestOffset() from generating an invalid offset by proactively verifying\n      // incompatible schema changes under column mapping. See more details in the method doc.\n      checkReadIncompatibleSchemaChangeOnStreamStartOnce(fromVersion)\n      Some(DeltaSource.buildOffsetFromIndexedFile(\n        tableId,\n        lastFileChange.get.version,\n        lastFileChange.get.index,\n        fromVersion,\n        isInitialSnapshot))\n    }\n  }\n\n  /**\n   * Return the next offset when previous offset exists.\n   */\n  protected def getNextOffsetFromPreviousOffset(\n      previousOffset: DeltaSourceOffset,\n      limits: Option[DeltaSource.AdmissionLimits]): Option[DeltaSourceOffset] = {\n    if (trackingMetadataChange) {\n      getNextOffsetFromPreviousOffsetIfPendingSchemaChange(previousOffset) match {\n        case None =>\n        case updatedPreviousOffsetOpt =>\n          // Stop generating new offset if there were pending schema changes\n          return updatedPreviousOffsetOpt\n      }\n    }\n\n    val changes = getFileChangesWithRateLimit(\n      previousOffset.reservoirVersion,\n      previousOffset.index,\n      previousOffset.isInitialSnapshot,\n      limits)\n\n    val lastFileChange = DeltaSource.iteratorLast(changes)\n\n    if (lastFileChange.isEmpty) {\n      Some(previousOffset)\n    } else {\n      // Similarly, block latestOffset() from generating an invalid offset by proactively\n      // verifying incompatible schema changes under column mapping. See more details in the\n      // method scala doc.\n      checkReadIncompatibleSchemaChangeOnStreamStartOnce(previousOffset.reservoirVersion)\n      Some(DeltaSource.buildOffsetFromIndexedFile(\n        tableId,\n        lastFileChange.get.version,\n        lastFileChange.get.index,\n        previousOffset.reservoirVersion,\n        previousOffset.isInitialSnapshot))\n    }\n  }\n\n  /**\n   * Return the DataFrame between start and end offset.\n   */\n  protected def createDataFrameBetweenOffsets(\n      startVersion: Long,\n      startIndex: Long,\n      isInitialSnapshot: Boolean,\n      startOffsetOption: Option[DeltaSourceOffset],\n      endOffset: DeltaSourceOffset): DataFrame = {\n    getFileChangesAndCreateDataFrame(startVersion, startIndex, isInitialSnapshot, endOffset)\n  }\n\n  protected def cleanUpSnapshotResources(): Unit = {\n    if (initialState != null) {\n      initialState.close(unpersistSnapshot = initialStateVersion < snapshotAtSourceInit.version)\n      initialState = null\n    }\n  }\n\n  /**\n   * Check read-incompatible schema changes during stream (re)start so we could fail fast.\n   *\n   * This only needs to be called ONCE in the life cycle of a stream, either at the very first\n   * latestOffset, or the very first getBatch to make sure we have detected an incompatible\n   * schema change.\n   * Typically, the verifyStreamHygiene that was called maybe good enough to detect these\n   * schema changes, there may be cases that wouldn't work, e.g. consider this sequence:\n   * 1. User starts a new stream @ startingVersion 1\n   * 2. latestOffset is called before getBatch() because there was no previous commits so\n   * getBatch won't be called as a recovery mechanism.\n   * Suppose there's a single rename/drop/nullability change S during computing next offset, S\n   * would look exactly the same as the latest schema so verifyStreamHygiene would not work.\n   * 3. latestOffset would return this new offset cross the schema boundary.\n   *\n   * If a schema log is already initialized, we don't have to run the initialization nor schema\n   * checks any more.\n   *\n   * @param batchStartVersion Start version we want to verify read compatibility against\n   * @param batchEndVersionOpt Optionally, if we are checking against an existing constructed batch\n   *                           during streaming initialization, we would also like to verify all\n   *                           schema changes in between as well before we can lazily initialize the\n   *                           schema log if needed.\n   */\n  protected def checkReadIncompatibleSchemaChangeOnStreamStartOnce(\n      batchStartVersion: Long,\n      batchEndVersionOpt: Option[Long] = None): Unit = {\n    if (trackingMetadataChange) return\n    if (hasCheckedReadIncompatibleSchemaChangesOnStreamStart) return\n\n    lazy val (startVersionSnapshotOpt, errOpt) =\n      Try(deltaLog.getSnapshotAt(batchStartVersion, catalogTableOpt = catalogTableOpt)) match {\n        case Success(snapshot) => (Some(snapshot), None)\n        case Failure(exception) => (None, Some(exception))\n      }\n\n    // Cannot perfectly verify column mapping schema changes if we cannot compute a start snapshot.\n    if (!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&\n        schemaReadOptions.isStreamingFromColumnMappingTable && errOpt.isDefined) {\n      throw DeltaErrors.failedToGetSnapshotDuringColumnMappingStreamingReadCheck(errOpt.get)\n    }\n\n    // Perform schema check if we need to, considering all escape flags.\n    if (!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges ||\n        schemaReadOptions.typeWideningEnabled ||\n        !schemaReadOptions.\n            forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart) {\n      startVersionSnapshotOpt.foreach { snapshot =>\n        checkReadIncompatibleSchemaChanges(\n          snapshot.metadata,\n          snapshot.version,\n          batchStartVersion,\n          batchEndVersionOpt,\n          validatedDuringStreamStart = true\n        )\n        // If end version is defined (i.e. we have a pending batch), let's also eagerly check all\n        // intermediate schema changes against the stream read schema to capture corners cases such\n        // as rename and rename back.\n        for {\n          endVersion <- batchEndVersionOpt\n          (version, metadata) <- collectMetadataActions(batchStartVersion, endVersion)\n        } {\n          checkReadIncompatibleSchemaChanges(\n            metadata,\n            version,\n            batchStartVersion,\n            Some(endVersion),\n            validatedDuringStreamStart = true)\n        }\n      }\n    }\n\n    // Mark as checked\n    hasCheckedReadIncompatibleSchemaChangesOnStreamStart = true\n  }\n\n  /**\n   * Narrow waist to verify a metadata action for read-incompatible schema changes, specifically:\n   * 1. Any column mapping related schema changes (rename / drop) columns\n   * 2. Standard read-compatibility changes including:\n   *    a) No missing columns\n   *    b) No data type changes\n   *    c) No read-incompatible nullability changes\n   * If the check fails, we throw an exception to exit the stream.\n   * If lazy log initialization is required, we also run a one time scan to safely initialize the\n   * metadata tracking log upon any non-additive schema change failures.\n   * @param metadata Metadata that contains a potential schema change\n   * @param version Version for the metadata action\n   * @param validatedDuringStreamStart Whether this check is being done during stream start.\n   */\n  protected def checkReadIncompatibleSchemaChanges(\n      metadata: Metadata,\n      version: Long,\n      batchStartVersion: Long,\n      batchEndVersionOpt: Option[Long] = None,\n      validatedDuringStreamStart: Boolean = false): Unit = {\n    log.info(s\"checking read incompatibility with schema at version $version, \" +\n      s\"inside batch[$batchStartVersion, ${batchEndVersionOpt.getOrElse(\"latest\")}]\")\n\n    val (newMetadata, oldMetadata) = if (version < snapshotAtSourceInit.version) {\n      (snapshotAtSourceInit.metadata, metadata)\n    } else {\n      (metadata, snapshotAtSourceInit.metadata)\n    }\n\n    // Table ID has changed during streaming\n    if (newMetadata.id != oldMetadata.id) {\n      throw DeltaErrors.differentDeltaTableReadByStreamingSource(\n        newTableId = newMetadata.id, oldTableId = oldMetadata.id)\n    }\n\n    checkNonAdditiveSchemaChanges(oldMetadata, newMetadata, validatedDuringStreamStart)\n\n    // Other standard read compatibility changes\n    if (!validatedDuringStreamStart ||\n        !schemaReadOptions.\n            forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart) {\n\n      val schemaChange = if (options.readChangeFeed) {\n        CDCReader.cdcReadSchema(metadata.schema)\n      } else {\n        metadata.schema\n      }\n\n      // There is a schema change. All of files after this commit will use `schemaChange`. Hence, we\n      // check whether we can use `schema` (the fixed source schema we use in the same run of the\n      // query) to read these new files safely.\n      val backfilling = version < snapshotAtSourceInit.version\n      // Partition column change will be ignored if user enable the unsafe flag\n      val newPartitionColumns =\n        if (schemaReadOptions.allowUnsafeStreamingReadOnPartitionColumnChanges) Seq.empty\n        else newMetadata.partitionColumns\n      val oldPartitionColumns =\n        if (schemaReadOptions.allowUnsafeStreamingReadOnPartitionColumnChanges) Seq.empty\n        else oldMetadata.partitionColumns\n\n      val checkResult = DeltaStreamUtils.checkSchemaChangesWhenNoSchemaTracking(\n        schemaChange, schema,\n        newPartitionColumns, oldPartitionColumns,\n        backfilling,\n        schemaReadOptions)\n\n      if (!DeltaStreamUtils.SchemaCompatibilityResult.isCompatible(checkResult)) {\n        val isRetryable =\n          DeltaStreamUtils.SchemaCompatibilityResult.isRetryableIncompatible(checkResult)\n        recordDeltaEvent(\n          deltaLog,\n          \"delta.streaming.source.schemaChanged\",\n          data = Map(\n            \"currentVersion\" -> snapshotAtSourceInit.version,\n            \"newVersion\" -> version,\n            \"retryable\" -> isRetryable,\n            \"backfilling\" -> backfilling,\n            \"readChangeDataFeed\" -> options.readChangeFeed,\n            \"typeWideningEnabled\" -> schemaReadOptions.typeWideningEnabled,\n            \"enableSchemaTrackingForTypeWidening\" ->\n                schemaReadOptions.enableSchemaTrackingForTypeWidening,\n            \"containsWideningTypeChanges\" ->\n              TypeWidening.containsWideningTypeChanges(schema, schemaChange)\n          )\n        )\n\n        throw DeltaErrors.schemaChangedException(\n          schema,\n          schemaChange,\n          retryable = isRetryable,\n          Some(version),\n          includeStartingVersionOrTimestampMessage = options.containsStartingVersionOrTimestamp)\n      }\n    }\n  }\n\n  /**\n   * Checks for non-additive schema changes (column renames, drops, type widening) and blocks\n   * the stream by throwing an exception if detected.\n   *\n   * Blocks when type widening tracking is enabled and widening changes exist, or when column\n   * mapping changes (rename/drop) are detected, unless `allowUnsafeStreamingReadOnColumnMapping\n   * SchemaChanges` is enabled. Upon blocking, the error requests the user to provide a schema\n   * tracking location to enable schema tracking. On restart, users must acknowledge changes via\n   * reader options or SQL confs.\n   * See [[DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked]].\n   *\n   * Note: Should not be called when schema tracking is active (trackingMetadataChange = true).\n   *\n   * @param oldMetadata Previous metadata (typically from stream initialization)\n   * @param newMetadata New metadata with potential schema changes\n   * @param validatedDuringStreamStart Whether validating during stream start vs. execution,\n   *                                   which affects the error message.\n   * @throws DeltaAnalysisException if non-additive schema changes require blocking\n   */\n  private def checkNonAdditiveSchemaChanges(\n      oldMetadata: Metadata,\n      newMetadata: Metadata,\n      validatedDuringStreamStart: Boolean): Unit = {\n    val shouldTrackSchema: Boolean =\n      if (schemaReadOptions.typeWideningEnabled &&\n          schemaReadOptions.enableSchemaTrackingForTypeWidening &&\n        TypeWidening.containsWideningTypeChanges(oldMetadata.schema, newMetadata.schema)) {\n        // If schema tracking is enabled for type widening, we will detect widening type changes and\n        // block the stream until the user sets `allowSourceColumnTypeChange` - similar to handling\n        // DROP/RENAME for column mapping.\n        true\n      } else if (schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges) {\n        false\n      } else {\n        // Column mapping schema changes\n        assert(!trackingMetadataChange, \"should not check schema change while tracking it\")\n        !DeltaColumnMapping.hasNoColumnMappingSchemaChanges(newMetadata, oldMetadata,\n          schemaReadOptions.allowUnsafeStreamingReadOnPartitionColumnChanges)\n      }\n\n    if (shouldTrackSchema) {\n      throw DeltaErrors.blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges(\n        spark,\n        oldMetadata.schema,\n        newMetadata.schema,\n        detectedDuringStreaming = !validatedDuringStreamStart)\n    }\n  }\n}\n\n/**\n * A streaming source for a Delta table.\n *\n * When a new stream is started, delta starts by constructing a\n * [[org.apache.spark.sql.delta.Snapshot]] at\n * the current version of the table. This snapshot is broken up into batches until\n * all existing data has been processed. Subsequent processing is done by tailing\n * the change log looking for new data. This results in the streaming query returning\n * the same answer as a batch query that had processed the entire dataset at any given point.\n */\ncase class DeltaSource(\n    spark: SparkSession,\n    deltaLog: DeltaLog,\n    catalogTableOpt: Option[CatalogTable],\n    options: DeltaOptions,\n    snapshotAtSourceInit: SnapshotDescriptor,\n    metadataPath: String,\n    metadataTrackingLog: Option[DeltaSourceMetadataTrackingLog] = None,\n    filters: Seq[Expression] = Nil)\n  extends DeltaSourceBase\n  with DeltaSourceCDCSupport\n  with DeltaSourceMetadataEvolutionSupport {\n\n  private val shouldValidateOffsets =\n    spark.sessionState.conf.getConf(DeltaSQLConf.STREAMING_OFFSET_VALIDATION)\n\n  // Deprecated. Please use `skipChangeCommits` from now on.\n  private val ignoreFileDeletion = {\n    if (options.ignoreFileDeletion) {\n      logConsole(DeltaErrors.ignoreStreamingUpdatesAndDeletesWarning(spark))\n      recordDeltaEvent(deltaLog, \"delta.deprecation.ignoreFileDeletion\")\n    }\n    options.ignoreFileDeletion\n  }\n\n  /** A check on the source table that skips commits that contain removes from the\n   * set of files. */\n  private val skipChangeCommits = options.skipChangeCommits\n\n  protected val excludeRegex: Option[Regex] = options.excludeRegex\n\n  // This was checked before creating ReservoirSource\n  assert(schema.nonEmpty)\n\n  protected val tableId = snapshotAtSourceInit.metadata.id\n\n  // A metadata snapshot when starting the query.\n  protected var initialState: DeltaSourceSnapshot = null\n  protected var initialStateVersion: Long = -1L\n\n  logInfo(log\"Filters being pushed down: ${MDC(DeltaLogKeys.FILTER, filters)}\")\n\n  /**\n   * Get the changes starting from (startVersion, startIndex). The start point should not be\n   * included in the result.\n   *\n   * @param endOffset If defined, do not return changes beyond this offset.\n   *                  If not defined, we must be scanning the log to find the next offset.\n   * @param verifyMetadataAction If true, we will break the stream when we detect any\n   *                             read-incompatible metadata changes.\n   */\n  protected def getFileChanges(\n      fromVersion: Long,\n      fromIndex: Long,\n      isInitialSnapshot: Boolean,\n      endOffset: Option[DeltaSourceOffset] = None,\n      verifyMetadataAction: Boolean = true\n  ): ClosableIterator[IndexedFile] = {\n\n    /** Returns matching files that were added on or after startVersion among delta logs. */\n    def filterAndIndexDeltaLogs(startVersion: Long): ClosableIterator[IndexedFile] = {\n      // TODO: handle the case when failOnDataLoss = false and we are missing change log files\n      //    in that case, we need to recompute the start snapshot and evolve the schema if needed\n      require(options.failOnDataLoss || !trackingMetadataChange,\n        \"Using schema from schema tracking log cannot tolerate missing commit files.\")\n      deltaLog.getChangeLogFiles(\n        startVersion, catalogTableOpt, options.failOnDataLoss).flatMapWithClose {\n        case (version, filestatus) =>\n          // First pass reads the whole commit and closes the iterator.\n          val iter = DeltaSource.createRewindableActionIterator(spark, deltaLog, filestatus)\n          val (shouldSkipCommit, metadataOpt, protocolOpt) = iter\n            .processAndClose { actionsIter =>\n              validateCommitAndDecideSkipping(\n                actionsIter, version,\n                fromVersion, endOffset,\n                verifyMetadataAction && !trackingMetadataChange\n              )\n            }\n          // Rewind the iterator to the beginning, if the actions are cached in memory, they will\n          // be reused again.\n          iter.rewind()\n          // Second pass reads the commit lazily.\n          iter.withClose { actionsIter =>\n            filterAndGetIndexedFiles(\n              actionsIter, version, shouldSkipCommit, metadataOpt, protocolOpt)\n          }\n      }\n    }\n\n    val (result, duration) = Utils.timeTakenMs {\n      var iter = if (isInitialSnapshot) {\n        Iterator(1, 2).flatMapWithClose { // so that the filterAndIndexDeltaLogs call is lazy\n          case 1 => getSnapshotAt(fromVersion)._1.toClosable\n          case 2 => filterAndIndexDeltaLogs(fromVersion + 1)\n        }\n      } else {\n        filterAndIndexDeltaLogs(fromVersion)\n      }\n\n      iter = iter.withClose { it =>\n        it.filter { file =>\n          file.version > fromVersion || file.index > fromIndex\n        }\n      }\n\n      // If endOffset is provided, we are getting a batch on a constructed range so we should use\n      // the endOffset as the limit.\n      // Otherwise, we are looking for a new offset, so we try to use the latestOffset we found for\n      // Trigger.availableNow() as limit. We know endOffset <= lastOffsetForTriggerAvailableNow.\n        val lastOffsetForThisScan = endOffset.orElse(lastOffsetForTriggerAvailableNow)\n\n        lastOffsetForThisScan.foreach { bound =>\n          iter = iter.withClose { it =>\n            it.takeWhile { file =>\n              file.version < bound.reservoirVersion ||\n                (file.version == bound.reservoirVersion && file.index <= bound.index)\n            }\n          }\n        }\n      iter\n    }\n    logInfo(log\"Getting file changes for delta_log_path=\" +\n      log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)} with \" +\n      log\"fromVersion=${MDC(DeltaLogKeys.START_VERSION, fromVersion)}, \" +\n      log\"fromIndex=${MDC(DeltaLogKeys.START_INDEX, fromIndex)}, \" +\n      log\"isInitialSnapshot=${MDC(DeltaLogKeys.IS_INIT_SNAPSHOT, isInitialSnapshot)} \" +\n      log\"took timeMs=${MDC(DeltaLogKeys.DURATION, duration)} ms\")\n    result\n  }\n\n  /**\n   * Adds dummy BEGIN_INDEX and END_INDEX IndexedFiles for @version before and after the\n   * contents of the iterator. The contents of the iterator must be the IndexedFiles that correspond\n   * to this version.\n   */\n  protected def addBeginAndEndIndexOffsetsForVersion(\n      version: Long, iterator: Iterator[IndexedFile]): Iterator[IndexedFile] = {\n    Iterator.single(IndexedFile(version, DeltaSourceOffset.BASE_INDEX, add = null)) ++\n      iterator ++\n      Iterator.single(IndexedFile(version, DeltaSourceOffset.END_INDEX, add = null))\n  }\n\n  /**\n   * This method computes the initial snapshot to read when Delta Source was initialized on a fresh\n   * stream.\n   * @return A tuple where the first element is an iterator of IndexedFiles and the second element\n   *         is the in-commit timestamp of the initial snapshot if available.\n   */\n  protected def getSnapshotAt(version: Long): (Iterator[IndexedFile], Option[Long]) = {\n    if (initialState == null || version != initialStateVersion) {\n      super[DeltaSourceBase].cleanUpSnapshotResources()\n      val snapshot = getSnapshotFromDeltaLog(version)\n\n      initialState = new DeltaSourceSnapshot(spark, snapshot, filters)\n      initialStateVersion = version\n\n      // This handle a special case for schema tracking log when it's initialized but the initial\n      // snapshot's schema has changed, suppose:\n      // 1. The stream starts and looks at the initial snapshot to compute the starting offset, say\n      //    at version 0 with schema <a>\n      // 2. User renames a column, creates version 1 with schema <b>\n      // 3. The read compatibility check fails during scanning version 1, initializes schema log\n      //    using the initial snapshot's schema (<a>, because that's the safest thing to do as we\n      //    have not served any data from initial snapshot yet) and exits stream.\n      // 4. Stream restarts, since no starting offset was generated, it will retry loading the\n      //    initial snapshot, which is now at version 1, but the tracked schema <a> is now different\n      //    from the \"new\" initial snapshot schema! Worse, since schema tracking ignores any schema\n      //    changes inside initial snapshot, we will then be reading the files using a wrong schema!\n      // The below logic allows us to detect any discrepancies when reading initial snapshot using\n      // a tracked schema, and reinitialize the log if needed.\n      if (trackingMetadataChange &&\n          initialState.snapshot.version >= readSnapshotDescriptor.version) {\n        updateMetadataTrackingLogAndFailTheStreamIfNeeded(\n          Some(initialState.snapshot.metadata),\n          Some(initialState.snapshot.protocol),\n          initialState.snapshot.version,\n          // The new schema should replace the previous initialized schema for initial snapshot\n          replace = true\n        )\n      }\n    }\n    val inCommitTimestampOpt =\n      Option.when(\n          DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(initialState.snapshot.metadata)) {\n        initialState.snapshot.timestamp\n      }\n    (addBeginAndEndIndexOffsetsForVersion(version, initialState.iterator()), inCommitTimestampOpt)\n  }\n\n  /**\n   * Narrow-waist for generating snapshot from Delta Log within Delta Source\n   */\n  protected def getSnapshotFromDeltaLog(version: Long): Snapshot = {\n    try {\n      deltaLog.getSnapshotAt(version, catalogTableOpt = catalogTableOpt)\n    } catch {\n      case e: FileNotFoundException =>\n        throw DeltaErrors.logFileNotFoundExceptionForStreamingSource(e)\n    }\n  }\n\n  private def getStartingOffset(\n      limits: Option[DeltaSource.AdmissionLimits]): Option[DeltaSourceOffset] = {\n\n    val (version, isInitialSnapshot) = getStartingVersion match {\n      case Some(v) => (v, false)\n      case None => (snapshotAtSourceInit.version, true)\n    }\n    if (version < 0) {\n      return None\n    }\n\n    getStartingOffsetFromSpecificDeltaVersion(version, isInitialSnapshot, limits)\n  }\n\n  override def getDefaultReadLimit: ReadLimit = {\n    DeltaSource.AdmissionLimits.toReadLimit(options)\n  }\n\n  def toDeltaSourceOffset(offset: streaming.Offset): DeltaSourceOffset = {\n    DeltaSourceOffset(tableId, offset)\n  }\n\n  /**\n   * This should only be called by the engine. Call `latestOffsetInternal` instead if you need to\n   * get the latest offset.\n   */\n  override def latestOffset(startOffset: streaming.Offset, limit: ReadLimit): streaming.Offset =\n    recordDeltaOperation(\n      snapshotAtSourceInit.deltaLog, opType = \"delta.streaming.source.latestOffset\") {\n    val deltaStartOffset = Option(startOffset).map(toDeltaSourceOffset)\n    initForTriggerAvailableNowIfNeeded(deltaStartOffset)\n    latestOffsetInternal(deltaStartOffset, limit).orNull\n  }\n\n  override protected def latestOffsetInternal(\n    startOffset: Option[DeltaSourceOffset], limit: ReadLimit): Option[DeltaSourceOffset] = {\n    val limits = DeltaSource.AdmissionLimits(options, limit)\n\n    val endOffset = startOffset.map(getNextOffsetFromPreviousOffset(_, limits))\n      .getOrElse(getStartingOffset(limits))\n\n    val startVersion = startOffset.map(_.reservoirVersion).getOrElse(-1L)\n    val endVersion = endOffset.map(_.reservoirVersion).getOrElse(-1L)\n    lazy val offsetRangeInfo = \"(latestOffsetInternal)startOffset -> endOffset:\" +\n      s\" $startOffset -> $endOffset\"\n    if (endVersion - startVersion > 1000L) {\n      // Improve the log level if the source is processing a large batch.\n      logInfo(offsetRangeInfo)\n    } else {\n      logDebug(offsetRangeInfo)\n    }\n    if (shouldValidateOffsets && startOffset.isDefined) {\n      endOffset.foreach { endOffset =>\n        DeltaSourceOffset.validateOffsets(startOffset.get, endOffset)\n      }\n    }\n    endOffset\n  }\n\n  override def getOffset: Option[Offset] = {\n    throw new UnsupportedOperationException(\n      \"latestOffset(Offset, ReadLimit) should be called instead of this method\")\n  }\n\n  /**\n   * Filter the iterator with only add files that contain data change and get indexed files.\n   * @return indexed add files\n   */\n  private def filterAndGetIndexedFiles(\n      iterator: Iterator[Action],\n      version: Long,\n      shouldSkipCommit: Boolean,\n      metadataOpt: Option[Metadata],\n      protocolOpt: Option[Protocol]): Iterator[IndexedFile] = {\n    val filteredIterator =\n      if (shouldSkipCommit) {\n        Iterator.empty\n      } else {\n        iterator.collect { case a: AddFile if a.dataChange => a }\n      }\n\n    var index = -1L\n    val indexedFiles = new Iterator[IndexedFile] {\n      override def hasNext: Boolean = filteredIterator.hasNext\n      override def next(): IndexedFile = {\n        index += 1 // pre-increment the index (so it starts from 0)\n        val add = filteredIterator.next().copy(stats = null)\n        IndexedFile(version, index, add)\n      }\n    }\n    addBeginAndEndIndexOffsetsForVersion(\n      version,\n      getMetadataOrProtocolChangeIndexedFileIterator(metadataOpt, protocolOpt, version) ++\n        indexedFiles)\n  }\n\n  /**\n   * Check stream for violating any constraints.\n   *\n   * If verifyMetadataAction = true, we will break the stream when we detect any read-incompatible\n   * metadata changes.\n   *\n   * @return (true if commit should be skipped, a metadata action if found)\n   */\n  protected def validateCommitAndDecideSkipping(\n      actions: Iterator[Action],\n      version: Long,\n      batchStartVersion: Long,\n      batchEndOffsetOpt: Option[DeltaSourceOffset] = None,\n      verifyMetadataAction: Boolean = true\n  ): (Boolean, Option[Metadata], Option[Protocol]) = {\n    // If the batch end is at the beginning of this exact version, then we actually stop reading\n    // just _before_ this version. So then we can ignore the version contents entirely.\n    if (batchEndOffsetOpt.exists(end =>\n      end.reservoirVersion == version && end.index == DeltaSourceOffset.BASE_INDEX)) {\n      return (false, None, None)\n    }\n\n    /** A check on the source table that disallows changes on the source data. */\n    val shouldAllowChanges = options.ignoreChanges || ignoreFileDeletion || skipChangeCommits\n    /** A check on the source table that disallows commits that only include deletes to the data. */\n    val shouldAllowDeletes = shouldAllowChanges || options.ignoreDeletes || ignoreFileDeletion\n\n    var seenFileAdd = false\n    var skippedCommit = false\n    var metadataAction: Option[Metadata] = None\n    var protocolAction: Option[Protocol] = None\n    var removeFileActionPath: Option[String] = None\n    var operation: Option[String] = None\n    actions.foreach {\n      case a: AddFile if a.dataChange =>\n        seenFileAdd = true\n      case r: RemoveFile if r.dataChange =>\n        skippedCommit = skipChangeCommits\n        if (removeFileActionPath.isEmpty) {\n          removeFileActionPath = Some(r.path)\n        }\n      case m: Metadata =>\n        if (verifyMetadataAction) {\n          checkReadIncompatibleSchemaChanges(\n            m, version, batchStartVersion, batchEndOffsetOpt.map(_.reservoirVersion))\n        }\n        assert(metadataAction.isEmpty,\n          \"Should not encounter two metadata actions in the same commit\")\n        metadataAction = Some(m)\n      case protocol: Protocol =>\n        deltaLog.protocolRead(protocol)\n        assert(protocolAction.isEmpty,\n          \"Should not encounter two protocol actions in the same commit\")\n        protocolAction = Some(protocol)\n      case commitInfo: CommitInfo =>\n        operation = Some(s\"${commitInfo.operation} (${commitInfo.operationParameters})\")\n      case _ => ()\n    }\n    if (removeFileActionPath.isDefined) {\n      if (seenFileAdd && !shouldAllowChanges) {\n        throw DeltaErrors.deltaSourceIgnoreChangesError(\n          version,\n          if (operation.nonEmpty) operation.get else removeFileActionPath.get,\n          deltaLog.dataPath.toString\n        )\n      } else if (!seenFileAdd && !shouldAllowDeletes) {\n        throw DeltaErrors.deltaSourceIgnoreDeleteError(\n          version,\n          removeFileActionPath.get,\n          deltaLog.dataPath.toString\n        )\n      }\n    }\n    (skippedCommit, metadataAction, protocolAction)\n  }\n\n  override def getBatch(startOffsetOption: Option[Offset], end: Offset): DataFrame =\n    recordDeltaOperation(\n      snapshotAtSourceInit.deltaLog, opType = \"delta.streaming.source.getBatch\") {\n    val endOffset = toDeltaSourceOffset(end)\n    val startDeltaOffsetOption = startOffsetOption.map(toDeltaSourceOffset)\n\n    val (startVersion, startIndex, isInitialSnapshot) =\n      extractStartingState(startDeltaOffsetOption, endOffset)\n\n    if (startOffsetOption.contains(endOffset)) {\n      // This happens only if we recover from a failure and `MicroBatchExecution` tries to call\n      // us with the previous offsets. The returned DataFrame will be dropped immediately, so we\n      // can return any DataFrame.\n      return emptyDataFrame\n    }\n\n    val offsetRangeInfo = s\"(getBatch)start: $startDeltaOffsetOption end: $end\"\n    if (endOffset.reservoirVersion - startVersion > 1000L) {\n      // Improve the log level if the source is processing a large batch.\n      logInfo(offsetRangeInfo)\n    } else {\n      logDebug(offsetRangeInfo)\n    }\n\n    // Initialize schema tracking log if possible, no-op if already initialized.\n    // This is one of the two places can initialize schema tracking.\n    // This case specifically handles initialization when we are already working with an initialized\n    // stream.\n    // Here we may have two conditions:\n    // 1. We are dealing with the recovery getBatch() that gives us the previous committed offset\n    // where start and end corresponds to the previous batch.\n    // In this case, we should initialize the schema at the previous committed offset (endOffset),\n    // which can be done using the same `initializeMetadataTrackingAndExitStream` method.\n    // This also means we are caught up with the stream and we can start schema tracking in the\n    // next latestOffset call.\n    // 2. We are running an already-constructed batch, we need the schema to be compatible\n    // with the entire batch, so we also pass the batch end offset. The schema tracking log will\n    // only be initialized if there exists a consistent read schema for the entire batch. If such\n    // a consistent schema does not exist, the stream will be broken. This case will be rare: it can\n    // only happen for streams where the schema tracking log was added after the stream has already\n    // been running, *and* the stream was running on an older version of the DeltaSource that did\n    // not detect non-additive schema changes, *and* it was stopped while processing a batch that\n    // contained such a schema change.\n    // In either world, the initialization logic would find the superset compatible schema for this\n    // batch by scanning Delta log.\n    validateAndInitMetadataLogForPlannedBatchesDuringStreamStart(startVersion, endOffset)\n\n    val createdDf = createDataFrameBetweenOffsets(\n      startVersion, startIndex, isInitialSnapshot, startDeltaOffsetOption, endOffset)\n\n    createdDf\n  }\n\n  /**\n   * Extracts the start state for a scan given an optional start offset and an end offset, so we\n   * know exactly where we should scan from for a batch end at the `endOffset`, invoked when:\n   *\n   * 1. We are in `getBatch` given a startOffsetOption and endOffset from streaming engine.\n   * 2. We are in the `init` method for every stream (re)start given a start offset for all pending\n   *    batches and the latest planned offset, and trying to figure out if this range contains any\n   *    non-additive schema changes.\n   *\n   * @param startOffsetOption Optional start offset, if not defined. This means we are trying to\n   *                          scan the very first batch where endOffset is the very first offset\n   *                          generated by `latestOffsets`, specifically `getStartingOffset`\n   * @param endOffset The end offset for a batch.\n   * @return (start commit version to scan from,\n   *         start offset index to scan from,\n   *         whether this version is part of the initial snapshot)\n   */\n  private def extractStartingState(\n      startOffsetOption: Option[DeltaSourceOffset],\n      endOffset: DeltaSourceOffset): (Long, Long, Boolean) = {\n    val (startVersion, startIndex, isInitialSnapshot) = if (startOffsetOption.isEmpty) {\n      getStartingVersion match {\n        case Some(v) =>\n          (v, DeltaSourceOffset.BASE_INDEX, false)\n\n        case None =>\n          if (endOffset.isInitialSnapshot) {\n            (endOffset.reservoirVersion, DeltaSourceOffset.BASE_INDEX, true)\n          } else {\n            assert(\n              endOffset.reservoirVersion > 0, s\"invalid reservoirVersion in endOffset: $endOffset\")\n            // Load from snapshot `endOffset.reservoirVersion - 1L` so that `index` in `endOffset`\n            // is still valid.\n            // It's OK to use the previous version as the updated initial snapshot, even if the\n            // initial snapshot might have been different from the last time when this starting\n            // offset was computed.\n            (endOffset.reservoirVersion - 1L, DeltaSourceOffset.BASE_INDEX, true)\n          }\n      }\n    } else {\n      val startOffset = startOffsetOption.get\n      if (!startOffset.isInitialSnapshot) {\n        // unpersist `snapshot` because it won't be used any more.\n        cleanUpSnapshotResources()\n      }\n      (startOffset.reservoirVersion, startOffset.index, startOffset.isInitialSnapshot)\n    }\n    (startVersion, startIndex, isInitialSnapshot)\n  }\n\n  /**\n   * Centralized place for validating and initializing schema log for all pending batch(es).\n   * This is called only during stream start.\n   *\n   * @param startVersion Start version of the pending batch range\n   * @param endOffset End offset for the pending batch range. end offset >= start offset\n   */\n  private def validateAndInitMetadataLogForPlannedBatchesDuringStreamStart(\n      startVersion: Long,\n      endOffset: DeltaSourceOffset): Unit = {\n    // We don't have to include the end reservoir version when the end offset is a base index, i.e.\n    // no data commit has been marked within a constructed batch, we can simply ignore end offset\n    // version. This can help us avoid overblocking a potential ending offset right at a schema\n    // change.\n    val endVersionForMetadataLogInit = if (endOffset.index == DeltaSourceOffset.BASE_INDEX) {\n      endOffset.reservoirVersion - 1\n    } else {\n      endOffset.reservoirVersion\n    }\n    // For eager initialization, we initialize the log right now.\n    if (readyToInitializeMetadataTrackingEagerly) {\n      initializeMetadataTrackingAndExitStream(startVersion, Some(endVersionForMetadataLogInit))\n    }\n\n    // Check for column mapping + streaming incompatible schema changes\n    // Note for initial snapshot, the startVersion should be the same as the latestOffset's\n    // version and therefore this check won't have any effect.\n    // This method would also handle read-compatibility checks against the pending batch(es)\n    // as well as lazy metadata log initialization.\n    checkReadIncompatibleSchemaChangeOnStreamStartOnce(\n      startVersion,\n      Some(endVersionForMetadataLogInit)\n    )\n  }\n\n  override def stop(): Unit = {\n    cleanUpSnapshotResources()\n  }\n\n  // Marks that the `end` offset is done and we can safely run any actions in response to that.\n  // This happens AFTER `end` offset is committed by the streaming engine so we can safely fail this\n  // if needed, e.g. for failing the stream to conduct schema evolution.\n  override def commit(end: Offset): Unit =\n    recordDeltaOperation(snapshotAtSourceInit.deltaLog, opType = \"delta.streaming.source.commit\") {\n    super.commit(end)\n    // IMPORTANT: for future developers, please place any work you would like to do in commit()\n    // before `updateSchemaTrackingLogAndFailTheStreamIfNeeded(end)` as it may throw an exception.\n    updateMetadataTrackingLogAndFailTheStreamIfNeeded(end)\n  }\n\n  override def toString(): String = s\"DeltaSource[${deltaLog.dataPath}]\"\n\n  /**\n   * Extracts whether users provided the option to time travel a relation. If a query restarts from\n   * a checkpoint and the checkpoint has recorded the offset, this method should never been called.\n   */\n  protected lazy val getStartingVersion: Option[Long] = {\n    // Note: returning a version beyond latest snapshot version won't be a problem as callers\n    // of this function won't use the version to retrieve snapshot(refer to [[getStartingOffset]]).\n    val allowOutOfRange =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP)\n    /** DeltaOption validates input and ensures that only one is provided. */\n    if (options.startingVersion.isDefined) {\n      val v = options.startingVersion.get match {\n        case StartingVersionLatest =>\n          deltaLog.update(catalogTableOpt = catalogTableOpt).version + 1\n        case StartingVersion(version) =>\n          if (!DeltaSource.validateProtocolAt(spark, deltaLog, catalogTableOpt, version)) {\n            // When starting from a given version, we don't require that the snapshot of this\n            // version can be reconstructed, even though the input table is technically in an\n            // inconsistent state. If the snapshot cannot be reconstructed, then the protocol\n            // check is skipped, so this is technically not safe, but we keep it this way for\n            // historical reasons.\n            deltaLog.history.checkVersionExists(\n              version, catalogTableOpt = None, mustBeRecreatable = false, allowOutOfRange)\n          }\n          version\n      }\n      Some(v)\n    } else if (options.startingTimestamp.isDefined) {\n      val tt: DeltaTimeTravelSpec = DeltaTimeTravelSpec(\n        timestamp = options.startingTimestamp.map(Literal(_)),\n        version = None,\n        creationSource = Some(\"deltaSource\"))\n      Some(DeltaSource\n        .getStartingVersionFromTimestamp(\n          spark,\n          deltaLog,\n          catalogTableOpt,\n          tt.getTimestamp(spark.sessionState.conf),\n          allowOutOfRange))\n    } else {\n      None\n    }\n  }\n}\n\nobject DeltaSource extends DeltaLogging {\n\n  trait DeltaSourceAdmissionBase { self: AdmissionLimits =>\n    // This variable indicates whether a commit has already been processed by a batch or not.\n    var commitProcessedInBatch = false\n\n    protected def take(files: Int, bytes: Long): Unit = {\n      filesToTake -= files\n      bytesToTake -= bytes\n    }\n\n    /**\n     * This overloaded method checks if all the FileActions for a commit can be accommodated by\n     * the rate limit.\n     */\n    def admit(admittableFiles: Seq[AdmittableFile]): Boolean = {\n      def getSize(actions: Seq[AdmittableFile]): Long = {\n        actions.filter(_.hasFileAction).foldLeft(0L) { (l, r) => l + r.getFileSize }\n      }\n      if (admittableFiles.isEmpty) {\n        true\n      } else {\n        // if no files have been admitted, then admit all to avoid deadlock\n        // else check if all of the files together satisfy the limit, only then admit\n        val bytesInFiles = getSize(admittableFiles)\n        val shouldAdmit = !commitProcessedInBatch ||\n          (filesToTake - admittableFiles.size >= 0 && bytesToTake - bytesInFiles >= 0)\n\n        commitProcessedInBatch = true\n        take(files = admittableFiles.size, bytes = bytesInFiles)\n        shouldAdmit\n      }\n    }\n\n    /**\n     * Whether to admit the next file. Dummy IndexedFile entries with no attached file action are\n     * always admitted.\n     */\n    def admit(admittableFile: AdmittableFile): Boolean = {\n      commitProcessedInBatch = true\n\n      if (!admittableFile.hasFileAction) {\n        // Don't count placeholders. They are not files. If we have empty commits, then we should\n        // not count the placeholders as files, or else we'll end up with under-filled batches.\n        return true\n      }\n\n      // We always admit a file if we still have capacity _before_ we take it. This ensures that we\n      // will even admit a file when it is larger than the remaining capacity, and that we will\n      // admit at least one file.\n      val shouldAdmit = hasCapacity\n      take(files = 1, bytes = admittableFile.getFileSize)\n      shouldAdmit\n    }\n\n    /** Returns whether admission limits has capacity to accept files or bytes */\n    def hasCapacity: Boolean = {\n      filesToTake > 0 && bytesToTake > 0\n    }\n\n  }\n\n  /**\n   * Class that helps controlling how much data should be processed by a single micro-batch.\n   */\n  case class AdmissionLimits(\n    options: DeltaOptions,\n    maxFiles: Option[Int] = None,\n    maxBytes: Option[Long] = None\n  ) extends DeltaSourceAdmissionBase {\n    var bytesToTake = maxBytes.getOrElse(options.maxBytesPerTrigger.getOrElse(Long.MaxValue))\n    var filesToTake = maxFiles.getOrElse {\n      if (options.maxBytesPerTrigger.isEmpty) {\n        DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION_DEFAULT\n      } else {\n        Int.MaxValue - 8 // - 8 to prevent JVM Array allocation OOM\n      }\n    }\n  }\n\n  object AdmissionLimits {\n\n    def toReadLimit(options: DeltaOptions): ReadLimit = {\n      if (options.maxFilesPerTrigger.isDefined && options.maxBytesPerTrigger.isDefined) {\n        CompositeLimit(\n          ReadMaxBytes(options.maxBytesPerTrigger.get),\n          ReadLimit.maxFiles(options.maxFilesPerTrigger.get).asInstanceOf[ReadMaxFiles])\n      } else if (options.maxBytesPerTrigger.isDefined) {\n        ReadMaxBytes(options.maxBytesPerTrigger.get)\n      } else {\n        ReadLimit.maxFiles(\n          options.maxFilesPerTrigger.getOrElse(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION_DEFAULT))\n      }\n    }\n\n    def apply(options: DeltaOptions, limit: ReadLimit): Option[AdmissionLimits] = limit match {\n      case _: ReadAllAvailable => None\n      case maxFiles: ReadMaxFiles =>\n        Some(new AdmissionLimits(\n          options = options,\n          maxFiles = Some(maxFiles.maxFiles()),\n          maxBytes = None))\n      case maxBytes: ReadMaxBytes =>\n        Some(new AdmissionLimits(\n          options = options,\n          maxFiles = None,\n          maxBytes = Some(maxBytes.maxBytes)))\n      case composite: CompositeLimit =>\n        Some(new AdmissionLimits(\n          options = options,\n          maxFiles = Some(composite.maxFiles.maxFiles()),\n          maxBytes = Some(composite.bytes.maxBytes)))\n      case other => throw DeltaErrors.unknownReadLimit(other.toString())\n    }\n  }\n\n  /**\n   * Validate the protocol at a given version. If the snapshot reconstruction fails for any other\n   * reason than table feature exception, we suppress it. This allows to fallback to previous\n   * behavior where the starting version/timestamp was not mandatory to point to reconstructable\n   * snapshot.\n   *\n   * Returns true when the validation was performed and succeeded.\n   */\n  def validateProtocolAt(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      version: Long): Boolean = {\n    val alwaysValidateProtocol = spark.sessionState.conf.getConf(\n      DeltaSQLConf.FAST_DROP_FEATURE_STREAMING_ALWAYS_VALIDATE_PROTOCOL)\n    if (!alwaysValidateProtocol) return false\n\n    try {\n      // We attempt to construct a snapshot at the startingVersion in order to validate the\n      // protocol. If snapshot reconstruction fails, fall back to the old behavior where the\n      // only requirement was for the commit to exist.\n      deltaLog.getSnapshotAt(version, catalogTableOpt = catalogTableOpt)\n      return true\n    } catch {\n      case e: DeltaUnsupportedTableFeatureException =>\n        recordDeltaEvent(\n          deltaLog = deltaLog,\n          opType = \"dropFeature.validateProtocolAt.unsupportedFeatureFound\",\n          data = Map(\"message\" -> e.getMessage))\n        throw e\n      case NonFatal(e) => // Suppress rest errors.\n        logWarning(log\"Protocol validation failed with '${MDC(DeltaLogKeys.EXCEPTION, e)}'.\")\n        recordDeltaEvent(\n          deltaLog = deltaLog,\n          opType = \"dropFeature.validateProtocolAt.error\",\n          data = Map(\"message\" -> e.getMessage))\n    }\n    false\n  }\n\n  /**\n   * Returns the earliest commit version whose timestamp is >= the provided timestamp.\n   *\n   * This method fetches the commit at the given timestamp via\n   * [[DeltaLog.history.getActiveCommitAtTime]], computes the starting version using\n   * [[DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp]], and validates the protocol\n   * at the returned version.\n   *\n   * @param spark - current spark session\n   * @param deltaLog - Delta log of the table for which we find the version.\n   * @param catalogTableOpt - The CatalogTable for the Delta table.\n   * @param timestamp - user specified timestamp\n   * @param canExceedLatest - if true, version can be greater than the latest snapshot commit\n   * @return - corresponding version number for timestamp\n   * @see [[DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp]] for the core version\n   *      computation logic\n   */\n  def getStartingVersionFromTimestamp(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      timestamp: Timestamp,\n      canExceedLatest: Boolean = false): Long = {\n    val tz = spark.sessionState.conf.sessionLocalTimeZone\n    val commit = deltaLog.history.getActiveCommitAtTime(\n      timestamp,\n      catalogTableOpt = catalogTableOpt,\n      canReturnLastCommit = true,\n      mustBeRecreatable = false,\n      canReturnEarliestCommit = true)\n    // Note: `getActiveCommitAtTime` has called `update`, so we don't need to call it again.\n    val latestVersion = deltaLog.unsafeVolatileSnapshot.version\n    val startingVersion = DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n      timeZone = tz,\n      commitTimestamp = commit.timestamp,\n      commitVersion = commit.version,\n      latestVersion = latestVersion,\n      timestamp = timestamp,\n      canExceedLatest = canExceedLatest\n    )\n    if (startingVersion <= latestVersion) {\n      validateProtocolAt(spark, deltaLog, catalogTableOpt, startingVersion)\n    }\n    startingVersion\n  }\n\n  /**\n   * Read an [[ClosableIterator]] of Delta actions from file status, considering memory constraints\n   */\n  def createRewindableActionIterator(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      fileStatus: FileStatus): ClosableIterator[Action] with SupportsRewinding[Action] = {\n    val threshold = spark.sessionState.conf.getConf(DeltaSQLConf.LOG_SIZE_IN_MEMORY_THRESHOLD)\n    lazy val actions =\n      deltaLog.store.read(fileStatus, deltaLog.newDeltaHadoopConf()).map(Action.fromJson)\n    // Return a new [[CloseableIterator]] over the commit. If the commit is smaller than the\n    // threshold, we will read it into memory once and iterate over that every time.\n    // Otherwise, we read it again every time.\n    val shouldLoadIntoMemory = fileStatus.getLen < threshold\n    def createClosableIterator(): ClosableIterator[Action] = if (shouldLoadIntoMemory) {\n      // Reuse in the memory actions\n      actions.toIterator.toClosable\n    } else {\n      deltaLog.store.readAsIterator(fileStatus, deltaLog.newDeltaHadoopConf())\n        .withClose {\n          _.map(Action.fromJson)\n        }\n    }\n    new ClosableIterator[Action] with SupportsRewinding[Action] {\n      var delegatedIterator: ClosableIterator[Action] = createClosableIterator()\n      override def hasNext: Boolean = delegatedIterator.hasNext\n      override def next(): Action = delegatedIterator.next()\n      override def close(): Unit = delegatedIterator.close()\n      override def rewind(): Unit = delegatedIterator = createClosableIterator()\n    }\n  }\n\n  /**\n   * Scan and get the last item of the iterator.\n   */\n  def iteratorLast[T](iter: ClosableIterator[T]): Option[T] = {\n    try {\n      var last: Option[T] = None\n      while (iter.hasNext) {\n        last = Some(iter.next())\n      }\n      last\n    } finally {\n      iter.close()\n    }\n  }\n\n  /**\n   * Build the latest offset based on the last indexedFile. The function also checks if latest\n   * version is valid by comparing with previous version.\n   * Public for use by SparkMicroBatchStream.\n   * @param tableId The table ID\n   * @param fileVersion The version of the last indexed file.\n   * @param fileIndex The index of the last indexed file.\n   * @param previousVersion Previous offset reservoir version.\n   * @param isInitialSnapshot Whether previous offset is starting version or not.\n   * @return A DeltaSourceOffset representing the next offset to read from.\n   */\n  def buildOffsetFromIndexedFile(\n      tableId: String,\n      fileVersion: Long,\n      fileIndex: Long,\n      previousVersion: Long,\n      isInitialSnapshot: Boolean): DeltaSourceOffset = {\n    val (v, i) = (fileVersion, fileIndex)\n    assert(v >= previousVersion,\n      s\"buildOffsetFromIndexedFile returns an invalid version: $v \" +\n        s\"(expected: >= $previousVersion), tableId: $tableId\")\n\n    // If the last file in previous batch is the end index of that version, automatically bump\n    // to next version to skip accessing that version file altogether. The END_INDEX should never\n    // be returned as an offset.\n    val offset = if (i == DeltaSourceOffset.END_INDEX) {\n      // isInitialSnapshot must be false here as we have bumped the version.\n      DeltaSourceOffset(\n        tableId,\n        v + 1,\n        index = DeltaSourceOffset.BASE_INDEX,\n        isInitialSnapshot = false)\n    } else {\n      // isInitialSnapshot will be true only if previous isInitialSnapshot is true and the next file\n      // is still at the same version.\n      DeltaSourceOffset(\n        tableId, v, i,\n        isInitialSnapshot = v == previousVersion && isInitialSnapshot\n        )\n    }\n    offset\n  }\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSourceCDCSupport.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport java.io.FileNotFoundException\n\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.actions.DomainMetadata\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.DeltaErrors\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.util.Utils\n\n/**\n * Helper functions for CDC-specific handling for DeltaSource.\n */\ntrait DeltaSourceCDCSupport { self: DeltaSource =>\n\n  /////////////////////////\n  // Nested helper class //\n  /////////////////////////\n\n  /**\n   * This class represents an iterator of Change metadata(AddFile, RemoveFile, AddCDCFile)\n   * for a particular version.\n   * @param fileActionsItr - Iterator of IndexedFiles for a particular commit.\n   * @param isInitialSnapshot - Indicates whether the commit version is the initial snapshot or not.\n   */\n  class IndexedChangeFileSeq(\n      fileActionsItr: Iterator[IndexedFile],\n      isInitialSnapshot: Boolean) {\n\n    private def moreThanFrom(\n        indexedFile: IndexedFile, fromVersion: Long, fromIndex: Long): Boolean = {\n      // we need to filter out files so that we get only files after the startingOffset\n      indexedFile.version > fromVersion || indexedFile.index > fromIndex\n    }\n\n    private def lessThanEnd(\n        indexedFile: IndexedFile,\n        endOffset: Option[DeltaSourceOffset]): Boolean = {\n      // we need to filter out files so that they are within the end offsets.\n      if (endOffset.isEmpty) {\n        true\n      } else {\n        indexedFile.version < endOffset.get.reservoirVersion ||\n          (indexedFile.version <= endOffset.get.reservoirVersion &&\n            indexedFile.index <= endOffset.get.index)\n      }\n    }\n\n    private def noMatchesRegex(indexedFile: IndexedFile): Boolean = {\n      if (hasNoFileActionAndStartOrEndIndex(indexedFile)) return true\n\n      excludeRegex.forall(_.findFirstIn(indexedFile.getFileAction.path).isEmpty)\n    }\n\n    private def hasFileAction(indexedFile: IndexedFile): Boolean = {\n      indexedFile.getFileAction != null\n    }\n\n    private def hasNoFileActionAndStartOrEndIndex(indexedFile: IndexedFile): Boolean = {\n      !indexedFile.hasFileAction &&\n        (indexedFile.index == DeltaSourceOffset.BASE_INDEX ||\n          indexedFile.index == DeltaSourceOffset.END_INDEX)\n    }\n\n    private def hasAddsOrRemoves(indexedFile: IndexedFile): Boolean = {\n      indexedFile.add != null || indexedFile.remove != null\n    }\n\n    private def isSchemaChangeIndexedFile(indexedFile: IndexedFile): Boolean = {\n      indexedFile.index == DeltaSourceOffset.METADATA_CHANGE_INDEX ||\n        indexedFile.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX\n    }\n\n    private def isValidIndexedFile(\n        indexedFile: IndexedFile,\n        fromVersion: Long,\n        fromIndex: Long,\n        endOffset: Option[DeltaSourceOffset]): Boolean = {\n      !indexedFile.shouldSkip &&\n        (hasFileAction(indexedFile) ||\n          hasNoFileActionAndStartOrEndIndex(indexedFile) ||\n          isSchemaChangeIndexedFile(indexedFile)) &&\n        moreThanFrom(indexedFile, fromVersion, fromIndex) &&\n        lessThanEnd(indexedFile, endOffset) && noMatchesRegex(indexedFile) &&\n        lessThanEnd(indexedFile, lastOffsetForTriggerAvailableNow)\n    }\n\n    /**\n     * Returns the IndexedFiles for particular commit version after rate-limiting and filtering\n     * out based on version boundaries.\n     */\n    def filterFiles(\n        fromVersion: Long,\n        fromIndex: Long,\n        limits: Option[DeltaSource.AdmissionLimits],\n        endOffset: Option[DeltaSourceOffset] = None): Iterator[IndexedFile] = {\n\n      if (limits.isEmpty) {\n        return fileActionsItr.filter(isValidIndexedFile(_, fromVersion, fromIndex, endOffset))\n      }\n      val admissionControl = limits.get\n      if (isInitialSnapshot) {\n        // NOTE: the initial snapshot can be huge hence we do not do a toSeq here.\n        fileActionsItr\n          .filter(isValidIndexedFile(_, fromVersion, fromIndex, endOffset))\n          .takeWhile { admissionControl.admit(_) }\n      } else {\n        // Change data for a commit can be either recorded by a Seq[AddCDCFiles] or\n        // a Seq[AddFile]/ Seq[RemoveFile]\n        val fileActions = fileActionsItr.toSeq\n\n        // If there exists a stopping iterator for this version, we should return right-away\n        fileActions.find(isSchemaChangeIndexedFile) match {\n          case Some(schemaChangeBarrier) =>\n            return Seq(schemaChangeBarrier).toIterator\n          case _ =>\n        }\n\n        val cdcFiles = fileActions.filter(_.cdc != null) // get only cdc commits.\n        if (cdcFiles.nonEmpty) {\n          // CDC of commit is represented by AddCDCFile\n          val filteredFiles = cdcFiles\n            .filter(isValidIndexedFile(_, fromVersion, fromIndex, endOffset))\n          // For CDC commits we either admit the entire commit or nothing at all.\n          // This is to avoid returning `update_preimage` and `update_postimage` in separate\n          // batches.\n          if (admissionControl.admit(filteredFiles)) {\n            filteredFiles.toIterator\n          } else {\n            Iterator()\n          }\n        } else {\n          // CDC is recorded as AddFile or RemoveFile\n          // We allow entries with no file actions and index as [[DeltaSourceOffset.BASE_INDEX]]\n          // that are used primarily to update latest offset when no other\n          // file action based entries are present.\n          val filteredFiles = fileActions\n            .filter { indexedFile =>\n              hasAddsOrRemoves(indexedFile) || hasNoFileActionAndStartOrEndIndex(indexedFile)\n            }\n            .filter(isValidIndexedFile(_, fromVersion, fromIndex, endOffset))\n          val hasDeletionVectors = fileActions.filter(_.hasFileAction).map(_.getFileAction).exists {\n            case add: AddFile => add.deletionVector != null\n            case remove: RemoveFile => remove.deletionVector != null\n            case _ => false\n          }\n          if (hasDeletionVectors) {\n            // We cannot split up add/remove pairs with Deletion Vectors, because we will get the\n            // wrong result.\n            // So in this case we behave as above with CDC files and either admit all or none.\n            if (admissionControl.admit(filteredFiles)) {\n              filteredFiles.toIterator\n            } else {\n              Iterator()\n            }\n          } else {\n            filteredFiles.takeWhile { admissionControl.admit(_) }.toIterator\n          }\n        }\n      }\n    }\n  }\n\n  ///////////////////////////////\n  // Util methods for children //\n  ///////////////////////////////\n\n  /**\n   * Get the changes from startVersion, startIndex to the end for CDC case. We need to call\n   * CDCReader to get the CDC DataFrame.\n   *\n   * @param startVersion - calculated starting version\n   * @param startIndex - calculated starting index\n   * @param isInitialSnapshot - whether the stream has to return the initial snapshot or not\n   * @param endOffset - Offset that signifies the end of the stream.\n   * @return the DataFrame containing the file changes (AddFile, RemoveFile, AddCDCFile)\n   */\n  protected def getCDCFileChangesAndCreateDataFrame(\n      startVersion: Long,\n      startIndex: Long,\n      isInitialSnapshot: Boolean,\n      endOffset: DeltaSourceOffset): DataFrame = {\n    val changes = getFileChangesForCDC(\n      startVersion, startIndex, isInitialSnapshot, limits = None, Some(endOffset))\n\n    val groupedFileAndCommitInfoActions =\n      changes.map { case (v, indexFiles, commitInfoOpt) =>\n        (v, indexFiles.filter(_.hasFileAction).map(_.getFileAction).toSeq ++ commitInfoOpt)\n      }\n\n    val (result, duration) = Utils.timeTakenMs {\n      // CDCReader calls getSnapshotAt directly instead of using DeltaSource's wrapper, which can\n      // result in FileNotFoundExceptions from the Delta log. We wrap these to present a clearer\n      // error message.\n      try {\n        CDCReader\n          .changesToDF(\n            readSnapshotDescriptor,\n            startVersion,\n            endOffset.reservoirVersion,\n            groupedFileAndCommitInfoActions,\n            spark,\n            catalogTableOpt,\n            isStreaming = true)\n          .fileChangeDf\n      } catch {\n        case e: FileNotFoundException =>\n          throw DeltaErrors.logFileNotFoundExceptionForStreamingSource(e)\n      }\n    }\n    logInfo(log\"Getting CDC dataFrame for delta_log_path=\" +\n      log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)} with \" +\n      log\"startVersion=${MDC(DeltaLogKeys.START_VERSION, startVersion)}, \" +\n      log\"startIndex=${MDC(DeltaLogKeys.START_INDEX, startIndex)}, \" +\n      log\"isInitialSnapshot=${MDC(DeltaLogKeys.IS_INIT_SNAPSHOT, isInitialSnapshot)}, \" +\n      log\"endOffset=${MDC(DeltaLogKeys.END_OFFSET, endOffset)} took timeMs=\" +\n      log\"${MDC(DeltaLogKeys.DURATION, duration)} ms\")\n    result\n  }\n\n  /**\n   * Get the changes starting from (fromVersion, fromIndex). fromVersion is included.\n   * It returns an iterator of (log_version, fileActions, Optional[CommitInfo]). The commit info\n   * is needed later on so that the InCommitTimestamp of the log files can be determined.\n   *\n   * If verifyMetadataAction = true, we will break the stream when we detect any read-incompatible\n   * metadata changes.\n   */\n  protected def getFileChangesForCDC(\n      fromVersion: Long,\n      fromIndex: Long,\n      isInitialSnapshot: Boolean,\n      limits: Option[DeltaSource.AdmissionLimits],\n      endOffset: Option[DeltaSourceOffset],\n      verifyMetadataAction: Boolean = true\n  ): Iterator[(Long, Iterator[IndexedFile], Option[CommitInfo])] = {\n\n    /** Returns matching files that were added on or after startVersion among delta logs. */\n    def filterAndIndexDeltaLogs(\n        startVersion: Long): Iterator[(Long, IndexedChangeFileSeq, Option[CommitInfo])] = {\n      // TODO: handle the case when failOnDataLoss = false and we are missing change log files\n      //    in that case, we need to recompute the start snapshot and evolve the schema if needed\n      require(options.failOnDataLoss || !trackingMetadataChange,\n        \"Using schema from schema tracking log cannot tolerate missing commit files.\")\n      deltaLog.getChanges(\n          startVersion, catalogTableOpt, options.failOnDataLoss).map { case (version, actions) =>\n        // skipIndexedFile must be applied after creating IndexedFile so that\n        // IndexedFile.index is consistent across all versions.\n        val (fileActions, skipIndexedFile, metadataOpt, protocolOpt, commitInfoOpt) =\n          filterCDCActions(\n            actions, version, fromVersion, endOffset.map(_.reservoirVersion),\n            verifyMetadataAction && !trackingMetadataChange)\n        val itr = addBeginAndEndIndexOffsetsForVersion(version,\n          getMetadataOrProtocolChangeIndexedFileIterator(metadataOpt, protocolOpt, version) ++\n            fileActions.zipWithIndex.map {\n              case (action: AddFile, index) =>\n                IndexedFile(\n                  version,\n                  index.toLong,\n                  action,\n                  shouldSkip = skipIndexedFile)\n              case (cdcFile: AddCDCFile, index) =>\n                IndexedFile(\n                  version,\n                  index.toLong,\n                  add = null,\n                  cdc = cdcFile,\n                  shouldSkip = skipIndexedFile)\n              case (remove: RemoveFile, index) =>\n                IndexedFile(\n                  version,\n                  index.toLong,\n                  add = null,\n                  remove = remove,\n                  shouldSkip = skipIndexedFile)\n            })\n        (version, new IndexedChangeFileSeq(itr, isInitialSnapshot = false), commitInfoOpt)\n      }\n    }\n\n    /** Verifies that provided version is <= endOffset version, if defined. */\n    def versionLessThanEndOffset(version: Long, endOffset: Option[DeltaSourceOffset]): Boolean = {\n      endOffset match {\n        case Some(eo) =>\n          version <= eo.reservoirVersion\n        case None =>\n          true\n      }\n    }\n\n    val (result, duration) = Utils.timeTakenMs {\n      val iter: Iterator[(Long, IndexedChangeFileSeq, Option[CommitInfo])] =\n        if (isInitialSnapshot) {\n          // If we are reading change data from the start of the table we need to\n          // get the latest snapshot of the table as well.\n          val (unprocessedSnapshot, snapshotInCommitTimestampOpt) = getSnapshotAt(fromVersion)\n          val snapshot: Iterator[IndexedFile] = unprocessedSnapshot.map { m =>\n            // When we get the snapshot the dataChange is false for the AddFile actions\n            // We need to set it to true for it to be considered by the CDCReader.\n            if (m.add != null) {\n              m.copy(add = m.add.copy(dataChange = true))\n            } else {\n              m\n            }\n          }\n          // This is a hack so that we can easily access the ICT later on.\n          // This `CommitInfo` action is not useful for anything else and should be filtered\n          // out later on.\n          val ictOnlyCommitInfo = Some(CommitInfo.empty(Some(-1))\n            .copy(inCommitTimestamp = snapshotInCommitTimestampOpt))\n          val snapshotItr: Iterator[(Long, IndexedChangeFileSeq, Option[CommitInfo])] = Iterator((\n            fromVersion,\n            new IndexedChangeFileSeq(snapshot, isInitialSnapshot = true),\n            ictOnlyCommitInfo\n          ))\n\n          snapshotItr ++ filterAndIndexDeltaLogs(fromVersion + 1)\n        } else {\n          filterAndIndexDeltaLogs(fromVersion)\n        }\n\n      // In this case, filterFiles will consume the available capacity. We use takeWhile\n      // to stop the iteration when we reach the limit or if endOffset is specified and the\n      // endVersion is reached which will save us from reading unnecessary log files.\n      iter.takeWhile { case (version, _, _) =>\n        limits.forall(_.hasCapacity) && versionLessThanEndOffset(version, endOffset)\n      }.map { case (version, indexItr, ci) =>\n        (version, indexItr.filterFiles(fromVersion, fromIndex, limits, endOffset), ci)\n      }\n    }\n\n    logInfo(log\"Getting CDC file changes for delta_log_path=\" +\n      log\"${MDC(DeltaLogKeys.PATH, deltaLog.logPath)} with \" +\n      log\"fromVersion=${MDC(DeltaLogKeys.START_VERSION, fromVersion)}, fromIndex=\" +\n      log\"${MDC(DeltaLogKeys.START_INDEX, fromIndex)}, \" +\n      log\"isInitialSnapshot=${MDC(DeltaLogKeys.IS_INIT_SNAPSHOT, isInitialSnapshot)} took timeMs=\" +\n      log\"${MDC(DeltaLogKeys.DURATION, duration)} ms\")\n    result\n  }\n\n  /////////////////////\n  // Private methods //\n  /////////////////////\n\n  /**\n   * Filter out non CDC actions and only return CDC ones. This will either be AddCDCFiles\n   * or AddFile and RemoveFiles\n   *\n   * If verifyMetadataAction = true, we will break the stream when we detect any read-incompatible\n   * metadata changes.\n   */\n  private def filterCDCActions(\n      actions: Seq[Action],\n      version: Long,\n      batchStartVersion: Long,\n      batchEndVersionOpt: Option[Long] = None,\n      verifyMetadataAction: Boolean = true\n  ): (Seq[FileAction], Boolean, Option[Metadata], Option[Protocol], Option[CommitInfo]) = {\n    var shouldSkipIndexedFile = false\n    var metadataAction: Option[Metadata] = None\n    var protocolAction: Option[Protocol] = None\n    var commitInfoAction: Option[CommitInfo] = None\n    def checkAndCacheMetadata(m: Metadata): Unit = {\n      if (verifyMetadataAction) {\n        checkReadIncompatibleSchemaChanges(m, version, batchStartVersion, batchEndVersionOpt)\n      }\n      assert(metadataAction.isEmpty,\n        \"Should not encounter two metadata actions in the same commit\")\n      metadataAction = Some(m)\n    }\n\n    if (actions.exists(_.isInstanceOf[AddCDCFile])) {\n      (actions.filter {\n        case _: AddCDCFile => true\n        case commitInfo: CommitInfo =>\n          commitInfoAction = Some(commitInfo)\n          false\n        case m: Metadata =>\n          checkAndCacheMetadata(m)\n          false\n        case p: Protocol =>\n          protocolAction = Some(p)\n          false\n        case _ => false\n      }.asInstanceOf[Seq[FileAction]],\n        shouldSkipIndexedFile,\n        metadataAction,\n        protocolAction,\n        commitInfoAction)\n    } else {\n      (actions.filter {\n        case a: AddFile =>\n          a.dataChange\n        case r: RemoveFile =>\n          r.dataChange\n        case m: Metadata =>\n          checkAndCacheMetadata(m)\n          false\n        case protocol: Protocol =>\n          deltaLog.protocolRead(protocol)\n          assert(protocolAction.isEmpty,\n            \"Should not encounter two protocol actions in the same commit\")\n          protocolAction = Some(protocol)\n          false\n        case commitInfo: CommitInfo =>\n          shouldSkipIndexedFile = CDCReader.shouldSkipFileActionsInCommit(commitInfo)\n          commitInfoAction = Some(commitInfo)\n          false\n        case _: AddCDCFile | _: SetTransaction | _: DomainMetadata =>\n          false\n        case null => // Some crazy future feature. Ignore\n          false\n      }.asInstanceOf[Seq[FileAction]],\n        shouldSkipIndexedFile,\n        metadataAction,\n        protocolAction,\n        commitInfoAction)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSourceMetadataEvolutionSupport.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport java.util.Locale\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{Action, Metadata, Protocol}\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.storage.ClosableIterator\nimport org.apache.spark.sql.delta.storage.ClosableIterator._\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.streaming.Offset\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Helper functions for metadata evolution related handling for DeltaSource.\n * A metadata change is one of:\n * 1. Schema change\n * 2. Delta table configuration change\n * 3. Delta protocol change\n * The documentation below will use schema change as example throughout.\n *\n * To achieve schema evolution, we intercept in different stages of the normal streaming process to:\n * 1. Capture all schema changes inside a stream\n * 2. Stop the latestOffset from crossing the schema change boundary\n * 3. Ensure the batch prior to the schema change can still be served correctly\n * 4. Ensure the stream fails if and only if the prior batch is served successfully\n * 5. Write the new schema to the schema tracking log prior to stream failure, so that next time\n      when it restarts we will use the updated schema.\n *\n * Specifically,\n * 1. During latestOffset calls, if we detect schema change at version V, we generate a special\n *    barrier [[DeltaSourceOffset]] X that has ver=V and index=INDEX_METADATA_CHANGE.\n *    (We first generate an [[IndexedFile]] at this index, and that gets converted into an\n *    equivalent [[DeltaSourceOffset]].)\n *    [[INDEX_METADATA_CHANGE]] comes after [[INDEX_VERSION_BASE]] (the first\n *    offset index that exists for any reservoir version) and before the offsets that represent data\n *    changes. This ensures that we apply the schema change before processing the data\n *    that uses that schema.\n * 2. When we see a schema change offset X, then this is treated as a barrier that ends the\n *    current batch. The remaining data is effectively unavailable until all the source data before\n *    the schema change has been committed.\n * 3. Then, when a [[commit]] is invoked on the offset schema change barrier offset X, we can\n *    then officially write the new schema into the schema tracking log and fail the stream.\n *    [[commit]] is only called after this batch ending at X is completed, so it would be safe to\n *    fail there.\n * 4. In between when offset X is generated and when it is committed, there could be arbitrary\n *    number of calls to [[latestOffset]], attempting to fetch new latestOffset. These calls mustn't\n *    generate new offsets until the schema change barrier offset has been committed, the new schema\n *    has been written to the schema tracking log, and the stream has been aborted and restarted.\n *    A nuance here - streaming engine won't [[commit]] until it sees a new offset that is\n *    semantically different, which is why we first generate an offset X with index\n *    INDEX_METADATA_CHANGE, but another second barrier offset X' immediately following\n *    it with index INDEX_POST_SCHEMA_CHANGE.\n  *    In this way, we could ensure:\n *    a) Offset with index INDEX_METADATA_CHANGE is always committed (typically)\n *    b) Even if streaming engine changed its behavior and ONLY offset with index\n *       INDEX_POST_SCHEMA_CHANGE is committed, we can still see this is a\n *       schema change barrier with a schema change ready to be evolved.\n *    c) Whenever [[latestOffset]] sees a startOffset with a schema change barrier index, we can\n *       easily tell that we should not progress past the schema change, unless the schema change\n *       has actually happened.\n * When a stream is restarted post a schema evolution (not initialization), it is guaranteed to have\n * >= 2 entries in the schema log. To prevent users from shooting themselves in the foot while\n * blindly restart stream without considering implications to downstream tables, by default we would\n * not allow stream to restart without a magic SQL conf that user has to set to allow non-additive\n * schema changes to propagate. We detect such non-additive schema changes during stream start by\n * comparing the last schema log entry with the current one.\n */\ntrait DeltaSourceMetadataEvolutionSupport extends DeltaSourceBase { base: DeltaSource =>\n\n  /**\n   * Whether this DeltaSource is utilizing a schema log entry as its read schema.\n   *\n   * If user explicitly turn on the flag to fall back to using latest schema to read (i.e. the\n   * legacy mode), we will ignore the schema log.\n   */\n  protected def trackingMetadataChange: Boolean =\n    !schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&\n      metadataTrackingLog.flatMap(_.getCurrentTrackedMetadata).nonEmpty\n\n  /**\n   * Whether a schema tracking log is provided (and is empty), so we could initialize eagerly.\n   * This should only be used for the first write to the schema log, after then, schema tracking\n   * should not rely on this state any more.\n   */\n  protected def readyToInitializeMetadataTrackingEagerly: Boolean =\n    !schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&\n      metadataTrackingLog.exists { log =>\n        log.getCurrentTrackedMetadata.isEmpty && log.initMetadataLogEagerly\n      }\n\n\n  /**\n   * This is called from getFileChangesWithRateLimit() during latestOffset().\n   */\n  protected def stopIndexedFileIteratorAtSchemaChangeBarrier(\n      fileActionScanIter: ClosableIterator[IndexedFile]): ClosableIterator[IndexedFile] = {\n    fileActionScanIter.withClose { iter =>\n      val (untilSchemaChange, fromSchemaChange) = iter.span { i =>\n        i.index != DeltaSourceOffset.METADATA_CHANGE_INDEX\n      }\n      // This will end at the schema change indexed file (inclusively)\n      // If there are no schema changes, this is an no-op.\n      untilSchemaChange ++ fromSchemaChange.take(1)\n    }\n  }\n\n  /**\n   * Check the table metadata or protocol changed since the initial read snapshot. We make sure:\n   * 1. The schema is the same, except for internal metadata, AND\n   * 2. The delta related table configurations are strictly equal, AND\n   * 3. The incoming metadata change should not be considered a failure-causing change if we have\n   *    marked the persisted schema and the stream progress is behind that schema version.\n   *    This could happen when we've already merged consecutive schema changes during the analysis\n   *    phase and we are using the merged schema as the read schema. All the schema changes in\n   *    between can be safely ignored because they won't contribute any data.\n   */\n  private def hasMetadataOrProtocolChangeComparedToStreamMetadata(\n      metadataChangeOpt: Option[Metadata],\n      protocolChangeOpt: Option[Protocol],\n      newSchemaVersion: Long): Boolean = {\n    if (persistedMetadataAtSourceInit.exists(_.deltaCommitVersion >= newSchemaVersion)) {\n      false\n    } else {\n      protocolChangeOpt.exists(_ != readProtocolAtSourceInit) ||\n      metadataChangeOpt.exists { newMetadata =>\n         hasSchemaChangeComparedToStreamMetadata(newMetadata.schema) ||\n           newMetadata.partitionSchema != readPartitionSchemaAtSourceInit ||\n           newMetadata.configuration.filterKeys(_.startsWith(\"delta.\")).toMap !=\n             readConfigurationsAtSourceInit.filterKeys(_.startsWith(\"delta.\")).toMap\n      }\n    }\n  }\n\n  /**\n   * Check that the give schema is the same as the schema from the initial read snapshot.\n   */\n  private def hasSchemaChangeComparedToStreamMetadata(newSchema: StructType): Boolean =\n    if (spark.conf.get(DeltaSQLConf.DELTA_STREAMING_IGNORE_INTERNAL_METADATA_FOR_SCHEMA_CHANGE)) {\n      DeltaTableUtils.removeInternalWriterMetadata(spark, newSchema) !=\n        DeltaTableUtils.removeInternalWriterMetadata(spark, readSchemaAtSourceInit)\n    } else {\n      newSchema != readSchemaAtSourceInit\n    }\n\n  /**\n   * If the current stream metadata is not equal to the metadata change in [[metadataChangeOpt]],\n   * return a metadata change barrier [[IndexedFile]].\n   * Only returns something if [[trackingMetadataChange]]is true.\n   */\n  protected def getMetadataOrProtocolChangeIndexedFileIterator(\n      metadataChangeOpt: Option[Metadata],\n      protocolChangeOpt: Option[Protocol],\n      version: Long): ClosableIterator[IndexedFile] = {\n    if (trackingMetadataChange && hasMetadataOrProtocolChangeComparedToStreamMetadata(\n        metadataChangeOpt, protocolChangeOpt, version)) {\n      // Create an IndexedFile with metadata change\n      Iterator.single(IndexedFile(version, DeltaSourceOffset.METADATA_CHANGE_INDEX, null))\n        .toClosable\n    } else {\n      Iterator.empty.toClosable\n    }\n  }\n\n  /**\n   * Collect all actions between start and end version, both inclusive\n   */\n  private def collectActions(\n      startVersion: Long,\n      endVersion: Long\n  ): ClosableIterator[(Long, Action)] = {\n    deltaLog.getChangeLogFiles(startVersion, catalogTableOpt, options.failOnDataLoss).takeWhile {\n      case (version, _) => version <= endVersion\n    }.flatMapWithClose { case (version, fileStatus) =>\n      DeltaSource.createRewindableActionIterator(spark, deltaLog, fileStatus)\n        .map((version, _))\n        .toClosable\n    }\n  }\n\n  /**\n   * Given the version range for an ALREADY fetched batch, check if there are any\n   * read-incompatible schema changes or protocol changes.\n   * In this case, the streaming engine wants to getBatch(X,Y) on an existing Y that is already\n   * loaded and saved in the offset log in the past before requesting new offsets. Therefore we\n   * should verify if we could find a schema or protocol that is safe to read this constructed batch\n   * , which then can be used to initialize the metadata log.\n   * If not, there's not much we could do, even with metadata log, because unlike finding new\n   * offsets, we don't have a chance to \"split\" this batch at schema change boundaries any more. The\n   * streaming engine is not able to change the ranges of a batch after it has created it.\n   * If there are no non-additive schema changes, or incompatible protocol changes, it is safe to\n   * mark the metadata and protocol safe to read for all data files between startVersion and\n   * endVersion.\n   */\n  private def validateAndResolveMetadataForLogInitialization(\n      startVersion: Long, endVersion: Long): (Metadata, Protocol) = {\n    val metadataChanges = collectMetadataActions(startVersion, endVersion).map(_._2)\n    val startSnapshot = getSnapshotFromDeltaLog(startVersion)\n    val startMetadata = startSnapshot.metadata\n\n    // Try to find rename or drop columns in between, or nullability/datatype changes by using\n    // the last schema as the read schema and if so we cannot find a good read schema.\n    // Otherwise, the most recent metadata change will be the most encompassing schema as well.\n    val mostRecentMetadataChangeOpt = metadataChanges.lastOption\n    mostRecentMetadataChangeOpt.foreach { mostRecentMetadataChange =>\n      val otherMetadataChanges = Seq(startMetadata) ++ metadataChanges.dropRight(1)\n      otherMetadataChanges.foreach { potentialSchemaChangeMetadata =>\n        if (!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(\n          newMetadata = mostRecentMetadataChange,\n          oldMetadata = potentialSchemaChangeMetadata) ||\n          !SchemaUtils.isReadCompatible(\n            existingSchema = potentialSchemaChangeMetadata.schema,\n            readSchema = mostRecentMetadataChange.schema,\n            forbidTightenNullability = true)) {\n          throw DeltaErrors.streamingMetadataLogInitFailedIncompatibleMetadataException(\n            startVersion, endVersion)\n        }\n      }\n    }\n\n    // Check protocol changes and use the most supportive protocol\n    val startProtocol = startSnapshot.protocol\n    val protocolChanges = collectProtocolActions(startVersion, endVersion).map(_._2)\n\n    var mostSupportiveProtocol = startProtocol\n    protocolChanges.foreach { p =>\n      if (mostSupportiveProtocol.readerAndWriterFeatureNames\n          .subsetOf(p.readerAndWriterFeatureNames)) {\n        mostSupportiveProtocol = p\n      } else {\n        // TODO: or use protocol union instead?\n        throw DeltaErrors.streamingMetadataLogInitFailedIncompatibleMetadataException(\n          startVersion, endVersion)\n      }\n    }\n\n    (mostRecentMetadataChangeOpt.getOrElse(startMetadata), mostSupportiveProtocol)\n  }\n\n  /**\n   * Collect a metadata action at the commit version if possible.\n   */\n  private def collectMetadataAtVersion(version: Long): Option[Metadata] = {\n    collectActions(version, version).processAndClose { iter =>\n      iter.map(_._2).collectFirst {\n        case a: Metadata => a\n      }\n    }\n  }\n\n  protected def collectMetadataActions(\n      startVersion: Long,\n      endVersion: Long): Seq[(Long, Metadata)] = {\n    collectActions(startVersion, endVersion).processAndClose { iter =>\n      iter.collect {\n        case (version, a: Metadata) => (version, a)\n      }.toSeq\n    }\n  }\n\n  /**\n   * Collect a protocol action at the commit version if possible.\n   */\n  private def collectProtocolAtVersion(version: Long): Option[Protocol] = {\n    collectActions(version, version).processAndClose { iter =>\n      iter.map(_._2).collectFirst {\n        case a: Protocol => a\n      }\n    }\n  }\n\n  protected def collectProtocolActions(\n      startVersion: Long,\n      endVersion: Long): Seq[(Long, Protocol)] = {\n    collectActions(startVersion, endVersion).processAndClose { iter =>\n      iter.collect {\n        case (version, a: Protocol) => (version, a)\n      }.toSeq\n    }\n  }\n\n\n  /**\n   * If the given previous Delta source offset is a schema change offset, returns the appropriate\n   * next offset. This should be called before trying any other means of determining the next\n   * offset.\n   * If this returns None, then there is no schema change, and the caller should determine the next\n   * offset in the normal way.\n   */\n  protected def getNextOffsetFromPreviousOffsetIfPendingSchemaChange(\n      previousOffset: DeltaSourceOffset): Option[DeltaSourceOffset] = {\n    // Check if we've generated a previous offset with schema change (i.e. offset X in class doc)\n    // Then, we will generate offset X' as mentioned in the class doc.\n    if (previousOffset.index == DeltaSourceOffset.METADATA_CHANGE_INDEX) {\n      return Some(previousOffset.copy(index = DeltaSourceOffset.POST_METADATA_CHANGE_INDEX))\n    }\n    // If the previous offset is already POST the schema change and schema evolution has not\n    // occurred, simply block as no-op.\n    if (previousOffset.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX &&\n      hasMetadataOrProtocolChangeComparedToStreamMetadata(\n        collectMetadataAtVersion(previousOffset.reservoirVersion),\n        collectProtocolAtVersion(previousOffset.reservoirVersion),\n        previousOffset.reservoirVersion)) {\n      return Some(previousOffset)\n    }\n\n    // Otherwise, no special handling\n    None\n  }\n\n  /**\n   * Initialize the schema tracking log if an empty schema tracking log is provided.\n   * This method also checks the range between batchStartVersion and batchEndVersion to ensure we\n   * a safe schema to be initialized in the log.\n   * @param batchStartVersion Start version of the batch of data to be proceed, it should typically\n   *                          be the schema that is safe to process incoming data.\n   * @param batchEndVersionOpt Optionally, if we are looking at a constructed batch with existing\n   *                           end offset, we need to double verify to ensure no read-incompatible\n   *                           within the batch range.\n   * @param alwaysFailUponLogInitialized Whether we should always fail with the schema evolution\n   *                                     exception.\n   */\n  protected def initializeMetadataTrackingAndExitStream(\n      batchStartVersion: Long,\n      batchEndVersionOpt: Option[Long] = None,\n      alwaysFailUponLogInitialized: Boolean = false): Unit = {\n    // If possible, initialize the metadata log with the desired start metadata instead of failing.\n    // If a `batchEndVersion` is provided, we also need to verify if there are no incompatible\n    // schema changes in a constructed batch, if so, we cannot find a proper schema to init the\n    // schema log.\n    val (version, metadata, protocol) = batchEndVersionOpt.map { endVersion =>\n      val (validMetadata, validProtocol) =\n        validateAndResolveMetadataForLogInitialization(batchStartVersion, endVersion)\n      // `endVersion` should be valid for initialization\n      (endVersion, validMetadata, validProtocol)\n    }.getOrElse {\n      val startSnapshot = getSnapshotFromDeltaLog(batchStartVersion)\n      (startSnapshot.version, startSnapshot.metadata, startSnapshot.protocol)\n    }\n\n    val newMetadata = PersistedMetadata(tableId, version, metadata, protocol, metadataPath)\n    // Always initialize the metadata log\n    metadataTrackingLog.get.writeNewMetadata(newMetadata)\n    if (hasMetadataOrProtocolChangeComparedToStreamMetadata(\n        Some(metadata), Some(protocol), version) || alwaysFailUponLogInitialized) {\n      // But trigger evolution exception when there's a difference\n      throw DeltaErrors.streamingMetadataEvolutionException(\n        newMetadata.dataSchema,\n        newMetadata.tableConfigurations.get,\n        newMetadata.protocol.get\n      )\n    }\n  }\n\n  /**\n   * Update the current stream schema in the schema tracking log and fail the stream.\n   * This is called during commit().\n   * It's ok to fail during commit() because in streaming's semantics, the batch with offset ending\n   * at `end` should've already being processed completely.\n   */\n  protected def updateMetadataTrackingLogAndFailTheStreamIfNeeded(end: Offset): Unit = {\n    val offset = DeltaSourceOffset(tableId, end)\n    if (trackingMetadataChange &&\n      (offset.index == DeltaSourceOffset.METADATA_CHANGE_INDEX ||\n        offset.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX)) {\n      // The offset must point to a metadata or protocol change action\n      val changedMetadataOpt = collectMetadataAtVersion(offset.reservoirVersion)\n      val changedProtocolOpt = collectProtocolAtVersion(offset.reservoirVersion)\n\n      // Evolve the schema when the schema is indeed different from the current stream schema. We\n      // need to check this because we could potentially generate two offsets before schema\n      // evolution each with different indices.\n      // Typically streaming engine will commit the first one and evolve the schema log, however,\n      // to be absolutely safe, we also consider the case when the first is skipped and only the\n      // second one is committed.\n      // If the first one is committed (typically), the stream will fail and restart with the\n      // evolved schema, then we should NOT fail/evolve again when we commit the second offset.\n      updateMetadataTrackingLogAndFailTheStreamIfNeeded(\n        changedMetadataOpt, changedProtocolOpt, offset.reservoirVersion)\n    }\n  }\n\n  /**\n   * Write a new potentially changed metadata into the metadata tracking log. Then fail the stream\n   * to allow reanalysis if there are changes.\n   * @param changedMetadataOpt Potentially changed metadata action\n   * @param changedProtocolOpt Potentially changed protocol action\n   * @param version The version of change\n   */\n  protected def updateMetadataTrackingLogAndFailTheStreamIfNeeded(\n      changedMetadataOpt: Option[Metadata],\n      changedProtocolOpt: Option[Protocol],\n      version: Long,\n      replace: Boolean = false): Unit = {\n    if (hasMetadataOrProtocolChangeComparedToStreamMetadata(\n        changedMetadataOpt, changedProtocolOpt, version)) {\n\n      val schemaToPersist = PersistedMetadata(\n        deltaLog.unsafeVolatileTableId,\n        version,\n        changedMetadataOpt.getOrElse(readSnapshotDescriptor.metadata),\n        changedProtocolOpt.getOrElse(readSnapshotDescriptor.protocol),\n        metadataPath\n      )\n      // Update schema log\n      if (replace) {\n        metadataTrackingLog.get.writeNewMetadata(schemaToPersist, replaceCurrent = true)\n      } else {\n        metadataTrackingLog.get.writeNewMetadata(schemaToPersist)\n      }\n      // Fail the stream with schema evolution exception\n      throw DeltaErrors.streamingMetadataEvolutionException(\n        schemaToPersist.dataSchema,\n        schemaToPersist.tableConfigurations.get,\n        schemaToPersist.protocol.get\n      )\n    }\n  }\n}\n\nobject DeltaSourceMetadataEvolutionSupport {\n  /** SQL configs that allow unblocking each type of schema changes. */\n  private val SQL_CONF_PREFIX = s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming\"\n\n  private final val SQL_CONF_UNBLOCK_RENAME_DROP =\n    SQL_CONF_PREFIX + \".allowSourceColumnRenameAndDrop\"\n  private final val SQL_CONF_UNBLOCK_RENAME = SQL_CONF_PREFIX + \".allowSourceColumnRename\"\n  private final val SQL_CONF_UNBLOCK_DROP = SQL_CONF_PREFIX + \".allowSourceColumnDrop\"\n  private final val SQL_CONF_UNBLOCK_TYPE_CHANGE = SQL_CONF_PREFIX + \".allowSourceColumnTypeChange\"\n\n  /**\n   * Defining the different combinations of non-additive schema changes to detect them and allow\n   * users to vet and unblock them using a corresponding SQL conf or reader option:\n   * - dropping columns\n   * - renaming columns\n   * - widening data types\n   */\n  private sealed trait SchemaChangeType {\n    val name: String\n    val isRename: Boolean\n    val isDrop: Boolean\n    val isTypeWidening: Boolean\n    val sqlConfsUnblock: Seq[String]\n    val readerOptionsUnblock: Seq[String]\n    val prettyColumnDetailsString: String\n\n    protected def getRenamedColumnsPrettyString(renamedColumns: Seq[RenamedColumn]): String = {\n      s\"\"\"Columns renamed:\n         |${renamedColumns.map { case RenamedColumn(fromFieldPath, toFieldPath) =>\n          s\"'${SchemaUtils.prettyFieldName(fromFieldPath)}' -> \" +\n            s\"'${SchemaUtils.prettyFieldName(toFieldPath)}'\"\n         }.mkString(\"\\n\")}\n         |\"\"\".stripMargin\n    }\n\n    protected def getDroppedColumnsPrettyString(droppedColumns: Seq[DroppedColumn]): String = {\n      s\"\"\"Columns dropped:\n         |${droppedColumns.map(\n            c => s\"'${SchemaUtils.prettyFieldName(c.fieldPath)}'\").mkString(\", \")}\n         |\"\"\".stripMargin\n    }\n\n    protected def getWidenedColumnsPrettyString(widenedColumns: Seq[TypeChange]): String = {\n      s\"\"\"Columns with widened types:\n         |${widenedColumns.map { case TypeChange(_, fromType, toType, fieldPath) =>\n          s\"'${SchemaUtils.prettyFieldName(fieldPath)}': ${fromType.sql} -> ${toType.sql}\"\n         }.mkString(\"\\n\")}\n         |\"\"\".stripMargin\n    }\n  }\n\n  // Single types of schema change, typically caused by a single ALTER TABLE operation.\n  private case class SchemaChangeRename(renamedColumns: Seq[RenamedColumn])\n      extends SchemaChangeType {\n    override val name = \"RENAME COLUMN\"\n    override val (isRename, isDrop, isTypeWidening) = (true, false, false)\n    override val sqlConfsUnblock: Seq[String] = Seq(SQL_CONF_UNBLOCK_RENAME)\n    override val readerOptionsUnblock: Seq[String] = Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_RENAME)\n    override val prettyColumnDetailsString: String = getRenamedColumnsPrettyString(renamedColumns)\n  }\n  private case class SchemaChangeDrop(droppedColumns: Seq[DroppedColumn]) extends SchemaChangeType {\n    override val name = \"DROP COLUMN\"\n    override val (isRename, isDrop, isTypeWidening) = (false, true, false)\n    override val sqlConfsUnblock: Seq[String] = Seq(SQL_CONF_UNBLOCK_DROP)\n    override val readerOptionsUnblock: Seq[String] = Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_DROP)\n    override val prettyColumnDetailsString: String = getDroppedColumnsPrettyString(droppedColumns)\n  }\n  private case class SchemaChangeTypeWidening(widenedColumns: Seq[TypeChange])\n      extends SchemaChangeType {\n    override val name = \"TYPE WIDENING\"\n    override val (isRename, isDrop, isTypeWidening) = (false, false, true)\n    override val sqlConfsUnblock: Seq[String] = Seq(SQL_CONF_UNBLOCK_TYPE_CHANGE)\n    override val readerOptionsUnblock: Seq[String] =\n      Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_TYPE_CHANGE)\n    override val prettyColumnDetailsString: String = getWidenedColumnsPrettyString(widenedColumns)\n  }\n\n  // Combinations of rename, drop and type change -> can be caused by a complete overwrite.\n  private case class SchemaChangeRenameAndDrop(\n      renamedColumns: Seq[RenamedColumn],\n      droppedColumns: Seq[DroppedColumn]) extends SchemaChangeType {\n    override val name = \"RENAME AND DROP COLUMN\"\n    override val (isRename, isDrop, isTypeWidening) = (true, true, false)\n    override val sqlConfsUnblock: Seq[String] = Seq(SQL_CONF_UNBLOCK_RENAME_DROP)\n    override val readerOptionsUnblock: Seq[String] =\n      Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_RENAME, DeltaOptions.ALLOW_SOURCE_COLUMN_DROP)\n    override val prettyColumnDetailsString: String =\n      getRenamedColumnsPrettyString(renamedColumns) + getDroppedColumnsPrettyString(droppedColumns)\n  }\n  private case class SchemaChangeRenameAndTypeWidening(\n      renamedColumns: Seq[RenamedColumn],\n      widenedColumns: Seq[TypeChange]) extends SchemaChangeType {\n    override val name = \"RENAME AND TYPE WIDENING\"\n    override val (isRename, isDrop, isTypeWidening) = (true, false, true)\n    override val sqlConfsUnblock: Seq[String] =\n      Seq(SQL_CONF_UNBLOCK_RENAME, SQL_CONF_UNBLOCK_TYPE_CHANGE)\n    override val readerOptionsUnblock: Seq[String] =\n      Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_RENAME, DeltaOptions.ALLOW_SOURCE_COLUMN_DROP)\n    override val prettyColumnDetailsString: String =\n      getRenamedColumnsPrettyString(renamedColumns) + getWidenedColumnsPrettyString(widenedColumns)\n  }\n  private case class SchemaChangeDropAndTypeWidening(\n      droppedColumns: Seq[DroppedColumn],\n      widenedColumns: Seq[TypeChange]) extends SchemaChangeType {\n    override val name = \"DROP AND TYPE WIDENING\"\n    override val (isRename, isDrop, isTypeWidening) = (false, true, true)\n    override val sqlConfsUnblock: Seq[String] =\n      Seq(SQL_CONF_UNBLOCK_DROP, SQL_CONF_UNBLOCK_TYPE_CHANGE)\n    override val readerOptionsUnblock: Seq[String] =\n      Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_DROP, DeltaOptions.ALLOW_SOURCE_COLUMN_TYPE_CHANGE)\n    override val prettyColumnDetailsString: String =\n      getDroppedColumnsPrettyString(droppedColumns) + getWidenedColumnsPrettyString(widenedColumns)\n  }\n  private case class SchemaChangeRenameAndDropAndTypeWidening(\n      renamedColumns: Seq[RenamedColumn],\n      droppedColumns: Seq[DroppedColumn],\n      widenedColumns: Seq[TypeChange]) extends SchemaChangeType {\n    override val name = \"RENAME, DROP AND TYPE WIDENING\"\n    override val (isRename, isDrop, isTypeWidening) = (true, true, true)\n    override val sqlConfsUnblock: Seq[String] =\n      Seq(SQL_CONF_UNBLOCK_RENAME_DROP, SQL_CONF_UNBLOCK_TYPE_CHANGE)\n    override val readerOptionsUnblock: Seq[String] =\n      Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_DROP, DeltaOptions.ALLOW_SOURCE_COLUMN_TYPE_CHANGE)\n    override val prettyColumnDetailsString: String =\n      getRenamedColumnsPrettyString(renamedColumns) +\n        getDroppedColumnsPrettyString(droppedColumns) +\n        getWidenedColumnsPrettyString(widenedColumns)\n  }\n\n  /**\n   * Build the final schema change descriptor after analyzing all possible schema changes.\n   * @param renamedColumns The columns that have been renamed.\n   * @param droppedColumns The columns that have been dropped.\n   * @param widenedColumns The columns that have been widened.\n   */\n  private def buildSchemaChangeDescriptor(\n      renamedColumns: Seq[RenamedColumn],\n      droppedColumns: Seq[DroppedColumn],\n      widenedColumns: Seq[TypeChange]): Option[SchemaChangeType] = {\n    (renamedColumns.nonEmpty, droppedColumns.nonEmpty, widenedColumns.nonEmpty) match {\n      case (true, false, false) =>\n        Some(SchemaChangeRename(renamedColumns))\n      case (false, true, false) =>\n        Some(SchemaChangeDrop(droppedColumns))\n      case (false, false, true) =>\n        Some(SchemaChangeTypeWidening(widenedColumns))\n      case (true, true, false) =>\n        Some(SchemaChangeRenameAndDrop(renamedColumns, droppedColumns))\n      case (true, false, true) =>\n        Some(SchemaChangeRenameAndTypeWidening(renamedColumns, widenedColumns))\n      case (false, true, true) =>\n        Some(SchemaChangeDropAndTypeWidening(droppedColumns, widenedColumns))\n      case (true, true, true) =>\n        Some(\n          SchemaChangeRenameAndDropAndTypeWidening(renamedColumns, droppedColumns, widenedColumns))\n      case _ => None\n    }\n  }\n\n  /**\n   * Determine the non-additive schema change type for an incoming schema change. None if it's\n   * additive.\n   */\n  private def determineNonAdditiveSchemaChangeType(\n      spark: SparkSession,\n      newSchema: StructType, oldSchema: StructType): Option[SchemaChangeType] = {\n    val renamedColumns = DeltaColumnMapping.collectRenamedColumns(newSchema, oldSchema)\n    val droppedColumns = DeltaColumnMapping.collectDroppedColumns(newSchema, oldSchema)\n    // Use physical column names to identify type changes. Dropping a column and adding a new column\n    // with a different type is historically allowed and is not considered a type change.\n    val oldPhysicalSchema = DeltaColumnMapping.renameColumns(oldSchema)\n    val newPhysicalSchema = DeltaColumnMapping.renameColumns(newSchema)\n    // Check if there are widening type changes. This assumes [[checkIncompatibleSchemaChange]] was\n    // already called before and failed if there were any non-widening type changes. The type change\n    // checks - both widening and non-widening - can be disabled by flag to revert to historical\n    // behavior where type changes are not considered a non-additive schema change and are allowed\n    // to propagate without user action.\n    val typeWideningChanges = if (allowTypeWidening(spark) && !bypassTypeChangeCheck(spark)) {\n      TypeWideningMetadata.collectTypeChanges(oldPhysicalSchema, newPhysicalSchema)\n    } else Seq.empty\n    buildSchemaChangeDescriptor(renamedColumns, droppedColumns, typeWideningChanges)\n  }\n\n  /**\n   * Returns whether the given type of non-additive schema change was unblocked by setting one of\n   * the corresponding SQL confs or reader options.\n   */\n  private def isChangeUnblocked(\n      spark: SparkSession,\n      change: SchemaChangeType,\n      options: DeltaOptions,\n      checkpointHash: Int,\n      schemaChangeVersion: Long): Boolean = {\n\n    def isUnblockedBySQLConf(sqlConf: String): Boolean = {\n      def getConf(key: String): Option[String] =\n        Option(spark.sessionState.conf.getConfString(key, null))\n          .map(_.toLowerCase(Locale.ROOT))\n      val validConfKeysValuePair = Seq(\n        (sqlConf, \"always\"),\n        (s\"$sqlConf.ckpt_$checkpointHash\", \"always\"),\n        (s\"$sqlConf.ckpt_$checkpointHash\", schemaChangeVersion.toString)\n      )\n      validConfKeysValuePair.exists(p => getConf(p._1).contains(p._2))\n    }\n\n    def isUnblockedByReaderOption(readerOption: Option[String]): Boolean = {\n      readerOption.contains(\"always\") || readerOption.contains(schemaChangeVersion.toString)\n    }\n\n    val isBlockedRename = change.isRename &&\n      !isUnblockedByReaderOption(options.allowSourceColumnRename) &&\n      !isUnblockedBySQLConf(SQL_CONF_UNBLOCK_RENAME) &&\n      !isUnblockedBySQLConf(SQL_CONF_UNBLOCK_RENAME_DROP)\n    val isBlockedDrop = change.isDrop &&\n      !isUnblockedByReaderOption(options.allowSourceColumnDrop) &&\n      !isUnblockedBySQLConf(SQL_CONF_UNBLOCK_DROP) &&\n      !isUnblockedBySQLConf(SQL_CONF_UNBLOCK_RENAME_DROP)\n    val isBlockedTypeChange = change.isTypeWidening &&\n      !isUnblockedByReaderOption(options.allowSourceColumnTypeChange) &&\n      !isUnblockedBySQLConf(SQL_CONF_UNBLOCK_TYPE_CHANGE)\n\n    !isBlockedRename && !isBlockedDrop && !isBlockedTypeChange\n  }\n\n  def getCheckpointHash(path: String): Int = path.hashCode\n\n  /**\n   * Whether to accept widening type changes:\n   *   - when true, widening type changes cause the stream to fail, requesting user to review and\n   *     unblock them via a SQL conf or reader option.\n   *   - when false, widening type changes are rejected without possibility to unblock, similar to\n   *     any other arbitrary type change.\n   */\n  def allowTypeWidening(spark: SparkSession): Boolean = {\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_ALLOW_TYPE_WIDENING_STREAMING_SOURCE)\n  }\n\n  /**\n   * We historically allowed any type changes to go through when schema tracking was enabled. This\n   * config allows reverting to that behavior.\n   */\n  def bypassTypeChangeCheck(spark: SparkSession): Boolean =\n    spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK)\n\n  // scalastyle:off\n  /**\n   * Given a non-additive operation type from a previous schema evolution, check we can process\n   * using the new schema given any SQL conf or dataframe reader option users have explicitly set to\n   * unblock.\n   * The SQL conf can take one of following formats:\n   * 1. spark.databricks.delta.streaming.allowSourceColumn$action = \"always\"\n   *    -> allows non-additive schema change to propagate for all streams.\n   * 2. spark.databricks.delta.streaming.allowSourceColumn$action.$checkpointHash = \"always\"\n   *    -> allows non-additive schema change to propagate for this particular stream.\n   * 3. spark.databricks.delta.streaming.allowSourceColumn$action.$checkpointHash = $deltaVersion\n   *    -> allow non-additive schema change to propagate only for this particular stream source\n   *        table version.\n   * The reader options can take one of the following format:\n   * 1.  .option(\"allowSourceColumn$action\", \"always\")\n   *    -> allows non-additive schema change to propagate for this particular stream.\n   * 2.  .option(\"allowSourceColumn$action\", \"$deltaVersion\")\n   *    -> allow non-additive schema change to propagate only for this particular stream source\n   *        table version.\n   * where `allowSourceColumn$action` is one of:\n   * 1. `allowSourceColumnRename` to allow column renames.\n   * 2. `allowSourceColumnDrop` to allow column drops.\n   * 3. `allowSourceColumnTypeChange` to allow widening type changes.\n   * For SQL confs only, action can also be `allowSourceColumnRenameAndDrop` to allow both column\n   * drops and renames.\n   *\n   * We will check for any of these configs given the non-additive operation, and throw a proper\n   * error message to instruct the user to set the SQL conf / reader options if they would like to\n   * unblock.\n   *\n   * @param metadataPath The path to the source-unique metadata location under checkpoint\n   * @param currentSchema The current persisted schema\n   * @param previousSchema The previous persisted schema\n   */\n  // scalastyle:on\n  protected[sources] def validateIfSchemaChangeCanBeUnblocked(\n      spark: SparkSession,\n      parameters: Map[String, String],\n      metadataPath: String,\n      currentSchema: PersistedMetadata,\n      previousSchema: PersistedMetadata): Unit = {\n    val options = new DeltaOptions(parameters, spark.sessionState.conf)\n    val checkpointHash = getCheckpointHash(metadataPath)\n\n    // The start version of a possible series of consecutive schema changes.\n    val previousSchemaChangeVersion = previousSchema.deltaCommitVersion\n    // The end version of a possible series of consecutive schema changes.\n    val currentSchemaChangeVersion = currentSchema.deltaCommitVersion\n\n    // Fail with a non-retryable exception if there are any type changes that we don't allow\n    // unblocking, i.e. non-widening type changes. We do allow changes caused by columns being\n    // dropped/renamed, e.g. dropping a column and adding it back with a different type. These were\n    // historically allowed and will be surfaced to the user as column drop/rename.\n    checkIncompatibleSchemaChange(\n      spark,\n      previousSchema = previousSchema.dataSchema,\n      currentSchema = currentSchema.dataSchema,\n      currentSchemaChangeVersion\n    )\n\n    determineNonAdditiveSchemaChangeType(\n      spark, currentSchema.dataSchema, previousSchema.dataSchema).foreach { change =>\n        if (!isChangeUnblocked(\n            spark, change, options, checkpointHash, currentSchemaChangeVersion)) {\n          // Throw error to prompt user to set the correct confs\n          throw DeltaErrors.cannotContinueStreamingPostSchemaEvolution(\n            change.name,\n            previousSchemaChangeVersion,\n            currentSchemaChangeVersion,\n            checkpointHash,\n            change.readerOptionsUnblock,\n            change.sqlConfsUnblock,\n            change.prettyColumnDetailsString)\n        }\n    }\n  }\n\n  /**\n   * Checks that the new schema only contains column rename/drop and widening type changes compared\n   * to the previous schema. That is, rejects any non-widening type changes.\n   */\n  private def checkIncompatibleSchemaChange(\n      spark: SparkSession,\n      previousSchema: StructType,\n      currentSchema: StructType,\n      currentSchemaChangeVersion: Long): Unit = {\n    if (bypassTypeChangeCheck(spark)) return\n\n    val incompatibleSchema =\n      !SchemaUtils.isReadCompatible(\n        // We want to ignore renamed/dropped columns here and let the check for non-additive\n        // schema changes handle them: we only check if an actual physical column had an\n        // incompatible type change.\n        existingSchema = DeltaColumnMapping.renameColumns(previousSchema),\n        readSchema = DeltaColumnMapping.renameColumns(currentSchema),\n        forbidTightenNullability = true,\n        allowMissingColumns = true,\n        typeWideningMode =\n          if (allowTypeWidening(spark)) TypeWideningMode.AllTypeWidening\n          else TypeWideningMode.NoTypeWidening\n      )\n    if (incompatibleSchema) {\n      throw DeltaErrors.schemaChangedException(\n        previousSchema,\n        currentSchema,\n        retryable = false,\n        Some(currentSchemaChangeVersion),\n        includeStartingVersionOrTimestampMessage = false)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSourceMetadataTrackingLog.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.InputStream\n\nimport scala.collection.JavaConverters._\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.streaming.{JsonSchemaSerializer, PartitionAndDataSchema, SchemaTrackingLog}\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, DeltaOptions, SnapshotDescriptor}\nimport org.apache.spark.sql.delta.actions.{Action, FileAction, Metadata, Protocol}\nimport org.apache.spark.sql.delta.storage.ClosableIterator._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.types.{DataType, StructType}\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n// scalastyle:on import.ordering.noEmptyLine\n\n/**\n * A [[PersistedMetadata]] is an entry in Delta streaming source schema log, which can be used to\n * read data files during streaming.\n *\n * @param tableId Delta table id\n * @param deltaCommitVersion Delta commit version in which this change is captured. It does not\n *                           necessarily have to be the commit when there's an actual change, e.g.\n *                           during initialization.\n *                           The invariant is that the metadata must be read-compatible with the\n *                           table snapshot at this version.\n * @param dataSchemaJson Full schema json\n * @param partitionSchemaJson Partition schema json\n * @param sourceMetadataPath The checkpoint path that is unique to each source.\n * @param tableConfigurations The configurations of the table inside the metadata when the schema\n *                            change was detected. It is used to correctly create the right file\n *                            format when we use a particular schema to read.\n *                            Default to None for backward compatibility.\n * @param protocolJson JSON of the protocol change if any.\n *                     Default to None for backward compatibility.\n * @param previousMetadataSeqNum When defined, it points to the batch ID / seq num for the previous\n *                           metadata in the log sequence. It is used when we could not reliably\n *                           tell if the currentBatchId - 1 is indeed the previous schema evolution,\n *                           e.g. when we are merging consecutive schema changes during the analysis\n *                           phase and we are appending an extra schema after the merge to the log.\n *                           Default to None for backward compatibility.\n */\ncase class PersistedMetadata(\n    tableId: String,\n    deltaCommitVersion: Long,\n    dataSchemaJson: String,\n    partitionSchemaJson: String,\n    sourceMetadataPath: String,\n    tableConfigurations: Option[Map[String, String]] = None,\n    protocolJson: Option[String] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    previousMetadataSeqNum: Option[Long] = None) extends PartitionAndDataSchema {\n\n  private def parseSchema(schemaJson: String): StructType = {\n    try {\n      DataType.fromJson(schemaJson).asInstanceOf[StructType]\n    } catch {\n      case NonFatal(_) =>\n        throw DeltaErrors.failToParseSchemaLog\n    }\n  }\n\n  @JsonIgnore\n  lazy val dataSchema: StructType = parseSchema(dataSchemaJson)\n\n  @JsonIgnore\n  lazy val partitionSchema: StructType = parseSchema(partitionSchemaJson)\n\n  @JsonIgnore\n  lazy val protocol: Option[Protocol] =\n    protocolJson.map(Action.fromJson).map(_.asInstanceOf[Protocol])\n\n  def validateAgainstSnapshot(snapshot: SnapshotDescriptor): Unit = {\n    if (snapshot.deltaLog.unsafeVolatileTableId != tableId) {\n      throw DeltaErrors.incompatibleSchemaLogDeltaTable(\n        tableId, snapshot.deltaLog.unsafeVolatileTableId)\n    }\n  }\n\n}\n\nobject PersistedMetadata {\n  val VERSION = 1\n  val EMPTY_JSON = \"{}\"\n\n  def fromJson(json: String): PersistedMetadata = JsonUtils.fromJson[PersistedMetadata](json)\n\n  def apply(\n      tableId: String,\n      deltaCommitVersion: Long,\n      metadata: Metadata,\n      protocol: Protocol,\n      sourceMetadataPath: String): PersistedMetadata = {\n    PersistedMetadata(tableId, deltaCommitVersion,\n      metadata.schema.json, metadata.partitionSchema.json,\n      // The schema is bound to the specific source\n      sourceMetadataPath,\n      // Table configurations come from the Metadata action\n      Some(metadata.configuration),\n      Some(protocol.json)\n    )\n  }\n}\n\n/**\n * Tracks the metadata changes for a particular Delta streaming source in a particular stream,\n * it is utilized to save and lookup the correct metadata during streaming from a Delta table.\n * This schema log is NOT meant to be shared across different Delta streaming source instances.\n *\n * @param rootMetadataLocation Metadata log location\n * @param sourceSnapshot Delta source snapshot for the Delta streaming source\n * @param sourceMetadataPathOpt The source metadata path that is used during streaming execution.\n * @param initMetadataLogEagerly If true, initialize metadata log as early as possible, otherwise,\n *                             initialize only when detecting non-additive schema change.\n */\nclass DeltaSourceMetadataTrackingLog private(\n    sparkSession: SparkSession,\n    rootMetadataLocation: String,\n    sourceSnapshot: SnapshotDescriptor,\n    sourceMetadataPathOpt: Option[String] = None,\n    val initMetadataLogEagerly: Boolean = true) {\n\n  import org.apache.spark.sql.delta.streaming.SchemaTrackingExceptions._\n\n  protected val schemaSerializer =\n    new JsonSchemaSerializer[PersistedMetadata](PersistedMetadata.VERSION) {\n      override def deserialize(in: InputStream): PersistedMetadata =\n        try super.deserialize(in) catch {\n          case FailedToDeserializeException =>\n            throw DeltaErrors.failToDeserializeSchemaLog(rootMetadataLocation)\n        }\n    }\n\n  protected val trackingLog =\n    new SchemaTrackingLog[PersistedMetadata](\n      sparkSession, rootMetadataLocation, schemaSerializer)\n\n  // Validate schema at log init\n  trackingLog.getCurrentTrackedSchema.foreach(_.validateAgainstSnapshot(sourceSnapshot))\n\n  /**\n   * Get the global latest metadata for this metadata location.\n   * Visible for testing\n   */\n  private[delta] def getLatestMetadata: Option[PersistedMetadata] =\n    trackingLog.getLatest().map(_._2)\n\n  /**\n   * Get the current schema that is being tracked by this schema log. This is typically the latest\n   * schema log entry to the best of this schema log's knowledge.\n   */\n  def getCurrentTrackedMetadata: Option[PersistedMetadata] =\n    trackingLog.getCurrentTrackedSchema\n\n  /**\n   * Get the current tracked seq num by this schema log or -1 if no schema has been tracked yet.\n   */\n  def getCurrentTrackedSeqNum: Long = trackingLog.getCurrentTrackedSeqNum\n\n  /**\n   * Get the logically-previous tracked seq num by this schema log.\n   * Considering the prev pointer from the latest entry if defined.\n   */\n  private def getPreviousTrackedSeqNum: Long = {\n    getCurrentTrackedMetadata.flatMap(_.previousMetadataSeqNum) match {\n      case Some(previousSeqNum) => previousSeqNum\n      case None => trackingLog.getCurrentTrackedSeqNum - 1\n    }\n  }\n\n  /**\n   * Get the logically-previous tracked schema entry by this schema log.\n   * DeltaSource requires it to compare the previous schema with the latest schema to determine if\n   * an automatic stream restart is allowed.\n   */\n  def getPreviousTrackedMetadata: Option[PersistedMetadata] =\n    trackingLog.getTrackedSchemaAtSeqNum(getPreviousTrackedSeqNum)\n\n  /**\n   * Track a new schema to the log.\n   *\n   * @param newMetadata The incoming new metadata with schema.\n   * @param replaceCurrent If true, we will set a previous seq num pointer on the incoming metadata\n   *                       change pointing to the previous seq num of the current latest metadata.\n   *                       So that once the new metadata is written, getPreviousTrackedMetadata()\n   *                       will return the updated reference.\n   *                       If a previous metadata does not exist, this is noop.\n   */\n  def writeNewMetadata(\n      newMetadata: PersistedMetadata,\n      replaceCurrent: Boolean = false): PersistedMetadata = {\n    try {\n      trackingLog.addSchemaToLog(\n        if (replaceCurrent && getCurrentTrackedMetadata.isDefined) {\n          newMetadata.copy(previousMetadataSeqNum = Some(getPreviousTrackedSeqNum))\n        } else newMetadata\n      )\n    } catch {\n      case FailedToEvolveSchema =>\n        throw DeltaErrors.sourcesWithConflictingSchemaTrackingLocation(\n          rootMetadataLocation, sourceSnapshot.deltaLog.dataPath.toString)\n    }\n  }\n}\n\nobject DeltaSourceMetadataTrackingLog extends Logging {\n\n  def fullMetadataTrackingLocation(\n      rootSchemaTrackingLocation: String,\n      tableId: String,\n      sourceTrackingId: Option[String] = None): String = {\n    val subdir = s\"_schema_log_$tableId\" + sourceTrackingId.map(n => s\"_$n\").getOrElse(\"\")\n    new Path(rootSchemaTrackingLocation, subdir).toString\n  }\n\n  /**\n   * Create a schema log instance for a schema location.\n   * The schema location is constructed as `$rootMetadataLocation/_schema_log_$tableId`\n   * a suffix of `_$sourceTrackingId` is appended if provided to further differentiate the sources.\n   *\n   * @param mergeConsecutiveSchemaChanges Defined during analysis phase.\n   * @param sourceMetadataPathOpt Defined during execution phase.\n   */\n  def create(\n      sparkSession: SparkSession,\n      rootMetadataLocation: String,\n      sourceSnapshot: SnapshotDescriptor,\n      catalogTableOpt: Option[CatalogTable],\n      parameters: Map[String, String],\n      sourceMetadataPathOpt: Option[String] = None,\n      mergeConsecutiveSchemaChanges: Boolean = false,\n      initMetadataLogEagerly: Boolean = true): DeltaSourceMetadataTrackingLog = {\n    val options = new CaseInsensitiveStringMap(parameters.asJava)\n    val sourceTrackingId = Option(options.get(DeltaOptions.STREAMING_SOURCE_TRACKING_ID))\n    val metadataTrackingLocation = fullMetadataTrackingLocation(\n      rootMetadataLocation, sourceSnapshot.deltaLog.unsafeVolatileTableId, sourceTrackingId)\n    val log = new DeltaSourceMetadataTrackingLog(\n      sparkSession,\n      metadataTrackingLocation,\n      sourceSnapshot,\n      sourceMetadataPathOpt,\n      initMetadataLogEagerly\n    )\n\n    // During initialize schema log, validate against:\n    // 1. table snapshot to check for partition and tahoe id mismatch\n    // 2. source metadata path to ensure we are not using the wrong schema log for the source\n    log.getCurrentTrackedMetadata.foreach { schema =>\n      schema.validateAgainstSnapshot(sourceSnapshot)\n      if (sparkSession.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_STREAMING_SCHEMA_TRACKING_METADATA_PATH_CHECK_ENABLED)) {\n        sourceMetadataPathOpt.foreach { metadataPath =>\n          require(metadataPath == schema.sourceMetadataPath,\n            s\"The Delta source metadata path used for execution '${metadataPath}' is different \" +\n              s\"from the one persisted for previous processing '${schema.sourceMetadataPath}'. \" +\n              s\"Please check if the schema location has been reused across different streaming \" +\n              s\"sources. Pick a new `${DeltaOptions.SCHEMA_TRACKING_LOCATION}` or use \" +\n              s\"`${DeltaOptions.STREAMING_SOURCE_TRACKING_ID}` to \" +\n              s\"distinguish between streaming sources.\")\n        }\n      }\n    }\n\n    // The consecutive schema merging logic is run in the *analysis* phase, when we figure the final\n    // schema to read for the streaming dataframe.\n    if (mergeConsecutiveSchemaChanges && log.getCurrentTrackedMetadata.isDefined) {\n      // If enable schema merging, skim ahead on consecutive schema changes and use the latest one\n      // to update the log again if possible.\n      // We add the prev pointer to the merged schema so that SQL conf validation logic later can\n      // reliably fetch the previous read schema and the latest schema and then be able to determine\n      // if it's OK for the stream to proceed.\n      getMergedConsecutiveMetadataChanges(\n        sparkSession,\n        sourceSnapshot.deltaLog,\n        catalogTableOpt,\n        log.getCurrentTrackedMetadata.get\n      ).foreach { mergedSchema =>\n        log.writeNewMetadata(mergedSchema, replaceCurrent = true)\n      }\n    }\n\n    // The validation is ran in *execution* phase where the metadata path becomes available.\n    // While loading the current persisted schema, validate against previous persisted schema\n    // to check if the stream can move ahead with the custom SQL conf.\n    (log.getPreviousTrackedMetadata, log.getCurrentTrackedMetadata, sourceMetadataPathOpt) match {\n      case (Some(prev), Some(curr), Some(metadataPath)) =>\n        DeltaSourceMetadataEvolutionSupport\n          .validateIfSchemaChangeCanBeUnblocked(\n            sparkSession, parameters, metadataPath, curr, prev)\n      case _ =>\n    }\n\n    log\n  }\n\n  /**\n   * Speculate ahead and find the next merged consecutive metadata change if possible.\n   * A metadata change is either:\n   * 1. A [[Metadata]] action change. OR\n   * 2. A [[Protocol]] change.\n   */\n  private def getMergedConsecutiveMetadataChanges(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTableOpt: Option[CatalogTable],\n      currentMetadata: PersistedMetadata): Option[PersistedMetadata] = {\n    val currentMetadataVersion = currentMetadata.deltaCommitVersion\n    // We start from the currentSchemaVersion so that we can stop early in case the current\n    // version still has file actions that potentially needs to be processed.\n    val untilMetadataChange =\n      deltaLog.getChangeLogFiles(\n          currentMetadataVersion, catalogTableOpt).map { case (version, fileStatus) =>\n        var metadataAction: Option[Metadata] = None\n        var protocolAction: Option[Protocol] = None\n        var hasFileAction = false\n        DeltaSource.createRewindableActionIterator(spark, deltaLog, fileStatus)\n          .processAndClose { actionsIter =>\n            actionsIter.foreach {\n              case m: Metadata => metadataAction = Some(m)\n              case p: Protocol => protocolAction = Some(p)\n              case _: FileAction => hasFileAction = true\n              case _ =>\n            }\n          }\n        (!hasFileAction && (metadataAction.isDefined || protocolAction.isDefined),\n          version, metadataAction, protocolAction)\n      }.takeWhile(_._1)\n    DeltaSource.iteratorLast(untilMetadataChange.toClosable)\n      .flatMap { case (_, version, metadataOpt, protocolOpt) =>\n      if (version == currentMetadataVersion) {\n        None\n      } else {\n        log.info(s\"Looked ahead from version $currentMetadataVersion and \" +\n          s\"will use metadata at version $version to read Delta stream.\")\n        Some(\n          currentMetadata.copy(\n            deltaCommitVersion = version,\n            dataSchemaJson =\n              metadataOpt.map(_.schema.json).getOrElse(currentMetadata.dataSchemaJson),\n            partitionSchemaJson =\n              metadataOpt.map(_.partitionSchema.json)\n                .getOrElse(currentMetadata.partitionSchemaJson),\n            tableConfigurations = metadataOpt.map(_.configuration)\n              .orElse(currentMetadata.tableConfigurations),\n            protocolJson = protocolOpt.map(_.json).orElse(currentMetadata.protocolJson)\n          )\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSourceOffset.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.IOException\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.core.{JsonGenerator, JsonParseException, JsonParser, JsonProcessingException}\nimport com.fasterxml.jackson.databind.{DeserializationContext, SerializerProvider}\nimport com.fasterxml.jackson.databind.annotation.{JsonDeserialize, JsonSerialize}\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer\nimport com.fasterxml.jackson.databind.exc.InvalidFormatException\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer\n\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.connector.read.streaming.{Offset => OffsetV2}\nimport org.apache.spark.sql.execution.streaming.Offset\n\n/**\n * Tracks how far we processed in when reading changes from the [[DeltaLog]].\n *\n * Note this class retains the naming of `Reservoir` to maintain compatibility\n * with serialized offsets from the beta period.\n *\n * @param reservoirId       The id of the table we are reading from. Used to detect\n *                          misconfiguration when restarting a query.\n * @param reservoirVersion  The version of the table that we are current processing.\n * @param index             The index in the sequence of AddFiles in this version. Used to\n *                          break large commits into multiple batches. This index is created by\n *                          sorting on modificationTimestamp and path.\n * @param isInitialSnapshot Whether this offset points into an initial full table snapshot at the\n *                          provided reservoir version rather than into the changes at that version.\n *                          When starting a new query, we first process all data present in the\n *                          table at the start and then move on to processing new data that has\n *                          arrived.\n */\n@JsonDeserialize(using = classOf[DeltaSourceOffset.Deserializer])\n@JsonSerialize(using = classOf[DeltaSourceOffset.Serializer])\ncase class DeltaSourceOffset private(\n    reservoirId: String,\n    reservoirVersion: Long,\n    index: Long,\n    isInitialSnapshot: Boolean\n  ) extends Offset with Comparable[DeltaSourceOffset] {\n\n  import DeltaSourceOffset._\n\n  assert(index != -1, \"Index should never be -1, it should be set to the BASE_INDEX instead.\")\n\n  override def json: String = {\n    JsonUtils.toJson(this)\n  }\n\n  /**\n   * Compare two DeltaSourceOffsets which are on the same table.\n   * @return 0 for equivalent offsets. negative if this offset is less than `otherOffset`. Positive\n   *         if this offset is greater than `otherOffset`\n   */\n  def compare(otherOffset: DeltaSourceOffset): Int = {\n    assert(reservoirId == otherOffset.reservoirId, \"Comparing offsets that do not refer to the\" +\n      \" same table is disallowed.\")\n    implicitly[Ordering[(Long, Long)]].compare((reservoirVersion, index),\n      (otherOffset.reservoirVersion, otherOffset.index))\n  }\n  override def compareTo(o: DeltaSourceOffset): Int = {\n    compare(o)\n  }\n}\n\nobject DeltaSourceOffset extends Logging {\n\n  private[DeltaSourceOffset] val VERSION_1 = 1\n  private[DeltaSourceOffset] val VERSION_2 = 2 // reserved\n  // Serialization version 3 adds support for schema change index values.\n  private[DeltaSourceOffset] val VERSION_3 = 3\n\n  private[DeltaSourceOffset] val CURRENT_VERSION = VERSION_3\n\n  // The base index within each reservoirVersion. This offset indicates the offset before all\n  // changes in the reservoirVersion. All other offsets within the reservoirVersion have an index\n  // that is higher than the base index.\n  //\n  // This index is for VERSION_3+. Unless there are other fields that force the version to be >=3,\n  // it should NOT be serialized into offset log for backward compatibility. Instead, we serialize\n  // this as INDEX_VERSION_BASE_V1, and set source version lower accordingly. It gets converted back\n  // to the VERSION_3 value at deserialization time, so that we only use the V3 value in memory.\n  private[DeltaSourceOffset] val BASE_INDEX_V3: Long = -100\n\n  // The V1 base index that should be serialized into the offset log\n  private[DeltaSourceOffset] val BASE_INDEX_V1: Long = -1\n\n  // The base index version clients of DeltaSourceOffset should use\n  val BASE_INDEX: Long = BASE_INDEX_V3\n\n  // The index for an IndexedFile that also contains a metadata change. (from VERSION_3)\n  val METADATA_CHANGE_INDEX: Long = -20\n  // The index for an IndexedFile that is right after a metadata change. (from VERSION_3)\n  val POST_METADATA_CHANGE_INDEX: Long = -19\n\n  // A value close to the end of the Long space. This is used to indicate that we are at the end of\n  // a reservoirVersion and need to move on to the next one. This should never be serialized into\n  // the offset log.\n  val END_INDEX: Long = Long.MaxValue - 100\n\n  /**\n   * The ONLY external facing constructor to create a DeltaSourceOffset in memory.\n   * @param reservoirId Table id\n   * @param reservoirVersion Table commit version\n   * @param index File action index in the commit version\n   * @param isInitialSnapshot Whether this offset is still in initial snapshot\n   */\n  def apply(\n      reservoirId: String,\n      reservoirVersion: Long,\n      index: Long,\n      isInitialSnapshot: Boolean\n  ): DeltaSourceOffset = {\n    // TODO should we detect `reservoirId` changes when a query is running?\n    new DeltaSourceOffset(\n      reservoirId,\n      reservoirVersion,\n      index,\n      isInitialSnapshot\n    )\n  }\n\n  /**\n   * Validate and parse a DeltaSourceOffset from its JSON serialized format\n   * @param reservoirId Table id\n   * @param json Raw JSON string\n   */\n  def apply(reservoirId: String, json: String): DeltaSourceOffset = {\n    val o = JsonUtils.mapper.readValue[DeltaSourceOffset](json)\n    if (o.reservoirId != reservoirId) {\n      throw DeltaErrors.differentDeltaTableReadByStreamingSource(\n        newTableId = reservoirId, oldTableId = o.reservoirId)\n    }\n    o\n  }\n\n  /**\n   * Validate and parse a DeltaSourceOffset from its serialized format\n   * @param reservoirId Table id\n   * @param offset Raw streaming offset\n   */\n  def apply(reservoirId: String, offset: OffsetV2): DeltaSourceOffset = {\n    offset match {\n      case o: DeltaSourceOffset => o\n      case s => apply(reservoirId, s.json)\n    }\n  }\n\n  /**\n   * Validate offsets to make sure we always move forward. Moving backward may make the query\n   * re-process data and cause data duplication.\n   */\n  def validateOffsets(previousOffset: DeltaSourceOffset, currentOffset: DeltaSourceOffset): Unit = {\n    if (previousOffset.isInitialSnapshot == false && currentOffset.isInitialSnapshot == true) {\n      throw new IllegalStateException(\n        s\"Found invalid offsets: 'isInitialSnapshot' flipped incorrectly. \" +\n          s\"Previous: $previousOffset, Current: $currentOffset\")\n    }\n    if (previousOffset.reservoirVersion > currentOffset.reservoirVersion) {\n      throw new IllegalStateException(\n        s\"Found invalid offsets: 'reservoirVersion' moved back. \" +\n          s\"Previous: $previousOffset, Current: $currentOffset\")\n    }\n    if (previousOffset.reservoirVersion == currentOffset.reservoirVersion &&\n      previousOffset.index > currentOffset.index) {\n      throw new IllegalStateException(\n        s\"Found invalid offsets. 'index' moved back. \" +\n          s\"Previous: $previousOffset, Current: $currentOffset\")\n    }\n  }\n\n  def isMetadataChangeIndex(index: Long): Boolean =\n    index == METADATA_CHANGE_INDEX || index == POST_METADATA_CHANGE_INDEX\n\n  /**\n   * This is a 1:1 copy of [[DeltaSourceOffset]] used for JSON serialization. Our serializers only\n   * want to adjust some field values and then serialize in the normal way. But we cannot access the\n   * \"default\" serializers once we've overridden them. So instead, we use a separate case class that\n   * gets serialized \"as-is\".\n   */\n  private case class DeltaSourceOffsetForSerialization private(\n      sourceVersion: Long,\n      reservoirId: String,\n      reservoirVersion: Long,\n      index: Long,\n      // This stores isInitialSnapshot.\n      // This was confusingly called \"starting version\" in earlier versions, even though enabling\n      // the option \"startingVersion\" actually causes this to be disabled. We still have to\n      // serialize it using the old name for backward compatibility.\n      isStartingVersion: Boolean\n    )\n\n  class Deserializer\n    extends StdDeserializer[DeltaSourceOffset](classOf[DeltaSourceOffset]) {\n    @throws[IOException]\n    @throws[JsonProcessingException]\n    override def deserialize(p: JsonParser, ctxt: DeserializationContext): DeltaSourceOffset = {\n      val o = try {\n        p.readValueAs(classOf[DeltaSourceOffsetForSerialization])\n      } catch {\n        case e: Throwable if e.isInstanceOf[JsonParseException] ||\n            e.isInstanceOf[InvalidFormatException] =>\n          // The version may be there with a different format, or something else might be off.\n          throw DeltaErrors.invalidSourceOffsetFormat()\n      }\n\n      if (o.sourceVersion < VERSION_1) {\n        throw DeltaErrors.invalidSourceVersion(o.sourceVersion.toString)\n      }\n      if (o.sourceVersion > CURRENT_VERSION) {\n        throw DeltaErrors.invalidFormatFromSourceVersion(o.sourceVersion, CURRENT_VERSION)\n      }\n      if (o.sourceVersion == VERSION_2) {\n        // Version 2 is reserved.\n        throw DeltaErrors.invalidSourceVersion(o.sourceVersion.toString)\n      }\n      // Always upgrade to use the current latest INDEX_VERSION_BASE\n      val offsetIndex = if (o.sourceVersion < VERSION_3 && o.index == BASE_INDEX_V1) {\n        logDebug(s\"upgrading offset to use latest version base index\")\n        BASE_INDEX\n      } else {\n        o.index\n      }\n      assert(offsetIndex != END_INDEX, \"Should not deserialize END_INDEX\")\n\n      // Leverage the only external facing constructor to initialize with latest sourceVersion\n      DeltaSourceOffset(\n        reservoirId = o.reservoirId,\n        reservoirVersion = o.reservoirVersion,\n        index = offsetIndex,\n        isInitialSnapshot = o.isStartingVersion\n      )\n    }\n  }\n\n  class Serializer\n    extends StdSerializer[DeltaSourceOffset](classOf[DeltaSourceOffset]) {\n\n    @throws[IOException]\n    override def serialize(\n        o: DeltaSourceOffset,\n        gen: JsonGenerator,\n        provider: SerializerProvider): Unit = {\n      assert(o.index != END_INDEX, \"Should not serialize END_INDEX\")\n\n      // We handle a few backward compatibility scenarios during Serialization here:\n      // 1. [Backward compatibility] If the source index is a schema changing base index, then\n      //    replace it with index = -1 and use VERSION_1. This allows older Delta to at least be\n      //    able to read the non-schema-changes stream offsets.\n      //    This needs to happen during serialization time so we won't be looking at a downgraded\n      //    index right away when we need to utilize this offset in memory.\n      // 2. [Backward safety] If the source index is a new schema changing index, then use\n      //    VERSION_3. Older Delta would explode upon seeing this, but that's the safe thing to do.\n      val minVersion = {\n        if (DeltaSourceOffset.isMetadataChangeIndex(o.index)) {\n          VERSION_3\n        }\n        else {\n          VERSION_1\n        }\n      }\n      val downgradedIndex = if (o.index == BASE_INDEX) {\n        BASE_INDEX_V1\n      } else {\n        o.index\n      }\n      gen.writeObject(DeltaSourceOffsetForSerialization(\n        sourceVersion = minVersion,\n        reservoirId = o.reservoirId,\n        reservoirVersion = o.reservoirVersion,\n        index = downgradedIndex,\n        isStartingVersion = o.isInitialSnapshot\n      ))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSourceUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport java.util.Locale\n\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.sources\nimport org.apache.spark.sql.sources.Filter\n\nobject DeltaSourceUtils {\n  val NAME = \"delta\"\n  val ALT_NAME = \"delta\"\n\n  // Batch relations don't pass partitioning columns to `CreatableRelationProvider`s, therefore\n  // as a hack, we pass in the partitioning columns among the options.\n  val PARTITIONING_COLUMNS_KEY = \"__partition_columns\"\n\n\n  // The metadata key recording the generation expression in a generated column's `StructField`.\n  val GENERATION_EXPRESSION_METADATA_KEY = \"delta.generationExpression\"\n\n\n  val IDENTITY_INFO_ALLOW_EXPLICIT_INSERT = \"delta.identity.allowExplicitInsert\"\n  val IDENTITY_INFO_START = \"delta.identity.start\"\n  val IDENTITY_INFO_STEP = \"delta.identity.step\"\n  val IDENTITY_INFO_HIGHWATERMARK = \"delta.identity.highWaterMark\"\n  val IDENTITY_COMMITINFO_TAG = \"delta.identity.schemaUpdate\"\n\n  def isDeltaDataSourceName(name: String): Boolean = {\n    name.toLowerCase(Locale.ROOT) == NAME || name.toLowerCase(Locale.ROOT) == ALT_NAME\n  }\n\n  /** Check whether this table is a Delta table based on information from the Catalog. */\n  def isDeltaTable(provider: Option[String]): Boolean = {\n    provider.exists(isDeltaDataSourceName)\n  }\n\n  /** Creates Spark literals from a value exposed by the public Spark API. */\n  private def createLiteral(value: Any): expressions.Literal = value match {\n    case v: String => expressions.Literal.create(v)\n    case v: Int => expressions.Literal.create(v)\n    case v: Byte => expressions.Literal.create(v)\n    case v: Short => expressions.Literal.create(v)\n    case v: Long => expressions.Literal.create(v)\n    case v: Double => expressions.Literal.create(v)\n    case v: Float => expressions.Literal.create(v)\n    case v: Boolean => expressions.Literal.create(v)\n    case v: java.sql.Date => expressions.Literal.create(v)\n    case v: java.sql.Timestamp => expressions.Literal.create(v)\n    case v: java.time.Instant => expressions.Literal.create(v)\n    case v: java.time.LocalDate => expressions.Literal.create(v)\n    case v: BigDecimal => expressions.Literal.create(v)\n  }\n\n  /** Translates the public Spark Filter APIs into Spark internal expressions. */\n  def translateFilters(filters: Array[Filter]): Expression = filters.map {\n    case sources.EqualTo(attribute, value) =>\n      expressions.EqualTo(UnresolvedAttribute(attribute), expressions.Literal.create(value))\n    case sources.EqualNullSafe(attribute, value) =>\n      expressions.EqualNullSafe(UnresolvedAttribute(attribute), expressions.Literal.create(value))\n    case sources.GreaterThan(attribute, value) =>\n      expressions.GreaterThan(UnresolvedAttribute(attribute), expressions.Literal.create(value))\n    case sources.GreaterThanOrEqual(attribute, value) =>\n      expressions.GreaterThanOrEqual(\n        UnresolvedAttribute(attribute), expressions.Literal.create(value))\n    case sources.LessThan(attribute, value) =>\n      expressions.LessThan(UnresolvedAttribute(attribute), expressions.Literal.create(value))\n    case sources.LessThanOrEqual(attribute, value) =>\n      expressions.LessThanOrEqual(UnresolvedAttribute(attribute), expressions.Literal.create(value))\n    case sources.In(attribute, values) =>\n      expressions.In(UnresolvedAttribute(attribute), values.map(createLiteral))\n    case sources.IsNull(attribute) => expressions.IsNull(UnresolvedAttribute(attribute))\n    case sources.IsNotNull(attribute) => expressions.IsNotNull(UnresolvedAttribute(attribute))\n    case sources.Not(otherFilter) => expressions.Not(translateFilters(Array(otherFilter)))\n    case sources.And(filter1, filter2) =>\n      expressions.And(translateFilters(Array(filter1)), translateFilters(Array(filter2)))\n    case sources.Or(filter1, filter2) =>\n      expressions.Or(translateFilters(Array(filter1)), translateFilters(Array(filter2)))\n    case sources.StringStartsWith(attribute, value) =>\n      new expressions.Like(\n        UnresolvedAttribute(attribute), expressions.Literal.create(s\"${value}%\"))\n    case sources.StringEndsWith(attribute, value) =>\n      new expressions.Like(\n        UnresolvedAttribute(attribute), expressions.Literal.create(s\"%${value}\"))\n    case sources.StringContains(attribute, value) =>\n      new expressions.Like(\n        UnresolvedAttribute(attribute), expressions.Literal.create(s\"%${value}%\"))\n    case sources.AlwaysTrue() => expressions.Literal.TrueLiteral\n    case sources.AlwaysFalse() => expressions.Literal.FalseLiteral\n  }.reduceOption(expressions.And).getOrElse(expressions.Literal.TrueLiteral)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaStreamUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport java.sql.Timestamp\n\nimport scala.collection.mutable\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.TypeWideningMode\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.util.{DateTimeUtils, TimestampFormatter}\n\nimport org.apache.spark.sql.{Column, DataFrame, SparkSession}\nimport org.apache.spark.sql.classic.ClassicConversions._\nimport org.apache.spark.sql.execution.QueryExecution\nimport org.apache.spark.sql.types.StructType\n\nobject DeltaStreamUtils {\n\n  /**\n   * Select `cols` from a micro batch DataFrame. Directly calling `select` won't work because it\n   * will create a `QueryExecution` rather than inheriting `IncrementalExecution` from\n   * the micro batch DataFrame. A streaming micro batch DataFrame to execute should use\n   * `IncrementalExecution`.\n   */\n  def selectFromStreamingDataFrame(\n      incrementalExecution: IncrementalExecution,\n      df: DataFrame,\n      cols: Column*): DataFrame = {\n    val newMicroBatch = df.select(cols: _*)\n    val newIncrementalExecution = createIncrementalExecution(\n      newMicroBatch.sparkSession,\n      newMicroBatch.queryExecution.logical,\n      incrementalExecution.outputMode,\n      incrementalExecution.checkpointLocation,\n      incrementalExecution.queryId,\n      incrementalExecution.runId,\n      incrementalExecution.currentBatchId,\n      incrementalExecution.prevOffsetSeqMetadata,\n      incrementalExecution.offsetSeqMetadata,\n      incrementalExecution.watermarkPropagator,\n      incrementalExecution.isFirstBatch)\n    newIncrementalExecution.executedPlan // Force the lazy generation of execution plan\n    DataFrameUtils.ofRows(newIncrementalExecution)\n  }\n\n  /**\n   * Configuration options for schema compatibility validation during Delta streaming reads.\n   *\n   * This class encapsulates various flags and settings that control how Delta streaming handles\n   * schema changes and compatibility checks.\n   *\n   * TODO(#5319): Clean up the configs that were intended as escape-hatches for behavior changes\n   * if they aren't needed anymore.\n   *\n   * @param allowUnsafeStreamingReadOnColumnMappingSchemaChanges\n   *        Flag that allows user to force enable unsafe streaming read on Delta table with\n   *        column mapping enabled AND drop/rename actions.\n   * @param allowUnsafeStreamingReadOnPartitionColumnChanges\n   *        Flag that allows user to force enable unsafe streaming read on Delta table with\n   *        column mapping enabled AND partition column changes.\n   * @param forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart\n   *        Flag that allows user to disable the read-compatibility check during stream start which\n   *        protects against a corner case in which verifyStreamHygiene could not detect.\n   *        This is a bug fix but yet a potential behavior change, so we add a flag to fallback.\n   * @param forceEnableUnsafeReadOnNullabilityChange\n   *        Flag that allows user to fallback to the legacy behavior in which user can allow\n   *        nullable=false schema to read nullable=true data, which is incorrect but a behavior\n   *        change regardless.\n   * @param isStreamingFromColumnMappingTable\n   *        Whether we are streaming from a table with column mapping enabled.\n   * @param typeWideningEnabled\n   *        Whether we are streaming from a table that has the type widening table feature enabled.\n   * @param enableSchemaTrackingForTypeWidening\n   *        Whether we should track widening type changes to allow users to accept them and resume\n   *        stream processing.\n   */\n  case class SchemaReadOptions(\n      allowUnsafeStreamingReadOnColumnMappingSchemaChanges: Boolean,\n      allowUnsafeStreamingReadOnPartitionColumnChanges: Boolean,\n      forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart: Boolean,\n      forceEnableUnsafeReadOnNullabilityChange: Boolean,\n      isStreamingFromColumnMappingTable: Boolean,\n      typeWideningEnabled: Boolean,\n      enableSchemaTrackingForTypeWidening: Boolean\n  )\n\n  object SchemaReadOptions {\n    /**\n     * Creates a SchemaReadOptions instance from SparkSession configuration settings.\n     *\n     * @param spark The SparkSession from which to read configuration values.\n     * @param isStreamingFromColumnMappingTable Whether the source table has column mapping enabled.\n     * @param isTypeWideningSupportedInProtocol Whether the table's protocol version supports\n     *        type widening.\n     * @return A [[SchemaReadOptions]] instance containing all schema validation flags derived from\n     *         the session configuration and provided table state.\n     */\n    def fromSparkSession(\n        spark: SparkSession,\n        isStreamingFromColumnMappingTable: Boolean,\n        isTypeWideningSupportedInProtocol: Boolean): SchemaReadOptions = {\n      val allowUnsafeStreamingReadOnColumnMappingSchemaChanges =\n        spark.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES)\n      val allowUnsafeStreamingReadOnPartitionColumnChanges =\n        spark.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_STREAMING_UNSAFE_READ_ON_PARTITION_COLUMN_CHANGE)\n      val forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart =\n        spark.sessionState.conf.getConf(DeltaSQLConf.\n            DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES_DURING_STREAM_START)\n      val forceEnableUnsafeReadOnNullabilityChange =\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_STREAM_UNSAFE_READ_ON_NULLABILITY_CHANGE)\n      val typeWideningEnabled =\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_ALLOW_TYPE_WIDENING_STREAMING_SOURCE) &&\n            isTypeWideningSupportedInProtocol\n      val enableSchemaTrackingForTypeWidening =\n        spark.sessionState.conf.getConf(\n          DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING)\n\n      new DeltaStreamUtils.SchemaReadOptions(\n        allowUnsafeStreamingReadOnColumnMappingSchemaChanges =\n          allowUnsafeStreamingReadOnColumnMappingSchemaChanges,\n        allowUnsafeStreamingReadOnPartitionColumnChanges =\n          allowUnsafeStreamingReadOnPartitionColumnChanges,\n        forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart =\n          forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart,\n        forceEnableUnsafeReadOnNullabilityChange = forceEnableUnsafeReadOnNullabilityChange,\n        isStreamingFromColumnMappingTable = isStreamingFromColumnMappingTable,\n        typeWideningEnabled = typeWideningEnabled,\n        enableSchemaTrackingForTypeWidening = enableSchemaTrackingForTypeWidening)\n    }\n  }\n\n  sealed trait SchemaCompatibilityResult\n  object SchemaCompatibilityResult {\n    // Indicates that the schema change is compatible and can be applied safely\n    case object Compatible extends SchemaCompatibilityResult\n    // Indicates that the schema change is incompatible and would break the query,\n    // but the change can be applied by recovering the query\n    case object RetryableIncompatible extends SchemaCompatibilityResult\n    // Indicates that the schema change is incompatible and would break the query,\n    // but the change cannot be applied by recovering the query\n    case object NonRetryableIncompatible extends SchemaCompatibilityResult\n\n    // helper methods for java interop\n    def isCompatible(result: SchemaCompatibilityResult): Boolean =\n      result == Compatible\n    def isRetryableIncompatible(result: SchemaCompatibilityResult): Boolean =\n      result == RetryableIncompatible\n  }\n\n  /**\n   * Validate schema compatibility between data schema and read schema. Checks for read\n   * compatibility considering nullability, type widening, missing columns, and partition changes.\n   *\n   * @param dataSchema The actual schema of the data\n   * @param readSchema The schema used by the reader to read data\n   * @param newPartitionColumns The partition columns for new metadata\n   * @param oldPartitionColumns The partition columns for old metadata\n   * @param backfilling Whether the check is triggered during backfilling (processing old data)\n   * @param readOptions Configuration options that control schema compatibility rules\n   *\n   * @return A [[SchemaCompatibilityResult]] on whether the data schema is compatible, and if not,\n   *         whether restarting the stream will allow processing data across the schema change.\n   */\n  def checkSchemaChangesWhenNoSchemaTracking(\n      dataSchema: StructType,\n      readSchema: StructType,\n      newPartitionColumns: Seq[String],\n      oldPartitionColumns: Seq[String],\n      backfilling: Boolean,\n      readOptions: SchemaReadOptions): SchemaCompatibilityResult = {\n    // We forbid the case when the data schema is nullable while the read schema is NOT\n    // nullable, or in other words, `readSchema` should not tighten nullability from `dataSchema`,\n    // because we don't ever want to read back any nulls when the read schema is non-nullable.\n    val shouldForbidTightenNullability = !readOptions.forceEnableUnsafeReadOnNullabilityChange\n    // If schema tracking is disabled for type widening, we allow widening type changes to go\n    // through without requiring the user to set `allowSourceColumnTypeChange`. The schema change\n    // will cause the stream to fail with a retryable exception, and the stream will restart using\n    // the new schema.\n    val allowWideningTypeChanges = readOptions.typeWideningEnabled &&\n        !readOptions.enableSchemaTrackingForTypeWidening\n    // If a user is streaming from a column mapping table and enable the unsafe flag to ignore\n    // column mapping schema changes, we can allow the standard check to allow missing columns\n    // from the read schema in the data schema, because the only case that happens is when\n    // user rename/drops column but they don't care so they enabled the flag to unblock.\n    // This is only allowed when we are \"backfilling\", i.e. the stream progress is older than\n    // the analyzed table version. Any schema change past the analysis should still throw\n    // exception, because additive schema changes MUST be taken into account.\n    val shouldAllowMissingColumns = readOptions.isStreamingFromColumnMappingTable &&\n        readOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges && backfilling\n    // When backfilling after a type change, allow processing the data using the new, wider\n    // type.\n    // typeWideningMode when using `readSchema` to read `dataSchema`\n    val forwardTypeWideningMode = if (allowWideningTypeChanges && backfilling) {\n      TypeWideningMode.AllTypeWidening\n    } else {\n      TypeWideningMode.NoTypeWidening\n    }\n\n    if (!SchemaUtils.isReadCompatible(\n      existingSchema = dataSchema,\n      readSchema = readSchema,\n      forbidTightenNullability = shouldForbidTightenNullability,\n      allowMissingColumns = shouldAllowMissingColumns,\n      typeWideningMode = forwardTypeWideningMode,\n      newPartitionColumns = newPartitionColumns,\n      oldPartitionColumns = oldPartitionColumns\n    )) {\n      // Check for widening type changes that would succeed on retry when we backfill batches.\n      // typeWideningMode when using `dataSchema` to read `readSchema`\n      val backwardTypeWideningMode = if (allowWideningTypeChanges) {\n        TypeWideningMode.AllTypeWidening\n      } else {\n        TypeWideningMode.NoTypeWidening\n      }\n      // Only schema change later than the current read snapshot/schema can be retried, in other\n      // words, backfills could never be retryable, because we have no way to refresh\n      // the latest schema to \"catch up\" when the schema change happens before than current read\n      // schema version.\n      // If not backfilling, we do another check to determine retryability, in which we assume\n      // we will be reading using this later `dataSchema` back on the current outdated `readSchema`,\n      // and if it works (including that `dataSchema` should not tighten the nullability\n      // constraint from `readSchema`), it is a retryable exception.\n      val retryable = !backfilling && SchemaUtils.isReadCompatible(\n        existingSchema = readSchema,\n        readSchema = dataSchema,\n        forbidTightenNullability = shouldForbidTightenNullability,\n        typeWideningMode = backwardTypeWideningMode\n      )\n      if (retryable) {\n        SchemaCompatibilityResult.RetryableIncompatible\n      } else {\n        SchemaCompatibilityResult.NonRetryableIncompatible\n      }\n    } else {\n      SchemaCompatibilityResult.Compatible\n    }\n  }\n\n  /**\n   * - If commit's timestamp exactly matches the provided timestamp, we return it.\n   * - Otherwise, we return the earliest commit version\n   *   with a timestamp greater than the provided one.\n   * - If the provided timestamp is larger than the timestamp\n   *   of any committed version, and canExceedLatest is disabled we throw an error.\n   * - If the provided timestamp is larger than the timestamp\n   *   of any committed version, and canExceedLatest is enabled we return a version that is greater\n   *   than commitVersion by one\n   *\n   * @param timeZone - time zone for formatting error messages\n   * @param commitTimestamp - timestamp of the commit\n   * @param commitVersion - version of the commit\n   * @param latestVersion - latest snapshot version\n   * @param timestamp - user specified timestamp\n   * @param canExceedLatest - if true, version can be greater than the latest snapshot commit\n   * @return - corresponding version number for timestamp\n   */\n  def getStartingVersionFromCommitAtTimestamp(\n      timeZone: String,\n      commitTimestamp: Long,\n      commitVersion: Long,\n      latestVersion: Long,\n      timestamp: Timestamp,\n      canExceedLatest: Boolean = false): Long = {\n    if (commitTimestamp >= timestamp.getTime) {\n      // Find the commit at the `timestamp` or the earliest commit\n      commitVersion\n    } else {\n      // commitTimestamp is not the same, so this commit is a commit before the timestamp and\n      // the next version if exists should be the earliest commit after the timestamp.\n      //\n      // Note: In the use case of [[CDCReader]] timestamp passed in can exceed the latest commit\n      // timestamp, caller doesn't expect exception, and can handle the non-existent version.\n      val latestNotExceeded = commitVersion + 1 <= latestVersion\n      if (latestNotExceeded || canExceedLatest) {\n        commitVersion + 1\n      } else {\n        val commitTs = new Timestamp(commitTimestamp)\n        val timestampFormatter = TimestampFormatter(DateTimeUtils.getTimeZone(timeZone))\n        val tsString = DateTimeUtils.timestampToString(\n          timestampFormatter, DateTimeUtils.fromJavaTimestamp(commitTs))\n        throw DeltaErrors.timestampGreaterThanLatestCommit(timestamp, commitTs, tsString)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/sources/limits.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.connector.read.streaming.{ReadLimit, ReadMaxFiles}\nimport org.apache.spark.sql.internal.SQLConf\n\n/** A read limit that admits a soft-max of `maxBytes` per micro-batch. */\ncase class ReadMaxBytes(maxBytes: Long) extends ReadLimit\n\n/**\n * A read limit that admits the given soft-max of `bytes` or max `maxFiles`, once `minFiles`\n * has been reached. Prior to that anything is admitted.\n */\ncase class CompositeLimit(\n  bytes: ReadMaxBytes,\n  maxFiles: ReadMaxFiles,\n  minFiles: ReadMinFiles = ReadMinFiles(-1)) extends ReadLimit\n\n\n/** A read limit that admits a min of `minFiles` per micro-batch. */\ncase class ReadMinFiles(minFiles: Int) extends ReadLimit\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/ArrayAccumulator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\n// scalastyle:off import.ordering.noEmptyLine\n\nimport org.apache.spark.util.AccumulatorV2\n\n/**\n * An accumulator that keeps arrays of counts. Counts from multiple partitions\n * are merged by index. -1 indicates a null and is handled using TVL (-1 + N = -1)\n */\nclass ArrayAccumulator(val size: Int) extends AccumulatorV2[(Int, Long), Array[Long]] {\n\n  protected val counts = new Array[Long](size)\n\n  override def isZero: Boolean = counts.forall(_ == 0)\n  override def copy(): AccumulatorV2[(Int, Long), Array[Long]] = {\n    val newCopy = new ArrayAccumulator(size)\n    (0 until size).foreach(i => newCopy.counts(i) = counts(i))\n    newCopy\n  }\n  override def reset(): Unit = (0 until size).foreach(counts(_) = 0)\n  override def add(v: (Int, Long)): Unit = {\n    if (v._2 == -1 || counts(v._1) == -1) {\n      counts(v._1) = -1\n    } else {\n      counts(v._1) += v._2\n    }\n  }\n  override def merge(o: AccumulatorV2[(Int, Long), Array[Long]]): Unit = {\n    val other = o.asInstanceOf[ArrayAccumulator]\n    assert(size == other.size)\n\n    (0 until size).foreach(i => {\n      if (counts(i) == -1 || other.counts(i) == -1) {\n        counts(i) = -1\n      } else {\n        counts(i) += other.counts(i)\n      }\n    })\n  }\n  override def value: Array[Long] = counts\n\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/AutoCompactPartitionStats.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport scala.collection.mutable\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.hooks.AutoCompactPartitionReserve\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.SparkSession\n\n/**\n * A collector used to aggregate auto-compaction stats for a single commit. The expectation\n * is to spin this up for a commit and then merging those local stats with the global stats.\n */\ntrait AutoCompactPartitionStatsCollector {\n  def collectPartitionStatsForAdd(file: AddFile): Unit\n  def collectPartitionStatsForRemove(file: RemoveFile): Unit\n  def finalizeStats(tableId: String): Unit\n}\n\n/**\n * This singleton object collect the table partition statistic for each commit that creates\n * AddFile or RemoveFile objects.\n * To control the memory usage, there are `maxNumTablePartitions` per table and 'maxNumPartitions'\n * partition entries across all tables.\n * Note:\n *   1. Since the partition of each table is limited, if this limitation is reached, the least\n *      recently used table partitions will be evicted.\n *   2. If all 'maxNumPartitions' are occupied, the partition stats of least recently used tables\n *      will be evicted until the used partitions fall back below to 'maxNumPartitions'.\n *   3. The un-partitioned tables are treated as tables with single partition.\n * @param maxNumTablePartitions The hash space of partition key to reduce memory usage per table.\n * @param maxNumPartitions The maximum number of partition that can be occupied.\n */\nclass AutoCompactPartitionStats(\n    private var maxNumTablePartitions: Int,\n    private var maxNumPartitions: Int\n) {\n\n  /**\n   * This class to store the states of one table partition. These state includes:\n   * -- the number of small files,\n   * -- the thread that assigned to compact this partition, and\n   * -- whether the partition was compacted.\n   *\n   * Note: Since this class keeps tracking of the statistics of the table partition and the state of\n   * the auto compaction thread that works on the table partition, any method that accesses any\n   * attribute of this class needs to be protected by synchronized context.\n   */\n  class PartitionStat(\n      var numFiles: Long,\n      var wasAutoCompacted: Boolean = false) {\n\n    /**\n     * Determine whether this partition can be autocompacted based on the number of small files or\n     * if this [[AutoCompactPartitionStats]] instance has not auto compacted it yet.\n     * @param minNumFiles The minimum number of files this table-partition should have to trigger\n     *                    Auto Compaction in case it has already been compacted once.\n     */\n    def hasSufficientSmallFilesOrHasNotBeenCompacted(minNumFiles: Long): Boolean =\n      !wasAutoCompacted || hasSufficientFiles(minNumFiles)\n\n    def hasSufficientFiles(minNumFiles: Long): Boolean = numFiles >= minNumFiles\n  }\n\n  /**\n   * This hashtable is used to store all table partition states of a table, the key is the hashcode\n   * of the partition, the value is [[PartitionStat]] object.\n   */\n  type TablePartitionStats = mutable.LinkedHashMap[Int, PartitionStat]\n\n  // The hash map to store the number of small files in each partition.\n  // -- Key is the hash code of the partition value.\n  // -- Values is the number of small files inside the corresponding partition.\n  type PartitionFilesMap = mutable.LinkedHashMap[Int, Long]\n\n  type PartitionKey = Map[String, String]\n\n  type PartitionKeySet = Set[Map[String, String]]\n\n  // This is a simple LRU to store the table partition statistics.\n  // Workspace private to enable testing.\n  private[delta] val tablePartitionStatsCache =\n    new mutable.LinkedHashMap[String, TablePartitionStats]()\n\n  // The number of partitions in this cache.\n  private[delta] var numUsedPartitions = 0\n\n  /**\n   * Helper class used to keep state regarding tracking auto-compaction stats of AddFile and\n   * RemoveFile actions in a single run that are greater than a passed-in minimum file size.\n   * If the collector runs into any non-fatal errors, it will invoke the error reporter on the error\n   * and then skip further execution.\n   *\n   * @param minFileSize    Minimum file size for files we track auto-compact stats\n   * @param errorReporter  Function that reports the first error, if any\n   * @return A collector object that tracks the Add/Remove file actions of the current commit.\n   */\n  def createStatsCollector(\n      minFileSize: Long,\n      errorReporter: Throwable => Unit):\n      AutoCompactPartitionStatsCollector = new AutoCompactPartitionStatsCollector {\n    private val inputPartitionFiles = new PartitionFilesMap()\n    private var shouldCollect = true\n\n    /**\n     * If the file is less than the specified min file size, updates the partition file map\n     * of stats with add or remove actions. If we encounter an error during stats collection,\n     * the remainder of the files will not be collected as well.\n     */\n    private def collectPartitionStatsForFile(file: FileAction, addSub: Int): Unit = {\n      try {\n        val minSizeThreshold = minFileSize\n        if (shouldCollect &&\n          file.estLogicalFileSize.getOrElse(file.getFileSize) <= minSizeThreshold\n        ) {\n          updatePartitionFileCounter(inputPartitionFiles, file.partitionValues, addSub)\n        }\n      } catch {\n        case NonFatal(e) =>\n          errorReporter(e)\n          shouldCollect = false\n      }\n    }\n    /**\n     * Adds one file to all the appropriate partition counters.\n     */\n    override def collectPartitionStatsForAdd(file: AddFile): Unit = {\n      collectPartitionStatsForFile(file, addSub = 1)\n    }\n    /**\n     * Removes one file from all the appropriate partition counters.\n     */\n    override def collectPartitionStatsForRemove(file: RemoveFile): Unit = {\n      collectPartitionStatsForFile(file, addSub = -1)\n    }\n\n    /**\n     * Merges the current collector's stats with the global one.\n     */\n    override def finalizeStats(tableId: String): Unit = {\n      try {\n        if (shouldCollect) merge(tableId, inputPartitionFiles.filter(_._2 != 0))\n      } catch {\n        case NonFatal(e) => errorReporter(e)\n      }\n    }\n  }\n\n  /**\n   * This method merges the `inputPartitionFiles` of current committed transaction to the\n   * global cache of table partition stats. After merge is completed, tablePath will be moved\n   * to most recently used position. If the number of occupied partitions exceeds\n   * MAX_NUM_PARTITIONS, the least recently used tables will be evicted out.\n   *\n   * @param tableId The path of the table that contains `inputPartitionFiles`.\n   * @param inputPartitionFiles The number of files, which are qualified for Auto Compaction, in\n   *                            each partition.\n   */\n  def merge(tableId: String, inputPartitionFiles: PartitionFilesMap): Unit = {\n    if (inputPartitionFiles.isEmpty) return\n    synchronized {\n      tablePartitionStatsCache.get(tableId) match {\n        case Some(cachedPartitionStates) =>\n          // If the table is already stored, merges inputPartitionFiles' content to\n          // existing PartitionFilesMap.\n          for ((partitionHashCode, numFilesDelta) <- inputPartitionFiles) {\n            assert(numFilesDelta != 0)\n            cachedPartitionStates.get(partitionHashCode) match {\n              case Some(partitionState) =>\n                // If there is an entry of partitionHashCode, updates its number of files\n                // and moves it to the most recently used slot.\n                partitionState.numFiles += numFilesDelta\n                moveAccessedPartitionToMru(cachedPartitionStates, partitionHashCode, partitionState)\n              case None =>\n                if (numFilesDelta > 0) {\n                  // New table partition is always in the most recently used entry.\n                  cachedPartitionStates.put(partitionHashCode, new PartitionStat(numFilesDelta))\n                  numUsedPartitions += 1\n                }\n            }\n          }\n          // Move the accessed table to MRU position and evicts the LRU partitions from it\n          // if necessary.\n          moveAccessedTableToMru(tableId, cachedPartitionStates)\n        case None =>\n          // If it is new table, just create new entry.\n          val newPartitionStates = inputPartitionFiles\n            .filter { case (_, numFiles) => numFiles > 0 }\n            .map { case (partitionHashCode, numFiles) =>\n              (partitionHashCode, new PartitionStat(numFiles))\n            }\n          tablePartitionStatsCache.put(tableId, newPartitionStates)\n          numUsedPartitions += newPartitionStates.size\n          moveAccessedTableToMru(tableId, newPartitionStates)\n      }\n      evictLruTablesIfNecessary()\n    }\n  }\n\n  /** Move the accessed table partition to the most recently used position. */\n  private def moveAccessedPartitionToMru(\n      cachedPartitionFiles: TablePartitionStats,\n      partitionHashCode: Int,\n      partitionState: PartitionStat): Unit = {\n    cachedPartitionFiles.remove(partitionHashCode)\n    if (partitionState.numFiles <= 0) {\n      numUsedPartitions -= 1\n    } else {\n      // If the newNumFiles is not empty, add it back and make it to be the\n      // most recently used entry.\n      cachedPartitionFiles.put(partitionHashCode, partitionState)\n    }\n  }\n\n  /** Move the accessed table to the most recently used position. */\n  private def moveAccessedTableToMru(\n      tableId: String,\n      cachedPartitionFiles: TablePartitionStats): Unit = {\n    // The tablePartitionStatsCache is insertion order preserved hash table. Thus,\n    // removing and adding back the entry make this to be most recently used entry.\n    // If cachedPartitionFiles's size is empty, no need to add it back to LRU.\n    tablePartitionStatsCache.remove(tableId)\n    numUsedPartitions -= cachedPartitionFiles.size\n    if (cachedPartitionFiles.nonEmpty) {\n      // Evict the least recently used partitions' statistics from table if necessary\n      val numExceededPartitions = cachedPartitionFiles.size - maxNumTablePartitions\n      if (numExceededPartitions > 0) {\n        val newPartitionStats = cachedPartitionFiles.drop(numExceededPartitions)\n        tablePartitionStatsCache.put(tableId, newPartitionStats)\n        numUsedPartitions += newPartitionStats.size\n      } else {\n        tablePartitionStatsCache.put(tableId, cachedPartitionFiles)\n        numUsedPartitions += cachedPartitionFiles.size\n      }\n    }\n  }\n\n  /**\n   * Evicts the Lru tables from 'tablePartitionStatsCache' until the total number of partitions\n   * is less than maxNumPartitions.\n   */\n  private def evictLruTablesIfNecessary(): Unit = {\n    // Keep removing the least recently used table until the used partition is lower than\n    // threshold.\n    while (numUsedPartitions > maxNumPartitions && tablePartitionStatsCache.nonEmpty) {\n      // Pick the least recently accessed table and remove it.\n      val (lruTable, tablePartitionStat) = tablePartitionStatsCache.head\n      numUsedPartitions -= tablePartitionStat.size\n      tablePartitionStatsCache.remove(lruTable)\n    }\n  }\n\n  /** Update the file count of `PartitionFilesMap` according to the hash value of `partition`. */\n  private def updatePartitionFileCounter(\n      partitionFileCounter: PartitionFilesMap,\n      partition: PartitionKey,\n      addSub: Int): Unit = {\n    partitionFileCounter.get(partition.##) match {\n      case Some(numFiles) =>\n        partitionFileCounter.update(partition.##, numFiles + addSub)\n      case None =>\n        partitionFileCounter.put(partition.##, addSub)\n    }\n  }\n\n  /** Get the maximum number of files among all partitions inside table `tableId`. */\n  def maxNumFilesInTable(tableId: String): Long = {\n    synchronized {\n      tablePartitionStatsCache.get(tableId) match {\n        case Some(partitionFileCounter) =>\n          if (partitionFileCounter.isEmpty) {\n            0\n          } else {\n            partitionFileCounter.map(_._2.numFiles).max\n          }\n        case None => 0\n      }\n    }\n  }\n\n  /**\n   * @return Filter partitions from targetPartitions that have not been auto-compacted or\n   *         that have enough small files.\n   */\n  def filterPartitionsWithSmallFiles(tableId: String, targetPartitions: Set[PartitionKey],\n      minNumFiles: Long): Set[PartitionKey] = synchronized {\n    tablePartitionStatsCache.get(tableId).map { tablePartitionStates =>\n      targetPartitions.filter { partitionKey =>\n        tablePartitionStates.get(partitionKey.##).exists { partitionState =>\n          partitionState.hasSufficientSmallFilesOrHasNotBeenCompacted(minNumFiles)\n        }\n      }\n    }.getOrElse(Set.empty)\n  }\n\n  def markPartitionsAsCompacted(tableId: String, compactedPartitions: Set[PartitionKey])\n  : Unit = synchronized {\n    tablePartitionStatsCache.get(tableId).foreach { tablePartitionStats =>\n      compactedPartitions\n        .foreach(partitionKey => tablePartitionStats.get(partitionKey.##)\n          .foreach(_.wasAutoCompacted = true))\n    }\n  }\n\n  /**\n   * Collect the number of files, which are less than minFileSize, added to or removed from each\n   * partition from `actions`.\n   * The stats collection is only complete when:\n   *  1. The returned iterator has been consumed AND\n   *  2. finalizeStats has been called on the collector.\n   */\n  def collectPartitionStats(\n      collector: AutoCompactPartitionStatsCollector,\n      actions: Iterator[Action]): Iterator[Action] = {\n    actions.map { action =>\n      action match {\n        case addFile: AddFile => collector.collectPartitionStatsForAdd(addFile)\n        case removeFile: RemoveFile => collector.collectPartitionStatsForRemove(removeFile)\n        case _ => // do nothing\n      }\n      action\n    }\n  }\n\n  /** This is test only code to reset the state of table partition statistics. */\n  private[delta] def resetTestOnly(newHashSpace: Int, newMaxNumPartitions: Int): Unit = {\n    synchronized {\n      tablePartitionStatsCache.clear()\n      maxNumTablePartitions = newHashSpace\n      maxNumPartitions = newMaxNumPartitions\n      numUsedPartitions = 0\n      AutoCompactPartitionReserve.resetTestOnly()\n    }\n  }\n\n  /**\n   * This is test only code to reset all partition statistic information and keep current\n   * configuration.\n   */\n  private[delta] def resetTestOnly(): Unit = resetTestOnly(maxNumTablePartitions, maxNumPartitions)\n}\n\nobject AutoCompactPartitionStats {\n  private var _instance: AutoCompactPartitionStats = null\n\n  /** The thread safe constructor of singleton. */\n  def instance(spark: SparkSession): AutoCompactPartitionStats = {\n    synchronized {\n      if (_instance == null) {\n        val config = spark.conf\n        val hashSpaceSize = config.get(DeltaSQLConf.DELTA_AUTO_COMPACT_MAX_TABLE_PARTITION_STATS)\n        val maxNumPartitions = config.get(DeltaSQLConf.DELTA_AUTO_COMPACT_PARTITION_STATS_SIZE)\n        _instance = new AutoCompactPartitionStats(\n          hashSpaceSize, maxNumPartitions\n        )\n      }\n    }\n    _instance\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DataSkippingPredicateBuilder.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.stats.DeltaStatistics.{MAX, MIN}\n\nimport org.apache.spark.sql.Column\n\n/**\n * A trait that defines interfaces for a data skipping predicate builder.\n *\n * Note that 'IsNull', 'IsNotNull' and 'StartsWith' are handled at a column (not expression) level\n * within [[DataSkippingReaderBase.DataFiltersBuilder.constructDataFilters]].\n *\n * Note that the 'value' passed in for each of the interface should be [[SkippingEligibleLiteral]].\n */\nprivate [sql] trait DataSkippingPredicateBuilder {\n  /** The predicate should match any file which contains the requested point. */\n  def equalTo(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate]\n\n  /** The predicate should match any file which contains anything other than the rejected point. */\n  def notEqualTo(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate]\n\n  /**\n   * The predicate should match any file which contains values less than the requested upper bound.\n   */\n  def lessThan(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate]\n\n  /**\n   * The predicate should match any file which contains values less than or equal to the requested\n   * upper bound.\n   */\n  def lessThanOrEqual(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate]\n\n  /**\n   * The predicate should match any file which contains values larger than the requested lower\n   * bound.\n   */\n  def greaterThan(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate]\n\n  /**\n   * The predicate should match any file which contains values larger than or equal to the requested\n   * lower bound.\n   */\n  def greaterThanOrEqual(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate]\n}\n\n/**\n * A collection of supported data skipping predicate builders.\n */\nobject DataSkippingPredicateBuilder {\n  /** Predicate builder for skipping eligible columns. */\n  case object ColumnBuilder extends ColumnPredicateBuilder\n}\n\n/**\n * Predicate builder for skipping eligible columns.\n */\nprivate [stats] class ColumnPredicateBuilder extends DataSkippingPredicateBuilder {\n  def equalTo(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate] = {\n    statsProvider.getPredicateWithStatTypesIfExists(colPath, value.expr.dataType, MIN, MAX) {\n      (min, max) => min <= value && value <= max\n    }\n  }\n\n  def notEqualTo(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate] = {\n    statsProvider.getPredicateWithStatTypesIfExists(colPath, value.expr.dataType, MIN, MAX) {\n      (min, max) => min < value || value < max\n    }\n  }\n\n  def lessThan(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate] =\n    statsProvider.getPredicateWithStatTypeIfExists(colPath, value.expr.dataType, MIN)(_ < value)\n\n  def lessThanOrEqual(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate] =\n    statsProvider.getPredicateWithStatTypeIfExists(colPath, value.expr.dataType, MIN)(_ <= value)\n\n  def greaterThan(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate] =\n    statsProvider.getPredicateWithStatTypeIfExists(colPath, value.expr.dataType, MAX)(_ > value)\n\n  def greaterThanOrEqual(statsProvider: StatsProvider, colPath: Seq[String], value: Column)\n    : Option[DataSkippingPredicate] =\n    statsProvider.getPredicateWithStatTypeIfExists(colPath, value.expr.dataType, MAX)(_ >= value)\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DataSkippingReader.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.Closeable\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaLog, DeltaTableUtils}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata}\nimport org.apache.spark.sql.delta.expressions.DecodeNestedZ85EncodedVariant\nimport org.apache.spark.sql.delta.implicits._\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeltaDataSkippingType.DeltaDataSkippingType\nimport org.apache.spark.sql.delta.stats.DeltaStatistics._\nimport org.apache.spark.sql.delta.util.StateCache\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{DataFrame, _}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.Literal.{FalseLiteral, TrueLiteral}\nimport org.apache.spark.sql.catalyst.util.TypeUtils\nimport org.apache.spark.sql.execution.InSubqueryExec\nimport org.apache.spark.sql.execution.datasources.VariantMetadata\nimport org.apache.spark.sql.expressions.SparkUserDefinedFunction\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{AtomicType, BooleanType, CalendarIntervalType, DataType, DateType, LongType, NumericType, StringType, StructField, StructType, TimestampNTZType, TimestampType, VariantType}\nimport org.apache.spark.unsafe.types.{CalendarInterval, UTF8String}\n\n/**\n * Used to hold the list of files and scan stats after pruning files using the limit.\n */\ncase class ScanAfterLimit(\n    files: Seq[AddFile],\n    byteSize: Option[Long],\n    numPhysicalRecords: Option[Long],\n    numLogicalRecords: Option[Long])\n\n/**\n * Used in deduplicateAndFilterRemovedLocally/getFilesAndNumRecords iterator for grouping\n * physical and logical number of records.\n *\n * @param numPhysicalRecords The number of records physically present in the file.\n * @param numLogicalRecords The physical number of records minus the Deletion Vector cardinality.\n */\ncase class NumRecords(numPhysicalRecords: java.lang.Long, numLogicalRecords: java.lang.Long)\n\n/**\n * Represents a stats column (MIN, MAX, etc) for a given (nested) user table column name. Used to\n * keep track of which stats columns a data skipping query depends on.\n *\n * The `pathToStatType` is path to a stats type accepted by `getStatsColumnOpt()`\n *  (see object `DeltaStatistics`);\n * `pathToColumn` is the nested name of the user column whose stats are to be accessed.\n * `columnDataType` is the data type of the column.\n */\nprivate[stats] case class StatsColumn private(\n    pathToStatType: Seq[String],\n    pathToColumn: Seq[String])\n\nobject StatsColumn {\n  def apply(statType: String, pathToColumn: Seq[String], columnDataType: DataType): StatsColumn = {\n    StatsColumn(Seq(statType), pathToColumn)\n  }\n}\n\n/**\n * A data skipping predicate, which includes the expression itself, plus the set of stats columns\n * that expression depends on. The latter is required to correctly handle missing stats, which would\n * make the predicate unreliable; for details, see `DataSkippingReader.verifyStatsForFilter`.\n *\n * NOTE: It would be more accurate to call these \"file keeping\" predicates, because they specify the\n * set of files a query must examine, not the set of rows a query can safely skip.\n */\nprivate [sql] case class DataSkippingPredicate(\n    expr: Column,\n    referencedStats: Set[StatsColumn]\n)\n\n/**\n * Overloads the constructor for `DataSkippingPredicate`, allowing callers to pass referenced stats\n * as individual arguments, rather than wrapped up as a Set.\n *\n * For example, instead of this:\n *\n *   DataSkippingPredicate(pred, Set(stat1, stat2))\n *\n * We can just do:\n *\n *   DataSkippingPredicate(pred, stat1, stat2)\n */\nprivate [sql] object DataSkippingPredicate {\n  def apply(filters: Column, referencedStats: StatsColumn*): DataSkippingPredicate = {\n    DataSkippingPredicate(filters, referencedStats.toSet)\n  }\n}\n\n/**\n * An extractor that matches on access of a skipping-eligible column. We only collect stats for leaf\n * columns, so internal columns of nested types are ineligible for skipping.\n *\n * NOTE: This check is sufficient for safe use of NULL_COUNT stats, but safe use of MIN and MAX\n * stats requires additional restrictions on column data type (see SkippingEligibleLiteral).\n *\n * @return The path to the column and the column's data type if it exists and is eligible.\n *         Otherwise, return None.\n */\nobject SkippingEligibleColumn {\n  def unapply(arg: Expression): Option[(Seq[String], DataType)] = {\n    // Only atomic types are eligible for skipping, and args should always be resolved by now.\n    // When `pushVariantIntoScan` is true, Variants in the read schema are transformed into Structs\n    // to facilitate shredded reads. Therefore, filters like `v is not null` where `v` is a variant\n    // column look like the filters on struct data. `VariantMetadata.isVariantStruct` helps in\n    // distinguishing between \"true structs\" and \"variant structs\".\n    val eligible = arg.resolved && (arg.dataType.isInstanceOf[AtomicType] ||\n      VariantMetadata.isVariantStruct(arg.dataType))\n    if (eligible) searchChain(arg).map(_ -> arg.dataType) else None\n  }\n\n  private def searchChain(arg: Expression): Option[Seq[String]] = arg match {\n    case a: Attribute => Some(a.name :: Nil)\n    case GetStructField(child, _, Some(name)) =>\n      searchChain(child).map(name +: _)\n    case g @ GetStructField(child, ord, None) if g.resolved =>\n      searchChain(child).map(g.childSchema(ord).name +: _)\n    case _ =>\n      None\n  }\n}\n\n/**\n * An extractor that matches on access of a skipping-eligible Literal. Delta tables track min/max\n * stats for a limited set of data types, and only Literals of those types are skipping-eligible.\n *\n * @return The Literal, if it is eligible. Otherwise, return None.\n */\nobject SkippingEligibleLiteral {\n  def unapply(arg: Literal): Option[Column] = {\n    if (SkippingEligibleDataType(arg.dataType)) Some(Column(arg)) else None\n  }\n}\n\nobject SkippingEligibleDataType {\n  // Call this directly, e.g. `SkippingEligibleDataType(dataType)`\n  def apply(dataType: DataType): Boolean = dataType match {\n    case _: NumericType | DateType | TimestampType | TimestampNTZType | StringType => true\n    case _: VariantType =>\n      SQLConf.get.getConf(DeltaSQLConf.COLLECT_VARIANT_DATA_SKIPPING_STATS)\n    case _ => false\n  }\n\n  // Use these in `match` statements\n  def unapply(dataType: DataType): Option[DataType] = {\n    if (SkippingEligibleDataType(dataType)) Some(dataType) else None\n  }\n\n  def unapply(f: StructField): Option[DataType] = unapply(f.dataType)\n}\n\n/**\n * An extractor that matches expressions that are eligible for data skipping predicates.\n *\n * @return A tuple of 1) column name referenced in the expression, 2) date type for the\n *         expression, 3) [[DataSkippingPredicateBuilder]] that builds the data skipping\n *         predicate for the expression, if the given expression is eligible.\n *         Otherwise, return None.\n */\nabstract class GenericSkippingEligibleExpression() {\n\n  def unapply(arg: Expression): Option[(Seq[String], DataType, DataSkippingPredicateBuilder)] = {\n    arg match {\n      case SkippingEligibleColumn(c, dt) =>\n        Some((c, dt, DataSkippingPredicateBuilder.ColumnBuilder))\n      case _ => None\n    }\n  }\n}\n\n/**\n * This object is used to avoid referencing DataSkippingReader in DetlaConfig.\n * Otherwise, it might cause the cyclic import through SQLConf -> SparkSession -> DetlaConfig.\n */\nprivate[delta] object DataSkippingReaderConf {\n\n  /**\n   * Default number of cols for which we should collect stats\n   */\n  val DATA_SKIPPING_NUM_INDEXED_COLS_DEFAULT_VALUE = 32\n}\n\nprivate[delta] object DataSkippingReader {\n\n  private[this] def col(e: Expression): Column = Column(e)\n  def fold(e: Expression): Column = col(new Literal(e.eval(), e.dataType))\n\n  // Literals often used in the data skipping reader expressions.\n  val trueLiteral: Column = col(TrueLiteral)\n  val falseLiteral: Column = col(FalseLiteral)\n  val nullStringLiteral: Column = col(new Literal(null, StringType))\n  val nullBooleanLiteral: Column = col(new Literal(null, BooleanType))\n  val oneMillisecondLiteralExpr: Literal = {\n    val oneMillisecond = new CalendarInterval(0, 0, 1000 /* micros */)\n    new Literal(oneMillisecond, CalendarIntervalType)\n  }\n\n  lazy val sizeCollectorInputEncoders: Seq[Option[ExpressionEncoder[_]]] = Seq(\n    Option(ExpressionEncoder[Boolean]()),\n    Option(ExpressionEncoder[java.lang.Long]()),\n    Option(ExpressionEncoder[java.lang.Long]()),\n    Option(ExpressionEncoder[java.lang.Long]()))\n\n  /**\n   * For timestamps, JSON serialization will truncate to milliseconds. This means\n   * that we must adjust 1 millisecond upwards for max stats, or we will incorrectly skip\n   * records that differ only in microsecond precision. (For example, a file containing only\n   * 01:02:03.456789 will be written with min == max == 01:02:03.456, so we must consider it\n   * to contain the range from 01:02:03.456 to 01:02:03.457.)\n   *\n   * To avoid overflow when the timestamp is near Long.MAX_VALUE, we check if adding 1\n   * millisecond would overflow. If so, we saturate to Long.MAX_VALUE to ensure the max stat\n   * is >= all actual values in the file while avoiding arithmetic overflow.\n   */\n  def getAdjustedTimestamp(col: Column, tsType: DataType): Column = {\n    val maxTimestampLiteral = Literal(Long.MaxValue, tsType)\n    val overflowThresholdLiteral = Literal(Long.MaxValue - 1000, tsType)\n    val adjustedExpr = If(\n      GreaterThan(col.expr, overflowThresholdLiteral),\n      maxTimestampLiteral,\n      TimestampAdd(\"MILLISECOND\", Literal(1L, LongType), col.expr))\n    Column(Cast(adjustedExpr, tsType))\n  }\n}\n\n/**\n * Adds the ability to use statistics to filter the set of files based on predicates\n * to a [[org.apache.spark.sql.delta.Snapshot]] of a given Delta table.\n */\ntrait DataSkippingReaderBase\n  extends DeltaScanGenerator\n  with StatisticsCollection\n  with ReadsMetadataFields\n  with StateCache\n  with DeltaLogging {\n\n  import DataSkippingReader._\n\n  def allFiles: Dataset[AddFile]\n  def path: Path\n  def version: Long\n  def metadata: Metadata\n  private[delta] def sizeInBytesIfKnown: Option[Long]\n  def deltaLog: DeltaLog\n  def schema: StructType\n  private[delta] def numOfFilesIfKnown: Option[Long]\n  def redactedPath: String\n\n  private def useStats = spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_STATS_SKIPPING)\n\n  private lazy val limitPartitionLikeFiltersToClusteringColumns = spark.sessionState.conf.getConf(\n    DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_CLUSTERING_COLUMNS_ONLY)\n  private lazy val additionalPartitionLikeFilterSupportedExpressions =\n    spark.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ADDITIONAL_SUPPORTED_EXPRESSIONS)\n      .toSet.flatMap((exprs: String) => exprs.split(\",\"))\n\n  /** Returns a DataFrame expression to obtain a list of files with parsed statistics. */\n  private def withStatsInternal0: DataFrame = {\n    val parsedStats = from_json(col(\"stats\"), statsSchema)\n    // Only use DecodeNestedZ85EncodedVariant if the schema contains VariantType.\n    // This avoids performance overhead for tables without variant columns.\n    // `DecodeNestedZ85EncodedVariant` is a temporary workaround since the Spark 4.1 from_json\n    // expression has no way to decode a VariantVal from an encoded Z85 string.\n    // TODO: Add Z85 decoding to Variant in Spark 4.2 and use that from_json option here.\n    val decodedStats = if (SchemaUtils.checkForVariantTypeColumnsRecursively(statsSchema)) {\n      Column(DecodeNestedZ85EncodedVariant(parsedStats.expr))\n    } else {\n      parsedStats\n    }\n    allFiles.withColumn(\"stats\", decodedStats)\n  }\n\n  private lazy val withStatsCache =\n    cacheDS(withStatsInternal0, s\"Delta Table State with Stats #$version - $redactedPath\")\n\n  protected def withStatsInternal: DataFrame = withStatsCache.getDS\n\n  /** All files with the statistics column dropped completely. */\n  def withNoStats: DataFrame = allFiles.drop(\"stats\")\n\n  /**\n   * Returns a parsed and cached representation of files with statistics.\n   *\n   *\n   * @return [[DataFrame]]\n   */\n  final def withStats: DataFrame = {\n    withStatsInternal\n  }\n\n  /**\n   * Constructs a [[DataSkippingPredicate]] for isNotNull predicates.\n   */\n   protected def constructNotNullFilter(\n      statsProvider: StatsProvider,\n      pathToColumn: Seq[String]): Option[DataSkippingPredicate] = {\n    val nullCountCol = StatsColumn(NULL_COUNT, pathToColumn, LongType)\n    val numRecordsCol = StatsColumn(NUM_RECORDS, pathToColumn = Nil, LongType)\n    statsProvider.getPredicateWithStatsColumnsIfExists(nullCountCol, numRecordsCol) {\n      (nullCount, numRecords) => nullCount < numRecords\n    }\n  }\n\n  def withStatsDeduplicated: DataFrame = withStats\n\n  /**\n   * Builds the data filters for data skipping.\n   */\n  class DataFiltersBuilder(\n      protected val spark: SparkSession,\n      protected val dataSkippingType: DeltaDataSkippingType)\n  {\n    protected val statsProvider: StatsProvider = new StatsProvider(getStatsColumnOpt)\n\n    object SkippingEligibleExpression extends GenericSkippingEligibleExpression()\n\n    // Main function for building data filters.\n    def apply(dataFilter: Expression): Option[DataSkippingPredicate] =\n      constructDataFilters(dataFilter, isNullExpansionDepth = 0)\n\n    /**\n     * Helper function to construct a [[DataSkippingPredicate]] for an IsNull predicate on\n     * null-intolerant expressions that are guaranteed to return non-null results for non-null\n     * inputs. This method is only valid *if and only if* the passed-in expression returns null\n     * for any null children. That is, if all children are non-null, the expression *must* return\n     * a non-null result.\n     * @param expr Expression to push down IsNull into.\n     * @return A [[DataSkippingPredicate]] that's the result of pushing IsNull down into expr's\n     *         children.\n     */\n    protected def constructIsNullFilterForNullIntolerant(\n        expr: Expression,\n        isNullExpansionDepth: Int): Option[DataSkippingPredicate] = {\n      val filters = expr.children.map {\n        // Resolve literal children directly. constructDataFilters does not support skipping on\n        // literal-only children.\n        case l: Literal =>\n          if (l.value == null) {\n            Some(DataSkippingPredicate(trueLiteral))\n          } else {\n            Some(DataSkippingPredicate(falseLiteral))\n          }\n        case c => constructDataFilters(IsNull(c), isNullExpansionDepth)\n      }\n      filters.reduceOption { (a, b) =>\n        (a, b) match {\n          case (Some(a), Some(b)) =>\n            Some(DataSkippingPredicate(a.expr || b.expr, a.referencedStats ++ b.referencedStats))\n          case _ => None\n        }\n      }.flatten\n    }\n\n    // Helper method for expression types that represent an IN-list of literal values.\n    //\n    //\n    // For excessively long IN-lists, we just test whether the file's min/max range overlaps the\n    // range spanned by the list's smallest and largest elements.\n    private def constructLiteralInListDataFilters(\n        a: Expression,\n        possiblyNullValues: Seq[Any],\n        isNullExpansionDepth: Int): Option[DataSkippingPredicate] = {\n      // The Ordering we use for sorting cannot handle null values, and these can anyway\n      // be safely ignored because they will never cause an IN-list predicate to return TRUE.\n      val values = possiblyNullValues.filter(_ != null)\n      if (values.isEmpty) {\n        // Handle the trivial empty case even for otherwise ineligible types.\n        // NOTE: SQL forbids empty in-list, but InSubqueryExec could have an empty subquery result\n        // or IN-list may contain only NULLs.\n        return Some(DataSkippingPredicate(falseLiteral))\n      }\n\n      val (pathToColumn, dt, builder) = SkippingEligibleExpression.unapply(a).getOrElse {\n        // The expression is not eligible for skipping, and we can stop constructing data filters\n        // for the expression by simply returning None.\n        return None\n      }\n\n      lazy val ordering = TypeUtils.getInterpretedOrdering(dt)\n      if (!SkippingEligibleDataType(dt)) {\n        // Don't waste time building expressions for incompatible types\n        None\n      }\n      else {\n        // Emit filters for an imprecise range test that covers the entire entire list.\n        val min = Literal(values.min(ordering), dt)\n        val max = Literal(values.max(ordering), dt)\n        constructDataFilters(\n          And(GreaterThanOrEqual(max, a), LessThanOrEqual(min, a)), isNullExpansionDepth)\n      }\n    }\n\n    /**\n     * Returns a file skipping predicate expression, derived from the user query, which uses column\n     * statistics to prune away files that provably contain no rows the query cares about.\n     *\n     * Specifically, the filter extraction code must obey the following rules:\n     *\n     * 1. Given a query predicate `e`, `constructDataFilters(e)` must return TRUE for a file unless\n     *    we can prove `e` will not return TRUE for any row the file might contain. For example,\n     *    given `a = 3` and min/max stat values [0, 100], this skipping predicate is safe:\n     *\n     *      AND(minValues.a <= 3, maxValues.a >= 3)\n     *\n     *    Because that condition must be true for any file that might possibly contain `a = 3`; the\n     *    skipping predicate could return FALSE only if the max is too low, or the min too high; it\n     *    could return NULL only if a is NULL in every row of the file. In both latter cases, it is\n     *    safe to skip the file because `a = 3` can never evaluate to TRUE.\n     *\n     * 2. It is unsafe to apply skipping to operators that can evaluate to NULL or produce an error\n     *    for non-NULL inputs. For example, consider this query predicate involving integer\n     *    addition:\n     *\n     *      a + 1 = 3\n     *\n     *    It might be tempting to apply the standard equality skipping predicate:\n     *\n     *      AND(minValues.a + 1 <= 3, 3 <= maxValues.a + 1)\n     *\n     *    However, the skipping predicate would be unsound, because the addition operator could\n     *    trigger integer overflow (e.g. minValues.a = 0 and maxValues.a = INT_MAX), even though the\n     *    file could very well contain rows satisfying a + 1 = 3.\n     *\n     * 3. Predicates involving NOT are ineligible for skipping, because\n     *    `Not(constructDataFilters(e))` is seldom equivalent to `constructDataFilters(Not(e))`.\n     *    For example, consider the query predicate:\n     *\n     *      NOT(a = 1)\n     *\n     *    A simple inversion of the data skipping predicate would be:\n     *\n     *      NOT(AND(minValues.a <= 1, maxValues.a >= 1))\n     *      ==> OR(NOT(minValues.a <= 1), NOT(maxValues.a >= 1))\n     *      ==> OR(minValues.a > 1, maxValues.a < 1)\n     *\n     *    By contrast, if we first combine the NOT with = to obtain\n     *\n     *      a != 1\n     *\n     *    We get a different skipping predicate:\n     *\n     *      NOT(AND(minValues.a = 1, maxValues.a = 1))\n     *      ==> OR(NOT(minValues.a = 1), NOT(maxValues.a = 1))\n     *      ==>  OR(minValues.a != 1, maxValues.a != 1)\n     *\n     *    A truth table confirms that the first (naively inverted) skipping predicate is incorrect:\n     *\n     *      minValues.a\n     *      | maxValues.a\n     *      | | OR(minValues.a > 1, maxValues.a < 1)\n     *      | | | OR(minValues.a != 1, maxValues.a != 1)\n     *      0 0 T T\n     *      0 1 F T    !! first predicate wrongly skipped a = 0\n     *      1 1 F F\n     *\n     *    Fortunately, we may be able to eliminate NOT from some (branches of some) predicates:\n     *\n     *    a. It is safe to push the NOT into the children of AND and OR using de Morgan's Law, e.g.\n     *\n     *         NOT(AND(a, b)) ==> OR(NOT(a), NOT(B)).\n     *\n     *    b. It is safe to fold NOT into other operators, when a negated form of the operator\n     *       exists:\n     *\n     *         NOT(NOT(x)) ==> x\n     *         NOT(a == b) ==> a != b\n     *         NOT(a > b) ==> a <= b\n     *\n     * NOTE: The skipping predicate must handle the case where min and max stats for a column are\n     * both NULL -- which indicates that all values in the file are NULL. Fortunately, most of the\n     * operators we support data skipping for are NULL intolerant, and thus trivially satisfy this\n     * requirement because they never return TRUE for NULL inputs. The only NULL tolerant operator\n     * we support -- IS [NOT] NULL -- is specifically NULL aware. AND and OR are also considered\n     * null tolerant, and have special-cased handling of null pushdowns.\n     *\n     * NOTE: The skipping predicate does *NOT* need to worry about missing stats columns (which also\n     * manifest as NULL). That case is handled separately by `verifyStatsForFilter` (which disables\n     * skipping for any file that lacks the needed stats columns).\n     *\n     * @return An optional data skipping predicate, if this function returns None, then this means\n     * that the dataFilter Expression is not eligible for data skipping, i.e. we cannot skip any\n     * files.\n     */\n    private[stats] def constructDataFilters(\n        dataFilter: Expression,\n        isNullExpansionDepth: Integer): Option[DataSkippingPredicate] = dataFilter match {\n      // Expressions that contain only literals are not eligible for skipping.\n      case cmp: Expression if cmp.children.forall(areAllLeavesLiteral) => None\n\n      // Push skipping predicate generation through the AND:\n      //\n      // constructDataFilters(AND(a, b))\n      // ==> AND(constructDataFilters(a), constructDataFilters(b))\n      //\n      // To see why this transformation is safe, consider that `constructDataFilters(a)` must\n      // evaluate to TRUE *UNLESS* we can prove that `a` would not evaluate to TRUE for any row the\n      // file might contain. Thus, if the rewritten form of the skipping predicate does not evaluate\n      // to TRUE, at least one of the skipping predicates must not have evaluated to TRUE, which in\n      // turn means we were able to prove that `a` and/or `b` will not evaluate to TRUE for any row\n      // of the file. If that is the case, then `AND(a, b)` also cannot evaluate to TRUE for any row\n      // of the file, which proves we have a valid data skipping predicate.\n      //\n      // NOTE: AND is special -- we can safely skip the file if one leg does not evaluate to TRUE,\n      // even if we cannot construct a skipping filter for the other leg.\n      case And(e1, e2) =>\n        val e1Filter = constructDataFilters(e1, isNullExpansionDepth)\n        val e2Filter = constructDataFilters(e2, isNullExpansionDepth)\n        if (e1Filter.isDefined && e2Filter.isDefined) {\n          Some(DataSkippingPredicate(\n            e1Filter.get.expr && e2Filter.get.expr,\n            e1Filter.get.referencedStats ++ e2Filter.get.referencedStats))\n        } else if (e1Filter.isDefined) {\n          e1Filter\n        } else {\n          e2Filter  // possibly None\n        }\n\n      // Use deMorgan's law to push the NOT past the AND. This is safe even with SQL tri-valued\n      // logic (see below), and is desirable because we cannot generally push predicate filters\n      // through NOT, but we *CAN* push predicate filters through AND and OR:\n      //\n      // constructDataFilters(NOT(AND(a, b)))\n      // ==> constructDataFilters(OR(NOT(a), NOT(b)))\n      // ==> OR(constructDataFilters(NOT(a)), constructDataFilters(NOT(b)))\n      //\n      // Assuming we can push the resulting NOT operations all the way down to some leaf operation\n      // it can fold into, the rewrite allows us to create a data skipping filter from the\n      // expression.\n      //\n      // a b AND(a, b)\n      // | | | NOT(AND(a, b))\n      // | | | | OR(NOT(a), NOT(b))\n      // T T T F F\n      // T F F T T\n      // T N N N N\n      // F F F T T\n      // F N F T T\n      // N N N N N\n      case Not(And(e1, e2)) =>\n        constructDataFilters(Or(Not(e1), Not(e2)), isNullExpansionDepth)\n\n      // Push skipping predicate generation through OR (similar to AND case).\n      //\n      // constructDataFilters(OR(a, b))\n      // ==> OR(constructDataFilters(a), constructDataFilters(b))\n      //\n      // Similar to AND case, if the rewritten predicate does not evaluate to TRUE, then it means\n      // that neither `constructDataFilters(a)` nor `constructDataFilters(b)` evaluated to TRUE,\n      // which in turn means that neither `a` nor `b` could evaluate to TRUE for any row the file\n      // might contain, which proves we have a valid data skipping predicate.\n      //\n      // Unlike AND, a single leg of an OR expression provides no filtering power -- we can only\n      // reject a file if both legs evaluate to false.\n      case Or(e1, e2) =>\n        val e1Filter = constructDataFilters(e1, isNullExpansionDepth)\n        val e2Filter = constructDataFilters(e2, isNullExpansionDepth)\n        if (e1Filter.isDefined && e2Filter.isDefined) {\n          Some(DataSkippingPredicate(\n            e1Filter.get.expr || e2Filter.get.expr,\n            e1Filter.get.referencedStats ++ e2Filter.get.referencedStats))\n        } else {\n          None\n        }\n\n      // Similar to AND, we can (and want to) push the NOT past the OR using deMorgan's law.\n      case Not(Or(e1, e2)) =>\n        constructDataFilters(And(Not(e1), Not(e2)), isNullExpansionDepth)\n\n      // Match any file whose null count is larger than zero.\n      // Note DVs might result in a redundant read of a file.\n      // However, they cannot lead to a correctness issue.\n      case IsNull(SkippingEligibleColumn(a, dt)) =>\n        statsProvider.getPredicateWithStatTypeIfExists(a, dt, NULL_COUNT) { nullCount =>\n          nullCount > Literal(0L)\n        }\n      // For these null-intolerant expressions, any null input resolves into a null output. In\n      // addition, these expressions are special in that a null output is only possible if one of\n      // the inputs was a NULL. Push down the IsNull operator to all children and return a\n      // DataSkippingPredicate that's the Or of all child expressions.\n      case IsNull(e @ (_: GreaterThan | _: GreaterThanOrEqual | _: LessThan | _: LessThanOrEqual |\n          _: EqualTo | _: Not | _: StartsWith)) if spark.conf.get(\n            DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_ENABLED) =>\n        constructIsNullFilterForNullIntolerant(e, isNullExpansionDepth)\n      // And and Or necessitate custom pushdown logic for IsNull, as both expressions are not\n      // considered null intolerant. Note that since the child expressions are duplicated in the\n      // expanded expression, we need to track the depth of the expansion in this function's\n      // signature to avoid exponential growth of the expression tree if the child expressions are\n      // themselves And/Or expressions.\n      case IsNull(And(left, right)) if spark.conf.get(\n          DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_ENABLED) && (isNullExpansionDepth <=\n            spark.conf.get(DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_MAX_DEPTH)) =>\n        // The result of an AND is only Null if either operand is a Null, and the other operand is\n        // True.\n        constructDataFilters(\n          And(\n            Or(IsNull(left), IsNull(right)),\n            Not(\n              Or(\n                EqualNullSafe(left, FalseLiteral),\n                EqualNullSafe(right, FalseLiteral)\n              )\n            )\n          ),\n          isNullExpansionDepth = isNullExpansionDepth + 1\n        )\n      case IsNull(Or(left, right)) if spark.conf.get(\n          DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_ENABLED) && (isNullExpansionDepth <=\n            spark.conf.get(DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_MAX_DEPTH)) =>\n        // The result of an OR is only Null if either operand is a Null, _and_ neither operand is\n        // true.\n        constructDataFilters(\n          And(\n            Or(IsNull(left), IsNull(right)),\n            Not(\n              Or(\n                EqualNullSafe(left, TrueLiteral),\n                EqualNullSafe(right, TrueLiteral)\n              )\n            )\n          ),\n          isNullExpansionDepth = isNullExpansionDepth + 1\n        )\n      case Not(IsNull(e)) =>\n        constructDataFilters(IsNotNull(e), isNullExpansionDepth)\n\n      // Match any file whose null count is less than the row count.\n      case IsNotNull(SkippingEligibleColumn(a, _)) =>\n        constructNotNullFilter(statsProvider, a)\n\n      case Not(IsNotNull(e)) =>\n        constructDataFilters(IsNull(e), isNullExpansionDepth)\n\n      // Match any file whose min/max range contains the requested point.\n      case EqualTo(SkippingEligibleExpression(c, _, builder), SkippingEligibleLiteral(v)) =>\n        builder.equalTo(statsProvider, c, v)\n      case EqualTo(v: Literal, a) =>\n        constructDataFilters(EqualTo(a, v), isNullExpansionDepth)\n\n      // Match any file whose min/max range contains anything other than the rejected point.\n      case Not(EqualTo(SkippingEligibleExpression(c, _, builder), SkippingEligibleLiteral(v))) =>\n        builder.notEqualTo(statsProvider, c, v)\n      case Not(EqualTo(v: Literal, a)) =>\n        constructDataFilters(Not(EqualTo(a, v)), isNullExpansionDepth)\n\n      // Rewrite `EqualNullSafe(a, NotNullLiteral)` as\n      // `And(IsNotNull(a), EqualTo(a, NotNullLiteral))` and rewrite `EqualNullSafe(a, null)` as\n      // `IsNull(a)` to let the existing logic handle it.\n      case EqualNullSafe(a, v: Literal) =>\n        val rewrittenExpr = if (v.value != null) And(IsNotNull(a), EqualTo(a, v)) else IsNull(a)\n        constructDataFilters(rewrittenExpr, isNullExpansionDepth)\n      case EqualNullSafe(v: Literal, a) =>\n        constructDataFilters(EqualNullSafe(a, v), isNullExpansionDepth)\n      case Not(EqualNullSafe(a, v: Literal)) =>\n        val rewrittenExpr = if (v.value != null) And(IsNotNull(a), EqualTo(a, v)) else IsNull(a)\n        constructDataFilters(Not(rewrittenExpr), isNullExpansionDepth)\n      case Not(EqualNullSafe(v: Literal, a)) =>\n        constructDataFilters(Not(EqualNullSafe(a, v)), isNullExpansionDepth)\n\n      // Match any file whose min is less than the requested upper bound.\n      case LessThan(SkippingEligibleExpression(c, _, builder), SkippingEligibleLiteral(v)) =>\n        builder.lessThan(statsProvider, c, v)\n      case LessThan(v: Literal, a) =>\n        constructDataFilters(GreaterThan(a, v), isNullExpansionDepth)\n      case Not(LessThan(a, b)) =>\n        constructDataFilters(GreaterThanOrEqual(a, b), isNullExpansionDepth)\n\n      // Match any file whose min is less than or equal to the requested upper bound\n      case LessThanOrEqual(SkippingEligibleExpression(c, _, builder), SkippingEligibleLiteral(v)) =>\n        builder.lessThanOrEqual(statsProvider, c, v)\n      case LessThanOrEqual(v: Literal, a) =>\n        constructDataFilters(GreaterThanOrEqual(a, v), isNullExpansionDepth)\n      case Not(LessThanOrEqual(a, b)) =>\n        constructDataFilters(GreaterThan(a, b), isNullExpansionDepth)\n\n      // Match any file whose max is larger than the requested lower bound.\n      case GreaterThan(SkippingEligibleExpression(c, _, builder), SkippingEligibleLiteral(v)) =>\n        builder.greaterThan(statsProvider, c, v)\n      case GreaterThan(v: Literal, a) =>\n        constructDataFilters(LessThan(a, v), isNullExpansionDepth)\n      case Not(GreaterThan(a, b)) =>\n        constructDataFilters(LessThanOrEqual(a, b), isNullExpansionDepth)\n\n      // Match any file whose max is larger than or equal to the requested lower bound.\n      case GreaterThanOrEqual(\n      SkippingEligibleExpression(c, _, builder), SkippingEligibleLiteral(v)) =>\n        builder.greaterThanOrEqual(statsProvider, c, v)\n      case GreaterThanOrEqual(v: Literal, a) =>\n        constructDataFilters(LessThanOrEqual(a, v), isNullExpansionDepth)\n      case Not(GreaterThanOrEqual(a, b)) =>\n        constructDataFilters(LessThan(a, b), isNullExpansionDepth)\n\n      // Similar to an equality test, except comparing against a prefix of the min/max stats, and\n      // neither commutative nor invertible.\n      case StartsWith(SkippingEligibleColumn(a, _), v @ Literal(s: UTF8String, dt: StringType)) =>\n        statsProvider.getPredicateWithStatTypesIfExists(a, dt, MIN, MAX) { (min, max) =>\n          val sLen = s.numChars()\n          substring(min, 0, sLen) <= v && substring(max, 0, sLen) >= v\n        }\n\n      // We can only handle-IN lists whose values can all be statically evaluated to literals.\n      case in @ In(a, values) if in.inSetConvertible =>\n        constructLiteralInListDataFilters(\n          a, values.map(_.asInstanceOf[Literal].value), isNullExpansionDepth)\n\n      // The optimizer automatically converts all but the shortest eligible IN-lists to InSet.\n      case InSet(a, values) =>\n        constructLiteralInListDataFilters(a, values.toSeq, isNullExpansionDepth)\n\n      // Treat IN(... subquery ...) as a normal IN-list, since the subquery already ran before now.\n      case in: InSubqueryExec =>\n        // At this point the subquery has been materialized, but values() can return None if\n        // the subquery was bypassed at runtime.\n        in.values().flatMap(v =>\n          constructLiteralInListDataFilters(in.child, v.toSeq, isNullExpansionDepth))\n\n\n      // Remove redundant pairs of NOT\n      case Not(Not(e)) =>\n        constructDataFilters(e, isNullExpansionDepth)\n\n      // WARNING: NOT is dangerous, because `Not(constructDataFilters(e))` is seldom equivalent to\n      // `constructDataFilters(Not(e))`. We must special-case every `Not(e)` we wish to support.\n      case Not(_) => None\n\n      // Unknown expression type... can't use it for data skipping.\n      case _ => None\n    }\n\n    // Lightweight wrapper to represent a fully resolved reference to an attribute for\n    // partition-like data filters. Contains the min/max/null count stats column expressions and\n    // the referenced stats column for the attribute.\n    private case class ResolvedPartitionLikeReference(\n        referencedStatsCols: Seq[StatsColumn],\n        minExpr: Expression,\n        maxExpr: Expression,\n        nullCountExpr: Expression)\n\n    /**\n     * Whitelist of expressions that can be rewritten as partition-like.\n     * Set to a finite list to avoid having to silently introducing correctness issues as new\n     * expressions that violate the assumptions of partition-like skipping are introduced.\n     * There's no need to include [[SkippingEligibleColumn]] here - it's already handled explicitly.\n     *\n     * The following expressions have been intentionally excluded from the whitelist of supported\n     * expressions:\n     *  - [[AttributeReference]]: Any non-skipping eligible column references can't be rewritten as\n     *    partition-like.\n     *  - Any nondeterministic expression: The value returned while skipping might be different when\n     *    the expression is evaluated again. For example, rand() > 0.5 would return ~25% of records\n     *    if used in data skipping, while the user would expect ~50% of records to be returned.\n     *  - [[UserDefinedExpression]]: Often nondeterministic, and may have side effects when executed\n     *    multiple times.\n     *  - [[RegExpReplace]], [[RegExpExtractBase]], [[Like]], [[MultiLikeBase]], [[InvokeLike]], and\n     *    [[JsonToStructs]]: These expressions might be very expensive to evalute more than once.\n     */\n    private def shouldRewriteAsPartitionLike(expr: Expression): Boolean = expr match {\n      // Expressions supported by traditional data skipping.\n      // Boolean operators.\n      case _: Not | _: Or | _: And => true\n      // Comparison operators.\n      case _: EqualNullSafe | _: EqualTo | _: GreaterThan | _: GreaterThanOrEqual | _: IsNull |\n           _: IsNotNull | _: LessThan | _: LessThanOrEqual => true\n      // String and set operators. InSubqueryExec is explicitly handled by the caller.\n      case _: In | _: InSet | _: StartsWith => true\n      case _: Literal => true\n\n      // Expressions only supported for partition-like data skipping.\n      // Date and time conversions.\n      case _: ConvertTimezone | _: DateFormatClass | _: Extract | _: GetDateField |\n           _: GetTimeField | _: IntegralToTimestampBase | _: MakeDate | _: MakeTimestamp |\n           _: ParseToDate | _: ParseToTimestamp | _: ToTimestamp | _: TruncDate |\n           _: TruncTimestamp | _: UTCTimestamp => true\n      // Unix date and timestamp conversions.\n      case _: DateFromUnixDate | _: FromUnixTime | _: TimestampToLongBase | _: ToUnixTimestamp |\n           _: UnixDate | _: UnixTime | _: UnixTimestamp => true\n      // Date and time arithmetic.\n      case expr if DateTimeExpressionShims.isDateTimeArithmeticExpression(expr) => true\n      // String expressions.\n      case _: Base64 | _: BitLength | _: Chr | _: ConcatWs | _: Decode | _: Elt | _: Empty2Null |\n           _: Encode | _: FormatNumber | _: FormatString | _: ILike | _: InitCap | _: Left |\n           _: Length | _: Levenshtein | _: Luhncheck | _: OctetLength | _: Overlay | _: Right |\n           _: Sentences | _: SoundEx | _: SplitPart | _: String2StringExpression |\n           _: String2TrimExpression | _: StringDecode | _: StringInstr | _: StringLPad |\n           _: StringLocate | _: StringPredicate | _: StringRPad | _: StringRepeat |\n           _: StringReplace | _: StringSpace | _: StringSplit | _: StringSplitSQL |\n           _: StringTranslate | _: StringTrimBoth | _: Substring | _: SubstringIndex | _: ToBinary |\n           _: TryToBinary | _: UnBase64 => true\n      // Arithmetic expressions.\n      case _: Abs | _: BinaryArithmetic | _: Greatest | _: Least | _: UnaryMinus |\n           _: UnaryPositive => true\n      // Array expressions.\n      case _: ArrayBinaryLike | _: ArrayCompact | _: ArrayContains | _: ArrayInsert | _: ArrayJoin |\n           _: ArrayMax | _: ArrayMin | _: ArrayPosition | _: ArrayRemove | _: ArrayRepeat |\n           _: ArraySetLike | _: ArraySize | _: ArraysZip |\n           _: BinaryArrayExpressionWithImplicitCast | _: Concat | _: CreateArray | _: ElementAt |\n           _: Flatten | _: Get | _: GetArrayItem | _: GetArrayStructFields |\n           _: Reverse | _: Sequence | _: Size | _: Slice | _: SortArray | _: TryElementAt => true\n      // Map expressions.\n      case _: CreateMap | _: GetMapValue | _: MapConcat | _: MapContainsKey | _: MapEntries |\n           _: MapFromArrays | _: MapFromEntries | _: MapKeys | _: MapValues | _: StringToMap => true\n      // Struct expressions.\n      case _: CreateNamedStruct | _: DropField | _: GetStructField | _: UpdateFields |\n           _: WithField => true\n      // Hash expressions.\n      case _: Crc32 | _: HashExpression[_] | _: Md5 | _: Sha1 | _: Sha2 => true\n      // URL expressions.\n      case _: ParseUrl | _: UrlDecode | _: UrlEncode => true\n      // NULL expressions.\n      case _: AtLeastNNonNulls | _: Coalesce | _: IsNaN | _: NaNvl | _: NullIf | _: Nvl |\n           _: Nvl2 => true\n      // Cast expressions.\n      case _: Cast | _: UpCast => true\n      // Conditional expressions.\n      case _: If | _: CaseWhen => true\n      case _: Alias => true\n\n      // Don't attempt partition-like skipping on any unknown expressions: there's no way to\n      // guarantee it's safe to do so.\n      case _ => additionalPartitionLikeFilterSupportedExpressions.contains(\n        expr.getClass.getCanonicalName)\n    }\n\n    /**\n     * Rewrites the references in an expression to point to the collected stats over that column\n     * (if possible).\n     *\n     * This is generally equivalent to [[DeltaLog.rewritePartitionFilters]], with a few differences:\n     * 1. This method checks the eligibility of the column datatype before rewriting it to point to\n     *    the stats column (which isn't needed for partition columns).\n     * 2. There's no need to handle scalar subqueries (other than InSubqueryExec) here - subqueries\n     *    other than InSubqueryExec aren't eligible for data filtering.\n     * 3. AND expressions may be partially rewritten as partition-like data filters if one branch\n     *    is eligible but the other is not.\n     *\n     * For example:\n     *  CAST(a AS DATE) = '2024-09-11' -> CAST(parsed_stats[minValues][a] AS DATE) = '2024-09-11'\n     *\n     * @param expr    The expression to rewrite.\n     * @param clusteringColumnPaths The logical paths to the clustering columns in the table.\n     * @return        If the expression is safe to rewrite, return the rewritten expression and a\n     *                set of referenced attributes (with both the logical path to the column and the\n     *                column type).\n     */\n    private def rewriteDataFiltersAsPartitionLikeInternal(\n        expr: Expression,\n        clusteringColumnPaths: Set[Seq[String]])\n    : Option[(Expression, Set[ResolvedPartitionLikeReference])] = expr match {\n      // The expression is an eligible reference to an attribute.\n      // Do NOT allow partition-like filtering on timestamp columns because timestamps are truncated\n      // to millisecond precision, meaning that we can't guarantee that the collected minVal and\n      // maxVal are the same.\n      // Applying these partition-like filters will generally only be beneficial if a large\n      // percentage of files have the same min-max value. As a rough heuristic, only allow rewriting\n      // expressions that reference only the clustering columns (since these columns are more likely\n      // to have the same min-max values).\n      case SkippingEligibleColumn(c, SkippingEligibleDataType(dt))\n        if dt != TimestampType && dt != TimestampNTZType &&\n          (!limitPartitionLikeFiltersToClusteringColumns ||\n            clusteringColumnPaths.exists(SchemaUtils.areLogicalNamesEqual(_, c.reverse))) =>\n        // Only rewrite the expression if all stats are collected for this column.\n        val minStatsCol = StatsColumn(MIN, c, dt)\n        val maxStatsCol = StatsColumn(MAX, c, dt)\n        val nullCountStatsCol = StatsColumn(NULL_COUNT, c, dt)\n        for {\n          minCol <- getStatsColumnOpt(minStatsCol);\n          maxCol <- getStatsColumnOpt(maxStatsCol);\n          nullCol <- getStatsColumnOpt(nullCountStatsCol)\n        } yield {\n          val resolvedAttribute = ResolvedPartitionLikeReference(\n            Seq(minStatsCol, maxStatsCol, nullCountStatsCol),\n            minCol.expr,\n            maxCol.expr,\n            nullCol.expr)\n          (minCol.expr, Set(resolvedAttribute))\n        }\n      // For other attribute references, we can't safely rewrite the expression.\n      case SkippingEligibleColumn(_, _) => None\n      // Explicitly disallow rewriting nondeterministic expressions. Even though this check isn't\n      // strictly necessary (there shouldn't be any nondeterministic expressions in the whitelist),\n      // defensively keep it due to the extreme risk of correctness issues if any nondeterministic\n      // expressions sneak into the whitelist.\n      case other if !other.deterministic => None\n      // Inline subquery results to support InSet. The subquery should generally have already been\n      // evaluated.\n      case in: InSubqueryExec =>\n        // Values may not be defined if the subquery has been skipped - we can't apply this filter.\n        in.values().flatMap { possiblyNullValues =>\n          // Rewrite the children of InSubqueryExec, then replace the subquery with an InSet\n          // containing the materialized values.\n          rewriteDataFiltersAsPartitionLikeInternal(in.child, clusteringColumnPaths).flatMap {\n            case (rewrittenChildren, referencedStats) =>\n              Some(InSet(rewrittenChildren, possiblyNullValues.toSet), referencedStats)\n          }\n        }\n      // For all other eligible expressions, recursively rewrite the children.\n      case other if shouldRewriteAsPartitionLike(other) =>\n        val childResults = other.children.map(\n          rewriteDataFiltersAsPartitionLikeInternal(_, clusteringColumnPaths))\n        Option.whenNot (childResults.exists(_.isEmpty)) {\n          val (children, stats) = childResults.map(_.get).unzip\n          (other.withNewChildren(children), stats.flatten.toSet)\n        }\n      // Don't attempt rewriting any non-whitelisted expressions.\n      case _ => None\n    }\n\n    /**\n     * Returns an expression that returns true if a file must be read because of a mismatched\n     * min-max value or partial nulls on a given column. For these files, it's not safe to apply\n     * arbitrary partition-like filters.\n     */\n    private def fileMustBeScanned(\n        resolvedPartitionLikeReference: ResolvedPartitionLikeReference,\n        numRecordsColOpt: Option[Column]): Expression = {\n      // Construct an expression to determine if all records in the file are null.\n      val nullCountExpr = resolvedPartitionLikeReference.nullCountExpr\n      val allNulls = numRecordsColOpt match {\n        case Some(physicalNumRecords) => EqualTo(nullCountExpr, physicalNumRecords.expr)\n        case _ => Literal(false)\n      }\n\n      // Note that there are 2 other differences in behavior between unpartitioned and partitioned\n      // tables:\n      // 1. If the column is a timestamp, the min-max stats are truncated to millisecond precision.\n      //    We shouldn't apply partition-like filters in this case, but\n      //    rewriteDataFiltersAsPartitionLikeInternal validates the column is not a Timestamp,\n      //    so we don't have to check here.\n      // 2. The min-max stats on a string column might be truncated for an unpartitioned table.\n      //    Note that just validating that the min and max are equal is enough to prevent this case\n      //    - if the string is truncated, the collected max value is guaranteed to be longer than\n      //    the min value due to the tiebreaker character(s) appended at the end of the max.\n      Not(\n        Or(\n          allNulls,\n          And(\n            EqualTo(\n              resolvedPartitionLikeReference.minExpr, resolvedPartitionLikeReference.maxExpr),\n            EqualTo(resolvedPartitionLikeReference.nullCountExpr, Literal(0L))\n          )\n        )\n      )\n    }\n\n    /**\n     * Rewrites the given expression as a partition-like expression if possible:\n     * 1. Rewrite the attribute references in the expression to reference the collected min stats\n     *     on the attribute reference's column.\n     * 2. Construct an expression that returns true if any of the referenced columns are not\n     *     partition-like on a given file.\n     * The rewritten expression is a union of the above expressions: a file is read if it's either\n     * not partition-like on any of the columns or if the rewritten expression evaluates to true.\n     *\n     * @param clusteringColumns   The columns that are used for clustering.\n     * @param expr                The data filtering expression to rewrite.\n     * @return                    If the expression is safe to rewrite, return the rewritten\n     *                            expression. Otherwise, return None.\n     */\n    def rewriteDataFiltersAsPartitionLike(\n        clusteringColumns: Seq[String], expr: Expression): Option[DataSkippingPredicate] = {\n      val clusteringColumnPaths =\n        clusteringColumns.map(UnresolvedAttribute.quotedString(_).nameParts).toSet\n      rewriteDataFiltersAsPartitionLikeInternal(expr, clusteringColumnPaths).map {\n        case (newExpr, referencedStats) =>\n          // Create an expression that returns true if a file must be read because it has mismatched\n          // min-max values or partial nulls on any of the referenced columns.\n          val numRecordsStatsCol = StatsColumn(NUM_RECORDS, pathToColumn = Nil, LongType)\n          val numRecordsColOpt = getStatsColumnOpt(numRecordsStatsCol)\n          val statsCols = referencedStats.flatMap(_.referencedStatsCols) + numRecordsStatsCol\n          val mustScanFileExpression = referencedStats.map { resolvedReference =>\n            fileMustBeScanned(resolvedReference, numRecordsColOpt)\n          }.toSeq.reduceLeftOption { (l, r) => Or(l, r) }.getOrElse(Literal(false))\n\n          // Only evaluate the rewritten expression if the file passes the validation expression,\n          // ensuring that any non-partition-like input (that might cause a filter evaluation\n          // exception) is skipped. Note that we cannot rely on short-circuiting here, since\n          // common subexpression elimination during codegen may move the evaluation of the\n          // condition before that of the file validation expression, so we need to explicitly use\n          // a conditional expression to guarantee the correct evaluation order.\n          val finalExpr = If(mustScanFileExpression, Literal(true), newExpr)\n\n          // Create the final data skipping expression - read a file either if it's has nulls on any\n          // referenced column, has mismatched stats on any referenced column, or the filter\n          // expression evaluates to `true`.\n          DataSkippingPredicate(Column(finalExpr), statsCols.toSet)\n      }\n    }\n\n    // We are doing the iterative approach because of stack depth concerns.\n    private[stats] def areAllLeavesLiteral(e: Expression): Boolean = {\n      val stack = scala.collection.mutable.Stack[Expression]()\n      def pushIfNonLiteral(e: Expression): Unit = e match {\n        case _: Literal =>\n        case _ => stack.push(e)\n      }\n      pushIfNonLiteral(e)\n      while (stack.nonEmpty) {\n        val children = stack.pop().children\n        if (children.isEmpty) {\n          return false\n        }\n        children.foreach(pushIfNonLiteral)\n      }\n      true\n    }\n  }\n\n  /**\n   * Returns an expression to access the given statistics for a specific column, or None if that\n   * stats column does not exist.\n   *\n   * @param pathToStatType Path components of one of the fields declared by the `DeltaStatistics`\n   *                       object. For statistics of collated strings, this path contains the\n   *                       versioned collation identifier. In all other cases the path only has one\n   *                       element. The path is in reverse order.\n   * @param pathToColumn The components of the nested column name to get stats for. The components\n   *                     are in reverse order.\n   */\n  final protected def getStatsColumnOpt(\n      pathToStatType: Seq[String], pathToColumn: Seq[String]): Option[Column] = {\n\n    require(pathToStatType.nonEmpty, \"No path to stats type provided.\")\n\n    // First validate that pathToStatType is a valid path in the statsSchema. We start at the root\n    // of the stats schema and then follow the path. Note that the path is stored in reverse order.\n    // If one of the path components does not exist, the foldRight operation returns None.\n    val (initialColumn, initialFieldType) = pathToStatType\n      .foldRight(Option((getBaseStatsColumn, statsSchema.asInstanceOf[DataType]))) {\n        case (statTypePathComponent: String, Some((column: Column, struct: StructType))) =>\n          // Find the field matching the current path component name or return None otherwise.\n          struct.fields.collectFirst {\n            case StructField(name, dataType: DataType, _, _) if name == statTypePathComponent =>\n              (column.getField(statTypePathComponent), dataType)\n          }\n        case _ => None\n      }\n      // If the requested stats type doesn't even exist, just return None right away. This can\n      // legitimately happen if we have no stats at all, or if column stats are disabled (in which\n      // case only the NUM_RECORDS stat type is available).\n      .getOrElse { return None }\n\n    // Given a set of path segments in reverse order, e.g. column a.b.c is Seq(\"c\", \"b\", \"a\"), we\n    // use a foldRight operation to build up the requested stats column, by successively applying\n    // each new path step against both the table schema and the stats schema. We can't use the stats\n    // schema alone, because the caller-provided path segments use logical column names, while the\n    // stats schema requires physical column names. Instead, we must step into the table schema to\n    // extract that field's physical column name, and use the result to step into the stats schema.\n    //\n    // We use a three-tuple to track state. The traversal starts with the base column for the\n    // requested stat type, the stats schema for the requested stat type, and the table schema. Each\n    // step of the traversal emits the updated column, along with the stats schema and table schema\n    // elements corresponding to that column.\n    val initialState: Option[(Column, DataType, DataType)] =\n      Some((initialColumn, initialFieldType, metadata.schema))\n    pathToColumn\n      .foldRight(initialState) {\n        // NOTE: Only match on StructType, because we cannot traverse through other DataTypes.\n        case (fieldName, Some((statCol, statsSchema: StructType, tableSchema: StructType))) =>\n          // First try to step into the table schema\n          val tableFieldOpt = tableSchema.findNestedFieldIgnoreCase(Seq(fieldName))\n\n          // If that worked, try to step into the stats schema, using its its physical name\n          val statsFieldOpt = tableFieldOpt\n            .map(DeltaColumnMapping.getPhysicalName)\n            .filter(physicalFieldName => statsSchema.exists(_.name == physicalFieldName))\n            .map(statsSchema(_))\n\n          // If all that succeeds, return the new stats column and the corresponding data types.\n          statsFieldOpt.map(statsField =>\n            (statCol.getField(statsField.name), statsField.dataType, tableFieldOpt.get.dataType))\n\n        // Propagate failure if the above match failed (or if already None)\n        case _ => None\n      }\n      // Filter out non-leaf columns -- they lack stats so skipping predicates can't use them.\n      .filterNot(_._2.isInstanceOf[StructType])\n      .map {\n        case (statCol, TimestampType, _) if pathToStatType.head == MAX =>\n          getAdjustedTimestamp(statCol, TimestampType)\n        case (statCol, TimestampNTZType, _) if pathToStatType.head == MAX =>\n          getAdjustedTimestamp(statCol, TimestampNTZType)\n        case (statCol, _, _) =>\n          statCol\n      }\n  }\n\n  /** Convenience overload for single element stat type paths. */\n  final protected def getStatsColumnOpt(\n      statType: String, pathToColumn: Seq[String] = Nil): Option[Column] =\n    getStatsColumnOpt(Seq(statType), pathToColumn)\n\n  /**\n   * Returns an expression to access the given statistics for a specific column, or a NULL\n   * literal expression if that column does not exist.\n   */\n  final protected[delta] def getStatsColumnOrNullLiteral(\n      statType: String,\n      pathToColumn: Seq[String] = Nil) : Column =\n    getStatsColumnOpt(Seq(statType), pathToColumn).getOrElse(lit(null))\n\n  /** Overload for convenience working with StatsColumn helpers */\n  final protected def getStatsColumnOpt(stat: StatsColumn): Option[Column] =\n    getStatsColumnOpt(stat.pathToStatType, stat.pathToColumn)\n\n  /** Overload for convenience working with StatsColumn helpers */\n  final protected[delta] def getStatsColumnOrNullLiteral(stat: StatsColumn): Column =\n    getStatsColumnOpt(stat.pathToStatType, stat.pathToColumn).getOrElse(lit(null))\n\n  /** Overload for delta table property override */\n  override protected def getDataSkippingStringPrefixLength: Int =\n    StatsCollectionUtils.getDataSkippingStringPrefixLength(spark, metadata)\n\n  /**\n   * Returns an expression that can be used to check that the required statistics are present for a\n   * given file. If any required statistics are missing we must include the corresponding file.\n   *\n   * NOTE: We intentionally choose to disable skipping for any file if any required stat is missing,\n   * because doing it that way allows us to check each stat only once (rather than once per\n   * use). Checking per-use would anyway only help for tables where the number of indexed columns\n   * has changed over time, producing add.stats_parsed records with differing schemas. That should\n   * be a rare enough case to not worry about optimizing for, given that the fix requires more\n   * complex skipping predicates that would penalize the common case.\n   */\n  protected def verifyStatsForFilter(referencedStats: Set[StatsColumn]): Column = {\n    recordFrameProfile(\"Delta\", \"DataSkippingReader.verifyStatsForFilter\") {\n      // The NULL checks for MIN and MAX stats depend on NULL_COUNT and NUM_RECORDS. Derive those\n      // implied dependencies first, so the main pass can treat them like any other column.\n      //\n      // NOTE: We must include explicit NULL checks on all stats columns we access here, because our\n      // caller will negate the expression we return. In case a stats column is NULL, `NOT(expr)`\n      // must return `TRUE`, and without these NULL checks it would instead return\n      // `NOT(NULL)` => `NULL`.\n      referencedStats.flatMap { stat => stat match {\n        case StatsColumn(MIN +: _, _) | StatsColumn(MAX +: _, _) =>\n          Seq(stat, StatsColumn(NULL_COUNT, stat.pathToColumn, LongType),\n            StatsColumn(NUM_RECORDS, pathToColumn = Nil, LongType))\n        case _ =>\n          Seq(stat)\n      }}.map{stat => stat match {\n        // A usable MIN or MAX stat must be non-NULL, unless the column is provably all-NULL\n        //\n        // NOTE: We don't care about NULL/missing NULL_COUNT and NUM_RECORDS here, because the\n        // separate NULL checks we emit for those columns will force the overall validation\n        // predicate conjunction to FALSE in that case -- AND(FALSE, <anything>) is FALSE.\n        case StatsColumn(MIN +: _, _) | StatsColumn(MAX +: _, _) =>\n          getStatsColumnOrNullLiteral(stat).isNotNull ||\n            (getStatsColumnOrNullLiteral(NULL_COUNT, stat.pathToColumn) ===\n              getStatsColumnOrNullLiteral(NUM_RECORDS))\n        case _ =>\n          // Other stats, such as NULL_COUNT and NUM_RECORDS stat, merely need to be non-NULL\n          getStatsColumnOrNullLiteral(stat).isNotNull\n      }}\n        .reduceLeftOption(_.and(_))\n        .getOrElse(trueLiteral)\n    }\n  }\n\n  private def buildSizeCollectorFilter(): (ArrayAccumulator, Column => Column) = {\n    val bytesCompressed = col(\"size\")\n    val rows = getStatsColumnOrNullLiteral(NUM_RECORDS)\n    val dvCardinality = coalesce(col(\"deletionVector.cardinality\"), lit(0L))\n    val logicalRows = (rows - dvCardinality).as(\"logicalRows\")\n\n    val accumulator = new ArrayAccumulator(4)\n\n    spark.sparkContext.register(accumulator)\n\n    // The arguments (order and datatype) must match the encoders defined in the\n    // `sizeCollectorInputEncoders` value.\n    val collector = (include: Boolean,\n                     bytesCompressed: java.lang.Long,\n                     logicalRows: java.lang.Long,\n                     rows: java.lang.Long) => {\n      if (include) {\n        accumulator.add((0, bytesCompressed)) /* count bytes of AddFiles */\n        accumulator.add((1, Option(rows).map(_.toLong).getOrElse(-1L))) /* count rows in AddFiles */\n        accumulator.add((2, 1)) /* count number of AddFiles */\n        accumulator.add((3, Option(logicalRows)\n          .map(_.toLong).getOrElse(-1L))) /* count logical rows in AddFiles */\n      }\n      include\n    }\n    val collectorUdf = SparkUserDefinedFunction(\n      f = collector,\n      dataType = BooleanType,\n      inputEncoders = sizeCollectorInputEncoders,\n      deterministic = false)\n\n    (accumulator, collectorUdf(_: Column, bytesCompressed, logicalRows, rows))\n  }\n\n  override def filesWithStatsForScan(partitionFilters: Seq[Expression]): DataFrame = {\n    DeltaLog.filterFileList(metadata.partitionSchema, withStats, partitionFilters)\n  }\n\n  /**\n   * Get all the files in this table.\n   *\n   * @param keepNumRecords Also select `stats.numRecords` in the query.\n   *                       This may slow down the query as it has to parse json.\n   */\n  protected def getAllFiles(keepNumRecords: Boolean): Seq[AddFile] = recordFrameProfile(\n      \"Delta\", \"DataSkippingReader.getAllFiles\") {\n    val ds = if (keepNumRecords) {\n      withStats // use withStats instead of allFiles so the `stats` column is already parsed\n        // keep only the numRecords field as a Json string in the stats field\n        .withColumn(\"stats\", to_json(struct(col(\"stats.numRecords\") as \"numRecords\")))\n    } else {\n      allFiles.withColumn(\"stats\", nullStringLiteral)\n    }\n    convertDataFrameToAddFiles(ds.toDF())\n  }\n\n  /**\n   * Given the partition filters on the data, rewrite these filters by pointing to the metadata\n   * columns.\n   */\n  protected def constructPartitionFilters(filters: Seq[Expression]): Column = {\n    recordFrameProfile(\"Delta\", \"DataSkippingReader.constructPartitionFilters\") {\n      val rewritten = DeltaLog.rewritePartitionFilters(\n        metadata.partitionSchema, spark.sessionState.conf.resolver, filters)\n      rewritten.reduceOption(And).map { expr => Column(expr) }.getOrElse(trueLiteral)\n    }\n  }\n\n  /**\n   * Get all the files in this table given the partition filter and the corresponding size of\n   * the scan.\n   *\n   * @param keepNumRecords Also select `stats.numRecords` in the query.\n   *                       This may slow down the query as it has to parse json.\n   */\n  protected def filterOnPartitions(\n      partitionFilters: Seq[Expression],\n      keepNumRecords: Boolean): (Seq[AddFile], DataSize) = recordFrameProfile(\n      \"Delta\", \"DataSkippingReader.filterOnPartitions\") {\n    val forceCollectRowCount =\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_ALWAYS_COLLECT_STATS)\n    val shouldCollectStats = keepNumRecords || forceCollectRowCount\n    val df = if (shouldCollectStats) {\n      // use withStats instead of allFiles so the `stats` column is already parsed\n      val filteredFiles =\n        DeltaLog.filterFileList(metadata.partitionSchema, withStats, partitionFilters)\n      filteredFiles\n        // keep only the numRecords field as a Json string in the stats field\n        .withColumn(\"stats\", to_json(struct(col(\"stats.numRecords\") as \"numRecords\")))\n    } else {\n      val filteredFiles =\n        DeltaLog.filterFileList(metadata.partitionSchema, allFiles.toDF(), partitionFilters)\n      filteredFiles\n        .withColumn(\"stats\", nullStringLiteral)\n    }\n    val files = convertDataFrameToAddFiles(df)\n    val sizeInBytesByPartitionFilters = files.map(_.size).sum\n    // Compute row count if we have stats available and forceCollectRowCount is enabled\n    val (rowCount, logicalRowCount) = if (forceCollectRowCount) {\n      sumRowCounts(files)\n    } else {\n      (None, None)\n    }\n    files.toSeq -> DataSize(Some(sizeInBytesByPartitionFilters), rowCount, Some(files.size),\n      logicalRowCount)\n  }\n\n  /**\n   * Sums up the numPhysicalRecords and numLogicalRecords from the given AddFile objects.\n   * Returns (None, None) if any file is missing physical record stats.\n   * Returns (Some(physical), None) if any file is missing logical record stats.\n   */\n  private def sumRowCounts(files: Seq[AddFile]): (Option[Long], Option[Long]) = {\n    var physicalRows = 0L\n    var logicalRows = 0L\n    var physicalMissing = false\n    var logicalMissing = false\n    files.foreach { file =>\n      physicalMissing = physicalMissing || file.numPhysicalRecords.isEmpty\n      logicalMissing = logicalMissing || file.numLogicalRecords.isEmpty\n      physicalRows += file.numPhysicalRecords.getOrElse(0L)\n      logicalRows += file.numLogicalRecords.getOrElse(0L)\n    }\n    (\n      if (physicalMissing) None else Some(physicalRows),\n      if (logicalMissing) None else Some(logicalRows)\n    )\n  }\n\n  /**\n   * Given the partition and data filters, leverage data skipping statistics to find the set of\n   * files that need to be queried. Returns a tuple of the files and optionally the size of the\n   * scan that's generated if there were no filters, if there were only partition filters, and\n   * combined effect of partition and data filters respectively.\n   */\n  protected def getDataSkippedFiles(\n      partitionFilters: Column,\n      dataFilters: DataSkippingPredicate,\n      keepNumRecords: Boolean): (Seq[AddFile], Seq[DataSize]) = recordFrameProfile(\n      \"Delta\", \"DataSkippingReader.getDataSkippedFiles\") {\n    val (totalSize, totalFilter) = buildSizeCollectorFilter()\n    val (partitionSize, partitionFilter) = buildSizeCollectorFilter()\n    val (scanSize, scanFilter) = buildSizeCollectorFilter()\n\n    // NOTE: If any stats are missing, the value of `dataFilters` is untrustworthy -- it could be\n    // NULL or even just plain incorrect. We rely on `verifyStatsForFilter` to be FALSE in that\n    // case, forcing the overall OR to evaluate as TRUE no matter what value `dataFilters` takes.\n    val filteredFiles = withStats.where(\n        totalFilter(trueLiteral) &&\n          partitionFilter(partitionFilters) &&\n          scanFilter(dataFilters.expr || !verifyStatsForFilter(dataFilters.referencedStats))\n      )\n\n    val statsColumn = if (keepNumRecords) {\n      // keep only the numRecords field as a Json string in the stats field\n      to_json(struct(col(\"stats.numRecords\") as \"numRecords\"))\n    } else nullStringLiteral\n\n    val files =\n      recordFrameProfile(\"Delta\", \"DataSkippingReader.getDataSkippedFiles.collectFiles\") {\n      val df = filteredFiles.withColumn(\"stats\", statsColumn)\n      convertDataFrameToAddFiles(df)\n    }\n    files.toSeq -> Seq(DataSize(totalSize), DataSize(partitionSize), DataSize(scanSize))\n  }\n\n  private def getCorrectDataSkippingType(\n      dataSkippingType: DeltaDataSkippingType): DeltaDataSkippingType = {\n    dataSkippingType\n  }\n\n  /**\n   * Gathers files that should be included in a scan based on the given predicates.\n   * Statistics about the amount of data that will be read are gathered and returned.\n   * Note, the statistics column that is added when keepNumRecords = true should NOT\n   * take into account DVs. Consumers of this method might commit the file. The semantics\n   * of the statistics need to be consistent across all files.\n   */\n  override def filesForScan(filters: Seq[Expression], keepNumRecords: Boolean): DeltaScan = {\n    val startTime = System.currentTimeMillis()\n    if (filters == Seq(TrueLiteral) || filters.isEmpty || schema.isEmpty) {\n      recordDeltaOperation(deltaLog, \"delta.skipping.none\") {\n        // When there are no filters we can just return allFiles with no extra processing\n        val forceCollectRowCount =\n          spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_ALWAYS_COLLECT_STATS)\n        val shouldCollectStats = keepNumRecords || forceCollectRowCount\n        lazy val files = getAllFiles(shouldCollectStats)\n        // Compute row count if forceCollectRowCount is enabled\n        val (rowCount, logicalRowCount) = if (forceCollectRowCount) {\n          sumRowCounts(files)\n        } else {\n          (None, None)\n        }\n        val dataSize = DataSize(\n          bytesCompressed = sizeInBytesIfKnown,\n          rows = rowCount,\n          files = numOfFilesIfKnown,\n          logicalRows = logicalRowCount)\n        return DeltaScan(\n          version = version,\n          files = files,\n          total = dataSize,\n          partition = dataSize,\n          scanned = dataSize)(\n          scannedSnapshot = snapshotToScan,\n          partitionFilters = ExpressionSet(Nil),\n          dataFilters = ExpressionSet(Nil),\n          partitionLikeDataFilters = ExpressionSet(Nil),\n          rewrittenPartitionLikeDataFilters = Set.empty,\n          unusedFilters = ExpressionSet(Nil),\n          scanDurationMs = System.currentTimeMillis() - startTime,\n          dataSkippingType = getCorrectDataSkippingType(DeltaDataSkippingType.noSkippingV1)\n        )\n      }\n    }\n\n    import DeltaTableUtils._\n    val partitionColumns = metadata.partitionColumns\n\n    // For data skipping, avoid using the filters that either:\n    // 1. involve subqueries.\n    // 2. are non-deterministic.\n    // 3. involve file metadata struct fields\n    var (ineligibleFilters, eligibleFilters) = filters.partition {\n      case f => containsSubquery(f) || !f.deterministic || f.exists {\n        case MetadataAttribute(_) => true\n        case _ => false\n      }\n    }\n\n\n    val (partitionFilters, dataFilters) = eligibleFilters\n      .partition(isPredicatePartitionColumnsOnly(_, partitionColumns, spark))\n\n    if (dataFilters.isEmpty) recordDeltaOperation(deltaLog, \"delta.skipping.partition\") {\n      // When there are only partition filters we can scan allFiles\n      // rather than withStats and thus we skip data skipping information.\n      val (files, scanSize) = filterOnPartitions(partitionFilters, keepNumRecords)\n      DeltaScan(\n        version = version,\n        files = files,\n        total = DataSize(sizeInBytesIfKnown, None, numOfFilesIfKnown),\n        partition = scanSize,\n        scanned = scanSize)(\n        scannedSnapshot = snapshotToScan,\n        partitionFilters = ExpressionSet(partitionFilters),\n        dataFilters = ExpressionSet(Nil),\n        partitionLikeDataFilters = ExpressionSet(Nil),\n        rewrittenPartitionLikeDataFilters = Set.empty,\n        unusedFilters = ExpressionSet(ineligibleFilters),\n        scanDurationMs = System.currentTimeMillis() - startTime,\n        dataSkippingType =\n          getCorrectDataSkippingType(DeltaDataSkippingType.partitionFilteringOnlyV1)\n      )\n    } else recordDeltaOperation(deltaLog, \"delta.skipping.data\") {\n      val finalPartitionFilters = constructPartitionFilters(partitionFilters)\n\n      val dataSkippingType = if (partitionFilters.isEmpty) {\n        DeltaDataSkippingType.dataSkippingOnlyV1\n      } else {\n        DeltaDataSkippingType.dataSkippingAndPartitionFilteringV1\n      }\n\n      var (skippingFilters, unusedFilters) = if (useStats) {\n        val constructDataFilters = new DataFiltersBuilder(spark, dataSkippingType)\n        dataFilters.map(f => (f, constructDataFilters(f))).partition(f => f._2.isDefined)\n      } else {\n        (Nil, dataFilters.map(f => (f, None)))\n      }\n\n      // If enabled, rewrite unused data filters to use partition-like data skipping for clustered\n      // tables. Only rewrite filters if the table is expected to benefit from partition-like\n      // data skipping:\n      // 1. The table should be have a large portion of files with the same min-max values on the\n      //    referenced columns - as a rough heuristic, require the table to be a clustered table, as\n      //    many files often have the same min-max on the clustering columns.\n      // 2. The table should be large enough to benefit from partition-like data skipping - as a\n      //    rough heuristic, require the table to no longer be considered a \"small delta table.\"\n      // 3. At least 1 data filter was not already used for data skipping.\n      val shouldRewriteDataFiltersAsPartitionLike =\n        spark.conf.get(DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ENABLED) &&\n          ClusteredTableUtils.isSupported(snapshotToScan.protocol) &&\n          snapshotToScan.numOfFilesIfKnown.exists(_ >=\n            spark.conf.get(DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_THRESHOLD)) &&\n          unusedFilters.nonEmpty\n      val partitionLikeFilters = if (shouldRewriteDataFiltersAsPartitionLike) {\n        val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(snapshotToScan)\n        val (rewrittenUsedFilters, rewrittenUnusedFilters) = {\n          val constructDataFilters = new DataFiltersBuilder(spark, dataSkippingType)\n          unusedFilters\n            .map { case (expr, _) =>\n              val rewrittenExprOpt = constructDataFilters.rewriteDataFiltersAsPartitionLike(\n                clusteringColumns, expr)\n              (expr, rewrittenExprOpt)\n            }\n            .partition(_._2.isDefined)\n        }\n        skippingFilters = skippingFilters ++ rewrittenUsedFilters\n        unusedFilters = rewrittenUnusedFilters\n        rewrittenUsedFilters.map { case (orig, rewrittenOpt) => (orig, rewrittenOpt.get) }\n      } else {\n        Nil\n      }\n\n      val finalSkippingFilters = skippingFilters\n        .map(_._2.get)\n        .reduceOption((skip1, skip2) => DataSkippingPredicate(\n          // Fold the filters into a conjunction, while unioning their referencedStats.\n          skip1.expr && skip2.expr, skip1.referencedStats ++ skip2.referencedStats))\n        .getOrElse(DataSkippingPredicate(trueLiteral))\n\n      val (files, sizes) = {\n        getDataSkippedFiles(finalPartitionFilters, finalSkippingFilters, keepNumRecords)\n      }\n\n      DeltaScan(\n        version = version,\n        files = files,\n        total = sizes(0),\n        partition = sizes(1),\n        scanned = sizes(2))(\n        scannedSnapshot = snapshotToScan,\n        partitionFilters = ExpressionSet(partitionFilters),\n        dataFilters = ExpressionSet(skippingFilters.map(_._1)),\n        partitionLikeDataFilters = ExpressionSet(partitionLikeFilters.map(_._1)),\n        rewrittenPartitionLikeDataFilters = partitionLikeFilters.map(_._2.expr.expr).toSet,\n        unusedFilters = ExpressionSet(unusedFilters.map(_._1) ++ ineligibleFilters),\n        scanDurationMs = System.currentTimeMillis() - startTime,\n        dataSkippingType = getCorrectDataSkippingType(dataSkippingType)\n      )\n    }\n  }\n\n  /**\n   * Gathers files that should be included in a scan based on the given predicates and limit.\n   * This will be called only when all predicates are on partitioning columns.\n   * Statistics about the amount of data that will be read are gathered and returned.\n   */\n  override def filesForScan(limit: Long, partitionFilters: Seq[Expression]): DeltaScan =\n    recordDeltaOperation(deltaLog, \"delta.skipping.filteredLimit\") {\n      val startTime = System.currentTimeMillis()\n      val finalPartitionFilters = constructPartitionFilters(partitionFilters)\n\n      val scan = {\n        pruneFilesByLimit(withStats.where(finalPartitionFilters), limit)\n      }\n\n      val totalDataSize = new DataSize(\n        sizeInBytesIfKnown,\n        None,\n        numOfFilesIfKnown,\n        None\n      )\n\n      val scannedDataSize = new DataSize(\n        scan.byteSize,\n        scan.numPhysicalRecords,\n        Some(scan.files.size),\n        scan.numLogicalRecords\n      )\n\n      DeltaScan(\n        version = version,\n        files = scan.files,\n        total = totalDataSize,\n        partition = null,\n        scanned = scannedDataSize)(\n        scannedSnapshot = snapshotToScan,\n        partitionFilters = ExpressionSet(partitionFilters),\n        dataFilters = ExpressionSet(Nil),\n        partitionLikeDataFilters = ExpressionSet(Nil),\n        rewrittenPartitionLikeDataFilters = Set.empty,\n        unusedFilters = ExpressionSet(Nil),\n        scanDurationMs = System.currentTimeMillis() - startTime,\n        dataSkippingType = DeltaDataSkippingType.filteredLimit\n      )\n    }\n\n  /**\n   * Get AddFile (with stats) actions corresponding to given set of paths in the Snapshot.\n   * If a path doesn't exist in snapshot, it will be ignored and no [[AddFile]] will be returned\n   * for it.\n   * @param paths Sequence of paths for which we want to get [[AddFile]] action\n   * @return a sequence of addFiles for the given `paths`\n   */\n  def getSpecificFilesWithStats(paths: Seq[String]): Seq[AddFile] = {\n    recordFrameProfile(\"Delta\", \"DataSkippingReader.getSpecificFilesWithStats\") {\n      val right = paths.toDF(spark, \"path\")\n      val df = allFiles.join(right, Seq(\"path\"), \"leftsemi\")\n      convertDataFrameToAddFiles(df)\n    }\n  }\n\n  /** Get the files and number of records within each file, to perform limit pushdown. */\n  def getFilesAndNumRecords(\n      df: DataFrame): Iterator[(AddFile, NumRecords)] with Closeable = recordFrameProfile(\n    \"Delta\", \"DataSkippingReaderEdge.getFilesAndNumRecords\") {\n    import org.apache.spark.sql.delta.implicits._\n\n    val dvCardinality = coalesce(col(\"deletionVector.cardinality\"), lit(0L))\n    val numLogicalRecords = col(\"stats.numRecords\") - dvCardinality\n\n    val result = df.withColumn(\"numPhysicalRecords\", col(\"stats.numRecords\")) // Physical\n      .withColumn(\"numLogicalRecords\", numLogicalRecords) // Logical\n      .withColumn(\"stats\", nullStringLiteral)\n      .select(struct(col(\"*\")).as[AddFile],\n        col(\"numPhysicalRecords\").as[java.lang.Long], col(\"numLogicalRecords\").as[java.lang.Long])\n      .collectAsList()\n\n    new Iterator[(AddFile, NumRecords)] with Closeable {\n      private val underlying = result.iterator\n      override def hasNext: Boolean = underlying.hasNext\n      override def next(): (AddFile, NumRecords) = {\n        val next = underlying.next()\n        (next._1, NumRecords(numPhysicalRecords = next._2, numLogicalRecords = next._3))\n      }\n\n      override def close(): Unit = {\n      }\n\n    }\n  }\n\n  protected def convertDataFrameToAddFiles(df: DataFrame): Array[AddFile] = {\n    df.as[AddFile].collect()\n  }\n\n  protected[delta] def pruneFilesByLimit(df: DataFrame, limit: Long): ScanAfterLimit = {\n    val withNumRecords = {\n      getFilesAndNumRecords(df)\n    }\n    pruneFilesWithIterator(withNumRecords, limit)\n  }\n\n  /**\n   * Accepts an iterator of files with record counts and prunes them based on the limit.\n   */\n  protected def pruneFilesWithIterator(\n      withNumRecords: Iterator[(AddFile, NumRecords)] with Closeable,\n      limit: Long): ScanAfterLimit = {\n\n    var logicalRowsToScan = 0L\n    var physicalRowsToScan = 0L\n    var bytesToScan = 0L\n    var bytesToIgnore = 0L\n    var rowsUnknown = false\n\n    val filesAfterLimit = try {\n      val iter = withNumRecords\n      val filesToScan = ArrayBuffer[AddFile]()\n      val filesToIgnore = ArrayBuffer[AddFile]()\n      while (iter.hasNext && logicalRowsToScan < limit) {\n        val file = iter.next()\n        if (file._2.numPhysicalRecords == null || file._2.numLogicalRecords == null) {\n          // this file has no stats, ignore for now\n          bytesToIgnore += file._1.size\n          filesToIgnore += file._1\n        } else {\n          physicalRowsToScan += file._2.numPhysicalRecords.toLong\n          logicalRowsToScan += file._2.numLogicalRecords.toLong\n          bytesToScan += file._1.size\n          filesToScan += file._1\n        }\n      }\n\n      // If the files that have stats do not contain enough rows, fall back to reading all files\n      if (logicalRowsToScan < limit && filesToIgnore.nonEmpty) {\n        filesToScan ++= filesToIgnore\n        bytesToScan += bytesToIgnore\n        rowsUnknown = true\n      }\n      filesToScan.toSeq\n    } finally {\n      withNumRecords.close()\n    }\n\n    if (rowsUnknown) {\n      ScanAfterLimit(filesAfterLimit, Some(bytesToScan), None, None)\n    } else {\n      ScanAfterLimit(filesAfterLimit, Some(bytesToScan),\n        Some(physicalRowsToScan), Some(logicalRowsToScan))\n    }\n  }\n}\n\ntrait DataSkippingReader extends DataSkippingReaderBase\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DataSkippingStatsTracker.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.expressions.JoinedProjection\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.expressions.aggregate._\nimport org.apache.spark.sql.catalyst.expressions.codegen.GenerateMutableProjection\nimport org.apache.spark.sql.execution.datasources._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.SerializableConfiguration\n\n/**\n * A [[WriteTaskStats]] that contains a map from file name to the json representation\n * of the collected statistics.\n */\ncase class DeltaFileStatistics(stats: Map[String, String]) extends WriteTaskStats\n\n/**\n * A per-task (i.e. one instance per executor) [[WriteTaskStatsTracker]] that collects the\n * statistics defined by [[StatisticsCollection]] for files that are being written into a delta\n * table.\n *\n * @param dataCols Resolved data (i.e. non-partitionBy) columns of the dataframe to be written.\n * @param statsColExpr Resolved expression for computing all the statistics that we want to gather.\n * @param rootPath The Reservoir's root path.\n * @param hadoopConf Hadoop Config for being able to instantiate a [[FileSystem]].\n */\nclass DeltaTaskStatisticsTracker(\n    dataCols: Seq[Attribute],\n    statsColExpr: Expression,\n    rootPath: Path,\n    hadoopConf: Configuration) extends WriteTaskStatsTracker {\n\n  protected[this] val submittedFiles = mutable.HashMap[String, InternalRow]()\n\n  // For example, when strings are involved, statsColExpr might look like\n  // struct(\n  //   count(new Column(\"*\")) as \"numRecords\"\n  //   struct(\n  //     substring(min(col), 0, stringPrefix))\n  //   ) as \"minValues\",\n  //   struct(\n  //     udf(max(col))\n  //   ) as \"maxValues\"\n  // ) as \"stats\"\n\n  // [[DeclarativeAggregate]] is the API to the Catalyst machinery for initializing and updating\n  // the result of an aggregate function. We will be using it here the same way it's used during\n  // query execution.\n\n  // Given the example above, aggregates would hold: Seq(count, min, max)\n  private val aggregates: Seq[DeclarativeAggregate] = statsColExpr.collect {\n    case ae: AggregateExpression if ae.aggregateFunction.isInstanceOf[DeclarativeAggregate] =>\n      ae.aggregateFunction.asInstanceOf[DeclarativeAggregate]\n  }\n\n  // The fields of aggBuffer - see below\n  protected val aggBufferAttrs: Seq[Attribute] = aggregates.flatMap(_.aggBufferAttributes)\n\n  // This projection initializes aggBuffer with the neutral values for the agg fcns e.g. 0 for sum\n  protected val initializeStats: MutableProjection = GenerateMutableProjection.generate(\n    expressions = aggregates.flatMap(_.initialValues),\n    inputSchema = Seq.empty,\n    useSubexprElimination = false\n  )\n\n  // This projection combines the intermediate results stored by aggBuffer with the values of the\n  // currently processed row and updates aggBuffer in place.\n  private val updateStats: MutableProjection = {\n    val aggs = aggregates.flatMap(_.updateExpressions)\n    val expressions = JoinedProjection.bind(aggBufferAttrs, dataCols, aggs)\n    if (SQLConf.get.getConf(\n        DeltaSQLConf.DELTA_STATS_COLLECTION_FALLBACK_TO_INTERPRETED_PROJECTION)) {\n      MutableProjection.create(\n        exprs = expressions,\n        inputSchema = Nil\n      )\n    } else {\n      GenerateMutableProjection.generate(\n        expressions = expressions,\n        inputSchema = Nil,\n        useSubexprElimination = true\n      )\n    }\n  }\n\n  // This executes the whole statsColExpr in order to compute the final stats value for the file.\n  // In order to evaluate it, we have to replace its aggregate functions with the corresponding\n  // aggregates' evaluateExpressions that basically just return the results stored in aggBuffer.\n  private val resultExpr: Expression = statsColExpr.transform {\n    case ae: AggregateExpression if ae.aggregateFunction.isInstanceOf[DeclarativeAggregate] =>\n      ae.aggregateFunction.asInstanceOf[DeclarativeAggregate].evaluateExpression\n  }\n\n  // See resultExpr above\n  private val getStats: Projection = UnsafeProjection.create(\n    exprs = Seq(resultExpr),\n    inputSchema = aggBufferAttrs\n  )\n\n  // This serves as input to updateStats, with aggBuffer always on the left, while the right side\n  // is every time replaced with the row currently being processed - see updateStats and newRow.\n  private val extendedRow: GenericInternalRow = new GenericInternalRow(2)\n\n  // file path to corresponding stats encoded as json\n  protected val results = new collection.mutable.HashMap[String, String]\n\n  // called once per file, executes the getStats projection\n  override def closeFile(filePath: String): Unit = {\n    // We assume file names are unique\n    val fileName = new Path(filePath).getName\n\n    assert(!results.contains(fileName), s\"Stats already recorded for file: $filePath\")\n    // this is statsColExpr's output (json string)\n    val jsonStats = getStats(submittedFiles(filePath)).getString(0)\n    results += ((fileName, jsonStats))\n    submittedFiles.remove(filePath)\n  }\n\n  override def newPartition(partitionValues: InternalRow): Unit = { }\n\n  protected def initializeAggBuf(buffer: SpecificInternalRow): InternalRow =\n    initializeStats.target(buffer).apply(EmptyRow)\n\n  override def newFile(newFilePath: String): Unit = {\n    submittedFiles.getOrElseUpdate(newFilePath, {\n      // `buffer` is a row that will start off by holding the initial values for the agg expressions\n      // (see the initializeStats: Projection), will then be updated in place every time a new row\n      // is processed (see updateStats: Projection), and will finally serve as an input for\n      // computing the per-file result of statsColExpr (see getStats: Projection)\n      val buffer = new SpecificInternalRow(aggBufferAttrs.map(_.dataType))\n      initializeAggBuf(buffer)\n    })\n  }\n\n  override def newRow(filePath: String, currentRow: InternalRow): Unit = {\n    val aggBuffer = submittedFiles(filePath)\n    extendedRow.update(0, aggBuffer)\n    extendedRow.update(1, currentRow)\n    updateStats.target(aggBuffer).apply(extendedRow)\n  }\n\n  override def getFinalStats(taskCommitTime: Long): DeltaFileStatistics = {\n    submittedFiles.keys.foreach(closeFile)\n    submittedFiles.clear()\n    DeltaFileStatistics(results.toMap)\n  }\n}\n\n/**\n * Serializable factory class that holds together all required parameters for being able to\n * instantiate a [[DeltaTaskStatisticsTracker]] on an executor.\n *\n * @param hadoopConf The Hadoop configuration object to use on an executor.\n * @param path Root Reservoir path\n * @param dataCols Resolved data (i.e. non-partitionBy) columns of the dataframe to be written.\n */\nclass DeltaJobStatisticsTracker(\n    @transient private val hadoopConf: Configuration,\n    @transient val path: Path,\n    val dataCols: Seq[Attribute],\n    val statsColExpr: Expression\n) extends WriteJobStatsTracker with EvalHelper {\n\n  var recordedStats: Map[String, String] = _\n\n  private val srlHadoopConf = new SerializableConfiguration(hadoopConf)\n  private val rootUri = path.getFileSystem(hadoopConf).makeQualified(path).toUri()\n\n  override def newTaskInstance(): WriteTaskStatsTracker = {\n    val rootPath = new Path(rootUri)\n    val hadoopConf = srlHadoopConf.value\n    new DeltaTaskStatisticsTracker(dataCols, prepareForEval(statsColExpr), rootPath, hadoopConf)\n  }\n\n  override def processStats(stats: Seq[WriteTaskStats], jobCommitTime: Long): Unit = {\n    recordedStats = stats.map(_.asInstanceOf[DeltaFileStatistics]).flatMap(_.stats).toMap\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DeletedRecordCountsHistogram.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.util.Arrays\n\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A Histogram class tracking the deleted record count distribution for all files in a table.\n * @param deletedRecordCounts An array with 10 bins where each slot represents the number of\n *                            files where the number of deleted records falls within the range\n *                            of the particular bin. The range of each bin is the following:\n *                            bin1  -> [0,0]\n *                            bin2  -> [1,9]\n *                            bin3  -> [10,99]\n *                            bin4  -> [100,999],\n *                            bin5  -> [1000,9999]\n *                            bin6  -> [10000,99999],\n *                            bin7  -> [100000,999999],\n *                            bin8  -> [1000000,9999999],\n *                            bin9  -> [10000000,Int.Max - 1],\n *                            bin10 -> [Int.Max,Long.Max].\n */\ncase class DeletedRecordCountsHistogram(deletedRecordCounts: Array[Long]) {\n  require(deletedRecordCounts.length == DeletedRecordCountsHistogramUtils.NUMBER_OF_BINS,\n    s\"There should be ${DeletedRecordCountsHistogramUtils.NUMBER_OF_BINS} bins in total\")\n\n  override def hashCode(): Int =\n    31 * Arrays.hashCode(deletedRecordCounts) + getClass.getCanonicalName.hashCode\n\n  override def equals(that: Any): Boolean = that match {\n    case DeletedRecordCountsHistogram(thatDP) =>\n      java.util.Arrays.equals(deletedRecordCounts, thatDP)\n    case _ => false\n  }\n\n  /**\n   * Insert a given value into the appropriate histogram bin.\n   */\n  def insert(numDeletedRecords: Long): Unit = {\n    if (numDeletedRecords >= 0) {\n      val index = DeletedRecordCountsHistogramUtils.getHistogramBin(numDeletedRecords)\n      deletedRecordCounts(index) += 1\n    }\n  }\n\n  /**\n   * Remove a given value from the appropriate histogram bin.\n   */\n  def remove(numDeletedRecords: Long): Unit = {\n    if (numDeletedRecords >= 0) {\n      val index = DeletedRecordCountsHistogramUtils.getHistogramBin(numDeletedRecords)\n      deletedRecordCounts(index) -= 1\n    }\n  }\n}\n\nprivate[delta] object DeletedRecordCountsHistogram {\n  def apply(deletionPercentages: Array[Long]): DeletedRecordCountsHistogram =\n    new DeletedRecordCountsHistogram(deletionPercentages)\n\n  lazy val schema: StructType = ExpressionEncoder[DeletedRecordCountsHistogram]().schema\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DeletedRecordCountsHistogramUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaUDF}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\n\nimport org.apache.spark.sql.Column\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.expressions.aggregate.TypedImperativeAggregate\nimport org.apache.spark.sql.catalyst.trees.UnaryLike\nimport org.apache.spark.sql.catalyst.util.GenericArrayData\nimport org.apache.spark.sql.functions.udf\nimport org.apache.spark.sql.types.{ArrayType, DataType, LongType}\nimport org.apache.spark.unsafe.Platform\n\n/**\n * This object contains helper functionality related to [[DeletedRecordCountsHistogram]].\n */\nobject DeletedRecordCountsHistogramUtils {\n  val BUCKET_BOUNDARIES = IndexedSeq(\n    0L, 1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, Int.MaxValue, Long.MaxValue)\n  val NUMBER_OF_BINS = BUCKET_BOUNDARIES.length - 1\n\n  def getDefaultBins: Array[Long] = Array.fill(NUMBER_OF_BINS)(0L)\n\n  def emptyHistogram: DeletedRecordCountsHistogram =\n    DeletedRecordCountsHistogram.apply(getDefaultBins)\n\n  def getHistogramBin(dvCardinality: Long): Int = {\n    import scala.collection.Searching._\n\n    require(dvCardinality >= 0)\n\n    if (dvCardinality == Long.MaxValue) return NUMBER_OF_BINS - 1\n\n    BUCKET_BOUNDARIES.search(dvCardinality) match {\n      case Found(index) =>\n        index\n      case InsertionPoint(insertionPoint) =>\n        insertionPoint - 1\n    }\n  }\n\n  /**\n   * An imperative aggregate implementation of DeletedRecordCountsHistogram.\n   *\n   * The return type of this Imperative Aggregate is of ArrayType(LongType). The array\n   * represents a [[DeletedRecordCountsHistogram]].\n   *\n   */\n  case class DeletedRecordCountsHistogramAgg(\n      child: Expression,\n      mutableAggBufferOffset: Int = 0,\n      inputAggBufferOffset: Int = 0)\n    extends TypedImperativeAggregate[Array[Long]]\n    with UnaryLike[Expression] {\n    override def createAggregationBuffer(): Array[Long] = getDefaultBins\n\n    override val dataType: DataType = ArrayType(LongType)\n\n    // This Aggregate doesn't return null.\n    override val nullable: Boolean = false\n\n    override protected def withNewChildInternal(\n        newChild: Expression): DeletedRecordCountsHistogramAgg = copy(child = newChild)\n\n    override def update(aggBuffer: Array[Long], input: InternalRow): Array[Long] = {\n      val value = child.eval(input)\n\n      if (value != null) {\n        val dvCardinality = value.asInstanceOf[Long]\n        val index = getHistogramBin(dvCardinality)\n        aggBuffer(index) += 1\n      }\n      aggBuffer\n    }\n\n    override def merge(buffer: Array[Long], input: Array[Long]): Array[Long] = {\n      require(buffer.length == input.length)\n      for (index <- buffer.indices) {\n        buffer(index) += input(index)\n      }\n      buffer\n    }\n\n    override def eval(buffer: Array[Long]): Any = new GenericArrayData(buffer)\n\n    /** Serializes the aggregation buffer to Array[Byte]. */\n    override def serialize(buffer: Array[Long]): Array[Byte] = {\n      require(buffer.length < 128)\n      val bytesPerLong = 8\n      // One 8bit value stores the number of elements, the remaining are bucket values.\n      val serializedByteSize = (buffer.length * bytesPerLong) + 1\n      val byteArray = new Array[Byte](serializedByteSize)\n      // Add buffer length for validation.\n      Platform.putByte(byteArray, Platform.BYTE_ARRAY_OFFSET, buffer.length.toByte)\n\n      for (index <- buffer.indices) {\n        val offset = Platform.BYTE_ARRAY_OFFSET + 1 + index * bytesPerLong\n        Platform.putLong(byteArray, offset, buffer(index))\n      }\n      byteArray\n    }\n\n    /** De-serializes the serialized format Array[Byte], and produces aggregation buffer. */\n    override def deserialize(bytes: Array[Byte]): Array[Long] = {\n      val bytesPerLong = 8\n      // One 8bit value stores the number of elements, the remaining are bucket values.\n      val numElementsFromSerializedByteSize = (bytes.length - 1) / bytesPerLong\n      val aggBuffer = new Array[Long](numElementsFromSerializedByteSize)\n      // At the first byte we store the length of the deserialized buffer for validation purposes.\n      val numElementsFromSerializedState = Platform.getByte(bytes, Platform.BYTE_ARRAY_OFFSET).toInt\n      if (numElementsFromSerializedByteSize != numElementsFromSerializedState) {\n        throw DeltaErrors.deletedRecordCountsHistogramDeserializationException()\n      }\n      for (index <- aggBuffer.indices) {\n        val offset = Platform.BYTE_ARRAY_OFFSET + 1 + index * bytesPerLong\n        aggBuffer(index) = Platform.getLong(bytes, offset)\n      }\n      aggBuffer\n    }\n\n    override def withNewMutableAggBufferOffset(offset: Int): DeletedRecordCountsHistogramAgg =\n      copy(mutableAggBufferOffset = offset)\n\n    override def withNewInputAggBufferOffset(offset: Int): DeletedRecordCountsHistogramAgg =\n      copy(inputAggBufferOffset = offset)\n  }\n\n  /**\n   * A UDF to convert a long array (returned by [[DeletedRecordCountsHistogramAgg]]) to\n   * [[DeletedRecordCountsHistogram]].\n   */\n  private lazy val HistogramAggrToHistogramUDF = {\n    DeltaUDF.deletedRecordCountsHistogramFromArrayLong { deletedRecordCountsHistogramArray =>\n      new DeletedRecordCountsHistogram(deletedRecordCountsHistogramArray) }\n  }\n\n  def histogramAggregate(dvCardinalityExpr: Column): Column = {\n    val aggregate =\n      Column(DeletedRecordCountsHistogramAgg(dvCardinalityExpr.expr).toAggregateExpression())\n    DeletedRecordCountsHistogramUtils.HistogramAggrToHistogramUDF(aggregate)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DeltaScan.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Snapshot\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.stats.DeltaDataSkippingType.DeltaDataSkippingType\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\n\nimport org.apache.spark.sql.catalyst.expressions._\n\n/**\n * DataSize describes following attributes for data that consists of a list of input files\n * @param bytesCompressed total size of the data\n * @param rows number of rows in the data\n * @param files number of input files\n * Note: Please don't add any new constructor to this class. `jackson-module-scala` always picks up\n * the first constructor returned by `Class.getConstructors` but the order of the constructors list\n * is non-deterministic. (SC-13343)\n */\ncase class DataSize(\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    bytesCompressed: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    rows: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    files: Option[Long] = None,\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    logicalRows: Option[Long] = None\n)\n\nobject DataSize {\n  def apply(a: ArrayAccumulator): DataSize = {\n    DataSize(\n      Option(a.value(0)).filterNot(_ == -1),\n      Option(a.value(1)).filterNot(_ == -1),\n      Option(a.value(2)).filterNot(_ == -1),\n      Option(a.value(3)).filterNot(_ == -1)\n    )\n  }\n}\n\nobject DeltaDataSkippingType extends Enumeration {\n  type DeltaDataSkippingType = Value\n  // V1: code path in DataSkippingReader.scala, which needs StateReconstruction\n  // noSkipping: no skipping and get all files from the Delta table\n  // partitionFiltering: filtering and skipping based on partition columns\n  // dataSkipping: filtering and skipping based on stats columns\n  // limit: skipping based on limit clause in DataSkippingReader.scala\n  // filteredLimit: skipping based on limit clause and partition columns in DataSkippingReader.scala\n  val noSkippingV1, noSkippingV2, partitionFilteringOnlyV1, partitionFilteringOnlyV2,\n    dataSkippingOnlyV1, dataSkippingOnlyV2, dataSkippingAndPartitionFilteringV1,\n    dataSkippingAndPartitionFilteringV2, limit, filteredLimit = Value\n}\n\n/**\n * Used to hold details the files and stats for a scan where we have already\n * applied filters and a limit.\n */\ncase class DeltaScan(\n    version: Long,\n    files: Seq[AddFile],\n    total: DataSize,\n    partition: DataSize,\n    scanned: DataSize)(\n    // Moved to separate argument list, to not be part of case class equals check -\n    // expressions can differ by exprId or ordering, but as long as same files are scanned, the\n    // PreparedDeltaFileIndex and HadoopFsRelation should be considered equal for reuse purposes.\n    val scannedSnapshot: Snapshot,\n    val partitionFilters: ExpressionSet,\n    val dataFilters: ExpressionSet,\n    val partitionLikeDataFilters: ExpressionSet,\n    // We can't use an ExpressionSet here because the rewritten filters aren't yet resolved when the\n    // DeltaScan is created. Since this is for logging only, it's OK to store the non-canonicalized\n    // expressions instead.\n    val rewrittenPartitionLikeDataFilters: Set[Expression],\n    val unusedFilters: ExpressionSet,\n    val scanDurationMs: Long,\n    val dataSkippingType: DeltaDataSkippingType) {\n  assert(version == scannedSnapshot.version)\n\n  /**\n   * For unresolved expressions, converting the expression to SQL may throw an exception (if the\n   * conversion to SQL requires the child types to be resolved). This method safely handles these\n   * cases by returning a placeholder string for unresolved expressions.\n   */\n  def safeExprToSQL(expr: Expression): String = {\n    try {\n      expr.sql\n    } catch {\n      case NonFatal(_) => s\"UNRESOLVED_EXPRESSION_(${expr.getClass.getSimpleName})\"\n    }\n  }\n\n  lazy val rewrittenPartitionLikeFilterSQL = rewrittenPartitionLikeDataFilters.map(safeExprToSQL)\n  lazy val filtersUsedForSkipping: ExpressionSet = partitionFilters ++ dataFilters\n  lazy val allFilters: ExpressionSet = filtersUsedForSkipping ++ unusedFilters\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/DeltaScanGenerator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.delta.{Snapshot, SnapshotDescriptor}\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}\n\n/** Trait representing a class that can generate [[DeltaScan]] given filters and a limit. */\ntrait DeltaScanGenerator {\n  /** The snapshot that the scan is being generated on. */\n  val snapshotToScan: Snapshot\n\n  /**\n   * Returns a DataFrame for the given partition filters. The schema of returned DataFrame is nearly\n   * the same as `AddFile`, except that the `stats` field is parsed to a struct from a json string.\n   */\n  def filesWithStatsForScan(partitionFilters: Seq[Expression]): DataFrame\n\n  /** Returns a [[DeltaScan]] based on the given filters. */\n  def filesForScan(filters: Seq[Expression], keepNumRecords: Boolean = false): DeltaScan\n\n  /** Returns a [[DeltaScan]] based on the given partition filters and limits. */\n  def filesForScan(limit: Long, partitionFilters: Seq[Expression]): DeltaScan\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/FileSizeHistogram.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.util.Arrays\n\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.types.StructType\n\n/**\n * A Histogram class tracking the file counts and total bytes in different size ranges\n * @param sortedBinBoundaries - a sorted list of bin boundaries where each element represents the\n *                              start of the bin (included) and the next element represents the end\n *                              of the bin (excluded)\n * @param fileCounts - an array of Int representing total number of files in different bins\n * @param totalBytes - an array of Long representing total number of bytes in different bins\n */\ncase class FileSizeHistogram(\n    sortedBinBoundaries: IndexedSeq[Long],\n    fileCounts: Array[Long],\n    totalBytes: Array[Long]) extends FileStatsHistogram {\n\n  /**\n   * Not intended to be used for [[Map]] structure keys. Implemented for the sole purpose of having\n   * an equals method, which requires overriding hashCode as well, so an incomplete hash is okay.\n   * We only require a == b implies a.hashCode == b.hashCode\n   */\n  override def hashCode(): Int = Arrays.hashCode(totalBytes)\n\n  override def equals(that: Any): Boolean = that match {\n    case h: FileSizeHistogram => equalsHistogram(h)\n    case _ => false\n  }\n\n  /**\n   * Insert a given value into the appropriate histogram bin\n   */\n  def insert(fileSize: Long): Unit = {\n    val index = FileSizeHistogram.getBinIndex(fileSize, sortedBinBoundaries)\n    if (index >= 0) {\n      fileCounts(index) += 1\n      totalBytes(index) += fileSize\n    }\n  }\n\n  /**\n   * Remove a given value from the appropriate histogram bin\n   * @param fileSize to remove\n   */\n  def remove(fileSize: Long): Unit = {\n    val index = FileSizeHistogram.getBinIndex(fileSize, sortedBinBoundaries)\n    if (index >= 0) {\n      fileCounts(index) -= 1\n      totalBytes(index) -= fileSize\n    }\n  }\n}\n\nprivate[delta] object FileSizeHistogram {\n\n  /**\n   * Returns the index of the bin to which given fileSize belongs OR -1 if given fileSize doesn't\n   * belongs to any bin\n   */\n  def getBinIndex(fileSize: Long, sortedBinBoundaries: IndexedSeq[Long]): Int = {\n    FileStatsHistogram.getBinIndex(fileSize, sortedBinBoundaries)\n  }\n\n  def apply(sortedBinBoundaries: IndexedSeq[Long]): FileSizeHistogram = {\n    new FileSizeHistogram(\n      sortedBinBoundaries,\n      Array.fill(sortedBinBoundaries.size)(0),\n      Array.fill(sortedBinBoundaries.size)(0)\n    )\n  }\n\n  lazy val schema: StructType = ExpressionEncoder[FileSizeHistogram]().schema\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/FileStatsHistogram.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.util.Arrays\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\n\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.util.GenericArrayData\nimport org.apache.spark.sql.types.{ArrayType, DataType, LongType}\nimport org.apache.spark.unsafe.Platform\n\n/**\n * Base trait for histogram implementations tracking file counts and total bytes across bins.\n */\ntrait FileStatsHistogram {\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  def sortedBinBoundaries: IndexedSeq[Long]\n  def fileCounts: Array[Long]\n  def totalBytes: Array[Long]\n\n  require(sortedBinBoundaries.nonEmpty)\n  require(sortedBinBoundaries.head == 0, \"The first bin should start from 0\")\n  require(sortedBinBoundaries.length == fileCounts.length,\n    \"number of binBoundaries should be same as size of fileCounts\")\n  require(sortedBinBoundaries.length == totalBytes.length,\n    \"number of binBoundaries should be same as size of totalBytes\")\n\n  /**\n   * Helper method for subclass equals implementations. Subclasses should override both\n   * equals and hashCode together.\n   */\n  protected def equalsHistogram(that: FileStatsHistogram): Boolean = {\n    sortedBinBoundaries == that.sortedBinBoundaries &&\n      java.util.Arrays.equals(fileCounts, that.fileCounts) &&\n      java.util.Arrays.equals(totalBytes, that.totalBytes)\n  }\n}\n\n/**\n * Companion object with utility functions for histograms.\n */\nobject FileStatsHistogram {\n\n  /**\n   * Returns the index of the bin to which given value belongs OR -1 if value doesn't belong\n   * to any bin\n   */\n  def getBinIndex(value: Long, sortedBinBoundaries: IndexedSeq[Long]): Int = {\n    import scala.collection.Searching._\n    val searchResult = sortedBinBoundaries.search(value)\n    searchResult match {\n      case Found(index) =>\n        index\n      case InsertionPoint(insertionPoint) =>\n        // insertionPoint=0 means that fileSize is lesser than min bucket of histogram\n        insertionPoint - 1\n    }\n  }\n\n  /**\n   * Returns a compacted version of a histogram where empty bins are merged together.\n   */\n  def compress[H <: FileStatsHistogram](\n      h: H,\n      constructor: (IndexedSeq[Long], Array[Long], Array[Long]) => H): H = {\n    val newSortedBinBoundaries = ArrayBuffer.empty[Long]\n    val newFileCounts = ArrayBuffer.empty[Long]\n    val newTotalBytes = ArrayBuffer.empty[Long]\n    if (h.sortedBinBoundaries.nonEmpty) {\n      newSortedBinBoundaries.append(h.sortedBinBoundaries(0))\n      newFileCounts.append(h.fileCounts(0))\n      newTotalBytes.append(h.totalBytes(0))\n      for (index <- 1 until h.sortedBinBoundaries.length) {\n        if (h.fileCounts(index) != 0 || h.fileCounts(index - 1) != 0) {\n          newSortedBinBoundaries.append(h.sortedBinBoundaries(index))\n          newFileCounts.append(h.fileCounts(index))\n          newTotalBytes.append(h.totalBytes(index))\n        }\n      }\n    }\n    constructor(newSortedBinBoundaries.toIndexedSeq, newFileCounts.toArray, newTotalBytes.toArray)\n  }\n\n  /**\n   * Base class for imperative aggregate implementations of file statistics histograms.\n   * This provides common functionality for both FileSizeHistogram and FileAgeHistogram aggregates.\n   *\n   * The return type of this Imperative Aggregate is of ArrayType(LongType). The array\n   * represents a flattened histogram with following structure:\n   *\n   * --------------------------------------------------------------------------------\n   * |  PART-1: sortedBinBoundaries  |  PART-2: fileCounts   | PART-3: totalBytes   |\n   * --------------------------------------------------------------------------------\n   *\n   * This Aggregate returns the flattened histogram and not the histogram object due to\n   * the limitation that Imperative aggregates can only return primitive/Array/Map types.\n   *\n   * The intermediate aggregation buffer consists of only PART-2 + PART-3 and doesn't contain\n   * the sortedBinBoundaries. sortedBinBoundaries are added only at the end when the aggregate\n   * is finalized.\n   */\n  abstract class FileStatsHistogramAggBase\n    extends org.apache.spark.sql.catalyst.expressions.aggregate\n      .TypedImperativeAggregate[Array[Long]]\n    with org.apache.spark.sql.catalyst.trees.UnaryLike[\n      org.apache.spark.sql.catalyst.expressions.Expression] {\n\n    import org.apache.spark.sql.catalyst.InternalRow\n    import org.apache.spark.sql.catalyst.util.GenericArrayData\n    import org.apache.spark.sql.types.{ArrayType, DataType, LongType}\n    import org.apache.spark.unsafe.Platform\n\n    def sortedBinBoundaries: IndexedSeq[Long]\n\n    // Size of underlying buffer for the aggregate. We make buffer of 2 * totalBins to store\n    // fileCount as well as totalBytes.\n    lazy val underlyingBufferSize: Int = sortedBinBoundaries.size * 2\n    lazy val secondHalfStartIndex: Int = sortedBinBoundaries.size\n\n    /**\n     * The aggregation buffer of this aggregate is an Array of Longs of size - 2 * NumBins\n     * 1. First half of the array represents fileCounts\n     * 2. Second half of the array represents totalBytes\n     *\n     * --------------------------------------------------------------------------------\n     * |     fileCounts related indices       |          totalBytes related indices   |\n     * --------------------------------------------------------------------------------\n     */\n    override def createAggregationBuffer(): Array[Long] = Array.fill(underlyingBufferSize)(0)\n\n    override def dataType: DataType = ArrayType(LongType)\n\n    override def nullable: Boolean = {\n      // This Aggregate doesn't return null\n      false\n    }\n\n    override def update(aggBuffer: Array[Long], input: InternalRow): Array[Long] = {\n      val value = child.eval(input)\n      if (value != null) {\n        val metricValue = value.asInstanceOf[Long]\n        val index = FileStatsHistogram.getBinIndex(metricValue, sortedBinBoundaries)\n        if (index >= 0) {\n          aggBuffer(index) += 1\n          aggBuffer(secondHalfStartIndex + index) += metricValue\n        }\n      }\n      aggBuffer\n    }\n\n    override def merge(buffer: Array[Long], input: Array[Long]): Array[Long] = {\n      buffer.indices.foreach { index =>\n        buffer(index) += input(index)\n      }\n      buffer\n    }\n\n    override def eval(buffer: Array[Long]): Any = {\n      new GenericArrayData(sortedBinBoundaries ++ buffer)\n    }\n\n    /** Serializes the aggregation buffer to Array[Byte] */\n    override def serialize(buffer: Array[Long]): Array[Byte] = {\n      val byteArray = new Array[Byte](buffer.length * 8)\n      buffer.indices.foreach { index =>\n        Platform.putLong(byteArray, Platform.BYTE_ARRAY_OFFSET + index * 8, buffer(index))\n      }\n      byteArray\n    }\n\n    /** De-serializes the serialized format Array[Byte], and produces aggregation buffer */\n    override def deserialize(bytes: Array[Byte]): Array[Long] = {\n      val aggBuffer = new Array[Long](bytes.length / 8)\n      aggBuffer.indices.foreach { index =>\n        aggBuffer(index) = Platform.getLong(bytes, Platform.BYTE_ARRAY_OFFSET + index * 8)\n      }\n      aggBuffer\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/PrepareDeltaScan.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.util.Objects\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol}\nimport org.apache.spark.sql.delta.files.{TahoeFileIndex, TahoeFileIndexWithSnapshotDescriptor, TahoeLogFileIndex}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.perf.OptimizeMetadataOnlyDeltaQuery\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.planning.PhysicalOperation\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.catalyst.trees.TreePattern.PROJECT\nimport org.apache.spark.sql.execution.datasources.{FileIndex, LogicalRelation}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Before query planning, we prepare any scans over delta tables by pushing\n * any projections or filters in allowing us to gather more accurate statistics\n * for CBO and metering.\n *\n * Note the following\n * - This rule also ensures that all reads from the same delta log use the same snapshot of log\n *   thus providing snapshot isolation.\n * - If this rule is invoked within an active [[OptimisticTransaction]], then the scans are\n *   generated using the transaction.\n */\ntrait PrepareDeltaScanBase extends Rule[LogicalPlan]\n  with PredicateHelper\n  with DeltaLogging\n  with OptimizeMetadataOnlyDeltaQuery\n  with SubqueryTransformerHelper { self: PrepareDeltaScan =>\n\n  /**\n   * Tracks the first-access snapshots of other logs planned by this rule. The snapshots are\n   * the keyed by the log's unique id. Note that the lifetime of this rule is a single\n   * query, therefore, the map tracks the snapshots only within a query.\n   */\n  private val scannedSnapshots =\n    new java.util.concurrent.ConcurrentHashMap[(String, Path), Snapshot]\n\n  /**\n   * Gets the [[DeltaScanGenerator]] for the given log, which will be used to generate\n   * [[DeltaScan]]s. Every time this method is called on a log within the lifetime of this\n   * rule (i.e., the lifetime of the query for which this rule was instantiated), the returned\n   * generator will read a snapshot that is pinned on the first access for that log.\n   *\n   * Internally, it will use the snapshot of the file index, the snapshot of the active transaction\n   * (if any), or the latest snapshot of the given log.\n   */\n  protected def getDeltaScanGenerator(index: TahoeLogFileIndex): DeltaScanGenerator = {\n    // The first case means that we've fixed the table snapshot for time travel\n    if (index.isTimeTravelQuery) return index.getSnapshot\n    val scanGenerator = OptimisticTransaction.getActive()\n      .map(_.getDeltaScanGenerator(index))\n      .getOrElse {\n        // Will be called only when the log is accessed the first time\n        scannedSnapshots.computeIfAbsent(index.deltaLog.compositeId, _ => index.getSnapshot)\n      }\n    import PrepareDeltaScanBase._\n    if (onGetDeltaScanGeneratorCallback != null) onGetDeltaScanGeneratorCallback(scanGenerator)\n    scanGenerator\n  }\n\n  /**\n   * Helper method to generate a [[PreparedDeltaFileIndex]]\n   */\n  protected def getPreparedIndex(\n      preparedScan: DeltaScan,\n      fileIndex: TahoeLogFileIndex): PreparedDeltaFileIndex = {\n    assert(fileIndex.partitionFilters.isEmpty,\n      \"Partition filters should have been extracted by DeltaAnalysis.\")\n    PreparedDeltaFileIndex(\n      spark,\n      fileIndex.deltaLog,\n      fileIndex.path,\n      fileIndex.catalogTableOpt,\n      preparedScan,\n      fileIndex.versionToUse)\n  }\n\n  /**\n   * Scan files using the given `filters` and return `DeltaScan`.\n   *\n   * Note: when `limitOpt` is non empty, `filters` must contain only partition filters. Otherwise,\n   * it can contain arbitrary filters. See `DeltaTableScan` for more details.\n   */\n  protected def filesForScan(\n      scanGenerator: DeltaScanGenerator,\n      limitOpt: Option[Int],\n      filters: Seq[Expression],\n      delta: LogicalRelation): DeltaScan = {\n    withStatusCode(\"DELTA\", \"Filtering files for query\") {\n      if (limitOpt.nonEmpty) {\n        // If we trigger limit push down, the filters must be partition filters. Since\n        // there are no data filters, we don't need to apply Generated Columns\n        // optimization. See `DeltaTableScan` for more details.\n        return scanGenerator.filesForScan(limitOpt.get, filters)\n      }\n      val filtersForScan =\n        if (!GeneratedColumn.partitionFilterOptimizationEnabled(spark)) {\n          filters\n        } else {\n          val generatedPartitionFilters = GeneratedColumn.generatePartitionFilters(\n            spark, scanGenerator.snapshotToScan, filters, delta)\n          filters ++ generatedPartitionFilters\n        }\n      scanGenerator.filesForScan(filtersForScan)\n    }\n  }\n\n  /**\n   * Prepares delta scans sequentially.\n   */\n  protected def prepareDeltaScan(plan: LogicalPlan): LogicalPlan = {\n    // A map from the canonicalized form of a DeltaTableScan operator to its corresponding delta\n    // scan. This map is used to avoid fetching duplicate delta indexes for structurally-equal\n    // delta scans.\n    val deltaScans = new mutable.HashMap[LogicalPlan, DeltaScan]()\n\n    transformWithSubqueries(plan) {\n        case scan @ DeltaTableScan(planWithRemovedProjections, filters, fileIndex,\n          limit, delta) =>\n          val scanGenerator = getDeltaScanGenerator(fileIndex)\n          val preparedScan = deltaScans.getOrElseUpdate(planWithRemovedProjections.canonicalized,\n              filesForScan(scanGenerator, limit, filters, delta))\n          val preparedIndex = getPreparedIndex(preparedScan, fileIndex)\n          optimizeGeneratedColumns(scan, preparedIndex, filters, limit, delta)\n      }\n  }\n\n  protected def optimizeGeneratedColumns(\n      scan: LogicalPlan,\n      preparedIndex: PreparedDeltaFileIndex,\n      filters: Seq[Expression],\n      limit: Option[Int],\n      delta: LogicalRelation): LogicalPlan = {\n    if (limit.nonEmpty) {\n      // If we trigger limit push down, the filters must be partition filters. Since\n      // there are no data filters, we don't need to apply Generated Columns\n      // optimization. See `DeltaTableScan` for more details.\n      return DeltaTableUtils.replaceFileIndex(scan, preparedIndex)\n    }\n    if (!GeneratedColumn.partitionFilterOptimizationEnabled(spark)) {\n      DeltaTableUtils.replaceFileIndex(scan, preparedIndex)\n    } else {\n      val generatedPartitionFilters =\n        GeneratedColumn.generatePartitionFilters(spark, preparedIndex, filters, delta)\n      val scanWithFilters =\n        if (generatedPartitionFilters.nonEmpty) {\n          scan transformUp {\n            case delta @ DeltaTable(_: TahoeLogFileIndex) =>\n              Filter(generatedPartitionFilters.reduceLeft(And), delta)\n          }\n        } else {\n          scan\n        }\n      DeltaTableUtils.replaceFileIndex(scanWithFilters, preparedIndex)\n    }\n  }\n\n  override def apply(_plan: LogicalPlan): LogicalPlan = {\n    var plan = _plan\n\n    val shouldPrepareDeltaScan = (\n      spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_STATS_SKIPPING)\n    )\n    val updatedPlan = if (shouldPrepareDeltaScan) {\n      // Should not be applied to subqueries to avoid duplicate delta jobs.\n      val isSubquery = isSubqueryRoot(plan)\n      // Should not be applied to DataSourceV2 write plans, because they'll be planned later\n      // through a V1 fallback and only that later planning takes place within the transaction.\n      val isDataSourceV2 = plan.isInstanceOf[V2WriteCommand]\n      if (isSubquery || isDataSourceV2) {\n        return plan\n      }\n\n      if (spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED)) {\n        plan = optimizeQueryWithMetadata(plan)\n      }\n      prepareDeltaScan(plan)\n    } else {\n      prepareDeltaScanWithoutFileSkipping(plan)\n    }\n    updatedPlan\n  }\n\n  protected def prepareDeltaScanWithoutFileSkipping(plan: LogicalPlan): LogicalPlan = {\n    // If this query is running inside an active transaction and is touching the same table\n    // as the transaction, then mark that the entire table as tainted to be safe.\n    OptimisticTransaction.getActive().foreach { txn =>\n      val logsInPlan = plan.collect { case DeltaTable(fileIndex: TahoeFileIndex) =>\n        fileIndex.deltaLog\n      }\n      if (logsInPlan.exists(_.isSameLogAs(txn.deltaLog))) {\n        txn.readWholeTable()\n      }\n    }\n\n    // Just return the plan if statistics based skipping is off.\n    // It will fall back to just partition pruning at planning time.\n    plan\n  }\n\n  /**\n   * This is an extractor object. See https://docs.scala-lang.org/tour/extractor-objects.html.\n   */\n  object DeltaTableScan extends DeltaTableScan[TahoeLogFileIndex] {\n\n    override def limitPushdownEnabled(plan: LogicalPlan): Boolean =\n      spark.conf.get(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED)\n\n    override def getPartitionColumns(fileIndex: TahoeLogFileIndex): Seq[String] =\n      fileIndex.snapshotAtAnalysis.metadata.partitionColumns\n\n    override def getPartitionFilters(fileIndex: TahoeLogFileIndex): Seq[Expression] =\n      fileIndex.partitionFilters\n  }\n\n  abstract class DeltaTableScan[FileIndexType <: FileIndex : scala.reflect.ClassTag] {\n\n    /**\n     * The components of DeltaTableScanType are:\n     * - the plan with removed projections. We remove projections as a plan differentiator\n     * because it does not affect file listing results.\n     * - filter expressions collected by `PhysicalOperation`\n     * - the `FileIndexType` of the matched DeltaTable`\n     * - integer value of limit expression, if any\n     * - matched `DeltaTable`\n     */\n    protected type DeltaTableScanType =\n      (LogicalPlan, Seq[Expression], FileIndexType, Option[Int], LogicalRelation)\n\n    /**\n     * This is an extractor method (basically, the opposite of a constructor) which takes in an\n     * object `plan` and tries to give back the arguments as a [[DeltaTableScanType]].\n     */\n    def unapply(plan: LogicalPlan): Option[DeltaTableScanType] = {\n      // Remove projections as a plan differentiator because it does not affect file listing\n      // results. Plans with the same filters but different projections therefore will not have\n      // duplicate delta indexes.\n      def canonicalizePlanForDeltaFileListing(plan: LogicalPlan): LogicalPlan = {\n        val planWithRemovedProjections = plan.transformWithPruning(_.containsPattern(PROJECT)) {\n          case p: Project if p.projectList.forall(_.isInstanceOf[AttributeReference]) => p.child\n        }\n        planWithRemovedProjections\n      }\n\n      plan match {\n        case LocalLimit(IntegerLiteral(limit),\n          PhysicalOperation(_, filters, delta @ RelationFileIndex(fileIndex: FileIndexType)))\n            if limitPushdownEnabled(plan) && containsPartitionFiltersOnly(filters, fileIndex) =>\n          Some((canonicalizePlanForDeltaFileListing(plan), filters, fileIndex, Some(limit), delta))\n        case PhysicalOperation(\n            _,\n            filters,\n            delta @ RelationFileIndex(fileIndex: FileIndexType)) =>\n          val allFilters = getPartitionFilters(fileIndex) ++ filters\n          Some((canonicalizePlanForDeltaFileListing(plan), allFilters, fileIndex, None, delta))\n\n        case _ => None\n      }\n    }\n\n    protected def containsPartitionFiltersOnly(\n        filters: Seq[Expression],\n        fileIndex: FileIndexType): Boolean = {\n      val partitionColumns = getPartitionColumns(fileIndex)\n      import DeltaTableUtils._\n      filters.forall(expr => !containsSubquery(expr) &&\n        isPredicatePartitionColumnsOnly(expr, partitionColumns, spark))\n    }\n\n    protected def limitPushdownEnabled(plan: LogicalPlan): Boolean\n\n    protected def getPartitionColumns(fileIndex: FileIndexType): Seq[String]\n\n    protected def getPartitionFilters(fileIndex: FileIndexType): Seq[Expression]\n  }\n}\n\nclass PrepareDeltaScan(protected val spark: SparkSession)\n  extends PrepareDeltaScanBase\n\nobject PrepareDeltaScanBase {\n\n  /**\n   * Optional callback function that is called after `getDeltaScanGenerator` is called\n   * by the PrepareDeltaScan rule. This is primarily used for testing purposes.\n   */\n  @volatile private var onGetDeltaScanGeneratorCallback: DeltaScanGenerator => Unit = _\n\n  /**\n   * Run a thunk of code with the given callback function injected into the PrepareDeltaScan rule.\n   * The callback function is called after `getDeltaScanGenerator` is called\n   * by the PrepareDeltaScan rule. This is primarily used for testing purposes.\n   */\n  private[delta] def withCallbackOnGetDeltaScanGenerator[T](\n      callback: DeltaScanGenerator => Unit)(thunk: => T): T = {\n    try {\n      onGetDeltaScanGeneratorCallback = callback\n      thunk\n    } finally {\n      onGetDeltaScanGeneratorCallback = null\n    }\n  }\n}\n\n/**\n * A [[TahoeFileIndex]] that uses a prepared scan to return the list of relevant files.\n * This is injected into a query right before query planning by [[PrepareDeltaScan]] so that\n * CBO and metering can accurately understand how much data will be read.\n *\n * @param versionScanned The version of the table that is being scanned, if a specific version\n *                       has specifically been requested, e.g. by time travel.\n */\ncase class PreparedDeltaFileIndex(\n    override val spark: SparkSession,\n    override val deltaLog: DeltaLog,\n    override val path: Path,\n    catalogTableOpt: Option[CatalogTable],\n    preparedScan: DeltaScan,\n    versionScanned: Option[Long])\n  extends TahoeFileIndexWithSnapshotDescriptor(spark, deltaLog, path, preparedScan.scannedSnapshot)\n  with DeltaLogging {\n\n  /**\n   * Returns all matching/valid files by the given `partitionFilters` and `dataFilters`\n   */\n  override def matchingFiles(\n      partitionFilters: Seq[Expression],\n      dataFilters: Seq[Expression]): Seq[AddFile] = {\n    val currentFilters = ExpressionSet(partitionFilters ++ dataFilters)\n    val (addFiles, eventData) = if (currentFilters == preparedScan.allFilters ||\n        currentFilters == preparedScan.filtersUsedForSkipping) {\n      // [[DeltaScan]] was created using `allFilters` out of which only `filtersUsedForSkipping`\n      // filters were used for skipping while creating the DeltaScan.\n      // If currentFilters is same as allFilters, then no need to recalculate files and we can use\n      // previous results.\n      // If currentFilters is same as filtersUsedForSkipping, then also we don't need to recalculate\n      // files as [[DeltaScan.files]] were calculates using filtersUsedForSkipping only. So if we\n      // recalculate, we will get same result. So we should use previous result in this case also.\n      val eventData = Map(\n        \"reused\" -> true,\n        \"currentFiltersSameAsPreparedAllFilters\" -> (currentFilters == preparedScan.allFilters),\n        \"currentFiltersSameAsPreparedFiltersUsedForSkipping\" ->\n          (currentFilters == preparedScan.filtersUsedForSkipping)\n      )\n      (preparedScan.files.distinct, eventData)\n    } else {\n      logInfo(\n        log\"\"\"\n           |Prepared scan does not match actual filters. Reselecting files to query.\n           |Prepared: ${MDC(DeltaLogKeys.FILTER, preparedScan.allFilters)}\n           |Actual: ${MDC(DeltaLogKeys.FILTER2, currentFilters)}\n         \"\"\".stripMargin)\n      val eventData = Map(\n        \"reused\" -> false,\n        \"preparedAllFilters\" -> preparedScan.allFilters.mkString(\",\"),\n        \"preparedFiltersUsedForSkipping\" -> preparedScan.filtersUsedForSkipping.mkString(\",\"),\n        \"currentFilters\" -> currentFilters.mkString(\",\")\n      )\n      val files = preparedScan.scannedSnapshot.filesForScan(partitionFilters ++ dataFilters).files\n      (files, eventData)\n    }\n    recordDeltaEvent(deltaLog,\n      opType = \"delta.preparedDeltaFileIndex.reuseSkippingResult\",\n      data = eventData)\n    addFiles\n  }\n\n  /**\n   * Returns the list of files that will be read when scanning this relation. This call may be\n   * very expensive for large tables.\n   */\n  override def inputFiles: Array[String] =\n    preparedScan.files.map(f => absolutePath(f.path).toString).toArray\n\n  /** Refresh any cached file listings */\n  override def refresh(): Unit = { }\n\n  /** Sum of table file sizes, in bytes */\n  override def sizeInBytes: Long =\n    preparedScan.scanned.bytesCompressed\n      .getOrElse(spark.sessionState.conf.defaultSizeInBytes)\n\n  override def equals(other: Any): Boolean = other match {\n    case p: PreparedDeltaFileIndex =>\n      p.deltaLog == deltaLog && p.path == path && p.preparedScan == preparedScan &&\n        p.partitionSchema == partitionSchema && p.versionScanned == versionScanned\n    case _ => false\n  }\n\n  override def hashCode(): Int = {\n    Objects.hash(deltaLog, path, preparedScan, partitionSchema, versionScanned)\n  }\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/ReadsMetadataFields.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.Column\nimport org.apache.spark.sql.functions.col\n\n/**\n * A mixin trait that provides access to the stats fields in the transaction log.\n */\ntrait ReadsMetadataFields {\n  /** Returns a Column that references the stats field data skipping should use */\n  def getBaseStatsColumn: Column = col(getBaseStatsColumnName)\n  def getBaseStatsColumnName: String = \"stats\"\n}\n\n/**\n * A singleton of the Delta statistics field names.\n */\nobject DeltaStatistics {\n  /* The total number of records in the file. */\n  val NUM_RECORDS = \"numRecords\"\n  /* The smallest (possibly truncated) value for a column. */\n  val MIN = \"minValues\"\n  /* The largest (possibly truncated) value for a column. */\n  val MAX = \"maxValues\"\n  /* The number of null values present for a column. */\n  val NULL_COUNT = \"nullCount\"\n  /*\n   * Whether the column has tight or wide bounds.\n   * This should only be present in tables with Deletion Vectors enabled.\n   */\n  val TIGHT_BOUNDS = \"tightBounds\"\n\n  val ALL_STAT_FIELDS = Seq(NUM_RECORDS, MIN, MAX, NULL_COUNT, TIGHT_BOUNDS)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/StatisticsCollection.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.util.Locale\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\nimport scala.language.existentials\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.{Checkpoints, DeletionVectorsTableFeature, DeltaColumnMapping, DeltaColumnMappingMode, DeltaConfigs, DeltaErrors, DeltaIllegalArgumentException, DeltaLog, DeltaUDF, NoMapping}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY\nimport org.apache.spark.sql.delta.DeltaOperations.ComputeStats\nimport org.apache.spark.sql.delta.OptimisticTransaction\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.commands.DeltaCommand\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.schema.SchemaUtils.transformSchema\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeltaStatistics._\nimport org.apache.spark.sql.delta.stats.StatisticsCollection.getIndexedColumns\nimport org.apache.spark.sql.delta.util.DeltaSqlParserUtils\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.catalyst.parser.{AbstractSqlParser, AstBuilder, ParseException, ParserUtils}\nimport org.apache.spark.sql.catalyst.parser.SqlBaseParser.MultipartIdentifierListContext\nimport org.apache.spark.sql.catalyst.util.quoteIfNeeded\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Used to report metrics on how predicates are used to prune the set of\n * files that are read by a query.\n *\n * @param predicate         A user readable version of the predicate.\n * @param pruningType       One of {partition, dataStats, none}.\n * @param filesMissingStats The number of files that were included due to missing statistics.\n * @param filesDropped      The number of files that were dropped by this predicate.\n */\ncase class QueryPredicateReport(\n    predicate: String,\n    pruningType: String,\n    filesMissingStats: Long,\n    filesDropped: Long)\n\n/** Used to report details about prequery filtering of what data is scanned. */\ncase class FilterMetric(numFiles: Long, predicates: Seq[QueryPredicateReport])\n\n/**\n * A helper trait that constructs expressions that can be used to collect global\n * and column level statistics for a collection of data, given its schema.\n *\n * Global statistics (such as the number of records) are stored as top level columns.\n * Per-column statistics (such as min/max) are stored in a struct that mirrors the\n * schema of the data.\n *\n * To illustrate, here is an example of a data schema along with the schema of the statistics\n * that would be collected.\n *\n * Data Schema:\n *  {{{\n *  |-- a: struct (nullable = true)\n *  |    |-- b: struct (nullable = true)\n *  |    |    |-- c: long (nullable = true)\n *  }}}\n *\n * Collected Statistics:\n *  {{{\n *  |-- stats: struct (nullable = true)\n *  |    |-- numRecords: long (nullable = false)\n *  |    |-- minValues: struct (nullable = false)\n *  |    |    |-- a: struct (nullable = false)\n *  |    |    |    |-- b: struct (nullable = false)\n *  |    |    |    |    |-- c: long (nullable = true)\n *  |    |-- maxValues: struct (nullable = false)\n *  |    |    |-- a: struct (nullable = false)\n *  |    |    |    |-- b: struct (nullable = false)\n *  |    |    |    |    |-- c: long (nullable = true)\n *  |    |-- nullCount: struct (nullable = false)\n *  |    |    |-- a: struct (nullable = false)\n *  |    |    |    |-- b: struct (nullable = false)\n *  |    |    |    |    |-- c: long (nullable = true)\n *  }}}\n */\ntrait StatisticsCollection extends DeltaLogging {\n  protected def spark: SparkSession\n  /** The schema of the target table of this statistics collection. */\n  def tableSchema: StructType\n  /**\n   * The output attributes (`outputAttributeSchema`) that are replaced with table schema with\n   * the physical mapping information.\n   * NOTE: The partition columns' definitions are not included in this schema.\n   */\n  def outputTableStatsSchema: StructType\n  /**\n   * The schema of the output attributes of the write queries that needs to collect statistics.\n   * The partition columns' definitions are not included in this schema.\n   */\n  def outputAttributeSchema: StructType\n  /** The statistic indexed column specification of the target delta table. */\n  val statsColumnSpec: DeltaStatsColumnSpec\n  /** The column mapping mode of the target delta table. */\n  def columnMappingMode: DeltaColumnMappingMode\n\n  protected def protocol: Protocol\n\n  lazy val deletionVectorsSupported = protocol.isFeatureSupported(DeletionVectorsTableFeature)\n\n  private def effectiveSchema: StructType = if (statsColumnSpec.numIndexedColsOpt.isDefined) {\n    outputTableStatsSchema\n  } else {\n    tableSchema\n  }\n\n  private lazy val explodedDataSchemaNames: Seq[String] =\n    SchemaMergingUtils.explodeNestedFieldNames(outputAttributeSchema)\n\n  /**\n   * statCollectionPhysicalSchema is the schema that is composed of all the columns that have the\n   * stats collected with our current table configuration.\n   */\n  lazy val statCollectionPhysicalSchema: StructType =\n    getIndexedColumns(explodedDataSchemaNames, statsColumnSpec, effectiveSchema, columnMappingMode)\n\n  /**\n   * statCollectionLogicalSchema is the logical schema that is composed of all the columns that have\n   * the stats collected with our current table configuration.\n   */\n  lazy val statCollectionLogicalSchema: StructType =\n    getIndexedColumns(explodedDataSchemaNames, statsColumnSpec, effectiveSchema, NoMapping)\n\n  /**\n   * Traverses the [[statisticsSchema]] for the provided [[statisticsColumn]]\n   * and applies [[function]] to leaves.\n   *\n   * Note, for values that are outside the domain of the partial function we keep the original\n   * column. If the caller wants to drop the column needs to explicitly return None.\n   */\n  def applyFuncToStatisticsColumn(\n      statisticsSchema: StructType,\n      statisticsColumn: Column)(\n      function: PartialFunction[(Column, StructField), Option[Column]]): Seq[Column] = {\n    statisticsSchema.flatMap {\n      case StructField(name, s: StructType, _, _) =>\n        val column = statisticsColumn.getItem(name)\n        applyFuncToStatisticsColumn(s, column)(function) match {\n          case colSeq if colSeq.nonEmpty => Some(struct(colSeq: _*) as name)\n          case _ => None\n        }\n\n      case structField@StructField(name, _, _, _) =>\n        val column = statisticsColumn.getItem(name)\n        function.lift(column, structField).getOrElse(Some(column)).map(_.as(name))\n    }\n  }\n\n  /**\n   * Sets the TIGHT_BOUNDS column to false and converts the logical nullCount\n   * to a tri-state nullCount. The nullCount states are the following:\n   *    1) For \"all-nulls\" columns we set the physical nullCount which is equal to the\n   *       physical numRecords.\n   *    2) \"no-nulls\" columns remain unchanged, i.e. zero nullCount is the same for both\n   *       physical and logical representations.\n   *    3) For \"some-nulls\" columns, we leave the existing value. In files with wide bounds,\n   *       the nullCount in SOME_NULLs columns is considered unknown.\n   *\n   * The file's state can transition back to tight when statistics are recomputed. In that case,\n   * TIGHT_BOUNDS is set back to true and nullCount back to the logical value.\n   *\n   * Note, this function gets as input parsed statistics and returns a json document\n   * similarly to allFiles. To further match the behavior of allFiles we always return\n   * a column named `stats` instead of statsColName.\n   *\n   * @param withStats A dataFrame of actions with parsed statistics.\n   * @param statsColName The name of the parsed statistics column.\n   */\n  def updateStatsToWideBounds(withStats: DataFrame, statsColName: String): DataFrame = {\n    val dvCardinalityCol = coalesce(col(\"deletionVector.cardinality\"), lit(0))\n    val physicalNumRecordsCol = col(s\"$statsColName.$NUM_RECORDS\")\n    val logicalNumRecordsCol = physicalNumRecordsCol - dvCardinalityCol\n    val nullCountCol = col(s\"$statsColName.$NULL_COUNT\")\n    val tightBoundsCol = col(s\"$statsColName.$TIGHT_BOUNDS\")\n    val statsSchema = withStats.schema.apply(statsColName).dataType.asInstanceOf[StructType]\n\n    val allStatCols = ALL_STAT_FIELDS.flatMap {\n      case TIGHT_BOUNDS => Some(lit(false).as(TIGHT_BOUNDS))\n      case NULL_COUNT if statsSchema.names.contains(NULL_COUNT) =>\n        // Use the schema of the existing stats column. We only want to modify the existing\n        // nullCount stats. Note, when the column mapping mode is enabled, the schema uses\n        // the physical column names, not the logical names.\n        val nullCountSchema = statsSchema\n          .apply(NULL_COUNT).dataType.asInstanceOf[StructType]\n\n        // When bounds are tight and we are about to transition to wide, store the physical null\n        // count for ALL_NULLs columns.\n        val nullCountColSeq = applyFuncToStatisticsColumn(nullCountSchema, nullCountCol) {\n          case (c, _) =>\n            val allNullTightBounds = tightBoundsCol && (c === logicalNumRecordsCol)\n            Some(when(allNullTightBounds, physicalNumRecordsCol).otherwise(c))\n        }\n        Some(struct(nullCountColSeq: _*).as(NULL_COUNT))\n      case f if statsSchema.names.contains(f) => Some(col(s\"${statsColName}.${f}\"))\n      case _ =>\n        // This stat is not present in the original stats schema, so we should not include it.\n        None\n    }\n\n    // This may be very expensive because it is rewriting JSON.\n    withStats\n      .withColumn(\"stats\", when(col(statsColName).isNotNull, to_json(struct(allStatCols: _*))))\n      .drop(col(Checkpoints.STRUCT_STATS_COL_NAME)) // Note: does not always exist.\n  }\n\n  /**\n   * Returns the prefix length of strings that should be used for data skipping.\n   * Intentionally left abstract to let implementation decide whether table property overrides\n   * need to be included.\n   */\n  protected def getDataSkippingStringPrefixLength: Int\n\n  /**\n   * Returns a struct column that can be used to collect statistics for the current\n   * schema of the table.\n   * The types we keep stats on must be consistent with DataSkippingReader.SkippingEligibleLiteral.\n   * If a column is missing from dataSchema (which will be filled with nulls), we will only\n   * collect the NULL_COUNT stats for it as the number of rows.\n   */\n  lazy val statsCollector: Column = {\n    val stringPrefix = getDataSkippingStringPrefixLength\n\n    // On file initialization/stat recomputation TIGHT_BOUNDS is always set to true\n    val tightBoundsColOpt =\n      Option.when(deletionVectorsSupported &&\n          !spark.sessionState.conf.getConf(DeltaSQLConf.TIGHT_BOUND_COLUMN_ON_FILE_INIT_DISABLED)) {\n        lit(true).as(TIGHT_BOUNDS)\n      }\n\n    val statCols = Seq(\n      count(new Column(\"*\")) as NUM_RECORDS,\n      collectStats(MIN, statCollectionPhysicalSchema) {\n        // Truncate string min values as necessary\n        case (c, SkippingEligibleDataType(StringType), true) =>\n          substring(min(c), 0, stringPrefix)\n\n        // Write null for min/max Variant stats because collecting variant stats is not supported\n        // yet.\n        case (c, SkippingEligibleDataType(_: VariantType), true) =>\n          lit(null).cast(VariantType)\n\n        // Collect all numeric min values\n        case (c, SkippingEligibleDataType(_), true) =>\n          min(c)\n      },\n      collectStats(MAX, statCollectionPhysicalSchema) {\n        // Truncate and pad string max values as necessary\n        case (c, SkippingEligibleDataType(StringType), true) =>\n          val udfTruncateMax =\n            DeltaUDF.stringFromString(StatisticsCollection.truncateMaxStringAgg(stringPrefix)_)\n          udfTruncateMax(max(c))\n\n        // Write null for min/max Variant stats because collecting variant stats is not supported\n        // yet.\n        case (c, SkippingEligibleDataType(_: VariantType), true) =>\n          lit(null).cast(VariantType)\n\n        // Collect all numeric max values\n        case (c, SkippingEligibleDataType(_), true) =>\n          max(c)\n      },\n      collectStats(NULL_COUNT, statCollectionPhysicalSchema) {\n        case (c, _, true) => sum(when(c.isNull, 1).otherwise(0))\n        case (_, _, false) => count(new Column(\"*\"))\n      }) ++ tightBoundsColOpt\n\n    struct(statCols: _*).as(\"stats\")\n  }\n\n\n  /** Returns schema of the statistics collected. */\n  lazy val statsSchema: StructType = {\n    // In order to get the Delta min/max stats schema from table schema, we do 1) replace field\n    // name with physical name 2) set nullable to true 3) only keep stats eligible fields\n    // 4) omits metadata in table schema as Delta stats schema does not need the metadata\n    def getMinMaxStatsSchema(schema: StructType): Option[StructType] = {\n      val fields = schema.fields.flatMap {\n        case f@StructField(_, dataType: StructType, _, _) =>\n          getMinMaxStatsSchema(dataType).map { newDataType =>\n            StructField(DeltaColumnMapping.getPhysicalName(f), newDataType)\n          }\n        case f@StructField(_, SkippingEligibleDataType(dataType), _, _) =>\n          Some(StructField(DeltaColumnMapping.getPhysicalName(f), dataType))\n        case _ => None\n      }\n      if (fields.nonEmpty) Some(StructType(fields)) else None\n    }\n\n    // In order to get the Delta null count schema from table schema, we do 1) replace field name\n    // with physical name 2) set nullable to true 3) use LongType for all fields\n    // 4) omits metadata in table schema as Delta stats schema does not need the metadata\n    def getNullCountSchema(schema: StructType): Option[StructType] = {\n      val fields = schema.fields.flatMap {\n        case f@StructField(_, dataType: StructType, _, _) =>\n          getNullCountSchema(dataType).map { newDataType =>\n            StructField(DeltaColumnMapping.getPhysicalName(f), newDataType)\n          }\n        case f: StructField =>\n          Some(StructField(DeltaColumnMapping.getPhysicalName(f), LongType))\n      }\n      if (fields.nonEmpty) Some(StructType(fields)) else None\n    }\n\n    val minMaxStatsSchemaOpt = getMinMaxStatsSchema(statCollectionPhysicalSchema)\n    val nullCountSchemaOpt = getNullCountSchema(statCollectionPhysicalSchema)\n    val tightBoundsFieldOpt =\n      Option.when(deletionVectorsSupported)(TIGHT_BOUNDS -> BooleanType)\n\n    val fields =\n      Array(NUM_RECORDS -> LongType) ++\n      minMaxStatsSchemaOpt.map(MIN -> _) ++\n      minMaxStatsSchemaOpt.map(MAX -> _) ++\n      nullCountSchemaOpt.map(NULL_COUNT -> _) ++\n      tightBoundsFieldOpt\n\n    StructType(fields.map {\n      case (name, dataType) => StructField(name, dataType)\n    })\n  }\n\n  /**\n   * Recursively walks the given schema, constructing an expression to calculate\n   * multiple statistics that mirrors structure of the data. When `function` is\n   * defined for a given column, it return value is added to statistics structure.\n   * When `function` is not defined, that column is skipped.\n   *\n   * @param name     The name of the top level column for this statistic (i.e. minValues).\n   * @param schema   The schema of the data to collect statistics from.\n   * @param function A partial function that is passed a tuple of (column, metadata about that\n   *                 column, a flag that indicates whether the column is in the data schema). Based\n   *                 on the metadata and flag, the function can decide if the given statistic should\n   *                 be collected on the column by returning the correct aggregate expression.\n   * @param includeAllColumns  should statistics all the columns be included?\n   */\n  private def collectStats(\n      name: String,\n      schema: StructType,\n      includeAllColumns: Boolean = false)(\n      function: PartialFunction[(Column, StructField, Boolean), Column]): Column = {\n\n    def collectStats(\n      schema: StructType,\n      parent: Option[Column],\n      parentFields: Seq[String],\n      function: PartialFunction[(Column, StructField, Boolean), Column]): Seq[Column] = {\n      schema.flatMap {\n        case f @ StructField(name, s: StructType, _, _) =>\n          val column = parent.map(_.getItem(name))\n            .getOrElse(Column(UnresolvedAttribute.quoted(name)))\n          val stats = collectStats(s, Some(column), parentFields :+ name, function)\n          if (stats.nonEmpty) {\n            Some(struct(stats: _*) as DeltaColumnMapping.getPhysicalName(f))\n          } else {\n            None\n          }\n        case f @ StructField(name, _, _, _) =>\n          val fieldPath = UnresolvedAttribute(parentFields :+ name).name\n          val column = parent.map(_.getItem(name))\n            .getOrElse(Column(UnresolvedAttribute.quoted(name)))\n          // alias the column with its physical name\n          // Note: explodedDataSchemaNames comes from dataSchema. In the read path, dataSchema comes\n          // from the table's metadata.dataSchema, which is the same as tableSchema. In the\n          // write path, dataSchema comes from the DataFrame schema. We then assume\n          // TransactionWrite.writeFiles has normalized dataSchema, and\n          // TransactionWrite.getStatsSchema has done the column mapping for tableSchema and\n          // dropped the partition columns for both dataSchema and tableSchema.\n          function.lift((column, f, explodedDataSchemaNames.contains(fieldPath))).\n            map(_.as(DeltaColumnMapping.getPhysicalName(f)))\n      }\n    }\n\n    val stats = collectStats(schema, None, Nil, function)\n    if (stats.nonEmpty) {\n      struct(stats: _*).as(name)\n    } else {\n      lit(null).as(name)\n    }\n  }\n}\n\n/**\n * Specifies the set of columns to be used for stats collection on a table.\n * The `deltaStatsColumnNamesOpt` has higher priority than `numIndexedColsOpt`. Thus, if\n * `deltaStatsColumnNamesOpt` is not None, StatisticsCollection would only collects file statistics\n * for all columns inside it. Otherwise, `numIndexedColsOpt` is used.\n */\ncase class DeltaStatsColumnSpec(\n    deltaStatsColumnNamesOpt: Option[Seq[UnresolvedAttribute]],\n    numIndexedColsOpt: Option[Int]) {\n  require(deltaStatsColumnNamesOpt.isEmpty || numIndexedColsOpt.isEmpty)\n}\n\nobject StatisticsCollection extends DeltaCommand {\n\n  val ASCII_MAX_CHARACTER = '\\u007F'\n\n  val UTF8_MAX_CHARACTER = new String(Character.toChars(Character.MAX_CODE_POINT))\n\n  /**\n   * This method is the wrapper method to validates the DATA_SKIPPING_STATS_COLUMNS value of\n   * metadata.\n   */\n  def validateDeltaStatsColumns(metadata: Metadata): Unit = {\n    DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.fromMetaData(metadata).foreach { statsColumns =>\n      StatisticsCollection.validateDeltaStatsColumns(\n        metadata.dataSchema, metadata.partitionColumns, statsColumns\n      )\n    }\n  }\n\n  /**\n   * This method validates that the data type of a data skipping column provided in\n   * [[DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS]] supports data skipping based on file statistics.\n   * If a struct column is specified, all its children are considered valid. This helps users\n   * who have complex nested types and wish to collect stats on all supported nested columns\n   * without specifying each field individually. At stats collection time, unsupported types will\n   * simply be skipped, so it is safe to allow those through.\n   * @param name The name of the data skipping column for validating data type.\n   * @param dataType The data type of the data skipping column.\n   * @param columnPaths The column paths of all valid fields.\n   * @param insideStruct Tracks if the field is inside a user-specified struct. Don't throw an\n   *                     error on ineligible skipping types inside structs as the user didn't\n   *                     specify them directly. Simply log a warning to let the user know\n   *                     statistics won't be collected on that nested field.\n   */\n  private def validateDataSkippingType(\n      name: String,\n      dataType: DataType,\n      columnPaths: ArrayBuffer[String],\n      insideStruct: Boolean = false): Unit = dataType match {\n    case s: StructType =>\n      s.foreach { field =>\n        // we need to make sure we quote the field if needed otherwise we will not handle\n        // column names with special characters correctly.\n        validateDataSkippingType(name + \".\" +\n          quoteIfNeeded(field.name), field.dataType, columnPaths, insideStruct = true)\n      }\n    case SkippingEligibleDataType(_) =>\n      if (insideStruct) {\n        // If this is inside the struct we are already quoting the nested field name.\n        columnPaths.append(name)\n      } else {\n        columnPaths.append(quoteIfNeeded(name))\n      }\n    case _ if insideStruct =>\n      logWarning(s\"Data skipping is not supported for column $name of type $dataType\")\n      columnPaths.append(name)\n    case _ =>\n      throw new DeltaIllegalArgumentException(\n        errorClass = \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_TYPE\",\n        messageParameters = Array(name, dataType.toString))\n  }\n\n  /**\n   * This method validates whether the DATA_SKIPPING_STATS_COLUMNS value satisfies following\n   * conditions:\n   * 1. Delta statistics columns must not be partitioned column.\n   * 2. Delta statistics column must exist in delta table's schema.\n   * 3. Delta statistics columns must be data skipping type.\n   */\n  def validateDeltaStatsColumns(\n      schema: StructType, partitionColumns: Seq[String], deltaStatsColumnsConfigs: String): Unit = {\n    val partitionColumnSet = partitionColumns.map(_.toLowerCase(Locale.ROOT)).toSet\n    val visitedColumns = ArrayBuffer.empty[String]\n    DeltaSqlParserUtils.parseMultipartColumnList(deltaStatsColumnsConfigs).foreach { columns =>\n      columns.foreach { columnAttribute =>\n        val columnFullPath = columnAttribute.nameParts\n        // Delta statistics columns must not be partitioned column.\n        if (partitionColumnSet.contains(columnAttribute.name.toLowerCase(Locale.ROOT))) {\n          throw new DeltaIllegalArgumentException(\n            errorClass = \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_PARTITIONED_COLUMN\",\n            messageParameters = Array(columnAttribute.name))\n        }\n        // Delta statistics column must exist in delta table's schema.\n        SchemaUtils.findColumnPosition(columnFullPath, schema)\n        // Delta statistics columns must be data skipping type.\n        val (prefixPath, columnName) = columnFullPath.splitAt(columnFullPath.size - 1)\n        transformSchema(schema, Some(columnName.head)) {\n          case (`prefixPath`, struct @ StructType(_), _) =>\n            val columnField = struct(columnName.head)\n            // We need to figure out if the column is top-level column\n            // or a column inside a struct, we support collecting null count stats\n            // on nested columns part of a struct.\n            val fieldInsideStruct = prefixPath.size > 0\n            validateDataSkippingType(\n              columnAttribute.name,\n              columnField.dataType,\n              visitedColumns,\n              insideStruct = fieldInsideStruct)\n            struct\n          case (_, other, _) => other\n        }\n      }\n    }\n    val duplicatedColumnNames = visitedColumns\n      .groupBy(identity)\n      .collect { case (attribute, occurrences) if occurrences.size > 1 => attribute }\n      .toSeq\n    if (duplicatedColumnNames.size > 0) {\n      throw new DeltaIllegalArgumentException(\n        errorClass = \"DELTA_DUPLICATE_DATA_SKIPPING_COLUMNS\",\n        messageParameters = Array(duplicatedColumnNames.mkString(\",\"))\n      )\n    }\n  }\n\n  /**\n   * Removes the dropped columns from delta statistics column list inside\n   * DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.\n   * Note: This method is matching the logical name of tables with the columns inside\n   * DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.\n   */\n  def dropDeltaStatsColumns(\n      metadata: Metadata,\n      columnsToDrop: Seq[Seq[String]]): Map[String, String] = {\n    if (columnsToDrop.isEmpty) return Map.empty[String, String]\n    val deltaStatsColumnSpec = configuredDeltaStatsColumnSpec(metadata)\n    deltaStatsColumnSpec.deltaStatsColumnNamesOpt.map { deltaColumnsNames =>\n      val droppedColumnSet = columnsToDrop.toSet\n      val deltaStatsColumnStr = deltaColumnsNames\n        .map(_.nameParts)\n        .filterNot { attributeNameParts =>\n          droppedColumnSet.filter { droppedColumnParts =>\n            val commonPrefix = droppedColumnParts.zip(attributeNameParts)\n              .takeWhile { case (left, right) => left == right }\n              .size\n            commonPrefix == droppedColumnParts.size\n          }.nonEmpty\n        }\n        .map(columnParts => UnresolvedAttribute(columnParts).name)\n        .mkString(\",\")\n      Map(DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.key -> deltaStatsColumnStr)\n    }.getOrElse(Map.empty[String, String])\n  }\n\n  /**\n   * Rename the delta statistics column `oldColumnPath` of DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS\n   * to `newColumnPath`.\n   * Note: This method is matching the logical name of tables with the columns inside\n   * DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.\n   */\n  def renameDeltaStatsColumn(\n      metadata: Metadata,\n      oldColumnPath: Seq[String],\n      newColumnPath: Seq[String]): Map[String, String] = {\n    if (oldColumnPath == newColumnPath) return Map.empty[String, String]\n    val deltaStatsColumnSpec = configuredDeltaStatsColumnSpec(metadata)\n    SchemaUtils.renameColumnForConfig(\n      oldColumnPath,\n      newColumnPath,\n      deltaStatsColumnSpec.deltaStatsColumnNamesOpt,\n      DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.key)\n  }\n\n  /** Returns the configured set of columns to be used for stats collection on a table */\n  def configuredDeltaStatsColumnSpec(metadata: Metadata): DeltaStatsColumnSpec = {\n    val indexedColNamesOpt = DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.fromMetaData(metadata)\n    val numIndexedCols = DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.fromMetaData(metadata)\n    indexedColNamesOpt.map { indexedColNames =>\n      DeltaStatsColumnSpec(DeltaSqlParserUtils.parseMultipartColumnList(indexedColNames), None)\n    }.getOrElse {\n      DeltaStatsColumnSpec(None, Some(numIndexedCols))\n    }\n  }\n\n  /**\n   * Convert the logical name of each field to physical name according to the column mapping mode.\n   */\n  private[sql] def convertToPhysicalName(\n      fullPath: String,\n      field: StructField,\n      schemaNames: Seq[String],\n      mappingMode: DeltaColumnMappingMode): StructField = {\n    // If mapping mode is NoMapping or the dataSchemaName already contains the mapped\n    // column name, the schema mapping can be skipped.\n    if (mappingMode == NoMapping || schemaNames.contains(fullPath)) return field\n    // Check if the physical name exists.\n    if (!DeltaColumnMapping.hasPhysicalName(field)) {\n      throw DeltaErrors.missingPhysicalName(mappingMode, field.name)\n    }\n    // Get the physical column name from metadata.\n    val physicalName = field.metadata.getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n    field.dataType match {\n      case structType: StructType =>\n        val newDataType = StructType(\n          structType.map(child => convertToPhysicalName(fullPath, child, schemaNames, mappingMode))\n        )\n        field.copy(name = physicalName, dataType = newDataType)\n      case _ => field.copy(name = physicalName)\n    }\n  }\n\n  /**\n   * Generates a filtered data schema for stats collection.\n   * Note: This method is matching the logical name of tables with the columns inside\n   * DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS. The output of the filter schema is translated into\n   * physical name.\n   *\n   * @param schemaNames the full name path of all columns inside `schema`.\n   * @param schema the original data schema.\n   * @param statsColPaths the specific set of columns to collect stats on.\n   * @param mappingMode the column mapping mode of this statistics collection.\n   * @param parentPath the parent column path of `schema`.\n   * @return filtered schema\n   */\n  private def filterSchema(\n      schemaNames: Seq[String],\n      schema: StructType,\n      statsColPaths: Seq[Seq[String]],\n      mappingMode: DeltaColumnMappingMode,\n      parentPath: Seq[String] = Seq.empty): StructType = {\n    // Find the unique column names at this nesting depth, each with its path remainders (if any)\n    val cols = statsColPaths.groupBy(_.head).mapValues(_.map(_.tail))\n    val newSchema = schema.flatMap { field =>\n      val lowerCaseFieldName = field.name.toLowerCase(Locale.ROOT)\n      cols.get(lowerCaseFieldName).flatMap { paths =>\n        field.dataType match {\n          case _ if paths.forall(_.isEmpty) =>\n            // Convert full path to lower cases to avoid schema name contains upper case\n            // characters.\n            val fullPath = (parentPath :+ field.name).mkString(\".\").toLowerCase(Locale.ROOT)\n            Some(convertToPhysicalName(fullPath, field, schemaNames, mappingMode))\n          case fieldSchema: StructType =>\n            // Convert full path to lower cases to avoid schema name contains upper case\n            // characters.\n            val fullPath = (parentPath :+ field.name).mkString(\".\").toLowerCase(Locale.ROOT)\n            val physicalName = if (mappingMode == NoMapping || schemaNames.contains(fullPath)) {\n              field.name\n            } else {\n              field.metadata.getString(COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n            }\n            // Recurse into the child fields of this struct.\n            val newSchema = filterSchema(\n              schemaNames,\n              fieldSchema,\n              paths.filterNot(_.isEmpty),\n              mappingMode,\n              parentPath:+ field.name\n            )\n            Some(field.copy(name = physicalName, dataType = newSchema))\n          case _ =>\n            // Filter expected a nested field and this isn't nested. No match\n            None\n        }\n      }\n    }\n    StructType(newSchema.toArray)\n  }\n\n  /**\n   * Computes the set of columns to be used for stats collection on a table. Specific named columns\n   * take precedence, if provided; otherwise the first numIndexedColsOpt are extracted from the\n   * schema.\n   */\n  def getIndexedColumns(\n      schemaNames: Seq[String],\n      spec: DeltaStatsColumnSpec,\n      schema: StructType,\n      mappingMode: DeltaColumnMappingMode): StructType = {\n    spec.deltaStatsColumnNamesOpt\n      .map { indexedColNames =>\n        // convert all index columns to lower case characters to avoid user assigning any upper\n        // case characters.\n        val indexedColPaths = indexedColNames.map(_.nameParts.map(_.toLowerCase(Locale.ROOT)))\n        filterSchema(schemaNames, schema, indexedColPaths, mappingMode)\n      }\n      .getOrElse {\n        val numIndexedCols = spec.numIndexedColsOpt.get\n        if (numIndexedCols < 0) {\n          schema // negative means don't truncate the schema\n        } else {\n          truncateSchema(schema, numIndexedCols)._1\n        }\n      }\n  }\n\n  /**\n   * Generates a truncated data schema for stats collection.\n   * @param schema the original data schema\n   * @param indexedCols the maximum number of leaf columns to collect stats on\n   * @return truncated schema and the number of leaf columns in this schema\n   */\n  private def truncateSchema(schema: StructType, indexedCols: Int): (StructType, Int) = {\n    var accCnt = 0\n    var i = 0\n    val fields = new ArrayBuffer[StructField]()\n    while (i < schema.length && accCnt < indexedCols) {\n      val field = schema.fields(i)\n      val newField = field match {\n        case StructField(name, st: StructType, nullable, metadata) =>\n          val (newSt, cnt) = truncateSchema(st, indexedCols - accCnt)\n          accCnt += cnt\n          StructField(name, newSt, nullable, metadata)\n        case f =>\n          accCnt += 1\n          f\n      }\n      i += 1\n      fields += newField\n    }\n    (StructType(fields.toSeq), accCnt)\n  }\n\n  /**\n   * Compute the AddFile entries with delta statistics entries by aggregating the data skipping\n   * columns of each parquet file.\n   */\n  private def computeNewAddFiles(\n      deltaLog: DeltaLog,\n      txn: OptimisticTransaction,\n      files: Seq[AddFile]): Array[AddFile] = {\n    val dataPath = deltaLog.dataPath\n    val pathToAddFileMap = generateCandidateFileMap(dataPath, files)\n    val persistentDVsReadable = DeletionVectorUtils.deletionVectorsReadable(txn.snapshot)\n    // Throw error when the table contains DVs, because existing method of stats\n    // recomputation doesn't work on tables with DVs. It needs to take into consideration of\n    // DV files (TODO).\n    if (persistentDVsReadable) {\n      throw DeltaErrors.statsRecomputeNotSupportedOnDvTables()\n    }\n    val fileDataFrame = deltaLog\n      .createDataFrame(txn.snapshot, addFiles = files, isStreaming = false)\n      .withColumn(\"path\", col(\"_metadata.file_path\"))\n    val newStats = fileDataFrame.groupBy(col(\"path\")).agg(to_json(txn.statsCollector))\n    newStats.collect().map { r =>\n      val add = getTouchedFile(dataPath, r.getString(0), pathToAddFileMap)\n      add.copy(dataChange = false, stats = r.getString(1))\n    }\n  }\n\n  /**\n   * Recomputes statistics for a Delta table. This can be used to compute stats if they were never\n   * collected or to recompute corrupted statistics.\n   * @param deltaLog Delta log for the table to update.\n   * @param predicates Which subset of the data to recompute stats for. Predicates must use only\n   *                   partition columns.\n   * @param fileFilter Filter for which AddFiles to recompute stats for.\n   */\n  def recompute(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      catalogTable: Option[CatalogTable],\n      predicates: Seq[Expression] = Seq(Literal(true)),\n      fileFilter: AddFile => Boolean = af => true): Unit = {\n    val txn = deltaLog.startTransaction(catalogTable)\n    verifyPartitionPredicates(spark, txn.metadata.partitionColumns, predicates)\n    // Save the current AddFiles that match the predicates so we can update their stats\n    val files = txn.filterFiles(predicates).filter(fileFilter)\n    val newAddFiles = computeNewAddFiles(deltaLog, txn, files)\n    txn.commit(newAddFiles, ComputeStats(predicates))\n  }\n\n  def truncateMinStringAgg(prefixLen: Int)(input: String): String = {\n    if (input == null || input.length <= prefixLen) {\n      return input\n    }\n    if (prefixLen <= 0) {\n      return null\n    }\n    if (Character.isHighSurrogate(input.charAt(prefixLen - 1)) &&\n        Character.isLowSurrogate(input.charAt(prefixLen))) {\n      // If the character at prefixLen - 1 is a high surrogate and the next character is a low\n      // surrogate, we need to include the next character in the prefix to ensure that we don't\n      // truncate the string in the middle of a surrogate pair.\n      input.take(prefixLen + 1)\n    } else {\n      input.take(prefixLen)\n    }\n  }\n\n  /**\n   * Helper method to truncate the input string `input` to the given `prefixLen` length, while also\n   * ensuring the any value in this column is less than or equal to the truncated max in UTF-8\n   * encoding.\n   */\n  def truncateMaxStringAgg(prefixLen: Int)(originalMax: String): String = {\n    // scalastyle:off nonascii\n    if (originalMax == null || originalMax.length <= prefixLen) {\n      return originalMax\n    }\n    if (prefixLen <= 0) {\n      return null\n    }\n\n    // Grab the prefix. We want to append max Unicode code point `\\uDBFF\\uDFFF` as a tie-breaker,\n    // but that is only safe if the character we truncated was smaller in UTF-8 encoded binary\n    // comparison. Keep extending the prefix until that condition holds, or we run off the end of\n    // the string.\n    // We also try to use the ASCII max character `\\u007F` as a tie-breaker if possible.\n    val maxLen = getExpansionLimit(prefixLen)\n    // Start with a valid prefix\n    var currLen = truncateMinStringAgg(prefixLen)(originalMax).length\n    while (currLen <= maxLen) {\n      if (currLen >= originalMax.length) {\n        // Return originalMax if we have reached the end of the string\n        return originalMax\n      } else if (currLen + 1 < originalMax.length &&\n          originalMax.substring(currLen, currLen + 2) == UTF8_MAX_CHARACTER) {\n        // Skip the UTF-8 max character. It occupies two characters in a Scala string.\n        currLen += 2\n      } else if (originalMax.charAt(currLen) < ASCII_MAX_CHARACTER) {\n        return originalMax.take(currLen) + ASCII_MAX_CHARACTER\n      } else {\n        return originalMax.take(currLen) + UTF8_MAX_CHARACTER\n      }\n    }\n\n    // Return null when the input string is too long to truncate.\n    null\n    // scalastyle:on nonascii\n  }\n\n  /**\n   * Calculates the upper character limit when constructing a maximum is not possible with only\n   * prefixLen chars.\n   */\n  private def getExpansionLimit(prefixLen: Int): Int = 2 * prefixLen\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/StatsCollectionUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.concurrent.duration.Duration\nimport scala.language.existentials\nimport scala.util.control.NonFatal\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaColumnMappingMode, DeltaConfigs, DeltaErrors, DeltaLog, IdMapping, NameMapping, NoMapping, Snapshot}\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata}\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeltaStatistics._\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, JsonUtils}\nimport org.apache.spark.sql.delta.util.threads.DeltaThreadPool\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.FileSystem\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.hadoop.ParquetFileReader\nimport org.apache.parquet.hadoop.metadata.{BlockMetaData, ParquetMetadata}\nimport org.apache.parquet.io.api.Binary\nimport org.apache.parquet.schema.LogicalTypeAnnotation._\nimport org.apache.parquet.schema.PrimitiveType\n\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils\nimport org.apache.spark.sql.execution.datasources.DataSourceUtils\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{ArrayType, LongType, MapType, StructField, StructType}\nimport org.apache.spark.util.SerializableConfiguration\n\n\nobject StatsCollectionUtils\n  extends Logging\n{\n\n  /**\n   * A helper function to compute stats of addFiles using StatsCollector.\n   *\n   * @param spark The SparkSession used to process data.\n   * @param conf The Hadoop configuration used to access file system.\n   * @param deltaLog The delta log of table, to which these AddFile(s) belong.\n   * @param snapshot The snapshot of the table used to derive table schema information. We do not\n   *                 derive it from deltaLog because a snapshot may not exist yet.\n   * @param addFiles The list of target AddFile(s) to be processed.\n   * @param numFilesOpt The number of AddFile(s) to process if known. Speeds up the query.\n   * @param ignoreMissingStats Whether to ignore missing stats during computation.\n   * @param setBoundsToWide Whether to set bounds to wide independently of whether or not\n   *                        the files have DVs.\n   *\n   * @return A list of AddFile(s) with newly computed stats, please note the existing stats from\n   *         the input addFiles will be ignored regardless.\n   */\n  def computeStats(\n      spark: SparkSession,\n      conf: Configuration,\n      deltaLog: DeltaLog,\n      snapshot: Snapshot,\n      addFiles: Dataset[AddFile],\n      numFilesOpt: Option[Long],\n      stringTruncateLength: Int,\n      ignoreMissingStats: Boolean = true,\n      setBoundsToWide: Boolean = false): Dataset[AddFile] = {\n\n    val useMultiThreadedStatsCollection = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_USE_MULTI_THREADED_STATS_COLLECTION)\n    val preparedAddFiles = if (useMultiThreadedStatsCollection) {\n      prepareRDDForMultiThreadedStatsCollection(spark, addFiles, numFilesOpt)\n    } else {\n      addFiles\n    }\n\n    val parquetRebaseMode =\n      spark.sessionState.conf.getConf(SQLConf.PARQUET_REBASE_MODE_IN_READ).toString\n\n    val statsCollector = StatsCollector(\n      snapshot.columnMappingMode,\n      snapshot.dataSchema,\n      snapshot.statsSchema,\n      parquetRebaseMode,\n      ignoreMissingStats,\n      Some(stringTruncateLength))\n\n    val serializableConf = new SerializableConfiguration(conf)\n    val broadcastConf = spark.sparkContext.broadcast(serializableConf)\n\n    val dataRootDir = deltaLog.dataPath.toString\n\n    import org.apache.spark.sql.delta.implicits._\n    preparedAddFiles.mapPartitions { addFileIter =>\n      val defaultFileSystem = new Path(dataRootDir).getFileSystem(broadcastConf.value.value)\n      if (useMultiThreadedStatsCollection) {\n        ParallelFetchPool.parallelMap(spark, addFileIter.toSeq) { addFile =>\n          computeStatsForFile(\n            addFile,\n            dataRootDir,\n            defaultFileSystem,\n            broadcastConf.value,\n            setBoundsToWide,\n            statsCollector)\n        }.toIterator\n      } else {\n        addFileIter.map { addFile =>\n          computeStatsForFile(\n            addFile,\n            dataRootDir,\n            defaultFileSystem,\n            broadcastConf.value,\n            setBoundsToWide,\n            statsCollector)\n        }\n      }\n    }\n  }\n\n  /**\n   * Prepares the underlying RDD of [[addFiles]] for multi-threaded stats collection by splitting\n   * them up into more partitions if necessary.\n   * If the number of partitions is too small, not every executor might\n   * receive a partition, which reduces the achievable parallelism. By increasing the number of\n   * partitions we can achieve more parallelism.\n   */\n  private def prepareRDDForMultiThreadedStatsCollection(\n      spark: SparkSession,\n      addFiles: Dataset[AddFile],\n      numFilesOpt: Option[Long]): Dataset[AddFile] = {\n\n    val numFiles = numFilesOpt.getOrElse(addFiles.count())\n    val currNumPartitions = addFiles.rdd.getNumPartitions\n    val numFilesPerPartition = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_STATS_COLLECTION_NUM_FILES_PARTITION)\n\n    // We should not create more partitions than the cluster can currently handle.\n    val minNumPartitions = Math.min(\n      spark.sparkContext.defaultParallelism,\n      numFiles / numFilesPerPartition + 1).toInt\n    // Only repartition if it would increase the achievable parallelism\n    if (currNumPartitions < minNumPartitions) {\n      addFiles.repartition(minNumPartitions)\n    } else {\n      addFiles\n    }\n  }\n\n  private def computeStatsForFile(\n      addFile: AddFile,\n      dataRootDir: String,\n      defaultFileSystem: FileSystem,\n      config: SerializableConfiguration,\n      setBoundsToWide: Boolean,\n      statsCollector: StatsCollector): AddFile = {\n    val path = DeltaFileOperations.absolutePath(dataRootDir, addFile.path)\n    val fileStatus = if (path.toString.startsWith(dataRootDir)) {\n      defaultFileSystem.getFileStatus(path)\n    } else {\n      path.getFileSystem(config.value).getFileStatus(path)\n    }\n\n    val (stats, metric) = statsCollector.collect(\n      ParquetFileReader.readFooter(config.value, fileStatus))\n\n    if (metric.totalMissingFields > 0 || metric.numMissingTypes > 0) {\n      logWarning(\n        log\"StatsCollection of file `${MDC(DeltaLogKeys.PATH, path)}` \" +\n        log\"misses fields/types: ${MDC(DeltaLogKeys.METRICS, JsonUtils.toJson(metric))}\")\n    }\n\n    val statsWithTightBoundsCol = {\n      val hasDeletionVector =\n        addFile.deletionVector != null && !addFile.deletionVector.isEmpty\n      stats + (TIGHT_BOUNDS -> !(setBoundsToWide || hasDeletionVector))\n    }\n\n    addFile.copy(stats = JsonUtils.toJson(statsWithTightBoundsCol))\n  }\n\n  /**\n   * Get the string prefix length used for data skipping based on the following precedence:\n   *   1. If the provided metadata is not null, and the delta table property is set inside, use it;\n   *   2. Otherwise, use the Spark configuration.\n   */\n  def getDataSkippingStringPrefixLength(spark: SparkSession, metadata: Metadata): Int = {\n    Option(metadata)\n      .flatMap(DeltaConfigs.DATA_SKIPPING_STRING_PREFIX_LENGTH.fromMetaData)\n      .getOrElse(spark.sessionState.conf.getConf(DeltaSQLConf.DATA_SKIPPING_STRING_PREFIX_LENGTH))\n  }\n}\n\nobject ParallelFetchPool {\n  val NUM_THREADS_PER_CORE = 10\n  val MAX_THREADS = 1024\n\n  val NUM_THREADS = Math.min(\n    Runtime.getRuntime.availableProcessors() * NUM_THREADS_PER_CORE, MAX_THREADS)\n\n  lazy val threadPool = DeltaThreadPool(\"stats-collection\", NUM_THREADS)\n  def parallelMap[T, R](\n      spark: SparkSession,\n      items: Iterable[T])(\n      f: T => R): Iterable[R] = threadPool.parallelMap(spark, items)(f)\n}\n\n/**\n * A helper class to collect stats of parquet data files for Delta table and its equivalent (tables\n * that can be converted into Delta table like Parquet/Iceberg table).\n *\n * @param dataSchema The data schema from table metadata, which is the logical schema with logical\n *                   to physical mapping per schema field. It is used to map statsSchema to parquet\n *                   metadata.\n * @param statsSchema The schema of stats to be collected, statsSchema should follow the physical\n *                    schema and must be generated by StatisticsCollection.\n * @param parquetRebaseMode The parquet rebase mode used to parse date and timestamp.\n * @param ignoreMissingStats Indicate whether to return partial result by ignoring missing stats\n *                           or throw an exception.\n * @param stringTruncateLength The optional max length of string stats to be truncated into.\n *\n * Scala Example:\n * {{{\n * import org.apache.spark.sql.delta.stats.StatsCollector\n *\n * val stringTruncateLength =\n *   spark.sessionState.conf.getConf(DeltaSQLConf.DATA_SKIPPING_STRING_PREFIX_LENGTH)\n *\n * val statsCollector = StatsCollector(\n *   snapshot.metadata.columnMappingMode, snapshot.metadata.dataSchema, snapshot.statsSchema,\n *   ignoreMissingStats = false, Some(stringTruncateLength))\n *\n * val filesWithStats = snapshot.allFiles.map { file =>\n *   val path = DeltaFileOperations.absolutePath(dataPath, file.path)\n *   val fileSystem = path.getFileSystem(hadoopConf)\n *   val fileStatus = fileSystem.listStatus(path).head\n *\n *   val footer = ParquetFileReader.readFooter(hadoopConf, fileStatus)\n *   val (stats, _) = statsCollector.collect(footer)\n *   file.copy(stats = JsonUtils.toJson(stats))\n * }\n * }}}\n */\nabstract class StatsCollector(\n    dataSchema: StructType,\n    statsSchema: StructType,\n    parquetRebaseMode: String,\n    ignoreMissingStats: Boolean,\n    stringTruncateLength: Option[Int])\n  extends Serializable\n{\n\n  final val NUM_MISSING_TYPES = \"numMissingTypes\"\n\n  /**\n   * Used to report number of missing fields per supported type and number of missing unsupported\n   * types in the collected statistics, currently the statistics collection supports 4 types of\n   * stats: NUM_RECORDS, MAX, MIN, NULL_COUNT.\n   *\n   * @param numMissingMax The number of missing fields for MAX\n   * @param numMissingMin The number of missing fields for MIN\n   * @param numMissingNullCount The number of missing fields for NULL_COUNT\n   * @param numMissingTypes The number of unsupported type being requested.\n   */\n  case class StatsCollectionMetrics(\n      numMissingMax: Long,\n      numMissingMin: Long,\n      numMissingNullCount: Long,\n      numMissingTypes: Long) {\n\n    val totalMissingFields: Long = Seq(numMissingMax, numMissingMin, numMissingNullCount).sum\n  }\n\n  object StatsCollectionMetrics {\n    def apply(missingFieldCounts: Map[String, Long]): StatsCollectionMetrics = {\n      StatsCollectionMetrics(\n        missingFieldCounts.getOrElse(MAX, 0L),\n        missingFieldCounts.getOrElse(MIN, 0L),\n        missingFieldCounts.getOrElse(NULL_COUNT, 0L),\n        missingFieldCounts.getOrElse(NUM_MISSING_TYPES, 0L))\n    }\n  }\n\n  /**\n   * A list of schema physical path and corresponding struct field of leaf fields. Beside primitive\n   * types, Map and Array (instead of their sub-columns) are also treated as leaf fields since we\n   * only compute null count of them, and null is counted based on themselves instead of sub-fields.\n   */\n  protected lazy val schemaPhysicalPathAndSchemaField: Seq[(Seq[String], StructField)] = {\n    def explode(schema: StructType): Seq[(Seq[String], StructField)] = {\n      schema.flatMap { field =>\n        val physicalName = DeltaColumnMapping.getPhysicalName(field)\n        field.dataType match {\n          case s: StructType =>\n            explode(s).map { case (path, field) => (Seq(physicalName) ++ path, field) }\n          case _ => (Seq(physicalName), field) :: Nil\n        }\n      }\n    }\n    explode(dataSchema)\n  }\n\n  /**\n   * Returns the map from schema physical field path (field for which to collect stats) to the\n   * parquet metadata column index (where to collect stats). statsSchema generated by\n   * StatisticsCollection always use physical field paths so physical field paths are the same as\n   * to the ones used in statsSchema. Child class must implement this method based on delta column\n   * mapping mode.\n   */\n  def getSchemaPhysicalPathToParquetIndex(blockMetaData: BlockMetaData): Map[Seq[String], Int]\n\n  /**\n   * Collects the stats from [[ParquetMetadata]]\n   *\n   * @param parquetMetadata The metadata of parquet file following physical schema, it contains\n   *                        statistics of row groups.\n   *\n   * @return A nested Map[String: Any] from requested stats field names to their stats field value\n   *         and [[StatsCollectionMetrics]] counting the number of missing fields/types.\n   */\n  final def collect(\n      parquetMetadata: ParquetMetadata): (Map[String, Any], StatsCollectionMetrics) = {\n    val blocks = parquetMetadata.getBlocks.asScala.toSeq\n    if (blocks.isEmpty) {\n      return (Map(NUM_RECORDS -> 0L), StatsCollectionMetrics(Map.empty[String, Long]))\n    }\n\n    val schemaPhysicalPathToParquetIndex = getSchemaPhysicalPathToParquetIndex(blocks.head)\n    val dateRebaseSpec = DataSourceUtils.datetimeRebaseSpec(\n      parquetMetadata.getFileMetaData.getKeyValueMetaData.get, parquetRebaseMode)\n    val dateRebaseFunc = DataSourceUtils.createDateRebaseFuncInRead(dateRebaseSpec.mode, \"Parquet\")\n\n    val missingFieldCounts =\n      mutable.Map(MAX -> 0L, MIN -> 0L, NULL_COUNT -> 0L, NUM_MISSING_TYPES -> 0L)\n\n    // Collect the actual stats.\n    //\n    // The result of this operation is a tree of maps that matches the structure of the stats\n    // schema. The stats schema is split by stats type at the top, and each type matches the\n    // structure of the data schema (can be subset), so we collect per stats type. E.g. the MIN\n    // values are under MIN.a, MIN.b.c, MIN.b.d etc., and then the MAX values are under MAX.a,\n    // MAX.b.c etc. Note, we do omit here the tightBounds column and add it at a later stage.\n    val collectedStats = statsSchema.filter(_.name != TIGHT_BOUNDS).map {\n      case StructField(NUM_RECORDS, LongType, _, _) =>\n        val numRecords = blocks.map { block =>\n          block.getRowCount\n        }.sum\n        NUM_RECORDS -> numRecords\n      case StructField(MIN, statsTypeSchema: StructType, _, _) =>\n        val (minValues, numMissingFields) =\n          collectStats(Seq.empty[String], statsTypeSchema, blocks, schemaPhysicalPathToParquetIndex,\n            ignoreMissingStats)(aggMaxOrMin(dateRebaseFunc, isMax = false))\n        missingFieldCounts(MIN) += numMissingFields\n        MIN -> minValues\n      case StructField(MAX, statsTypeSchema: StructType, _, _) =>\n        val (maxValues, numMissingFields) =\n          collectStats(Seq.empty[String], statsTypeSchema, blocks, schemaPhysicalPathToParquetIndex,\n            ignoreMissingStats)(aggMaxOrMin(dateRebaseFunc, isMax = true))\n        missingFieldCounts(MAX) += numMissingFields\n        MAX -> maxValues\n      case StructField(NULL_COUNT, statsTypeSchema: StructType, _, _) =>\n        val (nullCounts, numMissingFields) =\n          collectStats(Seq.empty[String], statsTypeSchema, blocks, schemaPhysicalPathToParquetIndex,\n            ignoreMissingStats)(aggNullCount)\n        missingFieldCounts(NULL_COUNT) += numMissingFields\n        NULL_COUNT -> nullCounts\n      case field: StructField =>\n        if (ignoreMissingStats) {\n          missingFieldCounts(NUM_MISSING_TYPES) += 1\n          field.name -> Map.empty[String, Any]\n        } else {\n          throw new UnsupportedOperationException(s\"stats type not supported: ${field.name}\")\n        }\n    }.toMap\n\n    (collectedStats, StatsCollectionMetrics(missingFieldCounts.toMap))\n  }\n\n  /**\n   * Collects statistics by recurring through the structure of statsSchema and tracks the fields\n   * that we have seen so far in parentPhysicalPath.\n   *\n   * @param parentPhysicalFieldPath The absolute path of parent field with physical names.\n   * @param statsSchema The schema with physical names to collect stats recursively.\n   * @param blocks The metadata of Parquet row groups, which contains the raw stats.\n   * @param schemaPhysicalPathToParquetIndex Map from schema path to parquet metadata column index.\n   * @param ignoreMissingStats Whether to ignore and log missing fields or throw an exception.\n   * @param aggFunc The aggregation function used to aggregate stats across row.\n   *\n   * @return A nested Map[String: Any] from schema field name to stats value and a count of missing\n   *         fields.\n   *\n   * Here is an example of stats:\n   *\n   * stats schema:\n   * | -- id: INT\n   * | -- person: STRUCT\n   *     | name: STRUCT\n   *         | -- first: STRING\n   *         | -- last: STRING\n   *     | height: LONG\n   *\n   * The stats:\n   * Map(\n   *   \"id\" -> 1003,\n   *   \"person\" -> Map(\n   *      \"name\" -> Map(\n   *         \"first\" -> \"Chris\",\n   *         \"last\" -> \"Green\"\n   *       ),\n   *       \"height\" -> 175L\n   *   )\n   * )\n   */\n  private def collectStats(\n      parentPhysicalFieldPath: Seq[String],\n      statsSchema: StructType,\n      blocks: Seq[BlockMetaData],\n      schemaPhysicalPathToParquetIndex: Map[Seq[String], Int],\n      ignoreMissingStats: Boolean)(\n      aggFunc: (Seq[BlockMetaData], Int) => Any): (Map[String, Any], Long) = {\n    val stats = mutable.Map.empty[String, Any]\n    var numMissingFields = 0L\n    statsSchema.foreach {\n      case StructField(name, dataType: StructType, _, _) =>\n        val (map, numMissingFieldsInSubtree) =\n          collectStats(parentPhysicalFieldPath :+ name, dataType, blocks,\n            schemaPhysicalPathToParquetIndex, ignoreMissingStats)(aggFunc)\n        numMissingFields += numMissingFieldsInSubtree\n        if (map.nonEmpty) {\n          stats += name -> map\n        }\n      case StructField(name, _, _, _) =>\n        val physicalFieldPath = parentPhysicalFieldPath :+ name\n        if (schemaPhysicalPathToParquetIndex.contains(physicalFieldPath)) {\n          try {\n            val value = aggFunc(blocks, schemaPhysicalPathToParquetIndex(physicalFieldPath))\n            // None value means the stats is undefined for this field (e.g., max/min of a field,\n            // whose values are nulls in all blocks), we use null to be consistent with stats\n            // generated from SQL.\n            if (value != None) {\n              stats += name -> value\n            } else {\n              stats += name -> null\n            }\n          } catch {\n            case NonFatal(_) if ignoreMissingStats => numMissingFields += 1L\n            case exception: Throwable => throw exception\n          }\n        } else if (ignoreMissingStats) {\n          // Physical field path requested by stats is missing in the mapping, so it's missing from\n          // the parquet metadata.\n          numMissingFields += 1L\n        } else {\n          val columnPath = physicalFieldPath.mkString(\"[\", \", \", \"]\")\n          throw DeltaErrors.deltaStatsCollectionColumnNotFound(\"all\", columnPath)\n        }\n    }\n\n    (stats.toMap, numMissingFields)\n  }\n\n  /**\n   * The aggregation function used to collect the max and min of a column across blocks,\n   * dateRebaseFunc is used to adapt legacy date.\n   */\n  private def aggMaxOrMin(\n      dateRebaseFunc: Int => Int, isMax: Boolean)(\n      blocks: Seq[BlockMetaData], index: Int): Any = {\n    val columnMetadata = blocks.head.getColumns.get(index)\n    val primitiveType = columnMetadata.getPrimitiveType\n    val logicalType = primitiveType.getLogicalTypeAnnotation\n    // Physical type of timestamp is INT96 in both Parquet and Delta.\n    if (primitiveType.getPrimitiveTypeName == PrimitiveType.PrimitiveTypeName.INT96 ||\n        logicalType.isInstanceOf[TimestampLogicalTypeAnnotation]) {\n      throw new UnsupportedOperationException(\n        s\"max/min stats is not supported for timestamp: ${columnMetadata.getPath}\")\n    }\n\n    var aggregatedValue: Any = None\n    blocks.foreach { block =>\n      val column = block.getColumns.get(index)\n      val statistics = column.getStatistics\n      // Skip this block if the column has null for all rows, stats is defined as long as it exists\n      // in even a single block.\n      if (statistics.hasNonNullValue) {\n        val currentValue = if (isMax) statistics.genericGetMax else statistics.genericGetMin\n        if (currentValue == null) {\n          throw DeltaErrors.deltaStatsCollectionColumnNotFound(\"max/min\", column.getPath.toString)\n        }\n\n        if (aggregatedValue == None) {\n          aggregatedValue = currentValue\n        } else {\n          // TODO: check NaN value for floating point columns.\n          val compareResult = currentValue.asInstanceOf[Comparable[Any]].compareTo(aggregatedValue)\n          if ((isMax && compareResult > 0) || (!isMax && compareResult < 0)) {\n            aggregatedValue = currentValue\n          }\n        }\n      }\n    }\n\n    // All blocks have null stats for this column, returns None to indicate the stats of this\n    // column is undefined.\n    if (aggregatedValue == None) return None\n\n    aggregatedValue match {\n      // String\n      case bytes: Binary if logicalType.isInstanceOf[StringLogicalTypeAnnotation] =>\n        val rawString = bytes.toStringUsingUTF8\n        if (stringTruncateLength.isDefined && rawString.length > stringTruncateLength.get) {\n          if (isMax) {\n            // Append tie breakers to assure that any value in this column is less than or equal to\n            // the max, check the helper function for more details.\n            StatisticsCollection.truncateMaxStringAgg(stringTruncateLength.get)(rawString)\n          } else {\n            StatisticsCollection.truncateMinStringAgg(stringTruncateLength.get)(rawString)\n          }\n        } else {\n          rawString\n        }\n      // Binary\n      case _: Binary =>\n        throw new UnsupportedOperationException(\n          s\"max/min stats is not supported for binary other than string: ${columnMetadata.getPath}\")\n      // Date\n      case date: Integer if logicalType.isInstanceOf[DateLogicalTypeAnnotation] =>\n        DateTimeUtils.toJavaDate(dateRebaseFunc(date)).toString\n      // Byte, Short, Integer and Long\n      case intValue @ (_: Integer | _: java.lang.Long)\n        if logicalType.isInstanceOf[IntLogicalTypeAnnotation] =>\n          logicalType.asInstanceOf[IntLogicalTypeAnnotation].getBitWidth match {\n            case 8 => intValue.asInstanceOf[Int].toByte\n            case 16 => intValue.asInstanceOf[Int].toShort\n            case 32 => intValue.asInstanceOf[Int]\n            case 64 => intValue.asInstanceOf[Long]\n            case other => throw new UnsupportedOperationException(\n              s\"max/min stats is not supported for $other-bits Integer: ${columnMetadata.getPath}\")\n          }\n      // Decimal\n      case _ if logicalType.isInstanceOf[DecimalLogicalTypeAnnotation] =>\n        throw new UnsupportedOperationException(\n          s\"max/min stats is not supported for decimal: ${columnMetadata.getPath}\")\n      // Integer, Long, Float and Double\n      case primitive @ (_: Integer | _: java.lang.Long | _: java.lang.Float | _: java.lang.Double)\n        if logicalType == null => primitive\n      // Throw an exception on the other unknown types for safety.\n      case unknown =>\n        throw new UnsupportedOperationException(\n          s\"max/min stats is not supported for ${unknown.getClass.getName} with $logicalType:\" +\n            columnMetadata.getPath.toString)\n    }\n  }\n\n  /** The aggregation function used to count null of a column across blocks */\n  private def aggNullCount(blocks: Seq[BlockMetaData], index: Int): Any = {\n    var count = 0L\n    blocks.foreach { block =>\n      val column = block.getColumns.get(index)\n      val statistics = column.getStatistics\n      if (!statistics.isNumNullsSet) {\n        throw DeltaErrors.deltaStatsCollectionColumnNotFound(\"nullCount\", column.getPath.toString)\n      }\n      count += statistics.getNumNulls\n    }\n    count.asInstanceOf[Any]\n  }\n}\n\nobject StatsCollector {\n  def apply(\n      columnMappingMode: DeltaColumnMappingMode,\n      dataSchema: StructType,\n      statsSchema: StructType,\n      parquetRebaseMode: String,\n      ignoreMissingStats: Boolean = true,\n      stringTruncateLength: Option[Int] = None): StatsCollector = {\n    columnMappingMode match {\n      case NoMapping | NameMapping =>\n        StatsCollectorNameMapping(\n          dataSchema, statsSchema, parquetRebaseMode, ignoreMissingStats, stringTruncateLength)\n      case IdMapping =>\n        StatsCollectorIdMapping(\n          dataSchema, statsSchema, parquetRebaseMode, ignoreMissingStats, stringTruncateLength)\n      case _ =>\n        throw new UnsupportedOperationException(\n          s\"$columnMappingMode mapping is currently not supported\")\n    }\n  }\n\n  private case class StatsCollectorNameMapping(\n      dataSchema: StructType,\n      statsSchema: StructType,\n      parquetRebaseMode: String,\n      ignoreMissingStats: Boolean,\n      stringTruncateLength: Option[Int])\n    extends StatsCollector(\n      dataSchema, statsSchema, parquetRebaseMode, ignoreMissingStats, stringTruncateLength) {\n\n    /**\n     * Maps schema physical field path to parquet metadata column index via parquet metadata column\n     * path in NoMapping and NameMapping modes\n     */\n    override def getSchemaPhysicalPathToParquetIndex(\n        blockMetaData: BlockMetaData): Map[Seq[String], Int] = {\n      val parquetColumnPathToIndex = getParquetColumnPathToIndex(blockMetaData)\n      columnPathSchemaToParquet.collect {\n        // Collect mapping of fields in physical schema that actually exist in parquet metadata,\n        // parquet metadata can miss field due to schema evolution. In case stats collection is\n        // requested on a column that is missing from parquet metadata, we will catch this in\n        // collectStats when looking up in this map.\n        case (schemaPath, parquetPath) if parquetColumnPathToIndex.contains(parquetPath) =>\n          schemaPath -> parquetColumnPathToIndex(parquetPath)\n      }\n    }\n\n    /**\n     * A map from schema field path (with physical names) to parquet metadata column path of schema\n     * leaf fields with special handling of Array and Map.\n     *\n     * Here is an example:\n     *\n     * Data Schema (physical name in the parenthesis)\n     * | -- id (a4def3): INT\n     * | -- history (23aa42): STRUCT\n     *     | -- cost (23ddb0): DOUBLE\n     *     | -- events (23dda1): ARRAY[STRING]\n     * | -- info (abb4d2): MAP[STRING, STRING]\n     *\n     * Block Metadata:\n     * Columns: [ [a4def3], [23aa42, 23ddb0], [23ddb0, 23dda1, list, element],\n     *            [abb4d2, key_value, key], [abb4d2, key_value, value] ]\n     *\n     * The mapping:\n     *   [a4def3] -> [a4def3]\n     *   [23aa42, 23ddb0] -> [23aa42, 23ddb0]\n     *   [23ddb0, 23dda1] -> [23ddb0, 23dda1, list, element]\n     *   [abb4d2] -> [abb4d2, key_value, key]\n     */\n    private lazy val columnPathSchemaToParquet: Map[Seq[String], Seq[String]] = {\n      // Parquet metadata column path contains addition keywords for Array and Map. Here we only\n      // support 2 cases below since stats is not available in the other cases:\n      // 1. Array with non-null elements of primitive types\n      // 2. Map with key of primitive types\n      schemaPhysicalPathAndSchemaField.map {\n        case(path, field) =>\n          field.dataType match {\n            // Here we don't check array element type and map key type for primitive type since\n            // parquet metadata column path always points to a primitive column. In other words,\n            // the type is primitive if the column path can be found in parquet metadata later.\n            case ArrayType(_, false) => path -> (path ++ Seq(\"list\", \"element\"))\n            case MapType(_, _, _) => path -> (path ++ Seq(\"key_value\", \"key\"))\n            case _ => path -> path\n          }\n      }.toMap\n    }\n\n    /**\n     * Returns a map from parquet metadata column path to index.\n     *\n     * Here is an example:\n     *\n     * Data Schema:\n     * |-- id : INT\n     * |-- person : STRUCT\n     *     |-- name: STRING\n     *     |-- phone: INT\n     * |-- eligible: BOOLEAN\n     *\n     * Block Metadata:\n     * Columns: [ [id], [person, name], [person, phone], [eligible] ]\n     *\n     * The mapping:\n     *   [id] -> 0\n     *   [person, name] -> 1\n     *   [person, phone] -> 2\n     *   [eligible] -> 3\n     */\n    private def getParquetColumnPathToIndex(block: BlockMetaData): Map[Seq[String], Int] = {\n      block.getColumns.asScala.zipWithIndex.map {\n        case (column, i) => column.getPath.toArray.toSeq -> i\n      }.toMap\n    }\n  }\n\n  private case class StatsCollectorIdMapping(\n      dataSchema: StructType,\n      statsSchema: StructType,\n      parquetRebaseMode: String,\n      ignoreMissingStats: Boolean,\n      stringTruncateLength: Option[Int])\n    extends StatsCollector(\n      dataSchema, statsSchema, parquetRebaseMode, ignoreMissingStats, stringTruncateLength) {\n\n    // Define a FieldId type to better disambiguate between ids and indices in the code\n    type FieldId = Int\n\n    /**\n     * Maps schema physical field path to parquet metadata column index via parquet metadata column\n     * id in IdMapping mode.\n     */\n    override def getSchemaPhysicalPathToParquetIndex(\n        blockMetaData: BlockMetaData): Map[Seq[String], Int] = {\n      val parquetColumnIdToIndex = getParquetColumnIdToIndex(blockMetaData)\n      schemaPhysicalPathToColumnId.collect {\n        // Collect mapping of fields in physical schema that actually exist in parquet metadata,\n        // parquet metadata can miss field due to schema evolution and non-primitive types like Map\n        // and Array. In case stats collection is requested on a column that is missing from\n        // parquet metadata, we will catch this in collectStats when looking up in this map.\n        case (schemaPath, columnId) if parquetColumnIdToIndex.contains(columnId) =>\n          schemaPath -> parquetColumnIdToIndex(columnId)\n      }\n    }\n\n    /**\n     * A map from schema field path (with physical names) to parquet metadata column id of schema\n     * leaf fields.\n     *\n     * Here is an example:\n     *\n     * Data Schema (physical name, id in the parenthesis)\n     * | -- id (a4def3, 1): INT\n     * | -- history (23aa42, 2): STRUCT\n     *     | -- cost (23ddb0, 3): DOUBLE\n     *     | -- events (23dda1, 4): ARRAY[STRING]\n     * | -- info (abb4d2, 5): MAP[STRING, STRING]\n     *\n     * The mapping:\n     *   [a4def3] -> 1\n     *   [23aa42, 23ddb0] -> 3\n     *   [23ddb0, 23dda1] -> 4\n     *   [abb4d2] -> 5\n     */\n    private lazy val schemaPhysicalPathToColumnId: Map[Seq[String], FieldId] = {\n      schemaPhysicalPathAndSchemaField.map {\n        case (path, field) => path -> DeltaColumnMapping.getColumnId(field)\n      }.toMap\n    }\n\n    /**\n     * Returns a map from parquet metadata column id to column index by skipping columns without id.\n     * E.g., subfields of ARRAY and MAP don't have id assigned.\n     *\n     * Here is an example:\n     *\n     * Data Schema (id in the parenthesis):\n     * |-- id (1) : INT\n     * |-- person (2) : STRUCT\n     *     |-- names (3) : ARRAY[STRING]\n     *     |-- phones (4) : MAP[STRING, INT]\n     * |-- eligible (5) : BOOLEAN\n     *\n     * Block Metadata (id in the parenthesis):\n     * Columns: [ [id](1), [person, names, list, element](null),\n     *            [person, phones, key_value, key](null), [person, phones, key_value, value](null),\n     *            [eligible](5) ]\n     *\n     * The mapping: 1 -> 0, 5 -> 4\n     */\n    private def getParquetColumnIdToIndex(block: BlockMetaData): Map[FieldId, Int] = {\n      block.getColumns.asScala.zipWithIndex.collect {\n        // Id of parquet metadata column is not guaranteed, subfields of Map and Array don't have\n        // id assigned. In case id is missing and null, we skip the parquet metadata column here\n        // and will catch this in collectStats when looking up in this map.\n        case (column, i) if column.getPrimitiveType.getId != null =>\n          column.getPrimitiveType.getId.intValue() -> i\n      }.toMap\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/stats/StatsProvider.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.Column\nimport org.apache.spark.sql.types.DataType\n\n/**\n * A helper class that provides the functionalities to create [[DataSkippingPredicate]] with\n * the statistics for a column.\n *\n * @param getStat A function that returns an expression to access the given statistics for a\n *                specific column, or None if that stats column does not exist. For example,\n *                [[DataSkippingReaderBase.getStatsColumnOpt]] can be used here.\n */\n\nprivate [stats] class StatsProvider(getStat: StatsColumn => Option[Column]) {\n  /**\n   * Given a [[StatsColumn]], which represents a stats column for a table column, returns a\n   * [[DataSkippingPredicate]] which includes a data skipping expression (the result of running\n   * `f` on the expression of accessing the given stats) and the stats column (which the data\n   * skipping expression depends on), or None if the stats column does not exist.\n   *\n   * @param statCol A stats column (MIN, MAX, etc) for a table column name.\n   * @param f A user-provided function that returns a data skipping expression given the expression\n   *          to access the statistics for `statCol`.\n   * @return A [[DataSkippingPredicate]] with a data skipping expression, or None if the given\n   *         stats column does not exist.\n   */\n  def getPredicateWithStatsColumnIfExists(statCol: StatsColumn)\n    (f: Column => Column): Option[DataSkippingPredicate] = {\n    for (stat <- getStat(statCol))\n      yield DataSkippingPredicate(f(stat), statCol)\n  }\n\n  /** A variant of [[getPredicateWithStatsColumnIfExists]] with two stats columns. */\n  def getPredicateWithStatsColumnsIfExists(statCol1: StatsColumn, statCol2: StatsColumn)\n    (f: (Column, Column) => Column): Option[DataSkippingPredicate] = {\n    for (stat1 <- getStat(statCol1); stat2 <- getStat(statCol2))\n      yield DataSkippingPredicate(f(stat1, stat2), statCol1, statCol2)\n  }\n\n  /** A variant of [[getPredicateWithStatsColumnIfExists]] with three stats columns. */\n  def getPredicateWithStatsColumnsIfExists(\n      statCol1: StatsColumn,\n      statCol2: StatsColumn,\n      statCol3: StatsColumn)\n    (f: (Column, Column, Column) => Column): Option[DataSkippingPredicate] = {\n    for (stat1 <- getStat(statCol1); stat2 <- getStat(statCol2); stat3 <- getStat(statCol3))\n      yield DataSkippingPredicate(f(stat1, stat2, stat3), statCol1, statCol2, statCol3)\n  }\n\n  /**\n   * Given a path to a table column and a stat type (MIN, MAX, etc.), returns a\n   * [[DataSkippingPredicate]] which includes a data skipping expression (the result of running\n   * `f` on the expression of accessing the given stats) and the stats column (which the data\n   * skipping expression depends on), or None if the stats column does not exist.\n   *\n   * @param pathToColumn The name of a column whose stats are to be accessed.\n   * @param statType The type of stats to access (MIN, MAX, etc.)\n   * @param f A user-provided function that returns a data skipping expression given the expression\n   *          to access the statistics for `statCol`.\n   * @return A [[DataSkippingPredicate]] with a data skipping expression, or None if the given\n   *         stats column does not exist.\n   */\n  def getPredicateWithStatTypeIfExists(\n      pathToColumn: Seq[String], columnDataType: DataType, statType: String)\n    (f: Column => Column): Option[DataSkippingPredicate] = {\n    getPredicateWithStatsColumnIfExists(StatsColumn(statType, pathToColumn, columnDataType))(f)\n  }\n\n  /** A variant of [[getPredicateWithStatTypeIfExists]] with two stat types. */\n  def getPredicateWithStatTypesIfExists(\n      pathToColumn: Seq[String], columnDataType: DataType, statType1: String, statType2: String)\n    (f: (Column, Column) => Column): Option[DataSkippingPredicate] = {\n    getPredicateWithStatsColumnsIfExists(\n      StatsColumn(statType1, pathToColumn, columnDataType),\n      StatsColumn(statType2, pathToColumn, columnDataType))(f)\n  }\n\n  /** A variant of [[getPredicateWithStatTypeIfExists]] with three stat types. */\n  def getPredicateWithStatTypesIfExists(\n      pathToColumn: Seq[String],\n      columnDataType: DataType,\n      statType1: String,\n      statType2: String,\n      statType3: String)\n    (f: (Column, Column, Column) => Column): Option[DataSkippingPredicate] = {\n    getPredicateWithStatsColumnsIfExists(\n      StatsColumn(statType1, pathToColumn, columnDataType),\n      StatsColumn(statType2, pathToColumn, columnDataType),\n      StatsColumn(statType3, pathToColumn, columnDataType))(f)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/AzureLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\n\n/**\n * LogStore implementation for Azure.\n *\n * We assume the following from Azure's [[FileSystem]] implementations:\n * - Rename without overwrite is atomic.\n * - List-after-write is consistent.\n *\n * Regarding file creation, this implementation:\n * - Uses atomic rename when overwrite is false; if the destination file exists or the rename\n *   fails, throws an exception.\n * - Uses create-with-overwrite when overwrite is true. This does not make the file atomically\n *   visible and therefore the caller must handle partial files.\n */\nclass AzureLogStore(sparkConf: SparkConf, hadoopConf: Configuration)\n  extends HadoopFileSystemLogStore(sparkConf, hadoopConf) {\n\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    writeWithRename(path, actions, overwrite)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    writeWithRename(path, actions, overwrite, hadoopConf)\n  }\n\n  override def invalidateCache(): Unit = {}\n\n  override def isPartialWriteVisible(path: Path): Boolean = true\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = true\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/ClosableIterator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.io.Closeable\n\ntrait SupportsRewinding[T] extends Iterator[T] {\n  // Overrides if class supports rewinding the iterator to the beginning efficiently.\n  def rewind(): Unit\n}\n\ntrait ClosableIterator[T] extends Iterator[T] with Closeable {\n  /** Calls f(this) and always closes the iterator afterwards. */\n  def processAndClose[R](f: Iterator[T] => R): R = {\n    try {\n      f(this)\n    } finally {\n      close()\n    }\n  }\n}\n\nobject ClosableIterator {\n  /**\n   * An implicit class for applying a function to a [[ClosableIterator]] and returning the\n   * resulting iterator as a [[ClosableIterator]] with the original `close()` method.\n   */\n  implicit class IteratorCloseOps[A](val closableIter: ClosableIterator[A]) extends AnyVal {\n    def withClose[B](f: Iterator[A] => Iterator[B]): ClosableIterator[B] = new ClosableIterator[B] {\n      private val iter =\n        try {\n          f(closableIter)\n        } catch {\n          case e: Throwable =>\n            closableIter.close()\n            throw e\n        }\n      override def next(): B = iter.next()\n      override def hasNext: Boolean = iter.hasNext\n      override def close(): Unit = closableIter.close()\n    }\n  }\n\n  /**\n   * An implicit class for a `flatMap` implementation that returns a [[ClosableIterator]]\n   * which (a) closes inner iterators upon reaching their end, and (b) has a `close()` method\n   * that closes any opened and unclosed inner iterators.\n   */\n  implicit class IteratorFlatMapCloseOp[A](val closableIter: Iterator[A]) extends AnyVal {\n    def flatMapWithClose[B](f: A => ClosableIterator[B]): ClosableIterator[B] =\n      new ClosableIterator[B] {\n        private var iter_curr: ClosableIterator[B] = null\n        override def next(): B = {\n          if (!hasNext) {\n            throw new NoSuchElementException\n          }\n          iter_curr.next()\n        }\n        @scala.annotation.tailrec\n        override def hasNext: Boolean = {\n          if (iter_curr == null && closableIter.hasNext) {\n            iter_curr = f(closableIter.next())\n          }\n          if (iter_curr == null) {\n            false\n          }\n          else if (iter_curr.hasNext) {\n            true\n          }\n          else {\n            iter_curr.close()\n            if (closableIter.hasNext) {\n              iter_curr = f(closableIter.next())\n              hasNext\n            } else {\n              iter_curr = null\n              false\n            }\n          }\n        }\n        override def close(): Unit = {\n          if (iter_curr != null) {\n            iter_curr.close()\n          }\n        }\n      }\n  }\n\n  /**\n   * An implicit class for wrapping an iterator to be a [[ClosableIterator]] with a `close` method\n   * that does nothing.\n   */\n  implicit class ClosableWrapper[A](val iter: Iterator[A]) extends AnyVal {\n    def toClosable: ClosableIterator[A] = new ClosableIterator[A] {\n      override def next(): A = iter.next()\n      override def hasNext: Boolean = iter.hasNext\n      override def close(): Unit = ()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/DelegatingLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.util.Locale\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.internal.MDC\n\n\n/**\n * A delegating LogStore used to dynamically resolve LogStore implementation based\n * on the scheme of paths.\n */\nclass DelegatingLogStore(hadoopConf: Configuration)\n  extends LogStore with DeltaLogging {\n\n  private val sparkConf = SparkEnv.get.conf\n\n  // Map scheme to the corresponding LogStore resolved and created. Accesses to this map need\n  // synchronization This could be accessed by multiple threads because it is shared through\n  // shared DeltaLog instances.\n  private val schemeToLogStoreMap = mutable.Map.empty[String, LogStore]\n\n  private lazy val defaultLogStore = createLogStore(DelegatingLogStore.defaultHDFSLogStoreClassName)\n\n  // Creates a LogStore with given LogStore class name.\n  private def createLogStore(className: String): LogStore = {\n    LogStore.createLogStoreWithClassName(className, sparkConf, hadoopConf)\n  }\n\n  // Create LogStore based on the scheme of `path`.\n  private def schemeBasedLogStore(path: Path): LogStore = {\n    val store = Option(path.toUri.getScheme) match {\n      case Some(origScheme) =>\n        val scheme = origScheme.toLowerCase(Locale.ROOT)\n        this.synchronized {\n          if (schemeToLogStoreMap.contains(scheme)) {\n            schemeToLogStoreMap(scheme)\n          } else {\n            // Resolve LogStore class based on the following order:\n            // 1. Scheme conf if set.\n            // 2. Defaults for scheme if exists.\n            // 3. Default.\n            val logStoreClassNameOpt = LogStore.getLogStoreConfValue( // we look for all viable keys\n              LogStore.logStoreSchemeConfKey(scheme), sparkConf)\n              .orElse(DelegatingLogStore.getDefaultLogStoreClassName(scheme))\n            val logStore = logStoreClassNameOpt.map(createLogStore(_)).getOrElse(defaultLogStore)\n            schemeToLogStoreMap += scheme -> logStore\n\n            val actualLogStoreClassName = logStore match {\n              case lsa: LogStoreAdaptor => s\"LogStoreAdapter(${lsa.logStoreImpl.getClass.getName})\"\n              case _ => logStore.getClass.getName\n            }\n            logInfo(log\"LogStore ${MDC(DeltaLogKeys.SYSTEM_CLASS_NAME, actualLogStoreClassName)} \" +\n              log\"is used for scheme ${MDC(DeltaLogKeys.FILE_SYSTEM_SCHEME, scheme)}\")\n\n            logStore\n          }\n        }\n      case _ => defaultLogStore\n    }\n    store\n  }\n\n  def getDelegate(path: Path): LogStore = schemeBasedLogStore(path)\n\n  //////////////////////////\n  // Public API Overrides //\n  //////////////////////////\n\n  override def read(path: Path): Seq[String] = {\n    getDelegate(path).read(path)\n  }\n\n  override def read(path: Path, hadoopConf: Configuration): Seq[String] = {\n    getDelegate(path).read(path, hadoopConf)\n  }\n\n  override def readAsIterator(path: Path): ClosableIterator[String] = {\n    getDelegate(path).readAsIterator(path)\n  }\n\n  override def readAsIterator(path: Path, hadoopConf: Configuration): ClosableIterator[String] = {\n    getDelegate(path).readAsIterator(path, hadoopConf)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean): Unit = {\n    getDelegate(path).write(path, actions, overwrite)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    getDelegate(path).write(path, actions, overwrite, hadoopConf)\n  }\n\n  override def listFrom(path: Path): Iterator[FileStatus] = {\n    getDelegate(path).listFrom(path)\n  }\n\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    getDelegate(path).listFrom(path, hadoopConf)\n  }\n\n  override def invalidateCache(): Unit = {\n    this.synchronized {\n      schemeToLogStoreMap.foreach { entry =>\n        entry._2.invalidateCache()\n      }\n    }\n    defaultLogStore.invalidateCache()\n  }\n\n  override def resolvePathOnPhysicalStorage(path: Path): Path = {\n    getDelegate(path).resolvePathOnPhysicalStorage(path)\n  }\n\n  override def resolvePathOnPhysicalStorage(path: Path, hadoopConf: Configuration): Path = {\n    getDelegate(path).resolvePathOnPhysicalStorage(path, hadoopConf)\n  }\n\n  override def isPartialWriteVisible(path: Path): Boolean = {\n    getDelegate(path).isPartialWriteVisible(path)\n  }\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = {\n    getDelegate(path).isPartialWriteVisible(path, hadoopConf)\n  }\n}\n\nobject DelegatingLogStore {\n\n  try {\n    // load any arbitrary delta-storage class to ensure the dependency has been included\n    classOf[io.delta.storage.LogStore]\n  } catch {\n    case e: NoClassDefFoundError =>\n      throw DeltaErrors.missingDeltaStorageJar(e)\n  }\n\n  /**\n   * Java LogStore (io.delta.storage) implementations are now the default.\n   */\n  val defaultS3LogStoreClassName = classOf[io.delta.storage.S3SingleDriverLogStore].getName\n  val defaultAzureLogStoreClassName = classOf[io.delta.storage.AzureLogStore].getName\n  val defaultHDFSLogStoreClassName = classOf[io.delta.storage.HDFSLogStore].getName\n  val defaultGCSLogStoreClassName = classOf[io.delta.storage.GCSLogStore].getName\n\n  // Supported schemes with default.\n  val s3Schemes = Set(\"s3\", \"s3a\", \"s3n\")\n  val azureSchemes = Set(\"abfs\", \"abfss\", \"adl\", \"wasb\", \"wasbs\")\n  val gsSchemes = Set(\"gs\")\n\n  // Returns the default LogStore class name for `scheme`.\n  // None if we do not have a default for it.\n  def getDefaultLogStoreClassName(scheme: String): Option[String] = {\n    if (s3Schemes.contains(scheme)) {\n      return Some(defaultS3LogStoreClassName)\n    } else if (DelegatingLogStore.azureSchemes(scheme: String)) {\n      return Some(defaultAzureLogStoreClassName)\n    } else if (DelegatingLogStore.gsSchemes(scheme: String)) {\n      return Some(defaultGCSLogStoreClassName)\n    }\n    None\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/HDFSLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.io.IOException\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.EnumSet\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs._\nimport org.apache.hadoop.fs.CreateFlag.CREATE\nimport org.apache.hadoop.fs.Options.{ChecksumOpt, CreateOpts}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.internal.Logging\n\n/**\n * The [[LogStore]] implementation for HDFS, which uses Hadoop [[FileContext]] API's to\n * provide the necessary atomic and durability guarantees:\n *\n * 1. Atomic visibility of files: `FileContext.rename` is used write files which is atomic for HDFS.\n *\n * 2. Consistent file listing: HDFS file listing is consistent.\n */\nclass HDFSLogStore(sparkConf: SparkConf, defaultHadoopConf: Configuration)\n  extends HadoopFileSystemLogStore(sparkConf, defaultHadoopConf) with Logging{\n\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  protected def getFileContext(path: Path): FileContext = {\n    FileContext.getFileContext(path.toUri, getHadoopConfiguration)\n  }\n\n  protected def getFileContext(path: Path, hadoopConf: Configuration): FileContext = {\n    FileContext.getFileContext(path.toUri, hadoopConf)\n  }\n\n  val noAbstractFileSystemExceptionMessage = \"No AbstractFileSystem\"\n\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    write(path, actions, overwrite, getHadoopConfiguration)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    val isLocalFs = path.getFileSystem(hadoopConf).isInstanceOf[RawLocalFileSystem]\n    if (isLocalFs) {\n      // We need to add `synchronized` for RawLocalFileSystem as its rename will not throw an\n      // exception when the target file exists. Hence we must make sure `exists + rename` in\n      // `writeInternal` for RawLocalFileSystem is atomic in our tests.\n      synchronized {\n        writeInternal(path, actions, overwrite, hadoopConf)\n      }\n    } else {\n      // rename is atomic and also will fail when the target file exists. Not need to add the extra\n      // `synchronized`.\n      writeInternal(path, actions, overwrite, hadoopConf)\n    }\n  }\n\n  private def writeInternal(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    val fc: FileContext = try {\n      getFileContext(path, hadoopConf)\n    } catch {\n      case e: IOException if e.getMessage.contains(noAbstractFileSystemExceptionMessage) =>\n        val newException = DeltaErrors.incorrectLogStoreImplementationException(sparkConf, e)\n        logError(newException.getMessage, newException.getCause)\n        throw newException\n    }\n    if (!overwrite && fc.util.exists(path)) {\n      // This is needed for the tests to throw error with local file system\n      throw DeltaErrors.fileAlreadyExists(path.toString)\n    }\n\n    val tempPath = createTempPath(path)\n    var streamClosed = false // This flag is to avoid double close\n    var renameDone = false // This flag is to save the delete operation in most of cases.\n    val stream = fc.create(\n      tempPath, EnumSet.of(CREATE), CreateOpts.checksumParam(ChecksumOpt.createDisabled()))\n\n    try {\n      actions.map(_ + \"\\n\").map(_.getBytes(UTF_8)).foreach(stream.write)\n      stream.close()\n      streamClosed = true\n      try {\n        val renameOpt = if (overwrite) Options.Rename.OVERWRITE else Options.Rename.NONE\n        fc.rename(tempPath, path, renameOpt)\n        renameDone = true\n        // TODO: this is a workaround of HADOOP-16255 - remove this when HADOOP-16255 is resolved\n        tryRemoveCrcFile(fc, tempPath)\n      } catch {\n        case e: org.apache.hadoop.fs.FileAlreadyExistsException =>\n          throw DeltaErrors.fileAlreadyExists(path.toString)\n      }\n    } finally {\n      if (!streamClosed) {\n        stream.close()\n      }\n      if (!renameDone) {\n        fc.delete(tempPath, false)\n      }\n    }\n\n    msyncIfSupported(path, hadoopConf)\n  }\n\n  /**\n   * Normally when using HDFS with an Observer NameNode setup, there would be read after write\n   * consistency within a single process, so the write would be guaranteed to be visible on the\n   * next read. However, since we are using the FileContext API for writing (for atomic rename),\n   * and the FileSystem API for reading (for more compatibility with various file systems), we\n   * are essentially using two separate clients that are not guaranteed to be kept in sync.\n   * Therefore we \"msync\" the FileSystem instance, which is cached across all uses of the same\n   * protocol/host combination, to make sure the next read through the HDFSLogStore can see this\n   * write.\n   * Any underlying FileSystem that is not the DistributedFileSystem will simply throw an\n   * UnsupportedOperationException, which can be ignored. Additionally, if an older version of\n   * Hadoop is being used that does not include msync, a NoSuchMethodError will be thrown while\n   * looking up the method, which can also be safely ignored.\n   */\n  private def msyncIfSupported(path: Path, hadoopConf: Configuration): Unit = {\n    try {\n      val fs = path.getFileSystem(hadoopConf)\n      val msync = fs.getClass.getMethod(\"msync\")\n      msync.invoke(fs)\n    } catch {\n      case NonFatal(_) => // ignore, calling msync is best effort\n    }\n  }\n\n  private def tryRemoveCrcFile(fc: FileContext, path: Path): Unit = {\n    try {\n      val checksumFile = new Path(path.getParent, s\".${path.getName}.crc\")\n      if (fc.util.exists(checksumFile)) {\n        // checksum file exists, deleting it\n        fc.delete(checksumFile, true)\n      }\n    } catch {\n      case NonFatal(_) => // ignore, we are removing crc file as \"best-effort\"\n    }\n  }\n\n  override def isPartialWriteVisible(path: Path): Boolean = true\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = true\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/HadoopFileSystemLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.io.{BufferedReader, InputStreamReader}\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.UUID\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.commons.io.IOUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, FSDataInputStream, Path}\n\nimport org.apache.spark.{SparkConf, SparkContext}\nimport org.apache.spark.sql.SparkSession\n\n/**\n * Default implementation of [[LogStore]] for Hadoop [[FileSystem]] implementations.\n */\nabstract class HadoopFileSystemLogStore(\n    sparkConf: SparkConf,\n    hadoopConf: Configuration) extends LogStore {\n\n  def this(sc: SparkContext) = this(sc.getConf, sc.hadoopConfiguration)\n\n  protected def getHadoopConfiguration: Configuration = {\n    // scalastyle:off deltahadoopconfiguration\n    SparkSession.getActiveSession.map(_.sessionState.newHadoopConf()).getOrElse(hadoopConf)\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  override def read(path: Path): Seq[String] = {\n    read(path, getHadoopConfiguration)\n  }\n\n  override def read(path: Path, hadoopConf: Configuration): Seq[String] = {\n    readStream(open(path, hadoopConf))\n  }\n\n  override def readAsIterator(path: Path): ClosableIterator[String] = {\n    readAsIterator(path, getHadoopConfiguration)\n  }\n\n  override def readAsIterator(path: Path, hadoopConf: Configuration): ClosableIterator[String] =\n    readStreamAsIterator(open(path, hadoopConf))\n\n  private def open(path: Path, hadoopConf: Configuration): FSDataInputStream =\n    path.getFileSystem(hadoopConf).open(path)\n\n  private def readStream(stream: FSDataInputStream): Seq[String] = {\n    try {\n      val reader = new BufferedReader(new InputStreamReader(stream, UTF_8))\n      IOUtils.readLines(reader).asScala.map(_.trim).toSeq\n    } finally {\n      stream.close()\n    }\n  }\n\n  private def readStreamAsIterator(stream: FSDataInputStream): ClosableIterator[String] = {\n    val reader = new BufferedReader(new InputStreamReader(stream, UTF_8))\n    new LineClosableIterator(reader)\n  }\n\n  override def listFrom(path: Path): Iterator[FileStatus] = {\n    listFrom(path, getHadoopConfiguration)\n  }\n\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    val fs = path.getFileSystem(hadoopConf)\n    if (!fs.exists(path.getParent)) {\n      throw DeltaErrors.fileOrDirectoryNotFoundException(s\"${path.getParent}\")\n    }\n    val files = fs.listStatus(path.getParent)\n    files.filter(_.getPath.getName >= path.getName).sortBy(_.getPath.getName).iterator\n  }\n\n  override def resolvePathOnPhysicalStorage(path: Path): Path = {\n    resolvePathOnPhysicalStorage(path, getHadoopConfiguration)\n  }\n\n  override def resolvePathOnPhysicalStorage(path: Path, hadoopConf: Configuration): Path = {\n    path.getFileSystem(hadoopConf).makeQualified(path)\n  }\n\n  /**\n   * An internal write implementation that uses FileSystem.rename().\n   *\n   * This implementation should only be used for the underlying file systems that support atomic\n   * renames, e.g., Azure is OK but HDFS is not.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  protected def writeWithRename(\n      path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    writeWithRename(path, actions, overwrite, getHadoopConfiguration)\n  }\n\n  /**\n   * An internal write implementation that uses FileSystem.rename().\n   *\n   * This implementation should only be used for the underlying file systems that support atomic\n   * renames, e.g., Azure is OK but HDFS is not.\n   */\n  protected def writeWithRename(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    val fs = path.getFileSystem(hadoopConf)\n\n    if (!fs.exists(path.getParent)) {\n      throw DeltaErrors.fileOrDirectoryNotFoundException(s\"${path.getParent}\")\n    }\n    if (overwrite) {\n      val stream = fs.create(path, true)\n      try {\n        actions.map(_ + \"\\n\").map(_.getBytes(UTF_8)).foreach(stream.write)\n      } finally {\n        stream.close()\n      }\n    } else {\n      if (fs.exists(path)) {\n        throw DeltaErrors.fileAlreadyExists(path.toString)\n      }\n      val tempPath = createTempPath(path)\n      var streamClosed = false // This flag is to avoid double close\n      var renameDone = false // This flag is to save the delete operation in most of cases.\n      val stream = fs.create(tempPath)\n      try {\n        actions.map(_ + \"\\n\").map(_.getBytes(UTF_8)).foreach(stream.write)\n        stream.close()\n        streamClosed = true\n        try {\n          if (fs.rename(tempPath, path)) {\n            renameDone = true\n          } else {\n            if (fs.exists(path)) {\n              throw DeltaErrors.fileAlreadyExists(path.toString)\n            } else {\n              throw DeltaErrors.cannotRenamePath(tempPath.toString, path.toString)\n            }\n          }\n        } catch {\n          case _: org.apache.hadoop.fs.FileAlreadyExistsException =>\n            throw DeltaErrors.fileAlreadyExists(path.toString)\n        }\n      } finally {\n        if (!streamClosed) {\n          stream.close()\n        }\n        if (!renameDone) {\n          fs.delete(tempPath, false)\n        }\n      }\n    }\n  }\n\n  protected def createTempPath(path: Path): Path = {\n    new Path(path.getParent, s\".${path.getName}.${UUID.randomUUID}.tmp\")\n  }\n\n  override def invalidateCache(): Unit = {}\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/LineClosableIterator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.io.Reader\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.commons.io.IOUtils\n\n/**\n * Turn a `Reader` to `ClosableIterator` which can be read on demand. Each element is\n * a trimmed line.\n */\nclass LineClosableIterator(_reader: Reader) extends ClosableIterator[String] {\n  private val reader = IOUtils.toBufferedReader(_reader)\n  // Whether `nextValue` is valid. If it's invalid, we should try to read the next line.\n  private var gotNext = false\n  // The next value to return when `next` is called. This is valid only if `getNext` is true.\n  private var nextValue: String = _\n  // Whether the reader is closed.\n  private var closed = false\n  // Whether we have consumed all data in the reader.\n  private var finished = false\n\n  override def hasNext: Boolean = {\n    if (!finished) {\n      // Check whether we have closed the reader before reading. Even if `nextValue` is valid, we\n      // still don't return `nextValue` after a reader is closed. Otherwise, it would be confusing.\n      if (closed) {\n        throw DeltaErrors.iteratorAlreadyClosed()\n      }\n      if (!gotNext) {\n        val nextLine = reader.readLine()\n        if (nextLine == null) {\n          finished = true\n          close()\n        } else {\n          nextValue = nextLine.trim\n        }\n        gotNext = true\n      }\n    }\n    !finished\n  }\n\n  override def next(): String = {\n    if (!hasNext) {\n      throw new NoSuchElementException(\"End of stream\")\n    }\n    gotNext = false\n    nextValue\n  }\n\n  override def close(): Unit = {\n    if (!closed) {\n      closed = true\n      reader.close()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/LocalLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\n\n/**\n * Default [[LogStore]] implementation (should be used for testing only!).\n *\n * Production users should specify the appropriate [[[LogStore]] implementation in Spark properties.\n *\n * We assume the following from [[org.apache.hadoop.fs.FileSystem]] implementations:\n * - Rename without overwrite is atomic.\n * - List-after-write is consistent.\n *\n * Regarding file creation, this implementation:\n * - Uses atomic rename when overwrite is false; if the destination file exists or the rename\n *   fails, throws an exception.\n * - Uses create-with-overwrite when overwrite is true. This does not make the file atomically\n *   visible and therefore the caller must handle partial files.\n */\nclass LocalLogStore(sparkConf: SparkConf, hadoopConf: Configuration)\n    extends HadoopFileSystemLogStore(sparkConf: SparkConf, hadoopConf: Configuration) {\n\n  /**\n   * This write implementation needs to wraps `writeWithRename` with `synchronized` as the rename()\n   * for [[org.apache.hadoop.fs.RawLocalFileSystem]] doesn't throw an exception when the target file\n   * exists. Hence we must make sure `exists + rename` in `writeWithRename` is atomic in our tests.\n   */\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    synchronized {\n      writeWithRename(path, actions, overwrite)\n    }\n  }\n\n  /**\n   * This write implementation needs to wraps `writeWithRename` with `synchronized` as the rename()\n   * for [[org.apache.hadoop.fs.RawLocalFileSystem]] doesn't throw an exception when the target file\n   * exists. Hence we must make sure `exists + rename` in `writeWithRename` is atomic in our tests.\n   */\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    synchronized {\n      writeWithRename(path, actions, overwrite, hadoopConf)\n    }\n  }\n\n  override def invalidateCache(): Unit = {}\n\n  override def isPartialWriteVisible(path: Path): Boolean = true\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = true\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/LogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog}\nimport io.delta.storage.CloseableIterator\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.{SparkConf, SparkContext}\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.util.Utils\n\n/**\n * General interface for all critical file system operations required to read and write the\n * [[DeltaLog]]. The correctness of the [[DeltaLog]] is predicated on the atomicity and\n * durability guarantees of the implementation of this interface. Specifically,\n *\n * 1. Atomic visibility of files: Any file written through this store must\n *    be made visible atomically. In other words, this should not generate partial files.\n *\n * 2. Mutual exclusion: Only one writer must be able to create (or rename) a file at the final\n *    destination.\n *\n * 3. Consistent listing: Once a file has been written in a directory, all future listings for\n *    that directory must return that file.\n */\ntrait LogStore {\n\n  /**\n   * Load the given file and return a `Seq` of lines. The line break will be removed from each\n   * line. This method will load the entire file into the memory. Call `readAsIterator` if possible\n   * as its implementation may be more efficient.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  final def read(path: String): Seq[String] = read(new Path(path))\n\n  /**\n   * Load the given file and return a `Seq` of lines. The line break will be removed from each\n   * line. This method will load the entire file into the memory. Call `readAsIterator` if possible\n   * as its implementation may be more efficient.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  def read(path: Path): Seq[String]\n\n  /**\n   * Load the given file and return a `Seq` of lines. The line break will be removed from each\n   * line. This method will load the entire file into the memory. Call `readAsIterator` if possible\n   * as its implementation may be more efficient.\n   *\n   * Note: The default implementation ignores the `hadoopConf` parameter to provide the backward\n   * compatibility. Subclasses should override this method and use `hadoopConf` properly to support\n   * passing Hadoop file system configurations through DataFrame options.\n   */\n  def read(path: Path, hadoopConf: Configuration): Seq[String] = read(path)\n\n  /**\n   * Load the given file represented by `fileStatus` and return a `Seq` of lines.\n   * The line break will be removed from each line.\n   *\n   * Note: Using a stale `FileStatus` may get an incorrect result.\n   */\n  final def read(fileStatus: FileStatus, hadoopConf: Configuration): Seq[String] = {\n    val iter = readAsIterator(fileStatus, hadoopConf)\n    try {\n      iter.toIndexedSeq\n    } finally {\n      iter.close()\n    }\n  }\n\n  /**\n   * Load the given file and return an iterator of lines. The line break will be removed from each\n   * line. The default implementation calls `read` to load the entire file into the memory.\n   * An implementation should provide a more efficient approach if possible. For example, the file\n   * content can be loaded on demand.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  final def readAsIterator(path: String): ClosableIterator[String] = {\n    readAsIterator(new Path(path))\n  }\n\n  /**\n   * Load the given file and return an iterator of lines. The line break will be removed from each\n   * line. The default implementation calls `read` to load the entire file into the memory.\n   * An implementation should provide a more efficient approach if possible. For example, the file\n   * content can be loaded on demand.\n   *\n   * Note: the returned [[ClosableIterator]] should be closed when it's no longer used to avoid\n   * resource leak.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  def readAsIterator(path: Path): ClosableIterator[String] = {\n    val iter = read(path).iterator\n    new ClosableIterator[String] {\n\n      override def hasNext: Boolean = iter.hasNext\n\n      override def next(): String = iter.next()\n\n      override def close(): Unit = {}\n    }\n  }\n\n  /**\n   * Load the given file and return an iterator of lines. The line break will be removed from each\n   * line. The default implementation calls `read` to load the entire file into the memory.\n   * An implementation should provide a more efficient approach if possible. For example, the file\n   * content can be loaded on demand.\n   *\n   * Note: the returned [[ClosableIterator]] should be closed when it's no longer used to avoid\n   * resource leak.\n   *\n   * Note: The default implementation ignores the `hadoopConf` parameter to provide the backward\n   * compatibility. Subclasses should override this method and use `hadoopConf` properly to support\n   * passing Hadoop file system configurations through DataFrame options.\n   */\n  def readAsIterator(path: Path, hadoopConf: Configuration): ClosableIterator[String] = {\n    readAsIterator(path)\n  }\n\n  /**\n   * Load the file represented by given fileStatus and return an iterator of lines. The line break\n   * will be removed from each line.\n   *\n   * Note-1: the returned [[ClosableIterator]] should be closed when it's no longer used to avoid\n   * resource leak.\n   *\n   * Note-2: Using a stale `FileStatus` may get an incorrect result.\n   */\n  def readAsIterator(\n      fileStatus: FileStatus,\n      hadoopConf: Configuration): ClosableIterator[String] = {\n    readAsIterator(fileStatus.getPath, hadoopConf)\n  }\n\n  /**\n   * Write the given `actions` to the given `path` without overwriting any existing file.\n   * Implementation must throw [[java.nio.file.FileAlreadyExistsException]] exception if the file\n   * already exists. Furthermore, implementation must ensure that the entire file is made\n   * visible atomically, that is, it should not generate partial files.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  final def write(path: String, actions: Iterator[String]): Unit = write(new Path(path), actions)\n\n  /**\n   * Write the given `actions` to the given `path` with or without overwrite as indicated.\n   * Implementation must throw [[java.nio.file.FileAlreadyExistsException]] exception if the file\n   * already exists and overwrite = false. Furthermore, implementation must ensure that the\n   * entire file is made visible atomically, that is, it should not generate partial files.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit\n\n  /**\n   * Write the given `actions` to the given `path` with or without overwrite as indicated.\n   * Implementation must throw [[java.nio.file.FileAlreadyExistsException]] exception if the file\n   * already exists and overwrite = false. Furthermore, implementation must ensure that the\n   * entire file is made visible atomically, that is, it should not generate partial files.\n   *\n   * Note: The default implementation ignores the `hadoopConf` parameter to provide the backward\n   * compatibility. Subclasses should override this method and use `hadoopConf` properly to support\n   * passing Hadoop file system configurations through DataFrame options.\n   */\n  def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    write(path, actions, overwrite)\n  }\n\n  /**\n   * List the paths in the same directory that are lexicographically greater or equal to\n   * (UTF-8 sorting) the given `path`. The result should also be sorted by the file name.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  final def listFrom(path: String): Iterator[FileStatus] =\n    listFrom(new Path(path))\n\n  /**\n   * List the paths in the same directory that are lexicographically greater or equal to\n   * (UTF-8 sorting) the given `path`. The result should also be sorted by the file name.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  def listFrom(path: Path): Iterator[FileStatus]\n\n  /**\n   * List the paths in the same directory that are lexicographically greater or equal to\n   * (UTF-8 sorting) the given `path`. The result should also be sorted by the file name.\n   *\n   * Note: The default implementation ignores the `hadoopConf` parameter to provide the backward\n   * compatibility. Subclasses should override this method and use `hadoopConf` properly to support\n   * passing Hadoop file system configurations through DataFrame options.\n   */\n  def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = listFrom(path)\n\n  /** Invalidate any caching that the implementation may be using */\n  def invalidateCache(): Unit\n\n  /** Resolve the fully qualified path for the given `path`. */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  def resolvePathOnPhysicalStorage(path: Path): Path = {\n    throw new UnsupportedOperationException()\n  }\n\n  /**\n   * Resolve the fully qualified path for the given `path`.\n   *\n   * Note: The default implementation ignores the `hadoopConf` parameter to provide the backward\n   * compatibility. Subclasses should override this method and use `hadoopConf` properly to support\n   * passing Hadoop file system configurations through DataFrame options.\n   */\n  def resolvePathOnPhysicalStorage(path: Path, hadoopConf: Configuration): Path = {\n    resolvePathOnPhysicalStorage(path)\n  }\n\n  /**\n   * Whether a partial write is visible when writing to `path`.\n   *\n   * As this depends on the underlying file system implementations, we require the input of `path`\n   * here in order to identify the underlying file system, even though in most cases a log store\n   * only deals with one file system.\n   *\n   * The default value is only provided here for legacy reasons, which will be removed.\n   * Any LogStore implementation should override this instead of relying on the default.\n   */\n  @deprecated(\"call the method that asks for a Hadoop Configuration object instead\")\n  def isPartialWriteVisible(path: Path): Boolean = true\n\n  /**\n   * Whether a partial write is visible when writing to `path`.\n   *\n   * As this depends on the underlying file system implementations, we require the input of `path`\n   * here in order to identify the underlying file system, even though in most cases a log store\n   * only deals with one file system.\n   *\n   * The default value is only provided here for legacy reasons, which will be removed.\n   * Any LogStore implementation should override this instead of relying on the default.\n   *\n   * Note: The default implementation ignores the `hadoopConf` parameter to provide the backward\n   * compatibility. Subclasses should override this method and use `hadoopConf` properly to support\n   * passing Hadoop file system configurations through DataFrame options.\n   */\n  def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = {\n    isPartialWriteVisible(path)\n  }\n}\n\nobject LogStore extends LogStoreProvider\n  with Logging {\n\n\n  def apply(spark: SparkSession): LogStore = {\n    // scalastyle:off deltahadoopconfiguration\n    // Ensure that the LogStore's hadoopConf has the values from the SQLConf.\n    // This ensures that io.delta.storage LogStore (Java) hadoopConf's are configured correctly.\n    apply(spark.sparkContext.getConf, spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  def apply(sparkConf: SparkConf, hadoopConf: Configuration): LogStore = {\n    createLogStore(sparkConf, hadoopConf)\n  }\n\n  // Creates a LogStore with the given LogStore class name and configurations.\n  def createLogStoreWithClassName(\n      className: String,\n      sparkConf: SparkConf,\n      hadoopConf: Configuration): LogStore = {\n    if (className == classOf[DelegatingLogStore].getName) {\n      new DelegatingLogStore(hadoopConf)\n    } else {\n      val logStoreClass = Utils.classForName(className)\n      if (classOf[io.delta.storage.LogStore].isAssignableFrom(logStoreClass)) {\n        new LogStoreAdaptor(logStoreClass.getConstructor(classOf[Configuration])\n          .newInstance(hadoopConf))\n      } else {\n        logStoreClass.getConstructor(classOf[SparkConf], classOf[Configuration])\n          .newInstance(sparkConf, hadoopConf).asInstanceOf[LogStore]\n      }\n    }\n  }\n}\n\ntrait LogStoreProvider {\n  val logStoreClassConfKey: String = \"spark.delta.logStore.class\"\n  val defaultLogStoreClass: String = classOf[DelegatingLogStore].getName\n\n  // The conf key for setting the LogStore implementation for `scheme`.\n  def logStoreSchemeConfKey(scheme: String): String = s\"spark.delta.logStore.${scheme}.impl\"\n\n  /**\n   * We accept keys both with and without the `spark.` prefix to maintain compatibility across the\n   * Delta ecosystem\n   * @param key the spark-prefixed key to access\n   */\n  def getLogStoreConfValue(key: String, sparkConf: SparkConf): Option[String] = {\n    // verifyLogStoreConfs already validated that if both keys exist the values are the same when\n    // the LogStore was instantiated\n    sparkConf.getOption(key)\n      .orElse(sparkConf.getOption(key.stripPrefix(\"spark.\")))\n  }\n\n  def createLogStore(spark: SparkSession): LogStore = {\n    LogStore(spark)\n  }\n\n  /**\n   * Check for conflicting LogStore configs in the spark configuration.\n   *\n   * To maintain compatibility across the Delta ecosystem, we accept keys both with and without the\n   * \"spark.\" prefix. This means for setting the class conf, we accept both\n   * \"spark.delta.logStore.class\" and \"delta.logStore.class\" and for scheme confs we accept both\n   * \"spark.delta.logStore.${scheme}.impl\" and \"delta.logStore.${scheme}.impl\"\n   *\n   * If a conf is set both with and without the spark prefix, it must be set to the same value,\n   * otherwise we throw an error.\n   */\n  def verifyLogStoreConfs(sparkConf: SparkConf): Unit = {\n    // check LogStore class conf key\n    val classConf = sparkConf.getOption(logStoreClassConfKey.stripPrefix(\"spark.\"))\n    classConf.foreach { nonPrefixValue =>\n      sparkConf.getOption(logStoreClassConfKey).foreach { prefixValue =>\n        // Both the spark-prefixed and non-spark-prefixed key is present in the sparkConf. Check\n        // that they store the same value, otherwise throw an error.\n        if (prefixValue != nonPrefixValue) {\n          throw DeltaErrors.inconsistentLogStoreConfs(\n            Seq((logStoreClassConfKey.stripPrefix(\"spark.\"), nonPrefixValue),\n            (logStoreClassConfKey, prefixValue)))\n        }\n      }\n    }\n\n    // check LogStore scheme conf keys\n    val schemeConfs = sparkConf.getAllWithPrefix(\"delta.logStore.\")\n      .filter(_._1.endsWith(\".impl\"))\n    schemeConfs.foreach { case (nonPrefixKey, nonPrefixValue) =>\n      val prefixKey = logStoreSchemeConfKey(nonPrefixKey.stripSuffix(\".impl\"))\n      sparkConf.getOption(prefixKey).foreach { prefixValue =>\n        // Both the spark-prefixed and non-spark-prefixed key is present in the sparkConf. Check\n        // that they store the same value, otherwise throw an error.\n        if (prefixValue != nonPrefixValue) {\n          throw DeltaErrors.inconsistentLogStoreConfs(\n            Seq((\"delta.logStore.\" + nonPrefixKey, nonPrefixValue), (prefixKey, prefixValue)))\n        }\n      }\n    }\n  }\n\n  def checkLogStoreConfConflicts(sparkConf: SparkConf): Unit = {\n    val sparkPrefixLogStoreConfs = sparkConf.getAllWithPrefix(\"spark.delta.logStore.\")\n      .map(kv => \"spark.delta.logStore.\" + kv._1 -> kv._2)\n    val nonSparkPrefixLogStoreConfs = sparkConf.getAllWithPrefix(\"delta.logStore.\")\n      .map(kv => \"delta.logStore.\" + kv._1 -> kv._2)\n    val (classConf, otherConf) = (sparkPrefixLogStoreConfs ++ nonSparkPrefixLogStoreConfs)\n      .partition(v => v._1.endsWith(\"class\"))\n    val schemeConf = otherConf.filter(_._1.endsWith(\".impl\"))\n    if (classConf.nonEmpty && schemeConf.nonEmpty) {\n      throw DeltaErrors.logStoreConfConflicts(classConf, schemeConf)\n    }\n  }\n\n  def createLogStore(sparkConf: SparkConf, hadoopConf: Configuration): LogStore = {\n    checkLogStoreConfConflicts(sparkConf)\n    verifyLogStoreConfs(sparkConf)\n    val logStoreClassName = getLogStoreConfValue(logStoreClassConfKey, sparkConf)\n      .getOrElse(defaultLogStoreClass)\n    LogStore.createLogStoreWithClassName(logStoreClassName, sparkConf, hadoopConf)\n  }\n}\n\nclass LogStoreInverseAdaptor(val logStoreImpl: LogStore, override val initHadoopConf: Configuration)\n    extends io.delta.storage.LogStore(initHadoopConf) {\n\n  override def read(\n      path: Path,\n      hadoopConf: Configuration): CloseableIterator[String] = {\n    val iter = logStoreImpl.readAsIterator(path, hadoopConf)\n    new CloseableIterator[String] {\n      override def close(): Unit = iter.close\n      override def hasNext: Boolean = iter.hasNext\n      override def next(): String = iter.next()\n    }\n  }\n\n  override def write(\n      path: Path,\n      actions: java.util.Iterator[String],\n      overwrite: java.lang.Boolean,\n      hadoopConf: Configuration): Unit = {\n    logStoreImpl.write(path, actions.asScala, overwrite, hadoopConf)\n  }\n\n  override def listFrom(\n      path: Path,\n      hadoopConf: Configuration): java.util.Iterator[FileStatus] =\n    logStoreImpl.listFrom(path, hadoopConf).asJava\n\n  override def resolvePathOnPhysicalStorage(\n      path: Path,\n      hadoopConf: Configuration): Path =\n    logStoreImpl.resolvePathOnPhysicalStorage(path, hadoopConf)\n\n  override def isPartialWriteVisible(\n      path: Path,\n      hadoopConf: Configuration): java.lang.Boolean =\n    logStoreImpl.isPartialWriteVisible(path, hadoopConf)\n}\n\nobject LogStoreInverseAdaptor {\n  def apply(logStoreImpl: LogStore, initHadoopConf: Configuration): LogStoreInverseAdaptor = {\n    new LogStoreInverseAdaptor(logStoreImpl, initHadoopConf)\n  }\n}\n\n/**\n * An adaptor from the new public LogStore API to the old private LogStore API. The old LogStore\n * API is still used in most places. Before we move all of them to the new API, adapting from\n * the new API to the old API is a cheap way to ensure that implementations of both APIs work.\n *\n * @param logStoreImpl An implementation of the new public LogStore API.\n */\nclass LogStoreAdaptor(val logStoreImpl: io.delta.storage.LogStore) extends LogStore {\n\n  private def getHadoopConfiguration: Configuration = {\n    // scalastyle:off deltahadoopconfiguration\n    SparkSession.getActiveSession.map(_.sessionState.newHadoopConf())\n      .getOrElse(logStoreImpl.initHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  override def read(path: Path): Seq[String] = {\n    read(path, getHadoopConfiguration)\n  }\n\n  override def read(path: Path, hadoopConf: Configuration): Seq[String] = {\n    var iter: io.delta.storage.CloseableIterator[String] = null\n    try {\n      iter = logStoreImpl.read(path, hadoopConf)\n      val contents = iter.asScala.toArray\n      contents\n    } finally {\n      if (iter != null) {\n        iter.close\n      }\n    }\n  }\n\n  override def readAsIterator(path: Path): ClosableIterator[String] = {\n    readAsIterator(path, getHadoopConfiguration)\n  }\n\n  override def readAsIterator(path: Path, hadoopConf: Configuration): ClosableIterator[String] = {\n    val iter = logStoreImpl.read(path, hadoopConf)\n    new ClosableIterator[String] {\n      override def close(): Unit = iter.close\n      override def hasNext: Boolean = iter.hasNext\n      override def next(): String = iter.next\n    }\n  }\n\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean): Unit = {\n    write(path, actions, overwrite, getHadoopConfiguration)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    logStoreImpl.write(path, actions.asJava, overwrite, hadoopConf)\n  }\n\n  override def listFrom(path: Path): Iterator[FileStatus] = {\n    listFrom(path, getHadoopConfiguration)\n  }\n\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    logStoreImpl.listFrom(path, hadoopConf).asScala\n  }\n\n  override def invalidateCache(): Unit = {}\n\n  override def resolvePathOnPhysicalStorage(path: Path): Path = {\n    resolvePathOnPhysicalStorage(path, getHadoopConfiguration)\n  }\n\n  override def resolvePathOnPhysicalStorage(path: Path, hadoopConf: Configuration): Path = {\n    logStoreImpl.resolvePathOnPhysicalStorage(path, hadoopConf)\n  }\n\n  override def isPartialWriteVisible(path: Path): Boolean = {\n    isPartialWriteVisible(path, getHadoopConfiguration)\n  }\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = {\n    logStoreImpl.isPartialWriteVisible(path, hadoopConf)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/S3SingleDriverLogStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.io.FileNotFoundException\nimport java.net.URI\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.concurrent.{ConcurrentHashMap, TimeUnit}\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.util.FileNames\nimport com.google.common.cache.CacheBuilder\nimport com.google.common.io.CountingOutputStream\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs._\n\nimport org.apache.spark.SparkConf\n\n/**\n * Single Spark-driver/JVM LogStore implementation for S3.\n *\n * We assume the following from S3's [[FileSystem]] implementations:\n * - File writing on S3 is all-or-nothing, whether overwrite or not.\n * - List-after-write can be inconsistent.\n *\n * Regarding file creation, this implementation:\n * - Opens a stream to write to S3 (regardless of the overwrite option).\n * - Failures during stream write may leak resources, but may never result in partial writes.\n *\n * Regarding directory listing, this implementation:\n * - returns a list by merging the files listed from S3 and recently-written files from the cache.\n */\nclass S3SingleDriverLogStore(\n    sparkConf: SparkConf,\n    hadoopConf: Configuration) extends HadoopFileSystemLogStore(sparkConf, hadoopConf) {\n  import S3SingleDriverLogStore._\n\n  private def resolved(path: Path, hadoopConf: Configuration): (FileSystem, Path) = {\n    val fs = path.getFileSystem(hadoopConf)\n    val resolvedPath = stripUserInfo(fs.makeQualified(path))\n    (fs, resolvedPath)\n  }\n\n  private def getPathKey(resolvedPath: Path): Path = {\n    stripUserInfo(resolvedPath)\n  }\n\n  private def stripUserInfo(path: Path): Path = {\n    val uri = path.toUri\n    val newUri = new URI(\n      uri.getScheme,\n      null,\n      uri.getHost,\n      uri.getPort,\n      uri.getPath,\n      uri.getQuery,\n      uri.getFragment)\n    new Path(newUri)\n  }\n\n  /**\n   * Merge two iterators of [[FileStatus]] into a single iterator ordered by file path name.\n   * In case both iterators have [[FileStatus]]s for the same file path, keep the one from\n   * `iterWithPrecedence` and discard that from `iter`.\n   */\n  private def mergeFileIterators(\n      iter: Iterator[FileStatus],\n      iterWithPrecedence: Iterator[FileStatus]): Iterator[FileStatus] = {\n    (iter.map(f => (f.getPath, f)).toMap ++ iterWithPrecedence.map(f => (f.getPath, f)))\n      .values\n      .toSeq\n      .sortBy(_.getPath.getName)\n      .iterator\n  }\n\n  /**\n   * List files starting from `resolvedPath` (inclusive) in the same directory.\n   */\n  private def listFromCache(fs: FileSystem, resolvedPath: Path) = {\n    val pathKey = getPathKey(resolvedPath)\n    writtenPathCache\n      .asMap()\n      .asScala\n      .iterator\n      .filter { case (path, _) =>\n        path.getParent == pathKey.getParent() && path.getName >= pathKey.getName }\n      .map { case (path, fileMetadata) =>\n        new FileStatus(\n          fileMetadata.length,\n          false,\n          1,\n          fs.getDefaultBlockSize(path),\n          fileMetadata.modificationTime,\n          path)\n      }\n  }\n\n  /**\n   * List files starting from `resolvedPath` (inclusive) in the same directory, which merges\n   * the file system list and the cache list when `useCache` is on, otherwise\n   * use file system list only.\n   */\n  private def listFromInternal(fs: FileSystem, resolvedPath: Path, useCache: Boolean = true) = {\n    val parentPath = resolvedPath.getParent\n    if (!fs.exists(parentPath)) {\n      throw DeltaErrors.fileOrDirectoryNotFoundException(parentPath.toString)\n    }\n    val listedFromFs =\n      fs.listStatus(parentPath).filter(_.getPath.getName >= resolvedPath.getName).iterator\n    val listedFromCache = if (useCache) listFromCache(fs, resolvedPath) else Iterator.empty\n\n    // File statuses listed from file system take precedence\n    mergeFileIterators(listedFromCache, listedFromFs)\n  }\n\n  override def listFrom(path: Path): Iterator[FileStatus] = {\n    listFrom(path, getHadoopConfiguration)\n  }\n\n  /**\n   * List files starting from `resolvedPath` (inclusive) in the same directory.\n   */\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    val (fs, resolvedPath) = resolved(path, hadoopConf)\n    listFromInternal(fs, resolvedPath)\n  }\n\n  /**\n   * Check if the path is an initial version of a Delta log.\n   */\n  private def isInitialVersion(path: Path): Boolean = {\n    FileNames.isDeltaFile(path) && FileNames.deltaVersion(path) == 0L\n  }\n\n  /**\n   * Check if a path exists. Normally we check both the file system and the cache, but when the\n   * path is the first version of a Delta log, we ignore the cache.\n   */\n  private def exists(fs: FileSystem, resolvedPath: Path): Boolean = {\n    // Ignore the cache for the first file of a Delta log\n    listFromInternal(fs, resolvedPath, useCache = !isInitialVersion(resolvedPath))\n      .take(1)\n      .exists(_.getPath.getName == resolvedPath.getName)\n  }\n\n  override def write(path: Path, actions: Iterator[String], overwrite: Boolean = false): Unit = {\n    write(path, actions, overwrite, getHadoopConfiguration)\n  }\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    val (fs, resolvedPath) = resolved(path, hadoopConf)\n    val lockedPath = getPathKey(resolvedPath)\n    acquirePathLock(lockedPath)\n    try {\n      if (exists(fs, resolvedPath) && !overwrite) {\n        throw new java.nio.file.FileAlreadyExistsException(resolvedPath.toUri.toString)\n      }\n      val stream = new CountingOutputStream(fs.create(resolvedPath, overwrite))\n      actions.map(_ + \"\\n\").map(_.getBytes(UTF_8)).foreach(stream.write)\n      stream.close()\n\n      // When a Delta log starts afresh, all cached files in that Delta log become obsolete,\n      // so we remove them from the cache.\n      if (isInitialVersion(resolvedPath)) {\n        val obsoleteFiles = writtenPathCache\n          .asMap()\n          .asScala\n          .keys\n          .filter(_.getParent == lockedPath.getParent())\n          .asJava\n\n        writtenPathCache.invalidateAll(obsoleteFiles)\n      }\n\n      // Cache the information of written files to help fix the inconsistency in future listings\n      writtenPathCache.put(lockedPath,\n        FileMetadata(stream.getCount(), System.currentTimeMillis()))\n    } catch {\n      // Convert Hadoop's FileAlreadyExistsException to Java's FileAlreadyExistsException\n      case e: org.apache.hadoop.fs.FileAlreadyExistsException =>\n          val converted = new java.nio.file.FileAlreadyExistsException(e.getMessage)\n          converted.initCause(e)\n          throw converted\n    } finally {\n      releasePathLock(lockedPath)\n    }\n  }\n\n  override def isPartialWriteVisible(path: Path): Boolean = false\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean = false\n\n  override def invalidateCache(): Unit = {\n    writtenPathCache.invalidateAll()\n  }\n}\n\nobject S3SingleDriverLogStore {\n  /**\n   * A global path lock to ensure that no concurrent writers writing to the same path in the same\n   * JVM.\n   */\n  private val pathLock = new ConcurrentHashMap[Path, AnyRef]()\n\n  /**\n   * A global cache that records the metadata of the files recently written.\n   * As list-after-write may be inconsistent on S3, we can use the files in the cache\n   * to fix the inconsistent file listing.\n   */\n  private val writtenPathCache =\n    CacheBuilder.newBuilder()\n      .expireAfterAccess(120, TimeUnit.MINUTES)\n      .build[Path, FileMetadata]()\n\n  /**\n   * Release the lock for the path after writing.\n   *\n   * Note: the caller should resolve the path to make sure we are locking the correct absolute path.\n   */\n  private def releasePathLock(resolvedPath: Path): Unit = {\n    val lock = pathLock.remove(resolvedPath)\n    lock.synchronized {\n      lock.notifyAll()\n    }\n  }\n\n  /**\n   * Acquire a lock for the path before writing.\n   *\n   * Note: the caller should resolve the path to make sure we are locking the correct absolute path.\n   */\n  private def acquirePathLock(resolvedPath: Path): Unit = {\n    while (true) {\n      val lock = pathLock.putIfAbsent(resolvedPath, new Object)\n      if (lock == null) return\n      lock.synchronized {\n        while (pathLock.get(resolvedPath) == lock) {\n          lock.wait()\n        }\n      }\n    }\n  }\n}\n\n/**\n * The file metadata to be stored in the cache.\n */\ncase class FileMetadata(length: Long, modificationTime: Long)\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/storage/dv/DeletionVectorStore.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage.dv\n\nimport java.io.{Closeable, DataInputStream}\nimport java.net.URI\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.UUID\nimport java.util.zip.CRC32\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, StoredBitmap}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.util.PathWithFileSystem\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, FSDataOutputStream, Path}\n\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.util.Utils\n\ntrait DeletionVectorStore extends DeltaLogging {\n  /**\n   * Read a Deletion Vector and parse it as [[RoaringBitmapArray]].\n   */\n  def read(\n      dvDescriptor: DeletionVectorDescriptor,\n      tablePath: Path): RoaringBitmapArray =\n    StoredBitmap.create(dvDescriptor, tablePath).load(this)\n\n  /**\n   * Read Deletion Vector and parse it as [[RoaringBitmapArray]].\n   */\n  def read(path: Path, offset: Int, size: Int): RoaringBitmapArray\n\n  /**\n   * Returns a writer that can be used to write multiple deletion vectors to the file at `path`.\n   */\n  def createWriter(path: PathWithFileSystem): DeletionVectorStore.Writer\n\n  /**\n   * Returns full path for a DV with `filedId` UUID under `targetPath`.\n   *\n   * Optionally, prepend a `prefix` to the name.\n   */\n  def generateFileNameInTable(\n      targetPath: PathWithFileSystem,\n      fileId: UUID,\n      prefix: String = \"\"): PathWithFileSystem = {\n    DeletionVectorStore.assembleDeletionVectorPathWithFileSystem(targetPath, fileId, prefix)\n  }\n\n  /**\n   * Return a new unique path under `targetPath`.\n   *\n   * Optionally, prepend a `prefix` to the name.\n   */\n  def generateUniqueNameInTable(\n      targetPath: PathWithFileSystem,\n      prefix: String = \"\"): PathWithFileSystem =\n    generateFileNameInTable(targetPath, UUID.randomUUID(), prefix)\n\n  /**\n   * Creates a [[PathWithFileSystem]] instance\n   * by using the configuration of this `DeletionVectorStore` instance\n   */\n  def pathWithFileSystem(path: Path): PathWithFileSystem\n}\n\n/**\n * Trait containing the utility and constants needed for [[DeletionVectorStore]]\n */\ntrait DeletionVectorStoreUtils {\n  final val DV_FILE_FORMAT_VERSION_ID_V1: Byte = 1\n\n  /** The length of a DV checksum. See [[calculateChecksum()]]. */\n  final val CHECKSUM_LEN = 4\n  /** The size of the stored length of a DV. */\n  final val DATA_SIZE_LEN = 4\n\n  // DV Format:<SerializedDV Size> <SerializedDV Bytes> <DV Checksum>\n  def getTotalSizeOfDVFieldsInFile(bitmapDataSize: Int): Int = {\n    DATA_SIZE_LEN + bitmapDataSize + CHECKSUM_LEN\n  }\n\n  /** Convert the given String path to a Hadoop Path. Please make sure the path is not escaped. */\n  def unescapedStringToPath(path: String): Path = SparkPath.fromPathString(path).toPath\n\n  /** Convert the given String path to a Hadoop Path, Please make sure the path is escaped. */\n  def escapedStringToPath(path: String): Path = SparkPath.fromUrlString(path).toPath\n\n  /** Convert the given Hadoop path to a String Path, handing special characters properly. */\n  def pathToEscapedString(path: Path): String = SparkPath.fromPath(path).urlEncoded\n\n  /**\n   * Calculate checksum of a serialized deletion vector. We are using CRC32 which has 4bytes size,\n   * but CRC32 implementation conforms to Java Checksum interface which requires a long. However,\n   * the high-order bytes are zero, so here is safe to cast to Int. This will result in negative\n   * checksums, but this is not a problem because we only care about equality.\n   */\n  def calculateChecksum(data: Array[Byte]): Int = {\n    val crc = new CRC32()\n    crc.update(data)\n    crc.getValue.toInt\n  }\n\n  /**\n   * Read a serialized deletion vector from a data stream.\n   */\n  def readRangeFromStream(reader: DataInputStream, size: Int): Array[Byte] = {\n    val sizeAccordingToFile = reader.readInt()\n    if (size != sizeAccordingToFile) {\n      throw DeltaErrors.deletionVectorSizeMismatch()\n    }\n\n    val buffer = new Array[Byte](size)\n    reader.readFully(buffer)\n\n    val expectedChecksum = reader.readInt()\n    val actualChecksum = calculateChecksum(buffer)\n    if (expectedChecksum != actualChecksum) {\n      throw DeltaErrors.deletionVectorChecksumMismatch()\n    }\n\n    buffer\n  }\n\n  /**\n   * Same as `assembleDeletionVectorPath`, but keeps the new path bundled with the fs.\n   */\n  def assembleDeletionVectorPathWithFileSystem(\n      targetParentPathWithFileSystem: PathWithFileSystem,\n      id: UUID,\n      prefix: String = \"\"): PathWithFileSystem = {\n    targetParentPathWithFileSystem.copy(path =\n      DeletionVectorDescriptor.assembleDeletionVectorPath(\n        targetParentPathWithFileSystem.path, id, prefix))\n  }\n\n  /** Descriptor for a serialized Deletion Vector in a file. */\n  case class DVRangeDescriptor(offset: Int, length: Int, checksum: Int)\n\n  trait Writer extends Closeable {\n    /**\n     * Appends the serialized deletion vector in `data` to the file, and returns the offset in the\n     * file that the deletion vector was written to and its checksum.\n     */\n    def write(data: Array[Byte]): DVRangeDescriptor\n\n    /**\n     * Returns UTF-8 encoded path of the file that is being written by this writer.\n     */\n    def serializedPath: Array[Byte]\n\n    /**\n     * Closes this writer. After calling this method it is no longer valid to call write (or close).\n     * This method must always be called when the owner of this writer is done writing deletion\n     * vectors.\n     */\n    def close(): Unit\n  }\n}\n\nobject DeletionVectorStore extends DeletionVectorStoreUtils {\n  /** Create a new instance of [[DeletionVectorStore]] from the given Hadoop configuration. */\n  private[delta] def createInstance(\n      hadoopConf: Configuration): DeletionVectorStore =\n    new HadoopFileSystemDVStore(hadoopConf)\n}\n\n/**\n * Default [[DeletionVectorStore]] implementation for Hadoop [[FileSystem]] implementations.\n *\n * Note: This class must be thread-safe,\n * because we sometimes write multiple deletion vectors in parallel through the same store.\n */\nclass HadoopFileSystemDVStore(hadoopConf: Configuration)\n    extends DeletionVectorStore {\n\n  override def read(path: Path, offset: Int, size: Int): RoaringBitmapArray = {\n    val fs = path.getFileSystem(hadoopConf)\n    val buffer = Utils.tryWithResource(fs.open(path)) { reader =>\n      reader.seek(offset)\n      DeletionVectorStore.readRangeFromStream(reader, size)\n    }\n    DeletionVectorUtils.deserialize(\n      buffer,\n      debugInfo = Map(\"path\" -> path, \"offset\" -> offset, \"size\" -> size))\n  }\n\n  override def createWriter(path: PathWithFileSystem): DeletionVectorStore.Writer = {\n    new DeletionVectorStore.Writer {\n      // Lazily create the writer for the deletion vectors, so that we don't write an empty file\n      // in case all deletion vectors are empty.\n      private var outputStream: FSDataOutputStream = _\n      private var writtenBytes = 0L\n\n      override def write(data: Array[Byte]): DeletionVectorStore.DVRangeDescriptor = {\n        if (outputStream == null) {\n          val overwrite = false // `create` Java API does not support named parameters\n          outputStream = path.fs.create(path.path, overwrite)\n          outputStream.writeByte(DeletionVectorStore.DV_FILE_FORMAT_VERSION_ID_V1)\n          writtenBytes += 1\n        }\n        val dvRange = DeletionVectorStore.DVRangeDescriptor(\n          offset = outputStream.size(),\n          length = data.length,\n          checksum = DeletionVectorStore.calculateChecksum(data))\n\n        if (writtenBytes != dvRange.offset) {\n          deltaAssert(\n            writtenBytes == dvRange.offset,\n            name = \"dv.write.offsetMismatch\",\n            msg = s\"Offset mismatch while writing deletion vector to file\",\n            data = Map(\n              \"path\" -> path.path.toString,\n              \"reportedOffset\" -> dvRange.offset,\n              \"calculatedOffset\" -> writtenBytes)\n          )\n          throw DeltaErrors.deletionVectorSizeMismatch()\n        }\n\n        log.debug(s\"Writing DV range to file: Path=${path.path}, Range=${dvRange}\")\n        outputStream.writeInt(data.length)\n        outputStream.write(data)\n        outputStream.writeInt(dvRange.checksum)\n        writtenBytes += DeletionVectorStore.getTotalSizeOfDVFieldsInFile(data.length)\n\n        dvRange\n      }\n\n      override val serializedPath: Array[Byte] =\n        DeletionVectorStore.pathToEscapedString(path.path).getBytes(UTF_8)\n\n      override def close(): Unit = {\n        if (outputStream != null) {\n          outputStream.close()\n        }\n      }\n    }\n  }\n\n  override def pathWithFileSystem(path: Path): PathWithFileSystem =\n    PathWithFileSystem.withConf(path, hadoopConf)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/streaming/SchemaTrackingLog.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.streaming\n\nimport java.io.{InputStream, OutputStream}\nimport java.nio.charset.StandardCharsets._\n\nimport scala.io.{Source => IOSource}\nimport scala.reflect.ClassTag\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.annotation.JsonIgnore\n\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.execution.streaming.HDFSMetadataLog\nimport org.apache.spark.sql.types.{DataType, StructType}\n\n/**\n * A serializable schema with a partition schema and a data schema.\n */\ntrait PartitionAndDataSchema {\n\n  @JsonIgnore\n  def dataSchema: DataType\n\n  @JsonIgnore\n  def partitionSchema: StructType\n}\n\n/**\n * A schema serializer handles the SerDe of a [[PartitionAndDataSchema]]\n */\nsealed trait SchemaSerializer[T <: PartitionAndDataSchema] {\n  def serdeVersion: Int\n\n  def serialize(schema: T, outputStream: OutputStream): Unit\n\n  def deserialize(in: InputStream): T\n}\n\n/**\n *A schema serializer that reads/writes schema using the following format:\n * {SERDE_VERSION}\n * {JSON of the serializable schema}\n */\nclass JsonSchemaSerializer[T <: PartitionAndDataSchema: ClassTag: Manifest]\n  (override val serdeVersion: Int) extends SchemaSerializer[T] {\n\n  import SchemaTrackingExceptions._\n\n  val EMPTY_JSON = \"{}\"\n\n  /**\n   * Deserializes the log entry from input stream.\n   * @throws FailedToDeserializeException\n   */\n  override def deserialize(in: InputStream): T = {\n    // Called inside a try-finally where the underlying stream is closed in the caller\n    val lines = IOSource.fromInputStream(in, UTF_8.name()).getLines()\n\n    if (!lines.hasNext) {\n      throw FailedToDeserializeException\n    }\n\n    MetadataVersionUtil.validateVersion(lines.next(), serdeVersion)\n    val schemaJson = if (lines.hasNext) lines.next() else EMPTY_JSON\n    JsonUtils.fromJson(schemaJson)\n  }\n\n  override def serialize(metadata: T, out: OutputStream): Unit = {\n    // Called inside a try-finally where the underlying stream is closed in the caller\n    out.write(s\"v${serdeVersion}\".getBytes(UTF_8))\n    out.write('\\n')\n\n    // Write metadata\n    out.write(JsonUtils.toJson(metadata).getBytes(UTF_8))\n  }\n}\n\n/**\n * The underlying class for a streaming log that keeps track of a sequence of schema changes.\n *\n * It keeps tracks of the sequence of schema changes that this log is aware of, and it detects any\n * concurrent modifications to the schema log to prevent accidents on a best effort basis.\n */\nclass SchemaTrackingLog[T <: PartitionAndDataSchema: ClassTag: Manifest](\n    sparkSession: SparkSession,\n    path: String,\n    schemaSerializer: SchemaSerializer[T])\n  extends HDFSMetadataLog[T](sparkSession, path) with Logging {\n\n  import SchemaTrackingExceptions._\n\n  // The schema and version detected when this log is initialized\n  private val schemaAndSeqNumAtLogInit: Option[(Long, T)] = getLatest()\n\n  // Next schema version to write, this should be updated after each schema evolution.\n  // This allow HDFSMetadataLog to best detect concurrent schema log updates.\n  private var currentSeqNum: Long = schemaAndSeqNumAtLogInit.map(_._1).getOrElse(-1L)\n  private var nextSeqNumToWrite: Long = currentSeqNum + 1\n\n  // The current persisted schema this log has been tracking. Note that this does NOT necessarily\n  // always equal to the globally latest schema. Attempting to commit to a schema version that\n  // already exists is illegal.\n  // Subclass can leverage this to compare the differences.\n  private var currentTrackedSchema: Option[T] = schemaAndSeqNumAtLogInit.map(_._2)\n\n\n  /**\n   * Get the latest tracked schema entry by this schema log\n   */\n  def getCurrentTrackedSchema: Option[T] = currentTrackedSchema\n\n  /**\n   * Get the latest tracked schema batch ID / seq num by this log\n   */\n  def getCurrentTrackedSeqNum: Long = currentSeqNum\n\n  /**\n   * Get the tracked schema at specified seq num.\n   */\n  def getTrackedSchemaAtSeqNum(seqNum: Long): Option[T] = get(seqNum)\n\n  /**\n   * Deserializes the log entry from input stream.\n   * @throws FailedToDeserializeException\n   */\n  override protected def deserialize(in: InputStream): T =\n    schemaSerializer.deserialize(in).asInstanceOf[T]\n\n  override protected def serialize(metadata: T, out: OutputStream): Unit =\n    schemaSerializer.serialize(metadata, out)\n\n  /**\n   * Main API to actually write the log entry to the schema log. Clients can leverage this\n   * to save their new schema to the log.\n   * @throws FailedToEvolveSchema\n   * @param newSchema New persisted schema\n   */\n  def addSchemaToLog(newSchema: T): T = {\n    // Write to schema log\n    logInfo(log\"Writing a new metadata version \" +\n      log\"${MDC(DeltaLogKeys.VERSION, nextSeqNumToWrite)} in the metadata log\")\n    if (currentTrackedSchema.contains(newSchema)) {\n      // Record a warning if schema has not changed\n      logWarning(log\"Schema didn't change after schema evolution. \" +\n        log\"currentSchema = ${MDC(DeltaLogKeys.SCHEMA, currentTrackedSchema)}.\")\n      return newSchema\n    }\n    // Similar to how MicrobatchExecution detects concurrent checkpoint updates\n    if (!add(nextSeqNumToWrite, newSchema)) {\n      throw FailedToEvolveSchema\n    }\n\n    currentTrackedSchema = Some(newSchema)\n    currentSeqNum = nextSeqNumToWrite\n    nextSeqNumToWrite += 1\n    newSchema\n  }\n}\n\nobject SchemaTrackingExceptions {\n  // Designated exceptions\n  val FailedToDeserializeException =\n    new RuntimeException(\"Failed to deserialize schema log\")\n  val FailedToEvolveSchema =\n    new RuntimeException(\"Failed to add schema entry to log. Concurrent operations detected.\")\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/tablefeatures/tableChanges.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.tablefeatures\n\nimport org.apache.spark.sql.connector.catalog.TableChange\n\n/**\n * Change to remove a feature from a table.\n * @param featureName The name of the feature\n * @param truncateHistory When true we set the minimum log retention period and clean up metadata.\n */\ncase class DropFeature(featureName: String, truncateHistory: Boolean) extends TableChange {}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/uniform/ParquetIcebergCompatV2Utils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFooterReaderShims\n\n/**\n * Contains utilities to check whether a specific parquet data file\n * is considered `IcebergCompatV2`.\n * See [[isParquetIcebergV2Compatible]] for details.\n */\nobject ParquetIcebergCompatV2Utils {\n  // TIMESTAMP stored as INT96 is NOT considered `IcebergCompatV2`.\n  // NOTE: `TIMESTAMP <-> INT96` is an exact *one-to-one* mapping\n  // in default and `IcebergCompatV1` delta table.\n  // this means we could confidently claim an `INT96` must be `TIMESTAMP`\n  // if found in the parquet footer schema field.\n  private val TIMESTAMP_AS_INT96 = org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT96\n\n  /**\n   * Recursively traverse a specific parquet schema field,\n   * check the following properties, i.e.,\n   * - for primitive type,\n   *   - check if TIMESTAMP is stored as INT96.\n   *   - check for the `field_id`.\n   * - for group type,\n   *   - check for the `field_id`.\n   *   - iterate through all fields and check each field recursively in the same way.\n   *\n   * @param field the field to check, this corresponds to a specific parquet file.\n   * @return whether the parquet field contains TIMESTAMP stored as INT96 or\n   *         lacking `field_id` for any (nested) field or not;\n   *         if so, return true; otherwise return false.\n   */\n  private def hasTimestampAsInt96OrFieldIdNotExistForType(\n      field: org.apache.parquet.schema.Type): Boolean = field match {\n    // note: `getId` returns null indicates the field does not contain `field_id`.\n    case p: org.apache.parquet.schema.PrimitiveType =>\n      (p.getPrimitiveTypeName == TIMESTAMP_AS_INT96) || (p.getId == null)\n    case g: org.apache.parquet.schema.GroupType =>\n      if (g.getId != null) {\n        val logicalAnnotation = g.getLogicalTypeAnnotation\n        val fields = if (logicalAnnotation != null &&\n            (logicalAnnotation.toString == \"LIST\" || logicalAnnotation.toString == \"MAP\")) {\n          // according to parquet's spec,\n          // - for LIST:\n          //   - the outer-most level must be a group annotated with LIST\n          //     that contains a **single** field named list.\n          // - for MAP:\n          //   - the outer-most level must be a group annotated with MAP\n          //     that contains a **single** field named key_value.\n          // details could be found at\n          // [[https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists]] and\n          // [[https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#maps]] and\n          g.getFields.get(0).asGroupType().getFields\n        } else {\n          g.getFields\n        }\n        fields.toArray.exists {\n          case field: org.apache.parquet.schema.Type =>\n            hasTimestampAsInt96OrFieldIdNotExistForType(field)\n        }\n      } else {\n        true\n      }\n  }\n\n  /**\n   * Check if the parquet file is `IcebergCompatV2` by inspecting the\n   * provided parquet footer.\n   *\n   * note: icebergV2-compatible check refer to the following two properties.\n   * 1. TIMESTAMP\n   *    - If TIMESTAMP is stored as `int96`, it's considered incompatible since\n   *      iceberg stores TIMESTAMP as `int64` according to the iceberg spec.\n   * 2. field_id\n   *    - `field_id` is needed for *every* field in a parquet footer, this includes\n   *      the field of each column, and the potential nested fields for nested types\n   *      like LIST, MAP or STRUCT.\n   *      See [[https://github.com/apache/parquet-format/blob/master/LogicalTypes.md]] for details.\n   *    - This is checked by inspecting whether the `field_id` for each column\n   *      is null or not recursively.\n   *\n   * @param footer the parquet footer to be checked.\n   * @return whether the parquet file is considered `IcebergCompatV2`.\n   */\n  def isParquetIcebergCompatV2(footer: ParquetMetadata): Boolean = {\n    // iterate through each column/field and check if there exists\n    // any column/field that contains TIMESTAMP stored as INT96,\n    // or lacking any `field_id` (included nested one as in LIST or MAP).\n    !footer.getFileMetaData.getSchema.getFields.toArray.exists {\n      case field: org.apache.parquet.schema.Type =>\n        hasTimestampAsInt96OrFieldIdNotExistForType(field)\n    }\n  }\n\n  /**\n   * Get the parquet footer based on the input `parquetPath`.\n   *\n   * @param parquetPath the absolute path to the parquet file.\n   * @return the corresponding parquet metadata/footer.\n   */\n  def getParquetFooter(parquetPath: String): ParquetMetadata = {\n    val path = new org.apache.hadoop.fs.Path(parquetPath)\n    val conf = new org.apache.hadoop.conf.Configuration\n    val fs = path.getFileSystem(conf)\n    val status = fs.getFileStatus(path)\n    ParquetFooterReaderShims.readParquetFooter(conf, status, ParquetMetadataConverter.NO_FILTER)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/AnalysisHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DataFrameUtils, DeltaAnalysisException, DeltaErrors}\n\nimport org.apache.spark.sql.{AnalysisException, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.ExtendedAnalysisException\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\n\ntrait AnalysisHelper {\n  import AnalysisHelper._\n\n  // Keeping the following two methods for backward compatibility with previous Delta versions.\n  protected def tryResolveReferences(\n      sparkSession: SparkSession)(\n      expr: Expression,\n      planContainingExpr: LogicalPlan): Expression =\n  tryResolveReferencesForExpressions(sparkSession)(Seq(expr), planContainingExpr.children).head\n\n  protected def tryResolveReferencesForExpressions(\n      sparkSession: SparkSession,\n      exprs: Seq[Expression],\n      planContainingExpr: LogicalPlan): Seq[Expression] =\n  tryResolveReferencesForExpressions(sparkSession)(exprs, planContainingExpr.children)\n\n  /**\n   * Resolve expressions using the attributes provided by `planProvidingAttrs`. Throw an error if\n   * failing to resolve any expressions.\n   */\n  protected def resolveReferencesForExpressions(\n      sparkSession: SparkSession,\n      exprs: Seq[Expression],\n      planProvidingAttrs: LogicalPlan): Seq[Expression] = {\n    val resolvedExprs =\n      tryResolveReferencesForExpressions(sparkSession)(exprs, Seq(planProvidingAttrs))\n    resolvedExprs.foreach { expr =>\n      if (!expr.resolved) {\n        throw new ExtendedAnalysisException(\n          new DeltaAnalysisException(\n            errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0012\",\n            messageParameters = Array(expr.toString)\n          ),\n          planProvidingAttrs\n        )\n      }\n    }\n    resolvedExprs\n  }\n\n  /**\n   * Resolve expressions using the attributes provided by `planProvidingAttrs`, ignoring errors.\n   */\n  protected def tryResolveReferencesForExpressions(\n      sparkSession: SparkSession)(\n      exprs: Seq[Expression],\n      plansProvidingAttrs: Seq[LogicalPlan]): Seq[Expression] = {\n    val newPlan = FakeLogicalPlan(exprs, plansProvidingAttrs)\n    sparkSession.sessionState.analyzer.execute(newPlan) match {\n      case FakeLogicalPlan(resolvedExprs, _) =>\n        // Return even if it did not successfully resolve\n        resolvedExprs\n      case _ =>\n        // This is unexpected\n        throw new ExtendedAnalysisException(\n          new DeltaAnalysisException(\n            errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0012\",\n            messageParameters = Array(exprs.mkString(\",\"))\n          ),\n          newPlan\n        )\n    }\n  }\n\n  protected def toDataset(sparkSession: SparkSession, logicalPlan: LogicalPlan): Dataset[Row] = {\n    DataFrameUtils.ofRows(sparkSession, logicalPlan)\n  }\n\n  protected def improveUnsupportedOpError[T](f: => T): T = {\n    val possibleErrorMsgs = Seq(\n      \"is only supported with v2 table\", // full error: DELETE is only supported with v2 tables\n      \"is not supported temporarily\",    // full error: UPDATE TABLE is not supported temporarily\n      \"Table does not support read\",\n      \"Table implementation does not support writes\"\n    ).map(_.toLowerCase())\n\n    def isExtensionOrCatalogError(error: Exception): Boolean = {\n      possibleErrorMsgs.exists { m =>\n        error.getMessage != null && error.getMessage.toLowerCase().contains(m)\n      }\n    }\n\n    try { f } catch {\n      case e: Exception if isExtensionOrCatalogError(e) =>\n        throw DeltaErrors.configureSparkSessionWithExtensionAndCatalog(Some(e))\n    }\n  }\n\n}\n\nobject AnalysisHelper {\n  /** LogicalPlan to help resolve the given expression */\n  case class FakeLogicalPlan(\n      exprs: Seq[Expression],\n      children: Seq[LogicalPlan])\n    extends LogicalPlan\n  {\n    override def output: Seq[Attribute] = Nil\n\n    override protected def withNewChildrenInternal(\n      newChildren: IndexedSeq[LogicalPlan]): FakeLogicalPlan = copy(children = newChildren)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/BinPackingIterator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.generic.Sizing\nimport scala.collection.mutable.ArrayBuffer\n\n/**\n * Iterator that packs objects in `inputIter` to create bins that have a total size of\n * 'targetSize'. Each [[T]] object may contain multiple inputs that are always packed into a\n * single bin. [[T]] instances must inherit from [[Sizing]] and define what is their size.\n */\nclass BinPackingIterator[T <: Sizing](\n    inputIter: Iterator[T],\n    targetSize: Long)\n  extends Iterator[Seq[T]] {\n\n  private val currentBin = new ArrayBuffer[T]()\n  private var sizeOfCurrentBin = 0L\n\n  override def hasNext: Boolean = inputIter.hasNext || currentBin.nonEmpty\n\n  override def next(): Seq[T] = {\n    var resultBin: Seq[T] = null\n    while (inputIter.hasNext && resultBin == null) {\n      val input = inputIter.next()\n\n      val sizeOfCurrentFile = input.size\n\n      // Start a new bin if the deletion vectors for the current Parquet file corresponding to\n      // `row` causes us to go over the target file size.\n      if (currentBin.nonEmpty &&\n        sizeOfCurrentBin + sizeOfCurrentFile > targetSize) {\n        resultBin = currentBin.toVector\n        sizeOfCurrentBin = 0L\n        currentBin.clear()\n      }\n\n      currentBin += input\n      sizeOfCurrentBin += sizeOfCurrentFile\n    }\n\n    // Finish the last bin.\n    if (resultBin == null && !inputIter.hasNext) {\n      resultBin = currentBin.toVector\n      currentBin.clear()\n    }\n\n    resultBin\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/BinPackingUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.mutable.ArrayBuffer\n\nobject BinPackingUtils {\n  /**\n   * Takes a sequence of items and groups them such that the size of each group is\n   * less than the specified maxBinSize.\n   */\n  @inline def binPackBySize[I, V](\n      elements: Seq[I],\n      sizeGetter: I => Long,\n      valueGetter: I => V,\n      maxBinSize: Long): Seq[Seq[V]] = {\n    val bins = new ArrayBuffer[Seq[V]]()\n\n    val currentBin = new ArrayBuffer[V]()\n    var currentSize = 0L\n\n    elements.sortBy(sizeGetter).foreach { element =>\n      val size = sizeGetter(element)\n      // Generally, a bin is a group of existing files, whose total size does not exceed the\n      // desired maxFileSize. They will be coalesced into a single output file.\n      if ((currentSize >= maxBinSize) || size + currentSize > maxBinSize) {\n        if (currentBin.nonEmpty) {\n          bins += currentBin.toVector\n          currentBin.clear()\n        }\n        currentBin += valueGetter(element)\n        currentSize = size\n      } else {\n        currentBin += valueGetter(element)\n        currentSize += size\n      }\n    }\n\n    if (currentBin.nonEmpty) {\n      bins += currentBin.toVector\n    }\n    bins.toSeq\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/Codec.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport java.nio.ByteBuffer\nimport java.nio.charset.StandardCharsets.US_ASCII\nimport java.util.UUID\n\nimport com.google.common.primitives.UnsignedInteger\n\n/** Additional codecs not supported by Apache Commons Codecs. */\nobject Codec {\n\n  def uuidToBytes(id: UUID): Array[Byte] = uuidToByteBuffer(id).array()\n\n  def uuidFromBytes(bytes: Array[Byte]): UUID = {\n    require(bytes.length == 16)\n    uuidFromByteBuffer(ByteBuffer.wrap(bytes))\n  }\n\n  def uuidToByteBuffer(id: UUID): ByteBuffer = {\n    val buffer = ByteBuffer.allocate(16)\n    buffer.putLong(id.getMostSignificantBits)\n    buffer.putLong(id.getLeastSignificantBits)\n    buffer.rewind()\n    buffer\n  }\n\n  def uuidFromByteBuffer(buffer: ByteBuffer): UUID = {\n    require(buffer.remaining() >= 16)\n    val highBits = buffer.getLong\n    val lowBits = buffer.getLong\n    new UUID(highBits, lowBits)\n  }\n\n  /**\n   * This implements Base85 using the 4 byte block aligned encoding and character set from Z85.\n   *\n   * @see https://rfc.zeromq.org/spec/32/\n   */\n  object Base85Codec {\n\n    final val ENCODE_MAP: Array[Byte] = {\n      val chars = ('0' to '9') ++ ('a' to 'z') ++ ('A' to 'Z') ++ \".-:+=^!/*?&<>()[]{}@%$#\"\n      chars.map(_.toByte).toArray\n    }\n\n    lazy val DECODE_MAP: Array[Byte] = {\n      require(ENCODE_MAP.length - 1 <= Byte.MaxValue)\n      // The bitmask is the same as largest possible value, so the length of the array must\n      // be one greater.\n      val map: Array[Byte] = Array.fill(ASCII_BITMASK + 1)(-1)\n      for ((b, i) <- ENCODE_MAP.zipWithIndex) {\n        map(b) = i.toByte\n      }\n      map\n    }\n\n    final val BASE: Long = 85L\n    final val BASE_2ND_POWER: Long = 7225L // 85^2\n    final val BASE_3RD_POWER: Long = 614125L // 85^3\n    final val BASE_4TH_POWER: Long = 52200625L // 85^4\n    final val ASCII_BITMASK: Int = 0x7F\n\n    // UUIDs always encode into 20 characters.\n    final val ENCODED_UUID_LENGTH: Int = 20\n\n    /** Encode a 16 byte UUID. */\n    def encodeUUID(id: UUID): String = {\n      val buffer = uuidToByteBuffer(id)\n      encodeBlocks(buffer)\n    }\n\n    /**\n     * Decode a 16 byte UUID. */\n    def decodeUUID(encoded: String): UUID = {\n      val buffer = decodeBlocks(encoded)\n      uuidFromByteBuffer(buffer)\n    }\n\n    /**\n     * Encode an arbitrary byte array.\n     *\n     * Unaligned input will be padded to a multiple of 4 bytes.\n     */\n    def encodeBytes(input: Array[Byte]): String = {\n      if (input.length % 4 == 0) {\n        encodeBlocks(ByteBuffer.wrap(input))\n      } else {\n        val alignedLength = ((input.length + 4) / 4) * 4\n        val buffer = ByteBuffer.allocate(alignedLength)\n        buffer.put(input)\n        while (buffer.hasRemaining) {\n          buffer.put(0.asInstanceOf[Byte])\n        }\n        buffer.rewind()\n        encodeBlocks(buffer)\n      }\n    }\n\n    /**\n     * Encode an arbitrary byte array using 4 byte blocks.\n     *\n     * Expects the input to be 4 byte aligned.\n     */\n    private def encodeBlocks(buffer: ByteBuffer): String = {\n      require(buffer.remaining() % 4 == 0)\n      val numBlocks = buffer.remaining() / 4\n      // Every 4 byte block gets encoded into 5 bytes/chars\n      val outputLength = numBlocks * 5\n      val output: Array[Byte] = Array.ofDim(outputLength)\n      var outputIndex = 0\n\n      while (buffer.hasRemaining) {\n        var sum: Long = buffer.getInt & 0x00000000ffffffffL\n        output(outputIndex) = ENCODE_MAP((sum / BASE_4TH_POWER).toInt)\n        sum %= BASE_4TH_POWER\n        output(outputIndex + 1) = ENCODE_MAP((sum / BASE_3RD_POWER).toInt)\n        sum %= BASE_3RD_POWER\n        output(outputIndex + 2) = ENCODE_MAP((sum / BASE_2ND_POWER).toInt)\n        sum %= BASE_2ND_POWER\n        output(outputIndex + 3) = ENCODE_MAP((sum / BASE).toInt)\n        output(outputIndex + 4) = ENCODE_MAP((sum % BASE).toInt)\n        outputIndex += 5\n      }\n\n      new String(output, US_ASCII)\n    }\n\n    /**\n     * Decode an arbitrary byte array.\n     *\n     * Only `outputLength` bytes will be returned.\n     * Any extra bytes, such as padding added because the input was unaligned, will be dropped.\n     */\n    def decodeBytes(encoded: String, outputLength: Int): Array[Byte] = {\n      val result = decodeBlocks(encoded)\n      if (result.remaining() > outputLength) {\n        // Only read the expected number of bytes.\n        val output: Array[Byte] = Array.ofDim(outputLength)\n        result.get(output)\n        output\n      } else {\n        result.array()\n      }\n    }\n\n    /**\n     * Decode an arbitrary byte array.\n     *\n     * Output may contain padding bytes, if the input was not 4 byte aligned.\n     * Use [[decodeBytes]] in that case and specify the expected number of output bytes\n     * without padding.\n     */\n    def decodeAlignedBytes(encoded: String): Array[Byte] = decodeBlocks(encoded).array()\n\n    /**\n     * Decode an arbitrary byte array.\n     *\n     * Output may contain padding bytes, if the input was not 4 byte aligned.\n     */\n    private def decodeBlocks(encoded: String): ByteBuffer = {\n      val input = encoded.toCharArray\n      require(input.length % 5 == 0, \"Input should be 5 character aligned.\")\n      val buffer = ByteBuffer.allocate(input.length / 5 * 4)\n\n      // A mechanism to detect invalid characters in the input while decoding, that only has a\n      // single conditional at the very end, instead of branching for every character.\n      var canary: Int = 0\n      def decodeInputChar(i: Int): Long = {\n        val c = input(i)\n        canary |= c // non-ascii char has bits outside of ASCII_BITMASK\n        val b = DECODE_MAP(c & ASCII_BITMASK)\n        canary |= b // invalid char maps to -1, which has bits outside ASCII_BITMASK\n        b.toLong\n      }\n\n      var inputIndex = 0\n      while (buffer.hasRemaining) {\n        var sum = 0L\n        sum += decodeInputChar(inputIndex) * BASE_4TH_POWER\n        sum += decodeInputChar(inputIndex + 1) * BASE_3RD_POWER\n        sum += decodeInputChar(inputIndex + 2) * BASE_2ND_POWER\n        sum += decodeInputChar(inputIndex + 3) * BASE\n        sum += decodeInputChar(inputIndex + 4)\n        buffer.putInt(sum.toInt)\n        inputIndex += 5\n      }\n      require((canary & ~ASCII_BITMASK) == 0, s\"Input is not valid Z85: $encoded\")\n      buffer.rewind()\n      buffer\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DatasetRefCache.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.concurrent.atomic.AtomicReference\n\nimport org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}\n\n/**\n * A [[Dataset]] reference cache to automatically create new [[Dataset]] objects when the active\n * [[SparkSession]] changes. This is useful when sharing objects holding [[Dataset]] references\n * cross multiple sessions. Without this, using a [[Dataset]] that holds a stale session may change\n * the active session and cause multiple issues (e.g., if we switch to a stale session coming from a\n * notebook that has been detached, we may not be able to use built-in functions because those are\n * cleaned up).\n *\n * The `creator` function will be called to create a new [[Dataset]] object when the old one has a\n * different session than the current active session. Note that one MUST use SparkSession.active\n * in the creator() if creator() needs to use Spark session.\n *\n * Unlike [[StateCache]], this class only caches the [[Dataset]] reference and doesn't cache the\n * underlying `RDD`.\n *\n * WARNING: If there are many concurrent Spark sessions and each session calls 'get' multiple times,\n *          then the cost of creator becomes more noticeable as everytime it switch the active\n *          session, the older session needs to call creator again when it becomes active.\n *\n * @param creator a function to create [[Dataset]].\n */\nclass DatasetRefCache[T] private[util](creator: () => Dataset[T]) {\n\n  private val holder = new AtomicReference[Dataset[T]]\n\n  private[delta] def invalidate() = holder.set(null)\n\n  def get: Dataset[T] = Option(holder.get())\n    .filter(_.sparkSession eq SparkSession.active)\n    .getOrElse {\n      val df = creator()\n      holder.set(df)\n      df\n    }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DateFormatter.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util\n\nimport java.time.{Instant, ZoneId}\nimport java.util.Locale\n\nimport org.apache.spark.sql.delta.util.DateTimeUtils.instantToDays\n\n/**\n * Forked from [[org.apache.spark.sql.catalyst.util.DateFormatter]]\n */\nsealed trait DateFormatter extends Serializable {\n  def parse(s: String): Int // returns days since epoch\n  def format(days: Int): String\n}\n\nclass Iso8601DateFormatter(\n    pattern: String,\n    locale: Locale) extends DateFormatter with DateTimeFormatterHelper {\n\n  @transient\n  private lazy val formatter = getOrCreateFormatter(pattern, locale)\n  private val UTC = ZoneId.of(\"UTC\")\n\n  private def toInstant(s: String): Instant = {\n    val temporalAccessor = formatter.parse(s)\n    toInstantWithZoneId(temporalAccessor, UTC)\n  }\n\n  override def parse(s: String): Int = instantToDays(toInstant(s))\n\n  override def format(days: Int): String = {\n    val instant = Instant.ofEpochSecond(days * DateTimeUtils.SECONDS_PER_DAY)\n    formatter.withZone(UTC).format(instant)\n  }\n}\n\nobject DateFormatter {\n  val defaultPattern: String = \"yyyy-MM-dd\"\n  val defaultLocale: Locale = Locale.US\n\n  def apply(format: String, locale: Locale): DateFormatter = {\n    new Iso8601DateFormatter(format, locale)\n  }\n\n  def apply(format: String): DateFormatter = apply(format, defaultLocale)\n\n  def apply(): DateFormatter = apply(defaultPattern)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DateTimeFormatterHelper.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util\n\nimport java.time._\nimport java.time.chrono.IsoChronology\nimport java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, ResolverStyle}\nimport java.time.temporal.{ChronoField, TemporalAccessor, TemporalQueries}\nimport java.util.Locale\nimport java.util.concurrent.Callable\n\nimport org.apache.spark.sql.delta.util.DateTimeFormatterHelper._\nimport com.google.common.cache.CacheBuilder\n\n/**\n * Forked from [[org.apache.spark.sql.catalyst.util.DateTimeFormatterHelper]]\n */\ntrait DateTimeFormatterHelper {\n  protected def toInstantWithZoneId(temporalAccessor: TemporalAccessor, zoneId: ZoneId): Instant = {\n    val localTime = if (temporalAccessor.query(TemporalQueries.localTime) == null) {\n      LocalTime.ofNanoOfDay(0)\n    } else {\n      LocalTime.from(temporalAccessor)\n    }\n    val localDate = LocalDate.from(temporalAccessor)\n    val localDateTime = LocalDateTime.of(localDate, localTime)\n    val zonedDateTime = ZonedDateTime.of(localDateTime, zoneId)\n    Instant.from(zonedDateTime)\n  }\n\n  // Gets a formatter from the cache or creates new one. The buildFormatter method can be called\n  // a few times with the same parameters in parallel if the cache does not contain values\n  // associated to those parameters. Since the formatter is immutable, it does not matter.\n  // In this way, synchronised is intentionally omitted in this method to make parallel calls\n  // less synchronised.\n  // The Cache.get method is not used here to avoid creation of additional instances of Callable.\n  protected def getOrCreateFormatter(pattern: String, locale: Locale): DateTimeFormatter = {\n    val key = (pattern, locale)\n    cache.get(key, new Callable[DateTimeFormatter] { def call = buildFormatter(pattern, locale) })\n  }\n}\n\nprivate object DateTimeFormatterHelper {\n  val cache = CacheBuilder.newBuilder()\n    .maximumSize(128)\n    .build[(String, Locale), DateTimeFormatter]()\n\n  def createBuilder(): DateTimeFormatterBuilder = {\n    new DateTimeFormatterBuilder().parseCaseInsensitive()\n  }\n\n  def toFormatter(builder: DateTimeFormatterBuilder, locale: Locale): DateTimeFormatter = {\n    builder\n      .parseDefaulting(ChronoField.ERA, 1)\n      .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)\n      .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)\n      .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)\n      .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)\n      .toFormatter(locale)\n      .withChronology(IsoChronology.INSTANCE)\n      .withResolverStyle(ResolverStyle.STRICT)\n  }\n\n  def buildFormatter(pattern: String, locale: Locale): DateTimeFormatter = {\n    val builder = createBuilder().appendPattern(pattern)\n    toFormatter(builder, locale)\n  }\n\n  lazy val fractionFormatter: DateTimeFormatter = {\n    val builder = createBuilder()\n      .append(DateTimeFormatter.ISO_LOCAL_DATE)\n      .appendLiteral(' ')\n      .appendValue(ChronoField.HOUR_OF_DAY, 2).appendLiteral(':')\n      .appendValue(ChronoField.MINUTE_OF_HOUR, 2).appendLiteral(':')\n      .appendValue(ChronoField.SECOND_OF_MINUTE, 2)\n      .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)\n    toFormatter(builder, TimestampFormatter.defaultLocale)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DateTimeUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util\n\nimport java.sql.Timestamp\nimport java.time._\nimport java.util.TimeZone\nimport java.util.concurrent.TimeUnit._\n\n/**\n * Forked from [[org.apache.spark.sql.catalyst.util.DateTimeUtils]].\n * Only included the methods that are used by Delta and added after Spark 2.4.\n */\n\n/**\n * Helper functions for converting between internal and external date and time representations.\n * Dates are exposed externally as java.sql.Date and are represented internally as the number of\n * dates since the Unix epoch (1970-01-01). Timestamps are exposed externally as java.sql.Timestamp\n * and are stored internally as longs, which are capable of storing timestamps with microsecond\n * precision.\n */\nobject DateTimeUtils {\n\n  // we use Int and Long internally to represent [[DateType]] and [[TimestampType]]\n  type SQLDate = Int\n  type SQLTimestamp = Long\n\n  // Pre-calculated values can provide an opportunity of additional optimizations\n  // to the compiler like constants propagation and folding.\n  final val NANOS_PER_MICROS: Long = 1000\n  final val MICROS_PER_MILLIS: Long = 1000\n  final val MILLIS_PER_SECOND: Long = 1000\n  final val SECONDS_PER_DAY: Long = 24 * 60 * 60\n  final val MICROS_PER_SECOND: Long = MILLIS_PER_SECOND * MICROS_PER_MILLIS\n  final val NANOS_PER_MILLIS: Long = NANOS_PER_MICROS * MICROS_PER_MILLIS\n  final val NANOS_PER_SECOND: Long = NANOS_PER_MICROS * MICROS_PER_SECOND\n  final val MICROS_PER_DAY: Long = SECONDS_PER_DAY * MICROS_PER_SECOND\n  final val MILLIS_PER_MINUTE: Long = 60 * MILLIS_PER_SECOND\n  final val MILLIS_PER_HOUR: Long = 60 * MILLIS_PER_MINUTE\n  final val MILLIS_PER_DAY: Long = SECONDS_PER_DAY * MILLIS_PER_SECOND\n\n  def defaultTimeZone(): TimeZone = TimeZone.getDefault\n\n  def getTimeZone(timeZoneId: String): TimeZone = {\n    val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)\n    TimeZone.getTimeZone(zoneId)\n  }\n\n  // Converts Timestamp to string according to Hive TimestampWritable convention.\n  def timestampToString(tf: TimestampFormatter, us: SQLTimestamp): String = {\n    tf.format(us)\n  }\n\n  def instantToMicros(instant: Instant): Long = {\n    val us = Math.multiplyExact(instant.getEpochSecond, MICROS_PER_SECOND)\n    val result = Math.addExact(us, NANOSECONDS.toMicros(instant.getNano))\n    result\n  }\n\n  def microsToInstant(us: Long): Instant = {\n    val secs = Math.floorDiv(us, MICROS_PER_SECOND)\n    val mos = Math.floorMod(us, MICROS_PER_SECOND)\n    Instant.ofEpochSecond(secs, mos * NANOS_PER_MICROS)\n  }\n\n  def instantToDays(instant: Instant): Int = {\n    val seconds = instant.getEpochSecond\n    val days = Math.floorDiv(seconds, SECONDS_PER_DAY)\n    days.toInt\n  }\n\n  /**\n   * Returns the number of micros since epoch from java.sql.Timestamp.\n   */\n  def fromJavaTimestamp(t: Timestamp): SQLTimestamp = {\n    if (t != null) {\n      MILLISECONDS.toMicros(t.getTime) + NANOSECONDS.toMicros(t.getNanos()) % NANOS_PER_MICROS\n    } else {\n      0L\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaCommitFileProvider.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.delta.Snapshot\nimport org.apache.spark.sql.delta.util.FileNames._\nimport org.apache.hadoop.fs.Path\n\n/**\n * Provides access to resolve Delta commit files names based on the commit-version.\n *\n * This class is part of the changes introduced to accommodate the adoption of coordinated-commits\n * in Delta Lake. Previously, certain code paths assumed the existence of delta files for a specific\n * version at a predictable path `_delta_log/$version.json`. With coordinated-commits, delta files\n * may alternatively be located at `_delta_log/_staged_commits/$version.$uuid.json`.\n * DeltaCommitFileProvider attempts to locate the correct delta files from the Snapshot's\n * LogSegment.\n *\n * @param logPath The path to the Delta table log directory.\n * @param maxVersionInclusive The maximum version of the Delta table (inclusive).\n * @param uuids A map of version numbers to their corresponding UUIDs.\n */\ncase class DeltaCommitFileProvider(\n    logPath: String,\n    maxVersionInclusive: Long,\n    uuids: Map[Long, String]) {\n  // Ensure the Path object is reused across Delta Files but not stored as part of the object state\n  // since it is not serializable.\n  @transient lazy val resolvedPath: Path = new Path(logPath)\n  lazy val minUnbackfilledVersion: Long =\n    if (uuids.keys.isEmpty) {\n      maxVersionInclusive + 1\n    } else {\n      uuids.keys.min\n    }\n\n  def deltaFile(version: Long): Path = {\n    if (version > maxVersionInclusive) {\n      throw new IllegalStateException(s\"Cannot resolve Delta table at version $version as the \" +\n        s\"state is currently at version $maxVersionInclusive. The requested version may be \" +\n        s\"incorrect or the state may be outdated. Please verify the requested version, update \" +\n        s\"the state if necessary, and try again\")\n    }\n    uuids.get(version) match {\n      case Some(uuid) => FileNames.unbackfilledDeltaFile(resolvedPath, version, Some(uuid))\n      case _ => FileNames.unsafeDeltaFile(resolvedPath, version)\n    }\n  }\n\n  /**\n   * Lists unbackfilled delta files in a sorted order without incurring additional IO operations.\n   */\n  def listSortedUnbackfilledDeltaFiles(startVersionOpt: Option[Long] = None): Seq[(Long, Path)] = {\n    val minVersion = startVersionOpt.getOrElse(minUnbackfilledVersion)\n    uuids\n      .toSeq\n      .sortBy(_._1)\n      .collect {\n        case (version, uuid) if version >= minVersion =>\n          (version, FileNames.unbackfilledDeltaFile(resolvedPath, version, Some(uuid)))\n      }\n  }\n}\n\nobject DeltaCommitFileProvider {\n  def apply(snapshot: Snapshot): DeltaCommitFileProvider = {\n    val uuids = snapshot.logSegment.deltas\n      .collect { case UnbackfilledDeltaFile(_, version, uuid) => version -> uuid }\n      .toMap\n    new DeltaCommitFileProvider(snapshot.path.toString, snapshot.version, uuids)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaEncoders.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.reflect.runtime.universe.TypeTag\n\nimport org.apache.spark.sql.delta.{DeltaHistory, DeltaHistoryManager, SerializableFileStatus, SnapshotState}\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.convert.ConvertTargetFile\nimport org.apache.spark.sql.delta.sources.IndexedFile\n\nimport org.apache.spark.sql.Encoder\nimport org.apache.spark.sql.catalyst.catalog.CatalogTypes\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\n\nprivate[delta] class DeltaEncoder[T: TypeTag] {\n  private lazy val _encoder = ExpressionEncoder[T]()\n\n  def get: Encoder[T] = {\n    _encoder.copy()\n  }\n}\n\n/**\n * Define a few `Encoder`s to reuse in Delta in order to avoid touching Scala reflection after\n * warming up. This will be mixed into `org.apache.spark.sql.delta.implicits`. Use\n * `import org.apache.spark.sql.delta.implicits._` to use these `Encoder`s.\n */\nprivate[delta] trait DeltaEncoders {\n  private lazy val _BooleanEncoder = new DeltaEncoder[Boolean]\n  implicit def booleanEncoder: Encoder[Boolean] = _BooleanEncoder.get\n\n  private lazy val _IntEncoder = new DeltaEncoder[Int]\n  implicit def intEncoder: Encoder[Int] = _IntEncoder.get\n\n  private lazy val _longEncoder = new DeltaEncoder[Long]\n  implicit def longEncoder: Encoder[Long] = _longEncoder.get\n\n  private lazy val _stringEncoder = new DeltaEncoder[String]\n  implicit def stringEncoder: Encoder[String] = _stringEncoder.get\n\n  private lazy val _longLongEncoder = new DeltaEncoder[(Long, Long)]\n  implicit def longLongEncoder: Encoder[(Long, Long)] = _longLongEncoder.get\n\n  private lazy val _stringLongEncoder = new DeltaEncoder[(String, Long)]\n  implicit def stringLongEncoder: Encoder[(String, Long)] = _stringLongEncoder.get\n\n  private lazy val _stringStringEncoder = new DeltaEncoder[(String, String)]\n  implicit def stringStringEncoder: Encoder[(String, String)] = _stringStringEncoder.get\n\n  private lazy val _javaLongEncoder = new DeltaEncoder[java.lang.Long]\n  implicit def javaLongEncoder: Encoder[java.lang.Long] = _javaLongEncoder.get\n\n  private lazy val _singleActionEncoder = new DeltaEncoder[SingleAction]\n  implicit def singleActionEncoder: Encoder[SingleAction] = _singleActionEncoder.get\n\n  private lazy val _addFileEncoder = new DeltaEncoder[AddFile]\n  implicit def addFileEncoder: Encoder[AddFile] = _addFileEncoder.get\n\n  private lazy val _removeFileEncoder = new DeltaEncoder[RemoveFile]\n  implicit def removeFileEncoder: Encoder[RemoveFile] = _removeFileEncoder.get\n\n  private lazy val _addCdcFileEncoder = new DeltaEncoder[AddCDCFile]\n  implicit def addCdcFileEncoder: Encoder[AddCDCFile] = _addCdcFileEncoder.get\n\n  private lazy val _pmtvEncoder = new DeltaEncoder[(Protocol, Metadata, Option[Long], Long)]\n  implicit def pmtvEncoder: Encoder[(Protocol, Metadata, Option[Long], Long)] = _pmtvEncoder.get\n\n  private lazy val _v2CheckpointActionsEncoder = new DeltaEncoder[(CheckpointMetadata, SidecarFile)]\n  implicit def v2CheckpointActionsEncoder: Encoder[(CheckpointMetadata, SidecarFile)] =\n    _v2CheckpointActionsEncoder.get\n\n  private lazy val _serializableFileStatusEncoder = new DeltaEncoder[SerializableFileStatus]\n  implicit def serializableFileStatusEncoder: Encoder[SerializableFileStatus] =\n    _serializableFileStatusEncoder.get\n\n  private lazy val _indexedFileEncoder = new DeltaEncoder[IndexedFile]\n  implicit def indexedFileEncoder: Encoder[IndexedFile] = _indexedFileEncoder.get\n\n  private lazy val _addFileWithIndexEncoder = new DeltaEncoder[(AddFile, Long)]\n  implicit def addFileWithIndexEncoder: Encoder[(AddFile, Long)] = _addFileWithIndexEncoder.get\n\n  private lazy val _addFileWithSourcePathEncoder = new DeltaEncoder[(AddFile, String)]\n  implicit def addFileWithSourcePathEncoder: Encoder[(AddFile, String)] =\n    _addFileWithSourcePathEncoder.get\n\n  private lazy val _deltaHistoryEncoder = new DeltaEncoder[DeltaHistory]\n  implicit def deltaHistoryEncoder: Encoder[DeltaHistory] = _deltaHistoryEncoder.get\n\n  private lazy val _historyCommitEncoder = new DeltaEncoder[DeltaHistoryManager.Commit]\n  implicit def historyCommitEncoder: Encoder[DeltaHistoryManager.Commit] = _historyCommitEncoder.get\n\n  private lazy val _snapshotStateEncoder = new DeltaEncoder[SnapshotState]\n  implicit def snapshotStateEncoder: Encoder[SnapshotState] = _snapshotStateEncoder.get\n\n  private lazy val _convertTargetFileEncoder = new DeltaEncoder[ConvertTargetFile]\n  implicit def convertTargetFileEncoder: Encoder[ConvertTargetFile] =\n    _convertTargetFileEncoder.get\n\n  private lazy val _fsPartitionSpecEncoder =\n    new DeltaEncoder[(SerializableFileStatus, CatalogTypes.TablePartitionSpec)]\n  implicit def fsPartitionSpecEncoder\n    : Encoder[(SerializableFileStatus, CatalogTypes.TablePartitionSpec)]\n      = _fsPartitionSpecEncoder.get\n\n  private lazy val _optionalHistoryCommitEncoder =\n    new DeltaEncoder[Option[DeltaHistoryManager.Commit]]\n  implicit def optionalHistoryCommitEncoder: Encoder[Option[DeltaHistoryManager.Commit]] =\n    _optionalHistoryCommitEncoder.get\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaFileOperations.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport java.io.{FileNotFoundException, IOException}\nimport java.net.URI\nimport java.util.Locale\n\nimport scala.util.Random\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Relocated._\nimport org.apache.spark.sql.delta.{DeltaErrors, SerializableFileStatus}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.storage.LogStore\nimport org.apache.commons.io.IOUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileAlreadyExistsException, FileStatus, FileSystem, FSDataInputStream, Path}\nimport org.apache.hadoop.io.IOUtils.copyBytes\nimport org.apache.parquet.format.converter.ParquetMetadataConverter.SKIP_ROW_GROUPS\nimport org.apache.parquet.hadoop.{Footer, ParquetFileReader}\n\nimport org.apache.spark.{SparkEnv, SparkException, TaskContext}\nimport org.apache.spark.broadcast.Broadcast\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.{Dataset, SparkSession}\nimport org.apache.spark.util.{SerializableConfiguration, ThreadUtils}\n\n/**\n * Some utility methods on files, directories, and paths.\n */\nobject DeltaFileOperations extends DeltaLogging {\n  /**\n   * Create an absolute path from `child` using the `basePath` if the child is a relative path.\n   * Return `child` if it is an absolute path.\n   *\n   * @param basePath Base path to prepend to `child` if child is a relative path.\n   *                 Note: It is assumed that the basePath do not have any escaped characters and\n   *                 is directly readable by Hadoop APIs.\n   * @param child    Child path to append to `basePath` if child is a relative path.\n   *                 Note: t is assumed that the child is escaped, that is, all special chars that\n   *                 need escaping by URI standards are already escaped.\n   * @return Absolute path without escaped chars that is directly readable by Hadoop APIs.\n   */\n  def absolutePath(basePath: String, child: String): Path = {\n    // scalastyle:off pathfromuri\n    val p = new Path(new URI(child))\n    if (p.isAbsolute) {\n      p\n    } else {\n      val merged = new Path(basePath, p)\n      // URI resolution strips the final `/` in `p` if it exists\n      val mergedUri = merged.toUri.toString\n      if (child.endsWith(\"/\") && !mergedUri.endsWith(\"/\")) {\n        new Path(new URI(mergedUri + \"/\"))\n      } else {\n        merged\n      }\n    }\n    // scalastyle:on pathfromuri\n  }\n\n  /**\n   * Given a path `child`:\n   *   1. Returns `child` if the path is already relative\n   *   2. Tries relativizing `child` with respect to `basePath`\n   *      a) If the `child` doesn't live within the same base path, returns `child` as is\n   *      b) If `child` lives in a different FileSystem, throws an exception\n   * Note that `child` may physically be pointing to a path within `basePath`, but may logically\n   * belong to a different FileSystem, e.g. DBFS mount points and direct S3 paths.\n   */\n  def tryRelativizePath(\n      fs: FileSystem,\n      basePath: Path,\n      child: Path,\n      ignoreError: Boolean = false): Path = {\n    // We can map multiple schemes to the same `FileSystem` class, but `FileSystem.getScheme` is\n    // usually just a hard-coded string. Hence, we need to use the scheme of the URI that we use to\n    // create the FileSystem here.\n    if (child.isAbsolute) {\n      try {\n        new Path(fs.makeQualified(basePath).toUri.relativize(fs.makeQualified(child).toUri))\n      } catch {\n        case _: IllegalArgumentException if ignoreError =>\n          // ES-85571: when the file system failed to make the child path qualified,\n          // it means the child path exists in a different file system\n          // (a different authority or schema). This usually happens when the file is coming\n          // from the across buckets or across cloud storage system shallow clone.\n          // When ignoreError being set to true, not try to relativize this path,\n          // ignore the error and just return `child` as is.\n          child\n        case e: IllegalArgumentException =>\n          logError(log\"Failed to relativize the path ${MDC(DeltaLogKeys.PATH, child)} \" +\n            log\"with the base path ${MDC(DeltaLogKeys.PATH2, basePath)} \" +\n            log\"and the file system URI ${MDC(DeltaLogKeys.URI, fs.getUri)}\", e)\n          throw DeltaErrors.failRelativizePath(child.toString)\n      }\n    } else {\n      child\n    }\n  }\n\n  /** Check if the thrown exception is a throttling error. */\n  private def isThrottlingError(t: Throwable): Boolean = {\n    Option(t.getMessage).exists(_.toLowerCase(Locale.ROOT).contains(\"slow down\"))\n  }\n\n  private def randomBackoff(\n      opName: String,\n      t: Throwable,\n      base: Int = 100,\n      jitter: Int = 1000): Unit = {\n    val sleepTime = Random.nextInt(jitter) + base\n    logWarning(log\"Sleeping for ${MDC(DeltaLogKeys.TIME_MS, sleepTime.toLong)} ms to rate limit \" +\n      log\"${MDC(DeltaLogKeys.OP_NAME, opName)}\", t)\n    Thread.sleep(sleepTime)\n  }\n\n  /** Iterate through the contents of directories.\n   *\n   * If `listAsDirectories` is enabled, then we consider each path in `subDirs` to be directories,\n   * and we list files under that path. If, for example, \"a/b\" is provided, we would attempt to\n   * list \"a/b/1.txt\", \"a/b/c/2.txt\", and so on. We would not list \"a/c\", since it's not the same\n   * directory as \"a/b\".\n   * If not, we consider that path to be a filename, and we list paths in the same directory with\n   * names after that path. So, if \"a/b\" is provided, we would list \"a/b/1.txt\", \"a/c\", \"a/d\", and\n   * so on. However a file like \"a/a.txt\" would not be listed, because lexically it appears before\n   * \"a/b\".\n   */\n  private def listUsingLogStore(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      subDirs: Iterator[String],\n      recurse: Boolean,\n      hiddenDirNameFilter: String => Boolean,\n      hiddenFileNameFilter: String => Boolean,\n      listAsDirectories: Boolean = true): Iterator[SerializableFileStatus] = {\n\n    def list(dir: String, tries: Int): Iterator[SerializableFileStatus] = {\n      logInfo(log\"Listing ${MDC(DeltaLogKeys.DIR, dir)}\")\n      try {\n        val path = if (listAsDirectories) new Path(dir, \"\\u0000\") else new Path(dir + \"\\u0000\")\n        logStore.listFrom(path, hadoopConf)\n          .filterNot{ f =>\n            val name = f.getPath.getName\n            if (f.isDirectory) hiddenDirNameFilter(name) else hiddenFileNameFilter(name)\n          }.map(SerializableFileStatus.fromStatus)\n      } catch {\n        case NonFatal(e) if isThrottlingError(e) && tries > 0 =>\n          randomBackoff(\"listing\", e)\n          list(dir, tries - 1)\n        case e: FileNotFoundException =>\n          // Can happen when multiple GCs are running concurrently or due to eventual consistency\n          Iterator.empty\n      }\n    }\n\n    val filesAndDirs = subDirs.flatMap { dir =>\n      list(dir, tries = 10)\n    }\n\n    if (recurse) {\n      recurseDirectories(\n        logStore, hadoopConf, filesAndDirs, hiddenDirNameFilter, hiddenFileNameFilter)\n    } else {\n      filesAndDirs\n    }\n  }\n\n  /** Given an iterator of files and directories, recurse directories with its contents. */\n  private def recurseDirectories(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      filesAndDirs: Iterator[SerializableFileStatus],\n      hiddenDirNameFilter: String => Boolean,\n      hiddenFileNameFilter: String => Boolean): Iterator[SerializableFileStatus] = {\n    filesAndDirs.flatMap {\n      case dir: SerializableFileStatus if dir.isDir =>\n        Iterator.single(dir) ++\n          listUsingLogStore(\n            logStore,\n            hadoopConf,\n            Iterator.single(dir.path),\n            recurse = true,\n            hiddenDirNameFilter,\n            hiddenFileNameFilter)\n      case file =>\n        Iterator.single(file)\n    }\n  }\n\n  /**\n   * The default filter for hidden files. Files names beginning with _ or . are considered hidden.\n   * @param fileName\n   * @return true if the file is hidden\n   */\n  def defaultHiddenFileFilter(fileName: String): Boolean = {\n    fileName.startsWith(\"_\") || fileName.startsWith(\".\")\n  }\n\n  /**\n   * Recursively lists all the files and directories for the given `subDirs` in a scalable manner.\n   *\n   * @param spark The SparkSession\n   * @param subDirs Absolute path of the subdirectories to list\n   * @param hadoopConf The Hadoop Configuration to get a FileSystem instance\n   * @param hiddenDirNameFilter A function that returns true when the directory should be considered\n   *                            hidden and excluded from results. Defaults to checking for prefixes\n   *                            of \".\" or \"_\".\n   * @param hiddenFileNameFilter A function that returns true when the file should be considered\n   *                             hidden and excluded from results. Defaults to checking for prefixes\n   *                             of \".\" or \"_\".\n   * @param listAsDirectories Whether to treat the paths in subDirs as directories, where all files\n   *                          that are children to the path will be listed. If false, the paths are\n   *                          treated as filenames, and files under the same folder with filenames\n   *                          after the path will be listed instead.\n   */\n  def recursiveListDirs(\n      spark: SparkSession,\n      subDirs: Seq[String],\n      hadoopConf: Broadcast[SerializableConfiguration],\n      hiddenDirNameFilter: String => Boolean = defaultHiddenFileFilter,\n      hiddenFileNameFilter: String => Boolean = defaultHiddenFileFilter,\n      fileListingParallelism: Option[Int] = None,\n      listAsDirectories: Boolean = true): Dataset[SerializableFileStatus] = {\n    import org.apache.spark.sql.delta.implicits._\n    if (subDirs.isEmpty) return spark.emptyDataset[SerializableFileStatus]\n    val listParallelism = fileListingParallelism.getOrElse(spark.sparkContext.defaultParallelism)\n    val subDirsParallelism = subDirs.length.min(spark.sparkContext.defaultParallelism)\n    val dirsAndFiles = spark.sparkContext.parallelize(\n        subDirs,\n        subDirsParallelism).mapPartitions { dirs =>\n      val logStore = LogStore(SparkEnv.get.conf, hadoopConf.value.value)\n      listUsingLogStore(\n        logStore,\n        hadoopConf.value.value,\n        dirs,\n        recurse = false,\n        hiddenDirNameFilter, hiddenFileNameFilter, listAsDirectories)\n    }.repartition(listParallelism) // Initial list of subDirs may be small\n\n    val allDirsAndFiles = dirsAndFiles.mapPartitions { firstLevelDirsAndFiles =>\n      val logStore = LogStore(SparkEnv.get.conf, hadoopConf.value.value)\n      recurseDirectories(\n        logStore,\n        hadoopConf.value.value,\n        firstLevelDirsAndFiles,\n        hiddenDirNameFilter,\n        hiddenFileNameFilter)\n    }\n    spark.createDataset(allDirsAndFiles)\n  }\n\n  /**\n   * Recursively and incrementally lists files with filenames after `listFilename` by alphabetical\n   * order. Helpful if you only want to list new files instead of the entire directory.\n   *\n   * Files located within `topDir` with filenames lexically after `listFilename` will be included,\n   * even if they may be located in parent/sibling folders of `listFilename`.\n   *\n   * @param spark The SparkSession\n   * @param listFilename Absolute path to a filename from which new files are listed (exclusive)\n   * @param topDir Absolute path to the original starting directory\n   * @param hadoopConf The Hadoop Configuration to get a FileSystem instance\n   * @param hiddenDirNameFilter A function that returns true when the directory should be considered\n   *                            hidden and excluded from results. Defaults to checking for prefixes\n   *                            of \".\" or \"_\".\n   * @param hiddenFileNameFilter A function that returns true when the file should be considered\n   *                             hidden and excluded from results. Defaults to checking for prefixes\n   *                             of \".\" or \"_\".\n   */\n  def recursiveListFrom(\n    spark: SparkSession,\n    listFilename: String,\n    topDir: String,\n    hadoopConf: Broadcast[SerializableConfiguration],\n    hiddenDirNameFilter: String => Boolean = defaultHiddenFileFilter,\n    hiddenFileNameFilter: String => Boolean = defaultHiddenFileFilter,\n    fileListingParallelism: Option[Int] = None): Dataset[SerializableFileStatus] = {\n\n    // Add folders from `listPath` to the depth before `topPath`, so as to ensure new folders/files\n    // in the parent directories are also included in the listing.\n    // If there are no new files, listing from parent directories are expected to be constant time.\n    val subDirs = getAllTopComponents(new Path(listFilename), new Path(topDir))\n\n    recursiveListDirs(spark, subDirs, hadoopConf, hiddenDirNameFilter, hiddenFileNameFilter,\n      fileListingParallelism, listAsDirectories = false)\n  }\n\n  /**\n   * Lists the directory locally using LogStore without launching a spark job. Returns an iterator\n   * from LogStore.\n   */\n  def localListDirs(\n      hadoopConf: Configuration,\n      dirs: Seq[String],\n      recursive: Boolean = true,\n      dirFilter: String => Boolean = defaultHiddenFileFilter,\n      fileFilter: String => Boolean = defaultHiddenFileFilter): Iterator[SerializableFileStatus] = {\n    val logStore = LogStore(SparkEnv.get.conf, hadoopConf)\n    listUsingLogStore(\n      logStore, hadoopConf, dirs.toIterator, recurse = recursive, dirFilter, fileFilter)\n  }\n\n  /**\n   * Incrementally lists files with filenames after `listDir` by alphabetical order. Helpful if you\n   * only want to list new files instead of the entire directory.\n   * Listed locally using LogStore without launching a spark job. Returns an iterator from LogStore.\n   */\n  def localListFrom(\n    hadoopConf: Configuration,\n    listFilename: String,\n    topDir: String,\n    recursive: Boolean = true,\n    dirFilter: String => Boolean = defaultHiddenFileFilter,\n    fileFilter: String => Boolean = defaultHiddenFileFilter): Iterator[SerializableFileStatus] = {\n    val logStore = LogStore(SparkEnv.get.conf, hadoopConf)\n    val listDirs = getAllTopComponents(new Path(listFilename), new Path(topDir))\n    listUsingLogStore(logStore, hadoopConf, listDirs.toIterator, recurse = recursive,\n      dirFilter, fileFilter, listAsDirectories = false)\n  }\n\n  /**\n   * Tries deleting a file or directory non-recursively. If the file/folder doesn't exist,\n   * that's fine, a separate operation may be deleting files/folders. If a directory is non-empty,\n   * we shouldn't delete it. FileSystem implementations throw an `IOException` in those cases,\n   * which we return as a \"we failed to delete\".\n   *\n   * Listing on S3 is not consistent after deletes, therefore in case the `delete` returns `false`,\n   * because the file didn't exist, then we still return `true`. Retries on S3 rate limits up to 3\n   * times.\n   */\n  def tryDeleteNonRecursive(fs: FileSystem, path: Path, tries: Int = 3): Boolean = {\n    try fs.delete(path, false) catch {\n      case _: FileNotFoundException => true\n      case _: IOException => false\n      case NonFatal(e) if isThrottlingError(e) && tries > 0 =>\n        randomBackoff(\"deletes\", e)\n        tryDeleteNonRecursive(fs, path, tries - 1)\n    }\n  }\n\n  /**\n   * Returns all the levels of sub directories that `path` has with respect to `base`. For example:\n   * getAllSubDirectories(\"/base\", \"/base/a/b/c\") =>\n   *   (Iterator(\"/base/a\", \"/base/a/b\"), \"/base/a/b/c\")\n   */\n  def getAllSubDirectories(base: String, path: String): (Iterator[String], String) = {\n    val baseSplits = base.split(Path.SEPARATOR)\n    val pathSplits = path.split(Path.SEPARATOR).drop(baseSplits.length)\n    val it = Iterator.tabulate(pathSplits.length - 1) { i =>\n      (baseSplits ++ pathSplits.take(i + 1)).mkString(Path.SEPARATOR)\n    }\n    (it, path)\n  }\n\n  /** Register a task failure listener to delete a temp file in our best effort. */\n  def registerTempFileDeletionTaskFailureListener(\n      conf: Configuration,\n      tempPath: Path): Unit = {\n    val tc = TaskContext.get()\n    if (tc == null) {\n      throw DeltaErrors.sparkTaskThreadNotFound\n    }\n    tc.addTaskFailureListener { (_, _) =>\n      // Best effort to delete the temp file\n      try {\n        tempPath.getFileSystem(conf).delete(tempPath, false /* = recursive */)\n      } catch {\n        case NonFatal(e) =>\n          logError(log\"Failed to delete ${MDC(DeltaLogKeys.PATH, tempPath)}\", e)\n      }\n      () // Make the compiler happy\n    }\n  }\n\n  /**\n   * Reads Parquet footers in multi-threaded manner.\n   * If the config \"spark.sql.files.ignoreCorruptFiles\" is set to true, we will ignore the corrupted\n   * files when reading footers.\n   */\n  def readParquetFootersInParallel(\n      conf: Configuration,\n      partFiles: Seq[FileStatus],\n      ignoreCorruptFiles: Boolean): Seq[Footer] = {\n    ThreadUtils.parmap(partFiles, \"readingParquetFooters\", 8) { currentFile =>\n      try {\n        // Skips row group information since we only need the schema.\n        // ParquetFileReader.readFooter throws RuntimeException, instead of IOException,\n        // when it can't read the footer.\n        Some(new Footer(currentFile.getPath(),\n          ParquetFileReader.readFooter(\n            conf, currentFile, SKIP_ROW_GROUPS)))\n      } catch { case e: RuntimeException =>\n        if (ignoreCorruptFiles) {\n          logWarning(log\"Skipped the footer in the corrupted file: \" +\n            log\"${MDC(DeltaLogKeys.FILE_STATUS, currentFile)}\", e)\n          None\n        } else {\n          throw DeltaErrors.failedReadFileFooter(currentFile.toString, e)\n        }\n      }\n    }.flatten\n  }\n\n  /**\n   * Get all parent directory paths from `listDir` until `topDir` (exclusive).\n   * For example, if `topDir` is \"/folder/\" and `currDir` is \"/folder/a/b/c\", we would return\n   * \"/folder/a/b/c\", \"/folder/a/b\" and \"/folder/a\".\n   */\n  def getAllTopComponents(listDir: Path, topDir: Path): List[String] = {\n    var ret: List[String] = List()\n    var currDir = listDir\n    while (currDir.depth() > topDir.depth()) {\n      ret = ret :+ currDir.toString\n      val parent = currDir.getParent\n      currDir = parent\n    }\n    ret\n  }\n\n  /** Expose `org.apache.spark.util.ThreadUtils.runInNewThread` to use in Delta code. */\n  def runInNewThread[T](\n      threadName: String,\n      isDaemon: Boolean = true)(body: => T): T = {\n    ThreadUtils.runInNewThread(threadName, isDaemon)(body)\n  }\n\n  /**\n   * Returns a `Dataset[AddFile]`, where all the `AddFile` actions have absolute paths. The files\n   * may have already had absolute paths, in which case they are left unchanged. Else, they are\n   * prepended with the `qualifiedSourcePath`.\n   *\n   * @param qualifiedTablePath Fully qualified path of Delta table root\n   * @param files List of `AddFile` instances\n   */\n  def makePathsAbsolute(\n      qualifiedTablePath: String,\n      files: Dataset[AddFile]): Dataset[AddFile] = {\n    import org.apache.spark.sql.delta.implicits._\n    files.mapPartitions { fileList =>\n      fileList.map { addFile =>\n        val fileSource = DeltaFileOperations.absolutePath(qualifiedTablePath, addFile.path)\n        if (addFile.deletionVector != null) {\n          val absoluteDV = addFile.deletionVector.copyWithAbsolutePath(new Path(qualifiedTablePath))\n          addFile.copy(path = fileSource.toUri.toString, deletionVector = absoluteDV)\n        } else {\n          addFile.copy(path = fileSource.toUri.toString)\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaLogGroupingIterator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.util.FileNames.{CheckpointFile, DeltaFile}\nimport org.apache.hadoop.fs.FileStatus\n\n/**\n * An iterator that groups same types of files by version.\n * Note that this class could handle only Checkpoints and Delta files.\n * For example for an input iterator:\n * - 11.checkpoint.0.1.parquet\n * - 11.checkpoint.1.1.parquet\n * - 11.json\n * - 12.checkpoint.parquet\n * - 12.json\n * - 13.json\n * - 14.json\n * - 15.checkpoint.0.1.parquet\n * - 15.checkpoint.1.1.parquet\n * - 15.checkpoint.<uuid>.parquet\n * - 15.json\n *  This will return:\n *  - (11, Seq(11.checkpoint.0.1.parquet, 11.checkpoint.1.1.parquet, 11.json))\n *  - (12, Seq(12.checkpoint.parquet, 12.json))\n *  - (13, Seq(13.json))\n *  - (14, Seq(14.json))\n *  - (15, Seq(15.checkpoint.0.1.parquet, 15.checkpoint.1.1.parquet, 15.checkpoint.<uuid>.parquet,\n *             15.json))\n */\nclass DeltaLogGroupingIterator(\n  checkpointAndDeltas: Iterator[FileStatus]) extends Iterator[(Long, ArrayBuffer[FileStatus])] {\n\n  private val bufferedIterator = checkpointAndDeltas.buffered\n\n  /**\n   * Validates that the underlying file is a checkpoint/delta file and returns the corresponding\n   * version.\n   */\n  private def getFileVersion(file: FileStatus): Long = {\n    file match {\n      case DeltaFile(_, version) => version\n      case CheckpointFile(_, version) => version\n      case _ =>\n        throw new IllegalStateException(\n          s\"${file.getPath} is not a valid commit file / checkpoint file\")\n    }\n  }\n\n  override def hasNext: Boolean = bufferedIterator.hasNext\n\n  override def next(): (Long, ArrayBuffer[FileStatus]) = {\n    val first = bufferedIterator.next()\n    val buffer = scala.collection.mutable.ArrayBuffer(first)\n    val firstFileVersion = getFileVersion(first)\n    while (bufferedIterator.headOption.exists(getFileVersion(_) == firstFileVersion)) {\n      buffer += bufferedIterator.next()\n    }\n    firstFileVersion -> buffer\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaProgressReporter.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\n\nimport org.apache.spark.SparkContext\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.sql.SparkSession\n\ntrait DeltaProgressReporter extends Logging {\n  /**\n   * Report a log to indicate some command is running.\n   */\n  def withStatusCode[T](\n      statusCode: String,\n      defaultMessage: String,\n      data: Map[String, Any] = Map.empty)(body: => T): T = {\n    logInfo(log\"${MDC(DeltaLogKeys.STATUS, statusCode)}: \" +\n      log\"${MDC(DeltaLogKeys.STATUS_MESSAGE, defaultMessage)}\")\n    val t = withJobDescription(defaultMessage)(body)\n    logInfo(log\"${MDC(DeltaLogKeys.STATUS, statusCode)}: Done\")\n    t\n  }\n  /**\n   * Wrap various delta operations to provide a more meaningful name in Spark UI\n   * This only has an effect if {{{body}}} actually runs a Spark job\n   * @param jobDesc a short description of the operation\n   */\n  private def withJobDescription[U](jobDesc: String)(body: => U): U = {\n    val sc = SparkSession.active.sparkContext\n    // will prefix jobDesc with whatever the user specified in the job description\n    // of the higher level operation that triggered this delta operation\n    val oldDesc = sc.getLocalProperty(SparkContext.SPARK_JOB_DESCRIPTION)\n    val suffix = if (oldDesc == null) {\n      \"\"\n    } else {\n      s\" $oldDesc:\"\n    }\n    try {\n      sc.setJobDescription(s\"Delta:$suffix $jobDesc\")\n      body\n    } finally {\n      sc.setJobDescription(oldDesc)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaSparkPlanUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.delta.{DeltaTable, DeltaTableReadPredicate}\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.expressions.{Exists, Expression, InSubquery, LateralSubquery, ScalarSubquery, SubqueryExpression => SparkSubqueryExpression, UserDefinedExpression}\nimport org.apache.spark.sql.catalyst.plans.logical.{Distinct, Filter, LeafNode, LogicalPlan, OneRowRelation, Project, SubqueryAlias, Union}\nimport org.apache.spark.sql.execution.columnar.InMemoryRelation\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\n\n\ntrait DeltaSparkPlanUtils {\n  import DeltaSparkPlanUtils._\n\n  protected def planContainsOnlyDeltaScans(source: LogicalPlan): Boolean =\n    findFirstNonDeltaScan(source).isEmpty\n\n  protected def findFirstNonDeltaScan(source: LogicalPlan): Option[LogicalPlan] = {\n    (source match {\n      case l: LogicalRelation =>\n        l match {\n          case DeltaTable(_) => None\n          case _ => Some(l)\n        }\n      case OneRowRelation() => None\n      case leaf: LeafNode => Some(leaf) // Any other LeafNode is a non Delta scan.\n      case node => collectFirst(node.children, findFirstNonDeltaScan)\n    }).orElse(\n      // If not found in main plan, look into subqueries.\n      collectFirst(source.subqueries, findFirstNonDeltaScan)\n    )\n  }\n\n  /** Returns whether part of the plan was cached using df.cache() or similar. */\n  protected def planContainsCachedRelation(df: DataFrame): Boolean =\n    df.queryExecution.withCachedData.exists(_.isInstanceOf[InMemoryRelation])\n\n  /**\n   * Returns `true` if `plan` has a safe level of determinism. This is a conservative\n   * approximation of `plan` being a truly deterministic query.\n   *\n   */\n  protected def planIsDeterministic(\n      plan: LogicalPlan,\n      checkDeterministicOptions: CheckDeterministicOptions): Boolean =\n    findFirstNonDeterministicNode(plan, checkDeterministicOptions).isEmpty\n\n  type PlanOrExpression = Either[LogicalPlan, Expression]\n\n  /**\n   * Returns a part of the `plan` that does not have a safe level of determinism.\n   * This is a conservative approximation of `plan` being a truly deterministic query.\n   */\n  protected def findFirstNonDeterministicNode(\n      plan: LogicalPlan,\n      checkDeterministicOptions: CheckDeterministicOptions): Option[PlanOrExpression] = {\n    plan match {\n      // This is very restrictive, allowing only deterministic filters and projections directly\n      // on top of a Delta Table.\n      case Distinct(child) => findFirstNonDeterministicNode(child, checkDeterministicOptions)\n      case Project(projectList, child) =>\n        findFirstNonDeterministicChildNode(projectList, checkDeterministicOptions) orElse {\n            findFirstNonDeterministicNode(child, checkDeterministicOptions)\n        }\n      case Filter(cond, child) =>\n        findFirstNonDeterministicNode(cond, checkDeterministicOptions) orElse {\n          findFirstNonDeterministicNode(child, checkDeterministicOptions)\n        }\n      case Union(children, _, _) => collectFirst[LogicalPlan, PlanOrExpression](\n        children,\n        c => findFirstNonDeterministicNode(c, checkDeterministicOptions))\n      case SubqueryAlias(_, child) =>\n        findFirstNonDeterministicNode(child, checkDeterministicOptions)\n      case DeltaTable(_) => None\n      case OneRowRelation() => None\n      case node => Some(Left(node))\n    }\n  }\n\n  protected def planContainsUdf(plan: LogicalPlan): Boolean = {\n    plan.collectWithSubqueries {\n      case node if node.expressions.exists(_.exists(_.isInstanceOf[UserDefinedExpression])) => ()\n    }.nonEmpty\n  }\n\n  protected def findFirstNonDeterministicChildNode(\n      children: Seq[Expression],\n      checkDeterministicOptions: CheckDeterministicOptions): Option[PlanOrExpression] =\n    collectFirst[Expression, PlanOrExpression](\n      children,\n      c => findFirstNonDeterministicNode(c, checkDeterministicOptions))\n\n  protected def findFirstNonDeterministicNode(\n      child: Expression,\n      checkDeterministicOptions: CheckDeterministicOptions): Option[PlanOrExpression] = {\n    child match {\n      case SubqueryExpression(plan) =>\n        if (SparkSubqueryExpression.hasCorrelatedSubquery(child)) {\n          // We consider joins potentially non-deterministic, and correlated subqueries are\n          // flattened into joins, so they should also be considered potentially non-deterministic.\n          Some(Right(child))\n        } else {\n          findFirstNonDeterministicNode(plan, checkDeterministicOptions)\n        }\n      case _: UserDefinedExpression if !checkDeterministicOptions.allowDeterministicUdf =>\n        Some(Right(child))\n      case p =>\n        collectFirst[Expression, PlanOrExpression](\n          p.children,\n          c => findFirstNonDeterministicNode(c, checkDeterministicOptions)) orElse {\n          if (p.deterministic) None else Some(Right(p))\n        }\n    }\n  }\n\n  protected def collectFirst[In, Out](\n      input: Iterable[In],\n      recurse: In => Option[Out]): Option[Out] = {\n    input.foldLeft(Option.empty[Out]) { case (acc, value) =>\n      acc.orElse(recurse(value))\n    }\n  }\n\n  /** Extractor object for the subquery plan of expressions that contain subqueries. */\n  object SubqueryExpression {\n    def unapply(expr: Expression): Option[LogicalPlan] = expr match {\n      case subquery: ScalarSubquery => Some(subquery.plan)\n      case exists: Exists => Some(exists.plan)\n      case subquery: InSubquery => Some(subquery.query.plan)\n      case subquery: LateralSubquery => Some(subquery.plan)\n      case _ => None\n    }\n  }\n\n  /** Returns whether the read predicates of a transaction contain any deterministic UDFs. */\n  def containsDeterministicUDF(\n      predicates: Seq[DeltaTableReadPredicate], partitionedOnly: Boolean): Boolean = {\n    if (partitionedOnly) {\n      predicates.exists {\n        _.partitionPredicates.exists(containsDeterministicUDF)\n      }\n    } else {\n      predicates.exists { p =>\n        p.dataPredicates.exists(containsDeterministicUDF) ||\n          p.partitionPredicates.exists(containsDeterministicUDF)\n      }\n    }\n  }\n\n  /** Returns whether an expression contains any deterministic UDFs. */\n  def containsDeterministicUDF(expr: Expression): Boolean = expr.exists {\n    case udf: UserDefinedExpression => udf.deterministic\n    case _ => false\n  }\n}\n\n\nobject DeltaSparkPlanUtils {\n  /**\n   * Options for deciding whether plans contain non-deterministic nodes and expressions.\n   *\n   * @param allowDeterministicUdf If true, allow UDFs that are marked by users as deterministic.\n   *                              If false, always treat them as non-deterministic to be more\n   *                              defensive against user bugs.\n   */\n  case class CheckDeterministicOptions(\n    allowDeterministicUdf: Boolean\n  )\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaSqlParserUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.parser.{AbstractSqlParser, AstBuilder, ParseException, ParserUtils, SqlBaseParser}\n\n/**\n * Utility functions for SQL parsing operations.\n */\nobject DeltaSqlParserUtils {\n  /**\n   * The SQL grammar already includes a `multipartIdentifierList` rule for parsing a string into a\n   * list of multi-part identifiers. We just expose it here, with a custom parser and AstBuilder.\n   */\n  private class MultipartIdentifierSqlParser extends AbstractSqlParser {\n    override val astBuilder = new AstBuilder {\n      override def visitMultipartIdentifierList(ctx: SqlBaseParser.MultipartIdentifierListContext)\n      : Seq[UnresolvedAttribute] = ParserUtils.withOrigin(ctx) {\n        ctx.multipartIdentifier.asScala.toSeq.map(typedVisit[Seq[String]])\n          .map(new UnresolvedAttribute(_))\n      }\n    }\n    def parseMultipartIdentifierList(sqlText: String): Seq[UnresolvedAttribute] = {\n      parse(sqlText) { parser =>\n        astBuilder.visitMultipartIdentifierList(parser.multipartIdentifierList())\n      }\n    }\n  }\n\n  private val multipartIdentifierSqlParser = new MultipartIdentifierSqlParser\n\n  /** Parses a comma-separated list of column names; returns None if parsing fails. */\n  def parseMultipartColumnList(columns: String): Option[Seq[UnresolvedAttribute]] = {\n    // The parser rejects empty lists, so handle that specially here.\n    if (columns.trim.isEmpty) return Some(Nil)\n    try {\n      Some(multipartIdentifierSqlParser.parseMultipartIdentifierList(columns))\n    } catch {\n      case _: ParseException => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/DeltaStatsJsonUtils.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.delta.shims.VariantStatsShims\nimport org.apache.spark.sql.delta.util.Codec.Base85Codec\nimport org.apache.spark.types.variant.{Variant, VariantUtil}\nimport org.apache.spark.unsafe.types.VariantVal\n\n/**\n * Utility functions for encoding/decoding Variant values as Z85 strings.\n * This is used for storing variant statistics in Delta checkpoints and stats.\n */\nobject DeltaStatsJsonUtils {\n\n  /**\n   * Encode a Variant as a Z85 string.\n   * The variant binary format stores metadata followed by value bytes.\n   * This concatenates them and encodes as Z85.\n   */\n  def encodeVariantAsZ85(v: Variant): String = {\n    val metadata = v.getMetadata\n    val value = v.getValue\n\n    val combined = new Array[Byte](metadata.length + value.length)\n    System.arraycopy(metadata, 0, combined, 0, metadata.length)\n    System.arraycopy(value, 0, combined, metadata.length, value.length)\n\n    Base85Codec.encodeBytes(combined)\n  }\n\n  /**\n   * Decode a Z85-encoded string back to a VariantVal.\n   */\n  def decodeVariantFromZ85(z85: String): VariantVal = {\n    val decoded = Base85Codec.decodeBytes(z85, z85.length)\n    val metadataSize = VariantStatsShims.metadataSize(decoded)\n    val valueWithPadding = decoded.slice(metadataSize, decoded.length)\n    val valueSize = VariantUtil.valueSize(valueWithPadding, 0)\n    val value = valueWithPadding.slice(0, valueSize)\n    val metadata = decoded.slice(0, metadataSize)\n    new VariantVal(value, metadata)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/FileNames.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\n/** Helper for creating file names for specific commits / checkpoints. */\nobject FileNames {\n\n  val deltaFileRegex = raw\"(\\d+)\\.json\".r\n  val uuidDeltaFileRegex = raw\"(\\d+)\\.([^.]+)\\.json\".r\n  val compactedDeltaFileRegex = raw\"(\\d+).(\\d+).compacted.json\".r\n  val checksumFileRegex = raw\"(\\d+)\\.crc\".r\n  val checkpointFileRegex = raw\"(\\d+)\\.checkpoint((\\.\\d+\\.\\d+)?\\.parquet|\\.[^.]+\\.(json|parquet))\".r\n\n  private val compactedDeltaFilePattern = compactedDeltaFileRegex.pattern\n  private val checksumFilePattern = checksumFileRegex.pattern\n  private val checkpointFilePattern = checkpointFileRegex.pattern\n\n  /**\n   * Returns the delta (json format) path for a given delta file.\n   * WARNING: This API is unsafe and can resolve to incorrect paths if the table has\n   * Coordinated Commits.\n   * Use DeltaCommitFileProvider(snapshot).deltaFile instead to guarantee accurate paths.\n   */\n  def unsafeDeltaFile(path: Path, version: Long): Path = new Path(path, f\"$version%020d.json\")\n\n  /**\n   * Returns the un-backfilled uuid formatted delta (json format) path for a given version.\n   *\n   * @param logPath The root path of the delta log.\n   * @param version The version of the delta file.\n   * @return The path to the un-backfilled delta file:\n   *         `<logPath>/_staged_commits/<version>.<uuid>.json`\n   */\n  def unbackfilledDeltaFile(\n      logPath: Path,\n      version: Long,\n      uuidString: Option[String] = None): Path = {\n    val basePath = commitDirPath(logPath)\n    val uuid = uuidString.getOrElse(UUID.randomUUID.toString)\n    new Path(basePath, f\"$version%020d.$uuid.json\")\n  }\n\n  /** Returns the path for a given sample file */\n  def sampleFile(path: Path, version: Long): Path = new Path(path, f\"$version%020d\")\n\n  /** Returns the path to the checksum file for the given version. */\n  def checksumFile(path: Path, version: Long): Path = new Path(path, f\"$version%020d.crc\")\n\n  /** Returns the path to the compacted delta file for the given version range. */\n  def compactedDeltaFile(\n      path: Path,\n      fromVersion: Long,\n      toVersion: Long): Path = {\n    new Path(path, f\"$fromVersion%020d.$toVersion%020d.compacted.json\")\n  }\n\n  /** Returns the version for the given delta path. */\n  def deltaVersion(path: Path): Long = path.getName.split(\"\\\\.\")(0).toLong\n  def deltaVersion(file: FileStatus): Long = deltaVersion(file.getPath)\n\n  /** Returns the version for the given checksum file. */\n  def checksumVersion(path: Path): Long = path.getName.stripSuffix(\".crc\").toLong\n  def checksumVersion(file: FileStatus): Long = checksumVersion(file.getPath)\n\n  def compactedDeltaVersions(path: Path): (Long, Long) = {\n    val parts = path.getName.split(\"\\\\.\")\n    (parts(0).toLong, parts(1).toLong)\n  }\n  def compactedDeltaVersions(file: FileStatus): (Long, Long) = compactedDeltaVersions(file.getPath)\n\n  /**\n   * Returns the prefix of all delta log files for the given version.\n   *\n   * Intended for use with listFrom to get all files from this version onwards. The returned Path\n   * will not exist as a file.\n   */\n  def listingPrefix(path: Path, version: Long): Path = new Path(path, f\"$version%020d.\")\n\n  /**\n   * Returns the path for a singular checkpoint up to the given version.\n   *\n   * In a future protocol version this path will stop being written.\n   */\n  def checkpointFileSingular(path: Path, version: Long): Path =\n    new Path(path, f\"$version%020d.checkpoint.parquet\")\n\n  /**\n   * Returns the paths for all parts of the checkpoint up to the given version.\n   *\n   * In a future protocol version we will write this path instead of checkpointFileSingular.\n   *\n   * Example of the format: 00000000000000004915.checkpoint.0000000020.0000000060.parquet is\n   * checkpoint part 20 out of 60 for the snapshot at version 4915. Zero padding is for\n   * lexicographic sorting.\n   */\n  def checkpointFileWithParts(path: Path, version: Long, numParts: Int): Seq[Path] = {\n    Range(1, numParts + 1)\n      .map(i => new Path(path, f\"$version%020d.checkpoint.$i%010d.$numParts%010d.parquet\"))\n  }\n\n  def numCheckpointParts(path: Path): Option[Int] = {\n    val segments = path.getName.split(\"\\\\.\")\n\n    if (segments.size != 5) None else Some(segments(3).toInt)\n  }\n\n  def isCheckpointFile(path: Path): Boolean = checkpointFilePattern.matcher(path.getName).matches()\n  def isCheckpointFile(file: FileStatus): Boolean = isCheckpointFile(file.getPath)\n\n  def isDeltaFile(path: Path): Boolean = DeltaFile.unapply(path).isDefined\n  def isDeltaFile(file: FileStatus): Boolean = isDeltaFile(file.getPath)\n\n  def isUnbackfilledDeltaFile(path: Path): Boolean = UnbackfilledDeltaFile.unapply(path).isDefined\n  def isUnbackfilledDeltaFile(file: FileStatus): Boolean = isUnbackfilledDeltaFile(file.getPath)\n\n  def isBackfilledDeltaFile(path: Path): Boolean = BackfilledDeltaFile.unapply(path).isDefined\n  def isBackfilledDeltaFile(file: FileStatus): Boolean = isBackfilledDeltaFile(file.getPath)\n\n  def isChecksumFile(path: Path): Boolean = checksumFilePattern.matcher(path.getName).matches()\n  def isChecksumFile(file: FileStatus): Boolean = isChecksumFile(file.getPath)\n\n  def isCompactedDeltaFile(path: Path): Boolean =\n    compactedDeltaFilePattern.matcher(path.getName).matches()\n  def isCompactedDeltaFile(file: FileStatus): Boolean = isCompactedDeltaFile(file.getPath)\n\n  def checkpointVersion(path: Path): Long = path.getName.split(\"\\\\.\")(0).toLong\n  def checkpointVersion(file: FileStatus): Long = checkpointVersion(file.getPath)\n\n  object CompactedDeltaFile {\n    def unapply(f: FileStatus): Option[(FileStatus, Long, Long)] =\n      unapply(f.getPath).map { case (_, startVersion, endVersion) => (f, startVersion, endVersion) }\n    def unapply(path: Path): Option[(Path, Long, Long)] = path.getName match {\n      case compactedDeltaFileRegex(lo, hi) => Some(path, lo.toLong, hi.toLong)\n      case _ => None\n    }\n  }\n\n\n  /**\n   * Get the version of the checkpoint, checksum or delta file. Returns None if an unexpected\n   * file type is seen.\n   */\n  def getFileVersionOpt(path: Path): Option[Long] = path match {\n    case DeltaFile(_, version) => Some(version)\n    case ChecksumFile(_, version) => Some(version)\n    case CheckpointFile(_, version) => Some(version)\n    case CompactedDeltaFile(_, _, endVersion) => Some(endVersion)\n    case _ => None\n  }\n\n  /**\n   * Get the version of the checkpoint, checksum or delta file. Throws an error if an unexpected\n   * file type is seen. These unexpected files should be filtered out to ensure forward\n   * compatibility in cases where new file types are added, but without an explicit protocol\n   * upgrade.\n   */\n  def getFileVersion(path: Path): Long = {\n    getFileVersionOpt(path).getOrElse {\n      // scalastyle:off throwerror\n      throw new AssertionError(\n        s\"Unexpected file type found in transaction log: $path\")\n      // scalastyle:on throwerror\n    }\n  }\n  def getFileVersion(file: FileStatus): Long = getFileVersion(file.getPath)\n\n  object DeltaFile {\n    def unapply(f: FileStatus): Option[(FileStatus, Long)] =\n      unapply(f.getPath).map { case (_, version) => (f, version) }\n    def unapply(path: Path): Option[(Path, Long)] = {\n      val parentDirName = path.getParent.getName\n      // If parent is `_staged_commits` dir, then match against unbackfilled commit file.\n      val regex = if (parentDirName == COMMIT_SUBDIR) uuidDeltaFileRegex else deltaFileRegex\n      regex.unapplySeq(path.getName).map(path -> _.head.toLong)\n    }\n  }\n  object ChecksumFile {\n    def unapply(f: FileStatus): Option[(FileStatus, Long)] =\n      unapply(f.getPath).map { case (_, version) => (f, version) }\n    def unapply(path: Path): Option[(Path, Long)] =\n      checksumFileRegex.unapplySeq(path.getName).map(path -> _.head.toLong)\n  }\n  object CheckpointFile {\n    def unapply(f: FileStatus): Option[(FileStatus, Long)] =\n      unapply(f.getPath).map { case (_, version) => (f, version) }\n    def unapply(path: Path): Option[(Path, Long)] = {\n      checkpointFileRegex.unapplySeq(path.getName).map(path -> _.head.toLong)\n    }\n  }\n  object BackfilledDeltaFile {\n    def unapply(f: FileStatus): Option[(FileStatus, Long)] =\n      unapply(f.getPath).map { case (_, version) => (f, version) }\n    def unapply(path: Path): Option[(Path, Long)] = {\n      // Don't match files in the `_staged_commits` subdirectory.\n      if (path.getParent.getName == COMMIT_SUBDIR) {\n        None\n      } else {\n        deltaFileRegex\n          .unapplySeq(path.getName)\n          .map(path -> _.head.toLong)\n      }\n    }\n  }\n  object UnbackfilledDeltaFile {\n    def unapply(f: FileStatus): Option[(FileStatus, Long, String)] =\n      unapply(f.getPath).map { case (_, version, uuidString) => (f, version, uuidString) }\n    def unapply(path: Path): Option[(Path, Long, String)] = {\n      // If parent is `_staged_commits` dir, then match against uuid commit file.\n      if (path.getParent.getName == COMMIT_SUBDIR) {\n        uuidDeltaFileRegex\n          .unapplySeq(path.getName)\n          .collect { case Seq(version, uuidString) => (path, version.toLong, uuidString) }\n      } else {\n        None\n      }\n    }\n  }\n\n  object FileType extends Enumeration {\n    val DELTA, CHECKPOINT, CHECKSUM, COMPACTED_DELTA, OTHER = Value\n  }\n\n  /** File path for a new V2 Checkpoint Json file */\n  def newV2CheckpointJsonFile(path: Path, version: Long): Path =\n    new Path(path, f\"$version%020d.checkpoint.${UUID.randomUUID.toString}.json\")\n\n  /** File path for a new V2 Checkpoint Parquet file */\n  def newV2CheckpointParquetFile(path: Path, version: Long): Path =\n    new Path(path, f\"$version%020d.checkpoint.${UUID.randomUUID.toString}.parquet\")\n\n  /** File path for a V2 Checkpoint's Sidecar file */\n  def newV2CheckpointSidecarFile(\n      logPath: Path,\n      version: Long,\n      numParts: Int,\n      currentPart: Int): Path = {\n    val basePath = sidecarDirPath(logPath)\n    val uuid = UUID.randomUUID.toString\n    new Path(basePath, f\"$version%020d.checkpoint.$currentPart%010d.$numParts%010d.$uuid.parquet\")\n  }\n\n  val SIDECAR_SUBDIR = \"_sidecars\"\n\n  /** Returns path to the sidecar directory */\n  def sidecarDirPath(logPath: Path): Path = new Path(logPath, SIDECAR_SUBDIR)\n\n  val COMMIT_SUBDIR = \"_staged_commits\"\n\n  /** Returns path to the staged commit directory */\n  def commitDirPath(logPath: Path): Path = new Path(logPath, COMMIT_SUBDIR)\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/InCommitTimestampUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Action, CommitInfo, Metadata}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.sql.SparkSession\n\nobject InCommitTimestampUtils {\n\n  final val TABLE_PROPERTY_CONFS = Seq(\n    DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED,\n    DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION,\n    DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP)\n\n  final val TABLE_PROPERTY_KEYS: Seq[String] = TABLE_PROPERTY_CONFS.map(_.key)\n\n  /** Returns true if the current transaction implicitly/explicitly enables ICT. */\n  def didCurrentTransactionEnableICT(\n      currentTransactionMetadata: Metadata,\n      readSnapshot: Snapshot): Boolean = {\n    // If ICT is currently enabled, and the read snapshot did not have ICT enabled,\n    // then the current transaction must have enabled it.\n    // In case of a conflict, any winning transaction that enabled it after\n    // our read snapshot would have caused a metadata conflict abort\n    // (see [[ConflictChecker.checkNoMetadataUpdates]]), so we know that\n    // all winning transactions' ICT enablement status must match the read snapshot.\n    //\n    // WARNING: The Metadata() of InitialSnapshot can enable ICT by default. To ensure that\n    // this function returns true if ICT is enabled during the first commit, we explicitly handle\n    // the case where the readSnapshot.version is -1.\n    val isICTCurrentlyEnabled =\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(currentTransactionMetadata)\n    val wasICTEnabledInReadSnapshot = readSnapshot.version != -1 &&\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(readSnapshot.metadata)\n    isICTCurrentlyEnabled && !wasICTEnabledInReadSnapshot\n  }\n\n  /**\n   * Returns the updated [[Metadata]] with inCommitTimestamp enablement related info\n   * (version and timestamp) correctly set.\n   * This enablement info will be set to the current commit's timestamp and version if:\n   * 1. If this transaction enables inCommitTimestamp.\n   * 2. If the commit version is not 0. This is because we only need to persist\n   *  the enablement info if there are non-ICT commits in the Delta log.\n   * For cases where ICT is enabled in both the current transaction and the read snapshot,\n   * we will retain the enablement info from the read snapshot. Note that this can\n   * happen for commands like REPLACE or CLONE, where we can end up dropping the enablement\n   * info due to the belief that ICT was just enabled.\n   * Note: This function must only be called after transaction conflicts have been resolved.\n   */\n  def getUpdatedMetadataWithICTEnablementInfo(\n      spark: SparkSession,\n      inCommitTimestamp: Long,\n      readSnapshot: Snapshot,\n      metadata: Metadata,\n      commitVersion: Long): Option[Metadata] = {\n    if (didCurrentTransactionEnableICT(metadata, readSnapshot) && commitVersion != 0) {\n      val enablementTrackingProperties = Map(\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key -> commitVersion.toString,\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.key -> inCommitTimestamp.toString)\n      Some(metadata.copy(configuration = metadata.configuration ++ enablementTrackingProperties))\n    } else if (DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(metadata) &&\n        !didCurrentTransactionEnableICT(metadata, readSnapshot) &&\n        // This check ensures that we don't make an unnecessary metadata update\n        // even when ICT enablement properties are not being dropped.\n        getValidatedICTEnablementInfo(readSnapshot.metadata).isDefined &&\n        getValidatedICTEnablementInfo(metadata).isEmpty &&\n        spark.conf.get(DeltaSQLConf.IN_COMMIT_TIMESTAMP_RETAIN_ENABLEMENT_INFO_FIX_ENABLED)\n    ) {\n      // If ICT was enabled in the readSnapshot and is still enabled, we should\n      // retain the enablement info from the read snapshot.\n      // This prevents enablement info from being dropped during REPLACE/CLONE.\n      val existingICTConfigs = readSnapshot.metadata.configuration\n        .filter { case (k, _) => TABLE_PROPERTY_KEYS.contains(k) }\n      Some(metadata.copy(configuration = metadata.configuration ++ existingICTConfigs))\n    } else {\n      None\n    }\n  }\n\n  def getValidatedICTEnablementInfo(metadata: Metadata): Option[DeltaHistoryManager.Commit] = {\n    val enablementTimestampOpt =\n      DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(metadata)\n    val enablementVersionOpt =\n      DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(metadata)\n    (enablementTimestampOpt, enablementVersionOpt) match {\n      case (Some(enablementTimestamp), Some(enablementVersion)) =>\n        Some(DeltaHistoryManager.Commit(enablementVersion, enablementTimestamp))\n      case (None, None) =>\n        None\n      case _ =>\n        throw new IllegalStateException(\n          \"Both enablement version and timestamp should be present or absent together.\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/JsonUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.core.StreamReadConstraints\nimport com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}\nimport com.fasterxml.jackson.module.scala.{DefaultScalaModule, ScalaObjectMapper}\n\n/** Useful json functions used around the Delta codebase. */\nobject JsonUtils {\n  /** Used to convert between classes and JSON. */\n  lazy val mapper = {\n    val _mapper = new ObjectMapper with ScalaObjectMapper\n    _mapper.setSerializationInclusion(Include.NON_ABSENT)\n    _mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n    _mapper.registerModule(DefaultScalaModule)\n\n    // We do not want to limit the length of JSON strings in the Delta log or table data. Also note\n    // that not having a limit was the default behavior before Jackson 2.15.\n    val streamReadConstraints = StreamReadConstraints\n      .builder()\n      .maxStringLength(Int.MaxValue)\n      .build()\n    _mapper.getFactory.setStreamReadConstraints(streamReadConstraints)\n\n    _mapper\n  }\n\n  def toJson[T: Manifest](obj: T): String = {\n    mapper.writeValueAsString(obj)\n  }\n\n  def toPrettyJson[T: Manifest](obj: T): String = {\n    mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)\n  }\n\n  def fromJson[T: Manifest](json: String): T = {\n    mapper.readValue[T](json)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/PartitionUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport java.lang.{Double => JDouble, Long => JLong}\nimport java.math.{BigDecimal => JBigDecimal}\nimport java.time.ZoneId\nimport java.util.{Locale, TimeZone}\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.Try\n\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, DeltaErrors}\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.unsafe.types.UTF8String\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.analysis._\nimport org.apache.spark.sql.catalyst.catalog.CatalogTypes.TablePartitionSpec\nimport org.apache.spark.sql.catalyst.expressions.{Attribute, Cast, Literal}\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils\nimport org.apache.spark.sql.catalyst.util.CaseInsensitiveMap\nimport org.apache.spark.sql.types._\n\n/**\n * This file is forked from [[org.apache.spark.sql.execution.datasources.PartitioningUtils]].\n */\n\n\n// In open-source Apache Spark, PartitionPath is defined as\n//\n//  case class PartitionPath(values: InternalRow, path: Path)\n//\n// but in Databricks we use a different representation where the Path is stored as a String\n// and converted back to a Path only when read. This significantly cuts memory consumption because\n// Hadoop Path objects are heavyweight. See SC-7591 for details.\nobject PartitionPath {\n  // Used only in tests:\n  def apply(values: InternalRow, path: String): PartitionPath = {\n    // Roundtrip through `new Path` to ensure any normalization done there is applied:\n    apply(values, new Path(path))\n  }\n\n  def apply(values: InternalRow, path: Path): PartitionPath = {\n    new PartitionPath(values, path.toString)\n  }\n}\n\n/**\n * Holds a directory in a partitioned collection of files as well as the partition values\n * in the form of a Row.  Before scanning, the files at `path` need to be enumerated.\n */\nclass PartitionPath private (val values: InternalRow, val pathStr: String) {\n  // Note: this isn't a case class because we don't want to have a public apply() method which\n  // accepts a string. The goal is to force every value stored in `pathStr` to have gone through\n  // a `new Path(...).toString` to ensure that canonicalization / normalization has taken place.\n  def path: Path = new Path(pathStr)\n  def withNewValues(newValues: InternalRow): PartitionPath = {\n    new PartitionPath(newValues, pathStr)\n  }\n  override def equals(other: Any): Boolean = other match {\n    case that: PartitionPath => values == that.values && pathStr == that.pathStr\n    case _ => false\n  }\n  override def hashCode(): Int = {\n    (values, pathStr).hashCode()\n  }\n  override def toString: String = {\n    s\"PartitionPath($values, $pathStr)\"\n  }\n}\n\ncase class PartitionSpec(\n    partitionColumns: StructType,\n    partitions: Seq[PartitionPath])\n\nobject PartitionSpec {\n  val emptySpec = PartitionSpec(StructType(Seq.empty[StructField]), Seq.empty[PartitionPath])\n}\n\nprivate[delta] object PartitionUtils {\n\n  lazy val timestampPartitionPattern = s\"yyyy-MM-dd HH:mm:ss${precisionMatchPatterns(6)}\"\n  lazy val utcFormatter = TimestampFormatter(s\"yyyy-MM-dd'T'HH:mm:ss.SSSSSSz\", ZoneId.of(\"Z\"))\n\n  private def precisionMatchPatterns(maxDigits: Int): String =\n    (maxDigits to 1 by -1)\n      .map(n => \"[.\" + (\"S\" * n) + \"]\")\n      .mkString\n\n  case class PartitionValues(columnNames: Seq[String], literals: Seq[Literal])\n  {\n    require(columnNames.size == literals.size)\n  }\n\n  import org.apache.spark.sql.catalyst.catalog.ExternalCatalogUtils.{escapePathName, unescapePathName, DEFAULT_PARTITION_NAME}\n\n  /**\n   * Given a group of qualified paths, tries to parse them and returns a partition specification.\n   * For example, given:\n   * {{{\n   *   hdfs://<host>:<port>/path/to/partition/a=1/b=hello/c=3.14\n   *   hdfs://<host>:<port>/path/to/partition/a=2/b=world/c=6.28\n   * }}}\n   * it returns:\n   * {{{\n   *   PartitionSpec(\n   *     partitionColumns = StructType(\n   *       StructField(name = \"a\", dataType = IntegerType, nullable = true),\n   *       StructField(name = \"b\", dataType = StringType, nullable = true),\n   *       StructField(name = \"c\", dataType = DoubleType, nullable = true)),\n   *     partitions = Seq(\n   *       Partition(\n   *         values = Row(1, \"hello\", 3.14),\n   *         path = \"hdfs://<host>:<port>/path/to/partition/a=1/b=hello/c=3.14\"),\n   *       Partition(\n   *         values = Row(2, \"world\", 6.28),\n   *         path = \"hdfs://<host>:<port>/path/to/partition/a=2/b=world/c=6.28\")))\n   * }}}\n   */\n  def parsePartitions(\n      paths: Seq[Path],\n      typeInference: Boolean,\n      basePaths: Set[Path],\n      userSpecifiedSchema: Option[StructType],\n      caseSensitive: Boolean,\n      validatePartitionColumns: Boolean,\n      timeZoneId: String): PartitionSpec = {\n    parsePartitions(paths, typeInference, basePaths, userSpecifiedSchema, caseSensitive,\n      validatePartitionColumns, DateTimeUtils.getTimeZone(timeZoneId))\n  }\n\n  def parsePartitions(\n      paths: Seq[Path],\n      typeInference: Boolean,\n      basePaths: Set[Path],\n      userSpecifiedSchema: Option[StructType],\n      caseSensitive: Boolean,\n      validatePartitionColumns: Boolean,\n      timeZone: TimeZone): PartitionSpec = {\n    val userSpecifiedDataTypes = if (userSpecifiedSchema.isDefined) {\n      val nameToDataType = mapNameToDataType(userSpecifiedSchema.get)\n      if (!caseSensitive) {\n        CaseInsensitiveMap(nameToDataType)\n      } else {\n        nameToDataType\n      }\n    } else {\n      Map.empty[String, DataType]\n    }\n\n    // SPARK-26990: use user specified field names if case insensitive.\n    val userSpecifiedNames = if (userSpecifiedSchema.isDefined && !caseSensitive) {\n      CaseInsensitiveMap(userSpecifiedSchema.get.fields.map(f => f.name -> f.name).toMap)\n    } else {\n      Map.empty[String, String]\n    }\n\n    val dateFormatter = DateFormatter()\n    val timestampFormatter = TimestampFormatter(timestampPartitionPattern, timeZone)\n    // First, we need to parse every partition's path and see if we can find partition values.\n    val (partitionValues, optDiscoveredBasePaths) = paths.map { path =>\n      parsePartition(path, typeInference, basePaths, userSpecifiedDataTypes,\n        validatePartitionColumns, timeZone, dateFormatter, timestampFormatter)\n    }.unzip\n\n    // We create pairs of (path -> path's partition value) here\n    // If the corresponding partition value is None, the pair will be skipped\n    val pathsWithPartitionValues = paths.zip(partitionValues).flatMap(x => x._2.map(x._1 -> _))\n\n    if (pathsWithPartitionValues.isEmpty) {\n      // This dataset is not partitioned.\n      PartitionSpec.emptySpec\n    } else {\n      // This dataset is partitioned. We need to check whether all partitions have the same\n      // partition columns and resolve potential type conflicts.\n\n      // Check if there is conflicting directory structure.\n      // For the paths such as:\n      // var paths = Seq(\n      //   \"hdfs://host:9000/invalidPath\",\n      //   \"hdfs://host:9000/path/a=10/b=20\",\n      //   \"hdfs://host:9000/path/a=10.5/b=hello\")\n      // It will be recognised as conflicting directory structure:\n      //   \"hdfs://host:9000/invalidPath\"\n      //   \"hdfs://host:9000/path\"\n      // TODO: Selective case sensitivity.\n      val discoveredBasePaths = optDiscoveredBasePaths.flatten.map(_.toString.toLowerCase())\n      assert(\n        discoveredBasePaths.distinct.size == 1,\n        \"Conflicting directory structures detected. Suspicious paths:\\b\" +\n          discoveredBasePaths.distinct.mkString(\"\\n\\t\", \"\\n\\t\", \"\\n\\n\") +\n          \"If provided paths are partition directories, please set \" +\n          \"\\\"basePath\\\" in the options of the data source to specify the \" +\n          \"root directory of the table. If there are multiple root directories, \" +\n          \"please load them separately and then union them.\")\n\n      val resolvedPartitionValues =\n        resolvePartitions(pathsWithPartitionValues, caseSensitive, timeZone)\n\n      // Creates the StructType which represents the partition columns.\n      val fields = {\n        val PartitionValues(columnNames, literals) = resolvedPartitionValues.head\n        columnNames.zip(literals).map { case (name, Literal(_, dataType)) =>\n          // We always assume partition columns are nullable since we've no idea whether null values\n          // will be appended in the future.\n          val resultName = userSpecifiedNames.getOrElse(name, name)\n          val resultDataType = userSpecifiedDataTypes.getOrElse(name, dataType)\n          StructField(resultName, resultDataType, nullable = true)\n        }\n      }\n\n      // Finally, we create `Partition`s based on paths and resolved partition values.\n      val partitions = resolvedPartitionValues.zip(pathsWithPartitionValues).map {\n        case (PartitionValues(_, literals), (path, _)) =>\n          PartitionPath(InternalRow.fromSeq(literals.map(_.value)), path)\n      }\n\n      PartitionSpec(StructType(fields), partitions)\n    }\n  }\n\n  /**\n   * Parses a single partition, returns column names and values of each partition column, also\n   * the path when we stop partition discovery.  For example, given:\n   * {{{\n   *   path = hdfs://<host>:<port>/path/to/partition/a=42/b=hello/c=3.14\n   * }}}\n   * it returns the partition:\n   * {{{\n   *   PartitionValues(\n   *     Seq(\"a\", \"b\", \"c\"),\n   *     Seq(\n   *       Literal.create(42, IntegerType),\n   *       Literal.create(\"hello\", StringType),\n   *       Literal.create(3.14, DoubleType)))\n   * }}}\n   * and the path when we stop the discovery is:\n   * {{{\n   *   hdfs://<host>:<port>/path/to/partition\n   * }}}\n   */\n  def parsePartition(\n      path: Path,\n      typeInference: Boolean,\n      basePaths: Set[Path],\n      userSpecifiedDataTypes: Map[String, DataType],\n      validatePartitionColumns: Boolean,\n      timeZone: TimeZone,\n      dateFormatter: DateFormatter,\n      timestampFormatter: TimestampFormatter,\n      useUtcNormalizedTimestamp: Boolean = false): (Option[PartitionValues], Option[Path]) = {\n    val columns = ArrayBuffer.empty[(String, Literal)]\n    // Old Hadoop versions don't have `Path.isRoot`\n    var finished = path.getParent == null\n    // currentPath is the current path that we will use to parse partition column value.\n    var currentPath: Path = path\n\n    while (!finished) {\n      // Sometimes (e.g., when speculative task is enabled), temporary directories may be left\n      // uncleaned. Here we simply ignore them.\n      if (currentPath.getName.toLowerCase(Locale.ROOT) == \"_temporary\") {\n        return (None, None)\n      }\n\n      if (basePaths.contains(currentPath)) {\n        // If the currentPath is one of base paths. We should stop.\n        finished = true\n      } else {\n        // Let's say currentPath is a path of \"/table/a=1/\", currentPath.getName will give us a=1.\n        // Once we get the string, we try to parse it and find the partition column and value.\n        val maybeColumn =\n        parsePartitionColumn(currentPath.getName, typeInference, userSpecifiedDataTypes,\n          validatePartitionColumns, timeZone, dateFormatter, timestampFormatter,\n          useUtcNormalizedTimestamp)\n        maybeColumn.foreach(columns += _)\n\n        // Now, we determine if we should stop.\n        // When we hit any of the following cases, we will stop:\n        //  - In this iteration, we could not parse the value of partition column and value,\n        //    i.e. maybeColumn is None, and columns is not empty. At here we check if columns is\n        //    empty to handle cases like /table/a=1/_temporary/something (we need to find a=1 in\n        //    this case).\n        //  - After we get the new currentPath, this new currentPath represent the top level dir\n        //    i.e. currentPath.getParent == null. For the example of \"/table/a=1/\",\n        //    the top level dir is \"/table\".\n        finished =\n          (maybeColumn.isEmpty && columns.nonEmpty) || currentPath.getParent == null\n\n        if (!finished) {\n          // For the above example, currentPath will be \"/table/\".\n          currentPath = currentPath.getParent\n        }\n      }\n    }\n\n    if (columns.isEmpty) {\n      (None, Some(path))\n    } else {\n      val (columnNames, values) = columns.reverse.unzip\n      (Some(PartitionValues(columnNames.toSeq, values.toSeq)), Some(currentPath))\n    }\n  }\n\n  private def parsePartitionColumn(\n      columnSpec: String,\n      typeInference: Boolean,\n      userSpecifiedDataTypes: Map[String, DataType],\n      validatePartitionColumns: Boolean,\n      timeZone: TimeZone,\n      dateFormatter: DateFormatter,\n      timestampFormatter: TimestampFormatter,\n      useUtcNormalizedTimestamp: Boolean = false): Option[(String, Literal)] = {\n    val equalSignIndex = columnSpec.indexOf('=')\n    if (equalSignIndex == -1) {\n      None\n    } else {\n      val columnName = unescapePathName(columnSpec.take(equalSignIndex))\n      assert(columnName.nonEmpty, s\"Empty partition column name in '$columnSpec'\")\n\n      val rawColumnValue = columnSpec.drop(equalSignIndex + 1)\n      assert(rawColumnValue.nonEmpty, s\"Empty partition column value in '$columnSpec'\")\n\n      val unescapedColumnValue = unescapePathName(rawColumnValue)\n      val columnValue = if (unescapedColumnValue == DEFAULT_PARTITION_NAME) {\n        null\n      } else {\n        unescapedColumnValue\n      }\n\n      // Workaround to maintain backward compatibility when UTC timestamp normalization is disabled.\n      // The UTC timestamp partition values feature (enabled by default via\n      // [[DeltaSQLConf.UTC_TIMESTAMP_PARTITION_VALUES]]) normalizes timestamp partition values to\n      // UTC ISO 8601 format. When this feature is disabled, [[useUtcNormalizedTimestamp = false]],\n      // we treat TimestampType partition columns as StringType to avoid timestamp parsing and\n      // formatting operations that could change the original string representation.\n      // This exists solely to sustain the existing behavior for legacy code paths that have\n      // UTC normalization disabled. The partition value will be parsed as a string literal and\n      // preserved exactly as provided, rather than being parsed as a timestamp and potentially\n      // reformatted.\n      val dataType = userSpecifiedDataTypes.get(columnName).map {\n        case TimestampType if !useUtcNormalizedTimestamp => StringType\n        case dt => dt\n      }\n\n      val literal = parsePartitionValue(\n        columnName,\n        columnValue,\n        dataType,\n        typeInference,\n        timeZone,\n        dateFormatter,\n        timestampFormatter,\n        validatePartitionColumns)\n      Some(columnName -> literal)\n    }\n  }\n\n  /**\n   * Given a partition path fragment, e.g. `fieldOne=1/fieldTwo=2`, returns a parsed spec\n   * for that fragment as a `TablePartitionSpec`, e.g. `Map((\"fieldOne\", \"1\"), (\"fieldTwo\", \"2\"))`.\n   */\n  def parsePathFragment(pathFragment: String): TablePartitionSpec = {\n    parsePathFragmentAsSeq(pathFragment).toMap\n  }\n\n  /**\n   * Given a partition path fragment, e.g. `fieldOne=1/fieldTwo=2`, returns a parsed spec\n   * for that fragment as a `Seq[(String, String)]`, e.g.\n   * `Seq((\"fieldOne\", \"1\"), (\"fieldTwo\", \"2\"))`.\n   */\n  def parsePathFragmentAsSeq(pathFragment: String): Seq[(String, String)] = {\n    pathFragment.stripPrefix(\"data/\").split(\"/\").map { kv =>\n      val pair = kv.split(\"=\", 2)\n      (unescapePathName(pair(0)), unescapePathName(pair(1)))\n    }\n  }\n\n  /**\n   * This is the inverse of parsePathFragment().\n   */\n  def getPathFragment(spec: TablePartitionSpec, partitionSchema: StructType): String = {\n    partitionSchema.map { field =>\n      escapePathName(field.name) + \"=\" + escapePathName(spec(field.name))\n    }.mkString(\"/\")\n  }\n\n  def getPathFragment(spec: TablePartitionSpec, partitionColumns: Seq[Attribute]): String = {\n    getPathFragment(spec, DataTypeUtils.fromAttributes(partitionColumns))\n  }\n\n  /**\n   * Normalize the column names in partition specification, w.r.t. the real partition column names\n   * and case sensitivity. e.g., if the partition spec has a column named `monTh`, and there is a\n   * partition column named `month`, and it's case insensitive, we will normalize `monTh` to\n   * `month`.\n   */\n  def normalizePartitionSpec[T](\n      partitionSpec: Map[String, T],\n      partColNames: Seq[String],\n      tblName: String,\n      resolver: Resolver): Map[String, T] = {\n    val normalizedPartSpec = partitionSpec.toSeq.map { case (key, value) =>\n      val normalizedKey = partColNames.find(resolver(_, key)).getOrElse {\n        throw DeltaErrors.invalidPartitionColumn(key, tblName)\n      }\n      normalizedKey -> value\n    }\n\n    checkColumnNameDuplication(\n      normalizedPartSpec.map(_._1), \"in the partition schema\", resolver)\n\n    normalizedPartSpec.toMap\n  }\n\n  /**\n   * Resolves possible type conflicts between partitions by up-casting \"lower\" types using\n   * [[findWiderTypeForPartitionColumn]].\n   */\n  def resolvePartitions(\n      pathsWithPartitionValues: Seq[(Path, PartitionValues)],\n      caseSensitive: Boolean,\n      timeZone: TimeZone): Seq[PartitionValues] = {\n    if (pathsWithPartitionValues.isEmpty) {\n      Seq.empty\n    } else {\n      val partColNames = if (caseSensitive) {\n        pathsWithPartitionValues.map(_._2.columnNames)\n      } else {\n        pathsWithPartitionValues.map(_._2.columnNames.map(_.toLowerCase()))\n      }\n      assert(\n        partColNames.distinct.size == 1,\n        listConflictingPartitionColumns(pathsWithPartitionValues))\n\n      // Resolves possible type conflicts for each column\n      val values = pathsWithPartitionValues.map(_._2)\n      val columnCount = values.head.columnNames.size\n      val resolvedValues = (0 until columnCount).map { i =>\n        resolveTypeConflicts(values.map(_.literals(i)), timeZone)\n      }\n\n      // Fills resolved literals back to each partition\n      values.zipWithIndex.map { case (d, index) =>\n        d.copy(literals = resolvedValues.map(_(index)))\n      }\n    }\n  }\n\n  def listConflictingPartitionColumns(\n      pathWithPartitionValues: Seq[(Path, PartitionValues)]): String = {\n    val distinctPartColNames = pathWithPartitionValues.map(_._2.columnNames).distinct\n\n    def groupByKey[K, V](seq: Seq[(K, V)]): Map[K, Iterable[V]] =\n      seq.groupBy { case (key, _) => key }.mapValues(_.map { case (_, value) => value }).toMap\n\n    val partColNamesToPaths = groupByKey(pathWithPartitionValues.map {\n      case (path, partValues) => partValues.columnNames -> path\n    })\n\n    val distinctPartColLists = distinctPartColNames.map(_.mkString(\", \")).zipWithIndex.map {\n      case (names, index) =>\n        s\"Partition column name list #$index: $names\"\n    }\n\n    // Lists out those non-leaf partition directories that also contain files\n    val suspiciousPaths = distinctPartColNames.sortBy(_.length).flatMap(partColNamesToPaths)\n\n    s\"Conflicting partition column names detected:\\n\" +\n      distinctPartColLists.mkString(\"\\n\\t\", \"\\n\\t\", \"\\n\\n\") +\n      \"For partitioned table directories, data files should only live in leaf directories.\\n\" +\n      \"And directories at the same level should have the same partition column name.\\n\" +\n      \"Please check the following directories for unexpected files or \" +\n      \"inconsistent partition column names:\\n\" +\n      suspiciousPaths.map(\"\\t\" + _).mkString(\"\\n\", \"\\n\", \"\")\n  }\n\n  // scalastyle:off line.size.limit\n  /**\n   * Converts a string to a [[Literal]] with automatic type inference. Currently only supports\n   * [[NullType]], [[IntegerType]], [[LongType]], [[DoubleType]], [[DecimalType]], [[DateType]]\n   * [[TimestampType]], and [[StringType]].\n   *\n   * When resolving conflicts, it follows the table below:\n   *\n   * +--------------------+-------------------+-------------------+-------------------+--------------------+------------+---------------+---------------+------------+\n   * | InputA \\ InputB    | NullType          | IntegerType       | LongType          | DecimalType(38,0)* | DoubleType | DateType      | TimestampType | StringType |\n   * +--------------------+-------------------+-------------------+-------------------+--------------------+------------+---------------+---------------+------------+\n   * | NullType           | NullType          | IntegerType       | LongType          | DecimalType(38,0)  | DoubleType | DateType      | TimestampType | StringType |\n   * | IntegerType        | IntegerType       | IntegerType       | LongType          | DecimalType(38,0)  | DoubleType | StringType    | StringType    | StringType |\n   * | LongType           | LongType          | LongType          | LongType          | DecimalType(38,0)  | StringType | StringType    | StringType    | StringType |\n   * | DecimalType(38,0)* | DecimalType(38,0) | DecimalType(38,0) | DecimalType(38,0) | DecimalType(38,0)  | StringType | StringType    | StringType    | StringType |\n   * | DoubleType         | DoubleType        | DoubleType        | StringType        | StringType         | DoubleType | StringType    | StringType    | StringType |\n   * | DateType           | DateType          | StringType        | StringType        | StringType         | StringType | DateType      | TimestampType | StringType |\n   * | TimestampType      | TimestampType     | StringType        | StringType        | StringType         | StringType | TimestampType | TimestampType | StringType |\n   * | StringType         | StringType        | StringType        | StringType        | StringType         | StringType | StringType    | StringType    | StringType |\n   * +--------------------+-------------------+-------------------+-------------------+--------------------+------------+---------------+---------------+------------+\n   * Note that, for DecimalType(38,0)*, the table above intentionally does not cover all other\n   * combinations of scales and precisions because currently we only infer decimal type like\n   * `BigInteger`/`BigInt`. For example, 1.1 is inferred as double type.\n   * Note: [[columnValue]] is expected to be an column value unescaped from path escaping,\n   * and [[DEFAULT_PARTITION_NAME]] should be replaced by null.\n   */\n  // scalastyle:on line.size.limit\n  def inferPartitionColumnValue(\n      columnValue: String,\n      typeInference: Boolean,\n      timeZone: TimeZone,\n      dateFormatter: DateFormatter,\n      timestampFormatter: TimestampFormatter): Literal = {\n    def decimalTry = Try {\n      // `BigDecimal` conversion can fail when the `field` is not a form of number.\n      val bigDecimal = new JBigDecimal(columnValue)\n      // It reduces the cases for decimals by disallowing values having scale (eg. `1.1`).\n      require(bigDecimal.scale <= 0)\n      // `DecimalType` conversion can fail when\n      //   1. The precision is bigger than 38.\n      //   2. scale is bigger than precision.\n      Literal(bigDecimal)\n    }\n\n    def dateTry = Try {\n      // try and parse the date, if no exception occurs this is a candidate to be resolved as\n      // DateType\n      dateFormatter.parse(columnValue)\n      // SPARK-23436: Casting the string to date may still return null if a bad Date is provided.\n      // This can happen since DateFormat.parse  may not use the entire text of the given string:\n      // so if there are extra-characters after the date, it returns correctly.\n      // We need to check that we can cast the raw string since we later can use Cast to get\n      // the partition values with the right DataType (see\n      // org.apache.spark.sql.execution.datasources.PartitioningAwareFileIndex.inferPartitioning)\n      val dateValue = Cast(Literal(columnValue), DateType).eval()\n      // Disallow DateType if the cast returned null\n      require(dateValue != null)\n      Literal.create(dateValue, DateType)\n    }\n\n    def timestampTry = Try {\n      // try and parse the date, if no exception occurs this is a candidate to be resolved as\n      // TimestampType\n      timestampFormatter.parse(columnValue)\n      // SPARK-23436: see comment for date\n      val timestampValue = Cast(Literal(columnValue), TimestampType, Some(timeZone.getID)).eval()\n      // Disallow TimestampType if the cast returned null\n      require(timestampValue != null)\n      Literal.create(timestampValue, TimestampType)\n    }\n\n    if (columnValue == null) {\n      Literal.default(NullType)\n    } else if (typeInference) {\n      // First tries integral types\n      Try(Literal.create(Integer.parseInt(columnValue), IntegerType))\n        .orElse(Try(Literal.create(JLong.parseLong(columnValue), LongType)))\n        .orElse(decimalTry)\n        // Then falls back to fractional types\n        .orElse(Try(Literal.create(JDouble.parseDouble(columnValue), DoubleType)))\n        // Then falls back to date/timestamp types\n        .orElse(timestampTry)\n        .orElse(dateTry)\n        // Then falls back to string\n        .getOrElse(Literal.create(columnValue, StringType))\n    } else {\n      Literal.create(columnValue, StringType)\n    }\n  }\n\n  def validatePartitionColumn(\n      schema: StructType,\n      partitionColumns: Seq[String],\n      caseSensitive: Boolean): Unit = {\n    checkColumnNameDuplication(\n      partitionColumns,\n      \"in the partition columns\",\n      caseSensitive)\n\n    partitionColumnsSchema(schema, partitionColumns, caseSensitive).foreach {\n      field => field.dataType match {\n        // Variant types are not orderable and thus cannot be partition columns.\n        case a: AtomicType if !a.isInstanceOf[VariantType] => // OK\n        case _ => throw DeltaErrors.cannotUseDataTypeForPartitionColumnError(field)\n      }\n    }\n\n    if (partitionColumns.nonEmpty && partitionColumns.size == schema.fields.length) {\n      throw new DeltaAnalysisException(\n        errorClass = \"DELTA_CANNOT_USE_ALL_COLUMNS_FOR_PARTITION\",\n        Array.empty)\n    }\n  }\n\n  def partitionColumnsSchema(\n      schema: StructType,\n      partitionColumns: Seq[String],\n      caseSensitive: Boolean): StructType = {\n    val equality = columnNameEquality(caseSensitive)\n    StructType(partitionColumns.map { col =>\n      schema.find(f => equality(f.name, col)).getOrElse {\n        val schemaCatalog = schema.catalogString\n        throw DeltaErrors.missingPartitionColumn(col, schemaCatalog)\n      }\n    }).asNullable\n  }\n\n  def mergeDataAndPartitionSchema(\n      dataSchema: StructType,\n      partitionSchema: StructType,\n      caseSensitive: Boolean): (StructType, Map[String, StructField]) = {\n    val overlappedPartCols = mutable.Map.empty[String, StructField]\n    partitionSchema.foreach { partitionField =>\n      val partitionFieldName = getColName(partitionField, caseSensitive)\n      if (dataSchema.exists(getColName(_, caseSensitive) == partitionFieldName)) {\n        overlappedPartCols += partitionFieldName -> partitionField\n      }\n    }\n\n    // When data and partition schemas have overlapping columns, the output\n    // schema respects the order of the data schema for the overlapping columns, and it\n    // respects the data types of the partition schema.\n    // `HadoopFsRelation` will be mapped to `FileSourceScanExec`, which always output\n    // all the partition columns physically. Here we need to make sure the final schema\n    // contains all the partition columns.\n    val fullSchema =\n    StructType(dataSchema.map(f => overlappedPartCols.getOrElse(getColName(f, caseSensitive), f)) ++\n      partitionSchema.filterNot(f => overlappedPartCols.contains(getColName(f, caseSensitive))))\n    (fullSchema, overlappedPartCols.toMap)\n  }\n\n  def getColName(f: StructField, caseSensitive: Boolean): String = {\n    if (caseSensitive) {\n      f.name\n    } else {\n      f.name.toLowerCase(Locale.ROOT)\n    }\n  }\n\n  private def columnNameEquality(caseSensitive: Boolean): (String, String) => Boolean = {\n    if (caseSensitive) {\n      org.apache.spark.sql.catalyst.analysis.caseSensitiveResolution\n    } else {\n      org.apache.spark.sql.catalyst.analysis.caseInsensitiveResolution\n    }\n  }\n\n  /**\n   * Given a collection of [[Literal]]s, resolves possible type conflicts by\n   * [[findWiderTypeForPartitionColumn]].\n   */\n  private def resolveTypeConflicts(literals: Seq[Literal], timeZone: TimeZone): Seq[Literal] = {\n    val litTypes = literals.map(_.dataType)\n    val desiredType = litTypes.reduce(findWiderTypeForPartitionColumn)\n\n    literals.map { case l @ Literal(_, dataType) =>\n      Literal.create(Cast(l, desiredType, Some(timeZone.getID)).eval(), desiredType)\n    }\n  }\n\n  /**\n   * Type widening rule for partition column types. It is similar to\n   * [[TypeCoercion.findWiderTypeForTwo]] but the main difference is that here we disallow\n   * precision loss when widening double/long and decimal, and fall back to string.\n   */\n  private val findWiderTypeForPartitionColumn: (DataType, DataType) => DataType = {\n    case (DoubleType, _: DecimalType) | (_: DecimalType, DoubleType) => StringType\n    case (DoubleType, LongType) | (LongType, DoubleType) => StringType\n    case (t1, t2) => TypeCoercion.findWiderTypeForTwo(t1, t2).getOrElse(StringType)\n  }\n\n  /** The methods below are forked from [[org.apache.spark.sql.util.SchemaUtils]] */\n\n  /**\n   * Checks if input column names have duplicate identifiers. This throws an exception if\n   * the duplication exists.\n   *\n   * @param columnNames column names to check\n   * @param colType column type name, used in an exception message\n   * @param resolver resolver used to determine if two identifiers are equal\n   */\n  def checkColumnNameDuplication(\n      columnNames: Seq[String], colType: String, resolver: Resolver): Unit = {\n    checkColumnNameDuplication(columnNames, colType, isCaseSensitiveAnalysis(resolver))\n  }\n\n  /**\n   * Checks if input column names have duplicate identifiers. This throws an exception if\n   * the duplication exists.\n   *\n   * @param columnNames column names to check\n   * @param colType column type name, used in an exception message\n   * @param caseSensitiveAnalysis whether duplication checks should be case sensitive or not\n   */\n  def checkColumnNameDuplication(\n      columnNames: Seq[String], colType: String, caseSensitiveAnalysis: Boolean): Unit = {\n    // scalastyle:off caselocale\n    val names = if (caseSensitiveAnalysis) columnNames else columnNames.map(_.toLowerCase)\n    // scalastyle:on caselocale\n    if (names.distinct.length != names.length) {\n      val duplicateColumns = names.groupBy(identity).collect {\n        case (x, ys) if ys.length > 1 => s\"`$x`\"\n      }\n      throw DeltaErrors.foundDuplicateColumnsException(colType,\n        duplicateColumns.mkString(\", \"))\n    }\n  }\n\n  // Returns true if a given resolver is case-sensitive\n  private def isCaseSensitiveAnalysis(resolver: Resolver): Boolean = {\n    if (resolver == caseSensitiveResolution) {\n      true\n    } else if (resolver == caseInsensitiveResolution) {\n      false\n    } else {\n      sys.error(\"A resolver to check if two identifiers are equal must be \" +\n        \"`caseSensitiveResolution` or `caseInsensitiveResolution` in o.a.s.sql.catalyst.\")\n    }\n  }\n\n  /**\n   * Converts a typed literal to a normalized string representation for correct comparisons.\n   * @param literal The Literal to convert to a string. Must have the correct type set.\n   * @param timeZoneId Optional timezone ID for timestamp types.\n   * @param useUtcNormalizedTimestamp If true, formats timestamp types into UTC ISO 8601 format.\n   * @return The string representation of the literal, or null if the literal is null.\n   */\n  def literalToNormalizedString(\n      literal: Literal,\n      timeZoneId: Option[String] = None,\n      useUtcNormalizedTimestamp: Boolean = false): String = {\n    if (literal == null || literal.value == null) {\n      return null\n    }\n\n    literal.dataType match {\n      case TimestampType if useUtcNormalizedTimestamp =>\n        // Format timestamp in UTC ISO 8601 format: \"2000-01-01T12:00:00.000000Z\"\n        utcFormatter.format(literal.value.asInstanceOf[Long])\n\n      case _ =>\n        // All other types can safely be converted to a string.\n        val castedValue = Cast(literal, StringType, timeZoneId, ansiEnabled = false).eval()\n        Option(castedValue).map(_.toString).orNull\n    }\n  }\n\n  /**\n   * Parses partition values (strings) to their corresponding Literal with the appropiate type\n   * as defined in the partition schema.\n   *\n   * @param partValuesMap Map of partition column names to their string values.\n   * @param partitionSchema Schema defining the data types for each partition column.\n   * @param timeZoneId Time zone ID used for casting timestamp values.\n   * @param validatePartitionColumns Throw an error when casting fails.\n   * @return Map of partition column names to their parsed Literal values.\n   */\n  def parsePartitionValues(\n      partValuesMap: Map[String, String],\n      partitionSchema: StructType,\n      timeZoneId: String,\n      validatePartitionColumns: Boolean = false): Map[String, Literal] = {\n    val partSchemaNames = partitionSchema.names.toSet\n    val partValuesMapKeys = partValuesMap.keySet\n    if (partSchemaNames != partValuesMapKeys) {\n      val errorMsg = s\"Partition values map keys $partValuesMapKeys should match \" +\n        s\"partition schema names $partSchemaNames.\"\n      if (partSchemaNames.map(_.toLowerCase(Locale.ROOT)) ==\n        partValuesMapKeys.map(_.toLowerCase(Locale.ROOT))) {\n        throw new IllegalStateException(s\"CASE_MISMATCH: $errorMsg\")\n      } else if (partValuesMapKeys.isEmpty) {\n        throw new IllegalStateException(s\"PARTITION_VALUES_EMPTY: $errorMsg\")\n      } else {\n        throw new IllegalStateException(errorMsg)\n      }\n    }\n\n    val timeZone = DateTimeUtils.getTimeZone(timeZoneId)\n    val dateFormatter = DateFormatter()\n    val timestampFormatter = TimestampFormatter(timestampPartitionPattern, timeZone)\n    val partColNameToDataType = mapNameToDataType(partitionSchema)\n\n    partValuesMap.map {\n      case (partColName, partValueStr) =>\n        val dataType = partColNameToDataType(partColName)\n        val literal = parsePartitionValue(\n          partColName,\n          partValueStr,\n          Some(dataType),\n          typeInference = false,\n          timeZone,\n          dateFormatter,\n          timestampFormatter,\n          validatePartitionColumns)\n        (partColName, literal)\n    }\n  }\n\n  /**\n   * Parses a single partition value string and returns a Literal with the appropriate type.\n   *\n   * @param columnName The name of the partition column (used for error messages).\n   * @param rawValue The raw string value of the partition.\n   * @param dataType Optional data type from the schema. If None, type inference is used.\n   * @param typeInference Whether to infer the type when dataType is None.\n   * @param timeZone Time zone for timestamp parsing.\n   * @param dateFormatter Formatter for date parsing.\n   * @param timestampFormatter Formatter for timestamp parsing.\n   * @param validatePartitionColumns Throw an error when casting fails.\n   * @return A Literal containing the parsed value.\n   */\n  private def parsePartitionValue(\n      columnName: String,\n      columnValue: String,\n      dataType: Option[DataType],\n      typeInference: Boolean,\n      timeZone: TimeZone,\n      dateFormatter: DateFormatter,\n      timestampFormatter: TimestampFormatter,\n      validatePartitionColumns: Boolean = false): Literal = {\n    dataType match {\n      case Some(dt) =>\n        // If columnValue is null, return a null literal with the appropriate type.\n        if (columnValue == null) {\n          return Literal.create(null, dt)\n        }\n\n        // Fall back string literal\n        val columnValueStringLiteral = Literal.create(columnValue, StringType)\n\n        dt match {\n          case TimestampType =>\n            Try {\n              Literal.create(\n                timestampFormatter.parse(columnValue),\n                TimestampType)\n            }.getOrElse {\n              // If the timestamp is not in the expected format, cast it manually.\n              val castedValue = Cast(\n                columnValueStringLiteral,\n                TimestampType,\n                Option(timeZone.getID),\n                ansiEnabled = false).eval()\n              if (castedValue != null) {\n                Literal.create(castedValue, TimestampType)\n              } else if (validatePartitionColumns) {\n                throw DeltaErrors.partitionColumnCastFailed(\n                  Option(columnValue).map(_.toString).getOrElse(\"null\"),\n                  TimestampType.toString,\n                  columnName)\n              } else {\n                columnValueStringLiteral\n              }\n            }\n\n          case _ =>\n            val castedValue = Cast(\n              columnValueStringLiteral, dt, Option(timeZone.getID), ansiEnabled = false).eval()\n            if (castedValue == null) {\n              if (validatePartitionColumns) {\n                throw DeltaErrors.partitionColumnCastFailed(\n                  Option(columnValue).map(_.toString).getOrElse(\"null\"), dt.toString, columnName)\n              }\n              columnValueStringLiteral\n            } else {\n              Literal.create(castedValue, dt)\n            }\n        }\n\n      case None =>\n        // No schema provided - use type inference\n        inferPartitionColumnValue(\n          columnValue,\n          typeInference,\n          timeZone,\n          dateFormatter,\n          timestampFormatter)\n    }\n  }\n\n  // TODO: Use helpers from Spark when https://github.com/apache/spark/pull/54117 is available.\n  private def mapNameToDataType(schema: StructType): Map[String, DataType] =\n    schema.fields.map(f => f.name -> f.dataType).toMap\n\n  def classifyPartitionValueParsingError(e: Throwable): String = e match {\n    case ex if ex.getMessage.contains(\"CASE_MISMATCH\") =>\n      \".caseMismatch\"\n    case ex if ex.getMessage.contains(\"PARTITION_VALUES_EMPTY\") =>\n      \".partitionValuesEmpty\"\n    case _ => \"\"\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/PathWithFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\n/**\n * Bundling the `Path` with the `FileSystem` instance ensures\n * that we never pass the wrong file system with the path to a function\n * at compile time.\n */\ncase class PathWithFileSystem private (path: Path, fs: FileSystem) {\n\n  /**\n   * Extends the path with `s`\n   *\n   * The resulting path must be on the same filesystem.\n   */\n  def withSuffix(s: String): PathWithFileSystem = new PathWithFileSystem(new Path(path, s), fs)\n\n  /**\n   * Qualify `path` using `fs`\n   */\n  def makeQualified(): PathWithFileSystem = {\n    val qualifiedPath = fs.makeQualified(path)\n    PathWithFileSystem(qualifiedPath, fs)\n  }\n}\n\nobject PathWithFileSystem {\n\n  /**\n   * Create a new `PathWithFileSystem` instance by calling `getFileSystem`\n   * on `path` with the given `hadoopConf`.\n   */\n  def withConf(path: Path, hadoopConf: Configuration): PathWithFileSystem = {\n    val fs = path.getFileSystem(hadoopConf)\n    PathWithFileSystem(path, fs)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/SetAccumulator.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.util.AccumulatorV2\n\n/**\n * Accumulator to collect distinct elements as a set.\n */\nclass SetAccumulator[T] extends AccumulatorV2[T, java.util.Set[T]] {\n  private var _set: java.util.Set[T] = _\n\n  private def getOrCreate = {\n    _set = Option(_set).getOrElse(java.util.Collections.synchronizedSet(new java.util.HashSet[T]()))\n    _set\n  }\n\n  override def isZero: Boolean = this.synchronized(getOrCreate.isEmpty)\n\n  override def reset(): Unit = this.synchronized {\n    _set = null\n  }\n  override def add(v: T): Unit = this.synchronized(getOrCreate.add(v))\n\n  override def merge(other: AccumulatorV2[T, java.util.Set[T]]): Unit = other match {\n    case o: SetAccumulator[T] => this.synchronized(getOrCreate.addAll(o.value))\n    case _ => throw new UnsupportedOperationException(\n      s\"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}\")\n  }\n\n  override def value: java.util.Set[T] = this.synchronized {\n    java.util.Collections.unmodifiableSet(new java.util.HashSet[T](getOrCreate))\n  }\n\n  override def copy(): AccumulatorV2[T, java.util.Set[T]] = {\n    val newAcc = new SetAccumulator[T]()\n    this.synchronized {\n      newAcc.getOrCreate.addAll(getOrCreate)\n    }\n    newAcc\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/StateCache.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.{DataFrameUtils, Snapshot}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.sql.{DataFrame, Dataset, SparkSession}\nimport org.apache.spark.sql.execution.{LogicalRDD, SQLExecution}\nimport org.apache.spark.storage.StorageLevel\n\n/**\n * Machinery that caches the reconstructed state of a Delta table\n * using the RDD cache. The cache is designed so that the first access\n * will materialize the results.  However, once uncache is called,\n * all data will be flushed and will not be cached again.\n */\ntrait StateCache extends DeltaLogging {\n  protected def spark: SparkSession\n\n  /** If state RDDs for this snapshot should still be cached. */\n  private var _isCached = true\n  /** A list of RDDs that we need to uncache when we are done with this snapshot. */\n  private val cached = ArrayBuffer[RDD[_]]()\n  private val cached_refs = ArrayBuffer[DatasetRefCache[_]]()\n\n  /** Method to expose the value of _isCached for testing. */\n  private[delta] def isCached: Boolean = _isCached\n\n  private val storageLevel = StorageLevel.fromString(\n    spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SNAPSHOT_CACHE_STORAGE_LEVEL))\n\n  class CachedDS[A] private[StateCache](ds: Dataset[A], name: String) {\n    // While we cache RDD to avoid re-computation in different spark sessions, `Dataset` can only be\n    // reused by the session that created it to avoid session pollution. So we use `DatasetRefCache`\n    // to re-create a new `Dataset` when the active session is changed. This is an optimization for\n    // single-session scenarios to avoid the overhead of `Dataset` creation which can take 100ms.\n    private val cachedDs = cached.synchronized {\n      if (isCached) {\n        val qe = ds.queryExecution\n        val rdd = SQLExecution.withNewExecutionId(qe, Some(s\"Cache $name\")) {\n          val rdd = recordFrameProfile(\"Delta\", \"CachedDS.toRdd\") {\n            // toRdd should always trigger execution\n            qe.toRdd.map(_.copy())\n          }\n          rdd.setName(name)\n          rdd.persist(storageLevel)\n        }\n        cached += rdd\n        val dsCache = datasetRefCache { () =>\n          val logicalRdd = LogicalRDD(qe.analyzed.output, rdd)(spark)\n          DataFrameUtils.ofRows(spark, logicalRdd)\n        }\n        Some(dsCache)\n      } else {\n        None\n      }\n    }\n\n    /**\n     * Retrieves the cached RDD in Dataframe form.\n     *\n     * If a RDD cache is available,\n     * - return the cached DF if called from the same session in which the cached DF is created, or\n     * - reconstruct the DF using the RDD cache if called from a different session.\n     *\n     * If no RDD cache is available,\n     * - return a copy of the original DF with updated spark session.\n     *\n     * Since a cached DeltaLog can be accessed from multiple Spark sessions, this interface makes\n     * sure that the original Spark session in the cached DF does not leak into the current active\n     * sessions.\n     */\n    def getDF: DataFrame = {\n      if (cached.synchronized(isCached) && cachedDs.isDefined) {\n        cachedDs.get.get\n      } else {\n        DataFrameUtils.ofRows(spark, ds.queryExecution.logical)\n      }\n    }\n\n    /**\n     * Retrieves the cached RDD as a strongly-typed Dataset.\n     */\n    def getDS: Dataset[A] = getDF.as(ds.encoder)\n  }\n\n  /**\n   * Create a CachedDS instance for the given Dataset and the name.\n   */\n  def cacheDS[A](ds: Dataset[A], name: String): CachedDS[A] = recordFrameProfile(\n    \"Delta\", \"CachedDS.cacheDS\") {\n    new CachedDS[A](ds, name)\n  }\n\n  def datasetRefCache[A](creator: () => Dataset[A]): DatasetRefCache[A] = {\n    val dsCache = new DatasetRefCache(creator)\n    cached_refs += dsCache\n    dsCache\n  }\n\n  /** Drop any cached data for this [[Snapshot]]. */\n  def uncache(): Unit = cached.synchronized {\n    if (isCached) {\n      _isCached = false\n      cached.foreach(_.unpersist(blocking = false))\n      cached_refs.foreach(_.invalidate())\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/TimestampFormatter.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util\n\nimport java.text.ParseException\nimport java.time._\nimport java.time.format.DateTimeParseException\nimport java.time.temporal.TemporalQueries\nimport java.util.{Locale, TimeZone}\n\nimport org.apache.spark.sql.delta.util.DateTimeUtils.instantToMicros\n\n/**\n * Forked from [[org.apache.spark.sql.catalyst.util.TimestampFormatter]]\n */\nsealed trait TimestampFormatter extends Serializable {\n  /**\n   * Parses a timestamp in a string and converts it to microseconds.\n   *\n   * @param s - string with timestamp to parse\n   * @return microseconds since epoch.\n   * @throws ParseException can be thrown by legacy parser\n   * @throws DateTimeParseException can be thrown by new parser\n   * @throws DateTimeException unable to obtain local date or time\n   */\n  @throws(classOf[ParseException])\n  @throws(classOf[DateTimeParseException])\n  @throws(classOf[DateTimeException])\n  def parse(s: String): Long\n  def format(us: Long): String\n}\n\nclass Iso8601TimestampFormatter(\n    pattern: String,\n    timeZone: ZoneId,\n    locale: Locale) extends TimestampFormatter with DateTimeFormatterHelper {\n  @transient\n  protected lazy val formatter = getOrCreateFormatter(pattern, locale)\n\n  private def toInstant(s: String): Instant = {\n    val temporalAccessor = formatter.parse(s)\n    if (temporalAccessor.query(TemporalQueries.offset()) == null) {\n      toInstantWithZoneId(temporalAccessor, timeZone)\n    } else {\n      Instant.from(temporalAccessor)\n    }\n  }\n\n  override def parse(s: String): Long = instantToMicros(toInstant(s))\n\n  override def format(us: Long): String = {\n    val instant = DateTimeUtils.microsToInstant(us)\n    formatter.withZone(timeZone).format(instant)\n  }\n}\n\n/**\n * The formatter parses/formats timestamps according to the pattern `yyyy-MM-dd HH:mm:ss.[..fff..]`\n * where `[..fff..]` is a fraction of second up to microsecond resolution. The formatter does not\n * output trailing zeros in the fraction. For example, the timestamp `2019-03-05 15:00:01.123400` is\n * formatted as the string `2019-03-05 15:00:01.1234`.\n *\n * @param timeZone the time zone in which the formatter parses or format timestamps\n */\nclass FractionTimestampFormatter(timeZone: TimeZone)\n  extends Iso8601TimestampFormatter(\"\", timeZone.toZoneId, TimestampFormatter.defaultLocale) {\n\n  @transient\n  override protected lazy val formatter = DateTimeFormatterHelper.fractionFormatter\n}\n\nobject TimestampFormatter {\n  val defaultPattern: String = \"yyyy-MM-dd HH:mm:ss\"\n  val defaultLocale: Locale = Locale.US\n\n  def apply(format: String, zoneId: ZoneId): TimestampFormatter = {\n    new Iso8601TimestampFormatter(format, zoneId, defaultLocale)\n  }\n\n  def apply(format: String, timeZone: TimeZone, locale: Locale): TimestampFormatter = {\n    new Iso8601TimestampFormatter(format, timeZone.toZoneId, locale)\n  }\n\n  def apply(format: String, timeZone: TimeZone): TimestampFormatter = {\n    apply(format, timeZone, defaultLocale)\n  }\n\n  def apply(timeZone: TimeZone): TimestampFormatter = {\n    apply(defaultPattern, timeZone, defaultLocale)\n  }\n\n  def getFractionFormatter(timeZone: TimeZone): TimestampFormatter = {\n    new FractionTimestampFormatter(timeZone)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/TransactionHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, CommitStats, CommittedTransaction, CoordinatedCommitsStats, CoordinatedCommitType, DeltaConfigs, DeltaLog, IsolationLevel, Serializable, Snapshot, WriteSerializable}\nimport org.apache.spark.sql.delta.DeltaOperations.Operation\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile, CommitInfo, DomainMetadata, FileAction, Metadata, Protocol, RemoveFile, SetTransaction}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, TableCommitCoordinatorClient}\nimport org.apache.spark.sql.delta.hooks.PostCommitHook\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.util.ScalaExtensions._\n\nimport org.apache.spark.internal.MDC\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * Contains helper methods for Delta transactions.\n */\ntrait TransactionHelper extends DeltaLogging {\n  def deltaLog: DeltaLog\n  def catalogTable: Option[CatalogTable]\n  def snapshot: Snapshot\n\n  /** Unique identifier for the transaction */\n  def txnId: String\n\n  /**\n   * Returns the metadata for this transaction. The metadata refers to the metadata of the snapshot\n   * at the transaction's read version unless updated during the transaction.\n   */\n  def metadata: Metadata\n\n  /** The protocol of the snapshot that this transaction is reading at. */\n  def protocol: Protocol\n\n  /**\n   * Default [[IsolationLevel]] as set in table metadata.\n   */\n  private[delta] def getDefaultIsolationLevel(): IsolationLevel = {\n    DeltaConfigs.ISOLATION_LEVEL.fromMetaData(metadata)\n  }\n\n  /**\n   * Determines if a transaction can downgrade to SnapshotIsolation.\n   *\n   * Note-1: For no-data-change transactions such as OPTIMIZE/Auto Compaction/ZorderBY, we can\n   * change the isolation level to SnapshotIsolation. SnapshotIsolation allows reduced conflict\n   * detection by skipping the\n   * [[ConflictChecker.checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn]] check i.e.\n   * don't worry about concurrent appends.\n   *\n   * Note-2:\n   * We can also use SnapshotIsolation for empty transactions. e.g. consider a commit:\n   * t0 - Initial state of table\n   * t1 - Q1, Q2 starts\n   * t2 - Q1 commits\n   * t3 - Q2 is empty and wants to commit.\n   * In this scenario, we can always allow Q2 to commit without worrying about new files\n   * generated by Q1.\n   * The final order which satisfies both Serializability and WriteSerializability is: Q2, Q1.\n   * Note that Metadata only update transactions shouldn't be considered empty. If Q2 above has\n   * a Metadata update (say schema change/identity column high watermark update), then Q2 can't\n   * be moved above Q1 in the final SERIALIZABLE order. This is because if Q2 is moved above Q1,\n   * then Q1 should see the updates from Q2 - which actually didn't happen.\n   *\n   * @param actions The sequence of actions being committed.\n   * @param opChangesData Whether the operation changes data (from DeltaOperations.Operation).\n   * @return true if the isolation level can be downgraded to SnapshotIsolation.\n   */\n  private[delta] def canDowngradeToSnapshotIsolation(\n      actions: Seq[Action],\n      opChangesData: Boolean): Boolean = {\n\n    var dataChanged = false\n    var hasIncompatibleActions = false\n    actions.foreach {\n      case f: FileAction =>\n        if (f.dataChange) {\n          dataChanged = true\n        }\n      // Row tracking is able to resolve write conflicts regardless of isolation level.\n      case d: DomainMetadata if RowTrackingMetadataDomain.isSameDomain(d) =>\n        // Do nothing\n      case _ =>\n        hasIncompatibleActions = true\n    }\n\n    if (hasIncompatibleActions) {\n      // If incompatible actions are present (e.g. METADATA etc.), then don't downgrade the\n      // isolation level to SnapshotIsolation.\n      return false\n    }\n\n    val noDataChanged = !dataChanged\n    val defaultIsolationLevel = getDefaultIsolationLevel()\n\n    val allowFallbackToSnapshotIsolation = defaultIsolationLevel match {\n      case Serializable => noDataChanged\n      case WriteSerializable => noDataChanged && !opChangesData\n      case _ => false // This case should never happen.\n    }\n    allowFallbackToSnapshotIsolation\n  }\n\n  /**\n   * Return the user-defined metadata for the operation.\n   */\n  def getUserMetadata(op: Operation): Option[String] = {\n    // option wins over config if both are set\n    op.userMetadata match {\n      case data @ Some(_) => data\n      case None => spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_USER_METADATA)\n    }\n  }\n\n  /** The current spark session */\n  protected def spark: SparkSession = SparkSession.active\n\n  private[delta] lazy val readSnapshotTableCommitCoordinatorClientOpt:\n      Option[TableCommitCoordinatorClient] = {\n    if (snapshot.isCatalogOwned) {\n      // Catalog owned table's commit coordinator is always determined by the catalog.\n      CatalogOwnedTableUtils.populateTableCommitCoordinatorFromCatalog(\n        spark, catalogTable, snapshot)\n    } else {\n      // The commit-coordinator of a table shouldn't change. If it is changed by a concurrent\n      // commit, then it will be detected as a conflict and the transaction will anyway fail.\n      snapshot.getTableCommitCoordinatorForWrites\n    }\n  }\n\n  def createCoordinatedCommitsStats(newProtocol: Option[Protocol]) : CoordinatedCommitsStats = {\n    val (coordinatedCommitsType, metadataToUse) =\n      readSnapshotTableCommitCoordinatorClientOpt match {\n        // TODO: Capture the CO -> FS downgrade case when we start\n        //       supporting downgrade for CO.\n        case Some(_) if snapshot.isCatalogOwned =>                             // CO commit\n          (CoordinatedCommitType.CO_COMMIT, snapshot.metadata)\n        case Some(_) if metadata.coordinatedCommitsCoordinatorName.isEmpty =>  // CC -> FS\n          (CoordinatedCommitType.CC_TO_FS_DOWNGRADE_COMMIT, snapshot.metadata)\n        // Only the 0th commit to a table can be a FS -> CO upgrade for now.\n        // Upgrading an existing FS table to CO through ALTER TABLE is not supported yet.\n        case None if newProtocol.exists(_.readerAndWriterFeatureNames\n            .contains(CatalogOwnedTableFeature.name)) =>                       // FS -> CO\n          (CoordinatedCommitType.FS_TO_CO_UPGRADE_COMMIT, metadata)\n        case None if metadata.coordinatedCommitsCoordinatorName.isDefined =>   // FS -> CC\n          (CoordinatedCommitType.FS_TO_CC_UPGRADE_COMMIT, metadata)\n        case Some(_) =>                                                        // CC commit\n          (CoordinatedCommitType.CC_COMMIT, snapshot.metadata)\n        case None =>                                                           // FS commit\n          (CoordinatedCommitType.FS_COMMIT, snapshot.metadata)\n        // Errors out in rest of the cases.\n        case _ =>\n          throw new IllegalStateException(\n            \"Unexpected state found when trying \" +\n            s\"to generate CoordinatedCommitsStats for table ${deltaLog.logPath}. \" +\n            s\"$readSnapshotTableCommitCoordinatorClientOpt, \" +\n            s\"$metadata, $snapshot, $catalogTable\")\n      }\n    CoordinatedCommitsStats(\n      coordinatedCommitsType = coordinatedCommitsType.toString,\n      commitCoordinatorName = if (Set(CoordinatedCommitType.CO_COMMIT,\n        CoordinatedCommitType.FS_TO_CO_UPGRADE_COMMIT).contains(coordinatedCommitsType)) {\n        // The catalog for FS -> CO upgrade commit would be\n        // \"CATALOG_EMPTY\" because `catalogTable` is not available\n        // for the 0th FS commit.\n        catalogTable.flatMap { ct =>\n          CatalogOwnedTableUtils.getCatalogName(\n            spark,\n            identifier = ct.identifier)\n        }.getOrElse(\"CATALOG_MISSING\")\n      } else {\n        metadataToUse.coordinatedCommitsCoordinatorName.getOrElse(\"NONE\")\n      },\n      // For Catalog-Owned table, the coordinator conf for UC-CC is [[Map.empty]]\n      // so we don't distinguish between CO/CC here.\n      commitCoordinatorConf = metadataToUse.coordinatedCommitsCoordinatorConf)\n  }\n\n  /**\n   * Determines if we should checkpoint the version that has just been committed.\n   */\n  protected def isCheckpointNeeded(\n      committedVersion: Long, postCommitSnapshot: Snapshot): Boolean = {\n    def checkpointInterval = deltaLog.checkpointInterval(postCommitSnapshot.metadata)\n    committedVersion != 0 && committedVersion % checkpointInterval == 0\n  }\n\n  /** Runs a post-commit hook, handling any exceptions that occur. */\n  protected def runPostCommitHook(\n      hook: PostCommitHook,\n      committedTransaction: CommittedTransaction): Unit = {\n    val version = committedTransaction.committedVersion\n    try {\n      hook.run(spark, committedTransaction)\n    } catch {\n      case NonFatal(e) =>\n        logWarning(log\"Error when executing post-commit hook \" +\n          log\"${MDC(DeltaLogKeys.HOOK_NAME, hook.name)} \" +\n          log\"for commit ${MDC(DeltaLogKeys.VERSION, version)}\", e)\n        recordDeltaEvent(deltaLog, \"delta.commit.hook.failure\", data = Map(\n          \"hook\" -> hook.name,\n          \"version\" -> version,\n          \"exception\" -> e.toString\n        ))\n        hook.handleError(spark, e, version)\n    }\n  }\n\n  /**\n   * Generates a timestamp which is greater than the commit timestamp\n   * of the last snapshot. Note that this is only needed when the\n   * feature `inCommitTimestamps` is enabled.\n   */\n  protected[delta] def generateInCommitTimestampForFirstCommitAttempt(\n      currentTimestamp: Long): Option[Long] =\n    Option.when(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(metadata)) {\n      val lastCommitTimestamp = snapshot.timestamp\n      math.max(currentTimestamp, lastCommitTimestamp + 1)\n    }\n\n  /**\n   * Computes and emits commit stats for the current transaction.\n   */\n  class CommitStatsComputer {\n    private var bytesNew: Long = 0L\n    private var numAdd: Int = 0\n    private var numOfDomainMetadatas: Int = 0\n    private var numRemove: Int = 0\n    private var numSetTransaction: Int = 0\n    private var numCdcFiles: Int = 0\n    private var cdcBytesNew: Long = 0L\n    private var numAbsolutePaths = 0\n    // We don't expect commits to have more than 2 billion actions\n    private var numActions: Int = 0\n    private val partitionsAdded = mutable.HashSet.empty[Map[String, String]]\n    private var newProtocolOpt = Option.empty[Protocol]\n    private var newMetadataOpt = Option.empty[Metadata]\n\n    private var inputActionsIteratorOpt = Option.empty[Iterator[Action]]\n\n\n    private def assertStateBeforeFinalization(): Unit = {\n      assert(\n        inputActionsIteratorOpt.isDefined,\n        \"addToCommitStats must be called before finalizing commit stats\")\n      assert(\n        !inputActionsIteratorOpt.get.hasNext,\n        \"The actions iterator must be consumed before finalizing commit stats\")\n    }\n\n    /**\n     * Takes in an iterator of actions and processes them to compute commit stats.\n     * The commit stats are computed as a side effect of the iterator processing\n     * and are only populated after the returned iterator is fully consumed.\n     * Note that this function will not consume the input iterator.\n     * @param actions An iterator of actions that are being committed in this transaction.\n     * @return An iterator of actions. This is will return the same actions as the\n     *         input iterator, but with the commit stats computed as a side effect.\n     */\n    def addToCommitStats(actions: Iterator[Action]): Iterator[Action] = {\n      assert(inputActionsIteratorOpt.isEmpty,\n        \"addToCommitStats should only be called once per transaction\")\n      inputActionsIteratorOpt = Some(actions)\n      actions.map { action =>\n        numActions += 1\n        action match {\n          case a: AddFile =>\n            numAdd += 1\n            if (a.pathAsUri.isAbsolute) numAbsolutePaths += 1\n            partitionsAdded += a.partitionValues\n            if (a.dataChange) bytesNew += a.size\n          case r: RemoveFile =>\n            numRemove += 1\n          case c: AddCDCFile =>\n            numCdcFiles += 1\n            cdcBytesNew += c.size\n          case _: SetTransaction =>\n            numSetTransaction += 1\n          case _: DomainMetadata =>\n            numOfDomainMetadatas += 1\n          case m: Metadata =>\n            newMetadataOpt = Some(m)\n          case p: Protocol =>\n            newProtocolOpt = Some(p)\n          case _ => ()\n        }\n        action\n      }\n    }\n\n    // scalastyle:off argcount\n    /**\n     * Finalizes the commit stats and emits them as a Delta event.\n     * This must be called after\n     * 1. [[addToCommitStats]] has been called on the actions iterator AND\n     * 2. after the actions iterator returned by [[addToCommitStats]]\n     *  has been fully consumed.\n     * @param spark The Spark session.\n     * @param attemptVersion The version of the table which is being written to the log.\n     * @param startVersion The version of the table which was read at the start of the transaction.\n     * @param commitDurationMs The duration of the commit in milliseconds.\n     * @param fsWriteDurationMs The duration of the file system write of the commit in milliseconds.\n     * @param txnExecutionTimeMs The total execution time of the transaction in milliseconds.\n     * @param stateReconstructionDurationMs The duration of post commit snapshot construction in\n     *   milliseconds.\n     * @param postCommitSnapshot The snapshot constructed after the commit.\n     * @param computedNeedsCheckpoint Whether a checkpoint needs to be created after this commit.\n     *  Computed in `setNeedsCheckpoint`.\n     * @param isolationLevel The isolation level used for this transaction.\n     * @param commitSizeBytes The total size of the commit in bytes, computed as the sum of\n     *  the JSON sizes of all actions in the commit.\n     * @param commitInfo The commit info for this transaction.\n     * @return A HashSet containing the partitions that were added in the transaction.\n     */\n    def finalizeAndEmitCommitStats(\n        spark: SparkSession,\n        attemptVersion: Long,\n        startVersion: Long,\n        commitDurationMs: Long,\n        fsWriteDurationMs: Long,\n        txnExecutionTimeMs: Long,\n        stateReconstructionDurationMs: Long,\n        postCommitSnapshot: Snapshot,\n        computedNeedsCheckpoint: Boolean,\n        isolationLevel: IsolationLevel,\n        commitInfoOpt: Option[CommitInfo],\n        commitSizeBytes: Long): Unit = {\n      assertStateBeforeFinalization()\n\n      val doCollectCommitStats =\n        computedNeedsCheckpoint ||\n          spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_FORCE_ALL_COMMIT_STATS)\n      // Stats that force an expensive snapshot state reconstruction:\n      val numFilesTotal = if (doCollectCommitStats) postCommitSnapshot.numOfFiles else -1L\n      val sizeInBytesTotal = if (doCollectCommitStats) postCommitSnapshot.sizeInBytes else -1L\n      val commitInfoToEmit = commitInfoOpt match {\n        case Some(ci) => ci.copy(readVersion = None, isolationLevel = None)\n        case None => null\n      }\n      val stats = CommitStats(\n        startVersion = startVersion,\n        commitVersion = attemptVersion,\n        readVersion = postCommitSnapshot.version,\n        txnDurationMs = txnExecutionTimeMs,\n        commitDurationMs = commitDurationMs,\n        fsWriteDurationMs = fsWriteDurationMs,\n        stateReconstructionDurationMs = stateReconstructionDurationMs,\n        numAdd = numAdd,\n        numRemove = numRemove,\n        numSetTransaction = numSetTransaction,\n        bytesNew = bytesNew,\n        numFilesTotal = numFilesTotal,\n        sizeInBytesTotal = sizeInBytesTotal,\n        numCdcFiles = numCdcFiles,\n        cdcBytesNew = cdcBytesNew,\n        protocol = postCommitSnapshot.protocol,\n        commitSizeBytes = commitSizeBytes,\n        checkpointSizeBytes = postCommitSnapshot.checkpointSizeInBytes(),\n        totalCommitsSizeSinceLastCheckpoint = postCommitSnapshot.deltaFileSizeInBytes(),\n        checkpointAttempt = computedNeedsCheckpoint,\n        info = commitInfoToEmit,\n        newMetadata = newMetadataOpt,\n        numAbsolutePathsInAdd = numAbsolutePaths,\n        numDistinctPartitionsInAdd = partitionsAdded.size,\n        numPartitionColumnsInTable = postCommitSnapshot.metadata.partitionColumns.size,\n        isolationLevel = isolationLevel.toString,\n        coordinatedCommitsInfo = createCoordinatedCommitsStats(newProtocolOpt),\n        numOfDomainMetadatas = numOfDomainMetadatas,\n        txnId = Some(txnId))\n      recordDeltaEvent(deltaLog, DeltaLogging.DELTA_COMMIT_STATS_OPTYPE, data = stats)\n    }\n\n    /**\n     * Returns the partitions that were added in this transaction.\n     */\n    def getPartitionsAddedByTransaction: mutable.HashSet[Map[String, String]] = {\n      assertStateBeforeFinalization()\n      partitionsAdded\n    }\n\n    /**\n     * Returns the number of actions that were added in this transaction.\n     */\n    def getNumActions: Int = {\n      assertStateBeforeFinalization()\n      numActions\n    }\n  }\n  // scalastyle:on argcount\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/Utils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.util.Random\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog}\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{functions, Column, Dataset}\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.ElementAt\n\n/**\n * Various utility methods used by Delta.\n */\nobject Utils {\n\n  /** Measures the time taken by function `f` */\n  def timedMs[T](f: => T): (T, Long) = {\n    val start = System.currentTimeMillis()\n    val res = f\n    val duration = System.currentTimeMillis() - start\n    (res, duration)\n  }\n\n  /** Returns the length of the random prefix to use for the data files of a Delta table. */\n  def getRandomPrefixLength(metadata: Metadata): Int = {\n    if (DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(metadata)) {\n      DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(metadata)\n    } else {\n      0\n    }\n  }\n\n  /** Generates a string created of `randomPrefixLength` alphanumeric characters. */\n  def getRandomPrefix(numChars: Int): String = {\n    Random.alphanumeric.take(numChars).mkString\n  }\n\n  /**\n   * Construct a delta log from either the catalog table or a path.\n   *\n   * If catalogTableOpt is defined, use it to construct the delta log; otherwise, fall back to use\n   * path-based delta log construction.\n   */\n  def getDeltaLogFromTableOrPath(\n      sparkSession: SparkSession,\n      catalogTableOpt: Option[CatalogTable],\n      path: Path,\n      options: Map[String, String] = Map.empty): DeltaLog = {\n    catalogTableOpt\n      .map(catalogTable => DeltaLog.forTable(sparkSession, catalogTable, options))\n      .getOrElse(DeltaLog.forTable(sparkSession, path, options))\n  }\n\n  /**\n   * Indicates whether Delta is currently running unit tests.\n   */\n  def isTesting: Boolean = {\n    System.getenv(\"DELTA_TESTING\") != null\n  }\n\n  /**\n   * Returns value for the given key in value if column is a map and the key is present, NULL\n   * otherwise.\n   */\n  def try_element_at(mapColumn: Column, key: Any): Column = {\n    functions.try_element_at(mapColumn, functions.lit(key))\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/threads/DeltaThreadPool.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util.threads\n\nimport java.util.concurrent._\n\nimport scala.concurrent.duration.Duration\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.DeltaErrors\nimport org.apache.spark.sql.delta.metering.DeltaLogging\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.util.ThreadUtils\nimport org.apache.spark.util.ThreadUtils.namedThreadFactory\n\n/** A wrapper for [[ThreadPoolExecutor]] whose tasks run with the caller's [[SparkSession]]. */\nprivate[delta] class DeltaThreadPool(tpe: ThreadPoolExecutor) {\n  def getActiveCount: Int = tpe.getActiveCount\n  def getMaximumPoolSize: Int = tpe.getMaximumPoolSize\n\n  /** Submits a task for execution and returns a [[Future]] representing that task. */\n  def submit[T](spark: SparkSession)(body: => T): Future[T] = {\n    tpe.submit { () => spark.withActive(body) }\n  }\n\n  /**\n   *  Executes `f` on each element of `items` as a task and returns the result.\n   *  Throws [[SparkException]] on error.\n   */\n  def parallelMap[T, R](\n      spark: SparkSession,\n      items: Iterable[T])(\n      f: T => R): Iterable[R] = {\n    // Materialize a list of futures, to ensure they all got submitted before we start waiting.\n    val futures = items.map(i => submit(spark)(f(i))).toList\n    futures.map(f => ThreadUtils.awaitResult(f, Duration.Inf)).toSeq\n  }\n\n  def submitNonFateSharing[T](f: SparkSession => T): NonFateSharingFuture[T] =\n    new NonFateSharingFuture(this)(f)\n}\n\n\n/** Convenience constructor that creates a [[ThreadPoolExecutor]] with sensible defaults. */\nprivate[delta] object DeltaThreadPool {\n  def apply(prefix: String, numThreads: Int): DeltaThreadPool =\n    new DeltaThreadPool(newDaemonCachedThreadPool(prefix, numThreads))\n\n  /**\n   * Create a cached thread pool whose max number of threads is `maxThreadNumber`. Thread names\n   * are formatted as prefix-ID, where ID is a unique, sequentially assigned integer.\n   */\n  def newDaemonCachedThreadPool(\n      prefix: String,\n      maxThreadNumber: Int): ThreadPoolExecutor = {\n    val keepAliveSeconds = 60\n    val queueSize = Integer.MAX_VALUE\n    val threadFactory = namedThreadFactory(prefix)\n    val threadPool = new SparkThreadLocalForwardingThreadPoolExecutor(\n      maxThreadNumber, // corePoolSize: the max number of threads to create before queuing the tasks\n      maxThreadNumber, // maximumPoolSize: because we use LinkedBlockingDeque, this one is not used\n      keepAliveSeconds,\n      TimeUnit.SECONDS,\n      new LinkedBlockingQueue[Runnable](queueSize),\n      threadFactory)\n    threadPool.allowCoreThreadTimeOut(true)\n    threadPool\n  }\n}\n\n/**\n * A future invocation of `f` which avoids \"fate sharing\" of errors, in case multiple threads could\n * wait on the future's result.\n *\n * The future is only launched if a [[SparkSession]] is available.\n *\n * If the future succeeds, any thread can consume the result.\n *\n * If the future fails, threads will just invoke `f` directly -- except that fatal errors will\n * propagate (once) if the caller is from the same [[SparkSession]] that created the future.\n */\nclass NonFateSharingFuture[T](pool: DeltaThreadPool)(f: SparkSession => T)\n  extends DeltaLogging {\n\n  // Submit `f` as a future if a spark session is available\n  @volatile private var futureOpt = SparkSession.getActiveSession.map { spark =>\n    spark -> pool.submit(spark) { f(spark) }\n  }\n\n  def get(timeout: Duration): T = {\n    // Prefer to get a prefetched result from the future, but never fail because of it.\n    val futureResult = futureOpt.flatMap { case (ownerSession, future) =>\n      try {\n        val result = Some(ThreadUtils.awaitResult(future, timeout))\n        // no reason to keep the reference to the calling session anymore\n        futureOpt = Some(null, future)\n        result\n      } catch {\n        // NOTE: ThreadUtils.awaitResult wraps all non-fatal exceptions other than TimeoutException\n        // with SparkException. Meanwhile, Java Future.get only throws four exceptions:\n        // ExecutionException (non-fatal, wrapped, and itself wraps any Throwable from the task\n        // itself), CancellationException (non-fatal, wrapped), InterruptedException (fatal, not\n        // wrapped), and TimeoutException (non-fatal, but not wrapped). Thus, any \"normal\" failure\n        // of the future will surface as SparkException(ExecutionException(OriginalException)).\n        case outer: SparkException => outer.getCause match {\n          case e: CancellationException =>\n            logWarning(log\"Future was cancelled\")\n            futureOpt = None\n            None\n          case inner: ExecutionException if inner.getCause != null => inner.getCause match {\n            case NonFatal(e) =>\n              logWarning(log\"Future threw non-fatal exception\", e)\n              futureOpt = None\n              None\n            case e: Throwable =>\n              logWarning(log\"Future threw fatal error\", e)\n              if (ownerSession eq SparkSession.active) {\n                futureOpt = None\n                throw e\n              }\n              None\n          }\n        }\n        case e: TimeoutException =>\n          logWarning(log\"Timed out waiting for future\")\n          None\n        case NonFatal(e) =>\n          logWarning(log\"Unknown failure while waiting for future\", e)\n          None\n      }\n    }\n\n    futureResult.getOrElse {\n      // Future missing or failed, so fall back to direct execution.\n      SparkSession.getActiveSession match {\n        case Some(spark) => f(spark)\n        case _ => throw DeltaErrors.sparkSessionNotSetException()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/util/threads/SparkThreadLocalForwardingThreadPoolExecutor.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util.threads\n\nimport java.util.Properties\nimport java.util.concurrent._\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.logging.DeltaLogKeys\n\nimport org.apache.spark.{SparkContext, TaskContext}\nimport org.apache.spark.internal.{Logging, MDC}\nimport org.apache.spark.util.{Utils => SparkUtils}\n\n/**\n * Implementation of ThreadPoolExecutor that captures the Spark ThreadLocals present at submit time\n * and inserts them into the thread before executing the provided runnable.\n */\nclass SparkThreadLocalForwardingThreadPoolExecutor(\n    corePoolSize: Int,\n    maximumPoolSize: Int,\n    keepAliveTime: Long,\n    unit: TimeUnit,\n    workQueue: BlockingQueue[Runnable],\n    threadFactory: ThreadFactory,\n    rejectedExecutionHandler: RejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy)\n  extends ThreadPoolExecutor(\n    corePoolSize, maximumPoolSize, keepAliveTime,\n    unit, workQueue, threadFactory, rejectedExecutionHandler) {\n\n  override def execute(command: Runnable): Unit =\n    super.execute(new SparkThreadLocalCapturingRunnable(command))\n}\n\n\ntrait SparkThreadLocalCapturingHelper extends Logging {\n  // At the time of creating this instance we capture the task context and command context.\n  val capturedTaskContext = TaskContext.get()\n  val sparkContext = SparkContext.getActive\n  // Capture an immutable threadsafe snapshot of the current local properties\n  val capturedProperties = sparkContext\n    .map(sc => CapturedSparkThreadLocals.toValuesArray(\n      SparkUtils.cloneProperties(sc.getLocalProperties)))\n\n  def runWithCaptured[T](body: => T): T = {\n    // Save the previous contexts, overwrite them with the captured contexts, and then restore the\n    // previous when execution completes.\n    // This has the unfortunate side effect of writing nulls to these thread locals if they were\n    // empty beforehand.\n    val previousTaskContext = TaskContext.get()\n    val previousProperties = sparkContext.map(_.getLocalProperties)\n\n    TaskContext.setTaskContext(capturedTaskContext)\n    for {\n      p <- capturedProperties\n      sc <- sparkContext\n    } {\n      sc.setLocalProperties(CapturedSparkThreadLocals.toProperties(p))\n    }\n\n    try {\n      body\n    } catch {\n      case t: Throwable =>\n        logError(log\"Exception in thread \" +\n          log\"${MDC(DeltaLogKeys.THREAD_NAME, Thread.currentThread().getName)}\", t)\n        throw t\n    } finally {\n      TaskContext.setTaskContext(previousTaskContext)\n      for {\n        p <- previousProperties\n        sc <- sparkContext\n      } {\n        sc.setLocalProperties(p)\n      }\n    }\n  }\n}\n\nclass CapturedSparkThreadLocals extends SparkThreadLocalCapturingHelper\n\nobject CapturedSparkThreadLocals {\n  def apply(): CapturedSparkThreadLocals = {\n    new CapturedSparkThreadLocals()\n  }\n\n  def toProperties(props: Array[(String, String)]): Properties = {\n    val resultProps = new Properties()\n    for ((key, value) <- props) {\n      resultProps.put(key, value)\n    }\n    resultProps\n  }\n\n  def toValuesArray(props: Properties): Array[(String, String)] = {\n    props.asScala.toArray\n  }\n\n}\n\nclass SparkThreadLocalCapturingRunnable(runnable: Runnable)\n    extends Runnable with SparkThreadLocalCapturingHelper {\n  override def run(): Unit = {\n    runWithCaptured(runnable.run())\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/v2/interop/AbstractCommitInfo.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.v2.interop\n\n/**\n * Abstract trait for commit info actions in Delta. This trait provides a common\n * abstraction that can be implemented by both Spark's V1 CommitInfo and Kernel's CommitInfo\n * in V2 connector. The V2 connector will implement adapters for reusing V1 utilities.\n */\ntrait AbstractCommitInfo {\n\n  /**\n   * Get the in-commit timestamp of the commit as milliseconds after the epoch.\n   * This is the timestamp recorded in the commit itself, used for time travel.\n   */\n  def getCommitTimestamp: Long\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/v2/interop/AbstractMetadata.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.v2.interop\n\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Abstract trait for metadata actions in Delta. This trait provides a common\n * abstraction that can be implemented by both Spark's V1 Metadata and Kernel's MetadataV2\n * in V2 connector. The V2 connector will implement adapters for reusing V1 utilities.\n */\ntrait AbstractMetadata {\n\n  /** A unique table identifier. */\n  def id: String\n\n  /** User-specified table identifier. */\n  def name: String\n\n  /** User-specified table description. */\n  def description: String\n\n  /** Returns the schema as a [[StructType]]. */\n  def schema: StructType\n\n  /** List of partition column names. */\n  def partitionColumns: Seq[String]\n\n  /** The table properties/configuration defined on the table. */\n  def configuration: Map[String, String]\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/v2/interop/AbstractProtocol.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.v2.interop\n\n/**\n * Abstract trait for protocol actions in Delta. This trait provides a common\n * abstraction that can be implemented by both Spark's V1 Protocol and Kernel's Protocol\n * in V2 connector. The V2 connector will implement adapters for reusing V1 utilities.\n */\ntrait AbstractProtocol {\n\n  /** The minimum reader version required to read the table. */\n  def minReaderVersion: Int\n\n  /** The minimum writer version required to write to the table. */\n  def minWriterVersion: Int\n\n  /**\n   * The reader features that need to be supported to read the table.\n   * Returns None if table features are not enabled for readers.\n   */\n  def readerFeatures: Option[Set[String]]\n\n  /**\n   * The writer features that need to be supported to write to the table.\n   * Returns None if table features are not enabled for writers.\n   */\n  def writerFeatures: Option[Set[String]]\n}\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/v2/interop/README.md",
    "content": "# V2 Connector Interop\n\nThis package contains abstract traits that enable interoperability between the Delta Spark connectors.\n\n## Purpose\n\nThese abstractions allow V1 utilities to be reused by the V2 connector through adapters.\n\n## Usage\n\nFor reusing V1 connector code in V2:\n\n1. **Refactor V1 utilities** to depend on abstract traits (e.g., `AbstractMetadata`, `AbstractProtocol`) instead of the concrete V1 implementations.\n\n2. **Implement adapters in V2 connector** that extend these abstractions, wrapping Kernel's action types (e.g., Kernel's `Metadata` → `AbstractMetadata`).\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/delta/zorder/ZCubeInfo.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.zorder\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.{DeltaErrors, DeltaLog, SnapshotIsolation}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.actions.AddFile.Tags.{ZCUBE_ID, ZCUBE_ZORDER_BY, ZCUBE_ZORDER_CURVE}\nimport org.apache.spark.sql.delta.commands.DeltaCommand\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo.ZCubeID\n\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal}\n\n\n/**\n * [[ZCube]] identifying information.\n * @param zCubeID unique identifier of a OPTIMIZE ZORDER BY command run\n * @param zOrderBy list of ZORDER BY columns for the run\n * @note This piece of info could be put in table Metadata or taken from CommitInfo\n *       but is currently inlined here for simplicity.\n */\ncase class ZCubeInfo(zCubeID: ZCubeID, zOrderBy: Seq[String]) {\n  require(zOrderBy.nonEmpty)\n}\n\nobject ZCubeInfo extends DeltaCommand {\n  type ZCubeID = String // Could be UUID, but there's no implicit encoding for that.\n\n  /**\n   * Preferred way of creating a ZCubeInfo for a new ZCube.\n   * Automatically generates a unique zCubeID.\n   */\n  def apply(zOrderBy: Seq[String]): ZCubeInfo = {\n    val zCubeID = UUID.randomUUID.toString\n    ZCubeInfo(zCubeID, zOrderBy)\n  }\n\n  private val ZCUBE_ID_KEY = AddFile.tag(ZCUBE_ID)\n  private val ZORDER_BY_KEY = AddFile.tag(ZCUBE_ZORDER_BY)\n  private val ZORDER_CURVE = AddFile.tag(ZCUBE_ZORDER_CURVE)\n\n  /**\n   * Serializes the given `zCubeInfo` to a Map[String, String] that can be used as or merged into\n   * [[AddFile.tags]].\n   */\n  def toAddFileTags(zCubeInfo: ZCubeInfo): Map[String, String] = {\n    Map(\n      ZCUBE_ID_KEY -> zCubeInfo.zCubeID,\n      ZORDER_BY_KEY -> JsonUtils.toJson(zCubeInfo.zOrderBy))\n  }\n\n  /**\n   * Deserializes a `ZCubeInfo` object from an [[AddFile.tags]] map, if present.\n   */\n  def fromAddFileTags(tags: Map[String, String]): Option[ZCubeInfo] = {\n    for {\n      zCubeID <- tags.get(ZCUBE_ID_KEY)\n      zOrderByColsAsJson <- tags.get(ZORDER_BY_KEY)\n    } yield {\n      val zOrderByCols = JsonUtils.fromJson[Seq[String]](zOrderByColsAsJson)\n      ZCubeInfo(zCubeID, zOrderByCols)\n    }\n  }\n\n  /**\n   * If the given file was written by an OPTIMIZE ZORDER BY job,\n   * return the corresponding [[ZCubeInfo]]. Otherwise return [[None]].\n   */\n  def getForFile(file: AddFile): Option[ZCubeInfo] = {\n    for {\n      tags <- Option(file.tags)\n      zCubeInfo <- ZCubeInfo.fromAddFileTags(tags)\n    } yield {\n      zCubeInfo\n    }\n  }\n\n  /**\n   * Update the given file's metadata to make it part of the given zCubeInfo.\n   */\n  def setForFile(file: AddFile, zCubeInfo: ZCubeInfo): AddFile = {\n    val newTags = file.tagsOrEmpty ++ ZCubeInfo.toAddFileTags(zCubeInfo)\n    file.copy(tags = newTags)\n  }\n\n  /**\n   * Clears the ZCubeInfo metadata of the given file to make it appear as unoptimized.\n   */\n  def unsetForFile(file: AddFile): AddFile = {\n    val oldTags = file.tags\n    val newTags = if (oldTags == null) null else oldTags --\n      Seq(ZCUBE_ID_KEY, ZORDER_BY_KEY, ZORDER_CURVE)\n    file.copy(tags = newTags)\n  }\n}\n\n\n"
  },
  {
    "path": "spark/src/main/scala/org/apache/spark/sql/util/ScalaExtensions.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.util\n\n/** Extension utility classes for built-in Scala functionality. */\nobject ScalaExtensions {\n\n  implicit class OptionExt[T](opt: Option[T]) {\n    /**\n     * Execute `f` on the content of `opt`, if `opt.isDefined`.\n     *\n     * This is basically a rename of `opt.foreach`, but with better readability.\n     */\n    def ifDefined(f: T => Unit): Unit = opt.foreach(f)\n  }\n\n  implicit class OptionExtCompanion(opt: Option.type) {\n    /**\n     * When a given condition is true, evaluates the a argument and returns Some(a).\n     * When the condition is false, a is not evaluated and None is returned.\n     */\n    def when[A](cond: Boolean)(a: => A): Option[A] = if (cond) Some(a) else None\n\n    /**\n     * When a given condition is false, evaluates the a argument and returns Some(a).\n     * When the condition is true, a is not evaluated and None is returned.\n     */\n    def whenNot[A](cond: Boolean)(a: => A): Option[A] = if (!cond) Some(a) else None\n\n    /** Sum up all the `options`, substituting `default` for each `None`. */\n    def sum[N : Numeric](default: N)(options: Option[N]*): N =\n      options.map(_.getOrElse(default)).sum\n  }\n\n  implicit class AnyExt(any: Any) {\n    /**\n     * Applies the partial function to any if it is defined and ignores the result if any.\n     */\n    def condDo(pf: PartialFunction[Any, Unit]): Unit = scala.PartialFunction.condOpt(any)(pf)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/CreateDeltaTableLikeShims.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.delta.DeltaOptions\n\nobject CreateDeltaTableLikeShims {\n\n  /**\n   * Differentiate between DataFrameWriterV1 and V2 so that we can decide\n   * what to do with table metadata. In DataFrameWriterV1, mode(\"overwrite\").saveAsTable,\n   * behaves as a CreateOrReplace table, but we have asked for \"overwriteSchema\" as an\n   * explicit option to overwrite partitioning or schema information. With DataFrameWriterV2,\n   * the behavior asked for by the user is clearer: .createOrReplace(), which means that we\n   * should overwrite schema and/or partitioning. Therefore we have this hack.\n   *\n   * In Spark 4.0 this horrible hack depends on the stack trace, where eager execution of the\n   * command pointed to the calling API.\n   *\n   * TODO: Shim no longer needed once spark-4.0 is removed.\n   */\n  def isV1WriterSaveAsTableOverwrite(options: DeltaOptions, mode: SaveMode): Boolean = {\n    Thread.currentThread().getStackTrace.exists(_.toString.contains(\n      classOf[org.apache.spark.sql.classic.DataFrameWriter[_]].getCanonicalName + \".\")) &&\n    mode == SaveMode.Overwrite\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/DataSourceV2RelationShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.execution.datasources.v2\n\nimport org.apache.spark.sql.catalyst.expressions.AttributeReference\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.connector.catalog.{CatalogPlugin, Identifier, Table}\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Shim for DataSourceV2Relation to handle API changes between Spark versions.\n * In Spark 4.0, DataSourceV2Relation has 5 constructor parameters.\n */\nobject DataSourceV2RelationShim {\n\n  /**\n   * Main extractor for DataSourceV2Relation that works across Spark versions.\n   * Returns the common fields that exist in all versions.\n   */\n  def unapply(plan: LogicalPlan): Option[\n    (Table, Seq[AttributeReference],\n      Option[CatalogPlugin],\n      Option[Identifier],\n      CaseInsensitiveStringMap)] = {\n    plan match {\n      case r: DataSourceV2Relation =>\n        Some((r.table, r.output, r.catalog, r.identifier, r.options))\n      case _ => None\n    }\n  }\n}\n\n/**\n * Simplified extractor when only table and options are needed.\n */\nobject DataSourceV2RelationSimple {\n  def unapply(plan: LogicalPlan): Option[(Table, CaseInsensitiveStringMap)] = {\n    plan match {\n      case r: DataSourceV2Relation =>\n        Some((r.table, r.options))\n      case _ => None\n    }\n  }\n}\n\n/**\n * Extractor for cases that only need the table.\n */\nobject DataSourceV2RelationTable {\n  def unapply(plan: LogicalPlan): Option[Table] = {\n    plan match {\n      case r: DataSourceV2Relation => Some(r.table)\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/DateTimeExpressionShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.catalyst.expressions\n\n/**\n * Shim for date/time expressions in Spark 4.0\n */\nobject DateTimeExpressionShims {\n  /**\n   * Check if the given expression is a date/time arithmetic expression\n   */\n  def isDateTimeArithmeticExpression(expr: Expression): Boolean = {\n    expr match {\n      case _: AddMonthsBase | _: DateAdd | _: DateAddInterval | _: DateDiff | _: DateSub |\n           _: DatetimeSub | _: LastDay | _: MonthsBetween | _: NextDay | _: SubtractDates |\n           _: SubtractTimestamps | _: TimeAdd | _: TimestampAdd | _: TimestampAddYMInterval |\n           _: TimestampDiff | _: TruncInstant => true\n      case _ => false\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/LogKeyShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.logging\n\nimport org.apache.spark.internal.LogKey\n\n/**\n * Shim for LogKey to handle API changes between Spark versions.\n * In Spark 4.0, LogKey is a Scala trait with a default implementation of `name`.\n *\n * DeltaLogKey is just a trait that extends LogKey, allowing case objects to extend it.\n */\ntrait DeltaLogKey extends LogKey\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/ParquetFooterReaderShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.execution.datasources.parquet\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\n\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetFooterReader\n\nobject ParquetFooterReaderShims {\n  def readParquetFooter(\n    conf: Configuration,\n    status: FileStatus,\n    filter: ParquetMetadataConverter.MetadataFilter) : ParquetMetadata = {\n    ParquetFooterReader.readFooter(conf, status, filter)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/ParseExceptionShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.parser\n\nimport org.antlr.v4.runtime.ParserRuleContext\n\nimport org.apache.spark.sql.catalyst.parser.{ParseException, ParserUtils}\nimport org.apache.spark.sql.catalyst.trees.Origin\nimport org.apache.spark.sql.delta.DeltaThrowable\n\n/**\n * DeltaParseException for Spark 4.0 and earlier.\n * In these versions, ParseException takes both start and stop Origin parameters.\n */\nclass DeltaParseException(\n    ctx: ParserRuleContext,\n    errorClass: String,\n    messageParameters: Map[String, String] = Map.empty)\n  extends ParseException(\n      Option(ParserUtils.command(ctx)),\n      ParserUtils.position(ctx.getStart),\n      ParserUtils.position(ctx.getStop),  // In Spark 4.0, we have the stop parameter\n      errorClass,\n      messageParameters\n    ) with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n}\n\n/**\n * Shim for ParseException to handle API changes between Spark versions.\n * In Spark 4.0 and earlier, ParseException has separate start and stop parameters.\n */\nobject ParseExceptionShims {\n\n  /**\n   * Create a ParseException with the appropriate constructor for this Spark version.\n   * In Spark 4.0, we use both start and stop Origin parameters.\n   */\n  def createParseException(\n      command: Option[String],\n      start: Origin,\n      stop: Origin,\n      errorClass: String,\n      messageParameters: Map[String, String]): ParseException = {\n    new ParseException(command, start, stop, errorClass, messageParameters)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/QualifiedColTypeShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.connector.catalog.TableChange.AddColumn\n\n/**\n * In Spark 4.0 QualifiedColType stores `default` as a String\n */\nobject QualifiedColTypeShims {\n\n  def getDefaultValueArgFromAddColumn(col: AddColumn): Option[String] = {\n    Option(col.defaultValue()).map(_.getSql())\n  }\n\n  def getDefaultValueStr(col: QualifiedColType): Option[String] = {\n    col.default\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/RelocatedStreamingClassesShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.execution.streaming.{OffsetSeqMetadata, WatermarkPropagator}\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types.StructType\n\nimport org.apache.spark.sql.execution.streaming.{\n  CheckpointFileManager => CheckpointFileManagerShim,\n  IncrementalExecution => IncrementalExecutionShim,\n  MetadataLogFileIndex => MetadataLogFileIndexShim,\n  StreamExecution => StreamExecutionShim,\n  StreamingRelation => StreamingRelationShim,\n  MetadataVersionUtil => MetadataVersionUtilShim\n}\nimport org.apache.spark.sql.execution.streaming.{FileStreamSink => FileStreamSinkShim}\n\nobject Relocated {\n  type CheckpointFileManager = CheckpointFileManagerShim\n  val CheckpointFileManager: CheckpointFileManagerShim.type = CheckpointFileManagerShim\n\n  type IncrementalExecution = IncrementalExecutionShim\n  // scalastyle:off argcount\n  def createIncrementalExecution(\n      sparkSession: org.apache.spark.sql.classic.SparkSession,\n      logicalPlan: LogicalPlan,\n      outputMode: OutputMode,\n      checkpointLocation: String,\n      queryId: UUID,\n      runId: UUID,\n      currentBatchId: Long,\n      prevOffsetSeqMetadata: Option[OffsetSeqMetadata],\n      offsetSeqMetadata: OffsetSeqMetadata,\n      watermarkPropagator: WatermarkPropagator,\n      isFirstBatch: Boolean): IncrementalExecution = {\n    // scalastyle:on argcount\n    new IncrementalExecutionShim(\n      sparkSession,\n      logicalPlan,\n      outputMode,\n      checkpointLocation,\n      queryId,\n      runId,\n      currentBatchId,\n      prevOffsetSeqMetadata,\n      offsetSeqMetadata,\n      watermarkPropagator,\n      isFirstBatch)\n  }\n\n  type StreamingRelation = StreamingRelationShim\n  val StreamingRelation: StreamingRelationShim.type = StreamingRelationShim\n\n  type MetadataLogFileIndex = MetadataLogFileIndexShim\n  def createMetadataLogFileIndex(\n      sparkSession: org.apache.spark.sql.SparkSession,\n      path: Path,\n      options: Map[String, String],\n      userSpecifiedSchema: Option[StructType]): MetadataLogFileIndex = {\n    new MetadataLogFileIndexShim(sparkSession, path, options, userSpecifiedSchema)\n  }\n\n  type FileStreamSink = FileStreamSinkShim\n  val FileStreamSink: FileStreamSinkShim.type = FileStreamSinkShim\n\n  type StreamExecution = StreamExecutionShim\n  val StreamExecution: StreamExecutionShim.type = StreamExecutionShim\n\n  val MetadataVersionUtil: MetadataVersionUtilShim.type = MetadataVersionUtilShim\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/SupportsV1OverwriteWithSaveAsTable.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.connector.catalog\n\n/* Interface exists in Spark 4.1+. Is noop for Spark 4.0. */\ntrait SupportsV1OverwriteWithSaveAsTable extends TableProvider {\n  def addV1OverwriteWithSaveAsTableOption(): Boolean = true\n}\n\nobject SupportsV1OverwriteWithSaveAsTable {\n  val OPTION_NAME: String = \"__v1_save_as_table_overwrite\"\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/VariantShreddingShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.shims\n\n/**\n * Shim for variant shredding configs to handle API changes between Spark versions.\n * In Spark 4.0, VARIANT_INFER_SHREDDING_SCHEMA config does not exist.\n *\n * This shim provides a way to conditionally add the config to the options map\n * when writing files.\n */\nobject VariantShreddingShims {\n  /**\n   * Returns a Map containing variant shredding related configs for file writing.\n   * In Spark 4.0, this returns an empty map since the config doesn't exist.\n   */\n  def getVariantInferShreddingSchemaOptions(enableVariantShredding: Boolean)\n    : Map[String, String] = {\n    // In Spark 4.0, VARIANT_INFER_SHREDDING_SCHEMA does not exist, so return empty map\n    Map.empty[String, String]\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.0/ViewShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * Shim for View to handle API changes between Spark versions.\n * In Spark 4.0 and earlier, View has fewer constructor parameters.\n */\nobject ViewShims {\n\n  /**\n   * Extractor that matches View(desc, true, child) pattern.\n   * Used in DeltaViewHelper for matching temp views with a specific structure.\n   */\n  object TempViewWithChild {\n    def unapply(plan: LogicalPlan): Option[(CatalogTable, LogicalPlan)] = plan match {\n      case View(desc, isTempView, child) if isTempView => Some((desc, child))\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/CreateDeltaTableLikeShims.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.delta.DeltaOptions\n\nobject CreateDeltaTableLikeShims {\n\n  /**\n   * Differentiate between DataFrameWriterV1 and V2 so that we can decide\n   * what to do with table metadata. In DataFrameWriterV1, mode(\"overwrite\").saveAsTable,\n   * behaves as a CreateOrReplace table, but we have asked for \"overwriteSchema\" as an\n   * explicit option to overwrite partitioning or schema information. With DataFrameWriterV2,\n   * the behavior asked for by the user is clearer: .createOrReplace(), which means that we\n   * should overwrite schema and/or partitioning. Therefore we have this hack.\n   *\n   * In Spark 4.1, DataFrameWriter provides the option \"__v1_save_as_table_overwrite\", because\n   * the stack trace does not indicate the calling API anymore in connect mode - planning and\n   * execution has been separated.\n   *\n   * TODO: Shim no longer needed once spark-4.0 is removed.\n   */\n  def isV1WriterSaveAsTableOverwrite(options: DeltaOptions, mode: SaveMode): Boolean = {\n    // Note: Spark is setting this only for SaveMode.Overwrite anyway, but we double check.\n    // The 4.0 shim relies on stack trace analysis instead, so it has to check.\n    // After 4.0 is dropped, we can simplify.\n    options.isDataFrameWriterV1SaveAsTableOverwrite && mode == SaveMode.Overwrite\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/DataSourceV2RelationShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.execution.datasources.v2\n\nimport org.apache.spark.sql.catalyst.expressions.AttributeReference\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.connector.catalog.{CatalogPlugin, Identifier, Table}\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Shim for DataSourceV2Relation to handle API changes between Spark versions.\n * In Spark 4.1, DataSourceV2Relation has 6 constructor parameters (added an extra parameter).\n */\nobject DataSourceV2RelationShim {\n\n  /**\n   * Main extractor for DataSourceV2Relation that works across Spark versions.\n   * Returns the common fields that exist in all versions.\n   */\n  def unapply(plan: LogicalPlan): Option[(\n    Table, Seq[AttributeReference],\n      Option[CatalogPlugin],\n      Option[Identifier],\n      CaseInsensitiveStringMap)] = {\n    plan match {\n      case r: DataSourceV2Relation =>\n        Some((r.table, r.output, r.catalog, r.identifier, r.options))\n      case _ => None\n    }\n  }\n}\n\n/**\n * Simplified extractor when only table and options are needed.\n */\nobject DataSourceV2RelationSimple {\n  def unapply(plan: LogicalPlan): Option[(Table, CaseInsensitiveStringMap)] = {\n    plan match {\n      case r: DataSourceV2Relation =>\n        Some((r.table, r.options))\n      case _ => None\n    }\n  }\n}\n\n/**\n * Extractor for cases that only need the table.\n */\nobject DataSourceV2RelationTable {\n  def unapply(plan: LogicalPlan): Option[Table] = {\n    plan match {\n      case r: DataSourceV2Relation => Some(r.table)\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/DateTimeExpressionShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.catalyst.expressions\n\n/**\n * Shim for date/time expressions in Spark 4.1\n * Note: TimeAdd is removed in Spark 4.1\n */\nobject DateTimeExpressionShims {\n  /**\n   * Check if the given expression is a date/time arithmetic expression\n   */\n  def isDateTimeArithmeticExpression(expr: Expression): Boolean = {\n    expr match {\n      case _: AddMonthsBase | _: DateAdd | _: DateAddInterval | _: DateDiff | _: DateSub |\n           _: DatetimeSub | _: LastDay | _: MonthsBetween | _: NextDay | _: SubtractDates |\n           _: SubtractTimestamps | _: TimestampAdd | _: TimestampAddYMInterval |\n           _: TimestampDiff | _: TruncInstant => true\n      case _ => false\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/LogKeyShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.logging\n\nimport java.util.Locale\n\nimport org.apache.spark.internal.LogKey\n\n/**\n * Shim for LogKey to handle API changes between Spark versions.\n * In Spark 4.1, LogKey is a Java interface requiring explicit implementation of `name()`.\n *\n * DeltaLogKey provides the implementation of name() that case objects can inherit.\n */\nabstract class DeltaLogKey extends LogKey {\n  override def name(): String = getClass.getSimpleName.stripSuffix(\"$\").toLowerCase(Locale.ROOT)\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/ParquetFooterReaderShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.execution.datasources.parquet\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\nimport org.apache.parquet.hadoop.util.HadoopInputFile\n\n\nobject ParquetFooterReaderShims {\n  def readParquetFooter(\n    conf: Configuration,\n    status: FileStatus,\n    filter: ParquetMetadataConverter.MetadataFilter) : ParquetMetadata = {\n    val inputFile = HadoopInputFile.fromStatus(status, conf)\n    ParquetFooterReader.readFooter(inputFile, filter)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/ParseExceptionShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.parser\n\nimport org.antlr.v4.runtime.ParserRuleContext\n\nimport org.apache.spark.sql.catalyst.parser.{ParseException, ParserUtils}\nimport org.apache.spark.sql.catalyst.trees.Origin\nimport org.apache.spark.sql.delta.DeltaThrowable\n\n/**\n * DeltaParseException for Spark 4.1.\n * In this version, ParseException only takes a single origin parameter (stop was removed).\n */\nclass DeltaParseException(\n    ctx: ParserRuleContext,\n    errorClass: String,\n    messageParameters: Map[String, String] = Map.empty)\n  extends ParseException(\n      Option(ParserUtils.command(ctx)),\n      ParserUtils.position(ctx.getStart),  // In Spark 4.1, only start position is used\n      // No stop parameter in Spark 4.1\n      errorClass,\n      messageParameters\n    ) with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n}\n\n/**\n * Shim for ParseException to handle API changes between Spark versions.\n * In Spark 4.1, ParseException only has a single origin parameter (stop was removed).\n */\nobject ParseExceptionShims {\n\n  /**\n   * Create a ParseException with the appropriate constructor for this Spark version.\n   * In Spark 4.1, we only use the start Origin (stop parameter was removed).\n   */\n  def createParseException(\n      command: Option[String],\n      start: Origin,\n      stop: Origin,  // This parameter is ignored in Spark 4.1\n      errorClass: String,\n      messageParameters: Map[String, String]): ParseException = {\n    // In Spark 4.1, ParseException only takes a single origin parameter\n    new ParseException(command, start, errorClass, messageParameters)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/QualifiedColTypeShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.connector.catalog.TableChange.AddColumn\n\n/**\n * In Spark 4.1 QualifiedColType stores `default` as a DefaultValueExpression\n */\nobject QualifiedColTypeShims {\n\n  def getDefaultValueArgFromAddColumn(col: AddColumn): Option[DefaultValueExpression] = {\n    Option(col.defaultValue).map(v =>\n    DefaultValueExpression(\n      org.apache.spark.sql.catalyst.parser.CatalystSqlParser.parseExpression(\n        v.getSql()),\n      v.getSql()))\n  }\n\n  def getDefaultValueStr(col: QualifiedColType): Option[String] = {\n    col.default.map { value =>\n      value match {\n        case DefaultValueExpression(_, originalSQL, _) => originalSQL\n        case _ => value.toString\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/RelocatedStreamingClassesShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.execution.streaming.runtime.WatermarkPropagator\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types.StructType\n\nimport org.apache.spark.sql.execution.streaming.checkpointing.{\n  CheckpointFileManager => CheckpointFileManagerShim,\n  MetadataVersionUtil => MetadataVersionUtilShim,\n  OffsetSeqMetadata\n}\nimport org.apache.spark.sql.execution.streaming.runtime.{\n  IncrementalExecution => IncrementalExecutionShim,\n  MetadataLogFileIndex => MetadataLogFileIndexShim,\n  StreamExecution => StreamExecutionShim,\n  StreamingRelation => StreamingRelationShim\n}\nimport org.apache.spark.sql.execution.streaming.sinks.{FileStreamSink => FileStreamSinkShim}\n\nobject Relocated {\n  type CheckpointFileManager = CheckpointFileManagerShim\n  val CheckpointFileManager: CheckpointFileManagerShim.type = CheckpointFileManagerShim\n\n  type IncrementalExecution = IncrementalExecutionShim\n  // scalastyle:off argcount\n  def createIncrementalExecution(\n      sparkSession: org.apache.spark.sql.classic.SparkSession,\n      logicalPlan: LogicalPlan,\n      outputMode: OutputMode,\n      checkpointLocation: String,\n      queryId: UUID,\n      runId: UUID,\n      currentBatchId: Long,\n      prevOffsetSeqMetadata: Option[OffsetSeqMetadata],\n      offsetSeqMetadata: OffsetSeqMetadata,\n      watermarkPropagator: WatermarkPropagator,\n      isFirstBatch: Boolean): IncrementalExecution = {\n    // scalastyle:on argcount\n    new IncrementalExecutionShim(\n      sparkSession,\n      logicalPlan,\n      outputMode,\n      checkpointLocation,\n      queryId,\n      runId,\n      currentBatchId,\n      prevOffsetSeqMetadata,\n      offsetSeqMetadata,\n      watermarkPropagator,\n      isFirstBatch)\n  }\n\n  type StreamingRelation = StreamingRelationShim\n  val StreamingRelation: StreamingRelationShim.type = StreamingRelationShim\n\n  type MetadataLogFileIndex = MetadataLogFileIndexShim\n  def createMetadataLogFileIndex(\n      sparkSession: org.apache.spark.sql.SparkSession,\n      path: Path,\n      options: Map[String, String],\n      userSpecifiedSchema: Option[StructType]): MetadataLogFileIndex = {\n    new MetadataLogFileIndexShim(sparkSession, path, options, userSpecifiedSchema)\n  }\n\n  type FileStreamSink = FileStreamSinkShim\n  val FileStreamSink: FileStreamSinkShim.type = FileStreamSinkShim\n\n  type StreamExecution = StreamExecutionShim\n  val StreamExecution: StreamExecutionShim.type = StreamExecutionShim\n\n  val MetadataVersionUtil: MetadataVersionUtilShim.type = MetadataVersionUtilShim\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/VariantShreddingShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.shims\n\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Shim for variant shredding configs to handle API changes between Spark versions.\n * In Spark 4.1, VARIANT_INFER_SHREDDING_SCHEMA config exists.\n *\n * This shim provides a way to conditionally add the config to the options map\n * when writing files.\n */\nobject VariantShreddingShims {\n  /**\n   * Returns a Map containing variant shredding related configs for file writing.\n   * In Spark 4.1, this returns the VARIANT_INFER_SHREDDING_SCHEMA config.\n   */\n  def getVariantInferShreddingSchemaOptions(enableVariantShredding: Boolean)\n    : Map[String, String] = {\n    Map(SQLConf.VARIANT_INFER_SHREDDING_SCHEMA.key -> enableVariantShredding.toString)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.1/ViewShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * Shim for View to handle API changes between Spark versions.\n * In Spark 4.1, View has an additional constructor parameter.\n */\nobject ViewShims {\n\n  /**\n   * Extractor that matches View(desc, true, child) pattern.\n   * Used in DeltaViewHelper for matching temp views with a specific structure.\n   * In Spark 4.1, View has an additional parameter, so we use a wildcard to ignore it.\n   */\n  object TempViewWithChild {\n    def unapply(plan: LogicalPlan): Option[(CatalogTable, LogicalPlan)] = plan match {\n      // In Spark 4.1, View has an additional parameter, we use _ to match it\n      case View(desc, isTempView, child, _) if isTempView => Some((desc, child))\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/CreateDeltaTableLikeShims.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.delta.DeltaOptions\n\nobject CreateDeltaTableLikeShims {\n\n  /**\n   * Differentiate between DataFrameWriterV1 and V2 so that we can decide\n   * what to do with table metadata. In DataFrameWriterV1, mode(\"overwrite\").saveAsTable,\n   * behaves as a CreateOrReplace table, but we have asked for \"overwriteSchema\" as an\n   * explicit option to overwrite partitioning or schema information. With DataFrameWriterV2,\n   * the behavior asked for by the user is clearer: .createOrReplace(), which means that we\n   * should overwrite schema and/or partitioning. Therefore we have this hack.\n   *\n   * In Spark 4.1, DataFrameWriter provides the option \"__v1_save_as_table_overwrite\", because\n   * the stack trace does not indicate the calling API anymore in connect mode - planning and\n   * execution has been separated.\n   *\n   * TODO: Shim no longer needed once spark-4.0 is removed.\n   */\n  def isV1WriterSaveAsTableOverwrite(options: DeltaOptions, mode: SaveMode): Boolean = {\n    // Note: Spark is setting this only for SaveMode.Overwrite anyway, but we double check.\n    // The 4.0 shim relies on stack trace analysis instead, so it has to check.\n    // After 4.0 is dropped, we can simplify.\n    options.isDataFrameWriterV1SaveAsTableOverwrite && mode == SaveMode.Overwrite\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/DataSourceV2RelationShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.execution.datasources.v2\n\nimport org.apache.spark.sql.catalyst.expressions.AttributeReference\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.connector.catalog.{CatalogPlugin, Identifier, Table}\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Shim for DataSourceV2Relation to handle API changes between Spark versions.\n * In Spark 4.2, DataSourceV2Relation has 6 constructor parameters (same as Spark 4.1).\n */\nobject DataSourceV2RelationShim {\n\n  /**\n   * Main extractor for DataSourceV2Relation that works across Spark versions.\n   * Returns the common fields that exist in all versions.\n   */\n  def unapply(plan: LogicalPlan): Option[(\n    Table, Seq[AttributeReference],\n      Option[CatalogPlugin],\n      Option[Identifier],\n      CaseInsensitiveStringMap)] = {\n    plan match {\n      case r: DataSourceV2Relation =>\n        Some((r.table, r.output, r.catalog, r.identifier, r.options))\n      case _ => None\n    }\n  }\n}\n\n/**\n * Simplified extractor when only table and options are needed.\n */\nobject DataSourceV2RelationSimple {\n  def unapply(plan: LogicalPlan): Option[(Table, CaseInsensitiveStringMap)] = {\n    plan match {\n      case r: DataSourceV2Relation =>\n        Some((r.table, r.options))\n      case _ => None\n    }\n  }\n}\n\n/**\n * Extractor for cases that only need the table.\n */\nobject DataSourceV2RelationTable {\n  def unapply(plan: LogicalPlan): Option[Table] = {\n    plan match {\n      case r: DataSourceV2Relation => Some(r.table)\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/DateTimeExpressionShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.catalyst.expressions\n\n/**\n * Shim for date/time expressions in Spark 4.2 (same as Spark 4.1)\n * Note: TimeAdd is removed in Spark 4.1+\n */\nobject DateTimeExpressionShims {\n  /**\n   * Check if the given expression is a date/time arithmetic expression\n   */\n  def isDateTimeArithmeticExpression(expr: Expression): Boolean = {\n    expr match {\n      case _: AddMonthsBase | _: DateAdd | _: DateAddInterval | _: DateDiff | _: DateSub |\n           _: DatetimeSub | _: LastDay | _: MonthsBetween | _: NextDay | _: SubtractDates |\n           _: SubtractTimestamps | _: TimestampAdd | _: TimestampAddYMInterval |\n           _: TimestampDiff | _: TruncInstant => true\n      case _ => false\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/LogKeyShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.logging\n\nimport java.util.Locale\n\nimport org.apache.spark.internal.LogKey\n\n/**\n * Shim for LogKey to handle API changes between Spark versions.\n * In Spark 4.2, LogKey is a Java interface requiring explicit implementation of `name()`\n * (same as Spark 4.1).\n *\n * DeltaLogKey provides the implementation of name() that case objects can inherit.\n */\nabstract class DeltaLogKey extends LogKey {\n  override def name(): String = getClass.getSimpleName.stripSuffix(\"$\").toLowerCase(Locale.ROOT)\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/ParquetFooterReaderShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.execution.datasources.parquet\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\nimport org.apache.parquet.hadoop.util.HadoopInputFile\n\n\nobject ParquetFooterReaderShims {\n  def readParquetFooter(\n    conf: Configuration,\n    status: FileStatus,\n    filter: ParquetMetadataConverter.MetadataFilter) : ParquetMetadata = {\n    val inputFile = HadoopInputFile.fromStatus(status, conf)\n    ParquetFooterReader.readFooter(inputFile, filter)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/ParseExceptionShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.parser\n\nimport org.antlr.v4.runtime.ParserRuleContext\n\nimport org.apache.spark.sql.catalyst.parser.{ParseException, ParserUtils}\nimport org.apache.spark.sql.catalyst.trees.Origin\nimport org.apache.spark.sql.delta.DeltaThrowable\n\n/**\n * DeltaParseException for Spark 4.2 (same as Spark 4.1).\n * In this version, ParseException only takes a single origin parameter (stop was removed).\n */\nclass DeltaParseException(\n    ctx: ParserRuleContext,\n    errorClass: String,\n    messageParameters: Map[String, String] = Map.empty)\n  extends ParseException(\n      Option(ParserUtils.command(ctx)),\n      ParserUtils.position(ctx.getStart),  // In Spark 4.2, only start position is used\n      // No stop parameter in Spark 4.2\n      errorClass,\n      messageParameters\n    ) with DeltaThrowable {\n  override def getErrorClass: String = errorClass\n}\n\n/**\n * Shim for ParseException to handle API changes between Spark versions.\n * In Spark 4.2, ParseException only has a single origin parameter (same as Spark 4.1,\n * stop was removed).\n */\nobject ParseExceptionShims {\n\n  /**\n   * Create a ParseException with the appropriate constructor for this Spark version.\n   * In Spark 4.2, we only use the start Origin (same as Spark 4.1, stop parameter was removed).\n   */\n  def createParseException(\n      command: Option[String],\n      start: Origin,\n      stop: Origin,  // This parameter is ignored in Spark 4.2 (same as 4.1)\n      errorClass: String,\n      messageParameters: Map[String, String]): ParseException = {\n    // In Spark 4.2, ParseException only takes a single origin parameter (same as 4.1)\n    new ParseException(command, start, errorClass, messageParameters)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/QualifiedColTypeShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.connector.catalog.TableChange.AddColumn\n\n/**\n * In Spark 4.2 QualifiedColType stores `default` as a DefaultValueExpression (same as Spark 4.1)\n */\nobject QualifiedColTypeShims {\n\n  def getDefaultValueArgFromAddColumn(col: AddColumn): Option[DefaultValueExpression] = {\n    Option(col.defaultValue).map(v =>\n    DefaultValueExpression(\n      org.apache.spark.sql.catalyst.parser.CatalystSqlParser.parseExpression(\n        v.getSql()),\n      v.getSql()))\n  }\n\n  def getDefaultValueStr(col: QualifiedColType): Option[String] = {\n    col.default.map { value =>\n      value match {\n        case DefaultValueExpression(_, originalSQL, _) => originalSQL\n        case _ => value.toString\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/RelocatedStreamingClassesShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.execution.streaming.runtime.WatermarkPropagator\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types.StructType\n\nimport org.apache.spark.sql.execution.streaming.checkpointing.{\n  CheckpointFileManager => CheckpointFileManagerShim,\n  MetadataVersionUtil => MetadataVersionUtilShim,\n  OffsetSeqMetadataBase\n}\nimport org.apache.spark.sql.execution.streaming.runtime.{\n  IncrementalExecution => IncrementalExecutionShim,\n  MetadataLogFileIndex => MetadataLogFileIndexShim,\n  StreamExecution => StreamExecutionShim,\n  StreamingRelation => StreamingRelationShim\n}\nimport org.apache.spark.sql.execution.streaming.sinks.{FileStreamSink => FileStreamSinkShim}\n\nobject Relocated {\n  type CheckpointFileManager = CheckpointFileManagerShim\n  val CheckpointFileManager: CheckpointFileManagerShim.type = CheckpointFileManagerShim\n\n  type IncrementalExecution = IncrementalExecutionShim\n  // scalastyle:off argcount\n  def createIncrementalExecution(\n      sparkSession: org.apache.spark.sql.classic.SparkSession,\n      logicalPlan: LogicalPlan,\n      outputMode: OutputMode,\n      checkpointLocation: String,\n      queryId: UUID,\n      runId: UUID,\n      currentBatchId: Long,\n      prevOffsetSeqMetadata: Option[OffsetSeqMetadataBase],\n      offsetSeqMetadata: OffsetSeqMetadataBase,\n      watermarkPropagator: WatermarkPropagator,\n      isFirstBatch: Boolean): IncrementalExecution = {\n    // scalastyle:on argcount\n    new IncrementalExecutionShim(\n      sparkSession,\n      logicalPlan,\n      outputMode,\n      checkpointLocation,\n      queryId,\n      runId,\n      currentBatchId,\n      prevOffsetSeqMetadata,\n      offsetSeqMetadata,\n      watermarkPropagator,\n      isFirstBatch)\n  }\n\n  type StreamingRelation = StreamingRelationShim\n  val StreamingRelation: StreamingRelationShim.type = StreamingRelationShim\n\n  type MetadataLogFileIndex = MetadataLogFileIndexShim\n  def createMetadataLogFileIndex(\n      sparkSession: org.apache.spark.sql.SparkSession,\n      path: Path,\n      options: Map[String, String],\n      userSpecifiedSchema: Option[StructType]): MetadataLogFileIndex = {\n    new MetadataLogFileIndexShim(sparkSession, path, options, userSpecifiedSchema)\n  }\n\n  type FileStreamSink = FileStreamSinkShim\n  val FileStreamSink: FileStreamSinkShim.type = FileStreamSinkShim\n\n  type StreamExecution = StreamExecutionShim\n  val StreamExecution: StreamExecutionShim.type = StreamExecutionShim\n\n  val MetadataVersionUtil: MetadataVersionUtilShim.type = MetadataVersionUtilShim\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/VariantShreddingShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.shims\n\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Shim for variant shredding configs to handle API changes between Spark versions.\n * In Spark 4.2, VARIANT_INFER_SHREDDING_SCHEMA config exists.\n *\n * This shim provides a way to conditionally add the config to the options map\n * when writing files.\n */\nobject VariantShreddingShims {\n  /**\n   * Returns a Map containing variant shredding related configs for file writing.\n   * In Spark 4.2, this returns the VARIANT_INFER_SHREDDING_SCHEMA config.\n   */\n  def getVariantInferShreddingSchemaOptions(enableVariantShredding: Boolean): Map[String, String] = {\n    Map(SQLConf.VARIANT_INFER_SHREDDING_SCHEMA.key -> enableVariantShredding.toString)\n  }\n}\n"
  },
  {
    "path": "spark/src/main/scala-shims/spark-4.2/ViewShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.plans.logical\n\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\n\n/**\n * Shim for View to handle API changes between Spark versions.\n * In Spark 4.2, View has an additional constructor parameter (same as Spark 4.1).\n */\nobject ViewShims {\n\n  /**\n   * Extractor that matches View(desc, true, child) pattern.\n   * Used in DeltaViewHelper for matching temp views with a specific structure.\n   * In Spark 4.2, View has an additional parameter (same as 4.1), so we use a wildcard to ignore\n   * it.\n   */\n  object TempViewWithChild {\n    def unapply(plan: LogicalPlan): Option[(CatalogTable, LogicalPlan)] = plan match {\n      // In Spark 4.2, View has an additional parameter (same as 4.1), we use _ to match it\n      case View(desc, isTempView, child, _) if isTempView => Some((desc, child))\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/java/io/delta/sql/JavaDeltaSparkSessionExtensionSuite.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sql;\n\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.util.Utils;\nimport org.junit.Test;\n\nimport java.io.IOException;\n\npublic class JavaDeltaSparkSessionExtensionSuite {\n\n    @Test\n    public void testSQLConf() throws IOException {\n        SparkSession spark = SparkSession.builder()\n                .appName(\"JavaDeltaSparkSessionExtensionSuiteUsingSQLConf\")\n                .master(\"local[2]\")\n                .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n                .config(\"spark.sql.catalog.spark_catalog\",\n                        \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n                .getOrCreate();\n        try {\n            String input = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"input\")\n                    .getCanonicalPath();\n            spark.range(1, 10).write().format(\"delta\").save(input);\n            spark.sql(\"vacuum delta.`\" + input + \"`\");\n        } finally {\n            spark.stop();\n        }\n    }\n}\n"
  },
  {
    "path": "spark/src/test/java/io/delta/tables/JavaDeltaTableBuilderSuite.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.apache.spark.sql.delta.DeltaLog;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.*;\n\nimport org.apache.spark.util.Utils;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.apache.spark.sql.delta.DeltaSQLCommandJavaTest;\n\nimport static org.apache.spark.sql.types.DataTypes.*;\n\npublic class JavaDeltaTableBuilderSuite implements DeltaSQLCommandJavaTest {\n\n    private transient SparkSession spark;\n    private transient String input;\n\n\n    @Before\n    public void setUp() {\n        // Trigger static initializer of TestData\n        spark = buildSparkSession();\n    }\n\n    @After\n    public void tearDown() {\n        if (spark != null) {\n            spark.stop();\n            spark = null;\n        }\n    }\n\n    private DeltaTable buildTable(DeltaTableBuilder builder) {\n        return builder.addColumn(\"c1\", \"int\")\n            .addColumn(\"c2\", IntegerType)\n            .addColumn(\"c3\", \"string\", false)\n            .addColumn(\"c4\", StringType, true)\n            .addColumn(DeltaTable.columnBuilder(spark, \"c5\")\n                .dataType(\"bigint\")\n                .comment(\"foo\")\n                .nullable(false)\n                .build()\n            )\n            .addColumn(DeltaTable.columnBuilder(spark, \"c6\")\n                .dataType(LongType)\n                .generatedAlwaysAs(\"c5 + 10\")\n                .build()\n            ).execute();\n    }\n\n    private DeltaTable createTable(boolean ifNotExists, String tableName) {\n        DeltaTableBuilder builder;\n        if (ifNotExists) {\n            builder = DeltaTable.createIfNotExists();\n        } else {\n            builder = DeltaTable.create();\n        }\n        if (tableName.startsWith(\"delta.`\")) {\n            tableName = tableName.substring(\"delta.`\".length());\n            String location = tableName.substring(0, tableName.length() - 1);\n            builder = builder.location(location);\n            DeltaLog.forTable(spark, location).clearCache();\n        } else {\n            builder = builder.tableName(tableName);\n            DeltaLog.forTable(spark, new Path(tableName)).clearCache();\n        }\n        return buildTable(builder);\n    }\n\n    private DeltaTable replaceTable(boolean orCreate, String tableName) {\n        DeltaTableBuilder builder;\n        if (orCreate) {\n            builder = DeltaTable.createOrReplace();\n        } else {\n            builder = DeltaTable.replace();\n        }\n        if (tableName.startsWith(\"delta.`\")) {\n            tableName = tableName.substring(\"delta.`\".length());\n            String location = tableName.substring(0, tableName.length() - 1);\n            builder = builder.location(location);\n        } else {\n            builder = builder.tableName(tableName);\n        }\n        return buildTable(builder);\n    }\n\n    private void verifyGeneratedColumn(String tableName, DeltaTable deltaTable) {\n        String cmd = String.format(\"INSERT INTO %s (c1, c2, c3, c4, c5, c6) %s\", tableName,\n            \"VALUES (1, 2, 'a', 'c', 1, 11)\");\n        spark.sql(cmd);\n        Map<String, String> set = new HashMap<String, String>() {{\n            put(\"c5\", \"10\");\n        }};\n        deltaTable.updateExpr(\"c6 = 11\", set);\n        assert(deltaTable.toDF().select(\"c6\").collectAsList().get(0).getLong(0) == 20);\n    }\n\n    @Test\n    public void testCreateTable() {\n        try {\n            // Test creating DeltaTable by name\n            DeltaTable table = createTable(false, \"deltaTable\");\n            verifyGeneratedColumn(\"deltaTable\", table);\n        } finally {\n            spark.sql(\"DROP TABLE IF EXISTS deltaTable\");\n        }\n        // Test creating DeltaTable by path.\n        String input = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"input\")\n            .toString();\n        DeltaTable table2 = createTable(false, String.format(\"delta.`%s`\", input));\n        verifyGeneratedColumn(String.format(\"delta.`%s`\", input), table2);\n    }\n\n    @Test\n    public void testCreateTableIfNotExists() {\n        // Ignore table creation if already exsits.\n        List<String> data = Arrays.asList(\"hello\", \"world\");\n        Dataset<Row> dataDF = spark.createDataset(data, Encoders.STRING()).toDF();\n        try {\n            // Test creating DeltaTable by name - not exists.\n            DeltaTable table = createTable(true, \"deltaTable\");\n            verifyGeneratedColumn(\"deltaTable\", table);\n\n            dataDF.write().format(\"delta\").mode(\"overwrite\").saveAsTable(\"deltaTable2\");\n\n            // Table 2 should be the old table saved by path.\n            DeltaTable table2 = DeltaTable.createIfNotExists().tableName(\"deltaTable2\")\n                .addColumn(\"value\", \"string\")\n                .execute();\n            QueryTest$.MODULE$.checkAnswer(table2.toDF(), dataDF.collectAsList());\n        } finally {\n            spark.sql(\"DROP TABLE IF EXISTS deltaTable\");\n            spark.sql(\"DROP TABLE IF EXISTS deltaTable2\");\n        }\n        // Test creating DeltaTable by path.\n        String input = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"input\")\n            .toString();\n        dataDF.write().format(\"delta\").mode(\"overwrite\").save(input);\n        DeltaTable table = createTable(true, String.format(\"delta.`%s`\", input));\n        QueryTest$.MODULE$.checkAnswer(table.toDF(), dataDF.collectAsList());\n    }\n\n    @Test\n    public void testCreateTableWithExistingSchema() {\n        try {\n            // Test create table with an existing schema.\n            List<String> data = Arrays.asList(\"hello\", \"world\");\n            Dataset<Row> dataDF = spark.createDataset(data, Encoders.STRING()).toDF();\n\n            DeltaLog.forTable(spark, new Path(\"deltaTable\")).clearCache();\n            DeltaTable table = DeltaTable.create().tableName(\"deltaTable\")\n                .addColumns(dataDF.schema())\n                .execute();\n            dataDF.write().format(\"delta\").mode(\"append\").saveAsTable(\"deltaTable\");\n\n            QueryTest$.MODULE$.checkAnswer(table.toDF(), dataDF.collectAsList());\n        } finally {\n            spark.sql(\"DROP TABLE IF EXISTS deltaTable\");\n        }\n    }\n\n    @Test\n    public void testReplaceTable() {\n        try {\n            // create a table first\n            spark.sql(\"CREATE TABLE deltaTable (col1 int) USING delta\");\n            // Test replacing DeltaTable by name\n            DeltaTable table = replaceTable(false, \"deltaTable\");\n            verifyGeneratedColumn(\"deltaTable\", table);\n        } finally {\n            spark.sql(\"DROP TABLE IF EXISTS deltaTable\");\n        }\n        String input = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"input\")\n            .toString();\n        List<String> data = Arrays.asList(\"hello\", \"world\");\n        Dataset<Row> dataDF = spark.createDataset(data, Encoders.STRING()).toDF();\n        dataDF.write().format(\"delta\").mode(\"overwrite\").save(input);\n        DeltaTable table = replaceTable(false, String.format(\"delta.`%s`\", input));\n        verifyGeneratedColumn(String.format(\"delta.`%s`\", input), table);\n    }\n\n    @Test\n    public void testCreateOrReplaceTable() {\n        try {\n            // Test creating DeltaTable by name if table to be replaced does not exist.\n            DeltaTable table = replaceTable(true, \"deltaTable\");\n            verifyGeneratedColumn(\"deltaTable\", table);\n        } finally {\n            spark.sql(\"DROP TABLE IF EXISTS deltaTable\");\n        }\n    }\n}\n"
  },
  {
    "path": "spark/src/test/java/io/delta/tables/JavaDeltaTableSuite.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.apache.spark.sql.test.*;\nimport org.apache.spark.sql.*;\n\nimport org.apache.spark.util.Utils;\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.apache.spark.sql.delta.DeltaSQLCommandJavaTest;\n\npublic class JavaDeltaTableSuite implements DeltaSQLCommandJavaTest {\n\n  private transient SparkSession spark;\n  private transient String input;\n\n\n  @Before\n  public void setUp() {\n    // Trigger static initializer of TestData\n    spark = buildSparkSession();\n  }\n\n  @After\n  public void tearDown() {\n    if (spark != null) {\n      spark.stop();\n      spark = null;\n    }\n  }\n\n  @Test\n  public void testAPI() {\n    try {\n      String input = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"input\").toString();\n      List<String> data = Arrays.asList(\"hello\", \"world\");\n      Dataset<Row> dataDF = spark.createDataset(data, Encoders.STRING()).toDF();\n      List<Row> dataRows = dataDF.collectAsList();\n      dataDF.write().format(\"delta\").mode(\"overwrite\").save(input);\n\n      // Test creating DeltaTable by path\n      DeltaTable table1 = DeltaTable.forPath(spark, input);\n      QueryTest$.MODULE$.checkAnswer(table1.toDF(), dataRows);\n\n      // Test creating DeltaTable by path picks up active SparkSession\n      DeltaTable table2 = DeltaTable.forPath(input);\n      QueryTest$.MODULE$.checkAnswer(table2.toDF(), dataRows);\n\n      dataDF.write().format(\"delta\").mode(\"overwrite\").saveAsTable(\"deltaTable\");\n\n      // Test creating DeltaTable by name\n      DeltaTable table3 = DeltaTable.forName(spark, \"deltaTable\");\n      QueryTest$.MODULE$.checkAnswer(table3.toDF(), dataRows);\n\n      // Test creating DeltaTable by name\n      DeltaTable table4 = DeltaTable.forName(\"deltaTable\");\n      QueryTest$.MODULE$.checkAnswer(table4.toDF(), dataRows);\n\n      // Test DeltaTable.as() creates subquery alias\n      QueryTest$.MODULE$.checkAnswer(table2.as(\"tbl\").toDF().select(\"tbl.value\"), dataRows);\n\n      // Test DeltaTable.isDeltaTable() is true for a Delta file path.\n      Assert.assertTrue(DeltaTable.isDeltaTable(input));\n    } finally {\n      spark.sql(\"DROP TABLE IF EXISTS deltaTable\");\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/java/org/apache/spark/sql/delta/DeleteJavaSuite.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport scala.Tuple2;\n\nimport io.delta.tables.DeltaTable;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport org.apache.spark.sql.*;\nimport org.apache.spark.util.Utils;\n\npublic class DeleteJavaSuite implements DeltaSQLCommandJavaTest {\n\n    private transient SparkSession spark;\n    private transient String tempPath;\n\n    @Before\n    public void setUp() {\n        spark = buildSparkSession();\n        tempPath = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"spark\").toString();\n    }\n\n    @After\n    public void tearDown() {\n        if (spark != null) {\n            spark.stop();\n            spark = null;\n        }\n    }\n\n    @Test\n    public void testWithoutCondition() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        target.delete();\n\n        List<Row> expectedAnswer = new ArrayList<>();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void testWithCondition() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        target.delete(\"key = 1 or key = 2\");\n\n        List<Row> expectedAnswer = createKVDataSet(\n            Arrays.asList(tuple2(3, 30), tuple2(4, 40))).collectAsList();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void testWithColumnCondition() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        target.delete(functions.expr(\"key = 1 or key = 2\"));\n\n        List<Row> expectedAnswer = createKVDataSet(\n            Arrays.asList(tuple2(3, 30), tuple2(4, 40))).collectAsList();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    private Dataset<Row> createKVDataSet(\n        List<Tuple2<Integer, Integer>> data, String keyName, String valueName) {\n        Encoder<Tuple2<Integer, Integer>> encoder = Encoders.tuple(Encoders.INT(), Encoders.INT());\n        return spark.createDataset(data, encoder).toDF(keyName, valueName);\n    }\n\n    private Dataset<Row> createKVDataSet(List<Tuple2<Integer, Integer>> data) {\n        Encoder<Tuple2<Integer, Integer>> encoder = Encoders.tuple(Encoders.INT(), Encoders.INT());\n        return spark.createDataset(data, encoder).toDF();\n    }\n\n    private <T1, T2> Tuple2<T1, T2> tuple2(T1 t1, T2 t2) {\n        return new Tuple2<>(t1, t2);\n    }\n}\n"
  },
  {
    "path": "spark/src/test/java/org/apache/spark/sql/delta/DeltaSQLCommandJavaTest.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\nimport org.apache.spark.sql.SparkSession;\n\npublic interface DeltaSQLCommandJavaTest {\n  default SparkSession buildSparkSession() {\n    // Set the configurations as DeltaSQLCommandTest\n    return SparkSession.builder()\n        .appName(\"JavaDeltaSparkSessionExtensionSuiteUsingSQLConf\")\n        .master(\"local[2]\")\n        .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n        .config(\"spark.sql.catalog.spark_catalog\",\n            \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n        .getOrCreate();\n  }\n}\n"
  },
  {
    "path": "spark/src/test/java/org/apache/spark/sql/delta/MergeIntoJavaSuite.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport scala.Tuple2;\n\nimport io.delta.tables.DeltaTable;\n\nimport org.apache.spark.sql.*;\nimport org.apache.spark.util.Utils;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport org.apache.spark.sql.test.TestSparkSession;\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog;\nimport org.apache.spark.sql.internal.SQLConf;\n\npublic class MergeIntoJavaSuite implements Serializable {\n    private transient TestSparkSession spark;\n    private transient String tempPath;\n\n    @Before\n    public void setUp() {\n        spark = new TestSparkSession();\n        tempPath = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"spark\").toString();\n        spark.sqlContext().conf().setConfString(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION().key(), DeltaCatalog.class.getCanonicalName());\n    }\n\n    @After\n    public void tearDown() {\n        spark.stop();\n        spark = null;\n    }\n\n    @Test\n    public void checkBasicApi() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20)), \"key1\", \"value1\");\n        targetTable.write().format(\"delta\").save(tempPath);\n\n        Dataset<Row> sourceTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 100), tuple2(3, 30)), \"key2\", \"value2\");\n\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n        Map<String, String> updateMap = new HashMap<String, String>() {{\n            put(\"key1\", \"key2\");\n            put(\"value1\", \"value2\");\n        }};\n        Map<String, String> insertMap = new HashMap<String, String>() {{\n            put(\"key1\", \"key2\");\n            put(\"value1\", \"value2\");\n        }};\n        target.merge(sourceTable, \"key1 = key2\")\n            .whenMatched()\n            .updateExpr(updateMap)\n            .whenNotMatched()\n            .insertExpr(insertMap)\n            .execute();\n\n        List<Row> expectedAnswer = createKVDataSet(\n            Arrays.asList(tuple2(1, 100), tuple2(2, 20), tuple2(3, 30))).collectAsList();\n\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void checkExtendedApi() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20)), \"key1\", \"value1\");\n        targetTable.write().format(\"delta\").save(tempPath);\n\n        Dataset<Row> sourceTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 100), tuple2(3, 30)), \"key2\", \"value2\");\n\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n        Map<String, String> updateMap = new HashMap<String, String>() {{\n            put(\"key1\", \"key2\");\n            put(\"value1\", \"value2\");\n        }};\n        Map<String, String> insertMap = new HashMap<String, String>() {{\n            put(\"key1\", \"key2\");\n            put(\"value1\", \"value2\");\n        }};\n        target.merge(sourceTable, \"key1 = key2\")\n            .whenMatched(\"key1 = 4\").delete()\n            .whenMatched(\"key2 = 1\")\n            .updateExpr(updateMap)\n            .whenNotMatched(\"key2 = 3\")\n            .insertExpr(insertMap)\n            .execute();\n\n        List<Row> expectedAnswer = createKVDataSet(\n            Arrays.asList(tuple2(1, 100), tuple2(2, 20), tuple2(3, 30))).collectAsList();\n\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void checkExtendedApiWithColumn() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(4, 40)), \"key1\", \"value1\");\n        targetTable.write().format(\"delta\").save(tempPath);\n\n        Dataset<Row> sourceTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 100), tuple2(3, 30), tuple2(4, 41)), \"key2\", \"value2\");\n\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n        Map<String, Column> updateMap = new HashMap<String, Column>() {{\n            put(\"key1\", functions.col(\"key2\"));\n            put(\"value1\", functions.col(\"value2\"));\n        }};\n        Map<String, Column> insertMap = new HashMap<String, Column>() {{\n            put(\"key1\", functions.col(\"key2\"));\n            put(\"value1\", functions.col(\"value2\"));\n        }};\n        target.merge(sourceTable, functions.expr(\"key1 = key2\"))\n            .whenMatched(functions.expr(\"key1 = 4\")).delete()\n            .whenMatched(functions.expr(\"key2 = 1\"))\n            .update(updateMap)\n            .whenNotMatched(functions.expr(\"key2 = 3\"))\n            .insert(insertMap)\n            .execute();\n\n        List<Row> expectedAnswer = createKVDataSet(\n            Arrays.asList(tuple2(1, 100), tuple2(2, 20), tuple2(3, 30))).collectAsList();\n\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void checkUpdateAllAndInsertAll() {\n        Dataset<Row> targetTable = createKVDataSet(Arrays.asList(\n            tuple2(1, 10), tuple2(2, 20), tuple2(4, 40), tuple2(5, 50)), \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n\n        Dataset<Row> sourceTable = createKVDataSet(Arrays.asList(\n            tuple2(1, 100), tuple2(3, 30), tuple2(4, 41), tuple2(5, 51), tuple2(6, 60)),\n            \"key\", \"value\");\n\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n        target.as(\"t\").merge(sourceTable.as(\"s\"), functions.expr(\"t.key = s.key\"))\n            .whenMatched().updateAll()\n            .whenNotMatched().insertAll()\n            .execute();\n\n        List<Row> expectedAnswer = createKVDataSet(Arrays.asList(tuple2(1, 100), tuple2(2, 20),\n            tuple2(3, 30), tuple2(4, 41), tuple2(5, 51), tuple2(6, 60))).collectAsList();\n\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    private Dataset<Row> createKVDataSet(\n        List<Tuple2<Integer, Integer>> data, String keyName, String valueName) {\n        Encoder<Tuple2<Integer, Integer>> encoder = Encoders.tuple(Encoders.INT(), Encoders.INT());\n        return spark.createDataset(data, encoder).toDF(keyName, valueName);\n    }\n\n    private Dataset<Row> createKVDataSet(List<Tuple2<Integer, Integer>> data) {\n        Encoder<Tuple2<Integer, Integer>> encoder = Encoders.tuple(Encoders.INT(), Encoders.INT());\n        return spark.createDataset(data, encoder).toDF();\n    }\n\n    private <T1, T2> Tuple2<T1, T2> tuple2(T1 t1, T2 t2) {\n        return new Tuple2<>(t1, t2);\n    }\n}\n"
  },
  {
    "path": "spark/src/test/java/org/apache/spark/sql/delta/UpdateJavaSuite.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta;\n\nimport java.util.*;\n\nimport scala.Tuple2;\n\nimport io.delta.tables.DeltaTable;\nimport org.junit.*;\n\nimport org.apache.spark.sql.*;\nimport org.apache.spark.util.Utils;\n\npublic class UpdateJavaSuite implements DeltaSQLCommandJavaTest {\n    private transient SparkSession spark;\n    private transient String tempPath;\n\n    @Before\n    public void setUp() {\n        spark = buildSparkSession();\n        tempPath = Utils.createTempDir(System.getProperty(\"java.io.tmpdir\"), \"spark\").toString();\n    }\n\n    @After\n    public void tearDown() {\n        if (spark != null) {\n            spark.stop();\n            spark = null;\n        }\n    }\n\n    @Test\n    public void testWithoutCondition() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        Map<String, String> set = new HashMap<String, String>() {{\n            put(\"key\", \"100\");\n        }};\n        target.updateExpr(set);\n\n        List<Row> expectedAnswer = createKVDataSet(Arrays.asList(\n            tuple2(100, 10), tuple2(100, 20), tuple2(100, 30), tuple2(100, 40))).collectAsList();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void testWithoutConditionUsingColumn() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        Map<String, Column> set = new HashMap<String, Column>() {{\n            put(\"key\", functions.expr(\"100\"));\n        }};\n        target.update(set);\n\n        List<Row> expectedAnswer = createKVDataSet(Arrays.asList(\n            tuple2(100, 10), tuple2(100, 20), tuple2(100, 30), tuple2(100, 40))).collectAsList();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void testWithCondition() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        Map<String, String> set = new HashMap<String, String>() {{\n            put(\"key\", \"100\");\n        }};\n        target.updateExpr(\"key = 1 or key = 2\", set);\n\n        List<Row> expectedAnswer = createKVDataSet(Arrays.asList(\n            tuple2(100, 10), tuple2(100, 20), tuple2(3, 30), tuple2(4, 40))).collectAsList();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    @Test\n    public void testWithConditionUsingColumn() {\n        Dataset<Row> targetTable = createKVDataSet(\n            Arrays.asList(tuple2(1, 10), tuple2(2, 20), tuple2(3, 30), tuple2(4, 40)),\n            \"key\", \"value\");\n        targetTable.write().format(\"delta\").save(tempPath);\n        DeltaTable target = DeltaTable.forPath(spark, tempPath);\n\n        Map<String, Column> set = new HashMap<String, Column>() {{\n            put(\"key\", functions.expr(\"100\"));\n        }};\n        target.update(functions.expr(\"key = 1 or key = 2\"), set);\n\n        List<Row> expectedAnswer = createKVDataSet(Arrays.asList(\n            tuple2(100, 10), tuple2(100, 20), tuple2(3, 30), tuple2(4, 40))).collectAsList();\n        QueryTest$.MODULE$.checkAnswer(target.toDF(), expectedAnswer);\n    }\n\n    private Dataset<Row> createKVDataSet(\n        List<Tuple2<Integer, Integer>> data, String keyName, String valueName) {\n        Encoder<Tuple2<Integer, Integer>> encoder = Encoders.tuple(Encoders.INT(), Encoders.INT());\n        return spark.createDataset(data, encoder).toDF(keyName, valueName);\n    }\n\n    private Dataset<Row> createKVDataSet(List<Tuple2<Integer, Integer>> data) {\n        Encoder<Tuple2<Integer, Integer>> encoder = Encoders.tuple(Encoders.INT(), Encoders.INT());\n        return spark.createDataset(data, encoder).toDF();\n    }\n\n    private <T1, T2> Tuple2<T1, T2> tuple2(T1 t1, T2 t2) {\n        return new Tuple2<>(t1, t2);\n    }\n}\n"
  },
  {
    "path": "spark/src/test/java/org/apache/spark/sql/delta/util/CatalogTableUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util;\n\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\n\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.junit.Test;\nimport scala.Option;\n\n/** Tests for {@link CatalogTableUtils}. */\npublic class CatalogTableUtilsTest {\n\n  @Test\n  public void testIsCatalogManaged_CatalogFlagEnabled_ReturnsTrue() {\n    CatalogTable table =\n        catalogTable(\n            Collections.emptyMap(), Map.of(CatalogTableUtils.FEATURE_CATALOG_MANAGED, \"supported\"));\n\n    assertTrue(\n        \"Catalog-managed flag should enable detection\",\n        CatalogTableUtils.isCatalogManaged(table));\n  }\n\n  @Test\n  public void testIsCatalogManaged_PreviewFlagEnabled_ReturnsTrue() {\n    CatalogTable table =\n        catalogTable(\n            Collections.emptyMap(),\n            Map.of(CatalogTableUtils.FEATURE_CATALOG_OWNED_PREVIEW, \"SuPpOrTeD\"));\n\n    assertTrue(\n        \"Preview flag should enable detection ignoring case\",\n        CatalogTableUtils.isCatalogManaged(table));\n  }\n\n  @Test\n  public void testIsCatalogManaged_NoFlags_ReturnsFalse() {\n    CatalogTable table = catalogTable(Collections.emptyMap(), Collections.emptyMap());\n\n    assertFalse(\n        \"No catalog flags should disable detection\",\n        CatalogTableUtils.isCatalogManaged(table));\n  }\n\n  @Test\n  public void testIsUnityCatalogManaged_FlagAndIdPresent_ReturnsTrue() {\n    CatalogTable table =\n        catalogTable(\n            Collections.emptyMap(),\n            Map.of(\n                CatalogTableUtils.FEATURE_CATALOG_MANAGED,\n                \"supported\",\n                UCCommitCoordinatorClient.UC_TABLE_ID_KEY,\n                \"abc-123\"));\n\n    assertTrue(\n        \"Unity Catalog detection should require flag and identifier\",\n        CatalogTableUtils.isUnityCatalogManagedTable(table));\n  }\n\n  @Test\n  public void testIsUnityCatalogManaged_MissingId_ReturnsFalse() {\n    CatalogTable table =\n        catalogTable(\n            Collections.emptyMap(), Map.of(CatalogTableUtils.FEATURE_CATALOG_MANAGED, \"supported\"));\n\n    assertFalse(\n        \"Missing table identifier should break Unity detection\",\n        CatalogTableUtils.isUnityCatalogManagedTable(table));\n  }\n\n  @Test\n  public void testIsUnityCatalogManaged_PreviewFlagMissingId_ReturnsFalse() {\n    CatalogTable table =\n        catalogTable(\n            Collections.emptyMap(),\n            Map.of(CatalogTableUtils.FEATURE_CATALOG_OWNED_PREVIEW, \"supported\"));\n\n    assertFalse(\n        \"Preview flag without ID should not be considered Unity managed\",\n        CatalogTableUtils.isUnityCatalogManagedTable(table));\n  }\n\n  @Test\n  public void testIsCatalogManaged_NullStorage_ReturnsFalse() {\n    CatalogTable table = catalogTableWithNullStorage(Collections.emptyMap());\n\n    assertFalse(\n        \"Null storage should not be considered catalog managed\",\n        CatalogTableUtils.isCatalogManaged(table));\n  }\n\n  @Test\n  public void testIsUnityCatalogManaged_NullStorage_ReturnsFalse() {\n    CatalogTable table = catalogTableWithNullStorage(Collections.emptyMap());\n\n    assertFalse(\n        \"Null storage should not be considered Unity managed\",\n        CatalogTableUtils.isUnityCatalogManagedTable(table));\n  }\n\n  @Test\n  public void testIsCatalogManaged_NullStorageProperties_ReturnsFalse() {\n    CatalogTable table = catalogTableWithNullStorageProperties(Collections.emptyMap());\n\n    assertFalse(\n        \"Null storage properties should not be considered catalog managed\",\n        CatalogTableUtils.isCatalogManaged(table));\n  }\n\n  @Test\n  public void testIsUnityCatalogManaged_NullStorageProperties_ReturnsFalse() {\n    CatalogTable table = catalogTableWithNullStorageProperties(Collections.emptyMap());\n\n    assertFalse(\n        \"Null storage properties should not be considered Unity managed\",\n        CatalogTableUtils.isUnityCatalogManagedTable(table));\n  }\n\n  private static CatalogTable catalogTable(\n      Map<String, String> properties, Map<String, String> storageProperties) {\n    return CatalogTableTestUtils$.MODULE$.createCatalogTable(\n        \"tbl\" /* tableName */,\n        Option.empty() /* catalogName */,\n        properties,\n        storageProperties,\n        Option.empty() /* locationUri */,\n        false /* nullStorage */,\n        false /* nullStorageProperties */);\n  }\n\n  private static CatalogTable catalogTableWithNullStorage(Map<String, String> properties) {\n    return CatalogTableTestUtils$.MODULE$.createCatalogTable(\n        \"tbl\" /* tableName */,\n        Option.empty() /* catalogName */,\n        properties,\n        new HashMap<>() /* storageProperties */,\n        Option.empty() /* locationUri */,\n        true /* nullStorage */,\n        false /* nullStorageProperties */);\n  }\n\n  private static CatalogTable catalogTableWithNullStorageProperties(\n      Map<String, String> properties) {\n    return CatalogTableTestUtils$.MODULE$.createCatalogTable(\n        \"tbl\" /* tableName */,\n        Option.empty() /* catalogName */,\n        properties,\n        new HashMap<>() /* storageProperties */,\n        Option.empty() /* locationUri */,\n        false /* nullStorage */,\n        true /* nullStorageProperties */);\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/resources/delta/dbr_8_0_non_generated_columns/_delta_log/00000000000000000000.crc",
    "content": "{\"tableSizeBytes\":422,\"numFiles\":1,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0}\n"
  },
  {
    "path": "spark/src/test/resources/delta/dbr_8_0_non_generated_columns/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1617557139648,\"operation\":\"CREATE TABLE AS SELECT\",\"operationParameters\":{\"isManaged\":\"false\",\"description\":null,\"partitionBy\":\"[]\",\"properties\":\"{}\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputBytes\":\"422\",\"numOutputRows\":\"0\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"027fb01c-94aa-4cab-87cb-5aab6aec6d17\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.generationExpression\\\":\\\"c1 + 1\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1617557137253}}\n{\"add\":{\"path\":\"part-00000-74e02f0d-e727-46e5-8d74-779d2abd616e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":422,\"modificationTime\":1617557139000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":0,\\\"minValues\\\":{},\\\"maxValues\\\":{},\\\"nullCount\\\":{}}\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/dbr_8_1_generated_columns/_delta_log/00000000000000000000.crc",
    "content": "{\"tableSizeBytes\":0,\"numFiles\":0,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0}\n"
  },
  {
    "path": "spark/src/test/resources/delta/dbr_8_1_generated_columns/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1617556462951,\"operation\":\"CREATE TABLE\",\"operationParameters\":{\"isManaged\":\"false\",\"description\":null,\"partitionBy\":\"[]\",\"properties\":\"{}\"},\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"operationMetrics\":{}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":4}}\n{\"metaData\":{\"id\":\"b406888a-3eb9-4dd5-a81a-ed0b0b535c00\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"c1\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"c2\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.generationExpression\\\":\\\"c1 + 1\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1617556462734}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-0.1.0/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":1}}\n{\"metaData\":{\"id\":\"2edf2c02-bb63-44e9-a84c-517fad0db296\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{}}}\n{\"add\":{\"path\":\"part-00000-f4aeebd0-a689-4e1b-bc7a-bbb0ec59dce5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":525,\"modificationTime\":1501109075000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-f1cb1cf9-7a73-439c-b0ea-dcba5c2280a6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":534,\"modificationTime\":1501109075000,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-0.1.0/_delta_log/00000000000000000001.json",
    "content": "{\"remove\":{\"path\":\"part-00001-f1cb1cf9-7a73-439c-b0ea-dcba5c2280a6-c000.snappy.parquet\",\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-f4aeebd0-a689-4e1b-bc7a-bbb0ec59dce5-c000.snappy.parquet\",\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-0.1.0/_delta_log/00000000000000000002.json",
    "content": "{\"txn\":{\"appId\":\"txnId\",\"version\":0}}\n{\"add\":{\"path\":\"part-00000-348d7f43-38f6-4778-88c7-45f379471c49-c000.snappy.parquet\",\"partitionValues\":{},\"size\":525,\"modificationTime\":1501109075000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-6d252218-2632-416e-9e46-f32316ec314a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":534,\"modificationTime\":1501109075000,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-0.1.0/_delta_log/00000000000000000003.json",
    "content": "{\"metaData\":{\"id\":\"2edf2c02-bb63-44e9-a84c-517fad0db296\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"id\"],\"configuration\":{}}}\n{\"remove\":{\"path\":\"part-00001-6d252218-2632-416e-9e46-f32316ec314a-c000.snappy.parquet\",\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-348d7f43-38f6-4778-88c7-45f379471c49-c000.snappy.parquet\",\"dataChange\":true}}\n{\"add\":{\"path\":\"id=5/part-00000-f1e0b560-ca00-409e-a274-f1ab264bc412.c000.snappy.parquet\",\"partitionValues\":{\"id\":\"5\"},\"size\":362,\"modificationTime\":1501109076000,\"dataChange\":true}}\n{\"add\":{\"path\":\"id=6/part-00000-adb59f54-6b8f-4bfd-9915-ae26bd0f0e2c.c000.snappy.parquet\",\"partitionValues\":{\"id\":\"6\"},\"size\":362,\"modificationTime\":1501109076000,\"dataChange\":true}}\n{\"add\":{\"path\":\"id=4/part-00001-36c738bf-7836-479b-9cc1-7a4934207856.c000.snappy.parquet\",\"partitionValues\":{\"id\":\"4\"},\"size\":362,\"modificationTime\":1501109076000,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-0.1.0/_delta_log/_last_checkpoint",
    "content": "{\"version\":3,\"size\":6}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-1.2.1/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"fbfd25ac-9401-4dac-a644-ae543f02cc0f\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1657517977667}}\n{\"add\":{\"path\":\"part-00000-87624dd4-c6dc-4163-a4e6-0e50caa28760-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1124,\"modificationTime\":1657517977000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":11,\\\"minValues\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"value\\\":10,\\\"col1\\\":6,\\\"col2\\\":2},\\\"nullCount\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"commitInfo\":{\"timestamp\":1657517977863,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"11\",\"numOutputBytes\":\"1124\"},\"engineInfo\":\"Apache-Spark/3.2.1 Delta-Lake/1.2.1\",\"txnId\":\"57be32c2-4b7d-415a-96a0-1499caf659e5\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-1.2.1/_delta_log/00000000000000000001.json",
    "content": "{\"metaData\":{\"id\":\"fbfd25ac-9401-4dac-a644-ae543f02cc0f\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col1\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"col2\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpointInterval\":\"2\"},\"createdTime\":1657517977667}}\n{\"commitInfo\":{\"timestamp\":1657517989647,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.checkpointInterval\\\":\\\"2\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"engineInfo\":\"Apache-Spark/3.2.1 Delta-Lake/1.2.1\",\"txnId\":\"b53af69e-b0aa-423b-af05-c3bff1c35a11\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-1.2.1/_delta_log/00000000000000000002.json",
    "content": "{\"add\":{\"path\":\"part-00000-59316e80-0f6c-491a-9716-5e0419434e46-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1124,\"modificationTime\":1657517994000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":11,\\\"minValues\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"value\\\":10,\\\"col1\\\":6,\\\"col2\\\":2},\\\"nullCount\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"commitInfo\":{\"timestamp\":1657517994301,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"11\",\"numOutputBytes\":\"1124\"},\"engineInfo\":\"Apache-Spark/3.2.1 Delta-Lake/1.2.1\",\"txnId\":\"6e6280cc-b8af-4e60-b2e1-766690b9faee\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-1.2.1/_delta_log/00000000000000000003.json",
    "content": "{\"add\":{\"path\":\"part-00000-635b7994-d3f9-4623-b032-8a9c8a7ca5b9-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1124,\"modificationTime\":1657518013000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":11,\\\"minValues\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"value\\\":10,\\\"col1\\\":6,\\\"col2\\\":2},\\\"nullCount\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"commitInfo\":{\"timestamp\":1657518013762,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"11\",\"numOutputBytes\":\"1124\"},\"engineInfo\":\"Apache-Spark/3.2.1 Delta-Lake/1.2.1\",\"txnId\":\"8e95e72f-dee7-4e0b-abb6-a47b4bcc46d2\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-1.2.1/_delta_log/00000000000000000004.json",
    "content": "{\"remove\":{\"path\":\"part-00000-635b7994-d3f9-4623-b032-8a9c8a7ca5b9-c000.snappy.parquet\",\"deletionTimestamp\":1657518515173,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1124}}\n{\"remove\":{\"path\":\"part-00000-87624dd4-c6dc-4163-a4e6-0e50caa28760-c000.snappy.parquet\",\"deletionTimestamp\":1657518515173,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1124}}\n{\"remove\":{\"path\":\"part-00000-59316e80-0f6c-491a-9716-5e0419434e46-c000.snappy.parquet\",\"deletionTimestamp\":1657518515173,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1124}}\n{\"add\":{\"path\":\"part-00000-e107d259-11d5-4e5b-b472-62daa676743b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1124,\"modificationTime\":1657518515000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":11,\\\"minValues\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"value\\\":10,\\\"col1\\\":8,\\\"col2\\\":2},\\\"nullCount\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-91d10124-a73d-42c2-9ef0-75ed41ca73d8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1124,\"modificationTime\":1657518515000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":11,\\\"minValues\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"value\\\":10,\\\"col1\\\":8,\\\"col2\\\":2},\\\"nullCount\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00002-dca394a5-9d0a-4630-a90a-a8f7f675e4e4-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1124,\"modificationTime\":1657518515000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":11,\\\"minValues\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0},\\\"maxValues\\\":{\\\"value\\\":10,\\\"col1\\\":8,\\\"col2\\\":2},\\\"nullCount\\\":{\\\"value\\\":0,\\\"col1\\\":0,\\\"col2\\\":0}}\"}}\n{\"commitInfo\":{\"timestamp\":1657518515749,\"operation\":\"UPDATE\",\"operationParameters\":{\"predicate\":\"(col2#477L = 2)\"},\"readVersion\":3,\"isolationLevel\":\"Serializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"3\",\"numCopiedRows\":\"24\",\"executionTimeMs\":\"2306\",\"scanTimeMs\":\"1738\",\"numAddedFiles\":\"3\",\"numUpdatedRows\":\"9\",\"rewriteTimeMs\":\"568\"},\"engineInfo\":\"Apache-Spark/3.2.1 Delta-Lake/1.2.1\",\"txnId\":\"342d874b-a8e5-49a0-8641-7e5b2285d7cb\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/delta-1.2.1/_delta_log/_last_checkpoint",
    "content": "{\"version\":4,\"size\":8}\n"
  },
  {
    "path": "spark/src/test/resources/delta/history/delta-0.2.0/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1564524295023,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isBlindAppend\":true}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"22ef18ba-191c-4c36-a606-3dad5cdf3830\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1564524294376}}\n{\"add\":{\"path\":\"part-00000-b44fcdb0-8b06-4f3a-8606-f8311a96f6dc-c000.snappy.parquet\",\"partitionValues\":{},\"size\":396,\"modificationTime\":1564524294000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-185eca06-e017-4dea-ae49-fc48b973e37e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":400,\"modificationTime\":1564524294000,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/history/delta-0.2.0/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1564524296741,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":0,\"isBlindAppend\":true}}\n{\"add\":{\"path\":\"part-00000-512e1537-8aaa-4193-b8b4-bef3de0de409-c000.snappy.parquet\",\"partitionValues\":{},\"size\":396,\"modificationTime\":1564524296000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-4327c977-2734-4477-9507-7ccf67924649-c000.snappy.parquet\",\"partitionValues\":{},\"size\":400,\"modificationTime\":1564524296000,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/history/delta-0.2.0/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1564524298214,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isBlindAppend\":false}}\n{\"add\":{\"path\":\"part-00000-7c2deba3-1994-4fb8-bc07-d46c948aa415-c000.snappy.parquet\",\"partitionValues\":{},\"size\":396,\"modificationTime\":1564524297000,\"dataChange\":true}}\n{\"add\":{\"path\":\"part-00001-c373a5bd-85f0-4758-815e-7eb62007a15c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":400,\"modificationTime\":1564524297000,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-512e1537-8aaa-4193-b8b4-bef3de0de409-c000.snappy.parquet\",\"deletionTimestamp\":1564524298213,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00000-b44fcdb0-8b06-4f3a-8606-f8311a96f6dc-c000.snappy.parquet\",\"deletionTimestamp\":1564524298214,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-185eca06-e017-4dea-ae49-fc48b973e37e-c000.snappy.parquet\",\"deletionTimestamp\":1564524298214,\"dataChange\":true}}\n{\"remove\":{\"path\":\"part-00001-4327c977-2734-4477-9507-7ccf67924649-c000.snappy.parquet\",\"deletionTimestamp\":1564524298214,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/history/delta-0.2.0/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1564524299648,\"operation\":\"STREAMING UPDATE\",\"operationParameters\":{\"outputMode\":\"Append\",\"queryId\":\"e4a20b59-dd0e-4c50-b074-e8ae4786df30\",\"epochId\":\"0\"},\"readVersion\":2,\"isBlindAppend\":true}}\n{\"txn\":{\"appId\":\"e4a20b59-dd0e-4c50-b074-e8ae4786df30\",\"version\":0,\"lastUpdated\":1564524299648}}\n{\"add\":{\"path\":\"part-00000-cb6b150b-30b8-4662-ad28-ff32ddab96d2-c000.snappy.parquet\",\"partitionValues\":{},\"size\":404,\"modificationTime\":1564524299000,\"dataChange\":true}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/history/delta-0.2.0/_delta_log/_last_checkpoint",
    "content": "{\"version\":3,\"size\":10}\n"
  },
  {
    "path": "spark/src/test/resources/delta/identity_test_written_by_version_5/_delta_log/00000000000000000000.crc",
    "content": "{\"tableSizeBytes\":2303,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0,\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2},\"metadata\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.identity.start\\\":1,\\\"delta.identity.step\\\":1,\\\"delta.identity.highWaterMark\\\":4,\\\"delta.identity.allowExplicitInsert\\\":true}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1638474481770}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/identity_test_written_by_version_5/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.identity.start\\\":1,\\\"delta.identity.step\\\":1,\\\"delta.identity.highWaterMark\\\":4,\\\"delta.identity.allowExplicitInsert\\\":true}},{\\\"name\\\":\\\"part\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1638474481770}}\n{\"add\":{\"path\":\"part-00000-1ec4087c-3109-48b4-9e1c-c44cad50f3d8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1149,\"modificationTime\":1638474496727,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"id\\\":1,\\\"part\\\":1,\\\"value\\\":\\\"one\\\"},\\\"maxValues\\\":{\\\"id\\\":2,\\\"part\\\":2,\\\"value\\\":\\\"two\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"part\\\":0,\\\"value\\\":0}}\",\"tags\":{\"INSERTION_TIME\":\"1638474496727000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00001-77d98c61-0299-4a5a-b68d-305cab1a46f6-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1154,\"modificationTime\":1638474496727,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":4,\\\"part\\\":3,\\\"value\\\":\\\"three\\\"},\\\"maxValues\\\":{\\\"id\\\":4,\\\"part\\\":3,\\\"value\\\":\\\"three\\\"},\\\"nullCount\\\":{\\\"id\\\":0,\\\"part\\\":0,\\\"value\\\":0}}\",\"tags\":{\"INSERTION_TIME\":\"1638474496727001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"commitInfo\":{\"timestamp\":1638474497049,\"operation\":\"CREATE TABLE AS SELECT\",\"operationParameters\":{\"isManaged\":\"false\",\"description\":null,\"partitionBy\":\"[]\",\"properties\":\"{}\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"3\",\"numOutputBytes\":\"2303\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/partitioned-table-with-dv-large/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1675465305121,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[\\\"partCol\\\"]\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"10\",\"numOutputRows\":\"2000\",\"numOutputBytes\":\"13989\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"ec179bfe-cc75-442f-bf1f-75a7a499d1ae\"}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"partCol\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[\"partCol\"],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1675465301176}}\n{\"add\":{\"path\":\"partCol=0/part-00000-757a3870-38dd-41ac-86f1-e1e6826df6bc.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"0\"},\"size\":1399,\"modificationTime\":1675465304390,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":1990},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390000\",\"MIN_INSERTION_TIME\":\"1675465304390000\",\"MAX_INSERTION_TIME\":\"1675465304390000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=1/part-00000-ffe81e1a-1a1f-4803-bc2a-e68f7b2ea122.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"1\"},\"size\":1399,\"modificationTime\":1675465304503,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":1},\\\"maxValues\\\":{\\\"id\\\":1991},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390001\",\"MIN_INSERTION_TIME\":\"1675465304390001\",\"MAX_INSERTION_TIME\":\"1675465304390001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=2/part-00000-5963000f-3e52-4c43-a106-d7e527f5722a.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"2\"},\"size\":1399,\"modificationTime\":1675465304550,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":2},\\\"maxValues\\\":{\\\"id\\\":1992},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390002\",\"MIN_INSERTION_TIME\":\"1675465304390002\",\"MAX_INSERTION_TIME\":\"1675465304390002\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=3/part-00000-068d9a17-0362-43f9-ad68-6bfcbd27448d.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"3\"},\"size\":1397,\"modificationTime\":1675465304596,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":3},\\\"maxValues\\\":{\\\"id\\\":1993},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390003\",\"MIN_INSERTION_TIME\":\"1675465304390003\",\"MAX_INSERTION_TIME\":\"1675465304390003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=4/part-00000-c66868e5-d1e0-4f22-ae89-9cc4d2a133fa.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"4\"},\"size\":1400,\"modificationTime\":1675465304641,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":4},\\\"maxValues\\\":{\\\"id\\\":1994},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390004\",\"MIN_INSERTION_TIME\":\"1675465304390004\",\"MAX_INSERTION_TIME\":\"1675465304390004\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=5/part-00000-70dbcf83-e5c0-4c91-8e1a-be86f08b98f4.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"5\"},\"size\":1399,\"modificationTime\":1675465304685,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":1995},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390005\",\"MIN_INSERTION_TIME\":\"1675465304390005\",\"MAX_INSERTION_TIME\":\"1675465304390005\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=6/part-00000-34e763ec-3291-4cd0-9b90-fd2d24c68098.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"6\"},\"size\":1399,\"modificationTime\":1675465304728,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":6},\\\"maxValues\\\":{\\\"id\\\":1996},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390006\",\"MIN_INSERTION_TIME\":\"1675465304390006\",\"MAX_INSERTION_TIME\":\"1675465304390006\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=7/part-00000-f43c32e8-3996-43ae-9b14-9b7f8fec6221.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"7\"},\"size\":1399,\"modificationTime\":1675465304770,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":7},\\\"maxValues\\\":{\\\"id\\\":1997},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390007\",\"MIN_INSERTION_TIME\":\"1675465304390007\",\"MAX_INSERTION_TIME\":\"1675465304390007\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=8/part-00000-a1137e9e-5425-4589-b039-84378f061fc4.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"8\"},\"size\":1399,\"modificationTime\":1675465304879,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":8},\\\"maxValues\\\":{\\\"id\\\":1998},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390008\",\"MIN_INSERTION_TIME\":\"1675465304390008\",\"MAX_INSERTION_TIME\":\"1675465304390008\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=9/part-00000-6bcf7302-8e23-4613-aec2-02856f8f1d05.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"9\"},\"size\":1399,\"modificationTime\":1675465304928,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":9},\\\"maxValues\\\":{\\\"id\\\":1999},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390009\",\"MIN_INSERTION_TIME\":\"1675465304390009\",\"MAX_INSERTION_TIME\":\"1675465304390009\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/partitioned-table-with-dv-large/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1675465322730,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(spark_catalog.delta.`/private/var/folders/g3/hcd28y8s71s0yh7whh443wz00000gp/T/spark-2434260e-1ecd-45b0-b08a-62dd7928b9ae`.id IN (0, 180, 308, 225, 756, 1007, 1503))\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"6\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"11013\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"7\",\"scanTimeMs\":\"10438\",\"numAddedFiles\":\"0\",\"rewriteTimeMs\":\"557\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"bf3a73e8-ad42-4a6a-8c7f-4430e1891c36\"}}\n{\"remove\":{\"path\":\"partCol=8/part-00000-a1137e9e-5425-4589-b039-84378f061fc4.c000.snappy.parquet\",\"deletionTimestamp\":1675465322727,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"8\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390008\",\"MIN_INSERTION_TIME\":\"1675465304390008\",\"MAX_INSERTION_TIME\":\"1675465304390008\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"partCol=5/part-00000-70dbcf83-e5c0-4c91-8e1a-be86f08b98f4.c000.snappy.parquet\",\"deletionTimestamp\":1675465322727,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"5\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390005\",\"MIN_INSERTION_TIME\":\"1675465304390005\",\"MAX_INSERTION_TIME\":\"1675465304390005\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"partCol=3/part-00000-068d9a17-0362-43f9-ad68-6bfcbd27448d.c000.snappy.parquet\",\"deletionTimestamp\":1675465322727,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"3\"},\"size\":1397,\"tags\":{\"INSERTION_TIME\":\"1675465304390003\",\"MIN_INSERTION_TIME\":\"1675465304390003\",\"MAX_INSERTION_TIME\":\"1675465304390003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"partCol=7/part-00000-f43c32e8-3996-43ae-9b14-9b7f8fec6221.c000.snappy.parquet\",\"deletionTimestamp\":1675465322727,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"7\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390007\",\"MIN_INSERTION_TIME\":\"1675465304390007\",\"MAX_INSERTION_TIME\":\"1675465304390007\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"partCol=6/part-00000-34e763ec-3291-4cd0-9b90-fd2d24c68098.c000.snappy.parquet\",\"deletionTimestamp\":1675465322727,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"6\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390006\",\"MIN_INSERTION_TIME\":\"1675465304390006\",\"MAX_INSERTION_TIME\":\"1675465304390006\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"partCol=0/part-00000-757a3870-38dd-41ac-86f1-e1e6826df6bc.c000.snappy.parquet\",\"deletionTimestamp\":1675465322727,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"0\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390000\",\"MIN_INSERTION_TIME\":\"1675465304390000\",\"MAX_INSERTION_TIME\":\"1675465304390000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=8/part-00000-a1137e9e-5425-4589-b039-84378f061fc4.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"8\"},\"size\":1399,\"modificationTime\":1675465304879,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":8},\\\"maxValues\\\":{\\\"id\\\":1998},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390008\",\"MIN_INSERTION_TIME\":\"1675465304390008\",\"MAX_INSERTION_TIME\":\"1675465304390008\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"partCol=5/part-00000-70dbcf83-e5c0-4c91-8e1a-be86f08b98f4.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"5\"},\"size\":1399,\"modificationTime\":1675465304685,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":5},\\\"maxValues\\\":{\\\"id\\\":1995},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390005\",\"MIN_INSERTION_TIME\":\"1675465304390005\",\"MAX_INSERTION_TIME\":\"1675465304390005\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":43,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"partCol=3/part-00000-068d9a17-0362-43f9-ad68-6bfcbd27448d.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"3\"},\"size\":1397,\"modificationTime\":1675465304596,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":3},\\\"maxValues\\\":{\\\"id\\\":1993},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390003\",\"MIN_INSERTION_TIME\":\"1675465304390003\",\"MAX_INSERTION_TIME\":\"1675465304390003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":85,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"partCol=7/part-00000-f43c32e8-3996-43ae-9b14-9b7f8fec6221.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"7\"},\"size\":1399,\"modificationTime\":1675465304770,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":7},\\\"maxValues\\\":{\\\"id\\\":1997},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390007\",\"MIN_INSERTION_TIME\":\"1675465304390007\",\"MAX_INSERTION_TIME\":\"1675465304390007\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":127,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"partCol=6/part-00000-34e763ec-3291-4cd0-9b90-fd2d24c68098.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"6\"},\"size\":1399,\"modificationTime\":1675465304728,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":6},\\\"maxValues\\\":{\\\"id\\\":1996},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390006\",\"MIN_INSERTION_TIME\":\"1675465304390006\",\"MAX_INSERTION_TIME\":\"1675465304390006\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":169,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"partCol=0/part-00000-757a3870-38dd-41ac-86f1-e1e6826df6bc.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"0\"},\"size\":1399,\"modificationTime\":1675465304390,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":1990},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390000\",\"MIN_INSERTION_TIME\":\"1675465304390000\",\"MAX_INSERTION_TIME\":\"1675465304390000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":211,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/partitioned-table-with-dv-large/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1675465324584,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"partCol\\\"]\"},\"readVersion\":1,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"1172\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"c72d2694-23fb-4adc-a315-8ee8c30853b0\"}}\n{\"add\":{\"path\":\"partCol=6/part-00000-2dee959e-3d92-4c43-ac01-24d888ba82fd.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"6\"},\"size\":586,\"modificationTime\":1675465324549,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":756},\\\"maxValues\\\":{\\\"id\\\":756},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465324549000\",\"MIN_INSERTION_TIME\":\"1675465324549000\",\"MAX_INSERTION_TIME\":\"1675465324549000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=8/part-00000-fe120a67-87dc-4997-8811-3ad9d8dc3743.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"8\"},\"size\":586,\"modificationTime\":1675465324578,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":308},\\\"maxValues\\\":{\\\"id\\\":308},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465324549001\",\"MIN_INSERTION_TIME\":\"1675465324549001\",\"MAX_INSERTION_TIME\":\"1675465324549001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/partitioned-table-with-dv-large/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1675465327086,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(spark_catalog.delta.`/private/var/folders/g3/hcd28y8s71s0yh7whh443wz00000gp/T/spark-2434260e-1ecd-45b0-b08a-62dd7928b9ae`.id IN (300, 257, 399, 786, 1353, 1567, 1800))\\\"]\"},\"readVersion\":2,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"1484\",\"numDeletionVectorsUpdated\":\"4\",\"numDeletedRows\":\"7\",\"scanTimeMs\":\"779\",\"numAddedFiles\":\"0\",\"rewriteTimeMs\":\"703\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"67b81203-e0e8-4eca-bb04-5806f4b1cad5\"}}\n{\"remove\":{\"path\":\"partCol=0/part-00000-757a3870-38dd-41ac-86f1-e1e6826df6bc.c000.snappy.parquet\",\"deletionTimestamp\":1675465327084,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"0\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390000\",\"MIN_INSERTION_TIME\":\"1675465304390000\",\"MAX_INSERTION_TIME\":\"1675465304390000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":211,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"remove\":{\"path\":\"partCol=3/part-00000-068d9a17-0362-43f9-ad68-6bfcbd27448d.c000.snappy.parquet\",\"deletionTimestamp\":1675465327084,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"3\"},\"size\":1397,\"tags\":{\"INSERTION_TIME\":\"1675465304390003\",\"MIN_INSERTION_TIME\":\"1675465304390003\",\"MAX_INSERTION_TIME\":\"1675465304390003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":85,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"partCol=6/part-00000-34e763ec-3291-4cd0-9b90-fd2d24c68098.c000.snappy.parquet\",\"deletionTimestamp\":1675465327084,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"6\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390006\",\"MIN_INSERTION_TIME\":\"1675465304390006\",\"MAX_INSERTION_TIME\":\"1675465304390006\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":169,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"partCol=7/part-00000-f43c32e8-3996-43ae-9b14-9b7f8fec6221.c000.snappy.parquet\",\"deletionTimestamp\":1675465327084,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"7\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390007\",\"MIN_INSERTION_TIME\":\"1675465304390007\",\"MAX_INSERTION_TIME\":\"1675465304390007\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"24t<Go!]IbX*CEDm3}TI\",\"offset\":127,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"partCol=9/part-00000-6bcf7302-8e23-4613-aec2-02856f8f1d05.c000.snappy.parquet\",\"deletionTimestamp\":1675465327084,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{\"partCol\":\"9\"},\"size\":1399,\"tags\":{\"INSERTION_TIME\":\"1675465304390009\",\"MIN_INSERTION_TIME\":\"1675465304390009\",\"MAX_INSERTION_TIME\":\"1675465304390009\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=0/part-00000-757a3870-38dd-41ac-86f1-e1e6826df6bc.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"0\"},\"size\":1399,\"modificationTime\":1675465304390,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":1990},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390000\",\"MIN_INSERTION_TIME\":\"1675465304390000\",\"MAX_INSERTION_TIME\":\"1675465304390000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Xnn-=:A*jwFzeMNSY9zC\",\"offset\":87,\"sizeInBytes\":40,\"cardinality\":4}}}\n{\"add\":{\"path\":\"partCol=3/part-00000-068d9a17-0362-43f9-ad68-6bfcbd27448d.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"3\"},\"size\":1397,\"modificationTime\":1675465304596,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":3},\\\"maxValues\\\":{\\\"id\\\":1993},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390003\",\"MIN_INSERTION_TIME\":\"1675465304390003\",\"MAX_INSERTION_TIME\":\"1675465304390003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Xnn-=:A*jwFzeMNSY9zC\",\"offset\":43,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"add\":{\"path\":\"partCol=6/part-00000-34e763ec-3291-4cd0-9b90-fd2d24c68098.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"6\"},\"size\":1399,\"modificationTime\":1675465304728,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":6},\\\"maxValues\\\":{\\\"id\\\":1996},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390006\",\"MIN_INSERTION_TIME\":\"1675465304390006\",\"MAX_INSERTION_TIME\":\"1675465304390006\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Xnn-=:A*jwFzeMNSY9zC\",\"offset\":181,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"add\":{\"path\":\"partCol=7/part-00000-f43c32e8-3996-43ae-9b14-9b7f8fec6221.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"7\"},\"size\":1399,\"modificationTime\":1675465304770,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":7},\\\"maxValues\\\":{\\\"id\\\":1997},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390007\",\"MIN_INSERTION_TIME\":\"1675465304390007\",\"MAX_INSERTION_TIME\":\"1675465304390007\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Xnn-=:A*jwFzeMNSY9zC\",\"offset\":135,\"sizeInBytes\":38,\"cardinality\":3}}}\n{\"add\":{\"path\":\"partCol=9/part-00000-6bcf7302-8e23-4613-aec2-02856f8f1d05.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"9\"},\"size\":1399,\"modificationTime\":1675465304928,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":200,\\\"minValues\\\":{\\\"id\\\":9},\\\"maxValues\\\":{\\\"id\\\":1999},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1675465304390009\",\"MIN_INSERTION_TIME\":\"1675465304390009\",\"MAX_INSERTION_TIME\":\"1675465304390009\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"Xnn-=:A*jwFzeMNSY9zC\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/partitioned-table-with-dv-large/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1675465328505,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[\\\"partCol\\\"]\"},\"readVersion\":3,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"1171\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"14dd0cb9-4d96-487f-af5b-1e29a5c1fa70\"}}\n{\"add\":{\"path\":\"partCol=3/part-00000-8775b518-3470-41d4-8d7e-27596c48053e.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"3\"},\"size\":585,\"modificationTime\":1675465328471,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1353},\\\"maxValues\\\":{\\\"id\\\":1353},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465328471000\",\"MIN_INSERTION_TIME\":\"1675465328471000\",\"MAX_INSERTION_TIME\":\"1675465328471000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"partCol=7/part-00000-156df4a5-759c-4b9f-82b1-9727a62b7990.c000.snappy.parquet\",\"partitionValues\":{\"partCol\":\"7\"},\"size\":586,\"modificationTime\":1675465328500,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"id\\\":1567},\\\"maxValues\\\":{\\\"id\\\":1567},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1675465328471001\",\"MIN_INSERTION_TIME\":\"1675465328471001\",\"MAX_INSERTION_TIME\":\"1675465328471001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-gigantic/_delta_log/00000000000000000000.json",
    "content": "{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1682351914000}}\n{\"add\":{\"path\":\"part-00000-2bc940f0-dd3f-461d-8581-136026bf6f95-c000.snappy.parquet\",\"partitionValues\":{},\"size\":8473865,\"modificationTime\":1682351914339,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2147483658,\\\"minValues\\\":{\\\"value\\\":0},\\\"maxValues\\\":{\\\"value\\\":21},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"o6J(G4p@f*QZS+b{khvI\",\"offset\":1,\"sizeInBytes\":4557136,\"cardinality\":2147484}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-large/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1674064770682,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"20\",\"numOutputRows\":\"2000\",\"numOutputBytes\":\"20157\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"f0ddc566-dfe6-4bd8-b264-ce100f9362ef\"}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1674064767118}}\n{\"add\":{\"path\":\"part-00000-f5c18e7b-d1bf-4ba5-85dd-e63ddc5931bf-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064769860,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":4},\\\"maxValues\\\":{\\\"value\\\":1967},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860000\",\"MIN_INSERTION_TIME\":\"1674064769860000\",\"MAX_INSERTION_TIME\":\"1674064769860000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00001-5dbf0ba2-220a-4770-8e26-18a77cf875f0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064769860,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":18},\\\"maxValues\\\":{\\\"value\\\":1988},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860001\",\"MIN_INSERTION_TIME\":\"1674064769860001\",\"MAX_INSERTION_TIME\":\"1674064769860001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00002-5459a52f-3fd3-4b79-83a6-e7f57db28650-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1007,\"modificationTime\":1674064770019,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":16},\\\"maxValues\\\":{\\\"value\\\":1977},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860002\",\"MIN_INSERTION_TIME\":\"1674064769860002\",\"MAX_INSERTION_TIME\":\"1674064769860002\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00003-0e842060-9e04-4896-ba21-029309ab8736-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770019,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":5},\\\"maxValues\\\":{\\\"value\\\":1982},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860003\",\"MIN_INSERTION_TIME\":\"1674064769860003\",\"MAX_INSERTION_TIME\":\"1674064769860003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00004-a72dbdec-2d0e-43d8-a756-4d0d63ef9fcb-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770100,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":1},\\\"maxValues\\\":{\\\"value\\\":1999},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860004\",\"MIN_INSERTION_TIME\":\"1674064769860004\",\"MAX_INSERTION_TIME\":\"1674064769860004\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00005-0972979f-852d-4f3e-8f64-bf0bf072de5f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770100,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":8},\\\"maxValues\\\":{\\\"value\\\":1914},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860005\",\"MIN_INSERTION_TIME\":\"1674064769860005\",\"MAX_INSERTION_TIME\":\"1674064769860005\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00006-227c6a1e-0180-4feb-8816-19eccf7939f5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770207,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":30},\\\"maxValues\\\":{\\\"value\\\":1992},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860006\",\"MIN_INSERTION_TIME\":\"1674064769860006\",\"MAX_INSERTION_TIME\":\"1674064769860006\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00007-7c37e5e3-abb2-419e-8cba-eba4eeb3b11a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770207,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":40},\\\"maxValues\\\":{\\\"value\\\":1990},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860007\",\"MIN_INSERTION_TIME\":\"1674064769860007\",\"MAX_INSERTION_TIME\":\"1674064769860007\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00008-1a0b4375-bbcc-4f3c-8e51-ecb551c89430-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770265,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":13},\\\"maxValues\\\":{\\\"value\\\":1897},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860008\",\"MIN_INSERTION_TIME\":\"1674064769860008\",\"MAX_INSERTION_TIME\":\"1674064769860008\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00009-52689115-1770-4f15-b98d-b942db5b7359-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770265,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":12},\\\"maxValues\\\":{\\\"value\\\":1987},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860009\",\"MIN_INSERTION_TIME\":\"1674064769860009\",\"MAX_INSERTION_TIME\":\"1674064769860009\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00010-7f35fa1b-7993-4aff-8f60-2b76f1eb3f2c-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770319,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":19},\\\"maxValues\\\":{\\\"value\\\":1993},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860010\",\"MIN_INSERTION_TIME\":\"1674064769860010\",\"MAX_INSERTION_TIME\":\"1674064769860010\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00011-fce7841f-be9a-43b8-b283-9e2308ef5487-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770319,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":11},\\\"maxValues\\\":{\\\"value\\\":1984},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860011\",\"MIN_INSERTION_TIME\":\"1674064769860011\",\"MAX_INSERTION_TIME\":\"1674064769860011\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00012-9b83c213-31ff-4b2c-a5d9-be1a2bc2431d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770372,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":33},\\\"maxValues\\\":{\\\"value\\\":1995},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860012\",\"MIN_INSERTION_TIME\":\"1674064769860012\",\"MAX_INSERTION_TIME\":\"1674064769860012\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00013-c6b05dd2-0143-4e9f-a231-1a2d08a83a0e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770372,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":20},\\\"maxValues\\\":{\\\"value\\\":1974},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860013\",\"MIN_INSERTION_TIME\":\"1674064769860013\",\"MAX_INSERTION_TIME\":\"1674064769860013\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00014-41a4f51e-62cd-41f5-bb03-afba1e70ea29-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1007,\"modificationTime\":1674064770427,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":3},\\\"maxValues\\\":{\\\"value\\\":1996},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860014\",\"MIN_INSERTION_TIME\":\"1674064769860014\",\"MAX_INSERTION_TIME\":\"1674064769860014\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00015-f2f141bb-fa8f-4553-a5db-d1b8d682153b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770427,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":0},\\\"maxValues\\\":{\\\"value\\\":1997},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860015\",\"MIN_INSERTION_TIME\":\"1674064769860015\",\"MAX_INSERTION_TIME\":\"1674064769860015\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00016-d8f58ffc-8bff-4e12-b709-e628f9bf2553-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770477,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":2},\\\"maxValues\\\":{\\\"value\\\":1986},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860016\",\"MIN_INSERTION_TIME\":\"1674064769860016\",\"MAX_INSERTION_TIME\":\"1674064769860016\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00017-45bac3c9-7eb8-42cb-bb51-fc5b4dd0be10-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770476,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":22},\\\"maxValues\\\":{\\\"value\\\":1998},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860017\",\"MIN_INSERTION_TIME\":\"1674064769860017\",\"MAX_INSERTION_TIME\":\"1674064769860017\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00018-9d74a51b-b800-4e4d-a258-738e585a78a5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770529,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":6},\\\"maxValues\\\":{\\\"value\\\":1983},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860018\",\"MIN_INSERTION_TIME\":\"1674064769860018\",\"MAX_INSERTION_TIME\":\"1674064769860018\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00019-a9bb3ce8-afba-47ec-8451-13edcd855b15-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1007,\"modificationTime\":1674064770528,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":36},\\\"maxValues\\\":{\\\"value\\\":1969},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860019\",\"MIN_INSERTION_TIME\":\"1674064769860019\",\"MAX_INSERTION_TIME\":\"1674064769860019\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-large/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1674064789962,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(spark_catalog.delta.`/private/var/folders/g3/hcd28y8s71s0yh7whh443wz00000gp/T/spark-f3dd4a29-dc57-42eb-b752-84179135f5b8`.value IN (0, 180, 300, 700, 1800))\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"5\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"12828\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"5\",\"scanTimeMs\":\"12323\",\"numAddedFiles\":\"0\",\"rewriteTimeMs\":\"487\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"5327cd46-c25b-4127-88fd-5b3c2402691b\"}}\n{\"remove\":{\"path\":\"part-00001-5dbf0ba2-220a-4770-8e26-18a77cf875f0-c000.snappy.parquet\",\"deletionTimestamp\":1674064789957,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860001\",\"MIN_INSERTION_TIME\":\"1674064769860001\",\"MAX_INSERTION_TIME\":\"1674064769860001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00003-0e842060-9e04-4896-ba21-029309ab8736-c000.snappy.parquet\",\"deletionTimestamp\":1674064789957,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860003\",\"MIN_INSERTION_TIME\":\"1674064769860003\",\"MAX_INSERTION_TIME\":\"1674064769860003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00012-9b83c213-31ff-4b2c-a5d9-be1a2bc2431d-c000.snappy.parquet\",\"deletionTimestamp\":1674064789957,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860012\",\"MIN_INSERTION_TIME\":\"1674064769860012\",\"MAX_INSERTION_TIME\":\"1674064769860012\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00015-f2f141bb-fa8f-4553-a5db-d1b8d682153b-c000.snappy.parquet\",\"deletionTimestamp\":1674064789957,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860015\",\"MIN_INSERTION_TIME\":\"1674064769860015\",\"MAX_INSERTION_TIME\":\"1674064769860015\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00019-a9bb3ce8-afba-47ec-8451-13edcd855b15-c000.snappy.parquet\",\"deletionTimestamp\":1674064789957,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1007,\"tags\":{\"INSERTION_TIME\":\"1674064769860019\",\"MIN_INSERTION_TIME\":\"1674064769860019\",\"MAX_INSERTION_TIME\":\"1674064769860019\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"part-00001-5dbf0ba2-220a-4770-8e26-18a77cf875f0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064769860,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":18},\\\"maxValues\\\":{\\\"value\\\":1988},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860001\",\"MIN_INSERTION_TIME\":\"1674064769860001\",\"MAX_INSERTION_TIME\":\"1674064769860001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":85,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00003-0e842060-9e04-4896-ba21-029309ab8736-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770019,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":5},\\\"maxValues\\\":{\\\"value\\\":1982},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860003\",\"MIN_INSERTION_TIME\":\"1674064769860003\",\"MAX_INSERTION_TIME\":\"1674064769860003\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":169,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00012-9b83c213-31ff-4b2c-a5d9-be1a2bc2431d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770372,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":33},\\\"maxValues\\\":{\\\"value\\\":1995},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860012\",\"MIN_INSERTION_TIME\":\"1674064769860012\",\"MAX_INSERTION_TIME\":\"1674064769860012\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00015-f2f141bb-fa8f-4553-a5db-d1b8d682153b-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770427,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":0},\\\"maxValues\\\":{\\\"value\\\":1997},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860015\",\"MIN_INSERTION_TIME\":\"1674064769860015\",\"MAX_INSERTION_TIME\":\"1674064769860015\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":43,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00019-a9bb3ce8-afba-47ec-8451-13edcd855b15-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1007,\"modificationTime\":1674064770528,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":36},\\\"maxValues\\\":{\\\"value\\\":1969},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860019\",\"MIN_INSERTION_TIME\":\"1674064769860019\",\"MAX_INSERTION_TIME\":\"1674064769860019\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":127,\"sizeInBytes\":34,\"cardinality\":1}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-large/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1674064791599,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"600\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"fb0a7015-0096-4d74-821b-3507163c17fa\"}}\n{\"add\":{\"path\":\"part-00000-51219d56-88a7-41cc-be5d-eada75aceb4f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":600,\"modificationTime\":1674064791593,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"value\\\":300},\\\"maxValues\\\":{\\\"value\\\":700},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064791593000\",\"MIN_INSERTION_TIME\":\"1674064791593000\",\"MAX_INSERTION_TIME\":\"1674064791593000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-large/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1674064797400,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(spark_catalog.delta.`/private/var/folders/g3/hcd28y8s71s0yh7whh443wz00000gp/T/spark-f3dd4a29-dc57-42eb-b752-84179135f5b8`.value IN (300, 250, 350, 900, 1353, 1567, 1800))\\\"]\"},\"readVersion\":2,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"3\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"4726\",\"numDeletionVectorsUpdated\":\"3\",\"numDeletedRows\":\"6\",\"scanTimeMs\":\"4057\",\"numAddedFiles\":\"0\",\"rewriteTimeMs\":\"667\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"d50de74c-f8c8-4e68-b120-267504045e9d\"}}\n{\"remove\":{\"path\":\"part-00000-51219d56-88a7-41cc-be5d-eada75aceb4f-c000.snappy.parquet\",\"deletionTimestamp\":1674064797399,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":600,\"tags\":{\"INSERTION_TIME\":\"1674064791593000\",\"MIN_INSERTION_TIME\":\"1674064791593000\",\"MAX_INSERTION_TIME\":\"1674064791593000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00001-5dbf0ba2-220a-4770-8e26-18a77cf875f0-c000.snappy.parquet\",\"deletionTimestamp\":1674064797399,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860001\",\"MIN_INSERTION_TIME\":\"1674064769860001\",\"MAX_INSERTION_TIME\":\"1674064769860001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":85,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part-00012-9b83c213-31ff-4b2c-a5d9-be1a2bc2431d-c000.snappy.parquet\",\"deletionTimestamp\":1674064797399,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860012\",\"MIN_INSERTION_TIME\":\"1674064769860012\",\"MAX_INSERTION_TIME\":\"1674064769860012\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"remove\":{\"path\":\"part-00014-41a4f51e-62cd-41f5-bb03-afba1e70ea29-c000.snappy.parquet\",\"deletionTimestamp\":1674064797399,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1007,\"tags\":{\"INSERTION_TIME\":\"1674064769860014\",\"MIN_INSERTION_TIME\":\"1674064769860014\",\"MAX_INSERTION_TIME\":\"1674064769860014\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00018-9d74a51b-b800-4e4d-a258-738e585a78a5-c000.snappy.parquet\",\"deletionTimestamp\":1674064797399,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1008,\"tags\":{\"INSERTION_TIME\":\"1674064769860018\",\"MIN_INSERTION_TIME\":\"1674064769860018\",\"MAX_INSERTION_TIME\":\"1674064769860018\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"remove\":{\"path\":\"part-00019-a9bb3ce8-afba-47ec-8451-13edcd855b15-c000.snappy.parquet\",\"deletionTimestamp\":1674064797399,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":1007,\"tags\":{\"INSERTION_TIME\":\"1674064769860019\",\"MIN_INSERTION_TIME\":\"1674064769860019\",\"MAX_INSERTION_TIME\":\"1674064769860019\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"m9JzgVlI!?Oy<+3x+y^b\",\"offset\":127,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00000-51219d56-88a7-41cc-be5d-eada75aceb4f-c000.snappy.parquet\",\"partitionValues\":{},\"size\":600,\"modificationTime\":1674064791593,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"value\\\":300},\\\"maxValues\\\":{\\\"value\\\":700},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064791593000\",\"MIN_INSERTION_TIME\":\"1674064791593000\",\"MAX_INSERTION_TIME\":\"1674064791593000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"UGM+pBY.mtVeP<X.WD9m\",\"offset\":173,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00001-5dbf0ba2-220a-4770-8e26-18a77cf875f0-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064769860,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":18},\\\"maxValues\\\":{\\\"value\\\":1988},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860001\",\"MIN_INSERTION_TIME\":\"1674064769860001\",\"MAX_INSERTION_TIME\":\"1674064769860001\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"UGM+pBY.mtVeP<X.WD9m\",\"offset\":43,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"add\":{\"path\":\"part-00012-9b83c213-31ff-4b2c-a5d9-be1a2bc2431d-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770372,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":33},\\\"maxValues\\\":{\\\"value\\\":1995},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860012\",\"MIN_INSERTION_TIME\":\"1674064769860012\",\"MAX_INSERTION_TIME\":\"1674064769860012\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"UGM+pBY.mtVeP<X.WD9m\",\"offset\":87,\"sizeInBytes\":36,\"cardinality\":2}}}\n{\"add\":{\"path\":\"part-00014-41a4f51e-62cd-41f5-bb03-afba1e70ea29-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1007,\"modificationTime\":1674064770427,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":3},\\\"maxValues\\\":{\\\"value\\\":1996},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860014\",\"MIN_INSERTION_TIME\":\"1674064769860014\",\"MAX_INSERTION_TIME\":\"1674064769860014\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"UGM+pBY.mtVeP<X.WD9m\",\"offset\":131,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00018-9d74a51b-b800-4e4d-a258-738e585a78a5-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1008,\"modificationTime\":1674064770529,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":6},\\\"maxValues\\\":{\\\"value\\\":1983},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860018\",\"MIN_INSERTION_TIME\":\"1674064769860018\",\"MAX_INSERTION_TIME\":\"1674064769860018\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"UGM+pBY.mtVeP<X.WD9m\",\"offset\":1,\"sizeInBytes\":34,\"cardinality\":1}}}\n{\"add\":{\"path\":\"part-00019-a9bb3ce8-afba-47ec-8451-13edcd855b15-c000.snappy.parquet\",\"partitionValues\":{},\"size\":1007,\"modificationTime\":1674064770528,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":100,\\\"minValues\\\":{\\\"value\\\":36},\\\"maxValues\\\":{\\\"value\\\":1969},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1674064769860019\",\"MIN_INSERTION_TIME\":\"1674064769860019\",\"MAX_INSERTION_TIME\":\"1674064769860019\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"UGM+pBY.mtVeP<X.WD9m\",\"offset\":215,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-large/_delta_log/00000000000000000004.json",
    "content": "{\"commitInfo\":{\"timestamp\":1674064798708,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":3,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"2\",\"numOutputBytes\":\"600\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"4016704a-babb-44a8-ae8b-c53303465742\"}}\n{\"add\":{\"path\":\"part-00000-7c52eadd-8da7-4782-a5d5-621cd92cab11-c000.snappy.parquet\",\"partitionValues\":{},\"size\":600,\"modificationTime\":1674064798704,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":2,\\\"minValues\\\":{\\\"value\\\":900},\\\"maxValues\\\":{\\\"value\\\":1567},\\\"nullCount\\\":{\\\"value\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1674064798704000\",\"MIN_INSERTION_TIME\":\"1674064798704000\",\"MAX_INSERTION_TIME\":\"1674064798704000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-small/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1673461409137,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"818\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"d54c00f5-9500-4ed5-b1b5-9f463861f4d3\"}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\",\"columnMapping\"],\"writerFeatures\":[\"deletionVectors\",\"columnMapping\"]}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{\\\"delta.columnMapping.id\\\":1,\\\"delta.columnMapping.physicalName\\\":\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\"}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.columnMapping.mode\":\"name\",\"delta.enableDeletionVectors\":\"true\",\"delta.columnMapping.maxColumnId\":\"1\"},\"createdTime\":1673461406485}}\n{\"add\":{\"path\":\"r4/part-00000-5521fc5e-6e49-4437-8b2d-ce6a1a94a34a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":818,\"modificationTime\":1673461408778,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\":0},\\\"maxValues\\\":{\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\":9},\\\"nullCount\\\":{\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\":0},\\\"tightBounds\\\":true}\",\"tags\":{\"INSERTION_TIME\":\"1673461408778000\",\"MIN_INSERTION_TIME\":\"1673461408778000\",\"MAX_INSERTION_TIME\":\"1673461408778000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-small/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1673461427387,\"operation\":\"DELETE\",\"operationParameters\":{\"predicate\":\"[\\\"(spark_catalog.delta.`/private/var/folders/g3/hcd28y8s71s0yh7whh443wz00000gp/T/spark-cb573b98-e75d-460f-9769-efd9e9bfeffc`.value IN (0, 9))\\\"]\"},\"readVersion\":0,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numRemovedFiles\":\"0\",\"numCopiedRows\":\"0\",\"numDeletionVectorsAdded\":\"1\",\"numDeletionVectorsRemoved\":\"0\",\"numAddedChangeFiles\":\"0\",\"executionTimeMs\":\"11114\",\"numDeletionVectorsUpdated\":\"0\",\"numDeletedRows\":\"2\",\"scanTimeMs\":\"10589\",\"numAddedFiles\":\"0\",\"rewriteTimeMs\":\"508\"},\"engineInfo\":\"<unknown>\",\"txnId\":\"3943baa4-30a0-44a4-a4f4-e5e92d2ab08b\"}}\n{\"remove\":{\"path\":\"r4/part-00000-5521fc5e-6e49-4437-8b2d-ce6a1a94a34a-c000.snappy.parquet\",\"deletionTimestamp\":1673461427383,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":818,\"tags\":{\"INSERTION_TIME\":\"1673461408778000\",\"MIN_INSERTION_TIME\":\"1673461408778000\",\"MAX_INSERTION_TIME\":\"1673461408778000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n{\"add\":{\"path\":\"r4/part-00000-5521fc5e-6e49-4437-8b2d-ce6a1a94a34a-c000.snappy.parquet\",\"partitionValues\":{},\"size\":818,\"modificationTime\":1673461408778,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\":0},\\\"maxValues\\\":{\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\":9},\\\"nullCount\\\":{\\\"col-4f064e48-f371-433a-b851-9e73c78fa9fc\\\":0},\\\"tightBounds\\\":false}\",\"tags\":{\"INSERTION_TIME\":\"1673461408778000\",\"MIN_INSERTION_TIME\":\"1673461408778000\",\"MAX_INSERTION_TIME\":\"1673461408778000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"},\"deletionVector\":{\"storageType\":\"u\",\"pathOrInlineDv\":\"WYbkwCTB$gH)J7t?$/sK\",\"offset\":1,\"sizeInBytes\":36,\"cardinality\":2}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/table-with-dv-special-char/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1708950820866,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"Serializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"539\"},\"engineInfo\":\"Apache-Spark/3.5.0 Delta-Lake/3.1.0-SNAPSHOT\",\"txnId\":\"1586fe39-668f-48c6-ad0b-af4a10b38f22\"}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"long\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableDeletionVectors\":\"true\"},\"createdTime\":1708950819168}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"deletionVectors\"],\"writerFeatures\":[\"deletionVectors\"]}}\n{\"add\":{\"path\":\"part-00000-8d24f407-08d3-49ab-9d1c-f7f6c129e882-c000.snappy.parquet\",\"partitionValues\":{},\"size\":539,\"modificationTime\":1708950820783,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"id\\\":0},\\\"maxValues\\\":{\\\"id\\\":9},\\\"nullCount\\\":{\\\"id\\\":0},\\\"tightBounds\\\":false}\",\"deletionVector\":{\"storageType\":\"p\",\"pathOrInlineDv\":\"file:{{FOLDER_WITH_SPECIAL_CHAR}}/test%25dv%25prefix-deletion_vector_67bc892e-2979-4760-a78c-856aba806564.bin\",\"offset\":1,\"sizeInBytes\":42,\"cardinality\":5}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000000.crc",
    "content": "{\"tableSizeBytes\":1594,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1623255695348,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"ErrorIfExists\",\"partitionBy\":\"[]\"},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1594\",\"numOutputRows\":\"9\"}}}\n{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1623255692280}}\n{\"add\":{\"path\":\"part-00000-dfb1dd9a-0fe2-420e-81d5-a84004aebcee-c000.snappy.parquet\",\"partitionValues\":{},\"size\":793,\"modificationTime\":1623255695000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"key\\\":1,\\\"value\\\":1},\\\"maxValues\\\":{\\\"key\\\":4,\\\"value\\\":4},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-d5da9c60-a615-4065-a3cb-4796d86fc797-c000.snappy.parquet\",\"partitionValues\":{},\"size\":801,\"modificationTime\":1623255695000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"key\\\":5,\\\"value\\\":5},\\\"maxValues\\\":{\\\"key\\\":9,\\\"value\\\":9},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000001.crc",
    "content": "{\"tableSizeBytes\":1594,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1623255703194,\"operation\":\"SET TBLPROPERTIES\",\"operationParameters\":{\"properties\":\"{\\\"delta.checkpoint.writeStatsAsStruct\\\":\\\"true\\\",\\\"delta.checkpoint.writeStatsAsJson\\\":\\\"false\\\"}\"},\"readVersion\":0,\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"operationMetrics\":{}}}\n{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpoint.writeStatsAsStruct\":\"true\",\"delta.checkpoint.writeStatsAsJson\":\"false\"},\"createdTime\":1623255692280}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000002.crc",
    "content": "{\"tableSizeBytes\":1594,\"numFiles\":2,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000002.json",
    "content": "{\"commitInfo\":{\"timestamp\":1623255706138,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Overwrite\",\"partitionBy\":\"[]\"},\"readVersion\":1,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":false,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1594\",\"numOutputRows\":\"9\"}}}\n{\"add\":{\"path\":\"part-00000-f654b1f4-e1ea-40e5-a8cd-452f7c3359d8-c000.snappy.parquet\",\"partitionValues\":{},\"size\":793,\"modificationTime\":1623255705000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"key\\\":1,\\\"value\\\":1},\\\"maxValues\\\":{\\\"key\\\":4,\\\"value\\\":4},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-bfb08fc5-c967-40e4-a646-c8178d8b5e21-c000.snappy.parquet\",\"partitionValues\":{},\"size\":801,\"modificationTime\":1623255705000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"key\\\":5,\\\"value\\\":5},\\\"maxValues\\\":{\\\"key\\\":9,\\\"value\\\":9},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\"}}\n{\"remove\":{\"path\":\"part-00000-dfb1dd9a-0fe2-420e-81d5-a84004aebcee-c000.snappy.parquet\",\"deletionTimestamp\":1623255706137,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":793}}\n{\"remove\":{\"path\":\"part-00001-d5da9c60-a615-4065-a3cb-4796d86fc797-c000.snappy.parquet\",\"deletionTimestamp\":1623255706138,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":801}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000003.crc",
    "content": "{\"tableSizeBytes\":3188,\"numFiles\":4,\"numMetadata\":1,\"numProtocol\":1,\"numTransactions\":0}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000003.json",
    "content": "{\"commitInfo\":{\"timestamp\":1623255724166,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"readVersion\":2,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1594\",\"numOutputRows\":\"9\"}}}\n{\"add\":{\"path\":\"part-00000-9f483b95-3ea3-44f0-b54d-73199574be15-c000.snappy.parquet\",\"partitionValues\":{},\"size\":793,\"modificationTime\":1623255724000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"key\\\":1,\\\"value\\\":1},\\\"maxValues\\\":{\\\"key\\\":4,\\\"value\\\":4},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\"}}\n{\"add\":{\"path\":\"part-00001-d1030238-b55d-48f8-a4d6-89ef12e9d501-c000.snappy.parquet\",\"partitionValues\":{},\"size\":801,\"modificationTime\":1623255724000,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"key\\\":5,\\\"value\\\":5},\\\"maxValues\\\":{\\\"key\\\":9,\\\"value\\\":9},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\"}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000004.json",
    "content": "{\"remove\":{\"path\":\"part-00000-9f483b95-3ea3-44f0-b54d-73199574be15-c000.snappy.parquet\",\"deletionTimestamp\":1623255727201,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":793}}\n{\"remove\":{\"path\":\"part-00001-d1030238-b55d-48f8-a4d6-89ef12e9d501-c000.snappy.parquet\",\"deletionTimestamp\":1623255727201,\"dataChange\":true,\"extendedFileMetadata\":true,\"partitionValues\":{},\"size\":801}}\n{\"some_new_action\":{\"a\":1}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/00000000000000000005.json",
    "content": "{\"commitInfo\":{\"timestamp\":1623255724166,\"operationMetrics\":{\"numFiles\":\"2\",\"numOutputBytes\":\"1594\",\"numOutputRows\":\"9\"},\"isolationLevel\":\"WriteSerializable\",\"operationParameters\":{\"mode\":\"Append\",\"partitionBy\":\"[]\"},\"operation\":\"WRITE\",\"isBlindAppend\":true,\"readVersion\":2},\"some_new_action_alongside_add_action\":[\"a\",\"1\"]}\n{\"add\":{\"stats\":\"{\\\"numRecords\\\":4,\\\"minValues\\\":{\\\"key\\\":1,\\\"value\\\":1},\\\"maxValues\\\":{\\\"key\\\":4,\\\"value\\\":4},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\",\"path\":\"part-00000-9f483b95-3ea3-44f0-b54d-73199574be15-c000.snappy.parquet\",\"size\":793,\"modificationTime\":1623255724000,\"dataChange\":true,\"some_new_column_in_add_action\":1,\"partitionValues\":{}},\"some_new_action_alongside_add_action\":[\"a\",\"1\"]}\n{\"add\":{\"stats\":\"{\\\"numRecords\\\":5,\\\"minValues\\\":{\\\"key\\\":5,\\\"value\\\":5},\\\"maxValues\\\":{\\\"key\\\":9,\\\"value\\\":9},\\\"nullCount\\\":{\\\"key\\\":0,\\\"value\\\":0}}\",\"path\":\"part-00001-d1030238-b55d-48f8-a4d6-89ef12e9d501-c000.snappy.parquet\",\"size\":801,\"modificationTime\":1623255724000,\"dataChange\":true,\"some_new_column_in_add_action\":1,\"partitionValues\":{}},\"some_new_action_alongside_add_action\":[\"a\",\"1\"]}\n"
  },
  {
    "path": "spark/src/test/resources/delta/transaction_log_schema_evolvability/_delta_log/_last_checkpoint",
    "content": "{\"version\":2,\"size\":6}\n"
  },
  {
    "path": "spark/src/test/resources/delta/variant-stats-no-checkpoint/_delta_log/00000000000000000000.crc",
    "content": "{\"txnId\":\"261694ff-57ea-40af-919a-eb1b606df973\",\"tableSizeBytes\":0,\"numFiles\":0,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"5053928a-f69d-4fc1-906e-e427c8cfbaef\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"i\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"nv\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpoint.writeStatsAsJson\":\"true\",\"delta.enableVariantShredding\":\"true\"},\"createdTime\":1769217561381},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\",\"variantShredding-preview\"],\"writerFeatures\":[\"variantType\",\"variantShredding-preview\",\"appendOnly\",\"invariants\"]},\"histogramOpt\":{\"sortedBinBoundaries\":[0,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,12582912,16777216,20971520,25165824,29360128,33554432,37748736,41943040,50331648,58720256,67108864,75497472,83886080,92274688,100663296,109051904,117440512,125829120,130023424,134217728,138412032,142606336,146800640,150994944,167772160,184549376,201326592,218103808,234881024,251658240,268435456,285212672,301989888,318767104,335544320,352321536,369098752,385875968,402653184,419430400,436207616,452984832,469762048,486539264,503316480,520093696,536870912,553648128,570425344,587202560,603979776,671088640,738197504,805306368,872415232,939524096,1006632960,1073741824,1140850688,1207959552,1275068416,1342177280,1409286144,1476395008,1610612736,1744830464,1879048192,2013265920,2147483648,2415919104,2684354560,2952790016,3221225472,3489660928,3758096384,4026531840,4294967296,8589934592,17179869184,34359738368,68719476736,137438953472,274877906944],\"fileCounts\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"totalBytes\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},\"allFiles\":[]}\n"
  },
  {
    "path": "spark/src/test/resources/delta/variant-stats-no-checkpoint/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1769217561450,\"operation\":\"CREATE OR REPLACE TABLE\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.checkpoint.writeStatsAsJson\\\":\\\"true\\\",\\\"delta.enableVariantShredding\\\":\\\"true\\\"}\",\"statsOnLoad\":false},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{},\"tags\":{\"restoresDeletedRows\":\"false\"},\"engineInfo\":\"Databricks-Runtime/<unknown>\",\"txnId\":\"261694ff-57ea-40af-919a-eb1b606df973\"}}\n{\"metaData\":{\"id\":\"5053928a-f69d-4fc1-906e-e427c8cfbaef\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"i\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"nv\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpoint.writeStatsAsJson\":\"true\",\"delta.enableVariantShredding\":\"true\"},\"createdTime\":1769217561381}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\",\"variantShredding-preview\"],\"writerFeatures\":[\"variantType\",\"variantShredding-preview\",\"appendOnly\",\"invariants\"]}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/variant-stats-no-checkpoint/_delta_log/00000000000000000001.crc",
    "content": "{\"txnId\":\"d49c9e08-79d2-4a65-ae2d-fbc5869eb8b4\",\"tableSizeBytes\":3390,\"numFiles\":1,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"5053928a-f69d-4fc1-906e-e427c8cfbaef\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"i\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"nv\\\",\\\"type\\\":{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]},\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.checkpoint.writeStatsAsJson\":\"true\",\"delta.enableVariantShredding\":\"true\"},\"createdTime\":1769217561381},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\",\"variantShredding-preview\"],\"writerFeatures\":[\"variantType\",\"variantShredding-preview\",\"appendOnly\",\"invariants\"]},\"histogramOpt\":{\"sortedBinBoundaries\":[0,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,12582912,16777216,20971520,25165824,29360128,33554432,37748736,41943040,50331648,58720256,67108864,75497472,83886080,92274688,100663296,109051904,117440512,125829120,130023424,134217728,138412032,142606336,146800640,150994944,167772160,184549376,201326592,218103808,234881024,251658240,268435456,285212672,301989888,318767104,335544320,352321536,369098752,385875968,402653184,419430400,436207616,452984832,469762048,486539264,503316480,520093696,536870912,553648128,570425344,587202560,603979776,671088640,738197504,805306368,872415232,939524096,1006632960,1073741824,1140850688,1207959552,1275068416,1342177280,1409286144,1476395008,1610612736,1744830464,1879048192,2013265920,2147483648,2415919104,2684354560,2952790016,3221225472,3489660928,3758096384,4026531840,4294967296,8589934592,17179869184,34359738368,68719476736,137438953472,274877906944],\"fileCounts\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"totalBytes\":[3390,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},\"allFiles\":[{\"path\":\"part-00000-20135f43-a68e-4348-9a46-e6eeed704c0e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3390,\"modificationTime\":1769217565612,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"i\\\":100,\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00icd1U*?]0000000000\\\",\\\"nv\\\":{\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00rif2@adK3ig5a00000\\\"}},\\\"maxValues\\\":{\\\"i\\\":109,\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00icd1VWza0000000000\\\",\\\"nv\\\":{\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00rif2@jgK6951j00000\\\"}},\\\"nullCount\\\":{\\\"i\\\":0,\\\"v\\\":0,\\\"nv\\\":{\\\"v\\\":0}}}\",\"tags\":{\"MAX_INSERTION_TIME\":\"1769217565612000\",\"INSERTION_TIME\":\"1769217565612000\",\"SHREDDING_STATE\":\"1\",\"MIN_INSERTION_TIME\":\"1769217565612000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}]}\n"
  },
  {
    "path": "spark/src/test/resources/delta/variant-stats-no-checkpoint/_delta_log/00000000000000000001.json",
    "content": "{\"commitInfo\":{\"timestamp\":1769217565748,\"operation\":\"WRITE\",\"operationParameters\":{\"mode\":\"Append\",\"statsOnLoad\":false,\"partitionBy\":\"[]\"},\"readVersion\":0,\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"10\",\"numOutputBytes\":\"3390\"},\"tags\":{\"noRowsCopied\":\"true\",\"restoresDeletedRows\":\"false\"},\"engineInfo\":\"Databricks-Runtime/<unknown>\",\"txnId\":\"d49c9e08-79d2-4a65-ae2d-fbc5869eb8b4\"}}\n{\"add\":{\"path\":\"part-00000-20135f43-a68e-4348-9a46-e6eeed704c0e-c000.snappy.parquet\",\"partitionValues\":{},\"size\":3390,\"modificationTime\":1769217565612,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":10,\\\"minValues\\\":{\\\"i\\\":100,\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00icd1U*?]0000000000\\\",\\\"nv\\\":{\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00rif2@adK3ig5a00000\\\"}},\\\"maxValues\\\":{\\\"i\\\":109,\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00icd1VWza0000000000\\\",\\\"nv\\\":{\\\"v\\\":\\\"0rJlc5f$EczEEx[cTw04cUW9st(>6d00rif2@jgK6951j00000\\\"}},\\\"nullCount\\\":{\\\"i\\\":0,\\\"v\\\":0,\\\"nv\\\":{\\\"v\\\":0}}}\",\"tags\":{\"MAX_INSERTION_TIME\":\"1769217565612000\",\"INSERTION_TIME\":\"1769217565612000\",\"SHREDDING_STATE\":\"1\",\"MIN_INSERTION_TIME\":\"1769217565612000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/delta/variant-stats-state-reconstruction/_delta_log/00000000000000000000.crc",
    "content": "{\"txnId\":\"56b6e637-4d75-4935-b186-205ed3ca5aff\",\"tableSizeBytes\":863,\"numFiles\":1,\"numMetadata\":1,\"numProtocol\":1,\"setTransactions\":[],\"domainMetadata\":[],\"metadata\":{\"id\":\"9eebd0cb-ecd2-48fb-8b02-352d81209730\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableVariantShredding\":\"true\",\"delta.checkpointInterval\":\"10000\"},\"createdTime\":1769474649842},\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\",\"variantShredding-preview\"],\"writerFeatures\":[\"variantType\",\"variantShredding-preview\",\"appendOnly\",\"invariants\"]},\"histogramOpt\":{\"sortedBinBoundaries\":[0,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,12582912,16777216,20971520,25165824,29360128,33554432,37748736,41943040,50331648,58720256,67108864,75497472,83886080,92274688,100663296,109051904,117440512,125829120,130023424,134217728,138412032,142606336,146800640,150994944,167772160,184549376,201326592,218103808,234881024,251658240,268435456,285212672,301989888,318767104,335544320,352321536,369098752,385875968,402653184,419430400,436207616,452984832,469762048,486539264,503316480,520093696,536870912,553648128,570425344,587202560,603979776,671088640,738197504,805306368,872415232,939524096,1006632960,1073741824,1140850688,1207959552,1275068416,1342177280,1409286144,1476395008,1610612736,1744830464,1879048192,2013265920,2147483648,2415919104,2684354560,2952790016,3221225472,3489660928,3758096384,4026531840,4294967296,8589934592,17179869184,34359738368,68719476736,137438953472,274877906944],\"fileCounts\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"totalBytes\":[863,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},\"allFiles\":[{\"path\":\"part-00031-b7a56bf1-1672-47dc-b8e2-255d62f630ee-c000.snappy.parquet\",\"partitionValues\":{},\"size\":863,\"modificationTime\":1769474653660,\"dataChange\":false,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"v\\\":\\\"0rAf3bMW#D00%Fx0000000000\\\"},\\\"maxValues\\\":{\\\"v\\\":\\\"0rAf3bMW#D00%Fx0000000000\\\"},\\\"nullCount\\\":{\\\"v\\\":0}}\",\"tags\":{\"MAX_INSERTION_TIME\":\"1769474653660000\",\"INSERTION_TIME\":\"1769474653660000\",\"SHREDDING_STATE\":\"0\",\"MIN_INSERTION_TIME\":\"1769474653660000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}]}\n"
  },
  {
    "path": "spark/src/test/resources/delta/variant-stats-state-reconstruction/_delta_log/00000000000000000000.json",
    "content": "{\"commitInfo\":{\"timestamp\":1769474654138,\"operation\":\"CREATE TABLE AS SELECT\",\"operationParameters\":{\"partitionBy\":\"[]\",\"clusterBy\":\"[]\",\"description\":null,\"isManaged\":\"false\",\"properties\":\"{\\\"delta.enableVariantShredding\\\":\\\"true\\\",\\\"delta.checkpointInterval\\\":\\\"10000\\\"}\",\"statsOnLoad\":false},\"isolationLevel\":\"WriteSerializable\",\"isBlindAppend\":true,\"operationMetrics\":{\"numFiles\":\"1\",\"numOutputRows\":\"1\",\"numOutputBytes\":\"863\"},\"tags\":{\"noRowsCopied\":\"true\",\"restoresDeletedRows\":\"false\"},\"engineInfo\":\"Databricks-Runtime/<unknown>\",\"txnId\":\"56b6e637-4d75-4935-b186-205ed3ca5aff\"}}\n{\"metaData\":{\"id\":\"9eebd0cb-ecd2-48fb-8b02-352d81209730\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"v\\\",\\\"type\\\":\\\"variant\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{\"delta.enableVariantShredding\":\"true\",\"delta.checkpointInterval\":\"10000\"},\"createdTime\":1769474649842}}\n{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[\"variantType\",\"variantShredding-preview\"],\"writerFeatures\":[\"variantType\",\"variantShredding-preview\",\"appendOnly\",\"invariants\"]}}\n{\"add\":{\"path\":\"part-00031-b7a56bf1-1672-47dc-b8e2-255d62f630ee-c000.snappy.parquet\",\"partitionValues\":{},\"size\":863,\"modificationTime\":1769474653660,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\":1,\\\"minValues\\\":{\\\"v\\\":\\\"0rAf3bMW#D00%Fx0000000000\\\"},\\\"maxValues\\\":{\\\"v\\\":\\\"0rAf3bMW#D00%Fx0000000000\\\"},\\\"nullCount\\\":{\\\"v\\\":0}}\",\"tags\":{\"MAX_INSERTION_TIME\":\"1769474653660000\",\"INSERTION_TIME\":\"1769474653660000\",\"SHREDDING_STATE\":\"0\",\"MIN_INSERTION_TIME\":\"1769474653660000\",\"OPTIMIZE_TARGET_SIZE\":\"268435456\"}}}\n"
  },
  {
    "path": "spark/src/test/resources/hms/README.md",
    "content": "The file `hive-schema-3.1.0.derby.sql` is copied from the Hive official repository. Hive MetaStore uses this file to create database schema used by the metastore with Apache Derby. We use it for the same purpose as the EmbeddedHMS is back by Apache Derby. The original file can be found [here](https://github.com/apache/hive/blob/master/standalone-metastore/metastore-server/src/main/sql/derby/hive-schema-3.1.0.derby.sql).\n\nIn the embedded HMS, we first create a derby instance, then load this script into derby to initialize the schema. See `org.apache.spark.sql.delta.uniform.ehms.EmbeddedHMS` for more details.\n"
  },
  {
    "path": "spark/src/test/resources/hms/hive-schema-3.1.0.derby.sql",
    "content": "-- Timestamp: 2011-09-22 15:32:02.024\n-- Source database is: /home/carl/Work/repos/hive1/metastore/scripts/upgrade/derby/mdb\n-- Connection URL is: jdbc:derby:/home/carl/Work/repos/hive1/metastore/scripts/upgrade/derby/mdb\n-- Specified schema is: APP\n-- appendLogs: false\n\n-- ----------------------------------------------\n-- DDL Statements for functions\n-- ----------------------------------------------\n\nCREATE FUNCTION \"APP\".\"NUCLEUS_ASCII\" (C CHAR(1)) RETURNS INTEGER LANGUAGE JAVA PARAMETER STYLE JAVA READS SQL DATA CALLED ON NULL INPUT EXTERNAL NAME 'org.datanucleus.store.rdbms.adapter.DerbySQLFunction.ascii' ;\n\nCREATE FUNCTION \"APP\".\"NUCLEUS_MATCHES\" (TEXT VARCHAR(8000),PATTERN VARCHAR(8000)) RETURNS INTEGER LANGUAGE JAVA PARAMETER STYLE JAVA READS SQL DATA CALLED ON NULL INPUT EXTERNAL NAME 'org.datanucleus.store.rdbms.adapter.DerbySQLFunction.matches' ;\n\n-- ----------------------------------------------\n-- DDL Statements for tables\n-- ----------------------------------------------\nCREATE TABLE \"APP\".\"DBS\" (\n  \"DB_ID\" BIGINT NOT NULL,\n  \"DESC\" VARCHAR(4000),\n  \"DB_LOCATION_URI\" VARCHAR(4000) NOT NULL,\n  \"NAME\" VARCHAR(128),\n  \"OWNER_NAME\" VARCHAR(128),\n  \"OWNER_TYPE\" VARCHAR(10),\n  \"CTLG_NAME\" VARCHAR(256) NOT NULL DEFAULT 'hive'\n);\n\nCREATE TABLE \"APP\".\"TBL_PRIVS\" (\"TBL_GRANT_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"TBL_PRIV\" VARCHAR(128), \"TBL_ID\" BIGINT, \"AUTHORIZER\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"DATABASE_PARAMS\" (\"DB_ID\" BIGINT NOT NULL, \"PARAM_KEY\" VARCHAR(180) NOT NULL, \"PARAM_VALUE\" VARCHAR(4000));\n\nCREATE TABLE \"APP\".\"TBL_COL_PRIVS\" (\"TBL_COLUMN_GRANT_ID\" BIGINT NOT NULL, \"COLUMN_NAME\" VARCHAR(767), \"CREATE_TIME\" INTEGER NOT NULL, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"TBL_COL_PRIV\" VARCHAR(128), \"TBL_ID\" BIGINT, \"AUTHORIZER\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"SERDE_PARAMS\" (\"SERDE_ID\" BIGINT NOT NULL, \"PARAM_KEY\" VARCHAR(256) NOT NULL, \"PARAM_VALUE\" CLOB);\n\nCREATE TABLE \"APP\".\"COLUMNS_V2\" (\"CD_ID\" BIGINT NOT NULL, \"COMMENT\" VARCHAR(4000), \"COLUMN_NAME\" VARCHAR(767) NOT NULL, \"TYPE_NAME\" CLOB, \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"SORT_COLS\" (\"SD_ID\" BIGINT NOT NULL, \"COLUMN_NAME\" VARCHAR(767), \"ORDER\" INTEGER NOT NULL, \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"CDS\" (\"CD_ID\" BIGINT NOT NULL);\n\nCREATE TABLE \"APP\".\"PARTITION_KEY_VALS\" (\"PART_ID\" BIGINT NOT NULL, \"PART_KEY_VAL\" VARCHAR(256), \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"DB_PRIVS\" (\"DB_GRANT_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"DB_ID\" BIGINT, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"DB_PRIV\" VARCHAR(128), \"AUTHORIZER\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"IDXS\" (\"INDEX_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"DEFERRED_REBUILD\" CHAR(1) NOT NULL, \"INDEX_HANDLER_CLASS\" VARCHAR(4000), \"INDEX_NAME\" VARCHAR(128), \"INDEX_TBL_ID\" BIGINT, \"LAST_ACCESS_TIME\" INTEGER NOT NULL, \"ORIG_TBL_ID\" BIGINT, \"SD_ID\" BIGINT);\n\nCREATE TABLE \"APP\".\"INDEX_PARAMS\" (\"INDEX_ID\" BIGINT NOT NULL, \"PARAM_KEY\" VARCHAR(256) NOT NULL, \"PARAM_VALUE\" VARCHAR(4000));\n\nCREATE TABLE \"APP\".\"PARTITIONS\" (\"PART_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"LAST_ACCESS_TIME\" INTEGER NOT NULL, \"PART_NAME\" VARCHAR(767), \"SD_ID\" BIGINT, \"TBL_ID\" BIGINT);\n\nCREATE TABLE \"APP\".\"SERDES\" (\"SERDE_ID\" BIGINT NOT NULL, \"NAME\" VARCHAR(128), \"SLIB\" VARCHAR(4000), \"DESCRIPTION\" VARCHAR(4000), \"SERIALIZER_CLASS\" VARCHAR(4000), \"DESERIALIZER_CLASS\" VARCHAR(4000), SERDE_TYPE INTEGER);\n\nCREATE TABLE \"APP\".\"PART_PRIVS\" (\"PART_GRANT_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PART_ID\" BIGINT, \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"PART_PRIV\" VARCHAR(128), \"AUTHORIZER\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"ROLE_MAP\" (\"ROLE_GRANT_ID\" BIGINT NOT NULL, \"ADD_TIME\" INTEGER NOT NULL, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"ROLE_ID\" BIGINT);\n\nCREATE TABLE \"APP\".\"TYPES\" (\"TYPES_ID\" BIGINT NOT NULL, \"TYPE_NAME\" VARCHAR(128), \"TYPE1\" VARCHAR(767), \"TYPE2\" VARCHAR(767));\n\nCREATE TABLE \"APP\".\"GLOBAL_PRIVS\" (\"USER_GRANT_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"USER_PRIV\" VARCHAR(128), \"AUTHORIZER\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"PARTITION_PARAMS\" (\"PART_ID\" BIGINT NOT NULL, \"PARAM_KEY\" VARCHAR(256) NOT NULL, \"PARAM_VALUE\" VARCHAR(4000));\n\nCREATE TABLE \"APP\".\"PARTITION_EVENTS\" (\n    \"PART_NAME_ID\" BIGINT NOT NULL,\n    \"CAT_NAME\" VARCHAR(256),\n    \"DB_NAME\" VARCHAR(128),\n    \"EVENT_TIME\" BIGINT NOT NULL,\n    \"EVENT_TYPE\" INTEGER NOT NULL,\n    \"PARTITION_NAME\" VARCHAR(767),\n    \"TBL_NAME\" VARCHAR(256)\n);\n\nCREATE TABLE \"APP\".\"COLUMNS\" (\"SD_ID\" BIGINT NOT NULL, \"COMMENT\" VARCHAR(256), \"COLUMN_NAME\" VARCHAR(128) NOT NULL, \"TYPE_NAME\" VARCHAR(4000) NOT NULL, \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"ROLES\" (\"ROLE_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"OWNER_NAME\" VARCHAR(128), \"ROLE_NAME\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"TBLS\" (\"TBL_ID\" BIGINT NOT NULL, \"CREATE_TIME\" INTEGER NOT NULL, \"DB_ID\" BIGINT, \"LAST_ACCESS_TIME\" INTEGER NOT NULL, \"OWNER\" VARCHAR(767), \"OWNER_TYPE\" VARCHAR(10), \"RETENTION\" INTEGER NOT NULL, \"SD_ID\" BIGINT, \"TBL_NAME\" VARCHAR(256), \"TBL_TYPE\" VARCHAR(128), \"VIEW_EXPANDED_TEXT\" LONG VARCHAR, \"VIEW_ORIGINAL_TEXT\" LONG VARCHAR, \"IS_REWRITE_ENABLED\" CHAR(1) NOT NULL DEFAULT 'N');\n\nCREATE TABLE \"APP\".\"PARTITION_KEYS\" (\"TBL_ID\" BIGINT NOT NULL, \"PKEY_COMMENT\" VARCHAR(4000), \"PKEY_NAME\" VARCHAR(128) NOT NULL, \"PKEY_TYPE\" VARCHAR(767) NOT NULL, \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"PART_COL_PRIVS\" (\"PART_COLUMN_GRANT_ID\" BIGINT NOT NULL, \"COLUMN_NAME\" VARCHAR(767), \"CREATE_TIME\" INTEGER NOT NULL, \"GRANT_OPTION\" SMALLINT NOT NULL, \"GRANTOR\" VARCHAR(128), \"GRANTOR_TYPE\" VARCHAR(128), \"PART_ID\" BIGINT, \"PRINCIPAL_NAME\" VARCHAR(128), \"PRINCIPAL_TYPE\" VARCHAR(128), \"PART_COL_PRIV\" VARCHAR(128), \"AUTHORIZER\" VARCHAR(128));\n\nCREATE TABLE \"APP\".\"SDS\" (\"SD_ID\" BIGINT NOT NULL, \"INPUT_FORMAT\" VARCHAR(4000), \"IS_COMPRESSED\" CHAR(1) NOT NULL, \"LOCATION\" VARCHAR(4000), \"NUM_BUCKETS\" INTEGER NOT NULL, \"OUTPUT_FORMAT\" VARCHAR(4000), \"SERDE_ID\" BIGINT, \"CD_ID\" BIGINT, \"IS_STOREDASSUBDIRECTORIES\" CHAR(1) NOT NULL);\n\nCREATE TABLE \"APP\".\"SEQUENCE_TABLE\" (\"SEQUENCE_NAME\" VARCHAR(256) NOT NULL, \"NEXT_VAL\" BIGINT NOT NULL);\n\nCREATE TABLE \"APP\".\"TAB_COL_STATS\"(\n    \"CAT_NAME\" VARCHAR(256) NOT NULL,\n    \"DB_NAME\" VARCHAR(128) NOT NULL,\n    \"TABLE_NAME\" VARCHAR(256) NOT NULL,\n    \"COLUMN_NAME\" VARCHAR(767) NOT NULL,\n    \"COLUMN_TYPE\" VARCHAR(128) NOT NULL,\n    \"LONG_LOW_VALUE\" BIGINT,\n    \"LONG_HIGH_VALUE\" BIGINT,\n    \"DOUBLE_LOW_VALUE\" DOUBLE,\n    \"DOUBLE_HIGH_VALUE\" DOUBLE,\n    \"BIG_DECIMAL_LOW_VALUE\" VARCHAR(4000),\n    \"BIG_DECIMAL_HIGH_VALUE\" VARCHAR(4000),\n    \"NUM_DISTINCTS\" BIGINT,\n    \"NUM_NULLS\" BIGINT NOT NULL,\n    \"AVG_COL_LEN\" DOUBLE,\n    \"MAX_COL_LEN\" BIGINT,\n    \"NUM_TRUES\" BIGINT,\n    \"NUM_FALSES\" BIGINT,\n    \"LAST_ANALYZED\" BIGINT,\n    \"CS_ID\" BIGINT NOT NULL,\n    \"TBL_ID\" BIGINT NOT NULL,\n    \"BIT_VECTOR\" BLOB\n);\n\nCREATE TABLE \"APP\".\"TABLE_PARAMS\" (\"TBL_ID\" BIGINT NOT NULL, \"PARAM_KEY\" VARCHAR(256) NOT NULL, \"PARAM_VALUE\" CLOB);\n\nCREATE TABLE \"APP\".\"BUCKETING_COLS\" (\"SD_ID\" BIGINT NOT NULL, \"BUCKET_COL_NAME\" VARCHAR(256), \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"TYPE_FIELDS\" (\"TYPE_NAME\" BIGINT NOT NULL, \"COMMENT\" VARCHAR(256), \"FIELD_NAME\" VARCHAR(128) NOT NULL, \"FIELD_TYPE\" VARCHAR(767) NOT NULL, \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"NUCLEUS_TABLES\" (\"CLASS_NAME\" VARCHAR(128) NOT NULL, \"TABLE_NAME\" VARCHAR(128) NOT NULL, \"TYPE\" VARCHAR(4) NOT NULL, \"OWNER\" VARCHAR(2) NOT NULL, \"VERSION\" VARCHAR(20) NOT NULL, \"INTERFACE_NAME\" VARCHAR(256) DEFAULT NULL);\n\nCREATE TABLE \"APP\".\"SD_PARAMS\" (\"SD_ID\" BIGINT NOT NULL, \"PARAM_KEY\" VARCHAR(256) NOT NULL, \"PARAM_VALUE\" CLOB);\n\nCREATE TABLE \"APP\".\"SKEWED_STRING_LIST\" (\"STRING_LIST_ID\" BIGINT NOT NULL);\n\nCREATE TABLE \"APP\".\"SKEWED_STRING_LIST_VALUES\" (\"STRING_LIST_ID\" BIGINT NOT NULL, \"STRING_LIST_VALUE\" VARCHAR(256), \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"SKEWED_COL_NAMES\" (\"SD_ID\" BIGINT NOT NULL, \"SKEWED_COL_NAME\" VARCHAR(256), \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"SKEWED_COL_VALUE_LOC_MAP\" (\"SD_ID\" BIGINT NOT NULL, \"STRING_LIST_ID_KID\" BIGINT NOT NULL, \"LOCATION\" VARCHAR(4000));\n\nCREATE TABLE \"APP\".\"SKEWED_VALUES\" (\"SD_ID_OID\" BIGINT NOT NULL, \"STRING_LIST_ID_EID\" BIGINT NOT NULL, \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"MASTER_KEYS\" (\"KEY_ID\" INTEGER NOT NULL generated always as identity (start with 1), \"MASTER_KEY\" VARCHAR(767));\n\nCREATE TABLE \"APP\".\"DELEGATION_TOKENS\" ( \"TOKEN_IDENT\" VARCHAR(767) NOT NULL, \"TOKEN\" VARCHAR(767));\n\nCREATE TABLE \"APP\".\"PART_COL_STATS\"(\n    \"CAT_NAME\" VARCHAR(256) NOT NULL,\n    \"DB_NAME\" VARCHAR(128) NOT NULL,\n    \"TABLE_NAME\" VARCHAR(256) NOT NULL,\n    \"PARTITION_NAME\" VARCHAR(767) NOT NULL,\n    \"COLUMN_NAME\" VARCHAR(767) NOT NULL,\n    \"COLUMN_TYPE\" VARCHAR(128) NOT NULL,\n    \"LONG_LOW_VALUE\" BIGINT,\n    \"LONG_HIGH_VALUE\" BIGINT,\n    \"DOUBLE_LOW_VALUE\" DOUBLE,\n    \"DOUBLE_HIGH_VALUE\" DOUBLE,\n    \"BIG_DECIMAL_LOW_VALUE\" VARCHAR(4000),\n    \"BIG_DECIMAL_HIGH_VALUE\" VARCHAR(4000),\n    \"NUM_DISTINCTS\" BIGINT,\n    \"BIT_VECTOR\" BLOB,\n    \"NUM_NULLS\" BIGINT NOT NULL,\n    \"AVG_COL_LEN\" DOUBLE,\n    \"MAX_COL_LEN\" BIGINT,\n    \"NUM_TRUES\" BIGINT,\n    \"NUM_FALSES\" BIGINT,\n    \"LAST_ANALYZED\" BIGINT,\n    \"CS_ID\" BIGINT NOT NULL,\n    \"PART_ID\" BIGINT NOT NULL\n);\n\nCREATE TABLE \"APP\".\"VERSION\" (\"VER_ID\" BIGINT NOT NULL, \"SCHEMA_VERSION\" VARCHAR(127) NOT NULL, \"VERSION_COMMENT\" VARCHAR(255));\n\nCREATE TABLE \"APP\".\"FUNCS\" (\"FUNC_ID\" BIGINT NOT NULL, \"CLASS_NAME\" VARCHAR(4000), \"CREATE_TIME\" INTEGER NOT NULL, \"DB_ID\" BIGINT, \"FUNC_NAME\" VARCHAR(128), \"FUNC_TYPE\" INTEGER NOT NULL, \"OWNER_NAME\" VARCHAR(128), \"OWNER_TYPE\" VARCHAR(10));\n\nCREATE TABLE \"APP\".\"FUNC_RU\" (\"FUNC_ID\" BIGINT NOT NULL, \"RESOURCE_TYPE\" INTEGER NOT NULL, \"RESOURCE_URI\" VARCHAR(4000), \"INTEGER_IDX\" INTEGER NOT NULL);\n\nCREATE TABLE \"APP\".\"NOTIFICATION_LOG\" (\n    \"NL_ID\" BIGINT NOT NULL,\n    \"CAT_NAME\" VARCHAR(256),\n    \"DB_NAME\" VARCHAR(128),\n    \"EVENT_ID\" BIGINT NOT NULL,\n    \"EVENT_TIME\" INTEGER NOT NULL,\n    \"EVENT_TYPE\" VARCHAR(32) NOT NULL,\n    \"MESSAGE\" CLOB,\n    \"TBL_NAME\" VARCHAR(256),\n    \"MESSAGE_FORMAT\" VARCHAR(16)\n);\n\nCREATE TABLE \"APP\".\"NOTIFICATION_SEQUENCE\" (\"NNI_ID\" BIGINT NOT NULL, \"NEXT_EVENT_ID\" BIGINT NOT NULL);\n\nCREATE TABLE \"APP\".\"KEY_CONSTRAINTS\" (\"CHILD_CD_ID\" BIGINT, \"CHILD_INTEGER_IDX\" INTEGER, \"CHILD_TBL_ID\" BIGINT, \"PARENT_CD_ID\" BIGINT , \"PARENT_INTEGER_IDX\" INTEGER, \"PARENT_TBL_ID\" BIGINT NOT NULL,  \"POSITION\" BIGINT NOT NULL, \"CONSTRAINT_NAME\" VARCHAR(400) NOT NULL, \"CONSTRAINT_TYPE\" SMALLINT NOT NULL, \"UPDATE_RULE\" SMALLINT, \"DELETE_RULE\" SMALLINT, \"ENABLE_VALIDATE_RELY\" SMALLINT NOT NULL, \"DEFAULT_VALUE\" VARCHAR(400));\n\nCREATE TABLE \"APP\".\"METASTORE_DB_PROPERTIES\" (\"PROPERTY_KEY\" VARCHAR(255) NOT NULL, \"PROPERTY_VALUE\" VARCHAR(1000) NOT NULL, \"DESCRIPTION\" VARCHAR(1000));\n\nCREATE TABLE \"APP\".\"WM_RESOURCEPLAN\" (RP_ID BIGINT NOT NULL, NAME VARCHAR(128) NOT NULL, QUERY_PARALLELISM INTEGER, STATUS VARCHAR(20) NOT NULL, DEFAULT_POOL_ID BIGINT);\n\nCREATE TABLE \"APP\".\"WM_POOL\" (POOL_ID BIGINT NOT NULL, RP_ID BIGINT NOT NULL, PATH VARCHAR(1024) NOT NULL, ALLOC_FRACTION DOUBLE, QUERY_PARALLELISM INTEGER, SCHEDULING_POLICY VARCHAR(1024));\n\nCREATE TABLE \"APP\".\"WM_TRIGGER\" (TRIGGER_ID BIGINT NOT NULL, RP_ID BIGINT NOT NULL, NAME VARCHAR(128) NOT NULL, TRIGGER_EXPRESSION VARCHAR(1024), ACTION_EXPRESSION VARCHAR(1024), IS_IN_UNMANAGED INTEGER NOT NULL DEFAULT 0);\n\nCREATE TABLE \"APP\".\"WM_POOL_TO_TRIGGER\"  (POOL_ID BIGINT NOT NULL, TRIGGER_ID BIGINT NOT NULL);\n\nCREATE TABLE \"APP\".\"WM_MAPPING\" (MAPPING_ID BIGINT NOT NULL, RP_ID BIGINT NOT NULL, ENTITY_TYPE VARCHAR(128) NOT NULL, ENTITY_NAME VARCHAR(128) NOT NULL, POOL_ID BIGINT, ORDERING INTEGER);\n\nCREATE TABLE \"APP\".\"MV_CREATION_METADATA\" (\n  \"MV_CREATION_METADATA_ID\" BIGINT NOT NULL,\n  \"CAT_NAME\" VARCHAR(256) NOT NULL,\n  \"DB_NAME\" VARCHAR(128) NOT NULL,\n  \"TBL_NAME\" VARCHAR(256) NOT NULL,\n  \"TXN_LIST\" CLOB,\n  \"MATERIALIZATION_TIME\" BIGINT NOT NULL\n);\n\nCREATE TABLE \"APP\".\"MV_TABLES_USED\" (\n  \"MV_CREATION_METADATA_ID\" BIGINT NOT NULL,\n  \"TBL_ID\" BIGINT NOT NULL\n);\n\nCREATE TABLE \"APP\".\"CTLGS\" (\n    \"CTLG_ID\" BIGINT NOT NULL,\n    \"NAME\" VARCHAR(256) UNIQUE,\n    \"DESC\" VARCHAR(4000),\n    \"LOCATION_URI\" VARCHAR(4000) NOT NULL);\n\n-- Insert a default value.  The location is TBD.  Hive will fix this when it starts\nINSERT INTO \"APP\".\"CTLGS\" VALUES (1, 'hive', 'Default catalog for Hive', 'TBD');\n\n-- ----------------------------------------------\n-- DML Statements\n-- ----------------------------------------------\n\nINSERT INTO \"APP\".\"NOTIFICATION_SEQUENCE\" (\"NNI_ID\", \"NEXT_EVENT_ID\") SELECT * FROM (VALUES (1,1)) tmp_table WHERE NOT EXISTS ( SELECT \"NEXT_EVENT_ID\" FROM \"APP\".\"NOTIFICATION_SEQUENCE\");\n\nINSERT INTO \"APP\".\"SEQUENCE_TABLE\" (\"SEQUENCE_NAME\", \"NEXT_VAL\") SELECT * FROM (VALUES ('org.apache.hadoop.hive.metastore.model.MNotificationLog', 1)) tmp_table WHERE NOT EXISTS ( SELECT \"NEXT_VAL\" FROM \"APP\".\"SEQUENCE_TABLE\" WHERE \"SEQUENCE_NAME\" = 'org.apache.hadoop.hive.metastore.model.MNotificationLog');\n\n-- ----------------------------------------------\n-- DDL Statements for indexes\n-- ----------------------------------------------\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUEINDEX\" ON \"APP\".\"IDXS\" (\"INDEX_NAME\", \"ORIG_TBL_ID\");\n\nCREATE INDEX \"APP\".\"TABLECOLUMNPRIVILEGEINDEX\" ON \"APP\".\"TBL_COL_PRIVS\" (\"AUTHORIZER\", \"TBL_ID\", \"COLUMN_NAME\", \"PRINCIPAL_NAME\", \"PRINCIPAL_TYPE\", \"TBL_COL_PRIV\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"DBPRIVILEGEINDEX\" ON \"APP\".\"DB_PRIVS\" (\"AUTHORIZER\", \"DB_ID\", \"PRINCIPAL_NAME\", \"PRINCIPAL_TYPE\", \"DB_PRIV\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE INDEX \"APP\".\"PCS_STATS_IDX\" ON \"APP\".\"PART_COL_STATS\" (\"CAT_NAME\", \"DB_NAME\",\"TABLE_NAME\",\"COLUMN_NAME\",\"PARTITION_NAME\");\n\nCREATE INDEX \"APP\".\"TAB_COL_STATS_IDX\" ON \"APP\".\"TAB_COL_STATS\" (\"CAT_NAME\", \"DB_NAME\", \"TABLE_NAME\", \"COLUMN_NAME\");\n\nCREATE INDEX \"APP\".\"PARTPRIVILEGEINDEX\" ON \"APP\".\"PART_PRIVS\" (\"AUTHORIZER\", \"PART_ID\", \"PRINCIPAL_NAME\", \"PRINCIPAL_TYPE\", \"PART_PRIV\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"ROLEENTITYINDEX\" ON \"APP\".\"ROLES\" (\"ROLE_NAME\");\n\nCREATE INDEX \"APP\".\"TABLEPRIVILEGEINDEX\" ON \"APP\".\"TBL_PRIVS\" (\"AUTHORIZER\", \"TBL_ID\", \"PRINCIPAL_NAME\", \"PRINCIPAL_TYPE\", \"TBL_PRIV\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUETABLE\" ON \"APP\".\"TBLS\" (\"TBL_NAME\", \"DB_ID\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_DATABASE\" ON \"APP\".\"DBS\" (\"NAME\", \"CTLG_NAME\");\n\nCREATE UNIQUE INDEX \"APP\".\"USERROLEMAPINDEX\" ON \"APP\".\"ROLE_MAP\" (\"PRINCIPAL_NAME\", \"ROLE_ID\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"GLOBALPRIVILEGEINDEX\" ON \"APP\".\"GLOBAL_PRIVS\" (\"AUTHORIZER\", \"PRINCIPAL_NAME\", \"PRINCIPAL_TYPE\", \"USER_PRIV\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_TYPE\" ON \"APP\".\"TYPES\" (\"TYPE_NAME\");\n\nCREATE INDEX \"APP\".\"PARTITIONCOLUMNPRIVILEGEINDEX\" ON \"APP\".\"PART_COL_PRIVS\" (\"AUTHORIZER\", \"PART_ID\", \"COLUMN_NAME\", \"PRINCIPAL_NAME\", \"PRINCIPAL_TYPE\", \"PART_COL_PRIV\", \"GRANTOR\", \"GRANTOR_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUEPARTITION\" ON \"APP\".\"PARTITIONS\" (\"PART_NAME\", \"TBL_ID\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUEFUNCTION\" ON \"APP\".\"FUNCS\" (\"FUNC_NAME\", \"DB_ID\");\n\nCREATE INDEX \"APP\".\"FUNCS_N49\" ON \"APP\".\"FUNCS\" (\"DB_ID\");\n\nCREATE INDEX \"APP\".\"FUNC_RU_N49\" ON \"APP\".\"FUNC_RU\" (\"FUNC_ID\");\n\nCREATE INDEX \"APP\".\"CONSTRAINTS_PARENT_TBL_ID_INDEX\" ON \"APP\".\"KEY_CONSTRAINTS\"(\"PARENT_TBL_ID\");\n\nCREATE INDEX \"APP\".\"CONSTRAINTS_CONSTRAINT_TYPE_INDEX\" ON \"APP\".\"KEY_CONSTRAINTS\"(\"CONSTRAINT_TYPE\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_WM_RESOURCEPLAN\" ON \"APP\".\"WM_RESOURCEPLAN\" (\"NAME\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_WM_POOL\" ON \"APP\".\"WM_POOL\" (\"RP_ID\", \"PATH\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_WM_TRIGGER\" ON \"APP\".\"WM_TRIGGER\" (\"RP_ID\", \"NAME\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_WM_MAPPING\" ON \"APP\".\"WM_MAPPING\" (\"RP_ID\", \"ENTITY_TYPE\", \"ENTITY_NAME\");\n\nCREATE UNIQUE INDEX \"APP\".\"MV_UNIQUE_TABLE\" ON \"APP\".\"MV_CREATION_METADATA\" (\"TBL_NAME\", \"DB_NAME\");\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_CATALOG\" ON \"APP\".\"CTLGS\" (\"NAME\");\n\n\n-- ----------------------------------------------\n-- DDL Statements for keys\n-- ----------------------------------------------\n\n-- primary/unique\nALTER TABLE \"APP\".\"IDXS\" ADD CONSTRAINT \"IDXS_PK\" PRIMARY KEY (\"INDEX_ID\");\n\nALTER TABLE \"APP\".\"TBL_COL_PRIVS\" ADD CONSTRAINT \"TBL_COL_PRIVS_PK\" PRIMARY KEY (\"TBL_COLUMN_GRANT_ID\");\n\nALTER TABLE \"APP\".\"CDS\" ADD CONSTRAINT \"SQL110922153006460\" PRIMARY KEY (\"CD_ID\");\n\nALTER TABLE \"APP\".\"DB_PRIVS\" ADD CONSTRAINT \"DB_PRIVS_PK\" PRIMARY KEY (\"DB_GRANT_ID\");\n\nALTER TABLE \"APP\".\"INDEX_PARAMS\" ADD CONSTRAINT \"INDEX_PARAMS_PK\" PRIMARY KEY (\"INDEX_ID\", \"PARAM_KEY\");\n\nALTER TABLE \"APP\".\"PARTITION_KEYS\" ADD CONSTRAINT \"PARTITION_KEY_PK\" PRIMARY KEY (\"TBL_ID\", \"PKEY_NAME\");\n\nALTER TABLE \"APP\".\"SEQUENCE_TABLE\" ADD CONSTRAINT \"SEQUENCE_TABLE_PK\" PRIMARY KEY (\"SEQUENCE_NAME\");\n\nALTER TABLE \"APP\".\"PART_PRIVS\" ADD CONSTRAINT \"PART_PRIVS_PK\" PRIMARY KEY (\"PART_GRANT_ID\");\n\nALTER TABLE \"APP\".\"SDS\" ADD CONSTRAINT \"SDS_PK\" PRIMARY KEY (\"SD_ID\");\n\nALTER TABLE \"APP\".\"SERDES\" ADD CONSTRAINT \"SERDES_PK\" PRIMARY KEY (\"SERDE_ID\");\n\nALTER TABLE \"APP\".\"COLUMNS\" ADD CONSTRAINT \"COLUMNS_PK\" PRIMARY KEY (\"SD_ID\", \"COLUMN_NAME\");\n\nALTER TABLE \"APP\".\"PARTITION_EVENTS\" ADD CONSTRAINT \"PARTITION_EVENTS_PK\" PRIMARY KEY (\"PART_NAME_ID\");\n\nALTER TABLE \"APP\".\"TYPE_FIELDS\" ADD CONSTRAINT \"TYPE_FIELDS_PK\" PRIMARY KEY (\"TYPE_NAME\", \"FIELD_NAME\");\n\nALTER TABLE \"APP\".\"ROLES\" ADD CONSTRAINT \"ROLES_PK\" PRIMARY KEY (\"ROLE_ID\");\n\nALTER TABLE \"APP\".\"TBL_PRIVS\" ADD CONSTRAINT \"TBL_PRIVS_PK\" PRIMARY KEY (\"TBL_GRANT_ID\");\n\nALTER TABLE \"APP\".\"SERDE_PARAMS\" ADD CONSTRAINT \"SERDE_PARAMS_PK\" PRIMARY KEY (\"SERDE_ID\", \"PARAM_KEY\");\n\nALTER TABLE \"APP\".\"NUCLEUS_TABLES\" ADD CONSTRAINT \"NUCLEUS_TABLES_PK\" PRIMARY KEY (\"CLASS_NAME\");\n\nALTER TABLE \"APP\".\"TBLS\" ADD CONSTRAINT \"TBLS_PK\" PRIMARY KEY (\"TBL_ID\");\n\nALTER TABLE \"APP\".\"SD_PARAMS\" ADD CONSTRAINT \"SD_PARAMS_PK\" PRIMARY KEY (\"SD_ID\", \"PARAM_KEY\");\n\nALTER TABLE \"APP\".\"DATABASE_PARAMS\" ADD CONSTRAINT \"DATABASE_PARAMS_PK\" PRIMARY KEY (\"DB_ID\", \"PARAM_KEY\");\n\nALTER TABLE \"APP\".\"DBS\" ADD CONSTRAINT \"DBS_PK\" PRIMARY KEY (\"DB_ID\");\n\nALTER TABLE \"APP\".\"ROLE_MAP\" ADD CONSTRAINT \"ROLE_MAP_PK\" PRIMARY KEY (\"ROLE_GRANT_ID\");\n\nALTER TABLE \"APP\".\"GLOBAL_PRIVS\" ADD CONSTRAINT \"GLOBAL_PRIVS_PK\" PRIMARY KEY (\"USER_GRANT_ID\");\n\nALTER TABLE \"APP\".\"BUCKETING_COLS\" ADD CONSTRAINT \"BUCKETING_COLS_PK\" PRIMARY KEY (\"SD_ID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"SORT_COLS\" ADD CONSTRAINT \"SORT_COLS_PK\" PRIMARY KEY (\"SD_ID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"PARTITION_KEY_VALS\" ADD CONSTRAINT \"PARTITION_KEY_VALS_PK\" PRIMARY KEY (\"PART_ID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"TYPES\" ADD CONSTRAINT \"TYPES_PK\" PRIMARY KEY (\"TYPES_ID\");\n\nALTER TABLE \"APP\".\"COLUMNS_V2\" ADD CONSTRAINT \"SQL110922153006740\" PRIMARY KEY (\"CD_ID\", \"COLUMN_NAME\");\n\nALTER TABLE \"APP\".\"PART_COL_PRIVS\" ADD CONSTRAINT \"PART_COL_PRIVS_PK\" PRIMARY KEY (\"PART_COLUMN_GRANT_ID\");\n\nALTER TABLE \"APP\".\"PARTITION_PARAMS\" ADD CONSTRAINT \"PARTITION_PARAMS_PK\" PRIMARY KEY (\"PART_ID\", \"PARAM_KEY\");\n\nALTER TABLE \"APP\".\"PARTITIONS\" ADD CONSTRAINT \"PARTITIONS_PK\" PRIMARY KEY (\"PART_ID\");\n\nALTER TABLE \"APP\".\"TABLE_PARAMS\" ADD CONSTRAINT \"TABLE_PARAMS_PK\" PRIMARY KEY (\"TBL_ID\", \"PARAM_KEY\");\n\nALTER TABLE \"APP\".\"SKEWED_STRING_LIST\" ADD CONSTRAINT \"SKEWED_STRING_LIST_PK\" PRIMARY KEY (\"STRING_LIST_ID\");\n\nALTER TABLE \"APP\".\"SKEWED_STRING_LIST_VALUES\" ADD CONSTRAINT \"SKEWED_STRING_LIST_VALUES_PK\" PRIMARY KEY (\"STRING_LIST_ID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"SKEWED_COL_NAMES\" ADD CONSTRAINT \"SKEWED_COL_NAMES_PK\" PRIMARY KEY (\"SD_ID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"SKEWED_COL_VALUE_LOC_MAP\" ADD CONSTRAINT \"SKEWED_COL_VALUE_LOC_MAP_PK\" PRIMARY KEY (\"SD_ID\", \"STRING_LIST_ID_KID\");\n\nALTER TABLE \"APP\".\"SKEWED_VALUES\" ADD CONSTRAINT \"SKEWED_VALUES_PK\" PRIMARY KEY (\"SD_ID_OID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"TAB_COL_STATS\" ADD CONSTRAINT \"TAB_COL_STATS_PK\" PRIMARY KEY (\"CS_ID\");\n\nALTER TABLE \"APP\".\"PART_COL_STATS\" ADD CONSTRAINT \"PART_COL_STATS_PK\" PRIMARY KEY (\"CS_ID\");\n\nALTER TABLE \"APP\".\"FUNCS\" ADD CONSTRAINT \"FUNCS_PK\" PRIMARY KEY (\"FUNC_ID\");\n\nALTER TABLE \"APP\".\"FUNC_RU\" ADD CONSTRAINT \"FUNC_RU_PK\" PRIMARY KEY (\"FUNC_ID\", \"INTEGER_IDX\");\n\nALTER TABLE \"APP\".\"NOTIFICATION_LOG\" ADD CONSTRAINT \"NOTIFICATION_LOG_PK\" PRIMARY KEY (\"NL_ID\");\n\nALTER TABLE \"APP\".\"NOTIFICATION_SEQUENCE\" ADD CONSTRAINT \"NOTIFICATION_SEQUENCE_PK\" PRIMARY KEY (\"NNI_ID\");\n\nALTER TABLE \"APP\".\"KEY_CONSTRAINTS\" ADD CONSTRAINT \"CONSTRAINTS_PK\" PRIMARY KEY (\"CONSTRAINT_NAME\", \"POSITION\");\n\nALTER TABLE \"APP\".\"METASTORE_DB_PROPERTIES\" ADD CONSTRAINT \"PROPERTY_KEY_PK\" PRIMARY KEY (\"PROPERTY_KEY\");\n\nALTER TABLE \"APP\".\"MV_CREATION_METADATA\" ADD CONSTRAINT \"MV_CREATION_METADATA_PK\" PRIMARY KEY (\"MV_CREATION_METADATA_ID\");\n\nALTER TABLE \"APP\".\"CTLGS\" ADD CONSTRAINT \"CTLG_PK\" PRIMARY KEY (\"CTLG_ID\");\n\n\n-- foreign\nALTER TABLE \"APP\".\"IDXS\" ADD CONSTRAINT \"IDXS_FK1\" FOREIGN KEY (\"ORIG_TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"IDXS\" ADD CONSTRAINT \"IDXS_FK2\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"IDXS\" ADD CONSTRAINT \"IDXS_FK3\" FOREIGN KEY (\"INDEX_TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TBL_COL_PRIVS\" ADD CONSTRAINT \"TBL_COL_PRIVS_FK1\" FOREIGN KEY (\"TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"DB_PRIVS\" ADD CONSTRAINT \"DB_PRIVS_FK1\" FOREIGN KEY (\"DB_ID\") REFERENCES \"APP\".\"DBS\" (\"DB_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"INDEX_PARAMS\" ADD CONSTRAINT \"INDEX_PARAMS_FK1\" FOREIGN KEY (\"INDEX_ID\") REFERENCES \"APP\".\"IDXS\" (\"INDEX_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PARTITION_KEYS\" ADD CONSTRAINT \"PARTITION_KEYS_FK1\" FOREIGN KEY (\"TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PART_PRIVS\" ADD CONSTRAINT \"PART_PRIVS_FK1\" FOREIGN KEY (\"PART_ID\") REFERENCES \"APP\".\"PARTITIONS\" (\"PART_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SDS\" ADD CONSTRAINT \"SDS_FK1\" FOREIGN KEY (\"SERDE_ID\") REFERENCES \"APP\".\"SERDES\" (\"SERDE_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SDS\" ADD CONSTRAINT \"SDS_FK2\" FOREIGN KEY (\"CD_ID\") REFERENCES \"APP\".\"CDS\" (\"CD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"COLUMNS\" ADD CONSTRAINT \"COLUMNS_FK1\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TYPE_FIELDS\" ADD CONSTRAINT \"TYPE_FIELDS_FK1\" FOREIGN KEY (\"TYPE_NAME\") REFERENCES \"APP\".\"TYPES\" (\"TYPES_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TBL_PRIVS\" ADD CONSTRAINT \"TBL_PRIVS_FK1\" FOREIGN KEY (\"TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SERDE_PARAMS\" ADD CONSTRAINT \"SERDE_PARAMS_FK1\" FOREIGN KEY (\"SERDE_ID\") REFERENCES \"APP\".\"SERDES\" (\"SERDE_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TBLS\" ADD CONSTRAINT \"TBLS_FK2\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TBLS\" ADD CONSTRAINT \"TBLS_FK1\" FOREIGN KEY (\"DB_ID\") REFERENCES \"APP\".\"DBS\" (\"DB_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"DBS\" ADD CONSTRAINT \"DBS_FK1\" FOREIGN KEY (\"CTLG_NAME\") REFERENCES \"APP\".\"CTLGS\" (\"NAME\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SD_PARAMS\" ADD CONSTRAINT \"SD_PARAMS_FK1\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"DATABASE_PARAMS\" ADD CONSTRAINT \"DATABASE_PARAMS_FK1\" FOREIGN KEY (\"DB_ID\") REFERENCES \"APP\".\"DBS\" (\"DB_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"ROLE_MAP\" ADD CONSTRAINT \"ROLE_MAP_FK1\" FOREIGN KEY (\"ROLE_ID\") REFERENCES \"APP\".\"ROLES\" (\"ROLE_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"BUCKETING_COLS\" ADD CONSTRAINT \"BUCKETING_COLS_FK1\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SORT_COLS\" ADD CONSTRAINT \"SORT_COLS_FK1\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PARTITION_KEY_VALS\" ADD CONSTRAINT \"PARTITION_KEY_VALS_FK1\" FOREIGN KEY (\"PART_ID\") REFERENCES \"APP\".\"PARTITIONS\" (\"PART_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"COLUMNS_V2\" ADD CONSTRAINT \"COLUMNS_V2_FK1\" FOREIGN KEY (\"CD_ID\") REFERENCES \"APP\".\"CDS\" (\"CD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PART_COL_PRIVS\" ADD CONSTRAINT \"PART_COL_PRIVS_FK1\" FOREIGN KEY (\"PART_ID\") REFERENCES \"APP\".\"PARTITIONS\" (\"PART_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PARTITION_PARAMS\" ADD CONSTRAINT \"PARTITION_PARAMS_FK1\" FOREIGN KEY (\"PART_ID\") REFERENCES \"APP\".\"PARTITIONS\" (\"PART_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PARTITIONS\" ADD CONSTRAINT \"PARTITIONS_FK1\" FOREIGN KEY (\"TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PARTITIONS\" ADD CONSTRAINT \"PARTITIONS_FK2\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TABLE_PARAMS\" ADD CONSTRAINT \"TABLE_PARAMS_FK1\" FOREIGN KEY (\"TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SKEWED_STRING_LIST_VALUES\" ADD CONSTRAINT \"SKEWED_STRING_LIST_VALUES_FK1\" FOREIGN KEY (\"STRING_LIST_ID\") REFERENCES \"APP\".\"SKEWED_STRING_LIST\" (\"STRING_LIST_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SKEWED_COL_NAMES\" ADD CONSTRAINT \"SKEWED_COL_NAMES_FK1\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SKEWED_COL_VALUE_LOC_MAP\" ADD CONSTRAINT \"SKEWED_COL_VALUE_LOC_MAP_FK1\" FOREIGN KEY (\"SD_ID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SKEWED_COL_VALUE_LOC_MAP\" ADD CONSTRAINT \"SKEWED_COL_VALUE_LOC_MAP_FK2\" FOREIGN KEY (\"STRING_LIST_ID_KID\") REFERENCES \"APP\".\"SKEWED_STRING_LIST\" (\"STRING_LIST_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SKEWED_VALUES\" ADD CONSTRAINT \"SKEWED_VALUES_FK1\" FOREIGN KEY (\"SD_ID_OID\") REFERENCES \"APP\".\"SDS\" (\"SD_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"SKEWED_VALUES\" ADD CONSTRAINT \"SKEWED_VALUES_FK2\" FOREIGN KEY (\"STRING_LIST_ID_EID\") REFERENCES \"APP\".\"SKEWED_STRING_LIST\" (\"STRING_LIST_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"TAB_COL_STATS\" ADD CONSTRAINT \"TAB_COL_STATS_FK\" FOREIGN KEY (\"TBL_ID\") REFERENCES TBLS(\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"PART_COL_STATS\" ADD CONSTRAINT \"PART_COL_STATS_FK\" FOREIGN KEY (\"PART_ID\") REFERENCES PARTITIONS(\"PART_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"VERSION\" ADD CONSTRAINT \"VERSION_PK\" PRIMARY KEY (\"VER_ID\");\n\nALTER TABLE \"APP\".\"FUNCS\" ADD CONSTRAINT \"FUNCS_FK1\" FOREIGN KEY (\"DB_ID\") REFERENCES \"APP\".\"DBS\" (\"DB_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"FUNC_RU\" ADD CONSTRAINT \"FUNC_RU_FK1\" FOREIGN KEY (\"FUNC_ID\") REFERENCES \"APP\".\"FUNCS\" (\"FUNC_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_RESOURCEPLAN\" ADD CONSTRAINT \"WM_RESOURCEPLAN_PK\" PRIMARY KEY (\"RP_ID\");\n\nALTER TABLE \"APP\".\"WM_POOL\" ADD CONSTRAINT \"WM_POOL_PK\" PRIMARY KEY (\"POOL_ID\");\n\nALTER TABLE \"APP\".\"WM_POOL\" ADD CONSTRAINT \"WM_POOL_FK1\" FOREIGN KEY (\"RP_ID\") REFERENCES \"APP\".\"WM_RESOURCEPLAN\" (\"RP_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_RESOURCEPLAN\" ADD CONSTRAINT \"WM_RESOURCEPLAN_FK1\" FOREIGN KEY (\"DEFAULT_POOL_ID\") REFERENCES \"APP\".\"WM_POOL\" (\"POOL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_TRIGGER\" ADD CONSTRAINT \"WM_TRIGGER_PK\" PRIMARY KEY (\"TRIGGER_ID\");\n\nALTER TABLE \"APP\".\"WM_TRIGGER\" ADD CONSTRAINT \"WM_TRIGGER_FK1\" FOREIGN KEY (\"RP_ID\") REFERENCES \"APP\".\"WM_RESOURCEPLAN\" (\"RP_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_POOL_TO_TRIGGER\" ADD CONSTRAINT \"WM_POOL_TO_TRIGGER_FK1\" FOREIGN KEY (\"POOL_ID\") REFERENCES \"APP\".\"WM_POOL\" (\"POOL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_POOL_TO_TRIGGER\" ADD CONSTRAINT \"WM_POOL_TO_TRIGGER_FK2\" FOREIGN KEY (\"TRIGGER_ID\") REFERENCES \"APP\".\"WM_TRIGGER\" (\"TRIGGER_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_MAPPING\" ADD CONSTRAINT \"WM_MAPPING_PK\" PRIMARY KEY (\"MAPPING_ID\");\n\nALTER TABLE \"APP\".\"WM_MAPPING\" ADD CONSTRAINT \"WM_MAPPING_FK1\" FOREIGN KEY (\"RP_ID\") REFERENCES \"APP\".\"WM_RESOURCEPLAN\" (\"RP_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"WM_MAPPING\" ADD CONSTRAINT \"WM_MAPPING_FK2\" FOREIGN KEY (\"POOL_ID\") REFERENCES \"APP\".\"WM_POOL\" (\"POOL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"MV_TABLES_USED\" ADD CONSTRAINT \"MV_TABLES_USED_FK1\" FOREIGN KEY (\"MV_CREATION_METADATA_ID\") REFERENCES \"APP\".\"MV_CREATION_METADATA\" (\"MV_CREATION_METADATA_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"MV_TABLES_USED\" ADD CONSTRAINT \"MV_TABLES_USED_FK2\" FOREIGN KEY (\"TBL_ID\") REFERENCES \"APP\".\"TBLS\" (\"TBL_ID\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\nALTER TABLE \"APP\".\"DBS\" ADD CONSTRAINT \"DBS_CTLG_FK\" FOREIGN KEY (\"CTLG_NAME\") REFERENCES \"APP\".\"CTLGS\" (\"NAME\") ON DELETE NO ACTION ON UPDATE NO ACTION;\n\n-- ----------------------------------------------\n-- DDL Statements for checks\n-- ----------------------------------------------\n\nALTER TABLE \"APP\".\"IDXS\" ADD CONSTRAINT \"SQL110318025504980\" CHECK (DEFERRED_REBUILD IN ('Y','N'));\n\nALTER TABLE \"APP\".\"SDS\" ADD CONSTRAINT \"SQL110318025505550\" CHECK (IS_COMPRESSED IN ('Y','N'));\n\n-- ----------------------------\n-- Transaction and Lock Tables\n-- ----------------------------\nCREATE TABLE TXNS (\n  TXN_ID bigint PRIMARY KEY,\n  TXN_STATE char(1) NOT NULL,\n  TXN_STARTED bigint NOT NULL,\n  TXN_LAST_HEARTBEAT bigint NOT NULL,\n  TXN_USER varchar(128) NOT NULL,\n  TXN_HOST varchar(128) NOT NULL,\n  TXN_AGENT_INFO varchar(128),\n  TXN_META_INFO varchar(128),\n  TXN_HEARTBEAT_COUNT integer,\n  TXN_TYPE integer\n);\n\nCREATE TABLE TXN_COMPONENTS (\n  TC_TXNID bigint NOT NULL REFERENCES TXNS (TXN_ID),\n  TC_DATABASE varchar(128) NOT NULL,\n  TC_TABLE varchar(128),\n  TC_PARTITION varchar(767),\n  TC_OPERATION_TYPE char(1) NOT NULL,\n  TC_WRITEID bigint\n);\n\nCREATE INDEX TC_TXNID_INDEX ON TXN_COMPONENTS (TC_TXNID);\n\nCREATE TABLE COMPLETED_TXN_COMPONENTS (\n  CTC_TXNID bigint NOT NULL,\n  CTC_DATABASE varchar(128) NOT NULL,\n  CTC_TABLE varchar(256),\n  CTC_PARTITION varchar(767),\n  CTC_TIMESTAMP timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,\n  CTC_WRITEID bigint,\n  CTC_UPDATE_DELETE char(1) NOT NULL\n);\n\nCREATE INDEX COMPLETED_TXN_COMPONENTS_IDX ON COMPLETED_TXN_COMPONENTS (CTC_DATABASE, CTC_TABLE, CTC_PARTITION);\n\nCREATE TABLE NEXT_TXN_ID (\n  NTXN_NEXT bigint NOT NULL\n);\nINSERT INTO NEXT_TXN_ID VALUES(1);\n\nCREATE TABLE HIVE_LOCKS (\n  HL_LOCK_EXT_ID bigint NOT NULL,\n  HL_LOCK_INT_ID bigint NOT NULL,\n  HL_TXNID bigint NOT NULL,\n  HL_DB varchar(128) NOT NULL,\n  HL_TABLE varchar(128),\n  HL_PARTITION varchar(767),\n  HL_LOCK_STATE char(1) NOT NULL,\n  HL_LOCK_TYPE char(1) NOT NULL,\n  HL_LAST_HEARTBEAT bigint NOT NULL,\n  HL_ACQUIRED_AT bigint,\n  HL_USER varchar(128) NOT NULL,\n  HL_HOST varchar(128) NOT NULL,\n  HL_HEARTBEAT_COUNT integer,\n  HL_AGENT_INFO varchar(128),\n  HL_BLOCKEDBY_EXT_ID bigint,\n  HL_BLOCKEDBY_INT_ID bigint,\n  PRIMARY KEY(HL_LOCK_EXT_ID, HL_LOCK_INT_ID)\n);\n\nCREATE INDEX HL_TXNID_INDEX ON HIVE_LOCKS (HL_TXNID);\n\nCREATE TABLE NEXT_LOCK_ID (\n  NL_NEXT bigint NOT NULL\n);\nINSERT INTO NEXT_LOCK_ID VALUES(1);\n\nCREATE TABLE COMPACTION_QUEUE (\n  CQ_ID bigint PRIMARY KEY,\n  CQ_DATABASE varchar(128) NOT NULL,\n  CQ_TABLE varchar(128) NOT NULL,\n  CQ_PARTITION varchar(767),\n  CQ_STATE char(1) NOT NULL,\n  CQ_TYPE char(1) NOT NULL,\n  CQ_TBLPROPERTIES varchar(2048),\n  CQ_WORKER_ID varchar(128),\n  CQ_START bigint,\n  CQ_RUN_AS varchar(128),\n  CQ_HIGHEST_WRITE_ID bigint,\n  CQ_META_INFO varchar(2048) for bit data,\n  CQ_HADOOP_JOB_ID varchar(32)\n);\n\nCREATE TABLE NEXT_COMPACTION_QUEUE_ID (\n  NCQ_NEXT bigint NOT NULL\n);\nINSERT INTO NEXT_COMPACTION_QUEUE_ID VALUES(1);\n\nCREATE TABLE COMPLETED_COMPACTIONS (\n  CC_ID bigint PRIMARY KEY,\n  CC_DATABASE varchar(128) NOT NULL,\n  CC_TABLE varchar(128) NOT NULL,\n  CC_PARTITION varchar(767),\n  CC_STATE char(1) NOT NULL,\n  CC_TYPE char(1) NOT NULL,\n  CC_TBLPROPERTIES varchar(2048),\n  CC_WORKER_ID varchar(128),\n  CC_START bigint,\n  CC_END bigint,\n  CC_RUN_AS varchar(128),\n  CC_HIGHEST_WRITE_ID bigint,\n  CC_META_INFO varchar(2048) for bit data,\n  CC_HADOOP_JOB_ID varchar(32)\n);\n\nCREATE TABLE AUX_TABLE (\n  MT_KEY1 varchar(128) NOT NULL,\n  MT_KEY2 bigint NOT NULL,\n  MT_COMMENT varchar(255),\n  PRIMARY KEY(MT_KEY1, MT_KEY2)\n);\n\n--1st 4 cols make up a PK but since WS_PARTITION is nullable we can't declare such PK\n--This is a good candidate for Index orgainzed table\nCREATE TABLE WRITE_SET (\n  WS_DATABASE varchar(128) NOT NULL,\n  WS_TABLE varchar(128) NOT NULL,\n  WS_PARTITION varchar(767),\n  WS_TXNID bigint NOT NULL,\n  WS_COMMIT_ID bigint NOT NULL,\n  WS_OPERATION_TYPE char(1) NOT NULL\n);\n\nCREATE TABLE TXN_TO_WRITE_ID (\n  T2W_TXNID bigint NOT NULL,\n  T2W_DATABASE varchar(128) NOT NULL,\n  T2W_TABLE varchar(256) NOT NULL,\n  T2W_WRITEID bigint NOT NULL\n);\n\nCREATE UNIQUE INDEX TBL_TO_TXN_ID_IDX ON TXN_TO_WRITE_ID (T2W_DATABASE, T2W_TABLE, T2W_TXNID);\nCREATE UNIQUE INDEX TBL_TO_WRITE_ID_IDX ON TXN_TO_WRITE_ID (T2W_DATABASE, T2W_TABLE, T2W_WRITEID);\n\nCREATE TABLE NEXT_WRITE_ID (\n  NWI_DATABASE varchar(128) NOT NULL,\n  NWI_TABLE varchar(256) NOT NULL,\n  NWI_NEXT bigint NOT NULL\n);\n\nCREATE UNIQUE INDEX NEXT_WRITE_ID_IDX ON NEXT_WRITE_ID (NWI_DATABASE, NWI_TABLE);\n\nCREATE TABLE MIN_HISTORY_LEVEL (\n  MHL_TXNID bigint NOT NULL,\n  MHL_MIN_OPEN_TXNID bigint NOT NULL,\n  PRIMARY KEY(MHL_TXNID)\n);\n\nCREATE INDEX MIN_HISTORY_LEVEL_IDX ON MIN_HISTORY_LEVEL (MHL_MIN_OPEN_TXNID);\n\nCREATE TABLE MATERIALIZATION_REBUILD_LOCKS (\n  MRL_TXN_ID BIGINT NOT NULL,\n  MRL_DB_NAME VARCHAR(128) NOT NULL,\n  MRL_TBL_NAME VARCHAR(256) NOT NULL,\n  MRL_LAST_HEARTBEAT BIGINT NOT NULL,\n  PRIMARY KEY(MRL_TXN_ID)\n);\n\nCREATE TABLE \"APP\".\"I_SCHEMA\" (\n  \"SCHEMA_ID\" bigint primary key,\n  \"SCHEMA_TYPE\" integer not null,\n  \"NAME\" varchar(256) unique,\n  \"DB_ID\" bigint references \"APP\".\"DBS\" (\"DB_ID\"),\n  \"COMPATIBILITY\" integer not null,\n  \"VALIDATION_LEVEL\" integer not null,\n  \"CAN_EVOLVE\" char(1) not null,\n  \"SCHEMA_GROUP\" varchar(256),\n  \"DESCRIPTION\" varchar(4000)\n);\n\nCREATE TABLE \"APP\".\"SCHEMA_VERSION\" (\n  \"SCHEMA_VERSION_ID\" bigint primary key,\n  \"SCHEMA_ID\" bigint references \"APP\".\"I_SCHEMA\" (\"SCHEMA_ID\"),\n  \"VERSION\" integer not null,\n  \"CREATED_AT\" bigint not null,\n  \"CD_ID\" bigint references \"APP\".\"CDS\" (\"CD_ID\"),\n  \"STATE\" integer not null,\n  \"DESCRIPTION\" varchar(4000),\n  \"SCHEMA_TEXT\" clob,\n  \"FINGERPRINT\" varchar(256),\n  \"SCHEMA_VERSION_NAME\" varchar(256),\n  \"SERDE_ID\" bigint references \"APP\".\"SERDES\" (\"SERDE_ID\")\n);\n\nCREATE UNIQUE INDEX \"APP\".\"UNIQUE_SCHEMA_VERSION\" ON \"APP\".\"SCHEMA_VERSION\" (\"SCHEMA_ID\", \"VERSION\");\n\nCREATE TABLE REPL_TXN_MAP (\n  RTM_REPL_POLICY varchar(256) NOT NULL,\n  RTM_SRC_TXN_ID bigint NOT NULL,\n  RTM_TARGET_TXN_ID bigint NOT NULL,\n  PRIMARY KEY (RTM_REPL_POLICY, RTM_SRC_TXN_ID)\n);\n\nCREATE TABLE \"APP\".\"RUNTIME_STATS\" (\n  \"RS_ID\" bigint primary key,\n  \"CREATE_TIME\" integer not null,\n  \"WEIGHT\" integer not null,\n  \"PAYLOAD\" BLOB\n);\n\nCREATE INDEX IDX_RUNTIME_STATS_CREATE_TIME ON RUNTIME_STATS(CREATE_TIME);\n\n-- -----------------------------------------------------------------\n-- Record schema version. Should be the last step in the init script\n-- -----------------------------------------------------------------\nINSERT INTO \"APP\".\"VERSION\" (VER_ID, SCHEMA_VERSION, VERSION_COMMENT) VALUES (1, '3.1.0', 'Hive release version 3.1.0');\n"
  },
  {
    "path": "spark/src/test/resources/log4j2.properties",
    "content": "#\n#  Copyright (2021) The Delta Lake Project Authors.\n#  Licensed under the Apache License, Version 2.0 (the \"License\");\n#  you may not use this file except in compliance with the License.\n#  You may obtain a copy of the License at\n#  http://www.apache.org/licenses/LICENSE-2.0\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Set everything to be logged to the file target/unit-tests.log\nrootLogger.level = warn\nrootLogger.appenderRef.file.ref = ${sys:test.appender:-File}\n\nappender.file.type = File\nappender.file.name = File\nappender.file.fileName = target/unit-tests.log\nappender.file.append = true\nappender.file.layout.type = PatternLayout\nappender.file.layout.pattern = %d{yy/MM/dd HH:mm:ss.SSS} %t %p %c{1}: %m%n\n\n# Structured Logging Appender\nappender.structured.type = File\nappender.structured.name = structured\nappender.structured.fileName = target/structured.log\nappender.structured.layout.type = JsonTemplateLayout\nappender.structured.layout.eventTemplateUri = classpath:org/apache/spark/SparkLayout.json\n\n# Custom logger for testing structured logging with Spark 4.0+\nlogger.structured_logging.name = org.apache.spark.sql.delta.logging.DeltaStructuredLoggingSuite\nlogger.structured_logging.level = trace\nlogger.structured_logging.appenderRefs = structured\nlogger.structured_logging.appenderRef.structured.ref = structured\n\n# Tests that launch java subprocesses can set the \"test.appender\" system property to\n# \"console\" to avoid having the child process's logs overwrite the unit test's\n# log file.\nappender.console.type = Console\nappender.console.name = console\nappender.console.target = SYSTEM_ERR\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = %d{yy/MM/dd HH:mm:ss.SSS} %t %p %c{1}: %m%n\n\n# Ignore messages below warning level from Jetty, because it's a bit verbose\nlogger.jetty.name = org.sparkproject.jetty\nlogger.jetty.level = warn\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/exceptions/DeltaConcurrentExceptionsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.exceptions\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaConcurrentExceptionsSuite extends SparkFunSuite with SharedSparkSession {\n\n  test(\"test ConcurrentWriteException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.concurrentWriteException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.concurrentWriteException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.ConcurrentWriteException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.concurrentWriteException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.ConcurrentWriteException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.ConcurrentWriteException(None)\n    }\n  }\n\n  test(\"test MetadataChangedException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.metadataChangedException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.metadataChangedException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.MetadataChangedException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.metadataChangedException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.MetadataChangedException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.MetadataChangedException(None)\n    }\n  }\n\n  test(\"test ProtocolChangedException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.protocolChangedException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.protocolChangedException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.ProtocolChangedException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.protocolChangedException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.ProtocolChangedException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.ProtocolChangedException(None)\n    }\n  }\n\n  test(\"test ConcurrentAppendException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.\n        concurrentAppendException(None, \"t\", -1, partitionOpt = None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.\n        concurrentAppendException(None, \"t\", -1, partitionOpt = None)\n    }\n\n    intercept[org.apache.spark.sql.delta.ConcurrentAppendException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.\n        concurrentAppendException(None, \"t\", -1, partitionOpt = None)\n    }\n  }\n\n  test(\"test ConcurrentDeleteReadException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.\n        concurrentDeleteReadException(None, \"t\", -1, partitionOpt = None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors\n        .concurrentDeleteReadException(None, \"t\", -1, partitionOpt = None)\n    }\n\n    intercept[org.apache.spark.sql.delta.ConcurrentDeleteReadException] {\n      throw org.apache.spark.sql.delta.DeltaErrors\n        .concurrentDeleteReadException(None, \"t\", -1, partitionOpt = None)\n    }\n  }\n\n  test(\"test ConcurrentDeleteDeleteException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors\n        .concurrentDeleteDeleteException(None, \"t\", -1, partitionOpt = None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors\n        .concurrentDeleteDeleteException(None, \"t\", -1, partitionOpt = None)\n    }\n\n    intercept[org.apache.spark.sql.delta.ConcurrentDeleteDeleteException] {\n      throw org.apache.spark.sql.delta.DeltaErrors\n        .concurrentDeleteDeleteException(None, \"t\", -1, partitionOpt = None)\n    }\n  }\n\n  test(\"test ConcurrentTransactionException\") {\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.concurrentTransactionException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.concurrentTransactionException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.ConcurrentTransactionException] {\n      throw org.apache.spark.sql.delta.DeltaErrors.concurrentTransactionException(None)\n    }\n\n    intercept[org.apache.spark.sql.delta.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.ConcurrentTransactionException(None)\n    }\n\n    intercept[io.delta.exceptions.DeltaConcurrentModificationException] {\n      throw new org.apache.spark.sql.delta.ConcurrentTransactionException(None)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/sql/DeltaExtensionAndCatalogSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sql\n\nimport java.nio.file.Files\n\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, DeltaLog}\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport io.delta.tables.DeltaTable\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.network.util.JavaUtils\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\n\nclass DeltaExtensionAndCatalogSuite extends SparkFunSuite {\n\n  private def createTempDir(): String = {\n    val dir = Files.createTempDirectory(\"DeltaSparkSessionExtensionSuite\").toFile\n    FileUtils.forceDeleteOnExit(dir)\n    dir.getCanonicalPath\n  }\n\n  private def verifyDeltaSQLParserIsActivated(spark: SparkSession): Unit = {\n    val input = Files.createTempDirectory(\"DeltaSparkSessionExtensionSuite\").toFile\n    try {\n      spark.range(1, 10).write.format(\"delta\").save(input.getCanonicalPath)\n      spark.sql(s\"vacuum delta.`${input.getCanonicalPath}`\")\n    } finally {\n      JavaUtils.deleteRecursively(input)\n    }\n  }\n\n  test(\"activate Delta SQL parser using SQL conf\") {\n    val spark = SparkSession.builder()\n      .appName(\"DeltaSparkSessionExtensionSuiteUsingSQLConf\")\n      .master(\"local[2]\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n    try {\n      verifyDeltaSQLParserIsActivated(spark)\n    } finally {\n      spark.close()\n    }\n  }\n\n  test(\"activate Delta SQL parser using withExtensions\") {\n    val spark = SparkSession.builder()\n      .appName(\"DeltaSparkSessionExtensionSuiteUsingWithExtensions\")\n      .master(\"local[2]\")\n      .withExtensions(new io.delta.sql.DeltaSparkSessionExtension)\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .getOrCreate()\n    try {\n      verifyDeltaSQLParserIsActivated(spark)\n    } finally {\n      spark.close()\n    }\n  }\n\n  test(\"DeltaCatalog class should be initialized correctly\") {\n    withSparkSession(\n      SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key ->\n        classOf[org.apache.spark.sql.delta.catalog.DeltaCatalog].getName\n    ) { spark =>\n      val v2Catalog = spark.sessionState.analyzer.catalogManager.catalog(\"spark_catalog\")\n      assert(v2Catalog.isInstanceOf[org.apache.spark.sql.delta.catalog.DeltaCatalog])\n    }\n  }\n\n  test(\"DeltaLog should not throw exception if spark.sql.catalog.spark_catalog is set\") {\n    withTempDir { dir =>\n      withSparkSession(\n        SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key ->\n          classOf[org.apache.spark.sql.delta.catalog.DeltaCatalog].getName\n      ) { spark =>\n        val path = new Path(dir.getCanonicalPath)\n        assert(DeltaLog.forTable(spark, path).tableExists == false)\n      }\n    }\n  }\n\n  test(\"DeltaLog should throw exception if spark.sql.catalog.spark_catalog \" +\n    \"config is not found\") {\n    withTempDir { dir =>\n      withSparkSession(\"\" -> \"\") { spark =>\n        val path = new Path(dir.getCanonicalPath)\n        val e = intercept[DeltaAnalysisException] {\n          DeltaLog.forTable(spark, path)\n        }\n        assert(e.isInstanceOf[DeltaAnalysisException])\n        assert(e.getErrorClass() == \"DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG\")\n      }\n    }\n  }\n\n  test(\"DeltaLog should not throw exception if spark.sql.catalog.spark_catalog \" +\n    \"config is not found and the check is disabled\") {\n    withTempDir { dir =>\n      withSparkSession(DeltaSQLConf.DELTA_REQUIRED_SPARK_CONFS_CHECK.key -> \"false\") { spark =>\n        val path = new Path(dir.getCanonicalPath)\n          DeltaLog.forTable(spark, path)\n        assert(DeltaLog.forTable(spark, path).tableExists == false)\n      }\n    }\n  }\n\n  private def withSparkSession(configs: (String, String)*)(f: SparkSession => Unit): Unit = {\n    var builder = SparkSession.builder()\n      .appName(\"DeltaSparkSessionExtensionSuite\")\n      .master(\"local[2]\")\n      .config(\"spark.sql.warehouse.dir\", createTempDir())\n\n    configs.foreach { c => builder = builder.config(c._1, c._2) }\n    val spark = builder.getOrCreate()\n    try {\n      f(spark)\n    } finally {\n      spark.close()\n    }\n  }\n\n  private def checkErrorMessage(f: => Unit): Unit = {\n    val e = intercept[AnalysisException](f)\n    val expectedStrs = Seq(\n      \"Delta operation requires the SparkSession to be configured\",\n      \"spark.sql.extensions\",\n      s\"${classOf[DeltaSparkSessionExtension].getName}\",\n      SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key,\n      s\"${classOf[DeltaCatalog].getName}\"\n    )\n    expectedStrs.foreach { m => assert(e.getMessage().contains(m), \"full exception: \" + e) }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/sql/parser/DeltaSqlParserSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sql.parser\n\nimport io.delta.tables.execution.VacuumTableCommand\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterByTransform\n\nimport org.apache.spark.sql.delta.CloneTableSQLTestUtils\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.{UnresolvedPathBasedDeltaTable, UnresolvedPathBasedTable}\nimport org.apache.spark.sql.delta.commands.{DeltaOptimizeContext, DescribeDeltaDetailCommand, DescribeDeltaHistory, OptimizeTableCommand, DeltaReorgTable}\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.{TableIdentifier, TimeTravel}\nimport org.apache.spark.sql.catalyst.analysis.{UnresolvedAttribute, UnresolvedRelation, UnresolvedTable}\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.plans.SQLHelper\nimport org.apache.spark.sql.catalyst.plans.logical.{AlterTableDropFeature, CloneTableStatement, CreateTable, CreateTableAsSelect, LogicalPlan, ReplaceTable, ReplaceTableAsSelect, RestoreTableStatement}\nimport org.apache.spark.sql.execution.SparkSqlParser\n\nclass DeltaSqlParserSuite extends SparkFunSuite with SQLHelper {\n\n  test(\"isValidDecimal should recognize a table identifier and not treat them as a decimal\") {\n    // Setting `delegate` to `null` is fine. The following tests don't need to touch `delegate`.\n    val parser = new DeltaSqlParser(null)\n    assert(parser.parsePlan(\"vacuum 123_\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"123_\"), \"VACUUM\"), None, None, None, false, None))\n    assert(parser.parsePlan(\"vacuum 1a.123_\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"1a\", \"123_\"), \"VACUUM\"), None, None, None, false,\n        None))\n    assert(parser.parsePlan(\"vacuum a.123A\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"a\", \"123A\"), \"VACUUM\"), None, None, None, false,\n        None))\n    assert(parser.parsePlan(\"vacuum a.123E3_column\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"a\", \"123E3_column\"), \"VACUUM\"),\n        None, None, None, false, None))\n    assert(parser.parsePlan(\"vacuum a.123D_column\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"a\", \"123D_column\"), \"VACUUM\"),\n        None, None, None, false, None))\n    assert(parser.parsePlan(\"vacuum a.123BD_column\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"a\", \"123BD_column\"), \"VACUUM\"),\n        None, None, None, false, None))\n    assert(parser.parsePlan(\"vacuum delta.`/tmp/table`\") ===\n      VacuumTableCommand(UnresolvedTable(Seq(\"delta\", \"/tmp/table\"), \"VACUUM\"),\n        None, None, None, false, None))\n    assert(parser.parsePlan(\"vacuum \\\"/tmp/table\\\"\") ===\n      VacuumTableCommand(\n        UnresolvedPathBasedDeltaTable(\"/tmp/table\", Map.empty, \"VACUUM\"), None, None, None, false,\n        None))\n  }\n\n  test(\"Restore command is parsed as expected\") {\n    val parser = new DeltaSqlParser(null)\n    var parsedCmd = parser.parsePlan(\"RESTORE catalog_foo.db.tbl TO VERSION AS OF 1;\")\n    assert(parsedCmd ===\n      RestoreTableStatement(TimeTravel(\n        UnresolvedRelation(Seq(\"catalog_foo\", \"db\", \"tbl\")),\n        None,\n        Some(1),\n        Some(\"sql\"))))\n\n    parsedCmd = parser.parsePlan(\"RESTORE delta.`/tmp` TO VERSION AS OF 1;\")\n    assert(parsedCmd ===\n      RestoreTableStatement(TimeTravel(\n        UnresolvedRelation(Seq(\"delta\", \"/tmp\")),\n        None,\n        Some(1),\n        Some(\"sql\"))))\n  }\n\n  test(\"OPTIMIZE command is parsed as expected\") {\n    val parser = new DeltaSqlParser(null)\n    var parsedCmd = parser.parsePlan(\"OPTIMIZE tbl\")\n    assert(parsedCmd ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"tbl\"), \"OPTIMIZE\"))\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE db.tbl\")\n    assert(parsedCmd ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\", \"db\")), Nil)(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"db\", \"tbl\"), \"OPTIMIZE\"))\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE catalog_foo.db.tbl\")\n    assert(parsedCmd ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\", \"db\", \"catalog_foo\")), Nil)(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"catalog_foo\", \"db\", \"tbl\"), \"OPTIMIZE\"))\n\n    assert(parser.parsePlan(\"OPTIMIZE tbl_${system:spark.testing}\") ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl_true\")), Nil)(Nil))\n\n    withSQLConf(\"tbl_var\" -> \"tbl\") {\n      assert(parser.parsePlan(\"OPTIMIZE ${tbl_var}\") ===\n        OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(Nil))\n\n      assert(parser.parsePlan(\"OPTIMIZE ${spark:tbl_var}\") ===\n        OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(Nil))\n\n      assert(parser.parsePlan(\"OPTIMIZE ${sparkconf:tbl_var}\") ===\n        OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(Nil))\n\n      assert(parser.parsePlan(\"OPTIMIZE ${hiveconf:tbl_var}\") ===\n        OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(Nil))\n\n      assert(parser.parsePlan(\"OPTIMIZE ${hivevar:tbl_var}\") ===\n        OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(Nil))\n    }\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE '/path/to/tbl'\")\n    assert(parsedCmd ===\n      OptimizeTableCommand(Some(\"/path/to/tbl\"), None, Nil)(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedPathBasedDeltaTable(\"/path/to/tbl\", Map.empty, \"OPTIMIZE\"))\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE delta.`/path/to/tbl`\")\n    assert(parsedCmd ===\n      OptimizeTableCommand(None, Some(tblId(\"/path/to/tbl\", \"delta\")), Nil)(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"delta\", \"/path/to/tbl\"), \"OPTIMIZE\"))\n\n    assert(parser.parsePlan(\"OPTIMIZE tbl WHERE part = 1\") ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Seq(\"part = 1\"))(Nil))\n\n    assert(parser.parsePlan(\"OPTIMIZE tbl ZORDER BY (col1)\") ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)\n      (Seq(unresolvedAttr(\"col1\"))))\n\n    assert(parser.parsePlan(\"OPTIMIZE tbl WHERE part = 1 ZORDER BY col1, col2.subcol\") ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Seq(\"part = 1\"))(\n        Seq(unresolvedAttr(\"col1\"), unresolvedAttr(\"col2\", \"subcol\"))))\n\n    assert(parser.parsePlan(\"OPTIMIZE tbl WHERE part = 1 ZORDER BY (col1, col2.subcol)\") ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Seq(\"part = 1\"))(\n        Seq(unresolvedAttr(\"col1\"), unresolvedAttr(\"col2\", \"subcol\"))))\n\n    // Validate OPTIMIZE works correctly with FULL keyword.\n    parsedCmd = parser.parsePlan(\"OPTIMIZE tbl FULL\")\n    assert(parsedCmd ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil, DeltaOptimizeContext(isFull = true))(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"tbl\"), \"OPTIMIZE\"))\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE catalog_foo.db.tbl FULL\")\n    assert(parsedCmd === OptimizeTableCommand(\n      None, Some(tblId(\"tbl\", \"db\", \"catalog_foo\")), Nil, DeltaOptimizeContext(isFull = true))(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"catalog_foo\", \"db\", \"tbl\"), \"OPTIMIZE\"))\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE '/path/to/tbl' FULL\")\n    assert(parsedCmd === OptimizeTableCommand(\n      Some(\"/path/to/tbl\"), None, Nil, DeltaOptimizeContext(isFull = true))(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedPathBasedDeltaTable(\"/path/to/tbl\", Map.empty, \"OPTIMIZE\"))\n\n    parsedCmd = parser.parsePlan(\"OPTIMIZE delta.`/path/to/tbl` FULL\")\n    assert(parsedCmd === OptimizeTableCommand(\n      None, Some(tblId(\"/path/to/tbl\", \"delta\")), Nil, DeltaOptimizeContext(isFull = true))(Nil))\n    assert(parsedCmd.asInstanceOf[OptimizeTableCommand].child ===\n      UnresolvedTable(Seq(\"delta\", \"/path/to/tbl\"), \"OPTIMIZE\"))\n  }\n\n  test(\"OPTIMIZE command new tokens are non-reserved keywords\") {\n    // new keywords: OPTIMIZE, ZORDER\n    val parser = new DeltaSqlParser(null)\n\n    // Use the new keywords in table name\n    assert(parser.parsePlan(\"OPTIMIZE optimize\") ===\n      OptimizeTableCommand(None, Some(tblId(\"optimize\")), Nil)(Nil))\n\n    assert(parser.parsePlan(\"OPTIMIZE zorder\") ===\n      OptimizeTableCommand(None, Some(tblId(\"zorder\")), Nil)(Nil))\n\n    assert(parser.parsePlan(\"OPTIMIZE full\") ===\n      OptimizeTableCommand(None, Some(tblId(\"full\")), Nil)(Nil))\n\n    // Use the new keywords in column name\n    assert(parser.parsePlan(\"OPTIMIZE tbl WHERE zorder = 1 and optimize = 2 and full = 3\") ===\n      OptimizeTableCommand(None,\n        Some(tblId(\"tbl\"))\n        , Seq(\"zorder = 1 and optimize = 2 and full = 3\"))(Nil))\n\n    assert(parser.parsePlan(\"OPTIMIZE tbl ZORDER BY (optimize, zorder, full)\") ===\n      OptimizeTableCommand(None, Some(tblId(\"tbl\")), Nil)(\n        Seq(unresolvedAttr(\"optimize\"), unresolvedAttr(\"zorder\"), unresolvedAttr(\"full\"))))\n  }\n\n  test(\"DESCRIBE DETAIL command is parsed as expected\") {\n    val parser = new DeltaSqlParser(null)\n\n    // Desc detail on a table\n    assert(parser.parsePlan(\"DESCRIBE DETAIL catalog_foo.db.tbl\") ===\n      DescribeDeltaDetailCommand(\n        UnresolvedTable(Seq(\"catalog_foo\", \"db\", \"tbl\"), DescribeDeltaDetailCommand.CMD_NAME),\n        Map.empty))\n\n    // Desc detail on a raw path\n    assert(parser.parsePlan(\"DESCRIBE DETAIL \\\"/tmp/table\\\"\") ===\n      DescribeDeltaDetailCommand(\n        UnresolvedPathBasedTable(\"/tmp/table\", Map.empty, DescribeDeltaDetailCommand.CMD_NAME),\n        Map.empty))\n\n    // Desc detail on a delta raw path\n    assert(parser.parsePlan(\"DESCRIBE DETAIL delta.`dummy_raw_path`\") ===\n      DescribeDeltaDetailCommand(\n        UnresolvedTable(Seq(\"delta\", \"dummy_raw_path\"), DescribeDeltaDetailCommand.CMD_NAME),\n        Map.empty))\n  }\n\n  test(\"DESCRIBE HISTORY command is parsed as expected\") {\n    val parser = new DeltaSqlParser(null)\n    var parsedCmd = parser.parsePlan(\"DESCRIBE HISTORY catalog_foo.db.tbl\")\n    assert(parsedCmd.asInstanceOf[DescribeDeltaHistory].child ===\n        UnresolvedTable(Seq(\"catalog_foo\", \"db\", \"tbl\"), DescribeDeltaHistory.COMMAND_NAME))\n    parsedCmd = parser.parsePlan(\"DESCRIBE HISTORY delta.`/path/to/tbl`\")\n    assert(parsedCmd.asInstanceOf[DescribeDeltaHistory].child ===\n      UnresolvedTable(Seq(\"delta\", \"/path/to/tbl\"), DescribeDeltaHistory.COMMAND_NAME))\n    parsedCmd = parser.parsePlan(\"DESCRIBE HISTORY '/path/to/tbl'\")\n    assert(parsedCmd.asInstanceOf[DescribeDeltaHistory].child ===\n      UnresolvedPathBasedDeltaTable(\"/path/to/tbl\", Map.empty, DescribeDeltaHistory.COMMAND_NAME))\n  }\n\n  private def targetPlanForTable(tableParts: String*): UnresolvedTable =\n    UnresolvedTable(tableParts.toSeq, \"REORG\")\n\n  test(\"REORG command is parsed as expected\") {\n    val parser = new DeltaSqlParser(null)\n\n    assert(parser.parsePlan(\"REORG TABLE tbl APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"tbl\"))(Nil))\n\n    assert(parser.parsePlan(\"REORG TABLE tbl_${system:spark.testing} APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"tbl_true\"))(Nil))\n\n    withSQLConf(\"tbl_var\" -> \"tbl\") {\n      assert(parser.parsePlan(\"REORG TABLE ${tbl_var} APPLY (PURGE)\") ===\n        DeltaReorgTable(targetPlanForTable(\"tbl\"))(Nil))\n\n      assert(parser.parsePlan(\"REORG TABLE ${spark:tbl_var} APPLY (PURGE)\") ===\n        DeltaReorgTable(targetPlanForTable(\"tbl\"))(Nil))\n\n      assert(parser.parsePlan(\"REORG TABLE ${sparkconf:tbl_var} APPLY (PURGE)\") ===\n        DeltaReorgTable(targetPlanForTable(\"tbl\"))(Nil))\n\n      assert(parser.parsePlan(\"REORG TABLE ${hiveconf:tbl_var} APPLY (PURGE)\") ===\n        DeltaReorgTable(targetPlanForTable(\"tbl\"))(Nil))\n\n      assert(parser.parsePlan(\"REORG TABLE ${hivevar:tbl_var} APPLY (PURGE)\") ===\n        DeltaReorgTable(targetPlanForTable(\"tbl\"))(Nil))\n    }\n\n    assert(parser.parsePlan(\"REORG TABLE delta.`/path/to/tbl` APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"delta\", \"/path/to/tbl\"))(Nil))\n\n    assert(parser.parsePlan(\"REORG TABLE tbl WHERE part = 1 APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"tbl\"))(Seq(\"part = 1\")))\n  }\n\n  test(\"REORG command new tokens are non-reserved keywords\") {\n    // new keywords: REORG, APPLY, PURGE\n    val parser = new DeltaSqlParser(null)\n\n    // Use the new keywords in table name\n    assert(parser.parsePlan(\"REORG TABLE reorg APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"reorg\"))(Nil))\n    assert(parser.parsePlan(\"REORG TABLE apply APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"apply\"))(Nil))\n    assert(parser.parsePlan(\"REORG TABLE purge APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"purge\"))(Nil))\n\n    // Use the new keywords in column name\n    assert(parser.parsePlan(\n      \"REORG TABLE tbl WHERE reorg = 1 AND apply = 2 AND purge = 3 APPLY (PURGE)\") ===\n      DeltaReorgTable(targetPlanForTable(\"tbl\"))(Seq(\"reorg = 1 AND apply =2 AND purge = 3\")))\n  }\n\n  // scalastyle:off argcount\n  private def checkCloneStmt(\n      parser: DeltaSqlParser,\n      source: String,\n      target: String,\n      sourceFormat: String = \"delta\",\n      sourceIsTable: Boolean = true,\n      sourceIs3LTable: Boolean = false,\n      targetIsTable: Boolean = true,\n      targetLocation: Option[String] = None,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      isCreate: Boolean = true,\n      isReplace: Boolean = false,\n      tableProperties: Map[String, String] = Map.empty): Unit = {\n    assert {\n      parser.parsePlan(CloneTableSQLTestUtils.buildCloneSqlString(\n        source,\n        target,\n        sourceIsTable,\n        targetIsTable,\n        sourceFormat,\n        targetLocation = targetLocation,\n        versionAsOf = versionAsOf,\n        timestampAsOf = timestampAsOf,\n        isCreate = isCreate,\n        isReplace = isReplace,\n        tableProperties = tableProperties\n      )) == {\n        val sourceRelation = if (sourceIs3LTable) {\n          new UnresolvedRelation(source.split('.'))\n        } else {\n          UnresolvedRelation(tblId(source, if (sourceIsTable) null else sourceFormat))\n        }\n        CloneTableStatement(\n          if (versionAsOf.isEmpty && timestampAsOf.isEmpty) {\n            sourceRelation\n          } else {\n            TimeTravel(\n              sourceRelation,\n              timestampAsOf.map(Literal(_)),\n              versionAsOf,\n              Some(\"sql\"))\n          },\n          new UnresolvedRelation(target.split('.')),\n          ifNotExists = false,\n          isReplaceCommand = isReplace,\n          isCreateCommand = isCreate,\n          tablePropertyOverrides = tableProperties,\n          targetLocation = targetLocation\n        )\n      }\n    }\n  }\n  // scalastyle:on argcount\n\n  test(\"CLONE command is parsed as expected\") {\n    val parser = new DeltaSqlParser(null)\n    // Standard shallow clone\n    checkCloneStmt(parser, source = \"t1\", target = \"t1\")\n    // Path based source table\n    checkCloneStmt(parser, source = \"/path/to/t1\", target = \"t1\", sourceIsTable = false)\n    // REPLACE\n    checkCloneStmt(parser, source = \"t1\", target = \"t1\", isCreate = false, isReplace = true)\n    // CREATE OR REPLACE\n    checkCloneStmt(parser, source = \"t1\", target = \"t1\", isCreate = true, isReplace = true)\n    // Clone with table properties\n    checkCloneStmt(parser, source = \"t1\", target = \"t1\", tableProperties = Map(\"a\" -> \"a\"))\n    // Clone with external location\n    checkCloneStmt(parser, source = \"t1\", target = \"t1\", targetLocation = Some(\"/new/path\"))\n    // Clone with time travel\n    checkCloneStmt(parser, source = \"t1\", target = \"t1\", versionAsOf = Some(1L))\n    // Clone with 3L table (only useful for Iceberg table now)\n    checkCloneStmt(parser, source = \"local.iceberg.table\", target = \"t1\", sourceIs3LTable = true)\n    checkCloneStmt(parser, source = \"local.iceberg.table\", target = \"delta.table\",\n      sourceIs3LTable = true)\n    // Custom source format with path\n    checkCloneStmt(parser, source = \"/path/to/iceberg\", target = \"t1\", sourceFormat = \"iceberg\",\n      sourceIsTable = false)\n\n    // Target table with 3L name\n    checkCloneStmt(parser, source = \"/path/to/iceberg\", target = \"a.b.t1\", sourceFormat = \"iceberg\",\n      sourceIsTable = false)\n    checkCloneStmt(\n      parser, source = \"spark_catalog.tmp.table\", target = \"a.b.t1\", sourceIs3LTable = true)\n    checkCloneStmt(parser, source = \"t2\", target = \"a.b.t1\")\n  }\n\n  for (truncateHistory <- Seq(true, false))\n  test(s\"DROP FEATURE command is parsed as expected - truncateHistory: $truncateHistory\") {\n    val parser = new DeltaSqlParser(null)\n    val table = \"tbl\"\n    val featureName = \"feature_name\"\n    val sql = s\"ALTER TABLE $table DROP FEATURE $featureName \" +\n      (if (truncateHistory) \"TRUNCATE HISTORY\" else \"\")\n    val parsedCmd = parser.parsePlan(sql)\n    assert(parsedCmd ===\n      AlterTableDropFeature(\n        UnresolvedTable(Seq(table), \"ALTER TABLE ... DROP FEATURE\"),\n        featureName,\n        truncateHistory))\n  }\n\n  private def unresolvedAttr(colName: String*): UnresolvedAttribute = {\n    new UnresolvedAttribute(colName)\n  }\n\n  private def tblId(\n      tblName: String,\n      schema: String = null,\n      catalog: String = null): TableIdentifier = {\n    if (catalog == null) {\n      if (schema == null) new TableIdentifier(tblName)\n      else new TableIdentifier(tblName, Some(schema))\n    } else {\n      assert(schema != null)\n      new TableIdentifier(tblName, Some(schema), Some(catalog))\n    }\n  }\n\n  private def clusterByStatement(\n      createOrReplaceClause: String,\n      asSelect: Boolean,\n      schema: String,\n      clusterByClause: String): String = {\n    val tableSchema = if (asSelect) {\n      \"\"\n    } else {\n      s\"($schema)\"\n    }\n    val select = if (asSelect) {\n      \"AS SELECT * FROM tbl2\"\n    } else {\n      \"\"\n    }\n    s\"$createOrReplaceClause TABLE tbl $tableSchema USING DELTA $clusterByClause $select\"\n  }\n\n  private def validateClusterByTransform(\n      clause: String,\n      asSelect: Boolean,\n      plan: LogicalPlan,\n      expectedColumns: Seq[Seq[String]]): Unit = {\n    val partitioning = if (clause == \"CREATE\") {\n      if (asSelect) {\n        plan.asInstanceOf[CreateTableAsSelect].partitioning\n      } else {\n        plan.asInstanceOf[CreateTable].partitioning\n      }\n    } else {\n      if (asSelect) {\n        plan.asInstanceOf[ReplaceTableAsSelect].partitioning\n      } else {\n        plan.asInstanceOf[ReplaceTable].partitioning\n      }\n    }\n    assert(partitioning.size === 1)\n    val transform = partitioning.head\n    val actualColumns = transform match {\n      case ClusterByTransform(columnNames) => columnNames.map(_.fieldNames.toSeq)\n      case _ => assert(false, \"Should not reach here\")\n    }\n    assert(actualColumns === expectedColumns)\n  }\n\n  for (asSelect <- BOOLEAN_DOMAIN) {\n    Seq(\"CREATE\", \"REPLACE\").foreach { clause =>\n      test(s\"CLUSTER BY - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql = clusterByStatement(clause, asSelect, \"a int, b string\", \"CLUSTER BY (a)\")\n        val parsedPlan = parser.parsePlan(sql)\n        validateClusterByTransform(clause, asSelect, parsedPlan, Seq(Seq(\"a\")))\n      }\n\n      test(s\"CLUSTER BY nested column - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(clause, asSelect, \"a struct<b int, c string>\", \"CLUSTER BY (a.b, a.c)\")\n        val parsedPlan = parser.parsePlan(sql)\n        validateClusterByTransform(clause, asSelect, parsedPlan, Seq(Seq(\"a\", \"b\"), Seq(\"a\", \"c\")))\n      }\n\n      test(s\"CLUSTER BY backquoted column - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(clause, asSelect, \"`a.b.c` int\", \"CLUSTER BY (`a.b.c`)\")\n        val parsedPlan = parser.parsePlan(sql)\n        validateClusterByTransform(clause, asSelect, parsedPlan, Seq(Seq(\"a.b.c\")))\n      }\n\n      test(s\"CLUSTER BY comma column - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(clause, asSelect, \"`a,b` int\", \"CLUSTER BY (`a,b`)\")\n        val parsedPlan = parser.parsePlan(sql)\n        validateClusterByTransform(clause, asSelect, parsedPlan, Seq(Seq(\"a,b\")))\n      }\n\n      test(s\"CLUSTER BY duplicated clauses - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(clause, asSelect, \"a int, b string\", \"CLUSTER BY (a) CLUSTER BY (b)\")\n        checkError(intercept[ParseException] {\n          parser.parsePlan(sql)\n        }, \"DUPLICATE_CLAUSES\", parameters = Map(\"clauseName\" -> \"CLUSTER BY\"))\n      }\n\n      test(\"CLUSTER BY set clustering column property is ignored - \" +\n        s\"$clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(\n            clause,\n            asSelect,\n            \"a int, b string\",\n            \"CLUSTER BY (a) \" +\n            s\"TBLPROPERTIES ('${ClusteredTableUtils.PROP_CLUSTERING_COLUMNS}' = 'b')\")\n        val parsedPlan = parser.parsePlan(sql)\n        validateClusterByTransform(clause, asSelect, parsedPlan, Seq(Seq(\"a\")))\n      }\n\n      test(s\"CLUSTER BY with PARTITIONED BY - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(\n            clause,\n            asSelect,\n            \"a int, b string\",\n            \"CLUSTER BY (a) PARTITIONED BY (b)\")\n        val errorMsg = \"Clustering and partitioning cannot both be specified. \" +\n          \"Please remove PARTITIONED BY if you want to create a Delta table with clustering\"\n        checkError(intercept[ParseException] {\n          parser.parsePlan(sql)\n        }, \"_LEGACY_ERROR_TEMP_0035\", parameters = Map(\"message\" -> errorMsg))\n      }\n\n      test(s\"CLUSTER BY with bucketing - $clause TABLE asSelect = $asSelect\") {\n        val parser = new DeltaSqlParser(new SparkSqlParser())\n        val sql =\n          clusterByStatement(\n            clause,\n            asSelect,\n            \"a int, b string\",\n            \"CLUSTER BY (a) CLUSTERED BY (b) INTO 2 BUCKETS\")\n        val errorMsg = \"Clustering and bucketing cannot both be specified. \" +\n          \"Please remove CLUSTERED BY INTO BUCKETS if you \" +\n          \"want to create a Delta table with clustering\"\n        checkError(intercept[ParseException] {\n          parser.parsePlan(sql)\n        }, \"_LEGACY_ERROR_TEMP_0035\", parameters = Map(\"message\" -> errorMsg))\n      }\n    }\n  }\n\n  test(\"string coalescing\") {\n    val parser = new DeltaSqlParser(new SparkSqlParser())\n\n    val pathToTable = \"/path/to/table\"\n    val partedPathes = Seq(\n      \"'/path/to/table'\",\n      \"'/path/to' '/table'\",\n      \"'/path' '/to' '/table'\"\n    )\n\n    partedPathes.foreach { path =>\n      // CLONE LOCATION\n      val cloneCmd = parser.parsePlan(\n        s\"CREATE TABLE t1 SHALLOW CLONE source LOCATION $path\")\n      assert(cloneCmd.asInstanceOf[CloneTableStatement].targetLocation === Some(pathToTable))\n\n      // OPTIMIZE\n      val optimizeCmd = parser.parsePlan(s\"OPTIMIZE $path\")\n      assert(optimizeCmd ===\n        OptimizeTableCommand(Some(pathToTable), None, Nil)(Nil))\n\n      // DESCRIBE HISTORY\n      var describeHistoryCmd = parser.parsePlan(s\"DESCRIBE HISTORY $path\")\n      assert(describeHistoryCmd.asInstanceOf[DescribeDeltaHistory].child ===\n        UnresolvedPathBasedDeltaTable(pathToTable, Map.empty, DescribeDeltaHistory.COMMAND_NAME))\n\n      // DESCRIBE DETAIL\n      val describeDetailCmd = parser.parsePlan(s\"DESCRIBE DETAIL $path\")\n      assert(describeDetailCmd ===\n        DescribeDeltaDetailCommand(\n          UnresolvedPathBasedTable(pathToTable, Map.empty, DescribeDeltaDetailCommand.CMD_NAME),\n          Map.empty))\n\n      // VACUUM\n      val vacuumCmd = parser.parsePlan(s\"VACUUM $path\")\n      assert(vacuumCmd ===\n        VacuumTableCommand(\n          UnresolvedPathBasedDeltaTable(pathToTable, Map.empty, \"VACUUM\"),\n          None, None, None, false, None))\n      }\n\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/tables/DeltaTableBuilderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtils\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, LongType, MetadataBuilder, StringType, StructType}\n\nclass DeltaTableBuilderSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with ClusteredTableTestUtils {\n\n  // Define the information for a default test table used by many tests.\n  protected val defaultTestTableSchema = \"c1 int, c2 int, c3 string\"\n  protected val defaultTestTableGeneratedColumns = Map(\"c2\" -> \"c1 + 10\")\n  protected val defaultTestTablePartitionColumns = Seq(\"c1\")\n  protected val defaultTestTableColumnComments = Map(\"c1\" -> \"foo\", \"c3\" -> \"bar\")\n  protected val defaultTestTableComment = \"tbl comment\"\n  protected val defaultTestTableNullableCols = Set(\"c1\", \"c3\")\n  protected val defaultTestTableProperty = (\"foo\", \"bar\")\n\n\n  /**\n   * Verify if the table metadata matches the test table. We use this to verify DDLs\n   * write correct table metadata into the transaction logs.\n   */\n  protected def verifyTestTableMetadata(\n      table: String,\n      schemaString: String,\n      generatedColumns: Map[String, String] = Map.empty,\n      colComments: Map[String, String] = Map.empty,\n      colNullables: Set[String] = Set.empty,\n      tableComment: Option[String] = None,\n      partitionCols: Seq[String] = Seq.empty,\n      tableProperty: Option[(String, String)] = None\n      ): Unit = {\n    val deltaLog = if (table.startsWith(\"delta.\")) {\n      DeltaLog.forTable(spark, table.stripPrefix(\"delta.`\").stripSuffix(\"`\"))\n    } else {\n      DeltaLog.forTable(spark, TableIdentifier(table))\n    }\n    val schema = StructType.fromDDL(schemaString)\n    val expectedSchema = StructType(schema.map { field =>\n      val newMetadata = new MetadataBuilder()\n        .withMetadata(field.metadata)\n      if (generatedColumns.contains(field.name)) {\n        newMetadata.putString(GENERATION_EXPRESSION_METADATA_KEY, generatedColumns(field.name))\n      }\n      if (colComments.contains(field.name)) {\n        newMetadata.putString(\"comment\", colComments(field.name))\n      }\n      field.copy(\n        nullable = colNullables.contains(field.name),\n        metadata = newMetadata.build)\n    })\n    val metadata = deltaLog.snapshot.metadata\n    assert(metadata.schema == expectedSchema)\n    assert(metadata.partitionColumns == partitionCols)\n    if (tableProperty.nonEmpty) {\n      assert(metadata.configuration(tableProperty.get._1).contentEquals(tableProperty.get._2))\n    }\n    if (tableComment.nonEmpty) {\n      assert(metadata.description.contentEquals(tableComment.get))\n    }\n  }\n\n  protected def testCreateTable(testName: String)(createFunc: String => Unit): Unit = {\n    test(testName) {\n      withTable(testName) {\n        createFunc(testName)\n        verifyTestTableMetadata(\n          testName, defaultTestTableSchema, defaultTestTableGeneratedColumns,\n          defaultTestTableColumnComments, defaultTestTableNullableCols,\n          Some(defaultTestTableComment), defaultTestTablePartitionColumns,\n          Some(defaultTestTableProperty)\n        )\n      }\n    }\n  }\n\n  protected def testCreateTableWithNameAndLocation(\n      testName: String)(createFunc: (String, String) => Unit): Unit = {\n    test(testName + \": external - with location and name\") {\n      withTempPath { path =>\n        withTable(testName) {\n          createFunc(testName, path.getCanonicalPath)\n          verifyTestTableMetadata(\n            testName,\n            defaultTestTableSchema, defaultTestTableGeneratedColumns,\n            defaultTestTableColumnComments, defaultTestTableNullableCols,\n            Some(defaultTestTableComment), defaultTestTablePartitionColumns,\n            Some(defaultTestTableProperty)\n          )\n        }\n      }\n    }\n  }\n\n  protected def testCreateTableWithLocationOnly(\n      testName: String)(createFunc: String => Unit): Unit = {\n    test(testName + \": external - location only\") {\n      withTempPath { path =>\n        withTable(testName) {\n          createFunc(path.getCanonicalPath)\n          verifyTestTableMetadata(\n            s\"delta.`${path.getCanonicalPath}`\",\n            defaultTestTableSchema, defaultTestTableGeneratedColumns,\n            defaultTestTableColumnComments, defaultTestTableNullableCols,\n            Some(defaultTestTableComment), defaultTestTablePartitionColumns,\n            Some(defaultTestTableProperty)\n          )\n        }\n      }\n    }\n  }\n\n  def defaultCreateTableBuilder(\n      ifNotExists: Boolean,\n      tableName: Option[String] = None,\n      location: Option[String] = None): DeltaTableBuilder = {\n    val tableBuilder = if (ifNotExists) {\n      io.delta.tables.DeltaTable.createIfNotExists()\n    } else {\n      io.delta.tables.DeltaTable.create()\n    }\n    defaultTableBuilder(tableBuilder, tableName, location)\n  }\n\n  def defaultReplaceTableBuilder(\n      orCreate: Boolean,\n      tableName: Option[String] = None,\n      location: Option[String] = None): DeltaTableBuilder = {\n    var tableBuilder = if (orCreate) {\n      io.delta.tables.DeltaTable.createOrReplace()\n    } else {\n      io.delta.tables.DeltaTable.replace()\n    }\n    defaultTableBuilder(tableBuilder, tableName, location)\n  }\n\n  private def defaultTableBuilder(\n      builder: DeltaTableBuilder,\n      tableName: Option[String],\n      location: Option[String]\n      ) = {\n    var tableBuilder = builder\n    if (tableName.nonEmpty) {\n      tableBuilder = tableBuilder.tableName(tableName.get)\n    }\n    if (location.nonEmpty) {\n      tableBuilder = tableBuilder.location(location.get)\n    }\n    tableBuilder.addColumn(\n      io.delta.tables.DeltaTable.columnBuilder(\"c1\").dataType(\"int\").nullable(true).comment(\"foo\")\n        .build()\n    )\n    tableBuilder.addColumn(\n      io.delta.tables.DeltaTable.columnBuilder(\"c2\").dataType(\"int\")\n        .nullable(false).generatedAlwaysAs(\"c1 + 10\").build()\n    )\n    tableBuilder.addColumn(\n      io.delta.tables.DeltaTable.columnBuilder(\"c3\").dataType(\"string\").comment(\"bar\").build()\n    )\n    tableBuilder.partitionedBy(\"c1\")\n    tableBuilder.property(\"foo\", \"bar\")\n    tableBuilder.comment(\"tbl comment\")\n    tableBuilder\n  }\n\n  test(\"create table with existing schema and extra column\") {\n    withTable(\"table\") {\n      withTempDir { dir =>\n        spark.range(10).toDF(\"key\").write.format(\"parquet\").saveAsTable(\"table\")\n        val existingSchema = spark.read.format(\"parquet\").table(\"table\").schema\n        io.delta.tables.DeltaTable.create()\n          .location(dir.getAbsolutePath)\n          .addColumns(existingSchema)\n          .addColumn(\"value\", \"string\", false)\n          .execute()\n        verifyTestTableMetadata(s\"delta.`${dir.getAbsolutePath}`\",\n          \"key bigint, value string\", colNullables = Set(\"key\"))\n      }\n    }\n  }\n\n  test(\"create table with variation of addColumns - with spark session\") {\n    withTable(\"test\") {\n      io.delta.tables.DeltaTable.create(spark)\n        .tableName(\"test\")\n        .addColumn(\"c1\", \"int\")\n        .addColumn(\"c2\", IntegerType)\n        .addColumn(\"c3\", \"string\", false)\n        .addColumn(\"c4\", StringType, true)\n        .addColumn(\n          io.delta.tables.DeltaTable.columnBuilder(spark, \"c5\")\n            .dataType(\"bigint\")\n            .comment(\"foo\")\n            .nullable(false)\n            .build\n        )\n        .addColumn(\n          io.delta.tables.DeltaTable.columnBuilder(spark, \"c6\")\n            .dataType(LongType)\n            .generatedAlwaysAs(\"c5 + 10\")\n            .build\n        ).execute()\n      verifyTestTableMetadata(\n        \"test\", \"c1 int, c2 int, c3 string, c4 string, c5 bigint, c6 bigint\",\n        generatedColumns = Map(\"c6\" -> \"c5 + 10\"),\n        colComments = Map(\"c5\" -> \"foo\"),\n        colNullables = Set(\"c1\", \"c2\", \"c4\", \"c6\")\n      )\n    }\n  }\n\n  test(\"test addColumn using columnBuilder, without dataType\") {\n    val e = intercept[AnalysisException] {\n      DeltaTable.columnBuilder(\"value\")\n        .generatedAlwaysAs(\"true\")\n        .nullable(true)\n        .build()\n    }\n    assert(e.getMessage.contains(\"The data type of the column `value` was not provided\"))\n  }\n\n  testCreateTable(\"create_table\") { table =>\n    defaultCreateTableBuilder(ifNotExists = false, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"create_table\") { (name, path) =>\n    defaultCreateTableBuilder(ifNotExists = false, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"create_table\") { path =>\n    defaultCreateTableBuilder(ifNotExists = false, location = Some(path)).execute()\n  }\n\n  test(\"create table - errors if already exists\") {\n    withTable(\"testTable\") {\n      sql(s\"CREATE TABLE testTable (c1 int) USING DELTA\")\n      intercept[TableAlreadyExistsException] {\n        defaultCreateTableBuilder(ifNotExists = false, Some(\"testTable\")).execute()\n      }\n    }\n  }\n\n  test(\"create table - ignore if already exists\") {\n    withTable(\"testTable\") {\n      sql(s\"CREATE TABLE testTable (c1 int) USING DELTA\")\n      defaultCreateTableBuilder(ifNotExists = true, Some(\"testTable\")).execute()\n      verifyTestTableMetadata(\"testTable\", \"c1 int\", colNullables = Set(\"c1\"))\n    }\n  }\n\n  testCreateTable(\"create_table_if_not_exists\") { table =>\n    defaultCreateTableBuilder(ifNotExists = true, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"create_table_if_not_exists\") { (name, path) =>\n    defaultCreateTableBuilder(ifNotExists = true, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"create_table_if_not_exists\") { path =>\n    defaultCreateTableBuilder(ifNotExists = true, location = Some(path)).execute()\n  }\n\n  test(\"replace table - errors if not exists\") {\n    intercept[AnalysisException] {\n      defaultReplaceTableBuilder(orCreate = false, Some(\"testTable\")).execute()\n    }\n  }\n\n  testCreateTable(\"replace_table\") { table =>\n    sql(s\"CREATE TABLE replace_table(c1 int) USING DELTA\")\n    defaultReplaceTableBuilder(orCreate = false, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"replace_table\") { (name, path) =>\n    sql(s\"CREATE TABLE $name (c1 int) USING DELTA LOCATION '$path'\")\n    defaultReplaceTableBuilder(orCreate = false, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"replace_table\") { path =>\n    sql(s\"CREATE TABLE delta.`$path` (c1 int) USING DELTA\")\n    defaultReplaceTableBuilder(orCreate = false, location = Some(path)).execute()\n  }\n\n  testCreateTable(\"replace_or_create_table\") { table =>\n    defaultReplaceTableBuilder(orCreate = true, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"replace_or_create_table\") { (name, path) =>\n    defaultReplaceTableBuilder(orCreate = true, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"replace_or_create_table\") { path =>\n    defaultReplaceTableBuilder(orCreate = true, location = Some(path)).execute()\n  }\n\n  test(\"test no identifier and no location\") {\n    val e = intercept[AnalysisException] {\n      io.delta.tables.DeltaTable.create().addColumn(\"c1\", \"int\").execute()\n    }\n    assert(e.getMessage.contains(\"Table name or location has to be specified\"))\n  }\n\n  test(\"partitionedBy only should contain columns in the schema\") {\n    val e = intercept[AnalysisException] {\n      io.delta.tables.DeltaTable.create().tableName(\"testTable\")\n        .addColumn(\"c1\", \"int\")\n        .partitionedBy(\"c2\")\n        .execute()\n    }\n    assert(e.getMessage.startsWith(\"Couldn't find column c2\"))\n  }\n\n  test(\"errors if table name and location are different paths\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      val e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.create().tableName(s\"delta.`$path`\")\n          .addColumn(\"c1\", \"int\")\n          .location(\"src/test/resources/delta/dbr_8_0_non_generated_columns\")\n          .execute()\n      }\n      assert(e.getMessage.contains(\n        \"Creating path-based Delta table with a different location isn't supported.\"))\n    }\n  }\n\n  test(\"table name and location are the same\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      io.delta.tables.DeltaTable.create().tableName(s\"delta.`$path`\")\n        .addColumn(\"c1\", \"int\")\n        .location(path)\n        .execute()\n    }\n  }\n\n  test(\"errors if use parquet path as identifier\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      val e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.create().tableName(s\"parquet.`$path`\")\n          .addColumn(\"c1\", \"int\")\n          .location(path)\n          .execute()\n      }\n      assert(e.getMessage == \"Database 'main.parquet' not found\" ||\n        e.getMessage == \"Database 'parquet' not found\" ||\n        e.getMessage.contains(\"is not a valid name\") ||\n        e.getMessage.contains(\"schema `parquet` cannot be found\")\n      )\n    }\n  }\n\n  test(\"delta table property case\") {\n    sealed trait DeltaTablePropertySetOperation {\n      val preservedCaseConfig = Map(\"delta.appendOnly\" -> \"true\", \"Foo\" -> \"Bar\", \"foo\" -> \"Bar\")\n      val lowerCaseEnforcedConfig = Map(\"delta.appendOnly\" -> \"true\", \"foo\" -> \"Bar\")\n\n      def setTableProperty(tablePath: String): Unit\n\n      def expectedConfig: Map[String, String]\n\n      def description: String\n    }\n\n    trait CasePreservingTablePropertySetOperation extends DeltaTablePropertySetOperation {\n\n      val expectedConfig = preservedCaseConfig\n    }\n\n    case object SetPropertyThroughCreate extends CasePreservingTablePropertySetOperation {\n      def setTableProperty(tablePath: String): Unit = sql(\n        s\"CREATE TABLE delta.`$tablePath`(id INT) \" +\n          s\"USING delta TBLPROPERTIES('delta.appendOnly'='true', 'Foo'='Bar', 'foo'='Bar' ) \"\n      )\n\n      val description = \"Setting Table Property at Table Creation\"\n    }\n\n    case object SetPropertyThroughAlter extends CasePreservingTablePropertySetOperation {\n      def setTableProperty(tablePath: String): Unit = {\n        spark.range(1, 10).write.format(\"delta\").save(tablePath)\n        sql(s\"ALTER TABLE delta.`$tablePath` \" +\n          s\"SET TBLPROPERTIES('delta.appendOnly'='true', 'Foo'='Bar', 'foo'='Bar')\")\n      }\n\n      val description = \"Setting Table Property via Table Alter\"\n    }\n\n    case class SetPropertyThroughTableBuilder(backwardCompatible: Boolean) extends\n      DeltaTablePropertySetOperation {\n\n      def setTableProperty(tablePath: String): Unit = {\n        withSQLConf(DeltaSQLConf.TABLE_BUILDER_FORCE_TABLEPROPERTY_LOWERCASE.key\n          -> backwardCompatible.toString) {\n          DeltaTable.create()\n            .location(tablePath)\n            .property(\"delta.appendOnly\", \"true\")\n            .property(\"Foo\", \"Bar\")\n            .property(\"foo\", \"Bar\")\n            .execute()\n        }\n      }\n\n      override def expectedConfig : Map[String, String] = {\n        if (backwardCompatible) {\n          lowerCaseEnforcedConfig\n        }\n        else {\n          preservedCaseConfig\n        }\n      }\n\n      val description = s\"Setting Table Property on DeltaTableBuilder.\" +\n        s\" Backward compatible enabled = ${backwardCompatible}\"\n    }\n\n    val examples = Seq(\n      SetPropertyThroughCreate,\n      SetPropertyThroughAlter,\n      SetPropertyThroughTableBuilder(backwardCompatible = true),\n      SetPropertyThroughTableBuilder(backwardCompatible = false)\n    )\n\n    for (example <- examples) {\n      withClue(example.description) {\n        withTempDir { dir =>\n          val path = dir.getCanonicalPath()\n          example.setTableProperty(path)\n          val config = DeltaLog.forTable(spark, path).snapshot.metadata.configuration\n          assert(\n            config == example.expectedConfig,\n            s\"$example's result is not correct: $config\")\n        }\n      }\n    }\n  }\n\n  test(\"create table with clustering\") {\n    withSQLConf(\n      // Enable update catalog for verifyClusteringColumns.\n      DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"true\") {\n      withTable(\"test\") {\n        io.delta.tables.DeltaTable.create().tableName(\"test\")\n          .addColumn(\"c1\", \"int\")\n          .clusterBy(\"c1\")\n          .execute()\n\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"test\"))\n        val metadata = deltaLog.snapshot.metadata\n        verifyClusteringColumns(TableIdentifier(\"test\"), Seq(\"c1\"))\n      }\n    }\n  }\n\n  test(\"errors if partition and cluster columns are provided\") {\n    withTable(\"test\") {\n      val e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.create().tableName(\"test\")\n          .addColumn(\"c1\", \"int\")\n          .clusterBy(\"c1\")\n          .partitionedBy(\"c1\")\n          .execute()\n      }\n\n      checkError(e, \"DELTA_CLUSTER_BY_WITH_PARTITIONED_BY\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/tables/DeltaTableForNameSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.mutable\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DummyCatalogWithNamespace}\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.connector.catalog.CatalogNotFoundException\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaTableForNameSuite extends QueryTest\n  with DeltaSQLCommandTest\n  with SharedSparkSession {\n  private val sparkCatalog = \"spark_catalog\"\n  private val catalogName = \"test_catalog\"\n  private val defaultSchema = \"default\"\n  private val nonDefaultSchema = \"non_default\"\n  private val commonTblName = \"tbl\"\n  private val defaultSchemaUniqueTbl = \"default_tbl\"\n  private val nonDefaultSchemaUniqueTbl = \"non_default_tbl\"\n  private val nonSessionCatalogNonDefaultSchema = \"non_default_session_schema\"\n  private val tableNameToId = mutable.Map.empty[String, String]\n\n  override def sparkConf: SparkConf =\n    super.sparkConf\n      .set(s\"spark.sql.catalog.$catalogName\", classOf[DummyCatalogWithNamespace].getName)\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // General Test setup\n    sql(s\"CREATE SCHEMA $nonDefaultSchema;\")\n    sql(s\"CREATE SCHEMA $catalogName.$defaultSchema;\")\n    sql(s\"CREATE SCHEMA $catalogName.$nonSessionCatalogNonDefaultSchema;\")\n    // Setup custom catalog default schema tables\n    sql(s\"SET CATALOG $catalogName\")\n    setUpTable(catalogName, defaultSchema, commonTblName)\n    setUpTable(catalogName, defaultSchema, defaultSchemaUniqueTbl)\n    // Setup custom catalog non default schema tables\n    setUpTable(catalogName, nonSessionCatalogNonDefaultSchema, commonTblName)\n    setUpTable(catalogName, nonSessionCatalogNonDefaultSchema, nonDefaultSchemaUniqueTbl)\n    // Setup session catalog default schema tables\n    sql(s\"SET CATALOG $sparkCatalog\")\n    setUpTable(sparkCatalog, defaultSchema, commonTblName)\n    setUpTable(sparkCatalog, defaultSchema, defaultSchemaUniqueTbl)\n    // Setup session catalog non default schema tables\n    setUpTable(sparkCatalog, nonDefaultSchema, commonTblName)\n    setUpTable(sparkCatalog, nonDefaultSchema, nonDefaultSchemaUniqueTbl)\n  }\n\n  override def afterAll(): Unit = {\n    sql(s\"DROP SCHEMA $sparkCatalog.$nonDefaultSchema CASCADE;\")\n    sql(s\"DROP SCHEMA $catalogName.$defaultSchema CASCADE;\")\n    sql(s\"DROP SCHEMA $catalogName.$nonSessionCatalogNonDefaultSchema CASCADE;\")\n    super.afterAll()\n  }\n\n  protected override def beforeEach(): Unit = {\n    super.beforeEach()\n    sql(s\"SET CATALOG $sparkCatalog\")\n  }\n\n  private def getTablePath(tableName: String): Path = {\n    new Path(DummyCatalogWithNamespace.catalogDir + s\"/$tableName\")\n  }\n\n  private def setUpTable(catalog: String, schema: String, table: String): Unit = {\n    val tableName = s\"$catalog.$schema.$table\"\n    val path = getTablePath(tableName)\n    sql(s\"CREATE OR REPLACE TABLE $tableName (id int) USING delta LOCATION '$path';\")\n    tableNameToId += (tableName -> DeltaLog.forTable(spark,\n      getTablePath(tableName)).tableId)\n  }\n\n  private def validateForNameTableId(\n      tableName: String,\n      expectedResult: Option[String] = None): Unit = {\n    val table = DeltaTable.forName(spark, tableName)\n    checkAnswer(table.detail().select(\"id\"), Seq(Row(expectedResult.getOrElse(\n      tableNameToId(tableName)\n    ))))\n  }\n\n  test(s\"forName resolves fully qualified tables in session catalog correctly\") {\n    validateForNameTableId(s\"$sparkCatalog.$defaultSchema.$commonTblName\")\n    validateForNameTableId(s\"$sparkCatalog.$defaultSchema.$defaultSchemaUniqueTbl\")\n\n    validateForNameTableId(s\"$sparkCatalog.$nonDefaultSchema.$commonTblName\")\n    validateForNameTableId(s\"$sparkCatalog.$nonDefaultSchema.$nonDefaultSchemaUniqueTbl\")\n  }\n\n  test(s\"forName resolves partially qualified tables in session catalog correctly\") {\n    validateForNameTableId(s\"$defaultSchema.$commonTblName\",\n      Some(tableNameToId(s\"$sparkCatalog.$defaultSchema.$commonTblName\")))\n    validateForNameTableId(s\"$defaultSchema.$defaultSchemaUniqueTbl\",\n      Some(tableNameToId(s\"$sparkCatalog.$defaultSchema.$defaultSchemaUniqueTbl\")))\n\n    validateForNameTableId(s\"$nonDefaultSchema.$commonTblName\",\n      Some(tableNameToId(s\"$sparkCatalog.$nonDefaultSchema.$commonTblName\")))\n    validateForNameTableId(s\"$nonDefaultSchema.$nonDefaultSchemaUniqueTbl\",\n      Some(tableNameToId(s\"$sparkCatalog.$nonDefaultSchema.$nonDefaultSchemaUniqueTbl\")))\n  }\n\n  test(s\"forName resolves fully qualified tables in non session catalog correctly\") {\n    validateForNameTableId(s\"$catalogName.$defaultSchema.$commonTblName\")\n    validateForNameTableId(s\"$catalogName.$defaultSchema.$defaultSchemaUniqueTbl\")\n\n    validateForNameTableId(s\"$catalogName.$nonSessionCatalogNonDefaultSchema.$commonTblName\")\n    validateForNameTableId(\n      s\"$catalogName.$nonSessionCatalogNonDefaultSchema.$nonDefaultSchemaUniqueTbl\")\n  }\n\n  for (table <- Seq(commonTblName, nonDefaultSchemaUniqueTbl))\n  test(s\"forName fails for partially \" +\n    s\"qualified tables in non session catalog with table=$table\") {\n    sql(s\"SET CATALOG $catalogName\")\n    val e = intercept[AnalysisException] {\n      DeltaTable.forName(spark, s\"$nonSessionCatalogNonDefaultSchema.$table\")\n    }\n    checkError(exception = e, \"DELTA_MISSING_DELTA_TABLE\",\n      parameters = Map(\"tableName\" -> s\"`$nonSessionCatalogNonDefaultSchema`.`$table`\"))\n  }\n\n  // forName currently doesn't resolve unqualified tables correctly for non session catalogs.\n  // in this test, it resolves to the table `spark_catalog.default.tbl` but it adds an incorrect\n  // identifier on top.\n  test(s\"forName resolves partially qualified tables in non session catalog incorrectly\") {\n    sql(s\"SET CATALOG $catalogName\")\n    validateForNameTableId(s\"$defaultSchema.$commonTblName\",\n      Some(tableNameToId(s\"$catalogName.$defaultSchema.$commonTblName\")))\n    validateForNameTableId(s\"$defaultSchema.$defaultSchemaUniqueTbl\",\n      Some(tableNameToId(s\"$catalogName.$defaultSchema.$defaultSchemaUniqueTbl\")))\n  }\n\n  test(\"forName with invalid non session catalog\") {\n    intercept[CatalogNotFoundException] {\n      DeltaTable.forName(spark, \"invalid_catalog.default.tbl\")\n    }\n  }\n\n  test(\"forName with unqualified non session catalog\") {\n    sql(s\"SET CATALOG $sparkCatalog\")\n    val e = intercept[AnalysisException] {\n      DeltaTable.forName(spark, s\"$nonSessionCatalogNonDefaultSchema.$commonTblName\")\n    }\n    checkError(exception = e, \"DELTA_MISSING_DELTA_TABLE\",\n      parameters = Map(\"tableName\" -> s\"`$nonSessionCatalogNonDefaultSchema`.`$commonTblName`\"))\n  }\n\n  test(\"forName fails with fully qualified non existent table\") {\n    val e = intercept[AnalysisException] {\n      DeltaTable.forName(spark, s\"$catalogName.$defaultSchema.invalid_table\")\n    }\n    checkError(exception = e, \"TABLE_OR_VIEW_NOT_FOUND\",\n      parameters = Map(\"relationName\" -> s\"`$catalogName`.`$defaultSchema`.`invalid_table`\"))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/tables/DeltaTableSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport java.io.File\nimport java.sql.Timestamp\nimport java.util.Locale\n\nimport scala.concurrent.duration._\nimport scala.language.postfixOps\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{AppendOnlyTableFeature, DeltaIllegalArgumentException, DeltaLog, DeltaTableFeatureException, FakeFileSystem, InvariantsTableFeature, TestReaderWriterFeature, TestRemovableReaderWriterFeature, TestRemovableWriterFeature, TestWriterFeature}\nimport org.apache.spark.sql.delta.actions.{ Metadata, Protocol }\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LocalLogStore\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.{Path, UnsupportedFileSystemException}\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.network.util.JavaUtils\nimport org.apache.spark.sql.{functions, AnalysisException, DataFrame, Dataset, QueryTest, Row}\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\nclass DeltaTableSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  test(\"forPath\") {\n    withTempDir { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      checkAnswer(\n        DeltaTable.forPath(spark, dir.getAbsolutePath).toDF,\n        testData.collect().toSeq)\n      checkAnswer(\n        DeltaTable.forPath(dir.getAbsolutePath).toDF,\n        testData.collect().toSeq)\n    }\n  }\n\n  test(\"forPath - with non-Delta table path\") {\n    val msg = \"not a delta table\"\n    withTempDir { dir =>\n      testData.write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n      testError(msg) { DeltaTable.forPath(spark, dir.getAbsolutePath) }\n      testError(msg) { DeltaTable.forPath(dir.getAbsolutePath) }\n    }\n  }\n\n  test(\"forName\") {\n    withTempDir { dir =>\n      withTable(\"deltaTable\") {\n        testData.write.format(\"delta\").saveAsTable(\"deltaTable\")\n\n        checkAnswer(\n          DeltaTable.forName(spark, \"deltaTable\").toDF,\n          testData.collect().toSeq)\n        checkAnswer(\n          DeltaTable.forName(\"deltaTable\").toDF,\n          testData.collect().toSeq)\n\n      }\n    }\n  }\n\n  def testForNameOnNonDeltaName(tableName: String): Unit = {\n    val msg = \"not a Delta table\"\n    testError(msg) { DeltaTable.forName(spark, tableName) }\n    testError(msg) { DeltaTable.forName(tableName) }\n  }\n\n  test(\"forName - with non-Delta table name\") {\n    withTempDir { dir =>\n      withTable(\"notADeltaTable\") {\n        testData.write.format(\"parquet\").mode(\"overwrite\").saveAsTable(\"notADeltaTable\")\n        testForNameOnNonDeltaName(\"notADeltaTable\")\n      }\n    }\n  }\n\n  test(\"forName - with temp view name\") {\n    withTempDir { dir =>\n      withTempView(\"viewOnDeltaTable\") {\n        testData.write.format(\"delta\").save(dir.getAbsolutePath)\n        spark.read.format(\"delta\").load(dir.getAbsolutePath)\n          .createOrReplaceTempView(\"viewOnDeltaTable\")\n        testForNameOnNonDeltaName(\"viewOnDeltaTable\")\n      }\n    }\n  }\n\n  test(\"forName - with delta.`path`\") {\n    // for name should work on Delta table paths\n    withTempDir { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      checkAnswer(\n        DeltaTable.forName(spark, s\"delta.`$dir`\").toDF,\n        testData.collect().toSeq)\n      checkAnswer(\n        DeltaTable.forName(s\"delta.`$dir`\").toDF,\n        testData.collect().toSeq)\n    }\n\n    // using forName on non Delta Table paths should fail\n    withTempDir { dir =>\n      testForNameOnNonDeltaName(s\"delta.`$dir`\")\n\n      testData.write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n      testForNameOnNonDeltaName(s\"delta.`$dir`\")\n    }\n  }\n\n  test(\"as\") {\n    withTempDir { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      checkAnswer(\n        DeltaTable.forPath(dir.getAbsolutePath).as(\"tbl\").toDF.select(\"tbl.value\"),\n        testData.select(\"value\").collect().toSeq)\n    }\n  }\n\n  test(\"isDeltaTable - path - with _delta_log dir\") {\n    withTempDir { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      assert(DeltaTable.isDeltaTable(dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - path - with empty _delta_log dir\") {\n    withTempDir { dir =>\n      new File(dir, \"_delta_log\").mkdirs()\n      assert(!DeltaTable.isDeltaTable(dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - path - with no _delta_log dir\") {\n    withTempDir { dir =>\n      assert(!DeltaTable.isDeltaTable(dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - path - with non-existent dir\") {\n    withTempDir { dir =>\n      JavaUtils.deleteRecursively(dir)\n      assert(!DeltaTable.isDeltaTable(dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - with non-Delta table path\") {\n    withTempDir { dir =>\n      testData.write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n      assert(!DeltaTable.isDeltaTable(dir.getAbsolutePath))\n    }\n  }\n\n  def testError(expectedMsg: String)(thunk: => Unit): Unit = {\n    val e = intercept[AnalysisException] { thunk }\n    assert(e.getMessage.toLowerCase(Locale.ROOT).contains(expectedMsg.toLowerCase(Locale.ROOT)))\n  }\n\n  test(\"DeltaTable is Java Serializable but cannot be used in executors\") {\n    import testImplicits._\n\n    // DeltaTable can be passed to executor without method calls.\n    withTempDir { dir =>\n      testData.write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n      val dt: DeltaTable = DeltaTable.forPath(dir.getAbsolutePath)\n      spark.range(5).as[Long].map{ row: Long =>\n        val foo = dt\n        row + 3\n      }.count()\n    }\n\n    // DeltaTable can be passed to executor but method call causes exception.\n    val e = intercept[Exception] {\n      withTempDir { dir =>\n        testData.write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n        val dt: DeltaTable = DeltaTable.forPath(dir.getAbsolutePath)\n        spark.range(5).as[Long].map{ row: Long =>\n          dt.toDF\n          row + 3\n        }.count()\n      }\n    }.getMessage\n    assert(e.contains(\"DeltaTable cannot be used in executors\"))\n  }\n}\n\nclass DeltaTableHadoopOptionsSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  protected override def sparkConf = {\n    // The drop feature test below is targeting the drop feature with history truncation\n    // implementation. The fast drop feature implementation adds a new writer feature when dropping\n    // a feature and also does not require any waiting time. The fast drop feature implementation\n    // is tested extensively in the DeltaFastDropFeatureSuite.\n    super.sparkConf\n      .set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, \"false\")\n      .set(\"spark.delta.logStore.fake.impl\", classOf[LocalLogStore].getName)\n  }\n\n  /**\n   * Create Hadoop file system options for `FakeFileSystem`. If Delta doesn't pick up them,\n   * it won't be able to read/write any files using `fake://`.\n   */\n  private def fakeFileSystemOptions: Map[String, String] = {\n    Map(\n      \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n      \"fs.fake.impl.disable.cache\" -> \"true\"\n    )\n  }\n\n  /** Create a fake file system path to test from the dir path. */\n  private def fakeFileSystemPath(dir: File): String = s\"fake://${dir.getCanonicalPath}\"\n\n  private def readDeltaTableByPath(path: String): DataFrame = {\n    spark.read.options(fakeFileSystemOptions).format(\"delta\").load(path)\n  }\n\n  // Ensure any new API from [[DeltaTable]] has to verify it can work with custom file system\n  // options.\n  private val publicMethods =\n  scala.reflect.runtime.universe.typeTag[io.delta.tables.DeltaTable].tpe.decls\n    .filter(_.isPublic)\n    .map(_.name.toString).toSet\n\n  private val ignoreMethods = Seq()\n\n  private val testedMethods = Seq(\n    \"addFeatureSupport\",\n    \"as\",\n    \"alias\",\n    \"clone\",\n    \"cloneAtTimestamp\",\n    \"cloneAtVersion\",\n    \"delete\",\n    \"detail\",\n    \"dropFeatureSupport\",\n    \"generate\",\n    \"history\",\n    \"merge\",\n    \"optimize\",\n    \"restoreToVersion\",\n    \"restoreToTimestamp\",\n    \"toDF\",\n    \"update\",\n    \"updateExpr\",\n    \"upgradeTableProtocol\",\n    \"vacuum\"\n  )\n\n  val untestedMethods = publicMethods -- ignoreMethods -- testedMethods\n  assert(\n    untestedMethods.isEmpty,\n    s\"Found new methods added to DeltaTable: $untestedMethods. \" +\n      \"Please make sure you add a new test to verify it works with file system \" +\n      \"options in this file, and update the `testedMethods` list. \" +\n      \"If this new method doesn't need to support file system options, \" +\n      \"you can add it to the `ignoredMethods` list\")\n\n  test(\"forPath: as/alias/toDF with filesystem options.\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n      testData.write.options(fsOptions).format(\"delta\").save(path)\n\n      checkAnswer(\n        DeltaTable.forPath(spark, path, fsOptions).as(\"tbl\").toDF.select(\"tbl.value\"),\n        testData.select(\"value\").collect().toSeq)\n\n      checkAnswer(\n        DeltaTable.forPath(spark, path, fsOptions).alias(\"tbl\").toDF.select(\"tbl.value\"),\n        testData.select(\"value\").collect().toSeq)\n    }\n  }\n\n  test(\"forPath with unsupported options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n      testData.write.options(fsOptions).format(\"delta\").save(path)\n\n      val finalOptions = fsOptions + (\"otherKey\" -> \"otherVal\")\n      assertThrows[DeltaIllegalArgumentException] {\n        io.delta.tables.DeltaTable.forPath(spark, path, finalOptions)\n      }\n    }\n  }\n\n  test(\"forPath error out without filesystem options passed in.\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n      testData.write.options(fsOptions).format(\"delta\").save(path)\n\n      val e = intercept[UnsupportedFileSystemException] {\n        io.delta.tables.DeltaTable.forPath(spark, path)\n      }.getMessage\n\n      assert(e.contains(\"\"\"No FileSystem for scheme \"fake\"\"\"\"))\n    }\n  }\n\n  test(\"forPath - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n      testData.write.options(fsOptions).format(\"delta\").save(path)\n\n      val deltaTable =\n        io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n\n      val testDataSeq = testData.collect().toSeq\n\n      // verify table can be read\n      checkAnswer(deltaTable.toDF, testDataSeq)\n\n      // verify java friendly API.\n      import scala.collection.JavaConverters._\n      val deltaTable2 = io.delta.tables.DeltaTable.forPath(\n        spark, path, new java.util.HashMap[String, String](fsOptions.asJava))\n      checkAnswer(deltaTable2.toDF, testDataSeq)\n    }\n  }\n\n  test(\"updateExpr - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n      val df = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n      df.write.options(fsOptions).format(\"delta\").save(path)\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n\n      table.updateExpr(Map(\"key\" -> \"100\"))\n\n      checkAnswer(readDeltaTableByPath(path),\n        Row(100, 10) :: Row(100, 20) :: Row(100, 30) :: Row(100, 40) :: Nil)\n    }\n  }\n\n  test(\"update - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val df = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n      df.write.options(fakeFileSystemOptions).format(\"delta\").save(path)\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path, fakeFileSystemOptions)\n\n      table.update(Map(\"key\" -> functions.expr(\"100\")))\n\n      checkAnswer(readDeltaTableByPath(path),\n        Row(100, 10) :: Row(100, 20) :: Row(100, 30) :: Row(100, 40) :: Nil)\n    }\n  }\n\n  test(\"delete - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val df = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n      df.write.options(fakeFileSystemOptions).format(\"delta\").save(path)\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path, fakeFileSystemOptions)\n\n      table.delete(functions.expr(\"key = 1 or key = 2\"))\n\n      checkAnswer(readDeltaTableByPath(path), Row(3, 30) :: Row(4, 40) :: Nil)\n    }\n  }\n\n  test(\"merge - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val target = Seq((1, 10), (2, 20)).toDF(\"key1\", \"value1\")\n      target.write.options(fakeFileSystemOptions).format(\"delta\").save(path)\n      val source = Seq((1, 100), (3, 30)).toDF(\"key2\", \"value2\")\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path, fakeFileSystemOptions)\n\n      table.merge(source, \"key1 = key2\")\n        .whenMatched().updateExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n        .whenNotMatched().insertExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n        .execute()\n\n      checkAnswer(readDeltaTableByPath(path), Row(1, 100) :: Row(2, 20) :: Row(3, 30) :: Nil)\n    }\n  }\n\n  test(\"vacuum - with filesystem options\") {\n    // Note: verify that [DeltaTableUtils.findDeltaTableRoot] works when either\n    // DELTA_FORMAT_CHECK_CACHE_ENABLED is on or off.\n    Seq(\"true\", \"false\").foreach{ deltaFormatCheckEnabled =>\n      withSQLConf(\n        \"spark.databricks.delta.formatCheck.cache.enabled\" -> deltaFormatCheckEnabled) {\n        withTempDir { dir =>\n          val path = fakeFileSystemPath(dir)\n          testData.write.options(fakeFileSystemOptions).format(\"delta\").save(path)\n          val table = io.delta.tables.DeltaTable.forPath(spark, path, fakeFileSystemOptions)\n\n          // create a uncommitted file.\n          val notCommittedFile = \"notCommittedFile.json\"\n          val file = new File(dir, notCommittedFile)\n          FileUtils.write(file, \"gibberish\")\n          // set to ancient time so that the file is eligible to be vacuumed.\n          file.setLastModified(0)\n          assert(file.exists())\n\n          table.vacuum()\n\n          val file2 = new File(dir, notCommittedFile)\n          assert(!file2.exists())\n        }\n      }\n    }\n  }\n\n  test(\"clone - with filesystem options\") {\n    withTempDir { dir =>\n      val baseDir = fakeFileSystemPath(dir)\n\n      val srcDir = new File(baseDir, \"source\").getCanonicalPath\n      val dstDir = new File(baseDir, \"destination\").getCanonicalPath\n\n      spark.range(10).write.options(fakeFileSystemOptions).format(\"delta\").save(srcDir)\n\n      val srcTable =\n        io.delta.tables.DeltaTable.forPath(spark, srcDir, fakeFileSystemOptions)\n      srcTable.clone(dstDir, isShallow = true)\n\n      val srcLog = DeltaLog.forTable(spark, new Path(srcDir), fakeFileSystemOptions)\n      val dstLog = DeltaLog.forTable(spark, new Path(dstDir), fakeFileSystemOptions)\n\n      checkAnswer(\n        spark.baseRelationToDataFrame(srcLog.createRelation()),\n        spark.baseRelationToDataFrame(dstLog.createRelation())\n      )\n    }\n  }\n\n  test(\"cloneAtVersion/timestamp - with filesystem options\") {\n    Seq(true, false).foreach { cloneWithVersion =>\n      withTempDir { dir =>\n        val baseDir = fakeFileSystemPath(dir)\n        val fsOptions = fakeFileSystemOptions\n\n        val srcDir = new File(baseDir, \"source\").getCanonicalPath\n        val dstDir = new File(baseDir, \"destination\").getCanonicalPath\n\n        val df1 = Seq(1, 2, 3).toDF(\"id\")\n        val df2 = Seq(4, 5).toDF(\"id\")\n        val df3 = Seq(6, 7).toDF(\"id\")\n\n        // version 0.\n        df1.write.format(\"delta\").options(fsOptions).save(srcDir)\n\n        // version 1.\n        df2.write.format(\"delta\").options(fsOptions).mode(\"append\").save(srcDir)\n\n        // version 2.\n        df3.write.format(\"delta\").options(fsOptions).mode(\"append\").save(srcDir)\n\n        val srcTable =\n          io.delta.tables.DeltaTable.forPath(spark, srcDir, fakeFileSystemOptions)\n\n        if (cloneWithVersion) {\n          srcTable.cloneAtVersion(0, dstDir, isShallow = true)\n        } else {\n          // clone with timestamp.\n          //\n          // set the time to first file with a early time and verify the delta table can be\n          // restored to it.\n          val desiredTime = new Timestamp(System.currentTimeMillis() - 5.days.toMillis)\n\n          val logPath = new Path(srcDir, \"_delta_log\")\n          val file = new File(FileNames.unsafeDeltaFile(logPath, 0).toString)\n          assert(file.setLastModified(desiredTime.getTime))\n          srcTable.cloneAtTimestamp(desiredTime.toString, dstDir, isShallow = true)\n        }\n\n        val dstLog = DeltaLog.forTable(spark, new Path(dstDir), fakeFileSystemOptions)\n\n        checkAnswer(\n          df1,\n          spark.baseRelationToDataFrame(dstLog.createRelation())\n        )\n      }\n    }\n  }\n\n  test(\"optimize - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n\n      Seq(1, 2, 3).toDF().write.options(fsOptions).format(\"delta\").save(path)\n      Seq(4, 5, 6)\n        .toDF().write.options(fsOptions).format(\"delta\").mode(\"append\").save(path)\n\n      val origData: DataFrame = spark.read.options(fsOptions).format(\"delta\").load(path)\n\n      val deltaLog = DeltaLog.forTable(spark, new Path(path), fsOptions)\n      val table = io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n      val versionBeforeOptimize = deltaLog.snapshot.version\n\n      table.optimize().executeCompaction()\n      deltaLog.update()\n      assert(deltaLog.snapshot.version == versionBeforeOptimize + 1)\n      checkDatasetUnorderly(origData.as[Int], 1, 2, 3, 4, 5, 6)\n    }\n  }\n\n  test(\"history - with filesystem options\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n\n      Seq(1, 2, 3).toDF().write.options(fsOptions).format(\"delta\").save(path)\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n      table.history().collect()\n    }\n  }\n\n  test(\"generate - with filesystem options\") {\n    withSQLConf(\"spark.databricks.delta.symlinkFormatManifest.fileSystemCheck.enabled\" -> \"false\") {\n      withTempDir { dir =>\n        val path = fakeFileSystemPath(dir)\n        val fsOptions = fakeFileSystemOptions\n\n        Seq(1, 2, 3).toDF().write.options(fsOptions).format(\"delta\").save(path)\n\n        val table = io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n        table.generate(\"symlink_format_manifest\")\n      }\n    }\n  }\n\n  test(\"restoreTable - with filesystem options\") {\n    withSQLConf(\"spark.databricks.service.checkSerialization\" -> \"false\") {\n      withTempDir { dir =>\n        val path = fakeFileSystemPath(dir)\n        val fsOptions = fakeFileSystemOptions\n\n        val df1 = Seq(1, 2, 3).toDF(\"id\")\n        val df2 = Seq(4, 5).toDF(\"id\")\n        val df3 = Seq(6, 7).toDF(\"id\")\n\n        // version 0.\n        df1.write.format(\"delta\").options(fsOptions).save(path)\n        val deltaLog = DeltaLog.forTable(spark, new Path(path), fsOptions)\n        assert(deltaLog.snapshot.version == 0)\n\n        // version 1.\n        df2.write.format(\"delta\").options(fsOptions).mode(\"append\").save(path)\n        deltaLog.update()\n        assert(deltaLog.snapshot.version == 1)\n\n        // version 2.\n        df3.write.format(\"delta\").options(fsOptions).mode(\"append\").save(path)\n        deltaLog.update()\n        assert(deltaLog.snapshot.version == 2)\n\n        checkAnswer(\n          spark.read.format(\"delta\").options(fsOptions).load(path),\n          df1.union(df2).union(df3))\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n        deltaTable.restoreToVersion(1)\n\n        checkAnswer(\n          spark.read.format(\"delta\").options(fsOptions).load(path),\n          df1.union(df2)\n        )\n\n        // set the time to first file with a early time and verify the delta table can be restored\n        // to it.\n        val desiredTime = new Timestamp(System.currentTimeMillis() - 5.days.toMillis)\n\n        val logPath = new Path(dir.getCanonicalPath, \"_delta_log\")\n        val file = new File(FileNames.unsafeDeltaFile(logPath, 0).toString)\n        assert(file.setLastModified(desiredTime.getTime))\n\n        val deltaTable2 = io.delta.tables.DeltaTable.forPath(spark, path, fsOptions)\n        deltaTable2.restoreToTimestamp(desiredTime.toString)\n\n        checkAnswer(\n          spark.read.format(\"delta\").options(fsOptions).load(path),\n          df1\n        )\n      }\n    }\n  }\n\n  test(\"upgradeTableProtocol - with filesystem options.\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n\n      // create a table with a default Protocol.\n      val testSchema = spark.range(1).schema\n      val log = DeltaLog.forTable(spark, new Path(path), fsOptions)\n      log.createLogDirectoriesIfNotExists()\n      log.store.write(\n        FileNames.unsafeDeltaFile(log.logPath, 0),\n        Iterator(Metadata(schemaString = testSchema.json).json, Protocol(0, 0).json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n      log.update()\n\n      // update the protocol.\n      val table = DeltaTable.forPath(spark, path, fsOptions)\n      table.upgradeTableProtocol(1, 2)\n\n      val expectedProtocol = Protocol(1, 2)\n      assert(log.snapshot.protocol === expectedProtocol)\n    }\n  }\n\n  test(\"addFeatureSupport - with filesystem options.\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n\n      // create a table with a default Protocol.\n      val testSchema = spark.range(1).schema\n      val log = DeltaLog.forTable(spark, new Path(path), fsOptions)\n      log.createLogDirectoriesIfNotExists()\n      log.store.write(\n        FileNames.unsafeDeltaFile(log.logPath, 0),\n        Iterator(Metadata(schemaString = testSchema.json).json, Protocol(1, 2).json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n      log.update()\n\n      // update the protocol to support a writer feature.\n      val table = DeltaTable.forPath(spark, path, fsOptions)\n      table.addFeatureSupport(TestWriterFeature.name)\n      assert(log.update().protocol === Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        TestWriterFeature)))\n      table.addFeatureSupport(TestReaderWriterFeature.name)\n      assert(\n        log.update().protocol === Protocol(3, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          TestWriterFeature,\n          TestReaderWriterFeature)))\n\n      // update the protocol again with invalid feature name.\n      assert(intercept[DeltaTableFeatureException] {\n        table.addFeatureSupport(\"__invalid_feature__\")\n      }.getErrorClass === \"DELTA_UNSUPPORTED_FEATURES_IN_CONFIG\")\n    }\n  }\n\n  test(\"dropFeatureSupport - with filesystem options.\") {\n    withTempDir { dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n\n      // create a table with a default Protocol.\n      val testSchema = spark.range(1).schema\n      val log = DeltaLog.forTable(spark, new Path(path), fsOptions)\n      log.createLogDirectoriesIfNotExists()\n      log.store.write(\n        FileNames.unsafeDeltaFile(log.logPath, 0),\n        Iterator(Metadata(schemaString = testSchema.json).json, Protocol(1, 2).json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n      log.update()\n\n      // update the protocol to support a writer feature.\n      val table = DeltaTable.forPath(spark, path, fsOptions)\n      table.addFeatureSupport(TestRemovableWriterFeature.name)\n      assert(log.update().protocol === Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        TestRemovableWriterFeature)))\n\n      // Attempt truncating the history when dropping a feature that is not required.\n      // This verifies the truncateHistory option was correctly passed.\n      assert(intercept[DeltaTableFeatureException] {\n        table.dropFeatureSupport(\"testRemovableWriter\", truncateHistory = true)\n      }.getErrorClass === \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\")\n\n      // Drop feature.\n      table.dropFeatureSupport(TestRemovableWriterFeature.name)\n      // After dropping the feature we should return back to the original protocol.\n      assert(log.update().protocol === Protocol(1, 2))\n\n      table.addFeatureSupport(TestRemovableReaderWriterFeature.name)\n      assert(\n        log.update().protocol === Protocol(3, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          TestRemovableReaderWriterFeature)))\n\n      // Drop feature.\n      table.dropFeatureSupport(TestRemovableReaderWriterFeature.name)\n      // After dropping the feature we should return back to the original protocol.\n      assert(log.update().protocol === Protocol(1, 2))\n\n      // Try to drop an unsupported feature.\n      assert(intercept[DeltaTableFeatureException] {\n        table.dropFeatureSupport(\"__invalid_feature__\")\n      }.getErrorClass === \"DELTA_FEATURE_DROP_UNSUPPORTED_CLIENT_FEATURE\")\n\n      // Try to drop a feature that is not present in the protocol.\n      assert(intercept[DeltaTableFeatureException] {\n        table.dropFeatureSupport(TestRemovableReaderWriterFeature.name)\n      }.getErrorClass === \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\")\n\n      table.addFeatureSupport(TestReaderWriterFeature.name)\n\n      // Try to drop a non-removable feature.\n      assert(intercept[DeltaTableFeatureException] {\n        table.dropFeatureSupport(TestReaderWriterFeature.name)\n      }.getErrorClass === \"DELTA_FEATURE_DROP_NONREMOVABLE_FEATURE\")\n    }\n  }\n\n  test(\"details - with filesystem options.\") {\n    withTempDir{ dir =>\n      val path = fakeFileSystemPath(dir)\n      val fsOptions = fakeFileSystemOptions\n      Seq(1, 2, 3).toDF().write.format(\"delta\").options(fsOptions).save(path)\n\n      val deltaTable = DeltaTable.forPath(spark, path, fsOptions)\n      checkAnswer(\n        deltaTable.detail().select(\"format\"),\n        Seq(Row(\"delta\"))\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/io/delta/tables/DeltaTableTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\n\nimport org.apache.spark.sql.DataFrame\n\nobject DeltaTableTestUtils {\n\n  /** A utility method to access the private constructor of [[DeltaTable]] in tests. */\n  def createTable(df: DataFrame, deltaLog: DeltaLog): DeltaTable = {\n    new DeltaTable(df, DeltaTableV2(df.sparkSession, deltaLog.dataPath))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ActionSerializerSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{QueryTest, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\n\n// scalastyle:off: removeFile\nclass ActionSerializerSuite extends QueryTest with SharedSparkSession with DeltaSQLCommandTest {\n\n  roundTripCompare(\"Add\",\n    AddFile(\"test\", Map.empty, 1, 1, dataChange = true))\n  roundTripCompare(\"Add with partitions\",\n    AddFile(\"test\", Map(\"a\" -> \"1\"), 1, 1, dataChange = true))\n  roundTripCompare(\"Add with stats\",\n    AddFile(\"test\", Map.empty, 1, 1, dataChange = true, stats = \"stats\"))\n  roundTripCompare(\"Add with tags\",\n    AddFile(\"test\", Map.empty, 1, 1, dataChange = true, tags = Map(\"a\" -> \"1\")))\n  roundTripCompare(\"Add with empty tags\",\n    AddFile(\"test\", Map.empty, 1, 1, dataChange = true, tags = Map.empty))\n\n  roundTripCompare(\"Remove\",\n    RemoveFile(\"test\", Some(2)))\n\n  test(\"AddFile tags\") {\n    val action1 =\n      AddFile(\n        path = \"a\",\n        partitionValues = Map.empty,\n        size = 1,\n        modificationTime = 2,\n        dataChange = false,\n        stats = null,\n        tags = Map(\"key1\" -> \"val1\", \"key2\" -> \"val2\"))\n    val json1 =\n      \"\"\"{\n        |  \"add\": {\n        |    \"path\": \"a\",\n        |    \"partitionValues\": {},\n        |    \"size\": 1,\n        |    \"modificationTime\": 2,\n        |    \"dataChange\": false,\n        |    \"tags\": {\n        |      \"key1\": \"val1\",\n        |      \"key2\": \"val2\"\n        |    }\n        |  }\n        |}\"\"\".stripMargin\n    assert(action1 === Action.fromJson(json1))\n    assert(action1.json === json1.replaceAll(\"\\\\s\", \"\"))\n\n    val json2 =\n      \"\"\"{\n        |  \"add\": {\n        |    \"path\": \"a\",\n        |    \"partitionValues\": {},\n        |    \"size\": 1,\n        |    \"modificationTime\": 2,\n        |    \"dataChange\": false,\n        |    \"tags\": {}\n        |  }\n        |}\"\"\".stripMargin\n    val action2 =\n      AddFile(\n        path = \"a\",\n        partitionValues = Map.empty,\n        size = 1,\n        modificationTime = 2,\n        dataChange = false,\n        stats = null,\n        tags = Map.empty)\n    assert(action2 === Action.fromJson(json2))\n    assert(action2.json === json2.replaceAll(\"\\\\s\", \"\"))\n  }\n\n  // This is the same test as \"removefile\" in OSS, but due to a Jackson library upgrade the behavior\n  // has diverged between Spark 3.1 and Spark 3.2.\n  // We don't believe this is a practical issue because all extant versions of Delta explicitly\n  // write the dataChange field.\n  test(\"remove file deserialization\") {\n    val removeJson = RemoveFile(\"a\", Some(2L)).json\n    assert(removeJson.contains(\"\"\"\"deletionTimestamp\":2\"\"\"))\n    assert(!removeJson.contains(\"\"\"delTimestamp\"\"\"))\n    val json1 = \"\"\"{\"remove\":{\"path\":\"a\",\"deletionTimestamp\":2,\"dataChange\":true}}\"\"\"\n    val json2 = \"\"\"{\"remove\":{\"path\":\"a\",\"dataChange\":false}}\"\"\"\n    val json4 = \"\"\"{\"remove\":{\"path\":\"a\",\"deletionTimestamp\":5}}\"\"\"\n    assert(Action.fromJson(json1) === RemoveFile(\"a\", Some(2L), dataChange = true))\n    assert(Action.fromJson(json2) === RemoveFile(\"a\", None, dataChange = false))\n    assert(Action.fromJson(json4) === RemoveFile(\"a\", Some(5L), dataChange = true))\n  }\n\n  roundTripCompare(\"SetTransaction\",\n    SetTransaction(\"a\", 1, Some(1234L)))\n\n  roundTripCompare(\"SetTransaction without lastUpdated\",\n    SetTransaction(\"a\", 1, None))\n\n  roundTripCompare(\"MetaData\",\n    Metadata(\n      \"id\",\n      \"table\",\n      \"testing\",\n      Format(\"parquet\", Map.empty),\n      new StructType().json,\n      Seq(\"a\")))\n\n  test(\"extra fields\") {\n    // TODO reading from checkpoint\n    Action.fromJson(\"\"\"{\"txn\": {\"test\": 1}}\"\"\")\n  }\n\n  test(\"deserialization of CommitInfo without tags\") {\n    val expectedCommitInfo = CommitInfo(\n      time = 123L,\n      operation = \"CONVERT\",\n      inCommitTimestamp = Some(123L),\n      operationParameters = Map.empty,\n      commandContext = Map.empty,\n      readVersion = Some(23),\n      isolationLevel = Some(\"SnapshotIsolation\"),\n      isBlindAppend = Some(true),\n      operationMetrics = Some(Map(\"m1\" -> \"v1\", \"m2\" -> \"v2\")),\n      userMetadata = Some(\"123\"),\n      tags = None,\n      txnId = None).copy(engineInfo = None)\n\n    // json of commit info actions without tag or engineInfo field\n    val json1 =\n      \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123,\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n        \"\"\"\"operationParameters\":{},\"readVersion\":23,\"\"\" +\n        \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n        \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\"}}\"\"\".stripMargin\n    assert(Action.fromJson(json1) === expectedCommitInfo)\n  }\n\n  test(\"deserialization of CommitInfo without commitTime\") {\n    val expectedCommitInfo = CommitInfo(\n      time = 123L,\n      operation = \"CONVERT\",\n      operationParameters = Map.empty,\n      commandContext = Map.empty,\n      readVersion = Some(23),\n      isolationLevel = Some(\"SnapshotIsolation\"),\n      isBlindAppend = Some(true),\n      operationMetrics = Some(Map(\"m1\" -> \"v1\", \"m2\" -> \"v2\")),\n      userMetadata = Some(\"123\"),\n      tags = None,\n      txnId = None).copy(engineInfo = None)\n\n    // json of commit info actions without tag or engineInfo field\n    val json1 =\n      \"\"\"{\"commitInfo\":{\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n        \"\"\"\"operationParameters\":{},\"readVersion\":23,\"\"\" +\n        \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n        \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\"}}\"\"\".stripMargin\n    assert(Action.fromJson(json1) === expectedCommitInfo)\n  }\n\n  test(\"deserialization of CommitInfo with a very small ICT\") {\n    val json1 =\n      \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123,\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n        \"\"\"\"operationParameters\":{},\"readVersion\":23,\"\"\" +\n        \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n        \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\"}}\"\"\".stripMargin\n    assert(Action.fromJson(json1).asInstanceOf[CommitInfo].inCommitTimestamp.get == 123L)\n  }\n\n  test(\"deserialization of CommitInfo with a very large ICT\") {\n    val json1 =\n      \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123333333,\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n        \"\"\"\"operationParameters\":{},\"readVersion\":23,\"\"\" +\n        \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n        \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\"}}\"\"\".stripMargin\n    assert(Action.fromJson(json1).asInstanceOf[CommitInfo].inCommitTimestamp.get == 123333333L)\n  }\n\n  test(\"deserialization of CommitInfo with missing ICT\") {\n    val json1 =\n      \"\"\"{\"commitInfo\":{\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n        \"\"\"\"operationParameters\":{},\"readVersion\":23,\"\"\" +\n        \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n        \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\"}}\"\"\".stripMargin\n    val ictOpt: Option[Long] = Action.fromJson(json1).asInstanceOf[CommitInfo].inCommitTimestamp\n    assert(ictOpt.isEmpty)\n  }\n\n  test(\"round trip of operation parameters: primitive types in operation parameters\") {\n    val rawOperationParameters: Map[String, Any] = Map(\n      \"catalogTable\" -> \"t1\",\n      \"numFiles\" -> 23L,\n      \"partitionedBy\" -> JsonUtils.toJson(Seq(\"a\", false)),\n      \"sourceFormat\" -> \"parquet\",\n      \"collectStats\" -> false,\n      \"k1\" -> null,\n      \"\" -> null)\n    val operationParameters = rawOperationParameters.mapValues(JsonUtils.toJson(_)).toMap\n\n    val expectedOperationParameters = Map(\n      \"catalogTable\" -> \"\\\"t1\\\"\",\n      \"numFiles\" -> \"23\",\n      \"partitionedBy\" -> \"\\\"[\\\\\\\"a\\\\\\\",false]\\\"\",\n      \"sourceFormat\" -> \"\\\"parquet\\\"\",\n      \"collectStats\" -> \"false\",\n      \"k1\" -> \"null\",\n      \"\" -> \"null\")\n\n    assert(operationParameters === expectedOperationParameters)\n\n    val expectedLegacyOperationParameters = Map(\n      \"catalogTable\" -> \"t1\",\n      \"numFiles\" -> \"23\",\n      \"partitionedBy\" -> \"[\\\"a\\\",false]\",\n      \"sourceFormat\" -> \"parquet\",\n      \"collectStats\" -> \"false\",\n      \"k1\" -> null,\n      \"\" -> null)\n\n    val commitInfo = CommitInfo.empty().withTimestamp(1)\n      .copy(operationParameters = operationParameters)\n\n    // Try a couple rounds of round trips.\n    val roundTrippedCommitInfo1 = JsonUtils.fromJson[CommitInfo](JsonUtils.toJson(commitInfo))\n    assert(roundTrippedCommitInfo1.operationParameters === expectedOperationParameters)\n    assert(CommitInfo.getLegacyPostDeserializationOperationParameters(\n      roundTrippedCommitInfo1.operationParameters) === expectedLegacyOperationParameters)\n\n    val roundTrippedCommitInfo2 =\n      JsonUtils.fromJson[CommitInfo](JsonUtils.toJson(roundTrippedCommitInfo1))\n    assert(roundTrippedCommitInfo2.operationParameters === expectedOperationParameters)\n    assert(CommitInfo.getLegacyPostDeserializationOperationParameters(\n      roundTrippedCommitInfo2.operationParameters) === expectedLegacyOperationParameters)\n  }\n\n  test(\"round trip of operation parameters: non-primitive types in operation parameters\") {\n    val rawOperationParameters: Map[String, Any] = Map(\n      \"k1\" -> Seq(1, 2),\n      \"k2\" -> Map(\"a\" -> \"x\", \"b\" -> 1, \"c\" -> TestObject(\"f1\", -1, None), \"d\" -> Seq(3, \"e\")),\n      \"k3\" -> Seq.empty,\n      \"k4\" -> TestObject(\"field1\", 99, Some(Seq(\"v1\", \"v2\"))))\n    val operationParameters = rawOperationParameters.mapValues(JsonUtils.toJson(_)).toMap\n\n    val expectedOperationParameters = Map(\n      \"k1\" -> \"[1,2]\",\n      \"k2\" -> \"{\\\"a\\\":\\\"x\\\",\\\"b\\\":1,\\\"c\\\":{\\\"field1\\\":\\\"f1\\\",\\\"field2\\\":-1},\\\"d\\\":[3,\\\"e\\\"]}\",\n      \"k3\" -> \"[]\",\n      \"k4\" -> \"{\\\"field1\\\":\\\"field1\\\",\\\"field2\\\":99,\\\"field3\\\":[\\\"v1\\\",\\\"v2\\\"]}\")\n\n    assert(operationParameters === expectedOperationParameters)\n\n    val expectedLegacyOperationParameters = Map(\n      \"k1\" -> \"[1,2]\",\n      \"k2\" -> \"{\\\"a\\\":\\\"x\\\",\\\"b\\\":1,\\\"c\\\":{\\\"field1\\\":\\\"f1\\\",\\\"field2\\\":-1},\\\"d\\\":[3,\\\"e\\\"]}\",\n      \"k3\" -> \"[]\",\n      \"k4\" -> \"{\\\"field1\\\":\\\"field1\\\",\\\"field2\\\":99,\\\"field3\\\":[\\\"v1\\\",\\\"v2\\\"]}\")\n\n    val commitInfo = CommitInfo.empty().withTimestamp(1)\n      .copy(operationParameters = operationParameters)\n\n    // Try a couple rounds of round trips.\n    val roundTrippedCommitInfo1 = JsonUtils.fromJson[CommitInfo](JsonUtils.toJson(commitInfo))\n    assert(roundTrippedCommitInfo1.operationParameters === expectedOperationParameters)\n    intercept[com.fasterxml.jackson.databind.exc.MismatchedInputException] {\n      // Non-primitive type values are not supported with legacy deserialization.\n      // Note that this is not a quirk specific to getLegacyPostDeserializationOperationParameters\n      // but rather a quirk of the legacy deserialization.\n      CommitInfo.getLegacyPostDeserializationOperationParameters(\n        roundTrippedCommitInfo1.operationParameters)\n    }\n\n    val roundTrippedCommitInfo2 =\n      JsonUtils.fromJson[CommitInfo](JsonUtils.toJson(roundTrippedCommitInfo1))\n    assert(roundTrippedCommitInfo2.operationParameters === expectedOperationParameters)\n  }\n\n  test(\"getLegacyPostDeserializationOperationParameters is same as reading operation parameters  \" +\n      \"without custom deserialize\") {\n    val rawOperationParameters: Map[String, Any] = Map(\n      \"catalogTable\" -> \"t1\",\n      \"numFiles\" -> 23L,\n      \"partitionedBy\" -> \"[\\\"a\\\",\\\"b\\\"]\",\n      \"sourceFormat\" -> \"parquet\",\n      \"collectStats\" -> false,\n      \"k1\" -> JsonUtils.toJson(Seq(1, 2)),\n      \"k2\" -> JsonUtils.toJson(Map(\"a\" -> \"x\", \"b\" -> 1, \"c\" -> Seq(3, \"e\"))),\n      \"k3\" -> null,\n      \"k4\" -> JsonUtils.toJson(Seq.empty),\n      \"\" -> null)\n    val operationParameters = rawOperationParameters.mapValues(JsonUtils.toJson(_)).toMap\n\n    val commitInfo = CommitInfo\n      .empty()\n      .withTimestamp(1)\n      .copy(operationParameters = operationParameters)\n\n    val testRawDeserialization = TestRawDeserialization(\n      operationParameters = commitInfo.operationParameters)\n    val expectedLegacyOperationParameters = JsonUtils.fromJson[TestRawDeserialization](\n      JsonUtils.toJson(testRawDeserialization)).operationParameters\n\n    // Try a couple rounds of round trips.\n    val roundTrippedCommitInfo1 = JsonUtils.fromJson[CommitInfo](JsonUtils.toJson(commitInfo))\n    assert(CommitInfo.getLegacyPostDeserializationOperationParameters(\n      roundTrippedCommitInfo1.operationParameters) === expectedLegacyOperationParameters)\n\n    val roundTrippedCommitInfo2 =\n      JsonUtils.fromJson[CommitInfo](JsonUtils.toJson(roundTrippedCommitInfo1))\n    assert(CommitInfo.getLegacyPostDeserializationOperationParameters(\n      roundTrippedCommitInfo2.operationParameters) === expectedLegacyOperationParameters)\n  }\n\n  testActionSerDe(\n    \"Protocol - json serialization/deserialization\",\n    Protocol(minReaderVersion = 1, minWriterVersion = 2),\n    expectedJson = \"\"\"{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\"\"\")\n\n  testActionSerDe(\n    \"Protocol - json serialization/deserialization with writer features\",\n    Protocol(minReaderVersion = 1, minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION)\n      .withFeature(AppendOnlyTableFeature),\n    expectedJson = \"\"\"{\"protocol\":{\"minReaderVersion\":1,\"\"\" +\n      s\"\"\"\"minWriterVersion\":$TABLE_FEATURES_MIN_WRITER_VERSION,\"\"\" +\n      \"\"\"\"writerFeatures\":[\"appendOnly\"]}}\"\"\")\n\n  testActionSerDe(\n    \"Protocol - json serialization/deserialization with reader and writer features\",\n    Protocol(\n      minReaderVersion = TABLE_FEATURES_MIN_READER_VERSION,\n      minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION)\n      .withFeature(TestLegacyReaderWriterFeature),\n    expectedJson =\n      s\"\"\"{\"protocol\":{\"minReaderVersion\":$TABLE_FEATURES_MIN_READER_VERSION,\"\"\" +\n        s\"\"\"\"minWriterVersion\":$TABLE_FEATURES_MIN_WRITER_VERSION,\"\"\" +\n        \"\"\"\"readerFeatures\":[\"testLegacyReaderWriter\"],\"\"\" +\n        \"\"\"\"writerFeatures\":[\"testLegacyReaderWriter\"]}}\"\"\")\n\n  testActionSerDe(\n    \"Protocol - json serialization/deserialization with several reader and writer features\",\n    Protocol(\n      minReaderVersion = TABLE_FEATURES_MIN_READER_VERSION,\n      minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION)\n      .withFeature(TestLegacyReaderWriterFeature)\n      .withFeature(TestReaderWriterFeature),\n    expectedJson =\n      s\"\"\"{\"protocol\":{\"minReaderVersion\":$TABLE_FEATURES_MIN_READER_VERSION,\"\"\" +\n        s\"\"\"\"minWriterVersion\":$TABLE_FEATURES_MIN_WRITER_VERSION,\"\"\" +\n        \"\"\"\"readerFeatures\":[\"testLegacyReaderWriter\",\"testReaderWriter\"],\"\"\" +\n        \"\"\"\"writerFeatures\":[\"testLegacyReaderWriter\",\"testReaderWriter\"]}}\"\"\")\n\n  testActionSerDe(\n    \"Protocol - json serialization/deserialization with empty reader and writer features\",\n    Protocol(\n      minReaderVersion = TABLE_FEATURES_MIN_READER_VERSION,\n      minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION),\n    expectedJson =\n      s\"\"\"{\"protocol\":{\"minReaderVersion\":$TABLE_FEATURES_MIN_READER_VERSION,\"\"\" +\n        s\"\"\"\"minWriterVersion\":$TABLE_FEATURES_MIN_WRITER_VERSION,\"\"\" +\n        \"\"\"\"readerFeatures\":[],\"writerFeatures\":[]}}\"\"\")\n\n  testActionSerDe(\n    \"SetTransaction (lastUpdated is None) - json serialization/deserialization\",\n    SetTransaction(appId = \"app-1\", version = 2L, lastUpdated = None),\n    expectedJson = \"\"\"{\"txn\":{\"appId\":\"app-1\",\"version\":2}}\"\"\".stripMargin)\n\n  testActionSerDe(\n    \"SetTransaction (lastUpdated is not None) - json serialization/deserialization\",\n    SetTransaction(appId = \"app-2\", version = 3L, lastUpdated = Some(4L)),\n    expectedJson = \"\"\"{\"txn\":{\"appId\":\"app-2\",\"version\":3,\"lastUpdated\":4}}\"\"\".stripMargin)\n\n  testActionSerDe(\n    \"AddFile (without tags) - json serialization/deserialization\",\n    AddFile(\"x=2/f1\", partitionValues = Map(\"x\" -> \"2\"),\n      size = 10, modificationTime = 1, dataChange = true, stats = \"{\\\"numRecords\\\": 2}\"),\n    expectedJson = \"\"\"{\"add\":{\"path\":\"x=2/f1\",\"partitionValues\":{\"x\":\"2\"},\"size\":10,\"\"\" +\n      \"\"\"\"modificationTime\":1,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\": 2}\"}}\"\"\".stripMargin)\n\n  testActionSerDe(\n    \"AddFile (with tags) - json serialization/deserialization\",\n    AddFile(\"part=p1/f1\", partitionValues = Map(\"x\" -> \"2\"), size = 10, modificationTime = 1,\n      dataChange = true, stats = \"{\\\"numRecords\\\": 2}\", tags = Map(\"TAG1\" -> \"23\")),\n    expectedJson = \"\"\"{\"add\":{\"path\":\"part=p1/f1\",\"partitionValues\":{\"x\":\"2\"},\"size\":10\"\"\" +\n      \"\"\",\"modificationTime\":1,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\": 2}\",\"\"\" +\n      \"\"\"\"tags\":{\"TAG1\":\"23\"}}}\"\"\"\n  )\n\n  testActionSerDe(\n    \"AddFile (with clusteringProvider) - json serialization/deserialization\",\n    AddFile(\n      \"clusteredFile.part\",\n      partitionValues = Map.empty[String, String],\n      size = 10,\n      modificationTime = 1,\n      dataChange = true,\n      stats = \"{\\\"numRecords\\\": 2}\",\n      tags = Map(\"TAG1\" -> \"23\"),\n      clusteringProvider = Some(\"liquid\")),\n    expectedJson =\n      \"\"\"{\"add\":{\"path\":\"clusteredFile.part\",\"partitionValues\":{},\"size\":10\"\"\" +\n        \"\"\",\"modificationTime\":1,\"dataChange\":true,\"stats\":\"{\\\"numRecords\\\": 2}\",\"\"\" +\n        \"\"\"\"tags\":{\"TAG1\":\"23\"}\"\"\" +\n        \"\"\",\"clusteringProvider\":\"liquid\"}}\"\"\")\n\n  testActionSerDe(\n    \"RemoveFile (without tags) - json serialization/deserialization\",\n    AddFile(\"part=p1/f1\", partitionValues = Map(\"x\" -> \"2\"), size = 10, modificationTime = 1,\n      dataChange = true, stats = \"{\\\"numRecords\\\": 2}\").removeWithTimestamp(timestamp = 11),\n    expectedJson = \"\"\"{\"remove\":{\"path\":\"part=p1/f1\",\"deletionTimestamp\":11,\"dataChange\":true,\"\"\" +\n      \"\"\"\"extendedFileMetadata\":true,\"partitionValues\":{\"x\":\"2\"},\"size\":10,\"\"\" +\n      \"\"\"\"stats\":\"{\\\"numRecords\\\": 2}\"}}\"\"\")\n\n  testActionSerDe(\n    \"RemoveFile (without tags and stats) - json serialization/deserialization\",\n    AddFile(\"part=p1/f1\", partitionValues = Map(\"x\" -> \"2\"), size = 10, modificationTime = 1,\n        dataChange = true, stats = \"{\\\"numRecords\\\": 2}\")\n      .removeWithTimestamp(timestamp = 11)\n      .copy(stats = null),\n    expectedJson = \"\"\"{\"remove\":{\"path\":\"part=p1/f1\",\"deletionTimestamp\":11,\"dataChange\":true,\"\"\" +\n      \"\"\"\"extendedFileMetadata\":true,\"partitionValues\":{\"x\":\"2\"},\"size\":10}}\"\"\")\n\n  private def deletionVectorWithRelativePath: DeletionVectorDescriptor =\n    DeletionVectorDescriptor.onDiskWithRelativePath(\n      id = UUID.randomUUID(),\n      randomPrefix = \"a1\",\n      sizeInBytes = 10,\n      cardinality = 2,\n      offset = Some(10))\n\n  private def deletionVectorWithAbsolutePath: DeletionVectorDescriptor =\n    DeletionVectorDescriptor.onDiskWithAbsolutePath(\n      path = \"/test.dv\",\n      sizeInBytes = 10,\n      cardinality = 2,\n      offset = Some(10))\n\n  private def deletionVectorInline: DeletionVectorDescriptor =\n    DeletionVectorDescriptor.inlineInLog(Array(1, 2, 3, 4), 1)\n\n  roundTripCompare(\"Add with deletion vector - relative path\",\n    AddFile(\n      path = \"test\",\n      partitionValues = Map.empty,\n      size = 1,\n      modificationTime = 1,\n      dataChange = true,\n      tags = Map.empty,\n      deletionVector = deletionVectorWithRelativePath))\n  roundTripCompare(\"Add with deletion vector - absolute path\",\n    AddFile(\n      path = \"test\",\n      partitionValues = Map.empty,\n      size = 1,\n      modificationTime = 1,\n      dataChange = true,\n      tags = Map.empty,\n      deletionVector = deletionVectorWithAbsolutePath))\n  roundTripCompare(\"Add with deletion vector - inline\",\n    AddFile(\n      path = \"test\",\n      partitionValues = Map.empty,\n      size = 1,\n      modificationTime = 1,\n      dataChange = true,\n      tags = Map.empty,\n      deletionVector = deletionVectorInline))\n\n  roundTripCompare(\"Remove with deletion vector - relative path\",\n    RemoveFile(\n      path = \"test\",\n      deletionTimestamp = Some(1L),\n      extendedFileMetadata = Some(true),\n      partitionValues = Map.empty,\n      dataChange = true,\n      size = Some(1L),\n      tags = Map.empty,\n      deletionVector = deletionVectorWithRelativePath))\n  roundTripCompare(\"Remove with deletion vector - absolute path\",\n    RemoveFile(\n      path = \"test\",\n      deletionTimestamp = Some(1L),\n      extendedFileMetadata = Some(true),\n      partitionValues = Map.empty,\n      dataChange = true,\n      size = Some(1L),\n      tags = Map.empty,\n      deletionVector = deletionVectorWithAbsolutePath))\n  roundTripCompare(\"Remove with deletion vector - inline\",\n    RemoveFile(\n      path = \"test\",\n      deletionTimestamp = Some(1L),\n      extendedFileMetadata = Some(true),\n      partitionValues = Map.empty,\n      dataChange = true,\n      size = Some(1L),\n      tags = Map.empty,\n      deletionVector = deletionVectorInline))\n\n  // These make sure we don't accidentally serialise something we didn't mean to.\n  testActionSerDe(\n    name = \"AddFile (with deletion vector) - json serialization/deserialization\",\n    action = AddFile(\n      path = \"test\",\n      partitionValues = Map.empty,\n      size = 1,\n      modificationTime = 1,\n      dataChange = true,\n      stats = \"\"\"{\"numRecords\":3}\"\"\",\n      tags = Map.empty,\n      deletionVector = deletionVectorWithAbsolutePath),\n    expectedJson =\n      \"\"\"\n        |{\"add\":{\n        |\"path\":\"test\",\n        |\"partitionValues\":{},\n        |\"size\":1,\n        |\"modificationTime\":1,\n        |\"dataChange\":true,\n        |\"stats\":\"{\\\"numRecords\\\":3}\",\n        |\"tags\":{},\n        |\"deletionVector\":{\n        |\"storageType\":\"p\",\n        |\"pathOrInlineDv\":\"/test.dv\",\n        |\"offset\":10,\n        |\"sizeInBytes\":10,\n        |\"cardinality\":2}}\n        |}\"\"\".stripMargin.replaceAll(\"\\n\", \"\"),\n    extraSettings = Seq(\n      // Skip the table property check, so this write doesn't fail.\n      DeltaSQLConf.DELETION_VECTORS_COMMIT_CHECK_ENABLED.key -> \"false\")\n  )\n\n  test(\"DomainMetadata action - json serialization/deserialization\") {\n    val table = \"testTable\"\n    withTable(table) {\n      sql(\n        s\"\"\"\n           | CREATE TABLE $table(id int) USING delta\n           | tblproperties\n           | ('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n           |\"\"\".stripMargin)\n      val deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n      val deltaLog = deltaTable.deltaLog\n      val domainMetadatas = DomainMetadata(\n        domain = \"testDomain\",\n        configuration = JsonUtils.toJson(Map(\"key1\" -> \"value1\")),\n        removed = false) :: Nil\n      val version = deltaTable.startTransactionWithInitialSnapshot()\n        .commit(domainMetadatas, ManualUpdate)\n      val committedActions = deltaLog.store.read(\n        FileNames.unsafeDeltaFile(deltaLog.logPath, version),\n        deltaLog.newDeltaHadoopConf())\n      assert(committedActions.size == 2)\n      val serializedJson = committedActions.last\n      val expectedJson =\n        \"\"\"\n          |{\"domainMetadata\":{\n          |\"domain\":\"testDomain\",\n          |\"configuration\":\n          |\"{\\\"key1\\\":\\\"value1\\\"}\",\n          |\"removed\":false}\n          |}\"\"\".stripMargin.replaceAll(\"\\n\", \"\")\n      assert(serializedJson === expectedJson)\n      val asObject = Action.fromJson(serializedJson)\n      assert(domainMetadatas.head === asObject)\n    }\n  }\n\n  test(\"CheckpointMetadata - serialize/deserialize\") {\n    val m1 = CheckpointMetadata(version = 1, tags = null) // tags are null\n    val m2 = m1.copy(tags = Map()) // tags are empty\n    val m3 = m1.copy( // tags are non empty\n      tags = Map(\"k1\" -> \"v1\", \"schema\" -> \"\"\"{\"type\":\"struct\",\"fields\":[]}\"\"\")\n    )\n\n    assert(m1.json === \"\"\"{\"checkpointMetadata\":{\"version\":1}}\"\"\")\n    assert(m2.json === \"\"\"{\"checkpointMetadata\":{\"version\":1,\"tags\":{}}}\"\"\")\n    assert(m3.json ===\n      \"\"\"{\"checkpointMetadata\":{\"version\":1,\"\"\" +\n        \"\"\"\"tags\":{\"k1\":\"v1\",\"schema\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[]}\"}}}\"\"\")\n\n    Seq(m1, m2, m3).foreach { metadata =>\n      assert(metadata === JsonUtils.fromJson[SingleAction](metadata.json).unwrap)\n    }\n  }\n\n  test(\"SidecarFile - serialize/deserialize\") {\n    val f1 = // tags are null\n      SidecarFile(path = \"/t1/p1\", sizeInBytes = 1L, modificationTime = 3, tags = null)\n    val f2 = f1.copy(tags = Map()) // tags are empty\n    val f3 = f2.copy( // tags are non empty\n      tags = Map(\"k1\" -> \"v1\", \"schema\" -> \"\"\"{\"type\":\"struct\",\"fields\":[]}\"\"\")\n    )\n\n    assert(f1.json ===\n      \"\"\"{\"sidecar\":{\"path\":\"/t1/p1\",\"sizeInBytes\":1,\"modificationTime\":3}}\"\"\")\n    assert(f2.json ===\n      \"\"\"{\"sidecar\":{\"path\":\"/t1/p1\",\"sizeInBytes\":1,\"\"\" +\n        \"\"\"\"modificationTime\":3,\"tags\":{}}}\"\"\")\n    assert(f3.json ===\n      \"\"\"{\"sidecar\":{\"path\":\"/t1/p1\",\"sizeInBytes\":1,\"modificationTime\":3,\"\"\" +\n        \"\"\"\"tags\":{\"k1\":\"v1\",\"schema\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[]}\"}}}\"\"\".stripMargin)\n\n    Seq(f1, f2, f3).foreach { file =>\n      assert(file === JsonUtils.fromJson[SingleAction](file.json).unwrap)\n    }\n  }\n\n  testActionSerDe(\n    \"AddCDCFile (without tags) - json serialization/deserialization\",\n    AddCDCFile(\"part=p1/f1\", partitionValues = Map(\"x\" -> \"2\"), size = 10),\n    expectedJson = \"\"\"{\"cdc\":{\"path\":\"part=p1/f1\",\"partitionValues\":{\"x\":\"2\"},\"\"\" +\n      \"\"\"\"size\":10,\"dataChange\":false}}\"\"\".stripMargin)\n\n  testActionSerDe(\n    \"AddCDCFile (with tags) - json serialization/deserialization\",\n    AddCDCFile(\"part=p2/f1\", partitionValues = Map(\"x\" -> \"2\"),\n      size = 11, tags = Map(\"key1\" -> \"value1\")),\n    expectedJson = \"\"\"{\"cdc\":{\"path\":\"part=p2/f1\",\"partitionValues\":{\"x\":\"2\"},\"\"\" +\n      \"\"\"\"size\":11,\"tags\":{\"key1\":\"value1\"},\"dataChange\":false}}\"\"\".stripMargin)\n\n  testActionSerDe(\n    \"AddCDCFile (without null value in partitionValues) - json serialization/deserialization\",\n    AddCDCFile(\"part=p1/f1\", partitionValues = Map(\"x\" -> null), size = 10),\n    expectedJson = \"\"\"{\"cdc\":{\"path\":\"part=p1/f1\",\"partitionValues\":{\"x\":null},\"\"\" +\n      \"\"\"\"size\":10,\"dataChange\":false}}\"\"\".stripMargin)\n\n  {\n    val metadata = Metadata(id = \"testId\", createdTime = Some(2222))\n    testActionSerDe(\n      \"Metadata (with all defaults) - json serialization/deserialization\",\n      metadata,\n      expectedJson = \"\"\"{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"\"\" +\n        \"\"\"\"options\":{}},\"partitionColumns\":[],\"configuration\":{},\"createdTime\":2222}}\"\"\")\n    testActionSerDe(\n      \"Metadata (with all defaults and empty createdTime) - json serialization/deserialization\",\n      metadata.copy(createdTime = None),\n      expectedJson = \"\"\"{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"\"\" +\n        \"\"\"\"options\":{}},\"partitionColumns\":[],\"configuration\":{}}}\"\"\")\n  }\n\n  {\n    val schemaStr = new StructType().add(\"a\", \"long\").json\n    val metadata = Metadata(\n      id = \"testId\",\n      name = \"t1\",\n      description = \"desc\",\n      format = Format(provider = \"parquet\", options = Map(\"o1\" -> \"v1\")),\n      partitionColumns = Seq(\"a\"),\n      createdTime = Some(2222),\n      configuration = Map(\"delta.enableXyz\" -> \"true\"),\n      schemaString = schemaStr)\n    testActionSerDe(\n      \"Metadata - json serialization/deserialization\", metadata,\n      expectedJson = \"\"\"{\"metaData\":{\"id\":\"testId\",\"name\":\"t1\",\"description\":\"desc\",\"\"\" +\n        \"\"\"\"format\":{\"provider\":\"parquet\",\"options\":{\"o1\":\"v1\"}},\"\"\" +\n        s\"\"\"\"schemaString\":${JsonUtils.toJson(schemaStr)},\"partitionColumns\":[\"a\"],\"\"\" +\n        \"\"\"\"configuration\":{\"delta.enableXyz\":\"true\"},\"createdTime\":2222}}\"\"\".stripMargin)\n    testActionSerDe(\n      \"Metadata with empty createdTime- json serialization/deserialization\",\n      metadata.copy(createdTime = None),\n      expectedJson = \"\"\"{\"metaData\":{\"id\":\"testId\",\"name\":\"t1\",\"description\":\"desc\",\"\"\" +\n        \"\"\"\"format\":{\"provider\":\"parquet\",\"options\":{\"o1\":\"v1\"}},\"\"\" +\n        s\"\"\"\"schemaString\":${JsonUtils.toJson(schemaStr)},\"partitionColumns\":[\"a\"],\"\"\" +\n        \"\"\"\"configuration\":{\"delta.enableXyz\":\"true\"}}}\"\"\".stripMargin)\n  }\n\n  {\n    // Test for CommitInfo\n    val commitInfo = CommitInfo(\n      time = 123L,\n      operation = \"CONVERT\",\n      inCommitTimestamp = Some(123L),\n      operationParameters = Map.empty,\n      commandContext = Map(\"clusterId\" -> \"23\"),\n      readVersion = Some(23),\n      isolationLevel = Some(\"SnapshotIsolation\"),\n      isBlindAppend = Some(true),\n      operationMetrics = Some(Map(\"m1\" -> \"v1\", \"m2\" -> \"v2\")),\n      userMetadata = Some(\"123\"),\n      tags = Some(Map(\"k1\" -> \"v1\")),\n      txnId = Some(\"123\")\n    ).copy(engineInfo = None)\n\n    testActionSerDe(\n      \"CommitInfo (without operationParameters) - json serialization/deserialization\",\n      commitInfo,\n      expectedJson =\n        \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123,\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n          \"\"\"\"operationParameters\":{},\"clusterId\":\"23\",\"readVersion\":23,\"\"\" +\n          \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n          \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\",\"\"\" +\n          \"\"\"\"tags\":{\"k1\":\"v1\"},\"txnId\":\"123\"}}\"\"\".stripMargin)\n\n    test(\"CommitInfo (with operationParameters) - json serialization/deserialization\") {\n      val operation = DeltaOperations.Convert(\n        numFiles = 23L,\n        partitionBy = Seq(\"a\", \"b\"),\n        collectStats = false,\n        catalogTable = Some(\"t1\"),\n        sourceFormat = Some(\"parquet\"))\n      val commitInfo1 = commitInfo.copy(operationParameters = operation.jsonEncodedValues)\n      val expectedCommitInfoJson1 = // TODO JSON ordering differs between 2.12 and 2.13\n        if (scala.util.Properties.versionNumberString.startsWith(\"2.13\")) {\n          \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123,\"\"\" +\n            \"\"\"\"timestamp\":123,\"operation\":\"CONVERT\",\"operationParameters\"\"\"\" +\n            \"\"\":{\"catalogTable\":\"t1\",\"numFiles\":23,\"partitionedBy\":\"[\\\"a\\\",\\\"b\\\"]\",\"\"\" +\n            \"\"\"\"sourceFormat\":\"parquet\",\"collectStats\":false},\"clusterId\":\"23\",\"readVersion\"\"\"\" +\n            \"\"\":23,\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n            \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"\"\" +\n            \"\"\"\"userMetadata\":\"123\",\"tags\":{\"k1\":\"v1\"},\"txnId\":\"123\"}}\"\"\"\n        } else {\n          \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123,\"\"\" +\n            \"\"\"\"timestamp\":123,\"operation\":\"CONVERT\",\"operationParameters\"\"\"\" +\n            \"\"\":{\"catalogTable\":\"t1\",\"numFiles\":23,\"partitionedBy\":\"[\\\"a\\\",\\\"b\\\"]\",\"\"\" +\n            \"\"\"\"sourceFormat\":\"parquet\",\"collectStats\":false},\"clusterId\":\"23\",\"readVersion\"\"\" +\n            \"\"\"\":23,\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n            \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"\"\" +\n            \"\"\"\"userMetadata\":\"123\",\"tags\":{\"k1\":\"v1\"},\"txnId\":\"123\"}}\"\"\"\n        }\n      assert(commitInfo1.json == expectedCommitInfoJson1)\n      val newCommitInfo1 = Action.fromJson(expectedCommitInfoJson1).asInstanceOf[CommitInfo]\n      assert(newCommitInfo1 == commitInfo1)\n    }\n\n    testActionSerDe(\n      \"CommitInfo (with engineInfo) - json serialization/deserialization\",\n      commitInfo.copy(engineInfo = Some(\"Apache-Spark/3.1.1 Delta-Lake/10.1.0\")),\n      expectedJson =\n        \"\"\"{\"commitInfo\":{\"inCommitTimestamp\":123,\"timestamp\":123,\"operation\":\"CONVERT\",\"\"\" +\n          \"\"\"\"operationParameters\":{},\"clusterId\":\"23\",\"readVersion\":23,\"\"\" +\n          \"\"\"\"isolationLevel\":\"SnapshotIsolation\",\"isBlindAppend\":true,\"\"\" +\n          \"\"\"\"operationMetrics\":{\"m1\":\"v1\",\"m2\":\"v2\"},\"userMetadata\":\"123\",\"\"\" +\n          \"\"\"\"tags\":{\"k1\":\"v1\"},\"engineInfo\":\"Apache-Spark/3.1.1 Delta-Lake/10.1.0\",\"\"\" +\n          \"\"\"\"txnId\":\"123\"}}\"\"\".stripMargin)\n  }\n\n  test(\"CommitInfo operationParameters deserialization with primitive types\") {\n    // Test edge cases described in JsonMapDeserializer for primitive values\n    // This test verifies that the custom deserializer correctly handles mixed primitive types\n    // in operationParameters: strings, numbers, booleans, and null values\n    val operationParameters = Map(\n      \"stringValue\" -> \"\\\"simpleString\\\"\",\n      \"numberValue\" -> \"123\",\n      \"booleanValue\" -> \"true\",\n      \"floatValue\" -> \"45.67\",\n      \"jsonEncodedString\" -> \"\\\"\\\\\\\"quoted string\\\\\\\"\\\"\"\n    )\n\n    val commitInfo = CommitInfo(\n      time = 1234567890L,\n      operation = \"WRITE\",\n      operationParameters = operationParameters,\n      commandContext = Map(\"clusterId\" -> \"test-cluster\"),\n      readVersion = Some(5),\n      isolationLevel = Some(\"WriteSerializable\"),\n      isBlindAppend = Some(false),\n      operationMetrics = Some(Map(\"numFiles\" -> \"10\")),\n      userMetadata = Some(\"test metadata\"),\n      tags = Some(Map(\"source\" -> \"test\")),\n      txnId = Some(\"txn-123\")\n    )\n\n    // Serialize and deserialize to test the JsonMapDeserializer\n    val serialized = commitInfo.json\n    val deserialized = Action.fromJson(serialized).asInstanceOf[CommitInfo]\n\n    // Verify that operationParameters are correctly preserved after round-trip\n    assert(deserialized.operationParameters == operationParameters)\n    assert(deserialized.operation == \"WRITE\")\n    assert(deserialized.readVersion == Some(5))\n\n    // Test that getLegacyPostDeserializationOperationParameters works correctly\n    val legacyParams = CommitInfo.getLegacyPostDeserializationOperationParameters(\n      deserialized.operationParameters)\n    assert(legacyParams.nonEmpty)\n  }\n\n  test(\"CommitInfo operationParameters deserialization with complex operation\") {\n    // Test with actual DeltaOperation parameters to verify real-world usage\n    // This focuses on the edge case where operation parameters contain JSON-encoded values\n    val operation = DeltaOperations.Write(\n      mode = SaveMode.Append,\n      partitionBy = Some(Seq(\"year\", \"month\")),\n      predicate = Some(\"id > 100\"),\n      userMetadata = Some(\"batch write operation\")\n    )\n\n    val commitInfo = CommitInfo(\n      time = 9876543210L,\n      operation = operation.name,\n      operationParameters = operation.jsonEncodedValues,\n      commandContext = Map(\"clusterId\" -> \"prod-cluster\"),\n      readVersion = Some(42),\n      isolationLevel = Some(\"WriteSerializable\"),\n      isBlindAppend = Some(true),\n      operationMetrics = Some(Map(\"numFiles\" -> \"25\", \"numOutputRows\" -> \"1000\")),\n      userMetadata = operation.userMetadata,\n      tags = Some(Map(\"environment\" -> \"production\", \"team\" -> \"data-eng\")),\n      txnId = Some(\"txn-write-456\")\n    )\n\n    // Test round-trip serialization/deserialization\n    val serialized = commitInfo.json\n    val deserialized = Action.fromJson(serialized).asInstanceOf[CommitInfo]\n\n    // Verify that complex operationParameters are correctly handled\n    assert(deserialized.operationParameters == operation.jsonEncodedValues)\n    assert(deserialized.operation == operation.name)\n    assert(deserialized.userMetadata == operation.userMetadata)\n\n    // Verify the specific operation parameters that should be preserved\n    val params = deserialized.operationParameters\n    assert(params.contains(\"mode\"))\n    assert(params.contains(\"partitionBy\"))\n    assert(params.contains(\"predicate\"))\n\n    // Test legacy operation parameters for backward compatibility\n    val legacyParams = CommitInfo.getLegacyPostDeserializationOperationParameters(\n      deserialized.operationParameters)\n    // Legacy parameters should be different due to the broken deserialization that the\n    // JsonMapDeserializer fixes\n    assert(legacyParams != deserialized.operationParameters)\n  }\n\n  private def roundTripCompare(name: String, actions: Action*) = {\n    test(name) {\n      val asJson = actions.map(_.json)\n      val asObjects = asJson.map(Action.fromJson)\n\n      assert(actions === asObjects)\n    }\n  }\n\n  /** Test serialization/deserialization of [[Action]] by doing an actual commit */\n  private def testActionSerDe(\n      name: String,\n      action: => Action,\n      expectedJson: String,\n      extraSettings: Seq[(String, String)] = Seq.empty,\n      testTags: Seq[org.scalatest.Tag] = Seq.empty): Unit = {\n    import org.apache.spark.sql.delta.test.DeltaTestImplicits._\n    test(name, testTags: _*) {\n      withTempDir { tempDir =>\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getAbsolutePath))\n        // Disable different delta validations so that the passed action can be committed in\n        // all cases.\n        val settings = Seq(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\",\n          DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED.key -> \"false\") ++ extraSettings\n        withSQLConf(settings: _*) {\n\n          // Do one empty commit so that protocol gets committed.\n          val protocol = Protocol(\n            minReaderVersion = spark.conf.get(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION),\n            minWriterVersion = spark.conf.get(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION))\n          deltaLog.startTransaction().commitManually(protocol, Metadata())\n\n          // Commit the actual action.\n          val version = deltaLog.startTransaction().commit(Seq(action), ManualUpdate)\n          // Read the commit file and get the serialized committed actions\n          val committedActions = deltaLog.store.read(\n            FileNames.unsafeDeltaFile(deltaLog.logPath, version),\n            deltaLog.newDeltaHadoopConf())\n\n          assert(committedActions.size == 2)\n          val serializedJson = committedActions.last\n          assert(serializedJson === expectedJson)\n          val asObject = Action.fromJson(serializedJson)\n          assert(action === asObject)\n        }\n      }\n    }\n  }\n}\n\n// Both of these are used for CommitInfo serde tests\ncase class TestObject(field1: String, field2: Int, field3: Option[Seq[String]])\n\n/**\n * Test class to deserialize operation parameters without using custom\n * JsonMapDeserializer.\n */\nprivate final case class TestRawDeserialization(\n    @JsonSerialize(using = classOf[JsonMapSerializer])\n    operationParameters: Map[String, String])\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/AutoCompactSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.commands.optimize._\nimport org.apache.spark.sql.delta.hooks.{AutoCompact, AutoCompactType}\nimport org.apache.spark.sql.delta.optimize.CompactionTestHelperForAutoCompaction\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.AutoCompactPartitionStats\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StringType\nimport org.apache.spark.unsafe.types.UTF8String\n\ntrait AutoCompactTestUtils {\n  def captureOptimizeLogs(metrics: String)(f: => Unit): Seq[UsageRecord] = {\n    val usageLogs = Log4jUsageLogger.track(f)\n    usageLogs.filter { usageLog =>\n      usageLog.tags.get(\"opType\") == Some(metrics)\n    }\n  }\n\n}\n\n\n/**\n * This class extends the [[CompactionSuiteBase]] and runs all the [[CompactionSuiteBase]] tests\n * with AutoCompaction.\n *\n * It also tests AutoCompaction specific behavior around configuration settings.\n */\nclass AutoCompactConfigurationSuite extends\n    CompactionTestHelperForAutoCompaction\n  with DeltaSQLCommandTest\n  with SharedSparkSession\n  with AutoCompactTestUtils {\n\n  private def setTableProperty(log: DeltaLog, key: String, value: String): Unit = {\n    spark.sql(s\"ALTER TABLE delta.`${log.dataPath}` SET TBLPROPERTIES \" +\n      s\"($key = $value)\")\n  }\n\n  test(\"auto-compact-type: test table properties\") {\n    withTempDir { tempDir =>\n      val dir = tempDir.getCanonicalPath\n      spark.range(0, 1).write.format(\"delta\").mode(\"append\").save(dir)\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val defaultAutoCompactType = AutoCompact.getAutoCompactType(conf, deltaLog.snapshot.metadata)\n      Map(\n        \"true\" -> Some(AutoCompactType.Enabled),\n        \"tRue\" -> Some(AutoCompactType.Enabled),\n        \"'true'\" -> Some(AutoCompactType.Enabled),\n        \"false\" -> None,\n        \"fALse\" -> None,\n        \"'false'\" -> None\n      ).foreach { case (propertyValue, expectedAutoCompactType) =>\n        setTableProperty(deltaLog, \"delta.autoOptimize.autoCompact\", propertyValue)\n        assert(AutoCompact.getAutoCompactType(conf, deltaLog.snapshot.metadata) ==\n          expectedAutoCompactType)\n      }\n    }\n  }\n\n  test(\"auto-compact-type: test confs\") {\n    withTempDir { tempDir =>\n      val dir = tempDir.getCanonicalPath\n      spark.range(0, 1).write.format(\"delta\").mode(\"append\").save(dir)\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val defaultAutoCompactType = AutoCompact.getAutoCompactType(conf, deltaLog.snapshot.metadata)\n\n      Map(\n        \"true\" -> Some(AutoCompactType.Enabled),\n        \"TrUE\" -> Some(AutoCompactType.Enabled),\n        \"false\" -> None,\n        \"FalsE\" -> None\n      ).foreach { case (confValue, expectedAutoCompactType) =>\n        withSQLConf(DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key -> confValue) {\n          assert(AutoCompact.getAutoCompactType(conf, deltaLog.snapshot.metadata) ==\n            expectedAutoCompactType)\n        }\n      }\n    }\n  }\n\n}\n\n/**\n * This class extends the [[CompactionSuiteBase]] and runs all the [[CompactionSuiteBase]] tests\n * with AutoCompaction.\n *\n * It also tests AutoCompaction specific behavior around compaction execution.\n */\nclass AutoCompactExecutionSuite extends\n    CompactionTestHelperForAutoCompaction\n  with DeltaSQLCommandTest\n  with SharedSparkSession\n  with AutoCompactTestUtils {\n  private def testBothModesViaProperty(testName: String)(f: String => Unit): Unit = {\n    def runTest(autoCompactConfValue: String): Unit = {\n      withTempDir { dir =>\n        withSQLConf(\n            \"spark.databricks.delta.properties.defaults.autoOptimize.autoCompact\" ->\n              s\"$autoCompactConfValue\",\n            DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"0\",\n            DeltaSQLConf.DELTA_AUTO_COMPACT_MODIFIED_PARTITIONS_ONLY_ENABLED.key -> \"false\") {\n          f(dir.getCanonicalPath)\n        }\n      }\n    }\n\n    test(s\"auto-compact-enabled-property: $testName\") { runTest(autoCompactConfValue = \"true\") }\n  }\n\n  private def testBothModesViaConf(testName: String)(f: String => Unit): Unit = {\n    def runTest(autoCompactConfValue: String): Unit = {\n      withTempDir { dir =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key -> s\"$autoCompactConfValue\",\n          DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"0\") {\n          f(dir.getCanonicalPath)\n        }\n      }\n    }\n\n    test(s\"auto-compact-enabled-conf: $testName\") { runTest(autoCompactConfValue = \"true\") }\n  }\n\n  private def checkAutoOptimizeLogging(f: => Unit): Boolean = {\n    val logs = Log4jUsageLogger.track {\n      f\n    }\n    logs.exists(_.opType.map(_.typeName) === Some(\"delta.commit.hooks.autoOptimize\"))\n  }\n\n  import testImplicits._\n\n  test(\"auto compact event log: inline AC\") {\n    withTempDir { dir =>\n      withSQLConf(\n          DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key -> s\"true\",\n          DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"30\") {\n        val path = dir.getCanonicalPath\n        // Append 1 file to each partition: record runOnModifiedPartitions event, as is first write\n        var usageLogs = captureOptimizeLogs(AutoCompact.OP_TYPE) {\n          createFilesToPartitions(numFilePartitions = 3, numFilesPerPartition = 1, path)\n        }\n        var log = JsonUtils.mapper.readValue[Map[String, String]](usageLogs.head.blob)\n        assert(log(\"status\") == \"runOnModifiedPartitions\" && log(\"partitions\") == \"3\")\n        // Append 10 more file to each partition: record skipInsufficientFilesInModifiedPartitions\n        // event.\n        usageLogs = captureOptimizeLogs(AutoCompact.OP_TYPE) {\n          createFilesToPartitions(numFilePartitions = 3, numFilesPerPartition = 10, path)\n        }\n        log = JsonUtils.mapper.readValue[Map[String, String]](usageLogs.head.blob)\n        assert(log(\"status\") == \"skipInsufficientFilesInModifiedPartitions\")\n        // Append 20 more files to each partition: record runOnModifiedPartitions on all 3\n        // partitions.\n        usageLogs = captureOptimizeLogs(AutoCompact.OP_TYPE) {\n          createFilesToPartitions(numFilePartitions = 3, numFilesPerPartition = 20, path)\n        }\n        log = JsonUtils.mapper.readValue[Map[String, String]](usageLogs.head.blob)\n        assert(log(\"status\") == \"runOnModifiedPartitions\" && log(\"partitions\") == \"3\")\n        // Append 30 more file to each partition and check OptimizeMetrics.\n        usageLogs = captureOptimizeLogs(metrics = s\"${AutoCompact.OP_TYPE}.execute.metrics\") {\n          createFilesToPartitions(numFilePartitions = 3, numFilesPerPartition = 30, path)\n        }\n        val metricsLog = JsonUtils.mapper.readValue[OptimizeMetrics](usageLogs.head.blob)\n        assert(metricsLog.numBytesSkippedToReduceWriteAmplification === 0)\n        assert(metricsLog.numFilesSkippedToReduceWriteAmplification === 0)\n        assert(metricsLog.totalConsideredFiles === 93)\n        assert(metricsLog.numFilesAdded == 3)\n        assert(metricsLog.numFilesRemoved == 93)\n        assert(metricsLog.numBins === 3)\n      }\n    }\n  }\n\n  /**\n   * Writes `df` twice to the same location and checks that\n   *   1. There is only one resultant file.\n   *   2. The result is equal to `df` unioned with itself.\n   */\n  private def checkAutoCompactionWorks(dir: String, df: DataFrame): Unit = {\n    df.write.format(\"delta\").mode(\"append\").save(dir)\n    val deltaLog = DeltaLog.forTable(spark, dir)\n    val newSnapshot = deltaLog.update()\n    assert(newSnapshot.version === 1) // 0 is the first commit, 1 is optimize\n    assert(deltaLog.update().numOfFiles === 1)\n\n    val isLogged = checkAutoOptimizeLogging {\n      df.write.format(\"delta\").mode(\"append\").save(dir)\n    }\n\n    assert(isLogged)\n    val lastEvent = deltaLog.history.getHistory(Some(1)).head\n    assert(lastEvent.operation === \"OPTIMIZE\")\n    assert(lastEvent.operationParameters(\"auto\") === \"true\")\n\n    assert(deltaLog.update().numOfFiles === 1, \"Files should be optimized into a single one\")\n    checkAnswer(\n      df.union(df).toDF(),\n      spark.read.format(\"delta\").load(dir)\n    )\n  }\n\n  testBothModesViaProperty(\"auto compact should kick in when enabled - table config\") { dir =>\n    checkAutoCompactionWorks(dir, spark.range(10).toDF(\"id\"))\n  }\n\n  testBothModesViaConf(\"auto compact should kick in when enabled - session config\") { dir =>\n    checkAutoCompactionWorks(dir, spark.range(10).toDF(\"id\"))\n  }\n\n  test(\"variant auto compact kicks in when enabled - table config\") {\n    withTempDir { dir =>\n      withSQLConf(\n          \"spark.databricks.delta.properties.defaults.autoOptimize.autoCompact\" -> \"true\",\n          DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"0\",\n          DeltaSQLConf.DELTA_AUTO_COMPACT_MODIFIED_PARTITIONS_ONLY_ENABLED.key -> \"false\") {\n        checkAutoCompactionWorks(\n          dir.getCanonicalPath, spark.range(10).selectExpr(\"parse_json(cast(id as string)) as v\"))\n      }\n    }\n  }\n\n  test(\"variant auto compact kicks in when enabled - session config\") {\n    withTempDir { dir =>\n      withSQLConf(\n          DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key -> \"true\",\n          DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"0\") {\n        checkAutoCompactionWorks(\n          dir.getCanonicalPath, spark.range(10).selectExpr(\"parse_json(cast(id as string)) as v\"))\n      }\n    }\n  }\n\n  testBothModesViaProperty(\"auto compact should not kick in when session config is off\") { dir =>\n    withSQLConf(DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key -> \"false\") {\n      val isLogged = checkAutoOptimizeLogging {\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(dir)\n      }\n\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val newSnapshot = deltaLog.update()\n      assert(newSnapshot.version === 0) // 0 is the first commit\n      assert(deltaLog.update().numOfFiles > 1)\n      assert(!isLogged)\n    }\n  }\n\n  test(\"auto compact should not kick in after optimize\") {\n    withTempDir { tempDir =>\n        val dir = tempDir.getCanonicalPath\n        spark.range(0, 12, 1, 4).write.format(\"delta\").mode(\"append\").save(dir)\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        val newSnapshot = deltaLog.update()\n        assert(newSnapshot.version === 0)\n        assert(deltaLog.update().numOfFiles === 4)\n        spark.sql(s\"ALTER TABLE delta.`${tempDir.getCanonicalPath}` SET TBLPROPERTIES \" +\n          \"(delta.autoOptimize.autoCompact = true)\")\n\n        val isLogged = checkAutoOptimizeLogging {\n          sql(s\"optimize delta.`$dir`\")\n        }\n\n        assert(!isLogged)\n        val lastEvent = deltaLog.history.getHistory(Some(1)).head\n        assert(lastEvent.operation === \"OPTIMIZE\")\n        assert(lastEvent.operationParameters(\"auto\") === \"false\")\n    }\n  }\n\n  testBothModesViaProperty(\"auto compact should not kick in when there aren't \" +\n    \"enough files\") { dir =>\n    withSQLConf(DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"5\") {\n      AutoCompactPartitionStats.instance(spark).resetTestOnly()\n      spark.range(10).repartition(4).write.format(\"delta\").mode(\"append\").save(dir)\n\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val newSnapshot = deltaLog.update()\n      assert(newSnapshot.version === 0)\n      assert(deltaLog.update().numOfFiles === 4)\n\n      val isLogged2 = checkAutoOptimizeLogging {\n        spark.range(10).repartition(4).write.format(\"delta\").mode(\"append\").save(dir)\n      }\n\n      assert(isLogged2)\n      val lastEvent = deltaLog.history.getHistory(Some(1)).head\n      assert(lastEvent.operation === \"OPTIMIZE\")\n      assert(lastEvent.operationParameters(\"auto\") === \"true\")\n\n      assert(deltaLog.update().numOfFiles === 1, \"Files should be optimized into a single one\")\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n    }\n  }\n\n  testBothModesViaProperty(\"ensure no NPE in auto compact UDF with null \" +\n    \"partition values\") { dir =>\n      Seq(null, \"\", \" \").zipWithIndex.foreach { case (partValue, i) =>\n        val path = new File(dir, i.toString).getCanonicalPath\n        val df1 = spark.range(5).withColumn(\"part\", lit(partValue))\n        val df2 = spark.range(5, 10).withColumn(\"part\", lit(\"1\"))\n        val isLogged = checkAutoOptimizeLogging {\n          // repartition to increase number of files written\n          df1.union(df2).repartition(4)\n            .write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(path)\n        }\n        val deltaLog = DeltaLog.forTable(spark, path)\n        val newSnapshot = deltaLog.update()\n        assert(newSnapshot.version === 1) // 0 is the first commit, 1 and 2 are optimizes\n        assert(newSnapshot.numOfFiles === 2)\n\n        assert(isLogged)\n        val lastEvent = deltaLog.history.getHistory(Some(1)).head\n        assert(lastEvent.operation === \"OPTIMIZE\")\n        assert(lastEvent.operationParameters(\"auto\") === \"true\")\n      }\n  }\n\n  testBothModesViaProperty(\"check auto compact recorded metrics\") { dir =>\n    val logs = Log4jUsageLogger.track {\n      spark.range(30).repartition(3).write.format(\"delta\").save(dir)\n    }\n    val metrics = JsonUtils.mapper.readValue[OptimizeMetrics](logs.filter(\n      _.tags.get(\"opType\") == Some(s\"${AutoCompact.OP_TYPE}.execute.metrics\")).head.blob)\n\n    assert(metrics.numFilesRemoved == 3)\n    assert(metrics.numFilesAdded == 1)\n  }\n\n  private def setTableProperty(log: DeltaLog, key: String, value: String): Unit = {\n    spark.sql(s\"ALTER TABLE delta.`${log.dataPath}` SET TBLPROPERTIES \" +\n      s\"($key = $value)\")\n  }\n}\n\nclass AutoCompactConfigurationIdColumnMappingSuite extends AutoCompactConfigurationSuite\n  with DeltaColumnMappingEnableIdMode {\n  override def runAllTests: Boolean = true\n}\n\nclass AutoCompactExecutionIdColumnMappingSuite extends AutoCompactExecutionSuite\n  with DeltaColumnMappingEnableIdMode {\n  override def runAllTests: Boolean = true\n}\n\nclass AutoCompactConfigurationNameColumnMappingSuite extends AutoCompactConfigurationSuite\n  with DeltaColumnMappingEnableNameMode {\n  override def runAllTests: Boolean = true\n}\n\nclass AutoCompactExecutionNameColumnMappingSuite extends AutoCompactExecutionSuite\n  with DeltaColumnMappingEnableNameMode {\n  override def runAllTests: Boolean = true\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/BlockWritesLocalFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.net.URI\nimport java.util.concurrent.CountDownLatch\n\nimport org.apache.spark.sql.delta.BlockWritesLocalFileSystem.scheme\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{DelegateToFileSystem, FSDataOutputStream, Path, RawLocalFileSystem}\nimport org.apache.hadoop.util.Progressable\n\n/**\n * This custom fs implementation is used for testing the execution multiple batches of Optimize.\n */\nclass BlockWritesLocalFileSystem extends RawLocalFileSystem {\n\n  private var uri: URI = _\n\n  override def getScheme: String = scheme\n\n  override def initialize(name: URI, conf: Configuration): Unit = {\n    uri = URI.create(name.getScheme + \":///\")\n    super.initialize(name, conf)\n  }\n\n  override def getUri(): URI = if (uri == null) {\n    // RawLocalFileSystem's constructor will call this one before `initialize` is called.\n    // Just return the super's URI to avoid NPE.\n    super.getUri\n  } else {\n    uri\n  }\n\n  override def create(\n      f: Path,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream = {\n    // called when data files and log files are written\n    BlockWritesLocalFileSystem.blockLatch.countDown()\n    BlockWritesLocalFileSystem.blockLatch.await()\n    super.create(f, overwrite, bufferSize, replication, blockSize, progress)\n  }\n}\n\n/**\n * An AbstractFileSystem implementation wrapper around [[BlockWritesLocalFileSystem]].\n */\nclass BlockWritesAbstractFileSystem(uri: URI, conf: Configuration)\n    extends DelegateToFileSystem(\n      uri,\n      new BlockWritesLocalFileSystem,\n      conf,\n      BlockWritesLocalFileSystem.scheme,\n      false)\n\n/**\n * Singleton for BlockWritesLocalFileSystem used to initialize the file system countdown latch.\n */\nobject BlockWritesLocalFileSystem {\n  val scheme = \"block\"\n\n  /** latch that blocks writes */\n  private var blockLatch: CountDownLatch = _\n\n  /**\n   * @param numWrites - writing is blocked until there are `numWrites` concurrent writes to\n   *                  the file system.\n   */\n  def blockUntilConcurrentWrites(numWrites: Integer): Unit = {\n    blockLatch = new CountDownLatch(numWrites)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CheckCDCAnswer.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\n\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader.{CDC_COMMIT_TIMESTAMP, CDC_COMMIT_VERSION}\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\n\ntrait CheckCDCAnswer extends QueryTest {\n  /**\n   * Check the result of a CDC operation. The expected answer should include only CDC type and\n   * log version - the timestamp is nondeterministic, so we'll check just that it matches the\n   * correct value in the Delta log.\n   *\n   * @param log            The Delta log for the table CDC is being extracted from.\n   * @param df             The computed dataframe, which should match the default CDC result schema.\n   *                       Callers doing projections on top should use checkAnswer directly.\n   * @param expectedAnswer The expected results for the CDC query, excluding the CDC_LOG_TIMESTAMP\n   *                       column which we handle inside this method.\n   */\n  def checkCDCAnswer(log: DeltaLog, df: => DataFrame, expectedAnswer: Seq[Row]): Unit = {\n    checkAnswer(df.drop(CDC_COMMIT_TIMESTAMP), expectedAnswer)\n\n    val timestampsByVersion = df.select(CDC_COMMIT_VERSION, CDC_COMMIT_TIMESTAMP).collect()\n      .map { row =>\n        val version = row.getLong(0)\n        val ts = row.getTimestamp(1)\n        (version -> ts)\n      }.toMap\n    val correctTimestampsByVersion = {\n      // Results should match the fully monotonized commits. Note that this map will include\n      // all versions of the table but only the ones in timestampsByVersion are checked for\n      // correctness.\n      val commits = log.history.getHistory(start = 0, end = None)\n      // Note that the timestamps are in milliseconds since epoch and we don't need to deal\n      // with timezones.\n      commits.map(f => (f.getVersion -> f.timestamp)).toMap\n    }\n\n    timestampsByVersion.keySet.foreach { version =>\n      assert(timestampsByVersion(version) === correctTimestampsByVersion(version))\n    }\n  }\n\n  def checkCDCAnswer(log: DeltaLog, df: => DataFrame, expectedAnswer: DataFrame): Unit = {\n    checkCDCAnswer(log, df, expectedAnswer.collect())\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CheckpointInstanceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.CheckpointInstance.Format\n\nimport org.apache.spark.SparkFunSuite\n\nclass CheckpointInstanceSuite extends SparkFunSuite {\n\n  test(\"checkpoint instance comparisons\") {\n    val ci1_single_1 = CheckpointInstance(1, Format.SINGLE, numParts = None)\n    val ci1_withparts_2 = CheckpointInstance(1, Format.WITH_PARTS, numParts = Some(2))\n    val ci1_sentinel = CheckpointInstance.sentinelValue(Some(1))\n\n    val ci2_single_1 = CheckpointInstance(2, Format.SINGLE, numParts = None)\n    val ci2_withparts_4 = CheckpointInstance(2, Format.WITH_PARTS, numParts = Some(4))\n    val ci2_sentinel = CheckpointInstance.sentinelValue(Some(2))\n\n    val ci3_single_1 = CheckpointInstance(3, Format.SINGLE, numParts = None)\n    val ci3_withparts_2 = CheckpointInstance(3, Format.WITH_PARTS, numParts = Some(2))\n\n    assert(ci1_single_1 < ci2_single_1) // version takes priority\n    assert(ci1_single_1 < ci1_withparts_2) // parts takes priority when versions are same\n    assert(ci2_withparts_4 < ci3_withparts_2) // version takes priority over parts\n\n    // all checkpoint instances for version 1/2 are less than sentinel value for version 2.\n    Seq(ci1_single_1, ci1_withparts_2, ci1_sentinel, ci2_single_1, ci2_withparts_4)\n      .foreach(ci => assert(ci < ci2_sentinel))\n\n    // all checkpoint instances for version 3 are greater than sentinel value for version 2.\n    Seq(ci3_single_1, ci3_withparts_2).foreach(ci => assert(ci > ci2_sentinel))\n\n    // Everything is less than CheckpointInstance.MaxValue\n    Seq(\n      ci1_single_1, ci1_withparts_2, ci1_sentinel,\n      ci2_single_1, ci2_withparts_4, ci2_sentinel,\n      ci3_single_1, ci3_withparts_2\n    ).foreach(ci => assert(ci < CheckpointInstance.MaxValue))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CheckpointProtectionTestUtilsMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\n\nimport org.apache.spark.util.ManualClock\n\ntrait CheckpointProtectionTestUtilsMixin\n    extends DeltaSQLCommandTest { self: DeltaRetentionSuiteBase =>\n\n  // scalastyle:off argcount\n  def testRequireCheckpointProtectionBeforeVersion(\n      createNumCommitsOutsideRetentionPeriod: Int,\n      createNumCommitsWithinRetentionPeriod: Int,\n      createCheckpoints: Set[Int],\n      requireCheckpointProtectionBeforeVersion: Int,\n      additionalFeatureToEnable: Option[TableFeature] = None,\n      unsupportedFeature: TableFeature = TestUnsupportedNoHistoryProtectionReaderWriterFeature,\n      unsupportedFeatureStartVersion: Option[Long] = None,\n      unsupportedFeatureEndVersion: Option[Long] = None,\n      incompleteCRCVersion: Option[Long] = None,\n      missingCRCVersion: Option[Long] = None,\n      expectedCommitsAfterCleanup: Seq[Int],\n      expectedCheckpointsAfterCleanup: Set[Int]): Unit = {\n    // scalastyle:on argcount\n    withTempDir { dir =>\n      val currentTime = System.currentTimeMillis()\n      val clock = new ManualClock(currentTime)\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n      val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n      val propertyKey = DeltaConfigs.REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION.key\n      val additionalFeatureEnablement =\n        additionalFeatureToEnable.map(f => s\"delta.feature.${f.name} = 'supported',\")\n          .getOrElse(\"\")\n      val featureEnablement = s\"delta.feature.${unsupportedFeature.name} = 'supported'\"\n      val featureEnablementAtCreateTable =\n        if (unsupportedFeatureStartVersion.exists(_ == 0)) s\"$featureEnablement,\" else \"\"\n\n      // Commit 0.\n      sql(\n        s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.feature.${CheckpointProtectionTableFeature.name} = 'supported',\n           |$additionalFeatureEnablement\n           |${featureEnablementAtCreateTable}\n           |$propertyKey = $requireCheckpointProtectionBeforeVersion\n           |)\"\"\".stripMargin)\n      if (createCheckpoints.contains(0)) deltaLog.checkpoint(deltaLog.update())\n      setModificationTime(deltaLog, startTime = currentTime, version = 0, dayNum = 0, fs)\n\n      def createCommit(version: Int): Unit = {\n        if (unsupportedFeatureStartVersion.exists(_ == version)) {\n          sql(s\"ALTER TABLE delta.`${deltaLog.dataPath}` SET TBLPROPERTIES ($featureEnablement)\")\n        } else if (unsupportedFeatureEndVersion.exists(_ == version)) {\n          sql(\n            s\"\"\"ALTER TABLE delta.`${deltaLog.dataPath}`\n               |DROP FEATURE ${unsupportedFeature.name}\n               |\"\"\".stripMargin)\n        } else {\n          spark.range(version, version + 1)\n            .write\n            .format(\"delta\")\n            .mode(\"append\")\n            .save(dir.getCanonicalPath)\n        }\n        if (createCheckpoints.contains(version)) deltaLog.checkpoint(deltaLog.update())\n      }\n\n      // Rest createNumCommitsOutsideRetentionPeriod - 1 commits.\n      for (n <- 1 to createNumCommitsOutsideRetentionPeriod - 1) {\n        createCommit(n)\n        setModificationTime(deltaLog, startTime = currentTime, version = n, dayNum = 0, fs)\n      }\n\n      val millisToAdvance =\n        intervalStringToMillis(DeltaConfigs.LOG_RETENTION.defaultValue) + TimeUnit.DAYS.toMillis(3)\n      clock.advance(millisToAdvance)\n\n      // Commits within retention period.\n      val daysToAdvance = TimeUnit.MILLISECONDS.toDays(millisToAdvance).toInt\n      for (n <- 0 to createNumCommitsWithinRetentionPeriod - 1) {\n          val m = createNumCommitsOutsideRetentionPeriod + n\n          createCommit(m)\n\n          // Advance the timestamp of the commit/checkpoint we just created.\n          setModificationTime(\n            deltaLog,\n            startTime = currentTime,\n            version = m,\n            // The files were created somewhere between day 32 and day 33.\n            dayNum = daysToAdvance - 1,\n            fs)\n        }\n\n      incompleteCRCVersion.foreach { version =>\n        val checksumFilePath = FileNames.checksumFile(deltaLog.logPath, version)\n        removeProtocolAndMetadataFromChecksumFile(checksumFilePath)\n      }\n\n      missingCRCVersion.foreach { version =>\n        val checksumFilePath = FileNames.checksumFile(deltaLog.logPath, version)\n        (new File(checksumFilePath.toUri)).delete()\n      }\n\n      deltaLog.cleanUpExpiredLogs(deltaLog.update())\n\n      val logPath = new File(deltaLog.logPath.toUri)\n      assert(getDeltaVersions(logPath).toSeq.sorted === expectedCommitsAfterCleanup.sorted)\n      assert(getCheckpointVersions(logPath) === expectedCheckpointsAfterCleanup)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CheckpointProviderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Action}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.FileNames._\n\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass CheckpointProviderSuite\n  extends SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  for (v2CheckpointFormat <- Seq(\"json\", \"parquet\"))\n  test(s\"V2 Checkpoint compat file equivalency to normal V2 Checkpoint\" +\n      s\" [v2CheckpointFormat: $v2CheckpointFormat]\") {\n    withSQLConf(\n      DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n      DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> v2CheckpointFormat\n    ) {\n      withTempDir { tempDir =>\n        spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n\n        spark.range(10).write.mode(\"append\").format(\"delta\").save(tempDir.getAbsolutePath)\n\n        deltaLog.checkpoint() // Checkpoint 1\n        val snapshot = deltaLog.update()\n\n        deltaLog.createSinglePartCheckpointForBackwardCompat(\n          snapshot, new deltaLog.V2CompatCheckpointMetrics) // Compatibility Checkpoint 1\n\n        val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n        val v2CompatCheckpoint = fs.getFileStatus(\n          checkpointFileSingular(deltaLog.logPath, snapshot.checkpointProvider.version))\n\n        val origCheckpoint = snapshot.checkpointProvider\n          .asInstanceOf[LazyCompleteCheckpointProvider]\n          .underlyingCheckpointProvider\n          .asInstanceOf[V2CheckpointProvider]\n        val compatCheckpoint = CheckpointProvider(\n          spark,\n          deltaLog.snapshot,\n          None,\n          UninitializedV2CheckpointProvider(\n            2L,\n            v2CompatCheckpoint,\n            deltaLog.logPath,\n            deltaLog.newDeltaHadoopConf(),\n            deltaLog.options,\n            deltaLog.store,\n            None))\n          .asInstanceOf[LazyCompleteCheckpointProvider]\n          .underlyingCheckpointProvider\n          .asInstanceOf[V2CheckpointProvider]\n\n        // Check whether these checkpoints are equivalent after being loaded\n        assert(compatCheckpoint.sidecarFiles.toSet === origCheckpoint.sidecarFiles.toSet)\n        assert(compatCheckpoint.checkpointMetadata === origCheckpoint.checkpointMetadata)\n\n        val compatDf =\n            deltaLog.loadIndex(compatCheckpoint.topLevelFileIndex.get, Action.logSchema)\n          // Check whether the manifest content is same or not\n        val originalDf =\n            deltaLog.loadIndex(origCheckpoint.topLevelFileIndex.get, Action.logSchema)\n            assert(originalDf.sort().collect() === compatDf.sort().collect())\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CheckpointsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.net.URI\nimport java.util.UUID\n\nimport scala.concurrent.duration._\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions, UsageRecord}\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.deletionvectors.DeletionVectorsSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LocalLogStore\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.shims.VariantStatsShims\nimport org.apache.spark.sql.delta.util.{Codec, DeltaCommitFileProvider, DeltaStatsJsonUtils}\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FSDataOutputStream, Path, RawLocalFileSystem}\nimport org.apache.hadoop.fs.permission.FsPermission\nimport org.apache.hadoop.util.Progressable\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.types.variant.{Variant, VariantUtil}\nimport org.apache.spark.unsafe.types.VariantVal\n\nclass CheckpointsSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaCheckpointTestUtils\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils\n  with CatalogOwnedTestBaseSuite {\n\n  def testDifferentV2Checkpoints(testName: String)(f: => Unit): Unit = {\n    for (checkpointFormat <- Seq(V2Checkpoint.Format.JSON.name, V2Checkpoint.Format.PARQUET.name)) {\n      test(s\"$testName [v2CheckpointFormat: $checkpointFormat]\") {\n        withSQLConf(\n          DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n          DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> checkpointFormat\n        ) {\n          f\n        }\n      }\n    }\n  }\n\n  /** Get V2 [[CheckpointProvider]] from the underlying deltalog snapshot */\n  def getV2CheckpointProvider(\n      deltaLog: DeltaLog,\n      update: Boolean = true): V2CheckpointProvider = {\n    val snapshot = if (update) deltaLog.update() else deltaLog.unsafeVolatileSnapshot\n    snapshot.checkpointProvider match {\n      case v2CheckpointProvider: V2CheckpointProvider =>\n        v2CheckpointProvider\n      case provider : LazyCompleteCheckpointProvider\n          if provider.underlyingCheckpointProvider.isInstanceOf[V2CheckpointProvider] =>\n        provider.underlyingCheckpointProvider.asInstanceOf[V2CheckpointProvider]\n      case EmptyCheckpointProvider =>\n        throw new IllegalStateException(\"underlying snapshot doesn't have a checkpoint\")\n      case other =>\n        throw new IllegalStateException(s\"The underlying checkpoint is not a v2 checkpoint. \" +\n          s\"It is: ${other.getClass.getName}\")\n    }\n  }\n\n  protected override def sparkConf = {\n    // Set the gs LogStore impl to `LocalLogStore` so that it will work with\n    // `FakeGCSFileSystemValidatingCheckpoint`.\n    // The default one is `HDFSLogStore` which requires a `FileContext` but we don't have one.\n    super.sparkConf.set(\"spark.delta.logStore.gs.impl\", classOf[LocalLogStore].getName)\n  }\n\n  test(\"checkpoint metadata - checkpoint schema above the configured threshold are not\" +\n      \" written to LAST_CHECKPOINT\") {\n    withClassicCheckpointPolicyForCatalogOwned {\n      withTempTable(createTable = false) { tableName =>\n        spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        deltaLog.checkpoint()\n        val lastCheckpointOpt = deltaLog.readLastCheckpointFile()\n        assert(lastCheckpointOpt.nonEmpty)\n        assert(lastCheckpointOpt.get.checkpointSchema.nonEmpty)\n        val expectedCheckpointSchema =\n          Seq(\"txn\", \"add\", \"remove\", \"metaData\", \"protocol\", \"domainMetadata\")\n        assert(lastCheckpointOpt.get.checkpointSchema.get.fieldNames.toSeq ===\n          expectedCheckpointSchema)\n\n        spark.range(10).write.mode(\"append\").format(\"delta\").saveAsTable(tableName)\n        withSQLConf(DeltaSQLConf.CHECKPOINT_SCHEMA_WRITE_THRESHOLD_LENGTH.key -> \"10\") {\n          deltaLog.checkpoint()\n          val lastCheckpointOpt = deltaLog.readLastCheckpointFile()\n          assert(lastCheckpointOpt.nonEmpty)\n          assert(lastCheckpointOpt.get.checkpointSchema.isEmpty)\n        }\n      }\n    }\n  }\n\n  testDifferentV2Checkpoints(\"checkpoint metadata - checkpoint schema not persisted in\" +\n      \" json v2 checkpoints but persisted in parquet v2 checkpoints\") {\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      deltaLog.checkpoint()\n      val lastCheckpointOpt = deltaLog.readLastCheckpointFile()\n      assert(lastCheckpointOpt.nonEmpty)\n      val expectedFormat =\n        spark.conf.getOption(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key)\n      assert(lastCheckpointOpt.get.checkpointSchema.isEmpty ===\n        (expectedFormat.contains(V2Checkpoint.Format.JSON.name)))\n    }\n  }\n\n  testDifferentCheckpoints(\"test empty checkpoints\") { (checkpointPolicy, _) =>\n    val tableName = \"test_empty_table\"\n    withTable(tableName) {\n      sql(s\"CREATE TABLE `$tableName` (a INT) USING DELTA\")\n      sql(s\"ALTER TABLE `$tableName` SET TBLPROPERTIES('comment' = 'A table comment')\")\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      deltaLog.checkpoint()\n      def validateSnapshot(snapshot: Snapshot): Unit = {\n        assert(!snapshot.checkpointProvider.isEmpty)\n        assert(snapshot.checkpointProvider.version === 1)\n        val checkpointFile = snapshot.checkpointProvider.topLevelFiles.head.getPath\n        val fileActions = getCheckpointDfForFilesContainingFileActions(deltaLog, checkpointFile)\n        assert(fileActions.where(\"add is not null or remove is not null\").collect().size === 0)\n        if (checkpointPolicy == CheckpointPolicy.V2) {\n          val v2CheckpointProvider = snapshot.checkpointProvider match {\n            case lazyCompleteCheckpointProvider: LazyCompleteCheckpointProvider =>\n              lazyCompleteCheckpointProvider.underlyingCheckpointProvider\n                .asInstanceOf[V2CheckpointProvider]\n            case cp: V2CheckpointProvider => cp\n            case _ => throw new IllegalStateException(\"Unexpected checkpoint provider\")\n          }\n          assert(v2CheckpointProvider.sidecarFiles.size === 1)\n          val sidecar = v2CheckpointProvider.sidecarFiles.head.toFileStatus(deltaLog.logPath)\n          assert(spark.read.parquet(sidecar.getPath.toString).count() === 0)\n        }\n      }\n      validateSnapshot(deltaLog.update())\n      DeltaLog.clearCache()\n      validateSnapshot(DeltaLog.forTable(spark, TableIdentifier(tableName)).unsafeVolatileSnapshot)\n    }\n  }\n\n  testDifferentV2Checkpoints(s\"V2 Checkpoint write test\" +\n      s\" - metadata, protocol, sidecar, checkpoint metadata actions\") {\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      deltaLog.checkpoint()\n      val checkpointFiles = deltaLog.listFrom(0).filter(FileNames.isCheckpointFile).toList\n      assert(checkpointFiles.length == 1)\n      val checkpoint = checkpointFiles.head\n      val fileNameParts = checkpoint.getPath.getName.split(\"\\\\.\")\n      // The file name should be <version>.checkpoint.<uniqueStr>.parquet.\n      assert(fileNameParts.length == 4)\n      fileNameParts match {\n        case Array(version, checkpointLiteral, _, format) =>\n          val expectedFormat =\n            spark.conf.getOption(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key).get\n          assert(format == expectedFormat)\n          assert(version.toLong == 0)\n          assert(checkpointLiteral == \"checkpoint\")\n      }\n\n      def getCheckpointFileActions(checkpoint: FileStatus) : Seq[Action] = {\n        if (checkpoint.getPath.toString.endsWith(\"json\")) {\n          deltaLog.store.read(checkpoint.getPath).map(Action.fromJson)\n        } else {\n          val fileIndex =\n            DeltaLogFileIndex(DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_PARQUET, Seq(checkpoint)).get\n          deltaLog.loadIndex(fileIndex, Action.logSchema)\n            .as[SingleAction].collect().map(_.unwrap).toSeq\n        }\n      }\n      val actions = getCheckpointFileActions(checkpoint)\n      // V2 Checkpoints should contain exactly one action each of types\n      // Metadata, CheckpointMetadata, and Protocol\n      // In this particular case, we should only have one sidecar file\n      val sidecarActions = actions.collect{ case s: SidecarFile => s}\n      assert(sidecarActions.length == 1)\n      val sidecarPath = sidecarActions.head.path\n      assert(sidecarPath.endsWith(\"parquet\"))\n\n      val metadataActions = actions.collect { case m: Metadata => m }\n      assert(metadataActions.length == 1)\n\n      val checkpointMetadataActions = actions.collect { case cm: CheckpointMetadata => cm }\n      assert(checkpointMetadataActions.length == 1)\n\n      assert(\n        DeltaConfigs.CHECKPOINT_POLICY.fromMetaData(metadataActions.head)\n        .needsV2CheckpointSupport\n      )\n\n      val protocolActions = actions.collect { case p: Protocol => p }\n      assert(protocolActions.length == 1)\n      assert(CheckpointProvider.isV2CheckpointEnabled(protocolActions.head))\n    }\n  }\n\n  test(\"SC-86940: isGCSPath\") {\n    val conf = new Configuration()\n    assert(Checkpoints.isGCSPath(conf, new Path(\"gs://foo/bar\")))\n    // Scheme is case insensitive\n    assert(Checkpoints.isGCSPath(conf, new Path(\"Gs://foo/bar\")))\n    assert(Checkpoints.isGCSPath(conf, new Path(\"GS://foo/bar\")))\n    assert(Checkpoints.isGCSPath(conf, new Path(\"gS://foo/bar\")))\n    assert(!Checkpoints.isGCSPath(conf, new Path(\"non-gs://foo/bar\")))\n    assert(!Checkpoints.isGCSPath(conf, new Path(\"/foo\")))\n    // Set the default file system and verify we can detect it\n    conf.set(\"fs.defaultFS\", \"gs://foo/\")\n    conf.set(\"fs.gs.impl\", classOf[FakeGCSFileSystemValidatingCheckpoint].getName)\n    conf.set(\"fs.gs.impl.disable.cache\", \"true\")\n    assert(Checkpoints.isGCSPath(conf, new Path(\"/foo\")))\n  }\n\n  test(\"SC-86940: writing a GCS checkpoint should happen in a new thread\") {\n    withTempDir { tempDir =>\n      // Use `FakeGCSFileSystemValidatingCheckpoint` which will verify we write in a separate gcs\n      // thread.\n      withSQLConf(\n          \"fs.gs.impl\" -> classOf[FakeGCSFileSystemValidatingCheckpoint].getName,\n          \"fs.gs.impl.disable.cache\" -> \"true\") {\n        val gsPath = s\"gs://${tempDir.getCanonicalPath}\"\n        val writer = spark.range(1).write.format(\"delta\")\n        if (catalogOwnedDefaultCreationEnabledInTests) {\n          // Setting checkpointPolicy=classic because this test is intended for v1 checkpoint only.\n          writer.option(DeltaConfigs.CHECKPOINT_POLICY.key, \"classic\")\n        }\n        writer.save(gsPath)\n        DeltaLog.clearCache()\n        val deltaLog = DeltaLog.forTable(spark, new Path(gsPath))\n        deltaLog.checkpoint()\n      }\n    }\n  }\n\n  private def verifyCheckpoint(\n      checkpoint: Option[LastCheckpointInfo],\n      version: Int,\n      parts: Option[Int]): Unit = {\n    assert(checkpoint.isDefined)\n    checkpoint.foreach { lastCheckpointInfo =>\n      assert(lastCheckpointInfo.version == version)\n      assert(lastCheckpointInfo.parts == parts)\n    }\n  }\n\n  test(\"multipart checkpoints\") {\n    withClassicCheckpointPolicyForCatalogOwned {\n      withTempTable(createTable = false) { tableName =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"10\",\n          DeltaConfigs.CHECKPOINT_INTERVAL.defaultTablePropertyKey -> \"1\") {\n          // 1 file actions\n          spark.range(1).repartition(1).write.format(\"delta\").saveAsTable(tableName)\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n          // 2 file actions, 1 new file\n          spark.range(1).repartition(1).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n\n          verifyCheckpoint(deltaLog.readLastCheckpointFile(), 1, None)\n\n          val checkpointPath =\n            FileNames.checkpointFileSingular(deltaLog.logPath, deltaLog.snapshot.version).toUri\n          assert(new File(checkpointPath).exists())\n\n          // 11 total file actions, 9 new files\n          spark.range(30).repartition(9).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n          verifyCheckpoint(deltaLog.readLastCheckpointFile(), 2, Some(2))\n\n          var checkpointPaths =\n            FileNames.checkpointFileWithParts(deltaLog.logPath, deltaLog.snapshot.version, 2)\n          checkpointPaths.foreach(p => assert(new File(p.toUri).exists()))\n\n          // 20 total actions, 9 new files\n          spark\n            .range(100)\n            .repartition(9)\n            .write\n            .format(\"delta\")\n            .mode(\"append\")\n            .saveAsTable(tableName)\n          verifyCheckpoint(deltaLog.readLastCheckpointFile(), 3, Some(2))\n\n          assert(deltaLog.snapshot.version == 3)\n          checkpointPaths =\n            FileNames.checkpointFileWithParts(deltaLog.logPath, deltaLog.snapshot.version, 2)\n          checkpointPaths.foreach(p => assert(new File(p.toUri).exists()))\n\n          // 31 total actions, 11 new files\n          spark\n            .range(100)\n            .repartition(11)\n            .write\n            .format(\"delta\")\n            .mode(\"append\")\n            .saveAsTable(tableName)\n          verifyCheckpoint(deltaLog.readLastCheckpointFile(), 4, Some(4))\n\n          assert(deltaLog.snapshot.version == 4)\n          checkpointPaths =\n            FileNames.checkpointFileWithParts(deltaLog.logPath, deltaLog.snapshot.version, 4)\n          checkpointPaths.foreach(p => assert(new File(p.toUri).exists()))\n        }\n\n        // Increase max actions\n        withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"100\") {\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          // 100 total actions, 69 new files\n          spark.range(1000)\n            .repartition(69).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n          verifyCheckpoint(deltaLog.readLastCheckpointFile(), 5, None)\n          val checkpointPath =\n            FileNames.checkpointFileSingular(deltaLog.logPath, deltaLog.snapshot.version).toUri\n          assert(new File(checkpointPath).exists())\n\n          // 101 total actions, 1 new file\n          spark.range(1).repartition(1).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n          verifyCheckpoint(deltaLog.readLastCheckpointFile(), 6, Some(2))\n          var checkpointPaths =\n            FileNames.checkpointFileWithParts(deltaLog.logPath, deltaLog.snapshot.version, 2)\n          checkpointPaths.foreach(p => assert(new File(p.toUri).exists()))\n        }\n      }\n    }\n  }\n\n  testDifferentV2Checkpoints(\"multipart v2 checkpoint\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      withSQLConf(\n        DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"10\",\n        DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n        DeltaConfigs.CHECKPOINT_INTERVAL.defaultTablePropertyKey -> \"1\") {\n        // 1 file actions\n        spark.range(1).repartition(1).write.format(\"delta\").save(path)\n        val deltaLog = DeltaLog.forTable(spark, path)\n\n        def getNumFilesInSidecarDirectory(): Int = {\n          val fs = deltaLog.sidecarDirPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n          fs.listStatus(deltaLog.sidecarDirPath).size\n        }\n\n        // 2 file actions, 1 new file\n        spark.range(1).repartition(1).write.format(\"delta\").mode(\"append\").save(path)\n        assert(getV2CheckpointProvider(deltaLog).version == 1)\n        assert(getV2CheckpointProvider(deltaLog).sidecarFileStatuses.size == 1)\n        assert(getNumFilesInSidecarDirectory() == 1)\n\n        // 11 total file actions, 9 new files\n        spark.range(30).repartition(9).write.format(\"delta\").mode(\"append\").save(path)\n        assert(getV2CheckpointProvider(deltaLog).version == 2)\n        assert(getV2CheckpointProvider(deltaLog).sidecarFileStatuses.size == 2)\n        assert(getNumFilesInSidecarDirectory() == 3)\n\n        // 20 total actions, 9 new files\n        spark.range(100).repartition(9).write.format(\"delta\").mode(\"append\").save(path)\n        assert(getV2CheckpointProvider(deltaLog).version == 3)\n        assert(getV2CheckpointProvider(deltaLog).sidecarFileStatuses.size == 2)\n        assert(getNumFilesInSidecarDirectory() == 5)\n\n        // 31 total actions, 11 new files\n        spark.range(100).repartition(11).write.format(\"delta\").mode(\"append\").save(path)\n        assert(getV2CheckpointProvider(deltaLog).version == 4)\n        assert(getV2CheckpointProvider(deltaLog).sidecarFileStatuses.size == 4)\n        assert(getNumFilesInSidecarDirectory() == 9)\n\n        // Increase max actions\n        withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"100\") {\n          // 100 total actions, 69 new files\n          spark.range(1000).repartition(69).write.format(\"delta\").mode(\"append\").save(path)\n          assert(getV2CheckpointProvider(deltaLog).version == 5)\n          assert(getV2CheckpointProvider(deltaLog).sidecarFileStatuses.size == 1)\n          assert(getNumFilesInSidecarDirectory() == 10)\n\n          // 101 total actions, 1 new file\n          spark.range(1).repartition(1).write.format(\"delta\").mode(\"append\").save(path)\n          assert(getV2CheckpointProvider(deltaLog).version == 6)\n          assert(getV2CheckpointProvider(deltaLog).sidecarFileStatuses.size == 2)\n          assert(getNumFilesInSidecarDirectory() == 12)\n        }\n      }\n    }\n  }\n\n  test(\"checkpoint does not contain CDC field\") {\n    withSQLConf(\n        DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\"\n    ) {\n      withTempDir { tempDir =>\n        withTempView(\"src\") {\n          spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n          spark.range(5, 15).createOrReplaceTempView(\"src\")\n          sql(\n            s\"\"\"\n               |MERGE INTO delta.`$tempDir` t USING src s ON t.id = s.id\n               |WHEN MATCHED THEN DELETE\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n          checkAnswer(\n            spark.read.format(\"delta\").load(tempDir.getAbsolutePath),\n            Seq(0, 1, 2, 3, 4, 10, 11, 12, 13, 14).map { i => Row(i) })\n\n          // CDC should exist in the log as seen through getChanges, but it shouldn't be in the\n          // snapshots and the checkpoint file shouldn't have a CDC column.\n          val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n          val deltaPath = DeltaCommitFileProvider(deltaLog.unsafeVolatileSnapshot)\n            .deltaFile(version = 1)\n          val deltaFileContent = deltaLog.store.read(deltaPath, deltaLog.newDeltaHadoopConf())\n          assert(deltaFileContent.map(Action.fromJson).exists(_.isInstanceOf[AddCDCFile]))\n          assert(deltaLog.snapshot.stateDS.collect().forall { sa => sa.cdc == null })\n          deltaLog.checkpoint()\n          val checkpointPathStr = DeltaLog.forTableWithSnapshot(spark, tempDir.getAbsolutePath)._2\n            .checkpointProvider.topLevelFiles.head.getPath.toString\n          val checkpointFormat = checkpointPathStr.substring(checkpointPathStr.lastIndexOf('.') + 1)\n          val checkpointSchema = spark.read.format(checkpointFormat).load(checkpointPathStr).schema\n          var expectedCheckpointSchema =\n            Seq(\n              \"txn\",\n              \"add\",\n              \"remove\",\n              \"metaData\",\n              \"protocol\",\n              \"domainMetadata\")\n          // For CCv1.5 table, v2 checkpoints is enabled by default.\n          if (catalogOwnedDefaultCreationEnabledInTests) {\n            // V2 checkpoint's schema is shared by sidecar files (contains all file actions)\n            // and the main v2 checkpoint file (contains all non-file actions).\n            // So file actions (e.g. `txn`, `add`, `remove`) are not included in the main v2\n            // checkpoint file.\n            expectedCheckpointSchema = Seq(\n              \"checkpointMetadata\", \"domainMetadata\", \"metaData\", \"protocol\", \"sidecar\")\n          }\n          assert(checkpointSchema.fieldNames.toSeq == expectedCheckpointSchema)\n        }\n      }\n    }\n  }\n\n  testDifferentV2Checkpoints(\"v2 checkpoint contains only addfile and removefile and\" +\n      \" remove file does not contain remove.tags and remove.numRecords\") {\n    withSQLConf(\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\"\n    ) {\n      val expectedCheckpointSchema = Seq(\"add\", \"remove\")\n      val expectedRemoveFileSchema = Seq(\n        \"path\",\n        \"deletionTimestamp\",\n        \"dataChange\",\n        \"extendedFileMetadata\",\n        \"partitionValues\",\n        \"size\",\n        \"deletionVector\",\n        \"baseRowId\",\n        \"defaultRowCommitVersion\")\n      withTempDir { tempDir =>\n        withTempView(\"src\") {\n          val tablePath = tempDir.getAbsolutePath\n          // Append rows [0, 9] to table and merge tablePath.\n          spark.range(end = 10).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n          spark.range(5, 15).createOrReplaceTempView(\"src\")\n          sql(\n            s\"\"\"\n               |MERGE INTO delta.`$tempDir` t USING src s ON t.id = s.id\n               |WHEN MATCHED THEN DELETE\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n          checkAnswer(\n            spark.read.format(\"delta\").load(tempDir.getAbsolutePath),\n            Seq(0, 1, 2, 3, 4, 10, 11, 12, 13, 14).map { i => Row(i) })\n\n          // CDC should exist in the log as seen through getChanges, but it shouldn't be in the\n          // snapshots and the checkpoint file shouldn't have a CDC column.\n          val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n          val deltaPath = DeltaCommitFileProvider(deltaLog.unsafeVolatileSnapshot)\n            .deltaFile(version = 1)\n          val deltaFileContent = deltaLog.store.read(deltaPath, deltaLog.newDeltaHadoopConf())\n          assert(deltaFileContent.map(Action.fromJson).exists(_.isInstanceOf[AddCDCFile]))\n          assert(deltaLog.snapshot.stateDS.collect().forall { sa => sa.cdc == null })\n          deltaLog.checkpoint()\n          var sidecarCheckpointFiles = getV2CheckpointProvider(deltaLog).sidecarFileStatuses\n          assert(sidecarCheckpointFiles.size == 1)\n          var sidecarFile = sidecarCheckpointFiles.head.getPath.toString\n          var checkpointSchema = spark.read.format(\"parquet\").load(sidecarFile).schema\n          var removeSchemaName =\n            checkpointSchema(\"remove\").dataType.asInstanceOf[StructType].fieldNames\n          assert(checkpointSchema.fieldNames.toSeq == expectedCheckpointSchema)\n          assert(removeSchemaName.toSeq === expectedRemoveFileSchema)\n\n          // Append rows [0, 9] to table and merge one more time.\n          spark.range(end = 10).write.format(\"delta\").mode(\"append\").save(tablePath)\n          sql(\n            s\"\"\"\n               |MERGE INTO delta.`$tempDir` t USING src s ON t.id = s.id\n               |WHEN MATCHED THEN DELETE\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n          deltaLog.checkpoint()\n          sidecarCheckpointFiles = getV2CheckpointProvider(deltaLog).sidecarFileStatuses\n          sidecarFile = sidecarCheckpointFiles.head.getPath.toString\n          checkpointSchema = spark.read.format(source = \"parquet\").load(sidecarFile).schema\n          removeSchemaName = checkpointSchema(\"remove\").dataType.asInstanceOf[StructType].fieldNames\n          assert(removeSchemaName.toSeq === expectedRemoveFileSchema)\n          checkAnswer(\n            spark.sql(s\"select * from delta.`$tablePath`\"),\n            Seq(0, 0, 1, 1, 2, 2, 3, 3, 4, 4).map { i => Row(i) })\n        }\n      }\n    }\n  }\n\n  test(\"checkpoint does not contain remove.tags and remove.numRecords\") {\n    withClassicCheckpointPolicyForCatalogOwned {\n      withTempDir { tempDir =>\n        val expectedRemoveFileSchema = Seq(\n          \"path\",\n          \"deletionTimestamp\",\n          \"dataChange\",\n          \"extendedFileMetadata\",\n          \"partitionValues\",\n          \"size\",\n          \"deletionVector\",\n          \"baseRowId\",\n          \"defaultRowCommitVersion\")\n\n        val tablePath = tempDir.getAbsolutePath\n        // Append rows [0, 9] to table and merge tablePath.\n        spark.range(end = 10).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n        spark.range(5, 15).createOrReplaceTempView(\"src\")\n        sql(\n          s\"\"\"\n             |MERGE INTO delta.`$tempDir` t USING src s ON t.id = s.id\n             |WHEN MATCHED THEN DELETE\n             |WHEN NOT MATCHED THEN INSERT *\n             |\"\"\".stripMargin)\n        val deltaLog = DeltaLog.forTable(spark, tablePath)\n        deltaLog.checkpoint()\n        var checkpointFile = FileNames.checkpointFileSingular(deltaLog.logPath, 1).toString\n        var checkpointSchema = spark.read.format(source = \"parquet\").load(checkpointFile).schema\n        var removeSchemaName =\n          checkpointSchema(\"remove\").dataType.asInstanceOf[StructType].fieldNames\n        assert(removeSchemaName.toSeq === expectedRemoveFileSchema)\n        checkAnswer(\n          spark.sql(s\"select * from delta.`$tablePath`\"),\n          Seq(0, 1, 2, 3, 4, 10, 11, 12, 13, 14).map { i => Row(i) })\n        // Append rows [0, 9] to table and merge one more time.\n        spark.range(end = 10).write.format(\"delta\").mode(\"append\").save(tablePath)\n        sql(\n          s\"\"\"\n             |MERGE INTO delta.`$tempDir` t USING src s ON t.id = s.id\n             |WHEN MATCHED THEN DELETE\n             |WHEN NOT MATCHED THEN INSERT *\n             |\"\"\".stripMargin)\n        deltaLog.checkpoint()\n        checkpointFile = FileNames.checkpointFileSingular(deltaLog.logPath, 1).toString\n        checkpointSchema = spark.read.format(source = \"parquet\").load(checkpointFile).schema\n        removeSchemaName = checkpointSchema(\"remove\").dataType.asInstanceOf[StructType].fieldNames\n        assert(removeSchemaName.toSeq === expectedRemoveFileSchema)\n        checkAnswer(\n          spark.sql(s\"select * from delta.`$tablePath`\"),\n          Seq(0, 0, 1, 1, 2, 2, 3, 3, 4, 4).map { i => Row(i) })\n      }\n    }\n  }\n\n  test(\"checkpoint with DVs\") {\n    for (v2Checkpoint <- Seq(true, false))\n    withTempDir { tempDir =>\n      val source = new File(DeletionVectorsSuite.table1Path) // this table has DVs in two versions\n      val targetName = s\"insertTest_${UUID.randomUUID().toString.replace(\"-\", \"\")}\"\n      val target = new File(tempDir, targetName)\n\n      // Copy the source2 DV table to a temporary directory, so that we do updates to it\n      FileUtils.copyDirectory(source, target)\n\n      if (v2Checkpoint) {\n        spark.sql(s\"ALTER TABLE delta.`${target.getAbsolutePath}` SET TBLPROPERTIES \" +\n          s\"('${DeltaConfigs.CHECKPOINT_POLICY.key}' = 'v2')\")\n      }\n\n      sql(s\"ALTER TABLE delta.`${target.getAbsolutePath}` \" +\n        s\"SET TBLPROPERTIES (${DeltaConfigs.CHECKPOINT_INTERVAL.key} = 10)\")\n      def insertData(data: String): Unit = {\n        spark.sql(s\"INSERT INTO TABLE delta.`${target.getAbsolutePath}` $data\")\n      }\n      val newData = Seq.range(3000, 3010)\n      newData.foreach { i => insertData(s\"VALUES($i)\") }\n\n      // Check the target file has checkpoint generated\n      val deltaLog = DeltaLog.forTable(spark, target.getAbsolutePath)\n      verifyCheckpoint(deltaLog.readLastCheckpointFile(), version = 10, parts = None)\n\n      // Delete the commit files 0-9, so that we are forced to read the checkpoint file\n      val logPath = new Path(new File(target, \"_delta_log\").getAbsolutePath)\n      for (i <- 0 to 9) {\n        val file = new File(FileNames.unsafeDeltaFile(logPath, version = i).toString)\n        file.delete()\n      }\n\n      // Make sure the contents are the same\n      import testImplicits._\n      checkAnswer(\n        spark.sql(s\"SELECT * FROM delta.`${target.getAbsolutePath}`\"),\n        (DeletionVectorsSuite.expectedTable1DataV4 ++ newData).toSeq.toDF())\n    }\n  }\n\n\n\n  testDifferentV2Checkpoints(s\"V2 Checkpoint compat file equivalency to normal V2 Checkpoint\") {\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n\n      spark.range(10, 20).write.mode(\"append\").format(\"delta\").save(tempDir.getAbsolutePath)\n\n      deltaLog.checkpoint() // Checkpoint 1\n      val normalCheckpointSnapshot = deltaLog.update()\n\n      deltaLog.createSinglePartCheckpointForBackwardCompat( // Compatibility Checkpoint 1\n        normalCheckpointSnapshot, new deltaLog.V2CompatCheckpointMetrics)\n\n      val allFiles = normalCheckpointSnapshot.allFiles.collect().sortBy(_.path).toList\n      val setTransactions = normalCheckpointSnapshot.setTransactions\n      val numOfFiles = normalCheckpointSnapshot.numOfFiles\n      val numOfRemoves = normalCheckpointSnapshot.numOfRemoves\n      val numOfMetadata = normalCheckpointSnapshot.numOfMetadata\n      val numOfProtocol = normalCheckpointSnapshot.numOfProtocol\n      val actions = normalCheckpointSnapshot.stateDS.collect().toSet\n\n      val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n\n      // Delete the normal V2 Checkpoint so that the snapshot can be initialized\n      // using the compat checkpoint.\n      fs.delete(normalCheckpointSnapshot.checkpointProvider.topLevelFiles.head.getPath)\n\n      DeltaLog.clearCache()\n      val deltaLog2 = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      val compatCheckpointSnapshot = deltaLog2.update()\n      assert(!compatCheckpointSnapshot.checkpointProvider.isEmpty)\n      assert(compatCheckpointSnapshot.checkpointProvider.version ==\n        normalCheckpointSnapshot.checkpointProvider.version)\n      assert(\n        compatCheckpointSnapshot.checkpointProvider.topLevelFiles.head.getPath.getName\n          ==\n          FileNames.checkpointFileSingular(\n            deltaLog2.logPath,\n            normalCheckpointSnapshot.checkpointProvider.version).getName\n      )\n\n      assert(\n        compatCheckpointSnapshot.allFiles.collect().sortBy(_.path).toList\n        == allFiles\n      )\n\n      assert(compatCheckpointSnapshot.setTransactions == setTransactions)\n\n      assert(compatCheckpointSnapshot.stateDS.collect().toSet == actions)\n\n      assert(compatCheckpointSnapshot.numOfFiles == numOfFiles)\n\n      assert(compatCheckpointSnapshot.numOfRemoves == numOfRemoves)\n\n      assert(compatCheckpointSnapshot.numOfMetadata == numOfMetadata)\n\n      assert(compatCheckpointSnapshot.numOfProtocol == numOfProtocol)\n\n      val tableData =\n        spark.sql(s\"SELECT * FROM delta.`${deltaLog.dataPath}` ORDER BY id\")\n          .collect()\n          .map(_.getLong(0))\n      assert(tableData.toSeq == (0 to 19))\n    }\n  }\n\n  testDifferentCheckpoints(\"last checkpoint contains correct schema for v1/v2\" +\n      \" Checkpoints\") { (checkpointPolicy, v2CheckpointFormatOpt) =>\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      deltaLog.checkpoint()\n      val lastCheckpointOpt = deltaLog.readLastCheckpointFile()\n      assert(lastCheckpointOpt.nonEmpty)\n      if (checkpointPolicy.needsV2CheckpointSupport) {\n        if (v2CheckpointFormatOpt.contains(V2Checkpoint.Format.JSON)) {\n          assert(lastCheckpointOpt.get.checkpointSchema.isEmpty)\n        } else {\n          assert(lastCheckpointOpt.get.checkpointSchema.nonEmpty)\n          assert(lastCheckpointOpt.get.checkpointSchema.get.fieldNames.toSeq ===\n            Seq(\"txn\", \"add\", \"remove\", \"metaData\", \"protocol\",\n              \"domainMetadata\", \"checkpointMetadata\", \"sidecar\"))\n        }\n      } else {\n        assert(lastCheckpointOpt.get.checkpointSchema.nonEmpty)\n        assert(lastCheckpointOpt.get.checkpointSchema.get.fieldNames.toSeq ===\n          Seq(\"txn\", \"add\", \"remove\", \"metaData\", \"protocol\", \"domainMetadata\"))\n      }\n    }\n  }\n\n  test(\"last checkpoint - v2 checkpoint fields threshold\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      spark.range(1).write.format(\"delta\").save(tablePath)\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      // Enable v2Checkpoint table feature.\n      spark.sql(s\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES \" +\n        s\"('${DeltaConfigs.CHECKPOINT_POLICY.key}' = 'v2')\")\n\n      def writeCheckpoint(\n        adds: Int,\n        nonFileActionThreshold: Int,\n        sidecarActionThreshold: Int): LastCheckpointInfo = {\n        withSQLConf(\n          DeltaSQLConf.LAST_CHECKPOINT_NON_FILE_ACTIONS_THRESHOLD.key -> s\"$nonFileActionThreshold\",\n          DeltaSQLConf.LAST_CHECKPOINT_SIDECARS_THRESHOLD.key -> s\"$sidecarActionThreshold\"\n        ) {\n          val addFiles = (1 to adds).map(_ =>\n            createTestAddFile(\n              encodedPath = java.util.UUID.randomUUID.toString,\n              partitionValues = Map(),\n              size = 128L\n            ))\n          deltaLog.startTransaction().commit(addFiles, DeltaOperations.ManualUpdate)\n          deltaLog.checkpoint()\n        }\n        val lastCheckpointInfoOpt = deltaLog.readLastCheckpointFile()\n        assert(lastCheckpointInfoOpt.nonEmpty)\n        lastCheckpointInfoOpt.get\n      }\n\n      // For CCv1.5 table, row tracking is enabled by default, there will be an extra\n      // DomainMetadata added by RowTracking as a non file action.\n      val domainMetadataAddedByRowTracking = if (catalogOwnedDefaultCreationEnabledInTests) 1 else 0\n      // Append 1 AddFile [AddFile-2]\n      val lc1 = writeCheckpoint(adds = 1, nonFileActionThreshold = 10, sidecarActionThreshold = 10)\n      assert(lc1.v2Checkpoint.nonEmpty)\n      // 3 non file actions - protocol/metadata/checkpointMetadata, 1 sidecar\n      assert(\n        lc1.v2Checkpoint.get.nonFileActions.get.size === 3\n          + domainMetadataAddedByRowTracking\n      )\n      assert(lc1.v2Checkpoint.get.sidecarFiles.get.size === 1)\n\n      // Append 1 SetTxn, 8 more AddFiles [SetTxn-1, AddFile-10]\n      deltaLog.startTransaction()\n        .commit(Seq(SetTransaction(\"app-1\", 2, None)), DeltaOperations.ManualUpdate)\n      val lc2 = writeCheckpoint(\n        adds = 8,\n        sidecarActionThreshold = 10,\n        nonFileActionThreshold = 4\n          + domainMetadataAddedByRowTracking\n      )\n      assert(lc2.v2Checkpoint.nonEmpty)\n      // 4 non file actions - protocol/metadata/checkpointMetadata/setTxn, 1 sidecar\n      assert(\n        lc2.v2Checkpoint.get.nonFileActions.get.size === 4\n          + domainMetadataAddedByRowTracking\n      )\n      assert(lc2.v2Checkpoint.get.sidecarFiles.get.size === 1)\n\n      // Append 10 more AddFiles [SetTxn-1, AddFile-20]\n      val lc3 = writeCheckpoint(adds = 10, nonFileActionThreshold = 3, sidecarActionThreshold = 10)\n      assert(lc3.v2Checkpoint.nonEmpty)\n      // non-file actions exceeded threshold, 1 sidecar\n      assert(lc3.v2Checkpoint.get.nonFileActions.isEmpty)\n      assert(lc3.v2Checkpoint.get.sidecarFiles.get.size === 1)\n\n      withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"5\") {\n        // Append 10 more AddFiles [SetTxn-1, AddFile-30]\n        val lc4 =\n          writeCheckpoint(adds = 10, nonFileActionThreshold = 3, sidecarActionThreshold = 10)\n        assert(lc4.v2Checkpoint.nonEmpty)\n        // non-file actions exceeded threshold\n        // total 30 file actions, across 6 sidecar files (5 actions per file)\n        assert(lc4.v2Checkpoint.get.nonFileActions.isEmpty)\n        assert(lc4.v2Checkpoint.get.sidecarFiles.get.size === 6)\n      }\n\n      withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"2\") {\n        // Append 0 AddFiles [SetTxn-1, AddFile-30]\n        val lc5 =\n          writeCheckpoint(adds = 0, nonFileActionThreshold = 10, sidecarActionThreshold = 10)\n        assert(lc5.v2Checkpoint.nonEmpty)\n        // 4 non file actions - protocol/metadata/checkpointMetadata/setTxn\n        // total 30 file actions, across 15 sidecar files (2 actions per file)\n        assert(\n          lc5.v2Checkpoint.get.nonFileActions.get.size === 4\n            + domainMetadataAddedByRowTracking\n        )\n        assert(lc5.v2Checkpoint.get.sidecarFiles.isEmpty)\n      }\n    }\n  }\n\n  def checkIntermittentError(\n      tempDir: File, lastCheckpointMissing: Boolean, crcMissing: Boolean): Unit = {\n    // Create a table with commit version 0, 1 and a checkpoint.\n    val tablePath = tempDir.getAbsolutePath\n    spark.range(10).write.format(\"delta\").save(tablePath)\n    spark.sql(s\"INSERT INTO delta.`$tablePath`\" +\n      s\"SELECT * FROM delta.`$tablePath` WHERE id = 1\").collect()\n\n    val log = DeltaLog.forTable(spark, tablePath)\n    val conf = log.newDeltaHadoopConf()\n    log.checkpoint()\n\n    // Delete _last_checkpoint based on test configuration.\n    val fs = log.logPath.getFileSystem(conf)\n    if (lastCheckpointMissing) {\n      fs.delete(log.LAST_CHECKPOINT)\n    }\n    // Delete CRC file based on test configuration.\n    if (crcMissing) {\n      // Delete all CRC files\n      (0L to log.update().version).foreach { version =>\n        fs.delete(FileNames.checksumFile(log.logPath, version))\n      }\n    }\n\n    // In order to trigger an intermittent failure while reading checkpoint, this test corrupts\n    // the checkpoint temporarily so that json/parquet checkpoint reader fails. The corrupted\n    // file is written with same length so that when the file is uncorrupted in future, then we\n    // can test that delta is able to read that file and produce correct results. If the \"bad\" file\n    // is not of same length, then the read with \"good\" file will also fail as parquet reader will\n    // use the cache file status's getLen to find out where the footer is and will fail after not\n    // finding the magic bytes.\n    val checkpointFileStatus =\n    log.listFrom(0).filter(FileNames.isCheckpointFile).toSeq.head\n    // Rename the correct checkpoint to a temp path and create a checkpoint with character 'r'\n    // repeated.\n    val tempPath = checkpointFileStatus.getPath.suffix(\".temp\")\n    fs.rename(checkpointFileStatus.getPath, tempPath)\n    val randomContentToWrite = Seq(\"r\" * (checkpointFileStatus.getLen.toInt - 1)) // + 1 (\\n)\n    log.store.write(\n      checkpointFileStatus.getPath, randomContentToWrite.toIterator, overwrite = true, conf)\n    assert(log.store.read(checkpointFileStatus.getPath, conf) === randomContentToWrite)\n    assert(fs.getFileStatus(tempPath).getLen === checkpointFileStatus.getLen)\n\n    DeltaLog.clearCache()\n    if (!crcMissing) {\n      // When CRC is present, then P&M will be taken from CRC and snapshot will be initialized\n      // without needing a checkpoint. But the underlying checkpoint provider points to a\n      // corrupted checkpoint and so any query/state reconstruction on this will fail.\n      intercept[Exception] {\n        sql(s\"SELECT * FROM delta.`$tablePath`\").collect()\n        DeltaLog.forTable(spark, tablePath).unsafeVolatileSnapshot.validateChecksum()\n      }\n      val snapshot = DeltaLog.forTable(spark, tablePath).unsafeVolatileSnapshot\n      intercept[Exception] {\n        snapshot.allFiles.collect()\n      }\n      // Undo the corruption\n      assert(fs.delete(checkpointFileStatus.getPath, true))\n      assert(fs.rename(tempPath, checkpointFileStatus.getPath))\n\n      // Once the corruption in undone, then the queries starts passing on top of same snapshot.\n      // This tests that we have not caches the intermittent error in the underlying checkpoint\n      // provider.\n      sql(s\"SELECT * FROM delta.`$tablePath`\").collect()\n      assert(DeltaLog.forTable(spark, tablePath).update() === snapshot)\n      return\n    }\n    // When CRC is missing, then P&M will be taken from checkpoint which is temporarily\n    // corrupted, so we will end up creating a new snapshot without using checkpoint and the\n    // query will succeed.\n    sql(s\"SELECT * FROM delta.`$tablePath`\").collect()\n    val snapshot = DeltaLog.forTable(spark, tablePath).unsafeVolatileSnapshot\n    snapshot.computeChecksum\n    snapshot.validateChecksum()\n    assert(snapshot.checkpointProvider.isEmpty)\n  }\n\n\n  /**\n   * Writes all actions in the top-level file of a new V2 Checkpoint. No sidecar files are\n   * written.\n   */\n  private def writeAllActionsInV2Manifest(\n      snapshot: Snapshot,\n      v2CheckpointFormat: V2Checkpoint.Format): Path = {\n    snapshot.ensureCommitFilesBackfilled()\n    val checkpointMetadata = CheckpointMetadata(version = snapshot.version)\n    val actionsDS = snapshot.stateDS\n      .where(\"checkpointMetadata is null and \" +\n        \"commitInfo is null and cdc is null and sidecar is null\")\n      .union(spark.createDataset(Seq(checkpointMetadata.wrap)))\n      .toDF()\n\n    val actionsToWrite = Checkpoints\n      .buildCheckpoint(actionsDS, snapshot)\n      .as[SingleAction]\n      .collect()\n      .toSeq\n      .map(_.unwrap)\n\n    val deltaLog = snapshot.deltaLog\n    val (v2CheckpointPath, _) =\n      if (v2CheckpointFormat == V2Checkpoint.Format.JSON) {\n        val v2CheckpointPath =\n          FileNames.newV2CheckpointJsonFile(deltaLog.logPath, snapshot.version)\n        deltaLog.store.write(\n          v2CheckpointPath,\n          actionsToWrite.map(_.json).toIterator,\n          overwrite = true,\n          hadoopConf = deltaLog.newDeltaHadoopConf())\n        (v2CheckpointPath, None)\n      } else if (v2CheckpointFormat == V2Checkpoint.Format.PARQUET) {\n        val sparkSession = spark\n        // scalastyle:off sparkimplicits\n        import sparkSession.implicits._\n        // scalastyle:on sparkimplicits\n        val dfToWrite = actionsToWrite.map(_.wrap).toDF()\n        val v2CheckpointPath =\n          FileNames.newV2CheckpointParquetFile(deltaLog.logPath, snapshot.version)\n        val schemaOfDfWritten =\n          Checkpoints.createCheckpointV2ParquetFile(\n            spark,\n            dfToWrite,\n            v2CheckpointPath,\n            deltaLog.newDeltaHadoopConf(),\n            false)\n        (v2CheckpointPath, Some(schemaOfDfWritten))\n      } else {\n        throw DeltaErrors.assertionFailedError(\n          s\"Unrecognized checkpoint V2 format: $v2CheckpointFormat\")\n      }\n    v2CheckpointPath\n  }\n\n  for (checkpointFormat <- V2Checkpoint.Format.ALL)\n  test(s\"All actions in V2 manifest [v2CheckpointFormat: ${checkpointFormat.name}]\") {\n    withSQLConf(\n      DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name) {\n      withTempDir { dir =>\n        spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        spark.sql(s\"INSERT INTO delta.`${log.dataPath}` VALUES (2718);\")\n        log\n        .startTransaction()\n        .commit(Seq(SetTransaction(\"app-1\", 2, None)), DeltaOperations.ManualUpdate)\n\n        val snapshot = log.update()\n        val allFiles = snapshot.allFiles.collect().toSet\n        val setTransactions = snapshot.setTransactions.toSet\n        val numOfFiles = snapshot.numOfFiles\n        val numOfRemoves = snapshot.numOfRemoves\n        val numOfMetadata = snapshot.numOfMetadata\n        val numOfProtocol = snapshot.numOfProtocol\n        val actions = snapshot.stateDS.collect().toSet\n\n        assert(snapshot.version == 2)\n\n        writeAllActionsInV2Manifest(snapshot, checkpointFormat)\n\n        DeltaLog.clearCache()\n        val checkpointSnapshot = log.update()\n\n        assert(!checkpointSnapshot.checkpointProvider.isEmpty)\n\n        assert(checkpointSnapshot.checkpointProvider.version == 2)\n\n        // Check the integrity of the data in the checkpoint-backed table.\n        val data = spark\n          .sql(s\"SELECT * FROM delta.`${log.dataPath}` ORDER BY ID;\")\n          .collect()\n          .map(_.getLong(0))\n\n        val expectedData = ((0 to 9).toList :+ 2718).toArray\n        assert(data sameElements expectedData)\n        assert(checkpointSnapshot.setTransactions.toSet == setTransactions)\n\n        assert(checkpointSnapshot.stateDS.collect().toSet == actions)\n\n        assert(checkpointSnapshot.numOfFiles == numOfFiles)\n\n        assert(checkpointSnapshot.numOfRemoves == numOfRemoves)\n\n        assert(checkpointSnapshot.numOfMetadata == numOfMetadata)\n\n        assert(checkpointSnapshot.numOfProtocol == numOfProtocol)\n\n        assert(checkpointSnapshot.allFiles.collect().toSet == allFiles)\n      }\n    }\n  }\n  for (lastCheckpointMissing <- BOOLEAN_DOMAIN)\n  testDifferentCheckpoints(\"intermittent error while reading checkpoint should not\" +\n      s\" stick to snapshot [lastCheckpointMissing: $lastCheckpointMissing]\") { (_, _) =>\n    withTempDir { tempDir =>\n      withSQLConf(\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_ENABLED.key -> \"false\"\n      ) {\n        checkIntermittentError(tempDir, lastCheckpointMissing, crcMissing = true)\n      }\n    }\n  }\n\n  test(\"validate metadata cleanup is not called with createCheckpointAtVersion API\") {\n    withTempDir { dir =>\n      val usageRecords1 = Log4jUsageLogger.track {\n        spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        log.createCheckpointAtVersion(0)\n      }\n      assert(filterUsageRecords(usageRecords1, \"delta.log.cleanup\").size === 0L)\n\n      val usageRecords2 = Log4jUsageLogger.track {\n        spark.range(10).write.mode(\"overwrite\").format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        log.checkpoint()\n\n      }\n      assert(filterUsageRecords(usageRecords2, \"delta.log.cleanup\").size > 0)\n    }\n  }\n\n  testDifferentCheckpoints(\"Ensure variant stats in checkpoint\") { (policy, _) =>\n    // Test all combinations of (writeStatsAsJson, writeStatsAsStruct)\n    // Skip (false, false) as that would have no stats at all\n    val combinations = Seq(\n      (true, false),\n      (false, true),\n      (true, true)\n    )\n\n    // Test with collectVariantStats = false and true\n    Seq(false, true).foreach { collectVariantStats =>\n      combinations.foreach { case (writeStatsAsJson, writeStatsAsStruct) =>\n        withClue(s\"collectVariantStats=$collectVariantStats, \" +\n            s\"writeStatsAsJson=$writeStatsAsJson, writeStatsAsStruct=$writeStatsAsStruct\") {\n          withSQLConf(\n            DeltaSQLConf.COLLECT_VARIANT_DATA_SKIPPING_STATS.key -> collectVariantStats.toString\n          ) {\n            withTempDir { tempDir =>\n              // Load golden table with variant stats (no checkpoint)\n              val source = new File(\"src/test/resources/delta/variant-stats-no-checkpoint\")\n              val target = new File(tempDir, \"variant-stats-table\")\n\n              FileUtils.copyDirectory(source, target)\n\n              val tablePath = target.getAbsolutePath\n\n              // Set the stats configuration via ALTER TABLE\n              spark.sql(s\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES \" +\n                s\"('${DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_JSON.key}' = '$writeStatsAsJson', \" +\n                s\"'${DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.key}' = '$writeStatsAsStruct')\")\n\n              if (policy == CheckpointPolicy.V2) {\n                spark.sql(s\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES \" +\n                  s\"('${DeltaConfigs.CHECKPOINT_POLICY.key}' = 'v2')\")\n              }\n\n              val deltaLog = DeltaLog.forTable(spark, tablePath)\n              val snapshot = deltaLog.update()\n\n              deltaLog.checkpoint(snapshot)\n              val checkpointFile = if (policy.needsV2CheckpointSupport) {\n                val provider = getV2CheckpointProvider(deltaLog)\n                provider.sidecarFileStatuses.head.getPath\n              } else {\n                FileNames.checkpointFileSingular(deltaLog.logPath, deltaLog.snapshot.version)\n              }\n\n              val checkpointDf = spark.read.format(\"parquet\").load(checkpointFile.toString)\n                .filter(col(\"add\").isNotNull)\n\n              // Helper function to decode Z85 and get variant JSON\n              def decodeZ85ToVariantJson(z85String: String): String = {\n                val decoded = Codec.Base85Codec.decodeBytes(z85String, z85String.length)\n                val metadataSize = VariantStatsShims.metadataSize(decoded)\n                val value = decoded.slice(metadataSize, decoded.length)\n                val variant = new Variant(value, decoded)\n                variant.toJson(java.time.ZoneId.of(\"UTC\"))\n              }\n\n              // Verify stats in add.stats (JSON format) when writeStatsAsJson=true\n              if (writeStatsAsJson) {\n                val checkpointStatsJson = checkpointDf\n                  .selectExpr(\n                    s\"get_json_object(add.stats, '$$.minValues.v')\",\n                    s\"get_json_object(add.stats, '$$.maxValues.v')\",\n                    s\"get_json_object(add.stats, '$$.minValues.nv.v')\",\n                    s\"get_json_object(add.stats, '$$.maxValues.nv.v')\").collect().head\n\n                // Verify top-level variant column stats\n                val actualMinTopLevel = decodeZ85ToVariantJson(checkpointStatsJson.getString(0))\n                val actualMaxTopLevel = decodeZ85ToVariantJson(checkpointStatsJson.getString(1))\n                assert(actualMinTopLevel == \"\"\"{\"$['id']\":0,\"$['name']\":\"1\"}\"\"\")\n                assert(actualMaxTopLevel == \"\"\"{\"$['id']\":9,\"$['name']\":\"9\"}\"\"\")\n\n                // Verify nested variant column stats\n                val actualMinNested = decodeZ85ToVariantJson(checkpointStatsJson.getString(2))\n                val actualMaxNested = decodeZ85ToVariantJson(checkpointStatsJson.getString(3))\n                assert(actualMinNested == \"\"\"{\"$['id']\":10,\"$['name']\":\"11\"}\"\"\")\n                assert(actualMaxNested == \"\"\"{\"$['id']\":19,\"$['name']\":\"20\"}\"\"\")\n              }\n\n              // Verify stats in add.stats_parsed (struct format) when writeStatsAsStruct=true\n              if (writeStatsAsStruct) {\n                if (collectVariantStats) {\n                  val checkpointStatsParsed = checkpointDf\n                    .selectExpr(\n                      \"add.stats_parsed.minValues.v\",\n                      \"add.stats_parsed.maxValues.v\",\n                      \"add.stats_parsed.minValues.nv.v\",\n                      \"add.stats_parsed.maxValues.nv.v\").collect().head\n\n                  // Verify top-level variant column stats\n                  val minVariantTopLevel = checkpointStatsParsed.getAs[VariantVal](0)\n                  val maxVariantTopLevel = checkpointStatsParsed.getAs[VariantVal](1)\n                  val minTopLevelVariant =\n                    new Variant(minVariantTopLevel.getValue, minVariantTopLevel.getMetadata)\n                  val maxTopLevelVariant =\n                    new Variant(maxVariantTopLevel.getValue, maxVariantTopLevel.getMetadata)\n                  assert(minTopLevelVariant.toJson(java.time.ZoneId.of(\"UTC\")) ==\n                    \"\"\"{\"$['id']\":0,\"$['name']\":\"1\"}\"\"\")\n                  assert(maxTopLevelVariant.toJson(java.time.ZoneId.of(\"UTC\")) ==\n                    \"\"\"{\"$['id']\":9,\"$['name']\":\"9\"}\"\"\")\n\n                  // Verify nested variant column stats\n                  val minVariantNested = checkpointStatsParsed.getAs[VariantVal](2)\n                  val maxVariantNested = checkpointStatsParsed.getAs[VariantVal](3)\n                  val minNestedVariant =\n                    new Variant(minVariantNested.getValue, minVariantNested.getMetadata)\n                  val maxNestedVariant =\n                    new Variant(maxVariantNested.getValue, maxVariantNested.getMetadata)\n                  assert(minNestedVariant.toJson(java.time.ZoneId.of(\"UTC\")) ==\n                    \"\"\"{\"$['id']\":10,\"$['name']\":\"11\"}\"\"\")\n                  assert(maxNestedVariant.toJson(java.time.ZoneId.of(\"UTC\")) ==\n                    \"\"\"{\"$['id']\":19,\"$['name']\":\"20\"}\"\"\")\n                } else {\n                  // When collectVariantStats=false, variant columns should not be in stats_parsed\n                  val statsParsedSchema = checkpointDf\n                    .select(\"add.stats_parsed.minValues\", \"add.stats_parsed.maxValues\")\n                    .schema\n                  val minValuesFields = statsParsedSchema(\"minValues\").dataType\n                    .asInstanceOf[StructType].fieldNames\n                  val maxValuesFields = statsParsedSchema(\"maxValues\").dataType\n                    .asInstanceOf[StructType].fieldNames\n                  assert(!minValuesFields.contains(\"v\"),\n                    \"minValues should not contain 'v' when collectVariantStats=false\")\n                  assert(!maxValuesFields.contains(\"v\"),\n                    \"maxValues should not contain 'v' when collectVariantStats=false\")\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  testDifferentCheckpoints(\"Ensure variant stats are preserved during state reconstruction\") {\n    case (_, _) =>\n    // Test different combinations of (writeStatsAsJson, writeStatsAsStruct)\n    // The golden table contains variant stats but NO checkpoint.\n    // We set the checkpoint properties and create the checkpoint in this test.\n    val combinations = Seq(\n      (\"true\", \"false\"),\n      (\"false\", \"true\"),\n      (\"true\", \"true\")\n      // Note: (\"false\", \"false\") would have no stats at all, so not testing it\n    )\n\n    // Expected Z85-encoded value for variant `0` (the golden table contains `id::variant`\n    // where id=0)\n    // This is the Z85 encoding of the variant binary representation of integer 0\n    val expectedZ85 = \"0rAf3bMW#D00%Fx0000000000\"\n\n    combinations.foreach { case (jsonStats, structStats) =>\n      withClue(s\"writeStatsAsJson=$jsonStats, writeStatsAsStruct=$structStats\") {\n        withTempDir { tempDir =>\n          // Copy golden table to temp directory\n          val source = new File(\"src/test/resources/delta/variant-stats-state-reconstruction\")\n          val target = new File(tempDir, \"variant-stats-table\")\n          FileUtils.copyDirectory(source, target)\n\n          val tablePath = target.getAbsolutePath\n\n          // Set checkpoint properties\n          spark.sql(\n            s\"\"\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES (\n            |  'delta.checkpoint.writeStatsAsJson' = '$jsonStats',\n            |  'delta.checkpoint.writeStatsAsStruct' = '$structStats'\n            |)\"\"\".stripMargin)\n\n          // Create checkpoint with the new properties\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          deltaLog.checkpoint(deltaLog.update())\n\n          // Clear cache to ensure fresh state reconstruction from checkpoint\n          DeltaLog.clearCache()\n\n          val snapshot = deltaLog.update()\n\n          // Get the reconstructed state and verify variant stats are present\n          val addFilesWithStats = snapshot.stateDS\n            .filter(\"add IS NOT NULL\")\n            .filter(\"add.stats IS NOT NULL AND add.stats != ''\")\n            .collect()\n\n          assert(\n            addFilesWithStats.nonEmpty,\n            s\"Expected at least one AddFile with stats for \" +\n              s\"writeStatsAsJson=$jsonStats, writeStatsAsStruct=$structStats\")\n\n          // Verify that the stats contain the expected Z85-encoded variant\n          val statsContainZ85 = addFilesWithStats.exists { action =>\n            val stats = action.add.stats\n            stats != null && stats.contains(expectedZ85)\n          }\n\n          assert(\n            statsContainZ85,\n            s\"Expected stats to contain Z85-encoded variant '$expectedZ85' for \" +\n              s\"writeStatsAsJson=$jsonStats, writeStatsAsStruct=$structStats. \" +\n              s\"Actual stats: ${addFilesWithStats.map(_.add.stats).mkString(\", \")}\")\n        }\n      }\n    }\n  }\n}\n\nclass OverwriteTrackingLogStore(sparkConf: SparkConf, hadoopConf: Configuration)\n  extends LocalLogStore(sparkConf, hadoopConf) {\n\n  var fileToOverwriteCount: Map[Path, Long] = Map[Path, Long]()\n\n  private var isPartialWriteVisibleBool: Boolean = false\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): Boolean =\n    isPartialWriteVisibleBool\n\n  override def write(\n      path: Path,\n      actions: Iterator[String],\n      overwrite: Boolean,\n      hadoopConf: Configuration): Unit = {\n    val toAdd = if (overwrite) 1 else 0\n    fileToOverwriteCount += path -> (fileToOverwriteCount.getOrElse(path, 0L) + toAdd)\n    super.write(path, actions, overwrite, hadoopConf)\n  }\n\n  def clearCounts(): Unit = {\n    fileToOverwriteCount = Map[Path, Long]()\n  }\n\n  def setPartialWriteVisible(isPartialWriteVisibleBool: Boolean): Unit = {\n    this.isPartialWriteVisibleBool = isPartialWriteVisibleBool\n  }\n}\n\nclass V2CheckpointManifestOverwriteSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaCheckpointTestUtils\n  with DeltaSQLCommandTest {\n  protected override def sparkConf = {\n    // Set the logStore to OverwriteTrackingLogStore.\n    super.sparkConf\n      .set(\"spark.delta.logStore.class\", classOf[OverwriteTrackingLogStore].getName)\n      .set(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key, V2Checkpoint.Format.JSON.name)\n  }\n  for (isPartialWriteVisible <- BOOLEAN_DOMAIN)\n  test(\"v2 checkpoint manifest write should use the logstore.write(overwrite) API correctly \" +\n      s\"isPartialWriteVisible = $isPartialWriteVisible\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      // Create a simple table with V2 checkpoints enabled and json manifest.\n      spark.range(10).write.format(\"delta\").save(tablePath)\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      spark.sql(s\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES \" +\n          s\"('${DeltaConfigs.CHECKPOINT_POLICY.key}' = 'v2')\")\n      val store = deltaLog.store.asInstanceOf[OverwriteTrackingLogStore]\n\n      store.clearCounts()\n      store.setPartialWriteVisible(isPartialWriteVisible)\n      deltaLog.checkpoint()\n\n      val snapshot = deltaLog.update()\n      assert(snapshot.checkpointProvider.version == 1)\n      // Two writes will use logStore.write:\n      // 1. Checkpoint Manifest\n      // 2. LAST_CHECKPOINT.\n      assert(store.fileToOverwriteCount.size == 2)\n      val manifestWriteRecord = store.fileToOverwriteCount.find {\n        case (path, _) => FileNames.isCheckpointFile(path)\n      }.getOrElse(fail(\"expected checkpoint manifest write using logStore.write\"))\n      val numOverwritesExpected = if (isPartialWriteVisible) 0 else 1\n      assert(manifestWriteRecord._2 == numOverwritesExpected)\n    }\n  }\n}\n\n/** A fake GCS file system to verify delta checkpoints are written in a separate gcs thread. */\nclass FakeGCSFileSystemValidatingCheckpoint extends RawLocalFileSystem {\n  override def getScheme: String = \"gs\"\n  override def getUri: URI = URI.create(\"gs:/\")\n\n  protected def shouldValidateFilePattern(f: Path): Boolean = f.getName.contains(\".checkpoint\")\n\n  protected def assertGCSThread(f: Path): Unit = {\n    if (shouldValidateFilePattern(f)) {\n      assert(\n        Thread.currentThread().getName.contains(\"delta-gcs-\"),\n        s\"writing $f was happening in non gcs thread: ${Thread.currentThread()}\")\n    }\n  }\n\n  override def create(\n      f: Path,\n      permission: FsPermission,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream = {\n    assertGCSThread(f)\n    super.create(f, permission, overwrite, bufferSize, replication, blockSize, progress)\n  }\n\n  override def create(\n      f: Path,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream = {\n    assertGCSThread(f)\n    super.create(f, overwrite, bufferSize, replication, blockSize, progress)\n  }\n}\n\n/** A fake GCS file system to verify delta commits are written in a separate gcs thread. */\nclass FakeGCSFileSystemValidatingCommits extends FakeGCSFileSystemValidatingCheckpoint {\n  override protected def shouldValidateFilePattern(f: Path): Boolean = f.getName.contains(\".json\")\n}\n\nclass CheckpointsWithCatalogOwnedBatch1Suite extends CheckpointsSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass CheckpointsWithCatalogOwnedBatch2Suite extends CheckpointsSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass CheckpointsWithCatalogOwnedBatch100Suite extends CheckpointsSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ChecksumDVMetricsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.{DeletedRecordCountsHistogram, DeletedRecordCountsHistogramUtils}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.DeltaEncoder\n\nimport org.apache.spark.sql.{DataFrame, Encoder, QueryTest, Row}\nimport org.apache.spark.sql.functions.{coalesce, col, count, lit, sum}\nimport org.apache.spark.sql.test.SharedSparkSession\n\ncase class StatsSchema(\n    numDeletedRecords: Long,\n    numDeletionVectors: Long,\n    deletedRecordCountsHistogramOpt: Option[DeletedRecordCountsHistogram])\n\nclass ChecksumDVMetricsSuite\n  extends QueryTest\n    with SharedSparkSession\n    with DeletionVectorsTestUtils\n    with DeltaSQLCommandTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark.conf)\n  }\n\n  protected implicit def statsSchemaEncoder: Encoder[StatsSchema] =\n    (new DeltaEncoder[StatsSchema]).get\n\n  /*\n   * Compare statistics in the checksum by comparing to the log statistics.\n   */\n  protected def validateChecksum(\n      snapshot: Snapshot,\n      statisticsExpected: Boolean = true,\n      histogramEnabled: Boolean = true): Unit = {\n    val checksum = snapshot.checksumOpt match {\n      case Some(checksum) => checksum\n      case None => snapshot.computeChecksum\n    }\n\n    if (statisticsExpected) {\n      val histogramAggregationOpt =\n        if (histogramEnabled) {\n          Some(DeletedRecordCountsHistogramUtils.histogramAggregate(\n            coalesce(col(\"deletionVector.cardinality\"), lit(0L))\n          ).as(\"deletedRecordCountsHistogramOpt\"))\n        } else {\n          Some(lit(null)\n            .cast(DeletedRecordCountsHistogram.schema)\n            .as(\"deletedRecordCountsHistogramOpt\"))\n        }\n\n      val aggregations = Seq(\n        sum(coalesce(col(\"deletionVector.cardinality\"), lit(0))).as(\"numDeletedRecords\"),\n        count(col(\"deletionVector\")).as(\"numDeletionVectors\")) ++\n        histogramAggregationOpt\n\n      val stats = snapshot.withStatsDeduplicated\n        .select(aggregations: _*)\n        .as[StatsSchema]\n        .first()\n\n      val numDeletedRecords = stats.numDeletedRecords\n      val numDeletionVectors = stats.numDeletionVectors\n      val deletionVectorHistogram = stats.deletedRecordCountsHistogramOpt\n\n      assert(checksum.numDeletedRecordsOpt === Some(numDeletedRecords))\n      assert(checksum.numDeletionVectorsOpt === Some(numDeletionVectors))\n      assert(checksum.deletedRecordCountsHistogramOpt === deletionVectorHistogram)\n    } else {\n      assert(checksum.numDeletedRecordsOpt === None)\n      assert(checksum.numDeletionVectorsOpt === None)\n      assert(checksum.deletedRecordCountsHistogramOpt === None)\n    }\n  }\n\n  protected def runMerge(\n      target: io.delta.tables.DeltaTable,\n      source: DataFrame,\n      deleteFromID: Int): Unit = {\n    target.as(\"t\").merge(source.as(\"s\"), \"t.id = s.id\")\n      .whenMatched(s\"s.id >= ${deleteFromID}\").delete()\n      .execute()\n  }\n\n  protected def runDelete(\n      target: io.delta.tables.DeltaTable,\n      source: DataFrame = null,\n      deleteFromID: Int): Unit = {\n    target.delete(s\"id >= ${deleteFromID}\")\n  }\n\n  protected def runUpdate(\n      target: io.delta.tables.DeltaTable,\n      source: DataFrame = null,\n      deleteFromID: Int): Unit = {\n    target.update(col(\"id\") >= lit(deleteFromID), Map(\"v\" -> lit(-1)))\n  }\n\n  for {\n    enableDVsOnTableDefault <- BOOLEAN_DOMAIN\n    enableDVCreation <- BOOLEAN_DOMAIN\n    enableIncrementalCommit <- BOOLEAN_DOMAIN\n    allowDVsOnOperation <- BOOLEAN_DOMAIN\n  } test(s\"Commit checksum captures DV statistics \" +\n      s\"enableDVsOnTableDefault: ${enableDVsOnTableDefault} \" +\n      s\"enableDVCreation: ${enableDVCreation} \" +\n      s\"enableIncrementalCommit: ${enableIncrementalCommit} \" +\n      s\"allowDVsOnOperation: ${allowDVsOnOperation}\") {\n    val targetDF = createTestDF(0, 100, 2)\n    val sourceDF = targetDF\n\n    val operations: Seq[(io.delta.tables.DeltaTable, DataFrame, Int) => Unit] =\n      Seq(runMerge, runDelete, runUpdate)\n\n    // We validate checksum validation for different feature combinations.\n    for (runOperation <- operations) {\n      withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey ->\n          enableDVsOnTableDefault.toString,\n        DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> enableIncrementalCommit.toString) {\n\n        withTempDeltaTable(targetDF, enableDVs = enableDVCreation) { (targetTable, targetLog) =>\n          validateChecksum(targetLog.update(), enableDVCreation)\n\n          // The first operation only deletes half the records from the second file.\n          runOperation(targetTable(), sourceDF, 75)\n          validateChecksum(targetLog.update(), enableDVCreation)\n\n          // The second operation deletes the remaining records from the second file.\n          withSQLConf(\n            DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key ->\n              allowDVsOnOperation.toString,\n            DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS.key ->\n              allowDVsOnOperation.toString) {\n            runOperation(targetTable(), sourceDF, 50)\n            validateChecksum(targetLog.update(), enableDVCreation)\n          }\n        }\n      }\n    }\n  }\n\n  test(s\"Verify checksum DV statistics are not produced when the relevant config is disabled\") {\n    val targetDF = createTestDF(0, 100, 2)\n\n    withSQLConf(DeltaSQLConf.DELTA_DELETED_RECORD_COUNTS_HISTOGRAM_ENABLED.key -> false.toString) {\n      withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n        runDelete(targetTable(), deleteFromID = 75)\n        validateChecksum(targetLog.update(), histogramEnabled = false)\n      }\n    }\n  }\n\n  for {\n    enableDVCreation <- BOOLEAN_DOMAIN\n    enableIncrementalCommit <- BOOLEAN_DOMAIN\n  } test(s\"Checksum is backward compatible \" +\n      s\"enableDVCreation: $enableDVCreation \" +\n      s\"enableIncrementalCommit: $enableIncrementalCommit\") {\n    val targetDF = createTestDF(0, 100, 2)\n    withSQLConf(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> enableIncrementalCommit.toString) {\n      withTempDeltaTable(targetDF, enableDVs = enableDVCreation) { (targetTable, targetLog) =>\n        validateChecksum(targetLog.update(), statisticsExpected = enableDVCreation)\n\n        runDelete(targetTable(), deleteFromID = 75)\n        validateChecksum(targetLog.update(), statisticsExpected = enableDVCreation)\n\n        // Flip DV setting on an existing table.\n        enableDeletionVectorsInTable(targetLog, enable = !enableDVCreation)\n\n        runDelete(targetTable(), deleteFromID = 50)\n\n        // When INCREMENTAL_COMMIT_ENABLED, enabling DVs midway would normally yield\n        // empty stats. This is due to the incremental nature of the computation and due to the\n        // fact we do not store stats for tables with no DVs. However, in this scenario we try\n        // to take advantage any recent snapshot reconstruction and harvest the stats from there.\n        // In the opposite scenario, disabling DVs midway, we maintain the previously computed\n        // statistics so we do not lose incrementality if DVs are enabled again.\n        // When incremental commit is disabled, both enabling and disabling DVs\n        // midway is not an issue. When DVs are enabled we produce results and when DVs are\n        // disabled we do not.\n        validateChecksum(targetLog.update(),\n          statisticsExpected = !(enableDVCreation && !enableIncrementalCommit))\n      }\n    }\n  }\n\n  test(\"Checksum is computed in incremental commit when full state recomputation is triggered\") {\n    val targetDF = createTestDF(0, 100, 2)\n    withSQLConf(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> true.toString,\n      DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY.key -> false.toString,\n      DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key -> false.toString,\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> false.toString) {\n      withTempDeltaTable(targetDF, enableDVs = false) { (targetTable, targetLog) =>\n        validateChecksum(targetLog.update(), statisticsExpected = false)\n\n        runDelete(targetTable(), deleteFromID = 75)\n        validateChecksum(targetLog.update(), statisticsExpected = false)\n\n        // Flip DV setting on an existing table.\n        enableDeletionVectorsInTable(targetLog)\n        runDelete(targetTable(), deleteFromID = 60)\n        validateChecksum(targetLog.update(), statisticsExpected = true)\n      }\n    }\n  }\n\n  for (enableIncrementalCommit <- BOOLEAN_DOMAIN)\n  test(s\"Verify checksum validation \" +\n    s\"incrementalCommit: $enableIncrementalCommit\") {\n    val targetDF = createTestDF(0, 100, 2)\n\n    withSQLConf(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> enableIncrementalCommit.toString) {\n      withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n        runDelete(targetTable(), deleteFromID = 75)\n        verifyDVsExist(targetLog, 1)\n\n        val snapshot = targetLog.update()\n        assert(snapshot.validateChecksum())\n      }\n    }\n  }\n\n  for (enableIncrementalCommit <- BOOLEAN_DOMAIN)\n  test(s\"Verify checksum validation when DVs are enabled on existing tables \" +\n    s\"incrementalCommit: $enableIncrementalCommit\") {\n    val targetDF = createTestDF(0, 100, 2)\n\n    withSQLConf(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> enableIncrementalCommit.toString) {\n      withTempDeltaTable(targetDF, enableDVs = false) { (targetTable, targetLog) =>\n        runDelete(targetTable(), deleteFromID = 75)\n\n        // This validation should not take into account DV statistics.\n        assert(targetLog.update().validateChecksum())\n\n        // Enable DVs, delete with DVs and validate checksum again.\n        enableDeletionVectorsInTable(targetLog)\n        runDelete(targetTable(), deleteFromID = 60)\n        verifyDVsExist(targetLog, 1)\n\n        // When incremental commit is enabled DV statistics should remain None since DVs were\n        // enabled midway. Checksum validation should not include DV statistics in the\n        // validation process.\n        assert(targetLog.update().validateChecksum())\n      }\n    }\n  }\n\n  test(\"Verify DeletedRecordsCountHistogram correctness\") {\n    val histogram1 = DeletedRecordCountsHistogramUtils.emptyHistogram\n\n    // Initialize histogram with 100 files.\n    (1 to 100).foreach(_ => histogram1.insert(0))\n    assert(histogram1.deletedRecordCounts === Seq(100, 0, 0, 0, 0, 0, 0, 0, 0, 0))\n\n    // Simulate record deletions from 10 files. This would generate 10 RemoveFile actions with\n    // zero DV cardinality. Then we generate 10 add files, 5 in the range 1-9 and 5 more in\n    // the range 1000-9999.\n    (1 to 10).foreach(_ => histogram1.remove(0))\n    (5 to 9).foreach(n => histogram1.insert(n))\n    (1000 to 1004).foreach(n => histogram1.insert(n))\n    assert(histogram1.deletedRecordCounts === Seq(90, 5, 0, 0, 5, 0, 0, 0, 0, 0))\n\n    (1 to 5).foreach(_ => histogram1.remove(0))\n    (100000 to 100004).foreach(n => histogram1.insert(n))\n    assert(histogram1.deletedRecordCounts === Seq(85, 5, 0, 0, 5, 0, 5, 0, 0, 0))\n\n    // Negative values should be ignored.\n    histogram1.insert(-123)\n    histogram1.remove(-14)\n    assert(histogram1.deletedRecordCounts === Seq(85, 5, 0, 0, 5, 0, 5, 0, 0, 0))\n\n    // Verify small numbers \"catch all\" bucket works.\n    (1 to 5).foreach(_ => histogram1.remove(0))\n    histogram1.insert(10000000L)\n    histogram1.insert(422290000L)\n    histogram1.insert(300000999L)\n    assert(histogram1.deletedRecordCounts === Seq(80, 5, 0, 0, 5, 0, 5, 0, 3, 0))\n\n    // Verify large numbers \"catch all\" bucket works.\n    histogram1.insert(252763333339L)\n    assert(histogram1.deletedRecordCounts === Seq(80, 5, 0, 0, 5, 0, 5, 0, 3, 1))\n\n    // Check edges.\n    val histogram2 = DeletedRecordCountsHistogramUtils.emptyHistogram\n    // Bin 1.\n    histogram2.insert(0)\n    assert(histogram2.deletedRecordCounts === Seq(1, 0, 0, 0, 0, 0, 0, 0, 0, 0))\n    // Bin 2.\n    histogram2.insert(1)\n    histogram2.insert(9)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 0, 0, 0, 0, 0, 0, 0, 0))\n    // Bin 3.\n    histogram2.insert(10)\n    histogram2.insert(99)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 0, 0, 0, 0, 0, 0, 0))\n    // Bin 4.\n    histogram2.insert(100)\n    histogram2.insert(999)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 0, 0, 0, 0, 0, 0))\n    // Bin 5.\n    histogram2.insert(1000)\n    histogram2.insert(9999)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 2, 0, 0, 0, 0, 0))\n    // Bin 6.\n    histogram2.insert(10000)\n    histogram2.insert(99999)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 2, 2, 0, 0, 0, 0))\n    // Bin 7.\n    histogram2.insert(100000)\n    histogram2.insert(999999)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 2, 2, 2, 0, 0, 0))\n    // Bin 8.\n    histogram2.insert(1000000)\n    histogram2.insert(9999999)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 2, 2, 2, 2, 0, 0))\n    // Bin 9.\n    histogram2.insert(10000000)\n    histogram2.insert(100000000)\n    histogram2.insert(1000000000)\n    histogram2.insert(Int.MaxValue - 1)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 2, 2, 2, 2, 4, 0))\n    // Bin 10.\n    histogram2.insert(Int.MaxValue)\n    histogram2.insert(Long.MaxValue)\n    assert(histogram2.deletedRecordCounts === Seq(1, 2, 2, 2, 2, 2, 2, 2, 4, 2))\n  }\n\n  test(\"Verify DeletedRecordsCountHistogram aggregate correctness\") {\n    import org.apache.spark.sql.delta.implicits._\n    val data = Seq(\n      0L, 1L, 9L, 10L, 99L, 100L, 999L, 1000L, 9999L, 10000L, 99999L, 100000L, 999999L,\n      1000000L, 9999999L, 10000000L, Int.MaxValue - 1, Int.MaxValue, Long.MaxValue)\n\n    val df = spark.createDataset(data).toDF(\"dvCardinality\")\n    val histogram = df\n      .select(DeletedRecordCountsHistogramUtils.histogramAggregate(col(\"dvCardinality\")))\n      .first()\n\n    val deletedRecordCounts = histogram\n      .getAs[Row](0)\n      .getAs[mutable.WrappedArray[Long]](\"deletedRecordCounts\")\n\n    assert(deletedRecordCounts === Seq(1, 2, 2, 2, 2, 2, 2, 2, 2, 2))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ChecksumSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.TimeZone\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.DeltaTestUtils._\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass ChecksumSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaTestUtilsBase\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils\n  with CatalogOwnedTestBaseSuite {\n\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS, false)\n\n  test(s\"A Checksum should be written after every commit when \" +\n    s\"${DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key} is true\") {\n    def testChecksumFile(writeChecksumEnabled: Boolean): Unit = {\n      // Set up the log by explicitly creating the table otherwise we can't\n      // construct the DeltaLog via the table name.\n      withTempTable(createTable = true) { tableName =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> writeChecksumEnabled.toString) {\n          def checksumExists(deltaLog: DeltaLog, version: Long): Boolean = {\n            val checksumFile = new File(FileNames.checksumFile(deltaLog.logPath, version).toUri)\n            checksumFile.exists()\n          }\n\n          // Commit the txn\n          val log = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          val txn = log.startTransaction(log.initialCatalogTable)\n          val txnCommitVersion = txn.commit(Seq.empty, DeltaOperations.Truncate())\n          assert(checksumExists(log, txnCommitVersion) == writeChecksumEnabled)\n        }\n      }\n    }\n\n    testChecksumFile(writeChecksumEnabled = true)\n    testChecksumFile(writeChecksumEnabled = false)\n  }\n\n  private def setTimeZone(timeZone: String): Unit = {\n    spark.sql(s\"SET spark.sql.session.timeZone = $timeZone\")\n    TimeZone.setDefault(TimeZone.getTimeZone(timeZone))\n  }\n\n  test(\"Incremental checksums: post commit snapshot should have a checksum \" +\n      \"without triggering state reconstruction\") {\n    for (incrementalCommitEnabled <- BOOLEAN_DOMAIN) {\n      withSQLConf(\n        DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\",\n        DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> incrementalCommitEnabled.toString\n      ) {\n        withTempTable(createTable = false) { tableName =>\n          // Set the timezone to UTC to avoid triggering force verification of all files in CRC\n          // for non utc environments.\n          setTimeZone(\"UTC\")\n          val df = spark.range(1)\n          df.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n          val log = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          log\n            .startTransaction()\n            .commit(Seq(createTestAddFile()), DeltaOperations.Write(SaveMode.Append))\n          val postCommitSnapshot = log.snapshot\n          assert(postCommitSnapshot.version == 1)\n          assert(!postCommitSnapshot.stateReconstructionTriggered)\n          assert(postCommitSnapshot.checksumOpt.isDefined == incrementalCommitEnabled)\n\n          postCommitSnapshot.checksumOpt.foreach { incrementalChecksum =>\n            val checksumFromStateReconstruction = postCommitSnapshot.computeChecksum\n            assert(incrementalChecksum.copy(txnId = None) == checksumFromStateReconstruction)\n          }\n        }\n      }\n    }\n  }\n\n  def testIncrementalChecksumWrites(tableMutationOperation: String => Unit): Unit = {\n    withTempTable(createTable = false) { tableName =>\n      withSQLConf(\n        DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"true\",\n        DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key ->\"true\") {\n        val df = spark.range(10).withColumn(\"id2\", col(\"id\") % 2)\n        df.write\n          .format(\"delta\")\n          .partitionBy(\"id\")\n          .mode(\"append\")\n          .saveAsTable(tableName)\n\n        tableMutationOperation(tableName)\n        val log = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val checksumOpt = log.snapshot.checksumOpt\n        assert(checksumOpt.isDefined)\n        val checksum = checksumOpt.get\n        val computedChecksum = log.snapshot.computeChecksum\n        assert(checksum.copy(txnId = None) === computedChecksum)\n      }\n    }\n  }\n\n  test(\"Incremental checksums: INSERT\") {\n    testIncrementalChecksumWrites { tableName =>\n      sql(s\"INSERT INTO $tableName SELECT *, 1 FROM range(10, 20)\")\n    }\n  }\n\n  test(\"Incremental checksums: UPDATE\") {\n    testIncrementalChecksumWrites { tableName =>\n      sql(s\"UPDATE $tableName SET id2 = id + 1 WHERE id % 2 = 0\")\n    }\n  }\n\n  test(\"Incremental checksums: DELETE\") {\n    testIncrementalChecksumWrites { tableName =>\n      sql(s\"DELETE FROM $tableName WHERE id % 2 = 0\")\n    }\n  }\n\n  test(\"Checksum validation should happen on checkpoint\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"true\",\n      DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> \"true\",\n      // Disabled for this test because with it enabled, a corrupted Protocol\n      // or Metadata will trigger a failure earlier than the full validation.\n      DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key -> \"false\"\n    ) {\n      withTempTable(createTable = false) { tableName =>\n        spark\n          .range(10)\n          .write\n          .format(\"delta\")\n          .saveAsTable(tableName)\n        spark.range(1)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(tableName)\n        val log = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val checksumOpt = log.readChecksum(1)\n        assert(checksumOpt.isDefined)\n        val checksum = checksumOpt.get\n        // Corrupt the checksum file.\n        val corruptedChecksum = checksum.copy(\n          protocol =\n            checksum.protocol.copy(minReaderVersion = checksum.protocol.minReaderVersion + 1),\n          metadata = checksum.metadata.copy(description = \"corrupted\"),\n          numProtocol = 2,\n          numMetadata = 2,\n          tableSizeBytes = checksum.tableSizeBytes + 1,\n          numFiles = checksum.numFiles + 1)\n        val corruptedChecksumJson = JsonUtils.toJson(corruptedChecksum)\n        log.store.write(\n          FileNames.checksumFile(log.logPath, 1),\n          Seq(corruptedChecksumJson).toIterator,\n          overwrite = true)\n        DeltaLog.clearCache()\n        val log2 = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val usageLogs = Log4jUsageLogger.track {\n          intercept[DeltaIllegalStateException] {\n            log2.checkpoint()\n          }\n        }\n        val validationFailureLogs = filterUsageRecords(usageLogs, \"delta.checksum.invalid\")\n        assert(validationFailureLogs.size == 1)\n        validationFailureLogs.foreach { log =>\n          val usageLogBlob = JsonUtils.fromJson[Map[String, Any]](log.blob)\n          val mismatchingFieldsOpt = usageLogBlob.get(\"mismatchingFields\")\n          assert(mismatchingFieldsOpt.isDefined)\n          val mismatchingFieldsSet = mismatchingFieldsOpt.get.asInstanceOf[Seq[String]].toSet\n          val expectedMismatchingFields = Set(\n            \"protocol\",\n            \"metadata\",\n            \"numOfProtocol\",\n            \"numOfMetadata\",\n            \"tableSizeBytes\",\n            \"numFiles\"\n          )\n          assert(mismatchingFieldsSet === expectedMismatchingFields)\n        }\n      }\n    }\n  }\n\n  test(\"incremental commit verify mode should always detect invalid .crc\") {\n    withSQLConf(\n      DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY.key -> \"true\",\n      DeltaSQLConf.DELTA_CHECKSUM_MISMATCH_IS_FATAL.key -> \"false\",\n      DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> \"true\",\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_ENABLED.key -> \"false\",\n      DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key -> \"true\"\n    ) {\n      // Explicitly create the table w/o any AddFile for the subsequent\n      // DeltaLog construction.\n      withTempTable(createTable = true) { tableName =>\n        import testImplicits._\n        val numAddFiles = 10\n\n        // Procedure:\n        // 1. Populate the table with several files\n        // 2. Start a new transaction\n        // 3. Intentionally try to commit the same files again\n        //    a. Silently duplicated AddFile breaks incremental commit invariants\n        //    b. Incrementally computed .crc is thus invalid\n        //    c. Incremental commit verification should detect the \"invalid\" .crc\n        //    d. Post-commit snapshot should have empty checksumOpt\n        // 4. Clear the delta log cache so we pick up the correct (fallback) .crc\n        // 5. Create a new snapshot and manually validate the .crc\n\n        val files = (1 to numAddFiles).map(i => createTestAddFile(encodedPath = i.toString))\n        DeltaLog\n          .forTable(spark, TableIdentifier(tableName))\n          .startTransaction()\n          .commitWriteAppend(files: _*)\n\n        val log = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val txn = log.startTransaction()\n        val expected =\n          s\"\"\"Table size (bytes) - Expected: ${2*numAddFiles} Computed: $numAddFiles\n             |Number of files - Expected: ${2*numAddFiles} Computed: $numAddFiles\n          \"\"\".stripMargin.trim\n\n        val Seq(corruptionReport) = collectUsageLogs(\"delta.checksum.invalid\") {\n          // Intentionally re-add the same files, without identifying them as duplicates\n          txn.commitWriteAppend(files: _*)\n        }\n        val error = JsonUtils.fromJson[Map[String, Any]](corruptionReport.blob).get(\"error\")\n        assert(error.exists(_.asInstanceOf[String].contains(expected)))\n        assert(log.snapshot.checksumOpt.isEmpty)\n      }\n    }\n  }\n\n  test(\"force checksum validation due to stale checkpoint\") {\n    withSQLConf(\n      DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY.key -> \"false\",\n      // Set this to 0 to ensure that validation is not\n      // skipped due the checkpoint not being old enough\n      DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_MIN_TIME_INTERVAL_MINUTES.key -> \"0\",\n      DeltaSQLConf.DELTA_CHECKSUM_MISMATCH_IS_FATAL.key -> \"true\",\n      DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> \"true\",\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_ENABLED.key -> \"false\",\n      DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_INTERVAL.key -> \"999\"\n    ) {\n      withTempTable(createTable = false) { tableName =>\n        spark\n          .range(4)\n          .write\n          .format(\"delta\")\n          .saveAsTable(tableName)\n        // Create checkpoint at version 0\n        DeltaLog.forTable(spark, TableIdentifier(tableName)).checkpoint()\n        def validateAttemptedTransactionFails: Unit = {\n          DeltaLog.clearCache()\n          val usageLogs = Log4jUsageLogger.track {\n            intercept[DeltaIllegalStateException] {\n              DeltaLog\n              .forTable(spark, TableIdentifier(tableName))\n              .startTransaction()\n            }\n          }\n          val validationFailureLogs = filterUsageRecords(usageLogs, \"delta.checksum.invalid\")\n          assert(validationFailureLogs.size == 1)\n          validationFailureLogs.foreach { log =>\n            val usageLogBlob = JsonUtils.fromJson[Map[String, Any]](log.blob)\n            val mismatchingFieldsOpt = usageLogBlob.get(\"mismatchingFields\")\n            assert(mismatchingFieldsOpt.isDefined)\n            val mismatchingFieldsSet = mismatchingFieldsOpt.get.asInstanceOf[Seq[String]].toSet\n            val expectedMismatchingFields = Set(\n              \"numOfProtocol\",\n              \"numOfMetadata\",\n              \"tableSizeBytes\",\n              \"numFiles\"\n            )\n            assert(mismatchingFieldsSet === expectedMismatchingFields)\n          }\n        }\n        // Write 4 commits. Also, corrupt every checksum file.\n        (1 to 4).foreach { version =>\n          spark.range(1)\n            .write\n            .format(\"delta\")\n            .mode(\"append\")\n            .saveAsTable(tableName)\n          // Corrupt the checksum file.\n          val log = DeltaLog\n            .forTable(spark, TableIdentifier(tableName))\n          val checksum = log.readChecksum(version).get\n          val corruptedChecksum = checksum.copy(\n            numProtocol = 2,\n            numMetadata = 2,\n            tableSizeBytes = checksum.tableSizeBytes + 1,\n            numFiles = checksum.numFiles + 1)\n          val corruptedChecksumJson = JsonUtils.toJson(corruptedChecksum)\n          log.store.write(\n            FileNames.checksumFile(log.logPath, version),\n            Seq(corruptedChecksumJson).toIterator,\n            overwrite = true)\n\n          withSQLConf(\n            // Set the forced checksum validation interval to the current version\n            // so that validation is triggered\n            DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_INTERVAL.key -> version.toString\n          ) {\n            validateAttemptedTransactionFails\n          }\n          withSQLConf(\n            // Set the validation interval to a value smaller than the current version\n            // so that validation is triggered\n            DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_INTERVAL.key -> \"0\"\n          ) {\n            validateAttemptedTransactionFails\n          }\n          withSQLConf(\n            DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_INTERVAL.key -> \"0\",\n            // Validation should only be triggered if the checkpoint was\n            // created more than 999 minutes ago. Which should not be\n            // the case here.\n            DeltaSQLConf.FORCED_CHECKSUM_VALIDATION_MIN_TIME_INTERVAL_MINUTES.key -> \"999\"\n          ) {\n            DeltaLog.clearCache()\n            DeltaLog\n              .forTable(spark, TableIdentifier(tableName))\n              .startTransaction()\n          }\n        }\n      }\n    }\n  }\n}\n\nclass ChecksumWithCatalogOwnedBatch1Suite extends ChecksumSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass ChecksumWithCatalogOwnedBatch2Suite extends ChecksumSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass ChecksumWithCatalogOwnedBatch100Suite extends ChecksumSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CloneParquetSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.DeltaExceptionTestUtils\n\nimport org.apache.spark.{SparkException, SparkThrowable}\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.functions.col\n\nclass CloneParquetByPathSuite extends CloneParquetSuiteBase\n    with DeltaExceptionTestUtils\n{\n\n  protected def withParquetTable(\n      df: DataFrame, partCols: Seq[String] = Seq.empty[String])(\n      func: ParquetIdent => Unit): Unit = {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      if (partCols.nonEmpty) {\n        df.write.format(\"parquet\").mode(\"overwrite\").partitionBy(partCols: _*).save(tempDir)\n      } else {\n        df.write.format(\"parquet\").mode(\"overwrite\").save(tempDir)\n      }\n\n      func(ParquetIdent(tempDir, isTable = false))\n    }\n  }\n\n  // CLONE doesn't support partitioned parquet table using path since it requires customer to\n  // provide the partition schema in the command like `CONVERT TO DELTA`, but such an option is not\n  // available in CLONE yet.\n  testClone(\"clone partitioned parquet to delta table\") { mode =>\n    val df = spark.range(100)\n      .withColumn(\"key1\", col(\"id\") % 4)\n      .withColumn(\"key2\", col(\"id\") % 7 cast \"String\")\n\n    withParquetTable(df, Seq(\"key1\", \"key2\")) { sourceIdent =>\n      val tableName = \"cloneTable\"\n      withTable(tableName) {\n        val ex = interceptWithUnwrapping[DeltaAnalysisException] {\n          sql(s\"CREATE TABLE $tableName $mode CLONE $sourceIdent\")\n        }\n        assert(ex.getMessage.contains(\"Expecting 0 partition column(s)\"))\n      }\n    }\n  }\n}\n\nclass CloneParquetByNameSuite extends CloneParquetSuiteBase\n{\n\n  protected def withParquetTable(\n    df: DataFrame, partCols: Seq[String] = Seq.empty[String])(\n    func: ParquetIdent => Unit): Unit = {\n    val tableName = \"parquet_table\"\n    withTable(tableName) {\n      if (partCols.nonEmpty) {\n        df.write.format(\"parquet\").partitionBy(partCols: _*).saveAsTable(tableName)\n      } else {\n        df.write.format(\"parquet\").saveAsTable(tableName)\n      }\n\n      func(ParquetIdent(tableName, isTable = true))\n    }\n  }\n\n  testClone(\"clone partitioned parquet to delta table\") { mode =>\n    val df = spark.range(100)\n      .withColumn(\"key1\", col(\"id\") % 4)\n      .withColumn(\"key2\", col(\"id\") % 7 cast \"String\")\n\n    withParquetTable(df, Seq(\"key1\", \"key2\")) { sourceIdent =>\n      val tableName = \"cloneTable\"\n      withTable(tableName) {\n        sql(s\"CREATE TABLE $tableName $mode CLONE $sourceIdent\")\n\n        checkAnswer(spark.table(tableName), df)\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CloneParquetSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.commands.CloneParquetSource\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait CloneParquetSuiteBase extends QueryTest\n  with DeltaSQLCommandTest\n  with SharedSparkSession {\n\n  // Identifier to represent a Parquet source\n  protected case class ParquetIdent(name: String, isTable: Boolean) {\n\n    override def toString: String = if (isTable) name else s\"parquet.`$name`\"\n\n    def toTableIdent: TableIdentifier =\n      if (isTable) TableIdentifier(name) else TableIdentifier(name, Some(\"parquet\"))\n\n    def toCloneSource: CloneParquetSource = {\n      val catalogTableOpt =\n        if (isTable) Some(spark.sessionState.catalog.getTableMetadata(toTableIdent)) else None\n      CloneParquetSource(toTableIdent, catalogTableOpt, spark)\n    }\n  }\n\n  protected def supportedModes: Seq[String] = Seq(\"SHALLOW\")\n\n  protected def testClone(testName: String)(f: String => Unit): Unit =\n    supportedModes.foreach { mode => test(s\"$testName - $mode\") { f(mode) } }\n\n  protected def withParquetTable(\n      df: DataFrame, partCols: Seq[String] = Seq.empty[String])(func: ParquetIdent => Unit): Unit\n\n  protected def validateBlob(\n      blob: Map[String, Any],\n      mode: String,\n      source: CloneParquetSource,\n      target: DeltaLog): Unit = {\n    // scalastyle:off deltahadoopconfiguration\n    val hadoopConf = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n\n    val sourcePath = source.dataPath\n    val sourceFs = sourcePath.getFileSystem(hadoopConf)\n    val qualifiedSourcePath = sourceFs.makeQualified(sourcePath)\n\n    val targetPath = target.dataPath\n    val targetFs = targetPath.getFileSystem(hadoopConf)\n    val qualifiedTargetPath = targetFs.makeQualified(targetPath)\n\n    assert(blob(\"sourcePath\") === qualifiedSourcePath.toString)\n    assert(blob(\"target\") === qualifiedTargetPath.toString)\n    assert(blob(\"sourceTableSize\") === source.sizeInBytes)\n    assert(blob(\"sourceNumOfFiles\") === source.numOfFiles)\n    assert(blob(\"partitionBy\") === source.metadata.partitionColumns)\n  }\n\n  testClone(\"validate clone metrics\") { mode =>\n    val df = spark.range(100).withColumn(\"key\", col(\"id\") % 3)\n    withParquetTable(df) { sourceIdent =>\n      val tableName = \"cloneTable\"\n      withTable(tableName) {\n        val allLogs = Log4jUsageLogger.track {\n          sql(s\"CREATE TABLE $tableName $mode CLONE $sourceIdent\")\n        }\n\n        val source = sourceIdent.toCloneSource\n        val target = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n        val blob = JsonUtils.fromJson[Map[String, Any]](allLogs\n          .filter(_.metric == \"tahoeEvent\")\n          .filter(_.tags.get(\"opType\").contains(\"delta.clone\"))\n          .filter(_.blob.contains(\"source\"))\n          .map(_.blob).last)\n        validateBlob(blob, mode, source, target)\n\n        val sourceMetadata = source.metadata\n        val targetMetadata = target.update().metadata\n\n        assert(sourceMetadata.schema === targetMetadata.schema)\n        assert(sourceMetadata.configuration === targetMetadata.configuration)\n        assert(sourceMetadata.dataSchema === targetMetadata.dataSchema)\n        assert(sourceMetadata.partitionColumns === targetMetadata.partitionColumns)\n      }\n    }\n  }\n\n  testClone(\"clone non-partitioned parquet to delta table\") { mode =>\n    val df = spark.range(100)\n      .withColumn(\"key1\", col(\"id\") % 4)\n      .withColumn(\"key2\", col(\"id\") % 7 cast \"String\")\n\n    withParquetTable(df) { sourceIdent =>\n      val tableName = \"cloneTable\"\n      withTable(tableName) {\n        sql(s\"CREATE TABLE $tableName $mode CLONE $sourceIdent\")\n\n        checkAnswer(spark.table(tableName), df)\n      }\n    }\n  }\n\n  testClone(\"clone non-partitioned parquet to delta path\") { mode =>\n    val df = spark.range(100)\n      .withColumn(\"key1\", col(\"id\") % 4)\n      .withColumn(\"key2\", col(\"id\") % 7 cast \"String\")\n\n    withParquetTable(df) { sourceIdent =>\n      withTempDir { dir =>\n        val deltaDir = dir.getCanonicalPath\n        sql(s\"CREATE TABLE delta.`$deltaDir` $mode CLONE $sourceIdent\")\n\n        checkAnswer(spark.read.format(\"delta\").load(deltaDir), df)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CloneTableSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.immutable.NumericRange\n\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaExcludedTestMixin, DeltaSQLCommandTest}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{AnalysisException, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.util.Utils\n\nclass CloneTableSQLSuite\n  extends CloneTableSuiteBase\n  with CloneTableSQLTestMixin\n  with DeltaColumnMappingTestUtils\n  with CatalogOwnedTestBaseSuite {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    disableDeletionVectors(spark.conf)\n  }\n\n  testAllClones(s\"table version as of syntax\") { (_, target, isShallow) =>\n    val tbl = \"source\"\n    testSyntax(\n      tbl,\n      target,\n      s\"CREATE TABLE delta.`$target` ${cloneTypeStr(isShallow)} CLONE $tbl VERSION AS OF 0\",\n      isShallow\n    )\n  }\n\n  testAllClones(\"CREATE OR REPLACE syntax when there is no existing table\") {\n    (_, clone, isShallow) =>\n      val tbl = \"source\"\n      testSyntax(\n        tbl,\n        clone,\n        s\"CREATE OR REPLACE TABLE delta.`$clone` ${cloneTypeStr(isShallow)} CLONE $tbl\",\n        isShallow\n      )\n  }\n\n  cloneTest(\"REPLACE cannot be used with IF NOT EXISTS\") { (shallow, _) =>\n    val tbl = \"source\"\n    intercept[ParseException] {\n      testSyntax(tbl, shallow,\n        s\"CREATE OR REPLACE TABLE IF NOT EXISTS delta.`$shallow` SHALLOW CLONE $tbl\")\n    }\n    intercept[ParseException] {\n      testSyntax(tbl, shallow,\n        s\"REPLACE TABLE IF NOT EXISTS delta.`$shallow` SHALLOW CLONE $tbl\")\n    }\n  }\n\n  testAllClones(\n    \"IF NOT EXISTS should not go through with CLONE if table exists\") { (tblExt, _, isShallow) =>\n    val sourceTable = \"source\"\n    val conflictingTable = \"conflict\"\n    withTable(sourceTable, conflictingTable) {\n      sql(s\"CREATE TABLE $conflictingTable \" +\n        s\"USING PARQUET LOCATION '$tblExt' TBLPROPERTIES ('abc'='def', 'def'='ghi') AS SELECT 1\")\n      spark.range(5).write.format(\"delta\").saveAsTable(sourceTable)\n\n      sql(s\"CREATE TABLE IF NOT EXISTS \" +\n        s\"$conflictingTable ${cloneTypeStr(isShallow)} CLONE $sourceTable\")\n\n      checkAnswer(sql(s\"SELECT COUNT(*) FROM $conflictingTable\"), Row(1))\n    }\n  }\n\n  testAllClones(\"IF NOT EXISTS should throw an error if path exists\") { (_, target, isShallow) =>\n    spark.range(5).write.format(\"delta\").save(target)\n\n    val ex = intercept[AnalysisException] {\n      sql(s\"CREATE TABLE IF NOT EXISTS \" +\n        s\"delta.`$target` ${cloneTypeStr(isShallow)} CLONE delta.`$target`\")\n    }\n\n    assert(ex.getMessage.contains(\"is not empty\"))\n  }\n\n  cloneTest(\"Negative test: REPLACE table where there is no existing table\") { (shallow, _) =>\n    val tbl = \"source\"\n    val ex = intercept[AnalysisException] {\n      testSyntax(tbl, shallow, s\"REPLACE TABLE delta.`$shallow` SHALLOW CLONE $tbl\")\n    }\n\n    assert(ex.getMessage.contains(\"cannot be replaced as it does not exist.\"))\n  }\n\n  cloneTest(\"cloning a table that doesn't exist\") { (tblExt, _) =>\n    val ex = intercept[AnalysisException] {\n      sql(s\"CREATE TABLE delta.`$tblExt` SHALLOW CLONE not_exists\")\n    }\n    assert(ex.getMessage.contains(\"Table not found\") ||\n      ex.getMessage.contains(\"The table or view `not_exists` cannot be found\"))\n\n    val ex2 = intercept[AnalysisException] {\n      sql(s\"CREATE TABLE delta.`$tblExt` SHALLOW CLONE not_exists VERSION AS OF 0\")\n    }\n    assert(ex2.getMessage.contains(\"Table not found\") ||\n      ex2.getMessage.contains(\"The table or view `not_exists` cannot be found\"))\n  }\n\n  cloneTest(\"cloning a view\") { (tblExt, _) =>\n    withTempView(\"tmp\") {\n      sql(\"CREATE OR REPLACE TEMP VIEW tmp AS SELECT * FROM range(10)\")\n      val ex = intercept[AnalysisException] {\n        sql(s\"CREATE TABLE delta.`$tblExt` SHALLOW CLONE tmp\")\n      }\n      assert(ex.errorClass === Some(\"DELTA_CLONE_UNSUPPORTED_SOURCE\"))\n      assert(ex.getMessage.contains(\"clone source 'tmp', whose format is View.\"))\n    }\n  }\n\n  cloneTest(\"Clone on table with delta statistics columns\") { (source, target) =>\n    withTable(\"delta_table\", \"delta_table_shadow_clone\", \"delta_table_clone\") {\n      sql(\n        \"create table delta_table (c0 long, c1 long, c2 long) using delta \" +\n        \"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1,c2', \" +\n        \"'delta.columnMapping.mode' = 'name', \" +\n        \"'delta.minReaderVersion' = '2', \" +\n        \"'delta.minWriterVersion' = '5')\"\n      )\n      sql(s\"CREATE TABLE delta_table_shadow_clone SHALLOW CLONE delta_table LOCATION '$source'\")\n      var dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table_shadow_clone\")\n        .collect()\n        .map { row => row.getString(0) -> row.getString(1) }\n        .filter(_._1 == \"delta.dataSkippingStatsColumns\")\n        .toSeq\n      val result1 = Seq((\"delta.dataSkippingStatsColumns\", \"c1,c2\"))\n      assert(dataSkippingStatsColumns == result1)\n    }\n  }\n\n  cloneTest(\"Clone on table with nested delta statistics columns\") { (source, target) =>\n      withTable(\"delta_table\", \"delta_table_shadow_clone\", \"delta_table_clone\") {\n        sql(\n          \"create table delta_table (c0 long, c1 long, c2 struct<a: int, b int>) using delta \" +\n            \"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1,c2.a,c2.b', \" +\n            \"'delta.columnMapping.mode' = 'name', \" +\n            \"'delta.minReaderVersion' = '2', \" +\n            \"'delta.minWriterVersion' = '5')\"\n        )\n        sql(s\"CREATE TABLE delta_table_shadow_clone SHALLOW CLONE delta_table LOCATION '$source'\")\n        var dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table_shadow_clone\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == \"delta.dataSkippingStatsColumns\")\n          .toSeq\n        val result1 = Seq((\"delta.dataSkippingStatsColumns\", \"c1,c2.a,c2.b\"))\n        assert(dataSkippingStatsColumns == result1)\n      }\n  }\n\n  cloneTest(\"cloning a view over a Delta table\") { (tblExt, _) =>\n    withTable(\"delta_table\") {\n      withView(\"tmp\") {\n        sql(\"CREATE TABLE delta_table USING delta AS SELECT * FROM range(10)\")\n        sql(\"CREATE VIEW tmp AS SELECT * FROM delta_table\")\n        val ex = intercept[AnalysisException] {\n          sql(s\"CREATE TABLE delta.`$tblExt` SHALLOW CLONE tmp\")\n        }\n        assert(ex.errorClass === Some(\"DELTA_CLONE_UNSUPPORTED_SOURCE\"))\n        assert(\n          ex.getMessage.contains(\"clone source\") &&\n            ex.getMessage.contains(\"default.tmp', whose format is View.\")\n        )\n      }\n    }\n  }\n\n  cloneTest(\"check metrics returned from shallow clone\", TAG_HAS_SHALLOW_CLONE) { (_, _) =>\n    val source = \"source\"\n    val target = \"target\"\n    withTable(source, target) {\n      spark.range(100).write.format(\"delta\").saveAsTable(source)\n\n      val res = sql(s\"CREATE TABLE $target SHALLOW CLONE $source\")\n\n      // schema check\n      val expectedColumns = Seq(\n        \"source_table_size\",\n        \"source_num_of_files\",\n        \"num_removed_files\",\n        \"num_copied_files\",\n        \"removed_files_size\",\n        \"copied_files_size\"\n      )\n      assert(expectedColumns == res.columns.toSeq)\n\n      // logic check\n      assert(res.count() == 1)\n      val returnedMetrics = res.first()\n      assert(returnedMetrics.getAs[Long](\"source_table_size\") != 0L)\n      assert(returnedMetrics.getAs[Long](\"source_num_of_files\") != 0L)\n      // Delta-OSS doesn't support copied file metrics\n      assert(returnedMetrics.getAs[Long](\"num_copied_files\") == 0L)\n      assert(returnedMetrics.getAs[Long](\"copied_files_size\") == 0L)\n    }\n  }\n\n  cloneTest(\"Negative test: Clone to target path and also have external location\") { (deep, ext) =>\n    val sourceTable = \"source\"\n    withTable(sourceTable) {\n      spark.range(5).write.format(\"delta\").saveAsTable(sourceTable)\n      val ex = intercept[IllegalArgumentException] {\n        runAndValidateClone(\n          sourceTable,\n          deep,\n          isShallow = true,\n          sourceIsTable = true,\n          targetLocation = Some(ext))()\n      }\n\n      assert(ex.getMessage.contains(\"Two paths were provided as the CLONE target\"))\n    }\n  }\n\n  test(\"Clone should populate override table properties to catalog\") {\n    val source = \"source\"\n    val target = \"target\"\n    withTable(source, target) {\n      withSQLConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"false\") {\n        spark.range(100).write.format(\"delta\").saveAsTable(source)\n        sql(s\"\"\"CREATE TABLE $target SHALLOW CLONE $source tblproperties(\"abc\" = \"123\")\"\"\")\n        val targetCatalogTable =\n          spark.sessionState.catalog.getTableMetadata(TableIdentifier(target))\n        targetCatalogTable.properties.get(\"abc\").contains(\"123\")\n      }\n    }\n  }\n}\n\n\nclass CloneTableSQLWithCatalogOwnedBatch1Suite\n  extends CloneTableSQLSuite\n{\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass CloneTableSQLWithCatalogOwnedBatch2Suite\n  extends CloneTableSQLSuite\n{\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass CloneTableSQLWithCatalogOwnedBatch100Suite\n  extends CloneTableSQLSuite\n{\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\nclass CloneTableSQLIdColumnMappingSuite\n  extends CloneTableSQLSuite\n    with CloneTableColumnMappingSuiteBase\n    with DeltaColumnMappingEnableIdMode {\n}\n\nclass CloneTableSQLNameColumnMappingSuite\n  extends CloneTableSQLSuite\n    with CloneTableColumnMappingNameSuiteBase\n    with DeltaColumnMappingEnableNameMode {\n}\n\nobject CloneTableSQLTestUtils {\n\n  // scalastyle:off argcount\n  def buildCloneSqlString(\n      source: String,\n      target: String,\n      sourceIsTable: Boolean = false,\n      targetIsTable: Boolean = false,\n      sourceFormat: String = \"delta\",\n      targetLocation: Option[String] = None,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      isCreate: Boolean = true,\n      isReplace: Boolean = false,\n      tableProperties: Map[String, String] = Map.empty): String = {\n    val header = if (isCreate && isReplace) {\n      \"CREATE OR REPLACE\"\n    } else if (isReplace) {\n      \"REPLACE\"\n    } else {\n      \"CREATE\"\n    }\n    // e.g. CREATE TABLE targetTable\n    val createTbl =\n      if (targetIsTable) s\"$header TABLE $target\" else s\"$header TABLE delta.`$target`\"\n    // e.g. CREATE TABLE targetTable SHALLOW CLONE\n    val withMethod =\n        createTbl + \" SHALLOW CLONE \"\n    // e.g. CREATE TABLE targetTable SHALLOW CLONE delta.`/source/table`\n    val withSource = if (sourceIsTable) {\n      withMethod + s\"$source \"\n    } else {\n      withMethod + s\"$sourceFormat.`$source` \"\n    }\n    // e.g. CREATE TABLE targetTable SHALLOW CLONE delta.`/source/table` VERSION AS OF 0\n    val withVersion = if (versionAsOf.isDefined) {\n      withSource + s\"VERSION AS OF ${versionAsOf.get}\"\n    } else if (timestampAsOf.isDefined) {\n      withSource + s\"TIMESTAMP AS OF '${timestampAsOf.get}'\"\n    } else {\n      withSource\n    }\n    // e.g. CREATE TABLE targetTable SHALLOW CLONE delta.`/source/table` VERSION AS OF 0\n    //      LOCATION '/desired/target/location'\n    val withLocation = if (targetLocation.isDefined) {\n      s\" $withVersion LOCATION '${targetLocation.get}'\"\n    } else {\n      withVersion\n    }\n    val withProperties = if (tableProperties.nonEmpty) {\n      val props = tableProperties.map(p => s\"'${p._1}' = '${p._2}'\").mkString(\",\")\n      s\" $withLocation TBLPROPERTIES ($props)\"\n    } else {\n      withLocation\n    }\n    withProperties\n  }\n  // scalastyle:on argcount\n}\n\nclass CloneTableScalaDeletionVectorSuite\n    extends CloneTableScalaSuite\n    with DeltaSQLCommandTest\n    with DeltaExcludedTestMixin\n    with DeletionVectorsTestUtils {\n\n  override def excluded: Seq[String] = super.excluded ++\n    Seq(\n      // These require the initial table protocol version to be low to work properly.\n      \"Cloning a table with new table properties that force protocol version upgrade -\" +\n        \" delta.enableChangeDataFeed\"\n      , \"Cloning a table with new table properties that force protocol version upgrade -\" +\n        \" delta.enableDeletionVectors\"\n      , \"Cloning a table without DV property should not upgrade protocol version\"\n      , \"CLONE respects table features set by table property override, targetExists=true\"\n      , \"CLONE ignores reader/writer session defaults\")\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark.conf)\n  }\n\n  testAllClones(\"Cloning table with persistent DVs\") { (source, target, isShallow) =>\n    // Create source table\n    writeMultiFileSourceTable(\n      source,\n      fileRanges = Seq(0L until 30L, 30L until 60L, 60L until 90L))\n    // Add DVs to 2 files, leave 1 file without DVs.\n    spark.sql(s\"DELETE FROM delta.`$source` WHERE id IN (24, 42)\")\n    runAndValidateCloneWithDVs(\n      source,\n      target,\n      isShallow,\n      expectedNumFilesWithDVs = 2)\n  }\n\n  testAllClones(\"Cloning table with persistent DVs and absolute parquet paths\"\n  ) { (source, target, isShallow) =>\n    withTempDir { originalSourceDir =>\n      val originalSource = originalSourceDir.getCanonicalPath\n      // Create source table, by writing to an upstream table and then shallow cloning before\n      // adding DVs.\n      writeMultiFileSourceTable(\n        source = originalSource,\n        fileRanges = Seq(0L until 30L, 30L until 60L, 60L until 90L))\n      spark.sql(s\"CREATE OR REPLACE TABLE delta.`$source` SHALLOW CLONE delta.`$originalSource`\")\n      // Add DVs to 2 files, leave 1 file without DVs.\n      spark.sql(s\"DELETE FROM delta.`$source` WHERE id IN (24, 42)\")\n      runAndValidateCloneWithDVs(\n        source,\n        target,\n        isShallow,\n        expectedNumFilesWithDVs = 2)\n    }\n  }\n\n  testAllClones(\"Cloning table with persistent DVs and absolute DV file paths\"\n  ) { (source, target, isShallow) =>\n    withTempDir { originalSourceDir =>\n      val originalSource = originalSourceDir.getCanonicalPath\n      // Create source table, by writing to an upstream table, adding DVs and then shallow cloning.\n      writeMultiFileSourceTable(\n        source = originalSource,\n        fileRanges = Seq(0L until 30L, 30L until 60L, 60L until 90L))\n      // Add DVs to 2 files, leave 1 file without DVs.\n      spark.sql(s\"DELETE FROM delta.`$originalSource` WHERE id IN (24, 42)\")\n      val originalSourceTable = io.delta.tables.DeltaTable.forPath(spark, originalSource)\n      spark.sql(s\"CREATE OR REPLACE TABLE delta.`$source` SHALLOW CLONE delta.`$originalSource`\")\n      // Double check this clone was correct.\n      checkAnswer(\n        spark.read.format(\"delta\").load(source), expectedAnswer = originalSourceTable.toDF)\n      runAndValidateCloneWithDVs(\n        source,\n        target,\n        isShallow,\n        expectedNumFilesWithDVs = 2)\n    }\n  }\n\n  cloneTest(\"Shallow clone round-trip with DVs\") { (source, target) =>\n    // Create source table.\n    writeMultiFileSourceTable(\n      source = source,\n      fileRanges = Seq(\n        0L until 30L, // file 1\n        30L until 60L, // file 2\n        60L until 90L, //  file 3\n        90L until 120L)) // file 4\n    // Add DVs to files 1 and 2 and then shallow clone.\n    spark.sql(s\"DELETE FROM delta.`$source` WHERE id IN (24, 42)\")\n    runAndValidateCloneWithDVs(\n      source = source,\n      target = target,\n      isShallow = true,\n      expectedNumFilesWithDVs = 2)\n\n    // Add a new DV to file 3 and update the DV file 2,\n    // leaving file 4 without a DV and file 1 with the existing DV.\n    // Then shallow clone back into source.\n    spark.sql(s\"DELETE FROM delta.`$target` WHERE id IN (43, 69)\")\n    runAndValidateCloneWithDVs(\n      source = target,\n      target = source,\n      isShallow = true,\n      expectedNumFilesWithDVs = 3,\n      isReplaceOperation = true)\n  }\n\n  /** Write one file per range in `fileRanges`. */\n  private def writeMultiFileSourceTable(\n    source: String,\n    fileRanges: Seq[NumericRange.Exclusive[Long]]): Unit = {\n    for (range <- fileRanges) {\n      spark.range(start = range.start, end = range.end, step = 1L, numPartitions = 1).toDF(\"id\")\n        .write.format(\"delta\").mode(\"append\").save(source)\n    }\n  }\n\n  private def tagAllFilesWithUniqueId(deltaLog: DeltaLog, tagName: String): Unit = {\n    deltaLog.withNewTransaction { txn =>\n      val allFiles = txn.snapshot.allFiles.collect()\n      val allFilesWithTags = allFiles.map { addFile =>\n        addFile.copyWithTags(Map(tagName -> java.util.UUID.randomUUID().toString))\n      }\n      txn.commit(allFilesWithTags, DeltaOperations.ManualUpdate)\n    }\n    // Double check that the result is as expected.\n    val snapshotWithTags = deltaLog.update()\n    val filesWithTags = snapshotWithTags.allFiles.collect()\n    assert(filesWithTags.forall(_.tags.get(tagName).isDefined))\n    assert(filesWithTags.map(_.tags(tagName)).toSet.size === filesWithTags.size)\n  }\n\n  private def runAndValidateCloneWithDVs(\n    source: String,\n    target: String,\n    isShallow: Boolean,\n    expectedNumFilesWithDVs: Int,\n    isReplaceOperation: Boolean = false): Unit = {\n    val sourceDeltaLog = DeltaLog.forTable(spark, source)\n    // Add a unique tag to each file, so we can use this later to match up pre-/post-clone entries\n    // without having to resolve all the possible combinations of relative vs. absolute paths.\n    val uniqueIdTag = \"unique-file-id\"\n    tagAllFilesWithUniqueId(sourceDeltaLog, uniqueIdTag)\n\n    val targetDeltaLog = DeltaLog.forTable(spark, target)\n    val filesWithDVsInSource = getFilesWithDeletionVectors(sourceDeltaLog)\n    assert(filesWithDVsInSource.size === expectedNumFilesWithDVs)\n    val numberOfUniqueDVFilesInSource = filesWithDVsInSource\n      .map(_.deletionVector.pathOrInlineDv)\n      .toSet\n      .size\n\n    runAndValidateClone(\n      source,\n      target,\n      isShallow,\n      isReplaceOperation = isReplaceOperation)()\n    val filesWithDVsInTarget = getFilesWithDeletionVectors(targetDeltaLog)\n    val numberOfUniqueDVFilesInTarget = filesWithDVsInTarget\n      .map(_.deletionVector.pathOrInlineDv)\n      .toSet\n      .size\n    // Make sure we didn't accidentally copy some file multiple times.\n    assert(numberOfUniqueDVFilesInSource === numberOfUniqueDVFilesInTarget)\n    // Check contents of the copied DV files.\n    val filesWithDVsInTargetByUniqueId = filesWithDVsInTarget\n      .map(addFile => addFile.tags(uniqueIdTag) -> addFile)\n      .toMap\n    // scalastyle:off deltahadoopconfiguration\n    val hadoopConf = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    for (sourceFile <- filesWithDVsInSource) {\n      val targetFile = filesWithDVsInTargetByUniqueId(sourceFile.tags(uniqueIdTag))\n      if (sourceFile.deletionVector.isInline) {\n        assert(targetFile.deletionVector.isInline)\n        assert(sourceFile.deletionVector.inlineData === targetFile.deletionVector.inlineData)\n      } else {\n        def readDVData(path: Path): Array[Byte] = {\n          val fs = path.getFileSystem(hadoopConf)\n          val size = fs.getFileStatus(path).getLen\n          val data = new Array[Byte](size.toInt)\n          Utils.tryWithResource(fs.open(path)) { reader =>\n            reader.readFully(data)\n          }\n          data\n        }\n        val sourceDVPath = sourceFile.deletionVector.absolutePath(sourceDeltaLog.dataPath)\n        val targetDVPath = targetFile.deletionVector.absolutePath(targetDeltaLog.dataPath)\n        val sourceData = readDVData(sourceDVPath)\n        val targetData = readDVData(targetDVPath)\n        assert(sourceData === targetData)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CloneTableScalaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.time.LocalDate\n\nimport org.apache.spark.sql.delta.util.AnalysisHelper\nimport org.apache.hadoop.fs.Path\n\nclass CloneTableScalaSuite extends CloneTableSuiteBase\n    with CloneTableScalaTestMixin\n    with AnalysisHelper\n    with DeltaColumnMappingTestUtils {\n\n  import testImplicits._\n\n  testAllClones(\"cloneAtVersion API\") { (source, target, isShallow) =>\n    spark.range(5).write.format(\"delta\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n\n    val sourceTbl = io.delta.tables.DeltaTable.forPath(source)\n    assert(spark.read.format(\"delta\").load(source).count() === 15)\n\n    runAndValidateClone(source, target, isShallow, sourceVersion = Some(0)) {\n      () => {\n        sourceTbl.cloneAtVersion(0, target, isShallow)\n      }\n    }\n  }\n\n  test(\"deep clone not supported yet\") {\n    withSourceTargetDir { (source, clone) =>\n      checkError(\n        intercept[DeltaIllegalArgumentException] {\n          val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\").withColumn(\"part\", 'id % 2)\n          val df2 = Seq(8, 9, 10).toDF(\"id\").withColumn(\"part\", 'id % 2)\n          df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n          df2.write.format(\"delta\").mode(\"append\").save(source)\n\n          runAndValidateClone(source, clone, isShallow = false)()\n        },\n        \"DELTA_UNSUPPORTED_DEEP_CLONE\"\n      )\n    }\n  }\n\n  testAllClones(\"clone API\") { (source, target, isShallow) =>\n    spark.range(5).write.format(\"delta\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n\n    val sourceTbl = io.delta.tables.DeltaTable.forPath(source)\n    assert(spark.read.format(\"delta\").load(source).count() === 15)\n\n    runAndValidateClone(source, target, isShallow) {\n      () => {\n        sourceTbl.clone(target, isShallow)\n      }\n    }\n  }\n\n  testAllClones(\"cloneAtTimestamp API\") { (source, target, isShallow) =>\n    spark.range(5).write.format(\"delta\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n\n    val sourceTbl = io.delta.tables.DeltaTable.forPath(source)\n    assert(spark.read.format(\"delta\").load(source).count() === 15)\n\n    val desiredTime = LocalDate.now().minusDays(5).toString // Date as of 5 days old\n    val format = new java.text.SimpleDateFormat(\"yyyy-MM-dd\")\n    val time = format.parse(desiredTime).getTime\n\n    val path = new Path(source + \"/_delta_log/00000000000000000000.json\")\n    // scalastyle:off deltahadoopconfiguration\n    val fs = path.getFileSystem(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    fs.setTimes(path, time, 0)\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      InCommitTimestampTestUtils.overwriteICTInDeltaFile(\n        DeltaLog.forTable(spark, source),\n        path,\n        Some(time))\n    }\n\n    runAndValidateClone(source, target, isShallow, sourceTimestamp = Some(desiredTime)) {\n      () => {\n        sourceTbl.cloneAtTimestamp(desiredTime, target, isShallow)\n      }\n    }\n  }\n}\n\nclass CloneTableScalaIdColumnMappingSuite\n    extends CloneTableScalaSuite\n    with CloneTableColumnMappingSuiteBase\n    with DeltaColumnMappingEnableIdMode {\n\n  override protected def runOnlyTests: Seq[String] = super.runOnlyTests ++ Seq(\n    \"cloneAtVersion API\",\n    \"clone API\",\n    \"cloneAtTimestamp API\"\n  )\n}\n\nclass CloneTableScalaNameColumnMappingSuite\n    extends CloneTableScalaSuite\n    with CloneTableColumnMappingNameSuiteBase\n    with DeltaColumnMappingEnableNameMode {\n\n  override protected def runOnlyTests: Seq[String] = super.runOnlyTests ++ Seq(\n    \"cloneAtVersion API\",\n    \"clone API\",\n    \"cloneAtTimestamp API\"\n  )\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CloneTableSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.time.LocalDate\n\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction, Metadata, Protocol, RemoveFile, SetTransaction, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION\nimport org.apache.spark.sql.delta.commands._\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite}\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsTestUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.util.FileNames.{isCheckpointFile, unsafeDeltaFile}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, StructType}\nimport org.apache.spark.util.Utils\n\ntrait CloneTableSuiteBase extends QueryTest\n  with SharedSparkSession\n  with CloneTableTestMixin\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils\n  with CatalogOwnedTestBaseSuite\n  with CoordinatedCommitsTestUtils\n  with DeletionVectorsTestUtils {\n\n  import testImplicits._\n\n  protected def deleteSourceAndCompareData(\n      source: String,\n      actual: => DataFrame,\n      expected: DataFrame): Unit = {\n    Utils.deleteRecursively(new File(source))\n    checkAnswer(actual, expected)\n  }\n\n  protected def verifyAllFilePaths(\n      table: String,\n      targetIsTable: Boolean = false,\n      expectAbsolute: Boolean): Unit = {\n    val targetLog = if (targetIsTable) {\n      DeltaLog.forTable(spark, TableIdentifier(table))\n    } else {\n      DeltaLog.forTable(spark, table)\n    }\n    assert(targetLog.unsafeVolatileSnapshot.allFiles.collect()\n          .forall(p => new Path(p.pathAsUri).isAbsolute == expectAbsolute))\n  }\n\n  protected def customConvertToDelta(internal: String, external: String): Unit = {\n    ConvertToDeltaCommand(\n      TableIdentifier(external, Some(\"parquet\")),\n      Option(new StructType().add(\"part\", IntegerType)),\n      collectStats = true,\n      Some(internal)).run(spark)\n  }\n\n   // Test a basic clone with different syntaxes\n  protected def testSyntax(\n      source: String,\n      target: String,\n      sqlString: String,\n      isShallow: Boolean = true,\n      targetIsTable: Boolean = false): Unit = {\n    withTable(source) {\n      spark.range(5).write.format(\"delta\").saveAsTable(source)\n      runAndValidateClone(\n        source,\n        target,\n        isShallow,\n        sourceIsTable = true,\n        targetIsTable = targetIsTable) {\n        () => sql(sqlString)\n      }\n    }\n  }\n\n  cloneTest(\"simple shallow clone\", TAG_HAS_SHALLOW_CLONE) { (source, clone) =>\n    val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\").withColumn(\"part\", 'id % 2)\n    val df2 = Seq(8, 9, 10).toDF(\"id\").withColumn(\"part\", 'id % 2)\n    df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n    df2.write.format(\"delta\").mode(\"append\").save(source)\n\n    runAndValidateClone(\n      source,\n      clone,\n      isShallow = true\n    )()\n    // no files should be copied\n    val cloneDir = new File(clone).list()\n    assert(cloneDir.length === 1,\n      s\"There should only be a _delta_log directory but found:\\n${cloneDir.mkString(\"\\n\")}\")\n\n    val cloneLog = DeltaLog.forTable(spark, clone)\n    assert(cloneLog.snapshot.version === 0)\n    assert(cloneLog.snapshot.metadata.partitionColumns === Seq(\"part\"))\n    val files = cloneLog.snapshot.allFiles.collect()\n    assert(files.forall(_.pathAsUri.toString.startsWith(\"file:/\")), \"paths must be absolute\")\n\n    checkAnswer(\n      spark.read.format(\"delta\").load(clone),\n      df1.union(df2)\n    )\n  }\n\n  cloneTest(\"shallow clone a shallow clone\", TAG_HAS_SHALLOW_CLONE) { (source, clone) =>\n    val shallow1 = new File(clone, \"shallow1\").getCanonicalPath\n    val shallow2 = new File(clone, \"shallow2\").getCanonicalPath\n    val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\").withColumn(\"part\", 'id % 2)\n    df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n\n    runAndValidateClone(\n      source,\n      shallow1,\n      isShallow = true\n    )()\n\n    runAndValidateClone(\n      shallow1,\n      shallow2,\n      isShallow = true\n    )()\n\n    deleteSourceAndCompareData(shallow1, spark.read.format(\"delta\").load(shallow2), df1)\n  }\n\n  testAllClones(s\"validate commitLarge usage metrics\") { (source, clone, isShallow) =>\n    val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\").withColumn(\"part\", 'id % 5)\n    df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n    val df2 = Seq(1, 2).toDF(\"id\").withColumn(\"part\", 'id % 5)\n    df2.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n\n    val numAbsolutePathsInAdd = if (isShallow) 7 else 0\n    val commitLargeMetricsMap = Map(\n      \"numAdd\" -> \"7\",\n      \"numRemove\" -> \"0\",\n      \"numFilesTotal\" -> \"7\",\n      \"numCdcFiles\" -> \"0\",\n      \"commitVersion\" -> \"0\",\n      \"readVersion\" -> \"0\",\n      \"numAbsolutePathsInAdd\" -> s\"$numAbsolutePathsInAdd\",\n      \"startVersion\" -> \"-1\",\n      \"numDistinctPartitionsInAdd\" -> \"5\")\n    runAndValidateClone(\n      source,\n      clone,\n      isShallow,\n      commitLargeMetricsMap = commitLargeMetricsMap)()\n\n    checkAnswer(\n      spark.read.format(\"delta\").load(clone),\n      df1.union(df2)\n    )\n  }\n\n  cloneTest(\"shallow clone across file systems\", TAG_HAS_SHALLOW_CLONE) { (source, clone) =>\n    withSQLConf(\n        \"fs.s3.impl\" -> classOf[S3LikeLocalFileSystem].getName,\n        \"fs.s3.impl.disable.cache\" -> \"true\") {\n      val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\")\n      df1.write.format(\"delta\").mode(\"append\").save(s\"s3:$source\")\n\n      runAndValidateClone(\n        s\"s3:$source\",\n        s\"file:$clone\",\n        isShallow = true\n      )()\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(clone),\n        df1\n      )\n\n      val cloneLog = DeltaLog.forTable(spark, clone)\n      assert(cloneLog.snapshot.version === 0)\n      val files = cloneLog.snapshot.allFiles.collect()\n      assert(files.forall(_.pathAsUri.toString.startsWith(\"s3:/\")))\n    }\n  }\n\n  testAllClones(\"Negative test: clone into a non-empty directory that has a path based \" +\n    \"delta table\") { (source, clone, isShallow) =>\n    // Create table to clone\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n\n    // Table already exists at destination directory\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(clone)\n\n    // Clone should fail since destination directory is non-empty\n    val ex = intercept[AnalysisException] {\n      runAndValidateClone(\n        source,\n        clone,\n        isShallow\n      )()\n    }\n    assert(ex.getMessage.contains(\"is not empty\"))\n  }\n\n  cloneTest(\"Negative test: cloning into a non-empty parquet directory\",\n      TAG_HAS_SHALLOW_CLONE) { (source, clone) =>\n    // Create table to clone\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n\n    // Table already exists at destination directory\n    spark.range(5).write.format(\"parquet\").mode(\"overwrite\").save(clone)\n\n    // Clone should fail since destination directory is non-empty\n    val ex = intercept[AnalysisException] {\n      sql(s\"CREATE TABLE delta.`$clone` SHALLOW CLONE delta.`$source`\")\n    }\n    assert(ex.getMessage.contains(\"is not empty and also not a Delta table\"))\n  }\n\n  testAllClones(\n    \"Changes to clones only affect the cloned directory\") { (source, target, isShallow) =>\n    // Create base directory\n    Seq(1, 2, 3, 4, 5).toDF(\"id\").write.format(\"delta\").save(source)\n\n    // Create a clone\n    runAndValidateClone(\n      source,\n      target,\n      isShallow\n    )()\n\n    // Write to clone should be visible\n    Seq(6, 7, 8).toDF(\"id\").write.format(\"delta\").mode(\"append\").save(target)\n    assert(spark.read.format(\"delta\").load(target).count() === 8)\n\n    // Write to clone should not be visible in original table\n    assert(spark.read.format(\"delta\").load(source).count() === 5)\n  }\n\n  testAllClones(\"simple clone of source using table name\") { (_, target, isShallow) =>\n    val tableName = \"source\"\n    withTable(tableName) {\n      spark.range(5).write.format(\"delta\").saveAsTable(tableName)\n      runAndValidateClone(\n        tableName,\n        target,\n        isShallow,\n        sourceIsTable = true)()\n    }\n  }\n\n  testAllClones(\"clone a time traveled source using version\") { (_, target, isShallow) =>\n    val tableName = \"source\"\n    withTable(tableName) {\n      spark.range(5).write.format(\"delta\").saveAsTable(tableName)\n      spark.range(5).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n      spark.range(5).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n      spark.range(5).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n      assert(DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))._2.version === 3)\n\n      runAndValidateClone(\n        tableName,\n        target,\n        isShallow,\n        sourceIsTable = true,\n        sourceVersion = Some(2))()\n      assert(spark.read.format(\"delta\").load(target).count() === 15)\n    }\n  }\n\n  Seq(true, false).foreach { isCreate =>\n    cloneTest(s\"create or replace table - shallow, isCreate: $isCreate\",\n        TAG_HAS_SHALLOW_CLONE) { (_, _) =>\n      val tbl = \"source\"\n      val target = \"target\"\n      withTable(tbl, target) {\n        spark.range(5).write.format(\"delta\").saveAsTable(tbl)\n        spark.range(25).write.format(\"delta\").saveAsTable(target)\n\n        runAndValidateClone(\n          tbl,\n          target,\n          isShallow = true,\n          sourceIsTable = true,\n          targetIsTable = true,\n          isCreate = isCreate,\n          isReplaceOperation = true)()\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isCreate =>\n    Seq(\"parquet\", \"json\").foreach { format =>\n      cloneTest(s\"create or replace non Delta table - shallow, isCreate: $isCreate, \" +\n          s\"format: $format\", TAG_HAS_SHALLOW_CLONE) { (_, _) =>\n        val tbl = \"source\"\n        val target = \"target\"\n        withTable(tbl, target) {\n          spark.range(5).write.format(\"delta\").saveAsTable(tbl)\n          spark.range(25).write.format(format).saveAsTable(target)\n\n          runAndValidateClone(\n            tbl,\n            target,\n            isShallow = true,\n            sourceIsTable = true,\n            targetIsTable = true,\n            isCreate = isCreate,\n            isReplaceOperation = true,\n            isReplaceDelta = false)()\n        }\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isCreate =>\n    cloneTest(s\"shallow clone a table unto itself, isCreate: $isCreate\",\n        TAG_HAS_SHALLOW_CLONE) { (_, _) =>\n      val tbl = \"source\"\n      withTable(tbl) {\n        spark.range(5).write.format(\"delta\").saveAsTable(tbl)\n\n        runAndValidateClone(\n          tbl,\n          tbl,\n          isShallow = true,\n          sourceIsTable = true,\n          targetIsTable = true,\n          isCreate = isCreate,\n          isReplaceOperation = true)()\n\n        val allFiles =\n          DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tbl))._2.allFiles.collect()\n        allFiles.foreach { file =>\n          assert(!file.pathAsUri.isAbsolute, \"File paths should not be absolute\")\n        }\n      }\n    }\n  }\n\n  cloneTest(\"CLONE ignores reader/writer session defaults\", TAG_HAS_SHALLOW_CLONE) {\n    (source, clone) =>\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        cancel(\"Expects base protocol version.\")\n      }\n      withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        // Create table without a default property setting.\n        spark.range(1L).write.format(\"delta\").mode(\"overwrite\").save(source)\n        val oldProtocol = DeltaLog.forTable(spark, source).update().protocol\n        assert(oldProtocol === Protocol(1, 1))\n        // Just use something that can be default.\n        withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"2\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\",\n          TableFeatureProtocolUtils.defaultPropertyKey(TestWriterFeature) -> \"enabled\") {\n          // Clone in a session with default properties and check that they aren't merged\n          // (i.e. target properties are identical to source properties).\n          runAndValidateClone(\n            source,\n            clone,\n            isShallow = true\n          )()\n        }\n\n        val log = DeltaLog.forTable(spark, clone)\n        val targetProtocol = log.update().protocol\n        assert(targetProtocol === oldProtocol)\n      }\n  }\n\n  testAllClones(\"clone a time traveled source using timestamp\") { (source, clone, isShallow) =>\n    // Create source\n    spark.range(5).write.format(\"delta\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n    spark.range(5).write.format(\"delta\").mode(\"append\").save(source)\n    assert(spark.read.format(\"delta\").load(source).count() === 15)\n\n    // Get time corresponding to date\n    val desiredTime = LocalDate.now().minusDays(5).toString // Date as of 5 days old\n    val format = new java.text.SimpleDateFormat(\"yyyy-MM-dd\")\n    val time = format.parse(desiredTime).getTime\n\n    // Change modification time of commit\n    val path = new Path(source + \"/_delta_log/00000000000000000000.json\")\n    // scalastyle:off deltahadoopconfiguration\n    val fs = path.getFileSystem(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    fs.setTimes(path, time, 0)\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      InCommitTimestampTestUtils.overwriteICTInDeltaFile(\n        DeltaLog.forTable(spark, source),\n        path,\n        Some(time))\n    }\n\n    runAndValidateClone(\n      source,\n      clone,\n      isShallow,\n      sourceTimestamp = Some(desiredTime))()\n  }\n\n  cloneTest(\"clones take protocol from the source\",\n    TAG_HAS_SHALLOW_CLONE, TAG_MODIFY_PROTOCOL, TAG_CHANGE_COLUMN_MAPPING_MODE) { (source, clone) =>\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"table needs to start with custom protocol versions but enabling \" +\n        \"catalogOwned automatically upgrades table protocol version.\")\n    }\n    // Change protocol versions of (read, write) = (2, 5). We cannot initialize this to (0, 0)\n    // because min reader and writer versions are at least 1.\n    val defaultNewTableProtocol = Protocol.forNewTable(spark, metadataOpt = None)\n    val sourceProtocol = Protocol(2, 5)\n    // Make sure this is actually an upgrade. Downgrades are not supported, and if it's the same\n    // version, we aren't testing anything there.\n    assert(sourceProtocol.minWriterVersion > defaultNewTableProtocol.minWriterVersion &&\n      sourceProtocol.minReaderVersion > defaultNewTableProtocol.minReaderVersion)\n    val log = DeltaLog.forTable(spark, source)\n    // make sure to have a dummy schema because we can't have empty schema table by default\n    val newSchema = new StructType().add(\"id\", IntegerType, nullable = true)\n    log.createLogDirectoriesIfNotExists()\n    log.store.write(\n      unsafeDeltaFile(log.logPath, 0),\n      Iterator(Metadata(schemaString = newSchema.json).json, sourceProtocol.json),\n      overwrite = false,\n      log.newDeltaHadoopConf())\n    log.update()\n\n    // Validate that clone has the new protocol version\n    runAndValidateClone(\n      source,\n      clone,\n      isShallow = true\n    )()\n  }\n\n  testAllClones(\"clones take the set transactions of the source\") { (_, target, isShallow) =>\n    withTempDir { dir =>\n      // Create source\n      val path = dir.getCanonicalPath\n      spark.range(5).write.format(\"delta\").save(path)\n\n      // Add a Set Transaction\n      val log = DeltaLog.forTable(spark, path)\n      val txn = log.startTransaction()\n      val setTxn = SetTransaction(\"app-id\", 0, Some(0L)) :: Nil\n      val op = DeltaOperations.StreamingUpdate(OutputMode.Complete(), \"app-id\", 0L)\n      txn.commit(setTxn, op)\n      log.update()\n\n      runAndValidateClone(\n        path,\n        target,\n        isShallow\n      )()\n    }\n  }\n\n  testAllClones(\"CLONE with table properties to disable DV\") { (source, target, isShallow) =>\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\",\n        DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> \"true\") {\n      spark.range(10).write.format(\"delta\").save(source)\n      spark.sql(s\"DELETE FROM delta.`$source` WHERE id = 1\")\n    }\n    intercept[DeltaCommandUnsupportedWithDeletionVectorsException] {\n      runAndValidateClone(\n        source,\n        target,\n        isShallow,\n        tableProperties = Map(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key -> \"false\"))()\n    }.getErrorClass === \"DELTA_ADDING_DELETION_VECTORS_DISALLOWED\"\n  }\n\n  for(targetExists <- BOOLEAN_DOMAIN)\n  testAllClones(s\"CLONE respects table features set by table property override, \" +\n    s\"targetExists=$targetExists\", TAG_MODIFY_PROTOCOL) {\n    (source, target, isShallow) =>\n      spark.range(10).write.format(\"delta\").save(source)\n\n      if (targetExists) {\n        spark.range(0).write.format(\"delta\").save(target)\n      }\n\n      val tblPropertyOverrides =\n        Seq(\n          s\"delta.feature.${TestWriterFeature.name}\" -> \"enabled\",\n          \"delta.minWriterVersion\" -> s\"$TABLE_FEATURES_MIN_WRITER_VERSION\").toMap\n      cloneTable(\n        source,\n        target,\n        isShallow,\n        isReplace = true,\n        tableProperties = tblPropertyOverrides)\n\n      val targetLog = DeltaLog.forTable(spark, target)\n      assert(targetLog.update().protocol.isFeatureSupported(TestWriterFeature))\n  }\n\n  case class TableFeatureWithProperty(\n      feature: TableFeature,\n      property: DeltaConfig[Boolean])\n\n  // Delta properties that automatically cause a version upgrade when enabled via ALTER TABLE.\n  final val featuresWithAutomaticProtocolUpgrade: Seq[TableFeatureWithProperty] = Seq(\n    TableFeatureWithProperty(ChangeDataFeedTableFeature, DeltaConfigs.CHANGE_DATA_FEED),\n    TableFeatureWithProperty(\n      DeletionVectorsTableFeature, DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION))\n  // This test ensures this upgrade also happens when enabled during a CLONE.\n  for (featureWithProperty <- featuresWithAutomaticProtocolUpgrade)\n    testAllClones(\"Cloning a table with new table properties\" +\n      s\" that force protocol version upgrade - ${featureWithProperty.property.key}\"\n    ) { (source, target, isShallow) =>\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        cancel(\"table needs to start with default protocol versions but enabling \" +\n          \"catalogOwned upgrades table protocol version.\")\n      }\n      import DeltaTestUtils.StrictProtocolOrdering\n\n      spark.range(5).write.format(\"delta\").save(source)\n      val sourceDeltaLog = DeltaLog.forTable(spark, source)\n      val sourceSnapshot = sourceDeltaLog.update()\n      // This only works if the featureWithProperty is not enabled by default.\n      assert(!featureWithProperty.property.fromMetaData(sourceSnapshot.metadata))\n      // Check that the original version is not already sufficient for the featureWithProperty.\n      assert(!StrictProtocolOrdering.fulfillsVersionRequirements(\n        actual = sourceSnapshot.protocol,\n        requirement = featureWithProperty.feature.minProtocolVersion\n      ))\n\n      // Clone the table, enabling the featureWithProperty in an override.\n      val tblProperties = Map(featureWithProperty.property.key -> \"true\")\n      cloneTable(\n        source,\n        target,\n        isShallow,\n        isReplace = true,\n        tableProperties = tblProperties)\n\n      val targetDeltaLog = DeltaLog.forTable(spark, target)\n      val targetSnapshot = targetDeltaLog.update()\n      assert(targetSnapshot.metadata.configuration ===\n        sourceSnapshot.metadata.configuration ++ tblProperties)\n      // Check that the protocol has been upgraded.\n      assert(StrictProtocolOrdering.fulfillsVersionRequirements(\n        actual = targetSnapshot.protocol,\n        requirement = featureWithProperty.feature.minProtocolVersion\n      ))\n    }\n\n  testAllClones(\"Cloning a table without DV property should not upgrade protocol version\"\n  ) { (source, target, isShallow) =>\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"table needs to start with default protocol versions but enabling \" +\n        \"catalogOwned upgrades table protocol version.\")\n    }\n    import DeltaTestUtils.StrictProtocolOrdering\n\n    spark.range(5).write.format(\"delta\").save(source)\n    withSQLConf(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\") {\n      val sourceDeltaLog = DeltaLog.forTable(spark, source)\n      val sourceSnapshot = sourceDeltaLog.update()\n      // Should not be enabled, just because it's allowed.\n      assert(!DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(sourceSnapshot.metadata))\n      // Check that the original version is not already sufficient for the feature.\n      assert(!StrictProtocolOrdering.fulfillsVersionRequirements(\n        actual = sourceSnapshot.protocol,\n        requirement = DeletionVectorsTableFeature.minProtocolVersion\n      ))\n\n      // Clone the table.\n      cloneTable(\n        source,\n        target,\n        isShallow,\n        isReplace = true)\n\n      val targetDeltaLog = DeltaLog.forTable(spark, target)\n      val targetSnapshot = targetDeltaLog.update()\n      // Protocol should not have been upgraded.\n      assert(sourceSnapshot.protocol === targetSnapshot.protocol)\n    }\n  }\n}\n\n\ntrait CloneTableColumnMappingSuiteBase\n  extends CloneTableSuiteBase\n    with DeltaColumnMappingSelectedTestMixin\n{\n\n  override protected def runOnlyTests: Seq[String] = Seq(\n    \"simple shallow clone\",\n    \"shallow clone a shallow clone\",\n    \"create or replace table - shallow, isCreate: false\",\n    \"create or replace table - shallow, isCreate: true\",\n    \"shallow clone a table unto itself, isCreate: false\",\n    \"shallow clone a table unto itself, isCreate: true\",\n    \"clone a time traveled source using version\",\n    \"clone a time traveled source using timestamp\",\n    \"validate commitLarge usage metrics\",\n    \"clones take the set transactions of the source\",\n    \"block changing column mapping mode and modify max id modes under CLONE\"\n  )\n\n  import testImplicits._\n\n  testAllClones(\"block changing column mapping mode and modify max id modes under CLONE\") {\n    (_, _, isShallow) =>\n      val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\").withColumn(\"part\", 'id % 2)\n\n      // block setting max id\n      def validateModifyMaxIdError(f: => Any): Unit = {\n        val e = intercept[UnsupportedOperationException] { f }\n        assert(e.getMessage == DeltaErrors.cannotModifyTableProperty(\n          DeltaConfigs.COLUMN_MAPPING_MAX_ID.key\n        ).getMessage)\n      }\n\n      withSourceTargetDir { (source, target) =>\n        df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n        // change max id w/ table property should be blocked\n        validateModifyMaxIdError {\n          cloneTable(\n            source,\n            target,\n            isShallow,\n            tableProperties = Map(\n              DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> \"123123\"\n          ))\n        }\n        // change max id w/ SQLConf should be blocked by table property guard\n        validateModifyMaxIdError {\n          withMaxColumnIdConf(\"123123\") {\n            cloneTable(\n              source,\n              target,\n              isShallow\n            )\n          }\n        }\n      }\n\n      // block changing column mapping mode\n      def validateChangeModeError(f: => Any): Unit = {\n        val e = intercept[ColumnMappingUnsupportedException] { f }\n        assert(e.getMessage.contains(\"Changing column mapping mode from\"))\n      }\n\n      val currentMode = columnMappingModeString\n\n      // currentMode to otherMode\n      val otherMode = if (currentMode == \"id\") \"name\" else \"id\"\n      withSourceTargetDir { (source, target) =>\n        df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n        // change mode w/ table property should be blocked\n        validateChangeModeError {\n          cloneTable(\n            source,\n            target,\n            isShallow,\n            tableProperties = Map(\n              DeltaConfigs.COLUMN_MAPPING_MODE.key -> otherMode\n          ))\n        }\n      }\n\n      withSourceTargetDir { (source, target) =>\n        df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n        // change mode w/ SQLConf should have no effects\n        withColumnMappingConf(otherMode) {\n          cloneTable(\n            source,\n            target,\n            isShallow\n          )\n        }\n        assert(DeltaLog.forTable(spark, target).snapshot.metadata.columnMappingMode.name ==\n          currentMode)\n      }\n\n      // currentMode to none\n      withSourceTargetDir { (source, target) =>\n        df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n        // change mode w/ table property\n        validateChangeModeError {\n          cloneTable(\n            source,\n            target,\n            isShallow,\n            tableProperties = Map(\n              DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"none\"\n          ))\n        }\n      }\n      withSourceTargetDir { (source, target) =>\n        df1.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(source)\n        // change mode w/ SQLConf should have no effects\n        withColumnMappingConf(\"none\") {\n          cloneTable(\n            source,\n            target,\n            isShallow\n          )\n        }\n        assert(DeltaLog.forTable(spark, target).snapshot.metadata.columnMappingMode.name ==\n          currentMode)\n      }\n  }\n}\n\ntrait CloneTableColumnMappingNameSuiteBase extends CloneTableColumnMappingSuiteBase {\n  override protected def customConvertToDelta(internal: String, external: String): Unit = {\n    withColumnMappingConf(\"none\") {\n      super.customConvertToDelta(internal, external)\n      sql(\n        s\"\"\"ALTER TABLE delta.`$internal` SET TBLPROPERTIES (\n           |${DeltaConfigs.COLUMN_MAPPING_MODE.key} = 'name',\n           |${DeltaConfigs.MIN_READER_VERSION.key} = '2',\n           |${DeltaConfigs.MIN_WRITER_VERSION.key} = '5'\n           |)\"\"\".stripMargin)\n        .collect()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CloneTableTestMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.Locale\n\nimport scala.jdk.CollectionConverters._\n\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction, RemoveFile, SingleAction}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.{CloneDeltaSource, CloneSource, CloneSourceFormat}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.FileNames.unsafeDeltaFile\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.{BeforeAndAfterAll, Tag}\n\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.connector.catalog.CatalogManager\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\n/** Common test setup and utils for CLONE TABLE tests. */\ntrait CloneTableTestMixin extends DeltaColumnMappingTestUtils\n  with BeforeAndAfterAll\n  with DeltaTestUtilsBase {\n  self: QueryTest with SharedSparkSession =>\n\n  protected val TAG_HAS_SHALLOW_CLONE = new Tag(\"SHALLOW CLONE\")\n  protected val TAG_MODIFY_PROTOCOL = new Tag(\"CHANGES PROTOCOL\")\n  protected val TAG_CHANGE_COLUMN_MAPPING_MODE = new Tag(\"CHANGES COLUMN MAPPING MODE\")\n  protected val TAG_USES_CONVERT_TO_DELTA = new Tag(\"USES CONVERT TO DELTA\")\n\n  // scalastyle:off argcount\n  protected def cloneTable(\n      source: String,\n      target: String,\n      isShallow: Boolean,\n      sourceIsTable: Boolean = false,\n      targetIsTable: Boolean = false,\n      targetLocation: Option[String] = None,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      isCreate: Boolean = true,\n      isReplace: Boolean = false,\n      tableProperties: Map[String, String] = Map.empty): Unit\n  // scalastyle:on argcount\n\n  protected def withSourceTargetDir(f: (String, String) => Unit): Unit = {\n    withTempDir { dir =>\n      val firstDir = new File(dir, \"source\").getCanonicalPath\n      val secondDir = new File(dir, \"clone\").getCanonicalPath\n      f(firstDir, secondDir)\n    }\n  }\n\n  protected def cloneTypeStr(isShallow: Boolean): String = {\n    \"SHALLOW\"\n  }\n\n  /**\n   * Run the given test function for SHALLOW clone.\n   */\n  protected def testAllClones(testName: String, testTags: org.scalatest.Tag*)\n      (testFunc: (String, String, Boolean) => Unit): Unit = {\n    val tags = Seq(TAG_HAS_SHALLOW_CLONE)\n    cloneTest(s\"$testName\", testTags ++ tags: _*) {\n      (source, target) => testFunc(source, target, true)\n    }\n  }\n\n  protected def cloneTest(\n      testName: String, testTags: org.scalatest.Tag*)(f: (String, String) => Unit): Unit = {\n    if (testTags.exists(_.name == TAG_CHANGE_COLUMN_MAPPING_MODE.name) &&\n        columnMappingMode != \"none\") {\n      ignore(testName + \" (not supporting changing column mapping mode)\") {\n        withSourceTargetDir(f)\n      }\n    } else {\n      test(testName, testTags: _*) {\n        withSourceTargetDir(f)\n      }\n    }\n  }\n\n  // Extracted function so it can be overriden in subclasses.\n  protected def uniqueFileActionGroupBy(action: FileAction): String = {\n    val filePath = action.pathAsUri.toString\n    val dvId = action match {\n      case add: AddFile => Option(add.deletionVector).map(_.uniqueId).getOrElse(\"\")\n      case remove: RemoveFile => Option(remove.deletionVector).map(_.uniqueId).getOrElse(\"\")\n      case _ => \"\"\n    }\n    filePath + dvId\n  }\n\n  import testImplicits._\n  // scalastyle:off\n  protected def runAndValidateClone(\n      source: String,\n      target: String,\n      isShallow: Boolean,\n      sourceIsTable: Boolean = false,\n      targetIsTable: Boolean = false,\n      targetLocation: Option[String] = None,\n      sourceVersion: Option[Long] = None,\n      sourceTimestamp: Option[String] = None,\n      isCreate: Boolean = true,\n      // If we are doing a replace on an existing table\n      isReplaceOperation: Boolean = false,\n      // If we are doing a replace, whether it is on a Delta table\n      isReplaceDelta: Boolean = true,\n      tableProperties: Map[String, String] = Map.empty,\n      commitLargeMetricsMap: Map[String, String] = Map.empty,\n      expectedDataframe: DataFrame = spark.emptyDataFrame)\n      (f: () => Unit =\n        () => cloneTable(\n          source,\n          target,\n          isShallow,\n          sourceIsTable,\n          targetIsTable,\n          targetLocation,\n          sourceVersion,\n          sourceTimestamp,\n          isCreate,\n          isReplaceOperation,\n          tableProperties)): Unit = {\n    // scalastyle:on\n    // Truncate table before REPLACE\n    try {\n      if (isReplaceOperation) {\n        val targetTbl = if (targetIsTable) {\n          target\n        } else {\n          s\"delta.`$target`\"\n        }\n        sql(s\"DELETE FROM $targetTbl\")\n      }\n    } catch {\n      case _: Throwable =>\n        // ignore all\n    }\n\n    // Check logged blob for expected values\n    val allLogs = Log4jUsageLogger.track {\n      f()\n    }\n    verifyAllCloneOperationsEmitted(allLogs,\n      isReplaceOperation && isReplaceDelta,\n      commitLargeMetricsMap)\n\n    val blob = JsonUtils.fromJson[Map[String, Any]](allLogs\n      .filter(_.metric == \"tahoeEvent\")\n      .filter(_.tags.get(\"opType\").contains(\"delta.clone\"))\n      .filter(_.blob.contains(\"source\"))\n      .map(_.blob).last)\n\n    val sourceIdent = resolveTableIdentifier(source, Some(\"delta\"), sourceIsTable)\n    val (cloneSource: CloneSource, sourceDf: DataFrame) = {\n      val sourceLog = DeltaLog.forTable(spark, sourceIdent)\n      val timeTravelSpec: Option[DeltaTimeTravelSpec] =\n        if (sourceVersion.isDefined || sourceTimestamp.isDefined) {\n          Some(DeltaTimeTravelSpec(sourceTimestamp.map(Literal(_)), sourceVersion, None))\n        } else {\n          None\n        }\n      val deltaTable = DeltaTableV2(spark, sourceLog.dataPath, timeTravelOpt = timeTravelSpec)\n      val sourceData = DataFrameUtils.ofRows(\n        spark,\n        LogicalRelation(sourceLog.createRelation(\n          snapshotToUseOpt = Some(deltaTable.initialSnapshot),\n          isTimeTravelQuery = sourceVersion.isDefined || sourceTimestamp.isDefined)))\n      (new CloneDeltaSource(deltaTable), sourceData)\n    }\n\n    val targetLog = if (targetIsTable) {\n      DeltaLog.forTable(spark, TableIdentifier(target))\n    } else {\n      DeltaLog.forTable(spark, target)\n    }\n\n    val sourceSnapshot = cloneSource.snapshot\n\n    val sourcePath = cloneSource.dataPath\n    // scalastyle:off deltahadoopconfiguration\n    val fs = sourcePath.getFileSystem(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    val qualifiedSourcePath = fs.makeQualified(sourcePath)\n    val logSource = if (sourceIsTable) {\n      val catalog = CatalogManager.SESSION_CATALOG_NAME\n      s\"$catalog.default.$source\".toLowerCase(Locale.ROOT)\n    } else {\n      s\"delta.`$qualifiedSourcePath`\"\n    }\n\n    val rawTarget = new Path(targetLocation.getOrElse(targetLog.dataPath.toString))\n    // scalastyle:off deltahadoopconfiguration\n    val targetFs = rawTarget.getFileSystem(targetLog.newDeltaHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    val qualifiedTarget = targetFs.makeQualified(rawTarget)\n\n    // Check whether recordEvent operation is of correct form\n    assert(blob(\"source\") != null)\n    val actualLogSource = blob(\"source\").toString\n    assert(actualLogSource === logSource)\n    if (source != target) {\n      assert(blob(\"sourceVersion\") === sourceSnapshot.get.version)\n    }\n    val replacingDeltaTable = isReplaceOperation && isReplaceDelta\n    assert(blob(\"sourcePath\") === qualifiedSourcePath.toString)\n    assert(blob(\"target\") === qualifiedTarget.toString)\n    assert(blob(\"isReplaceDelta\") === replacingDeltaTable)\n    assert(blob(\"sourceTableSize\") === cloneSource.sizeInBytes)\n    assert(blob(\"sourceNumOfFiles\") === cloneSource.numOfFiles)\n    assert(blob(\"partitionBy\") === cloneSource.metadata.partitionColumns)\n\n    // Check whether resulting metadata of target and source at version is the same\n    compareMetadata(\n      cloneSource,\n      targetLog.unsafeVolatileSnapshot,\n      targetLocation.isEmpty && targetIsTable,\n      isReplaceOperation)\n\n    val commit = unsafeDeltaFile(targetLog.logPath, targetLog.unsafeVolatileSnapshot.version)\n    val hadoopConf = targetLog.newDeltaHadoopConf()\n    val filePaths: Seq[FileAction] = targetLog.store.read(commit, hadoopConf).flatMap { line =>\n      JsonUtils.fromJson[SingleAction](line) match {\n        case a if a.add != null => Some(a.add)\n        case a if a.remove != null => Some(a.remove)\n        case _ => None\n      }\n    }\n    assert(filePaths.groupBy(uniqueFileActionGroupBy(_)).forall(_._2.length === 1),\n      \"A file was added and removed in the same commit\")\n    // Check whether the resulting datasets are the same\n    val targetDf = DataFrameUtils.ofRows(\n      spark,\n      LogicalRelation(targetLog.createRelation()))\n    checkAnswer(\n      targetDf,\n      sourceDf)\n  }\n\n\n  protected def verifyAllCloneOperationsEmitted(\n      allLogs: Seq[UsageRecord],\n      emitHandleExistingTable: Boolean,\n      commitLargeMetricsMap: Map[String, String] = Map.empty): Unit = {\n    val cloneLogs = allLogs\n      .filter(_.metric === \"sparkOperationDuration\")\n      .filter(_.opType.isDefined)\n      .filter(_.opType.get.typeName.contains(\"delta.clone\"))\n\n    assert(cloneLogs.count(_.opType.get.typeName.equals(\"delta.clone.makeAbsolute\")) == 1)\n\n    val commitStatsUsageRecords = allLogs\n      .filter(_.metric === \"tahoeEvent\")\n      .filter(_.tags.get(\"opType\") === Some(\"delta.commit.stats\"))\n    assert(commitStatsUsageRecords.length === 1)\n    val commitStatsMap = JsonUtils.fromJson[Map[String, Any]](commitStatsUsageRecords.head.blob)\n    commitLargeMetricsMap.foreach { case (name, expectedValue) =>\n      assert(commitStatsMap(name).toString == expectedValue,\n        s\"Expected value for $name metrics did not match with the captured value\")\n    }\n  }\n\n  private def compareMetadata(\n      cloneSource: CloneSource,\n      targetLog: Snapshot,\n      targetIsTable: Boolean,\n      isReplace: Boolean = false): Unit = {\n    val sourceMetadata = cloneSource.metadata\n    val targetMetadata = targetLog.metadata\n\n    /**\n     * Filter out row tracking properties from the configuration map.\n     * These properties are not expected to be the same in source and target metadata.\n     *\n     * @param conf The configuration map to filter.\n     * @return Filtered configuration map without row tracking properties.\n     */\n    def filterOutRowTrackingProps(conf: Map[String, String]): Map[String, String] = {\n      conf.filterNot { case (k, _) =>\n        k == MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP ||\n          k == MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP\n      }\n    }\n\n    val sourceConfigsWithoutRowTrackingProps =\n      filterOutRowTrackingProps(conf = sourceMetadata.configuration)\n    val targetConfigsWithoutRowTrackingProps =\n      filterOutRowTrackingProps(conf = targetMetadata.configuration)\n\n    assert(sourceMetadata.schema === targetMetadata.schema &&\n      sourceConfigsWithoutRowTrackingProps === targetConfigsWithoutRowTrackingProps &&\n      sourceMetadata.dataSchema === targetMetadata.dataSchema &&\n      sourceMetadata.partitionColumns === targetMetadata.partitionColumns &&\n      sourceMetadata.format === sourceMetadata.format)\n\n    // Protocol should be changed, if source.protocol >= target.protocol, otherwise target must\n    // retain it's existing protocol version (i.e. no downgrades).\n    assert(cloneSource.protocol === targetLog.protocol || (\n      cloneSource.protocol.minReaderVersion <= targetLog.protocol.minReaderVersion &&\n        cloneSource.protocol.minWriterVersion <= targetLog.protocol.minWriterVersion))\n\n    assert(targetLog.setTransactions.isEmpty)\n\n    if (!isReplace) {\n      assert(sourceMetadata.id != targetMetadata.id &&\n        targetMetadata.name === null &&\n        targetMetadata.description === null)\n    }\n  }\n\n  protected def resolveTableIdentifier(\n    name: String, format: Option[String], isTable: Boolean): TableIdentifier = {\n    if (isTable) {\n      TableIdentifier(name)\n    } else {\n      TableIdentifier(name, format)\n    }\n  }\n}\n\ntrait CloneTableSQLTestMixin extends CloneTableTestMixin {\n  self: QueryTest with SharedSparkSession =>\n\n  // scalastyle:off argcount\n  override protected def cloneTable(\n      source: String,\n      target: String,\n      isShallow: Boolean,\n      sourceIsTable: Boolean = false,\n      targetIsTable: Boolean = false,\n      targetLocation: Option[String] = None,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      isCreate: Boolean = true,\n      isReplace: Boolean = false,\n      tableProperties: Map[String, String] = Map.empty): Unit = {\n    val commandSql = CloneTableSQLTestUtils.buildCloneSqlString(\n      source, target,\n      sourceIsTable,\n      targetIsTable,\n      \"delta\",\n      targetLocation,\n      versionAsOf,\n      timestampAsOf,\n      isCreate,\n      isReplace,\n      tableProperties)\n    sql(commandSql)\n  }\n  // scalastyle:on argcount\n}\n\ntrait CloneTableScalaTestMixin extends CloneTableTestMixin {\n  self: QueryTest with SharedSparkSession =>\n\n  // scalastyle:off argcount\n  override protected def cloneTable(\n      source: String,\n      target: String,\n      isShallow: Boolean,\n      sourceIsTable: Boolean = false,\n      targetIsTable: Boolean = false,\n      targetLocation: Option[String] = None,\n      versionAsOf: Option[Long] = None,\n      timestampAsOf: Option[String] = None,\n      isCreate: Boolean = true,\n      isReplace: Boolean = false,\n      tableProperties: Map[String, String] = Map.empty): Unit = {\n    val table = if (sourceIsTable) {\n      io.delta.tables.DeltaTable.forName(spark, source)\n    } else {\n      io.delta.tables.DeltaTable.forPath(spark, source)\n    }\n\n    if (versionAsOf.isDefined) {\n      table.cloneAtVersion(versionAsOf.get,\n        target, isShallow = isShallow, replace = isReplace, tableProperties)\n    } else if (timestampAsOf.isDefined) {\n      table.cloneAtTimestamp(timestampAsOf.get,\n        target, isShallow = isShallow, replace = isReplace, tableProperties)\n    } else {\n      table.clone(target, isShallow = isShallow, replace = isReplace,\n        properties = new java.util.HashMap[String, String](tableProperties.asJava))\n    }\n  }\n  // scalastyle:on argcount\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CommitInfoSerializerSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{QueryTest, SaveMode}\nimport org.apache.spark.sql.catalyst.expressions.{EqualTo, Literal}\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}\n\n/**\n * Tests for the correct serialization and deserialization of CommitInfo.\n * The main focus is on the correct deserialization of operation parameters.\n * See [[JsonMapDeserializer]] for more details about how operation\n * parameter deserialization was broken before.\n */\nclass CommitInfoSerializerSuite extends QueryTest with SharedSparkSession {\n\n  def testOperationSerialization(operation: DeltaOperations.Operation): Unit = {\n    val commitInfo = CommitInfo(\n      time = 123L,\n      operation = operation.name,\n      inCommitTimestamp = Some(123L),\n      operationParameters = Map.empty,\n      commandContext = Map(\"clusterId\" -> \"23\"),\n      readVersion = Some(23),\n      isolationLevel = Some(\"SnapshotIsolation\"),\n      isBlindAppend = Some(true),\n      operationMetrics = Some(Map(\"m1\" -> \"v1\", \"m2\" -> \"v2\")),\n      userMetadata = Some(\"123\"),\n      tags = Some(Map(\"k1\" -> \"v1\")),\n      txnId = Some(\"123\")\n    ).copy(engineInfo = None)\n\n    val inMemoryCommitInfo = commitInfo.copy(operationParameters = operation.jsonEncodedValues)\n    val commitInfoSerialized = inMemoryCommitInfo.json\n    val roundTrippedCommitInfo = Action.fromJson(commitInfoSerialized).asInstanceOf[CommitInfo]\n    assert(roundTrippedCommitInfo.operationParameters == inMemoryCommitInfo.operationParameters)\n    assert(roundTrippedCommitInfo == inMemoryCommitInfo)\n\n    // Also ensure that CommitInfo.getLegacyOperationParameters is correct\n    val legacyPostDeserializationCommitInfo =\n      JsonUtils.mapper.readValue[ActionWrapper](commitInfoSerialized).commitInfo\n    val legacyOperationParametersActual =\n      CommitInfo.getLegacyPostDeserializationOperationParameters(\n        roundTrippedCommitInfo.operationParameters)\n    assert(\n      legacyOperationParametersActual == legacyPostDeserializationCommitInfo.operationParameters)\n  }\n\n  val testMetadata = Metadata(\n    id = \"test-id\",\n    name = \"test_table\",\n    description = \"Test table\",\n    format = Format(),\n    schemaString = StructType(Seq(\n      StructField(\"col1\", StringType),\n      StructField(\"col2\", IntegerType)\n    )).json,\n    partitionColumns = Seq(\"col1\"),\n    configuration = Map(\"property1\" -> \"value1\")\n  )\n\n  val oldSchema = StructType(Seq(StructField(\"col1\", StringType)))\n  val newSchema = StructType(Seq(StructField(\"col1\", StringType), StructField(\"col2\", IntegerType)))\n\n  val trackedOperationClasses = Map(\n    \"Convert\" -> (() => DeltaOperations.Convert(\n      numFiles = 23L,\n      partitionBy = Seq(\"a\", \"b\"),\n      collectStats = false,\n      catalogTable = Some(\"t1\"),\n      sourceFormat = Some(\"parquet\"))),\n    \"DomainMetadataCleanup\" -> (() => DeltaOperations.DomainMetadataCleanup(1)),\n    \"Write\" -> (() => DeltaOperations.Write(\n      mode = SaveMode.Append,\n      partitionBy = Some(Seq(\"col1\", \"col2\")),\n      predicate = Some(\"col1 > 10\"),\n      userMetadata = Some(\"test metadata\")\n    )),\n    \"StreamingUpdate\" -> (() => DeltaOperations.StreamingUpdate(\n      outputMode = OutputMode.Append(),\n      queryId = \"query-123\",\n      epochId = 42L,\n      userMetadata = Some(\"streaming metadata\")\n    )),\n    \"Delete\" -> (() => DeltaOperations.Delete(Seq(EqualTo(Literal(\"col1\"), Literal(\"value1\"))))),\n    \"Truncate\" -> (() => DeltaOperations.Truncate()),\n    \"Merge\" -> (() => DeltaOperations.Merge(\n      predicate = Some(EqualTo(Literal(\"source.id\"), Literal(\"target.id\"))),\n      updatePredicate = Some(\"source.value > target.value\"),\n      deletePredicate = Some(\"source.flag = 'delete'\"),\n      insertPredicate = Some(\"source.id IS NOT NULL\"),\n      matchedPredicates = Seq(DeltaOperations.MergePredicate(Some(\"matched\"), \"update\")),\n      notMatchedPredicates = Seq(DeltaOperations.MergePredicate(Some(\"not matched\"), \"insert\")),\n      notMatchedBySourcePredicates = Seq(\n        DeltaOperations.MergePredicate(Some(\"not matched by source\"), \"delete\"))\n    )),\n    \"Update\" -> (() => DeltaOperations.Update(Some(EqualTo(Literal(\"col1\"), Literal(\"value1\"))))),\n    \"CreateTable\" -> (() => DeltaOperations.CreateTable(\n      metadata = testMetadata,\n      isManaged = true,\n      asSelect = true,\n      clusterBy = Some(Seq(\"col1\"))\n    )),\n    \"ReplaceTable\" -> (() => DeltaOperations.ReplaceTable(\n      metadata = testMetadata,\n      isManaged = true,\n      orCreate = true,\n      asSelect = true,\n      userMetadata = Some(\"replace metadata\"),\n      clusterBy = Some(Seq(\"col2\")))),\n    \"SetTableProperties\" ->\n      (() => DeltaOperations.SetTableProperties(Map(\"key1\" -> \"value1\", \"key2\" -> \"value2\"))),\n    \"UnsetTableProperties\" ->\n      (() => DeltaOperations.UnsetTableProperties(Seq(\"key1\", \"key2\"), ifExists = true)),\n    \"DropTableFeature\" ->\n      (() => DeltaOperations.DropTableFeature(\"testFeature\", truncateHistory = true)),\n    \"AddColumns\" -> (() => DeltaOperations.AddColumns(Seq(\n      DeltaOperations.QualifiedColTypeWithPositionForLog(\n        Seq(\"newCol\"),\n        StructField(\"newCol\", StringType),\n        Some(\"AFTER col1\"))))),\n    \"DropColumns\" -> (() => DeltaOperations.DropColumns(Seq(Seq(\"col1\"), Seq(\"col2\")))),\n    \"RenameColumn\" -> (() => DeltaOperations.RenameColumn(Seq(\"oldCol\"), Seq(\"newCol\"))),\n    \"ChangeColumn\" -> (() => DeltaOperations.ChangeColumn(\n      columnPath = Seq(\"col1\"),\n      columnName = \"col1\",\n      newColumn = StructField(\"col1\", StringType),\n      colPosition = Some(\"FIRST\"))),\n    \"ChangeColumns\" -> (() => DeltaOperations.ChangeColumns(Seq(\n      DeltaOperations.ChangeColumn(\n        columnPath = Seq(\"col1\"),\n        columnName = \"col1\",\n        newColumn = StructField(\"col1\", StringType),\n        colPosition = Some(\"FIRST\"))))),\n    \"ReplaceColumns\" -> (() => DeltaOperations.ReplaceColumns(Seq(\n      StructField(\"newCol1\", StringType),\n      StructField(\"newCol2\", IntegerType)))),\n    \"UpgradeProtocol\" ->\n      (() => DeltaOperations.UpgradeProtocol(Protocol(minReaderVersion = 1, minWriterVersion = 2))),\n    \"UpdateColumnMetadata\" -> (() => DeltaOperations.UpdateColumnMetadata(\n      \"UPDATE COLUMN METADATA\",\n      Seq((Seq(\"col1\"), StructField(\"col1\", StringType))))),\n    \"UpdateSchema\" -> (() => DeltaOperations.UpdateSchema(oldSchema, newSchema)),\n    \"AddConstraint\" -> (() => DeltaOperations.AddConstraint(\"check_positive\", \"col1 > 0\")),\n    \"DropConstraint\" -> (() => DeltaOperations.DropConstraint(\"check_positive\", Some(\"col1 > 0\"))),\n    \"ComputeStats\" ->\n      (() => DeltaOperations.ComputeStats(Seq(EqualTo(Literal(\"col1\"), Literal(\"value1\"))))),\n    \"Restore\" -> (() => DeltaOperations.Restore(Some(5L), Some(\"2023-01-01T00:00:00Z\"))),\n    \"Optimize\" -> (() => DeltaOperations.Optimize(\n      predicate = Seq(EqualTo(Literal(\"col1\"), Literal(\"value1\"))),\n      zOrderBy = Seq(\"col1\", \"col2\"),\n      auto = true,\n      clusterBy = Some(Seq(\"col3\")),\n      isFull = false)),\n    \"Clone\" -> (() => DeltaOperations.Clone(\n      source = \"s3://bucket/path/to/table\",\n      sourceVersion = 10L)),\n    \"VacuumStart\" -> (() => DeltaOperations.VacuumStart(\n      retentionCheckEnabled = true,\n      specifiedRetentionMillis = Some(604800000L),\n      defaultRetentionMillis = 604800000L)),\n    \"VacuumEnd\" -> (() => DeltaOperations.VacuumEnd(\"COMPLETED\")),\n    \"Reorg\" -> (() => DeltaOperations.Reorg(\n      predicate = Seq(EqualTo(Literal(\"col1\"), Literal(\"value1\"))),\n      applyPurge = true)),\n    \"ClusterBy\" -> (() => DeltaOperations.ClusterBy(\n      oldClusteringColumns = JsonUtils.toJson(Seq(\"oldCol1\", \"oldCol2\")),\n      newClusteringColumns = JsonUtils.toJson(Seq(\"newCol1\", \"newCol2\")))),\n    \"RowTrackingBackfill\" -> (() => DeltaOperations.RowTrackingBackfill(batchId = 3)),\n    \"RowTrackingUnBackfill\" -> (() => DeltaOperations.RowTrackingUnBackfill(batchId = 4)),\n    \"UpgradeUniformProperties\" ->\n      (() => DeltaOperations.UpgradeUniformProperties(Map(\"uniform.property1\" -> \"value1\"))),\n    \"RemoveColumnMapping\" ->\n      (() => DeltaOperations.RemoveColumnMapping(Some(\"remove column mapping metadata\"))),\n    \"AddDeletionVectorsTombstones\" -> (() => DeltaOperations.AddDeletionVectorsTombstones),\n    \"ManualUpdate\" -> (() => DeltaOperations.ManualUpdate),\n    \"EmptyCommit\" -> (() => DeltaOperations.EmptyCommit)\n  )\n\n  trackedOperationClasses.foreach { case (operationName, operationGenerator) =>\n    test(s\"$operationName operation serialization\") {\n      testOperationSerialization(operationGenerator())\n    }\n  }\n\n  val ignoredOperationClasses = Set(\n    \"TestOperation\"\n  )\n\n  test(\"all operations should be tested in this suite\") {\n    val allOperations = DeltaTestUtils.getAllDeltaOperations\n    assert(\n      (allOperations -- ignoredOperationClasses) == trackedOperationClasses.keySet,\n      s\"if you add a new operation, please add a new test case in this suite \" +\n        \"for that operation and then add the operation name to the `trackedOperationClasses` \" +\n        \"Map in this test. Missing operations: \" +\n        s\"${allOperations -- ignoredOperationClasses -- trackedOperationClasses.keySet}\"\n    )\n  }\n\n}\n\n/**\n * A minimal CommitInfo with only operation parameters. This one\n * does not use the custom JsonMapDeserializer so we can\n * use it to test our ability to generate the legacy operation parameters.\n */\ncase class LegacyCommitInfoWithOperationParametersOnly(\n  operationParameters: Map[String, String]\n)\n\ncase class ActionWrapper(commitInfo: LegacyCommitInfoWithOperationParametersOnly = null)\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CommitSanityCheckSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.DeltaTestUtils.{filterUsageRecords, BOOLEAN_DOMAIN}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Tests for AddFile sanity checks during commit:\n * - Empty (0-byte) parquet file detection\n * - Null partition value validation for NOT NULL columns\n */\nclass CommitSanityCheckSuite extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils {\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaSQLConf.DELTA_EMPTY_FILE_CHECK_THROW_ENABLED.key, \"true\")\n      .set(DeltaSQLConf.DELTA_NULL_PARTITION_CHECK_THROW_ENABLED.key, \"true\")\n  }\n\n  private def createTable(tempDir: java.io.File): DeltaLog = {\n    sql(s\"CREATE TABLE delta.`${tempDir.getCanonicalPath}` \" +\n      s\"(id Long, value String) USING delta\")\n    DeltaLog.forTable(spark, tempDir.getCanonicalPath)\n  }\n\n  /**\n   * Creates a partitioned table with a NOT NULL partition column and returns the DeltaLog.\n   */\n  private def createPartitionedTableWithNotNullColumn(tempDir: java.io.File): DeltaLog = {\n    sql(s\"CREATE TABLE delta.`${tempDir.getCanonicalPath()}` \" +\n      s\"(part String NOT NULL, value Int) using delta PARTITIONED BY (part)\" )\n    DeltaLog.forTable(spark, tempDir.getCanonicalPath())\n  }\n\n  private def commit(\n    deltaLog: DeltaLog, addFile: AddFile, isCommitLarge: Boolean): Unit = {\n    val txn = deltaLog.startTransaction()\n    if (isCommitLarge) {\n      txn.commitLarge(\n        spark,\n        Seq(addFile).toIterator,\n        newProtocolOpt = None,\n        op = DeltaOperations.ManualUpdate,\n        context = Map.empty,\n        metrics = Map.empty\n      )\n    } else {\n      txn.commit(Seq(addFile), DeltaOperations.ManualUpdate)\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // Empty file checks\n  // ---------------------------------------------------------------------------\n\n  BOOLEAN_DOMAIN.foreach { isCommitLarge =>\n    test(s\"detect zero-byte AddFile [isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        val deltaLog = createTable(tempDir)\n\n        val addFile = AddFile(\n          path = \"part-00000.parquet\",\n          partitionValues = Map.empty,\n          size = 0,\n          modificationTime = System.currentTimeMillis(),\n          dataChange = true,\n          stats = \"\"\"{\"numRecords\": 0}\"\"\"\n        )\n\n        val e = intercept[IllegalStateException] {\n          commit(deltaLog, addFile, isCommitLarge)\n        }\n        assert(e.getMessage.contains(\"zero-byte\"))\n        assert(e.getMessage.contains(\"part-00000.parquet\"))\n      }\n    }\n\n    test(s\"no error when file size is positive [isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        val deltaLog = createTable(tempDir)\n\n        val addFile = AddFile(\n          path = \"part-00000.parquet\",\n          partitionValues = Map.empty,\n          size = 100,\n          modificationTime = System.currentTimeMillis(),\n          dataChange = true,\n          stats = \"\"\"{\"numRecords\": 1}\"\"\"\n        )\n\n        // Should not throw\n        commit(deltaLog, addFile, isCommitLarge)\n      }\n    }\n\n    test(s\"empty file check - only log when throw is disabled \" +\n        s\"[isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        withSQLConf(DeltaSQLConf.DELTA_EMPTY_FILE_CHECK_THROW_ENABLED.key -> \"false\") {\n          val deltaLog = createTable(tempDir)\n\n          val addFile = AddFile(\n            path = \"part-00000.parquet\",\n            partitionValues = Map.empty,\n            size = 0,\n            modificationTime = System.currentTimeMillis(),\n            dataChange = true,\n            stats = \"\"\"{\"numRecords\": 0}\"\"\"\n          )\n\n          // Should not throw when flag is disabled, only log\n          val events = Log4jUsageLogger.track {\n            commit(deltaLog, addFile, isCommitLarge)\n          }\n\n          val violationEvents =\n            filterUsageRecords(events, \"delta.sanityCheck.emptyParquetFile\")\n          assert(violationEvents.size == 1)\n\n          val eventBlob = JsonUtils.fromJson[Map[String, Any]](violationEvents.head.blob)\n          assert(eventBlob.contains(\"addFile\"))\n          assert(eventBlob.contains(\"stackTrace\"))\n        }\n      }\n    }\n\n    // ---------------------------------------------------------------------------\n    // Null partition checks\n    // ---------------------------------------------------------------------------\n\n    test(s\"detect null partition value for NOT NULL column with column mapping \" +\n        s\"[isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        sql(s\"CREATE TABLE delta.`${tempDir.getCanonicalPath()}` \" +\n          s\"(part String NOT NULL, value Int) USING delta PARTITIONED BY (part) \" +\n          s\"TBLPROPERTIES('delta.columnMapping.mode'='name')\")\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getCanonicalPath())\n        val physicalPartCol = deltaLog.snapshot.metadata.physicalPartitionColumns.head\n\n        val addFile = AddFile(\n          path = s\"$physicalPartCol=__HIVE_DEFAULT_PARTITION__/file.parquet\",\n          partitionValues = Map(physicalPartCol -> null),\n          size = 100,\n          modificationTime = System.currentTimeMillis(),\n          dataChange = true,\n          stats = \"\"\"{\"numRecords\": 1}\"\"\"\n        )\n\n        val e = intercept[IllegalStateException] {\n          commit(deltaLog, addFile, isCommitLarge)\n        }\n        assert(e.getMessage.contains(\"null partition value\"))\n        assert(e.getMessage.contains(s\"NOT NULL column '$physicalPartCol'\"))\n      }\n    }\n\n    test(s\"detect null partition value for NOT NULL column [isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        val deltaLog = createPartitionedTableWithNotNullColumn(tempDir)\n\n        // Create an AddFile with null partition value\n        val addFile = AddFile(\n          path = \"part=__HIVE_DEFAULT_PARTITION__/file.parquet\",\n          partitionValues = Map(\"part\" -> null),\n          size = 100,\n          modificationTime = System.currentTimeMillis(),\n          dataChange = true,\n          stats = \"\"\"{\"numRecords\": 1}\"\"\"\n        )\n\n        val e = intercept[IllegalStateException] {\n          commit(deltaLog, addFile, isCommitLarge)\n        }\n        assert(e.getMessage.contains(\"null partition value\"))\n        assert(e.getMessage.contains(\"NOT NULL column 'part'\"))\n      }\n    }\n\n    test(s\"no error when partition value is not null [isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        val deltaLog = createPartitionedTableWithNotNullColumn(tempDir)\n\n        // Create an AddFile with valid (non-null) partition value\n        val addFile = AddFile(\n          path = \"part=valid_value/file.parquet\",\n          partitionValues = Map(\"part\" -> \"valid_value\"),\n          size = 100,\n          modificationTime = System.currentTimeMillis(),\n          dataChange = true,\n          stats = \"\"\"{\"numRecords\": 1}\"\"\"\n        )\n\n        // Should not throw\n        commit(deltaLog, addFile, isCommitLarge)\n      }\n    }\n\n    test(s\"null partition check - only log when throw is disabled \" +\n        s\"[isCommitLarge: $isCommitLarge]\") {\n      withTempDir { tempDir =>\n        withSQLConf(DeltaSQLConf.DELTA_NULL_PARTITION_CHECK_THROW_ENABLED.key -> \"false\") {\n          val deltaLog = createPartitionedTableWithNotNullColumn(tempDir)\n\n          // Create an AddFile with null partition value\n          val addFile = AddFile(\n            path = \"part=__HIVE_DEFAULT_PARTITION__/file.parquet\",\n            partitionValues = Map(\"part\" -> null),\n            size = 100,\n            modificationTime = System.currentTimeMillis(),\n            dataChange = true,\n            stats = \"\"\"{\"numRecords\": 1}\"\"\"\n          )\n\n          // Should not throw when flag is disabled, only log\n          val events = Log4jUsageLogger.track {\n            commit(deltaLog, addFile, isCommitLarge)\n          }\n\n          // Validate that the null partition violation event was emitted\n          val violationEvents =\n            filterUsageRecords(events, \"delta.constraints.nullPartitionViolation\")\n          assert(violationEvents.size == 1)\n\n          val eventBlob = JsonUtils.fromJson[Map[String, Any]](violationEvents.head.blob)\n          assert(eventBlob.contains(\"addFile\"))\n          assert(eventBlob.contains(\"notNullPartitionCols\"))\n          assert(eventBlob(\"notNullPartitionCols\").toString == \"part\")\n          assert(eventBlob.contains(\"stackTrace\"))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ConflictCheckerPredicateEliminationUnitSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.util.DeltaSparkPlanUtils\n\nimport org.apache.spark.sql.{Column, QueryTest}\nimport org.apache.spark.sql.catalyst.dsl.expressions._\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal, Rand, ScalarSubquery}\nimport org.apache.spark.sql.functions.{col, udf}\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * A set cheaper unit tests, that behave the same no matter if DVs, CDF, etc. are enabled\n * and do not need to be repeated in each conflict checker suite.\n */\nclass ConflictCheckerPredicateEliminationUnitSuite\n  extends QueryTest\n  with SharedSparkSession\n  with ConflictCheckerPredicateElimination {\n\n  val simpleExpressionA: Expression = $\"a\" === 1\n  val simpleExpressionB: Expression = $\"b\" === \"test\"\n\n  val deterministicExpression: Expression = $\"c\" > 5L\n  val nonDeterministicExpression: Expression = $\"c\" > rand(0)\n  lazy val deterministicSubquery: Expression = {\n    val df = spark.sql(\"SELECT 5\")\n    df.collect()\n    $\"c\" > ScalarSubquery(df.queryExecution.analyzed)\n  }\n  lazy val nonDeterministicSubquery: Expression = {\n    val df = spark.sql(\"SELECT rand()\")\n    df.collect()\n    $\"c\" > ScalarSubquery(df.queryExecution.analyzed)\n  }\n\n  private def defaultEliminationFunction(e: Seq[Expression]): PredicateElimination = {\n    val options = DeltaSparkPlanUtils.CheckDeterministicOptions(allowDeterministicUdf = false)\n    eliminateNonDeterministicPredicates(e, options)\n  }\n\n  private def checkEliminationResult(\n      predicate: Expression,\n      expected: PredicateElimination,\n      eliminationFunction: Seq[Expression] => PredicateElimination = defaultEliminationFunction)\n  : Unit = {\n    require(expected.newPredicates.size === 1)\n    val actual = eliminationFunction(Seq(predicate))\n    assert(actual.newPredicates.size === 1)\n    assert(actual.newPredicates.head.canonicalized == expected.newPredicates.head.canonicalized,\n      s\"actual=$actual\\nexpected=$expected\")\n    assert(actual.eliminatedPredicates === expected.eliminatedPredicates)\n  }\n\n  for {\n    deterministic <- BOOLEAN_DOMAIN\n    subquery <- BOOLEAN_DOMAIN\n  } {\n    lazy val exprUnderTest = if (deterministic) {\n      if (subquery) deterministicSubquery else deterministicExpression\n    } else {\n      if (subquery) nonDeterministicSubquery else nonDeterministicExpression\n    }\n\n    val testSuffix = s\"deterministic $deterministic - subquery $subquery\"\n\n    def newPredicates(exprF: Expression => Expression): PredicateElimination = PredicateElimination(\n      newPredicates = Seq(exprF(if (deterministic) exprUnderTest else Literal.TrueLiteral)),\n      eliminatedPredicates = if (deterministic) Seq.empty else Seq(\"rand\"))\n\n    test(s\"and expression - $testSuffix\") {\n      checkEliminationResult(\n        predicate = simpleExpressionA && exprUnderTest,\n        expected = newPredicates { eliminatedExprUnderTest =>\n          if (deterministic) {\n            simpleExpressionA && eliminatedExprUnderTest\n          } else {\n            simpleExpressionA\n          }\n        }\n      )\n    }\n\n    test(s\"or expression - $testSuffix\") {\n      checkEliminationResult(\n        predicate = simpleExpressionA || exprUnderTest,\n        expected = newPredicates { _ =>\n          if (deterministic) {\n            simpleExpressionA || exprUnderTest\n          } else {\n            Literal.TrueLiteral\n          }\n        }\n      )\n    }\n\n    test(s\"and or expression - $testSuffix\") {\n      checkEliminationResult(\n        predicate = simpleExpressionA && (simpleExpressionB || exprUnderTest),\n        expected = newPredicates { _ =>\n          if (deterministic) {\n            simpleExpressionA && (simpleExpressionB || exprUnderTest)\n          } else {\n            simpleExpressionA\n          }\n        }\n      )\n    }\n\n    test(s\"or and expression - $testSuffix\") {\n      checkEliminationResult(\n        predicate = simpleExpressionA || (simpleExpressionB && exprUnderTest),\n        expected = newPredicates { _ =>\n          if (deterministic) {\n            simpleExpressionA || (simpleExpressionB && exprUnderTest)\n          } else {\n            simpleExpressionA || simpleExpressionB\n          }\n        }\n      )\n    }\n\n    test(s\"or not and expression - $testSuffix\") {\n      checkEliminationResult(\n        predicate = simpleExpressionA || !(simpleExpressionB && exprUnderTest),\n        expected = newPredicates { _ =>\n          if (deterministic) {\n            simpleExpressionA || !(simpleExpressionB && exprUnderTest)\n          } else {\n            Literal.TrueLiteral\n          }\n        }\n      )\n    }\n\n    test(s\"and not or expression - $testSuffix\") {\n      checkEliminationResult(\n        predicate = simpleExpressionA && !(simpleExpressionB || exprUnderTest),\n        expected = newPredicates { _ =>\n          if (deterministic) {\n            simpleExpressionA && !(simpleExpressionB || exprUnderTest)\n          } else {\n            simpleExpressionA\n          }\n        })\n    }\n  }\n\n  test(\"udf name is not exposed\") {\n    import testImplicits._\n    val random = udf(() => Math.random())\n      .asNondeterministic()\n      .withName(\"sensitive_udf_name\")\n    checkEliminationResult(\n      predicate = simpleExpressionA && (col(\"c\") > random()).expr,\n      expected = PredicateElimination(\n        newPredicates = Seq(simpleExpressionA),\n        eliminatedPredicates = Seq(\"scalaudf\")))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ConflictResolutionTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.{ExecutionException, ThreadPoolExecutor}\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.concurrency.{PhaseLockingTestMixin, TransactionExecutionTestMixin}\nimport org.apache.spark.sql.delta.fuzzer.{PhaseLockingTransactionExecutionObserver => TransactionObserver}\nimport org.apache.spark.sql.delta.rowid.RowIdTestUtils\nimport org.apache.spark.sql.util.ScalaExtensions.OptionExt\nimport io.delta.tables.{DeltaTable => IODeltaTable}\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.{ThreadUtils, Utils}\n\ntrait ConflictResolutionTestUtils\n    extends QueryTest\n    with SharedSparkSession\n    with PhaseLockingTestMixin\n    with TransactionExecutionTestMixin\n    with DeletionVectorsTestUtils\n    with RowIdTestUtils {\n\n  import testImplicits._\n\n  final val ID_COLUMN = \"idCol\"\n  final val PARTITION_COLUMN = \"partitionCol\"\n\n  override val timeout: FiniteDuration = 120.seconds\n\n  def abbreviate(str: String, abbrevMarker: String, len: Int): String = {\n    if (str == null || abbrevMarker == null) {\n      null\n    } else if (str.length() <= len || str.length() <= abbrevMarker.length()) {\n      str\n    } else {\n      str.substring(0, len - abbrevMarker.length()) + abbrevMarker\n    }\n  }\n\n  abstract class TestTransaction(sqlConf: Map[String, String] = Map.empty) {\n    val name: String\n    val sqlConfStr: String = sqlConf.map { case (k, v) => s\"$k=$v\" }.mkString(\",\")\n\n    def toSQL(tableName: String): String\n\n    def execute(ctx: TestContext): Unit = {\n      ctx.trackTransaction(this) {\n        withSQLConf(sqlConf.toSeq: _*) {\n          executeImpl(ctx)\n        }\n      }\n    }\n\n    def executeImpl(ctx: TestContext): Unit = {\n      val sqlStr = toSQL(s\"delta.`${ctx.deltaLog.dataPath.toUri.getPath}`\")\n      spark.sql(sqlStr).collect()\n    }\n\n    /** Whether this transaction is committing data change actions. */\n    def dataChange: Boolean\n\n    /** Whether writing Deletion Vectors is enabled for this transaction. */\n    def deletionVectorsEnabled(deltaLog: DeltaLog): Boolean = false\n\n    /** The executor thread to run this transaction. */\n    private lazy val executor: ThreadPoolExecutor =\n      ThreadUtils.newDaemonSingleThreadExecutor(threadName = s\"executor-$name\")\n\n    /** The transaction observer to step through the transaction phases. */\n    var observer: Option[TransactionObserver] = None\n\n    /** The asynchronous future for the result of the transaction. */\n    private var future: Option[Future[Array[Row]]] = None\n\n    /** Start transaction and unblock until precommit. */\n    def start(ctx: TestContext): Unit = {\n      withSQLConf(sqlConf.toSeq: _*) {\n        val (observer_, future_) = runFunctionWithObserver(name, executor,\n          fn = () => {\n            executeImpl(ctx)\n            // DV tests do not use the results. We just return an empty array to conform with\n            // function's signature.\n            Array.empty[Row]\n          })\n        unblockUntilPreCommit(observer_)\n        busyWaitFor(observer_.phases.preparePhase.hasEntered, timeout)\n\n        observer = Some(observer_)\n        future = Some(future_)\n      }\n    }\n\n    /** Commit the transaction. */\n    def commit(ctx: TestContext): Unit = {\n      assert(observer.isDefined, \"transaction not started\")\n      assert(future.isDefined, \"transaction not started\")\n      val preCommitVersion = ctx.deltaLog.update().version\n      withSQLConf(sqlConf.toSeq: _*) {\n        ctx.trackTransaction(this) {\n          unblockCommit(observer.get)\n          waitForCommit(observer.get)\n          ThreadUtils.awaitResult(future.get, Duration.Inf)\n        }\n      }\n\n      // Ensure that the transaction actually commits something.\n      val postCommitVersion = ctx.deltaLog.update().version\n      assert(postCommitVersion > preCommitVersion, s\"Transaction $this did not commit\")\n    }\n\n    /** Run transaction and interleave fn() while transaction is stopped in precommit. */\n    def interleave[T](ctx: TestContext)(fn: => Unit): Unit = {\n      start(ctx)\n      fn\n      commit(ctx)\n    }\n  }\n\n  /**\n   * Helper class containing the Delta log and committed transactions of a test.\n   */\n  class TestContext(val deltaLog: DeltaLog) {\n    /** The version of the Delta table after writing the initial data. */\n    val initialVersion: Long = deltaLog.update().version\n\n    private val committedTransactions: ArrayBuffer[TestTransaction] = ArrayBuffer.empty\n\n    /** Returns the transactions that successfully committed. */\n    def getCommittedTransactions: Seq[TestTransaction] = committedTransactions.toSeq\n\n    /** Execute fn() and record the transaction if it successfully created a commit. */\n    def trackTransaction(transaction: TestTransaction)(fn: => Unit): Unit = {\n      val preCommitVersion = deltaLog.update().version\n      fn\n      if (deltaLog.update().version > preCommitVersion) {\n        committedTransactions.append(transaction)\n      }\n    }\n\n    def deltaTable: IODeltaTable = IODeltaTable.forPath(deltaLog.dataPath.toString)\n  }\n\n  case class Insert(\n      rows: Seq[Long],\n      partitionColumn: Option[Long] = Some(0L),\n      sqlConf: Map[String, String] = Map.empty) extends TestTransaction(sqlConf) {\n    override val name: String = {\n      val rowsStr = abbreviate(rows.mkString(\",\"), \"...\", 10)\n      s\"INSERT($rowsStr)($sqlConfStr)\"\n    }\n\n    override def toSQL(tableName: String): String = {\n      throw new UnsupportedOperationException(\"toSQL for Insert is not implemented yet\")\n    }\n\n    override def executeImpl(ctx: TestContext): Unit = {\n      var df = rows.toDF(ID_COLUMN)\n      partitionColumn.ifDefined { p =>\n        df = df.withColumn(PARTITION_COLUMN, lit(p))\n      }\n      df.write.format(\"delta\").mode(\"append\").save(ctx.deltaLog.dataPath.toString)\n    }\n\n    override def dataChange: Boolean = true\n  }\n\n  case class Delete(\n      rows: Seq[Long],\n      sqlConf: Map[String, String] = Map.empty) extends TestTransaction(sqlConf) {\n    override val name: String = {\n      val rowsStr = abbreviate(rows.mkString(\",\"), \"...\", 10)\n      s\"DELETE($rowsStr)($sqlConfStr)\"\n    }\n\n    override def toSQL(tableName: String): String = {\n      val inRowsStr = rows.mkString(\"(\", \", \", \")\")\n      s\"DELETE FROM $tableName WHERE $ID_COLUMN IN $inRowsStr\"\n    }\n\n    override def dataChange: Boolean = true\n\n    override def deletionVectorsEnabled(deltaLog: DeltaLog): Boolean = {\n      var result = false\n      withSQLConf(sqlConf.toSeq: _*) {\n        result = deletionVectorsEnabledInDelete(spark, deltaLog)\n      }\n      result\n    }\n  }\n\n  case class Update(\n      rows: Seq[Long],\n      setValue: Long = 42,\n      sqlConf: Map[String, String] = Map.empty) extends TestTransaction(sqlConf) {\n    override val name: String = {\n      val rowsStr = abbreviate(rows.mkString(\",\"), \"...\", 10)\n      s\"UPDATE($rowsStr)($sqlConfStr)\"\n    }\n\n    override def toSQL(tableName: String): String = {\n      val inRowsStr = rows.mkString(\"(\", \", \", \")\")\n      // Dummy update.\n      s\"UPDATE $tableName SET $ID_COLUMN=$setValue WHERE $ID_COLUMN IN $inRowsStr\"\n    }\n\n    override def dataChange: Boolean = true\n\n    override def deletionVectorsEnabled(deltaLog: DeltaLog): Boolean = {\n      var result = false\n      withSQLConf(sqlConf.toSeq: _*) {\n        result = deletionVectorsEnabledInUpdate(spark, deltaLog)\n      }\n      result\n    }\n  }\n\n  // Delete-only MERGE.\n  case class Merge(\n      deleteRows: Seq[Long],\n      sqlConf: Map[String, String] = Map.empty) extends TestTransaction(sqlConf) {\n    override val name: String = {\n      val rowsStr = abbreviate(deleteRows.mkString(\",\"), \"...\", 10)\n      s\"MERGE($rowsStr)($sqlConfStr)\"\n    }\n\n    override def toSQL(tableName: String): String = {\n      val inRowsStr = deleteRows.mkString(\"(\", \", \", \")\")\n      s\"\"\"\n         |MERGE INTO $tableName t\n         |USING $tableName s\n         |ON t.$ID_COLUMN = s.$ID_COLUMN AND t.$ID_COLUMN IN $inRowsStr\n         |WHEN MATCHED THEN DELETE\n         |\"\"\".stripMargin\n    }\n\n    override def dataChange: Boolean = true\n\n    override def deletionVectorsEnabled(deltaLog: DeltaLog): Boolean = {\n      var result = false\n      withSQLConf(sqlConf.toSeq: _*) {\n        result = deletionVectorsEnabledInMerge(spark, deltaLog)\n      }\n      result\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ConvertToDeltaSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.execution.command.ExecutedCommandExec\nimport org.apache.spark.sql.functions.{col, from_json}\n\ntrait ConvertToDeltaSQLSuiteBase extends ConvertToDeltaSuiteBaseCommons\n  with DeltaSQLCommandTest {\n  override protected def convertToDelta(\n      identifier: String,\n      partitionSchema: Option[String] = None, collectStats: Boolean = true): Unit = {\n    if (partitionSchema.isEmpty) {\n      sql(s\"convert to delta $identifier ${collectStatisticsStringOption(collectStats)}\")\n    } else {\n      val stringSchema = partitionSchema.get\n      sql(s\"convert to delta $identifier ${collectStatisticsStringOption(collectStats)}\" +\n        s\" partitioned by ($stringSchema)\")\n    }\n  }\n\n  // TODO: Move to ConvertToDeltaSuiteBaseCommons when DeltaTable API contains collectStats option\n  test(\"convert with collectStats set to false\") {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n\n        val tempDir = dir.getCanonicalPath\n        writeFiles(tempDir, simpleDF)\n        convertToDelta(s\"parquet.`$tempDir`\", collectStats = false)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val history = io.delta.tables.DeltaTable.forPath(tempDir).history()\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir),\n          simpleDF\n        )\n        assert(history.count == 1)\n        val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select(from_json(col(\"stats\"), deltaLog.unsafeVolatileSnapshot.statsSchema)\n            .as(\"stats\")).select(\"stats.*\")\n        assert(statsDf.filter(col(\"numRecords\").isNotNull).count == 0)\n      }\n    }\n  }\n\n  for (numFiles <- Seq(1, 7)) {\n    test(s\"numConvertedFiles metric ($numFiles files)\") {\n      val testTableName = \"test_table\"\n      withTable(testTableName) {\n        spark.range(end = numFiles).toDF(\"part\").withColumn(\"data\", col(\"part\"))\n          .write.partitionBy(\"part\").mode(\"overwrite\").format(\"parquet\").saveAsTable(testTableName)\n\n        val plans = DeltaTestUtils.withPhysicalPlansCaptured(spark) {\n          convertToDelta(testTableName, Some(\"part long\"))\n        }\n\n        // Validate that the command node has the correct metrics.\n        val commandNode = plans.collect { case exe: ExecutedCommandExec => exe.cmd }.head\n        assert(commandNode.metrics(\"numConvertedFiles\").value === numFiles)\n      }\n    }\n  }\n}\n\nclass ConvertToDeltaSQLSuite extends ConvertToDeltaSQLSuiteBase\n  with ConvertToDeltaSuiteBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ConvertToDeltaScalaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.types.StructType\n\nclass ConvertToDeltaScalaSuite extends ConvertToDeltaSuiteBase {\n  override protected def convertToDelta(\n      identifier: String,\n      partitionSchema: Option[String] = None, collectStats: Boolean = true): Unit = {\n    if (partitionSchema.isDefined) {\n      io.delta.tables.DeltaTable.convertToDelta(\n        spark,\n        identifier,\n        StructType.fromDDL(partitionSchema.get)\n      )\n    } else {\n      io.delta.tables.DeltaTable.convertToDelta(\n        spark,\n        identifier\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ConvertToDeltaSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, FileNotFoundException}\n\nimport org.apache.spark.sql.delta.files.TahoeLogFileIndex\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaExceptionTestUtils, DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.streaming.Trigger\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\n/**\n * Common functions used across CONVERT TO DELTA test suites. We separate out these functions\n * so that we can re-use them in tests using Hive support. Tests that leverage Hive support cannot\n * extend the `SharedSparkSession`, therefore we keep this utility class as bare-bones as possible.\n */\ntrait ConvertToDeltaTestUtils extends QueryTest\n    with DeltaExceptionTestUtils { self: DeltaSQLTestUtils =>\n\n  protected def collectStatisticsStringOption(collectStats: Boolean): String = Option(collectStats)\n    .filterNot(identity).map(_ => \"NO STATISTICS\").getOrElse(\"\")\n\n  protected def simpleDF = spark.range(100)\n    .withColumn(\"key1\", col(\"id\") % 2)\n    .withColumn(\"key2\", col(\"id\") % 3 cast \"String\")\n\n  protected def convertToDelta(identifier: String, partitionSchema: Option[String] = None,\n      collectStats: Boolean = true): Unit\n\n  protected val blockNonDeltaMsg = \"A transaction log for Delta was found at\"\n  protected val parquetOnlyMsg = \"CONVERT TO DELTA only supports parquet tables\"\n  protected val invalidParquetMsg = \" not a Parquet file. Expected magic number at tail\"\n  // scalastyle:off deltahadoopconfiguration\n  protected def sessionHadoopConf = spark.sessionState.newHadoopConf\n  // scalastyle:on deltahadoopconfiguration\n\n  protected def deltaRead(df: => DataFrame): Boolean = {\n    val analyzed = df.queryExecution.analyzed\n    analyzed.find {\n      case DeltaTable(_: TahoeLogFileIndex) => true\n      case _ => false\n    }.isDefined\n  }\n\n  protected def writeFiles(\n      dir: String,\n      df: DataFrame,\n      format: String = \"parquet\",\n      partCols: Seq[String] = Nil,\n      mode: String = \"overwrite\"): Unit = {\n    if (partCols.nonEmpty) {\n      df.write.partitionBy(partCols: _*).format(format).mode(mode).save(dir)\n    } else {\n      df.write.format(format).mode(mode).save(dir)\n    }\n  }\n}\n\ntrait ConvertToDeltaSuiteBaseCommons extends ConvertToDeltaTestUtils\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with DeltaTestUtilsForTempViews\n\n/** Tests for CONVERT TO DELTA that can be leveraged across SQL and Scala APIs. */\ntrait ConvertToDeltaSuiteBase extends ConvertToDeltaSuiteBaseCommons\n  with ConvertToDeltaHiveTableTests {\n\n  import org.apache.spark.sql.functions._\n  import testImplicits._\n\n  // Use different batch sizes to cover different merge schema code paths.\n  protected def testSchemaMerging(testName: String)(block: => Unit): Unit = {\n    Seq(\"1\", \"5\").foreach { batchSize =>\n      test(s\"$testName - batch size: $batchSize\") {\n        withSQLConf(\n          DeltaSQLConf.DELTA_IMPORT_BATCH_SIZE_SCHEMA_INFERENCE.key -> batchSize) {\n          block\n        }\n      }\n    }\n  }\n\n  test(\"convert with collectStats true\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF)\n      convertToDelta(s\"parquet.`$tempDir`\", collectStats = true)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      val history = io.delta.tables.DeltaTable.forPath(tempDir).history()\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir),\n        simpleDF\n      )\n      assert(history.count == 1)\n      val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select(from_json($\"stats\", deltaLog.unsafeVolatileSnapshot.statsSchema)\n          .as(\"stats\")).select(\"stats.*\")\n      assert(statsDf.filter($\"numRecords\".isNull).count == 0)\n      assert(statsDf.agg(sum(\"numRecords\")).as[Long].head() == simpleDF.count)\n    }\n  }\n\n  test(\"convert with collectStats true but config set to false -> Do not collect stats\") {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val tempDir = dir.getCanonicalPath\n        writeFiles(tempDir, simpleDF)\n        convertToDelta(s\"parquet.`$tempDir`\", collectStats = true)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val history = io.delta.tables.DeltaTable.forPath(tempDir).history()\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir),\n          simpleDF\n        )\n        assert(history.count == 1)\n        val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select(from_json($\"stats\", deltaLog.unsafeVolatileSnapshot.statsSchema)\n            .as(\"stats\")).select(\"stats.*\")\n        assert(statsDf.filter($\"numRecords\".isNotNull).count == 0)\n      }\n    }\n  }\n\n  test(\"negative case: convert a non-delta path falsely claimed as parquet\") {\n    Seq(\"orc\", \"json\", \"csv\").foreach { format =>\n      withTempDir { dir =>\n        val tempDir = dir.getCanonicalPath\n        writeFiles(tempDir, simpleDF, format)\n        // exception from executor reading parquet footer\n        intercept[SparkException] {\n          convertToDelta(s\"parquet.`$tempDir`\")\n        }\n      }\n    }\n  }\n\n  test(\"negative case: convert non-parquet path to delta\") {\n    Seq(\"orc\", \"json\", \"csv\").foreach { format =>\n      withTempDir { dir =>\n        val tempDir = dir.getCanonicalPath\n        writeFiles(tempDir, simpleDF, format)\n        val ae = intercept[AnalysisException] {\n          convertToDelta(s\"$format.`$tempDir`\")\n        }\n        assert(ae.getMessage.contains(parquetOnlyMsg))\n      }\n    }\n  }\n\n  test(\"negative case: convert non-parquet file to delta\") {\n    Seq(\"orc\", \"json\", \"csv\").foreach { format =>\n      withTempDir { dir =>\n        val tempDir = dir.getCanonicalPath\n        writeFiles(tempDir, simpleDF, format)\n\n        val se = intercept[SparkException] {\n          convertToDelta(s\"parquet.`$tempDir`\")\n        }\n        assert(se.getMessage.contains(invalidParquetMsg))\n      }\n    }\n  }\n\n  test(\"filter non-parquet file for schema inference when not using catalog schema\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"corrupted_id\"), format = \"orc\")\n      writeFiles(tempDir + \"/part=2/\", Seq(2).toDF(\"id\"))\n\n      val tableName = \"pqtable\"\n      withTable(tableName) {\n        // Create a catalog table on top of the parquet table with the wrong schema\n        // The schema should be picked from the parquet data files\n        sql(s\"CREATE TABLE $tableName (key1 long, key2 string) \" +\n          s\"USING PARQUET PARTITIONED BY (part string) LOCATION '$dir'\")\n        // Required for discovering partition of the table\n        sql(s\"MSCK REPAIR TABLE $tableName\")\n\n        withSQLConf(\n          \"spark.sql.files.ignoreCorruptFiles\" -> \"false\",\n          DeltaSQLConf.DELTA_CONVERT_USE_CATALOG_SCHEMA.key -> \"false\") {\n          val se = intercept[SparkException] {\n            convertToDelta(tableName)\n          }\n          assert(se.getMessage.contains(invalidParquetMsg))\n        }\n\n        withSQLConf(\n          \"spark.sql.files.ignoreCorruptFiles\" -> \"true\",\n          DeltaSQLConf.DELTA_CONVERT_USE_CATALOG_SCHEMA.key -> \"false\") {\n\n          convertToDelta(tableName)\n\n          val tableId = TableIdentifier(tableName, Some(\"default\"))\n          val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, tableId)\n          val expectedSchema = StructType(\n            StructField(\"id\", IntegerType, true) :: StructField(\"part\", StringType, true) :: Nil)\n          // Schema is inferred from the data\n          assert(snapshot.schema.equals(expectedSchema))\n        }\n      }\n    }\n  }\n\n  test(\"filter non-parquet files during delta conversion\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"id\"), format = \"json\")\n      writeFiles(tempDir + \"/part=2/\", Seq(2).toDF(\"id\"))\n      withSQLConf(\"spark.sql.files.ignoreCorruptFiles\" -> \"true\") {\n        convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n        checkAnswer(spark.read.format(\"delta\").load(tempDir), Row(2, \"2\") :: Nil)\n      }\n    }\n  }\n\n  testQuietlyWithTempView(\"negative case: convert temp views to delta\") { isSQLTempView =>\n    val tableName = \"pqtbl\"\n    withTable(tableName) {\n      // Create view\n      simpleDF.write.format(\"parquet\").saveAsTable(tableName)\n      createTempViewFromTable(tableName, isSQLTempView, format = Some(\"parquet\"))\n\n      // Attempt to convert to delta\n      val ae = intercept[AnalysisException] {\n        convertToDelta(\"v\")\n      }\n\n      assert(ae.getMessage.contains(\"Converting a view to a Delta table\") ||\n        ae.getMessage.contains(\"Table default.v not found\") ||\n        ae.getMessage.contains(\"Table or view 'v' not found in database 'default'\") ||\n        ae.getMessage.contains(\"table or view `default`.`v` cannot be found\") ||\n        ae.getMessage.contains(\"The table or view `spark_catalog`.`default`.`v` cannot be found\"))\n    }\n  }\n\n  test(\"negative case: missing data source name\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF, \"parquet\", Seq(\"key1\", \"key2\"))\n      val ae = intercept[AnalysisException] {\n        convertToDelta(s\"`$tempDir`\", None)\n      }\n      assert(ae.getMessage.contains(parquetOnlyMsg))\n    }\n  }\n\n  test(\"negative case: # partitions unmatched\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      writeFiles(path, simpleDF, partCols = Seq(\"key1\", \"key2\"))\n\n      val ae = intercept[AnalysisException] {\n        convertToDelta(s\"parquet.`$path`\", Some(\"key1 long\"))\n      }\n      assert(ae.getMessage.contains(\"Expecting 1 partition column(s)\"))\n    }\n  }\n\n  test(\"negative case: unmatched partition column names\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      writeFiles(path, simpleDF, partCols = Seq(\"key1\", \"key2\"))\n\n      val ae = intercept[AnalysisException] {\n        convertToDelta(s\"parquet.`$path`\", Some(\"key1 long, key22 string\"))\n      }\n      assert(ae.getMessage.contains(\"Expecting partition column \"))\n    }\n  }\n\n  test(\"negative case: failed to cast partition value\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val df = simpleDF.withColumn(\"partKey\", lit(\"randomstring\"))\n      writeFiles(path, df, partCols = Seq(\"partKey\"))\n      val ae = intercept[RuntimeException] {\n        convertToDelta(s\"parquet.`$path`\", Some(\"partKey int\"))\n      }\n      assert(ae.getMessage.contains(\"Failed to cast partition value\"))\n    }\n  }\n\n  test(\"negative case: inconsistent directory structure\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF)\n      writeFiles(tempDir + \"/key1=1/\", simpleDF)\n\n      var ae = intercept[AnalysisException] {\n        convertToDelta(s\"parquet.`$tempDir`\")\n      }\n      assert(ae.getMessage.contains(\"Expecting 0 partition column\"))\n\n      ae = intercept[AnalysisException] {\n        convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 string\"))\n      }\n      assert(ae.getMessage.contains(\"Expecting 1 partition column\"))\n    }\n  }\n\n  test(\"negative case: empty and non-existent root dir\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      val re = intercept[FileNotFoundException] {\n        convertToDelta(s\"parquet.`$tempDir`\")\n      }\n      assert(re.getMessage.contains(\"No file found in the directory\"))\n      Utils.deleteRecursively(dir)\n\n      val ae = intercept[FileNotFoundException] {\n        convertToDelta(s\"parquet.`$tempDir`\")\n      }\n      assert(ae.getMessage.contains(\"doesn't exist\"))\n    }\n  }\n\n  testSchemaMerging(\"negative case: merge type conflict - string vs int\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"id\"))\n      for (i <- 2 to 8 by 2) {\n        writeFiles(tempDir + s\"/part=$i/\", Seq(1).toDF(\"id\"))\n      }\n      for (i <- 3 to 9 by 2) {\n        writeFiles(tempDir + s\"/part=$i/\", Seq(\"1\").toDF(\"id\"))\n      }\n\n      val ex = interceptWithUnwrapping[SparkException] {\n        convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n      }\n      assert(ex.getMessage.contains(\"Failed to merge\"))\n      assert(ex.getMessage.contains(\"/part=\"), \"Error message should contain the file name\")\n    }\n  }\n\n  test(\"convert a streaming parquet path: use metadata\") {\n    val stream = MemoryStream[Int]\n    val df = stream.toDS().toDF()\n\n    withTempDir { outputDir =>\n      val checkpoint = new File(outputDir, \"_check\").toString\n      val dataLocation = new File(outputDir, \"data\").toString\n      val options = Map(\"checkpointLocation\" -> checkpoint)\n\n      // Add initial data to parquet file sink\n      stream.addData(1, 2, 3)\n      df.writeStream\n        .options(options)\n        .format(\"parquet\")\n        .trigger(Trigger.AvailableNow())\n        .start(dataLocation)\n        .awaitTermination()\n\n      // Add non-streaming data: this should be ignored in conversion.\n      spark.range(10, 20).write.mode(\"append\").parquet(dataLocation)\n      sql(s\"CONVERT TO DELTA parquet.`$dataLocation`\")\n\n      // Write data to delta\n      stream.addData(4, 5, 6)\n      df.writeStream\n        .options(options)\n        .format(\"delta\")\n        .trigger(Trigger.AvailableNow())\n        .start(dataLocation)\n        .awaitTermination()\n\n      // Should only read streaming data.\n      checkAnswer(\n        spark.read.format(\"delta\").load(dataLocation),\n        (1 to 6).map { Row(_) }\n      )\n    }\n  }\n\n  test(\"convert a streaming parquet path: ignore metadata\") {\n    val stream = MemoryStream[Int]\n    val df = stream.toDS().toDF(\"col1\")\n\n    withTempDir { outputDir =>\n      val checkpoint = new File(outputDir, \"_check\").toString\n      val dataLocation = new File(outputDir, \"data\").toString\n      val options = Map(\n        \"checkpointLocation\" -> checkpoint\n      )\n\n      // Add initial data to parquet file sink\n      stream.addData(1 to 5)\n      df.writeStream\n        .options(options)\n        .format(\"parquet\")\n        .trigger(Trigger.AvailableNow())\n        .start(dataLocation)\n        .awaitTermination()\n\n      // Add non-streaming data: this should not be ignored in conversion.\n      spark.range(11, 21).select('id.cast(\"int\") as \"col1\")\n        .write.mode(\"append\").parquet(dataLocation)\n\n      withSQLConf((\"spark.databricks.delta.convert.useMetadataLog\", \"false\")) {\n        sql(s\"CONVERT TO DELTA parquet.`$dataLocation`\")\n      }\n\n      // Write data to delta\n      stream.addData(6 to 10)\n      df.writeStream\n        .options(options)\n        .format(\"delta\")\n        .trigger(Trigger.AvailableNow())\n        .start(dataLocation)\n        .awaitTermination()\n\n      // Should read all data not just streaming data\n      checkAnswer(\n        spark.read.format(\"delta\").load(dataLocation),\n        (1 to 20).map { Row(_) }\n      )\n    }\n  }\n\n  test(\"convert a parquet path\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF, partCols = Seq(\"key1\", \"key2\"))\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 long, key2 string\"))\n\n\n      // reads actually went through Delta\n      assert(deltaRead(spark.read.format(\"delta\").load(tempDir).select(\"id\")))\n\n      // query through Delta is correct\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 0\").select(\"id\"),\n        simpleDF.filter(\"id % 2 == 0\").select(\"id\"))\n\n\n      // delta writers went through\n      writeFiles(\n        tempDir, simpleDF, format = \"delta\", partCols = Seq(\"key1\", \"key2\"), mode = \"append\")\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 1\").select(\"id\"),\n        simpleDF.union(simpleDF).filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  private def testSpecialCharactersInDirectoryNames(c: String, expectFailure: Boolean): Unit = {\n    test(s\"partition column names and values contain '$c'\") {\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n\n        val key1 = s\"${c}key1${c}${c}\"\n        val key2 = s\"${c}key2${c}${c}\"\n\n        val valueA = s\"${c}some${c}${c}value${c}A\"\n        val valueB = s\"${c}some${c}${c}value${c}B\"\n        val valueC = s\"${c}some${c}${c}value${c}C\"\n        val valueD = s\"${c}some${c}${c}value${c}D\"\n\n        val df1 = spark.range(3)\n          .withColumn(key1, lit(valueA))\n          .withColumn(key2, lit(valueB))\n        val df2 = spark.range(4, 7)\n          .withColumn(key1, lit(valueC))\n          .withColumn(key2, lit(valueD))\n        val df = df1.union(df2)\n        writeFiles(path, df, format = \"parquet\", partCols = Seq(key1, key2))\n\n        if (expectFailure) {\n          val e = intercept[AnalysisException] {\n            convertToDelta(s\"parquet.`$path`\", Some(s\"`$key1` string, `$key2` string\"))\n          }\n          assert(e.getMessage.contains(\"invalid character\"))\n        } else {\n          convertToDelta(s\"parquet.`$path`\", Some(s\"`$key1` string, `$key2` string\"))\n\n          // missing one char from valueA, so no match\n          checkAnswer(\n            spark.read.format(\"delta\").load(path).where(s\"`$key1` = '${c}some${c}value${c}A'\")\n              .select(\"id\"), Nil)\n\n          checkAnswer(\n            spark.read.format(\"delta\").load(path)\n              .where(s\"`$key1` = '$valueA' and `$key2` = '$valueB'\").select(\"id\"),\n            Row(0) :: Row(1) :: Row(2) :: Nil)\n\n          checkAnswer(\n            spark.read.format(\"delta\").load(path).where(s\"`$key2` = '$valueD' and id > 4\")\n              .select(\"id\"),\n            Row(5) :: Row(6) :: Nil)\n        }\n      }\n    }\n  }\n\n  \" ,;{}()\\n\\t=\".foreach { char =>\n    testSpecialCharactersInDirectoryNames(char.toString, expectFailure = true)\n  }\n  testSpecialCharactersInDirectoryNames(\"%!@#$%^&*-\", expectFailure = false)\n  testSpecialCharactersInDirectoryNames(\"?.+<_>|/\", expectFailure = false)\n\n  test(\"can ignore empty sub-directories\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      val fs = new Path(tempDir).getFileSystem(sessionHadoopConf)\n\n      writeFiles(tempDir + \"/key1=1/\", Seq(1).toDF)\n      assert(fs.mkdirs(new Path(tempDir + \"/key1=2/\")))\n      assert(fs.mkdirs(new Path(tempDir + \"/random_dir/\")))\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 string\"))\n      checkAnswer(spark.read.format(\"delta\").load(tempDir), Row(1, \"1\"))\n    }\n  }\n\n  test(\"allow file names to have = character\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"id\"))\n\n      val fs = new Path(tempDir).getFileSystem(sessionHadoopConf)\n      // Rename the parquet file in partition \"part=1\" with something containing \"=\"\n      val files = fs.listStatus(new Path(tempDir + \"/part=1/\"))\n        .map(_.getPath)\n        .filter(path => !path.getName.startsWith(\"_\") && !path.getName.startsWith(\".\"))\n\n      assert(files.length == 1)\n      fs.rename(\n        files.head, new Path(files.head.getParent.getName, \"some-data-id=1.snappy.parquet\"))\n\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n      checkAnswer(spark.read.format(\"delta\").load(tempDir), Row(1, \"1\"))\n    }\n  }\n\n  test(\"allow file names to not have .parquet suffix\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"id\"))\n      writeFiles(tempDir + \"/part=2/\", Seq(2).toDF(\"id\"))\n\n      val fs = new Path(tempDir).getFileSystem(sessionHadoopConf)\n      // Remove the suffix of the parquet file in partition \"part=1\"\n      val files = fs.listStatus(new Path(tempDir + \"/part=1/\"))\n        .map(_.getPath)\n        .filter(path => !path.getName.startsWith(\"_\") && !path.getName.startsWith(\".\"))\n\n      assert(files.length == 1)\n      fs.rename(files.head, new Path(files.head.getParent.toString, \"unknown_suffix\"))\n\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n      checkAnswer(spark.read.format(\"delta\").load(tempDir), Row(1, \"1\") :: Row(2, \"2\") :: Nil)\n    }\n  }\n\n  test(\"backticks\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF)\n\n      // wrap parquet with backticks should work\n      convertToDelta(s\"`parquet`.`$tempDir`\", None)\n      checkAnswer(spark.read.format(\"delta\").load(tempDir), simpleDF)\n\n      // path with no backticks should fail parsing\n      intercept[ParseException] {\n        convertToDelta(s\"parquet.$tempDir\")\n      }\n    }\n  }\n\n  test(\"overlapping partition and data columns\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      val df = spark.range(1)\n        .withColumn(\"partKey1\", lit(\"1\"))\n        .withColumn(\"partKey2\", lit(\"2\"))\n      df.write.parquet(tempDir + \"/partKey1=1\")\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"partKey1 int\"))\n\n      // Same as in [[HadoopFsRelation]], for common columns,\n      // respecting the order of data schema but the type of partition schema\n      checkAnswer(spark.read.format(\"delta\").load(tempDir), Row(0, 1, \"2\"))\n    }\n  }\n\n  test(\"some partition value is null\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      val df1 = Seq(0).toDF(\"id\")\n        .withColumn(\"key1\", lit(\"A1\"))\n        .withColumn(\"key2\", lit(null))\n\n      val df2 = Seq(1).toDF(\"id\")\n        .withColumn(\"key1\", lit(null))\n        .withColumn(\"key2\", lit(100))\n\n      writeFiles(tempDir, df1.union(df2), partCols = Seq(\"key1\", \"key2\"))\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 string, key2 int\"))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key2 is null\")\n          .select(\"id\"), Row(0))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 is null\")\n          .select(\"id\"), Row(1))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 'A1'\")\n          .select(\"id\"), Row(0))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key2 = 100\")\n          .select(\"id\"), Row(1))\n    }\n  }\n\n  test(\"converting tables with dateType partition columns\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      val df1 = Seq(0).toDF(\"id\").withColumn(\"key1\", lit(\"2019-11-22\").cast(\"date\"))\n\n      val df2 = Seq(1).toDF(\"id\").withColumn(\"key1\", lit(null))\n\n      writeFiles(tempDir, df1.union(df2), partCols = Seq(\"key1\"))\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 date\"))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 is null\").select(\"id\"),\n        Row(1))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = '2019-11-22'\").select(\"id\"),\n        Row(0))\n    }\n  }\n\n  test(\"empty string partition value will be read back as null\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      val df1 = Seq(0).toDF(\"id\")\n        .withColumn(\"key1\", lit(\"A1\"))\n        .withColumn(\"key2\", lit(\"\"))\n\n      val df2 = Seq(1).toDF(\"id\")\n        .withColumn(\"key1\", lit(\"\"))\n        .withColumn(\"key2\", lit(\"\"))\n\n      writeFiles(tempDir, df1.union(df2), partCols = Seq(\"key1\", \"key2\"))\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 string, key2 string\"))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 is null and key2 is null\")\n          .select(\"id\"), Row(1))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 'A1'\")\n          .select(\"id\"), Row(0))\n    }\n  }\n\n  testSchemaMerging(\"can merge schema with different columns\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"id1\"))\n      writeFiles(tempDir + \"/part=2/\", Seq(2).toDF(\"id2\"))\n      writeFiles(tempDir + \"/part=3/\", Seq(3).toDF(\"id3\"))\n\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n\n      // spell out the columns as intra-batch and inter-batch merging logic may order\n      // the columns differently\n      val cols = Seq(\"id1\", \"id2\", \"id3\", \"part\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"id2 = 2\")\n          .select(cols.head, cols.tail: _*),\n        Row(null, 2, null, \"2\") :: Nil)\n    }\n  }\n\n  testSchemaMerging(\"can merge schema with different nullability\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1).toDF(\"id\"))\n      val schema = new StructType().add(StructField(\"id\", IntegerType, false))\n      val df = spark.createDataFrame(spark.sparkContext.parallelize(Seq(Row(1))), schema)\n      writeFiles(tempDir + \"/part=2/\", df)\n\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n      val fields = spark.read.format(\"delta\").load(tempDir).schema.fields.toSeq\n      assert(fields.map(_.name) === Seq(\"id\", \"part\"))\n      assert(fields.map(_.nullable) === Seq(true, true))\n    }\n  }\n\n  testSchemaMerging(\"can upcast in schema merging: short vs int\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir + \"/part=1/\", Seq(1 << 20).toDF(\"id\"))\n      writeFiles(tempDir + \"/part=2/\",\n        Seq(1).toDF(\"id\").select(col(\"id\") cast ShortType))\n\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"part string\"))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir), Row(1 << 20, \"1\") :: Row(1, \"2\") :: Nil)\n\n      val expectedSchema = new StructType().add(\"id\", IntegerType).add(\"part\", StringType)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      assert(deltaLog.update().metadata.schema === expectedSchema)\n    }\n  }\n\n  test(\"can fetch global configs\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val deltaLog = DeltaLog.forTable(spark, path)\n      withSQLConf(\"spark.databricks.delta.properties.defaults.appendOnly\" -> \"true\") {\n        writeFiles(path, simpleDF.coalesce(1))\n        convertToDelta(s\"parquet.`$path`\")\n      }\n      assert(deltaLog.snapshot.metadata.configuration(\"delta.appendOnly\") === \"true\")\n    }\n  }\n\n  test(\"convert to delta with string partition columns\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF, partCols = Seq(\"key1\", \"key2\"))\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 long, key2 string\"))\n\n      // reads actually went through Delta\n      assert(deltaRead(spark.read.format(\"delta\").load(tempDir).select(\"id\")))\n\n      // query through Delta is correct\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 0\").select(\"id\"),\n        simpleDF.filter(\"id % 2 == 0\").select(\"id\"))\n\n      // delta writers went through\n      writeFiles(\n        tempDir, simpleDF, format = \"delta\", partCols = Seq(\"key1\", \"key2\"), mode = \"append\")\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 1\").select(\"id\"),\n        simpleDF.union(simpleDF).filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  test(\"convert a delta path falsely claimed as parquet\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF, \"delta\")\n\n      // Convert to delta\n      convertToDelta(s\"parquet.`$tempDir`\")\n\n      // Verify that table converted to delta\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 1\").select(\"id\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  test(\"converting a delta path should not error for idempotency\") {\n    withTempDir { dir =>\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, simpleDF, \"delta\")\n      convertToDelta(s\"delta.`$tempDir`\")\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir).where(\"key1 = 1\").select(\"id\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  test(\"partition column name starting with underscore and dot\") {\n    withTempDir { dir =>\n      val df = spark.range(100)\n        .withColumn(\"_key1\", col(\"id\") % 2)\n        .withColumn(\".key2\", col(\"id\") % 7 cast \"String\")\n\n      val tempDir = dir.getCanonicalPath\n      writeFiles(tempDir, df, partCols = Seq(\"_key1\", \".key2\"))\n\n      convertToDelta(s\"parquet.`$tempDir`\", Some(\"_key1 long, `.key2` string\"))\n\n      checkAnswer(sql(s\"SELECT * FROM delta.`$tempDir`\"), df)\n    }\n  }\n}\n\n/**\n * Tests that involve tables defined in a Catalog such as Hive. We test in the sql as well as\n * hive package, where the hive package uses a proper HiveExternalCatalog to alter table definitions\n * in the HiveMetaStore. This test trait *should not* extend SharedSparkSession so that it can be\n * mixed in with the Hive test utilities.\n */\ntrait ConvertToDeltaHiveTableTests extends ConvertToDeltaTestUtils with DeltaSQLTestUtils {\n\n  // Test conversion with and without the new CatalogFileManifest.\n  protected def testCatalogFileManifest(testName: String)(block: (Boolean) => Unit): Unit = {\n    Seq(true, false).foreach { useCatalogFileManifest =>\n      test(s\"$testName - $useCatalogFileManifest\") {\n        withSQLConf(\n          DeltaSQLConf.DELTA_CONVERT_USE_CATALOG_PARTITIONS.key\n            -> useCatalogFileManifest.toString) {\n          block(useCatalogFileManifest)\n        }\n      }\n    }\n  }\n\n  protected def testCatalogSchema(testName: String)(testFn: (Boolean) => Unit): Unit = {\n    Seq(true, false).foreach {\n      useCatalogSchema =>\n        test(s\"$testName - $useCatalogSchema\") {\n          withSQLConf(\n            DeltaSQLConf.DELTA_CONVERT_USE_CATALOG_SCHEMA.key -> useCatalogSchema.toString) {\n            testFn(useCatalogSchema)\n          }\n        }\n    }\n  }\n\n  protected def getPathForTableName(tableName: String): String = {\n    spark\n      .sessionState\n      .catalog\n      .getTableMetadata(TableIdentifier(tableName, Some(\"default\"))).location.getPath\n  }\n\n  protected def verifyExternalCatalogMetadata(tableName: String): Unit = {\n    val catalog = spark.sessionState.catalog.externalCatalog.getTable(\"default\", tableName)\n    // Hive automatically adds some properties\n    val cleanProps = catalog.properties.filterKeys(_ != \"transient_lastDdlTime\")\n    assert(catalog.schema.isEmpty,\n      s\"Schema wasn't empty in the catalog for table $tableName: ${catalog.schema}\")\n    assert(catalog.partitionColumnNames.isEmpty, \"Partition columns weren't empty in the \" +\n      s\"catalog for table $tableName: ${catalog.partitionColumnNames}\")\n    assert(cleanProps.isEmpty,\n      s\"Table properties weren't empty for table $tableName: $cleanProps\")\n  }\n\n  testQuietly(\"negative case: converting non-parquet table\") {\n    val tableName = \"csvtable\"\n    withTable(tableName) {\n      // Create a csv table\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(\"csv\").saveAsTable(tableName)\n\n      // Attempt to convert to delta\n      val ae = intercept[AnalysisException] {\n        convertToDelta(tableName, Some(\"key1 long, key2 string\"))\n      }\n\n      // Get error message\n      assert(ae.getMessage.contains(parquetOnlyMsg))\n    }\n  }\n\n  testQuietly(\"negative case: convert parquet path to delta when there is a database called \" +\n    \"parquet but no table or path exists\") {\n    val dbName = \"parquet\"\n    withDatabase(dbName) {\n      withTempDir { dir =>\n        sql(s\"CREATE DATABASE $dbName\")\n\n        val tempDir = dir.getCanonicalPath\n        // Attempt to convert to delta\n        val ae = intercept[FileNotFoundException] {\n          convertToDelta(s\"parquet.`$tempDir`\")\n        }\n\n        // Get error message\n        assert(ae.getMessage.contains(\"No file found in the directory\"))\n      }\n    }\n  }\n\n  testQuietly(\"negative case: convert views to delta\") {\n    val viewName = \"view\"\n    val tableName = \"pqtbl\"\n    withTable(tableName) {\n      // Create view\n      simpleDF.write.format(\"parquet\").saveAsTable(tableName)\n      sql(s\"CREATE VIEW $viewName as SELECT * from $tableName\")\n\n      // Attempt to convert to delta\n      val ae = intercept[AnalysisException] {\n        convertToDelta(viewName)\n      }\n\n      assert(ae.getMessage.contains(\"Converting a view to a Delta table\"))\n    }\n  }\n\n  testQuietly(\"negative case: converting a table that doesn't exist but the database does\") {\n    val dbName = \"db\"\n    withDatabase(dbName) {\n      sql(s\"CREATE DATABASE $dbName\")\n\n      // Attempt to convert to delta\n      val ae = intercept[AnalysisException] {\n        convertToDelta(s\"$dbName.faketable\", Some(\"key1 long, key2 string\"))\n      }\n\n      assert(ae.getMessage.contains(\"Table or view 'faketable' not found\") ||\n        ae.getMessage.contains(s\"table or view `$dbName`.`faketable` cannot be found\"))\n    }\n  }\n\n  testQuietly(\"negative case: unmatched partition schema\") {\n    val tableName = \"pqtable\"\n    withTable(tableName) {\n      // Create a partitioned parquet table\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(\"parquet\").saveAsTable(tableName)\n\n      // Check the partition schema in the catalog, key1's data type is original Long.\n      assert(spark.sessionState.catalog.getTableMetadata(\n        TableIdentifier(tableName, Some(\"default\"))).partitionSchema\n        .equals(\n          (new StructType)\n            .add(StructField(\"key1\", LongType, true))\n            .add(StructField(\"key2\", StringType, true))\n        ))\n\n      // Convert to delta with partition schema mismatch on key1's data type, which is String.\n      val ae = intercept[AnalysisException] {\n        convertToDelta(tableName, Some(\"key1 string, key2 string\"))\n      }\n\n      assert(ae.getMessage.contains(\"CONVERT TO DELTA was called with a partition schema \" +\n        \"different from the partition schema inferred from the catalog\"))\n    }\n  }\n\n  testQuietly(\"convert two external tables pointing to same underlying files \" +\n    \"with differing table properties should error if conf enabled otherwise merge properties\") {\n    val externalTblName = \"extpqtbl\"\n    val secondExternalTbl = \"othertbl\"\n    withTable(externalTblName, secondExternalTbl) {\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n\n        // Create external table\n        sql(s\"CREATE TABLE $externalTblName \" +\n          s\"USING PARQUET LOCATION '$path' TBLPROPERTIES ('abc'='def', 'def'='ghi') AS SELECT 1\")\n\n        // Create second external table with different table properties\n        sql(s\"CREATE TABLE $secondExternalTbl \" +\n          s\"USING PARQUET LOCATION '$path' TBLPROPERTIES ('abc'='111', 'jkl'='mno')\")\n\n        // Convert first table to delta\n        convertToDelta(externalTblName)\n\n        // Verify that files converted to delta\n        checkAnswer(\n          sql(s\"select * from delta.`$path`\"), Row(1))\n\n        // Verify first table converted to delta\n        assert(spark.sessionState.catalog.getTableMetadata(\n          TableIdentifier(externalTblName, Some(\"default\"))).provider.contains(\"delta\"))\n\n        // Attempt to convert second external table to delta\n        val ae = intercept[AnalysisException] {\n          convertToDelta(secondExternalTbl)\n        }\n\n        assert(\n          ae.getMessage.contains(\"You are trying to convert a table which already has a delta\") &&\n            ae.getMessage.contains(\"convert.metadataCheck.enabled\"))\n\n        // Disable convert metadata check\n        withSQLConf(DeltaSQLConf.DELTA_CONVERT_METADATA_CHECK_ENABLED.key -> \"false\") {\n          // Convert second external table to delta\n          convertToDelta(secondExternalTbl)\n\n          // Check delta table configuration has updated properties\n          assert(DeltaLog.forTable(spark, path).startTransaction().metadata.configuration ==\n            Map(\"abc\" -> \"111\", \"def\" -> \"ghi\", \"jkl\" -> \"mno\"))\n        }\n      }\n    }\n  }\n\n  testQuietly(\"convert two external tables pointing to the same underlying files\") {\n    val externalTblName = \"extpqtbl\"\n    val secondExternalTbl = \"othertbl\"\n    withTable(externalTblName, secondExternalTbl) {\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n        writeFiles(path, simpleDF, \"delta\")\n        val deltaLog = DeltaLog.forTable(spark, path)\n\n        // Create external table\n        sql(s\"CREATE TABLE $externalTblName (key1 long, key2 string) \" +\n          s\"USING PARQUET LOCATION '$path'\")\n\n        // Create second external table\n        sql(s\"CREATE TABLE $secondExternalTbl (key1 long, key2 string) \" +\n          s\"USING PARQUET LOCATION '$path'\")\n\n        assert(deltaLog.update().version == 0)\n\n        // Convert first table to delta\n        convertToDelta(externalTblName)\n\n        // Convert should not update version since delta log metadata is not changing\n        assert(deltaLog.update().version == 0)\n        // Check that the metadata in the catalog was emptied and pushed to the delta log\n        verifyExternalCatalogMetadata(externalTblName)\n\n        // Convert second external table to delta\n        convertToDelta(secondExternalTbl)\n        verifyExternalCatalogMetadata(secondExternalTbl)\n\n        // Verify that underlying files converted to delta\n        checkAnswer(\n          sql(s\"select id from delta.`$path` where key1 = 1\"),\n          simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n\n        // Verify catalog table provider is 'delta' for both tables\n        assert(spark.sessionState.catalog.getTableMetadata(\n          TableIdentifier(externalTblName, Some(\"default\"))).provider.contains(\"delta\"))\n\n        assert(spark.sessionState.catalog.getTableMetadata(\n          TableIdentifier(secondExternalTbl, Some(\"default\"))).provider.contains(\"delta\"))\n\n      }\n    }\n  }\n\n  testQuietly(\"convert an external parquet table\") {\n    val tableName = \"pqtbl\"\n    val externalTblName = \"extpqtbl\"\n    withTable(tableName) {\n      simpleDF.write.format(\"parquet\").saveAsTable(tableName)\n\n      // Get where the table is stored and try to access it using parquet rather than delta\n      val path = getPathForTableName(tableName)\n\n      // Create external table\n      sql(s\"CREATE TABLE $externalTblName (key1 long, key2 string) \" +\n        s\"USING PARQUET LOCATION '$path'\")\n\n      // Convert to delta\n      sql(s\"convert to delta $externalTblName\")\n\n      assert(spark.sessionState.catalog.getTableMetadata(\n        TableIdentifier(externalTblName, Some(\"default\"))).provider.contains(\"delta\"))\n\n      // Verify that table converted to delta\n      checkAnswer(\n        sql(s\"select key2 from delta.`$path` where key1 = 1\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"key2\"))\n\n      checkAnswer(\n        sql(s\"select key2 from $externalTblName where key1 = 1\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"key2\"))\n    }\n  }\n\n  testCatalogSchema(\"convert a parquet table with catalog schema\") {\n    useCatalogSchema => {\n      withTempDir {\n        dir =>\n          // Create a parquet table with all 3 columns: id, key1 and key2\n          val tempDir = dir.getCanonicalPath\n          writeFiles(tempDir, simpleDF)\n\n          val tableName = \"pqtable\"\n          withTable(tableName) {\n            // Create a catalog table on top of the parquet table excluding column id\n            sql(s\"CREATE TABLE $tableName (key1 long, key2 string) \" +\n              s\"USING PARQUET LOCATION '$dir'\")\n\n            convertToDelta(tableName)\n\n          val tableId = TableIdentifier(tableName, Some(\"default\"))\n          val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, tableId)\n            val catalog_columns = Seq[StructField](\n              StructField(\"key1\", LongType, true),\n              StructField(\"key2\", StringType, true)\n            )\n\n            if (useCatalogSchema) {\n              // Catalog schema is used, column id is excluded.\n              assert(snapshot.metadata.schema.equals(StructType(catalog_columns)))\n            } else {\n              // Schema is inferred from the data, all 3 columns are included.\n              assert(snapshot.metadata.schema\n                .equals(StructType(StructField(\"id\", LongType, true) +: catalog_columns)))\n            }\n          }\n      }\n    }\n  }\n\n  testQuietly(\"converting a delta table should not error for idempotency\") {\n    val tableName = \"deltatbl\"\n    val format = \"delta\"\n    withTable(tableName) {\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(format).saveAsTable(tableName)\n      convertToDelta(tableName)\n\n      // reads actually went through Delta\n      val path = getPathForTableName(tableName)\n      checkAnswer(\n        sql(s\"select id from $format.`$path` where key1 = 1\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  testQuietly(\"convert to delta using table name without database name\") {\n    val tableName = \"pqtable\"\n    withTable(tableName) {\n      // Create a parquet table\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(\"parquet\").saveAsTable(tableName)\n\n      // Convert to delta using only table name\n      convertToDelta(tableName, Some(\"key1 long, key2 string\"))\n\n      // reads actually went through Delta\n      val path = getPathForTableName(tableName)\n      checkAnswer(\n        sql(s\"select id from delta.`$path` where key1 = 1\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  testQuietly(\"convert a parquet table to delta with database name as parquet\") {\n    val dbName = \"parquet\"\n    val tableName = \"pqtbl\"\n    withDatabase(dbName) {\n      withTable(dbName + \".\" + tableName) {\n        sql(s\"CREATE DATABASE $dbName\")\n        val table = TableIdentifier(tableName, Some(dbName))\n        simpleDF.write.partitionBy(\"key1\", \"key2\")\n          .format(\"parquet\").saveAsTable(dbName + \".\" + tableName)\n\n        convertToDelta(dbName + \".\" + tableName, Some(\"key1 long, key2 string\"))\n\n        // reads actually went through Delta\n        val path = spark\n          .sessionState\n          .catalog\n          .getTableMetadata(table).location.getPath\n\n        checkAnswer(\n          sql(s\"select id from delta.`$path` where key1 = 1\"),\n          simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n      }\n    }\n  }\n\n  testQuietly(\"convert a parquet path to delta while database called parquet exists\") {\n    val dbName = \"parquet\"\n    withDatabase(dbName) {\n      withTempDir { dir =>\n        // Create a database called parquet\n        sql(s\"CREATE DATABASE $dbName\")\n\n        // Create a parquet table at given path\n        val tempDir = dir.getCanonicalPath\n        writeFiles(tempDir, simpleDF, partCols = Seq(\"key1\", \"key2\"))\n\n        // Convert should convert the path instead of trying to find a table in that database\n        convertToDelta(s\"parquet.`$tempDir`\", Some(\"key1 long, key2 string\"))\n\n        // reads actually went through Delta\n        checkAnswer(\n          sql(s\"select id from delta.`$tempDir` where key1 = 1\"),\n          simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n      }\n    }\n  }\n\n  testQuietly(\"convert a delta table where metadata does not reflect that the table is \" +\n    \"already converted should update the metadata\") {\n    val tableName = \"deltatbl\"\n    withTable(tableName) {\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(\"parquet\").saveAsTable(tableName)\n\n      // Get where the table is stored and try to access it using parquet rather than delta\n      val path = getPathForTableName(tableName)\n\n      // Convert using path so that metadata is not updated\n      convertToDelta(s\"parquet.`$path`\", Some(\"key1 long, key2 string\"))\n\n      // Call convert again\n      convertToDelta(s\"default.$tableName\", Some(\"key1 long, key2 string\"))\n\n      // Metadata should be updated so we can use table name\n      checkAnswer(\n        sql(s\"select id from default.$tableName where key1 = 1\"),\n        simpleDF.filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  testQuietly(\"convert a parquet table using table name\") {\n    val tableName = \"pqtable2\"\n    withTable(tableName) {\n      // Create a parquet table\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(\"parquet\").saveAsTable(tableName)\n\n      // Convert to delta\n      convertToDelta(s\"default.$tableName\", Some(\"key1 long, key2 string\"))\n\n      // Get where the table is stored and try to access it using parquet rather than delta\n      val path = getPathForTableName(tableName)\n\n\n      // reads actually went through Delta\n      assert(deltaRead(sql(s\"select id from default.$tableName\")))\n\n      // query through Delta is correct\n      checkAnswer(\n        sql(s\"select id from default.$tableName where key1 = 0\"),\n        simpleDF.filter(\"id % 2 == 0\").select(\"id\"))\n\n\n      // delta writers went through\n      writeFiles(path, simpleDF, format = \"delta\", partCols = Seq(\"key1\", \"key2\"), mode = \"append\")\n\n      checkAnswer(\n        sql(s\"select id from default.$tableName where key1 = 1\"),\n        simpleDF.union(simpleDF).filter(\"id % 2 == 1\").select(\"id\"))\n    }\n  }\n\n  testQuietly(\"Convert a partitioned parquet table with partition schema autofill\") {\n    val tableName = \"ppqtable\"\n    withTable(tableName) {\n      // Create a partitioned parquet table\n      simpleDF.write.partitionBy(\"key1\", \"key2\").format(\"parquet\").saveAsTable(tableName)\n\n      // Convert to delta without partition schema, partition schema is autofill from catalog\n      convertToDelta(tableName)\n\n      // Verify that table is converted to delta\n      assert(spark.sessionState.catalog.getTableMetadata(\n        TableIdentifier(tableName, Some(\"default\"))).provider.contains(\"delta\"))\n\n      // Check the partition schema in the transaction log\n      val tableId = TableIdentifier(tableName, Some(\"default\"))\n      assert(DeltaLog.forTableWithSnapshot(spark, tableId)._2.metadata.partitionSchema.equals(\n        (new StructType())\n          .add(StructField(\"key1\", LongType, true))\n          .add(StructField(\"key2\", StringType, true))\n      ))\n\n      // Check data in the converted delta table.\n      checkAnswer(\n        sql(s\"SELECT id from default.$tableName where key2 = '2'\"),\n        simpleDF.filter(\"id % 3 == 2\").select(\"id\"))\n    }\n  }\n\n  testCatalogFileManifest(\"convert partitioned parquet table with catalog partitions\") {\n    useCatalogFileManifest => {\n      val tableName = \"ppqtable\"\n      withTable(tableName) {\n        simpleDF.write.partitionBy(\"key1\").format(\"parquet\").saveAsTable(tableName)\n        val path = getPathForTableName(tableName)\n\n        // Create an orphan partition\n        val df = spark.range(100, 200)\n          .withColumn(\"key1\", lit(2))\n          .withColumn(\"key2\", col(\"id\") % 4 cast \"String\")\n\n        df.write.partitionBy(\"key1\")\n          .format(\"parquet\")\n          .mode(\"Append\")\n          .save(path)\n\n        // The path should contains 3 partitions.\n        val partitionDirs = new File(path).listFiles().filter(_.isDirectory)\n        assert(partitionDirs.map(_.getName).sorted\n          .sameElements(Array(\"key1=0\", \"key1=1\", \"key1=2\")))\n\n        // Catalog only contains 2 partitions.\n        assert(spark.sessionState.catalog\n          .listPartitions(TableIdentifier(tableName, Some(\"default\"))).size == 2)\n\n        // Convert table to delta\n        convertToDelta(tableName)\n\n        // Verify that table is converted to delta\n        assert(spark.sessionState.catalog.getTableMetadata(\n          TableIdentifier(tableName, Some(\"default\"))).provider.contains(\"delta\"))\n\n        // Check data in the converted delta table.\n        if (useCatalogFileManifest) {\n          // Partition \"key1=2\" is pruned.\n          checkAnswer(sql(s\"SELECT DISTINCT key1 from default.${tableName}\"), spark.range(2).toDF())\n        } else {\n          // All partitions are preserved.\n          checkAnswer(sql(s\"SELECT DISTINCT key1 from default.${tableName}\"), spark.range(3).toDF())\n        }\n      }\n    }\n  }\n\n  test(\"external tables use correct path scheme\") {\n    withTempDir { dir =>\n      withTable(\"externalTable\") {\n        withSQLConf((\"fs.s3.impl\", classOf[S3LikeLocalFileSystem].getCanonicalName)) {\n          sql(s\"CREATE TABLE externalTable USING parquet LOCATION 's3://$dir' AS SELECT 1\")\n\n          // Ideally we would test a successful conversion with a remote filesystem, but there's\n          // no good way to set one up in unit tests. So instead we delete the data, and let the\n          // FileNotFoundException tell us which scheme it was using to look for it.\n          Utils.deleteRecursively(dir)\n\n          val ex = intercept[FileNotFoundException] {\n            convertToDelta(\"default.externalTable\", None)\n          }\n\n          // If the path incorrectly used the default scheme, this would be file: at the end.\n          assert(ex.getMessage.contains(s\"s3:$dir doesn't exist\"))\n        }\n      }\n    }\n  }\n\n  test(\"can convert a partition-like table path\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      writeFiles(path, simpleDF, partCols = Seq(\"key1\", \"key2\"))\n\n      val basePath = s\"$path/key1=1/\"\n      convertToDelta(s\"parquet.`$basePath`\", Some(\"key2 string\"))\n\n      checkAnswer(\n        sql(s\"select id from delta.`$basePath` where key2 = '1'\"),\n        simpleDF.filter(\"id % 2 == 1\").filter(\"id % 3 == 1\").select(\"id\"))\n    }\n  }\n\n  test(\"can convert table with partition overwrite\") {\n    val tableName = \"ppqtable\"\n    withTable(tableName) {\n      // Create table with original partitions of \"key1=0\" and \"key1=1\".\n      val df = spark.range(0, 100)\n        .withColumn(\"key1\", col(\"id\") % 2)\n        .withColumn(\"key2\", col(\"id\") % 3 cast \"String\")\n      df.write.format(\"parquet\").partitionBy(\"key1\").mode(\"append\").saveAsTable(tableName)\n      checkAnswer(sql(s\"SELECT id FROM $tableName\"), df.select(\"id\"))\n\n      val dataDir =\n        spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName)).location.toString\n\n      // Create orphan partition \"key1=0;key2=3\" with additional column.\n      val df1 = spark.range(100, 120, 2)\n        .withColumn(\"key1\", col(\"id\") % 2)\n        .withColumn(\"key2\", lit(\"3\"))\n      df1.write.format(\"parquet\").partitionBy(\"key1\", \"key2\").mode(\"append\").save(dataDir)\n\n      // Point table partition \"key1=0\" to the path of orphan partition \"key1=0;key2=3\"\n      sql(s\"ALTER TABLE $tableName PARTITION (key1=0) SET LOCATION '$dataDir/key1=0/key2=3/'\")\n      checkAnswer(sql(s\"SELECT id FROM $tableName WHERE key1 = 0\"), df1.select(\"id\"))\n\n      // ConvertToDelta should work without inferring the partition values from partition path.\n      convertToDelta(tableName)\n\n      // Verify that table is converted to delta\n      assert(spark.sessionState.catalog.getTableMetadata(\n        TableIdentifier(tableName, Some(\"default\"))).provider.contains(\"delta\"))\n\n      // Check data in the converted delta table.\n      checkAnswer(sql(s\"SELECT id FROM $tableName WHERE key1 = 0\"), df1.select(\"id\"))\n    }\n  }\n\n  test(s\"catalog partition values contain special characters\") {\n    // Add interesting special characters here for test\n    val specialChars = \" ,;{}()\\n\\t=!@#$%^&*-?.+<_>|/\"\n    val tableName = \"ppqtable\"\n    withTable(tableName) {\n      val valueA = s\"${specialChars}some${specialChars}${specialChars}value${specialChars}A\"\n      val valueB = s\"${specialChars}some${specialChars}${specialChars}value${specialChars}B\"\n      val valueC = s\"${specialChars}some${specialChars}${specialChars}value${specialChars}C\"\n      val valueD = s\"${specialChars}some${specialChars}${specialChars}value${specialChars}D\"\n\n      val df1 = spark.range(3).withColumn(\"key1\", lit(valueA)).withColumn(\"key2\", lit(valueB))\n      val df2 = spark.range(4, 7).withColumn(\"key1\", lit(valueC)).withColumn(\"key2\", lit(valueD))\n      df1.union(df2).write.format(\"parquet\").partitionBy(\"key1\", \"key2\").saveAsTable(tableName)\n\n      convertToDelta(tableName, Some(\"key1 string, key2 string\"))\n\n      // missing one char from valueA, so no match\n      checkAnswer(\n        spark.table(tableName)\n          .where(s\"key1 = '${specialChars}some${specialChars}value${specialChars}A'\")\n          .select(\"id\"), Nil)\n\n      checkAnswer(\n        spark.table(tableName).where(s\"key1 = '$valueA' and key2 = '$valueB'\")\n          .select(\"id\"),\n        Row(0) :: Row(1) :: Row(2) :: Nil)\n\n      checkAnswer(\n        spark.table(tableName).where(s\"key2 = '$valueD' and id > 4\")\n          .select(\"id\"),\n        Row(5) :: Row(6) :: Nil)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/CustomCatalogSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands._\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DummyCatalog, DummySessionCatalog, DummySessionCatalogInner}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.analysis.ResolvedTable\nimport org.apache.spark.sql.catalyst.plans.logical.{AppendData, SetTableProperties, UnaryNode, UnsetTableProperties}\nimport org.apache.spark.sql.connector.catalog.{Identifier, TableCatalog}\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2RelationShim\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass CustomCatalogSuite extends QueryTest with SharedSparkSession\n  with DeltaSQLCommandTest with DescribeDeltaDetailSuiteBase {\n\n  override def sparkConf: SparkConf =\n    super.sparkConf.set(\"spark.sql.catalog.dummy\", classOf[DummyCatalog].getName)\n\n  test(\"CatalogTable exists in DeltaTableV2 if use table identifier\") {\n    def catalogTableExists(sqlCmd: String): Unit = {\n      val plan = spark.sql(sqlCmd).queryExecution.analyzed\n      val catalogTable = plan match {\n        case cmd: UnaryNode with DeltaCommand =>\n          cmd.getDeltaTable(cmd.child, \"dummy\").catalogTable\n        case AppendData(DataSourceV2RelationShim(table: DeltaTableV2, _, _, _, _), _, _, _, _, _) =>\n          table.catalogTable\n        case cmd: DeleteCommand =>\n          cmd.catalogTable\n        case cmd: DescribeDeltaHistoryCommand =>\n          cmd.table.catalogTable\n        case cmd: MergeIntoCommand =>\n          cmd.catalogTable\n        case cmd: RestoreTableCommand =>\n          cmd.sourceTable.catalogTable\n        case SetTableProperties(ResolvedTable(_, _, table: DeltaTableV2, _), _) =>\n          table.catalogTable\n        case UnsetTableProperties(ResolvedTable(_, _, table: DeltaTableV2, _), _, _) =>\n          table.catalogTable\n        case cmd: UpdateCommand =>\n          cmd.catalogTable\n        case cmd: WriteIntoDelta =>\n          cmd.catalogTableOpt\n      }\n      assert(catalogTable.nonEmpty)\n    }\n\n    val mergeSrcTable = \"merge_src_table\"\n    val tableName = \"delta_commands_table\"\n\n    withTable(tableName, mergeSrcTable) {\n      sql(f\"CREATE TABLE $tableName (c1 int, c2 int) USING delta PARTITIONED BY (c1)\")\n      // DQL\n      catalogTableExists(s\"DESCRIBE DETAIL $tableName\")\n      catalogTableExists(s\"DESCRIBE HISTORY $tableName\")\n\n      // DDL\n      catalogTableExists(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('a' = 'b') \")\n      catalogTableExists(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('a') \")\n\n      // DML insert\n      catalogTableExists(s\"INSERT INTO $tableName VALUES (1, 1) \")\n\n      // DML merge\n      sql(s\"CREATE TABLE $mergeSrcTable (c1 int, c2 int) USING delta PARTITIONED BY (c1)\")\n      sql(s\"INSERT INTO $mergeSrcTable VALUES (1, 1) \")\n      catalogTableExists(s\"MERGE INTO $tableName USING $mergeSrcTable \" +\n        s\"ON ${mergeSrcTable}.c1 = ${tableName}.c1 WHEN MATCHED THEN DELETE\")\n\n      // DML update\n      catalogTableExists(s\"UPDATE $tableName SET c1 = 4 WHERE true \")\n\n      // DML delete\n      catalogTableExists(s\"DELETE FROM $tableName WHERE true \")\n\n      // optimize\n      sql(s\"INSERT INTO $tableName VALUES (1, 1) \")\n      sql(s\"INSERT INTO $tableName VALUES (1, 1) \")\n      catalogTableExists(s\"OPTIMIZE $tableName\")\n\n      // vacuum\n      catalogTableExists(s\"VACUUM $tableName\")\n    }\n  }\n\n  test(\"DESC DETAIL a delta table from DummyCatalog\") {\n    val tableName = \"desc_detail_table\"\n    withTable(tableName) {\n      val dummyCatalog =\n        spark.sessionState.catalogManager.catalog(\"dummy\").asInstanceOf[DummyCatalog]\n      val tablePath = dummyCatalog.getTablePath(tableName)\n      sql(\"SET CATALOG dummy\")\n      sql(f\"CREATE TABLE $tableName (id bigint) USING delta\")\n      sql(\"SET CATALOG spark_catalog\")\n      // Insert some data into the table in the dummy catalog.\n      // To make it simple, here we insert data directly into the table path.\n      sql(f\"INSERT INTO delta.`$tablePath` VALUES (0)\")\n      sql(\"SET CATALOG dummy\")\n      // Test simple desc detail command under the dummy catalog\n      checkResult(\n        sql(f\"DESC DETAIL $tableName\"),\n        Seq(\"delta\", 1),\n        Seq(\"format\", \"numFiles\"))\n      // Test 3-part identifier\n      checkResult(\n        sql(f\"DESC DETAIL dummy.default.$tableName\"),\n        Seq(\"delta\", 1),\n        Seq(\"format\", \"numFiles\"))\n      // Test table path\n      checkResult(\n        sql(f\"DESC DETAIL delta.`$tablePath`\"),\n        Seq(\"delta\", 1),\n        Seq(\"format\", \"numFiles\"))\n      // Test 3-part identifier when the current catalog is not dummy catalog\n      sql(\"SET CATALOG spark_catalog\")\n      checkResult(\n        sql(f\"DESC DETAIL dummy.default.$tableName\"),\n        Seq(\"delta\", 1),\n        Seq(\"format\", \"numFiles\"))\n    }\n  }\n\n  test(\"RESTORE a table from DummyCatalog\") {\n    val dummyCatalog =\n      spark.sessionState.catalogManager.catalog(\"dummy\").asInstanceOf[DummyCatalog]\n    val tableName = \"restore_table\"\n    val tablePath = dummyCatalog.getTablePath(tableName)\n    withTable(tableName) {\n      sql(\"SET CATALOG dummy\")\n      sql(f\"CREATE TABLE $tableName (id bigint) USING delta\")\n      sql(\"SET CATALOG spark_catalog\")\n      // Insert some data into the table in the dummy catalog.\n      // To make it simple, here we insert data directly into the table path.\n      sql(f\"INSERT INTO delta.`$tablePath` VALUES (0)\")\n      sql(f\"INSERT INTO delta.`$tablePath` VALUES (1)\")\n      // Test 3-part identifier when the current catalog is the default catalog\n      sql(f\"RESTORE TABLE dummy.default.$tableName VERSION AS OF 1\")\n      checkAnswer(spark.table(f\"dummy.default.$tableName\"), spark.range(1).toDF())\n\n      sql(\"SET CATALOG dummy\")\n      sql(f\"RESTORE TABLE $tableName VERSION AS OF 0\")\n      checkAnswer(spark.table(tableName), Nil)\n      sql(f\"RESTORE TABLE $tableName VERSION AS OF 1\")\n      checkAnswer(spark.table(tableName), spark.range(1).toDF())\n      // Test 3-part identifier\n      sql(f\"RESTORE TABLE dummy.default.$tableName VERSION AS OF 2\")\n      checkAnswer(spark.table(tableName), spark.range(2).toDF())\n      // Test file path table\n      sql(f\"RESTORE TABLE delta.`$tablePath` VERSION AS OF 1\")\n      checkAnswer(spark.table(tableName), spark.range(1).toDF())\n    }\n  }\n\n  test(\"Shallow Clone a table with time travel\") {\n    val srcTable = \"shallow_clone_src_table\"\n    val destTable1 = \"shallow_clone_dest_table_1\"\n    val destTable2 = \"shallow_clone_dest_table_2\"\n    val destTable3 = \"shallow_clone_dest_table_3\"\n    val destTable4 = \"shallow_clone_dest_table_4\"\n    val dummyCatalog =\n      spark.sessionState.catalogManager.catalog(\"dummy\").asInstanceOf[DummyCatalog]\n    val tablePath = dummyCatalog.getTablePath(srcTable)\n    withTable(srcTable) {\n      sql(\"SET CATALOG dummy\")\n      sql(f\"CREATE TABLE $srcTable (id bigint) USING delta\")\n      sql(\"SET CATALOG spark_catalog\")\n      // Insert some data into the table in the dummy catalog.\n      // To make it simple, here we insert data directly into the table path.\n      sql(f\"INSERT INTO delta.`$tablePath` VALUES (0)\")\n      sql(f\"INSERT INTO delta.`$tablePath` VALUES (1)\")\n      withTable(destTable1) {\n        // Test 3-part identifier when the current catalog is the default catalog\n        sql(f\"CREATE TABLE $destTable1 SHALLOW CLONE dummy.default.$srcTable VERSION AS OF 1\")\n        checkAnswer(spark.table(destTable1), spark.range(1).toDF())\n      }\n\n      sql(\"SET CATALOG dummy\")\n      Seq(true, false).foreach { createTableInDummy =>\n        val (dest2, dest3, dest4) = if (createTableInDummy) {\n          (destTable2, destTable3, destTable4)\n        } else {\n          val prefix = \"spark_catalog.default\"\n          (s\"$prefix.$destTable2\", s\"$prefix.$destTable3\", s\"$prefix.$destTable4\")\n        }\n        withTable(dest2, dest3, dest4) {\n          // Test simple shallow clone command under the dummy catalog\n          sql(f\"CREATE TABLE $dest2 SHALLOW CLONE $srcTable\")\n          checkAnswer(spark.table(dest2), spark.range(2).toDF())\n          // Test time travel on the src table\n          sql(f\"CREATE TABLE $dest3 SHALLOW CLONE dummy.default.$srcTable VERSION AS OF 1\")\n          checkAnswer(spark.table(dest3), spark.range(1).toDF())\n          // Test time travel on the src table delta path\n          sql(f\"CREATE TABLE $dest4 SHALLOW CLONE delta.`$tablePath` VERSION AS OF 1\")\n          checkAnswer(spark.table(dest4), spark.range(1).toDF())\n        }\n      }\n    }\n  }\n\n  test(\"DESCRIBE HISTORY a delta table from DummyCatalog\") {\n    val tableName = \"desc_history_table\"\n    withTable(tableName) {\n      sql(\"SET CATALOG dummy\")\n      val dummyCatalog =\n        spark.sessionState.catalogManager.catalog(\"dummy\").asInstanceOf[DummyCatalog]\n      val tablePath = dummyCatalog.getTablePath(tableName)\n      sql(f\"CREATE TABLE $tableName (column1 bigint) USING delta\")\n      sql(\"SET CATALOG spark_catalog\")\n      // Insert some data into the table in the dummy catalog.\n      sql(f\"INSERT INTO delta.`$tablePath` VALUES (0)\")\n\n      sql(\"SET CATALOG dummy\")\n      // Test simple desc detail command under the dummy catalog\n      var result = sql(s\"DESCRIBE HISTORY $tableName\").collect()\n      assert(result.length == 2)\n      assert(result(0).getAs[Long](\"version\") == 1)\n      // Test 3-part identifier\n      result = sql(f\"DESCRIBE HISTORY dummy.default.$tableName\").collect()\n      assert(result.length == 2)\n      assert(result(0).getAs[Long](\"version\") == 1)\n      // Test table path\n      sql(f\"DESC DETAIL delta.`$tablePath`\").collect()\n      assert(result.length == 2)\n      assert(result(0).getAs[Long](\"version\") == 1)\n      // Test 3-part identifier when the current catalog is not dummy catalog\n      sql(\"SET CATALOG spark_catalog\")\n      result = sql(s\"DESCRIBE HISTORY dummy.default.$tableName\").collect()\n      assert(result.length == 2)\n      assert(result(0).getAs[Long](\"version\") == 1)\n    }\n  }\n\n  test(\"SELECT Table Changes from DummyCatalog\") {\n    val dummyTableName = \"dummy_table\"\n    val sparkTableName = \"spark_catalog.default.spark_table\"\n    withTable(dummyTableName, sparkTableName) {\n      sql(\"SET CATALOG spark_catalog\")\n      sql(f\"CREATE TABLE $sparkTableName (id bigint, s string) USING delta\" +\n        f\" TBLPROPERTIES(delta.enableChangeDataFeed=true)\")\n      sql(f\"INSERT INTO $sparkTableName VALUES (0, 'a')\")\n      sql(f\"INSERT INTO $sparkTableName VALUES (1, 'b')\")\n      sql(\"SET CATALOG dummy\")\n      // Since the dummy catalog doesn't pass through the TBLPROPERTIES 'delta.enableChangeDataFeed'\n      // here we clone a table with the same schema as the spark table to test the table changes.\n      sql(f\"CREATE TABLE $dummyTableName SHALLOW CLONE $sparkTableName\")\n      // table_changes() should be able to read the table changes from the dummy catalog\n      Seq(dummyTableName, f\"dummy.default.$dummyTableName\").foreach { name =>\n        val rows = sql(f\"SELECT * from table_changes('$name', 1)\").collect()\n        assert(rows.length == 2)\n      }\n    }\n  }\n\n  test(\"custom catalog that adds additional table storage properties\") {\n    // Reset catalog manager so that the new `spark_catalog` implementation can apply.\n    spark.sessionState.catalogManager.reset()\n    withSQLConf(\"spark.sql.catalog.spark_catalog\" -> classOf[DummySessionCatalog].getName) {\n      withTable(\"t\") {\n        withTempPath { path =>\n          spark.range(10).write.format(\"delta\").save(path.getCanonicalPath)\n          sql(s\"CREATE TABLE t (id LONG) USING delta LOCATION '${path.getCanonicalPath}'\")\n          val t = spark.sessionState.catalogManager.v2SessionCatalog.asInstanceOf[TableCatalog]\n            .loadTable(Identifier.of(Array(\"default\"), \"t\")).asInstanceOf[DeltaTableV2]\n          assert(t.deltaLog.options(\"fs.myKey\") == \"val\")\n        }\n      }\n    }\n  }\n\n  test(\"custom catalog that generates location for managed tables\") {\n    // Reset catalog manager so that the new `spark_catalog` implementation can apply.\n    spark.sessionState.catalogManager.reset()\n    withSQLConf(\"spark.sql.catalog.spark_catalog\" -> classOf[DummySessionCatalog].getName) {\n      withTable(\"t\") {\n        withTempPath { path =>\n          sql(s\"CREATE TABLE t (id LONG) USING delta TBLPROPERTIES (fakeLoc='$path')\")\n          val t = spark.sessionState.catalogManager.v2SessionCatalog.asInstanceOf[TableCatalog]\n            .loadTable(Identifier.of(Array(\"default\"), \"t\"))\n          // It should be a managed table.\n          assert(!t.properties().containsKey(TableCatalog.PROP_EXTERNAL))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DDLTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\n\nimport org.apache.spark.sql.{QueryTest, SparkSession}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{DataType, LongType, StructField}\n\n/**\n * Base trait for specifying column definitions in tests in an API agnostic way.\n *\n * Note: we don't use StructField because StructField is defined in Spark. It's easier to\n * write tests with flexible helpers in our own project.\n */\ntrait ColumnSpec {\n  /** Name of the column. */\n  def colName: String\n\n  /** Spark logical type for the column. */\n  def dataType: DataType\n\n  /** Returns a String which can be used to define the column in SQL. */\n  def ddl: String\n\n  /** Return the specification as a StructField */\n  def structField(spark: SparkSession): StructField\n}\n\ncase class GeneratedColumnSpec(\n    colName: String,\n    dataType: DataType,\n    generatedExpression: String)\n  extends ColumnSpec {\n\n  override def ddl: String =\n    s\"$colName ${dataType.sql} GENERATED ALWAYS AS ($generatedExpression)\"\n\n  override def structField(spark: SparkSession): StructField = {\n    io.delta.tables.DeltaTable.columnBuilder(spark, colName)\n      .dataType(dataType)\n      .generatedAlwaysAs(generatedExpression)\n      .build()\n  }\n}\n\ncase class TestColumnSpec(\n    colName: String,\n    dataType: DataType)\n  extends ColumnSpec {\n\n  override def ddl: String = {\n    s\"$colName ${dataType.sql}\"\n  }\n\n  override def structField(spark: SparkSession): StructField = {\n    io.delta.tables.DeltaTable.columnBuilder(spark, colName)\n      .dataType(dataType)\n      .build()\n  }\n}\n\nobject GeneratedAsIdentityType extends Enumeration {\n  type GeneratedAsIdentityType = Value\n  val GeneratedAlways, GeneratedByDefault = Value\n}\n\ncase class IdentityColumnSpec(\n    generatedAsIdentityType: GeneratedAsIdentityType.GeneratedAsIdentityType,\n    startsWith: Option[Long] = None,\n    incrementBy: Option[Long] = None,\n    colName: String = \"id\",\n    dataType: DataType = LongType,\n    comment: Option[String] = None,\n    nullable: Boolean = true)\n  extends ColumnSpec {\n\n  override def ddl: String = {\n    throw new UnsupportedOperationException(\n      \"DDL generation is not supported for identity columns yet\")\n  }\n\n  override def structField(spark: SparkSession): StructField = {\n    var col = io.delta.tables.DeltaTable.columnBuilder(spark, colName)\n      .dataType(dataType)\n      .nullable(nullable)\n    val start = startsWith.getOrElse(IdentityColumn.defaultStart.toLong)\n    val step = incrementBy.getOrElse(IdentityColumn.defaultStep.toLong)\n    col = generatedAsIdentityType match {\n      case GeneratedAsIdentityType.GeneratedAlways =>\n        col.generatedAlwaysAsIdentity(start, step)\n      case GeneratedAsIdentityType.GeneratedByDefault =>\n        col.generatedByDefaultAsIdentity(start, step)\n    }\n\n    comment.foreach { c =>\n      col = col.comment(c)\n    }\n\n    col.build()\n  }\n}\n\ntrait DDLTestUtils\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest {\n  protected object DDLType extends Enumeration {\n    val CREATE, REPLACE, CREATE_OR_REPLACE = Value\n  }\n\n  /** Interface (SQL, Scala) agnostic helper to execute the DDL statement. */\n  protected def runDDL(\n      ddlType: DDLType.Value,\n      tableName: String,\n      columnSpecs: Seq[ColumnSpec],\n      partitionedBy: Seq[String],\n      tblProperties: Map[String, String]): Unit\n\n  def createTable(\n      tableName: String,\n      columnSpecs: Seq[ColumnSpec],\n      partitionedBy: Seq[String] = Nil,\n      tblProperties: Map[String, String] = Map.empty): Unit = {\n    runDDL(DDLType.CREATE, tableName, columnSpecs, partitionedBy, tblProperties)\n  }\n\n  def replaceTable(\n      tableName: String,\n      columnSpecs: Seq[ColumnSpec],\n      partitionedBy: Seq[String] = Nil,\n      tblProperties: Map[String, String] = Map.empty): Unit = {\n    runDDL(DDLType.REPLACE, tableName, columnSpecs, partitionedBy, tblProperties)\n  }\n\n  def createOrReplaceTable(\n      tableName: String,\n      columnSpecs: Seq[ColumnSpec],\n      partitionedBy: Seq[String] = Nil,\n      tblProperties: Map[String, String] = Map.empty): Unit = {\n    runDDL(DDLType.CREATE_OR_REPLACE, tableName, columnSpecs, partitionedBy, tblProperties)\n  }\n}\n\n\ntrait SQLDDLTestUtils extends DDLTestUtils {\n  private def getPartitionByClause(partitionedBy: Seq[String]): String = {\n    if (partitionedBy.nonEmpty) {\n      s\"PARTITIONED BY (${partitionedBy.mkString(\", \")})\"\n    } else {\n      \"\"\n    }\n  }\n\n  protected def runDDL(\n      ddlType: DDLType.Value,\n      tableName: String,\n      columnSpecs: Seq[ColumnSpec],\n      partitionedBy: Seq[String],\n      tblProperties: Map[String, String]): Unit = {\n    val columnDefinitions = columnSpecs.map(_.ddl).mkString(\",\\n\")\n    val ddlClause = ddlType match {\n      case DDLType.CREATE =>\n        \"CREATE TABLE\"\n      case DDLType.REPLACE =>\n        \"REPLACE TABLE\"\n      case DDLType.CREATE_OR_REPLACE =>\n        \"CREATE OR REPLACE TABLE\"\n    }\n\n    val tblPropertiesClause = if (tblProperties.nonEmpty) {\n      val tblPropertiesStr =\n        tblProperties.map { case (k, v) => s\"'$k' = '$v'\" }.mkString(\", \")\n      s\"TBLPROPERTIES ($tblPropertiesStr)\"\n    } else {\n      \"\"\n    }\n\n    sql(\n      s\"\"\"\n         |$ddlClause $tableName(\n         |  $columnDefinitions\n         |) USING delta\n         |${getPartitionByClause(partitionedBy)}\n         |$tblPropertiesClause\n         |\"\"\".stripMargin)\n  }\n}\n\ntrait ScalaDDLTestUtils extends DDLTestUtils {\n  protected def runDDL(\n      ddlType: DDLType.Value,\n      tableName: String,\n      columnSpecs: Seq[ColumnSpec],\n      partitionedBy: Seq[String],\n      tblProperties: Map[String, String]): Unit = {\n    val builder = ddlType match {\n      case DDLType.CREATE =>\n        io.delta.tables.DeltaTable.create(spark)\n      case DDLType.REPLACE =>\n        io.delta.tables.DeltaTable.replace(spark)\n      case DDLType.CREATE_OR_REPLACE =>\n        io.delta.tables.DeltaTable.createOrReplace(spark)\n    }\n\n    builder.tableName(tableName)\n\n    columnSpecs.foreach { columnSpec =>\n      val colAsStructField = columnSpec.structField(spark)\n      builder.addColumn(colAsStructField)\n    }\n\n    if (partitionedBy.nonEmpty) {\n      builder.partitionedBy(partitionedBy: _*)\n    }\n\n    for ((key, value) <- tblProperties) {\n      builder.property(key, value)\n    }\n\n    builder.execute()\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DelegatingLogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.storage.{DelegatingLogStore, LogStore, LogStoreAdaptor}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.{SparkConf, SparkFunSuite}\nimport org.apache.spark.sql.LocalSparkSession._\nimport org.apache.spark.sql.SparkSession\n\nclass DelegatingLogStoreSuite\n  extends SparkFunSuite {\n\n\n  private val customLogStoreClassName = classOf[CustomPublicLogStore].getName\n  private def fakeSchemeWithNoDefault = \"fake\"\n\n  private def constructSparkConf(confs: Seq[(String, String)]): SparkConf = {\n    val sparkConf = new SparkConf(loadDefaults = false).setMaster(\"local\")\n    confs.foreach { case (key, value) => sparkConf.set(key, value) }\n    sparkConf\n  }\n\n  /**\n   * Test DelegatingLogStore by directly creating a DelegatingLogStore and test LogStore\n   * resolution based on input `scheme`. This is not an end-to-end test.\n   *\n   * @param scheme The scheme to be used for testing.\n   * @param sparkConf The spark configuration to use.\n   * @param expClassName Expected LogStore class name resolved by DelegatingLogStore.\n   * @param expAdaptor True if DelegatingLogStore is expected to resolve to LogStore adaptor, for\n   *                   which the actual implementation inside will be checked. This happens when\n   *                   LogStore is set to subclass of the new [[io.delta.storage.LogStore]] API.\n   */\n  private def testDelegatingLogStore(\n      scheme: String,\n      sparkConf: SparkConf,\n      expClassName: String,\n      expAdaptor: Boolean): Unit = {\n    withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n      val sc = spark.sparkContext\n      val delegatingLogStore = new DelegatingLogStore(sc.hadoopConfiguration)\n      val actualLogStore = delegatingLogStore.getDelegate(\n        new Path(s\"${scheme}://dummy\"))\n      if (expAdaptor) {\n        assert(actualLogStore.isInstanceOf[LogStoreAdaptor])\n        assert(actualLogStore.asInstanceOf[LogStoreAdaptor]\n          .logStoreImpl.getClass.getName == expClassName)\n      } else {\n        assert(actualLogStore.getClass.getName == expClassName)\n      }\n    }\n  }\n\n  /** Test the default LogStore resolution for `scheme` */\n  private def testDefaultSchemeResolution(scheme: String, expClassName: String): Unit = {\n    testDelegatingLogStore(\n      scheme,\n      constructSparkConf(Seq.empty), // we set no custom LogStore confs\n      expClassName,\n      expAdaptor = true // all default implementations are from delta-storage\n    )\n  }\n\n  /** Test LogStore resolution with a customized scheme conf */\n  private def testCustomSchemeResolution(\n      scheme: String, className: String, expAdaptor: Boolean): Unit = {\n    val sparkPrefixKey = LogStore.logStoreSchemeConfKey(scheme)\n    val nonSparkPrefixKey = sparkPrefixKey.stripPrefix(\"spark.\")\n    // only set spark-prefixed key\n    testDelegatingLogStore(\n      scheme,\n      constructSparkConf(Seq((sparkPrefixKey, className))),\n      className, // we expect our custom-set LogStore class\n      expAdaptor\n    )\n    // only set non-spark-prefixed key\n    testDelegatingLogStore(\n      scheme,\n      constructSparkConf(Seq((nonSparkPrefixKey, className))),\n      className, // we expect our custom-set LogStore class\n      expAdaptor\n    )\n    // set both\n    testDelegatingLogStore(\n      scheme,\n      constructSparkConf(Seq((nonSparkPrefixKey, className), (sparkPrefixKey, className))),\n      className, // we expect our custom-set LogStore class\n      expAdaptor\n    )\n  }\n\n  test(\"DelegatingLogStore resolution using default scheme confs\") {\n    for (scheme <- DelegatingLogStore.s3Schemes) {\n      testDefaultSchemeResolution(\n        scheme,\n        expClassName = DelegatingLogStore.defaultS3LogStoreClassName)\n    }\n    for (scheme <- DelegatingLogStore.azureSchemes) {\n      testDefaultSchemeResolution(\n        scheme,\n        expClassName = DelegatingLogStore.defaultAzureLogStoreClassName)\n    }\n    for (scheme <- DelegatingLogStore.gsSchemes) {\n      testDefaultSchemeResolution(\n        scheme,\n        expClassName = DelegatingLogStore.defaultGCSLogStoreClassName)\n    }\n    testDefaultSchemeResolution(\n      scheme = fakeSchemeWithNoDefault,\n      expClassName = DelegatingLogStore.defaultHDFSLogStoreClassName)\n  }\n\n  test(\"DelegatingLogStore resolution using customized scheme confs\") {\n    val allTestSchemes = DelegatingLogStore.s3Schemes ++ DelegatingLogStore.azureSchemes +\n      fakeSchemeWithNoDefault\n    for (scheme <- allTestSchemes) {\n      for (store <- Seq(\n        // default (java) classes (in io.delta.storage)\n        \"io.delta.storage.S3SingleDriverLogStore\",\n        \"io.delta.storage.AzureLogStore\",\n        \"io.delta.storage.HDFSLogStore\",\n        // deprecated (scala) classes\n        classOf[org.apache.spark.sql.delta.storage.S3SingleDriverLogStore].getName,\n        classOf[org.apache.spark.sql.delta.storage.AzureLogStore].getName,\n        classOf[org.apache.spark.sql.delta.storage.HDFSLogStore].getName,\n        customLogStoreClassName)) {\n\n        // we set spark.delta.logStore.${scheme}.impl -> $store\n        testCustomSchemeResolution(\n          scheme,\n          store,\n          expAdaptor = store.contains(\"io.delta.storage\") || store == customLogStoreClassName)\n      }\n    }\n  }\n}\n\n//////////////////\n// Helper Class //\n//////////////////\n\nclass CustomPublicLogStore(initHadoopConf: Configuration)\n  extends io.delta.storage.LogStore(initHadoopConf) {\n\n  private val logStoreInternal = new io.delta.storage.HDFSLogStore(initHadoopConf)\n\n  override def read(\n      path: Path,\n      hadoopConf: Configuration): io.delta.storage.CloseableIterator[String] = {\n    logStoreInternal.read(path, hadoopConf)\n  }\n\n  override def write(\n      path: Path,\n      actions: java.util.Iterator[String],\n      overwrite: java.lang.Boolean,\n      hadoopConf: Configuration): Unit = {\n    logStoreInternal.write(path, actions, overwrite, hadoopConf)\n  }\n\n  override def listFrom(\n      path: Path,\n      hadoopConf: Configuration): java.util.Iterator[FileStatus] = {\n    logStoreInternal.listFrom(path, hadoopConf)\n  }\n\n  override def resolvePathOnPhysicalStorage(\n      path: Path,\n      hadoopConf: Configuration): Path = {\n    logStoreInternal.resolvePathOnPhysicalStorage(path, hadoopConf)\n  }\n\n  override def isPartialWriteVisible(path: Path, hadoopConf: Configuration): java.lang.Boolean = {\n    logStoreInternal.isPartialWriteVisible(path, hadoopConf)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeleteMetricsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.DatabricksLogging\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.commands.DeleteMetric\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{Dataset, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.expr\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Tests for metrics of Delta DELETE command.\n */\nclass DeleteMetricsSuite extends QueryTest\n  with SharedSparkSession\n  with DatabricksLogging\n  with DeltaSQLCommandTest {\n\n\n  /*\n   * Case class to parameterize tests.\n   */\n  case class TestConfiguration(\n      partitioned: Boolean,\n      cdfEnabled: Boolean\n  )\n\n  case class TestMetricResults(\n      operationMetrics: Map[String, Long],\n      numAffectedRows: Long\n  )\n\n  /*\n   * Helper to generate tests for all configuration parameters.\n   */\n  protected def testDeleteMetrics(name: String)(testFn: TestConfiguration => Unit): Unit = {\n    for {\n      partitioned <- BOOLEAN_DOMAIN\n      cdfEnabled <- BOOLEAN_DOMAIN\n    } {\n      val testConfig = TestConfiguration(\n        partitioned = partitioned,\n        cdfEnabled = cdfEnabled\n      )\n      var testName =\n        s\"delete-metrics: $name - Partitioned = $partitioned, cdfEnabled = $cdfEnabled\"\n      test(testName) {\n        testFn(testConfig)\n      }\n    }\n  }\n\n  /*\n   * Create a table from the provided dataset.\n   *\n   * If an partitioned table is needed, then we create one data partition per Spark partition,\n   * i.e. every data partition will contain one file.\n   *\n   * Also an extra column is added to be used in non-partition filters.\n   */\n  protected def createTempTable(\n      table: Dataset[_],\n      tableName: String,\n      testConfig: TestConfiguration): Unit = {\n    val numRows = table.count()\n    val numPartitions = table.rdd.getNumPartitions\n    val numRowsPerPart = if (numRows > 0 && numPartitions < numRows) numRows / numPartitions else 1\n    val partitionBy = if (testConfig.partitioned) Seq(\"partCol\") else Seq()\n    table.withColumn(\"partCol\", expr(s\"floor(id / $numRowsPerPart)\"))\n      .withColumn(\"extraCol\", expr(s\"$numRows - id\"))\n      .write\n      .partitionBy(partitionBy: _*)\n      .format(\"delta\")\n      .saveAsTable(tableName)\n  }\n\n  /*\n   * Run a delete command, and capture number of affected rows, operation metrics from Delta\n   * log and usage metrics.\n   */\n  def runDeleteAndCaptureMetrics(\n      table: Dataset[_],\n      where: String,\n      testConfig: TestConfiguration): TestMetricResults = {\n    val tableName = \"target\"\n    val whereClause = Option(where).map(c => s\"WHERE $c\").getOrElse(\"\")\n    var numAffectedRows = -1L\n    var operationMetrics: Map[String, Long] = null\n    withSQLConf(\n      DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\",\n      DeltaSQLConf.DELTA_SKIP_RECORDING_EMPTY_COMMITS.key -> \"false\",\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey ->\n        testConfig.cdfEnabled.toString) {\n      withTable(tableName) {\n        createTempTable(table, tableName, testConfig)\n\n          val resultDf = spark.sql(s\"DELETE FROM $tableName $whereClause\")\n          assert(!resultDf.isEmpty)\n          numAffectedRows = resultDf.take(1).head(0).toString.toLong\n\n        operationMetrics = DeltaMetricsUtils.getLastOperationMetrics(tableName)\n\n        // Check operation metrics against commit actions.\n        val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n        DeltaMetricsUtils.checkOperationMetricsAgainstCommitActions(\n          deltaLog, snapshot.version, operationMetrics)\n\n      }\n    }\n    TestMetricResults(\n      operationMetrics,\n      numAffectedRows\n    )\n  }\n\n  /*\n   * Run a delete command and check all available metrics.\n   * We allow some metrics to be missing, by setting their value to -1.\n   */\n  def runDeleteAndCheckMetrics(\n    table: Dataset[_],\n    where: String,\n    expectedNumAffectedRows: Long,\n    expectedOperationMetrics: Map[String, Long],\n    testConfig: TestConfiguration): Unit = {\n    // Run the delete capture and get all metrics.\n    val testMetricResults = runDeleteAndCaptureMetrics(table, where, testConfig)\n    val operationMetrics = testMetricResults.operationMetrics\n\n    // Check the number of deleted rows.\n    assert(testMetricResults.numAffectedRows === expectedNumAffectedRows)\n\n    // Check operation metrics schema.\n    val unknownKeys = operationMetrics.keySet -- DeltaOperationMetrics.DELETE --\n      DeltaOperationMetrics.WRITE\n    assert(unknownKeys.isEmpty,\n      s\"Unknown operation metrics for DELETE command: ${unknownKeys.mkString(\", \")}\")\n\n    // Check values of expected operation metrics. For all unspecified deterministic metrics,\n    // we implicitly expect a zero value.\n    val requiredMetrics = Set(\n      \"numCopiedRows\",\n      \"numDeletedRows\",\n      \"numAddedFiles\",\n      \"numRemovedFiles\",\n      \"numAddedChangeFiles\")\n    val expectedMetricsWithDefaults =\n      requiredMetrics.map(k => k -> 0L).toMap ++ expectedOperationMetrics\n    val expectedMetricsFiltered = expectedMetricsWithDefaults.filter(_._2 >= 0)\n    DeltaMetricsUtils.checkOperationMetrics(\n      expectedMetrics = expectedMetricsFiltered,\n      operationMetrics = operationMetrics)\n\n\n    // Check time operation metrics.\n    val expectedTimeMetrics =\n    Set(\"scanTimeMs\", \"rewriteTimeMs\", \"executionTimeMs\").filter(\n      k => expectedOperationMetrics.get(k).forall(_ >= 0)\n    )\n    DeltaMetricsUtils.checkOperationTimeMetrics(\n      operationMetrics = operationMetrics,\n      expectedMetrics = expectedTimeMetrics)\n  }\n\n\n  val zeroDeleteMetrics: DeleteMetric = DeleteMetric(\n    condition = \"\",\n    numFilesTotal = 0,\n    numTouchedFiles = 0,\n    numRewrittenFiles = 0,\n    numRemovedFiles = 0,\n    numAddedFiles = 0,\n    numAddedChangeFiles = 0,\n    numFilesBeforeSkipping = 0,\n    numBytesBeforeSkipping = -1, // We don't want to assert equality on bytes\n    numFilesAfterSkipping = 0,\n    numBytesAfterSkipping = -1, // We don't want to assert equality on bytes\n    numPartitionsAfterSkipping = None,\n    numPartitionsAddedTo = None,\n    numPartitionsRemovedFrom = None,\n    numCopiedRows = None,\n    numDeletedRows = None,\n    numBytesAdded = -1, // We don't want to assert equality on bytes\n    numBytesRemoved = -1, // We don't want to assert equality on bytes\n    changeFileBytes = -1, // We don't want to assert equality on bytes\n    scanTimeMs = 0,\n    rewriteTimeMs = 0,\n    numDeletionVectorsAdded = 0,\n    numDeletionVectorsRemoved = 0,\n    numDeletionVectorsUpdated = 0\n  )\n\n\n  test(\"delete along partition boundary\") {\n    import testImplicits._\n\n    Seq(true, false).foreach { cdfEnabled =>\n      Seq(true, false).foreach { deltaCollectStatsEnabled =>\n        Seq(true, false).foreach { deltaDmlMetricsFromMetadataEnabled =>\n          withSQLConf(\n            DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> cdfEnabled.toString,\n            DeltaSQLConf.DELTA_COLLECT_STATS.key -> deltaCollectStatsEnabled.toString,\n            DeltaSQLConf.DELTA_DML_METRICS_FROM_METADATA.key\n              -> deltaDmlMetricsFromMetadataEnabled.toString\n          ) {\n            withTable(\"t1\") {\n              spark.range(100).withColumn(\"part\", 'id % 10).toDF().write\n                .partitionBy(\"part\").format(\"delta\").saveAsTable(\"t1\")\n              val result = spark.sql(\"DELETE FROM t1 WHERE part=1\")\n                .take(1).head(0).toString.toLong\n              val opMetrics = DeltaMetricsUtils.getLastOperationMetrics(\"t1\")\n\n              assert(opMetrics(\"numRemovedFiles\") > 0)\n              if (deltaCollectStatsEnabled && deltaDmlMetricsFromMetadataEnabled) {\n                assert(opMetrics(\"numDeletedRows\") == 10)\n                assert(result == 10)\n              } else {\n                assert(!opMetrics.contains(\"numDeletedRows\"))\n                assert(result == -1)\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  testDeleteMetrics(\"delete from empty table\") { testConfig =>\n    for (where <- Seq(\"\", \"1 = 1\", \"1 != 1\", \"id > 50\")) {\n      def executeTest: Unit = runDeleteAndCheckMetrics(\n        table = spark.range(0),\n        where = where,\n        expectedNumAffectedRows = 0,\n        expectedOperationMetrics = Map(\n          \"numCopiedRows\" -> 0,\n          \"numDeletedRows\" -> 0,\n          \"numAddedFiles\" -> 0,\n          \"numRemovedFiles\" -> 0,\n          \"numAddedChangeFiles\" -> 0,\n          \"scanTimeMs\" -> -1,\n          \"rewriteTimeMs\" -> -1,\n          \"executionTimeMs\" -> -1\n        ),\n        testConfig = testConfig\n      )\n\n      executeTest\n    }\n  }\n\n  for (whereClause <- Seq(\"\", \"1 = 1\")) {\n    testDeleteMetrics(s\"delete all with where = '$whereClause'\") { testConfig =>\n      runDeleteAndCheckMetrics(\n        table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n        where = whereClause,\n        expectedNumAffectedRows = 100,\n        expectedOperationMetrics = Map(\n          \"numCopiedRows\" -> -1,\n          \"numDeletedRows\" -> 100,\n          \"numOutputRows\" -> -1,\n          \"numFiles\" -> -1,\n          \"numAddedFiles\" -> -1,\n          \"numRemovedFiles\" -> 5,\n          \"numAddedChangeFiles\" -> 0\n        ),\n        testConfig = testConfig\n      )\n    }\n  }\n\n  testDeleteMetrics(\"delete with false predicate\") { testConfig => {\n    runDeleteAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"1 != 1\",\n      expectedNumAffectedRows = 0L,\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numDeletedRows\" -> 0,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> 0,\n        \"numAddedChangeFiles\" -> 0,\n        \"scanTimeMs\" -> -1,\n        \"rewriteTimeMs\" -> -1,\n        \"executionTimeMs\" -> -1\n      ),\n      testConfig = testConfig\n    )\n  }}\n\n  testDeleteMetrics(\"delete with unsatisfied static predicate\") { testConfig => {\n    runDeleteAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id < 0 or id > 100\",\n      expectedNumAffectedRows = 0L,\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numDeletedRows\" -> 0,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> 0,\n        \"numAddedChangeFiles\" -> 0,\n        \"scanTimeMs\" -> -1,\n        \"rewriteTimeMs\" -> -1,\n        \"executionTimeMs\" -> -1\n      ),\n      testConfig = testConfig\n    )\n  }}\n\n  testDeleteMetrics(\"delete with unsatisfied dynamic predicate\") { testConfig => {\n    runDeleteAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id / 200 > 1 \",\n      expectedNumAffectedRows = 0L,\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numDeletedRows\" -> 0,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> 0,\n        \"numAddedChangeFiles\" -> 0,\n        \"scanTimeMs\" -> -1,\n        \"rewriteTimeMs\" -> -1,\n        \"executionTimeMs\" -> -1\n      ),\n      testConfig = testConfig\n    )\n  }}\n\n  for (whereClause <- Seq(\"id = 0\", \"id >= 49 and id < 50\")) {\n    testDeleteMetrics(s\"delete one row with where = `$whereClause`\") { testConfig =>\n      var numAddedFiles = 1\n      var numRemovedFiles = 1\n      val numRemovedRows = 1\n      var numCopiedRows = 19\n      runDeleteAndCheckMetrics(\n        table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n        where = whereClause,\n        expectedNumAffectedRows = 1L,\n        expectedOperationMetrics = Map(\n          \"numCopiedRows\" -> numCopiedRows,\n          \"numDeletedRows\" -> numRemovedRows,\n          \"numAddedFiles\" -> numAddedFiles,\n          \"numRemovedFiles\" -> numRemovedFiles,\n          \"numAddedChangeFiles\" -> {\n            if (testConfig.cdfEnabled\n            ) {\n              1\n            } else {\n              0\n            }\n          }\n        ),\n        testConfig = testConfig\n      )\n    }\n  }\n\n  testDeleteMetrics(\"delete one file\") { testConfig =>\n    val numRemovedFiles = 1\n    val numRemovedRows = 20\n\n    def executeTest: Unit = runDeleteAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id < 20\",\n      expectedNumAffectedRows = 20L,\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numDeletedRows\" -> numRemovedRows,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> numRemovedFiles,\n        \"numAddedChangeFiles\" -> {\n          if (testConfig.cdfEnabled\n          ) {\n            1\n          } else {\n            0\n          }\n        }\n      ),\n      testConfig = testConfig\n    )\n\n    executeTest\n  }\n\n  testDeleteMetrics(\"delete one row per file\") { testConfig =>\n    var numRemovedFiles = 5\n    val numRemovedRows = 5\n    var numCopiedRows = 95\n    var numAddedFiles = if (testConfig.partitioned) 5 else 2\n    runDeleteAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id in (5, 25, 45, 65, 85)\",\n      expectedNumAffectedRows = 5L,\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> numCopiedRows,\n        \"numDeletedRows\" -> numRemovedRows,\n        \"numAddedFiles\" -> numAddedFiles,\n        \"numRemovedFiles\" -> numRemovedFiles,\n        \"numAddedChangeFiles\" -> { if (testConfig.cdfEnabled) numAddedFiles else 0 }\n      ),\n    testConfig = testConfig\n    )\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeleteSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaExcludedTestMixin, DeltaSQLCommandTest}\n\nimport org.apache.spark.sql.Row\n\ntrait DeleteSQLMixin extends DeleteBaseMixin\n  with DeltaDMLTestUtils\n  with DeltaSQLCommandTest {\n\n  override protected def executeDelete(target: String, where: String = null): Unit = {\n    val whereClause = Option(where).map(c => s\"WHERE $c\").getOrElse(\"\")\n    sql(s\"DELETE FROM $target $whereClause\")\n  }\n}\n\ntrait DeleteSQLTests extends DeleteSQLMixin {\n  import testImplicits._\n\n  // For EXPLAIN, which is not supported in OSS\n  test(\"explain\") {\n    append(Seq((2, 2)).toDF(\"key\", \"value\"))\n    val df = sql(s\"EXPLAIN DELETE FROM $tableSQLIdentifier WHERE key = 2\")\n    val outputs = df.collect().map(_.mkString).mkString\n    assert(outputs.contains(\"Delta\"))\n    assert(!outputs.contains(\"index\") && !outputs.contains(\"ActionLog\"))\n    // no change should be made by explain\n    checkAnswer(readDeltaTableByIdentifier(), Row(2, 2))\n  }\n\n  test(\"delete from a temp view\") {\n    withTable(\"tab\") {\n      withTempView(\"v\") {\n        Seq((1, 1), (0, 3), (1, 5)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        spark.table(\"tab\").as(\"name\").createTempView(\"v\")\n        sql(\"DELETE FROM v WHERE key = 1\")\n        checkAnswer(spark.table(\"tab\"), Row(0, 3))\n      }\n    }\n  }\n\n  test(\"delete from a SQL temp view\") {\n    withTable(\"tab\") {\n      withTempView(\"v\") {\n        Seq((1, 1), (0, 3), (1, 5)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        sql(\"CREATE TEMP VIEW v AS SELECT * FROM tab\")\n        sql(\"DELETE FROM v WHERE key = 1 AND VALUE = 5\")\n        checkAnswer(spark.table(\"tab\"), Seq(Row(1, 1), Row(0, 3)))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { partitioned =>\n    test(s\"User defined _change_type column doesn't get dropped - partitioned=$partitioned\") {\n      withTable(\"tab\") {\n        sql(\n          s\"\"\"CREATE TABLE tab USING DELTA\n             |${if (partitioned) \"PARTITIONED BY (part) \" else \"\"}\n             |TBLPROPERTIES (delta.enableChangeDataFeed = false)\n             |AS SELECT id, int(id / 10) AS part, 'foo' as _change_type\n             |FROM RANGE(1000)\n             |\"\"\".stripMargin)\n        val rowsToDelete = (1 to 1000 by 42).mkString(\"(\", \", \", \")\")\n        executeDelete(\"tab\", s\"id in $rowsToDelete\")\n        sql(\"SELECT id, _change_type FROM tab\").collect().foreach { row =>\n          val _change_type = row.getString(1)\n          assert(_change_type === \"foo\", s\"Invalid _change_type for id=${row.get(0)}\")\n        }\n      }\n    }\n  }\n}\n\ntrait DeleteSQLNameColumnMappingMixin extends DeleteSQLMixin\n  with DeltaColumnMappingSelectedTestMixin {\n\n  protected override def runOnlyTests: Seq[String] = Seq(true, false).map { isPartitioned =>\n    s\"basic case - delete from a Delta table - Partition=$isPartitioned\"\n  } ++ Seq(true, false).flatMap { isPartitioned =>\n    Seq(\n      s\"where key columns - Partition=$isPartitioned\",\n      s\"where data columns - Partition=$isPartitioned\")\n  }\n\n}\n\ntrait DeleteSQLWithDeletionVectorsMixin extends DeleteSQLMixin\n  with DeltaExcludedTestMixin\n  with DeletionVectorsTestUtils\n  with DeltaDMLTestUtilsPathBased {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark, delete = true)\n  }\n\n  override def excluded: Seq[String] = super.excluded ++\n    Seq(\n      // The following two tests must fail when DV is used. Covered by another test case:\n      // \"throw error when non-pinned TahoeFileIndex snapshot is used\".\n      \"data and partition columns - Partition=true Skipping=false\",\n      \"data and partition columns - Partition=false Skipping=false\",\n      // The scan schema contains additional row index filter columns.\n      \"nested schema pruning on data condition\",\n      // The number of records is not recomputed when using DVs\n      \"delete throws error if number of records increases\",\n      \"delete logs error if number of records are missing in stats\"\n  )\n\n  // This works correctly with DVs, but fails in classic DELETE.\n  override def testSuperSetColsTempView(): Unit = {\n    testComplexTempViews(\"superset cols\")(\n      text = \"SELECT key, value, 1 FROM tab\",\n      expectResult = Row(0, 3, 1) :: Nil)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeleteScalaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.{DeltaExcludedTestMixin, DeltaSQLCommandTest}\n\nimport org.apache.spark.sql.{functions, Row}\n\ntrait DeleteScalaMixin\n  extends DeleteBaseMixin\n  with DeltaSQLCommandTest\n  with DeltaDMLTestUtilsPathBased\n  with DeltaExcludedTestMixin {\n\n  override protected def executeDelete(target: String, where: String = null): Unit = {\n    val deltaTable: io.delta.tables.DeltaTable =\n      DeltaTestUtils.getDeltaTableForIdentifierOrPath(\n        spark,\n        DeltaTestUtils.getTableIdentifierOrPath(target))\n\n    if (where != null) {\n      deltaTable.delete(where)\n    } else {\n      deltaTable.delete()\n    }\n  }\n}\n\ntrait DeleteScalaTests extends DeleteScalaMixin {\n  import testImplicits._\n\n  test(\"delete usage test - without condition\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.delete()\n    checkAnswer(readDeltaTable(tempPath), Nil)\n  }\n\n  test(\"delete usage test - with condition\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.delete(\"key = 1 or key = 2\")\n    checkAnswer(readDeltaTable(tempPath), Row(3, 30) :: Row(4, 40) :: Nil)\n  }\n\n  test(\"delete usage test - with Column condition\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.delete(functions.expr(\"key = 1 or key = 2\"))\n    checkAnswer(readDeltaTable(tempPath), Row(3, 30) :: Row(4, 40) :: Nil)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeleteSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.{SparkThrowable, SparkUnsupportedOperationException}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.execution.FileSourceScanExec\nimport org.apache.spark.sql.functions.{lit, struct}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\n\ntrait DeleteBaseMixin\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaDMLTestUtils\n  with DeltaTestUtilsForTempViews {\n\n  import testImplicits._\n\n  protected def executeDelete(target: String, where: String = null): Unit\n\n  protected def checkDelete(\n      condition: Option[String],\n      expectedResults: Seq[Row],\n      tableName: Option[String] = None): Unit = {\n    val target = tableName.getOrElse(tableSQLIdentifier)\n    executeDelete(target = target, where = condition.orNull)\n    checkAnswer(readDeltaTableByIdentifier(target), expectedResults)\n  }\n\n  protected def testInvalidTempViews(name: String)(\n      text: String,\n      expectedErrorMsgForSQLTempView: String = null,\n      expectedErrorMsgForDataSetTempView: String = null,\n      expectedErrorClassForSQLTempView: String = null,\n      expectedErrorClassForDataSetTempView: String = null): Unit = {\n    testWithTempView(s\"test delete on temp view - $name\") { isSQLTempView =>\n      withTable(\"tab\") {\n        Seq((0, 3), (1, 2)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        if (isSQLTempView) {\n          sql(s\"CREATE TEMP VIEW v AS $text\")\n        } else {\n          sql(text).createOrReplaceTempView(\"v\")\n        }\n        val ex = intercept[AnalysisException] {\n          executeDelete(\n            \"v\",\n            \"key >= 1 and value < 3\"\n          )\n        }\n        testErrorMessageAndClass(\n          isSQLTempView,\n          ex,\n          expectedErrorMsgForSQLTempView,\n          expectedErrorMsgForDataSetTempView,\n          expectedErrorClassForSQLTempView,\n          expectedErrorClassForDataSetTempView)\n      }\n    }\n  }\n\n  // Need to be able to override this, because it works in some configurations.\n  protected def testSuperSetColsTempView(): Unit = {\n    testInvalidTempViews(\"superset cols\")(\n      text = \"SELECT key, value, 1 FROM tab\",\n      // The analyzer can't tell whether the table originally had the extra column or not.\n      expectedErrorMsgForSQLTempView = \"Can't resolve column 1 in root\",\n      expectedErrorMsgForDataSetTempView = \"Can't resolve column 1 in root\"\n    )\n  }\n\n  protected def testComplexTempViews(name: String)(\n      text: String,\n      expectResult: Seq[Row]): Unit = {\n    testWithTempView(s\"test delete on temp view - $name\") { isSQLTempView =>\n        withTable(\"tab\") {\n          Seq((0, 3), (1, 2)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n          createTempViewFromSelect(text, isSQLTempView)\n          executeDelete(\n            \"v\",\n            \"key >= 1 and value < 3\"\n          )\n          checkAnswer(spark.read.format(\"delta\").table(\"v\"), expectResult)\n        }\n    }\n  }\n}\n\ntrait DeleteTempViewTests extends DeleteBaseMixin with DeltaDMLTestUtilsPathBased {\n  import testImplicits._\n\n  Seq(true, false).foreach { isPartitioned =>\n    val name = s\"test delete on temp view - basic - Partition=$isPartitioned\"\n    testWithTempView(name) { isSQLTempView =>\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n      createTempViewFromTable(tableSQLIdentifier, isSQLTempView)\n        checkDelete(\n          condition = Some(\"key <= 1\"),\n          expectedResults = Row(2, 2) :: Nil,\n          tableName = Some(\"v\"))\n    }\n  }\n\n  testInvalidTempViews(\"subset cols\")(\n    text = \"SELECT key FROM tab\",\n    expectedErrorClassForSQLTempView = \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n    expectedErrorClassForDataSetTempView = \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n  )\n\n  testSuperSetColsTempView()\n\n  testComplexTempViews(\"nontrivial projection\")(\n    text = \"SELECT value as key, key as value FROM tab\",\n    expectResult = Row(3, 0) :: Nil\n  )\n\n  testComplexTempViews(\"view with too many internal aliases\")(\n    text = \"SELECT * FROM (SELECT * FROM tab AS t1) AS t2\",\n    expectResult = Row(0, 3) :: Nil\n  )\n}\n\ntrait DeleteBaseTests extends DeleteBaseMixin {\n  import testImplicits._\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic case - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkDelete(condition = None, Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic case - delete from a Delta table - Partition=$isPartitioned\") {\n      withTable(\"deltaTable\") {\n        val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n        val input = Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n        append(input, partitions)\n\n        checkDelete(Some(\"value = 4 and key = 3\"),\n          Row(2, 2) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n        checkDelete(Some(\"value = 4 and key = 1\"),\n          Row(2, 2) :: Row(1, 1) :: Row(0, 3) :: Nil)\n        checkDelete(Some(\"value = 2 or key = 1\"),\n          Row(0, 3) :: Nil)\n        checkDelete(Some(\"key = 0 or value = 99\"), Nil)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic key columns - Partition=$isPartitioned\") {\n      val input = Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(input, partitions)\n\n      checkDelete(Some(\"key > 2\"), Row(2, 2) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n      checkDelete(Some(\"key < 2\"), Row(2, 2) :: Nil)\n      checkDelete(Some(\"key = 2\"), Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"where key columns - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkDelete(Some(\"key = 1\"), Row(2, 2) :: Row(0, 3) :: Nil)\n      checkDelete(Some(\"key = 2\"), Row(0, 3) :: Nil)\n      checkDelete(Some(\"key = 0\"), Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"where data columns - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkDelete(Some(\"value <= 2\"), Row(1, 4) :: Row(0, 3) :: Nil)\n      checkDelete(Some(\"value = 3\"), Row(1, 4) :: Nil)\n      checkDelete(Some(\"value != 0\"), Nil)\n    }\n  }\n\n  test(\"where data columns and partition columns\") {\n    val input = Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n    append(input, Seq(\"key\"))\n\n    checkDelete(Some(\"value = 4 and key = 3\"),\n      Row(2, 2) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n    checkDelete(Some(\"value = 4 and key = 1\"),\n      Row(2, 2) :: Row(1, 1) :: Row(0, 3) :: Nil)\n    checkDelete(Some(\"value = 2 or key = 1\"),\n      Row(0, 3) :: Nil)\n    checkDelete(Some(\"key = 0 or value = 99\"),\n      Nil)\n  }\n\n  Seq(true, false).foreach { skippingEnabled =>\n    Seq(true, false).foreach { isPartitioned =>\n      test(s\"data and partition columns - Partition=$isPartitioned Skipping=$skippingEnabled\") {\n        withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> skippingEnabled.toString) {\n          val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n          val input = Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n          append(input, partitions)\n\n          checkDelete(Some(\"value = 4 and key = 3\"),\n            Row(2, 2) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n          checkDelete(Some(\"value = 4 and key = 1\"),\n            Row(2, 2) :: Row(1, 1) :: Row(0, 3) :: Nil)\n          checkDelete(Some(\"value = 2 or key = 1\"),\n            Row(0, 3) :: Nil)\n          checkDelete(Some(\"key = 0 or value = 99\"),\n            Nil)\n        }\n      }\n    }\n  }\n\n  test(\"Negative case - non-Delta target\") {\n    writeTable(\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\")\n        .write\n        .mode(\"overwrite\")\n        .format(\"parquet\"),\n      tableSQLIdentifier)\n    intercept[SparkThrowable] {\n      executeDelete(target = tableSQLIdentifier)\n    } match {\n      // Thrown when running with path-based SQL\n      case e: DeltaAnalysisException if e.getCondition == \"DELTA_TABLE_NOT_FOUND\" =>\n        checkError(e, \"DELTA_TABLE_NOT_FOUND\",\n          parameters = Map(\"tableName\" -> tableSQLIdentifier.stripPrefix(\"delta.\")))\n      case e: DeltaAnalysisException if e.getCondition == \"DELTA_MISSING_TRANSACTION_LOG\" =>\n        checkErrorMatchPVals(e, \"DELTA_MISSING_TRANSACTION_LOG\",\n          parameters = Map(\"operation\" -> \"read from\", \"path\" -> \".*\", \"docLink\" -> \"https://.*\"))\n      // Thrown when running with path-based Scala API\n      case e: DeltaAnalysisException if e.getCondition == \"DELTA_MISSING_DELTA_TABLE\" =>\n        checkError(e, \"DELTA_MISSING_DELTA_TABLE\",\n          parameters = Map(\"tableName\" -> tableSQLIdentifier.stripPrefix(\"delta.\")))\n      // Thrown when running with name-based SQL\n      case e: AnalysisException =>\n        checkErrorMatchPVals(e, \"UNSUPPORTED_FEATURE.TABLE_OPERATION\",\n          parameters = Map(\n            \"tableName\" -> s\".*$tableSQLIdentifier.*\",\n            \"operation\" -> \"DELETE\"))\n    }\n  }\n\n  test(\"Negative case - non-deterministic condition\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    val e = intercept[AnalysisException] {\n      executeDelete(target = tableSQLIdentifier, where = \"rand() > 0.5\")\n    }.getMessage\n    assert(e.contains(\"nondeterministic expressions are only allowed in\") ||\n      e.contains(\"The operator expects a deterministic expression\"))\n  }\n\n  test(\"Negative case - DELETE the child directory\",\n      NameBasedAccessIncompatible) {\n    withTempPath { tempDir =>\n      val tempPath = tempDir.getCanonicalPath\n      val df = Seq((2, 2), (3, 2)).toDF(\"key\", \"value\")\n      df.write.format(\"delta\").partitionBy(\"key\").save(tempPath)\n\n      val e = intercept[AnalysisException] {\n        executeDelete(target = s\"delta.`$tempPath/key=2`\", where = \"value = 2\")\n      }.getMessage\n      assert(e.contains(\"Expect a full scan of Delta sources, but found a partial scan\"))\n    }\n  }\n\n  test(\"delete cached table by name\") {\n    withTable(\"cached_delta_table\") {\n      Seq((2, 2), (1, 4)).toDF(\"key\", \"value\")\n        .write.format(\"delta\").saveAsTable(\"cached_delta_table\")\n\n      spark.table(\"cached_delta_table\").cache()\n      spark.table(\"cached_delta_table\").collect()\n      executeDelete(target = \"cached_delta_table\", where = \"key = 2\")\n      checkAnswer(spark.table(\"cached_delta_table\"), Row(1, 4) :: Nil)\n    }\n  }\n\n  test(\"delete cached table\") {\n    append(Seq((2, 2), (1, 4)).toDF(\"key\", \"value\"))\n    readDeltaTableByIdentifier().cache()\n    readDeltaTableByIdentifier().collect()\n    executeDelete(tableSQLIdentifier, where = \"key = 2\")\n    checkAnswer(readDeltaTableByIdentifier(), Row(1, 4) :: Nil)\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"condition having current_date - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(\n        Seq((java.sql.Date.valueOf(\"1969-12-31\"), 2),\n          (java.sql.Date.valueOf(\"2099-12-31\"), 4))\n          .toDF(\"key\", \"value\"), partitions)\n\n      checkDelete(Some(\"CURRENT_DATE > key\"),\n        Row(java.sql.Date.valueOf(\"2099-12-31\"), 4) :: Nil)\n      checkDelete(Some(\"CURRENT_DATE <= key\"), Nil)\n    }\n  }\n\n  test(\"condition having current_timestamp - Partition by Timestamp\") {\n    append(\n      Seq((java.sql.Timestamp.valueOf(\"2012-12-31 16:00:10.011\"), 2),\n        (java.sql.Timestamp.valueOf(\"2099-12-31 16:00:10.011\"), 4))\n        .toDF(\"key\", \"value\"), Seq(\"key\"))\n\n    checkDelete(Some(\"CURRENT_TIMESTAMP > key\"),\n      Row(java.sql.Timestamp.valueOf(\"2099-12-31 16:00:10.011\"), 4) :: Nil)\n    checkDelete(Some(\"CURRENT_TIMESTAMP <= key\"), Nil)\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"foldable condition - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      val allRows = Row(2, 2) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil\n\n      checkDelete(Some(\"false\"), allRows)\n      checkDelete(Some(\"1 <> 1\"), allRows)\n      checkDelete(Some(\"1 > null\"), allRows)\n      checkDelete(Some(\"true\"), Nil)\n      checkDelete(Some(\"1 = 1\"), Nil)\n    }\n  }\n\n  test(\"SC-12232: should not delete the rows where condition evaluates to null\") {\n    append(Seq((\"a\", null), (\"b\", null), (\"c\", \"v\"), (\"d\", \"vv\")).toDF(\"key\", \"value\").coalesce(1))\n\n    // \"null = null\" evaluates to null\n    checkDelete(Some(\"value = null\"),\n      Row(\"a\", null) :: Row(\"b\", null) :: Row(\"c\", \"v\") :: Row(\"d\", \"vv\") :: Nil)\n\n    // these expressions evaluate to null when value is null\n    checkDelete(Some(\"value = 'v'\"),\n      Row(\"a\", null) :: Row(\"b\", null) :: Row(\"d\", \"vv\") :: Nil)\n    checkDelete(Some(\"value <> 'v'\"),\n      Row(\"a\", null) :: Row(\"b\", null) :: Nil)\n  }\n\n  test(\"SC-12232: delete rows with null values using isNull\") {\n    append(Seq((\"a\", null), (\"b\", null), (\"c\", \"v\"), (\"d\", \"vv\")).toDF(\"key\", \"value\").coalesce(1))\n\n    // when value is null, this expression evaluates to true\n    checkDelete(Some(\"value is null\"),\n      Row(\"c\", \"v\") :: Row(\"d\", \"vv\") :: Nil)\n  }\n\n  test(\"SC-12232: delete rows with null values using EqualNullSafe\") {\n    append(Seq((\"a\", null), (\"b\", null), (\"c\", \"v\"), (\"d\", \"vv\")).toDF(\"key\", \"value\").coalesce(1))\n\n    // when value is null, this expression evaluates to true\n    checkDelete(Some(\"value <=> null\"),\n      Row(\"c\", \"v\") :: Row(\"d\", \"vv\") :: Nil)\n  }\n\n  test(\"do not support subquery test\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"c\", \"d\").createOrReplaceTempView(\"source\")\n\n    // basic subquery\n    val e0 = intercept[AnalysisException] {\n      executeDelete(target = tableSQLIdentifier, \"key < (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e0.contains(\"Subqueries are not supported\"))\n\n    // subquery with EXISTS\n    val e1 = intercept[AnalysisException] {\n      executeDelete(target = tableSQLIdentifier, \"EXISTS (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e1.contains(\"Subqueries are not supported\"))\n\n    // subquery with NOT EXISTS\n    val e2 = intercept[AnalysisException] {\n      executeDelete(target = tableSQLIdentifier, \"NOT EXISTS (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e2.contains(\"Subqueries are not supported\"))\n\n    // subquery with IN\n    val e3 = intercept[AnalysisException] {\n      executeDelete(target = tableSQLIdentifier, \"key IN (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e3.contains(\"Subqueries are not supported\"))\n\n    // subquery with NOT IN\n    val e4 = intercept[AnalysisException] {\n      executeDelete(target = tableSQLIdentifier, \"key NOT IN (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e4.contains(\"Subqueries are not supported\"))\n  }\n\n  test(\"schema pruning on data condition\") {\n    val input = Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n    append(input, Nil)\n    // Start from a cached snapshot state\n    deltaLog.update().stateDF\n\n    val executedPlans = DeltaTestUtils.withPhysicalPlansCaptured(spark) {\n      checkDelete(Some(\"key = 2\"),\n        Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n    }\n\n    val scans = executedPlans.flatMap(_.collect {\n      case f: FileSourceScanExec => f\n    })\n\n    // The first scan is for finding files to delete. We only are matching against the key\n    // so that should be the only field in the schema\n    assert(scans.head.schema.findNestedField(Seq(\"key\")).nonEmpty)\n    assert(scans.head.schema.findNestedField(Seq(\"value\")).isEmpty)\n  }\n\n\n  test(\"nested schema pruning on data condition\") {\n    val input = Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n      .select(struct(\"key\", \"value\").alias(\"nested\"))\n    append(input, Nil)\n    // Start from a cached snapshot state\n    deltaLog.update().stateDF\n\n    val executedPlans = DeltaTestUtils.withPhysicalPlansCaptured(spark) {\n      checkDelete(Some(\"nested.key = 2\"),\n        Row(Row(1, 4)) :: Row(Row(1, 1)) :: Row(Row(0, 3)) :: Nil)\n    }\n\n    val scans = executedPlans.flatMap(_.collect {\n      case f: FileSourceScanExec => f\n    })\n\n    assert(scans.head.schema == StructType.fromDDL(\"nested STRUCT<key: int>\"))\n  }\n\n  /**\n   * @param function the unsupported function.\n   * @param functionType The type of the unsupported expression to be tested.\n   * @param data the data in the table.\n   * @param where the where clause containing the unsupported expression.\n   * @param expectException whether an exception is expected to be thrown\n   * @param customErrorRegex customized error regex.\n   */\n  private def testUnsupportedExpression(\n      function: String,\n      functionType: String,\n      data: => DataFrame,\n      where: String,\n      expectException: Boolean,\n      customErrorRegex: Option[String] = None) {\n    test(s\"$functionType functions in delete - expect exception: $expectException\") {\n      withTable(\"deltaTable\") {\n        data.write.format(\"delta\").saveAsTable(\"deltaTable\")\n\n        val expectedErrorRegex = \"(?s).*(?i)unsupported.*(?i).*Invalid expressions.*\"\n\n        var catchException = true\n\n        var errorRegex = if (functionType.equals(\"Generate\")) {\n          \".*Subqueries are not supported in the DELETE.*\"\n        } else customErrorRegex.getOrElse(expectedErrorRegex)\n\n\n        if (catchException) {\n          val dataBeforeException = spark.read.format(\"delta\").table(\"deltaTable\").collect()\n          val e = intercept[Exception] {\n            executeDelete(target = \"deltaTable\", where = where)\n          }\n          val message = if (e.getCause != null) {\n            e.getCause.getMessage\n          } else e.getMessage\n          assert(message.matches(errorRegex))\n          checkAnswer(spark.read.format(\"delta\").table(\"deltaTable\"), dataBeforeException)\n        } else {\n          executeDelete(target = \"deltaTable\", where = where)\n        }\n      }\n    }\n  }\n\n  testUnsupportedExpression(\n    function = \"row_number\",\n    functionType = \"Window\",\n    data = Seq((2, 2), (1, 4)).toDF(\"key\", \"value\"),\n    where = \"row_number() over (order by value) > 1\",\n    expectException = true\n  )\n\n  testUnsupportedExpression(\n    function = \"max\",\n    functionType = \"Aggregate\",\n    data = Seq((2, 2), (1, 4)).toDF(\"key\", \"value\"),\n    where = \"key > max(value)\",\n    expectException = true\n  )\n\n  // Explode functions are supported in where if only one row generated.\n  testUnsupportedExpression(\n    function = \"explode\",\n    functionType = \"Generate\",\n    data = Seq((2, List(2))).toDF(\"key\", \"value\"),\n    where = \"key = (select explode(value) from deltaTable)\",\n    expectException = false // generate only one row, no exception.\n  )\n\n  // Explode functions are supported in where but if there's more than one row generated,\n  // it will throw an exception.\n  testUnsupportedExpression(\n    function = \"explode\",\n    functionType = \"Generate\",\n    data = Seq((2, List(2)), (1, List(4, 5))).toDF(\"key\", \"value\"),\n    where = \"key = (select explode(value) from deltaTable)\",\n    expectException = true, // generate more than one row. Exception expected.\n    customErrorRegex =\n      Some(\".*More than one row returned by a subquery used as an expression(?s).*\")\n  )\n\n  test(\"Variant type\") {\n    val dstDf = sql(\n      \"\"\"SELECT parse_json(cast(id as string)) v, id i\n      FROM range(3)\"\"\")\n    append(dstDf)\n\n    executeDelete(target = tableSQLIdentifier, where = \"to_json(v) = '1'\")\n\n    checkAnswer(readDeltaTableByIdentifier().selectExpr(\"i\", \"to_json(v)\"),\n      Seq(Row(0, \"0\"), Row(2, \"2\")))\n  }\n\n  test(\"delete on partitioned table with special chars\") {\n    val partValue = \"part%one\"\n    append(\n      spark.range(0, 3, 1, 1).toDF(\"key\").withColumn(\"value\", lit(partValue)),\n      partitionBy = Seq(\"value\"))\n    checkDelete(\n      condition = Some(s\"value = '$partValue' and key = 1\"),\n      expectedResults = Row(0, partValue) :: Row(2, partValue) :: Nil)\n    checkDelete(\n      condition = Some(s\"value = '$partValue' and key = 2\"),\n      expectedResults = Row(0, partValue) :: Nil)\n    checkDelete(\n      condition = Some(s\"value = '$partValue'\"),\n      expectedResults = Nil)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeletionVectorsTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.DeltaOperations.Truncate\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, DeletionVectorDescriptor, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.{AlterTableDropFeatureDeltaCommand, DeletionVectorUtils}\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.PathWithFileSystem\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.internal.config.ConfigEntry\nimport org.apache.spark.sql.{DataFrame, QueryTest, RuntimeConfig, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait MergePersistentDVDisabled extends SharedSparkSession {\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key, \"false\")\n}\n\ntrait PersistentDVDisabled extends SharedSparkSession {\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"false\")\n}\n\ntrait PersistentDVEnabled extends DeletionVectorsTestUtils {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectorsInNewTables(spark.conf)\n  }\n}\n\ntrait PredicatePushdownDisabled extends SharedSparkSession {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key, \"false\")\n  }\n}\n\ntrait PredicatePushdownEnabled extends SharedSparkSession {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key, \"true\")\n  }\n}\n\n/** Collection of test utilities related with persistent Deletion Vectors. */\ntrait DeletionVectorsTestUtils extends QueryTest with SharedSparkSession with DeltaSQLTestUtils {\n\n  def enableDeletionVectors(\n      spark: SparkSession,\n      delete: Boolean = false,\n      update: Boolean = false,\n      merge: Boolean = false): Unit = {\n    val global = delete || update || merge\n    spark.conf\n      .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, global.toString)\n    spark.conf.set(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key, delete.toString)\n    spark.conf.set(DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS.key, update.toString)\n    spark.conf.set(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key, merge.toString)\n  }\n\n  /** Disable persistent deletion vectors in new tables and all supported DML commands. */\n  def disableDeletionVectors(conf: RuntimeConfig): Unit = {\n    conf.set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, false.toString)\n    conf.set(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key, false.toString)\n    conf.set(DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS.key, false.toString)\n  }\n\n  def enableDeletionVectorsForAllSupportedOperations(spark: SparkSession): Unit =\n    enableDeletionVectors(spark, delete = true, update = true)\n\n  def deletionVectorsEnabledInCommand(\n      sparkSession: SparkSession,\n      deltaLog: DeltaLog,\n      dmlConfig: ConfigEntry[Boolean]): Boolean =\n    DeletionVectorUtils.deletionVectorsWritable(deltaLog.update()) &&\n      sparkSession.sessionState.conf.getConf(dmlConfig)\n\n  /** Whether persistent Deletion Vectors are enabled in MERGE command. */\n  def deletionVectorsEnabledInMerge(spark: SparkSession, deltaLog: DeltaLog): Boolean = {\n    deletionVectorsEnabledInCommand(spark, deltaLog,\n      DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS)\n  }\n\n  /** Whether persistent Deletion Vectors are enabled in UPDATE command. */\n  def deletionVectorsEnabledInUpdate(spark: SparkSession, deltaLog: DeltaLog): Boolean =\n    deletionVectorsEnabledInCommand(spark, deltaLog,\n      DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS)\n\n  /** Whether persistent Deletion Vectors are enabled in DELETE command. */\n  def deletionVectorsEnabledInDelete(spark: SparkSession, deltaLog: DeltaLog): Boolean = {\n    deletionVectorsEnabledInCommand(\n      spark, deltaLog, DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS)\n  }\n\n  def testWithDVs(testName: String, testTags: org.scalatest.Tag*)(thunk: => Unit): Unit = {\n    test(testName, testTags : _*) {\n      withDeletionVectorsEnabled() {\n        thunk\n      }\n    }\n  }\n\n  /** Run a thunk with Deletion Vectors enabled/disabled. */\n  def withDeletionVectorsEnabled(enabled: Boolean = true)(thunk: => Unit): Unit = {\n    val enabledStr = enabled.toString\n    withSQLConf(\n      DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> enabledStr,\n      DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> enabledStr,\n      DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS.key -> enabledStr,\n      DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key -> enabledStr) {\n      thunk\n    }\n  }\n\n  def dropDVTableFeature(\n      spark: SparkSession,\n      log: DeltaLog,\n      truncateHistory: Boolean): Unit =\n    AlterTableDropFeatureDeltaCommand(\n      DeltaTableV2(spark, log.dataPath),\n      DeletionVectorsTableFeature.name,\n      truncateHistory = truncateHistory).run(spark)\n\n  /** Helper to run 'fn' with a temporary Delta table. */\n  def withTempDeltaTable(\n      dataDF: DataFrame,\n      partitionBy: Seq[String] = Seq.empty,\n      enableDVs: Boolean = true,\n      conf: Seq[(String, String)] = Nil,\n      createNameBasedTable: Boolean = false)\n      (fn: (() => io.delta.tables.DeltaTable, DeltaLog) => Unit): Unit = {\n    def createTable(tableNameOpt: Option[String], tablePathOpt: Option[Path]): Unit = {\n      assert((tableNameOpt.isDefined && tablePathOpt.isEmpty) ||\n        (tableNameOpt.isEmpty && tablePathOpt.isDefined))\n      withSQLConf(conf: _*) {\n        val df = dataDF.write\n          .option(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, enableDVs.toString)\n          .partitionBy(partitionBy: _*)\n          .format(\"delta\")\n        (tableNameOpt, tablePathOpt) match {\n          case (Some(tableName), None) => df.saveAsTable(tableName)\n          case (None, Some(tablePath)) => df.save(tablePath.toString)\n        }\n      }\n      // DeltaTable hangs on to the DataFrame it is created with for the entire object lifetime.\n      // That means subsequent `targetTable.toDF` calls will return the same snapshot.\n      // The DV tests are generally written assuming `targetTable.toDF` would return a new snapshot.\n      // So create a function here instead of an instance, so `targetTable().toDF`\n      // will actually provide a new snapshot.\n      val targetTable = (tableNameOpt, tablePathOpt) match {\n        case (Some(tableName), None) => () => io.delta.tables.DeltaTable.forName(tableName)\n        case (None, Some(tablePath)) => () => io.delta.tables.DeltaTable.forPath(tablePath.toString)\n      }\n\n      val targetLog = (tableNameOpt, tablePathOpt) match {\n        case (Some(tableName), None) => DeltaLog.forTable(spark, TableIdentifier(tableName))\n        case (None, Some(tablePath)) => DeltaLog.forTable(spark, tablePath)\n      }\n      fn(targetTable, targetLog)\n    }\n    if (createNameBasedTable) {\n      withTempTable(createTable = false) { tableName =>\n        createTable(tableNameOpt = Some(tableName), tablePathOpt = None)\n      }\n    } else {\n      withTempPath { path =>\n        val tablePath = new Path(path.getAbsolutePath)\n        createTable(tableNameOpt = None, tablePathOpt = Some(tablePath))\n      }\n    }\n  }\n\n  /** Create a temp path which contains special characters. */\n  override def withTempPath(f: File => Unit): Unit = {\n    super.withTempPath(prefix = \"s p a r k %2a\")(f)\n  }\n\n  /** Create a temp path which contains special characters. */\n  override protected def withTempDir(f: File => Unit): Unit = {\n    super.withTempDir(prefix = \"s p a r k %2a\")(f)\n  }\n\n  /** Helper that verifies whether a defined number of DVs exist */\n  def verifyDVsExist(targetLog: DeltaLog, filesWithDVsSize: Int): Unit = {\n    val filesWithDVs = getFilesWithDeletionVectors(targetLog)\n    assert(filesWithDVs.size === filesWithDVsSize)\n    assertDeletionVectorsExist(targetLog, filesWithDVs)\n  }\n\n  /** Returns all [[AddFile]] actions of a Delta table that contain Deletion Vectors. */\n  def getFilesWithDeletionVectors(log: DeltaLog): Seq[AddFile] =\n    log.update().allFiles.collect().filter(_.deletionVector != null).toSeq\n\n  /** Lists the Deletion Vectors files of a table. */\n  def listDeletionVectors(log: DeltaLog): Seq[File] = {\n    val dir = new File(log.dataPath.toUri.getPath)\n    dir.listFiles().filter(_.getName.startsWith(\n      DeletionVectorDescriptor.DELETION_VECTOR_FILE_NAME_CORE))\n  }\n\n  /** Helper to check that the Deletion Vectors of the provided file actions exist on disk. */\n  def assertDeletionVectorsExist(log: DeltaLog, filesWithDVs: Seq[AddFile]): Unit = {\n    val tablePath = new Path(log.dataPath.toUri.getPath)\n    for (file <- filesWithDVs) {\n      val dv = file.deletionVector\n      assert(dv != null)\n      assert(dv.isOnDisk && !dv.isInline)\n      assert(dv.offset.isDefined)\n\n      // Check that DV exists.\n      val dvPath = dv.absolutePath(tablePath)\n      assert(new File(dvPath.toString).exists(), s\"DV not found $dvPath\")\n\n      // Check that cardinality is correct.\n      val bitmap = newDVStore.read(dvPath, dv.offset.get, dv.sizeInBytes)\n      assert(dv.cardinality === bitmap.cardinality)\n    }\n  }\n\n  /** Enable persistent deletion vectors in new Delta tables. */\n  def enableDeletionVectorsInNewTables(conf: RuntimeConfig): Unit =\n    conf.set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"true\")\n\n  /** Enable persistent Deletion Vectors in a Delta table with table path. */\n  def enableDeletionVectorsInTable(tablePath: Path, enable: Boolean): Unit =\n    enableDeletionVectorsInTable(tableName = s\"delta.`$tablePath`\", enable)\n\n  /** Enable persistent Deletion Vectors in a Delta table with table name. */\n  def enableDeletionVectorsInTable(tableName: String, enable: Boolean): Unit =\n    spark.sql(\n      s\"\"\"ALTER TABLE $tableName\n         |SET TBLPROPERTIES ('${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = '$enable')\n         |\"\"\".stripMargin)\n\n  /** Enable persistent Deletion Vectors in a Delta table. */\n  def enableDeletionVectorsInTable(deltaLog: DeltaLog, enable: Boolean = true): Unit =\n    enableDeletionVectorsInTable(deltaLog.dataPath, enable)\n\n  /** Enable persistent deletion vectors in new tables and DELETE DML commands. */\n  def enableDeletionVectors(conf: RuntimeConfig): Unit = {\n    enableDeletionVectorsInNewTables(conf)\n    conf.set(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key, \"true\")\n  }\n\n  // ======== HELPER METHODS TO WRITE DVs ==========\n  /** Helper method to remove the specified rows in the given file using DVs */\n  protected def removeRowsFromFileUsingDV(\n      log: DeltaLog,\n      addFile: AddFile,\n      rowIds: Seq[Long]): Seq[Action] = {\n    val dv = RoaringBitmapArray(rowIds: _*)\n    writeFileWithDV(log, addFile, dv)\n  }\n\n  /** Utility method to remove a ratio of rows from the given file */\n  protected def deleteRows(\n      log: DeltaLog, file: AddFile, approxPhyRows: Long, ratioOfRowsToDelete: Double): Unit = {\n    val numRowsToDelete =\n      Math.ceil(ratioOfRowsToDelete * file.numPhysicalRecords.getOrElse(approxPhyRows)).toInt\n    removeRowsFromFile(log, file, Seq.range(0, numRowsToDelete))\n  }\n\n  /** Utility method to remove the given rows from the given file using DVs */\n  protected def removeRowsFromFile(\n      log: DeltaLog, addFile: AddFile, rowIndexesToRemove: Seq[Long]): Unit = {\n    val txn = log.startTransaction()\n    val actions = removeRowsFromFileUsingDV(log, addFile, rowIndexesToRemove)\n    txn.commit(actions, Truncate())\n  }\n\n  protected def getFileActionsInLastVersion(log: DeltaLog): (Seq[AddFile], Seq[RemoveFile]) = {\n    val version = log.update().version\n    val allFiles = log.getChanges(version).toSeq.head._2\n    val add = allFiles.collect { case a: AddFile => a }\n    val remove = allFiles.collect { case r: RemoveFile => r }\n    (add, remove)\n  }\n\n  protected def serializeRoaringBitmapArrayWithDefaultFormat(\n      dv: RoaringBitmapArray): Array[Byte] = {\n    val serializationFormat = RoaringBitmapArrayFormat.Portable\n    dv.serializeAsByteArray(serializationFormat)\n  }\n\n  /**\n   * Produce a new [[AddFile]] that will store `dv` in the log using default settings for choosing\n   * inline or on-disk storage.\n   *\n   * Also returns the corresponding [[RemoveFile]] action for `currentFile`.\n   *\n   * TODO: Always on-disk for now. Inline support comes later.\n   */\n  protected def writeFileWithDV(\n      log: DeltaLog,\n      currentFile: AddFile,\n      dv: RoaringBitmapArray): Seq[Action] = {\n    writeFileWithDVOnDisk(log, currentFile, dv)\n  }\n\n  /** Name of the partition column used by [[createTestDF()]]. */\n  val PARTITION_COL = \"partitionColumn\"\n\n  def createTestDF(\n    start: Long,\n    end: Long,\n    numFiles: Int,\n    partitionColumn: Option[Int] = None): DataFrame = {\n    val df = spark.range(start, end, 1, numFiles).withColumn(\"v\", col(\"id\"))\n    if (partitionColumn.isEmpty) {\n      df\n    } else {\n      df.withColumn(PARTITION_COL, lit(partitionColumn.get))\n    }\n  }\n\n  /**\n   * Produce a new [[AddFile]] that will reference the `dv` in the log while storing it on-disk.\n   *\n   * Also returns the corresponding [[RemoveFile]] action for `currentFile`.\n   */\n  protected def writeFileWithDVOnDisk(\n      log: DeltaLog,\n      currentFile: AddFile,\n      dv: RoaringBitmapArray): Seq[Action] = writeFilesWithDVsOnDisk(log, Seq((currentFile, dv)))\n\n  protected def withDVWriter[T](\n      log: DeltaLog,\n      dvFileID: UUID)(fn: DeletionVectorStore.Writer => T): T = {\n    val dvStore = newDVStore\n    // scalastyle:off deltahadoopconfiguration\n    val conf = spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n    val tableWithFS = PathWithFileSystem.withConf(log.dataPath, conf)\n    val dvPath =\n      DeletionVectorStore.assembleDeletionVectorPathWithFileSystem(tableWithFS, dvFileID)\n    val writer = dvStore.createWriter(dvPath)\n    try {\n      fn(writer)\n    } finally {\n      writer.close()\n    }\n  }\n\n  /**\n   * Produce new [[AddFile]] actions that will reference associated DVs in the log while storing\n   * all DVs in the same file on-disk.\n   *\n   * Also returns the corresponding [[RemoveFile]] actions for the original file entries.\n   */\n  protected def writeFilesWithDVsOnDisk(\n      log: DeltaLog,\n      filesWithDVs: Seq[(AddFile, RoaringBitmapArray)]): Seq[Action] = {\n    val dvFileId = UUID.randomUUID()\n    withDVWriter(log, dvFileId) { writer =>\n      filesWithDVs.flatMap { case (currentFile, dv) =>\n        val range = writer.write(serializeRoaringBitmapArrayWithDefaultFormat(dv))\n        val dvData = DeletionVectorDescriptor.onDiskWithRelativePath(\n          id = dvFileId,\n          sizeInBytes = range.length,\n          cardinality = dv.cardinality,\n          offset = Some(range.offset))\n        val (add, remove) = currentFile.removeRows(\n          dvData,\n          updateStats = true\n        )\n        Seq(add, remove)\n      }\n    }\n  }\n\n  /**\n   * Removes the `numRowsToRemovePerFile` from each file via DV.\n   * Returns the total number of rows removed.\n   */\n  protected def removeRowsFromAllFilesInLog(\n      log: DeltaLog,\n      numRowsToRemovePerFile: Long): Long = {\n    var numFiles: Option[Int] = None\n    // This is needed to make the manual commit work correctly, since we are not actually\n    // running a command that produces metrics.\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"false\") {\n      val txn = log.startTransaction()\n      val allAddFiles = txn.snapshot.allFiles.collect()\n      numFiles = Some(allAddFiles.length)\n      val bitmap = RoaringBitmapArray(0L until numRowsToRemovePerFile: _*)\n      val actions = allAddFiles.flatMap { file =>\n        if (file.numPhysicalRecords.isDefined) {\n          // Only when stats are enabled. Can't check when stats are disabled\n          assert(file.numPhysicalRecords.get > numRowsToRemovePerFile)\n        }\n        writeFileWithDV(log, file, bitmap)\n      }\n      txn.commit(actions, DeltaOperations.Delete(predicate = Seq.empty))\n    }\n    numFiles.get * numRowsToRemovePerFile\n  }\n\n  def newDVStore(): DeletionVectorStore = {\n    // scalastyle:off deltahadoopconfiguration\n    DeletionVectorStore.createInstance(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  /**\n   * Updates an [[AddFile]] with a [[DeletionVectorDescriptor]].\n   */\n  protected def updateFileDV(\n      addFile: AddFile,\n      dvDescriptor: DeletionVectorDescriptor): (AddFile, RemoveFile) = {\n    addFile.removeRows(\n      dvDescriptor,\n      updateStats = true\n    )\n  }\n\n  /** Delete the DV file in the given [[AddFile]]. Assumes the [[AddFile]] has a valid DV. */\n  protected def deleteDVFile(tablePath: String, addFile: AddFile): Unit = {\n    assert(addFile.deletionVector != null)\n    val dvPath = addFile.deletionVector.absolutePath(new Path(tablePath))\n    FileUtils.delete(new File(dvPath.toString))\n  }\n\n  /**\n   * Creates a [[DeletionVectorDescriptor]] from an [[RoaringBitmapArray]]\n   */\n  protected def writeDV(\n      log: DeltaLog,\n      bitmapArray: RoaringBitmapArray): DeletionVectorDescriptor = {\n    val dvFileId = UUID.randomUUID()\n    withDVWriter(log, dvFileId) { writer =>\n      val range = writer.write(serializeRoaringBitmapArrayWithDefaultFormat(bitmapArray))\n      DeletionVectorDescriptor.onDiskWithRelativePath(\n        id = dvFileId,\n        sizeInBytes = range.length,\n        cardinality = bitmapArray.cardinality,\n        offset = Some(range.offset))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaAllFilesInCrcSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.TimeZone\nimport java.util.concurrent.TimeUnit\n\nimport scala.concurrent.duration.Duration\n\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.DeltaTestUtils.{collectUsageLogs, BOOLEAN_DOMAIN}\nimport org.apache.spark.sql.delta.concurrency._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ThreadUtils\n\nclass DeltaAllFilesInCrcSuite\n    extends QueryTest\n    with SharedSparkSession\n    with TransactionExecutionTestMixin\n    with DeltaSQLCommandTest\n    with PhaseLockingTestMixin {\n  protected override def sparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key, \"true\")\n    .set(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_ENABLED.key, \"true\")\n    // Set the threshold to a very high number so that this test suite continues to use all files\n    // from CRC.\n    .set(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_THRESHOLD_FILES.key, \"10000\")\n    // needed for DELTA_ALL_FILES_IN_CRC_ENABLED\n    .set(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key, \"true\")\n    .set(DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key, \"true\")\n    // Turn on verification by default in the tests\n    .set(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key, \"true\")\n    // Turn off force verification for non-UTC timezones by default in the tests\n    .set(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key,\n      \"false\")\n\n  private def setTimeZone(timeZone: String): Unit = {\n    spark.sql(s\"SET spark.sql.session.timeZone = $timeZone\")\n    TimeZone.setDefault(TimeZone.getTimeZone(timeZone))\n  }\n\n  /** Filter usage records for specific `opType` */\n  protected def filterUsageRecords(\n      usageRecords: Seq[UsageRecord],\n      opType: String): Seq[UsageRecord] = {\n    usageRecords.filter { r =>\n      r.tags.get(\"opType\").contains(opType) || r.opType.map(_.typeName).contains(opType)\n    }\n  }\n\n  /** Deletes all delta/crc/checkpoint files later that given `version` for the delta table */\n  private def deleteDeltaFilesLaterThanVersion(deltaLog: DeltaLog, version: Long): Unit = {\n    val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n    deltaLog.listFrom(version + 1).filter { f =>\n      FileNames.isDeltaFile(f) || FileNames.isChecksumFile(f) || FileNames.isCheckpointFile(f)\n    }.foreach(f => fs.delete(f.getPath, true))\n    DeltaLog.clearCache()\n    assert(DeltaLog.forTable(spark, deltaLog.dataPath).update().version === version)\n  }\n\n  test(\"allFiles are written to CRC and different threshold configs are respected\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n\n      // Helper method to perform a commit with 10 AddFile actions to the table.\n      def writeToTable(\n          version: Long, newFilesToWrite: Int, expectedFilesInCRCOption: Option[Long]): Unit = {\n        spark.range(start = 1, end = 100, step = 1, numPartitions = newFilesToWrite)\n          .toDF(\"c1\")\n          .withColumn(\"c2\", col(\"c1\")).withColumn(\"c3\", col(\"c1\"))\n          .write.format(\"delta\").mode(\"append\").save(path)\n        assert(deltaLog.update().version === version)\n        assert(deltaLog.snapshot.checksumOpt.get.allFiles.map(_.size) === expectedFilesInCRCOption)\n        assert(deltaLog.readChecksum(version).get.allFiles.map(_.size) === expectedFilesInCRCOption)\n      }\n\n      def deltaLog: DeltaLog = DeltaLog.forTable(spark, path)\n\n      withSQLConf(\n          DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_THRESHOLD_FILES.key -> \"55\",\n          DeltaSQLConf.DELTA_AUTO_COMPACT_ENABLED.key -> \"false\") {\n        // Commit-0: Add 10 new files to table. Total files (10) is less than threshold.\n        writeToTable(version = 0, newFilesToWrite = 10, expectedFilesInCRCOption = Some(10))\n\n        withSQLConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_ENABLED.key -> \"false\") {\n          // Commit-1: Add 10 more files to table. Total files (20) is less than threshold.\n          // Still these won't be written to CRC as the conf is explicitly disabled.\n          writeToTable(version = 1, newFilesToWrite = 10, expectedFilesInCRCOption = None)\n        }\n        // Commit-2: Add 20 more files to table. Total files (40) is less than threshold.\n        writeToTable(version = 2, newFilesToWrite = 20, expectedFilesInCRCOption = Some(40))\n        // Commit-3: Add 13 more files to table. Total files (53) is less than threshold.\n        writeToTable(version = 3, newFilesToWrite = 13, expectedFilesInCRCOption = Some(53))\n        // Commit-4: Add 7 more files to table. Total files (60) is greater than the threshold (55).\n        // So files won't be persisted to CRC.\n        writeToTable(version = 4, newFilesToWrite = 7, expectedFilesInCRCOption = None)\n\n        // Commit-5: Delete all rows except with value=1. After this step, very few files will\n        // remain in table, still they won't be persisted to CRC as previous version had more than\n        // 55 files. We write files to CRC if both previous commit and this commit has files <= 55.\n        sql(s\"DELETE FROM delta.`$path` WHERE c1 != 1\").collect()\n        assert(deltaLog.update().version === 5)\n        assert(deltaLog.snapshot.checksumOpt.get.allFiles === None)\n        val fileCountAfterDeleteCommand = deltaLog.snapshot.checksumOpt.get.numFiles\n        assert(fileCountAfterDeleteCommand < 55)\n\n        // Commit-6: Commit 1 new file again. Now previous-version also had < 55 files. This version\n        // also has < 55 files.\n        writeToTable(version = 6, newFilesToWrite = 1,\n          expectedFilesInCRCOption = Some(fileCountAfterDeleteCommand + 1))\n\n        withSQLConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_THRESHOLD_INDEXED_COLS.key -> \"2\") {\n          // Table collects stats on 3 cols (col1/col2/col3) which is more than threshold.\n          // So optimization should be disabled by default.\n          writeToTable(version = 7, newFilesToWrite = 1, expectedFilesInCRCOption = None)\n        }\n\n        writeToTable(version = 8, newFilesToWrite = 1,\n          expectedFilesInCRCOption = Some(fileCountAfterDeleteCommand + 3))\n\n        // Commit-7: Delete all rows from table\n        sql(s\"DELETE FROM delta.`$path` WHERE c1 >= 0\").collect()\n        assert(deltaLog.update().version === 9)\n        assert(deltaLog.snapshot.checksumOpt.get.allFiles === Some(Seq()))\n      }\n    }\n  }\n\n\n  test(\"test all-files-in-crc verification failure also triggers and logs\" +\n    \" incremental-commit verification result\") {\n    withTempDir { tempDir =>\n      withSQLConf(\n          DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_THRESHOLD_FILES.key -> \"100\",\n          // Disable incremental commit force verifications in UTs - to mimic prod behavior\n          DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key -> \"false\",\n          // Enable all-files-in-crc verification mode\n          DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"true\",\n          DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n\n        val df = spark.range(2).coalesce(1).toDF()\n        df.write.format(\"delta\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(deltaLog.update().allFiles.collect().map(_.numPhysicalRecords).forall(_.isEmpty))\n\n        val records = Log4jUsageLogger.track {\n          val executor = ThreadUtils.newDaemonSingleThreadExecutor(threadName = \"executor-txn-A\")\n          try {\n            val query = s\"DELETE from delta.`${tempDir.getAbsolutePath}` WHERE id >= 0\"\n            val (observer, future) = runQueryWithObserver(name = \"A\", executor, query)\n            observer.phases.initialPhase.entryBarrier.unblock()\n            observer.phases.preparePhase.entryBarrier.unblock()\n            // Make sure that delete query has run the actual computation and has reached\n            // the 'prepare commit' phase. i.e. it just wants to commit.\n            busyWaitFor(observer.phases.preparePhase.hasLeft, timeout)\n            // Now delete and recreate the complete table.\n            deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n              .delete(deltaLog.dataPath, true)\n            spark.range(3, 4).coalesce(1).toDF().write.format(\"delta\").save(tempDir.toString())\n\n            // Allow the delete query to commit.\n            unblockCommit(observer)\n            waitForCommit(observer)\n            // Query will fail due to incremental-state-reconstruction validation failure.\n            // Note that this failure happens only in test. In prod, this would have just logged\n            // the incremental-state-reconstruction failure and query would have passed.\n            val ex = intercept[SparkException] { ThreadUtils.awaitResult(future, Duration.Inf) }\n            val message = ex.getMessage + \"\\n\" + ex.getCause.getMessage\n            assert(message.contains(\"Incremental state reconstruction validation failed\"))\n          } finally {\n            executor.shutdownNow()\n            executor.awaitTermination(timeout.toMillis, TimeUnit.MILLISECONDS)\n          }\n        }\n\n        // We will see all files in CRC verification failure.\n        // This will trigger the incremental commit verification which will fail.\n        assert(filterUsageRecords(records, \"delta.assertions.mismatchedAction\").size === 1)\n        val allFilesInCrcValidationFailureRecords =\n          filterUsageRecords(records, \"delta.allFilesInCrc.checksumMismatch.differentAllFiles\")\n        assert(allFilesInCrcValidationFailureRecords.size === 1)\n        val eventData =\n          JsonUtils.fromJson[Map[String, String]](allFilesInCrcValidationFailureRecords.head.blob)\n        assert(eventData(\"version\").toLong === 1L)\n        assert(eventData(\"mismatchWithStatsOnly\").toBoolean === false)\n        val expectedFilesCountFromCrc = 1L\n        assert(eventData(\"filesCountFromCrc\").toLong === expectedFilesCountFromCrc)\n        assert(eventData(\"filesCountFromStateReconstruction\").toLong ===\n          expectedFilesCountFromCrc + 1)\n        assert(eventData(\"incrementalCommitCrcValidationPassed\").toBoolean === false)\n        assert(eventData(\"errorForIncrementalCommitCrcValidation\").contains(\n          \"The metadata of your Delta table could not be recovered\"))\n      }\n    }\n  }\n\n  test(\"schema changing metadata operations should disable putting AddFile\" +\n      \" actions in crc but other metadata operations should not\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(1, 5).toDF(\"c1\").withColumn(\"c2\", col(\"c1\"))\n        .write.format(\"delta\").mode(\"append\").save(path)\n\n      def deltaLog: DeltaLog = DeltaLog.forTable(spark, path)\n      assert(deltaLog.update().checksumOpt.get.allFiles.nonEmpty)\n      sql(s\"ALTER TABLE delta.`$dir` CHANGE COLUMN c2 FIRST\")\n      assert(deltaLog.update().checksumOpt.get.allFiles.isEmpty)\n      sql(s\"ALTER TABLE delta.`$dir` SET TBLPROPERTIES ('a' = 'b')\")\n      assert(deltaLog.update().checksumOpt.get.allFiles.nonEmpty)\n    }\n  }\n\n  test(\"schema changing metadata operations on empty tables should not disable putting \" +\n    \"AddFile actions in crc\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      def deltaLog: DeltaLog = DeltaLog.forTable(spark, path)\n\n      def assertNoStateReconstructionTriggeredWhenPerfPackEnabled(f: => Unit): Unit = {\n        val oldSnapshot = deltaLog.update()\n        f\n        val newSnapshot = deltaLog.update()\n      }\n\n      withSQLConf(\n        // Disable test flags to make the behaviors verified in this test close to prod\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"false\",\n        DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key -> \"false\"\n      ) {\n        assertNoStateReconstructionTriggeredWhenPerfPackEnabled {\n          // Create a table with an empty schema so that the next write will change the schema\n          sql(s\"CREATE TABLE delta.`$path` USING delta LOCATION '$path'\")\n        }\n        assert(deltaLog.update().checksumOpt.get.allFiles == Option(Nil))\n\n        assertNoStateReconstructionTriggeredWhenPerfPackEnabled {\n          // Write zero files but update the table schema\n          spark.range(1, 5).filter(\"false\").write.format(\"delta\")\n            .option(\"mergeSchema\", \"true\").mode(\"append\").save(path)\n        }\n        // Make sure writing zero files still make a Delta commit so that this test is valid\n        assert(deltaLog.update().version == 1)\n        assert(deltaLog.update().checksumOpt.get.allFiles == Option(Nil))\n\n        assertNoStateReconstructionTriggeredWhenPerfPackEnabled {\n          // Write some files to the table\n          spark.range(1, 5).write.format(\"delta\").mode(\"append\").save(path)\n        }\n        assert(deltaLog.update().checksumOpt.get.allFiles.nonEmpty)\n        assert(deltaLog.update().checksumOpt.get.allFiles.get.size > 0)\n      }\n    }\n  }\n  private def withCrcVerificationEnabled(testCode: => Unit): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"true\") {\n      testCode\n    }\n  }\n\n  private def withCrcVerificationDisabled(testCode: => Unit): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"false\") {\n      testCode\n    }\n  }\n\n  private def write(deltaLog: DeltaLog, numFiles: Int, expectedFilesInCrc: Option[Int]): Unit = {\n    spark\n      .range(1, 100, 1, numPartitions = numFiles)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(deltaLog.dataPath.toString)\n    assert(deltaLog.snapshot.checksumOpt.get.allFiles.map(_.size) === expectedFilesInCrc)\n  }\n\n  private def corruptCRCNumFiles(deltaLog: DeltaLog, version: Int): Unit = {\n    val crc = deltaLog.readChecksum(version).get\n    assert(crc.allFiles.nonEmpty)\n    val filesInCrc = crc.allFiles.get\n\n    // Corrupt the CRC\n    val corruptedCrc = crc.copy(allFiles =\n      Some(filesInCrc.dropRight(1)), numFiles = crc.numFiles - 1)\n    val checksumFilePath = FileNames.checksumFile(deltaLog.logPath, version)\n    deltaLog.store.write(\n      checksumFilePath,\n      actions = Seq(JsonUtils.toJson(corruptedCrc)).toIterator,\n      overwrite = true,\n      hadoopConf = deltaLog.newDeltaHadoopConf())\n  }\n\n  private def corruptCRCAddFilesModificationTime(deltaLog: DeltaLog, version: Int): Unit = {\n    val crc = deltaLog.readChecksum(version).get\n    assert(crc.allFiles.nonEmpty)\n    val filesInCrc = crc.allFiles.get\n\n    // Corrupt the CRC\n    val corruptedCrc = crc.copy(allFiles = Some(filesInCrc.map(_.copy(modificationTime = 23))))\n    val checksumFilePath = FileNames.checksumFile(deltaLog.logPath, version)\n    deltaLog.store.write(\n      checksumFilePath,\n      actions = Seq(JsonUtils.toJson(corruptedCrc)).toIterator,\n      overwrite = true,\n      hadoopConf = deltaLog.newDeltaHadoopConf())\n  }\n\n  private def checkIfCrcModificationTimeCorrupted(\n     deltaLog: DeltaLog,\n     expectCorrupted: Boolean): Unit = {\n    val crc = deltaLog.readChecksum(deltaLog.update().version).get\n    assert(crc.allFiles.nonEmpty)\n    assert(crc.allFiles.get.count(_.modificationTime == 23L) > 0 === expectCorrupted)\n  }\n\n  test(\"allFilesInCRC verification with flag manipulation for UTC timezone\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n          \"true\") {\n      setTimeZone(\"UTC\")\n      withTempDir { dir =>\n        var deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n        // Commit 0: Initial write with verification enabled\n        withCrcVerificationEnabled {\n          write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(10))\n        }\n\n        // Corrupt the CRC at Version 0\n        corruptCRCAddFilesModificationTime(deltaLog, version = 0)\n        DeltaLog.clearCache()\n        deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n        // Commit 1: Write with verification flag off and Verify Incremental CRC at version 1 is\n        // also corrupted\n        withCrcVerificationDisabled {\n          write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(20))\n          checkIfCrcModificationTimeCorrupted(deltaLog, expectCorrupted = true)\n        }\n\n        // Commit 2: Write with verification flag on and it should fail because the AddFiles from\n        // base CRC at Version 1 are incorrect.\n        withCrcVerificationEnabled {\n          val usageRecords =\n            collectUsageLogs(\"delta.allFilesInCrc.checksumMismatch.differentAllFiles\") {\n              intercept[IllegalStateException] {\n                write(deltaLog, numFiles = 10, expectedFilesInCrc = None)\n              }\n            }\n          assert(usageRecords.size === 1)\n        }\n\n        // Commit 3: Write with verification flag on and it should pass since the base CRC is not\n        // corrupted anymore.\n        withCrcVerificationEnabled {\n          write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(40))\n          checkIfCrcModificationTimeCorrupted(deltaLog, expectCorrupted = false)\n        }\n      }\n    }\n  }\n\n  test(\"allFilesInCRC verification with flag manipulation for non-UTC timezone\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n          \"true\") {\n      setTimeZone(\"America/Los_Angeles\")\n      withTempDir { dir =>\n        var deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n        // Commit 0: Initial write with verification enabled\n        withCrcVerificationEnabled {\n          write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(10))\n        }\n\n        // Corrupt the CRC at Version 0\n        corruptCRCAddFilesModificationTime(deltaLog, version = 0)\n        DeltaLog.clearCache()\n        deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n        // Commit 1: Write with verification flag off and Verify Incremental CRC is still validated\n        // because timezone is non-UTC.\n        withCrcVerificationDisabled {\n          val usageRecords =\n            collectUsageLogs(\"delta.allFilesInCrc.checksumMismatch.differentAllFiles\") {\n              intercept[IllegalStateException] {\n                write(deltaLog, numFiles = 10, expectedFilesInCrc = None)\n              }\n            }\n          assert(usageRecords.size === 1)\n        }\n\n        // Commit 2: Write with verification flag on and it should pass since the base CRC is not\n        // corrupted anymore.\n        withCrcVerificationEnabled {\n          write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(30))\n          checkIfCrcModificationTimeCorrupted(deltaLog, expectCorrupted = false)\n        }\n      }\n    }\n  }\n\n  test(\"Verify aggregate stats are matched even when allFilesInCrc \" +\n      \"verification is disabled\") {\n    setTimeZone(\"UTC\")\n    withSQLConf(DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key -> \"false\") {\n      withCrcVerificationDisabled {\n        withTempDir { dir =>\n          var deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n          // Commit 0\n          write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(10))\n\n          // Corrupt the CRC at Version 0\n          corruptCRCNumFiles(deltaLog, version = 0)\n          DeltaLog.clearCache()\n          deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n          // Commit 1: Verify aggregate stats are matched even when verification is off\n          val usageRecords = collectUsageLogs(\"delta.allFilesInCrc.checksumMismatch.aggregated\") {\n            intercept[IllegalStateException] {\n              write(deltaLog, numFiles = 10, expectedFilesInCrc = None)\n            }\n          }\n          assert(usageRecords.size === 1)\n        }\n      }\n    }\n  }\n\n  test(\"allFilesInCRC validation during checkpoint must be opposite of per-commit \" +\n      \"validation\") {\n    withTempDir { dir =>\n      var deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      withCrcVerificationDisabled {\n        write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(10))\n        write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(20))\n        corruptCRCAddFilesModificationTime(deltaLog, version = 1)\n\n        DeltaLog.clearCache()\n        deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        deltaLog.update()\n\n        // Checkpoint should validate Checksum even when per-commit verification is disabled.\n        val usageRecords =\n          collectUsageLogs(\"delta.allFilesInCrc.checksumMismatch.differentAllFiles\") {\n            intercept[IllegalStateException] {\n              deltaLog.checkpoint()\n            }\n          }\n        assert(usageRecords.size === 1)\n        assert(usageRecords.head.blob.contains(\"\\\"context\\\":\" + \"\\\"triggeredFromCheckpoint\\\"\"),\n          usageRecords.head)\n\n        write(deltaLog, numFiles = 10, expectedFilesInCrc = Some(30))\n      }\n\n      // Checkpoint should not validate Checksum when per-commit verification is enabled.\n      withCrcVerificationEnabled {\n        val usageRecords =\n          collectUsageLogs(\"delta.allFilesInCrc.checksumMismatch.differentAllFiles\") {\n            deltaLog.checkpoint()\n          }\n        assert(usageRecords.isEmpty)\n      }\n    }\n  }\n\n  test(\"allFilesInCrcVerificationForceEnabled works as expected\") {\n    // Test with the non-UTC force verification conf enabled.\n    withSQLConf(\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n          \"true\") {\n      setTimeZone(\"UTC\")\n      assert(!Snapshot.allFilesInCrcVerificationForceEnabled(spark))\n      setTimeZone(\"America/Los_Angeles\")\n      assert(Snapshot.allFilesInCrcVerificationForceEnabled(spark))\n    }\n    // Test with the non-UTC force verification conf disabled.\n    withSQLConf(\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n          \"false\") {\n      assert(!Snapshot.allFilesInCrcVerificationForceEnabled(spark))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaAlterTableReplaceTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.functions.{array, col, map, struct}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{ArrayType, MapType, StructType}\n\ntrait DeltaAlterTableReplaceTests extends DeltaAlterTableTestBase {\n\n  import testImplicits._\n\n  ddlTest(\"REPLACE COLUMNS - add a comment\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  v1 int COMMENT 'a comment for v1',\n             |  v2 string COMMENT 'a comment for v2',\n             |  s STRUCT<\n             |    v1:int COMMENT 'a comment for s.v1',\n             |    v2:string COMMENT 'a comment for s.v2'> COMMENT 'a comment for s',\n             |  a ARRAY<STRUCT<\n             |    v1:int COMMENT 'a comment for a.v1',\n             |    v2:string COMMENT 'a comment for a.v2'>> COMMENT 'a comment for a',\n             |  m MAP<STRUCT<\n             |      v1:int COMMENT 'a comment for m.key.v1',\n             |      v2:string COMMENT 'a comment for m.key.v2'>,\n             |    STRUCT<\n             |      v1:int COMMENT 'a comment for m.value.v1',\n             |      v2:string COMMENT 'a comment for m.value.v2'>> COMMENT 'a comment for m'\n             |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      val expectedSchema = new StructType()\n        .add(\"v1\", \"integer\", true, \"a comment for v1\")\n        .add(\"v2\", \"string\", true, \"a comment for v2\")\n        .add(\"s\", new StructType()\n          .add(\"v1\", \"integer\", true, \"a comment for s.v1\")\n          .add(\"v2\", \"string\", true, \"a comment for s.v2\"), true, \"a comment for s\")\n        .add(\"a\", ArrayType(new StructType()\n          .add(\"v1\", \"integer\", true, \"a comment for a.v1\")\n          .add(\"v2\", \"string\", true, \"a comment for a.v2\")), true, \"a comment for a\")\n        .add(\"m\", MapType(\n          new StructType()\n            .add(\"v1\", \"integer\", true, \"a comment for m.key.v1\")\n            .add(\"v2\", \"string\", true, \"a comment for m.key.v2\"),\n          new StructType()\n            .add(\"v1\", \"integer\", true, \"a comment for m.value.v1\")\n            .add(\"v2\", \"string\", true, \"a comment for m.value.v2\")), true, \"a comment for m\")\n      assertEqual(snapshot.schema, expectedSchema)\n\n      implicit val ordering = Ordering.by[\n        (Int, String, (Int, String), Seq[(Int, String)], Map[(Int, String), (Int, String)]), Int] {\n        case (v1, _, _, _, _) => v1\n      }\n      checkDatasetUnorderly(\n        spark.table(tableName)\n          .as[(Int, String, (Int, String), Seq[(Int, String)], Map[(Int, String), (Int, String)])],\n        (1, \"a\", (1, \"a\"), Seq((1, \"a\")), Map((1, \"a\") -> ((1, \"a\")))),\n        (2, \"b\", (2, \"b\"), Seq((2, \"b\")), Map((2, \"b\") -> ((2, \"b\")))))\n\n      // REPLACE COLUMNS doesn't remove metadata.\n      sql(s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  v1 int,\n             |  v2 string,\n             |  s STRUCT<v1:int, v2:string>,\n             |  a ARRAY<STRUCT<v1:int, v2:string>>,\n             |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n             |)\"\"\".stripMargin)\n      assertEqual(deltaLog.snapshot.schema, expectedSchema)\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - reorder\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  m MAP<STRUCT<v2:string, v1:int>, STRUCT<v2:string, v1:int>>,\n             |  v2 string,\n             |  a ARRAY<STRUCT<v2:string, v1:int>>,\n             |  v1 int,\n             |  s STRUCT<v2:string, v1:int>\n             |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"m\", MapType(\n          new StructType().add(\"v2\", \"string\").add(\"v1\", \"integer\"),\n          new StructType().add(\"v2\", \"string\").add(\"v1\", \"integer\")))\n        .add(\"v2\", \"string\")\n        .add(\"a\", ArrayType(new StructType().add(\"v2\", \"string\").add(\"v1\", \"integer\")))\n        .add(\"v1\", \"integer\")\n        .add(\"s\", new StructType().add(\"v2\", \"string\").add(\"v1\", \"integer\")))\n\n      implicit val ordering = Ordering.by[\n        (Map[(String, Int), (String, Int)], String, Seq[(String, Int)], Int, (String, Int)), Int] {\n        case (_, _, _, v1, _) => v1\n      }\n      checkDatasetUnorderly(\n        spark.table(tableName)\n          .as[(Map[(String, Int), (String, Int)], String, Seq[(String, Int)], Int, (String, Int))],\n        (Map((\"a\", 1) -> ((\"a\", 1))), \"a\", Seq((\"a\", 1)), 1, (\"a\", 1)),\n        (Map((\"b\", 2) -> ((\"b\", 2))), \"b\", Seq((\"b\", 2)), 2, (\"b\", 2)))\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - add columns\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  v1 int,\n             |  v2 string,\n             |  v3 long,\n             |  s STRUCT<v1:int, v2:string, v3:long>,\n             |  a ARRAY<STRUCT<v1:int, v2:string, v3:long>>,\n             |  m MAP<STRUCT<v1:int, v2:string, v3:long>, STRUCT<v1:int, v2:string, v3:long>>\n             |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\")\n        .add(\"v2\", \"string\")\n        .add(\"v3\", \"long\")\n        .add(\"s\", new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\").add(\"v3\", \"long\"))\n        .add(\"a\", ArrayType(new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\").add(\"v3\", \"long\")))\n        .add(\"m\", MapType(\n          new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\").add(\"v3\", \"long\"),\n          new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\").add(\"v3\", \"long\"))))\n\n      implicit val ordering = Ordering.by[\n        (Int, String, Option[Long],\n          (Int, String, Option[Long]),\n          Seq[(Int, String, Option[Long])],\n          Map[(Int, String, Option[Long]), (Int, String, Option[Long])]), Int] {\n        case (v1, _, _, _, _, _) => v1\n      }\n      checkDatasetUnorderly(\n        spark.table(tableName).as[\n          (Int, String, Option[Long],\n            (Int, String, Option[Long]),\n            Seq[(Int, String, Option[Long])],\n            Map[(Int, String, Option[Long]), (Int, String, Option[Long])])],\n        (1, \"a\", None, (1, \"a\", None),\n          Seq((1, \"a\", None)), Map((1, \"a\", Option.empty[Long]) -> ((1, \"a\", None)))),\n        (2, \"b\", None, (2, \"b\", None),\n          Seq((2, \"b\", None)), Map((2, \"b\", Option.empty[Long]) -> ((2, \"b\", None)))))\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - special column names\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"x.x\", \"y.y\")\n      .withColumn(\"s.s\", struct(\"`x.x`\", \"`y.y`\"))\n      .withColumn(\"a.a\", array(\"`s.s`\"))\n      .withColumn(\"m.m\", map(col(\"`s.s`\"), col(\"`s.s`\")))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  `m.m` MAP<STRUCT<`y.y`:string, `x.x`:int>, STRUCT<`y.y`:string, `x.x`:int>>,\n             |  `y.y` string,\n             |  `a.a` ARRAY<STRUCT<`y.y`:string, `x.x`:int>>,\n             |  `x.x` int,\n             |  `s.s` STRUCT<`y.y`:string, `x.x`:int>\n             |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"m.m\", MapType(\n          new StructType().add(\"y.y\", \"string\").add(\"x.x\", \"integer\"),\n          new StructType().add(\"y.y\", \"string\").add(\"x.x\", \"integer\")))\n        .add(\"y.y\", \"string\")\n        .add(\"a.a\", ArrayType(new StructType().add(\"y.y\", \"string\").add(\"x.x\", \"integer\")))\n        .add(\"x.x\", \"integer\")\n        .add(\"s.s\", new StructType().add(\"y.y\", \"string\").add(\"x.x\", \"integer\")))\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - drop column\") {\n    // Column Mapping allows columns to be dropped\n    def checkReplace(\n        text: String,\n        tableName: String,\n        columnDropped: Seq[String],\n        messages: String*): Unit = {\n      if (columnMappingEnabled) {\n        spark.sql(text)\n        val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n        val field = snapshot.schema.findNestedField(columnDropped, includeCollections = true)\n        assert(field.isEmpty, \"Column was not deleted\")\n      } else {\n        assertNotSupported(text, messages: _*)\n      }\n    }\n\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n\n      // trying to drop v1 of each struct, but it should fail because dropping column is\n      // not supported unless column mapping is enabled\n      checkReplace(\n        s\"\"\"\n           |ALTER TABLE $tableName REPLACE COLUMNS (\n           |  v2 string,\n           |  s STRUCT<v1:int, v2:string>,\n           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n           |)\"\"\".stripMargin,\n        tableName, Seq(\"v1\"), \"dropping column(s)\", \"v1\")\n      // s.v1\n      checkReplace(\n        s\"\"\"\n           |ALTER TABLE $tableName REPLACE COLUMNS (\n           |  v1 int,\n           |  v2 string,\n           |  s STRUCT<v2:string>,\n           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n           |)\"\"\".stripMargin,\n        tableName, Seq(\"s\", \"v1\"), \"dropping column(s)\", \"v1\", \"from s\")\n      // a.v1\n      checkReplace(\n        s\"\"\"\n           |ALTER TABLE $tableName REPLACE COLUMNS (\n           |  v1 int,\n           |  v2 string,\n           |  s STRUCT<v1:int, v2:string>,\n           |  a ARRAY<STRUCT<v2:string>>,\n           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n           |)\"\"\".stripMargin,\n        tableName, Seq(\"a\", \"element\", \"v1\"), \"dropping column(s)\", \"v1\", \"from a\")\n      // m.key.v1\n      checkReplace(\n        s\"\"\"\n           |ALTER TABLE $tableName REPLACE COLUMNS (\n           |  v1 int,\n           |  v2 string,\n           |  s STRUCT<v1:int, v2:string>,\n           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n           |  m MAP<STRUCT<v2:string>, STRUCT<v1:int, v2:string>>\n           |)\"\"\".stripMargin,\n        tableName, Seq(\"m\", \"key\", \"v1\"), \"dropping column(s)\", \"v1\", \"from m.key\")\n      // m.value.v1\n      checkReplace(\n        s\"\"\"\n           |ALTER TABLE $tableName REPLACE COLUMNS (\n           |  v1 int,\n           |  v2 string,\n           |  s STRUCT<v1:int, v2:string>,\n           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v2:string>>\n           |)\"\"\".stripMargin,\n        tableName, Seq(\"m\", \"value\", \"v1\"), \"dropping column(s)\", \"v1\", \"from m.value\")\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - incompatible data type\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n\n      // trying to change the data type of v1 of each struct to long, but it should fail because\n      // changing data type is not supported.\n      assertNotSupported(s\"\"\"\n                            |ALTER TABLE $tableName REPLACE COLUMNS (\n                            |  v1 long,\n                            |  v2 string,\n                            |  s STRUCT<v1:int, v2:string>,\n                            |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                            |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                            |)\"\"\".stripMargin,\n        \"changing data type\", \"v1\", \"from IntegerType to LongType\")\n      // s.v1\n      assertNotSupported(s\"\"\"\n                            |ALTER TABLE $tableName REPLACE COLUMNS (\n                            |  v1 int,\n                            |  v2 string,\n                            |  s STRUCT<v1:long, v2:string>,\n                            |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                            |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                            |)\"\"\".stripMargin,\n        \"changing data type\", \"s.v1\", \"from IntegerType to LongType\")\n      // a.element.v1\n      assertNotSupported(s\"\"\"\n                            |ALTER TABLE $tableName REPLACE COLUMNS (\n                            |  v1 int,\n                            |  v2 string,\n                            |  s STRUCT<v1:int, v2:string>,\n                            |  a ARRAY<STRUCT<v1:long, v2:string>>,\n                            |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                            |)\"\"\".stripMargin,\n        \"changing data type\", \"a.element.v1\", \"from IntegerType to LongType\")\n      // m.key.v1\n      assertNotSupported(s\"\"\"\n                            |ALTER TABLE $tableName REPLACE COLUMNS (\n                            |  v1 int,\n                            |  v2 string,\n                            |  s STRUCT<v1:int, v2:string>,\n                            |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                            |  m MAP<STRUCT<v1:long, v2:string>, STRUCT<v1:int, v2:string>>\n                            |)\"\"\".stripMargin,\n        \"changing data type\", \"m.key.v1\", \"from IntegerType to LongType\")\n      // m.value.v1\n      assertNotSupported(s\"\"\"\n                            |ALTER TABLE $tableName REPLACE COLUMNS (\n                            |  v1 int,\n                            |  v2 string,\n                            |  s STRUCT<v1:int, v2:string>,\n                            |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                            |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:long, v2:string>>\n                            |)\"\"\".stripMargin,\n        \"changing data type\", \"m.value.v1\", \"from IntegerType to LongType\")\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - case insensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"s\", struct(\"v1\", \"v2\"))\n        .withColumn(\"a\", array(\"s\"))\n        .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n      withDeltaTable(df) { tableName =>\n\n        val (deltaLog, _) = getDeltaLogWithSnapshot(tableName)\n        def checkSchema(command: String): Unit = {\n          sql(command)\n\n          assertEqual(deltaLog.update().schema, new StructType()\n            .add(\"v1\", \"integer\")\n            .add(\"v2\", \"string\")\n            .add(\"s\", new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n            .add(\"a\", ArrayType(new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n            .add(\"m\", MapType(\n              new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"),\n              new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"))))\n        }\n\n        // trying to use V1 instead of v1 of each struct.\n        checkSchema(s\"\"\"\n                       |ALTER TABLE $tableName REPLACE COLUMNS (\n                       |  V1 int,\n                       |  v2 string,\n                       |  s STRUCT<v1:int, v2:string>,\n                       |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                       |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                       |)\"\"\".stripMargin)\n        // s.V1\n        checkSchema(s\"\"\"\n                       |ALTER TABLE $tableName REPLACE COLUMNS (\n                       |  v1 int,\n                       |  v2 string,\n                       |  s STRUCT<V1:int, v2:string>,\n                       |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                       |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                       |)\"\"\".stripMargin)\n        // a.V1\n        checkSchema(s\"\"\"\n                       |ALTER TABLE $tableName REPLACE COLUMNS (\n                       |  v1 int,\n                       |  v2 string,\n                       |  s STRUCT<v1:int, v2:string>,\n                       |  a ARRAY<STRUCT<V1:int, v2:string>>,\n                       |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                       |)\"\"\".stripMargin)\n        // m.key.V1\n        checkSchema(s\"\"\"\n                       |ALTER TABLE $tableName REPLACE COLUMNS (\n                       |  v1 int,\n                       |  v2 string,\n                       |  s STRUCT<v1:int, v2:string>,\n                       |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                       |  m MAP<STRUCT<V1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                       |)\"\"\".stripMargin)\n        // m.value.V1\n        checkSchema(s\"\"\"\n                       |ALTER TABLE $tableName REPLACE COLUMNS (\n                       |  v1 int,\n                       |  v2 string,\n                       |  s STRUCT<v1:int, v2:string>,\n                       |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                       |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<V1:int, v2:string>>\n                       |)\"\"\".stripMargin)\n      }\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - case sensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"s\", struct(\"v1\", \"v2\"))\n        .withColumn(\"a\", array(\"s\"))\n        .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n      withDeltaTable(df) { tableName =>\n\n        // trying to use V1 instead of v1 of each struct, but it should fail because case sensitive.\n        assertNotSupported(s\"\"\"\n                              |ALTER TABLE $tableName REPLACE COLUMNS (\n                              |  V1 int,\n                              |  v2 string,\n                              |  s STRUCT<v1:int, v2:string>,\n                              |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                              |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                              |)\"\"\".stripMargin,\n          \"ambiguous\", \"v1\")\n        // s.V1\n        assertNotSupported(s\"\"\"\n                              |ALTER TABLE $tableName REPLACE COLUMNS (\n                              |  v1 int,\n                              |  v2 string,\n                              |  s STRUCT<V1:int, v2:string>,\n                              |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                              |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                              |)\"\"\".stripMargin,\n          \"ambiguous\", \"data type of s\")\n        // a.V1\n        assertNotSupported(s\"\"\"\n                              |ALTER TABLE $tableName REPLACE COLUMNS (\n                              | v1 int,\n                              | v2 string,\n                              | s STRUCT<v1:int, v2:string>,\n                              | a ARRAY<STRUCT<V1:int, v2:string>>,\n                              | m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                              |)\"\"\".stripMargin,\n          \"ambiguous\", \"data type of a.element\")\n        // m.key.V1\n        assertNotSupported(s\"\"\"\n                              |ALTER TABLE $tableName REPLACE COLUMNS (\n                              |  v1 int,\n                              |  v2 string,\n                              |  s STRUCT<v1:int, v2:string>,\n                              |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                              |  m MAP<STRUCT<V1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                              |)\"\"\".stripMargin,\n          \"ambiguous\", \"data type of m.key\")\n        // m.value.V1\n        assertNotSupported(s\"\"\"\n                              |ALTER TABLE $tableName REPLACE COLUMNS (\n                              |  v1 int,\n                              |  v2 string,\n                              |  s STRUCT<v1:int, v2:string>,\n                              |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                              |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<V1:int, v2:string>>\n                              |)\"\"\".stripMargin,\n          \"ambiguous\", \"data type of m.value\")\n      }\n    }\n  }\n\n  ddlTest(\"REPLACE COLUMNS - duplicate\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"s\", struct(\"v1\", \"v2\"))\n        .withColumn(\"a\", array(\"s\"))\n        .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n      withDeltaTable(df) { tableName =>\n        def assertDuplicate(command: String): Unit = {\n          val ex = intercept[AnalysisException] {\n            sql(command)\n          }\n          assert(ex.getMessage.contains(\"duplicate column(s)\"))\n        }\n\n        // trying to add a V1 column, but it should fail because Delta doesn't allow columns\n        // at the same level of nesting that differ only by case.\n        assertDuplicate(s\"\"\"\n                           |ALTER TABLE $tableName REPLACE COLUMNS (\n                           |  v1 int,\n                           |  V1 int,\n                           |  v2 string,\n                           |  s STRUCT<v1:int, v2:string>,\n                           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                           |)\"\"\".stripMargin)\n        // s.V1\n        assertDuplicate(s\"\"\"\n                           |ALTER TABLE $tableName REPLACE COLUMNS (\n                           |  v1 int,\n                           |  v2 string,\n                           |  s STRUCT<v1:int, V1:int, v2:string>,\n                           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                           |)\"\"\".stripMargin)\n        // a.V1\n        assertDuplicate(s\"\"\"\n                           |ALTER TABLE $tableName REPLACE COLUMNS (\n                           |  v1 int,\n                           |  v2 string,\n                           |  s STRUCT<v1:int, v2:string>,\n                           |  a ARRAY<STRUCT<v1:int, V1:int, v2:string>>,\n                           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                           |)\"\"\".stripMargin)\n        // m.key.V1\n        assertDuplicate(s\"\"\"\n                           |ALTER TABLE $tableName REPLACE COLUMNS (\n                           |   v1 int,\n                           |   v2 string,\n                           |   s STRUCT<v1:int, v2:string>,\n                           |   a ARRAY<STRUCT<v1:int, v2:string>>,\n                           |   m MAP<STRUCT<v1:int, V1:int, v2:string>, STRUCT<v1:int, v2:string>>\n                           |)\"\"\".stripMargin)\n        // m.value.V1\n        assertDuplicate(s\"\"\"\n                           |ALTER TABLE $tableName REPLACE COLUMNS (\n                           |  v1 int,\n                           |  v2 string,\n                           |  s STRUCT<v1:int, v2:string>,\n                           |  a ARRAY<STRUCT<v1:int, v2:string>>,\n                           |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, V1:int, v2:string>>\n                           |)\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"REPLACE COLUMNS - loosen nullability with unenforced allowed\") {\n    withSQLConf((\"spark.databricks.delta.constraints.allowUnenforcedNotNull.enabled\", \"true\")) {\n      val schema =\n        \"\"\"\n          |  v1 int NOT NULL,\n          |  v2 string,\n          |  s STRUCT<v1:int NOT NULL, v2:string>,\n          |  a ARRAY<STRUCT<v1:int NOT NULL, v2:string>>,\n          |  m MAP<STRUCT<v1:int NOT NULL, v2:string>, STRUCT<v1:int NOT NULL, v2:string>>\n        \"\"\".stripMargin\n      withDeltaTable(schema) { tableName =>\n\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  v1 int,\n             |  v2 string,\n             |  s STRUCT<v1:int, v2:string>,\n             |  a ARRAY<STRUCT<v1:int, v2:string>>,\n             |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n             |)\"\"\".stripMargin)\n\n        val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n        assertEqual(snapshot.schema, new StructType()\n          .add(\"v1\", \"integer\")\n          .add(\"v2\", \"string\")\n          .add(\"s\", new StructType()\n            .add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n          .add(\"a\", ArrayType(new StructType()\n            .add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n          .add(\"m\", MapType(\n            new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"),\n            new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"))))\n      }\n    }\n  }\n\n  test(\"REPLACE COLUMNS - loosen nullability\") {\n    val schema =\n      \"\"\"\n        |  v1 int NOT NULL,\n        |  v2 string,\n        |  s STRUCT<v1:int NOT NULL, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>> NOT NULL,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>> NOT NULL\n      \"\"\".stripMargin\n    withDeltaTable(schema) { tableName =>\n\n      sql(s\"\"\"\n             |ALTER TABLE $tableName REPLACE COLUMNS (\n             |  v1 int,\n             |  v2 string,\n             |  s STRUCT<v1:int, v2:string>,\n             |  a ARRAY<STRUCT<v1:int, v2:string>>,\n             |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n             |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\")\n        .add(\"v2\", \"string\")\n        .add(\"s\", new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n        .add(\"a\", ArrayType(new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n        .add(\"m\", MapType(\n          new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"),\n          new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"))))\n    }\n  }\n\n  test(\"REPLACE COLUMNS - add not-null column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n      // trying to add not-null column, but it should fail because adding not-null column is\n      // not supported.\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  v3 long NOT NULL,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"NOT NULL is not supported in Hive-style REPLACE COLUMNS\")\n      // s.v3\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string, v3:long NOT NULL>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"adding non-nullable column\", \"s.v3\")\n      // a.element.v3\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string, v3:long NOT NULL>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"adding non-nullable column\", \"a.element.v3\")\n      // m.key.v3\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string, v3:long NOT NULL>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"adding non-nullable column\", \"m.key.v3\")\n      // m.value.v3\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string, v3:long NOT NULL>>\n        |)\"\"\".stripMargin,\n        \"adding non-nullable column\", \"m.value.v3\")\n    }\n  }\n\n  test(\"REPLACE COLUMNS - incompatible nullability\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n\n      // trying to change the data type of v1 of each struct to not null, but it should fail because\n      // tightening nullability is not supported.\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int NOT NULL,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"NOT NULL is not supported in Hive-style REPLACE COLUMNS\")\n      // s.v1\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int NOT NULL, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"tightening nullability\", \"s.v1\")\n      // a.element.v1\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int NOT NULL, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"tightening nullability\", \"a.element.v1\")\n      // m.key.v1\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int NOT NULL, v2:string>, STRUCT<v1:int, v2:string>>\n        |)\"\"\".stripMargin,\n        \"tightening nullability\", \"m.key.v1\")\n      // m.value.v1\n      assertNotSupported(s\"\"\"\n        |ALTER TABLE $tableName REPLACE COLUMNS (\n        |  v1 int,\n        |  v2 string,\n        |  s STRUCT<v1:int, v2:string>,\n        |  a ARRAY<STRUCT<v1:int, v2:string>>,\n        |  m MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int NOT NULL, v2:string>>\n        |)\"\"\".stripMargin,\n        \"tightening nullability\", \"m.value.v1\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaAlterTableTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\n\nimport org.apache.spark.sql.delta.DeltaConfigs.CHECKPOINT_INTERVAL\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.schema.{DeltaInvariantViolationException, SchemaUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.util.CharVarcharUtils\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\ntrait DeltaAlterTableTestBase\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaColumnMappingTestUtils\n  with DeltaTestUtilsForTempViews {\n\n  protected def createTable(schema: String, tblProperties: Map[String, String]): String\n\n  protected def createTable(df: DataFrame, partitionedBy: Seq[String]): String\n\n  protected def dropTable(identifier: String): Unit\n\n  protected def getDeltaLogWithSnapshot(identifier: String): (DeltaLog, Snapshot)\n\n  final protected def withDeltaTable(schema: String)(f: String => Unit): Unit = {\n    withDeltaTable(schema, Map.empty[String, String])(i => f(i))\n  }\n\n  final protected def withDeltaTable(\n      schema: String,\n      tblProperties: Map[String, String])(f: String => Unit): Unit = {\n    val identifier = createTable(schema, tblProperties)\n    try {\n      f(identifier)\n    } finally {\n      dropTable(identifier)\n    }\n  }\n\n  final protected def withDeltaTable(df: DataFrame)(f: String => Unit): Unit = {\n    withDeltaTable(df, Seq.empty[String])(i => f(i))\n  }\n\n  final protected def withDeltaTable(\n      df: DataFrame,\n      partitionedBy: Seq[String])(f: String => Unit): Unit = {\n    val identifier = createTable(df, partitionedBy)\n    try {\n      f(identifier)\n    } finally {\n      dropTable(identifier)\n    }\n  }\n\n  protected def ddlTest(testName: String)(f: => Unit): Unit = {\n    testQuietly(testName)(f)\n  }\n\n  protected def assertNotSupported(command: String, messages: String*): Unit = {\n    val ex = intercept[Exception] {\n      sql(command)\n    }.getMessage\n    assert(ex.contains(\"not supported\") || ex.contains(\"Unsupported\") || ex.contains(\"Cannot\"))\n    messages.foreach(msg => assert(ex.contains(msg)))\n  }\n}\n\ntrait DeltaAlterTableTests extends DeltaAlterTableTestBase {\n\n  import testImplicits._\n\n  ///////////////////////////////\n  // SET/UNSET TBLPROPERTIES\n  ///////////////////////////////\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - simple\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n\n      sql(s\"\"\"\n        |ALTER TABLE $tableName\n        |SET TBLPROPERTIES (\n        |  'delta.logRetentionDuration' = '2 weeks',\n        |  'delta.checkpointInterval' = '20',\n        |  'key' = 'value'\n        |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot1) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.logRetentionDuration\" -> \"2 weeks\",\n        \"delta.checkpointInterval\" -> \"20\",\n        \"key\" -> \"value\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot1.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot1.metadata) == 20)\n\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('delta.checkpointInterval', 'key')\")\n\n      val snapshot2 = deltaLog.update()\n      assertEqual(snapshot2.metadata.configuration,\n        Map(\"delta.logRetentionDuration\" -> \"2 weeks\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot2.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot2.metadata) ==\n        CHECKPOINT_INTERVAL.fromString(CHECKPOINT_INTERVAL.defaultValue))\n    }\n  }\n\n  testQuietlyWithTempView(\"negative case - not supported on temp views\") { isSQLTempView =>\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n      createTempViewFromTable(tableName, isSQLTempView)\n\n      val e = intercept[AnalysisException] {\n        sql(\n          \"\"\"\n            |ALTER TABLE v\n            |SET TBLPROPERTIES (\n            |  'delta.logRetentionDuration' = '2 weeks',\n            |  'delta.checkpointInterval' = '20',\n            |  'key' = 'value'\n            |)\"\"\".stripMargin)\n      }\n      assert(e.getMessage.contains(\"expects a table. Please use ALTER VIEW instead.\") ||\n        e.getMessage.contains(\"EXPECT_TABLE_NOT_VIEW.USE_ALTER_VIEW\"))\n    }\n  }\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - case insensitivity\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n\n      sql(s\"\"\"\n        |ALTER TABLE $tableName\n        |SET TBLPROPERTIES (\n        |  'dEltA.lOgrEteNtiOndURaTion' = '1 weeks',\n        |  'DelTa.ChEckPoiNtinTervAl' = '5',\n        |  'key' = 'value1'\n        |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot1) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.logRetentionDuration\" -> \"1 weeks\",\n        \"delta.checkpointInterval\" -> \"5\",\n        \"key\" -> \"value1\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot1.metadata) == 1 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot1.metadata) == 5)\n\n      sql(s\"\"\"\n        |ALTER TABLE $tableName\n        |SET TBLPROPERTIES (\n        |  'dEltA.lOgrEteNtiOndURaTion' = '2 weeks',\n        |  'DelTa.ChEckPoiNtinTervAl' = '20',\n        |  'kEy' = 'value2'\n        |)\"\"\".stripMargin)\n\n      val snapshot2 = deltaLog.update()\n      assertEqual(snapshot2.metadata.configuration, Map(\n        \"delta.logRetentionDuration\" -> \"2 weeks\",\n        \"delta.checkpointInterval\" -> \"20\",\n        \"key\" -> \"value1\",\n        \"kEy\" -> \"value2\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot2.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot2.metadata) == 20)\n\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('DelTa.ChEckPoiNtinTervAl', 'kEy')\")\n\n      val snapshot3 = deltaLog.update()\n      assertEqual(snapshot3.metadata.configuration,\n        Map(\"delta.logRetentionDuration\" -> \"2 weeks\", \"key\" -> \"value1\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot3.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot3.metadata) ==\n        CHECKPOINT_INTERVAL.fromString(CHECKPOINT_INTERVAL.defaultValue))\n    }\n  }\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - set unknown config\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('delta.key' = 'value')\")\n      }\n      assert(ex.getMessage.contains(\"Unknown configuration was specified: delta.key\"))\n    }\n  }\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - set invalid value\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n\n      val ex1 = intercept[Exception] {\n        sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('delta.randomPrefixLength' = '-1')\")\n      }\n      assert(ex1.getMessage.contains(\"randomPrefixLength needs to be greater than 0.\"))\n\n      val ex2 = intercept[Exception] {\n        sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('delta.randomPrefixLength' = 'value')\")\n      }\n      assert(ex2.getMessage.contains(\"randomPrefixLength needs to be greater than 0.\"))\n    }\n  }\n\n  ddlTest(\"SET TBLPROPERTIES - delta.randomizeFilePrefixes\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n      // Initially, randomizeFilePrefixes should be false (default)\n      val (deltaLog, initialSnapshot) = getDeltaLogWithSnapshot(tableName)\n      assert(!DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(initialSnapshot.metadata),\n        \"randomizeFilePrefixes should be false by default\")\n\n      // Set delta.randomizeFilePrefixes and delta.randomPrefixLength\n      sql(s\"\"\"\n         |ALTER TABLE $tableName\n         |SET TBLPROPERTIES (\n         |  'delta.randomizeFilePrefixes' = 'true',\n         |  'delta.randomPrefixLength' = '5'\n         |)\"\"\".stripMargin)\n\n      val snapshot1 = deltaLog.update()\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.randomizeFilePrefixes\" -> \"true\",\n        \"delta.randomPrefixLength\" -> \"5\"\n      ))\n\n      // Verify the configuration is properly parsed\n      assert(DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(snapshot1.metadata),\n        \"randomizeFilePrefixes should be enabled\")\n      assert(DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot1.metadata) == 5,\n        \"randomPrefixLength should be 5\")\n\n      // Insert data to create files with random prefixes\n      sql(s\"INSERT INTO $tableName VALUES (1, 'test1'), (2, 'test2'), (3, 'test3')\")\n\n      val snapshot2 = deltaLog.update()\n      val allFiles = snapshot2.allFiles.collect()\n\n      // Verify that files exist and have random prefixes\n      assert(allFiles.nonEmpty, \"Table should have data files\")\n\n      // Check that file paths contain 5-character random prefix pattern\n      val prefixLength = DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot2.metadata)\n      assert(prefixLength == 5, s\"Expected prefix length of 5, but got $prefixLength\")\n\n      val pattern = s\"[A-Za-z0-9]{$prefixLength}/.*part-.*parquet\"\n      allFiles.foreach { file =>\n        assert(file.path.matches(pattern),\n          s\"File path '${file.path}' does not match expected random prefix pattern '$pattern'\")\n      }\n    }\n  }\n\n  ddlTest(\"UNSET TBLPROPERTIES - delta.randomizeFilePrefixes\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n      // First, set the randomizeFilePrefixes properties\n      sql(s\"\"\"\n         |ALTER TABLE $tableName\n         |SET TBLPROPERTIES (\n         |  'delta.randomizeFilePrefixes' = 'true',\n         |  'delta.randomPrefixLength' = '8',\n         |  'key' = 'value'\n         |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot1) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.randomizeFilePrefixes\" -> \"true\",\n        \"delta.randomPrefixLength\" -> \"8\",\n        \"key\" -> \"value\"\n      ))\n\n      // Verify the configuration is properly set\n      assert(DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(snapshot1.metadata),\n        \"randomizeFilePrefixes should be enabled\")\n      assert(DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot1.metadata) == 8,\n        \"randomPrefixLength should be 8\")\n\n      // Insert data to create files with random prefixes\n      sql(s\"INSERT INTO $tableName VALUES (1, 'test1'), (2, 'test2')\")\n\n      val snapshot1WithData = deltaLog.update()\n      val filesWithPrefixes = snapshot1WithData.allFiles.collect()\n\n      // Verify files have random prefixes\n      assert(filesWithPrefixes.nonEmpty, \"Table should have data files\")\n      val pattern8 = s\"[A-Za-z0-9]{8}/.*part-.*parquet\"\n      filesWithPrefixes.foreach { file =>\n        assert(file.path.matches(pattern8),\n          s\"File path '${file.path}' should have 8-character random prefix\")\n      }\n\n      // Now UNSET the randomizeFilePrefixes property\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('delta.randomizeFilePrefixes', 'key')\")\n\n      val snapshot2 = deltaLog.update()\n      assertEqual(snapshot2.metadata.configuration,\n        Map(\"delta.randomPrefixLength\" -> \"8\"))\n\n      // Verify that randomizeFilePrefixes is now disabled (reverted to default)\n      assert(!DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(snapshot2.metadata),\n        \"randomizeFilePrefixes should be disabled after UNSET\")\n      assert(DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot2.metadata) == 8,\n        \"randomPrefixLength should still be 8 (not unset)\")\n\n      // Insert more data to verify new files don't have random prefixes\n      sql(s\"INSERT INTO $tableName VALUES (3, 'test3'), (4, 'test4')\")\n\n      val snapshot3 = deltaLog.update()\n      val allFiles = snapshot3.allFiles.collect()\n      val newFiles = allFiles.filterNot(f => filesWithPrefixes.exists(_.path == f.path))\n\n      // Verify that new files don't have random prefixes (should be regular paths)\n      assert(newFiles.nonEmpty, \"Should have new files after second insert\")\n      newFiles.foreach { file =>\n        assert(!file.path.matches(pattern8),\n          s\"New file path '${file.path}' should NOT have random prefix after UNSET\")\n        // New files should have regular naming without random prefixes\n        assert(file.path.matches(\".*part-.*parquet\"),\n          s\"New file path '${file.path}' should have regular parquet file naming\")\n      }\n    }\n  }\n\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - delta.randomizeFilePrefixes - partitioned table\") {\n    withDeltaTable(Seq((1, \"x\", 100), (2, \"y\", 200)).toDF(\"id\", \"part\", \"value\"),\n                   Seq(\"part\")) { tableName =>\n      // First, set the randomizeFilePrefixes properties\n      sql(s\"\"\"\n         |ALTER TABLE $tableName\n         |SET TBLPROPERTIES (\n         |  'delta.randomizeFilePrefixes' = 'true',\n         |  'delta.randomPrefixLength' = '7',\n         |  'key' = 'value'\n         |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot1) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.randomizeFilePrefixes\" -> \"true\",\n        \"delta.randomPrefixLength\" -> \"7\",\n        \"key\" -> \"value\"\n      ))\n\n      // Get initial files (created during table setup - should have partition structure)\n      val initialFiles = deltaLog.update().allFiles.collect()\n\n      // Insert data to create files with random prefixes\n      sql(s\"INSERT INTO $tableName VALUES (3, 'x', 300), (4, 'z', 400)\")\n\n      val snapshot1WithData = deltaLog.update()\n      val filesInSnapshot1 = snapshot1WithData.allFiles.collect()\n\n      // Separate initial files from new files with prefixes\n      val filesWithPrefixes = filesInSnapshot1.filterNot(f => initialFiles.exists(_.path == f.path))\n\n      // Verify INITIAL files have partition directory structure\n      // (created before random prefixes enabled)\n      val initialPartitionPattern = s\"part=[xyz]/.*part-.*parquet\"\n      initialFiles.foreach { file =>\n        assert(file.path.matches(initialPartitionPattern),\n          s\"Initial file path '${file.path}' should have partition directory structure\")\n      }\n\n      // Verify NEW files have random prefixes (created after random prefixes enabled)\n      assert(filesWithPrefixes.nonEmpty, \"Should have new files with random prefixes\")\n      val pattern7 = s\"[A-Za-z0-9]{7}/.*part-.*parquet\"\n      filesWithPrefixes.foreach { file =>\n        assert(file.path.matches(pattern7),\n          s\"New file path '${file.path}' should have 7-character random prefix\")\n      }\n\n      // Now UNSET the randomizeFilePrefixes property\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('delta.randomizeFilePrefixes', 'key')\")\n\n      val snapshot2 = deltaLog.update()\n      assertEqual(snapshot2.metadata.configuration,\n        Map(\"delta.randomPrefixLength\" -> \"7\"))\n\n      // Verify that randomizeFilePrefixes is now disabled\n      assert(!DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(snapshot2.metadata),\n        \"randomizeFilePrefixes should be disabled after UNSET\")\n      assert(DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot2.metadata) == 7,\n        \"randomPrefixLength should still be 7 (not unset)\")\n\n      // Insert more data to verify new files don't have random prefixes\n      sql(s\"INSERT INTO $tableName VALUES (5, 'x', 500), (6, 'y', 600)\")\n\n      val snapshot3 = deltaLog.update()\n      val allFinalFiles = snapshot3.allFiles.collect()\n      val filesAfterUnset = allFinalFiles.filterNot(f => filesInSnapshot1.exists(_.path == f.path))\n\n      // Verify that new files don't have random prefixes (should revert to partition structure)\n      assert(filesAfterUnset.nonEmpty, \"Should have new files after UNSET and second insert\")\n      val partitionPatternAfterUnset = s\"part=[xy]/.*part-.*parquet\"\n      filesAfterUnset.foreach { file =>\n        assert(!file.path.matches(pattern7),\n          s\"File after UNSET '${file.path}' should NOT have random prefix\")\n        // New files should revert to partition directory structure\n        assert(file.path.matches(partitionPatternAfterUnset),\n          s\"File after UNSET '${file.path}' should have partition directory structure\")\n      }\n    }\n  }\n\n  test(\"SET/UNSET comment by TBLPROPERTIES\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n      def assertCommentEmpty(): Unit = {\n        val props = sql(s\"DESC EXTENDED $tableName\").collect()\n        assert(!props.exists(_.getString(0) === \"Comment\"), \"Comment should be empty\")\n\n        val desc = sql(s\"DESCRIBE DETAIL $tableName\").head()\n        val fieldIndex = desc.fieldIndex(\"description\")\n        assert(desc.isNullAt(fieldIndex))\n      }\n\n      assertCommentEmpty()\n\n      sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('comment'='does it work?')\")\n\n      val props = sql(s\"DESC EXTENDED $tableName\").collect()\n      assert(props.exists(r => r.getString(0) === \"Comment\" && r.getString(1) === \"does it work?\"),\n        s\"Comment not found in:\\n${props.mkString(\"\\n\")}\")\n\n      val desc = sql(s\"DESCRIBE DETAIL $tableName\").head()\n      assert(desc.getAs[String](\"description\") === \"does it work?\")\n\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('comment')\")\n      assertCommentEmpty()\n    }\n  }\n\n  test(\"update comment by TBLPROPERTIES\") {\n    val tableName = \"comment_table\"\n\n    def checkComment(expected: String): Unit = {\n      val props = sql(s\"DESC EXTENDED $tableName\").collect()\n      assert(props.exists(r => r.getString(0) === \"Comment\" && r.getString(1) === expected),\n        s\"Comment not found in:\\n${props.mkString(\"\\n\")}\")\n\n      val desc = sql(s\"DESCRIBE DETAIL $tableName\").head()\n      assert(desc.getAs[String](\"description\") === expected)\n    }\n\n    withTable(tableName) {\n      sql(s\"CREATE TABLE $tableName (id bigint) USING delta COMMENT 'x'\")\n      checkComment(\"x\")\n\n      sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('comment'='y')\")\n\n      checkComment(\"y\")\n    }\n  }\n\n  ddlTest(\"Invalid TBLPROPERTIES\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n      // Handled by Spark\n      intercept[ParseException] {\n        sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('location'='/some/new/path')\")\n      }\n      // Handled by Spark\n      intercept[ParseException] {\n        sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('provider'='json')\")\n      }\n      // Illegal to add constraints\n      val e3 = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('delta.constraints.c1'='age >= 25')\")\n      }\n      assert(e3.getMessage.contains(\"ALTER TABLE ADD CONSTRAINT\"))\n    }\n  }\n\n  private def setProps(table: String, kvs: (String, String)*): Unit = {\n    val props = kvs.map { case (k, v) => s\"'$k'='$v'\" }.mkString(\", \")\n    val sqlString = s\"ALTER TABLE $table SET TBLPROPERTIES ($props)\"\n    spark.sql(sqlString)\n  }\n\n  private def expectValidationError(f: => Unit): Unit = {\n    val ex = intercept[Exception](f)\n    assert(\n      (ex.getMessage.contains(\"delta.logRetentionDuration\") &&\n      ex.getMessage.contains(\"delta.deletedFileRetentionDuration\")) &&\n      (ex.getMessage.contains(\"needs to be greater than or equal to\") ||\n        ex.getMessage.contains(\"needs to be less than or equal to\"))\n    )\n  }\n\n  ///////////////////////////////\n  // logRetentionDuration and deletedFileRetentionDuration table property\n  // compatibility tests\n  ///////////////////////////////\n\n  // cases where validation succeeds\n  test(\"log > deleted (same units) succeeds\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      setProps(t,\n        \"delta.deletedFileRetentionDuration\" -> \"interval 7 days\",\n        \"delta.logRetentionDuration\"         -> \"interval 30 days\"\n      )\n    }\n  }\n\n  test(\"log > deleted (different units) succeeds\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      setProps(t,\n        \"delta.deletedFileRetentionDuration\" -> \"interval 4 days\",\n        \"delta.logRetentionDuration\"         -> \"interval 120 hours\"\n      )\n    }\n  }\n\n  test(\"log > deleted one after the other succeeds\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      setProps(t,\n        \"delta.deletedFileRetentionDuration\" -> \"interval 6 days\"\n      )\n      setProps(t,\n        \"delta.logRetentionDuration\" -> \"interval 10 days\"\n      )\n    }\n  }\n\n  test(\"key case-insensitivity still succeeds\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      setProps(t,\n        \"delta.deletedFileRETENTIONDuration\" -> \"  interval 7 days \",\n        \"delta.logRetentionDURATION\"         -> \" INTERVAL 30 DAYS \"\n      )\n    }\n  }\n\n  test(\"equal durations shouldn't fail\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n        setProps(t,\n          \"delta.deletedFileRetentionDuration\" -> \"interval 7 days\",\n          \"delta.logRetentionDuration\"         -> \"interval 1 week\"\n        )\n    }\n  }\n\n  // cases where validation fails\n  test(\"log < deleted should fail\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      expectValidationError(\n        setProps(t,\n          \"delta.deletedFileRetentionDuration\" -> \"interval 10 days\",\n          \"delta.logRetentionDuration\"         -> \"interval 6 days\"\n        )\n      )\n    }\n  }\n\n  test(\"sequence that becomes invalid (raise deleted above log) should fail\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      setProps(t,\n        \"delta.deletedFileRetentionDuration\" -> \"interval 7 days\",\n        \"delta.logRetentionDuration\"         -> \"interval 30 days\"\n      )\n      expectValidationError(\n        setProps(t, \"delta.deletedFileRetentionDuration\" -> \"interval 60 days\")\n      )\n    }\n  }\n\n  test(\"default log vs explicit deleted that exceeds default should fail\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      // default log 30 days; setting deleted to 45 should fail\n      expectValidationError(\n        setProps(t, \"delta.deletedFileRetentionDuration\" -> \"interval 45 days\")\n      )\n    }\n  }\n\n  test(\"default deletedRetention vs explicit log retention that exceeds default should fail\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      // default deletedFileRetention 7 days; setting log to 5 should fail\n      expectValidationError(\n        setProps(t, \"delta.logRetentionDuration\" -> \"interval 5 days\")\n      )\n    }\n  }\n\n  test(\"key case-insensitivity still fails\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      expectValidationError(\n        setProps(t,\n          \"DELTA.DELETEDFILERETENTIONDURATION\" -> \"interval 14 days\",\n          \"delta.logRetentionDurATION\"         -> \"interval 7 days\"\n        )\n      )\n    }\n  }\n\n  test(\"reset to defaults becomes valid\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      // Start invalid\n      expectValidationError(\n        setProps(t,\n          \"delta.deletedFileRetentionDuration\" -> \"interval 40 days\",\n          \"delta.logRetentionDuration\"         -> \"interval 30 days\"\n        )\n      )\n      // Reset deleted; expect success if defaults are valid\n      spark.sql(s\"ALTER TABLE $t UNSET TBLPROPERTIES ('delta.deletedFileRetentionDuration')\")\n      // Now set log to something valid relative to default deleted (7d)\n      setProps(t, \"delta.logRetentionDuration\" -> \"interval 30 days\")\n    }\n  }\n\n  test(\"property values are invalid before. Setting an unrelated property shouldn't error out\") {\n    withDeltaTable(\"v1 int, v2 string\") { t =>\n      // Start invalid\n      withSQLConf(\n        DeltaSQLConf.ENFORCE_DELETED_FILE_AND_LOG_RETENTION_DURATION_COMPATIBILITY.key ->\n          false.toString) {\n        setProps(t,\n          \"delta.deletedFileRetentionDuration\" -> \"interval 40 days\",\n          \"delta.logRetentionDuration\" -> \"interval 30 days\"\n        )\n      }\n      // Now set unrelated table property\n      setProps(t, \"delta.checkpointInterval\" -> \"100\")\n    }\n  }\n\n  ///////////////////////////////\n  // ADD COLUMNS\n  ///////////////////////////////\n\n  ddlTest(\"ADD COLUMNS - simple\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long, v4 double)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"v3\", \"long\").add(\"v4\", \"double\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, Option[Long], Option[Double])],\n        (1, \"a\", None, None), (2, \"b\", None, None))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into complex types - Array\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", array(struct(\"v1\")))) { tableName =>\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS (a.element.v3 long)\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"a\", ArrayType(new StructType()\n          .add(\"v1\", \"integer\")\n          .add(\"v3\", \"long\"))))\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS (a.element.v4 struct<f1:long>)\n         \"\"\".stripMargin)\n\n      assertEqual(deltaLog.snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"a\", ArrayType(new StructType()\n          .add(\"v1\", \"integer\")\n          .add(\"v3\", \"long\")\n          .add(\"v4\", new StructType().add(\"f1\", \"long\")))))\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS (a.element.v4.f2 string)\n         \"\"\".stripMargin)\n\n      assertEqual(deltaLog.snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"a\", ArrayType(new StructType()\n          .add(\"v1\", \"integer\")\n          .add(\"v3\", \"long\")\n          .add(\"v4\", new StructType()\n            .add(\"f1\", \"long\")\n            .add(\"f2\", \"string\")))))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into complex types - Map with simple key\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map('v1, struct(\"v2\")))) { tableName =>\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS (m.value.mvv3 long)\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"m\", MapType(IntegerType,\n          new StructType()\n            .add(\"v2\", \"string\")\n            .add(\"mvv3\", \"long\"))))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into complex types - Map with simple value\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map(struct(\"v1\"), 'v2))) { tableName =>\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS (m.key.mkv3 long)\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"m\", MapType(\n          new StructType()\n            .add(\"v1\", \"integer\")\n            .add(\"mkv3\", \"long\"),\n          StringType)))\n    }\n  }\n\n  private def checkErrMsg(msg: String, field: Seq[String]): Unit = {\n    val fieldStr = field.map(f => s\"`$f`\").mkString(\".\")\n    val fieldParentStr = field.dropRight(1).map(f => s\"`$f`\").mkString(\".\")\n    assert(msg.contains(\n      s\"Field name $fieldStr is invalid: $fieldParentStr is not a struct\"))\n  }\n\n  ddlTest(\"ADD COLUMNS should not be able to add column to basic type key/value of \" +\n    \"MapType\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map('v1, 'v2))) { tableName =>\n      var ex = intercept[AnalysisException] {\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName ADD COLUMNS (m.key.mkv3 long)\n         \"\"\".stripMargin)\n      }\n      checkErrMsg(ex.getMessage, Seq(\"m\", \"key\", \"mkv3\"))\n\n      ex = intercept[AnalysisException] {\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName ADD COLUMNS (m.value.mkv3 long)\n         \"\"\".stripMargin)\n      }\n      checkErrMsg(ex.getMessage, Seq(\"m\", \"value\", \"mkv3\"))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into complex types - Map\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map(struct(\"v1\"), struct(\"v2\")))) { tableName =>\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS (m.key.mkv3 long, m.value.mvv3 long)\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"m\", MapType(\n          new StructType()\n            .add(\"v1\", \"integer\")\n            .add(\"mkv3\", \"long\"),\n          new StructType()\n            .add(\"v2\", \"string\")\n            .add(\"mvv3\", \"long\"))))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into complex types - Map (nested)\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map(struct(\"v1\"), struct(\"v2\")))) { tableName =>\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS\n           |(m.key.mkv3 long, m.value.mvv3 struct<f1: long, f2:array<struct<n:long>>>)\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"m\", MapType(\n          new StructType()\n            .add(\"v1\", \"integer\")\n            .add(\"mkv3\", \"long\"),\n          new StructType()\n            .add(\"v2\", \"string\")\n            .add(\"mvv3\", new StructType()\n              .add(\"f1\", \"long\")\n              .add(\"f2\", ArrayType(new StructType()\n                .add(\"n\", \"long\")))))))\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName ADD COLUMNS\n           |(m.value.mvv3.f2.element.p string)\n         \"\"\".stripMargin)\n\n      assertEqual(deltaLog.snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"m\", MapType(\n          new StructType()\n            .add(\"v1\", \"integer\")\n            .add(\"mkv3\", \"long\"),\n          new StructType()\n            .add(\"v2\", \"string\")\n            .add(\"mvv3\", new StructType()\n              .add(\"f1\", \"long\")\n              .add(\"f2\", ArrayType(new StructType()\n                .add(\"n\", \"long\")\n                .add(\"p\", \"string\")))))))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into Map should fail if key or value not specified\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map(struct(\"v1\"), struct(\"v2\")))) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName ADD COLUMNS (m.mkv3 long)\n           \"\"\".stripMargin)\n      }\n      checkErrMsg(ex.getMessage, Seq(\"m\", \"mkv3\"))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS into Array should fail if element is not specified\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", array(struct(\"v1\")))) { tableName =>\n\n      intercept[AnalysisException] {\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName ADD COLUMNS (a.v3 long)\n         \"\"\".stripMargin)\n      }\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - a partitioned table\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\"), Seq(\"v2\")) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long, v4 double)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"v3\", \"long\").add(\"v4\", \"double\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, Option[Long], Option[Double])],\n        (1, \"a\", None, None), (2, \"b\", None, None))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - with a comment\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long COMMENT 'new column')\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"v3\", \"long\", true, \"new column\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, Option[Long])],\n        (1, \"a\", None), (2, \"b\", None))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - adding to a non-struct column\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (v2.x long)\")\n      }\n      checkErrMsg(ex.getMessage, Seq(\"v2\", \"x\"))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - a duplicate name\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n      intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (v2 long)\")\n      }\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - a duplicate name (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n      intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (struct.v2 long)\")\n      }\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - column name with spaces\") {\n    if (!columnMappingEnabled) {\n      withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n        val ex = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName ADD COLUMNS (`a column name with spaces` long)\")\n        }\n        assert(ex.getMessage.contains(\"invalid character(s)\"))\n      }\n    } else {\n      // column mapping mode supports arbitrary column names\n      withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (`a column name with spaces` long)\")\n      }\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - column name with spaces (nested)\") {\n    if (!columnMappingEnabled) {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n      withDeltaTable(df) { tableName =>\n        val ex = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName ADD COLUMNS (struct.`a column name with spaces` long)\")\n        }\n        assert(ex.getMessage.contains(\"invalid character(s)\"))\n      }\n    } else {\n      // column mapping mode supports arbitrary column names\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n      withDeltaTable(df) { tableName =>\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (struct.`a column name with spaces` long)\")\n      }\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - special column names\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"z.z\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (Int, String))],\n        (1, \"a\", (1, \"a\")), (2, \"b\", (2, \"b\")))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (`x.x` long, `z.z`.`y.y` double)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"z.z\", new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\").add(\"y.y\", \"double\"))\n        .add(\"x.x\", \"long\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (Int, String, Option[Double]), Option[Long])],\n        (1, \"a\", (1, \"a\", None), None), (2, \"b\", (2, \"b\", None), None))\n    }\n  }\n\n  test(\"ADD COLUMNS - with positions\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long FIRST, v4 long AFTER v1, v5 long)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v3\", \"long\").add(\"v1\", \"integer\")\n        .add(\"v4\", \"long\").add(\"v2\", \"string\").add(\"v5\", \"long\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Option[Long], Int, Option[Long], String, Option[Long])],\n        (None, 1, None, \"a\", None), (None, 2, None, \"b\", None))\n    }\n  }\n\n  test(\"ADD COLUMNS - with positions using an added column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(\"delta_test\").as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n\n      sql(\"ALTER TABLE delta_test ADD COLUMNS (v3 long FIRST, v4 long AFTER v3, v5 long AFTER v4)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v3\", \"long\").add(\"v4\", \"long\").add(\"v5\", \"long\")\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n\n      checkDatasetUnorderly(\n        spark.table(\"delta_test\").as[(Option[Long], Option[Long], Option[Long], Int, String)],\n        (None, None, None, 1, \"a\"), (None, None, None, 2, \"b\"))\n    }\n  }\n\n  test(\"ADD COLUMNS - nested columns\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(\"delta_test\").as[(Int, String, (Int, String))],\n        (1, \"a\", (1, \"a\")), (2, \"b\", (2, \"b\")))\n\n      sql(\"ALTER TABLE delta_test ADD COLUMNS \" +\n        \"(struct.v3 long FIRST, struct.v4 long AFTER v1, struct.v5 long)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"struct\", new StructType()\n          .add(\"v3\", \"long\").add(\"v1\", \"integer\")\n          .add(\"v4\", \"long\").add(\"v2\", \"string\").add(\"v5\", \"long\")))\n\n      checkDatasetUnorderly(\n        spark.table(\"delta_test\")\n          .as[(Int, String, (Option[Long], Int, Option[Long], String, Option[Long]))],\n        (1, \"a\", (None, 1, None, \"a\", None)), (2, \"b\", (None, 2, None, \"b\", None)))\n    }\n  }\n\n  test(\"ADD COLUMNS - special column names with positions\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"z.z\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (Int, String))],\n        (1, \"a\", (1, \"a\")), (2, \"b\", (2, \"b\")))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (`x.x` long after v1, `z.z`.`y.y` double)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"x.x\", \"long\").add(\"v2\", \"string\")\n        .add(\"z.z\", new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\").add(\"y.y\", \"double\"))\n      )\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, Option[Long], String, (Int, String, Option[Double]))],\n        (1, None, \"a\", (1, \"a\", None)), (2, None, \"b\", (2, \"b\", None)))\n    }\n  }\n\n  test(\"ADD COLUMNS - adding after an unknown column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long AFTER unknown)\")\n      }\n      assert(\n        ex.getMessage.contains(\"Couldn't find\") || ex.getMessage.contains(\"No such struct field\"))\n    }\n  }\n\n  test(\"ADD COLUMNS - case insensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      withDeltaTable(df) { tableName =>\n\n        sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long AFTER V1)\")\n\n        val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n        assertEqual(snapshot.schema, new StructType()\n          .add(\"v1\", \"integer\").add(\"v3\", \"long\").add(\"v2\", \"string\"))\n      }\n    }\n  }\n\n  test(\"ADD COLUMNS - case sensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      withDeltaTable(df) { tableName =>\n\n        val ex = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long AFTER V1)\")\n        }\n        assert(\n          ex.getMessage.contains(\"Couldn't find\") || ex.getMessage.contains(\"No such struct field\"))\n      }\n    }\n  }\n\n  test(\"ADD COLUMNS - adding after an Array<MapType> column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"v3\", array(map(col(\"v1\"), col(\"v2\"))))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (v4 string AFTER V3)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", IntegerType)\n        .add(\"v2\", StringType)\n        .add(\"v3\", ArrayType(\n          MapType(IntegerType, StringType)))\n        .add(\"v4\", StringType))\n    }\n  }\n\n  ///////////////////////////////\n  // CHANGE COLUMN\n  ///////////////////////////////\n\n  ddlTest(\"CHANGE COLUMN - add a comment\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer COMMENT 'a comment'\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\", true, \"a comment\").add(\"v2\", \"string\"))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment to a partitioned table\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\"), Seq(\"v2\")) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v2 v2 string COMMENT 'a comment'\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\", true, \"a comment\"))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment to special column names (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"x.x\", \"y.y\")\n      .withColumn(\"z.z\", struct(\"`x.x`\", \"`y.y`\"))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN `z.z`.`x.x` `x.x` integer COMMENT 'a comment'\")\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN `x.x` `x.x` integer COMMENT 'another comment'\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"x.x\", \"integer\", true, \"another comment\")\n        .add(\"y.y\", \"string\")\n        .add(\"z.z\", new StructType()\n          .add(\"x.x\", \"integer\", true, \"a comment\").add(\"y.y\", \"string\")))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (Int, String))],\n        (1, \"a\", (1, \"a\")), (2, \"b\", (2, \"b\")))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment to a MapType (nested)\") {\n    val table = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", array(struct(array(struct(map(struct(\"v1\"), struct(\"v2\")))))))\n    withDeltaTable(table) { tableName =>\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName CHANGE COLUMN\n           |a.element.col1.element.col1 col1 MAP<STRUCT<v1:int>,\n           |STRUCT<v2:string>> COMMENT 'a comment'\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"a\", ArrayType(new StructType()\n            .add(\"col1\", ArrayType(new StructType()\n              .add(\"col1\", MapType(\n                new StructType()\n                  .add(\"v1\", \"integer\"),\n                new StructType()\n                  .add(\"v2\", \"string\")), nullable = true, \"a comment\"))))))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment to an ArrayType (nested)\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"m\", map(struct(\"v1\"), struct(array(struct(struct(\"v1\"))))))) { tableName =>\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName CHANGE COLUMN\n           |m.value.col1.element.col1.v1 v1 integer COMMENT 'a comment'\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"m\", MapType(\n          new StructType()\n            .add(\"v1\", \"integer\"),\n          new StructType()\n            .add(\"col1\", ArrayType(new StructType()\n              .add(\"col1\", new StructType()\n                .add(\"v1\", \"integer\", nullable = true, \"a comment\")))))))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment to an ArrayType\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", array('v1))) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN a a ARRAY<int> COMMENT 'a comment'\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"a\", ArrayType(IntegerType), nullable = true, \"a comment\"))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment to a MapType\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", map('v1, 'v2))) { tableName =>\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tableName CHANGE COLUMN\n           |a a MAP<int, string> COMMENT 'a comment'\n         \"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"a\", MapType(IntegerType, StringType), nullable = true, \"a comment\"))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - (unsupported) add a comment to key/value of a MapType\") {\n    val df = Seq((1, 1), (2, 2)).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", map('v1, 'v2))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.key COMMENT 'a comment'\")\n        },\n        \"DELTA_UNSUPPORTED_COMMENT_MAP_ARRAY\",\n        parameters = Map(\"fieldPath\" -> \"a.key\")\n      )\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.value COMMENT 'a comment'\")\n        },\n        \"DELTA_UNSUPPORTED_COMMENT_MAP_ARRAY\",\n        parameters = Map(\"fieldPath\" -> \"a.value\")\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - (unsupported) add a comment to element of an array\") {\n    val df = Seq(1, 2).toDF(\"v1\")\n      .withColumn(\"a\", array('v1))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.element COMMENT 'a comment'\")\n        },\n        \"DELTA_UNSUPPORTED_COMMENT_MAP_ARRAY\",\n        parameters = Map(\"fieldPath\" -> \"a.element\")\n      )\n    }\n  }\n\n  ddlTest(\"RENAME COLUMN - (unsupported) rename key/value of a MapType\") {\n    val df = Seq((1, 1), (2, 2)).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", map('v1, 'v2))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName RENAME COLUMN a.key TO key2\")\n        },\n        \"INVALID_FIELD_NAME\",\n        parameters = Map(\n          \"fieldName\" -> \"`a`.`key2`\",\n          \"path\" -> \"`a`\"\n        )\n      )\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName RENAME COLUMN a.value TO value2\")\n        },\n        \"INVALID_FIELD_NAME\",\n        parameters = Map(\n          \"fieldName\" -> \"`a`.`value2`\",\n          \"path\" -> \"`a`\"\n        )\n      )\n    }\n  }\n\n  ddlTest(\"RENAME COLUMN - (unsupported) rename element of an array\") {\n    val df = Seq(1, 2).toDF(\"v1\")\n      .withColumn(\"a\", array('v1))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName RENAME COLUMN a.element TO element2\")\n        },\n        \"INVALID_FIELD_NAME\",\n        parameters = Map(\n          \"fieldName\" -> \"`a`.`element2`\",\n          \"path\" -> \"`a`\"\n        )\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - change name\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n\n      assertNotSupported(s\"ALTER TABLE $tableName CHANGE COLUMN v2 v3 string\")\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - incompatible\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 long\")\n        },\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"v1\",\n          \"oldField\" -> \"INT\",\n          \"newField\" -> \"BIGINT\"\n        )\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - incompatible (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.v1 v1 long\")\n        },\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"struct.v1\",\n          \"oldField\" -> \"INT\",\n          \"newField\" -> \"BIGINT\"\n        )\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - (unsupported) change type of key of a MapType\") {\n    val df = Seq((1, 1), (2, 2)).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", map('v1, 'v2))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.key key long\")\n        },\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"a.key\",\n          \"oldField\" -> \"INT NOT NULL\",\n          \"newField\" -> \"BIGINT NOT NULL\"\n        )\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - (unsupported) change type of value of a MapType\") {\n    val df = Seq((1, 1), (2, 2)).toDF(\"v1\", \"v2\")\n      .withColumn(\"a\", map('v1, 'v2))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.value value long\")\n        },\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"a.value\",\n          \"oldField\" -> \"INT\",\n          \"newField\" -> \"BIGINT\"\n        )\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - (unsupported) change type of element of an ArrayType\") {\n    val df = Seq(1).toDF(\"v1\")\n      .withColumn(\"a\", array('v1))\n    withDeltaTable(df) { tableName =>\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.element element long\")\n        },\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"a.element\",\n          \"oldField\" -> \"INT\",\n          \"newField\" -> \"BIGINT\"\n        )\n      )\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to first\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v2 v2 string FIRST\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v2\", \"string\").add(\"v1\", \"integer\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(String, Int)],\n        (\"a\", 1), (\"b\", 2))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to first (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.v2 v2 string FIRST\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"struct\", new StructType()\n          .add(\"v2\", \"string\").add(\"v1\", \"integer\")))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (String, Int))],\n        (1, \"a\", (\"a\", 1)), (2, \"b\", (\"b\", 2)))\n\n      // Can't change the inner ordering\n      assertNotSupported(s\"ALTER TABLE $tableName CHANGE COLUMN struct struct \" +\n        \"STRUCT<v1:integer, v2:string> FIRST\")\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct struct \" +\n        \"STRUCT<v2:string, v1:integer> FIRST\")\n\n      assertEqual(deltaLog.update().schema, new StructType()\n        .add(\"struct\", new StructType().add(\"v2\", \"string\").add(\"v1\", \"integer\"))\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move a partitioned column to first\") {\n    val df = Seq((1, \"a\", true), (2, \"b\", false)).toDF(\"v1\", \"v2\", \"v3\")\n    withDeltaTable(df, Seq(\"v2\", \"v3\")) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v3 v3 boolean FIRST\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v3\", \"boolean\").add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Boolean, Int, String)],\n        (true, 1, \"a\"), (false, 2, \"b\"))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after some column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer AFTER v2\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v2\", \"string\").add(\"v1\", \"integer\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(String, Int)],\n        (\"a\", 1), (\"b\", 2))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after some column (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.v1 v1 integer AFTER v2\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"struct\", new StructType()\n          .add(\"v2\", \"string\").add(\"v1\", \"integer\")))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (String, Int))],\n        (1, \"a\", (\"a\", 1)), (2, \"b\", (\"b\", 2)))\n\n      // cannot change ordering within the struct\n      assertNotSupported(s\"ALTER TABLE $tableName CHANGE COLUMN struct struct \" +\n        \"STRUCT<v1:integer, v2:string> AFTER v1\")\n\n      // can move the struct itself however\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct struct \" +\n        \"STRUCT<v2:string, v1:integer> AFTER v1\")\n\n      assertEqual(deltaLog.update().schema, new StructType()\n        .add(\"v1\", \"integer\")\n        .add(\"struct\", new StructType().add(\"v2\", \"string\").add(\"v1\", \"integer\"))\n        .add(\"v2\", \"string\"))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after the same column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer AFTER v1\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after the same column (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.v1 v1 integer AFTER v1\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"struct\", new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (Int, String))],\n        (1, \"a\", (1, \"a\")), (2, \"b\", (2, \"b\")))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move a partitioned column to after some column\") {\n    val df = Seq((1, \"a\", true), (2, \"b\", false)).toDF(\"v1\", \"v2\", \"v3\")\n    withDeltaTable(df, Seq(\"v2\", \"v3\")) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v3 v3 boolean AFTER v1\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v3\", \"boolean\").add(\"v2\", \"string\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, Boolean, String)],\n        (1, true, \"a\"), (2, false, \"b\"))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after the last column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer AFTER v2\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v2\", \"string\").add(\"v1\", \"integer\"))\n    }\n  }\n\n  test(\"CHANGE COLUMN - special column names with positions\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"x.x\", \"y.y\")\n    withDeltaTable(df) { tableName =>\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN `x.x` `x.x` integer AFTER `y.y`\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"y.y\", \"string\").add(\"x.x\", \"integer\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(String, Int)],\n        (\"a\", 1), (\"b\", 2))\n    }\n  }\n\n  test(\"CHANGE COLUMN - special column names (nested) with positions\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"x.x\", \"y.y\")\n      .withColumn(\"z.z\", struct(\"`x.x`\", \"`y.y`\"))\n    withDeltaTable(df) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN `z.z`.`x.x` `x.x` integer AFTER `y.y`\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"x.x\", \"integer\").add(\"y.y\", \"string\")\n        .add(\"z.z\", new StructType()\n          .add(\"y.y\", \"string\").add(\"x.x\", \"integer\")))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, (String, Int))],\n        (1, \"a\", (\"a\", 1)), (2, \"b\", (\"b\", 2)))\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after an unknown column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer AFTER unknown\")\n      }\n      checkExceptionMessage(\n        ex,\n        \"Missing field unknown\",\n        \"Couldn't resolve positional argument AFTER unknown\",\n        \"A column, variable, or function parameter with name `unknown` cannot be resolved\")\n    }\n  }\n\n  test(\"CHANGE COLUMN - move to after an unknown column (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.v1 v1 integer AFTER unknown\")\n      }\n      checkExceptionMessage(\n        ex,\n        \"Missing field struct.unknown\",\n        \"Couldn't resolve positional argument AFTER unknown\",\n        \"A column, variable, or function parameter with name `struct`.`unknown` cannot be resolved\")\n    }\n  }\n\n  test(\"CHANGE COLUMN - complex types nullability tests\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n      // not supported to tighten nullabilities.\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN s s STRUCT<v1:int, v2:string NOT NULL>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN a a \" +\n          \"ARRAY<STRUCT<v1:int, v2:string NOT NULL>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int, v2:string NOT NULL>, STRUCT<v1:int, v2:string>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int, v2:string NOT NULL>>\")\n\n      // not supported to add not-null columns.\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN s s \" +\n          \"STRUCT<v1:int, v2:string, sv3:long, sv4:long NOT NULL>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN a a \" +\n          \"ARRAY<STRUCT<v1:int, v2:string, av3:long, av4:long NOT NULL>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int, v2:string, mkv3:long, mkv4:long NOT NULL>, \" +\n          \"STRUCT<v1:int, v2:string, mvv3:long>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int, v2:string, mkv3:long>, \" +\n          \"STRUCT<v1:int, v2:string, mvv3:long, mvv4:long NOT NULL>>\")\n    }\n  }\n\n  test(\"CHANGE COLUMN - (unsupported) change nullability of map key/value and array element\") {\n    val df = Seq((1, 1), (2, 2))\n      .toDF(\"key\", \"value\")\n      .withColumn(\"m\", map(col(\"key\"), col(\"value\")))\n      .withColumn(\"a\", array(col(\"value\")))\n\n    withDeltaTable(df) { tableName =>\n      val schema = spark.read.table(tableName).schema\n      assert(schema(\"m\").dataType ===\n        MapType(IntegerType, IntegerType, valueContainsNull = true))\n      assert(schema(\"a\").dataType ===\n        ArrayType(IntegerType, containsNull = true))\n\n      // No-op actions are allowed - map keys are always non-nullable.\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN m.key SET NOT NULL\")\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN m.value DROP NOT NULL\")\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN a.element DROP NOT NULL\")\n\n      // Changing the nullability of map/array fields is not allowed.\n      var statement = s\"ALTER TABLE $tableName CHANGE COLUMN m.key DROP NOT NULL\"\n      checkError(\n        intercept[AnalysisException] { sql(statement) },\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"m.key\",\n          \"oldField\" -> \"INT NOT NULL\",\n          \"newField\" -> \"INT\"\n        )\n      )\n\n      statement = s\"ALTER TABLE $tableName CHANGE COLUMN m.value SET NOT NULL\"\n      checkError(\n        intercept[AnalysisException] { sql(statement) },\n        \"_LEGACY_ERROR_TEMP_2330\",\n        parameters = Map(\n          \"fieldName\" -> \"m.value\"\n        ),\n        context = ExpectedContext(statement, 0, statement.length - 1)\n      )\n\n      statement = s\"ALTER TABLE $tableName CHANGE COLUMN a.element SET NOT NULL\"\n      checkError(\n        intercept[AnalysisException] { sql(statement) },\n        \"_LEGACY_ERROR_TEMP_2330\",\n        parameters = Map(\n          \"fieldName\" -> \"a.element\"\n        ),\n        context = ExpectedContext(statement, 0, statement.length - 1)\n      )\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - set comment on a varchar column\") {\n    withDeltaTable(schema = \"v varchar(1)\") { tableName =>\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v COMMENT 'test comment'\")\n      val expectedResult = Row(\"v\", \"string\", \"test comment\") :: Nil\n      checkAnswer(\n        sql(s\"DESCRIBE $tableName\").filter(\"col_name = 'v'\"),\n        expectedResult)\n      checkColType(spark.table(tableName).schema.head, VarcharType(1))\n      val e = intercept[DeltaInvariantViolationException] {\n        sql(s\"INSERT into $tableName values ('12')\")\n      }\n      assert(e.getMessage.contains(\"Value \\\"12\\\" exceeds char/varchar type length limitation. \" +\n        \"Failed check: ((v IS NULL) OR (length(v) <= 1))\"))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - set comment on a char column\") {\n    withDeltaTable(schema = \"v char(1)\") { tableName =>\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v COMMENT 'test comment'\")\n      val expectedResult = Row(\"v\", \"string\", \"test comment\") :: Nil\n      checkAnswer(\n        sql(s\"DESCRIBE $tableName\").filter(\"col_name = 'v'\"),\n        expectedResult)\n      checkColType(spark.table(tableName).schema.head, CharType(1))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - set comment on a array/map/struct<varchar> column\") {\n    val schema = \"\"\"\n      |arr_c array<char(1)>,\n      |map_cc map<char(1), char(1)>,\n      |map_sc map<string, char(1)>,\n      |map_cs map<char(1), string>,\n      |struct_c struct<v: char(1)>,\n      |arr_v array<varchar(1)>,\n      |map_vv map<varchar(1), varchar(1)>,\n      |map_sv map<string, varchar(1)>,\n      |map_vs map<varchar(1), string>,\n      |struct_v struct<v: varchar(1)>\"\"\".stripMargin\n    def testCommentOnVarcharInContainer(\n      colName: String,\n      expectedType: String,\n      goodInsertValue: String,\n      badInsertValue: String\n    ): Unit = {\n      withDeltaTable(schema = schema) { tableName =>\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN $colName COMMENT 'test comment'\")\n        val expectedResult = Row(colName, expectedType, \"test comment\") :: Nil\n        checkAnswer(\n          sql(s\"DESCRIBE $tableName\").filter(s\"col_name = '$colName'\"),\n          expectedResult)\n        sql(s\"INSERT into $tableName($colName) values ($goodInsertValue)\")\n        val e = intercept[DeltaInvariantViolationException] {\n          sql(s\"INSERT into $tableName($colName) values ($badInsertValue)\")\n        }\n        assert(e.getMessage.contains(\"exceeds char/varchar type length limitation\"))\n      }\n    }\n    testCommentOnVarcharInContainer(\n      colName = \"arr_c\",\n      expectedType = \"array<string>\",\n      goodInsertValue = \"array('1')\",\n      badInsertValue = \"array('12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"map_cc\",\n      expectedType = \"map<string,string>\",\n      goodInsertValue = \"map('1', '1')\",\n      badInsertValue = \"map('12', '12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"map_sc\",\n      expectedType = \"map<string,string>\",\n      goodInsertValue = \"map('123', '1')\",\n      badInsertValue = \"map('123', '12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"map_cs\",\n      expectedType = \"map<string,string>\",\n      goodInsertValue = \"map('1', '123')\",\n      badInsertValue = \"map('12', '123')\")\n    testCommentOnVarcharInContainer(\n      colName = \"struct_c\",\n      expectedType = \"struct<v:string>\",\n      goodInsertValue = \"named_struct('v', '1')\",\n      badInsertValue = \"named_struct('v', '12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"arr_v\",\n      expectedType = \"array<string>\",\n      goodInsertValue = \"array('1')\",\n      badInsertValue = \"array('12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"map_vv\",\n      expectedType = \"map<string,string>\",\n      goodInsertValue = \"map('1', '1')\",\n      badInsertValue = \"map('12', '12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"map_sv\",\n      expectedType = \"map<string,string>\",\n      goodInsertValue = \"map('123', '1')\",\n      badInsertValue = \"map('123', '12')\")\n    testCommentOnVarcharInContainer(\n      colName = \"map_vs\",\n      expectedType = \"map<string,string>\",\n      goodInsertValue = \"map('1', '123')\",\n      badInsertValue = \"map('12', '123')\")\n    testCommentOnVarcharInContainer(\n      colName = \"struct_v\",\n      expectedType = \"struct<v:string>\",\n      goodInsertValue = \"named_struct('v', '1')\",\n      badInsertValue = \"named_struct('v', '12')\")\n  }\n\n  ddlTest(\"CHANGE COLUMN - set a default value for a varchar column\") {\n    withDeltaTable(schema = \"v varchar(1)\") { tableName =>\n      sql(s\"ALTER TABLE $tableName \" +\n        s\"SET TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported')\")\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v set default cast('a' as varchar(1))\")\n      val expectedResult = Row(\"v\", \"string\", null) :: Nil\n      checkAnswer(\n        sql(s\"DESCRIBE $tableName\").filter(\"col_name = 'v'\"),\n        expectedResult)\n      checkColType(spark.table(tableName).schema.head, VarcharType(1))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - change name (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN struct.v2 v3 string\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN struct struct \" +\n          \"STRUCT<v1:integer, v3:string>\")\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.v1 v1 integer COMMENT 'a comment'\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"struct\", new StructType()\n          .add(\"v1\", \"integer\", true, \"a comment\").add(\"v2\", \"string\")))\n\n      assertNotSupported(s\"ALTER TABLE $tableName CHANGE COLUMN struct struct \" +\n        \"STRUCT<v1:integer, v2:string COMMENT 'a comment for v2'>\")\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - complex types not supported because behavior is ambiguous\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      .withColumn(\"a\", array(\"s\"))\n      .withColumn(\"m\", map(col(\"s\"), col(\"s\")))\n    withDeltaTable(df) { tableName =>\n      // not supported to add columns\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN s s STRUCT<v1:int, v2:string, sv3:long>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN a a ARRAY<STRUCT<v1:int, v2:string, av3:long>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int, v2:string, mkv3:long>, STRUCT<v1:int, v2:string, mvv3:long>>\")\n\n      // not supported to remove columns.\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN s s STRUCT<v1:int>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN a a ARRAY<STRUCT<v1:int>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int>, STRUCT<v1:int, v2:string>>\")\n      assertNotSupported(\n        s\"ALTER TABLE $tableName CHANGE COLUMN m m \" +\n          \"MAP<STRUCT<v1:int, v2:string>, STRUCT<v1:int>>\")\n    }\n  }\n\n  private def checkExceptionMessage(e: AnalysisException, messages: String*): Unit = {\n    assert(messages.exists(e.getMessage.contains), s\"${e.getMessage} did not contain $messages\")\n  }\n\n  test(\"CHANGE COLUMN - move unknown column\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n    withDeltaTable(df) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN unknown unknown string FIRST\")\n      }\n      checkExceptionMessage(\n        ex,\n        \"Missing field unknown\",\n        \"Cannot update missing field unknown\",\n        \"A column, variable, or function parameter with name `unknown` cannot be resolved\")\n    }\n  }\n\n  test(\"CHANGE COLUMN - move unknown column (nested)\") {\n    val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n      .withColumn(\"struct\", struct(\"v1\", \"v2\"))\n    withDeltaTable(df) { tableName =>\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN struct.unknown unknown string FIRST\")\n      }\n      checkExceptionMessage(\n        ex,\n        \"Missing field struct.unknown\",\n        \"Cannot update missing field struct.unknown\",\n        \"A column, variable, or function parameter with name `struct`.`unknown` cannot be resolved\")\n    }\n  }\n\n  test(\"CHANGE COLUMN - case insensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      withDeltaTable(df) { tableName =>\n\n        val (deltaLog, _) = getDeltaLogWithSnapshot(tableName)\n\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN V1 v1 integer\")\n\n        assertEqual(deltaLog.update().schema, new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n          .add(\"s\", new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 V1 integer\")\n\n        assertEqual(deltaLog.update().schema, new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n          .add(\"s\", new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer AFTER V2\")\n\n        assertEqual(deltaLog.update().schema, new StructType()\n          .add(\"v2\", \"string\").add(\"v1\", \"integer\")\n          .add(\"s\", new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\")))\n\n        // Since the struct doesn't match the case this fails\n        assertNotSupported(\n          s\"ALTER TABLE $tableName CHANGE COLUMN s s struct<V1:integer,v2:string> AFTER V2\")\n\n        sql(\n          s\"ALTER TABLE $tableName CHANGE COLUMN s s struct<v1:integer,v2:string> AFTER V2\")\n\n        assertEqual(deltaLog.update().schema, new StructType()\n          .add(\"v2\", \"string\")\n          .add(\"s\", new StructType().add(\"v1\", \"integer\").add(\"v2\", \"string\"))\n          .add(\"v1\", \"integer\"))\n      }\n    }\n  }\n\n  test(\"CHANGE COLUMN - case sensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      val df = Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .withColumn(\"s\", struct(\"v1\", \"v2\"))\n      withDeltaTable(df) { tableName =>\n\n        val ex1 = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN V1 V1 integer\")\n        }\n        checkExceptionMessage(\n          ex1,\n          \"Missing field V1\",\n          \"Cannot update missing field V1\",\n          \"A column, variable, or function parameter with name `V1` cannot be resolved.\")\n\n        val ex2 = intercept[ParseException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 V1 integer\")\n        }\n        assert(ex2.getMessage.contains(\"Renaming column is not supported\"))\n\n        val ex3 = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer AFTER V2\")\n        }\n        checkExceptionMessage(\n          ex2,\n          \"Missing field V2\",\n          \"Couldn't resolve positional argument AFTER V2\",\n          \"Renaming column is not supported in Hive-style ALTER COLUMN, \" +\n            \"please run RENAME COLUMN instead\")\n\n        val ex4 = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN s s struct<V1:integer,v2:string> AFTER v2\")\n        }\n        assert(ex4.getMessage.contains(\"Cannot update\"))\n      }\n    }\n  }\n\n  test(\"CHANGE COLUMN: allow to change change column from char to string type\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, c CHAR(4)) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN c TYPE STRING\")\n      assert(spark.table(\"t\").schema(1).dataType === StringType)\n    }\n  }\n\n  test(\"CHANGE COLUMN: allow to change map key from char to string type\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, m map<CHAR(4), INT>) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN m.key TYPE STRING\")\n      assert(spark.table(\"t\").schema(1).dataType === MapType(StringType, IntegerType))\n    }\n  }\n\n  test(\"CHANGE COLUMN: allow to change map value from char to string type\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, m map<INT, CHAR(4)>) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN m.value TYPE STRING\")\n      assert(spark.table(\"t\").schema(1).dataType === MapType(IntegerType, StringType))\n    }\n  }\n\n  test(\"CHANGE COLUMN: allow to change array element from char to string type\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, a array<CHAR(4)>) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN a.element TYPE STRING\")\n      assert(spark.table(\"t\").schema(1).dataType === ArrayType(StringType))\n    }\n  }\n\n  private def checkColType(f: StructField, dt: DataType): Unit = {\n    assert(f.dataType == CharVarcharUtils.replaceCharVarcharWithString(dt))\n    assert(CharVarcharUtils.getRawType(f.metadata).contains(dt))\n  }\n\n  test(\"CHANGE COLUMN: allow to change column from char(x) to varchar(y) type x <= y\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, c CHAR(4)) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN c TYPE VARCHAR(4)\")\n      checkColType(spark.table(\"t\").schema(1), VarcharType(4))\n    }\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, c CHAR(4)) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN c TYPE VARCHAR(5)\")\n      checkColType(spark.table(\"t\").schema(1), VarcharType(5))\n    }\n  }\n\n  test(\"CHANGE COLUMN: allow to change column from varchar(x) to varchar(y) type x <= y\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t(i STRING, c VARCHAR(4)) USING delta\")\n      sql(\"ALTER TABLE t CHANGE COLUMN c TYPE VARCHAR(4)\")\n      checkColType(spark.table(\"t\").schema(1), VarcharType(4))\n      sql(\"ALTER TABLE t CHANGE COLUMN c TYPE VARCHAR(5)\")\n      checkColType(spark.table(\"t\").schema(1), VarcharType(5))\n    }\n  }\n\n  for (charVarcharMitigationDisabled <- BOOLEAN_DOMAIN)\n  test(s\"CHANGE COLUMN: allow change from char(x) to string type \" +\n    s\"[charVarcharMitigationDisabled: $charVarcharMitigationDisabled]\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_BYPASS_CHARVARCHAR_TO_STRING_FIX.key ->\n        charVarcharMitigationDisabled.toString) {\n      withTable(\"t\") {\n        sql(\"CREATE TABLE t(i VARCHAR(4)) USING delta\")\n        sql(\"ALTER TABLE t CHANGE COLUMN i TYPE STRING\")\n        val col = spark.table(\"t\").schema.head\n        assert(col.dataType == StringType)\n        assert(CharVarcharUtils.getRawType(col.metadata).isDefined == charVarcharMitigationDisabled)\n        if (!charVarcharMitigationDisabled) {\n          sql(\"INSERT INTO t VALUES ('123456789')\")\n        }\n      }\n    }\n  }\n}\n\ntrait DeltaAlterTableByNameTests extends DeltaAlterTableTests {\n  import testImplicits._\n\n  override protected def createTable(schema: String, tblProperties: Map[String, String]): String = {\n    val props = tblProperties.map { case (key, value) =>\n      s\"'$key' = '$value'\"\n    }.mkString(\", \")\n    val propsString = if (tblProperties.isEmpty) \"\" else s\" TBLPROPERTIES ($props)\"\n    sql(s\"CREATE TABLE delta_test ($schema) USING delta$propsString\")\n    \"delta_test\"\n  }\n\n  override protected def createTable(df: DataFrame, partitionedBy: Seq[String]): String = {\n    df.write.partitionBy(partitionedBy: _*).format(\"delta\").saveAsTable(\"delta_test\")\n    \"delta_test\"\n  }\n\n  override protected def dropTable(identifier: String): Unit = {\n    sql(s\"DROP TABLE IF EXISTS $identifier\")\n  }\n\n  override protected def getDeltaLogWithSnapshot(identifier: String): (DeltaLog, Snapshot) = {\n    DeltaLog.forTableWithSnapshot(spark, TableIdentifier(identifier))\n  }\n\n  test(\"ADD COLUMNS - external table\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        val path = dir.getCanonicalPath\n        Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n          .write\n          .format(\"delta\")\n          .option(\"path\", path)\n          .saveAsTable(\"delta_test\")\n\n        checkDatasetUnorderly(\n          spark.table(\"delta_test\").as[(Int, String)],\n          (1, \"a\"), (2, \"b\"))\n\n        sql(\"ALTER TABLE delta_test ADD COLUMNS (v3 long, v4 double)\")\n\n        val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, path)\n        assertEqual(snapshot.schema, new StructType()\n          .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n          .add(\"v3\", \"long\").add(\"v4\", \"double\"))\n\n        checkDatasetUnorderly(\n          spark.table(\"delta_test\").as[(Int, String, Option[Long], Option[Double])],\n          (1, \"a\", None, None), (2, \"b\", None, None))\n        checkDatasetUnorderly(\n          spark.read.format(\"delta\").load(path).as[(Int, String, Option[Long], Option[Double])],\n          (1, \"a\", None, None), (2, \"b\", None, None))\n      }\n    }\n  }\n\n  // LOCATION tests do not make sense for by path access\n  testQuietly(\"SET LOCATION\") {\n    withTable(\"delta_table\") {\n      spark.range(1).write.format(\"delta\").saveAsTable(\"delta_table\")\n      val catalog = spark.sessionState.catalog\n      val table = catalog.getTableMetadata(TableIdentifier(tableName = \"delta_table\"))\n      val oldLocation = table.location.toString\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n        spark.range(1, 2).write.format(\"delta\").save(path)\n        checkAnswer(spark.table(\"delta_table\"), Seq(Row(0)))\n        sql(s\"alter table delta_table set location '$path'\")\n        checkAnswer(spark.table(\"delta_table\"), Seq(Row(1)))\n      }\n      Utils.deleteRecursively(new File(oldLocation.stripPrefix(\"file:\")))\n    }\n  }\n\n  testQuietly(\"SET LOCATION: external delta table\") {\n    withTable(\"delta_table\") {\n      withTempDir { oldDir =>\n        spark.range(1).write.format(\"delta\").save(oldDir.getCanonicalPath)\n        sql(s\"CREATE TABLE delta_table USING delta LOCATION '${oldDir.getCanonicalPath}'\")\n        withTempDir { dir =>\n          val path = dir.getCanonicalPath\n          spark.range(1, 2).write.format(\"delta\").save(path)\n          checkAnswer(spark.table(\"delta_table\"), Seq(Row(0)))\n          sql(s\"alter table delta_table set location '$path'\")\n          checkAnswer(spark.table(\"delta_table\"), Seq(Row(1)))\n        }\n      }\n    }\n  }\n\n  test(\n      \"SET LOCATION - negative cases\") {\n    withTable(\"delta_table\") {\n      spark.range(1).write.format(\"delta\").saveAsTable(\"delta_table\")\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n        val catalog = spark.sessionState.catalog\n        val table = catalog.getTableMetadata(TableIdentifier(tableName = \"delta_table\"))\n        val oldLocation = table.location.toString\n\n        // new location is not a delta table\n        var e = intercept[AnalysisException] {\n          sql(s\"alter table delta_table set location '$path'\")\n        }\n        assert(e.getMessage.contains(\"not a Delta table\"))\n\n        Seq(\"1\").toDF(\"id\").write.format(\"delta\").save(path)\n\n        // set location on specific partitions\n        e = intercept[AnalysisException] {\n          sql(s\"alter table delta_table partition (id = 1) set location '$path'\")\n        }\n        assert(Seq(\"partition\", \"not support\").forall(e.getMessage.contains))\n\n        // schema mismatch\n        e = intercept[AnalysisException] {\n          sql(s\"alter table delta_table set location '$path'\")\n        }\n        assert(e.getMessage.contains(\"different than the current table schema\"))\n\n        withSQLConf(DeltaSQLConf.DELTA_ALTER_LOCATION_BYPASS_SCHEMA_CHECK.key -> \"true\") {\n          checkAnswer(spark.table(\"delta_table\"), Seq(Row(0)))\n          // now we can bypass the schema mismatch check\n          sql(s\"alter table delta_table set location '$path'\")\n          checkAnswer(spark.table(\"delta_table\"), Seq(Row(\"1\")))\n        }\n        Utils.deleteRecursively(new File(oldLocation.stripPrefix(\"file:\")))\n      }\n    }\n  }\n}\n\n/**\n * For ByPath tests, we select a test case per ALTER TABLE command to simply test identifier\n * resolution.\n */\ntrait DeltaAlterTableByPathTests extends DeltaAlterTableTestBase {\n  override protected def createTable(schema: String, tblProperties: Map[String, String]): String = {\n      val tmpDir = Utils.createTempDir().getCanonicalPath\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tmpDir)\n      // This is a path-based table so we don't need to pass the catalogTable here\n      val txn = deltaLog.startTransaction(None, Some(snapshot))\n      val metadata = Metadata(\n        schemaString = StructType.fromDDL(schema).json,\n        configuration = tblProperties)\n      txn.commit(metadata :: Nil, DeltaOperations.ManualUpdate)\n      s\"delta.`$tmpDir`\"\n  }\n\n  override protected def createTable(df: DataFrame, partitionedBy: Seq[String]): String = {\n    val tmpDir = Utils.createTempDir().getCanonicalPath\n    df.write.format(\"delta\").partitionBy(partitionedBy: _*).save(tmpDir)\n    s\"delta.`$tmpDir`\"\n  }\n\n  override protected def dropTable(identifier: String): Unit = {\n    Utils.deleteRecursively(new File(identifier.stripPrefix(\"delta.`\").stripSuffix(\"`\")))\n  }\n\n  override protected def getDeltaLogWithSnapshot(identifier: String): (DeltaLog, Snapshot) = {\n    DeltaLog.forTableWithSnapshot(spark, identifier.stripPrefix(\"delta.`\").stripSuffix(\"`\"))\n  }\n\n  override protected def ddlTest(testName: String)(f: => Unit): Unit = {\n    super.ddlTest(testName)(f)\n\n    testQuietly(testName + \" with delta database\") {\n      withDatabase(\"delta\") {\n        spark.sql(\"CREATE DATABASE delta\")\n        f\n      }\n    }\n  }\n\n  import testImplicits._\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - simple\") {\n    withDeltaTable(\"v1 int, v2 string\") { tableName =>\n\n      sql(s\"\"\"\n             |ALTER TABLE $tableName\n             |SET TBLPROPERTIES (\n             |  'delta.logRetentionDuration' = '2 weeks',\n             |  'delta.checkpointInterval' = '20',\n             |  'key' = 'value'\n             |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot1) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.logRetentionDuration\" -> \"2 weeks\",\n        \"delta.checkpointInterval\" -> \"20\",\n        \"key\" -> \"value\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot1.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot1.metadata) == 20)\n\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('delta.checkpointInterval', 'key')\")\n\n      val snapshot2 = deltaLog.update()\n      assertEqual(snapshot2.metadata.configuration,\n        Map(\"delta.logRetentionDuration\" -> \"2 weeks\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot2.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot2.metadata) ==\n        CHECKPOINT_INTERVAL.fromString(CHECKPOINT_INTERVAL.defaultValue))\n    }\n  }\n\n  ddlTest(\"ADD COLUMNS - simple\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String)],\n        (1, \"a\"), (2, \"b\"))\n\n      sql(s\"ALTER TABLE $tableName ADD COLUMNS (v3 long, v4 double)\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\").add(\"v2\", \"string\")\n        .add(\"v3\", \"long\").add(\"v4\", \"double\"))\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[(Int, String, Option[Long], Option[Double])],\n        (1, \"a\", None, None), (2, \"b\", None, None))\n    }\n  }\n\n  ddlTest(\"CHANGE COLUMN - add a comment\") {\n    withDeltaTable(Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")) { tableName =>\n\n      sql(s\"ALTER TABLE $tableName CHANGE COLUMN v1 v1 integer COMMENT 'a comment'\")\n\n      val (deltaLog, snapshot) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot.schema, new StructType()\n        .add(\"v1\", \"integer\", true, \"a comment\").add(\"v2\", \"string\"))\n    }\n  }\n\n  test(\"SET LOCATION is not supported for path based tables\") {\n    val df = spark.range(1).toDF()\n    withDeltaTable(df) { identifier =>\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n        val e = intercept[DeltaAnalysisException] {\n          sql(s\"alter table $identifier set location '$path'\")\n        }\n        assert(e.getErrorClass == \"DELTA_CANNOT_SET_LOCATION_ON_PATH_IDENTIFIER\")\n        assert(e.getSqlState == \"42613\")\n        assert(e.getMessage == \"[DELTA_CANNOT_SET_LOCATION_ON_PATH_IDENTIFIER] \" +\n          \"Cannot change the location of a path based table.\")\n      }\n    }\n  }\n}\n\nclass DeltaAlterTableByNameSuite\n  extends DeltaAlterTableByNameTests\n  with DeltaSQLCommandTest {\n\n  ddlTest(\"SET/UNSET TBLPROPERTIES - unset non-existent config value should still\" +\n    \"unset the config if key matches\") {\n    val props = Map(\n      \"delta.randomizeFilePrefixes\" -> \"true\",\n      \"delta.randomPrefixLength\" -> \"5\",\n      \"key\" -> \"value\"\n    )\n    withDeltaTable(\"v1 int, v2 string\", props) { tableName =>\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('delta.randomizeFilePrefixes', 'kEy')\")\n\n      val (deltaLog, snapshot1) = getDeltaLogWithSnapshot(tableName)\n      assertEqual(snapshot1.metadata.configuration, Map(\n        \"delta.randomPrefixLength\" -> \"5\",\n        \"key\" -> \"value\"))\n\n      sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES IF EXISTS \" +\n        \"('delta.randomizeFilePrefixes', 'kEy')\")\n\n      val snapshot2 = deltaLog.update()\n      assertEqual(snapshot2.metadata.configuration,\n        Map(\"delta.randomPrefixLength\" -> \"5\", \"key\" -> \"value\"))\n    }\n  }\n\n}\n\nclass DeltaAlterTableByPathSuite extends DeltaAlterTableByPathTests with DeltaSQLCommandTest\n  with DeltaAlterTableReplaceTests\n\n\ntrait DeltaAlterTableColumnMappingSelectedTests extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests = Seq(\n    \"ADD COLUMNS into complex types - Array\",\n    \"CHANGE COLUMN - move to first (nested)\",\n    \"CHANGE COLUMN - case insensitive\")\n}\n\nclass DeltaAlterTableByNameIdColumnMappingSuite extends DeltaAlterTableByNameSuite\n  with DeltaColumnMappingEnableIdMode\n  with DeltaAlterTableColumnMappingSelectedTests\n\nclass DeltaAlterTableByPathIdColumnMappingSuite extends DeltaAlterTableByPathSuite\n  with DeltaColumnMappingEnableIdMode\n  with DeltaAlterTableColumnMappingSelectedTests\n\nclass DeltaAlterTableByNameNameColumnMappingSuite extends DeltaAlterTableByNameSuite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaAlterTableColumnMappingSelectedTests\n\nclass DeltaAlterTableByPathNameColumnMappingSuite extends DeltaAlterTableByPathSuite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaAlterTableColumnMappingSelectedTests\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaArbitraryColumnNameSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.JavaConverters._\n\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types.{ArrayType, IntegerType, MapType, StringType, StructType}\n\ntrait DeltaArbitraryColumnNameSuiteBase extends DeltaColumnMappingSuiteUtils {\n\n  protected val simpleNestedSchema = new StructType()\n    .add(\"a\", StringType, true)\n    .add(\"b\",\n      new StructType()\n        .add(\"c\", StringType, true)\n        .add(\"d\", IntegerType, true))\n    .add(\"map\", MapType(StringType, StringType), true)\n    .add(\"arr\", ArrayType(IntegerType), true)\n\n  protected val simpleNestedSchemaWithDuplicatedNestedColumnName = new StructType()\n    .add(\"a\",\n      new StructType()\n        .add(\"c\", StringType, true)\n        .add(\"d\", IntegerType, true), true)\n    .add(\"b\",\n      new StructType()\n        .add(\"c\", StringType, true)\n        .add(\"d\", IntegerType, true), true)\n    .add(\"map\", MapType(StringType, StringType), true)\n    .add(\"arr\", ArrayType(IntegerType), true)\n\n  protected val nestedSchema = new StructType()\n    .add(colName(\"a\"), StringType, true)\n    .add(colName(\"b\"),\n      new StructType()\n        .add(colName(\"c\"), StringType, true)\n        .add(colName(\"d\"), IntegerType, true))\n    .add(colName(\"map\"), MapType(StringType, StringType), true)\n    .add(colName(\"arr\"), ArrayType(IntegerType), true)\n\n  protected def simpleNestedData =\n    spark.createDataFrame(\n      Seq(\n        Row(\"str1\", Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11)),\n        Row(\"str2\", Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22))).asJava,\n      simpleNestedSchema)\n\n  protected def simpleNestedDataWithDuplicatedNestedColumnName =\n    spark.createDataFrame(\n      Seq(\n        Row(Row(\"str1\", 1), Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11)),\n        Row(Row(\"str2\", 2), Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22))).asJava,\n      simpleNestedSchemaWithDuplicatedNestedColumnName)\n\n  protected def nestedData =\n    spark.createDataFrame(\n      Seq(\n        Row(\"str1\", Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11)),\n        Row(\"str2\", Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22))).asJava,\n      nestedSchema)\n\n  // TODO: Refactor DeltaColumnMappingSuite and consolidate these table creation methods between\n  // the two suites.\n  protected def createTableWithSQLCreateOrReplaceAPI(\n      tableName: String,\n      data: DataFrame,\n      props: Map[String, String] = Map.empty,\n      partCols: Seq[String] = Nil): Unit = {\n    withTable(\"source\") {\n      createTableWithDataFrameWriterV2API(\n        \"source\",\n        data,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode(props)))\n\n      spark.sql(\n        s\"\"\"\n           |CREATE OR REPLACE TABLE $tableName\n           |USING DELTA\n           |${partitionStmt(partCols)}\n           |${propString(props)}\n           |AS SELECT * FROM source\n           |\"\"\".stripMargin)\n    }\n  }\n\n  protected def createTableWithSQLAPI(\n      tableName: String,\n      data: DataFrame,\n      props: Map[String, String] = Map.empty,\n      partCols: Seq[String] = Nil): Unit = {\n    withTable(\"source\") {\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE $tableName (${data.schema.toDDL})\n           |USING DELTA\n           |${partitionStmt(partCols)}\n           |${propString(props)}\n           |\"\"\".stripMargin)\n      data.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n    }\n  }\n\n  protected def createTableWithCTAS(\n      tableName: String,\n      data: DataFrame,\n      props: Map[String, String] = Map.empty,\n      partCols: Seq[String] = Nil): Unit = {\n    withTable(\"source\") {\n      createTableWithDataFrameWriterV2API(\n        \"source\",\n        data,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode(props)))\n\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE $tableName\n           |USING DELTA\n           |${partitionStmt(partCols)}\n           |${propString(props)}\n           |AS SELECT * FROM source\n           |\"\"\".stripMargin)\n    }\n  }\n\n  protected def createTableWithDataFrameAPI(\n      tableName: String,\n      data: DataFrame,\n      props: Map[String, String] = Map.empty,\n      partCols: Seq[String]): Unit = {\n    val sqlConfs = props.map { case (key, value) =>\n      \"spark.databricks.delta.properties.defaults.\" + key.stripPrefix(\"delta.\") -> value\n    }\n    withSQLConf(sqlConfs.toList: _*) {\n      if (partCols.nonEmpty) {\n        data.write.format(\"delta\")\n          .partitionBy(partCols.map(name => s\"`$name`\"): _*).saveAsTable(tableName)\n      } else {\n        data.write.format(\"delta\").saveAsTable(tableName)\n      }\n    }\n  }\n\n  protected def createTableWithDataFrameWriterV2API(\n      tableName: String,\n      data: DataFrame,\n      props: Map[String, String] = Map.empty,\n      partCols: Seq[String] = Seq.empty): Unit = {\n\n    val writer = data.writeTo(tableName).using(\"delta\")\n    props.foreach(prop => writer.tableProperty(prop._1, prop._2))\n    val partColumns = partCols.map(name => expr(s\"`$name`\"))\n    if (partCols.nonEmpty) writer.partitionedBy(partColumns.head, partColumns.tail: _*)\n    writer.create()\n  }\n\n  protected def assertException(message: String)(block: => Unit): Unit = {\n    val e = intercept[Exception](block)\n\n    assert(e.getMessage.contains(message))\n  }\n\n  protected def assertExceptionOneOf(messages: Seq[String])(block: => Unit): Unit = {\n    val e = intercept[Exception](block)\n    assert(messages.exists(x => e.getMessage.contains(x)))\n  }\n}\n\nclass DeltaArbitraryColumnNameSuite extends QueryTest\n  with DeltaArbitraryColumnNameSuiteBase\n  with GivenWhenThen {\n\n  private def testCreateTable(): Unit = {\n    val allProps = supportedModes\n      .map(mode => Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode)) ++\n      // none mode\n      Seq(Map.empty[String, String])\n\n    def withProps(props: Map[String, String])(createFunc: => Unit) = {\n      withTable(\"t1\") {\n        if (mode(props) != \"none\") {\n          createFunc\n          checkAnswer(spark.table(\"t1\"), nestedData)\n        } else {\n          val e = intercept[AnalysisException] {\n            createFunc\n          }\n          assert(e.getMessage.contains(\"Found invalid character(s)\"))\n        }\n      }\n    }\n\n    allProps.foreach { props =>\n      withProps(props) {\n        Given(s\"with SQL CREATE TABLE API, mode ${mode(props)}\")\n        createTableWithSQLAPI(\"t1\",\n          nestedData,\n          props,\n          partCols = Seq(colName(\"a\")))\n      }\n\n      withProps(props) {\n        Given(s\"with SQL CTAS API, mode ${mode(props)}\")\n        createTableWithCTAS(\"t1\",\n          nestedData,\n          props,\n          partCols = Seq(colName(\"a\"))\n        )\n      }\n\n      withProps(props) {\n        Given(s\"with SQL CREATE OR REPLACE TABLE API, mode ${mode(props)}\")\n        createTableWithSQLCreateOrReplaceAPI(\"t1\",\n          nestedData,\n          props,\n          partCols = Seq(colName(\"a\")))\n      }\n\n      withProps(props) {\n        Given(s\"with DataFrame API, mode ${mode(props)}\")\n        createTableWithDataFrameAPI(\"t1\",\n          nestedData,\n          props,\n          partCols = Seq(colName(\"a\")))\n      }\n\n      withProps(props) {\n        Given(s\"with DataFrameWriterV2 API, mode ${mode(props)}\")\n        createTableWithDataFrameWriterV2API(\"t1\",\n          nestedData,\n          props,\n          // TODO: make DataFrameWriterV2 work with arbitrary partition column names\n          partCols = Seq.empty)\n      }\n    }\n  }\n\n  test(\"create table\") {\n    testCreateTable()\n  }\n\n  testColumnMapping(\"schema evolution and simple query\") { mode =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        nestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        partCols = Seq(colName(\"a\"))\n      )\n      val newNestedData =\n        spark.createDataFrame(\n          Seq(Row(\"str3\", Row(\"str1.3\", 3), Map(\"k3\" -> \"v3\"), Array(3, 33), \"new value\")).asJava,\n          nestedSchema.add(colName(\"e\"), StringType))\n      newNestedData.write.format(\"delta\")\n        .option(\"mergeSchema\", \"true\")\n        .mode(\"append\").saveAsTable(\"t1\")\n      checkAnswer(\n        spark.table(\"t1\"),\n        Seq(\n          Row(\"str1\", Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11), null),\n          Row(\"str2\", Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22), null),\n          Row(\"str3\", Row(\"str1.3\", 3), Map(\"k3\" -> \"v3\"), Array(3, 33), \"new value\")))\n\n      val colA = colName(\"a\")\n      val colB = colName(\"b\")\n      val colC = colName(\"c\")\n      val colD = colName(\"d\")\n      checkAnswer(\n        spark.table(\"t1\")\n          .where(s\"`$colA` > 'str1'\")\n          .where(s\"`$colB`.`$colD` < 3\")\n          .select(s\"`$colB`.`$colC`\"),\n        Row(\"str1.2\"))\n    }\n  }\n\n  testColumnMapping(\"alter table add and replace columns\") { mode =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        nestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        partCols = Seq(colName(\"a\"))\n      )\n      spark.sql(s\"alter table t1 add columns (`${colName(\"e\")}` string)\")\n      spark.sql(\"insert into t1 \" +\n        \"values ('str3', struct('str1.3', 3), map('k3', 'v3'), array(3, 33), 'new value')\")\n\n      checkAnswer(\n        spark.table(\"t1\"),\n        Seq(\n          Row(\"str1\", Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11), null),\n          Row(\"str2\", Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22), null),\n          Row(\"str3\", Row(\"str1.3\", 3), Map(\"k3\" -> \"v3\"), Array(3, 33), \"new value\")))\n\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCDCColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader._\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaColumnMappingSelectedTestMixin\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{DataFrame, Row}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types._\n\ntrait DeltaCDCColumnMappingSuiteBase extends DeltaCDCSuiteBase\n  with DeltaColumnMappingTestUtils\n  with DeltaColumnMappingSelectedTestMixin {\n\n  import testImplicits._\n\n  implicit class DataFrameDropCDCFields(df: DataFrame) {\n    def dropCDCFields: DataFrame =\n      df.drop(CDC_COMMIT_TIMESTAMP)\n      .drop(CDC_TYPE_COLUMN_NAME)\n      .drop(CDC_COMMIT_VERSION)\n  }\n\n  test(\"upgrade to column mapping not blocked\") {\n    withTempDir { dir =>\n      setupInitialDeltaTable(dir, upgradeInNameMode = true)\n      implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val v1 = deltaLog.update().version\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v1.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        (0 until 10).map(_.toString).map(i => Row(i, i))\n      )\n    }\n  }\n\n  test(\"add column batch cdc read not blocked\") {\n    withTempDir { dir =>\n      // Set up an initial table with 10 records in schema <id string, value string>\n      setupInitialDeltaTable(dir)\n      implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n      // add column should not be blocked\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` ADD COLUMN (name string)\")\n\n      // write more data\n      writeDeltaData((10 until 15))\n\n      // None of the schema mode should block this use case\n      Seq(BatchCDFSchemaLegacy, BatchCDFSchemaLatest, BatchCDFSchemaEndVersion).foreach { mode =>\n        checkAnswer(\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            EndingVersion(deltaLog.update().version.toString),\n            Some(mode)).dropCDCFields,\n          (0 until 10).map(_.toString).toDF(\"id\")\n            .withColumn(\"value\", col(\"id\"))\n            .withColumn(\"name\", lit(null)) union\n            (10 until 15).map(_.toString).toDF(\"id\")\n              .withColumn(\"value\", col(\"id\"))\n              .withColumn(\"name\", col(\"id\")))\n      }\n    }\n  }\n\n  test(\"data type and nullability change batch cdc read blocked\") {\n    withTempDir { dir =>\n      // Set up an initial table with 10 records in schema <id string, value string>\n      setupInitialDeltaTable(dir)\n      implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val s1 = deltaLog.update()\n      val v1 = s1.version\n\n      // Change the data type of column\n      deltaLog.withNewTransaction { txn =>\n        // id was string\n        val updatedSchema =\n          SchemaMergingUtils.transformColumns(\n            StructType.fromDDL(\"id INT, value STRING\")) { (_, field, _) =>\n            val refField = s1.metadata.schema(field.name)\n            field.copy(metadata = refField.metadata)\n          }\n        txn.commit(s1.metadata.copy(schemaString = updatedSchema.json) :: Nil, ManualUpdate)\n      }\n      val v2 = deltaLog.update().version\n\n      // write more data in updated schema\n      Seq((10, \"10\")).toDF(\"id\", \"value\")\n        .write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      val v3 = deltaLog.update().version\n\n      // query all changes using latest schema blocked\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v3.toString)).collect()\n      }\n\n      // query using end version also blocked if cross schema change\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          schemaMode = BatchCDFSchemaEndVersion,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v3.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n\n      // query using end version NOT blocked if NOT cross schema change\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v3.toString),\n          EndingVersion(v3.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        Row(10, \"10\") :: Nil\n      )\n\n      val s2 = deltaLog.update()\n\n      // Change nullability unsafely\n      deltaLog.withNewTransaction { txn =>\n        // the schema was nullable, but we want to make it non-nullable\n        val updatedSchema =\n          SchemaMergingUtils.transformColumns(\n            StructType.fromDDL(\"id INT, value string\").asNullable) { (_, field, _) =>\n            val refField = s1.metadata.schema(field.name)\n            field.copy(metadata = refField.metadata, nullable = false)\n          }\n        txn.commit(s2.metadata.copy(schemaString = updatedSchema.json) :: Nil, ManualUpdate)\n      }\n      val v4 = deltaLog.update().version\n\n      // write more data in updated schema\n      Seq((11, \"11\")).toDF(\"id\", \"value\")\n        .write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      val v5 = deltaLog.update().version\n\n      // query changes using latest schema blocked\n      // Note this is not detected as an illegal schema change, but a data violation, because\n      // we attempt to read using latest schema @ v5 (nullable=false) to read some past data @ v3\n      // (nullable=true), which is unsafe.\n      assertBlocked(\n          expectedIncompatSchemaVersion = v3,\n          expectedReadSchemaVersion = v5,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          // v3 is the first version post the data type schema change\n          StartingVersion(v3.toString),\n          EndingVersion(v5.toString)).collect()\n      }\n\n      // query using end version also blocked if cross schema change\n      assertBlocked(\n          expectedIncompatSchemaVersion = v3,\n          expectedReadSchemaVersion = v5,\n          schemaMode = BatchCDFSchemaEndVersion,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v3.toString),\n          EndingVersion(v5.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n\n      // query using end version NOT blocked if NOT cross schema change\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v5.toString),\n          EndingVersion(v5.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        Row(11, \"11\") :: Nil\n      )\n    }\n  }\n\n  test(\"overwrite table with invalid schema change in non-column mapping table is blocked\") {\n    withTempDir { dir =>\n      withColumnMappingConf(\"none\") {\n        // Create table action sequence\n        Seq((1, \"a\")).toDF(\"id\", \"name\").write.format(\"delta\").save(dir.getCanonicalPath)\n        implicit val log: DeltaLog = DeltaLog.forTable(spark, dir)\n        val v1 = log.update().version\n\n        // Overwrite with dropped column\n        Seq(2).toDF(\"id\")\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"overwriteSchema\", \"true\")\n          .save(dir.getCanonicalPath)\n        val v2 = log.update().version\n\n        assertBlocked(\n            expectedIncompatSchemaVersion = v1,\n            expectedReadSchemaVersion = v2,\n            bySchemaChange = false) {\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(v1.toString),\n            EndingVersion(v2.toString),\n            schemaMode = Some(BatchCDFSchemaEndVersion)).collect()\n        }\n\n        // Overwrite with a renamed column\n        Seq(3).toDF(\"id2\")\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"overwriteSchema\", \"true\")\n          .save(dir.getCanonicalPath)\n        val v3 = log.update().version\n\n        assertBlocked(\n            expectedIncompatSchemaVersion = v2,\n            expectedReadSchemaVersion = v3,\n            bySchemaChange = false) {\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(v2.toString),\n            EndingVersion(v3.toString)).collect()\n        }\n      }\n    }\n  }\n\n  test(\"drop column batch cdc read blocked\") {\n    withTempDir { dir =>\n      // Set up an initial table with 10 records in schema <id string, value string>\n      setupInitialDeltaTable(dir)\n      implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val v1 = deltaLog.update().version\n\n      // drop column would cause CDC read to be blocked\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` DROP COLUMN value\")\n      val v2 = deltaLog.update().version\n\n      // write more data\n      writeDeltaData(Seq(10))\n      val v3 = deltaLog.update().version\n\n      // query all changes using latest schema blocked\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v3.toString)).collect()\n      }\n\n      // query just first two versions which have more columns than latest schema is also blocked\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(\"1\")).collect()\n      }\n\n      // query unblocked if force enabled by user\n      withSQLConf(\n        DeltaSQLConf.DELTA_CDF_UNSAFE_BATCH_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES.key -> \"true\") {\n        checkAnswer(\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            EndingVersion(v3.toString)).dropCDCFields,\n          // Note id is dropped because we are using latest schema\n          (0 until 11).map(i => Row(i.toString))\n        )\n      }\n\n      // querying changes using endVersion schema blocked if crossing schema boundary\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          schemaMode = BatchCDFSchemaEndVersion,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v3.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n\n      assertBlocked(\n          expectedIncompatSchemaVersion = v1,\n          expectedReadSchemaVersion = v3,\n          schemaMode = BatchCDFSchemaEndVersion,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v1.toString),\n          EndingVersion(v3.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n\n      // querying changes using endVersion schema NOT blocked if NOT crossing schema boundary\n      // with schema <id, value>\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v1.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        (0 until 10).map(_.toString).map(i => Row(i, i)))\n\n      // with schema <id>\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v3.toString),\n          EndingVersion(v3.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        Row(\"10\") :: Nil\n      )\n\n      // let's add the column back...\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` ADD COLUMN (value string)\")\n      val v4 = deltaLog.update().version\n\n      // write more data\n      writeDeltaData(Seq(11))\n      val v5 = deltaLog.update().version\n\n      // The read is still blocked, even schema @ 0 looks the \"same\" as the latest schema\n      // but the added column now maps to a different physical column.\n      // Note that this bypasses all the schema change actions in between because:\n      // 1. The schema after dropping @ v2 is a subset of the read schema -> this is fine\n      // 2. The schema after adding back @ v4 is the same as latest schema -> this is fine\n      // but our final check against the starting schema would catch it.\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v5,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v5.toString)).collect()\n      }\n\n      // In this case, tho there aren't any read-incompat schema changes in the querying range,\n      // the latest schema is not read-compat with the data files @ v0, so we still block.\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v5,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(\"1\")).collect()\n      }\n    }\n  }\n\n  test(\"rename column batch cdc read blocked\") {\n    withTempDir { dir =>\n      // Set up an initial table with 10 records in schema <id string, value string>\n      setupInitialDeltaTable(dir)\n      implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val v1 = deltaLog.update().version\n\n      // Rename column\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` RENAME COLUMN id TO id2\")\n      val v2 = deltaLog.update().version\n\n      // write more data\n      writeDeltaData(Seq(10))\n      val v3 = deltaLog.update().version\n\n      // query all versions using latest schema blocked\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v3.toString)).collect()\n      }\n\n      // query unblocked if force enabled by user\n      withSQLConf(\n        DeltaSQLConf.DELTA_CDF_UNSAFE_BATCH_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES.key -> \"true\") {\n        val df = cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v3.toString)).dropCDCFields\n        checkAnswer(df, (0 until 11).map(i => Row(i.toString, i.toString)))\n        // Note we serve the batch using the renamed column in the latest schema.\n        assert(df.schema.fieldNames.sameElements(Array(\"id2\", \"value\")))\n      }\n\n      // query just the first few versions using latest schema also blocked\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v3,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(\"1\")).collect()\n      }\n\n      // query using endVersion schema across schema boundary also blocked\n      assertBlocked(\n          expectedIncompatSchemaVersion = 0,\n          expectedReadSchemaVersion = v2,\n          schemaMode = BatchCDFSchemaEndVersion,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v2.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n\n      // query using endVersion schema NOT blocked if NOT crossing schema boundary\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v1.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        (0 until 10).map(_.toString).map(i => Row(i, i))\n      )\n\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v2.toString),\n          EndingVersion(v3.toString),\n          Some(BatchCDFSchemaEndVersion)).dropCDCFields,\n        Row(\"10\", \"10\") :: Nil\n      )\n\n      // Let's rename the column back\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` RENAME COLUMN id2 TO id\")\n      val v4 = deltaLog.update().version\n\n      // write more data\n      writeDeltaData(Seq(11))\n      val v5 = deltaLog.update().version\n\n      // query all changes using latest schema would still block because we crossed an\n      //   intermediary action with a conflicting schema (the first rename).\n      assertBlocked(expectedIncompatSchemaVersion = v2, expectedReadSchemaVersion = v5) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v5.toString)).collect()\n      }\n\n      // query all changes using LATEST schema would NOT block if we exclude the first\n      //   rename back, because the data schemas before that are now consistent with the latest.\n      checkAnswer(\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(v1.toString)).dropCDCFields,\n        (0 until 10).map(_.toString).map(i => Row(i, i)))\n\n      // query using endVersion schema is blocked if we cross schema boundary\n      assertBlocked(\n          expectedIncompatSchemaVersion = v3,\n          expectedReadSchemaVersion = v5,\n          schemaMode = BatchCDFSchemaEndVersion,\n          bySchemaChange = false) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          // v3 just pass the first schema change\n          StartingVersion(v3.toString),\n          EndingVersion(v5.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n\n      // Note how the conflictingVersion is v2 (the first rename), because v1 matches our end\n      // version schema due to renaming back.\n      assertBlocked(\n          expectedIncompatSchemaVersion = v2,\n          expectedReadSchemaVersion = v5,\n          schemaMode = BatchCDFSchemaEndVersion) {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(v1.toString),\n          EndingVersion(v5.toString),\n          Some(BatchCDFSchemaEndVersion)).collect()\n      }\n    }\n  }\n\n  override def runOnlyTests: Seq[String] = Seq(\n    \"changes from table by name\",\n    \"changes from table by path\",\n    \"batch write: append, dynamic partition overwrite + CDF\",\n    // incompatible schema changes & schema mode tests\n    \"add column batch cdc read not blocked\",\n    \"data type and nullability change batch cdc read blocked\",\n    \"drop column batch cdc read blocked\",\n    \"rename column batch cdc read blocked\",\n    \"filters with special characters in name should be pushed down\"\n  )\n\n  protected def assertBlocked(\n      expectedIncompatSchemaVersion: Long,\n      expectedReadSchemaVersion: Long,\n      schemaMode: DeltaBatchCDFSchemaMode = BatchCDFSchemaLegacy,\n      timeTravel: Boolean = false,\n      bySchemaChange: Boolean = true)(f: => Unit)(implicit log: DeltaLog): Unit = {\n    val e = intercept[DeltaUnsupportedOperationException] {\n      f\n    }\n    val (end, readSchemaJson) = if (bySchemaChange) {\n      assert(e.getErrorClass == \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_SCHEMA_CHANGE\")\n      val Seq(_, end, readSchemaJson, readSchemaVersion, incompatibleVersion, _, _, _, _) =\n        e.getMessageParametersArray.toSeq\n      assert(incompatibleVersion.toLong == expectedIncompatSchemaVersion)\n      assert(readSchemaVersion.toLong == expectedReadSchemaVersion)\n      (end, readSchemaJson)\n    } else {\n      assert(e.getErrorClass == \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_DATA_SCHEMA\")\n      val Seq(_, end, readSchemaJson, readSchemaVersion, incompatibleVersion, config) =\n        e.getMessageParametersArray.toSeq\n      assert(incompatibleVersion.toLong == expectedIncompatSchemaVersion)\n      assert(readSchemaVersion.toLong == expectedReadSchemaVersion)\n      assert(config == DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key)\n      (end, readSchemaJson)\n    }\n\n    val latestSnapshot = log.update()\n    schemaMode match {\n      case BatchCDFSchemaLegacy if timeTravel =>\n        // Read using time travelled schema, it can be arbitrary so nothing to check here\n      case BatchCDFSchemaEndVersion =>\n        // Read using end version schema\n        assert(expectedReadSchemaVersion == end.toLong &&\n          log.getSnapshotAt(expectedReadSchemaVersion).schema.json == readSchemaJson)\n      case _ =>\n        // non time-travel legacy mode and latest mode should both read latest schema\n        assert(expectedReadSchemaVersion == latestSnapshot.version &&\n          latestSnapshot.schema.json == readSchemaJson)\n    }\n  }\n\n  /**\n   * Write test delta data to test blocking column mapping for CDC batch queries, it takes a\n   * sequence and write out as a row of strings, assuming the delta log's schema are all strings.\n   */\n  protected def writeDeltaData(\n      data: Seq[Int],\n      userSpecifiedSchema: Option[StructType] = None)(implicit log: DeltaLog): Unit = {\n    val schema = userSpecifiedSchema.getOrElse(log.update().schema)\n    data.foreach { i =>\n      val data = Seq(Row(schema.map(_ => i.toString): _*))\n      spark.createDataFrame(data.asJava, schema)\n        .write.format(\"delta\").mode(\"append\").save(log.dataPath.toString)\n    }\n  }\n\n  /**\n   * Set up initial table data, considering current column mapping mode\n   *\n   * The table contains 10 rows, with schema <id, value> both are string\n   */\n  protected def setupInitialDeltaTable(dir: File, upgradeInNameMode: Boolean = false): Unit = {\n    require(columnMappingModeString != NoMapping.name)\n    val tablePath = dir.getCanonicalPath\n    implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, tablePath)\n\n    if (upgradeInNameMode && columnMappingModeString == NameMapping.name) {\n      // For name mode, we do an upgrade then write to test that behavior as well\n      // init table with 5 versions without column mapping\n      withColumnMappingConf(\"none\") {\n        writeDeltaData((0 until 5), userSpecifiedSchema = Some(\n          new StructType().add(\"id\", StringType, true).add(\"value\", StringType, true)\n        ))\n      }\n      // upgrade to name mode\n      val protocol = deltaLog.snapshot.protocol\n      val (r, w) = if (protocol.supportsReaderFeatures || protocol.supportsWriterFeatures) {\n        (TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION,\n          TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n      } else {\n        (ColumnMappingTableFeature.minReaderVersion, ColumnMappingTableFeature.minWriterVersion)\n      }\n      sql(\n        s\"\"\"\n           |ALTER TABLE delta.`${dir.getCanonicalPath}`\n           |SET TBLPROPERTIES (\n           |  ${DeltaConfigs.COLUMN_MAPPING_MODE.key} = \"name\",\n           |  ${DeltaConfigs.MIN_READER_VERSION.key} = \"$r\",\n           |  ${DeltaConfigs.MIN_WRITER_VERSION.key} = \"$w\")\"\"\".stripMargin)\n      // write more data\n      writeDeltaData((5 until 10))\n    } else {\n      // For id mode and non-upgrade name mode, we could just create a table from scratch\n      withColumnMappingConf(columnMappingModeString) {\n        writeDeltaData((0 until 10), userSpecifiedSchema = Some(\n          new StructType().add(\"id\", StringType, true).add(\"value\", StringType, true)\n        ))\n      }\n    }\n\n    checkAnswer(\n      cdcRead(\n        new TablePath(dir.getCanonicalPath),\n        StartingVersion(\"0\"),\n        EndingVersion(deltaLog.update().version.toString)).dropCDCFields,\n      (0 until 10).map(_.toString).toDF(\"id\").withColumn(\"value\", col(\"id\")))\n  }\n\n  test(\"filters with special characters in name should be pushed down\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      spark.range(end = 10).withColumn(\"id with space\", col(\"id\"))\n          .write.format(\"delta\").saveAsTable(tblName)\n\n      val plans = DeltaTestUtils.withAllPlansCaptured(spark) {\n        val res = cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"1\"))\n          .select(\"id with space\", \"_change_type\")\n          .where(col(\"id with space\") < lit(5))\n\n        assert(res.columns === Seq(\"id with space\", \"_change_type\"))\n        checkAnswer(\n          res,\n          spark.range(end = 5)\n            .withColumn(\"_change_type\", lit(\"insert\")))\n      }\n      assert(plans.map(_.executedPlan).toString\n        .contains(\"PushedFilters: [*IsNotNull(id with space), *LessThan(id with space,5)]\"))\n    }\n  }\n}\n\ntrait DeltaCDCColumnMappingScalaSuiteBase extends DeltaCDCColumnMappingSuiteBase {\n\n  import testImplicits._\n\n  test(\"time travel with batch cdf is disbaled by default\") {\n    withTempDir { dir =>\n      Seq(1).toDF(\"id\").write.format(\"delta\").save(dir.getCanonicalPath)\n      val e = intercept[DeltaAnalysisException] {\n        cdcRead(\n          new TablePath(dir.getCanonicalPath),\n          StartingVersion(\"0\"),\n          EndingVersion(\"1\"),\n          readerOptions = Map(DeltaOptions.VERSION_AS_OF -> \"0\")).collect()\n      }\n      assert(e.getErrorClass == \"DELTA_UNSUPPORTED_TIME_TRAVEL_VIEWS\")\n    }\n  }\n\n  // NOTE: we do not support time travel option with SQL API, so we will just test Scala API suite\n  test(\"cannot specify both time travel options and schema mode\") {\n    withSQLConf(DeltaSQLConf.DELTA_CDF_ALLOW_TIME_TRAVEL_OPTIONS.key -> \"true\") {\n      withTempDir { dir =>\n        Seq(1).toDF(\"id\").write.format(\"delta\").save(dir.getCanonicalPath)\n        val e = intercept[DeltaIllegalArgumentException] {\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            EndingVersion(\"1\"),\n            Some(BatchCDFSchemaEndVersion),\n            readerOptions = Map(DeltaOptions.VERSION_AS_OF -> \"0\")).collect()\n        }\n        assert(e.getMessage.contains(\n          DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key))\n      }\n    }\n  }\n\n  test(\"time travel option is respected\") {\n    withSQLConf(DeltaSQLConf.DELTA_CDF_ALLOW_TIME_TRAVEL_OPTIONS.key -> \"true\") {\n      withTempDir { dir =>\n        // Set up an initial table with 10 records in schema <id string, value string>\n        setupInitialDeltaTable(dir)\n        implicit val deltaLog: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        val v1 = deltaLog.update().version\n\n        // Add a column\n        sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` ADD COLUMN (prop string)\")\n        val v2 = deltaLog.update().version\n\n        // write more data\n        writeDeltaData(Seq(10))\n        val v3 = deltaLog.update().version\n\n        // Rename a column\n        sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` RENAME COLUMN id TO id2\")\n        val v4 = deltaLog.update().version\n\n        // write more data\n        writeDeltaData(Seq(11))\n        val v5 = deltaLog.update().version\n\n        // query changes between version 0 - v1, not crossing schema boundary\n        checkAnswer(\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            EndingVersion(v1.toString),\n            readerOptions = Map(DeltaOptions.VERSION_AS_OF -> v1.toString)).dropCDCFields,\n          (0 until 10).map(_.toString).map(i => Row(i, i)))\n\n        // query across add column, but not cross the rename, not blocked\n        checkAnswer(\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            EndingVersion(v3.toString),\n            // v2 is the add column schema change\n            readerOptions = Map(DeltaOptions.VERSION_AS_OF -> v2.toString)).dropCDCFields,\n          // Note how the first 10 records now misses a column, but it's fine\n          (0 until 10).map(_.toString).map(i => Row(i, i, null)) ++\n            Seq(Row(\"10\", \"10\", \"10\")))\n\n        // query across rename is blocked, if we are still specifying an old version\n        // note it failed at v4, because the initial schema does not conflict with schema @ v2\n        assertBlocked(\n            expectedIncompatSchemaVersion = v4,\n            expectedReadSchemaVersion = v2,\n            timeTravel = true) {\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            // v5 cross the v4 rename column\n            EndingVersion(v5.toString),\n            // v2 is the add column schema change\n            readerOptions = Map(DeltaOptions.VERSION_AS_OF -> v2.toString)).collect()\n        }\n\n        // Even the querying range has no schema change, the data files are still not\n        // compatible with the read schema due to arbitrary time travel.\n        assertBlocked(\n            expectedIncompatSchemaVersion = 0,\n            expectedReadSchemaVersion = v4,\n            timeTravel = true,\n            bySchemaChange = false) {\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(\"0\"),\n            // v1 still uses the schema prior to the rename\n            EndingVersion(v1.toString),\n            // v4 is the rename column change\n            readerOptions = Map(DeltaOptions.VERSION_AS_OF -> v4.toString)).collect()\n        }\n\n        // But without crossing schema change boundary (v4 - v5) using v4's renamed schema,\n        // we can load the batch.\n        checkAnswer(\n          cdcRead(\n            new TablePath(dir.getCanonicalPath),\n            StartingVersion(v4.toString),\n            EndingVersion(v5.toString),\n            readerOptions = Map(DeltaOptions.VERSION_AS_OF -> v4.toString)).dropCDCFields,\n          Seq(Row(\"11\", \"11\", \"11\")))\n      }\n    }\n  }\n}\n\nclass DeltaCDCIdColumnMappingSuite extends DeltaCDCScalaSuite\n  with DeltaCDCColumnMappingScalaSuiteBase\n  with DeltaColumnMappingEnableIdMode\n\nclass DeltaCDCNameColumnMappingSuite extends DeltaCDCScalaSuite\n  with DeltaCDCColumnMappingScalaSuiteBase\n  with DeltaColumnMappingEnableNameMode\n\nclass DeltaCDCSQLIdColumnMappingSuite extends DeltaCDCSQLSuite\n  with DeltaCDCColumnMappingSuiteBase\n  with DeltaColumnMappingEnableIdMode\n\nclass DeltaCDCSQLNameColumnMappingSuite extends DeltaCDCSQLSuite\n  with DeltaCDCColumnMappingSuiteBase\n  with DeltaColumnMappingEnableNameMode\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCDCSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Date\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.modifyCommitTimestamp\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTableUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.util.DateTimeTestUtils._\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types.LongType\n\nclass DeltaCDCSQLSuite extends DeltaCDCSuiteBase with DeltaColumnMappingTestUtils {\n\n  /** Single method to do all kinds of CDC reads */\n  def cdcRead(\n    tblId: TblId,\n    start: Boundary,\n    end: Boundary,\n    schemaMode: Option[DeltaBatchCDFSchemaMode] = Some(BatchCDFSchemaLegacy),\n    // SQL API does not support generic reader options, so it's a noop here\n    readerOptions: Map[String, String] = Map.empty): DataFrame = {\n\n    // Set the batch CDF schema mode using SQL conf if we specified it\n    if (schemaMode.isDefined) {\n      var result: DataFrame = null\n      withSQLConf(DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key ->\n        schemaMode.get.name) {\n        result = cdcRead(tblId, start, end, None, readerOptions)\n      }\n      return result\n    }\n\n    val startPrefix: String = start match {\n      case startingVersion: StartingVersion =>\n        s\"\"\"${startingVersion.value}\"\"\"\n\n      case startingTimestamp: StartingTimestamp =>\n        s\"\"\"'${startingTimestamp.value}'\"\"\"\n\n      case Unbounded =>\n        \"\"\n    }\n    val endPrefix: String = end match {\n      case endingVersion: EndingVersion =>\n        s\"\"\"${endingVersion.value}\"\"\"\n\n      case endingTimestamp: EndingTimestamp =>\n        s\"\"\"'${endingTimestamp.value}'\"\"\"\n\n      case Unbounded =>\n        \"\"\n    }\n    val fnName = tblId match {\n      case _: TablePath =>\n        DeltaTableValueFunctions.CDC_PATH_BASED\n      case _: TableName =>\n        DeltaTableValueFunctions.CDC_NAME_BASED\n      case _ =>\n        throw new IllegalArgumentException(\"No table name or path provided\")\n    }\n\n    if (endPrefix === \"\") {\n      sql(s\"SELECT * FROM $fnName('${tblId.id}', $startPrefix)\")\n    } else {\n      sql(s\"SELECT * FROM $fnName('${tblId.id}', $startPrefix, $endPrefix) \")\n    }\n  }\n\n  override def ctas(\n      srcTbl: String,\n      dstTbl: String,\n      disableCDC: Boolean = false): Unit = {\n\n    val prefix = s\"CREATE TABLE ${dstTbl} USING DELTA\"\n    val suffix = s\" AS SELECT * FROM table_changes('${srcTbl}', 0, 1)\"\n\n    if (disableCDC) {\n      sql(prefix + s\" TBLPROPERTIES (${DeltaConfigs.CHANGE_DATA_FEED.key} = false)\" + suffix)\n    } else {\n      sql(prefix + suffix)\n    }\n  }\n\n  private def testNullRangeBoundary(start: Boundary, end: Boundary): Unit = {\n    test(s\"range boundary cannot be null - start=$start end=$end\") {\n      val tblName = \"tbl\"\n      withTable(tblName) {\n        createTblWithThreeVersions(tblName = Some(tblName))\n\n        checkError(intercept[DeltaIllegalArgumentException] {\n          cdcRead(new TableName(tblName), start, end)\n        }, \"DELTA_CDC_READ_NULL_RANGE_BOUNDARY\")\n      }\n    }\n  }\n\n  for (end <- Seq(\n    Unbounded,\n    EndingVersion(\"null\"),\n    EndingVersion(\"0\"),\n    EndingTimestamp(dateFormat.format(new Date(1)))\n  )) {\n    testNullRangeBoundary(StartingVersion(\"null\"), end)\n  }\n\n  for (start <- Seq(StartingVersion(\"0\"), StartingTimestamp(dateFormat.format(new Date(1))))) {\n    testNullRangeBoundary(start, EndingVersion(\"null\"))\n  }\n\n  testNullRangeBoundary(StartingVersion(\"CAST(null AS INT)\"), Unbounded)\n\n  test(\"select individual column should push down filters\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val plans = DeltaTestUtils.withAllPlansCaptured(spark) {\n        val res = sql(s\"SELECT id, _change_type FROM table_changes('$tblName', 0, 1)\")\n          .where(col(\"id\") < lit(5))\n\n        assert(res.columns === Seq(\"id\", \"_change_type\"))\n        checkAnswer(\n          res,\n          spark.range(5)\n            .withColumn(\"_change_type\", lit(\"insert\")))\n      }\n      assert(plans.map(_.executedPlan).toString\n        .contains(\"PushedFilters: [*IsNotNull(id), *LessThan(id,5)]\"))\n    }\n  }\n\n  test(\"use cdc query as a subquery\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val res = sql(s\"\"\"\n          SELECT * FROM RANGE(30) WHERE id > (\n              SELECT count(*) FROM table_changes('$tblName', 0, 1))\n        \"\"\")\n      checkAnswer(\n        res,\n        spark.range(21, 30).toDF())\n    }\n  }\n\n  test(\"cdc table_changes is not case sensitive\") {\n    val tblName = \"tbl\"\n    withTempDir { dir =>\n      withTable(tblName) {\n        createTblWithThreeVersions(tblName = Some(tblName))\n\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM tabLe_chAnges('$tblName', 0, 1)\"),\n          spark.sql(s\"SELECT * FROM taBle_cHanges('$tblName', 0, 1)\")\n        )\n      }\n    }\n  }\n\n  test(\"cdc table_changes_by_path are not case sensitive\") {\n    withTempDir { dir =>\n      createTblWithThreeVersions(path = Some(dir.getAbsolutePath))\n\n      checkAnswer(\n        spark.sql(s\"SELECT * FROM tabLe_chaNges_By_pAth('${dir.getAbsolutePath}', 0, 1)\"),\n        spark.sql(s\"SELECT * FROM taBle_cHanges_bY_paTh('${dir.getAbsolutePath}', 0, 1)\")\n      )\n    }\n  }\n\n\n  test(\"parse multi part table name\") {\n    val tblName = \"tbl\"\n      withTable(tblName) {\n        createTblWithThreeVersions(tblName = Some(tblName))\n\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM table_changes('$tblName', 0, 1)\"),\n          spark.sql(s\"SELECT * FROM table_changes('default.`${tblName}`', 0, 1)\")\n        )\n      }\n  }\n\n  test(\"negative case - invalid number of args\") {\n    val tbl = \"tbl\"\n    withTable(tbl) {\n      spark.range(10).write.format(\"delta\").saveAsTable(tbl)\n\n      val invalidQueries = Seq(\n        s\"SELECT * FROM table_changes()\",\n        s\"SELECT * FROM table_changes('tbl', 1, 2, 3)\",\n        s\"SELECT * FROM table_changes('tbl')\",\n        s\"SELECT * FROM table_changes_by_path()\",\n        s\"SELECT * FROM table_changes_by_path('tbl', 1, 2, 3)\",\n        s\"SELECT * FROM table_changes_by_path('tbl')\"\n      )\n      invalidQueries.foreach { q =>\n        val e = intercept[AnalysisException] {\n          sql(q)\n        }\n        assert(e.getMessage.contains(\"requires at least 2 arguments and at most 3 arguments\"),\n          s\"failed query: $q \")\n      }\n    }\n  }\n\n  test(\"negative case - invalid type of args\") {\n    val tbl = \"tbl\"\n    withTable(tbl) {\n      spark.range(10).write.format(\"delta\").saveAsTable(tbl)\n\n      val invalidQueries = Seq(\n        s\"SELECT * FROM table_changes(1, 1)\",\n        s\"SELECT * FROM table_changes('$tbl', 1.0)\",\n        s\"SELECT * FROM table_changes_by_path(1, 1)\",\n        s\"SELECT * FROM table_changes_by_path('$tbl', 1.0)\"\n      )\n\n      invalidQueries.foreach { q =>\n        val e = intercept[AnalysisException] {\n          sql(q)\n        }\n        assert(e.getMessage.contains(\"Unsupported expression type\"), s\"failed query: $q\")\n      }\n    }\n  }\n\n  test(\"negative case - non-constant expressions in version/timestamp argument\") {\n    val tbl = \"tbl\"\n    val otherTbl = \"other_tbl\"\n    withTempDir { dir =>\n      withTable(tbl, otherTbl) {\n        spark.range(10).write.format(\"delta\").option(\"path\", dir.getAbsolutePath).saveAsTable(tbl)\n        spark.range(5).toDF(\"version\").write.format(\"delta\").saveAsTable(otherTbl)\n\n        // (query, expectedFunctionName, expectedParamName, expectedPos, sqlExprPattern)\n        val testCases = Seq(\n          // Scalar subquery as starting arg\n          (s\"SELECT * FROM table_changes('$tbl', (SELECT MAX(version) FROM $otherTbl))\",\n            \"table_changes\", \"starting\", 2, \"scalarsubquery.*\"),\n          // Scalar subquery as ending arg\n          (s\"SELECT * FROM table_changes('$tbl', 0, (SELECT MAX(version) FROM $otherTbl))\",\n            \"table_changes\", \"ending\", 3, \"scalarsubquery.*\"),\n          // Scalar subquery in table_changes_by_path\n          (s\"SELECT * FROM table_changes_by_path('${dir.getAbsolutePath}',\" +\n            s\" (SELECT MAX(version) FROM $otherTbl))\",\n            \"table_changes_by_path\", \"starting\", 2, \"scalarsubquery.*\"),\n          // Aggregate expression as starting arg\n          (s\"SELECT * FROM table_changes('$tbl', MAX(1))\",\n            \"table_changes\", \"starting\", 2, \".*[Mm]ax.*\"),\n          // Aggregate expression as ending arg\n          (s\"SELECT * FROM table_changes('$tbl', 0, MAX(1))\",\n            \"table_changes\", \"ending\", 3, \".*[Mm]ax.*\"),\n          // Aggregate expression in table_changes_by_path\n          (s\"SELECT * FROM table_changes_by_path('${dir.getAbsolutePath}', MAX(1))\",\n            \"table_changes_by_path\", \"starting\", 2, \".*[Mm]ax.*\")\n        )\n\n        testCases.foreach { case (q, expectedFn, expectedParam, expectedPos, sqlExprPattern) =>\n          checkErrorMatchPVals(\n            intercept[AnalysisException] { sql(q) },\n            \"DELTA_CDC_NON_CONSTANT_ARGUMENT\",\n            parameters = Map(\n              \"argumentName\" -> s\"`$expectedParam`\",\n              \"pos\" -> expectedPos.toString,\n              \"functionName\" -> s\"`$expectedFn`\",\n              \"sqlExpr\" -> sqlExprPattern\n            )\n          )\n        }\n      }\n    }\n  }\n\n  test(\"negative case - table_changes in correlated subquery with OuterReference\") {\n    // When table_changes() is used inside a correlated subquery with an expression that\n    // wraps an OuterReference (e.g. `o.version + 0`), the top-level node is not Unevaluable\n    // (Add is evaluable) but its child OuterReference is. The old isInstanceOf[Unevaluable]\n    // check on the top-level expression misses this case and .eval() throws INTERNAL_ERROR.\n    // We instead catch that SparkException and re-throw as DELTA_CDC_NON_CONSTANT_ARGUMENT.\n    val tbl = \"tbl\"\n    val otherTbl = \"other_tbl\"\n    withTable(tbl, otherTbl) {\n      spark.range(10).write.format(\"delta\").saveAsTable(tbl)\n      spark.range(5).toDF(\"version\").write.format(\"delta\").saveAsTable(otherTbl)\n\n      val q = s\"\"\"\n        SELECT * FROM $otherTbl o WHERE EXISTS (\n          SELECT 1 FROM table_changes('$tbl', o.version + 0)\n        )\n      \"\"\"\n      checkErrorMatchPVals(\n        intercept[AnalysisException] { sql(q) },\n        \"DELTA_CDC_NON_CONSTANT_ARGUMENT\",\n        parameters = Map(\n          \"argumentName\" -> \"`starting`\",\n          \"pos\" -> \"2\",\n          \"functionName\" -> \"`table_changes`\",\n          \"sqlExpr\" -> \".*\"\n        )\n      )\n    }\n  }\n\n  test(\"resolve expression for timestamp function\") {\n    val tbl = \"tbl\"\n    withDefaultTimeZone(UTC) {\n      withTable(tbl) {\n        createTblWithThreeVersions(tblName = Some(tbl))\n\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n\n        val currentTime = new Date().getTime\n        modifyCommitTimestamp(deltaLog, 0, currentTime - 100000)\n        modifyCommitTimestamp(deltaLog, 1, currentTime)\n        modifyCommitTimestamp(deltaLog, 2, currentTime + 100000)\n\n        // Make sure the snapshot used for the `table_changes` query is updated with the\n        // new timestamps. The ICT changes in un-backfilled commits will not trigger the real\n        // snapshot update, so we need to manually clear the cache and refresh the snapshot\n        // to ensure the new timestamps are used.\n        DeltaLog.clearCache()\n\n        val readDf = sql(s\"SELECT * FROM table_changes('$tbl', 0, now())\")\n        checkCDCAnswer(\n          DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n          readDf,\n          spark.range(20)\n            .withColumn(\"_change_type\", lit(\"insert\"))\n            .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n        )\n\n        // more complex expression\n        val readDf2 = sql(s\"SELECT * FROM table_changes('$tbl', 0, now() + interval 5 seconds)\")\n        checkCDCAnswer(\n          DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n          readDf2,\n          spark.range(20)\n            .withColumn(\"_change_type\", lit(\"insert\"))\n            .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n        )\n\n        val readDf3 = sql(\"SELECT * FROM table_changes\" +\n          s\"('$tbl', string(date_sub(current_date(), 1)), string(now()))\")\n        checkCDCAnswer(\n          DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n          readDf3,\n          spark.range(20)\n            .withColumn(\"_change_type\", lit(\"insert\"))\n            .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n        )\n      }\n    }\n  }\n\n  test(\"resolve invalid table name should throw error\") {\n    var e = intercept[AnalysisException] {\n      sql(s\"SELECT * FROM table_changes(now(), 1, 1)\")\n    }\n    assert(e.getMessage.contains(\"Unsupported expression type(TimestampType) for table name.\" +\n      \" The supported types are [StringType literal]\"))\n\n    e = intercept[AnalysisException] {\n      sql(s\"SELECT * FROM table_changes('invalidtable', 1, 1)\")\n    }\n    assert(e.getErrorClass === \"TABLE_OR_VIEW_NOT_FOUND\")\n\n    withTable (\"tbl\") {\n      spark.range(1).write.format(\"delta\").saveAsTable(\"tbl\")\n      val e = intercept[AnalysisException] {\n        sql(s\"SELECT * FROM table_changes(concat('tb', 'l'), 1, 1)\")\n      }\n      assert(e.getMessage.contains(\"Unsupported expression type(StringType) for table name.\" +\n        \" The supported types are [StringType literal]\"))\n    }\n  }\n\n  test(\"resolution of complex expression should throw an error\") {\n    val tbl = \"tbl\"\n    withTable(tbl) {\n      spark.range(10).write.format(\"delta\").saveAsTable(tbl)\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"SELECT * FROM table_changes('$tbl', 0, id)\")\n        },\n        \"UNRESOLVED_COLUMN.WITHOUT_SUGGESTION\",\n        parameters = Map(\"objectName\" -> \"`id`\"),\n        queryContext = Array(ExpectedContext(\n          fragment = \"id\",\n          start = 38,\n          stop = 39)))\n    }\n  }\n\n  test(\"protocol version\") {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"This test is intended to test the protocol version of `ChangeDataFeedTableFeature`.\" +\n        \"For CCv1.5 tables, the protocol version has different requirement and we already have \" +\n        \"the corresponding coverage in `CatalogOwnedEnablementSuite` and \" +\n        \"`CatalogOwnedPropertyEdgeSuite`.\")\n    }\n    withTable(\"tbl\") {\n      spark.range(10).write.format(\"delta\").saveAsTable(\"tbl\")\n      val log = DeltaLog.forTable(spark, TableIdentifier(tableName = \"tbl\"))\n      // We set CDC to be enabled by default, so this should automatically bump the writer protocol\n      // to the required version.\n      if (columnMappingEnabled) {\n        assert(log.update().protocol == Protocol(2, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          ChangeDataFeedTableFeature,\n          ColumnMappingTableFeature)))\n      } else {\n        assert(log.update().protocol == Protocol(1, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          ChangeDataFeedTableFeature)))\n      }\n    }\n  }\n\n  test(\"table_changes and table_changes_by_path with a non-delta table\") {\n    withTempDir { dir =>\n      withTable(\"tbl\") {\n        spark.range(10).write.format(\"parquet\")\n          .option(\"path\", dir.getAbsolutePath)\n          .saveAsTable(\"tbl\")\n\n        var e = intercept[AnalysisException] {\n          spark.sql(s\"SELECT * FROM table_changes('tbl', 0, 1)\")\n        }\n        assert(e.getErrorClass == \"DELTA_TABLE_ONLY_OPERATION\")\n        assert(e.getMessage.contains(\"table_changes\"))\n\n        e = intercept[AnalysisException] {\n          spark.sql(s\"SELECT * FROM table_changes_by_path('${dir.getAbsolutePath}', 0, 1)\")\n        }\n        assert(e.getErrorClass == \"DELTA_MISSING_DELTA_TABLE\")\n        assert(e.getMessage.contains(\"not a Delta table\"))\n      }\n    }\n  }\n}\n\nclass DeltaCDCSQLWithCatalogOwnedBatch1Suite extends DeltaCDCSQLSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaCDCSQLWithCatalogOwnedBatch2Suite extends DeltaCDCSQLSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaCDCSQLWithCatalogOwnedBatch100Suite extends DeltaCDCSQLSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCDCStreamSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\nimport java.text.SimpleDateFormat\nimport java.util.Date\n\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.modifyCommitTimestamp\nimport org.apache.spark.sql.delta.actions.AddCDCFile\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.{DeltaSourceOffset, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport io.delta.tables._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.streaming.{StreamingQuery, StreamingQueryException, StreamTest, Trigger}\nimport org.apache.spark.sql.types.StructType\n\ntrait DeltaCDCStreamSuiteBase extends StreamTest with DeltaSQLCommandTest\n  with DeltaSourceSuiteBase\n  with DeltaColumnMappingTestUtils {\n\n  import testImplicits._\n  import io.delta.implicits._\n\n  /**\n   * Returns the appropriate DeltaConfig\n   */\n  protected def cdcConfig: DeltaConfig[Boolean] = DeltaConfigs.CHANGE_DATA_FEED\n\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(cdcConfig.defaultTablePropertyKey, \"true\")\n\n  /**\n   * Create two tests for maxFilesPerTrigger and maxBytesPerTrigger\n   */\n  protected def testRateLimit(\n      name: String,\n      maxFilesPerTrigger: String,\n      maxBytesPerTrigger: String)(f: (String, String) => Unit): Unit = {\n    Seq((\"maxFilesPerTrigger\", maxFilesPerTrigger), (\"maxBytesPerTrigger\", maxBytesPerTrigger))\n      .foreach { case (key: String, value: String) =>\n        test(s\"rateLimit - $key - $name\") {\n          f(key, value)\n        }\n      }\n  }\n\n  testQuietly(\"no startingVersion should result fetch the entire snapshot\") {\n    withTempDir { inputDir =>\n      withSQLConf(cdcConfig.defaultTablePropertyKey -> \"false\") {\n        // version 0\n        Seq(1, 9).toDF(\"value\").write.format(\"delta\").save(inputDir.getAbsolutePath)\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n        // version 1\n        deltaTable.delete(\"value = 9\")\n\n        // version 2\n        Seq(2).toDF(\"value\").write.format(\"delta\")\n          .mode(\"append\")\n          .save(inputDir.getAbsolutePath)\n      }\n      // enable cdc - version 3\n      sql(s\"ALTER TABLE delta.`${inputDir.getAbsolutePath}` SET TBLPROPERTIES \" +\n        s\"(${cdcConfig.key}=true)\")\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .format(\"delta\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      val version = 3\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer((1, \"insert\", version), (2, \"insert\", version)),\n        Execute { _ =>\n          deltaTable.delete(\"value = 1\") // version 4\n        },\n        ProcessAllAvailable(),\n        CheckAnswer((1, \"insert\", version), (2, \"insert\", version), (1, \"delete\", version + 1))\n      )\n    }\n  }\n\n  testQuietly(\"CDC initial snapshot should end at base index of next version\") {\n    withTempDir { inputDir =>\n      withSQLConf(cdcConfig.defaultTablePropertyKey -> \"true\") {\n        // version 0\n        Seq(5, 6).toDF(\"value\").write.format(\"delta\").save(inputDir.getAbsolutePath)\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n\n        val df = spark.readStream\n          .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n          .format(\"delta\")\n          .load(inputDir.getCanonicalPath)\n          .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n        testStream(df)(\n          ProcessAllAvailable(),\n          CheckAnswer((5, \"insert\", 0), (6, \"insert\", 0)),\n          AssertOnQuery { q =>\n            val offset = q.committedOffsets.iterator.next()._2.asInstanceOf[DeltaSourceOffset]\n            // The initial snapshot (version 0) was completely processed, so we should now be at\n            // the start of version 1.\n            assert(offset.reservoirVersion === 1)\n            assert(offset.index === DeltaSourceOffset.BASE_INDEX)\n            true\n          },\n          StopStream\n        )\n      }\n    }\n  }\n\n  test(\"startingVersion = latest\") {\n    withTempDir { inputDir =>\n      Seq(1, 2).toDF(\"value\").write.format(\"delta\").save(inputDir.getAbsolutePath)\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"latest\")\n        .format(\"delta\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer(),\n        AddToReservoir(inputDir, Seq(3).toDF(\"value\")),\n        ProcessAllAvailable(),\n        CheckAnswer((3, \"insert\", 1))\n      )\n    }\n  }\n\n  test(\"user provided startingVersion\") {\n    withTempDir { inputDir =>\n      // version 0\n      Seq(1, 2, 3).toDF(\"id\").write.delta(inputDir.toString)\n\n      // version 1\n      Seq(4, 5).toDF(\"id\").write.mode(\"append\").delta(inputDir.toString)\n\n      // version 2\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"1\")\n        .format(\"delta\")\n        .load(inputDir.toString)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer((4, \"insert\", 1), (5, \"insert\", 1)),\n        Execute { _ =>\n          deltaTable.delete(\"id = 3\") // version 2\n        },\n        ProcessAllAvailable(),\n        CheckAnswer((4, \"insert\", 1), (5, \"insert\", 1), (3, \"delete\", 2))\n      )\n    }\n  }\n\n  test(\"user provided startingTimestamp\") {\n    withTempDir { inputDir =>\n      // version 0\n      Seq(1, 2, 3).toDF(\"id\").write.delta(inputDir.toString)\n      val deltaLog = DeltaLog.forTable(spark, inputDir.getAbsolutePath)\n      modifyCommitTimestamp(deltaLog, 0, 1000)\n\n      // version 1\n      Seq(-1).toDF(\"id\").write.mode(\"append\").delta(inputDir.toString)\n      modifyCommitTimestamp(deltaLog, 1, 2000)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      val startTs = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")\n        .format(new Date(2000))\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingTimestamp\", startTs)\n        .format(\"delta\")\n        .load(inputDir.toString)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer((-1, \"insert\", 1)),\n        Execute { _ =>\n          deltaTable.update(expr(\"id == -1\"), Map(\"id\" -> lit(\"4\")))\n        },\n        ProcessAllAvailable(),\n        CheckAnswer((-1, \"insert\", 1), (-1, \"update_preimage\", 2), (4, \"update_postimage\", 2))\n      )\n    }\n  }\n\n  testQuietly(\"starting[Version/Timestamp] > latest version\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // version 0\n      Seq(1, 2, 3, 4, 5, 6).toDF(\"id\").write.delta(inputDir.toString)\n      val deltaLog = DeltaLog.forTable(spark, inputDir.getAbsolutePath)\n      modifyCommitTimestamp(deltaLog, 0, 1000)\n\n      val df1 = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", 1)\n        .format(\"delta\")\n        .load(inputDir.toString)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      val startTs = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")\n        .format(new Date(3000))\n      val commitTs = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")\n      .format(new Date(1000))\n      val df2 = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingTimestamp\", startTs)\n        .format(\"delta\")\n        .load(inputDir.toString)\n\n      val e1 = VersionNotFoundException(1, 0, 0).getMessage\n      val e2 = DeltaErrors.timestampGreaterThanLatestCommit(\n        new Timestamp(3000), new Timestamp(1000), commitTs).getMessage\n\n      Seq((df1, e1), (df2, e2)).foreach { pair =>\n        val df = pair._1\n        val stream = df.select(\"id\").writeStream\n          .option(\"checkpointLocation\", checkpointDir.toString)\n          .outputMode(\"append\")\n          .format(\"delta\")\n          .start(outputDir.getAbsolutePath)\n        val e = intercept[StreamingQueryException] {\n          stream.processAllAvailable()\n        }\n        stream.stop()\n        assert(e.cause.getMessage === pair._2)\n      }\n    }\n  }\n\n  test(\"check starting[Version/Timestamp] > latest version without error\") {\n    Seq(\"version\", \"timestamp\").foreach { target =>\n      withTempDir { inputDir =>\n        withSQLConf(DeltaSQLConf.DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP.key -> \"true\") {\n          // version 0\n          Seq(1, 2, 3).toDF(\"id\").write.delta(inputDir.toString)\n          val inputPath = inputDir.getAbsolutePath\n          val deltaLog = DeltaLog.forTable(spark, inputPath)\n          modifyCommitTimestamp(deltaLog, 0, 1000)\n\n          val deltaTable = io.delta.tables.DeltaTable.forPath(inputPath)\n\n          // Pick both the timestamp and version beyond latest commmit's version.\n          val df = if (target == \"timestamp\") {\n            // build dataframe with starting timestamp option.\n            val startTs = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")\n              .format(new Date(2000))\n            spark.readStream\n              .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n              .option(\"startingTimestamp\", startTs)\n              .format(\"delta\")\n              .load(inputDir.toString)\n              .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n          } else {\n            assert(target == \"version\")\n            // build dataframe with starting version option.\n            spark.readStream\n              .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n              .option(\"startingVersion\", 1)\n              .format(\"delta\")\n              .load(inputDir.toString)\n              .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n          }\n\n          testStream(df)(\n            ProcessAllAvailable(),\n            // Expect empty update from the read stream.\n            CheckAnswer(),\n            // Verify new updates after the start timestamp/version can be read.\n            Execute { _ =>\n              deltaTable.update(expr(\"id == 1\"), Map(\"id\" -> lit(\"4\")))\n            },\n            ProcessAllAvailable(),\n            CheckAnswer((1, \"update_preimage\", 1), (4, \"update_postimage\", 1))\n          )\n        }\n      }\n    }\n  }\n\n  testQuietly(\"startingVersion and startingTimestamp are both set\") {\n    withTempDir { tableDir =>\n      val tablePath = tableDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(tableDir.getAbsolutePath)\n      val q = spark.readStream\n        .format(\"delta\")\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", 0L)\n        .option(\"startingTimestamp\", \"2020-07-15\")\n        .load(tablePath)\n        .writeStream\n        .format(\"console\")\n        .start()\n      assert(intercept[StreamingQueryException] {\n        q.processAllAvailable()\n      }.getMessage.contains(\"Please either provide 'startingVersion' or 'startingTimestamp'\"))\n      q.stop()\n    }\n  }\n\n  test(\"cdc streams should respect checkpoint\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // write 3 versions\n      Seq(1, 2, 3).toDF(\"id\").write.format(\"delta\").save(inputDir.getAbsolutePath)\n      Seq(4, 5, 6).toDF(\"id\").write.format(\"delta\")\n        .mode(\"append\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      deltaTable.delete(\"id = 5\")\n\n      val checkpointDir1 = new Path(checkpointDir.getAbsolutePath, \"ck1\")\n      val checkpointDir2 = new Path(checkpointDir.getAbsolutePath, \"ck2\")\n\n      def streamChanges(\n          startingVersion: Long,\n          checkpointLocation: String): Unit = {\n        val q = spark.readStream\n          .format(\"delta\")\n          .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n          .option(\"startingVersion\", startingVersion)\n          .load(inputDir.getCanonicalPath)\n          .select(\"id\")\n          .writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", checkpointLocation)\n          .start(outputDir.getCanonicalPath)\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n\n      streamChanges(1, checkpointDir1.toString)\n      checkAnswer(\n        spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n        Seq(4, 5, 5, 6).map(_.toLong).toDF(\"id\"))\n\n      // Second time streaming should not write the rows again\n      streamChanges(1, checkpointDir1.toString)\n      checkAnswer(\n        spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n        Seq(4, 5, 5, 6).map(_.toLong).toDF(\"id\"))\n\n      // new checkpoint location\n      streamChanges(1, checkpointDir2.toString)\n      checkAnswer(\n        spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n        Seq(4, 4, 5, 5, 5, 5, 6, 6).map(_.toLong).toDF(\"id\"))\n    }\n  }\n\n  test(\"cdc streams with noop merge\") {\n    withSQLConf(\n      // When DeletionVectors are enabled (e.g., via CatalogManaged QoL features), a truly no-op\n      // merge (all WHEN conditions false) produces empty actions (no FileActions) because\n      // writeUnmodifiedRows=false in the DV path. The default table isolation level (Serializable)\n      // allows canDowngradeToSnapshotIsolation to succeed (noDataChanged=true is sufficient), so\n      // the transaction runs at SnapshotIsolation and skipRecordingEmptyCommitAllowed returns\n      // true, causing commitIfNeeded to skip the commit entirely. This test requires version 1\n      // to exist for streaming, so we force the classic copy-on-write merge path.\n      DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key -> \"false\",\n      cdcConfig.defaultTablePropertyKey -> \"true\"\n    ) {\n      withTempDirs { (srcDir, targetDir, checkpointDir) =>\n        // write source table\n        Seq((1, \"a\"), (2, \"b\"))\n          .toDF(\"key1\", \"val1\")\n          .write\n          .format(\"delta\")\n          .save(srcDir.getCanonicalPath)\n\n        // write target table\n        Seq((1, \"t\"), (2, \"u\"))\n          .toDF(\"key2\", \"val2\")\n          .write\n          .format(\"delta\")\n          .save(targetDir.getCanonicalPath)\n\n        val srcDF = spark.read.format(\"delta\").load(srcDir.getCanonicalPath)\n        val tgtTable = io.delta.tables.DeltaTable.forPath(targetDir.getCanonicalPath)\n\n        // Perform the merge where all matching and non-matching conditions fail for\n        // target rows.\n        tgtTable\n          .merge(srcDF,\n            \"key1 = key2\")\n          .whenMatched(\"key1 = 10\")\n          .updateExpr(Map(\"key2\" -> \"key1\", \"val2\" -> \"val1\"))\n          .whenNotMatched(\"key1 = 11\")\n          .insertExpr(Map(\"key2\" -> \"key1\", \"val2\" -> \"val1\"))\n          .execute()\n\n        // Read the target dir with cdc read option and ensure that\n        // data frame is empty.\n        val q = spark.readStream\n          .format(\"delta\")\n          .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n          .option(\"startingVersion\", \"1\")\n          .load(targetDir.getCanonicalPath)\n          .writeStream\n          .format(\"memory\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .queryName(\"testQuery\")\n          .start()\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n\n        assert(spark.table(\"testQuery\").isEmpty)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { readChangeFeed =>\n    test(s\"streams updating latest offset with readChangeFeed=$readChangeFeed\") {\n      withTempDirs { (inputDir, checkpointDir, outputDir) =>\n        withSQLConf(cdcConfig.defaultTablePropertyKey -> \"true\") {\n\n          sql(s\"CREATE TABLE delta.`$inputDir` (id BIGINT, value STRING) USING DELTA\")\n          // save some rows to input table.\n          spark.range(10).withColumn(\"value\", lit(\"a\"))\n            .write.format(\"delta\").mode(\"overwrite\")\n            .option(\"enableChangeDataFeed\", \"true\").save(inputDir.getAbsolutePath)\n\n          def runStreamingQuery(): StreamingQuery = {\n            // process the input table in a CDC manner\n            val df = spark.readStream\n              .option(DeltaOptions.CDC_READ_OPTION, readChangeFeed)\n              .format(\"delta\")\n              .load(inputDir.getAbsolutePath)\n            val query = df\n              .select(\"id\")\n              .writeStream\n              .format(\"delta\")\n              .outputMode(\"append\")\n              .option(\"checkpointLocation\", checkpointDir.toString)\n              .start(outputDir.getAbsolutePath)\n\n            query.processAllAvailable()\n            query.stop()\n            query.awaitTermination()\n            query\n          }\n\n          var query = runStreamingQuery()\n\n          val deltaLog = DeltaLog.forTable(spark, inputDir.toString)\n          // Do three no-op updates to the table. These are tricky because the commits have no\n          // changes, but the stream should still pick up the new versions and progress past them.\n          for (i <- 0 to 2) {\n            deltaLog.startTransaction().commit(Seq(), DeltaOperations.ManualUpdate)\n          }\n\n          // Read again from input table and no new data should be generated\n          query = runStreamingQuery()\n\n          // check that the last batch was committed and that the\n          // reservoirVersion for the table was updated to latest\n          // in both cdf and non-cdf cases.\n          assert(query.lastProgress.batchId === 1)\n          val endOffset =\n            JsonUtils.fromJson[DeltaSourceOffset](query.lastProgress.sources.head.endOffset)\n          assert(endOffset.reservoirVersion === 5,\n            s\"endOffset = $endOffset\")\n          assert(endOffset.index === DeltaSourceOffset.BASE_INDEX, s\"endOffset = $endOffset\")\n        }\n      }\n    }\n  }\n\n  test(\"cdc streams should be able to get offset when there only RemoveFiles\") {\n    withTempDir { inputDir =>\n      // version 0\n      spark.range(2).withColumn(\"part\", 'id % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", 0)\n        .format(\"delta\")\n        .load(inputDir.toString)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer((0, 0, \"insert\", 0), (1, 1, \"insert\", 0)),\n        Execute { _ =>\n          deltaTable.delete(\"part = 0\") // version 2\n        },\n        ProcessAllAvailable(),\n        CheckAnswer((0, 0, \"insert\", 0), (1, 1, \"insert\", 0), (0, 0, \"delete\", 1))\n      )\n    }\n  }\n\n  test(\"cdc streams should work starting from RemoveFile\") {\n    withTempDir { inputDir =>\n      // version 0\n      spark.range(2).withColumn(\"part\", 'id % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n\n      deltaTable.delete(\"part = 0\")\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", 1)\n        .format(\"delta\")\n        .load(inputDir.toString)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer((0, 0, \"delete\", 1))\n      )\n    }\n  }\n\n  test(\"cdc streams should work starting from AddCDCFile\") {\n    withTempDir { inputDir =>\n      // version 0\n      spark.range(2).withColumn(\"col2\", 'id % 2)\n        .repartition(1)\n        .write\n        .format(\"delta\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n\n      deltaTable.delete(\"col2 = 0\")\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", 1)\n        .format(\"delta\")\n        .load(inputDir.toString)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df) (\n        ProcessAllAvailable(),\n        CheckAnswer((0, 0, \"delete\", 1)),\n        AddToReservoir(inputDir, spark.range(2, 3).withColumn(\"col2\", 'id % 2)),\n        ProcessAllAvailable(),\n        CheckAnswer((0, 0, \"delete\", 1), (2, 0, \"insert\", 2))\n      )\n    }\n  }\n\n  testRateLimit(s\"overall\", \"1\", \"1b\") { (key, value) =>\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n\n      // write - version 0 - 2 AddFiles - Adds 4 rows\n      spark.range(0, 4, 1, 1).toDF(\"id\")\n        .withColumn(\"part\", col(\"id\") % 2) // 2 partitions\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(inputDir.getAbsolutePath)\n\n      assert(deltaLog.snapshot.version == 0)\n      assert(deltaLog.snapshot.numOfFiles == 2)\n\n      // write - version 1 - 1 AddFile - Adds 1 row\n      Seq(4L).toDF(\"id\").withColumn(\"part\", lit(-1L))\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .partitionBy(\"part\")\n        .save(deltaLog.dataPath.toString)\n      assert(deltaLog.snapshot.version == 1)\n      assert(deltaLog.snapshot.numOfFiles == 3)\n\n      // delete - version 2 - 1 RemoveFile - Removes 1 row\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      deltaTable.delete(\"part = -1\")\n      assert(deltaLog.snapshot.version == 2)\n      assert(deltaLog.snapshot.numOfFiles == 2)\n\n      // update the table - version 3 - 2 cdc files - Updates 2 rows\n      deltaTable.update(expr(\"id < 2\"), Map(\"id\" -> lit(0L)))\n\n      // update the table - version 4 - 2 cdc files - Updates 2 rows\n      deltaTable.update(expr(\"id > 1\"), Map(\"id\" -> lit(0L)))\n\n      val rowsPerBatch = Seq(\n        2, // 2 rows from 1 AddFile\n        2, // 2 rows from the 2nd AddFile\n        1, // 1 row from the 3rd AddFile\n        1, // 1 row from the RemoveFile\n        4, // 4 rows(pre_image and post_image) from the 2 AddCDCFile\n        4 // 4 rows(pre_image and post_image) from the 2 AddCDCFile\n      )\n      val q = spark.readStream\n        .format(\"delta\")\n        .option(key, value)\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"0\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(q) (\n        ProcessAllAvailable(),\n        CheckProgress(rowsPerBatch),\n        CheckAnswer(\n          (0, 0, \"insert\", 0),\n          (1, 1, \"insert\", 0),\n          (2, 0, \"insert\", 0),\n          (3, 1, \"insert\", 0),\n          (4, -1, \"insert\", 1),\n          (4, -1, \"delete\", 2),\n          (0, 0, \"update_preimage\", 3),\n          (0, 0, \"update_postimage\", 3),\n          (1, 1, \"update_preimage\", 3),\n          (0, 1, \"update_postimage\", 3),\n          (2, 0, \"update_preimage\", 4),\n          (0, 0, \"update_postimage\", 4),\n          (3, 1, \"update_preimage\", 4),\n          (0, 1, \"update_postimage\", 4)\n        )\n      )\n    }\n  }\n\n  testRateLimit(s\"starting from initial snapshot\", \"1\", \"1b\") { (key, value) =>\n    withTempDir { inputDir =>\n      // 3 commits - 3 AddFiles each\n      (0 until 3).foreach { i =>\n        spark.range(i, i + 1, 1, 1)\n          .write\n          .mode(\"append\")\n          .format(\"delta\")\n          .save(inputDir.getAbsolutePath)\n      }\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      assert(deltaLog.snapshot.numOfFiles === 3)\n\n      // 1 commit - 2 AddFiles\n      spark.range(3, 5, 1, 2)\n        .write\n        .mode(\"append\")\n        .format(\"delta\")\n        .save(inputDir.getAbsolutePath)\n\n      assert(deltaLog.snapshot.numOfFiles === 5)\n\n      val q = spark.readStream\n        .format(\"delta\")\n        .option(key, value)\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      // 5 batches for the 5 commits split across commits and index number.\n      val rowsPerBatch = Seq(1, 1, 1, 1, 1)\n\n      testStream(q)(\n        ProcessAllAvailable(),\n        CheckProgress(rowsPerBatch),\n        CheckAnswer(\n          (0, \"insert\", 3),\n          (1, \"insert\", 3),\n          (2, \"insert\", 3),\n          (3, \"insert\", 3),\n          (4, \"insert\", 3)\n        )\n      )\n    }\n  }\n\n  testRateLimit(s\"should not deadlock\", \"1\", \"1b\") { (key, value) =>\n    withTempDir { inputDir =>\n      // version 0 - 2 AddFiles\n      spark.range(2)\n        .withColumn(\"part\", 'id % 2)\n        .withColumn(\"col3\", lit(0))\n        .repartition(1)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      // version 1 - 2 AddCDCFiles\n      deltaTable.update(expr(\"col3 < 2\"), Map(\"col3\" -> lit(\"0\")))\n\n      // version 2 - 2 AddCDCFiles\n      deltaTable.update(expr(\"col3 < 2\"), Map(\"col3\" -> lit(\"1\")))\n\n      val df = spark.readStream\n        .format(\"delta\")\n        .option(key, value)\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"1\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df)(\n        ProcessAllAvailable(),\n        CheckProgress(Seq(4, 4)),// 4 rows(2 pre- and 2 post-images) for each version\n        CheckAnswer(\n          (0, 0, 0, \"update_preimage\", 1),\n          (0, 0, 0, \"update_postimage\", 1),\n          (0, 0, 0, \"update_preimage\", 2),\n          (0, 0, 1, \"update_postimage\", 2),\n          (1, 1, 0, \"update_preimage\", 1),\n          (1, 1, 0, \"update_postimage\", 1),\n          (1, 1, 0, \"update_preimage\", 2),\n          (1, 1, 1, \"update_postimage\", 2)\n        )\n      )\n    }\n  }\n\n  test(\"maxFilesPerTrigger - 2 successive AddCDCFile commits\") {\n    withTempDir { inputDir =>\n      // version 0 - 2 AddFiles\n      spark.range(2)\n        .withColumn(\"part\", 'id % 2)\n        .withColumn(\"col3\", lit(0))\n        .repartition(1)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      // version 1 - 2 AddCDCFiles\n      deltaTable.update(expr(\"col3 < 2\"), Map(\"col3\" -> lit(\"0\")))\n\n      // version 2 - 2 AddCDCFiles\n      deltaTable.update(expr(\"col3 < 2\"), Map(\"col3\" -> lit(\"1\")))\n\n      val df = spark.readStream\n        .format(\"delta\")\n        .option(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, \"3\")\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"0\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      // test whether the AddCDCFile commits do not get split up.\n      val rowsPerBatch = Seq(\n        2, // 2 rows from the 2 AddFile\n        4, // 4 rows(pre and post image) from the 2 AddCDCFiles\n        4 // 4 rows(pre and post image) from 2 AddCDCFiles\n      )\n\n      testStream(df)(\n        ProcessAllAvailable(),\n        CheckProgress(rowsPerBatch),\n        CheckAnswer(\n          (0, 0, 0, \"insert\", 0),\n          (1, 1, 0, \"insert\", 0),\n          (0, 0, 0, \"update_preimage\", 1),\n          (0, 0, 0, \"update_postimage\", 1),\n          (1, 1, 0, \"update_preimage\", 1),\n          (1, 1, 0, \"update_postimage\", 1),\n          (0, 0, 0, \"update_preimage\", 2),\n          (0, 0, 1, \"update_postimage\", 2),\n          (1, 1, 0, \"update_preimage\", 2),\n          (1, 1, 1, \"update_postimage\", 2)\n        )\n      )\n    }\n  }\n\n  test(\"maxFilesPerTrigger with Trigger.AvailableNow respects read limits\") {\n    withTempDir { inputDir =>\n      // version 0 - 2 AddFiles\n      spark.range(2)\n        .withColumn(\"part\", 'id % 2)\n        .withColumn(\"col3\", lit(0))\n        .repartition(1)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      // version 1 - 2 AddCDCFiles\n      deltaTable.update(expr(\"col3 < 2\"), Map(\"col3\" -> lit(\"0\")))\n\n      // version 2 - 2 AddCDCFiles\n      deltaTable.update(expr(\"col3 < 2\"), Map(\"col3\" -> lit(\"1\")))\n\n      val df = spark.readStream\n        .format(\"delta\")\n        .option(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, \"3\")\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"0\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      // test whether the AddCDCFile commits do not get split up.\n      val rowsPerBatch = Seq(\n        2, // 2 rows from the 2 AddFile\n        4, // 4 rows(pre and post image) from the 2 AddCDCFiles\n        4 // 4 rows(pre and post image) from 2 AddCDCFiles\n      )\n\n      testStream(df)(\n        StartStream(Trigger.AvailableNow),\n        Execute { query =>\n          assert(query.awaitTermination(10000))\n        },\n        CheckProgress(rowsPerBatch),\n        CheckAnswer(\n          (0, 0, 0, \"insert\", 0),\n          (1, 1, 0, \"insert\", 0),\n          (0, 0, 0, \"update_preimage\", 1),\n          (0, 0, 0, \"update_postimage\", 1),\n          (1, 1, 0, \"update_preimage\", 1),\n          (1, 1, 0, \"update_postimage\", 1),\n          (0, 0, 0, \"update_preimage\", 2),\n          (0, 0, 1, \"update_postimage\", 2),\n          (1, 1, 0, \"update_preimage\", 2),\n          (1, 1, 1, \"update_postimage\", 2)\n        )\n      )\n    }\n  }\n\n  test(\"excludeRegex works with cdc\") {\n    withTempDir { inputDir =>\n      spark.range(2)\n        .withColumn(\"part\", 'id % 2)\n        .repartition(1)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(inputDir.getAbsolutePath)\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"0\")\n        .option(DeltaOptions.EXCLUDE_REGEX_OPTION, \"part=0\")\n        .format(\"delta\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df)(\n        ProcessAllAvailable(),\n        CheckAnswer((1, 1, \"insert\", 0)) // first file should get excluded\n      )\n    }\n  }\n\n  test(\"excludeRegex on cdcPath should not return Add/RemoveFiles\") {\n    withTempDir { inputDir =>\n      // version 0 - 1 AddFile\n      Seq(0).toDF(\"id\")\n        .withColumn(\"col2\", lit(\"0\"))\n        .repartition(1)\n        .write\n        .format(\"delta\")\n        .save(inputDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(inputDir.getAbsolutePath)\n      // version 1 - 1 ChangeFile\n      deltaTable.update(expr(\"col2 < 2\"), Map(\"col2\" -> lit(\"1\")))\n\n      val deltaLog = DeltaLog.forTable(spark, inputDir.getAbsolutePath)\n      val excludePath = deltaLog.getChanges(1).next()._2\n        .filter(_.isInstanceOf[AddCDCFile])\n        .head\n        .asInstanceOf[AddCDCFile]\n        .path\n\n      val df = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", \"0\")\n        .option(DeltaOptions.EXCLUDE_REGEX_OPTION, excludePath)\n        .format(\"delta\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df)(\n        ProcessAllAvailable(),\n        CheckAnswer((0, \"0\", \"insert\", 0)) // first file should get excluded\n      )\n    }\n  }\n\n  test(\"schema check for cdc stream\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        Seq(i).toDF.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val df = spark.readStream\n        .format(\"delta\")\n        .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n        .option(\"startingVersion\", 0)\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\n          (0, \"insert\", 0),\n          (1, \"insert\", 1),\n          (2, \"insert\", 2),\n          (3, \"insert\", 3),\n          (4, \"insert\", 4)\n        ),\n        // no schema changed exception should be thrown.\n        AssertOnQuery { _ =>\n          withMetadata(deltaLog, StructType.fromDDL(\"value int\"))\n          true\n        },\n        // Force processing of stream to prevent race condition between DeltaSource.getBatch and\n        // DeltaSource.checkReadIncompatibleSchemaChanges\n        ProcessAllAvailable(),\n        AssertOnQuery { _ =>\n          withMetadata(deltaLog, StructType.fromDDL(\"id int, value string\"))\n          true\n        },\n        ExpectFailure[DeltaIllegalStateException](t =>\n          assert(t.getMessage.contains(\"Detected schema change\")))\n      )\n    }\n  }\n\n  test(\"should not attempt to read a non exist version\") {\n    withTempDirs { (inputDir1, inputDir2, checkpointDir) =>\n      spark.range(1, 2).write.format(\"delta\").save(inputDir1.getCanonicalPath)\n      spark.range(1, 2).write.format(\"delta\").save(inputDir2.getCanonicalPath)\n\n      def startQuery(): StreamingQuery = {\n        val df1 = spark.readStream\n          .format(\"delta\")\n          .option(\"readChangeFeed\", \"true\")\n          .load(inputDir1.getCanonicalPath)\n        val df2 = spark.readStream\n          .format(\"delta\")\n          .option(\"readChangeFeed\", \"true\")\n          .load(inputDir2.getCanonicalPath)\n        df1.union(df2).writeStream\n          .format(\"noop\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .start()\n      }\n\n      var q = startQuery()\n      try {\n        q.processAllAvailable()\n        // current offsets:\n        // source1: DeltaSourceOffset(reservoirVersion=1,index=0,isInitialSnapshot=true)\n        // source2: DeltaSourceOffset(reservoirVersion=1,index=0,isInitialSnapshot=true)\n\n        spark.range(1, 2).write.format(\"delta\").mode(\"append\").save(inputDir1.getCanonicalPath)\n        spark.range(1, 2).write.format(\"delta\").mode(\"append\").save(inputDir2.getCanonicalPath)\n        q.processAllAvailable()\n        // current offsets:\n        // source1: DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // source2: DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // Note: version 2 doesn't exist in source1\n\n        spark.range(1, 2).write.format(\"delta\").mode(\"append\").save(inputDir2.getCanonicalPath)\n        q.processAllAvailable()\n        // current offsets:\n        // source1: DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // source2: DeltaSourceOffset(reservoirVersion=3,index=-1,isInitialSnapshot=false)\n        // Note: version 2 doesn't exist in source1\n\n        q.stop()\n        // Restart the query. It will call `getBatch` on the previous two offsets of `source1` which\n        // are both DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // As version 2 doesn't exist, we should not try to load version 2 in this case.\n        q = startQuery()\n        q.processAllAvailable()\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  // LC-1281: Ensure that when we would split batches into one file at a time, we still produce\n  // correct CDF even in cases where the CDF may need to compare multiple file actions from the\n  // same commit to be correct, such as with persistent deletion vectors.\n  test(\"double delete-only on the same file\") {\n    withTempDir { tableDir =>\n      val tablePath = tableDir.toString\n      spark.range(start = 0L, end = 10L, step = 1L, numPartitions = 1).toDF(\"id\")\n        .write.format(\"delta\").save(tablePath)\n\n      spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE id IN (1, 3, 6)\")\n      spark.sql(s\"DELETE FROM delta.`$tablePath` WHERE id IN (2, 4, 7)\")\n\n      val stream = spark.readStream\n        .format(\"delta\")\n        .option(DeltaOptions.CDC_READ_OPTION, true)\n        .option(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, 1)\n        .option(DeltaOptions.STARTING_VERSION_OPTION, 1)\n        .load(tablePath)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n      testStream(stream)(\n        ProcessAllAvailable(),\n        CheckAnswer(\n          (1L, \"delete\", 1L),\n          (3L, \"delete\", 1L),\n          (6L, \"delete\", 1L),\n          (2L, \"delete\", 2L),\n          (4L, \"delete\", 2L),\n          (7L, \"delete\", 2L)\n        )\n      )\n    }\n  }\n}\n\nclass DeltaCDCStreamDeletionVectorSuite extends DeltaCDCStreamSuite\n  with DeletionVectorsTestUtils {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectorsForAllSupportedOperations(spark)\n  }\n}\n\nclass DeltaCDCStreamSuite extends DeltaCDCStreamSuiteBase\n// Batch sizes 1, 2, and 100 exercise different backfill behaviors in the commit coordinator.\n// Batch size 1 triggers a backfill on every commit (commitVersion % 1 == 0), testing the most\n// granular backfill path. Batch size 2 triggers backfill every other commit, testing the boundary\n// between backfilled and unbackfilled commits. Batch size 100 leaves most commits unbackfilled,\n// testing the production-like path where streaming must read from both the commit coordinator\n// and the filesystem. This follows the same pattern as other CatalogManaged (CCv2) test suites\n// (DeltaLogSuite, DeltaSourceSuite, etc.).\n\nclass DeltaCDCStreamWithCatalogManagedBatch1Suite\n  extends DeltaCDCStreamSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaCDCStreamWithCatalogManagedBatch2Suite\n  extends DeltaCDCStreamSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaCDCStreamWithCatalogManagedBatch100Suite\n    extends DeltaCDCStreamSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\nabstract class DeltaCDCStreamColumnMappingSuiteBase extends DeltaCDCStreamSuite\n  with ColumnMappingStreamingBlockedWorkflowSuiteBase with DeltaColumnMappingSelectedTestMixin {\n\n  override protected def isCdcTest: Boolean = true\n\n\n  override def runOnlyTests: Seq[String] = Seq(\n    \"no startingVersion should result fetch the entire snapshot\",\n    \"user provided startingVersion\",\n    \"maxFilesPerTrigger - 2 successive AddCDCFile commits\",\n\n    // streaming blocking semantics test\n    \"deltaLog snapshot should not be updated outside of the stream\",\n    \"column mapping + streaming - allowed workflows - column addition\",\n    \"column mapping + streaming - allowed workflows - upgrade to name mode\",\n    \"column mapping + streaming: blocking workflow - drop column\",\n    \"column mapping + streaming: blocking workflow - rename column\"\n  )\n\n}\n\nclass DeltaCDCStreamIdColumnMappingSuite extends DeltaCDCStreamColumnMappingSuiteBase\n  with DeltaColumnMappingEnableIdMode {\n}\n\nclass DeltaCDCStreamNameColumnMappingSuite extends DeltaCDCStreamColumnMappingSuiteBase\n  with DeltaColumnMappingEnableNameMode {\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCDCSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaTestUtils.{modifyCommitTimestamp, BOOLEAN_DOMAIN}\nimport org.apache.spark.sql.delta.cdc.CDCEnabled\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader._\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, FileNames}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{col, current_timestamp, floor, lit, unix_timestamp}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.StreamingQueryException\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{LongType, StringType, StructType}\n\nabstract class DeltaCDCSuiteBase\n  extends QueryTest\n  with CDCEnabled\n  with SharedSparkSession\n  with CheckCDCAnswer\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite\n  with DeltaSQLTestUtils {\n\n  import testImplicits._\n\n  /** Represents path or metastore table name */\n  abstract case class TblId(id: String)\n  class TablePath(path: String) extends TblId(path)\n  class TableName(name: String) extends TblId(name)\n\n  /** Indicates either the starting or ending version/timestamp */\n  trait Boundary\n  case class StartingVersion(value: String) extends Boundary\n  case class StartingTimestamp(value: String) extends Boundary\n  case class EndingVersion(value: String) extends Boundary\n  case class EndingTimestamp(value: String) extends Boundary\n  case object Unbounded extends Boundary // used to model situation when a boundary isn't provided\n  val dateFormat = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss Z\")\n\n  def createTblWithThreeVersions(\n      tblName: Option[String] = None,\n      path: Option[String] = None): Unit = {\n    // version 0\n    if (tblName.isDefined && path.isDefined) {\n      spark.range(10).write.format(\"delta\")\n        .option(\"path\", path.get)\n        .saveAsTable(tblName.get)\n    } else if (tblName.isDefined) {\n      spark.range(10).write.format(\"delta\")\n        .saveAsTable(tblName.get)\n    } else if (path.isDefined) {\n      spark.range(10).write.format(\"delta\")\n        .save(path.get)\n    }\n\n    if (tblName.isDefined) {\n      // version 1\n      spark.range(10, 20).write.format(\"delta\").mode(\"append\").saveAsTable(tblName.get)\n\n      // version 2\n      spark.range(20, 30).write.format(\"delta\").mode(\"append\").saveAsTable(tblName.get)\n    } else if (path.isDefined) {\n      // version 1\n      spark.range(10, 20).write.format(\"delta\").mode(\"append\").save(path.get)\n\n      // version 2\n      spark.range(20, 30).write.format(\"delta\").mode(\"append\").save(path.get)\n    }\n  }\n\n  /** Single method to do all kinds of CDC reads */\n  // By default, we use the `legacy` batch CDF schema mode, in which either latest schema is used\n  // or the time-travelled schema is used.\n  def cdcRead(\n      tblId: TblId,\n      start: Boundary,\n      end: Boundary,\n      schemaMode: Option[DeltaBatchCDFSchemaMode] = Some(BatchCDFSchemaLegacy),\n      readerOptions: Map[String, String] = Map.empty): DataFrame\n\n  /** Create table utility method */\n  def ctas(srcTbl: String, dstTbl: String, disableCDC: Boolean = false): Unit = {\n    val readDf = cdcRead(new TableName(srcTbl), StartingVersion(\"0\"), EndingVersion(\"1\"))\n    if (disableCDC) {\n      withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"false\") {\n        readDf.write.format(\"delta\")\n          .saveAsTable(dstTbl)\n      }\n    } else {\n      readDf.write.format(\"delta\")\n        .saveAsTable(dstTbl)\n    }\n  }\n\n  private val validTimestampFormats =\n    Seq(\"yyyy-MM-dd HH:mm:ss\", \"yyyy-MM-dd HH:mm:ss.SSS\", \"yyyy-MM-dd\")\n  private val invalidTimestampFormats =\n    Seq(\"yyyyMMddHHmmssSSS\")\n\n  (validTimestampFormats ++ invalidTimestampFormats).foreach { formatStr =>\n    val isValid = validTimestampFormats.contains(formatStr)\n    val isValidStr = if (isValid) \"valid\" else \"invalid\"\n\n    test(s\"CDF timestamp format - $formatStr is $isValidStr\") {\n      withTable(\"src\") {\n        createTblWithThreeVersions(tblName = Some(\"src\"))\n\n        val timestamp = new SimpleDateFormat(formatStr).format(new Date(1))\n\n        def doRead(): Unit = {\n          cdcRead(new TableName(\"src\"), StartingTimestamp(timestamp), EndingVersion(\"1\"))\n        }\n\n        if (isValid) {\n          doRead()\n        } else {\n          val e = intercept[AnalysisException] {\n            doRead()\n          }.getMessage()\n          assert(e.contains(\"The provided timestamp\"))\n          assert(e.contains(\"cannot be converted to a valid timestamp\"))\n        }\n      }\n    }\n  }\n\n  testQuietly(\"writes with metadata columns\") {\n    withTable(\"src\", \"dst\") {\n\n      // populate src table with CDC data\n      createTblWithThreeVersions(tblName = Some(\"src\"))\n\n      // writing cdc data to a new table with cdc enabled should fail. the source table has columns\n      // that are reserved for CDC only, and shouldn't be allowed into the target table.\n      val e = intercept[IllegalStateException] {\n        ctas(\"src\", \"dst\")\n      }\n      val writeContainsCDCColumnsError = DeltaErrors.cdcColumnsInData(\n        cdcReadSchema(new StructType()).fieldNames).getMessage\n      val enablingCDCOnTableWithCDCColumns = DeltaErrors.tableAlreadyContainsCDCColumns(\n        cdcReadSchema(new StructType()).fieldNames).getMessage\n\n      assert(e.getMessage.contains(writeContainsCDCColumnsError))\n\n      // when cdc is disabled writes should work\n      ctas(\"src\", \"dst\", disableCDC = true)\n\n      // write some more data\n      withTable(\"more_data\") {\n        spark.range(20, 30)\n          .withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(\"_commit_version\", lit(2L))\n          .withColumn(\"_commit_timestamp\", current_timestamp)\n          .write.saveAsTable(\"more_data\")\n\n        spark.table(\"more_data\").write.format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(\"dst\")\n\n        checkAnswer(\n          spark.read.format(\"delta\").table(\"dst\"),\n          cdcRead(new TableName(\"src\"), StartingVersion(\"0\"), EndingVersion(\"1\"))\n            .union(spark.table(\"more_data\"))\n        )\n      }\n\n      // re-enabling cdc should be disallowed, since the dst table already contains column that are\n      // reserved for CDC only.\n      val e2 = intercept[IllegalStateException] {\n        sql(s\"ALTER TABLE dst SET TBLPROPERTIES \" +\n          s\"(${DeltaConfigs.CHANGE_DATA_FEED.key}=true)\")\n      }\n      assert(e2.getMessage.contains(enablingCDCOnTableWithCDCColumns))\n    }\n  }\n\n  // Test that schema evolution on a table with CDC enabled cannot add reserved columns.\n  for (operation <- Seq(\"merge\", \"write\")) {\n    test(s\"schema evolution with CDC reserved column names - op = $operation\") {\n      withTable(\"src\", \"dst\") {\n        // Create target table with CDC enabled.\n        createTblWithThreeVersions(tblName = Some(\"dst\"))\n        // Create the source table containing the CDC of the destination table.\n        ctas(srcTbl = \"dst\", dstTbl = \"src\", disableCDC = true)\n\n        // Write the source back to the target table.\n        val e = intercept[DeltaIllegalStateException] {\n          withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n            operation match {\n              case \"merge\" =>\n                spark.sql(\n                  \"\"\"\n                    |MERGE INTO dst USING src\n                    |ON dst.id = src.id\n                    |WHEN MATCHED THEN UPDATE SET *\n                    |WHEN NOT MATCHED THEN INSERT *\n                    |\"\"\".stripMargin)\n              case \"write\" =>\n                spark.table(\"src\").write.format(\"delta\")\n                  .option(\"mergeSchema\", \"true\").mode(\"append\").saveAsTable(\"dst\")\n            }\n          }\n        }\n        assert(e.getErrorClass === \"RESERVED_CDC_COLUMNS_ON_WRITE\")\n      }\n    }\n  }\n\n  test(\"changes from table by name\") {\n    withTable(\"tbl\") {\n      createTblWithThreeVersions(tblName = Some(\"tbl\"))\n\n      val readDf = cdcRead(new TableName(\"tbl\"), StartingVersion(\"0\"), EndingVersion(\"1\"))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n        readDf,\n        spark.range(20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n      )\n    }\n  }\n\n  test(\"changes from table by path\") {\n    withTempDir { dir =>\n      createTblWithThreeVersions(path = Some(dir.getAbsolutePath))\n\n      val readDf = cdcRead(\n        new TablePath(dir.getAbsolutePath), StartingVersion(\"0\"), EndingVersion(\"1\"))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, dir.getAbsolutePath),\n        readDf,\n        spark.range(20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n      )\n    }\n  }\n\n  test(\"changes - start and end are timestamps\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      // modify timestamps\n      // version 0\n      val currentTime = System.currentTimeMillis() - 5.days.toMillis\n      modifyCommitTimestamp(deltaLog, 0, currentTime + 0)\n      val tsV0 = dateFormat.format(new Date(currentTime))\n\n      // version 1\n      modifyCommitTimestamp(deltaLog, 1, currentTime + 1000)\n      val tsAfterV1 = dateFormat.format(new Date(currentTime + 2000))\n\n      modifyCommitTimestamp(deltaLog, 2, currentTime + 3000)\n\n      val readDf = cdcRead(\n        new TableName(tableName), StartingTimestamp(tsV0), EndingTimestamp(tsAfterV1))\n      // Answer should include version 0 and version 1, but not version 2.\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(tableName)),\n        readDf,\n        spark.range(20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"changes - only start is a timestamp\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      val currentTime = System.currentTimeMillis() - 5.days.toMillis\n      modifyCommitTimestamp(deltaLog, 0, currentTime + 0)\n      modifyCommitTimestamp(deltaLog, 1, currentTime + 10000)\n      modifyCommitTimestamp(deltaLog, 2, currentTime + 20000)\n\n      val ts0 = dateFormat.format(new Date(currentTime + 2000))\n      val readDf = cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingVersion(\"1\"))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(tableName)),\n        readDf,\n        spark.range(10, 20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"changes - only start is a timestamp - inclusive behavior\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      val currentTime = System.currentTimeMillis() - 5.days.toMillis\n      modifyCommitTimestamp(deltaLog, 0, currentTime + 0)\n      modifyCommitTimestamp(deltaLog, 1, currentTime + 1000)\n      modifyCommitTimestamp(deltaLog, 2, currentTime + 2000)\n\n      val ts0 = dateFormat.format(new Date(currentTime + 0))\n      val readDf = cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingVersion(\"1\"))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(tableName)),\n        readDf,\n        spark.range(20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"version from timestamp - before the first version\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      modifyCommitTimestamp(deltaLog, 0, 4000)\n      modifyCommitTimestamp(deltaLog, 1, 8000)\n      modifyCommitTimestamp(deltaLog, 2, 12000)\n\n      val ts0 = dateFormat.format(new Date(1000))\n      val ts1 = dateFormat.format(new Date(3000))\n      intercept[AnalysisException] {\n        cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingTimestamp(ts1)).collect()\n      }.getMessage.contains(\"before the earliest version\")\n    }\n  }\n\n  test(\"version from timestamp - between two valid versions\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      modifyCommitTimestamp(deltaLog, 0, 0)\n      modifyCommitTimestamp(deltaLog, 1, 4000)\n      modifyCommitTimestamp(deltaLog, 2, 8000)\n\n      val ts0 = dateFormat.format(new Date(1000))\n      val ts1 = dateFormat.format(new Date(3000))\n      val readDf = cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingTimestamp(ts1))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(tableName)),\n        readDf,\n        spark.range(0)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"version from timestamp - one version in between\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      val currentTime = System.currentTimeMillis() - 5.days.toMillis\n      modifyCommitTimestamp(deltaLog, 0, currentTime + 0)\n      modifyCommitTimestamp(deltaLog, 1, currentTime + 4000)\n      modifyCommitTimestamp(deltaLog, 2, currentTime + 8000)\n\n      val ts0 = dateFormat.format(new Date(currentTime + 3000))\n      val ts1 = dateFormat.format(new Date(currentTime + 5000))\n      val readDf = cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingTimestamp(ts1))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(tableName)),\n        readDf,\n        spark.range(10, 20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"version from timestamp - end before start\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      modifyCommitTimestamp(deltaLog, 0, 0)\n      modifyCommitTimestamp(deltaLog, 1, 4000)\n      modifyCommitTimestamp(deltaLog, 2, 8000)\n\n      val ts0 = dateFormat.format(new Date(3000))\n      val ts1 = dateFormat.format(new Date(1000))\n      intercept[DeltaIllegalArgumentException] {\n        cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingTimestamp(ts1))\n          .collect()\n      }.getErrorClass === \"DELTA_INVALID_CDC_RANGE\"\n    }\n  }\n\n  test(\"version from timestamp - end before start with one version in between\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      modifyCommitTimestamp(deltaLog, 0, 0)\n      modifyCommitTimestamp(deltaLog, 1, 4000)\n      modifyCommitTimestamp(deltaLog, 2, 8000)\n\n      val ts0 = dateFormat.format(new Date(5000))\n      val ts1 = dateFormat.format(new Date(3000))\n      intercept[DeltaIllegalArgumentException] {\n        cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingTimestamp(ts1))\n          .collect()\n      }.getErrorClass === \"DELTA_INVALID_CDC_RANGE\"\n    }\n  }\n\n  test(\"start version and end version are the same\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val readDf = cdcRead(\n        new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"0\"))\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n        readDf,\n        spark.range(10)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  for (readWithVersionNumber <- BOOLEAN_DOMAIN)\n  test(\n    s\"CDC read respects timezone and DST - readWithVersionNumber=$readWithVersionNumber\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n      val largeRetentionHours = 2 * System.currentTimeMillis().millis.toHours\n      // Set the deletedFileRetentionDuration and logRetentionDuration to a large value so that\n      // older versions can be accessed\n      spark.sql(s\"ALTER TABLE $tblName SET TBLPROPERTIES\" +\n        s\" ('delta.deletedFileRetentionDuration' = 'interval $largeRetentionHours HOURS',\" +\n        s\"'delta.logRetentionDuration' = 'interval $largeRetentionHours HOURS')\")\n\n\n      // Set commit time during Daylight savings time change.\n      val restoreDate = \"2022-11-06 01:42:44\"\n      val timestamp = dateFormat.parse(s\"$restoreDate -0800\").getTime\n      modifyCommitTimestamp(deltaLog, 0, timestamp)\n\n      // Verify DST is respected.\n      val e = intercept[Exception] {\n        cdcRead(new TableName(tblName),\n          StartingTimestamp(s\"$restoreDate -0700\"),\n          EndingTimestamp(s\"$restoreDate -0700\"))\n      }\n      assert(e.getMessage.contains(\"is before the earliest version available\"))\n\n      val readDf = if (readWithVersionNumber) {\n        cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"0\"))\n      } else {\n        cdcRead(\n          new TableName(tblName),\n          StartingTimestamp(s\"$restoreDate -0800\"),\n          EndingTimestamp(s\"$restoreDate -0800\"))\n      }\n\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(tblName)),\n        readDf,\n        spark.range(10)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"CDC read's commit timestamps are correct under different timezones\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      spark.sql(s\"CREATE OR REPLACE TABLE $tblName(id INT, name STRING, age INT) \" +\n        s\"USING DELTA TBLPROPERTIES (delta.enableChangeDataFeed = true)\")\n      spark.sql(s\"INSERT INTO $tblName(id, name, age) VALUES (1,'abc',20)\")\n      spark.sql(s\"INSERT INTO $tblName(id, name, age) VALUES (2,'def',21)\")\n      spark.sql(s\"UPDATE $tblName SET age = 19 WHERE id = 1\")\n      spark.sql(s\"INSERT INTO $tblName(id, name, age) VALUES (3,'ghi',15)\")\n      spark.sql(s\"DELETE FROM $tblName WHERE id = 3\")\n\n      // unix_timestamp() on a Timestamp column returns the UNIX timestamp of the specified\n      // time under the given SESSION_LOCAL_TIMEZONE, while collect() on a timestamp column\n      // always returns the Timestamp in UTC.\n      // By using unix_timestamp() on the commit timestamp column, we can accurately determine\n      // whether or not the timestamp under different timezones represent the same point in time.\n      val startingVersion = StartingVersion(\"0\")\n      val endingVersion = EndingVersion(\"10\")\n      spark.conf.set(SQLConf.SESSION_LOCAL_TIMEZONE.key, \"America/Chicago\")\n      val readDfChicago = cdcRead(new TableName(tblName), startingVersion, endingVersion)\n        .orderBy(CDC_COMMIT_VERSION, CDC_TYPE_COLUMN_NAME)\n        .select(col(CDC_COMMIT_VERSION), col(CDC_TYPE_COLUMN_NAME),\n          unix_timestamp(col(CDC_COMMIT_TIMESTAMP)))\n      val readDfChicagoRows = readDfChicago.collect()\n\n      spark.conf.set(SQLConf.SESSION_LOCAL_TIMEZONE.key, \"Asia/Ho_Chi_Minh\")\n      val readDfHCM = cdcRead(new TableName(tblName), startingVersion, endingVersion)\n        .orderBy(CDC_COMMIT_VERSION, CDC_TYPE_COLUMN_NAME)\n        .select(col(CDC_COMMIT_VERSION), col(CDC_TYPE_COLUMN_NAME),\n          unix_timestamp(col(CDC_COMMIT_TIMESTAMP)))\n      val readDfHCMRows = readDfHCM.collect()\n\n      spark.conf.set(SQLConf.SESSION_LOCAL_TIMEZONE.key, \"UTC\")\n      val readDfUTC = cdcRead(new TableName(tblName), startingVersion, endingVersion)\n        .orderBy(CDC_COMMIT_VERSION, CDC_TYPE_COLUMN_NAME)\n        .select(col(CDC_COMMIT_VERSION), col(CDC_TYPE_COLUMN_NAME),\n          unix_timestamp(col(CDC_COMMIT_TIMESTAMP)))\n      val readDfUTCRows = readDfUTC.collect()\n\n      def checkCDCTimestampEqual(firstRows: Array[Row], secondRows: Array[Row]): Boolean = {\n        assert(firstRows.length === secondRows.length,\n          \"Number of rows from 2 DFs should be the same.\")\n        for ((firstRow, secondRow) <- firstRows.zip(secondRows)) {\n          assert(firstRow.getLong(0) === secondRow.getLong(0),\n            \"Commit version should be the same for every rows.\")\n          assert(firstRow.getString(1) === secondRow.getString(1),\n            \"Change type should be the same for every rows.\")\n          if (firstRow.getLong(2) != secondRow.getLong(2)) {\n            return false\n          }\n        }\n        true\n      }\n\n      assert(checkCDCTimestampEqual(readDfChicagoRows, readDfHCMRows) === true)\n      assert(checkCDCTimestampEqual(readDfChicagoRows, readDfUTCRows) === true)\n      assert(checkCDCTimestampEqual(readDfHCMRows, readDfUTCRows) === true)\n    }\n  }\n\n  test(\"start version is provided and no end version\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val readDf = cdcRead(\n        new TableName(tblName), StartingVersion(\"0\"), Unbounded)\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n        readDf,\n        spark.range(30)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n    }\n  }\n\n  test(\"end timestamp < start timestamp\") {\n    withTempTable(createTable = false) { tableName =>\n      createTblWithThreeVersions(tblName = Some(tableName))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n      modifyCommitTimestamp(deltaLog, 0, 0)\n      modifyCommitTimestamp(deltaLog, 1, 1000)\n      modifyCommitTimestamp(deltaLog, 2, 2000)\n\n      val ts0 = dateFormat.format(new Date(2000))\n      val ts1 = dateFormat.format(new Date(1))\n      val e = intercept[IllegalArgumentException] {\n        cdcRead(new TableName(tableName), StartingTimestamp(ts0), EndingTimestamp(ts1))\n      }\n      assert(e.getMessage.contains(\"End cannot be before start\"))\n    }\n  }\n\n  test(\"end version < start version\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n      val e = intercept[IllegalArgumentException] {\n        cdcRead(new TableName(tblName), StartingVersion(\"1\"), EndingVersion(\"0\"))\n      }\n      assert(e.getMessage.contains(\"End cannot be before start\"))\n    }\n  }\n\n  test(\"cdc result dataframe can be transformed further\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val cdcResult = cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"1\"))\n      val transformedDf = cdcResult\n        .drop(CDC_COMMIT_TIMESTAMP)\n        .withColumn(\"col3\", lit(0))\n        .withColumn(\"still_there\", col(\"_change_type\"))\n\n      checkAnswer(\n        transformedDf,\n        spark.range(20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n          .withColumn(\"col3\", lit(0))\n          .withColumn(\"still_there\", col(\"_change_type\"))\n      )\n    }\n  }\n\n  test(\"multiple references on same table\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val cdcResult0_1 = cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"1\"))\n      val cdcResult0_2 = cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"2\"))\n\n      val diff = cdcResult0_2.except(cdcResult0_1)\n\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n        diff,\n        spark.range(20, 30)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType))\n      )\n    }\n  }\n\n  test(\"filtering cdc metadata columns\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n      val deltaTable = io.delta.tables.DeltaTable.forName(\"tbl\")\n      deltaTable.delete(\"id > 20\")\n\n      val cdcResult = cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"3\"))\n\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n        cdcResult.filter(\"_change_type != 'insert'\"),\n        spark.range(21, 30)\n          .withColumn(\"_change_type\", lit(\"delete\"))\n          .withColumn(\"_commit_version\", lit(3))\n      )\n\n      checkCDCAnswer(\n        DeltaLog.forTable(spark, TableIdentifier(\"tbl\")),\n        cdcResult.filter(\"_commit_version = 1\"),\n        spark.range(10, 20)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", lit(1))\n      )\n    }\n  }\n\n  test(\"aggregating non-numeric cdc data columns\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).selectExpr(\"id\", \"'text' as text\")\n          .write.format(\"delta\").saveAsTable(tableName)\n      val deltaTable = io.delta.tables.DeltaTable.forName(tableName)\n      deltaTable.delete(\"id > 5\")\n\n      val cdcResult = cdcRead(new TableName(tableName), StartingVersion(\"0\"), EndingVersion(\"3\"))\n\n      checkAnswer(\n        cdcResult.selectExpr(\"count(distinct text)\"),\n        Row(1)\n      )\n\n      checkAnswer(\n        cdcResult.selectExpr(\"first(text)\"),\n        Row(\"text\")\n      )\n    }\n  }\n\n  test(\"ending version not specified resolves to latest at execution time\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(5).selectExpr(\"id\", \"'text' as text\")\n        .write.format(\"delta\").saveAsTable(tableName)\n      val cdcResult = cdcRead(new TableName(tableName), StartingVersion(\"0\"), Unbounded)\n\n      checkAnswer(\n        cdcResult.selectExpr(\"id\", \"_change_type\", \"_commit_version\"),\n        Row(0, \"insert\", 0) :: Row(1, \"insert\", 0) :: Row(2, \"insert\", 0) ::\n          Row(3, \"insert\", 0):: Row(4, \"insert\", 0) :: Nil\n      )\n\n      // The next scan of `cdcResult` should include this delete even though the DF was defined\n      // before it.\n      val deltaTable = io.delta.tables.DeltaTable.forName(tableName)\n      deltaTable.delete(\"id > 2\")\n\n      checkAnswer(\n        cdcResult.selectExpr(\"id\", \"_change_type\", \"_commit_version\"),\n        Row(0, \"insert\", 0) :: Row(1, \"insert\", 0) :: Row(2, \"insert\", 0) ::\n          Row(3, \"insert\", 0):: Row(4, \"insert\", 0) ::\n          Row(3, \"delete\", 1):: Row(4, \"delete\", 1) :: Nil\n      )\n    }\n  }\n\n  test(\"table schema changed after dataframe with ending specified\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(5).selectExpr(\"id\", \"'text' as text\")\n        .write.format(\"delta\").saveAsTable(tableName)\n      val cdcResult = cdcRead(new TableName(tableName), StartingVersion(\"0\"), EndingVersion(\"1\"))\n      sql(s\"ALTER TABLE $tableName ADD COLUMN (newCol INT)\")\n\n      checkAnswer(\n        cdcResult.selectExpr(\"id\", \"_change_type\", \"_commit_version\"),\n        Row(0, \"insert\", 0) :: Row(1, \"insert\", 0) :: Row(2, \"insert\", 0) ::\n          Row(3, \"insert\", 0) :: Row(4, \"insert\", 0) :: Nil\n      )\n    }\n  }\n\n  test(\"table schema changed after dataframe with ending not specified\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(5).selectExpr(\"id\", \"'text' as text\")\n        .write.format(\"delta\").saveAsTable(tableName)\n      val cdcResult = cdcRead(new TableName(tableName), StartingVersion(\"0\"), Unbounded)\n      sql(s\"ALTER TABLE $tableName ADD COLUMN (newCol STRING)\")\n      sql(s\"INSERT INTO $tableName VALUES (5, 'text', 'newColVal')\")\n\n      // Just ignoring the new column is pretty weird, but it's what we do for non-CDC dataframes,\n      // so we preserve the behavior rather than adding a special case.\n      checkAnswer(\n        cdcResult.selectExpr(\"id\", \"_change_type\", \"_commit_version\"),\n        Row(0, \"insert\", 0) :: Row(1, \"insert\", 0) :: Row(2, \"insert\", 0) ::\n          Row(3, \"insert\", 0) :: Row(4, \"insert\", 0) :: Row(5, \"insert\", 2) :: Nil\n      )\n    }\n  }\n\n  test(\"An error should be thrown when CDC is not enabled\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"false\") {\n        // create version with cdc disabled - v0\n        spark.range(10).write.format(\"delta\").saveAsTable(tblName)\n      }\n      val deltaTable = io.delta.tables.DeltaTable.forName(tblName)\n      // v1\n      deltaTable.delete(\"id > 8\")\n\n      // v2\n      sql(s\"ALTER TABLE ${tblName} SET TBLPROPERTIES \" +\n        s\"(${DeltaConfigs.CHANGE_DATA_FEED.key}=true)\")\n\n      // v3\n      spark.range(10, 20).write.format(\"delta\").mode(\"append\").saveAsTable(tblName)\n\n      // v4\n      deltaTable.delete(\"id > 18\")\n\n      // v5\n      sql(s\"ALTER TABLE ${tblName} SET TBLPROPERTIES \" +\n        s\"(${DeltaConfigs.CHANGE_DATA_FEED.key}=false)\")\n\n      var e = intercept[AnalysisException] {\n        cdcRead(new TableName(tblName), StartingVersion(\"0\"), EndingVersion(\"4\")).collect()\n      }\n      assert(e.getMessage === DeltaErrors.changeDataNotRecordedException(0, 0, 4).getMessage)\n\n      val cdcDf = cdcRead(new TableName(tblName), StartingVersion(\"2\"), EndingVersion(\"4\"))\n      assert(cdcDf.count() == 11) // 10 rows inserted, 1 row deleted\n\n      // Check that we correctly detect CDC is disabled and fail the query for multiple types of\n      // ranges:\n      //  * disabled at the end but not start - (2, 5)\n      //  * disabled at the start but not end - (1, 4)\n      //  * disabled at both start and end (even though enabled in the middle) - (1, 5)\n      for ((start, end, firstDisabledVersion) <- Seq((2, 5, 5), (1, 4, 1), (1, 5, 1))) {\n        e = intercept[AnalysisException] {\n          cdcRead(\n            new TableName(tblName),\n            StartingVersion(start.toString), EndingVersion(end.toString)).collect()\n        }\n        assert(e.getMessage === DeltaErrors.changeDataNotRecordedException(\n          firstDisabledVersion, start, end).getMessage)\n      }\n    }\n  }\n\n  test(\"changes - start timestamp exceeding latest commit timestamp\") {\n    withTempTable(createTable = false) { tableName =>\n      withSQLConf(DeltaSQLConf.DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP.key -> \"true\") {\n        createTblWithThreeVersions(tblName = Some(tableName))\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n        // modify timestamps\n        // version 0\n        modifyCommitTimestamp(deltaLog, 0, 0)\n\n        // version 1\n        modifyCommitTimestamp(deltaLog, 1, 1000)\n\n        // version 2\n        modifyCommitTimestamp(deltaLog, 2, 2000)\n        val tsStart = dateFormat.format(new Date(3000))\n        val tsEnd = dateFormat.format(new Date(4000))\n\n        val readDf = cdcRead(\n          new TableName(tableName), StartingTimestamp(tsStart), EndingTimestamp(tsEnd))\n        checkCDCAnswer(\n          DeltaLog.forTable(spark, TableIdentifier(tableName)),\n          readDf,\n          sqlContext.emptyDataFrame)\n      }\n    }\n  }\n\n  test(\"changes - end timestamp exceeding latest commit timestamp\") {\n    withTempTable(createTable = false) { tableName =>\n      withSQLConf(DeltaSQLConf.DELTA_CDF_ALLOW_OUT_OF_RANGE_TIMESTAMP.key -> \"true\") {\n        createTblWithThreeVersions(tblName = Some(tableName))\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n        val currentTime = System.currentTimeMillis() - 5.days.toMillis\n        // modify timestamps\n        // version 0\n        modifyCommitTimestamp(deltaLog, 0, currentTime + 0)\n\n        // version 1\n        modifyCommitTimestamp(deltaLog, 1, currentTime + 1000)\n\n        // version 2\n        modifyCommitTimestamp(deltaLog, 2, currentTime + 2000)\n\n        val tsStart = dateFormat.format(new Date(currentTime + 0))\n        val tsEnd = dateFormat.format(new Date(currentTime + 4000))\n\n        val readDf = cdcRead(\n          new TableName(tableName), StartingTimestamp(tsStart), EndingTimestamp(tsEnd))\n        checkCDCAnswer(\n          DeltaLog.forTable(spark, TableIdentifier(tableName)),\n          readDf,\n          spark.range(30)\n            .withColumn(\"_change_type\", lit(\"insert\"))\n            .withColumn(\"_commit_version\", (col(\"id\") / 10).cast(LongType)))\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite + CDF\") {\n    withSQLConf(\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n      DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempTable(createTable = false) { tableName =>\n        def data: DataFrame = spark.read.format(\"delta\").table(tableName)\n\n        Seq((\"a\", \"x\"), (\"b\", \"y\"), (\"c\", \"x\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .saveAsTable(tableName)\n        checkAnswer(\n          cdcRead(new TableName(tableName), StartingVersion(\"0\"), EndingVersion(\"0\"))\n            .drop(CDC_COMMIT_TIMESTAMP),\n          Row(\"a\", \"x\", \"insert\", 0) :: Row(\"b\", \"y\", \"insert\", 0) ::\n            Row(\"c\", \"x\", \"insert\", 0) :: Nil\n        )\n\n        // ovewrite nothing\n        Seq((\"d\", \"z\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .saveAsTable(tableName)\n        checkDatasetUnorderly(data.select(\"value\", \"part\").as[(String, String)],\n          (\"a\", \"x\"), (\"b\", \"y\"), (\"c\", \"x\"), (\"d\", \"z\"))\n        checkAnswer(\n          cdcRead(new TableName(tableName), StartingVersion(\"1\"), EndingVersion(\"1\"))\n            .drop(CDC_COMMIT_TIMESTAMP),\n          Row(\"d\", \"z\", \"insert\", 1) :: Nil\n        )\n\n        // overwrite partition `part`=\"x\"\n        Seq((\"a\", \"x\"), (\"e\", \"x\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .saveAsTable(tableName)\n        checkDatasetUnorderly(data.select(\"value\", \"part\").as[(String, String)],\n          (\"a\", \"x\"), (\"b\", \"y\"), (\"d\", \"z\"), (\"e\", \"x\"))\n        checkAnswer(\n          cdcRead(new TableName(tableName), StartingVersion(\"2\"), EndingVersion(\"2\"))\n            .drop(CDC_COMMIT_TIMESTAMP),\n          Row(\"a\", \"x\", \"delete\", 2) :: Row(\"c\", \"x\", \"delete\", 2) ::\n            Row(\"a\", \"x\", \"insert\", 2) :: Row(\"e\", \"x\", \"insert\", 2) :: Nil\n        )\n      }\n    }\n  }\n}\n\nclass DeltaCDCScalaSuite extends DeltaCDCSuiteBase {\n\n  /** Single method to do all kinds of CDC reads */\n  def cdcRead(\n      tblId: TblId,\n      start: Boundary,\n      end: Boundary,\n      schemaMode: Option[DeltaBatchCDFSchemaMode] = Some(BatchCDFSchemaLegacy),\n      readerOptions: Map[String, String] = Map.empty): DataFrame = {\n\n    // Set the batch CDF schema mode using SQL conf if we specified it\n    if (schemaMode.isDefined) {\n      var result: DataFrame = null\n      withSQLConf(DeltaSQLConf.DELTA_CDF_DEFAULT_SCHEMA_MODE_FOR_COLUMN_MAPPING_TABLE.key ->\n        schemaMode.get.name) {\n        result = cdcRead(tblId, start, end, None, readerOptions)\n      }\n      return result\n    }\n\n    val startPrefix: (String, String) = start match {\n      case startingVersion: StartingVersion =>\n        (\"startingVersion\", startingVersion.value)\n\n      case startingTimestamp: StartingTimestamp =>\n        (\"startingTimestamp\", startingTimestamp.value)\n\n      case Unbounded =>\n        (\"\", \"\")\n    }\n    val endPrefix: (String, String) = end match {\n      case endingVersion: EndingVersion =>\n        (\"endingVersion\", endingVersion.value)\n\n      case endingTimestamp: EndingTimestamp =>\n        (\"endingTimestamp\", endingTimestamp.value)\n\n      case Unbounded =>\n        (\"\", \"\")\n    }\n\n    var dfr = spark.read.format(\"delta\")\n      .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n      .option(startPrefix._1, startPrefix._2)\n      .option(endPrefix._1, endPrefix._2)\n\n    readerOptions.foreach { case (k, v) =>\n      dfr = dfr.option(k, v)\n    }\n\n    tblId match {\n      case path: TablePath =>\n        dfr.load(path.id)\n\n      case tblName: TableName =>\n        dfr.table(tblName.id)\n\n      case _ =>\n        throw new IllegalArgumentException(\"No table name or path provided\")\n    }\n  }\n\n  private def testNullRangeBoundary(start: Boundary, end: Boundary): Unit = {\n    test(s\"range boundary cannot be null - start=$start end=$end\") {\n      val tblName = \"tbl\"\n      withTable(tblName) {\n        createTblWithThreeVersions(tblName = Some(tblName))\n\n        val expectedError = (start, end) match {\n          case (StartingVersion(null), _) => \"DELTA_VERSION_INVALID\"\n          case (StartingTimestamp(null), _) => \"DELTA_TIMESTAMP_INVALID\"\n          case (_, EndingVersion(null)) => \"DELTA_VERSION_INVALID\"\n          case (_, EndingTimestamp(null)) => \"DELTA_TIMESTAMP_INVALID\"\n        }\n        val expectedErrorParameters = expectedError match {\n          case \"DELTA_VERSION_INVALID\" => Map(\"version\" -> \"null\")\n          case \"DELTA_TIMESTAMP_INVALID\" => Map(\"expr\" -> \"NULL\")\n        }\n\n        checkError(\n          intercept[DeltaAnalysisException] {\n            cdcRead(new TableName(tblName), start, end)\n          },\n          expectedError,\n          parameters = expectedErrorParameters)\n      }\n    }\n  }\n\n  for {\n    start <- Seq(StartingVersion(\"0\"), StartingTimestamp(dateFormat.format(new Date(1))))\n    end <- Seq(EndingVersion(null), EndingTimestamp(null))\n  } {\n    testNullRangeBoundary(start, end)\n  }\n\n  for {\n    start <- Seq(StartingVersion(null), StartingTimestamp(null))\n    end <- Seq(\n      Unbounded,\n      EndingVersion(null),\n      EndingTimestamp(null),\n      EndingVersion(\"0\"),\n      EndingTimestamp(dateFormat.format(new Date(1))))\n  } {\n    testNullRangeBoundary(start, end)\n  }\n\n  test(\"filters should be pushed down\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n      val plans = DeltaTestUtils.withAllPlansCaptured(spark) {\n        val res = spark.read.format(\"delta\")\n          .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n          .option(\"startingVersion\", 0)\n          .option(\"endingVersion\", 1)\n          .table(tblName)\n          .select(\"id\", \"_change_type\")\n          .where(col(\"id\") < lit(5))\n        assert(res.columns === Seq(\"id\", \"_change_type\"))\n        checkAnswer(\n          res,\n          spark.range(5)\n            .withColumn(\"_change_type\", lit(\"insert\")))\n      }\n      assert(plans.map(_.executedPlan).toString\n        .contains(\"PushedFilters: [*IsNotNull(id), *LessThan(id,5)]\"))\n    }\n  }\n\n  test(\"start version or timestamp is not provided\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n\n      val e = intercept[AnalysisException] {\n        spark.read.format(\"delta\")\n          .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n          .option(\"endingVersion\", 1)\n          .table(tblName)\n          .show()\n      }\n      assert(e.getMessage.contains(DeltaErrors.noStartVersionForCDC().getMessage))\n    }\n  }\n\n  test(\"Not having readChangeFeed will not output cdc columns\") {\n    val tblName = \"tbl2\"\n    withTable(tblName) {\n      spark.range(0, 10).write.format(\"delta\").saveAsTable(tblName)\n      checkAnswer(spark.read.format(\"delta\").table(tblName), spark.range(0, 10).toDF(\"id\"))\n\n      checkAnswer(\n        spark.read.format(\"delta\")\n          .option(\"startingVersion\", \"0\")\n          .option(\"endingVersion\", \"0\")\n          .table(tblName),\n        spark.range(0, 10).toDF(\"id\"))\n    }\n  }\n\n  test(\"non-monotonic timestamps\") {\n    withTempTable(createTable = false) { tableName =>\n      var deltaLog: DeltaLog = null\n      val currentTime = System.currentTimeMillis() - 5.days.toMillis\n      (0 to 3).foreach { i =>\n        spark.range(i * 10, (i + 1) * 10).write.format(\"delta\").mode(\"append\")\n          .saveAsTable(tableName)\n        if (i == 0) {\n          deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        }\n        val file = new File(FileNames.unsafeDeltaFile(deltaLog.logPath, i).toUri)\n        file.setLastModified(currentTime + 300 - i)\n      }\n\n      checkCDCAnswer(\n        deltaLog,\n        cdcRead(new TableName(tableName), StartingVersion(\"0\"), EndingVersion(\"3\")),\n        spark.range(0, 40)\n          .withColumn(\"_change_type\", lit(\"insert\"))\n          .withColumn(\"_commit_version\", floor(col(\"id\") / 10)))\n    }\n  }\n\n  test(\"Repeated delete\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(0, 5, 1, numPartitions = 1).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      sql(s\"DELETE FROM $tableName WHERE id = 3\") // Version 1\n      sql(s\"DELETE FROM $tableName WHERE id = 4\") // Version 2\n      sql(s\"DELETE FROM $tableName WHERE id IN (0, 1, 2)\") // Version 3, remove the whole file\n\n      val allChanges: Map[Int, Seq[Row]] = Map(\n        1 -> (Row(3, \"delete\", 1) :: Nil),\n        2 -> (Row(4, \"delete\", 2) :: Nil),\n        3 -> (Row(0, \"delete\", 3) :: Row(1, \"delete\", 3) :: Row(2, \"delete\", 3) :: Nil)\n      )\n\n      for(start <- 1 to 3; end <- start to 3) {\n        checkCDCAnswer(\n          deltaLog,\n          cdcRead(\n            new TableName(tableName), StartingVersion(start.toString), EndingVersion(end.toString)),\n         (start to end).flatMap(v => allChanges(v)))\n      }\n    }\n  }\n\n  test(\"reader should accept case insensitive option\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      createTblWithThreeVersions(tblName = Some(tblName))\n      val res = spark.read.format(\"delta\")\n        .option(\"ReadChangeFEED\", \"tRuE\")\n        .option(\"STARTINGVERSION\", 0)\n        .option(\"endingVersion\", 1)\n        .table(tblName)\n        .select(\"id\", \"_change_type\")\n      assert(res.columns === Seq(\"id\", \"_change_type\"))\n      checkAnswer(\n        res,\n        spark.range(20).withColumn(\"_change_type\", lit(\"insert\")))\n\n      val resLegacy = spark.read.format(\"delta\")\n        .option(\"READCHANGEDATA\", \"TruE\")\n        .option(\"startingversion\", 0)\n        .option(\"ENDINGVERSION\", 1)\n        .table(tblName)\n        .select(\"id\", \"_change_type\")\n      assert(resLegacy.columns === Seq(\"id\", \"_change_type\"))\n      checkAnswer(\n        resLegacy,\n        spark.range(20).withColumn(\"_change_type\", lit(\"insert\")))\n    }\n  }\n\n}\n\nclass DeltaCDCScalaWithDeletionVectorsSuite extends DeltaCDCScalaSuite\n  with DeletionVectorsTestUtils {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectorsForAllSupportedOperations(spark)\n  }\n}\n\nclass DeltaCDCScalaWithCatalogOwnedBatch1Suite extends DeltaCDCScalaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaCDCScalaWithCatalogOwnedBatch2Suite extends DeltaCDCScalaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaCDCScalaWithCatalogOwnedBatch100Suite extends DeltaCDCScalaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCheckpointWithStructColsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions.{col, lit, struct}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nclass DeltaCheckpointWithStructColsSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  protected val checkpointFnsWithStructAndJsonStats: Seq[DeltaLog => Long] = Seq(\n    checkpointWithProperty(writeStatsAsJson = Some(true)),\n    checkpointWithProperty(writeStatsAsJson = None))\n\n  protected val checkpointFnsWithStructWithoutJsonStats: Seq[DeltaLog => Long] = Seq(\n    checkpointWithProperty(writeStatsAsJson = Some(false)))\n\n  protected val checkpointFnsWithoutStructWithJsonStats: Seq[DeltaLog => Long] = Seq(\n    checkpointWithProperty(writeStatsAsJson = Some(true), writeStatsAsStruct = false),\n    checkpointWithProperty(writeStatsAsJson = None, writeStatsAsStruct = false))\n\n  /**\n   * Creates a table from the given DataFrame and partitioning. Then for each checkpointing\n   * function, it runs the given validation function.\n   */\n  protected def checkpointSchemaForTable(df: DataFrame, partitionBy: String*)(\n      checkpointingFns: Seq[DeltaLog => Long],\n      expectedCols: Seq[(String, DataType)],\n      additionalValidationFn: Set[String] => Unit = _ => ()): Unit = {\n    checkpointingFns.foreach { checkpointingFn =>\n      withTempDir { dir =>\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        df.write.format(\"delta\").partitionBy(partitionBy: _*).save(dir.getCanonicalPath)\n        val version = checkpointingFn(deltaLog)\n\n        val f = spark.read.parquet(\n          FileNames.checkpointFileSingular(deltaLog.logPath, version).toString)\n        assert(f.schema.getFieldIndex(\"commitInfo\").isEmpty,\n          \"commitInfo should not be written to the checkpoint\")\n        val baseCols = Set(\"add\", \"metaData\", \"protocol\", \"remove\", \"txn\")\n        baseCols.foreach { name =>\n          assert(f.schema.getFieldIndex(name).nonEmpty, s\"Couldn't find required field $name \" +\n            s\"among: ${f.schema.fieldNames.mkString(\"[\", \", \", \" ]\")}\")\n        }\n\n        val addSchema = f.schema(\"add\").dataType.asInstanceOf[StructType]\n        val addColumns = SchemaMergingUtils.explodeNestedFieldNames(addSchema).toSet\n\n        val requiredCols = Seq(\n          \"path\" -> StringType,\n          \"partitionValues\" -> MapType(StringType, StringType),\n          \"size\" -> LongType,\n          \"modificationTime\" -> LongType,\n          \"dataChange\" -> BooleanType,\n          \"tags\" -> MapType(StringType, StringType)\n        )\n\n        val schema = deltaLog.update().schema\n        (requiredCols ++ expectedCols).foreach { case (expectedField, dataType) =>\n          // use physical name if possible\n          val expectedPhysicalField =\n            convertColumnNameToAttributeWithPhysicalName(expectedField, schema).name\n          assert(addColumns.contains(expectedPhysicalField))\n          // Check data type\n          assert(f.select(col(s\"add.$expectedPhysicalField\")).schema.head.dataType === dataType)\n        }\n\n        additionalValidationFn(addColumns)\n\n        DeltaLog.clearCache()\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getCanonicalPath),\n          df\n        )\n      }\n    }\n  }\n\n  test(\"unpartitioned table\") {\n    val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n    checkpointSchemaForTable(df)(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          unexpected = Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME))\n      }\n    )\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = false,\n          unexpected = Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME))\n      }\n    )\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          unexpected = Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME))\n      }\n    )\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = Seq(checkpointWithoutStats),\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns, statsAsJsonExists = false, Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME))\n      }\n    )\n  }\n\n  test(\"partitioned table\") {\n    val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n    // partitioned by \"part\"\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = Seq(\"partitionValues_parsed.part\" -> IntegerType),\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = Seq(\"partitionValues_parsed.part\" -> IntegerType),\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = false,\n          Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME))\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = Seq(checkpointWithoutStats),\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns, statsAsJsonExists = false, Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME))\n      }\n    )\n  }\n\n  test(\"special characters\") {\n    val weirdName1 = \"part%!@#_$%^&*-\"\n    val weirdName2 = \"part?_.+<>|/\"\n    val df = spark.range(10)\n      .withColumn(weirdName1, ('id / 2).cast(\"int\"))\n      .withColumn(weirdName2, ('id / 3).cast(\"int\"))\n      .withColumn(\"struct\", struct($\"id\", col(weirdName1), $\"id\".as(weirdName2)))\n\n    val structColumns = Seq(\n      s\"partitionValues_parsed.$weirdName1\" -> IntegerType,\n      s\"partitionValues_parsed.`$weirdName2`\" -> IntegerType)\n\n    // partitioned by weirdName1, weirdName2\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = structColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = false,\n          Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          structColumns.map(_._1))\n      }\n    )\n  }\n\n  test(\"timestamps as partition values\") {\n    withTempDir { dir =>\n      val df = Seq(\n        (java.sql.Timestamp.valueOf(\"2012-12-31 16:00:10.011\"), 2),\n        (java.sql.Timestamp.valueOf(\"2099-12-31 16:00:10.011\"), 4)).toDF(\"key\", \"value\")\n\n      df.write.format(\"delta\").partitionBy(\"key\").save(dir.getCanonicalPath)\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val version = checkpointWithProperty(\n        writeStatsAsJson = Some(true), writeStatsAsStruct = true)(deltaLog)\n      val f = spark.read.parquet(\n        FileNames.checkpointFileSingular(deltaLog.logPath, version).toString)\n\n      // use physical name\n      val key = getPhysicalName(\"key\", deltaLog.snapshot.schema)\n      checkAnswer(\n        f.select(s\"add.partitionValues_parsed.`$key`\"),\n        Seq(Row(null), Row(null)) ++ df.select(\"key\").collect()\n      )\n\n      sql(s\"DELETE FROM delta.`${dir.getCanonicalPath}` WHERE CURRENT_TIMESTAMP > key\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir.getCanonicalPath),\n        Row(java.sql.Timestamp.valueOf(\"2099-12-31 16:00:10.011\"), 4)\n      )\n\n      sql(s\"DELETE FROM delta.`${dir.getCanonicalPath}` WHERE CURRENT_TIMESTAMP < key\")\n    }\n  }\n\n\n  /**\n   * Creates a checkpoint by based on `writeStatsAsJson`/`writeStatsAsStruct` properties.\n   */\n  protected def checkpointWithProperty(\n      writeStatsAsJson: Option[Boolean],\n      writeStatsAsStruct: Boolean = true)(deltaLog: DeltaLog): Long = {\n    val asJson = writeStatsAsJson.map { v =>\n      s\", delta.checkpoint.writeStatsAsJson = $v\"\n    }.getOrElse(\"\")\n    sql(s\"ALTER TABLE delta.`${deltaLog.dataPath}` \" +\n      s\"SET TBLPROPERTIES (delta.checkpoint.writeStatsAsStruct = ${writeStatsAsStruct}${asJson})\")\n    deltaLog.checkpoint()\n    deltaLog.readLastCheckpointFile().get.version\n  }\n\n  /** A checkpoint that doesn't have any stats columns, i.e. `stats` and `stats_parsed`. */\n  protected def checkpointWithoutStats(deltaLog: DeltaLog): Long = {\n    sql(s\"ALTER TABLE delta.`${deltaLog.dataPath}` \" +\n      s\"SET TBLPROPERTIES (delta.checkpoint.writeStatsAsStruct = false, \" +\n      \"delta.checkpoint.writeStatsAsJson = false)\")\n    deltaLog.checkpoint()\n    deltaLog.readLastCheckpointFile().get.version\n  }\n\n  /**\n   * Check the existence of the stats field and also not existence of the `unexpected` fields. The\n   * `addColumns` is a Set of column names that contain the entire tree of columns in the `add`\n   * field of the schema.\n   */\n  protected def checkFields(\n      addColumns: Set[String],\n      statsAsJsonExists: Boolean,\n      unexpected: Seq[String]): Unit = {\n    if (statsAsJsonExists) {\n      assert(addColumns.contains(\"stats\"))\n    } else {\n      assert(!addColumns.contains(\"stats\"))\n    }\n    unexpected.foreach { colName =>\n      assert(!addColumns.contains(colName), s\"$colName shouldn't be part of the \" +\n        \"schema because it is of null type.\")\n    }\n  }\n\n  test(\"unpartitioned table - check stats\") {\n    val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n\n    val structStatsColumns = Seq(\n      \"stats_parsed.numRecords\" -> LongType,\n      \"stats_parsed.minValues.id\" -> LongType,\n      \"stats_parsed.maxValues.id\" -> LongType,\n      \"stats_parsed.nullCount.id\" -> LongType,\n      \"stats_parsed.minValues.part\" -> IntegerType,\n      \"stats_parsed.maxValues.part\" -> IntegerType,\n      \"stats_parsed.nullCount.part\" -> LongType)\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = structStatsColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = true, Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = structStatsColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = false, Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          unexpected = Seq(Checkpoints.STRUCT_PARTITIONS_COL_NAME) ++ structStatsColumns.map(_._1))\n      }\n    )\n\n    checkpointSchemaForTable(df)(\n      checkpointingFns = Seq(checkpointWithoutStats),\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = false, structStatsColumns.map(_._1))\n      }\n    )\n  }\n\n  test(\"use kill switch to disable stats as struct in checkpoint\") {\n    withSQLConf(DeltaSQLConf.STATS_AS_STRUCT_IN_CHECKPOINT_FORCE_DISABLED.key -> \"true\") {\n      val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n\n      val structStatsColumns = Seq(\n        \"stats_parsed.numRecords\",\n        \"stats_parsed.minValues.id\",\n        \"stats_parsed.maxValues.id\",\n        \"stats_parsed.nullCount.id\",\n        \"stats_parsed.minValues.part\",\n        \"stats_parsed.maxValues.part\",\n        \"stats_parsed.nullCount.part\")\n\n      checkpointSchemaForTable(df)(\n        checkpointingFns = checkpointFnsWithStructAndJsonStats,\n        expectedCols = Nil,\n        additionalValidationFn = addColumns => {\n          checkFields(\n            addColumns,\n            statsAsJsonExists = true,\n            unexpected = structStatsColumns\n          )\n        }\n      )\n    }\n  }\n\n  test(\"partitioned table - check stats\") {\n    val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n\n    val structStatsColumns = Seq(\n      \"stats_parsed.numRecords\" -> LongType,\n      \"stats_parsed.minValues.id\" -> LongType,\n      \"stats_parsed.maxValues.id\" -> LongType,\n      \"stats_parsed.nullCount.id\" -> LongType)\n\n    // partitioned by \"part\"\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = structStatsColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = true, Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = structStatsColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = false, Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          structStatsColumns.map(_._1))\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = Seq(checkpointWithoutStats),\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = false, structStatsColumns.map(_._1))\n      }\n    )\n  }\n\n  test(\"nested fields, dots, and boolean types\") {\n    val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n      .withColumn(\"struct\", struct($\"id\", $\"part\", $\"id\".as(\"with.dot\")))\n      .withColumn(\"bool\", lit(true))\n\n    val structColumns = Seq(\n      \"partitionValues_parsed.part\" -> IntegerType,\n      \"stats_parsed.numRecords\" -> LongType,\n      \"stats_parsed.minValues.id\" -> LongType,\n      \"stats_parsed.maxValues.id\" -> LongType,\n      \"stats_parsed.nullCount.id\" -> LongType,\n      \"stats_parsed.minValues.struct.id\" -> LongType,\n      \"stats_parsed.maxValues.struct.id\" -> LongType,\n      \"stats_parsed.nullCount.struct.id\" -> LongType,\n      \"stats_parsed.minValues.struct.part\" -> IntegerType,\n      \"stats_parsed.maxValues.struct.part\" -> IntegerType,\n      \"stats_parsed.nullCount.struct.part\" -> LongType,\n      \"stats_parsed.minValues.struct.`with.dot`\" -> LongType,\n      \"stats_parsed.maxValues.struct.`with.dot`\" -> LongType,\n      \"stats_parsed.nullCount.struct.`with.dot`\" -> LongType,\n      \"stats_parsed.nullCount.bool\" -> LongType)\n\n    val unexpectedCols = Seq(\n      \"stats_parsed.minValues.bool\",\n      \"stats_parsed.maxValues.bool\"\n    )\n\n    // partitioned by \"part\"\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = structColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = true, unexpectedCols)\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = structColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = false, unexpectedCols)\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          unexpectedCols ++ structColumns.map(_._1))\n      }\n    )\n\n    checkpointSchemaForTable(df, \"part\")(\n      checkpointingFns = Seq(checkpointWithoutStats),\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = false,\n          unexpectedCols ++ structColumns.map(_._1))\n      }\n    )\n  }\n\n  test(\"special characters - check stats\") {\n    val weirdName1 = \"part%!@#_$%^&*-\"\n    val weirdName2 = \"part?_.+<>|/\"\n    val df = spark.range(10)\n      .withColumn(weirdName1, ('id / 2).cast(\"int\"))\n      .withColumn(weirdName2, ('id / 3).cast(\"int\"))\n      .withColumn(\"struct\", struct($\"id\", col(weirdName1), $\"id\".as(weirdName2)))\n\n    val structColumns = Seq(\n      \"stats_parsed.numRecords\" -> LongType,\n      \"stats_parsed.minValues.id\" -> LongType,\n      \"stats_parsed.maxValues.id\" -> LongType,\n      \"stats_parsed.nullCount.id\" -> LongType,\n      \"stats_parsed.minValues.struct.id\" -> LongType,\n      \"stats_parsed.maxValues.struct.id\" -> LongType,\n      \"stats_parsed.nullCount.struct.id\" -> LongType,\n      s\"stats_parsed.minValues.struct.$weirdName1\" -> IntegerType,\n      s\"stats_parsed.maxValues.struct.$weirdName1\" -> IntegerType,\n      s\"stats_parsed.nullCount.struct.$weirdName1\" -> LongType,\n      s\"stats_parsed.minValues.struct.`$weirdName2`\" -> LongType,\n      s\"stats_parsed.maxValues.struct.`$weirdName2`\" -> LongType,\n      s\"stats_parsed.nullCount.struct.`$weirdName2`\" -> LongType,\n      s\"partitionValues_parsed.$weirdName1\" -> IntegerType,\n      s\"partitionValues_parsed.`$weirdName2`\" -> IntegerType)\n\n    // partitioned by weirdName1, weirdName2\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = checkpointFnsWithStructAndJsonStats,\n      expectedCols = structColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = true, Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n      expectedCols = structColumns,\n      additionalValidationFn = addColumns => {\n        checkFields(addColumns, statsAsJsonExists = false, Nil)\n      }\n    )\n\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = true,\n          structColumns.map(_._1))\n      }\n    )\n\n    checkpointSchemaForTable(df, weirdName1, weirdName2)(\n      checkpointingFns = Seq(checkpointWithoutStats),\n      expectedCols = Nil,\n      additionalValidationFn = addColumns => {\n        checkFields(\n          addColumns,\n          statsAsJsonExists = false,\n          structColumns.map(_._1))\n      }\n    )\n  }\n\n  test(\"no data column stats collected + unpartitioned\") {\n    val df = spark.range(10).withColumn(\"part\", ('id / 2).cast(\"int\"))\n      .withColumn(\"struct\", struct($\"id\", $\"part\", $\"id\".as(\"with.dot\")))\n      .withColumn(\"bool\", lit(true))\n\n    val expectedColumns = Seq(\"stats_parsed.numRecords\" -> LongType)\n\n    val unexpected = Seq(\n      \"stats_parsed.minValues\",\n      \"stats_parsed.maxValues\",\n      \"stats_parsed.nullCount\",\n      \"stats_parsed.minValues.id\",\n      \"stats_parsed.maxValues.id\",\n      \"stats_parsed.nullCount.id\",\n      \"stats_parsed.minValues.struct.id\",\n      \"stats_parsed.maxValues.struct.id\",\n      \"stats_parsed.nullCount.struct.id\",\n      \"stats_parsed.minValues.struct.part\",\n      \"stats_parsed.maxValues.struct.part\",\n      \"stats_parsed.nullCount.struct.part\",\n      \"stats_parsed.minValues.struct.`with.dot`\",\n      \"stats_parsed.maxValues.struct.`with.dot`\",\n      \"stats_parsed.nullCount.struct.`with.dot`\",\n      \"stats_parsed.nullCount.bool\",\n      \"stats_parsed.minValues.bool\",\n      \"stats_parsed.maxValues.bool\",\n      \"partitionValues_parsed\")\n\n    withSQLConf(s\"${DeltaConfigs.sqlConfPrefix}dataSkippingNumIndexedCols\" -> \"0\") {\n      checkpointSchemaForTable(df)(\n        checkpointingFns = checkpointFnsWithStructAndJsonStats,\n        expectedCols = expectedColumns,\n        additionalValidationFn = addColumns => {\n          // None of the stats column should exist instead of numRecords\n          checkFields(addColumns, statsAsJsonExists = true, unexpected)\n        }\n      )\n\n      checkpointSchemaForTable(df)(\n        checkpointingFns = checkpointFnsWithStructWithoutJsonStats,\n        expectedCols = expectedColumns,\n        additionalValidationFn = addColumns => {\n          // None of the stats column should exist instead of numRecords\n          checkFields(addColumns, statsAsJsonExists = false, unexpected)\n        }\n      )\n\n      checkpointSchemaForTable(df)(\n        checkpointingFns = checkpointFnsWithoutStructWithJsonStats,\n        expectedCols = Nil,\n        additionalValidationFn = addColumns => {\n          checkFields(\n            addColumns,\n            statsAsJsonExists = true,\n            unexpected :+ \"stats_parsed.numRecords\")\n        }\n      )\n\n      checkpointSchemaForTable(df)(\n        checkpointingFns = Seq(checkpointWithoutStats),\n        expectedCols = Nil,\n        additionalValidationFn = addColumns => {\n          checkFields(\n            addColumns,\n            statsAsJsonExists = false,\n            unexpected :+ \"stats_parsed.numRecords\")\n        }\n      )\n    }\n  }\n\n  test(\"checkpoint read succeeds with column pruning disabled\") {\n    withTempDir { dir =>\n      // Populate the table with three commits, take a checkpoint at v1, and delete v0. Otherwise,\n      // if the bug we test for caused snapshot construction to fail, Delta would silently retry\n      // without the checkpoint, and the test would always appear to succeed.\n      spark.range(1000).write.format(\"delta\").save(dir.getCanonicalPath)\n      spark.range(1000).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      deltaLog.checkpoint()\n      spark.range(1000).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      val firstCommit = deltaLog.store\n        .listFrom(FileNames.listingPrefix(deltaLog.logPath, 0), deltaLog.newDeltaHadoopConf())\n        .find(f => FileNames.isDeltaFile(f.getPath) && FileNames.deltaVersion(f.getPath) == 0)\n      assert(new File(firstCommit.get.getPath.toUri).delete())\n      DeltaLog.clearCache()\n\n      // Trigger both metadata reconstruction and state reconstruction queries with column pruning\n      // disabled. We must also disable reading metadata from .crc file, for the former case.\n      withSQLConf(\n        SQLConf.OPTIMIZER_EXCLUDED_RULES.key ->\n          org.apache.spark.sql.catalyst.optimizer.ColumnPruning.ruleName,\n        DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key -> \"false\") {\n\n        // NOTE: Just creating the snapshot should already trigger metadata reconstruction, but we\n        // still access it directly just to be extra sure.\n        logInfo(\"About to create a new snapshot\")\n        val snapshot = DeltaLog.forTable(spark, dir).snapshot\n        logInfo(\"About to access metadata\")\n        snapshot.metadata\n        logInfo(\"About to access withStats\")\n        snapshot.withStats.count()\n        logInfo(\"About to trigger state reconstrution\")\n        snapshot.stateDF\n      }\n    }\n  }\n\n  Seq(Seq(\"part\"), Nil).foreach { partitionBy =>\n    test(\"do not lose file stats after a checkpoint when writeStatsAsJson=false - isPartitioned: \" +\n        partitionBy.nonEmpty) {\n      withTempDir { dir =>\n        var start = 0\n        def writeNewData(mode: String): Unit = {\n          spark.range(start, start + 10).withColumn(\"part\", 'id % 4)\n            .write\n            .format(\"delta\")\n            .mode(mode)\n            .partitionBy(partitionBy: _*)\n            .save(dir.getCanonicalPath)\n          start += 10\n        }\n        writeNewData(\"append\")\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        checkpointWithProperty(writeStatsAsJson = Some(false))(deltaLog)\n\n        def checkpointAndCheck(): Unit = {\n          deltaLog.checkpoint()\n          val checkpoint = spark.read.parquet(\n            FileNames.checkpointFileSingular(deltaLog.logPath, deltaLog.snapshot.version).toString)\n\n          // use physical name if possible\n          val id = getPhysicalName(\"id\", deltaLog.snapshot.schema)\n          val adds = checkpoint.where(\"add is not null\").selectExpr(\"add.*\")\n          assert(adds.selectExpr(s\"stats_parsed.minValues.`$id`\")\n            .collect().forall(r => !r.isNullAt(0)),\n            \"minValues was null for some values.\\n\" + adds.collect().mkString(\"\\n\"))\n          assert(adds.selectExpr(s\"stats_parsed.maxValues.`$id`\")\n            .collect().forall(r => !r.isNullAt(0)),\n            \"maxValues was null for some values.\\n\" + adds.collect().mkString(\"\\n\"))\n          assert(adds.selectExpr(s\"stats_parsed.nullCount.`$id`\")\n            .collect().forall(r => !r.isNullAt(0)),\n            \"nullCount was null for some values.\\n\" + adds.collect().mkString(\"\\n\"))\n\n          checkAnswer(\n            adds.select(\"path\"),\n            deltaLog.snapshot.allFiles.select(\"path\")\n          )\n        }\n\n        writeNewData(\"append\")\n        checkpointAndCheck()\n        writeNewData(\"overwrite\")\n        checkpointAndCheck()\n      }\n    }\n  }\n\n  test(\"switching between v1 and v2 checkpoints\") {\n    withTempDir { dir =>\n      spark.range(0, 10).withColumn(\"part\", 'id % 4).write.format(\"delta\").partitionBy(\"part\")\n        .save(dir.getCanonicalPath)\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES \" +\n        s\"('delta.checkpoint.writeStatsAsStruct'='true')\")\n      deltaLog.checkpoint()\n      def getLatestCheckpoint: DataFrame = spark.read.parquet(\n        FileNames.checkpointFileSingular(deltaLog.logPath, deltaLog.snapshot.version).toString)\n\n      // statsAsStruct=true, statsAsJson=true\n      val withStructAndJson = getLatestCheckpoint\n\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES \" +\n        s\"('delta.checkpoint.writeStatsAsStruct'='false')\")\n      deltaLog.checkpoint()\n      // statsAsStruct=false, statsAsJson=true\n      val noStructWithJson = getLatestCheckpoint\n      // The columns should be the same, without the stats_parsed column in noStructWithJson\n      checkAnswer(\n        noStructWithJson.select(\"add.*\")\n          .select(\"path\", \"partitionValues\", \"modificationTime\", \"tags\", \"stats\"),\n        withStructAndJson.select(\"add.*\")\n          .select(\"path\", \"partitionValues\", \"modificationTime\", \"tags\", \"stats\")\n      )\n\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES \" +\n        s\"('delta.checkpoint.writeStatsAsStruct'='true',\" +\n        s\"'delta.checkpoint.writeStatsAsJson'='false')\")\n      deltaLog.checkpoint()\n      // statsAsStruct=true, statsAsJson=false\n      val withStructNoJson = getLatestCheckpoint\n      checkAnswer(\n        withStructNoJson.select(\"add.*\"),\n        withStructAndJson.select(\"add.*\").drop(\"stats\")\n      )\n\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES \" +\n        s\"('delta.checkpoint.writeStatsAsStruct'='false')\")\n      deltaLog.checkpoint()\n      // statsAsStruct=false, statsAsJson=false\n      val noStructNoJson = getLatestCheckpoint\n      // should not have the stats column anymore\n      checkAnswer(\n        noStructNoJson.select(\"add.*\"),\n        noStructWithJson.select(\"add.*\").drop(\"stats\"))\n\n      // going to a v2 checkpoint with the json stats\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES \" +\n        s\"('delta.checkpoint.writeStatsAsStruct'='true',\" +\n        s\"'delta.checkpoint.writeStatsAsJson'='true')\")\n      deltaLog.checkpoint()\n      // statsAsStruct=true, statsAsJson=true\n      val lostAllStats = getLatestCheckpoint\n      // should be identical to withStructAndJson\n      checkAnswer(\n        lostAllStats.select(\"add.*\"),\n        withStructAndJson.select(\"add.*\")\n          .withColumn(\"stats\", lit(null))\n          .withColumn(\"stats_parsed\", lit(null))\n      )\n    }\n  }\n}\n\n\nclass DeltaCheckpointWithStructColsNameColumnMappingSuite extends DeltaCheckpointWithStructColsSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  override protected def runOnlyTests = Seq(\n    \"unpartitioned table\",\n    \"partitioned table\"\n  )\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.nio.file.Files\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile, Metadata => MetadataAction, Protocol, SetTransaction}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.parquet.hadoop.ParquetFileReader\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n// scalastyle:on import.ordering.noEmptyLine\n\ntrait DeltaColumnMappingSuiteUtils\n  extends SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest {\n\n\n  protected def supportedModes: Seq[String] = Seq(\"id\", \"name\")\n\n  protected def colName(name: String) = s\"$name with special chars ,;{}()\\n\\t=\"\n\n  protected def partitionStmt(partCols: Seq[String]): String = {\n    if (partCols.nonEmpty) s\"PARTITIONED BY (${partCols.map(name => s\"`$name`\").mkString(\",\")})\"\n    else \"\"\n  }\n\n  protected def propString(props: Map[String, String]) = if (props.isEmpty) \"\"\n    else {\n      props\n        .map { case (key, value) => s\"'$key' = '$value'\" }\n        .mkString(\"TBLPROPERTIES (\", \",\", \")\")\n    }\n\n  protected def alterTableWithProps(\n    tableName: String,\n    props: Map[String, String]): Unit =\n    spark.sql(\n      s\"\"\"\n         | ALTER TABLE $tableName SET ${propString(props)}\n         |\"\"\".stripMargin)\n\n  protected def mode(props: Map[String, String]): String =\n      props.get(DeltaConfigs.COLUMN_MAPPING_MODE.key).getOrElse(\"none\")\n\n  protected def testColumnMapping(\n      testName: String,\n      enableSQLConf: Boolean = false,\n      modes: Option[Seq[String]] = None)(testCode: String => Unit): Unit = {\n    test(testName) {\n      modes.getOrElse(supportedModes).foreach { mode => {\n        withClue(s\"Testing under mode: $mode\") {\n          if (enableSQLConf) {\n            withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> mode) {\n              testCode(mode)\n            }\n          } else {\n            testCode(mode)\n          }\n        }\n      }}\n    }\n  }\n\n\n}\n\nclass DeltaColumnMappingSuite extends QueryTest\n  with GivenWhenThen\n  with DeltaColumnMappingSuiteUtils {\n\n  import testImplicits._\n\n  protected def withId(id: Long): Metadata =\n    new MetadataBuilder()\n      .putLong(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY, id)\n      .build()\n\n  protected def withPhysicalName(pname: String) =\n    new MetadataBuilder()\n      .putString(DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, pname)\n      .build()\n\n  protected def withIdAndPhysicalName(id: Long, pname: String): Metadata =\n    new MetadataBuilder()\n      .putLong(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY, id)\n      .putString(DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, pname)\n      .build()\n\n  protected def assertEqual(\n      actual: StructType,\n      expected: StructType,\n      ignorePhysicalName: Boolean = true): Unit = {\n\n    var actualSchema = actual\n    var expectedSchema = expected\n\n    val fieldsToRemove = mutable.Set[String]()\n    if (ignorePhysicalName) {\n      fieldsToRemove.add(DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY)\n    }\n\n    def removeFields(metadata: Metadata): Metadata = {\n      val metadataBuilder = new MetadataBuilder().withMetadata(metadata)\n      fieldsToRemove.foreach { field => {\n          if (metadata.contains(field)) {\n            metadataBuilder.remove(field)\n          }\n        }\n      }\n      metadataBuilder.build()\n    }\n\n    // drop fields if needed\n    actualSchema = SchemaMergingUtils.transformColumns(actual) { (_, field, _) =>\n      field.copy(metadata = removeFields(field.metadata))\n    }\n    expectedSchema = SchemaMergingUtils.transformColumns(expected) { (_, field, _) =>\n      field.copy(metadata = removeFields(field.metadata))\n    }\n\n    assert(expectedSchema === actualSchema,\n      s\"\"\"\n         |Schema mismatch:\n         |\n         |expected:\n         |${expectedSchema.prettyJson}\n         |\n         |actual:\n         |${actualSchema.prettyJson}\n         |\"\"\".stripMargin)\n\n  }\n\n  protected def checkSchema(\n      tableName: String,\n      expectedSchema: StructType,\n      ignorePhysicalName: Boolean = true): Unit = {\n\n    // snapshot schema should have all the expected metadata\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n    assertEqual(deltaLog.update().schema, expectedSchema, ignorePhysicalName)\n\n    // table schema should not have any metadata\n    assert(spark.table(tableName).schema ===\n      DeltaColumnMapping.dropColumnMappingMetadata(expectedSchema))\n  }\n\n  // NOTE:\n  // All attached metadata to the following sample inputs, if used in source dataframe,\n  // will be CLEARED out after metadata is imported into the target table\n  // See ImplicitMetadataOperation.updateMetadata() for how the old metadata is cleared\n  protected val schema = new StructType()\n    .add(\"a\", StringType, true)\n    .add(\"b\", IntegerType, true)\n\n  protected val schemaNested = new StructType()\n    .add(\"a\", StringType, true)\n    .add(\"b\",\n      new StructType()\n        .add(\"c\", StringType, true)\n        .add(\"d\", IntegerType, true),\n      true\n    )\n\n  protected val schemaWithId = new StructType()\n    .add(\"a\", StringType, true, withId(1))\n    .add(\"b\", IntegerType, true, withId(2))\n\n  protected val schemaWithIdRandom = new StructType()\n    .add(\"a\", StringType, true, withId(111))\n    .add(\"b\", IntegerType, true, withId(222))\n\n  protected val schemaWithIdAndPhysicalNameRandom = new StructType()\n    .add(\"a\", StringType, true, withIdAndPhysicalName(111, \"asjdklsajdkl\"))\n    .add(\"b\", IntegerType, true, withIdAndPhysicalName(222, \"iotiyoiopio\"))\n\n  protected val schemaWithDuplicatingIds = new StructType()\n    .add(\"a\", StringType, true, withId(1))\n    .add(\"b\", IntegerType, true, withId(2))\n    .add(\"c\", IntegerType, true, withId(2))\n\n  protected val schemaWithIdAndDuplicatingPhysicalNames = new StructType()\n    .add(\"a\", StringType, true, withIdAndPhysicalName(1, \"aaa\"))\n    .add(\"b\", IntegerType, true, withIdAndPhysicalName(2, \"bbb\"))\n    .add(\"c\", IntegerType, true, withIdAndPhysicalName(3, \"bbb\"))\n\n  protected val schemaWithDuplicatingPhysicalNames = new StructType()\n    .add(\"a\", StringType, true, withPhysicalName(\"aaa\"))\n    .add(\"b\", IntegerType, true, withPhysicalName(\"bbb\"))\n    .add(\"c\", IntegerType, true, withPhysicalName(\"bbb\"))\n\n  protected val schemaWithDuplicatingPhysicalNamesNested = new StructType()\n    .add(\"b\",\n      new StructType()\n        .add(\"c\", StringType, true, withPhysicalName(\"dupName\"))\n        .add(\"d\", IntegerType, true, withPhysicalName(\"dupName\")),\n      true,\n      withPhysicalName(\"b\")\n    )\n\n  protected val schemaWithIdNested = new StructType()\n    .add(\"a\", StringType, true, withId(1))\n    .add(\"b\",\n      new StructType()\n        .add(\"c\", StringType, true, withId(3))\n        .add(\"d\", IntegerType, true, withId(4)),\n      true,\n      withId(2)\n    )\n\n  protected val schemaWithPhysicalNamesNested = new StructType()\n    .add(\"a\", StringType, true, withIdAndPhysicalName(1, \"aaa\"))\n    .add(\"b\",\n      // let's call this nested struct 'X'.\n      new StructType()\n        .add(\"c\", StringType, true, withIdAndPhysicalName(2, \"ccc\"))\n        .add(\"d\", IntegerType, true, withIdAndPhysicalName(3, \"ddd\"))\n        .add(\"foo.bar\",\n          new StructType().add(\"f\", LongType, true, withIdAndPhysicalName(4, \"fff\")),\n          true,\n          withIdAndPhysicalName(5, \"foo.foo.foo.bar.bar.bar\")),\n      true,\n      withIdAndPhysicalName(6, \"bbb\")\n    )\n    .add(\"g\",\n      // nested struct 'X' (see above) is repeated here.\n      new StructType()\n        .add(\"c\", StringType, true, withIdAndPhysicalName(7, \"ccc\"))\n        .add(\"d\", IntegerType, true, withIdAndPhysicalName(8, \"ddd\"))\n        .add(\"foo.bar\",\n          new StructType().add(\"f\", LongType, true, withIdAndPhysicalName(9, \"fff\")),\n          true,\n          withIdAndPhysicalName(10, \"foo.foo.foo.bar.bar.bar\")),\n      true,\n      withIdAndPhysicalName(11, \"ggg\")\n    )\n    .add(\"h\", IntegerType, true, withIdAndPhysicalName(12, \"hhh\"))\n\n  protected val schemaWithIdNestedRandom = new StructType()\n    .add(\"a\", StringType, true, withId(111))\n    .add(\"b\",\n      new StructType()\n        .add(\"c\", StringType, true, withId(333))\n        .add(\"d\", IntegerType, true, withId(444)),\n      true,\n      withId(222)\n    )\n\n  // This schema has both a.b and a . b as physical path for its columns, we would like to make sure\n  // it shouldn't trigger the duplicated physical name check\n  protected val schemaWithDottedColumnNames = new StructType()\n    .add(\"a.b\", StringType, true, withIdAndPhysicalName(1, \"a.b\"))\n    .add(\"a\", new StructType()\n      .add(\"b\", StringType, true, withIdAndPhysicalName(3, \"b\")),\n      true, withIdAndPhysicalName(2, \"a\"))\n\n  protected def dfWithoutIds(spark: SparkSession) =\n    spark.createDataFrame(Seq(Row(\"str1\", 1), Row(\"str2\", 2)).asJava, schema)\n\n  protected def dfWithoutIdsNested(spark: SparkSession) =\n    spark.createDataFrame(\n      Seq(Row(\"str1\", Row(\"str1.1\", 1)), Row(\"str2\", Row(\"str1.2\", 2))).asJava, schemaNested)\n\n  protected def dfWithIds(spark: SparkSession, randomIds: Boolean = false) =\n    spark.createDataFrame(Seq(Row(\"str1\", 1), Row(\"str2\", 2)).asJava,\n      if (randomIds) schemaWithIdRandom else schemaWithId)\n\n  protected def dfWithIdsNested(spark: SparkSession, randomIds: Boolean = false) =\n    spark.createDataFrame(\n      Seq(Row(\"str1\", Row(\"str1.1\", 1)), Row(\"str2\", Row(\"str1.2\", 2))).asJava,\n      if (randomIds) schemaWithIdNestedRandom else schemaWithIdNested)\n\n  protected def checkProperties(\n      tableName: String,\n      mode: Option[String] = None,\n      readerVersion: Int = 1,\n      writerVersion: Int = 2,\n      curMaxId: Long = 0): Unit = {\n    val props =\n      spark.sql(s\"SHOW TBLPROPERTIES $tableName\").as[(String, String)].collect().toMap\n    assert(props.get(\"delta.minReaderVersion\").map(_.toInt) == Some(readerVersion))\n    assert(props.get(\"delta.minWriterVersion\").map(_.toInt) == Some(writerVersion))\n\n    assert(props.get(DeltaConfigs.COLUMN_MAPPING_MODE.key) == mode)\n    assert(props.get(DeltaConfigs.COLUMN_MAPPING_MAX_ID.key).map(_.toLong).getOrElse(0) == curMaxId)\n  }\n\n  protected def createTableWithDeltaTableAPI(\n      tableName: String,\n      props: Map[String, String] = Map.empty,\n      withColumnIds: Boolean = false,\n      isPartitioned: Boolean = false): Unit = {\n    val schemaToUse = if (withColumnIds) schemaWithId else schema\n    val builder = io.delta.tables.DeltaTable.createOrReplace(spark)\n      .tableName(tableName)\n      .addColumn(schemaToUse.fields(0))\n      .addColumn(schemaToUse.fields(1))\n    props.foreach { case (key, value) =>\n      builder.property(key, value)\n    }\n    if (isPartitioned) {\n      builder.partitionedBy(\"a\")\n    }\n    builder.execute()\n  }\n\n  protected def createTableWithSQLCreateOrReplaceAPI(\n      tableName: String,\n      props: Map[String, String] = Map.empty,\n      withColumnIds: Boolean = false,\n      isPartitioned: Boolean = false,\n      nested: Boolean = false,\n      randomIds: Boolean = false): Unit = {\n    withTable(\"source\") {\n      val dfToWrite = if (withColumnIds) {\n        if (nested) {\n          dfWithIdsNested(spark, randomIds)\n        } else {\n          dfWithIds(spark, randomIds)\n        }\n      } else {\n        if (nested) {\n          dfWithoutIdsNested(spark)\n        } else {\n          dfWithoutIds(spark)\n        }\n      }\n      dfToWrite.write.saveAsTable(\"source\")\n      val partitionStmt = if (isPartitioned) \"PARTITIONED BY (a)\" else \"\"\n      spark.sql(\n        s\"\"\"\n           |CREATE OR REPLACE TABLE $tableName\n           |USING DELTA\n           |$partitionStmt\n           |${propString(props)}\n           |AS SELECT * FROM source\n           |\"\"\".stripMargin)\n    }\n  }\n\n  protected def createTableWithSQLAPI(\n      tableName: String,\n      props: Map[String, String] = Map.empty,\n      withColumnIds: Boolean = false,\n      isPartitioned: Boolean = false,\n      nested: Boolean = false,\n      randomIds: Boolean = false): Unit = {\n    withTable(\"source\") {\n      val dfToWrite = if (withColumnIds) {\n        if (nested) {\n          dfWithIdsNested(spark, randomIds)\n        } else {\n          dfWithIds(spark, randomIds)\n        }\n      } else {\n        if (nested) {\n          dfWithoutIdsNested(spark)\n        } else {\n          dfWithoutIds(spark)\n        }\n      }\n      dfToWrite.write.saveAsTable(\"source\")\n      val partitionStmt = if (isPartitioned) \"PARTITIONED BY (a)\" else \"\"\n      spark.sql(\n        s\"\"\"\n           |CREATE TABLE $tableName\n           |USING DELTA\n           |$partitionStmt\n           |${propString(props)}\n           |AS SELECT * FROM source\n           |\"\"\".stripMargin)\n    }\n  }\n\n  protected def createTableWithDataFrameAPI(\n      tableName: String,\n      props: Map[String, String] = Map.empty,\n      withColumnIds: Boolean = false,\n      isPartitioned: Boolean = false,\n      nested: Boolean = false,\n      randomIds: Boolean = false): Unit = {\n    val sqlConfs = props.map { case (key, value) =>\n      \"spark.databricks.delta.properties.defaults.\" + key.stripPrefix(\"delta.\") -> value\n    }\n    withSQLConf(sqlConfs.toList: _*) {\n      val dfToWrite = if (withColumnIds) {\n        if (nested) {\n          dfWithIdsNested(spark, randomIds)\n        } else {\n          dfWithIds(spark, randomIds)\n        }\n      } else {\n        if (nested) {\n          dfWithoutIdsNested(spark)\n        } else {\n          dfWithoutIds(spark)\n        }\n      }\n      if (isPartitioned) {\n        dfToWrite.write.format(\"delta\").partitionBy(\"a\").saveAsTable(tableName)\n      } else {\n        dfToWrite.write.format(\"delta\").saveAsTable(tableName)\n      }\n    }\n  }\n\n  protected def createTableWithDataFrameWriterV2API(\n      tableName: String,\n      props: Map[String, String] = Map.empty,\n      withColumnIds: Boolean = false,\n      isPartitioned: Boolean = false,\n      nested: Boolean = false,\n      randomIds: Boolean = false): Unit = {\n    val dfToWrite = if (withColumnIds) {\n      if (nested) {\n        dfWithIdsNested(spark, randomIds)\n      } else {\n        dfWithIds(spark, randomIds)\n      }\n    } else {\n      if (nested) {\n        dfWithoutIdsNested(spark)\n      } else {\n        dfWithoutIds(spark)\n      }\n    }\n    val writer = dfToWrite.writeTo(tableName).using(\"delta\")\n    props.foreach(prop => writer.tableProperty(prop._1, prop._2))\n    if (isPartitioned) writer.partitionedBy('a)\n    writer.create()\n  }\n\n  protected def createStrictSchemaTableWithDeltaTableApi(\n      tableName: String,\n      schema: StructType,\n      props: Map[String, String] = Map.empty,\n      isPartitioned: Boolean = false): Unit = {\n    val builder = io.delta.tables.DeltaTable.createOrReplace(spark)\n      .tableName(tableName)\n    builder.addColumns(schema)\n    props.foreach(prop => builder.property(prop._1, prop._2))\n    if (isPartitioned) builder.partitionedBy(\"a\")\n    builder.execute()\n  }\n\n  protected def testCreateTableColumnMappingMode(\n      tableName: String,\n      expectedSchema: StructType,\n      ignorePhysicalName: Boolean,\n      mode: String,\n      createNewTable: Boolean = true,\n      tableFeaturesProtocolExpected: Boolean = true)(fn: => Unit): Unit = {\n    withTable(tableName) {\n        fn\n      checkProperties(tableName,\n        readerVersion = 2,\n        writerVersion = if (tableFeaturesProtocolExpected) 7 else 5,\n        mode = Some(mode),\n        curMaxId = DeltaColumnMapping.findMaxColumnId(expectedSchema)\n      )\n      checkSchema(tableName, expectedSchema, ignorePhysicalName)\n    }\n  }\n\n  test(\"find max column id in existing columns\") {\n    assert(DeltaColumnMapping.findMaxColumnId(schemaWithId) == 2)\n    assert(DeltaColumnMapping.findMaxColumnId(schemaWithIdNested) == 4)\n    assert(DeltaColumnMapping.findMaxColumnId(schemaWithIdRandom) == 222)\n    assert(DeltaColumnMapping.findMaxColumnId(schemaWithIdNestedRandom) == 444)\n    assert(DeltaColumnMapping.findMaxColumnId(schema) == 0)\n    assert(DeltaColumnMapping.findMaxColumnId(new StructType()) == 0)\n  }\n\n  test(\"Enable column mapping with schema change on table with no schema\") {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n      Seq((1, \"a\"), (2, \"b\")).toDF(\"id\", \"name\")\n        .write.mode(\"append\").format(\"delta\").save(tablePath)\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      val txn = deltaLog.startTransaction()\n      txn.commitManually(actions.Metadata()) // Whip the schema out\n      val txn2 = deltaLog.startTransaction()\n      txn2.commitManually(Protocol(2, 5))\n      txn2.updateMetadata(actions.Metadata(\n        configuration = Map(\"delta.columnMapping.mode\" -> \"name\"),\n        schemaString = new StructType().add(\"a\", StringType).json))\n\n      // Now ensure that it is not allowed to enable column mapping with schema change\n      // on a table with a schema\n      Seq((1, \"a\"), (2, \"b\")).toDF(\"id\", \"name\")\n        .write.mode(\"overwrite\").format(\"delta\")\n        .option(\"overwriteSchema\", \"true\")\n        .save(tablePath)\n      val txn3 = deltaLog.startTransaction()\n      txn3.commitManually(Protocol(2, 5))\n      val e = intercept[DeltaColumnMappingUnsupportedException] {\n        txn3.updateMetadata(\n          actions.Metadata(\n          configuration = Map(\"delta.columnMapping.mode\" -> \"name\"),\n          schemaString = new StructType().add(\"a\", StringType).json))\n      }\n      val msg = \"Schema changes are not allowed during the change of column mapping mode.\"\n      assert(e.getMessage.contains(msg))\n    }\n  }\n\n  // TODO: repurpose this once we roll out the proper semantics for CM + streaming\n  testColumnMapping(\"isColumnMappingReadCompatible\") { mode =>\n    // Set up table based on mode and return the initial metadata actions for comparison\n    def setupInitialTable(deltaLog: DeltaLog): (MetadataAction, MetadataAction) = {\n      val tablePath = deltaLog.dataPath.toString\n      if (mode == NameMapping.name) {\n        Seq((1, \"a\"), (2, \"b\")).toDF(\"id\", \"name\")\n          .write.mode(\"append\").format(\"delta\").save(tablePath)\n        // schema: <id, name>\n        val m0 = deltaLog.update().metadata\n\n        // add a column\n        sql(s\"ALTER TABLE delta.`$tablePath` ADD COLUMN (score long)\")\n        // schema: <id, name, score>\n        val m1 = deltaLog.update().metadata\n\n        // column mapping not enabled -> not blocked at all\n        assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m1, m0))\n\n        // upgrade to name mode\n        alterTableWithProps(s\"delta.`$tablePath`\", Map(\n          DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\",\n          DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n          DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n\n        (m0, m1)\n      } else {\n        // for id mode, just create the table\n        withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"id\") {\n          Seq((1, \"a\"), (2, \"b\")).toDF(\"id\", \"name\")\n            .write.mode(\"append\").format(\"delta\").save(tablePath)\n        }\n        // schema: <id, name>\n        val m0 = deltaLog.update().metadata\n\n        // add a column\n        sql(s\"ALTER TABLE delta.`$tablePath` ADD COLUMN (score long)\")\n        // schema: <id, name, score>\n        val m1 = deltaLog.update().metadata\n\n        // add column shouldn't block\n        assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m1, m0))\n\n        (m0, m1)\n      }\n    }\n\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n\n      val (m0, m1) = setupInitialTable(deltaLog)\n\n      // schema: <id, name, score>\n      val m2 = deltaLog.update().metadata\n\n      assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m2, m1))\n      assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m2, m0))\n\n      // rename column\n      sql(s\"ALTER TABLE delta.`$tablePath` RENAME COLUMN score TO age\")\n      // schema: <id, name, age>\n      val m3 = deltaLog.update().metadata\n\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m3, m2))\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m3, m1))\n      // But IS read compatible with the initial schema, because the added column should not\n      // be blocked by this column mapping check.\n      assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m3, m0))\n\n      // drop a column\n      sql(s\"ALTER TABLE delta.`$tablePath` DROP COLUMN age\")\n      // schema: <id, name>\n      val m4 = deltaLog.update().metadata\n\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m4, m3))\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m4, m2))\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m4, m1))\n      // but IS read compatible with the initial schema, because the added column is dropped\n      assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m4, m0))\n\n      // add back the same column\n      sql(s\"ALTER TABLE delta.`$tablePath` ADD COLUMN (score long)\")\n      // schema: <id, name, score>\n      val m5 = deltaLog.update().metadata\n\n      // It IS read compatible with the previous schema, because the added column should not\n      // blocked by this column mapping check.\n      assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m5, m4))\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m5, m3))\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m5, m2))\n      // But Since the new added column has a different physical name as all previous columns,\n      // even it has the same logical name as say, m1.schema, we will still block\n      assert(!DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m5, m1))\n      // But it IS read compatible with the initial schema, because the added column should not\n      // be blocked by this column mapping check.\n      assert(DeltaColumnMapping.hasNoColumnMappingSchemaChanges(m5, m0))\n    }\n  }\n\n  testColumnMapping(\"create table through raw schema API should \" +\n    \"auto bump the version and retain input metadata\") { mode =>\n\n    // provides id only (let Delta generate physical name for me)\n    testCreateTableColumnMappingMode(\n      \"t1\", schemaWithIdRandom, ignorePhysicalName = true, mode = mode) {\n      createStrictSchemaTableWithDeltaTableApi(\n        \"t1\",\n        schemaWithIdRandom,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n    }\n\n    // provides id and physical name (Delta shouldn't rebuild/override)\n    // we use random ids as input, which shouldn't be changed too\n    testCreateTableColumnMappingMode(\n      \"t1\", schemaWithIdAndPhysicalNameRandom, ignorePhysicalName = false, mode = mode) {\n      createStrictSchemaTableWithDeltaTableApi(\n        \"t1\",\n        schemaWithIdAndPhysicalNameRandom,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n    }\n\n  }\n\n  testColumnMapping(\"create table through dataframe should \" +\n    \"auto bumps the version and rebuild schema metadata/drop dataframe metadata\") { mode =>\n    // existing ids should be dropped/ignored and ids should be regenerated\n    // so for tests below even if we are ingesting dfs with random ids\n    // we should still expect schema with normal sequential ids\n    val expectedSchema = schemaWithId\n\n    testCreateTableColumnMappingMode(\n      \"t1\", expectedSchema, ignorePhysicalName = true, mode = mode) {\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        randomIds = true)\n    }\n\n    testCreateTableColumnMappingMode(\n      \"t1\", expectedSchema, ignorePhysicalName = true, mode = mode) {\n      createTableWithDataFrameAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        randomIds = true)\n    }\n\n    testCreateTableColumnMappingMode(\n      \"t1\", expectedSchema, ignorePhysicalName = true, mode = mode) {\n      createTableWithSQLCreateOrReplaceAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        randomIds = true)\n    }\n\n    testCreateTableColumnMappingMode(\n      \"t1\", expectedSchema, ignorePhysicalName = true, mode = mode) {\n      createTableWithDataFrameWriterV2API(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        randomIds = true)\n    }\n  }\n\n  test(\"create table with none mode\") {\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"none\"),\n        withColumnIds = true,\n        randomIds = true)\n\n      // Should be still on old protocol, the schema shouldn't have any metadata\n      checkProperties(\n        \"t1\",\n        mode = Some(\"none\"))\n\n      checkSchema(\"t1\", schema, ignorePhysicalName = false)\n    }\n  }\n\n  testColumnMapping(\"update column mapped table invalid max id property is blocked\") { mode =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true\n      )\n\n      val log = DeltaLog.forTable(spark, TableIdentifier(\"t1\"))\n      // Get rid of max column id prop\n      assert {\n        intercept[DeltaAnalysisException] {\n          log.withNewTransaction { txn =>\n            val existingMetadata = log.update().metadata\n            txn.commit(existingMetadata.copy(configuration =\n              existingMetadata.configuration - DeltaConfigs.COLUMN_MAPPING_MAX_ID.key) :: Nil,\n              DeltaOperations.ManualUpdate)\n          }\n        }.getErrorClass == \"DELTA_COLUMN_MAPPING_MAX_COLUMN_ID_NOT_SET\"\n      }\n      // Use an invalid max column id prop\n      assert {\n        intercept[DeltaAnalysisException] {\n          log.withNewTransaction { txn =>\n            val existingMetadata = log.update().metadata\n            txn.commit(existingMetadata.copy(configuration =\n              existingMetadata.configuration ++ Map(\n                // '1' is less than the current max\n                DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> \"1\"\n              )) :: Nil,\n              DeltaOperations.ManualUpdate)\n          }\n        }.getErrorClass == \"DELTA_COLUMN_MAPPING_MAX_COLUMN_ID_NOT_SET_CORRECTLY\"\n      }\n    }\n  }\n\n  testColumnMapping(\n    \"create column mapped table with duplicated id/physical name should error\"\n  ) { mode =>\n    withTable(\"t1\") {\n      val e = intercept[ColumnMappingException] {\n        createStrictSchemaTableWithDeltaTableApi(\n          \"t1\",\n          schemaWithDuplicatingIds,\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n      }\n      assert(\n        e.getMessage.contains(\n          s\"Found duplicated column id `2` in column mapping mode `$mode`\"))\n      assert(e.getMessage.contains(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY))\n\n      val e2 = intercept[ColumnMappingException] {\n        createStrictSchemaTableWithDeltaTableApi(\n          \"t1\",\n          schemaWithIdAndDuplicatingPhysicalNames,\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n      }\n      assert(\n        e2.getMessage.contains(\n          s\"Found duplicated physical name `bbb` in column mapping mode `$mode`\"))\n      assert(e2.getMessage.contains(DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY))\n    }\n\n    // for name mode specific, we would also like to check for name duplication\n    if (mode == \"name\") {\n      val e = intercept[ColumnMappingException] {\n        createStrictSchemaTableWithDeltaTableApi(\n          \"t1\",\n          schemaWithDuplicatingPhysicalNames,\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n      }\n      assert(\n        e.getMessage.contains(\n          s\"Found duplicated physical name `bbb` in column mapping mode `$mode`\")\n      )\n\n      val e2 = intercept[ColumnMappingException] {\n        createStrictSchemaTableWithDeltaTableApi(\n          \"t1\",\n          schemaWithDuplicatingPhysicalNamesNested,\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n      }\n      assert(\n        e2.getMessage.contains(\n          s\"Found duplicated physical name `b.dupName` in column mapping mode `$mode`\")\n      )\n    }\n  }\n\n  testColumnMapping(\n    \"create table in column mapping mode without defining ids explicitly\"\n  ) { mode =>\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        randomIds = true)\n      checkSchema(\"t1\", schemaWithId)\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = DeltaColumnMapping.findMaxColumnId(schemaWithId)\n      )\n    }\n  }\n\n  testColumnMapping(\"alter column order in schema on new protocol\") { mode =>\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithSQLAPI(\"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        nested = true,\n        randomIds = true)\n      spark.sql(\n        \"\"\"\n          |ALTER TABLE t1 ALTER COLUMN a AFTER b\n          |\"\"\".stripMargin\n      )\n\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = DeltaColumnMapping.findMaxColumnId(schemaWithIdNested))\n      checkSchema(\n        \"t1\",\n        schemaWithIdNested.copy(fields = schemaWithIdNested.fields.reverse))\n    }\n  }\n\n  testColumnMapping(\"add column in schema on new protocol\") { mode =>\n\n    def check(expectedSchema: StructType): Unit = {\n      val curMaxId = DeltaColumnMapping.findMaxColumnId(expectedSchema) + 1\n      checkSchema(\"t1\", expectedSchema)\n      spark.sql(\n        \"\"\"\n          |ALTER TABLE t1 ADD COLUMNS (c STRING AFTER b)\n          |\"\"\".stripMargin\n      )\n\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId)\n\n      checkSchema(\"t1\", expectedSchema.add(\"c\", StringType, true, withId(curMaxId)))\n\n      val curMaxId2 = DeltaColumnMapping.findMaxColumnId(expectedSchema) + 2\n\n      spark.sql(\n        \"\"\"\n          |ALTER TABLE t1 ADD COLUMNS (d STRING AFTER c)\n          |\"\"\".stripMargin\n      )\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId2)\n      checkSchema(\"t1\",\n        expectedSchema\n          .add(\"c\", StringType, true, withId(curMaxId))\n          .add(\"d\", StringType, true, withId(curMaxId2)))\n    }\n\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode), withColumnIds = true, randomIds = true)\n\n      check(schemaWithId)\n    }\n\n    withTable(\"t1\") {\n      // column ids will NOT be dropped, so future ids should update based on the current max\n      createStrictSchemaTableWithDeltaTableApi(\n        \"t1\",\n        schemaWithIdRandom,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode)\n      )\n\n      check(schemaWithIdRandom)\n    }\n  }\n\n  testColumnMapping(\"add nested column in schema on new protocol\") { mode =>\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        nested = true,\n        randomIds = true)\n\n      checkSchema(\"t1\", schemaWithIdNested)\n\n      val curMaxId = DeltaColumnMapping.findMaxColumnId(schemaWithIdNested) + 1\n\n      spark.sql(\n        \"\"\"\n          |ALTER TABLE t1 ADD COLUMNS (b.e STRING AFTER d)\n          |\"\"\".stripMargin\n      )\n\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId)\n      checkSchema(\"t1\",\n          schemaWithIdNested.merge(\n            new StructType().add(\n              \"b\",\n              new StructType().add(\n                \"e\", StringType, true, withId(5)),\n              true,\n              withId(2)\n            ))\n      )\n\n      val curMaxId2 = DeltaColumnMapping.findMaxColumnId(schemaWithIdNested) + 2\n      spark.sql(\n        \"\"\"\n          |ALTER TABLE t1 ADD COLUMNS (b.f STRING AFTER e)\n          |\"\"\".stripMargin\n      )\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId2)\n      checkSchema(\"t1\",\n          schemaWithIdNested.merge(\n            new StructType().add(\n              \"b\",\n              new StructType().add(\n                \"e\", StringType, true, withId(5)),\n              true,\n              withId(2)\n            )).merge(\n          new StructType().add(\n              \"b\",\n              new StructType()\n                .add(\"f\", StringType, true, withId(6)),\n              true,\n              withId(2))\n        ))\n\n    }\n  }\n\n  testColumnMapping(\"write/merge df to table\") { mode =>\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithDataFrameAPI(\"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode), withColumnIds = true, randomIds = true)\n      val curMaxId = DeltaColumnMapping.findMaxColumnId(schemaWithId)\n\n      val df1 = dfWithIds(spark)\n      df1.write\n         .format(\"delta\")\n         .mode(\"append\")\n         .saveAsTable(\"t1\")\n\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId)\n      checkSchema(\"t1\", schemaWithId)\n\n      val previousSchema = spark.table(\"t1\").schema\n      // ingest df with random id should not cause existing schema col id to change\n      val df2 = dfWithIds(spark, randomIds = true)\n      df2.write\n         .format(\"delta\")\n         .mode(\"append\")\n         .saveAsTable(\"t1\")\n\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId)\n\n      // with checkPhysicalSchema check\n      checkSchema(\"t1\", schemaWithId)\n\n      // compare with before\n      assertEqual(spark.table(\"t1\").schema,\n        previousSchema, ignorePhysicalName = false)\n\n      val df3 = spark.createDataFrame(\n        Seq(Row(\"str3\", 3, \"str3.1\"), Row(\"str4\", 4, \"str4.1\")).asJava,\n        schemaWithId.add(\"c\", StringType, true, withId(3))\n      )\n      df3.write\n         .option(\"mergeSchema\", \"true\")\n         .format(\"delta\")\n         .mode(\"append\")\n         .saveAsTable(\"t1\")\n\n      val curMaxId2 = DeltaColumnMapping.findMaxColumnId(schemaWithId) + 1\n      checkProperties(\"t1\",\n        readerVersion = 2,\n        writerVersion = 7,\n        mode = Some(mode),\n        curMaxId = curMaxId2)\n      checkSchema(\"t1\", schemaWithId.add(\"c\", StringType, true, withId(3)))\n    }\n  }\n\n  testColumnMapping(s\"try modifying restricted max id property should fail\") { mode =>\n    withTable(\"t1\") {\n      val e = intercept[UnsupportedOperationException] {\n        createTableWithSQLAPI(\n          \"t1\",\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode,\n              DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> \"100\"),\n          withColumnIds = true,\n          nested = true)\n      }\n      assert(e.getMessage.contains(s\"The Delta table configuration \" +\n        s\"${DeltaConfigs.COLUMN_MAPPING_MAX_ID.key} cannot be specified by the user\"))\n    }\n\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\n          \"t1\",\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n          withColumnIds = true,\n          nested = true)\n\n      val e2 = intercept[UnsupportedOperationException] {\n        alterTableWithProps(\"t1\", Map(DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> \"100\"))\n      }\n\n      assert(e2.getMessage.contains(s\"The Delta table configuration \" +\n        s\"${DeltaConfigs.COLUMN_MAPPING_MAX_ID.key} cannot be specified by the user\"))\n    }\n\n    withTable(\"t1\") {\n      val e = intercept[UnsupportedOperationException] {\n        createTableWithDataFrameAPI(\n          \"t1\",\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode,\n              DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> \"100\"),\n          withColumnIds = true,\n          nested = true)\n      }\n      assert(e.getMessage.contains(s\"The Delta table configuration \" +\n        s\"${DeltaConfigs.COLUMN_MAPPING_MAX_ID.key} cannot be specified by the user\"))\n    }\n  }\n\n  testColumnMapping(\"physical data and partition schema\") { mode =>\n    withTable(\"t1\") {\n      // column ids will be dropped, having the options here to make sure such happens\n      createTableWithSQLAPI(\"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        withColumnIds = true,\n        randomIds = true)\n\n      val metadata = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"t1\"))._2.metadata\n\n      assertEqual(metadata.schema, schemaWithId)\n      assertEqual(metadata.schema, StructType(metadata.partitionSchema ++ metadata.dataSchema))\n    }\n  }\n\n  testColumnMapping(\"block CONVERT TO DELTA\") { mode =>\n    withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> mode) {\n      withTempDir { tablePath =>\n        val tempDir = tablePath.getCanonicalPath\n        val df1 = Seq(0).toDF(\"id\")\n          .withColumn(\"key1\", lit(\"A1\"))\n          .withColumn(\"key2\", lit(\"A2\"))\n\n        df1.write\n          .partitionBy(Seq(\"key1\"): _*)\n          .format(\"parquet\")\n          .mode(\"overwrite\")\n          .save(tempDir)\n\n        val e = intercept[UnsupportedOperationException] {\n          sql(s\"convert to delta parquet.`$tempDir` partitioned by (key1 String)\")\n        }\n        assert(e.getMessage.contains(s\"cannot be set to `$mode` when using CONVERT TO DELTA\"))\n      }\n    }\n  }\n\n  testColumnMapping(\n    \"column mapping batch scan should detect physical name changes\",\n    enableSQLConf = true\n  ) { _ =>\n    withTempDir { dir =>\n      spark.range(10).toDF(\"id\")\n        .write.format(\"delta\").save(dir.getCanonicalPath)\n      // Analysis phase\n      val df1 = spark.read.format(\"delta\").load(dir.getCanonicalPath)\n      val df2 = spark.read.format(\"delta\").load(dir.getCanonicalPath)\n      // Overwrite schema but with same logical schema\n      withSQLConf(DeltaSQLConf.REUSE_COLUMN_MAPPING_METADATA_DURING_OVERWRITE.key -> \"false\") {\n        spark.range(10).toDF(\"id\")\n          .write.format(\"delta\").option(\"overwriteSchema\", \"true\").mode(\"overwrite\")\n          .save(dir.getCanonicalPath)\n      }\n      // The previous analyzed DF no longer is able to read the data any more because it generates\n      // new physical name for the underlying columns, so we should fail.\n      assert {\n        intercept[DeltaAnalysisException] {\n          df1.collect()\n        }.getErrorClass == \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\"\n      }\n      // See we can't read back the same data any more\n      // Note: We need to use separate dataframe, because the error in df1 will be cached.\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_ON_READ_CHECK_ENABLED.key -> \"false\") {\n        checkAnswer(\n          df2,\n          (0 until 10).map(_ => Row(null))\n        )\n      }\n    }\n  }\n\n  protected def testPartitionPath(tableName: String)(createFunc: Boolean => Unit): Unit = {\n    withTable(tableName) {\n      Seq(true, false).foreach { isPartitioned =>\n        spark.sql(s\"drop table if exists $tableName\")\n        createFunc(isPartitioned)\n        val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n        val prefixLen = DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot.metadata)\n        Seq((\"str3\", 3), (\"str4\", 4)).toDF(schema.fieldNames: _*)\n          .write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        checkAnswer(spark.table(tableName),\n          Row(\"str1\", 1) :: Row(\"str2\", 2) :: Row(\"str3\", 3) :: Row(\"str4\", 4) :: Nil)\n        // both new table writes and appends should use prefix\n        val pattern = s\"[A-Za-z0-9]{$prefixLen}/.*part-.*parquet\"\n        for (file <- snapshot.allFiles.collect()) {\n          assert(file.path.matches(pattern))\n        }\n      }\n    }\n  }\n\n  // Copied verbatim from the \"valid replaceWhere\" test in DeltaSuite\n  protected def testReplaceWhere(): Unit =\n    Seq(true, false).foreach { enabled =>\n      withSQLConf(DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED.key -> enabled.toString) {\n        Seq(true, false).foreach { partitioned =>\n          // Skip when it's not enabled and not partitioned.\n          if (enabled || partitioned) {\n            withTempDir { dir =>\n              val writer = Seq(1, 2, 3, 4).toDF()\n                .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                .write\n                .format(\"delta\")\n\n              if (partitioned) {\n                writer.partitionBy(\"is_odd\").save(dir.toString)\n              } else {\n                writer.save(dir.toString)\n              }\n\n              def data: DataFrame = spark.read.format(\"delta\").load(dir.toString)\n\n              Seq(5, 7).toDF()\n                .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                .write\n                .format(\"delta\")\n                .mode(\"overwrite\")\n                .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n                .save(dir.toString)\n              checkAnswer(\n                data,\n                Seq(2, 4, 5, 7).toDF()\n                  .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                  .withColumn(\"is_even\", $\"value\" % 2 === 0))\n\n              // replaceWhere on non-partitioning columns if enabled.\n              if (enabled) {\n                Seq(6, 8).toDF()\n                  .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                  .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                  .write\n                  .format(\"delta\")\n                  .mode(\"overwrite\")\n                  .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_even = true\")\n                  .save(dir.toString)\n                checkAnswer(\n                  data,\n                  Seq(5, 6, 7, 8).toDF()\n                    .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                    .withColumn(\"is_even\", $\"value\" % 2 === 0))\n              }\n            }\n          }\n        }\n      }\n    }\n\n  testColumnMapping(\"valid replaceWhere\", enableSQLConf = true) { _ =>\n    testReplaceWhere()\n  }\n\n  protected def verifyUpgradeAndTestSchemaEvolution(tableName: String): Unit = {\n    checkProperties(tableName,\n      readerVersion = 2,\n      writerVersion = 5,\n      mode = Some(\"name\"),\n      curMaxId = 4)\n    checkSchema(tableName, schemaWithIdNested)\n    val expectedSchema = new StructType()\n      .add(\"a\", StringType, true, withIdAndPhysicalName(1, \"a\"))\n      .add(\"b\",\n        new StructType()\n          .add(\"c\", StringType, true, withIdAndPhysicalName(3, \"c\"))\n          .add(\"d\", IntegerType, true, withIdAndPhysicalName(4, \"d\")),\n        true,\n        withIdAndPhysicalName(2, \"b\"))\n\n    assertEqual(\n      DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))._2.schema,\n      expectedSchema,\n      ignorePhysicalName = false)\n\n    checkAnswer(spark.table(tableName), dfWithoutIdsNested(spark))\n\n    // test schema evolution\n    val newNestedData =\n      spark.createDataFrame(\n        Seq(Row(\"str3\", Row(\"str1.3\", 3), \"new value\")).asJava,\n        schemaNested.add(\"e\", StringType))\n    newNestedData.write.format(\"delta\")\n      .option(\"mergeSchema\", \"true\")\n      .mode(\"append\").saveAsTable(tableName)\n    checkAnswer(\n      spark.table(tableName),\n      dfWithoutIdsNested(spark).withColumn(\"e\", lit(null)).union(newNestedData))\n\n    val newTableSchema = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))._2.schema\n    val newPhysicalName = DeltaColumnMapping.getPhysicalName(newTableSchema(\"e\"))\n\n    // physical name of new column should be GUID, not display name\n    assert(newPhysicalName.startsWith(\"col-\"))\n    assertEqual(\n      newTableSchema,\n      expectedSchema.add(\"e\", StringType, true, withIdAndPhysicalName(5, newPhysicalName)),\n      ignorePhysicalName = false)\n  }\n\n  test(\"change mode on new protocol table\") {\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\n        \"t1\",\n        isPartitioned = true,\n        nested = true,\n        props = Map(\n          DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n          DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n\n        alterTableWithProps(\"t1\", Map(\n          DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))\n      verifyUpgradeAndTestSchemaEvolution(\"t1\")\n    }\n  }\n\n  test(\"upgrade first and then change mode\") {\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\", isPartitioned = true, nested = true)\n      alterTableWithProps(\"t1\", Map(\n        DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n        DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n\n        alterTableWithProps(\"t1\", Map(\n          DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))\n      verifyUpgradeAndTestSchemaEvolution(\"t1\")\n    }\n  }\n\n  test(\"upgrade and change mode in one ALTER TABLE cmd\") {\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\", isPartitioned = true, nested = true)\n\n        alterTableWithProps(\"t1\", Map(\n          DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\",\n          DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n          DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n      verifyUpgradeAndTestSchemaEvolution(\"t1\")\n    }\n  }\n\n  test(\"illegal mode changes\") {\n    val oldModes = Seq(\"none\") ++ supportedModes\n    val newModes = Seq(\"none\") ++ supportedModes\n    val upgrade = Seq(true, false)\n    val removalAllowed = Seq(true, false)\n    for(oldMode <- oldModes; newMode <- newModes; ug <- upgrade; ra <- removalAllowed) {\n      val oldProps = Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> oldMode)\n      val newProps = Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> newMode) ++\n        (if (!ug) Map.empty else Map(\n          DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n          DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n      val isSupportedChange = {\n        // No change.\n        (oldMode == newMode) ||\n          // Downgrade allowed with a flag.\n          (ra && oldMode != NoMapping.name && newMode == NoMapping.name) ||\n          // Upgrade always allowed.\n          (oldMode == NoMapping.name && newMode == NameMapping.name)\n      }\n      if (!isSupportedChange) {\n        Given(s\"old mode: $oldMode, new mode: $newMode, upgrade: $ug, removalAllowed: $ra\")\n        val e = intercept[UnsupportedOperationException] {\n          withTable(\"t1\") {\n            createTableWithSQLAPI(\"t1\", props = oldProps)\n            withSQLConf(DeltaSQLConf.ALLOW_COLUMN_MAPPING_REMOVAL.key ->\n              ra.toString) {\n              alterTableWithProps(\"t1\", props = newProps)\n            }\n          }\n        }\n        assert(e.getMessage.contains(\"Changing column mapping mode from\"))\n      }\n    }\n  }\n\n  test(\"getPhysicalNameFieldMap\") {\n    // To keep things simple, we use schema `schemaWithPhysicalNamesNested` such that the\n    // physical name is just the logical name repeated three times.\n\n    val actual = DeltaColumnMapping\n      .getPhysicalNameFieldMap(schemaWithPhysicalNamesNested)\n      .map { case (physicalPath, field) => (physicalPath, field.name) }\n\n    val expected = Map[Seq[String], String](\n      Seq(\"aaa\") -> \"a\",\n      Seq(\"bbb\") -> \"b\",\n      Seq(\"bbb\", \"ccc\") -> \"c\",\n      Seq(\"bbb\", \"ddd\") -> \"d\",\n      Seq(\"bbb\", \"foo.foo.foo.bar.bar.bar\") -> \"foo.bar\",\n      Seq(\"bbb\", \"foo.foo.foo.bar.bar.bar\", \"fff\") -> \"f\",\n      Seq(\"ggg\") -> \"g\",\n      Seq(\"ggg\", \"ccc\") -> \"c\",\n      Seq(\"ggg\", \"ddd\") -> \"d\",\n      Seq(\"ggg\", \"foo.foo.foo.bar.bar.bar\") -> \"foo.bar\",\n      Seq(\"ggg\", \"foo.foo.foo.bar.bar.bar\", \"fff\") -> \"f\",\n      Seq(\"hhh\") -> \"h\"\n    )\n\n    assert(expected === actual,\n      s\"\"\"\n         |The actual physicalName -> logicalName map\n         |${actual.mkString(\"\\n\")}\n         |did not equal the expected map\n         |${expected.mkString(\"\\n\")}\n         |\"\"\".stripMargin)\n  }\n\n  testColumnMapping(\"is drop/rename column operation\") { mode =>\n    import DeltaColumnMapping.{isDropColumnOperation, isRenameColumnOperation}\n\n    withTable(\"t1\") {\n      def getMetadata(): MetadataAction = {\n        DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"t1\"))._2.metadata\n      }\n\n      createStrictSchemaTableWithDeltaTableApi(\n        \"t1\",\n        schemaWithPhysicalNamesNested,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode)\n      )\n\n      // case 1: currentSchema compared with itself\n      var currentMetadata = getMetadata()\n      var newMetadata = getMetadata()\n      def isBothCMEnabled: Boolean =\n        newMetadata.columnMappingMode != NoMapping &&\n          currentMetadata.columnMappingMode != NoMapping\n      assert(\n        !isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          !isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n\n      // case 2: add a top-level column\n      sql(\"ALTER TABLE t1 ADD COLUMNS (ping INT)\")\n      currentMetadata = newMetadata\n      newMetadata = getMetadata()\n      assert(\n        !isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          !isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n\n      // case 3: add a nested column\n      sql(\"ALTER TABLE t1 ADD COLUMNS (b.`foo.bar`.`my.new;col()` LONG)\")\n      currentMetadata = newMetadata\n      newMetadata = getMetadata()\n      assert(\n        !isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          !isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n\n      // case 4: drop a top-level column\n      sql(\"ALTER TABLE t1 DROP COLUMN (ping)\")\n      currentMetadata = newMetadata\n      newMetadata = getMetadata()\n      assert(\n        isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          !isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n\n      // case 5: drop a nested column\n      sql(\"ALTER TABLE t1 DROP COLUMN (g.`foo.bar`)\")\n      currentMetadata = newMetadata\n      newMetadata = getMetadata()\n      assert(\n        isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          !isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n\n      // case 6: rename a top-level column\n      sql(\"ALTER TABLE t1 RENAME COLUMN a TO pong\")\n      currentMetadata = newMetadata\n      newMetadata = getMetadata()\n      assert(\n        !isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n\n      // case 7: rename a nested column\n      sql(\"ALTER TABLE t1 RENAME COLUMN b.c TO c2\")\n      currentMetadata = newMetadata\n      newMetadata = getMetadata()\n      assert(\n        !isDropColumnOperation(\n          newMetadata.schema, currentMetadata.schema, isBothCMEnabled) &&\n          isRenameColumnOperation(\n            newMetadata.schema, currentMetadata.schema, isBothCMEnabled)\n      )\n    }\n  }\n\n  Seq(true, false).foreach { cdfEnabled =>\n    var shouldBlock = cdfEnabled\n\n    val shouldBlockStr = if (shouldBlock) \"should block\" else \"should not block\"\n\n    def checkHelper(\n        log: DeltaLog,\n        newSchema: StructType,\n        action: Action,\n        shouldFail: Boolean = shouldBlock): Unit = {\n      val txn = log.startTransaction()\n      txn.updateMetadata(txn.metadata.copy(schemaString = newSchema.json))\n\n      if (shouldFail) {\n        val e = intercept[DeltaUnsupportedOperationException] {\n          txn.commit(Seq(action), DeltaOperations.ManualUpdate)\n        }.getMessage\n        assert(e == \"[DELTA_BLOCK_COLUMN_MAPPING_AND_CDC_OPERATION] \" +\n          \"Operation \\\"Manual Update\\\" is not allowed when the table has enabled \" +\n          \"change data feed (CDF) and has undergone schema changes using DROP COLUMN or RENAME \" +\n          \"COLUMN.\")\n      } else {\n        txn.commit(Seq(action), DeltaOperations.ManualUpdate)\n      }\n    }\n\n    val fileActions = Seq(\n      AddFile(\"foo\", Map.empty, 1L, 1L, dataChange = true),\n      AddFile(\"foo\", Map.empty, 1L, 1L, dataChange = true).remove) ++\n      (if (cdfEnabled) AddCDCFile(\"foo\", Map.empty, 1L) :: Nil else Nil)\n\n    testColumnMapping(\n      s\"CDF and Column Mapping: $shouldBlockStr when CDF=$cdfEnabled\",\n      enableSQLConf = true) { mode =>\n\n      def createTable(): Unit = {\n        createStrictSchemaTableWithDeltaTableApi(\n          \"t1\",\n          schemaWithPhysicalNamesNested,\n          Map(\n            DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode,\n            DeltaConfigs.CHANGE_DATA_FEED.key -> cdfEnabled.toString\n          )\n        )\n      }\n\n      Seq(\"h\", \"b.`foo.bar`.f\").foreach { colName =>\n\n        // case 1: drop column with non-FileAction action should always pass\n        withTable(\"t1\") {\n          createTable()\n          val log = DeltaLog.forTable(spark, TableIdentifier(\"t1\"))\n          val droppedColumnSchema = sql(\"SELECT * FROM t1\").drop(colName).schema\n          checkHelper(log, droppedColumnSchema, SetTransaction(\"id\", 1, None), shouldFail = false)\n        }\n\n        // case 2: rename column with FileAction should fail if $shouldBlock == true\n        fileActions.foreach { fileAction =>\n          withTable(\"t1\") {\n            createTable()\n            val log = DeltaLog.forTable(spark, TableIdentifier(\"t1\"))\n            withSQLConf(\n                DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> mode) {\n              withTable(\"t2\") {\n                sql(\"DROP TABLE IF EXISTS t2\")\n                sql(\"CREATE TABLE t2 USING DELTA AS SELECT * FROM t1\")\n                sql(s\"ALTER TABLE t2 RENAME COLUMN $colName TO ii\")\n                val renamedColumnSchema = sql(\"SELECT * FROM t2\").schema\n                checkHelper(log, renamedColumnSchema, fileAction)\n              }\n            }\n          }\n        }\n\n        // case 3: drop column with FileAction should fail if $shouldBlock == true\n        fileActions.foreach { fileAction =>\n          {\n            withTable(\"t1\") {\n              createTable()\n              val log = DeltaLog.forTable(spark, TableIdentifier(\"t1\"))\n              val droppedColumnSchema = sql(\"SELECT * FROM t1\").drop(colName).schema\n              checkHelper(log, droppedColumnSchema, fileAction)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  testColumnMapping(\"id and name mode should write field_id in parquet schema\",\n      modes = Some(Seq(\"name\", \"id\"))) { mode =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\n        \"t1\",\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"t1\"))\n      val files = snapshot.allFiles.collect()\n      files.foreach { f =>\n        val footer = ParquetFileReader.readFooter(\n          log.newDeltaHadoopConf(),\n          f.absolutePath(log),\n          ParquetMetadataConverter.NO_FILTER)\n        footer.getFileMetaData.getSchema.getFields.asScala.foreach(f =>\n          // getId.intValue will throw NPE if field id does not exist\n          assert(f.getId.intValue >= 0)\n        )\n      }\n    }\n  }\n\n  test(\"should block CM upgrade when commit has FileActions and CDF enabled\") {\n    Seq(true, false).foreach { cdfEnabled =>\n      var shouldBlock = cdfEnabled\n\n      withTable(\"t1\") {\n        createTableWithSQLAPI(\n          \"t1\",\n          props = Map(DeltaConfigs.CHANGE_DATA_FEED.key -> cdfEnabled.toString))\n\n        val table = DeltaTableV2(spark, TableIdentifier(\"t1\"))\n        val currMetadata = table.snapshot.metadata\n        val upgradeMetadata = currMetadata.copy(\n          configuration = currMetadata.configuration ++ Map(\n            DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n            DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\",\n            DeltaConfigs.COLUMN_MAPPING_MODE.key -> NameMapping.name\n          )\n        )\n\n        val txn = table.startTransactionWithInitialSnapshot()\n        txn.updateMetadata(upgradeMetadata)\n\n        if (shouldBlock) {\n          val e = intercept[DeltaUnsupportedOperationException] {\n            txn.commit(\n              AddFile(\"foo\", Map.empty, 1L, 1L, dataChange = true) :: Nil,\n              DeltaOperations.ManualUpdate)\n          }.getMessage\n          assert(e == \"[DELTA_BLOCK_COLUMN_MAPPING_AND_CDC_OPERATION] \" +\n            \"Operation \\\"Manual Update\\\" is not allowed when the table has enabled \" +\n            \"change data feed (CDF) and has undergone schema changes using DROP COLUMN or RENAME \" +\n            \"COLUMN.\")\n        } else {\n          txn.commit(\n            AddFile(\"foo\", Map.empty, 1L, 1L, dataChange = true) :: Nil,\n            DeltaOperations.ManualUpdate)\n        }\n      }\n    }\n  }\n\n  test(\"upgrade with dot column name should not be blocked\") {\n    testCreateTableColumnMappingMode(\n      \"t1\",\n      schemaWithDottedColumnNames,\n      false,\n      \"name\",\n      createNewTable = false,\n      tableFeaturesProtocolExpected = false\n    ) {\n      sql(s\"CREATE TABLE t1 (${schemaWithDottedColumnNames.toDDL}) USING DELTA\")\n      alterTableWithProps(\"t1\", props = Map(\n        DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\",\n        DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n        DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n    }\n  }\n\n  test(\"explicit id matching\") {\n    // Explicitly disable field id reading to test id mode reinitialization\n    val requiredConfs = Seq(\n      SQLConf.PARQUET_FIELD_ID_READ_ENABLED,\n      SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED)\n\n    requiredConfs.foreach { conf =>\n      withSQLConf(conf.key -> \"false\") {\n        val e = intercept[IllegalArgumentException] {\n          withTable(\"t1\") {\n            createStrictSchemaTableWithDeltaTableApi(\n              \"t1\",\n              schemaWithIdNested,\n              Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"id\")\n            )\n            val testData = spark.createDataFrame(\n              Seq(Row(\"str3\", Row(\"str1.3\", 3))).asJava, schemaWithIdNested)\n            testData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n          }\n        }\n        assert(e.getMessage.contains(conf.key))\n      }\n    }\n\n    // The above configs are enabled by default, so no need to explicitly enable.\n    withTable(\"t1\") {\n      val testSchema = schemaWithIdNested.add(\"e\", StringType, true, withId(5))\n      val testData = spark.createDataFrame(\n        Seq(Row(\"str3\", Row(\"str1.3\", 3), \"str4\")).asJava, testSchema)\n\n      createStrictSchemaTableWithDeltaTableApi(\n        \"t1\",\n        testSchema,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"id\")\n      )\n\n      testData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n\n      def read: DataFrame = spark.read.format(\"delta\").table(\"t1\")\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"t1\"))\n\n      def updateFieldIdFor(fieldName: String, newId: Int): Unit = {\n        val currentMetadata = deltaLog.update().metadata\n        val currentSchema = currentMetadata.schema\n        val field = currentSchema(fieldName)\n        deltaLog.withNewTransaction { txn =>\n          val updated = field.copy(metadata =\n            new MetadataBuilder().withMetadata(field.metadata)\n              .putLong(DeltaColumnMapping.PARQUET_FIELD_ID_METADATA_KEY, newId)\n              .putLong(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY, newId)\n              .build())\n          val newSchema = StructType(Seq(updated) ++ currentSchema.filter(_.name != field.name))\n          txn.commit(currentMetadata.copy(\n            schemaString = newSchema.json,\n            configuration = currentMetadata.configuration ++\n              // Just a big id to bypass the check\n              Map(DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> \"10000\")) :: Nil, ManualUpdate)\n        }\n      }\n\n      // Case 1: manually modify the schema to read a non-existing id\n      updateFieldIdFor(\"a\", 100)\n      // Reading non-existing id should return null\n      checkAnswer(read.select(\"a\"), Row(null) :: Nil)\n\n      // Case 2: manually modify the schema to read another field's id\n      // First let's drop e, because Delta detects duplicated field\n      sql(s\"ALTER TABLE t1 DROP COLUMN e\")\n      // point to the dropped field <e>'s data\n      updateFieldIdFor(\"a\", 5)\n      checkAnswer(read.select(\"a\"), Row(\"str4\"))\n    }\n  }\n\n  test(\"drop and recreate external Delta table with name column mapping enabled\") {\n    withTempDir { dir =>\n      withTable(\"t1\") {\n        val createExternalTblCmd: String =\n          s\"\"\"\n             |CREATE EXTERNAL TABLE t1 (a long)\n             |USING DELTA\n             |LOCATION '${dir.getCanonicalPath}'\n             |TBLPROPERTIES('delta.columnMapping.mode'='name')\"\"\".stripMargin\n        sql(createExternalTblCmd)\n        // Add column and drop the old one to increment max column ID\n        sql(s\"ALTER TABLE t1 ADD COLUMN (b long)\")\n        sql(s\"ALTER TABLE t1 DROP COLUMN a\")\n        sql(s\"ALTER TABLE t1 RENAME COLUMN b to a\")\n        val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        val configBeforeDrop = log.update().metadata.configuration\n        assert(configBeforeDrop(\"delta.columnMapping.maxColumnId\") == \"2\")\n        sql(s\"DROP TABLE t1\")\n        sql(createExternalTblCmd)\n        // Configuration after recreating the external table should match the config right\n        // before initially dropping it.\n        assert(log.update().metadata.configuration == configBeforeDrop)\n        // Adding another column picks up from the last maxColumnId and increments it\n        sql(s\"ALTER TABLE t1 ADD COLUMN (c string)\")\n        assert(log.update().metadata.configuration(\"delta.columnMapping.maxColumnId\") == \"3\")\n      }\n    }\n  }\n\n  test(\"replace external Delta table with name column mapping enabled\") {\n    withTempDir { dir =>\n      withTable(\"t1\") {\n        val replaceExternalTblCmd: String =\n          s\"\"\"\n             |CREATE OR REPLACE TABLE t1 (a long)\n             |USING DELTA\n             |LOCATION '${dir.getCanonicalPath}'\n             |TBLPROPERTIES('delta.columnMapping.mode'='name')\"\"\".stripMargin\n        sql(replaceExternalTblCmd)\n        // Add column and drop the old one to increment max column ID\n        sql(s\"ALTER TABLE t1 ADD COLUMN (b long)\")\n        sql(s\"ALTER TABLE t1 DROP COLUMN a\")\n        sql(s\"ALTER TABLE t1 RENAME COLUMN b to a\")\n        val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        assert(log.update().metadata.configuration(\"delta.columnMapping.maxColumnId\") == \"2\")\n        withSQLConf(DeltaSQLConf.REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE.key -> \"true\") {\n          sql(replaceExternalTblCmd)\n          // Replace table doesn't reassign field id if column is unchanged\n          assert(log.update().metadata.configuration(\"delta.columnMapping.maxColumnId\") == \"2\")\n        }\n        withSQLConf(DeltaSQLConf.REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE.key -> \"false\") {\n          sql(replaceExternalTblCmd)\n          // Replace table starts assigning field id from previous maxColumnId.\n          assert(log.update().metadata.configuration(\"delta.columnMapping.maxColumnId\") == \"3\")\n        }\n      }\n    }\n  }\n\n  test(\"replace delta table will reuse the field id only when column name and type unchanged\") {\n    withTempDir { dir =>\n      withTable(\"t1\") {\n        sql(s\"\"\"\n          |CREATE TABLE t1 (a long, b int)\n          |USING DELTA\n          |LOCATION '${dir.getCanonicalPath}'\n          |TBLPROPERTIES('delta.columnMapping.mode'='name')\"\"\".stripMargin)\n\n        // Check field IDs before replacement\n        val logBefore = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        val colABefore = logBefore.update().metadata.schema.fields.find(_.name == \"a\").get\n        val colBBefore = logBefore.update().metadata.schema.fields.find(_.name == \"b\").get\n        assert(colABefore.metadata.getLong(\"delta.columnMapping.id\") === 1L)\n        assert(colBBefore.metadata.getLong(\"delta.columnMapping.id\") === 2L)\n\n        withSQLConf(DeltaSQLConf.REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE.key -> \"true\") {\n          sql(s\"\"\"\n            |REPLACE TABLE t1 (a long, b long)\n            |USING DELTA\n            |LOCATION '${dir.getCanonicalPath}'\n            |TBLPROPERTIES('delta.columnMapping.mode'='name')\"\"\".stripMargin)\n        }\n\n        // Check field IDs after replacement\n        val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        val colA = log.update().metadata.schema.fields.find(_.name == \"a\").get\n        val colB = log.update().metadata.schema.fields.find(_.name == \"b\").get\n        assert(colA.metadata.getLong(\"delta.columnMapping.id\") === 1L)\n        assert(colABefore.metadata.getString(\"delta.columnMapping.physicalName\")\n          === colA.metadata.getString(\"delta.columnMapping.physicalName\"))\n\n        assert(colB.metadata.getLong(\"delta.columnMapping.id\") === 3L)\n        assert(colBBefore.metadata.getString(\"delta.columnMapping.physicalName\")\n          !== colB.metadata.getString(\"delta.columnMapping.physicalName\"))\n\n      }\n    }\n  }\n\n  test(\"replace delta table will not reuse the field id when name mapping mode changed\") {\n    withSQLConf(DeltaSQLConf.REUSE_COLUMN_METADATA_DURING_REPLACE_TABLE.key -> \"true\") {\n      Seq(\"id\", \"none\").foreach { updatedNameMapping =>\n        withTempDir { dir =>\n          withTable(\"t1\") {\n            sql(s\"\"\"\n              |CREATE TABLE t1 (a long, b int)\n              |USING DELTA\n              |LOCATION '${dir.getCanonicalPath}'\n              |TBLPROPERTIES('delta.columnMapping.mode'='name')\"\"\".stripMargin)\n\n            // Check field IDs before replacement\n            val logBefore = DeltaLog.forTable(spark, dir.getCanonicalPath)\n            val colABefore = logBefore.update().metadata.schema.fields.find(_.name == \"a\").get\n            val colBBefore = logBefore.update().metadata.schema.fields.find(_.name == \"b\").get\n            assert(colABefore.metadata.getLong(\"delta.columnMapping.id\") === 1L)\n            assert(colBBefore.metadata.getLong(\"delta.columnMapping.id\") === 2L)\n\n            // Replace table with different mapping mode\n            sql(s\"\"\"\n              |REPLACE TABLE t1 (a long, b long)\n              |USING DELTA\n              |LOCATION '${dir.getCanonicalPath}'\n              |TBLPROPERTIES('delta.columnMapping.mode'='$updatedNameMapping')\"\"\".stripMargin)\n\n            val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n            val colA = log.update().metadata.schema.fields.find(_.name == \"a\").get\n            val colB = log.update().metadata.schema.fields.find(_.name == \"b\").get\n\n            if (updatedNameMapping == \"id\") {\n              assert(colA.metadata.getLong(\"delta.columnMapping.id\") === 3L)\n              assert(colB.metadata.getLong(\"delta.columnMapping.id\") === 4L)\n            } else {\n              assert(!colA.metadata.contains(\"delta.columnMapping.id\"))\n              assert(!colB.metadata.contains(\"delta.columnMapping.id\"))\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"restore Delta table with name column mapping enabled\") {\n    withTempDir { dir =>\n      withTable(\"t1\") {\n        sql(s\"\"\"\n               |CREATE OR REPLACE TABLE t1 (a long)\n               |USING DELTA\n               |LOCATION '${dir.getCanonicalPath}'\n               |TBLPROPERTIES('delta.columnMapping.mode'='name')\"\"\".stripMargin)\n        // Add column and drop the old one to increment max column ID\n        sql(s\"ALTER TABLE t1 ADD COLUMN (b long)\")\n        sql(s\"ALTER TABLE t1 DROP COLUMN a\")\n        sql(s\"ALTER TABLE t1 RENAME COLUMN b to a\")\n        val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        assert(log.update().metadata.configuration(\"delta.columnMapping.maxColumnId\") == \"2\")\n        sql(s\"RESTORE TABLE t1 TO VERSION AS OF 0\")\n        // Restore should not reduce the max field id,\n        // but it should also not give out new field ids to the restored schema.\n        assert(log.update().metadata.configuration(\"delta.columnMapping.maxColumnId\") == \"2\")\n      }\n    }\n  }\n\n  test(\"verify internal table properties only if property exists in spec and existing metadata\") {\n    val withoutMaxColumnId = Map[String, String](\"delta.columnMapping.mode\" -> \"name\")\n    val maxColumnIdOne = Map[String, String](\n      \"delta.columnMapping.mode\" -> \"name\",\n      \"delta.columnMapping.maxColumnId\" -> \"1\"\n    )\n    val maxColumnIdOneWithOthers = Map[String, String](\n      \"delta.columnMapping.mode\" -> \"name\",\n      \"delta.columnMapping.maxColumnId\" -> \"1\",\n      \"dummy.property\" -> \"dummy\"\n    )\n    val maxColumnIdTwo = Map[String, String](\n      \"delta.columnMapping.mode\" -> \"name\",\n      \"delta.columnMapping.maxColumnId\" -> \"2\"\n    )\n    // Max column ID is missing in first set of configs. So don't block on verification.\n    assert(DeltaColumnMapping.verifyInternalProperties(withoutMaxColumnId, maxColumnIdOne))\n    // Max column ID matches.\n    assert(DeltaColumnMapping.verifyInternalProperties(maxColumnIdOne, maxColumnIdOneWithOthers))\n    // Max column IDs don't match\n    assert(!DeltaColumnMapping.verifyInternalProperties(maxColumnIdOne, maxColumnIdTwo))\n  }\n\n  testColumnMapping(\n    \"overwrite a column mapping table should preserve column mapping metadata\",\n    enableSQLConf = true) { _ =>\n    val data = spark.range(10).toDF(\"id\").withColumn(\"value\", lit(1))\n\n    def checkReadability(\n        oldDf: DataFrame,\n        expected: DataFrame,\n        overwrite: () => Unit,\n        // Whether the new data files are readable after applying the fix.\n        readableWithFix: Boolean = true,\n        // Whether the method can read the new data files out of box, regardless of the fix.\n        readableOutOfBox: Boolean = false): Unit = {\n      // Overwrite\n      overwrite()\n      if (readableWithFix) {\n        // Previous analyzed DF is still readable\n        // Apply a .select so the plan cache won't kick in.\n        checkAnswer(oldDf.select(\"id\"), expected.select(\"id\").collect())\n        withSQLConf(DeltaSQLConf.REUSE_COLUMN_MAPPING_METADATA_DURING_OVERWRITE.key -> \"false\") {\n          // Overwrite again\n          overwrite()\n          if (readableOutOfBox) {\n            checkAnswer(oldDf.select(\"value\"), expected.select(\"value\").collect())\n          } else {\n            // Without the fix, will fail\n            assert {\n              intercept[DeltaAnalysisException] {\n                oldDf.select(\"value\").collect()\n              }.getErrorClass == \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\"\n            }\n          }\n        }\n      } else {\n        // Not readable, just fail\n        assert {\n          intercept[DeltaAnalysisException] {\n            oldDf.select(\"value\").collect()\n          }.getErrorClass == \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\"\n        }\n      }\n    }\n\n    // Readable - overwrite using DF\n    val overwriteData1 = spark.range(10, 20).toDF(\"id\").withColumn(\"value\", lit(2))\n    withTempDir { dir =>\n      data.write.format(\"delta\").save(dir.getCanonicalPath)\n      val df = spark.read.format(\"delta\").load(dir.getCanonicalPath)\n      checkAnswer(df, data.collect())\n      checkReadability(df, overwriteData1, () => {\n        overwriteData1.write.mode(\"overwrite\")\n          .option(\"overwriteSchema\", \"true\")\n          .format(\"delta\")\n          .save(dir.getCanonicalPath)\n      })\n    }\n\n    // Unreadable - data type changes\n    val overwriteIncompatibleDatatType =\n      spark.range(10, 20).toDF(\"id\").withColumn(\"value\", lit(\"name\"))\n    withTempDir { dir =>\n      data.write.format(\"delta\").save(dir.getCanonicalPath)\n      val df = spark.read.format(\"delta\").load(dir.getCanonicalPath)\n      checkAnswer(df, data.collect())\n      checkReadability(df, overwriteIncompatibleDatatType, () => {\n        overwriteIncompatibleDatatType.write.mode(\"overwrite\")\n          .option(\"overwriteSchema\", \"true\")\n          .format(\"delta\")\n          .save(dir.getCanonicalPath)\n      }, readableWithFix = false)\n    }\n\n    def withTestTable(f: (String, DataFrame) => Unit): Unit = {\n      val tableName = s\"cm_table\"\n      withTable(tableName) {\n        data.createOrReplaceTempView(\"src_data\")\n        spark.sql(s\"CREATE TABLE $tableName USING DELTA AS SELECT * FROM src_data\")\n        val df = spark.read.table(tableName)\n        checkAnswer(df, data.collect())\n\n        f(tableName, df)\n      }\n    }\n\n    withTestTable { (tableName, df) =>\n      // \"overwrite\" using REPLACE won't be covered by this fix because this is logically equivalent\n      // to DROP and RECREATE a new table. Therefore this optimization won't kick in.\n      overwriteData1.createOrReplaceTempView(\"overwrite_data\")\n      checkReadability(df, overwriteData1, () => {\n        spark.sql(s\"REPLACE TABLE $tableName USING DELTA AS SELECT * FROM overwrite_data\")\n      }, readableWithFix = false)\n    }\n\n    withTestTable { (tableName, df) =>\n      // \"overwrite\" using INSERT OVERWRITE actually works without this fix because it will NOT\n      // trigger the overwriteSchema code path. In this case, the pre and post schema are exactly\n      // the same, so in fact no schema updates would occur.\n      val overwriteData2 = spark.range(20, 30).toDF(\"id\").withColumn(\"value\", lit(2))\n      overwriteData2.createOrReplaceTempView(\"overwrite_data2\")\n      checkReadability(df, overwriteData2, () => {\n        spark.sql(s\"INSERT OVERWRITE $tableName SELECT * FROM overwrite_data2\")\n      }, readableOutOfBox = true)\n    }\n  }\n\n  test(\"column mapping upgrade with table features\") {\n    val testTableName = \"columnMappingTestTable\"\n    withTable(testTableName) {\n      val minReaderKey = DeltaConfigs.MIN_READER_VERSION.key\n      val minWriterKey = DeltaConfigs.MIN_WRITER_VERSION.key\n      sql(\n        s\"\"\"CREATE TABLE $testTableName\n           |USING DELTA\n           |TBLPROPERTIES(\n           |'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true'\n           |)\n           |AS SELECT * FROM RANGE(1)\n           |\"\"\".stripMargin)\n\n      // [[DeltaColumnMapping.verifyAndUpdateMetadataChange]] should not throw an error. The table\n      // does not need to support read table features too.\n      val columnMappingMode = DeltaConfigs.COLUMN_MAPPING_MODE.key\n      sql(\n        s\"\"\"ALTER TABLE $testTableName SET TBLPROPERTIES(\n           |'$columnMappingMode'='name'\n           |)\"\"\".stripMargin)\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      assert(deltaLog.update().protocol === Protocol(2, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        ColumnMappingTableFeature,\n        RowTrackingFeature\n      )))\n    }\n  }\n\n  test(\"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAMES exception should include column names\") {\n    val testTableName = \"columnMappingTestTable\"\n    withTable(testTableName) {\n      val invalidColName1 = colName(\"col1\")\n      val invalidColName2 = colName(\"col2\")\n      // Make sure the error class stays the same for a single and multiple columns.\n      testWithInvalidColumns(Seq(invalidColName1))\n      testWithInvalidColumns(Seq(invalidColName1, invalidColName2))\n\n      def testWithInvalidColumns(invalidColumns: Seq[String]): Unit = {\n        val allColumns = (Seq(\"a\", \"b\") ++ invalidColumns)\n          .mkString(\"(`\", \"` int, `\", \"` int)\")\n        val e = intercept[DeltaAnalysisException] {\n          sql(\n            s\"\"\"CREATE TABLE $testTableName $allColumns\n               |USING DELTA\n               |TBLPROPERTIES('${DeltaConfigs.COLUMN_MAPPING_MODE.key}'='none')\n               |\"\"\".stripMargin)\n        }\n        checkError(e, \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAMES\", \"42K05\",\n          Map(\"invalidColumnNames\" -> invalidColumns.mkString(\", \"))\n        )\n      }\n    }\n  }\n\n  test(\"filters pushed down to parquet use physical names\") {\n    val tableName = \"table_name\"\n    withTable(tableName) {\n      // Create a table with column mapping **disabled**\n      sql(\n        s\"\"\"CREATE TABLE $tableName (a INT, b INT)\n           |USING DELTA\n           |TBLPROPERTIES (\n           |  '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none',\n           |  '${DeltaConfigs.MIN_READER_VERSION.key}' = '2',\n           |  '${DeltaConfigs.MIN_WRITER_VERSION.key}' = '5'\n           |)\n           |\"\"\".stripMargin)\n\n      sql(s\"INSERT INTO $tableName VALUES (100, 1000)\")\n\n      sql(\n        s\"\"\"ALTER TABLE $tableName\n           |SET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n           |\"\"\".stripMargin)\n\n      // Confirm that the physical names are equal to the logical names\n      val schema = DeltaLog.forTable(spark, TableIdentifier(tableName)).update().schema\n      assert(DeltaColumnMapping.getPhysicalName(schema(\"a\")) == \"a\")\n      assert(DeltaColumnMapping.getPhysicalName(schema(\"b\")) == \"b\")\n\n      // Rename the columns so that the logical name of the second column is equal to the physical\n      // name of the first column.\n      sql(s\"ALTER TABLE $tableName RENAME COLUMN a TO c\")\n      sql(s\"ALTER TABLE $tableName RENAME COLUMN b TO a\")\n\n      // Filter the table by the second column. This will return empty results if the filter was\n      // (incorrectly) pushed down without translating the logical names to physical names.\n      checkAnswer(\n        sql(s\"SELECT * FROM $tableName WHERE a = 1000\"),\n        Seq(Row(100, 1000))\n      )\n    }\n  }\n\n  testColumnMapping(\"stream read from column mapping does not leak metadata\") { mode =>\n    withTempDir { dir =>\n      val (t1, t2, t3) = (\n        s\"t1_${System.currentTimeMillis()}\",\n        s\"t2_${System.currentTimeMillis()}\",\n        s\"t3_${System.currentTimeMillis()}\"\n      )\n      withTable(t1, t2, t3) {\n        // Create source table with column mapping mode and partitioning\n        sql(\n          s\"\"\"CREATE TABLE $t1 (a INT, b STRING)\n             |USING DELTA\n             |PARTITIONED BY (b)\n             |TBLPROPERTIES (\n             |  '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '$mode',\n             |  '${DeltaConfigs.MIN_READER_VERSION.key}' = '2',\n             |  '${DeltaConfigs.MIN_WRITER_VERSION.key}' = '5'\n             |)\n             |\"\"\".stripMargin)\n        // Insert data into source table\n        sql(s\"INSERT INTO $t1 VALUES (1, 'a'), (2, 'b')\")\n\n        // Stream read from source table\n        val streamDf = spark.readStream.format(\"delta\").table(t1)\n        // Should not contain column mapping metadata\n        assert(streamDf.schema.forall(_.metadata.json == \"{}\"))\n\n        // Create and write to another table\n        // The streaming create-table path is what currently leaks the column mapping metadata\n        // into the target table. If it was writing to an existing table via DeltaSink, it would not\n        // leak because we pruned the column mapping metadata in [[ImplicitMetadataOperations]] when\n        // we update the target metadata.\n        val q = streamDf.writeStream\n          .partitionBy(\"b\")\n          .trigger(org.apache.spark.sql.streaming.Trigger.AvailableNow())\n          .format(\"delta\")\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint1\").getCanonicalPath)\n          .toTable(t2)\n        q.awaitTermination()\n\n        // Check target table Delta log\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(t2))\n        assert(deltaLog.update().metadata.schema.forall(_.metadata.json == \"{}\"))\n        assert(deltaLog.update().metadata.columnMappingMode == NoMapping)\n\n        // Check target table data\n        checkAnswer(spark.table(t2), Seq(Row(1, \"a\"), Row(2, \"b\")))\n      }\n    }\n  }\n\n  for (txnIntroducesMetadata <- BOOLEAN_DOMAIN) {\n    test(\"column mapping metadata are stripped when feature is disabled - \" +\n      s\"txnIntroducesMetadata=$txnIntroducesMetadata\") {\n      withTempDir { dir =>\n        val tablePath = dir.getCanonicalPath\n        val deltaLog = DeltaLog.forTable(spark, tablePath)\n        // Create the original table.\n        val schemaV0 = if (txnIntroducesMetadata) {\n          new StructType().add(\"id\", LongType, true)\n        } else {\n          new StructType().add(\"id\", LongType, true, withIdAndPhysicalName(0, \"col-0\"))\n        }\n        withSQLConf(DeltaSQLConf.DELTA_COLUMN_MAPPING_STRIP_METADATA.key -> \"false\") {\n          deltaLog.withNewTransaction(catalogTableOpt = None) { txn =>\n            val metadata = actions.Metadata(\n              name = \"testTable\",\n              schemaString = schemaV0.json,\n              configuration = Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> NoMapping.name)\n            )\n            txn.updateMetadata(metadata)\n            txn.commit(Seq.empty, ManualUpdate)\n          }\n        }\n        val metadataV0 = deltaLog.update().metadata\n        assert(DeltaColumnMapping.schemaHasColumnMappingMetadata(metadataV0.schema) ===\n          !txnIntroducesMetadata)\n\n        // Update the schema of the existing table.\n        withSQLConf(DeltaSQLConf.DELTA_COLUMN_MAPPING_STRIP_METADATA.key -> \"true\") {\n          deltaLog.withNewTransaction(catalogTableOpt = None) { txn =>\n            val schemaV1 =\n              schemaV0.add(\"value\", LongType, true, withIdAndPhysicalName(0, \"col-0\"))\n            val metadata = metadataV0.copy(schemaString = schemaV1.json)\n            txn.updateMetadata(metadata)\n            txn.commit(Seq.empty, ManualUpdate)\n          }\n          val metadataV1 = deltaLog.update().metadata\n          assert(DeltaColumnMapping.schemaHasColumnMappingMetadata(metadataV1.schema) ===\n            !txnIntroducesMetadata)\n        }\n      }\n    }\n  }\n\n  test(\"Illegal null value specified for delta.columnMapping.mode option\") {\n    withTempPath { tempPath =>\n      val ex = intercept[DeltaIllegalArgumentException] {\n        spark.range(10).write.mode(\"overwrite\").format(\"delta\").\n          option(\"delta.columnMapping.mode\", null).save(tempPath.toString)\n      }\n      val supportedModes = DeltaColumnMapping.supportedModes.map(_.name).toSeq.mkString(\", \")\n      assert(ex.getErrorClass === \"DELTA_MODE_NOT_SUPPORTED\")\n      assert(ex.getMessage.contains(\"Specified mode 'null' is not supported. \" +\n        s\"Supported modes are: $supportedModes\"))\n    }\n  }\n\n  test(\"enabling column mapping disallowed if column mapping metadata already exists\") {\n    withSQLConf(\n      // enabling this fixes the issue of committing invalid metadata in the first place\n      DeltaSQLConf.DELTA_COLUMN_MAPPING_STRIP_METADATA.key -> \"false\"\n    ) {\n      withTempDir { dir =>\n        val path = dir.getCanonicalPath\n        val deltaLog = DeltaLog.forTable(spark, path)\n        deltaLog.withNewTransaction(catalogTableOpt = None) { txn =>\n          val schema =\n            new StructType().add(\"id\", IntegerType, true, withIdAndPhysicalName(0, \"col-0\"))\n          val metadata = actions.Metadata(\n            name = \"test_table\",\n            schemaString = schema.json,\n            configuration = Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> NoMapping.name)\n          )\n          txn.updateMetadata(metadata)\n          txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n\n          // Enabling the config will disallow enabling column mapping.\n          withSQLConf(DeltaSQLConf\n            .DELTA_COLUMN_MAPPING_DISALLOW_ENABLING_WHEN_METADATA_ALREADY_EXISTS.key\n            -> \"true\") {\n            val e = intercept[DeltaColumnMappingUnsupportedException] {\n              alterTableWithProps(\n                s\"delta.`$path`\",\n                Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> NameMapping.name))\n            }\n            assert(e.getErrorClass ==\n            \"DELTA_ENABLING_COLUMN_MAPPING_DISALLOWED_WHEN_COLUMN_MAPPING_METADATA_ALREADY_EXISTS\")\n          }\n\n          // Disabling the config will allow enabling column mapping.\n          withSQLConf(DeltaSQLConf\n              .DELTA_COLUMN_MAPPING_DISALLOW_ENABLING_WHEN_METADATA_ALREADY_EXISTS.key\n            -> \"false\") {\n            alterTableWithProps(\n              s\"delta.`$path`\",\n              Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> NameMapping.name))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"unit test physical name assigning is case-insensitive\") {\n    val schema = new StructType()\n      .add(\"A\", IntegerType)\n      .add(\"b\", IntegerType)\n    val fieldPathToPhysicalName = Map(Seq(\"a\") -> \"x\", Seq(\"b\") -> \"y\")\n    val schemaWithPhysicalNames = DeltaColumnMapping.setPhysicalNames(\n      schema = schema,\n      fieldPathToPhysicalName = fieldPathToPhysicalName)\n    assert(DeltaColumnMapping.getLogicalNameToPhysicalNameMap(schemaWithPhysicalNames) === Map(\n      Seq(\"A\") -> Seq(\"x\"),\n      Seq(\"b\") -> Seq(\"y\")))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaColumnMappingTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaColumnMappingSelectedTestMixin\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport io.delta.tables.{DeltaTable => OSSDeltaTable}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{Column, DataFrame, DataFrameWriter, Dataset, QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.ExternalCatalogUtils\nimport org.apache.spark.sql.catalyst.expressions.Attribute\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{AtomicType, StructField, StructType}\n\ntrait DeltaColumnMappingTestUtilsBase extends SharedSparkSession {\n\n  import testImplicits._\n\n  protected def columnMappingMode: String = NoMapping.name\n\n  private val PHYSICAL_NAME_REGEX =\n    \"col-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\".r\n\n  implicit class PhysicalNameString(s: String) {\n    def phy(deltaLog: DeltaLog): String = {\n      PHYSICAL_NAME_REGEX\n        .findFirstIn(s)\n        .getOrElse(getPhysicalName(s, deltaLog))\n    }\n  }\n\n  protected def columnMappingEnabled: Boolean = {\n    columnMappingModeString != \"none\"\n  }\n\n  protected def columnMappingModeString: String = {\n    spark.conf.getOption(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey)\n      .getOrElse(\"none\")\n  }\n\n  /**\n   * Check if two schemas are equal ignoring column mapping metadata\n   * @param schema1 Schema\n   * @param schema2 Schema\n   */\n  protected def assertEqual(schema1: StructType, schema2: StructType): Unit = {\n    if (columnMappingEnabled) {\n      assert(\n        DeltaColumnMapping.dropColumnMappingMetadata(schema1) ==\n        DeltaColumnMapping.dropColumnMappingMetadata(schema2)\n      )\n    } else {\n      assert(schema1 == schema2)\n    }\n  }\n\n  /**\n   * Check if two table configurations are equal ignoring column mapping metadata\n   * @param config1 Table config\n   * @param config2 Table config\n   */\n  protected def assertEqual(\n      config1: Map[String, String],\n      config2: Map[String, String]): Unit = {\n    if (columnMappingEnabled) {\n      assert(dropColumnMappingConfigurations(config1) == dropColumnMappingConfigurations(config2))\n    } else {\n      assert(config1 == config2)\n    }\n  }\n\n  /**\n   * Check if a partition with specific values exists.\n   * Handles both column mapped and non-mapped cases\n   * @param partCol Partition column name\n   * @param partValue Partition value\n   * @param deltaLog DeltaLog\n   */\n  protected def assertPartitionWithValueExists(\n      partCol: String,\n      partValue: String,\n      deltaLog: DeltaLog): Unit = {\n    assert(getPartitionFilePathsWithValue(partCol, partValue, deltaLog).nonEmpty)\n  }\n\n  /**\n   * Assert partition exists in an array of set of partition names/paths\n   * @param partCol Partition column name\n   * @param deltaLog Delta log\n   * @param inputFiles Input files to scan for DF\n   */\n  protected def assertPartitionExists(\n      partCol: String,\n      deltaLog: DeltaLog,\n      inputFiles: Array[String]): Unit = {\n    val physicalName = partCol.phy(deltaLog)\n    val allFiles = deltaLog.update().allFiles.collect()\n    // NOTE: inputFiles are *not* URL-encoded.\n    val filesWithPartitions = inputFiles.map { f =>\n      allFiles.filter { af =>\n        f.contains(af.toPath.toString)\n      }.flatMap(_.partitionValues.keys).toSet\n    }\n    assert(filesWithPartitions.forall(p => p.count(_ == physicalName) > 0))\n    // for non-column mapped mode, we can check the file paths as well\n    if (!columnMappingEnabled) {\n      assert(inputFiles.forall(path => path.contains(s\"$physicalName=\")),\n          s\"${inputFiles.toSeq.mkString(\"\\n\")}\\ndidn't contain partition columns $physicalName\")\n    }\n  }\n\n  /**\n   * Load Deltalog from path\n   * @param pathOrIdentifier Location\n   * @param isIdentifier Whether the previous argument is a metastore identifier\n   * @return\n   */\n  protected def loadDeltaLog(pathOrIdentifier: String, isIdentifier: Boolean = false): DeltaLog = {\n    if (isIdentifier) {\n      DeltaLog.forTable(spark, TableIdentifier(pathOrIdentifier))\n    } else {\n      DeltaLog.forTable(spark, pathOrIdentifier)\n    }\n  }\n\n  /**\n   * Convert a (nested) column string to sequence of name parts\n   * @param col Column string\n   * @return Sequence of parts\n   */\n  protected def columnNameToParts(col: String): Seq[String] = {\n    UnresolvedAttribute.parseAttributeName(col)\n  }\n\n  /**\n   * Get partition file paths for a specific partition value\n   * @param partCol Logical or physical partition name\n   * @param partValue Partition value\n   * @param deltaLog DeltaLog\n   * @return List of paths\n   */\n  protected def getPartitionFilePathsWithValue(\n      partCol: String,\n      partValue: String,\n      deltaLog: DeltaLog): Array[String] = {\n    getPartitionFilePaths(partCol, deltaLog).getOrElse(partValue, Array.empty)\n  }\n\n  /**\n   * Get the partition value for null\n   */\n  protected def nullPartitionValue: String = {\n    if (columnMappingEnabled) {\n      null\n    } else {\n      ExternalCatalogUtils.DEFAULT_PARTITION_NAME\n    }\n  }\n\n  /**\n   * Get partition file paths grouped by partition value\n   * @param partCol Logical or physical partition name\n   * @param deltaLog DeltaLog\n   * @return Partition value to paths\n   */\n  protected def getPartitionFilePaths(\n      partCol: String,\n      deltaLog: DeltaLog): Map[String, Array[String]] = {\n    if (columnMappingEnabled) {\n      val colName = partCol.phy(deltaLog)\n      deltaLog.update().allFiles.collect()\n        .groupBy(_.partitionValues(colName))\n        .mapValues(_.map(deltaLog.dataPath.toUri.getPath + \"/\" + _.path)).toMap\n    } else {\n      val partColEscaped = s\"${ExternalCatalogUtils.escapePathName(partCol)}\"\n      val dataPath = new File(deltaLog.dataPath.toUri.getPath)\n      dataPath.listFiles().filter(_.getName.startsWith(s\"$partColEscaped=\"))\n        .groupBy(_.getName.split(\"=\").last).mapValues(_.map(_.getPath)).toMap\n    }\n  }\n\n  /**\n   * Group a list of input file paths by partition key-value pair w.r.t. delta log\n   * @param inputFiles Input file paths\n   * @param deltaLog Delta log\n   * @return A mapped array each with the corresponding partition keys\n   */\n  protected def groupInputFilesByPartition(\n      inputFiles: Array[String],\n      deltaLog: DeltaLog): Map[(String, String), Array[String]] = {\n    if (columnMappingEnabled) {\n      val allFiles = deltaLog.update().allFiles.collect()\n      val grouped = inputFiles.flatMap { f =>\n        allFiles.find {\n          af => f.contains(af.toPath.toString)\n        }.head.partitionValues.map(entry => (f, entry))\n      }.groupBy(_._2)\n      grouped.mapValues(_.map(_._1)).toMap\n    } else {\n      inputFiles.groupBy(p => {\n        val nameParts = new Path(p).getParent.getName.split(\"=\")\n        (nameParts(0), nameParts(1))\n      })\n    }\n  }\n\n  /**\n   * Drop column mapping configurations from Map\n   * @param configuration Table configuration\n   * @return Configuration\n   */\n  protected def dropColumnMappingConfigurations(\n      configuration: Map[String, String]): Map[String, String] = {\n    configuration - DeltaConfigs.COLUMN_MAPPING_MODE.key - DeltaConfigs.COLUMN_MAPPING_MAX_ID.key\n  }\n\n  /**\n   * Drop column mapping configurations from Dataset (e.g. sql(\"SHOW TBLPROPERTIES t1\")\n   * @param configs Table configuration\n   * @return Configuration Dataset\n   */\n  protected def dropColumnMappingConfigurations(\n      configs: Dataset[(String, String)]): Dataset[(String, String)] = {\n    spark.createDataset(configs.collect().filter(p =>\n      !Seq(\n        DeltaConfigs.COLUMN_MAPPING_MAX_ID.key,\n        DeltaConfigs.COLUMN_MAPPING_MODE.key\n      ).contains(p._1)\n    ))\n  }\n\n  /** Return KV pairs of Protocol-related stuff for checking the result of DESCRIBE TABLE. */\n  protected def buildProtocolProps(snapshot: Snapshot): Seq[(String, String)] = {\n    val mergedConf =\n      DeltaConfigs.mergeGlobalConfigs(spark.sessionState.conf, snapshot.metadata.configuration)\n    val metadata = snapshot.metadata.copy(configuration = mergedConf)\n    var props = Seq(\n      (Protocol.MIN_READER_VERSION_PROP,\n        Protocol.forNewTable(spark, Some(metadata)).minReaderVersion.toString),\n      (Protocol.MIN_WRITER_VERSION_PROP,\n        Protocol.forNewTable(spark, Some(metadata)).minWriterVersion.toString))\n    if (snapshot.protocol.supportsReaderFeatures || snapshot.protocol.supportsWriterFeatures) {\n      props ++=\n        Protocol.minProtocolComponentsFromAutomaticallyEnabledFeatures(\n          spark, metadata, snapshot.protocol)\n          ._3\n          .map(f => (\n            s\"${TableFeatureProtocolUtils.FEATURE_PROP_PREFIX}${f.name}\",\n            TableFeatureProtocolUtils.FEATURE_PROP_SUPPORTED))\n    }\n    props\n  }\n\n  /**\n   * Convert (nested) column name string into physical name with reference from DeltaLog\n   * If target field does not have physical name, display name is returned\n   * @param col Logical column name\n   * @param deltaLog Reference DeltaLog\n   * @return Physical column name\n   */\n  protected def getPhysicalName(col: String, deltaLog: DeltaLog): String = {\n    val nameParts = UnresolvedAttribute.parseAttributeName(col)\n    val realSchema = deltaLog.update().schema\n    getPhysicalName(nameParts, realSchema)\n  }\n\n  protected def getPhysicalName(col: String, schema: StructType): String = {\n    val nameParts = UnresolvedAttribute.parseAttributeName(col)\n    getPhysicalName(nameParts, schema)\n  }\n\n  protected def getPhysicalName(nameParts: Seq[String], schema: StructType): String = {\n    SchemaUtils.findNestedFieldIgnoreCase(schema, nameParts, includeCollections = true)\n      .map(DeltaColumnMapping.getPhysicalName)\n      .get\n  }\n\n  protected def withColumnMappingConf(mode: String)(f: => Any): Any = {\n    withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> mode) {\n      f\n    }\n  }\n\n  protected def withMaxColumnIdConf(maxId: String)(f: => Any): Any = {\n    withSQLConf(DeltaConfigs.COLUMN_MAPPING_MAX_ID.defaultTablePropertyKey -> maxId) {\n      f\n    }\n  }\n\n  /**\n   * Gets the physical names of a path. This is used for converting column paths in stats schema,\n   * so it's ok to not support MapType and ArrayType.\n   */\n  def getPhysicalPathForStats(path: Seq[String], schema: StructType): Option[Seq[String]] = {\n    if (path.isEmpty) return Some(Seq.empty)\n    val field = schema.fields.find(_.name.equalsIgnoreCase(path.head))\n    field match {\n      case Some(f @ StructField(_, _: AtomicType, _, _ )) =>\n        if (path.size == 1) Some(Seq(DeltaColumnMapping.getPhysicalName(f))) else None\n      case Some(f @ StructField(_, st: StructType, _, _)) =>\n        val tail = getPhysicalPathForStats(path.tail, st)\n        tail.map(DeltaColumnMapping.getPhysicalName(f) +: _)\n      case _ =>\n        None\n    }\n  }\n\n   /**\n   * Convert (nested) column name string into physical name.\n   * Ignore parts of special paths starting with:\n   *  1. stats columns: minValues, maxValues, numRecords\n   *  2. stats df: stats_parsed\n   *  3. partition values: partitionValues_parsed, partitionValues\n   * @param col Logical column name (e.g. a.b.c)\n   * @param schema Reference schema with metadata\n   * @return Unresolved attribute with physical name paths\n   */\n  protected def convertColumnNameToAttributeWithPhysicalName(\n      col: String,\n      schema: StructType): UnresolvedAttribute = {\n    val parts = UnresolvedAttribute.parseAttributeName(col)\n    val shouldIgnoreFirstPart = Set(\n      \"minValues\",\n      \"maxValues\",\n      \"numRecords\",\n      Checkpoints.STRUCT_PARTITIONS_COL_NAME,\n      \"partitionValues\")\n    val shouldIgnoreSecondPart = Set(Checkpoints.STRUCT_STATS_COL_NAME, \"stats\")\n    val physical = if (shouldIgnoreFirstPart.contains(parts.head)) {\n      parts.head +: getPhysicalPathForStats(parts.tail, schema).getOrElse(parts.tail)\n    } else if (shouldIgnoreSecondPart.contains(parts.head)) {\n      parts.take(2) ++ getPhysicalPathForStats(parts.slice(2, parts.length), schema)\n          .getOrElse(parts.slice(2, parts.length))\n    } else {\n      getPhysicalPathForStats(parts, schema).getOrElse(parts)\n    }\n    UnresolvedAttribute(physical)\n  }\n\n  /**\n   * Convert a list of (nested) stats columns into physical name with reference from DeltaLog\n   * @param columns Logical columns\n   * @param deltaLog Reference DeltaLog\n   * @return Physical columns\n   */\n  protected def convertToPhysicalColumns(\n      columns: Seq[Column],\n      deltaLog: DeltaLog): Seq[Column] = {\n    val schema = deltaLog.update().schema\n    columns.map { col =>\n      val newExpr = col.expr.transform {\n        case a: Attribute =>\n          convertColumnNameToAttributeWithPhysicalName(a.name, schema)\n      }\n      Column(newExpr)\n    }\n  }\n\n  /**\n   * Standard CONVERT TO DELTA\n   * @param tableOrPath String\n   */\n  protected def convertToDelta(tableOrPath: String): Unit = {\n    sql(s\"CONVERT TO DELTA $tableOrPath\")\n  }\n\n  /**\n   * Force enable streaming read (with possible data loss) on column mapping enabled table with\n   * drop / rename schema changes.\n   */\n  protected def withStreamingReadOnColumnMappingTableEnabled(f: => Unit): Unit = {\n    if (columnMappingEnabled) {\n      withSQLConf(DeltaSQLConf\n        .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES.key -> \"true\") {\n        f\n      }\n    } else {\n      f\n    }\n  }\n\n}\n\ntrait DeltaColumnMappingTestUtils extends DeltaColumnMappingTestUtilsBase\n\n/**\n * Include this trait to enable Id column mapping mode for a suite\n */\ntrait DeltaColumnMappingEnableIdMode extends SharedSparkSession\n  with DeltaColumnMappingTestUtils\n  with DeltaColumnMappingSelectedTestMixin {\n\n  protected override def columnMappingMode: String = IdMapping.name\n\n  protected override def sparkConf: SparkConf =\n    super.sparkConf.set(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey, \"id\")\n\n  /**\n   * CONVERT TO DELTA blocked in id mode\n   */\n  protected override def convertToDelta(tableOrPath: String): Unit =\n    throw DeltaErrors.convertToDeltaWithColumnMappingNotSupported(\n      DeltaColumnMappingMode(columnMappingModeString)\n    )\n}\n\n/**\n * Include this trait to enable Name column mapping mode for a suite\n */\ntrait DeltaColumnMappingEnableNameMode extends SharedSparkSession\n  with DeltaColumnMappingTestUtils\n  with DeltaColumnMappingSelectedTestMixin {\n\n  protected override def columnMappingMode: String = NameMapping.name\n\n  protected override def sparkConf: SparkConf =\n    super.sparkConf.set(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey, columnMappingMode)\n\n  /**\n   * CONVERT TO DELTA can be possible under name mode in tests\n   */\n  protected override def convertToDelta(tableOrPath: String): Unit = {\n    withColumnMappingConf(\"none\") {\n      super.convertToDelta(tableOrPath)\n    }\n\n    val (deltaPath, deltaLog) =\n      if (tableOrPath.contains(\"parquet\") && tableOrPath.contains(\"`\")) {\n        // parquet.`PATH`\n        val plainPath = tableOrPath.split('.').last.drop(1).dropRight(1)\n        (s\"delta.`$plainPath`\", DeltaLog.forTable(spark, plainPath))\n      } else {\n        (tableOrPath, DeltaLog.forTable(spark, TableIdentifier(tableOrPath)))\n      }\n\n    val tableReaderVersion = deltaLog.unsafeVolatileSnapshot.protocol.minReaderVersion\n    val tableWriterVersion = deltaLog.unsafeVolatileSnapshot.protocol.minWriterVersion\n    val requiredReaderVersion = if (tableWriterVersion >=\n      TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION) {\n      // If the writer version of the table supports table features, we need to\n      // bump the reader version to table features to enable column mapping.\n      TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION\n    } else {\n      ColumnMappingTableFeature.minReaderVersion\n    }\n    val readerVersion = spark.conf.get(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION).max(\n      requiredReaderVersion)\n    val writerVersion = spark.conf.get(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION).max(\n      ColumnMappingTableFeature.minWriterVersion)\n\n    val properties = mutable.ListBuffer(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\")\n    if (tableReaderVersion < readerVersion) {\n      properties += DeltaConfigs.MIN_READER_VERSION.key -> readerVersion.toString\n    }\n    if (tableWriterVersion < writerVersion) {\n      properties += DeltaConfigs.MIN_WRITER_VERSION.key -> writerVersion.toString\n    }\n    val propertiesStr = properties.map(kv => s\"'${kv._1}' = '${kv._2}'\").mkString(\", \")\n    sql(s\"ALTER TABLE $deltaPath SET TBLPROPERTIES ($propertiesStr)\")\n  }\n\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaColumnRenameSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.types._\n\nclass DeltaColumnRenameSuite extends QueryTest\n  with DeltaArbitraryColumnNameSuiteBase\n  with GivenWhenThen {\n\n  testColumnMapping(\"rename in column mapping mode\") { mode =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode),\n        partCols = Seq(\"a\"))\n\n        spark.sql(s\"Alter table t1 RENAME COLUMN b to b1\")\n\n      // insert data after rename\n      spark.sql(\"insert into t1 \" +\n        \"values ('str3', struct('str1.3', 3), map('k3', 'v3'), array(3, 33))\")\n\n      // some queries\n      checkAnswer(\n        spark.table(\"t1\"),\n        Seq(\n          Row(\"str1\", Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11)),\n          Row(\"str2\", Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22)),\n          Row(\"str3\", Row(\"str1.3\", 3), Map(\"k3\" -> \"v3\"), Array(3, 33))))\n\n      checkAnswer(\n        spark.table(\"t1\").select(\"b1\"),\n        Seq(Row(Row(\"str1.1\", 1)), Row(Row(\"str1.2\", 2)), Row(Row(\"str1.3\", 3))))\n\n      checkAnswer(\n        spark.table(\"t1\").select(\"a\", \"b1.c\").where(\"b1.c = 'str1.2'\"),\n        Seq(Row(\"str2\", \"str1.2\")))\n\n      // b is no longer visible\n      val e = intercept[AnalysisException] {\n        spark.table(\"t1\").select(\"b\").collect()\n      }\n      // The error class is renamed in Spark 3.4\n      assert(e.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n        || e.getErrorClass == \"MISSING_COLUMN\" )\n\n      // rename partition column\n      spark.sql(s\"Alter table t1 RENAME COLUMN a to a1\")\n      // rename nested column\n      spark.sql(s\"Alter table t1 RENAME COLUMN b1.c to c1\")\n\n      // rename and verify rename history\n      val renameHistoryDf = sql(\"DESCRIBE HISTORY t1\")\n          .where(\"operation = 'RENAME COLUMN'\")\n          .select(\"version\", \"operationParameters\")\n\n      checkAnswer(renameHistoryDf,\n        Row(2, Map(\"oldColumnPath\" -> \"b\", \"newColumnPath\" -> \"b1\")) ::\n          Row(4, Map(\"oldColumnPath\" -> \"a\", \"newColumnPath\" -> \"a1\")) ::\n          Row(5, Map(\"oldColumnPath\" -> \"b1.c\", \"newColumnPath\" -> \"b1.c1\")) :: Nil)\n\n      // cannot rename column to the same name\n      assert(\n        intercept[AnalysisException] {\n          spark.sql(s\"Alter table t1 RENAME COLUMN map to map\")\n        }.getMessage.contains(\"already exists\"))\n\n      // cannot rename to a different casing\n      assert(\n        intercept[AnalysisException] {\n          spark.sql(\"Alter table t1 RENAME COLUMN arr to Arr\")\n        }.getMessage.contains(\"already exists\"))\n\n      // a is no longer visible\n      val e2 = intercept[AnalysisException] {\n        spark.table(\"t1\").select(\"a\").collect()\n      }\n      // The error class is renamed in Spark 3.4\n      assert(e2.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n        || e2.getErrorClass == \"MISSING_COLUMN\" )\n\n      // b1.c is no longer visible\n      val e3 = intercept[AnalysisException] {\n        spark.table(\"t1\").select(\"b1.c\").collect()\n      }\n      assert(e3.getMessage.contains(\"No such struct field\"))\n\n      // insert data after rename\n      spark.sql(\"insert into t1 \" +\n        \"values ('str4', struct('str1.4', 4), map('k4', 'v4'), array(4, 44))\")\n\n      checkAnswer(\n        spark.table(\"t1\").select(\"a1\", \"b1.c1\", \"map\")\n          .where(\"b1.c1 = 'str1.4'\"),\n        Seq(Row(\"str4\", \"str1.4\", Map(\"k4\" -> \"v4\"))))\n    }\n  }\n\n  test(\"rename workflow: error, upgrade to name mode and then rename\") {\n    // error when not in the correct protocol and mode\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        partCols = Seq(\"a\"))\n       val e = intercept[AnalysisException] {\n        spark.sql(s\"Alter table t1 RENAME COLUMN map to map1\")\n       }\n      assert(e.getMessage.contains(\"enable Column Mapping\") &&\n        e.getMessage.contains(\"mapping mode 'name'\"))\n\n      alterTableWithProps(\"t1\", Map(\n        DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\",\n        DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n        DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n\n      // rename a column to have arbitrary chars\n      spark.sql(s\"Alter table t1 RENAME COLUMN a to `${colName(\"a\")}`\")\n\n      // rename a column that already has arbitrary chars\n      spark.sql(s\"Alter table t1\" +\n        s\" RENAME COLUMN `${colName(\"a\")}` to `${colName(\"a1\")}`\")\n\n      // rename partition column\n      spark.sql(s\"Alter table t1 RENAME COLUMN map to `${colName(\"map\")}`\")\n\n      // insert data after rename\n      spark.sql(\"insert into t1 \" +\n        \"values ('str3', struct('str1.3', 3), map('k3', 'v3'), array(3, 33))\")\n\n      checkAnswer(\n        spark.table(\"t1\").select(colName(\"a1\"), \"b.d\", colName(\"map\"))\n          .where(\"b.c >= 'str1.2'\"),\n        Seq(Row(\"str2\", 2, Map(\"k2\" -> \"v2\")),\n          Row(\"str3\", 3, Map(\"k3\" -> \"v3\"))))\n\n      // add old column back?\n      spark.sql(s\"alter table t1 add columns (a string, map map<string, string>)\")\n\n      // insert data after rename\n      spark.sql(\"insert into t1 \" +\n        \"values ('str4', struct('str1.4', 4), map('k4', 'v4'), array(4, 44),\" +\n        \" 'new_str4', map('new_k4', 'new_v4'))\")\n\n      checkAnswer(\n        spark.table(\"t1\").select(colName(\"a1\"), \"a\", colName(\"map\"), \"map\")\n          .where(\"b.c >= 'str1.2'\"),\n        Seq(\n          Row(\"str2\", null, Map(\"k2\" -> \"v2\"), null),\n          Row(\"str3\", null, Map(\"k3\" -> \"v3\"), null),\n          Row(\"str4\", \"new_str4\", Map(\"k4\" -> \"v4\"), Map(\"new_k4\" -> \"new_v4\"))))\n    }\n  }\n\n  test(\"rename workflow: error, upgrade to name mode and then rename - \" +\n    \"nested data with duplicated column name\") {\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\", simpleNestedDataWithDuplicatedNestedColumnName)\n       val e = intercept[AnalysisException] {\n        spark.sql(s\"Alter table t1 RENAME COLUMN map to map1\")\n       }\n      assert(e.getMessage.contains(\"enable Column Mapping\") &&\n        e.getMessage.contains(\"mapping mode 'name'\"))\n\n      // Upgrading this schema shouldn't cause any errors even if there are leaf column name\n      // duplications such as a.c, b.c.\n      alterTableWithProps(\"t1\", Map(\n        DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\",\n        DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n        DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n\n      // rename shouldn't cause duplicates in column names\n      Seq((\"a\", \"b\"), (\"arr\", \"map\")).foreach { case (from, to) =>\n        val e = intercept[AnalysisException] {\n          spark.sql(s\"Alter table t1 RENAME COLUMN $from to $to\")\n        }\n        assert(e.getMessage.contains(\"Cannot rename column\"))\n      }\n\n      // spice things up by changing name to arbitrary chars\n      spark.sql(s\"Alter table t1 RENAME COLUMN a to `${colName(\"a\")}`\")\n      // rename partition column\n      spark.sql(s\"Alter table t1 RENAME COLUMN map to `${colName(\"map\")}`\")\n\n      // insert data after rename\n      spark.sql(\"insert into t1 \" +\n        \"values (struct('str3', 3), struct('str1.3', 3), map('k3', 'v3'), array(3, 33))\")\n\n      checkAnswer(\n        spark.table(\"t1\").select(colName(\"a\"), \"b.d\", colName(\"map\"))\n          .where(\"b.c >= 'str1.2'\"),\n        Seq(Row(Row(\"str2\", 2), 2, Map(\"k2\" -> \"v2\")),\n          Row(Row(\"str3\", 3), 3, Map(\"k3\" -> \"v3\"))))\n\n      // add old column back?\n      spark.sql(s\"alter table t1 add columns (a string, map map<string, string>)\")\n\n      // insert data after rename\n      spark.sql(\"insert into t1 \" +\n        \"values (struct('str4', 4), struct('str1.4', 4), map('k4', 'v4'), array(4, 44),\" +\n        \" 'new_str4', map('new_k4', 'new_v4'))\")\n\n      checkAnswer(\n        spark.table(\"t1\").select(colName(\"a\"), \"a\", colName(\"map\"), \"map\")\n          .where(\"b.c >= 'str1.2'\"),\n        Seq(\n          Row(Row(\"str2\", 2), null, Map(\"k2\" -> \"v2\"), null),\n          Row(Row(\"str3\", 3), null, Map(\"k3\" -> \"v3\"), null),\n          Row(Row(\"str4\", 4), \"new_str4\", Map(\"k4\" -> \"v4\"), Map(\"new_k4\" -> \"new_v4\"))))\n    }\n  }\n\n  test(\"rename with constraints\") {\n    withTable(\"t1\") {\n      val schemaWithNotNull =\n        simpleNestedData.schema.toDDL.replace(\"c: STRING\", \"c: STRING NOT NULL\")\n          .replace(\"`c`: STRING\", \"`c`: STRING NOT NULL\")\n\n      withTable(\"source\") {\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1 ($schemaWithNotNull)\n             |USING DELTA\n             |${partitionStmt(Seq(\"a\"))}\n             |${propString(Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))}\n             |\"\"\".stripMargin)\n        simpleNestedData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n      }\n\n      spark.sql(\"alter table t1 add constraint rangeABC check (concat(a, a) > 'str')\")\n      spark.sql(\"alter table t1 add constraint rangeBD check (`b`.`d` > 0)\")\n\n      spark.sql(\"alter table t1 add constraint arrValue check (arr[0] > 0)\")\n\n      assertException(\"Cannot alter column a\") {\n        spark.sql(\"alter table t1 rename column a to a1\")\n      }\n\n      assertException(\"Cannot alter column arr\") {\n        spark.sql(\"alter table t1 rename column arr to arr1\")\n      }\n\n\n      // cannot rename b because its child is referenced\n      assertException(\"Cannot alter column b\") {\n        spark.sql(\"alter table t1 rename column b to b1\")\n      }\n\n      // can still rename b.c because it's referenced by a null constraint\n      spark.sql(\"alter table t1 rename column b.c to c1\")\n\n      spark.sql(\"insert into t1 \" +\n        \"values ('str3', struct('str1.3', 3), map('k3', 'v3'), array(3, 33))\")\n\n      assertException(\"CHECK constraint rangeabc (concat(a, a) > 'str')\") {\n        spark.sql(\"insert into t1 \" +\n          \"values ('fail constraint', struct('str1.3', 3), map('k3', 'v3'), array(3, 33))\")\n      }\n\n      assertException(\"CHECK constraint rangebd (b.d > 0)\") {\n        spark.sql(\"insert into t1 \" +\n          \"values ('str3', struct('str1.3', -1), map('k3', 'v3'), array(3, 33))\")\n      }\n\n      assertException(\"NOT NULL constraint violated for column: b.c1\") {\n        spark.sql(\"insert into t1 \" +\n          \"values ('str3', struct(null, 3), map('k3', 'v3'), array(3, 33))\")\n      }\n\n      // this is a safety flag - it won't error when you turn it off\n      withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_CHANGE_COLUMN_CHECK_EXPRESSIONS.key -> \"false\") {\n        spark.sql(\"alter table t1 rename column a to a1\")\n        spark.sql(\"alter table t1 rename column arr to arr1\")\n        spark.sql(\"alter table t1 rename column b to b1\")\n      }\n    }\n  }\n\n  test(\"rename with constraints - map element\") {\n    withTable(\"t1\") {\n      val schemaWithNotNull =\n        simpleNestedData.schema.toDDL.replace(\"c: STRING\", \"c: STRING NOT NULL\")\n          .replace(\"`c`: STRING\", \"`c`: STRING NOT NULL\")\n\n      withTable(\"source\") {\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1 ($schemaWithNotNull)\n             |USING DELTA\n             |${partitionStmt(Seq(\"a\"))}\n             |${propString(Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))}\n             |\"\"\".stripMargin)\n        simpleNestedData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n      }\n\n      spark.sql(\"alter table t1 add constraint\" +\n        \" mapValue check (not array_contains(map_keys(map), 'k1') or map['k1'] = 'v1')\")\n\n      assertException(\"Cannot alter column map\") {\n        spark.sql(\"alter table t1 rename column map to map1\")\n      }\n\n      spark.sql(\"insert into t1 \" +\n        \"values ('str3', struct('str1.3', 3), map('k3', 'v3'), array(3, 33))\")\n    }\n  }\n\n  test(\"rename with generated column\") {\n    withTable(\"t1\") {\n      val tableBuilder = io.delta.tables.DeltaTable.create(spark).tableName(\"t1\")\n      tableBuilder.property(\"delta.columnMapping.mode\", \"name\")\n\n      // add existing columns\n      simpleNestedSchema.map(field => (field.name, field.dataType)).foreach(col => {\n        val (colName, dataType) = col\n        val columnBuilder = io.delta.tables.DeltaTable.columnBuilder(spark, colName)\n        columnBuilder.dataType(dataType.sql)\n        tableBuilder.addColumn(columnBuilder.build())\n      })\n\n      // add generated columns\n      val genCol1 = io.delta.tables.DeltaTable.columnBuilder(spark, \"genCol1\")\n        .dataType(\"int\")\n        .generatedAlwaysAs(\"length(a)\")\n        .build()\n\n      val genCol2 = io.delta.tables.DeltaTable.columnBuilder(spark, \"genCol2\")\n        .dataType(\"int\")\n        .generatedAlwaysAs(\"b.d * 100 + arr[0]\")\n        .build()\n\n      val genCol3 = io.delta.tables.DeltaTable.columnBuilder(spark, \"genCol3\")\n        .dataType(\"string\")\n        .generatedAlwaysAs(\"concat(a, a)\")\n        .build()\n\n      tableBuilder\n        .addColumn(genCol1)\n        .addColumn(genCol2)\n        .addColumn(genCol3)\n        .partitionedBy(\"genCol2\")\n        .execute()\n\n      simpleNestedData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n\n      assertException(\"Cannot alter column a\") {\n        spark.sql(\"alter table t1 rename column a to a1\")\n      }\n\n      assertException(\"Cannot alter column b\") {\n        spark.sql(\"alter table t1 rename column b to b1\")\n      }\n\n      assertException(\"Cannot alter column b.d\") {\n        spark.sql(\"alter table t1 rename column b.d to d1\")\n      }\n\n      assertException(\"Cannot alter column arr\") {\n        spark.sql(\"alter table t1 rename column arr to arr1\")\n      }\n\n      // you can still rename b.c\n      spark.sql(\"alter table t1 rename column b.c to c1\")\n\n      // The following is just to show generated columns are actually there\n\n      // add new data (without data for generated columns so that they are auto populated)\n      spark.createDataFrame(\n        Seq(Row(\"str3\", Row(\"str1.3\", 3), Map(\"k3\" -> \"v3\"), Array(3, 33))).asJava,\n        new StructType()\n         .add(\"a\", StringType, true)\n          .add(\"b\",\n        new StructType()\n          .add(\"c1\", StringType, true)\n          .add(\"d\", IntegerType, true))\n          .add(\"map\", MapType(StringType, StringType), true)\n          .add(\"arr\", ArrayType(IntegerType), true))\n      .write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n\n      checkAnswer(spark.table(\"t1\"),\n        Seq(\n            Row(\"str1\", Row(\"str1.1\", 1), Map(\"k1\" -> \"v1\"), Array(1, 11), 4, 101, \"str1str1\"),\n            Row(\"str2\", Row(\"str1.2\", 2), Map(\"k2\" -> \"v2\"), Array(2, 22), 4, 202, \"str2str2\"),\n            Row(\"str3\", Row(\"str1.3\", 3), Map(\"k3\" -> \"v3\"), Array(3, 33), 4, 303, \"str3str3\")))\n\n      // this is a safety flag - if you turn it off, it will still error but msg is not as helpful\n      withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_CHANGE_COLUMN_CHECK_EXPRESSIONS.key -> \"false\") {\n        assertException(\"A generated column cannot use a non-existent column\") {\n          spark.sql(\"alter table t1 rename column arr to arr1\")\n        }\n        assertExceptionOneOf(Seq(\"No such struct field d in c1, d1\",\n          \"No such struct field `d` in `c1`, `d1`\")) {\n          spark.sql(\"alter table t1 rename column b.d to d1\")\n        }\n      }\n    }\n  }\n\n  /**\n   * Covers renaming a nested field using the ALTER TABLE command.\n   * @param initialColumnType Type of the single column used to create the initial test table.\n   * @param fieldToRename     Old and new name of the field to rename.\n   * @param updatedColumnType Expected type of the single column after renaming the nested field.\n   */\n  def testRenameNestedField(testName: String)(\n      initialColumnType: String,\n      fieldToRename: (String, String),\n      updatedColumnType: String): Unit =\n    testColumnMapping(s\"ALTER TABLE RENAME COLUMN - nested $testName\") { mode =>\n      withTempDir { dir =>\n        withTable(\"delta_test\") {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test (data $initialColumnType)\n               |USING delta\n               |TBLPROPERTIES (${DeltaConfigs.COLUMN_MAPPING_MODE.key} = '${mode}')\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n\n          val expectedInitialType = initialColumnType.filterNot(_.isWhitespace)\n          val expectedUpdatedType = updatedColumnType.filterNot(_.isWhitespace)\n          val fieldName = s\"data.${fieldToRename._1}\"\n\n          def columnType: DataFrame =\n            sql(\"DESCRIBE TABLE delta_test\")\n              .filter(\"col_name = 'data'\")\n              .select(\"data_type\")\n          checkAnswer(columnType, Row(expectedInitialType))\n\n          sql(s\"ALTER TABLE delta_test RENAME COLUMN $fieldName TO ${fieldToRename._2}\")\n          checkAnswer(columnType, Row(expectedUpdatedType))\n        }\n      }\n    }\n\n  testRenameNestedField(\"struct in map key\")(\n    initialColumnType = \"map<struct<a: int, b: string>, int>\",\n    fieldToRename = \"key.b\" -> \"c\",\n    updatedColumnType = \"map<struct<a: int, c: string>, int>\")\n\n  testRenameNestedField(\"struct in map value\")(\n    initialColumnType = \"map<int, struct<a: int, b: string>>\",\n    fieldToRename = \"value.b\" -> \"c\",\n    updatedColumnType = \"map<int, struct<a: int, c: string>>\")\n\n  testRenameNestedField(\"struct in array\")(\n    initialColumnType = \"array<struct<a: int, b: string>>\",\n    fieldToRename = \"element.b\" -> \"c\",\n    updatedColumnType = \"array<struct<a: int, c: string>>\")\n\n  testRenameNestedField(\"struct in nested map keys\")(\n    initialColumnType = \"map<map<struct<a: int, b: string>, int>, int>\",\n    fieldToRename = \"key.key.b\" -> \"c\",\n    updatedColumnType = \"map<map<struct<a: int, c: string>, int>, int>\")\n\n  testRenameNestedField(\"struct in nested map values\")(\n    initialColumnType = \"map<int, map<int, struct<a: int, b: string>>>\",\n    fieldToRename = \"value.value.b\" -> \"c\",\n    updatedColumnType = \"map<int, map<int, struct<a: int, c: string>>>\")\n\n  testRenameNestedField(\"struct in nested arrays\")(\n    initialColumnType = \"array<array<struct<a: int, b: string>>>\",\n    fieldToRename = \"element.element.b\" -> \"c\",\n    updatedColumnType = \"array<array<struct<a: int, c: string>>>\")\n\n  testRenameNestedField(\"struct in nested array and map\")(\n    initialColumnType = \"array<map<int, struct<a: int, b: string>>>\",\n    fieldToRename = \"element.value.b\" -> \"c\",\n    updatedColumnType = \"array<map<int, struct<a: int, c: string>>>\")\n\n  testRenameNestedField(\"struct in nested map key and array\")(\n    initialColumnType = \"map<array<struct<a: int, b: string>>, int>\",\n    fieldToRename = \"key.element.b\" -> \"c\",\n    updatedColumnType = \"map<array<struct<a: int, c: string>>, int>\")\n\n  testRenameNestedField(\"struct in nested map value and array\")(\n    initialColumnType = \"map<int, array<struct<a: int, b: string>>>\",\n    fieldToRename = \"value.element.b\" -> \"c\",\n    updatedColumnType = \"map<int, array<struct<a: int, c: string>>>\")\n\n  testColumnMapping(\"ALTER TABLE RENAME COLUMN - rename fields nested in maps\") { mode =>\n    withTable(\"t1\") {\n      val rows = Seq(\n        Row(Map(Row(1) -> Map(Row(10) -> Row(11)))),\n        Row(Map(Row(2) -> Map(Row(20) -> Row(21)))))\n\n      val df = spark.createDataFrame(\n        rows = rows.asJava,\n        schema = new StructType()\n          .add(\"a\", MapType(\n            new StructType().add(\"x\", IntegerType),\n            MapType(\n              new StructType().add(\"y\", IntegerType),\n              new StructType().add(\"z\", IntegerType)))))\n\n      createTableWithSQLAPI(\"t1\", df, Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n\n      spark.sql(s\"ALTER TABLE t1 RENAME COLUMN a.key.x to x1\")\n      checkAnswer(spark.table(\"t1\"), rows)\n\n      spark.sql(s\"ALTER TABLE t1 RENAME COLUMN a.value.key.y to y1\")\n      checkAnswer(spark.table(\"t1\"), rows)\n\n      spark.sql(s\"ALTER TABLE t1 RENAME COLUMN a.value.value.z to z1\")\n      checkAnswer(spark.table(\"t1\"), rows)\n\n      // Insert data after rename.\n      spark.sql(\"INSERT INTO t1 \" +\n        \"VALUES (map(named_struct('x', 3), map(named_struct('y', 30), named_struct('z', 31))))\")\n      checkAnswer(spark.table(\"t1\"), rows :+ Row(Map(Row(3) -> Map(Row(30) -> Row(31)))))\n    }\n  }\n\n  testColumnMapping(\"ALTER TABLE RENAME COLUMN - rename fields nested in arrays\") { mode =>\n    withTable(\"t1\") {\n      val rows = Seq(\n        Row(Array(Array(Row(10, 11), Row(12, 13)), Array(Row(14, 15), Row(16, 17)))),\n        Row(Array(Array(Row(20, 21), Row(22, 23)), Array(Row(24, 25), Row(26, 27)))))\n\n      val schema = new StructType()\n        .add(\"a\", ArrayType(ArrayType(\n          new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType))))\n      val df = spark.createDataFrame(rows.asJava, schema)\n\n      createTableWithSQLAPI(\"t1\", df, Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> mode))\n\n      spark.sql(s\"ALTER TABLE t1 RENAME COLUMN a.element.element.x to x1\")\n      checkAnswer(spark.table(\"t1\"), df)\n\n      spark.sql(s\"ALTER TABLE t1 RENAME COLUMN a.element.element.y to y1\")\n      checkAnswer(spark.table(\"t1\"), df)\n\n      // Insert data after rename.\n      spark.sql(\n        \"\"\"\n          |INSERT INTO t1 VALUES (\n          |array(\n          |  array(named_struct('x', 30, 'y', 31), named_struct('x', 32, 'y', 33)),\n          |  array(named_struct('x', 34, 'y', 35), named_struct('x', 36, 'y', 37))))\n          \"\"\".stripMargin)\n\n      val expDf3 = spark.createDataFrame(\n        (rows :+ Row(Array(Array(Row(30, 31), Row(32, 33)), Array(Row(34, 35), Row(36, 37)))))\n          .asJava,\n        schema)\n      checkAnswer(spark.table(\"t1\"), expDf3)\n    }\n  }\n\n  testColumnMapping(\"rename column with special characters and data skipping stats\") { mode =>\n    withSQLConf(DeltaSQLConf.DELTA_RENAME_COLUMN_ESCAPE_NAME.key -> \"true\") {\n      withTable(\"t1\") {\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1 (c int, d string)\n             |USING DELTA\n             |TBLPROPERTIES (\n             |  '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '$mode',\n             |  '${DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.key}' = 'c,d'\n             |)\n             |\"\"\".stripMargin)\n        spark.sql(\"INSERT INTO t1 VALUES (1, 'value1'), (2, 'value2'), (3, 'value3')\")\n\n        // Verify stats are collected before rename\n        val deltaLog = DeltaLog.forTable(spark, spark.sessionState.catalog.getTableMetadata(\n          spark.sessionState.sqlParser.parseTableIdentifier(\"t1\")))\n        val statsBefore = deltaLog.update().allFiles.collect().head.stats\n        assert(statsBefore != null && statsBefore.contains(\"numRecords\"))\n\n        // Rename column c to a name with special characters\n        spark.sql(\"ALTER TABLE t1 RENAME COLUMN c TO `c#2`\")\n\n        // Verify the rename worked\n        checkAnswer(\n          spark.table(\"t1\"),\n          Seq(Row(1, \"value1\"), Row(2, \"value2\"), Row(3, \"value3\")))\n\n        // Verify we can query using the new column name\n        checkAnswer(\n          spark.sql(\"SELECT `c#2` FROM t1 WHERE `c#2` > 1\"),\n          Seq(Row(2), Row(3)))\n\n        // Insert data after rename to ensure stats collection still works\n        spark.sql(\"INSERT INTO t1 VALUES (4, 'value4'), (5, 'value5')\")\n\n        checkAnswer(\n          spark.table(\"t1\"),\n          Seq(\n            Row(1, \"value1\"),\n            Row(2, \"value2\"),\n            Row(3, \"value3\"),\n            Row(4, \"value4\"),\n            Row(5, \"value5\")))\n\n        // Verify stats are still being collected after rename\n        val statsAfter = deltaLog.update().allFiles.collect().last.stats\n        assert(statsAfter != null && statsAfter.contains(\"numRecords\"))\n\n        // Verify the rename history includes the escaped column name\n        val renameHistoryDf = sql(\"DESCRIBE HISTORY t1\")\n          .where(\"operation = 'RENAME COLUMN'\")\n          .select(\"operationParameters\")\n\n        val operationParams = renameHistoryDf.head().getMap[String, String](0)\n        assert(operationParams(\"oldColumnPath\") == \"c\")\n        assert(operationParams(\"newColumnPath\").contains(\"c#2\"))\n\n        // Rename c#2 back to c before renaming column d\n        spark.sql(\"ALTER TABLE t1 RENAME COLUMN `c#2` TO c\")\n\n        // Verify rename back worked\n        checkAnswer(\n          spark.sql(\"SELECT c FROM t1 WHERE c = 3\"),\n          Seq(Row(3)))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCommitLockSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.{AzureLogStore, S3SingleDriverLogStore}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.{LocalSparkSession, SparkSession}\nimport org.apache.spark.sql.catalyst.plans.SQLHelper\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.util.Utils\n\nclass DeltaCommitLockSuite extends SparkFunSuite with LocalSparkSession with SQLHelper {\n\n  private def verifyIsCommitLockEnabled(path: File, expected: Boolean): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, path)\n    val txn = deltaLog.startTransaction()\n    assert(txn.isCommitLockEnabled == expected)\n  }\n\n  test(\"commit lock flag on Azure\") {\n    spark = SparkSession.builder()\n      .config(\"spark.delta.logStore.class\", classOf[AzureLogStore].getName)\n      .master(\"local[2]\")\n      .config(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key, classOf[DeltaCatalog].getName)\n      .getOrCreate()\n    val path = Utils.createTempDir()\n    try {\n      // Should lock by default on Azure\n      verifyIsCommitLockEnabled(path, expected = true)\n      // Should respect user config\n      for (enabled <- true :: false :: Nil) {\n        withSQLConf(DeltaSQLConf.DELTA_COMMIT_LOCK_ENABLED.key -> enabled.toString) {\n          verifyIsCommitLockEnabled(path, expected = enabled)\n        }\n      }\n    } finally {\n      Utils.deleteRecursively(path)\n    }\n  }\n\n  test(\"commit lock flag on S3\") {\n    spark = SparkSession.builder()\n      .config(\"spark.delta.logStore.class\", classOf[S3SingleDriverLogStore].getName)\n      .master(\"local[2]\")\n      .config(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key, classOf[DeltaCatalog].getName)\n      .getOrCreate()\n    val path = Utils.createTempDir()\n    try {\n      // Should not lock by default on S3\n      verifyIsCommitLockEnabled(path, expected = false)\n      // Should respect user config\n      for (enabled <- true :: false :: Nil) {\n        withSQLConf(DeltaSQLConf.DELTA_COMMIT_LOCK_ENABLED.key -> enabled.toString) {\n          verifyIsCommitLockEnabled(path, expected = enabled)\n        }\n      }\n    } finally {\n      Utils.deleteRecursively(path)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaConfigSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta.DeltaConfigs.{getMilliSeconds, isValidIntervalConfigValue, parseCalendarInterval}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.unsafe.types.CalendarInterval\nimport org.apache.spark.util.ManualClock\n\nclass DeltaConfigSuite extends SparkFunSuite\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  test(\"parseCalendarInterval\") {\n    for (input <- Seq(\"5 MINUTES\", \"5 minutes\", \"5 Minutes\", \"inTERval 5 minutes\")) {\n      assert(parseCalendarInterval(input) ===\n        new CalendarInterval(0, 0, TimeUnit.MINUTES.toMicros(5)))\n    }\n\n    for (input <- Seq(null, \"\", \" \")) {\n      val e = intercept[IllegalArgumentException] {\n        parseCalendarInterval(input)\n      }\n      assert(e.getMessage.contains(\"cannot be null or blank\"))\n    }\n\n    for (input <- Seq(\"interval\", \"interval1 day\", \"foo\", \"foo 1 day\")) {\n      val e = intercept[IllegalArgumentException] {\n        parseCalendarInterval(input)\n      }\n      assert(e.getMessage.contains(\"not a valid INTERVAL\"))\n    }\n  }\n\n  test(\"isValidIntervalConfigValue\") {\n    for (input <- Seq(\n        // Allow 0 microsecond because we always convert microseconds to milliseconds so 0\n        // microsecond is the same as 100 microseconds.\n        \"0 microsecond\",\n        \"1 microsecond\",\n        \"1 millisecond\",\n        \"1 day\",\n        \"-1 day 86400001 milliseconds\", // This is 1 millisecond\n        \"1 day -1 microseconds\")) {\n      assert(isValidIntervalConfigValue(parseCalendarInterval(input)))\n    }\n    for (input <- Seq(\n        \"-1 microseconds\",\n        \"-1 millisecond\",\n        \"-1 day\",\n        \"1 day -86400001 milliseconds\", // This is -1 millisecond\n        \"1 month\",\n        \"1 year\")) {\n      assert(!isValidIntervalConfigValue(parseCalendarInterval(input)), s\"$input\")\n    }\n  }\n\n  test(\"Optional Calendar Interval config\") {\n    val clock = new ManualClock(System.currentTimeMillis())\n\n    // case 1: duration not specified\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\")\n\n      val retentionTimestampOpt = DeltaLog.forTable(spark, dir, clock)\n        .snapshot.minSetTransactionRetentionTimestamp\n\n      assert(retentionTimestampOpt.isEmpty)\n    }\n\n    // case 2: valid duration specified\n    withTempDir { dir =>\n      sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n           |TBLPROPERTIES ('delta.setTransactionRetentionDuration' = 'interval 1 days')\n           |\"\"\".stripMargin)\n\n      DeltaLog.clearCache() // we want to ensure we can use the ManualClock we pass in\n\n      val log = DeltaLog.forTable(spark, dir, clock)\n      val retentionTimestampOpt = log.snapshot.minSetTransactionRetentionTimestamp\n      assert(log.clock.getTimeMillis() == clock.getTimeMillis())\n      val expectedRetentionTimestamp =\n        clock.getTimeMillis() - getMilliSeconds(parseCalendarInterval(\"interval 1 days\"))\n\n      assert(retentionTimestampOpt.contains(expectedRetentionTimestamp))\n    }\n\n    // case 3: invalid duration specified\n    withTempDir { dir =>\n      val e = intercept[IllegalArgumentException] {\n        sql(\n          s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES ('delta.setTransactionRetentionDuration' = 'interval 1 foo')\n             |\"\"\".stripMargin)\n      }\n      assert(e.getMessage.contains(\"not a valid INTERVAL\"))\n    }\n  }\n\n  test(\"DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES = true\") {\n    withSQLConf(DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES.key -> \"true\") {\n      // (1) we can set arbitrary table properties\n      withTempDir { tempDir =>\n        sql(\n          s\"\"\"CREATE TABLE delta.`${tempDir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES ('delta.autoOptimize.autoCompact' = true)\n             |\"\"\".stripMargin)\n      }\n\n      // (2) we still validate matching properties\n      withTempDir { tempDir =>\n        val e = intercept[IllegalArgumentException] {\n          sql(\n            s\"\"\"CREATE TABLE delta.`${tempDir.getCanonicalPath}` (id bigint) USING delta\n               |TBLPROPERTIES ('delta.setTransactionRetentionDuration' = 'interval 1 foo')\n               |\"\"\".stripMargin)\n        }\n        assert(e.getMessage.contains(\"not a valid INTERVAL\"))\n      }\n    }\n  }\n\n  test(\"we don't allow arbitrary delta-prefixed table properties\") {\n\n    // standard behavior\n    withSQLConf(DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES.key -> \"false\") {\n      val e = intercept[AnalysisException] {\n        withTempDir { tempDir =>\n          sql(\n            s\"\"\"CREATE TABLE delta.`${tempDir.getCanonicalPath}` (id bigint) USING delta\n               |TBLPROPERTIES ('delta.foo' = true)\n               |\"\"\".stripMargin)\n        }\n      }\n      checkError(e, \"DELTA_UNKNOWN_CONFIGURATION\", \"F0000\", Map(\n        \"config\" -> \"delta.foo\",\n        \"disableCheckConfig\" -> DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES.key))\n    }\n  }\n\n  test(\"allow setting valid and supported isolation level\") {\n    // currently only Serializable isolation level is supported\n    withTempDir { dir =>\n      sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n           |TBLPROPERTIES ('delta.isolationLevel' = 'Serializable')\n           |\"\"\".stripMargin)\n\n      val isolationLevel =\n        DeltaLog.forTable(spark, dir.getCanonicalPath).startTransaction().getDefaultIsolationLevel()\n\n      assert(isolationLevel == Serializable)\n    }\n  }\n\n  test(\"do not allow setting valid but unsupported isolation level\") {\n    withTempDir { dir =>\n      val e = intercept[IllegalArgumentException] {\n        sql(\n          s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES ('delta.isolationLevel' = 'WriteSerializable')\n             |\"\"\".stripMargin)\n      }\n      val msg = \"requirement failed: delta.isolationLevel must be Serializable\"\n      assert(e.getMessage == msg)\n    }\n  }\n\n  test(\"do not allow setting invalid isolation level\") {\n    withTempDir { dir =>\n      val e = intercept[DeltaIllegalArgumentException] {\n        sql(\n          s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES ('delta.isolationLevel' = 'InvalidSerializable')\n             |\"\"\".stripMargin)\n      }\n      checkError(e, \"DELTA_INVALID_ISOLATION_LEVEL\", \"25000\",\n        Map(\"isolationLevel\" -> \"InvalidSerializable\"))\n    }\n  }\n\n  test(\"getAllConfigs API\") {\n    assert(DeltaConfigs.getAllConfigs.contains(\"minreaderversion\"))\n    assert(!DeltaConfigs.getAllConfigs.contains(\"confignotexist\"))\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaCreateTableLikeSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.delta.commands.{\n  CreateDeltaTableCommand,\n  CreateDeltaTableLike,\n  TableCreationModes\n}\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.scalatest.exceptions.TestFailedException\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\n\nclass DeltaCreateTableLikeSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils {\n\n  def checkTableEmpty(tblName: String): Boolean = {\n    val numRows = spark.sql(s\"SELECT * FROM $tblName\")\n    numRows.count() == 0\n  }\n\n  /**\n   * This method checks if certain properties and fields of delta tables are the\n   * same between the two delta tables. Boolean values can be passed in to check\n   * or not to check (assert) the specific property. Note that for checkLocation\n   * a boolean value is not passed in. If checkLocation argument is None, location\n   * of target table will not be checked.\n   *\n   * @param checkTargetTableByPath when true, targetTbl must be a path not table name\n   * @param checkSourceTableByPath when true, srcTbl must be a path not table name\n   */\n  def checkTableCopyDelta(\n      srcTbl: String,\n      targetTbl: String,\n      checkDesc: Boolean = true,\n      checkSchemaString: Boolean = true,\n      checkPartitionColumns: Boolean = true,\n      checkConfiguration: Boolean = true,\n      checkTargetTableByPath: Boolean = false,\n      checkSourceTableByPath: Boolean = false,\n      checkLocation: Option[String] = None): Unit = {\n    val src =\n      if (checkSourceTableByPath) {\n        DeltaLog.forTable(spark, srcTbl)\n      } else {\n        DeltaLog.forTable(spark, TableIdentifier(srcTbl))\n      }\n\n    val target =\n      if (checkTargetTableByPath) {\n        DeltaLog.forTable(spark, targetTbl)\n      } else {\n        DeltaLog.forTable(spark, TableIdentifier(targetTbl))\n      }\n    assert(src.unsafeVolatileSnapshot.protocol ==\n           target.unsafeVolatileSnapshot.protocol,\n          \"protocol does not match\")\n    if (checkDesc) {\n      assert(src.unsafeVolatileSnapshot.metadata.description ==\n        target.unsafeVolatileSnapshot.metadata.description,\n        \"description/comment does not match\")\n    }\n    if (checkSchemaString) {\n      assert(src.unsafeVolatileSnapshot.metadata.schemaString ==\n        target.unsafeVolatileSnapshot.metadata.schemaString,\n        \"schema does not match\")\n    }\n    if (checkPartitionColumns) {\n      assert(src.unsafeVolatileSnapshot.metadata.partitionColumns ==\n        target.unsafeVolatileSnapshot.metadata.partitionColumns,\n      \"partition columns do not match\")\n    }\n    if (checkConfiguration) {\n      // Checks Table properties and table constraints\n      assert(src.unsafeVolatileSnapshot.metadata.configuration ==\n        target.unsafeVolatileSnapshot.metadata.configuration,\n      \"configuration does not match\")\n    }\n\n    val catalog = spark.sessionState.catalog\n    if(checkLocation.isDefined) {\n      assert(\n        catalog.getTableMetadata(TableIdentifier(targetTbl)).location.toString + \"/\"\n          == checkLocation.get ||\n          catalog.getTableMetadata(TableIdentifier(targetTbl)).location.toString ==\n            checkLocation.get, \"location does not match\")\n    }\n\n  }\n\n  /**\n   * This method checks if certain properties and fields of a table are the\n   * same between two tables. Boolean values can be passed in to check\n   * or not to check (assert) the specific property. Note that for checkLocation\n   * a boolean value is not passed in. If checkLocation argument is None, location\n   * of target table will not be checked.\n   */\n  def checkTableCopy(\n      srcTbl: String, targetTbl: String,\n      checkDesc: Boolean = true,\n      checkSchemaString: Boolean = true,\n      checkPartitionColumns: Boolean = true,\n      checkConfiguration: Boolean = true,\n      checkProvider: Boolean = true,\n      checkLocation: Option[String] = None): Unit = {\n    val srcTblDesc = spark.sessionState.catalog.\n      getTempViewOrPermanentTableMetadata(TableIdentifier(srcTbl))\n    val targetTblDesc = DeltaLog.forTable(spark, TableIdentifier(targetTbl))\n    val targetTblMetadata = targetTblDesc.unsafeVolatileSnapshot.metadata\n    if (checkDesc) {\n      assert(srcTblDesc.comment == Some(targetTblMetadata.description),\n        \"description/comment does not match\")\n    }\n    if (checkSchemaString) {\n      assert(srcTblDesc.schema ==\n        targetTblDesc.unsafeVolatileSnapshot.metadata.schema,\n        \"schema does not match\")\n    }\n    if (checkPartitionColumns) {\n      assert(srcTblDesc.partitionColumnNames ==\n        targetTblMetadata.partitionColumns,\n        \"partition columns do not match\")\n    }\n    if (checkConfiguration) {\n      // Checks Table properties\n      assert(srcTblDesc.properties == targetTblMetadata.configuration,\n        \"configuration does not match\")\n    }\n    if (checkProvider) {\n      val targetTblProvider = spark.sessionState.catalog.\n        getTempViewOrPermanentTableMetadata(TableIdentifier(targetTbl)).provider\n      assert(srcTblDesc.provider == targetTblProvider,\n        \"provider does not match\")\n    }\n    val catalog = spark.sessionState.catalog\n    if(checkLocation.isDefined) {\n      assert(\n        catalog.getTableMetadata(TableIdentifier(targetTbl)).location.toString + \"/\"\n          == checkLocation.get ||\n          catalog.getTableMetadata(TableIdentifier(targetTbl)).location.toString ==\n            checkLocation.get)\n    }\n  }\n\n  def createTable(\n      srcTbl: String, format: String = \"delta\",\n      addTblProperties: Boolean = true,\n      addComment: Boolean = true): Unit = {\n    spark.range(100)\n      .withColumnRenamed(\"id\", \"key\")\n      .withColumn(\"newCol\", lit(1))\n      .write\n      .format(format)\n      .partitionBy(\"key\")\n      .saveAsTable(srcTbl)\n\n    if (addTblProperties) {\n      spark.sql(s\"ALTER TABLE $srcTbl\" +\n        \" SET TBLPROPERTIES(this.is.my.key = 14, 'this.is.my.key2' = false)\")\n    }\n    if (format == \"delta\") {\n      spark.sql(s\"ALTER TABLE $srcTbl SET TBLPROPERTIES('delta.minReaderVersion' = '2',\" +\n        \" 'delta.minWriterVersion' = '5')\")\n    }\n    if (addComment) {\n      spark.sql(s\"COMMENT ON TABLE $srcTbl IS 'srcTbl'\")\n    }\n  }\n\n  test(\"CREATE TABLE LIKE basic test\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl)\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE with no comment\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl, addComment = false)\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE with no added table properties\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl, addTblProperties = false)\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where table has no schema\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      spark.sql(s\"CREATE TABLE $srcTbl USING DELTA\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE with no added constraints\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl\n      )\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE with IF NOT EXISTS, given that targetTable does not exist\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl)\n      spark.sql(s\"CREATE TABLE IF NOT EXISTS $targetTbl LIKE $srcTbl USING DELTA\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE with IF NOT EXISTS, given that targetTable does exist\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl)\n      spark.sql(s\"CREATE TABLE $targetTbl(key4 INT) USING DELTA\")\n      spark.sql(s\"CREATE TABLE IF NOT EXISTS $targetTbl LIKE  $srcTbl\")\n\n      val msg = intercept[TestFailedException] {\n        checkTableCopyDelta(srcTbl, targetTbl)\n      }.getMessage\n      assert(msg.contains(\"protocol does not match\"))\n    }\n  }\n\n  test(\"CREATE TABLE LIKE without IF NOT EXISTS, given that targetTable does exist\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl)\n      spark.range(100).repartition(3)\n        .withColumnRenamed(\"id4\", \"key4\")\n        .write\n        .format(\"delta\")\n        .saveAsTable(targetTbl)\n\n      val msg = intercept[DeltaAnalysisException] {\n        spark.sql(s\"CREATE TABLE $targetTbl LIKE  $srcTbl\")\n      }.getMessage\n      msg.contains(\"Table `default`.`targetTbl` already exists.\")\n    }\n  }\n\n  test(\"concurrent create Managed Catalog table commands should not fail\") {\n    withTempDir { dir =>\n      withTable(\"t\") {\n        def getCatalogTable: CatalogTable = {\n          val storage = CatalogStorageFormat.empty.copy(\n            locationUri = Some(dir.toPath.resolve(UUID.randomUUID().toString).toUri))\n          val catalogTableTarget = CatalogTable(\n            identifier = TableIdentifier(\"t\"),\n            tableType = CatalogTableType.MANAGED,\n            storage = storage,\n            provider = Some(\"delta\"),\n            schema = new StructType().add(\"id\", \"long\"))\n          new DeltaCatalog()\n            .verifyTableAndSolidify(\n              tableDesc = catalogTableTarget,\n              query = None,\n              maybeClusterBySpec = None)\n        }\n        CreateDeltaTableCommand(\n          getCatalogTable,\n          existingTableOpt = None,\n          mode = SaveMode.Ignore,\n          query = None,\n          operation = TableCreationModes.Create).run(spark)\n        assert(spark.sessionState.catalog.tableExists(TableIdentifier(\"t\")))\n        CreateDeltaTableCommand(\n          getCatalogTable,\n          existingTableOpt = None, // Set to None to simulate concurrent table creation commands.\n          mode = SaveMode.Ignore,\n          query = None,\n          operation = TableCreationModes.Create).run(spark)\n        assert(spark.sessionState.catalog.tableExists(TableIdentifier(\"t\")))\n      }\n    }\n  }\n\n  test(\"catalog-managed CREATE OR REPLACE creates missing tables\") {\n    withTempDir { dir =>\n      withTable(\"t\") {\n        val storage = CatalogStorageFormat.empty.copy(\n          locationUri = Some(dir.toPath.resolve(UUID.randomUUID().toString).toUri))\n        val catalogTableTarget = CatalogTable(\n          identifier = TableIdentifier(\"t\"),\n          tableType = CatalogTableType.MANAGED,\n          storage = storage,\n          provider = Some(\"delta\"),\n          schema = new StructType().add(\"id\", \"long\"))\n        val command = CreateDeltaTableCommand(\n          new DeltaCatalog().verifyTableAndSolidify(\n            tableDesc = catalogTableTarget,\n            query = None,\n            maybeClusterBySpec = None),\n          existingTableOpt = None,\n          mode = SaveMode.ErrorIfExists,\n          query = None,\n          operation = TableCreationModes.CreateOrReplace,\n          allowCatalogManaged = true,\n          createTableFunc = None)\n\n        command.run(spark)\n        assert(spark.sessionState.catalog.tableExists(TableIdentifier(\"t\")))\n      }\n    }\n  }\n\n  test(\"catalog-managed CREATE OR REPLACE skips catalog create callback \" +\n      \"when metadata is unchanged\") {\n    withCatalogManagedTable(createTable = false) { tableName =>\n      spark.sql(\n        s\"\"\"CREATE TABLE $tableName (id LONG) USING DELTA\n           |TBLPROPERTIES ('delta.feature.catalogManaged' = 'supported')\n           |\"\"\".stripMargin)\n\n      val existingTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))\n      val snapshot = DeltaLog.forTable(spark, existingTable).update()\n      var createCallbackCalls = 0\n\n      val command = new CreateDeltaTableLike {\n        override val table: CatalogTable = existingTable\n        override val existingTableOpt: Option[CatalogTable] = Some(existingTable)\n        override val operation: TableCreationModes.CreationMode =\n          TableCreationModes.CreateOrReplace\n        override val mode: SaveMode = SaveMode.ErrorIfExists\n        override val allowCatalogManaged: Boolean = true\n\n        def runUpdateCatalog(): Unit = {\n          updateCatalog(\n            spark,\n            table,\n            snapshot,\n            query = None,\n            didNotChangeMetadata = true,\n            createTableFunc = Some((_: CatalogTable) => {\n              createCallbackCalls += 1\n            }))\n        }\n      }\n\n      command.runUpdateCatalog()\n      assert(createCallbackCalls === 0)\n    }\n  }\n\n  test(\"catalog-managed CREATE OR REPLACE rejects query-derived nullable schema\") {\n    withCatalogManagedTable(createTable = false) { tableName =>\n      withTable(\"source\") {\n        spark.sql(\n          s\"\"\"CREATE TABLE $tableName (id LONG NOT NULL) USING DELTA\n             |TBLPROPERTIES ('delta.feature.catalogManaged' = 'supported')\n             |\"\"\".stripMargin)\n        spark.sql(s\"INSERT INTO $tableName VALUES (1)\")\n        spark.sql(\"CREATE TABLE source (id LONG) USING DELTA\")\n        spark.sql(\"INSERT INTO source VALUES (2)\")\n\n        val existingTable =\n          spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))\n        val versionBefore = DeltaLog.forTable(spark, existingTable).update().version\n        val query = spark.sql(\"SELECT id FROM source\").logicalPlan\n        val err = intercept[AssertionError] {\n          new DeltaCatalog().verifyTableAndSolidify(\n            tableDesc = existingTable.copy(schema = query.schema.asNullable),\n            query = Some(query),\n            maybeClusterBySpec = None)\n        }\n\n        assert(err.getMessage.contains(\"Can't specify table schema in CTAS.\"))\n        assert(DeltaLog.forTable(spark, existingTable).update().version === versionBefore)\n        checkAnswer(spark.sql(s\"SELECT * FROM $tableName\"), Seq(org.apache.spark.sql.Row(1L)))\n      }\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where sourceTable is a json table\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl, format = \"json\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE  $srcTbl USING DELTA\")\n      // Provider should be different, expected exception to be thrown\n      val msg = intercept[TestFailedException] {\n        checkTableCopy(srcTbl, targetTbl, checkDesc = false)\n      }.getMessage\n      assert(msg.contains(\"provider does not match\"))\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where sourceTable is a parquet table\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl, format = \"parquet\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE  $srcTbl USING DELTA\")\n      // Provider should be different, expected exception to be thrown\n      val msg = intercept[TestFailedException] {\n        checkTableCopy(srcTbl, targetTbl, checkDesc = false)\n      }.getMessage\n      assert(msg.contains(\"provider does not match\"))\n    }\n  }\n\n  test(\"CREATE TABLE LIKE test where source table is an external table\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTempDir { dir =>\n      val path = dir.toURI.toString\n\n      new File(dir.getAbsolutePath, srcTbl).mkdir()\n      withTable(srcTbl, targetTbl) {\n        spark.sql(s\"CREATE TABLE $srcTbl (key STRING) USING DELTA LOCATION '$path/$srcTbl'\")\n        spark.sql(s\"ALTER TABLE $srcTbl\" +\n          s\" SET TBLPROPERTIES(this.is.my.key = 14, 'this.is.my.key2' = false)\")\n        spark.sql(s\"COMMENT ON TABLE $srcTbl IS 'srcTbl'\")\n        spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl\")\n\n        checkTableCopyDelta(srcTbl, targetTbl)\n      }\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where target table is a named external table\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTempDir(prefix = \"sparkdirprefix\") { dir =>\n      withTable(srcTbl) {\n        createTable(srcTbl)\n        spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl LOCATION '${dir.toURI.toString}'\")\n        checkTableCopyDelta(srcTbl, targetTbl, checkLocation = Some(dir.toURI.toString))\n      }\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where target table is a nameless table\") {\n    val srcTbl = \"srcTbl\"\n    withTempDir { dir =>\n      withTable(srcTbl) {\n        createTable(srcTbl)\n        spark.sql(s\"CREATE TABLE delta.`${dir.toURI.toString}` LIKE $srcTbl\")\n        checkTableCopyDelta(srcTbl, dir.toURI.toString, checkTargetTableByPath = true\n        )\n      }\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where source is a view\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    val srcView = \"srcView\"\n    withTable(srcTbl, targetTbl) {\n      withView(srcView) {\n        createTable(srcTbl)\n        spark.sql(s\"DROP TABLE IF EXISTS $targetTbl\")\n        spark.sql(s\"CREATE VIEW srcView AS SELECT * FROM $srcTbl\")\n        spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcView USING DELTA\")\n        val targetTableDesc = DeltaLog.forTable(spark, TableIdentifier(targetTbl))\n        val srcViewDesc = spark.sessionState.catalog.\n          getTempViewOrPermanentTableMetadata(TableIdentifier(srcView))\n        assert(targetTableDesc.unsafeVolatileSnapshot.metadata.schema == srcViewDesc.schema)\n      }\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where source is a temporary view\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    val srcView = \"srcView\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl)\n      spark.sql(s\"CREATE TEMPORARY VIEW srcView AS SELECT * FROM $srcTbl\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcView USING DELTA\")\n      val targetTableDesc = DeltaLog.forTable(spark, TableIdentifier(targetTbl))\n      val srcViewDesc = spark.sessionState.catalog.\n        getTempViewOrPermanentTableMetadata(TableIdentifier(srcView))\n      assert(targetTableDesc.unsafeVolatileSnapshot.metadata.schema == srcViewDesc.schema)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where source table has a column mapping\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl\n      )\n      // Need to set minWriterVersion to 5 for column mappings to work\n      spark.sql(s\"ALTER TABLE $srcTbl SET TBLPROPERTIES('delta.minReaderVersion' = '2',\" +\n        \" 'delta.minWriterVersion' = '5')\")\n      // Need to set delta.columnMapping.mode to 'name' for column mappings to work\n      spark.sql(s\"ALTER TABLE $srcTbl SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n      spark.sql(s\"ALTER TABLE $srcTbl RENAME COLUMN key TO key2\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl USING DELTA\")\n      checkTableCopyDelta(srcTbl, targetTbl)\n    }\n  }\n\n  test(\"CREATE TABLE LIKE where user explicitly provides table properties\") {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    val expectedTbl = \"expectedTbl\"\n    withTable(srcTbl, targetTbl, expectedTbl) {\n      createTable(srcTbl, addTblProperties = false)\n      createTable(expectedTbl, addTblProperties = false)\n      spark.sql(s\"ALTER TABLE $srcTbl\" +\n        \" SET TBLPROPERTIES(this.is.my.key = 14, 'this.is.my.key2' = false,\" +\n        \"'delta.appendOnly' = 'false')\")\n      spark.sql(s\"ALTER TABLE $expectedTbl\" +\n        \" SET TBLPROPERTIES(this.is.my.key = 14, 'this.is.my.key2' = false, \" +\n        \"'this.is.my.key3' = true, 'delta.appendOnly' = 'true')\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl TBLPROPERTIES('this.is.my.key3' = true, \" +\n        s\"'delta.appendOnly' = 'true')\")\n      checkTableCopyDelta(expectedTbl, targetTbl)\n    }\n  }\n\n    test(\"CREATE TABLE LIKE where sourceTable is a parquet table and \" +\n      \"user explicitly provides table properties\"\n      ) {\n    val srcTbl = \"srcTbl\"\n    val targetTbl = \"targetTbl\"\n    withTable(srcTbl, targetTbl) {\n      createTable(srcTbl, format = \"parquet\")\n      spark.sql(s\"CREATE TABLE $targetTbl LIKE $srcTbl USING DELTA \" +\n        \"TBLPROPERTIES('this.is.my.key3' = true)\")\n      spark.sql(s\"ALTER TABLE $srcTbl SET TBLPROPERTIES('this.is.my.key3' = true)\")\n      val msg = intercept[TestFailedException] {\n        checkTableCopy(srcTbl, targetTbl, checkDesc = false)\n      }.getMessage\n      assert(msg.contains(\"provider does not match\"))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaDDLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.JavaConverters._\n\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient.UC_TABLE_ID_KEY\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient.UC_TABLE_ID_KEY_OLD\nimport org.apache.spark.sql.delta.schema.InvariantViolationException\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.hadoop.fs.{Path, UnsupportedFileSystemException}\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.NoSuchPartitionException\nimport org.apache.spark.sql.catalyst.catalog.CatalogUtils\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, LongType, StringType, StructType}\n\nclass DeltaDDLSuite extends DeltaDDLTestBase with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  override protected def verifyNullabilityFailure(exception: AnalysisException): Unit = {\n    exception.getMessage.contains(\"Cannot change nullable column to non-nullable\")\n  }\n\n  test(\"protocol-related properties are not considered during duplicate table creation\") {\n    def createTable(tableName: String, location: String): Unit = {\n      sql(s\"\"\"\n             |CREATE TABLE $tableName (id INT, val STRING)\n             |USING DELTA\n             |LOCATION '$location'\n             |TBLPROPERTIES (\n             |  'delta.columnMapping.mode' = 'name',\n             |  'delta.minReaderVersion' = '2',\n             |  'delta.minWriterVersion' = '5'\n             |)\"\"\".stripMargin\n      )\n    }\n    withTempDir { dir =>\n      val table1 = \"t1\"\n      val table2 = \"t2\"\n      withTable(table1, table2) {\n        withSQLConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"true\") {\n          createTable(table1, dir.getCanonicalPath)\n          createTable(table2, dir.getCanonicalPath)\n          val catalogTable1 = spark.sessionState.catalog.getTableMetadata(TableIdentifier(table1))\n          val catalogTable2 = spark.sessionState.catalog.getTableMetadata(TableIdentifier(table2))\n          assert(catalogTable1.properties(\"delta.columnMapping.mode\") == \"name\")\n          assert(catalogTable2.properties(\"delta.columnMapping.mode\") == \"name\")\n        }\n      }\n    }\n  }\n\n  test(\"table creation with ambiguous paths only allowed with legacy flag\") {\n    // ambiguous paths not allowed\n    withTempDir { foo =>\n      withTempDir { bar =>\n          val fooPath = foo.getCanonicalPath()\n          val barPath = bar.getCanonicalPath()\n          val e = intercept[AnalysisException] {\n            sql(s\"CREATE TABLE delta.`$fooPath`(id LONG) USING delta LOCATION '$barPath'\")\n          }\n          assert(e.message.contains(\"legacy.allowAmbiguousPathsInCreateTable\"))\n      }\n    }\n\n    // allowed with legacy flag\n    withTempDir { foo =>\n      withTempDir { bar =>\n        val fooPath = foo.getCanonicalPath()\n        val barPath = bar.getCanonicalPath()\n        withSQLConf(DeltaSQLConf.DELTA_LEGACY_ALLOW_AMBIGUOUS_PATHS.key -> \"true\") {\n          sql(s\"CREATE TABLE delta.`$fooPath`(id LONG) USING delta LOCATION '$barPath'\")\n          assert(io.delta.tables.DeltaTable.isDeltaTable(fooPath))\n          assert(!io.delta.tables.DeltaTable.isDeltaTable(barPath))\n        }\n      }\n    }\n\n    // allowed if paths are the same\n    withTempDir { foo =>\n      val fooPath = foo.getCanonicalPath()\n      sql(s\"CREATE TABLE delta.`$fooPath`(id LONG) USING delta LOCATION '$fooPath'\")\n      assert(io.delta.tables.DeltaTable.isDeltaTable(fooPath))\n    }\n  }\n\n  test(\"append table when column name with special chars\") {\n    withTable(\"t\") {\n      val schema = new StructType().add(\"a`b\", \"int\")\n      val df = spark.createDataFrame(sparkContext.emptyRDD[Row], schema)\n      df.write.format(\"delta\").saveAsTable(\"t\")\n      df.write.format(\"delta\").mode(\"append\").saveAsTable(\"t\")\n      assert(spark.table(\"t\").collect().isEmpty)\n    }\n  }\n\n  test(\"CREATE TABLE with OPTIONS\") {\n    withTempPath { path =>\n      spark.range(10).write.format(\"delta\").save(path.getCanonicalPath)\n      withTable(\"t\") {\n        def createTableWithOptions(simulateUC: Boolean): Unit = {\n          sql(\n            s\"\"\"\n               |CREATE TABLE t USING delta LOCATION 'fake://${path.getCanonicalPath}'\n               |${if (simulateUC) \"TBLPROPERTIES (test.simulateUC=true)\" else \"\"}\n               |OPTIONS (\n               |  fs.fake.impl='${classOf[FakeFileSystem].getName}',\n               |  fs.fake.impl.disable.cache=true)\n               |\"\"\".stripMargin)\n        }\n        intercept[UnsupportedFileSystemException](createTableWithOptions(false))\n        createTableWithOptions(true)\n      }\n    }\n  }\n\n  test(\"CREATE TABLE should translate old property `ucTableId` to `io.unitycatalog.tableId`\") {\n    for (withBothNewAndOldProperty <- Seq(false, true)) {\n      withTempDir { dir =>\n        withTable(\"t\") {\n          val path = dir.getCanonicalPath\n\n          if (withBothNewAndOldProperty) {\n            // Create table with old and new property key using test.simulateUC to simulate Unity\n            // Catalog\n            sql(s\"\"\"\n               |CREATE TABLE t (id INT) USING delta LOCATION '$path'\n               |TBLPROPERTIES (\n               |  test.simulateUC=true,\n               |  '$UC_TABLE_ID_KEY_OLD' = 'some-other-id',\n               |  '$UC_TABLE_ID_KEY' = 'correct-table-id'\n               |)\n               |\"\"\".stripMargin)\n          } else {\n            // Create table with old property key using test.simulateUC to simulate Unity Catalog\n            sql(s\"\"\"\n               |CREATE TABLE t (id INT) USING delta LOCATION '$path'\n               |TBLPROPERTIES (\n               |  test.simulateUC=true,\n               |  '$UC_TABLE_ID_KEY_OLD' = 'correct-table-id'\n               |)\n               |\"\"\".stripMargin)\n          }\n\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"t\"))\n          val properties = deltaLog.snapshot.getProperties\n\n          // Verify the new table id is present with the value from the old key\n          assert(properties.contains(UC_TABLE_ID_KEY),\n            s\"New table id key '$UC_TABLE_ID_KEY' should be present in table properties\")\n          assert(properties(UC_TABLE_ID_KEY) == \"correct-table-id\",\n            s\"New table id key '$UC_TABLE_ID_KEY' should have value 'correct-table-id'\")\n        }\n      }\n    }\n  }\n}\n\n\nclass DeltaDDLNameColumnMappingSuite extends DeltaDDLSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  override protected def runOnlyTests = Seq(\n    \"create table with NOT NULL - check violation through file writing\",\n    \"ALTER TABLE CHANGE COLUMN with nullability change in struct type - relaxed\"\n  )\n}\n\n\nabstract class DeltaDDLTestBase extends QueryTest with DeltaSQLTestUtils {\n  import testImplicits._\n\n  protected def verifyDescribeTable(tblName: String): Unit = {\n    val res = sql(s\"DESCRIBE TABLE $tblName\").collect()\n    assert(res.takeRight(2).map(_.getString(0)) === Seq(\"name\", \"dept\"))\n  }\n\n  protected def verifyNullabilityFailure(exception: AnalysisException): Unit\n\n  protected def getDeltaLog(tableLocation: String): DeltaLog = {\n      DeltaLog.forTable(spark, tableLocation)\n  }\n\n\n  testQuietly(\"create table with NOT NULL - check violation through file writing\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        sql(s\"\"\"\n               |CREATE TABLE delta_test(a LONG, b String NOT NULL)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        val expectedSchema = new StructType()\n          .add(\"a\", LongType, nullable = true)\n          .add(\"b\", StringType, nullable = false)\n        assert(spark.table(\"delta_test\").schema === expectedSchema)\n\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\"))\n        assert(table.location == makeQualifiedPath(dir.getAbsolutePath))\n\n        Seq((1L, \"a\")).toDF(\"a\", \"b\")\n          .write.format(\"delta\").mode(\"append\").save(table.location.toString)\n        val read = spark.read.format(\"delta\").load(table.location.toString)\n        checkAnswer(read, Seq(Row(1L, \"a\")))\n\n        intercept[InvariantViolationException] {\n          Seq((2L, null)).toDF(\"a\", \"b\")\n            .write.format(\"delta\").mode(\"append\").save(table.location.getPath)\n        }\n      }\n    }\n  }\n\n  test(\"ALTER TABLE ADD COLUMNS with NOT NULL - not supported\") {\n    withTempDir { dir =>\n      val tableName = \"delta_test_add_not_null\"\n      withTable(tableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $tableName(a LONG)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n\n        val expectedSchema = new StructType().add(\"a\", LongType, nullable = true)\n        assert(spark.table(tableName).schema === expectedSchema)\n\n        val e = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tableName\n               |ADD COLUMNS (b String NOT NULL, c Int)\"\"\".stripMargin)\n        }\n        val msg = \"`NOT NULL in ALTER TABLE ADD COLUMNS` is not supported for Delta tables\"\n        assert(e.getMessage.contains(msg))\n      }\n    }\n  }\n\n  test(\"ALTER TABLE CHANGE COLUMN from nullable to NOT NULL - not supported\") {\n    withTempDir { dir =>\n      val tableName = \"delta_test_from_nullable_to_not_null\"\n      withTable(tableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $tableName(a LONG, b String)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n\n        val expectedSchema = new StructType()\n          .add(\"a\", LongType, nullable = true)\n          .add(\"b\", StringType, nullable = true)\n        assert(spark.table(tableName).schema === expectedSchema)\n\n        val e = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tableName\n               |CHANGE COLUMN b b String NOT NULL\"\"\".stripMargin)\n        }\n        verifyNullabilityFailure(e)\n      }\n    }\n  }\n\n  test(\"ALTER TABLE CHANGE COLUMN from NOT NULL to nullable\") {\n    withTempDir { dir =>\n      val tableName = \"delta_test_not_null_to_nullable\"\n      withTable(tableName) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $tableName(a LONG NOT NULL, b String)\n             |USING delta\n             |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n\n        val expectedSchema = new StructType()\n          .add(\"a\", LongType, nullable = false)\n          .add(\"b\", StringType, nullable = true)\n        assert(spark.table(tableName).schema === expectedSchema)\n\n        sql(s\"INSERT INTO $tableName SELECT 1, 'a'\")\n        checkAnswer(\n          sql(s\"SELECT * FROM $tableName\"),\n          Seq(Row(1L, \"a\")))\n\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName\n             |ALTER COLUMN a DROP NOT NULL\"\"\".stripMargin)\n        val expectedSchema2 = new StructType()\n          .add(\"a\", LongType, nullable = true)\n          .add(\"b\", StringType, nullable = true)\n        assert(spark.table(tableName).schema === expectedSchema2)\n\n        sql(s\"INSERT INTO $tableName SELECT NULL, 'b'\")\n        checkAnswer(\n          sql(s\"SELECT * FROM $tableName\"),\n          Seq(Row(1L, \"a\"), Row(null, \"b\")))\n      }\n    }\n  }\n\n  testQuietly(\"create table with NOT NULL - check violation through SQL\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        sql(s\"\"\"\n               |CREATE TABLE delta_test(a LONG, b String NOT NULL)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        val expectedSchema = new StructType()\n          .add(\"a\", LongType, nullable = true)\n          .add(\"b\", StringType, nullable = false)\n        assert(spark.table(\"delta_test\").schema === expectedSchema)\n\n        sql(\"INSERT INTO delta_test SELECT 1, 'a'\")\n        checkAnswer(\n          sql(\"SELECT * FROM delta_test\"),\n          Seq(Row(1L, \"a\")))\n\n        val e = intercept[InvariantViolationException] {\n          sql(\"INSERT INTO delta_test VALUES (2, null)\")\n        }\n        if (!e.getMessage.contains(\"nullable values to non-null column\")) {\n          verifyInvariantViolationException(e)\n        }\n      }\n    }\n  }\n\n  testQuietly(\"create table with NOT NULL in struct type - check violation\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        sql(s\"\"\"\n               |CREATE TABLE delta_test\n               |(x struct<a: LONG, b: String NOT NULL>, y LONG)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        val expectedSchema = new StructType()\n          .add(\"x\", new StructType().\n            add(\"a\", LongType, nullable = true)\n            .add(\"b\", StringType, nullable = false))\n          .add(\"y\", LongType, nullable = true)\n        assert(spark.table(\"delta_test\").schema === expectedSchema)\n\n        sql(\"INSERT INTO delta_test SELECT (1, 'a'), 1\")\n        checkAnswer(\n          sql(\"SELECT * FROM delta_test\"),\n          Seq(Row(Row(1L, \"a\"), 1)))\n\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\"))\n        assert(table.location == makeQualifiedPath(dir.getAbsolutePath))\n\n        val schema = new StructType()\n          .add(\"x\",\n            new StructType()\n              .add(\"a\", \"bigint\")\n              .add(\"b\", \"string\"))\n          .add(\"y\", \"bigint\")\n        val e = intercept[InvariantViolationException] {\n          spark.createDataFrame(\n            Seq(Row(Row(2L, null), 2L)).asJava,\n            schema\n          ).write.format(\"delta\").mode(\"append\").save(table.location.getPath)\n        }\n        verifyInvariantViolationException(e)\n      }\n    }\n  }\n\n  test(\"ALTER TABLE ADD COLUMNS with NOT NULL in struct type - not supported\") {\n    withTempDir { dir =>\n      val tableName = \"delta_test_not_null_struct\"\n      withTable(tableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $tableName\n               |(y LONG)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        val expectedSchema = new StructType()\n          .add(\"y\", LongType, nullable = true)\n        assert(spark.table(tableName).schema === expectedSchema)\n\n        val e = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tableName\n               |ADD COLUMNS (x struct<a: LONG, b: String NOT NULL>, z INT)\"\"\".stripMargin)\n        }\n        val msg = \"Operation not allowed: \" +\n          \"`NOT NULL in ALTER TABLE ADD COLUMNS` is not supported for Delta tables\"\n        assert(e.getMessage.contains(msg))\n      }\n    }\n  }\n\n  test(\"ALTER TABLE ADD COLUMNS to table with existing NOT NULL fields\") {\n    withTempDir { dir =>\n      val tableName = \"delta_test_existing_not_null\"\n      withTable(tableName) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $tableName\n             |(y LONG NOT NULL)\n             |USING delta\n             |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        val expectedSchema = new StructType()\n          .add(\"y\", LongType, nullable = false)\n        assert(spark.table(tableName).schema === expectedSchema)\n\n        sql(\n          s\"\"\"\n             |ALTER TABLE $tableName\n             |ADD COLUMNS (x struct<a: LONG, b: String>, z INT)\"\"\".stripMargin)\n        val expectedSchema2 = new StructType()\n          .add(\"y\", LongType, nullable = false)\n          .add(\"x\", new StructType()\n            .add(\"a\", LongType)\n            .add(\"b\", StringType))\n          .add(\"z\", IntegerType)\n        assert(spark.table(tableName).schema === expectedSchema2)\n      }\n    }\n  }\n\n  /**\n   * Covers adding and changing a nested field using the ALTER TABLE command.\n   * @param initialColumnType Type of the single column used to create the initial test table.\n   * @param fieldToAdd        Tuple (name, type) of the nested field to add and change.\n   * @param updatedColumnType Expected type of the single column after adding the nested field.\n   */\n  def testAlterTableNestedFields(testName: String)(\n      initialColumnType: String,\n      fieldToAdd: (String, String),\n      updatedColumnType: String): Unit = {\n    // Remove spaces in test name so we can re-use it as a unique table name.\n    val tableName = testName.replaceAll(\" \", \"\")\n    test(s\"ALTER TABLE ADD/CHANGE COLUMNS - nested $testName\") {\n      withTempDir { dir =>\n        withTable(tableName) {\n          sql(\n            s\"\"\"\n               |CREATE TABLE $tableName (data $initialColumnType)\n               |USING delta\n               |TBLPROPERTIES (${DeltaConfigs.COLUMN_MAPPING_MODE.key} = 'name')\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n\n          val expectedInitialType = initialColumnType.filterNot(_.isWhitespace)\n          val expectedUpdatedType = updatedColumnType.filterNot(_.isWhitespace)\n          val fieldName = s\"data.${fieldToAdd._1}\"\n          val fieldType = fieldToAdd._2\n\n          def columnType: DataFrame =\n            sql(s\"DESCRIBE TABLE $tableName\")\n              .where(\"col_name = 'data'\")\n              .select(\"data_type\")\n          checkAnswer(columnType, Row(expectedInitialType))\n\n          sql(s\"ALTER TABLE $tableName ADD COLUMNS ($fieldName $fieldType)\")\n          checkAnswer(columnType, Row(expectedUpdatedType))\n\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN $fieldName TYPE $fieldType\")\n          checkAnswer(columnType, Row(expectedUpdatedType))\n        }\n      }\n    }\n  }\n\n  testAlterTableNestedFields(\"struct in map key\")(\n    initialColumnType = \"map<struct<a: int>, int>\",\n    fieldToAdd = \"key.b\" -> \"string\",\n    updatedColumnType = \"map<struct<a: int, b: string>, int>\")\n\n  testAlterTableNestedFields(\"struct in map value\")(\n    initialColumnType = \"map<int, struct<a: int>>\",\n    fieldToAdd = \"value.b\" -> \"string\",\n    updatedColumnType = \"map<int, struct<a: int, b: string>>\")\n\n  testAlterTableNestedFields(\"struct in array\")(\n    initialColumnType = \"array<struct<a: int>>\",\n    fieldToAdd = \"element.b\" -> \"string\",\n    updatedColumnType = \"array<struct<a: int, b: string>>\")\n\n  testAlterTableNestedFields(\"struct in nested map keys\")(\n    initialColumnType = \"map<map<struct<a: int>, int>, int>\",\n    fieldToAdd = \"key.key.b\" -> \"string\",\n    updatedColumnType = \"map<map<struct<a: int, b: string>, int>, int>\")\n\n  testAlterTableNestedFields(\"struct in nested map values\")(\n    initialColumnType = \"map<int, map<int, struct<a: int>>>\",\n    fieldToAdd = \"value.value.b\" -> \"string\",\n    updatedColumnType = \"map<int, map<int, struct<a: int, b: string>>>\")\n\n  testAlterTableNestedFields(\"struct in nested arrays\")(\n    initialColumnType = \"array<array<struct<a: int>>>\",\n    fieldToAdd = \"element.element.b\" -> \"string\",\n    updatedColumnType = \"array<array<struct<a: int, b: string>>>\")\n\n  testAlterTableNestedFields(\"struct in nested array and map\")(\n    initialColumnType = \"array<map<int, struct<a: int>>>\",\n    fieldToAdd = \"element.value.b\" -> \"string\",\n    updatedColumnType = \"array<map<int, struct<a: int, b: string>>>\")\n\n  testAlterTableNestedFields(\"struct in nested map key and array\")(\n    initialColumnType = \"map<array<struct<a: int>>, int>\",\n    fieldToAdd = \"key.element.b\" -> \"string\",\n    updatedColumnType = \"map<array<struct<a: int, b: string>>, int>\")\n\n  testAlterTableNestedFields(\"struct in nested map value and array\")(\n    initialColumnType = \"map<int, array<struct<a: int>>>\",\n    fieldToAdd = \"value.element.b\" -> \"string\",\n    updatedColumnType = \"map<int, array<struct<a: int, b: string>>>\")\n\n  test(\"ALTER TABLE CHANGE COLUMN with nullability change in struct type - not supported\") {\n    withTempDir { dir =>\n      val tableName = \"not_supported_delta_test\"\n      withTable(tableName) {\n        sql(s\"\"\"\n               |CREATE TABLE $tableName\n               |(x struct<a: LONG, b: String>, y LONG)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        val expectedSchema = new StructType()\n          .add(\"x\", new StructType()\n            .add(\"a\", LongType)\n            .add(\"b\", StringType))\n          .add(\"y\", LongType, nullable = true)\n        assert(spark.table(tableName).schema === expectedSchema)\n\n        val e1 = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tableName\n               |CHANGE COLUMN x x struct<a: LONG, b: String NOT NULL>\"\"\".stripMargin)\n        }\n        assert(e1.getMessage.contains(\"Cannot update\"))\n        val e2 = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tableName\n               |CHANGE COLUMN x.b b String NOT NULL\"\"\".stripMargin) // this syntax may change\n        }\n        verifyNullabilityFailure(e2)\n      }\n    }\n  }\n\n  test(\"ALTER TABLE CHANGE COLUMN with nullability change in struct type - relaxed\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      withTempDir { dir =>\n        val tblName = \"delta_test2\"\n        withTable(tblName) {\n          sql(\n            s\"\"\"\n               |CREATE TABLE $tblName\n               |(x struct<a: LONG, b: String NOT NULL> NOT NULL, y LONG)\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n          val expectedSchema = new StructType()\n            .add(\"x\", new StructType()\n              .add(\"a\", LongType)\n              .add(\"b\", StringType, nullable = false), nullable = false)\n            .add(\"y\", LongType)\n          assert(spark.table(tblName).schema === expectedSchema)\n          sql(s\"INSERT INTO $tblName SELECT (1, 'a'), 1\")\n          checkAnswer(\n            sql(s\"SELECT * FROM $tblName\"),\n            Seq(Row(Row(1L, \"a\"), 1)))\n\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tblName\n               |ALTER COLUMN x.b DROP NOT NULL\"\"\".stripMargin) // relax nullability\n          sql(s\"INSERT INTO $tblName SELECT (2, null), null\")\n          checkAnswer(\n            sql(s\"SELECT * FROM $tblName\"),\n            Seq(\n              Row(Row(1L, \"a\"), 1),\n              Row(Row(2L, null), null)))\n\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tblName\n               |ALTER COLUMN x DROP NOT NULL\"\"\".stripMargin)\n          sql(s\"INSERT INTO $tblName SELECT null, 3\")\n          checkAnswer(\n            sql(s\"SELECT * FROM $tblName\"),\n            Seq(\n              Row(Row(1L, \"a\"), 1),\n              Row(Row(2L, null), null),\n              Row(null, 3)))\n        }\n      }\n    }\n  }\n\n  private def verifyInvariantViolationException(e: InvariantViolationException): Unit = {\n    if (e == null) {\n      fail(\"Didn't receive a InvariantViolationException.\")\n    }\n    assert(e.getMessage.contains(\"NOT NULL constraint violated for column\"))\n  }\n\n  test(\"ALTER TABLE RENAME TO\") {\n    withTable(\"tbl\", \"newTbl\") {\n      sql(s\"\"\"\n            |CREATE TABLE tbl\n            |USING delta\n            |AS SELECT 1 as a, 'a' as b\n           \"\"\".stripMargin)\n      sql(s\"ALTER TABLE tbl RENAME TO newTbl\")\n      checkDatasetUnorderly(sql(\"SELECT * FROM newTbl\").as[(Long, String)], 1L -> \"a\")\n    }\n  }\n\n\n  /**\n   * Although Spark 3.2 adds the support for SHOW CREATE TABLE for v2 tables, it doesn't work\n   * properly for Delta. For example, table properties, constraints and generated columns are not\n   * showed properly.\n   *\n   * TODO Implement Delta's own ShowCreateTableCommand to show the Delta table definition correctly\n   */\n  test(\"SHOW CREATE TABLE is not supported\") {\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"\n           |CREATE TABLE delta_test(a LONG, b String)\n           |USING delta\n           \"\"\".stripMargin)\n\n      val e = intercept[AnalysisException] {\n        sql(\"SHOW CREATE TABLE delta_test\").collect()(0).getString(0)\n      }\n      assert(e.message.contains(\"`SHOW CREATE TABLE` is not supported for Delta table\"))\n    }\n\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        val path = dir.getCanonicalPath()\n        sql(\n          s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta\n             |LOCATION '$path'\n             \"\"\".stripMargin)\n\n        val e = intercept[AnalysisException] {\n          sql(\"SHOW CREATE TABLE delta_test\").collect()(0).getString(0)\n        }\n        assert(e.message.contains(\"`SHOW CREATE TABLE` is not supported for Delta table\"))\n      }\n    }\n  }\n\n\n  test(\"DESCRIBE TABLE for partitioned table\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        val path = dir.getCanonicalPath()\n\n        val df = Seq(\n          (1, \"IT\", \"Alice\"),\n          (2, \"CS\", \"Bob\"),\n          (3, \"IT\", \"Carol\")).toDF(\"id\", \"dept\", \"name\")\n        df.write.format(\"delta\").partitionBy(\"name\", \"dept\").save(path)\n\n        sql(s\"CREATE TABLE delta_test USING delta LOCATION '$path'\")\n\n        verifyDescribeTable(\"delta_test\")\n        verifyDescribeTable(s\"delta.`$path`\")\n\n        assert(sql(\"DESCRIBE EXTENDED delta_test\").collect().length > 0)\n      }\n    }\n  }\n\n  test(\"snapshot returned after a dropped managed table should be empty\") {\n    withTable(\"delta_test\") {\n      sql(\"CREATE TABLE delta_test USING delta AS SELECT 'foo' as a\")\n      val tableLocation = sql(\"DESC DETAIL delta_test\").select(\"location\").as[String].head()\n      val snapshotBefore = getDeltaLog(tableLocation).update()\n      sql(\"DROP TABLE delta_test\")\n      val snapshotAfter = getDeltaLog(tableLocation).update()\n      assert(snapshotBefore ne snapshotAfter)\n      assert(snapshotAfter.version === -1)\n    }\n  }\n\n  test(\"snapshot returned after renaming a managed table should be empty\") {\n    val oldTableName = \"oldTableName\"\n    val newTableName = \"newTableName\"\n    withTable(oldTableName, newTableName) {\n      sql(s\"CREATE TABLE $oldTableName USING delta AS SELECT 'foo' as a\")\n      val tableLocation = sql(s\"DESC DETAIL $oldTableName\").select(\"location\").as[String].head()\n      val snapshotBefore = getDeltaLog(tableLocation).update()\n      sql(s\"ALTER TABLE $oldTableName RENAME TO $newTableName\")\n      val snapshotAfter = getDeltaLog(tableLocation).update()\n      assert(snapshotBefore ne snapshotAfter)\n      assert(snapshotAfter.version === -1)\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaDDLUsingPathSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.Tag\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, Dataset, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.connector.catalog.{CatalogManager, CatalogV2Util, TableCatalog}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\n\ntrait DeltaDDLUsingPathTests extends QueryTest\n    with SharedSparkSession with DeltaColumnMappingTestUtils {\n\n  import testImplicits._\n\n  protected def catalogName: String = {\n    CatalogManager.SESSION_CATALOG_NAME\n  }\n\n  protected def testUsingPath(command: String, tags: Tag*)(f: (String, String) => Unit): Unit = {\n    test(s\"$command - using path\", tags: _*) {\n      withTempDir { tempDir =>\n        withTable(\"delta_test\") {\n          val path = tempDir.getCanonicalPath\n          Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n            .withColumn(\"struct\",\n              struct((col(\"v1\") * 10).as(\"x\"), concat(col(\"v2\"), col(\"v2\")).as(\"y\")))\n            .write\n            .format(\"delta\")\n            .partitionBy(\"v1\")\n            .option(\"path\", path)\n            .saveAsTable(\"delta_test\")\n          f(\"`delta_test`\", path)\n        }\n      }\n    }\n    test(s\"$command - using path in 'delta' database\", tags: _*) {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n\n        withDatabase(\"delta\") {\n          sql(\"CREATE DATABASE delta\")\n\n          withTable(\"delta.delta_test\") {\n            Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n              .withColumn(\"struct\",\n                struct((col(\"v1\") * 10).as(\"x\"), concat(col(\"v2\"), col(\"v2\")).as(\"y\")))\n              .write\n              .format(\"delta\")\n              .partitionBy(\"v1\")\n              .option(\"path\", path)\n              .saveAsTable(\"delta.delta_test\")\n            f(\"`delta`.`delta_test`\", path)\n          }\n        }\n      }\n    }\n  }\n\n  protected def toQualifiedPath(path: String): String = {\n    val hadoopPath = new Path(path)\n    // scalastyle:off deltahadoopconfiguration\n    val fs = hadoopPath.getFileSystem(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    fs.makeQualified(hadoopPath).toString\n  }\n\n  protected def checkDescribe(describe: String, keyvalues: (String, String)*): Unit = {\n    val result = sql(describe).collect()\n    keyvalues.foreach { case (key, value) =>\n      val row = result.find(_.getString(0) == key)\n      assert(row.isDefined)\n      if (key == \"Location\") {\n        assert(toQualifiedPath(row.get.getString(1)) === toQualifiedPath(value))\n      } else {\n        assert(row.get.getString(1) === value)\n      }\n    }\n  }\n\n  private def errorContains(errMsg: String, str: String): Unit = {\n    assert(errMsg.contains(str))\n  }\n\n  testUsingPath(\"SELECT\") { (table, path) =>\n    Seq(table, s\"delta.`$path`\").foreach { tableOrPath =>\n      checkDatasetUnorderly(\n        sql(s\"SELECT * FROM $tableOrPath\").as[(Int, String, (Int, String))],\n        (1, \"a\", (10, \"aa\")), (2, \"b\", (20, \"bb\")))\n      checkDatasetUnorderly(\n        spark.table(tableOrPath).as[(Int, String, (Int, String))],\n        (1, \"a\", (10, \"aa\")), (2, \"b\", (20, \"bb\")))\n    }\n\n    val ex = intercept[AnalysisException] {\n      spark.table(s\"delta.`/path/to/delta`\")\n    }\n    assert(ex.getMessage.matches(\n      \".*Path does not exist: (file:)?/path/to/delta.?.*\"),\n      \"Found: \" + ex.getMessage)\n\n    withSQLConf(SQLConf.RUN_SQL_ON_FILES.key -> \"false\") {\n      val ex = intercept[AnalysisException] {\n        spark.table(s\"delta.`/path/to/delta`\")\n      }\n      assert(ex.getMessage.contains(\"Table or view not found: delta.`/path/to/delta`\") ||\n        ex.getMessage.contains(\"table or view `delta`.`/path/to/delta` cannot be found\"))\n    }\n  }\n\n  testUsingPath(\"DESCRIBE TABLE\") { (table, path) =>\n    val qualifiedPath = toQualifiedPath(path)\n\n    Seq(table, s\"delta.`$path`\").foreach { tableOrPath =>\n      checkDescribe(s\"DESCRIBE $tableOrPath\",\n        \"v1\" -> \"int\",\n        \"v2\" -> \"string\",\n        \"struct\" -> \"struct<x:int,y:string>\")\n\n      checkDescribe(s\"DESCRIBE EXTENDED $tableOrPath\",\n        \"v1\" -> \"int\",\n        \"v2\" -> \"string\",\n        \"struct\" -> \"struct<x:int,y:string>\",\n        \"Provider\" -> \"delta\",\n        \"Location\" -> qualifiedPath)\n    }\n  }\n\n  testUsingPath(\"SHOW TBLPROPERTIES\") { (table, path) =>\n    sql(s\"ALTER TABLE $table SET TBLPROPERTIES \" +\n      \"('delta.logRetentionDuration' = '2 weeks', 'key' = 'value')\")\n\n    val metadata = loadDeltaLog(path).snapshot.metadata\n\n    Seq(table, s\"delta.`$path`\").foreach { tableOrPath =>\n      checkDatasetUnorderly(\n        dropColumnMappingConfigurations(\n          sql(s\"SHOW TBLPROPERTIES $tableOrPath('delta.logRetentionDuration')\")\n            .as[(String, String)]),\n        \"delta.logRetentionDuration\" -> \"2 weeks\")\n      checkDatasetUnorderly(\n        dropColumnMappingConfigurations(\n          sql(s\"SHOW TBLPROPERTIES $tableOrPath('key')\").as[(String, String)]),\n        \"key\" -> \"value\")\n    }\n\n    val protocol = Protocol.forNewTable(spark, Some(metadata))\n    val supportedFeatures = protocol\n      .readerAndWriterFeatureNames\n      .map(name => s\"delta.feature.$name\" -> \"supported\")\n    val expectedProperties = Seq(\n      \"delta.logRetentionDuration\" -> \"2 weeks\",\n      \"delta.minReaderVersion\" -> protocol.minReaderVersion.toString,\n      \"delta.minWriterVersion\" -> protocol.minWriterVersion.toString,\n      \"key\" -> \"value\") ++ supportedFeatures\n\n    checkDatasetUnorderly(\n      dropColumnMappingConfigurations(\n        sql(s\"SHOW TBLPROPERTIES $table\").as[(String, String)]),\n      expectedProperties: _*)\n\n    checkDatasetUnorderly(\n      dropColumnMappingConfigurations(\n        sql(s\"SHOW TBLPROPERTIES delta.`$path`\").as[(String, String)]),\n      expectedProperties: _*)\n\n    if (table == \"`delta_test`\") {\n      val tableName = s\"$catalogName.default.delta_test\"\n      checkDatasetUnorderly(\n        dropColumnMappingConfigurations(\n          sql(s\"SHOW TBLPROPERTIES $table('dEltA.lOgrEteNtiOndURaTion')\").as[(String, String)]),\n        \"dEltA.lOgrEteNtiOndURaTion\" ->\n          s\"Table $tableName does not have property: dEltA.lOgrEteNtiOndURaTion\")\n      checkDatasetUnorderly(\n        dropColumnMappingConfigurations(\n          sql(s\"SHOW TBLPROPERTIES $table('kEy')\").as[(String, String)]),\n        \"kEy\" -> s\"Table $tableName does not have property: kEy\")\n    } else {\n      checkDatasetUnorderly(\n        dropColumnMappingConfigurations(\n          sql(s\"SHOW TBLPROPERTIES $table('kEy')\").as[(String, String)]),\n        \"kEy\" -> s\"Table $catalogName.delta.delta_test does not have property: kEy\")\n    }\n    checkDatasetUnorderly(\n      dropColumnMappingConfigurations(\n        sql(s\"SHOW TBLPROPERTIES delta.`$path`('dEltA.lOgrEteNtiOndURaTion')\")\n          .as[(String, String)]),\n      \"dEltA.lOgrEteNtiOndURaTion\" ->\n        s\"Table $catalogName.delta.`$path` does not have property: dEltA.lOgrEteNtiOndURaTion\")\n    checkDatasetUnorderly(\n      dropColumnMappingConfigurations(\n        sql(s\"SHOW TBLPROPERTIES delta.`$path`('kEy')\").as[(String, String)]),\n      \"kEy\" ->\n        s\"Table $catalogName.delta.`$path` does not have property: kEy\")\n\n    val e = intercept[AnalysisException] {\n      sql(s\"SHOW TBLPROPERTIES delta.`/path/to/delta`\").as[(String, String)]\n    }\n    assert(e.getMessage.contains(s\"not a Delta table\"))\n  }\n\n  testUsingPath(\"SHOW COLUMNS\") { (table, path) =>\n    Seq(table, s\"delta.`$path`\").foreach { tableOrPath =>\n      checkDatasetUnorderly(\n        sql(s\"SHOW COLUMNS IN $tableOrPath\").as[String],\n        \"v1\", \"v2\", \"struct\")\n    }\n    if (table == \"`delta_test`\") {\n      checkDatasetUnorderly(\n        sql(s\"SHOW COLUMNS IN $table\").as[String],\n        \"v1\", \"v2\", \"struct\")\n    } else {\n      checkDatasetUnorderly(\n        sql(s\"SHOW COLUMNS IN $table IN delta\").as[String],\n        \"v1\", \"v2\", \"struct\")\n    }\n    checkDatasetUnorderly(\n      sql(s\"SHOW COLUMNS IN `$path` IN delta\").as[String],\n      \"v1\", \"v2\", \"struct\")\n    checkDatasetUnorderly(\n      sql(s\"SHOW COLUMNS IN delta.`$path` IN delta\").as[String],\n      \"v1\", \"v2\", \"struct\")\n    val e = intercept[AnalysisException] {\n      sql(\"SHOW COLUMNS IN delta.`/path/to/delta`\")\n    }\n    assert(e.getMessage.contains(s\"not a Delta table\"))\n  }\n\n  testUsingPath(\"DESCRIBE COLUMN\") { (table, path) =>\n    Seq(table, s\"delta.`$path`\").foreach { tableOrPath =>\n      checkDatasetUnorderly(\n        sql(s\"DESCRIBE $tableOrPath v1\").as[(String, String)],\n        \"col_name\" -> \"v1\",\n        \"data_type\" -> \"int\",\n        \"comment\" -> \"NULL\")\n      checkDatasetUnorderly(\n        sql(s\"DESCRIBE $tableOrPath struct\").as[(String, String)],\n        \"col_name\" -> \"struct\",\n        \"data_type\" -> \"struct<x:int,y:string>\",\n        \"comment\" -> \"NULL\")\n      checkDatasetUnorderly(\n        sql(s\"DESCRIBE EXTENDED $tableOrPath v1\").as[(String, String)],\n        \"col_name\" -> \"v1\",\n        \"data_type\" -> \"int\",\n        \"comment\" -> \"NULL\"\n      )\n      val ex1 = intercept[AnalysisException] {\n        sql(s\"DESCRIBE $tableOrPath unknown\")\n      }\n      assert(ex1.getErrorClass() === \"UNRESOLVED_COLUMN.WITH_SUGGESTION\")\n      val ex2 = intercept[AnalysisException] {\n        sql(s\"DESCRIBE $tableOrPath struct.x\")\n      }\n      assert(ex2.getMessage.contains(\"DESC TABLE COLUMN does not support nested column: struct.x\"))\n    }\n    val ex = intercept[AnalysisException] {\n      sql(\"DESCRIBE delta.`/path/to/delta` v1\")\n    }\n    assert(ex.getMessage.contains(\"not a Delta table\"), s\"Original message: ${ex.getMessage()}\")\n  }\n}\n\nclass DeltaDDLUsingPathSuite extends DeltaDDLUsingPathTests with DeltaSQLCommandTest {\n}\n\n\nclass DeltaDDLUsingPathNameColumnMappingSuite extends DeltaDDLUsingPathSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  override protected def runOnlyTests = Seq(\n    \"create table with NOT NULL - check violation through file writing\",\n    \"ALTER TABLE CHANGE COLUMN with nullability change in struct type - relaxed\"\n  )\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaDataFrameHadoopOptionsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LocalLogStore\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaDataFrameHadoopOptionsSuite extends QueryTest\n  with DeltaSQLTestUtils\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  protected override def sparkConf =\n    super.sparkConf.set(\"spark.delta.logStore.fake.impl\", classOf[LocalLogStore].getName)\n\n  /**\n   * Create Hadoop file system options for `FakeFileSystem`. If Delta doesn't pick up them,\n   * it won't be able to read/write any files using `fake://`.\n   */\n  private def fakeFileSystemOptions: Map[String, String] = {\n    Map(\n      \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n      \"fs.fake.impl.disable.cache\" -> \"true\"\n    )\n  }\n\n  /** Create a fake file system path to test from the dir path. */\n  private def fakeFileSystemPath(dir: File): String = s\"fake://${dir.getCanonicalPath}\"\n\n  /** Clear cache to make sure we don't reuse the cached snapshot */\n  private def clearCachedDeltaLogToForceReload(): Unit = {\n    DeltaLog.clearCache()\n  }\n\n  // read/write parquet format check cache\n  test(\"SC-86916: \" +\n      \"read/write Delta paths using DataFrame should pick up Hadoop file system options\") {\n    withTempPath { dir =>\n      val path = fakeFileSystemPath(dir)\n      spark.range(1, 10)\n        .write\n        .format(\"delta\")\n        .options(fakeFileSystemOptions)\n        .save(path)\n      clearCachedDeltaLogToForceReload()\n      spark.read.format(\"delta\").options(fakeFileSystemOptions).load(path).foreach(_ => {})\n      // Test time travel\n      clearCachedDeltaLogToForceReload()\n      spark.read.format(\"delta\").options(fakeFileSystemOptions).load(path + \"@v0\").foreach(_ => {})\n      clearCachedDeltaLogToForceReload()\n      spark.read.format(\"delta\").options(fakeFileSystemOptions).option(\"versionAsOf\", 0)\n        .load(path).foreach(_ => {})\n\n    }\n  }\n\n  testQuietly(\"SC-86916: disabling the conf should not pick up Hadoop file system options\") {\n    withSQLConf(DeltaSQLConf.LOAD_FILE_SYSTEM_CONFIGS_FROM_DATAFRAME_OPTIONS.key -> \"false\") {\n      withTempPath { dir =>\n        val path = fakeFileSystemPath(dir)\n        intercept[Exception] {\n          spark.read.format(\"delta\").options(fakeFileSystemOptions).load(path)\n        }\n      }\n    }\n  }\n\n  test(\"SC-86916: checkpoint should pick up Hadoop file system options\") {\n    withSQLConf(DeltaConfigs.CHECKPOINT_INTERVAL.defaultTablePropertyKey -> \"1\") {\n      withTempPath { dir =>\n        val path = fakeFileSystemPath(dir)\n        spark.range(1, 10).write.format(\"delta\")\n          .options(fakeFileSystemOptions)\n          .mode(\"append\")\n          .save(path)\n        spark.range(1, 10).write.format(\"delta\")\n          .options(fakeFileSystemOptions)\n          .mode(\"append\")\n          .save(path)\n        // Ensure we did write the checkpoint and read it back\n        val deltaLog = DeltaLog.forTable(spark, new Path(path), fakeFileSystemOptions)\n        assert(deltaLog.readLastCheckpointFile().get.version == 1)\n      }\n    }\n  }\n\n  test(\"SC-86916: invalidateCache should invalidate all DeltaLogs of the given path\") {\n    withTempPath { dir =>\n      val pathStr = fakeFileSystemPath(dir)\n      val path = new Path(pathStr)\n      spark.range(1, 10).write.format(\"delta\")\n        .options(fakeFileSystemOptions)\n        .mode(\"append\")\n        .save(pathStr)\n      val deltaLog = DeltaLog.forTable(spark, path, fakeFileSystemOptions)\n      spark.range(1, 10).write.format(\"delta\")\n        .options(fakeFileSystemOptions)\n        .mode(\"append\")\n        .save(pathStr)\n      val cachedDeltaLog = DeltaLog.forTable(spark, path, fakeFileSystemOptions)\n      assert(deltaLog eq cachedDeltaLog)\n      withSQLConf(fakeFileSystemOptions.toSeq: _*) {\n        DeltaLog.invalidateCache(spark, path)\n      }\n      spark.range(1, 10).write.format(\"delta\")\n        .options(fakeFileSystemOptions)\n        .mode(\"append\")\n        .save(pathStr)\n      val newDeltaLog = DeltaLog.forTable(spark, path, fakeFileSystemOptions)\n      assert(deltaLog ne newDeltaLog)\n    }\n  }\n\n  test(\"SC-86916: Delta log cache should respect options\") {\n    withTempPath { dir =>\n      val path = fakeFileSystemPath(dir)\n      DeltaLog.clearCache()\n      spark.range(1, 10).write.format(\"delta\")\n        .options(fakeFileSystemOptions)\n        .mode(\"append\")\n        .save(path)\n      assert(DeltaLog.cacheSize == 1)\n\n      // Accessing the same table should not create a new entry in the cache\n      spark.read.format(\"delta\").options(fakeFileSystemOptions).load(path).foreach(_ => {})\n      assert(DeltaLog.cacheSize == 1)\n\n      // Accessing the table with different options should create a new entry\n      spark.read.format(\"delta\")\n        .options(fakeFileSystemOptions ++ Map(\"fs.foo\" -> \"foo\")).load(path).foreach(_ => {})\n      assert(DeltaLog.cacheSize == 2)\n\n      // Accessing the table without options should create a new entry\n      withSQLConf(fakeFileSystemOptions.toSeq: _*) {\n        spark.read.format(\"delta\").load(path).foreach(_ => {})\n      }\n      assert(DeltaLog.cacheSize == 3)\n\n      // Make sure we don't break existing cache logic\n      DeltaLog.clearCache()\n      withSQLConf(fakeFileSystemOptions.toSeq: _*) {\n        spark.read.format(\"delta\").load(path).foreach(_ => {})\n        spark.read.format(\"delta\").load(path).foreach(_ => {})\n      }\n      assert(DeltaLog.cacheSize == 1)\n    }\n  }\n\n  /**\n   * Clears the DeltaLog cache, runs the operation, then verifies that\n   * the resulting DeltaLog carries the expected fs.* options internally.\n   */\n  private def withOptionsPropagationCheck(path: String, desc: String)(op: => Unit): Unit = {\n    withClue(s\"$desc: \") {\n      clearCachedDeltaLogToForceReload()\n      op\n      val deltaLog = DeltaLog.forTable(spark, new Path(path), fakeFileSystemOptions)\n      assert(\n        deltaLog.options(\"fs.fake.impl\") == classOf[FakeFileSystem].getName,\n        \"fs.fake.impl was not propagated to DeltaLog.options\")\n      assert(\n        deltaLog.newDeltaHadoopConf().get(\"fs.fake.impl\") == classOf[FakeFileSystem].getName,\n        \"fs.fake.impl was not propagated to Hadoop configuration\")\n    }\n  }\n\n  test(\"all operations should propagate Hadoop file system options\") {\n    withTempPaths(/* numPaths = */ 2) { case Seq(inputDir, checkpointDir) =>\n      val path = fakeFileSystemPath(inputDir)\n\n      // Seed the table\n      spark.range(2).write.format(\"delta\")\n        .options(fakeFileSystemOptions).save(path)\n\n      withOptionsPropagationCheck(path, \"batch write (overwrite)\") {\n        spark.range(3).write.format(\"delta\")\n          .options(fakeFileSystemOptions).mode(\"overwrite\").save(path)\n      }\n\n      withOptionsPropagationCheck(path, \"batch read\") {\n        assert(spark.read.format(\"delta\")\n          .options(fakeFileSystemOptions).load(path).count() == 3)\n      }\n\n      withOptionsPropagationCheck(path, \"batch append\") {\n        spark.range(1).write.format(\"delta\")\n          .options(fakeFileSystemOptions).mode(\"append\").save(path)\n      }\n\n      withOptionsPropagationCheck(path, \"streaming read\") {\n        val query = spark.readStream.format(\"delta\")\n          .options(fakeFileSystemOptions)\n          .load(path)\n          .writeStream\n          .format(\"memory\")\n          .queryName(\"options_propagation_test\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .start()\n        try {\n          query.processAllAvailable()\n          assert(spark.table(\"options_propagation_test\").count() == 4)\n        } finally {\n          query.stop()\n        }\n      }\n    }\n  }\n\n  testQuietly(\"operations without Hadoop options should fail for fake:// filesystem\") {\n    withTempPaths(/* numPaths = */ 2) { case Seq(inputDir, checkpointDir) =>\n      val path = fakeFileSystemPath(inputDir)\n\n      // Write data with options so the Delta table physically exists.\n      spark.range(2).write.format(\"delta\")\n        .options(fakeFileSystemOptions).save(path)\n      clearCachedDeltaLogToForceReload()\n\n      // Batch read without options should fail\n      val batchEx = intercept[Exception] {\n        spark.read.format(\"delta\").load(path).foreach(_ => {})\n      }\n      assert(batchEx.getMessage.contains(\"\"\"No FileSystem for scheme \"fake\"\"\"\"))\n\n      clearCachedDeltaLogToForceReload()\n\n      // Streaming read without options should fail\n      val streamEx = intercept[Exception] {\n        spark.readStream.format(\"delta\")\n          .load(path)\n          .writeStream\n          .format(\"memory\")\n          .queryName(\"options_failure_test\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .start()\n          .processAllAvailable()\n      }\n      assert(streamEx.getMessage.contains(\"\"\"No FileSystem for scheme \"fake\"\"\"\"))\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaDataFrameWriterV2Suite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.actions.{Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.{DeltaCatalog, DeltaTableV2}\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.scalatest.BeforeAndAfter\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, CreateTableWriter, Dataset, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.analysis.{CannotReplaceMissingTableException, TableAlreadyExistsException}\nimport org.apache.spark.sql.connector.catalog.{CatalogManager, CatalogV2Util, Identifier, Table, TableCatalog}\nimport org.apache.spark.sql.connector.expressions._\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{LongType, StringType, StructType}\nimport org.apache.spark.util.Utils\n\n// These tests are copied from Apache Spark (minus partition by expressions) and should work exactly\n// the same with Delta minus some writer options\ntrait OpenSourceDataFrameWriterV2Tests\n    extends QueryTest\n    with SharedSparkSession\n    with BeforeAndAfter {\n\n  import testImplicits._\n\n  before {\n    val df = spark.createDataFrame(Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\"))).toDF(\"id\", \"data\")\n    df.createOrReplaceTempView(\"source\")\n    val df2 = spark.createDataFrame(Seq((4L, \"d\"), (5L, \"e\"), (6L, \"f\"))).toDF(\"id\", \"data\")\n    df2.createOrReplaceTempView(\"source2\")\n  }\n\n  after {\n    spark.sessionState.catalog.listTables(\"default\").foreach { ti =>\n      spark.sessionState.catalog.dropTable(ti, ignoreIfNotExists = false, purge = false)\n    }\n  }\n\n  def catalog: TableCatalog = {\n    spark.sessionState.catalogManager.currentCatalog.asInstanceOf[TableCatalog]\n  }\n\n  protected def catalogPrefix: String = {\n    s\"${CatalogManager.SESSION_CATALOG_NAME}.\"\n  }\n\n  protected def getProperties(table: Table): Map[String, String] = {\n    table.properties().asScala.toMap\n      .filterKeys(!CatalogV2Util.TABLE_RESERVED_PROPERTIES.contains(_))\n      .filterKeys(!TableFeatureProtocolUtils.isTableProtocolProperty(_))\n      .toMap\n  }\n\n  test(\"Append: basic append\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    spark.table(\"source\").writeTo(\"table_name\").append()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    spark.table(\"source2\").writeTo(\"table_name\").append()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\"), Row(4L, \"d\"), Row(5L, \"e\"), Row(6L, \"f\")))\n  }\n\n  test(\"Append: by name not position\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    val exc = intercept[AnalysisException] {\n      spark.table(\"source\").withColumnRenamed(\"data\", \"d\").writeTo(\"table_name\").append()\n    }\n\n    assert(exc.getMessage.contains(\"schema mismatch\"))\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq())\n  }\n\n  test(\"Append: fail if table does not exist\") {\n    val exc = intercept[AnalysisException] {\n      spark.table(\"source\").writeTo(\"table_name\").append()\n    }\n\n    assert(exc.getMessage.contains(\"table_name\"))\n  }\n\n  test(\"Overwrite: overwrite by expression: true\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    spark.table(\"source\").writeTo(\"table_name\").append()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    spark.table(\"source2\").writeTo(\"table_name\").overwrite(lit(true))\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(4L, \"d\"), Row(5L, \"e\"), Row(6L, \"f\")))\n  }\n\n  test(\"Overwrite: overwrite by expression: id = 3\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    spark.table(\"source\").writeTo(\"table_name\").append()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val e = intercept[AnalysisException] {\n      spark.table(\"source2\").writeTo(\"table_name\").overwrite($\"id\" === 3)\n    }\n    assert(e.getErrorClass == \"DELTA_REPLACE_WHERE_MISMATCH\")\n    assert(e.getMessage.startsWith(\n      \"[DELTA_REPLACE_WHERE_MISMATCH] Written data does not conform to partial table overwrite \" +\n        \"condition or constraint\"))\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n  }\n\n  test(\"Overwrite: by name not position\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    val exc = intercept[AnalysisException] {\n      spark.table(\"source\").withColumnRenamed(\"data\", \"d\")\n        .writeTo(\"table_name\").overwrite(lit(true))\n    }\n\n    assert(exc.getMessage.contains(\"schema mismatch\"))\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq())\n  }\n\n  test(\"Overwrite: fail if table does not exist\") {\n    val exc = intercept[AnalysisException] {\n      spark.table(\"source\").writeTo(\"table_name\").overwrite(lit(true))\n    }\n\n    assert(exc.getMessage.contains(\"table_name\"))\n  }\n\n  test(\"OverwritePartitions: overwrite conflicting partitions\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    spark.table(\"source\").writeTo(\"table_name\").append()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    spark.table(\"source2\").withColumn(\"id\", $\"id\" - 2)\n      .writeTo(\"table_name\").overwritePartitions()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"d\"), Row(3L, \"e\"), Row(4L, \"f\")))\n  }\n\n  test(\"OverwritePartitions: overwrite all rows if not partitioned\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    spark.table(\"source\").writeTo(\"table_name\").append()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    spark.table(\"source2\").writeTo(\"table_name\").overwritePartitions()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(4L, \"d\"), Row(5L, \"e\"), Row(6L, \"f\")))\n  }\n\n  test(\"OverwritePartitions: by name not position\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n\n    val e = intercept[AnalysisException] {\n      spark.table(\"source\").withColumnRenamed(\"data\", \"d\")\n        .writeTo(\"table_name\").overwritePartitions()\n    }\n\n    assert(e.getMessage.contains(\"schema mismatch\"))\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq())\n  }\n\n  test(\"OverwritePartitions: fail if table does not exist\") {\n    val exc = intercept[AnalysisException] {\n      spark.table(\"source\").writeTo(\"table_name\").overwritePartitions()\n    }\n\n    assert(exc.getMessage.contains(\"table_name\"))\n  }\n\n  test(\"Create: basic behavior\") {\n    spark.table(\"source\").writeTo(\"table_name\").using(\"delta\").create()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning.isEmpty)\n    assert(getProperties(table).isEmpty)\n  }\n\n  test(\"Create: with using\") {\n    spark.table(\"source\").writeTo(\"table_name\").using(\"delta\").create()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning.isEmpty)\n    assert(getProperties(table).isEmpty)\n  }\n\n  test(\"Create: with property\") {\n    spark.table(\"source\").writeTo(\"table_name\")\n      .tableProperty(\"prop\", \"value\").using(\"delta\").create()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning.isEmpty)\n    assert(getProperties(table) === Map(\"prop\" -> \"value\"))\n  }\n\n  test(\"Create: identity partitioned table\") {\n    spark.table(\"source\").writeTo(\"table_name\").using(\"delta\").partitionedBy($\"id\").create()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(table).isEmpty)\n  }\n\n  test(\"Create: fail if table already exists\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n\n    val exc = intercept[TableAlreadyExistsException] {\n      spark.table(\"source\").writeTo(\"table_name\").using(\"delta\").create()\n    }\n\n    assert(exc.getMessage.contains(\"table_name\"))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // table should not have been changed\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(table).isEmpty)\n  }\n\n  test(\"Replace: basic behavior\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n    spark.sql(\"INSERT INTO TABLE table_name SELECT * FROM source\")\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the initial table\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(table).isEmpty)\n\n    spark.table(\"source2\")\n      .withColumn(\"even_or_odd\", when(($\"id\" % 2) === 0, \"even\").otherwise(\"odd\"))\n      .writeTo(\"table_name\").using(\"delta\")\n      .tableProperty(\"deLta.aPpeNdonly\", \"true\").replace()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(4L, \"d\", \"even\"), Row(5L, \"e\", \"odd\"), Row(6L, \"f\", \"even\")))\n\n    val replaced = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the replacement table\n    assert(replaced.name === s\"${catalogPrefix}default.table_name\")\n    assert(replaced.schema === new StructType()\n      .add(\"id\", LongType)\n      .add(\"data\", StringType)\n      .add(\"even_or_odd\", StringType))\n    assert(replaced.partitioning.isEmpty)\n    assert(getProperties(replaced) === Map(\"delta.appendOnly\" -> \"true\"))\n  }\n\n  test(\"Replace: partitioned table\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n    spark.sql(\"INSERT INTO TABLE table_name SELECT * FROM source\")\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the initial table\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning.isEmpty)\n    assert(getProperties(table).isEmpty)\n\n    spark.table(\"source2\")\n      .withColumn(\"even_or_odd\", when(($\"id\" % 2) === 0, \"even\").otherwise(\"odd\"))\n      .writeTo(\"table_name\").using(\"delta\")\n      .partitionedBy($\"id\")\n      .replace()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(4L, \"d\", \"even\"), Row(5L, \"e\", \"odd\"), Row(6L, \"f\", \"even\")))\n\n    val replaced = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the replacement table\n    assert(replaced.name === s\"${catalogPrefix}default.table_name\")\n    assert(replaced.schema === new StructType()\n      .add(\"id\", LongType)\n      .add(\"data\", StringType)\n      .add(\"even_or_odd\", StringType))\n    assert(replaced.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(replaced).isEmpty)\n  }\n\n  test(\"Replace: fail if table does not exist\") {\n    val exc = intercept[AnalysisException] {\n      spark.table(\"source\").writeTo(\"table_name\").using(\"delta\").replace()\n    }\n\n    checkError(exc, \"TABLE_OR_VIEW_NOT_FOUND\", Some(\"42P01\"),\n      Map(\"relationName\" -> \"`default`.`table_name`\"))\n  }\n\n  test(\"CreateOrReplace: table does not exist\") {\n    spark.table(\"source2\").writeTo(\"table_name\").using(\"delta\").createOrReplace()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(4L, \"d\"), Row(5L, \"e\"), Row(6L, \"f\")))\n\n    val replaced = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the replacement table\n    assert(replaced.name === s\"${catalogPrefix}default.table_name\")\n    assert(replaced.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(replaced.partitioning.isEmpty)\n    assert(getProperties(replaced).isEmpty)\n  }\n\n  test(\"CreateOrReplace: table exists\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n    spark.sql(\"INSERT INTO TABLE table_name SELECT * FROM source\")\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the initial table\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(table).isEmpty)\n\n    spark.table(\"source2\")\n      .withColumn(\"even_or_odd\", when(($\"id\" % 2) === 0, \"even\").otherwise(\"odd\"))\n      .writeTo(\"table_name\").using(\"delta\").createOrReplace()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(4L, \"d\", \"even\"), Row(5L, \"e\", \"odd\"), Row(6L, \"f\", \"even\")))\n\n    val replaced = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the replacement table\n    assert(replaced.name === s\"${catalogPrefix}default.table_name\")\n    assert(replaced.schema === new StructType()\n      .add(\"id\", LongType)\n      .add(\"data\", StringType)\n      .add(\"even_or_odd\", StringType))\n    assert(replaced.partitioning.isEmpty)\n    assert(getProperties(replaced).isEmpty)\n  }\n\n  test(\"Create: partitioned by years(ts) - not supported\") {\n    val e = intercept[AnalysisException] {\n      spark.table(\"source\")\n        .withColumn(\"ts\", lit(\"2019-06-01 10:00:00.000000\").cast(\"timestamp\"))\n        .writeTo(\"table_name\")\n        .partitionedBy(years($\"ts\"))\n        .using(\"delta\")\n        .create()\n    }\n    assert(e.getMessage.contains(\"Partitioning by expressions\"))\n  }\n\n  test(\"Create: partitioned by months(ts) - not supported\") {\n    val e = intercept[AnalysisException] {\n      spark.table(\"source\")\n        .withColumn(\"ts\", lit(\"2019-06-01 10:00:00.000000\").cast(\"timestamp\"))\n        .writeTo(\"table_name\")\n        .partitionedBy(months($\"ts\"))\n        .using(\"delta\")\n        .create()\n    }\n    assert(e.getMessage.contains(\"Partitioning by expressions\"))\n  }\n\n  test(\"Create: partitioned by days(ts) - not supported\") {\n    val e = intercept[AnalysisException] {\n      spark.table(\"source\")\n        .withColumn(\"ts\", lit(\"2019-06-01 10:00:00.000000\").cast(\"timestamp\"))\n        .writeTo(\"table_name\")\n        .partitionedBy(days($\"ts\"))\n        .using(\"delta\")\n        .create()\n    }\n    assert(e.getMessage.contains(\"Partitioning by expressions\"))\n  }\n\n  test(\"Create: partitioned by hours(ts) - not supported\") {\n    val e = intercept[AnalysisException] {\n      spark.table(\"source\")\n        .withColumn(\"ts\", lit(\"2019-06-01 10:00:00.000000\").cast(\"timestamp\"))\n        .writeTo(\"table_name\")\n        .partitionedBy(hours($\"ts\"))\n        .using(\"delta\")\n        .create()\n    }\n    assert(e.getMessage.contains(\"Partitioning by expressions\"))\n  }\n\n  test(\"Create: partitioned by bucket(4, id) - not supported\") {\n    val e = intercept[AnalysisException] {\n      spark.table(\"source\")\n        .writeTo(\"table_name\")\n        .partitionedBy(bucket(4, $\"id\"))\n        .using(\"delta\")\n        .create()\n    }\n    assert(e.getMessage.contains(\"is not supported for Delta tables\"))\n  }\n}\n\nclass DeltaDataFrameWriterV2Suite\n  extends OpenSourceDataFrameWriterV2Tests\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  test(\"Append: basic append by path\") {\n    spark.sql(\"CREATE TABLE table_name (id bigint, data string) USING delta\")\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty)\n    val location = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n      .asInstanceOf[DeltaTableV2].path\n\n    spark.table(\"source\").writeTo(s\"delta.`$location`\").append()\n\n    checkAnswer(\n      spark.table(s\"delta.`$location`\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    // allows missing columns\n    Seq(4L).toDF(\"id\").writeTo(s\"delta.`$location`\").append()\n    checkAnswer(\n      spark.table(s\"delta.`$location`\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\"), Row(4L, null)))\n  }\n\n  test(\"Create: basic behavior by path\") {\n    withTempDir { tempDir =>\n      val dir = tempDir.getCanonicalPath\n      spark.table(\"source\").writeTo(s\"delta.`$dir`\").using(\"delta\").create()\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir),\n        Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n      val table = catalog.loadTable(Identifier.of(Array(\"delta\"), dir))\n\n      assert(table.name === s\"delta.`file:$dir`\")\n      assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n      assert(table.partitioning.isEmpty)\n      assert(getProperties(table).isEmpty)\n    }\n  }\n\n  test(\"Create: using empty dataframe\") {\n    spark.table(\"source\").where(\"false\")\n      .writeTo(\"table_name\").using(\"delta\")\n      .tableProperty(\"delta.appendOnly\", \"true\")\n      .partitionedBy($\"id\").create()\n\n    checkAnswer(spark.table(\"table_name\"), Seq.empty[Row])\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(table) === Map(\"delta.appendOnly\" -> \"true\"))\n  }\n\n  test(\"Replace: basic behavior using empty df\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n    spark.sql(\"INSERT INTO TABLE table_name SELECT * FROM source\")\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    val table = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the initial table\n    assert(table.name === s\"${catalogPrefix}default.table_name\")\n    assert(table.schema === new StructType().add(\"id\", LongType).add(\"data\", StringType))\n    assert(table.partitioning === Seq(IdentityTransform(FieldReference(\"id\"))))\n    assert(getProperties(table).isEmpty)\n\n    spark.table(\"source2\").where(\"false\")\n      .withColumn(\"even_or_odd\", when(($\"id\" % 2) === 0, \"even\").otherwise(\"odd\"))\n      .writeTo(\"table_name\").using(\"delta\")\n      .tableProperty(\"deLta.aPpeNdonly\", \"true\").replace()\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq.empty[Row])\n\n    val replaced = catalog.loadTable(Identifier.of(Array(\"default\"), \"table_name\"))\n\n    // validate the replacement table\n    assert(replaced.name === s\"${catalogPrefix}default.table_name\")\n    assert(replaced.schema === new StructType()\n        .add(\"id\", LongType)\n        .add(\"data\", StringType)\n        .add(\"even_or_odd\", StringType))\n    assert(replaced.partitioning.isEmpty)\n    assert(getProperties(replaced) === Map(\"delta.appendOnly\" -> \"true\"))\n  }\n\n  test(\"throw error with createOrReplace and Replace if overwriteSchema=false\") {\n    spark.sql(\n      \"CREATE TABLE table_name (id bigint, data string) USING delta PARTITIONED BY (id)\")\n    spark.sql(\"INSERT INTO TABLE table_name SELECT * FROM source\")\n\n    checkAnswer(\n      spark.table(\"table_name\"),\n      Seq(Row(1L, \"a\"), Row(2L, \"b\"), Row(3L, \"c\")))\n\n    def checkFailure(\n        df: Dataset[_],\n        errorMsg: String)(\n        f: CreateTableWriter[_] => CreateTableWriter[_]): Unit = {\n      val e = intercept[IllegalArgumentException] {\n        val dfwV2 = df.writeTo(\"table_name\")\n          .using(\"delta\")\n          .option(\"overwriteSchema\", \"false\")\n        f(dfwV2).replace()\n      }\n      assert(e.getMessage.contains(errorMsg))\n\n      val e2 = intercept[IllegalArgumentException] {\n        val dfwV2 = df.writeTo(\"table_name\")\n            .using(\"delta\")\n            .option(\"overwriteSchema\", \"false\")\n        f(dfwV2).createOrReplace()\n      }\n      assert(e2.getMessage.contains(errorMsg))\n    }\n\n    // schema changes\n    checkFailure(\n      spark.table(\"table_name\").withColumn(\"id2\", 'id + 1),\n      \"overwriteSchema is not allowed when replacing\")(a => a.partitionedBy($\"id\"))\n\n    // partitioning changes\n    // did not specify partitioning\n    checkFailure(spark.table(\"table_name\"),\n      \"overwriteSchema is not allowed when replacing\")(a => a)\n\n    // different partitioning column\n    checkFailure(spark.table(\"table_name\"),\n      \"overwriteSchema is not allowed when replacing\")(a => a.partitionedBy($\"data\"))\n\n    // different table Properties\n    checkFailure(spark.table(\"table_name\"), \"overwriteSchema is not allowed when replacing\")(a =>\n      a.partitionedBy($\"id\").tableProperty(\"delta.appendOnly\", \"true\"))\n  }\n\n  test(\"append or overwrite mode should not do implicit casting\") {\n    val table = \"not_implicit_casting\"\n    withTable(table) {\n      spark.sql(s\"CREATE TABLE $table(id bigint, p int) USING delta PARTITIONED BY (p)\")\n      def verifyNotImplicitCasting(f: => Unit): Unit = {\n        val e = intercept[DeltaAnalysisException](f)\n        checkError(\n          e.getCause.asInstanceOf[DeltaAnalysisException],\n          \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n          parameters = Map(\"currentDataType\" -> \"LongType\", \"updateDataType\" -> \"IntegerType\"))\n      }\n      verifyNotImplicitCasting {\n        Seq(1 -> 1).toDF(\"id\", \"p\").write.mode(\"append\").format(\"delta\").saveAsTable(table)\n      }\n      verifyNotImplicitCasting {\n        Seq(1 -> 1).toDF(\"id\", \"p\").write.mode(\"overwrite\").format(\"delta\").saveAsTable(table)\n      }\n      verifyNotImplicitCasting {\n        Seq(1 -> 1).toDF(\"id\", \"p\").writeTo(table).append()\n      }\n      verifyNotImplicitCasting {\n        Seq(1 -> 1).toDF(\"id\", \"p\").writeTo(table).overwrite($\"p\" === 1)\n      }\n      verifyNotImplicitCasting {\n        Seq(1 -> 1).toDF(\"id\", \"p\").writeTo(table).overwritePartitions()\n      }\n    }\n  }\n\n  test(\"append or overwrite mode allows missing columns\") {\n    val table = \"allow_missing_columns\"\n    withTable(table) {\n      spark.sql(\n        s\"CREATE TABLE $table(col1 int, col2 int, col3 int) USING delta PARTITIONED BY (col3)\")\n\n      // append\n      Seq((0, 10)).toDF(\"col1\", \"col3\").writeTo(table).append()\n      checkAnswer(\n        spark.table(table),\n        Seq(Row(0, null, 10))\n      )\n\n      // overwrite by expression\n      Seq((1, 11)).toDF(\"col1\", \"col3\").writeTo(table).overwrite($\"col3\" === 11)\n      checkAnswer(\n        spark.table(table),\n        Seq(Row(0, null, 10), Row(1, null, 11))\n      )\n\n      // dynamic partition overwrite\n      Seq((2, 10)).toDF(\"col1\", \"col3\").writeTo(table).overwritePartitions()\n      checkAnswer(\n        spark.table(table),\n        Seq(Row(2, null, 10), Row(1, null, 11))\n      )\n    }\n\n  }\n}\n\ntrait DeltaDataFrameWriterV2ColumnMappingSuiteBase extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests = Seq(\n    \"Append: basic append\",\n    \"Create: with using\",\n    \"Overwrite: overwrite by expression: true\",\n    \"Replace: partitioned table\"\n  )\n}\n\nclass DeltaDataFrameWriterV2IdColumnMappingSuite extends DeltaDataFrameWriterV2Suite\n  with DeltaColumnMappingEnableIdMode\n  with DeltaDataFrameWriterV2ColumnMappingSuiteBase {\n\n  override protected def getProperties(table: Table): Map[String, String] = {\n    // ignore column mapping configurations\n    dropColumnMappingConfigurations(super.getProperties(table))\n  }\n\n}\n\nclass DeltaDataFrameWriterV2NameColumnMappingSuite extends DeltaDataFrameWriterV2Suite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaDataFrameWriterV2ColumnMappingSuiteBase {\n\n  override protected def getProperties(table: Table): Map[String, String] = {\n    // ignore column mapping configurations\n    dropColumnMappingConfigurations(super.getProperties(table))\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaDropColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types.{ArrayType, IntegerType, MapType, StringType, StructType}\n\nclass DeltaDropColumnSuite extends QueryTest\n  with DeltaArbitraryColumnNameSuiteBase {\n\n  override protected val sparkConf: SparkConf =\n    super.sparkConf.set(DeltaSQLConf.DELTA_ALTER_TABLE_DROP_COLUMN_ENABLED.key, \"true\")\n\n  protected def dropTest(\n      testName: String,\n      testTags: org.scalatest.Tag*)(\n      f: ((String, Seq[String]) => Unit) => Unit): Unit = {\n    test(testName, testTags: _*) {\n      def drop(table: String, columns: Seq[String]): Unit =\n        sql(s\"alter table $table drop column (${columns.mkString(\",\")})\")\n      f(drop)\n\n    }\n  }\n\n  dropTest(\"drop column disallowed with sql flag off\") { drop =>\n    withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_DROP_COLUMN_ENABLED.key -> \"false\") {\n      withTable(\"t1\") {\n        createTableWithSQLAPI(\"t1\",\n          simpleNestedData,\n          Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))\n\n        assertException(\"DROP COLUMN is not supported for your Delta table\") {\n          drop(\"t1\", \"arr\" :: Nil)\n        }\n      }\n    }\n  }\n\n  dropTest(\"drop column disallowed with no mapping mode\") { drop =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\", simpleNestedData)\n\n      assertException(\"DROP COLUMN is not supported for your Delta table\") {\n        drop(\"t1\", \"arr\" :: Nil)\n      }\n    }\n  }\n\n  dropTest(\"drop column - basic\") { drop =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))\n\n      // drop single column\n      drop(\"t1\", \"arr\" :: Nil)\n      checkAnswer(spark.table(\"t1\"), simpleNestedData.drop(\"arr\"))\n\n      // drop multiple columns\n      drop(\"t1\", \"a\" :: \"b.c\" :: Nil)\n      checkAnswer(spark.table(\"t1\"),\n        Seq(\n          Row(Row(1), Map(\"k1\" -> \"v1\")),\n          Row(Row(2), Map(\"k2\" -> \"v2\"))))\n\n      // check delta history\n      checkAnswer(\n        spark.sql(\"describe history t1\")\n          .select(\"operation\", \"operationParameters\")\n          .where(\"version = 3\"),\n        Seq(Row(\"DROP COLUMNS\", Map(\"columns\" -> \"\"\"[\"a\",\"b.c\"]\"\"\"))))\n    }\n  }\n\n  dropTest(\"drop column - basic - path based table\") { drop =>\n    withTempDir { dir =>\n      simpleNestedData.write.mode(\"overwrite\").format(\"delta\").save(dir.getCanonicalPath)\n      alterTableWithProps(s\"delta.`${dir.getCanonicalPath}`\", Map(\n          DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\",\n          DeltaConfigs.MIN_READER_VERSION.key -> \"2\",\n          DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"))\n\n      // drop single column\n      drop(s\"delta.`${dir.getCanonicalPath}`\", \"arr\" :: Nil)\n      checkAnswer(spark.read.format(\"delta\").load(dir.getCanonicalPath),\n        simpleNestedData.drop(\"arr\"))\n    }\n  }\n\n  dropTest(\"dropped columns can no longer be queried\") { drop =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))\n\n      drop(\"t1\", \"a\" :: \"b.c\" :: \"arr\" :: Nil)\n\n      // dropped column cannot be queried anymore\n      val err1 = intercept[AnalysisException] {\n        spark.table(\"t1\").where(\"a = 'str1'\").collect()\n      }.getMessage\n      assert(\n        err1.contains(\"cannot be resolved\") ||\n        err1.contains(\"Column 'a' does not exist\") ||\n        err1.contains(\"cannot resolve\"))\n\n      val err2 = intercept[AnalysisException] {\n        spark.table(\"t1\").select(\"min(a)\").collect()\n      }.getMessage\n      assert(\n        err2.contains(\"cannot be resolved\") ||\n        err2.contains(\"Column '`min(a)`' does not exist\") ||\n        err2.contains(\"cannot resolve\"))\n    }\n  }\n\n  dropTest(\"drop column - corner cases\") { drop =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))\n\n      drop(\"t1\", \"a\" :: \"b.c\" :: \"arr\" :: Nil)\n\n      // cannot drop the last nested field\n      val e = intercept[AnalysisException] {\n        drop(\"t1\", \"b.d\" :: Nil)\n      }\n      assert(e.getMessage.contains(\"Cannot drop column from a schema with a single column\"))\n\n      // can drop the parent column\n      drop(\"t1\", \"b\" :: Nil)\n\n      // cannot drop the last top-level field\n      val e2 = intercept[AnalysisException] {\n        drop(\"t1\", \"map\" :: Nil)\n      }\n      assert(e2.getMessage.contains(\"Cannot drop column from a schema with a single column\"))\n\n      spark.sql(\"alter table t1 add column (e struct<e1 string, e2 string>)\")\n\n      // can drop a column with arbitrary chars\n      spark.sql(s\"alter table t1 rename column map to `${colName(\"map\")}`\")\n      drop(\"t1\", s\"`${colName(\"map\")}`\" :: Nil)\n\n      // only column e is left now\n      assert(spark.table(\"t1\").schema.map(_.name) == Seq(\"e\"))\n\n      // can drop a nested column when the top-level column is the only column\n      drop(\"t1\", \"e.e1\" :: Nil)\n      val resultSchema = spark.table(\"t1\").schema\n      assert(resultSchema.findNestedField(\"e\" :: \"e2\" :: Nil).isDefined)\n      assert(resultSchema.findNestedField(\"e\" :: \"e1\" :: Nil).isEmpty)\n    }\n  }\n\n  dropTest(\"drop column with constraints\") { drop =>\n    withTable(\"t1\") {\n      val schemaWithNotNull =\n        simpleNestedData.schema.toDDL.replace(\"c: STRING\", \"c: STRING NOT NULL\")\n\n      withTable(\"source\") {\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1 ($schemaWithNotNull)\n             |USING DELTA\n             |${propString(Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))}\n             |\"\"\".stripMargin)\n        simpleNestedData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n      }\n\n      spark.sql(\"alter table t1 add constraint rangeABC check (concat(a, a) > 'str')\")\n      spark.sql(\"alter table t1 add constraint rangeBD check (`b`.`d` > 0)\")\n\n      spark.sql(\"alter table t1 add constraint arrValue check (arr[0] > 0)\")\n\n      assertException(\"Cannot alter column a because this column is referenced by\") {\n        drop(\"t1\", \"a\" :: Nil)\n      }\n\n      assertException(\"Cannot alter column arr because this column is referenced by\") {\n        drop(\"t1\", \"arr\" :: Nil)\n      }\n\n\n      // cannot drop b because its child is referenced\n      assertException(\"Cannot alter column b because this column is referenced by\") {\n        drop(\"t1\", \"b\" :: Nil)\n      }\n\n      // can still drop b.c because it's referenced by a null constraint\n      drop(\"t1\", \"b.c\" :: Nil)\n\n      // this is a safety flag - it won't error when you turn it off\n      withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_CHANGE_COLUMN_CHECK_EXPRESSIONS.key -> \"false\") {\n        drop(\"t1\", \"b\" :: \"arr\" :: Nil)\n      }\n    }\n  }\n\n  test(\"drop column with constraints - map element\") {\n    def drop(table: String, columns: Seq[String]): Unit =\n      sql(s\"alter table $table drop column (${columns.mkString(\",\")})\")\n\n    withTable(\"t1\") {\n      val schemaWithNotNull =\n        simpleNestedData.schema.toDDL.replace(\"c: STRING\", \"c: STRING NOT NULL\")\n\n      withTable(\"source\") {\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1 ($schemaWithNotNull)\n             |USING DELTA\n             |${propString(Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"))}\n             |\"\"\".stripMargin)\n        simpleNestedData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n      }\n\n      spark.sql(\"alter table t1 add constraint\" +\n        \" mapValue check (not array_contains(map_keys(map), 'k1') or map['k1'] = 'v1')\")\n\n      assertException(\"Cannot alter column map because this column is referenced by\") {\n        drop(\"t1\", \"map\" :: Nil)\n      }\n    }\n  }\n\n  dropTest(\"drop with generated column\") { drop =>\n    withTable(\"t1\") {\n      withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_DROP_COLUMN_ENABLED.key -> \"true\") {\n        val tableBuilder = io.delta.tables.DeltaTable.create(spark).tableName(\"t1\")\n        tableBuilder.property(\"delta.columnMapping.mode\", \"name\")\n\n        // add existing columns\n        simpleNestedSchema.map(field => (field.name, field.dataType)).foreach(col => {\n          val (colName, dataType) = col\n          val columnBuilder = io.delta.tables.DeltaTable.columnBuilder(spark, colName)\n          columnBuilder.dataType(dataType.sql)\n          tableBuilder.addColumn(columnBuilder.build())\n        })\n\n        // add generated columns\n        val genCol1 = io.delta.tables.DeltaTable.columnBuilder(spark, \"genCol1\")\n          .dataType(\"int\")\n          .generatedAlwaysAs(\"length(a)\")\n          .build()\n\n        val genCol2 = io.delta.tables.DeltaTable.columnBuilder(spark, \"genCol2\")\n          .dataType(\"int\")\n          .generatedAlwaysAs(\"b.d * 100 + arr[0]\")\n          .build()\n\n        tableBuilder\n          .addColumn(genCol1)\n          .addColumn(genCol2)\n          .execute()\n\n        simpleNestedData.write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n\n        assertException(\"Cannot alter column a because this column is referenced by\") {\n          drop(\"t1\", \"a\" :: Nil)\n        }\n\n        assertException(\"Cannot alter column b because this column is referenced by\") {\n          drop(\"t1\", \"b\" :: Nil)\n        }\n\n        assertException(\"Cannot alter column b.d because this column is referenced by\") {\n          drop(\"t1\", \"b.d\" :: Nil)\n        }\n\n        assertException(\"Cannot alter column arr because this column is referenced by\") {\n          drop(\"t1\", \"arr\" :: Nil)\n        }\n\n        // you can still drop b.c as it has no dependent gen col\n        drop(\"t1\", \"b.c\" :: Nil)\n\n        // you can also drop a generated column itself\n        drop(\"t1\", \"genCol1\" :: Nil)\n\n        // add new data after dropping\n        spark.createDataFrame(\n          Seq(Row(\"str3\", Row(3), Map(\"k3\" -> \"v3\"), Array(3, 33))).asJava,\n          new StructType()\n            .add(\"a\", StringType, true)\n            .add(\"b\",\n              new StructType()\n                .add(\"d\", IntegerType, true))\n            .add(\"map\", MapType(StringType, StringType), true)\n            .add(\"arr\", ArrayType(IntegerType), true))\n          .write.format(\"delta\").mode(\"append\").saveAsTable(\"t1\")\n\n        checkAnswer(spark.table(\"t1\"),\n          Seq(\n            Row(\"str1\", Row(1), Map(\"k1\" -> \"v1\"), Array(1, 11), 101),\n            Row(\"str2\", Row(2), Map(\"k2\" -> \"v2\"), Array(2, 22), 202),\n            Row(\"str3\", Row(3), Map(\"k3\" -> \"v3\"), Array(3, 33), 303)))\n\n        // this is a safety flag - if you turn it off, it will still error but msg is not as helpful\n        withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_CHANGE_COLUMN_CHECK_EXPRESSIONS.key -> \"false\") {\n          assertException(\"A generated column cannot use a non-existent column\") {\n            drop(\"t1\", \"arr\" :: Nil)\n          }\n        }\n      }\n    }\n  }\n\n  dropTest(\"dropping all columns is not allowed\") { drop =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\")\n      )\n      val e = intercept[AnalysisException] {\n        drop(\"t1\", \"a\" :: \"b\" :: \"map\" :: \"arr\" :: Nil)\n      }\n      assert(e.getMessage.contains(\"Cannot drop column\"))\n    }\n  }\n\n  dropTest(\"dropping partition columns is not allowed\") { drop =>\n    withTable(\"t1\") {\n      createTableWithSQLAPI(\"t1\",\n        simpleNestedData,\n        Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\"),\n        partCols = Seq(\"a\")\n      )\n      val e = intercept[AnalysisException] {\n        drop(\"t1\", \"a\" :: Nil)\n      }\n      assert(e.getMessage.contains(\"Dropping partition columns (a) is not allowed\"))\n    }\n  }\n\n\n  /**\n   * Covers dropping a nested field using the ALTER TABLE command.\n   * @param initialColumnType Type of the single column used to create the initial test table.\n   * @param fieldToDrop       Name of the field to drop from the initial column type.\n   * @param updatedColumnType Expected type of the single column after dropping the nested field.\n   */\n  def testDropNestedField(testName: String)(\n      initialColumnType: String,\n      fieldToDrop: String,\n      updatedColumnType: String): Unit =\n    testColumnMapping(s\"ALTER TABLE DROP COLUMNS - nested $testName\") { mode =>\n      withTempDir { dir =>\n        withTable(\"delta_test\") {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test (data $initialColumnType)\n               |USING delta\n               |TBLPROPERTIES (${DeltaConfigs.COLUMN_MAPPING_MODE.key} = '$mode')\n               |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n\n          val expectedInitialType = initialColumnType.filterNot(_.isWhitespace)\n          val expectedUpdatedType = updatedColumnType.filterNot(_.isWhitespace)\n          val fieldName = s\"data.${fieldToDrop}\"\n\n          def columnType: DataFrame =\n            sql(\"DESCRIBE TABLE delta_test\")\n              .filter(\"col_name = 'data'\")\n              .select(\"data_type\")\n          checkAnswer(columnType, Row(expectedInitialType))\n\n          sql(s\"ALTER TABLE delta_test DROP COLUMNS ($fieldName)\")\n          checkAnswer(columnType, Row(expectedUpdatedType))\n        }\n      }\n    }\n\n  testDropNestedField(\"struct in map key\")(\n    initialColumnType = \"map<struct<a: int, b: string>, int>\",\n    fieldToDrop = \"key.b\",\n    updatedColumnType = \"map<struct<a: int>, int>\")\n\n  testDropNestedField(\"struct in map value\")(\n    initialColumnType = \"map<int, struct<a: int, b: string>>\",\n    fieldToDrop = \"value.b\",\n    updatedColumnType = \"map<int, struct<a: int>>\")\n\n  testDropNestedField(\"struct in array\")(\n    initialColumnType = \"array<struct<a: int, b: string>>\",\n    fieldToDrop = \"element.b\",\n    updatedColumnType = \"array<struct<a: int>>\")\n\n  testDropNestedField(\"struct in nested map keys\")(\n    initialColumnType = \"map<map<struct<a: int, b: string>, int>, int>\",\n    fieldToDrop = \"key.key.b\",\n    updatedColumnType = \"map<map<struct<a: int>, int>, int>\")\n\n  testDropNestedField(\"struct in nested map values\")(\n    initialColumnType = \"map<int, map<int, struct<a: int, b: string>>>\",\n    fieldToDrop = \"value.value.b\",\n    updatedColumnType = \"map<int, map<int, struct<a: int>>>\")\n\n  testDropNestedField(\"struct in nested arrays\")(\n    initialColumnType = \"array<array<struct<a: int, b: string>>>\",\n    fieldToDrop = \"element.element.b\",\n    updatedColumnType = \"array<array<struct<a: int>>>\")\n\n  testDropNestedField(\"struct in nested array and map\")(\n    initialColumnType = \"array<map<int, struct<a: int, b: string>>>\",\n    fieldToDrop = \"element.value.b\",\n    updatedColumnType = \"array<map<int, struct<a: int>>>\")\n\n  testDropNestedField(\"struct in nested map key and array\")(\n    initialColumnType = \"map<array<struct<a: int, b: string>>, int>\",\n    fieldToDrop = \"key.element.b\",\n    updatedColumnType = \"map<array<struct<a: int>>, int>\")\n\n  testDropNestedField(\"struct in nested map value and array\")(\n    initialColumnType = \"map<int, array<struct<a: int, b: string>>>\",\n    fieldToDrop = \"value.element.b\",\n    updatedColumnType = \"map<int, array<struct<a: int>>>\")\n\n  test(\"can't drop map key/value or array element\") {\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"\n           |CREATE TABLE delta_test (m map<int, int>, a array<int>)\n           |USING delta\n           |TBLPROPERTIES (${DeltaConfigs.COLUMN_MAPPING_MODE.key} = 'name')\n          \"\"\".stripMargin)\n      for {\n        field <- Seq(\"m.key\", \"m.value\", \"a.element\")\n      }\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"ALTER TABLE delta_test DROP COLUMN $field\")\n        },\n        \"DELTA_UNSUPPORTED_DROP_NESTED_COLUMN_FROM_NON_STRUCT_TYPE\",\n        parameters = Map(\n          \"struct\" -> \"IntegerType\"\n        )\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaErrorsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{FileNotFoundException, PrintWriter, StringWriter}\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\nimport scala.sys.process.Process\n\n// scalastyle:off import.ordering.noEmptyLine\n// scalastyle:off line.size.limit\nimport org.apache.spark.sql.delta.DeltaErrors.generateDocsLink\nimport org.apache.spark.sql.delta.actions.{Action, Metadata, Protocol}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.delta.constraints.CharVarcharConstraint\nimport org.apache.spark.sql.delta.constraints.Constraints\nimport org.apache.spark.sql.delta.constraints.Constraints.NotNull\nimport org.apache.spark.sql.delta.hooks.{AutoCompactType, PostCommitHook}\nimport org.apache.spark.sql.delta.schema.{DeltaInvariantViolationException, InvariantViolationException, SchemaMergingUtils, SchemaUtils, UnsupportedDataTypeInfo}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport io.delta.sql.DeltaSparkSessionExtension\nimport org.apache.hadoop.fs.Path\nimport org.json4s.JString\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.{SparkContext, SparkThrowable}\nimport org.apache.spark.sql.{AnalysisException, QueryTest, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable}\nimport org.apache.spark.sql.catalyst.dsl.expressions._\nimport org.apache.spark.sql.catalyst.expressions.{AttributeReference, ExprId, Length, LessThanOrEqual, Literal, SparkVersion}\nimport org.apache.spark.sql.catalyst.expressions.Uuid\nimport org.apache.spark.sql.catalyst.parser.CatalystSqlParser\nimport org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.errors.QueryErrorsBase\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\ntrait DeltaErrorsSuiteBase\n    extends QueryTest\n    with SharedSparkSession\n    with GivenWhenThen\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils\n    with QueryErrorsBase {\n\n  val MAX_URL_ACCESS_RETRIES = 3\n  val path = \"/sample/path\"\n\n  // Map of error function to the error\n  // When adding a function...\n  // (a) if the function is just a message: add the name of the message/function as the key, and an\n  // error that uses that message as the value\n  // (b) if the function is an error function: add the name of the function as the key, and the\n  // value as the error being thrown\n  def errorsToTest: Map[String, Throwable] = Map(\n    \"createExternalTableWithoutLogException\" ->\n      DeltaErrors.createExternalTableWithoutLogException(new Path(path), \"tableName\", spark),\n    \"createExternalTableWithoutSchemaException\" ->\n      DeltaErrors.createExternalTableWithoutSchemaException(new Path(path), \"tableName\", spark),\n    \"createManagedTableWithoutSchemaException\" ->\n      DeltaErrors.createManagedTableWithoutSchemaException(\"tableName\", spark),\n    \"multipleSourceRowMatchingTargetRowInMergeException\" ->\n      DeltaErrors.multipleSourceRowMatchingTargetRowInMergeException(spark),\n    \"concurrentModificationExceptionMsg\" -> new ConcurrentWriteException(None),\n    \"incorrectLogStoreImplementationException\" ->\n      DeltaErrors.incorrectLogStoreImplementationException(sparkConf, new Throwable()),\n    \"sourceNotDeterministicInMergeException\" ->\n      DeltaErrors.sourceNotDeterministicInMergeException(spark),\n    \"columnMappingAdviceMessage\" ->\n      DeltaErrors.columnRenameNotSupported,\n    \"icebergClassMissing\" -> DeltaErrors.icebergClassMissing(sparkConf, new Throwable()),\n    \"tableFeatureReadRequiresWriteException\" ->\n      DeltaErrors.tableFeatureReadRequiresWriteException(requiredWriterVersion = 7),\n    \"tableFeatureRequiresHigherReaderProtocolVersion\" ->\n      DeltaErrors.tableFeatureRequiresHigherReaderProtocolVersion(\n        feature = \"feature\",\n        currentVersion = 1,\n        requiredVersion = 7),\n    \"tableFeatureRequiresHigherWriterProtocolVersion\" ->\n      DeltaErrors.tableFeatureRequiresHigherReaderProtocolVersion(\n        feature = \"feature\",\n        currentVersion = 1,\n        requiredVersion = 7),\n    \"blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges\" ->\n      DeltaErrors.blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges(\n        spark,\n        StructType.fromDDL(\"id int\"),\n        StructType.fromDDL(\"id2 int\"),\n        detectedDuringStreaming = true),\n    \"concurrentAppendException\" ->\n      DeltaErrors.concurrentAppendException(None, \"t\", -1, partitionOpt = None),\n    \"concurrentDeleteDeleteException\" ->\n      DeltaErrors.concurrentDeleteDeleteException(None, \"t\", -1, partitionOpt = None),\n    \"concurrentDeleteReadException\" ->\n      DeltaErrors.concurrentDeleteReadException(None, \"t\", -1, partitionOpt = None),\n    \"concurrentWriteException\" ->\n      DeltaErrors.concurrentWriteException(None),\n    \"concurrentTransactionException\" ->\n      DeltaErrors.concurrentTransactionException(None),\n    \"metadataChangedException\" ->\n      DeltaErrors.metadataChangedException(None),\n    \"protocolChangedException\" ->\n      DeltaErrors.protocolChangedException(None)\n  )\n\n  def otherMessagesToTest: Map[String, String] = Map(\n    \"ignoreStreamingUpdatesAndDeletesWarning\" ->\n      DeltaErrors.ignoreStreamingUpdatesAndDeletesWarning(spark)\n  )\n\n  def errorMessagesToTest: Map[String, String] =\n    errorsToTest.mapValues(_.getMessage).toMap ++ otherMessagesToTest\n\n  def checkIfValidResponse(url: String, response: String): Boolean = {\n    response.contains(\"HTTP/1.1 200 OK\") || response.contains(\"HTTP/2 200\")\n  }\n\n  def getUrlsFromMessage(message: String): List[String] = {\n    val regexToFindUrl = \"https://[^\\\\s]+\".r\n    regexToFindUrl.findAllIn(message).toList\n  }\n\n  def testUrl(errName: String, url: String): Unit = {\n    Given(s\"*** Checking response for url: $url\")\n    val lastResponse = (1 to MAX_URL_ACCESS_RETRIES).map { attempt =>\n      if (attempt > 1) Thread.sleep(1000)\n      val response = try {\n        Process(\"curl -I -L \" + url).!!\n      } catch {\n        case e: RuntimeException =>\n          val sw = new StringWriter\n          e.printStackTrace(new PrintWriter(sw))\n          sw.toString\n      }\n      if (checkIfValidResponse(url, response)) {\n        // The URL is correct. No need to retry.\n        return\n      }\n      response\n    }.last\n\n    // None of the attempts resulted in a valid response. Fail the test.\n    fail(\n      s\"\"\"\n         |A link to the URL: '$url' is broken in the error: $errName, accessing this URL\n         |does not result in a valid response, received the following response: $lastResponse\n       \"\"\".stripMargin)\n  }\n\n  def testUrls(): Unit = {\n    errorMessagesToTest.foreach { case (errName, message) =>\n      getUrlsFromMessage(message).foreach { url =>\n        testUrl(errName, url)\n      }\n    }\n  }\n\n  def generateDocsLink(relativePath: String): String = DeltaErrors.generateDocsLink(\n      spark.sparkContext.getConf, relativePath, skipValidation = true)\n\n  test(\"Validate that links to docs in DeltaErrors are correct\") {\n    // verify DeltaErrors.errorsWithDocsLinks is consistent with DeltaErrorsSuite\n    assert(errorsToTest.keySet ++ otherMessagesToTest.keySet ==\n      DeltaErrors.errorsWithDocsLinks.toSet\n    )\n    testUrls()\n  }\n\n  protected def multipleSourceRowMatchingTargetRowInMergeUrl: String =\n    \"/delta-update.html#upsert-into-a-table-using-merge\"\n\n  test(\"test DeltaErrors methods -- part 1\") {\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.tableAlreadyContainsCDCColumns(Seq(\"col1\", \"col2\"))\n      }\n      checkError(e, \"DELTA_TABLE_ALREADY_CONTAINS_CDC_COLUMNS\", \"42711\",\n        Map(\"columnList\" -> \"[col1,col2]\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.cdcColumnsInData(Seq(\"col1\", \"col2\"))\n      }\n      checkError(e, \"RESERVED_CDC_COLUMNS_ON_WRITE\", \"42939\",\n        Map(\"columnList\" -> \"[col1,col2]\", \"config\" -> \"delta.enableChangeDataFeed\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.multipleCDCBoundaryException(\"starting\")\n      }\n      checkError(e, \"DELTA_MULTIPLE_CDC_BOUNDARY\", \"42614\",\n        Map(\"startingOrEnding\" -> \"starting\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.failOnCheckpointRename(new Path(\"path-1\"), new Path(\"path-2\"))\n      }\n      checkError(e, \"DELTA_CANNOT_RENAME_PATH\", \"22KD1\",\n        Map(\"currentPath\" -> \"path-1\", \"newPath\" -> \"path-2\"))\n    }\n    {\n      val e = intercept[DeltaInvariantViolationException] {\n        throw DeltaErrors.notNullColumnMissingException(NotNull(Seq(\"c0\", \"c1\")))\n      }\n      checkError(e, \"DELTA_MISSING_NOT_NULL_COLUMN_VALUE\", \"23502\",\n        Map(\"columnName\" -> \"c0.c1\"))\n    }\n    {\n      val parent = \"parent\"\n      val nested = IntegerType\n      val nestType = \"nestType\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nestedNotNullConstraint(parent, nested, nestType)\n      }\n      checkError(e, \"DELTA_NESTED_NOT_NULL_CONSTRAINT\", \"0AKDC\", Map(\n        \"parent\" -> parent,\n        \"nestedPrettyJson\" -> nested.prettyJson,\n        \"nestType\" -> nestType,\n        \"configKey\" -> DeltaSQLConf.ALLOW_UNENFORCED_NOT_NULL_CONSTRAINTS.key))\n    }\n    {\n      val e = intercept[DeltaInvariantViolationException] {\n        throw DeltaInvariantViolationException(Constraints.NotNull(Seq(\"col1\")))\n      }\n      checkError(e, \"DELTA_NOT_NULL_CONSTRAINT_VIOLATED\", \"23502\",\n        Map(\"columnName\" -> \"col1\"))\n    }\n    {\n      val expr = UnresolvedAttribute(\"col\")\n      val e = intercept[DeltaInvariantViolationException] {\n        throw DeltaInvariantViolationException(\n          Constraints.Check(CharVarcharConstraint.INVARIANT_NAME,\n            LessThanOrEqual(Length(expr), Literal(5))),\n          Map(\"col\" -> \"Hello World\"))\n      }\n      checkError(e, \"DELTA_EXCEED_CHAR_VARCHAR_LIMIT\", \"22001\",\n        Map(\"value\" -> \"Hello World\", \"expr\" -> \"(length(col) <= 5)\"))\n    }\n    {\n      val e = intercept[DeltaInvariantViolationException] {\n        throw DeltaInvariantViolationException(\n          Constraints.Check(\"__dummy__\",\n            CatalystSqlParser.parseExpression(\"id < 0\")),\n          Map(\"a\" -> \"b\"))\n      }\n      checkError(e, \"DELTA_VIOLATE_CONSTRAINT_WITH_VALUES\", \"23001\", Map(\n        \"constraintName\" -> \"__dummy__\",\n        \"expression\" -> \"(id < 0)\",\n        \"values\" -> \" - a : b\"))\n    }\n    {\n      val tableIdentifier = DeltaTableIdentifier(Some(\"tableName\"))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.notADeltaTableException(tableIdentifier)\n      }\n      checkError(e, \"DELTA_MISSING_DELTA_TABLE\", \"42P01\",\n        Map(\"tableName\" -> tableIdentifier.toString))\n    }\n    {\n      val tableIdentifier = DeltaTableIdentifier(Some(\"tableName\"))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.notADeltaTableException(\n          operation = \"delete\", tableIdentifier)\n      }\n      checkError(e, \"DELTA_TABLE_ONLY_OPERATION\", \"0AKDD\",\n        Map(\"tableName\" -> tableIdentifier.toString, \"operation\" -> \"delete\"))\n    }\n    {\n      val table = TableIdentifier(\"table\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotWriteIntoView(table)\n      }\n      checkError(e, \"DELTA_CANNOT_WRITE_INTO_VIEW\", \"0A000\",\n        Map(\"table\" -> table.toString))\n    }\n    {\n      val sourceType = IntegerType\n      val targetType = DateType\n      val columnName = \"column_name\"\n      val e = intercept[DeltaArithmeticException] {\n        throw DeltaErrors.castingCauseOverflowErrorInTableWrite(sourceType, targetType, columnName)\n      }\n      checkError(e, \"DELTA_CAST_OVERFLOW_IN_TABLE_WRITE\", \"22003\", Map(\n        \"sourceType\" -> toSQLType(sourceType),\n        \"targetType\" -> toSQLType(targetType),\n        \"columnName\" -> toSQLId(columnName),\n        \"storeAssignmentPolicyFlag\" -> SQLConf.STORE_ASSIGNMENT_POLICY.key,\n        \"updateAndMergeCastingFollowsAnsiEnabledFlag\" ->\n          DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key,\n        \"ansiEnabledFlag\" -> SQLConf.ANSI_ENABLED.key))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.invalidColumnName(name = \"col-1\")\n      }\n      checkError(e, \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAME\", \"42K05\",\n        Map(\"columnName\" -> \"col-1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.updateSetColumnNotFoundException(col = \"c0\", colList = Seq(\"c1\", \"c2\"))\n      }\n      checkError(e, \"DELTA_MISSING_SET_COLUMN\", \"42703\",\n        Map(\"columnName\" -> \"`c0`\", \"columnList\" -> \"[`c1`, `c2`]\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.updateSetConflictException(cols = Seq(\"c1\", \"c2\"))\n      }\n      checkError(e, \"DELTA_CONFLICT_SET_COLUMN\", \"42701\",\n        Map(\"columnList\" -> \"[`c1`, `c2`]\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterOnNestedColumnNotSupportedException(\"c0\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_NESTED_COLUMN_IN_BLOOM_FILTER\", \"0AKDC\",\n        Map(\"columnName\" -> \"c0\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterOnPartitionColumnNotSupportedException(\"c0\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_PARTITION_COLUMN_IN_BLOOM_FILTER\", \"0AKDC\",\n        Map(\"columnName\" -> \"c0\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterDropOnNonIndexedColumnException(\"c0\")\n      }\n      checkError(e, \"DELTA_CANNOT_DROP_BLOOM_FILTER_ON_NON_INDEXED_COLUMN\", \"42703\",\n        Map(\"columnName\" -> \"c0\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.cannotRenamePath(\"a\", \"b\")\n      }\n      checkError(e, \"DELTA_CANNOT_RENAME_PATH\", \"22KD1\",\n        Map(\"currentPath\" -> \"a\", \"newPath\" -> \"b\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.cannotSpecifyBothFileListAndPatternString()\n      }\n      checkError(e, \"DELTA_FILE_LIST_AND_PATTERN_STRING_CONFLICT\", \"42613\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotUpdateArrayField(\"t\", \"f\")\n      }\n      checkError(e, \"DELTA_CANNOT_UPDATE_ARRAY_FIELD\", \"429BQ\",\n        Map(\"tableName\" -> \"t\", \"fieldName\" -> \"f\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotUpdateMapField(\"t\", \"f\")\n      }\n      checkError(e, \"DELTA_CANNOT_UPDATE_MAP_FIELD\", \"429BQ\",\n        Map(\"tableName\" -> \"t\", \"fieldName\" -> \"f\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotUpdateStructField(\"t\", \"f\")\n      }\n      checkError(e, \"DELTA_CANNOT_UPDATE_STRUCT_FIELD\", \"429BQ\",\n        Map(\"tableName\" -> \"t\", \"fieldName\" -> \"f\"))\n    }\n    {\n      val tableName = \"table\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotUpdateOtherField(tableName, IntegerType)\n      }\n      checkError(e, \"DELTA_CANNOT_UPDATE_OTHER_FIELD\", \"429BQ\",\n        Map(\"tableName\" -> tableName, \"typeName\" -> IntegerType.toString))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.duplicateColumnsOnUpdateTable(originalException = new Exception(\"123\"))\n      }\n      checkError(e, \"DELTA_DUPLICATE_COLUMNS_ON_UPDATE_TABLE\", \"42701\",\n        Map(\"message\" -> \"123\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.maxCommitRetriesExceededException(0, 1, 2, 3, 4)\n      }\n      checkError(e, \"DELTA_MAX_COMMIT_RETRIES_EXCEEDED\", \"40000\",\n        Map(\"failVersion\" -> \"1\", \"startVersion\" -> \"2\", \"timeSpent\" -> \"4\",\n          \"numActions\" -> \"3\", \"numAttempts\" -> \"0\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.missingColumnsInInsertInto(\"c\")\n      }\n      checkError(e, \"DELTA_INSERT_COLUMN_MISMATCH\", \"42802\",\n        Map(\"columnName\" -> \"c\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.invalidAutoCompactType(\"invalid\")\n      }\n      val allowed = AutoCompactType.ALLOWED_VALUES.mkString(\"(\", \",\", \")\")\n      checkError(e, \"DELTA_INVALID_AUTO_COMPACT_TYPE\", \"22023\",\n        Map(\"value\" -> \"invalid\", \"allowed\" -> allowed))\n    }\n    {\n      val table = DeltaTableIdentifier(Some(\"path\"))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nonExistentDeltaTable(table)\n      }\n      checkError(e, \"DELTA_TABLE_NOT_FOUND\", \"42P01\",\n        Map(\"tableName\" -> table.toString))\n    }\n    {\n      val newTableId = \"027fb01c-94aa-4cab-87cb-5aab6aec6d17\"\n      val oldTableId = \"2edf2c02-bb63-44e9-a84c-517fad0db296\"\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.differentDeltaTableReadByStreamingSource(\n          newTableId = newTableId,\n          oldTableId = oldTableId)\n      }\n      checkError(e, \"DIFFERENT_DELTA_TABLE_READ_BY_STREAMING_SOURCE\", \"55019\", Map(\n          \"oldTableId\" -> oldTableId, \"newTableId\" -> newTableId))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nonExistentColumnInSchema(\"c\", \"s\")\n      }\n      checkError(e, \"DELTA_COLUMN_NOT_FOUND_IN_SCHEMA\", \"42703\", Map(\n        \"columnName\" -> \"c\", \"tableSchema\" -> \"s\"))\n    }\n    {\n      val ident = Identifier.of(Array(\"namespace\"), \"name\")\n      val e = intercept[DeltaNoSuchTableException] {\n        throw DeltaErrors.noRelationTable(ident)\n      }\n      checkError(e, \"DELTA_NO_RELATION_TABLE\", \"42P01\", Map(\"tableIdent\" -> ident.quoted))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.notADeltaTable(\"t\")\n      }\n      checkError(e, \"DELTA_NOT_A_DELTA_TABLE\", \"0AKDD\", Map(\"tableName\" -> \"t\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.notFoundFileToBeRewritten(\"f\", Seq(\"a\", \"b\"))\n      }\n      checkError(e, \"DELTA_FILE_TO_OVERWRITE_NOT_FOUND\", \"42K03\", Map(\"path\" -> \"f\", \"pathList\" -> \"a\\nb\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unsetNonExistentProperty(\"k\", \"t\")\n      }\n      checkError(e, \"DELTA_UNSET_NON_EXISTENT_PROPERTY\", \"42616\",\n        Map(\"property\" -> \"k\", \"tableName\" -> \"t\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsReferToWrongColumns(\n          new AnalysisException(\n            errorClass = \"INTERNAL_ERROR\",\n            messageParameters = Map(\"message\" -> \"internal test error msg\"))\n        )\n      }\n      checkError(e, \"DELTA_INVALID_GENERATED_COLUMN_REFERENCES\", \"42621\", Map.empty[String, String])\n      checkError(e.getCause.asInstanceOf[AnalysisException], \"INTERNAL_ERROR\", None,\n        Map(\"message\" -> \"internal test error msg\"))\n    }\n    {\n      val current = StructField(\"c0\", IntegerType)\n      val update = StructField(\"c0\", StringType)\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsUpdateColumnType(current, update)\n      }\n      checkError(e, \"DELTA_GENERATED_COLUMN_UPDATE_TYPE_MISMATCH\", \"42K09\", Map(\n        \"currentName\" -> current.name,\n        \"currentDataType\" -> current.dataType.sql,\n        \"updateDataType\" -> update.dataType.sql))\n    }\n    {\n      val e = intercept[DeltaColumnMappingUnsupportedException] {\n        throw DeltaErrors.changeColumnMappingModeNotSupported(oldMode = \"old\", newMode = \"new\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_COLUMN_MAPPING_MODE_CHANGE\", \"0AKDC\",\n        Map(\"oldMode\" -> \"old\", \"newMode\" -> \"new\"))\n    }\n    {\n      val e = intercept[DeltaColumnMappingUnsupportedException] {\n        throw DeltaErrors.generateManifestWithColumnMappingNotSupported\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_MANIFEST_GENERATION_WITH_COLUMN_MAPPING\", \"0AKDC\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.convertToDeltaNoPartitionFound(\"testTable\")\n      }\n      checkError(e, \"DELTA_CONVERSION_NO_PARTITION_FOUND\", \"42KD6\",\n        Map(\"tableName\" -> \"testTable\"))\n    }\n    {\n      val e = intercept[DeltaColumnMappingUnsupportedException] {\n        throw DeltaErrors.convertToDeltaWithColumnMappingNotSupported(IdMapping)\n      }\n      checkError(e, \"DELTA_CONVERSION_UNSUPPORTED_COLUMN_MAPPING\", \"0AKDC\", Map(\n        \"config\" -> DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey, \"mode\" -> \"id\"))\n    }\n    {\n      val oldSchema = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val newSchema = StructType(Seq(StructField(\"c1\", IntegerType)))\n      val e = intercept[DeltaColumnMappingUnsupportedException] {\n        throw DeltaErrors.schemaChangeDuringMappingModeChangeNotSupported(\n          oldSchema = oldSchema, newSchema = newSchema)\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_COLUMN_MAPPING_SCHEMA_CHANGE\", \"0AKDC\",\n        Map(\"oldTableSchema\" -> oldSchema.treeString, \"newTableSchema\" -> newSchema.treeString))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.notEnoughColumnsInInsert(\n          \"table\", 1, 2, Some(\"nestedField\"))\n      }\n      checkError(e, \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\", \"42802\",\n        Map(\"tableName\" -> \"table\", \"columnName\" -> \"not enough nested fields in nestedField\",\n          \"numColumns\" -> \"2\", \"insertColumns\" -> \"1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotInsertIntoColumn(\n          \"tableName\", \"source\", \"target\", \"targetType\")\n      }\n      checkError(e, \"DELTA_COLUMN_STRUCT_TYPE_MISMATCH\", \"2200G\",\n        Map(\"source\" -> \"source\", \"targetType\" -> \"targetType\", \"targetField\" -> \"target\",\n          \"targetTable\" -> \"tableName\"))\n    }\n    {\n      val colName = \"col1\"\n      val schema = Seq(UnresolvedAttribute(\"col2\"))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.partitionColumnNotFoundException(colName, schema)\n      }\n      checkError(e, \"DELTA_PARTITION_COLUMN_NOT_FOUND\", \"42703\",\n        Map(\"columnName\" -> DeltaErrors.formatColumn(colName),\n          \"schemaMap\" -> schema.map(_.name).mkString(\", \")))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.partitionPathParseException(\"fragment\")\n      }\n      checkError(e, \"DELTA_INVALID_PARTITION_PATH\", \"22KD1\", Map(\"path\" -> \"fragment\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.replaceWhereMismatchException(\"replaceWhereArgValue\",\n          new InvariantViolationException(\"Invariant violated.\"))\n      }\n      checkError(e, \"DELTA_REPLACE_WHERE_MISMATCH\", \"44000\",\n        Map(\"replaceWhere\" -> \"replaceWhereArgValue\", \"message\" -> \"Invariant violated.\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.replaceWhereMismatchException(\"replaceWhere\", \"badPartitions\")\n      }\n      checkError(e, \"DELTA_REPLACE_WHERE_MISMATCH\", \"44000\",\n        Map(\"replaceWhere\" -> \"replaceWhere\",\n          \"message\" -> \"Invalid data would be written to partitions badPartitions.\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.actionNotFoundException(\"action\", 0)\n      }\n      checkError(e, \"DELTA_STATE_RECOVER_ERROR\", \"XXKDS\",\n        Map(\"operation\" -> \"action\", \"version\" -> \"0\"))\n    }\n    {\n      val oldSchema = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val newSchema = StructType(Seq(StructField(\"c0\", StringType)))\n      for (retryable <- DeltaTestUtils.BOOLEAN_DOMAIN) {\n        val expectedClass: Class[_] = classOf[DeltaIllegalStateException]\n\n        var e = intercept[Exception with SparkThrowable] {\n          throw DeltaErrors.schemaChangedException(oldSchema, newSchema, retryable, None, false)\n        }\n        assert(expectedClass.isAssignableFrom(e.getClass))\n        checkError(e, \"DELTA_SCHEMA_CHANGED\", \"KD007\", Map(\n          \"readSchema\" -> DeltaErrors.formatSchema(oldSchema),\n          \"dataSchema\" -> DeltaErrors.formatSchema(newSchema)\n        ))\n\n        // Check the error message with version information\n        e = intercept[Exception with SparkThrowable] {\n          throw DeltaErrors.schemaChangedException(oldSchema, newSchema, retryable, Some(10), false)\n        }\n        assert(expectedClass.isAssignableFrom(e.getClass))\n        checkError(e, \"DELTA_SCHEMA_CHANGED_WITH_VERSION\", \"KD007\", Map(\n          \"version\" -> \"10\",\n          \"readSchema\" -> DeltaErrors.formatSchema(oldSchema),\n          \"dataSchema\" -> DeltaErrors.formatSchema(newSchema)\n        ))\n\n        // Check the error message with startingVersion/Timestamp error message\n        e = intercept[Exception with SparkThrowable] {\n          throw DeltaErrors.schemaChangedException(oldSchema, newSchema, retryable, Some(10), true)\n        }\n        assert(expectedClass.isAssignableFrom(e.getClass))\n        checkError(e, \"DELTA_SCHEMA_CHANGED_WITH_STARTING_OPTIONS\", \"KD007\", Map(\n          \"version\" -> \"10\",\n          \"readSchema\" -> DeltaErrors.formatSchema(oldSchema),\n          \"dataSchema\" -> DeltaErrors.formatSchema(newSchema)\n        ))\n      }\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.restoreVersionNotExistException(1, 2, 3)\n      }\n      checkError(e, \"DELTA_CANNOT_RESTORE_TABLE_VERSION\", \"22003\",\n        Map(\"version\" -> \"1\", \"startVersion\" -> \"2\", \"endVersion\" -> \"3\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.unsupportedColumnMappingModeException(\"modeName\")\n      }\n      checkError(e, \"DELTA_MODE_NOT_SUPPORTED\", \"0AKDC\", Map(\n        \"mode\" -> \"modeName\",\n        \"supportedModes\" -> DeltaColumnMapping.supportedModes.map(_.name).toSeq.mkString(\", \")\n      ))\n    }\n    {\n      import org.apache.spark.sql.delta.commands.DeltaGenerateCommand\n      val supportedModes = DeltaGenerateCommand.modeNameToGenerationFunc.keys.toSeq.mkString(\", \")\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.unsupportedGenerateModeException(\"modeName\")\n      }\n      checkError(e, \"DELTA_MODE_NOT_SUPPORTED\", \"0AKDC\", Map(\n        \"mode\" -> \"modeName\", \"supportedModes\" -> supportedModes))\n    }\n    {\n      import org.apache.spark.sql.delta.DeltaOptions.EXCLUDE_REGEX_OPTION\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.excludeRegexOptionException(EXCLUDE_REGEX_OPTION)\n      }\n      checkError(e, \"DELTA_REGEX_OPT_SYNTAX_ERROR\", \"2201B\", Map(\n        \"regExpOption\" -> EXCLUDE_REGEX_OPTION\n      ))\n    }\n    {\n      val e = intercept[DeltaFileNotFoundException] {\n        throw DeltaErrors.fileNotFoundException(\"somePath\")\n      }\n      checkError(e, \"DELTA_FILE_NOT_FOUND\", \"42K03\", Map(\n        \"path\" -> \"somePath\"\n      ))\n    }\n    {\n      val e = intercept[DeltaFileNotFoundException] {\n        throw DeltaErrors.logFileNotFoundException(new Path(\"file://table\"), None, 10)\n      }\n      checkError(e, \"DELTA_LOG_FILE_NOT_FOUND\", \"42K03\", Map(\n        \"version\" -> \"LATEST\",\n        \"checkpointVersion\" -> \"10\",\n        \"logPath\" -> \"file://table\"\n      ))\n    }\n    {\n      val ex = new FileNotFoundException(\"reason\")\n      val e = intercept[DeltaFileNotFoundException] {\n        throw DeltaErrors.logFileNotFoundExceptionForStreamingSource(ex)\n      }\n      checkError(e, \"DELTA_LOG_FILE_NOT_FOUND_FOR_STREAMING_SOURCE\", \"42K03\",\n        parameters = Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.invalidIsolationLevelException(\"level\")\n      }\n      checkError(e, \"DELTA_INVALID_ISOLATION_LEVEL\", \"25000\", Map(\n        \"isolationLevel\" -> \"level\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.columnNameNotFoundException(\"a\", \"b\")\n      }\n      checkError(e, \"DELTA_COLUMN_NOT_FOUND\", \"42703\", Map(\n        \"columnName\" -> \"a\",\n        \"columnList\" -> \"b\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.addColumnAtIndexLessThanZeroException(\"1\", \"a\")\n      }\n      checkError(e, \"DELTA_ADD_COLUMN_AT_INDEX_LESS_THAN_ZERO\", \"42KD3\", Map(\n        \"columnIndex\" -> \"1\",\n        \"columnName\" -> \"a\"\n      ))\n    }\n    {\n      val pos = -1\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.dropColumnAtIndexLessThanZeroException(pos)\n      }\n      checkError(e, \"DELTA_DROP_COLUMN_AT_INDEX_LESS_THAN_ZERO\", \"42KD8\",\n        Map(\"columnIndex\" -> pos.toString))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.incorrectArrayAccess()\n      }\n      checkError(e, \"DELTA_INCORRECT_ARRAY_ACCESS\", \"KD003\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaRuntimeException] {\n        throw DeltaErrors.partitionColumnCastFailed(\"Value\", \"Type\", \"Name\")\n      }\n      checkError(e, \"DELTA_PARTITION_COLUMN_CAST_FAILED\", \"22525\",\n        Map(\"value\" -> \"Value\", \"dataType\" -> \"Type\", \"columnName\" -> \"Name\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.invalidTimestampFormat(\"ts\", \"someFormat\")\n      }\n      checkError(e, \"DELTA_INVALID_TIMESTAMP_FORMAT\", \"22007\",\n        Map(\"timestamp\" -> \"ts\", \"format\" -> \"someFormat\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotChangeDataType(\"example message\")\n      }\n      checkError(e, \"DELTA_CANNOT_CHANGE_DATA_TYPE\", \"429BQ\", Map(\"dataType\" -> \"example message\"))\n    }\n    {\n      val table = CatalogTable(TableIdentifier(\"my table\"), null, null, null)\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.tableAlreadyExists(table)\n      }\n      checkError(e, \"DELTA_TABLE_ALREADY_EXISTS\", \"42P07\", Map(\"tableName\" -> \"`my table`\"))\n    }\n    {\n      val storage1 = CatalogStorageFormat(Option(new URI(\"loc1\")), null, null, null, false, Map.empty)\n      val storage2 = CatalogStorageFormat(Option(new URI(\"loc2\")), null, null, null, false, Map.empty)\n      val table = CatalogTable(TableIdentifier(\"table\"), null, storage1, null)\n      val existingTable = CatalogTable(TableIdentifier(\"existing table\"), null, storage2, null)\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.tableLocationMismatch(table, existingTable)\n      }\n      checkError(e, \"DELTA_TABLE_LOCATION_MISMATCH\", \"42613\", Map(\n        \"tableName\" -> \"`table`\",\n        \"tableLocation\" -> \"`loc1`\",\n        \"existingTableLocation\" -> \"`loc2`\"\n      ))\n    }\n    {\n      val ident = \"ident\"\n      val e = intercept[DeltaNoSuchTableException] {\n        throw DeltaErrors.nonSinglePartNamespaceForCatalog(ident)\n      }\n      checkError(e, \"DELTA_NON_SINGLE_PART_NAMESPACE_FOR_CATALOG\", \"42K05\",\n        Map(\"identifier\" -> ident))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.targetTableFinalSchemaEmptyException()\n      }\n      checkError(e, \"DELTA_TARGET_TABLE_FINAL_SCHEMA_EMPTY\", \"428GU\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nonDeterministicNotSupportedException(\"op\", Uuid())\n      }\n      checkError(e, \"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\", \"0AKDC\",\n        Map(\"operation\" -> \"op\", \"expression\" -> \"(condition = uuid()).\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.tableNotSupportedException(\"someOp\")\n      }\n      checkError(e, \"DELTA_TABLE_NOT_SUPPORTED_IN_OP\", \"42809\", Map(\"operation\" -> \"someOp\"))\n    }\n    {\n      val e = intercept[DeltaRuntimeException] {\n        throw DeltaErrors.postCommitHookFailedException(new PostCommitHook() {\n          override val name: String = \"DummyPostCommitHook\"\n          override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {}\n        }, 0, \"msg\", null)\n      }\n      checkError(e, \"DELTA_POST_COMMIT_HOOK_FAILED\", \"2DKD0\",\n        Map(\"version\" -> \"0\", \"name\" -> \"DummyPostCommitHook\", \"message\" -> \": msg\"))\n    }\n    {\n      val e = intercept[DeltaRuntimeException] {\n        throw DeltaErrors.postCommitHookFailedException(new PostCommitHook() {\n          override val name: String = \"DummyPostCommitHook\"\n          override def run(spark: SparkSession, txn: CommittedTransaction): Unit = {}\n        }, 0, null, null)\n      }\n      checkError(e, \"DELTA_POST_COMMIT_HOOK_FAILED\", \"2DKD0\", Map(\n        \"version\" -> \"0\", \"name\" -> \"DummyPostCommitHook\", \"message\" -> \"\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.indexLargerThanStruct(1, StructField(\"col1\", IntegerType), 1)\n      }\n      checkError(e, \"DELTA_INDEX_LARGER_THAN_STRUCT\", \"42KD8\", Map(\n        \"index\" -> \"1\",\n        \"columnName\" -> \"StructField(col1,IntegerType,true)\",\n        \"length\" -> \"1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.indexLargerOrEqualThanStruct(pos = 1, len = 2)\n      }\n      checkError(e, \"DELTA_INDEX_LARGER_OR_EQUAL_THAN_STRUCT\", \"42KD8\",\n        Map(\"index\" -> \"1\", \"length\" -> \"2\") )\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.invalidV1TableCall(\"v1Table\", \"DeltaTableV2\")\n      }\n      checkError(e, \"DELTA_INVALID_V1_TABLE_CALL\", \"XXKDS\",\n        Map(\"callVersion\" -> \"v1Table\", \"tableVersion\" -> \"DeltaTableV2\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.cannotGenerateUpdateExpressions()\n      }\n      checkError(e, \"DELTA_CANNOT_GENERATE_UPDATE_EXPRESSIONS\", \"XXKDS\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        val s1 = StructType(Seq(StructField(\"c0\", IntegerType)))\n        val s2 = StructType(Seq(StructField(\"c0\", StringType)))\n        SchemaMergingUtils.mergeSchemas(s1, s2)\n      }\n      checkError(\n        e,\n        \"DELTA_FAILED_TO_MERGE_FIELDS\",\n        parameters = Map(\"currentField\" -> \"c0\", \"updateField\" -> \"c0\"))\n      checkError(\n        e.getCause.asInstanceOf[DeltaAnalysisException],\n        \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n        parameters = Map(\"currentDataType\" -> \"IntegerType\", \"updateDataType\" -> \"StringType\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.describeViewHistory\n      }\n      checkError(e, \"DELTA_CANNOT_DESCRIBE_VIEW_HISTORY\", \"42809\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.unrecognizedInvariant()\n      }\n      checkError(e, \"DELTA_UNRECOGNIZED_INVARIANT\", \"56038\", Map.empty[String, String])\n    }\n    {\n      val baseSchema = StructType(Seq(StructField(\"c0\", StringType)))\n      val field = StructField(\"id\", IntegerType)\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotResolveColumn(field.name, baseSchema)\n      }\n      checkError(e, \"DELTA_CANNOT_RESOLVE_COLUMN\", \"42703\",\n        Map(\"schema\" -> baseSchema.treeString, \"columnName\" -> \"id\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.alterTableChangeColumnException(\n          fieldPath = \"a.b.c\",\n          oldField = StructField(\"c\", IntegerType),\n          newField = StructField(\"c\", LongType))\n      }\n      checkError(\n        e,\n        \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n        parameters = Map(\n          \"fieldPath\" -> \"a.b.c\",\n          \"oldField\" -> \"INT\",\n          \"newField\" -> \"BIGINT\"\n        )\n      )\n    }\n    {\n      val s1 = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val s2 = StructType(Seq(StructField(\"c0\", StringType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.alterTableReplaceColumnsException(s1, s2, \"incompatible\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_ALTER_TABLE_REPLACE_COL_OP\", \"0AKDC\", Map(\n        \"details\" -> \"incompatible\",\n        \"oldSchema\" -> s1.treeString,\n        \"newSchema\" -> s2.treeString))\n    }\n    {\n      checkError(\n        exception = intercept[DeltaUnsupportedOperationException] {\n          throw DeltaErrors.unsupportedTypeChangeInPreview(\n            fieldPath = Seq(\"origin\", \"country\"),\n            fromType = IntegerType,\n            toType = LongType,\n            feature = TypeWideningPreviewTableFeature\n          )\n        },\n        \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_PREVIEW\",\n        parameters = Map(\n          \"fieldPath\" -> \"origin.country\",\n          \"fromType\" -> \"INT\",\n          \"toType\" -> \"BIGINT\",\n          \"typeWideningFeatureName\" -> \"typeWidening-preview\"\n        ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.unsupportedTypeChangeInSchema(Seq(\"s\", \"a\"), IntegerType, StringType)\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_SCHEMA\", \"0AKDC\",\n        Map(\"fieldName\" -> \"s.a\", \"fromType\" -> \"INT\", \"toType\" -> \"STRING\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        val classConf = Seq((\"classKey\", \"classVal\"))\n        val schemeConf = Seq((\"schemeKey\", \"schemeVal\"))\n        throw DeltaErrors.logStoreConfConflicts(classConf, schemeConf)\n      }\n      checkError(e, \"DELTA_INVALID_LOGSTORE_CONF\", \"F0000\",\n        Map(\"classConfig\" -> \"classKey\", \"schemeConfig\" -> \"schemeKey\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        val schemeConf = Seq((\"key\", \"val\"))\n        throw DeltaErrors.inconsistentLogStoreConfs(\n          Seq((\"delta.key\", \"value1\"), (\"spark.delta.key\", \"value2\")))\n      }\n      checkError(e, \"DELTA_INCONSISTENT_LOGSTORE_CONFS\", \"F0000\",\n        Map(\"setKeys\" -> \"delta.key = value1, spark.delta.key = value2\"))\n    }\n    {\n      val e = intercept[DeltaSparkException] {\n        throw DeltaErrors.failedMergeSchemaFile(\n          file = \"someFile\",\n          schema = \"someSchema\",\n          cause = null)\n      }\n      checkError(e, \"DELTA_FAILED_MERGE_SCHEMA_FILE\", \"42KDA\",\n        Map(\"file\" -> \"someFile\", \"schema\" -> \"someSchema\"))\n    }\n    {\n      val id = TableIdentifier(\"id\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.operationNotSupportedException(\"op\", id)\n      }\n      checkError(e, \"DELTA_OPERATION_NOT_ALLOWED_DETAIL\", \"0AKDC\",\n        Map(\"operation\" -> \"op\", \"tableName\" -> \"`id`\"))\n    }\n    {\n      val e = intercept[DeltaFileNotFoundException] {\n        throw DeltaErrors.fileOrDirectoryNotFoundException(\"somePath\")\n      }\n      checkError(e, \"DELTA_FILE_OR_DIR_NOT_FOUND\", \"42K03\",\n        Map(\"path\" -> \"somePath\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.invalidPartitionColumn(\"col\", \"tbl\")\n      }\n      checkError(e, \"DELTA_INVALID_PARTITION_COLUMN\", \"42996\",\n        Map(\"columnName\" -> \"col\", \"tableName\" -> \"tbl\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.cannotFindSourceVersionException(\"someJson\")\n      }\n      checkError(e, \"DELTA_CANNOT_FIND_VERSION\", \"XXKDS\",\n        Map(\"json\" -> \"someJson\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unknownConfigurationKeyException(\"confKey\")\n      }\n      checkError(e, \"DELTA_UNKNOWN_CONFIGURATION\", \"F0000\", Map(\n        \"config\" -> \"confKey\",\n        \"disableCheckConfig\" -> DeltaSQLConf.ALLOW_ARBITRARY_TABLE_PROPERTIES.key\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.pathNotExistsException(\"somePath\")\n      }\n      checkError(e, \"DELTA_PATH_DOES_NOT_EXIST\", \"42K03\",\n        Map(\"path\" -> \"somePath\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.failRelativizePath(\"somePath\")\n      }\n      checkError(e, \"DELTA_FAIL_RELATIVIZE_PATH\", \"XXKDS\", Map(\n        \"path\" -> \"somePath\",\n        \"config\" -> DeltaSQLConf.DELTA_VACUUM_RELATIVIZE_IGNORE_ERROR.key\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.illegalFilesFound(\"someFile\")\n      }\n      checkError(e, \"DELTA_ILLEGAL_FILE_FOUND\", \"XXKDS\", Map(\"file\" -> \"someFile\"))\n    }\n    {\n      val name = \"name\"\n      val input = \"input\"\n      val explain = \"explain\"\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.illegalDeltaOptionException(name, input, explain)\n      }\n      checkError(e, \"DELTA_ILLEGAL_OPTION\", \"42616\", Map(\"name\" -> name, \"input\" -> input, \"explain\" -> explain))\n    }\n    {\n      val version = \"version\"\n      val timestamp = \"timestamp\"\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.startingVersionAndTimestampBothSetException(version, timestamp)\n      }\n      checkError(e, \"DELTA_STARTING_VERSION_AND_TIMESTAMP_BOTH_SET\", \"42613\",\n        Map(\"version\" -> version, \"timestamp\" -> timestamp))\n    }\n    {\n      val path = new Path(\"parent\", \"child\")\n      val specifiedSchema = StructType(Seq(StructField(\"a\", IntegerType)))\n      val existingSchema = StructType(Seq(StructField(\"b\", StringType)))\n      val diffs = Seq(\"a\", \"b\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createTableWithDifferentSchemaException(path, specifiedSchema, existingSchema, diffs)\n      }\n      checkError(e, \"DELTA_CREATE_TABLE_SCHEME_MISMATCH\", \"42KD7\", Map(\n        \"path\" -> path.toString,\n        \"specifiedSchema\" -> specifiedSchema.treeString,\n        \"existingSchema\" -> existingSchema.treeString,\n        \"schemaDifferences\" -> \"- a\\n- b\"))\n    }\n    {\n      val path = new Path(\"parent\", \"child\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.noHistoryFound(path)\n      }\n      checkError(e, \"DELTA_NO_COMMITS_FOUND\", \"KD006\", Map(\"logPath\" -> path.toString))\n    }\n    {\n      val path = new Path(\"parent\", \"child\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.noRecreatableHistoryFound(path)\n      }\n      checkError(e, \"DELTA_NO_RECREATABLE_HISTORY_FOUND\", \"KD006\", Map(\"logPath\" -> path.toString))\n    }\n    {\n      val e = intercept[DeltaRuntimeException] {\n        throw DeltaErrors.castPartitionValueException(\"partitionValue\", StringType)\n      }\n      checkError(e, \"DELTA_FAILED_CAST_PARTITION_VALUE\", \"22018\",\n        Map(\"value\" -> \"partitionValue\", \"dataType\" -> \"StringType\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.sparkSessionNotSetException()\n      }\n      checkError(e, \"DELTA_SPARK_SESSION_NOT_SET\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val id = Identifier.of(Array(\"namespace\"), \"name\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotReplaceMissingTableException(id)\n      }\n      checkError(e, \"DELTA_CANNOT_REPLACE_MISSING_TABLE\", \"42P01\", Map(\"tableName\" -> \"namespace.name\"))\n    }\n    {\n      val e = intercept[DeltaIOException] {\n        throw DeltaErrors.cannotCreateLogPathException(\"logPath\")\n      }\n      checkError(e, \"DELTA_CANNOT_CREATE_LOG_PATH\", \"42KD5\", Map(\"path\" -> \"logPath\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.protocolPropNotIntException(\n          key = \"someKey\",\n          value = \"someValue\")\n      }\n      checkError(e, \"DELTA_PROTOCOL_PROPERTY_NOT_INT\", \"42K06\",\n        Map(\"key\" -> \"someKey\", \"value\" -> \"someValue\"))\n    }\n    {\n      val path = new Path(\"parent\", \"child\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createExternalTableWithoutLogException(\n          path = path,\n          tableName = \"someTableName\",\n          spark = spark)\n      }\n      checkError(e, \"DELTA_CREATE_EXTERNAL_TABLE_WITHOUT_TXN_LOG\", \"42K03\", Map(\n        \"path\" -> path.toString,\n        \"logPath\" -> new Path(path, \"_delta_log\").toString,\n        \"tableName\" -> \"someTableName\",\n        \"docLink\" -> generateDocsLink(\"/index.html\")\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.ambiguousPathsInCreateTableException(\"id\", \"loc\")\n      }\n      checkError(e, \"DELTA_AMBIGUOUS_PATHS_IN_CREATE_TABLE\", \"42613\", Map(\n        \"identifier\" -> \"id\",\n        \"location\" -> \"loc\",\n        \"config\" -> DeltaSQLConf.DELTA_LEGACY_ALLOW_AMBIGUOUS_PATHS.key))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.illegalUsageException(\"overwriteSchema\", \"replacing\")\n      }\n      checkError(e, \"DELTA_ILLEGAL_USAGE\", \"42601\", Map(\n        \"option\" -> \"overwriteSchema\",\n        \"operation\" -> \"replacing\"\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.expressionsNotFoundInGeneratedColumn(\"col1\")\n      }\n      checkError(e, \"DELTA_EXPRESSIONS_NOT_FOUND_IN_GENERATED_COLUMN\", \"XXKDS\", Map(\n        \"columnName\" -> \"col1\"\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.activeSparkSessionNotFound()\n      }\n      checkError(e, \"DELTA_ACTIVE_SPARK_SESSION_NOT_FOUND\", \"08003\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.operationOnTempViewWithGenerateColsNotSupported(\"UPDATE\")\n      }\n      checkError(e, \"DELTA_OPERATION_ON_TEMP_VIEW_WITH_GENERATED_COLS_NOT_SUPPORTED\", \"0A000\", Map(\n        \"operation\" -> \"UPDATE\"\n      ))\n    }\n    {\n      val property = \"prop\"\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.cannotModifyTableProperty(property)\n      }\n      checkError(e, \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\", \"42939\",\n        Map(\"prop\" -> property))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.missingProviderForConvertException(\"parquet_path\")\n      }\n      checkError(e, \"DELTA_MISSING_PROVIDER_FOR_CONVERT\", \"0AKDC\", Map(\n        \"path\" -> \"parquet_path\"\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.iteratorAlreadyClosed()\n      }\n      checkError(e, \"DELTA_ITERATOR_ALREADY_CLOSED\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.activeTransactionAlreadySet()\n      }\n      checkError(e, \"DELTA_ACTIVE_TRANSACTION_ALREADY_SET\", \"0B000\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterMultipleConfForSingleColumnException(\"col1\")\n      }\n      checkError(e, \"DELTA_MULTIPLE_CONF_FOR_SINGLE_COLUMN_IN_BLOOM_FILTER\", \"42614\",\n        Map(\"columnName\" -> \"col1\"))\n    }\n    {\n      val e = intercept[DeltaIOException] {\n        throw DeltaErrors.incorrectLogStoreImplementationException(sparkConf, null)\n      }\n      checkError(e, \"DELTA_INCORRECT_LOG_STORE_IMPLEMENTATION\", \"0AKDC\", Map(\n        \"docLink\" -> generateDocsLink(\"/delta-storage.html\")\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.invalidSourceVersion(\"xyz\")\n      }\n      checkError(e, \"DELTA_INVALID_SOURCE_VERSION\", \"XXKDS\",\n        Map(\"version\" -> \"xyz\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.invalidSourceOffsetFormat()\n      }\n      checkError(e, \"DELTA_INVALID_SOURCE_OFFSET_FORMAT\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.invalidCommittedVersion(1L, 2L)\n      }\n      checkError(e, \"DELTA_INVALID_COMMITTED_VERSION\", \"XXKDS\", Map(\n        \"committedVersion\" -> \"1\",\n        \"currentVersion\" -> \"2\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nonPartitionColumnReference(\"col1\", Seq(\"col2\", \"col3\"))\n      }\n      checkError(e, \"DELTA_NON_PARTITION_COLUMN_REFERENCE\", \"42P10\",\n        Map(\"columnName\" -> \"col1\", \"columnList\" -> \"col2, col3\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        val attr = UnresolvedAttribute(\"col1\")\n        val attrs = Seq(UnresolvedAttribute(\"col2\"), UnresolvedAttribute(\"col3\"))\n        throw DeltaErrors.missingColumn(attr, attrs)\n      }\n      checkError(e, \"DELTA_MISSING_COLUMN\", \"42703\",\n        Map(\"columnName\" -> \"col1\", \"columnList\" -> \"col2, col3\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        val schema = StructType(Seq(StructField(\"c0\", IntegerType)))\n        throw DeltaErrors.missingPartitionColumn(\"c1\", schema.catalogString)\n      }\n      checkError(e, \"DELTA_MISSING_PARTITION_COLUMN\", \"42KD6\",\n        Map(\"columnName\" -> \"c1\", \"columnList\" -> \"struct<c0:int>\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.aggsNotSupportedException(\"op\", SparkVersion())\n      }\n      checkError(e, \"DELTA_AGGREGATION_NOT_SUPPORTED\", \"42903\",\n        Map(\"operation\" -> \"op\", \"predicate\" -> \"(condition = version())\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotChangeProvider()\n      }\n      checkError(e, \"DELTA_CANNOT_CHANGE_PROVIDER\", \"42939\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.noNewAttributeId(AttributeReference(\"attr1\", IntegerType)())\n      }\n      checkError(e, \"DELTA_NO_NEW_ATTRIBUTE_ID\", \"XXKDS\", Map(\"columnName\" -> \"attr1\"))\n    }\n    {\n      val e = intercept[ProtocolDowngradeException] {\n        val p1 = Protocol(1, 1)\n        val p2 = Protocol(2, 2)\n        throw new ProtocolDowngradeException(p1, p2)\n      }\n      checkError(e, \"DELTA_INVALID_PROTOCOL_DOWNGRADE\", \"KD004\",\n        Map(\"oldProtocol\" -> \"1,1\", \"newProtocol\" -> \"2,2\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsExprTypeMismatch(\"col1\", IntegerType, StringType)\n      }\n      checkError(e, \"DELTA_GENERATED_COLUMNS_EXPR_TYPE_MISMATCH\", \"42K09\",\n        Map(\"columnName\" -> \"col1\", \"expressionType\" -> \"STRING\", \"columnType\" -> \"INT\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.nonGeneratedColumnMissingUpdateExpression(\"attr1\")\n      }\n      checkError(e, \"DELTA_NON_GENERATED_COLUMN_MISSING_UPDATE_EXPR\", \"XXKDS\", Map(\"columnName\" -> \"attr1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.constraintDataTypeMismatch(\n          columnPath = Seq(\"a\", \"x\"),\n          columnType = ByteType,\n          dataType = IntegerType,\n          constraints = Map(\"ck1\" -> \"a > 0\", \"ck2\" -> \"hash(b) > 0\"))\n      }\n      checkError(e, \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\", \"42K09\", Map(\n        \"columnName\" -> \"a.x\",\n        \"columnType\" -> \"TINYINT\",\n        \"dataType\" -> \"INT\",\n        \"constraints\" -> \"ck1 -> a > 0\\nck2 -> hash(b) > 0\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsDataTypeMismatch(\n          columnPath = Seq(\"a\", \"x\"),\n          columnType = ByteType,\n          dataType = IntegerType,\n          generatedColumns = Map(\n            \"gen1\" -> \"a . x + 1\",\n            \"gen2\" -> \"3 + a . x\"\n          ))\n      }\n      checkError(e, \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\", \"42K09\", Map(\n        \"columnName\" -> \"a.x\",\n        \"columnType\" -> \"TINYINT\",\n        \"dataType\" -> \"INT\",\n        \"generatedColumns\" -> \"gen1 -> a . x + 1\\ngen2 -> 3 + a . x\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.useSetLocation()\n      }\n      checkError(e, \"DELTA_CANNOT_CHANGE_LOCATION\", \"42601\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nonPartitionColumnAbsentException(false)\n      }\n      checkError(e, \"DELTA_NON_PARTITION_COLUMN_ABSENT\", \"KD005\", Map(\"details\" -> \"\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nonPartitionColumnAbsentException(true)\n      }\n      checkError(e, \"DELTA_NON_PARTITION_COLUMN_ABSENT\", \"KD005\",\n        Map(\"details\" -> \" Columns which are of NullType have been dropped.\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.constraintAlreadyExists(\"name\", \"oldExpr\")\n      }\n      checkError(e, \"DELTA_CONSTRAINT_ALREADY_EXISTS\", \"42710\",\n        Map(\"constraintName\" -> \"name\", \"oldConstraint\" -> \"oldExpr\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.timeTravelNotSupportedException\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_TIME_TRAVEL_VIEWS\", \"0AKDC\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.addFilePartitioningMismatchException(Seq(\"col3\"), Seq(\"col2\"))\n      }\n      checkError(e, \"DELTA_INVALID_PARTITIONING_SCHEMA\", \"XXKDS\", Map(\n        \"neededPartitioning\" -> \"[`col2`]\",\n        \"specifiedPartitioning\" -> \"[`col3`]\",\n        \"config\" -> DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED.key))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.emptyCalendarInterval\n      }\n      checkError(e, \"DELTA_INVALID_CALENDAR_INTERVAL_EMPTY\", \"2200P\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createManagedTableWithoutSchemaException(\"table-1\", spark)\n      }\n      checkError(e, \"DELTA_INVALID_MANAGED_TABLE_SYNTAX_NO_SCHEMA\", \"42000\", Map(\n        \"tableName\" -> \"table-1\",\n        \"docLink\" -> generateDocsLink(\"/index.html\")\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsUnsupportedExpression(\"someExp\".expr)\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_EXPRESSION_GENERATED_COLUMN\", \"42621\",\n        Map(\"expression\" -> \"'someExp'\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unsupportedExpression(\"Merge\", DataTypes.DateType, Seq(\"Integer\", \"Long\"))\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_EXPRESSION\", \"0A000\", Map(\n        \"expType\" -> \"DateType\",\n        \"causedBy\" -> \"Merge\",\n        \"supportedTypes\" -> \"Integer,Long\"\n      ))\n    }\n    {\n      val expr = \"someExp\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsUDF(expr.expr)\n      }\n      checkError(e, \"DELTA_UDF_IN_GENERATED_COLUMN\", \"42621\", Map(\"udfExpr\" -> \"'someExp'\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterOnColumnTypeNotSupportedException(\"col1\", DateType)\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_COLUMN_TYPE_IN_BLOOM_FILTER\", \"0AKDC\", Map(\n        \"columnName\" -> \"col1\",\n        \"dataType\" -> \"date\"\n      ))\n    }\n    {\n      val e = intercept[DeltaTableFeatureException] {\n        throw DeltaErrors.tableFeatureDropHistoryTruncationNotAllowed()\n      }\n      checkError(e, \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\", \"42000\", Map.empty[String, String])\n    }\n    {\n      val logRetention = DeltaConfigs.LOG_RETENTION\n      val e = intercept[DeltaTableFeatureException] {\n        throw DeltaErrors.dropTableFeatureWaitForRetentionPeriod(\n          \"test_feature\",\n          Metadata(configuration = Map(logRetention.key -> \"30 days\"))\n        )\n      }\n      checkError(e, \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\", \"22KD0\", Map(\n        \"feature\" -> \"test_feature\",\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n    }\n  }\n\n  test(\"test DeltaErrors methods -- part 2\") {\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unsupportedDataTypes(\n          UnsupportedDataTypeInfo(\"foo\", CalendarIntervalType),\n          UnsupportedDataTypeInfo(\"bar\", TimestampNTZType))\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_DATA_TYPES\", \"0AKDC\", Map(\n        \"dataTypeList\" -> \"[foo: CalendarIntervalType, bar: TimestampNTZType]\",\n        \"config\" -> DeltaSQLConf.DELTA_SCHEMA_TYPE_CHECK.key\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.failOnDataLossException(12, 10)\n      }\n      checkError(e, \"DELTA_MISSING_FILES_UNEXPECTED_VERSION\", \"XXKDS\",\n        Map(\"startVersion\" -> \"12\", \"earliestVersion\" -> \"10\", \"option\" -> \"failOnDataLoss\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nestedFieldNotSupported(\"INSERT clause of MERGE operation\", \"col1\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_NESTED_FIELD_IN_OPERATION\", \"0AKDC\",\n        Map(\"operation\" -> \"INSERT clause of MERGE operation\", \"fieldName\" -> \"col1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.newCheckConstraintViolated(10, \"table-1\", \"sample\")\n      }\n      checkError(e, \"DELTA_NEW_CHECK_CONSTRAINT_VIOLATION\", \"23512\", Map(\n        \"checkConstraint\" -> \"sample\",\n        \"tableName\" -> \"table-1\",\n        \"numRows\" -> \"10\"\n      ))\n    }\n    {\n      val e = intercept[DeltaRuntimeException] {\n        throw DeltaErrors.failedInferSchema\n      }\n      checkError(e, \"DELTA_FAILED_INFER_SCHEMA\", \"42KD9\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unexpectedPartialScan(new Path(\"path-1\"))\n      }\n      checkError(e, \"DELTA_UNEXPECTED_PARTIAL_SCAN\", \"KD00A\", Map(\"path\" -> \"path-1\"))\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.unrecognizedLogFile(new Path(\"path-1\"))\n      }\n      checkError(e, \"DELTA_UNRECOGNIZED_LOGFILE\", \"KD00B\", Map(\"filename\" -> \"path-1\"))\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.unsupportedAbsPathAddFile(\"path-1\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_ABS_PATH_ADD_FILE\", \"0AKDC\", Map(\"path\" -> \"path-1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.outputModeNotSupportedException(\"source1\", \"sample\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_OUTPUT_MODE\", \"0AKDC\",\n        Map(\"dataSource\" -> \"source1\", \"mode\" -> \"sample\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        val sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm\", Locale.US)\n        throw DeltaErrors.timestampGreaterThanLatestCommit(\n          new Timestamp(sdf.parse(\"2022-02-28 10:30:00\").getTime),\n          new Timestamp(sdf.parse(\"2022-02-28 10:00:00\").getTime), \"2022-02-28 10:00:00\")\n      }\n      checkError(e, \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\", \"42816\", Map(\n        \"providedTimestamp\" -> \"2022-02-28 10:30:00.0\",\n        \"tableName\" -> \"2022-02-28 10:00:00.0\",\n        \"maximumTimestamp\" -> \"2022-02-28 10:00:00\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        val sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm\", Locale.US)\n        throw DeltaErrors.TimestampEarlierThanCommitRetentionException(\n          new Timestamp(sdf.parse(\"2022-02-28 10:00:00\").getTime),\n          new Timestamp(sdf.parse(\"2022-02-28 11:00:00\").getTime),\n          \"2022-02-28 11:00:00\")\n      }\n      checkError(e, \"DELTA_TIMESTAMP_EARLIER_THAN_COMMIT_RETENTION\", \"42816\", Map(\n        \"userTimestamp\" -> \"2022-02-28 10:00:00.0\",\n        \"commitTs\" -> \"2022-02-28 11:00:00.0\",\n        \"timestampString\" -> \"2022-02-28 11:00:00\"))\n    }\n    {\n      val expr = \"1\".expr\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.timestampInvalid(expr)\n      }\n      checkError(e, \"DELTA_TIMESTAMP_INVALID\", \"42816\", Map(\"expr\" -> expr.sql))\n    }\n    {\n      val version = \"null\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.versionInvalid(version)\n      }\n      checkError(e, \"DELTA_VERSION_INVALID\", \"42815\", Map(\"version\" -> version))\n    }\n    {\n      val version = 2\n      val earliest = 0\n      val latest = 1\n      val e = intercept[DeltaAnalysisException] {\n        throw VersionNotFoundException(version, earliest, latest)\n      }\n      checkError(e, \"DELTA_VERSION_NOT_FOUND\", \"22003\", Map(\n        \"userVersion\" -> version.toString,\n        \"earliest\" -> earliest.toString,\n        \"latest\" -> latest.toString))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.notADeltaSourceException(\"sample\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_SOURCE\", \"0AKDD\",\n        Map(\"operation\" -> \"sample\", \"plan\" -> \"\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.restoreTimestampGreaterThanLatestException(\"2022-02-02 12:12:12\", \"2022-02-02 12:12:10\")\n      }\n      checkError(e, \"DELTA_CANNOT_RESTORE_TIMESTAMP_GREATER\", \"22003\", Map(\n        \"requestedTimestamp\" -> \"2022-02-02 12:12:12\",\n        \"latestTimestamp\" -> \"2022-02-02 12:12:10\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.addColumnStructNotFoundException(\"pos1\")\n      }\n      checkError(e, \"DELTA_ADD_COLUMN_STRUCT_NOT_FOUND\", \"42KD3\", Map(\n        \"position\" -> \"pos1\"\n      ))\n    }\n    {\n      val column = StructField(\"c0\", IntegerType)\n      val other = IntegerType\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.addColumnParentNotStructException(column, other)\n      }\n      checkError(e, \"DELTA_ADD_COLUMN_PARENT_NOT_STRUCT\", \"42KD3\", Map(\n        \"columnName\" -> column.name,\n        \"other\" -> other.toString\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.updateNonStructTypeFieldNotSupportedException(\"col1\", DataTypes.DateType)\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_FIELD_UPDATE_NON_STRUCT\", \"0AKDC\",\n        Map(\"columnName\" -> \"`col1`\", \"dataType\" -> \"DateType\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.extractReferencesFieldNotFound(\"struct1\",\n          DeltaErrors.updateSchemaMismatchExpression(\n            StructType(Seq(StructField(\"c0\", IntegerType))),\n            StructType(Seq(StructField(\"c1\", IntegerType)))\n          ))\n      }\n      checkError(e, \"DELTA_EXTRACT_REFERENCES_FIELD_NOT_FOUND\", \"XXKDS\",\n        Map(\"fieldName\" -> \"struct1\"))\n    }\n    {\n      val e = intercept[DeltaIndexOutOfBoundsException] {\n        throw DeltaErrors.notNullColumnNotFoundInStruct(\"struct1\")\n      }\n      checkError(e, \"DELTA_NOT_NULL_COLUMN_NOT_FOUND_IN_STRUCT\", \"42K09\",\n        Map(\"struct\" -> \"struct1\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.invalidIdempotentWritesOptionsException(\"someReason\")\n      }\n      checkError(e, \"DELTA_INVALID_IDEMPOTENT_WRITES_OPTIONS\", \"42616\",\n        Map(\"reason\" -> \"someReason\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.operationNotSupportedException(\"dummyOp\")\n      }\n      checkError(e, \"DELTA_OPERATION_NOT_ALLOWED\", \"0AKDC\",\n        Map(\"operation\" -> \"dummyOp\"))\n    }\n    {\n      val s1 = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val s2 = StructType(Seq(StructField(\"c0\", StringType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.alterTableSetLocationSchemaMismatchException(s1, s2)\n      }\n      checkError(e, \"DELTA_SET_LOCATION_SCHEMA_MISMATCH\", \"42KD7\", Map(\n        \"original\" -> s1.treeString,\n        \"destination\" -> s2.treeString,\n        \"config\" -> DeltaSQLConf.DELTA_ALTER_LOCATION_BYPASS_SCHEMA_CHECK.key))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.foundDuplicateColumnsException(\"integer\", \"col1\")\n      }\n      checkError(e, \"DELTA_DUPLICATE_COLUMNS_FOUND\", \"42711\",\n        Map(\"coltype\" -> \"integer\", \"duplicateCols\" -> \"col1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.subqueryNotSupportedException(\"dummyOp\", \"col1\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_SUBQUERY\", \"0AKDC\",\n        Map(\"operation\" -> \"dummyOp\", \"cond\" -> \"'col1'\"))\n    }\n    {\n      val schema = StructType(Array(StructField(\"foo\", IntegerType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.foundMapTypeColumnException(\"dummyKey\", \"dummyVal\", schema)\n      }\n      checkError(e, \"DELTA_FOUND_MAP_TYPE_COLUMN\", \"KD003\", Map(\n        \"key\" -> \"dummyKey\",\n        \"value\" -> \"dummyVal\",\n        \"schema\" -> schema.treeString\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.columnOfTargetTableNotFoundInMergeException(\"target\", \"dummyCol\")\n      }\n      checkError(e, \"DELTA_COLUMN_NOT_FOUND_IN_MERGE\", \"42703\",\n        Map(\"targetCol\" -> \"target\", \"colNames\" -> \"dummyCol\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.multiColumnInPredicateNotSupportedException(\"dummyOp\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_MULTI_COL_IN_PREDICATE\", \"0AKDC\",\n        Map(\"operation\" -> \"dummyOp\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.newNotNullViolated(10L, \"table1\", UnresolvedAttribute(\"col1\"))\n      }\n      checkError(e, \"DELTA_NEW_NOT_NULL_VIOLATION\", \"23512\",\n        Map(\"numRows\" -> \"10\", \"tableName\" -> \"table1\", \"colName\" -> \"col1\"))\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.modifyAppendOnlyTableException(\"dummyTable\")\n      }\n      checkError(e, \"DELTA_CANNOT_MODIFY_APPEND_ONLY\", \"42809\",\n        Map(\"table_name\" -> \"dummyTable\", \"config\" -> DeltaConfigs.IS_APPEND_ONLY.key))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.schemaNotConsistentWithTarget(\"dummySchema\", \"targetAttr\")\n      }\n      checkError(e, \"DELTA_SCHEMA_NOT_CONSISTENT_WITH_TARGET\", \"XXKDS\", Map(\n        \"tableSchema\" -> \"dummySchema\",\n        \"targetAttrs\" -> \"targetAttr\"\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.sparkTaskThreadNotFound\n      }\n      checkError(e, \"DELTA_SPARK_THREAD_NOT_FOUND\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.staticPartitionsNotSupportedException\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_STATIC_PARTITIONS\", \"0AKDD\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unsupportedWriteStagedTable(\"table1\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_WRITES_STAGED_TABLE\", \"42807\", Map(\n        \"tableName\" -> \"table1\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.vacuumBasePathMissingException(new Path(\"path-1\"))\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_VACUUM_SPECIFIC_PARTITION\", \"0AKDC\",\n        Map(\"baseDeltaPath\" -> \"path-1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterCreateOnNonExistingColumnsException(Seq(\"col1\", \"col2\"))\n      }\n      checkError(e, \"DELTA_CANNOT_CREATE_BLOOM_FILTER_NON_EXISTING_COL\", \"42703\",\n        Map(\"unknownCols\" -> \"col1, col2\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.zOrderingColumnDoesNotExistException(\"colName\")\n      }\n      checkError(e, \"DELTA_ZORDERING_COLUMN_DOES_NOT_EXIST\", \"42703\", Map(\"columnName\" -> \"colName\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.zOrderingOnPartitionColumnException(\"column1\")\n      }\n      checkError(e, \"DELTA_ZORDERING_ON_PARTITION_COLUMN\", \"42P10\", Map(\"colName\" -> \"column1\"))\n    }\n    {\n      val colNames = Seq(\"col1\", \"col2\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.zOrderingOnColumnWithNoStatsException(colNames, spark)\n      }\n      checkError(e, \"DELTA_ZORDERING_ON_COLUMN_WITHOUT_STATS\", \"KD00D\", Map(\n        \"cols\" -> \"[col1, col2]\",\n        \"zorderColStatKey\" -> DeltaSQLConf.DELTA_OPTIMIZE_ZORDER_COL_STAT_CHECK.key\n      ))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw MaterializedRowId.missingMetadataException(\"table_name\")\n      }\n      checkError(e, \"DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\", \"22000\",\n        Map(\"rowTrackingColumn\" -> \"Row ID\", \"tableName\" -> \"table_name\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw MaterializedRowCommitVersion.missingMetadataException(\"table_name\")\n      }\n      checkError(e, \"DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\", \"22000\", Map(\n        \"rowTrackingColumn\" -> \"Row Commit Version\",\n        \"tableName\" -> \"table_name\"\n      ))\n    }\n    {\n      val path = new Path(\"a/b\")\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.catalogManagedTablePathBasedAccessNotAllowed(path)\n      }\n      checkError(e, \"DELTA_PATH_BASED_ACCESS_TO_CATALOG_MANAGED_TABLE_BLOCKED\", \"KD00G\",\n        Map(\"path\" -> path.toString))\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.operationBlockedOnCatalogManagedTable(\"OPTIMIZE\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\", \"0AKDC\",\n        Map(\"operation\" -> \"OPTIMIZE\"))\n    }\n  }\n\n  // The compiler complains the lambda function is too large if we put all tests in one lambda.\n  test(\"test DeltaErrors OSS methods more\") {\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.schemaNotSetException\n      }\n      checkError(e, \"DELTA_SCHEMA_NOT_SET\", \"KD008\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.schemaNotProvidedException\n      }\n      checkError(e, \"DELTA_SCHEMA_NOT_PROVIDED\", \"42908\", Map.empty[String, String])\n    }\n    {\n      val st1 = StructType(Seq(StructField(\"a0\", IntegerType)))\n      val st2 = StructType(Seq(StructField(\"b0\", IntegerType)))\n      val schemaDiff = SchemaUtils.reportDifferences(st1, st2)\n        .map(_.replace(\"Specified\", \"Latest\"))\n\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.schemaChangedSinceAnalysis(st1, st2)\n      }\n      checkError(e, \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\", \"KD007\", Map(\n        \"schemaDiff\" ->\n          \"Latest schema is missing field(s): a0\\nLatest schema has additional field(s): b0\",\n        \"legacyFlagMessage\" -> \"\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsAggregateExpression(\"1\".expr)\n      }\n      checkError(e, \"DELTA_AGGREGATE_IN_GENERATED_COLUMN\", \"42621\", Map(\"sqlExpr\" -> \"'1'\"))\n    }\n    {\n      val path = new Path(\"somePath\")\n      val specifiedColumns = Seq(\"col1\", \"col2\")\n      val existingColumns = Seq(\"col3\", \"col4\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createTableWithDifferentPartitioningException(path, specifiedColumns, existingColumns)\n      }\n      checkError(e, \"DELTA_CREATE_TABLE_WITH_DIFFERENT_PARTITIONING\", \"42KD7\", Map(\n        \"path\" -> \"somePath\",\n        \"specifiedColumns\" -> \"col1, col2\",\n        \"existingColumns\" -> \"col3, col4\"\n      ))\n    }\n    {\n      val path = new Path(\"a/b\")\n      val smaps = Map(\"abc\" -> \"xyz\")\n      val emaps = Map(\"def\" -> \"hjk\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createTableWithDifferentPropertiesException(path, smaps, emaps)\n      }\n\n      checkError(e, \"DELTA_CREATE_TABLE_WITH_DIFFERENT_PROPERTY\", \"42KD7\", Map(\n        \"path\" -> path.toString,\n        \"specifiedProperties\" -> smaps.map { case (k, v) => s\"$k=$v\" }.mkString(\"\\n\"),\n        \"existingProperties\" -> emaps.map { case (k, v) => s\"$k=$v\" }.mkString(\"\\n\")\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unsupportSubqueryInPartitionPredicates()\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_SUBQUERY_IN_PARTITION_PREDICATES\", \"0AKDC\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaFileNotFoundException] {\n        throw DeltaErrors.emptyDirectoryException(\"dir\")\n      }\n      checkError(e, \"DELTA_EMPTY_DIRECTORY\", \"42K03\", Map(\"directory\" -> \"dir\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.replaceWhereUsedWithDynamicPartitionOverwrite()\n      }\n      checkError(e, \"DELTA_REPLACE_WHERE_WITH_DYNAMIC_PARTITION_OVERWRITE\", \"42613\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.replaceWhereUsedInOverwrite()\n      }\n      checkError(e, \"DELTA_REPLACE_WHERE_IN_OVERWRITE\", \"42613\", Map.empty[String, String])\n    }\n    {\n      val schema = StructType(Array(StructField(\"foo\", IntegerType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.incorrectArrayAccessByName(\"right\", \"wrong\", schema)\n      }\n      checkError(e, \"DELTA_INCORRECT_ARRAY_ACCESS_BY_NAME\", \"KD003\", Map(\n        \"rightName\" -> \"right\",\n        \"wrongName\" -> \"wrong\",\n        \"schema\" -> schema.treeString))\n    }\n    {\n      val columnPath = \"colPath\"\n      val other = IntegerType\n      val column = Seq(\"col1\", \"col2\")\n      val schema = StructType(Array(StructField(\"foo\", IntegerType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.columnPathNotNested(columnPath, other, column, schema)\n      }\n      checkError(e, \"DELTA_COLUMN_PATH_NOT_NESTED\", \"42704\", Map(\n        \"columnPath\" -> columnPath,\n        \"other\" -> other.toString,\n        \"column\" -> column.mkString(\".\"),\n        \"schema\" -> schema.treeString))\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.multipleSourceRowMatchingTargetRowInMergeException(spark)\n      }\n      val docLink = generateDocsLink(multipleSourceRowMatchingTargetRowInMergeUrl)\n      checkError(e, \"DELTA_MULTIPLE_SOURCE_ROW_MATCHING_TARGET_ROW_IN_MERGE\", \"21506\",\n        Map(\"usageReference\" -> docLink))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.showPartitionInNotPartitionedTable(\"table\")\n      }\n      checkError(e, \"DELTA_SHOW_PARTITION_IN_NON_PARTITIONED_TABLE\", \"42809\",\n        Map(\"tableName\" -> \"table\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.showPartitionInNotPartitionedColumn(Set(\"col1\", \"col2\"))\n      }\n      checkError(e, \"DELTA_SHOW_PARTITION_IN_NON_PARTITIONED_COLUMN\", \"42P10\",\n        Map(\"badCols\" -> \"[col1, col2]\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.duplicateColumnOnInsert()\n      }\n      checkError(e, \"DELTA_DUPLICATE_COLUMNS_ON_INSERT\", \"42701\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.timeTravelInvalidBeginValue(\"key\", new Throwable)\n      }\n      checkError(e, \"DELTA_TIME_TRAVEL_INVALID_BEGIN_VALUE\", \"42604\",\n        Map(\"timeTravelKey\" -> \"key\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.metadataAbsentException()\n      }\n      checkError(e, \"DELTA_METADATA_ABSENT\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(errorClass = \"DELTA_CANNOT_USE_ALL_COLUMNS_FOR_PARTITION\",\n          Array.empty)\n      }\n      checkError(e, \"DELTA_CANNOT_USE_ALL_COLUMNS_FOR_PARTITION\", \"428FT\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIOException] {\n        throw DeltaErrors.failedReadFileFooter(\"test.txt\", null)\n      }\n      checkError(e, \"DELTA_FAILED_READ_FILE_FOOTER\", \"KD001\",\n        Map(\"currentFile\" -> \"test.txt\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.failedScanWithHistoricalVersion(123)\n      }\n      checkError(e, \"DELTA_FAILED_SCAN_WITH_HISTORICAL_VERSION\", \"KD002\",\n        Map(\"historicalVersion\" -> \"123\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.failedRecognizePredicate(\"select ALL\", new Throwable())\n      }\n      checkError(e, \"DELTA_FAILED_RECOGNIZE_PREDICATE\", \"42601\",\n        Map(\"predicate\" -> \"select ALL\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.failedFindAttributeInOutputColumns(\"col1\",\n          \"col2,col3,col4\")\n      }\n      checkError(e, \"DELTA_FAILED_FIND_ATTRIBUTE_IN_OUTPUT_COLUMNS\", \"42703\",\n        Map(\"newAttributeName\" -> \"col1\", \"targetOutputColumns\" -> \"col2,col3,col4\"))\n    }\n    {\n      val col = \"col1\"\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.failedFindPartitionColumnInOutputPlan(col)\n      }\n      checkError(e, \"DELTA_FAILED_FIND_PARTITION_COLUMN_IN_OUTPUT_PLAN\", \"XXKDS\",\n        Map(\"partitionColumn\" -> col))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.deltaTableFoundInExecutor()\n      }\n      checkError(e, \"DELTA_TABLE_FOUND_IN_EXECUTOR\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaFileAlreadyExistsException] {\n        throw DeltaErrors.fileAlreadyExists(\"file.txt\")\n      }\n      checkError(e, \"DELTA_FILE_ALREADY_EXISTS\", \"42K04\",\n        Map(\"path\" -> \"file.txt\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.configureSparkSessionWithExtensionAndCatalog(Some(new Throwable()))\n      }\n\n      checkError(e, \"DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG\", \"56038\", Map(\n        \"sparkSessionExtensionName\" -> classOf[DeltaSparkSessionExtension].getName,\n        \"catalogKey\" -> SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key,\n        \"catalogClassName\" -> classOf[DeltaCatalog].getName\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cdcNotAllowedInThisVersion()\n      }\n      checkError(e, \"DELTA_CDC_NOT_ALLOWED_IN_THIS_VERSION\", \"0AKDC\", Map.empty[String, String])\n    }\n    {\n      val ident = TableIdentifier(\"view1\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.convertNonParquetTablesException(ident, \"source1\")\n      }\n      checkError(e, \"DELTA_CONVERT_NON_PARQUET_TABLE\", \"0AKDC\",\n        Map(\"sourceName\" -> \"source1\", \"tableId\" -> \"`view1`\"))\n    }\n    {\n      val from = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val to = StructType(Seq(StructField(\"c1\", IntegerType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.updateSchemaMismatchExpression(from, to)\n      }\n      checkError(e, \"DELTA_UPDATE_SCHEMA_MISMATCH_EXPRESSION\", \"42846\",\n        Map(\"fromCatalog\" -> \"struct<c0:int>\", \"toCatalog\" -> \"struct<c1:int>\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.removeFileCDCMissingExtendedMetadata(\"someFile\")\n      }\n      checkError(e, \"DELTA_REMOVE_FILE_CDC_MISSING_EXTENDED_METADATA\", \"XXKDS\",\n        Map(\"file\" -> \"someFile\"))\n    }\n    {\n      val columnName = \"c0\"\n      val colMatches = Seq(StructField(\"c0\", IntegerType))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.ambiguousPartitionColumnException(columnName, colMatches)\n      }\n      checkError(e, \"DELTA_AMBIGUOUS_PARTITION_COLUMN\", \"42702\",\n        Map(\"column\" -> \"`c0`\", \"colMatches\" -> \"[`c0`]\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.truncateTablePartitionNotSupportedException\n      }\n      checkError(e, \"DELTA_TRUNCATE_TABLE_PARTITION_NOT_SUPPORTED\", \"0AKDC\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.invalidFormatFromSourceVersion(100, 10)\n      }\n      checkError(e, \"DELTA_INVALID_FORMAT_FROM_SOURCE_VERSION\", \"XXKDS\",\n        Map(\"expectedVersion\" -> \"10\", \"realVersion\" -> \"100\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.emptyDataException\n      }\n      checkError(e, \"DELTA_EMPTY_DATA\", \"428GU\", Map.empty[String, String])\n    }\n    {\n      val path = \"somePath\"\n      val parsedCol = \"col1\"\n      val expectedCol = \"col2\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unexpectedPartitionColumnFromFileNameException(path, parsedCol, expectedCol)\n      }\n      checkError(e, \"DELTA_UNEXPECTED_PARTITION_COLUMN_FROM_FILE_NAME\", \"KD009\",\n        Map(\"expectedCol\" -> \"`col2`\", \"parsedCol\" -> \"`col1`\", \"path\" -> \"somePath\"))\n    }\n    {\n      val path = \"somePath\"\n      val parsedCols = Seq(\"col1\", \"col2\")\n      val expectedCols = Seq(\"col3\", \"col4\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unexpectedNumPartitionColumnsFromFileNameException(path, parsedCols, expectedCols)\n      }\n      checkError(e, \"DELTA_UNEXPECTED_NUM_PARTITION_COLUMNS_FROM_FILE_NAME\", \"KD009\", Map(\n        \"expectedCols\" -> \"[`col3`, `col4`]\",\n        \"path\" -> \"somePath\",\n        \"parsedCols\" -> \"[`col1`, `col2`]\",\n        \"parsedColsSize\" -> \"2\",\n        \"expectedColsSize\" -> \"2\"))\n    }\n    {\n      val version = 100L\n      val removedFile = \"file\"\n      val dataPath = \"tablePath\"\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.deltaSourceIgnoreDeleteError(version, removedFile, dataPath)\n      }\n      checkError(e, \"DELTA_SOURCE_IGNORE_DELETE\", \"0A000\",\n        Map(\"removedFile\" -> \"file\", \"version\" -> \"100\", \"dataPath\" -> \"tablePath\"))\n    }\n    {\n      val tableId = \"someTableId\"\n      val tableLocation = \"path\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createTableWithNonEmptyLocation(tableId, tableLocation)\n      }\n      checkError(e, \"DELTA_CREATE_TABLE_WITH_NON_EMPTY_LOCATION\", \"42601\",\n        Map(\"tableId\" -> \"someTableId\", \"tableLocation\" -> \"path\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.maxArraySizeExceeded()\n      }\n      checkError(e, \"DELTA_MAX_ARRAY_SIZE_EXCEEDED\", \"42000\", Map.empty[String, String])\n    }\n    {\n      val unknownColumns = Seq(\"col1\", \"col2\")\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterDropOnNonExistingColumnsException(unknownColumns)\n      }\n      checkError(e, \"DELTA_BLOOM_FILTER_DROP_ON_NON_EXISTING_COLUMNS\", \"42703\",\n        Map(\"unknownColumns\" -> unknownColumns.mkString(\", \")))\n    }\n    {\n      val dataFilters = \"filters\"\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.replaceWhereWithFilterDataChangeUnset(dataFilters)\n      }\n\n      checkError(e, \"DELTA_REPLACE_WHERE_WITH_FILTER_DATA_CHANGE_UNSET\", \"42613\",\n        Map(\"dataFilters\" -> dataFilters))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.missingTableIdentifierException(\"read\")\n      }\n      checkError(e, \"DELTA_OPERATION_MISSING_PATH\", \"42601\",\n        Map(\"operation\" -> \"read\"))\n    }\n    {\n      val column = StructField(\"c0\", IntegerType)\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cannotUseDataTypeForPartitionColumnError(column)\n      }\n      checkError(e, \"DELTA_INVALID_PARTITION_COLUMN_TYPE\", \"42996\",\n        Map(\"name\" -> \"c0\", \"dataType\" -> \"IntegerType\"))\n    }\n    {\n      val catalogPartitionSchema = StructType(Seq(StructField(\"a\", IntegerType)))\n      val userPartitionSchema = StructType(Seq(StructField(\"b\", StringType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unexpectedPartitionSchemaFromUserException(catalogPartitionSchema,\n          userPartitionSchema)\n      }\n      checkError(e, \"DELTA_UNEXPECTED_PARTITION_SCHEMA_FROM_USER\", \"KD009\", Map(\n        \"catalogPartitionSchema\" -> catalogPartitionSchema.treeString,\n        \"userPartitionSchema\" -> userPartitionSchema.treeString))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.invalidInterval(\"interval1\")\n      }\n      checkError(e, \"DELTA_INVALID_INTERVAL\", \"22006\", Map(\"interval\" -> \"interval1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.cdcWriteNotAllowedInThisVersion\n      }\n      checkError(e, \"DELTA_CHANGE_TABLE_FEED_DISABLED\", \"42807\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.specifySchemaAtReadTimeException\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_SCHEMA_DURING_READ\", \"0AKDC\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.readSourceSchemaConflictException\n      }\n      checkError(e, \"DELTA_READ_SOURCE_SCHEMA_CONFLICT\", \"42K07\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.unexpectedDataChangeException(\"operation1\")\n      }\n      checkError(e, \"DELTA_DATA_CHANGE_FALSE\", \"0AKDE\", Map(\"op\" -> \"operation1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.noStartVersionForCDC\n      }\n      checkError(e, \"DELTA_NO_START_FOR_CDC_READ\", \"42601\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.unrecognizedColumnChange(\"change1\")\n      }\n      checkError(e, \"DELTA_UNRECOGNIZED_COLUMN_CHANGE\", \"42601\", Map(\"otherClass\" -> \"change1\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.nullRangeBoundaryInCDCRead()\n      }\n      checkError(e, \"DELTA_CDC_READ_NULL_RANGE_BOUNDARY\", \"22004\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.endBeforeStartVersionInCDC(2, 1)\n      }\n      checkError(e, \"DELTA_INVALID_CDC_RANGE\", \"22003\",\n        Map(\"start\" -> \"2\", \"end\" -> \"1\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.unexpectedChangeFilesFound(\"a.parquet\")\n      }\n      checkError(e, \"DELTA_UNEXPECTED_CHANGE_FILES_FOUND\", \"XXKDS\",\n        Map(\"fileList\" -> \"a.parquet\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.logFailedIntegrityCheck(2, \"option1\")\n      }\n      checkError(e, \"DELTA_TXN_LOG_FAILED_INTEGRITY\", \"XXKDS\", Map(\n        \"version\" -> \"2\",\n        \"mismatchStringOpt\" -> \"option1\"\n      ))\n    }\n    {\n      val path = new Path(\"parent\", \"child\")\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.checkpointNonExistTable(path)\n      }\n      checkError(e, \"DELTA_CHECKPOINT_NON_EXIST_TABLE\", \"42K03\", Map(\"path\" -> path.toString))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.unsupportedDeepCloneException()\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_DEEP_CLONE\", \"0A000\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.viewInDescribeDetailException(TableIdentifier(\"customer\"))\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_DESCRIBE_DETAIL_VIEW\", \"42809\", Map(\"view\" -> \"`customer`\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.pathAlreadyExistsException(new Path(path))\n      }\n      checkError(e, \"DELTA_PATH_EXISTS\", \"42K04\", Map(\"path\" -> \"/sample/path\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(\n          errorClass = \"DELTA_MERGE_MISSING_WHEN\",\n          messageParameters = Array.empty\n        )\n      }\n      checkError(e, \"DELTA_MERGE_MISSING_WHEN\", \"42601\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.unrecognizedFileAction(\"invalidAction\", \"invalidClass\")\n      }\n      checkError(e, \"DELTA_UNRECOGNIZED_FILE_ACTION\", \"XXKDS\", Map(\n        \"action\" -> \"invalidAction\",\n        \"actionClass\" -> \"invalidClass\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.streamWriteNullTypeException\n      }\n      checkError(e, \"DELTA_NULL_SCHEMA_IN_STREAMING_WRITE\", \"42P18\", Map.empty[String, String])\n    }\n    {\n      val expr = \"1\".expr\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw new DeltaIllegalArgumentException(\n          errorClass = \"DELTA_UNEXPECTED_ACTION_EXPRESSION\",\n          messageParameters = Array(s\"$expr\"))\n      }\n      checkError(e, \"DELTA_UNEXPECTED_ACTION_EXPRESSION\", \"42601\",\n        Map(\"expression\" -> \"1\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.unexpectedAlias(\"alias1\")\n      }\n      checkError(e, \"DELTA_UNEXPECTED_ALIAS\", \"XXKDS\", Map(\"alias\" -> \"alias1\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.unexpectedProject(\"project1\")\n      }\n      checkError(e, \"DELTA_UNEXPECTED_PROJECT\", \"XXKDS\", Map(\"project\" -> \"project1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.nullableParentWithNotNullNestedField\n      }\n      checkError(e, \"DELTA_NOT_NULL_NESTED_FIELD\", \"0A000\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.useAddConstraints\n      }\n      checkError(e, \"DELTA_ADD_CONSTRAINTS\", \"0A000\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.deltaSourceIgnoreChangesError(10, \"removedFile\", \"tablePath\")\n      }\n      checkError(e, \"DELTA_SOURCE_TABLE_IGNORE_CHANGES\", \"0A000\", Map(\n        \"version\" -> \"10\",\n        \"file\" -> \"removedFile\",\n        \"dataPath\" -> \"tablePath\"\n      ))\n    }\n    {\n      val limit = \"someLimit\"\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.unknownReadLimit(limit)\n      }\n      checkError(e, \"DELTA_UNKNOWN_READ_LIMIT\", \"42601\", Map(\"limit\" -> limit))\n    }\n    {\n      val privilege = \"unknown\"\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.unknownPrivilege(privilege)\n      }\n      checkError(e, \"DELTA_UNKNOWN_PRIVILEGE\", \"42601\", Map(\"privilege\" -> privilege))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.deltaLogAlreadyExistsException(\"somePath\")\n      }\n      checkError(e, \"DELTA_LOG_ALREADY_EXISTS\", \"42K04\", Map(\"path\" -> \"somePath\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.missingPartFilesException(10L, new FileNotFoundException(\"reason\"))\n      }\n      checkError(e, \"DELTA_MISSING_PART_FILES\", \"42KD6\", Map(\"version\" -> \"10\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.checkConstraintNotBoolean(\"name1\", \"expr1\")\n      }\n      checkError(e, \"DELTA_NON_BOOLEAN_CHECK_CONSTRAINT\", \"42621\", Map(\"name\" -> \"name1\", \"expr\" -> \"expr1\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.checkpointMismatchWithSnapshot\n      }\n      checkError(e, \"DELTA_CHECKPOINT_SNAPSHOT_MISMATCH\", \"XXKDS\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.notADeltaTableException(\"operation1\")\n      }\n      checkError(e, \"DELTA_ONLY_OPERATION\", \"0AKDD\", Map(\"operation\" -> \"operation1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.dropNestedColumnsFromNonStructTypeException(StringType)\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_DROP_NESTED_COLUMN_FROM_NON_STRUCT_TYPE\", \"0AKDC\",\n        Map(\"struct\" -> \"StringType\"))\n    }\n    {\n      val locations = Seq(\"location1\", \"location2\")\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.cannotSetLocationMultipleTimes(locations)\n      }\n      checkError(e, \"DELTA_CANNOT_SET_LOCATION_MULTIPLE_TIMES\", \"XXKDS\",\n        Map(\"location\" -> \"List(location1, location2)\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.metadataAbsentForExistingCatalogTable(\"tblName\", \"file://path/to/table\")\n      }\n      checkError(e, \"DELTA_METADATA_ABSENT_EXISTING_CATALOG_TABLE\", \"XXKDS\", Map(\n        \"tableName\" -> \"tblName\",\n        \"tablePath\" -> \"file://path/to/table\",\n        \"tableNameForDropCmd\" -> \"tblName\"\n      ))\n    }\n    {\n      val e = intercept[DeltaStreamingNonAdditiveSchemaIncompatibleException] {\n        throw DeltaErrors.blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges(\n          spark,\n          StructType.fromDDL(\"id int\"),\n          StructType.fromDDL(\"id2 int\"),\n          detectedDuringStreaming = true\n        )\n      }\n      val docLink = generateDocsLink(\"/versioning.html#column-mapping\")\n      checkError(e, \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_USE_SCHEMA_LOG\", \"42KD4\", Map(\n        \"docLink\" -> docLink,\n        \"readSchema\" -> StructType.fromDDL(\"id int\").json,\n        \"incompatibleSchema\" -> StructType.fromDDL(\"id2 int\").json\n      ))\n      assert(e.additionalProperties(\"detectedDuringStreaming\").toBoolean)\n    }\n    {\n      val e = intercept[DeltaStreamingNonAdditiveSchemaIncompatibleException] {\n        throw DeltaErrors.blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges(\n          spark,\n          StructType.fromDDL(\"id int\"),\n          StructType.fromDDL(\"id2 int\"),\n          detectedDuringStreaming = false\n        )\n      }\n      val docLink = generateDocsLink(\"/versioning.html#column-mapping\")\n      checkError(e, \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE_USE_SCHEMA_LOG\", \"42KD4\", Map(\n        \"docLink\" -> docLink,\n        \"readSchema\" -> StructType.fromDDL(\"id int\").json,\n        \"incompatibleSchema\" -> StructType.fromDDL(\"id2 int\").json\n      ))\n      assert(!e.additionalProperties(\"detectedDuringStreaming\").toBoolean)\n    }\n    {\n      val e = intercept[DeltaRuntimeException] {\n        throw DeltaErrors.cannotContinueStreamingPostSchemaEvolution(\n          nonAdditiveSchemaChangeOpType = \"RENAME AND TYPE WIDENING\",\n          previousSchemaChangeVersion = 0,\n          currentSchemaChangeVersion = 1,\n          readerOptionsUnblock = Seq(\"allowSourceColumnRename\", \"allowSourceColumnTypeChange\"),\n          sqlConfsUnblock = Seq(\n            \"spark.databricks.delta.streaming.allowSourceColumnRename\",\n            \"spark.databricks.delta.streaming.allowSourceColumnTypeChange\"),\n          checkpointHash = 15,\n          prettyColumnChangeDetails = \"some column details\")\n      }\n      checkError(e,\n        \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\",\n        parameters = Map(\n          \"opType\" -> \"RENAME AND TYPE WIDENING\",\n          \"previousSchemaChangeVersion\" -> \"0\",\n          \"currentSchemaChangeVersion\" -> \"1\",\n          \"columnChangeDetails\" -> \"some column details\",\n          \"unblockChangeOptions\" ->\n            s\"\"\"  .option(\"allowSourceColumnRename\", \"1\")\n               |  .option(\"allowSourceColumnTypeChange\", \"1\")\"\"\".stripMargin,\n          \"unblockStreamOptions\" ->\n            s\"\"\"  .option(\"allowSourceColumnRename\", \"always\")\n               |  .option(\"allowSourceColumnTypeChange\", \"always\")\"\"\".stripMargin,\n          \"unblockChangeConfs\" ->\n            s\"\"\"  SET spark.databricks.delta.streaming.allowSourceColumnRename.ckpt_15 = 1;\n               |  SET spark.databricks.delta.streaming.allowSourceColumnTypeChange.ckpt_15 = 1;\"\"\".stripMargin,\n          \"unblockStreamConfs\" ->\n            s\"\"\"  SET spark.databricks.delta.streaming.allowSourceColumnRename.ckpt_15 = \"always\";\n               |  SET spark.databricks.delta.streaming.allowSourceColumnTypeChange.ckpt_15 = \"always\";\"\"\".stripMargin,\n          \"unblockAllConfs\" ->\n            s\"\"\"  SET spark.databricks.delta.streaming.allowSourceColumnRename = \"always\";\n               |  SET spark.databricks.delta.streaming.allowSourceColumnTypeChange = \"always\";\"\"\".stripMargin\n        )\n      )\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.blockColumnMappingAndCdcOperation(DeltaOperations.ManualUpdate)\n      }\n      checkError(e, \"DELTA_BLOCK_COLUMN_MAPPING_AND_CDC_OPERATION\", \"42KD4\",\n        Map(\"opName\" -> \"Manual Update\"))\n    }\n    {\n      val options = Map(\n        \"foo\" -> \"1\",\n        \"bar\" -> \"2\"\n      )\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.unsupportedDeltaTableForPathHadoopConf(options)\n      }\n      val prefixStr = DeltaTableUtils.validDeltaTableHadoopPrefixes.mkString(\"[\", \",\", \"]\")\n      checkError(e, \"DELTA_TABLE_FOR_PATH_UNSUPPORTED_HADOOP_CONF\", \"0AKDC\",\n        Map(\"allowedPrefixes\" -> prefixStr, \"unsupportedOptions\" -> \"foo -> 1,bar -> 2\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.cloneOnRelativePath(\"somePath\")\n      }\n      checkError(e, \"DELTA_INVALID_CLONE_PATH\", \"22KD1\", Map(\"path\" -> \"somePath\"))\n    }\n    {\n      val e = intercept[AnalysisException] {\n        throw DeltaErrors.cloneFromUnsupportedSource( \"table-0\", \"CSV\")\n      }\n      checkError(e, \"DELTA_CLONE_UNSUPPORTED_SOURCE\", \"0AKDC\",\n        Map(\"name\" -> \"table-0\", \"format\" -> \"CSV\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.cloneReplaceUnsupported(TableIdentifier(\"customer\"))\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_CLONE_REPLACE_SAME_TABLE\", \"0AKDC\",\n        Map(\"tableName\" -> \"`customer`\"))\n    }\n    {\n      val e = intercept[DeltaIllegalArgumentException] {\n        throw DeltaErrors.cloneAmbiguousTarget(\"external-location\", TableIdentifier(\"table1\"))\n      }\n      checkError(e, \"DELTA_CLONE_AMBIGUOUS_TARGET\", \"42613\",\n        Map(\"externalLocation\" -> \"external-location\", \"targetIdentifier\" -> \"`table1`\"))\n    }\n    {\n      DeltaTableValueFunctions.supportedFnNames.foreach { fnName =>\n        {\n          val fnCall = s\"${fnName}()\"\n          val e = intercept[DeltaAnalysisException] {\n            sql(s\"SELECT * FROM $fnCall\").collect()\n          }\n          checkError(e, \"INCORRECT_NUMBER_OF_ARGUMENTS\", \"42605\",\n            Map(\"failure\" -> \"not enough args\", \"functionName\" -> fnName, \"minArgs\" -> \"2\",\n              \"maxArgs\" -> \"3\"),\n            ExpectedContext(fragment = fnCall, start = 14, stop = 14 + fnCall.length - 1))\n        }\n        {\n          val fnCall = s\"${fnName}(1, 2, 3, 4, 5)\"\n          val e = intercept[DeltaAnalysisException] {\n            sql(s\"SELECT * FROM ${fnCall}\").collect()\n          }\n          checkError(e, \"INCORRECT_NUMBER_OF_ARGUMENTS\", \"42605\",\n            Map(\"failure\" -> \"too many args\", \"functionName\" -> fnName, \"minArgs\" -> \"2\",\n              \"maxArgs\" -> \"3\"),\n            ExpectedContext(fragment = fnCall, start = 14, stop = 14 + fnCall.length - 1))\n        }\n      }\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.invalidTableValueFunction(\"invalid1\")\n      }\n      checkError(e, \"DELTA_INVALID_TABLE_VALUE_FUNCTION\", \"22000\",\n        Map(\"function\" -> \"invalid1\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(\n          errorClass = \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          messageParameters = Array(\"ALTER TABLE\"))\n      }\n      checkError(e, \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\", \"0AKDE\",\n        Map(\"commandType\" -> \"ALTER TABLE\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(\n          errorClass = \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_ALTER_TABLE_ADD_COLUMN_NOT_SUPPORTED\",\n          messageParameters = Array.empty)\n      }\n      checkError(e, \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_ALTER_TABLE_ADD_COLUMN_NOT_SUPPORTED\",\n        \"0AKDC\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.missingCommitInfo(\"featureName\", \"1225\")\n      }\n      checkError(e, \"DELTA_MISSING_COMMIT_INFO\", \"KD004\",\n        Map(\"featureName\" -> \"featureName\", \"version\" -> \"1225\"))\n    }\n    {\n      val e = intercept[DeltaIllegalStateException] {\n        throw DeltaErrors.missingCommitTimestamp(\"1225\")\n      }\n      checkError(e, \"DELTA_MISSING_COMMIT_TIMESTAMP\", \"KD004\",\n        Map(\"featureName\" -> \"inCommitTimestamp\", \"version\" -> \"1225\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.invalidConstraintName(\"foo\")\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0001\", None, Map(\"name\" -> \"foo\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.bloomFilterInvalidParameterValueException(\"foo\")\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0002\", None, Map(\"message\" -> \"foo\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.convertMetastoreMetadataMismatchException(\n          tableProperties = Map(\"delta.prop1\" -> \"foo\"),\n          deltaConfiguration = Map(\"delta.config1\" -> \"bar\"))\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0003\", None, Map(\n        \"tableProperties\" -> \"[delta.prop1=foo]\",\n        \"configuration\" -> \"[delta.config1=bar]\",\n        \"metadataCheckSqlConf\" -> DeltaSQLConf.DELTA_CONVERT_METADATA_CHECK_ENABLED.key))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.restoreTimestampBeforeEarliestException(\"2022-02-02 12:12:12\",\n          \"2022-02-02 12:12:14\")\n      }\n      checkError(e, \"DELTA_CANNOT_RESTORE_TIMESTAMP_EARLIER\", \"22003\", Map(\n        \"requestedTimestamp\" -> \"2022-02-02 12:12:12\",\n        \"earliestTimestamp\" -> \"2022-02-02 12:12:14\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.viewNotSupported(\"FOO_OP\")\n      }\n      checkError(e, \"DELTA_OPERATION_ON_VIEW_NOT_ALLOWED\", \"0AKDC\",\n        Map(\"operation\" -> \"FOO_OP\"))\n    }\n    {\n      val expr = \"1\".expr\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.generatedColumnsNonDeterministicExpression(expr)\n      }\n      checkError(e, \"DELTA_NON_DETERMINISTIC_EXPRESSION_IN_GENERATED_COLUMN\", \"42621\",\n        Map(\"expr\" -> \"'1'\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.columnBuilderMissingDataType(\"col1\")\n      }\n      checkError(e, \"DELTA_COLUMN_MISSING_DATA_TYPE\", \"42601\", Map(\"colName\" -> \"`col1`\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.foundViolatingConstraintsForColumnChange(\n          \"col1\", Map(\"foo\" -> \"bar\"))\n      }\n      checkError(e, \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\", \"42K09\",\n        Map(\"columnName\" -> \"col1\", \"constraints\" -> \"foo -> bar\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createTableMissingTableNameOrLocation()\n      }\n      checkError(e, \"DELTA_CREATE_TABLE_MISSING_TABLE_NAME_OR_LOCATION\", \"42601\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.createTableIdentifierLocationMismatch(\"delta.`somePath1`\", \"somePath2\")\n      }\n      checkError(e, \"DELTA_CREATE_TABLE_IDENTIFIER_LOCATION_MISMATCH\", \"0AKDC\",\n        Map(\"identifier\" -> \"delta.`somePath1`\", \"location\" -> \"somePath2\"))\n    }\n    {\n      val schema = StructType(Seq(StructField(\"col1\", IntegerType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.dropColumnOnSingleFieldSchema(schema)\n      }\n      checkError(e, \"DELTA_DROP_COLUMN_ON_SINGLE_FIELD_SCHEMA\", \"0AKDC\",\n        Map(\"schema\" -> schema.treeString))\n    }\n    {\n      val schema = StructType(Seq(StructField(\"col1\", IntegerType)))\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.errorFindingColumnPosition(Seq(\"col2\"), schema, \"foo\")\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0008\", None, Map(\n        \"column\" -> \"col2\",\n        \"schema\" -> schema.treeString,\n        \"message\" -> \"foo\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.foundViolatingGeneratedColumnsForColumnChange(\n          columnName = \"col1\",\n          generatedColumns = Map(\"col2\" -> \"col1 + 1\", \"col3\" -> \"col1 + 2\"))\n      }\n      checkError(e, \"DELTA_GENERATED_COLUMNS_DEPENDENT_COLUMN_CHANGE\", \"42K09\", Map(\n        \"columnName\" -> \"col1\",\n        \"generatedColumns\" -> \"col2 -> col1 + 1\\ncol3 -> col1 + 2\"\n      ))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.identityColumnInconsistentMetadata(\"col1\", true, true, true)\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0006\", None, Map(\n        \"colName\" -> \"col1\", \"hasStart\" -> \"true\", \"hasStep\" -> \"true\", \"hasInsert\" -> \"true\"))\n    }\n    {\n      // Test MetadataMismatchErrorBuilder with single sub-error (schema mismatch)\n      val errorBuilder = new MetadataMismatchErrorBuilder()\n      val schema1 = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val schema2 = StructType(Seq(StructField(\"c0\", StringType)))\n      errorBuilder.addSchemaMismatch(schema1, schema2, \"id\")\n      val e = intercept[DeltaAnalysisException] {\n        errorBuilder.finalizeAndThrow(spark.sessionState.conf)\n      }\n      checkError(e, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n      // Verify complete message format with main message + sub-error bullet\n      val message = e.getMessage\n      assert(message.contains(\n        \"\"\"[DELTA_METADATA_MISMATCH] A metadata mismatch was detected when writing to the Delta table.\n          |- A schema mismatch detected when writing to the Delta table (Table ID: id).\n          |To enable schema migration using DataFrameWriter or DataStreamWriter, please set: '.option(\"mergeSchema\", \"true\")'.\n          |For other operations, set the session configuration spark.databricks.delta.schema.autoMerge.enabled to \"true\". See the documentation specific to the operation for details.\n          |\n          |Table schema:\n          |root\n          | |-- c0: integer (nullable = true)\n          |\n          |\n          |Data schema:\n          |root\n          | |-- c0: string (nullable = true)\n          |\"\"\".stripMargin))\n    }\n    // Test with multiple sub-errors\n    {\n      val errorBuilder = new MetadataMismatchErrorBuilder()\n      val schema1 = StructType(Seq(StructField(\"c0\", IntegerType)))\n      val schema2 = StructType(Seq(StructField(\"c0\", StringType)))\n      errorBuilder.addSchemaMismatch(schema1, schema2, \"test-id\")\n      errorBuilder.addPartitioningMismatch(Seq(\"part1\"), Seq(\"part2\"))\n      errorBuilder.addOverwriteBit()\n      val e = intercept[DeltaAnalysisException] {\n        errorBuilder.finalizeAndThrow(spark.sessionState.conf)\n      }\n      checkError(e, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n      // Verify complete message format with main message + three sub-error bullets\n      val message = e.getMessage\n      assert(message.contains(\n        \"\"\"[DELTA_METADATA_MISMATCH] A metadata mismatch was detected when writing to the Delta table.\n          |- A schema mismatch detected when writing to the Delta table (Table ID: test-id).\n          |To enable schema migration using DataFrameWriter or DataStreamWriter, please set: '.option(\"mergeSchema\", \"true\")'.\n          |For other operations, set the session configuration spark.databricks.delta.schema.autoMerge.enabled to \"true\". See the documentation specific to the operation for details.\n          |\n          |Table schema:\n          |root\n          | |-- c0: integer (nullable = true)\n          |\n          |\n          |Data schema:\n          |root\n          | |-- c0: string (nullable = true)\n          |\n          |\n          |- Partition columns do not match the partition columns of the table.\n          |Given: [`part2`]\n          |Table: [`part1`]\n          |\n          |- To overwrite your schema or change partitioning, please set: '.option(\"overwriteSchema\", \"true\")'.\n          |Note that the schema can't be overwritten when using 'replaceWhere'.\"\"\".stripMargin))\n    }\n    // Test with partitioning mismatch only\n    {\n      val errorBuilder = new MetadataMismatchErrorBuilder()\n      errorBuilder.addPartitioningMismatch(Seq(\"year\", \"month\"), Seq(\"date\"))\n      val e = intercept[DeltaAnalysisException] {\n        errorBuilder.finalizeAndThrow(spark.sessionState.conf)\n      }\n      checkError(e, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n      // Verify complete message format with main message + one sub-error bullet\n      val message = e.getMessage\n      assert(message.contains(\n        \"\"\"[DELTA_METADATA_MISMATCH] A metadata mismatch was detected when writing to the Delta table.\n          |- Partition columns do not match the partition columns of the table.\n          |Given: [`date`]\n          |Table: [`year`, `month`]\n          |\"\"\".stripMargin))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.mergeAddVoidColumn(\"fooCol\")\n      }\n      checkError(e, \"DELTA_MERGE_ADD_VOID_COLUMN\", \"42K09\",\n        Map(\"newColumn\" -> \"`fooCol`\"))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentAppendException] {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .concurrentAppendException(None, \"t\", -1, partitionOpt = None)\n      }\n      checkError(e, \"DELTA_CONCURRENT_APPEND.WITHOUT_HINT\", \"2D521\",\n        Map(\n          \"operation\" -> \"TRANSACTION\", \"tableName\" -> \"t\", \"version\" -> \"-1\",\n          \"docLink\" -> generateDocsLink(\"/concurrency-control.html\")\n        )\n      )\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentAppendException] {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .concurrentAppendException(None, \"t\", -1, partitionOpt = Some(\"p1\"))\n      }\n      checkError(e, \"DELTA_CONCURRENT_APPEND.WITH_PARTITION_HINT\", \"2D521\",\n        Map(\"operation\" -> \"TRANSACTION\", \"tableName\" -> \"t\", \"version\" -> \"-1\",\n          \"partitionValues\" -> \"p1\",\n          \"docLink\" -> generateDocsLink(\"/concurrency-control.html\")))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentDeleteReadException] {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .concurrentDeleteReadException(None, \"t\", -1, partitionOpt = None)\n      }\n      checkError(e, \"DELTA_CONCURRENT_DELETE_READ.WITHOUT_HINT\", \"2D521\",\n        Map(\"operation\" -> \"TRANSACTION\", \"tableName\" -> \"t\", \"version\" -> \"-1\",\n          \"docLink\" -> generateDocsLink(\"/concurrency-control.html\")))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentDeleteReadException] {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .concurrentDeleteReadException(None, \"t\", -1, partitionOpt = Some(\"p1\"))\n      }\n      checkError(e, \"DELTA_CONCURRENT_DELETE_READ.WITH_PARTITION_HINT\", \"2D521\",\n        Map(\"operation\" -> \"TRANSACTION\", \"tableName\" -> \"t\", \"version\" -> \"-1\",\n          \"partitionValues\" -> \"p1\",\n          \"docLink\" -> generateDocsLink(\"/concurrency-control.html\")))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentDeleteDeleteException] {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .concurrentDeleteDeleteException(None, \"t\", -1, partitionOpt = None)\n      }\n      checkError(e, \"DELTA_CONCURRENT_DELETE_DELETE.WITHOUT_HINT\", \"2D521\",\n        Map(\"operation\" -> \"TRANSACTION\", \"tableName\" -> \"t\", \"version\" -> \"-1\",\n          \"docLink\" -> generateDocsLink(\"/concurrency-control.html\")))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentDeleteDeleteException] {\n        throw org.apache.spark.sql.delta.DeltaErrors\n          .concurrentDeleteDeleteException(None, \"t\", -1, partitionOpt = Some(\"p1\"))\n      }\n      checkError(e, \"DELTA_CONCURRENT_DELETE_DELETE.WITH_PARTITION_HINT\", \"2D521\",\n        Map(\"operation\" -> \"TRANSACTION\", \"tableName\" -> \"t\", \"version\" -> \"-1\",\n          \"partitionValues\" -> \"p1\",\n          \"docLink\" -> generateDocsLink(\"/concurrency-control.html\")))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentTransactionException] {\n        throw org.apache.spark.sql.delta.DeltaErrors.concurrentTransactionException(None)\n      }\n      checkError(e, \"DELTA_CONCURRENT_TRANSACTION\", \"2D521\", Map.empty[String, String])\n      assert(e.getMessage.contains(\"This error occurs when multiple streaming queries are using \" +\n        \"the same checkpoint to write into this table. Did you run multiple instances of the \" +\n        \"same streaming query at the same time?\"))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ConcurrentWriteException] {\n        throw org.apache.spark.sql.delta.DeltaErrors.concurrentWriteException(None)\n      }\n      checkError(e, \"DELTA_CONCURRENT_WRITE\", \"2D521\", Map.empty[String, String])\n      assert(e.getMessage.contains(\"A concurrent transaction has written new data since the \" +\n        \"current transaction read the table.\"))\n    }\n    {\n      val e = intercept[io.delta.exceptions.ProtocolChangedException] {\n        throw org.apache.spark.sql.delta.DeltaErrors.protocolChangedException(None)\n      }\n      checkError(e, \"DELTA_PROTOCOL_CHANGED\", \"2D521\", Map.empty[String, String])\n      assert(e.getMessage.contains(\"The protocol version of the Delta table has been changed \" +\n        \"by a concurrent update.\"))\n    }\n    {\n      val e = intercept[io.delta.exceptions.MetadataChangedException] {\n        throw org.apache.spark.sql.delta.DeltaErrors.metadataChangedException(None)\n      }\n      checkError(e, \"DELTA_METADATA_CHANGED\", \"2D521\", Map.empty[String, String])\n      assert(e.getMessage.contains(\"The metadata of the Delta table has been changed by a \" +\n        \"concurrent update.\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(\n          errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0009\",\n          messageParameters = Array(\"prefixMsg - \"))\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0009\", None,\n        Map(\"optionalPrefixMessage\" -> \"prefixMsg - \"))\n    }\n    {\n      val expr = \"someExp\".expr\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(\n          errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0010\",\n          messageParameters = Array(\"prefixMsg - \", expr.sql))\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0010\", None,\n        Map(\"optionalPrefixMessage\" -> \"prefixMsg - \", \"expression\" -> \"'someExp'\"))\n    }\n    {\n      val exprs = Seq(\"1\".expr, \"2\".expr)\n      val e = intercept[DeltaAnalysisException] {\n        throw new DeltaAnalysisException(\n          errorClass = \"_LEGACY_ERROR_TEMP_DELTA_0012\",\n          messageParameters = Array(exprs.mkString(\",\")))\n      }\n      checkError(e, \"_LEGACY_ERROR_TEMP_DELTA_0012\", None,\n        Map(\"expression\" -> exprs.mkString(\",\")))\n    }\n    {\n      val unsupportedDataType = IntegerType\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.identityColumnDataTypeNotSupported(unsupportedDataType)\n      }\n      checkError(e, \"DELTA_IDENTITY_COLUMNS_UNSUPPORTED_DATA_TYPE\", \"428H2\",\n        Map(\"dataType\" -> \"integer\"))\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.identityColumnIllegalStep()\n      }\n      checkError(e, \"DELTA_IDENTITY_COLUMNS_ILLEGAL_STEP\", \"42611\", Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaAnalysisException] {\n        throw DeltaErrors.identityColumnWithGenerationExpression()\n      }\n      checkError(e, \"DELTA_IDENTITY_COLUMNS_WITH_GENERATED_EXPRESSION\", \"42613\",\n        Map.empty[String, String])\n    }\n    {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        throw DeltaErrors.unsupportedWritesWithMissingCoordinators(\"test\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_WRITES_WITHOUT_COORDINATOR\", \"0AKDC\",\n        Map(\"coordinatorName\" -> \"test\"))\n    }\n    {\n      val exceptionWithContext =\n        DeltaErrors.multipleSourceRowMatchingTargetRowInMergeException(spark)\n      assert(exceptionWithContext.getMessage.contains(\"https\") === true)\n\n      val newSession = spark.newSession()\n      setCustomContext(newSession, null)\n      val exceptionWithoutContext =\n        DeltaErrors.multipleSourceRowMatchingTargetRowInMergeException(newSession)\n      assert(exceptionWithoutContext.getMessage.contains(\"https\") === false)\n    }\n  }\n\n  private def setCustomContext(session: SparkSession, context: SparkContext): Unit = {\n    val scField = session.getClass.getDeclaredField(\"sparkContext\")\n    scField.setAccessible(true)\n    try {\n      scField.set(session, context)\n    } finally {\n      scField.setAccessible(false)\n    }\n  }\n}\n\nclass DeltaErrorsSuite\n  extends DeltaErrorsSuiteBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaFastDropFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.concurrent.TimeUnit\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, DeletionVectorDescriptor, Protocol, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.AlterTableUnsetPropertiesDeltaCommand\nimport org.apache.spark.sql.delta.commands.DeltaReorgTableCommand\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.paths.SparkPath\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.analysis.ResolvedTable\nimport org.apache.spark.sql.functions.{col, not}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ManualClock\n\nclass DeltaFastDropFeatureSuite\n  extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeletionVectorsTestUtils\n    with DeltaRetentionSuiteBase {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, true.toString)\n    spark.conf.set(DeltaSQLConf.FAST_DROP_FEATURE_GENERATE_DV_TOMBSTONES.key, true.toString)\n    enableDeletionVectors(spark, false, false, false)\n  }\n\n  val barrierVersionPropKey = DeltaConfigs.REQUIRE_CHECKPOINT_PROTECTION_BEFORE_VERSION.key\n\n  protected def createTableWithFeature(\n      deltaLog: DeltaLog,\n      feature: TableFeature,\n      featurePropertyEnablement: Option[String] = None): Unit = {\n    val props = Seq(s\"delta.feature.${feature.name} = 'supported'\") ++ featurePropertyEnablement\n\n    sql(\n      s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n         |TBLPROPERTIES (\n         |${props.mkString(\",\")}\n         |)\"\"\".stripMargin)\n\n    assert(deltaLog.update().protocol.readerAndWriterFeatures.contains(feature))\n  }\n\n  protected def dropTableFeature(\n      deltaLog: DeltaLog,\n      feature: TableFeature,\n      truncateHistory: Boolean = false): Unit = {\n    val dropFeatureSQL =\n      s\"\"\"ALTER TABLE delta.`${deltaLog.dataPath}`\n         |DROP FEATURE ${feature.name}\n         |${if (truncateHistory) \"TRUNCATE HISTORY\" else \"\"}\"\"\".stripMargin\n\n    sql(dropFeatureSQL)\n\n    val snapshot = deltaLog.update()\n    assert(!snapshot.protocol.readerAndWriterFeatures.contains(feature))\n    assert(truncateHistory ||\n      !feature.asInstanceOf[RemovableFeature].requiresHistoryProtection ||\n      snapshot.protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n  }\n\n  protected def getLogFiles(dir: File): Seq[File] = Nil\n\n  protected def getDeltaVersions(dir: Path): Set[Long] = {\n    getFileVersions(getDeltaFiles(new File(dir.toUri)))\n  }\n\n  protected def getCheckpointVersions(dir: Path): Set[Long] = {\n    getFileVersions(getCheckpointFiles(new File(dir.toUri)))\n  }\n\n  protected def setModificationTimes(\n      log: DeltaLog,\n      startTime: Long = System.currentTimeMillis(),\n      startVersion: Long,\n      endVersion: Long,\n      daysToAdd: Int): Unit = {\n    val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n    for (version <- startVersion to endVersion) {\n      setModificationTime(log, startTime, version.toInt, daysToAdd, fs)\n    }\n  }\n\n  protected def addData(dir: File, start: Int, end: Int): Unit =\n    spark.range(start, end).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n  test(\"Dropping reader+writer feature\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n      val snapshot = deltaLog.update()\n      assert(snapshot.protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n      assert(snapshot.metadata.configuration.contains(barrierVersionPropKey))\n      assert(snapshot.metadata.configuration(barrierVersionPropKey).toInt === snapshot.version)\n      assert(getCheckpointVersions(deltaLog.logPath).filter(_ <= snapshot.version).size === 4)\n    }\n  }\n\n  test(\"Dropping writer feature\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      dropTableFeature(deltaLog, TestRemovableWriterFeature)\n\n      // Writer features do not require any checkpoint barriers.\n      val snapshot = deltaLog.update()\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(TestRemovableWriterFeature))\n      assert(!snapshot.metadata.configuration.contains(barrierVersionPropKey))\n      assert(getCheckpointVersions(deltaLog.logPath).size === 0)\n    }\n  }\n\n  test(\"Dropping a legacy reader+writer feature\") {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.TABLE_FEATURES_TEST_FEATURES_ENABLED.key -> false.toString) {\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n        sql(\n          s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n             |TBLPROPERTIES (\n             |delta.minReaderVersion=2,\n             |delta.minWriterVersion=5\n             |)\"\"\".stripMargin)\n\n        assert(deltaLog.update().protocol === Protocol(2, 5))\n\n        // Add some data. This is optional to create a more realistic scenario.\n        addData(dir, start = 0, end = 20)\n        addData(dir, start = 20, end = 40)\n\n        dropTableFeature(deltaLog, ColumnMappingTableFeature)\n\n        val snapshot = deltaLog.update()\n        assert(deltaLog.update().protocol === Protocol(1, 7).withFeatures(Seq(\n          InvariantsTableFeature,\n          AppendOnlyTableFeature,\n          CheckConstraintsTableFeature,\n          ChangeDataFeedTableFeature,\n          GeneratedColumnsTableFeature,\n          CheckpointProtectionTableFeature)))\n        assert(snapshot.metadata.configuration.contains(barrierVersionPropKey))\n        assert(snapshot.metadata.configuration(barrierVersionPropKey).toInt === snapshot.version)\n        assert(getCheckpointVersions(deltaLog.logPath).filter(_ <= snapshot.version).size === 4)\n      }\n    }\n  }\n\n  test(\"Dropping multiple features\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\n           |delta.feature.${VacuumProtocolCheckTableFeature.name} = 'supported'\n           |)\"\"\".stripMargin)\n\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 40, end = 60)\n\n      dropTableFeature(deltaLog, VacuumProtocolCheckTableFeature)\n\n      // When multiple features are dropped, the barrier version must contain the version of the\n      // last dropped feature.\n      val snapshot = deltaLog.update()\n      assert(snapshot.protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(VacuumProtocolCheckTableFeature))\n      assert(snapshot.metadata.configuration.contains(barrierVersionPropKey))\n      assert(snapshot.metadata.configuration(barrierVersionPropKey).toInt === snapshot.version)\n      assert(getCheckpointVersions(deltaLog.logPath).filter(_ <= snapshot.version).size === 8)\n    }\n  }\n\n  test(\"Drop feature with history truncation option\") {\n    // When using the TRUNCATE HISTORY option we fallback to the legacy implementation.\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some( s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      val e = intercept[DeltaTableFeatureException] {\n        dropTableFeature(deltaLog, TestRemovableReaderWriterFeature, truncateHistory = true)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n\n      clock.advance(TimeUnit.HOURS.toMillis(24) + TimeUnit.MINUTES.toMillis(5))\n\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature, truncateHistory = true)\n\n      val snapshot = deltaLog.update()\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n      assert(!snapshot.metadata.configuration.contains(barrierVersionPropKey))\n    }\n  }\n\n  test(\"Mixing drop feature implementations\") {\n    // When using the TRUNCATE HISTORY option we fallback to the legacy implementation.\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      val e = intercept[DeltaTableFeatureException] {\n        dropTableFeature(deltaLog, TestRemovableReaderWriterFeature, truncateHistory = true)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n\n      clock.advance(TimeUnit.HOURS.toMillis(24) + TimeUnit.MINUTES.toMillis(5))\n\n      // Adds the CheckpointProtectionTableFeature.\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature, truncateHistory = false)\n\n      val snapshot = deltaLog.update()\n      assert(snapshot.protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!snapshot.protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n      assert(snapshot.metadata.configuration.contains(barrierVersionPropKey))\n      assert(snapshot.metadata.configuration(barrierVersionPropKey).toInt === snapshot.version)\n\n      // Two checkpoints were created in the first invocation of the legacy implementation. Four\n      // more checkpoints were created in the second invocation.\n      val expectedResult = 5\n      assert(getCheckpointVersions(\n        deltaLog.logPath).filter(_ <= snapshot.version).size === expectedResult)\n    }\n  }\n\n  for (withFastDropFeatureEnabled <- BOOLEAN_DOMAIN)\n  test(\"Drop CheckpointProtectionTableFeature \" +\n      s\"withFastDropFeatureEnabled: $withFastDropFeatureEnabled\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      // Adds the CheckpointProtectionTableFeature.\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n      // More data. This is optional to create a more realistic scenario.\n      addData(dir, start = 40, end = 60)\n\n      val checkpointProtectionVersion =\n        CheckpointProtectionTableFeature.getCheckpointProtectionVersion(deltaLog.update())\n\n      withSQLConf(\n          DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key -> withFastDropFeatureEnabled.toString) {\n        val e = intercept[DeltaTableFeatureException] {\n          dropTableFeature(deltaLog, CheckpointProtectionTableFeature, truncateHistory = true)\n        }\n        checkError(\n          e,\n          \"DELTA_FEATURE_DROP_CHECKPOINT_PROTECTION_WAIT_FOR_RETENTION_PERIOD\",\n          parameters = Map(\"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n\n        clock.advance(TimeUnit.HOURS.toMillis(48))\n\n        dropTableFeature(deltaLog, CheckpointProtectionTableFeature, truncateHistory = true)\n      }\n\n      val snapshot = deltaLog.update()\n      val protocol = snapshot.protocol\n      assert(!protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n      assert(!snapshot.metadata.configuration.contains(barrierVersionPropKey))\n      assert(getDeltaVersions(deltaLog.logPath).min >= checkpointProtectionVersion)\n    }\n  }\n\n  test(\"Drop CheckpointProtectionTableFeature with fast drop feature\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      // Adds the CheckpointProtectionTableFeature.\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n      // This is optional since we won't be allowed to drop CheckpointProtectionTableFeature anyway.\n      // However, we show that in a scenario were the feature would normally dropped, it did not\n      // because we used the fast drop feature command.\n      clock.advance(TimeUnit.HOURS.toMillis(48))\n\n      val e = intercept[DeltaTableFeatureException] {\n        dropTableFeature(deltaLog, CheckpointProtectionTableFeature)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_CAN_ONLY_DROP_CHECKPOINT_PROTECTION_WITH_HISTORY_TRUNCATION\",\n        parameters = Map.empty)\n    }\n  }\n\n  test(\"Attempt dropping CheckpointProtectionTableFeature within the retention period\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      // Adds the CheckpointProtectionTableFeature.\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n      // More data. This is optional to create a more realistic scenario.\n      addData(dir, start = 40, end = 60)\n      deltaLog.checkpoint(deltaLog.update())\n\n      val e1 = intercept[DeltaTableFeatureException] {\n        dropTableFeature(deltaLog, CheckpointProtectionTableFeature, truncateHistory = true)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_CHECKPOINT_PROTECTION_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n\n      // TestRemovableReaderWriterFeature traces still exist in history.\n      clock.advance(TimeUnit.HOURS.toMillis(15))\n\n      val e2 = intercept[DeltaTableFeatureException] {\n        dropTableFeature(deltaLog, CheckpointProtectionTableFeature, truncateHistory = true)\n      }\n      checkError(\n        e2,\n        \"DELTA_FEATURE_DROP_CHECKPOINT_PROTECTION_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n    }\n  }\n\n  test(\"Drop CheckpointProtectionTableFeature when history is already truncated\") {\n    withTempDir { dir =>\n      val startTS = System.currentTimeMillis()\n      val clock = new ManualClock(startTS)\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableReaderWriterFeature,\n        Some(s\"${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = 'true'\"))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      // Adds the CheckpointProtectionTableFeature.\n      dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n      val v1 = deltaLog.update().version\n\n      // Default log retention is 30 days.\n      clock.advance(TimeUnit.DAYS.toMillis(32))\n\n      // More data and checkpoints. Data is optional but the checkpoints are used\n      // to cleanup the logs below.\n      addData(dir, start = 40, end = 60)\n      deltaLog.checkpoint(deltaLog.update())\n      addData(dir, start = 60, end = 80)\n      deltaLog.checkpoint(deltaLog.update())\n\n      val v2 = deltaLog.update().version\n      setModificationTimes(deltaLog, startVersion = v1 + 1, endVersion = v2, daysToAdd = 32)\n\n      deltaLog.cleanUpExpiredLogs(deltaLog.update())\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 80, end = 100)\n      addData(dir, start = 100, end = 120)\n\n      val v3 = deltaLog.update().version\n      setModificationTimes(deltaLog, startVersion = v2 + 1, endVersion = v3, daysToAdd = 32)\n\n      clock.advance(TimeUnit.HOURS.toMillis(48))\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 120, end = 140)\n      addData(dir, start = 140, end = 160)\n\n      // At this point history before the atomic cleanup version should already be clean.\n      val deltaVersionsBeforeDrop = getDeltaVersions(deltaLog.logPath)\n      val atomicHistoryCleanupVersion =\n        CheckpointProtectionTableFeature.getCheckpointProtectionVersion(deltaLog.update())\n      assert(deltaVersionsBeforeDrop.min >= atomicHistoryCleanupVersion)\n\n      val v4 = deltaLog.update().version\n      setModificationTimes(deltaLog, startVersion = v3 + 1, endVersion = v4, daysToAdd = 34)\n      dropTableFeature(deltaLog, CheckpointProtectionTableFeature, truncateHistory = true)\n\n      val snapshot = deltaLog.update()\n      val protocol = snapshot.protocol\n      assert(!protocol.readerAndWriterFeatures.contains(CheckpointProtectionTableFeature))\n      assert(!protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n      assert(!snapshot.metadata.configuration.contains(barrierVersionPropKey))\n\n      // No other commits should have been truncated.\n      assert(getDeltaVersions(deltaLog.logPath).min === deltaVersionsBeforeDrop.min)\n    }\n  }\n\n  for (timeTravelMethod <- Seq(\"restoreSQL\", \"restoreSQLTS\", \"selectSQL\", \"selectSQLTS\",\n                               \"getSnapshotAt\", \"restoreToVersion\", \"restoreToTS\",\n                               \"sparkVersion\", \"sparkTS\"))\n  test(s\"Protocol is validated when time traveling - time-travel method: $timeTravelMethod\") {\n    withTempDir { dir =>\n      def getTimestampForVersion(version: Long): String = {\n        val logPath = new Path(dir.getCanonicalPath, \"_delta_log\")\n        val file = new File(new Path(logPath, f\"$version%020d.json\").toString)\n        val sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\")\n        sdf.format(file.lastModified())\n      }\n\n      val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      createTableWithFeature(deltaLog, TestUnsupportedReaderWriterFeature)\n\n      // Add some data. This is optional to create a more realistic scenario.\n      addData(dir, start = 0, end = 20)\n      addData(dir, start = 20, end = 40)\n\n      val versionBeforeRemoval = deltaLog.update().version\n      val tsBeforeRemoval = getTimestampForVersion(versionBeforeRemoval)\n\n      // Adds the CheckpointProtectionTableFeature.\n      dropTableFeature(deltaLog, TestUnsupportedReaderWriterFeature)\n\n      // More data. This is optional to create a more realistic scenario.\n      addData(dir, start = 40, end = 60)\n      addData(dir, start = 60, end = 80)\n\n      withSQLConf(DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED.key -> true.toString) {\n        val e = intercept[DeltaUnsupportedTableFeatureException] {\n          val table = io.delta.tables.DeltaTable.forPath(dir.toString)\n          DeltaLog.clearCache()\n          val tablePath = s\"delta.`${dir.getCanonicalPath}`\"\n          timeTravelMethod match {\n            case \"restoreSQL\" => sql(s\"RESTORE $tablePath TO VERSION AS OF $versionBeforeRemoval\")\n            case \"restoreSQLTS\" => sql(s\"RESTORE $tablePath TO TIMESTAMP AS OF '$tsBeforeRemoval'\")\n            case \"selectSQL\" => sql(s\"SELECT * FROM $tablePath VERSION AS OF $versionBeforeRemoval\")\n            case \"selectSQLTS\" =>\n              sql(s\"SELECT * FROM $tablePath TIMESTAMP AS OF '$tsBeforeRemoval'\")\n            case \"getSnapshotAt\" => deltaLog.getSnapshotAt(versionBeforeRemoval)\n            case \"restoreToVersion\" => table.restoreToVersion(versionBeforeRemoval)\n            case \"restoreToTS\" => table.restoreToTimestamp(tsBeforeRemoval)\n            case \"sparkVersion\" => spark.read.format(\"delta\")\n              .option(\"versionAsOf\", versionBeforeRemoval).load(dir.getCanonicalPath)\n            case \"sparkTS\" => spark.read.format(\"delta\")\n              .option(\"timestampAsOf\", tsBeforeRemoval).load(dir.getCanonicalPath)\n            case _ => assert(false, \"non existent time travel method.\")\n          }\n        }\n        assert(e.getErrorClass === \"DELTA_UNSUPPORTED_FEATURES_FOR_READ\")\n      }\n    }\n  }\n\n  for (downgradeAllowed <- BOOLEAN_DOMAIN)\n  test(s\"Restore table works with fast drop feature - downgradeAllowed: $downgradeAllowed\") {\n    withSQLConf(\n        DeltaSQLConf.RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED.key -> downgradeAllowed.toString) {\n      withTempDir { dir =>\n        import org.apache.spark.sql.delta.implicits._\n\n        val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        createTableWithFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n        // Add some data.\n        addData(dir, start = 0, end = 20)\n        addData(dir, start = 20, end = 40)\n\n        val versionBeforeRemoval = deltaLog.update().version\n\n        // Adds the CheckpointProtectionTableFeature.\n        dropTableFeature(deltaLog, TestRemovableReaderWriterFeature)\n\n        // More data. This is optional to create a more realistic scenario.\n        addData(dir, start = 40, end = 60)\n        addData(dir, start = 60, end = 80)\n\n        sql(s\"RESTORE delta.`${dir.getCanonicalPath}` TO VERSION AS OF $versionBeforeRemoval\")\n\n        val protocol = deltaLog.update().protocol\n        assert(protocol.readerAndWriterFeatures.contains(TestRemovableReaderWriterFeature))\n        assert(protocol.readerAndWriterFeatures\n          .contains(CheckpointProtectionTableFeature) === !downgradeAllowed)\n\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.getCanonicalPath)\n        assert(targetTable.toDF.as[Long].collect().sorted === Seq.range(0, 40))\n      }\n    }\n  }\n\n  private def createTableWithDeletionVectors(deltaLog: DeltaLog): Unit = {\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> true.toString,\n        DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> true.toString) {\n      val dir = deltaLog.dataPath\n      val targetDF = spark.range(start = 0, end = 100, step = 1, numPartitions = 4)\n      targetDF.write.format(\"delta\").save(dir.toString)\n\n      val targetTable = io.delta.tables.DeltaTable.forPath(dir.toString)\n\n      // Add some DVs.\n      targetTable.delete(\"id < 5 or id >= 95\")\n\n      // Add some more DVs for the same set of files.\n      targetTable.delete(\"id < 10 or id >= 90\")\n\n      // Assert that DVs exist.\n      assert(deltaLog.update().numDeletionVectorsOpt === Some(2L))\n    }\n  }\n\n  private def dropDeletionVectors(deltaLog: DeltaLog, truncateHistory: Boolean = false): Unit = {\n    sql(s\"\"\"ALTER TABLE delta.`${deltaLog.dataPath}`\n         |DROP FEATURE deletionVectors\n         |${if (truncateHistory) \"TRUNCATE HISTORY\" else \"\"}\n         |\"\"\".stripMargin)\n\n    val snapshot = deltaLog.update()\n    val protocol = snapshot.protocol\n    assert(snapshot.numDeletionVectorsOpt.getOrElse(0L) === 0)\n    assert(snapshot.numDeletedRecordsOpt.getOrElse(0L) === 0)\n    assert(truncateHistory ||\n      !protocol.readerFeatureNames.contains(DeletionVectorsTableFeature.name))\n  }\n\n  private def validateTombstones(\n      log: DeltaLog,\n      expectedDVTombstoneCount: Option[Int] = None): Unit = {\n    import org.apache.spark.sql.delta.implicits._\n\n    val snapshot = log.update()\n    val dvPath = DeletionVectorDescriptor\n      .urlEncodedRelativePathIfExists(col(\"deletionVector\"), log.dataPath)\n    val isDVTombstone = DeletionVectorDescriptor.isDeletionVectorPath(col(\"path\"))\n    val isInlineDeletionVector = DeletionVectorDescriptor.isInline(col(\"deletionVector\"))\n\n    val uniqueDvsFromParquetRemoveFiles = snapshot\n      .tombstones\n      .filter(\"deletionVector IS NOT NULL\")\n      .filter(not(isInlineDeletionVector))\n      .filter(not(isDVTombstone))\n      .select(dvPath.as(\"path\"))\n      .filter(col(\"path\").isNotNull)\n      .distinct()\n      .as[String]\n\n    val dvTombstones = snapshot\n      .tombstones\n      .filter(isDVTombstone)\n      .select(\"path\")\n      .as[String]\n\n    val dvTombstonesSet = dvTombstones.collect().toSet\n\n    assert(dvTombstonesSet.nonEmpty || expectedDVTombstoneCount === Some(0))\n    assert(dvTombstonesSet.map(new Path(_)).forall(_.getParent.isRoot))\n    assert(uniqueDvsFromParquetRemoveFiles.collect().toSet === dvTombstonesSet)\n    expectedDVTombstoneCount.foreach(expected => assert(dvTombstonesSet.size === expected))\n  }\n\n  for (withCommitLarge <- BOOLEAN_DOMAIN)\n  test(\"DV tombstones are created when dropping DVs\" +\n      s\"withCommitLarge: $withCommitLarge\") {\n    val threshold = if (withCommitLarge) 0 else 10000\n    withSQLConf(\n        DeltaSQLConf.FAST_DROP_FEATURE_DV_TOMBSTONE_COUNT_THRESHOLD.key -> threshold.toString) {\n      withTempPath { dir =>\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        createTableWithDeletionVectors(deltaLog)\n        dropDeletionVectors(deltaLog)\n        validateTombstones(deltaLog)\n\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.toString)\n        assert(targetTable.toDF.collect().length === 80)\n        // DV Tombstones are recorded in the snapshot state.\n        assert(deltaLog.update().numOfRemoves === 8)\n      }\n    }\n  }\n\n  test(\"DV tombstones are generated when no action is taken in pre-downgrade\") {\n    withTempPath { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      createTableWithDeletionVectors(deltaLog)\n\n      // Remove all DV traces in advance. Table will look clean in DROP FEATURE.\n      val table = DeltaTableV2(spark, deltaLog.dataPath)\n      val properties = Seq(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key)\n      AlterTableUnsetPropertiesDeltaCommand(table, properties, ifExists = true).run(spark)\n\n      import org.apache.spark.sql.connector.catalog.CatalogV2Implicits._\n      val catalog = spark.sessionState.catalogManager.currentCatalog.asTableCatalog\n      val tableId = Seq(table.name()).asIdentifier\n      DeltaReorgTableCommand(ResolvedTable.create(catalog, tableId, table))(Nil).run(spark)\n      assert(deltaLog.update().numDeletedRecordsOpt.forall(_ == 0))\n\n      dropDeletionVectors(deltaLog)\n      validateTombstones(deltaLog)\n    }\n  }\n\n  test(\"We only create missing DV tombstones when dropping DVs\") {\n    withTempPath { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      createTableWithDeletionVectors(deltaLog)\n      dropDeletionVectors(deltaLog)\n      validateTombstones(deltaLog)\n\n      // Re enable the feature and add more DVs. The delete touches a new file as well as a file\n      // that already contains a DV within the retention period.\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n           |SET TBLPROPERTIES (\n           |delta.feature.${DeletionVectorsTableFeature.name} = 'enabled',\n           |${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key} = 'true'\n           |)\"\"\".stripMargin)\n\n      withSQLConf(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> true.toString) {\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.toString)\n        targetTable.delete(\"id > 20 and id <= 30\")\n        assert(deltaLog.update().numDeletionVectorsOpt === Some(2L))\n      }\n\n      sql(s\"ALTER TABLE delta.`${dir.getAbsolutePath}` DROP FEATURE deletionVectors\")\n\n      validateTombstones(deltaLog)\n    }\n  }\n  for (isShallowClone <- Seq(true))\n  test(s\"We do not create redundant DV tombstones after cloning \" +\n      s\"isShallowClone: $isShallowClone\") {\n    withTempPaths(2) { case Seq(sourceDir, targetDir) =>\n      val sourceLog = DeltaLog.forTable(spark, sourceDir.getAbsolutePath)\n      val targetLog = DeltaLog.forTable(spark, targetDir.getAbsolutePath)\n\n      createTableWithDeletionVectors(sourceLog)\n\n      io.delta.tables.DeltaTable.forPath(sourceLog.dataPath.toString).clone(\n        target = targetDir.getCanonicalPath,\n        isShallow = isShallowClone)\n\n      // We should not create any DV tombstones at this point since the shallow cloned table\n      // references the source table's data files.\n      val expectedDVTombstoneCount1 = Some(0)\n      dropDeletionVectors(targetLog)\n      validateTombstones(targetLog, expectedDVTombstoneCount1)\n\n      // Re-enable DVs in the target table.\n      sql(\n        s\"\"\"ALTER TABLE delta.`${targetDir.getAbsolutePath}`\n           |SET TBLPROPERTIES (\n           |delta.feature.${DeletionVectorsTableFeature.name} = 'enabled',\n           |${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key} = 'true'\n           |)\"\"\".stripMargin)\n\n      // Deleting rows causes shallow clone tables to create local files.\n      withSQLConf(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> \"true\") {\n        val targetTable = io.delta.tables.DeltaTable.forPath(targetDir.toString)\n        targetTable.delete(\"id > 20 and id <= 30\")\n        assert(targetLog.update().numDeletionVectorsOpt === Some(2L))\n      }\n\n      sql(s\"ALTER TABLE delta.`${targetDir.getAbsolutePath}` DROP FEATURE deletionVectors\")\n\n      // Verify that the DV tombstones created exactly match the unique DV paths\n      // found in files with DVs.\n      val expectedDVTombstoneCount2 = Some(1)\n      validateTombstones(targetLog, expectedDVTombstoneCount2)\n    }\n  }\n\n  test(\"We do not create tombstones when there are no RemoveFiles within the retention period\") {\n    withTempPath { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getAbsolutePath), clock)\n      createTableWithDeletionVectors(deltaLog)\n\n      // Pretend tombstone retention period has passed (default 1 week).\n      val clockAdvanceMillis = DeltaLog.tombstoneRetentionMillis(deltaLog.update().metadata)\n      clock.advance(clockAdvanceMillis + TimeUnit.DAYS.toMillis(3))\n\n      dropDeletionVectors(deltaLog)\n\n      assert(deltaLog.update().tombstones.collect().forall(_.isDVTombstone == false))\n    }\n  }\n\n  test(\"We create DV tombstones when mixing drop feature implementations\") {\n    // When using the TRUNCATE HISTORY option we fallback to the legacy implementation.\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      createTableWithDeletionVectors(deltaLog)\n\n      val e = intercept[DeltaTableFeatureException] {\n        dropDeletionVectors(deltaLog, truncateHistory = true)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> \"deletionVectors\",\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n\n      validateTombstones(deltaLog)\n      dropDeletionVectors(deltaLog, truncateHistory = false)\n      validateTombstones(deltaLog)\n    }\n  }\n\n  test(\"DV tombstones are not created for inline DVs\") {\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> true.toString) {\n      withTempPath { dir =>\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        val targetTable = () => io.delta.tables.DeltaTable.forPath(dir.toString)\n\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").save(dir.toString)\n\n        def removeRowsWithInlineDV(add: AddFile, markedRows: Long*): (AddFile, RemoveFile) = {\n          val bitmap = RoaringBitmapArray(markedRows: _*)\n          val serializedBitmap = bitmap.serializeAsByteArray(RoaringBitmapArrayFormat.Portable)\n          val cardinality = markedRows.size\n          val dv = DeletionVectorDescriptor.inlineInLog(serializedBitmap, cardinality)\n\n          add.removeRows(\n            deletionVector = dv,\n            updateStats = true)\n        }\n\n        // There should be a single AddFile.\n        val snapshot = deltaLog.update()\n        val addFile = snapshot.allFiles.first()\n        val (newAddFile, newRemoveFile) = removeRowsWithInlineDV(addFile, 3, 34, 67)\n        val actionsToCommit: Seq[Action] = Seq(newAddFile, newRemoveFile)\n\n        deltaLog.startTransaction(catalogTableOpt = None, snapshotOpt = Some(snapshot))\n          .commit(actionsToCommit, new DeltaOperations.TestOperation)\n\n        // Verify the table is\n        assert(deltaLog.update().numDeletedRecordsOpt.exists(_ === 3))\n        assert(deltaLog.update().numDeletionVectorsOpt.exists(_ === 1))\n\n        assert(targetTable().toDF.collect().length === 97)\n\n        dropDeletionVectors(deltaLog)\n        // No DV tombstones should be have been created.\n        assert(deltaLog.update().tombstones.collect().forall(_.isDVTombstone == false))\n\n        assert(targetTable().toDF.collect().length === 97)\n        assert(deltaLog.update().numDeletedRecordsOpt.forall(_ === 0))\n        assert(deltaLog.update().numDeletionVectorsOpt.forall(_ === 0))\n      }\n    }\n  }\n\n  for (generateDVTombstones <- BOOLEAN_DOMAIN)\n  test(s\"Vacuum does not delete deletion vector files.\" +\n      s\"generateDVTombstones: $generateDVTombstones\") {\n    val targetDF = spark.range(start = 0, end = 100, step = 1, numPartitions = 4)\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> true.toString,\n        DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> true.toString,\n        DeltaSQLConf.FAST_DROP_FEATURE_GENERATE_DV_TOMBSTONES.key -> generateDVTombstones.toString,\n        // With this config we pretend the client does not support DVs. Therefore, it will not\n        // discover DVs from the RemoveFile actions.\n        DeltaSQLConf.FAST_DROP_FEATURE_DV_DISCOVERY_IN_VACUUM_DISABLED.key -> true.toString) {\n      withTempPath { dir =>\n        val targetLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        targetDF.write.format(\"delta\").save(dir.toString)\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.toString)\n\n        // Add some DVs.\n        targetTable.delete(\"id < 5 or id >= 95\")\n        val versionWithDVs = targetLog.update().version\n\n        // Unfortunately, there is no point in advancing the clock because the deletion timestamps\n        // in the RemoveFiles do not use the clock. Instead, we set the creation time back 10 days\n        // to all files created so far. These will be eligible for vacuum.\n        val fs = targetLog.logPath.getFileSystem(targetLog.newDeltaHadoopConf())\n        val allFiles = DeltaFileOperations.localListDirs(\n          hadoopConf = targetLog.newDeltaHadoopConf(),\n          dirs = Seq(dir.getCanonicalPath),\n          recursive = false)\n        allFiles.foreach { p =>\n          fs.setTimes(p.getHadoopPath, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(10), 0)\n        }\n\n        // Add new DVs for the same set of files.\n        targetTable.delete(\"id < 10 or id >= 90\")\n\n        // Assert that DVs exist.\n        assert(targetLog.update().numDeletionVectorsOpt === Some(2L))\n\n        sql(s\"ALTER TABLE delta.`${dir.getAbsolutePath}` DROP FEATURE deletionVectors\")\n\n        val snapshot = targetLog.update()\n        val protocol = snapshot.protocol\n        assert(snapshot.numDeletionVectorsOpt.getOrElse(0L) === 0)\n        assert(snapshot.numDeletedRecordsOpt.getOrElse(0L) === 0)\n        assert(!protocol.readerFeatureNames.contains(DeletionVectorsTableFeature.name))\n\n        targetTable.delete(\"id < 15 or id >= 85\")\n\n        // The DV files are outside the retention period. However, the DVs are still referenced in\n        // the history. Normally we should not delete any DVs.\n        sql(s\"VACUUM '${dir.getAbsolutePath}'\")\n\n        val query =\n          sql(s\"SELECT * FROM delta.`${dir.getAbsolutePath}` VERSION AS OF $versionWithDVs\")\n\n        if (generateDVTombstones) {\n          // At version 1 we only deleted 10 rows.\n          assert(query.collect().length === 90)\n        } else {\n          val e = intercept[SparkException] {\n            query.collect()\n          }\n          val msg = e.getCause.getMessage\n          assert(msg.contains(\"RowIndexFilterFileNotFoundException\") ||\n            msg.contains(\".bin does not exist\"))\n        }\n      }\n    }\n  }\n\n  test(\"DV tombstones do not generate CDC\") {\n    import org.apache.spark.sql.delta.commands.cdc.CDCReader\n    withTempPath { dir =>\n      withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> true.toString) {\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        createTableWithDeletionVectors(deltaLog)\n        val versionBeforeDrop = deltaLog.update().version\n        dropDeletionVectors(deltaLog)\n\n        val deleteCountInDropFeature = CDCReader\n          .changesToBatchDF(deltaLog, versionBeforeDrop + 1, deltaLog.update().version, spark)\n          .filter(s\"${CDCReader.CDC_TYPE_COLUMN_NAME} = '${CDCReader.CDC_TYPE_DELETE_STRING}'\")\n          .count()\n        assert(deleteCountInDropFeature === 0)\n      }\n    }\n  }\n\n  for (incrementalCommitEnabled <- BOOLEAN_DOMAIN)\n  test(\"Checksum computation does not take into account DV tombstones\" +\n      s\"incrementalCommitEnabled: $incrementalCommitEnabled\") {\n    withTempPaths(2) { dirs =>\n      var checksumWithDVTombstones: VersionChecksum = null\n      withSQLConf(\n          DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> incrementalCommitEnabled.toString,\n          DeltaSQLConf.FAST_DROP_FEATURE_GENERATE_DV_TOMBSTONES.key -> true.toString) {\n        val deltaLog = DeltaLog.forTable(spark, dirs.head.getAbsolutePath)\n        createTableWithDeletionVectors(deltaLog)\n        dropDeletionVectors(deltaLog)\n        val snapshot = deltaLog.update()\n        checksumWithDVTombstones = snapshot.checksumOpt.getOrElse(snapshot.computeChecksum)\n      }\n\n      var checksumWithoutDVTombstones: VersionChecksum = null\n      withSQLConf(\n          DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> incrementalCommitEnabled.toString,\n          DeltaSQLConf.FAST_DROP_FEATURE_GENERATE_DV_TOMBSTONES.key -> false.toString) {\n        val deltaLog = DeltaLog.forTable(spark, dirs.last.getAbsolutePath)\n        createTableWithDeletionVectors(deltaLog)\n        dropDeletionVectors(deltaLog)\n        val snapshot = deltaLog.update()\n        checksumWithoutDVTombstones = snapshot.checksumOpt.getOrElse(snapshot.computeChecksum)\n      }\n\n      // DV tombstones do not affect the number of files.\n      assert(checksumWithoutDVTombstones.numFiles === checksumWithDVTombstones.numFiles)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaGenerateSymlinkManifestSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.net.URI\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaOperations.Delete\nimport org.apache.spark.sql.delta.commands.DeltaGenerateCommand\nimport org.apache.spark.sql.delta.hooks.GenerateSymlinkManifest\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs._\nimport org.apache.hadoop.fs.permission.FsPermission\nimport org.apache.hadoop.util.Progressable\n\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n// scalastyle:on import.ordering.noEmptyLine\n\nclass DeltaGenerateSymlinkManifestSuite\n  extends DeltaGenerateSymlinkManifestSuiteBase\n  with DeltaSQLCommandTest\n\ntrait DeltaGenerateSymlinkManifestSuiteBase\n  extends DeltaGenerateSymlinkManifestTestHelper\n  with DeletionVectorsTestUtils\n  with DeltaTestUtilsForTempViews {\n\n  import testImplicits._\n\n  test(\"basic case: SQL command - path-based table\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      spark.createDataset(spark.sparkContext.parallelize(1 to 100, 7))\n        .write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n\n      // Create a Delta table and call the scala api for generating manifest files\n      spark.sql(s\"GENERATE symlink_ForMat_Manifest FOR TABLE delta.`${tablePath.getAbsolutePath}`\")\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 7)\n    }\n  }\n\n  test(\"basic case: SQL command - name-based table\") {\n    val tableName = \"deltaTable\"\n    withTable(\"deltaTable\") {\n      spark.createDataset(spark.sparkContext.parallelize(1 to 100, 7))\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      assertManifest(tableName, expectSameFiles = false, expectedNumFiles = 0)\n\n      spark.sql(s\"GENERATE symlink_ForMat_Manifest FOR TABLE $tableName\")\n      assertManifest(tableName, expectSameFiles = true, expectedNumFiles = 7)\n    }\n  }\n\n  test(\"basic case: SQL command - throw error on bad tables\") {\n    var e: Exception = intercept[AnalysisException] {\n      spark.sql(\"GENERATE symlink_format_manifest FOR TABLE nonExistentTable\")\n    }\n    assert(e.getMessage.contains(\"not found\") || e.getMessage.contains(\"cannot be found\"))\n\n    withTable(\"nonDeltaTable\") {\n      spark.range(2).write.format(\"parquet\").saveAsTable(\"nonDeltaTable\")\n      e = intercept[AnalysisException] {\n        spark.sql(\"GENERATE symlink_format_manifest FOR TABLE nonDeltaTable\")\n      }\n      assert(e.getMessage.contains(\"only supported for Delta\"))\n    }\n  }\n\n  test(\"basic case: SQL command - throw error on non delta table paths\") {\n    withTempDir { dir =>\n      var e = intercept[AnalysisException] {\n        spark.sql(s\"GENERATE symlink_format_manifest FOR TABLE delta.`$dir`\")\n      }\n\n      assert(e.getMessage.contains(\"is not a Delta table\"))\n\n      spark.range(2).write.format(\"parquet\").mode(\"overwrite\").save(dir.toString)\n\n      e = intercept[AnalysisException] {\n        spark.sql(s\"GENERATE symlink_format_manifest FOR TABLE delta.`$dir`\")\n      }\n      assert(e.getMessage.contains(\"is not a Delta table\"))\n\n      e = intercept[AnalysisException] {\n        spark.sql(s\"GENERATE symlink_format_manifest FOR TABLE parquet.`$dir`\")\n      }\n      assert(e.getMessage.contains(\"not found\") || e.getMessage.contains(\"cannot be found\"))\n    }\n  }\n\n  testWithTempView(\"basic case: SQL command - throw error on temp views\") { isSQLTempView =>\n    withTable(\"t1\") {\n      spark.range(2).write.format(\"delta\").saveAsTable(\"t1\")\n      createTempViewFromTable(\"t1\", isSQLTempView)\n      val e = intercept[AnalysisException] {\n        spark.sql(s\"GENERATE symlink_format_manifest FOR TABLE v\")\n      }\n      assert(e.getMessage.contains(\"'GENERATE' expects a table but `v` is a view.\"))\n    }\n  }\n\n  test(\"basic case: SQL command - throw error on unsupported mode\") {\n    withTempDir { tablePath =>\n      spark.range(2).write.format(\"delta\").save(tablePath.getAbsolutePath)\n      val e = intercept[IllegalArgumentException] {\n        spark.sql(s\"GENERATE xyz FOR TABLE delta.`${tablePath.getAbsolutePath}`\")\n      }\n      assert(e.toString.contains(\"not supported\"))\n      DeltaGenerateCommand.modeNameToGenerationFunc.keys.foreach { modeName =>\n        assert(e.toString.contains(modeName))\n      }\n    }\n  }\n\n  test(\"basic case: Scala API - path-based table\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      spark.createDataset(spark.sparkContext.parallelize(1 to 100, 7))\n        .write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n\n      // Create a Delta table and call the scala api for generating manifest files\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tablePath.getAbsolutePath)\n      deltaTable.generate(\"symlink_format_manifest\")\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 7)\n    }\n  }\n\n  test(\"basic case: Scala API - name-based table\") {\n    val tableName = \"deltaTable\"\n    withTable(tableName) {\n      spark.createDataset(spark.sparkContext.parallelize(1 to 100, 7))\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      assertManifest(tableName, expectSameFiles = false, expectedNumFiles = 0)\n\n      val deltaTable = io.delta.tables.DeltaTable.forName(tableName)\n      deltaTable.generate(\"symlink_format_manifest\")\n      assertManifest(tableName, expectSameFiles = true, expectedNumFiles = 7)\n    }\n  }\n\n\n  test(\"full manifest: non-partitioned table\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      def write(parallelism: Int): Unit = {\n        spark.createDataset(spark.sparkContext.parallelize(1 to 100, parallelism))\n          .write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n      }\n\n      write(7)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 7)\n\n      // Reduce files\n      write(5)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 7)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 5)\n\n      // Remove all data\n      spark.emptyDataset[Int].write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 5)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(\n        tablePath, expectSameFiles = true, expectedNumFiles = 0)\n      assert(spark.read.format(\"delta\").load(tablePath.toString).count() == 0)\n\n      // delete all data\n      write(5)\n      assertManifest(\n        tablePath, expectSameFiles = false, expectedNumFiles = 0)\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tablePath.toString)\n      deltaTable.delete()\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 0)\n      assert(spark.read.format(\"delta\").load(tablePath.toString).count() == 0)\n    }\n  }\n\n  test(\"full manifest: partitioned table\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      def write(parallelism: Int, partitions1: Int, partitions2: Int): Unit = {\n        spark.createDataset(spark.sparkContext.parallelize(1 to 100, parallelism)).toDF(\"value\")\n          .withColumn(\"part1\", $\"value\" % partitions1)\n          .withColumn(\"part2\", $\"value\" % partitions2)\n          .write.format(\"delta\").partitionBy(\"part1\", \"part2\")\n          .mode(\"overwrite\").save(tablePath.toString)\n      }\n\n      write(10, 10, 10)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n      generateSymlinkManifest(tablePath.toString)\n      // 10 files each in ../part1=X/part2=X/ for X = 0 to 9\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 100)\n\n      // Reduce # partitions on both dimensions\n      write(1, 1, 1)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 100)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 1)\n\n      // Increase # partitions on both dimensions\n      write(5, 5, 5)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 1)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 25)\n\n      // Increase # partitions on only one dimension\n      write(5, 10, 5)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 25)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 50)\n\n      // Remove all data\n      spark.emptyDataset[Int].toDF(\"value\")\n        .withColumn(\"part1\", $\"value\" % 10)\n        .withColumn(\"part2\", $\"value\" % 10)\n        .write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 50)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 0)\n      assert(spark.read.format(\"delta\").load(tablePath.toString).count() == 0)\n\n      // delete all data\n      write(5, 5, 5)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 25)\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tablePath.toString)\n      deltaTable.delete()\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 0)\n      assert(spark.read.format(\"delta\").load(tablePath.toString).count() == 0)\n    }\n  }\n\n  test(\"incremental manifest: table property controls post commit manifest generation\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      def writeWithIncrementalManifest(enabled: Boolean, numFiles: Int): Unit = {\n        withIncrementalManifest(tablePath, enabled) {\n          spark.createDataset(spark.sparkContext.parallelize(1 to 100, numFiles))\n            .write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n        }\n      }\n\n      writeWithIncrementalManifest(enabled = false, numFiles = 1)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n\n      // Enabling it should automatically generate manifest files\n      writeWithIncrementalManifest(enabled = true, numFiles = 2)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 2)\n\n      // Disabling it should stop updating existing manifest files\n      writeWithIncrementalManifest(enabled = false, numFiles = 3)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 2)\n    }\n  }\n\n  test(\"incremental manifest: unpartitioned table\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      def write(numFiles: Int): Unit = withIncrementalManifest(tablePath, enabled = true) {\n        spark.createDataset(spark.sparkContext.parallelize(1 to 100, numFiles))\n          .write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n      }\n\n      write(1)\n      // first write won't generate automatic manifest as mode enable after first write\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n\n      // Increase files\n      write(7)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 7)\n\n      // Reduce files\n      write(5)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 5)\n\n      // Remove all data\n      spark.emptyDataset[Int].write.format(\"delta\").mode(\"overwrite\").save(tablePath.toString)\n      assert(spark.read.format(\"delta\").load(tablePath.toString).count() == 0)\n      assertManifest(\n        tablePath, expectSameFiles = true, expectedNumFiles = 0)\n    }\n  }\n\n  test(\"incremental manifest: partitioned table\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      def writePartitioned(parallelism: Int, numPartitions1: Int, numPartitions2: Int): Unit = {\n        withIncrementalManifest(tablePath, enabled = true) {\n          val input =\n            if (parallelism == 0) spark.emptyDataset[Int]\n            else spark.createDataset(spark.sparkContext.parallelize(1 to 100, parallelism))\n          input.toDF(\"value\")\n            .withColumn(\"part1\", $\"value\" % numPartitions1)\n            .withColumn(\"part2\", $\"value\" % numPartitions2)\n            .write.format(\"delta\").partitionBy(\"part1\", \"part2\")\n            .mode(\"overwrite\").save(tablePath.toString)\n        }\n      }\n\n      writePartitioned(1, 1, 1)\n      // Manifests wont be generated in the first write because `withIncrementalManifest` will\n      // enable manifest generation only after the first write defines the table log.\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n\n      writePartitioned(10, 10, 10)\n      // 10 files each in ../part1=X/part2=X/ for X = 0 to 9 (so only 10 subdirectories)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 100)\n\n      // Update such that 1 file is removed and 1 file is added in another partition\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tablePath.toString)\n      deltaTable.updateExpr(\"value = 1\", Map(\"part1\" -> \"0\", \"value\" -> \"-1\"))\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 100)\n\n      // Delete such that 1 file is removed\n      deltaTable.delete(\"value = -1\")\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 99)\n\n      // Reduce # partitions on both dimensions\n      writePartitioned(1, 1, 1)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 1)\n\n      // Increase # partitions on both dimensions\n      writePartitioned(5, 5, 5)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 25)\n\n      // Increase # partitions on only one dimension\n      writePartitioned(5, 10, 5)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 50)\n\n      // Remove all data\n      writePartitioned(0, 1, 1)\n      assert(spark.read.format(\"delta\").load(tablePath.toString).count() == 0)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 0)\n    }\n  }\n\n  test(\"incremental manifest: generate full manifest if manifest did not exist\") {\n    withTempDir { tablePath =>\n\n      def write(numPartitions: Int): Unit = {\n        spark.range(0, 100, 1, 1).toDF(\"value\").withColumn(\"part\", $\"value\" % numPartitions)\n          .write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(tablePath.toString)\n      }\n\n      write(10)\n      assertManifest(tablePath, expectSameFiles = false, expectedNumFiles = 0)\n\n      withIncrementalManifest(tablePath, enabled = true) {\n        write(1)  // update only one partition\n      }\n      // Manifests should be generated for all partitions\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 11)\n    }\n  }\n\n  test(\"incremental manifest: failure to generate manifest throws exception\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n\n      import SymlinkManifestFailureTestFileSystem._\n\n      withSQLConf(\n          s\"fs.$SCHEME.impl\" -> classOf[SymlinkManifestFailureTestFileSystem].getName,\n          s\"fs.$SCHEME.impl.disable.cache\" -> \"true\",\n          s\"fs.AbstractFileSystem.$SCHEME.impl\" ->\n            classOf[SymlinkManifestFailureTestAbstractFileSystem].getName,\n          s\"fs.AbstractFileSystem.$SCHEME.impl.disable.cache\" -> \"true\") {\n        def write(numFiles: Int): Unit = withIncrementalManifest(tablePath, enabled = true) {\n          spark.createDataset(spark.sparkContext.parallelize(1 to 100, numFiles))\n            .write.format(\"delta\").mode(\"overwrite\").save(s\"$SCHEME://$tablePath\")\n        }\n\n        val manifestPath = new File(tablePath, GenerateSymlinkManifest.MANIFEST_LOCATION)\n        require(!manifestPath.exists())\n        write(1) // first write enables the property does not write any file\n        require(!manifestPath.exists())\n\n        val ex = catalyst.util.quietly {\n          intercept[RuntimeException] { write(2) }\n        }\n\n        assert(ex.getMessage().contains(GenerateSymlinkManifest.name))\n        assert(ex.getCause().toString.contains(\"Test exception\"))\n      }\n    }\n  }\n\n  test(\"special partition column names\") {\n\n    def assertColNames(inputStr: String): Unit = withClue(s\"input: $inputStr\") {\n      withTempDir { tablePath =>\n        tablePath.delete()\n        val inputLines = inputStr.trim.stripMargin.trim.split(\"\\n\").toSeq\n        require(inputLines.size > 0)\n        val input = spark.read.json(inputLines.toDS)\n        val partitionCols = input.schema.fieldNames\n        val inputWithValue = input.withColumn(\"value\", lit(1))\n\n        inputWithValue.write.format(\"delta\").partitionBy(partitionCols: _*).save(tablePath.toString)\n        generateSymlinkManifest(tablePath.toString)\n        assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = inputLines.size)\n      }\n    }\n\n    intercept[AnalysisException] {\n      assertColNames(\"\"\"{ \" \" : 0 }\"\"\")\n    }\n    assertColNames(\"\"\"{ \"%\" : 0 }\"\"\")\n    assertColNames(\"\"\"{ \"a.b.\" : 0 }\"\"\")\n    assertColNames(\"\"\"{ \"a/b.\" : 0 }\"\"\")\n    assertColNames(\"\"\"{ \"a_b\" : 0 }\"\"\")\n    intercept[AnalysisException] {\n      assertColNames(\"\"\"{ \"a b\" : 0 }\"\"\")\n    }\n  }\n\n  test(\"special partition column values\") {\n    withTempDir { tablePath =>\n      tablePath.delete()\n      val inputStr = \"\"\"\n          |{ \"part1\" : 1,    \"part2\": \"$0$\", \"value\" : 1 }\n          |{ \"part1\" : null, \"part2\": \"_1_\", \"value\" : 1 }\n          |{ \"part1\" : 1,    \"part2\": \"\",    \"value\" : 1 }\n          |{ \"part1\" : null, \"part2\": \" \",   \"value\" : 1 }\n          |{ \"part1\" : 1,    \"part2\": \"  \",  \"value\" : 1 }\n          |{ \"part1\" : null, \"part2\": \"/\",   \"value\" : 1 }\n          |{ \"part1\" : 1,    \"part2\": null,  \"value\" : 1 }\n          |\"\"\"\n      val input = spark.read.json(inputStr.trim.stripMargin.trim.split(\"\\n\").toSeq.toDS)\n      input.write.format(\"delta\").partitionBy(\"part1\", \"part2\").save(tablePath.toString)\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 7)\n    }\n  }\n\n  test(\"root table path with escapable chars like space\") {\n    withTempDir { p =>\n      val tablePath = new File(p.toString, \"path with space\")\n      spark.createDataset(spark.sparkContext.parallelize(1 to 100, 1)).toDF(\"value\")\n        .withColumn(\"part\", $\"value\" % 2)\n        .write.format(\"delta\").partitionBy(\"part\").save(tablePath.toString)\n\n      generateSymlinkManifest(tablePath.toString)\n      assertManifest(tablePath, expectSameFiles = true, expectedNumFiles = 2)\n    }\n  }\n\n  test(\"block manifest generation with persistent DVs\") {\n    withDeletionVectorsEnabled() {\n      val rowsToBeRemoved = Seq(1L, 42L, 43L)\n\n      withTempDir { dir =>\n        val tablePath = dir.getAbsolutePath\n        // Write in 2 files.\n        spark.range(end = 50L).toDF(\"id\").coalesce(1)\n          .write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n        spark.range(start = 50L, end = 100L).toDF(\"id\").coalesce(1)\n          .write.format(\"delta\").mode(\"append\").save(tablePath)\n        val deltaLog = DeltaLog.forTable(spark, tablePath)\n        assert(deltaLog.snapshot.allFiles.count() === 2L)\n\n        // Step 1: Make sure generation works on DV enabled tables without a DV in the snapshot.\n        // Delete an entire file, which can't produce DVs.\n        spark.sql(s\"\"\"DELETE FROM delta.`$tablePath` WHERE id BETWEEN 0 and 49\"\"\")\n        val remainingFiles = deltaLog.snapshot.allFiles.collect()\n        assert(remainingFiles.size === 1L)\n        assert(remainingFiles(0).deletionVector === null)\n        // Should work fine, since the snapshot doesn't contain DVs.\n        spark.sql(s\"\"\"GENERATE symlink_format_manifest FOR TABLE delta.`$tablePath`\"\"\")\n\n        // Step 2: Make sure generation fails if there are DVs in the snapshot.\n\n        // This is needed to make the manual commit work correctly, since we are not actually\n        // running a command that produces metrics.\n        withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"false\") {\n          val txn = deltaLog.startTransaction()\n          assert(txn.snapshot.allFiles.count() === 1)\n          val file = txn.snapshot.allFiles.collect().head\n          val actions = removeRowsFromFileUsingDV(deltaLog, file, rowIds = rowsToBeRemoved)\n          txn.commit(actions, Delete(predicate = Seq.empty))\n        }\n        val e = intercept[DeltaCommandUnsupportedWithDeletionVectorsException] {\n          spark.sql(s\"\"\"GENERATE symlink_format_manifest FOR TABLE delta.`$tablePath`\"\"\")\n        }\n        checkErrorHelper(\n          exception = e,\n          errorClass = \"DELTA_UNSUPPORTED_GENERATE_WITH_DELETION_VECTORS\")\n      }\n    }\n  }\n\n  private def setEnabledIncrementalManifest(tablePath: String, enabled: Boolean): Unit = {\n    spark.sql(s\"ALTER TABLE delta.`$tablePath` \" +\n      s\"SET TBLPROPERTIES('${DeltaConfigs.SYMLINK_FORMAT_MANIFEST_ENABLED.key}'='$enabled')\")\n  }\n\n  test(\"block incremental manifest generation with persistent DVs\") {\n    import DeltaTablePropertyValidationFailedSubClass._\n\n    def expectConstraintViolation(subClass: DeltaTablePropertyValidationFailedSubClass)\n        (thunk: => Unit): Unit = {\n      val e = intercept[DeltaTablePropertyValidationFailedException] {\n        thunk\n      }\n      checkErrorHelper(\n        exception = e,\n        errorClass = \"DELTA_VIOLATE_TABLE_PROPERTY_VALIDATION_FAILED.\" + subClass.tag\n      )\n    }\n\n    withDeletionVectorsEnabled() {\n      val rowsToBeRemoved = Seq(1L, 42L, 43L)\n\n      withTempDir { dir =>\n        val tablePath = dir.getAbsolutePath\n        spark.range(end = 100L).toDF(\"id\").coalesce(1)\n          .write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n        val deltaLog = DeltaLog.forTable(spark, tablePath)\n\n        // Make sure both properties can't be enabled together.\n        enableDeletionVectorsInTable(new Path(tablePath), enable = true)\n        expectConstraintViolation(\n            subClass = PersistentDeletionVectorsWithIncrementalManifestGeneration) {\n          setEnabledIncrementalManifest(tablePath, enabled = true)\n        }\n        // Or in the other order.\n        enableDeletionVectorsInTable(new Path(tablePath), enable = false)\n        setEnabledIncrementalManifest(tablePath, enabled = true)\n        expectConstraintViolation(\n            subClass = PersistentDeletionVectorsWithIncrementalManifestGeneration)  {\n          enableDeletionVectorsInTable(new Path(tablePath), enable = true)\n        }\n        setEnabledIncrementalManifest(tablePath, enabled = false)\n        // Or both at once.\n        expectConstraintViolation(\n            subClass = PersistentDeletionVectorsWithIncrementalManifestGeneration)  {\n          spark.sql(s\"ALTER TABLE delta.`$tablePath` \" +\n            s\"SET TBLPROPERTIES('${DeltaConfigs.SYMLINK_FORMAT_MANIFEST_ENABLED.key}'='true',\" +\n            s\" '${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = 'true')\")\n        }\n\n        // If DVs were allowed at some point and are still present in the table,\n        // enabling incremental manifest generation must still fail.\n        enableDeletionVectorsInTable(new Path(tablePath), enable = true)\n        withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"false\") {\n          val txn = deltaLog.startTransaction()\n          assert(txn.snapshot.allFiles.count() === 1)\n          val file = txn.snapshot.allFiles.collect().head\n          val actions = removeRowsFromFileUsingDV(deltaLog, file, rowIds = rowsToBeRemoved)\n          txn.commit(actions, Delete(predicate = Seq.empty))\n        }\n        assert(getFilesWithDeletionVectors(deltaLog).nonEmpty)\n        enableDeletionVectorsInTable(new Path(tablePath), enable = false)\n        expectConstraintViolation(\n            subClass = ExistingDeletionVectorsWithIncrementalManifestGeneration)  {\n          setEnabledIncrementalManifest(tablePath, enabled = true)\n        }\n        // Purge\n        spark.sql(s\"REORG TABLE delta.`$tablePath` APPLY (PURGE)\")\n        assert(getFilesWithDeletionVectors(deltaLog).isEmpty)\n        // Now it should work.\n        setEnabledIncrementalManifest(tablePath, enabled = true)\n\n        // As a last fallback, in case some other writer put the table into an illegal state,\n        // we still need to fail the manifest generation if there are DVs.\n        // Reset table.\n        setEnabledIncrementalManifest(tablePath, enabled = false)\n        enableDeletionVectorsInTable(new Path(tablePath), enable = false)\n        spark.range(end = 100L).toDF(\"id\").coalesce(1)\n          .write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n        // Add DVs\n        enableDeletionVectorsInTable(new Path(tablePath), enable = true)\n        withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"false\") {\n          val txn = deltaLog.startTransaction()\n          assert(txn.snapshot.allFiles.count() === 1)\n          val file = txn.snapshot.allFiles.collect().head\n          val actions = removeRowsFromFileUsingDV(deltaLog, file, rowIds = rowsToBeRemoved)\n          txn.commit(actions, Delete(predicate = Seq.empty))\n        }\n        // Force enable manifest generation.\n        withSQLConf(DeltaSQLConf.DELTA_TABLE_PROPERTY_CONSTRAINTS_CHECK_ENABLED.key -> \"false\") {\n          setEnabledIncrementalManifest(tablePath, enabled = true)\n        }\n        val e2 = intercept[DeltaCommandUnsupportedWithDeletionVectorsException] {\n          spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n        }\n        checkErrorHelper(\n          exception = e2,\n          errorClass = \"DELTA_UNSUPPORTED_GENERATE_WITH_DELETION_VECTORS\")\n        // This is fine, since the new snapshot won't contain DVs.\n        spark.range(10).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n\n        // Make sure we can get the table back into a consistent state, as well\n        setEnabledIncrementalManifest(tablePath, enabled = false)\n        // No more exception.\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(tablePath)\n      }\n    }\n  }\n\n  private def checkErrorHelper(\n      exception: SparkThrowable,\n      errorClass: String\n  ): Unit = {\n    assert(exception.getErrorClass === errorClass,\n      s\"Expected errorClass $errorClass, but got $exception\")\n  }\n\n  Seq(true, false).foreach { useIncremental =>\n    test(s\"delete partition column with special char - incremental=$useIncremental\") {\n\n      def writePartition(dir: File, partName: String): Unit = {\n        spark.range(10)\n          .withColumn(\"part\", lit(partName))\n          .repartition(1)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .partitionBy(\"part\")\n          .save(dir.toString)\n      }\n\n      withTempDir { dir =>\n        // create table and write first manifest\n        writePartition(dir, \"noSpace\")\n        generateSymlinkManifest(dir.toString)\n\n        withIncrementalManifest(dir, useIncremental) {\n          // 1. test paths with spaces\n          writePartition(dir, \"yes space\")\n\n          if (!useIncremental) { generateSymlinkManifest(dir.toString) }\n          assertManifest(dir, expectSameFiles = true, expectedNumFiles = 2)\n\n          // delete partition\n          sql(s\"\"\"DELETE FROM delta.`${dir.toString}` WHERE part=\"yes space\";\"\"\")\n\n          if (!useIncremental) { generateSymlinkManifest(dir.toString) }\n          assertManifest(dir, expectSameFiles = true, expectedNumFiles = 1)\n\n          // 2. test special characters\n          // scalastyle:off nonascii\n          writePartition(dir, \"库尔 勒\")\n          if (!useIncremental) { generateSymlinkManifest(dir.toString) }\n          assertManifest(dir, expectSameFiles = true, expectedNumFiles = 2)\n\n          // delete partition\n          sql(s\"\"\"DELETE FROM delta.`${dir.toString}` WHERE part=\"库尔 勒\";\"\"\")\n          // scalastyle:on nonascii\n\n          if (!useIncremental) { generateSymlinkManifest(dir.toString) }\n          assertManifest(dir, expectSameFiles = true, expectedNumFiles = 1)\n        }\n      }\n    }\n  }\n}\n\ntrait DeltaGenerateSymlinkManifestTestHelper\n  extends QueryTest\n  with SharedSparkSession {\n\n  import testImplicits._\n\n  protected def assertManifest(\n    tablePath: File,\n    expectSameFiles: Boolean,\n    expectedNumFiles: Int): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, tablePath.toString)\n    assertManifest(snapshot, tablePath, expectSameFiles, expectedNumFiles)\n  }\n\n  protected def assertManifest(\n      tableName: String,\n      expectSameFiles: Boolean,\n      expectedNumFiles: Int): Unit = {\n    val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n    assertManifest(snapshot, new File(log.dataPath.toUri), expectSameFiles, expectedNumFiles)\n  }\n\n  /**\n   * Assert that the manifest files in the table meet the expectations.\n   * @param deltaSnapshot Snapshot of the Delta table to check against\n   * @param tablePath Path of the Delta table\n   * @param expectSameFiles Expect that the manifest files contain the same data files\n   *                        as the latest version of the table\n   * @param expectedNumFiles Expected number of manifest files\n   */\n  private def assertManifest(\n      deltaSnapshot: Snapshot,\n      tablePath: File,\n      expectSameFiles: Boolean,\n      expectedNumFiles: Int): Unit = {\n    val manifestPath = new File(tablePath, GenerateSymlinkManifest.MANIFEST_LOCATION)\n    if (!manifestPath.exists) {\n      assert(expectedNumFiles == 0 && !expectSameFiles)\n      return\n    }\n\n    // Validate the expected number of files are present in the manifest\n    val filesInManifest = spark.read.text(manifestPath.toString)\n      .select(\"value\")\n      .as[String]\n      .collect()\n      .map(_.stripPrefix(\"file:\"))\n      .toSeq\n      .toDF(\"file\")\n    assert(filesInManifest.count() == expectedNumFiles)\n\n    // Validate that files in the latest version of DeltaLog is same as those in the manifest\n    val filesInLog = deltaSnapshot.allFiles\n      .collect()\n      .map { addFile =>\n        // Note: this unescapes the relative path in `addFile`\n        DeltaFileOperations.absolutePath(tablePath.toString, addFile.path).toString\n      }\n      .toSeq\n      .toDF(\"file\")\n\n    if (expectSameFiles) {\n      checkAnswer(filesInManifest, filesInLog)\n\n      // Validate that each file in the manifest is actually present in table. This mainly checks\n      // whether the file names in manifest are not escaped and therefore are readable directly\n      // by Hadoop APIs.\n      val fs = new Path(manifestPath.toString)\n        .getFileSystem(deltaSnapshot.deltaLog.newDeltaHadoopConf())\n      spark.read.text(manifestPath.toString).select(\"value\").as[String].collect().foreach { p =>\n        assert(fs.exists(new Path(p)), s\"path $p in manifest not found in file system\")\n      }\n    } else {\n      assert(filesInManifest.as[String].collect().toSet != filesInLog.as[String].collect().toSet)\n    }\n\n    // If there are partitioned files, make sure the partitions values read from them are the\n    // same as those in the table.\n    val partitionCols = deltaSnapshot.metadata.partitionColumns.map(x => s\"`$x`\")\n    if (partitionCols.nonEmpty && expectSameFiles && expectedNumFiles > 0) {\n      val partitionsInManifest = spark.read.text(manifestPath.toString)\n        .selectExpr(partitionCols: _*).distinct()\n      val partitionsInData = spark.read.format(\"delta\").load(tablePath.toString)\n        .selectExpr(partitionCols: _*).distinct()\n      checkAnswer(partitionsInManifest, partitionsInData)\n    }\n  }\n\n  protected def withIncrementalManifest(tablePath: File, enabled: Boolean)(func: => Unit): Unit = {\n    if (tablePath.exists()) {\n      val latestMetadata = DeltaLog.forTable(spark, tablePath).update().metadata\n      if (DeltaConfigs.SYMLINK_FORMAT_MANIFEST_ENABLED.fromMetaData(latestMetadata) != enabled) {\n        spark.sql(s\"ALTER TABLE delta.`$tablePath` \" +\n          s\"SET TBLPROPERTIES(${DeltaConfigs.SYMLINK_FORMAT_MANIFEST_ENABLED.key}=$enabled)\")\n      }\n    }\n    func\n  }\n\n  protected def generateSymlinkManifest(tablePath: String): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, tablePath)\n    GenerateSymlinkManifest.generateFullManifest(spark, deltaLog, catalogTableOpt = None)\n  }\n}\n\nclass SymlinkManifestFailureTestAbstractFileSystem(\n    uri: URI,\n    conf: org.apache.hadoop.conf.Configuration)\n  extends org.apache.hadoop.fs.DelegateToFileSystem(\n    uri,\n    new SymlinkManifestFailureTestFileSystem,\n    conf,\n    SymlinkManifestFailureTestFileSystem.SCHEME,\n    false) {\n\n  // Implementation copied from RawLocalFs\n  import org.apache.hadoop.fs.local.LocalConfigKeys\n  import org.apache.hadoop.fs._\n\n  override def getUriDefaultPort(): Int = -1\n  override def getServerDefaults(): FsServerDefaults = LocalConfigKeys.getServerDefaults()\n  override def isValidName(src: String): Boolean = true\n}\n\n\nclass SymlinkManifestFailureTestFileSystem extends RawLocalFileSystem {\n\n  private var uri: URI = _\n  override def getScheme: String = SymlinkManifestFailureTestFileSystem.SCHEME\n\n  override def initialize(name: URI, conf: Configuration): Unit = {\n    uri = URI.create(name.getScheme + \":///\")\n    super.initialize(name, conf)\n  }\n\n  override def getUri(): URI = if (uri == null) {\n    // RawLocalFileSystem's constructor will call this one before `initialize` is called.\n    // Just return the super's URI to avoid NPE.\n    super.getUri\n  } else {\n    uri\n  }\n\n  // Override both create() method defined in RawLocalFileSystem such that any file creation\n  // throws error.\n\n  override def create(\n      path: Path,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream = {\n    if (path.toString.contains(GenerateSymlinkManifest.MANIFEST_LOCATION)) {\n      throw new RuntimeException(\"Test exception\")\n    }\n    super.create(path, overwrite, bufferSize, replication, blockSize, null)\n  }\n\n  override def create(\n      path: Path,\n      permission: FsPermission,\n      overwrite: Boolean,\n      bufferSize: Int,\n      replication: Short,\n      blockSize: Long,\n      progress: Progressable): FSDataOutputStream = {\n    if (path.toString.contains(GenerateSymlinkManifest.MANIFEST_LOCATION)) {\n      throw new RuntimeException(\"Test exception\")\n    }\n    super.create(path, permission, overwrite, bufferSize, replication, blockSize, progress)\n  }\n}\n\nobject SymlinkManifestFailureTestFileSystem {\n  val SCHEME = \"testScheme\"\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaHistoryManagerSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, FileNotFoundException}\nimport java.net.URI\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.sql.Timestamp\nimport java.text.SimpleDateFormat\nimport java.util.{Date, Locale}\n\nimport scala.concurrent.duration._\nimport scala.language.implicitConversions\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED\nimport org.apache.spark.sql.delta.DeltaTestUtils.{createTestAddFile, modifyCommitTimestamp}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatsUtils\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.util.quietly\nimport org.apache.spark.sql.connector.catalog.CatalogManager\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\n/** A set of tests which we can open source after Spark 3.0 is released. */\ntrait DeltaTimeTravelTests extends QueryTest\n    with SharedSparkSession\n    with GivenWhenThen\n    with DeltaSQLCommandTest\n    with StatsUtils\n    with CatalogOwnedTestBaseSuite {\n  protected implicit def durationToLong(duration: FiniteDuration): Long = {\n    duration.toMillis\n  }\n\n  protected implicit def longToTimestamp(ts: Long): Timestamp = new Timestamp(ts)\n\n  protected val timeFormatter = new SimpleDateFormat(\"yyyyMMddHHmmssSSS\")\n\n  protected def versionAsOf(table: String, version: Long): String = {\n    s\"$table version as of $version\"\n  }\n\n  protected def timestampAsOf(table: String, expr: String): String = {\n    s\"$table timestamp as of $expr\"\n  }\n\n  protected def verifyLogging(\n      tableVersion: Long,\n      queriedVersion: Long,\n      accessType: String,\n      apiUsed: String)(f: => Unit): Unit = {\n    // TODO: would be great to verify our logging metrics\n  }\n\n  protected def getTableLocation(table: String): String = {\n    spark.sessionState.catalog.getTableMetadata(TableIdentifier(table)).location.toString\n  }\n\n  /** Generate commits with the given timestamp in millis. */\n  protected def generateCommitsCheap(\n      deltaLog: DeltaLog, commits: Long*): Unit = {\n    var startVersion = deltaLog.snapshot.version + 1\n    commits.foreach { ts =>\n      val action =\n        createTestAddFile(encodedPath = startVersion.toString, modificationTime = startVersion)\n      deltaLog.startTransaction().commitManually(action)\n      modifyCommitTimestamp(deltaLog, startVersion, ts)\n      startVersion += 1\n    }\n  }\n\n  protected def generateCommitsAtPath(table: String, path: String, commits: Long*): Unit = {\n    generateCommitsBase(table, Some(path), commits: _*)\n  }\n\n  /** Generate commits with the given timestamp in millis. */\n  protected def generateCommits(table: String, commits: Long*): Unit = {\n    generateCommitsBase(table, None, commits: _*)\n  }\n\n  private def generateCommitsBase(table: String, path: Option[String], commits: Long*): Unit = {\n    var commitList = commits.toSeq\n    if (commitList.isEmpty) return\n    if (!spark.sessionState.catalog.tableExists(TableIdentifier(table))) {\n      if (path.isDefined) {\n        spark.range(0, 10).write.format(\"delta\")\n          .mode(\"append\")\n          .option(\"path\", path.get)\n          .saveAsTable(table)\n      } else {\n        spark.range(0, 10).write.format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(table)\n      }\n      val deltaLog = DeltaLog.forTable(spark, new TableIdentifier(table))\n      modifyCommitTimestamp(deltaLog, 0, commitList.head)\n      commitList = commits.slice(1, commits.length) // we already wrote the first commit here\n      var startVersion = deltaLog.snapshot.version + 1\n      commitList.foreach { ts =>\n        val rangeStart = startVersion * 10\n        val rangeEnd = rangeStart + 10\n        spark.range(rangeStart, rangeEnd).write.format(\"delta\").mode(\"append\").saveAsTable(table)\n        modifyCommitTimestamp(deltaLog, startVersion, ts)\n        startVersion += 1\n      }\n    }\n  }\n\n  /** Alternate for `withTables` as we leave some tables in an unusable state for clean up */\n  protected def withTable(tableName: String, dir: String)(f: => Unit): Unit = {\n    try f finally {\n      try {\n        Utils.deleteRecursively(new File(dir.toString))\n      } catch {\n        case _: Throwable =>\n          Nil // do nothing, this can fail if the table was deleted by the test.\n      } finally {\n        try {\n          sql(s\"DROP TABLE IF EXISTS $tableName\")\n        } catch {\n          case _: Throwable =>\n            // There is one test that fails the drop table as well\n            // we ignore this exception as that test uses a path based location.\n            Nil\n        }\n      }\n    }\n  }\n\n  protected implicit def longToTimestampExpr(value: Long): String = {\n    s\"cast($value / 1000 as timestamp)\"\n  }\n\n  import testImplicits._\n\n  test(\"time travel with partition changes and data skipping - should instantiate old schema\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val v0 = spark.range(10).withColumn(\"part5\", 'id % 5)\n\n      v0.write.format(\"delta\").partitionBy(\"part5\").mode(\"append\").save(tblLoc)\n      val deltaLog = DeltaLog.forTable(spark, tblLoc)\n\n      val schemaString = spark.range(10, 20).withColumn(\"part2\", 'id % 2).schema.json\n        deltaLog.startTransaction().commit(\n          Seq(deltaLog.snapshot.metadata.copy(\n            schemaString = schemaString,\n            partitionColumns = Seq(\"part2\"))),\n          DeltaOperations.ManualUpdate\n        )\n      checkAnswer(\n        spark.read.option(\"versionAsOf\", 0).format(\"delta\").load(tblLoc).where(\"part5 = 1\"),\n        v0.where(\"part5 = 1\"))\n    }\n  }\n\n  test(\"can't provide both version and timestamp in DataFrameReader\") {\n    val e = intercept[IllegalArgumentException] {\n      spark.read.option(\"versionaSof\", 1)\n          .option(\"timestampAsOF\", \"fake\").format(\"delta\").load(\"/some/fake\")\n    }\n    assert(e.getMessage.contains(\"either provide 'timestampAsOf' or 'versionAsOf'\"))\n  }\n\n\n  test(\"don't time travel a valid non-delta path with @ syntax\") {\n    val format = \"json\"\n    withTempDir { dir =>\n      val path = new File(dir, \"base@v0\").getCanonicalPath\n      spark.range(10).write.format(format).mode(\"append\").save(path)\n      spark.range(10).write.format(format).mode(\"append\").save(path)\n\n      checkAnswer(\n        spark.read.format(format).load(path),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      checkAnswer(\n        spark.table(s\"$format.`$path`\"),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      intercept[AnalysisException] {\n        spark.read.format(format).load(path + \"@v0\").count()\n      }\n\n      intercept[AnalysisException] {\n        spark.table(s\"$format.`$path@v0`\").count()\n      }\n    }\n  }\n\n  ///////////////////////////\n  // Time Travel SQL Tests //\n  ///////////////////////////\n\n  test(\"AS OF support does not impact non-delta tables\") {\n    withTable(\"t1\") {\n      spark.range(10).write.format(\"parquet\").mode(\"append\").saveAsTable(\"t1\")\n      spark.range(10, 20).write.format(\"parquet\").mode(\"append\").saveAsTable(\"t1\")\n\n      // We should still use the default, non-delta code paths for a non-delta table.\n      // For parquet, that means to fail with QueryCompilationErrors::tableNotSupportTimeTravelError\n      val e = intercept[Exception] {\n        spark.sql(\"SELECT * FROM t1 VERSION AS OF 0\")\n      }.getMessage\n      assert(e.contains(\"does not support time travel\") ||\n        e.contains(\"The feature is not supported: Time travel on the relation\"))\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  test(\"as of timestamp in between commits should use commit before timestamp\") {\n    // scalastyle:off line.size.limit\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblName, start, start + 20.minutes, start + 40.minutes)\n\n      verifyLogging(2L, 0L, \"timestamp\", \"sql\") {\n        checkAnswer(\n          sql(s\"select count(*) from ${timestampAsOf(tblName, start + 10.minutes)}\"),\n          Row(10L)\n        )\n      }\n\n\n      verifyLogging(2L, 0L, \"timestamp\", \"sql\") {\n        checkAnswer(\n          sql(\"select count(*) from \" +\n            s\"${timestampAsOf(s\"delta.`${getTableLocation(tblName)}`\", start + 10.minutes)}\"),\n          Row(10L)\n        )\n      }\n\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start + 30.minutes)}\"),\n        Row(20L)\n      )\n    }\n  }\n\n  test(\"as of timestamp on exact timestamp\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblName, start, start + 20.minutes)\n\n      // Simulate getting the timestamp directly from Spark SQL\n      val ts = Seq(new Timestamp(start), new Timestamp(start + 20.minutes)).toDF(\"ts\")\n        .select($\"ts\".cast(\"string\")).as[String].collect()\n        .map(i => s\"'$i'\")\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, ts(0))}\"),\n        Row(10L)\n      )\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start)}\"),\n        Row(10L)\n      )\n\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start + 20.minutes)}\"),\n        Row(20L)\n      )\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, ts(1))}\"),\n        Row(20L)\n      )\n    }\n  }\n\n  test(\"as of with versions\") {\n    val tblName = s\"delta_table\"\n    withTempDir { dir =>\n      withTable(tblName, dir.toString) {\n        val start = System.currentTimeMillis() - 5.days.toMillis\n        generateCommitsAtPath(tblName, dir.toString, start, start + 20.minutes, start + 40.minutes)\n        verifyLogging(2L, 0L, \"version\", \"sql\") {\n          checkAnswer(\n            sql(s\"select count(*) from ${versionAsOf(tblName, 0)}\"),\n            Row(10L)\n          )\n        }\n\n        verifyLogging(2L, 0L, \"version\", \"dfReader\") {\n          checkAnswer(\n            spark.read.format(\"delta\").option(\"versionAsOf\", \"0\")\n              .load(getTableLocation(tblName)).groupBy().count(),\n            Row(10)\n          )\n        }\n        checkAnswer(\n          sql(s\"select count(*) from ${versionAsOf(tblName, 1)}\"),\n          Row(20L)\n        )\n        checkAnswer(\n          spark.read.format(\"delta\").option(\"versionAsOf\", 1)\n            .load(getTableLocation(tblName)).groupBy().count(),\n          Row(20)\n        )\n        checkAnswer(\n          sql(s\"select count(*) from ${versionAsOf(tblName, 2)}\"),\n          Row(30L)\n        )\n        val e1 = intercept[AnalysisException] {\n          sql(s\"select count(*) from ${versionAsOf(tblName, 3)}\").collect()\n        }\n        assert(e1.getMessage.contains(\"[0, 2]\"))\n\n        val deltaLog = DeltaLog.forTable(spark, getTableLocation(tblName))\n        new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0).toUri).delete()\n        // Delta Lake will create a DeltaTableV2 explicitly with time travel options in the catalog.\n        // These options will be verified by DeltaHistoryManager, which will throw an\n        // AnalysisException.\n        val e2 = intercept[AnalysisException] {\n          sql(s\"select count(*) from ${versionAsOf(tblName, 0)}\").collect()\n        }\n        if (catalogOwnedCoordinatorBackfillBatchSize.exists(_ > 2)) {\n          assert(e2.getMessage.contains(\"No commits found at\"))\n        } else {\n          assert(e2.getMessage.contains(\"No recreatable commits found at\"))\n        }\n      }\n    }\n  }\n\n  test(\"as of exact timestamp after last commit should fail\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = 1540415658000L\n      generateCommits(tblName, start)\n\n      // Simulate getting the timestamp directly from Spark SQL\n      val ts = Seq(new Timestamp(start + 10.minutes)).toDF(\"ts\")\n        .select($\"ts\".cast(\"string\")).as[String].collect()\n        .map(i => s\"'$i'\")\n\n      val e1 = intercept[DeltaErrors.TemporallyUnstableInputException] {\n        sql(s\"select count(*) from ${timestampAsOf(tblName, ts(0))}\").collect()\n      }\n      checkError(\n        e1,\n        \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n        sqlState = \"42816\",\n        parameters = Map(\n          \"providedTimestamp\" -> \"2018-10-24 14:24:18.0\",\n          \"tableName\" -> \"2018-10-24 14:14:18.0\",\n          \"maximumTimestamp\" -> \"2018-10-24 14:14:18\")\n      )\n\n      val e2 = intercept[DeltaErrors.TemporallyUnstableInputException] {\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start + 10.minutes)}\").collect()\n      }\n      checkError(\n        e2,\n        \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n        sqlState = \"42816\",\n        parameters = Map(\n          \"providedTimestamp\" -> \"2018-10-24 14:24:18.0\",\n          \"tableName\" -> \"2018-10-24 14:14:18.0\",\n          \"maximumTimestamp\" -> \"2018-10-24 14:14:18\")\n      )\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, \"'2018-10-24 14:14:18'\")}\"),\n        Row(10)\n      )\n\n      verifyLogging(0L, 0L, \"timestamp\", \"dfReader\") {\n        checkAnswer(\n          spark.read.format(\"delta\").option(\"timestampAsOf\", \"2018-10-24 14:14:18\")\n            .load(getTableLocation(tblName)).groupBy().count(),\n          Row(10)\n        )\n      }\n    }\n  }\n\n  test(\"time travelling with adjusted timestamps\") {\n    if (isICTEnabledForNewTablesCatalogOwned) {\n      // ICT Timestamps are always monotonically increasing. Therefore,\n      // this test is not needed when ICT is enabled.\n      cancel(\"This test is not compatible with InCommitTimestamps.\")\n    }\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblName, start, start - 5.seconds, start + 3.minutes)\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start)}\"),\n        Row(10L)\n      )\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start + 1.milli)}\"),\n        Row(20L)\n      )\n\n      checkAnswer(\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start + 119.seconds)}\"),\n        Row(20L)\n      )\n\n      val e = intercept[AnalysisException] {\n        sql(s\"select count(*) from ${timestampAsOf(tblName, start - 3.seconds)}\").collect()\n      }\n      assert(e.getMessage.contains(\"before the earliest version\"))\n    }\n  }\n\n  test(\"Time travel with schema changes\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      spark.range(10).write.format(\"delta\").mode(\"append\").saveAsTable(tblName)\n      sql(s\"ALTER TABLE $tblName ADD COLUMNS (part bigint)\")\n      spark.range(10, 20).withColumn(\"part\", 'id)\n        .write.format(\"delta\").mode(\"append\").saveAsTable(tblName)\n\n      val tableLoc = getTableLocation(tblName)\n      checkAnswer(\n        sql(s\"select * from ${versionAsOf(tblName, 0)}\"),\n        spark.range(10).toDF())\n\n      checkAnswer(\n        sql(s\"select * from ${versionAsOf(s\"delta.`$tableLoc`\", 0)}\"),\n        spark.range(10).toDF())\n\n      checkAnswer(\n        spark.read.option(\"versionAsOf\", 0).format(\"delta\").load(tableLoc),\n        spark.range(10).toDF())\n\n    }\n  }\n\n  test(\"data skipping still works with time travel\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblName, start, start + 20.minutes)\n\n      def testScan(df: DataFrame): Unit = {\n        val scan = getStats(df)\n        assert(scan.scanned.bytesCompressed.get < scan.total.bytesCompressed.get)\n      }\n\n      testScan(sql(s\"select * from ${versionAsOf(tblName, 0)} where id = 2\"))\n\n      testScan(spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(getTableLocation(tblName))\n        .where(\"id = 2\"))\n\n    }\n  }\n\n  test(\"fail to time travel a different relation than Delta\") {\n    withTempDir { output =>\n      val dir = output.getCanonicalPath\n      spark.range(10).write.mode(\"append\").parquet(dir)\n      spark.range(10).write.mode(\"append\").parquet(dir)\n      def assertFormatFailure(f: => Unit): Unit = {\n        val e = intercept[AnalysisException] {\n          f\n        }\n        assert(\n          e.getMessage.contains(\"path-based tables\") ||\n            e.message.contains(\"[UNSUPPORTED_FEATURE.TIME_TRAVEL] The feature is not supported\"),\n          s\"Returned instead:\\n$e\")\n      }\n\n      assertFormatFailure {\n        sql(s\"select * from ${versionAsOf(s\"parquet.`$dir`\", 0)}\").collect()\n      }\n\n      assertFormatFailure {\n        sql(s\"select * from ${versionAsOf(s\"parquet.`$dir`\", 0)}\").collect()\n      }\n\n\n      checkAnswer(\n        spark.read.option(\"versionAsOf\", 0).parquet(dir), // do not time travel other relations\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      checkAnswer(\n        // do not time travel other relations\n        spark.read.option(\"timestampAsOf\", \"2018-10-12 01:01:01\").parquet(dir),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      val tblName = \"parq_table\"\n      withTable(tblName) {\n        sql(s\"create table $tblName using parquet as select * from parquet.`$dir`\")\n        val e = intercept[Exception] {\n          sql(s\"select * from ${versionAsOf(tblName, 0)}\").collect()\n        }\n        val catalogName = CatalogManager.SESSION_CATALOG_NAME\n        val catalogPrefix = catalogName + \".\"\n        assert(e.getMessage.contains(\n          s\"Table ${catalogPrefix}default.parq_table does not support time travel\") ||\n          e.getMessage.contains(s\"Time travel on the relation: `$catalogName`.`default`.`parq_table`\"))\n      }\n\n      val viewName = \"parq_view\"\n      assertFormatFailure {\n        sql(s\"create temp view $viewName as select * from ${versionAsOf(s\"parquet.`$dir`\", 0)}\")\n      }\n    }\n  }\n}\n\nabstract class DeltaHistoryManagerBase extends DeltaTimeTravelTests {\n  test(\"cannot time travel target tables of insert/delete/update/merge\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = 1540415658000L\n      generateCommits(tblName, start, start + 20.minutes)\n\n      // These all actually fail parsing\n      intercept[ParseException] {\n        sql(s\"insert into ${versionAsOf(tblName, 0)} values (11, 12, 13)\")\n      }\n\n      intercept[ParseException] {\n        sql(s\"update ${versionAsOf(tblName, 0)} set id = id - 1 where id < 10\")\n      }\n\n      intercept[ParseException] {\n        sql(s\"delete from ${versionAsOf(tblName, 0)} id < 10\")\n      }\n\n      intercept[ParseException] {\n        sql(s\"\"\"merge into ${versionAsOf(tblName, 0)} old\n               |using $tblName new\n               |on old.id = new.id\n               |when not matched then insert *\n           \"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"vacuumed version\") {\n    assume(!catalogOwnedDefaultCreationEnabledInTests,\n      \"VACUUM is blocked on catalog-managed tables\")\n\n    quietly {\n      val tblName = \"delta_table\"\n      withTable(tblName) {\n        val start = System.currentTimeMillis() - 5.days.toMillis\n        generateCommits(tblName, start, start + 20.minutes)\n        sql(s\"optimize $tblName\")\n\n        withSQLConf(\n          // Disable query rewrite or else the parquet files are not scanned.\n          DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"false\",\n          DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n          sql(s\"vacuum $tblName retain 0 hours\")\n          intercept[SparkException] {\n            sql(s\"select * from ${versionAsOf(tblName, 0)}\").collect()\n          }\n          intercept[SparkException] {\n            sql(s\"select count(*) from ${versionAsOf(tblName, 1)}\").collect()\n          }\n        }\n      }\n    }\n  }\n\n\n  test(\"as of with table API\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblName, start, start + 20.minutes, start + 40.minutes)\n\n      assert(spark.read.format(\"delta\").option(\"versionAsOf\", \"0\").table(tblName).count() == 10)\n      assert(spark.read.format(\"delta\").option(\"versionAsOf\", 1).table(tblName).count() == 20)\n      assert(spark.read.format(\"delta\").option(\"versionAsOf\", 2).table(tblName).count() == 30)\n      val e1 = intercept[AnalysisException] {\n        spark.read.format(\"delta\").option(\"versionAsOf\", 3).table(tblName).collect()\n      }\n      assert(e1.getMessage.contains(\"[0, 2]\"))\n\n      val e2 = intercept[org.apache.spark.sql.AnalysisException] {\n        spark.read.format(\"delta\")\n          .option(\"versionAsOf\", 3)\n          .option(\"timestampAsOf\", \"2020-10-22 23:20:11\")\n          .table(tblName).collect()\n      }\n\n      assert(e2.getMessage.contains(\"Cannot specify both version and timestamp\"))\n\n    }\n  }\n\n  test(\"getHistory returns the correct set of commits\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = 1540415658000L\n      generateCommits(tblName, start, start + 20.minutes, start + 40.minutes, start + 60.minutes)\n      val table = DeltaTableV2(spark, TableIdentifier(tblName))\n\n      def testGetHistory(\n          start: Long,\n          endOpt: Option[Long],\n          versions: Seq[Long],\n          expectedLogUpdates: Int): Unit = {\n        val usageRecords = Log4jUsageLogger.track {\n          val history = table.deltaLog.history.getHistory(start, endOpt, table.catalogTable)\n          assert(history.map(_.getVersion) == versions)\n        }\n        assert(filterUsageRecords(usageRecords, \"deltaLog.update\").size === expectedLogUpdates)\n      }\n\n      testGetHistory(start = 0, endOpt = Some(2), versions = Seq(2, 1, 0), expectedLogUpdates = 0)\n      testGetHistory(start = 1, endOpt = Some(1), versions = Seq(1), expectedLogUpdates = 0)\n      testGetHistory(start = 2, endOpt = None, versions = Seq(3, 2), expectedLogUpdates = 1)\n      testGetHistory(start = 1, endOpt = Some(5), versions = Seq(3, 2, 1), expectedLogUpdates = 1)\n      testGetHistory(start = 4, endOpt = None, versions = Seq.empty, expectedLogUpdates = 1)\n      testGetHistory(start = 2, endOpt = Some(1), versions = Seq.empty, expectedLogUpdates = 0)\n    }\n  }\n\n  test(\"getCommitFromNonICTRange should handle empty history by throwing proper error\") {\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      val start = 1540415658000L\n      generateCommits(tblName, start)\n      val deltaLog = DeltaLog.forTable(spark, getTableLocation(tblName))\n\n      val deltaFile = new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0).toUri)\n      assert(deltaFile.delete(), \"Failed to delete delta log file\")\n\n      val e = intercept[DeltaAnalysisException] {\n        deltaLog.history.getCommitFromNonICTRange(0, 1, start)\n      }\n\n      assert(e.getMessage.contains(\"DELTA_NO_COMMITS_FOUND\"))\n      assert(e.getMessage.contains(deltaLog.logPath.toString))\n    }\n  }\n\n  test(\"parallel search handles empty commits in a partition correctly\") {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"This test is not compatible with coordinated commits backfill timestamps.\")\n    }\n    val tblName = \"delta_table\"\n    withTable(tblName) {\n      // Small threshold to trigger parallel search\n      withSQLConf(\n          DeltaSQLConf.DELTA_HISTORY_PAR_SEARCH_THRESHOLD.key -> \"3\",\n          IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"false\") {\n        val start = 1540415658000L\n        // Generate 10 commits which will be processed in parallel due to threshold=3\n        val timestamps = (0 to 9).map(i => start + (i * 20).minutes)\n        generateCommits(tblName, timestamps: _*)\n        val table = DeltaTableV2(spark, TableIdentifier(tblName))\n        val deltaLog = table.deltaLog\n\n        // Delete all files in first partition to simulate concurrent metadata cleanup\n        val deltaFiles = (0 to 4).map { version =>\n          new File(FileNames.unsafeDeltaFile(deltaLog.logPath, version).toUri)\n        }\n        deltaFiles.foreach(f =>\n          assert(f.delete(), s\"Failed to delete delta log file ${f.getPath}\"))\n        assert(\n          deltaLog.history.getCommitFromNonICTRange(0, 9, start + (7 * 20).minutes).version == 7)\n      }\n    }\n  }\n}\n\n/** Uses V2 resolution code paths */\nclass DeltaHistoryManagerSuite extends DeltaHistoryManagerBase {\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf.set(SQLConf.USE_V1_SOURCE_LIST.key, \"parquet,json\")\n  }\n}\n\nclass DeltaHistoryManagerWithCatalogOwnedBatch1Suite extends DeltaHistoryManagerSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaHistoryManagerWithCatalogOwnedBatch2Suite extends DeltaHistoryManagerSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaHistoryManagerWithCatalogOwnedBatch100Suite extends DeltaHistoryManagerSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaImplicitsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.AddFile\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaImplicitsSuite extends SparkFunSuite with SharedSparkSession {\n\n  private def testImplict(name: String, func: => Unit): Unit = {\n    test(name) {\n      func\n    }\n  }\n\n  import org.apache.spark.sql.delta.implicits._\n\n  testImplict(\"int\", intEncoder)\n  testImplict(\"long\", longEncoder)\n  testImplict(\"string\", stringEncoder)\n  testImplict(\"longLong\", longLongEncoder)\n  testImplict(\"stringLong\", stringLongEncoder)\n  testImplict(\"stringString\", stringStringEncoder)\n  testImplict(\"javaLong\", javaLongEncoder)\n  testImplict(\"singleAction\", singleActionEncoder)\n  testImplict(\"addFile\", addFileEncoder)\n  testImplict(\"removeFile\", removeFileEncoder)\n  testImplict(\"serializableFileStatus\", serializableFileStatusEncoder)\n  testImplict(\"indexedFile\", indexedFileEncoder)\n  testImplict(\"addFileWithIndex\", addFileWithIndexEncoder)\n  testImplict(\"addFileWithSourcePath\", addFileWithSourcePathEncoder)\n  testImplict(\"deltaHistoryEncoder\", deltaHistoryEncoder)\n  testImplict(\"historyCommitEncoder\", historyCommitEncoder)\n  testImplict(\"snapshotStateEncoder\", snapshotStateEncoder)\n\n  testImplict(\"RichAddFileSeq: toDF\", Seq(AddFile(\"foo\", Map.empty, 0, 0, true)).toDF(spark))\n  testImplict(\"RichAddFileSeq: toDS\", Seq(AddFile(\"foo\", Map.empty, 0, 0, true)).toDS(spark))\n  testImplict(\"RichStringSeq: toDF\", Seq(\"foo\").toDF(spark))\n  testImplict(\"RichStringSeq: toDF(col)\", Seq(\"foo\").toDF(spark, \"str\"))\n  testImplict(\"RichIntSeq: toDF\", Seq(1).toDF(spark))\n  testImplict(\"RichIntSeq: toDF(col)\", Seq(1).toDF(spark, \"int\"))\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaIncrementalSetTransactionsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.UUID\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.UsageRecord\nimport org.apache.spark.sql.delta.DeltaTestUtils.{collectUsageLogs, createTestAddFile, BOOLEAN_DOMAIN}\nimport org.apache.spark.sql.delta.actions.{AddFile, SetTransaction, SingleAction}\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{QueryTest, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaIncrementalSetTransactionsSuite\n  extends QueryTest\n    with DeltaSQLCommandTest\n    with SharedSparkSession\n    with CatalogOwnedTestBaseSuite {\n\n  protected override def sparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key, \"true\")\n    .set(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key, \"true\")\n    // needed for DELTA_WRITE_SET_TRANSACTIONS_IN_CRC\n    .set(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key, \"true\")\n    // This test suite is sensitive to stateReconstruction we do at different places. So we disable\n    // [[INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS]] to simulate prod behaviour.\n    .set(DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key, \"false\")\n\n  /**\n   * Validates the result of [[Snapshot.setTransactions]] API for the latest snapshot of this\n   * [[DeltaLog]].\n   */\n  private def assertSetTransactions(\n      deltaLog: DeltaLog,\n      expectedTxns: Map[String, Long],\n      viaCRC: Boolean = false\n  ): Unit = {\n    val snapshot = deltaLog.update()\n    if (viaCRC) {\n      assert(snapshot.checksumOpt.flatMap(_.setTransactions).isDefined)\n      snapshot.checksumOpt.flatMap(_.setTransactions).foreach { setTxns =>\n        assert(setTxns.map(txn => (txn.appId, txn.version)).toMap === expectedTxns)\n      }\n    }\n    assert(snapshot.setTransactions.map(txn => (txn.appId, txn.version)).toMap === expectedTxns)\n    assert(snapshot.numOfSetTransactions === expectedTxns.size)\n    assert(expectedTxns === snapshot.transactions)\n  }\n\n\n  /** Commit given [[SetTransaction]] to `deltaLog`` */\n  private def commitSetTxn(\n      deltaLog: DeltaLog, appId: String, version: Long, lastUpdated: Long): Unit = {\n    commitSetTxn(deltaLog, Seq(SetTransaction(appId, version, Some(lastUpdated))))\n  }\n\n  /** Commit given [[SetTransaction]]s to `deltaLog`` */\n  private def commitSetTxn(\n      deltaLog: DeltaLog,\n      setTransactions: Seq[SetTransaction]): Unit = {\n    deltaLog.startTransaction().commit(\n      // Use createTestAddFile to create addFile with default stats for RowTracking.\n      setTransactions :+ createTestAddFile(encodedPath = s\"file-${UUID.randomUUID().toString}\"),\n      DeltaOperations.Write(SaveMode.Append)\n    )\n  }\n\n  test(\n    \"set-transaction tracking starts from 0th commit in CRC\"\n  ) {\n    withSQLConf(\n        DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"true\"\n    ) {\n      val tbl = \"test_table\"\n      withTable(tbl) {\n        sql(s\"CREATE TABLE $tbl USING delta as SELECT 1 as value\") // 0th commit\n        val log = DeltaLog.forTable(spark, TableIdentifier(tbl))\n        log.update()\n        // CRC for 0th commit has SetTransactions defined and are empty Seq.\n        assert(log.unsafeVolatileSnapshot.checksumOpt.flatMap(_.setTransactions).isDefined)\n        assert(log.unsafeVolatileSnapshot.checksumOpt.flatMap(_.setTransactions).get.isEmpty)\n        assertSetTransactions(log, expectedTxns = Map())\n\n        commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1) // 1st commit\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 1))\n        commitSetTxn(log, \"app-1\", version = 3, lastUpdated = 2) // 2nd commit\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 3))\n        commitSetTxn(log, \"app-2\", version = 100, lastUpdated = 3) // 3rd commit\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 3, \"app-2\" -> 100))\n        commitSetTxn(log, \"app-1\", version = 4, lastUpdated = 4) // 4th commit\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 4, \"app-2\" -> 100))\n\n        // 5th commit - Commit multiple [[SetTransaction]] in single commit\n        commitSetTxn(\n          log,\n          setTransactions = Seq(\n            SetTransaction(\"app-1\", version = 100, lastUpdated = Some(4)),\n            SetTransaction(\"app-3\", version = 300, lastUpdated = Some(4))\n          ))\n        assertSetTransactions(\n          log,\n          expectedTxns = Map(\"app-1\" -> 100, \"app-2\" -> 100, \"app-3\" -> 300))\n      }\n    }\n  }\n\n  test(\"set-transaction tracking starts for old tables after new commits\") {\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"false\") {\n      val tbl = \"test_table\"\n      withTable(tbl) {\n        // Create a table with feature disabled. So 0th/1st commit won't do SetTransaction\n        // tracking in CRC.\n        sql(s\"CREATE TABLE $tbl USING delta as SELECT 1 as value\") // 0th commit\n\n        def deltaLog: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n\n        assert(deltaLog.update().checksumOpt.get.setTransactions.isEmpty)\n        commitSetTxn(deltaLog, \"app-1\", version = 1, lastUpdated = 1) // 1st commit\n        assert(deltaLog.update().checksumOpt.get.setTransactions.isEmpty)\n\n        // Enable the SetTransaction tracking config and do more commits in the table.\n        withSQLConf(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"true\") {\n          DeltaLog.clearCache()\n          commitSetTxn(deltaLog, \"app-1\", version = 2, lastUpdated = 2) // 2nd commit\n          if (catalogOwnedDefaultCreationEnabledInTests) {\n            // For CatalogOwned tables with Row Tracking enabled (QoL feature), the commit DOES\n            // trigger state reconstruction via RowId.assignFreshRowIds -> extractHighWatermark ->\n            // domainMetadata access. This sets _computedStateTriggered=true, causing\n            // setTransactions to be included in the CRC immediately (not lazily as in the\n            // non-CatalogOwned case).\n            assert(deltaLog.update().checksumOpt.get.setTransactions.nonEmpty) // crc has set-txn\n            assertSetTransactions(deltaLog, expectedTxns = Map(\"app-1\" -> 2), viaCRC = true)\n          } else {\n            // By default, commit doesn't trigger stateReconstruction and so the\n            // incremental CRC won't have setTransactions present until `setTransactions` API is\n            // explicitly invoked before the commit.\n            assert(deltaLog.update().checksumOpt.get.setTransactions.isEmpty) // crc has no set-txn\n            assertSetTransactions(deltaLog, expectedTxns = Map(\"app-1\" -> 2), viaCRC = false)\n          }\n          DeltaLog.clearCache()\n\n          // Do commit after forcing computeState. Now SetTransaction tracking will start.\n          deltaLog.snapshot.setTransactions // This triggers computeState.\n          commitSetTxn(deltaLog, \"app-2\", version = 100, lastUpdated = 3) // 3rd commit\n          assert(deltaLog.update().checksumOpt.get.setTransactions.nonEmpty) // crc has set-txn\n        }\n      }\n    }\n  }\n\n  test(\"validate that crc doesn't contain SetTransaction when tracking is disabled\") {\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"false\") {\n      val tbl = \"test_table\"\n      withTable(tbl) {\n        sql(s\"CREATE TABLE $tbl (value Int) USING delta\")\n        val log = DeltaLog.forTable(spark, TableIdentifier(tbl))\n        // CRC for 0th commit should not have SetTransactions defined if conf is disabled.\n        assert(log.unsafeVolatileSnapshot.checksumOpt.flatMap(_.setTransactions).isEmpty)\n        assertSetTransactions(log, expectedTxns = Map(), viaCRC = false)\n\n        commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 1), viaCRC = false)\n\n        commitSetTxn(log, \"app-1\", version = 3, lastUpdated = 2)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 3), viaCRC = false)\n\n        commitSetTxn(log, \"app-2\", version = 100, lastUpdated = 3)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 3, \"app-2\" -> 100), viaCRC = false)\n\n        commitSetTxn(log, \"app-1\", version = 4, lastUpdated = 4)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 4, \"app-2\" -> 100), viaCRC = false)\n      }\n    }\n  }\n\n  for(computeStatePreloaded <- BOOLEAN_DOMAIN) {\n    test(\"set-transaction tracking should start if computeState is pre-loaded before\" +\n        s\" commit [computeState preloaded: $computeStatePreloaded]\") {\n\n      // Enable INCREMENTAL COMMITS and disable verification - to make sure that we\n      // don't trigger state reconstruction after a commit.\n      withSQLConf(\n        DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"false\",\n        DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> \"true\",\n        DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY.key -> \"false\"\n      ) {\n        val tbl = \"test_table\"\n\n        def log: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n\n        withTable(tbl) {\n          sql(s\"CREATE TABLE $tbl (value Int) USING delta\")\n          // After 0th commit - CRC shouldn't have SetTransactions as feature is disabled.\n          assertSetTransactions(log, expectedTxns = Map(), viaCRC = false)\n\n          DeltaLog.clearCache()\n          withSQLConf(DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"true\") {\n            commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1)\n            if (catalogOwnedDefaultCreationEnabledInTests) {\n              // For CatalogOwned tables with Row Tracking enabled (QoL feature), the commit DOES\n              // trigger state reconstruction via RowId.assignFreshRowIds, which sets\n              // _computedStateTriggered=true, causing setTransactions to be included in CRC.\n              assert(log.update().checksumOpt.flatMap(_.setTransactions).nonEmpty)\n            } else {\n              // During 1st commit, the feature is enabled. But still the new commit crc shouldn't\n              // contain the [[SetTransaction]] actions as we don't have an estimate of how many\n              // [[SetTransaction]] actions might be already part of this table till now.\n              // So incremental computation of [[SetTransaction]] won't trigger.\n              assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n            }\n\n            if (computeStatePreloaded) {\n              // Calling `validateChecksum` will pre-load the computeState\n              log.update().validateChecksum()\n            }\n\n            commitSetTxn(log, \"app-1\", version = 100, lastUpdated = 1)\n            if (catalogOwnedDefaultCreationEnabledInTests) {\n              // For CatalogOwned tables with Row Tracking enabled, state reconstruction is\n              // triggered during every commit (via RowId.assignFreshRowIds), so setTransactions are\n              // always included in CRC regardless of whether computeState was preloaded.\n              assert(log.update().checksumOpt.flatMap(_.setTransactions).nonEmpty)\n            } else {\n              // During 2nd commit, we have following 2 cases:\n              // 1. If `computeStatePreloaded` is set, then the Snapshot has already calculated\n              //    computeState, and so we have estimate of number of SetTransactions till this\n              //    point. So next commit will trigger incremental computation of [[SetTransaction]]\n              // 2. If `computeStatePreloaded` is not set, then Snapshot doesn't have computeState\n              //    pre-computed. So next commit will not trigger incremental computation of\n              //    [[SetTransaction]].\n              assert(log.update().checksumOpt.flatMap(_.setTransactions).nonEmpty ===\n                computeStatePreloaded)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"set-transaction tracking in CRC should stop once threshold is crossed\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_MAX_SET_TRANSACTIONS_IN_CRC.key -> \"2\",\n        DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"true\") {\n      val tbl = \"test_table\"\n      withTable(tbl) {\n        sql(s\"CREATE TABLE $tbl (value Int) USING delta\")\n        def log: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n        assertSetTransactions(log, expectedTxns = Map())\n\n        commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 1))\n\n        commitSetTxn(log, \"app-1\", version = 3, lastUpdated = 2)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n        assertSetTransactions(log, expectedTxns = Map(\"app-1\" -> 3))\n\n        commitSetTxn(log, \"app-2\", version = 100, lastUpdated = 3)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n        assertSetTransactions(\n          log, expectedTxns = Map(\"app-1\" -> 3, \"app-2\" -> 100))\n\n        commitSetTxn(log, \"app-1\", version = 4, lastUpdated = 4)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n        assertSetTransactions(\n          log, expectedTxns = Map(\"app-1\" -> 4, \"app-2\" -> 100))\n\n        commitSetTxn(log, \"app-3\", version = 1000, lastUpdated = 5)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n        assertSetTransactions(\n          log, expectedTxns = Map(\"app-1\" -> 4, \"app-2\" -> 100, \"app-3\" -> 1000), viaCRC = false)\n      }\n    }\n  }\n\n  test(\"set-transaction tracking in CRC should stop once setTxn retention conf is set\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_MAX_SET_TRANSACTIONS_IN_CRC.key -> \"2\",\n      DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"true\") {\n      val tbl = \"test_table\"\n      withTable(tbl) {\n        sql(s\"CREATE TABLE $tbl (value Int) USING delta\")\n        def log: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n\n        // Do 1 commit to table - set-transaction tracking continue to happen.\n        commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n\n        // Set any random table property - set-transaction tracking continue to happen.\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES ('randomProp1' = 'value1')\")\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isDefined)\n\n        // Set the `setTransactionRetentionDuration` table property - set-transaction tracking will\n        // stop.\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES \" +\n          s\"('delta.setTransactionRetentionDuration' = 'interval 1 days')\")\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n        commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1)\n        log.update().setTransactions\n        commitSetTxn(log, \"app-1\", version = 1, lastUpdated = 1)\n        assert(log.update().checksumOpt.flatMap(_.setTransactions).isEmpty)\n\n      }\n    }\n  }\n\n  for(checksumVerificationFailureIsFatal <- BOOLEAN_DOMAIN) {\n    // In this test we check that verification failed usage-logs are triggered when\n    // there is an issue in incremental computation and verification is explicitly enabled.\n    test(\"incremental set-transaction verification failures\" +\n        s\" [checksumVerificationFailureIsFatal: $checksumVerificationFailureIsFatal]\") {\n      withSQLConf(\n        DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> \"true\",\n        DeltaSQLConf.DELTA_WRITE_SET_TRANSACTIONS_IN_CRC.key -> \"true\",\n        // Enable verification explicitly as it is disabled by default.\n        DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY.key -> true.toString,\n        DeltaSQLConf.DELTA_CHECKSUM_MISMATCH_IS_FATAL.key -> s\"$checksumVerificationFailureIsFatal\"\n      ) {\n        withTempDir { tempDir =>\n          // Procedure:\n          // 1. Populate the table with 2 [[SetTransaction]]s and create a checkpoint, validate that\n          //    CRC has setTransactions present.\n          // 2. Intentionally corrupt the checkpoint - Remove one SetTransaction from it.\n          // 3. Clear the delta log cache so we pick up the checkpoint\n          // 4. Start a new transaction and attempt to commit the transaction\n          //    a. Incremental SetTransaction verification should fail\n          //    b. Post-commit snapshot should have checksumOpt with no [[SetTransaction]]s\n\n          // Step-1\n          val txn0 = SetTransaction(\"app-0\", version = 1, lastUpdated = Some(1))\n          val txn1 = SetTransaction(\"app-1\", version = 888, lastUpdated = Some(2))\n\n          def log: DeltaLog = DeltaLog.forTable(spark, tempDir)\n\n          // commit-0\n          val actions0 =\n            (1 to 10).map(i => createTestAddFile(encodedPath = i.toString)) :+ txn0\n          log.startTransaction().commitWriteAppend(actions0: _*)\n          // commit-1\n          val actions1 =\n            (11 to 20).map(i => createTestAddFile(encodedPath = i.toString)) :+ txn1\n          log.startTransaction().commitWriteAppend(actions1: _*)\n          assert(log.readChecksum(version = 1).get.setTransactions.nonEmpty)\n          log.checkpoint()\n\n          // Step-2\n          dropOneSetTransactionFromCheckpoint(log)\n\n          // Step-3\n          DeltaLog.clearCache()\n          assert(!log.update().logSegment.checkpointProvider.isEmpty)\n\n          // Step-4\n          // Create the txn with [[DELTA_CHECKSUM_MISMATCH_IS_FATAL]] as false so that pre-commit\n          // CRC validation doesn't fail. Our goal is to capture that post-commit verification\n          // catches any issues.\n          var txn: OptimisticTransactionImpl = null\n          withSQLConf(DeltaSQLConf.DELTA_CHECKSUM_MISMATCH_IS_FATAL.key -> \"false\") {\n            txn = log.startTransaction()\n          }\n          val Seq(corruptionReport) = collectSetTransactionCorruptionReport {\n            if (checksumVerificationFailureIsFatal) {\n              val e = intercept[DeltaIllegalStateException] {\n                withSQLConf(DeltaSQLConf.INCREMENTAL_COMMIT_VERIFY.key -> \"true\") {\n                  txn.commit(Seq(), DeltaOperations.Write(SaveMode.Append))\n                }\n              }\n              assert(e.getMessage.contains(\"SetTransaction mismatch\"))\n            } else {\n              txn.commit(Seq(), DeltaOperations.Write(SaveMode.Append))\n            }\n          }\n          val eventData = JsonUtils.fromJson[Map[String, Any]](corruptionReport.blob)\n\n          val expectedErrorEventData = Map(\n            \"unmatchedSetTransactionsCRC\" -> Seq(txn1),\n            \"unmatchedSetTransactionsComputedState\" -> Seq.empty,\n            \"version\" -> 2,\n            \"minSetTransactionRetentionTimestamp\" -> None,\n            \"repeatedEntriesForSameAppId\" -> Seq.empty,\n            \"exactMatchFailed\" -> true)\n\n          val observedMismatchingFields = eventData(\"mismatchingFields\").asInstanceOf[Seq[String]]\n          val observedErrorMessage = eventData(\"error\").asInstanceOf[String]\n          val observedDetailedErrorMap =\n            eventData(\"detailedErrorMap\").asInstanceOf[Map[String, String]]\n          assert(observedMismatchingFields === Seq(\"setTransactions\"))\n          assert(observedErrorMessage.contains(\"SetTransaction mismatch\"))\n          assert(observedDetailedErrorMap(\"setTransactions\") ===\n            JsonUtils.toJson(expectedErrorEventData))\n\n          if (checksumVerificationFailureIsFatal) {\n            // Due to failure, post-commit snapshot couldn't be updated\n            assert(log.snapshot.version === 1)\n            assert(log.readChecksum(version = 2).isEmpty)\n          } else {\n            assert(log.snapshot.version === 2)\n            assert(log.readChecksum(version = 2).get.setTransactions.isEmpty)\n          }\n        }\n      }\n    }\n  }\n\n  /** Drops one [[SetTransaction]] operation from checkpoint - the one with max appId */\n  private def dropOneSetTransactionFromCheckpoint(log: DeltaLog): Unit = {\n    import testImplicits._\n    val checkpointPath = log.update().checkpointProvider.topLevelFiles.head.getPath\n    val checkpointPathStr = checkpointPath.toString\n    // Get the checkpoint file format to read and write. Specify format to be compatible\n    // with v2 checkpoint b/c v2 checkpoint format could be either json/parquet.\n    // Test suites that extend w/ CC will enable v2 checkpoint by default.\n    val checkpointFormat = checkpointPathStr.substring(checkpointPathStr.lastIndexOf('.') + 1)\n    withTempDir { tmpCheckpoint =>\n      // count total rows in checkpoint\n      val checkpointDf = spark.read\n        .schema(SingleAction.encoder.schema)\n        .format(checkpointFormat).load(checkpointPathStr)\n      val initialActionCount = checkpointDf.count().toInt\n      val corruptedCheckpointData = checkpointDf\n        .orderBy(col(\"txn.appId\").asc_nulls_first) // force non setTransaction actions to front\n        .as[SingleAction].take(initialActionCount - 1) // Drop 1 action\n\n      corruptedCheckpointData.toSeq.toDS().coalesce(1).write\n        .mode(\"overwrite\").format(checkpointFormat).save(tmpCheckpoint.toString)\n      assert(\n        spark.read.format(checkpointFormat).load(tmpCheckpoint.toString).count()\n          === initialActionCount - 1)\n      val writtenCheckpoint =\n        tmpCheckpoint.listFiles().toSeq.filter(_.getName.startsWith(\"part\")).head\n      val checkpointFile = new File(checkpointPath.toUri)\n      new File(log.logPath.toUri).listFiles().toSeq.foreach { file =>\n        if (file.getName.startsWith(\".0\")) {\n          // we need to delete checksum files, otherwise trying to replace our incomplete\n          // checkpoint file fails due to the LocalFileSystem's checksum checks.\n          assert(file.delete(), \"Failed to delete checksum file\")\n        }\n      }\n      assert(checkpointFile.delete(), \"Failed to delete old checkpoint\")\n      assert(writtenCheckpoint.renameTo(checkpointFile),\n        \"Failed to rename corrupt checkpoint\")\n      val newCheckpoint = spark.read.format(checkpointFormat).load(checkpointFile.toString)\n      assert(newCheckpoint.count() === initialActionCount - 1,\n        \"Checkpoint file incorrect:\\n\" + newCheckpoint.collect().mkString(\"\\n\"))\n    }\n  }\n\n  private def collectSetTransactionCorruptionReport(f: => Unit): Seq[UsageRecord] = {\n    collectUsageLogs(\"delta.checksum.invalid\")(f).toSeq\n  }\n}\n\nclass DeltaIncrementalSetTransactionsWithCatalogOwnedBatch1Suite\n  extends DeltaIncrementalSetTransactionsSuite {\n\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaIncrementalSetTransactionsWithCatalogOwnedBatch2Suite\n  extends DeltaIncrementalSetTransactionsSuite {\n\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaIncrementalSetTransactionsWithCatalogOwnedBatch100Suite\n  extends DeltaIncrementalSetTransactionsSuite {\n\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaInsertIntoColumnOrderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Test suite covering INSERT operations with columns or struct fields ordered differently than in\n * the table schema.\n */\nclass DeltaInsertIntoColumnOrderSuite extends DeltaInsertIntoTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key, \"true\")\n    spark.conf.set(SQLConf.ANSI_ENABLED.key, \"true\")\n  }\n\n  /** Collects inserts that don't support implicit casting and will fail if the input data type\n   * doesn't match the expected column type.\n   * These are all dataframe inserts that use by name resolution, except for streaming writes.\n   */\n  private val insertsWithoutImplicitCastSupport: Set[Insert] =\n    insertsByName.intersect(insertsDataframe) - StreamingInsert\n\n  test(\"all test cases are implemented\") {\n    checkAllTestCasesImplemented()\n  }\n\n  // Inserting using a different ordering for top-level columns behaves as one would expect:\n  // inserts by position resolve columns based on position, inserts by name resolve based on name.\n  // Whether additional handling is required to add implicit casts doesn't impact this behavior.\n  for { (inserts, expectedAnswer) <- Seq(\n      insertsByPosition.intersect(insertsAppend) ->\n        TestData(\"a int, b int, c int\",\n          Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\", \"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5 }\"\"\")),\n      insertsByPosition.intersect(insertsOverwrite) ->\n        TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5 }\"\"\")),\n      insertsByName.intersect(insertsAppend) ->\n        TestData(\"a int, b int, c int\",\n          Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\", \"\"\"{ \"a\": 1, \"b\": 5, \"c\": 4 }\"\"\")),\n      insertsByName.intersect(insertsOverwrite) ->\n        TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 5, \"c\": 4 }\"\"\"))\n    )\n  } {\n    testInserts(s\"insert with different top-level column ordering\")(\n      initialData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, c int, b int\", Seq(\"\"\"{ \"a\": 1, \"c\": 4, \"b\": 5 }\"\"\")),\n      expectedResult = ExpectedResult.Success(expectedAnswer),\n      includeInserts = inserts\n    )\n\n    testInserts(s\"insert with implicit cast and different top-level column ordering\")(\n      initialData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a long, c int, b int\", Seq(\"\"\"{ \"a\": 1, \"c\": 4, \"b\": 5 }\"\"\")),\n      expectedResult = ExpectedResult.Success(expectedAnswer),\n      // Inserts that don't support implicit cast are failing, these are covered in the test below.\n      includeInserts = inserts -- insertsWithoutImplicitCastSupport\n    )\n  }\n\n  testInserts(s\"insert with implicit cast and different top-level column ordering\")(\n    initialData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n    partitionBy = Seq(\"a\"),\n    overwriteWhere = \"a\" -> 1,\n    insertData = TestData(\"a long, c int, b int\", Seq(\"\"\"{ \"a\": 1, \"c\": 4, \"b\": 4 }\"\"\")),\n    expectedResult = ExpectedResult.Failure(ex => {\n      checkError(\n        ex,\n        \"DELTA_FAILED_TO_MERGE_FIELDS\",\n        parameters = Map(\n          \"currentField\" -> \"a\",\n          \"updateField\" -> \"a\"\n        ))}),\n    includeInserts = insertsWithoutImplicitCastSupport\n  )\n\n  // Inserting using a different ordering for struct fields is full of surprises...\n  for { (inserts: Set[Insert], expectedAnswer) <- Seq(\n    // Most inserts use name based resolution for struct fields when there's no implicit cast\n    // required due to mismatching data types, except for `INSERT INTO/OVERWRITE (columns)` and\n    // `INSERT OVERWRITE PARTITION (partition) (columns)` which use position based resolution - even\n    // though these are by name inserts.\n    insertsAppend -\n      SQLInsertColList(SaveMode.Append) ->\n      TestData(\"a int, s struct <x int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\", \"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n    insertsOverwrite -\n      SQLInsertColList(SaveMode.Overwrite) - SQLInsertOverwritePartitionColList ->\n      TestData(\"a int, s struct <x int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n    Set(SQLInsertColList(SaveMode.Append)) ->\n      TestData(\"a int, s struct <x int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\", \"\"\"{ \"a\": 1, \"s\": { \"x\": 5, \"y\": 4 } }\"\"\")),\n    Set(SQLInsertColList(SaveMode.Overwrite), SQLInsertOverwritePartitionColList) ->\n      TestData(\"a int, s struct <x int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 5, \"y\": 4 } }\"\"\"))\n    )\n  } {\n    testInserts(s\"insert with different struct fields ordering\")(\n      initialData = TestData(\n        \"a int, s struct <x: int, y int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <y int, x: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5, \"x\": 4 } }\"\"\")),\n      expectedResult = ExpectedResult.Success(expectedAnswer),\n      includeInserts = inserts\n    )\n  }\n\n  for { (inserts: Set[Insert], expectedAnswer) <- Seq(\n    // When there's a type mismatch and an implicit cast is required, then all inserts use position\n    // based resolution for struct fields, except for `INSERT OVERWRITE PARTITION (partition)` and\n    // streaming insert which use name based resolution, and dataframe inserts by name which don't\n    // support implicit cast and fail - see negative test below.\n    insertsAppend - StreamingInsert ->\n      TestData(\"a int, s struct <x int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\", \"\"\"{ \"a\": 1, \"s\": { \"x\": 5, \"y\": 4 } }\"\"\")),\n    insertsOverwrite - SQLInsertOverwritePartitionByPosition ->\n      TestData(\"a int, s struct <x int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 5, \"y\": 4 } }\"\"\")),\n    Set(StreamingInsert) ->\n      TestData(\"a int, s struct <x int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\", \"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n    Set(SQLInsertOverwritePartitionByPosition) ->\n      TestData(\"a int, s struct <x int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\"))\n    )\n  } {\n    testInserts(s\"insert with implicit cast and different struct fields ordering\")(\n      initialData = TestData(\n        \"a int, s struct <x: int, y int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a long, s struct <y int, x: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5, \"x\": 4 } }\"\"\")),\n      expectedResult = ExpectedResult.Success(expectedAnswer),\n      // Inserts that don't support implicit cast are failing, these are covered in the test below.\n      includeInserts = inserts -- insertsWithoutImplicitCastSupport\n    )\n  }\n\n  testInserts(s\"insert with implicit cast and different struct fields ordering\")(\n    initialData = TestData(\n      \"a int, s struct <x: int, y int>\",\n      Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n    partitionBy = Seq(\"a\"),\n    overwriteWhere = \"a\" -> 1,\n    insertData = TestData(\"a long, s struct <y int, x: int>\",\n      Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5, \"x\": 4 } }\"\"\")),\n    expectedResult = ExpectedResult.Failure(ex => {\n      checkError(\n        ex,\n        \"DELTA_FAILED_TO_MERGE_FIELDS\",\n        parameters = Map(\n          \"currentField\" -> \"a\",\n          \"updateField\" -> \"a\"\n        ))}),\n    includeInserts = insertsWithoutImplicitCastSupport\n  )\n\n  for {\n    preserveNullSourceStructs <- BOOLEAN_DOMAIN\n    (inserts: Set[Insert], expectedAnswer) <- Seq(\n      insertsAppend ->\n        TestData(\"a int, s struct <x int, y: int>\",\n          Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\", \"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n      insertsOverwrite ->\n        TestData(\"a int, s struct <x int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": null }\"\"\"))\n    )\n  } {\n    testInserts(s\"null struct with different field order, \" +\n        s\"preserveNullSourceStructs=$preserveNullSourceStructs\")(\n      initialData = TestData(\n        \"a int, s struct <x: int, y int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <y int, x: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n      expectedResult = ExpectedResult.Success(expectedAnswer),\n      includeInserts = inserts,\n      confs = Seq(\n        // Implicit casts in streaming writes would cause the `null` struct to be incorrectly\n        // expanded. This conf allows skipping adding casts since there are no actual data type\n        // mismatch.\n        DeltaSQLConf.DELTA_STREAMING_SINK_IMPLICIT_CAST_FOR_TYPE_MISMATCH_ONLY.key -> \"true\",\n        DeltaSQLConf.DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS.key\n          -> preserveNullSourceStructs.toString\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaInsertIntoImplicitCastSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Test suite covering implicit casting in INSERT operations when the type of the data to insert\n * doesn't match the type in Delta table.\n *\n * The casting behavior is (unfortunately) dependent on the API used to run the INSERT, e.g.\n * Dataframe V1 insertInto() vs V2 saveAsTable() or using SQL.\n * This suite intends to exhaustively cover all the ways INSERT can be run on a Delta table. See\n * [[DeltaInsertIntoTest]] for a list of these INSERT operations covered.\n */\ntrait DeltaInsertIntoImplicitCastBase extends DeltaInsertIntoTest {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key, \"true\")\n    // Enable the null expansion fix by preserving NULL source structs in INSERT operations.\n    // Without this fix, NULL source structs are incorrectly expanded to structs with NULL fields.\n    spark.conf.set(DeltaSQLConf.DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS.key, \"true\")\n    spark.conf.set(SQLConf.ANSI_ENABLED.key, \"true\")\n  }\n\n  protected val ignoredTestCases: Map[String, Set[Insert]] = Map.empty\n\n  test(\"all test cases are implemented\") {\n    checkAllTestCasesImplemented(ignoredTestCases)\n  }\n}\n\ntrait DeltaInsertIntoImplicitCastTests extends DeltaInsertIntoImplicitCastBase {\n  for (schemaEvolution <- BOOLEAN_DOMAIN) {\n    testInserts(\"insert with implicit up and down cast on top-level fields, \" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a long, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"a\", LongType)\n          .add(\"b\", IntegerType)),\n      // The following insert operations don't implicitly cast the data but fail instead - see\n      // following test covering failure for these cases. We should change this to offer consistent\n      // behavior across all inserts.\n      excludeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      confs = Seq(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolution.toString)\n    )\n\n    testInserts(\"insert with implicit up and down cast on top-level fields, \" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a long, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Failure { ex =>\n        checkError(\n          ex,\n           \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"a\",\n            \"updateField\" -> \"a\"\n        ))\n      },\n      includeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      confs = Seq(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolution.toString)\n    )\n\n    testInserts(\"insert with implicit up and down cast on fields nested in array, \" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"key int, a array<struct<x: long, y: int>>\",\n        Seq(\"\"\"{ \"key\": 1, \"a\": [ { \"x\": 1, \"y\": 2 } ] }\"\"\")),\n      partitionBy = Seq(\"key\"),\n      overwriteWhere = \"key\" -> 1,\n      insertData = TestData(\"key int, a array<struct<x: int, y: long>>\",\n        Seq(\"\"\"{ \"key\": 1, \"a\": [ { \"x\": 3, \"y\": 4 } ] }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"key\", IntegerType)\n          .add(\"a\", ArrayType(new StructType()\n            .add(\"x\", LongType)\n            .add(\"y\", IntegerType, nullable = true)))),\n      // The following insert operations don't implicitly cast the data but fail instead - see\n      // following test covering failure for these cases. We should change this to offer consistent\n      // behavior across all inserts.\n      excludeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      confs = Seq(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolution.toString)\n    )\n\n    testInserts(\"insert with implicit up and down cast on fields nested in array, \" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"key int, a array<struct<x: long, y: int>>\",\n        Seq(\"\"\"{ \"key\": 1, \"a\": [ { \"x\": 1, \"y\": 2 } ] }\"\"\")),\n      partitionBy = Seq(\"key\"),\n      overwriteWhere = \"key\" -> 1,\n      insertData = TestData(\"key int, a array<struct<x: int, y: long>>\",\n        Seq(\"\"\"{ \"key\": 1, \"a\": [ { \"x\": 3, \"y\": 4 } ] }\"\"\")),\n      expectedResult = ExpectedResult.Failure { ex =>\n        checkError(\n          ex,\n           \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"a\",\n            \"updateField\" -> \"a\"\n        ))\n      },\n      includeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      confs = Seq(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolution.toString)\n    )\n\n    testInserts(\"insert with implicit up and down cast on fields nested in map, \" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"key int, m map<string, struct<x: long, y: int>>\",\n        Seq(\"\"\"{ \"key\": 1, \"m\": { \"a\": { \"x\": 1, \"y\": 2 } } }\"\"\")),\n      partitionBy = Seq(\"key\"),\n      overwriteWhere = \"key\" -> 1,\n      insertData = TestData(\"key int, m map<string, struct<x: int, y: long>>\",\n        Seq(\"\"\"{ \"key\": 1, \"m\": { \"a\": { \"x\": 3, \"y\": 4 } } }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"key\", IntegerType)\n          .add(\"m\", MapType(StringType, new StructType()\n            .add(\"x\", LongType)\n            .add(\"y\", IntegerType)))),\n      // The following insert operations don't implicitly cast the data but fail instead - see\n      // following test covering failure for these cases. We should change this to offer consistent\n      // behavior across all inserts.\n      excludeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      confs = Seq(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolution.toString)\n    )\n\n    testInserts(\"insert with implicit up and down cast on fields nested in map, \" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"key int, m map<string, struct<x: long, y: int>>\",\n        Seq(\"\"\"{ \"key\": 1, \"m\": { \"a\": { \"x\": 1, \"y\": 2 } } }\"\"\")),\n      partitionBy = Seq(\"key\"),\n      overwriteWhere = \"key\" -> 1,\n      insertData = TestData(\"key int, m map<string, struct<x: int, y: long>>\",\n        Seq(\"\"\"{ \"key\": 1, \"m\": { \"a\": { \"x\": 3, \"y\": 4 } } }\"\"\")),\n      expectedResult = ExpectedResult.Failure { ex =>\n        checkError(\n          ex,\n           \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"m\",\n            \"updateField\" -> \"m\"\n        ))\n      },\n      includeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      confs = Seq(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolution.toString)\n    )\n  }\n}\n\ntrait DeltaInsertIntoImplicitCastStreamingWriteTests extends DeltaInsertIntoImplicitCastBase {\n  override protected val ignoredTestCases: Map[String, Set[Insert]] = Map(\n    \"null struct with different field order, preserveNullSourceStructs=true\"\n      -> (insertsDataframe.intersect(insertsByName) - StreamingInsert),\n    \"null struct with different field order, preserveNullSourceStructs=false\"\n      -> (insertsDataframe.intersect(insertsByName) - StreamingInsert),\n    \"cast with dot in column name\"\n      -> (insertsDataframe.intersect(insertsByName) - StreamingInsert)\n  )\n\n  for {\n    preserveNullSourceStructs <- BOOLEAN_DOMAIN\n    (inserts: Set[Insert], expectedAnswer) <- Seq(\n      Set(SQLInsertColList(SaveMode.Append), StreamingInsert) ->\n        TestData(\"a long, s struct <x int, y: int>\",\n          Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\", \"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n      Set(SQLInsertColList(SaveMode.Overwrite),\n          SQLInsertOverwritePartitionByPosition,\n          SQLInsertOverwritePartitionColList) ->\n        TestData(\"a long, s struct <x int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n\n      // For all other INSERT types, the null struct gets incorrectly expanded to\n      // `struct<null, null> unless preserveNullSourceStructs is true.\n      insertsAppend - SQLInsertColList(SaveMode.Append) - StreamingInsert ->\n        TestData(\"a long, s struct <x int, y: int>\",\n          Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\",\n            if (preserveNullSourceStructs) {\n              \"\"\"{ \"a\": 1, \"s\": null }\"\"\"\n            } else {\n              \"\"\"{ \"a\": 1, \"s\": { \"x\": null, \"y\": null } }\"\"\"\n            }\n          )\n        ),\n      insertsOverwrite\n        - SQLInsertColList(SaveMode.Overwrite)\n        - SQLInsertOverwritePartitionByPosition\n        - SQLInsertOverwritePartitionColList ->\n        TestData(\"a long, s struct <x int, y: int>\",\n          Seq(\n            if (preserveNullSourceStructs) {\n              \"\"\"{ \"a\": 1, \"s\": null }\"\"\"\n            } else {\n              \"\"\"{ \"a\": 1, \"s\": { \"x\": null, \"y\": null } }\"\"\"\n            }\n          )\n        )\n    )\n  } {\n   testInserts(\"null struct with different field order, \" +\n       s\"preserveNullSourceStructs=$preserveNullSourceStructs\")(\n     initialData = TestData(\n       \"a long, s struct <x: int, y int>\",\n       Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n     partitionBy = Seq(\"a\"),\n     overwriteWhere = \"a\" -> 1,\n     insertData = TestData(\"a int, s struct <y int, x: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n     expectedResult = ExpectedResult.Success(expectedAnswer),\n     includeInserts = inserts,\n     // Dataframe INSERTs by name don't support implicit casting except for streaming\n     // writes, no point in testing them.\n     excludeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n     confs = Seq(DeltaSQLConf.DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS.key\n       -> preserveNullSourceStructs.toString)\n   )\n }\n\n  for { (inserts: Set[Insert], expectedAnswer) <- Seq(\n    insertsAppend ->\n      TestData(\"`s.a` long, s struct <x long, y: int>\",\n        Seq(\"\"\"{ \"s.a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\",\n        \"\"\"{ \"s.a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n    insertsOverwrite ->\n      TestData(\"`s.a` long, s struct <x long, y: int>\",\n        Seq(\"\"\"{ \"s.a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\"))\n    )\n  } {\n   testInserts(s\"cast with dot in column name\")(\n     initialData = TestData(\n       \"`s.a` long, s struct <x: long, y int>\",\n       Seq(\"\"\"{ \"s.a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n     partitionBy = Seq(\"`s.a`\"),\n     overwriteWhere = \"`s.a`\" -> 1,\n     insertData = TestData(\"`s.a` int, s struct <x int, y int>\",\n       Seq(\"\"\"{ \"s.a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n     expectedResult = ExpectedResult.Success(expectedAnswer),\n     includeInserts = inserts,\n     // Dataframe INSERTs by name don't support implicit casting except for streaming\n     // writes, no point in testing them.\n     excludeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n     confs = Seq(DeltaSQLConf.DELTA_STREAMING_SINK_IMPLICIT_CAST_ESCAPE_COLUMN_NAMES.key -> \"true\")\n   )\n }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaInsertIntoMissingColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Test suite covering behavior of INSERT operations with missing top-level columns or nested struct\n * fields.\n * This suite intends to exhaustively cover all the ways INSERT can be run on a Delta table. See\n * [[DeltaInsertIntoTest]] for a list of these INSERT operations covered.\n */\nclass DeltaInsertIntoMissingColumnSuite extends DeltaInsertIntoTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key, \"true\")\n    spark.conf.set(SQLConf.ANSI_ENABLED.key, \"true\")\n  }\n\n  test(\"all test cases are implemented\") {\n    checkAllTestCasesImplemented()\n  }\n\n  for (schemaEvolution <- BOOLEAN_DOMAIN) {\n    // Missing top-level columns are allowed for all inserts by name (SQL+dataframe) but missing\n    // nested fields are only allowed for dataframe inserts by name.\n    testInserts(s\"insert with missing top-level column, schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType)),\n      includeInserts = insertsByName,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with missing nested field, schemaEvolution=$schemaEvolution\")(\n      initialData =\n        TestData(\"a int, s struct<x: int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData =\n        TestData(\"a int, s struct<y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5 } }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"s\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n          )),\n      includeInserts = insertsByName.intersect(insertsDataframe),\n      withSchemaEvolution = schemaEvolution\n    )\n\n    // Missing columns for all inserts by name and missing nested fields for dataframe inserts by\n    // name are also allowed when the insert includes type mismatches, with the difference that\n    // dataframe inserts by name don't support implicit casting and will fail due to the type\n    // mismatch (but not the missing column/field per se).\n    testInserts(s\"insert with implicit cast and missing top-level column,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a long, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"a\", LongType)\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType)),\n      includeInserts = insertsByName.intersect(insertsSQL) + StreamingInsert,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with implicit cast and missing top-level column,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a long, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        // The missing column isn't an issue, but dataframe inserts by name (except streaming) don't\n        // support implicit casting to reconcile the type mismatch.\n        checkError(\n          ex,\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"a\",\n            \"updateField\" -> \"a\"\n          ))\n      }),\n      includeInserts = insertsByName.intersect(insertsDataframe) - StreamingInsert,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with implicit cast and missing nested field,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData =\n        TestData(\"a int, s struct<x: int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData =\n        TestData(\"a int, s struct<y: long>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5 } }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        // The missing column isn't an issue, but dataframe inserts by name (except streaming) don't\n        // support implicit casting to reconcile the type mismatch.\n        checkError(\n          ex,\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"s\",\n            \"updateField\" -> \"s\"\n          ))\n      }),\n      includeInserts = insertsByName.intersect(insertsDataframe) - StreamingInsert,\n      withSchemaEvolution = schemaEvolution\n    )\n\n  testInserts(s\"insert with implicit cast and missing nested field,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData =\n        TestData(\"a int, s struct<x: int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData =\n        TestData(\"a int, s struct<y: long>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5 } }\"\"\")),\n      // Missing nested fields are allowed when writing to a delta streaming sink when there's a\n      // type mismatch, same as when there's no type mismatch.\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"s\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType))),\n      includeInserts = Set(StreamingInsert),\n      withSchemaEvolution = schemaEvolution\n    )\n\n    // Missing columns for all inserts by position and missing nested fields for all inserts by\n    // position or SQL inserts are rejected. Whether the insert also includes a type mismatch\n    // doesn't play a role.\n    testInserts(s\"insert with missing top-level column, schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        checkError(\n          ex,\n          \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\",\n          parameters = Map(\n            \"tableName\" -> s\"$catalogName.default.target\",\n            \"columnName\" -> \"not enough data columns\",\n            \"numColumns\" -> \"3\",\n            \"insertColumns\" -> \"2\"\n          ))\n      }),\n      includeInserts = insertsByPosition,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with implicit cast and missing top-level column,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a long, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        checkError(\n          ex,\n          \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\",\n          parameters = Map(\n            \"tableName\" -> s\"$catalogName.default.target\",\n            \"columnName\" -> \"not enough data columns\",\n            \"numColumns\" -> \"3\",\n            \"insertColumns\" -> \"2\"\n          ))\n      }),\n      includeInserts = insertsByPosition,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with missing nested field, schemaEvolution=$schemaEvolution\")(\n      initialData =\n        TestData(\"a int, s struct<x: int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData =\n        TestData(\"a int, s struct<y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5 } }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        checkErrorMatchPVals(\n          ex,\n          \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\",\n          parameters = Map(\n            \"tableName\" -> s\"$catalogName\\\\.default\\\\.target\",\n            \"columnName\" -> s\"not enough nested fields in ($catalogName\\\\.default\\\\.source\\\\.)?s\",\n            \"numColumns\" -> \"2\",\n            \"insertColumns\" -> \"1\"\n          ))\n      }),\n      includeInserts = insertsByPosition ++ insertsSQL,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with implicit cast and missing nested field,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData =\n        TestData(\"a int, s struct<x: int, y: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": 3 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData =\n        TestData(\"a int, s struct<y: long>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"y\": 5 } }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        checkErrorMatchPVals(\n          ex,\n          \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\",\n          parameters = Map(\n            \"tableName\" -> s\"$catalogName\\\\.default\\\\.target\",\n            \"columnName\" -> s\"not enough nested fields in ($catalogName\\\\.default\\\\.source\\\\.)?s\",\n            \"numColumns\" -> \"2\",\n            \"insertColumns\" -> \"1\"\n          ))\n      }),\n      includeInserts = insertsByPosition ++ insertsSQL,\n      withSchemaEvolution = schemaEvolution\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaInsertIntoSchemaEvolutionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.sql.SaveMode\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Test suite covering behavior of INSERT operations with extra top-level columns or nested struct\n * fields in the input data.\n * This suite intends to exhaustively cover all the ways INSERT can be run on a Delta table. See\n * [[DeltaInsertIntoTest]] for a list of these INSERT operations covered.\n */\nclass DeltaInsertIntoSchemaEvolutionSuite extends DeltaInsertIntoTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key, \"true\")\n    spark.conf.set(SQLConf.ANSI_ENABLED.key, \"true\")\n  }\n\n  test(\"all test cases are implemented\") {\n    // we don't cover SQL INSERT with an explicit column list in this suite as it's not possible to\n    // specify a column that doesn't exist in the target table that way.\n    val ignoredTestCases = testCases.map { case (name, _) =>\n      name -> Set(\n        SQLInsertColList(SaveMode.Append),\n        SQLInsertColList(SaveMode.Overwrite),\n        SQLInsertOverwritePartitionColList)\n    }.toMap\n    checkAllTestCasesImplemented(ignoredTestCases)\n  }\n\n  for (enableAutoMergeSQLConf <- BOOLEAN_DOMAIN) {\n    val testMsg = s\"enableAutoMergeSQLConf=$enableAutoMergeSQLConf\"\n    testInserts(\"WITH SCHEMA EVOLUTION or .option always take precedence over the SQL Conf, \" +\n        testMsg)(\n      initialData = TestData(\"a int, s struct <x: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <x: int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n      expectedResult = ExpectedResult.Success(\n        expected = new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"s\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n          )),\n      confs = Seq(\n        DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> enableAutoMergeSQLConf.toString),\n      withSchemaEvolution = true\n    )\n  }\n\n  for (schemaEvolution <- BOOLEAN_DOMAIN) {\n    // We allow adding new top-level columns with schema evolution for all inserts.\n    testInserts(s\"insert with extra top-level column, schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5  }\"\"\")),\n      expectedResult = if (schemaEvolution) {\n        ExpectedResult.Success(\n          expected = new StructType()\n            .add(\"a\", IntegerType)\n            .add(\"b\", IntegerType)\n            .add(\"c\", IntegerType))\n      } else {\n        ExpectedResult.Failure(ex => {\n          checkError(ex, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n        })\n      },\n      excludeInserts = Set(\n        SQLInsertColList(SaveMode.Append),\n        SQLInsertColList(SaveMode.Overwrite),\n        SQLInsertOverwritePartitionColList\n      ),\n      withSchemaEvolution = schemaEvolution\n    )\n\n\n    // Adding new top-level columns with schema evolution is allowed for all inserts, but dataframe\n    // inserts by name don't support implicit casting and will fail due to the type mismatch.\n    testInserts(s\"insert with extra top-level column and implicit cast,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5  }\"\"\")),\n      expectedResult = if (schemaEvolution) {\n        ExpectedResult.Success(\n          expected = new StructType()\n            .add(\"a\", IntegerType)\n            .add(\"b\", IntegerType)\n            .add(\"c\", IntegerType))\n      } else {\n        ExpectedResult.Failure(ex => {\n          checkError(ex, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n        })\n      },\n      includeInserts = insertsByPosition + StreamingInsert,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with extra top-level column and implicit cast,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5  }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        checkError(\n          ex,\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"b\",\n            \"updateField\" -> \"b\"\n          ))\n      }),\n      includeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      withSchemaEvolution = schemaEvolution\n    )\n\n    // SQL inserts by name fail with a different error in the analysis when there's an extra column\n    // and schema evolution is disabled.\n    testInserts(s\"insert with extra top-level column and implicit cast,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, b long, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5  }\"\"\")),\n      expectedResult = if (schemaEvolution) {\n        ExpectedResult.Success(\n          expected = new StructType()\n            .add(\"a\", IntegerType)\n            .add(\"b\", IntegerType)\n            .add(\"c\", IntegerType))\n      } else {\n        ExpectedResult.Failure(ex => {\n          checkError(\n            ex,\n            \"INSERT_COLUMN_ARITY_MISMATCH.TOO_MANY_DATA_COLUMNS\",\n            parameters = Map(\n              \"tableName\" -> s\"`$catalogName`.`default`.`target`\",\n              \"tableColumns\" -> \"`a`, `b`\",\n              \"dataColumns\" -> \"`a`, `b`, `c`\"\n            ))\n        })\n      },\n      includeInserts = insertsSQL.intersect(insertsByName) -- Set(\n        // It's not possible to specify a column that doesn't exist in the target using SQL with an\n        // explicit column list.\n        SQLInsertColList(SaveMode.Append),\n        SQLInsertColList(SaveMode.Overwrite),\n        SQLInsertOverwritePartitionColList\n      ),\n      withSchemaEvolution = schemaEvolution\n    )\n\n    // We allow adding new nested struct fields for all inserts, including SQL inserts by name.\n    testInserts(s\"insert with extra nested field, schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, s struct <x: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <x: int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n      expectedResult = if (schemaEvolution) {\n        ExpectedResult.Success(\n          expected = new StructType()\n            .add(\"a\", IntegerType)\n            .add(\"s\", new StructType()\n              .add(\"x\", IntegerType)\n              .add(\"y\", IntegerType)\n            ))\n      } else {\n        ExpectedResult.Failure(ex => {\n          checkError(ex, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n        })\n      },\n      withSchemaEvolution = schemaEvolution\n    )\n  }\n\n  for {\n    preserveNullSourceStructs <- BOOLEAN_DOMAIN\n    (inserts: Set[Insert], expectedAnswer) <- Seq(\n      insertsAppend ->\n        TestData(\"a int, s struct <x: int, y: int>\",\n          Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2, \"y\": null } }\"\"\", \"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n      insertsOverwrite ->\n        TestData(\"a int, s struct <x: int, y: int>\",\n          Seq(\"\"\"{ \"a\": 1, \"s\": null }\"\"\"))\n    )\n  } {\n    testInserts(s\"insert with extra nested field, null struct, \" +\n        s\"preserveNullSourceStructs=$preserveNullSourceStructs\")(\n      initialData = TestData(\"a int, s struct <x: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <x: int, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": null }\"\"\")),\n      expectedResult = ExpectedResult.Success(expected = expectedAnswer),\n      includeInserts = inserts,\n      confs = Seq(\n        DeltaSQLConf.DELTA_INSERT_PRESERVE_NULL_SOURCE_STRUCTS.key\n          -> preserveNullSourceStructs.toString\n      ),\n      withSchemaEvolution = true\n    )\n  }\n\n  for (schemaEvolution <- BOOLEAN_DOMAIN) {\n    // Adding new nested struct fields with schema evolution is allowed for all inserts, but\n    // dataframe inserts by name don't support implicit casting and will fail due to the type\n    // mismatch.\n    testInserts(s\"insert with extra nested field and implicit cast,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, s struct <x: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <x: long, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n      expectedResult = if (schemaEvolution) {\n        ExpectedResult.Success(\n          expected = new StructType()\n            .add(\"a\", IntegerType)\n            .add(\"s\", new StructType()\n              .add(\"x\", IntegerType)\n              .add(\"y\", IntegerType)\n            ))\n      } else {\n        ExpectedResult.Failure(ex => {\n          checkError(ex, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n        })\n      },\n      includeInserts = insertsSQL ++ insertsByPosition + StreamingInsert -- Seq(\n        // It's not possible to specify a column that doesn't exist in the target using SQL with an\n        // explicit column list.\n        SQLInsertColList(SaveMode.Append),\n        SQLInsertColList(SaveMode.Overwrite),\n        SQLInsertOverwritePartitionColList\n      ),\n      withSchemaEvolution = schemaEvolution\n    )\n\n    testInserts(s\"insert with extra nested field and implicit cast,\" +\n      s\"schemaEvolution=$schemaEvolution\")(\n      initialData = TestData(\"a int, s struct <x: int>\", Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 2 } }\"\"\")),\n      partitionBy = Seq(\"a\"),\n      overwriteWhere = \"a\" -> 1,\n      insertData = TestData(\"a int, s struct <x: long, y: int>\",\n        Seq(\"\"\"{ \"a\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n      expectedResult = ExpectedResult.Failure(ex => {\n        checkError(\n          ex,\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n            \"currentField\" -> \"s\",\n            \"updateField\" -> \"s\"\n          ))\n      }),\n      includeInserts = insertsDataframe.intersect(insertsByName) - StreamingInsert,\n      withSchemaEvolution = schemaEvolution\n    )\n  }\n\n  // When DELTA_INSERT_BY_NAME_SCHEMA_EVOLUTION_ENABLED is disabled, SQL INSERT BY NAME with extra\n  // top-level columns should fail even when schema evolution is enabled.\n  test(\"insert by name with extra top-level column and implicit cast fails \" +\n      \"when byNameSchemaEvolution is disabled\") {\n    withTable(\"target\") {\n      sql(\"CREATE TABLE target (a INT, b INT) USING DELTA\")\n      withSQLConf(\n          DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\",\n          DeltaSQLConf.DELTA_INSERT_BY_NAME_SCHEMA_EVOLUTION_ENABLED.key -> \"false\") {\n        val ex = intercept[SparkThrowable] {\n          sql(\"INSERT INTO target BY NAME SELECT 1 AS a, 2L AS b, 3 AS c\")\n        }\n        checkError(\n          ex,\n          \"INSERT_COLUMN_ARITY_MISMATCH.TOO_MANY_DATA_COLUMNS\",\n          parameters = Map(\n            \"tableName\" -> s\"`$catalogName`.`default`.`target`\",\n            \"tableColumns\" -> \"`a`, `b`\",\n            \"dataColumns\" -> \"`a`, `b`, `c`\"\n          ))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaInsertIntoTableSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.util.TimeZone\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.schema.InvariantViolationException\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.InvalidDefaultValueErrorShims\nimport org.scalatest.BeforeAndAfter\n\nimport org.apache.spark.{SparkConf, SparkContext, SparkException, SparkThrowable}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.functions.{lit, struct}\nimport org.apache.spark.sql.internal.{LegacyBehaviorPolicy, SQLConf}\nimport org.apache.spark.sql.internal.SQLConf.{LEAF_NODE_DEFAULT_PARALLELISM, PARTITION_OVERWRITE_MODE, PartitionOverwriteMode}\nimport org.apache.spark.sql.test.{SharedSparkSession, TestSparkSession}\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\nclass DeltaInsertIntoSQLSuite\n  extends DeltaInsertIntoTestsWithTempViews(\n    supportsDynamicOverwrite = true,\n    includeSQLOnlyTests = true)\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  override protected def doInsert(tableName: String, insert: DataFrame, mode: SaveMode): Unit = {\n    val tmpView = \"tmp_view\"\n    withTempView(tmpView) {\n      insert.createOrReplaceTempView(tmpView)\n      val overwrite = if (mode == SaveMode.Overwrite) \"OVERWRITE\" else \"INTO\"\n      sql(s\"INSERT $overwrite TABLE $tableName SELECT * FROM $tmpView\")\n    }\n  }\n\n  test(\"Variant type\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (id LONG, v VARIANT) USING delta\")\n      sql(\"INSERT INTO t (id, v) VALUES (1, parse_json('{\\\"a\\\": 1}'))\")\n      sql(\"INSERT INTO t (id, v) VALUES (2, parse_json('{\\\"b\\\": 2}'))\")\n      sql(\n        \"INSERT INTO t SELECT id, parse_json(cast(id as string)) v FROM range(2)\")\n\n      checkAnswer(sql(\"select * from t\").selectExpr(\"id\", \"to_json(v)\"),\n        Seq(Row(1, \"{\\\"a\\\":1}\"), Row(2, \"{\\\"b\\\":2}\"), Row(0, \"0\"), Row(1, \"1\")))\n    }\n  }\n\n  test(\"insert overwrite should work with selecting constants\") {\n    withTable(\"t1\") {\n      sql(\"CREATE TABLE t1 (a int, b int, c int) USING delta PARTITIONED BY (b, c)\")\n      sql(\"INSERT OVERWRITE TABLE t1 PARTITION (c=3) SELECT 1, 2\")\n      checkAnswer(\n        sql(\"SELECT * FROM t1\"),\n        Row(1, 2, 3) :: Nil\n      )\n      sql(\"INSERT OVERWRITE TABLE t1 PARTITION (b=2, c=3) SELECT 1\")\n      checkAnswer(\n        sql(\"SELECT * FROM t1\"),\n        Row(1, 2, 3) :: Nil\n      )\n      sql(\"INSERT OVERWRITE TABLE t1 PARTITION (b=2, c) SELECT 1, 3\")\n      checkAnswer(\n        sql(\"SELECT * FROM t1\"),\n        Row(1, 2, 3) :: Nil\n      )\n    }\n  }\n\n  test(\"insertInto: append by name\") {\n    import testImplicits._\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n      val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n      sql(s\"INSERT INTO $t1(id, data) VALUES(1L, 'a')\")\n      // Can be in a different order\n      sql(s\"INSERT INTO $t1(data, id) VALUES('b', 2L)\")\n      // Can be casted automatically\n      sql(s\"INSERT INTO $t1(data, id) VALUES('c', 3)\")\n      verifyTable(t1, df)\n      withSQLConf(SQLConf.USE_NULLS_FOR_MISSING_DEFAULT_COLUMN_VALUES.key -> \"false\") {\n        // Missing columns\n        assert(intercept[AnalysisException] {\n          sql(s\"INSERT INTO $t1(data) VALUES(4)\")\n        }.getMessage.contains(\"Column id is not specified in INSERT\"))\n        // Missing columns with matching dataType\n        assert(intercept[AnalysisException] {\n          sql(s\"INSERT INTO $t1(data) VALUES('b')\")\n        }.getMessage.contains(\"Column id is not specified in INSERT\"))\n      }\n      // Duplicate columns\n      assert(intercept[AnalysisException](\n        sql(s\"INSERT INTO $t1(data, data) VALUES(5)\")).getMessage.nonEmpty)\n    }\n  }\n\n  test(\"insertInto: overwrite by name\") {\n    import testImplicits._\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n      sql(s\"INSERT OVERWRITE $t1(id, data) VALUES(1L, 'a')\")\n      verifyTable(t1, Seq((1L, \"a\")).toDF(\"id\", \"data\"))\n      // Can be in a different order\n      sql(s\"INSERT OVERWRITE $t1(data, id) VALUES('b', 2L)\")\n      verifyTable(t1, Seq((2L, \"b\")).toDF(\"id\", \"data\"))\n      // Can be casted automatically\n      sql(s\"INSERT OVERWRITE $t1(data, id) VALUES('c', 3)\")\n      verifyTable(t1, Seq((3L, \"c\")).toDF(\"id\", \"data\"))\n      withSQLConf(SQLConf.USE_NULLS_FOR_MISSING_DEFAULT_COLUMN_VALUES.key -> \"false\") {\n        // Missing columns\n        assert(intercept[AnalysisException] {\n          sql(s\"INSERT OVERWRITE $t1(data) VALUES(4)\")\n        }.getMessage.contains(\"Column id is not specified in INSERT\"))\n        // Missing columns with matching datatype\n        assert(intercept[AnalysisException] {\n          sql(s\"INSERT OVERWRITE $t1(data) VALUES(4L)\")\n        }.getMessage.contains(\"Column id is not specified in INSERT\"))\n      }\n      // Duplicate columns\n      assert(intercept[AnalysisException](\n        sql(s\"INSERT OVERWRITE $t1(data, data) VALUES(5)\")).getMessage.nonEmpty)\n    }\n  }\n\n  test(\"insertInto should throw an AnalysisError on name mismatch\") {\n    def testInsertByNameError(targetSchema: String, expectedErrorClass: String): Unit = {\n      val sourceTableName = \"source\"\n      val targetTableName = \"target\"\n      val format = \"delta\"\n      withTable(sourceTableName, targetTableName) {\n        sql(s\"CREATE TABLE $sourceTableName (a int, b int) USING $format\")\n        sql(s\"CREATE TABLE $targetTableName $targetSchema USING $format\")\n        val e = intercept[AnalysisException] {\n          sql(s\"INSERT INTO $targetTableName BY NAME SELECT * FROM $sourceTableName\")\n        }\n        assert(e.getErrorClass === expectedErrorClass)\n      }\n    }\n\n    // NOTE: We use upper case in the target schema so that needsSchemaAdjustmentByName returns\n    // true (due to case sensitivity) so that we call resolveQueryColumnsByName and hit the right\n    // code path.\n\n    // when the number of columns does not match and schema evolution is disabled, throw\n    // an arity mismatch error.\n    testInsertByNameError(\n      targetSchema = \"(A int)\",\n      expectedErrorClass = \"INSERT_COLUMN_ARITY_MISMATCH.TOO_MANY_DATA_COLUMNS\")\n\n    // when the number of columns matches, but the names do not, throw a missing column error.\n    testInsertByNameError(\n      targetSchema = \"(A int, c int)\", expectedErrorClass = \"DELTA_MISSING_COLUMN\")\n  }\n\n  dynamicOverwriteTest(\"insertInto: dynamic overwrite by name\") {\n    import testImplicits._\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string, data2 string) \" +\n        s\"USING $v2Format PARTITIONED BY (id)\")\n      sql(s\"INSERT OVERWRITE $t1(id, data, data2) VALUES(1L, 'a', 'b')\")\n      verifyTable(t1, Seq((1L, \"a\", \"b\")).toDF(\"id\", \"data\", \"data2\"))\n      // Can be in a different order\n      sql(s\"INSERT OVERWRITE $t1(data, data2, id) VALUES('b', 'd', 2L)\")\n      verifyTable(t1, Seq((1L, \"a\", \"b\"), (2L, \"b\", \"d\")).toDF(\"id\", \"data\", \"data2\"))\n      // Can be casted automatically\n      sql(s\"INSERT OVERWRITE $t1(data, data2, id) VALUES('c', 'e', 1)\")\n      verifyTable(t1, Seq((1L, \"c\", \"e\"), (2L, \"b\", \"d\")).toDF(\"id\", \"data\", \"data2\"))\n      withSQLConf(SQLConf.USE_NULLS_FOR_MISSING_DEFAULT_COLUMN_VALUES.key -> \"false\") {\n        // Missing columns\n        assert(intercept[AnalysisException] {\n          sql(s\"INSERT OVERWRITE $t1(data, id) VALUES('c', 1)\")\n        }.getMessage.contains(\"Column data2 is not specified in INSERT\"))\n        // Missing columns with matching datatype\n        assert(intercept[AnalysisException] {\n          sql(s\"INSERT OVERWRITE $t1(data, id) VALUES('c', 1L)\")\n        }.getMessage.contains(\"Column data2 is not specified in INSERT\"))\n      }\n      // Duplicate columns\n      assert(intercept[AnalysisException](\n        sql(s\"INSERT OVERWRITE $t1(data, data) VALUES(5)\")).getMessage.nonEmpty)\n    }\n  }\n\n  test(\"insertInto: static partition column name should not be used in the column list\") {\n    withTable(\"t\") {\n      sql(s\"CREATE TABLE t(i STRING, c string) USING $v2Format PARTITIONED BY (c)\")\n      checkError(\n        intercept[AnalysisException] {\n          sql(\"INSERT OVERWRITE t PARTITION (c='1') (c) VALUES ('2')\")\n        },\n        \"STATIC_PARTITION_COLUMN_IN_INSERT_COLUMN_LIST\",\n        parameters = Map(\"staticName\" -> \"c\"))\n    }\n  }\n\n\n  Seq((\"ordinal\", \"\"), (\"name\", \"(id, col2, col)\")).foreach { case (testName, values) =>\n    test(s\"INSERT OVERWRITE schema evolution works for array struct types - $testName\") {\n      val sourceSchema = \"id INT, col2 STRING, col ARRAY<STRUCT<f1: STRING, f2: STRING, f3: DATE>>\"\n      val sourceRecord = \"1, '2022-11-01', array(struct('s1', 's2', DATE'2022-11-01'))\"\n      val targetSchema = \"id INT, col2 DATE, col ARRAY<STRUCT<f1: STRING, f2: STRING>>\"\n      val targetRecord = \"1, DATE'2022-11-02', array(struct('t1', 't2'))\"\n\n      runInsertOverwrite(sourceSchema, sourceRecord, targetSchema, targetRecord) {\n        (sourceTable, targetTable) =>\n          sql(s\"INSERT OVERWRITE $targetTable $values SELECT * FROM $sourceTable\")\n\n          // make sure table is still writeable\n          sql(s\"\"\"INSERT INTO $targetTable VALUES (2, DATE'2022-11-02',\n               | array(struct('s3', 's4', DATE'2022-11-02')))\"\"\".stripMargin)\n          sql(s\"\"\"INSERT INTO $targetTable VALUES (3, DATE'2022-11-03',\n               |array(struct('s5', 's6', NULL)))\"\"\".stripMargin)\n          val df = spark.sql(\n            \"\"\"SELECT 1 as id, DATE'2022-11-01' as col2,\n              | array(struct('s1', 's2', DATE'2022-11-01')) as col UNION\n              | SELECT 2 as id, DATE'2022-11-02' as col2,\n              | array(struct('s3', 's4', DATE'2022-11-02')) as col UNION\n              | SELECT 3 as id, DATE'2022-11-03' as col2,\n              | array(struct('s5', 's6', NULL)) as col\"\"\".stripMargin)\n          verifyTable(targetTable, df)\n      }\n    }\n  }\n\n  Seq((\"ordinal\", \"\"), (\"name\", \"(id, col2, col)\")).foreach { case (testName, values) =>\n    test(s\"INSERT OVERWRITE schema evolution works for array nested types - $testName\") {\n      val sourceSchema = \"id INT, col2 STRING, \" +\n        \"col ARRAY<STRUCT<f1: INT, f2: STRUCT<f21: STRING, f22: DATE>, f3: STRUCT<f31: STRING>>>\"\n      val sourceRecord = \"1, '2022-11-01', \" +\n        \"array(struct(1, struct('s1', DATE'2022-11-01'), struct('s1')))\"\n      val targetSchema = \"id INT, col2 DATE, col ARRAY<STRUCT<f1: INT, f2: STRUCT<f21: STRING>>>\"\n      val targetRecord = \"2, DATE'2022-11-02', array(struct(2, struct('s2')))\"\n\n      runInsertOverwrite(sourceSchema, sourceRecord, targetSchema, targetRecord) {\n        (sourceTable, targetTable) =>\n          sql(s\"INSERT OVERWRITE $targetTable $values SELECT * FROM $sourceTable\")\n\n          // make sure table is still writeable\n          sql(s\"\"\"INSERT INTO $targetTable VALUES (2, DATE'2022-11-02',\n               | array(struct(2, struct('s2', DATE'2022-11-02'), struct('s2'))))\"\"\".stripMargin)\n          sql(s\"\"\"INSERT INTO $targetTable VALUES (3, DATE'2022-11-03',\n               | array(struct(3, struct('s3', NULL), struct(NULL))))\"\"\".stripMargin)\n          val df = spark.sql(\n            \"\"\"SELECT 1 as id, DATE'2022-11-01' as col2,\n              | array(struct(1, struct('s1', DATE'2022-11-01'), struct('s1'))) as col UNION\n              | SELECT 2 as id, DATE'2022-11-02' as col2,\n              | array(struct(2, struct('s2', DATE'2022-11-02'), struct('s2'))) as col UNION\n              | SELECT 3 as id, DATE'2022-11-03' as col2,\n              | array(struct(3, struct('s3', NULL), struct(NULL))) as col\n              |\"\"\".stripMargin)\n          verifyTable(targetTable, df)\n      }\n    }\n  }\n\n  // Schema evolution for complex map type\n  test(\"insertInto schema evolution with map type - append mode: field renaming + new field\") {\n    withTable(\"map_schema_evolution\") {\n      val tableName = \"map_schema_evolution\"\n      val initialSchema = StructType(Seq(\n        StructField(\"key\", IntegerType, nullable = false),\n        StructField(\"metrics\", MapType(StringType, StructType(Seq(\n          StructField(\"id\", IntegerType, nullable = false),\n          StructField(\"value\", IntegerType, nullable = false)\n        ))))\n      ))\n\n      val initialData = Seq(\n        Row(1, Map(\"event\" -> Row(1, 1)))\n      )\n\n      val initialRdd = spark.sparkContext.parallelize(initialData)\n      val initialDf = spark.createDataFrame(initialRdd, initialSchema)\n\n      // Write initial data\n      initialDf.write\n        .option(\"overwriteSchema\", \"true\")\n        .mode(\"overwrite\")\n        .format(\"delta\")\n        .saveAsTable(tableName)\n\n      // Evolved schema with field renamed and additional field in map struct\n      val evolvedSchema = StructType(Seq(\n        StructField(\"renamed_key\", IntegerType, nullable = false),\n        StructField(\"metrics\", MapType(StringType, StructType(Seq(\n          StructField(\"id\", IntegerType, nullable = false),\n          StructField(\"value\", IntegerType, nullable = false),\n          StructField(\"comment\", StringType, nullable = true)\n        ))))\n      ))\n\n      val evolvedData = Seq(\n        Row(1, Map(\"event\" -> Row(1, 1, \"deprecated\")))\n      )\n\n      val evolvedRdd = spark.sparkContext.parallelize(evolvedData)\n      val evolvedDf = spark.createDataFrame(evolvedRdd, evolvedSchema)\n\n      // insert data without schema evolution\n      val err = intercept[AnalysisException] {\n        evolvedDf.write\n          .mode(\"append\")\n          .format(\"delta\")\n          .insertInto(tableName)\n      }\n      checkError(err, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n\n      // insert data with schema evolution\n      withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n        evolvedDf.write\n          .mode(\"append\")\n          .format(\"delta\")\n          .insertInto(tableName)\n\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM $tableName\"),\n          Seq(\n            Row(1, Map(\"event\" -> Row(1, 1, null))),\n            Row(1, Map(\"event\" -> Row(1, 1, \"deprecated\")))\n        ))\n      }\n    }\n  }\n\n  test(\"not enough column in source to insert in nested map types\") {\n    withTable(\"source\", \"target\") {\n      sql(\n        \"\"\"CREATE TABLE source (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: INT>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\n        \"\"\"CREATE TABLE target (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: INT, comment: STRING>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\"INSERT INTO source VALUES (1, map('event', struct(1, 1)))\")\n\n      val e = intercept[AnalysisException] {\n        sql(\"INSERT INTO target SELECT * FROM source\")\n      }\n      checkError(\n        exception = e,\n        \"DELTA_INSERT_COLUMN_ARITY_MISMATCH\",\n        parameters = Map(\n          \"tableName\" -> \"spark_catalog.default.target\",\n          \"columnName\" -> \"not enough nested fields in value\",\n          \"numColumns\" -> \"3\",\n          \"insertColumns\" -> \"2\"\n        )\n      )\n    }\n  }\n\n  // not enough nested fields in value\n  test(\"more columns in source to insert in nested map types\") {\n    withTable(\"source\", \"target\") {\n      sql(\n        \"\"\"CREATE TABLE source (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: INT, comment: STRING>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\n        \"\"\"CREATE TABLE target (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: INT>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\"INSERT INTO source VALUES (1, map('event', struct(1, 1, 'deprecated')))\")\n\n      val e = intercept[AnalysisException] {\n        sql(\"INSERT INTO target SELECT * FROM source\")\n      }\n      checkError(e, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        sql(\"INSERT INTO target SELECT * FROM source\")\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM source\"),\n          Seq(\n            Row(1, Map(\"event\" -> Row(1, 1, \"deprecated\")))\n        ))\n      }\n    }\n  }\n\n  test(\"more columns in source to insert in nested 2-level deep map types\") {\n    withTable(\"source\", \"target\") {\n      sql(\n        \"\"\"CREATE TABLE source (\n          |  id INT,\n          |  metrics MAP<STRING, MAP<STRING, STRUCT<id: INT, value: INT, comment: STRING>>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\n        \"\"\"CREATE TABLE target (\n          |  id INT,\n          |  metrics MAP<STRING, MAP<STRING, STRUCT<id: INT, value: INT>>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\n        \"\"\"INSERT INTO source VALUES\n         | (1, map('event', map('subEvent', struct(1, 1, 'deprecated'))))\n         \"\"\".stripMargin)\n\n      val e = intercept[AnalysisException] {\n        sql(\"INSERT INTO target SELECT * FROM source\")\n      }\n      checkError(e, \"DELTA_METADATA_MISMATCH\", \"42KDG\", Map.empty[String, String])\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        sql(\"INSERT INTO target SELECT * FROM source\")\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM source\"),\n          Seq(\n            Row(1, Map(\"event\" -> Map(\"subEvent\" -> Row(1, 1, \"deprecated\"))))\n        ))\n      }\n    }\n  }\n\n  test(\"insert map type with different data type in key\") {\n    withTable(\"source\", \"target\") {\n      sql(\n        \"\"\"CREATE TABLE source (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: INT>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\n        \"\"\"CREATE TABLE target (\n          |  id INT,\n          |  metrics MAP<INT, STRUCT<id: INT, value: INT>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\"INSERT INTO source VALUES (1, map('1', struct(2, 3)))\")\n\n      sql(\"INSERT INTO target SELECT * FROM source\")\n\n      checkAnswer(\n        spark.sql(\"SELECT * FROM target\"),\n        Seq(\n          Row(1, Map(1 -> Row(2, 3)))\n      ))\n    }\n  }\n\n  test(\"insert map type with different data type in value\") {\n    withTable(\"source\", \"target\") {\n      sql(\n        \"\"\"CREATE TABLE source (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: LONG>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\n        \"\"\"CREATE TABLE target (\n          |  id INT,\n          |  metrics MAP<STRING, STRUCT<id: INT, value: INT>>\n          |) USING delta\"\"\".stripMargin)\n\n      sql(\"INSERT INTO source VALUES (1, map('m1', struct(2, 3L)))\")\n\n      sql(\"INSERT INTO target SELECT * FROM source\")\n\n      checkAnswer(\n        spark.sql(\"SELECT * FROM target\"),\n        Seq(\n          Row(1, Map(\"m1\" -> Row(2, 3)))\n      ))\n    }\n  }\n\n\n  def runInsertOverwrite(\n      sourceSchema: String,\n      sourceRecord: String,\n      targetSchema: String,\n      targetRecord: String)(\n      runAndVerify: (String, String) => Unit): Unit = {\n    val sourceTable = \"source\"\n    val targetTable = \"target\"\n    withTable(sourceTable) {\n      withTable(targetTable) {\n        withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n          // prepare source table\n          sql(s\"\"\"CREATE TABLE $sourceTable ($sourceSchema)\n                 | USING DELTA\"\"\".stripMargin)\n          sql(s\"INSERT INTO $sourceTable VALUES ($sourceRecord)\")\n          // prepare target table\n          sql(s\"\"\"CREATE TABLE $targetTable ($targetSchema)\n                 | USING DELTA\"\"\".stripMargin)\n          sql(s\"INSERT INTO $targetTable VALUES ($targetRecord)\")\n          runAndVerify(sourceTable, targetTable)\n        }\n      }\n    }\n  }\n}\n\nclass DeltaInsertIntoSQLByPathSuite\n  extends DeltaInsertIntoTests(supportsDynamicOverwrite = true, includeSQLOnlyTests = true)\n  with DeltaSQLCommandTest {\n  override protected def doInsert(tableName: String, insert: DataFrame, mode: SaveMode): Unit = {\n    val tmpView = \"tmp_view\"\n    withTempView(tmpView) {\n      insert.createOrReplaceTempView(tmpView)\n      val overwrite = if (mode == SaveMode.Overwrite) \"OVERWRITE\" else \"INTO\"\n      val ident = spark.sessionState.sqlParser.parseTableIdentifier(tableName)\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(ident)\n      sql(s\"INSERT $overwrite TABLE delta.`${catalogTable.location}` SELECT * FROM $tmpView\")\n    }\n  }\n\n  testQuietly(\"insertInto: cannot insert into a table that doesn't exist\") {\n    import testImplicits._\n    Seq(SaveMode.Append, SaveMode.Overwrite).foreach { mode =>\n      withTempDir { dir =>\n        val t1 = s\"delta.`${dir.getCanonicalPath}`\"\n        val tmpView = \"tmp_view\"\n        withTempView(tmpView) {\n          val overwrite = if (mode == SaveMode.Overwrite) \"OVERWRITE\" else \"INTO\"\n          val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n          df.createOrReplaceTempView(tmpView)\n\n          intercept[AnalysisException] {\n            sql(s\"INSERT $overwrite TABLE $t1 SELECT * FROM $tmpView\")\n          }\n\n          assert(new File(dir, \"_delta_log\").mkdirs(), \"Failed to create a _delta_log directory\")\n          intercept[AnalysisException] {\n            sql(s\"INSERT $overwrite TABLE $t1 SELECT * FROM $tmpView\")\n          }\n        }\n      }\n    }\n  }\n}\n\nclass DeltaInsertIntoDataFrameSuite\n  extends DeltaInsertIntoTestsWithTempViews(\n    supportsDynamicOverwrite = true,\n    includeSQLOnlyTests = false)\n  with DeltaSQLCommandTest {\n  override protected def doInsert(tableName: String, insert: DataFrame, mode: SaveMode): Unit = {\n    val dfw = insert.write.format(v2Format)\n    if (mode != null) {\n      dfw.mode(mode)\n    }\n    dfw.insertInto(tableName)\n  }\n}\n\nclass DeltaInsertIntoDataFrameByPathSuite\n  extends DeltaInsertIntoTests(supportsDynamicOverwrite = true, includeSQLOnlyTests = false)\n  with DeltaSQLCommandTest {\n  override protected def doInsert(tableName: String, insert: DataFrame, mode: SaveMode): Unit = {\n    val dfw = insert.write.format(v2Format)\n    if (mode != null) {\n      dfw.mode(mode)\n    }\n    val ident = spark.sessionState.sqlParser.parseTableIdentifier(tableName)\n    val catalogTable = spark.sessionState.catalog.getTableMetadata(ident)\n    dfw.insertInto(s\"delta.`${catalogTable.location}`\")\n  }\n\n  testQuietly(\"insertInto: cannot insert into a table that doesn't exist\") {\n    import testImplicits._\n    Seq(SaveMode.Append, SaveMode.Overwrite).foreach { mode =>\n      withTempDir { dir =>\n        val t1 = s\"delta.`${dir.getCanonicalPath}`\"\n        val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n\n        intercept[AnalysisException] {\n          df.write.mode(mode).insertInto(t1)\n        }\n\n        assert(new File(dir, \"_delta_log\").mkdirs(), \"Failed to create a _delta_log directory\")\n        intercept[AnalysisException] {\n          df.write.mode(mode).insertInto(t1)\n        }\n\n        // Test DataFrameWriterV2 as well\n        val dfW2 = df.writeTo(t1)\n        if (mode == SaveMode.Append) {\n          intercept[AnalysisException] {\n            dfW2.append()\n          }\n        } else {\n          intercept[AnalysisException] {\n            dfW2.overwrite(lit(true))\n          }\n        }\n      }\n    }\n  }\n}\n\n\ntrait DeltaInsertIntoColumnMappingSelectedTests extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests = Seq(\n    \"InsertInto: overwrite - mixed clause reordered - static mode\",\n    \"InsertInto: overwrite - multiple static partitions - dynamic mode\"\n  )\n}\n\nclass DeltaInsertIntoSQLNameColumnMappingSuite extends DeltaInsertIntoSQLSuite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaInsertIntoColumnMappingSelectedTests {\n  override protected def runOnlyTests: Seq[String] = super.runOnlyTests :+\n    \"insert overwrite should work with selecting constants\"\n}\n\nclass DeltaInsertIntoSQLByPathNameColumnMappingSuite extends DeltaInsertIntoSQLByPathSuite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaInsertIntoColumnMappingSelectedTests\n\nclass DeltaInsertIntoDataFrameNameColumnMappingSuite extends DeltaInsertIntoDataFrameSuite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaInsertIntoColumnMappingSelectedTests\n\nclass DeltaInsertIntoDataFrameByPathNameColumnMappingSuite\n  extends DeltaInsertIntoDataFrameByPathSuite\n    with DeltaColumnMappingEnableNameMode\n    with DeltaInsertIntoColumnMappingSelectedTests\n\nabstract class DeltaInsertIntoTestsWithTempViews(\n    supportsDynamicOverwrite: Boolean,\n    includeSQLOnlyTests: Boolean)\n  extends DeltaInsertIntoTests(supportsDynamicOverwrite, includeSQLOnlyTests)\n  with DeltaTestUtilsForTempViews {\n  protected def testComplexTempViews(name: String)(text: String, expectedResult: Seq[Row]): Unit = {\n    testWithTempView(s\"insertInto a temp view created on top of a table - $name\") { isSQLTempView =>\n      import testImplicits._\n      val t1 = \"tbl\"\n      sql(s\"CREATE TABLE $t1 (key int, value int) USING $v2Format\")\n      Seq(SaveMode.Append, SaveMode.Overwrite).foreach { mode =>\n        createTempViewFromSelect(text, isSQLTempView)\n        val df = Seq((0, 3), (1, 2)).toDF(\"key\", \"value\")\n        try {\n          doInsert(\"v\", df, mode)\n          checkAnswer(spark.table(\"v\"), expectedResult)\n        } catch {\n          case e: AnalysisException =>\n            assert(\n              e.getMessage.contains(\"[EXPECT_TABLE_NOT_VIEW.NO_ALTERNATIVE]\") ||\n              e.getMessage.contains(\"Inserting into an RDD-based table is not allowed\") ||\n              e.getMessage.contains(\"Table default.v not found\") ||\n              e.getMessage.contains(\"Table or view 'v' not found in database 'default'\") ||\n              e.getMessage.contains(\"The table or view `default`.`v` cannot be found\") ||\n              e.getMessage.contains(\n                \"[UNSUPPORTED_INSERT.RDD_BASED] Can't insert into the target.\") ||\n              e.getMessage.contains(\n                \"The table or view `spark_catalog`.`default`.`v` cannot be found\"))\n        }\n      }\n    }\n  }\n\n  testComplexTempViews(\"basic\") (\n    \"SELECT * FROM tbl\",\n    Seq(Row(0, 3), Row(1, 2))\n  )\n\n  testComplexTempViews(\"subset cols\")(\n    \"SELECT key FROM tbl\",\n    Seq(Row(0), Row(1))\n  )\n\n  testComplexTempViews(\"superset cols\")(\n    \"SELECT key, value, 1 FROM tbl\",\n    Seq(Row(0, 3, 1), Row(1, 2, 1))\n  )\n\n  testComplexTempViews(\"nontrivial projection\")(\n    \"SELECT value as key, key as value FROM tbl\",\n    Seq(Row(3, 0), Row(2, 1))\n  )\n\n  testComplexTempViews(\"view with too many internal aliases\")(\n    \"SELECT * FROM (SELECT * FROM tbl AS t1) AS t2\",\n    Seq(Row(0, 3), Row(1, 2))\n  )\n\n}\n\nclass DeltaColumnDefaultsInsertSuite extends InsertIntoSQLOnlyTests with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  override val supportsDynamicOverwrite = true\n  override val includeSQLOnlyTests = true\n\n  val tblPropertiesAllowDefaults =\n    \"\"\"tblproperties (\n      |  'delta.feature.allowColumnDefaults' = 'enabled',\n      |  'delta.columnMapping.mode' = 'name'\n      |)\"\"\".stripMargin\n\n  test(\"Column DEFAULT value support with Delta Lake, positive tests\") {\n    Seq(\n      PartitionOverwriteMode.STATIC.toString,\n      PartitionOverwriteMode.DYNAMIC.toString\n    ).foreach { partitionOverwriteMode =>\n      withSQLConf(\n        SQLConf.ENABLE_DEFAULT_COLUMNS.key -> \"true\",\n        SQLConf.PARTITION_OVERWRITE_MODE.key -> partitionOverwriteMode,\n        // Set these configs to allow writing test values like timestamps of Jan. 1, year 1, etc.\n        SQLConf.PARQUET_REBASE_MODE_IN_WRITE.key -> LegacyBehaviorPolicy.LEGACY.toString,\n        SQLConf.PARQUET_INT96_REBASE_MODE_IN_WRITE.key -> LegacyBehaviorPolicy.LEGACY.toString) {\n        withTable(\"t1\", \"t2\", \"t3\", \"t4\") {\n          // Positive tests:\n          // Create some columns with default values and then insert into them.\n          sql(\"create table t1(\" +\n            s\"a int default 42, b boolean default true, c string default 'abc') using $v2Format \" +\n            s\"partitioned by (a) $tblPropertiesAllowDefaults\")\n          sql(\"insert into t1 values (1, false, default)\")\n          sql(\"insert into t1 values (1, default, default)\")\n          sql(\"alter table t1 alter column c set default 'def'\")\n          sql(\"insert into t1 values (default, default, default)\")\n          sql(\"alter table t1 alter column c drop default\")\n          // Exercise INSERT INTO commands with VALUES lists mapping columns positionally.\n          sql(\"insert into t1 values (default, default, default)\")\n          // Write the data in the table 't1' to new table 't4' and then perform an INSERT OVERWRITE\n          // back to 't1' here, to exercise static and dynamic partition overwrites.\n          sql(f\"create table t4(a int, b boolean, c string) using $v2Format \" +\n            s\"partitioned by (a) $tblPropertiesAllowDefaults\")\n          // Exercise INSERT INTO commands with SELECT queries mapping columns by name.\n          sql(\"insert into t4(a, b, c) select a, b, c from t1\")\n          sql(\"insert overwrite table t1 select * from t4\")\n          checkAnswer(spark.table(\"t1\"), Seq(\n            Row(1, false, \"abc\"),\n            Row(1, true, \"abc\"),\n            Row(42, true, \"def\"),\n            Row(42, true, null)\n          ))\n          // Insert default values with all supported types.\n          sql(\"create table t2(\" +\n            \"s boolean default true, \" +\n            \"t byte default cast(null as byte), \" +\n            \"u short default cast(42 as short), \" +\n            \"v float default 0, \" +\n            \"w double default 0, \" +\n            \"x date default date'0000', \" +\n            \"y timestamp default timestamp'0000', \" +\n            \"z decimal(5, 2) default 123.45,\" +\n            \"a1 bigint default 43,\" +\n            \"a2 smallint default cast(5 as smallint),\" +\n            s\"a3 tinyint default cast(6 as tinyint)) using $v2Format \" +\n            tblPropertiesAllowDefaults)\n          sql(\"insert into t2 values (default, default, default, default, default, default, \" +\n            \"default, default, default, default, default)\")\n          val result: Array[Row] = spark.table(\"t2\").collect()\n          assert(result.length == 1)\n          val row: Row = result(0)\n          assert(row.length == 11)\n          assert(row(0) == true)\n          assert(row(1) == null)\n          assert(row(2) == 42)\n          assert(row(3) == 0.0f)\n          assert(row(4) == 0.0d)\n          assert(row(5).toString == \"0001-01-01\")\n          assert(row(6).toString == \"0001-01-01 00:00:00.0\")\n          assert(row(7).toString == \"123.45\")\n          assert(row(8) == 43L)\n          assert(row(9) == 5)\n          assert(row(10) == 6)\n        }\n        withTable(\"t3\") {\n          // Set a default value for a partitioning column.\n          sql(s\"create table t3(i boolean, s bigint, q int default 42) using $v2Format \" +\n            s\"partitioned by (i) $tblPropertiesAllowDefaults\")\n          sql(\"alter table t3 alter column i set default true\")\n          sql(\"insert into t3(i, s, q) values (default, default, default)\")\n          checkAnswer(spark.table(\"t3\"), Seq(\n            Row(true, null, 42)))\n          // Drop the column and add it again without the default. Querying the column now returns\n          // NULL.\n          sql(\"alter table t3 drop column q\")\n          sql(\"alter table t3 add column q int\")\n          checkAnswer(spark.table(\"t3\"), Seq(\n            Row(true, null, null)))\n        }\n      }\n    }\n  }\n\n  test(\"Column DEFAULT value support with Delta Lake, negative tests\") {\n    withSQLConf(SQLConf.ENABLE_DEFAULT_COLUMNS.key -> \"true\") {\n      // The table feature is not enabled via TBLPROPERTIES.\n      withTable(\"createTableWithDefaultFeatureNotEnabled\") {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(s\"create table createTableWithDefaultFeatureNotEnabled(\" +\n              s\"i boolean, s bigint, q int default 42) using $v2Format \" +\n              \"partitioned by (i)\")\n          },\n          \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          parameters = Map(\"commandType\" -> \"CREATE TABLE\")\n        )\n      }\n      withTable(\"alterTableSetDefaultFeatureNotEnabled\") {\n        sql(s\"create table alterTableSetDefaultFeatureNotEnabled(a int) using $v2Format\")\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\"alter table alterTableSetDefaultFeatureNotEnabled alter column a set default 42\")\n          },\n          \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_FEATURE_NOT_ENABLED\",\n          parameters = Map(\"commandType\" -> \"ALTER TABLE\")\n        )\n      }\n      // Adding a new column with a default value to an existing table is not allowed.\n      withTable(\"alterTableTest\") {\n        sql(s\"create table alterTableTest(i boolean, s bigint, q int default 42) using $v2Format \" +\n          s\"partitioned by (i) $tblPropertiesAllowDefaults\")\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\"alter table alterTableTest add column z int default 42\")\n          },\n          \"WRONG_COLUMN_DEFAULTS_FOR_DELTA_ALTER_TABLE_ADD_COLUMN_NOT_SUPPORTED\"\n        )\n      }\n      // The default value fails to analyze.\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"create table t4 (s int default badvalue) using $v2Format \" +\n            s\"$tblPropertiesAllowDefaults\")\n        },\n        InvalidDefaultValueErrorShims.INVALID_DEFAULT_VALUE_ERROR_CODE,\n        parameters = Map(\n          \"statement\" -> \"CREATE TABLE\",\n          \"colName\" -> \"`s`\",\n          \"defaultValue\" -> \"badvalue\"))\n\n      // The default value analyzes to a table not in the catalog.\n      // The error message reports that we failed to execute the command because subquery\n      // expressions are not allowed in DEFAULT values.\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"create table t4 (s int default (select min(x) from badtable)) using $v2Format \" +\n            tblPropertiesAllowDefaults)\n        },\n        \"INVALID_DEFAULT_VALUE.SUBQUERY_EXPRESSION\",\n        parameters = Map(\n          \"statement\" -> \"CREATE TABLE\",\n          \"colName\" -> \"`s`\",\n          \"defaultValue\" -> \"(select min(x) from badtable)\"))\n      // The default value has an explicit alias. It fails to evaluate when inlined into the\n      // VALUES list at the INSERT INTO time.\n      // The error message reports that we failed to execute the command because subquery\n      // expressions are not allowed in DEFAULT values.\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"create table t4 (s int default (select 42 as alias)) using $v2Format \" +\n            tblPropertiesAllowDefaults)\n        },\n        \"INVALID_DEFAULT_VALUE.SUBQUERY_EXPRESSION\",\n        parameters = Map(\n          \"statement\" -> \"CREATE TABLE\",\n          \"colName\" -> \"`s`\",\n          \"defaultValue\" -> \"(select 42 as alias)\"))\n      // The default value parses but the type is not coercible.\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"create table t4 (s bigint default false) \" +\n            s\"using $v2Format $tblPropertiesAllowDefaults\")\n        },\n        \"INVALID_DEFAULT_VALUE.DATA_TYPE\",\n        parameters = Map(\n          \"statement\" -> \"CREATE TABLE\",\n          \"colName\" -> \"`s`\",\n          \"expectedType\" -> \"\\\"BIGINT\\\"\",\n          \"actualType\" -> \"\\\"BOOLEAN\\\"\",\n          \"defaultValue\" -> \"false\"))\n      // It is possible to create a table with NOT NULL constraint and a DEFAULT value of NULL.\n      // However, future inserts into that table will fail.\n      withTable(\"t4\") {\n        sql(s\"create table t4(i boolean, s bigint, q int default null not null) using $v2Format \" +\n          s\"partitioned by (i) $tblPropertiesAllowDefaults\")\n        // The InvariantViolationException is not a SparkThrowable, so just check we receive one.\n        assert(intercept[InvariantViolationException] {\n          sql(\"insert into t4 values (default, default, default)\")\n        }.getMessage.nonEmpty)\n      }\n      // It is possible to create a table with a check constraint and a DEFAULT value that does not\n      // conform. However, future inserts into that table will fail.\n      withTable(\"t4\") {\n        sql(s\"create table t4(i boolean, s bigint, q int default 42) using $v2Format \" +\n          s\"partitioned by (i) $tblPropertiesAllowDefaults\")\n        sql(\"alter table t4 add constraint smallq check (q < 10)\")\n        assert(intercept[InvariantViolationException] {\n          sql(\"insert into t4 values (default, default, default)\")\n        }.getMessage.nonEmpty)\n      }\n    }\n    // Column default values are disabled per configuration in general.\n    withSQLConf(SQLConf.ENABLE_DEFAULT_COLUMNS.key -> \"false\") {\n      checkError(\n        intercept[ParseException] {\n          sql(s\"create table t4 (s int default 41 + 1) using $v2Format \" +\n            tblPropertiesAllowDefaults)\n        },\n        \"UNSUPPORTED_DEFAULT_VALUE.WITH_SUGGESTION\",\n        parameters = Map.empty,\n        context = ExpectedContext(fragment = \"s int default 41 + 1\", start = 17, stop = 36))\n    }\n  }\n\n  test(\"Exercise column defaults with dataframe writes\") {\n    // There are three column types exercising various combinations of implicit and explicit\n    // default column value references in the 'insert into' statements. Note these tests depend on\n    // enabling the configuration to use NULLs for missing DEFAULT column values.\n    withSQLConf(SQLConf.USE_NULLS_FOR_MISSING_DEFAULT_COLUMN_VALUES.key -> \"true\") {\n      for (useDataFrames <- Seq(false, true)) {\n        withTable(\"t1\", \"t2\") {\n          sql(s\"create table t1(j int, s bigint default 42, x bigint default 43) using $v2Format \" +\n            tblPropertiesAllowDefaults)\n          if (useDataFrames) {\n            // Use 'saveAsTable' to exercise mapping columns by name. Note that we have to specify\n            // values for all columns of the target table here whether we use 'saveAsTable' or\n            // 'insertInto', since the DataFrame generates a LogicalPlan equivalent to a SQL INSERT\n            // INTO command without any explicit user-specified column list. For example, if we\n            // used Seq((1)).toDF(\"j\", \"s\", \"x\").write.mode(\"append\") here instead, it would\n            // generate an unresolved LogicalPlan equivalent to the SQL query\n            // \"INSERT INTO t1 VALUES (1)\". This would fail with an error reporting the VALUES\n            // list is not long enough, since the analyzer would consider this equivalent to\n            // \"INSERT INTO t1 (j, s, x) VALUES (1)\".\n            Seq((1, 42L, 43L)).toDF(\"j\", \"s\", \"x\").write.mode(\"append\")\n              .format(\"delta\").saveAsTable(\"t1\")\n            Seq((2, 42L, 43L)).toDF(\"j\", \"s\", \"x\").write.mode(\"append\")\n              .format(\"delta\").saveAsTable(\"t1\")\n            Seq((3, 42L, 43L)).toDF(\"j\", \"s\", \"x\").write.mode(\"append\")\n              .format(\"delta\").saveAsTable(\"t1\")\n            Seq((4, 44L, 43L)).toDF(\"j\", \"s\", \"x\").write.mode(\"append\")\n              .format(\"delta\").saveAsTable(\"t1\")\n            Seq((5, 44L, 45L)).toDF(\"j\", \"s\", \"x\")\n              .write.mode(\"append\").format(\"delta\").saveAsTable(\"t1\")\n          } else {\n            sql(\"insert into t1(j) values(1)\")\n            sql(\"insert into t1(j, s) values(2, default)\")\n            sql(\"insert into t1(j, s, x) values(3, default, default)\")\n            sql(\"insert into t1(j, s) values(4, 44)\")\n            sql(\"insert into t1(j, s, x) values(5, 44, 45)\")\n          }\n          sql(s\"create table t2(j int, s bigint default 42, x bigint default 43) using $v2Format \" +\n            tblPropertiesAllowDefaults)\n          if (useDataFrames) {\n            // Use 'insertInto' to exercise mapping columns positionally.\n            spark.table(\"t1\").where(\"j = 1\").write.insertInto(\"t2\")\n            spark.table(\"t1\").where(\"j = 2\").write.insertInto(\"t2\")\n            spark.table(\"t1\").where(\"j = 3\").write.insertInto(\"t2\")\n            spark.table(\"t1\").where(\"j = 4\").write.insertInto(\"t2\")\n            spark.table(\"t1\").where(\"j = 5\").write.insertInto(\"t2\")\n          } else {\n            sql(\"insert into t2(j) select j from t1 where j = 1\")\n            sql(\"insert into t2(j, s) select j, default from t1 where j = 2\")\n            sql(\"insert into t2(j, s, x) select j, default, default from t1 where j = 3\")\n            sql(\"insert into t2(j, s) select j, s from t1 where j = 4\")\n            sql(\"insert into t2(j, s, x) select j, s, 45L from t1 where j = 5\")\n          }\n          checkAnswer(\n            spark.table(\"t2\"),\n            Row(1, 42L, 43L) ::\n              Row(2, 42L, 43L) ::\n              Row(3, 42L, 43L) ::\n              Row(4, 44L, 43L) ::\n              Row(5, 44L, 45L) :: Nil)\n          // Also exercise schema evolution with DataFrames.\n          if (useDataFrames) {\n            Seq((5, 44L, 45L, 46L)).toDF(\"j\", \"s\", \"x\", \"y\")\n              .write.mode(\"append\").format(\"delta\").option(\"mergeSchema\", \"true\")\n              .saveAsTable(\"t2\")\n            checkAnswer(\n              spark.table(\"t2\"),\n              Row(1, 42L, 43L, null) ::\n                Row(2, 42L, 43L, null) ::\n                Row(3, 42L, 43L, null) ::\n                Row(4, 44L, 43L, null) ::\n                Row(5, 44L, 45L, null) ::\n                Row(5, 44L, 45L, 46L) :: Nil)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"ReplaceWhere with column defaults with dataframe writes\") {\n    withTable(\"t1\", \"t2\", \"t3\") {\n      sql(s\"create table t1(j int, s bigint default 42, x bigint default 43) using $v2Format \" +\n        tblPropertiesAllowDefaults)\n      Seq((1, 42L, 43L)).toDF.write.insertInto(\"t1\")\n      Seq((2, 42L, 43L)).toDF.write.insertInto(\"t1\")\n      Seq((3, 42L, 43L)).toDF.write.insertInto(\"t1\")\n      Seq((4, 44L, 43L)).toDF.write.insertInto(\"t1\")\n      Seq((5, 44L, 45L)).toDF.write.insertInto(\"t1\")\n      spark.table(\"t1\")\n        .write.format(\"delta\")\n        .mode(\"overwrite\")\n        .option(\"replaceWhere\", \"j = default and s = default and x = default\")\n        .saveAsTable(\"t2\")\n      Seq(\"t1\", \"t2\").foreach { t =>\n        checkAnswer(\n          spark.table(t),\n          Row(1, 42L, 43L) ::\n            Row(2, 42L, 43L) ::\n            Row(3, 42L, 43L) ::\n            Row(4, 44L, 43L) ::\n            Row(5, 44L, 45L) :: Nil)\n      }\n    }\n  }\n\n  test(\"DESCRIBE and SHOW CREATE TABLE with column defaults\") {\n    withTable(\"t\") {\n      spark.sql(s\"CREATE TABLE t (id bigint default 42) \" +\n        s\"using $v2Format $tblPropertiesAllowDefaults\")\n      val descriptionDf = spark.sql(s\"DESCRIBE TABLE EXTENDED t\")\n      assert(descriptionDf.schema.map { field =>\n        (field.name, field.dataType)\n      } === Seq(\n        (\"col_name\", StringType),\n        (\"data_type\", StringType),\n        (\"comment\", StringType)))\n      QueryTest.checkAnswer(\n        descriptionDf.filter(\n          \"!(col_name in ('Catalog', 'Created Time', 'Created By', 'Database', \" +\n            \"'index', 'Is_managed_location', 'Location', 'Name', 'Owner', 'Partition Provider',\" +\n            \"'Provider', 'Table', 'Table Properties',  'Type', '_partition', 'Last Access', \" +\n            \"'Statistics', ''))\"),\n        Seq(\n          Row(\"# Column Default Values\", \"\", \"\"),\n          Row(\"# Detailed Table Information\", \"\", \"\"),\n          Row(\"id\", \"bigint\", \"42\"),\n          Row(\"id\", \"bigint\", null)\n        ))\n    }\n    withTable(\"t\") {\n      sql(\n        s\"\"\"\n           |CREATE TABLE t (\n           |  a bigint NOT NULL,\n           |  b bigint DEFAULT 42,\n           |  c string DEFAULT 'abc, \"def\"' COMMENT 'comment'\n           |)\n           |USING parquet\n           |COMMENT 'This is a comment'\n           |$tblPropertiesAllowDefaults\n        \"\"\".stripMargin)\n      val currentCatalog = spark.sessionState.catalogManager.currentCatalog.name()\n      QueryTest.checkAnswer(sql(\"SHOW CREATE TABLE T\"),\n        Seq(\n          Row(\n            s\"\"\"CREATE TABLE ${currentCatalog}.default.T (\n               |  a BIGINT,\n               |  b BIGINT DEFAULT 42,\n               |  c STRING DEFAULT 'abc, \"def\"' COMMENT 'comment')\n               |USING parquet\n               |COMMENT 'This is a comment'\n               |TBLPROPERTIES (\n               |  'delta.columnMapping.mode' = 'name',\n               |  'delta.feature.allowColumnDefaults' = 'enabled')\n               |\"\"\".stripMargin)))\n    }\n  }\n}\n\n/** These tests come from Apache Spark with some modifications to match Delta behavior. */\nabstract class DeltaInsertIntoTests(\n    override protected val supportsDynamicOverwrite: Boolean,\n    override protected val includeSQLOnlyTests: Boolean)\n  extends InsertIntoSQLOnlyTests {\n\n  import testImplicits._\n\n  override def afterEach(): Unit = {\n    spark.catalog.listTables().collect().foreach(t =>\n      sql(s\"drop table ${t.name}\"))\n    super.afterEach()\n  }\n\n  // START Apache Spark tests\n\n  /**\n   * Insert data into a table using the insertInto statement. Implementations can be in SQL\n   * (\"INSERT\") or using the DataFrameWriter (`df.write.insertInto`). Insertions will be\n   * by column ordinal and not by column name.\n   */\n  protected def doInsert(tableName: String, insert: DataFrame, mode: SaveMode = null): Unit\n\n  test(\"insertInto: append\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n    val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    doInsert(t1, df)\n    verifyTable(t1, df)\n  }\n\n  test(\"insertInto: append by position\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n    val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    val dfr = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"data\", \"id\")\n\n    doInsert(t1, dfr)\n    verifyTable(t1, df)\n  }\n\n  test(\"insertInto: append cast automatically\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n    val df = Seq((1, \"a\"), (2, \"b\"), (3, \"c\")).toDF(\"id\", \"data\")\n    doInsert(t1, df)\n    verifyTable(t1, df)\n  }\n\n\n  test(\"insertInto: append partitioned table\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n      val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n      doInsert(t1, df)\n      verifyTable(t1, df)\n    }\n  }\n\n  test(\"insertInto: overwrite non-partitioned table\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n    val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    val df2 = Seq((4L, \"d\"), (5L, \"e\"), (6L, \"f\")).toDF(\"id\", \"data\")\n    doInsert(t1, df)\n    doInsert(t1, df2, SaveMode.Overwrite)\n    verifyTable(t1, df2)\n  }\n\n  test(\"insertInto: overwrite partitioned table in static mode\") {\n    withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n      val t1 = \"tbl\"\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n      val init = Seq((2L, \"dummy\"), (4L, \"keep\")).toDF(\"id\", \"data\")\n      doInsert(t1, init)\n\n      val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n      doInsert(t1, df, SaveMode.Overwrite)\n      verifyTable(t1, df)\n    }\n  }\n\n\n  test(\"insertInto: overwrite by position\") {\n    withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n      val t1 = \"tbl\"\n      withTable(t1) {\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n        val init = Seq((2L, \"dummy\"), (4L, \"keep\")).toDF(\"id\", \"data\")\n        doInsert(t1, init)\n\n        val dfr = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"data\", \"id\")\n        doInsert(t1, dfr, SaveMode.Overwrite)\n\n        val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n        verifyTable(t1, df)\n      }\n    }\n  }\n\n  test(\"insertInto: overwrite cast automatically\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n    val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    val df2 = Seq((4L, \"d\"), (5L, \"e\"), (6L, \"f\")).toDF(\"id\", \"data\")\n    val df2c = Seq((4, \"d\"), (5, \"e\"), (6, \"f\")).toDF(\"id\", \"data\")\n    doInsert(t1, df)\n    doInsert(t1, df2c, SaveMode.Overwrite)\n    verifyTable(t1, df2)\n  }\n\n  test(\"insertInto: fails when missing a column\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string, missing string) USING $v2Format\")\n    val df1 = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    // mismatched datatype\n    val df2 = Seq((1, \"a\"), (2, \"b\"), (3, \"c\")).toDF(\"id\", \"data\")\n    for (df <- Seq(df1, df2)) {\n      val exc = intercept[AnalysisException] {\n        doInsert(t1, df)\n      }\n      verifyTable(t1, Seq.empty[(Long, String, String)].toDF(\"id\", \"data\", \"missing\"))\n      assert(exc.getMessage.contains(\"not enough data columns\"))\n    }\n  }\n\n  test(\"insertInto: overwrite fails when missing a column\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string, missing string) USING $v2Format\")\n    val df1 = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    // mismatched datatype\n    val df2 = Seq((1, \"a\"), (2, \"b\"), (3, \"c\")).toDF(\"id\", \"data\")\n    for (df <- Seq(df1, df2)) {\n      val exc = intercept[AnalysisException] {\n        doInsert(t1, df, SaveMode.Overwrite)\n      }\n      verifyTable(t1, Seq.empty[(Long, String, String)].toDF(\"id\", \"data\", \"missing\"))\n      assert(exc.getMessage.contains(\"not enough data columns\"))\n    }\n  }\n\n  // This behavior is specific to Delta\n  test(\"insertInto: fails when an extra column is present but can evolve schema\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n      val df = Seq((1L, \"a\", \"mango\")).toDF(\"id\", \"data\", \"fruit\")\n      val exc = intercept[AnalysisException] {\n        doInsert(t1, df)\n      }\n\n      verifyTable(t1, Seq.empty[(Long, String)].toDF(\"id\", \"data\"))\n      assert(exc.getMessage.contains(s\"mergeSchema\"))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        doInsert(t1, df)\n      }\n      verifyTable(t1, Seq((1L, \"a\", \"mango\")).toDF(\"id\", \"data\", \"fruit\"))\n    }\n  }\n\n  test(\"insertInto: UTC timestamp partition values round trip across different session TZ\") {\n    val t1 = \"utc_timestamp_partitioned_values\"\n    withTable(t1) {\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"UTC\") {\n        sql(s\"CREATE TABLE $t1 (data int, ts timestamp) USING delta PARTITIONED BY (ts)\")\n        sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2024-06-15T04:00:00UTC')\")\n        sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2024-06-15T4:00:00UTC+8')\")\n        sql(s\"INSERT INTO $t1 VALUES (3, timestamp'2024-06-15T5:00:00 UTC+01:00')\")\n        sql(s\"INSERT INTO $t1 VALUES (4, timestamp'2024-06-16T5:00:00.123456UTC')\")\n        sql(s\"INSERT INTO $t1 VALUES (5, timestamp'1903-12-28T5:00:00')\")\n      }\n\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n        val deltaLog = DeltaLog.forTable(\n          spark, TableIdentifier(t1))\n        val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n        val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n        checkAnswer(\n          allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n          Seq(\n            Row(Map(partitionColName -> \"2024-06-15T04:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2024-06-14T20:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2024-06-15T04:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2024-06-16T05:00:00.123456Z\")),\n            Row(Map(partitionColName -> \"1903-12-28T05:00:00.000000Z\"))\n          ))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-15T4:00:00UTC+8'\"),\n          Seq(Row(2)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-14T20:00:00UTC-08'\"),\n          Seq(Row(1), Row(3)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'1903-12-27T21:00:00UTC-08'\"),\n          Seq(Row(5)))\n\n        checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(4)))\n      }\n    }\n  }\n\n  test(\"insertInto: timestamp partition values across different\" +\n    \" non-UTC session timezones round-trip when UTC adjusted\") {\n    val t1 = \"utc_write_and_read_non_utc_tz\"\n    withTable(t1) {\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"America/Los_Angeles\") {\n        sql(s\"CREATE TABLE $t1 (data int, ts timestamp) USING delta PARTITIONED BY (ts)\")\n        sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2025-11-26T12:00:00')\")\n        sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2025-11-27T4:00:00UTC+8')\")\n        sql(s\"INSERT INTO $t1 VALUES (3, timestamp'2025-11-28T5:00:00 UTC+01:00')\")\n      }\n\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"Europe/Berlin\") {\n        val deltaLog = DeltaLog.forTable(\n          spark, TableIdentifier(t1))\n        val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n        val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n        checkAnswer(\n          allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n          Seq(\n            Row(Map(partitionColName -> \"2025-11-26T20:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2025-11-26T20:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2025-11-28T04:00:00.000000Z\"))))\n\n        // Berlin is UTC+1\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26T21:00:00' order by data\"),\n          Seq(Row(1), Row(2)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-28T5:00:00'\"),\n          Seq(Row(3)))\n\n        checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(2)))\n      }\n    }\n  }\n\n  test(\"insertInto: partition and non-partitioned timestamps have some behavior across timezones\") {\n    val t1 = \"utc_partition_and_non_partitioned_ts\"\n    withTable(t1) {\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"Asia/Kolkata\") {\n        sql(s\"CREATE TABLE $t1 (data int, ts_partition timestamp, ts_value timestamp) \" +\n          s\"USING delta PARTITIONED BY (ts_partition)\")\n        sql(s\"INSERT INTO $t1 VALUES \" +\n          s\"(1, timestamp'2025-11-27T01:30:00', timestamp'2025-11-27T01:30:00')\")\n        sql(s\"INSERT INTO $t1 VALUES \" +\n          s\"(2, timestamp'2025-11-27T4:00:00UTC+8', timestamp'2025-11-27T4:00:00UTC+8')\")\n        sql(s\"INSERT INTO $t1 VALUES \" +\n          s\"(3, timestamp'2025-11-28T5:00:00 UTC+01:00', timestamp'2025-11-28T5:00:00 UTC+01:00')\")\n      }\n\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"America/Los_Angeles\") {\n        val deltaLog = DeltaLog.forTable(\n          spark, TableIdentifier(t1))\n        val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n        val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n        checkAnswer(\n          allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n          Seq(\n            Row(Map(partitionColName -> \"2025-11-26T20:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2025-11-26T20:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2025-11-28T04:00:00.000000Z\"))))\n\n        // America/Los_Angeles is UTC-8\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where \" +\n            s\"ts_partition = timestamp'2025-11-26T12:00:00' and ts_value='2025-11-26T12:00:00' \" +\n            s\"order by data\"),\n          Seq(Row(1), Row(2)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where \" +\n            s\"ts_partition = timestamp'2025-11-27T20:00:00' \" +\n            s\"and ts_value = timestamp'2025-11-27T20:00:00'\"),\n          Seq(Row(3)))\n\n        checkAnswer(sql(s\"SELECT count(distinct(ts_partition)) from $t1\"), Seq(Row(2)))\n        checkAnswer(sql(s\"SELECT count(distinct(ts_value)) from $t1\"), Seq(Row(2)))\n      }\n    }\n  }\n\n  test(\"insertInto: Non-UTC and UTC partition values round trip same session TZ\") {\n    val t1 = \"utc_timestamp_partitioned_values\"\n    withTable(t1) {\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n        sql(s\"CREATE TABLE $t1 (data int, ts timestamp) USING delta PARTITIONED BY (ts)\")\n        sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2024-06-15T4:00:00UTC')\")\n        sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2024-06-15T4:00:00UTC+8')\")\n      }\n\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n        withSQLConf(DeltaSQLConf.UTC_TIMESTAMP_PARTITION_VALUES.key -> \"false\") {\n          sql(s\"INSERT INTO $t1 VALUES (3, timestamp'2024-06-15T5:00:00 UTC+01:00')\")\n          sql(s\"INSERT INTO $t1 VALUES (4, timestamp'1903-12-28T5:00:00')\")\n        }\n\n        val deltaLog = DeltaLog.forTable(\n          spark, TableIdentifier(t1))\n        val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n        val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n        checkAnswer(\n          allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n          Seq(\n            Row(Map(partitionColName -> \"2024-06-15T04:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2024-06-14T20:00:00.000000Z\")),\n            Row(Map(partitionColName -> \"2024-06-14 20:00:00\")),\n            Row(Map(partitionColName -> \"1903-12-28 05:00:00\"))\n          ))\n      }\n\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-15T4:00:00UTC+8'\"),\n          Seq(Row(2)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-14T20:00:00'\"),\n          Seq(Row(1), Row(3)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'1903-12-28T05:00:00'\"),\n          Seq(Row(4)))\n\n        checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(3)))\n      }\n    }\n  }\n\n  test(\"insertInto: Timestamp No Timezone can be interpreted across timezones\") {\n    val t1 = \"timestamp_ntz\"\n    withTable(t1) {\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n        sql(s\"CREATE TABLE $t1 (data int, ts timestamp_ntz) USING delta PARTITIONED BY (ts)\")\n        sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2024-06-15T4:00:00')\")\n        sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2024-06-16T5:00:00')\")\n        sql(s\"INSERT INTO $t1 VALUES (3, timestamp'1903-12-28T5:00:00')\")\n\n        val deltaLog = DeltaLog.forTable(\n          spark, TableIdentifier(t1))\n        val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n        val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n        checkAnswer(\n          allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n          Seq(\n            Row(Map(partitionColName -> \"2024-06-15 04:00:00\")),\n            Row(Map(partitionColName -> \"2024-06-16 05:00:00\")),\n            Row(Map(partitionColName -> \"1903-12-28 05:00:00\"))\n          ))\n      }\n\n      withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"UTC-03:00\") {\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-15T4:00:00'\"),\n          Seq(Row(1)))\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-16T05:00:00'\"),\n          Seq(Row(2)))\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'1903-12-28T05:00:00'\"),\n          Seq(Row(3)))\n        checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(3)))\n      }\n    }\n  }\n\n  test(\"insertInto: Timestamp round trips across same session time zone: UTC normalized\") {\n    val t1 = \"utc_timestamp_partitioned_values\"\n\n    withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n      sql(s\"CREATE TABLE $t1 (data int, ts timestamp) USING delta PARTITIONED BY (ts)\")\n      sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2024-06-15 04:00:00UTC')\")\n      sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2024-06-15T4:00:00UTC+8')\")\n      sql(s\"INSERT INTO $t1 VALUES (3, timestamp'2024-06-15T5:00:00 UTC+01:00')\")\n      val deltaLog = DeltaLog.forTable(\n        spark, TableIdentifier(t1))\n      val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n      val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n      checkAnswer(\n        allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n        Seq(\n          Row(Map(partitionColName -> \"2024-06-15T04:00:00.000000Z\")),\n          Row(Map(partitionColName -> \"2024-06-14T20:00:00.000000Z\")),\n          Row(Map(partitionColName -> \"2024-06-15T04:00:00.000000Z\"))\n        ))\n\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-15T04:00:00UTC'\"),\n        Seq(Row(1), Row(3)))\n\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-14T20:00:00UTC'\"),\n        Seq(Row(2)))\n\n      checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(2)))\n    }\n  }\n\n  test(\"insertInto: Timestamp round trips across same session time zone: session time normalized\") {\n    val t1 = \"utc_timestamp_partitioned_values\"\n\n    withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"UTC\") {\n      withSQLConf(DeltaSQLConf.UTC_TIMESTAMP_PARTITION_VALUES.key -> \"false\") {\n        sql(s\"CREATE TABLE $t1 (data int, ts timestamp) USING delta PARTITIONED BY (ts)\")\n        sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2024-06-15 04:00:00UTC+08:00')\")\n        sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2024-06-15T4:00:00UTC-08:00')\")\n        sql(s\"INSERT INTO $t1 VALUES (3, timestamp'2024-06-15T5:00:00UTC+09:00')\")\n        val deltaLog = DeltaLog.forTable(\n          spark, TableIdentifier(t1))\n        val allFiles = deltaLog.unsafeVolatileSnapshot.allFiles\n        val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n\n        checkAnswer(\n          allFiles.select(\"partitionValues\").orderBy(\"modificationTime\"),\n          Seq(\n            Row(Map(partitionColName -> \"2024-06-14 20:00:00\")),\n            Row(Map(partitionColName -> \"2024-06-15 12:00:00\")),\n            Row(Map(partitionColName -> \"2024-06-14 20:00:00\"))\n          ))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-14T20:00:00'\"),\n          Seq(Row(1), Row(3)))\n\n        checkAnswer(\n          sql(s\"SELECT data FROM $t1 where ts = timestamp'2024-06-15T12:00:00'\"),\n          Seq(Row(2)))\n\n        checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(2)))\n      }\n    }\n  }\n\n  test(\"insertInto: timestamp partition values with different precisions\") {\n    val t1 = \"utc_timestamp_partitioned_values_different_precisions\"\n    withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> \"GMT-8\") {\n      sql(s\"CREATE TABLE $t1 (data int, ts timestamp) USING delta PARTITIONED BY (ts)\")\n      sql(s\"INSERT INTO $t1 VALUES (1, timestamp'2025-11-26 04:00:00.1')\")\n      sql(s\"INSERT INTO $t1 VALUES (2, timestamp'2025-11-26 04:00:00.12')\")\n      sql(s\"INSERT INTO $t1 VALUES (3, timestamp'2025-11-26 04:00:00.123')\")\n      sql(s\"INSERT INTO $t1 VALUES (4, timestamp'2025-11-26 04:00:00.1234')\")\n      sql(s\"INSERT INTO $t1 VALUES (5, timestamp'2025-11-26 04:00:00.12345')\")\n      sql(s\"INSERT INTO $t1 VALUES (6, timestamp'2025-11-26 04:00:00.123456')\")\n\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26 04:00:00.1'\"), Seq(Row(1)))\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26 04:00:00.12'\"), Seq(Row(2)))\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26 04:00:00.123'\"), Seq(Row(3)))\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26 04:00:00.1234'\"), Seq(Row(4)))\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26 04:00:00.12345'\"), Seq(Row(5)))\n      checkAnswer(\n        sql(s\"SELECT data FROM $t1 where ts = timestamp'2025-11-26 04:00:00.123456'\"), Seq(Row(6)))\n\n      checkAnswer(sql(s\"SELECT count(distinct(ts)) from $t1\"), Seq(Row(6)))\n    }\n  }\n\n  // FIXME: Documenting existing behaviour. Fixing this should be a bugfix and not behavior change.\n  test(\"insertInto: __HIVE_DEFAULT_PARTITION__ results in null partition column\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (part string, data string) USING $v2Format PARTITIONED BY (part)\")\n\n      // Insert with __HIVE_DEFAULT_PARTITION__ as partition value\n      // __HIVE_DEFAULT_PARTITION__ is a tombstone value for null partition column\n      sql(s\"INSERT INTO $t1 VALUES ('__HIVE_DEFAULT_PARTITION__', 'test')\")\n\n      // Verify that the partition column is null\n      checkAnswer(\n        sql(s\"SELECT part, data FROM $t1\"),\n        Seq(Row(null, \"test\"))\n      )\n    }\n  }\n\n  // This behavior is specific to Delta\n  testQuietly(\"insertInto: schema enforcement\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n    val df = Seq((\"a\", 1L)).toDF(\"id\", \"data\") // reverse order\n\n    def getDF(rows: Row*): DataFrame = {\n      spark.createDataFrame(spark.sparkContext.parallelize(rows), spark.table(t1).schema)\n    }\n\n    withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"strict\") {\n      intercept[AnalysisException] {\n        doInsert(t1, df, SaveMode.Overwrite)\n      }\n\n      verifyTable(t1, Seq.empty[(Long, String)].toDF(\"id\", \"data\"))\n\n      intercept[AnalysisException] {\n        doInsert(t1, df)\n      }\n\n      verifyTable(t1, Seq.empty[(Long, String)].toDF(\"id\", \"data\"))\n    }\n\n    withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"ansi\") {\n      intercept[SparkException] {\n        doInsert(t1, df, SaveMode.Overwrite)\n      }\n\n      verifyTable(t1, Seq.empty[(Long, String)].toDF(\"id\", \"data\"))\n\n      intercept[SparkException] {\n        doInsert(t1, df)\n      }\n\n      verifyTable(t1, Seq.empty[(Long, String)].toDF(\"id\", \"data\"))\n    }\n\n    withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"legacy\") {\n      doInsert(t1, df, SaveMode.Overwrite)\n      verifyTable(\n        t1,\n        getDF(Row(null, \"1\")))\n\n      doInsert(t1, df)\n\n      verifyTable(\n        t1,\n        getDF(Row(null, \"1\"), Row(null, \"1\")))\n    }\n  }\n\n  testQuietly(\"insertInto: struct types and schema enforcement\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(\n        s\"\"\"CREATE TABLE $t1 (\n           |  id bigint,\n           |  point struct<x: double, y: double>\n           |)\n           |USING delta\"\"\".stripMargin)\n      val init = Seq((1L, (0.0, 1.0))).toDF(\"id\", \"point\")\n      doInsert(t1, init)\n\n      doInsert(t1, Seq((2L, (1.0, 0.0))).toDF(\"col1\", \"col2\")) // naming doesn't matter\n\n      // can handle null types\n      doInsert(t1, Seq((3L, (1.0, null))).toDF(\"col1\", \"col2\"))\n      doInsert(t1, Seq((4L, (null, 1.0))).toDF(\"col1\", \"col2\"))\n\n      val expected = Seq(\n        Row(1L, Row(0.0, 1.0)),\n        Row(2L, Row(1.0, 0.0)),\n        Row(3L, Row(1.0, null)),\n        Row(4L, Row(null, 1.0)))\n      verifyTable(\n        t1,\n        spark.createDataFrame(expected.asJava, spark.table(t1).schema))\n\n      // schema enforcement\n      val complexSchema = Seq((5L, (0.5, 0.5), (2.5, 2.5, 1.0), \"a\", (0.5, \"b\")))\n        .toDF(\"long\", \"struct\", \"newstruct\", \"string\", \"badstruct\")\n        .select(\n          $\"long\",\n          $\"struct\",\n          struct(\n            $\"newstruct._1\".as(\"x\"),\n            $\"newstruct._2\".as(\"y\"),\n            $\"newstruct._3\".as(\"z\")) as \"newstruct\",\n          $\"string\",\n          $\"badstruct\")\n\n      // new column in root\n      intercept[AnalysisException] {\n        doInsert(t1, complexSchema.select(\"long\", \"struct\", \"string\"))\n      }\n\n      // new column in struct not accepted\n      intercept[AnalysisException] {\n        doInsert(t1, complexSchema.select(\"long\", \"newstruct\"))\n      }\n\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"strict\") {\n        // bad data type not accepted\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"string\", \"struct\"))\n        }\n\n        // nested bad data type in struct not accepted\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"long\", \"badstruct\"))\n        }\n      }\n\n      // missing column in struct\n      intercept[AnalysisException] {\n        doInsert(t1, complexSchema.select($\"long\", struct(lit(0.1))))\n      }\n\n      // wrong ordering\n      intercept[AnalysisException] {\n        doInsert(t1, complexSchema.select(\"struct\", \"long\"))\n      }\n\n      // schema evolution\n      withSQLConf(\n          DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\",\n          SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"strict\") {\n        // ordering should still match\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"struct\", \"long\"))\n        }\n\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"struct\", \"long\", \"string\"))\n        }\n\n        // new column to the end works\n        doInsert(t1, complexSchema.select($\"long\", $\"struct\", $\"string\".as(\"letter\")))\n\n        // still cannot insert missing column\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"long\", \"struct\"))\n        }\n\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select($\"long\", struct(lit(0.1)), $\"string\"))\n        }\n\n        // still perform nested data type checks\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"long\", \"badstruct\", \"string\"))\n        }\n\n        // bad column within struct\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\n            $\"long\", struct(lit(0.1), lit(\"a\"), lit(0.2)), $\"string\"))\n        }\n\n        // Add column to nested field\n        doInsert(t1, complexSchema.select($\"long\", $\"newstruct\", lit(null)))\n\n        // cannot insert missing field into struct now\n        intercept[AnalysisException] {\n          doInsert(t1, complexSchema.select(\"long\", \"struct\", \"string\"))\n        }\n      }\n\n      val expected2 = Seq(\n        Row(1L, Row(0.0, 1.0, null), null),\n        Row(2L, Row(1.0, 0.0, null), null),\n        Row(3L, Row(1.0, null, null), null),\n        Row(4L, Row(null, 1.0, null), null),\n        Row(5L, Row(0.5, 0.5, null), \"a\"),\n        Row(5L, Row(2.5, 2.5, 1.0), null))\n      verifyTable(\n        t1,\n        spark.createDataFrame(expected2.asJava, spark.table(t1).schema))\n\n      val expectedSchema = new StructType()\n        .add(\"id\", LongType)\n        .add(\"point\", new StructType()\n          .add(\"x\", DoubleType)\n          .add(\"y\", DoubleType)\n          .add(\"z\", DoubleType))\n        .add(\"letter\", StringType)\n      val diff = SchemaUtils.reportDifferences(spark.table(t1).schema, expectedSchema)\n      if (diff.nonEmpty) {\n        fail(diff.mkString(\"\\n\"))\n      }\n    }\n  }\n\n  dynamicOverwriteTest(\"insertInto: overwrite partitioned table in dynamic mode\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n      val init = Seq((2L, \"dummy\"), (4L, \"keep\")).toDF(\"id\", \"data\")\n      doInsert(t1, init)\n\n      val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n      doInsert(t1, df, SaveMode.Overwrite)\n\n      verifyTable(t1, df.union(sql(\"SELECT 4L, 'keep'\")))\n    }\n  }\n\n  dynamicOverwriteTest(\"insertInto: overwrite partitioned table in dynamic mode by position\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n      val init = Seq((2L, \"dummy\"), (4L, \"keep\")).toDF(\"id\", \"data\")\n      doInsert(t1, init)\n\n      val dfr = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"data\", \"id\")\n      doInsert(t1, dfr, SaveMode.Overwrite)\n\n      val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\"), (4L, \"keep\")).toDF(\"id\", \"data\")\n      verifyTable(t1, df)\n    }\n  }\n\n  dynamicOverwriteTest(\n    \"insertInto: overwrite partitioned table in dynamic mode automatic casting\") {\n    val t1 = \"tbl\"\n    withTable(t1) {\n      sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n      val init = Seq((2L, \"dummy\"), (4L, \"keep\")).toDF(\"id\", \"data\")\n      doInsert(t1, init)\n\n      val df = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n      val dfc = Seq((1, \"a\"), (2, \"b\"), (3, \"c\")).toDF(\"id\", \"data\")\n      doInsert(t1, df, SaveMode.Overwrite)\n\n      verifyTable(t1, df.union(sql(\"SELECT 4L, 'keep'\")))\n    }\n  }\n\n  dynamicOverwriteTest(\"insertInto: overwrite fails when missing a column in dynamic mode\") {\n    val t1 = \"tbl\"\n    sql(s\"CREATE TABLE $t1 (id bigint, data string, missing string) USING $v2Format\")\n    val df1 = Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\")).toDF(\"id\", \"data\")\n    // mismatched datatype\n    val df2 = Seq((1, \"a\"), (2, \"b\"), (3, \"c\")).toDF(\"id\", \"data\")\n    for (df <- Seq(df1, df2)) {\n      val exc = intercept[AnalysisException] {\n        doInsert(t1, df, SaveMode.Overwrite)\n      }\n      verifyTable(t1, Seq.empty[(Long, String, String)].toDF(\"id\", \"data\", \"missing\"))\n      assert(exc.getMessage.contains(\"not enough data columns\"))\n    }\n  }\n\n  test(\"insert nested struct from view into delta\") {\n    withTable(\"testNestedStruct\") {\n      sql(s\"CREATE TABLE testNestedStruct \" +\n        s\" (num INT, text STRING, s STRUCT<a:STRING, s2: STRUCT<c:STRING,d:STRING>, b:STRING>)\" +\n        s\" USING DELTA\")\n      val data = sql(s\"SELECT 1, 'a', struct('a', struct('c', 'd'), 'b')\")\n      doInsert(\"testNestedStruct\", data)\n      verifyTable(\"testNestedStruct\",\n        sql(s\"SELECT 1 AS num, 'a' AS text, struct('a', struct('c', 'd') AS s2, 'b') AS s\"))\n    }\n  }\n}\n\ntrait InsertIntoSQLOnlyTests\n    extends QueryTest\n    with SharedSparkSession\n    with BeforeAndAfter {\n\n  import testImplicits._\n\n  /** Check that the results in `tableName` match the `expected` DataFrame. */\n  protected def verifyTable(tableName: String, expected: DataFrame): Unit = {\n    checkAnswer(spark.table(tableName), expected)\n  }\n\n  protected val v2Format: String = \"delta\"\n\n  /**\n   * Whether dynamic partition overwrites are supported by the `Table` definitions used in the\n   * test suites. Tables that leverage the V1 Write interface do not support dynamic partition\n   * overwrites.\n   */\n  protected val supportsDynamicOverwrite: Boolean\n\n  /** Whether to include the SQL specific tests in this trait within the extending test suite. */\n  protected val includeSQLOnlyTests: Boolean\n\n  private def withTableAndData(tableName: String)(testFn: String => Unit): Unit = {\n    withTable(tableName) {\n      val viewName = \"tmp_view\"\n      val df = spark.createDataFrame(Seq((1L, \"a\"), (2L, \"b\"), (3L, \"c\"))).toDF(\"id\", \"data\")\n      df.createOrReplaceTempView(viewName)\n      withTempView(viewName) {\n        testFn(viewName)\n      }\n    }\n  }\n\n  protected def dynamicOverwriteTest(testName: String)(f: => Unit): Unit = {\n    test(testName) {\n      try {\n        withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.DYNAMIC.toString) {\n          f\n        }\n        if (!supportsDynamicOverwrite) {\n          fail(\"Expected failure from test, because the table doesn't support dynamic overwrites\")\n        }\n      } catch {\n        case a: AnalysisException if !supportsDynamicOverwrite =>\n          assert(a.getMessage.contains(\"does not support dynamic overwrite\"))\n      }\n    }\n  }\n\n  if (includeSQLOnlyTests) {\n    test(\"InsertInto: when the table doesn't exist\") {\n      val t1 = \"tbl\"\n      val t2 = \"tbl2\"\n      withTableAndData(t1) { _ =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format\")\n        val e = intercept[AnalysisException] {\n          sql(s\"INSERT INTO $t2 VALUES (2L, 'dummy')\")\n        }\n        assert(e.getMessage.contains(t2))\n        assert(e.getMessage.contains(\"Table not found\") ||\n          e.getMessage.contains(s\"table or view `$t2` cannot be found\")\n        )\n      }\n    }\n\n    test(\"InsertInto: append to partitioned table - static clause\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n        sql(s\"INSERT INTO $t1 PARTITION (id = 23) SELECT data FROM $view\")\n        verifyTable(t1, sql(s\"SELECT 23, data FROM $view\"))\n      }\n    }\n\n    test(\"InsertInto: static PARTITION clause fails with non-partition column\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (data)\")\n\n        val exc = intercept[AnalysisException] {\n          sql(s\"INSERT INTO TABLE $t1 PARTITION (id=1) SELECT data FROM $view\")\n        }\n\n        verifyTable(t1, spark.emptyDataFrame)\n        assert(\n          exc.getMessage.contains(\"PARTITION clause cannot contain a non-partition column\") ||\n          exc.getMessage.contains(\"PARTITION clause cannot contain the non-partition column\") ||\n          exc.getMessage.contains(\n            \"[NON_PARTITION_COLUMN] PARTITION clause cannot contain the non-partition column\"))\n        assert(exc.getMessage.contains(\"id\"))\n      }\n    }\n\n    test(\"InsertInto: dynamic PARTITION clause fails with non-partition column\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n\n        val exc = intercept[AnalysisException] {\n          sql(s\"INSERT INTO TABLE $t1 PARTITION (data) SELECT * FROM $view\")\n        }\n\n        verifyTable(t1, spark.emptyDataFrame)\n        assert(\n          exc.getMessage.contains(\"PARTITION clause cannot contain a non-partition column\") ||\n          exc.getMessage.contains(\"PARTITION clause cannot contain the non-partition column\") ||\n          exc.getMessage.contains(\n            \"[NON_PARTITION_COLUMN] PARTITION clause cannot contain the non-partition column\"))\n        assert(exc.getMessage.contains(\"data\"))\n      }\n    }\n\n    test(\"InsertInto: overwrite - dynamic clause - static mode\") {\n      withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n        val t1 = \"tbl\"\n        withTableAndData(t1) { view =>\n          sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n          sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy'), (4L, 'also-deleted')\")\n          sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (id) SELECT * FROM $view\")\n          verifyTable(t1, Seq(\n            (1, \"a\"),\n            (2, \"b\"),\n            (3, \"c\")).toDF())\n        }\n      }\n    }\n\n    dynamicOverwriteTest(\"InsertInto: overwrite - dynamic clause - dynamic mode\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy'), (4L, 'keep')\")\n        sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (id) SELECT * FROM $view\")\n        verifyTable(t1, Seq(\n          (1, \"a\"),\n          (2, \"b\"),\n          (3, \"c\"),\n          (4, \"keep\")).toDF(\"id\", \"data\"))\n      }\n    }\n\n    test(\"InsertInto: overwrite - missing clause - static mode\") {\n      withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n        val t1 = \"tbl\"\n        withTableAndData(t1) { view =>\n          sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n          sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy'), (4L, 'also-deleted')\")\n          sql(s\"INSERT OVERWRITE TABLE $t1 SELECT * FROM $view\")\n          verifyTable(t1, Seq(\n            (1, \"a\"),\n            (2, \"b\"),\n            (3, \"c\")).toDF(\"id\", \"data\"))\n        }\n      }\n    }\n\n    dynamicOverwriteTest(\"InsertInto: overwrite - missing clause - dynamic mode\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string) USING $v2Format PARTITIONED BY (id)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy'), (4L, 'keep')\")\n        sql(s\"INSERT OVERWRITE TABLE $t1 SELECT * FROM $view\")\n        verifyTable(t1, Seq(\n          (1, \"a\"),\n          (2, \"b\"),\n          (3, \"c\"),\n          (4, \"keep\")).toDF(\"id\", \"data\"))\n      }\n    }\n\n    test(\"InsertInto: overwrite - static clause\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string, p1 int) \" +\n            s\"USING $v2Format PARTITIONED BY (p1)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 23), (4L, 'keep', 2)\")\n        verifyTable(t1, Seq(\n          (2L, \"dummy\", 23),\n          (4L, \"keep\", 2)).toDF(\"id\", \"data\", \"p1\"))\n        sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (p1 = 23) SELECT * FROM $view\")\n        verifyTable(t1, Seq(\n          (1, \"a\", 23),\n          (2, \"b\", 23),\n          (3, \"c\", 23),\n          (4, \"keep\", 2)).toDF(\"id\", \"data\", \"p1\"))\n      }\n    }\n\n    test(\"InsertInto: overwrite - mixed clause - static mode\") {\n      withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n        val t1 = \"tbl\"\n        withTableAndData(t1) { view =>\n          sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n              s\"USING $v2Format PARTITIONED BY (id, p)\")\n          sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'also-deleted', 2)\")\n          sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (id, p = 2) SELECT * FROM $view\")\n          verifyTable(t1, Seq(\n            (1, \"a\", 2),\n            (2, \"b\", 2),\n            (3, \"c\", 2)).toDF(\"id\", \"data\", \"p\"))\n        }\n      }\n    }\n\n    test(\"InsertInto: overwrite - mixed clause reordered - static mode\") {\n      withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n        val t1 = \"tbl\"\n        withTableAndData(t1) { view =>\n          sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n              s\"USING $v2Format PARTITIONED BY (id, p)\")\n          sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'also-deleted', 2)\")\n          sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (p = 2, id) SELECT * FROM $view\")\n          verifyTable(t1, Seq(\n            (1, \"a\", 2),\n            (2, \"b\", 2),\n            (3, \"c\", 2)).toDF(\"id\", \"data\", \"p\"))\n        }\n      }\n    }\n\n    test(\"InsertInto: overwrite - implicit dynamic partition - static mode\") {\n      withSQLConf(PARTITION_OVERWRITE_MODE.key -> PartitionOverwriteMode.STATIC.toString) {\n        val t1 = \"tbl\"\n        withTableAndData(t1) { view =>\n          sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n              s\"USING $v2Format PARTITIONED BY (id, p)\")\n          sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'also-deleted', 2)\")\n          sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (p = 2) SELECT * FROM $view\")\n          verifyTable(t1, Seq(\n            (1, \"a\", 2),\n            (2, \"b\", 2),\n            (3, \"c\", 2)).toDF(\"id\", \"data\", \"p\"))\n        }\n      }\n    }\n\n    dynamicOverwriteTest(\"InsertInto: overwrite - mixed clause - dynamic mode\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n            s\"USING $v2Format PARTITIONED BY (id, p)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'keep', 2)\")\n        sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (p = 2, id) SELECT * FROM $view\")\n        verifyTable(t1, Seq(\n          (1, \"a\", 2),\n          (2, \"b\", 2),\n          (3, \"c\", 2),\n          (4, \"keep\", 2)).toDF(\"id\", \"data\", \"p\"))\n      }\n    }\n\n    dynamicOverwriteTest(\"InsertInto: overwrite - mixed clause reordered - dynamic mode\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n            s\"USING $v2Format PARTITIONED BY (id, p)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'keep', 2)\")\n        sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (id, p = 2) SELECT * FROM $view\")\n        verifyTable(t1, Seq(\n          (1, \"a\", 2),\n          (2, \"b\", 2),\n          (3, \"c\", 2),\n          (4, \"keep\", 2)).toDF(\"id\", \"data\", \"p\"))\n      }\n    }\n\n    dynamicOverwriteTest(\"InsertInto: overwrite - implicit dynamic partition - dynamic mode\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n            s\"USING $v2Format PARTITIONED BY (id, p)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'keep', 2)\")\n        sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (p = 2) SELECT * FROM $view\")\n        verifyTable(t1, Seq(\n          (1, \"a\", 2),\n          (2, \"b\", 2),\n          (3, \"c\", 2),\n          (4, \"keep\", 2)).toDF(\"id\", \"data\", \"p\"))\n      }\n    }\n\n    test(\"insert nested struct literal into delta\") {\n      withTable(\"insertNestedTest\") {\n        sql(s\"CREATE TABLE insertNestedTest \" +\n          s\" (num INT, text STRING, s STRUCT<a:STRING, s2: STRUCT<c:STRING,d:STRING>, b:STRING>)\" +\n          s\" USING DELTA\")\n        sql(s\"INSERT INTO insertNestedTest VALUES (1, 'a', struct('a', struct('c', 'd'), 'b'))\")\n      }\n    }\n\n    dynamicOverwriteTest(\"InsertInto: overwrite - multiple static partitions - dynamic mode\") {\n      val t1 = \"tbl\"\n      withTableAndData(t1) { view =>\n        sql(s\"CREATE TABLE $t1 (id bigint, data string, p int) \" +\n            s\"USING $v2Format PARTITIONED BY (id, p)\")\n        sql(s\"INSERT INTO $t1 VALUES (2L, 'dummy', 2), (4L, 'keep', 2)\")\n        sql(s\"INSERT OVERWRITE TABLE $t1 PARTITION (id = 2, p = 2) SELECT data FROM $view\")\n        verifyTable(t1, Seq(\n          (2, \"a\", 2),\n          (2, \"b\", 2),\n          (2, \"c\", 2),\n          (4, \"keep\", 2)).toDF(\"id\", \"data\", \"p\"))\n      }\n    }\n\n    test(\"InsertInto: overwrite - dot in column names - static mode\") {\n      import testImplicits._\n      val t1 = \"tbl\"\n      withTable(t1) {\n        sql(s\"CREATE TABLE $t1 (`a.b` string, `c.d` string) USING $v2Format PARTITIONED BY (`a.b`)\")\n        sql(s\"INSERT OVERWRITE $t1 PARTITION (`a.b` = 'a') VALUES('b')\")\n        verifyTable(t1, Seq(\"a\" -> \"b\").toDF(\"id\", \"data\"))\n      }\n    }\n  }\n\n  // END Apache Spark tests\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaInsertIntoTest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.{DebugFilesystem, SparkThrowable}\nimport org.apache.spark.sql.{DataFrame, QueryTest, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.util.QuotingUtils\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.streaming.{StreamingQueryException, Trigger}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * There are **many** different ways to run an insert:\n * - Using SQL, the dataframe v1 and v2 APIs or the streaming API.\n * - Append vs. Overwrite / Partition overwrite.\n * - Position-based vs. name-based resolution.\n *\n * Each take a unique path through analysis. The abstractions below captures these different\n * inserts to allow more easily running tests with all or a subset of them.\n */\ntrait DeltaInsertIntoTest\n  extends QueryTest\n  with DeltaDMLTestUtilsPathBased\n  with DeltaSQLCommandTest {\n\n  val catalogName = \"spark_catalog\"\n\n  /**\n   * Represents one way of inserting data into a Delta table.\n   * @param name A human-readable name for the insert type displayed in the test names.\n   * @param mode Append or Overwrite. This dictates in particular what the expected result after the\n   *             insert should be.\n   * @param byName Whether the insert uses name-based resolution or position-based resolution.\n   * @param isSQL Whether the insert is done using SQL or the dataframe API (includes streaming\n   *              write).\n   */\n  trait Insert {\n    val name: String\n    val mode: SaveMode\n    val byName: Boolean\n    val isSQL: Boolean\n\n    /**\n     * The method that tests will call to run the insert. Each type of insert must implement its\n     * specific way to run insert.\n     */\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit\n\n    /** SQL keyword for this type of insert.  */\n    def intoOrOverwrite: String = if (mode == SaveMode.Append) \"INTO\" else \"OVERWRITE\"\n\n    /** The expected content of the table after the insert. */\n    def expectedResult(initialDF: DataFrame, insertedDF: DataFrame): DataFrame = {\n      // Always union with the initial data even if we're overwriting it to ensure the resulting\n      // schema contains all columns from the table in case some are missing in `insertedDF`.\n      val initial = if (mode == SaveMode.Overwrite) initialDF.limit(0) else initialDF\n      initial.unionByName(insertedDF, allowMissingColumns = true)\n    }\n  }\n\n  /** INSERT INTO/OVERWRITE */\n  case class SQLInsertByPosition(mode: SaveMode) extends Insert {\n    val name: String = s\"INSERT $intoOrOverwrite\"\n    val byName: Boolean = false\n    val isSQL: Boolean = true\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      withSQLConf(DeltaSQLConf.\n          DELTA_SCHEMA_AUTO_MIGRATE.key -> withSchemaEvolution.toString) {\n        sql(s\"INSERT $intoOrOverwrite target SELECT * FROM source\")\n      }\n    }\n  }\n\n  /** INSERT INTO/OVERWRITE (a, b) */\n  case class SQLInsertColList(mode: SaveMode) extends Insert {\n    val name: String = s\"INSERT $intoOrOverwrite (columns) - $mode\"\n    val byName: Boolean = true\n    val isSQL: Boolean = true\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      val colList = columns.mkString(\", \")\n      withSQLConf(DeltaSQLConf.\n          DELTA_SCHEMA_AUTO_MIGRATE.key -> withSchemaEvolution.toString) {\n        sql(s\"INSERT $intoOrOverwrite target ($colList) SELECT $colList FROM source\")\n      }\n    }\n  }\n\n  /** INSERT INTO/OVERWRITE BY NAME */\n  case class SQLInsertByName(mode: SaveMode) extends Insert {\n    val name: String = s\"INSERT $intoOrOverwrite BY NAME - $mode\"\n    val byName: Boolean = true\n    val isSQL: Boolean = true\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      withSQLConf(DeltaSQLConf.\n          DELTA_SCHEMA_AUTO_MIGRATE.key -> withSchemaEvolution.toString) {\n        sql(s\"INSERT $intoOrOverwrite target BY NAME \" +\n          s\"SELECT ${columns.mkString(\", \")} FROM source\")\n      }\n    }\n  }\n\n  /** INSERT INTO REPLACE WHERE */\n  object SQLInsertOverwriteReplaceWhere extends Insert {\n    val name: String = s\"INSERT INTO REPLACE WHERE\"\n    val mode: SaveMode = SaveMode.Overwrite\n    val byName: Boolean = false\n    val isSQL: Boolean = true\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      withSQLConf(DeltaSQLConf.\n          DELTA_SCHEMA_AUTO_MIGRATE.key -> withSchemaEvolution.toString) {\n        sql(s\"INSERT INTO target REPLACE WHERE $whereCol = $whereValue \" +\n          s\"SELECT ${columns.mkString(\", \")} FROM source\")\n      }\n    }\n  }\n\n  /** INSERT OVERWRITE PARTITION (part = 1) */\n  object SQLInsertOverwritePartitionByPosition extends Insert {\n    val name: String = s\"INSERT OVERWRITE PARTITION (partition)\"\n    val mode: SaveMode = SaveMode.Overwrite\n    val byName: Boolean = false\n    val isSQL: Boolean = true\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      val assignments = columns.filterNot(_ == whereCol).mkString(\", \")\n      withSQLConf(DeltaSQLConf.\n          DELTA_SCHEMA_AUTO_MIGRATE.key -> withSchemaEvolution.toString) {\n        sql(s\"INSERT OVERWRITE target PARTITION ($whereCol = $whereValue) \" +\n          s\"SELECT $assignments FROM source\")\n      }\n    }\n  }\n\n  /** INSERT OVERWRITE PARTITION (part = 1) (a, b) */\n  object SQLInsertOverwritePartitionColList extends Insert {\n    val name: String = s\"INSERT OVERWRITE PARTITION (partition) (columns)\"\n    val mode: SaveMode = SaveMode.Overwrite\n    val byName: Boolean = true\n    val isSQL: Boolean = true\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      val assignments = columns.filterNot(_ == whereCol).mkString(\", \")\n      withSQLConf(DeltaSQLConf.\n          DELTA_SCHEMA_AUTO_MIGRATE.key -> withSchemaEvolution.toString) {\n        sql(s\"INSERT OVERWRITE target \" +\n          s\"PARTITION ($whereCol = $whereValue) ($assignments) \" +\n          s\"SELECT $assignments FROM source\")\n      }\n    }\n  }\n\n  /** df.write.mode(mode).insertInto() */\n  case class DFv1InsertInto(mode: SaveMode) extends Insert {\n    val name: String = s\"DFv1 insertInto() - $mode\"\n    val byName: Boolean = false\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit =\n      spark.read.table(\"source\").write.mode(mode)\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .format(\"delta\")\n        .insertInto(\"target\")\n  }\n\n  /** df.write.mode(mode).saveAsTable() */\n  case class DFv1SaveAsTable(mode: SaveMode) extends Insert {\n    val name: String = s\"DFv1 saveAsTable() - $mode\"\n    val byName: Boolean = true\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      spark.read.table(\"source\").write.mode(mode)\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .format(\"delta\")\n        .saveAsTable(\"target\")\n    }\n  }\n\n  /** df.write.mode(mode).save() */\n  case class DFv1Save(mode: SaveMode) extends Insert {\n    val name: String = s\"DFv1 save() - $mode\"\n    val byName: Boolean = true\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n      spark.read.table(\"source\").write.mode(mode)\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .format(\"delta\")\n        .save(deltaLog.dataPath.toString)\n    }\n  }\n\n  /** df.write.mode(mode).option(\"partitionOverwriteMode\", \"dynamic\").insertInto() */\n  object DFv1InsertIntoDynamicPartitionOverwrite extends Insert {\n    val name: String = s\"DFv1 insertInto() - dynamic partition overwrite\"\n    val mode: SaveMode = SaveMode.Overwrite\n    val byName: Boolean = false\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit =\n      spark.read.table(\"source\").write\n        .mode(mode)\n        .option(\"partitionOverwriteMode\", \"dynamic\")\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .format(\"delta\")\n        .insertInto(\"target\")\n  }\n\n  /** df.writeTo.append() */\n  object DFv2Append extends Insert { self: Insert =>\n    val name: String = \"DFv2 append()\"\n    val mode: SaveMode = SaveMode.Append\n    val byName: Boolean = true\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      spark.read.table(\"source\")\n        .writeTo(\"target\")\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .append()\n    }\n  }\n\n  /** df.writeTo.overwrite() */\n  object DFv2Overwrite extends Insert { self: Insert =>\n    val name: String = s\"DFv2 overwrite()\"\n    val mode: SaveMode = SaveMode.Overwrite\n    val byName: Boolean = true\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      spark.read.table(\"source\")\n        .writeTo(\"target\")\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .overwrite(col(whereCol) === lit(whereValue))\n    }\n  }\n\n  /** df.writeTo.overwritePartitions() */\n  object DFv2OverwritePartition extends Insert { self: Insert =>\n    val name: String = s\"DFv2 overwritePartitions()\"\n    override val mode: SaveMode = SaveMode.Overwrite\n    val byName: Boolean = true\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      spark.read.table(\"source\")\n        .writeTo(\"target\")\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .overwritePartitions()\n    }\n  }\n\n  /** df.writeStream.toTable() */\n  object StreamingInsert extends Insert { self: Insert =>\n    val name: String = s\"Streaming toTable()\"\n    override val mode: SaveMode = SaveMode.Append\n    val byName: Boolean = true\n    val isSQL: Boolean = false\n    def runInsert(\n        columns: Seq[String],\n        whereCol: String,\n        whereValue: Int,\n        withSchemaEvolution: Boolean): Unit = {\n      val tablePath = DeltaLog.forTable(spark, TableIdentifier(\"target\")).dataPath\n      val checkpointLocation = new Path(tablePath, \"_checkpoint\")\n      val query = spark.readStream\n        .table(\"source\")\n        .writeStream\n        .option(\"checkpointLocation\", checkpointLocation.toString)\n        .option(\"mergeSchema\", withSchemaEvolution.toString)\n        .format(\"delta\")\n        .trigger(Trigger.AvailableNow())\n        .toTable(\"target\")\n      query.processAllAvailable()\n    }\n  }\n\n  /** Collects all the types of insert previously defined. */\n  protected lazy val allInsertTypes: Set[Insert] = Set(\n        SQLInsertOverwriteReplaceWhere,\n        SQLInsertOverwritePartitionByPosition,\n        SQLInsertOverwritePartitionColList,\n        DFv1InsertIntoDynamicPartitionOverwrite,\n        DFv2Append,\n        DFv2Overwrite,\n        DFv2OverwritePartition,\n        StreamingInsert\n  ) ++ (for {\n      mode: SaveMode <- Seq(SaveMode.Append, SaveMode.Overwrite)\n      insert: Insert <- Seq(\n        SQLInsertByPosition(mode),\n        SQLInsertColList(mode),\n        SQLInsertByName(mode),\n        DFv1InsertInto(mode),\n        DFv1SaveAsTable(mode),\n        DFv1Save(mode)\n      )\n    } yield insert).toSet\n\n  /** Collects inserts using resolution by name and by position respectively. */\n  protected lazy val (insertsByName, insertsByPosition): (Set[Insert], Set[Insert]) =\n    allInsertTypes.partition(_.byName)\n\n  /** Collects inserts run through SQL and the dataframe API respectively. */\n  protected lazy val (insertsSQL, insertsDataframe): (Set[Insert], Set[Insert]) =\n    allInsertTypes.partition(_.isSQL)\n\n  /** Collects append inserts vs. overwrite. */\n  protected lazy val (insertsAppend, insertsOverwrite): (Set[Insert], Set[Insert]) =\n    allInsertTypes.partition(_.mode == SaveMode.Append)\n\n  /** Collects all test cases defined, aggregated by test name. Used in\n   * [[checkAllTestCasesImplemented]] below to ensure each test covers all existing insert types.\n   */\n  protected val testCases: mutable.Map[String, Set[Insert]] =\n    mutable.HashMap.empty.withDefaultValue(Set.empty)\n\n  /** Tests should cover all insert types but it's easy to miss some cases. This method checks\n   * that each test cover all insert types.\n   */\n  def checkAllTestCasesImplemented(ignoredTestCases: Map[String, Set[Insert]] = Map.empty): Unit = {\n    val ignoredTests = ignoredTestCases.withDefaultValue(Set.empty)\n    val missingTests = testCases.map {\n      case (name, inserts) => name -> (allInsertTypes -- inserts -- ignoredTests(name))\n    }.collect {\n      case (name, missingInserts) if missingInserts.nonEmpty =>\n        s\"Test '$name' is not covering all insert types, missing: $missingInserts\"\n    }\n\n    if (missingTests.nonEmpty) {\n      fail(\"Missing test cases:\\n\" + missingTests)\n    }\n  }\n\n  /** Convenience wrapper define test data using a SQL schema and a JSON string for each row. */\n  case class TestData(schemaDDL: String, data: Seq[String]) {\n    val schema: StructType = StructType.fromDDL(schemaDDL)\n    def toDF: DataFrame = readFromJSON(data, schema)\n  }\n\n  /**\n   * Test runner to cover INSERT operations defined above.\n   * @param name                Test name\n   * @param initialData         Initial data used to create the table.\n   * @param partitionBy         Partition columns for the initial table.\n   * @param insertData          Additional data to be inserted.\n   * @param overwriteWhere      Where clause for overwrite PARTITION / REPLACE WHERE (as\n   *                            colName -> value)\n   * @param expectedResult      Expected result, see [[ExpectedResult]] above.\n   * @param includeInserts      List of insert types to run the test with.\n   *                            Defaults to all inserts.\n   * @param excludeInserts      List of insert types to exclude when running the test.\n   *                            Defaults to no  inserts excluded.\n   * @param confs               Custom spark confs to set before running the insert\n   *                            operation.\n   * @param withSchemaEvolution Whether to enable Automatic Schema Evolution.\n   */\n  def testInserts[T](name: String)(\n      initialData: TestData,\n      partitionBy: Seq[String] = Seq.empty,\n      insertData: TestData,\n      overwriteWhere: (String, Int),\n      expectedResult: ExpectedResult[T],\n      includeInserts: Set[Insert] = allInsertTypes,\n      excludeInserts: Set[Insert] = Set.empty,\n      confs: Seq[(String, String)] = Seq.empty,\n      withSchemaEvolution: Boolean = false): Unit = {\n    val inserts = includeInserts.filterNot(excludeInserts)\n    assert(inserts.nonEmpty, s\"Test '$name' doesn't cover any inserts. Please check the \" +\n      \"includeInserts/excludeInserts sets and ensure at least one insert is included.\")\n    testCases(name) ++= inserts\n\n    for (insert <- inserts) {\n      test(s\"${insert.name} - $name\") {\n        withTable(\"source\", \"target\") {\n          val writer = initialData.toDF.write.format(\"delta\")\n          if (partitionBy.nonEmpty) {\n            writer.partitionBy(partitionBy: _*)\n          }\n          writer.saveAsTable(\"target\")\n          // Write the data to insert to a table so that we can use it in both SQL and dataframe\n          // writer inserts.\n          insertData.toDF.write.format(\"delta\").saveAsTable(\"source\")\n\n          def runInsert(): Unit =\n            insert.runInsert(\n              columns = insertData.schema.map(f => QuotingUtils.quoteIfNeeded(f.name)),\n              whereCol = overwriteWhere._1,\n              whereValue = overwriteWhere._2,\n              withSchemaEvolution = withSchemaEvolution\n            )\n\n          withSQLConf(confs: _*) {\n            expectedResult match {\n              case ExpectedResult.Success(expectedSchema: StructType) =>\n                runInsert()\n                val target = spark.read.table(\"target\")\n                assert(target.schema === expectedSchema)\n                checkAnswer(target, insert.expectedResult(initialData.toDF, insertData.toDF))\n              case ExpectedResult.Success(expectedData: TestData) =>\n                runInsert()\n                val target = spark.read.table(\"target\")\n                assert(target.schema === expectedData.schema)\n                checkAnswer(spark.read.table(\"target\"), expectedData.toDF)\n              case ExpectedResult.Failure(checkError) =>\n                val ex = if (insert == StreamingInsert) {\n                  intercept[StreamingQueryException] {\n                    runInsert()\n                  }.getCause.asInstanceOf[SparkThrowable]\n                } else {\n                  intercept[SparkThrowable] {\n                    runInsert()\n                  }\n                }\n                checkError(ex)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaLimitPushDownSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.DatabricksLogging\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatsUtils\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, ScanReportHelper}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\ntrait DeltaLimitPushDownTests extends QueryTest\n    with SharedSparkSession\n    with DatabricksLogging\n    with ScanReportHelper\n    with DeletionVectorsTestUtils\n    with StatsUtils\n    with DeltaSQLCommandTest\n    with CatalogOwnedTestBaseSuite {\n\n  import testImplicits._\n\n\n  test(\"no filter or projection\") {\n    withTempTable(createTable = false) { tableName =>\n      val ds = Seq(1, 1, 2, 2, 3, 3).toDS().repartition(5, $\"value\")\n      ds.write.format(\"delta\").saveAsTable(tableName)\n\n      val Seq(deltaScan, deltaScanWithLimit) = getScanReport {\n        spark.read.format(\"delta\").table(tableName).collect()\n        val res = spark.read.format(\"delta\").table(tableName).limit(3).collect()\n        assert(res.size == 3)\n      }\n\n      assert(deltaScan.size(\"total\").bytesCompressed ===\n        deltaScanWithLimit.size(\"total\").bytesCompressed)\n\n      assert(deltaScan.size(\"scanned\").bytesCompressed !=\n        deltaScanWithLimit.size(\"scanned\").bytesCompressed)\n\n      assert(deltaScanWithLimit.size(\"scanned\").rows === Some(4L))\n    }\n  }\n\n  test(\"limit larger than total\") {\n    withTempTable(createTable = false) { tableName =>\n      val data = Seq(1, 1, 2, 2)\n      val ds = data.toDS().repartition($\"value\")\n      ds.write.format(\"delta\").saveAsTable(tableName)\n\n      val Seq(deltaScan, deltaScanWithLimit) = getScanReport {\n        spark.read.format(\"delta\").table(tableName).collect()\n        checkAnswer(spark.read.format(\"delta\").table(tableName).limit(5), data.toDF())\n      }\n\n      assert(deltaScan.size(\"total\").bytesCompressed ===\n        deltaScanWithLimit.size(\"total\").bytesCompressed)\n\n      assert(deltaScan.size(\"scanned\").bytesCompressed ===\n        deltaScanWithLimit.size(\"scanned\").bytesCompressed)\n    }\n  }\n\n  test(\"limit 0\") {\n    val records = getScanReport {\n      withTempTable(createTable = false) { tableName =>\n        val ds = Seq(1, 1, 2, 2, 3, 3).toDS().repartition($\"value\")\n        ds.write.format(\"delta\").saveAsTable(tableName)\n        val res = spark.read.format(\"delta\")\n          .table(tableName)\n          .limit(0)\n\n        checkAnswer(res, Seq())\n      }\n    }\n  }\n\n  test(\"insufficient rows have stats\") {\n    withSQLConf(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        val file = Seq(1, 2).toDS().coalesce(1)\n\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n          file.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        }\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n          file.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        }\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n          file.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        }\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n        val deltaScan = deltaLog.snapshot.filesForScan(limit = 3, partitionFilters = Seq.empty)\n\n        assert(deltaScan.scanned.bytesCompressed === deltaScan.total.bytesCompressed)\n      }\n    }\n  }\n\n  test(\"sufficient rows have stats\") {\n    withSQLConf(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        val file = Seq(1, 2).toDS().coalesce(1)\n\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n          file.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        }\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n          file.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        }\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n          file.write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n        }\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n        val deltaScan = deltaLog.snapshot.filesForScan(limit = 3, partitionFilters = Seq.empty)\n\n        assert(deltaScan.scanned.rows === Some(4))\n        assert(deltaScan.scanned.bytesCompressed != deltaScan.total.bytesCompressed)\n      }\n    }\n  }\n\n  test(\"with projection only\") {\n    withTempTable(createTable = false) { tableName =>\n      val ds = Seq((1, 1), (2, 1), (3, 1)).toDF(\"key\", \"value\").as[(Int, Int)]\n      ds.write.format(\"delta\").partitionBy(\"key\").saveAsTable(tableName)\n\n      val Seq(deltaScan) = getScanReport {\n        val res = spark.read.format(\"delta\").table(tableName).select(\"value\").limit(1).collect()\n        assert(res === Seq(Row(1)))\n      }\n\n      assert(deltaScan.size(\"scanned\").rows === Some(1L))\n    }\n  }\n\n  test(\"with partition filter only\") {\n    withTempTable(createTable = false) { tableName =>\n      val ds = Seq((1, 4), (2, 5), (3, 6)).toDF(\"key\", \"value\").as[(Int, Int)]\n      ds.write.format(\"delta\").partitionBy(\"key\").saveAsTable(tableName)\n\n      val Seq(deltaScan, deltaScanWithLimit, deltaScanWithLimit2) = getScanReport {\n        spark.read.format(\"delta\").table(tableName).where(\"key > 1\").collect()\n        val res1 = spark.read.format(\"delta\").table(tableName).where(\"key > 1\").limit(1).collect()\n        assert(res1 === Seq(Row(2, 5)) || res1 === Seq(Row(3, 6)))\n        val res2 = spark.read.format(\"delta\").table(tableName).where(\"key == 1\").limit(2).collect()\n        assert(res2 === Seq(Row(1, 4)))\n      }\n\n      assert(deltaScan.size(\"total\").bytesCompressed ===\n        deltaScanWithLimit.size(\"total\").bytesCompressed)\n\n      assert(deltaScan.size(\"scanned\").bytesCompressed !=\n        deltaScanWithLimit.size(\"scanned\").bytesCompressed)\n\n      assert(deltaScan.size(\"scanned\").bytesCompressed.get <\n        deltaScan.size(\"total\").bytesCompressed.get)\n      assert(deltaScanWithLimit.size(\"scanned\").rows === Some(1L))\n      assert(deltaScanWithLimit2.size(\"scanned\").rows === Some(1L))\n    }\n  }\n\n  test(\"with non-partition filter\") {\n    withTempTable(createTable = false) { tableName =>\n      val ds = Seq((1, 4), (2, 5), (3, 6)).toDF(\"key\", \"value\").as[(Int, Int)]\n      ds.write.format(\"delta\").partitionBy(\"key\").saveAsTable(tableName)\n\n      val Seq(deltaScan) = getScanReport { // this query should not trigger limit push-down\n        spark.read.format(\"delta\").table(tableName)\n          .where(\"key > 1\")\n          .where(\"value > 4\")\n          .limit(1)\n          .collect()\n      }\n      assert(deltaScan.size(\"scanned\").rows === Some(2L))\n    }\n  }\n\n  test(\"limit push-down flag\") {\n    withTempTable(createTable = false) { tableName =>\n      val ds = Seq((1, 4), (2, 5), (3, 6)).toDF(\"key\", \"value\").as[(Int, Int)]\n      ds.write.format(\"delta\").partitionBy(\"key\").saveAsTable(tableName)\n\n      val Seq(baseline, scan, scan2) = getScanReport {\n        withSQLConf(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key -> \"true\") {\n          spark.read.format(\"delta\").table(tableName).where(\"key > 1\").limit(1).collect()\n        }\n        withSQLConf(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key -> \"false\") {\n          spark.read.format(\"delta\").table(tableName).where(\"key > 1\").limit(1).collect()\n          spark.read.format(\"delta\").table(tableName).limit(2).collect()\n        }\n      }\n      assert(scan.size(\"scanned\").bytesCompressed.get >\n        baseline.size(\"scanned\").bytesCompressed.get)\n      assert(scan2.size(\"scanned\").bytesCompressed === scan2.size(\"total\").bytesCompressed)\n    }\n  }\n\n  test(\"GlobalLimit should be kept\") {\n    withTempTable(createTable = false) { tableName =>\n      (1 to 10).toDF.repartition(5).write.format(\"delta\").saveAsTable(tableName)\n      assert(spark.read.format(\"delta\").table(tableName).limit(5).collect().size == 5)\n    }\n  }\n\n  test(\"Works with union\") {\n    withTempTable(createTable = false) { tableName =>\n      (1 to 10).toDF.repartition(5).write.format(\"delta\").saveAsTable(tableName)\n      val t1 = spark.read.format(\"delta\").table(tableName)\n      val t2 = spark.read.format(\"delta\").table(tableName)\n      val union = t1.union(t2)\n\n      withSQLConf(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key -> \"true\") {\n        val Seq(scanFull1, scanFull2) = getScanReport {\n          union.collect()\n        }\n        val Seq(scanLimit1, scanLimit2) = getScanReport {\n          union.limit(1).collect()\n        }\n\n        assert(scanFull1.size(\"scanned\").bytesCompressed.get >\n          scanLimit1.size(\"scanned\").bytesCompressed.get)\n        assert(scanFull2.size(\"scanned\").bytesCompressed.get >\n          scanLimit2.size(\"scanned\").bytesCompressed.get)\n      }\n    }\n  }\n\n  private def withDVSettings(thunk: => Unit): Unit = {\n    withSQLConf(\n      DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"false\"\n    ) {\n      withDeletionVectorsEnabled() {\n        thunk\n      }\n    }\n  }\n\n  test(s\"Verify limit correctness in the presence of DVs\") {\n    withDVSettings {\n      val targetDF = spark.range(start = 0, end = 100, step = 1, numPartitions = 2)\n        .withColumn(\"value\", col(\"id\"))\n\n      withTempDeltaTable(targetDF, createNameBasedTable = true) { (targetTable, targetLog) =>\n        removeRowsFromAllFilesInLog(targetLog, numRowsToRemovePerFile = 10)\n        verifyDVsExist(targetLog, 2)\n\n        val targetDF = targetTable().toDF\n\n        // We have 2 files 50 rows each. We deleted 10 rows from the first file. The first file\n        // now contains 50 physical rows and 40 logical. Failing to take into account the DVs in\n        // the first file results into prematurely terminating the scan and returning an\n        // incorrect result. Note, the corner case in terms of correctness is when the limit is\n        // set to 50. When statistics collection is disabled, we read both files.\n        val limitToExpectedNumberOfFilesReadSeq = Range(10, 90, 10)\n          .map(n => (n, if (n < 50) 1 else 2))\n\n        for ((limit, expectedNumberOfFilesRead) <- limitToExpectedNumberOfFilesReadSeq) {\n          val df = targetDF.limit(limit)\n\n          // Assess correctness.\n          assert(df.count === limit)\n\n          val scanStats = getStats(df)\n\n          // Check we do not read more files than needed.\n          assert(scanStats.scanned.files === Some(expectedNumberOfFilesRead))\n\n          // Verify physical and logical rows are updated correctly.\n          val numDeletedRows = 10\n          val numPhysicalRowsPerFile = 50\n          val numTotalPhysicalRows = numPhysicalRowsPerFile * expectedNumberOfFilesRead\n          val numTotalLogicalRows = numTotalPhysicalRows -\n            (numDeletedRows * expectedNumberOfFilesRead)\n          val expectedNumTotalPhysicalRows = Some(numTotalPhysicalRows)\n          val expectedNumTotalLogicalRows = Some(numTotalLogicalRows)\n\n          assert(scanStats.scanned.rows === expectedNumTotalPhysicalRows)\n          assert(scanStats.scanned.logicalRows === expectedNumTotalLogicalRows)\n        }\n      }\n    }\n  }\n}\n\nclass DeltaLimitPushDownV1Suite extends DeltaLimitPushDownTests\n\nclass DeltaLimitPushDownWithCatalogOwnedBatch1Suite extends DeltaLimitPushDownTests {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaLimitPushDownWithCatalogOwnedBatch2Suite extends DeltaLimitPushDownTests {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaLimitPushDownWithCatalogOwnedBatch100Suite extends DeltaLimitPushDownTests {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaLogMinorCompactionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, FileNames, JsonUtils}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\n\n// scalastyle:off: removeFile\nclass DeltaLogMinorCompactionSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils\n  with CatalogOwnedTestBaseSuite {\n\n  /** Helper method to do minor compaction of [[DeltaLog]] from [startVersion, endVersion] */\n  private def minorCompactDeltaLog(\n      tablePath: String,\n      startVersion: Long,\n      endVersion: Long): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, tablePath)\n    val logReplay = new InMemoryLogReplay(\n      minFileRetentionTimestamp = None,\n      minSetTransactionRetentionTimestamp = None)\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n\n    (startVersion to endVersion).foreach { versionToRead =>\n      val file = FileNames.unsafeDeltaFile(deltaLog.logPath, versionToRead)\n      val actionsIterator = deltaLog.store.readAsIterator(file, hadoopConf).map(Action.fromJson)\n      logReplay.append(versionToRead, actionsIterator)\n    }\n    deltaLog.store.write(\n      path = FileNames.compactedDeltaFile(deltaLog.logPath, startVersion, endVersion),\n      actions = logReplay.checkpoint.map(_.json).toIterator,\n      overwrite = true,\n      hadoopConf = hadoopConf)\n  }\n\n  // Helper method to validate a commit.\n  protected def validateCommit(\n      log: DeltaLog,\n      version: Long,\n      numAdds: Int = 0,\n      numRemoves: Int = 0,\n      numMetadata: Int = 0): Unit = {\n    assert(log.update().version === version)\n    val filePath = DeltaCommitFileProvider(log.update()).deltaFile(version)\n    val actions = log.store.read(filePath, log.newDeltaHadoopConf()).map(Action.fromJson)\n    assert(actions.head.isInstanceOf[CommitInfo])\n    assert(actions.tail.count(_.isInstanceOf[AddFile]) === numAdds)\n    assert(actions.tail.count(_.isInstanceOf[RemoveFile]) === numRemoves)\n    assert(actions.tail.count(_.isInstanceOf[Metadata]) === numMetadata)\n  }\n\n  // Helper method to validate a compacted delta.\n  private def validateCompactedDelta(\n      log: DeltaLog,\n      filePath: Path,\n      expectedCompactedDelta: CompactedDelta): Unit = {\n    val actions = log.store.read(filePath, log.newDeltaHadoopConf()).map(Action.fromJson)\n    val observedCompactedDelta = CompactedDelta(\n      versionWindow = FileNames.compactedDeltaVersions(filePath),\n      numAdds = actions.count(_.isInstanceOf[AddFile]),\n      numRemoves = actions.count(_.isInstanceOf[RemoveFile]),\n      numMetadata = actions.count(_.isInstanceOf[Metadata])\n    )\n    assert(expectedCompactedDelta === observedCompactedDelta)\n  }\n\n\n  case class CompactedDelta(\n      versionWindow: (Long, Long),\n      numAdds: Int = 0,\n      numRemoves: Int = 0,\n      numMetadata: Int = 0)\n\n  def createTestAddFile(\n      path: String = \"foo\",\n      partitionValues: Map[String, String] = Map.empty,\n      size: Long = 1L,\n      modificationTime: Long = 1L,\n      dataChange: Boolean = true,\n      stats: String = \"{\\\"numRecords\\\": 1}\"): AddFile = {\n    AddFile(path, partitionValues, size, modificationTime, dataChange, stats)\n  }\n\n  def generateData(tableDir: String, checkpoints: Set[Int]): Unit = {\n    val files = (1 to 21).map( index => createTestAddFile(s\"f${index}\"))\n    // commit version 0 - AddFile: 4\n    val deltaLog = DeltaLog.forTable(spark, tableDir)\n    import org.apache.spark.sql.delta.test.DeltaTestImplicits._\n    val metadata = Metadata()\n    val tableMetadata = metadata.copy(\n      configuration = DeltaConfigs.mergeGlobalConfigs(conf, metadata.configuration))\n    deltaLog.startTransaction().commitManually(\n      files(1), files(2), files(3), files(4), tableMetadata)\n    validateCommit(deltaLog, version = 0, numAdds = 4, numRemoves = 0, numMetadata = 1)\n    if (checkpoints.contains(0)) deltaLog.checkpoint()\n    // commit version 1 - AddFile: 1\n    deltaLog.startTransaction().commit(files(5) :: Nil, ManualUpdate)\n    validateCommit(deltaLog, version = 1, numAdds = 1, numRemoves = 0)\n    if (checkpoints.contains(1)) deltaLog.checkpoint()\n    // commit version 2 - RemoveFile: 1, AddFile: 1\n    deltaLog.startTransaction().commit(Seq(files(5).remove, files(6)), ManualUpdate)\n    validateCommit(deltaLog, version = 2, numAdds = 1, numRemoves = 1)\n    if (checkpoints.contains(2)) deltaLog.checkpoint()\n    // commit version 3 - empty commit\n    deltaLog.startTransaction().commit(Seq(), ManualUpdate)\n    validateCommit(deltaLog, version = 3, numAdds = 0, numRemoves = 0)\n    if (checkpoints.contains(3)) deltaLog.checkpoint()\n    // commit version 4 - empty commit\n    deltaLog.startTransaction().commit(Seq(), ManualUpdate)\n    validateCommit(deltaLog, version = 4, numAdds = 0, numRemoves = 0)\n    if (checkpoints.contains(4)) deltaLog.checkpoint()\n    // commit version 5 - AddFile: 1, RemoveFile: 5\n    deltaLog.startTransaction().commit(\n      (1 to 4).map(i => files(i).remove) ++ Seq(files(6).remove, files(7)),\n      ManualUpdate)\n    validateCommit(deltaLog, version = 5, numAdds = 1, numRemoves = 5)\n    if (checkpoints.contains(5)) deltaLog.checkpoint()\n    // commit version 6 - AddFile: 10, RemoveFile: 0\n    deltaLog.startTransaction().commit((8 to 17).map(i => files(i)), ManualUpdate)\n    validateCommit(deltaLog, version = 6, numAdds = 10, numRemoves = 0)\n    if (checkpoints.contains(6)) deltaLog.checkpoint()\n    // commit version 7 - AddFile: 2, RemoveFile: 6\n    deltaLog.startTransaction().commit(\n      (10 to 15).map(i => files(i).remove) ++ Seq(files(18), files(19)),\n      ManualUpdate)\n    validateCommit(deltaLog, version = 7, numAdds = 2, numRemoves = 6)\n    if (checkpoints.contains(7)) deltaLog.checkpoint()\n    // commit version 8 - Metadata: 1\n    deltaLog.startTransaction().commit(Seq(deltaLog.unsafeVolatileSnapshot.metadata), ManualUpdate)\n    validateCommit(deltaLog, version = 8, numMetadata = 1)\n    if (checkpoints.contains(8)) deltaLog.checkpoint()\n    // commit version 9 - AddFile: 7\n    deltaLog.startTransaction().commit(\n      Seq(files(16), files(17), files(18), files(19), files(7), files(8), files(9))\n        .map(af => af.copy(dataChange = false)),\n      ManualUpdate)\n    validateCommit(deltaLog, version = 9, numAdds = 7)\n    if (checkpoints.contains(9)) deltaLog.checkpoint()\n    // commit version 10 - AddFiles: 1\n    deltaLog.startTransaction().commit(files(20) :: Nil, ManualUpdate)\n    validateCommit(deltaLog, version = 10, numAdds = 1, numRemoves = 0)\n  }\n\n  /**\n   * This test creates a Delta table with 11 commits (0, 1, ..., 10) and also creates compacted\n   * deltas based on the provided `compactionRange` tuples.\n   *\n   * At the end, we create a Snapshot and see if the Snapshot is initialized properly using the\n   * right compacted delta files instead of regular delta files. We also compare the\n   * `computeState`, `stateDF`, `allFiles` of this compacted delta backed Snapshot against a\n   * regular Snapshot backed by single delta files.\n   */\n  def testSnapshotCreation(\n      compactionWindows: Seq[(Long, Long)],\n      checkpoints: Set[Int] = Set.empty,\n      postDataGenerationFunc: Option[DeltaLog => Unit] = None,\n      postSetupFunc: Option[DeltaLog => Unit] = None,\n      expectedCompactedDeltas: Seq[CompactedDelta],\n      expectedDeltas: Seq[Long],\n      expectedCheckpoint: Long = -1L,\n      expectError: Boolean = false,\n      additionalConfs: Seq[(String, String)] = Seq.empty): Unit = {\n\n    val confs = Seq(\n      DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS.key -> \"true\",\n      DeltaSQLConf.DELTA_SKIP_RECORDING_EMPTY_COMMITS.key -> \"false\",\n      // Set CHECKPOINT_INTERVAL to high number so that we could checkpoint whenever we need as per\n      // test setup.\n      DeltaConfigs.CHECKPOINT_INTERVAL.defaultTablePropertyKey -> \"1000\"\n    ) ++ additionalConfs\n\n    withSQLConf(confs: _*) {\n      withTempDir { tmpDir =>\n        val tableDir = tmpDir.getAbsolutePath\n        generateData(tableDir, checkpoints)\n\n        val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, tableDir)\n        // Ensure all commits are backfilled after data generation.\n        CatalogOwnedTableUtils.populateTableCommitCoordinatorFromCatalog(\n            spark,\n            catalogTableOpt = None,\n            snapshot = snapshot).foreach { tcc =>\n          tcc.backfillToVersion(snapshot.version)\n        }\n\n        // Data generation complete - run post data generation function\n        postDataGenerationFunc.foreach(_.apply(deltaLog))\n\n        compactionWindows.foreach { case (startV, endV) =>\n          minorCompactDeltaLog(tableDir, startV, endV)\n        }\n\n        // Setup complete - run post setup function\n        postSetupFunc.foreach(_.apply(deltaLog))\n\n        DeltaLog.clearCache()\n        if (expectError) {\n          intercept[DeltaIllegalStateException] {\n            DeltaLog.forTable(spark, tableDir).unsafeVolatileSnapshot\n          }\n          return\n        }\n        val snapshot1 = DeltaLog.forTable(spark, tableDir).unsafeVolatileSnapshot\n        val (compactedDeltas1, deltas1) =\n          snapshot1.logSegment.deltas.map(_.getPath).partition(FileNames.isCompactedDeltaFile)\n        assert(compactedDeltas1.size === expectedCompactedDeltas.size)\n        compactedDeltas1.sorted\n          .zip(expectedCompactedDeltas.sortBy(_.versionWindow))\n          .foreach { case (compactedDeltaPath, expectedCompactedDelta) =>\n            validateCompactedDelta(deltaLog, compactedDeltaPath, expectedCompactedDelta)\n          }\n        assert(deltas1.sorted.map(FileNames.deltaVersion) === expectedDeltas)\n        assert(snapshot1.logSegment.checkpointProvider.version === expectedCheckpoint)\n\n        // Disable the conf and create a new Snapshot. The new snapshot should not use the comoacted\n        // deltas.\n        withSQLConf(DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS.key -> \"false\") {\n          DeltaLog.clearCache()\n          val snapshot2 = DeltaLog.forTable(spark, tableDir).unsafeVolatileSnapshot\n          val (compactedDeltas2, _) =\n            snapshot2.logSegment.deltas.map(_.getPath).partition(FileNames.isCompactedDeltaFile)\n          assert(compactedDeltas2.isEmpty)\n\n          // Compare checksum, state reconstruction result of these 2 different snapshots.\n          assert(snapshot2.computeChecksum === snapshot1.computeChecksum)\n          checkAnswer(snapshot2.stateDF, snapshot1.stateDF)\n          checkAnswer(snapshot2.allFiles.toDF(), snapshot1.allFiles.toDF())\n        }\n      }\n    }\n  }\n\n  ///////////////////////\n  // Without Checkpoints\n  //////////////////////\n\n  test(\"smallest interval is chosen first for Snapshot creation\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((1, 3), (2, 3), (3, 8)),\n      expectedCompactedDeltas = Seq(CompactedDelta((1, 3), numAdds = 1, numRemoves = 1)),\n      expectedDeltas = Seq(0, 4, 5, 6, 7, 8, 9, 10)\n    )\n  }\n\n  test(\"Snapshot backed by single compacted delta\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 10)),\n      expectedCompactedDeltas =\n        Seq(CompactedDelta((0, 10), numAdds = 8, numRemoves = 12, numMetadata = 1)),\n      expectedDeltas = Seq()\n    )\n  }\n\n  test(\"empty compacted delta, compacted delta covers the beginning part\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 2), (3, 4), (4, 5)),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((0, 2), numAdds = 5, numRemoves = 1, numMetadata = 1),\n        CompactedDelta((3, 4), numAdds = 0, numRemoves = 0) // empty compacted delta\n      ),\n      expectedDeltas = Seq(5, 6, 7, 8, 9, 10)\n    )\n  }\n\n  test(\"compacted delta covers the end part of LogSegment\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((7, 10), (8, 10)),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((7, 10), numAdds = 8, numRemoves = 6, numMetadata = 1)\n      ),\n      expectedDeltas = Seq(0, 1, 2, 3, 4, 5, 6)\n    )\n  }\n\n  test(\"multiple compacted delta covers full LogSegment\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 2), (3, 5), (6, 8), (9, 10)),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((0, 2), numAdds = 5, numRemoves = 1, numMetadata = 1),\n        CompactedDelta((3, 5), numAdds = 1, numRemoves = 5, numMetadata = 0),\n        CompactedDelta((6, 8), numAdds = 6, numRemoves = 6, numMetadata = 1),\n        CompactedDelta((9, 10), numAdds = 8, numRemoves = 0, numMetadata = 0)\n      ),\n      expectedDeltas = Seq()\n    )\n  }\n\n\n  ///////////////////////\n  // With Checkpoints\n  //////////////////////\n\n  test(\"smallest interval after last checkpoint is chosen for Snapshot creation\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((1, 3), (2, 3), (3, 8), (4, 9), (3, 10)),\n      checkpoints = Set(0, 2),\n      expectedCompactedDeltas =\n        Seq(CompactedDelta((3, 8), numAdds = 7, numRemoves = 11, numMetadata = 1)),\n      expectedDeltas = Seq(9, 10),\n      expectedCheckpoint = 2,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n    )\n  }\n\n  test(\"Snapshot backed by single compacted delta after LAST_CHECKPOINT\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 10), (5, 10)),\n      checkpoints = Set(2, 4),\n      expectedCompactedDeltas =\n        Seq(CompactedDelta((5, 10), numAdds = 8, numRemoves = 11, numMetadata = 1)),\n      expectedDeltas = Seq(),\n      expectedCheckpoint = 4,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n\n    )\n  }\n\n  test(\"empty compacted delta, compacted delta covers the beginning part after LAST_CHECKPOINT\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((1, 2), (3, 4), (4, 5)),\n      checkpoints = Set(0),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((1, 2), numAdds = 1, numRemoves = 1),\n        CompactedDelta((3, 4), numAdds = 0, numRemoves = 0) // empty compacted delta\n      ),\n      expectedDeltas = Seq(5, 6, 7, 8, 9, 10),\n      expectedCheckpoint = 0,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n    )\n  }\n\n  test(\"compacted delta covers the end part of LogSegment (with Checkpoint)\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((7, 10), (8, 10)),\n      checkpoints = Set(0, 2, 5),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((7, 10), numAdds = 8, numRemoves = 6, numMetadata = 1)\n      ),\n      expectedDeltas = Seq(6),\n      expectedCheckpoint = 5,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n    )\n  }\n\n  test(\"multiple compacted delta covers full LogSegment (with Checkpoint)\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 2), (3, 5), (3, 6), (9, 10)),\n      checkpoints = Set(0, 2),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((3, 5), numAdds = 1, numRemoves = 5, numMetadata = 0),\n        CompactedDelta((9, 10), numAdds = 8, numRemoves = 0, numMetadata = 0)\n      ),\n      expectedDeltas = Seq(6, 7, 8),\n      expectedCheckpoint = 2,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n    )\n  }\n\n  /////////////////////////////////////////////////////\n  // negative scenarios where deltaLog is manipulated\n  /////////////////////////////////////////////////////\n\n  test(\"when compacted delta is available till version 11 but actual delta files are\" +\n      \" till version 10\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 2), (3, 5), (3, 6), (9, 10)),\n      checkpoints = Set(0, 2),\n      postSetupFunc = Some(\n        (deltaLog: DeltaLog) => {\n          val logPath = deltaLog.logPath\n          val fromName = FileNames.compactedDeltaFile(logPath, fromVersion = 9, toVersion = 10)\n          val toName = FileNames.compactedDeltaFile(logPath, fromVersion = 9, toVersion = 11)\n          logPath.getFileSystem(deltaLog.newDeltaHadoopConf()).rename(fromName, toName)\n        }\n      ),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((3, 5), numAdds = 1, numRemoves = 5, numMetadata = 0)\n      ),\n      expectedDeltas = Seq(6, 7, 8, 9, 10),\n      expectedCheckpoint = 2,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n    )\n  }\n\n  test(\"compacted deltas should not be used when there are holes in deltas\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((0, 2), (3, 5), (3, 6)),\n      checkpoints = Set(0, 2),\n      postSetupFunc = Some(\n        (deltaLog: DeltaLog) => {\n          val logPath = deltaLog.logPath\n          val deltaFileToDelete = FileNames.unsafeDeltaFile(logPath, version = 4)\n          logPath.getFileSystem(deltaLog.newDeltaHadoopConf()).delete(deltaFileToDelete, true)\n        }\n      ),\n      expectError = true,\n      expectedCompactedDeltas = Seq(),\n      expectedDeltas = Seq(),\n      expectedCheckpoint = -1L,\n      // Disable DELTA_CHECKPOINT_V2_ENABLED conf so that we don't forcefully checkpoint at\n      // commit version 8 where we change `delta.dataSkippingNumIndexedCols` to 0.\n      additionalConfs =\n        Seq(DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey -> \"false\")\n    )\n  }\n\n  test(\"compacted deltas should include RemoveFiles that do not have deletionTimestamp\") {\n    testSnapshotCreation(\n      compactionWindows = Seq((1, 3), (4, 6), (7, 10)),\n      checkpoints = Set.empty,\n      postDataGenerationFunc = Some(\n        (deltaLog: DeltaLog) => {\n          val hadoopConf = deltaLog.newDeltaHadoopConf()\n          // Remove deletionTimestamp from RemoveFile in versions 1 to 10.\n          (1 to 10).foreach { versionToRead =>\n            val file = FileNames.unsafeDeltaFile(deltaLog.logPath, versionToRead)\n            val actions = deltaLog.store.readAsIterator(file, hadoopConf).map(Action.fromJson)\n            val actionsWithoutDeletionTimestamp = actions\n              .map {\n                case r: RemoveFile if r.deletionTimestamp.isDefined =>\n                  r.copy(deletionTimestamp = None)\n                case other => other\n              }\n              .map(_.json)\n              .toSeq\n            // The iterator is already consumed above so that we don't run into the issue of\n            // overwriting the file while reading it.\n            deltaLog.store.write(\n              path = file,\n              actions = actionsWithoutDeletionTimestamp.toIterator,\n              overwrite = true,\n              hadoopConf = hadoopConf)\n          }\n        }\n      ),\n      expectedCompactedDeltas = Seq(\n        CompactedDelta((1, 3), numAdds = 1, numRemoves = 1, numMetadata = 0),\n        CompactedDelta((4, 6), numAdds = 11, numRemoves = 5, numMetadata = 0),\n        CompactedDelta((7, 10), numAdds = 8, numRemoves = 6, numMetadata = 1)\n      ),\n      expectedDeltas = Seq(0)\n    )\n  }\n}\n\nclass DeltaLogMinorCompactionWithCatalogOwnedBatch1Suite\n  extends DeltaLogMinorCompactionSuite {\n  override val catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"false\")\n}\n\nclass DeltaLogMinorCompactionWithCatalogOwnedBatch2Suite\n  extends DeltaLogMinorCompactionSuite {\n  override val catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"false\")\n}\n\nclass DeltaLogMinorCompactionWithCatalogOwnedBatch100Suite\n    extends DeltaLogMinorCompactionSuite {\n  override val catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"false\")\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaLogSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{BufferedReader, File, InputStreamReader, IOException}\nimport java.nio.charset.StandardCharsets\nimport java.util.{Locale, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.language.postfixOps\n\nimport org.apache.spark.sql.delta.DeltaOperations.Truncate\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite, InMemoryCommitCoordinator, TrackingCommitCoordinatorClient}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport io.delta.storage.commit.{CommitCoordinatorClient, TableDescriptor}\nimport org.apache.hadoop.fs.Path\nimport org.apache.hadoop.fs.permission.FsPermission\n\nimport org.apache.spark.rdd.RDD\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.JsonToStructs\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.NullType\nimport org.apache.spark.unsafe.types.UTF8String\nimport org.apache.spark.util.Utils\n\n// scalastyle:off: removeFile\nclass DeltaLogSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite\n  with DeltaCheckpointTestUtils\n  with DeltaSQLTestUtils {\n\n\n  protected val testOp = Truncate()\n\n  testDifferentCheckpoints(\"checkpoint\", quiet = true) { (_, _) =>\n    val tempDir = Utils.createTempDir()\n    val log1 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n    (1 to 15).foreach { i =>\n      val txn = log1.startTransaction()\n      val file = createTestAddFile(encodedPath = i.toString) :: Nil\n      val delete: Seq[Action] = if (i > 1) {\n        RemoveFile(i - 1 toString, Some(System.currentTimeMillis()), true) :: Nil\n      } else {\n        Nil\n      }\n      txn.commitManually(delete ++ file: _*)\n    }\n\n    DeltaLog.clearCache()\n    val log2 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n    assert(log2.snapshot.version == log1.snapshot.version)\n    assert(log2.snapshot.allFiles.count == 1)\n  }\n\n  testDifferentCheckpoints(\"update deleted directory\", quiet = true) { (_, _) =>\n    withTempDir { dir =>\n      val path = new Path(dir.getCanonicalPath)\n      val log = DeltaLog.forTable(spark, path)\n\n      // Commit data so the in-memory state isn't consistent with an empty log.\n      val txn = log.startTransaction()\n      val files = (1 to 10).map(f => createTestAddFile(encodedPath = f.toString))\n      txn.commitManually(files: _*)\n      log.checkpoint()\n\n      val fs = path.getFileSystem(log.newDeltaHadoopConf())\n      fs.delete(path, true)\n\n      val snapshot = log.update()\n      assert(snapshot.version === -1)\n    }\n  }\n\n  testDifferentCheckpoints(\n      \"checkpoint write should use the correct Hadoop configuration\") { (_, _) =>\n    withTempDir { dir =>\n      withSQLConf(\n          \"fs.AbstractFileSystem.fake.impl\" -> classOf[FakeAbstractFileSystem].getName,\n          \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n          \"fs.fake.impl.disable.cache\" -> \"true\") {\n        val path = s\"fake://${dir.getCanonicalPath}\"\n        val log = DeltaLog.forTable(spark, path)\n        val txn = log.startTransaction()\n        txn.commitManually(createTestAddFile())\n        log.checkpoint()\n      }\n    }\n  }\n\n  testDifferentCheckpoints(\"update should pick up checkpoints\", quiet = true) { (_, _) =>\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      val checkpointInterval = log.checkpointInterval()\n      for (f <- 0 until (checkpointInterval * 2)) {\n        val txn = log.startTransaction()\n        txn.commitManually(createTestAddFile(encodedPath = f.toString))\n      }\n\n      def collectReservoirStateRDD(rdd: RDD[_]): Seq[RDD[_]] = {\n        if (rdd.name != null && rdd.name.startsWith(\"Delta Table State\")) {\n          Seq(rdd) ++ rdd.dependencies.flatMap(d => collectReservoirStateRDD(d.rdd))\n        } else {\n          rdd.dependencies.flatMap(d => collectReservoirStateRDD(d.rdd))\n        }\n      }\n\n      val numOfStateRDDs = collectReservoirStateRDD(log.snapshot.stateDS.rdd).size\n      assert(numOfStateRDDs >= 1, \"collectReservoirStateRDD may not work properly\")\n      assert(numOfStateRDDs < checkpointInterval)\n    }\n  }\n\n  testDifferentCheckpoints(\n      \"update shouldn't pick up delta files earlier than checkpoint\") { (_, _) =>\n    val tempDir = Utils.createTempDir()\n    val log1 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n    (1 to 5).foreach { i =>\n      val txn = log1.startTransaction()\n      val file = if (i > 1) {\n        createTestAddFile(encodedPath = i.toString) :: Nil\n      } else {\n        Metadata(configuration = Map(DeltaConfigs.CHECKPOINT_INTERVAL.key -> \"10\")) :: Nil\n      }\n      val delete: Seq[Action] = if (i > 1) {\n        RemoveFile(i - 1 toString, Some(System.currentTimeMillis()), true) :: Nil\n      } else {\n        Nil\n      }\n      txn.commitManually(delete ++ file: _*)\n    }\n\n    DeltaLog.clearCache()\n    val log2 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n    (6 to 15).foreach { i =>\n      val txn = log1.startTransaction()\n      val file = createTestAddFile(encodedPath = i.toString) :: Nil\n      val delete: Seq[Action] = if (i > 1) {\n        RemoveFile(i - 1 toString, Some(System.currentTimeMillis()), true) :: Nil\n      } else {\n        Nil\n      }\n      txn.commitManually(delete ++ file: _*)\n    }\n\n    // Since log2 is a separate instance, it shouldn't be updated to version 15\n    assert(log2.snapshot.version == 4)\n    val updateLog2 = log2.update()\n    assert(updateLog2.version == log1.snapshot.version, \"Did not update to correct version\")\n\n    val deltas = log2.snapshot.logSegment.deltas\n    assert(deltas.length === 4, \"Expected 4 files starting at version 11 to 14\")\n    val versions = deltas.map(FileNames.deltaVersion).sorted\n    assert(versions === Seq[Long](11, 12, 13, 14), \"Received the wrong files for update\")\n  }\n\n  testQuietly(\"ActionLog cache should use the normalized path as key\") {\n    withTempDir { tempDir =>\n      val dir = tempDir.getAbsolutePath.stripSuffix(\"/\")\n      assert(dir.startsWith(\"/\"))\n      // scalastyle:off deltahadoopconfiguration\n      val fs = new Path(\"/\").getFileSystem(spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      val samePaths = Seq(\n        new Path(dir + \"/foo\"),\n        new Path(dir + \"/foo/\"),\n        new Path(fs.getScheme + \":\" + dir + \"/foo\"),\n        new Path(fs.getScheme + \"://\" + dir + \"/foo\")\n      )\n      val logs = samePaths.map(DeltaLog.forTable(spark, _))\n      logs.foreach { log =>\n        assert(log eq logs.head)\n      }\n    }\n  }\n\n  testDifferentCheckpoints(\n      \"handle corrupted '_last_checkpoint' file\", quiet = true) { (checkpointPolicy, format) =>\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n      val checkpointInterval = log.checkpointInterval()\n      for (f <- 0 to checkpointInterval) {\n        val txn = log.startTransaction()\n        txn.commitManually(createTestAddFile(encodedPath = f.toString))\n      }\n      val lastCheckpointOpt = log.readLastCheckpointFile()\n      assert(lastCheckpointOpt.isDefined)\n      val lastCheckpoint = lastCheckpointOpt.get\n      import  CheckpointInstance.Format._\n      val expectedCheckpointFormat = if (checkpointPolicy == CheckpointPolicy.V2) V2 else SINGLE\n      assert(CheckpointInstance(lastCheckpoint).format === expectedCheckpointFormat)\n\n      // Create an empty \"_last_checkpoint\" (corrupted)\n      val fs = log.LAST_CHECKPOINT.getFileSystem(log.newDeltaHadoopConf())\n      fs.create(log.LAST_CHECKPOINT, true /* overwrite */).close()\n\n      // Create a new DeltaLog\n      DeltaLog.clearCache()\n      val log2 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      // Make sure we create a new DeltaLog in order to test the loading logic.\n      assert(log ne log2)\n\n      // We should get the same metadata even if \"_last_checkpoint\" is corrupted.\n      assert(CheckpointInstance(log2.readLastCheckpointFile().get) ===\n        CheckpointInstance(lastCheckpoint.version, SINGLE))\n    }\n  }\n\n  testQuietly(\"paths should be canonicalized\") {\n    Seq(\"file:\", \"file://\").foreach { scheme =>\n      withTempDir { dir =>\n        val log = DeltaLog.forTable(spark, dir)\n        assert(new File(log.logPath.toUri).mkdirs())\n        val path = \"/some/unqualified/absolute/path\"\n        val add = AddFile(\n          path, Map.empty, 100L, 10L, dataChange = true)\n        val rm = RemoveFile(\n          s\"$scheme$path\", Some(200L), dataChange = false)\n\n        log.store.write(\n          FileNames.unsafeDeltaFile(log.logPath, 0L),\n          Iterator(Action.supportedProtocolVersion(\n            featuresToExclude = Seq(CatalogOwnedTableFeature)), Metadata(), add)\n            .map(a => JsonUtils.toJson(a.wrap)),\n          overwrite = false,\n          log.newDeltaHadoopConf())\n        log.store.write(\n          FileNames.unsafeDeltaFile(log.logPath, 1L),\n          Iterator(JsonUtils.toJson(rm.wrap)),\n          overwrite = false,\n          log.newDeltaHadoopConf())\n\n        assert(log.update().version === 1)\n        assert(log.snapshot.numOfFiles === 0)\n      }\n    }\n  }\n\n  testQuietly(\"paths should be canonicalized - special characters\") {\n    Seq(\"file:\", \"file://\").foreach { scheme =>\n      withTempDir { dir =>\n        val log = DeltaLog.forTable(spark, dir)\n        assert(new File(log.logPath.toUri).mkdirs())\n        val path = new Path(\"/some/unqualified/with space/p@#h\").toUri.toString\n        val add = AddFile(\n          path, Map.empty, 100L, 10L, dataChange = true)\n        val rm = RemoveFile(\n          s\"$scheme$path\", Some(200L), dataChange = false)\n\n        log.store.write(\n          FileNames.unsafeDeltaFile(log.logPath, 0L),\n          Iterator(Action.supportedProtocolVersion(\n            featuresToExclude = Seq(CatalogOwnedTableFeature)), Metadata(), add)\n            .map(a => JsonUtils.toJson(a.wrap)),\n          overwrite = false,\n          log.newDeltaHadoopConf())\n        log.store.write(\n          FileNames.unsafeDeltaFile(log.logPath, 1L),\n          Iterator(JsonUtils.toJson(rm.wrap)),\n          overwrite = false,\n          log.newDeltaHadoopConf())\n\n        assert(log.update().version === 1)\n        assert(log.snapshot.numOfFiles === 0)\n      }\n    }\n  }\n\n  test(\"Reject read from Delta if no path is passed\") {\n    val e = intercept[IllegalArgumentException](spark.read.format(\"delta\").load()).getMessage\n    assert(e.contains(\"'path' is not specified\"))\n  }\n\n  test(\"do not relativize paths in RemoveFiles\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir)\n      assert(new File(log.logPath.toUri).mkdirs())\n      val path = new File(dir, \"a/b%c/d\").toURI.toString\n      val rm = RemoveFile(path, Some(System.currentTimeMillis()), dataChange = true)\n      log.startTransaction().commitManually(rm)\n\n      val committedRemove = log.update(stalenessAcceptable = false).tombstones.collect().head\n      assert(committedRemove.path === path)\n    }\n  }\n\n  test(\"delete and re-add the same file in different transactions\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir)\n      assert(new File(log.logPath.toUri).mkdirs())\n\n      val add1 = createTestAddFile(modificationTime = System.currentTimeMillis())\n      log.startTransaction().commitManually(add1)\n\n      val rm = add1.remove\n      log.startTransaction().commit(rm :: Nil, DeltaOperations.ManualUpdate)\n\n      val add2 = createTestAddFile(modificationTime = System.currentTimeMillis())\n      log.startTransaction().commit(add2 :: Nil, DeltaOperations.ManualUpdate)\n\n      // Add a new transaction to replay logs using the previous snapshot. If it contained\n      // AddFile(\"foo\") and RemoveFile(\"foo\"), \"foo\" would get removed and fail this test.\n      val otherAdd =\n        createTestAddFile(encodedPath = \"bar\", modificationTime = System.currentTimeMillis())\n      log.startTransaction().commit(otherAdd :: Nil, DeltaOperations.ManualUpdate)\n\n      assert(log.update().allFiles.collect().find(_.path == \"foo\")\n        // `dataChange` is set to `false` after replaying logs.\n        === Some(add2.copy(\n          dataChange = false, baseRowId = Some(1), defaultRowCommitVersion = Some(2))))\n    }\n  }\n\n  test(\"error - versions not contiguous\") {\n    if (catalogOwnedCoordinatorBackfillBatchSize.contains(100L)) {\n      cancel(\"Backfill size of 100 is not compatible w/ the test.\")\n    }\n    withTempDir { dir =>\n      val staleLog = DeltaLog.forTable(spark, dir)\n      DeltaLog.clearCache()\n\n      val log = DeltaLog.forTable(spark, dir)\n      assert(new File(log.logPath.toUri).mkdirs())\n\n      val metadata = Metadata(\n        // Needs to manually enable ICT during manual Metadata update for CC.\n        configuration = Map(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"true\"))\n      val add1 = AddFile(\"foo\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n      log.startTransaction().commit(metadata :: add1 :: Nil, DeltaOperations.ManualUpdate)\n\n      val add2 = AddFile(\"bar\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n      log.startTransaction().commit(add2 :: Nil, DeltaOperations.ManualUpdate)\n\n      val add3 = AddFile(\"baz\", Map.empty, 1L, System.currentTimeMillis(), dataChange = true)\n      log.startTransaction().commit(add3 :: Nil, DeltaOperations.ManualUpdate)\n\n      new File(new Path(log.logPath, \"00000000000000000001.json\").toUri).delete()\n\n      val ex = intercept[IllegalStateException] {\n        staleLog.update()\n      }\n      assert(ex.getMessage.contains(\"Versions (0, 2) are not contiguous.\"))\n    }\n  }\n\n  Seq(\"protocol\", \"metadata\").foreach { action =>\n    test(s\"state reconstruction without $action should fail\") {\n      withTempDir { tempDir =>\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(new File(log.logPath.toUri).mkdirs())\n        val selectedAction = if (action == \"metadata\") {\n          Protocol()\n        } else {\n          Metadata()\n        }\n        val file = AddFile(\"abc\", Map.empty, 1, 1, true)\n        log.store.write(\n          FileNames.unsafeDeltaFile(log.logPath, 0L),\n          Iterator(selectedAction, file).map(a => JsonUtils.toJson(a.wrap)),\n          overwrite = false,\n          log.newDeltaHadoopConf())\n        val e = intercept[IllegalStateException] {\n          log.update()\n        }\n        assert(e.getMessage === DeltaErrors.actionNotFoundException(action, 0).getMessage)\n      }\n    }\n  }\n\n  Seq(\"protocol\", \"metadata\").foreach { action =>\n    testDifferentCheckpoints(s\"state reconstruction from checkpoint with\" +\n        s\" missing $action should fail\", quiet = true) { (_, _) =>\n      withTempDir { tempDir =>\n        import testImplicits._\n        val staleLog = DeltaLog.forTable(spark, tempDir)\n        DeltaLog.clearCache()\n\n        val log = DeltaLog.forTable(spark, tempDir)\n        assert (staleLog != log)\n        val checkpointInterval = log.checkpointInterval()\n        // Create a checkpoint regularly\n        for (f <- 0 to checkpointInterval) {\n          val txn = log.startTransaction()\n          val addFile = createTestAddFile(encodedPath = f.toString)\n          if (f == 0) {\n            txn.commitManually(addFile)\n          } else {\n            txn.commit(Seq(addFile), testOp)\n          }\n        }\n\n        val checksumFilePath = FileNames.checksumFile(log.logPath, log.snapshot.version)\n        removeProtocolAndMetadataFromChecksumFile(checksumFilePath)\n\n        {\n          // Create an incomplete checkpoint without the action and overwrite the\n          // original checkpoint\n          val checkpointPathOpt =\n            log.listFrom(log.snapshot.version).find(FileNames.isCheckpointFile).map(_.getPath)\n          assert(checkpointPathOpt.nonEmpty)\n          assert(FileNames.checkpointVersion(checkpointPathOpt.get) === log.snapshot.version)\n          val checkpointPath = checkpointPathOpt.get\n          def removeActionFromParquetCheckpoint(tmpCheckpoint: File): Unit = {\n            val takeAction = if (action == \"metadata\") {\n              \"protocol\"\n            } else {\n              \"metadata\"\n            }\n            val corruptedCheckpointData = spark.read.schema(SingleAction.encoder.schema)\n              .parquet(checkpointPath.toString)\n              .where(s\"add is not null or $takeAction is not null\")\n              .as[SingleAction].collect()\n\n            // Keep the add files and also filter by the additional condition\n            corruptedCheckpointData.toSeq.toDS().coalesce(1).write\n              .mode(\"overwrite\").parquet(tmpCheckpoint.toString)\n            val writtenCheckpoint =\n              tmpCheckpoint.listFiles().toSeq.filter(_.getName.startsWith(\"part\")).head\n            val checkpointFile = new File(checkpointPath.toUri)\n            new File(log.logPath.toUri).listFiles().toSeq.foreach { file =>\n              if (file.getName.startsWith(\".0\")) {\n                // we need to delete checksum files, otherwise trying to replace our incomplete\n                // checkpoint file fails due to the LocalFileSystem's checksum checks.\n                require(file.delete(), \"Failed to delete checksum file\")\n              }\n            }\n            require(checkpointFile.delete(), \"Failed to delete old checkpoint\")\n            require(writtenCheckpoint.renameTo(checkpointFile),\n              \"Failed to rename corrupt checkpoint\")\n          }\n          if (checkpointPath.getName.endsWith(\"json\")) {\n            val conf = log.newDeltaHadoopConf()\n            val filteredActions = log.store\n              .read(checkpointPath, log.newDeltaHadoopConf())\n              .map(Action.fromJson)\n              .filter {\n                case _: Protocol => action != \"protocol\"\n                case _: Metadata => action != \"metadata\"\n                case _ => true\n              }.map(_.json)\n            log.store.write(checkpointPath, filteredActions.toIterator, overwrite = true, conf)\n          } else {\n            withTempDir { f => removeActionFromParquetCheckpoint(f) }\n          }\n        }\n\n        // Verify if the state reconstruction from the checkpoint fails.\n        val e = intercept[IllegalStateException] {\n          staleLog.update()\n        }\n        assert(e.getMessage ===\n          DeltaErrors.actionNotFoundException(action, checkpointInterval).getMessage)\n      }\n    }\n  }\n\n  testDifferentCheckpoints(\"deleting and recreating a directory should\" +\n      \" cause the snapshot to be recomputed\", quiet = true) { (_, _) =>\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      spark.range(10, 20).write.format(\"delta\").mode(\"append\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n      spark.range(20, 30).write.format(\"delta\").mode(\"append\").save(path)\n\n      // Store these for later usage\n      val actions = deltaLog.snapshot.stateDS.collect()\n      val commitTimestamp = deltaLog.snapshot.logSegment.lastCommitFileModificationTimestamp\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        spark.range(30).toDF()\n      )\n\n      val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n\n      // Now let's delete the last version\n      deltaLog.store\n        .listFrom(\n          FileNames.listingPrefix(deltaLog.logPath, deltaLog.snapshot.version),\n          deltaLog.newDeltaHadoopConf())\n        .filter(!_.getPath.getName.startsWith(\"_\"))\n        .foreach(f => fs.delete(f.getPath, true))\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        // For Catalog Owned table with a commit that is not backfilled, we can't use\n        // 00000000002.json yet. Contact commit coordinator to get uuid file path to malform json\n        // file.\n        val oc = getCatalogOwnedCommitCoordinatorClient(\n          catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING)\n        val tableDesc =\n          new TableDescriptor(deltaLog.logPath, Optional.empty(), Map.empty[String, String].asJava)\n        val commitResponse = oc.getCommits(tableDesc, 2, null)\n        if (!commitResponse.getCommits.isEmpty) {\n          val path = commitResponse.getCommits.asScala.last.getFileStatus.getPath\n          fs.delete(path, true)\n        }\n        // Also deletes it from in-memory commit coordinator.\n        oc.asInstanceOf[TrackingCommitCoordinatorClient]\n          .delegatingCommitCoordinatorClient\n          .asInstanceOf[InMemoryCommitCoordinator]\n          .removeCommitTestOnly(deltaLog.logPath, 2)\n      }\n\n      // Should show up to 20\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        spark.range(20).toDF()\n      )\n\n      // Now let's delete the checkpoint and json file for version 1. We will try to list from\n      // version 1, but since we can't find anything, we should start listing from version 0\n      deltaLog.store\n        .listFrom(\n          FileNames.listingPrefix(deltaLog.logPath, 1),\n          deltaLog.newDeltaHadoopConf())\n        .filter(!_.getPath.getName.startsWith(\"_\"))\n        .foreach(f => fs.delete(f.getPath, true))\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        val oc = getCatalogOwnedCommitCoordinatorClient(\n          catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING)\n        oc.asInstanceOf[TrackingCommitCoordinatorClient]\n          .delegatingCommitCoordinatorClient\n          .asInstanceOf[InMemoryCommitCoordinator]\n          .removeCommitTestOnly(deltaLog.logPath, 1)\n      }\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        spark.range(10).toDF()\n      )\n\n      // Now let's delete that commit as well, and write a new first version\n      deltaLog.listFrom(0)\n        .filter(!_.getPath.getName.startsWith(\"_\"))\n        .foreach(f => fs.delete(f.getPath, false))\n\n      assert(deltaLog.snapshot.version === 0)\n\n      deltaLog.store.write(\n        FileNames.unsafeDeltaFile(deltaLog.logPath, 0),\n        actions.map(_.unwrap.json).iterator,\n        overwrite = false,\n        deltaLog.newDeltaHadoopConf())\n\n      // To avoid flakiness, we manually set the modification timestamp of the file to a later\n      // second\n      new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0).toUri)\n        .setLastModified(commitTimestamp + 5000)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        spark.range(30).toDF()\n      )\n    }\n  }\n\n  test(\"forTableWithSnapshot should always return the latest snapshot\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      assert(deltaLog.snapshot.version === 0)\n\n      val (_, snapshot) = DeltaLog.withFreshSnapshot { _ =>\n        // This update is necessary to advance the lastUpdatedTs beyond the start time of\n        // withFreshSnapshot call.\n        deltaLog.update()\n        // Manually add a commit. However, the deltaLog should now be fresh enough\n        // that we don't trigger another update, and thus don't find the commit.\n        val add = AddFile(path, Map.empty, 100L, 10L, dataChange = true)\n        deltaLog.store.write(\n          FileNames.unsafeDeltaFile(deltaLog.logPath, 1L),\n          Iterator(JsonUtils.toJson(add.wrap)),\n          overwrite = false,\n          deltaLog.newDeltaHadoopConf())\n        (deltaLog, None)\n      }\n      assert(snapshot.version === 0)\n\n      val deltaLog2 = DeltaLog.forTable(spark, path)\n      assert(deltaLog2.snapshot.version === 0) // This shouldn't update\n      val (_, snapshot2) = DeltaLog.forTableWithSnapshot(spark, path)\n      assert(snapshot2.version === 1) // This should get the latest snapshot\n    }\n  }\n\n  test(\"Delta log should handle malformed json\") {\n    val mapper = new ObjectMapper()\n    mapper.registerModule(DefaultScalaModule)\n    def testJsonCommitParser(\n        path: String, func: Map[String, Map[String, String]] => String): Unit = {\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      spark.range(1).write.format(\"delta\").mode(\"append\").save(path)\n\n      val log = DeltaLog.forTable(spark, path)\n      var commitFilePath = FileNames.unsafeDeltaFile(log.logPath, 1L)\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        // For Catalog Owned table with a commit that is not backfilled, we can't use\n        // 00000000001.json yet. Contact commit coordinator to get uuid file path to malform json\n        // file.\n        val oc = getCatalogOwnedCommitCoordinatorClient(\n          catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING)\n        val tableDesc =\n          new TableDescriptor(log.logPath, Optional.empty(), Map.empty[String, String].asJava)\n        val commitResponse = oc.getCommits(tableDesc, 1, null)\n        if (!commitResponse.getCommits.isEmpty) {\n          commitFilePath = commitResponse.getCommits.asScala.head.getFileStatus.getPath\n        }\n      }\n      val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n      val stream = fs.open(commitFilePath)\n      val reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))\n      val commitInfo = reader.readLine() + \"\\n\"\n      val addFile = reader.readLine()\n      stream.close()\n\n      val map = mapper.readValue(addFile, classOf[Map[String, Map[String, String]]])\n      val output = fs.create(commitFilePath, true)\n      output.write(commitInfo.getBytes(StandardCharsets.UTF_8))\n      output.write(func(map).getBytes(StandardCharsets.UTF_8))\n      output.close()\n      DeltaLog.clearCache()\n\n      val parser = JsonToStructs(\n        schema = Action.logSchema,\n        options = DeltaLog.jsonCommitParseOption,\n        child = null,\n        timeZoneId = Some(spark.sessionState.conf.sessionLocalTimeZone))\n\n      val it = log.store.readAsIterator(commitFilePath, log.newDeltaHadoopConf())\n      try {\n        it.foreach { json =>\n          val utf8json = UTF8String.fromString(json)\n          parser.nullSafeEval(utf8json).asInstanceOf[InternalRow]\n        }\n      } finally {\n        it.close()\n      }\n    }\n\n    // Parser should succeed when AddFile in json commit has missing fields\n    withTempDir { dir =>\n      testJsonCommitParser(dir.toString, (content: Map[String, Map[String, String]]) => {\n        mapper.writeValueAsString(Map(\"add\" -> content(\"add\").-(\"path\").-(\"size\"))) + \"\\n\"\n      })\n    }\n\n    // Parser should succeed when AddFile in json commit has extra fields\n    withTempDir { dir =>\n      testJsonCommitParser(dir.toString, (content: Map[String, Map[String, String]]) => {\n        mapper.writeValueAsString(Map(\"add\" -> content(\"add\"). +(\"random\" -> \"field\"))) + \"\\n\"\n      })\n    }\n\n    // Parser should succeed when AddFile in json commit has mismatched schema\n    withTempDir { dir =>\n      val json = \"\"\"{\"x\": 1, \"y\": 2, \"z\": [10, 20]}\"\"\"\n      testJsonCommitParser(dir.toString, (content: Map[String, Map[String, String]]) => {\n        mapper.writeValueAsString(Map(\"add\" -> content(\"add\").updated(\"path\", json))) + \"\\n\"\n      })\n    }\n\n    // Parser should throw exception when AddFile is a bad json\n    withTempDir { dir =>\n      val e = intercept[Throwable] {\n        testJsonCommitParser(dir.toString, (content: Map[String, Map[String, String]]) => {\n          \"bad json{{{\"\n        })\n      }\n      assert(e.getMessage.contains(\"FAILFAST\"))\n    }\n  }\n\n  test(\"DeltaLog cache size should honor config limit\") {\n    def assertCacheSize(expected: Long): Unit = {\n      for (_ <- 1 to 6) {\n        withTempDir(dir => {\n          val path = dir.getCanonicalPath\n          spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n        })\n      }\n      assert(DeltaLog.cacheSize === expected)\n    }\n    DeltaLog.unsetCache()\n    withSQLConf(DeltaSQLConf.DELTA_LOG_CACHE_SIZE.key -> \"4\") {\n      assertCacheSize(4)\n      DeltaLog.unsetCache()\n      // the larger of SQLConf and env var is adopted\n      try {\n        System.getProperties.setProperty(\"delta.log.cacheSize\", \"5\")\n        assertCacheSize(5)\n      } finally {\n        System.getProperties.remove(\"delta.log.cacheSize\")\n      }\n    }\n\n    // assert timeconf returns correct value\n    withSQLConf(DeltaSQLConf.DELTA_LOG_CACHE_RETENTION_MINUTES.key -> \"100\") {\n      assert(spark.sessionState.conf.getConf(\n        DeltaSQLConf.DELTA_LOG_CACHE_RETENTION_MINUTES) === 100)\n    }\n  }\n\n  test(\"DeltaLog should create log directory when ensureLogDirectory is called\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val log = DeltaLog.forTable(spark, new Path(path))\n      log.createLogDirectoriesIfNotExists()\n\n      val logPath = log.logPath\n      val fs = logPath.getFileSystem(log.newDeltaHadoopConf())\n      assert(fs.exists(logPath), \"Log path should exist.\")\n      assert(fs.getFileStatus(logPath).isDirectory, \"Log path should be a directory\")\n      val commitPath = FileNames.commitDirPath(logPath)\n      assert(fs.exists(commitPath), \"Commit path should exist.\")\n      assert(fs.getFileStatus(commitPath).isDirectory, \"Commit path should be a directory\")\n    }\n  }\n\n  test(\"DeltaLog should throw exception when unable to create log directory \" +\n      \"with filesystem IO Exception\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val log = DeltaLog.forTable(spark, new Path(path))\n      val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n\n      // create a file in place of what should be the directory.\n      // Attempting to create a child file/directory should fail and throw an IOException.\n      fs.create(log.logPath)\n\n      val e = intercept[DeltaIOException] {\n        log.createLogDirectoriesIfNotExists()\n      }\n      checkError(e, \"DELTA_CANNOT_CREATE_LOG_PATH\", \"42KD5\", Map(\"path\" -> log.logPath.toString))\n      e.getCause match {\n        case e: IOException =>\n          assert(e.getMessage.contains(\"Parent path is not a directory\"))\n        case _ =>\n          fail(s\"Expected IOException, got ${e.getCause}\")\n      }\n    }\n  }\n\n  test(\"DeltaFileProviderUtils.getDeltaFilesInVersionRange\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(0, 1).write.format(\"delta\").mode(\"overwrite\").save(path)\n      spark.range(0, 1).write.format(\"delta\").mode(\"overwrite\").save(path)\n      spark.range(0, 1).write.format(\"delta\").mode(\"overwrite\").save(path)\n      spark.range(0, 1).write.format(\"delta\").mode(\"overwrite\").save(path)\n      val log = DeltaLog.forTable(spark, new Path(path))\n      val result = DeltaFileProviderUtils.getDeltaFilesInVersionRange(\n        spark, log, startVersion = 1, endVersion = 3, catalogTableOpt = None)\n      assert(result.map(FileNames.getFileVersion) === Seq(1, 2, 3))\n      val filesAreUnbackfilledArray = result.map(FileNames.isUnbackfilledDeltaFile)\n\n      val (fileV1, fileV2, fileV3) = (result(0), result(1), result(2))\n      assert(FileNames.getFileVersion(fileV1) === 1)\n      assert(FileNames.getFileVersion(fileV2) === 2)\n      assert(FileNames.getFileVersion(fileV3) === 3)\n\n      val backfillInterval =\n        catalogOwnedCoordinatorBackfillBatchSize.getOrElse(0L)\n      if (backfillInterval == 0 || backfillInterval == 1) {\n        assert(filesAreUnbackfilledArray === Seq(false, false, false))\n      } else if (backfillInterval == 2) {\n        assert(filesAreUnbackfilledArray === Seq(false, false, true))\n      } else {\n        assert(filesAreUnbackfilledArray === Seq(true, true, true))\n      }\n    }\n  }\n\n  test(\"checksum file should contain protocol and metadata\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"true\",\n      DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key -> \"true\"\n    ) {\n      withTempDir { dir =>\n        val path = new Path(\"file://\" + dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, path)\n\n        val txn = log.startTransaction()\n        val files = (1 to 10).map(f => createTestAddFile(encodedPath = f.toString))\n        txn.commitManually(files: _*)\n        val metadata = log.snapshot.metadata\n        val protocol = log.snapshot.protocol\n        DeltaLog.clearCache()\n\n        val readLog = DeltaLog.forTable(spark, path)\n        val checksum = readLog.snapshot.checksumOpt.get\n        assert(checksum.metadata != null)\n        assert(checksum.protocol != null)\n        assert(checksum.metadata.equals(metadata))\n        assert(checksum.protocol.equals(protocol))\n      }\n    }\n  }\n\n  test(\"checksum reader should be able to read incomplete checksum file without \" +\n    \"protocol and metadata\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"true\",\n      DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key -> \"true\"\n    ) {\n      withTempDir { dir =>\n        val path = new Path(\"file://\" + dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, path)\n\n        val txn = log.startTransaction()\n        val files = (1 to 10).map(f => createTestAddFile(encodedPath = f.toString))\n        txn.commitManually(files: _*)\n        val metadata = log.snapshot.metadata\n        val protocol = log.snapshot.protocol\n        DeltaLog.clearCache()\n        val checksumFilePath = FileNames.checksumFile(log.logPath, 0L)\n        removeProtocolAndMetadataFromChecksumFile(checksumFilePath)\n\n        val readLog = DeltaLog.forTable(spark, path)\n        val checksum = readLog.snapshot.checksumOpt.get\n        assert(checksum.metadata == null)\n        assert(checksum.protocol == null)\n\n        // check we are still able to read protocol and metadata from checkpoint\n        assert(readLog.snapshot.metadata.equals(metadata))\n        assert(readLog.snapshot.protocol.equals(protocol))\n      }\n    }\n  }\n\n  private def testCreateDataFrame(shouldDropNullTypeColumns: Boolean): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_CREATE_DATAFRAME_DROP_NULL_COLUMNS.key ->\n      shouldDropNullTypeColumns.toString) {\n      withTempDir { tempDir =>\n        spark.sql(\"select CAST(null as VOID) as nullTypeCol, id from range(10)\")\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val df = deltaLog.createDataFrame(deltaLog.update(), Seq.empty, isStreaming = false)\n        val nullTypeFields = df.schema.filter(_.dataType == NullType)\n        if (shouldDropNullTypeColumns) {\n          assert(nullTypeFields.isEmpty)\n        } else {\n          assert(nullTypeFields.size == 1)\n        }\n      }\n    }\n  }\n\n  test(\"DeltaLog.createDataFrame should drop null columns with feature flag\") {\n    testCreateDataFrame(shouldDropNullTypeColumns = true)\n  }\n\n  test(\"DeltaLog.createDataFrame should not drop null columns without feature flag\") {\n    testCreateDataFrame(shouldDropNullTypeColumns = false)\n  }\n}\n\nclass DeltaLogWithCatalogOwnedBatch1Suite extends DeltaLogSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaLogWithCatalogOwnedBatch2Suite extends DeltaLogSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaLogWithCatalogOwnedBatch100Suite extends DeltaLogSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaMetricsUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable.ArrayBuffer\n\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, CommitInfo, RemoveFile}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.scalatest.Assertions._\n\n/**\n * Various helper methods to for metric tests.\n */\nobject DeltaMetricsUtils\n  {\n\n  /**\n   * Get operation metrics of the last operation of a table.\n   *\n   * @param table The Delta table to query\n   * @return The operation metrics of the last command.\n   */\n  def getLastOperationMetrics(table: io.delta.tables.DeltaTable): Map[String, Long] = {\n    table.history().select(\"operationMetrics\").take(1).head.getMap(0)\n      .asInstanceOf[Map[String, String]].mapValues(_.toLong).toMap\n  }\n\n  def getLastOperationMetrics(tableName: String): Map[String, Long] = {\n    getLastOperationMetrics(io.delta.tables.DeltaTable.forName(tableName))\n  }\n\n   /**\n   * Assert that metrics of a Delta operation have the expected values.\n   *\n   * @param expectedMetrics The expected metrics the values of which to check.\n   * @param operationMetrics The operation metrics that were collected from Delta log.\n   */\n  def checkOperationMetrics(\n      expectedMetrics: Map[String, Long],\n      operationMetrics: Map[String, Long]): Unit = {\n    val sep = System.lineSeparator() * 2\n    val failMessages = expectedMetrics.flatMap { case (metric, expectedValue) =>\n      // Check missing metrics.\n      var errMsg = if (!operationMetrics.contains(metric)) {\n        Some(\n          s\"\"\"The recorded operation metrics does not contain metric: $metric\"\n             | ExpectedMetrics = $expectedMetrics\n             | ActualMetrics = $operationMetrics\n             |\"\"\".stripMargin)\n      } else {\n        None\n      }\n\n      // Check negative values.\n      errMsg = errMsg.orElse {\n        if (operationMetrics(metric) < 0) {\n          Some(s\"Invalid non-positive value for metric $metric: ${operationMetrics(metric)}\")\n        } else {\n          None\n        }\n      }\n\n      // Check unexpected values.\n      errMsg = errMsg.orElse {\n        if (expectedValue != operationMetrics(metric)) {\n          Some(\n            s\"\"\"The recorded metric for $metric does not equal the expected value.\n               | Expected = ${expectedMetrics(metric)}\n               | Actual = ${operationMetrics(metric)}\n               | ExpectedMetrics = $expectedMetrics\n               | ActualMetrics = $operationMetrics\n               |\"\"\".stripMargin)\n        } else {\n          None\n        }\n      }\n      errMsg\n    }.mkString(sep, sep, sep).trim\n    assert(failMessages.isEmpty)\n  }\n\n  /**\n   * Check that time metrics for a Delta operation are valid.\n   *\n   * @param operationMetrics The collected operation metrics from the Delta log.\n   * @param expectedMetrics The keys of the expected time metrics. Set to None to check for\n   *                        common time metrics.\n   */\n  def checkOperationTimeMetrics(\n      operationMetrics: Map[String, Long],\n      expectedMetrics: Set[String]): Unit = {\n    // Validate that all time metrics exist and have a non-negative value.\n    for (key <- expectedMetrics) {\n      assert(operationMetrics.contains(key), s\"Missing operation metric $key\")\n      val value: Long = operationMetrics(key)\n      assert(value >= 0,\n        s\"Invalid non-positive value for metric $key: $value\")\n    }\n\n    // Validate that if 'executionTimeMs' exists, is larger than all other time metrics.\n    if (expectedMetrics.contains(\"executionTimeMs\")) {\n      val executionTimeMs = operationMetrics(\"executionTimeMs\")\n      val maxTimeMs = operationMetrics.filterKeys(k => expectedMetrics.contains(k))\n        .valuesIterator.max\n      assert(executionTimeMs == maxTimeMs)\n    }\n  }\n\n  /**\n   * Computes the expected operation metrics from the actions in a Delta commit.\n   *\n   * @param deltaLog The Delta log of the table.\n   * @param version The version of the commit.\n   * @return A map with the expected operation metrics.\n   */\n  def getOperationMetricsFromCommitActions(\n      deltaLog: DeltaLog,\n      version: Long): Map[String, Long] = {\n    val (_, changes) = deltaLog.getChanges(version).next()\n    val commitInfo = changes.collect { case ci: CommitInfo => ci }.head\n    val operationName = commitInfo.operation\n\n    var filesAdded = ArrayBuffer.empty[AddFile]\n    var filesRemoved = ArrayBuffer.empty[RemoveFile]\n    val changeFilesAdded = ArrayBuffer.empty[AddCDCFile]\n    changes.foreach {\n      case a: AddFile => filesAdded.append(a)\n      case r: RemoveFile => filesRemoved.append(r)\n      case c: AddCDCFile => changeFilesAdded.append(c)\n      case _ => // Nothing\n    }\n\n    // Filter-out DV updates from files added and removed.\n    val pathsWithDvUpdate = filesAdded.map(_.path).toSet & filesRemoved.map(_.path).toSet\n    filesAdded = filesAdded.filter(a => !pathsWithDvUpdate.contains(a.path))\n    val numFilesAdded = filesAdded.size\n    val numBytesAdded = filesAdded.map(_.size).sum\n\n    filesRemoved = filesRemoved.filter(r => !pathsWithDvUpdate.contains(r.path))\n    val numFilesRemoved = filesRemoved.size\n    val numBytesRemoved = filesRemoved.map(_.size.getOrElse(0L)).sum\n\n    val numChangeFilesAdded = changeFilesAdded.size\n\n    operationName match {\n      case \"MERGE\" => Map(\n        \"numTargetFilesAdded\" -> numFilesAdded,\n        \"numTargetFilesRemoved\" -> numFilesRemoved,\n        \"numTargetBytesAdded\" -> numBytesAdded,\n        \"numTargetBytesRemoved\" -> numBytesRemoved,\n        \"numTargetChangeFilesAdded\" -> numChangeFilesAdded\n      )\n      case \"UPDATE\" | \"DELETE\" => Map(\n        \"numAddedFiles\" -> numFilesAdded,\n        \"numRemovedFiles\" -> numFilesRemoved,\n        \"numAddedBytes\" -> numBytesAdded,\n        \"numRemovedBytes\" -> numBytesRemoved,\n        \"numAddedChangeFiles\" -> numChangeFilesAdded\n      )\n      case _ =>\n        throw new UnsupportedOperationException(s\"Unsupported operation: $operationName\")\n    }\n  }\n\n  /**\n   * Checks the provided operation metrics against the actions in a Delta commit.\n   *\n   * @param deltaLog The Delta log of the table.\n   * @param version The version of the commit.\n   * @param operationMetrics The operation metrics that were collected from Delta log.\n   */\n  def checkOperationMetricsAgainstCommitActions(\n      deltaLog: DeltaLog,\n      version: Long,\n      operationMetrics: Map[String, Long]): Unit = {\n    checkOperationMetrics(\n      expectedMetrics = getOperationMetricsFromCommitActions(deltaLog, version),\n      operationMetrics = operationMetrics)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaNotSupportedDDLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Locale\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\n\nclass DeltaNotSupportedDDLSuite\n  extends DeltaNotSupportedDDLBase\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n\n\nabstract class DeltaNotSupportedDDLBase extends QueryTest\n    with DeltaSQLTestUtils {\n\n  val format = \"delta\"\n\n  val nonPartitionedTableName = \"deltaTbl\"\n\n  val partitionedTableName = \"partitionedTahoeTbl\"\n\n  protected override def beforeAll(): Unit = {\n    super.beforeAll()\n    try {\n      sql(s\"\"\"\n           |CREATE TABLE $nonPartitionedTableName\n           |USING $format\n           |AS SELECT 1 as a, 'a' as b\n         \"\"\".stripMargin)\n\n      sql(s\"\"\"\n            |CREATE TABLE $partitionedTableName (a INT, b STRING, p1 INT)\n            |USING $format\n            |PARTITIONED BY (p1)\n          \"\"\".stripMargin)\n      sql(s\"INSERT INTO $partitionedTableName SELECT 1, 'A', 2\")\n    } catch {\n      case NonFatal(e) =>\n        afterAll()\n        throw e\n    }\n  }\n\n  protected override def afterAll(): Unit = {\n    try {\n      sql(s\"DROP TABLE IF EXISTS $nonPartitionedTableName\")\n      sql(s\"DROP TABLE IF EXISTS $partitionedTableName\")\n    } finally {\n      super.afterAll()\n    }\n  }\n\n\n  def assertUnsupported(query: String, messages: String*): Unit = {\n    val allErrMessages = \"operation not allowed\" +: messages\n    val e = intercept[AnalysisException] {\n      sql(query)\n    }\n    assert(allErrMessages.exists(err => e.getMessage.toLowerCase(Locale.ROOT).contains(err)))\n  }\n\n  private def assertIgnored(query: String): Unit = {\n    val outputStream = new java.io.ByteArrayOutputStream()\n    Console.withOut(outputStream) {\n      sql(query)\n    }\n    assert(outputStream.toString.contains(\"The request is ignored\"))\n  }\n\n  test(\"bucketing is not supported for delta tables\") {\n    withTable(\"tbl\") {\n      assertUnsupported(\n        s\"\"\"\n          |CREATE TABLE tbl(a INT, b INT)\n          |USING $format\n          |CLUSTERED BY (a) INTO 5 BUCKETS\n        \"\"\".stripMargin)\n    }\n  }\n\n  test(\"ANALYZE TABLE PARTITION\") {\n    assertUnsupported(\n      s\"ANALYZE TABLE $partitionedTableName PARTITION (p1) COMPUTE STATISTICS\",\n      \"not supported for v2 tables\")\n  }\n\n  test(\"ALTER TABLE ADD PARTITION\") {\n    assertUnsupported(\n      s\"ALTER TABLE $partitionedTableName ADD PARTITION (p1=3)\",\n      \"does not support partition management\")\n  }\n\n  test(\"ALTER TABLE DROP PARTITION\") {\n    assertUnsupported(\n      s\"ALTER TABLE $partitionedTableName DROP PARTITION (p1=2)\",\n      \"does not support partition management\")\n  }\n\n  test(\"ALTER TABLE RECOVER PARTITIONS\") {\n    assertUnsupported(\n      s\"ALTER TABLE $partitionedTableName RECOVER PARTITIONS\",\n      \"alter table ... recover partitions is not supported for v2 tables\")\n    assertUnsupported(\n      s\"MSCK REPAIR TABLE $partitionedTableName\",\n      \"msck repair table is not supported for v2 tables\")\n  }\n\n  test(\"ALTER TABLE SET SERDEPROPERTIES\") {\n    assertUnsupported(\n      s\"ALTER TABLE $nonPartitionedTableName SET SERDEPROPERTIES (s1=3)\",\n      \"alter table ... set [serde|serdeproperties] is not supported for v2 tables\")\n  }\n\n\n  test(\"LOAD DATA\") {\n    assertUnsupported(\n      s\"\"\"LOAD DATA LOCAL INPATH '/path/to/home' INTO TABLE $nonPartitionedTableName\"\"\",\n      \"not supported for v2 tables\")\n  }\n\n  test(\"INSERT OVERWRITE DIRECTORY\") {\n    assertUnsupported(s\"INSERT OVERWRITE DIRECTORY '/path/to/home' USING $format VALUES (1, 'a')\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaOptionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Locale\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaOptions.{OVERWRITE_SCHEMA_OPTION, PARTITION_OVERWRITE_MODE_OPTION}\nimport org.apache.spark.sql.delta.actions.{Action, FileAction}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.commons.io.FileUtils\nimport org.apache.parquet.format.CompressionCodec\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest}\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.internal.SQLConf.PARTITION_OVERWRITE_MODE\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\nclass DeltaOptionSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n\n\n  test(\"support for setting dataChange to false\") {\n    val tempDir = Utils.createTempDir()\n\n    spark.range(100)\n      .write\n      .format(\"delta\")\n      .save(tempDir.toString)\n\n    val df = spark.read.format(\"delta\").load(tempDir.toString)\n\n    df\n      .write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(\"dataChange\", \"false\")\n      .save(tempDir.toString)\n\n    val deltaLog = DeltaLog.forTable(spark, tempDir)\n    val version = deltaLog.snapshot.version\n    val commitActions = deltaLog.store\n      .read(FileNames.unsafeDeltaFile(deltaLog.logPath, version), deltaLog.newDeltaHadoopConf())\n      .map(Action.fromJson)\n    val fileActions = commitActions.collect { case a: FileAction => a }\n\n    assert(fileActions.forall(!_.dataChange))\n  }\n\n  test(\"dataChange is by default set to true\") {\n    val tempDir = Utils.createTempDir()\n\n    spark.range(100)\n      .write\n      .format(\"delta\")\n      .save(tempDir.toString)\n\n    val df = spark.read.format(\"delta\").load(tempDir.toString)\n\n    df\n      .write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .save(tempDir.toString)\n\n    val deltaLog = DeltaLog.forTable(spark, tempDir)\n    val version = deltaLog.snapshot.version\n    val commitActions = deltaLog.store\n      .read(FileNames.unsafeDeltaFile(deltaLog.logPath, version), deltaLog.newDeltaHadoopConf())\n      .map(Action.fromJson)\n    val fileActions = commitActions.collect { case a: FileAction => a }\n\n    assert(fileActions.forall(_.dataChange))\n  }\n\n  test(\"dataChange is set to false on metadata changing operation\") {\n    withTempDir { tempDir =>\n      // Initialize a table while having dataChange set to false.\n      val e = intercept[AnalysisException] {\n        spark.range(100)\n          .write\n          .format(\"delta\")\n          .option(\"dataChange\", \"false\")\n          .save(tempDir.getAbsolutePath)\n      }\n      assert(e.getMessage ===\n        DeltaErrors.unexpectedDataChangeException(\"Create a Delta table\").getMessage)\n      spark.range(100)\n        .write\n        .format(\"delta\")\n        .save(tempDir.getAbsolutePath)\n\n      // Adding a new column to the existing table while having dataChange set to false.\n      val e2 = intercept[AnalysisException] {\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n        df.withColumn(\"id2\", 'id + 1)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"mergeSchema\", \"true\")\n          .option(\"dataChange\", \"false\")\n          .save(tempDir.getAbsolutePath)\n      }\n      assert(e2.getMessage ===\n        DeltaErrors.unexpectedDataChangeException(\"Change the Delta table schema\").getMessage)\n\n      // Overwriting the schema of the existing table while having dataChange as false.\n      val e3 = intercept[AnalysisException] {\n        spark.range(50)\n          .withColumn(\"id3\", 'id + 1)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"dataChange\", \"false\")\n          .option(\"overwriteSchema\", \"true\")\n          .save(tempDir.getAbsolutePath)\n      }\n      assert(e3.getMessage ===\n        DeltaErrors.unexpectedDataChangeException(\"Overwrite the Delta table schema or \" +\n          \"change the partition schema\").getMessage)\n    }\n  }\n\n\n  test(\"support the maxRecordsPerFile write option: path\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"maxRecordsPerFile\") {\n        spark.range(100)\n          .write\n          .format(\"delta\")\n          .option(\"maxRecordsPerFile\", 5)\n          .save(path)\n        assert(FileUtils.listFiles(tempDir, Array(\"parquet\"), false).size === 20)\n      }\n    }\n  }\n\n  test(\"support the maxRecordsPerFile write option: external table\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"maxRecordsPerFile\") {\n        spark.range(100)\n          .write\n          .format(\"delta\")\n          .option(\"maxRecordsPerFile\", 5)\n          .option(\"path\", path)\n          .saveAsTable(\"maxRecordsPerFile\")\n        assert(FileUtils.listFiles(tempDir, Array(\"parquet\"), false).size === 20)\n      }\n    }\n  }\n\n  test(\"support the maxRecordsPerFile write option: v2 write\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"maxRecordsPerFile\") {\n        spark.range(100)\n          .writeTo(\"maxRecordsPerFile\")\n          .using(\"delta\")\n          .option(\"maxRecordsPerFile\", 5)\n          .tableProperty(\"location\", path)\n          .create()\n        assert(FileUtils.listFiles(tempDir, Array(\"parquet\"), false).size === 20)\n      }\n    }\n  }\n\n  test(\"support no compression write option (defaults to snappy)\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"compression\") {\n        spark.range(100)\n          .writeTo(\"compression\")\n          .using(\"delta\")\n          .tableProperty(\"location\", path)\n          .create()\n        assert(FileUtils.listFiles(tempDir, Array(\"snappy.parquet\"), false).size > 0)\n      }\n    }\n  }\n\n  // LZO and BROTLI left out as additional library dependencies needed\n  val codecsAndSubExtensions = Seq(\n    CompressionCodec.UNCOMPRESSED -> \"\",\n    CompressionCodec.SNAPPY -> \"snappy.\",\n    CompressionCodec.GZIP -> \"gz.\",\n    CompressionCodec.LZ4 -> \"lz4hadoop.\",\n    // CompressionCodec.LZ4_RAW -> \"lz4raw.\", // Support is not yet available in Spark 3.5\n    CompressionCodec.ZSTD -> \"zstd.\"\n  )\n\n  codecsAndSubExtensions.foreach { case (codec, subExt) =>\n    val codecName = codec.name().toLowerCase(Locale.ROOT)\n    test(s\"support compression codec '$codecName' as write option\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n        withTable(s\"compression_$codecName\") {\n          spark.range(100)\n            .writeTo(s\"compression_$codecName\")\n            .using(\"delta\")\n            .option(\"compression\", codecName)\n            .tableProperty(\"location\", path)\n            .create()\n          assert(FileUtils.listFiles(tempDir, Array(s\"${subExt}parquet\"), false).size > 0)\n        }\n      }\n    }\n  }\n\n  test(\"invalid compression write option\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"compression\") {\n        assert(\n          intercept[java.lang.IllegalArgumentException] {\n            spark.range(100)\n              .writeTo(\"compression\")\n              .using(\"delta\")\n              .option(\"compression\", \"???\")\n              .tableProperty(\"location\", path)\n              .create()\n          }.getMessage.nonEmpty)\n      }\n    }\n  }\n\n  for {\n    invalidMode <- Seq(\"ADAPTIVE\", null)\n  } {\n    test(\"DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED = true: \" +\n      s\"partitionOverwriteMode is set to invalid value in options invalidMode=$invalidMode\") {\n      withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n        withTempDir { tempDir =>\n          val e = intercept[IllegalArgumentException] {\n            Seq(1, 2, 3).toDF\n              .withColumn(\"part\", $\"value\" % 2)\n              .write\n              .format(\"delta\")\n              .partitionBy(\"part\")\n              .option(\"partitionOverwriteMode\", invalidMode)\n              .save(tempDir.getAbsolutePath)\n          }\n          assert(e.getMessage ===\n            DeltaErrors.illegalDeltaOptionException(\n              PARTITION_OVERWRITE_MODE_OPTION, invalidMode, \"must be 'STATIC' or 'DYNAMIC'\"\n            ).getMessage\n          )\n        }\n      }\n    }\n  }\n\n  for {\n    invalidMode <- Seq(\"ADAPTIVE\", null)\n  } {\n    test(\"DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED = false: \" +\n      s\"partitionOverwriteMode is set to invalid value in options invalidMode=$invalidMode\") {\n      // partitionOverwriteMode is ignored and no error is thrown\n      withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"false\") {\n        withTempDir { tempDir =>\n          Seq(1, 2, 3).toDF\n            .withColumn(\"part\", $\"value\" % 2)\n            .write\n            .format(\"delta\")\n            .partitionBy(\"part\")\n            .option(\"partitionOverwriteMode\", invalidMode)\n            .save(tempDir.getAbsolutePath)\n        }\n      }\n    }\n  }\n\n  test(\"overwriteSchema=true should be invalid with partitionOverwriteMode=dynamic\") {\n    withTempDir { tempDir =>\n      val e = intercept[DeltaIllegalArgumentException] {\n        withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n          Seq(1, 2, 3).toDF\n            .withColumn(\"part\", $\"value\" % 2)\n            .write\n            .mode(\"overwrite\")\n            .format(\"delta\")\n            .partitionBy(\"part\")\n            .option(OVERWRITE_SCHEMA_OPTION, \"true\")\n            .option(PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n            .save(tempDir.getAbsolutePath)\n        }\n      }\n      assert(e.getErrorClass == \"DELTA_OVERWRITE_SCHEMA_WITH_DYNAMIC_PARTITION_OVERWRITE\")\n    }\n  }\n\n  test(\"overwriteSchema=true should be invalid with partitionOverwriteMode=dynamic, \" +\n      \"saveAsTable\") {\n    withTable(\"temp\") {\n      val e = intercept[DeltaIllegalArgumentException] {\n        withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n          Seq(1, 2, 3).toDF\n            .withColumn(\"part\", $\"value\" % 2)\n            .write\n            .mode(\"overwrite\")\n            .format(\"delta\")\n            .partitionBy(\"part\")\n            .option(OVERWRITE_SCHEMA_OPTION, \"true\")\n            .option(PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n            .saveAsTable(\"temp\")\n        }\n      }\n      assert(e.getErrorClass == \"DELTA_OVERWRITE_SCHEMA_WITH_DYNAMIC_PARTITION_OVERWRITE\")\n    }\n  }\n\n  test(\"Prohibit spark.databricks.delta.dynamicPartitionOverwrite.enabled=false in \" +\n    \"dynamic partition overwrite mode\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"false\") {\n        var e = intercept[DeltaIllegalArgumentException] {\n          Seq(1, 2, 3).toDF\n            .withColumn(\"part\", $\"value\" % 2)\n            .write\n            .format(\"delta\")\n            .partitionBy(\"part\")\n            .option(\"partitionOverwriteMode\", \"dynamic\")\n            .save(tempDir.getAbsolutePath)\n        }\n        assert(e.getErrorClass == \"DELTA_DYNAMIC_PARTITION_OVERWRITE_DISABLED\")\n        withSQLConf(PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n          e = intercept[DeltaIllegalArgumentException] {\n            Seq(1, 2, 3).toDF\n              .withColumn(\"part\", $\"value\" % 2)\n              .write\n              .format(\"delta\")\n              .partitionBy(\"part\")\n              .save(tempDir.getAbsolutePath)\n          }\n        }\n        assert(e.getErrorClass == \"DELTA_DYNAMIC_PARTITION_OVERWRITE_DISABLED\")\n      }\n    }\n  }\n\n  for (createOrReplace <- Seq(\"CREATE OR REPLACE\", \"REPLACE\")) {\n    test(s\"$createOrReplace table command should not respect \" +\n      \"dynamic partition overwrite mode\") {\n      withTempDir { tempDir =>\n        Seq(0, 1).toDF\n          .withColumn(\"key\", $\"value\" % 2)\n          .withColumn(\"stringColumn\", lit(\"string\"))\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .save(tempDir.getAbsolutePath)\n        withSQLConf(PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n          // Write only to one partition with a different schema type of stringColumn.\n          sql(\n            s\"\"\"\n               |$createOrReplace TABLE delta.`${tempDir.getAbsolutePath}`\n               |USING delta\n               |PARTITIONED BY (part)\n               |LOCATION '${tempDir.getAbsolutePath}'\n               |AS SELECT -1 as value, 0 as part, 0 as stringColumn\n               |\"\"\".stripMargin)\n          assert(spark.read.format(\"delta\").load(tempDir.getAbsolutePath).count() == 1,\n            \"Table should be fully replaced even with DPO mode enabled\")\n        }\n      }\n    }\n  }\n\n  // Same test as above but using DeltaWriter V2.\n  test(\"create or replace table V2 should not respect dynamic partition overwrite mode\") {\n    withTable(\"temp\") {\n      Seq(0, 1).toDF\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .saveAsTable(\"temp\")\n      withSQLConf(PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n        // Write to one partition only.\n        Seq(0).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .writeTo(\"temp\")\n          .using(\"delta\")\n          .createOrReplace()\n        assert(spark.read.format(\"delta\").table(\"temp\").count() == 1,\n          \"Table should be fully replaced even with DPO mode enabled\")\n      }\n    }\n  }\n\n  // Same test as above but using saveAsTable.\n  test(\"saveAsTable with overwrite should respect dynamic partition overwrite mode\") {\n    withTable(\"temp\") {\n      Seq(0, 1).toDF\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .saveAsTable(\"temp\")\n      // Write to one partition only.\n      Seq(0).toDF\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .mode(\"overwrite\")\n        .option(PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n        .partitionBy(\"part\")\n        .format(\"delta\")\n        .saveAsTable(\"temp\")\n      assert(spark.read.format(\"delta\").table(\"temp\").count() == 2,\n        \"Table should keep the original partition with DPO mode enabled.\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaParquetFileFormatSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.files.TahoeLogFileIndex\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.parquet.hadoop.ParquetFileReader\n\nimport org.apache.spark.sql.{DataFrame, Dataset, QueryTest}\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelation}\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait DeltaParquetFileFormatSuiteBase\n    extends QueryTest\n    with SharedSparkSession\n    with DeletionVectorsTestUtils\n    with DeltaSQLCommandTest {\n  import testImplicits._\n\n  /** Helper method to run the test with vectorized and non-vectorized Parquet readers */\n  protected def testWithBothParquetReaders(name: String)(f: => Any): Unit = {\n    for {\n      enableVectorizedParquetReader <- BOOLEAN_DOMAIN\n      readColumnarBatchAsRows <- BOOLEAN_DOMAIN\n      // don't run for the combination (vectorizedReader=false, readColumnarBathAsRows = false)\n      // as the non-vectorized reader always generates and returns rows, unlike the vectorized\n      // reader which internally generates columnar batches but can returns either columnar batches\n      // or rows from the columnar batch depending upon the config.\n      if enableVectorizedParquetReader || readColumnarBatchAsRows\n    } {\n      test(s\"$name, with vectorized Parquet reader=$enableVectorizedParquetReader, \" +\n        s\"with readColumnarBatchAsRows=$readColumnarBatchAsRows\") {\n        // Set the max code gen fields to 0 to force the vectorized Parquet reader generate rows\n        // from columnar batches.\n        val codeGenMaxFields = if (readColumnarBatchAsRows) \"0\" else \"100\"\n        withSQLConf(\n          \"spark.sql.parquet.enableVectorizedReader\" -> enableVectorizedParquetReader.toString,\n          \"spark.sql.codegen.maxFields\" -> codeGenMaxFields) {\n          f\n        }\n      }\n    }\n  }\n\n  /** Helper method to generate a table with single Parquet file with multiple rowgroups */\n  protected def generateData(tablePath: String): Unit = {\n    // This is to generate a Parquet file with two row groups\n    hadoopConf().set(\"parquet.block.size\", (1024 * 50).toString)\n\n    // Keep the number of partitions to 1 to generate a single Parquet data file\n    val df = Seq.range(0, 20000).toDF().repartition(1)\n    df.write.format(\"delta\").mode(\"append\").save(tablePath)\n\n    // Set DFS block size to be less than Parquet rowgroup size, to allow\n    // the file split logic to kick-in, but gets turned off due to the\n    // disabling of file splitting in DeltaParquetFileFormat when DVs are present.\n    hadoopConf().set(\"dfs.block.size\", (1024 * 20).toString)\n  }\n\n  protected def assertParquetHasMultipleRowGroups(filePath: Path): Unit = {\n    val parquetMetadata = ParquetFileReader.readFooter(\n      hadoopConf,\n      filePath,\n      ParquetMetadataConverter.NO_FILTER)\n    assert(parquetMetadata.getBlocks.size() > 1)\n  }\n\n  protected def hadoopConf(): Configuration = {\n    // scalastyle:off hadoopconfiguration\n    // This is to generate a Parquet file with two row groups\n    spark.sparkContext.hadoopConfiguration\n    // scalastyle:on hadoopconfiguration\n  }\n\n  lazy val dvStore: DeletionVectorStore = DeletionVectorStore.createInstance(hadoopConf)\n}\n\n\nclass DeltaParquetFileFormatSuite extends DeltaParquetFileFormatSuiteBase {\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key, \"false\")\n  }\n\n  // Read with deletion vectors has separate code paths based on vectorized Parquet\n  // reader is enabled or not. Test both the combinations\n  for {\n    readIsRowDeletedCol <- BOOLEAN_DOMAIN\n    readRowIndexCol <- BOOLEAN_DOMAIN\n    enableDVs <- BOOLEAN_DOMAIN\n    if (enableDVs && readIsRowDeletedCol) || !enableDVs\n  } {\n    testWithBothParquetReaders(\n      s\"isDeletionVectorsEnabled=$enableDVs, read DV metadata columns: \" +\n        s\"with isRowDeletedCol=$readIsRowDeletedCol, \" +\n        s\"with rowIndexCol=$readRowIndexCol\") {\n      withSQLConf(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey ->\n          enableDVs.toString) {\n        withTempDir { tempDir =>\n          val tablePath = tempDir.toString\n\n          // Generate a table with one parquet file containing multiple row groups.\n          generateData(tablePath)\n\n          val deltaLog = DeltaLog.forTable(spark, tempDir)\n          val metadata = deltaLog.snapshot.metadata\n\n          // Add additional field that has the deleted row flag to existing data schema\n          var readingSchema = metadata.schema\n          if (readIsRowDeletedCol) {\n            readingSchema = readingSchema.add(DeltaParquetFileFormat.IS_ROW_DELETED_STRUCT_FIELD)\n          }\n          if (readRowIndexCol) {\n            readingSchema = readingSchema.add(DeltaParquetFileFormat.ROW_INDEX_STRUCT_FIELD)\n          }\n\n          // Fetch the only file in the DeltaLog snapshot\n          val addFile = deltaLog.snapshot.allFiles.collect()(0)\n\n          if (enableDVs) {\n            removeRowsFromFile(deltaLog, addFile, Seq(0, 200, 300, 756, 10352, 19999))\n          }\n\n          val addFilePath = addFile.absolutePath(deltaLog)\n          assertParquetHasMultipleRowGroups(addFilePath)\n\n          val deltaParquetFormat = new DeltaParquetFileFormat(\n            deltaLog.snapshot.protocol,\n            metadata,\n            nullableRowTrackingConstantFields = false,\n            nullableRowTrackingGeneratedFields = false,\n            optimizationsEnabled = false,\n            if (enableDVs) Some(tablePath) else None)\n\n          val fileIndex = TahoeLogFileIndex(spark, deltaLog)\n\n          val relation = HadoopFsRelation(\n            fileIndex,\n            fileIndex.partitionSchema,\n            readingSchema,\n            bucketSpec = None,\n            deltaParquetFormat,\n            options = Map.empty)(spark)\n          val plan = LogicalRelation(relation)\n\n          if (readIsRowDeletedCol) {\n            val (deletedColumnValue, notDeletedColumnValue) = (1, 0)\n            if (enableDVs) {\n              // Select some rows that are deleted and some rows not deleted\n              // Deleted row `value`: 0, 200, 300, 756, 10352, 19999\n              // Not deleted row `value`: 7, 900\n              checkDatasetUnorderly(\n                DataFrameUtils.ofRows(spark, plan)\n                  .filter(\"value in (0, 7, 200, 300, 756, 900, 10352, 19999)\")\n                  .select(\"value\", DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME)\n                  .as[(Int, Int)],\n                (0, deletedColumnValue),\n                (7, notDeletedColumnValue),\n                (200, deletedColumnValue),\n                (300, deletedColumnValue),\n                (756, deletedColumnValue),\n                (900, notDeletedColumnValue),\n                (10352, deletedColumnValue),\n                (19999, deletedColumnValue))\n            } else {\n              checkDatasetUnorderly(\n                DataFrameUtils.ofRows(spark, plan)\n                  .filter(\"value in (0, 7, 200, 300, 756, 900, 10352, 19999)\")\n                  .select(\"value\", DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME)\n                  .as[(Int, Int)],\n                (0, notDeletedColumnValue),\n                (7, notDeletedColumnValue),\n                (200, notDeletedColumnValue),\n                (300, notDeletedColumnValue),\n                (756, notDeletedColumnValue),\n                (900, notDeletedColumnValue),\n                (10352, notDeletedColumnValue),\n                (19999, notDeletedColumnValue))\n            }\n          }\n\n          if (readRowIndexCol) {\n            def rowIndexes(df: DataFrame): Set[Long] = {\n              val colIndex = if (readIsRowDeletedCol) 2 else 1\n              df.collect().map(_.getLong(colIndex)).toSet\n            }\n\n            val df = DataFrameUtils.ofRows(spark, plan)\n            assert(rowIndexes(df) === Seq.range(0, 20000).toSet)\n\n            assert(\n              rowIndexes(\n                df.filter(\"value in (0, 7, 200, 300, 756, 900, 10352, 19999)\")) ===\n                Seq(0, 7, 200, 300, 756, 900, 10352, 19999).toSet)\n          }\n        }\n      }\n    }\n  }\n}\n\nclass DeltaParquetFileFormatWithPredicatePushdownSuite extends DeltaParquetFileFormatSuiteBase {\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectorsForAllSupportedOperations(spark)\n    spark.conf.set(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key, \"true\")\n  }\n\n  for {\n    rowIndexFilterType <- Seq(RowIndexFilterType.IF_CONTAINED, RowIndexFilterType.IF_NOT_CONTAINED)\n  } testWithBothParquetReaders(\"read DV metadata columns: \" +\n      s\"with rowIndexFilterType=$rowIndexFilterType\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.toString\n\n      // Generate a table with one parquet file containing multiple row groups.\n      generateData(tablePath)\n\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      val metadata = deltaLog.update().metadata\n\n      // Add additional field that has the deleted row flag to existing data schema\n      val readingSchema = metadata.schema.add(DeltaParquetFileFormat.IS_ROW_DELETED_STRUCT_FIELD)\n\n      // Fetch the only file in the DeltaLog snapshot\n      val addFile = deltaLog.update().allFiles.collect()(0)\n      removeRowsFromFile(deltaLog, addFile, Seq(0, 200, 300, 756, 10352, 19999))\n\n      val addFilePath = addFile.absolutePath(deltaLog)\n      assertParquetHasMultipleRowGroups(addFilePath)\n\n      val deltaParquetFormat = new DeltaParquetFileFormat(\n        deltaLog.update().protocol,\n        metadata,\n        nullableRowTrackingConstantFields = false,\n        nullableRowTrackingGeneratedFields = false,\n        optimizationsEnabled = true,\n        Some(tablePath))\n\n      val fileIndex = TahoeLogFileIndex(spark, deltaLog)\n\n      val relation = HadoopFsRelation(\n        fileIndex,\n        fileIndex.partitionSchema,\n        readingSchema,\n        bucketSpec = None,\n        deltaParquetFormat,\n        options = Map.empty)(spark)\n\n      val plan = LogicalRelation(relation)\n      val planWithMetadataCol =\n        plan.copy(output = plan.output :+ deltaParquetFormat.createFileMetadataCol())\n      val (deletedColumnValue, notDeletedColumnValue) = (1, 0)\n\n      // Select some rows that are deleted and some rows not deleted\n      // Deleted row `value`: 0, 200, 300, 756, 10352, 19999\n      // Not deleted row `value`: 7, 900\n      checkDatasetUnorderly(\n        DataFrameUtils.ofRows(spark, planWithMetadataCol)\n          .filter(\"value in (0, 7, 200, 300, 756, 900, 10352, 19999)\")\n          .select(\"value\", DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME)\n          .as[(Int, Int)],\n        (0, deletedColumnValue),\n        (7, notDeletedColumnValue),\n        (200, deletedColumnValue),\n        (300, deletedColumnValue),\n        (756, deletedColumnValue),\n        (900, notDeletedColumnValue),\n        (10352, deletedColumnValue),\n        (19999, deletedColumnValue))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaProtocolTransitionsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.AlterTableDropFeatureDeltaCommand\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait DeltaProtocolTransitionsBaseSuite\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"false\")\n  }\n\n  protected def testProtocolTransition(\n      createTableColumns: Seq[(String, String)] = Seq.empty,\n      createTableGeneratedColumns: Seq[(String, String, String)] = Seq.empty,\n      createTableProperties: Seq[(String, String)] = Seq.empty,\n      alterTableProperties: Seq[(String, String)] = Seq.empty,\n      dropFeatures: Seq[TableFeature] = Seq.empty,\n      expectedProtocol: Protocol): Unit = {\n\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      val tableBuilder = io.delta.tables.DeltaTable.create(spark)\n      tableBuilder.tableName(s\"delta.`$dir`\")\n\n      createTableColumns.foreach { c =>\n        tableBuilder.addColumn(c._1, c._2)\n      }\n\n      createTableGeneratedColumns.foreach { c =>\n        val columnBuilder = io.delta.tables.DeltaTable.columnBuilder(spark, c._1)\n        columnBuilder.dataType(c._2)\n        columnBuilder.generatedAlwaysAs(c._3)\n        tableBuilder.addColumn(columnBuilder.build())\n      }\n\n      createTableProperties.foreach { p =>\n        tableBuilder.property(p._1, p._2)\n      }\n\n      tableBuilder.location(dir.getCanonicalPath)\n      tableBuilder.execute()\n\n      if (alterTableProperties.nonEmpty) {\n        sql(\n          s\"\"\"ALTER TABLE delta.`${deltaLog.dataPath}`\n             |SET TBLPROPERTIES (\n             |${alterTableProperties.map(p => s\"'${p._1}' = '${p._2}'\").mkString(\",\")}\n             |)\"\"\".stripMargin)\n      }\n\n      // Drop features.\n      dropFeatures.foreach { f =>\n        sql(s\"ALTER TABLE delta.`${deltaLog.dataPath}` DROP FEATURE ${f.name}\")\n      }\n\n      assert(deltaLog.update().protocol === expectedProtocol)\n    }\n  }\n}\n\nclass DeltaProtocolTransitionsSuite extends DeltaProtocolTransitionsBaseSuite {\n\n  test(\"CREATE TABLE default protocol versions\") {\n    testProtocolTransition(\n      expectedProtocol = Protocol(1, 2))\n\n    // Setting table versions overrides protocol versions.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 1.toString)),\n      expectedProtocol = Protocol(1, 1))\n  }\n\n  test(\"CREATE TABLE normalization\") {\n    // Table features protocols without features are normalized to (1, 1).\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 3.toString),\n        (\"delta.minWriterVersion\", 7.toString)),\n      expectedProtocol = Protocol(1, 1))\n\n    // Default protocol is taken into account.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        InvariantsTableFeature,\n        AppendOnlyTableFeature,\n        TestRemovableWriterFeature)))\n\n    // Default protocol is not taken into account because we explicitly set the protocol versions.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 3.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(1, 7).withFeature(TestRemovableWriterFeature))\n\n    // Reader version normalizes correctly.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\"),\n        (s\"delta.feature.${ColumnMappingTableFeature.name}\", \"supported\")),\n      expectedProtocol =\n        Protocol(2, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          TestRemovableWriterFeature,\n          ColumnMappingTableFeature)))\n\n    // Reader version denormalizes correctly.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (s\"delta.feature.${TestRemovableReaderWriterFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(3, 7).withFeature(TestRemovableReaderWriterFeature))\n\n    // Reader version denormalizes correctly.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 2.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (s\"delta.feature.${TestRemovableReaderWriterFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(3, 7).withFeature(TestRemovableReaderWriterFeature))\n  }\n\n  test(\"Setting partial versions\") {\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minWriterVersion\", 3.toString)),\n      expectedProtocol = Protocol(1, 3))\n\n    testProtocolTransition(\n      alterTableProperties = Seq(\n        (\"delta.minWriterVersion\", 3.toString)),\n      expectedProtocol = Protocol(1, 3))\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minWriterVersion\", 3.toString),\n        (s\"delta.feature.${DeletionVectorsTableFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(3, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        CheckConstraintsTableFeature,\n        DeletionVectorsTableFeature)))\n\n    testProtocolTransition(\n      alterTableProperties = Seq(\n        (\"delta.minWriterVersion\", 3.toString),\n        (s\"delta.feature.${DeletionVectorsTableFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(3, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        CheckConstraintsTableFeature,\n        DeletionVectorsTableFeature)))\n  }\n\n  for ((readerVersion, writerVersion) <- Seq((2, 1), (2, 2), (2, 3), (2, 4), (1, 5)))\n  test(\"Invalid legacy protocol normalization\" +\n    s\" - invalidProtocol($readerVersion, $writerVersion)\") {\n\n    val expectedReaderVersion = 1\n    val expectedWriterVersion = Math.min(writerVersion, 4)\n\n    withSQLConf(DeltaSQLConf.TABLE_FEATURES_TEST_FEATURES_ENABLED.key -> false.toString) {\n      // Base case.\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", readerVersion.toString),\n          (\"delta.minWriterVersion\", writerVersion.toString)),\n        expectedProtocol = Protocol(expectedReaderVersion, expectedWriterVersion))\n\n      // Invalid legacy versions are normalized in default confs.\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> readerVersion.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> writerVersion.toString) {\n        testProtocolTransition(\n          expectedProtocol = Protocol(expectedReaderVersion, expectedWriterVersion))\n      }\n\n      // Invalid legacy versions are normalized in alter table.\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 1.toString)),\n        alterTableProperties = Seq(\n          (\"delta.minReaderVersion\", readerVersion.toString),\n          (\"delta.minWriterVersion\", writerVersion.toString)),\n        expectedProtocol = Protocol(expectedReaderVersion, expectedWriterVersion))\n    }\n  }\n\n  test(\"ADD FEATURE normalization\") {\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 1.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString)),\n      expectedProtocol = Protocol(1, 4))\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 2.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString)),\n      expectedProtocol = Protocol(1, 4))\n\n    // Setting lower legacy versions is noop.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 2.toString)),\n      expectedProtocol = Protocol(1, 4))\n\n    // Setting the same legacy versions is noop.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString)),\n      expectedProtocol = Protocol(1, 4))\n\n    // Setting legacy versions is an ADD operation.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 6.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 2.toString),\n        (\"delta.minWriterVersion\", 5.toString)),\n      expectedProtocol = Protocol(2, 6))\n\n    // The inverse of the above test.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 2.toString),\n        (\"delta.minWriterVersion\", 5.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 6.toString)),\n      expectedProtocol = Protocol(2, 6))\n\n    // Adding a legacy protocol to a table features protocol adds the features\n    // of the former to the later.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (s\"delta.feature.${TestWriterFeature.name}\", \"supported\")),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 3.toString)),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        CheckConstraintsTableFeature,\n        InvariantsTableFeature,\n        TestWriterFeature)))\n\n    // Variation of the above.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (s\"delta.feature.${TestWriterFeature.name}\", \"supported\"),\n        (s\"delta.feature.${IdentityColumnsTableFeature.name}\", \"supported\")),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 3.toString)),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        CheckConstraintsTableFeature,\n        InvariantsTableFeature,\n        IdentityColumnsTableFeature,\n        TestWriterFeature)))\n\n    // New feature is added to the table protocol features.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 3.toString)),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        CheckConstraintsTableFeature,\n        ChangeDataFeedTableFeature)))\n\n    // Addition result is normalized.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (s\"delta.feature.${InvariantsTableFeature.name}\", \"supported\")),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 2.toString)),\n      expectedProtocol = Protocol(1, 2))\n\n    // Variation of the above.\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (s\"delta.feature.${CheckConstraintsTableFeature.name}\", \"supported\")),\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 2.toString)),\n      expectedProtocol = Protocol(1, 3))\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 2.toString)),\n      alterTableProperties = Seq(\n        (s\"delta.feature.${CheckConstraintsTableFeature.name}\", \"supported\")),\n      expectedProtocol = Protocol(1, 3))\n\n    withSQLConf(DeltaSQLConf.TABLE_FEATURES_TEST_FEATURES_ENABLED.key -> false.toString) {\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 4.toString)),\n        alterTableProperties = Seq(\n          (s\"delta.feature.${ColumnMappingTableFeature.name}\", \"supported\")),\n        expectedProtocol = Protocol(2, 5))\n\n\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 4.toString)),\n        alterTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 7.toString),\n          (DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n        expectedProtocol = Protocol(2, 5))\n    }\n  }\n\n  for (withFastDropFeature <- BOOLEAN_DOMAIN)\n  test(s\"DROP FEATURE normalization. withFastDropFeature: $withFastDropFeature\") {\n    withSQLConf(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key -> withFastDropFeature.toString) {\n      // Can drop features on legacy protocols and the result is normalized.\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 3.toString)),\n        dropFeatures = Seq(CheckConstraintsTableFeature),\n        expectedProtocol = Protocol(1, 2))\n\n      // If the removal result does not match a legacy version use the denormalized form.\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 4.toString)),\n        dropFeatures = Seq(CheckConstraintsTableFeature),\n        expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          GeneratedColumnsTableFeature,\n          ChangeDataFeedTableFeature)))\n\n      // Normalization after dropping a table feature.\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\")),\n        dropFeatures = Seq(TestRemovableWriterFeature),\n        expectedProtocol = Protocol(1, 2))\n\n      // Variation of the above. Because the default protocol is overwritten the result\n      // is normalized to (1, 1).\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 7.toString),\n          (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\")),\n        dropFeatures = Seq(TestRemovableWriterFeature),\n        expectedProtocol = Protocol(1, 1))\n\n      // Reader version is normalized correctly to 2 after dropping the reader feature.\n      val checkpointProtectionTableFeatureOpt =\n        if (withFastDropFeature) Some(CheckpointProtectionTableFeature) else None\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (s\"delta.feature.${ColumnMappingTableFeature.name}\", \"supported\"),\n          (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\"),\n          (s\"delta.feature.${TestRemovableReaderWriterFeature.name}\", \"supported\")),\n        dropFeatures = Seq(TestRemovableReaderWriterFeature),\n        expectedProtocol = Protocol(2, 7).withFeatures(Seq(\n          InvariantsTableFeature,\n          AppendOnlyTableFeature,\n          ColumnMappingTableFeature,\n          TestRemovableWriterFeature) ++ checkpointProtectionTableFeatureOpt))\n\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (s\"delta.feature.${TestRemovableWriterFeature.name}\", \"supported\"),\n          (s\"delta.feature.${TestRemovableReaderWriterFeature.name}\", \"supported\")),\n        dropFeatures = Seq(TestRemovableReaderWriterFeature),\n        expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n          InvariantsTableFeature,\n          AppendOnlyTableFeature,\n          TestRemovableWriterFeature) ++ checkpointProtectionTableFeatureOpt))\n\n      val expectedProtocol = if (withFastDropFeature) {\n        Protocol(1, 7).withFeatures(Seq(\n          InvariantsTableFeature,\n          AppendOnlyTableFeature,\n          CheckConstraintsTableFeature,\n          ChangeDataFeedTableFeature,\n          GeneratedColumnsTableFeature,\n          CheckpointProtectionTableFeature))\n      } else {\n        Protocol(1, 4)\n      }\n      withSQLConf(DeltaSQLConf.TABLE_FEATURES_TEST_FEATURES_ENABLED.key -> false.toString) {\n        testProtocolTransition(\n          createTableProperties = Seq(\n            (\"delta.minReaderVersion\", 2.toString),\n            (\"delta.minWriterVersion\", 5.toString)),\n          dropFeatures = Seq(ColumnMappingTableFeature),\n          expectedProtocol = expectedProtocol)\n      }\n    }\n  }\n\n  test(\"Default Enabled native features\") {\n    withSQLConf(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\") {\n      // Table protocol is taken into account when default table features exist.\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 4.toString)),\n        expectedProtocol = Protocol(3, 7).withFeatures(Seq(\n          DeletionVectorsTableFeature,\n          InvariantsTableFeature,\n          AppendOnlyTableFeature,\n          CheckConstraintsTableFeature,\n          ChangeDataFeedTableFeature,\n          GeneratedColumnsTableFeature)))\n\n      // Default protocol versions are taken into account when default features exist.\n      testProtocolTransition(\n        expectedProtocol = Protocol(3, 7).withFeatures(Seq(\n          DeletionVectorsTableFeature,\n          InvariantsTableFeature,\n          AppendOnlyTableFeature)))\n    }\n\n    withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> 1.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> 7.toString,\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\") {\n      testProtocolTransition(\n        expectedProtocol = Protocol(3, 7).withFeature(DeletionVectorsTableFeature))\n    }\n  }\n\n  test(\"Default Enabled legacy features\") {\n    testProtocolTransition(\n      createTableProperties = Seq((DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        ChangeDataFeedTableFeature)))\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 3.toString),\n        (DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        CheckConstraintsTableFeature,\n        ChangeDataFeedTableFeature)))\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString),\n        (DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 4))\n\n    testProtocolTransition(\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 4.toString),\n        (DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 4))\n\n    withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n      testProtocolTransition(\n        expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          ChangeDataFeedTableFeature)))\n    }\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 7).withFeature(ChangeDataFeedTableFeature))\n\n    withSQLConf(\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> 1.toString,\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> 7.toString,\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n      testProtocolTransition(\n        expectedProtocol = Protocol(1, 7).withFeature(ChangeDataFeedTableFeature))\n    }\n\n    withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> 1.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> 7.toString) {\n      testProtocolTransition(\n        createTableProperties = Seq((DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n        expectedProtocol = Protocol(1, 7).withFeature(ChangeDataFeedTableFeature))\n    }\n\n    withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 7.toString)),\n        expectedProtocol = Protocol(1, 7).withFeature(ChangeDataFeedTableFeature))\n    }\n  }\n\n  test(\"Enabling legacy features on a table\") {\n    testProtocolTransition(\n      createTableColumns = Seq((\"id\", \"INT\")),\n      createTableGeneratedColumns = Seq((\"id2\", \"INT\", \"id + 1\")),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        GeneratedColumnsTableFeature)))\n\n    testProtocolTransition(\n      createTableColumns = Seq((\"id\", \"INT\")),\n      createTableGeneratedColumns = Seq((\"id2\", \"INT\", \"id + 1\")),\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString)),\n      expectedProtocol = Protocol(1, 7).withFeature(GeneratedColumnsTableFeature))\n\n    withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> 1.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> 7.toString) {\n      testProtocolTransition(\n        createTableColumns = Seq((\"id\", \"INT\")),\n        createTableGeneratedColumns = Seq((\"id2\", \"INT\", \"id + 1\")),\n        expectedProtocol = Protocol(1, 7).withFeature(GeneratedColumnsTableFeature))\n    }\n\n    testProtocolTransition(\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (DeltaConfigs.CHANGE_DATA_FEED.key, true.toString)),\n      expectedProtocol = Protocol(1, 7).withFeatures(Seq(\n        InvariantsTableFeature,\n        AppendOnlyTableFeature,\n        ChangeDataFeedTableFeature)))\n  }\n\n  test(\"Column Mapping does not require a manual protocol versions upgrade\") {\n    testProtocolTransition(\n      createTableProperties = Seq((DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n      expectedProtocol = Protocol(2, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        ColumnMappingTableFeature)))\n\n    withSQLConf(DeltaSQLConf.TABLE_FEATURES_TEST_FEATURES_ENABLED.key -> false.toString) {\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 4.toString),\n          (DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n        expectedProtocol = Protocol(2, 5))\n\n      testProtocolTransition(\n        createTableProperties = Seq(\n          (\"delta.minReaderVersion\", 1.toString),\n          (\"delta.minWriterVersion\", 4.toString)),\n        alterTableProperties = Seq(\n          (DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n        expectedProtocol = Protocol(2, 5))\n    }\n\n    testProtocolTransition(\n      createTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n      expectedProtocol = Protocol(2, 7).withFeature(ColumnMappingTableFeature))\n\n    withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> 1.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> 7.toString) {\n      testProtocolTransition(\n        createTableProperties = Seq((DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n        expectedProtocol = Protocol(2, 7).withFeature(ColumnMappingTableFeature))\n    }\n\n    testProtocolTransition(\n      alterTableProperties = Seq((DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n      expectedProtocol = Protocol(2, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        ColumnMappingTableFeature)))\n\n    testProtocolTransition(\n      alterTableProperties = Seq(\n        (\"delta.minReaderVersion\", 1.toString),\n        (\"delta.minWriterVersion\", 7.toString),\n        (DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")),\n      expectedProtocol = Protocol(2, 7).withFeatures(Seq(\n        InvariantsTableFeature,\n        AppendOnlyTableFeature,\n        ColumnMappingTableFeature)))\n  }\n\n  private def validVersions = Seq((1, 1), (1, 2), (1, 3), (1, 4), (2, 5), (1, 7), (3, 7))\n  private def invalidVersions = Seq((2, 2), (2, 3))\n  for ((readerVersion, writerVersion) <- validVersions ++ invalidVersions)\n    test(\"Legacy features are added when setting legacy versions: \" +\n      s\"readerVersionToSet = $readerVersion, writerVersionToSet = $writerVersion\") {\n      withTempDir { dir =>\n        val deltaLog = DeltaLog.forTable(spark, dir)\n\n        // Creates a table with (1, 7) versions with the given table feature.\n        sql(\n          s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n             |TBLPROPERTIES (\n             |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n             |)\"\"\".stripMargin)\n\n        sql(\n          s\"\"\"\n             |ALTER TABLE delta.`${deltaLog.dataPath}` SET TBLPROPERTIES (\n             |  'delta.minReaderVersion' = $readerVersion,\n             |  'delta.minWriterVersion' = $writerVersion\n             |)\"\"\".stripMargin)\n\n        val expected = Protocol(readerVersion, writerVersion).implicitlySupportedFeatures ++\n          Set(InvariantsTableFeature, AppendOnlyTableFeature, TestRemovableWriterFeature)\n        assert(deltaLog.update().protocol.readerAndWriterFeatureNames === expected.map(_.name))\n      }\n    }\n\n  for {\n    tableFeatureToAdd <- Seq(TestRemovableWriterFeature, TestRemovableReaderWriterFeature)\n    downgradeVersionToSet <- Seq(1, 2, 3, 4, 5, 6)\n    preemptiveVersionDowngrade <- BOOLEAN_DOMAIN\n  } test(\"Protocol versions are always downgraded to the minimum required \" +\n      s\"tableFeatureToAdd: ${tableFeatureToAdd.name}, \" +\n      s\"downgradeVersionToSet: $downgradeVersionToSet, \" +\n      s\"preemptiveVersionDowngrade: $preemptiveVersionDowngrade\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      sql(\n        s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.minReaderVersion = ${Math.max(tableFeatureToAdd.minReaderVersion, 1)},\n           |delta.minWriterVersion = $TABLE_FEATURES_MIN_WRITER_VERSION,\n           |delta.feature.${tableFeatureToAdd.name} = 'supported',\n           |delta.feature.${ChangeDataFeedTableFeature.name} = 'supported'\n           |)\"\"\".stripMargin)\n\n      val downgradeProtocolVersionsSQL =\n        s\"\"\"\n           |ALTER TABLE delta.`${deltaLog.dataPath}` SET TBLPROPERTIES (\n           |  'delta.minReaderVersion' = 1,\n           |  'delta.minWriterVersion' = $downgradeVersionToSet\n           |)\"\"\".stripMargin\n\n      if (preemptiveVersionDowngrade) sql(downgradeProtocolVersionsSQL)\n\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        tableFeatureToAdd.name,\n        truncateHistory = tableFeatureToAdd.isReaderWriterFeature).run(spark)\n\n      if (!preemptiveVersionDowngrade) sql(downgradeProtocolVersionsSQL)\n\n      val expectedProtocol = if (downgradeVersionToSet < 4) {\n        Protocol(tableFeatureToAdd.minReaderVersion, 7).withFeature(ChangeDataFeedTableFeature)\n          .merge(Protocol(1, downgradeVersionToSet))\n      } else {\n        Protocol(1, downgradeVersionToSet)\n      }\n      assert(deltaLog.update().protocol === expectedProtocol)\n    }\n  }\n\n  for {\n    tableFeatureToAdd <- Seq(TestRemovableWriterFeature, TestRemovableReaderWriterFeature)\n    setLegacyVersions <- BOOLEAN_DOMAIN\n    downgradeAfterDrop <- if (setLegacyVersions) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"SOP for downgrading to legacy protocol versions for tables created with features. \" +\n      s\"tableFeatureToAdd: ${tableFeatureToAdd.name}, setLegacyVersions: $setLegacyVersions, \" +\n      s\"downgradeAfterDrop: ${downgradeAfterDrop}\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      sql(\n        s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.minReaderVersion = $TABLE_FEATURES_MIN_READER_VERSION,\n           |delta.minWriterVersion = $TABLE_FEATURES_MIN_WRITER_VERSION,\n           |delta.feature.${tableFeatureToAdd.name} = 'supported',\n           |delta.feature.${ChangeDataFeedTableFeature.name} = 'supported'\n           |)\"\"\".stripMargin)\n\n      val downgradeProtocolVersionsSQL =\n        s\"\"\"\n           |ALTER TABLE delta.`${deltaLog.dataPath}` SET TBLPROPERTIES (\n           |  'delta.minReaderVersion' = 1,\n           |  'delta.minWriterVersion' = 4\n           |)\"\"\".stripMargin\n\n      if (setLegacyVersions && !downgradeAfterDrop) sql(downgradeProtocolVersionsSQL)\n\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        tableFeatureToAdd.name,\n        truncateHistory = tableFeatureToAdd.isReaderWriterFeature).run(spark)\n\n      if (setLegacyVersions && downgradeAfterDrop) sql(downgradeProtocolVersionsSQL)\n\n      val expectedProtocol = if (setLegacyVersions) {\n        Protocol(1, 4)\n      } else {\n        Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(ChangeDataFeedTableFeature)\n      }\n      assert(deltaLog.update().protocol === expectedProtocol)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaProtocolVersionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.nio.file.{Files, Paths, StandardOpenOption}\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.JavaConverters._\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions, UsageRecord}\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.{AlterTableDropFeatureDeltaCommand, AlterTableSetPropertiesDeltaCommand, AlterTableUnsetPropertiesDeltaCommand}\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.coordinatedcommits._\nimport org.apache.spark.sql.delta.redirect.{PathBasedRedirectSpec, RedirectReaderWriter, RedirectWriterOnly, TableRedirect}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.delta.util.FileNames.{unsafeDeltaFile, DeltaFile}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.TableDescriptor\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, QueryTest, SaveMode}\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.unsafe.types.CalendarInterval\nimport org.apache.spark.util.ManualClock\n\ntrait DeltaProtocolVersionSuiteBase extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeletionVectorsTestUtils {\n\n  // `.schema` generates NOT NULL columns which requires writer protocol 2. We convert all to\n  // NULLable to avoid silent writer protocol version bump.\n  private lazy val testTableSchema = spark.range(1).schema.asNullable\n  override protected def sparkConf: SparkConf = {\n    // All the drop feature tests below are targeting the drop feature with history truncation\n    // implementation. The fast drop feature implementation is tested extensively in\n    // DeltaFastDropFeatureSuite.\n    super.sparkConf.set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, \"false\")\n  }\n\n  // This is solely a test hook. Users cannot create new Delta tables with protocol lower than\n  // that of their current version.\n  protected def createTableWithProtocol(\n      protocol: Protocol,\n      path: File,\n      schema: StructType = testTableSchema): DeltaLog = {\n    val log = DeltaLog.forTable(spark, path)\n    log.createLogDirectoriesIfNotExists()\n    log.store.write(\n      unsafeDeltaFile(log.logPath, 0),\n      Iterator(Metadata(schemaString = schema.json).json, protocol.json),\n      overwrite = false,\n      log.newDeltaHadoopConf())\n    log.update()\n    log\n  }\n\n  test(\"protocol for empty folder\") {\n    def testEmptyFolder(\n        readerVersion: Int,\n        writerVersion: Int,\n        features: Iterable[TableFeature] = Seq.empty,\n        sqlConfs: Iterable[(String, String)] = Seq.empty,\n        expectedProtocol: Protocol): Unit = {\n      withTempDir { path =>\n        val configs = Seq(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> readerVersion.toString,\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> writerVersion.toString) ++\n          features.map(defaultPropertyKey(_) -> FEATURE_PROP_ENABLED) ++\n          sqlConfs\n        withSQLConf(configs: _*) {\n          val log = DeltaLog.forTable(spark, path)\n          assert(log.update().protocol === expectedProtocol)\n        }\n      }\n    }\n\n    testEmptyFolder(1, 1, expectedProtocol = Protocol(1, 1))\n    testEmptyFolder(1, 2, expectedProtocol = Protocol(1, 2))\n    testEmptyFolder(\n      readerVersion = 1,\n      writerVersion = 1,\n      sqlConfs = Seq((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")),\n      expectedProtocol = Protocol(1, 7).withFeature(ChangeDataFeedTableFeature))\n    testEmptyFolder(\n      readerVersion = 1,\n      writerVersion = 1,\n      features = Seq(TestLegacyReaderWriterFeature),\n      expectedProtocol = Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestLegacyReaderWriterFeature))\n    testEmptyFolder(\n      readerVersion = 1,\n      writerVersion = 1,\n      features = Seq(TestWriterFeature),\n      expectedProtocol = Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestWriterFeature))\n    testEmptyFolder(\n      readerVersion = TABLE_FEATURES_MIN_READER_VERSION,\n      writerVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n      features = Seq(TestLegacyReaderWriterFeature),\n      expectedProtocol =\n        Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeature(TestLegacyReaderWriterFeature))\n    testEmptyFolder(\n      readerVersion = 1,\n      writerVersion = 1,\n      features = Seq(TestWriterFeature),\n      sqlConfs = Seq((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")),\n      expectedProtocol = Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestWriterFeature, ChangeDataFeedTableFeature)))\n    testEmptyFolder(\n      readerVersion = 1,\n      writerVersion = 1,\n      features = Seq(TestLegacyReaderWriterFeature),\n      sqlConfs = Seq((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")),\n      expectedProtocol = Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestLegacyReaderWriterFeature, ChangeDataFeedTableFeature)))\n    testEmptyFolder(\n      readerVersion = 1,\n      writerVersion = 1,\n      features = Seq(TestWriterFeature, TestLegacyReaderWriterFeature),\n      expectedProtocol = Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestWriterFeature, TestLegacyReaderWriterFeature)))\n  }\n\n  test(\"upgrade to current version\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 1), path)\n      assert(log.snapshot.protocol === Protocol(1, 1))\n      log.upgradeProtocol(Action.supportedProtocolVersion(\n        featuresToExclude = Seq(CatalogOwnedTableFeature)))\n      assert(log.snapshot.protocol === Action.supportedProtocolVersion(\n        featuresToExclude = Seq(CatalogOwnedTableFeature)))\n    }\n  }\n\n  test(\"upgrade to a version with DeltaTable API\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(0, 0), path)\n      assert(log.snapshot.protocol === Protocol(0, 0))\n      val table = io.delta.tables.DeltaTable.forPath(spark, path.getCanonicalPath)\n      table.upgradeTableProtocol(1, 1)\n      assert(log.snapshot.protocol === Protocol(1, 1))\n      table.upgradeTableProtocol(1, 2)\n      assert(log.snapshot.protocol === Protocol(1, 2))\n      table.upgradeTableProtocol(1, 3)\n      assert(log.snapshot.protocol === Protocol(1, 3))\n      intercept[DeltaTableFeatureException] {\n        table.upgradeTableProtocol(\n          TABLE_FEATURES_MIN_READER_VERSION,\n          writerVersion = 1)\n      }\n      intercept[IllegalArgumentException] {\n        table.upgradeTableProtocol(\n          TABLE_FEATURES_MIN_READER_VERSION + 1,\n          TABLE_FEATURES_MIN_WRITER_VERSION)\n      }\n      intercept[IllegalArgumentException] {\n        table.upgradeTableProtocol(\n          TABLE_FEATURES_MIN_READER_VERSION,\n          TABLE_FEATURES_MIN_WRITER_VERSION + 1)\n      }\n    }\n  }\n\n  test(\"upgrade to support table features - no feature\") {\n    // Setting a table feature versions to a protocol without table features is a noop.\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 1), path)\n      assert(log.update().protocol === Protocol(1, 1))\n      val table = io.delta.tables.DeltaTable.forPath(spark, path.getCanonicalPath)\n      table.upgradeTableProtocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(log.update().protocol === Protocol(1, 1))\n      table.upgradeTableProtocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(log.update().protocol === Protocol(1, 1))\n    }\n  }\n\n  test(\"upgrade to support table features - writer-only feature\") {\n    // Setting table feature versions to a protocol without table features is a noop.\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 2), path)\n      assert(log.update().protocol === Protocol(1, 2))\n      val table = io.delta.tables.DeltaTable.forPath(spark, path.getCanonicalPath)\n      table.upgradeTableProtocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(log.update().protocol === Protocol(1, 2))\n      table.upgradeTableProtocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(log.update().protocol === Protocol(1, 2))\n    }\n  }\n\n  test(\"upgrade to support table features - many features\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(2, 5), path)\n      assert(log.update().protocol === Protocol(2, 5))\n      val table = io.delta.tables.DeltaTable.forPath(spark, path.getCanonicalPath)\n      table.upgradeTableProtocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n      // Setting table feature versions to a protocol without table features is a noop.\n      assert(log.update().protocol === Protocol(2, 5))\n      spark.sql(\n        s\"ALTER TABLE delta.`${path.getPath}` SET TBLPROPERTIES (\" +\n          s\"  delta.feature.${TestWriterFeature.name}='enabled'\" +\n          s\")\")\n      table.upgradeTableProtocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(\n        log.snapshot.protocol === Protocol(\n          minReaderVersion = 2,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = None,\n          writerFeatures = Some(\n            Set(\n              AppendOnlyTableFeature,\n              ChangeDataFeedTableFeature,\n              CheckConstraintsTableFeature,\n              ColumnMappingTableFeature,\n              GeneratedColumnsTableFeature,\n              InvariantsTableFeature,\n              TestLegacyWriterFeature,\n              TestRemovableLegacyWriterFeature,\n              TestLegacyReaderWriterFeature,\n              TestRemovableLegacyReaderWriterFeature,\n              TestWriterFeature)\n              .map(_.name))))\n    }\n  }\n\n  test(\"protocol upgrade using SQL API\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 2), path)\n\n      assert(log.update().protocol === Protocol(1, 2))\n      sql(\n        s\"ALTER TABLE delta.`${path.getCanonicalPath}` \" +\n          \"SET TBLPROPERTIES (delta.minWriterVersion = 3)\")\n      assert(log.update().protocol === Protocol(1, 3))\n      assertPropertiesAndShowTblProperties(log)\n      sql(s\"ALTER TABLE delta.`${path.getCanonicalPath}` \" +\n        s\"SET TBLPROPERTIES (delta.minWriterVersion=$TABLE_FEATURES_MIN_WRITER_VERSION)\")\n      assert(log.update().protocol === Protocol(1, 3))\n      assertPropertiesAndShowTblProperties(log, tableHasFeatures = false)\n      sql(s\"\"\"ALTER TABLE delta.`${path.getCanonicalPath}` SET TBLPROPERTIES (\n             |delta.minReaderVersion=$TABLE_FEATURES_MIN_READER_VERSION,\n             |delta.minWriterVersion=$TABLE_FEATURES_MIN_WRITER_VERSION\n             |)\"\"\".stripMargin)\n      assert(log.update().protocol === Protocol(1, 3))\n      assertPropertiesAndShowTblProperties(log, tableHasFeatures = false)\n    }\n  }\n\n  test(\"overwrite keeps the same protocol version\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(0, 0), path)\n      spark.range(1)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(path.getCanonicalPath)\n      log.update()\n      assert(log.snapshot.protocol === Protocol(0, 0))\n    }\n  }\n\n  test(\"overwrite keeps the same table properties\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(0, 0), path)\n      spark.sql(\n        s\"ALTER TABLE delta.`${path.getCanonicalPath}` SET TBLPROPERTIES ('myProp'='true')\")\n      spark\n        .range(1)\n        .write\n        .format(\"delta\")\n        .option(\"anotherProp\", \"true\")\n        .mode(\"overwrite\")\n        .save(path.getCanonicalPath)\n      log.update()\n      assert(log.snapshot.metadata.configuration.size === 1)\n      assert(log.snapshot.metadata.configuration(\"myProp\") === \"true\")\n    }\n  }\n\n  test(\"overwrite keeps the same protocol version and features\") {\n    withTempDir { path =>\n      val protocol = Protocol(0, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(AppendOnlyTableFeature)\n      val log = createTableWithProtocol(protocol, path)\n      spark\n        .range(1)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(path.getCanonicalPath)\n      log.update()\n      assert(log.snapshot.protocol === protocol)\n    }\n  }\n\n  test(\"overwrite with additional configs keeps the same protocol version and features\") {\n    withTempDir { path =>\n      val protocol = Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(AppendOnlyTableFeature)\n      val log = createTableWithProtocol(protocol, path)\n      spark\n        .range(1)\n        .write\n        .format(\"delta\")\n        .option(\"delta.feature.testWriter\", \"enabled\")\n        .option(\"delta.feature.testReaderWriter\", \"enabled\")\n        .mode(\"overwrite\")\n        .save(path.getCanonicalPath)\n      log.update()\n      assert(log.snapshot.protocol === protocol)\n    }\n  }\n\n  test(\"overwrite with additional session defaults keeps the same protocol version and features\") {\n    withTempDir { path =>\n      val protocol = Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(AppendOnlyTableFeature)\n      val log = createTableWithProtocol(protocol, path)\n      withSQLConf(\n        s\"$DEFAULT_FEATURE_PROP_PREFIX${TestLegacyWriterFeature.name}\" -> \"enabled\") {\n        spark\n          .range(1)\n          .write\n          .format(\"delta\")\n          .option(\"delta.feature.testWriter\", \"enabled\")\n          .option(\"delta.feature.testReaderWriter\", \"enabled\")\n          .mode(\"overwrite\")\n          .save(path.getCanonicalPath)\n      }\n      log.update()\n      assert(log.snapshot.protocol === protocol)\n    }\n  }\n\n  test(\"access with protocol too high\") {\n    withTempDir { path =>\n      val log = DeltaLog.forTable(spark, path)\n      log.createLogDirectoriesIfNotExists()\n      log.store.write(\n        unsafeDeltaFile(log.logPath, 0),\n        Iterator(Metadata().json, Protocol(Integer.MAX_VALUE, Integer.MAX_VALUE).json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n      intercept[InvalidProtocolVersionException] {\n        spark.range(1).write.format(\"delta\").save(path.getCanonicalPath)\n      }\n    }\n  }\n\n  test(\"Vacuum checks the write protocol\") {\n    withTempDir { path =>\n      spark.range(10).write.format(\"delta\").save(path.getCanonicalPath)\n      val log = DeltaLog.forTable(spark, path)\n\n      sql(s\"INSERT INTO delta.`${path.getCanonicalPath}` VALUES (10)\")\n      val vacuumCommandsToTry = Seq(\n        s\"vacuum delta.`${path.getCanonicalPath}` RETAIN 10000 HOURS\",\n        s\"vacuum delta.`${path.getCanonicalPath}` RETAIN 10000 HOURS DRY RUN\"\n      )\n      // Both vacuum and vacuum dry run works as expected\n      vacuumCommandsToTry.foreach(spark.sql(_).collect())\n\n      val snapshot = log.update()\n      val newProtocol = Protocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION).withWriterFeatures(Seq(\"newUnsupportedWriterFeature\"))\n      log.store.write(\n        unsafeDeltaFile(log.logPath, snapshot.version + 1),\n        Iterator(Metadata().json, newProtocol.json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n\n      // Both vacuum and vacuum dry run fails as expected\n      vacuumCommandsToTry.foreach { command =>\n        intercept[DeltaUnsupportedTableFeatureException] {\n          spark.sql(command).collect()\n        }\n      }\n    }\n  }\n\n  test(\"InvalidProtocolVersionException - error message with protocol too high - table path\") {\n    withTempDir { path =>\n      spark.range(1).write.format(\"delta\").save(path.getCanonicalPath)\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, path.getCanonicalPath)\n\n      var tableReaderVersion = 4\n      var tableWriterVersion = 7\n      var version = snapshot.version + 1\n      untrackedChangeProtocolVersion(deltaLog, version, tableReaderVersion, tableWriterVersion)\n\n      val exceptionRead = intercept[InvalidProtocolVersionException] {\n        spark.read.format(\"delta\").load(path.getCanonicalPath)\n      }\n\n      validateInvalidProtocolVersionException(\n        exceptionRead,\n        deltaLog.dataPath.toString,\n        tableReaderVersion,\n        tableWriterVersion)\n\n      tableReaderVersion = 3\n      tableWriterVersion = 8\n      version = version + 1\n      untrackedChangeProtocolVersion(deltaLog, version, tableReaderVersion, tableWriterVersion)\n\n      val exceptionWrite = intercept[InvalidProtocolVersionException] {\n        spark.range(1).write\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .save(path.getCanonicalPath)\n      }\n\n      validateInvalidProtocolVersionException(\n        exceptionWrite,\n        deltaLog.dataPath.toString,\n        tableReaderVersion,\n        tableWriterVersion)\n    }\n  }\n\n  def testInvalidProtocolErrorMessageWithTableName(warm: Boolean): Unit = {\n    val protocolTableName = \"mytableprotocoltoohigh\"\n    withTable(protocolTableName) {\n      spark.range(1).write.format(\"delta\").saveAsTable(protocolTableName)\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(\n        spark,\n        TableIdentifier(protocolTableName))\n\n      var tableReaderVersion = 4\n      var tableWriterVersion = 7\n      var version = snapshot.version + 1\n      untrackedChangeProtocolVersion(deltaLog, version, tableReaderVersion, tableWriterVersion)\n      if (!warm) {\n        DeltaLog.clearCache()\n      }\n\n      val exceptionRead = intercept[InvalidProtocolVersionException] {\n        spark.read.format(\"delta\").table(protocolTableName)\n      }\n\n      var pathInErrorMessage = \"default.\" + protocolTableName\n\n      validateInvalidProtocolVersionException(\n        exceptionRead,\n        pathInErrorMessage,\n        tableReaderVersion,\n        tableWriterVersion)\n\n      tableReaderVersion = 3\n      tableWriterVersion = 8\n      version = version + 1\n      untrackedChangeProtocolVersion(deltaLog, version, tableReaderVersion, tableWriterVersion)\n      if (!warm) {\n        DeltaLog.clearCache()\n      }\n\n      val exceptionWrite = intercept[InvalidProtocolVersionException] {\n        spark.range(1).write\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .saveAsTable(protocolTableName)\n      }\n\n      validateInvalidProtocolVersionException(\n        exceptionWrite,\n        pathInErrorMessage,\n        tableReaderVersion,\n        tableWriterVersion)\n\n      // Restore the protocol version or the clean-up fails\n      version = version + 1\n      untrackedChangeProtocolVersion(deltaLog, version, 1, 2)\n    }\n  }\n\n  test(\"InvalidProtocolVersionException - error message with table name - warm\") {\n    testInvalidProtocolErrorMessageWithTableName(true)\n  }\n\n  test(\"InvalidProtocolVersionException - error message with table name - cold\") {\n    testInvalidProtocolErrorMessageWithTableName(false)\n  }\n\n  test(\"InvalidProtocolVersionException - \" +\n    \"incompatible protocol change during the transaction - table name\") {\n    for (incompatibleProtocol <- Seq(\n      Protocol(minReaderVersion = Int.MaxValue),\n      Protocol(minWriterVersion = Int.MaxValue),\n      Protocol(minReaderVersion = Int.MaxValue, minWriterVersion = Int.MaxValue)\n    )) {\n      val tableName = \"mytableprotocoltoohigh\"\n      withTable(tableName) {\n        spark.range(0).write.format(\"delta\").saveAsTable(tableName)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val catalogTable = DeltaTableV2(spark, TableIdentifier(tableName)).catalogTable\n        val txn = deltaLog.startTransaction(catalogTable)\n        val currentVersion = txn.snapshot.version\n        untrackedChangeProtocolVersion(deltaLog, currentVersion + 1, incompatibleProtocol)\n\n        // Should detect the above incompatible protocol change and fail\n        val exception = intercept[InvalidProtocolVersionException] {\n          txn.commit(AddFile(\"test\", Map.empty, 1, 1, dataChange = true) :: Nil, ManualUpdate)\n        }\n\n        var pathInErrorMessage = \"default.\" + tableName\n\n        validateInvalidProtocolVersionException(\n          exception,\n          pathInErrorMessage,\n          incompatibleProtocol.minReaderVersion,\n          incompatibleProtocol.minWriterVersion)\n      }\n    }\n  }\n\n  private def untrackedChangeProtocolVersion(\n      log: DeltaLog,\n      version: Long,\n      tableProtocolReaderVersion: Int,\n      tableProtocolWriterVersion: Int)\n    {\n      untrackedChangeProtocolVersion(\n        log,\n        version,\n        Protocol(tableProtocolReaderVersion, tableProtocolWriterVersion))\n    }\n\n  private def untrackedChangeProtocolVersion(\n      log: DeltaLog,\n      version: Long,\n      protocol: Protocol): Unit = {\n    log.store.write(\n      unsafeDeltaFile(log.logPath, version),\n      Iterator(\n        Metadata().json,\n        protocol.json),\n      overwrite = false,\n      log.newDeltaHadoopConf())\n  }\n\n  def validateInvalidProtocolVersionException(\n      exception: InvalidProtocolVersionException,\n      tableNameOrPath: String,\n      readerRequiredVersion: Int,\n      writerRequiredVersion: Int): Unit = {\n    assert(exception.getErrorClass == \"DELTA_INVALID_PROTOCOL_VERSION\")\n    assert(exception.tableNameOrPath == tableNameOrPath)\n    assert(exception.readerRequiredVersion == readerRequiredVersion)\n    assert(exception.writerRequiredVersion == writerRequiredVersion)\n  }\n\n  test(\"DeltaUnsupportedTableFeatureException - error message - table path\") {\n    withTempDir { path =>\n      spark.range(1).write.format(\"delta\").save(path.getCanonicalPath)\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, path.getCanonicalPath)\n\n      var version = snapshot.version + 1\n      val invalidReaderFeatures = Seq(\"NonExistingReaderFeature1\", \"NonExistingReaderFeature2\")\n      val protocolReaderFeatures = Protocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withReaderFeatures(invalidReaderFeatures)\n      untrackedChangeProtocolVersion(deltaLog, version, protocolReaderFeatures)\n\n      val exceptionRead = intercept[DeltaUnsupportedTableFeatureException] {\n        spark.read.format(\"delta\").load(path.getCanonicalPath)\n      }\n\n      validateUnsupportedTableReadFeatureException(\n        exceptionRead,\n        deltaLog.dataPath.toString,\n        invalidReaderFeatures)\n\n      version = version + 1\n      val invalidWriterFeatures = Seq(\"NonExistingWriterFeature1\", \"NonExistingWriterFeature2\")\n      val protocolWriterFeatures = Protocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withWriterFeatures(invalidWriterFeatures)\n      untrackedChangeProtocolVersion(deltaLog, version, protocolWriterFeatures)\n\n      val exceptionWrite = intercept[DeltaUnsupportedTableFeatureException] {\n        spark.range(1).write\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .save(path.getCanonicalPath)\n      }\n\n      validateUnsupportedTableWriteFeatureException(\n        exceptionWrite,\n        deltaLog.dataPath.toString,\n        invalidWriterFeatures)\n    }\n  }\n\n  def testTableFeatureErrorMessageWithTableName(warm: Boolean): Unit = {\n    val featureTable = \"mytablefeaturesnotsupported\"\n    withTable(featureTable) {\n      spark.range(1).write.format(\"delta\").saveAsTable(featureTable)\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(featureTable))\n\n      var version = snapshot.version + 1\n      val invalidReaderFeatures = Seq(\"NonExistingReaderFeature1\", \"NonExistingReaderFeature2\")\n      val protocolReaderFeatures = Protocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withReaderFeatures(invalidReaderFeatures)\n      untrackedChangeProtocolVersion(deltaLog, version, protocolReaderFeatures)\n      if (!warm) {\n        DeltaLog.clearCache()\n      }\n\n      val exceptionRead = intercept[DeltaUnsupportedTableFeatureException] {\n        spark.read.format(\"delta\").table(featureTable)\n      }\n      val pathInErrorMessage = \"default.\" + featureTable\n\n      validateUnsupportedTableReadFeatureException(\n        exceptionRead,\n        pathInErrorMessage,\n        invalidReaderFeatures)\n\n      version = version + 1\n      val invalidWriterFeatures = Seq(\"NonExistingWriterFeature1\", \"NonExistingWriterFeature2\")\n      val protocolWriterFeatures = Protocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withWriterFeatures(invalidWriterFeatures)\n      untrackedChangeProtocolVersion(deltaLog, version, protocolWriterFeatures)\n      if (!warm) {\n        DeltaLog.clearCache()\n      }\n\n      val exceptionWrite = intercept[DeltaUnsupportedTableFeatureException] {\n        spark.range(1).write\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .saveAsTable(featureTable)\n      }\n\n      validateUnsupportedTableWriteFeatureException(\n        exceptionWrite,\n        pathInErrorMessage,\n        invalidWriterFeatures)\n\n      // Restore the protocol version or the clean-up fails\n      version = version + 1\n      untrackedChangeProtocolVersion(deltaLog, version, 1, 2)\n    }\n  }\n\n  test(\"DeltaUnsupportedTableFeatureException - error message with table name - warm\") {\n    testTableFeatureErrorMessageWithTableName(warm = true)\n  }\n\n  test(\"DeltaUnsupportedTableFeatureException - error message with table name - cold\") {\n    testTableFeatureErrorMessageWithTableName(warm = false)\n  }\n\n  test(\"DeltaUnsupportedTableFeatureException - \" +\n    \"incompatible protocol change during the transaction - table name\") {\n    for ((incompatibleProtocol, read) <- Seq(\n        (Protocol(\n          TABLE_FEATURES_MIN_READER_VERSION,\n          TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withReaderFeatures(Seq(\"NonExistingReaderFeature1\", \"NonExistingReaderFeature2\")),\n          true),\n        (Protocol(\n          TABLE_FEATURES_MIN_READER_VERSION,\n          TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withWriterFeatures(Seq(\"NonExistingWriterFeature1\", \"NonExistingWriterFeature2\")),\n          false)\n    )) {\n      val tableName = \"mytablefeaturesnotsupported\"\n      withTable(tableName) {\n        spark.range(0).write.format(\"delta\").saveAsTable(tableName)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val catalogTable = DeltaTableV2(spark, TableIdentifier(tableName)).catalogTable\n        val txn = deltaLog.startTransaction(catalogTable)\n        val currentVersion = txn.snapshot.version\n        untrackedChangeProtocolVersion(deltaLog, currentVersion + 1, incompatibleProtocol)\n\n        // Should detect the above incompatible feature and fail\n        val exception = intercept[DeltaUnsupportedTableFeatureException] {\n          txn.commit(AddFile(\"test\", Map.empty, 1, 1, dataChange = true) :: Nil, ManualUpdate)\n        }\n\n        var pathInErrorMessage = \"default.\" + tableName\n\n        read match {\n          case true =>\n            validateUnsupportedTableReadFeatureException(\n              exception,\n              pathInErrorMessage,\n              incompatibleProtocol.readerFeatures.get)\n          case false =>\n            validateUnsupportedTableWriteFeatureException(\n              exception,\n              pathInErrorMessage,\n              incompatibleProtocol.writerFeatures.get)\n        }\n      }\n    }\n  }\n\n  def validateUnsupportedTableReadFeatureException(\n      exception: DeltaUnsupportedTableFeatureException,\n      tableNameOrPath: String,\n      unsupportedFeatures: Iterable[String]): Unit = {\n    validateUnsupportedTableFeatureException(\n      exception,\n      \"DELTA_UNSUPPORTED_FEATURES_FOR_READ\",\n      tableNameOrPath,\n      unsupportedFeatures)\n  }\n\n  def validateUnsupportedTableWriteFeatureException(\n      exception: DeltaUnsupportedTableFeatureException,\n      tableNameOrPath: String,\n      unsupportedFeatures: Iterable[String]): Unit = {\n    validateUnsupportedTableFeatureException(\n      exception,\n      \"DELTA_UNSUPPORTED_FEATURES_FOR_WRITE\",\n      tableNameOrPath,\n      unsupportedFeatures)\n  }\n\n  def validateUnsupportedTableFeatureException(\n      exception: DeltaUnsupportedTableFeatureException,\n      errorClass: String,\n      tableNameOrPath: String,\n      unsupportedFeatures: Iterable[String]): Unit = {\n    assert(exception.getErrorClass == errorClass)\n    assert(exception.tableNameOrPath == tableNameOrPath)\n    assert(exception.unsupported.toSeq.sorted == unsupportedFeatures.toSeq.sorted)\n  }\n\n  test(\"protocol downgrade is a no-op\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(2, 5), path)\n      assert(log.update().protocol === Protocol(2, 5))\n\n      { // DeltaLog API. This API is internal-only and will fail when downgrade.\n\n        val e = intercept[ProtocolDowngradeException] {\n          log.upgradeProtocol(Protocol(1, 2))\n        }\n        assert(log.update().protocol == Protocol(2, 5))\n        assert(e.getErrorClass.contains(\"DELTA_INVALID_PROTOCOL_DOWNGRADE\"))\n      }\n      { // DeltaTable API\n        val table = io.delta.tables.DeltaTable.forPath(spark, path.getCanonicalPath)\n        val events = Log4jUsageLogger.track {\n          table.upgradeTableProtocol(1, 2)\n        }\n        assert(log.update().protocol == Protocol(2, 5))\n        assert(events.count(_.tags.get(\"opType\").contains(\"delta.protocol.downgradeIgnored\")) === 1)\n      }\n      { // SQL API\n        val events = Log4jUsageLogger.track {\n          sql(s\"ALTER TABLE delta.`${path.getCanonicalPath}` \" +\n            \"SET TBLPROPERTIES (delta.minWriterVersion = 2)\")\n        }\n        assert(log.update().protocol == Protocol(2, 5))\n        assert(events.count(_.tags.get(\"opType\").contains(\"delta.protocol.downgradeIgnored\")) === 1)\n      }\n    }\n  }\n\n  private case class SessionAndTableConfs(name: String, session: Seq[String], table: Seq[String])\n\n  for (confs <- Seq(\n      SessionAndTableConfs(\n        \"session\",\n        session = Seq(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.defaultTablePropertyKey),\n        table = Seq.empty[String]),\n      SessionAndTableConfs(\n        \"table\",\n        session = Seq.empty[String],\n        table = Seq(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key))))\n    test(s\"CREATE TABLE can ignore protocol defaults, configured in ${confs.name}\") {\n      withTempDir { path =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"3\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"7\",\n          defaultPropertyKey(ChangeDataFeedTableFeature) -> FEATURE_PROP_SUPPORTED) {\n          withSQLConf(confs.session.map(_ -> \"true\"): _*) {\n            spark\n              .range(10)\n              .write\n              .format(\"delta\")\n              .options(confs.table.map(_ -> \"true\").toMap)\n              .save(path.getCanonicalPath)\n          }\n        }\n\n        val snapshot = DeltaLog.forTable(spark, path).update()\n        assert(snapshot.protocol === Protocol(1, 1))\n        assert(\n          !snapshot.metadata.configuration\n            .contains(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key))\n      }\n    }\n\n  for (ignoreProtocolDefaults <- BOOLEAN_DOMAIN)\n    for (op <- Seq(\n        \"ALTER TABLE\",\n        \"SHALLOW CLONE\",\n        \"RESTORE\")) {\n      test(s\"$op always ignore protocol defaults (flag = $ignoreProtocolDefaults)\"\n      ) {\n        withTempDir { path =>\n          val expectedProtocol = if (ignoreProtocolDefaults) {\n            Protocol(1, 1)\n          } else {\n            Protocol(\n              spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION),\n              spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION))\n          }\n\n          val cPath = path.getCanonicalPath\n          spark\n            .range(10)\n            .write\n            .format(\"delta\")\n            .option(\n              DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key,\n              ignoreProtocolDefaults.toString)\n            .save(cPath)\n          val snapshot = DeltaLog.forTable(spark, path).update()\n          assert(snapshot.protocol === expectedProtocol)\n          assert(\n            !snapshot.metadata.configuration\n              .contains(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key))\n\n          withSQLConf(\n            DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"3\",\n            DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"7\",\n            defaultPropertyKey(ChangeDataFeedTableFeature) -> FEATURE_PROP_SUPPORTED) {\n            val snapshotAfter = op match {\n              case \"ALTER TABLE\" =>\n                sql(s\"ALTER TABLE delta.`$cPath` ALTER COLUMN id COMMENT 'hallo'\")\n                DeltaLog.forTable(spark, path).update()\n              case \"SHALLOW CLONE\" =>\n                var s: Snapshot = null\n                withTempDir { cloned =>\n                  sql(\n                    s\"CREATE TABLE delta.`${cloned.getCanonicalPath}` \" +\n                      s\"SHALLOW CLONE delta.`$cPath`\")\n                  s = DeltaLog.forTable(spark, cloned).update()\n                }\n                s\n              case \"RESTORE\" =>\n                sql(s\"INSERT INTO delta.`$cPath` VALUES (99)\") // version 2\n                sql(s\"RESTORE TABLE delta.`$cPath` TO VERSION AS OF 1\")\n                DeltaLog.forTable(spark, path).update()\n              case _ =>\n                throw new RuntimeException(\"OP is invalid. Add a match!\")\n            }\n            assert(snapshotAfter.protocol === expectedProtocol)\n            assert(\n              !snapshotAfter.metadata.configuration\n                .contains(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key))\n          }\n        }\n      }\n    }\n\n  test(\"concurrent upgrade\") {\n    withTempDir { path =>\n      val newProtocol = Protocol()\n      val log = createTableWithProtocol(Protocol(0, 0), path)\n\n      // We have to copy out the internals of upgradeProtocol to induce the concurrency.\n      val txn = log.startTransaction()\n      log.upgradeProtocol(newProtocol)\n      intercept[ProtocolChangedException] {\n        txn.commit(Seq(newProtocol), DeltaOperations.UpgradeProtocol(newProtocol))\n      }\n    }\n  }\n\n  test(\"incompatible protocol change during the transaction\") {\n    for (incompatibleProtocol <- Seq(\n      Protocol(minReaderVersion = Int.MaxValue),\n      Protocol(minWriterVersion = Int.MaxValue),\n      Protocol(minReaderVersion = Int.MaxValue, minWriterVersion = Int.MaxValue)\n    )) {\n      withTempDir { path =>\n        spark.range(0).write.format(\"delta\").save(path.getCanonicalPath)\n        val deltaLog = DeltaLog.forTable(spark, path)\n        val hadoopConf = deltaLog.newDeltaHadoopConf()\n        val txn = deltaLog.startTransaction()\n        val currentVersion = txn.snapshot.version\n        deltaLog.store.write(\n          unsafeDeltaFile(deltaLog.logPath, currentVersion + 1),\n          Iterator(incompatibleProtocol.json),\n          overwrite = false,\n          hadoopConf)\n\n        // Should detect the above incompatible protocol change and fail\n        intercept[InvalidProtocolVersionException] {\n          txn.commit(AddFile(\"test\", Map.empty, 1, 1, dataChange = true) :: Nil, ManualUpdate)\n        }\n        // Make sure we didn't commit anything\n        val p = unsafeDeltaFile(deltaLog.logPath, currentVersion + 2)\n        assert(\n          !p.getFileSystem(hadoopConf).exists(p),\n          s\"$p should not be committed\")\n      }\n    }\n  }\n\n  import testImplicits._\n  /** Creates a Delta table and checks the expected protocol version */\n  private def testCreation(tableName: String, writerVersion: Int, tableInitialized: Boolean = false)\n                          (fn: String => Unit): Unit = {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        withTable(tableName) {\n          fn(dir.getCanonicalPath)\n\n          val deltaLog = DeltaLog.forTable(spark, dir)\n          assert((deltaLog.snapshot.version != 0) == tableInitialized)\n          assert(deltaLog.snapshot.protocol.minWriterVersion === writerVersion)\n          assert(deltaLog.snapshot.protocol.minReaderVersion === 1)\n        }\n      }\n    }\n  }\n\n  test(\"can create table using features configured in session\") {\n    val readerVersion = Action.supportedProtocolVersion().minReaderVersion\n    val writerVersion = Action.supportedProtocolVersion().minWriterVersion\n    withTempDir { dir =>\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> writerVersion.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> readerVersion.toString,\n        s\"$DEFAULT_FEATURE_PROP_PREFIX${AppendOnlyTableFeature.name}\" -> \"enabled\",\n        s\"$DEFAULT_FEATURE_PROP_PREFIX${TestReaderWriterFeature.name}\" -> \"enabled\") {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\")\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(\n          deltaLog.snapshot.protocol ===\n            Action\n              .supportedProtocolVersion(withAllFeatures = false)\n              .withFeatures(Set(AppendOnlyTableFeature, TestReaderWriterFeature)))\n      }\n    }\n  }\n\n  test(\"can create table using features configured in table properties and session\") {\n    withTempDir { dir =>\n      withSQLConf(\n        s\"$DEFAULT_FEATURE_PROP_PREFIX${TestWriterFeature.name}\" -> \"enabled\") {\n        sql(\n          s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n            \"TBLPROPERTIES (\" +\n            s\"  delta.feature.${AppendOnlyTableFeature.name}='enabled',\" +\n            s\"  delta.feature.${TestLegacyReaderWriterFeature.name}='enabled'\" +\n            s\")\")\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.snapshot.protocol.minReaderVersion === 2,\n          \"reader protocol version should support table features because we used the \" +\n            \"'delta.feature.' config.\")\n        assert(\n          deltaLog.snapshot.protocol.minWriterVersion ===\n            TABLE_FEATURES_MIN_WRITER_VERSION,\n          \"reader protocol version should support table features because we used the \" +\n            \"'delta.feature.' config.\")\n        assert(\n          deltaLog.snapshot.protocol.readerAndWriterFeatureNames === Set(\n            AppendOnlyTableFeature,\n            InvariantsTableFeature,\n            TestLegacyReaderWriterFeature,\n            TestWriterFeature).map(_.name))\n      }\n    }\n  }\n\n  test(\"creating a new table with default protocol\") {\n    val tableName = \"delta_test\"\n\n    def testTableCreation(fn: String => Unit, tableInitialized: Boolean = false): Unit = {\n      testCreation(tableName, 1, tableInitialized) { dir =>\n        fn(dir)\n      }\n    }\n\n    testTableCreation { dir => spark.range(10).write.format(\"delta\").save(dir) }\n    testTableCreation { dir =>\n      spark.range(10).write.format(\"delta\").option(\"path\", dir).saveAsTable(tableName)\n    }\n    testTableCreation { dir =>\n      spark.range(10).writeTo(tableName).using(\"delta\").tableProperty(\"location\", dir).create()\n    }\n    testTableCreation { dir =>\n      sql(s\"CREATE TABLE $tableName (id bigint) USING delta LOCATION '$dir'\")\n    }\n    testTableCreation { dir =>\n      sql(s\"CREATE TABLE $tableName USING delta LOCATION '$dir' AS SELECT * FROM range(10)\")\n    }\n    testTableCreation(dir => {\n      val stream = MemoryStream[Int]\n      stream.addData(1 to 10)\n      val q = stream.toDF().writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getCanonicalPath)\n        .start(dir)\n      q.processAllAvailable()\n      q.stop()\n    }\n    )\n\n    testTableCreation { dir =>\n      spark.range(10).write.mode(\"append\").parquet(dir)\n      sql(s\"CONVERT TO DELTA parquet.`$dir`\")\n    }\n  }\n\n  test(\n    \"creating a new table with default protocol - requiring more recent protocol version\") {\n    val tableName = \"delta_test\"\n    def testTableCreation(fn: String => Unit, tableInitialized: Boolean = false): Unit =\n      testCreation(tableName, 7, tableInitialized)(fn)\n\n    testTableCreation { dir =>\n      spark.range(10).writeTo(tableName).using(\"delta\")\n        .tableProperty(\"location\", dir)\n        .tableProperty(\"delta.appendOnly\", \"true\")\n        .create()\n    }\n    testTableCreation { dir =>\n      sql(s\"CREATE TABLE $tableName (id bigint) USING delta LOCATION '$dir' \" +\n        s\"TBLPROPERTIES (delta.appendOnly = 'true')\")\n    }\n    testTableCreation { dir =>\n      sql(s\"CREATE TABLE $tableName USING delta TBLPROPERTIES (delta.appendOnly = 'true') \" +\n        s\"LOCATION '$dir' AS SELECT * FROM range(10)\")\n    }\n    testTableCreation { dir =>\n      sql(s\"CREATE TABLE $tableName (id bigint NOT NULL) USING delta LOCATION '$dir'\")\n    }\n\n    withSQLConf(\"spark.databricks.delta.properties.defaults.appendOnly\" -> \"true\") {\n      testTableCreation { dir => spark.range(10).write.format(\"delta\").save(dir) }\n      testTableCreation { dir =>\n        spark.range(10).write.format(\"delta\").option(\"path\", dir).saveAsTable(tableName)\n      }\n      testTableCreation { dir =>\n        spark.range(10).writeTo(tableName).using(\"delta\").tableProperty(\"location\", dir).create()\n      }\n      testTableCreation { dir =>\n        sql(s\"CREATE TABLE $tableName (id bigint) USING delta LOCATION '$dir'\")\n      }\n      testTableCreation { dir =>\n        sql(s\"CREATE TABLE $tableName USING delta LOCATION '$dir' AS SELECT * FROM range(10)\")\n      }\n      testTableCreation(dir => {\n        val stream = MemoryStream[Int]\n        stream.addData(1 to 10)\n        val q = stream.toDF().writeStream.format(\"delta\")\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getCanonicalPath)\n          .start(dir)\n        q.processAllAvailable()\n        q.stop()\n      }\n      )\n\n      testTableCreation { dir =>\n        spark.range(10).write.mode(\"append\").parquet(dir)\n        sql(s\"CONVERT TO DELTA parquet.`$dir`\")\n      }\n    }\n  }\n\n  test(\"replacing a new table with default protocol\") {\n    withTempDir { dir =>\n      // In this test we go back and forth through protocol versions, testing the various syntaxes\n      // of replacing tables\n      val tbl = \"delta_test\"\n      withTable(tbl) {\n        withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n          sql(s\"CREATE TABLE $tbl (id bigint) USING delta LOCATION '${dir.getCanonicalPath}'\")\n        }\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.update().protocol.minWriterVersion === 1,\n          \"Should've picked up the protocol from the configuration\")\n\n        // Replace the table and make sure the config is picked up\n        withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\") {\n          spark.range(10).writeTo(tbl).using(\"delta\")\n            .tableProperty(\"location\", dir.getCanonicalPath).replace()\n        }\n        assert(deltaLog.update().protocol.minWriterVersion === 2,\n          \"Should've picked up the protocol from the configuration\")\n\n        // Will not downgrade without special flag.\n        withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n          sql(s\"REPLACE TABLE $tbl (id bigint) USING delta LOCATION '${dir.getCanonicalPath}'\")\n          assert(deltaLog.update().protocol.minWriterVersion === 2,\n            \"Should not pick up the protocol from the configuration\")\n        }\n\n        // Replace with the old writer again\n        withSQLConf(\n            DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\",\n            DeltaSQLConf.REPLACE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED.key -> \"true\") {\n          sql(s\"REPLACE TABLE $tbl (id bigint) USING delta LOCATION '${dir.getCanonicalPath}'\")\n          assert(deltaLog.update().protocol.minWriterVersion === 1,\n            \"Should've created a new protocol\")\n\n          sql(s\"CREATE OR REPLACE TABLE $tbl (id bigint NOT NULL) USING delta \" +\n            s\"LOCATION '${dir.getCanonicalPath}'\")\n          assert(deltaLog.update().protocol === Protocol(1, 7).withFeature(InvariantsTableFeature),\n            \"Invariant should require the higher protocol\")\n\n          // Go back to version 1\n          sql(s\"REPLACE TABLE $tbl (id bigint) USING delta LOCATION '${dir.getCanonicalPath}'\")\n          assert(deltaLog.update().protocol.minWriterVersion === 1,\n            \"Should've created a new protocol\")\n\n          // Check table properties with different syntax\n          spark.range(10).writeTo(tbl).tableProperty(\"location\", dir.getCanonicalPath)\n            .tableProperty(\"delta.appendOnly\", \"true\").using(\"delta\").createOrReplace()\n          assert(deltaLog.update().protocol  === Protocol(1, 7).withFeature(AppendOnlyTableFeature),\n            \"appendOnly should require the higher protocol\")\n        }\n      }\n    }\n  }\n\n  test(\"create a table with no protocol\") {\n    withTempDir { path =>\n      val log = DeltaLog.forTable(spark, path)\n      log.createLogDirectoriesIfNotExists()\n      log.store.write(\n        unsafeDeltaFile(log.logPath, 0),\n        Iterator(Metadata().json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n\n      assert(intercept[DeltaIllegalStateException] {\n        log.update()\n      }.getErrorClass == \"DELTA_STATE_RECOVER_ERROR\")\n      assert(intercept[DeltaIllegalStateException] {\n        spark.read.format(\"delta\").load(path.getCanonicalPath)\n      }.getErrorClass == \"DELTA_STATE_RECOVER_ERROR\")\n      assert(intercept[DeltaIllegalStateException] {\n        spark.range(1).write.format(\"delta\").mode(SaveMode.Overwrite).save(path.getCanonicalPath)\n      }.getErrorClass == \"DELTA_STATE_RECOVER_ERROR\")\n    }\n  }\n\n  test(\"bad inputs for default protocol versions\") {\n    val readerVersion = Action.supportedProtocolVersion().minReaderVersion\n    val writerVersion = Action.supportedProtocolVersion().minWriterVersion\n    withTempDir { path =>\n      val dir = path.getCanonicalPath\n      Seq(\"abc\", \"\", \"0\", (readerVersion + 1).toString).foreach { conf =>\n        val e = intercept[IllegalArgumentException] {\n          withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> conf) {\n            spark.range(10).write.format(\"delta\").save(dir)\n          }\n        }\n      }\n      Seq(\"abc\", \"\", \"0\", (writerVersion + 1).toString).foreach { conf =>\n        intercept[IllegalArgumentException] {\n          withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> conf) {\n            spark.range(10).write.format(\"delta\").save(dir)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (delta.minWriterVersion=3)\")\n\n        assert(deltaLog.snapshot.protocol.minReaderVersion === 1)\n        assert(deltaLog.snapshot.protocol.minWriterVersion === 3)\n        assertPropertiesAndShowTblProperties(deltaLog)\n      }\n    }\n  }\n\n  test(\"table creation with writer-only features as table property\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (\" +\n          \"  DeLtA.fEaTurE.APPendONly='eNAbled',\" +\n          \"  delta.feature.testWriter='enabled'\" +\n          \")\")\n\n      assert(deltaLog.snapshot.protocol.minReaderVersion === 1)\n      assert(\n        deltaLog.snapshot.protocol.minWriterVersion === TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(\n        deltaLog.snapshot.protocol.readerAndWriterFeatureNames === Set(\n          AppendOnlyTableFeature, InvariantsTableFeature, TestWriterFeature).map(_.name))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with legacy reader-writer features as table property\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (DeLtA.fEaTurE.testLEGACYReaderWritER='eNAbled')\")\n\n      assert(\n        deltaLog.update().protocol === Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeatures(Seq(\n            AppendOnlyTableFeature,\n            InvariantsTableFeature,\n            TestLegacyReaderWriterFeature)))\n    }\n  }\n\n  test(\"table creation with native writer-only features as table property\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (DeLtA.fEaTurE.testWritER='eNAbled')\")\n\n      assert(\n        deltaLog.snapshot.protocol.minReaderVersion === 1)\n      assert(\n        deltaLog.snapshot.protocol.minWriterVersion ===\n          TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(\n        deltaLog.snapshot.protocol.readerAndWriterFeatureNames ===\n          Set(AppendOnlyTableFeature.name, InvariantsTableFeature.name, TestWriterFeature.name))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with reader-writer features as table property\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (\" +\n          \"  DeLtA.fEaTurE.testLEGACYReaderWritER='eNAbled',\" +\n          \"  DeLtA.fEaTurE.testReaderWritER='enabled'\" +\n          \")\")\n\n      assert(\n        deltaLog.snapshot.protocol.minReaderVersion === TABLE_FEATURES_MIN_READER_VERSION)\n      assert(\n        deltaLog.snapshot.protocol.minWriterVersion === TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(\n        deltaLog.snapshot.protocol.readerAndWriterFeatureNames === Set(\n          InvariantsTableFeature,\n          AppendOnlyTableFeature,\n          TestLegacyReaderWriterFeature,\n          TestReaderWriterFeature).map(_.name))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with feature as table property and supported protocol version\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (\" +\n          s\"  DEltA.MINREADERversion='$TABLE_FEATURES_MIN_READER_VERSION',\" +\n          s\"  DEltA.MINWRITERversion='$TABLE_FEATURES_MIN_WRITER_VERSION',\" +\n          \"  DeLtA.fEaTurE.testLEGACYReaderWriter='eNAbled'\" +\n          \")\")\n\n      assert(\n        deltaLog.snapshot.protocol === Protocol(\n          minReaderVersion = 2,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = None,\n          writerFeatures = Some(Set(TestLegacyReaderWriterFeature.name))))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with feature as table property and supported writer protocol version\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          s\"TBLPROPERTIES (\" +\n          s\"  delta.minWriterVersion='$TABLE_FEATURES_MIN_WRITER_VERSION',\" +\n          s\"  delta.feature.testLegacyWriter='enabled'\" +\n          s\")\")\n\n      assert(\n        deltaLog.snapshot.protocol === Protocol(\n          minReaderVersion = 1,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = None,\n          writerFeatures = Some(Set(TestLegacyWriterFeature.name))))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with automatically-enabled features\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta TBLPROPERTIES (\" +\n          s\"  ${TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY}='true'\" +\n          \")\")\n      assert(\n        deltaLog.snapshot.protocol === Protocol(\n          minReaderVersion = TABLE_FEATURES_MIN_READER_VERSION,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = Some(Set(TestReaderWriterMetadataAutoUpdateFeature.name)),\n          writerFeatures = Some(Set(\n            TestReaderWriterMetadataAutoUpdateFeature.name,\n            AppendOnlyTableFeature.name,\n            InvariantsTableFeature.name))))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with automatically-enabled legacy feature and unsupported protocol\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta TBLPROPERTIES (\" +\n          \"  delta.minReaderVersion='1',\" +\n          \"  delta.minWriterVersion='2',\" +\n          \"  delta.enableChangeDataFeed='true'\" +\n          \")\")\n      assert(deltaLog.update().protocol === Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        ChangeDataFeedTableFeature)))\n    }\n  }\n\n  test(\"table creation with automatically-enabled native feature and unsupported protocol\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta TBLPROPERTIES (\" +\n          \"  delta.minReaderVersion='1',\" +\n          \"  delta.minWriterVersion='2',\" +\n          s\"  ${TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY}='true'\" +\n          \")\")\n      assert(\n        deltaLog.snapshot.protocol === Protocol(\n          minReaderVersion = TABLE_FEATURES_MIN_READER_VERSION,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = Some(Set(TestReaderWriterMetadataAutoUpdateFeature.name)),\n          writerFeatures = Some(Set(\n            TestReaderWriterMetadataAutoUpdateFeature.name,\n            InvariantsTableFeature.name,\n            AppendOnlyTableFeature.name))))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  test(\"table creation with feature as table property and unsupported protocol version\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta TBLPROPERTIES (\" +\n          \"  delta.minReaderVersion='1',\" +\n          \"  delta.minWriterVersion='2',\" +\n          \"  delta.feature.testWriter='enabled'\" +\n          \")\")\n      assert(\n        deltaLog.snapshot.protocol === Protocol(\n          minReaderVersion = 1,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = None,\n          writerFeatures = Some(Set(\n            InvariantsTableFeature.name,\n            AppendOnlyTableFeature.name,\n            TestWriterFeature.name))))\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n    }\n  }\n\n  def testCreateTable(\n      name: String,\n      props: Map[String, String],\n      expectedExceptionClass: Option[String] = None,\n      expectedFinalProtocol: Option[Protocol] = None): Unit = {\n    test(s\"create table - $name\") {\n      withTempDir { dir =>\n        val log = DeltaLog.forTable(spark, dir)\n\n        val propString = props.map(kv => s\"'${kv._1}'='${kv._2}'\").mkString(\",\")\n        if (expectedExceptionClass.isDefined) {\n          assert(intercept[DeltaTableFeatureException] {\n            sql(\n              s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n                s\"TBLPROPERTIES ($propString)\")\n          }.getErrorClass === expectedExceptionClass.get)\n        } else {\n          sql(\n            s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n              s\"TBLPROPERTIES ($propString)\")\n        }\n        expectedFinalProtocol match {\n          case Some(p) => assert(log.update().protocol === p)\n          case None => // Do nothing\n        }\n      }\n    }\n  }\n\n  testCreateTable(\n    \"legacy protocol, legacy feature, metadata\",\n    Map(\"delta.appendOnly\" -> \"true\"),\n    expectedFinalProtocol = Some(Protocol(1, 2)))\n\n  testCreateTable(\n    \"legacy protocol, legacy feature, feature property\",\n    Map(s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeatures(Seq(\n        TestLegacyReaderWriterFeature,\n        AppendOnlyTableFeature,\n        InvariantsTableFeature))))\n\n  testCreateTable(\n    \"legacy protocol, legacy writer feature, feature property\",\n    Map(s\"delta.feature.${TestLegacyWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION).withFeatures(Seq(\n        TestLegacyWriterFeature,\n        AppendOnlyTableFeature,\n        InvariantsTableFeature ))))\n\n  testCreateTable(\n    \"legacy protocol, native auto-update feature, metadata\",\n    Map(TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(\n          TestReaderWriterMetadataAutoUpdateFeature,\n          AppendOnlyTableFeature,\n          InvariantsTableFeature))))\n\n  testCreateTable(\n    \"legacy protocol, native non-auto-update feature, metadata\",\n    Map(TestReaderWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(\n          TestReaderWriterMetadataNoAutoUpdateFeature,\n          AppendOnlyTableFeature,\n          InvariantsTableFeature))))\n\n  testCreateTable(\n    \"legacy protocol, native auto-update feature, feature property\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(\n          TestReaderWriterMetadataAutoUpdateFeature,\n          AppendOnlyTableFeature,\n          InvariantsTableFeature))))\n\n  testCreateTable(\n    \"legacy protocol, native non-auto-update feature, feature property\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataNoAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(\n          TestReaderWriterMetadataNoAutoUpdateFeature,\n          AppendOnlyTableFeature,\n          InvariantsTableFeature))))\n\n  testCreateTable(\n    \"legacy protocol with supported version props, legacy feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key ->\n        TestLegacyReaderWriterFeature.minReaderVersion.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key ->\n        TestLegacyReaderWriterFeature.minWriterVersion.toString,\n      s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(Protocol(\n      TestLegacyReaderWriterFeature.minReaderVersion,\n      TestLegacyReaderWriterFeature.minWriterVersion)))\n\n  testCreateTable(\n    \"legacy protocol with table feature version props, legacy feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)))\n\n  testCreateTable(\n    \"legacy protocol with supported version props, native feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)))\n\n  testCreateTable(\n    \"table features protocol, legacy feature, metadata\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      \"delta.appendOnly\" -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(AppendOnlyTableFeature)))\n\n  testCreateTable(\n    \"table features protocol, legacy feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)))\n\n  testCreateTable(\n    \"table features protocol, native auto-update feature, metadata\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)))\n\n  testCreateTable(\n    \"table features protocol, native non-auto-update feature, metadata\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      TestReaderWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataNoAutoUpdateFeature)))\n\n  testCreateTable(\n    \"table features protocol, native auto-update feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)))\n\n  testCreateTable(\n    \"table features protocol, native non-auto-update feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestReaderWriterMetadataNoAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataNoAutoUpdateFeature)))\n\n  testCreateTable(\n    name = \"feature with a dependency\",\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestFeatureWithDependency.name}\" -> \"supported\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestFeatureWithDependency, TestReaderWriterFeature))))\n\n  testCreateTable(\n    name = \"feature with a dependency, enabled using a feature property\",\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      TestFeatureWithDependency.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestFeatureWithDependency, TestReaderWriterFeature))))\n\n  testCreateTable(\n    name = \"feature with a dependency that has a dependency\",\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestFeatureWithTransitiveDependency.name}\" -> \"supported\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(\n          TestFeatureWithTransitiveDependency,\n          TestFeatureWithDependency,\n          TestReaderWriterFeature))))\n\n  def testAlterTable(\n      name: String,\n      props: Map[String, String],\n      expectedExceptionClass: Option[String] = None,\n      expectedFinalProtocol: Option[Protocol] = None,\n      tableProtocol: Protocol = Protocol(1, 1)): Unit = {\n    test(s\"alter table - $name\") {\n      withTempDir { dir =>\n        val log = createTableWithProtocol(tableProtocol, dir)\n\n        val propString = props.map(kv => s\"'${kv._1}'='${kv._2}'\").mkString(\",\")\n        if (expectedExceptionClass.isDefined) {\n          assert(intercept[DeltaTableFeatureException] {\n            sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES ($propString)\")\n          }.getErrorClass === expectedExceptionClass.get)\n        } else {\n          sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES ($propString)\")\n        }\n        expectedFinalProtocol match {\n          case Some(p) => assert(log.update().protocol === p)\n          case None => // Do nothing\n        }\n      }\n    }\n  }\n\n  testAlterTable(\n    name = \"downgrade reader version is a no-op\",\n    tableProtocol = Protocol(2, 5),\n    props = Map(DeltaConfigs.MIN_READER_VERSION.key -> \"1\"),\n    expectedFinalProtocol = Some(Protocol(2, 5)))\n\n  testAlterTable(\n    name = \"downgrade writer version is a no-op\",\n    tableProtocol = Protocol(1, 3),\n    props = Map(DeltaConfigs.MIN_WRITER_VERSION.key -> \"1\"),\n    expectedFinalProtocol = Some(Protocol(1, 3)))\n\n  testAlterTable(\n    name = \"downgrade both reader and versions version is a no-op\",\n    tableProtocol = Protocol(2, 5),\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> \"1\",\n      DeltaConfigs.MIN_WRITER_VERSION.key -> \"1\"),\n    expectedFinalProtocol = Some(Protocol(2, 5)))\n\n  testAlterTable(\n    name = \"downgrade reader but upgrade writer versions (legacy protocol)\",\n    tableProtocol = Protocol(2, 2),\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> \"1\",\n      DeltaConfigs.MIN_WRITER_VERSION.key -> \"5\"),\n    expectedFinalProtocol = Some(Protocol(2, 5)))\n\n  testAlterTable(\n    name = \"downgrade reader but upgrade writer versions (table features protocol)\",\n    tableProtocol = Protocol(2, 2),\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> \"1\",\n      DeltaConfigs.MIN_WRITER_VERSION.key -> \"7\"),\n    // There is no (2, 2) feature. Protocol versions are downgraded (1, 2).\n    expectedFinalProtocol = Some(Protocol(1, 2)))\n\n  testAlterTable(\n    name = \"downgrade while enabling a feature will become an upgrade\",\n    tableProtocol = Protocol(1, 2),\n    props = Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> \"1\",\n      DeltaConfigs.MIN_WRITER_VERSION.key -> \"1\",\n      DeltaConfigs.CHANGE_DATA_FEED.key -> \"true\"),\n    expectedFinalProtocol = Some(Protocol(1, 7).withFeatures(Seq(\n      AppendOnlyTableFeature,\n      InvariantsTableFeature,\n      ChangeDataFeedTableFeature))))\n\n  testAlterTable(\n    \"legacy protocol, legacy feature, metadata\",\n    Map(\"delta.appendOnly\" -> \"true\"),\n    expectedFinalProtocol = Some(Protocol(1, 7).withFeature(AppendOnlyTableFeature)))\n\n  testAlterTable(\n    \"legacy protocol, legacy feature, feature property\",\n    Map(s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)))\n\n  testAlterTable(\n    \"legacy protocol, legacy writer feature, feature property\",\n    Map(s\"delta.feature.${TestLegacyWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestLegacyWriterFeature)\n        .merge(Protocol(1, 2))),\n    tableProtocol = Protocol(1, 2))\n\n  testAlterTable(\n    \"legacy protocol, native auto-update feature, metadata\",\n    Map(TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)))\n\n  testAlterTable(\n    \"legacy protocol, native non-auto-update feature, metadata\",\n    Map(TestReaderWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedExceptionClass = Some(\"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\"))\n\n  testAlterTable(\n    \"legacy protocol, native non-auto-update feature, metadata and feature property\",\n    Map(\n      TestReaderWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY -> \"true\",\n      s\"delta.feature.${TestReaderWriterMetadataNoAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataNoAutoUpdateFeature)))\n\n  testAlterTable(\n    \"legacy protocol, native auto-update feature, feature property\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"supported\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)))\n\n  testAlterTable(\n    \"legacy protocol, native non-auto-update feature, feature property\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataNoAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataNoAutoUpdateFeature)))\n\n  testAlterTable(\n    \"legacy protocol with supported version props, legacy feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key ->\n        TestLegacyReaderWriterFeature.minReaderVersion.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key ->\n        TestLegacyReaderWriterFeature.minWriterVersion.toString,\n      s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .merge(TestLegacyReaderWriterFeature.minProtocolVersion)))\n\n  testAlterTable(\n    \"legacy protocol with table feature version props, legacy feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)))\n\n  testAlterTable(\n    \"legacy protocol with supported version props, native feature, feature property\",\n    Map(\n      DeltaConfigs.MIN_READER_VERSION.key -> TABLE_FEATURES_MIN_READER_VERSION.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n      s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)))\n\n  testAlterTable(\n    \"table features protocol, legacy feature, metadata\",\n    Map(\"delta.appendOnly\" -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(AppendOnlyTableFeature)),\n    tableProtocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION))\n\n  testAlterTable(\n    \"table features protocol, legacy feature, feature property\",\n    Map(s\"delta.feature.${TestLegacyReaderWriterFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)),\n    tableProtocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION))\n\n  testAlterTable(\n    \"table features protocol, native auto-update feature, metadata\",\n    Map(TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)),\n    tableProtocol =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION))\n\n  testAlterTable(\n    \"table features protocol, native non-auto-update feature, metadata\",\n    Map(TestReaderWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY -> \"true\"),\n    tableProtocol =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION),\n    expectedExceptionClass = Some(\"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\"))\n\n  testAlterTable(\n    \"table features protocol, native non-auto-update feature, metadata and feature property\",\n    Map(\n      TestReaderWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY -> \"true\",\n      s\"delta.feature.${TestReaderWriterMetadataNoAutoUpdateFeature.name}\" -> \"enabled\"),\n    tableProtocol =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataNoAutoUpdateFeature)))\n\n  testAlterTable(\n    \"table features protocol, native auto-update feature, feature property\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature)),\n    tableProtocol =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION))\n\n  testAlterTable(\n    \"table features protocol, native non-auto-update feature, feature property\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataNoAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataNoAutoUpdateFeature)),\n    tableProtocol =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION))\n\n  testAlterTable(\n    \"feature property merges the old protocol\",\n    Map(s\"delta.feature.${TestReaderWriterMetadataAutoUpdateFeature.name}\" -> \"enabled\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterMetadataAutoUpdateFeature).merge(Protocol(1, 2))),\n    tableProtocol = Protocol(1, 2))\n\n  testAlterTable(\n    name = \"feature with a dependency\",\n    tableProtocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION),\n    props = Map(s\"delta.feature.${TestFeatureWithDependency.name}\" -> \"supported\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestFeatureWithDependency, TestReaderWriterFeature))))\n\n  testAlterTable(\n    name = \"feature with a dependency, enabled using a feature property\",\n    tableProtocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION),\n    props = Map(TestFeatureWithDependency.TABLE_PROP_KEY -> \"true\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(TestFeatureWithDependency, TestReaderWriterFeature))))\n\n  testAlterTable(\n    name = \"feature with a dependency that has a dependency\",\n    tableProtocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION),\n    props = Map(s\"delta.feature.${TestFeatureWithTransitiveDependency.name}\" -> \"supported\"),\n    expectedFinalProtocol = Some(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeatures(Seq(\n          TestFeatureWithTransitiveDependency,\n          TestFeatureWithDependency,\n          TestReaderWriterFeature))))\n\n  test(\"non-auto-update capable feature requires manual enablement (via feature prop)\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        spark.range(10).writeTo(s\"delta.`${dir.getCanonicalPath}`\").using(\"delta\").create()\n      }\n      val expectedProtocolOnCreation = Protocol(1, 1)\n      assert(deltaLog.update().protocol === expectedProtocolOnCreation)\n\n      assert(intercept[DeltaTableFeatureException] {\n        withSQLConf(defaultPropertyKey(TestWriterMetadataNoAutoUpdateFeature) -> \"supported\") {\n          sql(\n            s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\" +\n              s\"  '${TestWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY}' = 'true')\")\n        }\n      }.getErrorClass === \"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\",\n      \"existing tables should ignore session defaults.\")\n\n      sql(\n        s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\" +\n          s\"  '${propertyKey(TestWriterMetadataNoAutoUpdateFeature)}' = 'supported',\" +\n          s\"  '${TestWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY}' = 'true')\")\n      assert(\n        deltaLog.update().protocol ===\n          Protocol(1, 7).withFeature(TestWriterMetadataNoAutoUpdateFeature)\n            .merge(TestWriterMetadataNoAutoUpdateFeature.minProtocolVersion))\n    }\n  }\n\n  test(\"non-auto-update capable error message is correct\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        spark.range(10).writeTo(s\"delta.`${dir.getCanonicalPath}`\")\n          .tableProperty(\"delta.appendOnly\", \"true\")\n          .using(\"delta\")\n          .create()\n        val protocolOfNewTable = Protocol(1, 7).withFeature(AppendOnlyTableFeature)\n        assert(deltaLog.update().protocol === protocolOfNewTable)\n\n        val e = intercept[DeltaTableFeatureException] {\n          // ALTER TABLE must not consider this SQL config\n          withSQLConf(defaultPropertyKey(TestWriterFeature) -> \"supported\") {\n            sql(\n              s\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\" +\n                s\"  'delta.appendOnly' = 'false',\" +\n                s\"  'delta.enableChangeDataFeed' = 'true',\" +\n                s\"  '${TestReaderWriterMetadataAutoUpdateFeature.TABLE_PROP_KEY}' = 'true',\" +\n                s\"  '${TestWriterMetadataNoAutoUpdateFeature.TABLE_PROP_KEY}' = 'true')\")\n          }\n        }\n\n        val unsupportedFeatures = TestWriterMetadataNoAutoUpdateFeature.name\n        val supportedFeatures =\n          (protocolOfNewTable.implicitlyAndExplicitlySupportedFeatures +\n            ChangeDataFeedTableFeature +\n            TestReaderWriterMetadataAutoUpdateFeature).map(_.name).toSeq.sorted.mkString(\", \")\n        assert(e.getErrorClass === \"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\")\n\n        // `getMessageParameters` is available starting from Spark 3.4.\n        // For now we have to check for substrings.\n        assert(e.getMessage.contains(s\" $unsupportedFeatures.\"))\n        assert(e.getMessage.contains(s\" $supportedFeatures.\"))\n\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property - property wins over conf\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"3\") {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (delta.MINwriterVERsion=2)\")\n\n        assert(deltaLog.snapshot.protocol.minWriterVersion === 2)\n        assertPropertiesAndShowTblProperties(deltaLog)\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property - feature requirements win SQL\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (delta.minWriterVersion=1, delta.appendOnly=true)\")\n\n        assert(deltaLog.update().protocol === Protocol(1, 7).withFeature(AppendOnlyTableFeature))\n        assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property - feature requirements win DF\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n        spark.range(10).writeTo(s\"delta.`${dir.getCanonicalPath}`\")\n          .tableProperty(\"delta.minWriterVersion\", \"1\")\n          .tableProperty(\"delta.appendOnly\", \"true\")\n          .using(\"delta\")\n          .create()\n\n        assert(deltaLog.update().protocol === Protocol(1, 7).withFeature(AppendOnlyTableFeature))\n        assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = true)\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property - default table properties\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf((DeltaConfigs.sqlConfPrefix + \"minWriterVersion\") -> \"3\") {\n        spark.range(10).writeTo(s\"delta.`${dir.getCanonicalPath}`\")\n          .using(\"delta\")\n          .create()\n\n        assert(deltaLog.snapshot.protocol.minWriterVersion === 3)\n        assertPropertiesAndShowTblProperties(deltaLog)\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property - explicit wins over conf\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf((DeltaConfigs.sqlConfPrefix + \"minWriterVersion\") -> \"3\") {\n        spark.range(10).writeTo(s\"delta.`${dir.getCanonicalPath}`\")\n          .tableProperty(\"delta.minWriterVersion\", \"2\")\n          .using(\"delta\")\n          .create()\n\n        assert(deltaLog.snapshot.protocol.minWriterVersion === 2)\n        assertPropertiesAndShowTblProperties(deltaLog)\n      }\n    }\n  }\n\n  test(\"table creation with protocol as table property - bad input\") {\n    withTempDir { dir =>\n      val e = intercept[IllegalArgumentException] {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (delta.minWriterVersion='delta rulz')\")\n      }\n      assert(e.getMessage.contains(\" one of \"))\n\n      val e2 = intercept[AnalysisException] {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (delta.minWr1terVersion=2)\") // Typo in minWriterVersion\n      }\n      assert(e2.getMessage.contains(\"Unknown configuration\"))\n\n      val e3 = intercept[IllegalArgumentException] {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n          \"TBLPROPERTIES (delta.minWriterVersion='-1')\")\n      }\n      assert(e3.getMessage.contains(\" one of \"))\n    }\n  }\n\n  test(\"protocol as table property - desc table\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      withSQLConf(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\") {\n        spark.range(10).writeTo(s\"delta.`${dir.getCanonicalPath}`\")\n          .using(\"delta\")\n          .tableProperty(\"delta.minWriterVersion\", \"3\")\n          .createOrReplace()\n      }\n      assert(deltaLog.snapshot.protocol.minWriterVersion === 3)\n\n      val output = spark.sql(s\"DESC EXTENDED delta.`${dir.getCanonicalPath}`\").collect()\n      assert(output.exists(_.toString.contains(\"delta.minWriterVersion\")),\n        s\"minWriterVersion not found in: ${output.mkString(\"\\n\")}\")\n      assert(output.exists(_.toString.contains(\"delta.minReaderVersion\")),\n        s\"minReaderVersion not found in: ${output.mkString(\"\\n\")}\")\n    }\n  }\n\n  test(\"auto upgrade protocol version - version 2\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 1), path)\n      spark.sql(s\"\"\"\n                   |ALTER TABLE delta.`${log.dataPath.toString}`\n                   |SET TBLPROPERTIES ('delta.appendOnly' = 'true')\n                 \"\"\".stripMargin)\n      assert(log.update().protocol === Protocol(1, 7).withFeature(AppendOnlyTableFeature))\n    }\n  }\n\n  test(\"auto upgrade protocol version - version 3\") {\n    withTempDir { path =>\n      val log = DeltaLog.forTable(spark, path)\n      sql(s\"CREATE TABLE delta.`${path.getCanonicalPath}` (id bigint) USING delta \" +\n        \"TBLPROPERTIES (delta.minWriterVersion=2)\")\n      assert(log.update().protocol.minWriterVersion === 2)\n      spark.sql(s\"\"\"\n                   |ALTER TABLE delta.`${path.getCanonicalPath}`\n                   |ADD CONSTRAINT test CHECK (id < 5)\n                 \"\"\".stripMargin)\n      assert(log.update().protocol.minWriterVersion === 3)\n    }\n  }\n\n  test(\"auto upgrade protocol version even with explicit protocol version configs\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 1), path)\n      spark.sql(s\"\"\"\n                   |ALTER TABLE delta.`${log.dataPath.toString}` SET TBLPROPERTIES (\n                   |  'delta.minWriterVersion' = '2',\n                   |  'delta.enableChangeDataFeed' = 'true'\n                   |)\"\"\".stripMargin)\n      assert(log.update().protocol === Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        ChangeDataFeedTableFeature)))\n    }\n  }\n\n  test(\"legacy feature can be listed during alter table with silent protocol upgrade\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 1), path)\n      spark.sql(s\"\"\"\n                   |ALTER TABLE delta.`${log.dataPath.toString}` SET TBLPROPERTIES (\n                   |  'delta.feature.testLegacyReaderWriter' = 'enabled'\n                   |)\"\"\".stripMargin)\n      assert(\n        log.update().protocol === Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeature(TestLegacyReaderWriterFeature))\n    }\n  }\n\n  test(\"legacy feature can be explicitly listed during alter table\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION), path)\n      spark.sql(s\"\"\"\n                   |ALTER TABLE delta.`${log.dataPath.toString}` SET TBLPROPERTIES (\n                   |  'delta.feature.testLegacyReaderWriter' = 'enabled'\n                   |)\"\"\".stripMargin)\n      assert(log.snapshot.protocol === Protocol(\n        2,\n        TABLE_FEATURES_MIN_WRITER_VERSION,\n        readerFeatures = None,\n        writerFeatures = Some(Set(TestLegacyReaderWriterFeature.name))))\n    }\n  }\n\n  test(\"native feature can be explicitly listed during alter table with silent protocol upgrade\") {\n    withTempDir { path =>\n      val log = createTableWithProtocol(Protocol(1, 2), path)\n      spark.sql(s\"\"\"\n                   |ALTER TABLE delta.`${log.dataPath.toString}` SET TBLPROPERTIES (\n                   |  'delta.feature.testReaderWriter' = 'enabled'\n                   |)\"\"\".stripMargin)\n      assert(\n        log.snapshot.protocol ===\n          TestReaderWriterFeature.minProtocolVersion\n            .withFeature(TestReaderWriterFeature)\n            .merge(Protocol(1, 2)))\n    }\n  }\n\n  test(\"all active features are enabled in protocol\") {\n    withTempDir { path =>\n      spark.range(10).write.format(\"delta\").save(path.getCanonicalPath)\n      val log = DeltaLog.forTable(spark, path)\n      val snapshot = log.unsafeVolatileSnapshot\n      val p = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n      val m = snapshot.metadata.copy(configuration = snapshot.metadata.configuration ++ Map(\n        DeltaConfigs.IS_APPEND_ONLY.key -> \"false\",\n        DeltaConfigs.CHANGE_DATA_FEED.key -> \"true\"))\n      log.store.write(\n        unsafeDeltaFile(log.logPath, snapshot.version + 1),\n        Iterator(m.json, p.json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n      val e = intercept[DeltaTableFeatureException] {\n        spark.read.format(\"delta\").load(path.getCanonicalPath).collect()\n      }\n      assert(e.getMessage.contains(\"enabled in metadata but not listed in protocol\"))\n      assert(e.getMessage.contains(\": changeDataFeed.\"))\n    }\n  }\n\n  test(\"table feature status\") {\n    withTempDir { path =>\n      withSQLConf(\n        defaultPropertyKey(ChangeDataFeedTableFeature) -> FEATURE_PROP_SUPPORTED,\n        defaultPropertyKey(GeneratedColumnsTableFeature) -> FEATURE_PROP_ENABLED) {\n        spark.range(10).write.format(\"delta\").save(path.getCanonicalPath)\n        val log = DeltaLog.forTable(spark, path)\n        val protocol = log.update().protocol\n\n        assert(protocol.isFeatureSupported(ChangeDataFeedTableFeature))\n        assert(protocol.isFeatureSupported(GeneratedColumnsTableFeature))\n      }\n    }\n  }\n\n  private def replaceTableAs(path: File): Unit = {\n    val p = path.getCanonicalPath\n    sql(s\"REPLACE TABLE delta.`$p` USING delta AS (SELECT * FROM delta.`$p`)\")\n  }\n\n  test(\"REPLACE AS updates protocol when defaults are higher\") {\n    withTempDir { path =>\n      spark\n        .range(10)\n        .write\n        .format(\"delta\")\n        .option(DeltaConfigs.MIN_READER_VERSION.key, 1)\n        .option(DeltaConfigs.MIN_WRITER_VERSION.key, 2)\n        .mode(\"append\")\n        .save(path.getCanonicalPath)\n      val log = DeltaLog.forTable(spark, path)\n      assert(log.update().protocol === Protocol(1, 2))\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"2\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"5\") {\n        replaceTableAs(path)\n      }\n      assert(log.update().protocol === Protocol(2, 5))\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"3\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"7\",\n        TableFeatureProtocolUtils.defaultPropertyKey(TestReaderWriterFeature) -> \"enabled\") {\n        replaceTableAs(path)\n      }\n      assert(\n        log.update().protocol ===\n          Protocol(2, 5).merge(Protocol(3, 7).withFeature(TestReaderWriterFeature)))\n    }\n  }\n\n  for (p <- Seq(Protocol(2, 5), Protocol(3, 7).withFeature(TestReaderWriterFeature)))\n    test(s\"REPLACE AS keeps protocol when defaults are lower ($p)\") {\n      withTempDir { path =>\n        spark\n          .range(10)\n          .write\n          .format(\"delta\")\n          .option(DeltaConfigs.MIN_READER_VERSION.key, p.minReaderVersion)\n          .option(DeltaConfigs.MIN_WRITER_VERSION.key, p.minWriterVersion)\n          .options(\n            p.readerAndWriterFeatureNames\n              .flatMap(TableFeature.featureNameToFeature)\n              .map(f => TableFeatureProtocolUtils.propertyKey(f) -> \"enabled\")\n              .toMap)\n          .mode(\"append\")\n          .save(path.getCanonicalPath)\n        val log = DeltaLog.forTable(spark, path)\n        assert(log.update().protocol === p)\n        withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\") {\n          replaceTableAs(path)\n        }\n        assert(log.update().protocol === p.merge(Protocol(1, 2)))\n        withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\",\n          TableFeatureProtocolUtils.defaultPropertyKey(TestReaderWriterFeature) -> \"enabled\") {\n          replaceTableAs(path)\n        }\n        assert(\n          log.update().protocol ===\n            p\n              .merge(Protocol(1, 2))\n              .merge(\n                TestReaderWriterFeature.minProtocolVersion.withFeature(TestReaderWriterFeature)))\n      }\n    }\n\n  test(\"REPLACE AS can ignore protocol defaults\") {\n    withTempDir { path =>\n      withSQLConf(\n          DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.defaultTablePropertyKey -> \"true\") {\n        spark.range(10).write.format(\"delta\").save(path.getCanonicalPath)\n      }\n      val log = DeltaLog.forTable(spark, path)\n      assert(log.update().protocol === Protocol(1, 1))\n\n      withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"3\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"7\",\n          defaultPropertyKey(ChangeDataFeedTableFeature) -> FEATURE_PROP_SUPPORTED,\n          DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.defaultTablePropertyKey -> \"true\") {\n        replaceTableAs(path)\n      }\n      assert(log.update().protocol === Protocol(1, 1))\n      assert(\n        !log.update().metadata.configuration\n          .contains(DeltaConfigs.CREATE_TABLE_IGNORE_PROTOCOL_DEFAULTS.key))\n    }\n  }\n\n  test(\"protocol change logging\") {\n    withTempDir { path =>\n      val dir = path.getCanonicalPath\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\") {\n        assert(\n          captureProtocolChangeEventBlob {\n            sql(s\"CREATE TABLE delta.`$dir` (id INT) USING delta\")\n          } === Map(\n            \"toProtocol\" -> Map(\n              \"minReaderVersion\" -> 1,\n              \"minWriterVersion\" -> 2,\n              \"supportedFeatures\" -> List(\"appendOnly\", \"invariants\")\n            ),\n            \"operationName\" -> \"CREATE TABLE\"))\n      }\n\n      // Upgrade protocol\n      assert(captureProtocolChangeEventBlob {\n        sql(\n          s\"ALTER TABLE delta.`$dir` \" +\n            s\"SET TBLPROPERTIES (${DeltaConfigs.MIN_WRITER_VERSION.key} = '3')\")\n      } === Map(\n        \"fromProtocol\" -> Map(\n          \"minReaderVersion\" -> 1,\n          \"minWriterVersion\" -> 2,\n          \"supportedFeatures\" -> List(\"appendOnly\", \"invariants\")\n        ),\n        \"toProtocol\" -> Map(\n          \"minReaderVersion\" -> 1,\n          \"minWriterVersion\" -> 3,\n          \"supportedFeatures\" -> List(\"appendOnly\", \"checkConstraints\", \"invariants\")\n        ),\n        \"operationName\" -> \"SET TBLPROPERTIES\"))\n\n      // Add feature\n      assert(captureProtocolChangeEventBlob {\n        sql(\n          s\"ALTER TABLE delta.`$dir` \" +\n            s\"SET TBLPROPERTIES (${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key} = 'true')\")\n      } === Map(\n        \"fromProtocol\" -> Map(\n          \"minReaderVersion\" -> 1,\n          \"minWriterVersion\" -> 3,\n          \"supportedFeatures\" -> List(\"appendOnly\", \"checkConstraints\", \"invariants\")\n        ),\n        \"toProtocol\" -> Map(\n          \"minReaderVersion\" -> 3,\n          \"minWriterVersion\" -> 7,\n          \"supportedFeatures\" ->\n            List(\"appendOnly\", \"checkConstraints\", \"deletionVectors\", \"invariants\")\n        ),\n        \"operationName\" -> \"SET TBLPROPERTIES\"))\n    }\n  }\n\n  test(\"protocol change logging using commitLarge\") {\n    withTempDir { path =>\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\") {\n        assert(\n          captureProtocolChangeEventBlob {\n            sql(s\"CREATE TABLE delta.`${path.getCanonicalPath}` (id INT) USING delta\")\n          } === Map(\n            \"toProtocol\" -> Map(\n              \"minReaderVersion\" -> 1,\n              \"minWriterVersion\" -> 2,\n              \"supportedFeatures\" -> List(\"appendOnly\", \"invariants\")\n            ),\n            \"operationName\" -> \"CREATE TABLE\"))\n      }\n\n      // Clone table to invoke commitLarge\n      withTempDir { clonedPath =>\n        assert(\n          captureProtocolChangeEventBlob {\n            sql(s\"CREATE TABLE delta.`${clonedPath.getCanonicalPath}` \" +\n              s\"SHALLOW CLONE delta.`${path.getCanonicalPath}` \" +\n              s\"TBLPROPERTIES (${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key} = 'true')\")\n          } === Map(\n            \"toProtocol\" -> Map(\n              \"minReaderVersion\" -> 3,\n              \"minWriterVersion\" -> 7,\n              \"supportedFeatures\" -> List(\"appendOnly\", \"deletionVectors\", \"invariants\")\n            ),\n            \"operationName\" -> \"CREATE TABLE\"))\n      }\n    }\n  }\n\n  def protocolWithFeatures(\n      readerFeatures: Seq[TableFeature] = Seq.empty,\n      writerFeatures: Seq[TableFeature] = Seq.empty): Protocol = {\n    val readerFeaturesEnabled = readerFeatures.nonEmpty\n    val writerFeaturesEnabled = readerFeatures.nonEmpty || writerFeatures.nonEmpty\n    val minReaderVersion = if (readerFeaturesEnabled) TABLE_FEATURES_MIN_READER_VERSION else 1\n    val minWriterVersion = if (writerFeaturesEnabled) TABLE_FEATURES_MIN_WRITER_VERSION else 1\n    val readerFeatureNames =\n      if (readerFeaturesEnabled) Some(readerFeatures.map(_.name).toSet) else None\n    val writerFeatureNames = if (writerFeaturesEnabled) {\n      Some((readerFeatures ++ writerFeatures).map(_.name).toSet)\n    } else {\n      None\n    }\n\n    Protocol(\n      minReaderVersion = minReaderVersion,\n      minWriterVersion = minWriterVersion,\n      readerFeatures = readerFeatureNames,\n      writerFeatures = writerFeatureNames)\n  }\n\n  def protocolWithReaderFeature(readerFeature: TableFeature): Protocol = {\n    protocolWithFeatures(readerFeatures = Seq(readerFeature))\n  }\n\n  def protocolWithWriterFeature(writerFeature: TableFeature): Protocol = {\n    protocolWithFeatures(writerFeatures = Seq(writerFeature))\n  }\n\n  def emptyProtocolWithWriterFeatures: Protocol =\n    Protocol(\n      minReaderVersion = 1,\n      minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n      readerFeatures = None,\n      writerFeatures = Some(Set.empty))\n\n  def emptyProtocolWithReaderFeatures: Protocol =\n    Protocol(\n      minReaderVersion = TABLE_FEATURES_MIN_READER_VERSION,\n      minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n      readerFeatures = Some(Set.empty),\n      writerFeatures = Some(Set.empty))\n\n  protected def createTableWithFeature(\n      deltaLog: DeltaLog,\n      feature: TableFeature,\n      featureProperty: String): Unit = {\n    sql(s\"\"\"CREATE TABLE delta.`${deltaLog.dataPath}` (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.feature.${feature.name} = 'supported',\n           |$featureProperty = \"true\"\n           |)\"\"\".stripMargin)\n\n    val readerVersion = Math.max(feature.minReaderVersion, 1)\n    val expectedWriterFeatures =\n      Some(Set(feature.name, InvariantsTableFeature.name, AppendOnlyTableFeature.name))\n    val expectedReaderFeatures: Option[Set[String]] =\n      if (supportsReaderFeatures(readerVersion)) Some(Set(feature.name)) else None\n\n    assert(\n      deltaLog.update().protocol === Protocol(\n        minReaderVersion = Math.max(feature.minReaderVersion, 1),\n        minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n        readerFeatures = expectedReaderFeatures,\n        writerFeatures = expectedWriterFeatures))\n  }\n\n  /** Assumes there is at least 1 commit. */\n  def getEarliestCommitVersion(deltaLog: DeltaLog): Long =\n    deltaLog.listFrom(0L).collectFirst { case DeltaFile(_, v) => v }.get\n\n  def testWriterFeatureRemoval(\n      feature: TableFeature,\n      featurePropertyKey: String): Unit = {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      createTableWithFeature(deltaLog, feature, featurePropertyKey)\n\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        feature.name).run(spark)\n\n      // Writer feature is removed from the writer features set.\n      val snapshot = deltaLog.update()\n      assert(snapshot.protocol === Protocol(1, 2))\n      assert(!snapshot.metadata.configuration.contains(featurePropertyKey))\n      assertPropertiesAndShowTblProperties(deltaLog)\n    }\n  }\n\n  def truncateHistoryDefaultLogRetention: CalendarInterval =\n    DeltaConfigs.parseCalendarInterval(\n      DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION.defaultValue)\n\n  def testReaderFeatureRemoval(\n      feature: TableFeature,\n      featurePropertyKey: String,\n      advanceClockPastRetentionPeriod: Boolean = true,\n      truncateHistory: Boolean = false,\n      truncateHistoryRetentionOpt: Option[String] = None): Unit = {\n    withTempDir { dir =>\n      val truncateHistoryRetention = truncateHistoryRetentionOpt\n        .map(DeltaConfigs.parseCalendarInterval)\n        .getOrElse(truncateHistoryDefaultLogRetention)\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(deltaLog, feature, featurePropertyKey)\n\n      if (truncateHistoryRetentionOpt.nonEmpty) {\n        val propertyKey = DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION.key\n        AlterTableSetPropertiesDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          Map(propertyKey -> truncateHistoryRetention.toString)).run(spark)\n      }\n\n      // First attempt should cleanup feature traces but fail with a message due to historical\n      // log entries containing the feature.\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          feature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> feature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryRetention.toString))\n\n      // Add some more commits.\n      spark.range(0, 100).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      spark.range(100, 120).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // Table still contains historical data with the feature. Attempt should fail.\n      val e2 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          feature.name).run(spark)\n      }\n      checkError(\n        e2,\n        \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n        parameters = Map(\n          \"feature\" -> feature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryRetention.toString))\n\n      // Generate commit.\n      spark.range(120, 140).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // Pretend retention period has passed.\n      if (advanceClockPastRetentionPeriod) {\n        val clockAdvanceMillis = if (truncateHistory) {\n          DeltaConfigs.getMilliSeconds(truncateHistoryRetention) + TimeUnit.HOURS.toMillis(24)\n        } else {\n          deltaLog.deltaRetentionMillis(deltaLog.update().metadata) + TimeUnit.DAYS.toMillis(3)\n        }\n        clock.advance(clockAdvanceMillis)\n      }\n\n      val dropCommand = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        feature.name,\n        truncateHistory = truncateHistory)\n\n      if (advanceClockPastRetentionPeriod) {\n        // History is now clean. We should be able to remove the feature.\n        dropCommand.run(spark)\n\n        // Reader+writer feature is removed from the features set.\n        val snapshot = deltaLog.update()\n        assert(snapshot.protocol === Protocol(1, 2))\n        assert(!snapshot.metadata.configuration.contains(featurePropertyKey))\n        assertPropertiesAndShowTblProperties(deltaLog)\n      } else {\n        // When the clock did not advance the logs are not cleaned. We should detect there\n        // are still versions that contain traces of the feature.\n        val e3 = intercept[DeltaTableFeatureException] {\n          dropCommand.run(spark)\n        }\n        checkError(\n          e3,\n          \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n          parameters = Map(\n            \"feature\" -> feature.name,\n            \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n            \"logRetentionPeriod\" -> \"30 days\",\n            \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryRetention.toString))\n      }\n\n      // Verify commits before the checkpoint are cleaned.\n      val earliestExpectedCommitVersion =\n        if (advanceClockPastRetentionPeriod) {\n          deltaLog.findEarliestReliableCheckpoint.get\n        } else {\n          0L\n        }\n      assert(getEarliestCommitVersion(deltaLog) === earliestExpectedCommitVersion)\n\n      // Validate extra commits.\n      val table = io.delta.tables.DeltaTable.forPath(deltaLog.dataPath.toString)\n      assert(table.toDF.count() == 140)\n    }\n  }\n\n  test(\"Remove writer feature\") {\n    testWriterFeatureRemoval(\n      TestRemovableWriterFeature,\n      TestRemovableWriterFeature.TABLE_PROP_KEY)\n  }\n\n  test(\"Remove legacy writer feature\") {\n    testWriterFeatureRemoval(\n      TestRemovableLegacyWriterFeature,\n      TestRemovableLegacyWriterFeature.TABLE_PROP_KEY)\n  }\n\n\n  for {\n    advanceClockPastRetentionPeriod <- BOOLEAN_DOMAIN\n    truncateHistory <- if (advanceClockPastRetentionPeriod) BOOLEAN_DOMAIN else Seq(false)\n    retentionOpt <- if (truncateHistory) Seq(Some(\"12 hours\"), None) else Seq(None)\n  } test(s\"Remove reader+writer feature \" +\n      s\"advanceClockPastRetentionPeriod: $advanceClockPastRetentionPeriod \" +\n      s\"truncateHistory: $truncateHistory \" +\n      s\"retentionOpt: ${retentionOpt.getOrElse(\"None\")}\") {\n    testReaderFeatureRemoval(\n      TestRemovableReaderWriterFeature,\n      TestRemovableReaderWriterFeature.TABLE_PROP_KEY,\n      advanceClockPastRetentionPeriod,\n      truncateHistory,\n      retentionOpt)\n  }\n\n  test(\"Remove legacy reader+writer feature\") {\n    testReaderFeatureRemoval(\n      TestRemovableLegacyReaderWriterFeature,\n      TestRemovableLegacyReaderWriterFeature.TABLE_PROP_KEY)\n  }\n\n  test(\"Remove writer feature when table protocol does not support reader features\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES (\n             |delta.feature.${TestWriterFeature.name} = 'supported',\n             |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n             |)\"\"\".stripMargin)\n\n      val protocol = deltaLog.update().protocol\n      assert(protocol === protocolWithFeatures(\n        writerFeatures = Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          TestWriterFeature,\n          TestRemovableWriterFeature)))\n\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableWriterFeature.name)\n      command.run(spark)\n\n      assert(\n        deltaLog.update().protocol === Protocol(\n          minReaderVersion = 1,\n          minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures = None,\n          writerFeatures = Some(Set(\n            TestWriterFeature.name,\n            AppendOnlyTableFeature.name,\n            InvariantsTableFeature.name))))\n    }\n  }\n\n  test(\"Remove a non-removable feature\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES (\n             |delta.feature.${TestWriterMetadataNoAutoUpdateFeature.name} = 'supported'\n             |)\"\"\".stripMargin)\n\n      val expectedProtocol = protocolWithFeatures(writerFeatures = Seq(\n          TestWriterMetadataNoAutoUpdateFeature,\n          AppendOnlyTableFeature,\n          InvariantsTableFeature))\n      assert(deltaLog.update().protocol === expectedProtocol)\n\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestWriterMetadataNoAutoUpdateFeature.name)\n\n      val e = intercept[DeltaTableFeatureException] {\n        command.run(spark)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_NONREMOVABLE_FEATURE\",\n        parameters = Map(\"feature\" -> TestWriterMetadataNoAutoUpdateFeature.name))\n    }\n  }\n\n  test(\"Remove an implicit writer feature\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES (\n             |delta.minWriterVersion = 2)\"\"\".stripMargin)\n\n      assert(deltaLog.update().protocol === Protocol(minReaderVersion = 1, minWriterVersion = 2))\n\n      // Try removing AppendOnly which is an implicitly supported feature (writer version 2).\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        AppendOnlyTableFeature.name)\n      val e = intercept[DeltaTableFeatureException] {\n        command.run(spark)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_NONREMOVABLE_FEATURE\",\n        parameters = Map(\"feature\" -> AppendOnlyTableFeature.name))\n    }\n  }\n\n  test(\"Remove a feature not supported by the client\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\")\n\n      assert(\n        deltaLog.update().protocol === Protocol(\n          minReaderVersion = 1,\n          minWriterVersion = 2,\n          readerFeatures = None,\n          writerFeatures = None))\n\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        \"NonSupportedFeature\")\n\n      val e = intercept[DeltaTableFeatureException] {\n        command.run(spark)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_UNSUPPORTED_CLIENT_FEATURE\",\n        parameters = Map(\"feature\" -> \"NonSupportedFeature\"))\n    }\n  }\n\n  for (withTableFeatures <- BOOLEAN_DOMAIN)\n  test(s\"Remove a feature not present in the protocol - withTableFeatures: $withTableFeatures\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\")\n\n      assert(deltaLog.update().protocol === Protocol(\n          minReaderVersion = 1,\n          minWriterVersion = 2,\n          readerFeatures = None,\n          writerFeatures = None))\n\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableWriterFeature.name)\n\n      val e = intercept[DeltaTableFeatureException] {\n        command.run(spark)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\",\n        parameters = Map(\"feature\" -> TestRemovableWriterFeature.name))\n    }\n  }\n\n  test(\"Reintroduce a feature after removing it\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n             |TBLPROPERTIES (\n             |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n             |)\"\"\".stripMargin)\n\n      val expectedFeatures =\n        Seq(AppendOnlyTableFeature, InvariantsTableFeature, TestRemovableWriterFeature)\n      val protocol = deltaLog.update().protocol\n      assert(protocol === protocolWithFeatures(writerFeatures = expectedFeatures))\n\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableWriterFeature.name)\n      command.run(spark)\n      assert(deltaLog.update().protocol === Protocol(1, 2))\n\n      sql(s\"\"\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\n             |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n             |)\"\"\".stripMargin)\n\n      val expectedProtocolAfterReintroduction =\n        protocolWithFeatures(writerFeatures = expectedFeatures)\n      assert(deltaLog.update().protocol === expectedProtocolAfterReintroduction)\n    }\n  }\n\n  test(\"Remove a feature which is a dependency of other features\") {\n    // TestRemovableWriterFeatureWithDependency has two dependencies:\n    // 1. TestRemovableReaderWriterFeature\n    // 2. TestRemovableWriterFeature\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      // Scenario-1: Create a table with `TestRemovableWriterFeature` feature and validate that we\n      // can drop it.\n      sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n           |)\"\"\".stripMargin)\n\n      var protocol = deltaLog.update().protocol\n      assert(protocol === protocolWithFeatures(writerFeatures = Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        TestRemovableWriterFeature)))\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableWriterFeature.name).run(spark)\n      assert(deltaLog.update().protocol === Protocol(1, 2))\n\n      // Scenario-2: Create a table with `TestRemovableWriterFeatureWithDependency` feature. This\n      // will enable 2 dependent features also.\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\n           |delta.feature.${TestRemovableWriterFeatureWithDependency.name} = 'supported'\n           |)\"\"\".stripMargin)\n      protocol = deltaLog.update().protocol\n      Seq(\n        TestRemovableWriterFeatureWithDependency,\n        TestRemovableReaderWriterFeature,\n        TestRemovableWriterFeature\n      ).foreach(f => assert(protocol.isFeatureSupported(f)))\n      // Now we should not be able to drop `TestRemovableWriterFeature` as it is a dependency of\n      // `TestRemovableWriterFeatureWithDependency`.\n      // Although we should be able to drop `TestRemovableReaderWriterFeature` as it is not a\n      // dependency of any other feature.\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableWriterFeature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\",\n        parameters = Map(\n          \"feature\" -> TestRemovableWriterFeature.name,\n          \"dependentFeatures\" -> TestRemovableWriterFeatureWithDependency.name))\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableWriterFeatureWithDependency.name).run(spark)\n      protocol = deltaLog.update().protocol\n      assert(!protocol.isFeatureSupported(TestRemovableWriterFeatureWithDependency))\n      assert(protocol.isFeatureSupported(TestRemovableWriterFeature))\n      assert(protocol.isFeatureSupported(TestRemovableReaderWriterFeature))\n\n      // Once the dependent feature is removed, we should be able to drop\n      // `TestRemovableWriterFeature` also.\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableWriterFeature.name).run(spark)\n      protocol = deltaLog.update().protocol\n      assert(!protocol.isFeatureSupported(TestRemovableWriterFeatureWithDependency))\n      assert(!protocol.isFeatureSupported(TestRemovableWriterFeature))\n      assert(protocol.isFeatureSupported(TestRemovableReaderWriterFeature))\n    }\n  }\n\n  test(s\"Truncate history while dropping a writer feature\") {\n    withTempDir { dir =>\n      val table = s\"delta.`${dir.getCanonicalPath}`\"\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      createTableWithFeature(\n        deltaLog,\n        feature = TestRemovableWriterFeature,\n        featureProperty = TestRemovableWriterFeature.TABLE_PROP_KEY)\n\n      val e = intercept[DeltaTableFeatureException] {\n        sql(s\"\"\"ALTER TABLE $table\n               |DROP FEATURE ${TestRemovableWriterFeature.name}\n               |TRUNCATE HISTORY\"\"\".stripMargin)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\",\n        parameters = Map.empty)\n    }\n  }\n\n  test(\"Try removing reader+writer feature but re-enable feature after disablement\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(\n        deltaLog,\n        feature = TestRemovableReaderWriterFeature,\n        featureProperty = TestRemovableReaderWriterFeature.TABLE_PROP_KEY)\n\n      // Add some more commits.\n      spark.range(0, 100).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      spark.range(100, 120).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // First attempt should cleanup feature traces but fail with a message due to historical\n      // log entries containing the feature.\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableReaderWriterFeature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n      // Advance clock.\n      clock.advance(TimeUnit.DAYS.toMillis(1) + TimeUnit.HOURS.toMillis(24))\n\n      // Generate commit.\n      spark.range(120, 140).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // Add feature property again.\n      val v2Table = DeltaTableV2(spark, deltaLog.dataPath)\n      AlterTableSetPropertiesDeltaCommand(\n        v2Table,\n        Map(TestRemovableReaderWriterFeature.TABLE_PROP_KEY -> true.toString))\n        .run(spark)\n\n      // Feature was enabled again in the middle of the timeframe. The feature traces are\n      // are cleaned up again and we get a new \"Wait for retention period message.\"\n      val e2 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          table = DeltaTableV2(spark, deltaLog.dataPath),\n          featureName = TestRemovableReaderWriterFeature.name,\n          truncateHistory = true).run(spark)\n        }\n\n      checkError(\n        e2,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n    }\n  }\n\n  test(\"Remove reader+writer feature with shortened retention period\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(\n        deltaLog,\n        feature = TestRemovableReaderWriterFeature,\n        featureProperty = TestRemovableReaderWriterFeature.TABLE_PROP_KEY)\n\n      // First attempt should cleanup feature traces but fail with a message due to historical\n      // log entries containing the feature.\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableReaderWriterFeature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n      // Set retention period to a day.\n      AlterTableSetPropertiesDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        Map(DeltaConfigs.LOG_RETENTION.key -> \"1 DAY\")).run(spark)\n\n      // Metadata is not cleaned yet. Attempt should fail.\n      val e2 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableReaderWriterFeature.name).run(spark)\n      }\n      checkError(\n        e2,\n        \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"1 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n      spark.range(1, 100).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // Pretend retention period has passed.\n      clock.advance(\n        deltaLog.deltaRetentionMillis(deltaLog.update().metadata) + TimeUnit.DAYS.toMillis(3))\n\n      // History is now clean. We should be able to remove the feature.\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableReaderWriterFeature.name).run(spark)\n\n      // Verify commits before the checkpoint are cleaned.\n      val earliestExpectedCommitVersion = deltaLog.findEarliestReliableCheckpoint.get\n      assert(getEarliestCommitVersion(deltaLog) === earliestExpectedCommitVersion)\n\n      // Reader+writer feature is removed from the features set.\n      val snapshot = deltaLog.update()\n      assert(snapshot.protocol === Protocol(1, 2))\n      assert(!snapshot.metadata.configuration\n        .contains(TestRemovableReaderWriterFeature.TABLE_PROP_KEY))\n      assertPropertiesAndShowTblProperties(deltaLog)\n    }\n  }\n\n  test(\"Try removing reader+writer feature after restore\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(\n        deltaLog,\n        feature = TestRemovableReaderWriterFeature,\n        featureProperty = TestRemovableReaderWriterFeature.TABLE_PROP_KEY)\n\n      val preRemovalVersion = deltaLog.update().version\n\n      // Cleanup feature traces and throw message to wait retention period to expire.\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableReaderWriterFeature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n      // Add some more commits.\n      spark.range(0, 100).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      spark.range(100, 120).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // Restore table to an older version with feature traces.\n      sql(s\"RESTORE delta.`${deltaLog.dataPath}` TO VERSION AS OF $preRemovalVersion\")\n\n      // Drop command should detect that latest version has feature traces and run\n      // preDowngrade again.\n      val e2 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableReaderWriterFeature.name).run(spark)\n      }\n      checkError(\n        e2,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n    }\n  }\n\n  test(\"Remove reader+writer feature after unrelated metadata change\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(\n        deltaLog,\n        feature = TestRemovableReaderWriterFeature,\n        featureProperty = TestRemovableReaderWriterFeature.TABLE_PROP_KEY)\n\n      // First attempt should cleanup feature traces but fail with a message due to historical\n      // log entries containing the feature.\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableReaderWriterFeature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableReaderWriterFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n      // Add some more commits.\n      spark.range(0, 100).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n      spark.range(100, 120).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n      // Pretend retention period has passed.\n      clock.advance(\n        deltaLog.deltaRetentionMillis(deltaLog.update().metadata) + TimeUnit.DAYS.toMillis(3))\n\n      // Perform an unrelated metadata change.\n      sql(s\"ALTER TABLE delta.`${deltaLog.dataPath}` ADD COLUMN (value INT)\")\n\n      // The unrelated metadata change should not interfere with validation and we should\n      // be able to downgrade the protocol.\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        TestRemovableReaderWriterFeature.name).run(spark)\n\n      // Verify commits before the checkpoint are cleaned.\n      val earliestExpectedCommitVersion = deltaLog.findEarliestReliableCheckpoint.get\n      assert(getEarliestCommitVersion(deltaLog) === earliestExpectedCommitVersion)\n    }\n  }\n\n  for {\n    withCatalog <- BOOLEAN_DOMAIN\n    quoteWith <- if (withCatalog) Seq (\"none\", \"single\", \"backtick\") else Seq(\"none\")\n  } test(s\"Drop feature DDL - withCatalog=$withCatalog, quoteWith=$quoteWith\") {\n    withTempDir { dir =>\n      val table = if (withCatalog) \"table\" else s\"delta.`${dir.getCanonicalPath}`\"\n      if (withCatalog) sql(s\"DROP TABLE IF EXISTS $table\")\n      sql(\n        s\"\"\"CREATE TABLE $table (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n           |)\"\"\".stripMargin)\n\n      val deltaLog = if (withCatalog) {\n        DeltaLog.forTable(spark, TableIdentifier(table))\n      } else {\n        DeltaLog.forTable(spark, dir)\n      }\n\n      AlterTableSetPropertiesDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        Map(TestRemovableWriterFeature.TABLE_PROP_KEY -> \"true\")).run(spark)\n\n      val protocol = deltaLog.update().protocol\n      assert(protocol === protocolWithFeatures(writerFeatures = Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        TestRemovableWriterFeature)))\n\n      val logs = Log4jUsageLogger.track {\n        val featureName = quoteWith match {\n          case \"none\" => s\"${TestRemovableWriterFeature.name}\"\n          case \"single\" => s\"'${TestRemovableWriterFeature.name}'\"\n          case \"backtick\" => s\"`${TestRemovableWriterFeature.name}`\"\n        }\n        sql(s\"ALTER TABLE $table DROP FEATURE $featureName\")\n        assert(deltaLog.update().protocol === Protocol(1, 2))\n      }\n      // Test that the write downgrade command was invoked.\n      val expectedOpType = \"delta.test.TestWriterFeaturePreDowngradeCommand\"\n      val blob = logs.collectFirst {\n        case r if r.metric == MetricDefinitions.EVENT_TAHOE.name &&\n          r.tags.get(\"opType\").contains(expectedOpType) => r.blob\n      }\n      assert(blob.nonEmpty, s\"Expecting an '$expectedOpType' event but didn't see any.\")\n    }\n  }\n\n  for {\n    withCatalog <- BOOLEAN_DOMAIN\n    quoteWith <- if (withCatalog) Seq(\"none\", \"single\", \"backtick\") else Seq(\"none\")\n  } test(s\"Drop feature DDL TRUNCATE HISTORY - withCatalog=$withCatalog, quoteWith=$quoteWith\") {\n    withTempDir { dir =>\n      val table: String = if (withCatalog) {\n        s\"${spark.sessionState.catalog.getCurrentDatabase}.table\"\n      } else {\n        s\"delta.`${dir.getCanonicalPath}`\"\n      }\n      if (withCatalog) sql(s\"DROP TABLE IF EXISTS $table\")\n      sql(\n        s\"\"\"CREATE TABLE $table (id bigint) USING delta\n           |TBLPROPERTIES (\n           |delta.feature.${TestRemovableReaderWriterFeature.name} = 'supported',\n           |${TestRemovableReaderWriterFeature.TABLE_PROP_KEY} = \"true\",\n           |${DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION.key} = \"0 hours\"\n           |)\"\"\".stripMargin)\n\n      // We need to use a Delta log object with the ManualClock created in this test instead of\n      // the default SystemClock. However, we can't pass the Delta log to use directly in the SQL\n      // command. Instead, we will\n      //  1. Clear the Delta log cache to remove the log associated with table creation.\n      //  2. Populate the Delta log cache with the Delta log object that has the ManualClock we\n      //  want to use\n      // TODO(c27kwan): Refactor this and provide a better way to control clocks in Delta tests.\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = if (withCatalog) {\n        val tableIdentifier =\n          TableIdentifier(\"table\", Some(spark.sessionState.catalog.getCurrentDatabase))\n        // We need to hack the Delta log cache with path based access to setup the right key.\n        val path = DeltaLog.forTable(spark, tableIdentifier, clock).dataPath\n        DeltaLog.clearCache()\n        DeltaLog.forTable(spark, path, clock)\n      } else {\n        DeltaLog.clearCache()\n        DeltaLog.forTable(spark, dir, clock)\n      }\n\n      val protocol = deltaLog.update().protocol\n      assert(protocol === protocolWithFeatures(\n        readerFeatures = Seq(TestRemovableReaderWriterFeature),\n        writerFeatures = Seq(\n          AppendOnlyTableFeature,\n          InvariantsTableFeature,\n          TestRemovableReaderWriterFeature)))\n\n      val logs = Log4jUsageLogger.track {\n        val featureName = quoteWith match {\n          case \"none\" => s\"${TestRemovableReaderWriterFeature.name}\"\n          case \"single\" => s\"'${TestRemovableReaderWriterFeature.name}'\"\n          case \"backtick\" => s\"`${TestRemovableReaderWriterFeature.name}`\"\n        }\n\n        // Expect an exception when dropping a reader writer feature on a table that\n        // still has traces of the feature.\n        intercept[DeltaTableFeatureException] {\n          sql(s\"ALTER TABLE $table DROP FEATURE $featureName\")\n        }\n\n        // Move past retention period.\n        clock.advance(TimeUnit.HOURS.toMillis(48))\n\n        sql(s\"ALTER TABLE $table DROP FEATURE $featureName TRUNCATE HISTORY\")\n        assert(deltaLog.update().protocol === Protocol(1, 2))\n      }\n\n      // Validate the correct downgrade command was invoked.\n      val expectedOpType = \"delta.test.TestReaderWriterFeaturePreDowngradeCommand\"\n      val blob = logs.collectFirst {\n        case r if r.metric == MetricDefinitions.EVENT_TAHOE.name &&\n          r.tags.get(\"opType\").contains(expectedOpType) => r.blob\n      }\n      assert(blob.nonEmpty, s\"Expecting an '$expectedOpType' event but didn't see any.\")\n    }\n  }\n\n  for {\n    propertyName <- Seq(\"delta.enableRowTracking\", \"DELTA.enableRowTracking\",\n      \"delta.ENABLEROWTRACKING\", \"DELTA.ENABLEROWTRACKING\")\n  } test(s\"Drop a table property using drop feature should fail\" +\n    s\" - with propertyName=$propertyName\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\")\n\n      val command = AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        propertyName)\n\n      val e = intercept[DeltaTableFeatureException] {\n        command.run(spark)\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_FEATURE_IS_DELTA_PROPERTY\",\n        parameters = Map(\"property\" -> propertyName)\n      )\n    }\n  }\n\n  protected def testProtocolVersionDowngrade(\n      initialMinReaderVersion: Int,\n      initialMinWriterVersion: Int,\n      featuresToAdd: Seq[TableFeature],\n      featuresToRemove: Seq[TableFeature],\n      expectedDowngradedProtocol: Protocol,\n      truncateHistory: Boolean = false): Unit = {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      spark.sql(s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n                   |TBLPROPERTIES (\n                   |delta.minReaderVersion = $initialMinReaderVersion,\n                   |delta.minWriterVersion = $initialMinWriterVersion\n                   |)\"\"\".stripMargin)\n\n      // Upgrade protocol to table features.\n      val newTBLProperties = featuresToAdd\n        .map(f => s\"delta.feature.${f.name}='supported'\")\n        .reduce(_ + \", \" + _)\n      spark.sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getPath}`\n           |SET TBLPROPERTIES (\n           |$newTBLProperties\n           |)\"\"\".stripMargin)\n\n      for (feature <- featuresToRemove) {\n        AlterTableDropFeatureDeltaCommand(\n          table = DeltaTableV2(spark, deltaLog.dataPath),\n          featureName = feature.name,\n          truncateHistory = truncateHistory).run(spark)\n      }\n      assert(deltaLog.update().protocol === expectedDowngradedProtocol)\n    }\n  }\n\n  test(\"Downgrade protocol version (1, 4)\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 4,\n      featuresToAdd = Seq(TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 4))\n  }\n\n  // Initial minReader version is (2, 4), however, there are no legacy features that require\n  // reader version 2. Therefore, the protocol version is downgraded to (1, 4).\n  test(\"Downgrade protocol version (2, 4)\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 2,\n      initialMinWriterVersion = 4,\n      featuresToAdd = Seq(TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 4))\n  }\n\n  // Version (2, 5) enables column mapping which is a reader+writer feature and requires (2, 5).\n  // Therefore, to downgrade from table features we need at least (2, 5).\n  test(\"Downgrade protocol version (2, 5)\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 2,\n      initialMinWriterVersion = 5,\n      featuresToAdd = Seq(TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(2, 5))\n  }\n\n\n  test(\"Downgrade protocol version (1, 1)\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 1,\n      featuresToAdd = Seq(TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 1))\n  }\n\n  test(\"Downgrade protocol version on table created with (3, 7)\") {\n    // When the table is initialized with table features there are no active (implicit) legacy\n    // features. After removing the last table feature we downgrade back to (1, 1).\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 1))\n  }\n\n  test(\"Downgrade protocol version on table created with (1, 7)\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 1))\n  }\n\n  test(\"Protocol version downgrade on a table with table features and added legacy feature\") {\n    // Added legacy feature should be removed and the protocol should be downgraded to (2, 5).\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd =\n        Seq(TestRemovableWriterFeature) ++ Protocol(2, 5).implicitlySupportedFeatures,\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(2, 5))\n\n    // Added legacy feature should not be removed and the protocol should stay on (1, 7).\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableWriterFeature, TestRemovableLegacyWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 7)\n        .withFeature(TestRemovableLegacyWriterFeature))\n\n    // Legacy feature was manually removed. Protocol should be downgraded to (1, 1).\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableWriterFeature, TestRemovableLegacyWriterFeature),\n      featuresToRemove = Seq(TestRemovableLegacyWriterFeature, TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 1))\n\n    // Start with writer table features and add a legacy reader+writer feature.\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableWriterFeature, ColumnMappingTableFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(2, 7).withFeature(ColumnMappingTableFeature))\n\n    // Remove reader+writer legacy feature as well.\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableLegacyReaderWriterFeature, TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableLegacyReaderWriterFeature, TestRemovableWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 1))\n  }\n\n  test(\"Protocol version is not downgraded when writer features exist\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 1,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableWriterFeature, DomainMetadataTableFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = protocolWithWriterFeature(DomainMetadataTableFeature))\n  }\n\n  test(\"Protocol version is not downgraded when multiple reader+writer features exist\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableReaderWriterFeature, DeletionVectorsTableFeature),\n      featuresToRemove = Seq(TestRemovableReaderWriterFeature),\n      expectedDowngradedProtocol = protocolWithReaderFeature(DeletionVectorsTableFeature))\n  }\n\n  test(\"Protocol version is not downgraded when reader+writer features exist\") {\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd = Seq(TestRemovableReaderWriterFeature, TestRemovableWriterFeature),\n      featuresToRemove = Seq(TestRemovableWriterFeature),\n      expectedDowngradedProtocol = protocolWithReaderFeature(TestRemovableReaderWriterFeature))\n  }\n\n  for {\n    truncateHistory <- BOOLEAN_DOMAIN\n    enableCDF <- if (truncateHistory) Seq(false) else BOOLEAN_DOMAIN\n  } test(s\"Remove Deletion Vectors feature \" +\n      s\"truncateHistory: $truncateHistory, enableCDF: $enableCDF\") {\n    val targetDF = spark.range(start = 0, end = 100, step = 1, numPartitions = 2)\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\",\n        DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> enableCDF.toString) {\n      withTempPath { dir =>\n        val clock = new ManualClock(System.currentTimeMillis())\n        val targetLog = DeltaLog.forTable(spark, dir, clock)\n        val defaultRetentionPeriod =\n          DeltaConfigs.LOG_RETENTION.fromMetaData(targetLog.update().metadata).toString\n\n        targetDF.write.format(\"delta\").save(dir.toString)\n\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.toString)\n\n        // Add some DVs.\n        targetTable.delete(\"id >= 90\")\n\n        // Assert that DVs exist.\n        val preDowngradeSnapshot = targetLog.update()\n        assert(DeletionVectorUtils.deletionVectorsWritable(preDowngradeSnapshot))\n        assert(preDowngradeSnapshot.numDeletionVectorsOpt === Some(1L))\n\n        // Attempting to drop Deletion Vectors feature will prohibit adding new DVs and remove\n        // all DVs from the latest snapshot, but ultimately fail, because history will still\n        // contain traces of the feature. For this reason, we have to wait for the retention period\n        // to be over before we can downgrade the protocol.\n        val e1 = intercept[DeltaTableFeatureException] {\n          dropDVTableFeature(spark, targetLog, truncateHistory = false)\n        }\n        checkError(\n          e1,\n          \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n          parameters = Map(\n            \"feature\" -> DeletionVectorsTableFeature.name,\n            \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n            \"logRetentionPeriod\" -> defaultRetentionPeriod,\n            \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n        val postCleanupSnapshot = targetLog.update()\n        assert(!DeletionVectorUtils.deletionVectorsWritable(postCleanupSnapshot))\n        assert(postCleanupSnapshot.numDeletionVectorsOpt.getOrElse(0L) === 0)\n        assert(postCleanupSnapshot.numDeletedRecordsOpt.getOrElse(0L) === 0)\n\n        spark.range(100, 120).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n        spark.range(120, 140).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n        // Table still contains historical data with DVs. Attempt should fail.\n        val e2 = intercept[DeltaTableFeatureException] {\n          dropDVTableFeature(spark, targetLog, truncateHistory = false)\n        }\n        checkError(\n          e2,\n          \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n          parameters = Map(\n            \"feature\" -> DeletionVectorsTableFeature.name,\n            \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n            \"logRetentionPeriod\" -> defaultRetentionPeriod,\n            \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n        // Pretend retention period has passed.\n        val clockAdvanceMillis = if (truncateHistory) {\n          DeltaConfigs.getMilliSeconds(truncateHistoryDefaultLogRetention) +\n            TimeUnit.HOURS.toMillis(24)\n        } else {\n          targetLog.deltaRetentionMillis(targetLog.update().metadata) + TimeUnit.DAYS.toMillis(3)\n        }\n        clock.advance(clockAdvanceMillis)\n\n        // Cleanup logs.\n        targetLog.cleanUpExpiredLogs(targetLog.update())\n\n        // History is now clean. We should be able to remove the feature.\n        dropDVTableFeature(spark, targetLog, truncateHistory)\n\n        val postDowngradeSnapshot = targetLog.update()\n        val protocol = postDowngradeSnapshot.protocol\n        assert(!DeletionVectorUtils.deletionVectorsWritable(postDowngradeSnapshot))\n        assert(postDowngradeSnapshot.numDeletionVectorsOpt.getOrElse(0L) === 0)\n        assert(postDowngradeSnapshot.numDeletedRecordsOpt.getOrElse(0L) === 0)\n        assert(!protocol.readerFeatureNames.contains(DeletionVectorsTableFeature.name))\n      }\n    }\n  }\n\n\n  test(\"Can drop reader+writer feature when there is nothing to clean\") {\n    withTempPath { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val targetLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(\n        targetLog,\n        TestRemovableReaderWriterFeature,\n        TestRemovableReaderWriterFeature.TABLE_PROP_KEY)\n\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getPath}` SET TBLPROPERTIES (\n           |'${TestRemovableReaderWriterFeature.TABLE_PROP_KEY}'='false'\n           |)\"\"\".stripMargin)\n\n      // Pretend retention period has passed.\n      val clockAdvanceMillis = DeltaConfigs.getMilliSeconds(truncateHistoryDefaultLogRetention)\n      clock.advance(clockAdvanceMillis + TimeUnit.HOURS.toMillis(24))\n\n      // History is now clean. We should be able to remove the feature.\n      AlterTableDropFeatureDeltaCommand(\n        DeltaTableV2(spark, targetLog.dataPath),\n        TestRemovableReaderWriterFeature.name,\n        truncateHistory = true).run(spark)\n\n      assert(targetLog.update().protocol == Protocol(1, 2))\n    }\n  }\n\n  for (truncateHistory <- BOOLEAN_DOMAIN)\n  test(s\"Protocol version downgrade with Table Features - Basic test \" +\n      s\"truncateHistory: ${truncateHistory}\") {\n    val expectedFeatures = Seq(RowTrackingFeature, DomainMetadataTableFeature)\n\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd = expectedFeatures :+ TestRemovableReaderWriterFeature,\n      featuresToRemove = Seq(TestRemovableReaderWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 7).withFeatures(expectedFeatures),\n      truncateHistory = truncateHistory)\n  }\n\n  for (truncateHistory <- BOOLEAN_DOMAIN)\n  test(s\"Protocol version downgrade with Table Features - include legacy writer features: \" +\n      s\"truncateHistory: ${truncateHistory}\") {\n    val expectedFeatures =\n      Seq(DomainMetadataTableFeature, ChangeDataFeedTableFeature, AppendOnlyTableFeature)\n\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd = expectedFeatures :+ TestRemovableReaderWriterFeature,\n      featuresToRemove = Seq(TestRemovableReaderWriterFeature),\n      expectedDowngradedProtocol = Protocol(1, 7).withFeatures(expectedFeatures),\n      truncateHistory = truncateHistory)\n  }\n\n  for (truncateHistory <- BOOLEAN_DOMAIN)\n  test(s\"Protocol version downgrade with Table Features - include legacy reader features: \" +\n    s\"truncateHistory: ${truncateHistory}\") {\n    val expectedFeatures =\n      Seq(DomainMetadataTableFeature, ChangeDataFeedTableFeature, ColumnMappingTableFeature)\n\n    testProtocolVersionDowngrade(\n      initialMinReaderVersion = 3,\n      initialMinWriterVersion = 7,\n      featuresToAdd = expectedFeatures :+ TestRemovableReaderWriterFeature,\n      featuresToRemove = Seq(TestRemovableReaderWriterFeature),\n      expectedDowngradedProtocol = Protocol(2, 7).withFeatures(expectedFeatures),\n      truncateHistory = truncateHistory)\n  }\n\n  for (truncateHistory <- BOOLEAN_DOMAIN)\n  test(\"Writer features that require history validation/truncation.\" +\n      s\" - truncateHistory: $truncateHistory\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(System.currentTimeMillis())\n      val deltaLog = DeltaLog.forTable(spark, dir, clock)\n\n      createTableWithFeature(deltaLog,\n        TestRemovableWriterWithHistoryTruncationFeature,\n        TestRemovableWriterWithHistoryTruncationFeature.TABLE_PROP_KEY)\n\n      // Add some data.\n      spark.range(100).write.format(\"delta\").mode(\"overwrite\").save(dir.getCanonicalPath)\n\n      val e1 = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          TestRemovableWriterWithHistoryTruncationFeature.name).run(spark)\n      }\n      checkError(\n        e1,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> TestRemovableWriterWithHistoryTruncationFeature.name,\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\"))\n\n      // Pretend retention period has passed.\n      val clockAdvanceMillis = if (truncateHistory) {\n        DeltaConfigs.getMilliSeconds(truncateHistoryDefaultLogRetention) +\n          TimeUnit.HOURS.toMillis(24)\n      } else {\n        deltaLog.deltaRetentionMillis(deltaLog.update().metadata) + TimeUnit.DAYS.toMillis(3)\n      }\n      clock.advance(clockAdvanceMillis)\n\n      AlterTableDropFeatureDeltaCommand(\n        table = DeltaTableV2(spark, deltaLog.dataPath),\n        featureName = TestRemovableWriterWithHistoryTruncationFeature.name,\n        truncateHistory = truncateHistory).run(spark)\n\n      assert(deltaLog.update().protocol === Protocol(1, 2))\n    }\n  }\n\n  private def dropV2CheckpointsTableFeature(spark: SparkSession, log: DeltaLog): Unit = {\n    spark.sql(s\"ALTER TABLE delta.`${log.dataPath}` DROP FEATURE \" +\n      s\"`${V2CheckpointTableFeature.name}`\")\n  }\n\n  private def testV2CheckpointTableFeatureDrop(\n      v2CheckpointFormat: V2Checkpoint.Format,\n      withInitialV2Checkpoint: Boolean,\n      forceMultiPartCheckpoint: Boolean = false): Unit = {\n    var confs = Seq(\n      DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n      DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> v2CheckpointFormat.name\n    )\n    val expectedClassicCheckpointType = if (forceMultiPartCheckpoint) {\n      confs :+= DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"1\"\n      CheckpointInstance.Format.WITH_PARTS\n    } else {\n      CheckpointInstance.Format.SINGLE\n    }\n    withSQLConf(confs: _*) {\n      withTempPath { dir =>\n        val clock = new ManualClock(System.currentTimeMillis())\n        val targetLog = DeltaLog.forTable(spark, dir, clock)\n        val defaultRetentionPeriod =\n          DeltaConfigs.LOG_RETENTION.fromMetaData(targetLog.update().metadata).toString\n\n        val targetDF = spark.range(start = 0, end = 100, step = 1, numPartitions = 2)\n        targetDF.write.format(\"delta\").save(dir.toString)\n\n        val initialCheckpointCount = if (withInitialV2Checkpoint) 1 else 0\n\n        if (withInitialV2Checkpoint) {\n          // Create a v2 checkpoint.\n          targetLog.checkpoint()\n        }\n\n        // Assert that the current checkpointing policy requires v2 checkpoint support.\n        val preDowngradeSnapshot = targetLog.update()\n        assert(\n          DeltaConfigs.CHECKPOINT_POLICY\n            .fromMetaData(preDowngradeSnapshot.metadata)\n            .needsV2CheckpointSupport)\n        val checkpointFiles = targetLog.listFrom(0).filter(FileNames.isCheckpointFile)\n        assert(checkpointFiles.length == initialCheckpointCount)\n        checkpointFiles.foreach { f =>\n          assert(CheckpointInstance(f.getPath).format == CheckpointInstance.Format.V2)\n        }\n\n        // Dropping the feature should fail because\n        // 1. The checkpointing policy in metadata requires v2 checkpoint support.\n        // 2. Also, when initialCheckpointCount = true, there is a v2 checkpoint.\n        val e1 = intercept[DeltaTableFeatureException] {\n          dropV2CheckpointsTableFeature(spark, targetLog)\n        }\n        checkError(\n          e1,\n          \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n          parameters = Map(\n            \"feature\" -> V2CheckpointTableFeature.name,\n            \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n            \"logRetentionPeriod\" -> defaultRetentionPeriod,\n            \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n        val postCleanupCheckpointFiles =\n          targetLog.listFrom(0).filter(FileNames.isCheckpointFile).toList\n\n        // Assert that a new classic checkpoint has been created.\n        val uniqueCheckpointCount = postCleanupCheckpointFiles\n          .drop(initialCheckpointCount)\n          .map { checkpointFile =>\n            val checkpointInstance = CheckpointInstance(checkpointFile.getPath)\n\n            assert(checkpointInstance.format == expectedClassicCheckpointType)\n\n            checkpointInstance.version\n          }\n          // Count a multi-part checkpoint as a single checkpoint.\n          .toSet.size\n        // Drop feature command generates one classic checkpoints after v2 checkpoint cleanup.\n        val expectedClassicCheckpointCount = 1\n        assert(uniqueCheckpointCount == expectedClassicCheckpointCount)\n\n        spark.range(100, 120).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n        // V2 Checkpoint related traces have not been cleaned up yet. Attempt should fail.\n        val e2 = intercept[DeltaTableFeatureException] {\n          dropV2CheckpointsTableFeature(spark, targetLog)\n        }\n        checkError(\n          e2,\n          \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n          parameters = Map(\n            \"feature\" -> V2CheckpointTableFeature.name,\n            \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n            \"logRetentionPeriod\" -> defaultRetentionPeriod,\n            \"truncateHistoryLogRetentionPeriod\" -> truncateHistoryDefaultLogRetention.toString))\n\n        // Pretend retention period has passed.\n        clock.advance(\n          targetLog.deltaRetentionMillis(targetLog.update().metadata) + TimeUnit.DAYS.toMillis(3))\n\n        // History is now clean. We should be able to remove the feature.\n        dropV2CheckpointsTableFeature(spark, targetLog)\n\n        val postDowngradeSnapshot = targetLog.update()\n        val protocol = postDowngradeSnapshot.protocol\n        assert(!protocol.readerFeatureNames.contains(V2CheckpointTableFeature.name))\n        assert(\n          !DeltaConfigs.CHECKPOINT_POLICY\n            .fromMetaData(postDowngradeSnapshot.metadata)\n            .needsV2CheckpointSupport)\n        assert(targetLog.listFrom(0).filter(FileNames.isCheckpointFile).forall { f =>\n          CheckpointInstance(f.getPath).format == expectedClassicCheckpointType\n        })\n      }\n    }\n  }\n\n  for (\n    v2CheckpointFormat <- V2Checkpoint.Format.ALL;\n    withInitialV2Checkpoint <- BOOLEAN_DOMAIN)\n  test(s\"Remove v2 Checkpoints Feature [v2CheckpointFormat: ${v2CheckpointFormat.name}; \" +\n      s\"withInitialV2Checkpoint: $withInitialV2Checkpoint; forceMultiPartCheckpoint: false]\") {\n    testV2CheckpointTableFeatureDrop(v2CheckpointFormat, withInitialV2Checkpoint)\n  }\n\n  test(\n    s\"Remove v2 Checkpoints Feature [v2CheckpointFormat: ${V2Checkpoint.Format.PARQUET.name}; \" +\n      s\"withInitialV2Checkpoint: true; forceMultiPartCheckpoint: true]\") {\n    testV2CheckpointTableFeatureDrop(V2Checkpoint.Format.PARQUET, true, true)\n  }\n\n  private def testRemoveVacuumProtocolCheckTableFeature(\n      enableFeatureInitially: Boolean,\n      additionalTableProperties: Seq[(String, String)] = Seq.empty,\n      downgradeFailsWithException: Option[String] = None,\n      featureExpectedAtTheEnd: Boolean = false): Unit = {\n    val featureName = VacuumProtocolCheckTableFeature.name\n    withTempTable(createTable = false) { tableName =>\n      // Register a temporary InMemory-CC builder to support CatalogOwned table creation.\n      CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n      CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n        catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING,\n        TrackingInMemoryCommitCoordinatorBuilder(batchSize = 1)\n      )\n      val finalAdditionalTableProperty = if (enableFeatureInitially) {\n        additionalTableProperties ++\n          Seq((s\"$FEATURE_PROP_PREFIX${featureName}\", \"supported\"))\n      } else {\n        additionalTableProperties\n      }\n      var additionalTablePropertyString =\n        finalAdditionalTableProperty.map { case (k, v) => s\"'$k' = '$v'\" }.mkString(\", \")\n      if (additionalTablePropertyString.nonEmpty) {\n        additionalTablePropertyString = s\", $additionalTablePropertyString\"\n      }\n      sql(\n        s\"\"\"CREATE TABLE $tableName (id bigint) USING delta\n           |TBLPROPERTIES (\n           |  delta.minReaderVersion = $TABLE_FEATURES_MIN_READER_VERSION,\n           |  delta.minWriterVersion = $TABLE_FEATURES_MIN_WRITER_VERSION\n           |  $additionalTablePropertyString\n           |)\"\"\".stripMargin)\n\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      val protocol = snapshot.protocol\n      assert(protocol.minReaderVersion ==\n        (if (enableFeatureInitially) TABLE_FEATURES_MIN_READER_VERSION else 1))\n      assert(protocol.minWriterVersion ==\n        (if (enableFeatureInitially) TABLE_FEATURES_MIN_WRITER_VERSION else 1))\n      assert(protocol.readerFeatures.isDefined === enableFeatureInitially)\n      downgradeFailsWithException match {\n        case Some(exceptionClass) =>\n          val e = intercept[DeltaTableFeatureException] {\n            AlterTableDropFeatureDeltaCommand(DeltaTableV2(spark, deltaLog.dataPath), featureName)\n              .run(spark)\n          }\n          assert(e.getErrorClass == exceptionClass)\n        case None =>\n          AlterTableDropFeatureDeltaCommand(DeltaTableV2(spark, deltaLog.dataPath), featureName)\n            .run(spark)\n      }\n      val latestProtocolReaderFeatures = deltaLog.update().protocol.readerFeatures.getOrElse(Set())\n      assert(\n        latestProtocolReaderFeatures.contains(VacuumProtocolCheckTableFeature.name) ===\n          featureExpectedAtTheEnd)\n      assertPropertiesAndShowTblProperties(deltaLog, tableHasFeatures = featureExpectedAtTheEnd)\n    }\n  }\n\n  test(\"Remove VacuumProtocolCheckTableFeature when it was enabled\") {\n    testRemoveVacuumProtocolCheckTableFeature(enableFeatureInitially = true)\n  }\n\n  test(\"Removing VacuumProtocolCheckTableFeature should fail when dependent feature \" +\n      \"Catalog Owned is enabled\") {\n    testRemoveVacuumProtocolCheckTableFeature(\n      enableFeatureInitially = true,\n      additionalTableProperties = Seq(\n        (s\"$FEATURE_PROP_PREFIX${CatalogOwnedTableFeature.name}\", \"supported\")),\n      downgradeFailsWithException = Some(\"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\"),\n      featureExpectedAtTheEnd = true)\n  }\n\n  test(\"Removing VacuumProtocolCheckTableFeature should fail when it is not enabled\") {\n    testRemoveVacuumProtocolCheckTableFeature(\n      enableFeatureInitially = false,\n      downgradeFailsWithException = Some(\"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\")\n    )\n  }\n\n  private def validateICTRemovalMetrics(\n      usageLogs: Seq[UsageRecord],\n      expectEnablementProperty: Boolean,\n      expectProvenanceTimestampProperty: Boolean,\n      expectProvenanceVersionProperty: Boolean): Unit = {\n    val dropFeatureBlob = usageLogs\n      .find(_.tags.get(\"opType\").contains(\"delta.inCommitTimestampFeatureRemovalMetrics\"))\n      .getOrElse(fail(\"Expected a log for inCommitTimestampFeatureRemovalMetrics\"))\n    val blob = JsonUtils.fromJson[Map[String, String]](dropFeatureBlob.blob)\n    assert(blob.contains(\"downgradeTimeMs\"))\n    val traceRemovalNeeded = expectEnablementProperty || expectProvenanceTimestampProperty ||\n      expectProvenanceVersionProperty\n    assert(blob.get(\"traceRemovalNeeded\").contains(traceRemovalNeeded.toString))\n    assert(blob\n      .get(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key)\n      .contains(expectEnablementProperty.toString))\n    assert(blob\n      .get(DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.key)\n      .contains(expectProvenanceTimestampProperty.toString))\n    assert(blob\n      .get(DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key)\n      .contains(expectProvenanceVersionProperty.toString))\n  }\n\n  test(\"drop InCommitTimestamp -- ICT enabled from commit 0\") {\n    withTempDir { dir =>\n      val featureEnablementKey = DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key\n      spark.sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\" +\n        s\" TBLPROPERTIES ('${featureEnablementKey}' = 'true')\")\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val featurePropertyKey = InCommitTimestampTableFeature.name\n\n      val usageLogs = Log4jUsageLogger.track {\n        AlterTableDropFeatureDeltaCommand(\n            DeltaTableV2(spark, deltaLog.dataPath),\n            featurePropertyKey)\n          .run(spark)\n      }\n\n      val snapshot = deltaLog.update()\n      // Writer feature is removed from the writer features set.\n      assert(!snapshot.protocol.writerFeatureNames.contains(featurePropertyKey))\n      assert(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata))\n      validateICTRemovalMetrics(\n        usageLogs,\n        expectEnablementProperty = true,\n        expectProvenanceTimestampProperty = false,\n        expectProvenanceVersionProperty = false)\n\n      // Running the command again should throw an exception.\n      val e = intercept[DeltaTableFeatureException] {\n        AlterTableDropFeatureDeltaCommand(\n            DeltaTableV2(spark, deltaLog.dataPath),\n            featurePropertyKey)\n          .run(spark)\n      }\n      assert(e.getErrorClass == \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\")\n    }\n  }\n\n  test(\"drop InCommitTimestamp -- ICT enabled after commit 0\") {\n    withTempDir { dir =>\n      val featureEnablementKey = DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key\n      val featurePropertyKey = InCommitTimestampTableFeature.name\n      sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta \" +\n        s\"TBLPROPERTIES ('${featureEnablementKey}' = 'false')\")\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      assert(!deltaLog.snapshot.metadata.configuration.contains(featurePropertyKey))\n\n      sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` \" +\n        s\"SET TBLPROPERTIES ('${featureEnablementKey}' = 'true')\")\n      val snapshotV1 = deltaLog.update()\n      assert(snapshotV1.protocol.writerFeatureNames.contains(featurePropertyKey))\n      assert(snapshotV1.metadata.configuration.contains(featureEnablementKey))\n      val ictProvenanceProperties = Seq(\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key,\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.key)\n      ictProvenanceProperties.foreach(prop =>\n        assert(snapshotV1.metadata.configuration.contains(prop)))\n\n      val usageLogs = Log4jUsageLogger.track {\n        AlterTableDropFeatureDeltaCommand(\n            DeltaTableV2(spark, deltaLog.dataPath),\n            featurePropertyKey)\n          .run(spark)\n      }\n\n      val snapshot = deltaLog.update()\n      // Writer feature is removed from the writer features set.\n      assert(!snapshot.protocol.writerFeatureNames.contains(featurePropertyKey))\n      assert(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata))\n      // The provenance properties should also have been removed.\n      ictProvenanceProperties.foreach(prop =>\n        assert(!snapshot.metadata.configuration.contains(prop)))\n      validateICTRemovalMetrics(\n        usageLogs,\n        expectEnablementProperty = true,\n        expectProvenanceTimestampProperty = true,\n        expectProvenanceVersionProperty = true)\n    }\n  }\n\n  test(\"drop InCommitTimestamp --- only one table property\") {\n    withTempDir { dir =>\n      // Dropping the ICT table feature should also remove any ICT provenance\n      // table properties even when the ICT enablement table property is not present.\n      spark.sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\" +\n          s\" TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      // Remove the enablement property.\n      AlterTableUnsetPropertiesDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        Seq(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key),\n        ifExists = true).run(spark)\n      // Set the IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION property.\n      AlterTableSetPropertiesDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        Map(DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key -> \"1\")).run(spark)\n      val snapshot1 = deltaLog.update()\n      assert(snapshot1.protocol.writerFeatureNames.contains(InCommitTimestampTableFeature.name))\n      // Ensure that the enablement property is not set.\n      assert(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot1.metadata))\n      assert(snapshot1.metadata.configuration.contains(\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key))\n\n      val usageLogs = Log4jUsageLogger.track {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          InCommitTimestampTableFeature.name)\n          .run(spark)\n      }\n      val snapshot2 = deltaLog.update()\n      assert(!snapshot2.protocol.writerFeatureNames.contains(InCommitTimestampTableFeature.name))\n      assert(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot2.metadata))\n      assert(!snapshot2.metadata.configuration.contains(\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key))\n      validateICTRemovalMetrics(\n        usageLogs,\n        expectEnablementProperty = false,\n        expectProvenanceTimestampProperty = false,\n        expectProvenanceVersionProperty = true)\n    }\n  }\n\n  test(\"drop InCommitTimestamp --- no table property\") {\n    withTempDir { dir =>\n      spark.sql(\n        s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\" +\n          s\" TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      // Remove the enablement property.\n      AlterTableUnsetPropertiesDeltaCommand(\n        DeltaTableV2(spark, deltaLog.dataPath),\n        Seq(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key),\n        ifExists = true).run(spark)\n\n      val usageLogs = Log4jUsageLogger.track {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, deltaLog.dataPath),\n          InCommitTimestampTableFeature.name)\n          .run(spark)\n      }\n      val snapshot = deltaLog.update()\n      assert(!snapshot.protocol.writerFeatureNames.contains(InCommitTimestampTableFeature.name))\n      assert(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata))\n      validateICTRemovalMetrics(\n        usageLogs,\n        expectEnablementProperty = false,\n        expectProvenanceTimestampProperty = false,\n        expectProvenanceVersionProperty = false)\n    }\n  }\n\n  // ---- Coordinated Commits Drop Feature Tests ----\n  private def setUpCoordinatedCommitsTable(dir: File, mcBuilder: CommitCoordinatorBuilder): Unit = {\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n    CommitCoordinatorProvider.registerBuilder(mcBuilder)\n    val tablePath = dir.getAbsolutePath\n    val log = DeltaLog.forTable(spark, tablePath)\n    val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n    val commitCoordinatorConf =\n      Map(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> mcBuilder.getName)\n    val newMetadata = Metadata().copy(configuration = commitCoordinatorConf)\n    log.startTransaction().commitManually(newMetadata)\n    assert(log.unsafeVolatileSnapshot.version === 0)\n    assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName ===\n      Some(mcBuilder.getName))\n    assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n    assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n    // upgrade commit always filesystem based\n    assert(fs.exists(FileNames.unsafeDeltaFile(log.logPath, 0)))\n\n    // Do a couple of commits on the coordinated-commits table\n    (1 to 2).foreach { version =>\n      log.startTransaction()\n        .commitManually(DeltaTestUtils.createTestAddFile(s\"$version\"))\n      assert(log.unsafeVolatileSnapshot.version === version)\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName.nonEmpty)\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorConf === Map.empty)\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n    }\n  }\n\n  private def validateCoordinatedCommitsDropLogs(\n      usageLogs: Seq[UsageRecord],\n      expectTablePropertiesPresent: Boolean,\n      expectUnbackfilledCommitsPresent: Boolean,\n      exceptionMessageOpt: Option[String] = None): Unit = {\n    val dropFeatureBlob = usageLogs\n      .find(_.tags.get(\"opType\").contains(\"delta.coordinatedCommitsFeatureRemovalMetrics\"))\n      .getOrElse(fail(\"Expected a log for coordinatedCommitsFeatureRemovalMetrics\"))\n    val blob = JsonUtils.fromJson[Map[String, String]](dropFeatureBlob.blob)\n    assert(blob.contains(\"downgradeTimeMs\"))\n    val expectTraceRemovalNeeded = expectTablePropertiesPresent || expectUnbackfilledCommitsPresent\n    assert(blob.get(\"traceRemovalNeeded\").contains(expectTraceRemovalNeeded.toString))\n    Seq(\n        DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key,\n        DeltaConfigs.COORDINATED_COMMITS_TABLE_CONF.key).foreach { prop =>\n      assert(blob.get(prop).contains(expectTablePropertiesPresent.toString))\n    }\n    // COORDINATED_COMMITS_COORDINATOR_CONF is not used by \"in-memory\" commit coordinator.\n    assert(blob\n      .get(\"postDisablementUnbackfilledCommitsPresent\")\n      .contains(expectUnbackfilledCommitsPresent.toString))\n    assert(\n      blob.get(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key).contains(\"false\"))\n    assert(blob.get(\"traceRemovalSuccess\").contains(exceptionMessageOpt.isEmpty.toString))\n    exceptionMessageOpt.foreach { exceptionMessage =>\n      assert(blob.get(\"traceRemovalException\").contains(exceptionMessage))\n    }\n  }\n\n  test(\"basic coordinated commits feature drop\") {\n    withTempDir { dir =>\n      val mcBuilder = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 1000)\n      setUpCoordinatedCommitsTable(dir, mcBuilder)\n      val log = DeltaLog.forTable(spark, dir)\n      val usageLogs = Log4jUsageLogger.track {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, log.dataPath),\n          CoordinatedCommitsTableFeature.name)\n          .run(spark)\n      }\n      val snapshot = log.update()\n      assert(!CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS.exists(\n        snapshot.metadata.configuration.contains(_)))\n      assert(!snapshot.protocol.writerFeatures.exists(\n        _.contains(CoordinatedCommitsTableFeature.name)))\n      validateCoordinatedCommitsDropLogs(\n        usageLogs, expectTablePropertiesPresent = true, expectUnbackfilledCommitsPresent = false)\n    }\n  }\n\n  test(\"backfill failure during coordinated commits feature drop\") {\n    withTempDir { dir =>\n      var shouldFailBackfill = true\n      val alternatingFailureBackfillClient =\n        new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(1000) {\n          override def backfillToVersion(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              startVersion: Long,\n              endVersionOpt: java.lang.Long): Unit = {\n            // Backfill fails on every other attempt.\n            if (shouldFailBackfill) {\n              shouldFailBackfill = !shouldFailBackfill\n              throw new IllegalStateException(\"backfill failed\")\n            } else {\n              super.backfillToVersion(\n                logStore,\n                hadoopConf,\n                tableDesc,\n                startVersion,\n                endVersionOpt)\n            }\n          }\n        })\n      val mcBuilder =\n        TrackingInMemoryCommitCoordinatorBuilder(100, Some(alternatingFailureBackfillClient))\n      setUpCoordinatedCommitsTable(dir, mcBuilder)\n      val log = DeltaLog.forTable(spark, dir)\n      val usageLogs = Log4jUsageLogger.track {\n        val e = intercept[IllegalStateException] {\n          AlterTableDropFeatureDeltaCommand(\n            DeltaTableV2(spark, log.dataPath),\n            CoordinatedCommitsTableFeature.name)\n            .run(spark)\n        }\n\n        assert(e.getMessage.contains(\"backfill failed\"))\n      }\n      validateCoordinatedCommitsDropLogs(\n        usageLogs,\n        expectTablePropertiesPresent = true,\n        expectUnbackfilledCommitsPresent = false,\n        exceptionMessageOpt = Some(\"backfill failed\"))\n      def backfilledCommitExists(v: Long): Boolean = {\n        val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n        fs.exists(FileNames.unsafeDeltaFile(log.logPath, v))\n      }\n      // Backfill of the commit which disables coordinated commits failed.\n      assert(!backfilledCommitExists(3))\n      // The commit coordinator still tracks the commit that disables it.\n      val commitsFromCommitCoordinator =\n        log.snapshot.tableCommitCoordinatorClientOpt.get.getCommits(Some(3L))\n      assert(commitsFromCommitCoordinator.getCommits.asScala.exists(_.getVersion == 3))\n      // The next drop attempt will also trigger an explicit backfill.\n      val usageLogs2 = Log4jUsageLogger.track {\n        AlterTableDropFeatureDeltaCommand(\n          DeltaTableV2(spark, log.dataPath),\n          CoordinatedCommitsTableFeature.name)\n          .run(spark)\n      }\n      validateCoordinatedCommitsDropLogs(\n        usageLogs2, expectTablePropertiesPresent = false, expectUnbackfilledCommitsPresent = true)\n      val snapshot = log.update()\n      assert(snapshot.version === 4)\n      assert(backfilledCommitExists(3))\n      // The protocol downgrade commit is performed through logstore directly.\n      assert(backfilledCommitExists(4))\n      assert(!CoordinatedCommitsUtils.TABLE_PROPERTY_KEYS.exists(\n          snapshot.metadata.configuration.contains(_)))\n      assert(!snapshot.protocol.writerFeatures.exists(\n        _.contains(CoordinatedCommitsTableFeature.name)))\n    }\n  }\n  // ---- End Coordinated Commits Drop Feature Tests ----\n\n  private def testRedirectFeature(\n      redirectFeature: TableFeature,\n      tableRedirect: TableRedirect,\n      enableFastDrop: Boolean,\n      unsetTableProperty: Boolean): Unit = {\n    withSQLConf(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key -> enableFastDrop.toString) {\n      test(s\"drop ${redirectFeature.name} with fast drop - \" +\n        s\"enableFastDrop=$enableFastDrop, unsetTableProperty=$unsetTableProperty\") {\n        withTempDir { dir =>\n          spark.sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\")\n          val deltaLog = DeltaLog.forTable(spark, dir)\n\n          val redirectSpec = new PathBasedRedirectSpec(\"sourcePath\", \"targetPath\")\n          tableRedirect.add(\n            deltaLog,\n            catalogTableOpt = None,\n            PathBasedRedirectSpec.REDIRECT_TYPE,\n            redirectSpec)\n\n          if (unsetTableProperty) {\n            sql(s\"ALTER TABLE delta.`${dir.getCanonicalPath}` UNSET TBLPROPERTIES \" +\n              s\"('${tableRedirect.config.key}')\")\n          }\n\n          val featureName = redirectFeature.name\n          // Both RedirectReaderWriterFeature and RedirectWriterOnlyFeature can be immediately\n          // dropped as they don't require history truncation. This is because there is no\n          // associated action with the features.\n          AlterTableDropFeatureDeltaCommand(DeltaTableV2(spark, deltaLog.dataPath), featureName)\n            .run(spark)\n\n          val snapshot = deltaLog.update()\n          // Writer feature is removed from the writer features set.\n          assert(!snapshot.protocol.writerFeatureNames.contains(featureName))\n          // Reader feature is removed from the reader features set.\n          assert(!snapshot.protocol.readerFeatureNames.contains(featureName))\n          assert(tableRedirect.config.fromMetaData(snapshot.metadata).isEmpty)\n\n          assertPropertiesAndShowTblProperties(deltaLog)\n\n          // Running the command again should throw an exception.\n          val e = intercept[DeltaTableFeatureException] {\n            AlterTableDropFeatureDeltaCommand(DeltaTableV2(spark, deltaLog.dataPath), featureName)\n              .run(spark)\n          }\n          assert(e.getErrorClass == \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\")\n        }\n      }\n    }\n  }\n\n  BOOLEAN_DOMAIN.foreach { unsetTableProperty =>\n    BOOLEAN_DOMAIN.foreach { enableFastDrop =>\n      // Test both writer-only and reader writer redirect feature.\n      testRedirectFeature(\n        RedirectWriterOnlyFeature, RedirectWriterOnly, enableFastDrop, unsetTableProperty)\n      testRedirectFeature(\n        RedirectReaderWriterFeature, RedirectReaderWriter, enableFastDrop, unsetTableProperty)\n    }\n  }\n\n  // Create a table for testing that has an unsupported feature.\n  private def withTestTableWithUnsupportedWriterFeature(\n      emptyTable: Boolean)(testCode: String => Unit): Unit = {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      if (emptyTable) {\n        sql(s\"CREATE TABLE $tableName(id INT) USING DELTA\")\n      } else {\n        sql(s\"CREATE TABLE $tableName USING DELTA AS SELECT 1 AS id\")\n      }\n\n      sql(s\"\"\"ALTER TABLE $tableName\n              SET TBLPROPERTIES ('delta.minReaderVersion' = '3', 'delta.minWriterVersion' = '7')\"\"\")\n\n      val deltaLogPath = DeltaLog.forTable(spark, TableIdentifier(tableName)).logPath\n        .toString.stripPrefix(\"file:\")\n\n      // scalastyle:off\n      val commitJson =\n        \"\"\"{\"metaData\":{\"id\":\"testId\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdTime\":1702304249309}}\n          |{\"protocol\":{\"minReaderVersion\":3,\"minWriterVersion\":7,\"readerFeatures\":[],\"writerFeatures\":[\"unsupportedWriter\"]}}\"\"\".stripMargin\n      // scalastyle:on\n\n      Files.write(Paths.get(deltaLogPath, \"00000000000000000002.json\"), commitJson.getBytes)\n\n      testCode(tableName)\n    }\n  }\n\n  // Test that write commands error out when unsupported features in the table protocol.\n  private def testUnsupportedFeature(\n      commandName: String, emptyTable: Boolean)(command: String => Unit): Unit = {\n    test(s\"Writes using $commandName error out when unsupported writer features are present\") {\n      withTestTableWithUnsupportedWriterFeature(emptyTable) { tableName =>\n        intercept[DeltaUnsupportedTableFeatureException] {\n          command(tableName)\n        }\n      }\n    }\n  }\n\n  testUnsupportedFeature(\"INSERT\", emptyTable = true) { testTableName =>\n    sql(s\"INSERT INTO $testTableName VALUES (2)\")\n  }\n\n  testUnsupportedFeature(\"UPDATE\", emptyTable = false) { testTableName =>\n    sql(s\"UPDATE $testTableName SET id = 2\")\n  }\n\n  testUnsupportedFeature(\"DELETE\", emptyTable = false) { testTableName =>\n    sql(s\"DELETE FROM $testTableName WHERE id > 0\")\n  }\n\n  testUnsupportedFeature(\"MERGE\", emptyTable = false) { testTableName =>\n    sql(s\"\"\"MERGE INTO $testTableName t\n           |USING $testTableName s\n           |ON s.id = t.id + 100\n           |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin)\n  }\n\n  testUnsupportedFeature(\"CREATE OR REPLACE TABLE\", emptyTable = false) { testTableName =>\n    sql(s\"CREATE OR REPLACE TABLE $testTableName  (other_column INT) USING DELTA\")\n  }\n\n  testUnsupportedFeature(\"ManualUpdate commit\", emptyTable = true) { testTableName =>\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n    deltaLog.startTransaction(None)\n      .commit(Seq(DeltaTestUtils.createTestAddFile()), DeltaOperations.ManualUpdate)\n  }\n\n  testUnsupportedFeature(\"SHALLOW CLONE\", emptyTable = true) { testTableName =>\n    val cloneSourceTableName = \"clone_source_table\"\n    withTable(cloneSourceTableName) {\n      sql(s\"DELETE FROM $testTableName\")\n      sql(s\"CREATE TABLE $cloneSourceTableName USING delta AS SELECT 1337 as id\")\n      sql(s\"CREATE OR REPLACE TABLE $testTableName SHALLOW CLONE $cloneSourceTableName\")\n    }\n  }\n\n  private def assertPropertiesAndShowTblProperties(\n      deltaLog: DeltaLog,\n      tableHasFeatures: Boolean = false): Unit = {\n    val configs = deltaLog.snapshot.metadata.configuration.map { case (k, v) =>\n      k.toLowerCase(Locale.ROOT) -> v\n    }\n    assert(!configs.contains(Protocol.MIN_READER_VERSION_PROP))\n    assert(!configs.contains(Protocol.MIN_WRITER_VERSION_PROP))\n    assert(!configs.exists(_._1.startsWith(FEATURE_PROP_PREFIX)))\n\n    val tblProperties =\n      sql(s\"SHOW TBLPROPERTIES delta.`${deltaLog.dataPath.toString}`\").collect()\n\n    assert(\n      tblProperties.exists(row => row.getAs[String](\"key\") == Protocol.MIN_READER_VERSION_PROP))\n    assert(\n      tblProperties.exists(row => row.getAs[String](\"key\") == Protocol.MIN_WRITER_VERSION_PROP))\n\n    assert(tableHasFeatures === tblProperties.exists(row =>\n      row.getAs[String](\"key\").startsWith(FEATURE_PROP_PREFIX)))\n    val rows =\n      tblProperties.filter(row =>\n        row.getAs[String](\"key\").startsWith(FEATURE_PROP_PREFIX))\n    for (row <- rows) {\n      val name = row.getAs[String](\"key\").substring(FEATURE_PROP_PREFIX.length)\n      val status = row.getAs[String](\"value\")\n      assert(TableFeature.featureNameToFeature(name).isDefined)\n      assert(status == FEATURE_PROP_SUPPORTED)\n    }\n  }\n\n  private def captureProtocolChangeEventBlob(f: => Unit): Map[String, Any] = {\n    val logs = Log4jUsageLogger.track(f)\n    val blob = logs.collectFirst {\n      case r if r.metric == MetricDefinitions.EVENT_TAHOE.name &&\n        r.tags.get(\"opType\").contains(\"delta.protocol.change\") => r.blob\n    }\n    require(blob.nonEmpty, \"Expecting a delta.protocol.change event but didn't see any.\")\n    blob.map(JsonUtils.fromJson[Map[String, Any]]).head\n  }\n}\n\nclass DeltaProtocolVersionSuite extends DeltaProtocolVersionSuiteBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaRestartSessionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.internal.SQLConf\n\nclass DeltaRestartSessionSuite extends SparkFunSuite {\n\n  test(\"restart Spark session should work\") {\n    withTempDir { dir =>\n      var spark = SparkSession.builder().master(\"local[2]\")\n        .config(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key, classOf[DeltaCatalog].getName)\n        .getOrCreate()\n      try {\n        val path = dir.getCanonicalPath\n        spark.range(10).write.format(\"delta\").mode(\"overwrite\").save(path)\n        spark.read.format(\"delta\").load(path).count()\n\n        spark.stop()\n        spark = SparkSession.builder().master(\"local[2]\")\n          .config(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key, classOf[DeltaCatalog].getName)\n          .getOrCreate()\n        spark.range(10).write.format(\"delta\").mode(\"overwrite\").save(path)\n        spark.read.format(\"delta\").load(path).count()\n      }\n      finally {\n        spark.stop()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaRetentionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.concurrent.duration._\nimport scala.language.postfixOps\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, RemoveFile, SetTransaction}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, Path, RawLocalFileSystem}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.util.ManualClock\n\n// scalastyle:off: removeFile\nclass DeltaRetentionSuite extends QueryTest\n  with DeltaRetentionSuiteBase\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with CheckpointProtectionTestUtilsMixin {\n\n  protected override def sparkConf: SparkConf = super.sparkConf\n\n  override protected def getLogFiles(dir: File): Seq[File] =\n    getDeltaFiles(dir) ++ getUnbackfilledDeltaFiles(dir) ++ getCheckpointFiles(dir)++\n      getCrcFiles(dir)\n\n  test(\"startTxnWithManualLogCleanup\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      startTxnWithManualLogCleanup(log).commit(Nil, testOp)\n      assert(!log.enableExpiredLogCleanup())\n    }\n  }\n\n  test(\"delete expired logs\") {\n    withTempDir { tempDir =>\n      val startTime = getStartTimeForRetentionTest\n      val clock = new ManualClock(startTime)\n      val actualTestStartTime = System.currentTimeMillis()\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      val logPath = new File(log.logPath.toUri)\n      (1 to 5).foreach { i =>\n        val txn = if (i == 1) startTxnWithManualLogCleanup(log) else log.startTransaction()\n        val file = createTestAddFile(encodedPath = i.toString) :: Nil\n        val delete: Seq[Action] = if (i > 1) {\n          val timestamp = startTime + (System.currentTimeMillis()-actualTestStartTime)\n          RemoveFile(i - 1 toString, Some(timestamp), true) :: Nil\n        } else {\n          Nil\n        }\n        txn.commit(delete ++ file, testOp)\n      }\n\n      val initialFiles = getLogFiles(logPath)\n      // Shouldn't clean up, no checkpoint, no expired files\n      log.cleanUpExpiredLogs(log.snapshot)\n\n      assert(initialFiles === getLogFiles(logPath))\n\n      clock.advance(intervalStringToMillis(DeltaConfigs.LOG_RETENTION.defaultValue) +\n        intervalStringToMillis(\"interval 1 day\"))\n\n      // Shouldn't clean up, no checkpoint, although all files have expired\n      log.cleanUpExpiredLogs(log.snapshot)\n      assert(initialFiles === getLogFiles(logPath))\n\n      log.checkpoint()\n\n      // With V2 checkpoints (QoL feature for CatalogOwned tables), checkpoint files have UUIDs\n      // and may be .json or .parquet (e.g., \"04.checkpoint.<uuid>.json\" instead of\n      // \"04.checkpoint.parquet\"). We check for the commit log and CRC, and verify that at least\n      // one checkpoint file exists for version 4.\n      log.cleanUpExpiredLogs(log.snapshot)\n      val afterCleanup = getLogFiles(logPath)\n      assert(initialFiles !== afterCleanup)\n      val afterCleanupNames = afterCleanup.map(_.getName)\n      assert(afterCleanupNames.exists(_.contains(\"00000000000000000004.json\")),\n        s\"Missing 04.json in: ${afterCleanupNames.mkString(\"\\n\")}\")\n      assert(afterCleanupNames.exists(name => name.contains(\"00000000000000000004.checkpoint\")),\n        s\"Missing 04.checkpoint file in: ${afterCleanupNames.mkString(\"\\n\")}\")\n      assert(afterCleanupNames.exists(_.contains(\"00000000000000000004.crc\")),\n        s\"Missing 04.crc in: ${afterCleanupNames.mkString(\"\\n\")}\")\n    }\n  }\n\n  test(\"log files being already deleted shouldn't fail log deletion job\") {\n    withTempDir { tempDir =>\n      val startTime = getStartTimeForRetentionTest\n      val clock = new ManualClock(startTime)\n      val actualTestStartTime = System.currentTimeMillis()\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      val logPath = new File(log.logPath.toUri)\n      val iterationCount = (log.checkpointInterval() * 2) + 1\n\n      (1 to iterationCount).foreach { i =>\n        val txn = if (i == 1) startTxnWithManualLogCleanup(log) else log.startTransaction()\n        val file = createTestAddFile(encodedPath = i.toString) :: Nil\n        val delete: Seq[Action] = if (i > 1) {\n          val timestamp = startTime + (System.currentTimeMillis()-actualTestStartTime)\n          RemoveFile(i - 1 toString, Some(timestamp), true) :: Nil\n        } else {\n          Nil\n        }\n        val version = txn.commit(delete ++ file, testOp)\n        val deltaFile = new File(FileNames.unsafeDeltaFile(log.logPath, version).toUri)\n        deltaFile.setLastModified(clock.getTimeMillis() + i * 10000)\n        val crcFile = new File(FileNames.checksumFile(log.logPath, version).toUri)\n        crcFile.setLastModified(clock.getTimeMillis() + i * 10000)\n        val chk = new File(FileNames.checkpointFileSingular(log.logPath, version).toUri)\n        if (chk.exists()) {\n          chk.setLastModified(clock.getTimeMillis() + i * 10000)\n        }\n      }\n\n      // delete some files in the middle\n      val middleStartIndex = log.checkpointInterval() / 2\n      getDeltaFiles(logPath).sortBy(_.getName).slice(\n        middleStartIndex, middleStartIndex + log.checkpointInterval()).foreach(_.delete())\n      clock.advance(intervalStringToMillis(DeltaConfigs.LOG_RETENTION.defaultValue) +\n        intervalStringToMillis(\"interval 2 day\"))\n      log.cleanUpExpiredLogs(log.snapshot)\n\n      val minDeltaFile =\n        getDeltaFiles(logPath).map(f => FileNames.deltaVersion(new Path(f.toString))).min\n      val maxChkFile = getCheckpointFiles(logPath).map(f =>\n        FileNames.checkpointVersion(new Path(f.toString))).max\n\n      assert(maxChkFile === minDeltaFile,\n        \"Delta files before the last checkpoint version should have been deleted\")\n      // With V2 checkpoints (QoL feature for CatalogOwned tables), cleanup behavior may retain\n      // additional checkpoint files for safety. We check that there are no more than 2 checkpoint\n      // files remaining (classic behavior expects 1, V2 may have up to 2).\n      assert(getCheckpointFiles(logPath).length <= 2,\n        s\"There should be at most 2 checkpoint files, but found: \" +\n          s\"${getCheckpointFiles(logPath).length}\")\n    }\n  }\n\n  testQuietly(\n    \"RemoveFiles persist across checkpoints as tombstones if retention time hasn't expired\") {\n    withTempDir { tempDir =>\n      val clock = new ManualClock(getStartTimeForRetentionTest)\n      val log1 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n\n      val txn = startTxnWithManualLogCleanup(log1)\n      val files1 = (1 to 10).map(f => createTestAddFile(encodedPath = f.toString))\n      txn.commit(files1, testOp)\n      val txn2 = log1.startTransaction()\n      val files2 = (1 to 4).map(f => RemoveFile(f.toString, Some(clock.getTimeMillis())))\n      txn2.commit(files2, testOp)\n      log1.checkpoint()\n\n      DeltaLog.clearCache()\n      val log2 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      assert(log2.snapshot.tombstones.count() === 4)\n      assert(log2.snapshot.allFiles.count() === 6)\n    }\n  }\n\n  def removeFileCountFromUnderlyingCheckpoint(snapshot: Snapshot): Long = {\n    val df = snapshot.checkpointProvider\n      .allActionsFileIndexes()\n      .map(snapshot.deltaLog.loadIndex(_))\n      .reduce(_.union(_))\n    df.where(\"remove is not null\").count()\n  }\n\n  testQuietly(\"retention timestamp is picked properly by the cold snapshot initialization\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(getStartTimeForRetentionTest)\n      def deltaLog: DeltaLog = DeltaLog.forTable(spark, new Path(dir.getCanonicalPath), clock)\n\n      // Create table with 30 day tombstone retention.\n      sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n           |TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 30 days')\n       \"\"\".stripMargin)\n\n\n      // 1st day - commit 10 new files and remove them also same day.\n      clock.advance(intervalStringToMillis(\"interval 1 days\"))\n      val files1 = (1 to 4).map(f => createTestAddFile(encodedPath = f.toString))\n      deltaLog.startTransaction().commit(files1, testOp)\n      val files2 = (1 to 4).map(f => RemoveFile(f.toString, Some(clock.getTimeMillis())))\n      deltaLog.startTransaction().commit(files2, testOp)\n\n      // Advance clock by 10 days.\n      clock.advance(intervalStringToMillis(\"interval 10 days\"))\n      DeltaLog.clearCache()\n      deltaLog.checkpoint()\n      DeltaLog.clearCache() // Clear cache and reinitialize snapshot with latest checkpoint.\n      assert(removeFileCountFromUnderlyingCheckpoint(deltaLog.unsafeVolatileSnapshot) === 4)\n\n      // Advance clock by 21 more days. Now checkpoint should stop tracking remove tombstones.\n      clock.advance(intervalStringToMillis(\"interval 21 days\"))\n      deltaLog.startTransaction().commit(Seq.empty, testOp)\n      DeltaLog.clearCache()\n      deltaLog.checkpoint(deltaLog.unsafeVolatileSnapshot)\n      DeltaLog.clearCache() // Clear cache and reinitialize snapshot with latest checkpoint.\n      assert(removeFileCountFromUnderlyingCheckpoint(deltaLog.unsafeVolatileSnapshot) === 0)\n    }\n  }\n\n\n  testQuietly(\"retention timestamp is lesser than the default value\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(getStartTimeForRetentionTest)\n      def deltaLog: DeltaLog = DeltaLog.forTable(spark, new Path(dir.getCanonicalPath), clock)\n\n      // Create table with 2 day tombstone retention.\n      sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n           |TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 2 days')\n       \"\"\".stripMargin)\n\n\n      // 1st day - commit 10 new files and remove them also same day.\n      {\n        clock.advance(intervalStringToMillis(\"interval 1 days\"))\n        val txn = deltaLog.startTransaction()\n        val files1 = (1 to 4).map(f => createTestAddFile(encodedPath = f.toString))\n        txn.commit(files1, testOp)\n        val txn2 = deltaLog.startTransaction()\n        val files2 = (1 to 4).map(f => RemoveFile(f.toString, Some(clock.getTimeMillis())))\n        txn2.commit(files2, testOp)\n      }\n\n\n      // Advance clock by 4 days.\n      clock.advance(intervalStringToMillis(\"interval 4 days\"))\n      DeltaLog.clearCache()\n      deltaLog.checkpoint(deltaLog.unsafeVolatileSnapshot)\n      DeltaLog.clearCache() // Clear cache and reinitialize snapshot with latest checkpoint.\n      assert(removeFileCountFromUnderlyingCheckpoint(deltaLog.unsafeVolatileSnapshot) === 0)\n    }\n  }\n\n  testQuietly(\"RemoveFiles get deleted during checkpoint if retention time has passed\") {\n    withTempDir { tempDir =>\n      val clock = new ManualClock(getStartTimeForRetentionTest)\n      val log1 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n\n      val txn = startTxnWithManualLogCleanup(log1)\n      val files1 = (1 to 10).map(f => createTestAddFile(encodedPath = f.toString))\n      txn.commit(files1, testOp)\n      val txn2 = log1.startTransaction()\n      val files2 = (1 to 4).map(f => RemoveFile(f.toString, Some(clock.getTimeMillis())))\n      txn2.commit(files2, testOp)\n\n      clock.advance(\n        intervalStringToMillis(DeltaConfigs.TOMBSTONE_RETENTION.defaultValue) + 1000000L)\n\n      log1.checkpoint()\n\n      DeltaLog.clearCache()\n      val log2 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      assert(log2.snapshot.tombstones.count() === 0)\n      assert(log2.snapshot.allFiles.count() === 6)\n    }\n  }\n\n  test(\"the checkpoint and checksum for version 0 should be cleaned\") {\n    withTempDir { tempDir =>\n      val clock = new ManualClock(getStartTimeForRetentionTest)\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      val logPath = new File(log.logPath.toUri)\n      startTxnWithManualLogCleanup(log).commit(createTestAddFile(encodedPath = \"0\") :: Nil, testOp)\n      log.checkpoint()\n\n      val initialFiles = getLogFiles(logPath)\n      clock.advance(intervalStringToMillis(DeltaConfigs.LOG_RETENTION.defaultValue) +\n        intervalStringToMillis(\"interval 1 day\"))\n\n      // Create new checkpoints so that the previous version can be deleted.\n      // With V2 checkpoints (QoL feature for CatalogOwned tables), we need to create version 2\n      // before version 0 can be cleaned up, as V2 checkpoints have stricter retention policies\n      // that require more checkpoint history before allowing cleanup.\n      log.startTransaction().commit(createTestAddFile(encodedPath = \"1\") :: Nil, testOp)\n      log.checkpoint()\n      log.startTransaction().commit(createTestAddFile(encodedPath = \"2\") :: Nil, testOp)\n      log.checkpoint()\n\n      // despite our clock time being set in the future, this doesn't change the FileStatus\n      // lastModified time. this can cause some flakiness during log cleanup. setting it fixes that.\n      getLogFiles(logPath)\n        .filterNot(f => initialFiles.contains(f))\n        .foreach(f => f.setLastModified(clock.getTimeMillis()))\n\n      log.cleanUpExpiredLogs(log.snapshot)\n      val afterCleanup = getLogFiles(logPath)\n      initialFiles.foreach { file =>\n        assert(!afterCleanup.contains(file))\n      }\n      // With V2 checkpoints, version 0 should be cleaned, but versions 1 and 2 may be retained\n      assert(!getCrcVersions(logPath).contains(0), \"Version 0 checksum should be deleted\")\n      assert(!getFileVersions(getDeltaFiles(logPath)).contains(0),\n        \"Version 0 commit should be deleted\")\n      assert(!getFileVersions(getCheckpointFiles(logPath)).contains(0),\n        \"Version 0 checkpoint should be deleted\")\n    }\n  }\n\n  test(\"allow users to expire transaction identifiers from checkpoints\") {\n    withTempDir { dir =>\n      val clock = new ManualClock(getStartTimeForRetentionTest)\n      val log = DeltaLog.forTable(spark, new Path(dir.getCanonicalPath), clock)\n      sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint) USING delta\n           |TBLPROPERTIES ('delta.setTransactionRetentionDuration' = 'interval 1 days')\n       \"\"\".stripMargin)\n\n      // commit at time < TRANSACTION_ID_RETENTION_DURATION\n      log.startTransaction().commitManually(SetTransaction(\"app\", 1, Some(clock.getTimeMillis())))\n      assert(log.update().transactions == Map(\"app\" -> 1))\n      assert(log.update().numOfSetTransactions == 1)\n\n      clock.advance(intervalStringToMillis(\"interval 1 days\"))\n\n      // query at time == TRANSACTION_ID_RETENTION_DURATION & NO new commit\n      // No new commit has been made, so we will see expired transactions (this is not ideal, but\n      // it's a tradeoff we've accepted)\n      assert(log.update().transactions == Map(\"app\" -> 1))\n      assert(log.snapshot.numOfSetTransactions == 1)\n\n      clock.advance(1)\n\n      // query at time > TRANSACTION_ID_RETENTION_DURATION & NO new commit\n      // we continue to see expired transactions\n      assert(log.update().transactions == Map(\"app\" -> 1))\n      assert(log.snapshot.numOfSetTransactions == 1)\n\n      // query at time > TRANSACTION_ID_RETENTION_DURATION & there IS a new commit\n      // We will only filter expired transactions when time is >= TRANSACTION_ID_RETENTION_DURATION\n      // and a new commit has been made\n      val addFile = createTestAddFile(encodedPath = \"fake/path/1\")\n      log.startTransaction().commitManually(addFile)\n      assert(log.update().transactions.isEmpty)\n      assert(log.snapshot.numOfSetTransactions == 0)\n    }\n  }\n\n  protected def cleanUpExpiredLogs(log: DeltaLog): Unit = {\n    val snapshot = log.update()\n\n    val checkpointVersion = snapshot.logSegment.checkpointProvider.version\n    logInfo(s\"snapshot version: ${snapshot.version} checkpoint: $checkpointVersion\")\n\n    log.cleanUpExpiredLogs(snapshot)\n  }\n\n  for (v2CheckpointFormat <- V2Checkpoint.Format.ALL_AS_STRINGS)\n  test(s\"sidecar file cleanup [v2CheckpointFormat: $v2CheckpointFormat]\") {\n    val checkpointPolicy = CheckpointPolicy.V2.name\n    withSQLConf((DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> v2CheckpointFormat)) {\n      withTempDir { tempDir =>\n        val startTime = getStartTimeForRetentionTest\n        val clock = new ManualClock(startTime)\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n        val logPath = new File(log.logPath.toUri)\n        val visitedFiles = scala.collection.mutable.Set.empty[String]\n\n        spark.sql(s\"\"\"CREATE TABLE delta.`${tempDir.toString()}` (id Int) USING delta\n                   | TBLPROPERTIES(\n                   |-- Disable the async log cleanup as this test needs to manually trigger log\n                   |-- clean up.\n                   |'delta.enableExpiredLogCleanup' = 'false',\n                   |'${DeltaConfigs.CHECKPOINT_POLICY.key}' = '$checkpointPolicy',\n                   |'${DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.key}' = 'false',\n                   |'delta.checkpointInterval' = '100000',\n                   |'delta.deletedFileRetentionDuration' = 'interval 1 days',\n                   |'delta.logRetentionDuration' = 'interval 6 days')\n                    \"\"\".stripMargin)\n\n        // day-1. Create a commit with 4 AddFiles.\n        clock.setTime(day(startTime, day = 1))\n        val file = (1 to 4).map(i => createTestAddFile(i.toString))\n        log.startTransaction().commit(file, testOp)\n        setModificationTimeOfNewFiles(log, clock, visitedFiles)\n\n        // Trigger 1 commit and 1 checkpoint daily for next 8 days\n        val sidecarFiles = scala.collection.mutable.Map.empty[Long, String]\n        val oddCommitSidecarFile_1 = createSidecarFile(log, Seq(1))\n        val evenCommitSidecarFile_1 = createSidecarFile(log, Seq(1))\n        def commitAndCheckpoint(dayNumber: Int): Unit = {\n          clock.setTime(day(startTime, dayNumber))\n\n          // Write a new commit on each day\n          log.startTransaction().commit(Seq(log.unsafeVolatileSnapshot.metadata), testOp)\n          setModificationTimeOfNewFiles(log, clock, visitedFiles)\n\n          // Write a new checkpoint on each day. Each checkpoint has 2 sodecars:\n          // 1. Common sidecar - one of oddCommitSidecarFile_1/evenCommitSidecarFile_1\n          // 2. A new sidecar just created for this checkpoint.\n          val sidecarFile1 =\n            if (dayNumber % 2 == 0) evenCommitSidecarFile_1 else oddCommitSidecarFile_1\n          val sidecarFile2 = createSidecarFile(log, Seq(2, 3, 4))\n          val checkpointVersion = log.update().version\n          createV2CheckpointWithSidecarFile(\n            log,\n            checkpointVersion,\n            sidecarFileNames = Seq(sidecarFile1, sidecarFile2))\n          setModificationTimeOfNewFiles(log, clock, visitedFiles)\n          sidecarFiles.put(checkpointVersion, sidecarFile2)\n        }\n\n        (2 to 9).foreach { dayNumber => commitAndCheckpoint(dayNumber) }\n        clock.setTime(day(startTime, day = 10))\n        log.update()\n\n        // Assert all log files are present.\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 2 to 9)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 0 to 9)\n        assert(\n          getSidecarFiles(log) ===\n            Set(\n              evenCommitSidecarFile_1,\n              oddCommitSidecarFile_1) ++ sidecarFiles.values.toIndexedSeq)\n\n        // Trigger metadata cleanup and validate that only last 6 days of deltas and checkpoints\n        // have been retained.\n        cleanUpExpiredLogs(log)\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 4 to 9)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 4 to 9)\n        // Check that all active sidecars are retained and expired ones are deleted.\n        assert(\n          getSidecarFiles(log) ===\n            Set(evenCommitSidecarFile_1, oddCommitSidecarFile_1) ++\n            (4 to 9).map(sidecarFiles(_)))\n\n        // Advance 1 day and again run metadata cleanup.\n        clock.setTime(day(startTime, day = 11))\n        cleanUpExpiredLogs(log)\n        setModificationTimeOfNewFiles(log, clock, visitedFiles)\n        // Commit 4 and checkpoint 4 have expired and were deleted.\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 5 to 9)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 5 to 9)\n        assert(\n          getSidecarFiles(log) ===\n            Set(evenCommitSidecarFile_1, oddCommitSidecarFile_1) ++\n            (5 to 9).map(sidecarFiles(_)))\n\n        // do 1 more commit and checkpoint on day 13 and run metadata cleanup.\n        commitAndCheckpoint(dayNumber = 13) // commit and checkpoint 10\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 5 to 10)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 5 to 10)\n        cleanUpExpiredLogs(log)\n        setModificationTimeOfNewFiles(log, clock, visitedFiles)\n        // Version 5 and 6 checkpoints and deltas have expired and were deleted.\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 7 to 10)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 7 to 10)\n\n        assert(\n          getSidecarFiles(log) ===\n            Set(evenCommitSidecarFile_1, oddCommitSidecarFile_1) ++\n            (7 to 10).map(sidecarFiles(_)))\n      }\n    }\n  }\n\n  for (v2CheckpointFormat <- V2Checkpoint.Format.ALL_AS_STRINGS)\n  test(\n    s\"compat file created with metadata cleanup when checkpoints are deleted\" +\n      s\" [v2CheckpointFormat: $v2CheckpointFormat]\") {\n    val checkpointPolicy = CheckpointPolicy.V2.name\n    withSQLConf((DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> v2CheckpointFormat)) {\n      withTempDir { tempDir =>\n        val startTime = getStartTimeForRetentionTest\n        val clock = new ManualClock(startTime)\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n        val logPath = new File(log.logPath.toUri)\n        val visitedFiles = scala.collection.mutable.Set.empty[String]\n\n        spark.sql(s\"\"\"CREATE TABLE delta.`${tempDir.toString()}` (id Int) USING delta\n                     | TBLPROPERTIES(\n                     |-- Disable the async log cleanup as this test needs to manually trigger log\n                     |-- clean up.\n                     |'delta.enableExpiredLogCleanup' = 'false',\n                     |'${DeltaConfigs.CHECKPOINT_POLICY.key}' = '$checkpointPolicy',\n                     |'${DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.key}' = 'false',\n                     |'delta.checkpointInterval' = '100000',\n                     |'delta.deletedFileRetentionDuration' = 'interval 1 days',\n                     |'delta.logRetentionDuration' = 'interval 6 days')\n        \"\"\".stripMargin)\n\n        (1 to 10).foreach { dayNum =>\n          clock.setTime(day(startTime, dayNum))\n          log.startTransaction().commit(Seq(), testOp)\n          setModificationTimeOfNewFiles(log, clock, visitedFiles)\n          clock.setTime(day(startTime, dayNum) + 10)\n          log.checkpoint(log.update())\n          setModificationTimeOfNewFiles(log, clock, visitedFiles)\n        }\n        clock.setTime(day(startTime, 11))\n        log.update()\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 1 to 10)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 0 to 10)\n\n        // 11th day Run metadata cleanup.\n        clock.setTime(day(startTime, 11))\n        cleanUpExpiredLogs(log)\n        compareVersions(getCheckpointVersions(logPath), \"checkpoint\", 5 to 10)\n        compareVersions(getDeltaVersions(logPath), \"delta\", 5 to 10)\n        val checkpointInstancesForV10 =\n          getCheckpointFiles(logPath)\n            .filter(f => getFileVersions(Seq(f)).head == 10)\n            .map(f => new Path(f.getAbsolutePath))\n            .sortBy(_.getName)\n            .map(CheckpointInstance.apply)\n\n        assert(checkpointInstancesForV10.size == 2)\n        assert(\n          checkpointInstancesForV10.map(_.format) ===\n            Seq(CheckpointInstance.Format.V2, CheckpointInstance.Format.SINGLE))\n      }\n    }\n  }\n\n  (Seq((\"Default\", Seq.empty[(String, String)])) ++ CheckpointPolicy.ALL.map {\n    case CheckpointPolicy.Classic =>\n      Seq(\n        (\"Classic\", Seq(\n          DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.Classic.name)),\n        (\"Multipart\", Seq(\n          DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.Classic.name,\n          DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"1\"))\n      )\n    case CheckpointPolicy.V2 =>\n      V2Checkpoint.Format.ALL_AS_STRINGS.map { v2CheckpointFormat =>\n        (s\"V2 $v2CheckpointFormat\",\n          Seq(DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n            DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> v2CheckpointFormat))\n      }\n  }.flatten).foreach { case (chkConfigName, chkConfig) =>\n  test(s\"cleanup does not delete the checkpoint if it is required by non-expired versions. \" +\n    s\"Config: $chkConfigName.\") {\n    withSQLConf(chkConfig: _*) {\n      // Disable the following check as the test relies on time travel beyond\n      // deletedFileRetentionDuration\n      withSQLConf(\n        DeltaSQLConf.ENFORCE_TIME_TRAVEL_WITHIN_DELETED_FILE_RETENTION_DURATION.key -> \"false\"\n      ) {\n        withTempDir { tempDir =>\n          val startTime = getStartTimeForRetentionTest\n          val clock = new ManualClock(startTime)\n          val actualTestStartTime = System.currentTimeMillis()\n          val tableReference = s\"delta.`${tempDir.getCanonicalPath()}`\"\n          val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n          val logPath = new File(log.logPath.toUri)\n          val minChksCount = if (chkConfigName == \"Multipart\") {\n            2\n          } else {\n            1\n          }\n\n          // commit 0\n          spark.sql(\n            s\"\"\"CREATE TABLE $tableReference (id Int) USING delta TBLPROPERTIES(\n               | 'delta.enableChangeDataFeed' = true)\n            \"\"\".stripMargin)\n          // Set time for commit 0 to ensure that the commits don't need timestamp adjustment.\n          val commit0Time = clock.getTimeMillis()\n          new File(FileNames.unsafeDeltaFile(log.logPath, 0).toUri).setLastModified(commit0Time)\n          new File(FileNames.checksumFile(log.logPath, 0).toUri).setLastModified(commit0Time)\n\n          def commitNewVersion(version: Long): Unit = {\n            spark.sql(s\"INSERT INTO $tableReference VALUES (1)\")\n\n            val deltaFile = new File(FileNames.unsafeDeltaFile(log.logPath, version).toUri)\n            val time = clock.getTimeMillis() + version * 1000\n            deltaFile.setLastModified(time)\n            val crcFile = new File(FileNames.checksumFile(log.logPath, version).toUri)\n            crcFile.setLastModified(time)\n            val chks = getCheckpointFiles(logPath)\n              .filter(f => FileNames.checkpointVersion(new Path(f.getCanonicalPath)) == version)\n\n            if (version % 10 == 0) {\n              assert(chks.length >= minChksCount)\n              chks.foreach { chk =>\n                assert(chk.exists())\n                chk.setLastModified(time)\n              }\n            } else {\n              assert(chks.isEmpty)\n            }\n          }\n\n          // Day 0: Add commits 1 to 15 --> creates 1 checkpoint at Day 0 for version 10\n          (1L to 15L).foreach(commitNewVersion)\n\n          // ensure that the checkpoint at version 10 exists\n          val checkpoint10Files = getCheckpointFiles(logPath)\n            .filter(f => FileNames.checkpointVersion(new Path(f.getCanonicalPath)) == 10)\n          assert(checkpoint10Files.length >= minChksCount)\n          assert(checkpoint10Files.forall(_.exists))\n          val deltaFiles = (0 to 15).map { i =>\n            new File(FileNames.unsafeDeltaFile(log.logPath, i).toUri)\n          }\n          deltaFiles.foreach { f =>\n            assert(f.exists())\n          }\n\n          // Day 35: Add commits 16 to 25 --> creates a checkpoint at Day 35 for version 20\n          clock.setTime(day(startTime, 35))\n          (16L to 25L).foreach(commitNewVersion)\n\n          assert(checkpoint10Files.forall(_.exists))\n          deltaFiles.foreach { f =>\n            assert(f.exists())\n          }\n\n          // auto cleanup is disabled in DeltaRetentionSuiteBase so tests have control when it\n          // happens\n          cleanUpExpiredLogs(log)\n\n          // assert that the checkpoint from day 0 (at version 10) and all the commits after\n          // that are still there\n          assert(checkpoint10Files.forall(_.exists))\n          deltaFiles.foreach { f =>\n            val version = FileNames.deltaVersion(new Path(f.toString()))\n            if (version < 10) {\n              assert(!f.exists, version)\n            } else {\n              assert(f.exists, version)\n            }\n          }\n\n          // Validate we can time travel to version >=10\n          val earliestExpectedChkVersion = 10\n          (0 to 25).map { version =>\n            val sqlCommand = s\"SELECT * FROM $tableReference VERSION AS OF $version\"\n            if (version < earliestExpectedChkVersion) {\n              val ex = intercept[org.apache.spark.sql.delta.VersionNotFoundException] {\n                spark.sql(sqlCommand).collect()\n              }\n              checkError(\n                ex,\n                \"DELTA_VERSION_NOT_FOUND\",\n                sqlState = \"22003\",\n                parameters = Map(\n                  \"userVersion\" -> version.toString,\n                  \"earliest\" -> earliestExpectedChkVersion.toString,\n                  \"latest\" -> \"25\"))\n            } else {\n              spark.sql(sqlCommand).collect()\n            }\n          }\n\n          // Validate CDF - SELECT * FROM table_changes_by_path('table', X, Y)\n          (0 to 24).map { version =>\n            val sqlCommand = s\"SELECT * FROM \" +\n              s\"table_changes_by_path('${tempDir.getCanonicalPath}', $version, 25)\"\n            if (version < earliestExpectedChkVersion) {\n              if (catalogOwnedDefaultCreationEnabledInTests) {\n                intercept[IllegalStateException] {\n                  spark.sql(sqlCommand).collect()\n                }\n              } else {\n                intercept[org.apache.spark.sql.delta.DeltaFileNotFoundException] {\n                  spark.sql(sqlCommand).collect()\n                }\n              }\n            } else {\n              spark.sql(sqlCommand).collect()\n            }\n          }\n        }\n      }\n    }\n  }\n  }\n\n  test(s\"cleanup does not delete the JSON logs if the multi-part checkpoint is incomplete.\") {\n    withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"1\") {\n    withTempDir { tempDir =>\n      val startTime = getStartTimeForRetentionTest\n      val clock = new ManualClock(startTime)\n      val actualTestStartTime = System.currentTimeMillis()\n      val tableReference = s\"delta.`${tempDir.getCanonicalPath()}`\"\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      val logPath = new File(log.logPath.toUri)\n\n      // commit 0\n      spark.sql(\n        s\"\"\"CREATE TABLE $tableReference (id Int) USING delta\n           | TBLPROPERTIES('delta.enableChangeDataFeed' = true)\n        \"\"\".stripMargin)\n      // Set time for commit 0 to ensure that the commits don't need timestamp adjustment.\n      val commit0Time = clock.getTimeMillis()\n      new File(FileNames.unsafeDeltaFile(log.logPath, 0).toUri).setLastModified(commit0Time)\n      new File(FileNames.checksumFile(log.logPath, 0).toUri).setLastModified(commit0Time)\n\n      def commitNewVersion(version: Long): Unit = {\n        spark.sql(s\"INSERT INTO $tableReference VALUES (1)\")\n\n        val deltaFile = new File(FileNames.unsafeDeltaFile(log.logPath, version).toUri)\n        val time = clock.getTimeMillis() + version * 1000\n        deltaFile.setLastModified(time)\n        val crcFile = new File(FileNames.checksumFile(log.logPath, version).toUri)\n        crcFile.setLastModified(time)\n        val chks = getCheckpointFiles(logPath)\n          .filter(f => FileNames.checkpointVersion(new Path(f.getCanonicalPath)) == version)\n\n        if (version % 10 == 0) {\n          // With V2 checkpoints (QoL feature for CatalogOwned), checkpoints may be single-file.\n          // Classic checkpoints with DELTA_CHECKPOINT_PART_SIZE=1 are multi-part (>= 2 files).\n          assert(chks.length >= 1) // At least one checkpoint file\n          chks.foreach { chk =>\n              assert(chk.exists())\n              chk.setLastModified(time)\n          }\n        } else { assert(chks.isEmpty) }\n      }\n\n      // Day 0: Add commits 1 to 15 --> creates 1 checkpoint at Day 0 for version 10\n      (1L to 15L).foreach(commitNewVersion)\n\n      // ensure that the checkpoint at version 10 exists\n      val checkpoint10Files = getCheckpointFiles(logPath)\n        .filter(f => FileNames.checkpointVersion(new Path(f.getCanonicalPath)) == 10)\n      // With V2 checkpoints (QoL feature for CatalogOwned), checkpoints may be single-file\n      assert(checkpoint10Files.length >= 1) // At least one checkpoint file\n      assert(checkpoint10Files.forall(_.exists))\n      val deltaFiles = (0 to 15).map { i =>\n        new File(FileNames.unsafeDeltaFile(log.logPath, i).toUri)\n      }\n      deltaFiles.foreach { f =>\n        assert(f.exists())\n      }\n\n      // Day 35: Add commits 16 to 25 --> creates a checkpoint at Day 35 for version 20\n      clock.setTime(day(startTime, 35))\n      (16L to 25L).foreach(commitNewVersion)\n\n      assert(checkpoint10Files.forall(_.exists))\n      deltaFiles.foreach { f =>\n        assert(f.exists())\n      }\n\n      checkpoint10Files.lastOption.foreach { lastPart =>\n        lastPart.delete() // delete the last part to simulate incomplete checkpoint\n      }\n\n      // auto cleanup is disabled in DeltaRetentionSuiteBase so tests have control when it happens\n      cleanUpExpiredLogs(log)\n\n      // assert that delta logs are not deleted due to missing checkpoint part\n      deltaFiles.foreach { f =>\n        val version = FileNames.deltaVersion(new Path(f.toString()))\n        assert(f.exists, s\"version $version should not be deleted\")\n      }\n    }\n    }\n  }\n\n  test(\"Metadata cleanup respects requireCheckpointProtectionBeforeVersion\") {\n    withSQLConf(\n        DeltaSQLConf.ALLOW_METADATA_CLEANUP_WHEN_ALL_PROTOCOLS_SUPPORTED.key -> \"false\",\n        DeltaSQLConf.ALLOW_METADATA_CLEANUP_CHECKPOINT_EXISTENCE_CHECK_DISABLED.key -> \"true\") {\n      // Commits should be cleaned up to the latest checkpoint.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 2,\n        expectedCommitsAfterCleanup = (8 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // Commits should be cleaned up to the latest checkpoint.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 6,\n        expectedCommitsAfterCleanup = (8 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // Commits should be cleaned up to the latest checkpoint.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 7,\n        expectedCommitsAfterCleanup = (8 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // Commits should be cleaned up to the latest checkpoint.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 8,\n        expectedCommitsAfterCleanup = (8 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // Commits should be cleaned up to the checkpoint 10.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 10,\n        createNumCommitsWithinRetentionPeriod = 10,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 9,\n        expectedCommitsAfterCleanup = (10 to 19),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(10))\n\n      // Checkpoint 8 is within the retention period.\n      // Cleanup should be skipped.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 9,\n        expectedCommitsAfterCleanup = (0 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(6, 8, 10))\n\n      // Cleanup should be skipped.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8),\n        requireCheckpointProtectionBeforeVersion = 20,\n        expectedCommitsAfterCleanup = (0 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(6, 8, 10))\n\n      // With multiple checkpoints (8, 12, 14) within the retention period.\n      // None of these should be cleaned up.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8, 12, 14),\n        requireCheckpointProtectionBeforeVersion = 8,\n        expectedCommitsAfterCleanup = (8 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(8, 10, 12, 14))\n\n      // With multiple checkpoints (8, 12, 14) within the retention period.\n      // requireCheckpointProtectionBeforeVersion = 9 is within the retention period.\n      // Cleanup should be skipped.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(6, 8, 12, 14),\n        requireCheckpointProtectionBeforeVersion = 9,\n        expectedCommitsAfterCleanup = (0 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(6, 8, 10, 12, 14))\n\n      // Corner cases.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 1,\n        createNumCommitsWithinRetentionPeriod = 15,\n        createCheckpoints = Set(1),\n        requireCheckpointProtectionBeforeVersion = 0,\n        expectedCommitsAfterCleanup = (1 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(1, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 1,\n        createNumCommitsWithinRetentionPeriod = 15,\n        createCheckpoints = Set(1),\n        requireCheckpointProtectionBeforeVersion = 1,\n        expectedCommitsAfterCleanup = (1 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(1, 10))\n\n      // v1 can't be deleted because it is the only checkpoint before version 2.\n      // v0 can't be deleted because of the checkpoint protection, v0 and v1 needs\n      // to be deleted together.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 1,\n        createNumCommitsWithinRetentionPeriod = 15,\n        createCheckpoints = Set(1),\n        requireCheckpointProtectionBeforeVersion = 2,\n        expectedCommitsAfterCleanup = (0 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(1, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 2,\n        createNumCommitsWithinRetentionPeriod = 14,\n        createCheckpoints = Set(1),\n        requireCheckpointProtectionBeforeVersion = 3,\n        expectedCommitsAfterCleanup = (0 to 15),\n        // Α checkpoint is automatically created every 10 commits.\n        expectedCheckpointsAfterCleanup = Set(1, 10))\n    }\n  }\n\n  test(\"Cleanup is allowed if a checkpoint already exists at the boundary\") {\n    withSQLConf(DeltaSQLConf.ALLOW_METADATA_CLEANUP_WHEN_ALL_PROTOCOLS_SUPPORTED.key -> \"false\") {\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        // Metadata cleanup should attempt to clean before version 8.\n        createCheckpoints = Set(0, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        unsupportedFeatureStartVersion = Some(8),\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n    }\n  }\n\n  test(\"Metadata cleanup protocol validation positive tests.\") {\n    withSQLConf(\n        DeltaSQLConf.ALLOW_METADATA_CLEANUP_CHECKPOINT_EXISTENCE_CHECK_DISABLED.key -> \"true\") {\n      // In all tests below, we cannot satisfy the version requirement and thus fallback\n      // to protocol validations. We identify we support all features and proceed to\n      // metadata cleanup.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // The protocol contains unsupported feature but at requireCheckpointProtectionBeforeVersion.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        unsupportedFeatureStartVersion = Some(10),\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // The protocol contains unsupported feature but after\n      // requireCheckpointProtectionBeforeVersion.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        unsupportedFeatureStartVersion = Some(11),\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // The protocol contains unsupported feature before requireCheckpointProtectionBeforeVersion\n      // but right after the boundary version where the cleanup ends.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        // Metadata cleanup should attempt to clean before version 8.\n        createCheckpoints = Set(0, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        unsupportedFeatureStartVersion = Some(9),\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // Other corner cases.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(1, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(0, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        expectedCommitsAfterCleanup = (8 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n    }\n  }\n\n  test(\"Metadata cleanup protocol validation negative tests.\") {\n    withSQLConf(\n        DeltaSQLConf.ALLOW_METADATA_CLEANUP_CHECKPOINT_EXISTENCE_CHECK_DISABLED.key -> \"true\") {\n      // In all tests below, we cannot satisfy the version requirement and thus fallback\n      // to protocol validations. We should detect the start version version includes a\n      // non-supported feature and skip the cleanup.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        // Unsupported feature in the first version.\n        unsupportedFeatureStartVersion = Some(0),\n        unsupportedFeatureEndVersion = Some(1),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(0, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        // Unsupported feature right before the boundary version where the cleanup ends.\n        unsupportedFeatureStartVersion = Some(7),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(0, 8, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        // Unsupported feature in intermediate versions.\n        unsupportedFeatureStartVersion = Some(4),\n        unsupportedFeatureEndVersion = Some(7),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        // Unsupported feature in dropped at the boundary version.\n        unsupportedFeatureStartVersion = Some(4),\n        unsupportedFeatureEndVersion = Some(8),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // The protocol contains unsupported feature before requireCheckpointProtectionBeforeVersion\n      // but at the boundary version where the cleanup ends.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        // Metadata cleanup should attempt to clean before version 8.\n        createCheckpoints = Set(0, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        unsupportedFeatureStartVersion = Some(8),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(0, 8, 10))\n\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        // Metadata cleanup should attempt to clean before version 8.\n        createCheckpoints = Set(0, 8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        // Make sure we correctly validate the protocol of the checkpoint version.\n        unsupportedFeature = TestUnsupportedWriterFeature,\n        unsupportedFeatureStartVersion = Some(8),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(0, 8, 10))\n    }\n  }\n\n  test(\"Metadata cleanup protocol validation with incomplete CRCs.\") {\n    withSQLConf(\n        DeltaSQLConf.ALLOW_METADATA_CLEANUP_CHECKPOINT_EXISTENCE_CHECK_DISABLED.key -> \"true\") {\n      // We fall back to protocol validations which cannot be completed due to missing\n      // protocol in one of the CRCs.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        incompleteCRCVersion = Some(3),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n\n      // Similar to above but a CRC file is missing.\n      testRequireCheckpointProtectionBeforeVersion(\n        createNumCommitsOutsideRetentionPeriod = 8,\n        createNumCommitsWithinRetentionPeriod = 8,\n        createCheckpoints = Set(8),\n        requireCheckpointProtectionBeforeVersion = 10,\n        missingCRCVersion = Some(3),\n        expectedCommitsAfterCleanup = (0 to 15),\n        expectedCheckpointsAfterCleanup = Set(8, 10))\n    }\n  }\n}\n\nclass DeltaRetentionWithCatalogOwnedBatch1Suite extends DeltaRetentionSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\n/**\n * This test suite does not extend other tests of DeltaRetentionSuiteEdge because\n * DeltaRetentionSuiteEdge contain tests that rely on setting the file modification time for delta\n * files. However, in this suite, delta files might be backfilled asynchronously, which means\n * setting the modification time will not work as expected.\n */\nclass DeltaRetentionWithCatalogOwnedBatch2Suite\n  extends QueryTest\n  with DeltaSQLCommandTest\n  with DeltaRetentionSuiteBase {\n\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n\n  override def getLogFiles(dir: File): Seq[File] =\n    getDeltaFiles(dir) ++ getUnbackfilledDeltaFiles(dir) ++ getCheckpointFiles(dir)\n\n  /**\n   * This test verifies that unbackfilled versions, i.e., versions for which backfilled deltas do\n   * not exist yet, are never considered for deletion, even if they fall outside the retention\n   * window. The primary reason for not deleting these versions is that the CommitCoordinator might\n   * be actively tracking those files, and currently, MetadataCleanup does not communicate with the\n   * CommitCoordinator.\n   *\n   * Although the fact that they are unbackfilled is somewhat redundant since these versions are\n   * currently already protected due to two additional reasons:\n   * 1.They will always be part of the latest snapshot.\n   * 2.They don't have two checkpoints after them.\n   * However, this test helps ensure that unbackfilled deltas remain protected in the future, even\n   * if the above two conditions are no longer triggered.\n   *\n   * Note: This test is too slow for batchSize = 100 and wouldn't necessarily work for batchSize = 1\n   */\n  test(\"unbackfilled expired commits are always retained\") {\n    withTempDir { tempDir =>\n      val startTime = getStartTimeForRetentionTest\n      val clock = new ManualClock(startTime)\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      val logPath = new File(log.logPath.toUri.getPath)\n      val fs = new RawLocalFileSystem()\n      fs.initialize(tempDir.toURI, new Configuration())\n\n      log.startTransaction().commitManually(createTestAddFile(\"1\"))\n      log.checkpoint()\n      spark.sql(s\"\"\"ALTER TABLE delta.`${tempDir.toString}`\n                   |SET TBLPROPERTIES(\n                   |-- Trigger log clean up manually.\n                   |'delta.enableExpiredLogCleanup' = 'false',\n                   |'delta.checkpointInterval' = '10000',\n                   |'delta.checkpointRetentionDuration' = 'interval 2 days',\n                   |'delta.logRetentionDuration' = 'interval 30 days',\n                   |'delta.enableFullRetentionRollback' = 'true')\n        \"\"\".stripMargin)\n      log.checkpoint()\n      setModificationTime(log, startTime, 0, 0, fs)\n      setModificationTime(log, startTime, 1, 0, fs)\n      // Create commits [2, 6] with a checkpoint per commit\n      2 to 6 foreach { i =>\n        log.startTransaction().commitManually(createTestAddFile(s\"$i\"))\n        log.checkpoint()\n        setModificationTime(log, startTime, i, 0, fs)\n      }\n      // Create unbackfilled commit [7] with no checkpoints\n      log.startTransaction().commitManually(createTestAddFile(\"7\"))\n      setModificationTime(log, startTime, 7, 0, fs)\n\n      // Everything is eligible for deletion but we don't consider the unbackfilled commit,\n      // i.e. [7], for  deletion because it is part of the current LogSegment.\n      clock.setTime(day(startTime, 100))\n      log.cleanUpExpiredLogs(log.update())\n      // Since we also need a checkpoint, [6] is also protected.\n      val firstProtectedVersion = 6\n      compareVersions(\n        getDeltaVersions(logPath),\n        \"backfilled delta\",\n        firstProtectedVersion to 6)\n      compareVersions(\n        getUnbackfilledDeltaVersions(logPath),\n        \"unbackfilled delta\",\n        firstProtectedVersion to 7)\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaRetentionSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.{Calendar, TimeZone}\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.DeltaOperations.Truncate\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{CheckpointMetadata, Metadata, SidecarFile}\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.delta.util.FileNames.{newV2CheckpointJsonFile, newV2CheckpointParquetFile}\nimport org.apache.commons.lang3.time.DateUtils\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.util.IntervalUtils\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.unsafe.types.UTF8String\nimport org.apache.spark.util.ManualClock\n\ntrait DeltaRetentionSuiteBase extends QueryTest\n  with SharedSparkSession\n  with CatalogOwnedTestBaseSuite {\n  protected val testOp = Truncate()\n\n  protected override def sparkConf: SparkConf = super.sparkConf\n    // Disable the log cleanup because it runs asynchronously and causes test flakiness\n    .set(\"spark.databricks.delta.properties.defaults.enableExpiredLogCleanup\", \"false\")\n\n  protected def intervalStringToMillis(str: String): Long = {\n    DeltaConfigs.getMilliSeconds(\n      IntervalUtils.safeStringToInterval(UTF8String.fromString(str)))\n  }\n\n  /**\n   * Returns milliseconds since epoch at 1:00am UTC of current day.\n   *\n   * Context:\n   * Most DeltaRetentionSuite tests rely on ManualClock to time travel and\n   * trigger metadata cleanup. Cleanup boundaries are determined by\n   * finding files that were modified before 00:00 of the day on which\n   * currentTime-LOG_RETENTION_PERIOD falls. This means that for a long running\n   * test started at 23:59, the number of expired files would jump suddenly\n   * in 1 minute (the expiration boundary would move by a day as soon as\n   * system clock hits 00:00 of the next day). By fixing the start time of the\n   * test to 01:00, we avoid these scenarios.\n   *\n   * This would still break if the test runs for more than 23 hours.\n   */\n  protected def getStartTimeForRetentionTest: Long = {\n    val currentTime = System.currentTimeMillis()\n    val date = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"))\n    date.setTimeInMillis(currentTime)\n    val dayStartTimeStamp = DateUtils.truncate(date, Calendar.DAY_OF_MONTH)\n    dayStartTimeStamp.add(Calendar.HOUR_OF_DAY, 1);\n    dayStartTimeStamp.getTimeInMillis\n  }\n\n  protected def getDeltaFiles(dir: File): Seq[File] =\n    dir.listFiles().filter(f => FileNames.isDeltaFile(new Path(f.getCanonicalPath)))\n\n  protected def getCheckpointFiles(dir: File): Seq[File] =\n    dir.listFiles().filter(f => FileNames.isCheckpointFile(new Path(f.getCanonicalPath)))\n\n  protected def getLogFiles(dir: File): Seq[File]\n\n  protected def getFileVersions(files: Seq[File]): Set[Long] = {\n    files.map(f => f.getName()).map(s => s.substring(0, s.indexOf(\".\")).toLong).toSet\n  }\n\n  protected def getCrcFiles(dir: File): Seq[File] =\n    dir.listFiles().filter(f => FileNames.isChecksumFile(new Path(f.getCanonicalPath)))\n\n  protected def getCrcVersions(dir: File): Set[Long] =\n    getFileVersions(getCrcFiles(dir))\n\n  protected def getDeltaAndCrcFiles(dir: File): Seq[File] =\n    getDeltaFiles(dir) ++ getCrcFiles(dir)\n\n\n  protected def getDeltaVersions(dir: File): Set[Long] = {\n    val backfilledDeltaVersions = getFileVersions(getDeltaFiles(dir))\n    val unbackfilledDeltaVersions = getUnbackfilledDeltaVersions(dir)\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      // The unbackfilled commit files (except commit 0) should be a superset of the backfilled\n      // commit files since they're always deleted together in this suite.\n      assert(\n        unbackfilledDeltaVersions.toArray.sorted.startsWith(\n          backfilledDeltaVersions.filter(_ != 0).toArray.sorted))\n    }\n    backfilledDeltaVersions\n  }\n\n  protected def getUnbackfilledDeltaFiles(dir: File): Seq[File] = {\n    val commitDirPath = FileNames.commitDirPath(new Path(dir.toURI))\n    getDeltaFiles(new File(commitDirPath.toUri))\n  }\n\n  protected def getUnbackfilledDeltaVersions(dir: File): Set[Long] =\n    getFileVersions(getUnbackfilledDeltaFiles(dir))\n\n  protected def getSidecarFiles(log: DeltaLog): Set[String] = {\n    new java.io.File(log.sidecarDirPath.toUri)\n      .listFiles()\n      .filter(_.getName.endsWith(\".parquet\"))\n      .map(_.getName)\n      .toSet\n  }\n\n  protected def getCheckpointVersions(dir: File): Set[Long] = {\n    getFileVersions(getCheckpointFiles(dir))\n  }\n\n  /** Compares the given versions with expected and generates a nice error message. */\n  protected def compareVersions(\n      versions: Set[Long],\n      logType: String,\n      expected: Iterable[Int]): Unit = {\n    val expectedSet = expected.map(_.toLong).toSet\n    val deleted = expectedSet -- versions\n    val notDeleted = versions -- expectedSet\n    if (!(deleted.isEmpty && notDeleted.isEmpty)) {\n      fail(s\"\"\"Mismatch in log clean up for ${logType}s:\n           |Shouldn't be deleted but deleted: ${deleted.toArray.sorted.mkString(\"[\", \", \", \"]\")}\n           |Should be deleted but not: ${notDeleted.toArray.sorted.mkString(\"[\", \", \", \"]\")}\n         \"\"\".stripMargin)\n    }\n  }\n\n  // Set modification time of the new files in _delta_log directory and mark them as visited.\n  def setModificationTimeOfNewFiles(\n      log: DeltaLog,\n      clock: ManualClock,\n      visitedFiled: mutable.Set[String]): Unit = {\n    val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n    val allFiles = fs.listFiles(log.logPath, true)\n    while (allFiles.hasNext) {\n      val file = allFiles.next()\n      if (!visitedFiled.contains(file.getPath.toString)) {\n        visitedFiled += file.getPath.toString\n        fs.setTimes(file.getPath, clock.getTimeMillis(), 0)\n      }\n    }\n  }\n\n  protected def setModificationTime(\n      log: DeltaLog,\n      startTime: Long,\n      version: Int,\n      dayNum: Int,\n      fs: FileSystem,\n      checkpointOnly: Boolean = false): Unit = {\n    val paths = log\n      .listFrom(version)\n      .collect { case FileNames.CheckpointFile(f, v) if v == version => f.getPath }\n      .toSeq\n    paths.foreach { cpPath =>\n      // Add some second offset so that we don't have files with same timestamps\n      fs.setTimes(cpPath, day(startTime, dayNum) + version * 1000, 0)\n    }\n    if (!checkpointOnly) {\n      val deltaPath = new Path(log.logPath, new Path(f\"$version%020d.json\"))\n      if (fs.exists(deltaPath)) {\n        // Add some second offset so that we don't have files with same timestamps\n        fs.setTimes(deltaPath, day(startTime, dayNum) + version * 1000, 0)\n      }\n      // Add the same timestamp for unbackfilled delta files as well\n      fs.listStatus(FileNames.commitDirPath(log.logPath))\n        .find(_.getPath.getName.startsWith(f\"$version%020d\"))\n        .foreach(f => fs.setTimes(f.getPath, day(startTime, dayNum) + version * 1000, 0))\n    }\n  }\n\n  protected def day(startTime: Long, day: Int): Long =\n    startTime + intervalStringToMillis(s\"interval $day days\")\n\n  /**\n   * Creates a sidecar file with the given AddFiles.\n   *\n   * @param log The DeltaLog to which the sidecar file will be added.\n   * @param files A sequence of integers representing the AddFile indices.\n   * @return The name of the created sidecar file.\n   */\n  protected def createSidecarFile(log: DeltaLog, files: Seq[Int]): String = {\n    val sparkSession = spark\n    // scalastyle:off sparkimplicits\n    import sparkSession.implicits._\n    // scalastyle:on sparkimplicits\n    var sidecarFileName: String = \"\"\n    withTempDir { dir =>\n      val snapshot = log.unsafeVolatileSnapshot\n      val isRowTrackingEnabled = RowTracking.isEnabled(snapshot.protocol, snapshot.metadata)\n\n      val adds = files.map { i =>\n        val baseAddFile = createTestAddFile(i.toString)\n        if (isRowTrackingEnabled) {\n          // When [[RowTrackingFeature]] is enabled, assign `baseRowId` and\n          // `defaultRowCommitVersion` to match what would happen during actual commit.\n          // Otherwise, CRC validation will fail for subsequent commits after the first\n          // checkpoint, since the AddFiles from state reconstruction (checkpoint) differ\n          // from the incremental ones.\n          baseAddFile.copy(\n            baseRowId = Some((i - 1).toLong),\n            // Use 1L as the default row commit version for the mock AddFiles in the sidecar file.\n            defaultRowCommitVersion = Some(1L))\n        } else {\n          baseAddFile\n        }\n      }\n\n      adds.map(_.wrap).toDF.repartition(1).write.mode(\"overwrite\").parquet(dir.getAbsolutePath)\n      val srcPath =\n        new Path(dir.listFiles().filter(_.getName.endsWith(\"parquet\")).head.getAbsolutePath)\n      val dstPath = new Path(log.sidecarDirPath, srcPath.getName)\n      val fs = srcPath.getFileSystem(log.newDeltaHadoopConf())\n      fs.mkdirs(log.sidecarDirPath)\n      fs.rename(srcPath, dstPath)\n      sidecarFileName = fs.getFileStatus(dstPath).getPath.getName\n    }\n    sidecarFileName\n  }\n\n  // Create a V2 Checkpoint at given version with given sidecar files.\n  protected def createV2CheckpointWithSidecarFile(\n      log: DeltaLog,\n      version: Long,\n      sidecarFileNames: Seq[String]): Unit = {\n    val hadoopConf = log.newDeltaHadoopConf()\n    val fs = log.logPath.getFileSystem(hadoopConf)\n    val sidecarFiles = sidecarFileNames.map { fileName =>\n      val sidecarPath = new Path(log.sidecarDirPath, fileName)\n      val fileStatus = SerializableFileStatus.fromStatus(fs.getFileStatus(sidecarPath))\n      SidecarFile(fileStatus)\n    }\n    val snapshot = log.getSnapshotAt(version)\n    val actionsForCheckpoint =\n      snapshot.nonFileActions ++ sidecarFiles :+ CheckpointMetadata(version)\n    val v2CheckpointFormat =\n      spark.conf.getOption(DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key)\n    v2CheckpointFormat match {\n      case Some(V2Checkpoint.Format.JSON.name) | None =>\n        log.store.write(\n          newV2CheckpointJsonFile(log.logPath, version),\n          actionsForCheckpoint.map(_.json).toIterator,\n          overwrite = true,\n          hadoopConf = hadoopConf)\n      case Some(V2Checkpoint.Format.PARQUET.name) =>\n        val parquetFile = newV2CheckpointParquetFile(log.logPath, version)\n        val sparkSession = spark\n        // scalastyle:off sparkimplicits\n        import sparkSession.implicits._\n        // scalastyle:on sparkimplicits\n        val dfToWrite = actionsForCheckpoint.map(_.wrap).toDF\n        Checkpoints.createCheckpointV2ParquetFile(\n          spark,\n          dfToWrite,\n          parquetFile,\n          hadoopConf,\n          useRename = false)\n      case _ =>\n        assert(false, \"Invalid v2 checkpoint format\")\n    }\n    log.writeLastCheckpointFile(\n      log,\n      LastCheckpointInfo(version, -1, None, None, None, None),\n      false)\n  }\n\n  /**\n   * Start a txn that disables automatic log cleanup. Some tests may need to manually clean up logs\n   * to get deterministic behaviors.\n   */\n  protected def startTxnWithManualLogCleanup(log: DeltaLog): OptimisticTransaction = {\n    val txn = log.startTransaction()\n    // This will pick up `spark.databricks.delta.properties.defaults.enableExpiredLogCleanup` to\n    // disable log cleanup.\n    txn.updateMetadata(Metadata())\n    txn\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSinkImplicitCastSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.sql.{Date, Timestamp}\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.Relocated.StreamExecution\nimport org.apache.spark.sql.delta.sources.{DeltaSink, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\n\nimport org.apache.spark.{SparkArithmeticException, SparkConf, SparkThrowable}\nimport org.apache.spark.sql.{DataFrame, Encoder, Row}\nimport org.apache.spark.sql.errors.QueryExecutionErrors.toSQLType\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy\nimport org.apache.spark.sql.streaming.{OutputMode, StreamingQueryException, Trigger}\nimport org.apache.spark.sql.types._\n\n/**\n * Defines helper class & methods to test writing to a Delta streaming sink using data types that\n * don't match the corresponding column type in the table schema.\n */\nabstract class DeltaSinkImplicitCastSuiteBase extends DeltaSinkTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key, \"true\")\n    spark.conf.set(SQLConf.ANSI_ENABLED.key, \"true\")\n  }\n\n  /**\n   * Helper to write to and read from a Delta sink. Creates and runs a streaming query for each call\n   * to `write`.\n   */\n  class TestDeltaStream[T: Encoder](\n      outputDir: File,\n      checkpointDir: File) {\n    private val source = MemoryStream[T]\n\n    def write(data: T*)(selectExpr: String*): Unit =\n      write(\n        outputMode = OutputMode.Append,\n        timeout = streamingTimeout,\n        extraOptions = Map.empty)(\n        data: _*)(\n        selectExpr: _*)\n\n    def write(\n        outputMode: OutputMode,\n        timeout: Duration,\n        extraOptions: Map[String, String])(\n        data: T*)(\n        selectExpr: String*): Unit = {\n      source.addData(data)\n      val query =\n        source.toDF()\n          .selectExpr(selectExpr: _*)\n          .writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .outputMode(outputMode)\n          .options(extraOptions)\n          .format(\"delta\")\n          .trigger(Trigger.AvailableNow())\n          .start(outputDir.getCanonicalPath)\n      try {\n        failAfter(timeout) {\n          query.processAllAvailable()\n        }\n      } finally {\n        query.stop()\n      }\n    }\n\n    def currentSchema: StructType =\n      spark.read.format(\"delta\").load(outputDir.getCanonicalPath).schema\n\n    def read(): DataFrame =\n      spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n\n    def deltaLog: DeltaLog =\n      DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n  }\n\n  /** Sets up a new [[TestDeltaStream]] to write to and read from a test Delta sink. */\n  def withDeltaStream[T: Encoder](f: TestDeltaStream[T] => Unit): Unit =\n    withTempDirs { (outputDir, checkpointDir) =>\n      f(new TestDeltaStream[T](outputDir, checkpointDir))\n    }\n\n  /**\n   * Validates that the table history for the test Delta sink matches the given list of operations.\n   */\n  def checkOperationHistory[T](stream: TestDeltaStream[T], expectedOperations: Seq[String])\n  : Unit = {\n    val history = sql(s\"DESCRIBE HISTORY delta.`${stream.deltaLog.dataPath}`\")\n      .sort(\"version\")\n      .select(\"operation\")\n    checkAnswer(history, expectedOperations.map(Row(_)))\n  }\n}\n\n/**\n * Covers handling implicit casting to handle type mismatches when writing data to a Delta sink.\n */\nclass DeltaSinkImplicitCastSuite extends DeltaSinkImplicitCastSuiteBase\n  with CatalogOwnedTestBaseSuite {\n  import testImplicits._\n\n  test(s\"write wider type - long -> int\") {\n    withDeltaStream[Long] { stream =>\n      // This is the first write in this test suite, use a larger timeout to allow for the initial\n      // streaming setup to take place.\n      stream.write(\n        outputMode = OutputMode.Append,\n        timeout = 600.seconds,\n        extraOptions = Map.empty)(17)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(17))\n\n      stream.write(23)(\"CAST(value AS LONG)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(17) :: Row(23) :: Nil)\n      checkOperationHistory(stream, expectedOperations = Seq(\n        \"STREAMING UPDATE\", // First write\n        \"STREAMING UPDATE\"  // Second write\n      ))\n    }\n  }\n\n  test(\"write wider type - long -> int - overflow with \" +\n    s\"storeAssignmentPolicy=${StoreAssignmentPolicy.STRICT}\") {\n    withDeltaStream[Long] { stream =>\n      stream.write(17)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(17))\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString) {\n        val ex = intercept[StreamingQueryException] {\n          stream.write(Long.MaxValue)(\"CAST(value AS LONG)\")\n        }\n        checkError(\n          ex.getCause.asInstanceOf[SparkThrowable],\n          \"CANNOT_UP_CAST_DATATYPE\",\n          parameters = Map(\n            \"expression\" -> \"value\",\n            \"sourceType\" -> toSQLType(\"BIGINT\"),\n            \"targetType\" -> toSQLType(\"INT\"),\n            \"details\" -> (\"The type path of the target object is:\\n\\nYou can either add an \" +\n              \"explicit cast to the input data or choose a higher precision type of the field in \" +\n              \"the target object\")\n          )\n        )\n      }\n    }\n  }\n\n  test(\"write wider type - long -> int - overflow with \" +\n    s\"storeAssignmentPolicy=${StoreAssignmentPolicy.ANSI}\") {\n    withDeltaStream[Long] { stream =>\n      stream.write(17)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(17))\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.ANSI.toString) {\n        val ex = intercept[StreamingQueryException] {\n          stream.write(Long.MaxValue)(\"CAST(value AS LONG)\")\n        }\n\n        def getSparkArithmeticException(ex: Throwable): SparkArithmeticException = ex match {\n          case e: SparkArithmeticException => e\n          case e: Throwable if e.getCause != null => getSparkArithmeticException(e.getCause)\n          case e => fail(s\"Unexpected exception: $e\")\n        }\n        checkError(\n          getSparkArithmeticException(ex),\n          \"CAST_OVERFLOW_IN_TABLE_INSERT\",\n          parameters = Map(\n          \"sourceType\" -> \"\\\"BIGINT\\\"\",\n          \"targetType\" -> \"\\\"INT\\\"\",\n          \"columnName\" -> \"`value`\")\n        )\n      }\n    }\n  }\n\n  test(\"write wider type - long -> int - overflow with \" +\n    s\"storeAssignmentPolicy=${StoreAssignmentPolicy.LEGACY}\") {\n    withDeltaStream[Long] { stream =>\n      stream.write(17)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(17))\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString) {\n        stream.write(Long.MaxValue)(\"CAST(value AS LONG)\")\n        // LEGACY allows the value to silently overflow.\n        checkAnswer(stream.read(), Row(17) :: Row(-1) :: Nil)\n      }\n    }\n  }\n\n  test(\"write wider type - Decimal(10, 4) -> Decimal(6, 2)\") {\n    withDeltaStream[BigDecimal] { stream =>\n      stream.write(BigDecimal(123456L, scale = 2))(\"CAST(value AS DECIMAL(6, 2))\")\n      assert(stream.currentSchema(\"value\").dataType === DecimalType(6, 2))\n      checkAnswer(stream.read(), Row(BigDecimal(123456L, scale = 2)))\n\n      stream.write(BigDecimal(987654L, scale = 4))(\"CAST(value AS DECIMAL(10, 4))\")\n      assert(stream.currentSchema(\"value\").dataType === DecimalType(6, 2))\n      checkAnswer(stream.read(),\n        Row(BigDecimal(123456L, scale = 2)) :: Row(BigDecimal(9877L, scale = 2)) :: Nil\n      )\n    }\n  }\n\n  test(\"write narrower type - int -> long\") {\n    withDeltaStream[Long] { stream =>\n      stream.write(Long.MinValue)(\"CAST(value AS LONG)\")\n      assert(stream.currentSchema(\"value\").dataType === LongType)\n      checkAnswer(stream.read(), Row(Long.MinValue))\n\n      stream.write(23)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === LongType)\n      checkAnswer(stream.read(), Row(Long.MinValue) :: Row(23) :: Nil)\n    }\n  }\n\n  test(\"write different type - date -> string\") {\n    withDeltaStream[String] { stream =>\n      stream.write(\"abc\")(\"CAST(value AS STRING)\")\n      assert(stream.currentSchema(\"value\").dataType === StringType)\n      checkAnswer(stream.read(), Row(\"abc\"))\n\n      stream.write(\"2024-07-25\")(\"CAST(value AS DATE)\")\n      assert(stream.currentSchema(\"value\").dataType === StringType)\n      checkAnswer(stream.read(), Row(\"abc\") :: Row(\"2024-07-25\") :: Nil)\n    }\n  }\n\n  test(\"implicit cast in nested struct/array/map\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"named_struct('a', value) AS s\")\n      assert(stream.currentSchema(\"s\").dataType === new StructType().add(\"a\", IntegerType))\n      checkAnswer(stream.read(), Row(Row(17)))\n\n      stream.write(-12)(\"named_struct('a', CAST(value AS LONG)) AS s\")\n      assert(stream.currentSchema(\"s\").dataType === new StructType().add(\"a\", IntegerType))\n      checkAnswer(stream.read(), Row(Row(17)) :: Row(Row(-12)) :: Nil)\n    }\n\n    withDeltaStream[(Int, Int)] { stream =>\n      stream.write((17, 57))(\"map(_1, _2) AS m\")\n      assert(stream.currentSchema(\"m\").dataType === MapType(IntegerType, IntegerType))\n      checkAnswer(stream.read(), Row(Map(17 -> 57)))\n      stream.write((-12, 3))(\"map(CAST(_1 AS LONG), CAST(_2 AS STRING)) AS m\")\n      assert(stream.currentSchema(\"m\").dataType === MapType(IntegerType, IntegerType))\n      checkAnswer(stream.read(), Row(Map(17 -> 57)) :: Row(Map(-12 -> 3)) :: Nil)\n    }\n\n    withDeltaStream[(Int, Int)] { stream =>\n      stream.write((17, 57))(\"array(_1, _2) AS a\")\n      assert(stream.currentSchema(\"a\").dataType === ArrayType(IntegerType))\n      checkAnswer(stream.read(), Row(Seq(17, 57)) :: Nil)\n      stream.write((-12, 3))(\"array(_1, _2) AS a\")\n      assert(stream.currentSchema(\"a\").dataType === ArrayType(IntegerType))\n      checkAnswer(stream.read(), Row(Seq(17, 57)) :: Row(Seq(-12, 3)) :: Nil)\n    }\n  }\n\n  test(\"write invalid nested type - array -> struct\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"named_struct('a', value) AS s\")\n      assert(stream.currentSchema(\"s\").dataType === new StructType().add(\"a\", IntegerType))\n      checkAnswer(stream.read(), Row(Row(17)))\n\n      val ex = intercept[StreamingQueryException] {\n        stream.write(-12)(\"array(value) AS s\")\n      }\n      checkError(\n        ex.getCause.asInstanceOf[SparkThrowable],\n        \"DELTA_FAILED_TO_MERGE_FIELDS\",\n        parameters = Map(\n        \"currentField\" -> \"s\",\n        \"updateField\" -> \"s\")\n      )\n    }\n  }\n\n  test(\"implicit cast on partition value\") {\n    withDeltaStream[(String, Int)] { stream =>\n      sql(\n        s\"\"\"\n          |CREATE TABLE delta.`${stream.deltaLog.dataPath}` (day date, value int)\n          |USING DELTA\n          |PARTITIONED BY (day)\n        \"\"\".stripMargin)\n\n      stream.write((\"2024-07-26\", 1))(\"CAST(_1 AS DATE) AS day\", \"_2 AS value\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"day\", DateType)\n        .add(\"value\", IntegerType))\n      checkAnswer(stream.read(), Row(Date.valueOf(\"2024-07-26\"), 1))\n\n      stream.write((\"2024-07-27\", 2))(\n        \"CAST(_1 AS TIMESTAMP) AS day\", \"CAST(_2 AS DECIMAL(4, 1)) AS value\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"day\", DateType)\n        .add(\"value\", IntegerType))\n      checkAnswer(stream.read(),\n        Row(Date.valueOf(\"2024-07-26\"), 1) :: Row(Date.valueOf(\"2024-07-27\"), 2) :: Nil)\n    }\n  }\n\n  test(\"implicit cast with schema evolution\") {\n    withDeltaStream[(Long, String)] { stream =>\n      stream.write((123, \"unused\"))(\"CAST(_1 AS DECIMAL(6, 3)) AS a\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", DecimalType(6, 3)))\n      checkAnswer(stream.read(), Row(BigDecimal(123000, scale = 3)))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        stream.write((678, \"abc\"))(\"CAST(_1 AS LONG) AS a\", \"_2 AS b\")\n        assert(stream.currentSchema === new StructType()\n          .add(\"a\", DecimalType(6, 3))\n          .add(\"b\", StringType))\n        checkAnswer(stream.read(),\n          Row(BigDecimal(123000, scale = 3), null) ::\n          Row(BigDecimal(678000, scale = 3), \"abc\") :: Nil)\n      }\n    }\n  }\n\n  test(\"implicit cast with schema overwrite\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val source = MemoryStream[Long]\n\n      def write(streamingDF: DataFrame, data: Long*): Unit = {\n        val query = streamingDF.writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .outputMode(OutputMode.Complete)\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n        try {\n          source.addData(data: _*)\n          failAfter(streamingTimeout) {\n            query.processAllAvailable()\n          }\n        } finally {\n          query.stop()\n        }\n      }\n\n      // Initial write to the sink with columns a, count, b, c.\n      val initialDF = source.toDF()\n        .selectExpr(\"CAST(value AS DECIMAL(6, 3)) AS a\")\n        .groupBy(\"a\")\n        .count()\n        .withColumn(\"b\", col(\"count\").cast(\"INT\"))\n        .withColumn(\"c\", lit(11).cast(\"STRING\"))\n      write(initialDF, 10)\n      val initialResult = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n      assert(initialResult.schema === new StructType()\n        .add(\"a\", DecimalType(6, 3))\n        .add(\"count\", LongType)\n        .add(\"b\", IntegerType)\n        .add(\"c\", StringType))\n      checkAnswer(initialResult, Row(BigDecimal(10000, scale = 3), 1, 1, \"11\"))\n\n      // Second write with overwrite schema: change type of column b and replace c with d.\n      val overwriteDF = source.toDF()\n        .selectExpr(\"CAST(value AS DECIMAL(6, 3)) AS a\")\n        .groupBy(\"a\")\n        .count()\n        .withColumn(\"b\", col(\"count\").cast(\"LONG\"))\n        .withColumn(\"d\", lit(21).cast(\"STRING\"))\n      write(overwriteDF, 20)\n      val overwriteResult = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n      assert(overwriteResult.schema === new StructType()\n        .add(\"a\", DecimalType(6, 3))\n        .add(\"count\", LongType)\n        .add(\"b\", LongType)\n        .add(\"d\", StringType))\n      checkAnswer(overwriteResult,\n        Row(BigDecimal(10000, scale = 3), 1, 1, \"21\") ::\n        Row(BigDecimal(20000, scale = 3), 1, 1, \"21\") :: Nil\n      )\n    }\n  }\n\n  // Writing to a delta sink is always case insensitive and ignores the value of\n  // 'spark.sql.caseSensitive'.\n  for (caseSensitive <- Seq(true, false))\n  test(s\"implicit cast with case sensitivity, caseSensitive=$caseSensitive\") {\n    withDeltaStream[Long] { stream =>\n      stream.write(17)(\"CAST(value AS LONG) AS value\")\n      assert(stream.currentSchema === new StructType().add(\"value\", LongType))\n      checkAnswer(stream.read(), Row(17))\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> caseSensitive.toString) {\n        stream.write(23)(\"CAST(value AS INT) AS VALUE\")\n        assert(stream.currentSchema === new StructType().add(\"value\", LongType))\n        checkAnswer(stream.read(), Row(17) :: Row(23) :: Nil)\n      }\n    }\n  }\n\n  test(\"implicit cast and missing column\") {\n    withDeltaStream[(String, String)] { stream =>\n      stream.write((\"2024-07-28 12:00:00\", \"abc\"))(\"CAST(_1 AS TIMESTAMP) AS a\", \"_2 AS b\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", TimestampType)\n        .add(\"b\", StringType))\n      checkAnswer(stream.read(), Row(Timestamp.valueOf(\"2024-07-28 12:00:00\"), \"abc\"))\n\n      stream.write((\"2024-07-29\", \"unused\"))(\"CAST(_1 AS DATE) AS a\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", TimestampType)\n        .add(\"b\", StringType))\n      checkAnswer(stream.read(),\n        Row(Timestamp.valueOf(\"2024-07-28 12:00:00\"), \"abc\") ::\n        Row(Timestamp.valueOf(\"2024-07-29 00:00:00\"), null) :: Nil)\n      checkOperationHistory(stream, expectedOperations = Seq(\n        \"STREAMING UPDATE\", // First write\n        \"STREAMING UPDATE\"  // Second write\n      ))\n    }\n  }\n\n  test(\"implicit cast after renaming/dropping columns with column mapping\") {\n    withDeltaStream[(Int, Int)] { stream =>\n      stream.write((1, 100))(\"_1 AS a\", \"CAST(_2 AS LONG) AS b\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", LongType))\n      checkAnswer(stream.read(), Row(1, 100))\n      sql(\n        s\"\"\"\n           |ALTER TABLE delta.`${stream.deltaLog.dataPath}` SET TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.minReaderVersion' = '2',\n           |  'delta.minWriterVersion' = '5'\n           |)\n         \"\"\".stripMargin)\n\n      sql(s\"ALTER TABLE delta.`${stream.deltaLog.dataPath}` DROP COLUMN a\")\n      sql(s\"ALTER TABLE delta.`${stream.deltaLog.dataPath}` RENAME COLUMN b to a\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", LongType))\n\n      stream.write((17, -1))(\"CAST(_1 AS STRING) AS a\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", LongType))\n      checkAnswer(stream.read(), Row(100) :: Row(17) :: Nil)\n\n      checkOperationHistory(stream, expectedOperations = Seq(\n        \"STREAMING UPDATE\",  // First write\n        \"SET TBLPROPERTIES\", // Enable column mapping\n        \"DROP COLUMNS\",      // Drop column\n        \"RENAME COLUMN\",     // Rename Column\n        \"STREAMING UPDATE\"   // Second write\n      ))\n    }\n  }\n\n  test(\"disallow implicit cast with spark.databricks.delta.streaming.sink.allowImplicitCasts\") {\n    withSQLConf(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> \"false\") {\n      withDeltaStream[Long] { stream =>\n        stream.write(17)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === IntegerType)\n        checkAnswer(stream.read(), Row(17))\n\n        val ex = intercept[StreamingQueryException] {\n          stream.write(23)(\"CAST(value AS LONG)\")\n        }\n        checkError(\n          ex.getCause.asInstanceOf[SparkThrowable],\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\n          \"currentField\" -> \"value\",\n          \"updateField\" -> \"value\")\n        )\n      }\n    }\n  }\n\n  for (allowImplicitCasts <- Seq(true, false))\n  test(s\"schema evolution with case sensitivity and without type mismatch, \" +\n    s\"allowImplicitCasts=$allowImplicitCasts\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> allowImplicitCasts.toString,\n      SQLConf.CASE_SENSITIVE.key -> \"true\",\n      DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\"\n    ) {\n      withDeltaStream[(Long, Long)] { stream =>\n        stream.write((17, -1))(\"CAST(_1 AS INT) AS a\")\n        assert(stream.currentSchema == new StructType().add(\"a\", IntegerType))\n        checkAnswer(stream.read(), Row(17))\n\n        stream.write((21, 22))(\"CAST(_1 AS INT) AS A\", \"_2 AS b\")\n        assert(stream.currentSchema == new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", LongType))\n        checkAnswer(stream.read(), Row(17, null) :: Row(21, 22) :: Nil)\n      }\n    }\n  }\n\n  test(\"handling type mismatch in addBatch\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      sqlContext.sparkContext.setLocalProperty(StreamExecution.QUERY_ID_KEY, \"streaming_query\")\n      val sink = DeltaSink(\n        sqlContext,\n        path = deltaLog.dataPath,\n        partitionColumns = Seq.empty,\n        outputMode = OutputMode.Append(),\n        options = new DeltaOptions(options = Map.empty, conf = spark.sessionState.conf)\n      )\n\n      val schema = new StructType().add(\"value\", IntegerType)\n\n      {\n        val data = Seq(0, 1).toDF(\"value\").selectExpr(\"CAST(value AS INT)\")\n        sink.addBatch(0, data)\n        val df = spark.read.format(\"delta\").load(tablePath)\n        assert(df.schema === schema)\n        checkAnswer(df, Row(0) :: Row(1) :: Nil)\n      }\n      {\n        val data = Seq(2, 3).toDF(\"value\").selectExpr(\"CAST(value AS LONG)\")\n        sink.addBatch(1, data)\n        val df = spark.read.format(\"delta\").load(tablePath)\n        assert(df.schema === schema)\n        checkAnswer(df, Row(0) :: Row(1) :: Row(2) :: Row(3) :: Nil)\n      }\n    }\n  }\n}\n\nclass DeltaSinkImplicitCastWithCatalogManagedBatch1Suite extends DeltaSinkImplicitCastSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSinkImplicitCastWithCatalogManagedBatch2Suite extends DeltaSinkImplicitCastSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaSinkImplicitCastWithCatalogManagedBatch100Suite extends DeltaSinkImplicitCastSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSinkSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.Locale\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.CommitInfo\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.{DeltaSink, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.{MemoryStream, MicroBatchExecution, StreamingQueryWrapper}\nimport org.apache.commons.io.FileUtils\nimport org.scalatest.time.SpanSugar._\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.execution.DataSourceScanExec\nimport org.apache.spark.sql.execution.datasources._\nimport org.apache.spark.sql.execution.streaming.sources.WriteToMicroBatchDataSourceV1\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.streaming._\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\nabstract class DeltaSinkTest\n  extends StreamTest\n  with DeltaSQLCommandTest {\n\n  override val streamingTimeout = 60.seconds\n  import testImplicits._\n\n  // Before we start running the tests in this suite, we should let Spark perform all necessary set\n  // up that needs to be done for streaming. Without this, the first test in the suite may be flaky\n  // as its running time can exceed the timeout for the test due to Spark setup. See: ES-235735\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Int].toDF()\n      val query = inputData.writeStream\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .format(\"delta\")\n        .start(outputDir.getCanonicalPath)\n\n      query.stop()\n    }\n  }\n\n  protected def withTempDirs(f: (File, File) => Unit): Unit = {\n    withTempDir { file1 =>\n      withTempDir { file2 =>\n        f(file1, file2)\n      }\n    }\n  }\n}\n\nclass DeltaSinkSuite\n  extends DeltaSinkTest\n  with DeltaColumnMappingTestUtils\n  with CatalogOwnedTestBaseSuite\n  with DeltaSQLTestUtils {\n\n  import testImplicits._\n\n  test(\"append mode\") {\n    failAfter(streamingTimeout) {\n      withTempDirs { (outputDir, checkpointDir) =>\n        val inputData = MemoryStream[Int]\n        val df = inputData.toDF()\n        val query = df.writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n        val log = DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n        try {\n          inputData.addData(1)\n          query.processAllAvailable()\n\n          val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n          checkDatasetUnorderly(outputDf.as[Int], 1)\n          assert(log.update().transactions.head == (query.id.toString -> 0L))\n\n          inputData.addData(2)\n          query.processAllAvailable()\n\n          checkDatasetUnorderly(outputDf.as[Int], 1, 2)\n          assert(log.update().transactions.head == (query.id.toString -> 1L))\n\n          inputData.addData(3)\n          query.processAllAvailable()\n\n          checkDatasetUnorderly(outputDf.as[Int], 1, 2, 3)\n          assert(log.update().transactions.head == (query.id.toString -> 2L))\n        } finally {\n          query.stop()\n        }\n      }\n    }\n  }\n\n  test(\"complete mode\") {\n    failAfter(streamingTimeout) {\n      withTempDirs { (outputDir, checkpointDir) =>\n        val inputData = MemoryStream[Int]\n        val df = inputData.toDF()\n        val query =\n          df.groupBy().count()\n            .writeStream\n            .outputMode(\"complete\")\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"delta\")\n            .start(outputDir.getCanonicalPath)\n        val log = DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n        try {\n          inputData.addData(1)\n          query.processAllAvailable()\n\n          val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n          checkDatasetUnorderly(outputDf.as[Long], 1L)\n          assert(log.update().transactions.head == (query.id.toString -> 0L))\n\n          inputData.addData(2)\n          query.processAllAvailable()\n\n          checkDatasetUnorderly(outputDf.as[Long], 2L)\n          assert(log.update().transactions.head == (query.id.toString -> 1L))\n\n          inputData.addData(3)\n          query.processAllAvailable()\n\n          checkDatasetUnorderly(outputDf.as[Long], 3L)\n          assert(log.update().transactions.head == (query.id.toString -> 2L))\n        } finally {\n          query.stop()\n        }\n      }\n    }\n  }\n\n  test(\"update mode: not supported\") {\n    failAfter(streamingTimeout) {\n      withTempDirs { (outputDir, checkpointDir) =>\n        val inputData = MemoryStream[Int]\n        val df = inputData.toDF()\n        val e = intercept[AnalysisException] {\n          df.writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .outputMode(\"update\")\n            .format(\"delta\")\n            .start(outputDir.getCanonicalPath)\n        }\n        Seq(\"update\", \"not support\").foreach { msg =>\n          assert(e.getMessage.toLowerCase(Locale.ROOT).contains(msg))\n        }\n      }\n    }\n  }\n\n  test(\"path not specified\") {\n    failAfter(streamingTimeout) {\n      withTempDir { checkpointDir =>\n        val inputData = MemoryStream[Int]\n        val df = inputData.toDF()\n        val e = intercept[IllegalArgumentException] {\n          df.writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"delta\")\n            .start()\n        }\n        Seq(\"path\", \" not specified\").foreach { msg =>\n          assert(e.getMessage.toLowerCase(Locale.ROOT).contains(msg))\n        }\n      }\n    }\n  }\n\n  test(\"SPARK-21167: encode and decode path correctly\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[String]\n      val query = inputData.toDS()\n        .map(s => (s, s.length))\n        .toDF(\"value\", \"len\")\n        .writeStream\n        .partitionBy(\"value\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .format(\"delta\")\n        .start(outputDir.getCanonicalPath)\n\n      try {\n        // The output is partitioned by \"value\", so the value will appear in the file path.\n        // This is to test if we handle spaces in the path correctly.\n        inputData.addData(\"hello world\")\n        failAfter(streamingTimeout) {\n          query.processAllAvailable()\n        }\n        val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n        checkDatasetUnorderly(outputDf.as[(String, Int)], (\"hello world\", \"hello world\".length))\n      } finally {\n        query.stop()\n      }\n    }\n  }\n\n  test(\"partitioned writing and batch reading\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Int]\n      val ds = inputData.toDS()\n      val query =\n        ds.map(i => (i, i * 1000))\n          .toDF(\"id\", \"value\")\n          .writeStream\n          .partitionBy(\"id\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      try {\n\n        inputData.addData(1, 2, 3)\n        failAfter(streamingTimeout) {\n          query.processAllAvailable()\n        }\n\n        val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n        val expectedSchema = new StructType()\n          .add(StructField(\"id\", IntegerType))\n          .add(StructField(\"value\", IntegerType))\n        assert(outputDf.schema === expectedSchema)\n\n        // Verify the correct partitioning schema has been inferred\n        val hadoopFsRelations = outputDf.queryExecution.analyzed.collect {\n          case LogicalRelationWithTable(baseRelation, _) if\n              baseRelation.isInstanceOf[HadoopFsRelation] =>\n            baseRelation.asInstanceOf[HadoopFsRelation]\n        }\n        assert(hadoopFsRelations.size === 1)\n        assert(hadoopFsRelations.head.partitionSchema.exists(_.name == \"id\"))\n        assert(hadoopFsRelations.head.dataSchema.exists(_.name == \"value\"))\n\n        // Verify the data is correctly read\n        checkDatasetUnorderly(\n          outputDf.as[(Int, Int)],\n          (1, 1000), (2, 2000), (3, 3000))\n\n        /** Check some condition on the partitions of the FileScanRDD generated by a DF */\n        def checkFileScanPartitions(df: DataFrame)(func: Seq[FilePartition] => Unit): Unit = {\n          val filePartitions = df.queryExecution.executedPlan.collect {\n            case scan: DataSourceScanExec if scan.inputRDDs().head.isInstanceOf[FileScanRDD] =>\n              scan.inputRDDs().head.asInstanceOf[FileScanRDD].filePartitions\n          }.flatten\n          if (filePartitions.isEmpty) {\n            fail(s\"No FileScan in query\\n${df.queryExecution}\")\n          }\n          func(filePartitions)\n        }\n\n        // Read without pruning\n        checkFileScanPartitions(outputDf) { partitions =>\n          // There should be as many distinct partition values as there are distinct ids\n          assert(partitions.flatMap(_.files.map(_.partitionValues)).distinct.size === 3)\n        }\n\n        // Read with pruning, should read only files in partition dir id=1\n        checkFileScanPartitions(outputDf.filter(\"id = 1\")) { partitions =>\n          // use physical name\n          val filesToBeRead = partitions.flatMap(_.files)\n          assert(filesToBeRead.forall(_.partitionValues.getInt(0) == 1))\n          assert(filesToBeRead.map(_.partitionValues).distinct.size === 1)\n        }\n\n        // Read with pruning, should read only files in partition dir id=1 and id=2\n        checkFileScanPartitions(outputDf.filter(\"id in (1,2)\")) { partitions =>\n          val filesToBeRead = partitions.flatMap(_.files)\n          assert(filesToBeRead.forall(_.partitionValues.getInt(0) != 3))\n          assert(filesToBeRead.map(_.partitionValues).distinct.size === 2)\n        }\n      } finally {\n        if (query != null) {\n          query.stop()\n        }\n      }\n    }\n  }\n\n  test(\"work with aggregation + watermark\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Long]\n      val inputDF = inputData.toDF.toDF(\"time\")\n      val outputDf = inputDF\n        .selectExpr(\"CAST(time AS timestamp) AS timestamp\")\n        .withWatermark(\"timestamp\", \"10 seconds\")\n        .groupBy(window($\"timestamp\", \"5 seconds\"))\n        .count()\n        .select(\"window.start\", \"window.end\", \"count\")\n\n      val query =\n        outputDf.writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      try {\n        def addTimestamp(timestampInSecs: Int*): Unit = {\n          inputData.addData(timestampInSecs.map(_ * 1L): _*)\n          failAfter(streamingTimeout) {\n            query.processAllAvailable()\n          }\n        }\n\n        def check(expectedResult: ((Long, Long), Long)*): Unit = {\n          val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n            .selectExpr(\n              \"CAST(start as BIGINT) AS start\",\n              \"CAST(end as BIGINT) AS end\",\n              \"count\")\n          checkDatasetUnorderly(\n            outputDf.as[(Long, Long, Long)],\n            expectedResult.map(x => (x._1._1, x._1._2, x._2)): _*)\n        }\n\n        addTimestamp(100) // watermark = None before this, watermark = 100 - 10 = 90 after this\n        addTimestamp(104, 123) // watermark = 90 before this, watermark = 123 - 10 = 113 after this\n\n        addTimestamp(140) // wm = 113 before this, emit results on 100-105, wm = 130 after this\n        check((100L, 105L) -> 2L, (120L, 125L) -> 1L) // no-data-batch emits results on 120-125\n\n      } finally {\n        if (query != null) {\n          query.stop()\n        }\n      }\n    }\n  }\n\n  test(\"throw exception when users are trying to write in batch with different partitioning\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Int]\n      val ds = inputData.toDS()\n      val query =\n        ds.map(i => (i, i * 1000))\n          .toDF(\"id\", \"value\")\n          .writeStream\n          .partitionBy(\"id\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      try {\n\n        inputData.addData(1, 2, 3)\n        failAfter(streamingTimeout) {\n          query.processAllAvailable()\n        }\n\n        val e = intercept[AnalysisException] {\n          spark.range(100)\n            .select('id.cast(\"integer\"), 'id % 4 as \"by4\", 'id.cast(\"integer\") * 1000 as \"value\")\n            .write\n            .format(\"delta\")\n            .partitionBy(\"id\", \"by4\")\n            .mode(\"append\")\n            .save(outputDir.getCanonicalPath)\n        }\n        assert(e.getMessage.contains(\"Partition columns do not match\"))\n\n      } finally {\n        query.stop()\n      }\n    }\n  }\n\n  testQuietly(\"incompatible schema merging throws errors - first streaming then batch\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Int]\n      val ds = inputData.toDS()\n      val query =\n        ds.map(i => (i, i * 1000))\n          .toDF(\"id\", \"value\")\n          .writeStream\n          .partitionBy(\"id\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      try {\n\n        inputData.addData(1, 2, 3)\n        failAfter(streamingTimeout) {\n          query.processAllAvailable()\n        }\n\n        val e = intercept[AnalysisException] {\n          spark.range(100).select('id, ('id * 3).cast(\"string\") as \"value\")\n            .write\n            .partitionBy(\"id\")\n            .format(\"delta\")\n            .mode(\"append\")\n            .save(outputDir.getCanonicalPath)\n        }\n        checkError(\n          e,\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\"currentField\" -> \"id\", \"updateField\" -> \"id\"))\n      } finally {\n        query.stop()\n      }\n    }\n  }\n\n  test(\"incompatible schema merging throws errors - first batch then streaming\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Int]\n      val ds = inputData.toDS()\n      val dsWriter =\n        ds.map(i => (i, i * 1000))\n          .toDF(\"id\", \"value\")\n          .writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n      spark.range(100).select('id, ('id * 3).cast(\"string\") as \"value\")\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(outputDir.getCanonicalPath)\n\n      // More tests covering type changes can be found in [[DeltaSinkImplicitCastSuite]]. This only\n      // covers type changes disabled.\n      withSQLConf(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> \"false\") {\n        val wrapperException = intercept[StreamingQueryException] {\n          val q = dsWriter.start(outputDir.getCanonicalPath)\n          inputData.addData(1, 2, 3)\n          q.processAllAvailable()\n        }\n        assert(wrapperException.cause.isInstanceOf[AnalysisException])\n        checkError(\n          wrapperException.cause.asInstanceOf[AnalysisException],\n          \"DELTA_FAILED_TO_MERGE_FIELDS\",\n          parameters = Map(\"currentField\" -> \"id\", \"updateField\" -> \"id\"))\n      }\n    }\n  }\n\n  private def verifyDeltaSinkCatalog(f: DataStreamWriter[_] => StreamingQuery): Unit = {\n    // Create a Delta sink whose target table is defined by our caller.\n    val input = MemoryStream[Int]\n    val streamWriter = input.toDF\n      .writeStream\n      .format(\"delta\")\n      .option(\n        \"checkpointLocation\",\n        Utils.createTempDir(namePrefix = \"tahoe-test\").getCanonicalPath)\n    val q = f(streamWriter).asInstanceOf[StreamingQueryWrapper]\n\n    // WARNING: Only the query execution thread is allowed to initialize the logical plan (enforced\n    // by an assertion in MicroBatchExecution.scala). To avoid flaky failures, run the stream to\n    // completion, to guarantee the query execution thread ran before we try to access the plan.\n    try {\n      input.addData(1, 2, 3)\n      q.processAllAvailable()\n    } finally {\n      q.stop()\n    }\n\n    val plan = q.streamingQuery.logicalPlan\n    val WriteToMicroBatchDataSourceV1(catalogTable, sink: DeltaSink, _, _, _, _, _) = plan\n    assert(catalogTable === sink.catalogTable)\n  }\n\n  test(\"DeltaSink.catalogTable is correctly populated - catalog-based table\") {\n    withTable(\"tab\") {\n      verifyDeltaSinkCatalog(_.toTable(\"tab\"))\n    }\n  }\n\n  test(\"DeltaSink.catalogTable is correctly populated - path-based table\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n      verifyDeltaSinkCatalog(_.start(tempDir.getCanonicalPath))\n    }\n  }\n\n  test(\"can't write out with all columns being partition columns\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n      val inputData = MemoryStream[Int]\n      val ds = inputData.toDS()\n      val query =\n        ds.map(i => (i, i * 1000))\n          .toDF(\"id\", \"value\")\n          .writeStream\n          .partitionBy(\"id\", \"value\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      val e = intercept[StreamingQueryException] {\n        inputData.addData(1)\n        query.awaitTermination(30000)\n      }\n      assert(e.cause.isInstanceOf[AnalysisException])\n    }\n  }\n\n  test(\"streaming write correctly sets isBlindAppend in CommitInfo\") {\n    withTempDirs { (outputDir, checkpointDir) =>\n\n      val input = MemoryStream[Int]\n      val inputDataStream = input.toDF().toDF(\"value\")\n\n      def tableData: DataFrame = spark.read.format(\"delta\").load(outputDir.toString)\n\n      def appendToTable(df: DataFrame): Unit = failAfter(streamingTimeout) {\n        var q: StreamingQuery = null\n        try {\n          input.addData(0)\n          q = df.writeStream\n            .format(\"delta\")\n            .option(\"checkpointLocation\", checkpointDir.toString)\n            .start(outputDir.toString)\n          q.processAllAvailable()\n        } finally {\n          if (q != null) q.stop()\n        }\n      }\n\n      var lastCheckedVersion = -1L\n      def isLastCommitBlindAppend: Boolean = {\n        val log = DeltaLog.forTable(spark, outputDir.toString)\n        val lastVersion = log.update().version\n        assert(lastVersion > lastCheckedVersion, \"no new commit was made\")\n        lastCheckedVersion = lastVersion\n        val lastCommitChanges = log.getChanges(lastVersion).toSeq.head._2\n        lastCommitChanges.collectFirst { case c: CommitInfo => c }.flatMap(_.isBlindAppend).get\n      }\n\n      // Simple streaming write should have isBlindAppend = true\n      appendToTable(inputDataStream)\n      assert(\n        isLastCommitBlindAppend,\n        \"simple write to target table should have isBlindAppend = true\")\n\n      // Join with the table should have isBlindAppend = false\n      appendToTable(inputDataStream.join(tableData, \"value\"))\n      assert(\n        !isLastCommitBlindAppend,\n        \"joining with target table in the query should have isBlindAppend = false\")\n    }\n  }\n\n  test(\"do not trust user nullability, so that parquet files aren't corrupted\") {\n    val jsonRec = \"\"\"{\"s\": \"ss\", \"b\": {\"s\": \"ss\"}}\"\"\"\n    val schema = new StructType()\n      .add(\"s\", StringType)\n      .add(\"b\", new StructType()\n        .add(\"s\", StringType)\n        .add(\"i\", IntegerType, nullable = false))\n      .add(\"c\", IntegerType, nullable = false)\n\n    withTempDir { base =>\n      val sourceDir = new File(base, \"source\").getCanonicalPath\n      val tableDir = new File(base, \"output\").getCanonicalPath\n      val chkDir = new File(base, \"checkpoint\").getCanonicalPath\n\n      FileUtils.write(new File(sourceDir, \"a.json\"), jsonRec)\n\n      val q = spark.readStream\n        .format(\"json\")\n        .schema(schema)\n        .load(sourceDir)\n        .withColumn(\"file\", input_file_name()) // Not sure why needs this to reproduce\n        .writeStream\n        .format(\"delta\")\n        .trigger(org.apache.spark.sql.streaming.Trigger.Once)\n        .option(\"checkpointLocation\", chkDir)\n        .start(tableDir)\n\n      q.awaitTermination()\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tableDir).drop(\"file\"),\n        Seq(Row(\"ss\", Row(\"ss\", null), null)))\n    }\n  }\n\n  test(\"history includes user-defined metadata for DataFrame.writeStream API\") {\n    failAfter(streamingTimeout) {\n      withTempDirs { (outputDir, checkpointDir) =>\n        val inputData = MemoryStream[Int]\n        val df = inputData.toDF()\n        val query = df.writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .option(\"userMetadata\", \"testMeta!\")\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n        val log = DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n\n        inputData.addData(1)\n        query.processAllAvailable()\n\n        val lastCommitInfo = io.delta.tables.DeltaTable.forPath(spark, outputDir.getCanonicalPath)\n            .history(1).as[DeltaHistory].head\n\n        assert(lastCommitInfo.userMetadata === Some(\"testMeta!\"))\n        query.stop()\n      }\n    }\n  }\n\n  test(\n    \"DeltaSink.deltaLog is not initialized in DeltaSink constructor\"\n  ) {\n    withTempTable(createTable = true) { tableName =>\n      val outputDir = DeltaLog.forTable(spark, TableIdentifier(tableName)).dataPath\n\n      // Create a DeltaSink instance directly\n      val deltaSink = new DeltaSink(\n        spark.sqlContext,\n        outputDir,\n        partitionColumns = Seq.empty[String],\n        outputMode = OutputMode.Append,\n        options = new DeltaOptions(Map(\n          \"checkpointlocation\" -> outputDir.toString,\n          \"path\" -> outputDir.toString\n        ), spark.sessionState.conf)\n      )\n\n      // Helper function to check if deltaLog is initialized using reflection\n      def isDeltaLogInitialized(sink: DeltaSink): Boolean = {\n        val fieldOpt = classOf[DeltaSink].getDeclaredFields.find(\n          f => f.getName.contains(\"deltaLog\") && f.getType == classOf[DeltaLog])\n        assert(fieldOpt.isDefined, \"deltaLog field not found\")\n        fieldOpt.exists { field =>\n          field.setAccessible(true)\n          field.get(sink) != null\n        }\n      }\n\n      // Test that deltaLog is NOT initialized after constructor\n      assert(!isDeltaLogInitialized(deltaSink),\n        \"deltaLog should not be initialized after constructor\")\n    }\n  }\n\n  test(\"DeltaSink rejects DataFrame with UDT containing NullType\") {\n    failAfter(streamingTimeout) {\n      withTempDirs { (outputDir, checkpointDir) =>\n        val inputData = MemoryStream[Int]\n        val ds = inputData.toDS()\n        val dsWriter =\n          ds.map(i => (i, new NullData()))\n            .toDF(\"id\", \"value\")\n            .writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"delta\")\n\n        val wrapperException = intercept[StreamingQueryException] {\n          val q = dsWriter.start(outputDir.getCanonicalPath)\n          inputData.addData(42)\n          q.processAllAvailable()\n        }\n        assert(wrapperException.cause.isInstanceOf[AnalysisException])\n        checkError(\n          wrapperException.cause.asInstanceOf[AnalysisException],\n          \"DELTA_NULL_SCHEMA_IN_STREAMING_WRITE\")\n      }\n    }\n  }\n}\n\n// Batch sizes 1, 2, and 100 exercise different backfill behaviors in the commit coordinator.\n// Batch size 1 triggers a backfill on every commit (commitVersion % 1 == 0), testing the most\n// granular backfill path. Batch size 2 triggers backfill every other commit, testing the boundary\n// between backfilled and unbackfilled commits. Batch size 100 leaves most commits unbackfilled,\n// testing the production-like path where streaming must read from both the commit coordinator\n// and the filesystem. This follows the same pattern as other CatalogManaged (CCv2) test suites\n// (DeltaLogSuite, DeltaSourceSuite, etc.).\n\nclass DeltaSinkWithCatalogManagedBatch1Suite extends DeltaSinkSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSinkWithCatalogManagedBatch2Suite extends DeltaSinkSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaSinkWithCatalogManagedBatch100Suite extends DeltaSinkSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\nabstract class DeltaSinkColumnMappingSuiteBase extends DeltaSinkSuite\n  with DeltaColumnMappingSelectedTestMixin {\n  import testImplicits._\n\n  override protected def runOnlyTests = Seq(\n    \"append mode\",\n    \"complete mode\",\n    \"partitioned writing and batch reading\",\n    \"work with aggregation + watermark\"\n  )\n\n\n  test(\"allow schema evolution after renaming column\") {\n    Seq(true, false).foreach { schemaMergeEnabled =>\n      withClue(s\"Schema merge enabled: $schemaMergeEnabled\") {\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaMergeEnabled.toString) {\n          failAfter(streamingTimeout) {\n            withTempDirs { (outputDir, checkpointDir) =>\n              val sourceDir = Utils.createTempDir()\n              def addData(df: DataFrame): Unit =\n                df.coalesce(1).write.mode(\"append\").save(sourceDir.getCanonicalPath)\n\n              // save data to target dir\n              Seq(100).toDF(\"value\").write.format(\"delta\").save(outputDir.getCanonicalPath)\n              // use parquet stream as MemoryStream doesn't support recovering failed batches\n              val df = spark.readStream\n                .schema(new StructType().add(\"value\", IntegerType, true))\n                .parquet(sourceDir.getCanonicalPath)\n              // start writing into Delta sink\n              def queryGen(df: DataFrame): StreamingQuery = df.writeStream\n                .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n                .format(\"delta\")\n                .start(outputDir.getCanonicalPath)\n\n              val query = queryGen(df)\n              val log = DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n\n              // delta sink contains [100, 1]\n              addData(Seq(1).toDF(\"value\"))\n              query.processAllAvailable()\n\n              def outputDf: DataFrame =\n                spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n              checkDatasetUnorderly(outputDf.as[Int], 100, 1)\n              require(log.update().transactions.head == (query.id.toString -> 0L))\n\n              sql(s\"ALTER TABLE delta.`${outputDir.getAbsolutePath}` \" +\n                s\"RENAME COLUMN value TO new_value\")\n\n              if (!schemaMergeEnabled) {\n                // schema has changed, we can't automatically migrate the schema\n                val e = intercept[StreamingQueryException] {\n                  addData(Seq(2).toDF(\"value\"))\n                  query.processAllAvailable()\n                }\n                assert(e.cause.isInstanceOf[AnalysisException])\n                assert(e.cause.getMessage.contains(\"A schema mismatch detected when writing\"))\n\n                // restart using the same query would still fail\n                val query2 = queryGen(df)\n                val e2 = intercept[StreamingQueryException] {\n                  addData(Seq(2).toDF(\"value\"))\n                  query2.processAllAvailable()\n                }\n                assert(e2.cause.isInstanceOf[AnalysisException])\n                assert(e2.cause.getMessage.contains(\"A schema mismatch detected when writing\"))\n\n                // but reingest using new schema should work\n                val df2 = spark.readStream\n                  .schema(new StructType().add(\"value\", IntegerType, true))\n                  .parquet(sourceDir.getCanonicalPath)\n                  .withColumnRenamed(\"value\", \"new_value\")\n                val query3 = queryGen(df2)\n                // delta sink contains [100, 1, 2] + [2, 2] due to recovering the failed batched\n                addData(Seq(2).toDF(\"value\"))\n                query3.processAllAvailable()\n                checkAnswer(outputDf,\n                  Row(100) :: Row(1) :: Row(2) :: Row(2) :: Row(2) :: Nil)\n                assert(outputDf.schema == new StructType().add(\"new_value\", IntegerType, true))\n                query3.stop()\n              } else {\n                // we allow auto schema migration, delta sink contains [100, 1, 2]\n                addData(Seq(2).toDF(\"value\"))\n                query.processAllAvailable()\n                // Since the incoming `value` column is now merged as a new column (even though it\n                // has the same value as the original name) in which only the 3rd record has data.\n                checkAnswer(outputDf, Row(100, null) :: Row(1, null) :: Row(null, 2) :: Nil)\n                assert(outputDf.schema ==\n                  new StructType().add(\"new_value\", IntegerType, true)\n                    .add(\"value\", IntegerType, true))\n                query.stop()\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"allow schema evolution after dropping column\") {\n    Seq(true, false).foreach { schemaMergeEnabled =>\n      withClue(s\"Schema merge enabled: $schemaMergeEnabled\") {\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaMergeEnabled.toString) {\n          failAfter(streamingTimeout) {\n            withTempDirs { (outputDir, checkpointDir) =>\n              val sourceDir = Utils.createTempDir()\n              def addData(df: DataFrame): Unit =\n                df.coalesce(1).write.mode(\"append\").save(sourceDir.getCanonicalPath)\n\n              // save data to target dir\n              Seq((1, 100)).toDF(\"id\", \"value\").write.format(\"delta\")\n                .save(outputDir.getCanonicalPath)\n\n              // use parquet stream as MemoryStream doesn't support recovering failed batches\n              val df = spark.readStream\n                .schema(new StructType().add(\"id\", IntegerType, true)\n                  .add(\"value\", IntegerType, true))\n                .parquet(sourceDir.getCanonicalPath)\n\n              // start writing into Delta sink\n              def queryGen(df: DataFrame): StreamingQuery = df.writeStream\n                .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n                .format(\"delta\")\n                .start(outputDir.getCanonicalPath)\n\n              val query = queryGen(df)\n              val log = DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n              // delta sink contains [(1, 100), (2, 200)]\n              addData(Seq((2, 200)).toDF(\"id\", \"value\"))\n              query.processAllAvailable()\n\n              def outputDf: DataFrame =\n                spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n\n              checkDatasetUnorderly(outputDf.as[(Int, Int)], (1, 100), (2, 200))\n              assert(log.update().transactions.head == (query.id.toString -> 0L))\n\n              withSQLConf(DeltaSQLConf.DELTA_ALTER_TABLE_DROP_COLUMN_ENABLED.key -> \"true\") {\n                sql(s\"ALTER TABLE delta.`${outputDir.getAbsolutePath}` DROP COLUMN value\")\n              }\n\n              if (!schemaMergeEnabled) {\n                // schema changed, we can't automatically migrate the schema\n                val e = intercept[StreamingQueryException] {\n                  addData(Seq((3, 300)).toDF(\"id\", \"value\"))\n                  query.processAllAvailable()\n                }\n                assert(e.cause.isInstanceOf[AnalysisException])\n                assert(e.cause.getMessage.contains(\"A schema mismatch detected when writing\"))\n\n                // restart using the same query would still fail\n                val query2 = queryGen(df)\n                val e2 = intercept[StreamingQueryException] {\n                  addData(Seq((3, 300)).toDF(\"id\", \"value\"))\n                  query2.processAllAvailable()\n                }\n                assert(e2.cause.isInstanceOf[AnalysisException])\n                assert(e2.cause.getMessage.contains(\"A schema mismatch detected when writing\"))\n\n                // but reingest using new schema should work\n                val df2 = spark.readStream\n                  .schema(new StructType().add(\"id\", IntegerType, true))\n                  .parquet(sourceDir.getCanonicalPath)\n                val query3 = queryGen(df2)\n                // delta sink contains [1, 2, 3] + [3, 3] due to\n                // recovering failed batches\n                addData(Seq((3, 300)).toDF(\"id\", \"value\"))\n                query3.processAllAvailable()\n                checkAnswer(outputDf,\n                  Row(1) :: Row(2) :: Row(3) :: Row(3) :: Row(3) :: Nil)\n                assert(outputDf.schema == new StructType().add(\"id\", IntegerType, true))\n                query3.stop()\n              } else {\n                addData(Seq((3, 300)).toDF(\"id\", \"value\"))\n                query.processAllAvailable()\n                // None/null value appears because even though the added column has the same\n                // logical name (`value`) as the dropped column, the physical name has been\n                // changed so the old data could not be loaded.\n                checkAnswer(outputDf, Row(1, null) :: Row(2, null) :: Row(3, 300) :: Nil)\n                assert(outputDf.schema ==\n                  new StructType().add(\"id\", IntegerType, true).add(\"value\", IntegerType, true))\n                query.stop()\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n}\n\nclass DeltaSinkIdColumnMappingSuite extends DeltaSinkColumnMappingSuiteBase\n  with DeltaColumnMappingEnableIdMode\n  with DeltaColumnMappingTestUtils\n\nclass DeltaSinkNameColumnMappingSuite extends DeltaSinkColumnMappingSuiteBase\n  with DeltaColumnMappingEnableNameMode\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.UUID\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.Relocated.StreamExecution\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.{DeltaSource, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.DeltaColumnMappingSelectedTestMixin\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.commons.io.FileUtils\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, Row}\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.StreamingExecutionRelation\nimport org.apache.spark.sql.streaming.{DataStreamReader, StreamTest}\nimport org.apache.spark.sql.types.{StringType, StructType}\nimport org.apache.spark.util.Utils\n\ntrait ColumnMappingStreamingTestUtils extends StreamTest with DeltaColumnMappingTestUtils {\n\n  // Whether we are requesting CDC streaming changes\n  protected def isCdcTest: Boolean\n\n  protected val ProcessAllAvailableIgnoreError = Execute { q =>\n    try {\n      q.processAllAvailable()\n    } catch {\n      case _: Throwable =>\n        // swallow the errors so we could check answer and failure on the query later\n    }\n  }\n\n  protected def isColumnMappingSchemaIncompatibleFailure(\n      t: Throwable,\n      detectedDuringStreaming: Boolean): Boolean = t match {\n    case e: DeltaStreamingNonAdditiveSchemaIncompatibleException =>\n      e.additionalProperties.get(\"detectedDuringStreaming\")\n        .exists(_.toBoolean == detectedDuringStreaming)\n    case _ => false\n  }\n\n  protected val ExpectStreamStartInCompatibleSchemaFailure =\n    ExpectFailure[DeltaStreamingNonAdditiveSchemaIncompatibleException] { t =>\n      assert(isColumnMappingSchemaIncompatibleFailure(t, detectedDuringStreaming = false))\n    }\n\n  protected val ExpectInStreamSchemaChangeFailure =\n    ExpectFailure[DeltaStreamingNonAdditiveSchemaIncompatibleException] { t =>\n      assert(isColumnMappingSchemaIncompatibleFailure(t, detectedDuringStreaming = true))\n    }\n\n  protected val ExpectGenericSchemaIncompatibleFailure =\n    ExpectFailure[DeltaStreamingNonAdditiveSchemaIncompatibleException]()\n\n  // Failure thrown by the current DeltaSource schema change incompatible check\n  protected val ExistingRetryableInStreamSchemaChangeFailure = Execute { q =>\n    // Similar to ExpectFailure but allows more fine-grained checking of exceptions\n    failAfter(streamingTimeout) {\n      try {\n        q.awaitTermination()\n      } catch {\n        case _: Throwable =>\n          // swallow the exception\n      }\n      val cause = ExceptionUtils.getRootCause(q.exception.get)\n      assert(cause.getMessage.contains(\"Detected schema change\"))\n    }\n  }\n\n  protected def getLatestCommittedDeltaVersion(q: StreamExecution): Long =\n    JsonUtils.fromJson[Map[String, Any]](\n      q.committedOffsets.values.head.json()\n    ).apply(\"reservoirVersion\").asInstanceOf[Number].longValue()\n\n  // Drop CDC fields because they are not useful for testing the blocking behavior\n  protected def dropCDCFields(df: DataFrame): DataFrame =\n    df.drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n      .drop(CDCReader.CDC_TYPE_COLUMN_NAME)\n      .drop(CDCReader.CDC_COMMIT_VERSION)\n}\n\ntrait ColumnMappingStreamingBlockedWorkflowSuiteBase extends ColumnMappingStreamingTestUtils {\n\n  import testImplicits._\n\n  // DataStreamReader to use\n  // Set a small max file per trigger to ensure we could catch failures ASAP\n  private def dsr: DataStreamReader = if (isCdcTest) {\n    spark.readStream.format(\"delta\")\n      .option(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, \"1\")\n      .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n  } else {\n    spark.readStream.format(\"delta\")\n      .option(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, \"1\")\n  }\n\n  private def checkStreamStartBlocked(\n      df: DataFrame,\n      ckpt: File,\n      expectedFailure: StreamAction): Unit = {\n    // Restart the stream from the same checkpoint will pick up the dropped schema and our\n    // column mapping check will kick in and error out.\n    testStream(df)(\n      StartStream(checkpointLocation = ckpt.getCanonicalPath),\n      ProcessAllAvailableIgnoreError,\n      // No batches have been served\n      CheckLastBatch(Nil: _*),\n      expectedFailure\n    )\n  }\n\n  protected def writeDeltaData(\n      data: Seq[Int],\n      deltaLog: DeltaLog,\n      userSpecifiedSchema: Option[StructType] = None): Unit = {\n    val schema = userSpecifiedSchema.getOrElse(deltaLog.update().schema)\n    data.foreach { i =>\n      val data = Seq(Row(schema.map(_ => i.toString): _*))\n      spark.createDataFrame(data.asJava, schema)\n        .write.format(\"delta\").mode(\"append\").save(deltaLog.dataPath.toString)\n    }\n  }\n\n  test(\"deltaLog snapshot should not be updated outside of the stream\") {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n      // write initial data\n      Seq(1).toDF(\"id\").write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      // record initial snapshot version and warm DeltaLog cache\n      val initialDeltaLog = DeltaLog.forTable(spark, tablePath)\n      // start streaming\n      val df = spark.readStream.format(\"delta\").load(tablePath)\n      testStream(df)(\n        StartStream(),\n        ProcessAllAvailable(),\n        AssertOnQuery { q =>\n          // write more data\n          Seq(2).toDF(\"id\").write.format(\"delta\").mode(\"append\").save(tablePath)\n          // update deltaLog externally\n          initialDeltaLog.update()\n          assert(initialDeltaLog.snapshot.version == 1)\n          // query start snapshot should not change\n          val source = q.logicalPlan.collectFirst {\n            case r: StreamingExecutionRelation =>\n              r.source.asInstanceOf[DeltaSource]\n          }.get\n          // same delta log but stream start version not affected\n          source.snapshotAtSourceInit.deltaLog == initialDeltaLog &&\n            source.snapshotAtSourceInit.version == 0\n        }\n      )\n    }\n  }\n\n  test(\"column mapping + streaming - allowed workflows - column addition\") {\n    // column addition schema evolution should not be blocked upon restart\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      writeDeltaData(0 until 5, deltaLog, Some(StructType.fromDDL(\"id string, value string\")))\n\n      val checkpointDir = new File(inputDir, \"_checkpoint\")\n\n      def loadDf(): DataFrame = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n\n      testStream(loadDf())(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        ProcessAllAvailable(),\n        CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n        Execute { _ =>\n          sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` ADD COLUMN (value2 string)\")\n        },\n        Execute { _ =>\n          writeDeltaData(5 until 10, deltaLog)\n        },\n        ExistingRetryableInStreamSchemaChangeFailure\n      )\n\n      testStream(loadDf())(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        ProcessAllAvailable(),\n        // Sink is reinitialized, only 5-10 are ingested\n        CheckAnswer(\n          (5 until 10).map(i => (i.toString, i.toString, i.toString)): _*)\n      )\n    }\n\n  }\n\n  test(\"column mapping + streaming - allowed workflows - upgrade to name mode\") {\n    // upgrade should not blocked both during the stream AND during stream restart\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withColumnMappingConf(\"none\") {\n        writeDeltaData(0 until 5, deltaLog, Some(StructType.fromDDL(\"id string, name string\")))\n      }\n\n      def createNewDf(): DataFrame = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n\n      val checkpointDir = new File(inputDir, \"_checkpoint\")\n\n      testStream(createNewDf())(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        ProcessAllAvailable(),\n        CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n        Execute { _ =>\n          sql(\n            s\"\"\"\n               |ALTER TABLE delta.`${inputDir.getCanonicalPath}`\n               |SET TBLPROPERTIES (\n               |  ${DeltaConfigs.COLUMN_MAPPING_MODE.key} = \"name\",\n               |  ${DeltaConfigs.MIN_READER_VERSION.key} = \"2\",\n               |  ${DeltaConfigs.MIN_WRITER_VERSION.key} = \"5\")\"\"\".stripMargin)\n        },\n        Execute { _ =>\n          writeDeltaData(5 until 10, deltaLog)\n        },\n        ProcessAllAvailable(),\n        CheckAnswer((0 until 10).map(i => (i.toString, i.toString)): _*),\n        // add column schema evolution should fail the stream\n        Execute { _ =>\n          sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` ADD COLUMN (value2 string)\")\n        },\n        Execute { _ =>\n          writeDeltaData(10 until 15, deltaLog)\n        },\n        ExistingRetryableInStreamSchemaChangeFailure\n      )\n\n      // but should not block after restarting, now in column mapping mode\n      testStream(createNewDf())(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        ProcessAllAvailable(),\n        // Sink is reinitialized, only 10-15 are ingested\n        CheckAnswer(\n          (10 until 15).map(i => (i.toString, i.toString, i.toString)): _*)\n      )\n\n      // use a different checkpoint to simulate a clean stream restart\n      val checkpointDir2 = new File(inputDir, \"_checkpoint2\")\n\n      testStream(createNewDf())(\n        StartStream(checkpointLocation = checkpointDir2.getCanonicalPath),\n        ProcessAllAvailable(),\n        // Since the latest schema contain the additional column, it is null for previous batches.\n        // This is fine as it is consistent with the current semantics.\n        CheckAnswer((0 until 10).map(i => (i.toString, i.toString, null)) ++\n          (10 until 15).map(i => (i.toString, i.toString, i.toString)): _*),\n        StopStream\n      )\n\n      // Refresh delta log so we could catch the latest schema with column mapping mode\n      deltaLog.update()\n      // test read prior to upgrade batches with latest metadata should also work\n      val checkpointDir3 = new File(inputDir, \"_checkpoint3\")\n      testStream(dropCDCFields(dsr.option(\"startingVersion\", 0).load(inputDir.getCanonicalPath)))(\n        StartStream(checkpointLocation = checkpointDir3.getCanonicalPath),\n        ProcessAllAvailable(),\n        // Since the latest schema contain the additional column, it is null for previous batches.\n        // This is fine as it is consistent with the current semantics.\n        CheckAnswer((0 until 10).map(i => (i.toString, i.toString, null)) ++\n          (10 until 15).map(i => (i.toString, i.toString, i.toString)): _*),\n        StopStream\n      )\n\n    }\n  }\n\n  /**\n   * Setup the test table for testing blocked workflow, this will create a id or name mode table\n   * based on which tests it is run.\n   */\n  protected def setupTestTable(deltaLog: DeltaLog): Unit = {\n    require(columnMappingModeString != NoMapping.name)\n    val tablePath = deltaLog.dataPath.toString\n\n    // For name mapping, we use upgrade to stir things up a little\n    if (columnMappingModeString == NameMapping.name) {\n      // initialize with no column mapping\n      withColumnMappingConf(\"none\") {\n        writeDeltaData(0 until 5, deltaLog, Some(StructType.fromDDL(\"id string, value string\")))\n      }\n\n      // upgrade to name mode\n      val protocol = deltaLog.snapshot.protocol\n        val (r, w) = if (protocol.supportsReaderFeatures || protocol.supportsWriterFeatures) {\n        (TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION,\n          TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n      } else {\n        (spark.conf\n          .get(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION)\n          .max(ColumnMappingTableFeature.minReaderVersion),\n          spark.conf\n            .get(DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION)\n            .max(ColumnMappingTableFeature.minWriterVersion))\n      }\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE delta.`${tablePath}`\n           |SET TBLPROPERTIES (\n           |  ${DeltaConfigs.COLUMN_MAPPING_MODE.key} = \"name\",\n           |  ${DeltaConfigs.MIN_READER_VERSION.key} = \"$r\",\n           |  ${DeltaConfigs.MIN_WRITER_VERSION.key} = \"$w\")\"\"\".stripMargin)\n\n      // write more data post upgrade\n      writeDeltaData(5 until 10, deltaLog)\n    }\n    // For id mapping, we could only create the table from scratch\n    else if (columnMappingModeString == IdMapping.name) {\n      withColumnMappingConf(\"id\") {\n        writeDeltaData(0 until 10, deltaLog, Some(StructType.fromDDL(\"id string, value string\")))\n      }\n    }\n  }\n\n  test(\n    \"column mapping + streaming: blocking workflow - drop column\"\n  ) {\n    val schemaAlterQuery = \"DROP COLUMN value\"\n    val schemaRestoreQuery = \"ADD COLUMN (value string)\"\n\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      setupTestTable(deltaLog)\n\n      // change schema\n      sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` $schemaAlterQuery\")\n\n      // write more data post change schema\n      writeDeltaData(10 until 15, deltaLog)\n\n      // Test the two code paths below\n      // Case 1 - Restart did not specify a start version, this will successfully serve the initial\n      //          entire existing data based on the initial snapshot's schema, which is basically\n      //          the stream schema, all schema changes in between are ignored.\n      //          But once the initial snapshot is served, all subsequent batches will fail if\n      //          encountering a schema change during streaming, and all restart effort should fail.\n      val checkpointDir = new File(inputDir, \"_checkpoint\")\n      val df = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n\n      testStream(df)(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        ProcessAllAvailable(),\n        // Initial data (pre + post upgrade + post change schema) all served\n        CheckAnswer((0 until 15).map(i => i.toString): _*),\n        Execute { _ =>\n          // write more data in new schema during streaming\n          writeDeltaData(15 until 20, deltaLog)\n        },\n        ProcessAllAvailable(),\n        // can still work because the schema is still compatible\n        CheckAnswer((0 until 20).map(i => i.toString): _*),\n        // But a new schema change would cause stream to fail\n        // Note here we are restoring back the original schema, see next case for how we test\n        // some extra special cases when schemas are reverted.\n        Execute { _ =>\n          sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` $schemaRestoreQuery\")\n        },\n        // write more data in updated schema again\n        Execute { _ =>\n          writeDeltaData(20 until 25, deltaLog)\n        },\n        // The last batch should not be processed and stream should fail\n        ProcessAllAvailableIgnoreError,\n        // sink data did not change\n        CheckAnswer((0 until 20).map(i => i.toString): _*),\n        // The schemaRestoreQuery for DROP column is ADD column so it fails a more benign error\n        ExistingRetryableInStreamSchemaChangeFailure\n      )\n\n      val df2 = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n      // Since the initial snapshot ignores all schema changes, the most recent schema change\n      // is just ADD COLUMN, which can be retried.\n      testStream(df2)(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        // but an additional drop should fail the stream as we are capturing data changes now\n        Execute { _ =>\n          sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` $schemaAlterQuery\")\n        },\n        ProcessAllAvailableIgnoreError,\n        ExpectInStreamSchemaChangeFailure\n      )\n      // The latest DROP columns blocks the stream.\n      if (isCdcTest) {\n        checkStreamStartBlocked(df2, checkpointDir, ExpectGenericSchemaIncompatibleFailure)\n      } else {\n        val expectedError = ExpectStreamStartInCompatibleSchemaFailure\n        checkStreamStartBlocked(df2, checkpointDir, expectedError)\n      }\n\n      // Case 2 - Specifically we use startingVersion=0 to simulate serving the entire table's data\n      //          in a streaming fashion, ignoring the initialSnapshot.\n      //          Here we test the special case when the latest schema is \"restored\".\n      val checkpointDir2 = new File(inputDir, \"_checkpoint2\")\n      val dfStartAtZero = dropCDCFields(dsr\n        .option(DeltaOptions.STARTING_VERSION_OPTION, \"0\")\n        .load(inputDir.getCanonicalPath))\n\n      if (isCdcTest) {\n        checkStreamStartBlocked(\n          dfStartAtZero, checkpointDir2, ExpectGenericSchemaIncompatibleFailure)\n      } else {\n        // In the case when we drop and add a column back\n        // the restart should still fail directly because all the historical batches with the same\n        // old logical name now will have a different physical name we would have data loss\n\n        // lets add back the column we just dropped before\n        sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` $schemaRestoreQuery\")\n        assert(DeltaLog.forTable(spark, inputDir.getCanonicalPath).snapshot.schema.size == 2)\n\n        // restart should block right away\n        checkStreamStartBlocked(\n          dfStartAtZero, checkpointDir, ExpectStreamStartInCompatibleSchemaFailure)\n      }\n    }\n  }\n\n\n  test(\"column mapping + streaming: blocking workflow - rename column\") {\n    val schemaAlterQuery = \"RENAME COLUMN value TO value2\"\n    val schemaRestoreQuery = \"RENAME COLUMN value2 TO value\"\n\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      setupTestTable(deltaLog)\n\n      // change schema\n      sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` $schemaAlterQuery\")\n\n      // write more data post change schema\n      writeDeltaData(10 until 15, deltaLog)\n\n      // Test the two code paths below\n      // Case 1 - Restart did not specify a start version, this will successfully serve the initial\n      //          entire existing data based on the initial snapshot's schema, which is basically\n      //          the stream schema, all schema changes in between are ignored.\n      //          But once the initial snapshot is served, all subsequent batches will fail if\n      //          encountering a schema change during streaming, and all restart effort should fail.\n      val checkpointDir = new File(inputDir, \"_checkpoint\")\n      def df: DataFrame = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n\n      testStream(df)(\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        ProcessAllAvailable(),\n        // Initial data (pre + post upgrade + post change schema) all served\n        CheckAnswer((0 until 15).map(i => (i.toString, i.toString)): _*),\n        Execute { _ =>\n          // write more data in new schema during streaming\n          writeDeltaData(15 until 20, deltaLog)\n        },\n        ProcessAllAvailable(),\n        // can still work because the schema is still compatible\n        CheckAnswer((0 until 20).map(i => (i.toString, i.toString)): _*),\n        // stop stream to allow schema change + data update to start in a batch\n        StopStream,\n        // But a new schema change would cause stream to fail\n        // Note here we are restoring back the original schema, see next case for how we test\n        // some extra special cases when schemas are reverted.\n        Execute { _ =>\n          writeDeltaData(20 until 25, deltaLog)\n          sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` $schemaRestoreQuery\")\n        }\n      )\n\n      val df2 = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n      testStream(df2)(\n        // Restart stream\n        StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n        // the last batch should not be processed because the batch cross an incompatible\n        // schema change.\n        ProcessAllAvailableIgnoreError,\n        // no data processed\n        CheckAnswer(Nil: _*),\n        // detected schema change while trying to generate the next offset\n        ExpectStreamStartInCompatibleSchemaFailure\n      )\n\n      // Case 2 - Specifically we use startingVersion=0 to simulate serving the entire table's data\n      //          in a streaming fashion, ignoring the initialSnapshot.\n      //          Here we test the special case when the latest schema is \"restored\".\n      if (isCdcTest) {\n        val checkpointDir2 = new File(inputDir, \"_checkpoint2\")\n        val dfStartAtZero = dropCDCFields(dsr\n          .option(DeltaOptions.STARTING_VERSION_OPTION, \"0\")\n          .load(inputDir.getCanonicalPath))\n        testStream(dfStartAtZero)(\n          StartStream(checkpointLocation = checkpointDir2.getCanonicalPath),\n          ProcessAllAvailableIgnoreError,\n          ExpectGenericSchemaIncompatibleFailure\n        )\n      } else {\n        // In the trickier case when we rename a column and rename back, we could not\n        // immediately detect the schema incompatibility at stream start, so we will move on.\n        // This is fine because the batches served will be compatible until the in-stream check\n        // finds another schema change action and fail.\n        val checkpointDir2 = new File(inputDir, s\"_checkpoint_${UUID.randomUUID.toString}\")\n        val dfStartAtZero = dropCDCFields(dsr\n          .option(DeltaOptions.STARTING_VERSION_OPTION, \"0\")\n          .load(inputDir.getCanonicalPath))\n        testStream(dfStartAtZero)(\n          // The stream could not move past version 10, because batches after which\n          // will be incompatible with the latest schema.\n          StartStream(checkpointLocation = checkpointDir2.getCanonicalPath),\n          ProcessAllAvailableIgnoreError,\n          AssertOnQuery { q =>\n            val latestCommittedVersion = getLatestCommittedDeltaVersion(q)\n            latestCommittedVersion <= 10\n          },\n          ExpectInStreamSchemaChangeFailure\n        )\n        // restart won't move forward either\n        val df2 = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n        checkStreamStartBlocked(df2, checkpointDir2, ExpectInStreamSchemaChangeFailure)\n      }\n    }\n  }\n\n  test(\"column mapping + streaming: blocking workflow - \" +\n    \"should not generate latestOffset past schema change\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      writeDeltaData(0 until 5, deltaLog,\n        userSpecifiedSchema = Some(\n          new StructType()\n          .add(\"id\", StringType, true)\n          .add(\"value\", StringType, true)))\n      // rename column\n      sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` RENAME COLUMN value TO value2\")\n      val renameVersion = deltaLog.update().version\n      // write more data\n      writeDeltaData(5 until 10, deltaLog)\n\n      // Case 1 - Stream start failure should not progress new latestOffset\n      // Since we had a rename, the data files prior to that should not be served with the renamed\n      // schema <id, value2>, but the original schema <id, value>. latestOffset() should not create\n      // a new offset moves past the schema change.\n      val df1 = dropCDCFields(\n        dsr.option(\"startingVersion\", \"1\") // start from 1 to ignore the initial schema change\n           .load(inputDir.getCanonicalPath))\n      testStream(df1)(\n        StartStream(), // fresh checkpoint\n        ProcessAllAvailableIgnoreError,\n        AssertOnQuery { q =>\n          // This should come from the latestOffset checker\n          q.availableOffsets.isEmpty && q.latestOffsets.isEmpty &&\n            q.exception.get.cause.getStackTrace.exists(_.toString.contains(\"latestOffset\"))\n        },\n        ExpectStreamStartInCompatibleSchemaFailure\n      )\n\n      // try drop column now\n      sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` DROP COLUMN value2\")\n      val dropVersion = deltaLog.update().version\n      // write more data\n      writeDeltaData(10 until 15, deltaLog)\n\n      val df2 = dropCDCFields(\n        dsr.option(\"startingVersion\", renameVersion + 1) // so we could detect drop column\n          .load(inputDir.getCanonicalPath))\n      testStream(df2)(\n        StartStream(), // fresh checkpoint\n        ProcessAllAvailableIgnoreError,\n        AssertOnQuery { q =>\n          // This should come from the latestOffset stream start checker\n          q.availableOffsets.isEmpty && q.latestOffsets.isEmpty &&\n            q.exception.get.cause.getStackTrace.exists(_.toString.contains(\"latestOffset\"))\n        },\n        ExpectStreamStartInCompatibleSchemaFailure\n      )\n\n      // Case 2 - in stream failure should not progress latest offset too\n      // This is the handle prior to SC-111607, which should cover the major cases.\n      def loadDf(): DataFrame = dropCDCFields(\n        dsr.option(\"startingVersion\", dropVersion + 1) // so we could move on to in stream failure\n           .load(inputDir.getCanonicalPath))\n\n      val ckpt = Utils.createTempDir().getCanonicalPath\n      var latestAvailableOffsets: Seq[String] = null\n      testStream(loadDf())(\n        StartStream(checkpointLocation = ckpt), // fresh checkpoint\n        ProcessAllAvailable(),\n        CheckAnswer((10 until 15).map(i => (i.toString)): _*),\n        Execute { q =>\n          latestAvailableOffsets = q.availableOffsets.values.map(_.json()).toSeq\n        },\n        // add more data and rename column\n        Execute { _ =>\n          sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` RENAME COLUMN id TO id2\")\n          writeDeltaData(15 until 16, deltaLog)\n        },\n        ProcessAllAvailableIgnoreError,\n        CheckAnswer((10 until 15).map(i => (i.toString)): _*), // no data processed\n        AssertOnQuery { q =>\n          // Available offsets should not change\n          // This should come from the latestOffset in-stream checker\n          q.availableOffsets.values.map(_.json()) == latestAvailableOffsets &&\n            q.latestOffsets.isEmpty &&\n            q.exception.get.cause.getStackTrace.exists(_.toString.contains(\"latestOffset\"))\n        },\n        ExpectInStreamSchemaChangeFailure\n      )\n\n      // Case 3 - resuming from existing checkpoint, note that getBatch's stream start check\n      // should be called instead of latestOffset for recovery.\n      // This is also the handle prior to SC-111607, which should cover the major cases.\n      testStream(loadDf())(\n        StartStream(checkpointLocation = ckpt), // existing checkpoint\n        ProcessAllAvailableIgnoreError,\n        CheckAnswer(Nil: _*),\n        AssertOnQuery { q =>\n          // This should come from the latestOffset in-stream checker\n          q.availableOffsets.values.map(_.json()) == latestAvailableOffsets &&\n            q.latestOffsets.isEmpty &&\n            q.exception.get.cause.getStackTrace.exists(_.toString.contains(\"getBatch\"))\n        },\n        ExpectStreamStartInCompatibleSchemaFailure\n      )\n    }\n  }\n\n  test(\"unsafe flag can unblock drop or rename column\") {\n    // upgrade should not blocked both during the stream AND during stream restart\n    withTempDir { inputDir =>\n      Seq(\n        s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` DROP COLUMN value\",\n        s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` RENAME COLUMN value TO value2\"\n      ).foreach { schemaChangeQuery =>\n        FileUtils.deleteDirectory(inputDir)\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        withColumnMappingConf(\"none\") {\n          writeDeltaData(0 until 5, deltaLog,\n            Some(StructType.fromDDL(\"id string, value string\")))\n        }\n\n        def createNewDf(): DataFrame = dropCDCFields(dsr.load(inputDir.getCanonicalPath))\n\n        val checkpointDir = new File(inputDir, s\"_checkpoint_${schemaChangeQuery.hashCode}\")\n        val isRename = schemaChangeQuery.contains(\"RENAME\")\n        testStream(createNewDf())(\n          StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n          ProcessAllAvailable(),\n          CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n          Execute { _ =>\n            sql(\n              s\"\"\"\n                 |ALTER TABLE delta.`${inputDir.getCanonicalPath}`\n                 |SET TBLPROPERTIES (\n                 |  ${DeltaConfigs.COLUMN_MAPPING_MODE.key} = \"name\",\n                 |  ${DeltaConfigs.MIN_READER_VERSION.key} = \"2\",\n                 |  ${DeltaConfigs.MIN_WRITER_VERSION.key} = \"5\")\"\"\".stripMargin)\n            // Add another schema change to ensure even after enable the flag, we would still hit\n            // a schema change with more columns than read schema so `verifySchemaChange` would see\n            // that can complain.\n            sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` ADD COLUMN (random STRING)\")\n            sql(schemaChangeQuery)\n            writeDeltaData(5 until 10, deltaLog)\n          },\n          ProcessAllAvailableIgnoreError,\n          ExistingRetryableInStreamSchemaChangeFailure\n        )\n\n        // Without the flag it would still fail\n        testStream(createNewDf())(\n          StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n          ProcessAllAvailableIgnoreError,\n          CheckAnswer(Nil: _*),\n          ExpectStreamStartInCompatibleSchemaFailure\n        )\n\n        val checkExpectedResult = if (isRename) {\n          CheckAnswer((5 until 10).map(i => (i.toString, i.toString, i.toString)): _*)\n        } else {\n          CheckAnswer((5 until 10).map(i => (i.toString, i.toString)): _*)\n        }\n\n        withSQLConf(DeltaSQLConf\n            .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES\n            .key -> \"true\") {\n          testStream(createNewDf())(\n            StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n            // The processing will pass, ignoring any schema column missing in the backfill.\n            ProcessAllAvailable(),\n            // Show up as dropped column\n            checkExpectedResult,\n            Execute { _ =>\n              // But any schema change post the stream analysis would still cause exceptions\n              // as usual, which is critical to avoid data loss.\n              sql(s\"ALTER TABLE delta.`${inputDir.getCanonicalPath}` ADD COLUMN (random2 STRING)\")\n            },\n            ProcessAllAvailableIgnoreError,\n            ExistingRetryableInStreamSchemaChangeFailure\n          )\n        }\n      }\n    }\n  }\n}\n\ntrait DeltaSourceColumnMappingSuiteBase extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests = Seq(\n    \"basic\",\n    \"maxBytesPerTrigger: metadata checkpoint\",\n    \"maxFilesPerTrigger: metadata checkpoint\",\n    \"allow to change schema before starting a streaming query\",\n\n    // streaming blocking semantics test\n    \"deltaLog snapshot should not be updated outside of the stream\",\n    \"column mapping + streaming - allowed workflows - column addition\",\n    \"column mapping + streaming - allowed workflows - upgrade to name mode\",\n    \"column mapping + streaming: blocking workflow - drop column\",\n    \"column mapping + streaming: blocking workflow - rename column\",\n    \"column mapping + streaming: blocking workflow - \" +\n      \"should not generate latestOffset past schema change\"\n  )\n}\n\nclass DeltaSourceIdColumnMappingSuite extends DeltaSourceSuite\n  with ColumnMappingStreamingBlockedWorkflowSuiteBase\n  with DeltaColumnMappingEnableIdMode\n  with DeltaSourceColumnMappingSuiteBase {\n\n  override protected def isCdcTest: Boolean = false\n\n}\n\nclass DeltaSourceNameColumnMappingSuite extends DeltaSourceSuite\n  with ColumnMappingStreamingBlockedWorkflowSuiteBase\n  with DeltaColumnMappingEnableNameMode\n  with DeltaSourceColumnMappingSuiteBase {\n\n  override protected def isCdcTest: Boolean = false\n\n}\n\n// Batch sizes 1, 2, and 100 exercise different backfill behaviors in the commit coordinator.\n// Batch size 1 triggers a backfill on every commit (commitVersion % 1 == 0), testing the most\n// granular backfill path. Batch size 2 triggers backfill every other commit, testing the boundary\n// between backfilled and unbackfilled commits. Batch size 100 leaves most commits unbackfilled,\n// testing the production-like path where streaming must read from both the commit coordinator\n// and the filesystem. This follows the same pattern as other CatalogManaged (CCv2) test suites\n// (DeltaLogSuite, DeltaCDCStreamSuite, etc.).\n\nclass DeltaSourceIdColumnMappingWithCatalogManagedBatch1Suite\n    extends DeltaSourceIdColumnMappingSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSourceIdColumnMappingWithCatalogManagedBatch2Suite\n    extends DeltaSourceIdColumnMappingSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaSourceIdColumnMappingWithCatalogManagedBatch100Suite\n    extends DeltaSourceIdColumnMappingSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\nclass DeltaSourceNameColumnMappingWithCatalogManagedBatch1Suite\n    extends DeltaSourceNameColumnMappingSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSourceNameColumnMappingWithCatalogManagedBatch2Suite\n    extends DeltaSourceNameColumnMappingSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaSourceNameColumnMappingWithCatalogManagedBatch100Suite\n    extends DeltaSourceNameColumnMappingSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceDeletionVectorsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.Relocated.StreamExecution\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.concurrent.Eventually\nimport org.scalatest.concurrent.PatienceConfiguration.Timeout\n\nimport org.apache.spark.sql.streaming.{StreamTest, Trigger}\nimport org.apache.spark.sql.streaming.util.StreamManualClock\n\ntrait DeltaSourceDeletionVectorTests extends StreamTest\n  with DeletionVectorsTestUtils {\n  self: DeltaSourceConnectorTrait =>\n\n  import testImplicits._\n\n  /**\n   * Executes a DML SQL statement (DELETE, INSERT, etc.).\n   * Overridable so that V2 suites can route DML through the V1 connector,\n   * since SparkTable (V2) is read-only and does not support writes.\n   */\n  protected def executeDml(sqlText: String): Unit = sql(sqlText)\n\n  test(\"allow to delete files before starting a streaming query\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n      executeDml(s\"DELETE FROM delta.`$inputDir`\")\n      (5 until 10).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n      deltaLog.checkpoint()\n      assert(deltaLog.readLastCheckpointFile().nonEmpty, \"this test requires a checkpoint\")\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      testStream(df)(\n        AssertOnQuery { q =>\n          q.processAllAvailable()\n          true\n        },\n        CheckAnswer((5 until 10).map(_.toString): _*))\n    }\n  }\n\n  test(\"allow to delete files before staring a streaming query without checkpoint\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n      executeDml(s\"DELETE FROM delta.`$inputDir`\")\n      (5 until 7).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n      assert(deltaLog.readLastCheckpointFile().isEmpty, \"this test requires no checkpoint\")\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      testStream(df)(\n        AssertOnQuery { q =>\n          q.processAllAvailable()\n          true\n        },\n        CheckAnswer((5 until 7).map(_.toString): _*))\n    }\n  }\n\n  /**\n   * If deletion vectors are expected here, return true if they are present. If none are expected,\n   * return true if none are present.\n   */\n  protected def deletionVectorsPresentIfExpected(\n      inputDir: String,\n      expectDVs: Boolean): Boolean = {\n    val deltaLog = DeltaLog.forTable(spark, inputDir)\n    val filesWithDVs = getFilesWithDeletionVectors(deltaLog)\n    logWarning(s\"Expecting DVs=$expectDVs - found ${filesWithDVs.size}\")\n    if (expectDVs) {\n      filesWithDVs.nonEmpty\n    } else {\n      filesWithDVs.isEmpty\n    }\n  }\n\n  private def ignoreOperationsTest(\n      inputDir: String,\n      sourceOptions: Seq[(String, String)],\n      sqlCommand: String,\n      commandShouldProduceDVs: Option[Boolean] = None)(expectations: StreamAction*): Unit = {\n    (0 until 10 by 2).foreach { i =>\n      Seq(i, i + 1).toDF().coalesce(1).write.format(\"delta\").mode(\"append\").save(inputDir)\n    }\n\n    val df = loadStreamWithOptions(inputDir, sourceOptions.toMap)\n    val expectDVs = commandShouldProduceDVs.getOrElse(\n      sqlCommand.toUpperCase().startsWith(\"DELETE\"))\n\n    val base = Seq(\n      AssertOnQuery { q =>\n        q.processAllAvailable()\n        true\n      },\n      CheckAnswer((0 until 10): _*),\n      AssertOnQuery { q =>\n        executeDml(sqlCommand)\n        deletionVectorsPresentIfExpected(inputDir, expectDVs)\n      })\n\n    testStream(df)((base ++ expectations): _*)\n  }\n\n  private def ignoreOperationsTestWithManualClock(\n      inputDir: String,\n      sourceOptions: Seq[(String, String)],\n      sqlCommand1: String,\n      sqlCommand2: String,\n      command1ShouldProduceDVs: Option[Boolean] = None,\n      command2ShouldProduceDVs: Option[Boolean] = None,\n      expectations: List[StreamAction]): Unit = {\n    val clock = new StreamManualClock\n\n    (0 until 15 by 3).foreach { i =>\n      Seq(i, i + 1, i + 2).toDF().coalesce(1).write.format(\"delta\").mode(\"append\").save(inputDir)\n    }\n    val log = DeltaLog.forTable(spark, inputDir)\n    val commitVersionBeforeDML = log.update().version\n    val df = loadStreamWithOptions(inputDir, sourceOptions.toMap)\n    def expectDVsInCommand(shouldProduceDVs: Option[Boolean], command: String): Boolean = {\n      shouldProduceDVs.getOrElse(command.toUpperCase().startsWith(\"DELETE\"))\n    }\n    val expectDVsInCommand1 = expectDVsInCommand(command1ShouldProduceDVs, sqlCommand1)\n    val expectDVsInCommand2 = expectDVsInCommand(command2ShouldProduceDVs, sqlCommand2)\n\n    // If it's expected to fail we must be sure not to actually process it in here,\n    // or it'll fail too early instead of being caught by ExpectFailure.\n    val shouldFailAfterCommands = expectations.exists(_.isInstanceOf[ExpectFailure[_]])\n\n    val baseActions: Seq[StreamAction] = Seq(\n      StartStream(Trigger.ProcessingTime(1000), clock),\n      AdvanceManualClock(1000L),\n      CheckAnswer((0 until 15): _*),\n      AssertOnQuery { q =>\n        // Ensure we only processed a single batch since the initial data load.\n        q.commitLog.getLatestBatchId().get == 0\n      },\n      AssertOnQuery { q =>\n        eventually(\"Stream never stopped processing\") {\n          // Wait until the stream stops processing, so we aren't racing with the next two\n          // commands on whether or not they end up in the same batch.\n          assert(!q.status.isTriggerActive)\n          assert(!q.status.isDataAvailable)\n        }\n        true\n      },\n      AssertOnQuery { q =>\n        executeDml(sqlCommand1)\n        deletionVectorsPresentIfExpected(inputDir, expectDVsInCommand1)\n      },\n      AssertOnQuery { q =>\n        executeDml(sqlCommand2)\n        deletionVectorsPresentIfExpected(inputDir, expectDVsInCommand2)\n      },\n      AssertOnQuery { q =>\n        // Ensure we still didn't process the DML commands.\n        q.commitLog.getLatestBatchId().get == 0\n      },\n      // Advance the clock, so that we process the two DML commands.\n      AdvanceManualClock(2000L)) ++\n      (if (shouldFailAfterCommands) {\n         Seq.empty[StreamAction]\n       } else {\n         Seq(\n           // This makes it move to the next batch.\n           AssertOnQuery { q =>\n             eventually(\"Next batch was never processed\") {\n               // Ensure we only processed a single batch with the DML commands.\n               assert(q.commitLog.getLatestBatchId().get === 1)\n             }\n             true\n           })\n       })\n\n    testStream(df)((baseActions ++ expectations): _*)\n  }\n\n  protected def eventually[T](message: String)(func: => T): T = {\n    try {\n      Eventually.eventually(Timeout(streamingTimeout)) {\n        func\n      }\n    } catch {\n      case NonFatal(e) =>\n        fail(message, e)\n    }\n  }\n\n  testQuietly(s\"deleting files fails query if ignoreDeletes = false\") {\n    withTempDir { inputDir =>\n      ignoreOperationsTest(\n        inputDir.getAbsolutePath,\n        sourceOptions = Nil,\n        sqlCommand = s\"DELETE FROM delta.`$inputDir`\",\n        // Whole table deletes do not produce DVs.\n        commandShouldProduceDVs = Some(false))(ExpectFailure[DeltaUnsupportedOperationException] {\n        e =>\n          for (msg <- Seq(\"Detected deleted data\", \"not supported\", \"ignoreDeletes\", \"true\")) {\n            assert(e.getMessage.contains(msg))\n          }\n      })\n    }\n  }\n\n  Seq(\"ignoreFileDeletion\", DeltaOptions.IGNORE_DELETES_OPTION).foreach { ignoreDeletes =>\n    testQuietly(\n      s\"allow to delete files after staring a streaming query when $ignoreDeletes is true\") {\n      withTempDir { inputDir =>\n        ignoreOperationsTest(\n          inputDir.getAbsolutePath,\n          sourceOptions = Seq(ignoreDeletes -> \"true\"),\n          sqlCommand = s\"DELETE FROM delta.`$inputDir`\",\n          // Whole table deletes do not produce DVs.\n          commandShouldProduceDVs = Some(false))(\n          AssertOnQuery { q =>\n            Seq(10).toDF().write.format(\"delta\").mode(\"append\").save(inputDir.getAbsolutePath)\n            q.processAllAvailable()\n            true\n          },\n          CheckAnswer((0 to 10): _*))\n      }\n    }\n  }\n\n  case class SourceChangeVariant(\n      label: String,\n      query: File => String,\n      answerWithIgnoreChanges: Seq[Int])\n\n  val sourceChangeVariants: Seq[SourceChangeVariant] = Seq(\n    // A partial file delete is treated like an update by the Source.\n    SourceChangeVariant(\n      label = \"DELETE\",\n      query = inputDir => s\"DELETE FROM delta.`$inputDir` WHERE value = 3\",\n      // 2 occurs in the same file as 3, so it gets duplicated during processing.\n      answerWithIgnoreChanges = (0 to 10) :+ 2))\n\n  for (variant <- sourceChangeVariants)\n  testQuietly(\n    \"updating the source table causes failure when ignoreChanges = false\" +\n      s\" - using ${variant.label}\") {\n    withTempDir { inputDir =>\n      ignoreOperationsTest(\n        inputDir.getAbsolutePath,\n        sourceOptions = Nil,\n        sqlCommand = variant.query(inputDir))(\n        ExpectFailure[DeltaUnsupportedOperationException] { e =>\n          for (msg <- Seq(\"data update\", \"not supported\", \"skipChangeCommits\", \"true\")) {\n            assert(e.getMessage.contains(msg))\n          }\n        })\n    }\n  }\n\n  for (variant <- sourceChangeVariants)\n  testQuietly(\n    \"allow to update the source table when ignoreChanges = true\" +\n      s\" - using ${variant.label}\") {\n    withTempDir { inputDir =>\n      ignoreOperationsTest(\n        inputDir.getAbsolutePath,\n        sourceOptions = Seq(DeltaOptions.IGNORE_CHANGES_OPTION -> \"true\"),\n        sqlCommand = variant.query(inputDir))(\n        AssertOnQuery { q =>\n          Seq(10).toDF().write.format(\"delta\").mode(\"append\").save(inputDir.getAbsolutePath)\n          q.processAllAvailable()\n          true\n        },\n        CheckAnswer(variant.answerWithIgnoreChanges: _*))\n    }\n  }\n\n  testQuietly(\"deleting files when ignoreChanges = true doesn't fail the query\") {\n    withTempDir { inputDir =>\n      ignoreOperationsTest(\n        inputDir.getAbsolutePath,\n        sourceOptions = Seq(DeltaOptions.IGNORE_CHANGES_OPTION -> \"true\"),\n        sqlCommand = s\"DELETE FROM delta.`$inputDir`\",\n        // Whole table deletes do not produce DVs.\n        commandShouldProduceDVs = Some(false))(\n        AssertOnQuery { q =>\n          Seq(10).toDF().write.format(\"delta\").mode(\"append\").save(inputDir.getAbsolutePath)\n          q.processAllAvailable()\n          true\n        },\n        CheckAnswer((0 to 10): _*))\n    }\n  }\n\n  for (variant <- sourceChangeVariants)\n  testQuietly(\"updating source table when ignoreDeletes = true fails the query\" +\n      s\" - using ${variant.label}\") {\n    withTempDir { inputDir =>\n      ignoreOperationsTest(\n        inputDir.getAbsolutePath,\n        sourceOptions = Seq(DeltaOptions.IGNORE_DELETES_OPTION -> \"true\"),\n        sqlCommand = variant.query(inputDir))(\n        ExpectFailure[DeltaUnsupportedOperationException] { e =>\n          for (msg <- Seq(\"data update\", \"not supported\", \"skipChangeCommits\", \"true\")) {\n            assert(e.getMessage.contains(msg))\n          }\n        })\n    }\n  }\n\n  private val allSourceOptions = Seq(\n    Nil,\n    List(DeltaOptions.IGNORE_DELETES_OPTION),\n    List(DeltaOptions.IGNORE_CHANGES_OPTION),\n    List(DeltaOptions.SKIP_CHANGE_COMMITS_OPTION))\n    .map { options =>\n      options.map(key => key -> \"true\")\n    }\n\n  for (sourceOption <- allSourceOptions)\n  testQuietly(\n    \"subsequent DML commands are processed correctly in a batch - DELETE->DELETE\" +\n      s\" - $sourceOption\") {\n    val expectations: List[StreamAction] =\n      sourceOption.map(_._1) match {\n        case List(DeltaOptions.IGNORE_DELETES_OPTION) | Nil =>\n          // These two do not allow updates.\n          ExpectFailure[DeltaUnsupportedOperationException] { e =>\n            for (msg <- Seq(\"data update\", \"not supported\", \"skipChangeCommits\", \"true\")) {\n              assert(e.getMessage.contains(msg))\n            }\n          } :: Nil\n        case List(DeltaOptions.IGNORE_CHANGES_OPTION) =>\n          // The 4 and 5 are in the same file as 3, so the first DELETE is going to duplicate them.\n          // 5 is still in the same file as 4 after the first DELETE, so the second DELETE is going\n          // to duplicate it again.\n          CheckAnswer((0 until 15) ++ Seq(4, 5, 5): _*) :: Nil\n        case List(DeltaOptions.SKIP_CHANGE_COMMITS_OPTION) =>\n          // This will completely ignore the DELETEs.\n          CheckAnswer((0 until 15): _*) :: Nil\n      }\n\n    withTempDir { inputDir =>\n      ignoreOperationsTestWithManualClock(\n        inputDir.getAbsolutePath,\n        sourceOptions = sourceOption,\n        sqlCommand1 = s\"DELETE FROM delta.`$inputDir` WHERE value == 3\",\n        sqlCommand2 = s\"DELETE FROM delta.`$inputDir` WHERE value == 4\",\n        expectations = expectations)\n    }\n  }\n\n  for (sourceOption <- allSourceOptions)\n  testQuietly(\"subsequent DML commands are processed correctly in a batch - INSERT->DELETE\" +\n      s\" - $sourceOption\") {\n    val expectations: List[StreamAction] = sourceOption.map(_._1) match {\n      case List(DeltaOptions.IGNORE_DELETES_OPTION) | Nil =>\n        // These two do not allow updates.\n        ExpectFailure[DeltaUnsupportedOperationException] { e =>\n          for (msg <- Seq(\"data update\", \"not supported\", \"skipChangeCommits\", \"true\")) {\n            assert(e.getMessage.contains(msg))\n          }\n        } :: Nil\n      case List(DeltaOptions.IGNORE_CHANGES_OPTION) =>\n        // 15 and 16 are in the same file, so 16 will get duplicated by the DELETE.\n        CheckAnswer((0 to 16) ++ Seq(16): _*) :: Nil\n      case List(DeltaOptions.SKIP_CHANGE_COMMITS_OPTION) =>\n        // This will completely ignore the DELETE.\n        CheckAnswer((0 to 16): _*) :: Nil\n    }\n\n    withTempDir { inputDir =>\n      ignoreOperationsTestWithManualClock(\n        inputDir.getAbsolutePath,\n        sourceOptions = sourceOption,\n        sqlCommand1 =\n          s\"INSERT INTO delta.`$inputDir` SELECT /*+ COALESCE(1) */ * FROM VALUES 15, 16\",\n        sqlCommand2 = s\"DELETE FROM delta.`$inputDir` WHERE value == 15\",\n        expectations = expectations)\n    }\n  }\n\n  test(\"multiple deletion vectors per file with initial snapshot\") {\n    withTempDir { inputDir =>\n      val path = inputDir.getAbsolutePath\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n\n      // V0: 10 rows in a single file\n      (0 until 10).toDF(\"value\").coalesce(1).write.format(\"delta\").save(path)\n\n      // V1: Delete row 0\n      executeDml(s\"DELETE FROM delta.`$path` WHERE value = 0\")\n\n      // V2: Delete row 1\n      executeDml(s\"DELETE FROM delta.`$path` WHERE value = 1\")\n\n      // V3: Delete row 2\n      executeDml(s\"DELETE FROM delta.`$path` WHERE value = 2\")\n\n      // Verify DVs are present\n      assert(getFilesWithDeletionVectors(deltaLog).nonEmpty,\n        \"This test requires deletion vectors to be present\")\n\n      val df = loadStreamWithOptions(path, Map.empty)\n\n      testStream(df)(\n        // Process the initial snapshot\n        AssertOnQuery { q =>\n          q.processAllAvailable()\n          true\n        },\n        CheckAnswer((3 until 10): _*)\n      )\n    }\n  }\n\n  private val multiDVSourceOptions = Seq(\n    List(DeltaOptions.IGNORE_FILE_DELETION_OPTION),\n    List(DeltaOptions.IGNORE_CHANGES_OPTION))\n    .map(options => options.map(key => key -> \"true\"))\n\n  for (sourceOptions <- multiDVSourceOptions)\n  test(s\"multiple deletion vectors per file - $sourceOptions\") {\n    withTempDir { inputDir =>\n      val path = inputDir.getAbsolutePath\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n\n      // V0: 10 rows in a single file\n      (0 until 10).toDF(\"value\").coalesce(1).write.format(\"delta\").save(path)\n\n      val df = loadStreamWithOptions(path, sourceOptions.toMap)\n\n      testStream(df)(\n        AssertOnQuery { q =>\n          q.processAllAvailable()\n          true\n        },\n        CheckAnswer((0 until 10): _*),\n        AssertOnQuery { q =>\n          // V1: Delete row 0 - creates first DV (version 1)\n          executeDml(s\"DELETE FROM delta.`$path` WHERE value = 0\")\n          true\n        },\n        AssertOnQuery { q =>\n          // V2: Delete row 1 - updates DV (version 2). DV is cumulative: {0, 1}\n          executeDml(s\"DELETE FROM delta.`$path` WHERE value = 1\")\n          true\n        },\n        AssertOnQuery { q =>\n          // Verify DVs are present\n          assert(getFilesWithDeletionVectors(deltaLog).nonEmpty,\n            \"This test requires deletion vectors to be present\")\n          true\n        },\n        AssertOnQuery { q =>\n          q.processAllAvailable()\n          true\n        },\n        // One file is read out 3 times!\n        // This matches the expectation for ignoreChanges & ignoreFileDeletion:\n        // After a data changing operation, unchanged rows are re-emitted.\n        // - v0: rows 0-9\n        // - v1: file re-added with rows 1-9 (DV excludes 0)\n        // - v2: file re-added with rows 2-9 (DV excludes 0,1)\n        CheckAnswer((0 until 10) ++ (1 until 10) ++ (2 until 10): _*))\n    }\n  }\n}\n\nclass DeltaSourceDeletionVectorsSuite extends DeltaSourceSuiteBase\n  with DeltaSQLCommandTest\n  with DeltaSourceDeletionVectorTests\n  with PersistentDVEnabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceFastDropFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.text.SimpleDateFormat\n\nimport org.apache.spark.sql.delta.cdc.CDCEnabled\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.streaming.{DataStreamWriter, StreamingQuery, StreamingQueryException}\n\nclass DeltaSourceFastDropFeatureSuite\n  extends DeltaSourceSuiteBase\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, \"true\")\n  }\n\n  protected def dropUnsupportedFeature(dir: File): Unit =\n    sql(\n      s\"\"\"ALTER TABLE delta.`${dir.getCanonicalPath}`\n         |DROP FEATURE  ${TestUnsupportedReaderWriterFeature.name}\n         |\"\"\".stripMargin)\n\n  protected def addUnsupportedFeature(dir: File): Unit =\n    sql(\n      s\"\"\"ALTER TABLE delta.`${dir.getCanonicalPath}` SET TBLPROPERTIES (\n         |delta.feature.${TestUnsupportedReaderWriterFeature.name} = 'supported'\n         |)\"\"\".stripMargin)\n\n  protected def getReadOnlyStream(\n      dir: File,\n      cdcReadEnabled: Boolean = false): DataStreamWriter[Row] =\n    spark.readStream\n      .option(DeltaOptions.CDC_READ_OPTION, cdcReadEnabled)\n      .format(\"delta\")\n      .load(dir.getCanonicalPath)\n      .writeStream\n      .format(\"noop\")\n\n  protected def addData(dir: File, value: Int): Unit =\n    Seq(value).toDF.write.mode(\"append\").format(\"delta\").save(dir.getCanonicalPath)\n\n  protected lazy val cdcReadEnabled =\n    spark.conf.getOption(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey)\n      .map(_.toBoolean)\n      .getOrElse(false)\n\n  test(\"Latest protocol is checked for unsupported features\") {\n    withTempDir { inputDir =>\n      addData(inputDir, value = 1)\n      addUnsupportedFeature(inputDir)\n\n      withSQLConf(DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED.key -> true.toString) {\n        DeltaLog.clearCache()\n        val e = intercept[DeltaUnsupportedTableFeatureException] {\n          getReadOnlyStream(inputDir, cdcReadEnabled).start()\n        }\n        assert(e.getErrorClass === \"DELTA_UNSUPPORTED_FEATURES_FOR_READ\")\n      }\n    }\n  }\n\n  for (useStartingTS <- DeltaTestUtils.BOOLEAN_DOMAIN)\n  test(s\"Protocol is checked when using startingVersion - useStartingTS: $useStartingTS.\") {\n    withTempDir { inputDir =>\n      def getTimestampForVersion(version: Long): String = {\n        val logPath = new Path(inputDir.getCanonicalPath, \"_delta_log\")\n        val file = new File(new Path(logPath, f\"$version%020d.json\").toString)\n        val sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\")\n        sdf.format(file.lastModified())\n      }\n\n      addData(inputDir, value = 1)\n      addUnsupportedFeature(inputDir)\n      addData(inputDir, value = 2) // More data.\n      val versionAfterProtocolUpgrade = DeltaLog.forTable(spark, inputDir).update().version\n      dropUnsupportedFeature(inputDir)\n\n      withSQLConf(DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED.key -> true.toString) {\n        // No problem loading from the latest version. Feature is dropped.\n        DeltaLog.clearCache()\n        getReadOnlyStream(inputDir, cdcReadEnabled).start()\n\n        // Start a stream to a version the feature was active.\n        val e = intercept[StreamingQueryException] {\n          val stream = spark.readStream\n            .option(DeltaOptions.CDC_READ_OPTION, cdcReadEnabled)\n            .format(\"delta\")\n\n          if (useStartingTS) {\n            stream.option(\"startingTimestamp\", getTimestampForVersion(versionAfterProtocolUpgrade))\n          } else {\n            stream.option(\"startingVersion\", versionAfterProtocolUpgrade)\n          }\n\n          val q = stream\n            .load(inputDir.getCanonicalPath)\n            .writeStream\n            .format(\"noop\")\n            .start()\n\n          // At initialization get attempt to get a snapshot at the starting version.\n          // This will validate whether the client supports the protocol at that version.\n          // Note, the protocol upgrade happened before the startingVersion. Therefore,\n          // we are certain the exception here does not stem from coming across the protocol\n          // bump while processing the stream.\n          q.processAllAvailable()\n        }\n        assert(e.getCause.getMessage.contains(\"DELTA_UNSUPPORTED_FEATURES_FOR_READ\"))\n      }\n    }\n  }\n\n  test(\"Protocol check at startingVersion is skipped when config is disabled\") {\n    withTempDir { inputDir =>\n      addData(inputDir, value = 1)\n      addUnsupportedFeature(inputDir)\n      addData(inputDir, value = 2) // More data.\n      val versionAfterProtocolUpgrade = DeltaLog.forTable(spark, inputDir).update().version\n      dropUnsupportedFeature(inputDir)\n\n      withSQLConf(\n          DeltaSQLConf.FAST_DROP_FEATURE_STREAMING_ALWAYS_VALIDATE_PROTOCOL.key -> false.toString,\n          DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED.key -> true.toString) {\n        // Start a stream to a version the feature was active.\n        val q = spark.readStream\n          .option(DeltaOptions.CDC_READ_OPTION, cdcReadEnabled)\n          .format(\"delta\")\n          .option(\"startingVersion\", versionAfterProtocolUpgrade)\n          .load(inputDir.getCanonicalPath)\n          .writeStream\n          .format(\"noop\")\n          .start()\n\n        try {\n          // Should had produced an exception but the check is disabled.\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"Protocol is checked when coming across an action with a protocol upgrade\") {\n    withTempDir { inputDir =>\n      addData(inputDir, value = 1)\n      addData(inputDir, value = 2) // More data. Optional.\n      val versionBeforeProtocolUpgrade = DeltaLog.forTable(spark, inputDir).update().version\n      addUnsupportedFeature(inputDir)\n      dropUnsupportedFeature(inputDir)\n\n      // Latest version looks clean. Feature is dropped.\n      val stream = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, cdcReadEnabled)\n        .format(\"delta\")\n        .option(\"startingVersion\", versionBeforeProtocolUpgrade)\n        .load(inputDir.getCanonicalPath)\n        .writeStream\n        .format(\"noop\")\n\n      withSQLConf(DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED.key -> true.toString) {\n        val q = stream.start()\n        val e = intercept[StreamingQueryException] {\n          // We come across the protocol upgrade commit and fail.\n          q.processAllAvailable()\n        }\n        q.stop()\n        assert(e.getCause.getMessage.contains(\"DELTA_UNSUPPORTED_FEATURES_FOR_READ\"))\n      }\n    }\n  }\n\n  test(\"Protocol validations after restarting from a checkpoint\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      addData(inputDir, value = 1)\n      addData(inputDir, value = 2) // More data. Optional.\n      addUnsupportedFeature(inputDir)\n\n      val stream = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, cdcReadEnabled)\n        .format(\"delta\")\n        .option(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, \"1\")\n        .load(inputDir.getCanonicalPath)\n        .drop(CDCReader.CDC_TYPE_COLUMN_NAME)\n        .drop(CDCReader.CDC_COMMIT_VERSION)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n        .writeStream\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .format(\"delta\")\n\n      val q = stream.start(outputDir.getCanonicalPath)\n      q.processAllAvailable()\n\n      // Validate progress so far.\n      val progress = q.recentProgress.filter(_.numInputRows != 0)\n      assert(progress.length === 2)\n      progress.foreach { p => assert(p.numInputRows === 1) }\n      checkAnswer(\n        spark.read.format(\"delta\").load(outputDir.getAbsolutePath),\n        (1 until 3).toDF())\n\n      q.stop()\n\n      // More stuff happened since the stream stopped.\n      addData(inputDir, value = 3) // More data. Optional.\n      addData(inputDir, value = 4) // More data. Optional.\n      addData(inputDir, value = 5) // More data. Optional.\n      dropUnsupportedFeature(inputDir)\n\n      // Query is restarted from checkpoint. Latest protocol looks clean because we dropped the\n      // unsupported feature. Furthermore, the protocol upgrade is before the checkpoint, thus\n      // we cannot come across it while streaming.\n      // The initial state of the stream is null because it was stopped. As a result, the client\n      // attempts to create a snapshot at the checkpoint version. This version contains the\n      // unsupported feature and fails.\n      withSQLConf(DeltaSQLConf.UNSUPPORTED_TESTING_FEATURES_ENABLED.key -> true.toString) {\n        DeltaLog.clearCache()\n        val q2 = stream.start(outputDir.getCanonicalPath)\n\n        val e = intercept[StreamingQueryException] {\n          // We come across the protocol upgrade commit and fail.\n          q2.processAllAvailable()\n        }\n        assert(e.getCause.getMessage.contains(\"DELTA_UNSUPPORTED_FEATURES_FOR_READ\"))\n        q2.stop()\n      }\n    }\n  }\n\n  test(\"Protocol validations supress errors when snapshot cannot be reconstructed\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, inputDir)\n\n      // Add some data.\n      addData(inputDir, value = 0) // Version 0.\n      addData(inputDir, value = 1) // Version 1.\n      addData(inputDir, value = 2) // Version 2.\n      addData(inputDir, value = 3) // Version 3.\n      deltaLog.checkpoint(deltaLog.update()) // Version 3.\n      addData(inputDir, value = 4) // Version 4.\n\n      // Delete version 1.\n      new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 1).toUri).delete()\n\n      withSQLConf(\n          DeltaSQLConf.FAST_DROP_FEATURE_STREAMING_ALWAYS_VALIDATE_PROTOCOL.key -> \"true\") {\n        DeltaLog.clearCache()\n        val q = spark.readStream\n          .option(DeltaOptions.CDC_READ_OPTION, cdcReadEnabled)\n          .format(\"delta\")\n          // Starting version exists but we cannot reconstruct a snapshot because version 1\n          // is missing.\n          .option(\"startingVersion\", 2)\n          .load(inputDir.getCanonicalPath)\n          .writeStream\n          .format(\"noop\")\n          .start()\n        try {\n          if (cdcReadEnabled) {\n            // With CDC enabled, this scenario always produces an exception. In that sense,\n            // CDC is more restrictive. This exception is produced in changesToDF when trying\n            // to construct a snapshot at the starting version. This is existing\n            // behaviour.\n            assert(intercept[StreamingQueryException] {\n              q.processAllAvailable()\n            }.getCause.getMessage.contains(\"DELTA_VERSIONS_NOT_CONTIGUOUS\"))\n          } else {\n            q.processAllAvailable()\n          }\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n}\n\nclass DeltaSourceFastDropFeatureCDCSuite extends DeltaSourceFastDropFeatureSuite with CDCEnabled {\n  override protected def excluded: Seq[String] =\n    super.excluded ++ Seq(\n      // Excluded because in CDC streaming the current behaviour is to always check the protocol at\n      // the starting version.\n      \"Protocol check at startingVersion is skipped when config is disabled\")\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceLargeLogSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkConf\n\nclass DeltaSourceLargeLogSuite extends DeltaSourceSuite {\n  protected override def sparkConf = {\n    super.sparkConf.set(DeltaSQLConf.LOG_SIZE_IN_MEMORY_THRESHOLD.key, \"0\")\n  }\n}\n\n// Batch sizes 1, 2, and 100 exercise different backfill behaviors in the commit coordinator.\n// Batch size 1 triggers a backfill on every commit (commitVersion % 1 == 0), testing the most\n// granular backfill path. Batch size 2 triggers backfill every other commit, testing the boundary\n// between backfilled and unbackfilled commits. Batch size 100 leaves most commits unbackfilled,\n// testing the production-like path where streaming must read from both the commit coordinator\n// and the filesystem. This follows the same pattern as other CatalogManaged (CCv2) test suites\n// (DeltaLogSuite, DeltaCDCStreamSuite, etc.).\n\nclass DeltaSourceLargeLogWithCatalogManagedBatch1Suite\n    extends DeltaSourceLargeLogSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSourceLargeLogWithCatalogManagedBatch2Suite\n    extends DeltaSourceLargeLogSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaSourceLargeLogWithCatalogManagedBatch100Suite\n    extends DeltaSourceLargeLogSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceOffsetSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.sources.DeltaSourceOffset\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.sql.execution.streaming.SerializedOffset\n\nclass DeltaSourceOffsetSuite extends SparkFunSuite {\n\n  test(\"unknown sourceVersion value\") {\n    // Set unknown sourceVersion as the max allowed version plus 1.\n    val unknownVersion = 4\n\n    // Note: \"isStartingVersion\" corresponds to DeltaSourceOffset.isInitialSnapshot.\n    val json =\n      s\"\"\"\n         |{\n         |  \"sourceVersion\": $unknownVersion,\n         |  \"reservoirVersion\": 1,\n         |  \"index\": 1,\n         |  \"isStartingVersion\": true\n         |}\n      \"\"\".stripMargin\n    val e = intercept[SparkThrowable] {\n      DeltaSourceOffset(\n        UUID.randomUUID().toString,\n        SerializedOffset(json))\n    }\n    assert(e.getErrorClass == \"DELTA_INVALID_FORMAT_FROM_SOURCE_VERSION\")\n    assert(e.toString.contains(\"Please upgrade to newer version of Delta\"))\n  }\n\n  test(\"invalid sourceVersion value\") {\n    // Note: \"isStartingVersion\" corresponds to DeltaSourceOffset.isInitialSnapshot.\n    val json =\n      \"\"\"\n        |{\n        |  \"sourceVersion\": \"foo\",\n        |  \"reservoirVersion\": 1,\n        |  \"index\": 1,\n        |  \"isStartingVersion\": true\n        |}\n      \"\"\".stripMargin\n    val e = intercept[SparkThrowable] {\n      DeltaSourceOffset(\n        UUID.randomUUID().toString,\n        SerializedOffset(json))\n    }\n    assert(e.getErrorClass == \"DELTA_INVALID_SOURCE_OFFSET_FORMAT\")\n    assert(e.toString.contains(\"source offset format is invalid\"))\n  }\n\n  test(\"missing sourceVersion\") {\n    // Note: \"isStartingVersion\" corresponds to DeltaSourceOffset.isInitialSnapshot.\n    val json =\n      \"\"\"\n        |{\n        |  \"reservoirVersion\": 1,\n        |  \"index\": 1,\n        |  \"isStartingVersion\": true\n        |}\n      \"\"\".stripMargin\n    val e = intercept[SparkThrowable] {\n      DeltaSourceOffset(\n        UUID.randomUUID().toString,\n        SerializedOffset(json))\n    }\n    assert(e.getErrorClass == \"DELTA_INVALID_SOURCE_VERSION\")\n    for (msg <- \"is invalid\") {\n      assert(e.toString.contains(msg))\n    }\n  }\n\n  test(\"unmatched reservoir id\") {\n    // Note: \"isStartingVersion\" corresponds to DeltaSourceOffset.isInitialSnapshot.\n    val json =\n      s\"\"\"\n        |{\n        |  \"reservoirId\": \"${UUID.randomUUID().toString}\",\n        |  \"sourceVersion\": 1,\n        |  \"reservoirVersion\": 1,\n        |  \"index\": 1,\n        |  \"isStartingVersion\": true\n        |}\n      \"\"\".stripMargin\n    val e = intercept[SparkThrowable] {\n      DeltaSourceOffset(\n        UUID.randomUUID().toString,\n        SerializedOffset(json))\n    }\n    assert(e.getErrorClass == \"DIFFERENT_DELTA_TABLE_READ_BY_STREAMING_SOURCE\")\n    for (msg <- Seq(\"delete\", \"checkpoint\", \"restart\")) {\n      assert(e.toString.contains(msg))\n    }\n  }\n\n  test(\"isInitialSnapshot serializes as isStartingVersion\") {\n    for (isStartingVersion <- Seq(false, true)) {\n      // From serialized to object\n      val reservoirId = UUID.randomUUID().toString\n      val json =\n        s\"\"\"\n           |{\n           |  \"reservoirId\": \"$reservoirId\",\n           |  \"sourceVersion\": 1,\n           |  \"reservoirVersion\": 1,\n           |  \"index\": 1,\n           |  \"isStartingVersion\": $isStartingVersion\n           |}\n      \"\"\".stripMargin\n      val offsetDeserialized = DeltaSourceOffset(reservoirId, SerializedOffset(json))\n      assert(offsetDeserialized.isInitialSnapshot === isStartingVersion)\n\n      // From object to serialized\n      val offset = DeltaSourceOffset(\n        reservoirId = reservoirId,\n        reservoirVersion = 7,\n        index = 13,\n        isInitialSnapshot = isStartingVersion)\n      assert(offset.json.contains(s\"\"\"\"isStartingVersion\":$isStartingVersion\"\"\"))\n    }\n  }\n\n  test(\"DeltaSourceOffset deserialization\") {\n    // Source version 1 with BASE_INDEX_V1\n    val reservoirId = UUID.randomUUID().toString\n    val jsonV1 =\n      s\"\"\"\n         |{\n         |  \"reservoirId\": \"$reservoirId\",\n         |  \"sourceVersion\": 1,\n         |  \"reservoirVersion\": 3,\n         |  \"index\": -1,\n         |  \"isStartingVersion\": false\n         |}\n    \"\"\".stripMargin\n    val offsetDeserializedV1 = JsonUtils.fromJson[DeltaSourceOffset](jsonV1)\n    assert(offsetDeserializedV1 ==\n      DeltaSourceOffset(reservoirId, 3, DeltaSourceOffset.BASE_INDEX, false))\n\n    // Source version 3 with BASE_INDEX_V3\n    val jsonV3 =\n      s\"\"\"\n         |{\n         |  \"reservoirId\": \"$reservoirId\",\n         |  \"sourceVersion\": 3,\n         |  \"reservoirVersion\": 7,\n         |  \"index\": -100,\n         |  \"isStartingVersion\": false\n         |}\n    \"\"\".stripMargin\n    val offsetDeserializedV3 = JsonUtils.fromJson[DeltaSourceOffset](jsonV3)\n    assert(offsetDeserializedV3 ==\n      DeltaSourceOffset(reservoirId, 7, DeltaSourceOffset.BASE_INDEX, false))\n\n    // Source version 3 with METADATA_CHANGE_INDEX\n    val jsonV3metadataChange =\n      s\"\"\"\n         |{\n         |  \"reservoirId\": \"$reservoirId\",\n         |  \"sourceVersion\": 3,\n         |  \"reservoirVersion\": 7,\n         |  \"index\": -20,\n         |  \"isStartingVersion\": false\n         |}\n    \"\"\".stripMargin\n    val offsetDeserializedV3metadataChange =\n      JsonUtils.fromJson[DeltaSourceOffset](jsonV3metadataChange)\n    assert(offsetDeserializedV3metadataChange ==\n      DeltaSourceOffset(reservoirId, 7, DeltaSourceOffset.METADATA_CHANGE_INDEX, false))\n\n    // Source version 3 with regular index and isStartingVersion = true\n    val jsonV3start =\n      s\"\"\"\n         |{\n         |  \"reservoirId\": \"$reservoirId\",\n         |  \"sourceVersion\": 3,\n         |  \"reservoirVersion\": 9,\n         |  \"index\": 23,\n         |  \"isStartingVersion\": true\n         |}\n    \"\"\".stripMargin\n    val offsetDeserializedV3start = JsonUtils.fromJson[DeltaSourceOffset](jsonV3start)\n    assert(offsetDeserializedV3start == DeltaSourceOffset(reservoirId, 9, 23, true))\n  }\n\n  test(\"DeltaSourceOffset deserialization error\") {\n    val reservoirId = UUID.randomUUID().toString\n    // This is missing a double quote so it's unbalanced.\n    val jsonV1 =\n      s\"\"\"\n         |{\n         |  \"reservoirId\": \"$reservoirId\",\n         |  \"sourceVersion\": 23x,\n         |  \"reservoirVersion\": 3,\n         |  \"index\": -1,\n         |  \"isStartingVersion\": false\n         |}\n    \"\"\".stripMargin\n    val e = intercept[SparkThrowable] {\n      JsonUtils.fromJson[DeltaSourceOffset](jsonV1)\n    }\n    assert(e.getErrorClass == \"DELTA_INVALID_SOURCE_OFFSET_FORMAT\")\n  }\n\n  test(\"DeltaSourceOffset serialization\") {\n    val reservoirId = UUID.randomUUID().toString\n    // BASE_INDEX is always serialized as V1.\n    val offsetV1 = DeltaSourceOffset(reservoirId, 3, DeltaSourceOffset.BASE_INDEX, false)\n    assert(JsonUtils.toJson(offsetV1) ===\n      s\"\"\"{\"sourceVersion\":1,\"reservoirId\":\"$reservoirId\",\"reservoirVersion\":3,\"index\":-1,\"\"\" +\n      s\"\"\"\"isStartingVersion\":false}\"\"\")\n    // The same serializer should be used by both methods.\n    assert(JsonUtils.toJson(offsetV1) === offsetV1.json)\n\n    // METADATA_CHANGE_INDEX is always serialized as V3\n    val offsetV3metadataChange =\n      DeltaSourceOffset(reservoirId, 7, DeltaSourceOffset.METADATA_CHANGE_INDEX, false)\n    assert(JsonUtils.toJson(offsetV3metadataChange) ===\n      s\"\"\"{\"sourceVersion\":3,\"reservoirId\":\"$reservoirId\",\"reservoirVersion\":7,\"index\":-20,\"\"\" +\n      s\"\"\"\"isStartingVersion\":false}\"\"\")\n    // The same serializer should be used by both methods.\n    assert(JsonUtils.toJson(offsetV3metadataChange) === offsetV3metadataChange.json)\n\n    // Regular index and isStartingVersion = true, serialized as V1\n    val offsetV1start = DeltaSourceOffset(reservoirId, 9, 23, true)\n    assert(JsonUtils.toJson(offsetV1start) ===\n      s\"\"\"{\"sourceVersion\":1,\"reservoirId\":\"$reservoirId\",\"reservoirVersion\":9,\"index\":23,\"\"\" +\n      s\"\"\"\"isStartingVersion\":true}\"\"\")\n    // The same serializer should be used by both methods.\n    assert(JsonUtils.toJson(offsetV1start) === offsetV1start.json)\n  }\n\n  test(\"DeltaSourceOffset.validateOffsets\") {\n    DeltaSourceOffset.validateOffsets(\n      previousOffset = DeltaSourceOffset(\n        reservoirId = \"foo\",\n        reservoirVersion = 4,\n        index = 10,\n        isInitialSnapshot = false),\n      currentOffset = DeltaSourceOffset(\n        reservoirId = \"foo\",\n        reservoirVersion = 4,\n        index = 10,\n        isInitialSnapshot = false))\n    DeltaSourceOffset.validateOffsets(\n      previousOffset = DeltaSourceOffset(\n        reservoirId = \"foo\",\n        reservoirVersion = 4,\n        index = 10,\n        isInitialSnapshot = false),\n      currentOffset = DeltaSourceOffset(\n        reservoirId = \"foo\",\n        reservoirVersion = 5,\n        index = 1,\n        isInitialSnapshot = false))\n\n    assert(intercept[IllegalStateException] {\n      DeltaSourceOffset.validateOffsets(\n        previousOffset = DeltaSourceOffset(\n          reservoirId = \"foo\",\n          reservoirVersion = 4,\n          index = 10,\n          isInitialSnapshot = false),\n        currentOffset = DeltaSourceOffset(\n          reservoirId = \"foo\",\n          reservoirVersion = 4,\n          index = 10,\n          isInitialSnapshot = true))\n    }.getMessage.contains(\"Found invalid offsets: 'isInitialSnapshot' flipped incorrectly.\"))\n    assert(intercept[IllegalStateException] {\n      DeltaSourceOffset.validateOffsets(\n        previousOffset = DeltaSourceOffset(\n          reservoirId = \"foo\",\n          reservoirVersion = 4,\n          index = 10,\n          isInitialSnapshot = false),\n        currentOffset = DeltaSourceOffset(\n          reservoirId = \"foo\",\n          reservoirVersion = 1,\n          index = 10,\n          isInitialSnapshot = false))\n    }.getMessage.contains(\"Found invalid offsets: 'reservoirVersion' moved back.\"))\n    assert(intercept[IllegalStateException] {\n      DeltaSourceOffset.validateOffsets(\n        previousOffset = DeltaSourceOffset(\n          reservoirId = \"foo\",\n          reservoirVersion = 4,\n          index = 10,\n          isInitialSnapshot = false),\n        currentOffset = DeltaSourceOffset(\n          reservoirId = \"foo\",\n          reservoirVersion = 4,\n          index = 9,\n          isInitialSnapshot = false))\n    }.getMessage.contains(\"Found invalid offsets. 'index' moved back.\"))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceSchemaEvolutionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.nio.charset.Charset\n\nimport scala.collection.JavaConverters._\nimport scala.util.Try\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.sources._\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.commons.io.FileUtils\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.fs.Path\nimport org.apache.logging.log4j.Level\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, Row}\nimport org.apache.spark.sql.catalyst.{InternalRow, TableIdentifier}\nimport org.apache.spark.sql.execution.streaming.Offset\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.streaming.{StreamingQueryException, Trigger}\nimport org.apache.spark.sql.types.{StringType, StructType}\nimport org.apache.spark.util.Utils\n\ntrait StreamingSchemaEvolutionSuiteBase extends ColumnMappingStreamingTestUtils\n  with DeltaSourceSuiteBase with DeltaColumnMappingSelectedTestMixin with DeltaSQLCommandTest {\n\n  override protected def runOnlyTests: Seq[String] = Seq(\n    \"schema log initialization with additive schema changes\",\n    \"detect incompatible schema change while streaming\",\n    \"trigger.Once with deferred commit should work\",\n    \"trigger.AvailableNow should work\",\n    \"consecutive schema evolutions\",\n    \"latestOffset should not progress before schema evolved\"\n  )\n\n  override protected def sparkConf: SparkConf = {\n    val conf = super.sparkConf\n    // Enable for testing\n    conf.set(DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING.key, \"true\")\n    conf.set(\n      DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING_MERGE_CONSECUTIVE_CHANGES.key, \"true\")\n    conf.set(\n      s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming.allowSourceColumnRenameAndDrop\", \"always\")\n    if (isCdcTest) {\n      conf.set(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")\n    } else {\n      conf\n    }\n  }\n\n  protected def withoutAllowStreamRestart(f: => Unit): Unit = {\n    withSQLConf(s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming\" +\n      s\".allowSourceColumnRenameAndDrop\" -> \"false\") {\n      f\n    }\n  }\n\n  protected def testWithoutAllowStreamRestart(testName: String)(f: => Unit): Unit = {\n    test(testName) {\n      withoutAllowStreamRestart(f)\n    }\n  }\n\n  import testImplicits._\n\n  protected val ExpectSchemaLogInitializationFailedException =\n    ExpectFailure[DeltaRuntimeException](e =>\n      assert(\n        e.asInstanceOf[DeltaRuntimeException].getErrorClass ==\n          \"DELTA_STREAMING_SCHEMA_LOG_INIT_FAILED_INCOMPATIBLE_METADATA\" &&\n          // Does NOT come from the stream start check which is for lazy initialization ...\n          !e.getStackTrace.exists(\n            _.toString.contains(\"checkReadIncompatibleSchemaChangeOnStreamStartOnce\")) &&\n          // Coming from the check against constructed batches\n          e.getStackTrace.exists(\n            _.toString.contains(\"validateAndInitMetadataLogForPlannedBatchesDuringStreamStart\"))\n      )\n    )\n\n  protected val ExpectMetadataEvolutionException =\n    ExpectFailure[DeltaRuntimeException](e =>\n      assert(\n        e.asInstanceOf[DeltaRuntimeException].getErrorClass ==\n          \"DELTA_STREAMING_METADATA_EVOLUTION\" &&\n          e.getStackTrace.exists(\n            _.toString.contains(\"updateMetadataTrackingLogAndFailTheStreamIfNeeded\"))\n      )\n    )\n\n  protected val ExpectMetadataEvolutionExceptionFromInitialization =\n    ExpectFailure[DeltaRuntimeException](e =>\n      assert(\n        e.asInstanceOf[DeltaRuntimeException].getErrorClass ==\n          \"DELTA_STREAMING_METADATA_EVOLUTION\" &&\n          !e.getStackTrace.exists(_.toString.contains(\"checkReadIncompatibleSchemaChanges\")) &&\n          e.getStackTrace.exists(_.toString.contains(\"initializeMetadataTrackingAndExitStream\"))\n      )\n    )\n\n  protected val indexWhenSchemaLogIsUpdated = DeltaSourceOffset.POST_METADATA_CHANGE_INDEX\n\n  protected val AwaitTermination = AssertOnQuery { q =>\n    q.awaitTermination(600 * 1000) // 600 seconds\n    true\n  }\n\n  protected val AwaitTerminationIgnoreError = AssertOnQuery { q =>\n    try {\n      q.awaitTermination(600 * 1000) // 600 seconds\n    } catch {\n      case _: Throwable =>\n        // ignore\n    }\n    true\n  }\n\n  protected def allowSchemaLocationOutsideCheckpoint(f: => Unit): Unit = {\n    val allowSchemaLocationOutSideCheckpointConf =\n      DeltaSQLConf.DELTA_STREAMING_ALLOW_SCHEMA_LOCATION_OUTSIDE_CHECKPOINT_LOCATION.key\n    withSQLConf(allowSchemaLocationOutSideCheckpointConf -> \"true\") {\n      f\n    }\n  }\n\n  protected def testSchemaEvolution(\n      testName: String,\n      columnMapping: Boolean = true,\n      tags: Seq[org.scalatest.Tag] = Seq.empty)(f: DeltaLog => Unit): Unit = {\n    super.test(testName, tags: _*) {\n      if (columnMapping) {\n        withStarterTable { log =>\n          f(log)\n        }\n      } else {\n        withColumnMappingConf(\"none\") {\n          withStarterTable { log =>\n            f(log)\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Initialize a starter table with 6 rows and schema STRUCT<a STRING, b STRING>\n   */\n  protected def withStarterTable(f: DeltaLog => Unit): Unit = {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n      // Write 6 versions, the first version 0 will contain data -1 and will come with the default\n      // schema initialization actions.\n      (-1 until 5).foreach { i =>\n        Seq((i.toString, i.toString)).toDF(\"a\", \"b\")\n          .write.mode(\"append\").format(\"delta\")\n          .save(tablePath)\n      }\n      val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      deltaLog.update()\n      f(deltaLog)\n    }\n  }\n\n  protected def addData(\n      data: Seq[Int],\n      userSpecifiedSchema: Option[StructType] = None)(implicit log: DeltaLog): Unit = {\n    val schema = userSpecifiedSchema.getOrElse(log.update().schema)\n    data.foreach { i =>\n      val data = Seq(Row(schema.map(_ => i.toString): _*))\n      spark.createDataFrame(data.asJava, schema)\n        .write.format(\"delta\").mode(\"append\").save(log.dataPath.toString)\n    }\n  }\n\n  protected def readStream(\n      schemaLocation: Option[String] = None,\n      sourceTrackingId: Option[String] = None,\n      startingVersion: Option[Long] = None,\n      maxFilesPerTrigger: Option[Int] = None,\n      ignoreDeletes: Option[Boolean] = None)(implicit log: DeltaLog): DataFrame = {\n    var dsr = spark.readStream.format(\"delta\")\n    if (isCdcTest) {\n      dsr = dsr.option(DeltaOptions.CDC_READ_OPTION, \"true\")\n    }\n    schemaLocation.foreach { loc => dsr = dsr.option(DeltaOptions.SCHEMA_TRACKING_LOCATION, loc) }\n    sourceTrackingId.foreach { name =>\n      dsr = dsr.option(DeltaOptions.STREAMING_SOURCE_TRACKING_ID, name)\n    }\n    startingVersion.foreach { v => dsr = dsr.option(\"startingVersion\", v) }\n    maxFilesPerTrigger.foreach { f => dsr = dsr.option(\"maxFilesPerTrigger\", f) }\n    ignoreDeletes.foreach{ i => dsr.option(\"ignoreDeletes\", i) }\n    val df = {\n        dsr.load(log.dataPath.toString)\n    }\n    if (isCdcTest) {\n      dropCDCFields(df)\n    } else {\n      df\n    }\n  }\n\n  protected def getDefaultSchemaLog(\n      sourceTrackingId: Option[String] = None,\n      initializeEagerly: Boolean = true\n  )(implicit log: DeltaLog): DeltaSourceMetadataTrackingLog =\n    DeltaSourceMetadataTrackingLog.create(\n      spark, getDefaultSchemaLocation.toString, log.update(),\n      catalogTableOpt = None,\n      parameters = sourceTrackingId.map(DeltaOptions.STREAMING_SOURCE_TRACKING_ID -> _).toMap,\n      initMetadataLogEagerly = initializeEagerly)\n\n  protected def getDefaultCheckpoint(implicit log: DeltaLog): Path =\n    new Path(log.dataPath, \"_checkpoint\")\n\n  protected def getDefaultSchemaLocation(implicit log: DeltaLog): Path =\n    new Path(getDefaultCheckpoint, \"_schema_location\")\n\n  protected def addColumn(column: String, dt: String = \"STRING\")(implicit log: DeltaLog): Unit = {\n    sql(s\"ALTER TABLE delta.`${log.dataPath}` ADD COLUMN ($column $dt)\")\n  }\n\n  protected def renameColumn(oldColumn: String, newColumn: String)(implicit log: DeltaLog): Unit = {\n    sql(s\"ALTER TABLE delta.`${log.dataPath}` RENAME COLUMN $oldColumn TO $newColumn\")\n  }\n\n  protected def dropColumn(column: String)(implicit log: DeltaLog): Unit = {\n    sql(s\"ALTER TABLE delta.`${log.dataPath}` DROP COLUMN $column\")\n  }\n\n  protected def overwriteSchema(\n      schema: StructType,\n      partitionColumns: Seq[String] = Nil)(implicit log: DeltaLog): Unit = {\n    spark.sqlContext.internalCreateDataFrame(spark.sparkContext.emptyRDD[InternalRow], schema)\n      .write.format(\"delta\")\n      .mode(\"overwrite\")\n      .partitionBy(partitionColumns: _*)\n      .option(\"overwriteSchema\", \"true\")\n      .save(log.dataPath.toString)\n  }\n\n  protected def upgradeToNameMode(implicit log: DeltaLog): Unit = {\n    sql(\n      s\"\"\"ALTER TABLE delta.`${log.dataPath}` SET TBLPROPERTIES (\n         |'delta.columnMapping.mode' = \"name\",\n         |'delta.minReaderVersion' = '2',\n         |'delta.minWriterVersion' = '5'\n         |)\n         |\"\"\".stripMargin)\n  }\n\n  protected def makeMetadata(\n      schema: StructType,\n      partitionSchema: StructType)(implicit log: DeltaLog): Metadata = {\n    log.update().metadata.copy(\n      schemaString = schema.json,\n      partitionColumns = partitionSchema.fieldNames\n    )\n  }\n\n  protected def testSchemasLocationMustBeUnderCheckpoint(\n      checkpointLocation: String,\n      schemaLocation: String,\n      expectValid: Boolean,\n      verify: DeltaAnalysisException => Boolean = _ => true)(implicit log: DeltaLog): Unit = {\n    val dest = Utils.createTempDir().getCanonicalPath\n\n    if (!expectValid) {\n      // By default it should fail\n      val e = intercept[DeltaAnalysisException] {\n        readStream(schemaLocation = Some(schemaLocation))\n          .writeStream.option(\"checkpointLocation\", checkpointLocation).start(dest)\n      }\n      assert(e.getErrorClass == \"DELTA_STREAMING_SCHEMA_LOCATION_NOT_UNDER_CHECKPOINT\")\n      assert(verify(e))\n      // But can be lifted with the flag\n      allowSchemaLocationOutsideCheckpoint {\n        testStream(readStream(schemaLocation = Some(schemaLocation)))(\n          StartStream(checkpointLocation = checkpointLocation),\n          ProcessAllAvailable(),\n          CheckAnswer((-1 until 5).map(i => (i.toString, i.toString)): _*)\n        )\n      }\n    } else {\n      // Should just work\n      testStream(readStream(schemaLocation = Some(schemaLocation)))(\n        StartStream(checkpointLocation = checkpointLocation),\n        ProcessAllAvailable(),\n        CheckAnswer((-1 until 5).map(i => (i.toString, i.toString)): _*)\n      )\n    }\n  }\n\n  testSchemaEvolution(\"schema location not under checkpoint\") { implicit log =>\n    testSchemasLocationMustBeUnderCheckpoint(\n      getDefaultCheckpoint.toString,\n      Utils.createTempDir().getCanonicalPath,\n      expectValid = false,\n      verify = e => {\n        val Array(schemaLocation, checkpointLocation) = e.getMessageParametersArray\n        // Make sure paths with interchangeable schemes are handled\n        schemaLocation.startsWith(\"/\") && checkpointLocation.startsWith(\"file:\")\n      }\n    )\n  }\n\n  testSchemaEvolution(\"schema location same as checkpoint\") { implicit log =>\n    testSchemasLocationMustBeUnderCheckpoint(\n      getDefaultCheckpoint.toString,\n      getDefaultCheckpoint.toString,\n      expectValid = true\n    )\n  }\n\n  testSchemaEvolution(\"schema location using a different file system\") { implicit log =>\n    withSQLConf(\n        \"fs.s3.impl\" -> classOf[S3LikeLocalFileSystem].getCanonicalName,\n        \"fs.s3.impl.disable.cache\" -> \"true\") {\n      testSchemasLocationMustBeUnderCheckpoint(\n        getDefaultCheckpoint.toString,\n        s\"s3:${Utils.createTempDir().getCanonicalPath}\",\n        expectValid = false\n      )\n    }\n  }\n\n  private case class SchemaLocationUnderCheckpointUnitTest(\n      checkpointLocation: String,\n      schemaLocation: String,\n      expectValid: Boolean,\n      sqlConfs: Map[String, String] = Map.empty)\n\n  private val schemaLocationUnderCheckpointUnitTests = Map(\n    \"checkpoint location and schema location are the same\" -> {\n      val path = Utils.createTempDir().getCanonicalPath\n      SchemaLocationUnderCheckpointUnitTest(path, path, expectValid = true)\n    },\n    \"schema location is under checkpoint location\" -> {\n      val checkpoint = Utils.createTempDir().getCanonicalPath\n      val schema = new File(checkpoint, \"schema\").getCanonicalPath\n      // Also test that file:/ scheme is treated the same.\n      SchemaLocationUnderCheckpointUnitTest(checkpoint, s\"file:$schema\", expectValid = true)\n    },\n    \"schema location is not under checkpoint location\" -> {\n      val checkpoint = Utils.createTempDir().getCanonicalPath\n      val schema = Utils.createTempDir().getCanonicalPath\n      SchemaLocationUnderCheckpointUnitTest(\n        checkpoint,\n        schema,\n        expectValid = false\n      )\n    },\n    \"schema location and checkpoint location are on different file systems\" -> {\n      val checkpoint = Utils.createTempDir().getCanonicalPath\n      val schema = s\"s3:${Utils.createTempDir().getCanonicalPath}\"\n      SchemaLocationUnderCheckpointUnitTest(\n        checkpoint,\n        schema,\n        sqlConfs = Map(\n          \"fs.s3.impl\" -> classOf[S3LikeLocalFileSystem].getCanonicalName,\n          \"fs.s3.impl.disable.cache\" -> \"true\"\n        ),\n        expectValid = false)\n    },\n    \"schema location and checkpoint location are the same but with explicit file scheme\" -> {\n      val path = Utils.createTempDir().getCanonicalPath\n      SchemaLocationUnderCheckpointUnitTest(\n        path,\n        s\"file:$path\",\n        expectValid = true\n      )\n    },\n    \"special characters in schema location\" -> {\n      val checkpoint = Utils.createTempDir().getCanonicalPath\n      val schema = s\"$checkpoint/a % ^ * _ b\"\n      SchemaLocationUnderCheckpointUnitTest(\n        checkpoint,\n        schema,\n        expectValid = true\n      )\n    }\n  )\n\n  schemaLocationUnderCheckpointUnitTests.foreach { case (testName, testCase) =>\n    test(s\"schema / checkpoint location unit tests - $testName\") {\n      val analysis = new DeltaAnalysis(spark)\n      withSQLConf(testCase.sqlConfs.toSeq: _*) {\n        val resultTry = Try(\n          analysis.assertSchemaTrackingLocationUnderCheckpoint(\n            testCase.checkpointLocation,\n            testCase.schemaLocation\n          )\n        )\n        if (testCase.expectValid) {\n          assert(resultTry.isSuccess)\n        } else {\n          assert(resultTry.isFailure)\n          val e = resultTry.failed.get\n          logInfo(\"Expected exception\", e)\n          assert(e.isInstanceOf[DeltaAnalysisException])\n          checkError(\n            e.asInstanceOf[DeltaAnalysisException],\n            \"DELTA_STREAMING_SCHEMA_LOCATION_NOT_UNDER_CHECKPOINT\",\n            \"22000\",\n            Map(\n              \"schemaTrackingLocation\" -> testCase.schemaLocation,\n              \"checkpointLocation\" -> testCase.checkpointLocation\n            )\n          )\n        }\n      }\n    }\n  }\n\n  testSchemaEvolution(\"multiple delta source sharing same schema log is blocked\") { implicit log =>\n    allowSchemaLocationOutsideCheckpoint {\n      val dest = Utils.createTempDir().getCanonicalPath\n      val ckpt = getDefaultCheckpoint.toString\n      val schemaLocation = getDefaultSchemaLocation.toString\n\n      // Two INSTANCES of Delta sources sharing same schema location should be blocked\n      val df1 = readStream(schemaLocation = Some(schemaLocation))\n      val df2 = readStream(schemaLocation = Some(schemaLocation))\n      val sdf = df1 union df2\n\n      val e = intercept[DeltaAnalysisException] {\n        sdf.writeStream.option(\"checkpointLocation\", ckpt).start(dest)\n      }\n      assert(e.getErrorClass == \"DELTA_STREAMING_SCHEMA_LOCATION_CONFLICT\")\n\n\n      // But providing an additional source name can differentiate\n      val df3 = readStream(schemaLocation = Some(schemaLocation), sourceTrackingId = Some(\"a\"))\n      val df4 = readStream(schemaLocation = Some(schemaLocation), sourceTrackingId = Some(\"b\"))\n      val sdf2 = df3 union df4\n      testStream(sdf2)(\n        StartStream(checkpointLocation = ckpt),\n        ProcessAllAvailable(),\n        CheckAnswer(((-1 until 5) union (-1 until 5)).map(i => (i.toString, i.toString)): _*)\n      )\n\n      // But if they are the same instance it should not be blocked, because they will be\n      // unified to the same source during execution.\n      val sdf3 = df1 union df1\n      testStream(sdf3)(\n        StartStream(checkpointLocation = ckpt),\n        ProcessAllAvailable(),\n        AssertOnQuery { q =>\n          // Just one source being executed\n          q.committedOffsets.size == 1\n        }\n      )\n    }\n  }\n\n  // Disable column mapping for this test so we could save some schema metadata manipulation hassle\n  testSchemaEvolution(\"schema log is applied\", columnMapping = false) { implicit log =>\n    withSQLConf(\n      DeltaSQLConf.DELTA_STREAMING_SCHEMA_TRACKING_METADATA_PATH_CHECK_ENABLED.key -> \"false\") {\n      // Schema log's schema is respected\n      val schemaLog = getDefaultSchemaLog()\n      val newSchema = PersistedMetadata(log.unsafeVolatileTableId, 0,\n        makeMetadata(\n          new StructType().add(\"a\", StringType, true)\n            .add(\"b\", StringType, true)\n            .add(\"c\", StringType, true),\n          partitionSchema = new StructType()\n        ),\n        log.update().protocol,\n        sourceMetadataPath = \"\"\n      )\n      schemaLog.writeNewMetadata(newSchema)\n\n      testStream(\n          readStream(schemaLocation = Some(getDefaultSchemaLocation.toString),\n            // Ignore initial snapshot\n          startingVersion = Some(1L)))(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailable(),\n        // See how the schema returns one more dimension for `c`\n        CheckAnswer((0 until 5).map(_.toString).map(i => (i, i, null)): _*)\n      )\n\n      // Cannot use schema from another table\n      val newSchemaWithTableId = PersistedMetadata(\n        \"some_random_id\", 0,\n        makeMetadata(\n          new StructType().add(\"a\", StringType, true)\n          .add(\"b\", StringType, true),\n          partitionSchema = new StructType()\n        ),\n        log.update().protocol,\n        sourceMetadataPath = \"\"\n      )\n      schemaLog.writeNewMetadata(newSchemaWithTableId)\n      assert {\n        val e = intercept[DeltaAnalysisException] {\n          val q = readStream(\n              schemaLocation = Some(getDefaultSchemaLocation.toString),\n              // Ignore initial snapshot\n              startingVersion = Some(1L))\n            .writeStream\n            .option(\"checkpointLocation\", getDefaultCheckpoint.toString)\n            .outputMode(\"append\")\n            .format(\"console\")\n            .start()\n          q.processAllAvailable()\n          q.stop()\n        }\n        ExceptionUtils.getRootCause(e).asInstanceOf[DeltaAnalysisException]\n          .getErrorClass == \"DELTA_STREAMING_SCHEMA_LOG_INCOMPATIBLE_DELTA_TABLE_ID\"\n      }\n    }\n  }\n\n  test(\"concurrent schema log modification should be detected\") {\n    withStarterTable { implicit log =>\n      // Note: this test assumes schema log files are written one after another, which is majority\n      // of the case; True concurrent execution would require commit service to protected against.\n      val schemaLocation = getDefaultSchemaLocation.toString\n      val snapshot = log.update()\n      val schemaLog1 = DeltaSourceMetadataTrackingLog.create(\n        spark, schemaLocation, snapshot, catalogTableOpt = None, parameters = Map.empty)\n      val schemaLog2 = DeltaSourceMetadataTrackingLog.create(\n        spark, schemaLocation, snapshot, catalogTableOpt = None, Map.empty)\n      val newSchema =\n        PersistedMetadata(\"1\", 1,\n          makeMetadata(new StructType(), partitionSchema = new StructType()),\n          Protocol(),\n          sourceMetadataPath = \"\")\n\n      schemaLog1.writeNewMetadata(newSchema)\n      val e = intercept[DeltaAnalysisException] {\n        schemaLog2.writeNewMetadata(newSchema)\n      }\n      assert(e.getErrorClass == \"DELTA_STREAMING_SCHEMA_LOCATION_CONFLICT\")\n    }\n  }\n\n  /**\n   * Manually create a new offset with targeted reservoirVersion by copying it from the previous\n   * offset.\n   * @param checkpoint Checkpoint location\n   * @param version Target version\n   * @param index Target index fle.\n   * @return The raw content for the updated offset file\n   */\n  protected def manuallyCreateLatestStreamingOffsetUntilReservoirVersion(\n      checkpoint: String,\n      version: Long,\n      index: Long = DeltaSourceOffset.BASE_INDEX): String = {\n    // manually create another offset to latest version\n    val offsetDir = new File(checkpoint.stripPrefix(\"file:\") + \"/offsets\")\n    val previousOffset = offsetDir.listFiles().filter(!_.getName.endsWith(\".crc\"))\n      .maxBy(_.getName.toInt)\n    val previousOffsetContent = FileUtils\n      .readFileToString(previousOffset, Charset.defaultCharset())\n\n    val reservoirVersionRegex = \"\"\"\"reservoirVersion\":[0-9]+\"\"\".r\n    val indexRegex = \"\"\"\"index\":-?\\d+\"\"\".r\n    var updated = reservoirVersionRegex\n      .replaceAllIn(previousOffsetContent, s\"\"\"\"reservoirVersion\":$version\"\"\")\n    updated = indexRegex.replaceAllIn(updated, s\"\"\"\"index\":$index\"\"\")\n\n    val newOffsetFile = new File(previousOffset.getParent,\n      (previousOffset.getName.toInt + 1).toString)\n    FileUtils.writeStringToFile(newOffsetFile, updated, Charset.defaultCharset())\n    updated\n  }\n\n  /**\n   * Write serialized offset content as a batch id for a particular checkpoint.\n   * @param checkpoint Checkpoint location\n   * @param batchId Target batch ID to write to\n   * @param offsetContent Offset content\n   */\n  protected def manuallyCreateStreamingOffsetAtBatchId(\n      checkpoint: String, batchId: Long, offsetContent: String): Unit = {\n    // manually create another offset to latest version\n    val offsetDir = new File(checkpoint.stripPrefix(\"file:\") + \"/offsets\")\n    val newOffsetFile = new File(offsetDir, batchId.toString)\n    FileUtils.writeStringToFile(newOffsetFile, offsetContent, Charset.defaultCharset())\n  }\n\n  /**\n   * Manually delete the latest offset\n   * @param checkpoint Checkpoint location\n   */\n  protected def manuallyDeleteLatestBatchId(checkpoint: String): Unit = {\n    // manually create another offset to latest version\n    val offsetDir = new File(checkpoint.stripPrefix(\"file:\") + \"/offsets\")\n    val latestOffsetFile = offsetDir.listFiles().filter(!_.getName.endsWith(\".crc\"))\n      .maxBy(_.getName.toInt)\n    latestOffsetFile.delete()\n  }\n\n  testSchemaEvolution(\"schema log initialization with additive schema changes\") { implicit log =>\n    // Provide a schema log by default\n    def createNewDf(): DataFrame =\n      readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    // Initialize snapshot schema same as latest, no need to fail stream\n    testStream(createNewDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((-1 until 5).map(_.toString).map(i => (i, i)): _*)\n    )\n\n    val v0 = log.update().version\n\n    // And schema log is initialized already, even though there aren't schema evolution exceptions\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v0)\n\n    // Add a column and some data\n    addColumn(\"c\")\n    val v1 = log.update().version\n\n    addData(5 until 10)\n\n    // Update schema log to v1\n    testStream(createNewDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v1)\n\n    var v2: Long = -1\n    testStream(createNewDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      // Process successfully\n      CheckAnswer((5 until 10).map(_.toString).map(i => (i, i, i)): _*),\n      // Trigger additive schema change would evolve schema as well\n      Execute { _ =>\n        addColumn(\"d\")\n        v2 = log.update().version\n      },\n      Execute { _ => addData(10 until 15) },\n      ExpectMetadataEvolutionException,\n      AssertOnQuery { q =>\n        val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n        offset.index == indexWhenSchemaLogIsUpdated\n      }\n    )\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v2)\n    testStream(createNewDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((10 until 15).map(_.toString).map(i => (i, i, i, i)): _*)\n    )\n  }\n\n  testSchemaEvolution(\"detect incompatible schema change while streaming\") { implicit log =>\n    // Rename as part of initial snapshot\n    renameColumn(\"b\", \"c\")\n    // Write more data\n    addData(5 until 10)\n    // Source df without schema location\n    val df = readStream()\n    var schemaChangeDeltaVersion: Long = -1\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      // schema change inside initial snapshot should not throw error\n      CheckAnswer((-1 until 10).map(i => (i.toString, i.toString)): _*),\n      // This new rename should throw the legacy error because we have not provided a schema\n      // location\n      Execute { _ =>\n        renameColumn(\"c\", \"d\")\n        schemaChangeDeltaVersion = log.update().version\n      },\n      // Add some data in new schema\n      Execute { _ => addData(10 until 15) },\n      ProcessAllAvailableIgnoreError,\n      // No more data should've been processed\n      CheckAnswer((-1 until 10).map(i => (i.toString, i.toString)): _*),\n      // Detected by the in stream check\n      ExpectInStreamSchemaChangeFailure\n    )\n    // Start the stream again with a schema location\n    val df2 = readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    assert(getDefaultSchemaLog().getLatestMetadata.isEmpty)\n    testStream(df2)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      // No data should've been processed\n      CheckAnswer(Nil: _*),\n      // Schema evolution exception!\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    // We should've updated the schema to the version just before the schema change version\n    // because that's the previous version's schema we left with. To be safe and in case there\n    // are more file actions to process, we saved that schema instead of the renamed schema.\n    // Also, since the previous batch was still on initial snapshot, the last file action was not\n    // bumped to the next version, so the schema initialization effectively did not consider the\n    // rename column schema change's version.\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion ==\n      schemaChangeDeltaVersion - 1)\n    // Start the stream again with the same schema location\n    val df3 = readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    testStream(df3)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      // Again, no data should've been processed because the next version has a rename\n      CheckAnswer(Nil: _*),\n      // And schema will be evolved again\n      ExpectMetadataEvolutionException\n    )\n    // Now finally the schema log is up to date\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion ==\n      schemaChangeDeltaVersion)\n    // Start the stream again should process the rest of the data without a problem\n    val df4 = readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    val v1 = log.update().version\n    testStream(df4)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((10 until 15).map(i => (i.toString, i.toString)): _*),\n      AssertOnQuery { q =>\n        val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n        // bumped from file action, no pending schema change\n        offset.reservoirVersion == v1 + 1 &&\n          offset.index == DeltaSourceOffset.BASE_INDEX &&\n          // BASE_INDEX is -100 but serialized form should use version 1 & index -1 for backward\n          // compatibility\n          offset.json.contains(s\"\"\"\"sourceVersion\":1\"\"\") &&\n          offset.json.contains(s\"\"\"\"index\":-1\"\"\")\n      },\n      // Trigger another schema change\n      Execute { _ =>\n        addColumn(\"e\")\n        addData(15 until 20)\n      },\n      ProcessAllAvailableIgnoreError,\n      // No more new data\n      CheckAnswer((10 until 15).map(i => (i.toString, i.toString)): _*),\n      AssertOnQuery { q =>\n        val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n        // latest offset should have a schema attached and evolved set to true\n        // note the reservoir version has not changed\n        offset.reservoirVersion == v1 + 1 &&\n          offset.index == indexWhenSchemaLogIsUpdated\n      },\n      ExpectMetadataEvolutionException\n    )\n\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v1 + 1)\n\n    val df5 = readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    // Process the rest\n    testStream(df5)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((15 until 20).map(i => (i.toString, i.toString, i.toString)): _*)\n    )\n  }\n\n  testSchemaEvolution(\"detect incompatible schema change during first getBatch\") { implicit log =>\n    renameColumn(\"b\", \"c\")\n    val schemaChangeVersion = log.update().version\n    // Source df without schema location, and start at version 1 to ignore initial snapshot\n    // We also use maxFilePerTrigger=1 so that the first getBatch will conduct the check instead\n    // of latestOffset() scanning far ahead and throw the In-Stream version of the exception.\n    val df = readStream(startingVersion = Some(1), maxFilesPerTrigger = Some(1))\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      // Add more data\n      Execute { _ => addData(5 until 10) },\n      // Try processing\n      ProcessAllAvailableIgnoreError,\n      // No data should've been processed :)\n      CheckAnswer(Nil: _*),\n      // The first getBatch should fail\n      if (isCdcTest) {\n        ExpectGenericSchemaIncompatibleFailure\n      } else {\n        ExpectStreamStartInCompatibleSchemaFailure\n      }\n    )\n    // Restart with a schema location, note that maxFilePerTrigger is not needed now\n    // because a schema location is provided and any exception would evolve the schema.\n    val df2 = readStream(startingVersion = Some(1),\n      schemaLocation = Some(getDefaultSchemaLocation.toString))\n    assert(getDefaultSchemaLog().getLatestMetadata.isEmpty)\n    testStream(df2)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      // Again, no data is processed\n      CheckLastBatch(Nil: _*),\n      // Schema evolution exception!\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    // Since the error happened during the first getBatch, we initialize schema log to schema@v1\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == 1)\n    // Restart again with a schema location\n    val df3 = readStream(startingVersion = Some(1),\n      schemaLocation = Some(getDefaultSchemaLocation.toString))\n    testStream(df3)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      // Note that the default maxFilePerTrigger is 1000, so this shows that the batch has been\n      // split and the available data prior to schema change should've been served.\n      // Also since we started at v1, -1 is not included.\n      CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n      // Schema evolution exception!\n      ExpectMetadataEvolutionException\n    )\n    // Now the schema is up to date\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == schemaChangeVersion)\n    // Restart again should pick up the new schema and process the rest without a problem.\n    // Note that startingVersion is ignored when we have existing progress to work with.\n    val df4 = readStream(startingVersion = Some(1),\n      schemaLocation = Some(getDefaultSchemaLocation.toString))\n    testStream(df4)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((5 until 10).map(i => (i.toString, i.toString)): _*)\n    )\n  }\n\n  test(\"identity columns shouldn't cause schema mismatches\") {\n    withTable(\"source\") {\n      sql(\n        s\"\"\"\n          |CREATE TABLE source (key INT, id LONG GENERATED ALWAYS AS IDENTITY)\n          |USING DELTA\n        \"\"\".stripMargin\n      )\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"source\"))\n      deltaLog.update()\n      val schemaLocation = getDefaultSchemaLocation(deltaLog).toString\n      val checkpointLocation = getDefaultCheckpoint(deltaLog).toString\n\n      def addData(values: Seq[Int]): Unit =\n        spark.createDataFrame(values.map(Row(_)).asJava, StructType.fromDDL(\"key INT\"))\n          .write.format(\"delta\").mode(\"append\").saveAsTable(\"source\")\n\n      def readStream(): DataFrame =\n        spark.readStream\n          .format(\"delta\")\n          .option(DeltaOptions.SCHEMA_TRACKING_LOCATION, schemaLocation)\n          .table(\"source\")\n\n      // Check fix disabled: writing to the table updates the identity column's high-water mark\n      // stored in the table schema, causing a schema change to be detected.\n      addData(values = 0 until 5)\n      withSQLConf(\n        DeltaSQLConf.DELTA_STREAMING_IGNORE_INTERNAL_METADATA_FOR_SCHEMA_CHANGE.key -> \"false\"\n      ) {\n        testStream(readStream())(\n          StartStream(checkpointLocation = checkpointLocation),\n          ProcessAllAvailable(),\n          Execute { _ => addData(values = 10 until 15) },\n          ExpectMetadataEvolutionException\n        )\n      }\n\n      // Check fix enabled: high-water mark updates are ignored when checking for schema changes.\n      addData(values = 15 until 20)\n      withSQLConf(\n        DeltaSQLConf.DELTA_STREAMING_IGNORE_INTERNAL_METADATA_FOR_SCHEMA_CHANGE.key -> \"true\"\n      ) {\n        testStream(readStream())(\n          StartStream(checkpointLocation = checkpointLocation),\n          ProcessAllAvailable(),\n          Execute { _ => addData(values = 20 until 25) },\n          ProcessAllAvailable()\n        )\n        // No schema change detected. Note that the identity column metadata is still present in the\n        // tracked schema\n        val field = getDefaultSchemaLog()(deltaLog).getLatestMetadata.get.dataSchema(\"id\")\n        assert(field.metadata.contains(DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK))\n      }\n    }\n  }\n\n  /**\n   * This test manually generates Delta source offsets that crosses non-additive schema change\n   * boundaries to test if the schema log initialization check logic can detect those changes and\n   * error out.\n   */\n  protected def testDetectingInvalidOffsetDuringLogInit(\n      invalidAction: String,\n      readStreamWithSchemaLocation: => DataFrame,\n      expectedLogInitException: StreamAction)(implicit log: DeltaLog): Unit = {\n    // start a stream to initialize checkpoint\n    val ckpt = getDefaultCheckpoint.toString\n    val schemaLoc = getDefaultSchemaLocation.toString\n    val df = readStream(startingVersion = Some(1))\n    testStream(df)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailable(),\n      CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n      StopStream\n    )\n    // Add more data to create room for data offsets, so when the stream resumes, the latest\n    // committed offset if still in the old schema.\n    addData(Seq(6))\n    if (invalidAction == \"rename\") {\n      renameColumn(\"b\", \"c\")\n    } else if (invalidAction == \"drop\") {\n      addColumn(\"c\")\n    }\n    // write more data\n    addData(Seq(7))\n    // Add a rename or drop commit that reverses the previous change, to ensure that our check\n    // has validated all the schema changes, instead of just checking the start schema.\n    if (invalidAction == \"rename\") {\n      renameColumn(\"c\", \"b\")\n    } else if (invalidAction == \"drop\") {\n      dropColumn(\"c\")\n    } else {\n      assert(false, s\"unexpected action ${invalidAction}\")\n    }\n    // write more data\n    addData(Seq(8))\n    val latestVersion = log.update().version\n    // Manually create another offset to latest version to simulate the situation that an end\n    // offset is somehow generated that bypasses the block, e.g. they were upgrading from a\n    // super old version that did not have the block logic, and is left with a constructed\n    // batch that bypasses a schema change.\n    // There should be at MOST one such trailing batch as of today's streaming engine semantics.\n    val offsetContent =\n      manuallyCreateLatestStreamingOffsetUntilReservoirVersion(ckpt, latestVersion)\n\n    // rerun the stream should detect that and fail, even with schema location\n    testStream(readStreamWithSchemaLocation)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      expectedLogInitException\n    )\n\n    // Let's also test the case when we only have one offset in the checkpoint without any committed\n    // Delete everything except the metadata file to avoid triggering metadata validation error\n    val ckptDir = new File(ckpt.stripPrefix(\"file:\"))\n    val metadataFile = new File(ckptDir, \"metadata\")\n    // Delete all checkpoint subdirectories and files except metadata\n    ckptDir.listFiles().foreach { f =>\n      if (f.getAbsolutePath != metadataFile.getAbsolutePath) {\n        if (f.isDirectory) {\n          FileUtils.deleteDirectory(f)\n        } else {\n          f.delete()\n        }\n      }\n    }\n    FileUtils.deleteDirectory(new File(schemaLoc.stripPrefix(\"file:\")))\n\n    // Create a single offset that points to the latest version of the table.\n    manuallyCreateStreamingOffsetAtBatchId(ckpt, 0, offsetContent)\n\n    // One more non additive schema change\n    if (invalidAction == \"rename\") {\n      renameColumn(\"a\", \"x\")\n    } else if (invalidAction == \"drop\") {\n      dropColumn(\"b\")\n    }\n\n    addData(Seq(9))\n\n    val latestVersion2 = log.update().version\n\n    // Create another offset point to the updated latest version\n    manuallyCreateLatestStreamingOffsetUntilReservoirVersion(ckpt, latestVersion2)\n\n    // This should also fail because it crossed the new non-additive schema change above, note that\n    // since we didn't have a committed offset nor a user specified startingVersion, the first\n    // offset will re-read using latestVersion2 - 1 as the initial snapshot now.\n    // Without this new non-additive schema change the validation would actually pass.\n    testStream(readStreamWithSchemaLocation)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      expectedLogInitException\n    )\n  }\n\n  Seq(\"rename\", \"drop\").foreach { invalidAction =>\n    testSchemaEvolution(s\"detect invalid offset during initialization before \" +\n      s\"initializing schema log - $invalidAction\") { implicit log =>\n      def provideStreamingDf: DataFrame =\n        readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n      testDetectingInvalidOffsetDuringLogInit(\n        invalidAction,\n        provideStreamingDf,\n        ExpectSchemaLogInitializationFailedException\n      )\n    }\n  }\n\n  /**\n   * This test checks a corner case on the initialization of the schema log.\n   * When a log is initialized, we would check over ALL pending batches and their delta versions\n   * to ensure we have a safe schema to read all of them (i.e. no non-additive schema changes)\n   * within the range.\n   * This test checks the case when the last version of the range is a non-additive schema change,\n   * but it does not need to be blocked because there's no data to be read during initialization.\n   */\n  protected def testLogInitializationWithoutBlockingOnSchemaChangeInTheEnd(\n      readStreamWithSchemaLocation: => DataFrame,\n      expectLogInitException: StreamAction)(implicit log: DeltaLog): Unit = {\n    // Start a stream to initialize checkpoint\n    val ckpt = getDefaultCheckpoint.toString\n    val df = readStream(startingVersion = Some(1))\n    testStream(df)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailable(),\n      CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n      StopStream\n    )\n    val v0 = log.update().version\n    // The previous committed offset ends at (v0 + 1, -100).\n\n    // Add more data\n    addData(Seq(5))\n    // Non-additive schema change\n    renameColumn(\"b\", \"c\")\n    val v1 = log.update().version\n\n    // Manually create another offset ending on [v1, -100]\n    manuallyCreateLatestStreamingOffsetUntilReservoirVersion(ckpt, v1)\n\n    // Start stream again would attempt to run the constructed batch first.\n    // Since the ending offset does not yet contain the metadata action, we won't need to block\n    // the schema log initialization\n    testStream(readStreamWithSchemaLocation)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      expectLogInitException\n    )\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v0 + 1)\n\n    testStream(readStreamWithSchemaLocation)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailableIgnoreError,\n      // Data processed\n      CheckAnswer((\"5\", \"5\")),\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v1)\n  }\n\n  testSchemaEvolution(s\"no need to block schema log initialization if \" +\n    s\"constructed batch ends on schema change\") { implicit log =>\n    def provideStreamingDf: DataFrame =\n      readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    testLogInitializationWithoutBlockingOnSchemaChangeInTheEnd(\n      provideStreamingDf,\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n  }\n\n  testSchemaEvolution(\"resolve the most encompassing schema during getBatch \" +\n    \"to initialize schema log\") { implicit log =>\n    // start a stream to initialize checkpoint\n    val ckpt = getDefaultCheckpoint.toString\n    val df = readStream(startingVersion = Some(1))\n    testStream(df)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailable()\n    )\n    val v1 = log.update().version\n    // add a new column\n    addColumn(\"c\")\n    // write more data\n    addData(5 until 6)\n    // add another column\n    addColumn(\"d\")\n    val secondAddColumnVersion = log.update().version\n    addData(6 until 10)\n    // add an invalid commit so we could fail directly\n    renameColumn(\"d\", \"d2\")\n    val renamedVersion = log.update().version\n    // v2 should include the two add column change but not the renamed version\n    val v2 = v1 + 5\n    // manually create another offset to latest version\n    manuallyCreateLatestStreamingOffsetUntilReservoirVersion(ckpt, v2, -1)\n    // rerun the stream should detect rename with the stream start check, but since within the\n    // offsets the schema changes are all additive, we could use the encompassing schema <a,b,c,d>.\n    val schemaLocation = getDefaultSchemaLocation.toString\n    testStream(readStream(schemaLocation = Some(schemaLocation)))(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      // Schema can be evolved\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    // Schema log is ready and populated with <a,b,c,d>\n    assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n      .sameElements(Array(\"a\", \"b\", \"c\", \"d\")))\n    // ... which is the schema that should be valid until v2 - 1 (the batch end version).\n    // It is v2 - 1 because the latest offset sits on the BASE_INDEX of v2, which does not contain\n    // any data, so there's no need to consider that for schema change initialization.\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v2 - 1)\n    // Keep going until rename is found\n    testStream(readStream(schemaLocation = Some(schemaLocation)))(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer((Seq(5).map(i => (i.toString, i.toString, i.toString, null)) ++\n        (6 until 10).map(i => (i.toString, i.toString, i.toString, i.toString))): _*),\n      ExpectMetadataEvolutionException\n    )\n    // Schema log is evolved with <a,b,c,d2>\n    assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n      .sameElements(Array(\"a\", \"b\", \"c\", \"d2\")))\n    // ... which is the renamed version\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == renamedVersion)\n  }\n\n  test(\"trigger.Once with deferred commit should work\") {\n    withStarterTable { implicit log =>\n      dropColumn(\"b\")\n      val schemaChangeVersion = log.update().version\n      addData(5 until 10)\n\n      val ckpt = getDefaultCheckpoint.toString\n      val schemaLoc = getDefaultSchemaLocation.toString\n\n      // Use starting version to ignore initial snapshot\n      def read: DataFrame = readStream(schemaLocation = Some(schemaLoc), startingVersion = Some(1))\n\n      // Use once trigger to execute streaming one step a time\n      val StartThisStream = StartStream(trigger = Trigger.Once, checkpointLocation = ckpt)\n      // This trigger:\n      // 1. The stream starts with an uninitialized schema log.\n      // 2. The stream schema is taken from the latest version of the Delta table.\n      // 3. The schema tracking log must initialized immediately, in this case from latestOffset\n      //    because this is the first time the stream starts. The schema is initialized to the\n      //    schema at version 1.\n      // 4. Because the schema at version 1 is not equal to the stream schema, the stream must be\n      //    restarted.\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer(Nil: _*),\n        ExpectMetadataEvolutionExceptionFromInitialization\n      )\n      // Latest schema in schema log has been initialized\n      assert(getDefaultSchemaLog().getLatestMetadata.exists(_.deltaCommitVersion == 1))\n\n      // This trigger:\n      // 1. Finds the latest offset that ends with the schema change\n      // 2. Serve all batches prior to the schema change\n      // Note that the schema has NOT evolved yet because the batch ending at the schema change has\n      // not being committed, and thus we have not triggered the schema evolution and will need an\n      // extra restart.\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer((0 until 5).map(i => (i.toString, i.toString)): _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          // bumped from file action\n          offset.reservoirVersion == schemaChangeVersion &&\n            offset.index == DeltaSourceOffset.METADATA_CHANGE_INDEX &&\n            // serialized as version 3 because METADATA_CHANGE_INDEX is only available in v3\n            offset.json.contains(s\"\"\"\"sourceVersion\":3\"\"\")\n        }\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata.exists(_.deltaCommitVersion == 1))\n      // This trigger:\n      // 1. Finds a NEW latest offset that sets the dummy offset index post schema change\n      // 2. The previous valid batch can be committed\n      // 3. The commit evolves the schema and exit the stream.\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer(Nil: _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          // still stuck, but the pending schema change is marked as evolved\n          offset.reservoirVersion == schemaChangeVersion &&\n            offset.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX &&\n            // serialized as version 3 because POST_METADATA_CHANGE_INDEX is only available in v3\n            offset.json.contains(s\"\"\"\"sourceVersion\":3\"\"\")\n        },\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata\n        .exists(_.deltaCommitVersion == schemaChangeVersion))\n\n      // This trigger:\n      // 1. GetBatch for the empty batch because it was constructed and now no schema mismatches\n      testStream(read)(\n        StartThisStream,\n        AwaitTermination,\n        CheckAnswer(Nil: _*)\n      )\n\n      // This trigger:\n      // 1. Find the latest offset till end of data\n      // 2. Commits the previous empty batch (with no schema change), so no schema evolution\n      // 3. GetBatch of all data\n      val v2 = log.update().version\n      testStream(read)(\n        StartThisStream,\n        AwaitTermination,\n        CheckAnswer((5 until 10).map(i => (i.toString)): _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          // bumped by file action, and since it's an non schema change, just clear schema change\n          offset.reservoirVersion == v2 + 1 &&\n            offset.index == DeltaSourceOffset.BASE_INDEX\n        }\n      )\n\n      // Create a new schema change\n      addColumn(\"b\")\n      val v3 = log.update().version\n      addData(10 until 11)\n\n      // This trigger:\n      // 1. Finds a new offset ending with the schema change index\n      // 2. Commits previous batch (no schema change, thus no schema evolution)\n      // 3. GetBatch of this empty batch\n      testStream(read)(\n        StartThisStream,\n        AwaitTermination,\n        CheckAnswer(Nil: _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          offset.reservoirVersion == v2 + 1 &&\n            offset.index == DeltaSourceOffset.METADATA_CHANGE_INDEX\n        }\n      )\n\n      // This trigger:\n      // 1. Again, finds an empty batch but now ending at the dummy post schema change index.\n      // 2. Commits the previous batch, evolve the schema and fail the stream.\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer(Nil: _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          offset.reservoirVersion == v3 &&\n            offset.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX\n        },\n        ExpectMetadataEvolutionException\n      )\n    }\n  }\n\n  test(\"trigger.AvailableNow should work\") {\n    withStarterTable { implicit log =>\n      dropColumn(\"b\")\n      val schemaChangeVersion = log.update().version\n      addData(5 until 10)\n\n      val ckpt = getDefaultCheckpoint.toString\n      val schemaLoc = getDefaultSchemaLocation.toString\n\n      // Use starting version to ignore initial snapshot\n      def read: DataFrame = readStream(schemaLocation = Some(schemaLoc), startingVersion = Some(1))\n\n      // Use trigger available now\n      val StartThisStream = StartStream(trigger = Trigger.AvailableNow(), checkpointLocation = ckpt)\n\n      // Similar to once trigger, this:\n      // 1. Detects the schema change right-away from computing latest offset\n      // 2. Initialize the schema log and exit stream\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer(Nil: _*),\n        ExpectMetadataEvolutionExceptionFromInitialization\n      )\n      // Latest schema in schema log has been updated\n      assert(getDefaultSchemaLog().getLatestMetadata.exists(_.deltaCommitVersion == 1))\n\n      // Now, this trigger:\n      // 1. Finds the latest offset RIGHT AT the schema change ending at schema change index\n      // 2. GetBatch till that offset\n      // 3. Finds ANOTHER the latest offset ending at the dummy post schema change index\n      // 4. GetBatch for this empty batch\n      // 5. Commits the previous batch\n      // 6. Triggers schema evolution\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer((0 until 5).map(_.toString).map(i => (i, i)): _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          offset.reservoirVersion == schemaChangeVersion &&\n            // schema change marked as evolved\n            offset.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX\n        },\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata\n        .exists(_.deltaCommitVersion == schemaChangeVersion))\n\n      // This trigger:\n      // 1. Finds the next latest offset, which is the end of data\n      // 2. Commit previous empty batch with no pending schema change\n      // 3. GetBatch with the remaining data\n      val latestVersion = log.update().version\n      testStream(read)(\n        StartThisStream,\n        AwaitTermination,\n        CheckAnswer((5 until 10).map(i => (i.toString)): _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          // schema change cleared because it's a non-schema change offset\n          offset.reservoirVersion == latestVersion + 1 &&\n            offset.index == DeltaSourceOffset.BASE_INDEX\n        }\n      )\n\n      // Create a new schema change\n      addColumn(\"b\")\n      val v3 = log.update().version\n      addData(10 until 11)\n\n      // This trigger:\n      // 1. Finds the latest offset, again ending at the schema change index\n      // 2. Commits previous batch\n      // 3. GetBatch with empty data and schema change ending offset\n      // 4. Finds another latest offset, ending at the dummy post schema change index\n      // 5. Commits the empty batch at 3, evolves schema log and restart stream.\n      testStream(read)(\n        StartThisStream,\n        AwaitTerminationIgnoreError,\n        CheckAnswer(Nil: _*),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          offset.reservoirVersion == v3 &&\n            offset.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX\n        },\n        ExpectMetadataEvolutionException\n      )\n\n      // Finish the rest\n      testStream(read)(\n        StartThisStream,\n        AwaitTermination,\n        CheckAnswer((10 until 11).map(_.toString).map(i => (i, i)): _*)\n      )\n    }\n  }\n\n  testSchemaEvolution(\"consecutive schema evolutions without schema merging\") { implicit log =>\n    withSQLConf(\n      DeltaSQLConf.DELTA_STREAMING_ENABLE_SCHEMA_TRACKING_MERGE_CONSECUTIVE_CHANGES.key\n        -> \"false\") {\n      val v5 = log.update().version // v5 has an ADD file action with value (4, 4)\n      renameColumn(\"b\", \"c\") // v6\n      renameColumn(\"c\", \"b\") // v7\n      dropColumn(\"b\") // v9\n      addColumn(\"b\") // v10\n\n      def df: DataFrame = readStream(\n        schemaLocation = Some(getDefaultSchemaLocation.toString), startingVersion = Some(v5))\n\n      // The schema log initializes @ v1 with schema <a, b>\n      testStream(df)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        AssertOnQuery { q =>\n          // initialization does not generate any offsets\n          q.availableOffsets.isEmpty\n        },\n        ExpectMetadataEvolutionExceptionFromInitialization\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n        .sameElements(Array(\"a\", \"b\")))\n      // Encounter next schema change <a, c>\n      testStream(df)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        CheckAnswer(Seq(4).map(_.toString).map(i => (i, i)): _*),\n        AssertOnQuery { q =>\n          q.availableOffsets.size == 1 && {\n            val offset = DeltaSourceOffset(\n              log.unsafeVolatileTableId, q.availableOffsets.values.head)\n            offset.reservoirVersion == v5 + 1 && offset.index == indexWhenSchemaLogIsUpdated\n          }\n        },\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5 + 1)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n        .sameElements(Array(\"a\", \"c\")))\n      // Encounter next schema change <a, b> again\n      testStream(df)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        AssertOnQuery { q =>\n          // size is 1 because commit removes previous offset\n          q.availableOffsets.size == 1 && {\n            val offset = DeltaSourceOffset(\n              log.unsafeVolatileTableId, q.availableOffsets.values.head)\n            offset.reservoirVersion == v5 + 2 && offset.index == indexWhenSchemaLogIsUpdated\n          }\n        },\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5 + 2)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n        .sameElements(Array(\"a\", \"b\")))\n      // Encounter next schema change <a>\n      testStream(df)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        AssertOnQuery { q =>\n          q.availableOffsets.size == 1 && {\n            val offset = DeltaSourceOffset(\n              log.unsafeVolatileTableId, q.availableOffsets.values.head)\n            offset.reservoirVersion == v5 + 3 && offset.index == indexWhenSchemaLogIsUpdated\n          }\n        },\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5 + 3)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n        .sameElements(Array(\"a\")))\n      // Encounter next schema change <a, b> again\n      testStream(df)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        AssertOnQuery { q =>\n          q.availableOffsets.size == 1 && {\n            val offset = DeltaSourceOffset(\n              log.unsafeVolatileTableId, q.availableOffsets.values.head)\n            offset.reservoirVersion == v5 + 4 && offset.index == indexWhenSchemaLogIsUpdated\n          }\n        },\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5 + 4)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n        .sameElements(Array(\"a\", \"b\")))\n    }\n  }\n\n  testSchemaEvolution(\"consecutive schema evolutions\") { implicit log =>\n    // By default we have consecutive schema merging turned on\n    val v5 = log.update().version // v5 has an ADD file action with value (4, 4)\n    renameColumn(\"b\", \"c\") // v6\n    renameColumn(\"c\", \"b\") // v7\n    dropColumn(\"b\") // v9\n    addColumn(\"b\") // v10\n    val v10 = log.update().version\n    // Write some more data post the consecutive schema changes\n    addData(5 until 6)\n\n    def df: DataFrame = readStream(\n      schemaLocation = Some(getDefaultSchemaLocation.toString), startingVersion = Some(v5))\n\n    // The schema log initializes @ v1 with schema <a, b>\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      AssertOnQuery { q =>\n        // initialization does not generate any offsets\n        q.availableOffsets.isEmpty\n      },\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5)\n    assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n      .sameElements(Array(\"a\", \"b\")))\n    // Encounter next schema change <a, c>\n    // This still fails schema evolution exception and won't scan ahead\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Seq(4).map(_.toString).map(i => (i, i)): _*),\n      AssertOnQuery { q =>\n        q.availableOffsets.size == 1 && {\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.head)\n          offset.reservoirVersion == v5 + 1 && offset.index == indexWhenSchemaLogIsUpdated\n        }\n      },\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5 + 1)\n    assert(getDefaultSchemaLog().getLatestMetadata.get.dataSchema.fieldNames\n      .sameElements(Array(\"a\", \"c\")))\n\n    // Now the next restart would scan over the consecutive schema changes and use the last one\n    // to initialize the schema again.\n    val latestDf = df\n    assert(latestDf.schema.fieldNames.sameElements(Array(\"a\", \"b\")))\n    // The analysis phase should've already updated schema log\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v10)\n    // Processing should ignore the intermediary schema changes and process the data using the\n    // merged schema.\n    testStream(latestDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((5 until 6).map(i => (i.toString, i.toString)): _*)\n    )\n  }\n\n  testSchemaEvolution(\"upgrade and downgrade\") { implicit log =>\n    val ckpt = getDefaultCheckpoint.toString\n    val df = readStream(startingVersion = Some(1))\n    val v0 = log.update().version\n    // Initialize a stream\n    testStream(df)(\n      StartStream(checkpointLocation = ckpt),\n      ProcessAllAvailable(),\n      CheckAnswer((0 until 5).map(_.toString).map(i => (i, i)): _*),\n      AssertOnQuery { q =>\n        assert(q.availableOffsets.size == 1)\n        val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n        offset.reservoirVersion == v0 + 1 &&\n          offset.index == DeltaSourceOffset.BASE_INDEX\n      }\n    )\n\n    addData(Seq(5))\n    val v1 = log.update().version\n    dropColumn(\"b\")\n    val v2 = log.update().version\n\n    // Restart with schema location should initialize\n    val df2 = readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    testStream(df2)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      AssertOnQuery { q =>\n        // initialization does not generate any more offsets\n        q.availableOffsets.size <= 1\n      },\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    // The schema should be valid until v1 (the batch end version).\n    // It is v1 - 1 because the latest offset sits on the BASE_INDEX of v1, which does not contain\n    // any data, so there's no need to consider that for schema change initialization.\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v1 - 1)\n\n    // Restart again should be able to use the new offset version\n    val df3 = readStream(schemaLocation = Some(getDefaultSchemaLocation.toString))\n    val logAppenderUpgrade = new LogAppender(\"Should convert legacy offset\", maxEvents = 1e6.toInt)\n    logAppenderUpgrade.setThreshold(Level.DEBUG)\n\n    withLogAppender(logAppenderUpgrade, level = Some(Level.DEBUG)) {\n      testStream(df3)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        CheckAnswer((\"5\", \"5\")),\n        AssertOnQuery { q =>\n          val offset = DeltaSourceOffset(log.unsafeVolatileTableId, q.availableOffsets.values.last)\n          offset.reservoirVersion == v2 &&\n            offset.index == indexWhenSchemaLogIsUpdated\n        },\n        ExpectMetadataEvolutionException\n      )\n    }\n    assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v2)\n    // Should've upgraded the legacy offset\n    val target = logAppenderUpgrade.loggingEvents.find(\n      _.getMessage.toString.contains(\"upgrading offset \"))\n    assert(target.isDefined)\n\n    // Add more data\n    addData(Seq(6))\n\n    // Suppose now the user doesn't want to use schema tracking any more, and whats to downgrade\n    // to use latest schema again, it should be able to do that.\n    val df4 = readStream() // without schema location\n    val logAppenderDowngrade = new LogAppender(\"Should convert new offset\", maxEvents = 1e6.toInt)\n    logAppenderDowngrade.setThreshold(Level.DEBUG)\n\n    withSQLConf(\n      DeltaSQLConf.DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_SCHEMA_CHANGES_DURING_STREAM_START\n        .key -> \"true\",\n      DeltaSQLConf.DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES\n        .key -> \"true\") {\n      withLogAppender(logAppenderDowngrade, level = Some(Level.DEBUG)) {\n        testStream(df4)(\n          StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n          ProcessAllAvailable(),\n          // See the next read just falls back to use latest schema\n          CheckAnswer((\"6\"))\n        )\n      }\n    }\n  }\n\n  testSchemaEvolution(\"multiple sources with schema evolution\"\n    ) { implicit log =>\n    val v5 = log.update().version // v5 has an ADD file action with value (4, 4)\n    renameColumn(\"b\", \"c\")\n    addData(5 until 10)\n\n    val schemaLog1Location = new Path(getDefaultCheckpoint, \"_schema_log1\").toString\n    val schemaLog2Location = new Path(getDefaultCheckpoint, \"_schema_log2\").toString\n\n    // Join two individual sources with two schema log\n    // Each source should return an identical batch and therefore the output batch should also be\n    // identical, we are just using join to create a multi-source situation.\n    def df: DataFrame =\n      readStream(schemaLocation =\n        Some(schemaLog1Location),\n        startingVersion = Some(v5))\n        .unionByName(\n          readStream(schemaLocation =\n            Some(schemaLog2Location),\n            startingVersion = Some(v5)), allowMissingColumns = true)\n\n    // Both schema log initialized\n    def schemaLog1: DeltaSourceMetadataTrackingLog = DeltaSourceMetadataTrackingLog.create(\n      spark, schemaLog1Location, log.update(), catalogTableOpt = None, parameters = Map.empty)\n    def schemaLog2: DeltaSourceMetadataTrackingLog = DeltaSourceMetadataTrackingLog.create(\n      spark, schemaLog2Location, log.update(), catalogTableOpt = None, parameters = Map.empty)\n\n    // The schema log initializes @ v5 with schema <a, b>\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      AssertOnQuery { q =>\n        // initialization does not generate any offsets\n        q.availableOffsets.isEmpty\n      },\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n\n    // But takes another restart for the other Delta source\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      AssertOnQuery { q =>\n        // initialization does not generate any offsets\n        q.availableOffsets.isEmpty\n      },\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n\n    // Both schema log should be initialized\n    assert(schemaLog1.getCurrentTrackedMetadata.map(_.deltaCommitVersion) ==\n      schemaLog2.getCurrentTrackedMetadata.map(_.deltaCommitVersion))\n\n    // One of the source will commit and fail\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      // The data prior to schema change is served\n      // Two rows in schema [a, b]\n      CheckAnswer((\"4\", \"4\"), (\"4\", \"4\")),\n      ExpectMetadataEvolutionException\n    )\n\n    // Restart should fail the other commit\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      ExpectMetadataEvolutionException\n    )\n\n    assert(schemaLog1.getCurrentTrackedMetadata.map(_.deltaCommitVersion) ==\n      schemaLog2.getCurrentTrackedMetadata.map(_.deltaCommitVersion))\n\n    // Restart stream should proceed on loading the rest of data\n    testStream(df)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      // Unioned data is served\n      // 10 rows in schema [a, c]\n      CheckAnswer((5 until 10).map(_.toString).flatMap(i => Seq((i, i), (i, i))): _*)\n    )\n\n    // Attempt to use the wrong schema log for each source will be detected\n    val wrongDf = readStream(schemaLocation =\n      // instead of using schemaLog1Location\n      Some(schemaLog2Location),\n      startingVersion = Some(v5))\n      .unionByName(\n        readStream(schemaLocation =\n          // instead of using schemaLog2Location\n          Some(schemaLog1Location),\n          startingVersion = Some(v5)), allowMissingColumns = true)\n\n    testStream(wrongDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      ExpectFailure[IllegalArgumentException](t =>\n        assert(t.getMessage.contains(\"The Delta source metadata path used for execution\")))\n    )\n  }\n\n  testSchemaEvolution(\"schema evolution with Delta sink\") { implicit log =>\n    val v5 = log.update().version // v5 has an ADD file action with value (4)\n    renameColumn(\"b\", \"c\")\n    val renameVersion1 = log.update().version\n    addData(5 until 10)\n    renameColumn(\"c\", \"b\")\n    val renameVersion2 = log.update().version\n    addData(10 until 15)\n    dropColumn(\"b\")\n    val dropVersion = log.update().version\n    addData(15 until 20)\n    addColumn(\"b\")\n    val addVersion = log.update().version\n    addData(20 until 25)\n\n    withTempDir { sink =>\n      def writeStream(df: DataFrame): Unit = {\n        val q = df.writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", getDefaultCheckpoint.toString)\n          .option(\"mergeSchema\", \"true\") // for automatically adding columns\n          .start(sink.getCanonicalPath)\n        q.processAllAvailable()\n        q.stop()\n      }\n\n      def df: DataFrame = readStream(\n        schemaLocation = Some(getDefaultSchemaLocation.toString), startingVersion = Some(v5))\n      def readSink: DataFrame = spark.read.format(\"delta\").load(sink.getCanonicalPath)\n\n      val e1 = ExceptionUtils.getRootCause {\n        intercept[StreamingQueryException] {\n          writeStream(df)\n        }\n      }\n      ExpectMetadataEvolutionExceptionFromInitialization.assertFailure(e1)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == v5)\n\n      val e2 = ExceptionUtils.getRootCause {\n        intercept[StreamingQueryException] {\n          writeStream(df)\n        }\n      }\n      assert(readSink.schema.fieldNames sameElements Array(\"a\", \"b\"))\n      checkAnswer(readSink, Seq(4).map(_.toString).map(i => Row(i, i)))\n      ExpectMetadataEvolutionException.assertFailure(e2)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == renameVersion1)\n\n      val e3 = ExceptionUtils.getRootCause {\n        intercept[StreamingQueryException] {\n          writeStream(df)\n        }\n      }\n      // c added as a new column\n      assert(readSink.schema.fieldNames sameElements Array(\"a\", \"b\", \"c\"))\n      checkAnswer(readSink, Seq(4).map(_.toString).map(i => Row(i, i, null)) ++\n        (5 until 10).map(_.toString).map(i => Row(i, null, i)))\n      ExpectMetadataEvolutionException.assertFailure(e3)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == renameVersion2)\n\n      val e4 = ExceptionUtils.getRootCause {\n        intercept[StreamingQueryException] {\n          writeStream(df)\n        }\n      }\n      // c was renamed to b, new data now writes to b\n      assert(readSink.schema.fieldNames sameElements Array(\"a\", \"b\", \"c\"))\n      checkAnswer(readSink, Seq(4).map(_.toString).map(i => Row(i, i, null)) ++\n        (5 until 10).map(_.toString).map(i => Row(i, null, i)) ++\n        (10 until 15).map(_.toString).map(i => Row(i, i, null)))\n      ExpectMetadataEvolutionException.assertFailure(e4)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == dropVersion)\n\n      val e5 = ExceptionUtils.getRootCause {\n        intercept[StreamingQueryException] {\n          writeStream(df)\n        }\n      }\n      // b was dropped, but sink remains the same\n      assert(readSink.schema.fieldNames sameElements Array(\"a\", \"b\", \"c\"))\n      checkAnswer(readSink, Seq(4).map(_.toString).map(i => Row(i, i, null)) ++\n        (5 until 10).map(_.toString).map(i => Row(i, null, i)) ++\n        (10 until 15).map(_.toString).map(i => Row(i, i, null)) ++\n        (15 until 20).map(_.toString).map(i => Row(i, null, null)))\n      ExpectMetadataEvolutionException.assertFailure(e5)\n      assert(getDefaultSchemaLog().getLatestMetadata.get.deltaCommitVersion == addVersion)\n\n      // Finish the stream without errors\n      writeStream(df)\n      // b was added back, sink remains the same\n      assert(readSink.schema.fieldNames sameElements Array(\"a\", \"b\", \"c\"))\n      checkAnswer(readSink, Seq(4).map(_.toString).map(i => Row(i, i, null)) ++\n        (5 until 10).map(_.toString).map(i => Row(i, null, i)) ++\n        (10 until 15).map(_.toString).map(i => Row(i, i, null)) ++\n        (15 until 20).map(_.toString).map(i => Row(i, null, null)) ++\n        (20 until 25).map(_.toString).map(i => Row(i, i, null)))\n    }\n  }\n\n  testSchemaEvolution(\"latestOffset should not progress before schema evolved\") { implicit log =>\n    val s0 = log.update()\n    // Change schema\n    renameColumn(\"b\", \"c\")\n    val v0 = log.update().version\n    addData(Seq(5))\n    val v1 = log.update().version\n\n    // Manually construct a Delta source since it's hard to test multiple (2+) latestOffset() calls\n    // with the current streaming engine without incurring the schema evolution failure.\n    def getSource: DeltaSource = DeltaSource(\n      spark,\n      log,\n      catalogTableOpt = None,\n      new DeltaOptions(Map(\"startingVersion\" -> \"0\"), spark.sessionState.conf),\n      log.update(),\n      metadataPath = \"\",\n      Some(getDefaultSchemaLog()))\n\n    def getLatestOffset(source: DeltaSource, start: Option[Offset] = None): DeltaSourceOffset =\n      DeltaSourceOffset(log.unsafeVolatileTableId,\n        source.latestOffset(start.orNull, source.getDefaultReadLimit))\n\n    // Initialize the schema log to skip initialization failure\n    getDefaultSchemaLog().writeNewMetadata(\n      PersistedMetadata(\n        log.unsafeVolatileTableId,\n        0L,\n        s0.metadata,\n        s0.protocol,\n        sourceMetadataPath = \"\"\n      )\n    )\n\n    val source1 = getSource\n\n    // 1st call, land at INDEX_SCHEMA_CHANGE\n    val ofs1 = getLatestOffset(source1)\n    assert(ofs1.index == DeltaSourceOffset.METADATA_CHANGE_INDEX)\n    source1.getBatch(startOffsetOption = None, ofs1)\n    // 2nd call, land at INDEX_POST_SCHEMA_CHANGE\n    val ofs2 = getLatestOffset(source1, Some(ofs1))\n    assert(ofs2.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX)\n    source1.getBatch(Some(ofs1), ofs2)\n    // 3rd call, still land at INDEX_POST_SCHEMA_CHANGE, because schema evolution has not happened\n    val ofs3 = getLatestOffset(source1, Some(ofs2))\n    assert(ofs3.index == DeltaSourceOffset.POST_METADATA_CHANGE_INDEX)\n    // Commit and restart\n    val e = intercept[DeltaRuntimeException] {\n      source1.commit(ofs2)\n    }\n    ExpectMetadataEvolutionException.assertFailure(e)\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v0)\n\n    val source2 = getSource\n    // restore previousOffset\n    source2.getBatch(Some(ofs3), ofs3)\n    // 4th call, should move on to latest version + 1 (bumped by file action)\n    val ofs4 = getLatestOffset(source2, Some(ofs3))\n    assert(ofs4.index == DeltaSourceOffset.BASE_INDEX &&\n      ofs4.reservoirVersion == v1 + 1)\n  }\n\n  protected def expectSqlConfException(\n      opType: String,\n      ver: Long,\n      columnChangeDetails: String,\n      checkpointHash: Int) = {\n    ExpectFailure[DeltaRuntimeException] { e =>\n      val se = e.asInstanceOf[DeltaRuntimeException]\n      assert {\n        se.getErrorClass == \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\" &&\n          se.messageParameters(0) == opType && se.messageParameters(2) == ver.toString &&\n          se.messageParameters(3).contains(columnChangeDetails) &&\n          se.messageParameters.exists(_.contains(checkpointHash.toString))\n      }\n    }\n  }\n\n  /**\n   * Initialize a simple streaming DF for a simple table with just one (0, 0) entry for schema <a,b>\n   * We also prepare an initialized schema log to skip the initialization phase.\n   */\n  protected def withSimpleStreamingDf(f: (() => DataFrame, DeltaLog) => Unit): Unit = {\n    withTempDir { dir =>\n      val tablePath = dir.getCanonicalPath\n      Seq((\"0\", \"0\")).toDF(\"a\", \"b\")\n        .write.mode(\"append\").format(\"delta\").save(tablePath)\n      implicit val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val s0 = log.update()\n      val schemaLog = getDefaultSchemaLog()\n      schemaLog.writeNewMetadata(\n        PersistedMetadata(log.unsafeVolatileTableId, s0.version, s0.metadata, s0.protocol,\n          sourceMetadataPath = \"\")\n      )\n\n      def read(): DataFrame =\n        readStream(\n          Some(getDefaultSchemaLocation.toString),\n          startingVersion = Some(s0.version))\n\n      // Initialize checkpoint\n      withSQLConf(\n        DeltaSQLConf.DELTA_STREAMING_SCHEMA_TRACKING_METADATA_PATH_CHECK_ENABLED.key -> \"false\") {\n        testStream(read())(\n          StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n          ProcessAllAvailable(),\n          CheckAnswer((\"0\", \"0\")),\n          StopStream\n        )\n        f(read, log)\n      }\n    }\n  }\n\n  testWithoutAllowStreamRestart(\"unblock with sql conf\") {\n    def testStreamFlow(\n        changeSchema: DeltaLog => Unit,\n        schemaChangeType: String,\n        columnChangeDetails: String,\n        getConfKV: (Int, Long) => (String, String)): Unit = {\n      withSimpleStreamingDf { (readDf, log) =>\n        val ckptHash = (getDefaultCheckpoint(log).toString + \"/sources/0\").hashCode\n        changeSchema(log)\n        val v1 = log.update().version\n        addData(Seq(1))(log)\n        // Encounter schema evolution exception\n        testStream(readDf())(\n          StartStream(checkpointLocation = getDefaultCheckpoint(log).toString),\n          ProcessAllAvailableIgnoreError,\n          CheckAnswer(Nil: _*),\n          ExpectMetadataEvolutionException\n        )\n        // Restart would fail due to SQL conf validation\n        testStream(readDf())(\n          StartStream(checkpointLocation = getDefaultCheckpoint(log).toString),\n          ProcessAllAvailableIgnoreError,\n          CheckAnswer(Nil: _*),\n          expectSqlConfException(schemaChangeType, v1, columnChangeDetails, ckptHash)\n        )\n        // Another restart still fails\n        testStream(readDf())(\n          StartStream(checkpointLocation = getDefaultCheckpoint(log).toString),\n          ProcessAllAvailableIgnoreError,\n          CheckAnswer(Nil: _*),\n          expectSqlConfException(schemaChangeType, v1, columnChangeDetails, ckptHash)\n        )\n        // With SQL Conf set we can move on\n        val (k, v) = getConfKV(ckptHash, v1)\n        withSQLConf(k -> v) {\n          testStream(readDf())(\n            StartStream(checkpointLocation = getDefaultCheckpoint(log).toString),\n            ProcessAllAvailable()\n          )\n        }\n      }\n    }\n\n    // Test drop column\n    Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnDrop\").foreach { allow =>\n      Seq(\n        (\n          (log: DeltaLog) => {\n            dropColumn(\"a\")(log)\n            // Revert the drop to test consecutive schema changes won't affect sql conf validation\n            // the new column will show up with different physical name so it can trigger the\n            // DROP COLUMN detection logic\n            addColumn(\"a\")(log)\n          },\n          (ckptHash: Int, _: Long) =>\n            (s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming.$allow.ckpt_$ckptHash\", \"always\")\n        ),\n        (\n          (log: DeltaLog) => {\n            dropColumn(\"a\")(log)\n            // Ditto\n            addColumn(\"a\")(log)\n          },\n          (ckptHash: Int, ver: Long) =>\n            (s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming.$allow.ckpt_$ckptHash\", ver.toString)\n        )\n      ).foreach { case (changeSchema, getConfKV) =>\n        testStreamFlow(\n          changeSchema,\n          schemaChangeType = \"DROP COLUMN\",\n          columnChangeDetails =\n            s\"\"\"Columns dropped:\n               |'a'\n               |\"\"\".stripMargin,\n          getConfKV)\n      }\n    }\n\n    // Test rename column\n    Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnRename\").foreach { allow =>\n      Seq(\n        (\n          (log: DeltaLog) => {\n            renameColumn(\"b\", \"c\")(log)\n          },\n          (ckptHash: Int, _: Long) =>\n            (s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming.$allow.ckpt_$ckptHash\", \"always\")\n        ),\n        (\n          (log: DeltaLog) => {\n            renameColumn(\"b\", \"c\")(log)\n          },\n          (ckptHash: Int, ver: Long) =>\n            (s\"${DeltaSQLConf.SQL_CONF_PREFIX}.streaming.$allow.ckpt_$ckptHash\", ver.toString)\n        )\n      ).foreach { case (changeSchema, getConfKV) =>\n        testStreamFlow(\n          changeSchema,\n          schemaChangeType = \"RENAME COLUMN\",\n          columnChangeDetails =\n            s\"\"\"Columns renamed:\n               |'b' -> 'c'\n               |\"\"\".stripMargin,\n          getConfKV\n        )\n      }\n    }\n  }\n\n  testSchemaEvolution(\n    \"schema tracking interacting with unsafe escape flag\") { implicit log =>\n    renameColumn(\"b\", \"c\")\n    // Even when schema location is provided, it won't be initialized because the unsafe\n    // flag is turned on.\n    val df = readStream(\n      schemaLocation = Some(getDefaultSchemaLocation.toString), startingVersion = Some(1L))\n    withSQLConf(\n      DeltaSQLConf.DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES.key\n        -> \"true\") {\n      testStream(df)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailable(),\n        CheckAnswer((0 until 5).map(_.toString).map(i => (i, i)): _*)\n      )\n    }\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.isEmpty)\n  }\n\n  testSchemaEvolution(\n    \"streaming with a column mapping upgrade\", columnMapping = false) { implicit log =>\n    upgradeToNameMode\n    val v0 = log.update().version\n    renameColumn(\"b\", \"c\")\n    val v1 = log.update().version\n    addData(5 until 10)\n\n    // Start schema tracking from prior to upgrade\n    // Initialize schema tracking log\n    def readDf(): DataFrame =\n      readStream(\n        schemaLocation = Some(getDefaultSchemaLocation.toString),\n        startingVersion = Some(1))\n\n    testStream(readDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    assert {\n      val schemaEntry = getDefaultSchemaLog().getCurrentTrackedMetadata.get\n      schemaEntry.deltaCommitVersion == 1 &&\n        // no physical name entry\n        !DeltaColumnMapping.hasPhysicalName(schemaEntry.dataSchema.head)\n    }\n\n    testStream(readDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer((0 until 5).map(_.toString).map(i => (i, i)): _*),\n      ExpectMetadataEvolutionException\n    )\n    assert {\n      val schemaEntry = getDefaultSchemaLog().getCurrentTrackedMetadata.get\n      // stopped at the upgrade commit\n      schemaEntry.deltaCommitVersion == v0 &&\n        // now with physical name entry\n        DeltaColumnMapping.hasPhysicalName(schemaEntry.dataSchema.head)\n    }\n\n    // Note that since we have schema merging, we won't need to fail again at the rename column\n    // schema change, the rest of the data can be served altogether.\n    testStream(readDf())(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((5 until 10).map(_.toString).map(i => (i, i)): _*)\n    )\n\n    assert {\n      val schemaEntry = getDefaultSchemaLog().getCurrentTrackedMetadata.get\n      // schema log updated implicitly\n      schemaEntry.deltaCommitVersion == v1 &&\n        schemaEntry.dataSchema.fieldNames.sameElements(Array(\"a\", \"c\"))\n    }\n\n  }\n\n  test(\"backward-compat: latest version can read back older JSON\") {\n    val serialized = JsonUtils.toJson {\n      OldPersistedSchema(\n        tableId = \"test\",\n        deltaCommitVersion = 1,\n        StructType.fromDDL(\"a INT\").json,\n        StructType.fromDDL(\"a INT\").json,\n        sourceMetadataPath = \"\"\n      )\n    }\n\n    val schemaFromJson = PersistedMetadata.fromJson(serialized)\n    assert(schemaFromJson == PersistedMetadata(\n      tableId = \"test\",\n      deltaCommitVersion = 1,\n      StructType.fromDDL(\"a INT\").json,\n      StructType.fromDDL(\"a INT\").json,\n      sourceMetadataPath = \"\",\n      tableConfigurations = None,\n      protocolJson = None,\n      previousMetadataSeqNum = None\n    ))\n  }\n\n  test(\"forward-compat: older version can read back newer JSON\") {\n    val newSchema = PersistedMetadata(\n      tableId = \"test\",\n      deltaCommitVersion = 1,\n      StructType.fromDDL(\"a INT\").json,\n      StructType.fromDDL(\"a INT\").json,\n      sourceMetadataPath = \"/path\",\n      tableConfigurations = Some(Map(\"a\" -> \"b\")),\n      protocolJson = Some(Protocol(1, 2).json),\n      previousMetadataSeqNum = Some(1L)\n    )\n\n    assert {\n      JsonUtils.fromJson[OldPersistedSchema](JsonUtils.toJson(newSchema)) == OldPersistedSchema(\n        tableId = \"test\",\n        deltaCommitVersion = 1,\n        StructType.fromDDL(\"a INT\").json,\n        StructType.fromDDL(\"a INT\").json,\n        sourceMetadataPath = \"/path\"\n      )\n    }\n  }\n\n  testSchemaEvolution(\"partition evolution\") { implicit log =>\n    // Same schema but different partition\n    overwriteSchema(log.update().schema, partitionColumns = Seq(\"a\"))\n    val v0 = log.update().version\n    addData(5 until 10)\n    overwriteSchema(log.update().schema, partitionColumns = Seq(\"b\"))\n    val v1 = log.update().version\n    def readDf: DataFrame =\n      readStream(schemaLocation = Some(getDefaultSchemaLocation.toString),\n        startingVersion = Some(1),\n        // ignoreDeletes because overwriteSchema would generate RemoveFiles.\n        ignoreDeletes = Some(true))\n\n    // Init schema log\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      AwaitTerminationIgnoreError,\n      CheckAnswer(Nil: _*),\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    // Latest schema in schema log has been updated\n    assert(getDefaultSchemaLog().getLatestMetadata.exists(_.deltaCommitVersion == 1))\n    // Process the first batch before overwrite\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer((0 until 5).map(_.toString).map(i => (i, i)): _*),\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getLatestMetadata.exists(_.deltaCommitVersion == v0))\n\n    // Process until the next overwrite\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(\n        // TODO: since we did an overwrite, the previous RemoveFiles are also captured, but they are\n        //  using the old physical schema, we cannot read them back correctly. This is a corner case\n        //  with schema overwrite + CDC, although technically CDC should not worry about overwrite\n        //  because that means the downstream table needs to be truncated after applying CDC.\n        // Note that since we support reuse physical name across overwrite, the value of partition\n        // can still be read.\n        (if (isCdcTest) (-1 until 5).map(_.toString).map(i => (null, i)) else Nil) ++\n        (5 until 10).map(_.toString).map(i => (i, i)): _*),\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getLatestMetadata.exists(_.deltaCommitVersion == v1))\n  }\n\n  testSchemaEvolution(\"schema log replace current\", columnMapping = false) { implicit log =>\n    withSQLConf(\n      DeltaSQLConf.DELTA_STREAMING_SCHEMA_TRACKING_METADATA_PATH_CHECK_ENABLED.key -> \"false\") {\n      // Schema log's schema is respected\n      val schemaLog = getDefaultSchemaLog()\n      val s0 = PersistedMetadata(log.unsafeVolatileTableId, 0,\n        makeMetadata(\n          new StructType().add(\"a\", StringType, true)\n            .add(\"b\", StringType, true)\n            .add(\"c\", StringType, true),\n          partitionSchema = new StructType()\n        ),\n        log.update().protocol,\n        sourceMetadataPath = \"\"\n      )\n      // The `replaceCurrent` is noop because there is no previous schema.\n      schemaLog.writeNewMetadata(s0, replaceCurrent = true)\n      assert(schemaLog.getCurrentTrackedMetadata.contains(s0))\n      assert(schemaLog.getPreviousTrackedMetadata.isEmpty)\n\n      val s1 = s0.copy(deltaCommitVersion = 1L)\n      schemaLog.writeNewMetadata(s1)\n      assert(schemaLog.getCurrentTrackedMetadata.contains(s1))\n      assert(schemaLog.getPreviousTrackedMetadata.contains(s0))\n\n      val s2 = s1.copy(deltaCommitVersion = 2L)\n      schemaLog.writeNewMetadata(s2, replaceCurrent = true)\n      assert(schemaLog.getCurrentTrackedMetadata.contains(\n        s2.copy(previousMetadataSeqNum = Some(0L))))\n      assert(schemaLog.getPreviousTrackedMetadata.contains(s0))\n\n      val s3 = s2.copy(deltaCommitVersion = 3L)\n      schemaLog.writeNewMetadata(s3, replaceCurrent = true)\n      assert(schemaLog.getCurrentTrackedMetadata.contains(\n        s3.copy(previousMetadataSeqNum = Some(0L))))\n      assert(schemaLog.getPreviousTrackedMetadata.contains(s0))\n\n      val s4 = s3.copy(deltaCommitVersion = 4L)\n      schemaLog.writeNewMetadata(s4)\n      assert(schemaLog.getCurrentTrackedMetadata.contains(s4))\n      assert(schemaLog.getPreviousTrackedMetadata.contains(\n        s3.copy(previousMetadataSeqNum = Some(0L))))\n\n      val s5 = s4.copy(deltaCommitVersion = 5L)\n      schemaLog.writeNewMetadata(s5, replaceCurrent = true)\n      assert(schemaLog.getCurrentTrackedMetadata.contains(\n        s5.copy(previousMetadataSeqNum = Some(3L))))\n      assert(schemaLog.getPreviousTrackedMetadata.contains(\n        s3.copy(previousMetadataSeqNum = Some(0L))))\n    }\n  }\n}\n\n// Needs to be top-level for serialization to work.\ncase class OldPersistedSchema(\n  tableId: String,\n  deltaCommitVersion: Long,\n  dataSchemaJson: String,\n  partitionSchemaJson: String,\n  sourceMetadataPath: String\n)\n\nclass DeltaSourceSchemaEvolutionNameColumnMappingSuite\n  extends StreamingSchemaEvolutionSuiteBase\n    with DeltaColumnMappingEnableNameMode {\n  override def isCdcTest: Boolean = false\n}\n\nclass DeltaSourceSchemaEvolutionIdColumnMappingSuite\n  extends StreamingSchemaEvolutionSuiteBase\n    with DeltaColumnMappingEnableIdMode {\n  override def isCdcTest: Boolean = false\n}\n\ntrait CDCStreamingSchemaEvolutionSuiteBase extends StreamingSchemaEvolutionSuiteBase {\n  override def isCdcTest: Boolean = true\n\n  import testImplicits._\n\n  // This test will generate AddCDCFiles\n  test(\"CDC streaming with schema evolution\") {\n    withTempDir { dir =>\n      spark.range(10).toDF(\"id\").write.format(\"delta\").save(dir.getCanonicalPath)\n      implicit val log: DeltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n      {\n        withTable(\"merge_source\") {\n          spark.range(10).filter(_ % 2 == 0)\n            .toDF(\"id\").withColumn(\"age\", lit(\"string\"))\n            .createOrReplaceTempView(\"data\")\n\n          spark.sql(s\"CREATE TABLE merge_source USING delta AS SELECT * FROM data\")\n\n          // Use merge to trigger schema evolution as well (add column age)\n          withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n            spark.sql(\n              s\"\"\"\n                 |MERGE INTO delta.`${log.dataPath}` t\n                 |USING merge_source s\n                 |ON t.id = s.id\n                 |WHEN MATCHED\n                 |  THEN UPDATE SET *\n                 |WHEN NOT MATCHED\n                 |  THEN INSERT *\n                 |\"\"\".stripMargin)\n          }\n        }\n      }\n      val v1 = log.update().version\n\n      def readDf: DataFrame =\n        readStream(schemaLocation = Some(getDefaultSchemaLocation.toString),\n          startingVersion = Some(0))\n\n      // Init schema log\n      testStream(readDf)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        CheckAnswer(Nil: _*),\n        ExpectMetadataEvolutionExceptionFromInitialization\n      )\n      assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == 0L)\n\n      // Streaming CDC until the MERGE invoked schema change\n      testStream(readDf)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailableIgnoreError,\n        // The first 10 inserts\n        CheckAnswer((0L until 10L): _*),\n        ExpectMetadataEvolutionException\n      )\n      assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v1 &&\n        getDefaultSchemaLog().getCurrentTrackedMetadata.get.dataSchema.fieldNames.sameElements(\n          Array(\"id\", \"age\")))\n\n      // Streaming CDC of the MERGE\n      testStream(readDf)(\n        StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n        ProcessAllAvailable(),\n        CheckAnswer(\n          // odd numbers have UPDATE actions (preimage and postimage)\n          (0L until 10L).filter(_ % 2 == 0).flatMap(i => Seq((i, null), (i, \"string\"))): _*\n        )\n      )\n    }\n  }\n\n  testSchemaEvolution(\n    \"protocol and configuration evolution\", columnMapping = false) { implicit log =>\n    // Updates table properties / protocol\n    spark.sql(\n      s\"\"\"\n         |ALTER TABLE delta.`${log.dataPath}`\n         |SET TBLPROPERTIES (\n         |  'delta.minReaderVersion' = 2,\n         |  'delta.minWriterVersion' = 5\n         |)\n         |\"\"\".stripMargin)\n    val v1 = log.update().version\n\n    addData(5 until 10)\n    // Update just delta table property\n    spark.sql(\n      s\"\"\"\n         |ALTER TABLE delta.`${log.dataPath}`\n         |SET TBLPROPERTIES (\n         |  'delta.isolationLevel' = 'SERIALIZABLE'\n         |)\n         |\"\"\".stripMargin\n    )\n    val v2 = log.update().version\n\n    addData(10 until 13)\n    // Update non-delta property won't need stream stop\n    spark.sql(\n      s\"\"\"\n         |ALTER TABLE delta.`${log.dataPath}`\n         |SET TBLPROPERTIES (\n         |  'hello' = 'its me'\n         |)\n         |\"\"\".stripMargin\n    )\n    addData(13 until 15)\n\n    def readDf: DataFrame =\n      readStream(schemaLocation = Some(getDefaultSchemaLocation.toString),\n        startingVersion = Some(1L))\n\n    // Init schema log\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer(Nil: _*),\n      ExpectMetadataEvolutionExceptionFromInitialization\n    )\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == 1L)\n\n    // Reaching the first protocol change\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer((0 until 5).map(_.toString).map(i => (i, i)): _*),\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v1)\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.protocol.contains(Protocol(2, 5)))\n\n    // Reaching the second property change\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailableIgnoreError,\n      CheckAnswer((5 until 10).map(_.toString).map(i => (i, i)): _*),\n      ExpectMetadataEvolutionException\n    )\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.deltaCommitVersion == v2)\n    assert(getDefaultSchemaLog().getCurrentTrackedMetadata.get.tableConfigurations\n      .get.contains(\"delta.isolationLevel\"))\n\n    // The final property change won't stop stream because it's non delta\n    testStream(readDf)(\n      StartStream(checkpointLocation = getDefaultCheckpoint.toString),\n      ProcessAllAvailable(),\n      CheckAnswer((10 until 15).map(_.toString).map(i => (i, i)): _*)\n    )\n  }\n}\n\nclass DeltaSourceSchemaEvolutionCDCNameColumnMappingSuite\n  extends CDCStreamingSchemaEvolutionSuiteBase\n    with DeltaColumnMappingEnableNameMode {\n  override def isCdcTest: Boolean = true\n}\n\nclass DeltaSourceSchemaEvolutionCDCIdColumnMappingSuite\n  extends CDCStreamingSchemaEvolutionSuiteBase\n    with DeltaColumnMappingEnableIdMode {\n  override def isCdcTest: Boolean = true\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, FileInputStream, OutputStream, PrintWriter, StringWriter}\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.util.UUID\nimport java.util.concurrent.TimeoutException\n\nimport scala.concurrent.duration._\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.DeltaTestUtils.modifyCommitTimestamp\nimport org.apache.spark.sql.delta.Relocated\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol}\nimport org.apache.spark.sql.delta.sources.{DeltaDataSource, DeltaSQLConf, DeltaSource, DeltaSourceOffset}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.{MemoryStream, OffsetSeqLog}\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.commons.io.FileUtils\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.hadoop.fs.{FileStatus, Path, RawLocalFileSystem}\nimport org.scalatest.time.{Seconds, Span}\n\nimport org.apache.spark.{SparkConf, SparkThrowable}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, Dataset, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.util.IntervalUtils\nimport org.apache.spark.sql.execution.streaming._\nimport org.apache.spark.sql.functions.when\nimport org.apache.spark.sql.streaming.{OutputMode, StreamingQuery, StreamingQueryException, Trigger}\nimport org.apache.spark.sql.streaming.util.StreamManualClock\nimport org.apache.spark.sql.types.{IntegerType, LongType, NullType, StringType, StructField, StructType}\nimport org.apache.spark.unsafe.types.UTF8String\nimport org.apache.spark.util.{ManualClock, Utils}\n\nclass DeltaSourceSuite extends DeltaSourceSuiteBase\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  def testNullTypeColumn(shouldDropNullTypeColumns: Boolean): Unit = {\n    withTempPaths(3) { case Seq(sourcePath, sinkPath, checkpointPath) =>\n      withSQLConf(\n        DeltaSQLConf.DELTA_STREAMING_CREATE_DATAFRAME_DROP_NULL_COLUMNS.key ->\n          shouldDropNullTypeColumns.toString) {\n\n        spark.sql(\"select CAST(null as VOID) as nullTypeCol, id from range(10)\")\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(sourcePath.getCanonicalPath)\n\n        def runStream() = {\n          loadStreamWithOptions(sourcePath.getCanonicalPath, Map.empty)\n            // Need to drop null type columns because it's not supported by the writer.\n            .drop(\"nullTypeCol\")\n            .writeStream\n            .option(\"checkpointLocation\", checkpointPath.getCanonicalPath)\n            .format(\"delta\")\n            .start(sinkPath.getCanonicalPath)\n            .processAllAvailable()\n        }\n        if (shouldDropNullTypeColumns) {\n          val e = intercept[StreamingQueryException] {\n            runStream()\n          }\n          assert(e.getErrorClass == \"STREAM_FAILED\")\n          // This assertion checks the schema of the source did not change while processing a batch.\n          assert(e.getMessage.contains(\"assertion failed: Invalid batch: nullTypeCol\"))\n        } else {\n          runStream()\n        }\n      }\n    }\n  }\n\n  test(\"streaming delta source should not drop null columns\") {\n    testNullTypeColumn(shouldDropNullTypeColumns = false)\n  }\n\n  test(\"streaming delta source should drop null columns without feature flag\") {\n    testNullTypeColumn(shouldDropNullTypeColumns = true)\n  }\n\n  test(\"no schema should throw an exception\") {\n    withTempDir { inputDir =>\n      new File(inputDir, \"_delta_log\").mkdir()\n      val e = intercept[AnalysisException] {\n        loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n      }\n      for (msg <- Seq(\"Table schema is not set\", \"CREATE TABLE\")) {\n        assert(e.getMessage.contains(msg))\n      }\n    }\n  }\n\n  test(\"disallow user specified schema\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      val e = intercept[AnalysisException] {\n        spark.readStream\n          .schema(StructType.fromDDL(\"a INT, b STRING\"))\n          .format(\"delta\")\n          .load(inputDir.getCanonicalPath)\n      }\n      for (\n        msg <- Seq(\n          \"The schema provided for the source read doesn't match the schema of the Delta table\")\n      ) {\n        assert(e.getMessage.contains(msg))\n      }\n\n      val e2 = intercept[Exception] {\n        spark.readStream\n          .schema(StructType.fromDDL(\"value STRING\"))\n          .format(\"delta\")\n          .load(inputDir.getCanonicalPath)\n      }\n      assert(e2.getMessage.contains(\"does not support user-specified schema\"))\n    }\n  }\n\n  test(\"allow user specified schema if consistent: v1 source\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      import org.apache.spark.sql.execution.datasources.DataSource\n      // User-specified schema is allowed if it's consistent with the actual Delta table schema.\n      // Here we use Spark internal APIs to trigger v1 source code path. That being said, we\n      // are not fixing end-user behavior, but advanced Spark plugins.\n      val v1DataSource = DataSource(\n        spark,\n        userSpecifiedSchema = Some(StructType.fromDDL(\"value STRING\")),\n        className = \"delta\",\n        options = Map(\"path\" -> inputDir.getCanonicalPath))\n      DataFrameUtils.ofRows(spark, Relocated.StreamingRelation(v1DataSource))\n    }\n  }\n\n  test(\"createSource should create source with empty or matching table schema provided\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n\n      sql(s\"CREATE TABLE delta.`$path` (id INT NOT NULL, name STRING) USING delta\")\n\n      val deltaSource = new DeltaDataSource()\n      val parameters = Map(\"path\" -> path)\n      val metadataPath = tempDir.getCanonicalPath + \"/_metadata\"\n\n      val tableSchema = StructType(Seq(\n        StructField(\"id\", IntegerType, false),\n        StructField(\"name\", StringType, true)\n      ))\n      val emptySchema = new StructType()\n      val allowedCreationSchemas = Seq(emptySchema, tableSchema)\n      for (schema <- allowedCreationSchemas) {\n        val source = deltaSource.createSource(\n          sqlContext,\n          metadataPath = metadataPath,\n          schema = Some(schema),\n          providerName = \"delta\",\n          parameters = parameters\n        )\n\n        val actualSchema = source.asInstanceOf[DeltaSource].schema\n        assert(actualSchema.fields.map(_.name).toSet == Set(\"id\", \"name\"))\n      }\n\n      val conflictingSchemas = Seq(\n        StructType(Seq(\n          StructField(\"id\", IntegerType, true)\n          // missing field \"name\"\n        )),\n        StructType(Seq(\n          StructField(\"id\", IntegerType, false),\n          StructField(\"name\", StringType, true),\n          StructField(\"age\", IntegerType, true) // extra field\n        ))\n      )\n\n      for (schema <- conflictingSchemas) {\n        val e = intercept[Exception] {\n          deltaSource.createSource(\n            sqlContext,\n            metadataPath = metadataPath,\n            schema = Some(schema),\n            providerName = \"delta\",\n            parameters = parameters\n          )\n        }\n        assert(e.getMessage.contains(\n          \"[DELTA_READ_SOURCE_SCHEMA_CONFLICT] \" +\n            \"The schema provided for the source read doesn't match the schema of the Delta table\"))\n      }\n    }\n  }\n\n  test(\"basic\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n        .filter($\"value\" contains \"keep\")\n\n      testStream(df)(\n        AddToReservoir(inputDir, Seq(\"keep1\", \"keep2\", \"drop3\").toDF),\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\"keep1\", \"keep2\"),\n        StopStream,\n        AddToReservoir(inputDir, Seq(\"drop4\", \"keep5\", \"keep6\").toDF),\n        StartStream(),\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\"keep1\", \"keep2\", \"keep5\", \"keep6\"),\n        AddToReservoir(inputDir, Seq(\"keep7\", \"drop8\", \"keep9\").toDF),\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\"keep1\", \"keep2\", \"keep5\", \"keep6\", \"keep7\", \"keep9\")\n      )\n    }\n  }\n\n  test(\"initial snapshot ends at base index of next version\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n      // Add data before creating the stream, so that it becomes part of the initial snapshot.\n      Seq(\"keep1\", \"keep2\", \"drop3\").toDF.write\n        .format(\"delta\").mode(\"append\").save(inputDir.getAbsolutePath)\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n        .filter($\"value\" contains \"keep\")\n\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        AssertOnQuery { q =>\n          val offset = q.committedOffsets.iterator.next()._2.asInstanceOf[DeltaSourceOffset]\n          assert(offset.reservoirVersion === 2)\n          assert(offset.index === DeltaSourceOffset.BASE_INDEX)\n          true\n        },\n        CheckAnswer(\"keep1\", \"keep2\"),\n        StopStream\n      )\n    }\n  }\n\n  test(\"allow to change schema before starting a streaming query\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF(\"id\")\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      withMetadata(deltaLog, StructType.fromDDL(\"id STRING, value STRING\"))\n\n      (5 until 10).foreach { i =>\n        val v = Seq(i.toString -> i.toString).toDF(\"id\", \"value\")\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      val expected = (\n          (0 until 5).map(_.toString -> null) ++ (5 until 10).map(_.toString).map(x => x -> x)\n        ).toDF(\"id\", \"value\").collect()\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(expected: _*)\n      )\n    }\n  }\n\n  testQuietly(\"disallow to change schema after starting a streaming query\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer((0 until 5).map(_.toString): _*),\n        AssertOnQuery { _ =>\n          withMetadata(deltaLog, StructType.fromDDL(\"id int, value int\"))\n          true\n        },\n        ExpectFailure[DeltaIllegalStateException](t =>\n          assert(t.getMessage.contains(\"Detected schema change\")))\n      )\n    }\n  }\n\n  test(\"maxFilesPerTrigger\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"))\n        .writeStream\n        .format(\"memory\")\n        .queryName(\"maxFilesPerTriggerTest\")\n        .start()\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 5)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(sql(\"SELECT * from maxFilesPerTriggerTest\"), (0 until 5).map(_.toString).toDF)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"maxFilesPerTrigger: metadata checkpoint\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 20).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"))\n        .writeStream\n        .format(\"memory\")\n        .queryName(\"maxFilesPerTriggerTest\")\n        .start()\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 20)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(sql(\"SELECT * from maxFilesPerTriggerTest\"), (0 until 20).map(_.toString).toDF)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"maxFilesPerTrigger: change and restart\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 10).foreach { i =>\n        val v = Seq(i.toString).toDF()\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"))\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(outputDir.getCanonicalPath)\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 10)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getAbsolutePath),\n          (0 until 10).map(_.toString).toDF())\n      } finally {\n        q.stop()\n      }\n\n      (10 until 20).foreach { i =>\n        val v = Seq(i.toString).toDF()\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q2 = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"2\"))\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(outputDir.getCanonicalPath)\n      try {\n        q2.processAllAvailable()\n        val progress = q2.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 5)\n        progress.foreach { p =>\n          assert(p.numInputRows === 2)\n        }\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getAbsolutePath),\n          (0 until 20).map(_.toString).toDF())\n      } finally {\n        q2.stop()\n      }\n    }\n  }\n\n  testQuietly(\"maxFilesPerTrigger: invalid parameter\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      Seq(0, -1, \"string\").foreach { invalidMaxFilesPerTrigger =>\n        val e = intercept[StreamingQueryException] {\n          loadStreamWithOptions(\n            inputDir.getCanonicalPath,\n            Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> invalidMaxFilesPerTrigger.toString))\n            .writeStream\n            .format(\"console\")\n            .start()\n            .processAllAvailable()\n        }\n        assert(e.getCause.isInstanceOf[IllegalArgumentException])\n        for (msg <- Seq(\"Invalid\", DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION, \"positive\")) {\n          assert(e.getCause.getMessage.contains(msg))\n        }\n      }\n    }\n  }\n\n  test(\"maxFilesPerTrigger: ignored when using Trigger.Once\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      def runTriggerOnceAndVerifyResult(expected: Seq[Int]): Unit = {\n        val q = loadStreamWithOptions(\n          inputDir.getCanonicalPath,\n          Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"))\n          .writeStream\n          .format(\"memory\")\n          .trigger(Trigger.Once)\n          .queryName(\"triggerOnceTest\")\n          .start()\n        try {\n          assert(q.awaitTermination(streamingTimeout.toMillis))\n          assert(q.recentProgress.count(_.numInputRows != 0) == 1) // only one trigger was run\n          checkAnswer(sql(\"SELECT * from triggerOnceTest\"), expected.map(_.toString).toDF)\n        } finally {\n          q.stop()\n        }\n      }\n\n      runTriggerOnceAndVerifyResult(0 until 5)\n\n      // Write more data and start a second batch.\n      (5 until 10).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n      // Verify we can see all of latest data.\n      runTriggerOnceAndVerifyResult(0 until 10)\n    }\n  }\n\n  test(\"maxFilesPerTrigger: Trigger.AvailableNow respects read limits\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, inputDir)\n      // Write versions 0, 1, 2, 3, 4.\n      (0 to 4).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val stream = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"))\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .trigger(Trigger.AvailableNow)\n        .queryName(\"maxFilesPerTriggerTest\")\n\n      var q = stream.start(outputDir.getCanonicalPath)\n      try {\n        assert(q.awaitTermination(streamingTimeout.toMillis))\n        assert(q.recentProgress.length === 5)\n        // The first 5 versions each contain one file. They are processed as part of the initial\n        // snapshot (reservoir version 4) with one index per file.\n        (0 to 3).foreach { i =>\n          val p = q.recentProgress(i)\n          assert(p.numInputRows === 1)\n          val endOffset = JsonUtils.fromJson[DeltaSourceOffset](p.sources.head.endOffset)\n          assert(endOffset == DeltaSourceOffset(\n            endOffset.reservoirId, reservoirVersion = 4, index = i, isInitialSnapshot = true))\n        }\n        // The last batch ends at the base index of the next reservoir version (5).\n        val p4 = q.recentProgress(4)\n        assert(p4.numInputRows === 1)\n        val endOffset = JsonUtils.fromJson[DeltaSourceOffset](p4.sources.head.endOffset)\n        assert(endOffset == DeltaSourceOffset(\n          endOffset.reservoirId,\n          reservoirVersion = 5,\n          index = DeltaSourceOffset.BASE_INDEX,\n          isInitialSnapshot = false))\n\n        checkAnswer(\n          sql(s\"SELECT * from delta.`${outputDir.getCanonicalPath}`\"),\n          (0 to 4).map(_.toString).toDF)\n\n        // Restarting the stream should immediately terminate with no progress because no more data\n        q = stream.start(outputDir.getCanonicalPath)\n        assert(q.awaitTermination(streamingTimeout.toMillis))\n        // The streaming engine always reports one batch, even if it's empty.\n        assert(q.recentProgress.length === 1)\n        assert(q.recentProgress(0).sources.head.startOffset ==\n          q.recentProgress(0).sources.head.endOffset)\n\n        // Write versions 5, 6, 7.\n        (5 to 7).foreach { i =>\n          val v = Seq(i.toString).toDF\n          v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n\n        q = stream.start(outputDir.getCanonicalPath)\n        assert(q.awaitTermination(streamingTimeout.toMillis))\n        // These versions are processed one by one outside the initial snapshot.\n        assert(q.recentProgress.length === 3)\n\n        (5 to 7).foreach { i =>\n          val p = q.recentProgress(i - 5)\n          assert(p.numInputRows === 1)\n          val endOffset = JsonUtils.fromJson[DeltaSourceOffset](p.sources.head.endOffset)\n          assert(endOffset == DeltaSourceOffset(\n            endOffset.reservoirId,\n            reservoirVersion = i + 1,\n            index = DeltaSourceOffset.BASE_INDEX,\n            isInitialSnapshot = false))\n        }\n\n        // Restarting the stream should immediately terminate with no progress because no more data\n        q = stream.start(outputDir.getCanonicalPath)\n        assert(q.awaitTermination(streamingTimeout.toMillis))\n        assert(q.recentProgress.length === 1)\n        assert(q.recentProgress(0).sources.head.startOffset ==\n          q.recentProgress(0).sources.head.endOffset)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"Trigger.AvailableNow with an empty table\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (value STRING) USING delta\")\n\n      val stream = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"))\n        .writeStream\n        .format(\"memory\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .trigger(Trigger.AvailableNow)\n        .queryName(\"emptyTableTriggerAvailableNow\")\n\n      var q = stream.start()\n      try {\n        assert(q.awaitTermination(10000))\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 0)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"maxBytesPerTrigger: process at least one file\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"1b\"))\n        .writeStream\n        .format(\"memory\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .queryName(\"maxBytesPerTriggerTest\")\n        .start()\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 5)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(sql(\"SELECT * from maxBytesPerTriggerTest\"), (0 until 5).map(_.toString).toDF)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"maxBytesPerTrigger: metadata checkpoint\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 20).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"1b\"))\n        .writeStream\n        .format(\"memory\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .queryName(\"maxBytesPerTriggerTest\")\n        .start()\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 20)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(sql(\"SELECT * from maxBytesPerTriggerTest\"), (0 until 20).map(_.toString).toDF)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"maxBytesPerTrigger: change and restart\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 10).foreach { i =>\n        val v = Seq(i.toString).toDF()\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"1b\"))\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(outputDir.getCanonicalPath)\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 10)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getAbsolutePath),\n          (0 until 10).map(_.toString).toDF())\n      } finally {\n        q.stop()\n      }\n\n      (10 until 20).foreach { i =>\n        val v = Seq(i.toString).toDF()\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val q2 = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"100g\"))\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(outputDir.getCanonicalPath)\n      try {\n        q2.processAllAvailable()\n        val progress = q2.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 1)\n        progress.foreach { p =>\n          assert(p.numInputRows === 10)\n        }\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getAbsolutePath),\n          (0 until 20).map(_.toString).toDF())\n      } finally {\n        q2.stop()\n      }\n    }\n  }\n\n  testQuietly(\"maxBytesPerTrigger: invalid parameter\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      Seq(0, -1, \"string\").foreach { invalidMaxBytesPerTrigger =>\n        val e = intercept[StreamingQueryException] {\n          loadStreamWithOptions(\n            inputDir.getCanonicalPath,\n            Map(DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> invalidMaxBytesPerTrigger.toString))\n            .writeStream\n            .format(\"console\")\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .start()\n            .processAllAvailable()\n        }\n        assert(e.getCause.isInstanceOf[IllegalArgumentException])\n        for (msg <- Seq(\"Invalid\", DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION, \"size\")) {\n          assert(e.getCause.getMessage.contains(msg))\n        }\n      }\n    }\n  }\n\n  test(\"maxBytesPerTrigger: Trigger.AvailableNow respects read limits\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, inputDir)\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      val stream = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"1b\"))\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .trigger(Trigger.AvailableNow)\n        .queryName(\"maxBytesPerTriggerTest\")\n\n      var q = stream.start(outputDir.getCanonicalPath)\n      try {\n        assert(q.awaitTermination(streamingTimeout.toMillis))\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 5)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(\n          sql(s\"SELECT * from delta.`${outputDir.getCanonicalPath}`\"),\n          (0 until 5).map(_.toString).toDF)\n\n        // Restarting the stream should immediately terminate with no progress because no more data\n        q = stream.start(outputDir.getCanonicalPath)\n        assert(q.awaitTermination(streamingTimeout.toMillis))\n        assert(q.recentProgress.length === 1)\n        assert(q.recentProgress(0).sources.head.startOffset ==\n          q.recentProgress(0).sources.head.endOffset)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"maxBytesPerTrigger: max bytes and max files together\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 5).foreach { i =>\n        val v = Seq(i.toString).toDF\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      // should process a file at a time due to MAX_FILES_PER_TRIGGER_OPTION\n      val q = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(\n          DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\",\n          DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"100gb\"))\n        .writeStream\n        .format(\"memory\")\n        .queryName(\"maxBytesPerTriggerTest\")\n        .start()\n      try {\n        q.processAllAvailable()\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 5)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(sql(\"SELECT * from maxBytesPerTriggerTest\"), (0 until 5).map(_.toString).toDF)\n      } finally {\n        q.stop()\n      }\n\n      val q2 = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(\n          DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"2\",\n          DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION -> \"1b\"))\n        .writeStream\n        .format(\"memory\")\n        .queryName(\"maxBytesPerTriggerTest\")\n        .start()\n      try {\n        q2.processAllAvailable()\n        val progress = q2.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === 5)\n        progress.foreach { p =>\n          assert(p.numInputRows === 1)\n        }\n        checkAnswer(sql(\"SELECT * from maxBytesPerTriggerTest\"), (0 until 5).map(_.toString).toDF)\n      } finally {\n        q2.stop()\n      }\n    }\n  }\n\n  // DeltaSourceOffset unit tests have been moved to DeltaSourceOffsetSuite.\n\n  testQuietly(\"streaming query should fail when table is deleted and recreated with new id\") {\n    withTempDir { inputDir =>\n      val tablePath = new Path(inputDir.toURI)\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n        .filter($\"value\" contains \"keep\")\n\n      testStream(df)(\n        AddToReservoir(inputDir, Seq(\"keep1\", \"keep2\", \"drop3\").toDF),\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\"keep1\", \"keep2\"),\n        StopStream,\n        AssertOnQuery { _ =>\n          Utils.deleteRecursively(inputDir)\n          // This test deletes and recreates a table at the same path. The InMemoryCommitCoordinator\n          // keys on logPath, so stale coordinator state from the old table must be cleared before\n          // the new table is created. In production, UC handles table lifecycle management, so\n          // explicit coordinator cleanup is not needed.\n          if (catalogOwnedDefaultCreationEnabledInTests) {\n            deleteCatalogOwnedTableFromCommitCoordinator(tablePath)\n          }\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          // All Delta tables in tests use the same tableId by default. Here we pass a new tableId\n          // to simulate a new table creation in production\n          withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"), tableId = Some(\"tableId-1234\"))\n          true\n        },\n        StartStream(),\n        ExpectFailure[DeltaIllegalStateException] { e =>\n          for (msg <- Seq(\"delete\", \"checkpoint\", \"restart\")) {\n            assert(e.getMessage.contains(msg))\n          }\n        }\n      )\n    }\n  }\n\n  test(\"excludeRegex works and doesn't mess up offsets across restarts - parquet version\") {\n    withTempDir { inputDir =>\n      val chk = new File(inputDir, \"_checkpoint\").toString\n\n      def excludeReTest(s: Option[String], expected: String*): Unit = {\n        val options = s.map(regex =>\n          Map(DeltaOptions.EXCLUDE_REGEX_OPTION -> regex)).getOrElse(Map.empty)\n        val df = loadStreamWithOptions(inputDir.getCanonicalPath, options).groupBy('value).count\n        testStream(df, OutputMode.Complete())(\n          StartStream(checkpointLocation = chk),\n          AssertOnQuery { sq => sq.processAllAvailable(); true },\n          CheckLastBatch(expected.map((_, 1)): _*),\n          StopStream\n        )\n      }\n\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n\n      def writeFile(name: String, content: String): AddFile = {\n        FileUtils.write(new File(inputDir, name), content)\n        // CatalogManaged (CCv2) tables auto-enable row tracking, which requires numRecords in\n        // AddFile stats to assign base row IDs. Without numRecords, the commit fails with\n        // DELTA_ROW_ID_ASSIGNMENT_WITHOUT_STATS.\n        AddFile(name, Map.empty, content.length, System.currentTimeMillis(), dataChange = true,\n          stats = s\"\"\"{\"numRecords\": 1}\"\"\")\n      }\n\n      def commitFiles(files: AddFile*): Unit = {\n        deltaLog.startTransaction().commit(files, DeltaOperations.ManualUpdate)\n      }\n\n      Seq(\"abc\", \"def\").toDF().write.format(\"delta\").save(inputDir.getAbsolutePath)\n      commitFiles(\n        writeFile(\"batch1-ignore-file1\", \"ghi\"),\n        writeFile(\"batch1-ignore-file2\", \"jkl\")\n      )\n      excludeReTest(Some(\"ignore\"), \"abc\", \"def\")\n    }\n  }\n\n  testQuietly(\"excludeRegex throws good error on bad regex pattern\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      val e = intercept[StreamingQueryException] {\n        loadStreamWithOptions(\n          inputDir.getCanonicalPath,\n          Map(DeltaOptions.EXCLUDE_REGEX_OPTION -> \"[abc\"))\n          .writeStream\n          .format(\"console\")\n          .start()\n          .awaitTermination()\n      }.cause\n      assert(e.isInstanceOf[IllegalArgumentException])\n      assert(e.getMessage.contains(DeltaOptions.EXCLUDE_REGEX_OPTION))\n    }\n  }\n\n  test(\"a fast writer should not starve a Delta source\") {\n    val deltaPath = Utils.createTempDir().getCanonicalPath\n    val checkpointPath = Utils.createTempDir().getCanonicalPath\n    val writer = spark.readStream\n      .format(\"rate\")\n      .load()\n      .writeStream\n      .format(\"delta\")\n      .option(\"checkpointLocation\", checkpointPath)\n      .start(deltaPath)\n    try {\n      eventually(timeout(streamingTimeout)) {\n        assert(spark.read.format(\"delta\").load(deltaPath).count() > 0)\n      }\n      val testTableName = \"delta_source_test\"\n      withTable(testTableName) {\n        val reader = loadStreamWithOptions(deltaPath, Map.empty)\n          .writeStream\n          .format(\"memory\")\n          .queryName(testTableName)\n          .start()\n        try {\n          eventually(timeout(streamingTimeout)) {\n            assert(spark.table(testTableName).count() > 0)\n          }\n        } finally {\n          reader.stop()\n        }\n      }\n    } finally {\n      writer.stop()\n    }\n  }\n\n  test(\"start from corrupt checkpoint\") {\n    withTempDir { inputDir =>\n      val path = inputDir.getAbsolutePath\n      for (i <- 1 to 5) {\n        Seq(i).toDF(\"id\").write.mode(\"append\").format(\"delta\").save(path)\n      }\n      val deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n      Seq(6).toDF(\"id\").write.mode(\"append\").format(\"delta\").save(path)\n      val checkpoints = new File(deltaLog.logPath.toUri).listFiles()\n        .filter(f => FileNames.isCheckpointFile(new Path(f.getAbsolutePath)))\n      checkpoints.last.delete()\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(1, 2, 3, 4, 5, 6),\n        StopStream\n      )\n    }\n  }\n\n  test(\"SC-11561: can consume new data without update\") {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      // clear the cache so that the writer creates its own DeltaLog instead of reusing the reader's\n      DeltaLog.clearCache()\n      (0 until 3).foreach { i =>\n        Seq(i.toString).toDF(\"value\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      // check that reader consumed new data without updating its DeltaLog\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\"0\", \"1\", \"2\")\n      )\n      assert(deltaLog.snapshot.version == 0)\n\n      (3 until 5).foreach { i =>\n        Seq(i.toString).toDF(\"value\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      // check that reader consumed new data without update despite checkpoint\n      val writersLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      writersLog.checkpoint()\n      testStream(df)(\n        AssertOnQuery { q => q.processAllAvailable(); true },\n        CheckAnswer(\"0\", \"1\", \"2\", \"3\", \"4\")\n      )\n      assert(deltaLog.snapshot.version == 0)\n    }\n  }\n\n  test(\"startingVersion specific version: new commits arrive after stream initialization\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // Add version 0 and version 1\n      Seq(1, 2, 3).toDF(\"value\").write.format(\"delta\").save(inputDir.getCanonicalPath)\n      Seq(4, 5, 6).toDF(\"value\").write\n        .format(\"delta\").mode(\"append\").save(inputDir.getCanonicalPath)\n\n      // Start streaming from version 1\n      val df = loadStreamWithOptions(\n        inputDir.getCanonicalPath,\n        Map(\n          \"startingVersion\" -> \"1\",\n          DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION -> \"1\"\n        )\n      )\n\n      val q = df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(outputDir.getCanonicalPath)\n\n      try {\n        // Process version 1 only\n        q.processAllAvailable()\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n          Seq(4, 5, 6).toDF(\"value\"))\n\n        // Add version 2 and version 3 (after snapshotAtSourceInit was captured)\n        Seq(7, 8, 9).toDF(\"value\").write\n          .format(\"delta\").mode(\"append\").save(inputDir.getCanonicalPath)\n        Seq(10, 11, 12).toDF(\"value\").write\n          .format(\"delta\").mode(\"append\").save(inputDir.getCanonicalPath)\n\n        q.processAllAvailable()\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n          (4 to 12).toDF(\"value\"))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\n      \"can delete old files of a snapshot without update\"\n  ) {\n    withTempDir { inputDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      withMetadata(deltaLog, StructType.fromDDL(\"value STRING\"))\n\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      // clear the cache so that the writer creates its own DeltaLog instead of reusing the reader's\n      DeltaLog.clearCache()\n      val clock = new ManualClock(System.currentTimeMillis())\n      val writersLog = DeltaLog.forTable(spark, new Path(inputDir.toURI), clock)\n      (0 until 3).foreach { i =>\n        Seq(i.toString).toDF(\"value\")\n          .write.mode(\"append\").format(\"delta\").save(inputDir.getCanonicalPath)\n      }\n\n      // Create a checkpoint so that logs before checkpoint can be expired and deleted\n      writersLog.checkpoint()\n      // This isn't stable, but it shouldn't change during the test.\n      val tahoeId = deltaLog.unsafeVolatileTableId\n\n      testStream(df)(\n        StartStream(Trigger.ProcessingTime(\"10 seconds\"), new StreamManualClock),\n        AdvanceManualClock(10 * 1000L),\n        CheckLastBatch(\"0\", \"1\", \"2\"),\n        Assert {\n          val defaultLogRetentionMillis = DeltaConfigs.getMilliSeconds(\n            IntervalUtils.safeStringToInterval(\n              UTF8String.fromString(DeltaConfigs.LOG_RETENTION.defaultValue)))\n          clock.advance(defaultLogRetentionMillis + 100000000L)\n\n          // Delete all logs before checkpoint\n          writersLog.cleanUpExpiredLogs(writersLog.snapshot)\n\n          // Check that the first few log files have been deleted\n          val logPath = new File(inputDir, \"_delta_log\")\n          val logVersions = logPath.listFiles().map(_.getName)\n              .filter(_.endsWith(\".json\"))\n              .map(_.stripSuffix(\".json\").toInt)\n\n          !logVersions.contains(0) && !logVersions.contains(1)\n        },\n        Assert {\n          (3 until 5).foreach { i =>\n            Seq(i.toString).toDF(\"value\")\n              .write.mode(\"append\").format(\"delta\").save(inputDir.getCanonicalPath)\n          }\n          true\n        },\n        // can process new data without update, despite that previous log files have been deleted\n        AdvanceManualClock(10 * 1000L),\n        AdvanceManualClock(10 * 1000L),\n        CheckNewAnswer(\"3\", \"4\")\n      )\n      assert(deltaLog.snapshot.version == 0)\n    }\n  }\n\n  test(\"Delta sources don't write offsets with null json\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      Seq(1, 2, 3).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n\n      val df = loadStreamWithOptions(inputDir.toString, Map.empty)\n      val stream = df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .start(outputDir.toString)\n      stream.processAllAvailable()\n      val offsetFile = checkpointDir.toString + \"/offsets/0\"\n\n      // Make sure JsonUtils doesn't serialize it as null\n      val deltaSourceOffsetLine =\n        scala.io.Source.fromFile(offsetFile).getLines.toSeq.last\n      val deltaSourceOffset = JsonUtils.fromJson[DeltaSourceOffset](deltaSourceOffsetLine)\n      assert(deltaSourceOffset.json != null, \"Delta sources shouldn't write null json field\")\n\n      // Make sure OffsetSeqLog won't choke on the offset we wrote\n      withTempDir { logPath =>\n        new OffsetSeqLog(spark, logPath.toString) {\n          val offsetSeq = this.deserialize(new FileInputStream(offsetFile))\n          val out = new OutputStream() { override def write(b: Int): Unit = { } }\n          this.serialize(offsetSeq, out)\n        }\n      }\n\n      stream.stop()\n    }\n  }\n\n  test(\"Delta source advances with non-data inserts and generates empty dataframe for \" +\n    \"non-data operations\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // Version 0\n      Seq(1L, 2L, 3L).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n\n      val df = loadStreamWithOptions(inputDir.toString, Map.empty)\n\n      val stream = df\n        .writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .foreachBatch(\n          (outputDf: DataFrame, bid: Long) => {\n              // Apart from first batch, rest of batches work with non-data operations\n              // for which we expect an empty dataframe to be generated.\n              if (bid > 0) {\n                assert(outputDf.isEmpty)\n              }\n              outputDf\n                .write\n                .format(\"delta\")\n                .mode(\"append\")\n                .save(outputDir.toString)\n            }\n        )\n        .start()\n\n      val deltaLog = DeltaLog.forTable(spark, inputDir.toString)\n      def expectLatestOffset(offset: DeltaSourceOffset) {\n          val lastOffset = DeltaSourceOffset(\n            deltaLog.unsafeVolatileTableId,\n            SerializedOffset(stream.lastProgress.sources.head.endOffset)\n          )\n\n          assert(lastOffset == offset)\n      }\n\n      try {\n        stream.processAllAvailable()\n        expectLatestOffset(DeltaSourceOffset(\n          deltaLog.unsafeVolatileTableId,\n          reservoirVersion = 1,\n          DeltaSourceOffset.BASE_INDEX,\n          isInitialSnapshot = false))\n\n        deltaLog.startTransaction().commit(Seq(), DeltaOperations.ManualUpdate)\n        stream.processAllAvailable()\n        expectLatestOffset(DeltaSourceOffset(\n          deltaLog.unsafeVolatileTableId,\n          reservoirVersion = 2,\n          DeltaSourceOffset.BASE_INDEX,\n          isInitialSnapshot = false))\n\n        deltaLog.startTransaction().commit(Seq(), DeltaOperations.ManualUpdate)\n        stream.processAllAvailable()\n        expectLatestOffset(DeltaSourceOffset(\n          deltaLog.unsafeVolatileTableId,\n          reservoirVersion = 3,\n          DeltaSourceOffset.BASE_INDEX,\n          isInitialSnapshot = false))\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  test(\"Rate limited Delta source advances with non-data inserts\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // Version 0\n      Seq(1L, 2L, 3L).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n\n      val df = loadStreamWithOptions(inputDir.toString, Map.empty)\n      val stream = df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .option(\"maxFilesPerTrigger\", 2)\n        .start(outputDir.toString)\n\n      try {\n        val deltaLog = DeltaLog.forTable(spark, inputDir.toString)\n        def waitForOffset(offset: DeltaSourceOffset) {\n          eventually(timeout(streamingTimeout)) {\n            val lastOffset = DeltaSourceOffset(\n              deltaLog.unsafeVolatileTableId,\n              SerializedOffset(stream.lastProgress.sources.head.endOffset)\n            )\n\n            assert(lastOffset == offset)\n          }\n        }\n\n        // Process the initial snapshot (version 0) and end up at the start of version 1 which\n        // does not exist yet.\n        stream.processAllAvailable()\n        waitForOffset(DeltaSourceOffset(\n          deltaLog.unsafeVolatileTableId, 1, DeltaSourceOffset.BASE_INDEX, false))\n\n        // Add Versions 1, 2, 3, and 4\n        for(i <- 1 to 4) {\n          deltaLog.startTransaction().commit(Seq(), DeltaOperations.ManualUpdate)\n        }\n\n        // The manual commits don't have any files in them, but they do have indexes: BASE_INDEX\n        // and END_INDEX. Neither of those indexes are counted for rate limiting. We end up at\n        // v4[END_INDEX] which is then rounded up to v5[BASE_INDEX] even though v5 does not exist\n        // yet.\n        stream.processAllAvailable()\n        waitForOffset(\n          DeltaSourceOffset(deltaLog.unsafeVolatileTableId, 5, DeltaSourceOffset.BASE_INDEX, false))\n\n        // Add Version 5\n        deltaLog.startTransaction().commit(Seq(), DeltaOperations.ManualUpdate)\n\n        // The stream progresses to v5[END_INDEX] which is rounded up to v6[BASE_INDEX]. (In prior\n        // versions of the code we did not have END_INDEX. In that case the stream would not have\n        // moved forward from v5, because there were no indexes after v5[BASE_INDEX].\n        stream.processAllAvailable()\n        waitForOffset(\n          DeltaSourceOffset(deltaLog.unsafeVolatileTableId, 6, DeltaSourceOffset.BASE_INDEX, false))\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  testQuietly(\"Delta sources should verify the protocol reader version\") {\n    withTempDir { tempDir =>\n      spark.range(0).write.format(\"delta\").save(tempDir.getCanonicalPath)\n\n      val df = loadStreamWithOptions(tempDir.getCanonicalPath, Map.empty)\n      val stream = df.writeStream\n        .format(\"console\")\n        .start()\n      try {\n        stream.processAllAvailable()\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        deltaLog.store.write(\n          FileNames.unsafeDeltaFile(deltaLog.logPath, deltaLog.snapshot.version + 1),\n          // Write a large reader version to fail the streaming query\n          Iterator(Protocol(minReaderVersion = Int.MaxValue).json),\n          overwrite = false,\n          deltaLog.newDeltaHadoopConf())\n\n        // The streaming query should fail because its version is too old\n        val e = intercept[StreamingQueryException] {\n          stream.processAllAvailable()\n        }\n        val cause = e.getCause\n        val sw = new StringWriter()\n        cause.printStackTrace(new PrintWriter(sw))\n        assert(\n          cause.isInstanceOf[InvalidProtocolVersionException] ||\n          // When coordinated commits are enabled, the following assertion error coming from\n          // CoordinatedCommitsUtils.getCommitCoordinatorClient may get hit\n          (cause.isInstanceOf[AssertionError] &&\n           e.getCause.getMessage.contains(\"coordinated commits table feature is not supported\")),\n          s\"Caused by: ${sw.toString}\")\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  /** Generate commits with the given timestamp in millis. */\n  private def generateCommits(location: String, commits: Long*): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, location)\n    var startVersion = deltaLog.snapshot.version + 1\n    commits.foreach { ts =>\n      val rangeStart = startVersion * 10\n      val rangeEnd = rangeStart + 10\n      spark.range(rangeStart, rangeEnd).write.format(\"delta\").mode(\"append\").save(location)\n      modifyCommitTimestamp(deltaLog, startVersion, ts)\n      startVersion += 1\n    }\n  }\n\n  private implicit def durationToLong(duration: FiniteDuration): Long = {\n    duration.toMillis\n  }\n\n  /** Disable log cleanup to avoid deleting logs we are testing. */\n  protected def disableLogCleanup(tablePath: String): Unit = {\n    sql(s\"alter table delta.`$tablePath` \" +\n      s\"set tblproperties (${DeltaConfigs.ENABLE_EXPIRED_LOG_CLEANUP.key} = false)\")\n  }\n\n  testQuietly(\"startingVersion\") {\n    withTempDir { tableDir =>\n      val tablePath = tableDir.getCanonicalPath\n      val start = 1594795800000L\n      generateCommits(tablePath, start, start + 20.minutes)\n\n      def testStartingVersion(startingVersion: Long): Unit = {\n        val df = loadStreamWithOptions(\n          tablePath, Map(\"startingVersion\" -> startingVersion.toString))\n        val q = df.writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersion_test\")\n          .start()\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n\n      for ((startingVersion, expected) <- Seq(\n        0 -> (0 until 20),\n        1 -> (10 until 20))\n      ) {\n        withTempView(\"startingVersion_test\") {\n          testStartingVersion(startingVersion)\n          checkAnswer(\n            spark.table(\"startingVersion_test\"),\n            expected.map(_.toLong).toDF())\n        }\n      }\n\n      assert(intercept[StreamingQueryException] {\n        testStartingVersion(-1)\n      }.getMessage.contains(\"Invalid value '-1' for option 'startingVersion'\"))\n      assert(intercept[StreamingQueryException] {\n        testStartingVersion(2)\n      }.getMessage.contains(\"Cannot time travel Delta table to version 2\"))\n\n      // Create a checkpoint at version 2 and delete version 0\n      disableLogCleanup(tablePath)\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      assert(deltaLog.update().version == 2)\n      deltaLog.checkpoint()\n      new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0).toUri).delete()\n\n      // Cannot start from version 0\n      assert(intercept[StreamingQueryException] {\n        testStartingVersion(0)\n      }.getMessage.contains(\"Cannot time travel Delta table to version 0\"))\n\n      // Can start from version 1 even if it's not recreatable\n      // TODO: currently we would error out if we couldn't construct the snapshot to check column\n      //  mapping enable tables. Unblock this once we roll out the proper semantics.\n      withStreamingReadOnColumnMappingTableEnabled {\n        withTempView(\"startingVersion_test\") {\n          testStartingVersion(1L)\n          checkAnswer(\n            spark.table(\"startingVersion_test\"),\n            (10 until 20).map(_.toLong).toDF())\n        }\n      }\n    }\n  }\n\n  // Row tracking forces actions to appear after AddFiles within commits. This will verify that\n  // we correctly skip processed commits, even when an AddFile is not the last action within a\n  // commit.\n  Seq(true, false).foreach { withRowTracking =>\n    testQuietly(s\"startingVersion should be ignored when restarting from a checkpoint, \" +\n      s\"withRowTracking = $withRowTracking\") {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        val start = 1594795800000L\n        withSQLConf(\n          DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> withRowTracking.toString) {\n          generateCommits(inputDir.getCanonicalPath, start, start + 20.minutes)\n        }\n\n        def testStartingVersion(\n            startingVersion: Long,\n            checkpointLocation: String = checkpointDir.getCanonicalPath): Unit = {\n          val q = loadStreamWithOptions(\n            inputDir.getCanonicalPath,\n            Map(\"startingVersion\" -> startingVersion.toString))\n            .writeStream\n            .format(\"delta\")\n            .option(\"checkpointLocation\", checkpointLocation)\n            .start(outputDir.getCanonicalPath)\n          try {\n            q.processAllAvailable()\n          } finally {\n            q.stop()\n          }\n        }\n\n        testStartingVersion(1L)\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n          (10 until 20).map(_.toLong).toDF())\n\n        // Add two new commits\n        generateCommits(inputDir.getCanonicalPath, start + 40.minutes)\n        disableLogCleanup(inputDir.getCanonicalPath)\n        val deltaLog = DeltaLog.forTable(spark, inputDir.getCanonicalPath)\n        assert(deltaLog.update().version == 3)\n        deltaLog.checkpoint()\n\n        // Make the streaming query move forward. When we restart here, we still need to touch\n        // `DeltaSource.getStartingVersion` because the engine will call `getBatch`\n        // that was committed (start is None) during the restart.\n        testStartingVersion(1L)\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n          (10 until 30).map(_.toLong).toDF())\n\n        // Add one commit and delete version 0 and version 1\n        generateCommits(inputDir.getCanonicalPath, start + 60.minutes)\n        (0 to 1).foreach { v =>\n          new File(FileNames.unsafeDeltaFile(deltaLog.logPath, v).toUri).delete()\n        }\n\n        // Although version 1 has been deleted, restarting the query should still work as we have\n        // processed files in version 1.\n        // In other words, query restart should ignore \"startingVersion\"\n        // TODO: currently we would error out if we couldn't construct the snapshot to check column\n        //  mapping enable tables. Unblock this once we roll out the proper semantics.\n        withStreamingReadOnColumnMappingTableEnabled {\n          testStartingVersion(1L)\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n            // the gap caused by \"alter table\"\n            ((10 until 30) ++ (40 until 50)).map(_.toLong).toDF())\n\n          // But if we start a new query, it should fail.\n          val newCheckpointDir = Utils.createTempDir()\n          try {\n            assert(intercept[StreamingQueryException] {\n              testStartingVersion(1L, newCheckpointDir.getCanonicalPath)\n            }.getMessage.contains(\"[2, 4]\"))\n          } finally {\n            Utils.deleteRecursively(newCheckpointDir)\n          }\n        }\n      }\n    }\n  }\n\n  testQuietly(\"startingTimestamp\") {\n    withTempDir { tableDir =>\n      val tablePath = tableDir.getCanonicalPath\n      val start = 1594795800000L // 2020-07-14 23:50:00 PDT\n      generateCommits(tablePath, start, start + 20.minutes)\n\n      def testStartingTimestamp(startingTimestamp: String): Unit = {\n        val q = loadStreamWithOptions(\n          tablePath,\n          Map(\"startingTimestamp\" -> startingTimestamp))\n          .writeStream\n          .format(\"memory\")\n          .queryName(\"startingTimestamp_test\")\n          .start()\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n\n      for ((startingTimestamp, expected) <- Seq(\n        \"2020-07-14\" -> (0 until 20),\n        \"2020-07-14 23:40:00\" -> (0 until 20),\n        \"2020-07-14 23:50:00\" -> (0 until 20), // the timestamp of version 0\n        \"2020-07-14 23:50:01\" -> (10 until 20),\n        \"2020-07-15\" -> (10 until 20),\n        \"2020-07-15 00:00:00\" -> (10 until 20),\n        \"2020-07-15 00:10:00\" -> (10 until 20)) // the timestamp of version 1\n      ) {\n        withTempView(\"startingTimestamp_test\") {\n          testStartingTimestamp(startingTimestamp)\n          checkAnswer(\n            spark.table(\"startingTimestamp_test\"),\n            expected.map(_.toLong).toDF())\n        }\n      }\n      assert(intercept[StreamingQueryException] {\n        testStartingTimestamp(\"2020-07-15 00:10:01\")\n      }.getMessage.contains(\"The provided timestamp (2020-07-15 00:10:01.0) \" +\n        \"is after the latest version\"))\n      assert(intercept[StreamingQueryException] {\n        testStartingTimestamp(\"2020-07-16\")\n      }.getMessage.contains(\"The provided timestamp (2020-07-16 00:00:00.0) \" +\n        \"is after the latest version\"))\n      assert(intercept[StreamingQueryException] {\n        testStartingTimestamp(\"i am not a timestamp\")\n      }.getMessage.contains(\"The provided timestamp ('i am not a timestamp') \" +\n        \"cannot be converted to a valid timestamp\"))\n\n      // With non-strict parsing this produces null when casted to a timestamp and then parses\n      // to 1970-01-01 (unix time 0).\n      withSQLConf(DeltaSQLConf.DELTA_TIME_TRAVEL_STRICT_TIMESTAMP_PARSING.key -> \"false\") {\n        withTempView(\"startingTimestamp_test\") {\n          testStartingTimestamp(\"i am not a timestamp\")\n          checkAnswer(\n            spark.table(\"startingTimestamp_test\"),\n            (0L until 20L).toDF())\n        }\n      }\n\n      // Create a checkpoint at version 2 and delete version 0\n      disableLogCleanup(tablePath)\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      assert(deltaLog.update().version == 2)\n      deltaLog.checkpoint()\n      new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0).toUri).delete()\n\n      // Can start from version 1 even if it's not recreatable\n      // TODO: currently we would error out if we couldn't construct the snapshot to check column\n      //  mapping enable tables. Unblock this once we roll out the proper semantics.\n      withStreamingReadOnColumnMappingTableEnabled {\n        withTempView(\"startingTimestamp_test\") {\n          testStartingTimestamp(\"2020-07-14\")\n          checkAnswer(\n            spark.table(\"startingTimestamp_test\"),\n            (10 until 20).map(_.toLong).toDF())\n        }\n      }\n    }\n  }\n\n  testQuietly(\"startingVersion and startingTimestamp are both set\") {\n    withTempDir { tableDir =>\n      val tablePath = tableDir.getCanonicalPath\n      generateCommits(tablePath, 0)\n      val q = loadStreamWithOptions(\n        tablePath,\n        Map(\"startingVersion\" -> \"0\", \"startingTimestamp\" -> \"2020-07-15\"))\n        .writeStream\n        .format(\"console\")\n        .start()\n      try {\n        assert(intercept[StreamingQueryException] {\n          q.processAllAvailable()\n        }.getMessage.contains(\"Please either provide 'startingVersion' or 'startingTimestamp'\"))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"startingVersion: user defined start works with mergeSchema\") {\n    withTempDir { inputDir =>\n      withTempView(\"startingVersionTest\") {\n        spark.range(10)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(inputDir.getCanonicalPath)\n\n        // Change schema at version 1\n        spark.range(10, 20)\n          .withColumn(\"id2\", 'id)\n          .write\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(inputDir.getCanonicalPath)\n\n        // Change schema at version 2\n        spark.range(20, 30)\n          .withColumn(\"id2\", 'id)\n          .withColumn(\"id3\", 'id)\n          .write\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(inputDir.getCanonicalPath)\n\n        // check answer from version 1\n        val q = loadStreamWithOptions(\n          inputDir.getCanonicalPath,\n          Map(\"startingVersion\" -> \"1\")\n        ).writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersionTest\")\n          .start()\n        try {\n          q.processAllAvailable()\n          checkAnswer(\n            sql(\"select * from startingVersionTest\"),\n            ((10 until 20).map(x => (x.toLong, x.toLong, \"null\")) ++\n              (20 until 30).map(x => (x.toLong, x.toLong, x.toString)))\n              .toDF(\"id\", \"id2\", \"id3\")\n              .selectExpr(\n                \"id\",\n                \"id2\",\n                \"CASE WHEN id3 = 'null' THEN NULL ELSE cast(id3 as long) END as id3\")\n          )\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"startingVersion latest\") {\n    withTempDir { dir =>\n      withTempView(\"startingVersionTest\") {\n        val path = dir.getAbsolutePath\n        spark.range(0, 10).write.format(\"delta\").save(path)\n        val q = loadStreamWithOptions(path, Map(\"startingVersion\" -> \"latest\"))\n          .writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersionLatest\")\n          .start()\n        try {\n          // Starting from latest shouldn't include any data at first, even the most recent version.\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), Seq.empty)\n\n          // After we add some batches the stream should continue as normal.\n          spark.range(10, 15).write.format(\"delta\").mode(\"append\").save(path)\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), (10 until 15).map(Row(_)))\n          spark.range(15, 20).write.format(\"delta\").mode(\"append\").save(path)\n          spark.range(20, 25).write.format(\"delta\").mode(\"append\").save(path)\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), (10 until 25).map(Row(_)))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"startingVersion latest defined before started\") {\n    withTempDir { dir =>\n      withTempView(\"startingVersionTest\") {\n        val path = dir.getAbsolutePath\n        spark.range(0, 10).write.format(\"delta\").save(path)\n        // Define the stream, but don't start it, before a second write. The startingVersion\n        // latest should be resolved when the query *starts*, so there'll be no data even though\n        // some was added after the stream was defined.\n        val streamDef = loadStreamWithOptions(path, Map(\"startingVersion\" -> \"latest\"))\n          .writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersionLatest\")\n        spark.range(10, 20).write.format(\"delta\").mode(\"append\").save(path)\n        val q = streamDef.start()\n\n        try {\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), Seq.empty)\n          spark.range(20, 25).write.format(\"delta\").mode(\"append\").save(path)\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), (20 until 25).map(Row(_)))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"startingVersion latest works on defined but empty table\") {\n    withTempDir { dir =>\n      withTempView(\"startingVersionTest\") {\n        val path = dir.getAbsolutePath\n        spark.range(0).write.format(\"delta\").save(path)\n        val streamDef = loadStreamWithOptions(path, Map(\"startingVersion\" -> \"latest\"))\n          .writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersionLatest\")\n        val q = streamDef.start()\n\n        try {\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), Seq.empty)\n          spark.range(0, 5).write.format(\"delta\").mode(\"append\").save(path)\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), (0 until 5).map(Row(_)))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"startingVersion latest calls update when starting\") {\n    withTempDir { dir =>\n      withTempView(\"startingVersionTest\") {\n        val path = dir.getAbsolutePath\n        spark.range(0).write.format(\"delta\").save(path)\n\n        val streamDef = loadStreamWithOptions(\n          path,\n          Map(\"startingVersion\" -> \"latest\")\n        ).writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersionLatest\")\n        val log = DeltaLog.forTable(spark, path)\n        val originalSnapshot = log.snapshot\n        val timestamp = System.currentTimeMillis()\n\n        // We write out some new data, and then do a dirty reflection hack to produce an un-updated\n        // Delta log. The stream should still update when started and not produce any data.\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n        // The field is actually declared in the SnapshotManagement trait, but because traits don't\n        // exist in the JVM DeltaLog is where it ends up in reflection.\n        val snapshotField = classOf[DeltaLog].getDeclaredField(\"currentSnapshot\")\n        snapshotField.setAccessible(true)\n        snapshotField.set(log, CapturedSnapshot(originalSnapshot, timestamp))\n\n        val q = streamDef.start()\n\n        try {\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionLatest\"), Seq.empty)\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"startingVersion should work with rate time\") {\n    withTempDir { dir =>\n      withTempView(\"startingVersionWithRateLimit\") {\n        val path = dir.getAbsolutePath\n        // Create version 0 and version 1 and each version has two files\n        spark.range(0, 5).repartition(2).write.mode(\"append\").format(\"delta\").save(path)\n        spark.range(5, 10).repartition(2).write.mode(\"append\").format(\"delta\").save(path)\n\n        val q = loadStreamWithOptions(\n          path,\n          Map(\n            \"startingVersion\" -> \"1\",\n            \"maxFilesPerTrigger\" -> \"1\"\n          )\n        ).writeStream\n          .format(\"memory\")\n          .queryName(\"startingVersionWithRateLimit\")\n          .start()\n        try {\n          q.processAllAvailable()\n          checkAnswer(sql(\"select * from startingVersionWithRateLimit\"), (5 until 10).map(Row(_)))\n          val id = DeltaLog.forTable(spark, path).snapshot.metadata.id\n          val endOffsets = q.recentProgress\n            .map(_.sources(0).endOffset)\n            .map(offsetJson => DeltaSourceOffset(\n              id,\n              SerializedOffset(offsetJson)\n            ))\n          assert(endOffsets.toList ==\n            DeltaSourceOffset(id, 1, 0, isInitialSnapshot = false)\n              // When we reach the end of version 1, we will jump to version 2 with index -1\n              :: DeltaSourceOffset(id, 2, DeltaSourceOffset.BASE_INDEX, isInitialSnapshot = false)\n              :: Nil)\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  testQuietly(\"deltaSourceIgnoreChangesError contains removeFile, version, tablePath\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      Seq(1, 2, 3).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n      val df = loadStreamWithOptions(inputDir.toString, Map.empty)\n      df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .start(outputDir.toString)\n        .processAllAvailable()\n\n      // Overwrite values, causing AddFile & RemoveFile actions to be triggered\n      Seq(1, 2, 3).toDF(\"x\")\n        .write\n        .mode(\"overwrite\")\n        .format(\"delta\")\n        .save(inputDir.toString)\n\n      val e = intercept[StreamingQueryException] {\n        val q = df.writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", checkpointDir.toString)\n          // DeltaOptions.IGNORE_CHANGES_OPTION is false by default\n          .start(outputDir.toString)\n\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n\n      assert(e.getCause.isInstanceOf[UnsupportedOperationException])\n      assert(e.getCause.getMessage.contains(\n        \"This is currently not supported. If this is going to happen regularly and you are okay\" +\n          \" to skip changes, set the option 'skipChangeCommits' to 'true'.\"\n      ))\n      assert(e.getCause.getMessage.contains(\"for example\"))\n      assert(e.getCause.getMessage.contains(\"version\"))\n      assert(e.getCause.getMessage.matches(s\".*$inputDir.*\"))\n    }\n  }\n\n  testQuietly(\"deltaSourceIgnoreDeleteError contains removeFile, version, tablePath\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      Seq(1, 2, 3).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n      val df = loadStreamWithOptions(inputDir.toString, Map.empty)\n      df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .start(outputDir.toString)\n        .processAllAvailable()\n\n      // Delete the table, causing only RemoveFile (not AddFile) actions to be triggered\n      io.delta.tables.DeltaTable.forPath(spark, inputDir.getAbsolutePath).delete()\n\n      val e = intercept[StreamingQueryException] {\n        val q = df.writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", checkpointDir.toString)\n          // DeltaOptions.IGNORE_DELETES_OPTION is false by default\n          .start(outputDir.toString)\n\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n\n      assert(e.getCause.isInstanceOf[UnsupportedOperationException])\n      assert(e.getCause.getMessage.contains(\n        \"This is currently not supported. If you'd like to ignore deletes, set the option \" +\n          \"'ignoreDeletes' to 'true'.\"))\n      assert(e.getCause.getMessage.contains(\"for example\"))\n      assert(e.getCause.getMessage.contains(\"version\"))\n      assert(e.getCause.getMessage.matches(s\".*$inputDir.*\"))\n    }\n  }\n\n  test(\"streaming with ignoreDeletes = true skips delete-only commits\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // Write initial data\n      Seq(1, 2, 3).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n\n      val df = loadStreamWithOptions(\n        inputDir.toString, Map(DeltaOptions.IGNORE_DELETES_OPTION -> \"true\",\n          \"startingVersion\" -> \"0\"))\n\n      val q = df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .start(outputDir.toString)\n      try {\n        q.processAllAvailable()\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.toString),\n          Seq(1, 2, 3).map(Row(_)))\n\n        // Delete all rows: produces only RemoveFile actions\n        io.delta.tables.DeltaTable.forPath(spark, inputDir.getAbsolutePath).delete()\n\n        // Append new data after the delete\n        Seq(4, 5).toDF(\"x\").write.format(\"delta\").mode(\"append\").save(inputDir.toString)\n\n        q.processAllAvailable()\n\n        // The delete commit should be silently skipped; only inserts are processed\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.toString),\n          Seq(1, 2, 3, 4, 5).map(Row(_)))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  testQuietly(\"streaming with ignoreDeletes = true still fails on change commits\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      Seq(1, 2, 3).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n\n      val df = loadStreamWithOptions(\n        inputDir.toString, Map(DeltaOptions.IGNORE_DELETES_OPTION -> \"true\"))\n      df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .start(outputDir.toString)\n        .processAllAvailable()\n\n      // Overwrite produces both AddFile and RemoveFile actions (a change commit)\n      Seq(4, 5, 6).toDF(\"x\")\n        .write\n        .mode(\"overwrite\")\n        .format(\"delta\")\n        .save(inputDir.toString)\n\n      val e = intercept[StreamingQueryException] {\n        val q = df.writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", checkpointDir.toString)\n          .start(outputDir.toString)\n\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n\n      assert(e.getCause.isInstanceOf[UnsupportedOperationException])\n      assert(e.getCause.getMessage.contains(\n        \"This is currently not supported. If this is going to happen regularly and you are okay\" +\n          \" to skip changes, set the option 'skipChangeCommits' to 'true'.\"\n      ))\n    }\n  }\n\n  test(\"streaming with skipChangeCommits = true skips both delete and change commits\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      Seq(1, 2, 3).toDF(\"x\").write.format(\"delta\").save(inputDir.toString)\n\n      val df = loadStreamWithOptions(\n        inputDir.toString, Map(DeltaOptions.SKIP_CHANGE_COMMITS_OPTION -> \"true\"))\n\n      val q = df.writeStream\n        .format(\"delta\")\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .start(outputDir.toString)\n      try {\n        q.processAllAvailable()\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.toString),\n          Seq(1, 2, 3).map(Row(_)))\n\n        // Delete all rows: produces only RemoveFile actions (delete-only commit)\n        io.delta.tables.DeltaTable.forPath(spark, inputDir.getAbsolutePath).delete()\n\n        Seq(4, 5).toDF(\"x\").write.format(\"delta\").mode(\"append\").save(inputDir.toString)\n\n        // Overwrite produces both AddFile and RemoveFile actions (change commit)\n        Seq(6, 7, 8).toDF(\"x\")\n          .write\n          .mode(\"overwrite\")\n          .format(\"delta\")\n          .save(inputDir.toString)\n\n        Seq(9, 10).toDF(\"x\").write.format(\"delta\").mode(\"append\").save(inputDir.toString)\n\n        q.processAllAvailable()\n\n        // Both the delete and overwrite commits are silently skipped; only inserts are processed\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.toString),\n          Seq(1, 2, 3, 4, 5, 9, 10).map(Row(_)))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"fail on data loss - starting from missing files\") {\n    withTempDirs { (srcData, targetData, chkLocation) =>\n      def addData(): Unit = {\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(srcData.getCanonicalPath)\n      }\n\n      addData()\n      val df = loadStreamWithOptions(srcData.getCanonicalPath, Map.empty)\n\n      val q = df.writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n        .start(targetData.getCanonicalPath)\n      q.processAllAvailable()\n      q.stop()\n\n      addData()\n      addData()\n      addData()\n\n      val srcLog = DeltaLog.forTable(spark, srcData)\n      // Create a checkpoint so that we can create a snapshot without json files before version 3\n      srcLog.checkpoint()\n      // Delete the first file\n      assert(new File(FileNames.unsafeDeltaFile(srcLog.logPath, 1).toUri).delete())\n\n      val e = intercept[StreamingQueryException] {\n        val q = df.writeStream.format(\"delta\")\n          .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n          .start(targetData.getCanonicalPath)\n        q.processAllAvailable()\n      }\n      assert(e.getCause.getMessage === DeltaErrors.failOnDataLossException(1L, 2L).getMessage)\n    }\n  }\n\n  test(\"fail on data loss - gaps of files\") {\n    withTempDirs { (srcData, targetData, chkLocation) =>\n      def addData(): Unit = {\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(srcData.getCanonicalPath)\n      }\n\n      addData()\n      val df = loadStreamWithOptions(srcData.getCanonicalPath, Map.empty)\n\n      val q = df.writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n        .start(targetData.getCanonicalPath)\n      q.processAllAvailable()\n      q.stop()\n\n      addData()\n      addData()\n      addData()\n\n      val srcLog = DeltaLog.forTable(spark, srcData)\n      // Create a checkpoint so that we can create a snapshot without json files before version 3\n      srcLog.checkpoint()\n      // Delete the second file\n      assert(new File(FileNames.unsafeDeltaFile(srcLog.logPath, 2).toUri).delete())\n\n      val e = intercept[StreamingQueryException] {\n        val q = df.writeStream.format(\"delta\")\n          .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n          .start(targetData.getCanonicalPath)\n        q.processAllAvailable()\n      }\n      assert(e.getCause.getMessage === DeltaErrors.failOnDataLossException(2L, 3L).getMessage)\n    }\n  }\n\n  test(\"fail on data loss - starting from missing files with option off\") {\n    withTempDirs { (srcData, targetData, chkLocation) =>\n      def addData(): Unit = {\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(srcData.getCanonicalPath)\n      }\n\n      addData()\n      val df = loadStreamWithOptions(\n        srcData.getCanonicalPath,\n        Map(\"failOnDataLoss\" -> \"false\"))\n\n      val q = df.writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n        .start(targetData.getCanonicalPath)\n      q.processAllAvailable()\n      q.stop()\n\n      addData()\n      addData()\n      addData()\n\n      val srcLog = DeltaLog.forTable(spark, srcData)\n      // Create a checkpoint so that we can create a snapshot without json files before version 3\n      srcLog.checkpoint()\n      // Delete the first file\n      assert(new File(FileNames.unsafeDeltaFile(srcLog.logPath, 1).toUri).delete())\n\n      val q2 = df.writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n        .start(targetData.getCanonicalPath)\n      q2.processAllAvailable()\n      q2.stop()\n\n      assert(spark.read.format(\"delta\").load(targetData.getCanonicalPath).count() === 30)\n    }\n  }\n\n  test(\"fail on data loss - gaps of files with option off\") {\n    withTempDirs { (srcData, targetData, chkLocation) =>\n      def addData(): Unit = {\n        spark.range(10).write.format(\"delta\").mode(\"append\").save(srcData.getCanonicalPath)\n      }\n\n      addData()\n      val df = loadStreamWithOptions(\n        srcData.getCanonicalPath,\n        Map(\"failOnDataLoss\" -> \"false\"))\n\n      val q = df.writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n        .start(targetData.getCanonicalPath)\n      q.processAllAvailable()\n      q.stop()\n\n      addData()\n      addData()\n      addData()\n\n      val srcLog = DeltaLog.forTable(spark, srcData)\n      // Create a checkpoint so that we can create a snapshot without json files before version 3\n      srcLog.checkpoint()\n      // Delete the second file\n      assert(new File(FileNames.unsafeDeltaFile(srcLog.logPath, 2).toUri).delete())\n\n      val q2 = df.writeStream.format(\"delta\")\n        .option(\"checkpointLocation\", chkLocation.getCanonicalPath)\n        .start(targetData.getCanonicalPath)\n      q2.processAllAvailable()\n      q2.stop()\n\n      assert(spark.read.format(\"delta\").load(targetData.getCanonicalPath).count() === 30)\n    }\n  }\n\n  test(\"make sure that the delta sources works fine\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n\n      import io.delta.implicits._\n\n      Seq(1, 2, 3).toDF().write.delta(inputDir.toString)\n\n      val df = spark.readStream.delta(inputDir.toString)\n\n      val stream = df.writeStream\n        .option(\"checkpointLocation\", checkpointDir.toString)\n        .delta(outputDir.toString)\n\n      stream.processAllAvailable()\n      stream.stop()\n\n      val writtenStreamDf = spark.read.delta(outputDir.toString)\n      val expectedRows = Seq(Row(1), Row(2), Row(3))\n\n      checkAnswer(writtenStreamDf, expectedRows)\n    }\n  }\n\n\n  test(\"should not attempt to read a non exist version\") {\n    withTempDirs { (inputDir1, inputDir2, checkpointDir) =>\n      spark.range(1, 2).write.format(\"delta\").save(inputDir1.getCanonicalPath)\n      spark.range(1, 2).write.format(\"delta\").save(inputDir2.getCanonicalPath)\n\n      def startQuery(): StreamingQuery = {\n        val df1 = loadStreamWithOptions(inputDir1.getCanonicalPath, Map.empty)\n        val df2 = loadStreamWithOptions(inputDir2.getCanonicalPath, Map.empty)\n        df1.union(df2).writeStream\n          .format(\"noop\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .start()\n      }\n\n      var q = startQuery()\n      try {\n        q.processAllAvailable()\n        // current offsets:\n        // source1: DeltaSourceOffset(reservoirVersion=1,index=0,isInitialSnapshot=true)\n        // source2: DeltaSourceOffset(reservoirVersion=1,index=0,isInitialSnapshot=true)\n\n        spark.range(1, 2).write.format(\"delta\").mode(\"append\").save(inputDir1.getCanonicalPath)\n        spark.range(1, 2).write.format(\"delta\").mode(\"append\").save(inputDir2.getCanonicalPath)\n        q.processAllAvailable()\n        // current offsets:\n        // source1: DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // source2: DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // Note: version 2 doesn't exist in source1\n\n        spark.range(1, 2).write.format(\"delta\").mode(\"append\").save(inputDir2.getCanonicalPath)\n        q.processAllAvailable()\n        // current offsets:\n        // source1: DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // source2: DeltaSourceOffset(reservoirVersion=3,index=-1,isInitialSnapshot=false)\n        // Note: version 2 doesn't exist in source1\n\n        q.stop()\n        // Restart the query. It will call `getBatch` on the previous two offsets of `source1` which\n        // are both DeltaSourceOffset(reservoirVersion=2,index=-1,isInitialSnapshot=false)\n        // As version 2 doesn't exist, we should not try to load version 2 in this case.\n        q = startQuery()\n        q.processAllAvailable()\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"self union a Delta table should pass the catalog table assert\") {\n    withTable(\"self_union_delta\") {\n      spark.range(10).write.format(\"delta\").saveAsTable(\"self_union_delta\")\n      val df = spark.readStream.format(\"delta\").table(\"self_union_delta\")\n      val q = df.union(df).writeStream.format(\"noop\").start()\n      try {\n        q.processAllAvailable()\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"ES-445863: delta source should not hang or reprocess data when using AvailableNow\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      def runQuery(): Unit = {\n        val q = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n          // Require a partition filter. The max index of files matching the partition filter must\n          // be less than the number of files in the second commit.\n          .where(\"part = 0\")\n          .writeStream\n          .format(\"delta\")\n          .trigger(Trigger.AvailableNow)\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .start(outputDir.getCanonicalPath)\n        try {\n          if (!q.awaitTermination(60000)) {\n            throw new TimeoutException(\"the query didn't stop in 60 seconds\")\n          }\n        } finally {\n          q.stop()\n        }\n      }\n\n      spark.range(0, 1)\n        .selectExpr(\"id\", \"id as part\")\n        .repartition(10)\n        .write\n        .partitionBy(\"part\")\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(inputDir.getCanonicalPath)\n      runQuery()\n\n      spark.range(1, 10)\n        .selectExpr(\"id\", \"id as part\")\n        .repartition(9)\n        .write\n        .partitionBy(\"part\")\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(inputDir.getCanonicalPath)\n      runQuery()\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n        Row(0, 0) :: Nil)\n    }\n  }\n\n  test(\"add column: restarting with new DataFrame should recover\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 10).foreach { i =>\n        val v = Seq(i.toString).toDF(\"id\")\n        v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      def startQuery(): StreamingQuery = {\n        loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n          .writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .option(\"mergeSchema\", \"true\")\n          // use delta sink because we need to check the result\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      }\n\n      var q = startQuery()\n      try {\n        q.processAllAvailable()\n        checkAnswer(\n          spark.read.format(\"delta\").load(outputDir.getCanonicalPath),\n          (0 until 10).map(i => Row(i.toString)))\n\n        // Clear delta log cache\n        DeltaLog.clearCache()\n        // Change the table schema using the non-cached `DeltaLog` to mimic the case that the\n        // table schema change happens on a different cluster\n        withMetadata(deltaLog, StructType.fromDDL(\"id STRING, newcol STRING\"))\n        Seq((\"10\", \"a\")).toDF(\"id\", \"newcol\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n\n        // The streaming query should fail when detecting a schema change\n        val e = intercept[StreamingQueryException] {\n          q.processAllAvailable()\n        }\n        assert(e.getMessage.contains(\"Detected schema change\"))\n\n        // Restarting the query with a new DataFrame should recover from the schema change\n        q = startQuery()\n        q.processAllAvailable()\n        // Verify the output schema includes the new column\n        val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n        assert(outputDf.schema.fieldNames.toSet == Set(\"id\", \"newcol\"),\n          s\"Expected schema with {id, newcol} but got ${outputDf.schema.fieldNames.mkString(\", \")}\")\n        checkAnswer(outputDf,\n          (0 until 10).map(i => Row(i.toString, null)) :+ Row(\"10\", \"a\"))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"add column: restarting with stale DataFrame should fail\") {\n    withTempDir { inputDir =>\n      withTempDir { checkpointDir =>\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        (0 until 2).foreach { i =>\n          val v = Seq(i.toString).toDF(\"id\")\n          v.write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n        val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n        // First run: stream detects schema change and fails\n        testStream(df)(\n          StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n          ProcessAllAvailable(),\n          CheckAnswer(\"0\", \"1\"),\n          Execute { _ =>\n            withMetadata(deltaLog, StructType.fromDDL(\"id STRING, newcol STRING\"))\n            Seq((\"2\", \"a\")).toDF(\"id\", \"newcol\")\n              .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n          },\n          ExpectFailure[DeltaIllegalStateException](t =>\n            assert(t.getMessage.contains(\"Detected schema change\"))),\n          Execute { q =>\n            assert(!q.isActive)\n          }\n        )\n\n        // Restarting with the same DataFrame cannot recover from column addition because the\n        // plan's read schema is fixed at analysis time. User must create a new DataFrame.\n        val e = intercept[StreamingQueryException] {\n          val q = df.writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"noop\")\n            .start()\n          try q.processAllAvailable() finally q.stop()\n        }\n        // V1 fails with an internal batch schema assertion (\"Invalid batch\"),\n        // V2 fails with DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART.\n        assert(\n          e.getMessage.contains(\"Invalid batch\") ||\n            e.getMessage.contains(\"DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART\"),\n          s\"Expected schema mismatch error but got: ${e.getMessage}\"\n        )\n      }\n    }\n  }\n\n  test(\"relax nullability: restarting with new DataFrame should recover\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` \" +\n        \"(id STRING NOT NULL) USING DELTA\")\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 2).foreach { i =>\n        Seq(i.toString).toDF(\"id\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n\n      def startQuery(): StreamingQuery = {\n        loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n          .writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .option(\"mergeSchema\", \"true\")\n          // use delta sink because we need to check the result\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n      }\n\n      var q = startQuery()\n      try {\n        q.processAllAvailable()\n\n        // Clear delta log cache\n        DeltaLog.clearCache()\n        // Relax nullability from \"id STRING NOT NULL\" to \"id STRING\" using the non-cached\n        // `DeltaLog` to mimic the case that the schema change happens on a different cluster\n        withMetadata(deltaLog, StructType.fromDDL(\"id STRING\"))\n        // Insert a null row\n        Seq(Option.empty[String]).toDF(\"id\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n\n        // The streaming query should fail when detecting a schema change\n        val e = intercept[StreamingQueryException] {\n          q.processAllAvailable()\n        }\n        assert(e.getMessage.contains(\"Detected schema change\"))\n\n        // Restarting the query with a new DataFrame should recover from the schema change\n        q = startQuery()\n        q.processAllAvailable()\n        val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n        assert(outputDf.schema(\"id\").nullable,\n          \"Expected 'id' column to be nullable after relaxing nullability\")\n        checkAnswer(outputDf.orderBy(\"id\"),\n          Seq(Row(null), Row(\"0\"), Row(\"1\")))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"relax nullability: restarting with stale DataFrame should recover\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` \" +\n        \"(id STRING NOT NULL) USING DELTA\")\n      val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n      (0 until 2).foreach { i =>\n        Seq(i.toString).toDF(\"id\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n      }\n      val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n      var q = df.writeStream\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .option(\"mergeSchema\", \"true\")\n        // use delta sink because we need to check the result\n        .format(\"delta\")\n        .start(outputDir.getCanonicalPath)\n      try {\n        q.processAllAvailable()\n\n        // Clear delta log cache\n        DeltaLog.clearCache()\n        // Relax nullability from \"id STRING NOT NULL\" to \"id STRING\" using the non-cached\n        // `DeltaLog` to mimic the case that the schema change happens on a different cluster\n        withMetadata(deltaLog, StructType.fromDDL(\"id STRING\"))\n        // Insert a null row\n        Seq(Option.empty[String]).toDF(\"id\")\n          .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n\n        // The streaming query should fail when detecting a schema change\n        val e = intercept[StreamingQueryException] {\n          q.processAllAvailable()\n        }\n        assert(e.getMessage.contains(\"Detected schema change\"))\n\n        // Restarting the query with the stale DataFrame should still recover\n        q = df.writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .option(\"mergeSchema\", \"true\")\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n        q.processAllAvailable()\n        val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n        assert(outputDf.schema(\"id\").nullable,\n          \"Expected 'id' column to be nullable after relaxing nullability\")\n        checkAnswer(outputDf.orderBy(\"id\"),\n          Seq(Row(null), Row(\"0\"), Row(\"1\")))\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"type widening: restarting with new DataFrame should recover\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING.key -> \"false\",\n      DeltaConfigs.ENABLE_TYPE_WIDENING.defaultTablePropertyKey -> \"true\",\n      DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> \"true\") {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id INT) \" +\n          \"USING DELTA TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\")\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        (0 until 2).foreach { i =>\n          Seq(i).toDF(\"id\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n\n        def startQuery(): StreamingQuery = {\n          loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n            .writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .option(\"mergeSchema\", \"true\")\n            // use delta sink because we need to check the result\n            .format(\"delta\")\n            .start(outputDir.getCanonicalPath)\n        }\n\n        var q = startQuery()\n        try {\n          q.processAllAvailable()\n\n          // Clear delta log cache\n          DeltaLog.clearCache()\n          // Widen the column type from INT to BIGINT using the non-cached `DeltaLog` to mimic\n          // the case that the schema change happens on a different cluster\n          withMetadata(deltaLog, StructType.fromDDL(\"id BIGINT\"))\n          // 2^31 cannot be represented as an int, so it will be inserted as a bigint\n          Seq(2147483648L).toDF(\"id\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n\n          // The streaming query should fail when detecting a schema change\n          val e = intercept[StreamingQueryException] {\n            q.processAllAvailable()\n          }\n          assert(e.getMessage.contains(\"Detected schema change\"))\n\n          // Restarting the query with a new DataFrame should recover from the schema change\n          q = startQuery()\n          q.processAllAvailable()\n          val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n          assert(outputDf.schema(\"id\").dataType === LongType,\n            s\"Expected 'id' column to be LongType after type widening but got \" +\n              s\"${outputDf.schema(\"id\").dataType}\")\n          checkAnswer(outputDf.orderBy(\"id\"),\n            Seq(Row(0L), Row(1L), Row(2147483648L)))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"type widening: restarting with stale DataFrame should recover\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING.key -> \"false\",\n      DeltaConfigs.ENABLE_TYPE_WIDENING.defaultTablePropertyKey -> \"true\",\n      DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> \"true\") {\n      withTempDirs { (inputDir, outputDir, checkpointDir) =>\n        sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id INT) \" +\n          \"USING DELTA TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\")\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        (0 until 2).foreach { i =>\n          Seq(i).toDF(\"id\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n        val df = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n\n        var q = df.writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .option(\"mergeSchema\", \"true\")\n          // use delta sink because we need to check the result\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n        try {\n          q.processAllAvailable()\n\n          // Clear delta log cache\n          DeltaLog.clearCache()\n          // Widen the column type from INT to BIGINT using the non-cached `DeltaLog` to mimic\n          // the case that the schema change happens on a different cluster\n          withMetadata(deltaLog, StructType.fromDDL(\"id BIGINT\"))\n          // 2^31 cannot be represented as an int, so it will be inserted as a bigint\n          Seq(2147483648L).toDF(\"id\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n\n          // The streaming query should fail when detecting a schema change\n          val e = intercept[StreamingQueryException] {\n            q.processAllAvailable()\n          }\n          assert(e.getMessage.contains(\"Detected schema change\"))\n\n          // Restarting the query with the stale DataFrame should still recover\n          q = df.writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .option(\"mergeSchema\", \"true\")\n            .format(\"delta\")\n            .start(outputDir.getCanonicalPath)\n          q.processAllAvailable()\n          val outputDf = spark.read.format(\"delta\").load(outputDir.getCanonicalPath)\n          assert(outputDf.schema(\"id\").dataType === LongType,\n            s\"Expected 'id' column to be LongType after type widening but got \" +\n              s\"${outputDf.schema(\"id\").dataType}\")\n          checkAnswer(outputDf.orderBy(\"id\"),\n            Seq(Row(0L), Row(1L), Row(2147483648L)))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"drop column: should fail with non-additive schema change error\") {\n    withTempDir { inputDir =>\n      withTempDir { checkpointDir =>\n        // Create a table with column mapping enabled (required for drop/rename column)\n        sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id STRING, value STRING) \" +\n          \"USING DELTA \" +\n          \"TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        (0 until 5).foreach { i =>\n          Seq((i.toString, s\"val$i\")).toDF(\"id\", \"value\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n\n        def startQuery(): StreamingQuery = {\n          loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n            .writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"noop\")\n            .start()\n        }\n\n        val q = startQuery()\n        try {\n          q.processAllAvailable()\n\n          DeltaLog.clearCache()\n          // Simulate dropping \"value\" column by committing new metadata with only \"id\".\n          // This is a non-additive schema change (column removal).\n          withMetadata(deltaLog, StructType.fromDDL(\"id STRING\"))\n\n          val e = intercept[StreamingQueryException] {\n            q.processAllAvailable()\n          }\n          assert(e.getMessage.contains(\n            \"Streaming read is not supported on tables with read-incompatible schema changes\"))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"drop column: should succeed with unsafe column mapping schema change flag enabled\") {\n    withTempDirs { (inputDir, outputDir, checkpointDir) =>\n      // Enable the unsafe flag that allows streaming to continue past column mapping\n      // schema changes (drop/rename) instead of failing.\n      withSQLConf(\n        DeltaSQLConf\n          .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES\n          .key -> \"true\"\n      ) {\n        sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id STRING, value STRING) \" +\n          \"USING DELTA \" +\n          \"TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        (0 until 5).foreach { i =>\n          Seq((i.toString, s\"val$i\")).toDF(\"id\", \"value\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n\n        val q = loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n          .writeStream\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .format(\"delta\")\n          .start(outputDir.getCanonicalPath)\n        try {\n          q.processAllAvailable()\n\n          DeltaLog.clearCache()\n          // Simulate dropping \"value\" column by committing new metadata with only \"id\".\n          withMetadata(deltaLog, StructType.fromDDL(\"id STRING\"))\n\n          // Write more data after the drop column schema change\n          (5 until 10).foreach { i =>\n            Seq(i.toString).toDF(\"id\")\n              .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n          }\n\n          // With the unsafe flag, the stream should process all data without failing.\n          // Post-drop rows have null for the removed \"value\" column.\n          q.processAllAvailable()\n\n          checkAnswer(\n            spark.read.format(\"delta\").load(outputDir.getAbsolutePath),\n            (0 until 5).map(i => (i.toString, s\"val$i\")).toDF(\"id\", \"value\") union\n              (5 until 10).map(i => (i.toString, null: String)).toDF(\"id\", \"value\"))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"rename column: should fail with non-additive schema change error\") {\n    withTempDir { inputDir =>\n      withTempDir { checkpointDir =>\n        sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id STRING, value STRING) \" +\n          \"USING DELTA \" +\n          \"TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n        val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n        (0 until 5).foreach { i =>\n          Seq((i.toString, s\"val$i\")).toDF(\"id\", \"value\")\n            .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n        }\n\n        def startQuery(): StreamingQuery = {\n          loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n            .writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"noop\")\n            .start()\n        }\n\n        var q = startQuery()\n        try {\n          q.processAllAvailable()\n\n          DeltaLog.clearCache()\n          // Simulate renaming \"value\" -> \"renamed_value\" by committing new metadata.\n          // Column rename is a non-additive schema change under column mapping.\n          withMetadata(deltaLog, StructType.fromDDL(\"id STRING, renamed_value STRING\"))\n\n          val e = intercept[StreamingQueryException] {\n            q.processAllAvailable()\n          }\n          assert(e.getMessage.contains(\n            \"Streaming read is not supported on tables with read-incompatible schema changes\"))\n        } finally {\n          q.stop()\n        }\n      }\n    }\n  }\n\n  test(\"rename column: should throw schema change error with unsafe flag enabled\") {\n    withTempDir { inputDir =>\n      withTempDir { checkpointDir =>\n        // The unsafe flag only helps with drop-column; rename is still blocked because\n        // it changes logical column identity, which can silently corrupt data.\n        withSQLConf(\n          DeltaSQLConf\n            .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES\n            .key -> \"true\"\n        ) {\n          sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id STRING, value STRING) \" +\n            \"USING DELTA \" +\n            \"TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n          val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n          (0 until 5).foreach { i =>\n            Seq((i.toString, s\"val$i\")).toDF(\"id\", \"value\")\n              .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n          }\n\n          def startQuery(): StreamingQuery = {\n            loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n              .writeStream\n              .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n              .format(\"noop\")\n              .start()\n          }\n\n          var q = startQuery()\n          try {\n            q.processAllAvailable()\n\n            DeltaLog.clearCache()\n            // Simulate renaming \"value\" -> \"renamed_value\" by committing new metadata.\n            withMetadata(deltaLog, StructType.fromDDL(\"id STRING, renamed_value STRING\"))\n\n            // Even with the unsafe flag, rename still fails - but with a different error\n            // (\"Detected schema change\") rather than the non-additive schema change error.\n            val e = intercept[StreamingQueryException] {\n              q.processAllAvailable()\n            }\n            assert(e.getMessage.contains(\"Detected schema change in version 6\"))\n          } finally {\n            q.stop()\n          }\n        }\n      }\n    }\n  }\n\n  test(\"type widening: should fail with non-additive schema change error \"\n    + \"when enable schema tracking\") {\n    withTempDir { inputDir =>\n      withTempDir { checkpointDir =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING.key -> \"true\"\n        ) {\n          // Table with type widening enabled so that type changes are allowed but tracked.\n          sql(s\"CREATE TABLE delta.`${inputDir.getCanonicalPath}` (id INT, value STRING) \" +\n            \"USING DELTA \" +\n            \"TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\")\n          val deltaLog = DeltaLog.forTable(spark, new Path(inputDir.toURI))\n          (0 until 5).foreach { i =>\n            Seq((i, s\"val$i\")).toDF(\"id\", \"value\")\n              .write.mode(\"append\").format(\"delta\").save(deltaLog.dataPath.toString)\n          }\n\n          def startQuery(): StreamingQuery = {\n            loadStreamWithOptions(inputDir.getCanonicalPath, Map.empty)\n              .writeStream\n              .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n              .format(\"noop\")\n              .start()\n          }\n\n          var q = startQuery()\n          try {\n            q.processAllAvailable()\n\n            DeltaLog.clearCache()\n            // Simulate widening \"id\" from INT -> BIGINT by committing new metadata.\n            // Type widening is a non-additive schema change for streaming reads.\n            withMetadata(deltaLog, StructType.fromDDL(\"id BIGINT, value STRING\"))\n\n            val e = intercept[StreamingQueryException] {\n              q.processAllAvailable()\n            }\n            assert(e.getMessage.contains(\n              \"Streaming read is not supported on tables with read-incompatible schema changes\"))\n          } finally {\n            q.stop()\n          }\n        }\n      }\n    }\n  }\n\n  test(\"handling nullability schema changes\") {\n    withTable(\"srcTable\") {\n      withTempDirs { (srcTblDir, checkpointDir, checkpointDir2) =>\n        def readStream(startingVersion: Option[Long] = None): DataFrame = {\n          var dsr = spark.readStream\n          startingVersion.foreach { v =>\n            dsr = dsr.option(\"startingVersion\", v)\n          }\n          dsr.table(\"srcTable\")\n        }\n\n        sql(s\"\"\"\n             |CREATE TABLE srcTable (\n             |  a STRING NOT NULL,\n             |  b STRING NOT NULL\n             |) USING DELTA LOCATION '${srcTblDir.getCanonicalPath}'\n             |\"\"\".stripMargin)\n        sql(\"\"\"\n            |INSERT INTO srcTable\n            | VALUES (\"a\", \"b\")\n            |\"\"\".stripMargin)\n\n        // Initialize the stream to pass the initial snapshot\n        testStream(readStream())(\n          StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n          ProcessAllAvailable(),\n          CheckAnswer((\"a\", \"b\"))\n        )\n\n        // It is ok to relax nullability during streaming post analysis, and restart would fix it.\n        var v1 = 0L\n        val clock = new StreamManualClock(System.currentTimeMillis())\n        testStream(readStream())(\n          StartStream(checkpointLocation = checkpointDir.getCanonicalPath,\n            trigger = ProcessingTimeTrigger(1000), triggerClock = clock),\n          ProcessAllAvailable(),\n          // Write more data and drop NOT NULL constraint\n          Execute { _ =>\n            // A batch of Delta actions\n            sql(\"\"\"\n              |INSERT INTO srcTable\n              |VALUES (\"c\", \"d\")\n              |\"\"\".stripMargin)\n            sql(\"ALTER TABLE srcTable ALTER COLUMN a DROP NOT NULL\")\n            sql(\"\"\"\n              |INSERT INTO srcTable\n              |VALUES (\"e\", \"f\")\n              |\"\"\".stripMargin)\n            v1 = DeltaLog.forTable(spark, TableIdentifier(\"srcTable\")).update().version\n          },\n          // Process next trigger\n          AdvanceManualClock(1 * 1000L),\n          // The query would fail because the read schema has nullable=false but the schema change\n          // tries to relax it, we cannot automatically move ahead with it.\n          ExpectFailure[DeltaIllegalStateException](t =>\n            assert(t.getMessage.contains(\"Detected schema change\"))),\n          Execute { q =>\n            assert(!q.isActive)\n          },\n          // Upon restart, the backfill can work with relaxed nullability read schema\n          StartStream(checkpointLocation = checkpointDir.getCanonicalPath),\n          ProcessAllAvailable(),\n          // See how it loads data from across the nullability change without a problem\n          CheckAnswer((\"c\", \"d\"), (\"e\", \"f\"))\n        )\n\n        // However, it is NOT ok to read data with relaxed nullability during backfill, and restart\n        // would NOT fix it.\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"srcTable\"))\n        deltaLog.withNewTransaction { txn =>\n          val schema = txn.snapshot.metadata.schema\n          val newSchema = StructType(schema(\"a\").copy(nullable = false) :: schema(\"b\") :: Nil)\n          txn.commit(txn.metadata.copy(schemaString = newSchema.json) :: Nil,\n            DeltaOperations.ManualUpdate)\n        }\n        sql(\"\"\"\n            |INSERT INTO srcTable\n            |VALUES (\"g\", \"h\")\n            |\"\"\".stripMargin)\n        // Backfill from the ADD file action prior to the nullable=false, the latest schema has\n        // nullable = false, but the ADD file has nullable = true, which is not allowed as we don't\n        // want to show any nulls.\n        // It queries [INSERT (e, f), nullable=false schema change, INSERT (g, h)]\n        testStream(readStream(startingVersion = Some(v1)))(\n          StartStream(checkpointLocation = checkpointDir2.getCanonicalPath),\n          // See how it is:\n          // 1. a non-retryable exception as it is a backfill.\n          // 2. it comes from the new stream start check we added, before this, verifyStreamHygiene\n          //    could not detect because the most recent schema change looks exactly like the latest\n          //    schema.\n          ExpectFailure[DeltaIllegalStateException](t =>\n            assert(t.getMessage.contains(\"Detected schema change\") &&\n              t.getStackTrace.exists(\n                _.toString.contains(\"checkReadIncompatibleSchemaChangeOnStreamStartOnce\"))))\n        )\n      }\n    }\n  }\n}\n\n/**\n * A FileSystem implementation that returns monotonically increasing timestamps for file creation.\n * Note that we may return a different timestamp for the same file. This is okay for the tests\n * where we use this though.\n */\nclass MonotonicallyIncreasingTimestampFS extends RawLocalFileSystem {\n  private var time: Long = System.currentTimeMillis()\n\n  override def getScheme: String = MonotonicallyIncreasingTimestampFS.scheme\n\n  override def getUri: URI = {\n    URI.create(s\"$getScheme:///\")\n  }\n\n  override def getFileStatus(f: Path): FileStatus = {\n    val original = super.getFileStatus(f)\n    time += 1000L\n    new FileStatus(original.getLen, original.isDirectory, 0, 0, time, f)\n  }\n}\n\nobject MonotonicallyIncreasingTimestampFS {\n  val scheme = s\"MonotonicallyIncreasingTimestampFS\"\n}\n\n// Batch sizes 1, 2, and 100 exercise different backfill behaviors in the commit coordinator.\n// Batch size 1 triggers a backfill on every commit (commitVersion % 1 == 0), testing the most\n// granular backfill path. Batch size 2 triggers backfill every other commit, testing the boundary\n// between backfilled and unbackfilled commits. Batch size 100 leaves most commits unbackfilled,\n// testing the production-like path where streaming must read from both the commit coordinator\n// and the filesystem. This follows the same pattern as other CatalogManaged (CCv2) test suites\n// (DeltaLogSuite, DeltaCDCStreamSuite, etc.).\n\nclass DeltaSourceWithCatalogManagedBatch1Suite extends DeltaSourceSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSourceWithCatalogManagedBatch2Suite extends DeltaSourceSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaSourceWithCatalogManagedBatch100Suite extends DeltaSourceSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.actions.Format\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\n\nimport org.apache.spark.sql.{Column, DataFrame}\nimport org.apache.spark.sql.streaming.StreamTest\nimport org.apache.spark.sql.types.StructType\nimport org.scalactic.source.Position\nimport org.scalatest.Tag\n\n/**\n * Trait that provides abstraction for testing both DSv1 and DSv2 connectors.\n */\ntrait DeltaSourceConnectorTrait {\n  self: DeltaSQLTestUtils =>\n\n  protected def useDsv2: Boolean = false\n\n  protected def loadStreamWithOptions(path: String, options: Map[String, String]): DataFrame = {\n    val reader = spark.readStream\n    options.foreach { case (k, v) => reader.option(k, v) }\n    if (useDsv2) {\n      // This will route through DeltaCatalog which checks V2_ENABLE_MODE\n      reader.table(s\"delta.`$path`\")\n    } else {\n      reader.format(\"delta\").load(path)\n    }\n  }\n}\n\ntrait DeltaSourceSuiteBase extends StreamTest\n  with DeltaSQLTestUtils\n  with CatalogOwnedTestBaseSuite\n  with DeltaSourceConnectorTrait {\n\n  /**\n   * Creates 3 temporary directories for use within a function.\n   * @param f function to be run with created temp directories\n   */\n  protected def withTempDirs(f: (File, File, File) => Unit): Unit = {\n    withTempDir { file1 =>\n      withTempDir { file2 =>\n        withTempDir { file3 =>\n          f(file1, file2, file3)\n        }\n      }\n    }\n  }\n\n  /**\n   * Creates 3 temporary directories for use within a function using a given prefix.\n   * @param f function to be run with created temp directories\n   */\n  protected def withTempDirs(prefix: String)(f: (File, File, File) => Unit): Unit = {\n    withTempDir(prefix) { file1 =>\n      withTempDir(prefix) { file2 =>\n        withTempDir(prefix) { file3 =>\n          f(file1, file2, file3)\n        }\n      }\n    }\n  }\n\n  /**\n   * Copy metadata for fields in newSchema from currentSchema\n   * @param newSchema new schema\n   * @param currentSchema current schema to reference\n   * @param columnMappingMode mode for column mapping\n   * @return updated new schema\n   */\n  protected def copyOverMetadata(\n      newSchema: StructType,\n      currentSchema: StructType,\n      columnMappingMode: DeltaColumnMappingMode): StructType = {\n    SchemaMergingUtils.transformColumns(newSchema) { (path, field, _) =>\n      val fullName = path :+ field.name\n      val inSchema = SchemaUtils.findNestedFieldIgnoreCase(\n        currentSchema, fullName, includeCollections = true\n      )\n      inSchema.map { refField =>\n        val sparkMetadata = DeltaColumnMapping.getColumnMappingMetadata(refField, columnMappingMode)\n        field.copy(metadata = sparkMetadata)\n      }.getOrElse {\n        field\n      }\n    }\n  }\n\n  protected def withMetadata(\n      deltaLog: DeltaLog,\n      schema: StructType,\n      format: String = \"parquet\",\n      tableId: Option[String] = None): Unit = {\n    val txn = deltaLog.startTransaction()\n    val baseMetadata = tableId.map { tId => txn.metadata.copy(id = tId) }.getOrElse(txn.metadata)\n    // We need to fill up the missing id/physical name in column mapping mode\n    // while maintaining existing metadata if there is any\n    val updatedSchema = copyOverMetadata(\n      schema, baseMetadata.schema,\n      baseMetadata.columnMappingMode)\n    // Configure CatalogManaged (CCv2) table settings.\n    val updatedConfiguration = if (catalogOwnedDefaultCreationEnabledInTests) {\n      // This withMetadata helper calls txn.commit(Metadata, ManualUpdate) directly, bypassing\n      // the normal CREATE TABLE path (CreateDeltaTableCommand, DeltaTestImplicits.commitActions)\n      // that populates newProtocol with CatalogOwnedTableFeature and auto-enables ICT. Without\n      // ICT, the CatalogManaged commit coordinator rejects the commit because it requires\n      // commitTimestamp on every version. This is a test-only issue: txn.commit is not a\n      // user-facing table creation API, and production tables always go through the full DDL\n      // path. We enable ICT manually here as the simplest fix.\n      baseMetadata.configuration +\n        (DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"true\")\n    } else {\n      baseMetadata.configuration\n    }\n    txn.commit(\n      DeltaColumnMapping.assignColumnIdAndPhysicalName(\n        baseMetadata.copy(\n          schemaString = updatedSchema.json,\n          configuration = updatedConfiguration,\n          format = Format(format)),\n        baseMetadata,\n        isChangingModeOnExistingTable = false,\n        isOverwritingSchema = false) :: Nil, DeltaOperations.ManualUpdate)\n  }\n\n  object AddToReservoir {\n    def apply(path: File, data: DataFrame): AssertOnQuery =\n      AssertOnQuery { _ =>\n        data.write.format(\"delta\").mode(\"append\").save(path.getAbsolutePath)\n        true\n      }\n  }\n\n  object UpdateReservoir {\n    def apply(path: File, updateExpression: Map[String, Column]): AssertOnQuery =\n      AssertOnQuery { _ =>\n        io.delta.tables.DeltaTable.forPath(path.getAbsolutePath).update(updateExpression)\n        true\n      }\n  }\n\n  object DeleteFromReservoir {\n    def apply(path: File, deleteCondition: Column): AssertOnQuery =\n      AssertOnQuery { _ =>\n        io.delta.tables.DeltaTable.forPath(path.getAbsolutePath).delete(deleteCondition)\n        true\n      }\n  }\n\n  object MergeIntoReservoir {\n    def apply(path: File, dfToMerge: DataFrame, mergeCondition: Column,\n              updateExpression: Map[String, Column]): AssertOnQuery =\n      AssertOnQuery { _ =>\n        io.delta.tables.DeltaTable\n          .forPath(path.getAbsolutePath)\n          .as(\"table\")\n          .merge(dfToMerge, mergeCondition)\n          .whenMatched()\n          .update(updateExpression)\n          .whenNotMatched()\n          .insertAll()\n          .execute()\n        true\n      }\n  }\n\n  object CheckProgress {\n    def apply(rowsPerBatch: Seq[Int]): AssertOnQuery =\n      Execute { q =>\n        val progress = q.recentProgress.filter(_.numInputRows != 0)\n        assert(progress.length === rowsPerBatch.size, \"Expected batches don't match\")\n        progress.zipWithIndex.foreach { case (p, i) =>\n          assert(p.numInputRows === rowsPerBatch(i), s\"Expected rows in batch $i does not match \")\n        }\n      }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSourceTableAPISuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsBaseSuite\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\n\nimport org.apache.spark.sql.{AnalysisException, Dataset}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.SessionCatalog.DEFAULT_DATABASE\nimport org.apache.spark.sql.execution.streaming._\nimport org.apache.spark.sql.streaming.{StreamingQuery, StreamTest}\nimport org.apache.spark.util.Utils\n\nclass DeltaSourceTableAPISuite extends StreamTest\n  with DeltaSQLCommandTest\n  with CoordinatedCommitsBaseSuite {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n  }\n\n  import testImplicits._\n  test(\"table API\") {\n    withTempDir { tempDir =>\n      val tblName = \"my_table\"\n      val dir = tempDir.getAbsolutePath\n      withTable(tblName) {\n        spark.range(3).write.format(\"delta\").option(\"path\", dir).saveAsTable(tblName)\n\n        testStream(spark.readStream.table(tblName))(\n          ProcessAllAvailable(),\n          CheckAnswer(0, 1, 2)\n        )\n      }\n    }\n  }\n\n  test(\"table API with database\") {\n    withTempDir { tempDir =>\n      val tblName = \"my_table\"\n      val dir = tempDir.getAbsolutePath\n      withTempDatabase { db =>\n        withTable(tblName) {\n          spark.sql(s\"USE $db\")\n          spark.range(3).write.format(\"delta\").option(\"path\", dir).saveAsTable(tblName)\n          spark.sql(s\"USE $DEFAULT_DATABASE\")\n\n          testStream(spark.readStream.table(s\"$db.$tblName\"))(\n            ProcessAllAvailable(),\n            CheckAnswer(0, 1, 2)\n          )\n        }\n      }\n    }\n  }\n\n  private def startTableStream(\n    ds: Dataset[_],\n    tableName: String,\n    baseDir: Option[File] = None,\n    partitionColumns: Seq[String] = Nil,\n    format: String = \"delta\"): StreamingQuery = {\n    val checkpoint = baseDir.map(new File(_, \"_checkpoint\"))\n      .getOrElse(Utils.createTempDir().getCanonicalFile)\n    val dsw = ds.writeStream.format(format).partitionBy(partitionColumns: _*)\n    baseDir.foreach { output =>\n      dsw.option(\"path\", output.getCanonicalPath)\n    }\n    dsw.option(\"checkpointLocation\", checkpoint.getCanonicalPath).toTable(tableName)\n  }\n\n  test(\"writeStream.table - create new external table\") {\n    withTempDir { dir =>\n      val memory = MemoryStream[Int]\n      val tableName = \"stream_test\"\n      withTable(tableName) {\n        val sq = startTableStream(memory.toDS(), tableName, Some(dir))\n        memory.addData(1, 2, 3)\n        sq.processAllAvailable()\n\n        checkDatasetUnorderly(\n          spark.table(tableName).as[Int],\n          1, 2, 3)\n\n        checkDatasetUnorderly(\n          spark.read.format(\"delta\").load(dir.getCanonicalPath).as[Int],\n          1, 2, 3)\n      }\n    }\n  }\n\n  test(\"writeStream.table - create new managed table\") {\n    val memory = MemoryStream[Int]\n    val tableName = \"stream_test\"\n    withTable(tableName) {\n      val sq = startTableStream(memory.toDS(), tableName, None)\n      memory.addData(1, 2, 3)\n      sq.processAllAvailable()\n\n      checkDatasetUnorderly(\n        spark.table(tableName).as[Int],\n        1, 2, 3)\n\n      val path = spark.sessionState.catalog.getTableRawMetadata(TableIdentifier(tableName)).location\n      checkDatasetUnorderly(\n        spark.read.format(\"delta\").load(new File(path).getCanonicalPath).as[Int],\n        1, 2, 3)\n    }\n  }\n\n  test(\"writeStream.table - create new managed table with database\") {\n    val memory = MemoryStream[Int]\n    val db = \"my_db\"\n    val tableName = s\"$db.stream_test\"\n    withDatabase(db) {\n      sql(s\"create database $db\")\n      withTable(tableName) {\n        val sq = startTableStream(memory.toDS(), tableName, None)\n        memory.addData(1, 2, 3)\n        sq.processAllAvailable()\n\n        checkDatasetUnorderly(\n          spark.table(tableName).as[Int],\n          1, 2, 3)\n\n        val path = spark.sessionState.catalog.getTableRawMetadata(\n          spark.sessionState.sqlParser.parseTableIdentifier(tableName)).location\n        checkDatasetUnorderly(\n          spark.read.format(\"delta\").load(new File(path).getCanonicalPath).as[Int],\n          1, 2, 3)\n      }\n    }\n  }\n\n  test(\"writeStream.table - create table from existing output\") {\n    withTempDir { dir =>\n      Seq(4, 5, 6).toDF(\"value\").write.format(\"delta\").save(dir.getCanonicalPath)\n      val memory = MemoryStream[Int]\n      val tableName = \"stream_test\"\n      withTable(tableName) {\n        val sq = startTableStream(memory.toDS(), tableName, Some(dir))\n        memory.addData(1, 2, 3)\n        sq.processAllAvailable()\n\n        checkDatasetUnorderly(\n          spark.table(tableName).as[Int],\n          1, 2, 3, 4, 5, 6)\n\n        checkDatasetUnorderly(\n          spark.read.format(\"delta\").load(dir.getCanonicalPath).as[Int],\n          1, 2, 3, 4, 5, 6)\n      }\n    }\n  }\n\n  test(\"writeStream.table - fail writing into a view\") {\n    val memory = MemoryStream[Int]\n    val tableName = \"stream_test\"\n    withTable(tableName) {\n      val viewName = tableName + \"_view\"\n      withView(viewName) {\n        Seq(4, 5, 6).toDF(\"value\").write.saveAsTable(tableName)\n        sql(s\"create view $viewName as select * from $tableName\")\n        val e = intercept[AnalysisException] {\n          startTableStream(memory.toDS(), viewName, None)\n        }\n        assert(e.getMessage.contains(\"views\"))\n      }\n    }\n  }\n\n  test(\"writeStream.table - fail due to different schema than existing Delta table\") {\n    withTempDir { dir =>\n      Seq(4, 5, 6).toDF(\"id\").write.format(\"delta\").save(dir.getCanonicalPath)\n      val memory = MemoryStream[Int]\n      val tableName = \"stream_test\"\n      withTable(tableName) {\n        val e = intercept[Exception] {\n          val sq = startTableStream(memory.toDS(), tableName, Some(dir))\n          memory.addData(1, 2, 3)\n          sq.processAllAvailable()\n        }\n        assert(e.getMessage.contains(\"The specified schema does not match the existing schema\"))\n      }\n    }\n  }\n\n  test(\"writeStream.table - fail due to different partitioning on existing Delta table\") {\n    withTempDir { dir =>\n      Seq(4 -> \"a\").toDF(\"id\", \"key\").write.format(\"delta\").save(dir.getCanonicalPath)\n      val memory = MemoryStream[(Int, String)]\n      val tableName = \"stream_test\"\n      withTable(tableName) {\n        val e = intercept[Exception] {\n          val sq = startTableStream(\n            memory.toDS().toDF(\"id\", \"key\"), tableName, Some(dir), Seq(\"key\"))\n          memory.addData(1 -> \"a\")\n          sq.processAllAvailable()\n        }\n        assert(e.getMessage.contains(\n          \"The specified partitioning does not match the existing partitioning\"))\n      }\n    }\n  }\n\n  test(\"writeStream.table - fail writing into an external nonDelta table\") {\n    withTempDir { dir =>\n      val memory = MemoryStream[(Int, String)]\n      val tableName = \"stream_test\"\n      withTable(tableName) {\n        Seq(1).toDF(\"value\").write.format(\"parquet\")\n          .option(\"path\", dir.getCanonicalPath).saveAsTable(tableName)\n        val e = intercept[AnalysisException] {\n          startTableStream(memory.toDS(), tableName, Some(dir))\n        }\n        assert(e.getMessage.contains(\"delta\"))\n      }\n    }\n  }\n\n  test(\"writeStream.table - fail writing into an external nonDelta path\") {\n    withTempDir { dir =>\n      val memory = MemoryStream[Int]\n      val tableName = \"stream_test\"\n      withTable(tableName) {\n        Seq(1).toDF(\"value\").write.mode(\"append\").parquet(dir.getCanonicalPath)\n        val e = intercept[AnalysisException] {\n          startTableStream(memory.toDS(), tableName, Some(dir))\n        }\n        assert(e.getMessage.contains(\"Delta\"))\n      }\n    }\n  }\n}\n\nclass DeltaSourceTableAPIWithCoordinatedCommitsBatch1Suite extends DeltaSourceTableAPISuite {\n  override def coordinatedCommitsBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaSourceTableAPIWithCoordinatedCommitsBatch100Suite extends DeltaSourceTableAPISuite {\n  override def coordinatedCommitsBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaStreamUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\n\nimport org.apache.spark.SparkFunSuite\n\nimport org.apache.spark.sql.delta.sources.DeltaStreamUtils\n\nclass DeltaStreamUtilsSuite extends SparkFunSuite {\n\n  // ========== getStartingVersionFromCommitAtTimestamp ==========\n\n  test(\"getStartingVersionFromCommitAtTimestamp - \" +\n    \"commit at timestamp returns commitVersion\") {\n    val timeZone = \"UTC\"\n    val commitTs = 1000L\n    val commitVersion = 2L\n    val latestVersion = 5L\n    val timestamp = new Timestamp(1000)\n    val result = DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n      timeZone, commitTs, commitVersion, latestVersion, timestamp)\n    assert(result == 2L)\n  }\n\n  test(\"getStartingVersionFromCommitAtTimestamp - \" +\n    \"commit after timestamp returns commitVersion\") {\n    val timeZone = \"UTC\"\n    val commitTs = 2000L\n    val commitVersion = 2L\n    val latestVersion = 5L\n    val timestamp = new Timestamp(1000)\n    val result = DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n      timeZone, commitTs, commitVersion, latestVersion, timestamp)\n    assert(result == 2L)\n  }\n\n  test(\"getStartingVersionFromCommitAtTimestamp - \" +\n    \"commit before timestamp returns commitVersion+1\") {\n    val timeZone = \"UTC\"\n    val commitTs = 1000L\n    val commitVersion = 2L\n    val latestVersion = 5L\n    val timestamp = new Timestamp(2000)\n    val result = DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n      timeZone, commitTs, commitVersion, latestVersion, timestamp)\n    assert(result == 3L)\n  }\n\n  test(\"getStartingVersionFromCommitAtTimestamp - \" +\n    \"timestamp after latest throws when canExceedLatest false\") {\n    val timeZone = \"UTC\"\n    val commitTs = 1000L\n    val commitVersion = 5L\n    val latestVersion = 5L\n    val timestamp = new Timestamp(2000)\n    val e = intercept[Exception] {\n      DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n        timeZone, commitTs, commitVersion, latestVersion, timestamp, canExceedLatest = false)\n    }\n    assert(e.getMessage.contains(\"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\"))\n  }\n\n  test(\"getStartingVersionFromCommitAtTimestamp - \" +\n    \"timestamp after latest returns commitVersion+1 when canExceedLatest true\") {\n    val timeZone = \"UTC\"\n    val commitTs = 1000L\n    val commitVersion = 5L\n    val latestVersion = 5L\n    val timestamp = new Timestamp(2000)\n    val result = DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n      timeZone, commitTs, commitVersion, latestVersion, timestamp, canExceedLatest = true)\n    assert(result == 6L)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, FileNotFoundException}\nimport java.util.concurrent.atomic.AtomicInteger\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.{Action, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite}\nimport org.apache.spark.sql.delta.files.TahoeLogFileIndex\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, FileNames}\nimport org.apache.spark.sql.delta.util.FileNames.unsafeDeltaFile\nimport org.apache.hadoop.fs.{FileSystem, FSDataInputStream, Path, PathHandle}\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.scheduler.{SparkListener, SparkListenerJobStart}\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.connector.catalog.TableCatalog\nimport org.apache.spark.sql.catalyst.expressions.InSet\nimport org.apache.spark.sql.catalyst.expressions.Literal.TrueLiteral\nimport org.apache.spark.sql.catalyst.plans.logical.Filter\nimport org.apache.spark.sql.execution.FileSourceScanExec\nimport org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelationWithTable}\nimport org.apache.spark.sql.functions.{asc, col, expr, lit, map_values, struct}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.StreamingQuery\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\nclass DeltaSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite {\n\n  import testImplicits._\n\n  private def tryDeleteNonRecursive(fs: FileSystem, path: Path): Boolean = {\n    try fs.delete(path, false) catch {\n      case _: FileNotFoundException => true\n    }\n  }\n\n  test(\"handle partition filters and data filters\") {\n    withTempDir { inputDir =>\n      val testPath = inputDir.getCanonicalPath\n      spark.range(10)\n        .map(_.toInt)\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .mode(\"append\")\n        .save(testPath)\n\n      val ds = spark.read.format(\"delta\").load(testPath).as[(Int, Int)]\n      // partition filter\n      checkDatasetUnorderly(\n        ds.where(\"part = 1\"),\n        1 -> 1, 3 -> 1, 5 -> 1, 7 -> 1, 9 -> 1)\n      checkDatasetUnorderly(\n        ds.where(\"part = 0\"),\n        0 -> 0, 2 -> 0, 4 -> 0, 6 -> 0, 8 -> 0)\n      // data filter\n      checkDatasetUnorderly(\n        ds.where(\"value >= 5\"),\n        5 -> 1, 6 -> 0, 7 -> 1, 8 -> 0, 9 -> 1)\n      checkDatasetUnorderly(\n        ds.where(\"value < 5\"),\n        0 -> 0, 1 -> 1, 2 -> 0, 3 -> 1, 4 -> 0)\n      // partition filter + data filter\n      checkDatasetUnorderly(\n        ds.where(\"part = 1 and value >= 5\"),\n        5 -> 1, 7 -> 1, 9 -> 1)\n      checkDatasetUnorderly(\n        ds.where(\"part = 1 and value < 5\"),\n        1 -> 1, 3 -> 1)\n    }\n  }\n\n  test(\"query with predicates should skip partitions\") {\n    withTempDir { tempDir =>\n      val testPath = tempDir.getCanonicalPath\n\n      // Generate two files in two partitions\n      spark.range(2)\n        .withColumn(\"part\", $\"id\" % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .mode(\"append\")\n        .save(testPath)\n\n      // Read only one partition\n      val query = spark.read.format(\"delta\").load(testPath).where(\"part = 1\")\n      val fileScans = query.queryExecution.executedPlan.collect {\n        case f: FileSourceScanExec => f\n      }\n\n      // Force the query to read files and generate metrics\n      query.queryExecution.executedPlan.execute().count()\n\n      // Verify only one file was read\n      assert(fileScans.size == 1)\n      val numFilesAferPartitionSkipping = fileScans.head.metrics.get(\"numFiles\")\n      assert(numFilesAferPartitionSkipping.nonEmpty)\n      assert(numFilesAferPartitionSkipping.get.value == 1)\n      checkAnswer(query, Seq(Row(1, 1)))\n    }\n  }\n\n  test(\"partition column location should not impact table schema\") {\n    val tableColumns = Seq(\"c1\", \"c2\")\n    for (partitionColumn <- tableColumns) {\n      withTempDir { inputDir =>\n        val testPath = inputDir.getCanonicalPath\n        Seq(1 -> \"a\", 2 -> \"b\").toDF(tableColumns: _*)\n          .write\n          .format(\"delta\")\n          .partitionBy(partitionColumn)\n          .save(testPath)\n        val ds = spark.read.format(\"delta\").load(testPath).as[(Int, String)]\n        checkDatasetUnorderly(ds, 1 -> \"a\", 2 -> \"b\")\n      }\n    }\n  }\n\n  test(\"SC-8078: read deleted directory\") {\n    val tempDir = Utils.createTempDir()\n    val path = new Path(tempDir.getCanonicalPath)\n    Seq(1).toDF().write.format(\"delta\").save(tempDir.toString)\n\n    val df = spark.read.format(\"delta\").load(tempDir.toString)\n    // scalastyle:off deltahadoopconfiguration\n    val fs = path.getFileSystem(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    fs.delete(path, true)\n\n    val e = intercept[AnalysisException] {\n      withSQLConf(DeltaSQLConf.DELTA_ASYNC_UPDATE_STALENESS_TIME_LIMIT.key -> \"0s\") {\n        checkAnswer(df, Row(1) :: Nil)\n      }\n    }.getMessage\n    assert(e.contains(\"The schema of your Delta table has changed\"))\n    val e2 = intercept[AnalysisException] {\n      withSQLConf(DeltaSQLConf.DELTA_ASYNC_UPDATE_STALENESS_TIME_LIMIT.key -> \"0s\") {\n        // Define new DataFrame\n        spark.read.format(\"delta\").load(tempDir.toString).collect()\n      }\n    }.getMessage\n    assert(e2.contains(\"Path does not exist\"))\n  }\n\n  test(\"SC-70676: directory deleted before first DataFrame is defined\") {\n    val tempDir = Utils.createTempDir()\n    val path = new Path(tempDir.getCanonicalPath)\n    Seq(1).toDF().write.format(\"delta\").save(tempDir.toString)\n\n    // scalastyle:off deltahadoopconfiguration\n    val fs = path.getFileSystem(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    fs.delete(path, true)\n\n    val e = intercept[AnalysisException] {\n      spark.read.format(\"delta\").load(tempDir.toString).collect()\n    }.getMessage\n    assert(e.contains(\"Path does not exist\"))\n  }\n\n  test(\"append then read\") {\n    val tempDir = Utils.createTempDir()\n    Seq(1).toDF().write.format(\"delta\").save(tempDir.toString)\n    Seq(2, 3).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n    def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n    checkAnswer(data, Row(1) :: Row(2) :: Row(3) :: Nil)\n\n    // append more\n    Seq(4, 5, 6).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n    checkAnswer(data.toDF(), Row(1) :: Row(2) :: Row(3) :: Row(4) :: Row(5) :: Row(6) :: Nil)\n  }\n\n  test(\"null struct with NullType field kept as null\") {\n    withTempTable(createTable = false) { tableName =>\n      Seq(((null, 2), 1), (null, 2)).toDF(\"key\", \"value\")\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      // Evolve the schema because tables with NullType columns cannot be read currently.\n      Seq(((10, 10), 10)).toDF(\"key\", \"value\")\n        .write\n        .format(\"delta\")\n        .option(\"mergeSchema\", \"true\")\n        .mode(\"append\")\n        .saveAsTable(tableName)\n\n      // Confirm struct value stays as null (fields are not set to null).\n      val rowWithNullStruct = spark.read.format(\"delta\").table(tableName).filter($\"value\" === 2)\n      checkAnswer(rowWithNullStruct, Row(null, 2) :: Nil)\n    }\n  }\n\n  test(\"null struct with NullType field, with backticks in the column name, kept as null\") {\n    withTempTable(createTable = false) { tableName =>\n      Seq(((null, 2), 1), (null, 2)).toDF(\"key`\", \"val`ue\")\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      // Evolve the schema because tables with NullType columns cannot be read currently.\n      Seq(((10, 10), 10)).toDF(\"key`\", \"val`ue\")\n        .write\n        .format(\"delta\")\n        .option(\"mergeSchema\", \"true\")\n        .mode(\"append\")\n        .saveAsTable(tableName)\n\n      // Confirm struct value stays as null (fields are not set to null).\n      val rowWithNullStruct = spark.read.format(\"delta\").table(tableName).filter($\"`val``ue`\" === 2)\n      checkAnswer(rowWithNullStruct, Row(null, 2) :: Nil)\n    }\n  }\n\n  test(\"Cannot create table with NullType UDT column\") {\n    val table_name = \"test_table\"\n    withTable(table_name) {\n      checkError(\n        intercept[DeltaAnalysisException] {\n          Seq((1, new NullData())).toDF(\"id\", \"value\")\n            .write.format(\"delta\").saveAsTable(table_name)\n        },\n        \"DELTA_USER_DEFINED_TYPE_COLUMN_CONTAINS_NULL_TYPE\",\n        sqlState = Some(\"22005\"),\n        parameters = Map(\"columnName\" -> \"value\", \"userClass\" -> classOf[NullData].getName)\n      )\n    }\n  }\n\n  test(\"Cannot create table with NullType in a complex UDT column\") {\n    val table_name = \"test_table\"\n    withTable(table_name) {\n      checkError(\n        intercept[DeltaAnalysisException] {\n          Seq((1, new ComplexData())).toDF(\"id\", \"value\")\n            .write.format(\"delta\").saveAsTable(table_name)\n        },\n        \"DELTA_USER_DEFINED_TYPE_COLUMN_CONTAINS_NULL_TYPE\",\n        sqlState = Some(\"22005\"),\n        parameters = Map(\"columnName\" -> \"value\", \"userClass\" -> classOf[ComplexData].getName)\n      )\n    }\n  }\n\n  test(\"partitioned append - nulls\") {\n    val tempDir = Utils.createTempDir()\n    Seq(Some(1), None).toDF()\n      .withColumn(\"is_odd\", $\"value\" % 2 === 1)\n      .write\n      .format(\"delta\")\n      .partitionBy(\"is_odd\")\n      .save(tempDir.toString)\n\n    val df = spark.read.format(\"delta\").load(tempDir.toString)\n\n    // Verify the correct partitioning schema is picked up\n    val hadoopFsRelations = df.queryExecution.analyzed.collect {\n      case LogicalRelationWithTable(h: HadoopFsRelation, _) => h\n    }\n    assert(hadoopFsRelations.size === 1)\n    assert(hadoopFsRelations.head.partitionSchema.exists(_.name == \"is_odd\"))\n    assert(hadoopFsRelations.head.dataSchema.exists(_.name == \"value\"))\n\n    checkAnswer(df.where(\"is_odd = true\"), Row(1, true) :: Nil)\n    checkAnswer(df.where(\"is_odd IS NULL\"), Row(null, null) :: Nil)\n  }\n\n  test(\"input files should be absolute paths\") {\n    withTempDir { dir =>\n      val basePath = dir.getAbsolutePath\n      spark.range(10).withColumn(\"part\", 'id % 3)\n        .write.format(\"delta\").partitionBy(\"part\").save(basePath)\n\n      val df1 = spark.read.format(\"delta\").load(basePath)\n      val df2 = spark.read.format(\"delta\").load(basePath).where(\"part = 1\")\n      val df3 = spark.read.format(\"delta\").load(basePath).where(\"part = 1\").limit(3)\n\n      assert(df1.inputFiles.forall(_.contains(basePath)))\n      assert(df2.inputFiles.forall(_.contains(basePath)))\n      assert(df3.inputFiles.forall(_.contains(basePath)))\n    }\n  }\n\n  test(\"invalid replaceWhere\") {\n    Seq(true, false).foreach { enabled =>\n      withSQLConf(DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED.key -> enabled.toString) {\n        val tempDir = Utils.createTempDir()\n        Seq(1, 2, 3, 4).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"is_odd\")\n          .save(tempDir.toString)\n        val e1 = intercept[AnalysisException] {\n          Seq(6).toDF()\n            .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n            .save(tempDir.toString)\n        }.getMessage\n        assert(e1.contains(\"does not conform to partial table overwrite condition or constraint\"))\n\n        val e2 = intercept[AnalysisException] {\n          Seq(true).toDF(\"is_odd\")\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n            .save(tempDir.toString)\n        }.getMessage\n        assert(e2.contains(\n          \"Data written into Delta needs to contain at least one non-partitioned\"))\n\n        val e3 = intercept[AnalysisException] {\n          Seq(6).toDF()\n            .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"not_a_column = true\")\n            .save(tempDir.toString)\n        }.getMessage\n        if (enabled) {\n          assert(e3.contains(\n            \"or function parameter with name `not_a_column` cannot be resolved\") ||\n            e3.contains(\"Column 'not_a_column' does not exist. Did you mean one of \" +\n              \"the following? [value, is_odd]\"))\n        } else {\n          assert(e3.contains(\n            \"Predicate references non-partition column 'not_a_column'. Only the \" +\n              \"partition columns may be referenced: [is_odd]\"))\n        }\n\n        val e4 = intercept[AnalysisException] {\n          Seq(6).toDF()\n            .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"value = 1\")\n            .save(tempDir.toString)\n        }.getMessage\n        if (enabled) {\n          assert(e4.contains(\n            \"Written data does not conform to partial table overwrite condition \" +\n              \"or constraint 'value = 1'\"))\n        } else {\n          assert(e4.contains(\"Predicate references non-partition column 'value'. Only the \" +\n            \"partition columns may be referenced: [is_odd]\"))\n        }\n\n        val e5 = intercept[AnalysisException] {\n          Seq(6).toDF()\n            .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"\")\n            .save(tempDir.toString)\n        }.getMessage\n        assert(e5.contains(\"Cannot recognize the predicate ''\"))\n      }\n    }\n  }\n\n  test(\"replaceWhere with rearrangeOnly\") {\n    withTempDir { dir =>\n      Seq(1, 2, 3, 4).toDF()\n        .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"is_odd\")\n        .save(dir.toString)\n\n      // dataFilter non empty\n      val e = intercept[AnalysisException] {\n        Seq(9).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true and value < 2\")\n          .option(DeltaOptions.DATA_CHANGE_OPTION, \"false\")\n          .save(dir.toString)\n      }.getMessage\n      assert(e.contains(\n        \"'replaceWhere' cannot be used with data filters when 'dataChange' is set to false\"))\n\n      Seq(9).toDF()\n        .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n        .option(DeltaOptions.DATA_CHANGE_OPTION, \"false\")\n        .save(dir.toString)\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir.toString),\n        Seq(2, 4, 9).toDF().withColumn(\"is_odd\", $\"value\" % 2 =!= 0))\n    }\n  }\n\n  test(\"valid replaceWhere\") {\n    Seq(true, false).foreach { enabled =>\n      withSQLConf(DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED.key -> enabled.toString) {\n        Seq(true, false).foreach { partitioned =>\n          // Skip when it's not enabled and not partitioned.\n          if (enabled || partitioned) {\n            withTempDir { dir =>\n              val writer = Seq(1, 2, 3, 4).toDF()\n                .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                .write\n                .format(\"delta\")\n\n              if (partitioned) {\n                writer.partitionBy(\"is_odd\").save(dir.toString)\n              } else {\n                writer.save(dir.toString)\n              }\n\n              def data: DataFrame = spark.read.format(\"delta\").load(dir.toString)\n\n              Seq(5, 7).toDF()\n                .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                .write\n                .format(\"delta\")\n                .mode(\"overwrite\")\n                .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n                .save(dir.toString)\n              checkAnswer(\n                data,\n                Seq(2, 4, 5, 7).toDF()\n                  .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                  .withColumn(\"is_even\", $\"value\" % 2 === 0))\n\n              // replaceWhere on non-partitioning columns if enabled.\n              if (enabled) {\n                Seq(6, 8).toDF()\n                  .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                  .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                  .write\n                  .format(\"delta\")\n                  .mode(\"overwrite\")\n                  .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_even = true\")\n                  .save(dir.toString)\n                checkAnswer(\n                  data,\n                  Seq(5, 6, 7, 8).toDF()\n                    .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                    .withColumn(\"is_even\", $\"value\" % 2 === 0))\n\n                // nothing to be replaced because the condition is false.\n                Seq(10, 12).toDF()\n                  .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                  .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                  .write\n                  .format(\"delta\")\n                  .mode(\"overwrite\")\n                  .option(DeltaOptions.REPLACE_WHERE_OPTION, \"1 = 2\")\n                  .save(dir.toString)\n                checkAnswer(\n                  data,\n                  Seq(5, 6, 7, 8, 10, 12).toDF()\n                    .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                    .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                )\n\n                // replace the whole thing because the condition is true.\n                Seq(10, 12).toDF()\n                  .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                  .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                  .write\n                  .format(\"delta\")\n                  .mode(\"overwrite\")\n                  .option(DeltaOptions.REPLACE_WHERE_OPTION, \"1 = 1\")\n                  .save(dir.toString)\n                checkAnswer(\n                  data,\n                  Seq(10, 12).toDF()\n                    .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n                    .withColumn(\"is_even\", $\"value\" % 2 === 0)\n                )\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n    Seq(false, true).foreach { replaceWhereInDataColumn =>\n      test(s\"valid replaceWhere with cdf enabled, \" +\n        s\"replaceWhereInDataColumn = $replaceWhereInDataColumn\") {\n        testReplaceWhereWithCdf(\n          replaceWhereInDataColumn)\n      }\n    }\n\n  def testReplaceWhereWithCdf(\n    replaceWhereInDataColumn: Boolean): Unit = {\n      withSQLConf(\n        DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED.key -> replaceWhereInDataColumn.toString,\n        DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n        withTempDir { dir =>\n          Seq(1, 2, 3, 4).map(i => (i, i + 2)).toDF(\"key\", \"value.1\")\n            .withColumn(\"is_odd\", $\"`value.1`\" % 2 =!= 0)\n            .withColumn(\"is_even\", $\"`value.1`\" % 2 === 0)\n            .coalesce(1)\n            .write\n            .format(\"delta\")\n            .partitionBy(\"is_odd\").save(dir.toString)\n\n          checkAnswer(\n            CDCReader.changesToBatchDF(DeltaLog.forTable(spark, dir), 0, 0, spark)\n              .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n            Row(1, 3, true, false, \"insert\", 0) :: Row(3, 5, true, false, \"insert\", 0) ::\n              Row(2, 4, false, true, \"insert\", 0) :: Row(4, 6, false, true, \"insert\", 0) :: Nil)\n\n          def data: DataFrame = spark.read.format(\"delta\").load(dir.toString)\n\n          Seq(5, 7).map(i => (i, i + 2)).toDF(\"key\", \"value.1\")\n            .withColumn(\"is_odd\", $\"`value.1`\" % 2 =!= 0)\n            .withColumn(\"is_even\", $\"`value.1`\" % 2 === 0)\n            .coalesce(1)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n            .save(dir.toString)\n          checkAnswer(\n            data,\n            Seq(2, 4, 5, 7).map(i => (i, i + 2)).toDF(\"key\", \"value.1\")\n              .withColumn(\"is_odd\", $\"`value.1`\" % 2 =!= 0)\n              .withColumn(\"is_even\", $\"`value.1`\" % 2 === 0))\n\n          checkAnswer(\n            CDCReader.changesToBatchDF(DeltaLog.forTable(spark, dir), 1, 1, spark)\n              .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n            Row(1, 3, true, false, \"delete\", 1) :: Row(3, 5, true, false, \"delete\", 1) ::\n              Row(5, 7, true, false, \"insert\", 1) :: Row(7, 9, true, false, \"insert\", 1) :: Nil)\n\n          if (replaceWhereInDataColumn) {\n            // replaceWhere on non-partitioning columns if enabled.\n            Seq((4, 8)).toDF(\"key\", \"value.1\")\n              .withColumn(\"is_odd\", $\"`value.1`\" % 2 =!= 0)\n              .withColumn(\"is_even\", $\"`value.1`\" % 2 === 0)\n              .write\n              .format(\"delta\")\n              .mode(\"overwrite\")\n              .option(DeltaOptions.REPLACE_WHERE_OPTION, \"key = 4\")\n              .save(dir.toString)\n            checkAnswer(\n              data,\n              Seq((2, 4), (4, 8), (5, 7), (7, 9)).toDF(\"key\", \"value.1\")\n                .withColumn(\"is_odd\", $\"`value.1`\" % 2 =!= 0)\n                .withColumn(\"is_even\", $\"`value.1`\" % 2 === 0))\n\n            checkAnswer(\n              CDCReader.changesToBatchDF(DeltaLog.forTable(spark, dir), 2, 2, spark)\n                .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n              Row(4, 6, false, true, \"delete\", 2) :: Row(4, 8, false, true, \"insert\", 2) :: Nil)\n          }\n        }\n    }\n  }\n\n  test(\"replace arbitrary with multiple references\") {\n    withTempDir { dir =>\n      def data: DataFrame = spark.read.format(\"delta\").load(dir.toString)\n\n      Seq((1, 3, 8), (1, 5, 9)).toDF(\"a\", \"b\", \"c\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(dir.toString)\n\n      Seq((2, 4, 6)).toDF(\"a\", \"b\", \"c\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .option(DeltaOptions.REPLACE_WHERE_OPTION, \"a + c < 10\")\n        .save(dir.toString)\n\n      checkAnswer(\n        data,\n        Seq((1, 5, 9), (2, 4, 6)).toDF(\"a\", \"b\", \"c\"))\n    }\n  }\n\n  test(\"replaceWhere with constraint check disabled\") {\n    withSQLConf(DeltaSQLConf.REPLACEWHERE_CONSTRAINT_CHECK_ENABLED.key -> \"false\") {\n      withTempDir { dir =>\n        Seq(1, 2, 3, 4).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"is_odd\")\n          .save(dir.toString)\n\n        def data: DataFrame = spark.read.format(\"delta\").load(dir.toString)\n\n        Seq(6).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n          .save(dir.toString)\n\n        checkAnswer(data, Seq(2, 4, 6).toDF().withColumn(\"is_odd\", $\"value\" % 2 =!= 0))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { p =>\n    test(s\"replaceWhere user defined _change_type column doesn't get dropped - partitioned=$p\") {\n      withTable(\"tab\") {\n        sql(\n          s\"\"\"CREATE TABLE tab USING DELTA\n             |${if (p) \"PARTITIONED BY (part) \" else \"\"}\n             |TBLPROPERTIES (delta.enableChangeDataFeed = false)\n             |AS SELECT id, floor(id / 10) AS part, 'foo' as _change_type\n             |FROM RANGE(1000)\n             |\"\"\".stripMargin)\n        Seq(33L).map(id => id * 42).toDF(\"id\")\n          .withColumn(\"part\", expr(\"floor(id / 10)\"))\n          .withColumn(\"_change_type\", lit(\"bar\"))\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"id % 7 = 0\")\n          .saveAsTable(\"tab\")\n\n        sql(\"SELECT id, _change_type FROM tab\").collect().foreach { row =>\n          val _change_type = row.getString(1)\n          assert(_change_type === \"foo\" || _change_type === \"bar\",\n            s\"Invalid _change_type for id=${row.get(0)}\")\n        }\n      }\n    }\n  }\n\n  test(\"move delta table\") {\n    val tempDir = Utils.createTempDir()\n    Seq(1, 2, 3).toDS().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n    def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n    checkAnswer(data.toDF(), Row(1) :: Row(2) :: Row(3) :: Nil)\n\n    // Append files in log path should use relative paths and should work with file renaming.\n    val targetDir = new File(Utils.createTempDir(), \"target\")\n    assert(tempDir.renameTo(targetDir))\n\n    def data2: DataFrame = spark.read.format(\"delta\").load(targetDir.toString)\n    checkDatasetUnorderly(data2.toDF().as[Int], 1, 2, 3)\n  }\n\n  test(\"append table to itself\") {\n    val tempDir = Utils.createTempDir()\n    Seq(1, 2, 3).toDS().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n    def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n    checkDatasetUnorderly(data.toDF.as[Int], 1, 2, 3)\n    data.write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n    checkDatasetUnorderly(data.toDF.as[Int], 1, 1, 2, 2, 3, 3)\n  }\n\n  test(\"missing partition columns\") {\n    val tempDir = Utils.createTempDir()\n    Seq(1, 2, 3).toDF()\n      .withColumn(\"part\", $\"value\" % 2)\n      .write\n      .format(\"delta\")\n      .partitionBy(\"part\")\n      .save(tempDir.toString)\n\n    val e = intercept[Exception] {\n      Seq(1, 2, 3).toDF()\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tempDir.toString)\n    }\n    assert(e.getMessage contains \"Partition column\")\n    assert(e.getMessage contains \"part\")\n    assert(e.getMessage contains \"not found\")\n  }\n\n  test(\"batch write: append, overwrite\") {\n    withTempDir { tempDir =>\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n      Seq(1, 2, 3).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.as[Int], 1, 2, 3)\n\n      Seq(4, 5, 6).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.as[Int], 4, 5, 6)\n    }\n  }\n\n  test(\"batch write: overwrite an empty directory with replaceWhere\") {\n    withTempDir { tempDir =>\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n      Seq (1, 3, 5).toDF\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .partitionBy(\"part\")\n        .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 1\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.as[(Int, Int)], 1 -> 1, 3 -> 1, 5 -> 1)\n    }\n  }\n\n  test(\"batch write: append, overwrite where\") {\n    withTempDir { tempDir =>\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n      Seq (1, 2, 3).toDF\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .mode(\"append\")\n        .save(tempDir.getCanonicalPath)\n\n      Seq(1, 5).toDF\n        .withColumn(\"part\", $\"value\" % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .mode(\"overwrite\")\n        .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part=1\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.select($\"value\".as[Int]), 1, 2, 5)\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite integer partition column\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 2, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite string partition column\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq((\"a\", \"x\"), (\"b\", \"y\"), (\"c\", \"x\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq((\"a\", \"x\"), (\"d\", \"x\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[String], \"a\", \"b\", \"d\")\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite string and integer partition column\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq((1, \"x\"), (2, \"y\"), (3, \"z\")).toDF(\"value\", \"part2\")\n          .withColumn(\"part1\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq((5, \"x\"), (7, \"y\")).toDF(\"value\", \"part2\")\n          .withColumn(\"part1\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 2, 3, 5, 7)\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite overwrites nothing\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq((\"a\", \"x\"), (\"b\", \"y\"), (\"c\", \"x\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq((\"d\", \"z\")).toDF(\"value\", \"part\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\", \"part\").as[(String, String)],\n          (\"a\", \"x\"), (\"b\", \"y\"), (\"c\", \"x\"), (\"d\", \"z\"))\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite multiple partition columns\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq((\"a\", \"x\", 1), (\"b\", \"y\", 2), (\"c\", \"x\", 3)).toDF(\"part1\", \"part2\", \"value\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq((\"a\", \"x\", 4), (\"d\", \"x\", 5)).toDF(\"part1\", \"part2\", \"value\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"part1\", \"part2\", \"value\").as[(String, String, Int)],\n          (\"a\", \"x\", 4), (\"b\", \"y\", 2), (\"c\", \"x\", 3), (\"d\", \"x\", 5))\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite without partitionBy\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 2, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite conf, replaceWhere takes precedence\") {\n    // when dynamic partition overwrite mode is enabled in the spark configuration, and a\n    // replaceWhere expression is provided, we delete data according to the replaceWhere expression\n    withSQLConf(\n      DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\",\n      SQLConf.PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq((1, \"x\"), (2, \"y\"), (3, \"z\")).toDF(\"value\", \"part2\")\n          .withColumn(\"part1\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq((5, \"x\")).toDF(\"value\", \"part2\")\n          .withColumn(\"part1\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part1 = 1\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select($\"value\").as[Int], 2, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, replaceWhere + dynamic partition overwrite enabled in options\") {\n    // when dynamic partition overwrite mode is enabled in the DataFrameWriter options, and\n    // a replaceWhere expression is provided, we throw an error\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        Seq((1, \"x\"), (2, \"y\"), (3, \"z\")).toDF(\"value\", \"part2\")\n          .withColumn(\"part1\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        val e = intercept[IllegalArgumentException] {\n          Seq((3, \"x\"), (5, \"x\")).toDF(\"value\", \"part2\")\n            .withColumn(\"part1\", $\"value\" % 2)\n            .write\n            .format(\"delta\")\n            .partitionBy(\"part1\", \"part2\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part1 = 1\")\n            .save(tempDir.getCanonicalPath)\n        }\n        assert(e.getMessage === \"[DELTA_REPLACE_WHERE_WITH_DYNAMIC_PARTITION_OVERWRITE] \" +\n          \"A 'replaceWhere' expression and \" +\n          \"'partitionOverwriteMode'='dynamic' cannot both be set in the DataFrameWriter options.\")\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite set via conf\") {\n    withSQLConf(\n      DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\",\n      SQLConf.PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"overwrite\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 2, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition overwrite set via conf and overridden via option\") {\n    withSQLConf(\n      DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\",\n      SQLConf.PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"static\")\n          .mode(\"overwrite\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, overwrite without partitions should ignore partition overwrite mode\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, overwrite non-partitioned table with replaceWhere ignores partition \" +\n    \"overwrite mode option\") {\n    // we check here that setting both replaceWhere and dynamic partition overwrite in the\n    // DataFrameWriter options is allowed for a non-partitioned table\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 1\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 2, 5)\n      }\n    }\n  }\n\n  test(\"batch write: append, dynamic partition with 'partitionValues' column\") {\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq(1, 2, 3).toDF\n          .withColumn(\"partitionValues\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"partitionValues\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        Seq(1, 5).toDF\n          .withColumn(\"partitionValues\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"partitionValues\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"value\").as[Int], 1, 2, 5)\n      }\n    }\n  }\n\n  test(\"batch write: ignore\") {\n    withTempDir { tempDir =>\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n      Seq(1, 2, 3).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"ignore\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.as[Int], 1, 2, 3)\n\n      // The following data will be ignored\n      Seq(4, 5, 6).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"ignore\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.as[Int], 1, 2, 3)\n    }\n  }\n\n  test(\"batch write: error\") {\n    withTempDir { tempDir =>\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n      Seq(1, 2, 3).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"error\")\n        .save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(data.toDF.as[Int], 1, 2, 3)\n\n      val e = intercept[AnalysisException] {\n        Seq(4, 5, 6).toDF\n          .write\n          .format(\"delta\")\n          .mode(\"error\")\n          .save(tempDir.getCanonicalPath)\n      }\n      assert(e.getMessage.contains(\"Cannot write to already existent path\"))\n    }\n  }\n\n  testQuietly(\"creating log should not create the log directory\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      // Creating an empty log should not create the directory\n      assert(!tempDir.exists())\n\n      // Writing to table should create the directory\n      Seq(1, 2, 3).toDF\n        .write\n        .format(\"delta\")\n        .save(tempDir.getCanonicalPath)\n\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n      checkDatasetUnorderly(data.toDF.as[Int], 1, 2, 3)\n    }\n  }\n\n  test(\"read via data source API when the directory doesn't exist\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      // a batch query should fail at once\n      var e = intercept[AnalysisException] {\n        spark.read\n          .format(\"delta\")\n          .load(tempDir.getCanonicalPath)\n          .show()\n      }\n\n      assert(e.getMessage.contains(\"Path does not exist\"))\n      assert(e.getMessage.contains(tempDir.getCanonicalPath))\n\n      assert(!tempDir.exists())\n\n      // a streaming query will also fail but it's because there is no schema\n      e = intercept[AnalysisException] {\n        spark.readStream\n          .format(\"delta\")\n          .load(tempDir.getCanonicalPath)\n      }\n      assert(e.getMessage.contains(\"Table schema is not set\"))\n      assert(e.getMessage.contains(\"CREATE TABLE\"))\n    }\n  }\n\n  test(\"write via data source API when the directory doesn't exist\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      // a batch query should create the output directory automatically\n      Seq(1, 2, 3).toDF\n        .write\n        .format(\"delta\").save(tempDir.getCanonicalPath)\n      checkDatasetUnorderly(\n        spark.read.format(\"delta\").load(tempDir.getCanonicalPath).as[Int],\n        1, 2, 3)\n\n      Utils.deleteRecursively(tempDir)\n      assert(!tempDir.exists())\n\n      // a streaming query should create the output directory automatically\n      val input = MemoryStream[Int]\n      val q = input.toDF\n        .writeStream\n        .format(\"delta\")\n        .option(\n          \"checkpointLocation\",\n          Utils.createTempDir(namePrefix = \"tahoe-test\").getCanonicalPath)\n        .start(tempDir.getCanonicalPath)\n      try {\n        input.addData(1, 2, 3)\n        q.processAllAvailable()\n        checkDatasetUnorderly(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath).as[Int],\n          1, 2, 3)\n      } finally {\n        q.stop()\n      }\n    }\n  }\n\n  test(\"support partitioning with batch data source API - append\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      spark.range(100).select('id, 'id % 4 as \"by4\", 'id % 8 as \"by8\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"by4\", \"by8\")\n        .save(tempDir.toString)\n\n      val files = spark.read.format(\"delta\").load(tempDir.toString).inputFiles\n\n      val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n      assertPartitionExists(\"by4\", deltaLog, files)\n      assertPartitionExists(\"by8\", deltaLog, files)\n    }\n  }\n\n  test(\"support removing partitioning\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      spark.range(100).select('id, 'id % 4 as \"by4\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"by4\")\n        .save(tempDir.toString)\n\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      assert(deltaLog.snapshot.metadata.partitionColumns === Seq(\"by4\"))\n\n      spark.read.format(\"delta\").load(tempDir.toString).write\n        .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n        .format(\"delta\")\n        .mode(SaveMode.Overwrite)\n        .save(tempDir.toString)\n\n      assert(deltaLog.snapshot.metadata.partitionColumns === Nil)\n    }\n  }\n\n  test(\"columns with commas as partition columns\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      val dfw = spark.range(100).select('id, 'id % 4 as \"by,4\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"by,4\")\n\n      // if in column mapping mode, we should not expect invalid character errors\n      if (!columnMappingEnabled) {\n        val e = intercept[AnalysisException] {\n          dfw.save(tempDir.toString)\n        }\n        assert(e.getMessage.contains(\"invalid character(s)\"))\n      }\n\n      withSQLConf(DeltaSQLConf.DELTA_PARTITION_COLUMN_CHECK_ENABLED.key -> \"false\") {\n        dfw.save(tempDir.toString)\n      }\n\n      // Note: although we are able to write, we cannot read the table with Spark 3.2+ with\n      // OSS Delta 1.1.0+ because SPARK-36271 adds a column name check in the read path.\n    }\n  }\n\n  test(\"throw exception when users are trying to write in batch with different partitioning\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      spark.range(100).select('id, 'id % 4 as \"by4\", 'id % 8 as \"by8\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"by4\", \"by8\")\n        .save(tempDir.toString)\n\n      val e = intercept[AnalysisException] {\n        spark.range(100).select('id, 'id % 4 as \"by4\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"by4\")\n          .mode(\"append\")\n          .save(tempDir.toString)\n      }\n      assert(e.getMessage.contains(\"Partition columns do not match\"))\n    }\n  }\n\n  test(\"incompatible schema merging throws errors\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      spark.range(100).select('id, ('id * 3).cast(\"string\") as \"value\")\n        .write\n        .format(\"delta\")\n        .save(tempDir.toString)\n\n      val e = intercept[AnalysisException] {\n        spark.range(100).select('id, 'id * 3 as \"value\")\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.toString)\n      }\n      checkError(\n        e,\n        \"DELTA_FAILED_TO_MERGE_FIELDS\",\n        parameters = Map(\"currentField\" -> \"value\", \"updateField\" -> \"value\"))\n    }\n  }\n\n  test(\"support partitioning with batch data source API - overwrite\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      spark.range(100).select('id, 'id % 4 as \"by4\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"by4\")\n        .save(tempDir.toString)\n\n      val files = spark.read.format(\"delta\").load(tempDir.toString).inputFiles\n\n      val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n      assertPartitionExists(\"by4\", deltaLog, files)\n\n      spark.range(101, 200).select('id, 'id % 4 as \"by4\", 'id % 8 as \"by8\")\n        .write\n        .format(\"delta\")\n        .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"true\")\n        .mode(\"overwrite\")\n        .save(tempDir.toString)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir.toString),\n        spark.range(101, 200).select('id, 'id % 4 as \"by4\", 'id % 8 as \"by8\"))\n    }\n  }\n\n  test(\"overwrite and replaceWhere should check partitioning compatibility\") {\n    withTempDir { tempDir =>\n      if (tempDir.exists()) {\n        assert(tempDir.delete())\n      }\n\n      spark.range(100).select('id, 'id % 4 as \"by4\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"by4\")\n        .save(tempDir.toString)\n\n      val files = spark.read.format(\"delta\").load(tempDir.toString).inputFiles\n\n      val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n      assertPartitionExists(\"by4\", deltaLog, files)\n\n      val e = intercept[AnalysisException] {\n        spark.range(101, 200).select('id, 'id % 4 as \"by4\", 'id % 8 as \"by8\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"by4\", \"by8\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"by4 > 0\")\n          .mode(\"overwrite\")\n          .save(tempDir.toString)\n      }\n      assert(e.getMessage.contains(\"Partition columns do not match\"))\n    }\n  }\n\n  test(\"can't write out with all columns being partition columns\") {\n    withTempDir { tempDir =>\n      SaveMode.values().foreach { mode =>\n        if (tempDir.exists()) {\n          assert(tempDir.delete())\n        }\n\n        val e = intercept[AnalysisException] {\n          spark.range(100).select('id, 'id % 4 as \"by4\")\n            .write\n            .format(\"delta\")\n            .partitionBy(\"by4\", \"id\")\n            .mode(mode)\n            .save(tempDir.toString)\n        }\n        assert(e.getMessage.contains(\"Cannot use all columns for partition columns\"))\n      }\n    }\n  }\n\n  test(\"SC-8727 - default snapshot num partitions\") {\n    withTempDir { tempDir =>\n      spark.range(10).write.format(\"delta\").save(tempDir.toString)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      val numParts = spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_SNAPSHOT_PARTITIONS).get\n      assert(deltaLog.snapshot.stateDS.rdd.getNumPartitions == numParts)\n    }\n  }\n\n  test(\"SC-8727 - can't set negative num partitions\") {\n    withTempDir { tempDir =>\n      val caught = intercept[IllegalArgumentException] {\n        withSQLConf((\"spark.databricks.delta.snapshotPartitions\", \"-1\")) {}\n      }\n\n      assert(caught.getMessage.contains(\"Delta snapshot partition number must be positive.\"))\n    }\n  }\n\n  test(\"SC-8727 - reconfigure num partitions\") {\n    withTempDir { tempDir =>\n      withSQLConf((\"spark.databricks.delta.snapshotPartitions\", \"410\")) {\n        spark.range(10).write.format(\"delta\").save(tempDir.toString)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        assert(deltaLog.snapshot.stateDS.rdd.getNumPartitions == 410)\n      }\n    }\n  }\n\n  test(\"SC-8727 - can't set zero num partitions\") {\n    withTempDir { tempDir =>\n      val caught = intercept[IllegalArgumentException] {\n        withSQLConf((\"spark.databricks.delta.snapshotPartitions\", \"0\")) {}\n      }\n\n      assert(caught.getMessage.contains(\"Delta snapshot partition number must be positive.\"))\n    }\n  }\n\n  testQuietly(\"SC-8810: skip deleted file\") {\n    withSQLConf(\n      (\"spark.sql.files.ignoreMissingFiles\", \"true\")) {\n      withTempDir { tempDir =>\n        val tempDirPath = new Path(tempDir.getCanonicalPath)\n        Seq(1).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n        Seq(2, 2).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n        Seq(4).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n        Seq(5).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n        // The file names are opaque. To identify which one we're deleting, we ensure that only one\n        // append has 2 partitions, and give them the same value so we know what was deleted.\n        val inputFiles = TahoeLogFileIndex(spark, deltaLog).inputFiles.toSeq\n        assert(inputFiles.size == 5)\n\n        val filesToDelete = inputFiles.filter(_.split(\"/\").last.contains(\"part-00001\"))\n        assert(filesToDelete.size == 1)\n        filesToDelete.foreach { f =>\n          val deleted = tryDeleteNonRecursive(\n            tempDirPath.getFileSystem(deltaLog.newDeltaHadoopConf()),\n            new Path(tempDirPath, f))\n          assert(deleted)\n        }\n\n        // The single 2 that we deleted should be missing, with the rest of the data still present.\n        checkAnswer(data.toDF(), Row(1) :: Row(2) :: Row(4) :: Row(5) :: Nil)\n      }\n    }\n  }\n\n\n  testQuietly(\"SC-8810: skipping deleted file still throws on corrupted file\") {\n    withSQLConf((\"spark.sql.files.ignoreMissingFiles\", \"true\")) {\n      withTempDir { tempDir =>\n        val tempDirPath = new Path(tempDir.getCanonicalPath)\n        Seq(1).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n        Seq(2, 2).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n        Seq(4).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n        Seq(5).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n        // The file names are opaque. To identify which one we're deleting, we ensure that only one\n        // append has 2 partitions, and give them the same value so we know what was deleted.\n        val inputFiles = TahoeLogFileIndex(spark, deltaLog).inputFiles.toSeq\n        assert(inputFiles.size == 5)\n\n        val filesToCorrupt = inputFiles.filter(_.split(\"/\").last.contains(\"part-00001\"))\n        assert(filesToCorrupt.size == 1)\n        val fs = tempDirPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n        filesToCorrupt.foreach { f =>\n          val filePath = new Path(tempDirPath, f)\n          fs.create(filePath, true).close()\n        }\n\n        val thrown = intercept[SparkException] {\n          data.toDF().collect()\n        }\n        assert(thrown.getMessage.contains(\"[FAILED_READ_FILE.NO_HINT]\"))\n      }\n    }\n  }\n\n  testQuietly(\"SC-8810: skip multiple deleted files\") {\n    withSQLConf((\"spark.sql.files.ignoreMissingFiles\", \"true\")) {\n      withTempDir { tempDir =>\n        val tempDirPath = new Path(tempDir.getCanonicalPath)\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n        Range(0, 10).foreach(n =>\n          Seq(n).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString))\n\n        val inputFiles = TahoeLogFileIndex(spark, deltaLog).inputFiles.toSeq\n\n        val filesToDelete = inputFiles.take(4)\n        filesToDelete.foreach { f =>\n          val deleted = tryDeleteNonRecursive(\n            tempDirPath.getFileSystem(deltaLog.newDeltaHadoopConf()),\n            new Path(tempDirPath, f))\n          assert(deleted)\n        }\n\n        // We don't have a good way to tell which specific values got deleted, so just check that\n        // the right number remain. (Note that this works because there's 1 value per append, which\n        // means 1 value per file.)\n        assert(data.toDF().collect().size == 6)\n      }\n    }\n  }\n\n  testQuietly(\"deleted files cause failure by default\") {\n    withTempDir { tempDir =>\n      val tempDirPath = new Path(tempDir.getCanonicalPath)\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n      Range(0, 10).foreach(n =>\n        Seq(n).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString))\n\n      val inputFiles = TahoeLogFileIndex(spark, deltaLog).inputFiles.toSeq\n      val fileToDelete = inputFiles.head\n      val pathToDelete = new Path(tempDirPath, fileToDelete)\n      val deleted = tryDeleteNonRecursive(\n        tempDirPath.getFileSystem(deltaLog.newDeltaHadoopConf()), pathToDelete)\n      assert(deleted)\n\n      val thrown = intercept[SparkException] {\n        data.toDF().collect()\n      }\n      assert(thrown.getMessage.contains(\"[FAILED_READ_FILE.FILE_NOT_EXIST]\"))\n    }\n  }\n\n\n  test(\"ES-4716: Delta shouldn't be broken when users turn on case sensitivity\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      withTempDir { tempDir =>\n        // We use a column with the weird name just to make sure that customer configurations still\n        // work. The original bug was within the `Snapshot` code, where we referred to `metaData`\n        // as `metadata`.\n        Seq(1, 2, 3).toDF(\"aBc\").write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n        def testDf(columnName: Symbol): Unit = {\n          DeltaLog.clearCache()\n          val df = spark.read.format(\"delta\").load(tempDir.getCanonicalPath).select(columnName)\n          checkDatasetUnorderly(df.as[Int], 1, 2, 3)\n        }\n\n        withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n          testDf('aBc)\n\n          intercept[AnalysisException] {\n            testDf('abc)\n          }\n        }\n        testDf('aBc)\n        testDf('abc)\n      }\n    }\n  }\n\n  test(\"special chars in base path\") {\n    withTempDir { dir =>\n      val basePath = new File(new File(dir, \"some space\"), \"and#spec*al+ch@rs\")\n      spark.range(10).write.format(\"delta\").save(basePath.getCanonicalPath)\n      checkAnswer(\n        spark.read.format(\"delta\").load(basePath.getCanonicalPath),\n        spark.range(10).toDF()\n      )\n    }\n  }\n\n  test(\"get touched files for update, delete and merge\") {\n    withTempDir { dir =>\n      val directory = new File(dir, \"test with space\")\n      val df = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n      val writer = df.write.format(\"delta\").mode(\"append\")\n      writer.save(directory.getCanonicalPath)\n      spark.sql(s\"UPDATE delta.`${directory.getCanonicalPath}` SET value = value + 10\")\n      spark.sql(s\"DELETE FROM delta.`${directory.getCanonicalPath}` WHERE key = 4\")\n      Seq((3, 30)).toDF(\"key\", \"value\").createOrReplaceTempView(\"inbound\")\n      spark.sql(s\"\"\"|MERGE INTO delta.`${directory.getCanonicalPath}` AS base\n                       |USING inbound\n                       |ON base.key = inbound.key\n                       |WHEN MATCHED THEN UPDATE SET base.value =\n                       |base.value+inbound.value\"\"\".stripMargin)\n      spark.sql(s\"UPDATE delta.`${directory.getCanonicalPath}` SET value = 40 WHERE key = 1\")\n      spark.sql(s\"DELETE FROM delta.`${directory.getCanonicalPath}` WHERE key = 2\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(directory.getCanonicalPath),\n        Seq((1, 40), (3, 70)).toDF(\"key\", \"value\")\n      )\n    }\n  }\n\n  test(\"support Java8 API for DATE type\") {\n    withSQLConf(SQLConf.DATETIME_JAVA8API_ENABLED.key -> \"true\") {\n      val tableName = \"my_table\"\n      withTable(tableName) {\n        spark.sql(s\"CREATE TABLE $tableName (id STRING, date DATE) USING DELTA;\")\n        spark.sql(\n          s\"\"\"\n             |INSERT INTO $tableName REPLACE\n             |where (DATE IN (DATE('2024-03-11'), DATE('2024-03-13')))\n             |VALUES ('2', DATE('2024-03-13')), ('3', DATE('2024-03-11'))\n             |\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"support Java8 API for TIMESTAMP type\") {\n    withSQLConf(SQLConf.DATETIME_JAVA8API_ENABLED.key -> \"true\") {\n      val tableName = \"my_table\"\n      withTable(tableName) {\n        spark.sql(s\"CREATE TABLE $tableName (id STRING, timestamp TIMESTAMP) USING DELTA;\")\n        spark.sql(\n          s\"\"\"\n             |INSERT INTO $tableName REPLACE\n             |where\n             | (timestamp IN (TIMESTAMP('2022-12-22 15:50:00'), TIMESTAMP('2022-12-23 15:50:00')))\n             | VALUES\n             | ('2', TIMESTAMP('2022-12-22 15:50:00')), ('3', TIMESTAMP('2022-12-23 15:50:00'))\n             |\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"all operations with special characters in path\") {\n    withTempDir { dir =>\n        val directory = new File(dir, \"test with space\")\n        val df = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n        val writer = df.write.format(\"delta\").mode(\"append\")\n        writer.save(directory.getCanonicalPath)\n\n        // UPDATE and DELETE\n        spark.sql(s\"UPDATE delta.`${directory.getCanonicalPath}` SET value = 99\")\n        spark.sql(s\"DELETE FROM delta.`${directory.getCanonicalPath}` WHERE key = 4\")\n        spark.sql(s\"DELETE FROM delta.`${directory.getCanonicalPath}` WHERE key = 3\")\n        checkAnswer(\n          spark.read.format(\"delta\").load(directory.getCanonicalPath),\n          Seq((1, 99), (2, 99)).toDF(\"key\", \"value\")\n        )\n\n        // INSERT\n        spark.sql(s\"INSERT INTO delta.`${directory.getCanonicalPath}` VALUES (5, 50)\")\n        spark.sql(s\"INSERT INTO delta.`${directory.getCanonicalPath}` VALUES (5, 50)\")\n        checkAnswer(\n          spark.read.format(\"delta\").load(directory.getCanonicalPath),\n          Seq((1, 99), (2, 99), (5, 50), (5, 50)).toDF(\"key\", \"value\")\n        )\n\n        // MERGE\n        Seq((1, 1), (3, 88), (5, 88)).toDF(\"key\", \"value\").createOrReplaceTempView(\"inbound\")\n        spark.sql(\n          s\"\"\"|MERGE INTO delta.`${directory.getCanonicalPath}` AS base\n              |USING inbound\n              |ON base.key = inbound.key\n              |WHEN MATCHED THEN DELETE\n              |WHEN NOT MATCHED THEN INSERT *\n              |\"\"\".stripMargin)\n        checkAnswer(\n          spark.read.format(\"delta\").load(directory.getCanonicalPath),\n          Seq((2, 99), (3, 88)).toDF(\"key\", \"value\")\n        )\n\n        // DELETE and INSERT again\n        spark.sql(s\"DELETE FROM delta.`${directory.getCanonicalPath}` WHERE key = 3\")\n        spark.sql(s\"INSERT INTO delta.`${directory.getCanonicalPath}` VALUES (5, 99)\")\n        checkAnswer(\n          spark.read.format(\"delta\").load(directory.getCanonicalPath),\n          Seq((2, 99), (5, 99)).toDF(\"key\", \"value\")\n        )\n\n        assume(!catalogOwnedDefaultCreationEnabledInTests,\n          \"VACUUM is blocked on catalog-managed tables\")\n\n        // VACUUM\n        withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n          spark.sql(s\"VACUUM delta.`${directory.getCanonicalPath}` RETAIN 0 HOURS\")\n        }\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM delta.`${directory.getCanonicalPath}@v8`\"),\n          Seq((2, 99), (5, 99)).toDF(\"key\", \"value\")\n        )\n        // Version 0 should be lost, as version 1 rewrites the whole file\n        val ex = intercept[Exception] {\n          checkAnswer(\n            spark.sql(s\"SELECT * FROM delta.`${directory.getCanonicalPath}@v0`\"),\n            spark.emptyDataFrame\n          )\n        }\n        var cause = ex.getCause\n        while (cause.getCause != null) {\n          cause = cause.getCause\n        }\n        assert(cause.getMessage.contains(\".parquet does not exist\"))\n    }\n  }\n\n  test(\"can't create zero-column table with a write\") {\n    withTempDir { dir =>\n      intercept[AnalysisException] {\n        Seq(1).toDF(\"a\").drop(\"a\").write.format(\"delta\").save(dir.getAbsolutePath)\n      }\n    }\n  }\n\n  test(\"SC-10573: InSet operator prunes partitions properly\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      Seq((1, 1L, \"1\")).toDS()\n        .write\n        .format(\"delta\")\n        .partitionBy(\"_2\", \"_3\")\n        .save(path)\n      val df = spark.read.format(\"delta\").load(path)\n        .where(\"_2 IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)\").select(\"_1\")\n      val condition = df.queryExecution.optimizedPlan.collectFirst {\n        case f: Filter => f.condition\n      }\n      assert(condition.exists(_.isInstanceOf[InSet]))\n      checkAnswer(df, Row(1))\n    }\n  }\n\n  test(\"SC-24886: partition columns have correct datatype in metadata scans\") {\n    withTempDir { inputDir =>\n      Seq((\"foo\", 2019)).toDF(\"name\", \"y\")\n        .write.format(\"delta\").partitionBy(\"y\").mode(\"overwrite\")\n        .save(inputDir.getAbsolutePath)\n\n      // Before the fix, this query would fail because it tried to read strings from the metadata\n      // partition values as the LONG type that the actual partition columns are. This works now\n      // because we added a cast.\n      val df = spark.read.format(\"delta\")\n        .load(inputDir.getAbsolutePath)\n        .where(\n          \"\"\"cast(format_string(\"%04d-01-01 12:00:00\", y) as timestamp) is not null\"\"\".stripMargin)\n      assert(df.collect().length == 1)\n    }\n  }\n\n  test(\"SC-11332: session isolation for cached delta logs\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      val oldSession = spark\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val maxSLL = deltaLog.maxSnapshotLineageLength\n\n      val activeSession = oldSession.newSession()\n      SparkSession.setActiveSession(activeSession)\n      activeSession.sessionState.conf.setConf(\n        DeltaSQLConf.DELTA_MAX_SNAPSHOT_LINEAGE_LENGTH, maxSLL + 1)\n\n      // deltaLog fetches conf from active session\n      assert(deltaLog.maxSnapshotLineageLength == maxSLL + 1)\n\n      // new session confs don't propagate to old session\n      assert(maxSLL ==\n        oldSession.sessionState.conf.getConf(DeltaSQLConf.DELTA_MAX_SNAPSHOT_LINEAGE_LENGTH))\n    }\n  }\n\n  test(\"SC-11198: global configs - save to path\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      withSQLConf(\"spark.databricks.delta.properties.defaults.dataSkippingNumIndexedCols\" -> \"1\") {\n        spark.range(5).write.format(\"delta\").save(path)\n\n        val tableConfigs = DeltaLog.forTable(spark, path).update().metadata.configuration\n        assert(tableConfigs.get(\"delta.dataSkippingNumIndexedCols\") == Some(\"1\"))\n      }\n    }\n  }\n\n  test(\"SC-24982 - initial snapshot has zero partitions\") {\n    withTempDir { tempDir =>\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      assert(deltaLog.snapshot.stateDS.rdd.getNumPartitions == 0)\n    }\n  }\n\n  test(\"SC-24982 - initial snapshot does not trigger jobs\") {\n    val jobCount = new AtomicInteger(0)\n    val listener = new SparkListener {\n      override def onJobStart(jobStart: SparkListenerJobStart): Unit = {\n        // Spark will always log a job start/end event even when the job does not launch any task.\n        if (jobStart.stageInfos.exists(_.numTasks > 0)) {\n          jobCount.incrementAndGet()\n        }\n      }\n    }\n    sparkContext.listenerBus.waitUntilEmpty(15000)\n    sparkContext.addSparkListener(listener)\n    try {\n      withTempDir { tempDir =>\n        val files = DeltaLog.forTable(spark, tempDir).snapshot.stateDS.collect()\n        assert(files.isEmpty)\n      }\n      sparkContext.listenerBus.waitUntilEmpty(15000)\n      assert(jobCount.get() == 0)\n    } finally {\n      sparkContext.removeSparkListener(listener)\n    }\n  }\n\n  def lastDeltaHistory(dir: String): DeltaHistory =\n    io.delta.tables.DeltaTable.forPath(spark, dir).history(1).as[DeltaHistory].head\n\n  test(\"history includes user-defined metadata for DataFrame.Write API\") {\n    val tempDir = Utils.createTempDir().toString\n    val df = Seq(2).toDF().write.format(\"delta\").mode(\"overwrite\")\n\n    df.option(\"userMetadata\", \"meta1\")\n      .save(tempDir)\n\n    assert(lastDeltaHistory(tempDir).userMetadata === Some(\"meta1\"))\n\n    df.option(\"userMetadata\", \"meta2\")\n      .save(tempDir)\n\n    assert(lastDeltaHistory(tempDir).userMetadata === Some(\"meta2\"))\n  }\n\n  test(\"history includes user-defined metadata for SQL API\") {\n    val tempDir = Utils.createTempDir().toString\n    val tblName = \"tblName\"\n\n    withTable(tblName) {\n      withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta1\") {\n        spark.sql(s\"CREATE TABLE $tblName (data STRING) USING delta LOCATION '$tempDir';\")\n      }\n      assert(lastDeltaHistory(tempDir).userMetadata === Some(\"meta1\"))\n\n      withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta2\") {\n        spark.sql(s\"INSERT INTO $tblName VALUES ('test');\")\n      }\n      assert(lastDeltaHistory(tempDir).userMetadata === Some(\"meta2\"))\n\n      withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta3\") {\n        spark.sql(s\"INSERT INTO $tblName VALUES ('test2');\")\n      }\n      assert(lastDeltaHistory(tempDir).userMetadata === Some(\"meta3\"))\n    }\n  }\n\n  test(\"history includes user-defined metadata for DF.Write API and config setting\") {\n    val tempDir = Utils.createTempDir().toString\n    val df = Seq(2).toDF().write.format(\"delta\").mode(\"overwrite\")\n\n    withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta1\") {\n      df.save(tempDir)\n    }\n    assert(lastDeltaHistory(tempDir).userMetadata === Some(\"meta1\"))\n\n    withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta2\") {\n      df.option(\"userMetadata\", \"optionMeta2\")\n        .save(tempDir)\n    }\n    assert(lastDeltaHistory(tempDir).userMetadata === Some(\"optionMeta2\"))\n  }\n\n  test(\"history includes user-defined metadata for SQL + DF.Write API\") {\n    val tempDir = Utils.createTempDir().toString\n    val df = Seq(2).toDF().write.format(\"delta\").mode(\"overwrite\")\n\n    // metadata given in `option` should beat config\n    withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta1\") {\n      df.option(\"userMetadata\", \"optionMeta1\")\n        .save(tempDir)\n    }\n    assert(lastDeltaHistory(tempDir).userMetadata === Some(\"optionMeta1\"))\n\n    withSQLConf(DeltaSQLConf.DELTA_USER_METADATA.key -> \"meta2\") {\n      df.option(\"userMetadata\", \"optionMeta2\")\n        .save(tempDir)\n    }\n    assert(lastDeltaHistory(tempDir).userMetadata === Some(\"optionMeta2\"))\n  }\n\n  test(\"SC-77958 - history includes user-defined metadata for createOrReplace\") {\n    withTable(\"tbl\") {\n      spark.range(10).writeTo(\"tbl\").using(\"delta\").option(\"userMetadata\", \"meta\").createOrReplace()\n\n      val history = sql(\"DESCRIBE HISTORY tbl LIMIT 1\").as[DeltaHistory].head()\n      assert(history.userMetadata === Some(\"meta\"))\n    }\n  }\n\n  test(\"SC-77958 - history includes user-defined metadata for saveAsTable\") {\n    withTable(\"tbl\") {\n      spark.range(10).write.format(\"delta\").option(\"userMetadata\", \"meta1\")\n        .mode(\"overwrite\").saveAsTable(\"tbl\")\n\n      val history = sql(\"DESCRIBE HISTORY tbl LIMIT 1\").as[DeltaHistory].head()\n      assert(history.userMetadata === Some(\"meta1\"))\n    }\n  }\n\n  test(\"lastCommitVersionInSession - init\") {\n    spark.sessionState.conf.unsetConf(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION)\n    withTempDir { tempDir =>\n\n      assert(spark.conf.get(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION) === None)\n\n      Seq(1).toDF\n        .write\n        .format(\"delta\")\n        .save(tempDir.getCanonicalPath)\n\n      assert(spark.conf.get(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION) === Some(0))\n    }\n  }\n\n  test(\"lastCommitVersionInSession - SQL\") {\n    spark.sessionState.conf.unsetConf(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION)\n    withTempDir { tempDir =>\n\n      val k = DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION.key\n      assert(sql(s\"SET $k\").head().get(1) === \"<undefined>\")\n\n      Seq(1).toDF\n        .write\n        .format(\"delta\")\n        .save(tempDir.getCanonicalPath)\n\n      assert(sql(s\"SET $k\").head().get(1) === \"0\")\n    }\n  }\n\n  test(\"lastCommitVersionInSession - SQL only\") {\n    spark.sessionState.conf.unsetConf(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION)\n    withTable(\"test_table\") {\n      val k = DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION.key\n      assert(sql(s\"SET $k\").head().get(1) === \"<undefined>\")\n\n      sql(\"CREATE TABLE test_table USING delta AS SELECT * FROM range(10)\")\n      assert(sql(s\"SET $k\").head().get(1) === \"0\")\n    }\n  }\n\n  test(\"lastCommitVersionInSession - CONVERT TO DELTA\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath + \"/table\"\n      spark.range(10).write.format(\"parquet\").save(path)\n      convertToDelta(s\"parquet.`$path`\")\n\n      // In column mapping (name mode), we perform convertToDelta with a CONVERT and an ALTER,\n      // so the version has been updated\n      val commitVersion = if (columnMappingEnabled) 1 else 0\n      assert(spark.conf.get(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION) ===\n        Some(commitVersion))\n    }\n  }\n\n  test(\"lastCommitVersionInSession - many writes\") {\n    withTempDir { tempDir =>\n\n      for (i <- 0 until 10) {\n        Seq(i).toDF\n          .write\n          .mode(\"overwrite\")\n          .format(\"delta\")\n          .save(tempDir.getCanonicalPath)\n      }\n\n      Seq(10).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tempDir.getCanonicalPath)\n\n      assert(spark.conf.get(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION) === Some(10))\n    }\n  }\n\n  test(\"lastCommitVersionInSession - new thread writes\") {\n    withTempDir { tempDir =>\n\n      Seq(1).toDF\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tempDir.getCanonicalPath)\n\n      val t = new Thread {\n        override def run(): Unit = {\n          Seq(2).toDF\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .save(tempDir.getCanonicalPath)\n        }\n      }\n\n      t.start\n      t.join\n      assert(spark.conf.get(DeltaSQLConf.DELTA_LAST_COMMIT_VERSION_IN_SESSION) === Some(1))\n    }\n  }\n\n  // This test is only compatible w/ backfill batch size = 1.\n  testWithCatalogOwned(backfillBatchSize = 1)(\n      \"An external write should be reflected during analysis of a path based query\") {\n    val tempDir = Utils.createTempDir().toString\n    spark.range(10).coalesce(1).write.format(\"delta\").mode(\"append\").save(tempDir)\n    spark.range(10, 20).coalesce(1).write.format(\"delta\").mode(\"append\").save(tempDir)\n\n    val deltaLog = DeltaLog.forTable(spark, tempDir)\n    val hadoopConf = deltaLog.newDeltaHadoopConf()\n    val snapshot = deltaLog.snapshot\n    val files = snapshot.allFiles.collect()\n\n    // assign physical name to new schema\n    val newMetadata = if (columnMappingEnabled) {\n      DeltaColumnMapping.assignColumnIdAndPhysicalName(\n        snapshot.metadata.copy(schemaString = new StructType().add(\"data\", \"bigint\").json),\n        snapshot.metadata,\n        isChangingModeOnExistingTable = false,\n        isOverwritingSchema = false)\n    } else {\n      snapshot.metadata.copy(schemaString = new StructType().add(\"data\", \"bigint\").json)\n    }\n\n    // Now make a commit that comes from an \"external\" writer that deletes existing data and\n    // changes the schema\n    val actions = Seq(Action.supportedProtocolVersion(\n      featuresToExclude = Seq(CatalogOwnedTableFeature)), newMetadata) ++ files.map(_.remove)\n    deltaLog.store.write(\n      FileNames.unsafeDeltaFile(deltaLog.logPath, snapshot.version + 1),\n      actions.map(_.json).iterator,\n      overwrite = false,\n      hadoopConf)\n\n    deltaLog.store.write(\n      FileNames.unsafeDeltaFile(deltaLog.logPath, snapshot.version + 2),\n      files.take(1).map(_.json).iterator,\n      overwrite = false,\n      hadoopConf)\n\n    // Since the column `data` doesn't exist in our old files, we read it as null.\n    checkAnswer(\n      spark.read.format(\"delta\").load(tempDir),\n      Seq.fill(10)(Row(null))\n    )\n  }\n\n  test(\"isBlindAppend with save and saveAsTable\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"blind_append\") {\n        sql(s\"CREATE TABLE blind_append(value INT) USING delta LOCATION '$path'\") // version = 0\n        sql(\"INSERT INTO blind_append VALUES(1)\") // version = 1\n        spark.read.format(\"delta\").load(path)\n          .where(\"value = 1\")\n          .write.mode(\"append\").format(\"delta\").save(path) // version = 2\n        checkAnswer(spark.table(\"blind_append\"), Row(1) :: Row(1) :: Nil)\n        assert(sql(\"desc history blind_append\")\n          .select(\"version\", \"isBlindAppend\").head == Row(2, false))\n        spark.table(\"blind_append\").where(\"value = 1\").write.mode(\"append\").format(\"delta\")\n          .saveAsTable(\"blind_append\") // version = 3\n        checkAnswer(spark.table(\"blind_append\"), Row(1) :: Row(1) :: Row(1) :: Row(1) :: Nil)\n        assert(sql(\"desc history blind_append\")\n          .select(\"version\", \"isBlindAppend\").head == Row(3, false))\n      }\n    }\n  }\n\n  test(\"isBlindAppend with DataFrameWriterV2\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"blind_append\") {\n        sql(s\"CREATE TABLE blind_append(value INT) USING delta LOCATION '$path'\") // version = 0\n        sql(\"INSERT INTO blind_append VALUES(1)\") // version = 1\n        spark.read.format(\"delta\").load(path)\n          .where(\"value = 1\")\n          .writeTo(\"blind_append\").append() // version = 2\n        checkAnswer(spark.table(\"blind_append\"), Row(1) :: Row(1) :: Nil)\n        assert(sql(\"desc history blind_append\")\n          .select(\"version\", \"isBlindAppend\").head == Row(2, false))\n      }\n    }\n  }\n\n  test(\"isBlindAppend with RTAS\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTable(\"blind_append\") {\n        sql(s\"CREATE TABLE blind_append(value INT) USING delta LOCATION '$path'\") // version = 0\n        sql(\"INSERT INTO blind_append VALUES(1)\") // version = 1\n        sql(\"REPLACE TABLE blind_append USING delta AS SELECT * FROM blind_append\") // version = 2\n        checkAnswer(spark.table(\"blind_append\"), Row(1) :: Nil)\n        assert(sql(\"desc history blind_append\")\n          .select(\"version\", \"isBlindAppend\").head == Row(2, false))\n      }\n    }\n  }\n\n  test(\"replaceWhere should support backtick when flag is disabled\") {\n    val table = \"replace_where_backtick\"\n    withSQLConf(DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED.key -> \"false\") {\n      withTable(table) {\n        // The STRUCT column is added to prevent us from introducing any ambiguity in future\n        sql(s\"CREATE TABLE $table(`a.b` STRING, `c.d` STRING, a STRUCT<b:STRING>)\" +\n          s\"USING delta PARTITIONED BY (`a.b`)\")\n        Seq((\"a\", \"b\", \"c\"))\n          .toDF(\"a.b\", \"c.d\", \"ab\")\n          .withColumn(\"a\", struct($\"ab\".alias(\"b\")))\n          .drop(\"ab\")\n          .write\n          .format(\"delta\")\n          // \"replaceWhere\" should support backtick and remove it correctly. Technically,\n          // \"a.b\" is not correct, but some users may already use it,\n          // so we keep supporting both. This is not ambiguous since \"replaceWhere\" only\n          // supports partition columns and it doesn't support struct type or map type.\n          .option(\"replaceWhere\", \"`a.b` = 'a' AND a.b = 'a'\")\n          .mode(\"overwrite\")\n          .saveAsTable(table)\n        checkAnswer(sql(s\"SELECT `a.b`, `c.d`, a.b from $table\"), Row(\"a\", \"b\", \"c\") :: Nil)\n      }\n    }\n  }\n\n  test(\"replaceArbitrary should enforce proper usage of backtick\") {\n    val table = \"replace_where_backtick\"\n    withTable(table) {\n      sql(s\"CREATE TABLE $table(`a.b` STRING, `c.d` STRING, a STRUCT<b:STRING>)\" +\n        s\"USING delta PARTITIONED BY (`a.b`)\")\n\n      // User has to use backtick properly. If they want to use a.b to match on `a.b`,\n      // error will be thrown if `a.b` doesn't have the value.\n      val e = intercept[AnalysisException] {\n        Seq((\"a\", \"b\", \"c\"))\n          .toDF(\"a.b\", \"c.d\", \"ab\")\n          .withColumn(\"a\", struct($\"ab\".alias(\"b\")))\n          .drop(\"ab\")\n          .write\n          .format(\"delta\")\n          .option(\"replaceWhere\", \"a.b = 'a' AND `a.b` = 'a'\")\n          .mode(\"overwrite\")\n          .saveAsTable(table)\n      }\n      assert(e.getMessage.startsWith(\"[DELTA_REPLACE_WHERE_MISMATCH] \" +\n        \"Written data does not conform to partial table overwrite condition or constraint\"))\n\n      Seq((\"a\", \"b\", \"c\"), (\"d\", \"e\", \"f\"))\n        .toDF(\"a.b\", \"c.d\", \"ab\")\n        .withColumn(\"a\", struct($\"ab\".alias(\"b\")))\n        .drop(\"ab\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .saveAsTable(table)\n\n      // Use backtick properly for `a.b`\n      Seq((\"a\", \"h\", \"c\"))\n        .toDF(\"a.b\", \"c.d\", \"ab\")\n        .withColumn(\"a\", struct($\"ab\".alias(\"b\")))\n        .drop(\"ab\")\n        .write\n        .format(\"delta\")\n        .option(\"replaceWhere\", \"`a.b` = 'a'\")\n        .mode(\"overwrite\")\n        .saveAsTable(table)\n\n      checkAnswer(sql(s\"SELECT `a.b`, `c.d`, a.b from $table\"),\n        Row(\"a\", \"h\", \"c\") :: Row(\"d\", \"e\", \"f\") :: Nil)\n\n      // struct field can only be referred by \"a.b\".\n      Seq((\"a\", \"b\", \"c\"))\n        .toDF(\"a.b\", \"c.d\", \"ab\")\n        .withColumn(\"a\", struct($\"ab\".alias(\"b\")))\n        .drop(\"ab\")\n        .write\n        .format(\"delta\")\n        .option(\"replaceWhere\", \"a.b = 'c'\")\n        .mode(\"overwrite\")\n        .saveAsTable(table)\n      checkAnswer(sql(s\"SELECT `a.b`, `c.d`, a.b from $table\"),\n        Row(\"a\", \"b\", \"c\") :: Row(\"d\", \"e\", \"f\") :: Nil)\n    }\n  }\n\n  test(\"need to update DeltaLog on DataFrameReader.load() code path\") {\n    // Due to possible race conditions (like in mounting/unmounting paths) there might be an initial\n    // snapshot that gets cached for a table that should have a valid (non-initial) snapshot. In\n    // such a case we need to call deltaLog.update() in the DataFrame read paths to update the\n    // initial snapshot to a valid one.\n    //\n    // We simulate a cached InitialSnapshot + valid delta table by creating an empty DeltaLog\n    // (which creates an InitialSnapshot cached for that path) then move an actual Delta table's\n    // transaction log into the path for the empty log.\n    val dir1 = Utils.createTempDir()\n    val dir2 = Utils.createTempDir()\n    val log = DeltaLog.forTable(spark, dir1)\n    assert(!log.tableExists)\n    spark.range(10).write.format(\"delta\").save(dir2.getCanonicalPath)\n    // rename dir2 to dir1 then read\n    dir2.renameTo(dir1)\n    checkAnswer(spark.read.format(\"delta\").load(dir1.getCanonicalPath), spark.range(10).toDF)\n  }\n\n  test(\"set metadata upon write\") {\n    withSQLConf(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"false\") {\n      withTempDir { inputDir =>\n        val testPath = inputDir.getCanonicalPath\n        spark.range(10)\n          .map(_.toInt)\n          .withColumn(\"part\", $\"value\" % 2)\n          .write\n          .format(\"delta\")\n          .option(\"delta.logRetentionDuration\", \"123 days\")\n          .option(\"mergeSchema\", \"true\")\n          .partitionBy(\"part\")\n          .mode(\"append\")\n          .save(testPath)\n\n        val deltaLog = DeltaLog.forTable(spark, testPath)\n        val metadata = deltaLog.snapshot.metadata\n        // We need to drop default properties set by subclasses to make this test pass in them\n        // We need to drop `enableDeletionVectors` property b/c it is explicitly set to false.\n        assert(\n          metadata.configuration\n            .filter { case (k, _) => !k.startsWith(\"delta.columnMapping.\") &&\n              !k.startsWith(\"delta.enableDeletionVectors\")} ===\n          Map(\"delta.logRetentionDuration\" -> \"123 days\") ++\n            extractCatalogOwnedSpecificPropertiesIfEnabled(metadata))\n      }\n    }\n  }\n\n  test(\"idempotent write: idempotent DataFrame insert\") {\n    withTempDir { tableDir =>\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"insertTest\")\n\n      io.delta.tables.DeltaTable.createOrReplace(spark)\n        .addColumn(\"col1\", \"INT\")\n        .addColumn(\"col2\", \"INT\")\n        .location(tableDir.getCanonicalPath)\n        .execute()\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tableDir.getCanonicalPath)\n\n      def runInsert(data: (Int, Int)): Unit = {\n        Seq(data).toDF(\"col1\", \"col2\")\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tableDir.getCanonicalPath)\n      }\n\n      def assertTable(numRows: Int): Unit = {\n        val count = deltaTable.toDF.count()\n        assert(count == numRows)\n      }\n\n      // run insert (1,1), table should have 1 row (1,1)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runInsert((1, 1))\n      assertTable(1)\n      // run insert (2,2), table should have 2 rows (1,1),(2,2)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runInsert((2, 2))\n      assertTable(2)\n      // retry update 2, table should have 2 rows (1,1),(2,2)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runInsert((2, 2))\n      assertTable(2)\n      // run insert (3,3), table should have 3 rows (1,1),(2,2),(3,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runInsert((3, 3))\n      assertTable(3)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent SQL insert\") {\n    withTempDir { tableDir =>\n      val tableName = \"myInsertTable\"\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"insertTestSQL\")\n\n      spark.sql(s\"CREATE TABLE $tableName (col1 INT, col2 INT) USING DELTA LOCATION '\" +\n        tableDir.getCanonicalPath + \"'\")\n\n      def runInsert(data: (Int, Int)): Unit = {\n        spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (${data._1}, ${data._2})\")\n      }\n\n      def assertTable(numRows: Int): Unit = {\n        val count = spark.sql(s\"SELECT * FROM $tableName\").count()\n        assert(count == numRows)\n      }\n\n      // run insert (1,1), table should have 1 row (1,1)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runInsert((1, 1))\n      assertTable(1)\n      // run insert (2,2), table should have 2 rows (1,1),(2,2)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runInsert((2, 2))\n      assertTable(2)\n      // retry update 2, table should have 2 rows (1,1),(2,2)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runInsert((2, 2))\n      assertTable(2)\n      // run insert (3,3), table should have 3 rows (1,1),(2,2),(3,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runInsert((3, 3))\n      assertTable(3)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent DeltaTable merge\") {\n    withTempDir { tableDir =>\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"mergeTest\")\n\n      io.delta.tables.DeltaTable.createOrReplace(spark)\n        .addColumn(\"col1\", \"INT\")\n        .addColumn(\"col2\", \"INT\")\n        .location(tableDir.getCanonicalPath)\n        .execute()\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tableDir.getCanonicalPath)\n\n      def runMerge(data: (Int, Int)): Unit = {\n        val df = Seq(data).toDF(\"col1\", \"col2\")\n        deltaTable.as(\"t\")\n          .merge(\n            df.as(\"s\"),\n            \"t.col1 = s.col1\")\n          .whenMatched.updateExpr(Map(\"t.col2\" -> \"t.col2 + s.col2\"))\n          .whenNotMatched().insertAll()\n          .execute()\n      }\n\n      def assertTable(col2Val: Int, numRows: Int): Unit = {\n        val res1 = deltaTable.toDF.select(\"col2\").where(\"col1 = 1\").collect()\n        assert(res1.length == numRows)\n        assert(res1(0).getInt(0) == col2Val)\n      }\n\n      // merge (1,0) into empty table, table should have 1 row (1,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runMerge((1, 0))\n      assertTable(0, 1)\n      // merge (1,2) into table, table should have 1 row (1,2)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runMerge((1, 2))\n      assertTable(2, 1)\n      // retry merge 2, table should have 1 row (1,2)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runMerge((1, 2))\n      assertTable(2, 1)\n      // merge (1,3) into table, table should have 1 row (1,5)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runMerge((1, 3))\n      assertTable(5, 1)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent SQL merge\") {\n    def withTempDirs(f: (File, File) => Unit): Unit = {\n      withTempDir { file1 =>\n        withTempDir { file2 =>\n          f(file1, file2)\n        }\n      }\n    }\n\n    withTempDirs { (tableDir, updateTableDir) =>\n      val targetTableName = \"myMergeTable\"\n      val sourceTableName = \"updates\"\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"mergeTestSQL\")\n\n      spark.sql(s\"CREATE TABLE $targetTableName (col1 INT, col2 INT) USING DELTA LOCATION '\" +\n        tableDir.getCanonicalPath + \"'\")\n      spark.sql(s\"CREATE TABLE $sourceTableName (col1 INT, col2 INT) USING DELTA LOCATION '\" +\n        updateTableDir.getCanonicalPath + \"'\")\n\n      def runMerge(data: (Int, Int), txnVersion: Int): Unit = {\n        val df = Seq(data).toDF(\"col1\", \"col2\")\n        spark.conf.set(\"spark.databricks.delta.write.txnVersion\", s\"$txnVersion\")\n        df.write.format(\"delta\").mode(\"overwrite\").save(updateTableDir.getCanonicalPath)\n        spark.conf.set(\"spark.databricks.delta.write.txnVersion\", s\"$txnVersion\")\n        spark.sql(s\"\"\"\n                     |MERGE INTO $targetTableName AS t USING $sourceTableName AS s\n                     | ON t.col1 = s.col1\n                     | WHEN MATCHED THEN UPDATE SET t.col2 = t.col2 + s.col2\n                     | WHEN NOT MATCHED THEN INSERT (col1, col2) VALUES (col1, col2)\n                     |\"\"\".stripMargin)\n      }\n\n      def assertTable(col2Val: Int, numRows: Int): Unit = {\n        val res1 = spark.sql(s\"SELECT col2 FROM $targetTableName WHERE col1 = 1\").collect()\n        assert(res1.length == numRows)\n        assert(res1(0).getInt(0) == col2Val)\n      }\n\n      // merge (1,0) into empty table, table should have 1 row (1,0)\n      runMerge((1, 0), 1)\n      assertTable(0, 1)\n      // merge (1,2) into table, table should have 1 row (1,2)\n      runMerge((1, 2), 2)\n      assertTable(2, 1)\n      // retry merge 2, table should have 1 row (1,2)\n      runMerge((1, 2), 2)\n      assertTable( 2, 1)\n      // merge (1,3) into table, table should have 1 row (1,5)\n      runMerge((1, 3), 3)\n      assertTable(5, 1)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent DeltaTable update\") {\n    withTempDir { tableDir =>\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"updateTest\")\n\n      io.delta.tables.DeltaTable.createOrReplace(spark)\n        .addColumn(\"col1\", \"INT\")\n        .addColumn(\"col2\", \"INT\")\n        .location(tableDir.getCanonicalPath)\n        .execute()\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tableDir.getCanonicalPath)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"0\")\n      Seq((1, 0)).toDF(\"col1\", \"col2\")\n        .write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n      def runUpdate(data: (Int, Int)): Unit = {\n        deltaTable.update(\n          condition = expr(s\"col1 == ${data._1}\"),\n          set = Map(\"col2\" -> expr(s\"col2 + ${data._2}\"))\n        )\n      }\n\n      def assertTable(col2Val: Int, numRows: Int): Unit = {\n        val res1 = deltaTable.toDF.select(\"col2\").where(\"col1 = 1\").collect()\n        assert(res1.length == numRows)\n        assert(res1(0).getInt(0) == col2Val)\n      }\n\n      // run update (1,1), table should have 1 row (1,1)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runUpdate((1, 1))\n      assertTable(1, 1)\n      // run update (1,2), table should have 1 row (1,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runUpdate((1, 2))\n      assertTable(3, 1)\n      // retry update 2, table should have 1 row (1,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runUpdate((1, 2))\n      assertTable(3, 1)\n      // retry update 1, table should have 1 row (1,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runUpdate((1, 1))\n      assertTable(3, 1)\n      // run update (1,3) into table, table should have 1 row (1,6)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runUpdate((1, 3))\n      assertTable(6, 1)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent SQL update\") {\n    withTempDir { tableDir =>\n      val tableName = \"myUpdateTable\"\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"updateTestSQL\")\n\n      spark.sql(s\"CREATE TABLE $tableName (col1 INT, col2 INT) USING DELTA LOCATION '\" +\n        tableDir.getCanonicalPath + \"'\")\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"0\")\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (1, 0)\")\n\n      def runUpdate(data: (Int, Int)): Unit = {\n        spark.sql(s\"\"\"\n                     |UPDATE $tableName SET\n                     | col2 = col2 + ${data._2} WHERE col1 = ${data._1}\n              \"\"\".stripMargin)\n      }\n\n      def assertTable(col2Val: Int, numRows: Int): Unit = {\n        val res1 = spark.sql(s\"SELECT col2 FROM $tableName WHERE col1 = 1\").collect()\n        assert(res1.length == numRows)\n        assert(res1(0).getInt(0) == col2Val)\n      }\n\n      // run update (1,1), table should have 1 row (1,1)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runUpdate((1, 1))\n      assertTable(1, 1)\n      // run update (1,2), table should have 1 row (1,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runUpdate((1, 2))\n      assertTable(3, 1)\n      // retry update 2, table should have 1 row (1,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      runUpdate((1, 2))\n      assertTable(3, 1)\n      // retry update 1, table should have 1 row (1,3)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runUpdate((1, 1))\n      assertTable(3, 1)\n      // run update (1,3) into table, table should have 1 row (1,6)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runUpdate((1, 3))\n      assertTable(6, 1)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent DeltaTable delete\") {\n    withTempDir { tableDir =>\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"deleteTest\")\n\n      io.delta.tables.DeltaTable.createOrReplace(spark)\n        .addColumn(\"col1\", \"INT\")\n        .addColumn(\"col2\", \"INT\")\n        .location(tableDir.getCanonicalPath)\n        .execute()\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tableDir.getCanonicalPath)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"0\")\n      Seq((1, 0), (2, 0), (3, 0), (4, 0)).toDF(\"col1\", \"col2\")\n        .write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n      def runDelete(toDelete: Int): Unit = {\n        deltaTable.delete(s\"col1 = $toDelete\")\n      }\n\n      def assertTable(numRows: Int): Unit = {\n        val rows = deltaTable.toDF.count()\n        assert(rows == numRows)\n      }\n\n      // run delete (1), table should have 3 rows (2,0),(3,0),(4,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runDelete(1)\n      assertTable(3)\n      // add (1,0) back to table\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      Seq((1, 0)).toDF(\"col1\", \"col2\")\n        .write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n      assertTable(4)\n      // retry delete 1, table should have 4 rows (2,0),(3,0),(4,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runDelete(1)\n      assertTable(4)\n      // run delete (1), table should have 3 rows (2,0),(3,0),(4,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runDelete(1)\n      assertTable(3)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: idempotent SQL delete\") {\n    withTempDir { tableDir =>\n      val tableName = \"myDeleteTable\"\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"deleteTestSQL\")\n\n      spark.sql(s\"CREATE TABLE $tableName (col1 INT, col2 INT) USING DELTA LOCATION '\" +\n        tableDir.getCanonicalPath + \"'\")\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"0\")\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (1, 0), (2, 0), (3, 0), (4, 0)\")\n\n      def runDelete(toDelete: Int): Unit = {\n        spark.sql(s\"DELETE FROM $tableName WHERE col1 = $toDelete\")\n      }\n\n      def assertTable(numRows: Long): Unit = {\n        val res1 = spark.sql(s\"SELECT COUNT(*) FROM $tableName\").collect()\n        assert(res1.length == 1)\n        assert(res1(0).getLong(0) == numRows)\n      }\n\n      // run delete (1), table should have 3 rows (2,0),(3,0),(4,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runDelete(1)\n      assertTable(3)\n      // add (1,0) back to table\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (1, 0)\")\n      assertTable(4)\n      // retry delete (1), table should have 4 rows (2,0),(3,0),(4,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n      runDelete(1)\n      assertTable(4)\n      // run delete (1), table should have 3 rows (2,0),(3,0),(4,0)\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n      runDelete(1)\n      assertTable(3)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  test(\"idempotent write: valid txnVersion\") {\n    spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"deleteTestSQL\")\n    val e = intercept[IllegalArgumentException] {\n      spark.sessionState.conf.setConfString(\n        \"spark.databricks.delta.write.txnVersion\", \"someVersion\")\n    }\n    assert(e.getMessage ==\n      \"spark.databricks.delta.write.txnVersion should be long, but was someVersion\" ||\n      e.getMessage.contains(\"INVALID_CONF_VALUE.TYPE_MISMATCH\"))\n\n    // clean up\n    spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n    spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n  }\n\n  Seq(\"REPLACE\", \"CREATE OR REPLACE\").foreach { command =>\n    test(s\"Idempotent $command command\") {\n      withTempDir { tableDir =>\n        val tableName = \"myIdempotentReplaceTable\"\n        withTable(tableName) {\n          spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"replaceTestSQL\")\n          spark.sql(s\"CREATE TABLE $tableName(c1 INT, c2 INT, c3 INT)\" +\n            s\"USING DELTA LOCATION '\" + tableDir.getCanonicalPath + \"'\")\n\n          def runReplace(data: (Int, Int, Int)): Unit = {\n            spark.sql(s\"$command table $tableName USING DELTA \" +\n              s\"as SELECT ${data._1} as c1, ${data._2} as c2, ${data._3} as c3\")\n          }\n\n          def assertTable(numRows: Int, commitVersion: Int, data: (Int, Int, Int)): Unit = {\n            val count = spark.sql(s\"SELECT * FROM $tableName\").count()\n            assert(count == numRows)\n            val snapshot = DeltaLog.forTable(spark, tableDir.getCanonicalPath).update()\n            assert(snapshot.version == commitVersion)\n            val tableContent = spark.sql(s\"SELECT * FROM $tableName\").collect().head\n            assert(tableContent.getInt(0) == data._1)\n            assert(tableContent.getInt(1) == data._2)\n            assert(tableContent.getInt(2) == data._3)\n          }\n\n          // run replace (1,1,1) with version 1, table should have 1 row (1,1,1).\n          spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"1\")\n          runReplace((1, 1, 1))\n          assertTable(1, 1, (1, 1, 1))\n          // run replace (2,2,2) with version 2, table should have 1 row (2,2,2)\n          spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n          runReplace((2, 2, 2))\n          assertTable(1, 2, (2, 2, 2))\n          // retry replace (3,3,3) with version 2, table should have 1 row (2,2,2).\n          spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"2\")\n          runReplace((3, 3, 3))\n          assertTable(1, 2, (2, 2, 2))\n          // run replace (4,4,4) with version 3, table should have 1 row (4,4,4).\n          spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n          runReplace((4, 4, 4))\n          assertTable(1, 3, (4, 4, 4))\n          // run replace (5,5,5) with version 3, table should have 1 row (4,4,4).\n          spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"3\")\n          runReplace((5, 5, 5))\n          assertTable(1, 3, (4, 4, 4))\n          // clean up\n          spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n          spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n        }\n      }\n    }\n  }\n\n  test(\"idempotent write: auto reset txnVersion\") {\n    withTempDir { tableDir =>\n      val tableName = \"myAutoResetTable\"\n      spark.conf.set(\"spark.databricks.delta.write.txnAppId\", \"autoReset\")\n      spark.sql(s\"CREATE TABLE $tableName (col1 INT, col2 INT) USING DELTA LOCATION '\" +\n        tableDir.getCanonicalPath + \"'\")\n\n      // this write is done with txn version 0\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"0\")\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (1, 0)\")\n      // this write should be skipped as the version is not reset so it will be applied\n      // with the same version\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (2, 0)\")\n      assert(spark.sql(s\"SELECT * FROM $tableName\").count() == 1)\n\n      // now enable auto reset\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion.autoReset.enabled\", \"true\")\n\n      // this write should be skipped as it is using the same txnVersion as the first write\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"0\")\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (3, 0)\")\n      // this should throw an exception as the txn version is automatically reset\n      val e1 = intercept[DeltaIllegalArgumentException] {\n        spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (4, 0)\")\n      }\n      checkError(e1, \"DELTA_INVALID_IDEMPOTENT_WRITES_OPTIONS\", \"42616\", Map(\"reason\" -> (\n        \"Both spark.databricks.delta.write.txnAppId and spark.databricks.delta.write.txnVersion \" +\n          \"must be specified for idempotent Delta writes\")\n      ))\n      // this write should succeed as it's using a newer version than the latest\n      spark.conf.set(\"spark.databricks.delta.write.txnVersion\", \"10\")\n      spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (2, 0)\")\n      // this should throw an exception as the txn version is automatically reset\n      val e2 = intercept[DeltaIllegalArgumentException] {\n        spark.sql(s\"INSERT INTO $tableName (col1, col2) VALUES (3, 0)\")\n      }\n      checkError(e2, \"DELTA_INVALID_IDEMPOTENT_WRITES_OPTIONS\", \"42616\", Map(\"reason\" -> (\n        \"Both spark.databricks.delta.write.txnAppId and spark.databricks.delta.write.txnVersion \" +\n          \"must be specified for idempotent Delta writes\")\n      ))\n\n      val res = spark.sql(s\"SELECT col1 FROM $tableName\")\n        .orderBy(asc(\"col1\"))\n        .collect()\n      assert(res.length == 2)\n      assert(res(0).getInt(0) == 1)\n      assert(res(1).getInt(0) == 2)\n\n      // clean up\n      spark.conf.unset(\"spark.databricks.delta.write.txnAppId\")\n      spark.conf.unset(\"spark.databricks.delta.write.txnVersion\")\n    }\n  }\n\n  def idempotentWrite(\n      mode: String,\n      appId: String,\n      seq: DataFrame,\n      path: String,\n      name: String,\n      version: Long,\n      expectedCount: Long,\n      commitVersion: Int,\n      isSaveAsTable: Boolean = true): Unit = {\n    val df = seq.write.format(\"delta\")\n      .option(DeltaOptions.TXN_VERSION, version)\n      .option(DeltaOptions.TXN_APP_ID, appId)\n      .mode(mode)\n    if (isSaveAsTable) {\n      df.option(\"path\", path).saveAsTable(name)\n    } else {\n      df.save(path)\n    }\n    val i = spark.read.format(\"delta\").load(path).count()\n    assert(i == expectedCount)\n    val snapshot = DeltaLog.forTable(spark, path).update()\n    assert(snapshot.version == (commitVersion - 1))\n  }\n\n  Seq((true, true), (true, false), (false, true), (false, false))\n    .foreach {case (isSaveAsTable, isLegacy) =>\n      val op = if (isSaveAsTable) \"saveAsTable\" else \"save\"\n      val version = if (isLegacy) \"legacy\" else \"non-legacy\"\n      val appId1 = \"myAppId1\"\n      val appId2 = \"myAppId2\"\n      val confs = if (isLegacy) Seq(SQLConf.USE_V1_SOURCE_LIST.key -> \"tahoe,delta\") else Seq.empty\n\n      if (!(isSaveAsTable && isLegacy)) {\n        test(s\"Idempotent $version Dataframe $op: append\") {\n          withSQLConf(confs: _*) {\n            withTempDir { dir =>\n              val path = dir.getCanonicalPath\n              val name = \"append_table_t1\"\n              val mode = \"append\"\n              sql(\"DROP TABLE IF EXISTS append_table_t1\")\n              val df = Seq((1, 2, 3), (4, 5, 6), (7, 8, 9)).toDF(\"a\", \"b\", \"c\")\n              // The first 2 runs must succeed increasing the expected count.\n              idempotentWrite(mode, appId1, df, path, name, 1, 3, 1, isSaveAsTable)\n              idempotentWrite(mode, appId1, df, path, name, 2, 6, 2, isSaveAsTable)\n\n              // Even if the version is not consecutive, higher versions should commit successfully.\n              idempotentWrite(mode, appId1, df, path, name, 5, 9, 3, isSaveAsTable)\n\n              // This run should be ignored because it uses an older version.\n              idempotentWrite(mode, appId1, df, path, name, 5, 9, 3, isSaveAsTable)\n\n              // Use a different app ID, but same version. This should succeed.\n              idempotentWrite(mode, appId2, df, path, name, 5, 12, 4, isSaveAsTable)\n              idempotentWrite(mode, appId2, df, path, name, 5, 12, 4, isSaveAsTable)\n\n              // Verify that specifying only one of the options -- either appId or version -- fails.\n              val e1 = intercept[Exception] {\n                val stage = df.write.format(\"delta\").option(DeltaOptions.TXN_APP_ID, 1).mode(mode)\n                if (isSaveAsTable) {\n                  stage.option(\"path\", path).saveAsTable(name)\n                } else {\n                  stage.save(path)\n                }\n              }\n              assert(e1.getMessage.contains(\"Invalid options for idempotent Dataframe writes\"))\n              val e2 = intercept[Exception] {\n                val stage = df.write.format(\"delta\").option(DeltaOptions.TXN_VERSION, 1).mode(mode)\n                if (isSaveAsTable) {\n                  stage.option(\"path\", path).saveAsTable(name)\n                } else {\n                  stage.save(path)\n                }\n              }\n              assert(e2.getMessage.contains(\"Invalid options for idempotent Dataframe writes\"))\n            }\n          }\n        }\n      }\n\n      test(s\"Idempotent $version Dataframe $op: overwrite\") {\n        withSQLConf(confs: _*) {\n          withTempDir { dir =>\n            val path = dir.getCanonicalPath\n            val name = \"overwrite_table_t1\"\n            val mode = \"overwrite\"\n            sql(\"DROP TABLE IF EXISTS overwrite_table_t1\")\n            val df = Seq((1, 2, 3), (4, 5, 6), (7, 8, 9)).toDF(\"a\", \"b\", \"c\")\n            // The first 2 runs must succeed increasing the expected count.\n            idempotentWrite(mode, appId1, df, path, name, 1, 3, 1, isSaveAsTable)\n            idempotentWrite(mode, appId1, df, path, name, 2, 3, 2, isSaveAsTable)\n\n            // Even if the version is not consecutive, higher versions should commit successfully.\n            idempotentWrite(mode, appId1, df, path, name, 5, 3, 3, isSaveAsTable)\n\n            // This run should be ignored because it uses an older version.\n            idempotentWrite(mode, appId1, df, path, name, 5, 3, 3, isSaveAsTable)\n\n            // Use a different app ID, but same version. This should succeed.\n            idempotentWrite(mode, appId2, df, path, name, 5, 3, 4, isSaveAsTable)\n            idempotentWrite(mode, appId2, df, path, name, 5, 3, 4, isSaveAsTable)\n\n            // Verify that specifying only one of the options -- either appId or version -- fails.\n            val e1 = intercept[Exception] {\n              val stage = df.write.format(\"delta\").option(DeltaOptions.TXN_APP_ID, 1).mode(mode)\n              if (isSaveAsTable) stage.option(\"path\", path).saveAsTable(name) else stage.save(path)\n            }\n            assert(e1.getMessage.contains(\"Invalid options for idempotent Dataframe writes\"))\n            val e2 = intercept[Exception] {\n              val stage = df.write.format(\"delta\").option(DeltaOptions.TXN_VERSION, 1).mode(mode)\n              if (isSaveAsTable) stage.option(\"path\", path).saveAsTable(name) else stage.save(path)\n            }\n            assert(e2.getMessage.contains(\"Invalid options for idempotent Dataframe writes\"))\n          }\n        }\n      }\n  }\n\n  test(\"idempotent writes in streaming foreachBatch\") {\n    // Function to get a checkpoint location and 2 table locations.\n    def withTempDirs(f: (File, File, File) => Unit): Unit = {\n      withTempDir { file1 =>\n        withTempDir { file2 =>\n          withTempDir { file3 =>\n            f(file1, file2, file3)\n          }\n        }\n      }\n    }\n\n    // In this test, we are going to run a streaming query in a deterministic way.\n    // This streaming query uses foreachBatch to append data to two tables, and\n    // depending on a boolean flag, the query can fail between the two table writes.\n    // By setting this flag, we will test whether both tables are consistenly updated\n    // when query resumes after failure - no duplicates, no data missing.\n\n    withTempDirs { (checkpointDir, table1Dir, table2Dir) =>\n      @volatile var shouldFail = false\n\n      /* Function to write a batch's data to 2 tables */\n      def runBatch(batch: DataFrame, appId: String, batchId: Long): Unit = {\n        // Append to table 1\n        batch.write.format(\"delta\")\n          .option(DeltaOptions.TXN_VERSION, batchId)\n          .option(DeltaOptions.TXN_APP_ID, appId)\n          .mode(\"append\").save(table1Dir.getCanonicalPath)\n        if (shouldFail) {\n          throw new Exception(\"Terminating execution\")\n        } else {\n          // Append to table 2\n          batch.write.format(\"delta\")\n            .option(DeltaOptions.TXN_VERSION, batchId)\n            .option(DeltaOptions.TXN_APP_ID, appId)\n            .mode(\"append\").save(table2Dir.getCanonicalPath)\n        }\n      }\n\n      @volatile var query: StreamingQuery = null\n\n      // Prepare a streaming query\n      val inputData = MemoryStream[Int]\n      val df = inputData.toDF()\n      val streamWriter = df.writeStream\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .foreachBatch { (batch: DataFrame, id: Long) => {\n          runBatch(batch, query.id.toString, id) }\n        }\n\n      /* Add data and run streaming query, then verify # rows in 2 tables */\n      def runQuery(dataToAdd: Int, expectedTable1Count: Int, expectedTable2Count: Int): Unit = {\n        inputData.addData(dataToAdd)\n        query = streamWriter.start()\n        try {\n          query.processAllAvailable()\n        } catch {\n          case e: Exception =>\n            assert(e.getMessage.contains(\"Terminating execution\"))\n        } finally {\n          query.stop()\n        }\n        val t1Count = spark.read.format(\"delta\").load(table1Dir.getCanonicalPath).count()\n        assert(t1Count == expectedTable1Count)\n        val t2Count = spark.read.format(\"delta\").load(table2Dir.getCanonicalPath).count()\n        assert(t2Count == expectedTable2Count)\n      }\n\n      // Run the query 3 times. First time without failure, both the output tables are updated.\n      shouldFail = false\n      runQuery(dataToAdd = 0, expectedTable1Count = 1, expectedTable2Count = 1)\n      // Second time with failure. Only one of the tables should be updated.\n      shouldFail = true\n      runQuery(dataToAdd = 1, expectedTable1Count = 2, expectedTable2Count = 1)\n      // Third time without failure. Both the tables should be consistently updated.\n      shouldFail = false\n      runQuery(dataToAdd = 2, expectedTable1Count = 3, expectedTable2Count = 3)\n    }\n  }\n\n\n  test(\"parsing table name and alias using test helper\") {\n    import DeltaTestUtils.parseTableAndAlias\n    // Parse table name from path and optional alias.\n    assert(parseTableAndAlias(\"delta.`store_sales`\") === \"delta.`store_sales`\" -> None)\n    assert(parseTableAndAlias(\"delta.`store sales`\") === \"delta.`store sales`\" -> None)\n    assert(parseTableAndAlias(\"delta.`store_sales` s\") === \"delta.`store_sales`\" -> Some(\"s\"))\n    assert(parseTableAndAlias(\"delta.`store sales` as s\") === \"delta.`store sales`\" -> Some(\"s\"))\n    assert(parseTableAndAlias(\"delta.`store%sales` AS s\") === \"delta.`store%sales`\" -> Some(\"s\"))\n\n    // Parse table name and optional alias.\n    assert(parseTableAndAlias(\"store_sales\") === \"store_sales\" -> None)\n    assert(parseTableAndAlias(\"store sales\") === \"store\" -> Some(\"sales\"))\n    assert(parseTableAndAlias(\"store_sales s\") === \"store_sales\" -> Some(\"s\"))\n    assert(parseTableAndAlias(\"'store sales' as s\") === \"'store sales'\" -> Some(\"s\"))\n    assert(parseTableAndAlias(\"'store%sales' AS s\") === \"'store%sales'\" -> Some(\"s\"))\n\n    // Not properly supported: ambiguous without special handling for escaping.\n    assert(parseTableAndAlias(\"'store sales'\") === \"'store\" -> Some(\"sales'\"))\n  }\n\n  test(\"DeltaTableV2.properties() filters fs.* storage properties injected by catalogs\") {\n    withTempDir { dir =>\n      spark.range(1).write.format(\"delta\").save(dir.getAbsolutePath)\n\n      val tablePath = new Path(dir.toURI)\n\n      // Simulate catalog (e.g., Unity Catalog) injecting fs.* credentials and metadata\n      // into CatalogTable.storage.properties at table-load time.\n      val injectedFsProps = Map(\n        \"fs.s3a.fake-endpoint\" -> \"s3.us-west-2.amazonaws.com\",\n        \"fs.unitycatalog.uri\" -> \"https://uc.example.com\",\n        \"fs.unitycatalog.auth.fake-token\" -> \"dapi_secret_token\"\n      )\n      val otherStorageProps = Map(\n        \"nonFsProp\" -> \"visible_value\",\n        \"path\" -> dir.getAbsolutePath\n      )\n      val allStorageProps = injectedFsProps ++ otherStorageProps\n\n      val catalogTable = CatalogTable(\n        identifier = TableIdentifier(\"test_fs_filter\"),\n        tableType = CatalogTableType.EXTERNAL,\n        storage = CatalogStorageFormat(\n          locationUri = Some(dir.toURI),\n          inputFormat = None,\n          outputFormat = None,\n          serde = None,\n          compressed = false,\n          properties = allStorageProps\n        ),\n        schema = new StructType().add(\"id\", \"long\"),\n        provider = Some(\"delta\")\n      )\n\n      val deltaTable = DeltaTableV2(spark, tablePath, Some(catalogTable))\n      val v2Props = deltaTable.properties()\n\n      injectedFsProps.keys.foreach { fsKey =>\n        assert(!v2Props.containsKey(TableCatalog.OPTION_PREFIX + fsKey),\n          s\"DeltaTableV2.properties() should hide '${TableCatalog.OPTION_PREFIX}$fsKey'\")\n      }\n      injectedFsProps.keys.foreach { fsKey =>\n        assert(!v2Props.containsKey(fsKey),\n          s\"DeltaTableV2.properties() should also hide '$fsKey'\")\n      }\n      assert(v2Props.get(TableCatalog.OPTION_PREFIX + \"nonFsProp\") === \"visible_value\",\n        \"Non-fs storage properties should remain visible in DeltaTableV2.properties()\")\n    }\n  }\n}\n\n\nclass DeltaNameColumnMappingSuite extends DeltaSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  import testImplicits._\n\n  override protected def runOnlyTests = Seq(\n    \"handle partition filters and data filters\",\n    \"query with predicates should skip partitions\",\n    \"valid replaceWhere\",\n    \"batch write: append, overwrite where\",\n    \"get touched files for update, delete and merge\",\n    \"isBlindAppend with save and saveAsTable\"\n  )\n\n\n  test(\n    \"dynamic partition overwrite with conflicting logical vs. physical named partition columns\") {\n    // It isn't sufficient to just test with column mapping enabled because the physical names are\n    // generated automatically and thus are unique w.r.t. the logical names.\n    // Instead we need to have: ColA.logicalName = ColB.physicalName,\n    // which means we need to start with columnMappingMode=None, and then upgrade to\n    // columnMappingMode=name and rename our columns\n\n    withSQLConf(DeltaSQLConf.DYNAMIC_PARTITION_OVERWRITE_ENABLED.key -> \"true\",\n      DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey-> NoMapping.name) {\n      withTempDir { tempDir =>\n        def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n        Seq((\"a\", \"x\", 1), (\"b\", \"y\", 2), (\"c\", \"x\", 3)).toDF(\"part1\", \"part2\", \"value\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part1\", \"part2\")\n          .mode(\"append\")\n          .save(tempDir.getCanonicalPath)\n\n        val protocol = DeltaLog.forTable(spark, tempDir).snapshot.protocol\n        val (r, w) = if (protocol.supportsReaderFeatures || protocol.supportsWriterFeatures) {\n          (TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION,\n            TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n        } else {\n          (ColumnMappingTableFeature.minReaderVersion, ColumnMappingTableFeature.minWriterVersion)\n        }\n\n        spark.sql(\n          s\"\"\"\n             |ALTER TABLE delta.`${tempDir.getCanonicalPath}` SET TBLPROPERTIES (\n             |  'delta.minReaderVersion' = '$r',\n             |  'delta.minWriterVersion' = '$w',\n             |  'delta.columnMapping.mode' = 'name'\n             |)\n             |\"\"\".stripMargin)\n\n        spark.sql(\n          s\"\"\"\n             |ALTER TABLE delta.`${tempDir.getCanonicalPath}` RENAME COLUMN part1 TO temp\n             |\"\"\".stripMargin)\n        spark.sql(\n          s\"\"\"\n             |ALTER TABLE delta.`${tempDir.getCanonicalPath}` RENAME COLUMN part2 TO part1\n             |\"\"\".stripMargin)\n        spark.sql(\n          s\"\"\"\n             |ALTER TABLE delta.`${tempDir.getCanonicalPath}` RENAME COLUMN temp TO part2\n             |\"\"\".stripMargin)\n\n        Seq((\"a\", \"x\", 4), (\"d\", \"x\", 5)).toDF(\"part2\", \"part1\", \"value\")\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part2\", \"part1\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.PARTITION_OVERWRITE_MODE_OPTION, \"dynamic\")\n          .save(tempDir.getCanonicalPath)\n        checkDatasetUnorderly(data.select(\"part2\", \"part1\", \"value\").as[(String, String, Int)],\n          (\"a\", \"x\", 4), (\"b\", \"y\", 2), (\"c\", \"x\", 3), (\"d\", \"x\", 5))\n      }\n    }\n  }\n\n  test(\"replaceWhere dataframe V2 API with less than predicate\") {\n    withTempDir { dir =>\n      val insertedDF = spark.range(10).toDF()\n\n      insertedDF.write.format(\"delta\").save(dir.toString)\n\n      val otherDF = spark.range(start = 0, end = 4).toDF()\n      otherDF.writeTo(s\"delta.`${dir.toString}`\").overwrite(col(\"id\") < 6)\n      checkAnswer(spark.read.load(dir.toString),\n        insertedDF.filter(col(\"id\") >= 6).union(otherDF))\n    }\n  }\n\n  test(\"replaceWhere SQL - partition column - dynamic filter\") {\n    withTempDir { dir =>\n      // create partitioned table\n      spark.range(100).withColumn(\"part\", 'id % 10)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(dir.toString)\n\n      // ans will be used to replace the entire contents of the table\n      val ans = spark.range(10)\n        .withColumn(\"part\", lit(0))\n\n      ans.createOrReplaceTempView(\"replace\")\n      sql(s\"INSERT INTO delta.`${dir.toString}` REPLACE WHERE part >=0 SELECT * FROM replace\")\n      checkAnswer(spark.read.format(\"delta\").load(dir.toString), ans)\n    }\n  }\n\n  test(\"replaceWhere SQL - partition column - static filter\") {\n    withTable(\"tbl\") {\n      // create partitioned table\n      spark.range(100).withColumn(\"part\", lit(0))\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .saveAsTable(\"tbl\")\n\n      val partEq1DF = spark.range(10, 20)\n        .withColumn(\"part\", lit(1))\n      partEq1DF.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n\n\n      val replacer = spark.range(10)\n        .withColumn(\"part\", lit(0))\n\n      replacer.createOrReplaceTempView(\"replace\")\n      sql(s\"INSERT INTO tbl REPLACE WHERE part=0 SELECT * FROM replace\")\n      checkAnswer(spark.read.format(\"delta\").table(\"tbl\"), replacer.union(partEq1DF))\n    }\n  }\n\n  test(\"replaceWhere SQL - data column - dynamic\") {\n    withTable(\"tbl\") {\n      // write table\n      spark.range(100).withColumn(\"col\", lit(1))\n        .write\n        .format(\"delta\")\n        .saveAsTable(\"tbl\")\n\n      val colGt2DF = spark.range(100, 200)\n        .withColumn(\"col\", lit(3))\n\n      colGt2DF.write\n        .format(\"delta\")\n        .mode(\"append\")\n        .saveAsTable(\"tbl\")\n\n      val replacer = spark.range(10)\n        .withColumn(\"col\", lit(1))\n\n      replacer.createOrReplaceTempView(\"replace\")\n      sql(s\"INSERT INTO tbl REPLACE WHERE col < 2 SELECT * FROM replace\")\n      checkAnswer(\n        spark.read.format(\"delta\").table(\"tbl\"),\n        replacer.union(colGt2DF)\n      )\n    }\n  }\n\n  test(\"replaceWhere SQL - data column - static\") {\n    withTempDir { dir =>\n      // write table\n      spark.range(100).withColumn(\"col\", lit(2))\n        .write\n        .format(\"delta\")\n        .save(dir.toString)\n\n      val colEq2DF = spark.range(100, 200)\n        .withColumn(\"col\", lit(1))\n\n      colEq2DF.write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(dir.toString)\n\n      val replacer = spark.range(10)\n        .withColumn(\"col\", lit(2))\n\n      replacer.createOrReplaceTempView(\"replace\")\n      sql(s\"INSERT INTO delta.`${dir.toString}` REPLACE WHERE col = 2 SELECT * FROM replace\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir.toString),\n        replacer.union(colEq2DF)\n      )\n    }\n  }\n\n  test(\"replaceWhere SQL - multiple predicates - static\") {\n    withTempDir { dir =>\n      // write table\n      spark.range(100).withColumn(\"col\", lit(2))\n        .write\n        .format(\"delta\")\n        .save(dir.toString)\n\n      spark.range(100, 200).withColumn(\"col\", lit(5))\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(dir.toString)\n\n      val colEq2DF = spark.range(100, 200)\n        .withColumn(\"col\", lit(1))\n\n      colEq2DF.write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(dir.toString)\n\n      val replacer = spark.range(10)\n        .withColumn(\"col\", lit(2))\n\n      replacer.createOrReplaceTempView(\"replace\")\n      sql(s\"INSERT INTO delta.`${dir.toString}` REPLACE WHERE col = 2 OR col = 5 \" +\n        s\"SELECT * FROM replace\")\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir.toString),\n        replacer.union(colEq2DF)\n      )\n    }\n  }\n\n  test(\"replaceWhere with less than predicate\") {\n    withTempDir { dir =>\n      val insertedDF = spark.range(10).toDF()\n\n      insertedDF.write.format(\"delta\").save(dir.toString)\n\n      val otherDF = spark.range(start = 0, end = 4).toDF()\n      otherDF.write.format(\"delta\").mode(\"overwrite\")\n        .option(DeltaOptions.REPLACE_WHERE_OPTION, \"id < 6\")\n        .save(dir.toString)\n      checkAnswer(spark.read.load(dir.toString),\n        insertedDF.filter(col(\"id\") >= 6).union(otherDF))\n    }\n  }\n\n  test(\"replaceWhere SQL with less than predicate\") {\n    withTempDir { dir =>\n      val insertedDF = spark.range(10).toDF()\n\n      insertedDF.write.format(\"delta\").save(dir.toString)\n\n      val otherDF = spark.range(start = 0, end = 4).toDF()\n      otherDF.createOrReplaceTempView(\"replace\")\n\n      sql(\n        s\"\"\"\n           |INSERT INTO delta.`${dir.getAbsolutePath}`\n           |REPLACE WHERE id < 6\n           |SELECT * FROM replace\n           |\"\"\".stripMargin)\n      checkAnswer(spark.read.load(dir.toString),\n        insertedDF.filter(col(\"id\") >= 6).union(otherDF))\n    }\n  }\n}\n\nclass DeltaWithCatalogOwnedBatch1Suite extends DeltaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaWithCatalogOwnedBatch2Suite extends DeltaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaWithCatalogOwnedBatch100Suite extends DeltaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\n@SQLUserDefinedType(udt = classOf[NullUDT])\nclass NullData extends Serializable\n\nclass NullUDT extends UserDefinedType[NullData] {\n  override def sqlType: DataType = NullType\n  override def userClass: Class[NullData] = classOf[NullData]\n  override def serialize(obj: NullData): Any = null\n  override def deserialize(datum: Any): NullData = new NullData()\n}\n\n@SQLUserDefinedType(udt = classOf[ComplexUDT])\nclass ComplexData extends Serializable\n\nclass ComplexUDT extends UserDefinedType[ComplexData] {\n  override def sqlType: DataType = new MapType(\n    StringType,\n    new ArrayType(\n      new StructType().add(\"a\", IntegerType).add(\"b\", new NullUDT), containsNull = true),\n    valueContainsNull = true)\n  override def userClass: Class[ComplexData] = classOf[ComplexData]\n  override def serialize(obj: ComplexData): Any = null\n  override def deserialize(datum: Any): ComplexData = new ComplexData()\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaTableCreationTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.Locale\n\n// scalastyle:off import.ordering.noEmptyLine\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.Metadata\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.NoSuchTableException\nimport org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType, ExternalCatalogUtils, SessionCatalog}\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.util.ResolveDefaultColumnsUtils\nimport org.apache.spark.sql.connector.catalog.{CatalogV2Util, Identifier, Table, TableCatalog}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{MetadataBuilder, StructType}\nimport org.apache.spark.util.Utils\n\ntrait DeltaTableCreationTests\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaColumnMappingTestUtils {\n\n  import testImplicits._\n\n  val format = \"delta\"\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      // to make compatible with existing empty schema fail tests\n      .set(DeltaSQLConf.DELTA_ALLOW_CREATE_EMPTY_SCHEMA_TABLE.key, \"false\")\n  }\n\n  private def createDeltaTableByPath(\n      path: File,\n      df: DataFrame,\n      tableName: String,\n      partitionedBy: Seq[String] = Nil): Unit = {\n    df.write\n      .partitionBy(partitionedBy: _*)\n      .mode(SaveMode.Append)\n      .format(format)\n      .save(path.getCanonicalPath)\n\n    sql(s\"\"\"\n           |CREATE TABLE delta_test\n           |USING delta\n           |LOCATION '${path.getCanonicalPath}'\n         \"\"\".stripMargin)\n  }\n\n  private implicit def toTableIdentifier(tableName: String): TableIdentifier = {\n    spark.sessionState.sqlParser.parseTableIdentifier(tableName)\n  }\n\n  protected def getTablePath(tableName: String): String = {\n    new Path(spark.sessionState.catalog.getTableMetadata(tableName).location).toString\n  }\n\n  protected def getDefaultTablePath(tableName: String): String = {\n    new Path(spark.sessionState.catalog.defaultTablePath(tableName)).toString\n  }\n\n  protected def getPartitioningColumns(tableName: String): Seq[String] = {\n    spark.sessionState.catalog.getTableMetadata(tableName).partitionColumnNames\n  }\n\n  protected def getSchema(tableName: String): StructType = {\n    spark.sessionState.catalog.getTableMetadata(tableName).schema\n  }\n\n  protected def getTableProperties(tableName: String): Map[String, String] = {\n    spark.sessionState.catalog.getTableMetadata(tableName).properties\n  }\n\n  private def getDeltaLog(table: CatalogTable): DeltaLog = {\n    getDeltaLog(new Path(table.storage.locationUri.get))\n  }\n\n  private def getDeltaLog(tableName: String): DeltaLog = {\n    getDeltaLog(spark.sessionState.catalog.getTableMetadata(tableName))\n  }\n\n  protected def getDeltaLog(path: Path): DeltaLog = {\n    DeltaLog.forTable(spark, path)\n  }\n\n  protected def verifyTableInCatalog(catalog: SessionCatalog, table: String): Unit = {\n    val externalTable =\n        catalog.externalCatalog.getTable(\"default\", table)\n    assertEqual(externalTable.schema, new StructType())\n    assert(externalTable.partitionColumnNames.isEmpty)\n  }\n\n  protected def checkResult(\n    result: DataFrame,\n    expected: Seq[Any],\n    columns: Seq[String]): Unit = {\n    checkAnswer(\n      result.select(columns.head, columns.tail: _*),\n      Seq(Row(expected: _*))\n    )\n  }\n\n  Seq(\"partitioned\" -> Seq(\"v2\"), \"non-partitioned\" -> Nil).foreach { case (isPartitioned, cols) =>\n    SaveMode.values().foreach { saveMode =>\n      test(s\"saveAsTable to a new table (managed) - $isPartitioned, saveMode: $saveMode\") {\n        val tbl = \"delta_test\"\n        withTable(tbl) {\n          Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n            .write\n            .partitionBy(cols: _*)\n            .mode(saveMode)\n            .format(format)\n            .saveAsTable(tbl)\n\n          checkDatasetUnorderly(spark.table(tbl).as[(Long, String)], 1L -> \"a\")\n            assert(getTablePath(tbl) === getDefaultTablePath(tbl), \"Table path is wrong\")\n          assert(getPartitioningColumns(tbl) === cols, \"Partitioning columns don't match\")\n        }\n      }\n\n      test(s\"saveAsTable to a new table (managed) - $isPartitioned,\" +\n        s\" saveMode: $saveMode (empty df)\") {\n        val tbl = \"delta_test\"\n        withTable(tbl) {\n          Seq(1L -> \"a\").toDF(\"v1\", \"v2\").where(\"false\")\n            .write\n            .partitionBy(cols: _*)\n            .mode(saveMode)\n            .format(format)\n            .saveAsTable(tbl)\n\n          checkDatasetUnorderly(spark.table(tbl).as[(Long, String)])\n            assert(getTablePath(tbl) === getDefaultTablePath(tbl), \"Table path is wrong\")\n          assert(getPartitioningColumns(tbl) === cols, \"Partitioning columns don't match\")\n        }\n      }\n    }\n\n    SaveMode.values().foreach { saveMode =>\n      test(s\"saveAsTable to a new table (external) - $isPartitioned, saveMode: $saveMode\") {\n        withTempDir { dir =>\n          val tbl = \"delta_test\"\n          withTable(tbl) {\n            Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n              .write\n              .partitionBy(cols: _*)\n              .mode(saveMode)\n              .format(format)\n              .option(\"path\", dir.getCanonicalPath)\n              .saveAsTable(tbl)\n\n            checkDatasetUnorderly(spark.table(tbl).as[(Long, String)], 1L -> \"a\")\n            assert(getTablePath(tbl) === new Path(dir.toURI).toString.stripSuffix(\"/\"),\n              \"Table path is wrong\")\n            assert(getPartitioningColumns(tbl) === cols, \"Partitioning columns don't match\")\n          }\n        }\n      }\n\n      test(s\"saveAsTable to a new table (external) - $isPartitioned,\" +\n        s\" saveMode: $saveMode (empty df)\") {\n        withTempDir { dir =>\n          val tbl = \"delta_test\"\n          withTable(tbl) {\n            Seq(1L -> \"a\").toDF(\"v1\", \"v2\").where(\"false\")\n              .write\n              .partitionBy(cols: _*)\n              .mode(saveMode)\n              .format(format)\n              .option(\"path\", dir.getCanonicalPath)\n              .saveAsTable(tbl)\n\n            checkDatasetUnorderly(spark.table(tbl).as[(Long, String)])\n            assert(getTablePath(tbl) === new Path(dir.toURI).toString.stripSuffix(\"/\"),\n              \"Table path is wrong\")\n            assert(getPartitioningColumns(tbl) === cols, \"Partitioning columns don't match\")\n          }\n        }\n      }\n    }\n\n    test(s\"saveAsTable (append) to an existing table - $isPartitioned\") {\n      withTempDir { dir =>\n        val tbl = \"delta_test\"\n        withTable(tbl) {\n          createDeltaTableByPath(dir, Seq(1L -> \"a\").toDF(\"v1\", \"v2\"), tbl, cols)\n\n          Seq(2L -> \"b\").toDF(\"v1\", \"v2\")\n            .write\n            .partitionBy(cols: _*)\n            .mode(SaveMode.Append)\n            .format(format)\n            .saveAsTable(tbl)\n\n          checkDatasetUnorderly(spark.table(tbl).as[(Long, String)], 1L -> \"a\", 2L -> \"b\")\n        }\n      }\n    }\n\n    test(s\"saveAsTable (overwrite) to an existing table - $isPartitioned\") {\n      withTempDir { dir =>\n        val tbl = \"delta_test\"\n        withTable(tbl) {\n          createDeltaTableByPath(dir, Seq(1L -> \"a\").toDF(\"v1\", \"v2\"), tbl, cols)\n\n          Seq(2L -> \"b\").toDF(\"v1\", \"v2\")\n            .write\n            .partitionBy(cols: _*)\n            .mode(SaveMode.Overwrite)\n            .format(format)\n            .saveAsTable(tbl)\n\n          checkDatasetUnorderly(spark.table(tbl).as[(Long, String)], 2L -> \"b\")\n        }\n      }\n    }\n\n    test(s\"saveAsTable (ignore) to an existing table - $isPartitioned\") {\n      withTempDir { dir =>\n        val tbl = \"delta_test\"\n        withTable(tbl) {\n          createDeltaTableByPath(dir, Seq(1L -> \"a\").toDF(\"v1\", \"v2\"), tbl, cols)\n\n          Seq(2L -> \"b\").toDF(\"v1\", \"v2\")\n            .write\n            .partitionBy(cols: _*)\n            .mode(SaveMode.Ignore)\n            .format(format)\n            .saveAsTable(tbl)\n\n          checkDatasetUnorderly(spark.table(tbl).as[(Long, String)], 1L -> \"a\")\n        }\n      }\n    }\n\n    test(s\"saveAsTable (error if exists) to an existing table - $isPartitioned\") {\n      withTempDir { dir =>\n        val tbl = \"delta_test\"\n        withTable(tbl) {\n          createDeltaTableByPath(dir, Seq(1L -> \"a\").toDF(\"v1\", \"v2\"), tbl, cols)\n\n          val e = intercept[AnalysisException] {\n            Seq(2L -> \"b\").toDF(\"v1\", \"v2\")\n              .write\n              .partitionBy(cols: _*)\n              .mode(SaveMode.ErrorIfExists)\n              .format(format)\n              .saveAsTable(tbl)\n          }\n          assert(e.getMessage.contains(tbl))\n          assert(e.getMessage.contains(\"already exists\"))\n\n          checkDatasetUnorderly(spark.table(tbl).as[(Long, String)], 1L -> \"a\")\n        }\n      }\n    }\n  }\n\n  test(\"saveAsTable (append) + insert to a table created without a schema\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n          .write\n          .mode(SaveMode.Append)\n          .partitionBy(\"v2\")\n          .format(format)\n          .option(\"path\", dir.getCanonicalPath)\n          .saveAsTable(\"delta_test\")\n\n        // Out of order\n        Seq(\"b\" -> 2L).toDF(\"v2\", \"v1\")\n          .write\n          .partitionBy(\"v2\")\n          .mode(SaveMode.Append)\n          .format(format)\n          .saveAsTable(\"delta_test\")\n\n        Seq(3L -> \"c\").toDF(\"v1\", \"v2\")\n          .write\n          .format(format)\n          .insertInto(\"delta_test\")\n\n        checkDatasetUnorderly(\n          spark.table(\"delta_test\").as[(Long, String)], 1L -> \"a\", 2L -> \"b\", 3L -> \"c\")\n      }\n    }\n  }\n\n  test(\"saveAsTable to a table created with an invalid partitioning column\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n          .write\n          .mode(SaveMode.Append)\n          .partitionBy(\"v2\")\n          .format(format)\n          .option(\"path\", dir.getCanonicalPath)\n          .saveAsTable(\"delta_test\")\n        checkDatasetUnorderly(spark.table(\"delta_test\").as[(Long, String)], 1L -> \"a\")\n\n        var ex = intercept[Exception] {\n          Seq(\"b\" -> 2L).toDF(\"v2\", \"v1\")\n            .write\n            .partitionBy(\"v1\")\n            .mode(SaveMode.Append)\n            .format(format)\n            .saveAsTable(\"delta_test\")\n        }.getMessage\n        assert(ex.contains(\"not match\"))\n        assert(ex.contains(\"partition\"))\n        checkDatasetUnorderly(spark.table(\"delta_test\").as[(Long, String)], 1L -> \"a\")\n\n        ex = intercept[Exception] {\n          Seq(\"b\" -> 2L).toDF(\"v3\", \"v1\")\n            .write\n            .partitionBy(\"v1\")\n            .mode(SaveMode.Append)\n            .format(format)\n            .saveAsTable(\"delta_test\")\n        }.getMessage\n        assert(ex.contains(\"not match\"))\n        assert(ex.contains(\"partition\"))\n        checkDatasetUnorderly(spark.table(\"delta_test\").as[(Long, String)], 1L -> \"a\")\n\n        Seq(\"b\" -> 2L).toDF(\"v1\", \"v3\")\n          .write\n          .partitionBy(\"v1\")\n          .mode(SaveMode.Ignore)\n          .format(format)\n          .saveAsTable(\"delta_test\")\n        checkDatasetUnorderly(spark.table(\"delta_test\").as[(Long, String)], 1L -> \"a\")\n\n        ex = intercept[AnalysisException] {\n          Seq(\"b\" -> 2L).toDF(\"v1\", \"v3\")\n            .write\n            .partitionBy(\"v1\")\n            .mode(SaveMode.ErrorIfExists)\n            .format(format)\n            .saveAsTable(\"delta_test\")\n        }.getMessage\n        assert(ex.contains(\"delta_test\"))\n        assert(ex.contains(\"already exists\"))\n        checkDatasetUnorderly(spark.table(\"delta_test\").as[(Long, String)], 1L -> \"a\")\n      }\n    }\n  }\n\n  testQuietly(\"create delta table with spaces in column names\") {\n    val tableName = \"delta_test\"\n\n    val tableLoc =\n      new File(spark.sessionState.catalog.defaultTablePath(TableIdentifier(tableName)))\n    Utils.deleteRecursively(tableLoc)\n\n    def createTableUsingDF: Unit = {\n      Seq(1, 2, 3).toDF(\"a column name with spaces\")\n        .write\n        .format(format)\n        .mode(SaveMode.Overwrite)\n        .saveAsTable(tableName)\n    }\n\n    def createTableUsingSQL: DataFrame = {\n      sql(s\"CREATE TABLE $tableName(`a column name with spaces` LONG, b String) USING delta\")\n    }\n\n    withTable(tableName) {\n      if (!columnMappingEnabled) {\n        val ex = intercept[AnalysisException] {\n          createTableUsingDF\n        }\n        assert(\n          ex.getMessage.contains(\"[INVALID_COLUMN_NAME_AS_PATH]\") ||\n            ex.getMessage.contains(\"invalid character(s)\")\n        )\n        assert(!tableLoc.exists())\n      } else {\n        // column mapping modes support creating table with arbitrary col names\n        createTableUsingDF\n          assert(tableLoc.exists())\n      }\n    }\n\n    withTable(tableName) {\n      if (!columnMappingEnabled) {\n        val ex2 = intercept[AnalysisException] {\n          createTableUsingSQL\n        }\n        assert(\n          ex2.getMessage.contains(\"[INVALID_COLUMN_NAME_AS_PATH]\") ||\n            ex2.getMessage.contains(\"invalid character(s)\")\n        )\n        assert(!tableLoc.exists())\n      } else {\n        // column mapping modes support creating table with arbitrary col names\n        createTableUsingSQL\n          assert(tableLoc.exists())\n      }\n    }\n  }\n\n  testQuietly(\"cannot create delta table when using buckets\") {\n    withTable(\"bucketed_table\") {\n      val e = intercept[AnalysisException] {\n        Seq(1L -> \"a\").toDF(\"i\", \"j\").write\n          .format(format)\n          .partitionBy(\"i\")\n          .bucketBy(numBuckets = 8, \"j\")\n          .saveAsTable(\"bucketed_table\")\n      }\n      assert(e.getMessage.toLowerCase(Locale.ROOT).contains(\n        \"is not supported for delta tables\"))\n    }\n  }\n\n  test(\"save without a path\") {\n    val e = intercept[IllegalArgumentException] {\n      Seq(1L -> \"a\").toDF(\"i\", \"j\").write\n        .format(format)\n        .partitionBy(\"i\")\n        .save()\n    }\n    assert(e.getMessage.toLowerCase(Locale.ROOT).contains(\"'path' is not specified\"))\n  }\n\n  test(\"save with an unknown partition column\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val e = intercept[AnalysisException] {\n        Seq(1L -> \"a\").toDF(\"i\", \"j\").write\n          .format(format)\n          .partitionBy(\"unknownColumn\")\n          .save(path)\n      }\n      assert(e.getMessage.contains(\"unknownColumn\"))\n    }\n  }\n\n  test(\"create a table with special column names\") {\n    withTable(\"t\") {\n      Seq(1 -> \"a\").toDF(\"x.x\", \"y.y\").write.format(format).saveAsTable(\"t\")\n      Seq(2 -> \"b\").toDF(\"x.x\", \"y.y\").write.format(format).mode(\"append\").saveAsTable(\"t\")\n      checkAnswer(spark.table(\"t\"), Row(1, \"a\") :: Row(2, \"b\") :: Nil)\n    }\n  }\n\n  testQuietly(\"saveAsTable (overwrite) to a non-partitioned table created with different paths\") {\n    withTempDir { dir1 =>\n      withTempDir { dir2 =>\n        withTable(\"delta_test\") {\n          Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n            .write\n            .mode(SaveMode.Append)\n            .format(format)\n            .option(\"path\", dir1.getCanonicalPath)\n            .saveAsTable(\"delta_test\")\n\n          val ex = intercept[AnalysisException] {\n            Seq((3L, \"c\")).toDF(\"v1\", \"v2\")\n              .write\n              .mode(SaveMode.Overwrite)\n              .format(format)\n              .option(\"path\", dir2.getCanonicalPath)\n              .saveAsTable(\"delta_test\")\n          }.getMessage\n          assert(ex.contains(\"The location of the existing table\"))\n          assert(ex.contains(\"`default`.`delta_test`\"))\n          checkAnswer(\n            spark.table(\"delta_test\"), Row(1L, \"a\") :: Nil)\n        }\n      }\n    }\n  }\n\n  test(\"saveAsTable (append) to a non-partitioned table created without path\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n          .write\n          .mode(SaveMode.Overwrite)\n          .format(format)\n          .option(\"path\", dir.getCanonicalPath)\n          .saveAsTable(\"delta_test\")\n\n        Seq((3L, \"c\")).toDF(\"v1\", \"v2\")\n          .write\n          .mode(SaveMode.Append)\n          .format(format)\n          .saveAsTable(\"delta_test\")\n\n        checkAnswer(\n          spark.table(\"delta_test\"), Row(1L, \"a\") :: Row(3L, \"c\") :: Nil)\n      }\n    }\n  }\n\n  test(\"saveAsTable (append) to a non-partitioned table created with identical paths\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        Seq(1L -> \"a\").toDF(\"v1\", \"v2\")\n          .write\n          .mode(SaveMode.Overwrite)\n          .format(format)\n          .option(\"path\", dir.getCanonicalPath)\n          .saveAsTable(\"delta_test\")\n\n        Seq((3L, \"c\")).toDF(\"v1\", \"v2\")\n          .write\n          .mode(SaveMode.Append)\n          .format(format)\n          .option(\"path\", dir.getCanonicalPath)\n          .saveAsTable(\"delta_test\")\n\n        checkAnswer(\n          spark.table(\"delta_test\"), Row(1L, \"a\") :: Row(3L, \"c\") :: Nil)\n      }\n    }\n  }\n\n  test(\"overwrite mode saveAsTable without path shouldn't create managed table\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        sql(\n          s\"\"\"CREATE TABLE delta_test\n             |USING delta\n             |LOCATION '${dir.getAbsolutePath}'\n             |AS SELECT 1 as a\n          \"\"\".stripMargin)\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.snapshot.version === 0, \"CTAS should be a single commit\")\n\n        checkAnswer(spark.table(\"delta_test\"), Row(1) :: Nil)\n\n        Seq((2, \"key\")).toDF(\"a\", \"b\")\n          .write\n          .mode(SaveMode.Overwrite)\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .format(format)\n          .saveAsTable(\"delta_test\")\n\n        assert(deltaLog.snapshot.version === 1, \"Overwrite mode shouldn't create new managed table\")\n\n        checkAnswer(spark.table(\"delta_test\"), Row(2, \"key\") :: Nil)\n\n      }\n    }\n  }\n\n  testQuietly(\"reject table creation with column names that only differ by case\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      withTempDir { dir =>\n        withTable(\"delta_test\") {\n          intercept[AnalysisException] {\n            sql(\n              s\"\"\"CREATE TABLE delta_test\n                 |USING delta\n                 |LOCATION '${dir.getAbsolutePath}'\n                 |AS SELECT 1 as a, 2 as A\n              \"\"\".stripMargin)\n          }\n\n          intercept[AnalysisException] {\n            sql(\n              s\"\"\"CREATE TABLE delta_test(\n                 |  a string,\n                 |  A string\n                 |)\n                 |USING delta\n                 |LOCATION '${dir.getAbsolutePath}'\n              \"\"\".stripMargin)\n          }\n\n          intercept[ParseException] {\n            sql(\n              s\"\"\"CREATE TABLE delta_test(\n                 |  a string,\n                 |  b string\n                 |)\n                 |partitioned by (a, a)\n                 |USING delta\n                 |LOCATION '${dir.getAbsolutePath}'\n              \"\"\".stripMargin)\n          }\n        }\n      }\n    }\n  }\n\n  testQuietly(\"saveAsTable into a view throws exception around view definition\") {\n    withTempDir { dir =>\n      val viewName = \"delta_test\"\n      withView(viewName) {\n        Seq((1, \"key\")).toDF(\"a\", \"b\").write.format(format).save(dir.getCanonicalPath)\n        sql(s\"create view $viewName as select * from delta.`${dir.getCanonicalPath}`\")\n        val e = intercept[AnalysisException] {\n          Seq((2, \"key\")).toDF(\"a\", \"b\").write.format(format).mode(\"append\").saveAsTable(viewName)\n        }\n        assert(e.getMessage.contains(\"a view\"))\n      }\n    }\n  }\n\n  testQuietly(\"saveAsTable into a parquet table throws exception around format\") {\n    withTempPath { dir =>\n      val tabName = \"delta_test\"\n      withTable(tabName) {\n        Seq((1, \"key\")).toDF(\"a\", \"b\").write.format(\"parquet\")\n          .option(\"path\", dir.getCanonicalPath).saveAsTable(tabName)\n        intercept[AnalysisException] {\n          Seq((2, \"key\")).toDF(\"a\", \"b\").write.format(\"delta\").mode(\"append\").saveAsTable(tabName)\n        }\n      }\n    }\n  }\n\n  test(\"create table with schema and path\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        sql(\n          s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta\n             |OPTIONS('path'='${dir.getCanonicalPath}')\"\"\".stripMargin)\n        sql(\"INSERT INTO delta_test SELECT 1, 'a'\")\n        checkDatasetUnorderly(\n          sql(\"SELECT * FROM delta_test\").as[(Long, String)],\n          1L -> \"a\")\n\n      }\n    }\n  }\n\n  protected def createTableWithEmptySchemaQuery(\n      tableName: String,\n      provider: String = \"delta\",\n      location: Option[String] = None): String = {\n    var query = s\"CREATE TABLE $tableName USING $provider\"\n    if (location.nonEmpty) {\n      query = s\"$query LOCATION '${location.get}'\"\n    }\n    query\n  }\n\n  testQuietly(\"failed to create a table and then able to recreate it\") {\n    withTable(\"delta_test\") {\n      val createEmptySchemaQuery = createTableWithEmptySchemaQuery(\"delta_test\")\n      val e = intercept[AnalysisException] {\n        sql(createEmptySchemaQuery)\n      }.getMessage\n      assert(e.contains(\"but the schema is not specified\"))\n\n      sql(\"CREATE TABLE delta_test(a LONG, b String) USING delta\")\n\n      sql(\"INSERT INTO delta_test SELECT 1, 'a'\")\n\n      checkDatasetUnorderly(\n        sql(\"SELECT * FROM delta_test\").as[(Long, String)],\n        1L -> \"a\")\n    }\n  }\n\n  test(\"create external table without schema\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\", \"delta_test1\") {\n        Seq(1L -> \"a\").toDF()\n          .selectExpr(\"_1 as v1\", \"_2 as v2\")\n          .write\n          .mode(\"append\")\n          .partitionBy(\"v2\")\n          .format(\"delta\")\n          .save(dir.getCanonicalPath)\n\n        sql(s\"\"\"\n               |CREATE TABLE delta_test\n               |USING delta\n               |OPTIONS('path'='${dir.getCanonicalPath}')\n            \"\"\".stripMargin)\n\n        spark.catalog.createTable(\"delta_test1\", dir.getCanonicalPath, \"delta\")\n\n        checkDatasetUnorderly(\n          sql(\"SELECT * FROM delta_test\").as[(Long, String)],\n          1L -> \"a\")\n\n        checkDatasetUnorderly(\n          sql(\"SELECT * FROM delta_test1\").as[(Long, String)],\n          1L -> \"a\")\n      }\n    }\n  }\n\n  testQuietly(\"create managed table without schema\") {\n    withTable(\"delta_test\") {\n      val createEmptySchemaQuery = createTableWithEmptySchemaQuery(\"delta_test\")\n      val e = intercept[AnalysisException] {\n        sql(createEmptySchemaQuery)\n      }.getMessage\n      assert(e.contains(\"but the schema is not specified\"))\n    }\n  }\n\n  testQuietly(\"reject creating a delta table pointing to non-delta files\") {\n    withTempPath { dir =>\n      withTable(\"delta_test\") {\n        val path = dir.getCanonicalPath\n        Seq(1L -> \"a\").toDF(\"col1\", \"col2\").write.parquet(path)\n        val e = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test (col1 int, col2 string)\n               |USING delta\n               |LOCATION '$path'\n             \"\"\".stripMargin)\n        }.getMessage\n        var catalogPrefix = \"\"\n        assert(e.contains(\n          s\"Cannot create table ('$catalogPrefix`default`.`delta_test`'). The associated location\"))\n      }\n    }\n  }\n\n  testQuietly(\"create external table without schema but using non-delta files\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        Seq(1L -> \"a\").toDF().selectExpr(\"_1 as v1\", \"_2 as v2\").write\n          .mode(\"append\").partitionBy(\"v2\").format(\"parquet\").save(dir.getCanonicalPath)\n\n        val createEmptySchemaQuery = createTableWithEmptySchemaQuery(\n          \"delta_test\", location = Some(dir.getCanonicalPath))\n        val e = intercept[AnalysisException] {\n          sql(createEmptySchemaQuery)\n        }.getMessage\n        assert(e.contains(\"but there is no transaction log\"))\n      }\n    }\n  }\n\n  testQuietly(\"create external table without schema and input files\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        val createEmptySchemaQuery = createTableWithEmptySchemaQuery(\n          \"delta_test\", location = Some(dir.getCanonicalPath))\n        val e = intercept[AnalysisException] {\n          sql(createEmptySchemaQuery)\n        }.getMessage\n        assert(e.contains(\"but the schema is not specified\") && e.contains(\"input path is empty\"))\n      }\n    }\n  }\n\n  test(\"create and drop delta table - external\") {\n    val catalog = spark.sessionState.catalog\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        sql(\"CREATE TABLE delta_test(a LONG, b String) USING delta \" +\n          s\"OPTIONS (path='${tempDir.getCanonicalPath}')\")\n        val table = catalog.getTableMetadata(TableIdentifier(\"delta_test\"))\n        assert(table.tableType == CatalogTableType.EXTERNAL)\n        assert(table.provider.contains(\"delta\"))\n\n        // Query the data and the metadata directly via the DeltaLog\n        val deltaLog = getDeltaLog(table)\n\n        assertEqual(\n          deltaLog.snapshot.schema, new StructType().add(\"a\", \"long\").add(\"b\", \"string\"))\n        assertEqual(\n          deltaLog.snapshot.metadata.partitionSchema, new StructType())\n\n        assertEqual(deltaLog.snapshot.schema, getSchema(\"delta_test\"))\n        assert(getPartitioningColumns(\"delta_test\").isEmpty)\n\n        // External catalog does not contain the schema and partition column names.\n        verifyTableInCatalog(catalog, \"delta_test\")\n\n        sql(\"INSERT INTO delta_test SELECT 1, 'a'\")\n        checkDatasetUnorderly(\n          sql(\"SELECT * FROM delta_test\").as[(Long, String)],\n          1L -> \"a\")\n\n        sql(\"DROP TABLE delta_test\")\n        intercept[NoSuchTableException](catalog.getTableMetadata(TableIdentifier(\"delta_test\")))\n        // Verify that the underlying location is not deleted for an external table\n        checkAnswer(spark.read.format(\"delta\")\n          .load(new Path(tempDir.getCanonicalPath).toString), Seq(Row(1L, \"a\")))\n      }\n    }\n  }\n\n  test(\"create and drop delta table - managed\") {\n    val catalog = spark.sessionState.catalog\n    withTable(\"delta_test\") {\n      sql(\"CREATE TABLE delta_test(a LONG, b String) USING delta\")\n      val table = catalog.getTableMetadata(TableIdentifier(\"delta_test\"))\n      assert(table.tableType == CatalogTableType.MANAGED)\n      assert(table.provider.contains(\"delta\"))\n\n      // Query the data and the metadata directly via the DeltaLog\n      val deltaLog = getDeltaLog(table)\n\n      assertEqual(\n        deltaLog.snapshot.schema, new StructType().add(\"a\", \"long\").add(\"b\", \"string\"))\n      assertEqual(\n        deltaLog.snapshot.metadata.partitionSchema, new StructType())\n\n      assertEqual(deltaLog.snapshot.schema, getSchema(\"delta_test\"))\n      assert(getPartitioningColumns(\"delta_test\").isEmpty)\n      assertEqual(getSchema(\"delta_test\"), new StructType().add(\"a\", \"long\").add(\"b\", \"string\"))\n\n      // External catalog does not contain the schema and partition column names.\n      verifyTableInCatalog(catalog, \"delta_test\")\n\n      sql(\"INSERT INTO delta_test SELECT 1, 'a'\")\n      checkDatasetUnorderly(\n        sql(\"SELECT * FROM delta_test\").as[(Long, String)],\n        1L -> \"a\")\n\n      sql(\"DROP TABLE delta_test\")\n      intercept[NoSuchTableException](catalog.getTableMetadata(TableIdentifier(\"delta_test\")))\n      // Verify that the underlying location is deleted for a managed table\n        assert(!new File(table.location).exists())\n    }\n  }\n\n  test(\"create table using - with partitioned by\") {\n    val catalog = spark.sessionState.catalog\n    withTable(\"delta_test\") {\n      sql(\"CREATE TABLE delta_test(a LONG, b String) USING delta PARTITIONED BY (a)\")\n      val table = catalog.getTableMetadata(TableIdentifier(\"delta_test\"))\n      assert(table.tableType == CatalogTableType.MANAGED)\n      assert(table.provider.contains(\"delta\"))\n\n\n      // Query the data and the metadata directly via the DeltaLog\n      val deltaLog = getDeltaLog(table)\n\n      assertEqual(\n        deltaLog.snapshot.schema, new StructType().add(\"a\", \"long\").add(\"b\", \"string\"))\n      assertEqual(\n        deltaLog.snapshot.metadata.partitionSchema, new StructType().add(\"a\", \"long\"))\n\n      assertEqual(deltaLog.snapshot.schema, getSchema(\"delta_test\"))\n      assert(getPartitioningColumns(\"delta_test\") == Seq(\"a\"))\n      assertEqual(getSchema(\"delta_test\"), new StructType().add(\"a\", \"long\").add(\"b\", \"string\"))\n\n      // External catalog does not contain the schema and partition column names.\n      verifyTableInCatalog(catalog, \"delta_test\")\n\n      sql(\"INSERT INTO delta_test SELECT 1, 'a'\")\n\n      assertPartitionWithValueExists(\"a\", \"1\", deltaLog)\n\n      checkDatasetUnorderly(\n        sql(\"SELECT * FROM delta_test\").as[(Long, String)],\n        1L -> \"a\")\n    }\n  }\n\n  test(\"CTAS a managed table with the existing empty directory\") {\n    val tableLoc = new File(spark.sessionState.catalog.defaultTablePath(TableIdentifier(\"tab1\")))\n    try {\n      tableLoc.mkdir()\n      withTable(\"tab1\") {\n        sql(\"CREATE TABLE tab1 USING delta AS SELECT 2, 'b'\")\n        checkAnswer(spark.table(\"tab1\"), Row(2, \"b\"))\n      }\n    } finally {\n      waitForTasksToFinish()\n      Utils.deleteRecursively(tableLoc)\n    }\n  }\n\n  test(\"create a managed table with the existing empty directory\") {\n    val tableLoc = new File(spark.sessionState.catalog.defaultTablePath(TableIdentifier(\"tab1\")))\n    try {\n      tableLoc.mkdir()\n      withTable(\"tab1\") {\n        sql(\"CREATE TABLE tab1 (col1 int, col2 string) USING delta\")\n        sql(\"INSERT INTO tab1 VALUES (2, 'B')\")\n        checkAnswer(spark.table(\"tab1\"), Row(2, \"B\"))\n      }\n    } finally {\n      waitForTasksToFinish()\n      Utils.deleteRecursively(tableLoc)\n    }\n  }\n\n  testQuietly(\n      \"create a managed table with the existing non-empty directory\") {\n    withTable(\"tab1\") {\n      val tableLoc = new File(spark.sessionState.catalog.defaultTablePath(TableIdentifier(\"tab1\")))\n      try {\n        // create an empty hidden file\n        tableLoc.mkdir()\n        val hiddenGarbageFile = new File(tableLoc.getCanonicalPath, \".garbage\")\n        hiddenGarbageFile.createNewFile()\n        var ex = intercept[AnalysisException] {\n          sql(\"CREATE TABLE tab1 USING delta AS SELECT 2, 'b'\")\n        }.getMessage\n        assert(ex.contains(\"Cannot create table\"))\n\n        ex = intercept[AnalysisException] {\n          sql(\"CREATE TABLE tab1 (col1 int, col2 string) USING delta\")\n        }.getMessage\n        assert(ex.contains(\"Cannot create table\"))\n      } finally {\n        waitForTasksToFinish()\n        Utils.deleteRecursively(tableLoc)\n      }\n    }\n  }\n\n  test(\"create table with table properties\") {\n    withTable(\"delta_test\") {\n      sql(s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta\n             |TBLPROPERTIES(\n             |  'delta.logRetentionDuration' = '2 weeks',\n             |  'delta.checkpointInterval' = '20',\n             |  'key' = 'value'\n             |)\n          \"\"\".stripMargin)\n\n      val deltaLog = getDeltaLog(\"delta_test\")\n\n      val snapshot = deltaLog.update()\n      assertEqual(snapshot.metadata.configuration, Map(\n        \"delta.logRetentionDuration\" -> \"2 weeks\",\n        \"delta.checkpointInterval\" -> \"20\",\n        \"key\" -> \"value\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot.metadata) == 20)\n    }\n  }\n\n  test(\"create table with table properties - case insensitivity\") {\n    withTable(\"delta_test\") {\n      sql(s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta\n             |TBLPROPERTIES(\n             |  'dEltA.lOgrEteNtiOndURaTion' = '2 weeks',\n             |  'DelTa.ChEckPoiNtinTervAl' = '20'\n             |)\n          \"\"\".stripMargin)\n\n      val deltaLog = getDeltaLog(\"delta_test\")\n\n      val snapshot = deltaLog.update()\n      assertEqual(snapshot.metadata.configuration,\n        Map(\"delta.logRetentionDuration\" -> \"2 weeks\", \"delta.checkpointInterval\" -> \"20\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot.metadata) == 20)\n    }\n  }\n\n  test(\n      \"create table with table properties - case insensitivity with existing configuration\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val path = tempDir.getCanonicalPath\n\n        val deltaLog = getDeltaLog(new Path(path))\n\n        val txn = deltaLog.startTransaction()\n        txn.commit(Seq(Metadata(\n          schemaString = new StructType().add(\"a\", \"long\").add(\"b\", \"string\").json,\n          configuration = Map(\n            \"delta.logRetentionDuration\" -> \"2 weeks\",\n            \"delta.checkpointInterval\" -> \"20\",\n            \"key\" -> \"value\"))),\n          ManualUpdate)\n\n        sql(s\"\"\"\n               |CREATE TABLE delta_test(a LONG, b String)\n               |USING delta LOCATION '$path'\n               |TBLPROPERTIES(\n               |  'dEltA.lOgrEteNtiOndURaTion' = '2 weeks',\n               |  'DelTa.ChEckPoiNtinTervAl' = '20',\n               |  'key' = \"value\"\n               |)\n            \"\"\".stripMargin)\n\n        val snapshot = deltaLog.update()\n        assertEqual(snapshot.metadata.configuration, Map(\n          \"delta.logRetentionDuration\" -> \"2 weeks\",\n          \"delta.checkpointInterval\" -> \"20\",\n          \"key\" -> \"value\"))\n        assert(deltaLog.deltaRetentionMillis(snapshot.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n        assert(deltaLog.checkpointInterval(snapshot.metadata) == 20)\n      }\n    }\n  }\n\n test(\"create table with table properties - delta.randomizeFilePrefixes\") {\n    withTable(\"delta_test\") {\n      sql(s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta\n             |TBLPROPERTIES(\n             |  'delta.randomizeFilePrefixes' = 'true',\n             |  'delta.randomPrefixLength' = '5'\n             |)\n          \"\"\".stripMargin)\n\n      val deltaLog = getDeltaLog(\"delta_test\")\n      val snapshot = deltaLog.update()\n\n      // Verify the properties are set correctly\n      assertEqual(snapshot.metadata.configuration, Map(\n        \"delta.randomizeFilePrefixes\" -> \"true\",\n        \"delta.randomPrefixLength\" -> \"5\"\n      ))\n\n      // Insert some data to create files\n      sql(\"INSERT INTO delta_test VALUES (1, 'test1'), (2, 'test2'), (3, 'test3')\")\n\n      val updatedSnapshot = deltaLog.update()\n      val allFiles = updatedSnapshot.allFiles.collect()\n\n      // Verify that files exist and have random prefixes\n      assert(allFiles.nonEmpty, \"Table should have data files\")\n\n      // Check that file paths contain 5-character random prefix pattern\n      val prefixLength = DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(updatedSnapshot.metadata)\n      assert(prefixLength == 5, s\"Expected prefix length of 5, but got $prefixLength\")\n\n      val pattern = s\"[A-Za-z0-9]{$prefixLength}/.*part-.*parquet\"\n      allFiles.foreach { file =>\n        assert(file.path.matches(pattern),\n          s\"File path '${file.path}' does not match expected random prefix pattern '$pattern'\")\n      }\n    }\n  }\n\n  test(\"create partitioned table with table properties - delta.randomizeFilePrefixes\") {\n    withTable(\"delta_test\") {\n      sql(s\"\"\"\n             |CREATE TABLE delta_test(id LONG, part String, value INT)\n             |USING delta\n             |PARTITIONED BY (part)\n             |TBLPROPERTIES(\n             |  'delta.randomizeFilePrefixes' = 'true',\n             |  'delta.randomPrefixLength' = '4'\n             |)\n          \"\"\".stripMargin)\n\n      val deltaLog = getDeltaLog(\"delta_test\")\n      val snapshot = deltaLog.update()\n\n      // Verify the properties are set correctly\n      assertEqual(snapshot.metadata.configuration, Map(\n        \"delta.randomizeFilePrefixes\" -> \"true\",\n        \"delta.randomPrefixLength\" -> \"4\"\n      ))\n\n      // Verify the configuration is properly parsed\n      assert(DeltaConfigs.RANDOMIZE_FILE_PREFIXES.fromMetaData(snapshot.metadata),\n        \"randomizeFilePrefixes should be enabled\")\n      assert(DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(snapshot.metadata) == 4,\n        \"randomPrefixLength should be 4\")\n\n      // Verify table is partitioned correctly\n      assert(snapshot.metadata.partitionColumns == Seq(\"part\"),\n        \"Table should be partitioned by 'part' column\")\n\n      // Insert data to create files with random prefixes across multiple partitions\n      sql(\"\"\"INSERT INTO delta_test VALUES\n            |(1, 'A', 100), (2, 'B', 200), (3, 'A', 300), (4, 'C', 400)\"\"\".stripMargin)\n\n      val updatedSnapshot = deltaLog.update()\n      val allFiles = updatedSnapshot.allFiles.collect()\n\n      // Verify that files exist and have random prefixes\n      assert(allFiles.nonEmpty, \"Partitioned table should have data files\")\n\n      // Check that file paths contain 4-character random prefix pattern\n      val prefixLength = DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(updatedSnapshot.metadata)\n      assert(prefixLength == 4, s\"Expected prefix length of 4, but got $prefixLength\")\n\n      // For partitioned tables, files still use random prefix pattern (same as non-partitioned)\n      // Partition information is stored separately in metadata\n      val pattern = s\"[A-Za-z0-9]{$prefixLength}/.*part-.*parquet\"\n      allFiles.foreach { file =>\n        assert(file.path.matches(pattern),\n          s\"Partitioned file path '${file.path}' does not match expected random prefix pattern \" +\n          s\"'$pattern'\")\n      }\n\n      // Verify we have files for multiple partitions (by checking partition values in metadata)\n      val partitionValues = allFiles.map(_.partitionValues(\"part\")).distinct\n      assert(partitionValues.length >= 2,\n        s\"Expected files in multiple partitions, but only found partitions: \" +\n        s\"${partitionValues.mkString(\", \")}\")\n\n      // Verify we have the expected partition values\n      val expectedPartitions = Set(\"A\", \"B\", \"C\")\n      assert(partitionValues.toSet.subsetOf(expectedPartitions),\n        s\"Found unexpected partition values: ${partitionValues.toSet}\")\n    }\n  }\n\n  test(\"schema mismatch between DDL and table location should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val deltaLog = getDeltaLog(new Path(tempDir.getCanonicalPath))\n\n        val txn = deltaLog.startTransaction()\n        txn.commit(\n          Seq(Metadata(schemaString = new StructType().add(\"a\", \"long\").add(\"b\", \"long\").json)),\n          DeltaOperations.ManualUpdate)\n\n        val ex = intercept[AnalysisException] {\n          sql(\"CREATE TABLE delta_test(a LONG, b String)\" +\n            s\" USING delta OPTIONS (path '${tempDir.getCanonicalPath}')\")\n        }\n        assert(ex.getMessage.contains(\"The specified schema does not match the existing schema\"))\n        assert(ex.getMessage.contains(\"Specified type for b is different\"))\n\n        val ex1 = intercept[AnalysisException] {\n          sql(\"CREATE TABLE delta_test(a LONG)\" +\n            s\" USING delta OPTIONS (path '${tempDir.getCanonicalPath}')\")\n        }\n        assert(ex1.getMessage.contains(\"The specified schema does not match the existing schema\"))\n        assert(ex1.getMessage.contains(\"Specified schema is missing field\"))\n\n        val ex2 = intercept[AnalysisException] {\n          sql(\"CREATE TABLE delta_test(a LONG, b String, c INT, d LONG)\" +\n            s\" USING delta OPTIONS (path '${tempDir.getCanonicalPath}')\")\n        }\n        assert(ex2.getMessage.contains(\"The specified schema does not match the existing schema\"))\n        assert(ex2.getMessage.contains(\"Specified schema has additional field\"))\n        assert(ex2.getMessage.contains(\"Specified type for b is different\"))\n      }\n    }\n  }\n\n  test(\n      \"schema metadata mismatch between DDL and table location should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val deltaLog = getDeltaLog(new Path(tempDir.getCanonicalPath))\n\n        val txn = deltaLog.startTransaction()\n        txn.commit(\n          Seq(Metadata(schemaString = new StructType().add(\"a\", \"long\")\n            .add(\"b\", \"string\", nullable = true,\n              new MetadataBuilder().putBoolean(\"pii\", value = true).build()).json)),\n          DeltaOperations.ManualUpdate)\n        val ex = intercept[AnalysisException] {\n          sql(\"CREATE TABLE delta_test(a LONG, b String)\" +\n            s\" USING delta OPTIONS (path '${tempDir.getCanonicalPath}')\")\n        }\n        assert(ex.getMessage.contains(\"The specified schema does not match the existing schema\"))\n        assert(ex.getMessage.contains(\"metadata for field b is different\"))\n      }\n    }\n  }\n\n  test(\n      \"partition schema mismatch between DDL and table location should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val deltaLog = getDeltaLog(new Path(tempDir.getCanonicalPath))\n\n        val txn = deltaLog.startTransaction()\n        txn.commit(\n          Seq(Metadata(\n            schemaString = new StructType().add(\"a\", \"long\").add(\"b\", \"string\").json,\n            partitionColumns = Seq(\"a\"))),\n          DeltaOperations.ManualUpdate)\n        val ex = intercept[AnalysisException](sql(\"CREATE TABLE delta_test(a LONG, b String)\" +\n          s\" USING delta PARTITIONED BY(b) LOCATION '${tempDir.getCanonicalPath}'\"))\n        assert(ex.getMessage.contains(\n          \"The specified partitioning does not match the existing partitioning\"))\n      }\n    }\n  }\n\n  testQuietly(\"create table with unknown table properties should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val ex = intercept[AnalysisException](sql(\n          s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta LOCATION '${tempDir.getCanonicalPath}'\n             |TBLPROPERTIES('delta.key' = 'value')\n          \"\"\".stripMargin))\n        assert(ex.getMessage.contains(\n          \"Unknown configuration was specified: delta.key\"))\n      }\n    }\n  }\n\n  testQuietly(\"create table with invalid table properties should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val ex1 = intercept[IllegalArgumentException](sql(\n          s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta LOCATION '${tempDir.getCanonicalPath}'\n             |TBLPROPERTIES('delta.randomPrefixLength' = '-1')\n          \"\"\".stripMargin))\n        assert(ex1.getMessage.contains(\n          \"randomPrefixLength needs to be greater than 0.\"))\n\n        val ex2 = intercept[IllegalArgumentException](sql(\n          s\"\"\"\n             |CREATE TABLE delta_test(a LONG, b String)\n             |USING delta LOCATION '${tempDir.getCanonicalPath}'\n             |TBLPROPERTIES('delta.randomPrefixLength' = 'value')\n          \"\"\".stripMargin))\n        assert(ex2.getMessage.contains(\n          \"randomPrefixLength needs to be greater than 0.\"))\n      }\n    }\n  }\n\n  test(\n      \"table properties mismatch between DDL and table location should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val deltaLog = getDeltaLog(new Path(tempDir.getCanonicalPath))\n\n        val txn = deltaLog.startTransaction()\n        txn.commit(\n          Seq(Metadata(\n            schemaString = new StructType().add(\"a\", \"long\").add(\"b\", \"string\").json)),\n          DeltaOperations.ManualUpdate)\n        val ex = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test(a LONG, b String)\n               |USING delta LOCATION '${tempDir.getCanonicalPath}'\n               |TBLPROPERTIES('delta.randomizeFilePrefixes' = 'true')\n            \"\"\".stripMargin)\n        }\n\n        assert(ex.getMessage.contains(\n          \"The specified properties do not match the existing properties\"))\n      }\n    }\n  }\n\n  test(\"create table on an existing table location\") {\n    val catalog = spark.sessionState.catalog\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val deltaLog = getDeltaLog(new Path(tempDir.getCanonicalPath))\n\n        val txn = deltaLog.startTransaction()\n        txn.commit(\n          Seq(Metadata(\n            schemaString = new StructType().add(\"a\", \"long\").add(\"b\", \"string\").json,\n            partitionColumns = Seq(\"b\"))),\n          DeltaOperations.ManualUpdate)\n        sql(\"CREATE TABLE delta_test(a LONG, b String) USING delta \" +\n          s\"OPTIONS (path '${tempDir.getCanonicalPath}') PARTITIONED BY(b)\")\n        val table = catalog.getTableMetadata(TableIdentifier(\"delta_test\"))\n        assert(table.tableType == CatalogTableType.EXTERNAL)\n        assert(table.provider.contains(\"delta\"))\n\n        // Query the data and the metadata directly via the DeltaLog\n        val deltaLog2 = getDeltaLog(table)\n\n        // Since we manually committed Metadata without schema, we won't have column metadata in\n        // the latest deltaLog snapshot\n        assert(\n          deltaLog2.snapshot.schema == new StructType().add(\"a\", \"long\").add(\"b\", \"string\"))\n        assert(\n          deltaLog2.snapshot.metadata.partitionSchema == new StructType().add(\"b\", \"string\"))\n\n        assert(getSchema(\"delta_test\") === deltaLog2.snapshot.schema)\n        assert(getPartitioningColumns(\"delta_test\") === Seq(\"b\"))\n\n        // External catalog does not contain the schema and partition column names.\n        verifyTableInCatalog(catalog, \"delta_test\")\n      }\n    }\n  }\n\n  test(\"create datasource table with a non-existing location\") {\n    withTempPath { dir =>\n      withTable(\"t\") {\n        spark.sql(s\"CREATE TABLE t(a int, b int) USING delta LOCATION '${dir.getAbsolutePath}'\")\n\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t\"))\n        assert(table.location == makeQualifiedPath(dir.getAbsolutePath))\n\n        spark.sql(\"INSERT INTO TABLE t SELECT 1, 2\")\n        assert(dir.exists())\n\n        checkDatasetUnorderly(\n          sql(\"SELECT * FROM t\").as[(Int, Int)],\n          1 -> 2)\n      }\n    }\n\n    // partition table\n    withTempPath { dir =>\n      withTable(\"t1\") {\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1(a int, b int) USING delta PARTITIONED BY(a)\n             |LOCATION '${dir.getAbsolutePath}'\n             |\"\"\".stripMargin)\n\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t1\"))\n        assert(table.location == makeQualifiedPath(dir.getAbsolutePath))\n\n        Seq((1, 2)).toDF(\"a\", \"b\")\n          .write.format(\"delta\").mode(\"append\").save(table.location.getPath)\n        val read = spark.read.format(\"delta\").load(table.location.getPath)\n        checkAnswer(read, Seq(Row(1, 2)))\n\n        val deltaLog = loadDeltaLog(table.location.getPath)\n        assert(deltaLog.update().version > 0)\n        assertPartitionWithValueExists(\"a\", \"1\", deltaLog)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { shouldDelete =>\n    val tcName = if (shouldDelete) \"non-existing\" else \"existing\"\n    test(s\"CTAS for external data source table with $tcName location\") {\n      val catalog = spark.sessionState.catalog\n      withTable(\"t\", \"t1\") {\n        withTempDir { dir =>\n          if (shouldDelete) dir.delete()\n          spark.sql(\n            s\"\"\"\n               |CREATE TABLE t\n               |USING delta\n               |LOCATION '${dir.getAbsolutePath}'\n               |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n             \"\"\".stripMargin)\n          val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t\"))\n          assert(table.tableType == CatalogTableType.EXTERNAL)\n          assert(table.provider.contains(\"delta\"))\n          assert(table.location == makeQualifiedPath(dir.getAbsolutePath))\n\n          // Query the data and the metadata directly via the DeltaLog\n          val deltaLog = getDeltaLog(table)\n          assert(deltaLog.update().version >= 0)\n\n          assertEqual(deltaLog.snapshot.schema, new StructType()\n            .add(\"a\", \"integer\").add(\"b\", \"integer\")\n            .add(\"c\", \"integer\").add(\"d\", \"integer\"))\n          assertEqual(\n            deltaLog.snapshot.metadata.partitionSchema, new StructType())\n\n          assertEqual(getSchema(\"t\"), deltaLog.snapshot.schema)\n          assert(getPartitioningColumns(\"t\").isEmpty)\n\n          // External catalog does not contain the schema and partition column names.\n          verifyTableInCatalog(catalog, \"t\")\n\n          // Query the table\n          checkAnswer(spark.table(\"t\"), Row(3, 4, 1, 2))\n\n          // Directly query the reservoir\n          checkAnswer(spark.read.format(\"delta\")\n            .load(new Path(table.storage.locationUri.get).toString), Seq(Row(3, 4, 1, 2)))\n        }\n        // partition table\n        withTempDir { dir =>\n          if (shouldDelete) dir.delete()\n          spark.sql(\n            s\"\"\"\n               |CREATE TABLE t1\n               |USING delta\n               |PARTITIONED BY(a, b)\n               |LOCATION '${dir.getAbsolutePath}'\n               |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n             \"\"\".stripMargin)\n          val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t1\"))\n          assert(table.tableType == CatalogTableType.EXTERNAL)\n          assert(table.provider.contains(\"delta\"))\n          assert(table.location == makeQualifiedPath(dir.getAbsolutePath))\n\n          // Query the data and the metadata directly via the DeltaLog\n          val deltaLog = getDeltaLog(table)\n\n          assertEqual(deltaLog.snapshot.schema, new StructType()\n            .add(\"a\", \"integer\").add(\"b\", \"integer\")\n            .add(\"c\", \"integer\").add(\"d\", \"integer\"))\n          assertEqual(\n            deltaLog.snapshot.metadata.partitionSchema, new StructType()\n            .add(\"a\", \"integer\").add(\"b\", \"integer\"))\n\n          assertEqual(getSchema(\"t1\"), deltaLog.snapshot.schema)\n          assert(getPartitioningColumns(\"t1\") == Seq(\"a\", \"b\"))\n\n          // External catalog does not contain the schema and partition column names.\n          verifyTableInCatalog(catalog, \"t1\")\n\n          // Query the table\n          checkAnswer(spark.table(\"t1\"), Row(3, 4, 1, 2))\n\n          // Directly query the reservoir\n          checkAnswer(spark.read.format(\"delta\")\n            .load(new Path(table.storage.locationUri.get).toString), Seq(Row(3, 4, 1, 2)))\n        }\n      }\n    }\n  }\n\n  test(\"CTAS with table properties\") {\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"\n           |CREATE TABLE delta_test\n           |USING delta\n           |TBLPROPERTIES(\n           |  'delta.logRetentionDuration' = '2 weeks',\n           |  'delta.checkpointInterval' = '20',\n           |  'key' = 'value'\n           |)\n           |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n        \"\"\".stripMargin)\n\n      val deltaLog = getDeltaLog(\"delta_test\")\n\n      val snapshot = deltaLog.update()\n      assertEqual(snapshot.metadata.configuration, Map(\n        \"delta.logRetentionDuration\" -> \"2 weeks\",\n        \"delta.checkpointInterval\" -> \"20\",\n        \"key\" -> \"value\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot.metadata) == 20)\n    }\n  }\n\n  test(\"CTAS with table properties - case insensitivity\") {\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"\n           |CREATE TABLE delta_test\n           |USING delta\n           |TBLPROPERTIES(\n           |  'dEltA.lOgrEteNtiOndURaTion' = '2 weeks',\n           |  'DelTa.ChEckPoiNtinTervAl' = '20'\n           |)\n           |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n        \"\"\".stripMargin)\n\n      val deltaLog = getDeltaLog(\"delta_test\")\n\n      val snapshot = deltaLog.update()\n      assertEqual(snapshot.metadata.configuration,\n        Map(\"delta.logRetentionDuration\" -> \"2 weeks\", \"delta.checkpointInterval\" -> \"20\"))\n      assert(deltaLog.deltaRetentionMillis(snapshot.metadata) == 2 * 7 * 24 * 60 * 60 * 1000)\n      assert(deltaLog.checkpointInterval(snapshot.metadata) == 20)\n    }\n  }\n\n  testQuietly(\"CTAS external table with existing data should fail\") {\n    withTable(\"t\") {\n      withTempDir { dir =>\n        dir.delete()\n        Seq((3, 4)).toDF(\"a\", \"b\")\n          .write.format(\"delta\")\n          .save(dir.getAbsolutePath)\n        val ex = intercept[AnalysisException](spark.sql(\n          s\"\"\"\n             |CREATE TABLE t\n             |USING delta\n             |LOCATION '${dir.getAbsolutePath}'\n             |AS SELECT 1 as a, 2 as b\n             \"\"\".stripMargin))\n        assert(ex.getMessage.contains(\"Cannot create table\"))\n      }\n    }\n\n    withTable(\"t\") {\n      withTempDir { dir =>\n        dir.delete()\n        Seq((3, 4)).toDF(\"a\", \"b\").write.format(\"parquet\").save(dir.getCanonicalPath)\n        val ex = intercept[AnalysisException](spark.sql(\n          s\"\"\"\n             |CREATE TABLE t\n             |USING delta\n             |LOCATION '${dir.getAbsolutePath}'\n             |AS SELECT 1 as a, 2 as b\n             \"\"\".stripMargin))\n        assert(ex.getMessage.contains(\"Cannot create table\"))\n      }\n    }\n  }\n\n  testQuietly(\"CTAS with unknown table properties should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val ex = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test\n               |USING delta\n               |LOCATION '${tempDir.getCanonicalPath}'\n               |TBLPROPERTIES('delta.key' = 'value')\n               |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n            \"\"\".stripMargin)\n        }\n        assert(ex.getMessage.contains(\n          \"Unknown configuration was specified: delta.key\"))\n      }\n    }\n  }\n\n  testQuietly(\"CTAS with invalid table properties should throw an error\") {\n    withTempDir { tempDir =>\n      withTable(\"delta_test\") {\n        val ex1 = intercept[IllegalArgumentException] {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test\n               |USING delta\n               |LOCATION '${tempDir.getCanonicalPath}'\n               |TBLPROPERTIES('delta.randomPrefixLength' = '-1')\n               |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n            \"\"\".stripMargin)\n        }\n        assert(ex1.getMessage.contains(\n          \"randomPrefixLength needs to be greater than 0.\"))\n\n        val ex2 = intercept[IllegalArgumentException] {\n          sql(\n            s\"\"\"\n               |CREATE TABLE delta_test\n               |USING delta\n               |LOCATION '${tempDir.getCanonicalPath}'\n               |TBLPROPERTIES('delta.randomPrefixLength' = 'value')\n               |AS SELECT 3 as a, 4 as b, 1 as c, 2 as d\n            \"\"\".stripMargin)\n        }\n        assert(ex2.getMessage.contains(\n          \"randomPrefixLength needs to be greater than 0.\"))\n      }\n    }\n  }\n\n  Seq(\"a:b\", \"a%b\").foreach { specialChars =>\n    test(s\"data source table:partition column name containing $specialChars\") {\n      // On Windows, it looks colon in the file name is illegal by default. See\n      // https://support.microsoft.com/en-us/help/289627\n      assume(!Utils.isWindows || specialChars != \"a:b\")\n\n      withTable(\"t\") {\n        withTempDir { dir =>\n          spark.sql(\n            s\"\"\"\n               |CREATE TABLE t(a string, `$specialChars` string)\n               |USING delta\n               |PARTITIONED BY(`$specialChars`)\n               |LOCATION '${dir.getAbsolutePath}'\n             \"\"\".stripMargin)\n\n          assert(dir.listFiles().forall(_.toString.contains(\"_delta_log\")))\n          spark.sql(s\"INSERT INTO TABLE t SELECT 1, 2\")\n\n          val deltaLog = loadDeltaLog(dir.getAbsolutePath)\n          assert(deltaLog.update().version > 0)\n          assertPartitionWithValueExists(specialChars, \"2\", deltaLog)\n\n          checkAnswer(spark.table(\"t\"), Row(\"1\", \"2\") :: Nil)\n        }\n      }\n    }\n  }\n\n  Seq(\"a b\", \"a:b\", \"a%b\").foreach { specialChars =>\n    test(s\"location uri contains $specialChars for datasource table\") {\n      // On Windows, it looks colon in the file name is illegal by default. See\n      // https://support.microsoft.com/en-us/help/289627\n      assume(!Utils.isWindows || specialChars != \"a:b\")\n\n      withTable(\"t\", \"t1\") {\n        withTempDir { dir =>\n          val loc = new File(dir, specialChars)\n          loc.mkdir()\n          // The parser does not recognize the backslashes on Windows as they are.\n          // These currently should be escaped.\n          val escapedLoc = loc.getAbsolutePath.replace(\"\\\\\", \"\\\\\\\\\")\n          spark.sql(\n            s\"\"\"\n               |CREATE TABLE t(a string)\n               |USING delta\n               |LOCATION '$escapedLoc'\n             \"\"\".stripMargin)\n\n          val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t\"))\n          assert(table.location == makeQualifiedPath(loc.getAbsolutePath))\n          assert(new Path(table.location).toString.contains(specialChars))\n\n          assert(loc.listFiles().forall(_.toString.contains(\"_delta_log\")))\n          spark.sql(\"INSERT INTO TABLE t SELECT 1\")\n          assert(!loc.listFiles().forall(_.toString.contains(\"_delta_log\")))\n          checkAnswer(spark.table(\"t\"), Row(\"1\") :: Nil)\n        }\n\n        withTempDir { dir =>\n          val loc = new File(dir, specialChars)\n          loc.mkdir()\n          // The parser does not recognize the backslashes on Windows as they are.\n          // These currently should be escaped.\n          val escapedLoc = loc.getAbsolutePath.replace(\"\\\\\", \"\\\\\\\\\")\n          spark.sql(\n            s\"\"\"\n               |CREATE TABLE t1(a string, b string)\n               |USING delta\n               |PARTITIONED BY(b)\n               |LOCATION '$escapedLoc'\n             \"\"\".stripMargin)\n\n          val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t1\"))\n          assert(table.location == makeQualifiedPath(loc.getAbsolutePath))\n          assert(new Path(table.location).toString.contains(specialChars))\n\n          assert(loc.listFiles().forall(_.toString.contains(\"_delta_log\")))\n          spark.sql(\"INSERT INTO TABLE t1 SELECT 1, 2\")\n\n          checkAnswer(spark.table(\"t1\"), Row(\"1\", \"2\") :: Nil)\n\n          if (columnMappingEnabled) {\n           // column mapping always use random file prefixes so we can't compare path\n            val deltaLog = loadDeltaLog(loc.getCanonicalPath)\n            val partPaths = getPartitionFilePathsWithValue(\"b\", \"2\", deltaLog)\n            assert(partPaths.nonEmpty)\n            assert(partPaths.forall { p =>\n              val parentPath = new File(p).getParentFile\n              !parentPath.listFiles().forall(_.toString.contains(\"_delta_log\"))\n            })\n\n            // In column mapping mode, as we are using random file prefixes,\n            // this partition value is valid\n            spark.sql(\"INSERT INTO TABLE t1 SELECT 1, '2017-03-03 12:13%3A14'\")\n            assertPartitionWithValueExists(\"b\", \"2017-03-03 12:13%3A14\", deltaLog)\n            checkAnswer(\n                spark.table(\"t1\"), Row(\"1\", \"2\") :: Row(\"1\", \"2017-03-03 12:13%3A14\") :: Nil)\n          } else {\n            val partFile = new File(loc, \"b=2\")\n            assert(!partFile.listFiles().forall(_.toString.contains(\"_delta_log\")))\n            spark.sql(\"INSERT INTO TABLE t1 SELECT 1, '2017-03-03 12:13%3A14'\")\n            val partFile1 = new File(loc, \"b=2017-03-03 12:13%3A14\")\n            assert(!partFile1.exists())\n\n            if (!Utils.isWindows) {\n              // Actual path becomes \"b=2017-03-03%2012%3A13%253A14\" on Windows.\n              val partFile2 = new File(loc, \"b=2017-03-03 12%3A13%253A14\")\n              assert(!partFile2.listFiles().forall(_.toString.contains(\"_delta_log\")))\n              checkAnswer(\n                spark.table(\"t1\"), Row(\"1\", \"2\") :: Row(\"1\", \"2017-03-03 12:13%3A14\") :: Nil)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"the qualified path of a delta table is stored in the catalog\") {\n    withTempDir { dir =>\n      withTable(\"t\", \"t1\") {\n        assert(!dir.getAbsolutePath.startsWith(\"file:/\"))\n        // The parser does not recognize the backslashes on Windows as they are.\n        // These currently should be escaped.\n        val escapedDir = dir.getAbsolutePath.replace(\"\\\\\", \"\\\\\\\\\")\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t(a string)\n             |USING delta\n             |LOCATION '$escapedDir'\n           \"\"\".stripMargin)\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t\"))\n        assert(table.location.toString.startsWith(\"file:/\"))\n      }\n    }\n\n    withTempDir { dir =>\n      withTable(\"t\", \"t1\") {\n        assert(!dir.getAbsolutePath.startsWith(\"file:/\"))\n        // The parser does not recognize the backslashes on Windows as they are.\n        // These currently should be escaped.\n        val escapedDir = dir.getAbsolutePath.replace(\"\\\\\", \"\\\\\\\\\")\n        spark.sql(\n          s\"\"\"\n             |CREATE TABLE t1(a string, b string)\n             |USING delta\n             |PARTITIONED BY(b)\n             |LOCATION '$escapedDir'\n           \"\"\".stripMargin)\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t1\"))\n        assert(table.location.toString.startsWith(\"file:/\"))\n      }\n    }\n  }\n\n  testQuietly(\"CREATE TABLE with existing data path\") {\n    // Re-use `filterV2TableProperties()` from `SQLTestUtils` as soon as it will be released.\n    def isReservedProperty(propName: String): Boolean = {\n      CatalogV2Util.TABLE_RESERVED_PROPERTIES.contains(propName) ||\n        propName.startsWith(TableCatalog.OPTION_PREFIX) ||\n        propName == TableCatalog.PROP_EXTERNAL\n    }\n    def filterV2TableProperties(properties: Map[String, String]): Map[String, String] = {\n      properties.filterNot(kv => isReservedProperty(kv._1))\n    }\n\n    withTempPath { path =>\n      withTable(\"src\", \"t1\", \"t2\", \"t3\", \"t4\", \"t5\", \"t6\") {\n        sql(\"CREATE TABLE src(i int, p string) USING delta PARTITIONED BY (p) \" +\n          \"TBLPROPERTIES('delta.randomizeFilePrefixes' = 'true') \" +\n          s\"LOCATION '${path.getAbsolutePath}'\")\n        sql(\"INSERT INTO src SELECT 1, 'a'\")\n\n        // CREATE TABLE without specifying anything works\n        sql(s\"CREATE TABLE t1 USING delta LOCATION '${path.getAbsolutePath}'\")\n        checkAnswer(spark.table(\"t1\"), Row(1, \"a\"))\n\n        // CREATE TABLE with the same schema and partitioning but no properties works\n        sql(s\"CREATE TABLE t2(i int, p string) USING delta PARTITIONED BY (p) \" +\n          s\"LOCATION '${path.getAbsolutePath}'\")\n        checkAnswer(spark.table(\"t2\"), Row(1, \"a\"))\n        // Table properties should not be changed to empty.\n        assert(filterV2TableProperties(getTableProperties(\"t2\")) ==\n          Map(\"delta.randomizeFilePrefixes\" -> \"true\"))\n\n        // CREATE TABLE with the same schema but no partitioning fails.\n        val e0 = intercept[AnalysisException] {\n          sql(s\"CREATE TABLE t3(i int, p string) USING delta LOCATION '${path.getAbsolutePath}'\")\n        }\n        assert(e0.message.contains(\"The specified partitioning does not match the existing\"))\n\n        // CREATE TABLE with different schema fails\n        val e1 = intercept[AnalysisException] {\n          sql(s\"CREATE TABLE t4(j int, p string) USING delta LOCATION '${path.getAbsolutePath}'\")\n        }\n        assert(e1.message.contains(\"The specified schema does not match the existing\"))\n\n        // CREATE TABLE with different partitioning fails\n        val e2 = intercept[AnalysisException] {\n          sql(s\"CREATE TABLE t5(i int, p string) USING delta PARTITIONED BY (i) \" +\n            s\"LOCATION '${path.getAbsolutePath}'\")\n        }\n        assert(e2.message.contains(\"The specified partitioning does not match the existing\"))\n\n        // CREATE TABLE with different table properties fails\n        val e3 = intercept[AnalysisException] {\n          sql(s\"CREATE TABLE t6 USING delta \" +\n            \"TBLPROPERTIES ('delta.randomizeFilePrefixes' = 'false') \" +\n            s\"LOCATION '${path.getAbsolutePath}'\")\n        }\n        assert(e3.message.contains(\"The specified properties do not match the existing\"))\n      }\n    }\n  }\n\n  test(\"CREATE TABLE on existing data should not commit metadata\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath()\n      val df = Seq(1, 2, 3, 4, 5).toDF()\n      df.write.format(\"delta\").save(path)\n      val deltaLog = getDeltaLog(new Path(path))\n\n      val oldVersion = deltaLog.snapshot.version\n      sql(s\"CREATE TABLE table USING delta LOCATION '$path'\")\n      assert(oldVersion == deltaLog.snapshot.version)\n    }\n  }\n}\n\nclass DeltaTableCreationSuite\n  extends DeltaTableCreationTests\n  with DeltaSQLCommandTest {\n\n  private def loadTable(tableName: String): Table = {\n    val ti = spark.sessionState.sqlParser.parseMultipartIdentifier(tableName)\n    val namespace = if (ti.length == 1) Array(\"default\") else ti.init.toArray\n    spark.sessionState.catalogManager.currentCatalog.asInstanceOf[TableCatalog]\n      .loadTable(Identifier.of(namespace, ti.last))\n  }\n\n  override protected def getPartitioningColumns(tableName: String): Seq[String] = {\n    loadTable(tableName).partitioning()\n      .map(_.references().head.fieldNames().mkString(\".\"))\n  }\n\n  override def getSchema(tableName: String): StructType = {\n    loadTable(tableName).schema()\n  }\n\n  override protected def getTableProperties(tableName: String): Map[String, String] = {\n    loadTable(tableName).properties().asScala.toMap\n      .filterKeys(!CatalogV2Util.TABLE_RESERVED_PROPERTIES.contains(_))\n      .filterKeys(!TableFeatureProtocolUtils.isTableProtocolProperty(_))\n      .toMap\n  }\n\n  testQuietly(\"REPLACE TABLE\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        sql(\n          s\"\"\"CREATE TABLE delta_test\n             |USING delta\n             |LOCATION '${dir.getAbsolutePath}'\n             |AS SELECT 1 as a\n          \"\"\".stripMargin)\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.snapshot.version === 0, \"CTAS should be a single commit\")\n\n        sql(\n          s\"\"\"REPLACE TABLE delta_test (col string)\n             |USING delta\n             |LOCATION '${dir.getAbsolutePath}'\n          \"\"\".stripMargin)\n        assert(deltaLog.snapshot.version === 1)\n        assertEqual(\n          deltaLog.snapshot.schema, new StructType().add(\"col\", \"string\"))\n\n\n        val e2 = intercept[AnalysisException] {\n          sql(\n            s\"\"\"REPLACE TABLE delta_test\n               |USING delta\n               |LOCATION '${dir.getAbsolutePath}'\n          \"\"\".stripMargin)\n        }\n        assert(e2.getMessage.contains(\"schema is not provided\"))\n      }\n    }\n  }\n\n  testQuietly(\"CREATE OR REPLACE TABLE on table without schema\") {\n    withTempDir { dir =>\n      withTable(\"delta_test\") {\n        spark.range(10).write.format(\"delta\").option(\"path\", dir.getCanonicalPath)\n          .saveAsTable(\"delta_test\")\n        // We need the schema\n        val e = intercept[AnalysisException] {\n          sql(s\"\"\"CREATE OR REPLACE TABLE delta_test\n                 |USING delta\n                 |LOCATION '${dir.getAbsolutePath}'\n               \"\"\".stripMargin)\n        }\n        assert(e.getMessage.contains(\"schema is not provided\"))\n      }\n    }\n  }\n\n  testQuietly(\"CREATE OR REPLACE TABLE on non-empty directory\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getCanonicalPath)\n      withTable(\"delta_test\") {\n        // We need the schema\n        val e = intercept[AnalysisException] {\n          sql(s\"\"\"CREATE OR REPLACE TABLE delta_test\n                 |USING delta\n                 |LOCATION '${dir.getAbsolutePath}'\n               \"\"\".stripMargin)\n        }\n        assert(e.getMessage.contains(\"schema is not provided\"))\n      }\n    }\n  }\n\n  testQuietly(\n      \"REPLACE TABLE on non-empty directory\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getCanonicalPath)\n      withTable(\"delta_test\") {\n        val e = intercept[AnalysisException] {\n          sql(\n            s\"\"\"REPLACE TABLE delta_test\n               |USING delta\n               |LOCATION '${dir.getAbsolutePath}'\n           \"\"\".stripMargin)\n        }\n        assert(e.getMessage.contains(\"cannot be replaced as it did not exist\") ||\n          e.getMessage.contains(s\"table or view `default`.`delta_test` cannot be found\"))\n      }\n    }\n  }\n\n  test(\"Create a table without comment\") {\n    withTempDir { dir =>\n      val table = \"delta_without_comment\"\n      withTable(table) {\n        sql(s\"CREATE TABLE $table (col string) USING delta LOCATION '${dir.getAbsolutePath}'\")\n        checkResult(\n          sql(s\"DESCRIBE DETAIL $table\"),\n          Seq(\"delta\", null),\n          Seq(\"format\", \"description\"))\n      }\n    }\n  }\n\n  protected def withEmptySchemaTable(emptyTableName: String)(f: => Unit): Unit = {\n    def getDeltaLog: DeltaLog =\n      DeltaLog.forTable(spark, TableIdentifier(emptyTableName))\n\n    // create using SQL API\n    withTable(emptyTableName) {\n      sql(s\"CREATE TABLE $emptyTableName USING delta\")\n      assert(getDeltaLog.snapshot.schema.isEmpty)\n      f\n\n      // just make sure this statement runs\n      sql(s\"CREATE TABLE IF NOT EXISTS $emptyTableName USING delta\")\n    }\n\n    // create using Delta table API (creates v1 table)\n    withTable(emptyTableName) {\n      io.delta.tables.DeltaTable\n        .create(spark)\n        .tableName(emptyTableName)\n        .execute()\n      assert(getDeltaLog.snapshot.schema.isEmpty)\n      f\n      io.delta.tables.DeltaTable\n        .createIfNotExists(spark)\n        .tableName(emptyTableName)\n        .execute()\n    }\n\n  }\n\n  test(\"Create an empty table without schema - unsupported cases\") {\n    import testImplicits._\n\n    withSQLConf(DeltaSQLConf.DELTA_ALLOW_CREATE_EMPTY_SCHEMA_TABLE.key -> \"true\") {\n      val emptySchemaTableName = \"t1\"\n\n      // TODO: support CREATE OR REPLACE code path if needed in the future\n      intercept[AnalysisException] {\n        sql(s\"CREATE OR REPLACE TABLE $emptySchemaTableName USING delta\")\n      }\n\n      // similarly blocked using Delta Table API\n      withTable(emptySchemaTableName) {\n        intercept[AnalysisException] {\n          io.delta.tables.DeltaTable\n            .createOrReplace(spark)\n            .tableName(emptySchemaTableName)\n            .execute()\n        }\n      }\n\n      withTable(emptySchemaTableName) {\n        io.delta.tables.DeltaTable\n          .create(spark)\n          .tableName(emptySchemaTableName)\n          .execute()\n\n        intercept[AnalysisException] {\n          io.delta.tables.DeltaTable\n            .replace(spark)\n            .tableName(emptySchemaTableName)\n            .execute()\n        }\n      }\n\n      // external table with an invalid location it shouldn't work (e.g. no transaction log present)\n      withTable(emptySchemaTableName) {\n        withTempDir { dir =>\n          Seq(1, 2, 3).toDF().write.format(\"delta\").save(dir.getAbsolutePath)\n          Utils.deleteRecursively(new File(dir, \"_delta_log\"))\n          val e = intercept[AnalysisException] {\n            sql(s\"CREATE TABLE $emptySchemaTableName USING delta LOCATION '${dir.getAbsolutePath}'\")\n          }\n          assert(e.getErrorClass == \"DELTA_CREATE_EXTERNAL_TABLE_WITHOUT_TXN_LOG\")\n        }\n      }\n\n      // CTAS from an empty schema dataframe should be blocked\n      intercept[AnalysisException] {\n        withTable(emptySchemaTableName) {\n          val df = spark.emptyDataFrame\n          df.createOrReplaceTempView(\"empty_df\")\n          sql(s\"CREATE TABLE $emptySchemaTableName USING delta AS SELECT * FROM empty_df\")\n        }\n      }\n\n      // create empty schema table using dataframe api should be blocked\n      intercept[AnalysisException] {\n        withTable(emptySchemaTableName) {\n          spark.emptyDataFrame\n            .write.format(\"delta\")\n            .saveAsTable(emptySchemaTableName)\n        }\n      }\n\n      intercept[AnalysisException] {\n        withTable(emptySchemaTableName) {\n          spark.emptyDataFrame\n            .writeTo(emptySchemaTableName)\n            .using(\"delta\")\n            .create()\n        }\n      }\n\n      def assertFailToRead(f: => Any): Unit = {\n        try f catch {\n          case e: AnalysisException =>\n            assert(e.getMessage.contains(\"that does not have any columns.\"))\n        }\n      }\n\n      def assertSchemaEvolutionRequired(f: => Any): Unit = {\n        val e = intercept[AnalysisException] {\n          f\n        }\n        assert(e.getMessage.contains(\"A schema mismatch detected when writing to the Delta\"))\n      }\n\n      // data reading or writing without mergeSchema should fail\n      withEmptySchemaTable(emptySchemaTableName) {\n        assertFailToRead {\n          spark.read.table(emptySchemaTableName).collect()\n        }\n\n        assertFailToRead {\n          sql(s\"SELECT * FROM $emptySchemaTableName\").collect()\n        }\n\n        assertSchemaEvolutionRequired {\n          sql(s\"INSERT INTO $emptySchemaTableName VALUES (1,2,3)\")\n        }\n\n        // but enabling auto merge should make insert work\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n          sql(s\"INSERT INTO $emptySchemaTableName VALUES (1,2,3)\")\n          checkAnswer(spark.read.table(emptySchemaTableName), Seq(Row(1, 2, 3)))\n        }\n      }\n\n      // allows drop and recreate the same table with empty schema\n      withTempDir { dir =>\n        withTable(emptySchemaTableName) {\n          sql(s\"CREATE TABLE $emptySchemaTableName USING delta LOCATION '${dir.getCanonicalPath}'\")\n          val snapshot = DeltaLog.forTable(spark, TableIdentifier(emptySchemaTableName)).update()\n          assert(snapshot.schema.isEmpty && snapshot.version == 0)\n          assertFailToRead {\n            sql(s\"SELECT * FROM $emptySchemaTableName\")\n          }\n          // drop the table\n          sql(s\"DROP TABLE $emptySchemaTableName\")\n          // recreate the table again should work\n          sql(s\"CREATE TABLE $emptySchemaTableName USING delta LOCATION '${dir.getCanonicalPath}'\")\n          assertFailToRead {\n            sql(s\"SELECT * FROM $emptySchemaTableName\")\n          }\n          // write some data to it\n          withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n            sql(s\"INSERT INTO $emptySchemaTableName VALUES (1,2,3)\")\n            checkAnswer(spark.read.table(emptySchemaTableName), Seq(Row(1, 2, 3)))\n          }\n          // drop again\n          sql(s\"DROP TABLE $emptySchemaTableName\")\n          // recreate the table again should work\n          sql(s\"CREATE TABLE $emptySchemaTableName USING delta LOCATION '${dir.getCanonicalPath}'\")\n          checkAnswer(spark.read.table(emptySchemaTableName), Seq(Row(1, 2, 3)))\n        }\n      }\n    }\n  }\n\n  test(\"Create an empty table without schema - supported cases\") {\n    import testImplicits._\n\n    withSQLConf(DeltaSQLConf.DELTA_ALLOW_CREATE_EMPTY_SCHEMA_TABLE.key -> \"true\") {\n      val emptyTableName = \"t1\"\n\n      def getDeltaLog: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(emptyTableName))\n\n      // yet CTAS should be allowed\n      withTable(emptyTableName) {\n        sql(s\"CREATE TABLE $emptyTableName USING delta AS SELECT 1\")\n        assert(getDeltaLog.snapshot.schema.size == 1)\n      }\n\n      // and create Delta table using existing valid location should work without ()\n      withTable(emptyTableName) {\n        withTempDir { dir =>\n          Seq(1, 2, 3).toDF().write.format(\"delta\").save(dir.getAbsolutePath)\n          sql(s\"CREATE TABLE $emptyTableName USING delta LOCATION '${dir.getAbsolutePath}'\")\n          assert(getDeltaLog.snapshot.schema.size == 1)\n        }\n      }\n\n      // checkpointing should work\n      withEmptySchemaTable(emptyTableName) {\n        getDeltaLog.checkpoint()\n        assert(getDeltaLog.readLastCheckpointFile().exists(_.version == 0))\n        // run some operations\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n          sql(s\"INSERT INTO $emptyTableName VALUES (1,2,3)\")\n          checkAnswer(spark.read.table(emptyTableName), Seq(Row(1, 2, 3)))\n        }\n        getDeltaLog.checkpoint()\n        assert(getDeltaLog.readLastCheckpointFile().exists(_.version == 1))\n      }\n\n      withEmptySchemaTable(emptyTableName) {\n        // TODO: possibly support MERGE into the future\n        try {\n          val source = \"t2\"\n          withTable(source) {\n            sql(s\"CREATE TABLE $source USING delta AS SELECT 1\")\n            sql(\n              s\"\"\"\n                 |MERGE INTO $emptyTableName\n                 |USING $source\n                 |ON FALSE\n                 |WHEN NOT MATCHED\n                 |  THEN INSERT *\n                 |\"\"\".stripMargin)\n          }\n        } catch {\n            case _: AssertionError | _: SparkException =>\n        }\n      }\n\n      // Delta specific DMLs should work, though they should basically be noops\n      withEmptySchemaTable(emptyTableName) {\n        sql(s\"OPTIMIZE $emptyTableName\")\n        sql(s\"VACUUM $emptyTableName\")\n\n        assert(getDeltaLog.snapshot.schema.isEmpty)\n      }\n\n      // metadata DDL should work\n      withEmptySchemaTable(emptyTableName) {\n        sql(s\"ALTER TABLE $emptyTableName SET TBLPROPERTIES ('a' = 'b')\")\n        assert(DeltaLog.forTable(spark,\n          TableIdentifier(emptyTableName)).snapshot.metadata.configuration.contains(\"a\"))\n\n        checkAnswer(\n          sql(s\"COMMENT ON TABLE $emptyTableName IS 'My Empty Cool Table'\"), Nil)\n        assert(sql(s\"DESCRIBE TABLE $emptyTableName\").collect().length == 0)\n\n        // create table, alter tbl property, tbl comment\n        assert(sql(s\"DESCRIBE HISTORY $emptyTableName\").collect().length == 3)\n\n        checkAnswer(sql(s\"SHOW COLUMNS IN $emptyTableName\"), Nil)\n      }\n\n      // schema evolution ddl should work\n      withEmptySchemaTable(emptyTableName) {\n        sql(s\"ALTER TABLE $emptyTableName ADD COLUMN (id long COMMENT 'haha')\")\n        assert(getDeltaLog.snapshot.schema.size == 1)\n      }\n\n      withEmptySchemaTable(emptyTableName) {\n        sql(s\"ALTER TABLE $emptyTableName ADD COLUMNS (id long, id2 long)\")\n        assert(getDeltaLog.snapshot.schema.size == 2)\n      }\n\n      // schema evolution through df should work\n      // - v1 api\n      withEmptySchemaTable(emptyTableName) {\n        Seq(1, 2, 3).toDF()\n          .write.format(\"delta\")\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .saveAsTable(emptyTableName)\n\n        assert(getDeltaLog.snapshot.schema.size == 1)\n      }\n\n      withEmptySchemaTable(emptyTableName) {\n        Seq(1, 2, 3).toDF()\n          .write.format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"overwriteSchema\", \"true\")\n          .saveAsTable(emptyTableName)\n\n        assert(getDeltaLog.snapshot.schema.size == 1)\n      }\n\n      // - v2 api\n      withEmptySchemaTable(emptyTableName) {\n        Seq(1, 2, 3).toDF()\n            .writeTo(emptyTableName)\n            .option(\"mergeSchema\", \"true\")\n            .append()\n\n        assert(getDeltaLog.snapshot.schema.size == 1)\n      }\n\n      withEmptySchemaTable(emptyTableName) {\n        Seq(1, 2, 3).toDF()\n            .writeTo(emptyTableName)\n            .using(\"delta\")\n            .replace()\n\n        assert(getDeltaLog.snapshot.schema.size == 1)\n      }\n\n\n    }\n  }\n\n  test(\"Create a table with comment\") {\n    val table = \"delta_with_comment\"\n    withTempDir { dir =>\n      withTable(table) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $table (col string)\n             |USING delta\n             |COMMENT 'This is my table'\n             |LOCATION '${dir.getAbsolutePath}'\n            \"\"\".stripMargin)\n        checkResult(\n          sql(s\"DESCRIBE DETAIL $table\"),\n          Seq(\"delta\", \"This is my table\"),\n          Seq(\"format\", \"description\"))\n      }\n    }\n  }\n\n  test(\"Replace a table without comment\") {\n    withTempDir { dir =>\n      val table = \"replace_table_without_comment\"\n      val location = dir.getAbsolutePath\n      withTable(table) {\n        sql(s\"CREATE TABLE $table (col string) USING delta COMMENT 'Table' LOCATION '$location'\")\n        sql(s\"REPLACE TABLE $table (col string) USING delta LOCATION '$location'\")\n        checkResult(\n          sql(s\"DESCRIBE DETAIL $table\"),\n          Seq(\"delta\", null),\n          Seq(\"format\", \"description\"))\n      }\n    }\n  }\n\n  test(\"Replace a table with comment\") {\n    withTempDir { dir =>\n      val table = \"replace_table_with_comment\"\n      val location = dir.getAbsolutePath\n      withTable(table) {\n        sql(s\"CREATE TABLE $table (col string) USING delta LOCATION '$location'\")\n        sql(\n          s\"\"\"\n             |REPLACE TABLE $table (col string)\n             |USING delta\n             |COMMENT 'This is my table'\n             |LOCATION '$location'\n            \"\"\".stripMargin)\n        checkResult(\n          sql(s\"DESCRIBE DETAIL $table\"),\n          Seq(\"delta\", \"This is my table\"),\n          Seq(\"format\", \"description\"))\n      }\n    }\n  }\n\n  test(\"CTAS a table without comment\") {\n    val table = \"ctas_without_comment\"\n    withTable(table) {\n      sql(s\"CREATE TABLE $table USING delta AS SELECT * FROM range(10)\")\n      checkResult(\n        sql(s\"DESCRIBE DETAIL $table\"),\n        Seq(\"delta\", null),\n        Seq(\"format\", \"description\"))\n    }\n  }\n\n  test(\"CTAS a table with comment\") {\n    val table = \"ctas_with_comment\"\n    withTable(table) {\n      sql(\n        s\"\"\"CREATE TABLE $table\n           |USING delta\n           |COMMENT 'This table is created with existing data'\n           |AS SELECT * FROM range(10)\n          \"\"\".stripMargin)\n      checkResult(\n        sql(s\"DESCRIBE DETAIL $table\"),\n        Seq(\"delta\", \"This table is created with existing data\"),\n        Seq(\"format\", \"description\"))\n    }\n  }\n\n  test(\"Replace CTAS a table without comment\") {\n    val table = \"replace_ctas_without_comment\"\n    withTable(table) {\n      sql(\n        s\"\"\"CREATE TABLE $table\n           |USING delta\n           |COMMENT 'This table is created with existing data'\n           |AS SELECT * FROM range(10)\n          \"\"\".stripMargin)\n      sql(s\"REPLACE TABLE $table USING delta AS SELECT * FROM range(10)\")\n      checkResult(\n        sql(s\"DESCRIBE DETAIL $table\"),\n        Seq(\"delta\", null),\n        Seq(\"format\", \"description\"))\n    }\n  }\n\n  test(\"Replace CTAS a table with comment\") {\n    val table = \"replace_ctas_with_comment\"\n    withTable(table) {\n      sql(s\"CREATE TABLE $table USING delta COMMENT 'a' AS SELECT * FROM range(10)\")\n      sql(\n        s\"\"\"REPLACE TABLE $table\n           |USING delta\n           |COMMENT 'This table is created with existing data'\n           |AS SELECT * FROM range(10)\n          \"\"\".stripMargin)\n      checkResult(\n        sql(s\"DESCRIBE DETAIL $table\"),\n        Seq(\"delta\", \"This table is created with existing data\"),\n        Seq(\"format\", \"description\"))\n    }\n  }\n\n  /**\n   * Verifies that the correct table properties are stored in the transaction log as well as the\n   * catalog.\n   */\n  private def verifyTableProperties(\n      tableName: String,\n      deltaLogPropertiesContains: Seq[String],\n      deltaLogPropertiesMissing: Seq[String],\n      catalogStorageProps: Seq[String] = Nil): Unit = {\n    val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))\n\n    if (catalogStorageProps.isEmpty) {\n      assert(table.storage.properties.isEmpty)\n    } else {\n      assert(catalogStorageProps.forall(table.storage.properties.contains),\n        s\"Catalog didn't contain properties: ${catalogStorageProps}.\\n\" +\n          s\"Catalog: ${table.storage.properties}\")\n    }\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n\n    deltaLogPropertiesContains.foreach { prop =>\n      assert(deltaLog.snapshot.getProperties.contains(prop))\n    }\n\n    deltaLogPropertiesMissing.foreach { prop =>\n      assert(!deltaLog.snapshot.getProperties.contains(prop))\n    }\n  }\n\n  test(\"do not store write options in the catalog - DataFrameWriter\") {\n    withTempDir { dir =>\n      withTable(\"t\") {\n        spark.range(10).write.format(\"delta\")\n          .option(\"path\", dir.getCanonicalPath)\n          .option(\"mergeSchema\", \"true\")\n          .option(\"delta.appendOnly\", \"true\")\n          .saveAsTable(\"t\")\n\n        verifyTableProperties(\n          \"t\",\n          // Still allow delta prefixed confs\n          Seq(\"delta.appendOnly\"),\n          Seq(\"mergeSchema\")\n        )\n        // Sanity check that table is readable\n        checkAnswer(spark.table(\"t\"), spark.range(10).toDF())\n      }\n    }\n  }\n\n\n  test(\"do not store write options in the catalog - DataFrameWriterV2\") {\n    withTempDir { dir =>\n      withTable(\"t\") {\n        spark.range(10).writeTo(\"t\").using(\"delta\")\n          .option(\"path\", dir.getCanonicalPath)\n          .option(\"mergeSchema\", \"true\")\n          .option(\"delta.appendOnly\", \"true\")\n          .tableProperty(\"key\", \"value\")\n          .create()\n\n        verifyTableProperties(\n          \"t\",\n          Seq(\n            \"delta.appendOnly\",   // Still allow delta prefixed confs\n            \"key\"                 // Explicit properties should work\n          ),\n          Seq(\"mergeSchema\")\n        )\n        // Sanity check that table is readable\n        checkAnswer(spark.table(\"t\"), spark.range(10).toDF())\n      }\n    }\n  }\n\n  test(\n      \"do not store write options in the catalog - legacy flag\") {\n    withTempDir { dir =>\n      withTable(\"t\") {\n        withSQLConf(DeltaSQLConf.DELTA_LEGACY_STORE_WRITER_OPTIONS_AS_PROPS.key -> \"true\") {\n          spark.range(10).write.format(\"delta\")\n            .option(\"path\", dir.getCanonicalPath)\n            .option(\"mergeSchema\", \"true\")\n            .option(\"delta.appendOnly\", \"true\")\n            .saveAsTable(\"t\")\n\n          verifyTableProperties(\n            \"t\",\n            // Everything gets stored in the transaction log\n            Seq(\"delta.appendOnly\", \"mergeSchema\"),\n            Nil,\n            // Things get stored in the catalog props as well\n            Seq(\"delta.appendOnly\", \"mergeSchema\")\n          )\n\n          checkAnswer(spark.table(\"t\"), spark.range(10).toDF())\n        }\n      }\n    }\n  }\n\n  test(\"create table using varchar at the same location should succeed\") {\n    withTempDir { location =>\n      withTable(\"t1\", \"t2\") {\n        sql(s\"\"\"\n               |create table t1\n               |(colourID string, colourName varchar(128), colourGroupID string)\n               |USING delta LOCATION '$location'\"\"\".stripMargin)\n        sql(\n          s\"\"\"\n             |insert into t1 (colourID, colourName, colourGroupID)\n             |values ('1', 'RED', 'a'), ('2', 'BLUE', 'b')\n             |\"\"\".stripMargin)\n        sql(s\"\"\"\n               |create table t2\n               |(colourID string, colourName varchar(128), colourGroupID string)\n               |USING delta LOCATION '$location'\"\"\".stripMargin)\n        // Verify that select from the second table should be the same as inserted\n        val readout = sql(\n          s\"\"\"\n             |select * from t2 order by colourID\n             |\"\"\".stripMargin).collect()\n        assert(readout.length == 2)\n        assert(readout(0).get(0) == \"1\")\n        assert(readout(0).get(1) == \"RED\")\n        assert(readout(1).get(0) == \"2\")\n        assert(readout(1).get(1) == \"BLUE\")\n      }\n    }\n  }\n\n  test(\"CREATE OR REPLACE TABLE on a catalog table where the backing \" +\n    \"directory has been deleted\") {\n    val tbl = \"delta_tbl\"\n    withTempDir { dir =>\n      withTable(tbl) {\n        val subdir = new File(dir, \"subdir\")\n        sql(s\"CREATE OR REPLACE table $tbl (id String) USING delta \" +\n          s\"LOCATION '${subdir.getCanonicalPath}'\")\n        val tableIdentifier =\n          spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl)).identifier\n        val tableName = tableIdentifier.copy(catalog = None).toString\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n        sql(s\"INSERT INTO $tbl VALUES ('1')\")\n        FileUtils.deleteDirectory(subdir)\n        val e = intercept[DeltaIllegalStateException] {\n          sql(\n            s\"CREATE OR REPLACE table $tbl (id String) USING delta\" +\n              s\" LOCATION '${subdir.getCanonicalPath}'\")\n        }\n        checkError(\n          e,\n          \"DELTA_METADATA_ABSENT_EXISTING_CATALOG_TABLE\",\n          parameters = Map(\n            \"tableName\" -> tableName,\n            \"tablePath\" -> deltaLog.logPath.toString,\n            \"tableNameForDropCmd\" -> tableName\n          ))\n\n        // Table creation should work after running DROP TABLE.\n        sql(s\"DROP table ${e.getMessageParameters().get(\"tableNameForDropCmd\")}\")\n        sql(s\"CREATE OR REPLACE table $tbl (id String) USING delta \" +\n          s\"LOCATION '${subdir.getCanonicalPath}'\")\n        sql(s\"INSERT INTO $tbl VALUES ('21')\")\n        val data = sql(s\"SELECT * FROM $tbl\").collect()\n        assert(data.length == 1)\n      }\n    }\n  }\n\n  private def schemaContainsExistsDefaultKey(testTableName: String): Boolean = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n    snapshot.metadata.schema.fields.exists(\n      _.metadata.contains(ResolveDefaultColumnsUtils.EXISTS_DEFAULT_COLUMN_METADATA_KEY))\n  }\n\n  private def withDeltaTableUsingExistsDefault(testFun: String => Unit): Unit = {\n    val testTableName = \"test_table\"\n    withTable(testTableName) {\n      withSQLConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA.key -> \"false\") {\n        sql(s\"\"\"CREATE TABLE $testTableName (id INT, column_with_default INT DEFAULT 1)\n               |USING DELTA\n               |TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported')\"\"\".stripMargin)\n\n        assert(schemaContainsExistsDefaultKey(testTableName))\n      }\n\n      testFun(testTableName)\n    }\n  }\n\n  test(\"Default column values: Writes do not remove EXISTS_DEFAULT from a table\") {\n    val testTableName = \"test_table\"\n    val writeDF = spark.range(end = 1)\n      .withColumn(\"id\", col(\"id\").cast(\"int\"))\n      .withColumn(\"column_with_default\", col(\"id\"))\n      .write\n      .format(\"delta\")\n    val writeOperations = Seq(\n      () => { sql(s\"ALTER TABLE $testTableName ALTER COLUMN id SET DEFAULT 2\") },\n      () => { sql(s\"ALTER TABLE $testTableName CLUSTER BY (id)\") },\n      () => { sql(s\"COMMENT ON TABLE $testTableName IS 'test comment'\") },\n      () => { sql(s\"INSERT INTO $testTableName VALUES (1, 1)\") },\n      () => { writeDF.mode(\"append\").saveAsTable(testTableName) },\n      () => { writeDF.mode(\"overwrite\").saveAsTable(testTableName) },\n      () => { writeDF.mode(\"append\")\n        .save(DeltaLog.forTable(spark, TableIdentifier(testTableName)).dataPath.toString) }\n    )\n\n    writeOperations.foreach { writeOperation =>\n      withDeltaTableUsingExistsDefault { testTableName =>\n        // Execute the operation and assert that it keep EXISTS_DEFAULT in the schema.\n        withSQLConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA.key -> \"true\") {\n          writeOperation()\n          assert(schemaContainsExistsDefaultKey(testTableName),\n            s\"Operation '$writeOperation' did remove EXISTS_DEFAULT from the schema.\")\n        }\n      }\n    }\n  }\n\n  for ((shortName, createOperation) <- Seq(\n    \"CREATE TABLE\" -> (() => {\n      sql(s\"\"\"CREATE TABLE test_table(int_with_default INT DEFAULT 2)\n             |USING DELTA\n             |TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported')\"\"\".stripMargin)\n    }),\n    \"CREATE OR REPLACE TABLE that CREATES\" -> (() => {\n      sql(s\"\"\"CREATE OR REPLACE TABLE test_table(int_with_default INT DEFAULT 2)\n             |USING DELTA\n             |TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported')\"\"\".stripMargin)\n    }),\n    \"REPLACE TABLE\" -> (() => {\n      withSQLConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA.key -> \"false\") {\n        sql(\"CREATE TABLE test_table(id INT) USING DELTA\")\n      }\n      sql(s\"\"\"REPLACE TABLE test_table(int_with_default INT DEFAULT 2)\n             |USING DELTA\n             |TBLPROPERTIES ('delta.feature.allowColumnDefaults'= 'supported')\"\"\".stripMargin)\n    }),\n    \"CREATE OR REPLACE TABLE that REPLACES\" -> (() => {\n      withSQLConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA.key -> \"false\") {\n        sql(\"CREATE TABLE test_table(id INT) USING DELTA\")\n      }\n      sql(s\"\"\"CREATE OR REPLACE TABLE test_table(int_with_default INT DEFAULT 2)\n             |USING DELTA\n             |TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported')\"\"\".stripMargin)\n    }))\n  ) {\n    test(s\"Default column values: Storing 'EXISTS_DEFAULT' in $shortName with column defaults\") {\n      val testTableName = \"test_table\"\n      for (removeExistsDefault <- Seq(true, false)) {\n        withTable(testTableName) {\n          withSQLConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA.key ->\n            removeExistsDefault.toString) {\n            createOperation()\n            assert(schemaContainsExistsDefaultKey(testTableName) != removeExistsDefault)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Default column values: SHALLOW CLONE keeps EXISTS_DEFAULT\") {\n    withDeltaTableUsingExistsDefault { testTableName =>\n      val targetTableName = \"target_table\"\n      withTable(targetTableName) {\n        sql(s\"CREATE TABLE $targetTableName SHALLOW CLONE $testTableName\")\n        assert(schemaContainsExistsDefaultKey(targetTableName),\n          s\"SHALLOW CLONE did remove EXISTS_DEFAULT from the schema.\")\n      }\n    }\n  }\n\n  test(\"Default column values: CONVERT TO DELTA keeps EXISTS_DEFAULT\") {\n    withTable(\"test_table\") {\n      withSQLConf(\"spark.databricks.delta.properties.defaults.columnMapping.mode\" -> \"none\") {\n        spark.range(end = 1).write.format(\"parquet\").saveAsTable(\"test_table\")\n        // EXISTS_DEFAULT is used for the existing row.\n        sql(\"ALTER TABLE test_table ADD COLUMN new_column_with_a_default INT DEFAULT 1\")\n\n        sql(\"CONVERT TO DELTA test_table\")\n\n        checkAnswer(spark.table(\"test_table\"), Row(0, 1) :: Nil)\n      }\n    }\n  }\n\n  test(\"Default column values: CREATE TABLE AS SELECT from a table with column defaults\") {\n    for (sourceTableSchemaContainsKey <- Seq(true, false)) {\n      withTable(\"test_table\", \"test_table_2\", \"test_table_3\", \"test_table_4\") {\n          // To test with the 'EXISTS_DEFAULT' key present in the source table, we disable removal.\n          withSQLConf(DeltaSQLConf.REMOVE_EXISTS_DEFAULT_FROM_SCHEMA.key\n            -> (!sourceTableSchemaContainsKey).toString) {\n            // Defaults are only possible for top level columns.\n            sql(\"\"\"CREATE TABLE test_table(int_col INT DEFAULT 2)\n                  |USING DELTA\n                  |TBLPROPERTIES ('delta.feature.allowColumnDefaults' = 'supported')\"\"\".stripMargin)\n          }\n\n          def schemaContainsExistsKey(tableName: String): Boolean = {\n            val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n            snapshot.schema.fields.exists { field =>\n              field.metadata.contains(ResolveDefaultColumnsUtils.EXISTS_DEFAULT_COLUMN_METADATA_KEY)\n            }\n          }\n\n          def schemaContainsCurrentDefaultKey(tableName: String): Boolean = {\n            val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n            snapshot.schema.fields.exists { field =>\n              field.metadata.contains(\n                ResolveDefaultColumnsUtils.CURRENT_DEFAULT_COLUMN_METADATA_KEY)\n            }\n          }\n\n          def defaultsTableFeatureEnabled(tableName: String): Boolean = {\n            val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n            val isEnabled =\n              snapshot.protocol.writerFeatureNames.contains(AllowColumnDefaultsTableFeature.name)\n            assert(schemaContainsCurrentDefaultKey(tableName) === isEnabled)\n            isEnabled\n          }\n\n          assert(schemaContainsExistsKey(\"test_table\") == sourceTableSchemaContainsKey)\n          assert(defaultsTableFeatureEnabled(\"test_table\"))\n\n          // It is not possible to add a column with a default to a Delta table.\n          assertThrows[DeltaAnalysisException] {\n            sql(\"ALTER TABLE test_table ADD COLUMN new_column_with_a_default INT DEFAULT 0\")\n          }\n\n          // @TODO: It is currently not possible to CTAS from a table with an active column default\n          //        without explicitly enabling the table feature.\n          assertThrows[AnalysisException] {\n            sql(\"CREATE TABLE test_table_2 USING DELTA AS SELECT * FROM test_table\")\n          }\n\n          // @TODO: It is possible to CTAS from a table with an active column default when the table\n          //        feature is explicitly enabled. This copies the default values setting, which is\n          //        probably not the desired behaviour.\n          sql(\"\"\"CREATE TABLE test_table_3\n                |USING DELTA\n                |TBLPROPERTIES ('delta.feature.allowColumnDefaults' = 'supported')\n                |AS SELECT * FROM test_table\"\"\".stripMargin)\n          assert(schemaContainsCurrentDefaultKey(\"test_table_3\"))\n          assert(schemaContainsExistsKey(\"test_table_3\") === false)\n          assert(defaultsTableFeatureEnabled(\"test_table_3\"))\n\n          // Remove the active column default from the source table and CTAS from it.\n          sql(\"ALTER TABLE test_table ALTER COLUMN int_col DROP DEFAULT\")\n          sql(\"CREATE TABLE test_table_4 USING DELTA AS SELECT * FROM test_table\")\n          assert(schemaContainsCurrentDefaultKey(\"test_table_4\") === false)\n          assert(schemaContainsExistsKey(\"test_table_4\") === false)\n          assert(defaultsTableFeatureEnabled(\"test_table_4\") === false)\n      }\n    }\n  }\n}\n\ntrait DeltaTableCreationColumnMappingSuiteBase extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests: Seq[String] = Seq(\n    \"create table with schema and path\",\n    \"create external table without schema\",\n    \"REPLACE TABLE\",\n    \"CREATE OR REPLACE TABLE on non-empty directory\"\n  ) ++ Seq(\"partitioned\" -> Seq(\"v2\"), \"non-partitioned\" -> Nil)\n    .flatMap { case (isPartitioned, cols) =>\n      SaveMode.values().flatMap { saveMode =>\n        Seq(\n          s\"saveAsTable to a new table (managed) - $isPartitioned, saveMode: $saveMode\",\n          s\"saveAsTable to a new table (external) - $isPartitioned, saveMode: $saveMode\")\n      }\n    } ++ Seq(\"a b\", \"a:b\", \"a%b\").map { specialChars =>\n      s\"location uri contains $specialChars for datasource table\"\n    }\n}\n\nclass DeltaTableCreationIdColumnMappingSuite extends DeltaTableCreationSuite\n  with DeltaColumnMappingEnableIdMode {\n\n  override protected def getTableProperties(tableName: String): Map[String, String] = {\n    // ignore comparing column mapping properties\n    dropColumnMappingConfigurations(super.getTableProperties(tableName))\n  }\n}\n\nclass DeltaTableCreationNameColumnMappingSuite extends DeltaTableCreationSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  override protected def getTableProperties(tableName: String): Map[String, String] = {\n    // ignore comparing column mapping properties\n    dropColumnMappingConfigurations(super.getTableProperties(tableName))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaTableFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.coordinatedcommits.{CommitCoordinatorProvider, InMemoryCommitCoordinatorBuilder}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames.unsafeDeltaFile\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\n\nclass DeltaTableFeatureSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  private lazy val testTableSchema = spark.range(1).schema\n  override protected def sparkConf: SparkConf = {\n    // All the drop feature tests below are targeting the drop feature with history truncation\n    // implementation. The fast drop feature implementation adds a new writer feature when dropping\n    // a feature and also does not require any waiting time. The fast drop feature implementation\n    // is tested extensively in the DeltaFastDropFeatureSuite.\n    super.sparkConf.set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, \"false\")\n  }\n\n  // This is solely a test hook. Users cannot create new Delta tables with protocol lower than\n  // that of their current version.\n  protected def createTableWithProtocol(\n      protocol: Protocol,\n      path: File,\n      schema: StructType = testTableSchema): DeltaLog = {\n    val log = DeltaLog.forTable(spark, path)\n    log.createLogDirectoriesIfNotExists()\n    log.store.write(\n      unsafeDeltaFile(log.logPath, 0),\n      Iterator(Metadata(schemaString = schema.json).json, protocol.json),\n      overwrite = false,\n      log.newDeltaHadoopConf())\n    log.update()\n    log\n  }\n\n  test(\"all defined table features are registered\") {\n    import scala.reflect.runtime.{universe => ru}\n\n    val subClassNames = mutable.Set[String]()\n    def collect(clazz: ru.Symbol): Unit = {\n      val collected = clazz.asClass.knownDirectSubclasses\n      // add only table feature objects to the result set\n      subClassNames ++= collected.filter(_.isModuleClass).map(_.name.toString)\n      collected.filter(_.isAbstract).foreach(collect)\n    }\n    collect(ru.typeOf[TableFeature].typeSymbol)\n\n    val registeredFeatures = TableFeature.allSupportedFeaturesMap.values\n      .map(_.getClass.getSimpleName.stripSuffix(\"$\")) // remove '$' from object names\n      .toSet\n    val notRegisteredFeatures = subClassNames.diff(registeredFeatures)\n\n    assert(\n      notRegisteredFeatures.isEmpty,\n      \"Expecting all defined table features are registered (either as prod or testing-only) \" +\n        s\"but the followings are not: $notRegisteredFeatures\")\n  }\n\n  test(\"adding feature requires supported protocol version\") {\n    assert(\n      intercept[DeltaTableFeatureException] {\n        Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeature(TestLegacyReaderWriterFeature)\n      }.getMessage.contains(\"Unable to enable table feature testLegacyReaderWriter because it \" +\n        \"requires a higher reader protocol version\"))\n\n    assert(intercept[DeltaTableFeatureException] {\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, 6)\n    }.getMessage.contains(\"Unable to upgrade only the reader protocol version\"))\n\n    assert(\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(AppendOnlyTableFeature)\n        .readerAndWriterFeatureNames === Set(AppendOnlyTableFeature.name))\n\n    assert(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestReaderWriterFeature)\n        .readerAndWriterFeatureNames === Set(TestReaderWriterFeature.name))\n  }\n\n  test(\"adding feature automatically adds all dependencies\") {\n    assert(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestFeatureWithDependency)\n        .readerAndWriterFeatureNames ===\n        Set(TestFeatureWithDependency.name, TestReaderWriterFeature.name))\n\n    assert(\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(TestFeatureWithTransitiveDependency)\n        .readerAndWriterFeatureNames ===\n        Set(\n          TestFeatureWithTransitiveDependency.name,\n          TestFeatureWithDependency.name,\n          TestReaderWriterFeature.name))\n\n    // Validate new protocol has required features enabled when a writer feature requires a\n    // reader/write feature.\n    val metadata = Metadata(\n      configuration = Map(\n        TableFeatureProtocolUtils.propertyKey(TestWriterFeatureWithTransitiveDependency) ->\n          TableFeatureProtocolUtils.FEATURE_PROP_SUPPORTED))\n    assert(\n      Protocol\n        .forNewTable(\n          spark,\n          Some(metadata))\n        .readerAndWriterFeatureNames ===\n        Set(\n          AppendOnlyTableFeature.name,\n          InvariantsTableFeature.name,\n          TestWriterFeatureWithTransitiveDependency.name,\n          TestFeatureWithDependency.name,\n          TestReaderWriterFeature.name))\n  }\n\n  test(\"implicitly-enabled features\") {\n    assert(\n      Protocol(2, 6).implicitlySupportedFeatures === Set(\n        AppendOnlyTableFeature,\n        ColumnMappingTableFeature,\n        InvariantsTableFeature,\n        CheckConstraintsTableFeature,\n        ChangeDataFeedTableFeature,\n        GeneratedColumnsTableFeature,\n        IdentityColumnsTableFeature,\n        TestLegacyWriterFeature,\n        TestLegacyReaderWriterFeature,\n        TestRemovableLegacyWriterFeature,\n        TestRemovableLegacyReaderWriterFeature))\n    assert(\n      Protocol(2, 5).implicitlySupportedFeatures === Set(\n        AppendOnlyTableFeature,\n        ColumnMappingTableFeature,\n        InvariantsTableFeature,\n        CheckConstraintsTableFeature,\n        ChangeDataFeedTableFeature,\n        GeneratedColumnsTableFeature,\n        TestLegacyWriterFeature,\n        TestLegacyReaderWriterFeature,\n        TestRemovableLegacyWriterFeature,\n        TestRemovableLegacyReaderWriterFeature))\n    assert(Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).implicitlySupportedFeatures === Set())\n    assert(\n      Protocol(\n        TABLE_FEATURES_MIN_READER_VERSION,\n        TABLE_FEATURES_MIN_WRITER_VERSION).implicitlySupportedFeatures === Set())\n  }\n\n  test(\"implicit feature listing\") {\n    assert(\n      intercept[DeltaTableFeatureException] {\n        Protocol(1, 4).withFeature(TestLegacyReaderWriterFeature)\n      }.getMessage.contains(\n        \"Unable to enable table feature testLegacyReaderWriter because it requires a higher \" +\n          \"reader protocol version (current 1)\"))\n\n    assert(\n      intercept[DeltaTableFeatureException] {\n        Protocol(2, 4).withFeature(TestLegacyReaderWriterFeature)\n      }.getMessage.contains(\n        \"Unable to enable table feature testLegacyReaderWriter because it requires a higher \" +\n          \"writer protocol version (current 4)\"))\n\n    assert(\n      intercept[DeltaTableFeatureException] {\n        Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)\n      }.getMessage.contains(\n        \"Unable to enable table feature testLegacyReaderWriter because it requires a higher \" +\n          \"reader protocol version (current 1)\"))\n\n    val protocol =\n      Protocol(2, TABLE_FEATURES_MIN_WRITER_VERSION).withFeature(TestLegacyReaderWriterFeature)\n    assert(!protocol.readerFeatures.isDefined)\n    assert(\n      protocol.writerFeatures.get === Set(TestLegacyReaderWriterFeature.name))\n  }\n\n  test(\"merge protocols\") {\n    val tfProtocol1 = Protocol(1, TABLE_FEATURES_MIN_WRITER_VERSION)\n    val tfProtocol2 =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n\n    assert(tfProtocol1.merge(Protocol(1, 2)) === Protocol(1, 2))\n    assert(tfProtocol2.merge(Protocol(2, 6)) === Protocol(2, 6))\n  }\n\n  test(\"protocol upgrade compatibility\") {\n    assert(Protocol(1, 1).canUpgradeTo(Protocol(1, 1)))\n    assert(Protocol(1, 1).canUpgradeTo(Protocol(2, 1)))\n    assert(\n      Protocol(1, 1).canUpgradeTo(\n        Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)))\n    assert(\n      !Protocol(2, 3).canUpgradeTo(\n        Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)))\n    assert(\n      !Protocol(2, 6).canUpgradeTo(\n        Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeatures(\n            Seq(\n              // With one feature not referenced, `canUpgradeTo` must be `false`.\n              // AppendOnlyTableFeature,\n              InvariantsTableFeature,\n              CheckConstraintsTableFeature,\n              ChangeDataFeedTableFeature,\n              GeneratedColumnsTableFeature,\n              IdentityColumnsTableFeature,\n              ColumnMappingTableFeature,\n              TestLegacyWriterFeature,\n              TestLegacyReaderWriterFeature,\n              TestRemovableLegacyWriterFeature,\n              TestRemovableLegacyReaderWriterFeature))))\n    assert(\n      Protocol(2, 6).canUpgradeTo(\n        Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeatures(Seq(\n            AppendOnlyTableFeature,\n            InvariantsTableFeature,\n            CheckConstraintsTableFeature,\n            ChangeDataFeedTableFeature,\n            GeneratedColumnsTableFeature,\n            IdentityColumnsTableFeature,\n            ColumnMappingTableFeature,\n            TestLegacyWriterFeature,\n            TestLegacyReaderWriterFeature,\n            TestRemovableLegacyWriterFeature,\n            TestRemovableLegacyReaderWriterFeature))))\n  }\n\n  test(\"protocol downgrade compatibility\") {\n    val tableFeatureProtocol =\n      Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n    assert(Protocol(1, 7).withFeature(TestWriterFeature)\n      .canDowngradeTo(Protocol(1, 7), droppedFeatureName = TestWriterFeature.name))\n    // When there are no explicit features the protocol versions need to be downgraded\n    // below table features. The new protocol versions need to match exactly the supported\n    // legacy features.\n    for (n <- 1 to 3) {\n      assert(\n        !Protocol(n, 7)\n          .withFeatures(Seq(TestWriterFeature, AppendOnlyTableFeature))\n          .canDowngradeTo(Protocol(1, 2), droppedFeatureName = TestWriterFeature.name))\n      assert(\n        Protocol(n, 7)\n          .withFeatures(Seq(TestWriterFeature, AppendOnlyTableFeature, InvariantsTableFeature))\n          .canDowngradeTo(Protocol(1, 2), droppedFeatureName = TestWriterFeature.name))\n    }\n    assert(tableFeatureProtocol.withFeatures(Seq(TestReaderWriterFeature))\n      .canDowngradeTo(Protocol(1, 1), droppedFeatureName = TestReaderWriterFeature.name))\n    assert(\n      tableFeatureProtocol\n        .withFeatures(Seq(TestReaderWriterFeature, TestRemovableLegacyReaderWriterFeature))\n        .merge(Protocol(2, 5))\n        .canDowngradeTo(Protocol(2, 5), droppedFeatureName = TestReaderWriterFeature.name))\n    // Downgraded protocol must be able to support all legacy table features.\n    assert(\n      !tableFeatureProtocol\n        .withFeatures(Seq(TestWriterFeature, AppendOnlyTableFeature, ColumnMappingTableFeature))\n        .canDowngradeTo(Protocol(2, 4), droppedFeatureName = TestWriterFeature.name))\n    assert(\n      tableFeatureProtocol\n        .withFeatures(Seq(TestWriterFeature, AppendOnlyTableFeature, ColumnMappingTableFeature))\n        .merge(Protocol(2, 5))\n        .canDowngradeTo(Protocol(2, 5), droppedFeatureName = TestWriterFeature.name))\n  }\n\n  test(\"add reader and writer feature descriptors\") {\n    var p = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n    val name = AppendOnlyTableFeature.name\n    p = p.withReaderFeatures(Seq(name))\n    assert(p.readerFeatures === Some(Set(name)))\n    assert(p.writerFeatures === Some(Set.empty))\n    p = p.withWriterFeatures(Seq(name))\n    assert(p.readerFeatures === Some(Set(name)))\n    assert(p.writerFeatures === Some(Set(name)))\n  }\n\n  test(\"native automatically-enabled feature can't be implicitly enabled\") {\n    val p = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n    assert(p.implicitlySupportedFeatures.isEmpty)\n  }\n\n  test(\"Table features are not automatically enabled by default table property settings\") {\n    withTable(\"tbl\") {\n      spark.range(10).write.format(\"delta\").saveAsTable(\"tbl\")\n      val snapshot = DeltaLog.forTable(spark, TableIdentifier(\"tbl\")).update()\n      TableFeature.allSupportedFeaturesMap.values.foreach {\n        case feature: FeatureAutomaticallyEnabledByMetadata =>\n          assert(\n            !feature.metadataRequiresFeatureToBeEnabled(\n              snapshot.protocol, snapshot.metadata, spark),\n            s\"\"\"\n               |${feature.name} is automatically enabled by the default metadata. This will lead to\n               |the inability of reading existing tables that do not have the feature enabled and\n               |should not reach production! If this is only for testing purposes, ignore this test.\n               \"\"\".stripMargin)\n        case _ =>\n      }\n    }\n  }\n\n  test(\"Can enable legacy metadata table feature by setting default table property key\") {\n    withSQLConf(\n      s\"$DEFAULT_FEATURE_PROP_PREFIX${TestWriterFeature.name}\" -> \"enabled\",\n      DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\") {\n      withTable(\"tbl\") {\n        spark.range(10).write.format(\"delta\").saveAsTable(\"tbl\")\n        val log = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n        val protocol = log.update().protocol\n        assert(protocol.readerAndWriterFeatureNames === Set(\n          AppendOnlyTableFeature.name,\n          InvariantsTableFeature.name,\n          ColumnMappingTableFeature.name,\n          TestWriterFeature.name))\n      }\n    }\n  }\n\n  test(\"CLONE does not take into account default table features\") {\n    withTable(\"tbl\") {\n      spark.range(0).write.format(\"delta\").saveAsTable(\"tbl\")\n      val log = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n      val protocolBefore = log.update().protocol\n      withSQLConf(defaultPropertyKey(TestWriterFeature) -> \"enabled\") {\n        sql(buildTablePropertyModifyingCommand(\n          commandName = \"CLONE\", targetTableName = \"tbl\", sourceTableName = \"tbl\")\n        )\n      }\n      val protocolAfter = log.update().protocol\n      assert(protocolBefore === protocolAfter)\n    }\n  }\n\n  test(\"CLONE only enables enabled metadata table features\") {\n    withTable(\"src\", \"target\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key ->\n          TABLE_FEATURES_MIN_WRITER_VERSION.toString,\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key ->\n          TABLE_FEATURES_MIN_READER_VERSION.toString,\n        DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\") {\n        spark.range(0).write.format(\"delta\").saveAsTable(\"src\")\n      }\n      sql(buildTablePropertyModifyingCommand(\n        commandName = \"CLONE\", targetTableName = \"target\", sourceTableName = \"src\"))\n      val targetLog = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n      val protocol = targetLog.update().protocol\n      assert(protocol.readerAndWriterFeatureNames === Set(\n        ColumnMappingTableFeature.name))\n    }\n  }\n\n  for(commandName <- Seq(\"ALTER\", \"REPLACE\", \"CREATE OR REPLACE\", \"CLONE\")) {\n    test(s\"Can enable legacy metadata table feature during $commandName TABLE\") {\n      withSQLConf(\n        s\"${defaultPropertyKey(TestWriterFeature)}\" -> \"enabled\") {\n        withTable(\"tbl\") {\n          spark.range(0).write.format(\"delta\").saveAsTable(\"tbl\")\n          val log = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n\n          val tblProperties = Seq(\"'delta.enableChangeDataFeed' = true\")\n          sql(buildTablePropertyModifyingCommand(\n            commandName, targetTableName = \"tbl\", sourceTableName = \"tbl\", tblProperties))\n          val protocol = log.update().protocol\n          assert(protocol.readerAndWriterFeatureNames === Set(\n            AppendOnlyTableFeature.name,\n            InvariantsTableFeature.name,\n            ChangeDataFeedTableFeature.name,\n            TestWriterFeature.name))\n        }\n      }\n    }\n  }\n\n  for(commandName <- Seq(\"ALTER\", \"CLONE\", \"REPLACE\", \"CREATE OR REPLACE\")) {\n    test(\"Enabling table feature on already existing table enables all table features \" +\n      s\"up to the table's protocol version during $commandName TABLE\") {\n      withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\") {\n        withTable(\"tbl\") {\n          spark.range(0).write.format(\"delta\").saveAsTable(\"tbl\")\n          val log = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n          assert(log.update().protocol === Protocol(2, 7).withFeatures(Seq(\n            AppendOnlyTableFeature,\n            InvariantsTableFeature,\n            ColumnMappingTableFeature)))\n          val tblProperties = Seq(s\"'$FEATURE_PROP_PREFIX${TestWriterFeature.name}' = 'enabled'\",\n            s\"'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION\")\n          sql(buildTablePropertyModifyingCommand(\n            commandName, targetTableName = \"tbl\", sourceTableName = \"tbl\", tblProperties))\n          val newProtocol = log.update().protocol\n          assert(newProtocol.readerAndWriterFeatureNames === Set(\n            AppendOnlyTableFeature.name,\n            InvariantsTableFeature.name,\n            ColumnMappingTableFeature.name,\n            TestWriterFeature.name))\n        }\n      }\n    }\n  }\n\n  for(commandName <- Seq(\"ALTER\", \"CLONE\", \"REPLACE\", \"CREATE OR REPLACE\")) {\n    test(s\"Vacuum Protocol Check is disabled by default but can be enabled during $commandName\") {\n      val table = \"tbl\"\n      withTable(table) {\n        spark.range(0).write.format(\"delta\").saveAsTable(table)\n        val log = DeltaLog.forTable(spark, TableIdentifier(table))\n        val protocol = log.update().protocol\n        assert(!protocol.readerAndWriterFeatureNames.contains(VacuumProtocolCheckTableFeature.name))\n\n        val tblProperties1 = Seq(s\"'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION\")\n        sql(buildTablePropertyModifyingCommand(\n          commandName, targetTableName = table, sourceTableName = table, tblProperties1))\n        val newProtocol1 = log.update().protocol\n        assert(!newProtocol1.readerAndWriterFeatureNames.contains(\n          VacuumProtocolCheckTableFeature.name))\n\n        val tblProperties2 = Seq(s\"'$FEATURE_PROP_PREFIX${VacuumProtocolCheckTableFeature.name}' \" +\n          s\"= 'supported', 'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION\")\n        sql(buildTablePropertyModifyingCommand(\n          commandName, targetTableName = table, sourceTableName = table, tblProperties2))\n        val newProtocol2 = log.update().protocol\n        assert(newProtocol2.readerAndWriterFeatureNames.contains(\n          VacuumProtocolCheckTableFeature.name))\n      }\n    }\n  }\n\n  test(\"drop table feature works with coordinated commits\") {\n    val table = \"tbl\"\n    withTable(table) {\n      spark.range(0).write.format(\"delta\").saveAsTable(table)\n      val log = DeltaLog.forTable(spark, TableIdentifier(table))\n      val featureName = TestRemovableReaderWriterFeature.name\n      assert(!log.update().protocol.readerAndWriterFeatureNames.contains(featureName))\n\n      // Add coordinated commits table feature to the table\n      CommitCoordinatorProvider.registerBuilder(InMemoryCommitCoordinatorBuilder(batchSize = 100))\n      val tblProperties1 =\n        Seq(s\"'${DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key}' = 'in-memory'\",\n          s\"'${DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '{}'\")\n      sql(buildTablePropertyModifyingCommand(\n        \"ALTER\", targetTableName = table, sourceTableName = table, tblProperties1))\n\n      // Add TestRemovableReaderWriterFeature to the table in unbackfilled delta files\n      val tblProperties2 = Seq(s\"'$FEATURE_PROP_PREFIX$featureName' = 'supported', \" +\n        s\"'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION, \" +\n        s\"'${TestRemovableReaderWriterFeature.TABLE_PROP_KEY}' = 'true'\")\n      sql(buildTablePropertyModifyingCommand(\n        \"ALTER\", targetTableName = table, sourceTableName = table, tblProperties2))\n      assert(log.update().protocol.readerAndWriterFeatureNames.contains(featureName))\n\n      // Disable feature on the latest snapshot\n      val tblProperties3 = Seq(s\"'${TestRemovableReaderWriterFeature.TABLE_PROP_KEY}' = 'false'\")\n      sql(buildTablePropertyModifyingCommand(\n        \"ALTER\", targetTableName = table, sourceTableName = table, tblProperties3))\n\n      val tableFeature =\n        TableFeature.featureNameToFeature(featureName).get.asInstanceOf[RemovableFeature]\n      assert(tableFeature.historyContainsFeature(\n        spark, DeltaTableV2(spark, log.dataPath), log.update()))\n\n      // Dropping feature should fail because the feature still has traces in deltas.\n      val e = intercept[DeltaTableFeatureException] {\n        sql(s\"ALTER TABLE $table DROP FEATURE $featureName\")\n      }\n      assert(e.getMessage.contains(\"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\"), e)\n\n      // Add in a checkpoint and cleanUp up older logs containing feature traces\n      log.startTransaction().commitManually()\n      log.checkpoint()\n      log.cleanUpExpiredLogs(log.update(), deltaRetentionMillisOpt = Some(-1000000000000L))\n      sql(s\"ALTER TABLE $table DROP FEATURE $featureName\")\n      assert(!log.update().protocol.readerAndWriterFeatureNames.contains(featureName))\n    }\n  }\n\n  private def buildTablePropertyModifyingCommand(\n      commandName: String,\n      targetTableName: String,\n      sourceTableName: String,\n      tblProperties: Seq[String] = Seq.empty): String = {\n    val commandStr = if (commandName == \"CLONE\") {\n      \"CREATE OR REPLACE\"\n    } else {\n      commandName\n    }\n\n    val cloneClause = if (commandName == \"CLONE\") {\n      s\"SHALLOW CLONE $sourceTableName\"\n    } else {\n      \"\"\n    }\n\n    val (usingDeltaClause, dataSourceClause) = if (\"ALTER\" != commandName &&\n      \"CLONE\" != commandName) {\n      (\"USING DELTA\", s\"AS SELECT * FROM $sourceTableName\")\n    } else {\n      (\"\", \"\")\n    }\n    var tblPropertiesClause = \"\"\n    if (tblProperties.nonEmpty) {\n      if (commandName == \"ALTER\") {\n        tblPropertiesClause += \"SET \"\n      }\n      tblPropertiesClause += s\"TBLPROPERTIES ${tblProperties.mkString(\"(\", \",\", \")\")}\"\n    }\n    s\"\"\"$commandStr TABLE $targetTableName\n       |$usingDeltaClause\n       |$cloneClause\n       |$tblPropertiesClause\n       |$dataSourceClause\n       |\"\"\".stripMargin\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaTableUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.net.URI\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.hadoop.fs.{Path, RawLocalFileSystem}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nclass DeltaTableUtilsSuite extends SharedSparkSession with DeltaSQLCommandTest {\n\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(\"spark.hadoop.fs.s3.impl\", classOf[MockS3FileSystem].getCanonicalName)\n\n  test(\"findDeltaTableRoot correctly combines paths\") {\n    val path1 = new Path(\"s3://my-bucket\")\n    assert(DeltaTableUtils.findDeltaTableRoot(spark, path1).isEmpty)\n    val path2 = new Path(\"s3://my-bucket/\")\n    assert(DeltaTableUtils.findDeltaTableRoot(spark, path2).isEmpty)\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE myTable (id INT) USING DELTA LOCATION '${dir.getAbsolutePath}'\")\n      val path = new Path(s\"file://${dir.getAbsolutePath}\")\n      assert(DeltaTableUtils.findDeltaTableRoot(spark, path).contains(path))\n    }\n  }\n\n  test(\"safeConcatPaths\") {\n    val basePath = new Path(\"s3://my-bucket/subfolder\")\n    val basePathEmpty = new Path(\"s3://my-bucket\")\n    assert(DeltaTableUtils.safeConcatPaths(basePath, \"_delta_log\") ==\n      new Path(\"s3://my-bucket/subfolder/_delta_log\"))\n    assert(DeltaTableUtils.safeConcatPaths(basePathEmpty, \"_delta_log\") ==\n      new Path(\"s3://my-bucket/_delta_log\"))\n    assert(DeltaTableUtils.safeConcatPaths(basePath, \"_delta/_log\") ==\n      new Path(\"s3://my-bucket/subfolder/_delta/_log\"))\n    assert(DeltaTableUtils.safeConcatPaths(basePathEmpty, \"_delta/_log\") ==\n      new Path(\"s3://my-bucket/_delta/_log\"))\n\n    withSQLConf(DeltaSQLConf.DELTA_WORK_AROUND_COLONS_IN_HADOOP_PATHS.key -> \"false\") {\n      assert(intercept[IllegalArgumentException] {\n        DeltaTableUtils.safeConcatPaths(basePath, \"part-2024-03-05T16:08:53.002.csv\")\n      }.getMessage.contains(\"Relative path in absolute URI\"))\n    }\n\n    withSQLConf(DeltaSQLConf.DELTA_WORK_AROUND_COLONS_IN_HADOOP_PATHS.key -> \"true\") {\n      assert(DeltaTableUtils.safeConcatPaths(basePath, \"part-2024-03-05T16:08:53.002.csv\") ==\n        new Path(\"s3://my-bucket/subfolder/part-2024-03-05T16:08:53.002.csv\"))\n      assert(DeltaTableUtils.safeConcatPaths(basePathEmpty, \"part-2024-03-05T16:08:53.002.csv\") ==\n        new Path(\"s3://my-bucket/part-2024-03-05T16:08:53.002.csv\"))\n      assert(DeltaTableUtils.safeConcatPaths(basePath, \"part/2024-03-05T16:08:53.002.csv\") ==\n        new Path(\"s3://my-bucket/subfolder/part/2024-03-05T16:08:53.002.csv\"))\n      assert(DeltaTableUtils.safeConcatPaths(basePathEmpty, \"part/2024-03-05T16:08:53.002.csv\") ==\n        new Path(\"s3://my-bucket/part/2024-03-05T16:08:53.002.csv\"))\n    }\n  }\n\n  test(\"removeInternalWriterMetadata\") {\n    for (flag <- BOOLEAN_DOMAIN) {\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_REMOVE_SPARK_INTERNAL_METADATA.key -> flag.toString) {\n        for (internalMetadataKey <- DeltaTableUtils.SPARK_INTERNAL_METADATA_KEYS) {\n          val metadata = new MetadataBuilder()\n            .putString(internalMetadataKey, \"foo\")\n            .putString(\"other\", \"bar\")\n            .build()\n          val schema = StructType(Seq(StructField(\"foo\", StringType, metadata = metadata)))\n          val newSchema = DeltaTableUtils.removeInternalWriterMetadata(spark, schema)\n          newSchema.foreach { f =>\n            if (flag) {\n              // Flag on: should remove internal metadata\n              assert(!f.metadata.contains(internalMetadataKey))\n              // Should reserve non internal metadata\n              assert(f.metadata.contains(\"other\"))\n            } else {\n              // Flag off: no-op\n              assert(f.metadata == metadata)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\nprivate class MockS3FileSystem extends RawLocalFileSystem {\n  override def getScheme: String = \"s3\"\n  override def getUri: URI = URI.create(\"s3://my-bucket\")\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{BufferedReader, File, InputStreamReader}\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.{Locale, TimeZone}\nimport java.util.concurrent.{ConcurrentHashMap, TimeUnit}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent\nimport scala.reflect.ClassTag\nimport scala.reflect.runtime.universe._\nimport scala.util.matching.Regex\n\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.DeltaTestUtils.Plans\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, FileNames}\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport io.delta.tables.{DeltaTable => IODeltaTable}\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.hadoop.fs.Path\nimport org.scalactic.source.Position\nimport org.scalatest.{BeforeAndAfterEach, Tag}\n\nimport org.apache.spark.{SparkConf, SparkContext, SparkFunSuite, SparkThrowable}\nimport org.apache.spark.scheduler.{JobFailed, SparkListener, SparkListenerJobEnd, SparkListenerJobStart}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, DataFrameWriter, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.util.{quietly, FailFastMode}\nimport org.apache.spark.sql.execution.{FileSourceScanExec, QueryExecution, RDDScanExec, SparkPlan, WholeStageCodegenExec}\nimport org.apache.spark.sql.execution.aggregate.HashAggregateExec\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.sql.util.QueryExecutionListener\nimport org.apache.spark.util.{ManualClock, SystemClock, Utils}\n\nobject DeltaTestUtilsBase {\n  final val BOOLEAN_DOMAIN: Seq[Boolean] = Seq(true, false)\n}\n\ntrait CDCTestMixin extends SharedSparkSession {\n  // Setting the spark Conf is left to the test implementation.\n\n  def computeCDC(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      startVersion: Long,\n      endVersion: Long,\n      predicates: Seq[Expression] = Seq.empty): DataFrame = {\n    CDCReader.changesToBatchDF(deltaLog, startVersion, endVersion, spark)\n  }\n}\n\ntrait DeltaTestUtilsBase {\n  import DeltaTestUtils.TableIdentifierOrPath\n\n  // Re-define here to avoid the need to import it before using\n  final def BOOLEAN_DOMAIN: Seq[Boolean] = DeltaTestUtilsBase.BOOLEAN_DOMAIN\n\n  class PlanCapturingListener() extends QueryExecutionListener {\n\n    private[this] var capturedPlans = List.empty[Plans]\n\n    def plans: Seq[Plans] = capturedPlans.reverse\n\n    override def onSuccess(funcName: String, qe: QueryExecution, durationNs: Long): Unit = {\n      capturedPlans ::= Plans(\n          qe.analyzed,\n          qe.optimizedPlan,\n          qe.sparkPlan,\n          qe.executedPlan)\n    }\n\n    override def onFailure(\n      funcName: String, qe: QueryExecution, error: Exception): Unit = {}\n  }\n\n  /**\n   * Run a thunk with physical plans for all queries captured and passed into a provided buffer.\n   */\n  def withLogicalPlansCaptured[T](\n      spark: SparkSession,\n      optimizedPlan: Boolean)(\n      thunk: => Unit): Seq[LogicalPlan] = {\n    val planCapturingListener = new PlanCapturingListener\n\n    spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n    spark.listenerManager.register(planCapturingListener)\n    try {\n      thunk\n      spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n      planCapturingListener.plans.map { plans =>\n        if (optimizedPlan) plans.optimized else plans.analyzed\n      }\n    } finally {\n      spark.listenerManager.unregister(planCapturingListener)\n    }\n  }\n\n  /**\n   * Run a thunk with physical plans for all queries captured and passed into a provided buffer.\n   */\n  def withPhysicalPlansCaptured[T](\n      spark: SparkSession)(\n      thunk: => Unit): Seq[SparkPlan] = {\n    val planCapturingListener = new PlanCapturingListener\n\n    spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n    spark.listenerManager.register(planCapturingListener)\n    try {\n      thunk\n      spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n      planCapturingListener.plans.map(_.sparkPlan)\n    } finally {\n      spark.listenerManager.unregister(planCapturingListener)\n    }\n  }\n\n  /**\n   * Run a thunk with logical and physical plans for all queries captured and passed\n   * into a provided buffer.\n   */\n  def withAllPlansCaptured[T](\n      spark: SparkSession)(\n      thunk: => Unit): Seq[Plans] = {\n    val planCapturingListener = new PlanCapturingListener\n\n    spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n    spark.listenerManager.register(planCapturingListener)\n    try {\n      thunk\n      spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n      planCapturingListener.plans\n    } finally {\n      spark.listenerManager.unregister(planCapturingListener)\n    }\n  }\n\n  def countSparkJobs(sc: SparkContext, f: => Unit): Int = {\n    val jobs: concurrent.Map[Int, Long] = new ConcurrentHashMap[Int, Long]().asScala\n    val listener = new SparkListener {\n      override def onJobStart(jobStart: SparkListenerJobStart): Unit = {\n        jobs.put(jobStart.jobId, jobStart.stageInfos.map(_.numTasks).sum)\n      }\n      override def onJobEnd(jobEnd: SparkListenerJobEnd): Unit = jobEnd.jobResult match {\n        case JobFailed(_) => jobs.remove(jobEnd.jobId)\n        case _ => // On success, do nothing.\n      }\n    }\n    sc.addSparkListener(listener)\n    try {\n      sc.listenerBus.waitUntilEmpty(15000)\n      f\n      sc.listenerBus.waitUntilEmpty(15000)\n    } finally {\n      sc.removeSparkListener(listener)\n    }\n    // Spark will always log a job start/end event even when the job does not launch any task.\n    jobs.values.count(_ > 0)\n  }\n\n  /** Filter `usageRecords` by the `opType` tag or field. */\n  def filterUsageRecords(usageRecords: Seq[UsageRecord], opType: String): Seq[UsageRecord] = {\n    usageRecords.filter { r =>\n      r.tags.get(\"opType\").contains(opType) || r.opType.map(_.typeName).contains(opType)\n    }\n  }\n\n  def collectUsageLogs(opType: String)(f: => Unit): collection.Seq[UsageRecord] = {\n    Log4jUsageLogger.track(f).filter { r =>\n      r.metric == \"tahoeEvent\" &&\n        r.tags.get(\"opType\").contains(opType)\n    }\n  }\n\n  /**\n   * Remove protocol and metadata fields from checksum file of json format\n   */\n  def removeProtocolAndMetadataFromChecksumFile(checksumFilePath : Path): Unit = {\n    // scalastyle:off deltahadoopconfiguration\n    val fs = checksumFilePath.getFileSystem(\n      SparkSession.getActiveSession.map(_.sessionState.newHadoopConf()).get\n    )\n    // scalastyle:on deltahadoopconfiguration\n    if (!fs.exists(checksumFilePath)) return\n    val stream = fs.open(checksumFilePath)\n    val reader = new BufferedReader(new InputStreamReader(stream, UTF_8))\n    val content = reader.readLine()\n    stream.close()\n    val mapper = new ObjectMapper()\n    mapper.registerModule(DefaultScalaModule)\n    val map = mapper.readValue(content, classOf[Map[String, String]])\n    val partialContent = mapper.writeValueAsString(map.-(\"protocol\").-(\"metadata\")) + \"\\n\"\n    val output = fs.create(checksumFilePath, true)\n    output.write(partialContent.getBytes(UTF_8))\n    output.close()\n  }\n\n  protected def getfindTouchedFilesJobPlans(plans: Seq[Plans]): SparkPlan = {\n    // The expected plan for touched file computation is of the format below.\n    // The data column should be pruned from both leaves.\n    // HashAggregate(output=[count#3463L])\n    // +- HashAggregate(output=[count#3466L])\n    //   +- Project\n    //      +- Filter (isnotnull(count#3454L) AND (count#3454L > 1))\n    //         +- HashAggregate(output=[count#3454L])\n    //            +- HashAggregate(output=[_row_id_#3418L, sum#3468L])\n    //               +- Project [_row_id_#3418L, UDF(_file_name_#3422) AS one#3448]\n    //                  +- BroadcastHashJoin [id#3342L], [id#3412L], Inner, BuildLeft\n    //                     :- Project [id#3342L]\n    //                     :  +- Filter isnotnull(id#3342L)\n    //                     :     +- FileScan parquet [id#3342L,part#3343L]\n    //                     +- Filter isnotnull(id#3412L)\n    //                        +- Project [...]\n    //                           +- Project [...]\n    //                             +- FileScan parquet [id#3412L,part#3413L]\n    // Note: It can be RDDScanExec instead of FileScan if the source was materialized.\n    // We pick the first plan starting from FileScan and ending in HashAggregate as a\n    // stable heuristic for the one we want.\n    plans.map(_.executedPlan)\n      .filter {\n        case WholeStageCodegenExec(hash: HashAggregateExec) =>\n          hash.collectLeaves().size == 2 &&\n            hash.collectLeaves()\n              .forall { s =>\n                s.isInstanceOf[FileSourceScanExec] ||\n                  s.isInstanceOf[RDDScanExec]\n              }\n        case _ => false\n      }.head\n  }\n\n  /**\n   * Separate name- from path-based SQL table identifiers.\n   */\n  def getTableIdentifierOrPath(sqlIdentifier: String): TableIdentifierOrPath = {\n    // Match: delta.`path`[[ as] alias] or tahoe.`path`[[ as] alias]\n    val pathMatcher: Regex = raw\"(?:delta|tahoe)\\.`([^`]+)`(?:(?: as)? (.+))?\".r\n    // Match: db.table[[ as] alias]\n    val qualifiedDbMatcher: Regex = raw\"`?([^\\.` ]+)`?\\.`?([^\\.` ]+)`?(?:(?: as)? (.+))?\".r\n    // Match: table[[ as] alias]\n    val unqualifiedNameMatcher: Regex = raw\"([^ ]+)(?:(?: as)? (.+))?\".r\n    sqlIdentifier match {\n      case pathMatcher(path, alias) =>\n        TableIdentifierOrPath.Path(path, Option(alias))\n      case qualifiedDbMatcher(dbName, tableName, alias) =>\n        TableIdentifierOrPath.Identifier(TableIdentifier(tableName, Some(dbName)), Option(alias))\n      case unqualifiedNameMatcher(tableName, alias) =>\n        TableIdentifierOrPath.Identifier(TableIdentifier(tableName), Option(alias))\n    }\n  }\n\n  /**\n   * Produce a DeltaTable instance given a `TableIdentifierOrPath` instance.\n   */\n  def getDeltaTableForIdentifierOrPath(\n      spark: SparkSession,\n      identifierOrPath: TableIdentifierOrPath): IODeltaTable = {\n    identifierOrPath match {\n      case TableIdentifierOrPath.Identifier(id, optionalAlias) =>\n        val table = IODeltaTable.forName(spark, id.unquotedString)\n        optionalAlias.map(table.as(_)).getOrElse(table)\n      case TableIdentifierOrPath.Path(path, optionalAlias) =>\n        val table = IODeltaTable.forPath(spark, path)\n        optionalAlias.map(table.as(_)).getOrElse(table)\n    }\n  }\n\n  @deprecated(\"Use checkError() instead\")\n  protected def errorContains(errMsg: String, str: String): Unit = {\n    assert(errMsg.toLowerCase(Locale.ROOT).contains(str.toLowerCase(Locale.ROOT)))\n  }\n\n  /**\n   * Helper types to define the expected result of a test case.\n   * Either:\n   * - Success: include an expected value to check, e.g. expected schema or result as a DF or rows.\n   * - Failure: an exception is thrown and the caller passes a function to check that it matches an\n   *     expected error, typ. `checkError()` or `checkErrorMatchPVals()`.\n   */\n  sealed trait ExpectedResult[-T]\n  object ExpectedResult {\n    case class Success[T](expected: T) extends ExpectedResult[T]\n    case class Failure[T](checkError: SparkThrowable => Unit = _ => ()) extends ExpectedResult[T]\n  }\n\n  /** Utility method to check exception `e` is of type `E` or a cause of it is of type `E` */\n  def findIfResponsible[E <: Throwable: ClassTag](e: Throwable): Option[E] = e match {\n    case culprit: E => Some(culprit)\n    case _ =>\n      val children = Option(e.getCause).iterator ++ e.getSuppressed.iterator\n      children\n        .map(findIfResponsible[E](_))\n        .collectFirst { case Some(culprit) => culprit }\n  }\n\n  def verifyBackfilled(file: FileStatus): Unit = {\n    val unbackfilled = file.getPath.getName.matches(FileNames.uuidDeltaFileRegex.toString)\n    assert(!unbackfilled, s\"File $file was not backfilled\")\n  }\n\n  def verifyUnbackfilled(file: FileStatus): Unit = {\n    val unbackfilled = file.getPath.getName.matches(FileNames.uuidDeltaFileRegex.toString)\n    assert(unbackfilled, s\"File $file was backfilled\")\n  }\n}\n\ntrait DeltaCheckpointTestUtils\n  extends DeltaTestUtilsBase { self: SparkFunSuite with SharedSparkSession =>\n\n  def testDifferentCheckpoints(testName: String, quiet: Boolean = false)\n      (f: (CheckpointPolicy.Policy, Option[V2Checkpoint.Format]) => Unit): Unit = {\n    test(s\"$testName [Checkpoint V1]\") {\n      def testFunc(): Unit = {\n        withSQLConf(DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey ->\n          CheckpointPolicy.Classic.name) {\n          f(CheckpointPolicy.Classic, None)\n        }\n      }\n      if (quiet) quietly { testFunc() } else testFunc()\n    }\n    for (checkpointFormat <- V2Checkpoint.Format.ALL)\n    test(s\"$testName [Checkpoint V2, format: ${checkpointFormat.name}]\") {\n      def testFunc(): Unit = {\n        withSQLConf(\n          DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n          DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> checkpointFormat.name\n        ) {\n          f(CheckpointPolicy.V2, Some(checkpointFormat))\n        }\n      }\n      if (quiet) quietly { testFunc() } else testFunc()\n    }\n  }\n\n  /**\n   * Helper method to get the dataframe corresponding to the files which has the file actions for a\n   * given checkpoint.\n   */\n  def getCheckpointDfForFilesContainingFileActions(\n      log: DeltaLog,\n      checkpointFile: Path): DataFrame = {\n    val ci = CheckpointInstance.apply(checkpointFile)\n    val allCheckpointFiles = log\n        .listFrom(ci.version)\n        .filter(FileNames.isCheckpointFile)\n        .filter(f => CheckpointInstance(f.getPath) == ci)\n        .toSeq\n    val fileActionsFileIndex = ci.format match {\n      case CheckpointInstance.Format.V2 =>\n        val incompleteCheckpointProvider = ci.getCheckpointProvider(log, allCheckpointFiles)\n        val df = log.loadIndex(incompleteCheckpointProvider.topLevelFileIndex.get, Action.logSchema)\n        val sidecarFileStatuses = df.as[SingleAction].collect().map(_.unwrap).collect {\n          case sf: SidecarFile => sf\n        }.map(sf => sf.toFileStatus(log.logPath))\n        DeltaLogFileIndex(DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_PARQUET, sidecarFileStatuses)\n      case CheckpointInstance.Format.SINGLE | CheckpointInstance.Format.WITH_PARTS =>\n        DeltaLogFileIndex(DeltaLogFileIndex.CHECKPOINT_FILE_FORMAT_PARQUET,\n          allCheckpointFiles.toArray)\n      case _ =>\n        throw new Exception(s\"Unexpected checkpoint format for file $checkpointFile\")\n    }\n    fileActionsFileIndex.files\n      .map(fileStatus => spark.read.parquet(fileStatus.getPath.toString))\n      .reduce(_.union(_))\n  }\n}\n\nobject DeltaTestUtils extends DeltaTestUtilsBase {\n\n  sealed trait TableIdentifierOrPath\n  object TableIdentifierOrPath {\n    case class Identifier(id: TableIdentifier, alias: Option[String])\n      extends TableIdentifierOrPath\n    case class Path(path: String, alias: Option[String]) extends TableIdentifierOrPath\n  }\n\n  case class Plans(\n      analyzed: LogicalPlan,\n      optimized: LogicalPlan,\n      sparkPlan: SparkPlan,\n      executedPlan: SparkPlan)\n\n  /**\n   * Creates an AddFile that can be used for tests where the exact parameters do not matter.\n   */\n  def createTestAddFile(\n      encodedPath: String = \"foo\",\n      partitionValues: Map[String, String] = Map.empty,\n      size: Long = 1L,\n      modificationTime: Long = 1L,\n      dataChange: Boolean = true,\n      stats: String = \"{\\\"numRecords\\\": 1}\"): AddFile = {\n    AddFile(encodedPath, partitionValues, size, modificationTime, dataChange, stats)\n  }\n\n\n  /**\n   * Discovers all DeltaOperations.Operation subclasses using reflection.\n   * Returns a Set of operation class names.\n   *\n   * This is useful for tests that need to ensure exhaustive coverage of all operations.\n   */\n  def getAllDeltaOperations: Set[String] = {\n    val mirror = runtimeMirror(getClass.getClassLoader)\n    val moduleSymbol =\n      mirror.staticModule(\"org.apache.spark.sql.delta.DeltaOperations\")\n    val moduleMirror = mirror.reflectModule(moduleSymbol)\n    val instance = moduleMirror.instance\n\n    val instanceMirror = mirror.reflect(instance)\n    val symbol = instanceMirror.symbol\n    val traitOperation =\n      typeOf[org.apache.spark.sql.delta.DeltaOperations.Operation].typeSymbol\n\n    symbol.typeSignature.members.flatMap {\n      case cls: ClassSymbol\n        if cls.isCaseClass && cls.isPublic && cls.toType.baseClasses.contains(traitOperation) =>\n        Some(cls.name.toString)\n      case obj: ModuleSymbol\n        if obj.isPublic && obj.moduleClass.asType.toType.baseClasses.contains(traitOperation) =>\n        Some(obj.name.toString)\n      case _ => None\n    }.toSet\n  }\n\n  /**\n   * Extracts the table name and alias (if any) from the given string. Correctly handles whitespaces\n   * in table name but doesn't support whitespaces in alias.\n   */\n  def parseTableAndAlias(table: String): (String, Option[String]) = {\n    // Matches 'delta.`path` AS alias' (case insensitive).\n    val deltaPathWithAsAlias = raw\"(?i)(delta\\.`.+`)(?: AS) (\\S+)\".r\n    // Matches 'delta.`path` alias'.\n    val deltaPathWithAlias = raw\"(delta\\.`.+`) (\\S+)\".r\n    // Matches 'delta.`path`'.\n    val deltaPath = raw\"(delta\\.`.+`)\".r\n    // Matches 'tableName AS alias' (case insensitive).\n    val tableNameWithAsAlias = raw\"(?i)(.+)(?: AS) (\\S+)\".r\n    // Matches 'tableName alias'.\n    val tableNameWithAlias = raw\"(.+) (.+)\".r\n\n    table match {\n      case deltaPathWithAsAlias(tableName, alias) => tableName -> Some(alias)\n      case deltaPathWithAlias(tableName, alias) => tableName -> Some(alias)\n      case deltaPath(tableName) => tableName -> None\n      case tableNameWithAsAlias(tableName, alias) => tableName -> Some(alias)\n      case tableNameWithAlias(tableName, alias) => tableName -> Some(alias)\n      case tableName => tableName -> None\n    }\n  }\n\n  /**\n   * Implements an ordering where `x < y` iff both reader and writer versions of\n   * `x` are strictly less than those of `y`.\n   *\n   * Can be used to conveniently check that this relationship holds in tests/assertions\n   * without having to write out the conjunction of the two subconditions every time.\n   */\n  case object StrictProtocolOrdering extends PartialOrdering[Protocol] {\n    override def tryCompare(x: Protocol, y: Protocol): Option[Int] = {\n      if (x.minReaderVersion == y.minReaderVersion &&\n        x.minWriterVersion == y.minWriterVersion) {\n        Some(0)\n      } else if (x.minReaderVersion < y.minReaderVersion &&\n        x.minWriterVersion < y.minWriterVersion) {\n        Some(-1)\n      } else if (x.minReaderVersion > y.minReaderVersion &&\n        x.minWriterVersion > y.minWriterVersion) {\n        Some(1)\n      } else {\n        None\n      }\n    }\n\n    override def lteq(x: Protocol, y: Protocol): Boolean =\n      x.minReaderVersion <= y.minReaderVersion && x.minWriterVersion <= y.minWriterVersion\n\n    // Just a more readable version of `lteq`.\n    def fulfillsVersionRequirements(actual: Protocol, requirement: Protocol): Boolean =\n      lteq(requirement, actual)\n  }\n\n  def modifyCommitTimestamp(deltaLog: DeltaLog, version: Long, ts: Long): Unit = {\n    val filePath = DeltaCommitFileProvider(deltaLog.update()).deltaFile(version)\n    val file = new File(filePath.toUri)\n    InCommitTimestampTestUtils.overwriteICTInDeltaFile(\n      deltaLog,\n      new Path(file.getPath),\n      Some(ts))\n    file.setLastModified(ts)\n    if (FileNames.isUnbackfilledDeltaFile(filePath)) {\n      // Also change the ICT in the backfilled file if it exists.\n      val backfilledFilePath = FileNames.unsafeDeltaFile(deltaLog.logPath, version)\n      val fs = backfilledFilePath.getFileSystem(deltaLog.newDeltaHadoopConf())\n      if (fs.exists(backfilledFilePath)) {\n        InCommitTimestampTestUtils.overwriteICTInDeltaFile(deltaLog, backfilledFilePath, Some(ts))\n      }\n    }\n    val crc = new File(FileNames.checksumFile(deltaLog.logPath, version).toUri)\n    if (crc.exists()) {\n      InCommitTimestampTestUtils.overwriteICTInCrc(deltaLog, version, Some(ts))\n      crc.setLastModified(ts)\n    }\n  }\n\n  def withTimeZone(zone: String)(f: => Unit): Unit = {\n    val currentDefault = TimeZone.getDefault\n    try {\n      TimeZone.setDefault(TimeZone.getTimeZone(zone))\n      f\n    } finally {\n      TimeZone.setDefault(currentDefault)\n    }\n  }\n}\n\ntrait DeltaTestUtilsForTempViews\n  extends SharedSparkSession\n  with DeltaTestUtilsBase {\n\n  def testWithTempView(testName: String)(testFun: Boolean => Any): Unit = {\n    Seq(true, false).foreach { isSQLTempView =>\n      val tempViewUsed = if (isSQLTempView) \"SQL TempView\" else \"Dataset TempView\"\n      test(s\"$testName - $tempViewUsed\") {\n        withTempView(\"v\") {\n          testFun(isSQLTempView)\n        }\n      }\n    }\n  }\n\n  def testQuietlyWithTempView(testName: String)(testFun: Boolean => Any): Unit = {\n    Seq(true, false).foreach { isSQLTempView =>\n      val tempViewUsed = if (isSQLTempView) \"SQL TempView\" else \"Dataset TempView\"\n      testQuietly(s\"$testName - $tempViewUsed\") {\n        withTempView(\"v\") {\n          testFun(isSQLTempView)\n        }\n      }\n    }\n  }\n\n  def createTempViewFromTable(\n      tableName: String,\n      isSQLTempView: Boolean,\n      format: Option[String] = None): Unit = {\n    if (isSQLTempView) {\n      sql(s\"CREATE OR REPLACE TEMP VIEW v AS SELECT * from $tableName\")\n    } else {\n      spark.read.format(format.getOrElse(\"delta\")).table(tableName).createOrReplaceTempView(\"v\")\n    }\n  }\n\n  def createTempViewFromSelect(text: String, isSQLTempView: Boolean): Unit = {\n    if (isSQLTempView) {\n      sql(s\"CREATE OR REPLACE TEMP VIEW v AS $text\")\n    } else {\n      sql(text).createOrReplaceTempView(\"v\")\n    }\n  }\n\n  def testErrorMessageAndClass(\n      isSQLTempView: Boolean,\n      ex: AnalysisException,\n      expectedErrorMsgForSQLTempView: String = null,\n      expectedErrorMsgForDataSetTempView: String = null,\n      expectedErrorClassForSQLTempView: String = null,\n      expectedErrorClassForDataSetTempView: String = null): Unit = {\n    if (isSQLTempView) {\n      if (expectedErrorMsgForSQLTempView != null) {\n        errorContains(ex.getMessage, expectedErrorMsgForSQLTempView)\n      }\n      if (expectedErrorClassForSQLTempView != null) {\n        assert(ex.getErrorClass == expectedErrorClassForSQLTempView)\n      }\n    } else {\n      if (expectedErrorMsgForDataSetTempView != null) {\n        errorContains(ex.getMessage, expectedErrorMsgForDataSetTempView)\n      }\n      if (expectedErrorClassForDataSetTempView != null) {\n        assert(ex.getErrorClass == expectedErrorClassForDataSetTempView, ex.getMessage)\n      }\n    }\n  }\n}\n\n/**\n * Trait collecting helper methods for DML tests e.p. creating a test table for each test and\n * cleaning it up after each test.\n */\ntrait DeltaDMLTestUtils\n  extends DeltaSQLTestUtils\n  with DeltaTestUtilsBase\n  with BeforeAndAfterEach\n  with CDCTestMixin {\n  self: SharedSparkSession =>\n\n  import testImplicits._\n\n  protected def tableSQLIdentifier: String\n\n  protected def tableIdentifier: TableIdentifier\n\n  protected def dropTable(): Unit\n\n  /**\n   * Clock used for [[deltaLog]]. [[SystemClock]] is used if not set via [[setupManualClock]].\n   */\n  protected var clock: ManualClock = _\n\n  protected def setupManualClock(): Unit = {\n    clock = new ManualClock(System.currentTimeMillis())\n    // Override the (cached) delta log with one using our manual clock.\n    DeltaLog.clearCache()\n    deltaLog\n  }\n\n  /**\n   * Use this to artificially move the current time to after the table retention period.\n   */\n  protected def advancePastRetentionPeriod(): Unit = {\n    assert(clock != null, \"Must call setupManualClock in tests that are using this method.\")\n    clock.advance(\n      deltaLog.deltaRetentionMillis(deltaLog.update().metadata) +\n        TimeUnit.DAYS.toMillis(3))\n  }\n\n  // No need to cache deltaLog here as it is already cached\n  protected def deltaLog: DeltaLog = {\n    if (clock != null) {\n      DeltaLog.forTable(spark, tableIdentifier, clock)\n    } else {\n      DeltaLog.forTable(spark, tableIdentifier)\n    }\n  }\n\n  override protected def afterEach(): Unit = {\n    try {\n      dropTable()\n    } finally {\n      super.afterEach()\n    }\n  }\n\n  protected def append(df: DataFrame, partitionBy: Seq[String] = Nil): Unit = {\n    val dfw = df.write.format(\"delta\").mode(\"append\")\n    if (partitionBy.nonEmpty) {\n      dfw.partitionBy(partitionBy: _*)\n    }\n    writeTable(dfw, tableSQLIdentifier)\n  }\n\n  protected def withKeyValueData(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      isKeyPartitioned: Boolean = false,\n      sourceKeyValueNames: (String, String) = (\"key\", \"value\"),\n      targetKeyValueNames: (String, String) = (\"key\", \"value\"))(\n      thunk: (String, String) => Unit = null): Unit = {\n\n    import testImplicits._\n\n    append(target.toDF(targetKeyValueNames._1, targetKeyValueNames._2).coalesce(2),\n      if (isKeyPartitioned) Seq(targetKeyValueNames._1) else Nil)\n    withTempView(\"source\") {\n      source.toDF(sourceKeyValueNames._1, sourceKeyValueNames._2).createOrReplaceTempView(\"source\")\n      thunk(\"source\", tableSQLIdentifier)\n    }\n  }\n\n  /**\n   * Parse the input JSON data into a dataframe, one row per input element.\n   * Throws an exception on malformed inputs or records that don't comply with the provided schema.\n   */\n  protected def readFromJSON(data: Seq[String], schema: StructType = null): DataFrame = {\n    if (schema != null) {\n      spark.read\n        .schema(schema)\n        .option(\"mode\", FailFastMode.name)\n        .json(data.toDS)\n    } else {\n      spark.read\n        .option(\"mode\", FailFastMode.name)\n        .json(data.toDS)\n    }\n  }\n\n  /**\n   * Reads a delta table by its identifier. The identifier can either be the table name or table\n   * path that is in the form of delta.`tablePath`.\n   */\n  protected def readDeltaTableByIdentifier(\n      tableIdentifier: String = tableSQLIdentifier): DataFrame = {\n    spark.read.format(\"delta\").table(tableIdentifier)\n  }\n\n  protected def writeTable[T](dfw: DataFrameWriter[T], tableName: String): Unit = {\n    import DeltaTestUtils.TableIdentifierOrPath\n\n    getTableIdentifierOrPath(tableName) match {\n      case TableIdentifierOrPath.Identifier(id, _) => dfw.saveAsTable(id.toString)\n      // A cleaner way to write this is to just use `saveAsTable` where the\n      // table name is delta.`path`. However, it will throw an error when\n      // we use \"append\" mode and the table does not exist, so we use `save`\n      // here instead.\n      case TableIdentifierOrPath.Path(path, _) => dfw.save(path)\n    }\n  }\n\n  /**\n   * Finds the latest operation of the given type that ran on the test table and returns the\n   * dataframe with the changes of the corresponding table version.\n   *\n   * @param operation Delta operation name, see [[DeltaOperations]].\n   */\n  protected def getCDCForLatestOperation(deltaLog: DeltaLog, operation: String): DataFrame = {\n    val latestOperation = deltaLog.history\n      .getHistory(None)\n      .find(_.operation == operation)\n    assert(latestOperation.nonEmpty, s\"Couldn't find a ${operation} operation to check CDF\")\n\n    val latestOperationVersion = latestOperation.get.version\n    assert(latestOperationVersion.nonEmpty,\n      s\"Latest ${operation} operation doesn't have a version associated with it\")\n\n    computeCDC(\n        spark,\n        deltaLog,\n        latestOperationVersion.get,\n        latestOperationVersion.get\n    )\n      .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n      .drop(CDCReader.CDC_COMMIT_VERSION)\n  }\n}\n\ntrait DeltaDMLTestUtilsPathBased extends DeltaDMLTestUtils {\n  self: SharedSparkSession =>\n\n  protected var tempDir: File = _\n\n  protected def tempPath: String = tempDir.getCanonicalPath\n\n  override protected def tableIdentifier: TableIdentifier = TableIdentifier(tempPath, Some(\"delta\"))\n\n  override protected def beforeEach(): Unit = {\n    super.beforeEach()\n    // Using a space in path to provide coverage for special characters.\n    tempDir = Utils.createTempDir(namePrefix = \"spark test\")\n  }\n\n  override protected def tableSQLIdentifier: String = s\"delta.`$tempPath`\"\n\n  protected def readDeltaTable(path: String): DataFrame = {\n    spark.read.format(\"delta\").load(path)\n  }\n\n  override protected def dropTable(): Unit = {\n    Utils.deleteRecursively(tempDir)\n    DeltaLog.clearCache()\n  }\n}\n\n/**\n * Represents a test that is incompatible with name-based table access\n */\ncase object NameBasedAccessIncompatible extends Tag(\"NameBasedAccessIncompatible\")\n\ntrait DeltaDMLTestUtilsNameBased extends DeltaDMLTestUtils {\n  self: SharedSparkSession =>\n\n  override protected def test(testName: String, testTags: Tag*)(testFun: => Any)(\n      implicit pos: Position): Unit = {\n    if (testTags.contains(NameBasedAccessIncompatible)) {\n      super.ignore(testName, testTags: _*)(testFun)\n    } else {\n      super.test(testName, testTags: _*)(testFun)\n    }\n  }\n\n  override protected def tableIdentifier: TableIdentifier = TableIdentifier(tableSQLIdentifier)\n\n  override protected def append(df: DataFrame, partitionBy: Seq[String] = Nil): Unit = {\n    super.append(df, partitionBy)\n  }\n\n  // Keep this all lowercase. Otherwise, for tests with spark.sql.caseSensitive set to\n  // true, the table name used for dropping the table will not match the created table\n  // name, causing the table not being dropped.\n  override protected def tableSQLIdentifier: String = \"test_delta_table\"\n\n  override protected def dropTable(): Unit = {\n    spark.sql(s\"DROP TABLE IF EXISTS $tableSQLIdentifier\")\n    DeltaLog.clearCache()\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaThrowableSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.Files\n\nimport scala.collection.immutable.SortedMap\n\nimport org.apache.spark.sql.delta.DeltaThrowableHelper.{deltaErrorClassSource, sparkErrorClassSource}\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.core.JsonParser.Feature.STRICT_DUPLICATE_DETECTION\nimport com.fasterxml.jackson.core.`type`.TypeReference\nimport com.fasterxml.jackson.core.util.{DefaultIndenter, DefaultPrettyPrinter}\nimport com.fasterxml.jackson.databind.SerializationFeature\nimport com.fasterxml.jackson.databind.json.JsonMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport org.apache.commons.io.{FileUtils, IOUtils}\n\nimport org.apache.spark.{ErrorClassesJsonReader, ErrorInfo, SparkFunSuite}\n\n/** Test suite for Delta Throwables. */\nclass DeltaThrowableSuite extends SparkFunSuite {\n\n  private lazy val sparkErrorClassesMap = {\n    new ErrorClassesJsonReader(Seq(sparkErrorClassSource)).errorInfoMap\n  }\n\n  private lazy val deltaErrorClassToInfoMap = {\n    new ErrorClassesJsonReader(Seq(deltaErrorClassSource)).errorInfoMap\n  }\n\n   /* Used to regenerate the error class file. Run:\n   {{{\n      SPARK_GENERATE_GOLDEN_FILES=1 build/sbt \\\n        \"sql/testOnly *DeltaThrowableSuite -- -t \\\"Error classes are correctly formatted\\\"\"\n   }}}\n   */\n\n  def checkIfUnique(ss: Seq[Any]): Unit = {\n    val duplicatedKeys = ss.groupBy(identity).mapValues(_.size).filter(_._2 > 1).keys.toSeq\n    assert(duplicatedKeys.isEmpty)\n  }\n\n  def checkCondition(ss: Seq[String], fx: String => Boolean): Unit = {\n    ss.foreach { s =>\n      assert(fx(s))\n    }\n  }\n\n  test(\"No duplicate error classes in Delta\") {\n    // Enabling this feature incurs performance overhead (20-30%)\n    val mapper = JsonMapper.builder()\n      .addModule(DefaultScalaModule)\n      .enable(STRICT_DUPLICATE_DETECTION)\n      .build()\n    mapper.readValue(deltaErrorClassSource, new TypeReference[Map[String, ErrorInfo]]() {})\n  }\n\n  test(\"No error classes are shared by Delta and Spark\") {\n    assert(deltaErrorClassToInfoMap.keySet.intersect(sparkErrorClassesMap.keySet).isEmpty)\n  }\n\n  test(\"No word 'databricks' in OSS Delta errors\") {\n    val errorClasses = deltaErrorClassToInfoMap.keys.toSeq\n    val errorMsgs = deltaErrorClassToInfoMap.values.toSeq.flatMap(_.message)\n    checkCondition(errorClasses ++ errorMsgs, s => !s.toLowerCase().contains(\"databricks\"))\n  }\n\n  test(\"Delta error classes are correctly formatted with keys in alphabetical order\") {\n    lazy val ossDeltaErrorFile = new File(getWorkspaceFilePath(\n      \"delta\", \"core\", \"src\", \"main\", \"resources\", \"error\").toFile,\n      \"delta-error-classes.json\")\n    val errorClassFileContents = {\n      IOUtils.toString(deltaErrorClassSource.openStream())\n    }\n    val mapper = JsonMapper.builder()\n      .addModule(DefaultScalaModule)\n      .enable(SerializationFeature.INDENT_OUTPUT)\n      .build()\n    val prettyPrinter = new DefaultPrettyPrinter()\n      .withArrayIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE)\n    val rewrittenString = {\n      val writer = mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)\n        .setSerializationInclusion(Include.NON_ABSENT)\n        .writer(prettyPrinter)\n      writer.writeValueAsString(deltaErrorClassToInfoMap)\n    }\n\n    if (regenerateGoldenFiles) {\n      if (rewrittenString.trim != errorClassFileContents.trim) {\n        logInfo(s\"Regenerating error class file $ossDeltaErrorFile\")\n        Files.delete(ossDeltaErrorFile.toPath)\n        FileUtils.writeStringToFile(ossDeltaErrorFile, rewrittenString, StandardCharsets.UTF_8)\n      }\n    } else {\n      assert(rewrittenString.trim == errorClassFileContents.trim)\n    }\n  }\n\n  test(\"Delta message format invariants\") {\n    val messageFormats = deltaErrorClassToInfoMap.values.toSeq.flatMap { i =>\n      i.subClass match {\n        // Has sub error class: the message template should be: base + sub\n        case Some(subs) =>\n          subs.values.toSeq.map(sub => s\"${i.messageTemplate} ${sub.messageTemplate}\")\n        // Does not have any sub error class: the message template is itself\n        case None => Seq(i.messageTemplate)\n      }\n    }\n    checkCondition(messageFormats, s => s != null)\n    checkIfUnique(messageFormats)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaTimeTravelSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.sql.Timestamp\nimport java.text.SimpleDateFormat\nimport java.util.Date\n\nimport scala.concurrent.duration._\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.DeltaHistoryManager.BufferingLogDeletionIterator\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{Action, CommitInfo, SingleAction}\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DateTimeUtils, DeltaCommitFileProvider, FileNames,\n  JsonUtils, TimestampFormatter}\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.sql.{functions, AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ManualClock\n\nclass DeltaTimeTravelSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite {\n\n  import testImplicits._\n\n  private val timeFormatter = new SimpleDateFormat(\"yyyyMMddHHmmssSSS\")\n\n  private implicit def durationToLong(duration: FiniteDuration): Long = {\n    duration.toMillis\n  }\n\n  private implicit def longToTimestamp(ts: Long): Timestamp = new Timestamp(ts)\n\n  private def modifyCommitTimestamp(deltaLog: DeltaLog, version: Long, ts: Long): Unit = {\n    val file = new File(DeltaCommitFileProvider(deltaLog.update()).deltaFile(version).toUri)\n    file.setLastModified(ts)\n    val crc = new File(FileNames.checksumFile(deltaLog.logPath, version).toUri)\n    if (crc.exists()) {\n      crc.setLastModified(ts)\n    }\n  }\n\n  private def modifyCheckpointTimestamp(deltaLog: DeltaLog, version: Long, ts: Long): Unit = {\n    val file = new File(FileNames.checkpointFileSingular(deltaLog.logPath, version).toUri)\n    file.setLastModified(ts)\n  }\n\n  /** Generate commits with the given timestamp in millis. */\n  private def generateCommitsCheap(deltaLog: DeltaLog, clock: ManualClock, commits: Long*): Unit = {\n    var startVersion = deltaLog.unsafeVolatileSnapshot.version + 1\n    commits.foreach { ts =>\n      val action =\n        createTestAddFile(encodedPath = startVersion.toString, modificationTime = startVersion)\n      clock.setTime(ts)\n      deltaLog.startTransaction().commitManually(action)\n      modifyCommitTimestamp(deltaLog, startVersion, ts)\n      startVersion += 1\n    }\n  }\n\n  /** Generate commits with the given timestamp in millis. */\n  private def generateCommits(location: String, commits: Long*): Unit = {\n    var deltaLog = DeltaLog.forTable(spark, dataPath = location)\n    var startVersion = deltaLog.unsafeVolatileSnapshot.version + 1\n    commits.foreach { ts =>\n      val rangeStart = startVersion * 10\n      val rangeEnd = rangeStart + 10\n      spark.range(rangeStart, rangeEnd).write.format(\"delta\").mode(\"append\").save(location)\n      // Construct a new delta log here before calling `DeltaCommitFileProvider` to get the commit\n      // file path. This is b/c [[Snapshot.logSegment.deltas]] will *not* be automatically updated\n      // after triggering backfill.\n      // We will then overwrite the commit timestamp for unbackilled commits even if we have already\n      // backfilled them. This leads to the failure in certain UT where we manually modify/overwrite\n      // the commit timestamps.\n      // To correctly update the deltas in [[LogSegment]], we construct a fresh delta log.\n      DeltaLog.clearCache()\n      deltaLog = DeltaLog.forTable(spark, dataPath = location)\n      val filePath = DeltaCommitFileProvider\n        .apply(snapshot = deltaLog.unsafeVolatileSnapshot)\n        .deltaFile(version = startVersion)\n      if (isICTEnabledForNewTablesCatalogOwned) {\n        InCommitTimestampTestUtils.overwriteICTInDeltaFile(deltaLog, filePath, Some(ts))\n        InCommitTimestampTestUtils.overwriteICTInCrc(deltaLog, startVersion, Some(ts))\n      } else {\n        val file = new File(filePath.toUri)\n        file.setLastModified(ts)\n      }\n      startVersion += 1\n    }\n  }\n\n  private def identifierWithTimestamp(identifier: String, ts: Long): String = {\n    s\"$identifier@${timeFormatter.format(new Date(ts))}\"\n  }\n\n  private def identifierWithVersion(identifier: String, v: Long): String = {\n    s\"$identifier@v$v\"\n  }\n\n  private implicit def longToTimestampExpr(value: Long): String = {\n    s\"cast($value / 1000 as timestamp)\"\n  }\n\n  private def getSparkFormattedTimestamps(values: Long*): Seq[String] = {\n    // Simulates getting timestamps directly from Spark SQL\n    values.map(new Timestamp(_)).toDF(\"ts\")\n      .select($\"ts\".cast(\"string\")).as[String].collect()\n      .map(i => s\"$i\")\n  }\n\n  private def historyTest(testName: String)(f: (DeltaLog, ManualClock) => Unit): Unit = {\n    testQuietly(testName) {\n      val clock = new ManualClock()\n      withTempDir { dir => f(DeltaLog.forTable(spark, dir, clock), clock) }\n    }\n  }\n\n  historyTest(\"getCommits should monotonize timestamps\") { (deltaLog, clock) =>\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      // This is fine for CC tables since ICT should've been enabled from the beginning.\n      // Hence, we should *never* call [[DeltaHistoryManager.getCommitsWithNonIctTimestamps]].\n      cancel(\"This test is not compatible with CC since ICT should've been enabled from the \" +\n        \"beginning for CC tables.\")\n    }\n    val start = 1540415658000L\n    // Make the commits out of order\n    generateCommitsCheap(deltaLog,\n      clock,\n      start,\n      start - 5.seconds, // adjusts to start + 1 ms\n      start + 1.milli,   // adjusts to start + 2 ms\n      start + 2.millis,  // adjusts to start + 3 ms\n      start - 2.seconds, // adjusts to start + 4 ms\n      start + 10.seconds)\n\n    val commits = DeltaHistoryManager.getCommitsWithNonIctTimestamps(\n      deltaLog.store,\n      deltaLog.logPath,\n      0,\n      None,\n      deltaLog.newDeltaHadoopConf())\n    // Note that when InCommitTimestamps are enabled, the monotization of timestamps is not\n    // performed by getCommits. Instead, the timestamps are already monotonized before they\n    // are written in the commit.\n    assert(commits.map(_.timestamp) === Seq(start,\n      start + 1.millis, start + 2.millis, start + 3.millis, start + 4.millis, start + 10.seconds))\n  }\n\n  historyTest(\"describe history timestamps are adjusted according to file timestamp\") {\n      (deltaLog, clock) =>\n    if (isICTEnabledForNewTablesCatalogOwned) {\n      // File timestamp adjustment is not needed when ICT is enabled.\n      cancel(\"This test is not compatible with InCommitTimestamps.\")\n    }\n    // this is in '2018-10-24', so earlier than today. The recorded timestamps in commitInfo will\n    // be much after this\n    val start = 1540415658000L\n    // Make the commits out of order\n    generateCommitsCheap(deltaLog,\n      clock,\n      start,\n      start - 5.seconds, // adjusts to start + 1 ms\n      start + 1.milli   // adjusts to start + 2 ms\n    )\n\n    val history = new DeltaHistoryManager(deltaLog)\n    val commits = history.getHistory(None)\n    assert(commits.map(_.timestamp.getTime) === Seq(start + 2.millis, start + 1.milli, start))\n  }\n\n  historyTest(\"should filter only delta files when computing earliest version\") {\n      (deltaLog, clock) =>\n    val start = 1540415658000L\n    clock.setTime(start)\n    generateCommitsCheap(deltaLog, clock, start, start + 10.seconds, start + 20.seconds)\n\n    val history = new DeltaHistoryManager(deltaLog)\n    assert(history.getActiveCommitAtTime(start + 15.seconds, false).version === 1)\n\n    val commits2 = history.getHistory(Some(10))\n    assert(commits2.last.version === Some(0))\n\n    assert(new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0L).toUri).delete())\n    val e = intercept[AnalysisException] {\n      history.getActiveCommitAtTime(start + 15.seconds, false).version\n    }\n    if (catalogOwnedDefaultCreationEnabledInTests &&\n        // Since we are creating a table w/ three initial commits, the table would have\n        // unbackfilled commits if the backfill batch size is greater or equal to three.\n        // See [[generateCommitsCheap]] for details.\n        catalogOwnedCoordinatorBackfillBatchSize.exists(_ >= 3)) {\n      // We throw an \"incorrect\" exception for CC tables if there exist any unbackfilled commits\n      // and the backfilled commits have been manually deleted. E.g., the 0.json we are deleting\n      // in this UT.\n      //\n      // Please see the comment in [[DeltaHistoryManager.getEarliestRecreatableCommit]] for the\n      // detailed rationale.\n      assert(e.getMessage.contains(\"[DELTA_NO_COMMITS_FOUND]\"))\n    } else {\n      assert(e.getMessage.contains(\"recreatable\"))\n    }\n  }\n\n  historyTest(\"resolving commits should return commit before timestamp\") { (deltaLog, clock) =>\n    val start = 1540415658000L\n    clock.setTime(start)\n    // Make a commit every 20 minutes\n    val commits = Seq.tabulate(10)(i => start + (i * 20).minutes)\n    generateCommitsCheap(deltaLog, clock, commits: _*)\n    // When maxKeys is 2, we will use the parallel search algorithm, when it is 1000, we will\n    // use the linear search method\n    Seq(1, 2, 1000).foreach { maxKeys =>\n      val history = new DeltaHistoryManager(deltaLog, maxKeys)\n\n      (0 until 10).foreach { i =>\n        assert(history.getActiveCommitAtTime(start + (i * 20 + 10).minutes, true).version === i)\n      }\n\n      val e = intercept[DeltaErrors.TemporallyUnstableInputException] {\n        // This is 20 minutes after the last commit\n        history.getActiveCommitAtTime(start + 200.minutes, false)\n      }\n      checkError(\n        e,\n        \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n        sqlState = \"42816\",\n        parameters = Map(\n          \"providedTimestamp\" -> \"2018-10-24 17:34:18.0\",\n          \"tableName\" -> \"2018-10-24 17:14:18.0\",\n          \"maximumTimestamp\" -> \"2018-10-24 17:14:18\")\n      )\n      assert(history.getActiveCommitAtTime(start + 180.minutes, true).version === 9)\n\n      val e2 = intercept[AnalysisException] {\n        history.getActiveCommitAtTime(start - 10.minutes, true)\n      }\n      assert(e2.getMessage.contains(\"before the earliest version\"))\n    }\n  }\n\n  /**\n   * Creates FileStatus objects, where the name is the version of a commit, and the modification\n   * timestamps come from the input.\n   */\n  private def createFileStatuses(modTimes: Long*): Iterator[FileStatus] = {\n    modTimes.zipWithIndex.map { case (time, version) => new FileStatus(\n      10L, false, 1, 10L, time, FileNames.checkpointFileSingular(new Path(\"/foo\"), version))\n    }.iterator\n  }\n\n  /**\n   * Creates a log deletion iterator with a retention `maxTimestamp` and `maxVersion` (both\n   * inclusive). The input iterator takes the original file timestamps, and the deleted output will\n   * return the adjusted timestamps of files that would actually be consumed by the iterator.\n   */\n  private def testBufferingLogDeletionIterator(\n      maxTimestamp: Long,\n      maxVersion: Long)(inputTimestamps: Seq[Long], deleted: Seq[Long]): Unit = {\n    val i = new BufferingLogDeletionIterator(createFileStatuses(inputTimestamps: _*),\n      maxTimestamp, maxVersion, FileNames.getFileVersion)\n    deleted.foreach { ts =>\n      assert(i.hasNext, s\"Was supposed to delete $ts, but iterator returned hasNext: false\")\n      assert(i.next().getModificationTime === ts, \"Returned files out of order!\")\n    }\n    assert(!i.hasNext, \"Iterator should be consumed\")\n  }\n\n  test(\"BufferingLogDeletionIterator: iterator behavior\") {\n    val i1 = new BufferingLogDeletionIterator(Iterator.empty, 100, 100, _ => 1)\n    intercept[NoSuchElementException](i1.next())\n    assert(!i1.hasNext)\n\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 100)(\n      inputTimestamps = Seq(10, 11),\n      deleted = Seq(10)\n    )\n\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 100)(\n      inputTimestamps = Seq(10, 15, 25, 26),\n      deleted = Seq(10, 15, 25)\n    )\n  }\n\n  test(\"BufferingLogDeletionIterator: \" +\n    \"early exit while handling adjusted timestamps due to timestamp\") {\n    // only should return 5 because 5 < 7\n    testBufferingLogDeletionIterator(maxTimestamp = 7, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5)\n    )\n\n    // Should only return 5, because 10 is used to adjust the following 8 to 11\n    testBufferingLogDeletionIterator(maxTimestamp = 10, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5)\n    )\n\n    // When it is 11, we can delete both 10 and 8\n    testBufferingLogDeletionIterator(maxTimestamp = 11, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5, 10, 11)\n    )\n\n    // When it is 12, we can return all, except last one\n    testBufferingLogDeletionIterator(maxTimestamp = 12, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 12, 13),\n      deleted = Seq(5, 10, 11, 12)\n    )\n\n    // Should only return 5, because 10 is used to adjust the following 8 to 11\n    testBufferingLogDeletionIterator(maxTimestamp = 10, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8),\n      deleted = Seq(5)\n    )\n\n    // When it is 11, we can delete both 10 and 8\n    testBufferingLogDeletionIterator(maxTimestamp = 11, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5, 10, 11)\n    )\n  }\n\n  test(\"BufferingLogDeletionIterator: \" +\n    \"early exit while handling adjusted timestamps due to version\") {\n    // only should return 5 because we can delete only up to version 0\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 0)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5)\n    )\n\n    // Should only return 5, because 10 is used to adjust the following 8 to 11\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 1)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5)\n    )\n\n    // When we can delete up to version 2, we can return up to version 2\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 2)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5, 10, 11)\n    )\n\n    // When it is version 3, we can return all, except last one\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 3)(\n      inputTimestamps = Seq(5, 10, 8, 12, 13),\n      deleted = Seq(5, 10, 11, 12)\n    )\n\n    // Should only return 5, because 10 is used to adjust the following 8 to 11\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 1)(\n      inputTimestamps = Seq(5, 10, 8),\n      deleted = Seq(5)\n    )\n\n    // When we can delete up to version 2, we can return up to version 2\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 2)(\n      inputTimestamps = Seq(5, 10, 8, 12),\n      deleted = Seq(5, 10, 11)\n    )\n  }\n\n  test(\"BufferingLogDeletionIterator: multiple adjusted timestamps\") {\n    Seq(9, 10, 11).foreach { retentionTimestamp =>\n      // Files should be buffered but not deleted, because of the file 11, which has adjusted ts 12\n      testBufferingLogDeletionIterator(maxTimestamp = retentionTimestamp, maxVersion = 100)(\n        inputTimestamps = Seq(5, 10, 8, 11, 14),\n        deleted = Seq(5)\n      )\n    }\n\n    // Safe to delete everything before (including) file: 11 which has adjusted timestamp 12\n    testBufferingLogDeletionIterator(maxTimestamp = 12, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 11, 14),\n      deleted = Seq(5, 10, 11, 12)\n    )\n\n    Seq(0, 1, 2).foreach { retentionVersion =>\n      testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = retentionVersion)(\n        inputTimestamps = Seq(5, 10, 8, 11, 14),\n        deleted = Seq(5)\n      )\n    }\n\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 3)(\n      inputTimestamps = Seq(5, 10, 8, 11, 14),\n      deleted = Seq(5, 10, 11, 12)\n    )\n\n    // Test when the last element is adjusted with both timestamp and version\n    Seq(9, 10, 11).foreach { retentionTimestamp =>\n      testBufferingLogDeletionIterator(maxTimestamp = retentionTimestamp, maxVersion = 100)(\n        inputTimestamps = Seq(5, 10, 8, 9),\n        deleted = Seq(5)\n      )\n    }\n\n    testBufferingLogDeletionIterator(maxTimestamp = 12, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 9, 13),\n      deleted = Seq(5, 10, 11, 12)\n    )\n\n    Seq(0, 1, 2).foreach { retentionVersion =>\n      testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = retentionVersion)(\n        inputTimestamps = Seq(5, 10, 8, 9),\n        deleted = Seq(5)\n      )\n    }\n\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 3)(\n      inputTimestamps = Seq(5, 10, 8, 9, 13),\n      deleted = Seq(5, 10, 11, 12)\n    )\n\n    Seq(9, 10, 11).foreach { retentionTimestamp =>\n      testBufferingLogDeletionIterator(maxTimestamp = retentionTimestamp, maxVersion = 100)(\n        inputTimestamps = Seq(10, 8, 9),\n        deleted = Nil\n      )\n    }\n\n    // Test the first element causing cascading adjustments\n    testBufferingLogDeletionIterator(maxTimestamp = 12, maxVersion = 100)(\n      inputTimestamps = Seq(10, 8, 9, 13),\n      deleted = Seq(10, 11, 12)\n    )\n\n    Seq(0, 1).foreach { retentionVersion =>\n      testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = retentionVersion)(\n        inputTimestamps = Seq(10, 8, 9),\n        deleted = Nil\n      )\n    }\n\n    testBufferingLogDeletionIterator(maxTimestamp = 100, maxVersion = 2)(\n      inputTimestamps = Seq(10, 8, 9, 13),\n      deleted = Seq(10, 11, 12)\n    )\n\n    // Test multiple batches of time adjustments\n    testBufferingLogDeletionIterator(maxTimestamp = 12, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 9, 12, 15, 14, 14), // 5, 10, 11, 12, 13, 15, 16, 17\n      deleted = Seq(5)\n    )\n\n    Seq(13, 14, 15, 16).foreach { retentionTimestamp =>\n      testBufferingLogDeletionIterator(maxTimestamp = retentionTimestamp, maxVersion = 100)(\n        inputTimestamps = Seq(5, 10, 8, 9, 12, 15, 14, 14), // 5, 10, 11, 12, 13, 15, 16, 17\n        deleted = Seq(5, 10, 11, 12, 13)\n      )\n    }\n\n    testBufferingLogDeletionIterator(maxTimestamp = 17, maxVersion = 100)(\n      inputTimestamps = Seq(5, 10, 8, 9, 12, 15, 14, 14, 18), // 5, 10, 11, 12, 13, 15, 16, 17, 18\n      deleted = Seq(5, 10, 11, 12, 13, 15, 16, 17)\n    )\n  }\n\n  test(\"[SPARK-45383] Time travel on a non-existing table should throw AnalysisException\") {\n    intercept[AnalysisException] {\n      spark.sql(\"SELECT * FROM not_existing VERSION AS OF 0\")\n    }\n  }\n\n  test(\"as of timestamp in between commits should use commit before timestamp\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblLoc, start, start + 20.minutes, start + 40.minutes)\n\n      val tablePathUri = identifierWithTimestamp(tblLoc, start + 10.minutes)\n\n      val df1 = spark.read.format(\"delta\").load(tablePathUri)\n      checkAnswer(df1.groupBy().count(), Row(10L))\n\n      // 2 minutes after start\n      val timeTwoMinutesAfterStart = new Timestamp(start + 2.minutes)\n      val df2 = spark.read.format(\"delta\")\n        .option(\"timestampAsOf\", timeTwoMinutesAfterStart.toString).load(tblLoc)\n\n      checkAnswer(df2.groupBy().count(), Row(10L))\n    }\n  }\n\n  test(\"as of timestamp on exact timestamp\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblLoc, start, start + 20.minutes)\n\n      // Simulate getting the timestamp directly from Spark SQL\n      val ts = getSparkFormattedTimestamps(start, start + 20.minutes)\n\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"timestampAsOf\", ts.head).load(tblLoc).groupBy().count(),\n        Row(10L)\n      )\n\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"timestampAsOf\", ts(1)).load(tblLoc).groupBy().count(),\n        Row(20L)\n      )\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(identifierWithTimestamp(tblLoc, start)).groupBy().count(),\n        Row(10L)\n      )\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(identifierWithTimestamp(tblLoc, start + 20.minutes))\n          .groupBy().count(),\n        Row(20L)\n      )\n    }\n  }\n\n  test(\"as of timestamp on invalid timestamp\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = 1540415658000L\n      generateCommits(tblLoc, start, start + 20.minutes)\n\n      val ex = intercept[AnalysisException] {\n        spark.read.format(\"delta\").option(\"timestampAsOf\", \"i am not a timestamp\")\n          .load(tblLoc).groupBy().count()\n      }\n\n      assert(ex.getMessage.contains(\n        \"The provided timestamp ('i am not a timestamp') cannot be converted to a valid timestamp\"))\n    }\n  }\n\n  test(\"as of exact timestamp after last commit should fail\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = 1540415658000L\n      generateCommits(tblLoc, start)\n\n      // Simulate getting the timestamp directly from Spark SQL\n      val ts = getSparkFormattedTimestamps(start + 10.minutes)\n\n      val e1 = intercept[DeltaErrors.TemporallyUnstableInputException] {\n        spark.read.format(\"delta\").option(\"timestampAsOf\", ts.head).load(tblLoc).collect()\n      }\n      checkError(\n        e1,\n        \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n        sqlState = \"42816\",\n        parameters = Map(\n          \"providedTimestamp\" -> \"2018-10-24 14:24:18.0\",\n          \"tableName\" -> \"2018-10-24 14:14:18.0\",\n          \"maximumTimestamp\" -> \"2018-10-24 14:14:18\")\n      )\n\n      val e2 = intercept[DeltaErrors.TemporallyUnstableInputException] {\n        spark.read.format(\"delta\").load(identifierWithTimestamp(tblLoc, start + 10.minutes))\n          .collect()\n      }\n      checkError(\n        e2,\n        \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n        sqlState = \"42816\",\n        parameters = Map(\n          \"providedTimestamp\" -> \"2018-10-24 14:24:18.0\",\n          \"tableName\" -> \"2018-10-24 14:14:18.0\",\n          \"maximumTimestamp\" -> \"2018-10-24 14:14:18\")\n      )\n\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"timestampAsOf\", \"2018-10-24 14:14:18\")\n          .load(tblLoc).groupBy().count(),\n        Row(10)\n      )\n    }\n  }\n\n  test(\"as of with versions\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblLoc, start, start + 20.minutes, start + 40.minutes)\n\n      val df = spark.read.format(\"delta\").load(identifierWithVersion(tblLoc, 0))\n      checkAnswer(df.groupBy().count(), Row(10L))\n\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"versionAsOf\", \"0\").load(tblLoc).groupBy().count(),\n        Row(10)\n      )\n\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"versionAsOf\", 1).load(tblLoc).groupBy().count(),\n        Row(20)\n      )\n\n      val e1 = intercept[AnalysisException] {\n        spark.read.format(\"delta\").option(\"versionAsOf\", 3).load(tblLoc).collect()\n      }\n      assert(e1.getMessage.contains(\"[0, 2]\"))\n\n      val deltaLog = DeltaLog.forTable(spark, tblLoc)\n      new File(FileNames.unsafeDeltaFile(deltaLog.logPath, 0).toUri).delete()\n      val e2 = intercept[AnalysisException] {\n        spark.read.format(\"delta\").option(\"versionAsOf\", 0).load(tblLoc).collect()\n      }\n      if (catalogOwnedDefaultCreationEnabledInTests &&\n          // Since we are creating a table w/ three initial commits, the table would have\n          // unbackfilled commits if the backfill batch size is greater or equal to three.\n          // See [[generateCommits]] for details.\n          catalogOwnedCoordinatorBackfillBatchSize.exists(_ >= 3)) {\n        // We throw an \"incorrect\" exception for CC tables if there exist any unbackfilled commits\n        // and the backfilled commits have been manually deleted. E.g., the 0.json we are deleting\n        // in this UT.\n        //\n        // Please see the comment in [[DeltaHistoryManager.getEarliestRecreatableCommit]] for the\n        // detailed rationale.\n        assert(e2.getMessage.contains(\"[DELTA_NO_COMMITS_FOUND]\"))\n      } else {\n        assert(e2.getMessage.contains(\"recreatable\"))\n      }\n    }\n  }\n\n  test(\"time travelling with adjusted timestamps\") {\n    if (isICTEnabledForNewTablesCatalogOwned) {\n      // ICT Timestamps are always monotonically increasing. Therefore,\n      // this test is not needed when ICT is enabled.\n      cancel(\"This test is not compatible with InCommitTimestamps.\")\n    }\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblLoc, start, start - 5.seconds, start + 3.minutes)\n\n      val ts = getSparkFormattedTimestamps(\n        start, start + 1.milli, start + 119.seconds, start - 3.seconds)\n\n      checkAnswer(\n        spark.read.option(\"timestampAsOf\", ts.head).format(\"delta\").load(tblLoc).groupBy().count(),\n        Row(10L)\n      )\n\n      checkAnswer(\n        spark.read.option(\"timestampAsOf\", ts(1)).format(\"delta\").load(tblLoc).groupBy().count(),\n        Row(20L)\n      )\n\n      checkAnswer(\n        spark.read.option(\"timestampAsOf\", ts(2)).format(\"delta\").load(tblLoc).groupBy().count(),\n        Row(20L)\n      )\n\n      val e = intercept[AnalysisException] {\n        spark.read.option(\"timestampAsOf\", ts(3)).format(\"delta\").load(tblLoc).collect()\n      }\n      assert(e.getMessage.contains(\"before the earliest version\"))\n    }\n  }\n\n  test(\"can't provide both version and timestamp in DataFrameReader\") {\n    val e = intercept[IllegalArgumentException] {\n      spark.read.option(\"versionaSof\", 1)\n        .option(\"timestampAsOF\", \"fake\").format(\"delta\").load(\"/some/fake\")\n    }\n    assert(e.getMessage.contains(\"either provide 'timestampAsOf' or 'versionAsOf'\"))\n  }\n\n  test(\"don't time travel a valid delta path with @ syntax\") {\n    withTempDir { dir =>\n      val path = new File(dir, \"base@v0\").getCanonicalPath\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path + \"@v0\"),\n        spark.range(10).toDF()\n      )\n    }\n  }\n\n  test(\"don't time travel a valid non-delta path with @ syntax\") {\n    val format = \"json\"\n    withTempDir { dir =>\n      val path = new File(dir, \"base@v0\").getCanonicalPath\n      spark.range(10).write.format(format).mode(\"append\").save(path)\n      spark.range(10).write.format(format).mode(\"append\").save(path)\n\n      checkAnswer(\n        spark.read.format(format).load(path),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      checkAnswer(\n        spark.table(s\"$format.`$path`\"),\n        spark.range(10).union(spark.range(10)).toDF()\n      )\n\n      intercept[AnalysisException] {\n        spark.read.format(format).load(path + \"@v0\").count()\n      }\n\n      intercept[AnalysisException] {\n        spark.table(s\"$format.`$path@v0`\").count()\n      }\n    }\n  }\n\n  test(\"scans on different versions of same table are executed correctly\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(5).selectExpr(\"id as key\", \"id * 10 as value\").write.format(\"delta\").save(path)\n\n      spark.range(5, 10).selectExpr(\"id as key\", \"id * 10 as value\")\n        .write.format(\"delta\").mode(\"append\").save(path)\n\n      val df = spark.read.format(\"delta\").option(\"versionAsOf\", \"0\").load(path).as(\"a\").join(\n        spark.read.format(\"delta\").option(\"versionAsOf\", \"1\").load(path).as(\"b\"),\n        functions.expr(\"a.key == b.key\"),\n        \"fullOuter\"\n      ).where(\"a.key IS NULL\")  // keys 5 to 9 should be null\n      assert(df.count() == 5)\n    }\n  }\n\n  test(\"timestamp as of expression for table in database\") {\n    withDatabase(\"testDb\") {\n      sql(\"CREATE DATABASE testDb\")\n      withTable(\"tbl\") {\n        spark.range(10).write.format(\"delta\").saveAsTable(\"testDb.tbl\")\n        val ts = sql(\"DESCRIBE HISTORY testDb.tbl\").select(\"timestamp\").head().getTimestamp(0)\n\n        sql(s\"SELECT * FROM testDb.tbl TIMESTAMP AS OF \" +\n          s\"coalesce(CAST ('$ts' AS TIMESTAMP), current_date())\")\n      }\n    }\n  }\n\n  test(\"time travel with schema changes - should instantiate old schema\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(tblLoc)\n      spark.range(10, 20).withColumn(\"part\", 'id)\n        .write.format(\"delta\").mode(\"append\").option(\"mergeSchema\", true).save(tblLoc)\n\n      checkAnswer(\n        spark.read.option(\"versionAsOf\", 0).format(\"delta\").load(tblLoc),\n        spark.range(10).toDF())\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(identifierWithVersion(tblLoc, 0)),\n        spark.range(10).toDF())\n    }\n  }\n\n  test(\"time travel with partition changes - should instantiate old schema\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val v0 = spark.range(10).withColumn(\"part5\", 'id % 5)\n\n      v0.write.format(\"delta\").partitionBy(\"part5\").mode(\"append\").save(tblLoc)\n      spark.range(10, 20).withColumn(\"part2\", 'id % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part2\")\n        .mode(\"overwrite\")\n        .option(\"overwriteSchema\", true)\n        .save(tblLoc)\n\n      checkAnswer(\n        spark.read.option(\"versionAsOf\", 0).format(\"delta\").load(tblLoc),\n        v0)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(identifierWithVersion(tblLoc, 0)),\n        v0)\n    }\n  }\n\n  test(\"time travel support in SQL\") {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val start = System.currentTimeMillis() - 5.days.toMillis\n      generateCommits(tblLoc, start, start + 20.minutes)\n      val tableName = \"testTable\"\n\n      withTable(tableName) {\n        spark.sql(s\"create table $tableName(id long) using delta location '$tblLoc'\")\n\n        checkAnswer(\n          spark.sql(s\"SELECT * from $tableName FOR VERSION AS OF 0\"),\n          spark.read.option(\"versionAsOf\", 0).format(\"delta\").load(tblLoc))\n\n        checkAnswer(\n          spark.sql(s\"SELECT * from $tableName VERSION AS OF 1\"),\n          spark.read.option(\"versionAsOf\", 1).format(\"delta\").load(tblLoc))\n\n        val ex = intercept[VersionNotFoundException] {\n          spark.sql(s\"SELECT * from $tableName FOR VERSION AS OF 2\")\n        }\n        checkError(\n          ex,\n          \"DELTA_VERSION_NOT_FOUND\",\n          sqlState = \"22003\",\n          parameters = Map(\"userVersion\" -> \"2\", \"earliest\" -> \"0\", \"latest\" -> \"1\"))\n\n        val timeAtVersion0 = new Timestamp(start).toString\n        val timeAtVersion1 = new Timestamp(start + 20.minutes).toString\n        val timeAfterVersion2 = new Timestamp(start + 6.hours).toString\n\n        checkAnswer(\n          spark.sql(s\"SELECT * from $tableName FOR TIMESTAMP AS OF '$timeAtVersion0'\"),\n          spark.read.option(\"versionAsOf\", 0).format(\"delta\").load(tblLoc))\n\n        checkAnswer(\n          spark.sql(s\"SELECT * from $tableName TIMESTAMP AS OF '$timeAtVersion1'\"),\n          spark.read.option(\"versionAsOf\", 1).format(\"delta\").load(tblLoc))\n\n        val ex2 = intercept[DeltaErrors.TemporallyUnstableInputException] {\n          spark.sql(s\"SELECT * from $tableName FOR TIMESTAMP AS OF '$timeAfterVersion2'\")\n        }\n\n        checkError(\n          ex2,\n          \"DELTA_TIMESTAMP_GREATER_THAN_COMMIT\",\n          sqlState = \"42816\",\n          parameters = Map(\n            \"providedTimestamp\" -> s\"$timeAfterVersion2\",\n            \"tableName\" -> s\"$timeAtVersion1\",\n            \"maximumTimestamp\" -> s\"${timeAtVersion1.replaceFirst(\"\\\\.\\\\d+$\", \"\")}\") // exclude ms\n        )\n      }\n    }\n  }\n\n\n  test(\"SPARK-41154: Correct relation caching for queries with time travel spec\") {\n    val tblName = \"tab\"\n    withTable(tblName) {\n      sql(s\"CREATE TABLE $tblName USING DELTA AS SELECT 1 as c\")\n      sql(s\"INSERT INTO $tblName SELECT 2 as c\")\n      checkAnswer(\n        sql(s\"\"\"\n          |SELECT * FROM $tblName VERSION AS OF '0'\n          |UNION ALL\n          |SELECT * FROM $tblName VERSION AS OF '1'\n          |\"\"\".stripMargin),\n        Row(1) :: Row(1) :: Row(2) :: Nil)\n    }\n  }\n\n  test(\"Dataframe-based time travel works with different timestamp precisions\") {\n    val tblName = \"test_tab\"\n    withTable(tblName) {\n      sql(s\"CREATE TABLE spark_catalog.default.$tblName (a int) USING DELTA\")\n      // Ensure that the current timestamp is different from the one in the table.\n      Thread.sleep(1000)\n      // Microsecond precision timestamp.\n      val current_time_micros = spark.sql(\"SELECT current_timestamp() as ts\")\n        .select($\"ts\".cast(\"string\"))\n        .head().getString(0)\n      // Millisecond precision timestamp.\n      val current_time_millis = new Timestamp(System.currentTimeMillis())\n      // Second precision timestamp.\n      val sdf = new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")\n      val current_time_seconds = sdf.format(new java.sql.Timestamp(System.currentTimeMillis()))\n\n      sql(s\"INSERT INTO spark_catalog.default.$tblName VALUES (1)\")\n      checkAnswer(spark.read.option(\"timestampAsOf\", current_time_micros)\n        .table(s\"spark_catalog.default.$tblName\"), Seq.empty)\n      checkAnswer(spark.read.option(\"timestampAsOf\", current_time_millis.toString)\n        .table(s\"spark_catalog.default.$tblName\"), Seq.empty)\n      checkAnswer(spark.read.option(\"timestampAsOf\", current_time_seconds)\n        .table(s\"spark_catalog.default.$tblName\"), Seq.empty)\n    }\n  }\n\n  // Helper to generate a unique test table, commits, and timestamps for time travel blocking tests\n  def withTestTable(testBody: (String, String, String) => Unit): Unit = {\n    withTempDir { dir =>\n      val tblLoc = dir.getCanonicalPath\n      val tableName = \"testTable\"\n      withTable(tableName) {\n        // create six versions spaced 1 day apart\n        val start = System.currentTimeMillis() - 10.days.toMillis\n        generateCommits(\n          tblLoc,\n          start, // v0\n          start + 0.5.days, // v1\n          start + 1.days, // v2\n          start + 2.days, // v3\n          start + 2.5.days, // v4\n          start + 4.days, // v5\n          start + 5.days  // v6\n        )\n        // timestamps for v4 and v6\n        val t4 = new Timestamp(start + 2.5.days.toMillis).toString\n        val t6 = new Timestamp(start + 5.days.toMillis).toString\n\n        spark.sql(s\"CREATE TABLE $tableName(id LONG) USING delta LOCATION '$tblLoc'\")\n        spark.sql(s\"ALTER TABLE $tableName\" +\n          s\" SET TBLPROPERTIES ('delta.enableChangeDataFeed' = 'true')\")\n\n        testBody(tableName, t4, t6)\n      }\n    }\n  }\n\n  // Helper to assert whether a given SQL should or should not throw the retention exception\n  def assertBlocked(sql: String, shouldThrow: Boolean): Unit = {\n    val msg = \"Cannot time travel beyond delta.deletedFileRetentionDuration\"\n    if (shouldThrow) {\n      val ex = intercept[Exception]( spark.sql(sql) )\n      assert(ex.getMessage.contains(msg))\n    } else {\n      spark.sql(sql)  // must succeed\n    }\n  }\n\n  // 1) SELECT ... AS OF\n  test(\"Block time travel beyond deletedFileRetention\") {\n    withTestTable { (tbl, t4, t6) =>\n      Seq(\n        s\"SELECT * FROM $tbl VERSION AS OF 2\" -> true,\n        s\"SELECT * FROM $tbl TIMESTAMP AS OF '$t4'\" -> true,\n        s\"SELECT * FROM $tbl VERSION AS OF 5\" -> false,\n        s\"SELECT * FROM $tbl TIMESTAMP AS OF '$t6'\" -> false\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n\n      spark.sql(s\"ALTER TABLE $tbl \" +\n        s\"SET TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 0 HOURS')\")\n      // Even after lowering retention to zero, a simple select * should still work\n      // which references the latest version\n      assertBlocked(s\"SELECT * FROM $tbl\", false)\n      // After setting it to zero, any time travel will fail\n      Seq(\n        s\"SELECT * FROM $tbl VERSION AS OF 5\" -> true,\n        s\"SELECT * FROM $tbl TIMESTAMP AS OF '$t6'\" -> true\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n    }\n  }\n\n  // 2) SELECT ... CHANGES AS OF\n  test(\"Block CDC beyond deletedFileRetention\") {\n    withTestTable { (tbl, t4, t6) =>\n      Seq(\n        s\"SELECT * FROM table_changes('$tbl', 2)\" -> true,\n        s\"SELECT * FROM table_changes('$tbl', '$t4')\" -> true,\n        s\"SELECT * FROM table_changes('$tbl', 5)\" -> false,\n        s\"SELECT * FROM table_changes('$tbl', '$t6')\" -> false\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n\n      spark.sql(s\"ALTER TABLE $tbl \" +\n        s\"SET TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 0 HOURS')\")\n      // After setting it to zero, any previous version will fail\n      Seq(\n        s\"SELECT * FROM table_changes('$tbl', 5)\" -> true,\n        s\"SELECT * FROM table_changes('$tbl', '$t6')\" -> true\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n    }\n  }\n\n  // 3) RESTORE ... AS OF\n  test(\"Block restore table beyond deletedFileRetention\") {\n    withTestTable { (tbl, t4, t6) =>\n      Seq(\n        s\"RESTORE TABLE $tbl TO VERSION AS OF 2\" -> true,\n        s\"RESTORE TABLE $tbl TO TIMESTAMP AS OF '$t4'\" -> true,\n        s\"RESTORE TABLE $tbl TO VERSION AS OF 5\" -> false,\n        s\"RESTORE TABLE $tbl TO TIMESTAMP AS OF '$t6'\" -> false\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n\n      spark.sql(s\"ALTER TABLE $tbl\" +\n        s\" SET TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 0 HOURS')\")\n      // After setting it to zero, any previous version will fail\n      Seq(\n        s\"RESTORE TABLE $tbl TO VERSION AS OF 5\" -> true,\n        s\"RESTORE TABLE $tbl TO TIMESTAMP AS OF '$t6'\" -> true\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n    }\n  }\n\n  // 4) CLONE ... AS OF\n  test(\"Block clone table beyond deletedFileRetention\") {\n    withTestTable { (tbl, t4, t6) =>\n      val targets = Seq(\"targetTable1\", \"targetTable2\", \"targetTable3\")\n\n      Seq(\n        s\"CREATE TABLE ${targets(0)} SHALLOW CLONE $tbl VERSION AS OF 2\" -> true,\n        s\"CREATE TABLE ${targets(0)} SHALLOW CLONE $tbl TIMESTAMP AS OF '$t4'\" -> true,\n        s\"CREATE TABLE ${targets(0)} SHALLOW CLONE $tbl VERSION AS OF 5\" -> false,\n        s\"CREATE TABLE ${targets(1)} SHALLOW CLONE $tbl TIMESTAMP AS OF '$t6'\" -> false\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n\n      spark.sql(s\"ALTER TABLE $tbl\" +\n        s\" SET TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 0 HOURS')\")\n      // After setting it to zero, any previous version will fail\n      Seq(\n        s\"CREATE TABLE ${targets(0)} SHALLOW CLONE $tbl VERSION AS OF 5\" -> true,\n        s\"CREATE TABLE ${targets(0)} SHALLOW CLONE $tbl TIMESTAMP AS OF '$t6'\" -> true\n      ).foreach { case (sql, fail) => assertBlocked(sql, fail) }\n    }\n  }\n}\n\nclass DeltaTimeTravelWithCatalogOwnedBatch1Suite extends DeltaTimeTravelSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaTimeTravelWithCatalogOwnedBatch2Suite extends DeltaTimeTravelSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaTimeTravelWithCatalogOwnedBatch100Suite extends DeltaTimeTravelSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaTimestampNTZSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\nimport java.time.LocalDateTime\n\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkThrowable\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\n\nclass DeltaTimestampNTZSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  private def getProtocolForTable(table: String): Protocol = {\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n    deltaLog.unsafeVolatileSnapshot.protocol\n  }\n\n  test(\"create a new table with TIMESTAMP_NTZ, higher protocol and feature should be picked.\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(c1 STRING, c2 TIMESTAMP, c3 TIMESTAMP_NTZ) USING DELTA\")\n      sql(\n        \"\"\"INSERT INTO tbl VALUES\n          |('foo','2022-01-02 03:04:05.123456','2022-01-02 03:04:05.123456')\"\"\".stripMargin)\n      assert(spark.table(\"tbl\").head == Row(\n        \"foo\",\n        new Timestamp(2022 - 1900, 0, 2, 3, 4, 5, 123456000),\n        LocalDateTime.of(2022, 1, 2, 3, 4, 5, 123456000)))\n      assert(getProtocolForTable(\"tbl\") ==\n        TimestampNTZTableFeature.minProtocolVersion.withFeatures(Seq(\n          AppendOnlyTableFeature, InvariantsTableFeature, TimestampNTZTableFeature))\n      )\n    }\n  }\n\n  test(\"creating a table without TIMESTAMP_NTZ should use the usual minimum protocol\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(c1 STRING, c2 TIMESTAMP, c3 TIMESTAMP) USING DELTA\")\n      assert(getProtocolForTable(\"tbl\") == Protocol(1, 2))\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n      assert(\n        !deltaLog.unsafeVolatileSnapshot.protocol.isFeatureSupported(TimestampNTZTableFeature),\n        s\"Table tbl contains TimestampNTZFeature descriptor when its not supposed to\"\n      )\n    }\n  }\n\n  test(\"add a new column using TIMESTAMP_NTZ should upgrade to the correct protocol versions\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(c1 STRING, c2 TIMESTAMP) USING delta\")\n      assert(getProtocolForTable(\"tbl\") == Protocol(1, 2))\n\n      // Should throw error\n      val e = intercept[SparkThrowable] {\n        sql(\"ALTER TABLE tbl ADD COLUMN c3 TIMESTAMP_NTZ\")\n      }\n\n      // add table feature\n      sql(s\"ALTER TABLE tbl \" +\n          s\"SET TBLPROPERTIES('delta.feature.timestampNtz' = 'supported')\")\n\n      sql(\"ALTER TABLE tbl ADD COLUMN c3 TIMESTAMP_NTZ\")\n\n\n      sql(\n        \"\"\"INSERT INTO tbl VALUES\n          |('foo','2022-01-02 03:04:05.123456','2022-01-02 03:04:05.123456')\"\"\".stripMargin)\n      assert(spark.table(\"tbl\").head == Row(\n        \"foo\",\n        new Timestamp(2022 - 1900, 0, 2, 3, 4, 5, 123456000),\n        LocalDateTime.of(2022, 1, 2, 3, 4, 5, 123456000)))\n\n      assert(getProtocolForTable(\"tbl\") ==\n        TimestampNTZTableFeature.minProtocolVersion\n          .withFeature(TimestampNTZTableFeature)\n          .withFeature(InvariantsTableFeature)\n          .withFeature(AppendOnlyTableFeature)\n      )\n    }\n  }\n\n  test(\"use TIMESTAMP_NTZ in a partition column\") {\n    withTable(\"delta_test\") {\n      sql(\n        \"\"\"CREATE TABLE delta_test(c1 STRING, c2 TIMESTAMP, c3 TIMESTAMP_NTZ)\n          |USING delta\n          |PARTITIONED BY (c3)\"\"\".stripMargin)\n      sql(\n        \"\"\"INSERT INTO delta_test VALUES\n          |('foo','2022-01-02 03:04:05.123456','2022-01-02 03:04:05.123456')\"\"\".stripMargin)\n      assert(spark.table(\"delta_test\").head == Row(\n        \"foo\",\n        new Timestamp(2022 - 1900, 0, 2, 3, 4, 5, 123456000),\n        LocalDateTime.of(2022, 1, 2, 3, 4, 5, 123456000)))\n      assert(getProtocolForTable(\"delta_test\") ==\n        TimestampNTZTableFeature.minProtocolVersion.withFeatures(Seq(\n          AppendOnlyTableFeature, InvariantsTableFeature, TimestampNTZTableFeature))\n      )\n    }\n  }\n\n  test(\"min/max stats collection should apply on TIMESTAMP_NTZ\") {\n    withTable(\"delta_test\") {\n      val schemaString = \"c1 STRING, c2 TIMESTAMP, c3 TIMESTAMP_NTZ\"\n      sql(s\"CREATE TABLE delta_test($schemaString) USING delta\")\n      val statsSchema = DeltaLog.forTable(spark, TableIdentifier(\"delta_test\"))\n        .unsafeVolatileSnapshot.statsSchema\n      assert(statsSchema(\"minValues\").dataType == StructType\n        .fromDDL(schemaString))\n      assert(statsSchema(\"maxValues\").dataType == StructType\n        .fromDDL(schemaString))\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaUDFSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.{Encoder, QueryTest, Row}\nimport org.apache.spark.sql.expressions.UserDefinedFunction\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaUDFSuite extends QueryTest with SharedSparkSession {\n\n  import testImplicits._\n\n  private def testUDF(\n      name: String,\n      testResultFunc: => Unit): Unit = {\n    test(name) {\n      // Verify the returned UDF function is working correctly\n      testResultFunc\n    }\n  }\n\n  private def testUDF(\n      name: String,\n      func: => UserDefinedFunction,\n      expected: Any): Unit = {\n    testUDF(\n      name,\n      checkAnswer(Seq(\"foo\").toDF.select(func()), Row(expected))\n    )\n  }\n\n  private def testUDF[T: Encoder](\n      name: String,\n      func: => UserDefinedFunction,\n      input: T,\n      expected: Any): Unit = {\n    testUDF(\n      name,\n      checkAnswer(Seq(input).toDF.select(func(col(\"value\"))), Row(expected))\n    )\n  }\n\n  private def testUDF[T1: Encoder, T2: Encoder](\n      name: String,\n      func: => UserDefinedFunction,\n      input1: T1,\n      input2: T2,\n      expected: Any): Unit = {\n    testUDF(\n      name,\n      {\n        val df = Seq(input1)\n          .toDF(\"value1\")\n          .withColumn(\"value2\", lit(input2).as[T2])\n          .select(func(col(\"value1\"), col(\"value2\")))\n        checkAnswer(df, Row(expected))\n      }\n    )\n  }\n\n  testUDF(\n    name = \"stringFromString\",\n    func = DeltaUDF.stringFromString(x => x),\n    input = \"foo\",\n    expected = \"foo\")\n  testUDF(\n    name = \"intFromString\",\n    func = DeltaUDF.intFromString(x => x.toInt),\n    input = \"100\",\n    expected = 100)\n  testUDF(\n    name = \"intFromStringBoolean\",\n    func = DeltaUDF.intFromStringBoolean((x, y) => 1),\n    input1 = \"foo\",\n    input2 = true,\n    expected = 1)\n  testUDF(name = \"boolean\", func = DeltaUDF.boolean(() => true), expected = true)\n  testUDF(\n    name = \"stringFromMap\",\n    func = DeltaUDF.stringFromMap(x => x.toString),\n    input = Map(\"foo\" -> \"bar\"),\n    expected = \"Map(foo -> bar)\")\n  testUDF(\n    name = \"booleanFromMap\",\n    func = DeltaUDF.booleanFromMap(x => x.isEmpty),\n    input = Map(\"foo\" -> \"bar\"),\n    expected = false)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaUpdateCatalogSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.util.control.NonFatal\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.hooks.UpdateCatalog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaHiveTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport com.fasterxml.jackson.core.JsonParseException\n\nimport org.apache.spark.{SparkConf, SparkContext}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.ExternalCatalogWithListener\nimport org.apache.spark.sql.functions.{lit, struct}\nimport org.apache.spark.sql.hive.HiveExternalCatalog\nimport org.apache.spark.sql.types.{ArrayType, DoubleType, IntegerType, LongType, MapType, StringType, StructField, StructType}\nimport org.apache.spark.util.{ThreadUtils, Utils}\n\nclass DeltaUpdateCatalogSuite\n  extends DeltaUpdateCatalogSuiteBase\n    with DeltaHiveTest {\n\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key, \"true\")\n  }\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    cleanupDefaultTable()\n  }\n\n  override def afterEach(): Unit = {\n    if (!UpdateCatalog.awaitCompletion(10000)) {\n      logWarning(s\"There are active catalog udpate requests after 10 seconds\")\n    }\n    cleanupDefaultTable()\n    super.afterEach()\n  }\n\n  /** Remove Hive specific table properties. */\n  override protected def filterProperties(properties: Map[String, String]): Map[String, String] = {\n    properties.filterKeys(_ != \"transient_lastDdlTime\").toMap\n  }\n\n\n  test(\"streaming\") {\n    withTable(tbl) {\n      implicit val sparkSession: SparkSession = spark\n      implicit val _sqlContext = spark.sqlContext\n      val stream = MemoryStream[Long]\n      val df1 = stream.toDF().toDF(\"id\")\n\n      withTempDir { dir =>\n        try {\n          val q = df1.writeStream\n            .option(\"checkpointLocation\", dir.getCanonicalPath)\n            .format(\"delta\")\n            .toTable(tbl)\n\n          verifyTableMetadata(expectedSchema = df1.schema.asNullable)\n\n          stream.addData(1, 2, 3)\n          q.processAllAvailable()\n          q.stop()\n\n          val q2 = df1.withColumn(\"id2\", 'id)\n            .writeStream\n            .format(\"delta\")\n            .option(\"mergeSchema\", \"true\")\n            .option(\"checkpointLocation\", dir.getCanonicalPath)\n            .toTable(tbl)\n\n          stream.addData(4, 5, 6)\n          q2.processAllAvailable()\n\n          verifyTableMetadataAsync(expectedSchema = df1.schema.asNullable.add(\"id2\", LongType))\n        } finally {\n          spark.streams.active.foreach(_.stop())\n        }\n      }\n    }\n  }\n\n  test(\"streaming - external location\") {\n    withTempDir { dir =>\n      withTable(tbl) {\n        implicit val sparkSession: SparkSession = spark\n        implicit val _sqlContext = spark.sqlContext\n        val stream = MemoryStream[Long]\n        val df1 = stream.toDF().toDF(\"id\")\n\n        val chk = new File(dir, \"chkpoint\").getCanonicalPath\n        val data = new File(dir, \"data\").getCanonicalPath\n        try {\n          val q = df1.writeStream\n            .option(\"checkpointLocation\", chk)\n            .format(\"delta\")\n            .option(\"path\", data)\n            .toTable(tbl)\n\n          verifyTableMetadata(expectedSchema = df1.schema.asNullable)\n\n          stream.addData(1, 2, 3)\n          q.processAllAvailable()\n          q.stop()\n\n          val q2 = df1.withColumn(\"id2\", 'id)\n            .writeStream\n            .format(\"delta\")\n            .option(\"mergeSchema\", \"true\")\n            .option(\"checkpointLocation\", chk)\n            .toTable(tbl)\n\n          stream.addData(4, 5, 6)\n          q2.processAllAvailable()\n\n          verifyTableMetadataAsync(expectedSchema = df1.schema.add(\"id2\", LongType).asNullable)\n        } finally {\n          spark.streams.active.foreach(_.stop())\n        }\n      }\n    }\n  }\n\n  test(\"streaming - external table that already exists\") {\n    withTable(tbl) {\n      implicit val sparkSession: SparkSession = spark\n      implicit val _sqlContext = spark.sqlContext\n      val stream = MemoryStream[Long]\n      val df1 = stream.toDF().toDF(\"id\")\n\n      withTempDir { dir =>\n        val chk = new File(dir, \"chkpoint\").getCanonicalPath\n        val data = new File(dir, \"data\").getCanonicalPath\n\n        spark.range(10).write.format(\"delta\").save(data)\n        try {\n          val q = df1.writeStream\n            .option(\"checkpointLocation\", chk)\n            .format(\"delta\")\n            .option(\"path\", data)\n            .toTable(tbl)\n\n          verifyTableMetadataAsync(expectedSchema = df1.schema.asNullable)\n\n          stream.addData(1, 2, 3)\n          q.processAllAvailable()\n          q.stop()\n\n          val q2 = df1.withColumn(\"id2\", 'id)\n            .writeStream\n            .format(\"delta\")\n            .option(\"mergeSchema\", \"true\")\n            .option(\"checkpointLocation\", chk)\n            .toTable(tbl)\n\n          stream.addData(4, 5, 6)\n          q2.processAllAvailable()\n\n          verifyTableMetadataAsync(expectedSchema = df1.schema.add(\"id2\", LongType).asNullable)\n        } finally {\n          spark.streams.active.foreach(_.stop())\n        }\n      }\n    }\n  }\n\n  val MAX_CATALOG_TYPE_DDL_LENGTH: Long =\n    DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD.defaultValue.get\n\n\n  test(\"convert to delta with partitioning change\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2).withColumn(\"id2\", 'id)\n      df.writeTo(tbl)\n        .partitionedBy('part)\n        .using(\"parquet\")\n        .create()\n\n      // Partitioning columns go to the end for parquet tables\n      val tableSchema =\n        new StructType().add(\"id\", LongType).add(\"id2\", LongType).add(\"part\", DoubleType)\n      verifyTableMetadata(\n        expectedSchema = tableSchema,\n        expectedProperties = Map.empty,\n        partitioningCols = Seq(\"part\")\n      )\n\n      sql(s\"CONVERT TO DELTA $tbl PARTITIONED BY (part double)\")\n      // Information is duplicated for now\n      verifyTableMetadata(\n        expectedSchema = tableSchema,\n        expectedProperties = Map.empty,\n        partitioningCols = Seq(\"part\")\n      )\n\n      // Remove partitioning of table\n      df.writeTo(tbl).using(\"delta\").replace()\n\n      assert(snapshot.metadata.partitionColumns === Nil, \"Table is unpartitioned\")\n\n      // Hive does not allow for the removal of the partition column once it has\n      // been added. Spark keeps the partition columns towards the end if it\n      // finds them in Hive. So, for converted tables with partitions,\n      // Hive schema != df.schema\n      val expectedSchema = tableSchema\n\n      // Schema converts to Delta's format\n      verifyTableMetadata(\n        expectedSchema = expectedSchema,\n        expectedProperties = getBaseProperties(snapshot),\n        partitioningCols = Seq(\"part\") // The partitioning information cannot be removed...\n      )\n\n      // table is still usable\n      checkAnswer(spark.table(tbl), df)\n\n      val df2 = spark.range(10).withColumn(\"id2\", 'id)\n      // Gets rid of partition column \"part\" from the schema\n      df2.writeTo(tbl).using(\"delta\").replace()\n\n      val expectedSchema2 = new StructType()\n        .add(\"id\", LongType).add(\"id2\", LongType).add(\"part\", DoubleType)\n      verifyTableMetadataAsync(\n        expectedSchema = expectedSchema2,\n        expectedProperties = getBaseProperties(snapshot),\n        partitioningCols = Seq(\"part\") // The partitioning information cannot be removed...\n      )\n\n      // table is still usable\n      checkAnswer(spark.table(tbl), df2)\n    }\n  }\n\n  test(\"partitioned table + add column\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2).withColumn(\"id2\", 'id)\n      df.writeTo(tbl)\n        .partitionedBy('part)\n        .using(\"delta\")\n        .create()\n\n      val tableSchema =\n        new StructType().add(\"id\", LongType).add(\"part\", DoubleType).add(\"id2\", LongType)\n      verifyTableMetadata(\n        expectedSchema = tableSchema,\n        expectedProperties = getBaseProperties(snapshot),\n        partitioningCols = Seq())\n\n      sql(s\"ALTER TABLE $tbl ADD COLUMNS (id3 bigint)\")\n      verifyTableMetadataAsync(\n        expectedSchema = tableSchema.add(\"id3\", LongType),\n        expectedProperties = getBaseProperties(snapshot),\n        partitioningCols = Seq())\n    }\n  }\n\n  test(\"partitioned convert to delta with schema change\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2).withColumn(\"id2\", 'id)\n      df.writeTo(tbl)\n        .partitionedBy('part)\n        .using(\"parquet\")\n        .create()\n\n      // Partitioning columns go to the end\n      val tableSchema =\n        new StructType().add(\"id\", LongType).add(\"id2\", LongType).add(\"part\", DoubleType)\n      verifyTableMetadata(\n        expectedSchema = tableSchema,\n        expectedProperties = Map.empty,\n        partitioningCols = Seq(\"part\")\n      )\n\n      sql(s\"CONVERT TO DELTA $tbl PARTITIONED BY (part double)\")\n      // Information is duplicated for now\n      verifyTableMetadata(\n        expectedSchema = tableSchema,\n        expectedProperties = Map.empty,\n        partitioningCols = Seq(\"part\")\n      )\n\n      sql(s\"ALTER TABLE $tbl ADD COLUMNS (id3 bigint)\")\n\n      // Hive does not allow for the removal of the partition column once it has\n      // been added. Spark keeps the partition columns towards the end if it\n      // finds them in Hive. So, for converted tables with partitions,\n      // Hive schema != df.schema\n      val expectedSchema = new StructType()\n        .add(\"id\", LongType)\n        .add(\"id2\", LongType)\n        .add(\"id3\", LongType)\n        .add(\"part\", DoubleType)\n\n      verifyTableMetadataAsync(\n        expectedSchema = expectedSchema,\n        partitioningCols = Seq(\"part\")\n      )\n\n      // Table is still queryable\n      checkAnswer(\n        spark.table(tbl),\n        // Ordering of columns are different than df due to Hive semantics\n        spark.range(10).withColumn(\"id2\", 'id)\n          .withColumn(\"part\", 'id / 2)\n          .withColumn(\"id3\", lit(null)))\n    }\n  }\n\n\n  test(\"Very long schemas can be stored in the catalog\") {\n    withTable(tbl) {\n      val schema = StructType(Seq.tabulate(1000)(i => StructField(s\"col$i\", StringType)))\n      require(schema.toDDL.length >= MAX_CATALOG_TYPE_DDL_LENGTH,\n        s\"The length of the schema should be over $MAX_CATALOG_TYPE_DDL_LENGTH \" +\n          \"characters for this test\")\n\n      sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n      verifyTableMetadata(expectedSchema = schema)\n    }\n  }\n\n\n  for (truncationThreshold <- Seq(99999, MAX_CATALOG_TYPE_DDL_LENGTH, 4020))\n  test(s\"Schemas that contain very long fields cannot be stored in the catalog \" +\n    \" when longer than the truncation threshold \" +\n    s\" [DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD = $truncationThreshold]\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD.key ->\n            truncationThreshold.toString) {\n      withTable(tbl) {\n        val schema = new StructType()\n          .add(\"i\", StringType)\n          .add(\"struct\", StructType(Seq.tabulate(1000)(i => StructField(s\"col$i\", StringType))))\n        require(\n          schema.toDDL.length >= 4020,\n          s\"The length of the schema should be over 4020 \" +\n            s\"characters for this test\")\n\n        sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n        if (truncationThreshold > 4020) {\n          verifyTableMetadata(expectedSchema = schema)\n        } else {\n          verifySchemaInCatalog()\n        }\n      }\n    }\n  }\n\n  for (truncationThreshold <- Seq(99999, MAX_CATALOG_TYPE_DDL_LENGTH))\n  test(s\"Schemas that contain very long fields cannot be stored in the catalog - array\" +\n      \" when longer than the truncation threshold \" +\n      s\" [DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD = $truncationThreshold]\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD.key ->\n            truncationThreshold.toString) {\n      withTable(tbl) {\n        val struct = StructType(Seq.tabulate(1000)(i => StructField(s\"col$i\", StringType)))\n        val schema = new StructType()\n          .add(\"i\", StringType)\n          .add(\"array\", ArrayType(struct))\n        require(schema.toDDL.length >= MAX_CATALOG_TYPE_DDL_LENGTH,\n          s\"The length of the schema should be over $MAX_CATALOG_TYPE_DDL_LENGTH \" +\n            s\"characters for this test\")\n\n        sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n        if (truncationThreshold == 99999) {\n          verifyTableMetadata(expectedSchema = schema)\n        } else {\n          verifySchemaInCatalog()\n        }\n      }\n    }\n  }\n\n  for (truncationThreshold <- Seq(99999, MAX_CATALOG_TYPE_DDL_LENGTH))\n  test(s\"Schemas that contain very long fields cannot be stored in the catalog - map\" +\n      \" when longer than the truncation threshold \" +\n      s\" [DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD = $truncationThreshold]\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD.key ->\n            truncationThreshold.toString) {\n      withTable(tbl) {\n        val struct = StructType(Seq.tabulate(1000)(i => StructField(s\"col$i\", StringType)))\n        val schema = new StructType()\n          .add(\"i\", StringType)\n          .add(\"map\", MapType(StringType, struct))\n        require(schema.toDDL.length >= MAX_CATALOG_TYPE_DDL_LENGTH,\n          s\"The length of the schema should be over $MAX_CATALOG_TYPE_DDL_LENGTH \" +\n            s\"characters for this test\")\n\n        sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n        if (truncationThreshold == 99999) {\n          verifyTableMetadata(expectedSchema = schema)\n        } else {\n          verifySchemaInCatalog()\n        }\n      }\n    }\n  }\n\n  for (truncationThreshold <- Seq(99999, MAX_CATALOG_TYPE_DDL_LENGTH))\n  test(s\"Very long nested fields cannot be stored in the catalog - partitioned\" +\n      \" when longer than the truncation threshold \" +\n      s\" [DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD = $truncationThreshold]\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_UPDATE_CATALOG_LONG_FIELD_TRUNCATION_THRESHOLD.key ->\n            truncationThreshold.toString) {\n      withTable(tbl) {\n        val schema = new StructType()\n          .add(\"i\", StringType)\n          .add(\"part\", StringType)\n          .add(\"struct\", StructType(Seq.tabulate(1000)(i => StructField(s\"col$i\", StringType))))\n        require(\n          schema.toDDL.length >= MAX_CATALOG_TYPE_DDL_LENGTH,\n          \"The length of the schema should be over 4000 characters for this test\")\n\n        sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta PARTITIONED BY (part)\")\n        if (truncationThreshold == 99999) {\n          verifyTableMetadata(expectedSchema = schema)\n        } else {\n          verifySchemaInCatalog()\n        }\n      }\n    }\n  }\n\n  test(\"Very long schemas can be stored in the catalog - partitioned\") {\n    withTable(tbl) {\n      val schema = StructType(Seq.tabulate(1000)(i => StructField(s\"col$i\", StringType)))\n        .add(\"part\", StringType)\n      require(schema.toDDL.length >= MAX_CATALOG_TYPE_DDL_LENGTH,\n        \"The length of the schema should be over 4000 characters for this test\")\n\n      sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta PARTITIONED BY (part)\")\n      verifyTableMetadata(expectedSchema = schema)\n    }\n  }\n\n\n  // scalastyle:off nonascii\n  test(\"Schema containing non-latin characters cannot be stored - top-level\") {\n    withTable(tbl) {\n      val schema = new StructType().add(\"今天\", \"string\")\n      sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n      verifySchemaInCatalog(expectedErrorMessage = UpdateCatalog.NON_LATIN_CHARS_ERROR)\n    }\n  }\n\n  test(\"Schema containing non-latin characters cannot be stored - struct\") {\n    withTable(tbl) {\n      val schema = new StructType().add(\"struct\", new StructType().add(\"今天\", \"string\"))\n      sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n      verifySchemaInCatalog(expectedErrorMessage = UpdateCatalog.NON_LATIN_CHARS_ERROR)\n    }\n  }\n\n  test(\"Schema containing non-latin characters cannot be stored - array\") {\n    withTable(tbl) {\n      val schema = new StructType()\n        .add(\"i\", StringType)\n        .add(\"array\", ArrayType(new StructType().add(\"今天\", \"string\")))\n\n      sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n      verifySchemaInCatalog(expectedErrorMessage = UpdateCatalog.NON_LATIN_CHARS_ERROR)\n    }\n  }\n\n  test(\"Schema containing non-latin characters cannot be stored - map\") {\n    withTable(tbl) {\n      val schema = new StructType()\n        .add(\"i\", StringType)\n        .add(\"map\", MapType(StringType, new StructType().add(\"今天\", \"string\")))\n\n      sql(s\"CREATE TABLE $tbl (${schema.toDDL}) USING delta\")\n      verifySchemaInCatalog(expectedErrorMessage = UpdateCatalog.NON_LATIN_CHARS_ERROR)\n    }\n  }\n  // scalastyle:on nonascii\n\n  /**\n   * Verifies that the schema stored in the catalog explicitly is empty, however the getTablesByName\n   * method still correctly returns the actual schema.\n   */\n  private def verifySchemaInCatalog(\n      table: String = tbl,\n      catalogPartitionCols: Seq[String] = Nil,\n      expectedErrorMessage: String = UpdateCatalog.LONG_SCHEMA_ERROR): Unit = {\n    val cat = spark.sessionState.catalog.externalCatalog.getTable(\"default\", table)\n    assert(cat.schema.isEmpty, s\"Schema wasn't empty\")\n    assert(cat.partitionColumnNames === catalogPartitionCols)\n    getBaseProperties(snapshot).foreach { case (k, v) =>\n      assert(cat.properties.get(k) === Some(v),\n        s\"Properties didn't match for table: $table. Expected: ${getBaseProperties(snapshot)}, \" +\n        s\"Got: ${cat.properties}\")\n    }\n    assert(cat.properties(UpdateCatalog.ERROR_KEY) === expectedErrorMessage)\n\n    // Make sure table is readable\n    checkAnswer(spark.table(table), Nil)\n  }\n\n  def testAddRemoveProperties(): Unit = {\n    withTable(tbl) {\n      val df = spark.range(10).toDF(\"id\")\n      df.writeTo(tbl)\n        .using(\"delta\")\n        .create()\n\n      var initialProperties: Map[String, String] = Map.empty\n      val logs = Log4jUsageLogger.track {\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES(some.key = 1, another.key = 2)\")\n\n        initialProperties = getBaseProperties(snapshot)\n        verifyTableMetadataAsync(\n          expectedSchema = df.schema.asNullable,\n          expectedProperties = Map(\"some.key\" -> \"1\", \"another.key\" -> \"2\") ++\n            initialProperties\n        )\n      }\n      val updateLogged = logs.filter(_.metric == \"tahoeEvent\")\n        .filter(_.tags.get(\"opType\").exists(_.startsWith(\"delta.catalog.update.properties\")))\n      assert(updateLogged.nonEmpty, \"Ensure that the schema update in the MetaStore is logged\")\n\n      // The UpdateCatalog hook only checks if new properties have been\n      // added. If properties have been removed only, no metadata update will be triggered.\n      val logs2 = Log4jUsageLogger.track {\n        sql(s\"ALTER TABLE $tbl UNSET TBLPROPERTIES(another.key)\")\n        verifyTableMetadataAsync(\n          expectedSchema = df.schema.asNullable,\n          expectedProperties = Map(\"some.key\" -> \"1\", \"another.key\" -> \"2\") ++\n            initialProperties\n        )\n      }\n      val updateLogged2 = logs2.filter(_.metric == \"tahoeEvent\")\n        .filter(_.tags.get(\"opType\").exists(_.startsWith(\"delta.catalog.update.properties\")))\n      assert(updateLogged2.size == 0, \"Ensure that the schema update in the MetaStore is logged\")\n\n      // Adding a new property will trigger an update\n      val logs3 = Log4jUsageLogger.track {\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES(a.third.key = 3)\")\n        verifyTableMetadataAsync(\n          expectedSchema = df.schema.asNullable,\n          expectedProperties = Map(\"some.key\" -> \"1\", \"a.third.key\" -> \"3\") ++\n            getBaseProperties(snapshot)\n        )\n      }\n      val updateLogged3 = logs3.filter(_.metric == \"tahoeEvent\")\n        .filter(_.tags.get(\"opType\").exists(_.startsWith(\"delta.catalog.update.properties\")))\n      assert(updateLogged3.nonEmpty, \"Ensure that the schema update in the MetaStore is logged\")\n    }\n  }\n\n  test(\"add and remove properties\") {\n    testAddRemoveProperties()\n  }\n\n  test(\"alter table commands update the catalog\") {\n    runAlterTableTests { (tableName, expectedSchema) =>\n      verifyTableMetadataAsync(\n        expectedSchema = expectedSchema,\n        // The ALTER TABLE statements in runAlterTableTests create table version 7.\n        // However, version 7 is created by dropping a CHECK constraint, which currently\n        // *does not* trigger a catalog update. For Hive tables, only *adding* properties\n        // causes a catalog update, not *removing*. Hence, the metadata in the catalog should\n        // still be at version 6.\n        expectedProperties = getBaseProperties(snapshotAt(6)) ++\n          Map(\"some\" -> \"thing\", \"delta.constraints.id_3\" -> \"id3 > 10\"),\n        table = tableName\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaUpdateCatalogSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.hooks.UpdateCatalog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.scalatest.time.SpanSugar\n\nimport org.apache.spark.{SparkConf, SparkContext}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{lit, struct}\nimport org.apache.spark.sql.types.{BooleanType, DoubleType, IntegerType, LongType, StringType, StructField, StructType}\nimport org.apache.spark.util.{ThreadUtils, Utils}\n\nabstract class DeltaUpdateCatalogSuiteBase\n  extends QueryTest\n    with DeltaSQLTestUtils\n    with SpanSugar {\n\n  protected val tbl = \"delta_table\"\n\n  import testImplicits._\n\n  protected def cleanupDefaultTable(): Unit = disableUpdates {\n    spark.sql(s\"DROP TABLE IF EXISTS $tbl\")\n    val path = spark.sessionState.catalog.defaultTablePath(TableIdentifier(tbl))\n    try Utils.deleteRecursively(new File(path)) catch {\n      case NonFatal(e) => // do nothing\n    }\n  }\n\n  /** Turns off the storing of metadata (schema + properties) in the catalog. */\n  protected def disableUpdates(f: => Unit): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"false\") {\n      f\n    }\n  }\n\n  protected def deltaLog: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n  protected def snapshot: Snapshot = deltaLog.unsafeVolatileSnapshot\n  protected def snapshotAt(v: Long): Snapshot = deltaLog.getSnapshotAt(v)\n\n  protected def getBaseProperties(snapshot: Snapshot): Map[String, String] = {\n    Map(\n      DeltaConfigs.METASTORE_LAST_UPDATE_VERSION -> snapshot.version.toString,\n      DeltaConfigs.METASTORE_LAST_COMMIT_TIMESTAMP -> snapshot.timestamp.toString,\n      DeltaConfigs.MIN_READER_VERSION.key -> snapshot.protocol.minReaderVersion.toString,\n      DeltaConfigs.MIN_WRITER_VERSION.key -> snapshot.protocol.minWriterVersion.toString) ++\n      snapshot.protocol.readerAndWriterFeatureNames.map { name =>\n        s\"${TableFeatureProtocolUtils.FEATURE_PROP_PREFIX}$name\" ->\n          TableFeatureProtocolUtils.FEATURE_PROP_SUPPORTED\n      } ++ snapshot.metadata.configuration.get(\"delta.enableDeletionVectors\")\n        .map(\"delta.enableDeletionVectors\" -> _).toMap\n  }\n\n  /**\n   * Verifies that the table metadata in the catalog are eventually up-to-date. Updates to the\n   * catalog are generally asynchronous, except explicit DDL operations, e.g. CREATE/REPLACE.\n   */\n  protected def verifyTableMetadataAsync(\n      expectedSchema: StructType,\n      expectedProperties: Map[String, String] = getBaseProperties(snapshot),\n      table: String = tbl,\n      partitioningCols: Seq[String] = Nil): Unit = {\n    // We unfortunately need an eventually, because the updates can be async\n    eventually(timeout(10.seconds)) {\n      verifyTableMetadata(expectedSchema, expectedProperties, table, partitioningCols)\n    }\n    // Ensure that no other threads will later revert us back to the state we just checked\n    if (!UpdateCatalog.awaitCompletion(10000)) {\n      logWarning(s\"There are active catalog udpate requests after 10 seconds\")\n    }\n  }\n\n  protected def filterProperties(properties: Map[String, String]): Map[String, String]\n\n  /** Verifies that the table metadata in the catalog are up-to-date. */\n  protected def verifyTableMetadata(\n      expectedSchema: StructType,\n      expectedProperties: Map[String, String] = getBaseProperties(snapshot),\n      table: String = tbl,\n      partitioningCols: Seq[String] = Nil): Unit = {\n    DeltaLog.clearCache()\n    val cat = spark.sessionState.catalog.externalCatalog.getTable(\"default\", table)\n    assert(cat.schema === expectedSchema, s\"Schema didn't match for table: $table\")\n    assert(cat.partitionColumnNames === partitioningCols)\n    assert(filterProperties(cat.properties) === expectedProperties,\n      s\"Properties didn't match for table: $table\")\n\n    val tables = spark.sessionState.catalog.getTablesByName(Seq(TableIdentifier(table)))\n\n    assert(tables.head.schema === expectedSchema)\n    assert(tables.head.partitionColumnNames === partitioningCols)\n    assert(filterProperties(tables.head.properties) === expectedProperties)\n  }\n\n\n  test(\"mergeSchema\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2)\n      df.writeTo(tbl).using(\"delta\").create()\n\n      verifyTableMetadata(expectedSchema = df.schema.asNullable)\n\n      val df2 = spark.range(10).withColumn(\"part\", 'id / 2).withColumn(\"id2\", 'id)\n      df2.writeTo(tbl)\n        .option(\"mergeSchema\", \"true\")\n        .append()\n\n      verifyTableMetadataAsync(expectedSchema = df2.schema.asNullable)\n    }\n  }\n\n  test(\"mergeSchema - nested data types\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2)\n        .withColumn(\"str\", struct('id.cast(\"int\") as \"int\"))\n      df.writeTo(tbl).using(\"delta\").create()\n\n      verifyTableMetadata(expectedSchema = df.schema.asNullable)\n\n      val df2 = spark.range(10).withColumn(\"part\", 'id / 2)\n        .withColumn(\"str\", struct('id as \"id2\", 'id.cast(\"int\") as \"int\"))\n      df2.writeTo(tbl)\n        .option(\"mergeSchema\", \"true\")\n        .append()\n\n      val schema = new StructType()\n        .add(\"id\", LongType)\n        .add(\"part\", DoubleType)\n        .add(\"str\", new StructType()\n          .add(\"int\", IntegerType)\n          .add(\"id2\", LongType)) // New columns go to the end\n      verifyTableMetadataAsync(expectedSchema = schema)\n    }\n  }\n\n\n  test(\"merge\") {\n    val tmp = \"tmpView\"\n    withDeltaTable { df =>\n      withTempView(tmp) {\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n          df.withColumn(\"id2\", 'id).createOrReplaceTempView(tmp)\n          sql(\n            s\"\"\"MERGE INTO $tbl t\n               |USING $tmp s\n               |ON t.id = s.id\n               |WHEN NOT MATCHED THEN INSERT *\n             \"\"\".stripMargin)\n\n          verifyTableMetadataAsync(df.withColumn(\"id2\", 'id).schema.asNullable)\n        }\n      }\n    }\n  }\n\n  test(\"creating and replacing a table puts the schema and table properties in the metastore\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2).withColumn(\"id2\", 'id)\n      df.writeTo(tbl)\n        .tableProperty(\"delta.checkpointInterval\", \"5\")\n        .tableProperty(\"some\", \"thing\")\n        .partitionedBy('part)\n        .using(\"delta\")\n        .create()\n\n      verifyTableMetadata(\n        expectedSchema = df.schema.asNullable,\n        expectedProperties = getBaseProperties(snapshot) ++ Map(\n          \"delta.checkpointInterval\" -> \"5\",\n          \"some\" -> \"thing\")\n      )\n\n      val df2 = spark.range(10).withColumn(\"part\", 'id / 2)\n      df2.writeTo(tbl)\n        .tableProperty(\"other\", \"thing\")\n        .using(\"delta\")\n        .replace()\n\n      verifyTableMetadata(\n        expectedSchema = df2.schema.asNullable,\n        expectedProperties = getBaseProperties(snapshot) ++ Map(\"other\" -> \"thing\")\n      )\n    }\n  }\n\n  test(\"creating table in metastore over existing path\") {\n    withTempDir { dir =>\n      withTable(tbl) {\n        val df = spark.range(10).withColumn(\"part\", 'id % 2).withColumn(\"id2\", 'id)\n        df.write.format(\"delta\").partitionBy(\"part\").save(dir.getCanonicalPath)\n\n        sql(s\"CREATE TABLE $tbl USING delta LOCATION '${dir.getCanonicalPath}'\")\n        verifyTableMetadata(df.schema.asNullable)\n      }\n    }\n  }\n\n  test(\"replacing non-Delta table\") {\n    withTable(tbl) {\n      val df = spark.range(10).withColumn(\"part\", 'id / 2).withColumn(\"id2\", 'id)\n      df.writeTo(tbl)\n        .tableProperty(\"delta.checkpointInterval\", \"5\")\n        .tableProperty(\"some\", \"thing\")\n        .partitionedBy('part)\n        .using(\"parquet\")\n        .create()\n\n      val e = intercept[AnalysisException] {\n        df.writeTo(tbl).using(\"delta\").replace()\n      }\n\n      assert(e.getMessage.contains(\"not a Delta table\"))\n    }\n  }\n\n  test(\"alter table add columns\") {\n    withDeltaTable { df =>\n      sql(s\"ALTER TABLE $tbl ADD COLUMNS (id2 bigint)\")\n      verifyTableMetadataAsync(df.withColumn(\"id2\", 'id).schema.asNullable)\n    }\n  }\n\n  protected def runAlterTableTests(f: (String, StructType) => Unit): Unit = {\n    // We set the default minWriterVersion to the version required to ADD/DROP CHECK constraints\n    // to prevent an automatic protocol  upgrade  (i.e. an implicit property change) when adding\n    // the CHECK constraint below.\n    withSQLConf(\n      \"spark.databricks.delta.properties.defaults.minReaderVersion\" -> \"1\",\n      \"spark.databricks.delta.properties.defaults.minWriterVersion\" -> \"3\") {\n      withDeltaTable { _ =>\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES ('some' = 'thing', 'other' = 'thing')\")\n        sql(s\"ALTER TABLE $tbl UNSET TBLPROPERTIES ('other')\")\n        sql(s\"ALTER TABLE $tbl ADD COLUMNS (id2 bigint, id3 bigint)\")\n        sql(s\"ALTER TABLE $tbl CHANGE COLUMN id2 id2 bigint FIRST\")\n        sql(s\"ALTER TABLE $tbl REPLACE COLUMNS (id3 bigint, id2 bigint, id bigint)\")\n        sql(s\"ALTER TABLE $tbl ADD CONSTRAINT id_3 CHECK (id3 > 10)\")\n        sql(s\"ALTER TABLE $tbl DROP CONSTRAINT id_3\")\n\n        val expectedSchema = StructType(Seq(\n          StructField(\"id3\", LongType, true),\n          StructField(\"id2\", LongType, true),\n          StructField(\"id\", LongType, true))\n        )\n\n        f(tbl, expectedSchema)\n      }\n    }\n  }\n\n  /**\n   * Creates a table with the name `tbl` and executes a function that takes a representative\n   * DataFrame with the schema of the table. Performs cleanup of the table afterwards.\n   */\n  protected def withDeltaTable(f: DataFrame => Unit): Unit = {\n    // Turn off async updates so that we don't update the catalog during table cleanup\n    disableUpdates {\n      withTable(tbl) {\n        withSQLConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"true\") {\n          sql(s\"CREATE TABLE $tbl (id bigint) USING delta\")\n          val df = spark.range(10)\n          verifyTableMetadata(df.schema.asNullable)\n\n          f(df.toDF())\n        }\n      }\n    }\n  }\n\n  test(\"skip update when flag is not set\") {\n    withDeltaTable(df => {\n      withSQLConf(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"false\") {\n        val propertiesAtV1 = getBaseProperties(snapshot)\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES(some.key = 1)\")\n        verifyTableMetadataAsync(\n          expectedSchema = df.schema.asNullable,\n          expectedProperties = propertiesAtV1)\n      }\n    })\n  }\n\n\n  test(s\"REORG TABLE does not perform catalog update\") {\n    val tableName = \"myTargetTable\"\n    withDeltaTable { df =>\n      sql(s\"REORG TABLE $tbl APPLY (PURGE)\")\n      verifyTableMetadataAsync(df.schema.asNullable)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaUsageLogsOpsTypes.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nobject DeltaUsageLogsOpTypes {\n  final val BACKFILL_COMMAND = \"delta.rowTracking.backfill.stats\"\n  final val BACKFILL_BATCH = \"delta.rowTracking.backfill.batch.stats\"\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaVacuumSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.DeltaOperations.{Delete, Write}\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.DeltaUnsupportedOperationException\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, Metadata, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.VacuumCommand\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, DeltaFileOperations, FileNames}\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.FileSystem\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row, SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTableType\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.catalyst.util.IntervalUtils\nimport org.apache.spark.sql.execution.metric.SQLMetric\nimport org.apache.spark.sql.execution.metric.SQLMetrics.createMetric\nimport org.apache.spark.sql.functions.{col, expr, lit}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.unsafe.types.UTF8String\nimport org.apache.spark.util.ManualClock\n\ntrait DeltaVacuumSuiteBase extends QueryTest\n  with SharedSparkSession\n  with GivenWhenThen\n  with DeltaSQLTestUtils\n  with DeletionVectorsTestUtils\n  with DeltaTestUtilsForTempViews\n  with CatalogOwnedTestBaseSuite {\n\n  private def executeWithEnvironment(file: File)(f: (File, ManualClock) => Unit): Unit = {\n    val clock = new ManualClock()\n    withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n      f(file, clock)\n    }\n  }\n\n  protected def isLiteVacuum: Boolean = false\n\n  protected def testFullVacuumOnly(\n      testName: String, testTags: org.scalatest.Tag*)(\n      testFun: => Any): Unit = {\n    // Certain tests are not valid for lite vacuum as lite vacuum doesn't care about\n    // the files not tracked by the delta log.\n    if (isLiteVacuum) {\n      ignore(testName + \" (full Vacuum only)\", testTags: _*)(testFun)\n    } else {\n      test(testName, testTags: _*)(testFun)\n    }\n  }\n\n  protected def withEnvironment(f: (File, ManualClock) => Unit): Unit =\n    withTempDir(file => executeWithEnvironment(file)(f))\n\n  protected def withEnvironment(prefix: String)(f: (File, ManualClock) => Unit): Unit =\n    withTempDir(prefix)(file => executeWithEnvironment(file)(f))\n\n  protected def defaultTombstoneInterval: Long = {\n    DeltaConfigs.getMilliSeconds(\n      IntervalUtils.safeStringToInterval(\n        UTF8String.fromString(DeltaConfigs.TOMBSTONE_RETENTION.defaultValue)))\n  }\n\n  /** Lists the data files in a given dir recursively. */\n  protected def listDataFiles(spark: SparkSession, tableDir: String): Seq[String] = {\n    val result = ArrayBuffer.empty[String]\n    // scalastyle:off deltahadoopconfiguration\n    val fs = FileSystem.get(spark.sessionState.newHadoopConf())\n    // scalastyle:on deltahadoopconfiguration\n    val iterator = fs.listFiles(fs.makeQualified(new Path(tableDir)), true)\n    while (iterator.hasNext) {\n      val path = iterator.next().getPath.toUri.toString\n      if (path.endsWith(\".parquet\") && !path.contains(\".checkpoint\")) {\n        result += path\n      }\n    }\n    result.toSeq\n  }\n\n  protected def assertNumFiles(\n      deltaLog: DeltaLog,\n      addFiles: Int,\n      addFilesWithDVs: Int,\n      dvFiles: Int,\n      dataFiles: Int): Unit = {\n    assert(deltaLog.update().allFiles.count() === addFiles)\n    assert(getFilesWithDeletionVectors(deltaLog).size === addFilesWithDVs)\n    assert(listDeletionVectors(deltaLog).size === dvFiles)\n    assert(listDataFiles(spark, deltaLog.dataPath.toString).size === dataFiles)\n  }\n\n  implicit def fileToPathString(f: File): String = new Path(f.getAbsolutePath).toString\n\n  trait Operation\n  /**\n   * Write a file to the given absolute or relative path. Could be inside or outside the Reservoir\n   * base path. The file can be committed to the action log to be tracked, or left out for deletion.\n   */\n  case class CreateFile(\n      path: String,\n      commitToActionLog: Boolean,\n      partitionValues: Map[String, String] = Map.empty) extends Operation\n  /** Create a directory at the given path. */\n  case class CreateDirectory(path: String) extends Operation\n  /**\n   * Logically deletes a file in the action log. Paths can be absolute or relative paths, and can\n   * point to files inside and outside a reservoir.\n   */\n  case class LogicallyDeleteFile(path: String) extends Operation\n  /** Check that the given paths exist. */\n  case class CheckFiles(paths: Seq[String], exist: Boolean = true) extends Operation\n  /** Garbage collect the reservoir. */\n  case class GC(\n      dryRun: Boolean,\n      expectedDf: Seq[String],\n      retentionHours: Option[Double] = None) extends Operation\n  case class GCByInventory(dryRun: Boolean, expectedDf: Seq[String],\n      retentionHours: Option[Double] = None,\n      inventory: Option[DataFrame] = Option.empty[DataFrame]) extends Operation\n  /** Garbage collect the reservoir. */\n  case class ExecuteVacuumInScala(\n      deltaTable: io.delta.tables.DeltaTable,\n      expectedDf: Seq[String],\n      retentionHours: Option[Double] = None) extends Operation\n  /** Advance the time. */\n  case class AdvanceClock(timeToAdd: Long) extends Operation\n  /** Execute SQL command */\n  case class ExecuteVacuumInSQL(\n      identifier: String,\n      expectedDf: Seq[String],\n      retentionHours: Option[Long] = None,\n      dryRun: Boolean = false) extends Operation {\n    def sql: String = {\n      val retainStr = retentionHours.map { h => s\"RETAIN $h HOURS\"}.getOrElse(\"\")\n      val dryRunStr = if (dryRun) \"DRY RUN\" else \"\"\n      s\"VACUUM $identifier $retainStr $dryRunStr\"\n    }\n  }\n  /**\n   * Expect a failure with the given exception type. Expect the given `msg` fragments as the error\n   * message.\n   */\n  case class ExpectFailure[T <: Throwable](\n      action: Operation,\n      expectedError: Class[T],\n      msg: Seq[String]) extends Operation\n\n  private final val RANDOM_FILE_CONTENT = \"gibberish\"\n\n  protected def createFile(\n      reservoirBase: String,\n      filePath: String,\n      file: File,\n      clock: ManualClock,\n      partitionValues: Map[String, String] = Map.empty): AddFile = {\n    FileUtils.write(file, RANDOM_FILE_CONTENT)\n    file.setLastModified(clock.getTimeMillis())\n    createTestAddFile(\n      encodedPath = filePath,\n      partitionValues = partitionValues,\n      modificationTime = clock.getTimeMillis())\n  }\n\n  protected def gcTest(table: DeltaTableV2, clock: ManualClock)(actions: Operation*): Unit = {\n    import testImplicits._\n    val basePath = table.deltaLog.dataPath.toString\n    val fs = new Path(basePath).getFileSystem(table.deltaLog.newDeltaHadoopConf())\n    actions.foreach {\n      case CreateFile(path, commit, partitionValues) =>\n        Given(s\"*** Writing file to $path. Commit to log: $commit\")\n        val sanitizedPath = new Path(path).toUri.toString\n        val file = new File(\n          fs.makeQualified(DeltaFileOperations.absolutePath(basePath, sanitizedPath)).toUri)\n        if (commit) {\n          if (!DeltaTableUtils.isDeltaTable(spark, new Path(basePath))) {\n            // initialize the table\n            val version = table.startTransaction().commitManually()\n            setCommitClock(table, version, clock)\n          }\n          val txn = table.startTransaction()\n          val action = createFile(basePath, sanitizedPath, file, clock, partitionValues)\n          val version = txn.commit(Seq(action), Write(SaveMode.Append))\n          setCommitClock(table, version, clock)\n        } else {\n          createFile(basePath, path, file, clock)\n        }\n      case CreateDirectory(path) =>\n        Given(s\"*** Creating directory at $path\")\n        val dir = new File(DeltaFileOperations.absolutePath(basePath, path).toUri)\n        assert(dir.mkdir(), s\"Couldn't create directory at $path\")\n        assert(dir.setLastModified(clock.getTimeMillis()))\n      case LogicallyDeleteFile(path) =>\n        Given(s\"*** Removing files\")\n        val txn = table.startTransaction()\n        // scalastyle:off\n        val metrics = Map[String, SQLMetric](\n          \"numRemovedFiles\" -> createMetric(sparkContext, \"number of files removed.\"),\n          \"numAddedFiles\" -> createMetric(sparkContext, \"number of files added.\"),\n          \"numDeletedRows\" -> createMetric(sparkContext, \"number of rows deleted.\"),\n          \"numCopiedRows\" -> createMetric(sparkContext, \"total number of rows.\")\n        )\n        txn.registerSQLMetrics(spark, metrics)\n        val encodedPath = new Path(path).toUri.toString\n        val size = Some(RANDOM_FILE_CONTENT.length.toLong)\n        val version = txn.commit(\n          Seq(RemoveFile(encodedPath, Option(clock.getTimeMillis()), size = size)),\n          Delete(Seq(Literal.TrueLiteral)))\n        setCommitClock(table, version, clock)\n      // scalastyle:on\n      case e: ExecuteVacuumInSQL =>\n        Given(s\"*** Executing SQL: ${e.sql}\")\n        val qualified = e.expectedDf.map(p => fs.makeQualified(new Path(p)).toString)\n        val df = spark.sql(e.sql).as[String]\n        checkDatasetUnorderly(df, qualified: _*)\n      case CheckFiles(paths, exist) =>\n        Given(s\"*** Checking files exist=$exist\")\n        paths.foreach { p =>\n          val sp = new Path(p).toUri.toString\n          val f = new File(fs.makeQualified(DeltaFileOperations.absolutePath(basePath, sp)).toUri)\n          val res = if (exist) f.exists() else !f.exists()\n          assert(res, s\"Expectation: exist=$exist, paths: $p\")\n        }\n      case GC(dryRun, expectedDf, retention) =>\n        Given(\"*** Garbage collecting Reservoir\")\n        val result = VacuumCommand.gc(spark, table, dryRun, retention, clock = clock)\n        val qualified = expectedDf.map(p => fs.makeQualified(new Path(p)).toString)\n        checkDatasetUnorderly(result.as[String], qualified: _*)\n      case GCByInventory(dryRun, expectedDf, retention, inventory) =>\n        Given(\"*** Garbage collecting using inventory\")\n        val result =\n          VacuumCommand.gc(spark, table, dryRun, retention, inventory, clock = clock)\n        val qualified = expectedDf.map(p => fs.makeQualified(new Path(p)).toString)\n        checkDatasetUnorderly(result.as[String], qualified: _*)\n      case ExecuteVacuumInScala(deltaTable, expectedDf, retention) =>\n        Given(\"*** Garbage collecting Reservoir using Scala\")\n        val result = if (retention.isDefined) {\n          deltaTable.vacuum(retention.get)\n        } else {\n          deltaTable.vacuum()\n        }\n        if(expectedDf == Seq()) {\n          assert(result === spark.emptyDataFrame)\n        } else {\n          val qualified = expectedDf.map(p => fs.makeQualified(new Path(p)).toString)\n          checkDatasetUnorderly(result.as[String], qualified: _*)\n        }\n      case AdvanceClock(timeToAdd: Long) =>\n        Given(s\"*** Advancing clock by $timeToAdd millis\")\n        clock.advance(timeToAdd)\n      case ExpectFailure(action, failure, msg) =>\n        Given(s\"*** Expecting failure of ${failure.getName} for action: $action\")\n        val e = intercept[Exception](gcTest(table, clock)(action))\n        assert(e.getClass === failure)\n        assert(\n          msg.forall(m =>\n            e.getMessage.toLowerCase(Locale.ROOT).contains(m.toLowerCase(Locale.ROOT))),\n          e.getMessage + \"didn't contain: \" + msg.mkString(\"[\", \", \", \"]\"))\n    }\n  }\n\n  protected def vacuumSQLTest(table: DeltaTableV2, tableName: String) {\n    val committedFile = \"committedFile.txt\"\n    val notCommittedFile = \"notCommittedFile.txt\"\n\n    val expectedDf = Option.when(!isLiteVacuum)(new Path(table.path, notCommittedFile).toString)\n    gcTest(table, new ManualClock())(\n      // Prepare the table with files with timestamp of epoch-time 0 (i.e. 01-01-1970 00:00)\n      CreateFile(committedFile, commitToActionLog = true),\n      CreateFile(notCommittedFile, commitToActionLog = false),\n      CheckFiles(Seq(committedFile, notCommittedFile)),\n\n      // Dry run should return the not committed file and but not delete files\n      ExecuteVacuumInSQL(tableName, expectedDf = expectedDf.toSeq, dryRun = true),\n      CheckFiles(Seq(committedFile, notCommittedFile)),\n\n      // Actual run should not delete the committed file but delete the not-committed file\n      ExecuteVacuumInSQL(tableName, Seq(table.path.toString)),\n      CheckFiles(Seq(committedFile)),\n      // File ts older than default retention\n      // However, non committed files are not deleted by lite vacuum.\n      CheckFiles(Seq(notCommittedFile), exist = isLiteVacuum),\n\n      // Logically delete the file.\n      LogicallyDeleteFile(committedFile),\n      CheckFiles(Seq(committedFile)),\n\n      // Vacuum with 0 retention should actually delete the file.\n      ExecuteVacuumInSQL(tableName, Seq(table.path.toString), Some(0)),\n      CheckFiles(Seq(committedFile), exist = false))\n  }\n\n  protected def vacuumScalaTest(deltaTable: io.delta.tables.DeltaTable, tablePath: String) {\n    val table = DeltaTableV2(spark, new Path(tablePath), options = Map.empty, \"test\")\n    val committedFile = \"committedFile.txt\"\n    val notCommittedFile = \"notCommittedFile.txt\"\n\n    gcTest(table, new ManualClock())(\n      // Prepare the table with files with timestamp of epoch-time 0 (i.e. 01-01-1970 00:00)\n      CreateFile(committedFile, commitToActionLog = true),\n      CreateFile(notCommittedFile, commitToActionLog = false),\n      CheckFiles(Seq(committedFile, notCommittedFile)),\n\n      // Actual run should delete the not committed file and but not delete files\n      ExecuteVacuumInScala(deltaTable, Seq()),\n      CheckFiles(Seq(committedFile)),\n      // File ts older than default retention\n      // However, non committed files are not deleted by lite vacuum.\n      CheckFiles(Seq(notCommittedFile), exist = isLiteVacuum),\n\n      // Logically delete the file.\n      LogicallyDeleteFile(committedFile),\n      CheckFiles(Seq(committedFile)),\n\n      // Vacuum with 0 retention should actually delete the file.\n      ExecuteVacuumInScala(deltaTable, Seq(), Some(0)),\n      CheckFiles(Seq(committedFile), exist = false))\n  }\n\n  /**\n   * Helper method to tell us if the given filePath exists. Thus, it can be used to detect if a\n   * file has been deleted.\n   */\n  protected def pathExists(deltaLog: DeltaLog, filePath: String): Boolean = {\n    val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n    fs.exists(DeltaFileOperations.absolutePath(deltaLog.dataPath.toString, filePath))\n  }\n\n  protected def deleteCommitFile(table: DeltaTableV2, version: Long) = {\n    new File(DeltaCommitFileProvider(table.update()).deltaFile(version).toUri).delete()\n  }\n\n  /**\n   * Helper method to get all of the [[AddCDCFile]]s that exist in the delta table\n   */\n  protected def getCDCFiles(deltaLog: DeltaLog): Seq[AddCDCFile] = {\n    val changes = deltaLog.getChanges(\n      startVersion = 0, catalogTableOpt = None, failOnDataLoss = true)\n    changes.flatMap(_._2).collect { case a: AddCDCFile => a }.toList\n  }\n\n  protected def setCommitClock(table: DeltaTableV2, version: Long, clock: ManualClock) = {\n    val f = new File(DeltaCommitFileProvider(table.update()).deltaFile(version).toUri)\n    f.setLastModified(clock.getTimeMillis())\n  }\n\n  protected def testCDCVacuumForUpdateMerge(): Unit = {\n    withSQLConf(\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n      DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\"\n    ) {\n      withTempDir { dir =>\n        // create table - version 0\n        spark.range(10)\n          .repartition(1)\n          .write\n          .format(\"delta\")\n          .save(dir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n        // update table - version 1\n        deltaTable.update(expr(\"id == 0\"), Map(\"id\" -> lit(\"11\")))\n\n        // merge table - version 2\n        deltaTable.as(\"target\")\n          .merge(\n            spark.range(0, 12).toDF().as(\"src\"),\n            \"src.id = target.id\")\n          .whenMatched()\n          .updateAll()\n          .whenNotMatched()\n          .insertAll()\n          .execute()\n\n        val df1 = sql(s\"SELECT * FROM delta.`${dir.getAbsolutePath}`\").collect()\n\n        val changes = getCDCFiles(deltaLog)\n\n        // vacuum will not delete the cdc files if they are within retention\n        sql(s\"VACUUM '${dir.getAbsolutePath}' RETAIN 100 HOURS\")\n        changes.foreach { change =>\n          assert(pathExists(deltaLog, change.path)) // cdc file exists\n        }\n\n        // vacuum will delete the cdc files if they are outside retention\n        sql(s\"VACUUM '${dir.getAbsolutePath}' RETAIN 0 HOURS\")\n        changes.foreach { change =>\n          assert(!pathExists(deltaLog, change.path)) // cdc file has been removed\n        }\n\n        // try reading the table\n        checkAnswer(sql(s\"SELECT * FROM delta.`${dir.getAbsolutePath}`\"), df1)\n\n        // try reading cdc data\n        val e = intercept[SparkException] {\n          spark.read\n            .format(\"delta\")\n            .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n            .option(\"startingVersion\", 1)\n            .option(\"endingVersion\", 2)\n            .load(dir.getAbsolutePath)\n            .count()\n        }\n        // QueryExecutionErrors.readCurrentFileNotFoundError\n        var expectedErrorMessage = \"It is possible the underlying files have been updated.\"\n        assert(e.getMessage.contains(expectedErrorMessage))\n      }\n    }\n  }\n\n  protected def testCDCVacuumForTombstones(): Unit = {\n    withSQLConf(\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n      DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\"\n    ) {\n      withTempDir { dir =>\n        // create table - version 0\n        spark.range(0, 10, 1, 1)\n          .withColumn(\"part\", col(\"id\") % 2)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .save(dir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n        // create version 1 - delete single row should generate one cdc file\n        deltaTable.delete(col(\"id\") === lit(9))\n        val changes = getCDCFiles(deltaLog)\n        assert(changes.size === 1)\n        val cdcPath = changes.head.path\n        assert(pathExists(deltaLog, cdcPath))\n        val df1 = sql(s\"SELECT * FROM delta.`${dir.getAbsolutePath}`\").collect()\n\n        // vacuum will not delete the cdc files if they are within retention\n        sql(s\"VACUUM '${dir.getAbsolutePath}' RETAIN 100 HOURS\")\n        assert(pathExists(deltaLog, cdcPath)) // cdc path exists\n\n        // vacuum will delete the cdc files when they are outside retention\n        // one cdc file and one RemoveFile should be deleted by vacuum\n        sql(s\"VACUUM '${dir.getAbsolutePath}' RETAIN 0 HOURS\")\n        assert(!pathExists(deltaLog, cdcPath)) // cdc file is removed\n\n        // try reading the table\n        checkAnswer(sql(s\"SELECT * FROM delta.`${dir.getAbsolutePath}`\"), df1)\n\n        // create version 2 - partition delete - does not create new cdc files\n        deltaTable.delete(col(\"part\") === lit(0))\n\n        assert(getCDCFiles(deltaLog).size === 1) // still just the one cdc file from before.\n\n        // try reading cdc data\n        val e = intercept[SparkException] {\n          spark.read\n            .format(\"delta\")\n            .option(DeltaOptions.CDC_READ_OPTION, \"true\")\n            .option(\"startingVersion\", 1)\n            .option(\"endingVersion\", 2)\n            .load(dir.getAbsolutePath)\n            .count()\n        }\n        // QueryExecutionErrors.readCurrentFileNotFoundError\n        var expectedErrorMessage = \"It is possible the underlying files have been updated.\"\n        assert(e.getMessage.contains(expectedErrorMessage))\n      }\n    }\n  }\n}\n\nclass DeltaVacuumSuite extends DeltaVacuumSuiteBase with DeltaSQLCommandTest {\n  import testImplicits._\n\n  override def sparkConf: SparkConf = {\n    super.sparkConf.set(\"spark.sql.sources.parallelPartitionDiscovery.parallelism\", \"2\")\n  }\n\n  testQuietly(\"basic case - SQL command on path-based tables with direct 'path'\") {\n    withEnvironment { (tempDir, _) =>\n      val table = DeltaTableV2(spark, tempDir)\n      vacuumSQLTest(table, tableName = s\"'$tempDir'\")\n    }\n  }\n\n  testQuietly(\"basic case - SQL command on path-based table with delta.`path`\") {\n    withEnvironment { (tempDir, _) =>\n      val table = DeltaTableV2(spark, tempDir)\n      vacuumSQLTest(table, tableName = s\"delta.`$tempDir`\")\n    }\n  }\n\n  testQuietly(\"basic case - SQL command on name-based table\") {\n    val tableName = \"deltaTable\"\n    withEnvironment { (_, _) =>\n      withTable(tableName) {\n        import testImplicits._\n        spark.emptyDataset[Int].write.format(\"delta\").saveAsTable(tableName)\n        val table = DeltaTableV2(spark, new TableIdentifier(tableName))\n        vacuumSQLTest(table, tableName)\n      }\n    }\n  }\n\n  testQuietlyWithTempView(\"basic case - SQL command on temp view not supported\") { isSQLTempView =>\n    val tableName = \"deltaTable\"\n    val viewName = \"v\"\n    withEnvironment { (_, _) =>\n      withTable(tableName) {\n        import testImplicits._\n        spark.emptyDataset[Int].write.format(\"delta\").saveAsTable(tableName)\n        createTempViewFromTable(tableName, isSQLTempView)\n        val table = DeltaTableV2(spark, new TableIdentifier(tableName))\n        val e = intercept[AnalysisException] {\n          vacuumSQLTest(table, viewName)\n        }\n        assert(e.getMessage.contains(\"'VACUUM' expects a table but `v` is a view\"))\n      }\n    }\n  }\n\n  test(\"basic case - Scala on path-based table\") {\n    withEnvironment { (tempDir, _) =>\n      import testImplicits._\n      spark.emptyDataset[Int].write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.getAbsolutePath)\n      vacuumScalaTest(deltaTable, tempDir.getAbsolutePath)\n    }\n  }\n\n  test(\"basic case - Scala on name-based table\") {\n    val tableName = \"deltaTable\"\n    withEnvironment { (tempDir, _) =>\n      withTable(tableName) {\n        // Initialize the table so that we can create the DeltaTable object\n        import testImplicits._\n        spark.emptyDataset[Int].write.format(\"delta\").saveAsTable(tableName)\n        val deltaTable = io.delta.tables.DeltaTable.forName(tableName)\n        val tablePath =\n          new File(spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName)).location)\n        vacuumScalaTest(deltaTable, tablePath)\n      }\n    }\n  }\n\n  test(\"don't delete data in a non-reservoir\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateFile(\"file1.txt\", commitToActionLog = false),\n        CreateDirectory(\"abc\"),\n        ExpectFailure(\n          GC(dryRun = false, Nil), classOf[IllegalArgumentException], Seq(\"no state defined\"))\n      )\n    }\n  }\n\n  test(\"invisible files and dirs\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateFile(\"file1.txt\", commitToActionLog = true),\n        CreateFile(\"_hidden_dir/000001.text\", commitToActionLog = false),\n        CreateFile(\".hidden.txt\", commitToActionLog = false),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\n          \"file1.txt\", \"_delta_log\", \"_hidden_dir\", \"_hidden_dir/000001.text\", \".hidden.txt\"))\n      )\n    }\n  }\n\n  test(\"partition column name starting with underscore\") {\n    // We should be able to see inside partition directories to GC them, even if they'd normally\n    // be considered invisible because of their name.\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      val txn = table.startTransaction()\n      val schema = new StructType().add(\"_underscore_col_\", IntegerType).add(\"n\", IntegerType)\n      val metadata =\n        Metadata(schemaString = schema.json, partitionColumns = Seq(\"_underscore_col_\"))\n      val version = txn.commitActions(\n        DeltaOperations.CreateTable(metadata, isManaged = true), metadata)\n      setCommitClock(table, version, clock)\n      gcTest(table, clock)(\n        CreateFile(\"file1.txt\", commitToActionLog = true, Map(\"_underscore_col_\" -> \"10\")),\n        CreateFile(\"_underscore_col_=10/test.txt\", true, Map(\"_underscore_col_\" -> \"10\")),\n        CheckFiles(Seq(\"file1.txt\", \"_underscore_col_=10\")),\n        LogicallyDeleteFile(\"_underscore_col_=10/test.txt\"),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\"file1.txt\")),\n        CheckFiles(Seq(\"_underscore_col_=10/test.txt\"), exist = false)\n      )\n    }\n  }\n\n  test(\"schema validation for vacuum by using inventory dataframe\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      val txn = table.startTransaction()\n      val schema = new StructType().add(\"_underscore_col_\", IntegerType).add(\"n\", IntegerType)\n      val metadata =\n        Metadata(schemaString = schema.json, partitionColumns = Seq(\"_underscore_col_\"))\n      val version = txn.commitActions(\n        DeltaOperations.CreateTable(metadata, isManaged = true), metadata)\n      setCommitClock(table, version, clock)\n      val inventorySchema = StructType(\n        Seq(\n          StructField(\"file\", StringType),\n          StructField(\"size\", LongType),\n          StructField(\"isDir\", BooleanType),\n          StructField(\"modificationTime\", LongType)\n        ))\n      val inventory = spark.createDataFrame(\n        spark.sparkContext.parallelize(Seq.empty[Row]), inventorySchema)\n      gcTest(table, clock)(\n        ExpectFailure(\n          GCByInventory(dryRun = false, expectedDf = Seq(tempDir), inventory = Some(inventory)),\n          classOf[DeltaAnalysisException],\n          Seq( \"The schema for the specified INVENTORY\",\n            \"does not contain all of the required fields.\",\n            \"Required fields are:\",\n            s\"${VacuumCommand.INVENTORY_SCHEMA.treeString}\")\n        )\n      )\n    }\n  }\n\n  test(\"run vacuum by using inventory dataframe\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      val txn = table.startTransaction()\n      val schema = new StructType().add(\"_underscore_col_\", IntegerType).add(\"n\", IntegerType)\n\n      // Vacuum should consider partition folders even for clean up even though it starts with `_`\n      val metadata =\n        Metadata(schemaString = schema.json, partitionColumns = Seq(\"_underscore_col_\"))\n      val version = txn.commitActions(\n        DeltaOperations.CreateTable(metadata, isManaged = true), metadata)\n      setCommitClock(table, version, clock)\n      val dataPath = table.deltaLog.dataPath\n      // Create a Seq of Rows containing the data\n      val data = Seq(\n        Row(s\"${dataPath}\", 300000L, true, 0L),\n        Row(s\"${dataPath}/file1.txt\", 300000L, false, 0L),\n        Row(s\"${dataPath}/file2.txt\", 300000L, false, 0L),\n        Row(s\"${dataPath}/_underscore_col_=10/test.txt\", 300000L, false, 0L),\n        Row(s\"${dataPath}/_underscore_col_=10/test2.txt\", 300000L, false, 0L),\n        // Below file is not within Delta table path and should be ignored by vacuum\n        Row(s\"/tmp/random/_underscore_col_=10/test2.txt\", 300000L, false, 0L),\n        // Below are Delta table root location and vacuum must safely handle them\n        Row(s\"${dataPath}\", 300000L, true, 0L)\n      )\n      val inventory = spark.createDataFrame(spark.sparkContext.parallelize(data),\n        VacuumCommand.INVENTORY_SCHEMA)\n      gcTest(table, clock)(\n        CreateFile(\"file1.txt\", commitToActionLog = true, Map(\"_underscore_col_\" -> \"10\")),\n        CreateFile(\"file2.txt\", commitToActionLog = false, Map(\"_underscore_col_\" -> \"10\")),\n        CreateFile(\"_underscore_col_=10/test.txt\", true, Map(\"_underscore_col_\" -> \"10\")),\n        CreateFile(\"_underscore_col_=10/test2.txt\", false, Map(\"_underscore_col_\" -> \"10\")),\n        CheckFiles(Seq(\"file1.txt\", \"_underscore_col_=10\", \"file2.txt\")),\n        LogicallyDeleteFile(\"_underscore_col_=10/test.txt\"),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GCByInventory(dryRun = true, expectedDf = Seq(\n          s\"${dataPath}/file2.txt\",\n          s\"${dataPath}/_underscore_col_=10/test.txt\",\n          s\"${dataPath}/_underscore_col_=10/test2.txt\"\n        ), inventory = Some(inventory)),\n        GCByInventory(dryRun = false, expectedDf = Seq(tempDir), inventory = Some(inventory)),\n        CheckFiles(Seq(\"file1.txt\")),\n        CheckFiles(Seq(\"file2.txt\", \"_underscore_col_=10/test.txt\",\n          \"_underscore_col_=10/test2.txt\"), exist = false)\n      )\n    }\n  }\n\n  test(\"vacuum using inventory delta table and should not touch hidden files\") {\n    withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n      withEnvironment { (tempDir, clock) =>\n        import testImplicits._\n        val path = s\"\"\"${tempDir.getCanonicalPath}_data\"\"\"\n        val inventoryPath = s\"\"\"${tempDir.getCanonicalPath}_inventory\"\"\"\n\n        // Define test delta table\n        val data = Seq(\n          (10, 1, \"a\"),\n          (10, 2, \"a\"),\n          (10, 3, \"a\"),\n          (10, 4, \"a\"),\n          (10, 5, \"a\")\n        )\n        data.toDF(\"v1\", \"v2\", \"v3\")\n          .write\n          .partitionBy(\"v1\", \"v2\")\n          .format(\"delta\")\n          .save(path)\n        val table = DeltaTableV2(spark, new File(path), clock)\n        val dataPath = table.deltaLog.dataPath\n        val reservoirData = Seq(\n          Row(s\"${dataPath}/file1.txt\", 300000L, false, 0L),\n          Row(s\"${dataPath}/file2.txt\", 300000L, false, 0L),\n          Row(s\"${dataPath}/_underscore_col_=10/test.txt\", 300000L, false, 0L),\n          Row(s\"${dataPath}/_underscore_col_=10/test2.txt\", 300000L, false, 0L)\n        )\n        spark.createDataFrame(\n          spark.sparkContext.parallelize(reservoirData), VacuumCommand.INVENTORY_SCHEMA)\n          .write\n          .format(\"delta\")\n          .save(inventoryPath)\n        gcTest(table, clock)(\n          CreateFile(\"file1.txt\", commitToActionLog = false),\n          CreateFile(\"file2.txt\", commitToActionLog = false),\n          // Delta marks dirs starting with `_` as hidden unless specified as partition folder\n          CreateFile(\"_underscore_col_=10/test.txt\", false),\n          CreateFile(\"_underscore_col_=10/test2.txt\", false),\n          AdvanceClock(defaultTombstoneInterval + 1000)\n        )\n        sql(s\"vacuum delta.`$path` using inventory delta.`$inventoryPath` retain 0 hours\")\n        gcTest(table, clock)(\n          CheckFiles(Seq(\"file1.txt\", \"file2.txt\"), exist = false),\n          // hidden files must not be dropped\n          CheckFiles(Seq(\"_underscore_col_=10/test.txt\", \"_underscore_col_=10/test2.txt\"))\n        )\n      }\n    }\n  }\n\n  test(\"vacuum using inventory query and should not touch hidden files\") {\n    withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n      withEnvironment { (tempDir, clock) =>\n        import testImplicits._\n        val path = s\"\"\"${tempDir.getCanonicalPath}_data\"\"\"\n        val reservoirPath = s\"\"\"${tempDir.getCanonicalPath}_reservoir\"\"\"\n\n        // Define test delta table\n        val data = Seq(\n          (10, 1, \"a\"),\n          (10, 2, \"a\"),\n          (10, 3, \"a\"),\n          (10, 4, \"a\"),\n          (10, 5, \"a\")\n        )\n        data.toDF(\"v1\", \"v2\", \"v3\")\n          .write\n          .partitionBy(\"v1\", \"v2\")\n          .format(\"delta\")\n          .save(path)\n        val table = DeltaTableV2(spark, new File(path), clock)\n        val dataPath = table.deltaLog.dataPath\n        val reservoirData = Seq(\n          Row(s\"${dataPath}/file1.txt\", 300000L, false, 0L),\n          Row(s\"${dataPath}/file2.txt\", 300000L, false, 0L),\n          Row(s\"${dataPath}/_underscore_col_=10/test.txt\", 300000L, false, 0L),\n          Row(s\"${dataPath}/_underscore_col_=10/test2.txt\", 300000L, false, 0L)\n        )\n        spark.createDataFrame(\n          spark.sparkContext.parallelize(reservoirData), VacuumCommand.INVENTORY_SCHEMA)\n          .write\n          .format(\"delta\")\n          .save(reservoirPath)\n        gcTest(table, clock)(\n          CreateFile(\"file1.txt\", commitToActionLog = false),\n          CreateFile(\"file2.txt\", commitToActionLog = false),\n          // Delta marks dirs starting with `_` as hidden unless specified as partition folder\n          CreateFile(\"_underscore_col_=10/test.txt\", false),\n          CreateFile(\"_underscore_col_=10/test2.txt\", false)\n        )\n        sql(s\"\"\"vacuum delta.`$path`\n             |using inventory (select * from delta.`$reservoirPath`)\n             |retain 0 hours\"\"\".stripMargin)\n        gcTest(table, clock)(\n          AdvanceClock(defaultTombstoneInterval + 1000),\n          CheckFiles(Seq(\"file1.txt\", \"file2.txt\"), exist = false),\n          // hidden files must not be dropped\n          CheckFiles(Seq(\"_underscore_col_=10/test.txt\", \"_underscore_col_=10/test2.txt\"))\n        )\n      }\n    }\n  }\n\n  // Since lite vacuum uses delta log, it doesn't delete empty directories.\n  testFullVacuumOnly(\"multiple levels of empty directory deletion\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateFile(\"file1.txt\", commitToActionLog = true),\n        CreateFile(\"abc/def/file2.txt\", commitToActionLog = false),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/def\")),\n        CheckFiles(Seq(\"abc/def/file2.txt\"), exist = false),\n        GC(dryRun = false, Seq(tempDir)),\n        // we need two GCs to guarantee the deletion of the directories\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\"file1.txt\")),\n        CheckFiles(Seq(\"abc\", \"abc/def\"), exist = false)\n      )\n    }\n  }\n\n  test(\"gc doesn't delete base path\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateFile(\"file1.txt\", commitToActionLog = true),\n        AdvanceClock(100),\n        LogicallyDeleteFile(\"file1.txt\"),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(tempDir.toString)),\n        CheckFiles(Seq(\"file1.txt\"), exist = false),\n        GC(dryRun = false, Seq(tempDir.toString)) // shouldn't throw an error\n      )\n    }\n  }\n\n  testQuietly(\"correctness test\") {\n    withEnvironment { (tempDir, clock) =>\n\n      val reservoirDir = new File(tempDir.getAbsolutePath, \"reservoir\")\n      assert(reservoirDir.mkdirs())\n      val externalDir = new File(tempDir.getAbsolutePath, \"external\")\n      assert(externalDir.mkdirs())\n      val table = DeltaTableV2(spark, reservoirDir, clock)\n\n      val externalFile = new File(externalDir, \"file4.txt\").getAbsolutePath\n\n      gcTest(table, clock)(\n        // Create initial state\n        CreateFile(\"file1.txt\", commitToActionLog = true),\n        CreateDirectory(\"abc\"),\n        CreateFile(\"abc/file2.txt\", commitToActionLog = true),\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/file2.txt\")),\n\n        // Nothing should be deleted here, since we didn't logically delete any file\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/file2.txt\")),\n\n        // Create an untracked file\n        CreateFile(\"file3.txt\", commitToActionLog = false),\n        CheckFiles(Seq(\"file3.txt\")),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file3.txt\")),\n        AdvanceClock(defaultTombstoneInterval - 1000), // file is still new\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file3.txt\")),\n        AdvanceClock(2000),\n        // Since file3.txt is not committed, it's not tracked by lite vacuum.\n        GC(dryRun = true,\n          Option.when(!isLiteVacuum)(new File(reservoirDir, \"file3.txt\").toString).toSeq),\n        // nothing should be deleted\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/file2.txt\", \"file3.txt\")),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/file2.txt\")),\n        // Since file3.txt is not committed, it would be deleted only if it's non-lite-vacuum\n        CheckFiles(Seq(\"file3.txt\"), exist = isLiteVacuum),\n\n        // Verify tombstones\n        LogicallyDeleteFile(\"abc/file2.txt\"),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/file2.txt\")),\n        AdvanceClock(defaultTombstoneInterval - 1000),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\", \"abc\", \"abc/file2.txt\")),\n        AdvanceClock(2000), // tombstone should expire\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\", \"abc\")),\n        CheckFiles(Seq(\"abc/file2.txt\"), exist = false),\n        // Second gc should clear empty directory if it's not lite vacuum\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\")),\n        CheckFiles(Seq(\"abc\"), exist = isLiteVacuum),\n\n        // Make sure that files outside the reservoir are not affected\n        CreateFile(externalFile, commitToActionLog = true),\n        AdvanceClock(100),\n        CheckFiles(Seq(\"file1.txt\", externalFile)),\n        LogicallyDeleteFile(externalFile),\n        AdvanceClock(defaultTombstoneInterval * 2),\n        CheckFiles(Seq(\"file1.txt\", externalFile))\n      )\n    }\n  }\n\n  test(\"parallel file delete\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      withSQLConf(\"spark.databricks.delta.vacuum.parallelDelete.enabled\" -> \"true\") {\n        gcTest(table, clock)(\n          CreateFile(\"file1.txt\", commitToActionLog = true),\n          CreateFile(\"file2.txt\", commitToActionLog = true),\n          LogicallyDeleteFile(\"file1.txt\"),\n          CheckFiles(Seq(\"file1.txt\", \"file2.txt\")),\n          AdvanceClock(defaultTombstoneInterval + 1000),\n          GC(dryRun = false, Seq(tempDir)),\n          CheckFiles(Seq(\"file1.txt\"), exist = false),\n          CheckFiles(Seq(\"file2.txt\")),\n          GC(dryRun = false, Seq(tempDir)), // shouldn't throw an error with no files to delete\n          CheckFiles(Seq(\"file2.txt\"))\n        )\n      }\n    }\n  }\n\n  test(\"retention duration must be greater than 0\") {\n    withSQLConf(\"spark.databricks.delta.vacuum.retentionWindowIgnore.enabled\" -> \"false\") {\n      withEnvironment { (tempDir, clock) =>\n        val table = DeltaTableV2(spark, tempDir, clock)\n        gcTest(table, clock)(\n          CreateFile(\"file1.txt\", commitToActionLog = true),\n          CheckFiles(Seq(\"file1.txt\")),\n          ExpectFailure(\n            GC(false, Seq(tempDir), Some(-2)),\n            classOf[DeltaIllegalArgumentException],\n            Seq(\"Retention\", \"less than\", \"0\"))\n        )\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n        gcTest(table, clock)(\n          CreateFile(\"file2.txt\", commitToActionLog = true),\n          CheckFiles(Seq(\"file2.txt\")),\n          ExpectFailure(\n            ExecuteVacuumInScala(deltaTable, Seq(), Some(-2)),\n            classOf[DeltaIllegalArgumentException],\n            Seq(\"Retention\", \"less than\", \"0\"))\n        )\n      }\n    }\n  }\n\n  test(\"deleting directories\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateFile(\"abc/def/file1.txt\", commitToActionLog = true),\n        CreateFile(\"abc/def/file2.txt\", commitToActionLog = true),\n        CreateDirectory(\"ghi\"),\n        CheckFiles(Seq(\"abc\", \"abc/def\", \"ghi\")),\n        // Since \"ghi\" is a empty directory not tracked by the delta log,\n        // lite Vacuum won't delete it.\n        GC(dryRun = true, Option.when(!isLiteVacuum)(new File(tempDir, \"ghi\").toString).toSeq),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\"abc\", \"abc/def\")),\n        CheckFiles(Seq(\"ghi\"), exist = isLiteVacuum)\n      )\n    }\n  }\n\n  test(\"deleting files with special characters in path\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      // Non committed files are not deleted by lite vacuum.\n      val expected = Option.when(!isLiteVacuum)(new File(tempDir, \"abc def/#1/file2.txt\").toString)\n      gcTest(table, clock)(\n        CreateFile(\"abc def/#1/file1.txt\", commitToActionLog = true),\n        CreateFile(\"abc def/#1/file2.txt\", commitToActionLog = false),\n        CheckFiles(Seq(\"abc def\", \"abc def/#1\")),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = true, expected.toSeq),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\"abc def/#1\", \"abc def/#1/file1.txt\")),\n        CheckFiles(Seq(\"abc def/#1/file2.txt\"), exist = isLiteVacuum)\n      )\n    }\n  }\n\n  testQuietly(\"additional retention duration check with vacuum command\") {\n    withEnvironment { (tempDir, clock) =>\n      val table = DeltaTableV2(spark, tempDir, clock)\n      withSQLConf(\"spark.databricks.delta.retentionDurationCheck.enabled\" -> \"true\") {\n        gcTest(table, clock)(\n          CreateFile(\"file1.txt\", commitToActionLog = true),\n          CheckFiles(Seq(\"file1.txt\")),\n          ExpectFailure(\n            GC(false, Nil, Some(0)),\n            classOf[DeltaIllegalArgumentException],\n            Seq(\"delta.retentionDurationCheck.enabled = false\", \"168 hours\"))\n        )\n      }\n\n      gcTest(table, clock)(\n        CreateFile(\"file2.txt\", commitToActionLog = true),\n        CheckFiles(Seq(\"file2.txt\")),\n        GC(false, Seq(tempDir.toString), Some(0))\n      )\n    }\n  }\n\n  test(\"vacuum for a partition path\") {\n    withEnvironment { (tempDir, _) =>\n      import testImplicits._\n      val path = tempDir.getCanonicalPath\n      Seq((1, \"a\"), (2, \"b\")).toDF(\"v1\", \"v2\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"v2\")\n        .save(path)\n\n      val ex = intercept[AnalysisException] {\n        sql(s\"vacuum '$path/v2=a' retain 0 hours\")\n      }\n      assert(\n        ex.getMessage.contains(\n          s\"`$path/v2=a` is not a Delta table. VACUUM is only supported for Delta tables.\"))\n    }\n  }\n\n  test(s\"vacuum table with DVs and zero retention policy throws exception by default\") {\n    val targetDF = spark.range(0, 100, 1, 2)\n      .withColumn(\"value\", col(\"id\"))\n\n      withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n        // Add some DVs.\n        targetTable().delete(\"id < 10\")\n        val e = intercept[DeltaIllegalArgumentException] {\n          spark.sql(s\"VACUUM delta.`${targetLog.dataPath}` RETAIN 0 HOURS\")\n        }\n        assert(e.getMessage.contains(\n          \"The specified VACUUM retention period is too low\"))\n      }\n  }\n\n  test(s\"vacuum after purge with zero retention policy\") {\n    val tableName = \"testTable\"\n    withDeletionVectorsEnabled() {\n      withSQLConf(\n          DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n        withTable(tableName) {\n          // Create a Delta Table with 5 files of 10 rows, and delete half rows from first 4 files.\n          spark.range(0, 50, step = 1, numPartitions = 5)\n            .write.format(\"delta\").saveAsTable(tableName)\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          spark.sql(s\"DELETE from $tableName WHERE ID % 2 = 0 and ID < 40\")\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 4, dvFiles = 1, dataFiles = 5)\n\n          purgeDVs(tableName)\n\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 1, dataFiles = 9)\n          spark.sql(s\"VACUUM $tableName RETAIN 0 HOURS\")\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 0, dataFiles = 5)\n\n          checkAnswer(\n            spark.read.table(tableName),\n            Seq.range(0, 50).filterNot(x => x < 40 && x % 2 == 0).toDF)\n        }\n      }\n    }\n  }\n\n\n  // Since lite vacuum uses delta log, it doesn't delete uniform metadata directories\n  // as they are not reachable through delta log.\n  testFullVacuumOnly(\"gc metadata dir when uniform disabled\") {\n    withEnvironment { (tempDir, clock) =>\n      spark.emptyDataset[Int].write.format(\"delta\").save(tempDir)\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateDirectory(\"metadata\"),\n        CreateFile(\"metadata/file1.json\", false),\n\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\"metadata/file1.json\"), exist = false),\n        GC(dryRun = false, Seq(tempDir)), // Second GC clears empty dir\n        CheckFiles(Seq(\"metadata\"), exist = false)\n      )\n    }\n  }\n\n  test(\"hudi metadata dir\") {\n    withEnvironment { (tempDir, clock) =>\n      spark.emptyDataset[Int].write.format(\"delta\").save(tempDir)\n      val table = DeltaTableV2(spark, tempDir, clock)\n      gcTest(table, clock)(\n        CreateDirectory(\".hoodie\"),\n        CreateFile(\".hoodie/00001.commit\", false),\n\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = false, Seq(tempDir)),\n        CheckFiles(Seq(\".hoodie\", \".hoodie/00001.commit\"))\n      )\n    }\n  }\n\n  // Helper method to remove the DVs in Delta table and rewrite the data files\n  def purgeDVs(tableName: String): Unit = {\n    withSQLConf(\n      // Set the max file size to low so that we always rewrite the single file without DVs\n      // and not combining with other data files.\n      DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> \"2\") {\n      spark.sql(s\"REORG TABLE $tableName APPLY (PURGE)\")\n    }\n  }\n\n  test(s\"vacuum after purging deletion vectors\") {\n    import org.apache.spark.sql.delta.test.DeltaTestImplicits.DeltaTableV2ObjectTestHelper\n    val tableName = \"testTable\"\n    withDeletionVectorsEnabled() {\n      withSQLConf(\n          DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\",\n          // Disable the following check since the test relies on time travel beyond\n          // deletedFileRetentionDuration.\n          DeltaSQLConf.ENFORCE_TIME_TRAVEL_WITHIN_DELETED_FILE_RETENTION_DURATION.key -> \"false\") {\n        withTable(tableName) {\n          // Create Delta table with 5 files of 10 rows.\n          spark.range(0, 50, step = 1, numPartitions = 5)\n            .write\n            .format(\"delta\")\n            .option(\"delta.deletedFileRetentionDuration\", \"interval 1 hours\")\n            .saveAsTable(tableName)\n          // The following is done to ensure deltaLog object uses the same clock that Vacuum\n          // logic uses.\n          val deltaLogThrowaway = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          val tablePath = deltaLogThrowaway.dataPath\n          DeltaLog.clearCache()\n          val clock = new ManualClock(System.currentTimeMillis())\n          val deltaLog = DeltaLog.forTable(spark, tablePath, clock)\n          val deltaTable = DeltaTableV2(spark, TableIdentifier(tableName))\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 0, dataFiles = 5)\n\n          // Delete 1 row from each file. DVs will be packed to one DV file.\n          val deletedRows1 = Seq(0, 10, 20, 30, 40)\n          val deletedRowsStr1 = deletedRows1.mkString(\"(\", \",\", \")\")\n          spark.sql(s\"DELETE FROM $tableName WHERE id IN $deletedRowsStr1\")\n          val snapshotV1 = deltaTable.update()\n          // We retrieve both timestamp and file modification time b/c when ICT is enabled,\n          // timestamp represents ICT instead of file modification time. Lite vacuum relies on\n          // both the ICT and the file modification time to determine cleanup behavior.\n          val timestampV1 = snapshotV1.timestamp\n          val fileModificationTimeV1 = snapshotV1.logSegment.lastCommitFileModificationTimestamp\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 5, dvFiles = 1, dataFiles = 5)\n\n          // Delete all rows from the first file. An ephemeral DV will still be created.\n          // We need to add 1000 ms for local filesystems that only write modificationTimes to the\n          // second precision.\n          Thread.sleep(1000) // Ensure it's been at least 1000 ms since V1\n          // Assign clock to the current system time so that the ICT falls within\n          // (fileModificationTimeV(X-1) + 1000, fileModificationTimeV(X+1)]. This ensures we can\n          // later manually adjust the clock time to test both full and lite vacuum behavior.\n          clock.setTime(System.currentTimeMillis())\n          spark.sql(s\"DELETE FROM $tableName WHERE id < 10\")\n          val snapshotV2 = deltaTable.update()\n          val timestampV2 = snapshotV2.timestamp\n          val fileModificationTimeV2 = snapshotV2.logSegment.lastCommitFileModificationTimestamp\n          assertNumFiles(deltaLog, addFiles = 4, addFilesWithDVs = 4, dvFiles = 2, dataFiles = 5)\n          val expectedAnswerV2 = Seq.range(0, 50).filterNot(deletedRows1.contains).filterNot(_ < 10)\n\n          // Delete 1 more row from each file.\n          Thread.sleep(1000) // Ensure it's been at least 1000 ms since V2\n          clock.setTime(System.currentTimeMillis())\n          val deletedRows2 = Seq(11, 21, 31, 41)\n          val deletedRowsStr2 = deletedRows2.mkString(\"(\", \",\", \")\")\n          spark.sql(s\"DELETE FROM $tableName WHERE id IN $deletedRowsStr2\")\n          val snapshotV3 = deltaTable.update()\n          val timestampV3 = snapshotV3.timestamp\n          val fileModificationTimeV3 = snapshotV3.logSegment.lastCommitFileModificationTimestamp\n          assertNumFiles(deltaLog, addFiles = 4, addFilesWithDVs = 4, dvFiles = 3, dataFiles = 5)\n          val expectedAnswerV3 = expectedAnswerV2.filterNot(deletedRows2.contains)\n\n          // Delete DVs by rewriting the data files with DVs.\n          Thread.sleep(1000) // Ensure it's been at least 1000 ms since V3\n          clock.setTime(System.currentTimeMillis())\n          purgeDVs(tableName)\n\n          val numFilesAfterPurge = 4\n          val snapshotV4 = deltaTable.update()\n          val timestampV4 = snapshotV4.timestamp\n          val fileModificationTimeV4 = snapshotV4.logSegment.lastCommitFileModificationTimestamp\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0, dvFiles = 3,\n            dataFiles = 9)\n\n          // Run VACUUM with nothing expired. It should not delete anything.\n          clock.setTime(System.currentTimeMillis())\n          VacuumCommand.gc(\n            spark, deltaTable, retentionHours = Some(1), clock = clock, dryRun = false)\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0, dvFiles = 3,\n            dataFiles = 9)\n\n          val oneHour = TimeUnit.HOURS.toMillis(1)\n          // Run VACUUM @ V1.\n          // The clock time must be set such that: (X is the version where we run VACUUM)\n          // 1. (clock time - retention time) falls within\n          //    (fileModificationTimeV(X), fileModificationTimeV(X+1)] for both lite and full\n          //    vacuum, since both use file modification time to determine if the files are valid\n          //    for cleanup.\n          // 2. (clock time - retention time) falls within [timestampV(X), timestampV(X+1)) for\n          //    lite vacuum, since it uses [[DeltaHistoryManager(deltaLog).getActiveCommitAtTime]]\n          //    which depends on timestamp to capture files of commit-X as candidates for cleanup.\n          clock.setTime(Math.max(fileModificationTimeV1 + 1, timestampV1) + oneHour)\n          VacuumCommand.gc(\n            spark, deltaTable, retentionHours = Some(1), clock = clock, dryRun = false)\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0, dvFiles = 3,\n            dataFiles = 9)\n\n          // Run VACUUM @ V2. It should delete the ephemeral DV and the removed Parquet file.\n          // Since ephemeral DV is not GC'ed by Lite Vacuum, the number of DVs we expect will be\n          // one more in case of lite Vacuum\n          val numDVstoAdd = if (isLiteVacuum) 1 else 0\n          clock.setTime(Math.max(fileModificationTimeV2 + 1, timestampV2) + oneHour)\n          VacuumCommand.gc(\n            spark, deltaTable, retentionHours = Some(1), clock = clock, dryRun = false)\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0,\n            dvFiles = 2 + numDVstoAdd, dataFiles = 8)\n          checkAnswer(\n            spark.sql(s\"SELECT * FROM $tableName VERSION AS OF 2\"), expectedAnswerV2.toDF)\n\n          // Run VACUUM @ V3. It should delete the persistent DVs from V1.\n          clock.setTime(Math.max(fileModificationTimeV3 + 1, timestampV3) + oneHour)\n          VacuumCommand.gc(\n            spark, deltaTable, retentionHours = Some(1), clock = clock, dryRun = false)\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0,\n            dvFiles = 1 + numDVstoAdd, dataFiles = 8)\n          checkAnswer(\n            spark.sql(s\"SELECT * FROM $tableName VERSION AS OF 3\"), expectedAnswerV3.toDF)\n\n          // Run VACUUM @ V4. It should delete the Parquet files and DVs of V3.\n          clock.setTime(Math.max(fileModificationTimeV4 + 1, timestampV4) + oneHour)\n          VacuumCommand.gc(\n            spark, deltaTable, retentionHours = Some(1), clock = clock, dryRun = false)\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0,\n            dvFiles = 0 + numDVstoAdd, dataFiles = 4)\n          checkAnswer(\n            spark.sql(s\"SELECT * FROM $tableName VERSION AS OF 4\"), expectedAnswerV3.toDF)\n\n          // Run VACUUM with zero retention period. It should not delete anything.\n          clock.setTime(Math.max(fileModificationTimeV4 + 1, timestampV4) + oneHour)\n          VacuumCommand.gc(\n            spark, deltaTable, retentionHours = Some(0), clock = clock, dryRun = false)\n          assertNumFiles(deltaLog, addFiles = numFilesAfterPurge, addFilesWithDVs = 0,\n            dvFiles = 0 + numDVstoAdd, dataFiles = 4)\n\n          // Last version should still be readable.\n          checkAnswer(spark.sql(s\"SELECT * FROM $tableName\"), expectedAnswerV3.toDF)\n        }\n      }\n    }\n  }\n\n  for (partitioned <- DeltaTestUtils.BOOLEAN_DOMAIN) {\n    test(s\"delete persistent deletion vectors - partitioned = $partitioned\") {\n      val targetDF = spark.range(0, 100, 1, 10).toDF\n        .withColumn(\"v\", col(\"id\"))\n        .withColumn(\"partCol\", lit(0))\n      val partitionBy = if (partitioned) Seq(\"partCol\") else Seq.empty\n      withSQLConf(\n          DeltaSQLConf.DELETION_VECTOR_PACKING_TARGET_SIZE.key -> \"0\",\n          DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n        withDeletionVectorsEnabled() {\n          withTempDeltaTable(\n              targetDF,\n              partitionBy = partitionBy) { (targetTable, targetLog) =>\n            val targetDir = targetLog.dataPath\n\n            // Add a DV to all files and check that DVs are not deleted.\n            targetTable().delete(\"id % 2 == 0\")\n\n            assert(listDeletionVectors(targetLog).size == 10)\n            targetTable().vacuum(0)\n            assert(listDeletionVectors(targetLog).size == 10)\n            checkAnswer(sql(s\"select count(*) from delta.`$targetDir`\"), Row(50))\n\n            // Update the DV of the first file by deleting two rows and check that previous DV is\n            // deleted.\n            targetTable().delete(\"id  < 10 AND id % 3 == 0\")\n\n            assert(listDeletionVectors(targetLog).size == 11)\n            targetTable().vacuum(0)\n            assert(listDeletionVectors(targetLog).size == 10)\n            checkAnswer(sql(s\"select count(*) from delta.`$targetDir`\"), Row(48))\n\n            // Delete all rows in first 5 files and check that DVs are not deleted due to\n            // the retention period, but deleted after that.\n            // with lite vacuum ephemeral dvs are not going to be GC'ed. So, the dvs we expect\n            // will be 5 more for lite vacuum\n            val dvsToAdd = if (isLiteVacuum) 5 else 0\n            targetTable().delete(\"id < 50\")\n\n            assert(listDeletionVectors(targetLog).size == 15)\n            targetTable().vacuum(10)\n            assert(listDeletionVectors(targetLog).size == 15)\n            targetTable().vacuum(0)\n            assert(listDeletionVectors(targetLog).size == 5 + dvsToAdd)\n            checkAnswer(sql(s\"select count(*) from delta.`$targetDir`\"), Row(25))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"vacuum a non-existent path and a non Delta table\") {\n    def assertNotADeltaTableException(path: String): Unit = {\n      for (table <- Seq(s\"'$path'\", s\"delta.`$path`\")) {\n        val e = intercept[AnalysisException] {\n          sql(s\"vacuum $table\")\n        }\n        assert(e.getMessage.contains(\"is not a Delta table.\"))\n      }\n    }\n    withTempPath { tempDir =>\n      assert(!tempDir.exists())\n      assertNotADeltaTableException(tempDir.getCanonicalPath)\n    }\n    withTempPath { tempDir =>\n      spark.range(1, 10).write.parquet(tempDir.getCanonicalPath)\n      assertNotADeltaTableException(tempDir.getCanonicalPath)\n    }\n  }\n\n  test(\"vacuum for cdc - update/merge\") {\n    testCDCVacuumForUpdateMerge()\n  }\n\n  test(\"vacuum for cdc - delete tombstones\") {\n    testCDCVacuumForTombstones()\n  }\n\n  private def getFromHistory(history: DataFrame, key: String, pos: Integer): Map[String, String] = {\n    val op = history.select(key).take(pos + 1)\n    if (pos == 0) {\n      op.head.getMap(0).asInstanceOf[Map[String, String]]\n    } else {\n      op.tail.head.getMap(0).asInstanceOf[Map[String, String]]\n    }\n  }\n\n  private def testEventLogging(\n      isDryRun: Boolean,\n      loggingEnabled: Boolean,\n      retentionHours: Long,\n      timeGapHours: Long): Unit = {\n\n    test(s\"vacuum event logging dryRun=$isDryRun loggingEnabled=$loggingEnabled\" +\n      s\" retentionHours=$retentionHours timeGap=$timeGapHours\") {\n      withSQLConf(DeltaSQLConf.DELTA_VACUUM_LOGGING_ENABLED.key -> loggingEnabled.toString) {\n        withEnvironment { (dir, clock) =>\n          clock.setTime(System.currentTimeMillis())\n          spark\n            .range(2)\n            .write\n            .format(\"delta\")\n            .option(\"delta.deletedFileRetentionDuration\", s\"interval $retentionHours hours\")\n            .save(dir.getAbsolutePath)\n          // The following is done to ensure deltaLog object uses the same clock that Vacuum\n          // logic uses.\n          DeltaLog.clearCache()\n          val table = DeltaTableV2(spark, dir, clock)\n\n          setCommitClock(table, 0L, clock)\n          val expectedReturn = if (isDryRun) {\n            // dry run returns files that will be deleted\n            Seq(new Path(dir.getAbsolutePath, \"file1.txt\").toString)\n          } else {\n            Seq(dir.getAbsolutePath)\n          }\n\n          gcTest(table, clock)(\n            CreateFile(\"file1.txt\", commitToActionLog = true),\n            CreateFile(\"file2.txt\", commitToActionLog = true),\n            LogicallyDeleteFile(\"file1.txt\"),\n            AdvanceClock(timeGapHours * 1000 * 60 * 60),\n            GC(dryRun = isDryRun, expectedReturn, Some(retentionHours))\n          )\n          val deltaTable = io.delta.tables.DeltaTable.forPath(table.deltaLog.dataPath.toString)\n          val history = deltaTable.history()\n          if (isDryRun || !loggingEnabled) {\n            // We do not record stats when logging is disabled or dryRun\n            assert(history.select(\"operation\").head() == Row(\"DELETE\"))\n          } else {\n            assert(history.select(\"operation\").head() == Row(\"VACUUM END\"))\n            assert(history.select(\"operation\").collect()(1) == Row(\"VACUUM START\"))\n\n            val operationParamsBegin = getFromHistory(history, \"operationParameters\", 1)\n            val operationParamsEnd = getFromHistory(history, \"operationParameters\", 0)\n            val operationMetricsBegin = getFromHistory(history, \"operationMetrics\", 1)\n            val operationMetricsEnd = getFromHistory(history, \"operationMetrics\", 0)\n\n            val filesDeleted = if (retentionHours > timeGapHours) { 0 } else { 1 }\n            assert(operationParamsBegin(\"retentionCheckEnabled\") === \"false\")\n            assert(operationMetricsBegin(\"numFilesToDelete\") === filesDeleted.toString)\n            assert(operationMetricsBegin(\"sizeOfDataToDelete\") === (filesDeleted * 9).toString)\n\n            if (retentionHours == 0) {\n              assert(\n                operationParamsBegin(\"specifiedRetentionMillis\") ===\n                  (retentionHours * 60 * 60 * 1000).toString)\n            }\n            assert(\n              operationParamsBegin(\"defaultRetentionMillis\") ===\n                DeltaLog.tombstoneRetentionMillis(table.initialSnapshot.metadata).toString)\n\n            assert(operationParamsEnd === Map(\"status\" -> \"COMPLETED\"))\n            assert(operationMetricsEnd === Map(\"numDeletedFiles\" -> filesDeleted.toString,\n              \"numVacuumedDirectories\" -> \"1\"))\n          }\n        }\n      }\n    }\n  }\n\n  testEventLogging(\n    isDryRun = false,\n    loggingEnabled = true,\n    retentionHours = 0,\n    timeGapHours = 10\n  )\n\n  testEventLogging(\n    isDryRun = true, // dry run will not record the vacuum\n    loggingEnabled = true,\n    retentionHours = 5,\n    timeGapHours = 10\n  )\n\n  testEventLogging(\n    isDryRun = false,\n    loggingEnabled = false,\n    retentionHours = 5,\n    timeGapHours = 0\n  )\n\n  testEventLogging(\n    isDryRun = false,\n    loggingEnabled = true,\n    retentionHours = 20, // vacuum will not delete any files\n    timeGapHours = 10\n  )\n\n  test(s\"vacuum sql syntax checks\") {\n    val tableName = \"testTable\"\n    withTable(tableName) {\n      withDeletionVectorsEnabled() {\n        withSQLConf(\n          DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\",\n          DeltaSQLConf.LITE_VACUUM_ENABLED.key -> \"false\"\n        ) {\n          spark.range(0, 50, step = 1, numPartitions = 5).write.format(\"delta\")\n            .saveAsTable(tableName)\n          var e = intercept[AnalysisException] {\n            spark.sql(s\"Vacuum $tableName DRY RUN DRY RUN\")\n          }\n          assert(e.getMessage.contains(\"Found duplicate clauses: DRY RUN\"))\n\n          e = intercept[AnalysisException] {\n            spark.sql(s\"Vacuum $tableName RETAIN 200 HOURS RETAIN 200 HOURS\")\n          }\n          assert(e.getMessage.contains(\"Found duplicate clauses: RETAIN\"))\n\n          e = intercept[AnalysisException] {\n            spark.sql(s\"Vacuum $tableName FULL LITE\")\n          }\n          assert(e.getMessage.contains(\"Found duplicate clauses: LITE/FULL\"))\n\n          e = intercept[AnalysisException] {\n            spark.sql(s\"Vacuum $tableName USING INVENTORY $tableName INVENTORY $tableName\")\n          }\n          assert(e.getMessage.contains(\"Syntax error at or near\"))\n\n          e = intercept[AnalysisException] {\n            spark.sql(s\"Vacuum $tableName USING INVENTORY $tableName LITE\")\n          }\n          assert(e.getMessage.contains(\"Inventory option is not compatible with LITE\"))\n\n          // create an uncommitted file. Presence or lack of this file will help us\n          // validate that we ran the right type of Vacuum.\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          val basePath = deltaLog.dataPath.toString\n          val clock = new ManualClock()\n          val fs = new Path(basePath).getFileSystem(deltaLog.newDeltaHadoopConf())\n          val sanitizedPath = new Path(\"UnCommittedFile.parquet\").toUri.toString\n          val file = new File(\n            fs.makeQualified(DeltaFileOperations.absolutePath(basePath, sanitizedPath)).toUri)\n          createFile(basePath, sanitizedPath, file, clock)\n\n          spark.sql(s\"DELETE from $tableName WHERE ID % 2 = 0 and ID < 40\")\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 4, dvFiles = 1, dataFiles = 6)\n          purgeDVs(tableName)\n\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 1,\n            dataFiles = 10) // 9 file actions + one  uncommitted file\n\n          spark.sql(s\"Vacuum $tableName LITE DRY RUN RETAIN 0 HOURS\")\n          // DRY RUN option doesn't change anything.\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 1,\n            dataFiles = 10)\n\n          // LITE will be able to GC 4 files removed by DELETE.\n          spark.sql(s\"Vacuum $tableName LITE RETAIN 0 HOURS\")\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 0,\n            dataFiles = 6)\n\n          // Default is full and it's able to delete the 'notCommittedFile.parquet'\n          spark.sql(s\"Vacuum $tableName RETAIN 0 HOURS\")\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 0,\n            dataFiles = 5)\n          // Create the uncommittedFile file again to make sure explicit vacuum full works as\n          // expected.\n          createFile(basePath, sanitizedPath, file, clock)\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 0,\n            dataFiles = 6)\n          spark.sql(s\"Vacuum $tableName FULL RETAIN 0 HOURS\")\n          assertNumFiles(deltaLog, addFiles = 5, addFilesWithDVs = 0, dvFiles = 0,\n            dataFiles = 5)\n        }\n      }\n    }\n  }\n\n  test(\"running vacuum on a catalog managed table should fail\") {\n    withCatalogManagedTable() { tableName =>\n      checkError(\n        intercept[DeltaUnsupportedOperationException] {\n          spark.sql(s\"VACUUM $tableName\")\n        },\n        \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\",\n        parameters = Map(\"operation\" -> \"VACUUM\")\n      )\n      checkError(\n        intercept[DeltaUnsupportedOperationException] {\n          spark.sql(s\"VACUUM $tableName DRY RUN\")\n        },\n        \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\",\n        parameters = Map(\"operation\" -> \"VACUUM\")\n      )\n    }\n  }\n}\n\nclass DeltaLiteVacuumSuite\n  extends DeltaVacuumSuite {\n  override def isLiteVacuum: Boolean = true\n\n  private var oldValue: Boolean = false\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    oldValue = spark.conf.get(DeltaSQLConf.LITE_VACUUM_ENABLED)\n    spark.conf.set(DeltaSQLConf.LITE_VACUUM_ENABLED.key, \"true\")\n  }\n\n  override def afterAll(): Unit = {\n    spark.conf.set(DeltaSQLConf.LITE_VACUUM_ENABLED.key, oldValue)\n    super.afterAll()\n  }\n\n  test(\"lite vacuum not possible - commit 0 is missing\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\"\n    ) {\n      withTempDir { dir =>\n        // create table versions 0 and 1\n        spark.range(10)\n          .write\n          .format(\"delta\")\n          .save(dir.getAbsolutePath)\n        spark.range(10)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(dir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n        val table = DeltaTableV2(spark, new Path(dir.getAbsolutePath))\n        deltaTable.delete()\n        // Checkpoints will allow us to construct the table snapshot\n        table.deltaLog.createCheckpointAtVersion(2L)\n        deleteCommitFile(table, 0L) // delete version 0\n\n        val e = intercept[DeltaIllegalStateException] {\n          VacuumCommand.gc(spark, table, dryRun = true, retentionHours = Some(0))\n        }\n        assert(e.getMessage.contains(\"VACUUM LITE cannot delete all eligible files as some files\" +\n          \" are not referenced by the Delta log. Please run VACUUM FULL.\"))\n      }\n    }\n  }\n\n  test(\"lite vacuum not possible - commits since last vacuum is missing\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\"\n    ) {\n      withTempDir { dir =>\n        // create table - version 0\n        spark.range(10)\n          .write\n          .format(\"delta\")\n          .save(dir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n        val table = DeltaTableV2(spark, new Path(dir.getAbsolutePath))\n        deltaTable.delete() // version 1\n        // The following Vacuum saves latestCommitVersionOutsideOfRetentionWindow as 1\n        VacuumCommand.gc(spark, table, dryRun = false, retentionHours = Some(0))\n        spark.range(10)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(dir.getAbsolutePath) // version 2\n        deltaTable.delete() // version 3\n        // Checkpoint will allow us to construct the table snapshot\n        table.deltaLog.createCheckpointAtVersion(3L)\n        // Deleting version 0 shouldn't fail the vacuum since\n        // latestCommitVersionOutsideOfRetentionWindow is already at 1\n        deleteCommitFile(table, 0L)// delete version 0.\n        VacuumCommand.gc(spark, table, dryRun = true, retentionHours = Some(0))\n        // Since commit versions 1 and 2 are required for lite vacuum, deleting them will\n        // fail the command.\n        for (i <- 1 to 2) {\n          deleteCommitFile(table, i)\n        }\n\n        val e = intercept[DeltaIllegalStateException] {\n          VacuumCommand.gc(spark, table, dryRun = true, retentionHours = Some(0))\n        }\n        assert(e.getMessage.contains(\"VACUUM LITE cannot delete all eligible files as some files\" +\n          \" are not referenced by the Delta log. Please run VACUUM FULL.\"))\n      }\n    }\n  }\n\n  test(\"repeated invocations for lite vacuum is a no-op and doesn't throw any exception\") {\n    withEnvironment { (tempDir, clock) =>\n      val reservoirDir = new File(tempDir.getAbsolutePath, \"reservoir\")\n      val table = DeltaTableV2(spark, reservoirDir, clock)\n\n      gcTest(table, clock)(\n        // create 2  files\n        CreateFile(\"file1.txt\", commitToActionLog = true),\n        CreateFile(\"file2.txt\", commitToActionLog = true),\n        LogicallyDeleteFile(\"file1.txt\"),\n        LogicallyDeleteFile(\"file2.txt\"),\n        CheckFiles(Seq(\"file1.txt\", \"file2.txt\")),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = true, Seq(reservoirDir.toString + \"/file1.txt\",\n          reservoirDir.toString + \"/file2.txt\")),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        CheckFiles(Seq(\"file1.txt\", \"file2.txt\"), exist = false),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = true, Seq()),\n        GC(dryRun = false, Seq(reservoirDir.toString)),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        GC(dryRun = true, Seq()),\n        GC(dryRun = false, Seq(reservoirDir.toString))\n      )\n    }\n  }\n\n  test(\"Vacuum retain argument is ignored if it's not 0 hours\") {\n    withEnvironment { (tempDir, clock) =>\n      val reservoirDir = new File(tempDir.getAbsolutePath, \"reservoir\")\n      val table = DeltaTableV2(spark, reservoirDir, clock)\n\n      gcTest(table, clock)(\n        // create 2  files\n        CreateFile(\"file1.txt\", commitToActionLog = true),\n        CreateFile(\"file2.txt\", commitToActionLog = true),\n        LogicallyDeleteFile(\"file1.txt\"),\n        AdvanceClock(defaultTombstoneInterval + 1000),\n        LogicallyDeleteFile(\"file2.txt\"),\n        CheckFiles(Seq(\"file1.txt\", \"file2.txt\")),\n        AdvanceClock((24 * 60 * 60 * 1000) + 1000), // 24 hours + 1000 ms\n        // 24 hours retain argument is ignored and only file1.txt is eligible for GC\n        GC(dryRun = true, Seq(reservoirDir.toString + \"/file1.txt\"), retentionHours = Some(24)),\n        GC(dryRun = false, Seq(reservoirDir.toString), retentionHours = Some(24)),\n        CheckFiles(Seq(\"file2.txt\"))\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaVariantShreddingSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils, TestsStatistics}\nimport org.apache.spark.sql.delta.test.shims.VariantShreddingTestShims\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.execution.datasources.parquet.{ParquetToSparkSchemaConverter, SparkShreddingUtils}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nimport org.apache.parquet.hadoop.ParquetFileReader\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\nimport org.apache.parquet.schema.{GroupType, MessageType, Type}\nimport org.scalatest.Ignore\n\nclass DeltaVariantShreddingSuite\n  extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils\n    with TestsStatistics {\n\n  import testImplicits._\n\n  private def numShreddedFiles(path: String, validation: GroupType => Boolean = _ => true): Int = {\n    def listParquetFilesRecursively(dir: String): Seq[String] = {\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val files = deltaLog.snapshot.allFiles\n      files.collect().map { file: AddFile =>\n        file.absolutePath(deltaLog).toString\n      }\n    }\n\n    val parquetFiles = listParquetFilesRecursively(path)\n\n    def hasStructWithFieldNamesInternal(schema: List[Type], fieldNames: Set[String]): Boolean = {\n      schema.exists {\n        case group: GroupType if group.getFields.asScala.map(_.getName).toSet == fieldNames =>\n          true\n        case group: GroupType =>\n          hasStructWithFieldNamesInternal(group.getFields.asScala.toList, fieldNames)\n        case _ => false\n      }\n    }\n\n    def hasStructWithFieldNames(schema: MessageType, fieldNames: Set[String]): Boolean = {\n      schema.getFields.asScala.exists {\n        case group: GroupType if group.getFields.asScala.map(_.getName).toSet == fieldNames &&\n          validation(group) =>\n          true\n        case group: GroupType =>\n          hasStructWithFieldNamesInternal(group.getFields.asScala.toList, fieldNames)\n        case _ => false\n      }\n    }\n\n    val requiredFieldNames = Set(\"value\", \"metadata\", \"typed_value\")\n    val conf = new Configuration()\n    parquetFiles.count { p =>\n      val reader = ParquetFileReader.open(conf, new Path(p))\n      val footer: ParquetMetadata = reader.getFooter\n      val isShredded =\n        hasStructWithFieldNames(footer.getFileMetaData().getSchema, requiredFieldNames)\n      reader.close()\n      isShredded\n    }\n  }\n\n  test(\"variant shredding table property\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, i INTEGER) USING DELTA\")\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"tbl\"))\n      assert(!snapshot.protocol\n        .isFeatureSupported(VariantShreddingPreviewTableFeature),\n        s\"Table tbl contains ShreddedVariantTableFeature descriptor when its not supposed to\"\n      )\n      sql(s\"ALTER TABLE tbl \" +\n        s\"SET TBLPROPERTIES('${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'true')\")\n      assert(getProtocolForTable(\"tbl\")\n        .readerAndWriterFeatures.contains(VariantShreddingPreviewTableFeature))\n    }\n    withTable(\"tbl\") {\n      sql(s\"CREATE TABLE tbl(s STRING, i INTEGER) USING DELTA \" +\n        s\"TBLPROPERTIES('${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'true')\")\n      assert(getProtocolForTable(\"tbl\")\n        .readerAndWriterFeatures.contains(VariantShreddingPreviewTableFeature))\n    }\n    assert(DeltaConfigs.ENABLE_VARIANT_SHREDDING.key == \"delta.enableVariantShredding\")\n  }\n\n  test(\"Spark can read shredded table containing the shredding table feature\") {\n    withTable(\"tbl\") {\n      withTempDir { dir =>\n        val schema = \"a int, b string, c decimal(15, 1)\"\n        val df = spark.sql(\n          \"\"\"\n            | select id i, case\n            | when id = 0 then parse_json('{\"a\": 1, \"b\": \"2\", \"c\": 3.3, \"d\": 4.4}')\n            | when id = 1 then parse_json('{\"a\": [1,2,3], \"b\": \"hello\", \"c\": {\"x\": 0}}')\n            | when id = 2 then parse_json('{\"A\": 1, \"c\": 1.23}')\n            | end v from range(0, 3, 1, 1)\n            |\"\"\".stripMargin)\n\n        sql(\"CREATE TABLE tbl (i long, v variant) USING DELTA \" +\n          s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'true') \" +\n          s\"LOCATION '${dir.getAbsolutePath}'\")\n        assert(getProtocolForTable(\"tbl\")\n          .readerAndWriterFeatures.contains(VariantShreddingPreviewTableFeature))\n        withSQLConf(SQLConf.VARIANT_WRITE_SHREDDING_ENABLED.key -> true.toString,\n          SQLConf.VARIANT_ALLOW_READING_SHREDDED.key -> true.toString,\n          SQLConf.VARIANT_FORCE_SHREDDING_SCHEMA_FOR_TEST.key -> schema) {\n\n          df.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n          // Make sure the actual parquet files are shredded\n          assert(numShreddedFiles(dir.getAbsolutePath, validation = { field: GroupType =>\n            field.getName == \"v\" && (field.getType(\"typed_value\") match {\n              case t: GroupType =>\n                t.getFields.asScala.map(_.getName).toSet == Set(\"a\", \"b\", \"c\")\n              case _ => false\n            })\n          }) == 1)\n          checkAnswer(\n            spark.read.format(\"delta\").load(dir.getAbsolutePath).selectExpr(\"i\", \"to_json(v)\"),\n            df.selectExpr(\"i\", \"to_json(v)\").collect()\n          )\n        }\n      }\n    }\n  }\n\n  test(\"Test shredding property controls shredded writes\") {\n    val schema = \"a int, b string, c decimal(15, 1)\"\n    val df = spark.sql(\n      \"\"\"\n        | select id i, case\n        | when id = 0 then parse_json('{\"a\": 1, \"b\": \"2\", \"c\": 3.3, \"d\": 4.4}')\n        | when id = 1 then parse_json('{\"a\": [1,2,3], \"b\": \"hello\", \"c\": {\"x\": 0}}')\n        | when id = 2 then parse_json('{\"A\": 1, \"c\": 1.23}')\n        | end v from range(0, 3, 1, 1)\n        |\"\"\".stripMargin)\n    // Table property not present or false\n    Seq(\"\", s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'false') \")\n      .foreach { tblProperties =>\n        withTable(\"tbl\") {\n          withTempDir { dir =>\n            sql(\"CREATE TABLE tbl (i long, v variant) USING DELTA \" + tblProperties +\n              s\"LOCATION '${dir.getAbsolutePath}'\")\n            withSQLConf(SQLConf.VARIANT_WRITE_SHREDDING_ENABLED.key -> true.toString,\n              SQLConf.VARIANT_ALLOW_READING_SHREDDED.key -> true.toString,\n              SQLConf.VARIANT_FORCE_SHREDDING_SCHEMA_FOR_TEST.key -> schema) {\n\n              val e = intercept[DeltaSparkException] {\n                df.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n              }\n              checkError(e, \"DELTA_SHREDDING_TABLE_PROPERTY_DISABLED\", parameters = Map())\n              assert(e.getMessage.contains(\n                \"Attempted to write shredded Variants but the table does not support shredded \" +\n                  \"writes. Consider setting the table property enableVariantShredding to true.\"))\n              assert(numShreddedFiles(dir.getAbsolutePath, validation = { field: GroupType =>\n                field.getName == \"v\" && (field.getType(\"typed_value\") match {\n                  case t: GroupType =>\n                    t.getFields.asScala.map(_.getName).toSet == Set(\"a\", \"b\", \"c\")\n                  case _ => false\n                })\n              }) == 0)\n              checkAnswer(\n                spark.read.format(\"delta\").load(dir.getAbsolutePath).selectExpr(\"i\", \"to_json(v)\"),\n                Seq()\n              )\n            }\n          }\n        }\n      }\n  }\n\n  test(\"Set table property to invalid value\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, i INTEGER) USING DELTA\")\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"tbl\"))\n      assert(!snapshot.protocol\n        .isFeatureSupported(VariantShreddingPreviewTableFeature),\n        s\"Table tbl contains ShreddedVariantTableFeature descriptor when its not supposed to\"\n      )\n      checkError(\n        intercept[SparkException] {\n          sql(s\"ALTER TABLE tbl \" +\n            s\"SET TBLPROPERTIES('${DeltaConfigs.ENABLE_VARIANT_SHREDDING.key}' = 'bla')\")\n        },\n        \"_LEGACY_ERROR_TEMP_2045\",\n        parameters = Map(\n          \"message\" -> \"For input string: \\\"bla\\\"\"\n        )\n      )\n      assert(!getProtocolForTable(\"tbl\")\n        .readerAndWriterFeatures.contains(VariantShreddingPreviewTableFeature))\n    }\n  }\n\n  test(\"Infer schema for Delta table\") {\n    // Skip this test if VARIANT_INFER_SHREDDING_SCHEMA is not supported (Spark 4.0)\n    assume(VariantShreddingTestShims.variantInferShreddingSchemaSupported,\n      \"VARIANT_INFER_SHREDDING_SCHEMA is not supported in this Spark version\")\n\n    // make sure top level conf has no effect and table property is respected.\n    Seq(false, true).foreach { inferShreddingSchema =>\n      withSQLConf(VariantShreddingTestShims.variantInferShreddingSchemaKey ->\n        inferShreddingSchema.toString) {\n        Seq(false, true).foreach { enable =>\n          val tbl = s\"tbl_$enable\"\n          withTable(tbl) {\n            withTempDir { dir =>\n              val query =\n                \"\"\"select parse_json('{\"a\": ' || id || ', \"b\": \"' || id || '\"}') as v\n            | from range(0, 3, 1, 1)\"\"\".stripMargin\n              val properties = if (enable) {\n                \"tblproperties ('delta.enableVariantShredding' = 'true')\"\n              } else {\n                \"\"\n              }\n              spark.sql(\n                s\"\"\"\n            | create table $tbl using delta\n            | $properties\n            | location '${dir.getAbsolutePath}'\n            |  as\n            | $query\n            |\"\"\".stripMargin)\n              if (enable) {\n                assert(numShreddedFiles(\n                  dir.getAbsolutePath,\n                  validation = { field: GroupType =>\n                    field.getName == \"v\" && (field.getType(\"typed_value\") match {\n                      case t: GroupType => t.getFields.asScala.map(_.getName).toSet == Set(\"a\", \"b\")\n                      case _ => false\n                    })\n                  }) == 1)\n              } else {\n                assert(numShreddedFiles(dir.getAbsolutePath) == 0)\n              }\n              checkAnswer(spark.table(tbl), spark.sql(query).collect())\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"creating table with preview feature does not add stable feature (and vice versa)\") {\n    Seq(\"-preview\", \"\").foreach { featureSuffix =>\n      withTable(\"tbl\") {\n        sql(s\"\"\"CREATE TABLE tbl(i INT)\n                USING delta\n                TBLPROPERTIES(\n                  'delta.enableVariantShredding' = 'true',\n                  'delta.feature.variantShredding$featureSuffix' = 'supported'\n                )\"\"\")\n        DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n          spark,\n          \"tbl\",\n          expectPreviewFeature = featureSuffix.nonEmpty,\n          expectStableFeature = featureSuffix.isEmpty)\n      }\n    }\n  }\n\n  test(\"manually enabling preview and stable table feature\") {\n    Seq(false, true).foreach { forcePreview =>\n      withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_SHREDDING_FEATURE.key -> forcePreview.toString) {\n        withTable(\"tbl\") {\n          sql(\"\"\"CREATE TABLE tbl(i INT)\n              USING delta\n              TBLPROPERTIES(\n                'delta.feature.variantShredding' = 'supported',\n                'delta.feature.variantShredding-preview' = 'supported'\n              )\"\"\")\n          DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n            spark,\n            \"tbl\",\n            expectPreviewFeature = true,\n            expectStableFeature = true)\n          sql(\"\"\"ALTER TABLE tbl SET TBLPROPERTIES ('delta.enableVariantShredding' = 'true')\"\"\")\n          DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n            spark,\n            \"tbl\",\n            expectPreviewFeature = true,\n            expectStableFeature = true)\n        }\n      }\n    }\n  }\n\n  test(\"enabling 'FORCE_USE_PREVIEW_SHREDDING_FEATURE' adds preview table feature for new table\") {\n    Seq(false, true).foreach { forcePreview =>\n      withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_SHREDDING_FEATURE.key -> forcePreview.toString) {\n        withTable(\"tbl\") {\n          sql(\"CREATE TABLE tbl(s STRING) USING DELTA TBLPROPERTIES \" +\n            \"('delta.enableVariantShredding' = 'true')\")\n          DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n            spark,\n            \"tbl\",\n            expectPreviewFeature = forcePreview,\n            expectStableFeature = !forcePreview)\n        }\n      }\n    }\n  }\n\n  test(\"enabling 'FORCE_USE_PREVIEW_SHREDDING_FEATURE' and setting shredding table property \" +\n    \"adds the preview table feature\") {\n    Seq(false, true).foreach { forcePreview =>\n      withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_SHREDDING_FEATURE.key -> forcePreview.toString) {\n        withTable(\"tbl\") {\n          sql(\"CREATE TABLE tbl(s STRING) USING DELTA\")\n          DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n            spark,\n            \"tbl\",\n            expectPreviewFeature = false,\n            expectStableFeature = false)\n\n          sql(\"ALTER TABLE tbl SET TBLPROPERTIES ('delta.enableVariantShredding' = 'true')\")\n\n          DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n            spark,\n            \"tbl\",\n            expectPreviewFeature = forcePreview,\n            expectStableFeature = !forcePreview)\n        }\n      }\n    }\n  }\n\n  test(\"enabling 'FORCE_USE_PREVIEW_VARIANT_FEATURE' on table with stable feature does not \" +\n    \"require adding preview feature\") {\n    Seq(false, true).foreach { forcePreview =>\n      withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_SHREDDING_FEATURE.key -> forcePreview.toString) {\n        withTable(\"tbl\") {\n          sql(\"CREATE TABLE tbl(s STRING) USING DELTA TBLPROPERTIES \" +\n            \"('delta.enableVariantShredding' = 'true')\")\n          DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n            spark,\n            \"tbl\",\n            expectPreviewFeature = forcePreview,\n            expectStableFeature = !forcePreview)\n\n          withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_VARIANT_FEATURE.key ->\n            (!forcePreview).toString) {\n            // Reset the table property and set it again to see if it modifies to protocol\n            sql(\"ALTER TABLE tbl SET \" +\n              \"TBLPROPERTIES ('delta.enableVariantShredding' = 'false')\")\n            DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n              spark,\n              \"tbl\",\n              expectPreviewFeature = forcePreview,\n              expectStableFeature = !forcePreview)\n            sql(\"ALTER TABLE tbl SET \" +\n              \"TBLPROPERTIES ('delta.enableVariantShredding' = 'true')\")\n            DeltaVariantShreddingSuite.assertVariantShreddingTableFeatures(\n              spark,\n              \"tbl\",\n              expectPreviewFeature = forcePreview,\n              expectStableFeature = !forcePreview)\n          }\n        }\n      }\n    }\n  }\n}\n\nobject DeltaVariantShreddingSuite {\n  def assertVariantShreddingTableFeatures(\n      spark: SparkSession,\n      tableName: String,\n      expectPreviewFeature: Boolean,\n      expectStableFeature: Boolean): Unit = {\n    val features = DeltaLog.forTable(spark, TableIdentifier(tableName)).update().protocol\n      .readerAndWriterFeatures\n    assert(expectPreviewFeature == features.contains(VariantShreddingPreviewTableFeature))\n    assert(expectStableFeature == features.contains(VariantShreddingTableFeature))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaVariantSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable.{Seq => MutableSeq}\nimport scala.io.Source\n\nimport io.delta.tables.DeltaTable\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils, TestsStatistics}\n\nimport org.apache.spark.{SparkException, SparkThrowable}\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.delta.DeltaAnalysisException\nimport org.apache.spark.sql.delta.schema.DeltaInvariantViolationException\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.scalatest.Ignore\n\nclass DeltaVariantSuite\n  extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils\n    with TestsStatistics {\n\n  import testImplicits._\n\n  private def assertVariantTypeTableFeatures(\n    tableName: String,\n    expectPreviewFeature: Boolean,\n    expectStableFeature: Boolean): Unit = {\n    val features = getProtocolForTable(\"tbl\").readerAndWriterFeatures\n    if (expectPreviewFeature) {\n      assert(features.contains(VariantTypePreviewTableFeature))\n    } else {\n      assert(!features.contains(VariantTypePreviewTableFeature))\n    }\n    if (expectStableFeature) {\n      assert(features.contains(VariantTypeTableFeature))\n    } else {\n      assert(!features.contains(VariantTypeTableFeature))\n    }\n  }\n\n  test(\"create a new table with Variant, higher protocol and feature should be picked.\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, v VARIANT) USING DELTA\")\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n      assertVariantTypeTableFeatures(\n        \"tbl\", expectPreviewFeature = false, expectStableFeature = true)\n    }\n  }\n\n  test(\"creating a table without Variant should use the usual minimum protocol\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, i INTEGER) USING DELTA\")\n      assert(getProtocolForTable(\"tbl\") == Protocol(1, 2))\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n      assert(\n        !deltaLog.unsafeVolatileSnapshot.protocol.isFeatureSupported(\n          VariantTypePreviewTableFeature) &&\n          !deltaLog.unsafeVolatileSnapshot.protocol.isFeatureSupported(\n            VariantTypeTableFeature),\n        s\"Table tbl contains VariantTypeFeature descriptor when its not supposed to\"\n      )\n    }\n  }\n\n  test(\"add a new Variant column should upgrade to the correct protocol versions\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING) USING delta\")\n      assert(getProtocolForTable(\"tbl\") == Protocol(1, 2))\n      sql(\"ALTER TABLE tbl ADD COLUMN v VARIANT\")\n\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n\n      sql(\"ALTER TABLE tbl ADD COLUMN v2 STRUCT<v21 VARIANT>\")\n      sql(\"INSERT INTO tbl (SELECT 'bar', \" +\n        \"parse_json(cast(id + 100 as string)), struct(parse_json(cast(id + 101 as string))) \" +\n        \"FROM range(1))\")\n      checkAnswer(spark.table(\"tbl\").selectExpr(\"v::int\"), Seq(Row(99), Row(100)))\n      checkAnswer(spark.table(\"tbl\").selectExpr(\"v2.v21::int\"), Seq(Row(null), Row(101)))\n\n      assert(\n        getProtocolForTable(\"tbl\") ==\n          VariantTypeTableFeature.minProtocolVersion\n            .withFeature(VariantTypeTableFeature)\n            .withFeature(InvariantsTableFeature)\n            .withFeature(AppendOnlyTableFeature)\n      )\n    }\n  }\n\n  test(\"variant stable and preview features can be supported simultaneously and read\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(v VARIANT) USING delta\")\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n      assertVariantTypeTableFeatures(\n        \"tbl\", expectPreviewFeature = false, expectStableFeature = true)\n      sql(\n        s\"ALTER TABLE tbl \" +\n          s\"SET TBLPROPERTIES('delta.feature.variantType-preview' = 'supported')\"\n      )\n      assertVariantTypeTableFeatures(\n        \"tbl\", expectPreviewFeature = true, expectStableFeature = true)\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n    }\n  }\n\n  test(\"creating a new variant table uses only the stable table feature\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, v VARIANT) USING DELTA\")\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n      assertVariantTypeTableFeatures(\n        \"tbl\", expectPreviewFeature = false, expectStableFeature = true)\n    }\n  }\n\n  test(\"manually adding preview table feature does not require adding stable table feature\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING) USING delta\")\n      sql(\n        s\"ALTER TABLE tbl \" +\n          s\"SET TBLPROPERTIES('delta.feature.variantType-preview' = 'supported')\"\n      )\n\n      sql(\"ALTER TABLE tbl ADD COLUMN v VARIANT\")\n\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n\n      assertVariantTypeTableFeatures(\n        \"tbl\",\n        expectPreviewFeature = true,\n        expectStableFeature = false\n      )\n    }\n  }\n\n  test(\"creating table with preview feature does not add stable feature\") {\n    withTable(\"tbl\") {\n      sql(s\"\"\"CREATE TABLE tbl(v VARIANT)\n              USING delta\n              TBLPROPERTIES('delta.feature.variantType-preview' = 'supported')\"\"\"\n      )\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assertVariantTypeTableFeatures(\n        \"tbl\",\n        expectPreviewFeature = true,\n        expectStableFeature = false\n      )\n    }\n  }\n\n  test(\"enabling 'FORCE_USE_PREVIEW_VARIANT_FEATURE' adds preview table feature for new table\") {\n    withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_VARIANT_FEATURE.key -> \"true\") {\n      withTable(\"tbl\") {\n        sql(\"CREATE TABLE tbl(s STRING, v VARIANT) USING DELTA\")\n        sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n        assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n        assertVariantTypeTableFeatures(\n          \"tbl\", expectPreviewFeature = true, expectStableFeature = false)\n      }\n    }\n  }\n\n  test(\"enabling 'FORCE_USE_PREVIEW_VARIANT_FEATURE' and adding a variant column adds the \" +\n    \"preview table feature\") {\n    withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_VARIANT_FEATURE.key -> \"true\") {\n      withTable(\"tbl\") {\n        sql(\"CREATE TABLE tbl(s STRING) USING delta\")\n\n        sql(\"ALTER TABLE tbl ADD COLUMN v VARIANT\")\n        sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n        assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n\n        sql(\"ALTER TABLE tbl ADD COLUMN v2 STRUCT<v21 VARIANT>\")\n        sql(\"INSERT INTO tbl (SELECT 'bar', \" +\n          \"parse_json(cast(id + 100 as string)), struct(parse_json(cast(id + 101 as string))) \" +\n          \"FROM range(1))\")\n        checkAnswer(spark.table(\"tbl\").selectExpr(\"v::int\"), Seq(Row(99), Row(100)))\n        checkAnswer(spark.table(\"tbl\").selectExpr(\"v2.v21::int\"), Seq(Row(null), Row(101)))\n\n        assertVariantTypeTableFeatures(\n          \"tbl\",\n          expectPreviewFeature = true,\n          expectStableFeature = false\n        )\n      }\n    }\n  }\n\n  test(\"enabling 'FORCE_USE_PREVIEW_VARIANT_FEATURE' on table with stable feature does not \" +\n    \"require adding preview feature\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, v VARIANT) USING DELTA\")\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n      assert(spark.table(\"tbl\").selectExpr(\"v::int\").head == Row(99))\n      assertVariantTypeTableFeatures(\n        \"tbl\", expectPreviewFeature = false, expectStableFeature = true)\n\n      withSQLConf(DeltaSQLConf.FORCE_USE_PREVIEW_VARIANT_FEATURE.key -> \"true\") {\n        sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n        assert(spark.table(\"tbl\").selectExpr(\"v::int\").count == 2)\n        assertVariantTypeTableFeatures(\n          \"tbl\", expectPreviewFeature = false, expectStableFeature = true)\n      }\n    }\n  }\n\n  test(\"VariantType may not be used as a partition column\") {\n    withTable(\"delta_test\") {\n      checkError(\n        intercept[AnalysisException] {\n          sql(\n            \"\"\"CREATE TABLE delta_test(s STRING, v VARIANT)\n              |USING delta\n              |PARTITIONED BY (v)\"\"\".stripMargin)\n        },\n        \"INVALID_PARTITION_COLUMN_DATA_TYPE\",\n        parameters = Map(\"type\" -> \"\\\"VARIANT\\\"\")\n      )\n    }\n  }\n\n  test(\"streaming variant delta table\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      spark.range(100)\n        .selectExpr(\"parse_json(cast(id as string)) v\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(path)\n\n      val streamingDf = spark.readStream\n        .format(\"delta\")\n        .load(path)\n        .selectExpr(\"v::int as extractedVal\")\n\n      val q = streamingDf.writeStream\n        .format(\"memory\")\n        .queryName(\"test_table\")\n        .start()\n\n      q.processAllAvailable()\n      q.stop()\n\n      val actual = spark.sql(\"select extractedVal from test_table\")\n      val expected = spark.sql(\"select id from range(100)\")\n      checkAnswer(actual, expected.collect())\n    }\n  }\n\n  test(\"variant works with schema evolution for INSERT\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      spark.range(0, 100, 1, 1)\n        .selectExpr(\"id\", \"parse_json(cast(id as string)) v\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(path)\n\n      spark.range(100, 200, 1, 1)\n        .selectExpr(\n          \"id\",\n          \"parse_json(cast(id as string)) v\",\n          \"parse_json(cast(id as string)) v_two\"\n        )\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .save(path)\n\n      val expected = spark.range(0, 200, 1, 1).selectExpr(\n        \"id\",\n        \"parse_json(cast(id as string)) v\",\n        \"case when id >= 100 then parse_json(cast(id as string)) else null end v_two\"\n      )\n\n      val read = spark.read.format(\"delta\").load(path)\n      checkAnswer(read, expected.collect())\n    }\n  }\n\n  test(\"variant works with schema evolution for MERGE\") {\n    withTempDir { dir =>\n      withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n        val path = dir.getAbsolutePath\n        spark.range(0, 100, 1, 1)\n          .selectExpr(\"id\", \"parse_json(cast(id as string)) v\")\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .save(path)\n\n        val sourceDf = spark.range(50, 200, 1, 1)\n          .selectExpr(\n            \"id\",\n            \"parse_json(cast(id as string)) v\",\n            \"parse_json(cast(id as string)) v_two\"\n          )\n\n        DeltaTable.forPath(spark, path)\n          .as(\"source\")\n          .merge(sourceDf.as(\"target\"), \"source.id = target.id\")\n          .whenMatched()\n          .updateAll()\n          .whenNotMatched()\n          .insertAll()\n          .execute()\n\n        val expected = spark.range(0, 200, 1, 1).selectExpr(\n          \"id\",\n          \"parse_json(cast(id as string)) v\",\n          \"case when id >= 50 then parse_json(cast(id as string)) else null end v_two\"\n        )\n\n        val read = spark.read.format(\"delta\").load(path)\n        checkAnswer(read, expected.collect())\n      }\n    }\n  }\n\n  test(\"variant cannot be used as a clustering column\") {\n    withTable(\"tbl\") {\n      val e = intercept[DeltaAnalysisException] {\n        sql(\"CREATE TABLE tbl(v variant) USING DELTA CLUSTER BY (v)\")\n      }\n      checkError(\n        e,\n        \"DELTA_CLUSTERING_COLUMNS_DATATYPE_NOT_SUPPORTED\",\n        parameters = Map(\"columnsWithDataTypes\" -> \"v : VARIANT\")\n      )\n    }\n  }\n\n  test(\"describe history works with variant column\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, v VARIANT) USING DELTA\")\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n      // Create and insert should result in two table versions.\n      assert(sql(\"DESCRIBE HISTORY tbl\").count() == 2)\n    }\n  }\n\n  test(\"describe detail works with variant column\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(s STRING, v VARIANT) USING DELTA\")\n      sql(\"INSERT INTO tbl (SELECT 'foo', parse_json(cast(id + 99 as string)) FROM range(1))\")\n\n      val tableFeatures = sql(\"DESCRIBE DETAIL tbl\")\n        .selectExpr(\"tableFeatures\")\n        .collect()(0)\n        .getAs[MutableSeq[String]](0)\n      assert(tableFeatures.find(f => f == VariantTypePreviewTableFeature.name).isEmpty)\n      assert(tableFeatures.find(f => f == VariantTypeTableFeature.name).nonEmpty)\n    }\n  }\n\n  test(\"time travel with variant column works\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      val initialDf = spark.range(0, 100, 1, 1).selectExpr(\"parse_json(cast(id as string)) v\")\n      initialDf\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(path)\n\n      spark.range(100, 150, 1, 1).selectExpr(\"parse_json(cast(id as string)) v\")\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(path)\n\n      val timeTravelDf = spark.read.format(\"delta\").option(\"versionAsOf\", \"0\").load(path)\n      checkAnswer(timeTravelDf, initialDf.collect())\n    }\n  }\n\n  statsTest(\"optimize variant\") {\n    withTable(\"tbl\") {\n      spark.range(0, 100)\n        .selectExpr(\"case when id % 2 = 0 then parse_json(cast(id as string)) else null end as v\")\n        .repartition(100)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .saveAsTable(\"tbl\")\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n      val res = sql(\"OPTIMIZE tbl\")\n      val metrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n      assert(metrics.numFilesAdded > 0)\n      assert(metrics.numTableColumnsWithStats == 1)\n\n      val statsDf = getStatsDf(deltaLog, Seq($\"numRecords\", $\"nullCount\"))\n      checkAnswer(statsDf, Row(100, Row(50)))\n    }\n  }\n\n  test(\"Zorder is not supported for Variant\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl USING DELTA AS SELECT id, cast(null as variant) v from range(100)\")\n      val e = intercept[SparkException](sql(\"optimize tbl zorder by (v)\"))\n      checkError(\n        e.getCause.asInstanceOf[SparkThrowable],\n        \"DATATYPE_MISMATCH.TYPE_CHECK_FAILURE_WITH_HINT\",\n        parameters = Map(\n          \"msg\" -> \"cannot sort data type variant\",\n          \"hint\" -> \"\",\n          \"sqlExpr\" -> \"\\\"rangepartitionid(v)\\\"\"))\n    }\n  }\n\n  test(\"Table with variant type can use CDF\") {\n    withTable(\"tbl\") {\n      sql(\"\"\"CREATE TABLE tbl USING DELTA TBLPROPERTIES (delta.enableChangeDataFeed = true)\n          AS SELECT parse_json(cast(id as string)) v from range(100)\"\"\")\n\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id as string)) as v from range(0, 1))\")\n      sql(\"DELETE FROM tbl WHERE v::int = 0\")\n      sql(\"UPDATE tbl SET v = parse_json('-2') WHERE v::int = 50\")\n\n      checkAnswer(\n        sql(\"\"\"select _change_type, v::int from table_changes('tbl', 0)\n               where _change_type = 'delete'\"\"\"),\n        Seq(Row(\"delete\", 0), Row(\"delete\", 0))\n      )\n      checkAnswer(\n        sql(\"\"\"select _change_type, v::int from table_changes('tbl', 0)\n               where _change_type = 'update_preimage'\"\"\"),\n        Seq(Row(\"update_preimage\", 50))\n      )\n      checkAnswer(\n        sql(\"\"\"select _change_type, v::int from table_changes('tbl', 0)\n               where _change_type = 'update_postimage'\"\"\"),\n        Seq(Row(\"update_postimage\", -2))\n      )\n    }\n  }\n\n  test(\"Existing table with variant type can enable CDF\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(v variant) USING DELTA\")\n      sql(\"ALTER TABLE tbl SET TBLPROPERTIES (delta.enableChangeDataFeed = true)\")\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id as string)) as v from range(0, 100))\")\n      sql(\"DELETE FROM tbl WHERE v::string = '0'\")\n      sql(\"UPDATE tbl SET v = parse_json('-2') WHERE v::int = 50\")\n\n      checkAnswer(\n        sql(\"\"\"select _change_type, v::int from table_changes('tbl', 1)\n               where _change_type = 'delete'\"\"\"),\n        Seq(Row(\"delete\", 0))\n      )\n      checkAnswer(\n        sql(\"\"\"select _change_type, v::int from table_changes('tbl', 1)\n               where _change_type = 'update_preimage'\"\"\"),\n        Seq(Row(\"update_preimage\", 50))\n      )\n      checkAnswer(\n        sql(\"\"\"select _change_type, v::int from table_changes('tbl', 1)\n               where _change_type = 'update_postimage'\"\"\"),\n        Seq(Row(\"update_postimage\", -2))\n      )\n    }\n  }\n\n  test(s\"shallow cloning table with variant\") {\n    withTable(\"tbl\", \"clone_tbl\") {\n      sql(\"\"\"CREATE TABLE tbl USING DELTA AS\n          SELECT parse_json(cast(id as string)) v FROM range(100)\"\"\")\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id as string)) as v from range(0, 10))\")\n\n      sql(s\"CREATE TABLE IF NOT EXISTS clone_tbl SHALLOW CLONE tbl\")\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id as string)) as v from range(0, 10))\")\n      sql(\"INSERT INTO clone_tbl (SELECT parse_json(cast(id as string)) as v from range(0, 10))\")\n\n      val origTable = spark.sql(\"select * from tbl\")\n      val clonedTable = spark.sql(\"select * from clone_tbl\")\n      checkAnswer(clonedTable, origTable.collect())\n    }\n  }\n\n  Seq(\"\", \"NO STATISTICS\").foreach { statsClause =>\n    test(s\"Convert to Delta from parquet - $statsClause\") {\n      withTempDir { dir =>\n        val path = dir.getAbsolutePath\n\n        spark.range(0, 100).selectExpr(\"parse_json(cast(id as string)) as v\")\n          .write\n          .format(\"parquet\")\n          .mode(\"overwrite\")\n          .save(path)\n\n        sql(s\"CONVERT TO DELTA parquet.`$path` $statsClause\")\n        // Ensure Delta feature like column renaming works.\n        sql(s\"ALTER TABLE delta.`$path` SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n        sql(s\"ALTER TABLE delta.`$path` RENAME COLUMN v TO new_v\")\n\n        val expectedDf = spark.range(0, 100).selectExpr(\"parse_json(cast(id as string)) as new_v\")\n        val actualDf = spark.sql(s\"select * from delta.`$path`\")\n        checkAnswer(actualDf, expectedDf.collect())\n      }\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { mode =>\n    Seq(false, true).foreach { pushVariantIntoScan =>\n      withSQLConf(\n        SQLConf.PUSH_VARIANT_INTO_SCAN.key -> pushVariantIntoScan.toString\n      ) {\n        test(s\"column mapping works - $mode - $pushVariantIntoScan\") {\n          withTable(\"tbl\") {\n            sql(s\"\"\"CREATE TABLE tbl USING DELTA\n            TBLPROPERTIES (\n              'delta.columnMapping.mode' = '$mode',\n              'delta.enableVariantShredding' = 'true'\n            )\n            AS SELECT parse_json(cast(id as string)) v, parse_json(cast(id as string)) v_two\n            FROM range(5)\"\"\")\n            val expectedAnswer = spark.sql(\"select v from tbl\").collect()\n\n            sql(\"ALTER TABLE tbl RENAME COLUMN v TO new_v\")\n            checkAnswer(spark.sql(\"select new_v from tbl\"), expectedAnswer)\n\n            sql(\"ALTER TABLE tbl DROP COLUMN new_v\")\n            // 'SELECT *' from the test table should return the same as `expectedAnswer` because `v`\n            // and `v_two` are initially identical and `v` is dropped, resulting in a single column.\n            checkAnswer(spark.sql(\"select * from tbl\"), expectedAnswer)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Variant can have default value set\") {\n    withTable(\"tbl\") {\n      sql(\"\"\"CREATE TABLE tbl USING DELTA\n          TBLPROPERTIES ('delta.feature.allowColumnDefaults' = 'enabled')\n          AS SELECT parse_json(cast(id as string)) v from range(5)\"\"\")\n\n      sql(\"INSERT INTO tbl VALUES (DEFAULT)\")\n      val nullCount = spark.sql(\"SELECT * FROM tbl WHERE v is null\").count()\n      // Default DEFAULT value is null.\n      assert(nullCount == 1)\n\n      sql(\"ALTER TABLE tbl ALTER COLUMN v SET DEFAULT (parse_json('{\\\"k\\\": \\\"v\\\"}'))\")\n      sql(\"INSERT INTO tbl VALUES (DEFAULT)\")\n      checkAnswer(\n        sql(\"SELECT v FROM tbl WHERE variant_get(v, '$.k', 'STRING') = 'v'\"),\n        sql(\"select parse_json('{\\\"k\\\": \\\"v\\\"}')\").collect\n      )\n    }\n  }\n\n  test(\"Variant can be used as a source for generated columns\") {\n    withTable(\"tbl\") {\n      DeltaTable.create(spark)\n        .tableName(\"tbl\")\n        .addColumn(\"v\", \"VARIANT\")\n        .addColumn(\n          DeltaTable.columnBuilder(spark, \"vInt\")\n            .dataType(\"INT\")\n            .generatedAlwaysAs(\"v::int\")\n            .build()\n        )\n        .execute()\n      spark.range(0, 100)\n        .selectExpr(\"parse_json(cast(id as string)) as v\")\n        .write\n        .mode(\"append\")\n        .format(\"delta\")\n        .saveAsTable(\"tbl\")\n      val expectedDf = spark.range(0, 100).selectExpr(\n        \"parse_json(cast(id as string)) v\",\n        \"cast(id as int) vInt\"\n      )\n      val actualDf = spark.sql(\"select * from tbl\")\n      checkAnswer(actualDf, expectedDf.collect())\n    }\n  }\n\n  test(\"Variant cannot be created as a generated column\") {\n    withTable(\"tbl\") {\n      val e = intercept[DeltaAnalysisException] {\n        DeltaTable.create(spark)\n          .tableName(\"tbl\")\n          .addColumn(\"id\", \"INT\")\n          .addColumn(\n            DeltaTable.columnBuilder(spark, \"v\")\n              .dataType(\"VARIANT\")\n              .generatedAlwaysAs(\"parse_json(cast(id as string))\")\n              .build()\n          )\n          .execute()\n      }\n      checkError(\n        e,\n        \"DELTA_UNSUPPORTED_DATA_TYPE_IN_GENERATED_COLUMN\",\n        parameters = Map(\"dataType\" -> \"VARIANT\")\n      )\n    }\n  }\n\n  test(\"Variant respects Delta table IS NOT NULL constraints\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(v variant NOT NULL) USING DELTA\")\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id as string)) from range(0, 100))\")\n      val insertException = intercept[DeltaInvariantViolationException] {\n        sql(\"INSERT INTO tbl VALUES (cast(null as variant))\")\n      }\n      checkError(\n        insertException,\n        \"DELTA_NOT_NULL_CONSTRAINT_VIOLATED\",\n        parameters = Map(\"columnName\" -> \"v\")\n      )\n\n      sql(\"ALTER TABLE tbl ALTER COLUMN v DROP NOT NULL\")\n      // Inserting null value should work now.\n      sql(\"INSERT INTO tbl VALUES (cast(null as variant))\")\n      val nullCount = spark.sql(\"select * from tbl where v is null\").count()\n      assert(nullCount == 1)\n    }\n  }\n\n  test(\"Variant respects Delta table CHECK constraints\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(v variant) USING DELTA\")\n      sql(\"ALTER TABLE tbl ADD CONSTRAINT variantGTEZero CHECK (variant_get(v, '$', 'INT') >= 0)\")\n      sql(\"INSERT INTO tbl (SELECT parse_json(cast(id as string)) from range(0, 100))\")\n\n      val insertException = intercept[DeltaInvariantViolationException] {\n        sql(\"INSERT INTO tbl (select parse_json(cast(id as string)) from range(-1, 0))\")\n      }\n      checkError(\n        insertException,\n        \"DELTA_VIOLATE_CONSTRAINT_WITH_VALUES\",\n        parameters = Map(\n          \"constraintName\" -> \"variantgtezero\",\n          \"expression\" -> \"(variant_get(v, '$', 'INT') >= 0)\", \"values\" -> \" - v : -1\"\n        )\n      )\n\n      sql(\"ALTER TABLE tbl DROP CONSTRAINT variantGTEZero\")\n      sql(\"INSERT INTO tbl (select parse_json(cast(id as string)) from range(-1, 0))\")\n      val lessThanZeroCount = spark.sql(\"select * from tbl where v::int < 0\").count()\n      // Inserting variant with value less than zero should work after dropping constraint.\n      assert(lessThanZeroCount == 1)\n      val addConstraintException = intercept[DeltaAnalysisException] {\n        sql(\"ALTER TABLE tbl ADD CONSTRAINT variantGTEZero CHECK (variant_get(v, '$', 'INT') >= 0)\")\n      }\n      checkError(\n        addConstraintException,\n        \"DELTA_NEW_CHECK_CONSTRAINT_VIOLATION\",\n        parameters = Map(\n          \"numRows\" -> \"1\",\n          \"tableName\" -> \"spark_catalog.default.tbl\",\n          \"checkConstraint\" -> \"variant_get ( v , '$' , 'INT' ) >= 0\"\n        )\n      )\n\n      sql(\"DELETE FROM tbl WHERE variant_get(v, '$', 'INT') < 0\")\n      // Adding the constraint should work after deleting the variant that is less than zero.\n      sql(\"ALTER TABLE tbl ADD CONSTRAINT variantGTEZero CHECK (variant_get(v, '$', 'INT') >= 0)\")\n      val newLessThanZeroCount = spark.sql(\"select * from tbl where v::int < 0\").count()\n      assert(newLessThanZeroCount == 0)\n    }\n  }\n\n  test(\"column mapping with pushVariantIntoScan\") {\n    withSQLConf(SQLConf.PUSH_VARIANT_INTO_SCAN.key -> \"true\") {\n      withTable(\"t1\") {\n        sql(\n          \"\"\"create table t1 (v variant) using delta\n            |tblproperties (\n            |  'delta.columnMapping.mode' = 'name',\n            |  'delta.enableVariantShredding' = 'true'\n            |)\"\"\".stripMargin)\n        sql(\"\"\"insert into t1 (v) select parse_json('{\"a\": 1}')\"\"\")\n\n        checkAnswer(sql(\"select to_json(v) from t1\"), Seq(Row(\"\"\"{\"a\":1}\"\"\")))\n        checkAnswer(sql(\"select variant_get(v,'$.a','int') from t1\"), Seq(Row(1)))\n      }\n\n      // Ensure it also works when the variant is nested in a struct.\n      withTable(\"t2\") {\n        sql(\n          \"\"\"create table t2 (s struct<v variant>) using delta\n            |tblproperties (\n            |  'delta.columnMapping.mode' = 'name',\n            |  'delta.enableVariantShredding' = 'true'\n            |)\"\"\".stripMargin)\n        sql(\"\"\"insert into t2 (s) select named_struct('v', parse_json('{\"a\": 2}'))\"\"\")\n\n        checkAnswer(sql(\"select to_json(s) from t2\"), Seq(Row(\"\"\"{\"v\":{\"a\":2}}\"\"\")))\n        checkAnswer(sql(\"select to_json(s.v) from t2\"), Seq(Row(\"\"\"{\"a\":2}\"\"\")))\n        checkAnswer(sql(\"select variant_get(s.v, '$.a', 'int') from t2\"), Seq(Row(2)))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaWithNewTransactionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaTestUtils._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{DataFrame, Dataset, QueryTest}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.{ThreadUtils, Utils}\n\ntrait DeltaWithNewTransactionSuiteBase extends QueryTest\n  with SharedSparkSession\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite {\n\n  /**\n   * Test whether `withNewTransaction` captures all delta read made within it and correctly\n   * detects conflicts in transaction table and provides snapshot isolation for other table reads.\n   *\n   * The order in which the given thunks are executed is as follows.\n   * - Txn started using `withNewTransaction`. The following are executed while the txn is active.\n   * - currentThreadReadOp - Read operations performed in current thread.\n   * - concurrentUpdateOp - Update operations performed in different thread to\n   *                        simulate concurrent modification. This is synchronously completed\n   *                        before moving on.\n   * - currentThreadCommitOperation - Attempt to commit changes in the txn.\n   */\n  protected def testWithNewTransaction(\n      name: String,\n      partitionedTableKeys: Seq[Int],\n      preTxnSetup: DeltaLog => Unit = null,\n      currentThreadReadOp: DataFrame => Unit,\n      concurrentUpdateOp: String => Unit,\n      currentThreadCommitOperation: OptimisticTransaction => Unit,\n      shouldFail: Boolean,\n      confs: Map[String, String] = Map.empty,\n      partitionTablePath: String = Utils.createTempDir().getAbsolutePath): Unit = {\n\n    val tableName = \"NewTransactionTest\"\n    require(currentThreadCommitOperation != null)\n\n    import testImplicits._\n\n    test(s\"withNewTransaction - $name\") {\n      withSQLConf(confs.toSeq: _*) { withTable(tableName) {\n        sql(s\"CREATE TABLE NewTransactionTest(key int, value int) \" +\n          s\"USING delta partitioned by (key) LOCATION '$partitionTablePath'\")\n        partitionedTableKeys.toDS.select('value as \"key\", 'value)\n          .write.mode(\"append\").partitionBy(\"key\").format(\"delta\").saveAsTable(tableName)\n\n        val log = DeltaLog.forTable(spark, partitionTablePath)\n        assert(OptimisticTransaction.getActive().isEmpty, \"active txn already set\")\n\n        if (preTxnSetup != null) preTxnSetup(log)\n\n        log.withNewTransaction { txn =>\n          assert(OptimisticTransaction.getActive().nonEmpty, \"active txn not set\")\n\n          currentThreadReadOp(spark.table(tableName))\n\n          ThreadUtils.runInNewThread(s\"withNewTransaction test - $name\") {\n            concurrentUpdateOp(tableName)\n          }\n\n          if (shouldFail) {\n            intercept[DeltaConcurrentModificationException] { currentThreadCommitOperation(txn) }\n          } else {\n            currentThreadCommitOperation(txn)\n          }\n        }\n        assert(OptimisticTransaction.getActive().isEmpty, \"active txn not cleared\")\n      }}\n    }\n  }\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with no filters (i.e. full scan)\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 1\")\n    },\n    currentThreadCommitOperation = txn => {\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with partition filter + conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 1\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Concurrent delete op touches the same partition as those read in the active txn.\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"snapshot isolation for query that can leverage metadata query optimization\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 1\")\n    },\n    currentThreadCommitOperation = txn => {\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"snapshot isolation for query that can leverage metadata query optimization \" +\n      \"with partition filter + conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 1\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Concurrent delete op touches the same partition as those read in the active txn.\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with data filter + conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),  // will generate (key, value) = (1, 1), (2, 2), (3, 3)\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"value == 1\").count()  // pure data filter that touches one file\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 1\")  // deletes the one file read above\n    },\n    currentThreadCommitOperation = txn => {\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with partition filter + non-conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 2\")\n      sql(s\"INSERT INTO $txnTableName SELECT 4, 4\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Concurrent delete op touches the different files as those read in the active txn.\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = false)\n\n  testWithNewTransaction(\n    name = \"snapshot isolation for metadata optimizable query with partition filter +\" +\n      \" non-conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 2\")\n      sql(s\"INSERT INTO $txnTableName SELECT 4, 4\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Concurrent delete op touches the different files as those read in the active txn.\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = false)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with filter+limit and conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").limit(1).collect()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 1\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Concurrent delete op touches the same files as those read in the active txn.\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with filter+limit and non-conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").limit(1).collect()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE key = 2\")\n      sql(s\"INSERT INTO $txnTableName SELECT 4, 4\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Concurrent delete op touches the different files as those read in the active txn.\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = false)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table with limit + conflicting concurrent updates\",\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.limit(1).collect()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"DELETE FROM $txnTableName WHERE true\")\n    },\n    currentThreadCommitOperation = txn => {\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table even when limit pushdown is disabled\",\n    confs = Map(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key -> \"false\"),\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.limit(1).collect()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"UPDATE $txnTableName SET key = 2 WHERE key = 3\")\n    },\n    currentThreadCommitOperation = txn => {\n      // Any concurrent change (even if its seemingly non-conflicting) should fail the filter as\n      // the whole table will be scanned by the filter when data skipping is disabled\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n    },\n    shouldFail = true)\n\n  test(\"withNewTransaction - nesting withNewTransaction is not supported\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      log.withNewTransaction { txn =>\n        assert(OptimisticTransaction.getActive() === Some(txn))\n        intercept[IllegalStateException] {\n          log.withNewTransaction { txn2 => }\n        }\n        assert(OptimisticTransaction.getActive() === Some(txn))\n      }\n      assert(OptimisticTransaction.getActive().isEmpty)\n    }\n  }\n\n  test(\"withActiveTxn idempotency\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val txn = log.startTransaction()\n      assert(OptimisticTransaction.getActive().isEmpty)\n      OptimisticTransaction.withActive(txn) {\n        assert(OptimisticTransaction.getActive() === Some(txn))\n        OptimisticTransaction.withActive(txn) {\n          assert(OptimisticTransaction.getActive() === Some(txn))\n        }\n        assert(OptimisticTransaction.getActive() === Some(txn))\n\n        val txn2 = log.startTransaction()\n        intercept[IllegalStateException] {\n          OptimisticTransaction.withActive(txn2) { }\n        }\n        intercept[IllegalStateException] {\n          OptimisticTransaction.setActive(txn2)\n        }\n        assert(OptimisticTransaction.getActive() === Some(txn))\n      }\n      assert(OptimisticTransaction.getActive().isEmpty)\n    }\n  }\n\n  testWithNewTransaction(\n    name = \"capture reads on txn table even when data skipping is disabled\",\n    confs = Map(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> \"false\"),\n    partitionedTableKeys = Seq(1, 2, 3),\n    currentThreadReadOp = txnTable => {\n      txnTable.filter(\"key == 1\").count()\n    },\n    concurrentUpdateOp = txnTableName => {\n      sql(s\"UPDATE $txnTableName SET key = 2 WHERE key = 3\")\n    },\n    currentThreadCommitOperation = txn => {\n      // use physical name\n      val key = getPhysicalName(\"key\", txn.metadata.schema)\n      // Any concurrent change (even if its seemingly non-conflicting) should fail the filter as\n      // the whole table will be scanned by the filter when data skipping is disabled.\n      // Note: Adding a file to avoid snapshot isolation level for the commit.\n      txn.commit(\n        Seq(createTestAddFile(encodedPath = \"a\", partitionValues = Map(key -> \"2\"))),\n        DeltaOperations.ManualUpdate\n      )\n    },\n    shouldFail = true)\n\n  def testSnapshotIsolation(): Unit = {\n    val txnTablePath = Utils.createTempDir().getCanonicalPath\n    val nonTxnTablePath = Utils.createTempDir().getCanonicalPath\n\n    def txnTable: DataFrame = spark.read.format(\"delta\").load(txnTablePath)\n    def nonTxnTable: DataFrame = spark.read.format(\"delta\").load(nonTxnTablePath)\n\n    def writeToNonTxnTable(ds: Dataset[java.lang.Long]): Unit = {\n      import testImplicits._\n      ds.toDF(\"key\").select('key, 'key as \"value\")\n        .write.format(\"delta\").mode(\"append\").partitionBy(\"key\").save(nonTxnTablePath)\n      DeltaLog.forTable(spark, nonTxnTablePath).update(stalenessAcceptable = false)\n    }\n\n    testWithNewTransaction(\n      name = s\"snapshot isolation uses first-access snapshots when enabled\",\n      partitionTablePath = txnTablePath,\n      partitionedTableKeys = Seq(1, 2, 3, 4, 5),  // Prepare txn-table\n      preTxnSetup = _ => {\n        writeToNonTxnTable(spark.range(3))        // Prepare non-txn table\n      },\n      currentThreadReadOp = txnTable => {\n        // First read on tables\n        require(txnTable.count() == 5)\n        require(nonTxnTable.count() === 3)\n      },\n      concurrentUpdateOp = txnTableName => {\n        // Update tables in a different thread and make sure the DeltaLog gets updated\n        sql(s\"INSERT INTO $txnTableName SELECT 6, 6\")\n        DeltaLog.forTable(spark, txnTablePath).update(stalenessAcceptable = false)\n        require(txnTable.count() == 6)\n\n        writeToNonTxnTable(spark.range(3, 10))\n        require(nonTxnTable.count() == 10)\n      },\n      currentThreadCommitOperation = _ => {\n        // Second read on concurrently updated tables should read old snapshots\n        assert(txnTable.count() == 5, \"snapshot isolation failed on txn table\")\n        assert(nonTxnTable.count() == 3, \"snapshot isolation failed on non-txn table\")\n      },\n      shouldFail = false)\n  }\n\n  testSnapshotIsolation()\n}\n\nclass DeltaWithNewTransactionSuite extends DeltaWithNewTransactionSuiteBase\n\nclass DeltaWithNewTransactionIdColumnMappingSuite extends DeltaWithNewTransactionSuite\n  with DeltaColumnMappingEnableIdMode\n\nclass DeltaWithNewTransactionNameColumnMappingSuite extends DeltaWithNewTransactionSuite\n  with DeltaColumnMappingEnableNameMode\n\nclass DeltaWithNewTransactionWithCatalogOwnedBatch1Suite\n    extends DeltaWithNewTransactionSuite {\n  override val catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DeltaWithNewTransactionWithCatalogOwnedBatch2Suite\n    extends DeltaWithNewTransactionSuite {\n  override val catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DeltaWithNewTransactionWithCatalogOwnedBatch100Suite\n   extends DeltaWithNewTransactionSuite {\n  override val catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DeltaWriteConfigsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Locale\n\nimport scala.collection.mutable.ListBuffer\n\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StringType\n\n/**\n * This test suite tests all (or nearly-all) combinations of ways to write configs to a delta table.\n *\n * At a high level, it tests the following matrix of conditions:\n *\n * - DataFrameWriter or DataStreamWriter or DataFrameWriterV2 or DeltaTableBuilder or SQL API\n * X\n * - option is / is not prefixed with 'delta'\n * X\n * - using table name or table path\n * X\n * - CREATE or REPLACE or CREATE OR REPLACE (table already exists) OR CREATE OR REPLACE (table\n * doesn't already exist)\n *\n * At the end of the test suite, it prints out summary tables all of the cases above.\n */\nclass DeltaWriteConfigsSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  val config_no_prefix = \"dataSkippingNumIndexedCols\"\n  val config_no_prefix_value = \"33\"\n\n  val config_prefix = \"delta.deletedFileRetentionDuration\"\n  val config_prefix_value = \"interval 2 weeks\"\n\n  val config_no_prefix_2 = \"logRetentionDuration\"\n  val config_no_prefix_2_value = \"interval 60 days\"\n\n  val config_prefix_2 = \"delta.checkpointInterval\"\n  val config_prefix_2_value = \"20\"\n\n  override def afterAll(): Unit = {\n    import testImplicits._\n    // scalastyle:off println\n\n    println(\"DataFrameWriter Test Output\")\n    dfw_output.toSeq\n      .toDF(\"Output Location\", \"Output Mode\", s\"Contains No-Prefix Option\",\n        \"Contains Prefix-Option\", \"Config\")\n      .show(100, false)\n\n    println(\"DataStreamWriter Test Output\")\n    dsw_output.toSeq\n      .toDF(\"Output Location\", \"Output Mode\", s\"Contains No-Prefix Option\",\n        \"Contains Prefix-Option\", \"Config\")\n      .show(100, false)\n\n    println(\"DataFrameWriterV2 Test Output\")\n    dfw_v2_output.toSeq\n      .toDF(\"Output Location\", \"Output Mode\", s\"Contains No-Prefix Option\",\n        \"Contains Prefix-Option\", \"Config\")\n      .show(100, false)\n\n    println(\"DeltaTableBuilder Test Output\")\n    dtb_output.toSeq\n      .toDF(\"Output Location\", \"Output Mode\", s\"Contains No-Prefix Option (lowercase)\",\n        s\"Contains No-Prefix Option\", \"Contains Prefix-Option\", \"ERROR\", \"Config\")\n      .show(100, false)\n\n    println(\"SQL Test Output\")\n    sql_output.toSeq\n      .toDF(\"Output Location\", \"Config Input\", s\"SQL Operation\", \"AS SELECT\",\n        \"Contains OPTION no-prefix\", \"Contains OPTION prefix\", \"Contains TBLPROPERTIES no-prefix\",\n        \"Contains TBLPROPERTIES prefix\", \"Config\")\n      .show(100, false)\n\n    // scalastyle:on println\n    super.afterAll()\n  }\n\n\n  private val dfw_output = new ListBuffer[DeltaFrameStreamAPITestOutput]\n  private val dsw_output = new ListBuffer[DeltaFrameStreamAPITestOutput]\n  private val dfw_v2_output = new ListBuffer[DeltaFrameStreamAPITestOutput]\n  private val dtb_output = new ListBuffer[DeltaTableBuilderAPITestOutput]\n  private val sql_output = new ListBuffer[SQLAPIOutput]\n\n  // scalastyle:off line.size.limit\n  /*\n  DataFrameWriter Test Output\n  +---------------+-----------+-------------------------+----------------------+------------------------------------------------------+\n  |Output Location|Output Mode|Contains No-Prefix Option|Contains Prefix-Option|Config                                                |\n  +---------------+-----------+-------------------------+----------------------+------------------------------------------------------+\n  |path           |create     |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |path           |overwrite  |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |path           |append     |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |create     |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |overwrite  |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |append     |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  +---------------+-----------+-------------------------+----------------------+------------------------------------------------------+\n  */\n  // scalastyle:on line.size.limit\n  Seq(\"path\", \"table\").foreach { outputLoc =>\n    Seq(\"create\", \"overwrite\", \"append\").foreach { outputMode =>\n      val testName = s\"DataFrameWriter - outputLoc=$outputLoc & mode=$outputMode\"\n      test(testName) {\n        withTempDir { dir =>\n          withTable(\"tbl\") {\n            var data = spark.range(10).write.format(\"delta\")\n              .option(config_no_prefix, config_no_prefix_value)\n              .option(config_prefix, config_prefix_value)\n\n            if (outputMode != \"create\") {\n              data = data.mode(outputMode)\n            }\n\n            val log = outputLoc match {\n              case \"path\" =>\n                data.save(dir.getCanonicalPath)\n                DeltaLog.forTable(spark, dir)\n              case \"table\" =>\n                data.saveAsTable(\"tbl\")\n                DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n            }\n\n            val config = log.snapshot.metadata.configuration\n            val answer_no_prefix = config.contains(config_no_prefix)\n            val answer_prefix = config.contains(config_prefix)\n\n            assert(!answer_no_prefix)\n            assert(answer_prefix)\n            assert(config.size == 1)\n\n            dfw_output += DeltaFrameStreamAPITestOutput(\n              outputLocation = outputLoc,\n              outputMode = outputMode,\n              containsNoPrefixOption = answer_no_prefix,\n              containsPrefixOption = answer_prefix,\n              config = config.mkString(\",\")\n            )\n\n          }\n        }\n      }\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  /*\n  DataStreamWriter Test Output\n  +---------------+-----------+-------------------------+----------------------+------+\n  |Output Location|Output Mode|Contains No-Prefix Option|Contains Prefix-Option|Config|\n  +---------------+-----------+-------------------------+----------------------+------+\n  |path           |create     |false                    |false                 |      |\n  |path           |append     |false                    |false                 |      |\n  |path           |complete   |false                    |false                 |      |\n  |table          |create     |false                    |false                 |      |\n  |table          |append     |false                    |false                 |      |\n  |table          |complete   |false                    |false                 |      |\n  +---------------+-----------+-------------------------+----------------------+------+\n  */\n  // scalastyle:on line.size.limit\n  // Data source DeltaDataSource does not support Update output mode\n  Seq(\"path\", \"table\").foreach { outputLoc =>\n    Seq(\"create\", \"append\", \"complete\").foreach { outputMode =>\n      val testName = s\"DataStreamWriter - outputLoc=$outputLoc & outputMode=$outputMode\"\n      test(testName) {\n        withTempDir { dir =>\n          withTempDir { checkpointDir =>\n            withTable(\"src\", \"tbl\") {\n              spark.range(10).write.format(\"delta\").saveAsTable(\"src\")\n\n              var data = spark.readStream.format(\"delta\").table(\"src\")\n\n              // Needed to resolve error: Complete output mode not supported when there are no\n              // streaming aggregations on streaming DataFrames/Datasets\n              if (outputMode == \"complete\") {\n                data = data.groupBy().count()\n              }\n\n              var stream = data.writeStream\n                .format(\"delta\")\n                .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n                .option(config_no_prefix, config_no_prefix_value)\n                .option(config_prefix, config_prefix_value)\n\n              if (outputMode != \"create\") {\n                stream = stream.outputMode(outputMode)\n              }\n\n              val log = outputLoc match {\n                case \"path\" =>\n                  stream.start(dir.getCanonicalPath).stop()\n                  DeltaLog.forTable(spark, dir)\n                case \"table\" =>\n                  stream.toTable(\"tbl\").stop()\n                  DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n              }\n\n              val config = log.snapshot.metadata.configuration\n              val answer_no_prefix = config.contains(config_no_prefix)\n              val answer_prefix = config.contains(config_prefix)\n\n              assert(config.isEmpty)\n              assert(!answer_no_prefix)\n              assert(!answer_prefix)\n\n              dsw_output += DeltaFrameStreamAPITestOutput(\n                outputLocation = outputLoc,\n                outputMode = outputMode,\n                containsNoPrefixOption = answer_no_prefix,\n                containsPrefixOption = answer_prefix,\n                config = config.mkString(\",\")\n              )\n\n            }\n          }\n        }\n      }\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  /*\n  DataFrameWriterV2 Test Output\n  +---------------+--------------+-------------------------+----------------------+------------------------------------------------------+\n  |Output Location|Output Mode   |Contains No-Prefix Option|Contains Prefix-Option|Config                                                |\n  +---------------+--------------+-------------------------+----------------------+------------------------------------------------------+\n  |path           |create        |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |path           |replace       |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |path           |c_or_r_create |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |path           |c_or_r_replace|false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |create        |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |replace       |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |c_or_r_create |false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |c_or_r_replace|false                    |true                  |delta.deletedFileRetentionDuration -> interval 2 weeks|\n  +---------------+--------------+-------------------------+----------------------+------------------------------------------------------+\n  */\n  // scalastyle:on line.size.limit\n  Seq(\"path\", \"table\").foreach { outputLoc =>\n    Seq(\"create\", \"replace\", \"c_or_r_create\", \"c_or_r_replace\").foreach { outputMode =>\n      val testName = s\"DataFrameWriterV2 - outputLoc=$outputLoc & outputMode=$outputMode\"\n      test(testName) {\n        withTempDir { dir =>\n          withTable(\"tbl\") {\n            val table = outputLoc match {\n              case \"path\" => s\"delta.`${dir.getCanonicalPath}`\"\n              case \"table\" => \"tbl\"\n            }\n\n            val data = spark.range(10).writeTo(table).using(\"delta\")\n              .option(config_no_prefix, config_no_prefix_value)\n              .option(config_prefix, config_prefix_value)\n\n            if (outputMode.contains(\"replace\")) {\n              spark.range(100).writeTo(table).using(\"delta\").create()\n            }\n\n            outputMode match {\n              case \"create\" => data.create()\n              case \"replace\" => data.replace()\n              case \"c_or_r_create\" | \"c_or_r_replace\" => data.createOrReplace()\n            }\n\n            val log = outputLoc match {\n              case \"path\" => DeltaLog.forTable(spark, dir)\n              case \"table\" => DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n            }\n\n            val config = log.snapshot.metadata.configuration\n\n            val answer_no_prefix = config.contains(config_no_prefix)\n            val answer_prefix = config.contains(config_prefix)\n\n            assert(!answer_no_prefix)\n            assert(answer_prefix)\n            assert(config.size == 1)\n\n            dfw_v2_output += DeltaFrameStreamAPITestOutput(\n              outputLocation = outputLoc,\n              outputMode = outputMode,\n              containsNoPrefixOption = answer_no_prefix,\n              containsPrefixOption = answer_prefix,\n              config = config.mkString(\",\")\n            )\n\n          }\n        }\n      }\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  /*\n  DeltaTableBuilder Test Output\n  +---------------+--------------+-------------------------------------+-------------------------+----------------------+-----+---------------------------------------------------------------------------------------+\n  |Output Location|Output Mode   |Contains No-Prefix Option (lowercase)|Contains No-Prefix Option|Contains Prefix-Option|ERROR|Config                                                                                 |\n  +---------------+--------------+-------------------------------------+-------------------------+----------------------+-----+---------------------------------------------------------------------------------------+\n  |path           |create        |true                                 |false                    |true                  |false|delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33|\n  |path           |replace       |true                                 |false                    |true                  |false|delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33|\n  |path           |c_or_r_create |true                                 |false                    |true                  |false|delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33|\n  |path           |c_or_r_replace|false                                |false                    |false                 |true |                                                                                       |\n  |table          |create        |true                                 |false                    |true                  |false|dataSkippingNumIndexedCols -> 33,delta.deletedFileRetentionDuration -> interval 2 weeks|\n  |table          |replace       |true                                 |false                    |true                  |false|delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33|\n  |table          |c_or_r_create |true                                 |false                    |true                  |false|delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33|\n  |table          |c_or_r_replace|true                                 |false                    |true                  |false|delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33|\n  +---------------+--------------+-------------------------------------+-------------------------+----------------------+-----+---------------------------------------------------------------------------------------+\n  */\n  // scalastyle:on line.size.limit\n  Seq(\"path\", \"table\").foreach { outputLoc =>\n    Seq(\"create\", \"replace\", \"c_or_r_create\", \"c_or_r_replace\").foreach { outputMode =>\n      val testName = s\"DeltaTableBuilder - outputLoc=$outputLoc & outputMode=$outputMode\"\n      test(testName) {\n        withTempDir { dir =>\n          withTable(\"tbl\") {\n\n            if (outputMode.contains(\"replace\")) {\n              outputLoc match {\n                case \"path\" =>\n                  io.delta.tables.DeltaTable.create()\n                    .addColumn(\"bar\", StringType).location(dir.getCanonicalPath).execute()\n                case \"table\" =>\n                  io.delta.tables.DeltaTable.create()\n                    .addColumn(\"bar\", StringType).tableName(\"tbl\").execute()\n              }\n            }\n\n            var tblBuilder = outputMode match {\n              case \"create\" =>\n                io.delta.tables.DeltaTable.create()\n              case \"replace\" =>\n                io.delta.tables.DeltaTable.replace()\n              case \"c_or_r_create\" | \"c_or_r_replace\" =>\n                io.delta.tables.DeltaTable.createOrReplace()\n            }\n\n            tblBuilder.addColumn(\"foo\", StringType)\n            tblBuilder = tblBuilder.property(config_no_prefix, config_no_prefix_value)\n            tblBuilder = tblBuilder.property(config_prefix, config_prefix_value)\n\n            val log = (outputLoc, outputMode) match {\n              case (\"path\", \"c_or_r_replace\") =>\n                intercept[DeltaAnalysisException] {\n                  tblBuilder.location(dir.getCanonicalPath).execute()\n                }\n                null\n              case (\"path\", _) =>\n                tblBuilder.location(dir.getCanonicalPath).execute()\n                DeltaLog.forTable(spark, dir)\n              case (\"table\", _) =>\n                tblBuilder.tableName(\"tbl\").execute()\n                DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n            }\n\n            log match {\n              case null =>\n                // CREATE OR REPLACE seems broken when using path and the table already exists\n                // with a different schema.\n                // DeltaAnalysisException: The specified schema does not match the existing schema\n                // ...\n                // Specified schema is missing field(s): bar\n                // Specified schema has additional field(s): foo\n                assert(outputLoc == \"path\" && outputMode == \"c_or_r_replace\")\n                dtb_output += DeltaTableBuilderAPITestOutput(\n                  outputLocation = outputLoc,\n                  outputMode = outputMode,\n                  containsNoPrefixOptionLowerCase = false,\n                  containsNoPrefixOption = false,\n                  containsPrefixOption = false,\n                  error = true,\n                  config = \"\"\n                )\n              case _ =>\n                val config = log.snapshot.metadata.configuration\n\n                val answer_no_prefix_lowercase =\n                  config.contains(config_no_prefix.toLowerCase(Locale.ROOT))\n                val answer_no_prefix = config.contains(config_no_prefix)\n                val answer_prefix = config.contains(config_prefix)\n\n                assert(!answer_no_prefix_lowercase)\n                assert(answer_no_prefix)\n                assert(answer_prefix)\n                assert(config.size == 2)\n\n                dtb_output += DeltaTableBuilderAPITestOutput(\n                  outputLocation = outputLoc,\n                  outputMode = outputMode,\n                  containsNoPrefixOptionLowerCase = answer_no_prefix_lowercase,\n                  containsNoPrefixOption = answer_no_prefix,\n                  containsPrefixOption = answer_prefix,\n                  error = false,\n                  config = config.mkString(\",\")\n                )\n            }\n          }\n        }\n      }\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  /*\n  SQL Test Output\n  +---------------+-------------------------+--------------+---------+-------------------------+----------------------+--------------------------------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n  |Output Location|Config Input             |SQL Operation |AS SELECT|Contains OPTION no-prefix|Contains OPTION prefix|Contains TBLPROPERTIES no-prefix|Contains TBLPROPERTIES prefix|Config                                                                                                                                                                                                                                                               |\n  +---------------+-------------------------+--------------+---------+-------------------------+----------------------+--------------------------------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n  |path           |options                  |create        |true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |options                  |create        |false    |true                     |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33,option.delta.deletedFileRetentionDuration -> interval 2 weeks,option.dataSkippingNumIndexedCols -> 33                                                                        |\n  |path           |options                  |replace       |true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |options                  |replace       |false    |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |options                  |c_or_r_create |true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |options                  |c_or_r_create |false    |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |options                  |c_or_r_replace|true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |options                  |c_or_r_replace|false    |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |path           |tblproperties            |create        |true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |create        |false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |replace       |true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |replace       |false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |c_or_r_create |true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |c_or_r_create |false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |c_or_r_replace|true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |tblproperties            |c_or_r_replace|false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |path           |options_and_tblproperties|create        |true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |path           |options_and_tblproperties|create        |false    |true                     |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20,option.delta.deletedFileRetentionDuration -> interval 2 weeks,option.dataSkippingNumIndexedCols -> 33|\n  |path           |options_and_tblproperties|replace       |true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |path           |options_and_tblproperties|replace       |false    |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |path           |options_and_tblproperties|c_or_r_create |true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |path           |options_and_tblproperties|c_or_r_create |false    |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |path           |options_and_tblproperties|c_or_r_replace|true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |path           |options_and_tblproperties|c_or_r_replace|false    |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options                  |create        |true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |options                  |create        |false    |true                     |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33,option.delta.deletedFileRetentionDuration -> interval 2 weeks,option.dataSkippingNumIndexedCols -> 33                                                                        |\n  |table          |options                  |replace       |true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |options                  |replace       |false    |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |options                  |c_or_r_create |true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |options                  |c_or_r_create |false    |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |options                  |c_or_r_replace|true     |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |options                  |c_or_r_replace|false    |false                    |true                  |N/A                             |N/A                          |delta.deletedFileRetentionDuration -> interval 2 weeks                                                                                                                                                                                                               |\n  |table          |tblproperties            |create        |true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |create        |false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |replace       |true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |replace       |false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |c_or_r_create |true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |c_or_r_create |false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |c_or_r_replace|true     |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |tblproperties            |c_or_r_replace|false    |N/A                      |N/A                   |true                            |true                         |logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                                                                              |\n  |table          |options_and_tblproperties|create        |true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options_and_tblproperties|create        |false    |true                     |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,dataSkippingNumIndexedCols -> 33,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20,option.delta.deletedFileRetentionDuration -> interval 2 weeks,option.dataSkippingNumIndexedCols -> 33|\n  |table          |options_and_tblproperties|replace       |true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options_and_tblproperties|replace       |false    |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options_and_tblproperties|c_or_r_create |true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options_and_tblproperties|c_or_r_create |false    |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options_and_tblproperties|c_or_r_replace|true     |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  |table          |options_and_tblproperties|c_or_r_replace|false    |false                    |true                  |true                            |true                         |delta.deletedFileRetentionDuration -> interval 2 weeks,logRetentionDuration -> interval 60 days,delta.checkpointInterval -> 20                                                                                                                                       |\n  +---------------+-------------------------+--------------+---------+-------------------------+----------------------+--------------------------------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n  */\n  // scalastyle:on line.size.limit\n  Seq(\"path\", \"table\").foreach { outputLoc =>\n    Seq(\"options\", \"tblproperties\", \"options_and_tblproperties\").foreach { configInput =>\n      Seq(\"create\", \"replace\", \"c_or_r_create\", \"c_or_r_replace\").foreach { sqlOp =>\n        Seq(true, false).foreach { useAsSelectStmt =>\n          val testName = s\"SQL - outputLoc=$outputLoc & configInput=$configInput & sqlOp=$sqlOp\" +\n            s\" & useAsSelectStmt=$useAsSelectStmt\"\n\n          test(testName) {\n            withTempDir { dir =>\n              withTable(\"tbl\", \"other\") {\n                if (sqlOp.contains(\"replace\")) {\n                  var stmt = \"CREATE TABLE tbl (ID INT) USING DELTA\"\n                  if (outputLoc == \"path\") {\n                    stmt = stmt + s\" LOCATION '${dir.getCanonicalPath}'\"\n                  }\n                  sql(stmt)\n                }\n\n                val sqlOpStr = sqlOp match {\n                  case \"c_or_r_create\" | \"c_or_r_replace\" => \"CREATE OR REPLACE\"\n                  case _ => sqlOp.toUpperCase(Locale.ROOT)\n                }\n\n                val schemaStr = if (useAsSelectStmt) \"\" else \"(id INT) \"\n                var stmt = sqlOpStr + \" TABLE tbl \" + schemaStr + \"USING DELTA\\n\"\n\n                if (configInput.contains(\"options\")) {\n                  stmt = stmt + s\"OPTIONS(\" +\n                    s\"'$config_no_prefix'=$config_no_prefix_value,\" +\n                    s\"'$config_prefix'='$config_prefix_value')\\n\"\n                }\n                if (outputLoc == \"path\") {\n                  stmt = stmt + s\"LOCATION '${dir.getCanonicalPath}'\\n\"\n                }\n                if (configInput.contains(\"tblproperties\")) {\n                  stmt = stmt + s\"TBLPROPERTIES(\" +\n                    s\"'$config_no_prefix_2'='$config_no_prefix_2_value',\" +\n                    s\"'$config_prefix_2'=$config_prefix_2_value)\\n\"\n                }\n                if (useAsSelectStmt) {\n                  sql(\"CREATE TABLE other (id INT) USING DELTA\")\n                  stmt = stmt + \"AS SELECT * FROM other\\n\"\n                }\n\n                // scalastyle:off println\n                println(stmt)\n                // scalastyle:on println\n\n                sql(stmt)\n\n                val log = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n                val config = log.snapshot.metadata.configuration\n\n                val option_was_set = configInput.contains(\"options\")\n                val tblproperties_was_set = configInput.contains(\"tblproperties\")\n\n                val option_no_prefix = config.contains(config_no_prefix)\n                val option_prefix = config.contains(config_prefix)\n                val tblproperties_no_prefix = config.contains(config_no_prefix_2)\n                val tblproperties_prefix = config.contains(config_prefix_2)\n\n                var expectedSize = 0\n                if (option_was_set) {\n                  assert(option_prefix)\n                  expectedSize += 1\n                  if (sqlOp == \"create\" && !useAsSelectStmt) {\n                    assert(option_no_prefix)\n                    assert(config.contains(s\"option.$config_prefix\"))\n                    assert(config.contains(s\"option.$config_no_prefix\"))\n                    expectedSize += 3\n                  }\n                }\n                if (tblproperties_was_set) {\n                  assert(tblproperties_prefix)\n                  assert(tblproperties_no_prefix)\n                  expectedSize += 2\n                }\n\n                assert(config.size == expectedSize)\n\n                sql_output += SQLAPIOutput(\n                  outputLoc,\n                  configInput,\n                  sqlOp,\n                  useAsSelectStmt,\n                  if (option_was_set) option_no_prefix.toString else \"N/A\",\n                  if (option_was_set) option_prefix.toString else \"N/A\",\n                  if (tblproperties_was_set) tblproperties_no_prefix.toString else \"N/A\",\n                  if (tblproperties_was_set) tblproperties_prefix.toString else \"N/A\",\n                  config.mkString(\",\")\n                )\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n// Need to be outside to be stable references for Spark to generate the case classes\ncase class DeltaFrameStreamAPITestOutput(\n    outputLocation: String,\n    outputMode: String,\n    containsNoPrefixOption: Boolean,\n    containsPrefixOption: Boolean,\n    config: String)\n\ncase class DeltaTableBuilderAPITestOutput(\n    outputLocation: String,\n    outputMode: String,\n    containsNoPrefixOptionLowerCase: Boolean,\n    containsNoPrefixOption: Boolean,\n    containsPrefixOption: Boolean,\n    error: Boolean,\n    config: String)\n\ncase class SQLAPIOutput(\n    outputLocation: String,\n    confiInput: String,\n    sqlOperation: String,\n    asSelect: Boolean,\n    containsOptionNoPrefix: String,\n    containsOptionPrefix: String,\n    containsTblPropertiesNoPrefix: String,\n    containsTblPropertiesPrefix: String,\n    config: String)\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DescribeDeltaDetailSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.io.FileNotFoundException\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.connector.catalog.CatalogManager.SESSION_CATALOG_NAME\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\ntrait DescribeDeltaDetailSuiteBase extends QueryTest\n  with SharedSparkSession\n  with CatalogOwnedTestBaseSuite\n  with DeltaTestUtilsForTempViews {\n\n  import testImplicits._\n\n  val catalogAndSchema = {\n    s\"$SESSION_CATALOG_NAME.default.\"\n  }\n\n  protected def checkResult(\n    result: DataFrame,\n    expected: Seq[Any],\n    columns: Seq[String]): Unit = {\n    checkAnswer(\n      result.select(columns.head, columns.tail: _*),\n      Seq(Row(expected: _*))\n    )\n  }\n\n  def describeDeltaDetailTest(f: File => String): Unit = {\n    val tempDir = Utils.createTempDir()\n    Seq(1 -> 1).toDF(\"column1\", \"column2\")\n      .write\n      .format(\"delta\")\n      .partitionBy(\"column1\")\n      .save(tempDir.toString())\n\n    // Check SQL details\n    checkResult(\n      sql(s\"DESCRIBE DETAIL ${f(tempDir)}\"),\n      Seq(\"delta\", Array(\"column1\"), 1),\n      Seq(\"format\", \"partitionColumns\", \"numFiles\"))\n\n    // Check Scala details\n    val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.toString)\n    checkResult(\n      deltaTable.detail(),\n      Seq(\"delta\", Array(\"column1\"), 1),\n      Seq(\"format\", \"partitionColumns\", \"numFiles\"))\n  }\n\n  test(\"delta table: Scala details using table name\") {\n    withTable(\"delta_test\") {\n      Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"delta_test\")\n\n      val deltaTable = io.delta.tables.DeltaTable.forName(spark, \"delta_test\")\n      checkAnswer(\n        deltaTable.detail().select(\"format\"),\n        Seq(Row(\"delta\"))\n      )\n    }\n  }\n\n  test(\"delta table: path\") {\n    describeDeltaDetailTest(f => s\"'${f.toString()}'\")\n  }\n\n  test(\"delta table: delta table identifier\") {\n    describeDeltaDetailTest(f => s\"delta.`${f.toString()}`\")\n  }\n\n  test(\"non-delta table: SQL details using table name\") {\n    withTable(\"describe_detail\") {\n      sql(\n        \"\"\"\n          |CREATE TABLE describe_detail(column1 INT, column2 INT)\n          |USING parquet\n          |PARTITIONED BY (column1)\n          |COMMENT \"this is a table comment\"\n        \"\"\".stripMargin)\n      sql(\n        \"\"\"\n          |INSERT INTO describe_detail VALUES(1, 1)\n        \"\"\".stripMargin\n      )\n      checkResult(\n        sql(\"DESCRIBE DETAIL describe_detail\"),\n        Seq(\"parquet\", Array(\"column1\")),\n        Seq(\"format\", \"partitionColumns\"))\n    }\n  }\n\n  test(\"non-delta table: SQL details using table path\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq(1 -> 1).toDF(\"column1\", \"column2\")\n      .write\n      .format(\"parquet\")\n      .partitionBy(\"column1\")\n      .mode(\"overwrite\")\n      .save(tempDir)\n    checkResult(\n      sql(s\"DESCRIBE DETAIL '$tempDir'\"),\n      Seq(tempDir),\n      Seq(\"location\"))\n  }\n\n  test(\"non-delta table: SQL details when table path doesn't exist\") {\n    val tempDir = Utils.createTempDir()\n    tempDir.delete()\n    val e = intercept[FileNotFoundException] {\n      sql(s\"DESCRIBE DETAIL '$tempDir'\")\n    }\n    assert(e.getMessage.contains(tempDir.toString))\n  }\n\n  test(\"delta table: SQL details using table name\") {\n    withTable(\"describe_detail\") {\n      sql(\n        \"\"\"\n          |CREATE TABLE describe_detail(column1 INT, column2 INT)\n          |USING delta\n          |PARTITIONED BY (column1)\n          |COMMENT \"describe a non delta table\"\n        \"\"\".stripMargin)\n      sql(\n        \"\"\"\n          |INSERT INTO describe_detail VALUES(1, 1)\n        \"\"\".stripMargin\n      )\n      checkResult(\n        sql(\"DESCRIBE DETAIL describe_detail\"),\n        Seq(\"delta\", Array(\"column1\"), 1),\n        Seq(\"format\", \"partitionColumns\", \"numFiles\"))\n    }\n  }\n\n  test(\"delta table: create table on an existing delta log\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq(1 -> 1).toDF(\"column1\", \"column2\")\n      .write\n      .format(\"delta\")\n      .partitionBy(\"column1\")\n      .mode(\"overwrite\")\n      .save(tempDir)\n    val tblName1 = \"tbl_name1\"\n    val tblName2 = \"tbl_name2\"\n    withTable(tblName1, tblName2) {\n      sql(s\"CREATE TABLE $tblName1 USING DELTA LOCATION '$tempDir'\")\n      sql(s\"CREATE TABLE $tblName2 USING DELTA LOCATION '$tempDir'\")\n      checkResult(\n        sql(s\"DESCRIBE DETAIL $tblName1\"),\n        Seq(s\"$catalogAndSchema$tblName1\"),\n        Seq(\"name\"))\n      checkResult(\n        sql(s\"DESCRIBE DETAIL $tblName2\"),\n        Seq(s\"$catalogAndSchema$tblName2\"),\n        Seq(\"name\"))\n      checkResult(\n        sql(s\"DESCRIBE DETAIL delta.`$tempDir`\"),\n        Seq(null),\n        Seq(\"name\"))\n      checkResult(\n        sql(s\"DESCRIBE DETAIL '$tempDir'\"),\n        Seq(null),\n        Seq(\"name\"))\n    }\n  }\n\n  testWithTempView(s\"SC-37296: describe detail on temp view\") { isSQLTempView =>\n    withTable(\"t1\") {\n      Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"t1\")\n      val viewName = \"v\"\n      createTempViewFromTable(\"t1\", isSQLTempView)\n      val e = intercept[AnalysisException] {\n        sql(s\"DESCRIBE DETAIL $viewName\")\n      }\n      assert(e.getMessage.contains(\"'DESCRIBE DETAIL' expects a table\"))\n    }\n  }\n\n  test(\"SC-37296: describe detail on permanent view\") {\n    val view = \"detailTestView\"\n    withView(view) {\n      sql(s\"CREATE VIEW $view AS SELECT 1\")\n      val e = intercept[AnalysisException] { sql(s\"DESCRIBE DETAIL $view\") }\n      assert(e.getMessage.contains(\"'DESCRIBE DETAIL' expects a table\"))\n    }\n  }\n\n  test(\"delta table: describe detail always run on the latest snapshot\") {\n    val tableName = \"tbl_name_on_latest_snapshot\"\n    withTable(tableName) {\n        val tempDir = Utils.createTempDir().toString\n        sql(s\"CREATE TABLE $tableName USING DELTA LOCATION '$tempDir'\")\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        DeltaLog.clearCache()\n\n        // Cache a new DeltaLog\n        sql(s\"DESCRIBE DETAIL $tableName\")\n\n        val txn = deltaLog.startTransaction()\n        val metadata = txn.snapshot.metadata\n        val newMetadata = metadata.copy(configuration =\n          metadata.configuration ++ Map(\"foo\" -> \"bar\")\n        )\n        txn.commit(newMetadata :: Nil, DeltaOperations.ManualUpdate)\n        val catalogOwnedProperties = constructCatalogOwnedSpecificTableProperties(\n          spark, newMetadata)\n        checkResult(sql(s\"DESCRIBE DETAIL $tableName\"),\n          Seq(Map(\"foo\" -> \"bar\") ++ catalogOwnedProperties),\n          Seq(\"properties\")\n        )\n      }\n  }\n\n  test(\"delta table: describe detail shows table features\") {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"CatalogOwned is not compatible w/ the test since protocol version would be \" +\n        \"set to (3, 7) by default for CC tables.\")\n    }\n    withTable(\"t1\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n        DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\"\n      ) {\n        Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"t1\")\n      }\n      val p = DeltaLog.forTable(spark, TableIdentifier(\"t1\")).snapshot.protocol\n\n      checkResult(\n        sql(s\"DESCRIBE DETAIL t1\"),\n        Seq(\n          p.minReaderVersion,\n          p.minWriterVersion,\n          p.implicitlySupportedFeatures.map(_.name).toArray.sorted),\n        Seq(\"minReaderVersion\", \"minWriterVersion\", \"tableFeatures\"))\n\n      val features = p.readerAndWriterFeatureNames ++ p.implicitlySupportedFeatures.map(_.name)\n      sql(s\"\"\"ALTER TABLE t1 SET TBLPROPERTIES (\n             |  delta.minReaderVersion = $TABLE_FEATURES_MIN_READER_VERSION,\n             |  delta.minWriterVersion = $TABLE_FEATURES_MIN_WRITER_VERSION,\n             |  delta.feature.${TestReaderWriterFeature.name} = 'enabled'\n             |)\"\"\".stripMargin)\n\n      checkResult(\n        sql(s\"DESCRIBE DETAIL t1\"),\n        Seq(\n          TABLE_FEATURES_MIN_READER_VERSION,\n          TABLE_FEATURES_MIN_WRITER_VERSION,\n          (features + TestReaderWriterFeature.name).toArray.sorted),\n        Seq(\"minReaderVersion\", \"minWriterVersion\", \"tableFeatures\"))\n    }\n  }\n\n  test(\"describe detail contains table name\") {\n    val tblName = \"test_table\"\n    withTable(tblName) {\n      spark.sql(s\"CREATE TABLE $tblName(id INT) USING delta\")\n      val deltaTable = io.delta.tables.DeltaTable.forName(tblName)\n      checkResult(\n        deltaTable.detail(),\n        Seq(s\"$catalogAndSchema$tblName\"),\n        Seq(\"name\")\n      )\n    }\n  }\n\n  private def withTempTableOrDir(useTable: Boolean = true)(f: String => Unit): Unit = {\n    if (useTable) {\n      val testTable = \"test_table\"\n      withTable(testTable) {\n        f(testTable)\n      }\n    } else {\n      withTempDir { dir =>\n        f(s\"delta.`$dir`\")\n      }\n    }\n  }\n\n  private def checkResultForClusteredTable(\n      table: String,\n      clusteringColumns: Array[String]): Unit = {\n    // Check SQL API.\n    checkResult(\n      sql(s\"DESCRIBE DETAIL $table\"),\n      Seq(\"delta\", Array.empty, clusteringColumns, 0),\n      Seq(\"format\", \"partitionColumns\", \"clusteringColumns\", \"numFiles\"))\n\n    // Check DeltaTable APIs.\n    val isPathBased = table.startsWith(\"delta.\")\n    val deltaTable = if (isPathBased) {\n      val path = table.replace(\"delta.`\", \"\").dropRight(1)\n      io.delta.tables.DeltaTable.forPath(path)\n    } else {\n      io.delta.tables.DeltaTable.forName(table)\n    }\n    checkResult(\n      deltaTable.detail(),\n      Seq(\"delta\", Array.empty, clusteringColumns, 0),\n      Seq(\"format\", \"partitionColumns\", \"clusteringColumns\", \"numFiles\"))\n  }\n\n  Seq(true -> \"\", false -> \" - path based\").foreach { case (useTable, testSuffix) =>\n    test(s\"describe liquid table$testSuffix\") {\n      withTempTableOrDir(useTable) { testTable =>\n        sql(s\"CREATE TABLE $testTable(a STRUCT<b INT, c STRING>, d INT) USING DELTA \" +\n          \"CLUSTER BY (a.b, d)\")\n\n        checkResultForClusteredTable(testTable, Array(\"a.b\", \"d\"))\n\n        sql(s\"ALTER TABLE $testTable CLUSTER BY NONE\")\n\n        checkResultForClusteredTable(testTable, Array.empty)\n      }\n    }\n\n    test(s\"describe liquid table - column mapping$testSuffix\") {\n      withTempTableOrDir(useTable) { testTable =>\n        sql(s\"CREATE TABLE $testTable (col1 STRING, col2 INT) USING delta CLUSTER BY (col1, col2)\")\n        sql(s\"ALTER TABLE $testTable SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name',\" +\n          \"'delta.minReaderVersion' = '3',\" +\n          \"'delta.minWriterVersion'= '7')\")\n        sql(s\"ALTER TABLE $testTable RENAME COLUMN col2 TO new_col_name\")\n\n        checkResultForClusteredTable(testTable, Array(\"col1\", \"new_col_name\"))\n      }\n    }\n  }\n\n  // TODO: run it with OSS Delta after it's supported\n}\n\nclass DescribeDeltaDetailSuite\n  extends DescribeDeltaDetailSuiteBase with DeltaSQLCommandTest\n\nclass DescribeDeltaDetailWithCatalogOwnedBatch1Suite extends DescribeDeltaDetailSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DescribeDeltaDetailWithCatalogOwnedBatch2Suite extends DescribeDeltaDetailSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DescribeDeltaDetailWithCatalogOwnedBatch100Suite extends DescribeDeltaDetailSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DescribeDeltaHistorySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\n\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile, Metadata, Protocol, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.DescribeDeltaHistoryCommand\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.scalactic.source.Position\nimport org.scalatest.Tag\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, Column, DataFrame, QueryTest, Row, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{BooleanType, LongType, MapType, StringType, StructField, StructType, TimestampType}\nimport org.apache.spark.util.Utils\n\ntrait DescribeDeltaHistorySuiteBase\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaTestUtilsForTempViews\n  with MergeIntoMetricsBase\n  with CatalogOwnedTestBaseSuite\n  with WriteOptionsTestBase {\n\n  import testImplicits._\n\n  protected val evolvabilityResource = {\n    new File(\"src/test/resources/delta/history/delta-0.2.0\").getAbsolutePath()\n  }\n\n  protected val evolvabilityLastOp = Seq(\"STREAMING UPDATE\", null, null)\n\n  protected def deleteMetricsSchema(partitioned: Boolean) =\n    if (partitioned) DeltaOperationMetrics.DELETE_PARTITIONS else DeltaOperationMetrics.DELETE\n\n  protected val updateMetricsSchema = DeltaOperationMetrics.UPDATE\n  protected val mergeMetricsSchema = DeltaOperationMetrics.MERGE\n  protected val replaceWhereMetricsSchema = DeltaOperationMetrics.WRITE_REPLACE_WHERE\n\n  protected def testWithFlag(name: String, tags: Tag*)(f: => Unit): Unit = {\n    test(name, tags: _*) {\n        f\n    }\n  }\n\n  protected def checkLastOperation(\n      basePath: String,\n      expectedOperationParameters: Seq[String],\n      expectedColVals: Seq[String],\n      columns: Seq[Column] = Seq($\"operation\", $\"operationParameters.mode\"),\n      removeExpressionId: Boolean = false): Unit = {\n    var df = io.delta.tables.DeltaTable.forPath(spark, basePath).history(1)\n\n    val operationParametersRow = df.select(\"operationParameters\").collect()(0)\n    assert(operationParametersRow.getAs[Map[String, String]](0).keys.toSeq\n      === expectedOperationParameters)\n\n    df = df.select(columns: _*)\n    if (removeExpressionId) {\n      // As the expression ID is written as part of the column predicate (in the form of col#expId)\n      // but it is non-deterministic, we remove it here so that any comparison can just go against\n      // the column name\n      df = df.withColumn(\"predicate\", regexp_replace(col(\"predicate\"), \"#[0-9]+\", \"\"))\n    }\n    checkAnswer(df, Seq(Row(expectedColVals: _*)))\n    df = spark.sql(s\"DESCRIBE HISTORY delta.`$basePath` LIMIT 1\")\n    df = df.select(columns: _*)\n    if (removeExpressionId) {\n      df = df.withColumn(\"predicate\", regexp_replace(col(\"predicate\"), \"#[0-9]+\", \"\"))\n    }\n    checkAnswer(df, Seq(Row(expectedColVals: _*)))\n  }\n\n  /**\n   * a separate check on properties is needed because order inside properties\n   * is determined by order in Map and can differ between scala versions\n   * Thus, we want to make sure check on properties can ignore orders and\n   * check if all (key, value) property-pairs are expected\n   */\n  protected def checkLastOperationProperties(\n      basePath: String, expectedProperties: Map[String, String]): Unit = {\n    def checkFirstRowPropertyCol(df: DataFrame): Unit = {\n      val propertyDf = df.select(Seq($\"operationParameters.properties\"): _*)\n      val actualPropertiesJson = propertyDf.take(1).head.getString(0)\n      val actualProperties = JsonUtils.fromJson[Map[String, String]](actualPropertiesJson)\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        // We need to filter out the following two properties b/c\n        // they are generated as part of [[RowTrackingFeature]] enablement,\n        // the values of which are non-deterministic so we only verify the\n        // existence.\n        assert(actualProperties.contains(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP) &&\n          actualProperties.contains(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP),\n          \"RowTracking should be enabled as part of CatalogOwned QoL features, \" +\n          s\"expecting ${MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP} and \" +\n          s\"${MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP} to be present. \" +\n          s\"The `actualProperties`: $actualProperties\")\n        val actualPropertiesForCO = actualProperties.filterNot { case (k, v) =>\n          k == MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP ||\n            k == MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP\n        }\n        assert(actualPropertiesForCO == expectedProperties)\n      } else {\n        assert(actualProperties == expectedProperties)\n      }\n    }\n    var df = io.delta.tables.DeltaTable.forPath(spark, basePath).history(1)\n    checkFirstRowPropertyCol(df)\n    // double verification\n    df = spark.sql(s\"DESCRIBE HISTORY delta.`$basePath` LIMIT 1\")\n    checkFirstRowPropertyCol(df)\n  }\n\n  protected def checkOperationMetrics(\n      expectedMetrics: Map[String, String],\n      operationMetrics: Map[String, String],\n      metricsSchema: Set[String]): Unit = {\n    if (metricsSchema != operationMetrics.keySet) {\n      fail(\n        s\"\"\"The collected metrics does not match the defined schema for the metrics.\n           | Expected : $metricsSchema\n           | Actual : ${operationMetrics.keySet}\n           \"\"\".stripMargin)\n    }\n    expectedMetrics.keys.foreach { key =>\n      if (!operationMetrics.contains(key)) {\n        fail(s\"The recorded operation metrics does not contain key: $key\")\n      }\n      if (expectedMetrics(key) != operationMetrics(key)) {\n        fail(\n          s\"\"\"The recorded metric for $key does not equal the expected value.\n             | expected = ${expectedMetrics(key)} ,\n             | But actual = ${operationMetrics(key)}\n           \"\"\".stripMargin\n        )\n      }\n    }\n  }\n\n  /**\n   * Check all expected metrics exist and executime time (if expected to exist) is the largest time\n   * metric.\n   */\n  protected def checkOperationTimeMetricsInvariant(\n      expectedMetrics: Set[String],\n      operationMetrics: Map[String, String]): Unit = {\n    expectedMetrics.foreach {\n      m => assert(operationMetrics.contains(m))\n    }\n    if (expectedMetrics.contains(\"executionTimeMs\")) {\n      val executionTimeMs = operationMetrics(\"executionTimeMs\").toLong\n      val maxTimeMs = operationMetrics.filterKeys(expectedMetrics.contains(_))\n        .mapValues(v => v.toLong).valuesIterator.max\n      assert(executionTimeMs == maxTimeMs)\n    }\n  }\n\n  protected def getOperationMetrics(history: DataFrame): Map[String, String] = {\n    history.select(\"operationMetrics\")\n      .take(1)\n      .head\n      .getMap(0)\n      .asInstanceOf[Map[String, String]]\n  }\n\n  // Returns necessary delta property json expected for the test. If Catalog-Owned is enabled,\n  // a few properties will be automatically populated, and this method will take care of it.\n  protected def getProperties(\n      extraProperty: Option[Map[String, String]] = None): Map[String, String] = {\n    val catalogOwnedProperty = if (catalogOwnedDefaultCreationEnabledInTests) {\n      CatalogOwnedTableUtils.QOL_TABLE_FEATURES_AND_PROPERTIES.collect {\n        case (feature, config, value)\n        => config.key -> value\n      }.toMap ++\n      // DV is explicitly disabled here b/c the current suite is incompatible\n      // w/ DV, and we automatically enable it as part of CatalogOwned QoL features.\n      Map(s\"${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}\" -> \"false\")\n    } else {\n      Map.empty[String, String]\n    }\n    // For history command, the output omits the empty config value, so we also need to\n    // manually omit the value here.\n    val properties = catalogOwnedProperty.filterNot { case (_, value) => value == \"{}\" }\n    val finalProperties = extraProperty.map(properties ++ _).getOrElse(properties)\n    finalProperties.asInstanceOf[Map[String, String]]\n  }\n\n  testWithFlag(\"basic case - Scala history with path-based table\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq(1, 2, 3).toDF().write.format(\"delta\").save(tempDir)\n    Seq(4, 5, 6).toDF().write.format(\"delta\").mode(\"overwrite\").save(tempDir)\n\n    val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir)\n    // Full History\n    checkAnswer(\n      deltaTable.history().select(\"operation\", \"operationParameters.mode\"),\n      Seq(Row(\"WRITE\", \"Overwrite\"), Row(\"WRITE\", \"ErrorIfExists\")))\n\n    // History with limit\n    checkAnswer(\n      deltaTable.history(1).select(\"operation\", \"operationParameters.mode\"),\n      Seq(Row(\"WRITE\", \"Overwrite\")))\n  }\n\n  test(\"basic case - Scala history with name-based table\") {\n    withTable(\"delta_test\") {\n      Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"delta_test\")\n      Seq(4, 5, 6).toDF().write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"delta_test\")\n\n      val deltaTable = io.delta.tables.DeltaTable.forName(spark, \"delta_test\")\n      // Full History\n      checkAnswer(\n        deltaTable.history().select(\"operation\"),\n        Seq(Row(\"CREATE OR REPLACE TABLE AS SELECT\"), Row(\"CREATE TABLE AS SELECT\")))\n\n      // History with limit\n      checkAnswer(\n        deltaTable.history(1).select(\"operation\"),\n        Seq(Row(\"CREATE OR REPLACE TABLE AS SELECT\")))\n    }\n  }\n\n  testWithFlag(\"basic case - SQL describe history with path-based table\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq(1, 2, 3).toDF().write.format(\"delta\").save(tempDir)\n    Seq(4, 5, 6).toDF().write.format(\"delta\").mode(\"overwrite\").save(tempDir)\n\n    // With delta.`path` format\n    checkAnswer(\n      sql(s\"DESCRIBE HISTORY delta.`$tempDir`\").select(\"operation\", \"operationParameters.mode\"),\n      Seq(Row(\"WRITE\", \"Overwrite\"), Row(\"WRITE\", \"ErrorIfExists\")))\n\n    checkAnswer(\n      sql(s\"DESCRIBE HISTORY delta.`$tempDir` LIMIT 1\")\n        .select(\"operation\", \"operationParameters.mode\"),\n      Seq(Row(\"WRITE\", \"Overwrite\")))\n\n    // With direct path format\n    checkAnswer(\n      sql(s\"DESCRIBE HISTORY '$tempDir'\").select(\"operation\", \"operationParameters.mode\"),\n      Seq(Row(\"WRITE\", \"Overwrite\"), Row(\"WRITE\", \"ErrorIfExists\")))\n\n    checkAnswer(\n      sql(s\"DESCRIBE HISTORY '$tempDir' LIMIT 1\")\n        .select(\"operation\", \"operationParameters.mode\"),\n      Seq(Row(\"WRITE\", \"Overwrite\")))\n  }\n\n  testWithFlag(\"basic case - SQL describe history with name-based table\") {\n    withTable(\"delta_test\") {\n      Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"delta_test\")\n      Seq(4, 5, 6).toDF().write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"delta_test\")\n\n      checkAnswer(\n        sql(s\"DESCRIBE HISTORY delta_test\").select(\"operation\"),\n        Seq(Row(\"CREATE OR REPLACE TABLE AS SELECT\"), Row(\"CREATE TABLE AS SELECT\")))\n\n      checkAnswer(\n        sql(s\"DESCRIBE HISTORY delta_test LIMIT 1\").select(\"operation\"),\n        Seq(Row(\"CREATE OR REPLACE TABLE AS SELECT\")))\n    }\n  }\n\n  testWithFlag(\"describe history command passes catalogTable to getHistory\") {\n    withTable(\"delta_catalog_test\") {\n      Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"delta_catalog_test\")\n\n      val table = DeltaTableV2(spark, TableIdentifier(\"delta_catalog_test\"))\n      assert(table.catalogTable.isDefined, \"Managed table should have catalogTable defined\")\n\n      val deltaLog = table.deltaLog\n      val originalHistory = deltaLog.history\n      var catalogTableWasPassed = false\n\n      // Create a wrapper that tracks if catalogTable is passed to getHistory.\n      // Note: getHistory(limitOpt) delegates to getHistory(limitOpt, None), so we only\n      // need to override the two-parameter version to detect whether catalogTable is passed.\n      val trackingHistory = new DeltaHistoryManager(\n        deltaLog,\n        spark.sessionState.conf.getConf(DeltaSQLConf.DELTA_HISTORY_PAR_SEARCH_THRESHOLD)) {\n        override def getHistory(\n            limitOpt: Option[Int],\n            catalogTableOpt: Option[CatalogTable]): Seq[DeltaHistory] = {\n          catalogTableWasPassed = catalogTableOpt.isDefined\n          originalHistory.getHistory(limitOpt, catalogTableOpt)\n        }\n      }\n\n      // Replace history field using reflection\n      val historyField = deltaLog.getClass.getDeclaredField(\"history\")\n      historyField.setAccessible(true)\n      historyField.set(deltaLog, trackingHistory)\n\n      // Run the command\n      DescribeDeltaHistoryCommand(\n        table = table,\n        limit = Some(10),\n        output = toAttributes(ExpressionEncoder[DeltaHistory]().schema)\n      ).run(spark)\n\n      assert(catalogTableWasPassed,\n        \"DescribeDeltaHistoryCommand should pass table.catalogTable to getHistory\")\n    }\n  }\n\n  testWithFlag(\"describe history fails on views\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq(1, 2, 3).toDF().write.format(\"delta\").save(tempDir)\n    val viewName = \"delta_view\"\n    withView(viewName) {\n      sql(s\"create view $viewName as select * from delta.`$tempDir`\")\n\n      val e = intercept[AnalysisException] {\n        sql(s\"DESCRIBE HISTORY $viewName\").collect()\n      }\n\n      assert(e.getMessage.contains(\n        \"'DESCRIBE HISTORY' expects a table but `spark_catalog`.`default`.`delta_view` is a view.\"))\n    }\n  }\n\n  testWithTempView(\"describe history fails on temp views\") { isSQLTempView =>\n      withTable(\"t1\") {\n        Seq(1, 2, 3).toDF().write.format(\"delta\").saveAsTable(\"t1\")\n        val viewName = \"v\"\n        createTempViewFromTable(\"t1\", isSQLTempView)\n\n        val e = intercept[AnalysisException] {\n          sql(s\"DESCRIBE HISTORY $viewName\").collect()\n        }\n\n        assert(e.getMessage.contains(\"'DESCRIBE HISTORY' expects a table but `v` is a view.\"))\n      }\n  }\n\n  private val expectedCreateOperationParameters =\n    Seq(\"partitionBy\", \"clusterBy\", \"description\", \"isManaged\", \"properties\")\n\n  testWithFlag(\"operations - create table\") {\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"create table delta_test (\n           |  a int,\n           |  b string\n           |)\n           |using delta\n           |partitioned by (b)\n           |comment 'this is my table'\n           |tblproperties (delta.appendOnly=true)\n         \"\"\".stripMargin)\n      val basePath =\n        spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\")).location.getPath\n      val appendOnlyTableProperty = Map(\"delta.appendOnly\" -> \"true\")\n      checkLastOperation(\n        basePath,\n        expectedOperationParameters = expectedCreateOperationParameters,\n        expectedColVals = Seq(\"CREATE TABLE\", \"true\", \"\"\"[\"b\"]\"\"\", \"\"\"[]\"\"\", \"this is my table\"),\n        columns = Seq(\n          $\"operation\", $\"operationParameters.isManaged\", $\"operationParameters.partitionBy\",\n          $\"operationParameters.clusterBy\", $\"operationParameters.description\"))\n      checkLastOperationProperties(basePath, getProperties(Some(appendOnlyTableProperty)))\n    }\n  }\n\n  testWithFlag(\"operations - ctas (saveAsTable)\") {\n    val tempDir = Utils.createTempDir().toString\n    withTable(\"delta_test\") {\n      Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\").write.format(\"delta\")\n        .option(\"path\", tempDir).saveAsTable(\"delta_test\")\n      checkLastOperation(\n        tempDir,\n        expectedOperationParameters = expectedCreateOperationParameters,\n        expectedColVals = Seq(\"CREATE TABLE AS SELECT\", \"false\", \"\"\"[]\"\"\", \"\"\"[]\"\"\", null),\n        columns =\n          Seq($\"operation\", $\"operationParameters.isManaged\", $\"operationParameters.partitionBy\",\n          $\"operationParameters.clusterBy\", $\"operationParameters.description\"))\n      checkLastOperationProperties(tempDir, getProperties())\n    }\n  }\n\n  testWithFlag(\"operations - ctas (sql)\") {\n    val tempDir = Utils.createTempDir().toString\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"create table delta_test\n           |using delta\n           |location '$tempDir'\n           |tblproperties (delta.appendOnly=true)\n           |partitioned by (b)\n           |as select 1 as a, 'x' as b\n         \"\"\".stripMargin)\n      val appendOnlyProperty = Map[String, String](\"delta.appendOnly\" -> \"true\")\n      checkLastOperation(\n        tempDir,\n        expectedOperationParameters = expectedCreateOperationParameters,\n        expectedColVals = Seq(\"CREATE TABLE AS SELECT\", \"false\", \"\"\"[\"b\"]\"\"\", \"\"\"[]\"\"\", null),\n        columns =\n          Seq($\"operation\", $\"operationParameters.isManaged\", $\"operationParameters.partitionBy\",\n            $\"operationParameters.clusterBy\", $\"operationParameters.description\"))\n      checkLastOperationProperties(tempDir, getProperties(Some(appendOnlyProperty)))\n    }\n    val tempDir2 = Utils.createTempDir().toString\n    withTable(\"delta_test\") {\n      sql(\n        s\"\"\"create table delta_test\n           |using delta\n           |location '$tempDir2'\n           |comment 'this is my table'\n           |as select 1 as a, 'x' as b\n         \"\"\".stripMargin)\n      // TODO(burak): Fix comments for CTAS\n      checkLastOperation(\n        tempDir2,\n        expectedOperationParameters = expectedCreateOperationParameters,\n        expectedColVals =\n          Seq(\"CREATE TABLE AS SELECT\", \"false\", \"\"\"[]\"\"\", \"\"\"[]\"\"\", \"this is my table\"),\n        columns =\n          Seq($\"operation\", $\"operationParameters.isManaged\", $\"operationParameters.partitionBy\",\n            $\"operationParameters.clusterBy\", $\"operationParameters.description\"))\n      checkLastOperationProperties(tempDir2, getProperties())\n    }\n  }\n\n\n  testWithFlag(\"operations - [un]set tbproperties\") {\n    withTable(\"delta_test\") {\n      sql(\"CREATE TABLE delta_test (v1 int, v2 string) USING delta\")\n\n      sql(\"\"\"\n            |ALTER TABLE delta_test\n            |SET TBLPROPERTIES (\n            |  'delta.checkpointInterval' = '20',\n            |  'key' = 'value'\n            |)\"\"\".stripMargin)\n      checkLastOperation(\n        spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\")).location.getPath,\n        expectedOperationParameters = Seq(\"properties\"),\n        expectedColVals =\n          Seq(\"SET TBLPROPERTIES\", \"\"\"{\"delta.checkpointInterval\":\"20\",\"key\":\"value\"}\"\"\"),\n        columns = Seq($\"operation\", $\"operationParameters.properties\"))\n\n      sql(\"ALTER TABLE delta_test UNSET TBLPROPERTIES ('key')\")\n      checkLastOperation(\n        spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\")).location.getPath,\n        expectedOperationParameters = Seq(\"properties\", \"ifExists\"),\n        expectedColVals = Seq(\"UNSET TBLPROPERTIES\", \"\"\"[\"key\"]\"\"\", \"true\"),\n        columns =\n          Seq($\"operation\", $\"operationParameters.properties\", $\"operationParameters.ifExists\"))\n    }\n  }\n\n  testWithFlag(\"operations - add columns\") {\n    withTable(\"delta_test\") {\n      sql(\"CREATE TABLE delta_test (v1 int, v2 string) USING delta\")\n\n      sql(\"ALTER TABLE delta_test ADD COLUMNS (v3 long, v4 int AFTER v1)\")\n      val column3 = \"\"\"{\"name\":\"v3\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}\"\"\"\n      val column4 = \"\"\"{\"name\":\"v4\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}\"\"\"\n      checkLastOperation(\n        spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\")).location.getPath,\n        expectedOperationParameters = Seq(\"columns\"),\n        expectedColVals = Seq(\"ADD COLUMNS\",\n          s\"\"\"[{\"column\":$column3},{\"column\":$column4,\"position\":\"AFTER v1\"}]\"\"\"),\n        columns = Seq($\"operation\", $\"operationParameters.columns\"))\n    }\n  }\n\n  testWithFlag(\"operations - change column\") {\n    withTable(\"delta_test\") {\n      sql(\"CREATE TABLE delta_test (v1 int, v2 string) USING delta\")\n\n      sql(\"ALTER TABLE delta_test CHANGE COLUMN v1 v1 integer AFTER v2\")\n      checkLastOperation(\n        spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"delta_test\")).location.getPath,\n        expectedOperationParameters = Seq(\"column\", \"position\"),\n        expectedColVals = Seq(\"CHANGE COLUMN\",\n          s\"\"\"{\"name\":\"v1\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}\"\"\",\n          \"AFTER v2\"),\n        columns = Seq($\"operation\", $\"operationParameters.column\", $\"operationParameters.position\"))\n    }\n  }\n\n  test(\"operations - upgrade protocol\") {\n    val readerVersion = Action.supportedProtocolVersion().minReaderVersion\n    val writerVersion = Action.supportedProtocolVersion().minWriterVersion\n    withTempDir { path =>\n      val log = DeltaLog.forTable(spark, path)\n      log.createLogDirectoriesIfNotExists()\n      log.store.write(\n        FileNames.unsafeDeltaFile(log.logPath, 0),\n        Iterator(\n          Metadata(schemaString = spark.range(1).schema.asNullable.json).json,\n          Protocol(1, 1).json),\n        overwrite = false,\n        log.newDeltaHadoopConf())\n      log.update()\n      log.upgradeProtocol(\n        Action.supportedProtocolVersion(withAllFeatures = false)\n          .withFeature(TestLegacyReaderWriterFeature))\n      // scalastyle:off line.size.limit\n      checkLastOperation(\n        path.toString,\n        expectedOperationParameters = Seq(\"newProtocol\"),\n        expectedColVals = Seq(\"UPGRADE PROTOCOL\",\n          s\"\"\"{\"minReaderVersion\":$readerVersion,\"\"\" +\n            s\"\"\"\"minWriterVersion\":$writerVersion,\"\"\" +\n            s\"\"\"\"readerFeatures\":[\"${TestLegacyReaderWriterFeature.name}\"],\"\"\" +\n            s\"\"\"\"writerFeatures\":[\"${TestLegacyReaderWriterFeature.name}\"]}\"\"\"),\n        columns = Seq($\"operation\", $\"operationParameters.newProtocol\"))\n      // scalastyle:on line.size.limit\n    }\n  }\n\n  val expectedInsertOperationParameters =\n    Seq(\"mode\", \"partitionBy\")\n\n  testWithFlag(\"operations - insert append with partition columns\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\")\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .partitionBy(\"id\")\n      .save(tempDir)\n\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters,\n      expectedColVals = Seq(\"WRITE\", \"Append\", \"\"\"[\"id\"]\"\"\"),\n      columns =\n        Seq($\"operation\", $\"operationParameters.mode\", $\"operationParameters.partitionBy\"))\n  }\n\n  testWithFlag(\"operations - insert append without partition columns\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\").write.format(\"delta\").save(tempDir)\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters,\n      expectedColVals = Seq(\"WRITE\", \"ErrorIfExists\", \"\"\"[]\"\"\"),\n      columns =\n        Seq($\"operation\", $\"operationParameters.mode\", $\"operationParameters.partitionBy\"))\n  }\n\n  testWithFlag(\"operations - insert error if exists with partitions\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"id\")\n        .mode(\"errorIfExists\")\n        .save(tempDir)\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters,\n      expectedColVals = Seq(\"WRITE\", \"ErrorIfExists\", \"\"\"[\"id\"]\"\"\"),\n      columns =\n        Seq($\"operation\", $\"operationParameters.mode\", $\"operationParameters.partitionBy\"))\n  }\n\n  testWithFlag(\"operations - insert error if exists without partitions\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\")\n        .write\n        .format(\"delta\")\n        .mode(\"errorIfExists\")\n        .save(tempDir)\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters,\n      expectedColVals = Seq(\"WRITE\", \"ErrorIfExists\", \"\"\"[]\"\"\"),\n      columns =\n        Seq($\"operation\", $\"operationParameters.mode\", $\"operationParameters.partitionBy\"))\n  }\n\n  test(\"operations - streaming append with transaction ids\") {\n\n    val tempDir = Utils.createTempDir().toString\n    val checkpoint = Utils.createTempDir().toString\n\n    val data = MemoryStream[Int]\n    data.addData(1, 2, 3)\n    val stream = data.toDF()\n      .writeStream\n      .format(\"delta\")\n      .option(\"checkpointLocation\", checkpoint)\n      .start(tempDir)\n    stream.processAllAvailable()\n    stream.stop()\n\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = Seq(\"outputMode\", \"queryId\", \"epochId\"),\n      expectedColVals = Seq(\"STREAMING UPDATE\", \"Append\", \"0\"),\n      columns =\n        Seq($\"operation\", $\"operationParameters.outputMode\", $\"operationParameters.epochId\"))\n  }\n\n  testWithFlag(\"operations - insert overwrite with predicate\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\").write.format(\"delta\").partitionBy(\"id\").save(tempDir)\n\n    Seq((1, \"b\")).toDF(\"id\", \"data\").write\n      .format(\"delta\")\n      .mode(\"overwrite\")\n      .option(DeltaOptions.REPLACE_WHERE_OPTION, \"id = 1\")\n      .save(tempDir)\n\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters ++ Seq(\"predicate\"),\n      expectedColVals = Seq(\"WRITE\", \"Overwrite\", \"\"\"id = 1\"\"\"),\n      columns = Seq($\"operation\", $\"operationParameters.mode\", $\"operationParameters.predicate\"))\n  }\n\n  testWithFlag(\"operations - delete with predicate\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq((1, \"a\"), (2, \"3\")).toDF(\"id\", \"data\").write.format(\"delta\").partitionBy(\"id\").save(tempDir)\n    val deltaLog = DeltaLog.forTable(spark, tempDir)\n    val deltaTable = io.delta.tables.DeltaTable.forPath(spark, deltaLog.dataPath.toString)\n    deltaTable.delete(\"id = 1\")\n\n    checkLastOperation(\n      tempDir,\n      expectedOperationParameters = Seq(\"predicate\"),\n      expectedColVals = Seq(\"DELETE\", \"\"\"[\"(id = 1)\"]\"\"\"),\n      columns = Seq($\"operation\", $\"operationParameters.predicate\"), removeExpressionId = true)\n  }\n\n  testWithFlag(\"old and new writers\") {\n    val tempDir = Utils.createTempDir().toString\n    Seq(1, 2, 3).toDF().write.format(\"delta\").save(tempDir.toString)\n\n    checkLastOperation(tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters,\n      expectedColVals = Seq(\"WRITE\", \"ErrorIfExists\"),\n      columns = Seq($\"operation\", $\"operationParameters.mode\"))\n    Seq(1, 2, 3).toDF().write.format(\"delta\").mode(\"append\").save(tempDir.toString)\n\n    assert(spark.sql(s\"DESCRIBE HISTORY delta.`$tempDir`\").count() === 2)\n    checkLastOperation(tempDir,\n      expectedOperationParameters = expectedInsertOperationParameters,\n      expectedColVals = Seq(\"WRITE\", \"Append\"),\n      columns = Seq($\"operation\", $\"operationParameters.mode\"))\n  }\n\n  testWithFlag(\"order history by version\") {\n    val tempDir = Utils.createTempDir().toString\n\n    Seq(0).toDF().write.format(\"delta\").save(tempDir)\n    Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tempDir)\n\n    Seq(2).toDF().write.format(\"delta\").mode(\"append\").save(tempDir)\n    Seq(3).toDF().write.format(\"delta\").mode(\"overwrite\").save(tempDir)\n\n    Seq(4).toDF().write.format(\"delta\").mode(\"overwrite\").save(tempDir)\n\n\n    val ans = io.delta.tables.DeltaTable.forPath(spark, tempDir)\n      .history().as[DeltaHistory].collect()\n    assert(ans.map(_.version) === Seq(Some(4), Some(3), Some(2), Some(1), Some(0)))\n\n    val ans2 = sql(s\"DESCRIBE HISTORY delta.`$tempDir`\").as[DeltaHistory].collect()\n    assert(ans2.map(_.version) === Seq(Some(4), Some(3), Some(2), Some(1), Some(0)))\n  }\n\n  test(\"read version\") {\n    val tempDir = Utils.createTempDir().toString\n\n    Seq(0).toDF().write.format(\"delta\").save(tempDir) // readVersion = None as first commit\n    Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tempDir) // readVersion = Some(0)\n\n    val log = DeltaLog.forTable(spark, tempDir)\n    val txn = log.startTransaction()   // should read snapshot version 1\n\n\n    Seq(2).toDF().write.format(\"delta\").mode(\"append\").save(tempDir)  // readVersion = Some(1)\n    Seq(3).toDF().write.format(\"delta\").mode(\"append\").save(tempDir)  // readVersion = Some(2)\n\n\n    txn.commit(Seq.empty, DeltaOperations.Truncate())  // readVersion = Some(1)\n\n    Seq(5).toDF().write.format(\"delta\").mode(\"append\").save(tempDir)   // readVersion = Some(4)\n    val ans = sql(s\"DESCRIBE HISTORY delta.`$tempDir`\").as[DeltaHistory].collect()\n    assert(ans.map(x => x.version.get -> x.readVersion) ===\n      Seq(5 -> Some(4), 4 -> Some(1), 3 -> Some(2), 2 -> Some(1), 1 -> Some(0), 0 -> None))\n  }\n\n  testWithFlag(\"evolvability test\") {\n    checkLastOperation(\n      evolvabilityResource,\n      expectedOperationParameters = Seq(\"outputMode\", \"queryId\", \"epochId\"),\n      expectedColVals = evolvabilityLastOp,\n      columns = Seq($\"operation\", $\"operationParameters.mode\", $\"operationParameters.partitionBy\"))\n  }\n\n  test(\"using on non delta\") {\n    withTempDir { basePath =>\n      val e = intercept[AnalysisException] {\n        sql(s\"describe history '$basePath'\").collect()\n      }\n      assert(Seq(\"supported\", \"Delta\").forall(e.getMessage.contains))\n    }\n  }\n\n  test(\"describe history a non-existent path and a non Delta table\") {\n    def assertNotADeltaTableException(path: String): Unit = {\n      for (table <- Seq(s\"'$path'\", s\"delta.`$path`\")) {\n        val e = intercept[AnalysisException] {\n          sql(s\"describe history $table\").show()\n        }\n        Seq(\"is not a Delta table\").foreach { msg =>\n          assert(e.getMessage.contains(msg))\n        }\n      }\n    }\n    withTempPath { tempDir =>\n      assert(!tempDir.exists())\n      assertNotADeltaTableException(tempDir.getCanonicalPath)\n    }\n    withTempPath { tempDir =>\n      spark.range(1, 10).write.parquet(tempDir.getCanonicalPath)\n      assertNotADeltaTableException(tempDir.getCanonicalPath)\n    }\n  }\n\n  test(\"operation metrics - write metrics\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        // create table\n        spark.range(100).repartition(5).write.format(\"delta\").save(tempDir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.getAbsolutePath)\n\n        // get last command history\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        val expectedMetrics = Map(\n          \"numFiles\" -> \"5\",\n          \"numOutputRows\" -> \"100\"\n        )\n\n        // Check if operation metrics from history are accurate\n        checkOperationMetrics(expectedMetrics, operationMetrics, DeltaOperationMetrics.WRITE)\n        assert(operationMetrics(\"numOutputBytes\").toLong > 0)\n      }\n    }\n  }\n\n  test(\"operation metrics - merge\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        // create target\n        spark.range(100).write.format(\"delta\").save(tempDir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.getAbsolutePath)\n\n        // run merge\n        deltaTable.as(\"t\")\n          .merge(spark.range(50, 150).toDF().as(\"s\"), \"s.id = t.id\")\n          .whenMatched()\n          .updateAll()\n          .whenNotMatched()\n          .insertAll()\n          .execute()\n\n        // Get operation metrics\n        val operationMetrics: Map[String, String] = getOperationMetrics(deltaTable.history(1))\n\n        val expectedMetrics = Map(\n          \"numTargetRowsInserted\" -> \"50\",\n          \"numTargetRowsUpdated\" -> \"50\",\n          \"numTargetRowsDeleted\" -> \"0\",\n          \"numOutputRows\" -> \"100\",\n          \"numSourceRows\" -> \"100\"\n        )\n        val copiedRows = operationMetrics(\"numTargetRowsCopied\").toInt\n        assert(0 <= copiedRows && copiedRows <= 50)\n        checkOperationMetrics(\n          expectedMetrics,\n          operationMetrics,\n          mergeMetricsSchema)\n        val expectedTimeMetrics = Set(\"executionTimeMs\", \"scanTimeMs\", \"rewriteTimeMs\")\n        checkOperationTimeMetricsInvariant(expectedTimeMetrics, operationMetrics)\n      }\n    }\n  }\n\n  test(\"operation metrics - streaming update\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        val memoryStream = MemoryStream[Long]\n        val df = memoryStream.toDF()\n\n        val tbl = tempDir.getAbsolutePath + \"tbl1\"\n\n        spark.range(10).write.format(\"delta\").save(tbl)\n        // ensure that you are writing out a single file per batch\n        val q = df.coalesce(1)\n          .withColumnRenamed(\"value\", \"id\")\n          .writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", tempDir + \"checkpoint\")\n          .start(tbl)\n        memoryStream.addData(1)\n        q.processAllAvailable()\n        val deltaTable = io.delta.tables.DeltaTable.forPath(tbl)\n        var operationMetrics: Map[String, String] = getOperationMetrics(deltaTable.history(1))\n        val expectedMetrics = Map(\n          \"numAddedFiles\" -> \"1\",\n          \"numRemovedFiles\" -> \"0\",\n          \"numOutputRows\" -> \"1\"\n        )\n        checkOperationMetrics(\n          expectedMetrics, operationMetrics, DeltaOperationMetrics.STREAMING_UPDATE)\n\n        // check if second batch also returns correct metrics.\n        memoryStream.addData(1, 2, 3)\n        q.processAllAvailable()\n        operationMetrics = getOperationMetrics(deltaTable.history(1))\n        val expectedMetrics2 = Map(\n          \"numAddedFiles\" -> \"1\",\n          \"numRemovedFiles\" -> \"0\",\n          \"numOutputRows\" -> \"3\"\n        )\n        checkOperationMetrics(\n          expectedMetrics2, operationMetrics, DeltaOperationMetrics.STREAMING_UPDATE)\n        assert(operationMetrics(\"numOutputBytes\").toLong > 0)\n        q.stop()\n      }\n    }\n  }\n\n  test(\"operation metrics - streaming update - complete mode\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        val memoryStream = MemoryStream[Long]\n        val df = memoryStream.toDF()\n\n        val tbl = tempDir.getAbsolutePath + \"tbl1\"\n\n        Seq(1L -> 1L, 2L -> 2L).toDF(\"value\", \"count\")\n          .coalesce(1)\n          .write\n          .format(\"delta\")\n          .save(tbl)\n\n        // ensure that you are writing out a single file per batch\n        val q = df.groupBy(\"value\").count().coalesce(1)\n          .writeStream\n          .format(\"delta\")\n          .outputMode(\"complete\")\n          .option(\"checkpointLocation\", tempDir + \"checkpoint\")\n          .start(tbl)\n        memoryStream.addData(1)\n        q.processAllAvailable()\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(tbl)\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        val expectedMetrics = Map(\n          \"numAddedFiles\" -> \"1\",\n          \"numRemovedFiles\" -> \"1\",\n          \"numOutputRows\" -> \"1\"\n        )\n        checkOperationMetrics(\n          expectedMetrics, operationMetrics, DeltaOperationMetrics.STREAMING_UPDATE)\n      }\n    }\n  }\n\n  def getLastCommitNumAddedAndRemovedBytes(deltaLog: DeltaLog): (Long, Long) = {\n    val changes = deltaLog.getChanges(deltaLog.update().version).flatMap(_._2).toSeq\n    val addedBytes = changes.collect { case a: AddFile => a.size }.sum\n    val removedBytes = changes.collect { case r: RemoveFile => r.getFileSize }.sum\n\n    (addedBytes, removedBytes)\n  }\n\n  def metricsUpdateTest : Unit = withTempDir { tempDir =>\n    // Create the initial table as a single file\n    Seq(1, 2, 5, 11, 21, 3, 4, 6, 9, 7, 8, 0).toDF(\"key\")\n      .withColumn(\"value\", 'key % 2)\n      .write\n      .format(\"delta\")\n      .save(tempDir.getAbsolutePath)\n\n    // append additional data with the same number range to the table.\n    // This data is saved as a separate file as well\n    Seq(15, 16, 17).toDF(\"key\")\n      .withColumn(\"value\", 'key % 2)\n      .repartition(1)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(tempDir.getAbsolutePath)\n    val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n    val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n    deltaLog.snapshot.numOfFiles\n\n    // update the table\n    deltaTable.update(col(\"key\") === lit(\"16\"), Map(\"value\" -> lit(\"1\")))\n    // The file from the append gets updated but the file from the initial table gets scanned\n    // as well. We want to make sure numCopied rows is calculated from written files and not\n    // scanned files[SC-33980]\n\n    // get operation metrics\n    val operationMetrics = getOperationMetrics(deltaTable.history(1))\n    val (addedBytes, removedBytes) = getLastCommitNumAddedAndRemovedBytes(deltaLog)\n    val expectedMetrics = Map(\n      \"numAddedFiles\" -> \"1\",\n      \"numRemovedFiles\" -> \"1\",\n      \"numUpdatedRows\" -> \"1\",\n      \"numCopiedRows\" -> \"2\", // There should be only three rows in total(updated + copied)\n      \"numAddedBytes\" -> addedBytes.toString,\n      \"numRemovedBytes\" -> removedBytes.toString\n    )\n    checkOperationMetrics(\n      expectedMetrics,\n      operationMetrics,\n      updateMetricsSchema)\n    val expectedTimeMetrics = Set(\"executionTimeMs\", \"scanTimeMs\", \"rewriteTimeMs\")\n    checkOperationTimeMetricsInvariant(expectedTimeMetrics, operationMetrics)\n  }\n\n  test(\"operation metrics - update\") {\n    withSQLConf((DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\")) {\n      metricsUpdateTest\n    }\n  }\n\n  def metricsUpdatePartitionedColumnTest : Unit = {\n    val numRows = 100\n    val numPartitions = 5\n    withTempDir { tempDir =>\n      spark.range(numRows)\n        .withColumn(\"c1\", 'id + 1)\n        .withColumn(\"c2\", 'id % numPartitions)\n        .write\n        .partitionBy(\"c2\")\n        .format(\"delta\")\n        .save(tempDir.getAbsolutePath)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      val numFilesBeforeUpdate = deltaLog.snapshot.numOfFiles\n      deltaTable.update(col(\"c2\") < 1, Map(\"c2\" -> lit(\"1\")))\n      val numFilesAfterUpdate = deltaLog.snapshot.numOfFiles\n\n      val operationMetrics = getOperationMetrics(deltaTable.history(1))\n      val newFiles = numFilesAfterUpdate - numFilesBeforeUpdate\n      val oldFiles = numFilesBeforeUpdate / numPartitions\n      val addedFiles = newFiles + oldFiles\n      val (addedBytes, removedBytes) = getLastCommitNumAddedAndRemovedBytes(deltaLog)\n      val expectedMetrics = Map(\n        \"numUpdatedRows\" -> (numRows / numPartitions).toString,\n        \"numCopiedRows\" -> \"0\",\n        \"numAddedFiles\" -> addedFiles.toString,\n        \"numRemovedFiles\" -> (numFilesBeforeUpdate / numPartitions).toString,\n        \"numAddedBytes\" -> addedBytes.toString,\n        \"numRemovedBytes\" -> removedBytes.toString\n      )\n      checkOperationMetrics(\n        expectedMetrics,\n        operationMetrics,\n        updateMetricsSchema)\n    }\n  }\n\n  test(\"operation metrics - update - partitioned column\") {\n    withSQLConf((DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\")) {\n      metricsUpdatePartitionedColumnTest\n    }\n  }\n\n  test(\"operation metrics - delete\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        // Create the initial table as a single file\n        Seq(1, 2, 5, 11, 21, 3, 4, 6, 9, 7, 8, 0).toDF(\"key\")\n          .withColumn(\"value\", 'key % 2)\n          .repartition(1)\n          .write\n          .format(\"delta\")\n          .save(tempDir.getAbsolutePath)\n\n        // Append to the initial table additional data in the same numerical range\n        Seq(15, 16, 17).toDF(\"key\")\n          .withColumn(\"value\", 'key % 2)\n          .repartition(1)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n        deltaLog.snapshot.numOfFiles\n\n        // delete the table\n        deltaTable.delete(col(\"key\") === lit(\"16\"))\n        // The file from the append gets deleted but the file from the initial table gets scanned\n        // as well. We want to make sure numCopied rows is calculated from the written files instead\n        // of the scanned files.[SC-33980]\n\n        // get operation metrics\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n\n        // get expected byte level metrics\n        val (numAddedBytesExpected, numRemovedBytesExpected) =\n          getLastCommitNumAddedAndRemovedBytes(deltaLog)\n        val expectedMetrics = Map(\n          \"numAddedFiles\" -> \"1\",\n          \"numAddedBytes\" -> numAddedBytesExpected.toString,\n          \"numRemovedFiles\" -> \"1\",\n          \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n          \"numDeletedRows\" -> \"1\",\n          \"numCopiedRows\" -> \"2\" // There should be only three rows in total(deleted + copied)\n        )\n        checkOperationMetrics(\n          expectedMetrics,\n          operationMetrics,\n          deleteMetricsSchema(partitioned = false))\n        val expectedTimeMetrics = Set(\"executionTimeMs\", \"scanTimeMs\", \"rewriteTimeMs\")\n        checkOperationTimeMetricsInvariant(expectedTimeMetrics, operationMetrics)\n      }\n    }\n  }\n\n  test(\"operation metrics - delete - partition column\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      val numRows = 100\n      val numPartitions = 5\n      withTempDir { tempDir =>\n        spark.range(numRows)\n          .withColumn(\"c1\", 'id % numPartitions)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"c1\")\n          .save(tempDir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n        val numFilesBeforeDelete = deltaLog.snapshot.numOfFiles\n        val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.getAbsolutePath)\n\n        deltaTable.delete(\"c1 = 1\")\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        // get expected byte level metrics\n        val (numAddedBytesExpected, numRemovedBytesExpected) =\n          getLastCommitNumAddedAndRemovedBytes(deltaLog)\n        val expectedMetrics = Map[String, String](\n          \"numRemovedFiles\" -> (numFilesBeforeDelete / numPartitions).toString,\n          \"numAddedBytes\" -> numAddedBytesExpected.toString,\n          \"numRemovedBytes\" -> numRemovedBytesExpected.toString\n        )\n        // row level metrics are not collected for deletes with parition columns\n        checkOperationMetrics(\n          expectedMetrics,\n          operationMetrics,\n          deleteMetricsSchema(partitioned = true))\n        val expectedTimeMetrics = Set(\"executionTimeMs\", \"scanTimeMs\", \"rewriteTimeMs\")\n        checkOperationTimeMetricsInvariant(expectedTimeMetrics, operationMetrics)\n      }\n    }\n  }\n\n  test(\"operation metrics - delete - full\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      val numRows = 100\n      val numPartitions = 5\n      withTempDir { tempDir =>\n        spark.range(numRows)\n          .withColumn(\"c1\", 'id % numPartitions)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"c1\")\n          .save(tempDir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n        val numFilesBeforeDelete = deltaLog.snapshot.numOfFiles\n        val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.getAbsolutePath)\n\n        deltaTable.delete()\n\n        // get expected byte level metrics\n        val (numAddedBytesExpected, numRemovedBytesExpected) =\n          getLastCommitNumAddedAndRemovedBytes(deltaLog)\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        val expectedMetrics = Map[String, String](\n          \"numRemovedFiles\" -> numFilesBeforeDelete.toString,\n          \"numAddedBytes\" -> numAddedBytesExpected.toString,\n          \"numRemovedBytes\" -> numRemovedBytesExpected.toString\n        )\n        checkOperationMetrics(\n          expectedMetrics,\n          operationMetrics,\n          deleteMetricsSchema(partitioned = true))\n        val expectedTimeMetrics = Set(\"executionTimeMs\", \"scanTimeMs\", \"rewriteTimeMs\")\n        checkOperationTimeMetricsInvariant(expectedTimeMetrics, operationMetrics)\n      }\n    }\n  }\n\n  test(\"operation metrics - convert to delta\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      val numPartitions = 5\n      withTempDir { tempDir =>\n        // Create a parquet table\n        val dir = tempDir.getAbsolutePath()\n        spark.range(10)\n          .withColumn(\"col2\", 'id % numPartitions)\n          .write\n          .format(\"parquet\")\n          .mode(\"overwrite\")\n          .partitionBy(\"col2\")\n          .save(dir)\n\n        // convert to delta\n        val deltaTable = io.delta.tables.DeltaTable.convertToDelta(spark, s\"parquet.`$dir`\",\n          \"col2 long\")\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        val expectedMetrics = Map(\n          \"numConvertedFiles\" -> deltaLog.snapshot.numOfFiles.toString\n        )\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        checkOperationMetrics(expectedMetrics, operationMetrics, DeltaOperationMetrics.CONVERT)\n      }\n    }\n  }\n\n  test(\"sort and collect the DESCRIBE HISTORY result\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      Seq(1, 2, 3).toDF().write.format(\"delta\").save(path)\n      val rows = sql(s\"DESCRIBE HISTORY delta.`$path`\")\n        .orderBy(\"version\")\n        .collect()\n      assert(rows.map(_.getAs[Long](\"version\")).toList == 0L :: Nil)\n      withSQLConf(SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> \"false\") {\n        val rows = sql(s\"DESCRIBE HISTORY delta.`$path`\")\n          .filter(\"version >= 0\")\n          .orderBy(\"version\")\n          .collect()\n        assert(rows.map(_.getAs[Long](\"version\")).toList == 0L :: Nil)\n      }\n    }\n  }\n\n  test(\"operation metrics - create table\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      val tblName = \"tblName\"\n      val numRows = 10\n      withTable(tblName) {\n        sql(s\"CREATE TABLE $tblName USING DELTA SELECT * from range($numRows)\")\n        val deltaTable = io.delta.tables.DeltaTable.forName(tblName)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        val numFiles = deltaLog.snapshot.numOfFiles\n        val expectedMetrics = Map(\n          \"numFiles\" -> numFiles.toString,\n          \"numOutputRows\" -> numRows.toString\n        )\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        assert(operationMetrics(\"numOutputBytes\").toLong > 0)\n        checkOperationMetrics(expectedMetrics, operationMetrics, DeltaOperationMetrics.WRITE)\n      }\n    }\n  }\n\n  test(\"operation metrics - create table - without data\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      val tblName = s\"tbl_${System.currentTimeMillis()}\" // unique name\n      withTable(tblName) {\n        sql(s\"CREATE TABLE $tblName(id bigint) USING DELTA\")\n        val deltaTable = io.delta.tables.DeltaTable.forName(tblName)\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        assert(operationMetrics === Map.empty)\n      }\n    }\n  }\n\n  def testReplaceWhere(testName: String)(f: (Boolean, Boolean) => Unit): Unit = {\n    Seq(true, false).foreach { enableCDF =>\n      Seq(true, false).foreach { enableStats =>\n        test(testName + s\"enableCDF=${enableCDF} -  enableStats ${enableStats}\") {\n          withSQLConf(\n              DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> enableCDF.toString,\n              DeltaSQLConf.DELTA_COLLECT_STATS.key ->enableStats.toString,\n              DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n            if (!enableStats) {\n              // Row IDs assignment needs row count statistics. So we need to disable RowTracking\n              // here for CCv1.5's QoL features if we are not enabling [[DELTA_COLLECT_STATS]].\n              spark.conf.set(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey, \"false\")\n            }\n            f(enableCDF, enableStats)\n            if (!enableStats && spark.sessionState.conf.contains(\n                DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey)) {\n              spark.conf.unset(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  testReplaceWhere(\"replaceWhere on data column\") { (enableCDF, enableStats) =>\n    withTable(\"tbl\") {\n      // create a table with one row\n      spark.range(10)\n        .repartition(1) // 1 file table\n        .withColumn(\"b\", lit(1))\n        .write\n        .format(\"delta\")\n        .saveAsTable(\"tbl\")\n      val deltaTable = io.delta.tables.DeltaTable.forName(\"tbl\")\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n\n      // replace where\n      spark.range(20)\n        .withColumn(\"b\", lit(1))\n        .repartition(1) // write 1 file\n        .write\n        .format(\"delta\")\n        .option(\"replaceWhere\", \"b = 1\")\n        .mode(\"overwrite\")\n        .saveAsTable(\"tbl\")\n\n      val numWrittenFiles = deltaLog.getChanges(1).flatMap {\n        case (a, v) => v\n      }.filter(_.isInstanceOf[AddFile])\n        .toSeq\n        .size\n\n      val numAddedChangeFiles = if (enableCDF) {\n        deltaLog.getChanges(1).flatMap {\n          case (a, v) => v\n        }.filter(_.isInstanceOf[AddCDCFile])\n          .toSeq\n          .size\n      } else {\n        0\n      }\n\n      // get expected byte level metrics\n      val (numAddedBytesExpected, numRemovedBytesExpected) =\n        getLastCommitNumAddedAndRemovedBytes(deltaLog)\n\n      if (enableStats) {\n        checkOperationMetrics(\n          Map(\n            \"numFiles\" -> (numWrittenFiles).toString,\n            \"numOutputRows\" -> \"20\",\n            \"numCopiedRows\" -> \"0\",\n            \"numOutputBytes\" -> numAddedBytesExpected.toString,\n            \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n            \"numAddedChangeFiles\" -> numAddedChangeFiles.toString,\n            \"numDeletedRows\" -> \"10\",\n            \"numRemovedFiles\" -> \"1\"\n          ),\n          getOperationMetrics(deltaTable.history(1)),\n          replaceWhereMetricsSchema\n        )\n      } else {\n        checkOperationMetrics(\n          Map(\n            \"numFiles\" -> (numWrittenFiles).toString,\n            \"numOutputBytes\" -> numAddedBytesExpected.toString,\n            \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n            \"numAddedChangeFiles\" -> numAddedChangeFiles.toString,\n            \"numRemovedFiles\" -> \"1\"\n          ),\n          getOperationMetrics(deltaTable.history(1)),\n          replaceWhereMetricsSchema.filter(!_.contains(\"Rows\"))\n        )\n      }\n    }\n  }\n\n  testReplaceWhere(s\"replaceWhere on data column - partial rewrite\") { (enableCDF, enableStats) =>\n    // Whats different from the above test\n    // replace where has a append + delete.\n    // make the delete also write new files\n    withTable(\"tbl\") {\n      // create a table with one row\n      spark.range(10)\n        .repartition(1) // 1 file table\n        .withColumn(\"b\", 'id % 2) // 1 file contains 2 values\n        .write\n        .format(\"delta\")\n        .saveAsTable(\"tbl\")\n      val deltaTable = io.delta.tables.DeltaTable.forName(\"tbl\")\n\n      // replace where\n      spark.range(20)\n        .withColumn(\"b\", lit(1L))\n        .repartition(3) // write 3 files\n        .write\n        .format(\"delta\")\n        .option(\"replaceWhere\", \"b = 1\") // partial match\n        .mode(\"overwrite\")\n        .saveAsTable(\"tbl\")\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n      val numAddedChangeFiles = if (enableCDF) {\n        deltaLog.getChanges(1).flatMap {\n          case (a, v) => v\n        }.filter(_.isInstanceOf[AddCDCFile])\n          .toSeq\n          .size\n      } else {\n        0\n      }\n\n      // get expected byte level metrics\n      val (numAddedBytesExpected, numRemovedBytesExpected) =\n        getLastCommitNumAddedAndRemovedBytes(deltaLog)\n\n      if (enableStats) {\n        checkOperationMetrics(\n          Map(\n            \"numFiles\" -> \"4\", // 3(append) + 1(delete)\n            \"numOutputRows\" -> \"25\", // 20 + 5\n            \"numCopiedRows\" -> \"5\",\n            \"numAddedChangeFiles\" -> numAddedChangeFiles.toString,\n            \"numDeletedRows\" -> \"5\",\n            \"numOutputBytes\" -> numAddedBytesExpected.toString,\n            \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n            \"numRemovedFiles\" -> \"1\"\n          ),\n          getOperationMetrics(deltaTable.history(1)),\n          replaceWhereMetricsSchema\n        )\n      } else {\n        checkOperationMetrics(\n          Map(\n            \"numFiles\" -> \"4\", // 3(append) + 1(delete)\n            \"numAddedChangeFiles\" -> numAddedChangeFiles.toString,\n            \"numOutputBytes\" -> numAddedBytesExpected.toString,\n            \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n            \"numRemovedFiles\" -> \"1\"\n          ),\n          getOperationMetrics(deltaTable.history(1)),\n          replaceWhereMetricsSchema.filter(!_.contains(\"Rows\"))\n        )\n\n      }\n    }\n  }\n\n  Seq(\"true\", \"false\").foreach { enableArbitraryRW =>\n    testReplaceWhere(s\"replaceWhere on partition column \" +\n        s\"- arbitraryReplaceWhere=${enableArbitraryRW}\") { (enableCDF, enableStats) =>\n      withSQLConf(\n          DeltaSQLConf.REPLACEWHERE_DATACOLUMNS_ENABLED.key -> enableArbitraryRW,\n          DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED.key -> \"true\") {\n        withTable(\"tbl\") {\n          // create a table with one row\n          spark.range(10)\n            .repartition(1) // 1 file table\n            .withColumn(\"b\", lit(1))\n            .write\n            .format(\"delta\")\n            .partitionBy(\"b\")\n            .saveAsTable(\"tbl\")\n          val deltaTable = io.delta.tables.DeltaTable.forName(\"tbl\")\n\n          // replace where\n          spark.range(20)\n            .repartition(2) // write 2 files\n            .withColumn(\"b\", lit(1))\n            .write\n            .format(\"delta\")\n            .option(\"replaceWhere\", \"b = 1\") // partial match\n            .mode(\"overwrite\")\n            .saveAsTable(\"tbl\")\n\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n          // get expected byte level metrics\n          val (numAddedBytesExpected, numRemovedBytesExpected) =\n            getLastCommitNumAddedAndRemovedBytes(deltaLog)\n\n          // metrics are a subset here as it would involve a partition delete\n          if (enableArbitraryRW.toBoolean) {\n            if (enableStats) {\n              checkOperationMetrics(\n                Map(\n                  \"numFiles\" -> \"2\",\n                  \"numOutputRows\" -> \"20\",\n                  \"numAddedChangeFiles\" -> \"0\",\n                  \"numRemovedFiles\" -> \"1\",\n                  \"numCopiedRows\" -> \"0\",\n                  \"numOutputBytes\" -> numAddedBytesExpected.toString,\n                  \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n                  \"numDeletedRows\" -> \"10\"\n              ),\n                getOperationMetrics(deltaTable.history(1)),\n                replaceWhereMetricsSchema\n              )\n            } else {\n              checkOperationMetrics(\n                Map(\n                  \"numFiles\" -> \"2\",\n                  \"numAddedChangeFiles\" -> \"0\",\n                  \"numOutputBytes\" -> numAddedBytesExpected.toString,\n                  \"numRemovedBytes\" -> numRemovedBytesExpected.toString,\n                  \"numRemovedFiles\" -> \"1\"\n                ),\n                getOperationMetrics(deltaTable.history(1)),\n                replaceWhereMetricsSchema.filter(!_.contains(\"Rows\"))\n              )\n\n            }\n          } else {\n            // legacy replace where mentioned output rows regardless of stats or not.\n            checkOperationMetrics(\n              Map(\n                \"numFiles\" -> \"2\",\n                \"numOutputRows\" -> \"20\",\n                \"numOutputBytes\" -> numAddedBytesExpected.toString\n              ),\n              getOperationMetrics(deltaTable.history(1)),\n              DeltaOperationMetrics.WRITE ++ DeltaOperationMetrics.OVERWRITE_REMOVES\n            )\n          }\n        }\n      }\n    }\n  }\n\n  test(\"replaceWhere metrics turned off - reverts to old behavior\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\",\n        DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\",\n        // We need to turn RowTracking off b/c it needs the row count\n        // statistics w/ [[DELTA_COLLECT_STATS]] enabled.\n        // We automatically enable [[RowTracking]] as part\n        // of CCv1.5's QoL features enablement.\n        DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"false\",\n        DeltaSQLConf.REPLACEWHERE_METRICS_ENABLED.key -> \"false\",\n        DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED.key -> \"false\") {\n      withTable(\"tbl\") {\n        // create a table with one row\n        spark.range(10)\n          .repartition(1) // 1 file table\n          .withColumn(\"b\", lit(1))\n          .write\n          .format(\"delta\")\n          .partitionBy(\"b\")\n          .saveAsTable(\"tbl\")\n        val deltaTable = io.delta.tables.DeltaTable.forName(\"tbl\")\n\n        // replace where\n        spark.range(20)\n          .repartition(2) // write 2 files\n          .withColumn(\"b\", lit(1))\n          .write\n          .format(\"delta\")\n          .option(\"replaceWhere\", \"b = 1\") // partial match\n          .mode(\"overwrite\")\n          .saveAsTable(\"tbl\")\n\n        checkOperationMetrics(\n          Map(\n            \"numFiles\" -> \"2\",\n            \"numOutputRows\" -> \"20\"\n          ),\n          getOperationMetrics(deltaTable.history(1)),\n          DeltaOperationMetrics.WRITE\n        )\n      }\n    }\n  }\n\n  test(\"enable remove metrics in insert with overwrite\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\",\n        DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\",\n        // We need to turn RowTracking off b/c it needs the row count\n        // statistics w/ [[DELTA_COLLECT_STATS]] enabled.\n        // We automatically enable [[RowTracking]] as part\n        // of CCv1.5's QoL features enablement.\n        DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"false\",\n        DeltaSQLConf.REPLACEWHERE_METRICS_ENABLED.key -> \"false\",\n        DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED.key -> \"true\") {\n      withTable(\"tbl\") {\n        spark.range(10).repartition(4).write.format(\"delta\").saveAsTable(\"tbl\")\n        spark.range(20).repartition(2).write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"tbl\")\n        val deltaTable = io.delta.tables.DeltaTable.forName(\"tbl\")\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n        checkOperationMetrics(\n          Map(\n            \"numFiles\" -> \"2\",\n            \"numOutputRows\" -> \"20\",\n            \"numRemovedFiles\" -> \"4\"\n          ),\n          operationMetrics,\n          DeltaOperationMetrics.WRITE ++ DeltaOperationMetrics.OVERWRITE_REMOVES\n        )\n        assert(operationMetrics(\"numRemovedBytes\").toLong > 0)\n      }\n    }\n  }\n\n  test(\"operation metrics - create table - v2\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\",\n        DeltaSQLConf.OVERWRITE_REMOVE_METRICS_ENABLED.key -> \"true\") {\n      val tblName = \"tblName\"\n      withTable(tblName) {\n        // Create\n        spark.range(100).writeTo(tblName).using(\"delta\").create()\n        val deltaTable = io.delta.tables.DeltaTable.forName(spark, tblName)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        var operationMetrics = getOperationMetrics(deltaTable.history(1))\n        var expectedMetrics = Map(\n          \"numFiles\" -> deltaLog.snapshot.numOfFiles.toString,\n          \"numOutputRows\" -> \"100\"\n        )\n        assert(operationMetrics(\"numOutputBytes\").toLong > 0)\n        checkOperationMetrics(expectedMetrics, operationMetrics, DeltaOperationMetrics.WRITE)\n\n        // replace\n        spark.range(50).writeTo(tblName).using(\"delta\").replace()\n        deltaLog.update()\n        expectedMetrics = Map(\n          \"numFiles\" -> deltaLog.snapshot.numOfFiles.toString,\n          \"numOutputRows\" -> \"50\"\n        )\n        operationMetrics = getOperationMetrics(deltaTable.history(1))\n        assert(operationMetrics(\"numOutputBytes\").toLong > 0)\n        checkOperationMetrics(\n          expectedMetrics,\n          operationMetrics,\n          DeltaOperationMetrics.WRITE ++ DeltaOperationMetrics.OVERWRITE_REMOVES\n        )\n\n        // create or replace\n        spark.range(70).writeTo(tblName).using(\"delta\").createOrReplace()\n        deltaLog.update()\n        expectedMetrics = Map(\n          \"numFiles\" -> deltaLog.snapshot.numOfFiles.toString,\n          \"numOutputRows\" -> \"70\"\n        )\n        operationMetrics = getOperationMetrics(deltaTable.history(1))\n        assert(operationMetrics(\"numOutputBytes\").toLong > 0)\n        checkOperationMetrics(\n          expectedMetrics,\n          operationMetrics,\n          DeltaOperationMetrics.WRITE ++ DeltaOperationMetrics.OVERWRITE_REMOVES\n        )\n      }\n    }\n  }\n\n  test(\"operation metrics for RESTORE\") {\n      withTempDir { dir =>\n        // version 0\n        spark.range(5).write.format(\"delta\").save(dir.getCanonicalPath)\n\n        val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, dir.getAbsolutePath)\n        val numFilesV0 = deltaLog.snapshot.numOfFiles\n        val sizeBytesV0 = deltaLog.snapshot.sizeInBytes\n\n        // version 1\n        spark.range(10, 12).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n        val numFilesV1 = deltaLog.snapshot.numOfFiles\n        val sizeBytesV1 = deltaLog.snapshot.sizeInBytes\n\n        // version 2 - RESTORE table to version 0\n        sql(s\"RESTORE TABLE delta.`${dir.getAbsolutePath}` VERSION AS OF 0\")\n\n        val expectedMetrics = Map(\n          \"tableSizeAfterRestore\" -> sizeBytesV0,\n          \"numOfFilesAfterRestore\" -> numFilesV0,\n          \"numRemovedFiles\" -> (numFilesV1 - numFilesV0),\n          \"numRestoredFiles\" -> 0,\n          \"removedFilesSize\" -> (sizeBytesV1 - sizeBytesV0),\n          \"restoredFilesSize\" -> 0).mapValues(_.toString).toMap\n\n        val operationMetrics = getOperationMetrics(deltaTable.history(1))\n\n        checkOperationMetrics(\n          expectedMetrics, operationMetrics, DeltaOperationMetrics.RESTORE)\n\n        // check operation parameters\n        checkLastOperation(\n          dir.getAbsolutePath,\n          expectedOperationParameters = Seq(\"version\", \"timestamp\"),\n          expectedColVals = Seq(\"RESTORE\", \"0\"),\n          columns = Seq($\"operation\", $\"operationParameters.version\"))\n\n        // we can check metrics for a case where we restore files as well.\n        // version 3\n        spark.range(10, 12).write.format(\"delta\").mode(\"append\").save(dir.getCanonicalPath)\n\n        // version 4  - delete all rows\n        sql(s\"DELETE FROM delta.`${dir.getAbsolutePath}`\")\n\n        val numFilesV4 = deltaLog.update().numOfFiles\n        val sizeBytesV4 = deltaLog.update().sizeInBytes\n\n        // version 5 - RESTORE table to version 3\n        sql(s\"RESTORE TABLE delta.`${dir.getAbsolutePath}` VERSION AS OF 3\")\n\n        val numFilesV5 = deltaLog.update().numOfFiles\n        val sizeBytesV5 = deltaLog.update().sizeInBytes\n\n        val expectedMetrics2 = Map(\n          \"tableSizeAfterRestore\" -> sizeBytesV5,\n          \"numOfFilesAfterRestore\" -> numFilesV5,\n          \"numRemovedFiles\" -> 0,\n          \"numRestoredFiles\" -> (numFilesV5 - numFilesV4),\n          \"removedFilesSize\" -> 0,\n          \"restoredFilesSize\" -> (sizeBytesV5 - sizeBytesV4)).mapValues(_.toString).toMap\n\n        val operationMetrics2 = getOperationMetrics(deltaTable.history(1))\n\n        checkOperationMetrics(\n          expectedMetrics2, operationMetrics2, DeltaOperationMetrics.RESTORE)\n      }\n  }\n\n\n  test(\"test output schema of describe delta history command\") {\n    val tblName = \"tbl\"\n    withTable(tblName) {\n      sql(s\"CREATE TABLE $tblName(id bigint) USING DELTA\")\n      val deltaTable = io.delta.tables.DeltaTable.forName(tblName)\n      val expectedSchema = StructType(Seq(\n        StructField(\"version\", LongType, nullable = true),\n        StructField(\"timestamp\", TimestampType, nullable = true),\n        StructField(\"userId\", StringType, nullable = true),\n        StructField(\"userName\", StringType, nullable = true),\n        StructField(\"operation\", StringType, nullable = true),\n        StructField(\"operationParameters\",\n          MapType(StringType, StringType, valueContainsNull = true), nullable = true),\n        StructField(\"job\",\n          StructType(Seq(\n            StructField(\"jobId\", StringType, nullable = true),\n            StructField(\"jobName\", StringType, nullable = true),\n            StructField(\"jobRunId\", StringType, nullable = true),\n            StructField(\"runId\", StringType, nullable = true),\n            StructField(\"jobOwnerId\", StringType, nullable = true),\n            StructField(\"triggerType\", StringType, nullable = true))),\n          nullable = true),\n        StructField(\"notebook\",\n          StructType(Seq(StructField(\"notebookId\", StringType, nullable = true))), nullable = true),\n        StructField(\"clusterId\", StringType, nullable = true),\n        StructField(\"readVersion\", LongType, nullable = true),\n        StructField(\"isolationLevel\", StringType, nullable = true),\n        StructField(\"isBlindAppend\", BooleanType, nullable = true),\n        StructField(\"operationMetrics\",\n          MapType(StringType, StringType, valueContainsNull = true), nullable = true),\n        StructField(\"userMetadata\", StringType, nullable = true),\n        StructField(\"engineInfo\", StringType, nullable = true)))\n\n      // Test schema from [[io.delta.tables.DeltaTable.history]] api\n      val df1 = deltaTable.history(1)\n      assert(df1.schema == expectedSchema)\n\n      // Test schema from SQL api\n      val df2 = spark.sql(s\"DESCRIBE HISTORY $tblName LIMIT 1\")\n      assert(df2.schema == expectedSchema)\n    }\n  }\n\n  testPathWrite(\"DPO and replaceWhere conflict throws exception\") { path =>\n    val ex = intercept[DeltaIllegalArgumentException] {\n      testData(Seq(10), Seq(1)).write.format(\"delta\")\n        .mode(SaveMode.Overwrite)\n        .option(\"replaceWhere\", \"part = 1\")\n        .option(\"partitionOverwriteMode\", \"dynamic\")\n        .save(path)\n    }\n    assert(ex.getErrorClass === \"DELTA_REPLACE_WHERE_WITH_DYNAMIC_PARTITION_OVERWRITE\")\n  }(WriteOptionsAssertion())\n\n  override def executePathWriteTest(\n      write: String => Unit)(assertions: WriteOptionsAssertion): Unit = {\n    withTempDir { tempDir =>\n      val path = createPartitionedTable(tempDir)\n      write(path)\n      assertWriteOptions(path, assertions)\n    }\n  }\n\n  /**\n   * Execute a write operation and run assertions on a name-based table.\n   */\n  protected def executeTableWriteTest(\n      write: String => Unit)(assertions: WriteOptionsAssertion): Unit = {\n    withTable(\"test_table\") {\n      // Create initial partitioned table\n      Seq((1, 1, \"event1\"), (2, 2, \"event2\"), (3, 1, \"event3\"))\n        .toDF(\"id\", \"part\", \"event_name\")\n        .write.format(\"delta\").partitionBy(\"part\").saveAsTable(\"test_table\")\n\n      // Execute the write operation\n      write(\"test_table\")\n\n      // Assert write options using table name\n      val opParams = getTableCommitInfo(\"test_table\")\n      assertWriteOptionsFromParams(opParams, assertions)\n    }\n  }\n\n  def assertWriteOptions(\n      path: String,\n      assertions: WriteOptionsAssertion): Unit = {\n    val opParams = getCommitOpParams(path)\n    assertWriteOptionsFromParams(opParams, assertions)\n  }\n\n  private def assertWriteOptionsFromParams(\n      opParams: Map[String, String],\n      asserts: WriteOptionsAssertion): Unit = {\n    val expected = Map.newBuilder[String, String]\n\n    if (asserts.isDynamicPartitionOverwrite) expected += (\"isDynamicPartitionOverwrite\" -> \"true\")\n    if (asserts.canOverwriteSchema) expected += (\"canOverwriteSchema\" -> \"true\")\n    if (asserts.canMergeSchema) expected += (\"canMergeSchema\" -> \"true\")\n    asserts.predicate.foreach(pred => expected += (\"predicate\" -> pred))\n    assertInHistory(opParams, expected.result())\n  }\n\n  def assertInHistory(opParams: Map[String, String], expected: Map[String, String]): Unit = {\n    val allParams = Seq(\n      \"isDynamicPartitionOverwrite\", \"predicate\", \"canOverwriteSchema\", \"canMergeSchema\"\n    )\n    expected.foreach { case (key, value) =>\n      assert(opParams.get(key).exists(_.contains(value)),\n        s\"Expected $key=$value in DESCRIBE HISTORY, got ${opParams.get(key)}\")\n    }\n\n    assertNotInHistory(opParams, (allParams.toSet -- expected.keySet).toSeq: _*)\n  }\n\n  def assertNotInHistory(opParams: Map[String, String], keys: String*): Unit = {\n    keys.foreach { key =>\n      assert(!opParams.contains(key), s\"Expected $key not in DESCRIBE HISTORY\")\n    }\n  }\n\n  def getCommitOpParams(tablePath: String): Map[String, String] = {\n    val recentCommits = DeltaLog.forTable(spark, tablePath).history.getHistory(Some(1))\n    DeltaLog.forTable(spark, tablePath).history.getHistory(Some(1)).head.operationParameters\n  }\n\n  def getTableCommitInfo(tableName: String): Map[String, String] = {\n    val deltaTable = io.delta.tables.DeltaTable.forName(spark, tableName)\n    deltaTable.history(1).select(\"operationParameters\")\n      .head()\n      .getMap(0)\n      .asInstanceOf[Map[String, String]]\n  }\n\n  test(\"isV1SaveAsTableOverwrite logged only for v1 saveAsTable overwrite\") {\n    withTable(\"tbl\") {\n      // Initial create via saveAsTable - not an overwrite, flag should be absent\n      spark.range(5).write.format(\"delta\").saveAsTable(\"tbl\")\n      assert(!getTableCommitInfo(\"tbl\").contains(\"isV1SaveAsTableOverwrite\"))\n\n      // v1 saveAsTable overwrite -> ReplaceTable with isV1SaveAsTableOverwrite = true\n      spark.range(10).write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"tbl\")\n      assert(getTableCommitInfo(\"tbl\").get(\"isV1SaveAsTableOverwrite\").contains(\"true\"))\n\n      // SQL REPLACE TABLE AS SELECT -> ReplaceTable but not v1 saveAsTable, flag should be absent\n      sql(\"CREATE OR REPLACE TABLE tbl USING delta AS SELECT id FROM range(3)\")\n      assert(!getTableCommitInfo(\"tbl\").contains(\"isV1SaveAsTableOverwrite\"))\n\n      // V2 writeTo.createOrReplace -> ReplaceTable but not v1 saveAsTable, flag should be absent\n      spark.range(5).writeTo(\"tbl\").using(\"delta\").createOrReplace()\n      assert(!getTableCommitInfo(\"tbl\").contains(\"isV1SaveAsTableOverwrite\"))\n\n      // V2 writeTo.replace -> ReplaceTable but not v1 saveAsTable, flag should be absent\n      spark.range(5).writeTo(\"tbl\").using(\"delta\").replace()\n      assert(!getTableCommitInfo(\"tbl\").contains(\"isV1SaveAsTableOverwrite\"))\n    }\n  }\n}\n\ncase class WriteOptionsAssertion(\n    mode: String = \"\",\n    isDynamicPartitionOverwrite: Boolean = false,\n    canOverwriteSchema: Boolean = false,\n    canMergeSchema: Boolean = false,\n    predicate: Option[String] = None\n)\n\n/**\n * Shared test utilities for validating write options in DESCRIBE HISTORY / commit stats.\n */\ntrait WriteOptionsTestBase {\n  this: QueryTest with SharedSparkSession =>\n  import testImplicits._\n\n  // Execute a write operation and run assertions.\n  protected def executePathWriteTest(write: String => Unit)(assertions: WriteOptionsAssertion): Unit\n\n  // Execute a table based write operation and run assertions.\n  protected def executeTableWriteTest(\n    write: String => Unit)(assertions: WriteOptionsAssertion): Unit\n\n  // Data generation helpers\n  def createPartitionedTable(tempDir: java.io.File): String = {\n    val tablePath = tempDir.getAbsolutePath\n    Seq((1, 1, \"event1\"), (2, 2, \"event2\"), (3, 1, \"event3\"))\n      .toDF(\"id\", \"part\", \"event_name\")\n      .write.format(\"delta\").partitionBy(\"part\").save(tablePath)\n    tablePath\n  }\n\n  def testData(ids: Seq[Int], parts: Seq[Int]): DataFrame = {\n    ids.zip(parts).map { case (id, part) => (id, part, s\"event$id\") }\n      .toDF(\"id\", \"part\", \"event_name\")\n  }\n\n  def testDataWithCols(id: Int, part: Int, extraCols: (String, String)*): DataFrame = {\n    var df = Seq((id, part, s\"event$id\")).toDF(\"id\", \"part\", \"event_name\")\n    extraCols.foreach { case (name, value) =>\n      df = df.withColumn(name, lit(value))\n    }\n    df\n  }\n\n  def testPathWrite(\n      testName: String)(testBody: String => Unit)(assertions: WriteOptionsAssertion): Unit = {\n    test(testName) {\n      executePathWriteTest(testBody)(assertions)\n    }\n  }\n\n  def testTableWrite(\n      testName: String)(testBody: String => Unit)(assertions: WriteOptionsAssertion): Unit = {\n    test(testName) {\n      executeTableWriteTest(testBody)(assertions)\n    }\n  }\n\n  def testWriteVariants(testName: String)(\n      writeVariants: Seq[(String, Boolean, String => Unit)])(\n      assertions: WriteOptionsAssertion): Unit = {\n    writeVariants.foreach { case (variantName, isPathBased, writeFunc) =>\n      test(s\"$testName via $variantName\") {\n        if (isPathBased) {\n          executePathWriteTest(writeFunc)(assertions)\n        } else {\n          executeTableWriteTest(writeFunc)(assertions)\n        }\n      }\n    }\n  }\n\n  testWriteVariants(\"write options for dynamic partition overwrite\")(\n    Seq(\n      (\"SQL\", true, { path: String =>\n        withSQLConf(SQLConf.PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n          spark.sql(s\"INSERT OVERWRITE TABLE delta.`$path` \" +\n            s\"SELECT 5 as id, 1 as part, 'event5' as event_name\")\n        }\n      }),\n      (\"DFv1\", true, { path: String =>\n        testData(Seq(4), Seq(1)).write.format(\"delta\")\n          .mode(SaveMode.Overwrite)\n          .option(\"partitionOverwriteMode\", \"dynamic\")\n          .save(path)\n      }),\n      (\"DFv1 saveAsTable\", false, { tableName: String =>\n        withSQLConf(SQLConf.PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n          testData(Seq(7), Seq(1))\n            .write\n            .format(\"delta\")\n            .mode(SaveMode.Overwrite)\n            .option(\"partitionOverwriteMode\", \"dynamic\")\n            .partitionBy(\"part\")\n            .saveAsTable(tableName)\n        }\n      }),\n      (\"DFv2\", true, { path: String =>\n        testData(Seq(5), Seq(1)).writeTo(s\"delta.`$path`\").overwritePartitions()\n      })\n    )\n  )(WriteOptionsAssertion(isDynamicPartitionOverwrite = true))\n\n  testWriteVariants(\"write options for replaceWhere\")(\n    Seq(\n      (\"SQL\", true, { path: String =>\n        spark.sql(s\"INSERT INTO TABLE delta.`$path` \" +\n          s\"REPLACE WHERE part = 1 SELECT 6 as id, 1 as part, 'event6' as event_name\")\n      }),\n      (\"DFv1\", true, { path: String =>\n        testData(Seq(5, 6), Seq(1, 1)).write.format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"replaceWhere\", \"part = 1\")\n          .save(path)\n      }),\n      (\"DFv1 saveAsTable\", false, { tableName: String =>\n        testData(Seq(7, 8), Seq(1, 1))\n          .write\n          .format(\"delta\")\n          .mode(SaveMode.Overwrite)\n          .option(\"replaceWhere\", \"part = 1\")\n          .partitionBy(\"part\")\n          .saveAsTable(tableName)\n      }),\n      (\"DFv2\", true, { path: String =>\n        testData(Seq(5, 6), Seq(1, 1)).writeTo(s\"delta.`$path`\")\n          .overwrite($\"part\" === 1)\n      })\n    )\n  )(WriteOptionsAssertion(predicate = Some(\"part = 1\")))\n\n  testPathWrite(\"explicitly false option not persisted in commit info\") { path =>\n    testData(Seq(9), Seq(1)).write.format(\"delta\")\n      .mode(SaveMode.Append)\n      .option(\"mergeSchema\", \"false\")\n      .save(path)\n  }(WriteOptionsAssertion())\n\n  testPathWrite(\"multiple false options not persisted in commit info\") { path =>\n    testData(Seq(13), Seq(1)).write.format(\"delta\")\n      .mode(SaveMode.Overwrite)\n      .option(\"mergeSchema\", \"false\")\n      .option(\"overwriteSchema\", \"false\")\n      .save(path)\n  }(WriteOptionsAssertion())\n\n  testPathWrite(\"write options for overwriteSchema\") { path =>\n    testDataWithCols(7, 1, \"newcol\" -> \"extra\").write.format(\"delta\")\n      .mode(SaveMode.Overwrite)\n      .option(\"overwriteSchema\", \"true\")\n      .save(path)\n  }(WriteOptionsAssertion(canOverwriteSchema = true))\n\n  testWriteVariants(\"write options for DFv2 replace with overwriteSchema\")(\n    Seq(\n      (\"replace()\", false, { path: String =>\n        testDataWithCols(14, 1, \"newcol\" -> \"extra\")\n          .writeTo(path)\n          .using(\"delta\")\n          .option(\"overwriteSchema\", \"true\")\n          .replace()\n      }),\n      (\"createOrReplace()\", false, { path: String =>\n        testDataWithCols(15, 1, \"newcol\" -> \"extra\")\n          .writeTo(path)\n          .using(\"delta\")\n          .option(\"overwriteSchema\", \"true\")\n          .createOrReplace()\n      })\n    )\n  )(WriteOptionsAssertion(canOverwriteSchema = true))\n\n  testWriteVariants(\"write options for DFv2 replace with mergeSchema\")(\n    Seq(\n      (\"replace()\", false, { path: String =>\n        testDataWithCols(16, 1, \"newcol\" -> \"extra\")\n          .writeTo(path)\n          .using(\"delta\")\n          .option(\"mergeSchema\", \"true\")\n          .replace()\n      }),\n      (\"createOrReplace()\", false, { path: String =>\n        testDataWithCols(17, 1, \"newcol\" -> \"extra\")\n          .writeTo(path)\n          .using(\"delta\")\n          .option(\"mergeSchema\", \"true\")\n          .createOrReplace()\n      })\n    )\n  )(WriteOptionsAssertion(canMergeSchema = true))\n\n  testWriteVariants(\"write options for mergeSchema\")(\n    Seq(\n      (\"DFv1 option\", true, { path: String =>\n        testDataWithCols(8, 1, \"newcol\" -> \"extra\", \"anothercol\" -> \"extra2\")\n          .write.format(\"delta\")\n          .mode(SaveMode.Append)\n          .option(\"mergeSchema\", \"true\")\n          .save(path)\n      }),\n      (\"saveAsTable\", false, { tableName: String =>\n        testDataWithCols(14, 1, \"newcol\" -> \"extra\")\n          .write\n          .format(\"delta\")\n          .mode(SaveMode.Append)\n          .option(\"mergeSchema\", \"true\")\n          .saveAsTable(tableName)\n      }),\n      (\"SQL config\", true, { path: String =>\n        withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n          testDataWithCols(13, 1, \"newcol\" -> \"extra\").write.format(\"delta\")\n            .mode(SaveMode.Append).save(path)\n        }\n      })\n    )\n  )(WriteOptionsAssertion(mode = \"Append\", canMergeSchema = true))\n\n  testPathWrite(\"write options - both replaceWhere and overwriteSchema logged \" +\n    \"even though replaceWhere takes precedence\") { path =>\n    testData(Seq(12), Seq(1)).write.format(\"delta\")\n      .mode(SaveMode.Overwrite)\n      .option(\"replaceWhere\", \"part = 1\")\n      .option(\"overwriteSchema\", \"true\")\n      .save(path)\n  }(WriteOptionsAssertion(\n    canOverwriteSchema = true,\n    predicate = Some(\"part = 1\")\n  ))\n\n  testPathWrite(\"write options - mergeSchema and overwriteSchema combination\") { path =>\n    testDataWithCols(11, 1, \"newcol\" -> \"extra\").write.format(\"delta\")\n      .mode(SaveMode.Overwrite)\n      .option(\"overwriteSchema\", \"true\")\n      .option(\"mergeSchema\", \"true\")\n      .save(path)\n  }(WriteOptionsAssertion(canOverwriteSchema = true, canMergeSchema = true))\n\n  testPathWrite(\"write options - DPO with mergeSchema\") { path =>\n    testDataWithCols(14, 1, \"newcol\" -> \"extra\").write.format(\"delta\")\n      .mode(SaveMode.Overwrite)\n      .option(\"partitionOverwriteMode\", \"dynamic\")\n      .option(\"mergeSchema\", \"true\")\n      .save(path)\n  }(WriteOptionsAssertion(isDynamicPartitionOverwrite = true, canMergeSchema = true))\n\n  testPathWrite(\"write options - DFv2 overwriteSchema option\") { path =>\n    testDataWithCols(7, 1)\n      .writeTo(s\"delta.`$path`\")\n      .option(\"overwriteSchema\", \"true\")\n      .append()\n  }(WriteOptionsAssertion(mode = \"Append\", canOverwriteSchema = true))\n\n  testPathWrite(\"write options - DFv2 mergeSchema option\") { path =>\n    testDataWithCols(8, 1, \"newcol\" -> \"extra\")\n      .writeTo(s\"delta.`$path`\")\n      .option(\"mergeSchema\", \"true\")\n      .append()\n  }(WriteOptionsAssertion(mode = \"Append\", canMergeSchema = true))\n\n  testPathWrite(\"write options - REPLACE TABLE with DPO\") { path =>\n    withSQLConf(SQLConf.PARTITION_OVERWRITE_MODE.key -> \"dynamic\") {\n      spark.sql(s\"\"\"\n        CREATE OR REPLACE TABLE delta.`$path`\n        USING delta\n        PARTITIONED BY (part)\n        AS SELECT 7 as id, 1 as part, 'event7' as event_name\n      \"\"\")\n    }\n  }(WriteOptionsAssertion(isDynamicPartitionOverwrite = true))\n}\n\nclass DescribeDeltaHistorySuite\n  extends DescribeDeltaHistorySuiteBase with DeltaSQLCommandTest\n\nclass DescribeDeltaHistoryWithCatalogOwnedBatch100Suite extends DescribeDeltaHistorySuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey, \"false\")\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DomainMetadataRemovalSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DomainMetadataRemovalSuite\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest {\n\n  val testTableName = \"test_domain_metadata_removal_table\"\n\n  def addData(deltaLog: DeltaLog, start: Long, end: Long): Unit = {\n    spark.range(start, end, step = 1, numPartitions = 2)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(deltaLog.dataPath.toString)\n  }\n\n  def createTableWithDomainMetadata(): DeltaLog = {\n    sql(s\"DROP TABLE IF EXISTS $testTableName\")\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (id LONG)\n         |USING DELTA\n         |TBLPROPERTIES(\n         |'delta.feature.${DomainMetadataTableFeature.name}' = 'supported'\n         |)\"\"\".stripMargin)\n\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n    assert(deltaLog.update().protocol.isFeatureSupported(DomainMetadataTableFeature))\n    addData(deltaLog, 0, 100)\n    deltaLog\n  }\n\n  def dropFeature(\n      feature: TableFeature,\n      truncateHistory: Boolean = false): Unit = {\n    val sqlText =\n      s\"\"\"\n         |ALTER TABLE $testTableName\n         |DROP FEATURE ${feature.name}\n         |${if (truncateHistory) \"TRUNCATE HISTORY\" else \"\"}\n         |\"\"\".stripMargin\n    sql(sqlText)\n  }\n\n  def validateDomainMetadataRemoval(deltaLog: DeltaLog): Unit = {\n    val snapshot = deltaLog.update()\n    assert(!snapshot.protocol.isFeatureSupported(DomainMetadataTableFeature))\n    assert(snapshot.domainMetadata.isEmpty)\n  }\n\n  test(\"DomainMetadata can be dropped\") {\n    val deltaLog = createTableWithDomainMetadata()\n    dropFeature(DomainMetadataTableFeature)\n    validateDomainMetadataRemoval(deltaLog)\n  }\n\n  test(\"Drop DomainMetadata feature when leaked domain metadata exist in the table\") {\n    case class TestMetadataDomain() extends JsonMetadataDomain[TestMetadataDomain] {\n      override val domainName: String = \"delta.test\"\n    }\n\n    val deltaLog = createTableWithDomainMetadata()\n\n    // Add a domainMetadata action.\n    deltaLog\n      .startTransaction(catalogTableOpt = None)\n      .commit(Seq(TestMetadataDomain().toDomainMetadata), DeltaOperations.ManualUpdate)\n\n    dropFeature(DomainMetadataTableFeature)\n    validateDomainMetadataRemoval(deltaLog)\n  }\n\n  test(\"Cannot drop DomainMetadata if there is a dependent feature on the table\") {\n    createTableWithDomainMetadata()\n\n    // Enable row tracking on the table.\n    sql(\n      s\"\"\"ALTER TABLE $testTableName\n         |SET TBLPROPERTIES(\n         |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\n         |)\"\"\".stripMargin)\n\n    val e = intercept[DeltaTableFeatureException] {\n      dropFeature(DomainMetadataTableFeature)\n    }\n    checkError(\n      e,\n      \"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\",\n      parameters = Map(\n        \"feature\" -> \"domainMetadata\",\n        \"dependentFeatures\" -> \"rowTracking\"))\n  }\n\n  test(\"Drop domainMetadata after dropping a dependent feature\") {\n    val deltaLog = createTableWithDomainMetadata()\n    addData(deltaLog, 0, 100)\n\n    // Enable row tracking on the table. This also enables the domainMetadata feature.\n    sql(\n      s\"\"\"ALTER TABLE $testTableName\n         |SET TBLPROPERTIES(\n         |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\n         |)\"\"\".stripMargin)\n\n    assert(deltaLog.update().protocol.isFeatureSupported(DomainMetadataTableFeature))\n\n    dropFeature(RowTrackingFeature)\n    dropFeature(DomainMetadataTableFeature)\n    validateDomainMetadataRemoval(deltaLog)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DomainMetadataSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.concurrent.ExecutionException\n\nimport scala.util.{Failure, Success, Try}\n\nimport org.apache.spark.sql.delta.DeltaOperations.{ManualUpdate, Truncate}\nimport org.apache.spark.sql.delta.Relocated.CheckpointFileManager\nimport org.apache.spark.sql.delta.actions.{DomainMetadata, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.junit.Assert._\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DomainMetadataSuite\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest {\n  import testImplicits._\n\n  private def sortByDomain(domainMetadata: Seq[DomainMetadata]): Seq[DomainMetadata] =\n    domainMetadata.sortBy(_.domain)\n\n  /**\n   * A helper to validate the [[DomainMetadata]] actions can be retained during the delta state\n   * reconstruction.\n   *\n   * @param doCheckpoint: Explicitly create a delta log checkpoint if marked as true.\n   * @param doChecksum: Disable writting checksum file if marked as false.\n  */\n  private def validateStateReconstructionHelper(\n      doCheckpoint: Boolean,\n      doChecksum: Boolean): Unit = {\n    val table = \"testTable\"\n    withTable(table) {\n      withSQLConf(\n        DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> doChecksum.toString) {\n        sql(\n          s\"\"\"\n             | CREATE TABLE $table(id int) USING delta\n             | tblproperties\n             | ('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n             |\"\"\".stripMargin)\n        (1 to 100).toDF(\"id\").write.format(\"delta\").mode(\"append\").saveAsTable(table)\n\n        var deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n        def deltaLog = deltaTable.deltaLog\n        assert(deltaTable.snapshot.domainMetadata.isEmpty)\n\n        val domainMetadata = DomainMetadata(\"testDomain1\", \"\", false) ::\n          DomainMetadata(\"testDomain2\", \"{\\\"key1\\\":\\\"value1\\\"\", false) :: Nil\n        deltaTable.startTransactionWithInitialSnapshot().commit(domainMetadata, Truncate())\n        assertEquals(sortByDomain(domainMetadata), sortByDomain(deltaLog.update().domainMetadata))\n        assert(deltaLog.update().logSegment.checkpointProvider.version === -1)\n\n        if (doCheckpoint) {\n          deltaLog.checkpoint(deltaLog.unsafeVolatileSnapshot)\n          // Clear the DeltaLog cache to force creating a new DeltaLog instance which will build\n          // the Snapshot from the checkpoint file.\n          DeltaLog.clearCache()\n          deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n          assert(!deltaTable.snapshot.logSegment.checkpointProvider.isEmpty)\n\n          assertEquals(\n            sortByDomain(domainMetadata),\n            sortByDomain(deltaTable.snapshot.domainMetadata))\n        }\n\n        DeltaLog.clearCache()\n        deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n        val checksumOpt = deltaTable.snapshot.checksumOpt\n        if (doChecksum) {\n          assertEquals(\n            sortByDomain(checksumOpt.get.domainMetadata.get), sortByDomain(domainMetadata))\n        } else {\n          assert(checksumOpt.isEmpty)\n        }\n        assert(deltaLog.update().validateChecksum())\n      }\n    }\n  }\n\n  // A helper to validate [[DomainMetadata]] actions can be deleted.\n  private def validateDeletionHelper(doCheckpoint: Boolean, doChecksum: Boolean): Unit = {\n    val table = \"testTable\"\n    withTable(table) {\n      withSQLConf(\n        DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> doChecksum.toString\n      ) {\n        sql(\n          s\"\"\"\n             | CREATE TABLE $table(id int) USING delta\n             | tblproperties\n             | ('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n             |\"\"\".stripMargin)\n        (1 to 100).toDF(\"id\").write.format(\"delta\").mode(\"append\").saveAsTable(table)\n\n        DeltaLog.clearCache()\n        val deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n        val deltaLog = deltaTable.deltaLog\n        assert(deltaTable.snapshot.domainMetadata.isEmpty)\n\n        val domainMetadata = DomainMetadata(\"testDomain1\", \"\", false) ::\n          DomainMetadata(\"testDomain2\", \"{\\\"key1\\\":\\\"value1\\\"}\", false) :: Nil\n\n        deltaTable.startTransactionWithInitialSnapshot().commit(domainMetadata, Truncate())\n        assertEquals(sortByDomain(domainMetadata), sortByDomain(deltaLog.update().domainMetadata))\n        assert(deltaLog.update().logSegment.checkpointProvider.version === -1)\n\n        // Delete testDomain1.\n        deltaTable.startTransaction().commit(\n          DomainMetadata(\"testDomain1\", \"\", true) :: Nil, Truncate())\n        val domainMetadatasAfterDeletion = DomainMetadata(\n          \"testDomain2\",\n          \"{\\\"key1\\\":\\\"value1\\\"}\", false) :: Nil\n        assertEquals(\n          sortByDomain(domainMetadatasAfterDeletion),\n          sortByDomain(deltaLog.update().domainMetadata))\n\n        // Create a new commit and validate the incrementally built snapshot state respects the\n        // DomainMetadata deletion.\n        deltaTable.startTransaction().commit(Nil, ManualUpdate)\n        var snapshot = deltaLog.update()\n        assertEquals(sortByDomain(domainMetadatasAfterDeletion), snapshot.domainMetadata)\n        if (doCheckpoint) {\n          deltaLog.checkpoint(snapshot)\n          assertEquals(\n            sortByDomain(domainMetadatasAfterDeletion),\n            deltaLog.update().domainMetadata)\n        }\n\n        // force state reconstruction and validate it respects the DomainMetadata retention.\n        DeltaLog.clearCache()\n        snapshot = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))._2\n        assertEquals(sortByDomain(domainMetadatasAfterDeletion), snapshot.domainMetadata)\n      }\n    }\n  }\n\n  test(\"DomainMetadata actions tracking in CRC should stop once threshold is crossed\") {\n    def assertDomainMetadatas(\n        deltaLog: DeltaLog,\n        expectedDomainMetadatas: Seq[DomainMetadata],\n        expectedInCrc: Boolean): Unit = {\n      val snapshot = deltaLog.update()\n      assert(snapshot.validateChecksum())\n      assertEquals(sortByDomain(expectedDomainMetadatas), sortByDomain(snapshot.domainMetadata))\n      assert(snapshot.checksumOpt.nonEmpty)\n      if (expectedInCrc) {\n        assert(snapshot.checksumOpt.get.domainMetadata.nonEmpty)\n        assertEquals(\n          sortByDomain(expectedDomainMetadatas),\n          sortByDomain(snapshot.checksumOpt.get.domainMetadata.get))\n      } else {\n        assert(snapshot.checksumOpt.get.domainMetadata.isEmpty)\n      }\n    }\n\n    val table = \"testTable\"\n    withSQLConf(\n      DeltaSQLConf.DELTA_MAX_DOMAIN_METADATAS_IN_CRC.key -> \"2\") {\n      withTable(table) {\n        sql(\n          s\"\"\"\n             | CREATE TABLE $table(id int) USING delta\n             | tblproperties\n             | ('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n             |\"\"\".stripMargin)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n        assertDomainMetadatas(deltaLog, Seq.empty, true)\n\n        deltaLog\n          .startTransaction()\n          .commit(DomainMetadata(\"testDomain1\", \"\", false) :: Nil, Truncate())\n        assertDomainMetadatas(\n          deltaLog, DomainMetadata(\"testDomain1\", \"\", false) :: Nil, true)\n\n        deltaLog\n          .startTransaction()\n          .commit(DomainMetadata(\"testDomain2\", \"\", false) :: Nil, Truncate())\n        assertDomainMetadatas(\n          deltaLog,\n          DomainMetadata(\"testDomain1\", \"\", false) ::\n            DomainMetadata(\"testDomain2\", \"\", false) :: Nil,\n          true)\n\n        deltaLog\n          .startTransaction()\n          .commit(DomainMetadata(\"testDomain3\", \"\", false) :: Nil, Truncate())\n        assertDomainMetadatas(\n          deltaLog,\n          DomainMetadata(\"testDomain1\", \"\", false) ::\n            DomainMetadata(\"testDomain2\", \"\", false) ::\n            DomainMetadata(\"testDomain3\", \"\", false) :: Nil,\n          false)\n      }\n    }\n  }\n\n  test(\"Validate crc can be read when domainMetadata is missing\") {\n    val table = \"testTable\"\n    withTable(table) {\n      sql(\n        s\"\"\"\n           | CREATE TABLE $table(id int) USING delta\n           | tblproperties\n           | ('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n           |\"\"\".stripMargin)\n      val deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n      val deltaLog = deltaTable.deltaLog\n      val version =\n        deltaTable\n          .startTransactionWithInitialSnapshot()\n          .commit(DomainMetadata(\"testDomain1\", \"\", false) :: Nil, Truncate())\n      val snapshot = deltaLog.update()\n      assert(snapshot.checksumOpt.nonEmpty)\n      assert(snapshot.checksumOpt.get.domainMetadata.nonEmpty)\n      val originalChecksum = snapshot.checksumOpt.get\n\n      // Write out a checksum without domainMetadata.\n      val checksumWithoutDomainMetadata = originalChecksum.copy(domainMetadata = None)\n      val writer = CheckpointFileManager.create(deltaLog.logPath, deltaLog.newDeltaHadoopConf())\n      val toWrite = JsonUtils.toJson(checksumWithoutDomainMetadata) + \"\\n\"\n      val stream = writer.createAtomic(\n        FileNames.checksumFile(deltaLog.logPath, version + 1),\n        overwriteIfPossible = false)\n      stream.write(toWrite.getBytes(UTF_8))\n      stream.close()\n\n      // Make sure the read is not broken.\n      val content =\n        deltaLog\n          .store\n          .read(\n            FileNames.checksumFile(deltaLog.logPath, version + 1),\n            deltaLog.newDeltaHadoopConf())\n      val checksumFromFile = JsonUtils.mapper.readValue[VersionChecksum](content.head)\n      assert(checksumWithoutDomainMetadata == checksumFromFile)\n    }\n  }\n\n\n  test(\"DomainMetadata action survives state reconstruction [w/o checkpoint, w/o checksum]\") {\n    validateStateReconstructionHelper(doCheckpoint = false, doChecksum = false)\n  }\n\n  test(\"DomainMetadata action survives state reconstruction [w/ checkpoint, w/ checksum]\") {\n    validateStateReconstructionHelper(doCheckpoint = true, doChecksum = true)\n  }\n\n  test(\"DomainMetadata action survives state reconstruction [w/ checkpoint, w/o checksum]\") {\n    validateStateReconstructionHelper(doCheckpoint = true, doChecksum = false)\n  }\n\n  test(\"DomainMetadata action survives state reconstruction [w/o checkpoint, w/ checksum]\") {\n    validateStateReconstructionHelper(doCheckpoint = false, doChecksum = true)\n  }\n\n  test(\"DomainMetadata deletion [w/o checkpoint, w/o checksum]\") {\n    validateDeletionHelper(doCheckpoint = false, doChecksum = false)\n  }\n\n  test(\"DomainMetadata deletion [w/ checkpoint, w/o checksum]\") {\n    validateDeletionHelper(doCheckpoint = true, doChecksum = false)\n  }\n\n  test(\"DomainMetadata deletion [w/o checkpoint, w/ checksum]\") {\n    validateDeletionHelper(doCheckpoint = false, doChecksum = true)\n  }\n\n  test(\"DomainMetadata deletion [w/ checkpoint, w/ checksum]\") {\n    validateDeletionHelper(doCheckpoint = true, doChecksum = true)\n  }\n\n  test(\"Multiple DomainMetadatas with the same domain should fail in single transaction\") {\n    val table = \"testTable\"\n    withTable(table) {\n      sql(\n        s\"\"\"\n           | CREATE TABLE $table(id int) USING delta\n           | tblproperties\n           | ('${TableFeatureProtocolUtils.propertyKey(DomainMetadataTableFeature)}' = 'enabled')\n           |\"\"\".stripMargin)\n      (1 to 100).toDF(\"id\").write.format(\"delta\").mode(\"append\").saveAsTable(table)\n      val deltaTable = DeltaTableV2(spark, TableIdentifier(table))\n      val domainMetadata =\n        DomainMetadata(\"testDomain1\", \"\", false) ::\n          DomainMetadata(\"testDomain1\", \"\", false) :: Nil\n      val e = intercept[DeltaIllegalArgumentException] {\n        deltaTable.startTransactionWithInitialSnapshot().commit(domainMetadata, Truncate())\n      }\n      checkError(e, \"DELTA_DUPLICATE_DOMAIN_METADATA_INTERNAL_ERROR\", \"42601\",\n        Map(\"domainName\" -> \"testDomain1\"))\n    }\n  }\n\n  test(\"Validate the failure when table feature is not enabled\") {\n    withTempDir { dir =>\n      (1 to 100).toDF().write.format(\"delta\").save(dir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val domainMetadata = DomainMetadata(\"testDomain1\", \"\", false) :: Nil\n      val e = intercept[DeltaIllegalArgumentException] {\n        deltaLog.startTransaction().commit(domainMetadata, Truncate())\n      }\n      checkError(e, \"DELTA_DOMAIN_METADATA_NOT_SUPPORTED\", \"0A000\",\n        Map(\"domainNames\" -> \"[testDomain1]\"))\n    }\n  }\n\n  test(\"Validate the lifespan of metadata domains for the REPLACE TABLE operation\") {\n    val existingDomainMetadatas =\n      DomainMetadata(\"testDomain1\", \"\", false) ::\n        DomainMetadata(\"testDomain2\", \"\", false) ::\n        Nil\n    val newDomainMetadatas =\n        DomainMetadata(\"testDomain2\", \"key=val\", false) ::\n        DomainMetadata(\"testDomain3\", \"\", false) ::\n        Nil\n\n    val result = DomainMetadataUtils.handleDomainMetadataForReplaceTable(\n      existingDomainMetadatas, newDomainMetadatas)\n\n    // testDomain1: survives by default (not in the final list since it already\n    //              exists in the snapshot).\n    // testDomain2: overwritten by new domain metadata\n    // testDomain3: added to the final list since it only appears in the new set.\n    assert(result ===\n        DomainMetadata(\"testDomain2\", \"key=val\", false) :: // Overwritten\n        DomainMetadata(\"testDomain3\", \"\", false) :: // New metadata domain\n        Nil)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/DuplicatingListLogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File}\nimport java.nio.charset.StandardCharsets\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.{HDFSLogStore}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport com.google.common.io.Files\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DuplicatingListLogStore(sparkConf: SparkConf, defaultHadoopConf: Configuration)\n  extends HDFSLogStore(sparkConf, defaultHadoopConf) {\n\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    val list = super.listFrom(path, hadoopConf).toSeq\n    // The first listing if directory will be listed twice to mimic the WASBS Log Store\n    if (!list.isEmpty && list.head.isDirectory) {\n      (Seq(list.head) ++ list).toIterator\n    } else {\n      list.toIterator\n    }\n  }\n}\n\nclass DuplicatingListLogStoreSuite extends SharedSparkSession with DeltaSQLCommandTest {\n\n  override def sparkConf: SparkConf = {\n    super.sparkConf.set(\"spark.databricks.tahoe.logStore.class\",\n      classOf[DuplicatingListLogStore].getName)\n  }\n\n  def pathExists(deltaLog: DeltaLog, filePath: String): Boolean = {\n    val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n    fs.exists(DeltaFileOperations.absolutePath(deltaLog.dataPath.toString, filePath))\n  }\n\n  test(\"vacuum should handle duplicate listing\") {\n    withTempDir { dir =>\n      // create cdc file (lexicographically < _delta_log)\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      val deltaTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      val cdcDir = new File(new Path(dir.getAbsolutePath, \"_change_data\").toString)\n      cdcDir.mkdir()\n      val cdcPath = new File(\n        new Path(cdcDir.getAbsolutePath, \"dupFile\").toString)\n      Files.write(\"test\", cdcPath, StandardCharsets.UTF_8)\n\n      require(pathExists(deltaLog, cdcPath.toString))\n      require(pathExists(deltaLog, cdcDir.toString))\n\n      withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n        deltaTable.vacuum(0)\n\n        // check if path doesn't exists\n        assert(!pathExists(deltaLog, cdcPath.toString))\n\n        // to delete directories\n        deltaTable.vacuum(0)\n        assert(!pathExists(deltaLog, cdcDir.toString))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/EvolvabilitySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.hadoop.fs.Path\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.Trigger\nimport org.apache.spark.sql.types.StringType\nimport org.apache.spark.util.Utils\n\nclass EvolvabilitySuite extends EvolvabilitySuiteBase with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  test(\"delta 0.1.0\") {\n    testEvolvability(\"src/test/resources/delta/delta-0.1.0\")\n  }\n\n  test(\"delta 0.1.0 - case sensitivity enabled\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      testEvolvability(\"src/test/resources/delta/delta-0.1.0\")\n    }\n  }\n\n  test(\"serialized partition values must contain null values\") {\n    val tempDir = Utils.createTempDir().toString\n    val df1 = spark.range(5).withColumn(\"part\", lit(null).cast(StringType))\n    val df2 = spark.range(5).withColumn(\"part\", lit(\"1\"))\n    df1.union(df2).coalesce(1).write.partitionBy(\"part\").format(\"delta\").save(tempDir)\n\n    // Clear the cache\n    DeltaLog.clearCache()\n    val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n    val dataThere = deltaLog.snapshot.allFiles.collect().forall { addFile =>\n      if (!addFile.partitionValues.contains(\"part\")) {\n        fail(s\"The partition values: ${addFile.partitionValues} didn't contain the column 'part'.\")\n      }\n      val value = addFile.partitionValues(\"part\")\n      value === null || value === \"1\"\n    }\n\n    assert(dataThere, \"Partition values didn't match with null or '1'\")\n\n    // Check serialized JSON as well\n    val contents = deltaLog.store.read(\n      FileNames.unsafeDeltaFile(deltaLog.logPath, 0L),\n      deltaLog.newDeltaHadoopConf())\n    assert(contents.exists(_.contains(\"\"\"\"part\":null\"\"\")), \"null value should be written in json\")\n  }\n\n  testQuietly(\"parse old version LastCheckpointInfo\") {\n    assert(JsonUtils.mapper.readValue[LastCheckpointInfo](\"\"\"{\"version\":1,\"size\":1}\"\"\")\n      === LastCheckpointInfo(1, 1, None, None, None, None))\n  }\n\n  test(\"parse partial version LastCheckpointInfo\") {\n    assert(JsonUtils.mapper.readValue[LastCheckpointInfo](\n      \"\"\"{\"version\":1,\"size\":1,\"parts\":100}\"\"\") ===\n      LastCheckpointInfo(1, 1, Some(100), None, None, None))\n  }\n\n  // Following tests verify that operations on Delta table won't fail when there is an\n  // unknown column in Delta files and checkpoints.\n  // The modified Delta files and checkpoints with an extra column is generated by\n  // `EvolvabilitySuiteBase.generateTransactionLogWithExtraColumn()`\n\n  test(\"transaction log schema evolvability - batch change data read\") {\n    withTempDir { dir =>\n      withSQLConf(\n        DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n        // All files verification will always fail in this test since we the extra column\n        // will not be present in the `allFiles` of the CRC.\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"false\",\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n          \"false\"\n      ) {\n        EvolvabilitySuiteBase.generateTransactionLogWithExtraColumn(spark, dir.getAbsolutePath)\n        spark.sql(s\"UPDATE delta.`${dir.getAbsolutePath}` SET value = 10\")\n        spark.read.format(\"delta\").option(\"readChangeFeed\", \"true\")\n          .option(\"startingVersion\", 0).load(dir.getAbsolutePath).collect()\n\n        val expectedPreimage = (1 until 10).flatMap(x => Seq(x, x)).toSeq\n        val expectedPostimage = Seq.fill(18)(10)\n        testCdfUpdate(dir.getAbsolutePath, 6, expectedPreimage, expectedPostimage)\n      }\n    }\n  }\n\n  test(\"transaction log schema evolvability - streaming change data read\") {\n    withTempDir { dir =>\n      withSQLConf(\n        DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n        // All files verification will always fail in this test since we the extra column\n        // will not be present in the `allFiles` of the CRC.\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"false\",\n        DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n          \"false\"\n      ) {\n        EvolvabilitySuiteBase.generateTransactionLogWithExtraColumn(spark, dir.getAbsolutePath)\n        spark.sql(s\"UPDATE delta.`${dir.getAbsolutePath}` SET value = 10\")\n        val query = spark.readStream.format(\"delta\")\n          .option(\"readChangeFeed\", \"true\")\n          .option(\"startingVersion\", 0)\n          .load(dir.getAbsolutePath)\n          .writeStream.format(\"noop\").start()\n        try {\n          query.processAllAvailable()\n        } finally {\n          query.stop()\n        }\n\n        val expectedPreimage = (1 until 10).flatMap(x => Seq(x, x)).toSeq\n        val expectedPostimage = Seq.fill(18)(10)\n        testCdfUpdate(dir.getAbsolutePath, 6, expectedPreimage, expectedPostimage, true)\n      }\n    }\n  }\n\n  test(\"transaction log schema evolvability - batch read\") {\n    testLogSchemaEvolvability(\n      (path: String) => { spark.read.format(\"delta\").load(path).collect() }\n    )\n  }\n\n  test(\"transaction log schema evolvability - batch write\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        (10 until 20).map(num => (num, num)).toDF(\"key\", \"value\")\n          .write.format(\"delta\").mode(\"append\").save(path)\n        spark.read.format(\"delta\").load(path).collect()\n      }\n    )\n  }\n\n  test(\"transaction log schema evolvability - streaming read\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        val query = spark.readStream.format(\"delta\").load(path).writeStream.format(\"noop\").start()\n        try {\n          query.processAllAvailable()\n        } finally {\n          query.stop()\n        }\n      }\n    )\n  }\n\n  test(\"transaction log schema evolvability - streaming write\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        withTempDir { tempDir =>\n          val memStream = MemoryStream[(Int, Int)]\n          memStream.addData((11, 11), (12, 12))\n          val stream = memStream.toDS().toDF(\"key\", \"value\")\n            .coalesce(1).writeStream\n            .format(\"delta\")\n            .trigger(Trigger.Once)\n            .outputMode(\"append\")\n            .option(\"checkpointLocation\", tempDir.getCanonicalPath + \"/cp\")\n            .start(path)\n          try {\n            stream.processAllAvailable()\n          } finally {\n            stream.stop()\n          }\n        }\n      }\n    )\n  }\n\n  test(\"transaction log schema evolvability - describe commands\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        spark.sql(s\"DESCRIBE delta.`$path`\")\n        spark.sql(s\"DESCRIBE HISTORY delta.`$path`\")\n        spark.sql(s\"DESCRIBE DETAIL delta.`$path`\")\n      }\n    )\n  }\n\n  test(\"transaction log schema evolvability - vacuum\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        sql(s\"VACUUM delta.`$path`\")\n      }\n    )\n  }\n\n  test(\"transaction log schema evolvability - alter table\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        sql(s\"ALTER TABLE delta.`$path` ADD COLUMNS (col int)\")\n      }\n    )\n  }\n\n  test(\"transaction log schema evolvability - delete\") {\n    testLogSchemaEvolvability(\n      (path: String) => { sql(s\"DELETE FROM delta.`$path` WHERE key = 1\") }\n    )\n  }\n\n  test(\"transaction log schema evolvability - update\") {\n    testLogSchemaEvolvability(\n      (path: String) => { sql(s\"UPDATE delta.`$path` set value = 100 WHERE key = 1\") }\n    )\n  }\n\n  test(\"transaction log schema evolvability - merge\") {\n    testLogSchemaEvolvability(\n      (path: String) => {\n        withTable(\"source\") {\n          Seq((1, 5), (11, 12))\n            .toDF(\"key\", \"value\")\n            .write\n            .mode(\"overwrite\")\n            .format(\"delta\")\n            .saveAsTable(\"source\")\n          sql(\n            s\"\"\"\n               |MERGE INTO delta.`$path` tgrt\n               |USING source src\n               |ON src.key = tgrt.key\n               |WHEN MATCHED THEN\n               |  UPDATE SET key = 20 + src.key, value = 20 + src.value\n               |WHEN NOT MATCHED THEN\n               |  INSERT (key, value) VALUES (src.key + 5, src.value + 10)\n           \"\"\".stripMargin\n          )\n        }\n      }\n    )\n  }\n\n  test(\"Delta Lake issue 1229: able to read a checkpoint containing `numRecords`\") {\n    // table created using Delta 1.2.1 which has additional field `numRecords` in\n    // checkpoint schema. It is removed in version after 1.2.1.\n    // Make sure we are able to read the Delta table in the latest version.\n    val tablePath = \"src/test/resources/delta/delta-1.2.1\"\n    assert(\n      spark.read.format(\"delta\")\n        .load(tablePath).where(\"col1 = 8\").count() === 9L)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/EvolvabilitySuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, FileAction, SingleAction}\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.util.Utils\n\nabstract class EvolvabilitySuiteBase extends QueryTest with SharedSparkSession\n    with DeltaSQLTestUtils {\n  import testImplicits._\n\n  protected def testEvolvability(tablePath: String): Unit = {\n    // Check we can load everything from a log checkpoint\n    val deltaLog = DeltaLog.forTable(spark, new Path(tablePath))\n    val path = deltaLog.dataPath.toString\n    checkDatasetUnorderly(\n      spark.read.format(\"delta\").load(path).select(\"id\", \"value\").as[(Int, String)],\n      4 -> \"d\", 5 -> \"e\", 6 -> \"f\")\n    assert(deltaLog.snapshot.metadata.schema === StructType.fromDDL(\"id INT, value STRING\"))\n    assert(deltaLog.snapshot.metadata.partitionSchema === StructType.fromDDL(\"id INT\"))\n\n    // Check we can load LastCheckpointInfo\n    val lastCheckpointOpt = deltaLog.readLastCheckpointFile()\n    assert(lastCheckpointOpt.get.version === 3)\n    assert(lastCheckpointOpt.get.size === 6L)\n    assert(lastCheckpointOpt.get.checkpointSchema.isEmpty)\n\n    // Check we can parse all `Action`s in delta files. It doesn't check correctness.\n    deltaLog.getChanges(0L).toList.map(_._2.toList)\n  }\n\n  /**\n   * This tests the evolution of the schema at delta file and checkpoint file.\n   * Operations on the Delta table shouldn't fail when there is an unknown column\n   * in delta file and checkpoint file.\n   *\n   * Table Schema: StructType(StructField(\"key\", StringType), StructField(\"value\", StringType))\n   * Overwritten Delta file: {\"some_new_feature\":{\"a\":1}}\n   * Overwritten checkpoint file with a new column called `unknown` with boolean type.\n   *\n   * The delta file and checkpoint file with an unknown column are generated by\n   * `EvolvabilitySuiteBase.generateTransactionLogWithExtraColumn()`.\n   */\n  protected def testLogSchemaEvolvability(operation: String => Unit): Unit = {\n    withTempDir { tempDir =>\n      // copy the existing dir to the temp data dir.\n      FileUtils.copyDirectory(\n        new File(\"src/test/resources/delta/transaction_log_schema_evolvability\"), tempDir)\n      makeWritable(tempDir)\n      DeltaLog.clearCache()\n      operation(tempDir.getAbsolutePath)\n    }\n  }\n\n  /**\n   * Recursively make all files in a directory writable.\n   */\n  private def makeWritable(directory: File): Unit = {\n    if (!directory.isDirectory) return\n    directory.listFiles().foreach { file =>\n      if (file.isDirectory) {\n        makeWritable(file)\n      } else {\n        file.setWritable(true)\n      }\n    }\n  }\n\n  /**\n   * Read from a table's CDF and check for the expected preimage/postimage after applying an update\n   */\n  protected def testCdfUpdate(\n      tablePath: String,\n      commitVersion: Long,\n      expectedPreimage: Seq[Int],\n      expectedPostimage: Seq[Int],\n      streaming: Boolean = false): Unit = {\n\n    val df = if (streaming) {\n      val q = spark.readStream.format(\"delta\")\n        .option(\"readChangeFeed\", \"true\")\n        .option(\"startingVersion\", commitVersion)\n        .option(\"endingVersion\", commitVersion)\n        .load(tablePath)\n        .writeStream\n        .option(\"checkpointLocation\", tablePath + \"-checkpoint\")\n        .toTable(\"streaming\");\n      try {\n        q.processAllAvailable()\n      } finally {\n        q.stop()\n      }\n      spark.read.table(\"streaming\")\n    } else {\n      spark.read.format(\"delta\")\n        .option(\"readChangeFeed\", \"true\")\n        .option(\"startingVersion\", commitVersion)\n        .option(\"endingVersion\", commitVersion)\n        .load(tablePath)\n    }\n\n    val preimage = df.where(\"_change_type = 'update_preimage'\").select(\"value\")\n    val postimage = df.where(\"_change_type = 'update_postimage'\").select(\"value\")\n\n    checkAnswer(preimage, expectedPreimage.map(Row(_)))\n    checkAnswer(postimage, expectedPostimage.map(Row(_)))\n  }\n}\n\n\n// scalastyle:off\n/***\n * A tool to generate data and transaction log for evolvability tests.\n *\n * Here are the steps to generate data.\n *\n * 1. Update `EvolvabilitySuite.generateData` if there are new [[Action]] types.\n * 2. Change the following command with the right path and run it. Note: the working directory is \"[delta_project_root]\".\n *\n * scalastyle:off\n * ```\n * build/sbt \"core/test:runMain org.apache.spark.sql.delta.EvolvabilitySuite src/test/resources/delta/delta-0.1.0 generateData\"\n * ```\n *\n * You can also use this tool to generate DeltaLog that contains a checkpoint a json log with a new column.\n *\n * scalastyle:off\n * ```\n * build/sbt \"core/test:runMain org.apache.spark.sql.delta.EvolvabilitySuite /path/src/test/resources/delta/transaction_log_schema_evolvability generateTransactionLogWithExtraColumn\"\n * ```\n */\n// scalastyle:on\nobject EvolvabilitySuiteBase {\n\n  def generateData(\n      spark: SparkSession,\n      path: String,\n      tblProps: Map[DeltaConfig[_], String] = Map.empty): Unit = {\n    import org.apache.spark.sql.delta.implicits._\n    implicit val sparkSession: SparkSession = spark\n    implicit val s = spark.sqlContext\n\n    Seq(1, 2, 3).toDF(spark).write.format(\"delta\").save(path)\n    if (tblProps.nonEmpty) {\n      val tblPropsStr = tblProps.map { case (k, v) => s\"'${k.key}' = '$v'\" }.mkString(\", \")\n      spark.sql(s\"CREATE TABLE test USING DELTA LOCATION '$path'\")\n      spark.sql(s\"ALTER TABLE test SET TBLPROPERTIES($tblPropsStr)\")\n    }\n    Seq(1, 2, 3).toDF(spark).write.format(\"delta\").mode(\"append\").save(path)\n    Seq(1, 2, 3).toDF(spark).write.format(\"delta\").mode(\"overwrite\").save(path)\n\n    val checkpoint = Utils.createTempDir().toString\n    val data = StreamingTestShims.MemoryStream[Int]\n    data.addData(1, 2, 3)\n    val stream = data.toDF()\n      .writeStream\n      .format(\"delta\")\n      .option(\"checkpointLocation\", checkpoint)\n      .start(path)\n    stream.processAllAvailable()\n    stream.stop()\n\n    DeltaLog.forTable(spark, path).checkpoint()\n  }\n\n  /** Validate the generated data contains all [[Action]] types */\n  def validateData(spark: SparkSession, path: String): Unit = {\n    import org.apache.spark.sql.delta.util.FileNames._\n    import scala.reflect.runtime.{universe => ru}\n    import org.apache.spark.sql.delta.implicits._\n\n    val mirror = ru.runtimeMirror(this.getClass.getClassLoader)\n\n    val tpe = ru.typeOf[Action]\n    val clazz = tpe.typeSymbol.asClass\n    assert(clazz.isSealed, s\"${classOf[Action]} must be sealed\")\n\n    val deltaLog = DeltaLog.forTable(spark, new Path(path))\n    val deltas = 0L to deltaLog.snapshot.version\n    val deltaFiles = deltas.map(unsafeDeltaFile(deltaLog.logPath, _)).map(_.toString)\n    val actionsTypesInLog =\n      spark.read.schema(Action.logSchema).json(deltaFiles: _*)\n        .as[SingleAction]\n        .collect()\n        .map(_.unwrap.getClass.asInstanceOf[Class[_]])\n        .toSet\n\n    val allActionTypes =\n      clazz.knownDirectSubclasses\n        .flatMap {\n          case t if t == ru.typeOf[FileAction].typeSymbol => t.asClass.knownDirectSubclasses\n          case t => Set(t)\n        }\n        .map(t => mirror.runtimeClass(t.asClass))\n\n    val missingTypes = allActionTypes -- actionsTypesInLog\n    val unknownTypes = actionsTypesInLog -- allActionTypes\n    assert(\n      missingTypes.isEmpty,\n      s\"missing types: $missingTypes. \" +\n        \"Please update EvolveabilitySuite.generateData to include them in the log.\")\n    assert(\n      unknownTypes.isEmpty,\n      s\"unknown types: $unknownTypes. \" +\n        s\"Please make sure they inherit ${classOf[Action]} or ${classOf[FileAction]} directly.\")\n  }\n\n  /** Generate the transaction log with extra column in checkpoint and json. */\n  def generateTransactionLogWithExtraColumn(spark: SparkSession, path: String): Unit = {\n    // scalastyle:off sparkimplicits\n    import spark.implicits._\n    // scalastyle:on sparkimplicits\n    implicit val s = spark.sqlContext\n\n    val absPath = new File(path).getAbsolutePath\n\n    (1 until 10).map(num => (num, num)).toDF(\"key\", \"value\").write.format(\"delta\").save(path)\n\n    // Enable struct-only stats\n    spark.sql(s\"ALTER TABLE delta.`$absPath` \" +\n      s\"SET TBLPROPERTIES (delta.checkpoint.writeStatsAsStruct = true, \" +\n      \"delta.checkpoint.writeStatsAsJson = false)\")\n\n    (1 until 10).map(num => (num, num)).toDF(\"key\", \"value\").write\n      .format(\"delta\").mode(\"overwrite\").save(path)\n\n    val deltaLog = DeltaLog.forTable(spark, new Path(path))\n\n    deltaLog.checkpoint()\n\n    // Create an incomplete checkpoint without the action and overwrite the\n    // original checkpoint\n    val checkpointPath = FileNames.checkpointFileSingular(deltaLog.logPath,\n      deltaLog.snapshot.version)\n    val tmpCheckpoint = Utils.createTempDir()\n    val checkpointDataWithNewCol = spark.read.parquet(checkpointPath.toString)\n      .withColumn(\"unknown\", lit(true))\n\n    // Keep the add files and also filter by the additional condition\n    checkpointDataWithNewCol.coalesce(1).write\n      .mode(\"overwrite\").parquet(tmpCheckpoint.toString)\n    val writtenCheckpoint =\n      tmpCheckpoint.listFiles().toSeq.filter(_.getName.startsWith(\"part\")).head\n    val checkpointFile = new File(checkpointPath.toUri)\n    new File(deltaLog.logPath.toUri).listFiles().toSeq.foreach { file =>\n      if (file.getName.startsWith(\".0\")) {\n        // we need to delete checksum files,\n        // otherwise trying to replace our incomplete\n        // checkpoint file fails due to the LocalFileSystem's checksum checks.\n        require(file.delete(), \"Failed to delete checksum file\")\n      }\n    }\n    require(checkpointFile.delete(), \"Failed to delete old checkpoint\")\n    require(writtenCheckpoint.renameTo(checkpointFile),\n      \"Failed to rename corrupt checkpoint\")\n\n    (1 until 10).map(num => (num, num)).toDF(\"key\", \"value\").write\n      .format(\"delta\").mode(\"append\").save(path)\n\n    // Shouldn't fail here\n    deltaLog.update()\n\n    val version = deltaLog.snapshot.version\n    // We want to have a delta log with a new column after a checkpoint, to test out operations\n    // against both checkpoint with unknown column and delta log with unkown column.\n\n    // manually remove AddFile in the previous commit and append a new column.\n    val records = deltaLog.store.read(\n      FileNames.unsafeDeltaFile(deltaLog.logPath, version),\n      deltaLog.newDeltaHadoopConf())\n    val actions = records.map(Action.fromJson).filter(action => action.isInstanceOf[AddFile])\n      .map { action => action.asInstanceOf[AddFile].remove}\n      .toIterator\n    val recordsWithNewAction = actions.map(_.json) ++ Iterator(\"\"\"{\"some_new_action\":{\"a\":1}}\"\"\")\n    deltaLog.store.write(\n      FileNames.unsafeDeltaFile(deltaLog.logPath, version + 1),\n      recordsWithNewAction,\n      overwrite = false,\n      deltaLog.newDeltaHadoopConf())\n\n    // manually add those files back and add a unknown field to it.\n    val newRecords = records.map{ record =>\n      val recordMap = JsonUtils.fromJson[Map[String, Any]](record)\n      val newRecordMap = if (recordMap.contains(\"add\")) {\n        // add a unknown column inside action fields.\n        val actionFields = recordMap(\"add\").asInstanceOf[Map[String, Any]] +\n          (\"some_new_column_in_add_action\" -> 1)\n        recordMap + (\"add\" -> actionFields)\n      } else recordMap\n      // add a unknown column outside action fields.\n      JsonUtils.toJson(newRecordMap + (\"some_new_action_alongside_add_action\" -> (\"a\" -> \"1\")))\n    }.toIterator\n    deltaLog.store.write(\n      FileNames.unsafeDeltaFile(deltaLog.logPath, version + 2),\n      newRecords,\n      overwrite = false,\n      deltaLog.newDeltaHadoopConf())\n\n    // Shouldn't fail here\n    deltaLog.update()\n\n    DeltaLog.clearCache()\n  }\n\n  def main(args: Array[String]): Unit = {\n    val spark = SparkSession.builder().master(\"local[2]\").getOrCreate()\n    val path = new File(args(0))\n    if (path.exists()) {\n      // Don't delete automatically in case the user types a wrong path.\n      // scalastyle:off throwerror\n      throw new AssertionError(s\"${path.getCanonicalPath} exists. Please delete it and retry.\")\n      // scalastyle:on throwerror\n    }\n    args(1) match {\n      case \"generateData\" =>\n        generateData(spark, path.toString)\n        validateData(spark, path.toString)\n      case \"generateTransactionLogWithExtraColumn\" =>\n        generateTransactionLogWithExtraColumn(spark, path.toString)\n      case _ =>\n        throw new RuntimeException(\"Unrecognized (or omitted) argument. \" +\n          \"Please try again (no data generated).\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/FakeFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.net.URI\n\nimport org.apache.hadoop.fs.RawLocalFileSystem\n\n/** A fake file system to test whether session Hadoop configuration will be picked up. */\nclass FakeFileSystem extends RawLocalFileSystem {\n  override def getScheme: String = FakeFileSystem.scheme\n  override def getUri: URI = FakeFileSystem.uri\n}\n\nobject FakeFileSystem {\n  val scheme = \"fake\"\n  val uri = URI.create(s\"$scheme:///\")\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/FeatureEnablementConcurrencySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.{AddFile, Format, Metadata}\nimport org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver => TransactionObserver}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, FileNames}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.parquet.hadoop.ParquetFileReader\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetReadSupport\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ThreadUtils\n\nclass FeatureEnablementConcurrencySuite\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with ConflictResolutionTestUtils {\n\n  val testTableName = \"test_feature_enablement_table\"\n\n  /** Represents a transaction that alters a table property. */\n  case class AlterTableProperty(\n      property: String,\n      value: String) extends TestTransaction(Map.empty) {\n    override val name: String = s\"ALTER TABLE($property $value)\"\n    override def dataChange: Boolean = false\n    override def toSQL(tableName: String): String = {\n      s\"ALTER TABLE $tableName SET TBLPROPERTIES ('$property' = '$value')\"\n    }\n  }\n\n  /** Represents a transaction that unsets a table property. */\n  case class UnsetTableProperty(property: String) extends TestTransaction(Map.empty) {\n    override val name: String = s\"UNSET PROPERTY($property)\"\n    override def dataChange: Boolean = false\n    override def toSQL(tableName: String): String = {\n      s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('$property')\"\n    }\n  }\n\n  private def createTestTable(\n      properties: Seq[String] = Seq.empty,\n      numPartitions: Int = 2): (DeltaLog, CatalogTable) = {\n    sql(s\"DROP TABLE IF EXISTS $testTableName\")\n    val propertiesString = if (properties.nonEmpty) properties.mkString(\",\") + \",\" else \"\"\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (idCol bigint)\n         |USING delta\n         |TBLPROPERTIES (\n         |$propertiesString\n         |'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'false',\n         |'${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = 'false'\n         |)\"\"\".stripMargin)\n\n    spark.range(start = 0, end = 100, step = 1, numPartitions)\n      .withColumnRenamed(\"id\", \"idCol\")\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .saveAsTable(testTableName)\n\n    val catalogTable = spark.sessionState.catalog.getTableMetadata(\n      TableIdentifier(testTableName))\n    (DeltaLog.forTable(spark, catalogTable), catalogTable)\n  }\n\n  private def getParquetFooter(deltaLog: DeltaLog, file: AddFile): ParquetMetadata = {\n    val dataPath = deltaLog.dataPath.toString\n    val filePath = file.path\n    val hadoopConf = new Configuration()\n\n    val path = DeltaFileOperations.absolutePath(dataPath, filePath)\n    val fileSystem = path.getFileSystem(hadoopConf)\n    val fileStatus = fileSystem.listStatus(path).head\n    ParquetFileReader.readFooter(hadoopConf, fileStatus)\n  }\n\n  private def validateFooter(footer: ParquetMetadata, expected: Boolean): Unit = {\n    val footerMetadata = footer.getFileMetaData.getKeyValueMetaData\n    assert(footerMetadata.containsKey(ParquetReadSupport.SPARK_METADATA_KEY))\n    val fieldMetadata = footerMetadata.get(ParquetReadSupport.SPARK_METADATA_KEY)\n\n    assert(\n      fieldMetadata.contains(DeltaColumnMapping.PARQUET_FIELD_ID_METADATA_KEY) === expected &&\n        fieldMetadata.contains(DeltaColumnMapping.COLUMN_MAPPING_METADATA_ID_KEY) === expected &&\n        fieldMetadata.contains(DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY) === expected)\n  }\n\n  test(\"Validate Metadata diff\") {\n    val metadataA = Metadata(\n      id = \"idA\",\n      name = \"nameA\",\n      description = \"descriptionA\",\n      format = Format(options = Map(\"OptionA\" -> \"A\")),\n      schemaString = \"schemaA\",\n      partitionColumns = Seq(\"colA1\", \"colA2\"),\n      configuration = Map(\"propA1\" -> \"valueA1\", \"propA2\" -> \"valueA2\"),\n      createdTime = Some(1L))\n\n    val metadataB = Metadata(\n      id = \"idB\",\n      name = \"nameB\",\n      description = \"descriptionB\",\n      format = Format(),\n      schemaString = \"schemaB\",\n      partitionColumns = Seq(\"colB1\"),\n      configuration = Map(\"propB1\" -> \"valueB1\"),\n      createdTime = Some(2L))\n\n    // Diff should output all properties in correct order.\n    val expectedDiff = Set(\n      \"id\", \"name\", \"description\", \"format\", \"schemaString\",\n      \"partitionColumns\", \"configuration\", \"createdTime\")\n    assert(metadataA.diffFieldNames(metadataB) === expectedDiff,\n      \"\"\"The Metadata properties do not match the expected diff.\n        |If you are extending Metadata please check Metadata.diff as well as\n        |ConflictChecker.attemptToResolveMetadataConflicts\"\"\".stripMargin)\n\n    val metadataA_1 = Metadata(id = \"idA\")\n    val metadataB_1 = Metadata(id = \"idB\")\n    assert(metadataA_1.diffFieldNames(metadataB_1) === Set(\"id\"))\n\n    val metadataA_2 = Metadata(id = \"id\", name = \"nameA\")\n    val metadataB_2 = Metadata(id = \"id\", name = \"nameB\")\n    assert(metadataA_2.diffFieldNames(metadataB_2) === Set(\"name\"))\n\n    val metadataA_3 = Metadata(id = \"id\", description = \"descriptionA\")\n    val metadataB_3 = Metadata(id = \"id\", description = \"descriptionB\")\n    assert(metadataA_3.diffFieldNames(metadataB_3) === Set(\"description\"))\n\n    val metadataA_4 = Metadata(id = \"id\", format = Format(options = Map(\"OptionA\" -> \"A\")))\n    val metadataB_4 = Metadata(id = \"id\", format = Format())\n    assert(metadataA_4.diffFieldNames(metadataB_4) === Set(\"format\"))\n\n    val metadataA_5 = Metadata(id = \"id\", schemaString = \"schemaA\")\n    val metadataB_5 = Metadata(id = \"id\", schemaString = \"schemaB\")\n    assert(metadataA_5.diffFieldNames(metadataB_5) === Set(\"schemaString\"))\n\n    val metadataA_6 = Metadata(id = \"id\", partitionColumns = Seq(\"colA1\"))\n    val metadataB_6 = Metadata(id = \"id\", partitionColumns = Seq(\"colB1\"))\n    assert(metadataA_6.diffFieldNames(metadataB_6) === Set(\"partitionColumns\"))\n\n    val metadataA_7 = Metadata(id = \"id\", configuration = Map.empty)\n    val metadataB_7 = Metadata(id = \"id\", configuration = Map(\"propB1\" -> \"valueB1\"))\n    assert(metadataA_7.diffFieldNames(metadataB_7) === Set(\"configuration\"))\n\n    val metadataA_8 = Metadata(id = \"id\", createdTime = Some(1L))\n    val metadataB_8 = Metadata(id = \"id\", createdTime = Some(2L))\n    assert(metadataA_8.diffFieldNames(metadataB_8) === Set(\"createdTime\"))\n\n    val metadataA_9 = Metadata(id = \"idA\", createdTime = Some(1L))\n    val metadataB_9 = Metadata(id = \"idB\", createdTime = Some(2L))\n    assert(metadataA_9.diffFieldNames(metadataB_9) === Set(\"id\", \"createdTime\"))\n  }\n\n  test(\"checkConfigurationChangesForConflicts\") {\n    val (deltaLog, catalogTable) = createTestTable()\n    val snapshot = deltaLog.update(catalogTableOpt = Some(catalogTable))\n    val dummyTransactionInfo = CurrentTransactionInfo(\n      txnId = \"txn 1\",\n      readPredicates = Vector.empty,\n      readFiles = Set.empty,\n      readWholeTable = false,\n      readAppIds = Set.empty,\n      metadata = Metadata(),\n      protocol = snapshot.protocol,\n      actions = Seq.empty[AddFile],\n      readSnapshot = snapshot,\n      commitInfo = None,\n      readRowIdHighWatermark = 0L,\n      catalogTable = Some(catalogTable),\n      domainMetadata = Seq.empty,\n      op = DeltaOperations.ManualUpdate)\n\n    val lastVersion = snapshot.version\n    val dummyCommit = deltaLog\n      .getChangeLogFiles(\n        startVersion = lastVersion,\n        endVersion = lastVersion,\n        catalogTableOpt = Some(catalogTable),\n        failOnDataLoss = false)\n      .map { case (_, file) => file }\n      .filter(FileNames.isDeltaFile)\n      .take(1)\n      .toList\n      .last\n    val dummySummary = WinningCommitSummary.createFromFileStatus(deltaLog, dummyCommit)\n\n    val conflictChecker = new ConflictChecker(\n      spark,\n      initialCurrentTransactionInfo = dummyTransactionInfo,\n      winningCommitSummary = dummySummary,\n      isolationLevel = WriteSerializable)\n\n    // Test 1: Change 2 configs. One is allowed, the other is not.\n    val current = Metadata(configuration = Map(\"prop1\" -> \"value1\", \"prop2\" -> \"value2\"))\n    val winning = Metadata(configuration = Map(\"prop1\" -> \"newValue1\", \"prop2\" -> \"newValue2\"))\n\n    val result1 = conflictChecker.checkConfigurationChangesForConflicts(\n      current,\n      winning,\n      allowList = Set(\"prop1\"))\n    val expected1 = conflictChecker.ConfigurationChanges(\n      areValid = false,\n      changed = Map(\"prop1\" -> \"newValue1\", \"prop2\" -> \"newValue2\"))\n    assert(result1 === expected1)\n\n    // Test 2: Change 2 configs. Both allowed.\n    val result2 = conflictChecker.checkConfigurationChangesForConflicts(\n      current,\n      winning,\n      allowList = Set(\"prop1\", \"prop2\"))\n    val expected2 = conflictChecker.ConfigurationChanges(\n      areValid = true,\n      changed = Map(\"prop1\" -> \"newValue1\", \"prop2\" -> \"newValue2\"))\n    assert(result2 === expected2)\n\n    // Test 3: Same as previous but one property is added instead of changed.\n    val result3 = conflictChecker.checkConfigurationChangesForConflicts(\n      currentMetadata = Metadata(configuration = Map(\"prop1\" -> \"value1\")),\n      winningMetadata =\n        Metadata(configuration = Map(\"prop1\" -> \"newValue1\", \"prop2\" -> \"newValue2\")),\n      allowList = Set(\"prop1\", \"prop2\"))\n    val expected3 = conflictChecker.ConfigurationChanges(\n      areValid = true,\n      added = Map(\"prop2\" -> \"newValue2\"),\n      changed = Map(\"prop1\" -> \"newValue1\"))\n    assert(result3 === expected3)\n\n    // Test 4: Removals are not allowed.\n    val result4 = conflictChecker.checkConfigurationChangesForConflicts(\n      currentMetadata = Metadata(configuration = Map(\"prop1\" -> \"value1\")),\n      winningMetadata = Metadata(configuration = Map(\"prop2\" -> \"newValue2\")),\n      allowList = Set(\"prop1\", \"prop2\"))\n    val expected4 = conflictChecker.ConfigurationChanges(\n      areValid = false,\n      removed = Set(\"prop1\"),\n      added = Map(\"prop2\" -> \"newValue2\"))\n    assert(result4 === expected4)\n\n  }\n\n  def testFeatureDisablement(property: String, withUnset: Boolean): Unit = {\n    val (deltaLog, _) = createTestTable()\n    val ctx = new TestContext(deltaLog)\n    AlterTableProperty(property, value = \"true\")\n      .execute(ctx)\n\n    val businessTxn = Delete(rows = Seq(90L))\n    val disableTxn = if (withUnset) {\n      UnsetTableProperty(property)\n    } else {\n      AlterTableProperty(property, value = \"false\")\n    }\n\n    businessTxn.start(ctx)\n    disableTxn.execute(ctx)\n    val e = intercept[org.apache.spark.SparkException] {\n      businessTxn.commit(ctx)\n    }\n    assert(e.getCause.asInstanceOf[DeltaThrowable].getErrorClass() === \"DELTA_METADATA_CHANGED\")\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Row tracking Enablement:\n   *                 protocol ---------- Unbackfill ------------------------ Metadata ------------\n   *                 Upgrade             Batch 1                             Update\n   *                 prep+commit         prep+commit                         prep+commit\n   *\n   *\n   * Concurrent Txn                                                    prep                commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for {\n    concurrentTxnName <- Seq(\"alterTableProperty\", \"delete\")\n  } test(\"Enable row tracking feature \" +\n      s\"concurrent txn: $concurrentTxnName\") {\n    val (deltaLog, catalogTable) = createTestTable()\n    val ctx = new TestContext(deltaLog)\n\n    val enableFeatureFn = () => {\n      AlterTableProperty(property = DeltaConfigs.ROW_TRACKING_ENABLED.key, value = \"true\")\n        .execute(ctx)\n      Array.empty[Row]\n    }\n\n    val Seq(enableFuture) = runFunctionsWithOrderingFromObserver(Seq(enableFeatureFn)) {\n      case (protocolUpgradeObserver :: Nil) =>\n        val backfillObserver = new TransactionObserver(\n          OptimisticTransactionPhases.forName(\"Backfill\"))\n        val metadataUpdateObserver = new TransactionObserver(\n          OptimisticTransactionPhases.forName(\"Metadata Update\"))\n        protocolUpgradeObserver.setNextObserver(backfillObserver, autoAdvance = true)\n        backfillObserver.setNextObserver(metadataUpdateObserver, autoAdvance = true)\n\n        prepareAndCommitWithNextObserverSet(protocolUpgradeObserver)\n        prepareAndCommitWithNextObserverSet(backfillObserver)\n\n        val concurrentTxn = if (concurrentTxnName == \"alterTableProperty\") {\n          AlterTableProperty(\n            property = DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, value = \"true\")\n        } else {\n          Delete(rows = Seq(90L))\n        }\n        concurrentTxn.start(ctx)\n        concurrentTxn\n          .observer\n          .foreach(o => busyWaitFor(o.phases.commitPhase.hasReached, timeout))\n        prepareAndCommit(metadataUpdateObserver)\n\n        val expectException = concurrentTxnName == \"alterTableProperty\"\n        if (expectException) {\n          val e = intercept[org.apache.spark.SparkException] {\n            concurrentTxn.commit(ctx)\n          }.getCause.asInstanceOf[DeltaThrowable]\n          assert(e.getErrorClass() === \"DELTA_METADATA_CHANGED\")\n        } else {\n          concurrentTxn.commit(ctx)\n        }\n    }\n    ThreadUtils.awaitResult(enableFuture, timeout)\n    assert(DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(\n        deltaLog.update(catalogTableOpt = Some(catalogTable)).metadata))\n  }\n\n  for (withUnset <- BOOLEAN_DOMAIN)\n  test(s\"Disable row tracking feature - withUnset: $withUnset\") {\n    testFeatureDisablement(DeltaConfigs.ROW_TRACKING_ENABLED.key, withUnset)\n  }\n\n  test(\"Validate column metadata schema\") {\n    val (deltaLog, catalogTable) = createTestTable()\n    val schema = deltaLog.update(catalogTableOpt = Some(catalogTable)).metadata.schema\n    assert(schema.fields.head.productArity === 4,\n      \"\"\"\n        |Got a non expected field column arity.\n        |If extending the StructField schema please check validateSchemaChanges.\n        |\"\"\".stripMargin)\n  }\n\n  for (mode <- Seq(NameMapping, IdMapping))\n  test(s\"Create table with column mapping - mode: ${mode.name}\") {\n    val (deltaLog, catalogTable) = createTestTable(\n      properties = Seq(s\"'${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '${mode.name}'\"))\n\n    deltaLog.update(catalogTableOpt = Some(catalogTable)).allFiles.collect().foreach { addFile =>\n      val footer = getParquetFooter(deltaLog, addFile)\n      validateFooter(footer, expected = true)\n    }\n  }\n\n  for (txnInterleaved <- BOOLEAN_DOMAIN)\n  test(s\"Enable column mapping feature - txnInterleaved: $txnInterleaved\") {\n    val (deltaLog, catalogTable) = createTestTable()\n    val ctx = new TestContext(deltaLog)\n\n    val columnMappingEnablementTxn = AlterTableProperty(\n      property = DeltaConfigs.COLUMN_MAPPING_MODE.key, value = NameMapping.name)\n    val businessTxn = Update(rows = Seq(90L), setValue = -1L)\n\n    if (txnInterleaved) {\n      businessTxn.interleave(ctx) {\n        columnMappingEnablementTxn.execute(ctx)\n      }\n    } else {\n      columnMappingEnablementTxn.execute(ctx)\n    }\n\n    val metadata = deltaLog.update(catalogTableOpt = Some(catalogTable)).metadata\n    assert(metadata.columnMappingMode === NameMapping)\n    assert(metadata.schema.fields.map(_.metadata).forall { m =>\n      m.contains(\"delta.columnMapping.id\") && m.contains(\"delta.columnMapping.physicalName\")\n    })\n\n    val tableDf = io.delta.tables.DeltaTable.forName(testTableName).toDF\n    val expectedResult = if (txnInterleaved) {\n      Seq.range(0L, 100L).filterNot(_ == 90L) :+ -1L\n    } else {\n      Seq.range(0L, 100L)\n    }\n    assert(tableDf.orderBy(\"idCol\").collect() === expectedResult.sorted.map(Row(_)))\n\n    // When column mapping is enabled on an existing table we do not expect any metadata in the\n    // parquet footer.\n    deltaLog.update(catalogTableOpt = Some(catalogTable)).allFiles.collect().foreach { addFile =>\n      val footer = getParquetFooter(deltaLog, addFile)\n      validateFooter(footer, expected = false)\n    }\n  }\n\n  for (startMode <- Seq(NameMapping, IdMapping, NoMapping))\n  test(s\"Verify invalid column mapping transitions - startMode: ${startMode.name}\") {\n    val (deltaLog, _) = createTestTable(\n      properties = Seq(s\"'${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '${startMode.name}'\"))\n    val ctx = new TestContext(deltaLog)\n\n    // Not allowed transitions.\n    val newMode = startMode match {\n      case NameMapping => IdMapping\n      case IdMapping => NameMapping\n      case NoMapping => IdMapping\n    }\n    val e = intercept[DeltaColumnMappingUnsupportedException] {\n      AlterTableProperty(property = DeltaConfigs.COLUMN_MAPPING_MODE.key, value = newMode.name)\n        .execute(ctx)\n    }\n    checkError(\n      e,\n      \"DELTA_UNSUPPORTED_COLUMN_MAPPING_MODE_CHANGE\",\n      parameters = Map(\"oldMode\" -> startMode.name, \"newMode\" -> newMode.name))\n  }\n\n  for (startMode <- Seq(NameMapping, IdMapping))\n  test(s\"Removing column mapping mode produces conflict - startMode: ${startMode.name}\") {\n    val (deltaLog, catalogTable) = createTestTable(\n      properties = Seq(s\"'${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '${startMode.name}'\"))\n    val ctx = new TestContext(deltaLog)\n\n    val columnMappingDisablementTxn = AlterTableProperty(\n      property = DeltaConfigs.COLUMN_MAPPING_MODE.key, value = NoMapping.name)\n    val businessTxn = Delete(rows = Seq(90L))\n\n    businessTxn.start(ctx)\n    columnMappingDisablementTxn.execute(ctx)\n    val e = intercept[org.apache.spark.SparkException] {\n      businessTxn.commit(ctx)\n    }\n    assert(e.getCause.asInstanceOf[DeltaThrowable].getErrorClass() === \"DELTA_METADATA_CHANGED\")\n    assert(deltaLog.update(\n      catalogTableOpt = Some(catalogTable)).metadata.columnMappingMode === NoMapping)\n  }\n\n  test(\"Column mapping enablement with RESTORE\") {\n    val (deltaLog, catalogTable) = createTestTable(\n      properties = Seq(s\"'${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '${IdMapping.name}'\"))\n    val ctx = new TestContext(deltaLog)\n\n    val columnMappingEnabledVersion = deltaLog.update(catalogTableOpt = Some(catalogTable)).version\n\n    // Disable column mapping.\n    AlterTableProperty(property = DeltaConfigs.COLUMN_MAPPING_MODE.key, value = NoMapping.name)\n      .execute(ctx)\n\n    // Cannot re-enable column mapping with RESTORE.\n    val e = intercept[DeltaColumnMappingUnsupportedException] {\n      sql(s\"RESTORE TABLE $testTableName TO VERSION AS OF $columnMappingEnabledVersion\")\n    }\n    checkError(\n      e,\n      \"DELTA_UNSUPPORTED_COLUMN_MAPPING_MODE_CHANGE\",\n      parameters = Map(\"oldMode\" -> NoMapping.name, \"newMode\" -> IdMapping.name))\n  }\n\n  test(\"Enable deletion vectors feature\") {\n    val (deltaLog, _) = createTestTable()\n    val ctx = new TestContext(deltaLog)\n\n    val dvEnablementTxn = AlterTableProperty(\n      property = DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, value = \"true\")\n    val businessTxn = Delete(rows = Seq(90L))\n\n    businessTxn.interleave(ctx) {\n      dvEnablementTxn.execute(ctx)\n    }\n\n    assert(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(deltaLog.update().metadata))\n  }\n\n  for (withUnset <- BOOLEAN_DOMAIN)\n  test(s\"Disable Deletion Vectors feature - withUnset: $withUnset\") {\n    testFeatureDisablement(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, withUnset)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/FileMetadataMaterializationTrackerSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.Semaphore\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.scalatest.concurrent.TimeLimits\nimport org.scalatest.time.{Seconds, Span}\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass FileMetadataMaterializationTrackerSuite\n    extends SparkFunSuite\n    with TimeLimits\n    with SharedSparkSession {\n  test(\"tracker - unit test\") {\n\n    def acquireForTask(tracker: FileMetadataMaterializationTracker, numPermits: Int): Unit = {\n      val taskLevelPermitAllocator = tracker.createTaskLevelPermitAllocator()\n      for (i <- 1 to numPermits) {\n        taskLevelPermitAllocator.acquirePermit()\n      }\n    }\n\n    // Initialize the semaphore for tests\n    val totalAvailablePermits = spark.sessionState.conf.getConf(\n      DeltaSQLConf.DELTA_COMMAND_FILE_MATERIALIZATION_LIMIT)\n    val semaphore = new Semaphore(totalAvailablePermits)\n    FileMetadataMaterializationTracker.initializeSemaphoreForTests(semaphore)\n    val tracker = new FileMetadataMaterializationTracker()\n\n    // test that acquiring a permit should work and decrement the available permits.\n    acquireForTask(tracker, 1)\n    assert(semaphore.availablePermits() === totalAvailablePermits - 1)\n\n    // releasing the permit should increment the semaphore's count\n    tracker.releasePermits(1)\n    assert(semaphore.availablePermits() === totalAvailablePermits)\n\n    // test overallocation\n    acquireForTask(tracker, totalAvailablePermits + 1) // allowed to over allocate\n    assert(semaphore.availablePermits() === 0)\n    assert(semaphore.availablePermits() === 0)\n    tracker.releasePermits(totalAvailablePermits + 1)\n    assert(semaphore.availablePermits() === totalAvailablePermits) // make sure we don't overflow\n\n    // test - wait for other task to release overallocation lock\n    acquireForTask(tracker, totalAvailablePermits + 1)\n\n    val acquireThread = new Thread() {\n      override def run(): Unit = {\n        val taskLevelPermitAllocator = tracker.createTaskLevelPermitAllocator()\n        taskLevelPermitAllocator.acquirePermit()\n      }\n    }\n    // we acquire in a separate thread so that we can make sure the acquiring is blocked\n    // until another thread(main thread here) releases a permit.\n    acquireThread.start()\n    Thread.sleep(2000) // Sleep for 2 seconds to make sure the acquireThread is blocked\n    assert(acquireThread.isAlive) // acquire thread is actually blocked\n    tracker.releasePermits(totalAvailablePermits + 1)\n    failAfter(Span(2, Seconds)) {\n      acquireThread.join() // acquire thread should get unblocked\n    }\n\n    // test releaseAllPermits\n    assert(semaphore.availablePermits() === totalAvailablePermits - 1)\n    tracker.releaseAllPermits()\n    assert(semaphore.availablePermits() === totalAvailablePermits)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/FileNamesSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkFunSuite\n\nclass FileNamesSuite extends SparkFunSuite {\n\n  import org.apache.spark.sql.delta.util.FileNames._\n\n  test(\"isDeltaFile\") {\n    assert(isDeltaFile(new Path(\"/a/_delta_log/123.json\")))\n    assert(isDeltaFile(new Path(\"/a/123.json\")))\n    assert(!isDeltaFile(new Path(\"/a/_delta_log/123ajson\")))\n    assert(!isDeltaFile(new Path(\"/a/_delta_log/123.jso\")))\n    assert(!isDeltaFile(new Path(\"/a/_delta_log/123a.json\")))\n    assert(!isDeltaFile(new Path(\"/a/_delta_log/a123.json\")))\n\n    // UUID Files\n    assert(!isDeltaFile(new Path(\"/a/123.uuid.json\")))\n    assert(!isDeltaFile(new Path(\"/a/_delta_log/123.uuid.json\")))\n    assert(isDeltaFile(new Path(\"/a/_delta_log/_staged_commits/123.uuid.json\")))\n    assert(!isDeltaFile(new Path(\"/a/_delta_log/_staged_commits/123.uuid1.uuid2.json\")))\n  }\n\n  test(\"DeltaFile.unapply\") {\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/123.json\")) ===\n      Some((new Path(\"/a/_delta_log/123.json\"), 123)))\n    assert(DeltaFile.unapply(new Path(\"/a/123.json\")) ===\n      Some((new Path(\"/a/123.json\"), 123)))\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/123ajson\")).isEmpty)\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/123.jso\")).isEmpty)\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/123a.json\")).isEmpty)\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/a123.json\")).isEmpty)\n\n    // UUID Files\n    assert(DeltaFile.unapply(new Path(\"/a/123.uuid.json\")).isEmpty)\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/123.uuid.json\")).isEmpty)\n    assert(DeltaFile.unapply(new Path(\"/a/_delta_log/_staged_commits/123.uuid.json\")) ===\n      Some((new Path(\"/a/_delta_log/_staged_commits/123.uuid.json\"), 123)))\n    assert(DeltaFile.unapply(\n      new Path(\"/a/_delta_log/_staged_commits/123.uuid1.uuid2.json\")).isEmpty)\n  }\n\n  test(\"isCheckpointFile\") {\n    assert(isCheckpointFile(new Path(\"/a/123.checkpoint.parquet\")))\n    assert(isCheckpointFile(new Path(\"/a/123.checkpoint.0000000001.0000000087.parquet\")))\n    assert(!isCheckpointFile(new Path(\"/a/123.json\")))\n  }\n\n  test(\"checkpointVersion\") {\n    assert(checkpointVersion(new Path(\"/a/123.checkpoint.parquet\")) == 123)\n    assert(checkpointVersion(new Path(\"/a/0.checkpoint.parquet\")) == 0)\n    assert(checkpointVersion(new Path(\"/a/00000000000000000151.checkpoint.parquet\")) == 151)\n    assert(checkpointVersion(new Path(\"/a/999.checkpoint.0000000090.0000000099.parquet\")) == 999)\n  }\n\n  test(\"listingPrefix\") {\n    assert(listingPrefix(new Path(\"/a\"), 1234) == new Path(\"/a/00000000000000001234.\"))\n  }\n\n  test(\"checkpointFileWithParts\") {\n    assert(checkpointFileWithParts(new Path(\"/a\"), 1, 1) == Seq(\n      new Path(\"/a/00000000000000000001.checkpoint.0000000001.0000000001.parquet\")))\n    assert(checkpointFileWithParts(new Path(\"/a\"), 1, 2) == Seq(\n      new Path(\"/a/00000000000000000001.checkpoint.0000000001.0000000002.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000002.0000000002.parquet\")))\n    assert(checkpointFileWithParts(new Path(\"/a\"), 1, 5) == Seq(\n      new Path(\"/a/00000000000000000001.checkpoint.0000000001.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000002.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000003.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000004.0000000005.parquet\"),\n      new Path(\"/a/00000000000000000001.checkpoint.0000000005.0000000005.parquet\")))\n  }\n\n  test(\"numCheckpointParts\") {\n    assert(numCheckpointParts(new Path(\"/a/00000000000000000099.checkpoint.parquet\")).isEmpty)\n    assert(\n      numCheckpointParts(\n        new Path(\"/a/00000000000000000099.checkpoint.0000000078.0000000092.parquet\"))\n        .contains(92))\n  }\n\n  test(\"commitDirPath\") {\n    assert(commitDirPath(logPath = new Path(\"/a/_delta_log\")) ===\n      new Path(\"/a/_delta_log/_staged_commits\"))\n    assert(commitDirPath(logPath = new Path(\"/a/_delta_log/\")) ===\n      new Path(\"/a/_delta_log/_staged_commits\"))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/FindLastCompleteCheckpointSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.CheckpointInstance.Format\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsBaseSuite\nimport org.apache.spark.sql.delta.storage.LocalLogStore\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass FindLastCompleteCheckpointSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with CoordinatedCommitsBaseSuite {\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(\"spark.delta.logStore.class\", classOf[CustomListingLogStore].getName)\n  }\n\n  private def pathToFileStatus(path: Path, length: Long = 20): FileStatus = {\n    SerializableFileStatus(path.toString, length, isDir = true, modificationTime = 0L).toFileStatus\n  }\n\n  private def commitFiles(logPath: Path, versions: Seq[Long]): Seq[FileStatus] = {\n    versions.map { version => pathToFileStatus(FileNames.unsafeDeltaFile(logPath, version)) }\n  }\n\n  private def singleCheckpointFiles(\n      logPath: Path, versions: Seq[Long],\n      length: Long = 20): Seq[FileStatus] = {\n    versions.map { v => pathToFileStatus(FileNames.checkpointFileSingular(logPath, v), length) }\n  }\n\n  private def multipartCheckpointFiles(\n      logPath: Path,\n      versions: Seq[Long],\n      numParts: Int,\n      length: Long = 20): Seq[FileStatus] = {\n    versions.flatMap { version =>\n      FileNames.checkpointFileWithParts(logPath, version, numParts).map(pathToFileStatus(_, length))\n    }\n  }\n\n  private def checksumFiles(logPath: Path, versions: Seq[Long]): Seq[FileStatus] = {\n    versions.map { version => pathToFileStatus(FileNames.checksumFile(logPath, version)) }\n  }\n\n  def getLastCompleteCheckpointUsageLog(f: => Unit): Map[String, String] = {\n    val usageRecords = Log4jUsageLogger.track {\n      f\n    }\n    val opType = \"delta.findLastCompleteCheckpointBefore\"\n    val records = usageRecords.filter { r =>\n      r.tags.get(\"opType\").contains(opType) || r.opType.map(_.typeName).contains(opType)\n    }\n    assert(records.size === 1)\n    JsonUtils.fromJson[Map[String, String]](records.head.blob)\n  }\n\n  test(\"findLastCompleteCheckpoint without any argument\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val logPath = log.logPath\n      val logStore = log.store.asInstanceOf[CustomListingLogStore]\n\n      // Case-1: Multiple checkpoint exists in table dir\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to 3000) ++\n          singleCheckpointFiles(logPath, Seq(100, 200, 1000, 2000))\n      )\n      val eventData1 = getLastCompleteCheckpointUsageLog {\n        assert(log.findLastCompleteCheckpointBefore().contains(CheckpointInstance(version = 2000)))\n      }\n      assert(!eventData1.contains(\"iterations\"))\n      assert(logStore.listFromCount == 1)\n      assert(logStore.elementsConsumedFromListFromIter == 3005)\n      logStore.reset()\n\n      // Case-2: No checkpoint exists in table dir\n      logStore.customListingResult = Some(commitFiles(logPath, 0L to 3000))\n      val eventData2 = getLastCompleteCheckpointUsageLog {\n        assert(log.findLastCompleteCheckpointBefore().isEmpty)\n      }\n      assert(!eventData2.contains(\"iterations\"))\n      assert(logStore.listFromCount == 1)\n      assert(logStore.elementsConsumedFromListFromIter == 3001)\n      logStore.reset()\n\n      // Case-3: Multiple checkpoints for same version exists in table dir\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to 3000) ++\n          singleCheckpointFiles(logPath, Seq(100, 200, 1000, 2000)) ++\n          multipartCheckpointFiles(logPath, Seq(300, 2000), numParts = 4)\n      )\n      val eventData3 = getLastCompleteCheckpointUsageLog {\n        assert(log.findLastCompleteCheckpointBefore().contains(\n          CheckpointInstance(version = 2000, Format.WITH_PARTS, numParts = Some(4))))\n      }\n      assert(!eventData2.contains(\"iterations\"))\n      assert(logStore.listFromCount == 1)\n      assert(logStore.elementsConsumedFromListFromIter == 3013)\n      logStore.reset()\n    }\n  }\n\n  test(\"findLastCompleteCheckpoint with an upperBound which exists\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val logPath = log.logPath\n      val logStore = log.store.asInstanceOf[CustomListingLogStore]\n      logStore.reset()\n\n      // Case-1: The upperBound exists and it should not be returned\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to 3000) ++\n          singleCheckpointFiles(logPath, Seq(100, 200, 1000, 2000))\n      )\n      val eventData1 = getLastCompleteCheckpointUsageLog {\n        assert(\n          log.findLastCompleteCheckpointBefore(Some(CheckpointInstance(version = 2000)))\n            .contains(CheckpointInstance(version = 1000)))\n      }\n      assert(logStore.listFromCount == 1)\n      assert(logStore.elementsConsumedFromListFromIter == 1002 + 2) // commits + checkpoint\n      assert(eventData1(\"iterations\") == \"1\")\n      assert(eventData1(\"numFilesScanned\") == \"1004\")\n      logStore.reset()\n\n      // Case-2: The exact upperBound (a multi-part checkpoint) doesn't exist but another single\n      // part checkpoint for same version exists.\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to 3000) ++\n          singleCheckpointFiles(logPath, Seq(100, 200, 1000, 2000))\n      )\n      var sentinelCheckpoint =\n        CheckpointInstance(version = 2000, Format.WITH_PARTS, numParts = Some(4))\n      val eventData2 = getLastCompleteCheckpointUsageLog {\n        assert(log.findLastCompleteCheckpointBefore(Some(sentinelCheckpoint))\n          .contains(CheckpointInstance(version = 2000)))\n      }\n      assert(logStore.listFromCount == 1)\n      assert(logStore.elementsConsumedFromListFromIter == 1002 + 2) // commits + checkpoint\n      assert(eventData2(\"iterations\") == \"1\")\n      assert(eventData2(\"numFilesScanned\") == \"1004\")\n      logStore.reset()\n\n      // Case-3: The last complete checkpoint doesn't exist in last 1000 elements and needs\n      // multiple iterations.\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to 2500) ++\n          singleCheckpointFiles(logPath, Seq(100, 150))\n      )\n      val eventData3 = getLastCompleteCheckpointUsageLog {\n        assert(\n          log.findLastCompleteCheckpointBefore(2200).contains(CheckpointInstance(version = 150)))\n      }\n      assert(logStore.listFromCount == 3)\n      // the first listing will consume 1000 elements from 1200 to 2201 => 1002 commits\n      // the second listing will consume 1000 elements from 200 to 1201 => 1002 commits\n      // the third listing will consume 501 elements from 0 to 201 => 202 commits + 2 checkpoints\n      assert(logStore.elementsConsumedFromListFromIter == 2208) // commits + checkpoint\n      assert(eventData3(\"iterations\") == \"3\")\n      assert(eventData3(\"numFilesScanned\") == \"2208\")\n      logStore.reset()\n    }\n  }\n\n  for (passSentinelInstance <- BOOLEAN_DOMAIN)\n  test(\"findLastCompleteCheckpoint ignores 0B files \" +\n      s\"[passSentinelInstance: $passSentinelInstance]\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val logPath = log.logPath\n      val logStore = log.store.asInstanceOf[CustomListingLogStore]\n      logStore.reset()\n\n      val lastCommitVersion = 1400\n      val sentinelInstance =\n        if (passSentinelInstance) Some(CheckpointInstance(version = 1200)) else None\n      val expectedListCount = if (passSentinelInstance) 2 else 1\n      def getExpectedFileCount(filesPerCheckpoint: Int): Int = {\n        if (passSentinelInstance) {\n          // commits and checkpoints from 200 to 1201 => 1002 + 1-checkpoint\n          // commits and checkpoints from 0 to 201 => 202 + 2-checkpoint\n          1204 + 3 * filesPerCheckpoint\n        } else {\n          val totalCommits = lastCommitVersion + 1 // commit starts from 0\n          totalCommits + 2 * filesPerCheckpoint\n        }\n      }\n\n      // Case-1: `findLastCompleteCheckpointBefore` invoked without upperBound, with 0B single part\n      // checkpoint.\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to lastCommitVersion) ++\n        singleCheckpointFiles(logPath, Seq(100), length = 20) ++\n        singleCheckpointFiles(logPath, Seq(200), length = 0))\n      val eventData1 = getLastCompleteCheckpointUsageLog {\n        assert(\n          log.findLastCompleteCheckpointBefore(sentinelInstance)\n            .contains(CheckpointInstance(version = 100)))\n      }\n      assert(logStore.listFromCount == expectedListCount)\n      assert(logStore.elementsConsumedFromListFromIter ===\n        getExpectedFileCount(filesPerCheckpoint = 1))\n      if (passSentinelInstance) {\n        assert(eventData1(\"iterations\") == expectedListCount.toString)\n        assert(eventData1(\"numFilesScanned\") ==\n          getExpectedFileCount(filesPerCheckpoint = 1).toString)\n      } else {\n        assert(Seq(\"iterations\", \"numFilesScanned\").forall(!eventData1.contains(_)))\n      }\n\n      logStore.reset()\n\n      // Case-2: `findLastCompleteCheckpointBefore` invoked with upperBound, with a multi-part\n      // checkpoint having one of the part as 0B.\n      val badCheckpointV200 = {\n        val checkpointV200 = multipartCheckpointFiles(logPath, Seq(200), numParts = 4)\n        SerializableFileStatus.fromStatus(checkpointV200.head).copy(length = 0).toFileStatus +:\n          checkpointV200.tail\n      }\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to lastCommitVersion) ++\n        multipartCheckpointFiles(logPath, Seq(100), numParts = 4) ++\n        badCheckpointV200\n      )\n      val eventData2 = getLastCompleteCheckpointUsageLog {\n        assert(log.findLastCompleteCheckpointBefore(sentinelInstance)\n          .contains(CheckpointInstance(version = 100, Format.WITH_PARTS, numParts = Some(4))))\n      }\n      if (passSentinelInstance) {\n        assert(eventData2(\"iterations\") == expectedListCount.toString)\n        assert(eventData2(\"numFilesScanned\") ==\n          getExpectedFileCount(filesPerCheckpoint = 4).toString)\n      } else {\n        assert(Seq(\"iterations\", \"numFilesScanned\").forall(!eventData2.contains(_)))\n      }\n      assert(logStore.listFromCount == expectedListCount)\n      assert(logStore.elementsConsumedFromListFromIter ===\n        getExpectedFileCount(filesPerCheckpoint = 4))\n      logStore.reset()\n    }\n  }\n\n  for (passSentinelInstance <- BOOLEAN_DOMAIN)\n  test(\"findLastCompleteCheckpoint ignores incomplete multi-part checkpoint \" +\n      s\"[passSentinelInstance: $passSentinelInstance]\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val logPath = log.logPath\n      val logStore = log.store.asInstanceOf[CustomListingLogStore]\n      logStore.reset()\n\n      val lastCommitVersion = 1400\n      val sentinelInstance =\n        if (passSentinelInstance) Some(CheckpointInstance(version = 1200)) else None\n      val expectedListCount = if (passSentinelInstance) 2 else 1\n\n      def getExpectedFileCount(fileInCheckpointV200: Int, filesInCheckpointV100: Int): Int = {\n        if (passSentinelInstance) {\n          // commits and checkpoints from 200 to 1201 => 1002 + 1-checkpoint\n          // commits and checkpoints from 0 to 201 => 202 + 2-checkpoint\n          1204 + (fileInCheckpointV200 + fileInCheckpointV200 + filesInCheckpointV100)\n        } else {\n          val totalCommits = lastCommitVersion + 1 // commit starts from 0\n          totalCommits + (fileInCheckpointV200 + filesInCheckpointV100)\n        }\n      }\n\n      // Case-1: `findLastCompleteCheckpointBefore` invoked, with 0B single part checkpoint.\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to lastCommitVersion) ++\n          multipartCheckpointFiles(logPath, Seq(100), numParts = 4, length = 20) ++\n          multipartCheckpointFiles(logPath, Seq(200), numParts = 4, length = 20).take(3))\n      val eventData1 = getLastCompleteCheckpointUsageLog {\n        assert(\n          log.findLastCompleteCheckpointBefore(sentinelInstance)\n            .contains(CheckpointInstance(100, Format.WITH_PARTS, numParts = Some(4))))\n      }\n      assert(logStore.listFromCount == expectedListCount)\n      assert(logStore.elementsConsumedFromListFromIter ===\n        getExpectedFileCount(fileInCheckpointV200 = 3, filesInCheckpointV100 = 4))\n      if (passSentinelInstance) {\n        assert(eventData1(\"iterations\") == expectedListCount.toString)\n        assert(eventData1(\"numFilesScanned\") ==\n          getExpectedFileCount(fileInCheckpointV200 = 3, filesInCheckpointV100 = 4).toString)\n      } else {\n        assert(Seq(\"iterations\", \"numFilesScanned\").forall(!eventData1.contains(_)))\n      }\n\n      logStore.reset()\n    }\n  }\n\n  test(\"findLastCompleteCheckpoint with CheckpointInstance.MAX value\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val logPath = log.logPath\n      val logStore = log.store.asInstanceOf[CustomListingLogStore]\n      logStore.reset()\n\n      logStore.customListingResult = Some(\n        commitFiles(logPath, 0L to 3000) ++\n          singleCheckpointFiles(logPath, Seq(100, 200, 1000, 1200))\n      )\n      val eventData = getLastCompleteCheckpointUsageLog {\n        assert(\n          log.findLastCompleteCheckpointBefore(Some(CheckpointInstance.MaxValue))\n            .contains(CheckpointInstance(version = 1200)))\n      }\n      assert(!eventData.contains(\"iterations\"))\n      assert(!eventData.contains(\"upperBoundVersion\"))\n      assert(eventData(\"totalTimeTakenMs\").toLong > 0)\n      assert(logStore.listFromCount == 1)\n      assert(logStore.elementsConsumedFromListFromIter == 3001 + 4) // commits + checkpoint\n      logStore.reset()\n    }\n  }\n}\n\n/**\n * A custom log store that allows to provide custom listing results. This is useful to test\n * `DeltaLog.findLastCompleteCheckpointBefore` method.\n */\nclass CustomListingLogStore(\n  sparkConf: SparkConf,\n  hadoopConf: Configuration) extends LocalLogStore(sparkConf, hadoopConf) {\n\n  var listFromCount = 0\n  var elementsConsumedFromListFromIter = 0\n  // The custom listing result that will be returned by `listFrom` method. If this is None, then\n  // the default listing result from the actual filesystem will be returned.\n  var customListingResult: Option[Seq[FileStatus]] = None\n\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    customListingResult.map { results =>\n      listFromCount += 1\n      results\n        .sortBy(_.getPath)\n        .dropWhile(_.getPath.toString < path.toString)\n        .toIterator\n        .map { file =>\n          elementsConsumedFromListFromIter += 1\n          file\n        }\n    }.getOrElse(super.listFrom(path, hadoopConf))\n  }\n\n  def reset(): Unit = {\n    listFromCount = 0\n    elementsConsumedFromListFromIter = 0\n    customListingResult = None\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/GenerateIdentityValuesSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.IdentityColumn.IdentityInfo\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{Column, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.expressions.{GreaterThan, If, Literal}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass GenerateIdentityValuesSuite extends QueryTest with SharedSparkSession {\n  import testImplicits._\n  private val colName = \"id\"\n\n  /**\n   * Verify the generated IDENTITY values are correct.\n   *\n   * @param df A DataFrame with a single column containing all the generated IDENTITY values.\n   * @param identityInfo IDENTITY information used for verification.\n   * @param rowCount Expected row count.\n   */\n  private def verifyIdentityValues(\n      df: => DataFrame,\n      identityInfo: IdentityInfo,\n      rowCount: Long): Unit = {\n\n    // Check row count is expected.\n    checkAnswer(df.select(count(col(colName))), Row(rowCount))\n\n    // Check there is no duplicate.\n    checkAnswer(df.select(count_distinct(Column(colName))), Row(rowCount))\n\n    // Check every value follows start and step configuration\n    val condViolateConfig = s\"($colName - ${identityInfo.start}) % ${identityInfo.step} != 0\"\n    checkAnswer(df.where(condViolateConfig), Seq.empty)\n\n    // Check every value is after high watermark OR >= start.\n    val highWaterMark = identityInfo.highWaterMark.getOrElse(identityInfo.start - identityInfo.step)\n    val condViolateHighWaterMark = s\"(($colName - $highWaterMark)/${identityInfo.step}) < 0\"\n    checkAnswer(df.where(condViolateHighWaterMark), Seq.empty)\n\n    // When high watermark is empty, the first value should be start.\n    if (identityInfo.highWaterMark.isEmpty) {\n      val agg = if (identityInfo.step > 0) min(Column(colName)) else max(Column(colName))\n      checkAnswer(df.select(agg), Row(identityInfo.start))\n    }\n  }\n\n  test(\"basic\") {\n    val sizes = Seq(100, 1000, 10000)\n    val slices = Seq(2, 7, 15)\n    val starts = Seq(-3, 0, 1, 5, 43)\n    val steps = Seq(-3, -2, -1, 1, 2, 3)\n    for (size <- sizes; slice <- slices; start <- starts; step <- steps) {\n      val highWaterMarks = Seq(None, Some((start + 100 * step).toLong))\n      val df = spark.range(1, size + 1, 1, slice).toDF(colName)\n      highWaterMarks.foreach { highWaterMark =>\n        verifyIdentityValues(\n          df.select(Column(GenerateIdentityValues(start, step, highWaterMark)).alias(colName)),\n          IdentityInfo(start, step, highWaterMark),\n          size\n        )\n      }\n    }\n  }\n\n  test(\"shared state\") {\n    val size = 10000\n    val slice = 7\n    val start = -1\n    val step = 3\n    val highWaterMarks = Seq(None, Some((start + 100 * step).toLong))\n    val df = spark.range(1, size + 1, 1, slice).toDF(colName)\n    highWaterMarks.foreach { highWaterMark =>\n      // Create two GenerateIdentityValues expressions that share the same state. They should\n      // generate distinct values.\n      val gev = GenerateIdentityValues(start, step, highWaterMark)\n      val gev2 = gev.copy()\n      verifyIdentityValues(\n        df.select(Column(\n            If(GreaterThan(col(colName).expr, right = Literal(10)), gev, gev2)).alias(colName)),\n        IdentityInfo(start, step, highWaterMark),\n        size\n      )\n    }\n  }\n\n  test(\"bigint value range\") {\n    val size = 1000\n    val slice = 32\n    val start = Integer.MAX_VALUE.toLong + 1\n    val step = 10\n    val highWaterMark = start - step\n    val df = spark.range(1, size + 1, 1, slice).toDF(colName)\n    verifyIdentityValues(\n      df.select(\n        Column(GenerateIdentityValues(start, step, Some(highWaterMark))).alias(colName)),\n      IdentityInfo(start, step, Some(highWaterMark)),\n      size\n    )\n  }\n\n  test(\"overflow initial value\") {\n    val events = Log4jUsageLogger.track {\n      val df = spark.range(1, 10, 1, 5).toDF(colName)\n        .select(Column(GenerateIdentityValues(\n          start = 2,\n          step = Long.MaxValue,\n          highWaterMarkOpt = Some(2 - Long.MaxValue))))\n      val ex = intercept[SparkException] {\n        df.collect()\n      }\n      assert(ex.getMessage.contains(\"java.lang.ArithmeticException: long overflow\"))\n    }\n    val filteredEvents = events.filter { e =>\n      e.tags.get(\"opType\").exists(_ == \"delta.identityColumn.overflow\")\n    }\n    assert(filteredEvents.size > 0)\n  }\n\n  test(\"overflow next\") {\n    val events = Log4jUsageLogger.track {\n      val df = spark.range(1, 10, 1, 5).toDF(colName)\n        .select(Column(GenerateIdentityValues(\n          start = Long.MaxValue - 1,\n          step = 2,\n          highWaterMarkOpt = Some(Long.MaxValue - 3))))\n      val ex = intercept[SparkException] {\n        df.collect()\n      }\n      assert(ex.getMessage.contains(\"java.lang.ArithmeticException: long overflow\"))\n    }\n    val filteredEvents = events.filter { e =>\n      e.tags.get(\"opType\").exists(_ == \"delta.identityColumn.overflow\")\n    }\n    assert(filteredEvents.size > 0)\n  }\n\n  test(\"invalid high water mark\") {\n    val df = spark.range(1, 10, 1, 5).toDF(colName)\n    intercept[IllegalArgumentException] {\n      df.select(Column(GenerateIdentityValues(\n        start = 1,\n        step = 2,\n        highWaterMarkOpt = Some(4)))\n      ).collect()\n    }\n  }\n\n  test(\"invalid step\") {\n    val df = spark.range(1, 10, 1, 5).toDF(colName)\n    intercept[IllegalArgumentException] {\n      df.select(Column(GenerateIdentityValues(\n        start = 1,\n        step = 0,\n        highWaterMarkOpt = Some(4)))\n      ).collect()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/GeneratedColumnCompatibilitySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.commons.io.FileUtils\n\nimport org.apache.spark.sql.AnalysisException\n\n/**\n * We store the generation expressions in column's metadata. As Spark will propagate column metadata\n * to downstream operations when reading a table, old versions may create tables that have\n * generation expressions with an old writer version. For such tables, this test suite will verify\n * it behaves as a normal table. In other words, the generation expressions should be ignored in\n * new versions that understand generated columns so that all versions will have the same behaviors.\n */\nclass GeneratedColumnCompatibilitySuite extends GeneratedColumnTest {\n  import GeneratedColumn._\n  import testImplicits._\n\n  /**\n   * This test uses a special table generated by the following steps:\n   *\n   * 1. Run the following command using DBR 8.1 to generate a generated column table.\n   *\n   * ```\n   * spark.sql(\"\"\"CREATE TABLE generated_columns_table(\n   *             |c1 INT,\n   *             |c2 INT GENERATED ALWAYS AS ( c1 + 1 )\n   *             |) USING DELTA\n   *             |LOCATION 'sql/core/src/test/resources/delta/dbr_8_1_generated_columns'\n   *             |\"\"\".stripMargin)\n   * ```\n   *\n   * 2. Run the following command using DBR 8.0 to read the above table and create a new one.\n   *\n   * ```\n   * spark.sql(\"\"\"CREATE TABLE delta_non_generated_columns\n   *             |USING DELTA\n   *             |LOCATION 'sql/core/src/test/resources/delta/dbr_8_0_non_generated_columns'\n   *             |AS SELECT * FROM\n   *             |delta.`sql/core/src/test/resources/delta/dbr_8_1_generated_columns`\n   *             |\"\"\".stripMargin)\n   * ```\n   *\n   * Now the schema of `dbr_8_0_non_generated_columns` will contain generation expressions but it\n   * has an old writer version. This test will verify this test is treated as a non generated column\n   * table, which means new versions will have the exact behaviors as the old versions when reading\n   * or writing this table.\n   */\n  def withDBR8_0Table(func: String => Unit): Unit = {\n    val resourcePath = \"src/test/resources/delta/dbr_8_0_non_generated_columns\"\n    withTempDir { tempDir =>\n      // Prepare a table that has the old writer version and generation expressions\n      FileUtils.copyDirectory(new File(resourcePath), tempDir)\n      val path = tempDir.getCanonicalPath\n      val deltaLog = DeltaLog.forTable(spark, path)\n      // Verify the test table has the old writer version and generation expressions\n      assert(hasGeneratedColumns(deltaLog.snapshot.metadata.schema))\n      assert(!enforcesGeneratedColumns(deltaLog.snapshot.protocol, deltaLog.snapshot.metadata))\n      func(path)\n    }\n  }\n\n  test(\"dbr 8_0\") {\n    withDBR8_0Table { path =>\n      withTempDir { normalTableDir =>\n        // Prepare a normal table\n        val normalTablePath = normalTableDir.getCanonicalPath\n        spark.sql(\n          s\"\"\"CREATE TABLE generated_columns_table(\n             |c1 INT,\n             |c2 INT\n             |) USING DELTA\n             |LOCATION '$normalTablePath'\n             |\"\"\".stripMargin)\n\n        // Now we are going to verify commands on `path` and `normalTablePath` should be the same.\n\n        // Update `path` and `normalTablePath` using the same func and verify they have the\n        // same result\n        def updateTableAndCheckAnswer(func: String => Unit): Unit = {\n          func(path)\n          func(normalTablePath)\n          checkAnswer(\n            spark.read.format(\"delta\").load(path),\n            spark.read.format(\"delta\").load(normalTablePath)\n          )\n        }\n\n\n        // Insert values that violate the generation expression should be okay because the table\n        // should not be treated as a generated column table.\n        updateTableAndCheckAnswer { tablePath =>\n          sql(s\"INSERT INTO delta.`$tablePath`VALUES(1, 10)\")\n        }\n        updateTableAndCheckAnswer { tablePath =>\n          sql(s\"INSERT INTO delta.`$tablePath`(c2, c1) VALUES(11, 1)\")\n        }\n        updateTableAndCheckAnswer { tablePath =>\n          sql(s\"INSERT OVERWRITE delta.`$tablePath`VALUES(1, 13)\")\n        }\n        updateTableAndCheckAnswer { tablePath =>\n          sql(s\"INSERT OVERWRITE delta.`$tablePath`(c2, c1) VALUES(14, 1)\")\n        }\n        updateTableAndCheckAnswer { tablePath =>\n          // Append (1, null) to the table\n          Seq(1).toDF(\"c1\").write.format(\"delta\").mode(\"append\").save(tablePath)\n        }\n        updateTableAndCheckAnswer { tablePath =>\n          Seq(1 -> 15).toDF(\"c1\", \"c2\").write.format(\"delta\").mode(\"append\").save(tablePath)\n        }\n        updateTableAndCheckAnswer { tablePath =>\n          // Overwrite the table with (2, null)\n          Seq(2).toDF(\"c1\").write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n        }\n      }\n    }\n  }\n\n  test(\"adding a new column should not enable generated columns\") {\n    withDBR8_0Table { path =>\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val protocolBeforeUpdate = deltaLog.snapshot.protocol\n      sql(s\"ALTER TABLE delta.`$path` ADD COLUMNS (c3 INT)\")\n      deltaLog.update()\n      // The generation expressions should be dropped\n      assert(!hasGeneratedColumns(deltaLog.snapshot.metadata.schema))\n      assert(deltaLog.snapshot.protocol == protocolBeforeUpdate)\n      assert(!enforcesGeneratedColumns(deltaLog.snapshot.protocol, deltaLog.snapshot.metadata))\n    }\n  }\n\n  test(\"specifying a min writer version should not enable generated column\") {\n    withDBR8_0Table { path =>\n      val deltaLog = DeltaLog.forTable(spark, path)\n      sql(s\"ALTER TABLE delta.`$path` SET TBLPROPERTIES ('delta.minWriterVersion'='4')\")\n      deltaLog.update()\n      // The generation expressions should be dropped\n      assert(!hasGeneratedColumns(deltaLog.snapshot.metadata.schema))\n      assert(deltaLog.snapshot.protocol == Protocol(1, 4))\n      assert(!enforcesGeneratedColumns(deltaLog.snapshot.protocol, deltaLog.snapshot.metadata))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/GeneratedColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off typedlit\nimport java.sql.{Date, Timestamp}\nimport java.util.UUID\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.schema.{DeltaInvariantViolationException, InvariantViolationException}\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.GeneratedColumnValidateOnWriteMode\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.spark.sql.delta.util.FileNames\n\nimport org.apache.spark.sql.{AnalysisException, Column, DataFrame, Dataset, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.util.quietly\nimport org.apache.spark.sql.functions.{lit, make_dt_interval, struct, typedLit}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.{StreamingQueryException, Trigger}\nimport org.apache.spark.sql.types.{ArrayType, DataType, DateType, IntegerType, LongType, MetadataBuilder, ShortType, StringType, StructField, StructType, TimestampType}\n\ntrait GeneratedColumnSuiteBase\n    extends GeneratedColumnTest {\n\n  import GeneratedColumn._\n  import testImplicits._\n\n  protected def replaceTable(\n      tableName: String,\n      path: Option[String],\n      schemaString: String,\n      generatedColumns: Map[String, String],\n      partitionColumns: Seq[String],\n      notNullColumns: Set[String] = Set.empty,\n      comments: Map[String, String] = Map.empty,\n      properties: Map[String, String] = Map.empty,\n      orCreate: Option[Boolean] = None): Unit = {\n    var tableBuilder = if (orCreate.getOrElse(false)) {\n      io.delta.tables.DeltaTable.createOrReplace(spark)\n    } else {\n      io.delta.tables.DeltaTable.replace(spark)\n    }\n    buildTable(tableBuilder, tableName, path, schemaString,\n      generatedColumns, partitionColumns, notNullColumns, comments, properties).execute()\n  }\n\n  // Define the information for a default test table used by many tests.\n  protected val defaultTestTableSchema =\n    \"c1 bigint, c2_g bigint, c3_p string, c4_g_p date, c5 timestamp, c6 int, c7_g_p int, c8 date\"\n  protected val defaultTestTableGeneratedColumns = Map(\n    \"c2_g\" -> \"c1 + 10\",\n    \"c4_g_p\" -> \"cast(c5 as date)\",\n    \"c7_g_p\" -> \"c6 * 10\"\n  )\n  protected val defaultTestTablePartitionColumns = \"c3_p, c4_g_p, c7_g_p\".split(\", \").toList\n\n  protected def createDefaultTestTable(tableName: String, path: Option[String] = None): Unit = {\n    createTable(\n      tableName,\n      path,\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns\n    )\n  }\n\n  /**\n   * @param updateFunc A function that's called with the table information (tableName, path). It\n   *                   should execute update operations, and return the expected data after\n   *                   updating.\n   */\n  protected def testTableUpdate(\n      testName: String,\n      isStreaming: Boolean = false)(updateFunc: (String, String) => Seq[Row]): Unit = {\n    def testBody(): Unit = {\n      val table = testName\n      withTempDir { path =>\n        withTable(table) {\n          createDefaultTestTable(tableName = table, path = Some(path.getCanonicalPath))\n          val expected = updateFunc(testName, path.getCanonicalPath)\n          checkAnswer(sql(s\"select * from $table\"), expected)\n        }\n      }\n    }\n\n    if (isStreaming) {\n      test(testName) {\n        testBody()\n      }\n    } else {\n      test(testName) {\n        testBody()\n      }\n    }\n  }\n\n  private def errorContains(errMsg: String, str: String): Unit = {\n     assert(errMsg.contains(str))\n  }\n\n  protected def testTableUpdateDPO(\n    testName: String)(updateFunc: (String, String) => Seq[Row]): Unit = {\n    withSQLConf(SQLConf.PARTITION_OVERWRITE_MODE.key ->\n      SQLConf.PartitionOverwriteMode.DYNAMIC.toString) {\n      testTableUpdate(\"dpo_\" + testName)(updateFunc)\n    }\n  }\n\n  testTableUpdate(\"append_data\") { (table, path) =>\n    Seq(\n      Tuple5(1L, \"foo\", \"2020-10-11 12:30:30\", 100, \"2020-11-12\")\n    ).toDF(\"c1\", \"c3_p\", \"c5\", \"c6\", \"c8\")\n      .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n      .withColumn(\"c8\", $\"c8\".cast(DateType))\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(path)\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"append_data_in_different_column_order\") { (table, path) =>\n    Seq(\n      Tuple5(\"2020-10-11 12:30:30\", 100, \"2020-11-12\", 1L, \"foo\")\n    ).toDF(\"c5\", \"c6\", \"c8\", \"c1\", \"c3_p\")\n      .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n      .withColumn(\"c8\", $\"c8\".cast(DateType))\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(path)\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"append_data_v2\") { (table, _) =>\n    Seq(\n      Tuple5(1L, \"foo\", \"2020-10-11 12:30:30\", 100, \"2020-11-12\")\n    ).toDF(\"c1\", \"c3_p\", \"c5\", \"c6\", \"c8\")\n      .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n      .withColumn(\"c8\", $\"c8\".cast(DateType))\n      .writeTo(table)\n      .append()\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"append_data_in_different_column_order_v2\") { (table, _) =>\n    Seq(\n      Tuple5(\"2020-10-11 12:30:30\", 100, \"2020-11-12\", 1L, \"foo\")\n    ).toDF(\"c5\", \"c6\", \"c8\", \"c1\", \"c3_p\")\n      .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n      .withColumn(\"c8\", $\"c8\".cast(DateType))\n      .writeTo(table)\n      .append()\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n\n  testTableUpdate(\"insert_into_values_provide_all_columns\") { (table, path) =>\n    sql(s\"INSERT INTO $table VALUES\" +\n      s\"(1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_into_by_name_provide_all_columns\") { (table, _) =>\n    sql(s\"INSERT INTO $table (c5, c6, c7_g_p, c8, c1, c2_g, c3_p, c4_g_p) VALUES\" +\n      s\"('2020-10-11 12:30:30', 100, 1000, '2020-11-12', 1, 11, 'foo', '2020-10-11')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_into_by_name_not_provide_generated_columns\") { (table, _) =>\n    sql(s\"INSERT INTO $table (c6, c8, c1, c3_p, c5) VALUES\" +\n      s\"(100, '2020-11-12', 1L, 'foo', '2020-10-11 12:30:30')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_into_by_name_with_some_generated_columns\") { (table, _) =>\n    sql(s\"INSERT INTO $table (c5, c6, c8, c1, c3_p, c4_g_p) VALUES\" +\n      s\"('2020-10-11 12:30:30', 100, '2020-11-12', 1L, 'foo', '2020-10-11')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_into_select_provide_all_columns\") { (table, path) =>\n    sql(s\"INSERT INTO $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_into_by_name_not_provide_normal_columns\") { (table, _) =>\n    val e = intercept[AnalysisException] {\n      withSQLConf(SQLConf.USE_NULLS_FOR_MISSING_DEFAULT_COLUMN_VALUES.key -> \"false\") {\n        sql(s\"INSERT INTO $table (c6, c8, c1, c3_p) VALUES\" +\n          s\"(100, '2020-11-12', 1L, 'foo')\")\n      }\n    }\n    errorContains(e.getMessage, \"Column c5 is not specified in INSERT\")\n    Nil\n  }\n\n  testTableUpdate(\"insert_overwrite_values_provide_all_columns\") { (table, path) =>\n    sql(s\"INSERT OVERWRITE TABLE $table VALUES\" +\n      s\"(1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_overwrite_select_provide_all_columns\") { (table, path) =>\n    sql(s\"INSERT OVERWRITE TABLE $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_overwrite_by_name_provide_all_columns\") { (table, _) =>\n    sql(s\"INSERT OVERWRITE $table (c5, c6, c7_g_p, c8, c1, c2_g, c3_p, c4_g_p) VALUES\" +\n      s\"('2020-10-11 12:30:30', 100, 1000, '2020-11-12', 1, 11, 'foo', '2020-10-11')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_overwrite_by_name_not_provide_generated_columns\") { (table, _) =>\n    sql(s\"INSERT OVERWRITE $table (c6, c8, c1, c3_p, c5) VALUES\" +\n      s\"(100, '2020-11-12', 1L, 'foo', '2020-10-11 12:30:30')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_overwrite_by_name_with_some_generated_columns\") { (table, _) =>\n    sql(s\"INSERT OVERWRITE $table (c5, c6, c8, c1, c3_p, c4_g_p) VALUES\" +\n      s\"('2020-10-11 12:30:30', 100, '2020-11-12', 1L, 'foo', '2020-10-11')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"insert_overwrite_by_name_not_provide_normal_columns\") { (table, _) =>\n    val e = intercept[AnalysisException] {\n      withSQLConf(SQLConf.USE_NULLS_FOR_MISSING_DEFAULT_COLUMN_VALUES.key -> \"false\") {\n        sql(s\"INSERT OVERWRITE $table (c6, c8, c1, c3_p) VALUES\" +\n          s\"(100, '2020-11-12', 1L, 'foo')\")\n      }\n    }\n    errorContains(e.getMessage, \"Column c5 is not specified in INSERT\")\n    Nil\n  }\n\n  testTableUpdateDPO(\"insert_overwrite_values_provide_all_columns\") { (table, path) =>\n    sql(s\"INSERT OVERWRITE TABLE $table VALUES\" +\n      s\"(1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12')\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdateDPO(\"insert_overwrite_select_provide_all_columns\") { (table, path) =>\n    sql(s\"INSERT OVERWRITE TABLE $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdateDPO(\"insert_overwrite_by_name_values_provide_all_columns\") { (table, _) =>\n    sql(s\"INSERT OVERWRITE $table (c5, c6, c7_g_p, c8, c1, c2_g, c3_p, c4_g_p) VALUES\" +\n      s\"(CAST('2020-10-11 12:30:30' AS TIMESTAMP), 100, 1000, CAST('2020-11-12' AS DATE), \" +\n      s\"1L, 11L, 'foo', CAST('2020-10-11' AS DATE))\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdateDPO(\n    \"insert_overwrite_by_name_not_provide_generated_columns\") { (table, _) =>\n    sql(s\"INSERT OVERWRITE $table (c6, c8, c1, c3_p, c5) VALUES\" +\n      s\"(100, CAST('2020-11-12' AS DATE), 1L, 'foo', CAST('2020-10-11 12:30:30' AS TIMESTAMP))\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdateDPO(\"insert_overwrite_by_name_with_some_generated_columns\") { (table, _) =>\n    sql(s\"INSERT OVERWRITE $table (c5, c6, c8, c1, c3_p, c4_g_p) VALUES\" +\n      s\"(CAST('2020-10-11 12:30:30' AS TIMESTAMP), 100, CAST('2020-11-12' AS DATE), 1L, \" +\n      s\"'foo', CAST('2020-10-11' AS DATE))\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdateDPO(\"insert_overwrite_by_name_not_provide_normal_columns\") { (table, _) =>\n    val e = intercept[AnalysisException] {\n      sql(s\"INSERT OVERWRITE $table (c6, c8, c1, c3_p) VALUES\" +\n        s\"(100, '2020-11-12', 1L, 'foo')\")\n    }\n    assert(e.getMessage.contains(\"with name `c5` cannot be resolved\") ||\n        e.getMessage.contains(\"Column c5 is not specified in INSERT\"))\n    Nil\n  }\n\n  testTableUpdate(\"delete\") { (table, path) =>\n    Seq(\n      Tuple5(1L, \"foo\", \"2020-10-11 12:30:30\", 100, \"2020-11-12\"),\n      Tuple5(2L, \"foo\", \"2020-10-11 13:30:30\", 100, \"2020-12-12\")\n    ).toDF(\"c1\", \"c3_p\", \"c5\", \"c6\", \"c8\")\n      .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n      .withColumn(\"c8\", $\"c8\".cast(DateType))\n      .coalesce(1)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(path)\n    // Make sure we create only one file so that we will trigger file rewriting.\n    assert(DeltaLog.forTable(spark, path).snapshot.allFiles.count == 1)\n    sql(s\"DELETE FROM $table WHERE c1 = 2\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"update_generated_column_with_correct_value\") { (table, path) =>\n    sql(s\"INSERT INTO $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    sql(s\"UPDATE $table SET c2_g = 11 WHERE c1 = 1\")\n    Row(1, 11, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"update_generated_column_with_incorrect_value\") { (table, path) =>\n    sql(s\"INSERT INTO $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    val e = intercept[InvariantViolationException] {\n      quietly {\n        sql(s\"UPDATE $table SET c2_g = 12 WHERE c1 = 1\")\n      }\n    }\n    errorContains(e.getMessage,\n      \"CHECK constraint Generated Column (c2_g <=> (c1 + 10)) violated by row with values\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"update_source_column_used_by_generated_column\") { (table, _) =>\n    sql(s\"INSERT INTO $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    sql(s\"UPDATE $table SET c1 = 2 WHERE c1 = 1\")\n    Row(2, 12, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"update_source_and_generated_columns_with_correct_value\") { (table, _) =>\n    sql(s\"INSERT INTO $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    sql(s\"UPDATE $table SET c2_g = 12, c1 = 2 WHERE c1 = 1\")\n    Row(2, 12, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"update_source_and_generated_columns_with_incorrect_value\") { (table, _) =>\n    sql(s\"INSERT INTO $table SELECT \" +\n      s\"1, 11, 'foo', '2020-10-11', '2020-10-11 12:30:30', 100, 1000, '2020-11-12'\")\n    val e = intercept[InvariantViolationException] {\n      quietly {\n        sql(s\"UPDATE $table SET c2_g = 12, c1 = 3 WHERE c1 = 1\")\n      }\n    }\n    errorContains(e.getMessage,\n      \"CHECK constraint Generated Column (c2_g <=> (c1 + 10)) violated by row with values\")\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  test(\"various update commands\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTableName(\"update_commands\") { table =>\n        createTable(table, Some(path), \"c INT, g INT\", Map(\"g\" -> \"c + 10\"), Nil)\n        sql(s\"INSERT INTO $table VALUES(10, 20)\")\n        sql(s\"UPDATE $table SET c = 20\")\n        checkAnswer(spark.table(table), Row(20, 30) :: Nil)\n        sql(s\"UPDATE delta.`$path` SET c = 30\")\n        checkAnswer(spark.table(table), Row(30, 40) :: Nil)\n        io.delta.tables.DeltaTable.forName(table).updateExpr(Map(\"c\" -> \"40\"))\n        checkAnswer(spark.table(table), Row(40, 50) :: Nil)\n        io.delta.tables.DeltaTable.forPath(path).updateExpr(Map(\"c\" -> \"50\"))\n        checkAnswer(spark.table(table), Row(50, 60) :: Nil)\n      }\n    }\n  }\n\n  test(\"update with various column references\") {\n    withTableName(\"update_with_various_references\") { table =>\n      createTable(table, None, \"c1 INT, c2 INT, g INT\", Map(\"g\" -> \"c1 + 10\"), Nil)\n      sql(s\"INSERT INTO $table VALUES(10, 50, 20)\")\n      sql(s\"UPDATE $table SET c1 = 20\")\n      checkAnswer(spark.table(table), Row(20, 50, 30) :: Nil)\n      sql(s\"UPDATE $table SET c1 = c2 + 100, c2 = 1000\")\n      checkAnswer(spark.table(table), Row(150, 1000, 160) :: Nil)\n      sql(s\"UPDATE $table SET c1 = c2 + g\")\n      checkAnswer(spark.table(table), Row(1160, 1000, 1170) :: Nil)\n      sql(s\"UPDATE $table SET c1 = g\")\n      checkAnswer(spark.table(table), Row(1170, 1000, 1180) :: Nil)\n    }\n  }\n\n  test(\"update a struct source column\") {\n    withTableName(\"update_struct_column\") { table =>\n      createTable(table,\n        None,\n        \"s STRUCT<s1: INT, s2: STRING>, g INT\",\n        Map(\"g\" -> \"s.s1 + 10\"),\n        Nil)\n      sql(s\"INSERT INTO $table VALUES(struct(10, 'foo'), 20)\")\n      sql(s\"UPDATE $table SET s.s1 = 20 WHERE s.s1 = 10\")\n      checkAnswer(spark.table(table), Row(Row(20, \"foo\"), 30) :: Nil)\n    }\n  }\n\n  test(\"updating a temp view is not supported\") {\n    withTableName(\"update_temp_view\") { table =>\n      createTable(table, None, \"c1 INT, c2 INT\", Map(\"c2\" -> \"c1 + 10\"), Nil)\n      withTempView(\"test_view\") {\n        sql(s\"CREATE TEMP VIEW test_view AS SELECT * FROM $table\")\n        val e = intercept[AnalysisException] {\n          sql(s\"UPDATE test_view SET c1 = 2 WHERE c1 = 1\")\n        }\n        assert(e.getMessage.contains(\"a temp view\"))\n      }\n    }\n  }\n\n  testTableUpdate(\"streaming_write\", isStreaming = true) { (table, path) =>\n    withTempDir { checkpointDir =>\n      val stream = MemoryStream[Int]\n      val q = stream.toDF\n        .map(_ => Tuple5(1L, \"foo\", \"2020-10-11 12:30:30\", 100, \"2020-11-12\"))\n        .toDF(\"c1\", \"c3_p\", \"c5\", \"c6\", \"c8\")\n        .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n        .withColumn(\"c8\", $\"c8\".cast(DateType))\n        .writeStream\n        .format(\"delta\")\n        .outputMode(\"append\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(path)\n      stream.addData(1)\n      q.processAllAvailable()\n      q.stop()\n    }\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"streaming_write_with_different_case\", isStreaming = true) { (table, path) =>\n    withTempDir { checkpointDir =>\n      val stream = MemoryStream[Int]\n      val q = stream.toDF\n        .map(_ => Tuple5(1L, \"foo\", \"2020-10-11 12:30:30\", 100, \"2020-11-12\"))\n        .toDF(\"C1\", \"c3_p\", \"c5\", \"c6\", \"c8\") // C1 is using upper case\n        .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n        .withColumn(\"c8\", $\"c8\".cast(DateType))\n        .writeStream\n        .format(\"delta\")\n        .outputMode(\"append\")\n        .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n        .start(path)\n      stream.addData(1)\n      q.processAllAvailable()\n      q.stop()\n    }\n    Row(1L, 11L, \"foo\", sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:30:30\"),\n      100, 1000, sqlDate(\"2020-11-12\")) :: Nil\n  }\n\n  testTableUpdate(\"streaming_write_incorrect_value\", isStreaming = true) { (table, path) =>\n    withTempDir { checkpointDir =>\n      quietly {\n        val stream = MemoryStream[Int]\n        val q = stream.toDF\n          // 2L is an incorrect value. The correct value should be 11L\n          .map(_ => Tuple6(1L, 2L, \"foo\", \"2020-10-11 12:30:30\", 100, \"2020-11-12\"))\n          .toDF(\"c1\", \"c2_g\", \"c3_p\", \"c5\", \"c6\", \"c8\")\n          .withColumn(\"c5\", $\"c5\".cast(TimestampType))\n          .withColumn(\"c8\", $\"c8\".cast(DateType))\n          .writeStream\n          .format(\"delta\")\n          .outputMode(\"append\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .start(path)\n        stream.addData(1)\n        val e = intercept[StreamingQueryException] {\n          q.processAllAvailable()\n        }\n        errorContains(e.getMessage,\n          \"CHECK constraint Generated Column (c2_g <=> (c1 + 10)) violated by row with values\")\n        q.stop()\n      }\n    }\n    Nil\n  }\n\n  testQuietly(\"write to a generated column with an incorrect value\") {\n    withTableName(\"write_incorrect_value\") { table =>\n      createTable(table, None, \"id INT, id2 INT\", Map(\"id2\" -> \"id + 10\"), partitionColumns = Nil)\n      val e = intercept[InvariantViolationException] {\n        sql(s\"INSERT INTO $table VALUES(1, 12)\")\n      }\n      errorContains(e.getMessage,\n        \"CHECK constraint Generated Column (id2 <=> (id + 10)) violated by row with values\")\n    }\n  }\n\n  test(\"dot in the column name\") {\n    withTableName(\"dot_in_column_name\") { table =>\n      createTable(table, None, \"`a.b` INT, `x.y` INT\", Map(\"x.y\" -> \"`a.b` + 10\"), Nil)\n      sql(s\"INSERT INTO $table VALUES(1, 11)\")\n      sql(s\"INSERT INTO $table VALUES(2, 12)\")\n      checkAnswer(sql(s\"SELECT * FROM $table\"), Row(1, 11) :: Row(2, 12) :: Nil)\n    }\n  }\n\n  test(\"validateGeneratedColumns: generated columns should not refer to non-existent columns\") {\n    val f1 = StructField(\"c1\", IntegerType)\n    val f2 = withGenerationExpression(StructField(\"c2\", IntegerType), \"c10 + 10\")\n    val schema = StructType(f1 :: f2 :: Nil)\n    val e = intercept[DeltaAnalysisException](validateGeneratedColumns(spark, schema))\n    errorContains(e.getMessage,\n      \"A generated column cannot use a non-existent column or another generated column\")\n  }\n\n  test(\"validateGeneratedColumns: no generated columns\") {\n    val f1 = StructField(\"c1\", IntegerType)\n    val f2 = StructField(\"c2\", IntegerType)\n    val schema = StructType(f1 :: f2 :: Nil)\n    validateGeneratedColumns(spark, schema)\n  }\n\n  test(\"validateGeneratedColumns: all generated columns\") {\n    val f1 = withGenerationExpression(StructField(\"c1\", IntegerType), \"1 + 2\")\n    val f2 = withGenerationExpression(StructField(\"c1\", IntegerType), \"3 + 4\")\n    val schema = StructType(f1 :: f2 :: Nil)\n    validateGeneratedColumns(spark, schema)\n  }\n\n  test(\"validateGeneratedColumns: generated columns should not refer to other generated columns\") {\n    val f1 = StructField(\"c1\", IntegerType)\n    val f2 = withGenerationExpression(StructField(\"c2\", IntegerType), \"c1 + 10\")\n    val f3 = withGenerationExpression(StructField(\"c3\", IntegerType), \"c2 + 10\")\n    val schema = StructType(f1 :: f2 :: f3 :: Nil)\n    val e = intercept[DeltaAnalysisException](validateGeneratedColumns(spark, schema))\n    errorContains(e.getMessage,\n      \"A generated column cannot use a non-existent column or another generated column\")\n  }\n\n  test(\"validateGeneratedColumns: supported expressions\") {\n    for (exprString <- Seq(\n      // Generated column should support timestamp to date\n      \"to_date(foo, \\\"yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'\\\")\")) {\n      val f1 = StructField(\"foo\", TimestampType)\n      val f2 = withGenerationExpression(StructField(\"bar\", DateType), exprString)\n      val schema = StructType(Seq(f1, f2))\n      validateGeneratedColumns(spark, schema)\n    }\n  }\n\n  test(\"validateGeneratedColumns: unsupported expressions\") {\n    spark.udf.register(\"myudf\", (s: Array[Int]) => s)\n    for ((exprString, error) <- Seq(\n      \"myudf(foo)\" -> \"Found myudf(foo). A generated column cannot use a user-defined function\",\n      \"rand()\" ->\n        \"Found rand(). A generated column cannot use a non deterministic expression\",\n      \"max(foo)\" -> \"Found max(foo). A generated column cannot use an aggregate expression\",\n      \"explode(foo)\" -> \"explode(foo) cannot be used in a generated column\",\n      \"current_timestamp\" -> \"current_timestamp() cannot be used in a generated column\"\n    )) {\n      val f1 = StructField(\"foo\", ArrayType(IntegerType, true))\n      val f2 = withGenerationExpression(StructField(\"bar\", IntegerType), exprString)\n      val schema = StructType(f1 :: f2 :: Nil)\n      val e = intercept[AnalysisException](validateGeneratedColumns(spark, schema))\n      errorContains(e.getMessage, error)\n    }\n  }\n\n  protected def testTypeMismatch(\n      generatedColumnType: DataType,\n      generatedColumnNullable: Boolean,\n      generateAsExpression: Column,\n      expectSuccess: Boolean\n  ): Unit = {\n    val verb = if (expectSuccess) \"matches\" else \"doesn't match\"\n    val columnTypeString = if (generatedColumnNullable) {\n      generatedColumnType.sql\n    } else {\n      s\"${generatedColumnType.sql} NOT NULL\"\n    }\n    test(s\"validateGeneratedColumns: column type ${columnTypeString}\" +\n        s\" $verb expression type $generateAsExpression\") {\n      val f1 = StructField(\"nullableIntCol\", IntegerType, nullable = true)\n      val f2 = withGenerationExpression(\n        StructField(\"genCol\", generatedColumnType, nullable = generatedColumnNullable),\n        generateAsExpression.expr.sql)\n      val schema = StructType(f1 :: f2 :: Nil)\n      if (expectSuccess) {\n        validateGeneratedColumns(spark, schema)\n      } else {\n        val e = intercept[AnalysisException](validateGeneratedColumns(spark, schema))\n        val expressionTypeString = if (generateAsExpression.expr.resolved) {\n          generateAsExpression.expr.dataType.sql\n        } else {\n          val df1 = spark.createDataFrame(spark.emptyDataFrame.rdd, schema)\n            .select(generateAsExpression)\n          df1.schema.fields.head.dataType.sql\n        }\n        checkErrorMatchPVals(e,\n          \"DELTA_GENERATED_COLUMNS_EXPR_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"genCol\",\n            \"expressionType\" -> s\".*${expressionTypeString}.*\",\n            \"columnType\" -> s\".*${generatedColumnType.sql}.*\"\n          ))\n      }\n    }\n  }\n\n  testTypeMismatch(\n    generatedColumnType = IntegerType,\n    generatedColumnNullable = true,\n    generateAsExpression = $\"nullableIntCol\",\n    expectSuccess = true\n  )\n\n  testTypeMismatch(\n    generatedColumnType = IntegerType,\n    generatedColumnNullable = false,\n    generateAsExpression = $\"nullableIntCol\",\n    // Even though foo is nullable, we allow this and fail at runtime when foo actually contains\n    // a NULL value.\n    expectSuccess = true\n  )\n\n  testTypeMismatch(\n    generatedColumnType = IntegerType,\n    generatedColumnNullable = false,\n    generateAsExpression = lit(5),\n    expectSuccess = true\n  )\n\n\n  testTypeMismatch(\n    generatedColumnType = IntegerType,\n    generatedColumnNullable = false,\n    // We need to force this to be INT NULL not a VOID NULL.\n    generateAsExpression = typedLit[java.lang.Integer](null),\n    // Even though the expression is clearly nullable, we allow this and fail at runtime\n    // when actually generating a NULL value.\n    expectSuccess = true\n  )\n\n  testTypeMismatch(\n    generatedColumnType = IntegerType,\n    generatedColumnNullable = true,\n    // We need to force this to be INT NULL not a VOID NULL.\n    generateAsExpression = typedLit[java.lang.Integer](null),\n    expectSuccess = true\n  )\n\n  for (generatedColumnNullable <- BOOLEAN_DOMAIN) {\n    testTypeMismatch(\n      generatedColumnType = IntegerType,\n      generatedColumnNullable = generatedColumnNullable,\n      generateAsExpression = $\"nullableIntCol\".cast(StringType),\n      expectSuccess = false)\n\n    testTypeMismatch(\n      generatedColumnType = IntegerType,\n      generatedColumnNullable = generatedColumnNullable,\n      generateAsExpression = $\"nullableIntCol\".cast(LongType),\n      expectSuccess = false)\n\n    testTypeMismatch(\n      generatedColumnType = IntegerType,\n      generatedColumnNullable = generatedColumnNullable,\n      generateAsExpression = $\"nullableIntCol\".cast(ShortType),\n      expectSuccess = false)\n  }\n\n  testTypeMismatch(\n    generatedColumnType = StructType(Array(StructField(\"first\", IntegerType, nullable = false))),\n    generatedColumnNullable = true,\n    generateAsExpression = struct($\"nullableIntCol\".as(\"first\")),\n    // Even though foo is nullable, we allow this and fail at runtime when foo actually contains\n    // a NULL value.\n    expectSuccess = true\n  )\n\n  testTypeMismatch(\n    generatedColumnType =\n      StructType(Array(StructField(\"firstNullable\", IntegerType, nullable = true))),\n    generatedColumnNullable = true,\n    generateAsExpression = struct($\"nullableIntCol\".as(\"firstNullable\")),\n    expectSuccess = true\n  )\n\n  test(\"nullability mismatch fails at runtime\") {\n    withTableName(\"tbl\") { tbl =>\n      createTable(\n        tableName = tbl,\n        path = None,\n        schemaString = \"base STRING, gen STRING\",\n        generatedColumns = Map(\"gen\" -> \"concat(base, '-generated')\"),\n        partitionColumns = Seq.empty,\n        // base is nullable, but gen isn't even though it's derived from base.\n        notNullColumns = Set(\"gen\"))\n      // Perform a legal write.\n      Seq(\"1\").toDF(\"base\")\n        .write.format(\"delta\").mode(\"append\").saveAsTable(tbl)\n\n      // Perform an illegal write.\n      val e = intercept[DeltaInvariantViolationException] {\n        Seq(null.asInstanceOf[String]).toDF(\"base\")\n          .write.format(\"delta\").mode(\"append\").saveAsTable(tbl)\n      }\n      checkError(e,\n        \"DELTA_NOT_NULL_CONSTRAINT_VIOLATED\",\n        parameters = Map(\"columnName\" -> \"gen\"))\n\n      // Ensure the result is correct.\n      checkAnswer(\n        spark.read.table(tbl),\n        Row(\"1\", \"1-generated\") :: Nil\n      )\n    }\n  }\n\n\n  test(\"test partition transform expressions end to end\") {\n    withTableName(\"partition_transform_expressions\") { table =>\n      createTable(table, None,\n        \"time TIMESTAMP, year DATE, month DATE, day DATE, hour TIMESTAMP\",\n        Map(\n          \"year\" -> \"make_date(year(time), 1, 1)\",\n          \"month\" -> \"make_date(year(time), month(time), 1)\",\n          \"day\" -> \"make_date(year(time), month(time), day(time))\",\n          \"hour\" -> \"make_timestamp(year(time), month(time), day(time), hour(time), 0, 0)\"\n        ),\n        partitionColumns = Nil)\n      Seq(\"2020-10-11 12:30:30\")\n        .toDF(\"time\")\n        .withColumn(\"time\", $\"time\".cast(TimestampType))\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .saveAsTable(table)\n      checkAnswer(\n        sql(s\"SELECT * from $table\"),\n        Row(sqlTimestamp(\"2020-10-11 12:30:30\"), sqlDate(\"2020-01-01\"), sqlDate(\"2020-10-01\"),\n          sqlDate(\"2020-10-11\"), sqlTimestamp(\"2020-10-11 12:00:00\"))\n      )\n    }\n  }\n\n  test(\"the generation expression constraint should support null values\") {\n    withTableName(\"null\") { table =>\n      createTable(table, None, \"c1 STRING, c2 STRING\", Map(\"c2\" -> \"CONCAT(c1, 'y')\"), Nil)\n      sql(s\"INSERT INTO $table VALUES('x', 'xy')\")\n      sql(s\"INSERT INTO $table VALUES(null, null)\")\n      checkAnswer(\n        sql(s\"SELECT * from $table\"),\n        Row(\"x\", \"xy\") :: Row(null, null) :: Nil\n      )\n      quietly {\n        val e =\n          intercept[InvariantViolationException](sql(s\"INSERT INTO $table VALUES('foo', null)\"))\n        errorContains(e.getMessage,\n          \"CHECK constraint Generated Column (c2 <=> CONCAT(c1, 'y')) \" +\n            \"violated by row with values\")\n      }\n      quietly {\n        val e =\n          intercept[InvariantViolationException](sql(s\"INSERT INTO $table VALUES(null, 'foo')\"))\n        errorContains(e.getMessage,\n          \"CHECK constraint Generated Column (c2 <=> CONCAT(c1, 'y')) \" +\n            \"violated by row with values\")\n      }\n    }\n  }\n\n  test(\"complex type extractors\") {\n    withTableName(\"struct_field\") { table =>\n      createTable(\n        table,\n        None,\n        \"`a.b` STRING, a STRUCT<b: INT, c: STRING>, array ARRAY<INT>, \" +\n          \"c1 STRING, c2 INT, c3 INT\",\n        Map(\"c1\" -> \"CONCAT(`a.b`, 'b')\", \"c2\" -> \"a.b + 100\", \"c3\" -> \"array[1]\"),\n        Nil)\n      sql(s\"INSERT INTO $table VALUES(\" +\n        s\"'a', struct(100, 'foo'), array(1000, 1001), \" +\n        s\"'ab', 200, 1001)\")\n      checkAnswer(\n        spark.table(table),\n        Row(\"a\", Row(100, \"foo\"), Array(1000, 1001), \"ab\", 200, 1001) :: Nil)\n    }\n  }\n\n  test(\"getGeneratedColumnsAndColumnsUsedByGeneratedColumns\") {\n    def testSchema(schema: Seq[StructField], expected: Set[String]): Unit = {\n      assert(getGeneratedColumnsAndColumnsUsedByGeneratedColumns(StructType(schema)) == expected)\n    }\n\n    val f1 = StructField(\"c1\", IntegerType)\n    val f2 = withGenerationExpression(StructField(\"c2\", IntegerType), \"c1 + 10\")\n    val f3 = StructField(\"c3\", IntegerType)\n    val f4 = withGenerationExpression(StructField(\"c4\", IntegerType), \"hash(c3 + 10)\")\n    val f5 = withGenerationExpression(StructField(\"c5\", IntegerType), \"hash(C1 + 10)\")\n    val f6 = StructField(\"c6\", StructType(StructField(\"x\", IntegerType) :: Nil))\n    val f6x = StructField(\"c6.x\", IntegerType)\n    val f7x = withGenerationExpression(StructField(\"c7.x\", IntegerType), \"`c6.x` + 10\")\n    val f8 = withGenerationExpression(StructField(\"c8\", IntegerType), \"c6.x + 10\")\n    testSchema(Seq(f1, f2), Set(\"c1\", \"c2\"))\n    testSchema(Seq(f1, f2, f3), Set(\"c1\", \"c2\"))\n    testSchema(Seq(f1, f2, f3, f4), Set(\"c1\", \"c2\", \"c3\", \"c4\"))\n    testSchema(Seq(f1, f2, f5), Set(\"c1\", \"c2\", \"c5\"))\n    testSchema(Seq(f6x, f7x), Set(\"c6.x\", \"c7.x\"))\n    testSchema(Seq(f6, f6x, f7x), Set(\"c6.x\", \"c7.x\"))\n    testSchema(Seq(f6, f6x, f8), Set(\"c6\", \"c8\"))\n  }\n\n  test(\"disallow column type evolution\") {\n    withTableName(\"disallow_column_type_evolution\") { table =>\n    // \"HASH(c1)\" returns different results for INT and LONG. For example, \"SELECT hash(32767)\"\n    // returns 1249274084, but \"SELECT hash(32767L)\" returns -860381306. Hence we should\n    // not allow updating column type from INT to LONG.\n    createTable(table, None, \"c1 INT, c2 INT\",\n        Map(\"c2\" -> \"HASH(c1)\"), Nil)\n      val tableSchema = spark.table(table).schema\n      Seq(32767).toDF(\"c1\").write.format(\"delta\").mode(\"append\").saveAsTable(table)\n      assert(tableSchema == spark.table(table).schema)\n      // Insert a LONG to `c1` should fail rather than changing the `c1` type to LONG.\n      checkError(\n        intercept[AnalysisException] {\n          Seq(32767.toLong).toDF(\"c1\").write.format(\"delta\").mode(\"append\")\n            .option(\"mergeSchema\", \"true\")\n            .saveAsTable(table)\n        },\n        \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n        parameters = Map(\n          \"columnName\" -> \"c1\",\n          \"columnType\" -> \"INT\",\n          \"dataType\" -> \"BIGINT\",\n          \"generatedColumns\" -> \"c2 -> HASH(c1)\"\n        ))\n      checkAnswer(spark.table(table), Row(32767, 1249274084) :: Nil)\n    }\n  }\n\n  test(\"disallow column type evolution - nesting\") {\n    withTableName(\"disallow_column_type_evolution\") { table =>\n      createTable(table, None, \"a SMALLINT, c1 STRUCT<a: SMALLINT>, c2 INT\",\n        Map(\"c2\" -> \"HASH(a)\"), Nil)\n      val tableSchema = spark.table(table).schema\n      Seq(32767.toShort).toDF(\"a\")\n        .selectExpr(\"a\", \"named_struct('a', a) as c1\")\n        .write.format(\"delta\").mode(\"append\").saveAsTable(table)\n      assert(tableSchema == spark.table(table).schema)\n\n      // INSERT an INT to `c1.a` should not fail\n      Seq((32767.toShort, 32767)).toDF(\"a\", \"c1a\")\n        .selectExpr(\"a\", \"named_struct('a', c1a) as c1\")\n        .write.format(\"delta\").mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .saveAsTable(table)\n\n      // Insert an INT to `a` should fail rather than changing the `a` type to INT\n      checkError(\n        intercept[AnalysisException] {\n          Seq((32767, 32767)).toDF(\"a\", \"c1a\")\n            .selectExpr(\"a\", \"named_struct('a', c1a) as c1\")\n            .write.format(\"delta\").mode(\"append\")\n            .option(\"mergeSchema\", \"true\")\n            .saveAsTable(table)\n        },\n        \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n        parameters = Map(\n          \"columnName\" -> \"a\",\n          \"columnType\" -> \"SMALLINT\",\n          \"dataType\" -> \"INT\",\n          \"generatedColumns\" -> \"c2 -> HASH(a)\"\n        )\n      )\n    }\n  }\n\n  test(\"changing the type of a nested field named the same as the generated column\") {\n    withTableName(\"disallow_column_type_evolution\") { table =>\n      createTable(table, None, \"a INT, t STRUCT<gen: SMALLINT>, gen INT\",\n        Map(\"gen\" -> \"HASH(a)\"), Nil)\n      // Changing the type of `t.gen` should succeed since it's not actually the generated column.\n      Seq((32767, 32767)).toDF(\"a\", \"gen\")\n        .selectExpr(\"a\", \"named_struct('gen', gen) as t\")\n        .write.format(\"delta\").mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .saveAsTable(table)\n      checkAnswer(spark.table(table), Row(32767, Row(32767), 1249274084) :: Nil)\n    }\n  }\n\n  test(\"changing the type of nested field not referenced by a generated col\") {\n    withTableName(\"disallow_column_type_evolution\") { table =>\n      createTable(table, None, \"t STRUCT<a: SMALLINT, b: SMALLINT>, gen INT\",\n        Map(\"gen\" -> \"HASH(t.a)\"), Nil)\n\n      // changing the type of `t.b` should succeed since it is not being\n      // referenced by any CHECK constraints or generated columns.\n      Seq((32767.toShort, 32767)).toDF(\"a\", \"b\")\n        .selectExpr(\"named_struct('a', a, 'b', b) as t\")\n        .write.format(\"delta\").mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .saveAsTable(table)\n      checkAnswer(spark.table(table), Row(Row(32767, 32767), 1249274084) :: Nil)\n    }\n  }\n\n\n  test(\"reading from a Delta table should not see generation expressions\") {\n    def verifyNoGenerationExpression(df: Dataset[_]): Unit = {\n      assert(!hasGeneratedColumns(df.schema))\n    }\n\n    withTableName(\"test_source\") { table =>\n      createTable(table, None, \"c1 INT, c2 INT\", Map(\"c1\" -> \"c2 + 1\"), Nil)\n      sql(s\"INSERT INTO $table VALUES(2, 1)\")\n      val path = DeltaLog.forTable(spark, TableIdentifier(table)).dataPath.toString\n\n      verifyNoGenerationExpression(spark.table(table))\n      verifyNoGenerationExpression(spark.sql(s\"select * from $table\"))\n      verifyNoGenerationExpression(spark.sql(s\"select * from delta.`$path`\"))\n      verifyNoGenerationExpression(spark.read.format(\"delta\").load(path))\n      verifyNoGenerationExpression(spark.read.format(\"delta\").table(table))\n      verifyNoGenerationExpression(spark.readStream.format(\"delta\").load(path))\n      verifyNoGenerationExpression(spark.readStream.format(\"delta\").table(table))\n      withTempDir { checkpointDir =>\n        val q = spark.readStream.format(\"delta\").table(table).writeStream\n          .trigger(Trigger.Once)\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .foreachBatch { (ds: DataFrame, _: Long) =>\n            verifyNoGenerationExpression(ds)\n          }.start()\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n      withTempDir { outputDir =>\n        withTempDir { checkpointDir =>\n          val q = spark.readStream.format(\"delta\").table(table).writeStream\n            .trigger(Trigger.Once)\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"delta\")\n            .start(outputDir.getCanonicalPath)\n          try {\n            q.processAllAvailable()\n          } finally {\n            q.stop()\n          }\n          val deltaLog = DeltaLog.forTable(spark, outputDir)\n          assert(deltaLog.snapshot.version >= 0)\n          assert(!hasGeneratedColumns(deltaLog.snapshot.schema))\n        }\n      }\n    }\n  }\n\n  /**\n   * Verify if the table metadata matches the default test table. We use this to verify DDLs\n   * write correct table metadata into the transaction logs.\n   */\n  protected def verifyDefaultTestTableMetadata(table: String): Unit = {\n    val (deltaLog, snapshot) = if (table.startsWith(\"delta.\")) {\n      DeltaLog.forTableWithSnapshot(spark, table.stripPrefix(\"delta.`\").stripSuffix(\"`\"))\n    } else {\n      DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n    }\n    val schema = StructType.fromDDL(defaultTestTableSchema)\n    val expectedSchema = StructType(schema.map { field =>\n      defaultTestTableGeneratedColumns.get(field.name).map { expr =>\n        withGenerationExpression(field, expr)\n      }.getOrElse(field)\n    })\n    val partitionColumns = defaultTestTablePartitionColumns\n    val metadata = snapshot.metadata\n    assert(metadata.schema == expectedSchema)\n    assert(metadata.partitionColumns == partitionColumns)\n  }\n\n  protected def testCreateTable(testName: String)(createFunc: String => Unit): Unit = {\n    test(testName) {\n      withTable(testName) {\n        createFunc(testName)\n        verifyDefaultTestTableMetadata(testName)\n      }\n    }\n  }\n\n  protected def testCreateTableWithLocation(\n      testName: String)(createFunc: (String, String) => Unit): Unit = {\n    test(testName + \": external\") {\n      withTempPath { path =>\n        withTable(testName) {\n          createFunc(testName, path.getCanonicalPath)\n          verifyDefaultTestTableMetadata(testName)\n          verifyDefaultTestTableMetadata(s\"delta.`${path.getCanonicalPath}`\")\n        }\n      }\n    }\n  }\n\n  testCreateTable(\"create_table\") { table =>\n    createTable(\n      table,\n      None,\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns\n    )\n  }\n\n  testCreateTable(\"replace_table\") { table =>\n    createTable(table, None, \"id bigint\", Map.empty, Seq.empty)\n    replaceTable(\n      table,\n      None,\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns\n    )\n  }\n\n  testCreateTable(\"create_or_replace_table_non_exist\") { table =>\n    replaceTable(\n      table,\n      None,\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns,\n      orCreate = Some(true)\n    )\n  }\n\n  testCreateTable(\"create_or_replace_table_exist\") { table =>\n    createTable(table, None, \"id bigint\", Map.empty, Seq.empty)\n    replaceTable(\n      table,\n      None,\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns,\n      orCreate = Some(true)\n    )\n  }\n\n  testCreateTableWithLocation(\"create_table\") { (table, path) =>\n    createTable(\n      table,\n      Some(path),\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns\n    )\n  }\n\n  testCreateTableWithLocation(\"replace_table\") { (table, path) =>\n    createTable(\n      table,\n      Some(path),\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns\n    )\n    replaceTable(\n      table,\n      Some(path),\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns\n    )\n  }\n\n  testCreateTableWithLocation(\"create_or_replace_table_non_exist\") { (table, path) =>\n    replaceTable(\n      table,\n      Some(path),\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns,\n      orCreate = Some(true)\n    )\n  }\n\n  testCreateTableWithLocation(\"create_or_replace_table_exist\") { (table, path) =>\n    createTable(\n      table,\n      Some(path),\n      \"id bigint\",\n      Map.empty,\n      Seq.empty\n    )\n    replaceTable(\n      table,\n      Some(path),\n      defaultTestTableSchema,\n      defaultTestTableGeneratedColumns,\n      defaultTestTablePartitionColumns,\n      orCreate = Some(true)\n    )\n  }\n\n  test(\"using generated columns should upgrade the protocol\") {\n    withTableName(\"upgrade_protocol\") { table =>\n      // Use the default protocol versions when not using computed partitions.\n      createTable(table, None, \"i INT\", Map.empty, Seq.empty)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = table))\n      assert(deltaLog.update().protocol == Protocol(1, 2))\n      assert(DeltaLog.forTable(spark, TableIdentifier(tableName = table)).snapshot.version == 0)\n\n      // Protocol versions should be upgraded when using computed partitions.\n      replaceTable(\n        table,\n        None,\n        defaultTestTableSchema,\n        defaultTestTableGeneratedColumns,\n        defaultTestTablePartitionColumns)\n      assert(deltaLog.update().protocol == Protocol(1, 7).withFeatures(Seq(\n        AppendOnlyTableFeature,\n        InvariantsTableFeature,\n        GeneratedColumnsTableFeature)))\n      // Make sure we did overwrite the table rather than deleting and re-creating.\n      assert(DeltaLog.forTable(spark, TableIdentifier(tableName = table)).update().version == 1)\n    }\n  }\n\n  test(\"creating a table with a different schema should fail\") {\n    withTempPath { path =>\n      // Currently SQL is the only way to define a table using generated columns. So we create a\n      // temp table and drop it to get a path for such table.\n      withTableName(\"temp_generated_column_table\") { table =>\n        createTable(\n          null,\n          Some(path.toString),\n          defaultTestTableSchema,\n          defaultTestTableGeneratedColumns,\n          defaultTestTablePartitionColumns\n        )\n      }\n      withTableName(\"table_with_no_schema\") { table =>\n        createTable(\n          table,\n          Some(path.toString),\n          \"\",\n          Map.empty,\n          Seq.empty\n        )\n        verifyDefaultTestTableMetadata(table)\n      }\n      withTableName(\"table_with_different_expr\") { table =>\n        val e = intercept[AnalysisException](\n          createTable(\n            table,\n            Some(path.toString),\n            defaultTestTableSchema,\n            Map(\n              \"c2_g\" -> \"c1 + 11\", // use a different generated expr\n              \"c4_g_p\" -> \"CAST(c5 AS date)\",\n              \"c7_g_p\" -> \"c6 * 10\"\n            ),\n            defaultTestTablePartitionColumns\n          )\n        )\n        assert(e.getMessage.contains(\n          \"Specified generation expression for field c2_g is different from existing schema\"))\n        assert(e.getMessage.contains(\"Specified: c1 + 11\"))\n        assert(e.getMessage.contains(\"Existing:  c1 + 10\"))\n      }\n      withTableName(\"table_add_new_expr\") { table =>\n        val e = intercept[AnalysisException](\n          createTable(\n            table,\n            Some(path.toString),\n            defaultTestTableSchema,\n            Map(\n              \"c2_g\" -> \"c1 + 10\",\n              \"c3_p\" -> \"CAST(c1 AS string)\", // add a generated expr\n              \"c4_g_p\" -> \"CAST(c5 AS date)\",\n              \"c7_g_p\" -> \"c6 * 10\"\n            ),\n            defaultTestTablePartitionColumns\n          )\n        )\n        assert(e.getMessage.contains(\n          \"Specified generation expression for field c3_p is different from existing schema\"))\n        assert(e.getMessage.contains(\"CAST(c1 AS string)\"))\n        assert(e.getMessage.contains(\"Existing:  \\n\"))\n      }\n    }\n  }\n\n  test(\"use the generation expression, column comment and NOT NULL at the same time\") {\n    withTableName(\"generation_expression_comment\") { table =>\n      createTable(\n        table,\n        None,\n        \"c1 INT, c2 INT, c3 INT\",\n        Map(\"c2\" -> \"c1 + 10\", \"c3\" -> \"c1 + 10\"),\n        Seq.empty,\n        Set(\"c3\"),\n        Map(\"c2\" -> \"foo\", \"c3\" -> \"foo\")\n      )\n      // Verify schema\n      val f1 = StructField(\"c1\", IntegerType, nullable = true)\n      val fieldMetadata = new MetadataBuilder()\n        .putString(GENERATION_EXPRESSION_METADATA_KEY, \"c1 + 10\")\n        .putString(\"comment\", \"foo\")\n        .build()\n      val f2 = StructField(\"c2\", IntegerType, nullable = true, metadata = fieldMetadata)\n      val f3 = StructField(\"c3\", IntegerType, nullable = false, metadata = fieldMetadata)\n      val expectedSchema = StructType(f1 :: f2 :: f3 :: Nil)\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n      assert(snapshot.metadata.schema == expectedSchema)\n      // Verify column comment\n      val comments = sql(s\"DESC $table\")\n        .where(\"col_name = 'c2'\")\n        .select(\"comment\")\n        .as[String]\n        .collect()\n        .toSeq\n      assert(\"foo\" :: Nil == comments)\n    }\n  }\n\n  test(\"generation expression allows timestampdiff & timestampadd\") {\n    withTableName(\"generation_expression_timestamp_diff_add\") { tableName =>\n      createTable(\n        tableName,\n        path = None,\n        schemaString = \"c1 TIMESTAMP, c2 TIMESTAMP, c3 BIGINT, c4 TIMESTAMP\",\n        generatedColumns =\n          Map(\"c3\" -> \"timestampdiff(MONTH, c1, c2)\", \"c4\" -> \"timestampadd(MONTH, 1, c1)\"),\n        partitionColumns = Seq.empty)\n    }\n  }\n\n  test(\"MERGE UPDATE basic\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET ${tgt}.c2 = ${src}.c2\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 3, 4))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE set both generated column and its input\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET ${tgt}.c2 = ${src}.c2, ${tgt}.c3 = ${src}.c3\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 3, 4))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE set star\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 4, 5);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET *\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 4, 5))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE set star add column\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTableName(\"source\") { src =>\n        withTableName(\"target\") { tgt =>\n          createTable(src, None, \"c1 INT, c2 INT, c4 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (1, 20, 40);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n          sql(\n            s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET *\n               |\"\"\".stripMargin)\n          checkAnswer(\n            sql(s\"SELECT * FROM ${tgt}\"),\n            Seq(Row(1, 20, 21, 40))\n          )\n        }\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE using value from target\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET ${tgt}.c2 = ${tgt}.c3\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 3, 4))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE using value from both target and source\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET ${tgt}.c2 = ${tgt}.c3 + ${src}.c3\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 7, 8))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE set to null\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE SET ${tgt}.c2 = null\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, null, null))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE multiple columns\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED THEN UPDATE\n               |  SET ${tgt}.c2 = ${src}.c1 * 10, ${tgt}.c1 = ${tgt}.c1 * 100\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(100, 10, 11))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE source is a query\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING (SELECT c1, max(c3) + min(c2) AS m FROM ${src} GROUP BY c1) source\n               |on ${tgt}.c1 = source.c1\n               |WHEN MATCHED THEN UPDATE SET ${tgt}.c2 = source.m\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 7, 8))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE UPDATE temp view is not supported\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        withTempView(\"test_temp_view\") {\n          createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (1, 3, 4);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n          sql(s\"CREATE TEMP VIEW test_temp_view AS SELECT c1 as c2, c2 as c1, c3 FROM ${tgt}\")\n          val e = intercept[AnalysisException] {\n            sql(s\"\"\"\n                   |MERGE INTO test_temp_view\n                   |USING ${src}\n                   |on test_temp_view.c2 = ${src}.c1\n                   |WHEN MATCHED THEN UPDATE SET test_temp_view.c1 = ${src}.c2\n                   |\"\"\".stripMargin)\n          }\n          assert(e.getMessage.contains(\"a temp view\"))\n        }\n      }\n    }\n  }\n\n  test(\"MERGE INSERT star satisfies constraint\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(2, 3, 4))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT star violates constraint\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 5);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        val e = intercept[InvariantViolationException](\n          sql(s\"\"\"\n                 |MERGE INTO ${tgt}\n                 |USING ${src}\n                 |on ${tgt}.c1 = ${src}.c1\n                 |WHEN NOT MATCHED THEN INSERT *\n                 |\"\"\".stripMargin)\n        )\n        assert(e.getMessage.contains(\"CHECK constraint Generated Column\"))\n      }\n    }\n  }\n\n  test(\"MERGE INSERT star add column\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTableName(\"source\") { src =>\n        withTableName(\"target\") { tgt =>\n          createTable(src, None, \"c1 INT, c2 INT, c4 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (2, 3, 5);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n          sql(s\"\"\"\n                 |MERGE INTO ${tgt}\n                 |USING ${src}\n                 |on ${tgt}.c1 = ${src}.c1\n                 |WHEN NOT MATCHED THEN INSERT *\n                 |\"\"\".stripMargin)\n          checkAnswer(\n            sql(s\"SELECT * FROM ${tgt}\"),\n            Seq(Row(1, 2, 3, null), Row(2, 3, 4, 5))\n          )\n        }\n      }\n    }\n  }\n\n  test(\"MERGE INSERT star add column violates constraint\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTableName(\"source\") { src =>\n        withTableName(\"target\") { tgt =>\n          createTable(src, None, \"c1 INT, c3 INT, c4 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (2, 3, 5);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n          val e = intercept[InvariantViolationException](\n            sql(s\"\"\"\n                 |MERGE INTO ${tgt}\n                 |USING ${src}\n                 |on ${tgt}.c1 = ${src}.c1\n                 |WHEN NOT MATCHED THEN INSERT *\n                 |\"\"\".stripMargin)\n          )\n          assert(e.getMessage.contains(\"CHECK constraint Generated Column\"))\n        }\n      }\n    }\n  }\n\n  test(\"MERGE INSERT star add column unrelated to generated columns\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTableName(\"source\") { src =>\n        withTableName(\"target\") { tgt =>\n          createTable(src, None, \"c1 INT, c4 INT, c5 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (2, 3, 5);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n          sql(s\"\"\"\n                 |MERGE INTO ${tgt}\n                 |USING ${src}\n                 |on ${tgt}.c1 = ${src}.c1\n                 |WHEN NOT MATCHED THEN INSERT *\n                 |\"\"\".stripMargin)\n          checkAnswer(\n            sql(s\"SELECT * FROM ${tgt}\"),\n            Seq(Row(1, 2, 3, null, null), Row(2, null, null, 3, 5))\n          )\n        }\n      }\n    }\n  }\n\n  test(\"MERGE INSERT unrelated columns\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT (c1) VALUES (${src}.c1)\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(2, null, null))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT unrelated columns with const\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT (c1) VALUES (3)\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(3, null, null))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT referenced column only\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT (c2) VALUES (10)\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(null, 10, 11))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT referenced column with null\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT (c2) VALUES (null)\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(null, null, null))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT not all referenced column inserted\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + c1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT (c2) VALUES (5)\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(null, 5, null))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT generated column only\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        val e = intercept[InvariantViolationException](\n          sql(s\"\"\"\n                 |MERGE INTO ${tgt}\n                 |USING ${src}\n                 |on ${tgt}.c1 = ${src}.c1\n                 |WHEN NOT MATCHED THEN INSERT (c3) VALUES (10)\n                 |\"\"\".stripMargin)\n        )\n        assert(e.getMessage.contains(\"CHECK constraint Generated Column\"))\n      }\n    }\n  }\n\n  test(\"MERGE INSERT referenced and generated columns satisfies constraint\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 4);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN NOT MATCHED THEN INSERT (c2, c3) VALUES (${src}.c2, ${src}.c3)\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, 3), Row(null, 3, 4))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT referenced and generated columns violates constraint\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (2, 3, 5);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3);\")\n        val e = intercept[InvariantViolationException](\n          sql(s\"\"\"\n                 |MERGE INTO ${tgt}\n                 |USING ${src}\n                 |on ${tgt}.c1 = ${src}.c1\n                 |WHEN NOT MATCHED THEN INSERT (c2, c3) VALUES (${src}.c2, ${src}.c3)\n                 |\"\"\".stripMargin)\n        )\n        assert(e.getMessage.contains(\"CHECK constraint Generated Column\"))\n      }\n    }\n  }\n\n  test(\"MERGE INSERT and UPDATE\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(src, None, \"c1 INT, c2 INT, c3 INT\", Map.empty, Seq.empty)\n        sql(s\"INSERT INTO ${src} values (1, 11, 12), (2, 3, 4), (3, 20, 21), (4, 5, 6), (5, 6, 7);\")\n        createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n        sql(s\"INSERT INTO ${tgt} values (1, 2, 3), (2, 100, 101);\")\n        sql(s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED AND ${src}.c1 = 1 THEN UPDATE SET ${tgt}.c2 = 100\n               |WHEN MATCHED THEN UPDATE SET *\n               |WHEN NOT MATCHED AND ${src}.c1 = 4 THEN INSERT (c1, c2) values (${src}.c1, 22)\n               |WHEN NOT MATCHED AND ${src}.c1 = 5 THEN INSERT (c1, c2) values (5, ${src}.c3)\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 100, 101), Row(2, 3, 4), Row(3, 20, 21), Row(4, 22, 23), Row(5, 7, 8))\n        )\n      }\n    }\n  }\n\n  test(\"MERGE INSERT and UPDATE schema evolution\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTableName(\"source\") { src =>\n        withTableName(\"target\") { tgt =>\n          createTable(src, None, \"c1 INT, c2 INT, c4 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (1, 11, 12), (2, 3, 4), (3, 20, 21), \" +\n            \"(4, 5, 6), (5, 6, 7);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\", Map(\"c3\" -> \"c2 + 1\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 3), (2, 100, 101);\")\n          sql(\n            s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED AND ${src}.c1 = 1 THEN UPDATE SET ${tgt}.c2 = 100\n               |WHEN MATCHED THEN UPDATE SET *\n               |WHEN NOT MATCHED AND ${src}.c1 = 4 THEN INSERT (c1, c2) values (${src}.c1, 22)\n               |WHEN NOT MATCHED AND ${src}.c1 = 5 THEN INSERT (c1, c2) values (5, ${src}.c4)\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n          checkAnswer(\n            sql(s\"SELECT * FROM ${tgt}\"),\n            Seq(\n              Row(1, 100, 101, null),\n              Row(2, 3, 4, 4),\n              Row(3, 20, 21, 21),\n              Row(4, 22, 23, null),\n              Row(5, 7, 8, null)\n            )\n          )\n        }\n      }\n    }\n  }\n\n  test(\"MERGE INSERT and UPDATE schema evolution multiple referenced columns\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTableName(\"source\") { src =>\n        withTableName(\"target\") { tgt =>\n          createTable(src, None, \"c1 INT, c2 INT, c4 INT\", Map.empty, Seq.empty)\n          sql(s\"INSERT INTO ${src} values (1, 11, 12), (2, null, 4), (3, 20, 21), \" +\n            \"(4, 5, 6), (5, 6, 7);\")\n          createTable(tgt, None, \"c1 INT, c2 INT, c3 INT\",\n            Map(\"c3\" -> \"c1 + CAST(ISNULL(c2) AS INT)\"), Seq.empty)\n          sql(s\"INSERT INTO ${tgt} values (1, 2, 1), (2, 100, 2);\")\n          sql(\n            s\"\"\"\n               |MERGE INTO ${tgt}\n               |USING ${src}\n               |on ${tgt}.c1 = ${src}.c1\n               |WHEN MATCHED AND ${src}.c1 = 1 THEN UPDATE SET ${tgt}.c2 = 100\n               |WHEN MATCHED THEN UPDATE SET *\n               |WHEN NOT MATCHED AND ${src}.c1 = 4 THEN INSERT (c1, c2) values (${src}.c1, 22)\n               |WHEN NOT MATCHED AND ${src}.c1 = 5 THEN INSERT (c1) values (5)\n               |WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n          checkAnswer(\n            sql(s\"SELECT * FROM ${tgt}\"),\n            Seq(\n              Row(1, 100, 1, null),\n              Row(2, null, 3, 4),\n              Row(3, 20, 3, 21),\n              Row(4, 22, 4, null),\n              Row(5, null, 6, null)\n            )\n          )\n        }\n      }\n    }\n  }\n\n  test(\"MERGE INSERT with schema evolution on different name case\") {\n    withTableName(\"source\") { src =>\n      withTableName(\"target\") { tgt =>\n        createTable(\n          tableName = src,\n          path = None,\n          schemaString = \"c1 INT, c2 INT\",\n          generatedColumns = Map.empty,\n          partitionColumns = Seq.empty\n        )\n        sql(s\"INSERT INTO ${src} values (2, 4);\")\n        createTable(\n          tableName = tgt,\n          path = None,\n          schemaString = \"c1 INT, c3 INT\",\n          generatedColumns = Map(\"c3\" -> \"c1 + 1\"),\n          partitionColumns = Seq.empty\n        )\n        sql(s\"INSERT INTO ${tgt} values (1, 2);\")\n\n        withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n          sql(s\"\"\"\n             |MERGE INTO ${tgt}\n             |USING ${src}\n             |on ${tgt}.c1 = ${src}.c1\n             |WHEN NOT MATCHED THEN INSERT (c1, C2) VALUES (${src}.c1, ${src}.c2)\n             |\"\"\".stripMargin)\n        }\n        checkAnswer(\n          sql(s\"SELECT * FROM ${tgt}\"),\n          Seq(Row(1, 2, null), Row(2, 3, 4))\n        )\n      }\n    }\n  }\n\n  test(\"generated columns with cdf\") {\n    val tableName1 = \"gcEnabledCDCOn\"\n    val tableName2 = \"gcEnabledCDCOff\"\n    withTable(tableName1, tableName2) {\n      def readCdf(startingVersion: Long): DataFrame = {\n        spark.read.format(\"delta\").option(\"readChangeData\", \"true\")\n          .option(\"startingVersion\", startingVersion)\n          .table(tableName1)\n          .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n      }\n\n      createTable(\n        tableName1,\n        None,\n        schemaString = \"id LONG, timeCol TIMESTAMP, dateCol DATE\",\n        generatedColumns = Map(\n          \"dateCol\" -> \"CAST(timeCol AS DATE)\"\n        ),\n        partitionColumns = Seq(\"dateCol\"),\n        properties = Map(\n          \"delta.enableChangeDataFeed\" -> \"true\"\n        )\n      )\n\n      checkAnswer(readCdf(startingVersion = 0), Seq())\n\n      spark.range(100).repartition(10)\n        .withColumn(\n          \"timeCol\", lit(sqlTimestamp(\"1970-01-01 00:00:00\")) + make_dt_interval($\"id\"))\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .saveAsTable(tableName1)\n\n      spark.sql(s\"DELETE FROM $tableName1 WHERE id < 3\")\n\n      checkAnswer(\n        readCdf(startingVersion = 2),\n        Seq(\n          Row(0, sqlTimestamp(\"1970-01-01 00:00:00\"), sqlDate(\"1970-01-01\"), \"delete\", 2),\n          Row(1, sqlTimestamp(\"1970-01-02 00:00:00\"), sqlDate(\"1970-01-02\"), \"delete\", 2),\n          Row(2, sqlTimestamp(\"1970-01-03 00:00:00\"), sqlDate(\"1970-01-03\"), \"delete\", 2)\n        )\n      )\n\n      // Now write out the data frame of cdc to another table that has generated columns but not\n      // cdc enabled.\n      createTable(\n        tableName2,\n        None,\n        schemaString = \"id LONG, _change_type STRING, timeCol TIMESTAMP, dateCol DATE\",\n        generatedColumns = Map(\n          \"dateCol\" -> \"CAST(timeCol AS DATE)\"\n        ),\n        partitionColumns = Seq(\"dateCol\"),\n        properties = Map(\n          \"delta.enableChangeDataFeed\" -> \"false\"\n        )\n      )\n\n      val cdcRead = spark.read.format(\"delta\").option(\"readChangeData\", \"true\")\n        .option(\"startingVersion\", \"2\")\n        .table(tableName1)\n        .select(\"id\", CDCReader.CDC_TYPE_COLUMN_NAME, \"timeCol\")\n\n      cdcRead\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .saveAsTable(tableName2)\n\n      checkAnswer(\n        cdcRead,\n        spark.table(tableName2).drop(\"dateCol\")\n      )\n    }\n  }\n\n  test(\"not null should be enforced with generated columns\") {\n    withTableName(\"tbl\") { tbl =>\n      createTable(tbl,\n        None, \"c1 INT, c2 STRING, c3 INT\", Map(\"c3\" -> \"c1 + 1\"), Seq.empty, Set(\"c1\", \"c2\", \"c3\"))\n\n      // try to write data without c2 in the DF\n      val schemaWithoutColumnC2 = StructType(\n        Seq(StructField(\"c1\", IntegerType, true)))\n      val data1 = List(Row(3))\n      val df1 = spark.createDataFrame(data1.asJava, schemaWithoutColumnC2)\n\n      val e1 = intercept[DeltaInvariantViolationException] {\n        df1.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n      }\n      assert(e1.getMessage.contains(\"Column c2, which has a NOT NULL constraint,\" +\n        \" is missing from the data being written into the table.\"))\n    }\n  }\n\n  Seq(true, false).foreach { allowNullInsert =>\n    test(\"nullable column should work with generated columns - \" +\n      \"allowNullInsert enabled=\" + allowNullInsert) {\n      withTableName(\"tbl\") { tbl =>\n        withSQLConf(DeltaSQLConf.GENERATED_COLUMN_ALLOW_NULLABLE.key -> allowNullInsert.toString) {\n          createTable(\n            tbl, None, \"c1 INT, c2 STRING, c3 INT\", Map(\"c3\" -> \"c1 + 1\"), Seq.empty)\n\n          // create data frame that matches the table's schema\n          val data1 = List(Row(1, \"a1\"), Row(2, \"a2\"))\n          val schema = StructType(\n            Seq(StructField(\"c1\", IntegerType, true), StructField(\"c2\", StringType, true)))\n          val df1 = spark.createDataFrame(data1.asJava, schema)\n          df1.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n\n          // create a data frame that does not have c2\n          val schemaWithoutOptionalColumnC2 = StructType(\n            Seq(StructField(\"c1\", IntegerType, true)))\n\n          val data2 = List(Row(3))\n          val df2 = spark.createDataFrame(data2.asJava, schemaWithoutOptionalColumnC2)\n\n          if (allowNullInsert) {\n            df2.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n            // check correctness\n            val expectedDF = df1\n              .union(df2.withColumn(\"c2\", lit(null).cast(StringType)))\n              .withColumn(\"c3\", 'c1 + 1)\n            checkAnswer(spark.read.table(tbl), expectedDF)\n          } else {\n            // when allow null insert is not enabled.\n            val e = intercept[AnalysisException] {\n              df2.write.format(\"delta\").mode(\"append\").saveAsTable(\"tbl\")\n            }\n            e.getMessage.contains(\n              \"A column, variable, or function parameter with name `c2` cannot be resolved\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"generated column metadata is not exposed in schema\") {\n    val tableName = \"table\"\n    withTable(tableName) {\n      createDefaultTestTable(tableName)\n      Seq((1L, \"foo\", Timestamp.valueOf(\"2020-10-11 12:30:30\"), 100, Date.valueOf(\"2020-11-12\")))\n        .toDF(\"c1\", \"c3_p\", \"c5\", \"c6\", \"c8\")\n        .write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n\n      val expectedSchema = new StructType()\n        .add(\"c1\", LongType)\n        .add(\"c2_g\", LongType)\n        .add(\"c3_p\", StringType)\n        .add(\"c4_g_p\", DateType)\n        .add(\"c5\", TimestampType)\n        .add(\"c6\", IntegerType)\n        .add(\"c7_g_p\", IntegerType)\n        .add(\"c8\", DateType)\n\n      assert(spark.read.table(tableName).schema === expectedSchema)\n\n      val ttDf = spark.read.option(DeltaOptions.VERSION_AS_OF, 0).table(tableName)\n      assert(ttDf.schema === expectedSchema)\n\n      val cdcDf = spark.read\n        .option(DeltaOptions.CDC_READ_OPTION, true)\n        .option(DeltaOptions.STARTING_VERSION_OPTION, 0)\n        .table(tableName)\n      assert(cdcDf.schema === expectedSchema\n        .add(\"_change_type\", StringType)\n        .add(\"_commit_version\", LongType)\n        .add(\"_commit_timestamp\", TimestampType)\n      )\n\n      assert(spark.readStream.table(tableName).schema === expectedSchema)\n\n      val cdcStreamDf = spark.readStream\n        .option(DeltaOptions.CDC_READ_OPTION, true)\n        .option(DeltaOptions.STARTING_VERSION_OPTION, 0)\n        .table(tableName)\n      assert(cdcStreamDf.schema === expectedSchema\n        .add(\"_change_type\", StringType)\n        .add(\"_commit_version\", LongType)\n        .add(\"_commit_timestamp\", TimestampType)\n      )\n    }\n  }\n\n  test(\"DML into table with generated column, char column and readSideCharPadding=true\") {\n    val tableName = \"table\"\n    withTable(tableName) {\n      withSQLConf(SQLConf.READ_SIDE_CHAR_PADDING.key -> \"true\") {\n        createTable(tableName, None, \"c1 INT, c2 CHAR(5), c3 INT\", Map(\"c3\" -> \"c1 + 1\"), Nil)\n        spark.sql(\n          s\"\"\"\n             |MERGE INTO $tableName AS TARGET\n             |USING (SELECT id as c1, cast(id AS CHAR(5)) as c2 FROM RANGE(10)) AS SOURCE\n             |ON TARGET.c1 = SOURCE.c1\n             |WHEN MATCHED THEN UPDATE SET c1 = SOURCE.c1, c2 = SOURCE.c2\n             |WHEN NOT MATCHED THEN INSERT (c1, c2) VALUES (SOURCE.c1, SOURCE.c2)\n             |\"\"\".stripMargin)\n        spark.sql(s\"UPDATE $tableName SET c2 = 'upd' WHERE c1 = 1\")\n      }\n    }\n  }\n}\n\nclass GeneratedColumnSuite extends GeneratedColumnSuiteBase\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/GeneratedColumnTest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.PrintWriter\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.schema.{DeltaInvariantViolationException, InvariantViolationException, SchemaUtils}\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport io.delta.tables.DeltaTableBuilder\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, Dataset, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.util.DateTimeUtils.{getZoneId, stringToDate, stringToTimestamp, toJavaDate, toJavaTimestamp}\nimport org.apache.spark.sql.catalyst.util.quietly\nimport org.apache.spark.sql.functions.{current_timestamp, lit}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.{StreamingQueryException, Trigger}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{ArrayType, DateType, IntegerType, MetadataBuilder, StringType, StructField, StructType, TimestampType}\nimport org.apache.spark.unsafe.types.UTF8String\n\ntrait GeneratedColumnTest extends QueryTest with SharedSparkSession with DeltaSQLCommandTest\n    with DeltaSQLTestUtils {\n\n\n  protected def sqlDate(date: String): java.sql.Date = {\n    toJavaDate(stringToDate(UTF8String.fromString(date)).get)\n  }\n\n  protected def sqlTimestamp(timestamp: String): java.sql.Timestamp = {\n    toJavaTimestamp(stringToTimestamp(\n      UTF8String.fromString(timestamp),\n      getZoneId(SQLConf.get.sessionLocalTimeZone)).get)\n  }\n\n  protected def withTableName[T](tableName: String)(func: String => T): Unit = {\n    withTable(tableName) {\n      func(tableName)\n    }\n  }\n\n  /** Create a new field with the given generation expression. */\n  def withGenerationExpression(field: StructField, expr: String): StructField = {\n    val newMetadata = new MetadataBuilder()\n      .withMetadata(field.metadata)\n      .putString(GENERATION_EXPRESSION_METADATA_KEY, expr)\n      .build()\n    field.copy(metadata = newMetadata)\n  }\n\n  protected def buildTable(\n      builder: DeltaTableBuilder,\n      tableName: String,\n      path: Option[String],\n      schemaString: String,\n      generatedColumns: Map[String, String],\n      partitionColumns: Seq[String],\n      notNullColumns: Set[String],\n      comments: Map[String, String],\n      properties: Map[String, String]): DeltaTableBuilder = {\n    val schema = if (schemaString.nonEmpty) {\n      StructType.fromDDL(schemaString)\n    } else {\n      new StructType()\n    }\n    val cols = schema.map(field => (field.name, field.dataType))\n    if (tableName != null) {\n      builder.tableName(tableName)\n    }\n    cols.foreach(col => {\n      val (colName, dataType) = col\n      val nullable = !notNullColumns.contains(colName)\n      var columnBuilder = io.delta.tables.DeltaTable.columnBuilder(spark, colName)\n      columnBuilder.dataType(dataType.sql)\n      columnBuilder.nullable(nullable)\n      if (generatedColumns.contains(colName)) {\n        columnBuilder.generatedAlwaysAs(generatedColumns(colName))\n      }\n      if (comments.contains(colName)) {\n        columnBuilder.comment(comments(colName))\n      }\n      builder.addColumn(columnBuilder.build())\n    })\n    if (partitionColumns.nonEmpty) {\n      builder.partitionedBy(partitionColumns: _*)\n    }\n    if (path.nonEmpty) {\n      builder.location(path.get)\n    }\n    properties.foreach { case (key, value) =>\n      builder.property(key, value)\n    }\n    builder\n  }\n\n  protected def createTable(\n      tableName: String,\n      path: Option[String],\n      schemaString: String,\n      generatedColumns: Map[String, String],\n      partitionColumns: Seq[String],\n      notNullColumns: Set[String] = Set.empty,\n      comments: Map[String, String] = Map.empty,\n      properties: Map[String, String] = Map.empty): Unit = {\n    var tableBuilder = io.delta.tables.DeltaTable.create(spark)\n    buildTable(tableBuilder, tableName, path, schemaString,\n      generatedColumns, partitionColumns, notNullColumns, comments, properties)\n      .execute()\n  }\n}\n\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/HiveConvertToDeltaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaHiveTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\n\nimport org.apache.spark.sql.{AnalysisException, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{col, from_json}\nimport org.apache.spark.sql.hive.test.TestHiveSingleton\n\nabstract class HiveConvertToDeltaSuiteBase\n  extends ConvertToDeltaHiveTableTests\n  with DeltaSQLTestUtils {\n\n  override protected def convertToDelta(\n      identifier: String,\n      partitionSchema: Option[String] = None, collectStats: Boolean = true): Unit = {\n    if (partitionSchema.isEmpty) {\n      sql(s\"convert to delta $identifier ${collectStatisticsStringOption(collectStats)} \")\n    } else {\n      val stringSchema = partitionSchema.get\n      sql(s\"convert to delta $identifier ${collectStatisticsStringOption(collectStats)}\" +\n        s\" partitioned by ($stringSchema) \")\n    }\n  }\n\n  override protected def verifyExternalCatalogMetadata(tableName: String): Unit = {\n    val catalogTable = spark.sessionState.catalog.externalCatalog.getTable(\"default\", tableName)\n    // Hive automatically adds some properties\n    val cleanProps = catalogTable.properties.filterKeys(_ != \"transient_lastDdlTime\")\n    // We can't alter the schema in the catalog at the moment :(\n    assert(cleanProps.isEmpty,\n      s\"Table properties weren't empty for table $tableName: $cleanProps\")\n  }\n\n  test(\"convert with statistics\") {\n    val tbl = \"hive_parquet\"\n      withTable(tbl) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $tbl (id int, str string)\n             |PARTITIONED BY (part string)\n             |STORED AS PARQUET\n         \"\"\".stripMargin)\n\n        sql(s\"insert into $tbl VALUES (1, 'a', 1)\")\n\n        val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl))\n        convertToDelta(tbl, Some(\"part string\"), collectStats = true)\n        val deltaLog = DeltaLog.forTable(spark, catalogTable)\n        val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select(\n            from_json(col(\"stats\"), deltaLog.unsafeVolatileSnapshot.statsSchema).as(\"stats\"))\n          .select(\"stats.*\")\n        assert(statsDf.filter(col(\"numRecords\").isNull).count == 0)\n        val history = io.delta.tables.DeltaTable.forPath(catalogTable.location.getPath).history()\n        assert(history.count == 1)\n      }\n  }\n\n  test(\"convert without statistics\") {\n    val tbl = \"hive_parquet\"\n    withTable(tbl) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tbl (id int, str string)\n           |PARTITIONED BY (part string)\n           |STORED AS PARQUET\n         \"\"\".stripMargin)\n\n      sql(s\"insert into $tbl VALUES (1, 'a', 1)\")\n\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl))\n      convertToDelta(tbl, Some(\"part string\"), collectStats = false)\n      val deltaLog = DeltaLog.forTable(spark, catalogTable)\n      val statsDf = deltaLog.unsafeVolatileSnapshot.allFiles\n        .select(from_json(col(\"stats\"), deltaLog.unsafeVolatileSnapshot.statsSchema).as(\"stats\"))\n        .select(\"stats.*\")\n      assert(statsDf.filter(col(\"numRecords\").isNotNull).count == 0)\n      val history = io.delta.tables.DeltaTable.forPath(catalogTable.location.getPath).history()\n      assert(history.count == 1)\n\n    }\n  }\n\n  test(\"convert a Hive based parquet table\") {\n    val tbl = \"hive_parquet\"\n    withTable(tbl) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tbl (id int, str string)\n           |PARTITIONED BY (part string)\n           |STORED AS PARQUET\n         \"\"\".stripMargin)\n\n      sql(s\"insert into $tbl VALUES (1, 'a', 1)\")\n\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl))\n      assert(catalogTable.provider === Some(\"hive\"))\n      assert(catalogTable.storage.serde.exists(_.contains(\"parquet\")))\n\n      convertToDelta(tbl, Some(\"part string\"))\n\n      checkAnswer(\n        sql(s\"select * from delta.`${getPathForTableName(tbl)}`\"),\n        Row(1, \"a\", \"1\"))\n\n      verifyExternalCatalogMetadata(tbl)\n      val updatedTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl))\n      assert(updatedTable.provider === Some(\"delta\"))\n    }\n  }\n\n  test(\"convert a Hive based external parquet table\") {\n    val tbl = \"hive_parquet\"\n    withTempDir { dir =>\n      withTable(tbl) {\n        sql(\n          s\"\"\"\n             |CREATE EXTERNAL TABLE $tbl (id int, str string)\n             |PARTITIONED BY (part string)\n             |STORED AS PARQUET\n             |LOCATION '${dir.getCanonicalPath}'\n         \"\"\".stripMargin)\n        sql(s\"insert into $tbl VALUES (1, 'a', 1)\")\n\n        val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl))\n        assert(catalogTable.provider === Some(\"hive\"))\n        assert(catalogTable.storage.serde.exists(_.contains(\"parquet\")))\n\n        convertToDelta(tbl, Some(\"part string\"))\n\n        checkAnswer(\n          sql(s\"select * from delta.`${dir.getCanonicalPath}`\"),\n          Row(1, \"a\", \"1\"))\n\n        verifyExternalCatalogMetadata(tbl)\n        val updatedTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tbl))\n        assert(updatedTable.provider === Some(\"delta\"))\n      }\n    }\n  }\n\n  test(\"negative case: convert empty partitioned parquet table\") {\n    val tbl = \"hive_parquet\"\n    withTempDir { dir =>\n      withTable(tbl) {\n        sql(\n          s\"\"\"\n             |CREATE EXTERNAL TABLE $tbl (id int, str string)\n             |PARTITIONED BY (part string)\n             |STORED AS PARQUET\n             |LOCATION '${dir.getCanonicalPath}'\n         \"\"\".stripMargin)\n\n        val ae = intercept[AnalysisException] {\n          convertToDelta(tbl, Some(\"part string\"))\n        }\n\n        assert(ae.getErrorClass == \"DELTA_CONVERSION_NO_PARTITION_FOUND\")\n        assert(ae.getSqlState == \"42KD6\")\n        assert(ae.getMessage.contains(tbl))\n      }\n    }\n  }\n}\n\nclass HiveConvertToDeltaSuite extends HiveConvertToDeltaSuiteBase with DeltaHiveTest\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/HiveDeltaDDLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaHiveTest\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.hive.test.TestHiveSingleton\n\nabstract class HiveDeltaDDLSuiteBase\n  extends DeltaDDLTestBase {\n  import testImplicits._\n\n  override protected def verifyNullabilityFailure(exception: AnalysisException): Unit = {\n    exception.getMessage.contains(\"not supported for changing column\")\n  }\n\n}\n\nclass HiveDeltaDDLSuite extends HiveDeltaDDLSuiteBase with DeltaHiveTest\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/HiveDeltaNotSupportedDDLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.DeltaHiveTest\n\nimport org.apache.spark.sql.hive.test.TestHiveSingleton\n\nclass HiveDeltaNotSupportedDDLSuite extends DeltaNotSupportedDDLBase with DeltaHiveTest\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IcebergCompatUtilsBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\n/**\n * The shared utils base to be extended by corresponding suites/traits.\n */\ntrait IcebergCompatUtilsBase extends QueryTest {\n  override protected def spark: SparkSession\n\n  protected val compatObject: IcebergCompatBase = null\n\n  protected def compatVersion: Int = Option(compatObject).map(_.version.toInt).getOrElse(-1)\n\n  protected def enableCompatTableProperty: String = compatObject.config.key\n\n  protected val compatColumnMappingMode: String = \"name\"\n\n  protected def compatTableFeature: TableFeature = compatObject.tableFeature\n\n  protected val allReaderWriterVersions: Seq[(Int, Int)] = (1 to 3)\n    .flatMap { r => (1 to 7).map(w => (r, w)) }\n    // can only be at minReaderVersion >= 3 if minWriterVersion is >= 7\n    .filterNot { case (r, w) => w < 7 && r >= 3 }\n\n  protected val defaultSchemaName: String = \"default\"\n\n  protected val defaultCatalogName: String = \"main\"\n\n  def getRndTableId: TableIdentifier = {\n    val rndTableName = s\"testTable${UUID.randomUUID()}\"\n    TableIdentifier(rndTableName, Some(defaultSchemaName), Some(defaultCatalogName))\n  }\n\n  /**\n   * Executes `f` with params (tableId, tempPath).\n   *\n   * We want to use a temp directory in addition to a unique temp table so that when the async\n   * iceberg conversion runs and completes, the parent folder is still removed.\n   */\n  protected def withTempTableAndDir(f: (String, String) => Unit): Unit\n\n  protected def executeSql(sqlStr: String): DataFrame\n\n  protected def getProperties(tableId: String): Map[String, String] = {\n    val table = DeltaTableV2(spark, TableIdentifier(tableId))\n    table.update.getProperties.toMap\n  }\n\n  protected def assertIcebergCompatProtocolAndProperties(\n      tableId: String,\n      compatObj: IcebergCompatBase = compatObject): Unit = {\n    val table = DeltaTableV2(spark, TableIdentifier(tableId))\n    val snapshot = table.update\n    val protocol = snapshot.protocol\n    val tblProperties = snapshot.getProperties\n    val tableFeature = compatObj.tableFeature\n\n    val expectedMinReaderVersion = Math.max(\n      ColumnMappingTableFeature.minReaderVersion,\n      tableFeature.minReaderVersion\n    )\n\n    val expectedMinWriterVersion = Math.max(\n      ColumnMappingTableFeature.minWriterVersion,\n      tableFeature.minWriterVersion\n    )\n\n    assert(protocol.minReaderVersion >= expectedMinReaderVersion)\n    assert(protocol.minWriterVersion >= expectedMinWriterVersion)\n    assert(protocol.writerFeatures.get.contains(tableFeature.name))\n    assert(tblProperties(compatObj.config.key) === \"true\")\n    assert(Seq(\"name\", \"id\").contains(tblProperties(\"delta.columnMapping.mode\")))\n  }\n\n  protected def parseIcebergVersion(metadataLocation: String): Int = {\n    val versionStart = metadataLocation.lastIndexOf('/') + 1\n    val versionEnd = metadataLocation.indexOf('-', versionStart)\n    if (versionEnd < 0) throw new RuntimeException(\n      s\"No version end found in $metadataLocation: $versionEnd\")\n    Integer.valueOf(metadataLocation.substring(versionStart, versionEnd))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnAdmissionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, FileNotFoundException, PrintWriter}\n\nimport org.apache.spark.sql.delta.GeneratedAsIdentityType.GeneratedAlways\nimport org.apache.spark.sql.delta.actions.RemoveFile\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.streaming.{StreamingQueryException, Trigger}\nimport org.apache.spark.sql.types.{DoubleType, IntegerType, LongType}\nimport org.apache.spark.util.Utils\n\n// Test command that should be allowed and disallowed on IDENTITY columns.\ntrait IdentityColumnAdmissionSuiteBase\n  extends IdentityColumnTestUtils {\n\n  import testImplicits._\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaSQLConf.DELTA_IDENTITY_COLUMN_ENABLED.key, \"true\")\n  }\n\n  test(\"alter table change column type\") {\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      keyword <- Seq(\"ALTER\", \"CHANGE\")\n      targetType <- Seq(IntegerType, DoubleType)\n    } {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        targetType match {\n          case IntegerType =>\n            // Long -> Integer (downcast) is rejected early during analysis by Spark.\n            val ex = intercept[AnalysisException] {\n              sql(s\"ALTER TABLE $tblName $keyword COLUMN id TYPE ${targetType.sql}\")\n            }\n            assert(ex.getErrorClass === \"NOT_SUPPORTED_CHANGE_COLUMN\")\n          case DoubleType =>\n            // Long -> Double (upcast) is rejected in Delta when altering data type of an\n            // identity column.\n            val ex = intercept[DeltaAnalysisException] {\n              sql(s\"ALTER TABLE $tblName $keyword COLUMN id TYPE ${targetType.sql}\")\n            }\n            assert(ex.getErrorClass === \"DELTA_IDENTITY_COLUMNS_ALTER_COLUMN_NOT_SUPPORTED\")\n          case _ => fail(\"unexpected targetType\")\n        }\n      }\n    }\n  }\n\n  test(\"alter table change column comment\") {\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      keyword <- Seq(\"ALTER\", \"CHANGE\")\n    } {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        sql(s\"ALTER TABLE $tblName $keyword COLUMN id COMMENT 'comment'\")\n      }\n    }\n  }\n\n  test(\"identity columns can be renamed\") {\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n    } {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        sql(s\"ALTER TABLE $tblName SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n        sql(s\"INSERT INTO $tblName (value) VALUES (1)\")\n        sql(s\"INSERT INTO $tblName (value) VALUES (2)\")\n        checkAnswer(sql(s\"SELECT id, value FROM $tblName\"), Seq(Row(1, 1), Row(2, 2)))\n\n        sql(s\"ALTER TABLE $tblName RENAME COLUMN id TO id2\")\n        sql(s\"INSERT INTO $tblName (value) VALUES (0)\")\n        checkAnswer(sql(s\"SELECT id2, value FROM $tblName\"), Seq(Row(1, 1), Row(2, 2), Row(3, 0)))\n      }\n    }\n  }\n\n  test(\"cannot set default value for identity column\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        val ex = intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tblName ALTER COLUMN id SET DEFAULT 1\")\n        }\n        assert(ex.getMessage.contains(\"ALTER TABLE ALTER COLUMN is not supported\"))\n      }\n    }\n  }\n\n  test(\"position of identity column can be moved\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        sql(s\"ALTER TABLE $tblName ALTER COLUMN id AFTER value\")\n        sql(s\"INSERT INTO $tblName (value) VALUES (1)\")\n        sql(s\"INSERT INTO $tblName (value) VALUES (2)\")\n        checkAnswer(sql(s\"SELECT id, value FROM $tblName\"), Seq(Row(1, 1), Row(2, 2)))\n\n        sql(s\"ALTER TABLE $tblName ALTER COLUMN id FIRST\")\n        sql(s\"INSERT INTO $tblName (value) VALUES (3)\")\n        checkAnswer(sql(s\"SELECT id, value FROM $tblName\"), Seq(Row(1, 1), Row(2, 2), Row(3, 3)))\n      }\n    }\n  }\n\n  test(\"alter table replace columns\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        val ex = intercept[DeltaAnalysisException] {\n          sql(s\"ALTER TABLE $tblName REPLACE COLUMNS (id BIGINT, value INT)\")\n        }\n        assert(ex.getMessage.contains(\"ALTER TABLE REPLACE COLUMNS is not supported\"))\n      }\n    }\n  }\n\n  test(\"create table partitioned by identity column\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        val ex1 = intercept[DeltaAnalysisException] {\n          createTable(\n            tblName,\n            Seq(\n              IdentityColumnSpec(generatedAsIdentityType),\n              TestColumnSpec(\"value1\", dataType = IntegerType),\n              TestColumnSpec(\"value2\", dataType = DoubleType)\n            ),\n            partitionedBy = Seq(\"id\")\n          )\n        }\n        assert(ex1.getMessage.contains(\"PARTITIONED BY IDENTITY column\"))\n        val ex2 = intercept[DeltaAnalysisException] {\n          createTable(\n            tblName,\n            Seq(\n              IdentityColumnSpec(generatedAsIdentityType),\n              TestColumnSpec(\"value1\", dataType = IntegerType),\n              TestColumnSpec(\"value2\", dataType = DoubleType)\n            ),\n            partitionedBy = Seq(\"id\", \"value1\")\n          )\n        }\n        assert(ex2.getMessage.contains(\"PARTITIONED BY IDENTITY column\"))\n      }\n    }\n  }\n\n  test(\"replace with table partitioned by identity column\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        // First create a table with no identity column and no partitions.\n        createTable(\n          tblName,\n          Seq(\n            TestColumnSpec(\"id\", dataType = LongType),\n            TestColumnSpec(\"value1\", dataType = IntegerType),\n            TestColumnSpec(\"value2\", dataType = DoubleType)\n          )\n        )\n        // CREATE OR REPLACE should not allow a table using identity column with partition.\n        val ex1 = intercept[DeltaAnalysisException] {\n          createOrReplaceTable(\n            tblName,\n            Seq(\n              IdentityColumnSpec(generatedAsIdentityType),\n              TestColumnSpec(\"value1\", dataType = IntegerType),\n              TestColumnSpec(\"value2\", dataType = DoubleType)\n            ),\n            partitionedBy = Seq(\"id\")\n          )\n        }\n        assert(ex1.getMessage.contains(\"PARTITIONED BY IDENTITY column\"))\n        // REPLACE should also not allow a table using identity column as partition.\n        val ex2 = intercept[DeltaAnalysisException] {\n          replaceTable(\n            tblName,\n            Seq(\n              IdentityColumnSpec(generatedAsIdentityType),\n              TestColumnSpec(\"value1\", dataType = IntegerType),\n              TestColumnSpec(\"value2\", dataType = DoubleType)\n            ),\n            partitionedBy = Seq(\"id\", \"value1\")\n          )\n        }\n        assert(ex2.getMessage.contains(\"PARTITIONED BY IDENTITY column\"))\n      }\n    }\n  }\n\n  test(\"CTAS does not inherit IDENTITY column\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      val ctasTblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        withTable(ctasTblName) {\n          sql(s\"INSERT INTO $tblName (value) VALUES (1), (2)\")\n          sql(\n            s\"\"\"\n               |CREATE TABLE $ctasTblName USING delta AS SELECT * FROM $tblName\n               |\"\"\".stripMargin)\n          val dl = DeltaLog.forTable(spark, TableIdentifier(ctasTblName))\n          assert(!dl.snapshot.metadata.schemaString.contains(DeltaSourceUtils.IDENTITY_INFO_START))\n        }\n      }\n    }\n  }\n\n  test(\"insert generated always as\") {\n    val tblName = getRandomTableName\n    withIdentityColumnTable(GeneratedAlways, tblName) {\n      // Test SQLs.\n      val blockedStmts = Seq(\n        s\"INSERT INTO $tblName VALUES (1,1)\",\n        s\"INSERT INTO $tblName (value, id) VALUES (1,1)\",\n        s\"INSERT OVERWRITE $tblName VALUES (1,1)\",\n        s\"INSERT OVERWRITE $tblName (value, id) VALUES (1,1)\"\n      )\n      for (stmt <- blockedStmts) {\n        val ex = intercept[DeltaAnalysisException](sql(stmt))\n        assert(ex.getMessage.contains(\"Providing values for GENERATED ALWAYS AS IDENTITY\"))\n      }\n\n      // Test DataFrame V1 and V2 API.\n      val df = (1 to 10).map(v => (v.toLong, v)).toDF(\"id\", \"value\")\n\n      val path = DeltaLog.forTable(spark, TableIdentifier(tblName)).dataPath.toString\n      val exV1 =\n        intercept[DeltaAnalysisException](df.write.format(\"delta\").mode(\"append\").save(path))\n      assert(exV1.getMessage.contains(\"Providing values for GENERATED ALWAYS AS IDENTITY\"))\n\n      val exV2 = intercept[DeltaAnalysisException](df.writeTo(tblName).append())\n      assert(exV2.getMessage.contains(\"Providing values for GENERATED ALWAYS AS IDENTITY\"))\n\n    }\n  }\n\n  test(\"streaming\") {\n    val tblName = getRandomTableName\n    withIdentityColumnTable(GeneratedAlways, tblName) {\n      val path = DeltaLog.forTable(spark, TableIdentifier(tblName)).dataPath.toString\n      withTempDir { checkpointDir =>\n        val ex = intercept[StreamingQueryException] {\n          val stream = MemoryStream[Int]\n          val q = stream\n            .toDF\n            .map(_ => Tuple2(1L, 1))\n            .toDF(\"id\", \"value\")\n            .writeStream\n            .format(\"delta\")\n            .outputMode(\"append\")\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .trigger(Trigger.AvailableNow)\n            .start(path)\n          stream.addData(1 to 10)\n          q.processAllAvailable()\n          q.stop()\n        }\n        assert(ex.getMessage.contains(\"Providing values for GENERATED ALWAYS AS IDENTITY\"))\n      }\n    }\n  }\n\n  test(\"update\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        sql(s\"INSERT INTO $tblName (value) VALUES (1), (2)\")\n\n        val blockedStatements = Seq(\n          // Unconditional UPDATE.\n          s\"UPDATE $tblName SET id = 1\",\n          // Conditional UPDATE.\n          s\"UPDATE $tblName SET id = 1 WHERE value = 2\"\n        )\n        for (stmt <- blockedStatements) {\n          val ex = intercept[DeltaAnalysisException](sql(stmt))\n          assert(ex.getMessage.contains(\"UPDATE on IDENTITY column\"))\n        }\n      }\n    }\n  }\n\n  test(\"merge\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val source = s\"${getRandomTableName}_source\"\n      val target = s\"${getRandomTableName}_target\"\n      withIdentityColumnTable(generatedAsIdentityType, target) {\n        withTable(source) {\n          sql(\n            s\"\"\"\n               |CREATE TABLE $source (\n               |  value INT,\n               |  id BIGINT\n               |) USING delta\n               |\"\"\".stripMargin)\n          sql(\n            s\"\"\"\n               |INSERT INTO $source VALUES (1, 100), (2, 200), (3, 300)\n               |\"\"\".stripMargin)\n          sql(\n            s\"\"\"\n               |INSERT INTO $target(value) VALUES (2), (3), (4)\n               |\"\"\".stripMargin)\n\n          val updateStmt =\n            s\"\"\"\n               |MERGE INTO $target\n               |  USING $source on $target.value = $source.value\n               |  WHEN MATCHED THEN UPDATE SET *\n               |\"\"\".stripMargin\n          val updateEx = intercept[DeltaAnalysisException](sql(updateStmt))\n          assert(updateEx.getMessage.contains(\"UPDATE on IDENTITY column\"))\n\n          val insertStmt =\n            s\"\"\"\n               |MERGE INTO $target\n               |  USING $source on $target.value = $source.value\n               |  WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin\n\n          if (generatedAsIdentityType == GeneratedAlways) {\n            val insertEx = intercept[DeltaAnalysisException](sql(insertStmt))\n            assert(\n              insertEx.getMessage.contains(\"Providing values for GENERATED ALWAYS AS IDENTITY\"))\n          } else {\n            sql(insertStmt)\n          }\n        }\n      }\n    }\n  }\n}\n\nclass IdentityColumnAdmissionScalaSuite\n  extends IdentityColumnAdmissionSuiteBase\n  with ScalaDDLTestUtils\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnConflictSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.concurrent.duration.Duration\nimport scala.util.control.NonFatal\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.concurrency.{PhaseLockingTestMixin, TransactionExecutionTestMixin}\nimport org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.execution.QueryExecution\nimport org.apache.spark.util.ThreadUtils\n\n/**\n * Helper class used in this test suite for describing different transaction conflict scenarios.\n */\nsealed trait TransactionConflictTestCase {\n  /** label for this transaction scenario. */\n  def name: String\n  /** SQL command to be executed. */\n  def sqlCommand: String\n  /** Boolean indicating whether this transaction does a metadata update. */\n  def hasMetadataUpdate: Boolean\n  /** Boolean indicating whether the SQL command appends data (add files) to the table. */\n  def isAppend: Boolean\n}\n\ncase class NoMetadataUpdateTestCase(\n    name: String,\n    sqlCommand: String,\n    isAppend: Boolean) extends TransactionConflictTestCase {\n  val hasMetadataUpdate = false\n}\n\n/**\n * A transaction that will do a metadata update but will not be tagged as identity column only nor\n * row tracking enablement only.\n */\ncase class GenericMetadataUpdateTestCase(\n    name: String,\n    sqlCommand: String,\n    isAppend: Boolean) extends TransactionConflictTestCase {\n  val hasMetadataUpdate = true\n}\n\n/** A transaction that will be tagged as a metadata update only for identity column. */\ncase class IdentityOnlyMetadataUpdateTestCase(\n    name: String,\n    sqlCommand: String,\n    isAppend: Boolean) extends TransactionConflictTestCase {\n  val hasMetadataUpdate = true\n}\n\n/** A transaction that will be tagged as a metadata update only for row tracking enablement. */\ncase class RowTrackingEnablementOnlyTestCase(\n    name: String,\n    sqlCommand: String,\n    isAppend: Boolean) extends TransactionConflictTestCase {\n  val hasMetadataUpdate = true\n}\n\ntrait IdentityColumnConflictSuiteBase\n    extends IdentityColumnTestUtils\n    with TransactionExecutionTestMixin\n    with PhaseLockingTestMixin {\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key, \"true\")\n    .set(DeltaSQLConf.FEATURE_ENABLEMENT_CONFLICT_RESOLUTION_ENABLED.key, \"true\")\n\n  val colName = \"id\"\n\n  private def setupEmptyTableWithRowTrackingTableFeature(\n      tblIsoLevel: Option[IsolationLevel], tblName: String): Unit = {\n    val tblPropertiesMap: Map[String, String] = Map(\n      TableFeatureProtocolUtils.propertyKey(RowTrackingFeature) -> \"supported\",\n      DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"false\",\n      DeltaConfigs.ISOLATION_LEVEL.key ->\n        tblIsoLevel.map(_.toString).getOrElse(DeltaConfigs.ISOLATION_LEVEL.defaultValue)\n    )\n\n    createTableWithIdColAndIntValueCol(\n      tableName = tblName,\n      generatedAsIdentityType = GeneratedAsIdentityType.GeneratedByDefault,\n      startsWith = Some(1),\n      incrementBy = Some(1),\n      tblProperties = tblPropertiesMap\n    )\n  }\n\n  /**\n   * Returns the expected exception class for the test case.\n   * Returns None if no exception is expected.\n   */\n  protected def expectedExceptionClass(\n      currentTxn: TransactionConflictTestCase,\n      winningTxn: TransactionConflictTestCase): Option[Class[_ <: RuntimeException]] = {\n    val currentTxnShouldAbortDueToMetadataUpdate = winningTxn match {\n      case _: NoMetadataUpdateTestCase => false\n      case _: IdentityOnlyMetadataUpdateTestCase if !currentTxn.hasMetadataUpdate => false\n      case _: RowTrackingEnablementOnlyTestCase if !currentTxn.hasMetadataUpdate => false\n      case _ => true\n    }\n\n    // Metadata update is checked before concurrent append in ConflictChecker.\n    if (currentTxnShouldAbortDueToMetadataUpdate) {\n      return Some(classOf[io.delta.exceptions.MetadataChangedException])\n    }\n\n    val currentTxnShouldAbortDueToConcurrentAppend = winningTxn.isAppend &&\n      currentTxn.isInstanceOf[IdentityOnlyMetadataUpdateTestCase] && !currentTxn.isAppend\n\n    if (currentTxnShouldAbortDueToConcurrentAppend) {\n      return Some(classOf[io.delta.exceptions.ConcurrentAppendException])\n    }\n\n    None\n  }\n\n  /** Executes the winning transaction SQL. Overridable for custom RPC assertions. */\n  protected def sqlWithTotalRpcBound(sqlText: String): Unit = sql(sqlText)\n\n  /**\n   * Helper function to test two concurrently running commands. Winning transaction commits before\n   * current transaction commits.\n   */\n  protected def transactionIdentityConflictHelper(\n      currentTxn: TransactionConflictTestCase,\n      winningTxn: TransactionConflictTestCase,\n      tblIsoLevel: Option[IsolationLevel]): Unit = {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      // We start with an empty table that has row tracking table feature support and row tracking\n      // table property disabled. This way, when we set the table property to true, it will not\n      // also do a protocol upgrade and we don't need any backfill commit.\n      setupEmptyTableWithRowTrackingTableFeature(tblIsoLevel, tblName)\n\n      val threadPool =\n        ThreadUtils.newDaemonSingleThreadExecutor(threadName = \"identity-column-thread-pool\")\n      val (txnObserver, future) = runQueryWithObserver(\n        name = \"current\", threadPool, currentTxn.sqlCommand.replace(\"{tblName}\", tblName))\n\n      unblockUntilPreCommit(txnObserver)\n      busyWaitFor(txnObserver.phases.preparePhase.hasEntered, timeout)\n\n      sqlWithTotalRpcBound(winningTxn.sqlCommand.replace(\"{tblName}\", tblName))\n\n      val expectedException = expectedExceptionClass(currentTxn, winningTxn)\n      val events = Log4jUsageLogger.track {\n        try {\n          unblockCommit(txnObserver)\n          ThreadUtils.awaitResult(future, Duration.Inf)\n          assert(expectedException.isEmpty, \"Expected txn to fail, but no exception was thrown\")\n        } catch {\n          case NonFatal(e) => expectedException match {\n            case None => fail(\"Expecting no exception, but an exception was thrown\", e)\n            case Some(expected) if (e.getCause == null) || e.getCause.getClass != expected =>\n              fail(s\"Expected exception of type ${expected.getName}, \" +\n                \"but got a different exception\", e)\n            case Some(_) => // Expected exception was thrown, test passes.\n          }\n        }\n      }\n\n      // We should log if the txn is aborted due to identity column only metadata update.\n      if (currentTxn.hasMetadataUpdate\n          && winningTxn.isInstanceOf[IdentityOnlyMetadataUpdateTestCase]) {\n        val identityColumnAbortEvents = events\n            .filter(_.tags.get(\"opType\").contains(IdentityColumn.opTypeAbort))\n        assert(identityColumnAbortEvents.size === 1)\n      }\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  /**\n   * We are testing the following combinations (see [[ConflictChecker.checkNoMetadataUpdates]]\n   * for details).\n   *\n   * |                                               | Winning Metadata (id) | Winning Metadata Row Tracking Enablement Only | Winning Metadata (other) | Winning No Metadata |\n   * | --------------------------------------------- | --------------------- | --------------------------------------------- | ------------------------ | ------------------- |\n   * | Current Metadata (id)                         | Conflict              | Conflict                                      | Conflict                 | No conflict         |\n   * | Current Metadata Row Tracking Enablement Only | Conflict              | Conflict                                      | Conflict                 | No conflict         |\n   * | Current Metadata (other)                      | Conflict              | Conflict                                      | Conflict                 | No conflict         |\n   * | Current No Metadata                           | No conflict           | No conflict                                   | Conflict                 | No conflict         |\n   */\n  // scalastyle:on line.size.limit\n\n  // System generated IDENTITY value will have a metadata update for IDENTITY high water marks.\n  private val generatedIdTestCase = IdentityOnlyMetadataUpdateTestCase(\n    name = \"generatedId\",\n    sqlCommand = s\"INSERT INTO {tblName}(value) VALUES (1)\",\n    isAppend = true\n  )\n\n  // SYNC IDENTITY updates the high water mark based on the values in the IDENTITY column.\n  private val syncIdentityTestCase = IdentityOnlyMetadataUpdateTestCase(\n    name = \"syncIdentity\",\n    sqlCommand = s\"ALTER TABLE {tblName} ALTER COLUMN $colName SYNC IDENTITY\",\n    isAppend = false\n  )\n\n  // Explicitly provided IDENTITY value will not generate a metadata update.\n  private val noMetadataUpdateTestCase = NoMetadataUpdateTestCase(\n    name = \"noMetadataUpdate\",\n    sqlCommand = s\"INSERT INTO {tblName} VALUES (1, 1)\",\n    isAppend = true\n  )\n\n  private val rowTrackingEnablementTestCase = RowTrackingEnablementOnlyTestCase(\n    name = \"rowTrackingEnablement\",\n    sqlCommand =\n      s\"\"\"ALTER TABLE {tblName}\n         |SET TBLPROPERTIES(\n         |'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true'\n         |)\"\"\".stripMargin,\n    isAppend = false\n  )\n\n  private val otherMetadataUpdateTestCase = GenericMetadataUpdateTestCase(\n    name = \"otherMetadataUpdate\",\n    sqlCommand = s\"ALTER TABLE {tblName} ADD COLUMN value2 STRING\",\n    isAppend = false\n  )\n\n  protected val conflictTestCases: Seq[TransactionConflictTestCase] = Seq(\n    generatedIdTestCase,\n    syncIdentityTestCase,\n    noMetadataUpdateTestCase,\n    rowTrackingEnablementTestCase,\n    otherMetadataUpdateTestCase\n  )\n\n  for {\n    currentTxn <- conflictTestCases\n    winningTxn <- conflictTestCases\n  } {\n    val testName =\n      s\"identity conflict test: [currentTxn: ${currentTxn.name}, winningTxn: ${winningTxn.name}]\"\n    test(testName) {\n      transactionIdentityConflictHelper(\n        currentTxn,\n        winningTxn,\n        tblIsoLevel = None\n      )\n    }\n  }\n\n  test(\"ALTER TABLE SYNC IDENTITY conflict on serializable table\") {\n    transactionIdentityConflictHelper(\n      syncIdentityTestCase,\n      noMetadataUpdateTestCase,\n      tblIsoLevel = Some(Serializable)\n    )\n  }\n\n  test(\"high watermark changes after analysis but before execution of merge\") {\n    val tblName = getRandomTableName\n    withIdentityColumnTable(GeneratedAsIdentityType.GeneratedAlways, tblName) {\n      // Create a QueryExecution object for a MERGE statement, and it forces the command to be\n      // analyzed, but does not execute the command yet.\n      val parsedMerge = spark.sessionState.sqlParser.parsePlan(\n        s\"\"\"MERGE INTO $tblName t\n           |USING (SELECT * FROM range(1000)) s\n           |ON t.id = s.id\n           |WHEN NOT MATCHED THEN INSERT (value) VALUES (s.id)\"\"\".stripMargin)\n      val qeMerge = new QueryExecution(spark, parsedMerge)\n      qeMerge.analyzed\n\n      // Insert a row, forcing the high watermark to be updated.\n      sql(s\"INSERT INTO $tblName (value) VALUES (0)\")\n\n      // Force merge to be executed. This should fail, as MERGE is still using the old high\n      // watermark in its insert action.\n      intercept[MetadataChangedException] {\n        qeMerge.commandExecuted\n      }\n    }\n  }\n}\n\nclass IdentityColumnConflictScalaSuite\n  extends IdentityColumnConflictSuiteBase\n  with ScalaDDLTestUtils\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnDMLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions}\nimport org.apache.spark.sql.delta.GeneratedAsIdentityType.{GeneratedAlways, GeneratedByDefault}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.merge.MergeStats\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{AnalysisException, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.CodegenObjectFactoryMode\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Identity Column test suite for DML operations, including INSERT REPLACE WHERE.\n */\ntrait IdentityColumnDMLSuiteBase\n  extends IdentityColumnTestUtils {\n\n  import testImplicits._\n\n  test(\"delete\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withIdentityColumnTable(generatedAsIdentityType, tblName) {\n        sql(s\"INSERT INTO $tblName (value) VALUES (1), (2)\")\n        val prevMax = sql(s\"SELECT MAX(id) FROM $tblName\").collect().head.getLong(0)\n        sql(s\"DELETE FROM $tblName WHERE value = 1\")\n        checkAnswer(\n          sql(s\"SELECT COUNT(*) FROM $tblName\"),\n          Row(1L)\n        )\n        sql(s\"DELETE FROM $tblName\")\n        checkAnswer(\n          sql(s\"SELECT COUNT(*) FROM $tblName\"),\n          Row(0L)\n        )\n        sql(s\"INSERT INTO $tblName (value) VALUES (1), (2)\")\n        checkAnswer(\n          sql(s\"SELECT COUNT(*) FROM $tblName where id <= $prevMax\"),\n          Row(0L)\n        )\n      }\n    }\n  }\n\n  test(\"merge with insert and update\") {\n    val start = 1L\n    val step = 2L\n    withSQLConf(\n        DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key -> \"false\") {\n      val source = s\"${getRandomTableName}_src\"\n      val target = s\"${getRandomTableName}_tgt\"\n      withTable(source, target) {\n        var highWaterMark = start - step\n        createTable(\n          source,\n          Seq(\n            TestColumnSpec(colName = \"value\", dataType = IntegerType),\n            TestColumnSpec(colName = \"value2\", dataType = IntegerType)\n          )\n        )\n        sql(\n          s\"\"\"\n             |INSERT INTO $source VALUES (1, 100), (2, 200), (3, 300)\n             |\"\"\".stripMargin)\n        createTableWithIdColAndIntValueCol(\n          target, GeneratedAlways, startsWith = Some(start), incrementBy = Some(step))\n        sql(\n          s\"\"\"\n             |INSERT INTO $target(value) VALUES (2), (3), (4)\n             |\"\"\".stripMargin)\n        highWaterMark = validateIdentity(target, 3, start, step, 2, 4, highWaterMark)\n        val idBeforeMerge1 = sql(s\"SELECT id FROM $target WHERE value in (2, 3)\").collect()\n\n        sql(\n          s\"\"\"\n             |MERGE INTO $target\n             |  USING $source on $target.value = $source.value\n             |  WHEN MATCHED THEN UPDATE SET $target.value = $source.value2\n             |  WHEN NOT MATCHED THEN INSERT (value) VALUES ($source.value2)\n             |\"\"\".stripMargin)\n        highWaterMark = validateIdentity(target, 4, start, step, 100, 100, highWaterMark)\n        // IDENTITY values for updated rows shouldn't change.\n        checkAnswer(\n          sql(s\"SELECT id FROM $target WHERE value in (200, 300)\"),\n          idBeforeMerge1\n        )\n        val idBeforeMerge2 =\n          sql(s\"SELECT id FROM $target WHERE value in (100, 300, 4)\").collect()\n\n        sql(s\"INSERT OVERWRITE $source VALUES(200, 2000), (4, 400), (5, 500)\")\n        sql(\n          s\"\"\"\n             |MERGE INTO $target\n             |  USING $source on $target.value = $source.value\n             |  WHEN MATCHED AND $source.value = 200 THEN DELETE\n             |  WHEN MATCHED THEN UPDATE SET $target.value = $source.value2\n             |  WHEN NOT MATCHED THEN INSERT (value) VALUES ($source.value2)\n             |\"\"\".stripMargin)\n        highWaterMark = validateIdentity(target, 4, start, step, 500, 500, highWaterMark)\n        // IDENTITY values for updated rows shouldn't change.\n        checkAnswer(\n          sql(s\"SELECT id FROM $target WHERE value in (100, 300, 400)\"),\n          idBeforeMerge2\n        )\n      }\n    }\n  }\n\n  test(\"merge with insert and update and schema evolution\") {\n    val start = 1L\n    val step = 3L\n\n    withSQLConf(\n        \"spark.databricks.delta.schema.autoMerge.enabled\"-> \"true\",\n        DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key -> \"false\") {\n      val source = s\"${getRandomTableName}_src\"\n      val target = s\"${getRandomTableName}_tgt\"\n      withTable(source, target) {\n        var highWaterMark = start - step\n        createTable(\n          source,\n          Seq(\n            TestColumnSpec(colName = \"id2\", dataType = LongType),\n            TestColumnSpec(colName = \"value2\", dataType = IntegerType)\n          )\n        )\n        sql(s\"INSERT INTO $source VALUES (4, 44), (9, 99)\")\n\n        createTable(\n          target,\n          Seq(\n            IdentityColumnSpec(\n              GeneratedAlways,\n              startsWith = Some(start),\n              incrementBy = Some(step)\n            ),\n            TestColumnSpec(colName = \"id2\", dataType = LongType),\n            TestColumnSpec(colName = \"value\", dataType = IntegerType)\n          )\n        )\n        sql(s\"INSERT INTO $target (id2, value) VALUES(1, 1), (4, 4), (7, 7), (10, 10)\")\n        highWaterMark = validateIdentity(target, 4, start, step, 1, 10, highWaterMark)\n\n        val idBeforeMerge1 = sql(s\"SELECT id FROM $target WHERE id2 in (1, 4, 7, 10)\")\n        sql(\n          s\"\"\"\n             |MERGE INTO $target\n             |  USING $source on $target.id2 = $source.id2\n             |  WHEN NOT MATCHED THEN INSERT *\n             |\"\"\".stripMargin)\n        checkAnswer(\n          sql(s\"SELECT id FROM $target WHERE id2 in (1, 4, 7, 10)\"),\n          idBeforeMerge1\n        )\n        checkAnswer(\n          sql(s\"SELECT COUNT(DISTINCT id) == COUNT(*) FROM $target\"),\n          Row(true)\n        )\n\n        val idBeforeMerge2 = sql(s\"SELECT id FROM $target WHERE id2 in (1, 4, 7, 9, 10)\")\n        sql(s\"INSERT OVERWRITE $source VALUES(9, 999), (11, 1100)\")\n        val events = Log4jUsageLogger.track {\n          sql(\n            s\"\"\"\n               |MERGE INTO $target\n               |  USING $source on $target.id2 = $source.id2\n               |  WHEN MATCHED THEN UPDATE SET $target.value = $source.value2\n               |  WHEN NOT MATCHED THEN INSERT *\n               |\"\"\".stripMargin)\n        }\n        checkAnswer(\n          sql(s\"SELECT id FROM $target WHERE id2 in (1, 4, 7, 9, 10)\"),\n          idBeforeMerge2\n        )\n        checkAnswer(\n          sql(s\"SELECT COUNT(DISTINCT id) == COUNT(*) FROM $target\"),\n          Row(true)\n        )\n        val mergeStats = events.filter { e =>\n          e.metric == MetricDefinitions.EVENT_TAHOE.name &&\n            e.tags.get(\"opType\").contains(\"delta.dml.merge.stats\")\n        }\n        assert(mergeStats.size == 1)\n      }\n    }\n  }\n\n  test(\"MERGE/UPDATE/DELETE which does not INSERT any new data but just touches old rows\" +\n    \" should not change the HIGH WATERMARK\") {\n    for {\n      increment <- Seq(1, -1)\n    } {\n      withSQLConf(\n          DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key -> \"false\") {\n        val src = s\"${getRandomTableName}_src\"\n        val tgt = s\"${getRandomTableName}_tgt\"\n        withTable(src, tgt) {\n          sql(s\"DROP TABLE IF EXISTS $tgt\")\n          createTable(\n            tgt,\n            Seq(\n              IdentityColumnSpec(\n                GeneratedAlways,\n                startsWith = Some(0),\n                incrementBy = Some(increment)\n              ),\n              TestColumnSpec(colName = \"col1\", dataType = IntegerType)\n            )\n          )\n\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tgt))\n          assert(deltaLog.snapshot.version === 0L)\n\n          // INSERT 10 rows where each row is inserted into different file.\n          (1 to 10)\n            .toDF(\"col1\")\n            .repartition(10)\n            .write.mode(\"overwrite\").format(\"delta\").saveAsTable(tgt)\n          assert(deltaLog.snapshot.version === 1L)\n          assert(highWaterMark(deltaLog.snapshot, \"id\") === increment * 9L)\n\n          // Create src table with only 1 row having value 5.\n          Seq(5).toDF(\"col1\").write.saveAsTable(src)\n          // The MERGE query will just UPDATE only one file that has one row only.\n          sql(\n            s\"\"\"\n               | MERGE INTO $tgt tgt USING $src src\n               | ON src.col1 = tgt.col1\n               | WHEN MATCHED\n               |   THEN UPDATE SET tgt.col1 = 100\n               | WHEN NOT MATCHED THEN INSERT (tgt.col1) VALUES (src.col1)\n               | \"\"\".stripMargin).collect()\n          assert(deltaLog.snapshot.version === 2L)\n          // The MERGE query shouldn't change the high watermark as it has not INSERTED any new\n          // data.\n          assert(highWaterMark(deltaLog.snapshot, \"id\") === increment * 9L)\n\n          // Write 10 more rows to the table using single task and make sure that HIGH WATERMARK is\n          // moved 10 units in either direction.\n          val newDfToWrite = (11 to 20).toDF(\"col1\").repartition(1)\n          newDfToWrite.write.format(\"delta\").mode(\"append\").saveAsTable(tgt)\n          assert(highWaterMark(deltaLog.snapshot, \"id\") === increment * 19L)\n          // validate no duplicate identity values\n          checkAnswer(sql(s\"SELECT COUNT(DISTINCT id) == COUNT(*) FROM $tgt\"), Row(true))\n        }\n      }\n    }\n  }\n\n  // Helper function to test multiple \"WHEN NOT MATCHED THEN INSERT\" clauses in a single MERGE with\n  // different variations - enable/disable WSCG, vary num partitions in the identity column\n  // generation stage.\n  private def testMergeWithMultipleWhenNotMatchedClauses(\n      numPartitions: Int = 2,\n      codegenEnabled: Boolean = true): Unit = {\n    val codegenFactoryMode = if (codegenEnabled) {\n      CodegenObjectFactoryMode.CODEGEN_ONLY\n    } else {\n      CodegenObjectFactoryMode.NO_CODEGEN\n    }\n    withSQLConf(\n        SQLConf.CODEGEN_FACTORY_MODE.key -> codegenFactoryMode.toString,\n        DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key -> \"false\",\n        SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> s\"$codegenEnabled\") {\n      val src = s\"${getRandomTableName}_src\"\n      val tgt = s\"${getRandomTableName}_tgt\"\n      withTable(src, tgt) {\n        (1 to 10)\n          .map(i => (i, i % 2))\n          .toDF(\"col1\", \"col2\")\n          .repartition(numPartitions)\n          .write.mode(\"overwrite\")\n          .saveAsTable(src)\n        sql(s\"DROP TABLE IF EXISTS $tgt\")\n        createTable(\n          tgt,\n          Seq(\n            IdentityColumnSpec(\n              GeneratedAlways,\n              startsWith = Some(5),\n              incrementBy = Some(5),\n              colName = \"id1\"\n            ),\n            IdentityColumnSpec(\n              GeneratedAlways,\n              startsWith = Some(0),\n              incrementBy = Some(3),\n              colName = \"id2\"\n            ),\n            TestColumnSpec(colName = \"col1\", dataType = LongType),\n            TestColumnSpec(colName = \"col2\", dataType = LongType)\n          )\n        )\n        sql(s\"INSERT INTO $tgt (col1, col2) VALUES (5, 100), (6, 101)\")\n\n        sql(\n          s\"\"\"\n             | MERGE INTO $tgt tgt USING $src src\n             | ON src.col1 = tgt.col1\n             | WHEN MATCHED AND tgt.col1 == 5\n             |   THEN UPDATE SET tgt.col2 = src.col2\n             | WHEN NOT MATCHED AND src.col1 % 3 != 0\n             |   THEN INSERT (tgt.col1, tgt.col2) VALUES (src.col1, src.col2)\n             | WHEN NOT MATCHED AND src.col1 % 3 == 0\n             |   THEN INSERT (tgt.col1, tgt.col2) VALUES (src.col1, src.col2)\n             | \"\"\".stripMargin).collect()\n\n        Seq(\"id1\", \"id2\").foreach { idCol =>\n          checkAnswer(\n            sql(s\"SELECT COUNT(DISTINCT $idCol) == COUNT(*) FROM $tgt\"),\n            Row(true))\n        }\n        assert(sql(s\"SELECT * FROM $tgt WHERE id1 % 5 != 0\").count() === 0)\n        assert(sql(s\"SELECT * FROM $tgt WHERE id2 % 3 != 0\").count() === 0)\n\n        checkAnswer(\n          sql(s\"SELECT col1, col2 FROM $tgt\"),\n          Seq(Row(1, 1), Row(2, 0), Row(3, 1), Row(4, 0), Row(5, 1),\n            Row(6, 101), Row(7, 1), Row(8, 0), Row(9, 1), Row(10, 0))\n        )\n      }\n    }\n  }\n\n  test(s\"MERGE with multiple WHEN NOT MATCHED THEN INSERT clauses\") {\n    testMergeWithMultipleWhenNotMatchedClauses()\n  }\n\n  test(s\"MERGE with multiple WHEN NOT MATCHED THEN INSERT clauses + WholeStageCodeGen disabled\") {\n    testMergeWithMultipleWhenNotMatchedClauses(codegenEnabled = false)\n  }\n\n  test(s\"MERGE with multiple WHEN NOT MATCHED THEN INSERT clauses + single partition\") {\n    testMergeWithMultipleWhenNotMatchedClauses(numPartitions = 1)\n  }\n\n  private def testReplaceWhereWithCDF(isPartitioned: Boolean): Unit = {\n    val start = 1L\n    val step = 2L\n    withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n      val table = getRandomTableName\n      withTable(table) {\n        var highWaterMarkFromData = start - step\n        createTable(\n          table,\n          Seq(\n            IdentityColumnSpec(\n              GeneratedAlways,\n              startsWith = Some(start),\n              incrementBy = Some(step)\n            ),\n            TestColumnSpec(colName = \"value\", dataType = IntegerType),\n            TestColumnSpec(colName = \"is_odd\", dataType = BooleanType),\n            TestColumnSpec(colName = \"is_even\", dataType = BooleanType)\n          ),\n          partitionedBy = if (isPartitioned) Seq(\"is_odd\") else Nil\n        )\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n\n        def highWatermarkFromDeltaLog(): Long = highWaterMark(deltaLog.update(), \"id\")\n\n        Seq(1, 2, 3, 4).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .withColumn(\"is_even\", $\"value\" % 2 === 0)\n          .coalesce(1)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(table)\n        highWaterMarkFromData = validateIdentity(\n          table,\n          expectedRowCount = 4,\n          start = start,\n          step = step,\n          minValue = 1,\n          maxValue = 4,\n          oldHighWaterMark = highWaterMarkFromData)\n        assert(highWaterMarkFromData === highWatermarkFromDeltaLog())\n\n        val idBeforeReplaceWhere1 = sql(s\"SELECT id FROM $table WHERE is_even = true\").collect()\n\n        Seq(5, 7).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .withColumn(\"is_even\", $\"value\" % 2 === 0)\n          .coalesce(1)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_odd = true\")\n          .saveAsTable(table)\n\n        highWaterMarkFromData = validateIdentity(\n          table,\n          expectedRowCount = 4,\n          start = start,\n          step = step,\n          minValue = 5,\n          maxValue = 7,\n          oldHighWaterMark = highWaterMarkFromData)\n        assert(highWaterMarkFromData === highWatermarkFromDeltaLog())\n\n        // IDENTITY values for not-updated shouldn't change.\n        checkAnswer(\n          sql(s\"SELECT id FROM $table WHERE is_even = true\"),\n          idBeforeReplaceWhere1\n        )\n\n        // IDENTITY VALUES for inserted change records and new data should be consistent.\n        checkAnswer(\n          sql(s\"SELECT id FROM table_changes('$table', 2, 2) \" +\n            \"WHERE is_odd = true and _change_type = 'insert'\"),\n          sql(s\"SELECT id FROM $table WHERE is_odd = true\")\n        )\n\n        val idBeforeReplaceWhere2 = sql(s\"SELECT id FROM $table WHERE is_odd = true\").collect()\n\n        Seq(10, 12).toDF()\n          .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n          .withColumn(\"is_even\", $\"value\" % 2 === 0)\n          .coalesce(1)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_even = true\")\n          .saveAsTable(table)\n\n        highWaterMarkFromData = validateIdentity(\n          table,\n          expectedRowCount = 4,\n          start = start,\n          step = step,\n          minValue = 10,\n          maxValue = 12,\n          oldHighWaterMark = highWaterMarkFromData)\n        assert(highWaterMarkFromData === highWatermarkFromDeltaLog())\n        // IDENTITY values for not-updated shouldn't change.\n        checkAnswer(\n          sql(s\"SELECT id FROM $table WHERE is_odd = true\"),\n          idBeforeReplaceWhere2\n        )\n\n        // IDENTITY VALUES for inserted change records and data should be consistent.\n        checkAnswer(\n          sql(s\"SELECT id FROM table_changes('$table', 3, 3) \" +\n            \"WHERE is_even = true and _change_type = 'insert'\"),\n          sql(s\"SELECT id FROM $table WHERE is_even = true\")\n        )\n\n        // ReplaceWhere source data contains an Identity Column will be blocked.\n        val e = intercept[AnalysisException] {\n          Seq((15, 14), (17, 16)).toDF(\"id\", \"value\")\n            .withColumn(\"is_odd\", $\"value\" % 2 =!= 0)\n            .withColumn(\"is_even\", $\"value\" % 2 === 0)\n            .coalesce(1)\n            .write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"is_even = true\")\n            .saveAsTable(table)\n        }\n        assert(e.getMessage.contains(\n          \"Providing values for GENERATED ALWAYS AS IDENTITY column id is not supported.\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"replaceWhere with CDF enabled [isPartitioned: $isPartitioned]\") {\n      testReplaceWhereWithCDF(isPartitioned)\n    }\n  }\n\n  test(\"CDF for tables with identity column - MERGE\") {\n    val src = s\"${getRandomTableName}_src\"\n    val tgt = s\"${getRandomTableName}_tgt\"\n    withTable(tgt) {\n      generateTableWithIdentityColumn(tgt)\n      sql(s\"ALTER TABLE $tgt SET TBLPROPERTIES (${DeltaConfigs.CHANGE_DATA_FEED.key}=true)\")\n\n      withTempView(src) {\n        (1 :: 20 :: 30 :: Nil).toDF(\"value\").createOrReplaceTempView(src)\n        sql(\n          s\"\"\"MERGE INTO $tgt target\n             |USING $src src ON target.value = src.value\n             |WHEN MATCHED THEN UPDATE SET target.value = src.value\n             |WHEN NOT MATCHED THEN INSERT (target.value) VALUES (src.value)\"\"\".stripMargin)\n      }\n\n      val sortedResult = sql(\n        s\"\"\"SELECT id, value\n           |FROM $tgt\n           |ORDER BY id\"\"\".stripMargin)\n        .as[IdentityColumnTestTableRow]\n        .collect()\n\n      assert(sortedResult.length === 8)\n      checkGeneratedIdentityValues(\n        sortedRows = sortedResult,\n        start = 0,\n        step = 1,\n        expectedLowerBound = 0,\n        expectedUpperBound = 20,\n        expectedDistinctCount = 8)\n\n      def getIdForValue(value: Int): Long = {\n        val rowWithValue = sortedResult.filter(_.value == value.toString)\n        assert(\n          rowWithValue.length === 1,\n          s\"Expected 1 row for value $value, found ${rowWithValue.length}\")\n        rowWithValue.head.id\n      }\n\n      // Validate the ids in CDCReader match those in logical data.\n      checkAnswer(sql(\n        s\"\"\"SELECT id, value, ${CDCReader.CDC_TYPE_COLUMN_NAME}\n           |FROM table_changes('$tgt', 8)\n           |ORDER BY value, id, _change_type\"\"\".stripMargin),\n        Seq(\n          Row(1, 1, CDCReader.CDC_TYPE_UPDATE_POSTIMAGE),\n          Row(1, 1, CDCReader.CDC_TYPE_UPDATE_PREIMAGE),\n          Row(getIdForValue(value = 20), 20, CDCReader.CDC_TYPE_INSERT),\n          Row(getIdForValue(value = 30), 30, CDCReader.CDC_TYPE_INSERT)))\n    }\n  }\n\n  test(\"CDF for tables with identity column - UPDATE\") {\n    val tgt = s\"${getRandomTableName}_tgt\"\n    withTable(tgt) {\n\n      generateTableWithIdentityColumn(tgt)\n      sql(s\"ALTER TABLE $tgt SET TBLPROPERTIES (${DeltaConfigs.CHANGE_DATA_FEED.key}=true)\")\n\n      sql(\n        s\"\"\"UPDATE $tgt\n           |SET value = value + 100\n           |WHERE id < 3\"\"\".stripMargin)\n\n      checkAnswer(sql(\n        s\"\"\"SELECT *\n           |FROM $tgt\n           |ORDER BY id, value\"\"\".stripMargin),\n        Seq(\n          Row(0, 100), Row(1, 101), Row(2, 102),\n          Row(3, 3), Row(4, 4), Row(5, 5)))\n\n      checkAnswer(sql(\n        s\"\"\"SELECT id, value, ${CDCReader.CDC_TYPE_COLUMN_NAME}\n           |FROM table_changes('$tgt', 8)\n           |ORDER BY id, value, _change_type\"\"\".stripMargin),\n        Seq(\n          Row(0, 0, CDCReader.CDC_TYPE_UPDATE_PREIMAGE),\n          Row(0, 100, CDCReader.CDC_TYPE_UPDATE_POSTIMAGE),\n          Row(1, 1, CDCReader.CDC_TYPE_UPDATE_PREIMAGE),\n          Row(1, 101, CDCReader.CDC_TYPE_UPDATE_POSTIMAGE),\n          Row(2, 2, CDCReader.CDC_TYPE_UPDATE_PREIMAGE),\n          Row(2, 102, CDCReader.CDC_TYPE_UPDATE_POSTIMAGE)))\n    }\n  }\n\n  test(\"UPDATE cannot lead to bad high watermarks\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      createTable(\n        tblName,\n        Seq(\n          IdentityColumnSpec(\n            GeneratedByDefault,\n            startsWith = Some(1),\n            incrementBy = Some(1)),\n          TestColumnSpec(colName = \"value\", dataType = IntegerType)\n        ),\n        partitionedBy = Seq(\"value\"),\n        tblProperties = Map(\n          DeltaConfigs.CHANGE_DATA_FEED.key -> \"true\",\n          DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key -> \"false\"\n        )\n      )\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n\n      sql(s\"INSERT INTO $tblName(id, value) VALUES (-5, -5), (-3, -3), (-1, -1)\")\n      val valuesStr = (-999 to -900).map(id => s\"($id, -3)\").mkString(\", \")\n      sql(s\"INSERT INTO $tblName(id, value) VALUES $valuesStr\")\n      sql(s\"INSERT INTO $tblName(id, value) VALUES (-1, -1)\")\n      assert(getHighWaterMark(deltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted values\")\n\n      Seq((-1000L, -3)).toDF(\"id\", \"value\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .option(DeltaOptions.REPLACE_WHERE_OPTION, \"value = -3 and id <= -987\")\n        .saveAsTable(tblName)\n\n      assert(getHighWaterMark(deltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted values\")\n\n      sql(s\"UPDATE $tblName SET value = -3 WHERE id = -1\")\n      assert(getHighWaterMark(deltaLog.update(), colName = \"id\").isEmpty,\n        \"Updates should not update high watermark\")\n    }\n  }\n}\n\nclass IdentityColumnDMLScalaSuite\n  extends IdentityColumnDMLSuiteBase\n  with ScalaDDLTestUtils\n\nclass IdentityColumnDMLScalaIdColumnMappingSuite\n  extends IdentityColumnDMLSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableIdMode\n\nclass IdentityColumnDMLScalaNameColumnMappingSuite\n  extends IdentityColumnDMLSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableNameMode\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnIngestionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.PrintWriter\n\nimport org.apache.spark.sql.delta.GeneratedAsIdentityType.{GeneratedAlways, GeneratedByDefault}\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types._\n\n/**\n * Identity Column test suite for ingestion, including insert-only MERGE.\n * Tests with identity columns where MERGE does data modification should be\n * in IdentityColumnDMLSuiteBase.\n */\ntrait IdentityColumnIngestionSuiteBase extends IdentityColumnTestUtils {\n\n  import testImplicits._\n\n  private val tempCsvFileName = \"test.csv\"\n\n  /** Helper function to write a single 'value' column into `sourcePath`. */\n  private def setupSimpleCsvFiles(sourcePath: String, start: Int, end: Int): Unit = {\n    val writer = new PrintWriter(s\"$sourcePath/$tempCsvFileName\")\n    // Write header.\n    writer.write(\"value\\n\")\n    // Write values.\n    (start to end).foreach { v =>\n      writer.write(s\"$v\\n\")\n    }\n    writer.close()\n  }\n\n  object IngestMode extends Enumeration {\n    // Ingest using data frame append v1.\n    val appendV1 = Value\n\n    // Ingest using data frame append v2.\n    val appendV2 = Value\n\n    // Ingest using \"INSERT INTO ... VALUES\".\n    val insertIntoValues = Value\n\n    // Ingest using \"INSERT INTO ... SELECT ...\".\n    val insertIntoSelect = Value\n\n    // Ingest using \"INSERT OVERWRITE ... VALUES\".\n    val insertOverwriteValues = Value\n\n    // Ingest using \"INSERT OVERWRITE ... SELECT ...\".\n    val insertOverwriteSelect = Value\n\n\n    // Ingest using streaming query.\n    val streaming = Value\n\n    // Ingest using MERGE INTO ... WHEN NOT MATCHED INSERT\n    val mergeInsert = Value\n  }\n\n  case class IngestTestCase(start: Long, step: Long, iteration: Int, batchSize: Int)\n\n  /**\n   * Helper function to test ingesting data to delta table with IDENTITY columns.\n   *\n   * @param start     IDENTITY start configuration.\n   * @param step      IDENTITY step configuration.\n   * @param iteration How many batch to ingest.\n   * @param batchSize How many rows to ingest in each batch.\n   * @param mode      Specifies what command to use to ingest data.\n   */\n  private def testIngestData(\n      start: Long,\n      step: Long,\n      iteration: Int,\n      batchSize: Int,\n      mode: IngestMode.Value): Unit = {\n    var highWaterMark = start - step\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      createTableWithIdColAndIntValueCol(\n        tblName, GeneratedAlways, startsWith = Some(start), incrementBy = Some(step))\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n      for (iter <- 0 to iteration - 1) {\n        val batchStart = iter * batchSize + 1\n        val batchEnd = (iter + 1) * batchSize\n\n        // Used by data frame append v1 and append v2.\n        val df = (batchStart to batchEnd).toDF(\"value\")\n        // Used by insertInto, insertIntoSelect, insertOverwrite, insertOverwriteSelect\n        val insertValues = (batchStart to batchEnd).map(v => s\"($v)\").mkString(\",\")\n        val tempTblName = s\"${getRandomTableName}_temp\"\n\n        mode match {\n          case IngestMode.appendV1 =>\n            df.write.format(\"delta\").mode(\"append\").save(deltaLog.dataPath.toString)\n\n          case IngestMode.appendV2 =>\n            df.writeTo(tblName).append()\n\n          case IngestMode.insertIntoValues =>\n            val insertStmt = s\"INSERT INTO $tblName(value) VALUES $insertValues;\"\n            sql(insertStmt)\n\n          case IngestMode.insertIntoSelect =>\n            withTable(tempTblName) {\n              // Insert values into a separate table, then select into the destination table.\n              createTable(\n                tempTblName, Seq(TestColumnSpec(colName = \"value\", dataType = IntegerType)))\n              sql(s\"INSERT INTO $tempTblName VALUES $insertValues\")\n              sql(s\"INSERT INTO $tblName(value) SELECT value FROM $tempTblName\")\n            }\n\n          case IngestMode.insertOverwriteSelect =>\n            withTable(tempTblName) {\n              // Insert values into a separate table, then select into the destination table.\n              createTable(\n                tempTblName, Seq(TestColumnSpec(colName = \"value\", dataType = IntegerType)))\n              sql(s\"INSERT INTO $tempTblName VALUES $insertValues\")\n              sql(s\"INSERT OVERWRITE $tblName(value) SELECT value FROM $tempTblName\")\n            }\n\n          case IngestMode.insertOverwriteValues =>\n            val insertStmt = s\"INSERT OVERWRITE $tblName(value) VALUES $insertValues\"\n            sql(insertStmt)\n\n          case IngestMode.streaming =>\n            withTempDir { checkpointDir =>\n              val stream = MemoryStream[Int]\n              val q = stream\n                .toDF\n                .toDF(\"value\")\n                .writeStream\n                .format(\"delta\")\n                .outputMode(\"append\")\n                .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n                .start(deltaLog.dataPath.toString)\n              stream.addData(batchStart to batchEnd)\n              q.processAllAvailable()\n              q.stop()\n            }\n\n          case IngestMode.mergeInsert =>\n            withTable(tempTblName) {\n              // Insert values into a separate table, then merge into the destination table.\n              createTable(\n                tempTblName, Seq(TestColumnSpec(colName = \"value\", dataType = IntegerType)))\n              sql(s\"INSERT INTO $tempTblName VALUES $insertValues\")\n              sql(\n                s\"\"\"\n                   |MERGE INTO $tblName\n                   |  USING $tempTblName ON $tblName.value = $tempTblName.value\n                   |  WHEN NOT MATCHED THEN INSERT (value) VALUES ($tempTblName.value)\n                   |\"\"\".stripMargin)\n            }\n\n          case _ => assert(false, \"Unrecognized ingestion mode\")\n        }\n\n        val expectedRowCount = mode match {\n          case _@(IngestMode.insertOverwriteValues | IngestMode.insertOverwriteSelect) =>\n            // These modes keep the row count unchanged.\n            batchSize\n          case _ => batchSize * (iter + 1)\n        }\n\n        highWaterMark = validateIdentity(tblName, expectedRowCount, start, step,\n          batchStart, batchEnd, highWaterMark)\n      }\n    }\n  }\n\n  test(\"append v1\") {\n    val testCases = Seq(\n      IngestTestCase(1, 1, 4, 250),\n      IngestTestCase(1, -3, 10, 23)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize, IngestMode.appendV1)\n    }\n  }\n\n  test(\"append v2\") {\n    val testCases = Seq(\n      IngestTestCase(100, 100, 3, 300),\n      IngestTestCase(Integer.MAX_VALUE.toLong + 1, -1000, 10, 23)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize, IngestMode.appendV2)\n    }\n  }\n\n  test(\"insert into values\") {\n    val testCases = Seq(\n      IngestTestCase(100, -100, 4, 201),\n      IngestTestCase(Integer.MAX_VALUE.toLong + 1, 1000, 10, 37)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize, IngestMode.insertIntoValues)\n    }\n  }\n\n  test(\"insert into select\") {\n    val testCases = Seq(\n      IngestTestCase(23, 102, 3, 77),\n      IngestTestCase(Integer.MAX_VALUE.toLong - 12345, 99, 8, 25)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize, IngestMode.insertIntoSelect)\n    }\n  }\n\n  test(\"insert overwrite values\") {\n    val testCases = Seq(\n      IngestTestCase(-10, 3, 5, 30),\n      IngestTestCase(Integer.MIN_VALUE.toLong - 1000, -18, 2, 100)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize,\n        IngestMode.insertOverwriteValues)\n    }\n  }\n\n  test(\"insert overwrite select\") {\n    val testCases = Seq(\n      IngestTestCase(-15, 20, 4, 35),\n      IngestTestCase(200, 50, 3, 7)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize,\n        IngestMode.insertOverwriteSelect)\n    }\n  }\n\n  test(\"streaming\") {\n    val testCases = Seq(\n      IngestTestCase(-2000, 19, 5, 20),\n      IngestTestCase(10, 10, 4, 17)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize, IngestMode.streaming)\n    }\n  }\n\n  test(\"merge insert\") {\n    val testCases = Seq(\n      IngestTestCase(10, 20, 5, 8),\n      IngestTestCase(-5000, 37, 7, 99)\n    )\n    for (tc <- testCases) {\n      testIngestData(tc.start, tc.step, tc.iteration, tc.batchSize, IngestMode.mergeInsert)\n    }\n  }\n\n  test(\"explicit insert not allowed\") {\n    val tblName = getRandomTableName\n    withIdentityColumnTable(GeneratedAlways, tblName) {\n      val ex = intercept[AnalysisException](sql(s\"INSERT INTO $tblName values(1,1);\"))\n      assert(ex.getMessage.contains(\"Providing values for GENERATED ALWAYS AS IDENTITY\"))\n    }\n  }\n\n  test(\"explicit insert should not update high water mark\") {\n    val tblName = getRandomTableName\n    withIdentityColumnTable(GeneratedByDefault, tblName) {\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n      val schema1 = deltaLog.update().metadata.schemaString\n\n      // System generated IDENTITY value - should update schema.\n      sql(s\"INSERT INTO $tblName(value) VALUES (1);\")\n      val snapshot2 = deltaLog.update()\n      val highWatermarkAfterGeneration = getHighWaterMark(snapshot2, \"id\")\n      assert(highWatermarkAfterGeneration.isDefined)\n      val schema2 = snapshot2.metadata.schemaString\n      assert(schema1 != schema2)\n\n      // Explicitly provided IDENTITY value - should not update schema.\n      sql(s\"INSERT INTO $tblName VALUES (1,1);\")\n      val snapshot3 = deltaLog.update()\n      val schema3 = snapshot3.metadata.schemaString\n      val highWatermarkAfterUserInsert = getHighWaterMark(snapshot3, \"id\")\n      assert(highWatermarkAfterUserInsert == highWatermarkAfterGeneration)\n      assert(schema2 == schema3)\n    }\n  }\n\n  test(\"merge command with nondeterministic functions in conditions\") {\n    val source = \"identity_merge_source\"\n    val target = \"identity_merge_target\"\n    withIdentityColumnTable(GeneratedByDefault, target) {\n      withTable(source) {\n        createTable(\n          source,\n          Seq(\n            TestColumnSpec(colName = \"id2\", dataType = LongType),\n            TestColumnSpec(colName = \"value2\", dataType = LongType)\n          )\n        )\n\n        val ex1 = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |MERGE INTO $target\n               |  USING $source ON $target.value = $source.value2 + rand()\n               |  WHEN NOT MATCHED THEN INSERT (value) VALUES ($source.value2)\n               |\"\"\".stripMargin)\n        }\n        assert(ex1.getMessage.contains(\"Non-deterministic functions are not supported\"))\n        val ex2 = intercept[AnalysisException] {\n          sql(\n            s\"\"\"\n               |MERGE INTO $target\n               |  USING $source ON $target.value = $source.value2\n               |  WHEN NOT MATCHED AND $source.value2 = rand()\n               |    THEN INSERT (value) VALUES ($source.value2)\n               |\"\"\".stripMargin)\n        }\n        assert(ex2.getMessage.contains(\"Non-deterministic functions are not supported\"))\n      }\n    }\n  }\n\n  /**\n   * Creates a source and destination table with the same schema such that if it is a positive step,\n   * the source table has identity column values < the target table's start value. If it's\n   * a negative step, the source table has identity column values > the target table's start value.\n   * @param isSrcDataSubsetOfTgt Whether the source data is a subset of the target data. If false,\n   *                             some data is inserted into the target table below the start of\n   *                             the identity column value.\n   * @param positiveStep Whether the identity column values are generated in a positive step.\n   * @param expectValidHighWaterMark Whether the high water mark is expected to be set to a valid\n   *                                 value in the target table after running `insertDataFn`. If so,\n   *                                 we check that it respects the start value of the column.\n   * @param insertDataFn Function that inserts data from the source table to the target table.\n   */\n  private def withSrcAndDestTables(\n      isSrcDataSubsetOfTgt: Boolean,\n      positiveStep: Boolean,\n      expectValidHighWaterMark: Boolean)(\n      insertDataFn: (String, String) => Unit): Unit = {\n    import testImplicits._\n    val srcTblName = s\"${getRandomTableName}_src\"\n    val tgtTblName = s\"${getRandomTableName}_tgt\"\n    withTable(srcTblName, tgtTblName) {\n      val targetTableStartWith = if (positiveStep) 100000 else -100000\n      val targetTableIncrementBy = if (positiveStep) 53 else -53\n      // Create a generated always source table with (id, value)\n      // starting with 0 and incrementing by targetTableIncrementBy.\n      generateTableWithIdentityColumn(srcTblName, step = targetTableIncrementBy)\n\n      val srcDeltaLog = DeltaLog.forTable(spark, TableIdentifier(srcTblName))\n      assert(getHighWaterMark(srcDeltaLog.update(), colName = \"id\").isDefined)\n      // While id col values generation is nondeterministic, the high water mark\n      // should really not exceed this value.\n      if (positiveStep) {\n        assert(highWaterMark(srcDeltaLog.update(), colName = \"id\") < targetTableStartWith)\n      } else {\n        assert(highWaterMark(srcDeltaLog.update(), colName = \"id\") > targetTableStartWith)\n      }\n\n      // Create a generated by default target table with (id, value)\n      createTableWithIdColAndIntValueCol(\n        tgtTblName,\n        GeneratedByDefault,\n        startsWith = Some(targetTableStartWith),\n        incrementBy = Some(targetTableIncrementBy),\n        tblProperties = Map.empty)\n\n      val tgtDeltaLog = DeltaLog.forTable(spark, TableIdentifier(tgtTblName))\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set if the table is empty.\")\n\n      if (isSrcDataSubsetOfTgt) {\n        sql(s\"INSERT INTO $tgtTblName(id, value) SELECT * FROM $srcTblName\")\n      } else {\n        // Manually insert some data into the target table below the startWith.\n        if (positiveStep) {\n          sql(s\"INSERT INTO $tgtTblName(id, value) VALUES (1, 100), (2, 101)\")\n        } else {\n          sql(s\"INSERT INTO $tgtTblName(id, value) VALUES (-1, 100), (-2, 101)\")\n        }\n      }\n\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n      if (positiveStep) {\n        assert(sql(s\"SELECT max(id) FROM $tgtTblName\").as[Long].head < targetTableStartWith)\n      } else {\n        assert(sql(s\"SELECT min(id) FROM $tgtTblName\").as[Long].head > targetTableStartWith)\n      }\n\n      insertDataFn(srcTblName, tgtTblName)\n\n      if (expectValidHighWaterMark) {\n        if (positiveStep) {\n          assert(highWaterMark(tgtDeltaLog.update(), colName = \"id\") >= targetTableStartWith)\n        } else {\n          assert(highWaterMark(tgtDeltaLog.update(), colName = \"id\") <= targetTableStartWith)\n        }\n      }\n    }\n  }\n\n  test(\"Appending from a source table with a high water mark should not update\" +\n    \" the target table's high water mark\") {\n    withSrcAndDestTables(\n      isSrcDataSubsetOfTgt = false,\n      positiveStep = true,\n      expectValidHighWaterMark = false) { (srcTblName, tgtTblName) =>\n      val tgtDeltaLog = DeltaLog.forTable(spark, TableIdentifier(tgtTblName))\n      // dataframe v2\n      spark.table(srcTblName).writeTo(tgtTblName).append()\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n\n      // v1\n      spark.table(srcTblName).write.format(\"delta\").mode(\"append\").saveAsTable(tgtTblName)\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n\n      spark.table(srcTblName).write.insertInto(tgtTblName)\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n\n      // SQL\n      sql(s\"INSERT INTO $tgtTblName SELECT * FROM $srcTblName\")\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n\n      sql(s\"INSERT INTO $tgtTblName BY NAME SELECT * FROM $srcTblName\")\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n\n      sql(s\"INSERT INTO $tgtTblName(id, value) SELECT id, value FROM $srcTblName\")\n      assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty,\n        \"High watermark should not be set for user inserted data.\")\n    }\n  }\n\n  for {\n    cdfEnabled <- DeltaTestUtils.BOOLEAN_DOMAIN\n    isSrcDataSubsetOfTgt <- DeltaTestUtils.BOOLEAN_DOMAIN\n    positiveStep <- DeltaTestUtils.BOOLEAN_DOMAIN\n    statementWithOnlyUpdates <- DeltaTestUtils.BOOLEAN_DOMAIN\n  } test(\n      s\"MERGE UPSERT with source on identity column, cdfEnabled=$cdfEnabled, \" +\n      s\"isSrcDataSubsetOfTgt=$isSrcDataSubsetOfTgt, \" +\n      s\"positiveStep=$positiveStep, statementWithOnlyUpdates=$statementWithOnlyUpdates\") {\n    val expectValidHighWaterMark = !statementWithOnlyUpdates && !isSrcDataSubsetOfTgt\n    withSrcAndDestTables(\n        isSrcDataSubsetOfTgt,\n        positiveStep,\n        expectValidHighWaterMark) { (srcTblName, tgtTblName) =>\n      if (cdfEnabled) {\n        val cdfPropKey = DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey\n        sql(s\"ALTER TABLE $tgtTblName SET TBLPROPERTIES('$cdfPropKey' = 'true')\")\n      }\n      // Merge into the target table from the source table.\n      // The target table will generate values starting from targetTableStartWith.\n      // The high water mark from the source should not interfere.\n      if (statementWithOnlyUpdates) {\n        sql(\n          s\"\"\"\n             |MERGE INTO $tgtTblName tgt\n             |USING $srcTblName src ON tgt.id = src.id\n             |WHEN MATCHED THEN UPDATE SET tgt.value = src.value\n             |\"\"\".stripMargin)\n      } else {\n        sql(\n          s\"\"\"\n             |MERGE INTO $tgtTblName tgt\n             |USING $srcTblName src ON tgt.id = src.id\n             |WHEN MATCHED THEN UPDATE SET tgt.value = src.value\n             |WHEN NOT MATCHED THEN INSERT (value) VALUES (src.value)\n             |\"\"\".stripMargin)\n      }\n\n      if (!expectValidHighWaterMark) {\n        val tgtDeltaLog = DeltaLog.forTable(spark, TableIdentifier(tgtTblName))\n        assert(getHighWaterMark(tgtDeltaLog.update(), colName = \"id\").isEmpty)\n      }\n    }\n  }\n\n  for (positiveStep <- DeltaTestUtils.BOOLEAN_DOMAIN)\n  test(s\"MERGE UPSERT into a table with a bad watermark, positiveStep=$positiveStep\") {\n    // Suppose that a table has a bad watermark (for whatever reason), the system should still\n    // have a sensible behavior and be robust to these bad watermark.\n    withSrcAndDestTables(\n        isSrcDataSubsetOfTgt = false,\n        positiveStep,\n        expectValidHighWaterMark = false) { (srcTblName, tgtTblName) =>\n      val tgtDeltaLog = DeltaLog.forTable(spark, TableIdentifier(tgtTblName))\n      forceBadWaterMark(tgtDeltaLog)\n      val badWaterMark = highWaterMark(tgtDeltaLog.update(), colName = \"id\")\n      sql(\n        s\"\"\"\n           |MERGE INTO $tgtTblName tgt\n           |USING $srcTblName src ON tgt.id = src.id\n           |WHEN MATCHED THEN UPDATE SET tgt.value = src.value\n           |WHEN NOT MATCHED THEN INSERT (value) VALUES (src.value)\n           |\"\"\".stripMargin)\n\n      // Even though the high water mark is invalid, we don't want to prevent updates to the high\n      // water mark as this would lead to us generating the same values over and over.\n      val newHighWaterMark = highWaterMark(tgtDeltaLog.update(), colName = \"id\")\n      assert(newHighWaterMark !== badWaterMark,\n        \"New data was inserted. The high water mark should have updated\")\n      if (positiveStep) {\n        assert(newHighWaterMark > badWaterMark)\n      } else {\n        assert(newHighWaterMark < badWaterMark)\n      }\n    }\n  }\n}\n\nclass IdentityColumnIngestionScalaSuite\n  extends IdentityColumnIngestionSuiteBase\n  with ScalaDDLTestUtils\n\nclass IdentityColumnIngestionScalaIdColumnMappingSuite\n  extends IdentityColumnIngestionSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableIdMode\n\nclass IdentityColumnIngestionScalaNameColumnMappingSuite\n  extends IdentityColumnIngestionSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableNameMode\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport scala.collection.mutable.ListBuffer\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.GeneratedAsIdentityType.{GeneratedAlways, GeneratedAsIdentityType, GeneratedByDefault}\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.commons.io.FileUtils\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, Dataset, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.Trigger\nimport org.apache.spark.sql.types._\n\n/**\n * General test suite for identity columns.\n */\ntrait IdentityColumnSuiteBase extends IdentityColumnTestUtils {\n\n  import testImplicits._\n\n  test(\"Don't allow IDENTITY column in the schema if the feature is disabled\") {\n    val tblName = getRandomTableName\n    withSQLConf(DeltaSQLConf.DELTA_IDENTITY_COLUMN_ENABLED.key -> \"false\") {\n      withTable(tblName) {\n        val e = intercept[DeltaUnsupportedTableFeatureException] {\n          createTableWithIdColAndIntValueCol(\n            tblName, GeneratedByDefault, startsWith = None, incrementBy = None)\n        }\n        val errorMsg = e.getMessage\n        assert(errorMsg.contains(\"requires writer table feature(s) that are unsupported\"))\n        assert(errorMsg.contains(IdentityColumnsTableFeature.name))\n      }\n    }\n  }\n\n  // Build expected schema of the following table definition for verification:\n  // CREATE TABLE tableName (\n  //   id BIGINT <keyword> IDENTITY (START WITH <start> INCREMENT BY <step>),\n  //   value INT\n  // );\n  private def expectedSchema(\n      generatedAsIdentityType: GeneratedAsIdentityType,\n      start: Long = IdentityColumn.defaultStart,\n      step: Long = IdentityColumn.defaultStep): StructType = {\n    val colFields = new ListBuffer[StructField]\n\n    val allowExplicitInsert = generatedAsIdentityType == GeneratedByDefault\n    val builder = new MetadataBuilder()\n    builder.putBoolean(DeltaSourceUtils.IDENTITY_INFO_ALLOW_EXPLICIT_INSERT,\n      allowExplicitInsert)\n    builder.putLong(DeltaSourceUtils.IDENTITY_INFO_START, start)\n    builder.putLong(DeltaSourceUtils.IDENTITY_INFO_STEP, step)\n    colFields += StructField(\"id\", LongType, true, builder.build())\n    colFields += StructField(\"value\", IntegerType)\n\n    StructType(colFields.toSeq)\n  }\n\n  test(\"various configuration\") {\n    val starts = Seq(\n      Long.MinValue,\n      Integer.MIN_VALUE.toLong,\n      -100L,\n      0L,\n      1000L,\n      Integer.MAX_VALUE.toLong,\n      Long.MaxValue\n    )\n    val steps = Seq(\n      Long.MinValue,\n      Integer.MIN_VALUE.toLong,\n      -100L,\n      1000L,\n      Integer.MAX_VALUE.toLong,\n      Long.MaxValue\n    )\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      startsWith <- starts\n      incrementBy <- steps\n    } {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        createTableWithIdColAndIntValueCol(\n          tblName, generatedAsIdentityType, Some(startsWith), Some(incrementBy))\n        val table = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        val actualSchema =\n          DeltaColumnMapping.dropColumnMappingMetadata(table.snapshot.metadata.schema)\n        assert(actualSchema === expectedSchema(generatedAsIdentityType, startsWith, incrementBy))\n      }\n    }\n  }\n\n  test(\"default configuration\") {\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      startsWith <- Seq(Some(1L), None)\n      incrementBy <- Seq(Some(1L), None)\n    } {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        createTableWithIdColAndIntValueCol(\n          tblName, generatedAsIdentityType, startsWith, incrementBy)\n        val table = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        val actualSchema =\n          DeltaColumnMapping.dropColumnMappingMetadata(table.snapshot.metadata.schema)\n        assert(actualSchema === expectedSchema(generatedAsIdentityType))\n      }\n    }\n  }\n\n  test(\"logging\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      val eventsDefinition = Log4jUsageLogger.track {\n        createTable(\n          tblName,\n          Seq(\n            IdentityColumnSpec(\n              GeneratedByDefault,\n              startsWith = Some(1),\n              incrementBy = Some(1),\n              colName = \"id1\"\n            ),\n            IdentityColumnSpec(\n              GeneratedAlways,\n              startsWith = Some(1),\n              incrementBy = Some(1),\n              colName = \"id2\"\n            ),\n            IdentityColumnSpec(\n              GeneratedAlways,\n              startsWith = Some(1),\n              incrementBy = Some(1),\n              colName = \"id3\"\n            ),\n            TestColumnSpec(colName = \"value\", dataType = IntegerType)\n          )\n        )\n      }.filter { e =>\n        e.tags.get(\"opType\").exists(_ == IdentityColumn.opTypeDefinition)\n      }\n      assert(eventsDefinition.size == 1)\n      assert(JsonUtils.fromJson[Map[String, String]](eventsDefinition.head.blob)\n        .get(\"numIdentityColumns\").exists(_ == \"3\"))\n\n      val eventsWrite = Log4jUsageLogger.track {\n        sql(s\"INSERT INTO $tblName (id1, value) VALUES (1, 10), (2, 20)\")\n      }.filter { e =>\n        e.tags.get(\"opType\").exists(_ == IdentityColumn.opTypeWrite)\n      }\n      assert(eventsWrite.size == 1)\n      val data = JsonUtils.fromJson[Map[String, String]](eventsWrite.head.blob)\n      assert(data.get(\"numInsertedRows\").exists(_ == \"2\"))\n      assert(data.get(\"generatedIdentityColumnNames\").exists(_ == \"id2,id3\"))\n      assert(data.get(\"generatedIdentityColumnCount\").exists(_ == \"2\"))\n      assert(data.get(\"explicitIdentityColumnNames\").exists(_ == \"id1\"))\n      assert(data.get(\"explicitIdentityColumnCount\").exists(_ == \"1\"))\n    }\n  }\n\n  test(\"reading table should not see identity column properties\") {\n    def verifyNoIdentityColumn(id: Int, f: () => Dataset[_]): Unit = {\n      assert(!ColumnWithDefaultExprUtils.hasIdentityColumn(f().schema), s\"test $id failed\")\n    }\n\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      createTable(\n        tblName,\n        Seq(\n          IdentityColumnSpec(GeneratedByDefault),\n          TestColumnSpec(colName = \"part\", dataType = LongType),\n          TestColumnSpec(colName = \"value\", dataType = StringType)\n        ),\n        partitionedBy = Seq(\"part\")\n      )\n\n      sql(\n        s\"\"\"\n           |INSERT INTO $tblName (part, value) VALUES\n           |  (1, \"one\"),\n           |  (2, \"two\"),\n           |  (3, \"three\")\n           |\"\"\".stripMargin)\n      val path = DeltaLog.forTable(spark, TableIdentifier(tblName)).dataPath.toString\n\n      val commands: Seq[() => Dataset[_]] = Seq(\n        () => spark.table(tblName),\n        () => sql(s\"SELECT * FROM $tblName\"),\n        () => sql(s\"SELECT * FROM delta.`$path`\"),\n        () => spark.read.format(\"delta\").load(path),\n        () => spark.read.format(\"delta\").table(tblName),\n        () => spark.readStream.format(\"delta\").load(path),\n        () => spark.readStream.format(\"delta\").table(tblName)\n      )\n      commands.zipWithIndex.foreach {\n        case (f, id) => verifyNoIdentityColumn(id, f)\n      }\n      withTempDir { checkpointDir =>\n        val q = spark.readStream.format(\"delta\").table(tblName).writeStream\n          .trigger(Trigger.Once)\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .foreachBatch { (df: DataFrame, _: Long) =>\n            assert(!ColumnWithDefaultExprUtils.hasIdentityColumn(df.schema))\n            ()\n          }.start()\n        try {\n          q.processAllAvailable()\n        } finally {\n          q.stop()\n        }\n      }\n      withTempDir { outputDir =>\n        withTempDir { checkpointDir =>\n          val q = spark.readStream.format(\"delta\").table(tblName).writeStream\n            .trigger(Trigger.Once)\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"delta\")\n            .start(outputDir.getCanonicalPath)\n          try {\n            q.processAllAvailable()\n          } finally {\n            q.stop()\n          }\n          val deltaLog = DeltaLog.forTable(spark, outputDir.getCanonicalPath)\n          assert(deltaLog.snapshot.version >= 0)\n          assert(!ColumnWithDefaultExprUtils.hasIdentityColumn(deltaLog.snapshot.schema))\n        }\n      }\n    }\n  }\n\n  private def withWriterVersion5Table(func: String => Unit): Unit = {\n    // The table on the following path is created with the following steps:\n    // (1) Create a table with IDENTITY column using writer version 6\n    //     CREATE TABLE $tblName (\n    //       id BIGINT GENERATED BY DEFAULT AS IDENTITY,\n    //       part INT,\n    //       value STRING\n    //     ) USING delta\n    //     PARTITIONED BY (part)\n    // (2) CTAS from the above table using writer version 5.\n    // This will result in a table created using protocol (1, 2) with IDENTITY columns.\n    val resourcePath = \"src/test/resources/delta/identity_test_written_by_version_5\"\n    withTempDir { tempDir =>\n      // Prepare a table that has the old writer version and identity columns.\n      FileUtils.copyDirectory(new File(resourcePath), tempDir)\n      val path = tempDir.getCanonicalPath\n      val deltaLog = DeltaLog.forTable(spark, path)\n      // Verify the table has old writer version and identity columns.\n      assert(ColumnWithDefaultExprUtils.hasIdentityColumn(deltaLog.snapshot.schema))\n      val writerVersionOnTable = deltaLog.snapshot.protocol.minWriterVersion\n      assert(writerVersionOnTable < IdentityColumnsTableFeature.minWriterVersion)\n      func(path)\n    }\n  }\n\n  test(\"compatibility\") {\n    withWriterVersion5Table { v5TablePath =>\n      // Verify initial data.\n      checkAnswer(\n        sql(s\"SELECT * FROM delta.`$v5TablePath`\"),\n        Row(1, 1, \"one\") :: Row(2, 2, \"two\") :: Row(4, 3, \"three\") :: Nil\n      )\n      // Insert new data should generate correct IDENTITY values.\n      sql(s\"\"\"INSERT INTO delta.`$v5TablePath` VALUES (5, 5, \"five\")\"\"\")\n      checkAnswer(\n        sql(s\"SELECT COUNT(DISTINCT id) FROM delta.`$v5TablePath`\"),\n        Row(4L)\n      )\n\n      val deltaLog = DeltaLog.forTable(spark, v5TablePath)\n      val protocolBeforeUpdate = deltaLog.snapshot.protocol\n\n      // ALTER TABLE should drop the IDENTITY columns and keeps the protocol version unchanged.\n      sql(s\"ALTER TABLE delta.`$v5TablePath` ADD COLUMNS (value2 DOUBLE)\")\n      deltaLog.update()\n      assert(deltaLog.snapshot.protocol == protocolBeforeUpdate)\n      assert(!ColumnWithDefaultExprUtils.hasIdentityColumn(deltaLog.snapshot.schema))\n\n      // Specifying a min writer version should not enable IDENTITY column.\n      sql(s\"ALTER TABLE delta.`$v5TablePath` SET TBLPROPERTIES ('delta.minWriterVersion'='4')\")\n      deltaLog.update()\n      assert(deltaLog.snapshot.protocol == Protocol(1, 4))\n      assert(!ColumnWithDefaultExprUtils.hasIdentityColumn(deltaLog.snapshot.schema))\n    }\n  }\n\n  for {\n    generatedAsIdentityType <- GeneratedAsIdentityType.values\n  } {\n    test(\n        \"replace table with identity column should upgrade protocol, \"\n          + s\"identityType: $generatedAsIdentityType\") {\n\n      val tblName = getRandomTableName\n      def getProtocolVersions: (Int, Int) = {\n        sql(s\"DESC DETAIL $tblName\")\n          .select(\"minReaderVersion\", \"minWriterVersion\")\n          .as[(Int, Int)]\n          .head()\n      }\n\n      withTable(tblName) {\n        createTable(\n          tblName,\n          Seq(\n            TestColumnSpec(colName = \"id\", dataType = LongType),\n            TestColumnSpec(colName = \"value\", dataType = IntegerType))\n        )\n        assert(getProtocolVersions == (1, 2) || getProtocolVersions == (2, 7))\n        assert(DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot.version == 0)\n\n        replaceTable(\n          tblName,\n          Seq(\n            IdentityColumnSpec(\n              generatedAsIdentityType,\n              startsWith = Some(1),\n              incrementBy = Some(1)\n            ),\n            TestColumnSpec(colName = \"value\", dataType = IntegerType)\n          )\n        )\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        val snapshot = deltaLog.update()\n        val protocol = snapshot.protocol\n        assert(getProtocolVersions == (1, 7) ||\n          protocol.readerAndWriterFeatures.contains(IdentityColumnsTableFeature))\n        assert(snapshot.version == 1)\n        assert(getHighWaterMark(snapshot, \"id\").isEmpty)\n      }\n    }\n  }\n\n  test(\"ctas/rtas does not produce an identity column\") {\n    def assertIdentityColumn(tblName: String, idColExpected: Boolean): Unit = {\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n      val snapshot = deltaLog.update()\n      assert(ColumnWithDefaultExprUtils.hasIdentityColumn(snapshot.tableSchema) === idColExpected)\n      assert(getHighWaterMark(snapshot, colName = \"id\").isDefined === idColExpected)\n    }\n    val tblName = getRandomTableName\n    val ctasTblName = s\"ctas_$tblName\"\n    withTable(tblName) {\n      generateTableWithIdentityColumn(tblName)\n      assertIdentityColumn(tblName, idColExpected = true)\n\n      withTable(ctasTblName) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $ctasTblName\n             |USING DELTA\n             |AS SELECT * FROM $tblName\n             |\"\"\".stripMargin)\n        assertIdentityColumn(ctasTblName, idColExpected = false)\n        sql(\n          s\"\"\"\n             |CREATE OR REPLACE TABLE $ctasTblName\n             |USING DELTA\n             |AS SELECT * FROM $tblName\n             |\"\"\".stripMargin)\n        assertIdentityColumn(ctasTblName, idColExpected = false)\n      }\n    }\n  }\n\n  test(\"create or replace on a table resets high watermark\") {\n    val tblName = getRandomTableName\n    val initialStartsWith = 100L\n    val increment = 1L\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      isPartitioned <- DeltaTestUtils.BOOLEAN_DOMAIN\n    } {\n      val partitionedBy = if (isPartitioned) Seq(\"value\") else Nil\n      withTable(tblName) {\n        createTable(\n          tblName,\n          Seq(\n            IdentityColumnSpec(\n              generatedAsIdentityType,\n              Some(initialStartsWith),\n              Some(increment)\n            ),\n            TestColumnSpec(colName = \"value\", dataType = IntegerType)\n          ),\n          partitionedBy = partitionedBy\n        )\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n\n        // A column that has not yet generated values does not have a high watermark.\n        assert(getHighWaterMark(deltaLog.update(), colName = \"id\").isEmpty)\n\n        // The system generates one row for the identity column .\n        // The high watermark should now be set to the start value.\n        sql(s\"INSERT INTO $tblName (value) VALUES (1)\")\n        assert(highWaterMark(deltaLog.update(), colName = \"id\") === initialStartsWith)\n\n        for {\n          replaceType <- Seq(DDLType.REPLACE, DDLType.CREATE_OR_REPLACE)\n        } {\n          // After a REPLACE or CREATE OR REPLACE TABLE, there should be no high watermark.\n          val newStartsWith = 50000L\n          runDDL(\n            replaceType,\n            tblName,\n            Seq(\n              IdentityColumnSpec(\n                generatedAsIdentityType,\n                startsWith = Some(newStartsWith),\n                incrementBy = Some(increment)),\n              TestColumnSpec(colName = \"value\", dataType = StringType)\n            ),\n            partitionedBy = partitionedBy,\n            tblProperties = Map.empty\n          )\n          assert(getHighWaterMark(deltaLog.update(), colName = \"id\").isEmpty)\n\n          // Sanity check that the new table is using the new start for the next high watermark.\n          sql(s\"INSERT INTO $tblName (value) VALUES (-1)\")\n          assert(highWaterMark(deltaLog.update(), colName = \"id\") === newStartsWith)\n        }\n      }\n    }\n  }\n\n  ignore(\"identity value start at boundaries\") {\n    val starts = Seq(Long.MinValue, Long.MaxValue)\n    val steps = Seq(1, 2, -1, -2)\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      start <- starts\n      step <- steps\n    } {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        createTableWithIdColAndIntValueCol(\n          tblName, generatedAsIdentityType, Some(start), Some(step))\n        val table = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        val actualSchema =\n          DeltaColumnMapping.dropColumnMappingMetadata(table.snapshot.metadata.schema)\n        assert(actualSchema === expectedSchema(generatedAsIdentityType, start, step))\n        if ((start < 0L) == (step < 0L)) {\n          // test long underflow and overflow\n          val ex = intercept[org.apache.spark.SparkException](\n            sql(s\"INSERT INTO $tblName(value) SELECT 1 UNION ALL SELECT 2\")\n          )\n          assert(ex.getMessage.contains(\"long overflow\"))\n        } else {\n          sql(s\"INSERT INTO $tblName(value) SELECT 1 UNION ALL SELECT 2\")\n          checkAnswer(sql(s\"SELECT COUNT(DISTINCT id) == COUNT(*) FROM $tblName\"), Row(true))\n          sql(s\"INSERT INTO $tblName(value) SELECT 1 UNION ALL SELECT 2\")\n          checkAnswer(sql(s\"SELECT COUNT(DISTINCT id) == COUNT(*) FROM $tblName\"), Row(true))\n          assert(highWaterMark(table.update(), \"id\") ===\n            (start + (3 * step)))\n        }\n      }\n    }\n  }\n\n  test(\"restore - positive step\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      generateTableWithIdentityColumn(tblName)\n      sql(s\"RESTORE TABLE $tblName TO VERSION AS OF 3\")\n      sql(s\"INSERT INTO $tblName (value) VALUES (6)\")\n      checkAnswer(\n        sql(s\"SELECT id, value FROM $tblName ORDER BY value ASC\"),\n        Seq(Row(0, 0), Row(1, 1), Row(2, 2), Row(6, 6))\n      )\n    }\n  }\n\n  test(\"restore - negative step\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      generateTableWithIdentityColumn(tblName, step = -1)\n      sql(s\"RESTORE TABLE $tblName TO VERSION AS OF 3\")\n      sql(s\"INSERT INTO $tblName (value) VALUES (6)\")\n      checkAnswer(\n        sql(s\"SELECT id, value FROM $tblName ORDER BY value ASC\"),\n        Seq(Row(0, 0), Row(-1, 1), Row(-2, 2), Row(-6, 6))\n      )\n    }\n  }\n\n  test(\"restore - on partitioned table\") {\n      for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n        val tblName = getRandomTableName\n        withTable(tblName) {\n          // v0.\n          createTable(\n            tblName,\n            Seq(\n              IdentityColumnSpec(generatedAsIdentityType),\n              TestColumnSpec(colName = \"value\", dataType = IntegerType)\n            ),\n            partitionedBy = Seq(\"value\")\n          )\n          // v1.\n          sql(s\"INSERT INTO $tblName (value) VALUES (1), (2)\")\n          val v1Content = sql(s\"SELECT * FROM $tblName\").collect()\n          // v2.\n          sql(s\"INSERT INTO $tblName (value) VALUES (3), (4)\")\n          // v3: RESTORE to v1.\n          sql(s\"RESTORE TABLE $tblName TO VERSION AS OF 1\")\n          checkAnswer(\n            sql(s\"SELECT COUNT(DISTINCT id) FROM $tblName\"),\n            Row(2L)\n          )\n          checkAnswer(\n            sql(s\"SELECT * FROM $tblName\"),\n            v1Content\n          )\n          // v4.\n          sql(s\"INSERT INTO $tblName (value) VALUES (5), (6)\")\n          checkAnswer(\n            sql(s\"SELECT COUNT(DISTINCT id) FROM $tblName\"),\n            Row(4L)\n          )\n        }\n      }\n  }\n\n  test(\"clone\") {\n      for {\n        generatedAsIdentityType <- GeneratedAsIdentityType.values\n      } {\n        val oldTbl = s\"${getRandomTableName}_old\"\n        val newTbl = s\"${getRandomTableName}_new\"\n        withIdentityColumnTable(generatedAsIdentityType, oldTbl) {\n          withTable(newTbl) {\n            sql(s\"INSERT INTO $oldTbl (value) VALUES (1), (2)\")\n            val oldSchema = DeltaLog.forTable(spark, TableIdentifier(oldTbl)).snapshot.schema\n            sql(\n              s\"\"\"\n                 |CREATE TABLE $newTbl\n                 |  SHALLOW CLONE $oldTbl\n                 |\"\"\".stripMargin)\n            val newSchema = DeltaLog.forTable(spark, TableIdentifier(newTbl)).snapshot.schema\n\n            assert(newSchema(\"id\").metadata.getLong(DeltaSourceUtils.IDENTITY_INFO_START) == 1L)\n            assert(newSchema(\"id\").metadata.getLong(DeltaSourceUtils.IDENTITY_INFO_STEP) == 1L)\n            assert(oldSchema == newSchema)\n\n            sql(s\"INSERT INTO $newTbl (value) VALUES (1), (2)\")\n            checkAnswer(\n              sql(s\"SELECT COUNT(DISTINCT id) FROM $newTbl\"),\n              Row(4L)\n            )\n          }\n        }\n      }\n  }\n}\n\nclass IdentityColumnScalaSuite\n  extends IdentityColumnSuiteBase\n  with ScalaDDLTestUtils {\n\n  test(\"unsupported column type\") {\n    for (unsupportedType <- unsupportedDataTypes) {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        val ex = intercept[DeltaUnsupportedOperationException] {\n          createTable(\n            tblName,\n            Seq(\n              IdentityColumnSpec(GeneratedAlways, dataType = unsupportedType),\n              TestColumnSpec(colName = \"value\", dataType = StringType)\n            )\n          )\n        }\n        assert(ex.getErrorClass === \"DELTA_IDENTITY_COLUMNS_UNSUPPORTED_DATA_TYPE\")\n        assert(ex.getMessage.contains(\"is not supported for IDENTITY columns\"))\n      }\n    }\n  }\n\n  test(\"unsupported step\") {\n    for {\n      generatedAsIdentityType <- GeneratedAsIdentityType.values\n      startsWith <- Seq(Some(1L), None)\n    } {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        val ex = intercept[DeltaAnalysisException] {\n          createTableWithIdColAndIntValueCol(\n            tblName, generatedAsIdentityType, startsWith, incrementBy = Some(0))\n        }\n        assert(ex.getErrorClass === \"DELTA_IDENTITY_COLUMNS_ILLEGAL_STEP\")\n        assert(ex.getMessage.contains(\"step cannot be 0.\"))\n      }\n    }\n  }\n\n  test(\"cannot specify generatedAlwaysAs with identity columns\") {\n    def expectColumnBuilderError(f: => StructField): Unit = {\n      val ex = intercept[DeltaAnalysisException] {\n        f\n      }\n      assert(ex.getErrorClass === \"DELTA_IDENTITY_COLUMNS_WITH_GENERATED_EXPRESSION\")\n      ex.getMessage.contains(\n        \"Identity column cannot be specified with a generated column expression.\")\n    }\n    val generatedColumn = io.delta.tables.DeltaTable.columnBuilder(spark, \"id\")\n      .dataType(LongType)\n      .generatedAlwaysAs(\"id + 1\")\n\n    expectColumnBuilderError {\n      generatedColumn.generatedAlwaysAsIdentity().build()\n    }\n\n    expectColumnBuilderError {\n      generatedColumn.generatedByDefaultAsIdentity().build()\n    }\n  }\n}\n\nclass IdentityColumnScalaIdColumnMappingSuite\n  extends IdentityColumnSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableIdMode\n\nclass IdentityColumnScalaNameColumnMappingSuite\n  extends IdentityColumnSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableNameMode\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnSyncSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.GeneratedAsIdentityType.GeneratedByDefault\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\n\nimport org.apache.spark.sql.{AnalysisException, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types._\n\ncase class IdentityColumnTestTableRow(id: Long, value: String)\n\n/**\n * Identity Column test suite for the SYNC IDENTITY command.\n */\ntrait IdentityColumnSyncSuiteBase\n  extends IdentityColumnTestUtils {\n\n  import testImplicits._\n\n  /**\n   * Create and manage a table with a single identity column \"id\" generated by default and a single\n   * String \"value\" column.\n   */\n  private def withSimpleGeneratedByDefaultTable(\n      tblName: String, startsWith: Long, incrementBy: Long)(f: => Unit): Unit = {\n    withTable(tblName) {\n      createTable(\n        tblName,\n        Seq(\n          IdentityColumnSpec(\n            GeneratedByDefault,\n            startsWith = Some(startsWith),\n            incrementBy = Some(incrementBy)),\n          TestColumnSpec(colName = \"value\", dataType = StringType)\n        )\n      )\n\n      f\n    }\n  }\n\n  test(\"alter table sync identity on delta table\") {\n    val starts = Seq(-1, 1)\n    val steps = Seq(-3, 3)\n    val alterKeywords = Seq(\"ALTER\", \"CHANGE\")\n    for (start <- starts; step <- steps; alterKeyword <- alterKeywords) {\n      val tblName = getRandomTableName\n      withSimpleGeneratedByDefaultTable(tblName, start, step) {\n        // Test empty table.\n        val oldSchema = DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot.schema\n        sql(s\"ALTER TABLE $tblName $alterKeyword COLUMN id SYNC IDENTITY\")\n        assert(DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot.schema === oldSchema)\n\n        // Test a series of values that are not all following start and step configurations.\n        for (i <- start to (start + step * 10)) {\n          sql(s\"INSERT INTO $tblName VALUES($i, 'v')\")\n          sql(s\"ALTER TABLE $tblName $alterKeyword COLUMN id SYNC IDENTITY\")\n          val expected = start + (((i - start) + (step - 1)) / step) * step\n          val schema = DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot.schema\n          assert(schema(\"id\").metadata.getLong(DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK) ===\n            expected)\n        }\n      }\n    }\n  }\n\n  test(\"sync identity with values before start\") {\n    val tblName = getRandomTableName\n    withSimpleGeneratedByDefaultTable(tblName, startsWith = 100L, incrementBy = 2L) {\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n      assert(getHighWaterMark(deltaLog.update(), \"id\").isEmpty,\n        \"an empty table does not have an identity high watermark\")\n\n      sql(s\"INSERT INTO $tblName (id, value) VALUES (1, 'a'), (2, 'b'), (99, 'c')\")\n      assert(getHighWaterMark(deltaLog.update(), \"id\").isEmpty,\n        \"user inserted values do not update the high watermark\")\n\n      sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      assert(getHighWaterMark(deltaLog.update(), \"id\").isEmpty,\n        \"sync identity must not add a high watermark that is lower \" +\n        \"than the start value when it has positive increment\")\n\n      sql(s\"INSERT INTO $tblName (value) VALUES ('d'), ('e'), ('f')\")\n\n      val result = spark.read.table(tblName)\n        .as[IdentityColumnTestTableRow]\n        .collect()\n        .sortBy(_.id)\n      assert(result.length === 6)\n      assert(result.take(3) === Seq(IdentityColumnTestTableRow(1, \"a\"),\n                                    IdentityColumnTestTableRow(2, \"b\"),\n                                    IdentityColumnTestTableRow(99, \"c\")))\n      checkGeneratedIdentityValues(\n        sortedRows = result.takeRight(3),\n        start = 100,\n        step = 2,\n        expectedLowerBound = 100,\n        expectedUpperBound =\n          highWaterMark(DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot, \"id\"),\n        expectedDistinctCount = 3)\n    }\n  }\n\n  test(\"sync identity with start in table\") {\n    val tblName = getRandomTableName\n    withSimpleGeneratedByDefaultTable(tblName, startsWith = 100L, incrementBy = 2L) {\n      sql(s\"INSERT INTO $tblName (id, value) VALUES (1, 'a'), (2, 'b'), (100, 'c')\")\n      sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      sql(s\"INSERT INTO $tblName (value) VALUES ('d'), ('e'), ('f')\")\n\n      val result = spark.read.table(tblName)\n        .as[IdentityColumnTestTableRow]\n        .collect()\n        .sortBy(_.id)\n      assert(result.length === 6)\n      assert(result.take(3) === Seq(IdentityColumnTestTableRow(1, \"a\"),\n                                    IdentityColumnTestTableRow(2, \"b\"),\n                                    IdentityColumnTestTableRow(100, \"c\")))\n      checkGeneratedIdentityValues(\n        sortedRows = result.takeRight(3),\n        start = 100,\n        step = 2,\n        expectedLowerBound = 101,\n        expectedUpperBound =\n          highWaterMark(DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot, \"id\"),\n        expectedDistinctCount = 3)\n    }\n  }\n\n  test(\"sync identity with values before and after start\") {\n    val tblName = getRandomTableName\n    withSimpleGeneratedByDefaultTable(tblName, startsWith = 100L, incrementBy = 2L) {\n      sql(s\"INSERT INTO $tblName (id, value) VALUES (1, 'a'), (2, 'b'), (101, 'c')\")\n      sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      sql(s\"INSERT INTO $tblName (value) VALUES ('d'), ('e'), ('f')\")\n\n      val result = spark.read.table(tblName)\n        .as[IdentityColumnTestTableRow]\n        .collect()\n        .sortBy(_.id)\n      assert(result.length === 6)\n      assert(result.take(3) === Seq(IdentityColumnTestTableRow(1, \"a\"),\n                                    IdentityColumnTestTableRow(2, \"b\"),\n                                    IdentityColumnTestTableRow(101, \"c\")))\n      checkGeneratedIdentityValues(\n        sortedRows = result.takeRight(3),\n        start = 100,\n        step = 2,\n        expectedLowerBound = 102,\n        expectedUpperBound =\n          highWaterMark(DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot, \"id\"),\n        expectedDistinctCount = 3)\n    }\n  }\n\n  test(\"sync identity with values before start and negative step\") {\n    val tblName = getRandomTableName\n    withSimpleGeneratedByDefaultTable(tblName, startsWith = -10L, incrementBy = -2L) {\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n      assert(getHighWaterMark(deltaLog.update(), \"id\").isEmpty,\n        \"an empty table does not have an identity high watermark\")\n\n      sql(s\"INSERT INTO $tblName (id, value) VALUES (1, 'a'), (2, 'b'), (-9, 'c')\")\n      assert(getHighWaterMark(deltaLog.update(), \"id\").isEmpty,\n        \"user inserted values do not update the high watermark\")\n\n      sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      assert(getHighWaterMark(deltaLog.update(), \"id\").isEmpty,\n        \"sync identity must not add a high watermark that is higher \" +\n        \"than the start value when it has negative increment\")\n\n      sql(s\"INSERT INTO $tblName (value) VALUES ('d'), ('e'), ('f')\")\n\n      val result = spark.read.table(tblName)\n        .as[IdentityColumnTestTableRow]\n        .collect()\n        .sortBy(_.id)\n      assert(result.length === 6)\n      assert(result.takeRight(3) === Seq(IdentityColumnTestTableRow(-9, \"c\"),\n                                         IdentityColumnTestTableRow(1, \"a\"),\n                                         IdentityColumnTestTableRow(2, \"b\")))\n      checkGeneratedIdentityValues(\n        sortedRows = result.take(3),\n        start = -10,\n        step = -2,\n        expectedLowerBound =\n          highWaterMark(DeltaLog.forTable(spark, TableIdentifier(tblName)).snapshot, \"id\"),\n        expectedUpperBound = -10,\n        expectedDistinctCount = 3)\n    }\n  }\n\n  test(\"alter table sync identity - deleting high watermark rows followed by sync identity\" +\n    \" brings down the highWatermark only with a flag\") {\n    for (generatedAsIdentityType <- GeneratedAsIdentityType.values) {\n      val tblName = getRandomTableName\n      withTable(tblName) {\n        createTableWithIdColAndIntValueCol(tblName, generatedAsIdentityType, Some(1L), Some(10L))\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n        (0 to 4).foreach { v =>\n          sql(s\"INSERT INTO $tblName(value) VALUES ($v)\")\n        }\n\n        checkAnswer(sql(s\"SELECT max(id) FROM $tblName\"), Row(41))\n        sql(s\"DELETE FROM $tblName WHERE value IN (0, 3, 4)\")\n        assert(highWaterMark(deltaLog.snapshot, \"id\") === 41L)\n        // Unless this flag is enabled, the high watermark is not updated if it is lower\n        // than the previous high watermark.\n        withSQLConf(\n            DeltaSQLConf.DELTA_IDENTITY_ALLOW_SYNC_IDENTITY_TO_LOWER_HIGH_WATER_MARK.key -> \"false\"\n        ) {\n          sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n          assert(highWaterMark(deltaLog.update(), \"id\") === 41L)\n        }\n        // With the flag enabled, the high watermark is updated even if it is lower,\n        // than the previous high watermark, as long as it is higher than the defined start.\n        withSQLConf(\n            DeltaSQLConf.DELTA_IDENTITY_ALLOW_SYNC_IDENTITY_TO_LOWER_HIGH_WATER_MARK.key -> \"true\"\n        ) {\n          sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n          assert(highWaterMark(deltaLog.update(), \"id\") === 21L)\n        }\n        sql(s\"INSERT INTO $tblName(value) VALUES (8)\")\n        checkAnswer(sql(s\"SELECT max(id) FROM $tblName\"), Row(31))\n        checkAnswer(sql(s\"SELECT COUNT(DISTINCT id) == COUNT(*) FROM $tblName\"), Row(true))\n      }\n    }\n  }\n\n  test(\"alter table sync identity overflow error\") {\n    val tblName = getRandomTableName\n    withSimpleGeneratedByDefaultTable(tblName, startsWith = 1L, incrementBy = 10L) {\n      sql(s\"INSERT INTO $tblName VALUES (${Long.MaxValue}, 'a')\")\n      assertThrows[ArithmeticException] {\n        sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      }\n    }\n  }\n\n  test(\"alter table sync identity on non delta table error\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tblName (\n           |  id BIGINT,\n           |  value INT\n           |) USING parquet;\n           |\"\"\".stripMargin)\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      }\n      assert(ex.getMessage.contains(\n        \"ALTER TABLE ALTER COLUMN SYNC IDENTITY is only supported by Delta.\"))\n    }\n  }\n\n  test(\"alter table sync identity on non identity column error\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      createTable(\n        tblName,\n        Seq(\n          TestColumnSpec(colName = \"id\", dataType = LongType),\n          TestColumnSpec(colName = \"value\", dataType = IntegerType)\n        )\n      )\n      val ex = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      }\n      assert(ex.getMessage.contains(\n        \"ALTER TABLE ALTER COLUMN SYNC IDENTITY cannot be called on non IDENTITY columns.\"))\n    }\n  }\n\n  for (positiveStep <- DeltaTestUtils.BOOLEAN_DOMAIN)\n  test(s\"SYNC IDENTITY on table with bad water mark. positiveStep = $positiveStep\") {\n    val tblName = getRandomTableName\n    withTable(tblName) {\n      val incrementBy = if (positiveStep)  48 else -48\n      createTableWithIdColAndIntValueCol(\n        tblName,\n        GeneratedByDefault,\n        startsWith = Some(100),\n        incrementBy = Some(incrementBy)\n      )\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tblName))\n\n      // Insert data that don't respect the start.\n      if (positiveStep) {\n        sql(s\"INSERT INTO $tblName(id, value) VALUES (4, 4)\")\n      } else {\n        sql(s\"INSERT INTO $tblName(id, value) VALUES (196, 196)\")\n      }\n      forceBadWaterMark(deltaLog)\n      val badWaterMark = highWaterMark(deltaLog.snapshot, \"id\")\n\n      // Even though the candidate high water mark and the existing high water mark is invalid,\n      // we don't want to prevent updates to the high water mark as this would lead to us\n      // generating the same values over and over.\n      sql(s\"ALTER TABLE $tblName ALTER COLUMN id SYNC IDENTITY\")\n      val newHighWaterMark = highWaterMark(deltaLog.update(), colName = \"id\")\n      assert(newHighWaterMark !== badWaterMark,\n        \"Sync identity should update the high water mark based on the data.\")\n      if (positiveStep) {\n        assert(newHighWaterMark > badWaterMark)\n      } else {\n        assert(newHighWaterMark < badWaterMark)\n      }\n    }\n  }\n\n  for {\n    allowExplicitInsert <- DeltaTestUtils.BOOLEAN_DOMAIN\n    allowLoweringHighWatermarkForSyncIdentity <- DeltaTestUtils.BOOLEAN_DOMAIN\n  } test(s\"IdentityColumn.updateToValidHighWaterMark - allowExplicitInsert = $allowExplicitInsert,\"\n    + s\" allowLoweringHighWatermarkForSyncIdentity = $allowLoweringHighWatermarkForSyncIdentity\") {\n    /**\n     * Unit test for the updateToValidHighWaterMark function by creating a StructField with the\n     * specified start, step, and existing high water mark. After calling the function, we verify\n     * the StructField's metadata has the expect high water mark.\n     */\n    def testUpdateToValidHighWaterMark(\n        start: Long,\n        step: Long,\n        allowExplicitInsert: Boolean,\n        allowLoweringHighWatermarkForSyncIdentity: Boolean,\n        existingHighWaterMark: Option[Long],\n        candidateHighWaterMark: Long,\n        expectedHighWaterMark: Option[Long]): Unit = {\n      /** Creates a MetadataBuilder for Struct Metadata. */\n      def getMetadataBuilder(highWaterMarkOpt: Option[Long]): MetadataBuilder = {\n        var metadataBuilder = new MetadataBuilder()\n          .putLong(DeltaSourceUtils.IDENTITY_INFO_START, start)\n          .putLong(DeltaSourceUtils.IDENTITY_INFO_STEP, step)\n          .putBoolean(DeltaSourceUtils.IDENTITY_INFO_ALLOW_EXPLICIT_INSERT, allowExplicitInsert)\n\n        highWaterMarkOpt match {\n          case Some(oldHighWaterMark) =>\n            metadataBuilder = metadataBuilder.putLong(\n              DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK, oldHighWaterMark)\n          case None => ()\n        }\n        metadataBuilder\n      }\n\n      val initialStructField = StructField(\n        name = \"id\",\n        LongType,\n        nullable = false,\n        metadata = getMetadataBuilder(existingHighWaterMark).build())\n      val (updatedStructField, _) =\n        IdentityColumn.updateToValidHighWaterMark(\n          initialStructField, candidateHighWaterMark, allowLoweringHighWatermarkForSyncIdentity)\n      val expectedMetadata = getMetadataBuilder(expectedHighWaterMark).build()\n      assert(updatedStructField.metadata === expectedMetadata)\n    }\n\n    // existingHighWaterMark = None, positive step\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = 2L,\n      expectedHighWaterMark = Some(4L) // rounded up\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = 0L,\n      expectedHighWaterMark = None // below start\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = 1L,\n      expectedHighWaterMark = Some(1L) // equal to start\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = 7L,\n      expectedHighWaterMark = Some(7L) // respects start and step\n    )\n\n    // existingHighWaterMark = None, negative step\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = -1L,\n      expectedHighWaterMark = Some(-2L) // rounded up\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = 2L,\n      expectedHighWaterMark = None // above start\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = 1L,\n      expectedHighWaterMark = Some(1L) // equal to start\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = None,\n      candidateHighWaterMark = -5L,\n      expectedHighWaterMark = Some(-5L) // respects start and step\n    )\n\n    // existingHighWaterMark = Some, positive step\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(4L),\n      candidateHighWaterMark = 5L,\n      expectedHighWaterMark = Some(7L) // rounded up\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(4L),\n      candidateHighWaterMark = 0L,\n      expectedHighWaterMark = Some(4L) // below start, preserve existing high watermark\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-5L),\n      candidateHighWaterMark = -2L,\n      expectedHighWaterMark = Some(-2L) // below start, bad existing water mark, update to candidate\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-5L),\n      candidateHighWaterMark = 0L,\n      expectedHighWaterMark = Some(1L) // below start, bad existing water mark, update rounded up\n    )\n\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-5L),\n      candidateHighWaterMark = -9L,\n      expectedHighWaterMark = if (allowLoweringHighWatermarkForSyncIdentity) {\n        // below start, bad existing water mark, allow lowering, rounded down\n        Some(-8L)\n      } else {\n        // below start, bad existing water mark, keep existing\n        Some(-5L)\n      }\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(4L),\n      candidateHighWaterMark = 1L,\n      expectedHighWaterMark = if (allowLoweringHighWatermarkForSyncIdentity) {\n        Some(1L) // allow lowering\n      } else {\n        Some(4L) // below existing high watermark\n      }\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = 3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(4L),\n      candidateHighWaterMark = 7L,\n      expectedHighWaterMark = Some(7L) // respects start and step\n    )\n\n    // existingHighWaterMark = Some, negative step\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-2L),\n      candidateHighWaterMark = -3L,\n      expectedHighWaterMark = Some(-5L) // rounded up\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-2L),\n      candidateHighWaterMark = 2L,\n      expectedHighWaterMark = Some(-2L) // above start, preserve existing high water mark\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(7L),\n      candidateHighWaterMark = 4L,\n      expectedHighWaterMark = Some(4L) // above start, bad existing water mark, update to candidate\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(7L),\n      candidateHighWaterMark = 6L,\n      expectedHighWaterMark = Some(4L) // above start, bad existing water mark, update rounded down\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(7L),\n      candidateHighWaterMark = 11L,\n      expectedHighWaterMark = if (allowLoweringHighWatermarkForSyncIdentity) {\n        // above start, bad existing water mark, allow lowering, rounded down\n        Some(10L)\n      } else {\n        // above start, bad existing water mark, keep existing\n        Some(7L)\n      }\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-2L),\n      candidateHighWaterMark = 1L,\n      expectedHighWaterMark = if (allowLoweringHighWatermarkForSyncIdentity) {\n        Some(1L) // allow lowering\n      } else {\n        Some(-2L) // higher than high watermark\n      }\n    )\n    testUpdateToValidHighWaterMark(\n      start = 1L,\n      step = -3L,\n      allowExplicitInsert = allowExplicitInsert,\n      allowLoweringHighWatermarkForSyncIdentity = allowLoweringHighWatermarkForSyncIdentity,\n      existingHighWaterMark = Some(-2L),\n      candidateHighWaterMark = -5L,\n      expectedHighWaterMark = Some(-5L) // respects start and step\n    )\n  }\n\n  test(\"IdentityColumn.roundToNext\") {\n    val posStart = 7L\n    val negStart = -7L\n    val posLargeStart = Long.MaxValue - 10000\n    val negLargeStart = Long.MinValue + 10000\n    for (start <- Seq(posStart, negStart, posLargeStart, negLargeStart)) {\n      assert(IdentityColumn.roundToNext(start = start, step = 3L, value = start) === start)\n      assert(IdentityColumn.roundToNext(\n        start = start, step = 3L, value = start + 5L) === start + 6L)\n      assert(IdentityColumn.roundToNext(\n        start = start, step = 3L, value = start + 6L) === start + 6L)\n      assert(IdentityColumn.roundToNext(\n        start = start, step = 3L, value = start - 5L) === start - 3L) // bad watermark\n      assert(IdentityColumn.roundToNext(\n        start = start, step = 3L, value = start - 7L) === start - 6L) // bad watermark\n      assert(IdentityColumn.roundToNext(\n        start = start, step = 3L, value = start - 6L) === start - 6L) // bad watermark\n      assert(IdentityColumn.roundToNext(start = start, step = -3L, value = start) === start)\n      assert(IdentityColumn.roundToNext(\n        start = start, step = -3L, value = start - 5L) === start - 6L)\n      assert(IdentityColumn.roundToNext(\n        start = start, step = -3L, value = start - 6L) === start - 6L)\n      assert(IdentityColumn.roundToNext(\n        start = start, step = -3L, value = start + 5L) === start + 3L) // bad watermark\n      assert(IdentityColumn.roundToNext(\n        start = start, step = -3L, value = start + 7L) === start + 6L) // bad watermark\n      assert(IdentityColumn.roundToNext(\n        start = start, step = -3L, value = start + 6L) === start + 6L) // bad watermark\n    }\n  }\n}\n\nclass IdentityColumnSyncScalaSuite\n  extends IdentityColumnSyncSuiteBase\n  with ScalaDDLTestUtils\n\nclass IdentityColumnSyncScalaIdColumnMappingSuite\n  extends IdentityColumnSyncSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableIdMode\n\nclass IdentityColumnSyncScalaNameColumnMappingSuite\n  extends IdentityColumnSyncSuiteBase\n  with ScalaDDLTestUtils\n  with DeltaColumnMappingEnableNameMode\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/IdentityColumnTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta.GeneratedAsIdentityType.{GeneratedAlways, GeneratedAsIdentityType}\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.types._\n\ntrait IdentityColumnTestUtils\n  extends DDLTestUtils {\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaSQLConf.DELTA_IDENTITY_COLUMN_ENABLED.key, \"true\")\n  }\n\n  protected def getRandomTableName: String =\n    s\"identity_test_${UUID.randomUUID()}\".replaceAll(\"-\", \"_\")\n\n  protected val unsupportedDataTypes: Seq[DataType] = Seq(\n    BooleanType,\n    ByteType,\n    ShortType,\n    IntegerType,\n    DoubleType,\n    DateType,\n    TimestampType,\n    StringType,\n    BinaryType,\n    DecimalType(precision = 5, scale = 2),\n    YearMonthIntervalType(startField = 0, endField = 0) // Interval Year\n  )\n\n  def createTableWithIdColAndIntValueCol(\n      tableName: String,\n      generatedAsIdentityType: GeneratedAsIdentityType,\n      startsWith: Option[Long],\n      incrementBy: Option[Long],\n      tblProperties: Map[String, String] = Map.empty): Unit = {\n    createTable(\n      tableName,\n      Seq(\n        IdentityColumnSpec(\n          generatedAsIdentityType,\n          startsWith,\n          incrementBy\n        ),\n        TestColumnSpec(colName = \"value\", dataType = IntegerType)\n      ),\n      tblProperties = tblProperties\n    )\n  }\n\n  /**\n   * Creates and manages a simple identity column table with one other column \"value\" of type int\n   */\n  protected def withIdentityColumnTable(\n     generatedAsIdentityType: GeneratedAsIdentityType,\n     tableName: String)(f: => Unit): Unit = {\n    withTable(tableName) {\n      createTableWithIdColAndIntValueCol(tableName, generatedAsIdentityType, None, None)\n      f\n    }\n  }\n\n  protected def generateTableWithIdentityColumn(tableName: String, step: Long = 1): Unit = {\n    createTableWithIdColAndIntValueCol(\n      tableName,\n      GeneratedAlways,\n      startsWith = Some(0),\n      incrementBy = Some(step)\n    )\n\n    // Insert numRows and make sure they assigned sequential IDs\n    val numRows = 6\n    for (i <- 0 until numRows) {\n      sql(s\"INSERT INTO $tableName (value) VALUES ($i)\")\n    }\n    val expectedAnswer = for (i <- 0 until numRows) yield Row(i * step, i)\n    checkAnswer(sql(s\"SELECT * FROM $tableName ORDER BY value ASC\"), expectedAnswer)\n  }\n\n\n  /**\n   * Retrieves the high watermark information for the given `colName` in the metadata of\n   * given `snapshot`, if it's present. Returns None if the high watermark has not been set yet.\n   */\n  protected def getHighWaterMark(snapshot: Snapshot, colName: String): Option[Long] = {\n    val metadata = snapshot.schema(colName).metadata\n    if (metadata.contains(DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK)) {\n      Some(metadata.getLong(DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK))\n    } else {\n      None\n    }\n  }\n\n  /**\n   * Retrieves the high watermark information for the given `colName` in the metadata of\n   * given `snapshot`\n   */\n  protected def highWaterMark(snapshot: Snapshot, colName: String): Long = {\n    getHighWaterMark(snapshot, colName).get\n  }\n\n  /**\n   * Helper function to validate values of IDENTITY column `id` in table `tableName`. Returns the\n   * new high water mark. We use minValue and maxValue to filter column `value` to get the set of\n   * values we are checking in this batch.\n   */\n  protected def validateIdentity(\n      tableName: String,\n      expectedRowCount: Long,\n      start: Long,\n      step: Long,\n      minValue: Long,\n      maxValue: Long,\n      oldHighWaterMark: Long): Long = {\n    // Check row count.\n    checkAnswer(\n      sql(s\"SELECT COUNT(*) FROM $tableName\"),\n      Row(expectedRowCount)\n    )\n    // Check values are unique.\n    checkAnswer(\n      sql(s\"SELECT COUNT(DISTINCT id) FROM $tableName\"),\n      Row(expectedRowCount)\n    )\n    // Check values follow start and step configuration.\n    checkAnswer(\n      sql(s\"SELECT COUNT(*) FROM $tableName WHERE (id - $start) % $step != 0\"),\n      Row(0)\n    )\n    // Check values generated in this batch are after previous high water mark.\n    checkAnswer(\n      sql(\n        s\"\"\"\n           |SELECT COUNT(*) FROM $tableName\n           |  WHERE (value BETWEEN $minValue and $maxValue)\n           |    AND ((id - $oldHighWaterMark) / $step < 0)\n           |\"\"\".stripMargin),\n      Row(0)\n    )\n    // Update high water mark.\n    val func = if (step > 0) \"MAX\" else \"MIN\"\n    sql(s\"SELECT $func(id) FROM $tableName\").collect().head.getLong(0)\n  }\n\n  /**\n   * Helper function to validate generated identity values in sortedRows.\n   *\n   * @param sortedRows rows of the table sorted by id\n   * @param start start value of the identity column\n   * @param step step value of the identity column\n   * @param expectedLowerBound expected lower bound of the generated values\n   * @param expectedUpperBound expected upper bound of the generated values\n   * @param expectedDistinctCount expected distinct count of the generated values\n   */\n  protected def checkGeneratedIdentityValues(\n      sortedRows: Seq[IdentityColumnTestTableRow],\n      start: Long,\n      step: Long,\n      expectedLowerBound: Long,\n      expectedUpperBound: Long,\n      expectedDistinctCount: Long): Unit = {\n    assert(sortedRows.head.id >= expectedLowerBound)\n    for (row <- sortedRows) {\n      assert((row.id - start) % step === 0)\n    }\n    assert(sortedRows.last.id <= expectedUpperBound)\n    assert(sortedRows.map(_.id).distinct.size === expectedDistinctCount)\n  }\n\n  /** Force a bad high water mark on all identity columns in the table with a manual commit. */\n  def forceBadWaterMark(deltaLog: DeltaLog): Unit = {\n    deltaLog.withNewTransaction { txn =>\n      // Manually corrupt the high water mark.\n      val tblSchema = txn.snapshot.schema\n      val badTblSchema = StructType(tblSchema.map {\n        case tblIdCol if ColumnWithDefaultExprUtils.isIdentityColumn(tblIdCol) =>\n          val identityInfo = IdentityColumn.getIdentityInfo(tblIdCol)\n          // This bad water mark needs to follow the step and start,\n          // otherwise we fail the requirement in GenerateIdentityValues\n          val badWaterMark = identityInfo.start - identityInfo.step * 1000\n          val tblColMetadata = tblIdCol.metadata\n          val badMetadata = new MetadataBuilder().withMetadata(tblColMetadata)\n            .putLong(DeltaSourceUtils.IDENTITY_INFO_HIGHWATERMARK, badWaterMark).build()\n          tblIdCol.copy(metadata = badMetadata)\n        case f => f\n      })\n      val updatedMetadata = txn.snapshot.metadata.copy(schemaString = badTblSchema.json)\n      txn.updateMetadata(updatedMetadata, ignoreDefaultProperties = false)\n      txn.commit(Nil, DeltaOperations.ManualUpdate)\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ImplicitDMLCastingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.annotation.tailrec\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaExceptionTestUtils, DeltaSQLCommandTest}\n\nimport org.apache.spark.{SparkConf, SparkException, SparkThrowable}\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Tests for casts that are implicitly added in DML commands modifying Delta tables.\n * These casts are added to convert values to the schema of a table.\n * INSERT operations are excluded as they are covered by InsertSuite and InsertSuiteEdge.\n */\nabstract class ImplicitDMLCastingSuite extends QueryTest\n  with DeltaExceptionTestUtils\n  with DeltaSQLCommandTest {\n\n  /** Implement the actual test for a specific DML command in subclasses. */\n  protected def commandTest(sqlConfig: SqlConfiguration, testConfig: TestConfiguration): Unit\n\n  protected case class TestConfiguration(\n      sourceType: String,\n      sourceTypeInErrorMessage: String,\n      targetType: String,\n      targetTypeInErrorMessage: String,\n      validValue: String,\n      overflowValue: String,\n      // String because SparkArithmeticException is private and cannot be used for matching.\n      exceptionAnsiCast: String\n  ) {\n    override def toString: String = s\"sourceType: $sourceType, targetType: $targetType\"\n  }\n\n  protected case class SqlConfiguration(\n      followAnsiEnabled: Boolean,\n      ansiEnabled: Boolean,\n      storeAssignmentPolicy: SQLConf.StoreAssignmentPolicy.Value) {\n\n    def withSqlSettings(f: => Unit): Unit =\n      withSQLConf(\n        DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key\n          -> followAnsiEnabled.toString,\n        SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n        SQLConf.ANSI_ENABLED.key -> ansiEnabled.toString)(f)\n\n    override def toString: String =\n      s\"followAnsiEnabled: $followAnsiEnabled, ansiEnabled: $ansiEnabled,\" +\n        s\" storeAssignmentPolicy: $storeAssignmentPolicy\"\n  }\n\n  protected def expectLegacyCastingBehaviour(sqlConfig: SqlConfiguration): Boolean = {\n    (sqlConfig.followAnsiEnabled && !sqlConfig.ansiEnabled) ||\n      (!sqlConfig.followAnsiEnabled &&\n        sqlConfig.storeAssignmentPolicy == SQLConf.StoreAssignmentPolicy.LEGACY)\n  }\n\n  // Note that DATE to TIMESTAMP casts are not in this list as they always throw an error on\n  // overflow no matter if ANSI is enabled or not.\n  private val testConfigurations = Seq(\n    TestConfiguration(sourceType = \"INT\", sourceTypeInErrorMessage = \"INT\",\n      targetType = \"TINYINT\", targetTypeInErrorMessage = \"TINYINT\",\n      validValue = \"1\", overflowValue = Int.MaxValue.toString,\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"INT\", sourceTypeInErrorMessage = \"INT\",\n      targetType = \"SMALLINT\", targetTypeInErrorMessage = \"SMALLINT\",\n      validValue = \"1\", overflowValue = Int.MaxValue.toString,\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"BIGINT\", sourceTypeInErrorMessage = \"BIGINT\",\n      targetType = \"INT\", targetTypeInErrorMessage = \"INT\",\n      validValue = \"1\", overflowValue = Long.MaxValue.toString,\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"DOUBLE\", sourceTypeInErrorMessage = \"DOUBLE\",\n      targetType = \"BIGINT\", targetTypeInErrorMessage = \"BIGINT\",\n      validValue = \"1\", overflowValue = \"12345678901234567890D\",\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"BIGINT\", sourceTypeInErrorMessage = \"BIGINT\",\n      targetType = \"DECIMAL(7,2)\", targetTypeInErrorMessage = \"DECIMAL(7,2)\",\n      validValue = \"1\", overflowValue = Long.MaxValue.toString,\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"Struct<value:BIGINT>\", sourceTypeInErrorMessage = \"BIGINT\",\n      targetType = \"Struct<value:INT>\", targetTypeInErrorMessage = \"INT\",\n      validValue = \"named_struct('value', 1)\",\n      overflowValue = s\"named_struct('value', ${Long.MaxValue.toString})\",\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"ARRAY<BIGINT>\", sourceTypeInErrorMessage = \"ARRAY<BIGINT>\",\n      targetType = \"ARRAY<INT>\", targetTypeInErrorMessage = \"ARRAY<INT>\",\n      validValue = \"ARRAY(1)\", overflowValue = s\"ARRAY(${Long.MaxValue.toString})\",\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"STRING\", sourceTypeInErrorMessage = \"STRING\",\n      targetType = \"INT\", targetTypeInErrorMessage = \"INT\",\n      validValue = \"'1'\", overflowValue = s\"'${Long.MaxValue.toString}'\",\n      exceptionAnsiCast = \"SparkNumberFormatException\"),\n    TestConfiguration(sourceType = \"MAP<STRING, BIGINT>\",\n      sourceTypeInErrorMessage = \"MAP<STRING, BIGINT>\", targetType = \"MAP<STRING, INT>\",\n      targetTypeInErrorMessage = \"MAP<STRING, INT>\", validValue = \"map('abc', 1)\",\n      overflowValue = s\"map('abc', ${Long.MaxValue.toString})\",\n      exceptionAnsiCast = \"SparkArithmeticException\"),\n    TestConfiguration(sourceType = \"DECIMAL(3,1)\",\n      sourceTypeInErrorMessage = \"DECIMAL(3,1)\", targetType = \"DECIMAL(3,2)\",\n      targetTypeInErrorMessage = \"DECIMAL(3,2)\", validValue = \"CAST(1 AS DECIMAL(3,1))\",\n      overflowValue = s\"CAST(12.3 AS DECIMAL(3,1))\",\n      exceptionAnsiCast = \"SparkArithmeticException\")\n  )\n\n  /** Returns cast failure exception if present in the cause chain. None otherwise. */\n  @tailrec\n  private def castFailureCause(exception: Throwable): Option[Throwable] = {\n    exception match {\n      case arithmeticException: ArithmeticException => Some(arithmeticException)\n      case numberFormatException: NumberFormatException => Some(numberFormatException)\n      case _ if exception.getCause != null => castFailureCause(exception.getCause)\n      case _ => None\n    }\n  }\n\n  /**\n   * Validate that a custom error is throws in case ansi.enabled is false, or a different\n   * overflow error is case ansi.enabled is true.\n   */\n  protected def validateException(\n      exception: Throwable, sqlConfig: SqlConfiguration, testConfig: TestConfiguration): Unit = {\n    // Validate that the type of error matches the expected error type.\n    castFailureCause(exception) match {\n      case Some(failureCause) if sqlConfig.followAnsiEnabled =>\n        assert(sqlConfig.ansiEnabled)\n        assert(failureCause.toString.contains(testConfig.exceptionAnsiCast))\n\n        val sparkThrowable = failureCause.asInstanceOf[SparkThrowable]\n        assert(Seq(\n          \"CAST_OVERFLOW\",\n          \"NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION\",\n          \"CAST_INVALID_INPUT\"\n        ).contains(sparkThrowable.getErrorClass))\n      case Some(failureCause) if !sqlConfig.followAnsiEnabled =>\n        assert(sqlConfig.storeAssignmentPolicy === SQLConf.StoreAssignmentPolicy.ANSI)\n\n        val sparkThrowable = failureCause.asInstanceOf[SparkThrowable]\n        // Only arithmetic exceptions get a custom error message.\n        if (testConfig.exceptionAnsiCast == \"SparkArithmeticException\") {\n          assert(sparkThrowable.getErrorClass == \"DELTA_CAST_OVERFLOW_IN_TABLE_WRITE\")\n          assert(sparkThrowable.getMessageParameters ==\n            Map(\"sourceType\" -> (\"\\\"\" + testConfig.sourceTypeInErrorMessage + \"\\\"\"),\n                \"targetType\" -> (\"\\\"\" + testConfig.targetTypeInErrorMessage + \"\\\"\"),\n                \"columnName\" -> \"`value`\",\n                \"storeAssignmentPolicyFlag\" -> SQLConf.STORE_ASSIGNMENT_POLICY.key,\n                \"updateAndMergeCastingFollowsAnsiEnabledFlag\" ->\n                  DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key,\n                \"ansiEnabledFlag\" -> SQLConf.ANSI_ENABLED.key).asJava)\n        } else {\n          assert(sparkThrowable.getErrorClass == \"CAST_INVALID_INPUT\")\n          assert(sparkThrowable.getMessageParameters.get(\"sourceType\") == \"\\\"STRING\\\"\")\n        }\n      case None => assert(false, s\"No arithmetic exception thrown: $exception\")\n    }\n  }\n  for {\n    followAnsiEnabled <- BOOLEAN_DOMAIN\n    ansiEnabled <- BOOLEAN_DOMAIN\n    storeAssignmentPolicy <-\n      Seq(SQLConf.StoreAssignmentPolicy.LEGACY, SQLConf.StoreAssignmentPolicy.ANSI)\n    sqlConfiguration <-\n      Some(SqlConfiguration(followAnsiEnabled, ansiEnabled, storeAssignmentPolicy))\n    testConfiguration <- testConfigurations\n  } commandTest(sqlConfiguration, testConfiguration)\n\n\n  test(\"Details are part of the error message\") {\n    val sourceTableName = \"source_table_name\"\n    val sourceValueType = \"INT\"\n    val targetTableName = \"target_table_name\"\n    val targetValueType = \"LONG\"\n    val valueColumnName = \"value\"\n\n    withTable(sourceTableName, targetTableName) {\n      sql(s\"CREATE OR REPLACE TABLE $targetTableName(id LONG, $valueColumnName $sourceValueType) \" +\n        \"USING DELTA\")\n      sql(s\"CREATE OR REPLACE TABLE $sourceTableName(id LONG, $valueColumnName $targetValueType) \" +\n        \"USING DELTA\")\n      sql(s\"INSERT INTO $sourceTableName VALUES(0, 9223372036854775807)\")\n\n      val userFacingError = interceptWithUnwrapping[DeltaArithmeticException] {\n        sql(s\"\"\"MERGE INTO $targetTableName t\n               |USING $sourceTableName s\n               |ON s.id = t.id\n               |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin)\n      }\n      val expectedDetails =\n        Seq(\"DELTA_CAST_OVERFLOW_IN_TABLE_WRITE\", sourceValueType, valueColumnName)\n      for (detail <- expectedDetails) {\n        assert(userFacingError.toString.contains(detail))\n      }\n    }\n  }\n}\n\nclass ImplicitUpdateCastingSuite extends ImplicitDMLCastingSuite {\n\n  /** Test an UPDATE that requires to cast the update value that is part of the SET clause. */\n  override protected def commandTest(\n      sqlConfig: SqlConfiguration,\n      testConfig: TestConfiguration): Unit = {\n    val testName = s\"UPDATE overflow $testConfig $sqlConfig\"\n    test(testName) {\n      sqlConfig.withSqlSettings {\n        val tableName = \"overflowTable\"\n        withTable(tableName) {\n          sql(s\"\"\"CREATE TABLE $tableName USING DELTA\n                 |AS SELECT cast(${testConfig.validValue} AS ${testConfig.targetType}) AS value\n                 |\"\"\".stripMargin)\n          val updateCommand = s\"UPDATE $tableName SET value = ${testConfig.overflowValue}\"\n\n          if (expectLegacyCastingBehaviour(sqlConfig)) {\n            sql(updateCommand)\n          } else {\n            val exception = intercept[Throwable] {\n              sql(updateCommand)\n            }\n\n            validateException(exception, sqlConfig, testConfig)\n          }\n        }\n      }\n    }\n  }\n\n  for (preserveNullSourceStructs <- BOOLEAN_DOMAIN) {\n    test(s\"Implicit cast with NULL struct, preserveNullSourceStructs=$preserveNullSourceStructs\") {\n      withSQLConf(DeltaSQLConf.DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS.key\n          -> preserveNullSourceStructs.toString) {\n        val tableName = \"struct_null_expansion_test\"\n        withTable(tableName) {\n          sql(\n            s\"\"\"CREATE TABLE $tableName (\n               |  col1 STRUCT<x: INT>,\n               |  col2 STRUCT<x:INT, y: INT>\n               |) USING DELTA\"\"\".stripMargin)\n          sql(s\"INSERT INTO $tableName VALUES (NULL, NULL)\")\n\n          // col1: cast col1.x from INT to LONG\n          // col2: reorder col2.x and col2.y\n          sql(\n            s\"\"\"UPDATE $tableName SET\n               |  col1 = CAST(NULL AS STRUCT<x: LONG>),\n               |  col2 = CAST(NULL AS STRUCT<y: INT, x: INT>)\n               |\"\"\".stripMargin)\n\n          val expectedRow = if (preserveNullSourceStructs) {\n            // The entire structs should be null\n            Row(null, null)\n          } else {\n            // Results are structs with null fields\n            Row(Row(null), Row(null, null))\n          }\n\n          checkAnswer(spark.table(tableName), Seq(expectedRow))\n        }\n      }\n    }\n  }\n}\n\nclass ImplicitMergeCastingSuite extends ImplicitDMLCastingSuite {\n\n  /** Tests for MERGE with overflows cause by the different conditions. */\n  override protected def commandTest(\n      sqlConfig: SqlConfiguration,\n      testConfig: TestConfiguration): Unit = {\n    mergeTest(matchedCondition = s\"WHEN MATCHED THEN UPDATE SET t.value = s.value\",\n      sqlConfig, testConfig)\n\n    mergeTest(matchedCondition = s\"WHEN NOT MATCHED THEN INSERT *\", sqlConfig, testConfig)\n\n    mergeTest(matchedCondition =\n      s\"WHEN NOT MATCHED BY SOURCE THEN UPDATE SET t.value = ${testConfig.overflowValue}\",\n      sqlConfig, testConfig)\n  }\n\n  private def mergeTest(\n      matchedCondition: String,\n      sqlConfig: SqlConfiguration,\n      testConfig: TestConfiguration): Unit = {\n    val testName = s\"MERGE overflow in $matchedCondition $testConfig $sqlConfig\"\n    test(testName) {\n      sqlConfig.withSqlSettings {\n        val targetTableName = \"target_table\"\n        val sourceViewName = \"source_view\"\n        withTable(targetTableName) {\n          withTempView(sourceViewName) {\n            val numRows = 10\n            sql(s\"\"\"CREATE TABLE $targetTableName USING DELTA\n                   |AS SELECT col as key,\n                   |  cast(${testConfig.validValue} AS ${testConfig.targetType}) AS value\n                   |FROM explode(sequence(0, $numRows))\"\"\".stripMargin)\n            // The view maps the key space such that we get matched, not matched by source, and\n            // not match by target rows.\n            sql(s\"\"\"CREATE TEMPORARY VIEW $sourceViewName\n                   |AS SELECT key + ($numRows / 2) AS key,\n                   |  cast(${testConfig.overflowValue} AS ${testConfig.sourceType}) AS value\n                   |FROM $targetTableName\"\"\".stripMargin)\n            val mergeCommand = s\"\"\"MERGE INTO $targetTableName t\n                                  |USING $sourceViewName s\n                                  |ON s.key = t.key\n                                  |$matchedCondition\n                                  |\"\"\".stripMargin\n\n            if (expectLegacyCastingBehaviour(sqlConfig)) {\n              sql(mergeCommand)\n            } else {\n              val exception = intercept[Throwable] {\n                sql(mergeCommand)\n              }\n\n              validateException(exception, sqlConfig, testConfig)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\nclass ImplicitStreamingMergeCastingSuite extends ImplicitDMLCastingSuite {\n\n  /** A merge that is executed for each batch of a stream and has to cast values before insert. */\n  override protected def commandTest(\n      sqlConfig: SqlConfiguration,\n      testConfig: TestConfiguration): Unit = {\n    val testName = s\"Streaming MERGE overflow $testConfig $sqlConfig\"\n    test(testName) {\n      sqlConfig.withSqlSettings {\n        val targetTableName = \"target_table\"\n        val sourceTableName = \"source_table\"\n        withTable(sourceTableName, targetTableName) {\n          sql(s\"CREATE TABLE $targetTableName (key INT, value ${testConfig.targetType})\" +\n            \" USING DELTA\")\n          sql(s\"CREATE TABLE $sourceTableName (key INT, value ${testConfig.sourceType})\" +\n            \" USING DELTA\")\n\n          def upsertToDelta(microBatchOutputDF: DataFrame, batchId: Long): Unit = {\n            microBatchOutputDF.createOrReplaceTempView(\"micro_batch_output\")\n\n            microBatchOutputDF.sparkSession.sql(s\"\"\"MERGE INTO $targetTableName t\n                                                   |USING micro_batch_output s\n                                                   |ON s.key = t.key\n                                                   |WHEN NOT MATCHED THEN INSERT *\n                                                   |\"\"\".stripMargin)\n          }\n\n          val sourceStream = spark.readStream.table(sourceTableName)\n          val streamWriter =\n            sourceStream\n              .writeStream\n              .format(\"delta\")\n              .foreachBatch(upsertToDelta _)\n              .outputMode(\"update\")\n              .start()\n\n          sql(s\"INSERT INTO $sourceTableName(key, value) VALUES(0, ${testConfig.overflowValue})\")\n\n          if (expectLegacyCastingBehaviour(sqlConfig)) {\n            streamWriter.processAllAvailable()\n          } else {\n            val exception = intercept[Throwable] {\n              streamWriter.processAllAvailable()\n            }\n\n            validateException(exception, sqlConfig, testConfig)\n          }\n        }\n      }\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/InCommitTimestampSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\n\nimport scala.collection.JavaConverters._\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{Action, CommitInfo}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedCommitCoordinatorProvider, CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite, CommitCoordinatorProvider, CommitCoordinatorUtilBase, TrackingInMemoryCommitCoordinatorBuilder}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DateTimeUtils, DeltaCommitFileProvider,\n  FileNames, JsonUtils, TimestampFormatter}\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.StreamTest\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ManualClock\n\nclass InCommitTimestampSuite\n  extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils\n    with DeltaTestUtilsBase\n    with CatalogOwnedTestBaseSuite\n    with StreamTest {\n  import InCommitTimestampTestUtils._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey, \"true\")\n    spark.conf.set(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key, \"true\")\n    spark.conf.set(DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key, \"true\")\n  }\n\n  /**\n   * Create a delta table with 10 rows and a single commit with an AddFile.\n   * This is used to create the initial state of the delta table for testing.\n   *\n   * @param tableName The name of the delta table.\n   * @return The DeltaLog for the created table.\n   */\n  private def createInitialTable(tableName: String): DeltaLog = {\n    spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n    deltaLog.startTransaction().commit(Seq(createTestAddFile(\"1\")), ManualUpdate)\n    deltaLog\n  }\n\n  /**\n   * Construct a delta log w/ a specific manual clock.\n   *\n   * @param tableName The name of the Delta table.\n   * @param clock The manual clock to use for the DeltaLog.\n   */\n  private def getDeltaLogWithClock(tableName: String, clock: ManualClock): DeltaLog = {\n    // Construct a delta log by name first.\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n    // Get the data path.\n    val dataPath = deltaLog.dataPath\n    // Clear the cache to ensure that a fresh DeltaLog is created with the provided clock.\n    DeltaLog.clearCache()\n    // Create a new DeltaLog with the clock and the log path.\n    DeltaLog.forTable(spark, dataPath, clock)\n  }\n\n  test(\"Enable ICT on commit 0\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val ver0Snapshot = deltaLog.snapshot\n      assert(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(ver0Snapshot.metadata))\n      assert(ver0Snapshot.timestamp == getInCommitTimestamp(deltaLog, 0))\n    }\n  }\n\n  // Catalog Owned will also automatically enable ICT.\n  testWithDefaultCommitCoordinatorUnset(\n    \"Create a non-inCommitTimestamp table and then enable timestamp\") {\n    withSQLConf(\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> false.toString\n    ) {\n      withTempTable(createTable = false) { tableName =>\n        spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n        spark.sql(s\"INSERT INTO $tableName VALUES 10\")\n        val ver1Snapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot\n        // File timestamp should be the same as snapshot.getTimestamp when inCommitTimestamp is not\n        // enabled\n        assert(\n          ver1Snapshot.logSegment.lastCommitFileModificationTimestamp == ver1Snapshot.timestamp)\n\n        spark.sql(s\"ALTER TABLE $tableName \" +\n          s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n\n        val ver2Snapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot\n        // File timestamp should be different from snapshot.getTimestamp when inCommitTimestamp is\n        // enabled\n        assert(ver2Snapshot.timestamp == getInCommitTimestamp(ver2Snapshot.deltaLog, version = 2))\n\n        assert(ver2Snapshot.timestamp > ver1Snapshot.timestamp)\n\n        spark.sql(s\"INSERT INTO $tableName VALUES 11\")\n\n        val ver3Snapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot\n\n        assert(ver3Snapshot.timestamp > ver2Snapshot.timestamp)\n      }\n    }\n  }\n\n  test(\"InCommitTimestamps are monotonic even when the clock is skewed\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val deltaLog = getDeltaLogWithClock(tableName, clock)\n      // Move backwards in time.\n      deltaLog.startTransaction().commit(Seq(createTestAddFile(\"1\")), ManualUpdate)\n      val ver1Timestamp = deltaLog.snapshot.timestamp\n      clock.setTime(startTime - 10000)\n      deltaLog.startTransaction().commit(Seq(createTestAddFile(\"2\")), ManualUpdate)\n      val ver2Timestamp = deltaLog.snapshot.timestamp\n      assert(ver2Timestamp > ver1Timestamp)\n    }\n  }\n\n  test(\"Conflict resolution of timestamps\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      // Clear the cache to ensure that a new DeltaLog is created with the new clock.\n      val deltaLog = getDeltaLogWithClock(tableName, clock)\n      val txn1 = deltaLog.startTransaction()\n      clock.setTime(startTime)\n      deltaLog.startTransaction().commit(Seq(createTestAddFile(\"1\")), ManualUpdate)\n      // Move time backwards for the conflicting commit.\n      clock.setTime(startTime - 10000)\n      val usageRecords = Log4jUsageLogger.track {\n        txn1.commit(Seq(createTestAddFile(\"2\")), ManualUpdate)\n      }\n      // Make sure that this transaction resulted in a conflict.\n      assert(filterUsageRecords(usageRecords, \"delta.commit.retry\").length == 1)\n      assert(getInCommitTimestamp(deltaLog, 2) > getInCommitTimestamp(deltaLog, 1))\n    }\n  }\n\n  for (useCommitLarge <- BOOLEAN_DOMAIN)\n  test(\"txn.commit should use clock.currentTimeMillis() for ICT\" +\n    s\" [useCommitLarge: $useCommitLarge]\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(2).write.format(\"delta\").saveAsTable(tableName)\n      val expectedCommit1Time = System.currentTimeMillis()\n      val clock = new ManualClock(expectedCommit1Time)\n      val deltaLog = getDeltaLogWithClock(tableName, clock)\n      val ver0Snapshot = deltaLog.snapshot\n      assert(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(ver0Snapshot.metadata))\n      val usageRecords = Log4jUsageLogger.track {\n        if (useCommitLarge) {\n          deltaLog.startTransaction().commitLarge(\n            spark,\n            Seq(createTestAddFile(\"1\")).toIterator,\n            newProtocolOpt = None,\n            DeltaOperations.ManualUpdate,\n            context = Map.empty,\n            metrics = Map.empty)\n        } else {\n          deltaLog.startTransaction().commit(\n            Seq(createTestAddFile(\"1\")),\n            DeltaOperations.ManualUpdate,\n            tags = Map.empty\n          )\n        }\n      }\n      val ver1Snapshot = deltaLog.snapshot\n      val retrievedTimestamp = getInCommitTimestamp(deltaLog, version = 1)\n      assert(ver1Snapshot.timestamp == retrievedTimestamp)\n      assert(ver1Snapshot.timestamp == expectedCommit1Time)\n      val expectedOpType = if (useCommitLarge) \"delta.commit.large\" else \"delta.commit\"\n      assert(filterUsageRecords(usageRecords, expectedOpType).length == 1)\n    }\n  }\n\n  test(\"Missing CommitInfo should result in a DELTA_MISSING_COMMIT_INFO exception\") {\n    // Make sure that we don't retrieve the time from the CRC.\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        val deltaLog = createInitialTable(tableName)\n        // Remove CommitInfo from the commit.\n        val commit1Path = DeltaCommitFileProvider(deltaLog.unsafeVolatileSnapshot).deltaFile(1)\n        val actions = deltaLog.store.readAsIterator(commit1Path, deltaLog.newDeltaHadoopConf())\n        val actionsWithoutCommitInfo =\n          actions.filterNot(Action.fromJson(_).isInstanceOf[CommitInfo])\n        deltaLog.store.write(\n          commit1Path,\n          actionsWithoutCommitInfo,\n          overwrite = true,\n          deltaLog.newDeltaHadoopConf())\n\n        DeltaLog.clearCache()\n        val latestSnapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot\n        val e = intercept[DeltaIllegalStateException] {\n          latestSnapshot.timestamp\n        }\n        checkError(\n          e,\n          \"DELTA_MISSING_COMMIT_INFO\",\n          parameters = Map(\n            \"featureName\" -> InCommitTimestampTableFeature.name,\n            \"version\" -> \"1\"))\n      }\n    }\n  }\n\n  test(\"Missing CommitInfo.commitTimestamp should result in a \" +\n    \"DELTA_MISSING_COMMIT_TIMESTAMP exception\") {\n    // Make sure that we don't retrieve the time from the CRC.\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        val deltaLog = createInitialTable(tableName)\n        // Remove CommitInfo.commitTimestamp from the commit.\n        val commit1Path = DeltaCommitFileProvider(deltaLog.unsafeVolatileSnapshot).deltaFile(1)\n        val actions = deltaLog.store.readAsIterator(\n          commit1Path,\n          deltaLog.newDeltaHadoopConf()).toList\n        val actionsWithoutCommitInfoCommitTimestamp =\n          actions.map(Action.fromJson).map {\n            case ci: CommitInfo =>\n              ci.copy(inCommitTimestamp = None).json\n            case other =>\n              other.json\n          }.toIterator\n        deltaLog.store.write(\n          commit1Path,\n          actionsWithoutCommitInfoCommitTimestamp,\n          overwrite = true,\n          deltaLog.newDeltaHadoopConf())\n\n        DeltaLog.clearCache()\n        val latestSnapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot\n        val e = intercept[DeltaIllegalStateException] {\n          latestSnapshot.timestamp\n        }\n        checkError(\n          e,\n          \"DELTA_MISSING_COMMIT_TIMESTAMP\",\n          parameters = Map(\"featureName\" -> InCommitTimestampTableFeature.name, \"version\" -> \"1\"))\n      }\n    }\n  }\n\n  test(\"InCommitTimestamp is equal to snapshot.timestamp\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val ver0Snapshot = deltaLog.snapshot\n\n      assert(ver0Snapshot.timestamp == getInCommitTimestamp(deltaLog, 0))\n    }\n  }\n\n  test(\"CREATE OR REPLACE should not disable ICT\") {\n    withoutDefaultCCTableFeature {\n      withSQLConf(\n        DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> false.toString\n      ) {\n        withTempDir { tempDir =>\n          spark.range(10).write.format(\"delta\").save(tempDir.getAbsolutePath)\n          spark.sql(\n            s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` \" +\n              s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n\n          spark.sql(\n            s\"CREATE OR REPLACE TABLE delta.`${tempDir.getAbsolutePath}` (id long) USING delta\")\n\n          val deltaLogAfterCreateOrReplace =\n            DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n          val snapshot = deltaLogAfterCreateOrReplace.snapshot\n          assert(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata))\n          assert(snapshot.timestamp ==\n            getInCommitTimestamp(deltaLogAfterCreateOrReplace, snapshot.version))\n        }\n      }\n    }\n  }\n\n  test(\"Enablement tracking properties should not be added if ICT is enabled on commit 0\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val ver0Snapshot = deltaLog.snapshot\n\n      val observedEnablementTimestamp =\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(ver0Snapshot.metadata)\n      val observedEnablementVersion =\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(ver0Snapshot.metadata)\n      assert(observedEnablementTimestamp.isEmpty)\n      assert(observedEnablementVersion.isEmpty)\n    }\n  }\n\n  // Catalog Owned will also automatically enable ICT.\n  testWithDefaultCommitCoordinatorUnset(\n    \"Enablement tracking works when ICT is enabled post commit 0\") {\n    withSQLConf(\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> false.toString\n    ) {\n      withTempTable(createTable = false) { tableName =>\n        spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n        spark.sql(s\"INSERT INTO $tableName VALUES 10\")\n\n        spark.sql(\n          s\"ALTER TABLE $tableName \" +\n            s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val ver2Snapshot = deltaLog.snapshot\n        val observedEnablementTimestamp =\n          DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(ver2Snapshot.metadata)\n        val observedEnablementVersion =\n          DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(ver2Snapshot.metadata)\n        assert(observedEnablementTimestamp.isDefined)\n        assert(observedEnablementVersion.isDefined)\n        assert(observedEnablementTimestamp.get == getInCommitTimestamp(deltaLog, version = 2))\n        assert(observedEnablementVersion.get == 2)\n      }\n    }\n  }\n\n  // Catalog Owned will also automatically enable ICT.\n  testWithDefaultCommitCoordinatorUnset(\"Conflict resolution of enablement version\") {\n    withSQLConf(\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> false.toString\n    ) {\n      withTempTable(createTable = false) { tableName =>\n        spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n        spark.sql(s\"INSERT INTO $tableName VALUES 10\")\n        val startTime = System.currentTimeMillis()\n        val clock = new ManualClock(startTime)\n        val deltaLog = getDeltaLogWithClock(tableName, clock)\n        val snapshot = deltaLog.snapshot\n        val txn1 = deltaLog.startTransaction()\n        clock.setTime(startTime)\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(\"1\")), ManualUpdate)\n        val ictEnablementMetadataConfig = snapshot.metadata.configuration ++ Map(\n          DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"true\")\n        val ictEnablementMetadata =\n          snapshot.metadata.copy(configuration = ictEnablementMetadataConfig)\n        val usageRecords = Log4jUsageLogger.track {\n          txn1.commit(Seq(ictEnablementMetadata), ManualUpdate)\n        }\n        val ver3Snapshot = deltaLog.update()\n        val observedEnablementTimestamp =\n          DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(ver3Snapshot.metadata)\n        val observedEnablementVersion =\n          DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(ver3Snapshot.metadata)\n        // Make sure that this transaction resulted in a conflict.\n        assert(filterUsageRecords(usageRecords, \"delta.commit.retry\").length == 1)\n        assert(observedEnablementTimestamp.get == getInCommitTimestamp(deltaLog, version = 3))\n        assert(observedEnablementVersion.get == 3)\n      }\n    }\n  }\n\n  // Catalog Owned will also automatically enable ICT.\n  testWithDefaultCommitCoordinatorUnset(\n    \"commitLarge should correctly set the enablement tracking properties\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(2).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val ver0Snapshot = deltaLog.snapshot\n      assert(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(ver0Snapshot.metadata))\n      // Disable ICT in version 1.\n      spark.sql(\n        s\"ALTER TABLE $tableName \" +\n          s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'false')\")\n      assert(!DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(deltaLog.update().metadata))\n\n      // Use a restore command to return the table state to version 0.\n      // This should internally invoke commitLarge and the enablement tracking properties should be\n      // updated correctly.\n      val usageRecords = Log4jUsageLogger.track {\n        spark.sql(s\"RESTORE TABLE $tableName TO VERSION AS OF 0\")\n      }\n      val ver2Snapshot = deltaLog.update()\n      assert(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(ver2Snapshot.metadata))\n      val observedEnablementTimestamp =\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(ver2Snapshot.metadata)\n      val observedEnablementVersion =\n        DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(ver2Snapshot.metadata)\n      assert(filterUsageRecords(usageRecords, \"delta.commit.large\").length == 1)\n      assert(observedEnablementTimestamp.isDefined)\n      assert(observedEnablementVersion.isDefined)\n      assert(observedEnablementTimestamp.get == getInCommitTimestamp(deltaLog, version = 2))\n      assert(observedEnablementVersion.get == 2)\n    }\n  }\n\n  test(\"snapshot.timestamp should be read from the CRC\") {\n    withTempTable(createTable = false) { tableName =>\n      var deltaLog: DeltaLog = null\n      var timestamp = -1L\n      val usageRecords = Log4jUsageLogger.track {\n        spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n        DeltaLog.clearCache() // Clear the post-commit snapshot from the cache.\n        deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n        assert(fs.exists(FileNames.checksumFile(deltaLog.logPath, 0)))\n        timestamp = deltaLog.snapshot.timestamp\n      }\n      assert(timestamp == getInCommitTimestamp(deltaLog, 0))\n      // No explicit read.\n      assert(filterUsageRecords(usageRecords, \"delta.inCommitTimestamp.read\").isEmpty)\n    }\n  }\n\n  test(\"postCommitSnapshot.timestamp should be populated by protocolMetadataAndICTReconstruction \" +\n     \"when the table has no checkpoints\") {\n    // Make sure that we don't retrieve the time from the CRC.\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        var deltaLog: DeltaLog = null\n        var timestamp = -1L\n        spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n        DeltaLog.clearCache()\n        val usageRecords = Log4jUsageLogger.track {\n          deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          timestamp = deltaLog.snapshot.timestamp\n        }\n        assert(timestamp == getInCommitTimestamp(deltaLog, 0))\n        // No explicit read.\n        assert(filterUsageRecords(usageRecords, \"delta.inCommitTimestamp.read\").isEmpty)\n      }\n    }\n  }\n\n  test(\"snapshot.timestamp should be populated by protocolMetadataAndICTReconstruction \" +\n     \"during cold reads of checkpoints + deltas\") {\n    // Make sure that we don't retrieve the time from the CRC.\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        var deltaLog: DeltaLog = null\n        var timestamp = -1L\n        spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n        deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        deltaLog.createCheckpointAtVersion(0)\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(\"c1\")), ManualUpdate)\n\n        val usageRecords = Log4jUsageLogger.track {\n          DeltaLog.clearCache() // Clear the post-commit snapshot from the cache.\n          deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          timestamp = deltaLog.snapshot.timestamp\n        }\n        assert(deltaLog.snapshot.checkpointProvider.version == 0)\n        assert(deltaLog.snapshot.version == 1)\n        assert(timestamp == getInCommitTimestamp(deltaLog, 1))\n        // No explicit read.\n        assert(filterUsageRecords(usageRecords, \"delta.inCommitTimestamp.read\").isEmpty)\n      }\n    }\n  }\n\n  test(\"snapshot.timestamp cannot be populated by protocolMetadataAndICTReconstruction \" +\n     \"during cold reads of checkpoints\") {\n    // Make sure that we don't retrieve the time from the CRC.\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        var deltaLog: DeltaLog = null\n        var timestamp = -1L\n        spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n        DeltaLog.forTable(spark, TableIdentifier(tableName)).createCheckpointAtVersion(0)\n        val usageRecords = Log4jUsageLogger.track {\n          DeltaLog.clearCache() // Clear the post-commit snapshot from the cache.\n          deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n          timestamp = deltaLog.snapshot.timestamp\n        }\n        assert(deltaLog.snapshot.checkpointProvider.version == 0)\n        assert(timestamp == getInCommitTimestamp(deltaLog, 0))\n        assert(filterUsageRecords(usageRecords, \"delta.inCommitTimestamp.read\").length == 1)\n      }\n    }\n  }\n\n  test(\"snapshot.timestamp is read from file when CRC doesn't have ICT and \" +\n    \"the latest version has a checkpoint\") {\n    withTempTable(createTable = false) { tableName =>\n      var deltaLog: DeltaLog = null\n      var timestamp = -1L\n      spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n      deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      deltaLog.createCheckpointAtVersion(0)\n      // Remove the ICT from the CRC.\n      InCommitTimestampTestUtils.overwriteICTInCrc(deltaLog, 0, None)\n      val usageRecords = Log4jUsageLogger.track {\n        DeltaLog.clearCache() // Clear the post-commit snapshot from the cache.\n        deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        timestamp = deltaLog.snapshot.timestamp\n      }\n      assert(deltaLog.snapshot.checkpointProvider.version == 0)\n      assert(timestamp == getInCommitTimestamp(deltaLog, 0))\n      val ictReadLog = filterUsageRecords(usageRecords, \"delta.inCommitTimestamp.read\").head\n      val blob = JsonUtils.fromJson[Map[String, String]](ictReadLog.blob)\n      assert(blob(\"version\") == \"0\")\n      assert(blob(\"checkpointVersion\") == \"0\")\n      assert(blob(\"isCRCPresent\") == \"true\")\n    }\n  }\n\n  test(\"Exceptions during ICT reads from file should be logged\") {\n    // Make sure that we don't retrieve the time from the CRC.\n    withSQLConf(DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\") {\n      withTempTable(createTable = false) { tableName =>\n        val deltaLog = createInitialTable(tableName)\n        // Remove CommitInfo from the commit.\n        val commit1Path = DeltaCommitFileProvider(deltaLog.unsafeVolatileSnapshot).deltaFile(1)\n        val actions = deltaLog.store.readAsIterator(commit1Path, deltaLog.newDeltaHadoopConf())\n        val actionsWithoutCommitInfo =\n          actions.filterNot(Action.fromJson(_).isInstanceOf[CommitInfo])\n        deltaLog.store.write(\n          commit1Path,\n          actionsWithoutCommitInfo,\n          overwrite = true,\n          deltaLog.newDeltaHadoopConf())\n\n        DeltaLog.clearCache()\n        val latestSnapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot\n        val usageRecords = Log4jUsageLogger.track {\n          try {\n            latestSnapshot.timestamp\n          } catch {\n            case _ : DeltaIllegalStateException => ()\n          }\n        }\n        val ictReadLog = filterUsageRecords(usageRecords, \"delta.inCommitTimestamp.read\").head\n        val blob = JsonUtils.fromJson[Map[String, String]](ictReadLog.blob)\n        assert(blob(\"version\") == \"1\")\n        assert(blob(\"checkpointVersion\") == \"-1\")\n        assert(blob(\"exceptionMessage\").startsWith(\"[DELTA_MISSING_COMMIT_INFO]\"))\n        assert(blob(\"exceptionStackTrace\").contains(Snapshot.getClass.getName.stripSuffix(\"$\")))\n      }\n    }\n  }\n\n  test(\"DeltaHistoryManager.getActiveCommitAtTimeFromICTRange\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val deltaLog = getDeltaLogWithClock(tableName, clock)\n      val commitTimeDelta = 10\n      val numberAdditionalCommits = 25\n      assert(clock eq deltaLog.clock)\n      for (i <- 1 to numberAdditionalCommits) {\n        clock.setTime(startTime + i*commitTimeDelta)\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n      }\n      val deltaCommitFileProvider = DeltaCommitFileProvider(deltaLog.update())\n      val commit0 = DeltaHistoryManager.Commit(0, getInCommitTimestamp(deltaLog, 0))\n      var commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          startTime + commitTimeDelta*11,\n          startCommit = commit0,\n          numberAdditionalCommits + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit.version == 11)\n      assert(commit.version == deltaLog.history.getActiveCommitAtTime(\n        startTime + commitTimeDelta*11, true).version)\n\n      // Search for commit 11 when the timestamp is not an exact match.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          startTime + commitTimeDelta * 11 + 5,\n          startCommit = commit0,\n          numberAdditionalCommits + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit.version == 11)\n\n      // Search for the last commit.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          startTime + commitTimeDelta*25,\n          startCommit = commit0,\n          numberAdditionalCommits + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit.version == 25)\n      // Search for the first commit.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          commit0.timestamp,\n          startCommit = commit0,\n          numberAdditionalCommits + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit.version == 0)\n    }\n  }\n\n  test(\"DeltaHistoryManager.getActiveCommitAtTimeFromICTRange --- \" +\n    \"search for a timestamp after the last commit\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val deltaLog = getDeltaLogWithClock(tableName, clock)\n      val commitTimeDelta = 10\n      val numberAdditionalCommits = 2\n      assert(clock eq deltaLog.clock)\n      for (i <- 1 to numberAdditionalCommits) {\n        clock.setTime(startTime + i * commitTimeDelta)\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n      }\n      val commit = deltaLog.history.getActiveCommitAtTime(\n        new Timestamp(startTime + commitTimeDelta * (numberAdditionalCommits + 1)),\n        catalogTableOpt = None,\n        canReturnLastCommit = true)\n      assert(commit.version == numberAdditionalCommits)\n\n      // Searching beyond the last commit should throw an error\n      // when canReturnLastCommit is false.\n      val e = intercept[DeltaErrors.TemporallyUnstableInputException] {\n        deltaLog.history.getActiveCommitAtTime(\n          new Timestamp(startTime + commitTimeDelta * (numberAdditionalCommits + 1)),\n          catalogTableOpt = None,\n          canReturnLastCommit = false)\n      }\n      assert(e.getMessage.contains(\"The provided timestamp\") && e.getMessage.contains(\"is after\"))\n    }\n  }\n\n  // Catalog Owned will also automatically enable ICT.\n  testWithDefaultCommitCoordinatorUnset(\"DeltaHistoryManager.getActiveCommitAtTime: \" +\n    \"works correctly when the history has both ICT and non-ICT commits\") {\n    withSQLConf(\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> false.toString) {\n      withTempTable(createTable = false) { tableName =>\n        spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n        val numNonICTCommits = 6\n        val numICTCommits = 5\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        for (i <- 1 to (numNonICTCommits-1)) {\n          deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n        }\n\n        // Enable ICT.\n        spark.sql(\n          s\"ALTER TABLE $tableName \" +\n            s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n\n        for (i <- 1 to (numICTCommits-1)) {\n          deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n        }\n        val currentVersion = deltaLog.update().version\n        for (version <- 0L to currentVersion) {\n          val ts = deltaLog.getSnapshotAt(version).timestamp\n          // Search for the exact timestamp.\n          var commit = deltaLog.history.getActiveCommitAtTime(ts, true)\n          assert(commit.version == version)\n\n          // Search using a timestamp just before the current timestamp.\n          commit = deltaLog.history.getActiveCommitAtTime(\n            new Timestamp(ts-1),\n            catalogTableOpt = None,\n            canReturnLastCommit = true,\n            canReturnEarliestCommit = true)\n          val expectedVersion = if (version == 0) 0 else version - 1\n          assert(commit.version == expectedVersion)\n\n          // Search using a timestamp just after the current timestamp.\n          commit = deltaLog.history.getActiveCommitAtTime(ts + 1, true)\n          assert(commit.version == version)\n        }\n\n        val enablementCommit =\n          InCommitTimestampUtils.getValidatedICTEnablementInfo(deltaLog.snapshot.metadata).get\n        // Create a checkpoint before deleting commits.\n        deltaLog.createCheckpointAtVersion(enablementCommit.version + 2)\n\n        // Search for an ICT commit when all the ICT commits leading up to and including it are\n        // absent.\n        val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n        // Search for the commit immediately after the enablement commit.\n        val searchTimestamp = getInCommitTimestamp(deltaLog, enablementCommit.version + 1)\n        val minTimestamp = getInCommitTimestamp(deltaLog, enablementCommit.version + 2)\n        val timestampFormatter = TimestampFormatter(\n          DateTimeUtils.getTimeZone(SQLConf.get.sessionLocalTimeZone))\n        val minTimestampString = DateTimeUtils.timestampToString(\n          timestampFormatter, DateTimeUtils.fromJavaTimestamp(new Timestamp(minTimestamp)))\n        // Delete the first two ICT commits before performing the search.\n        (enablementCommit.version to enablementCommit.version + 1).foreach { version =>\n          fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, version), false)\n        }\n        val e = intercept[DeltaErrors.TimestampEarlierThanCommitRetentionException] {\n          deltaLog.history.getActiveCommitAtTime(searchTimestamp, false)\n        }\n        checkError(\n          e,\n          \"DELTA_TIMESTAMP_EARLIER_THAN_COMMIT_RETENTION\",\n          sqlState = \"42816\",\n          parameters = Map(\n            \"userTimestamp\" -> new Timestamp(searchTimestamp).toString,\n            \"commitTs\" -> new Timestamp(minTimestamp).toString,\n            \"timestampString\" -> minTimestampString)\n        )\n        assert(\n          e.getMessage.contains(\"The provided timestamp\") && e.getMessage.contains(\"is before\"))\n\n        // Search for a non-ICT commit when all the non-ICT commits are missing.\n        // Delete all the non-ICT commits.\n        (0L until numNonICTCommits).foreach { version =>\n          fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, version), false)\n        }\n        // The earliest available commit is at enablementCommit.version + 2 because we deleted\n        // the first two ICT commits earlier.\n        val earliestAvailableCommitTs = getInCommitTimestamp(\n          deltaLog, enablementCommit.version + 2)\n        val earliestAvailableCommitTimestampString = DateTimeUtils.timestampToString(\n          timestampFormatter, DateTimeUtils.fromJavaTimestamp(\n            new Timestamp(earliestAvailableCommitTs)))\n        val e2 = intercept[DeltaErrors.TimestampEarlierThanCommitRetentionException] {\n          deltaLog.history.getActiveCommitAtTime(enablementCommit.timestamp-1, false)\n        }\n        checkError(\n          e2,\n          \"DELTA_TIMESTAMP_EARLIER_THAN_COMMIT_RETENTION\",\n          sqlState = \"42816\",\n          parameters = Map(\n            \"userTimestamp\" -> new Timestamp(enablementCommit.timestamp-1).toString,\n            \"commitTs\" -> new Timestamp(earliestAvailableCommitTs).toString,\n            \"timestampString\" -> earliestAvailableCommitTimestampString)\n        )\n        // The same query should work when the earliest commit is allowed to be returned.\n        // The returned commit will be the earliest available ICT commit.\n        val commit = deltaLog.history.getActiveCommitAtTime(\n          new Timestamp(enablementCommit.timestamp-1),\n          catalogTableOpt = None,\n          canReturnLastCommit = false,\n          canReturnEarliestCommit = true)\n        // Note that we have already deleted the first two ICT commits.\n        assert(commit.version == enablementCommit.version + 2)\n        val earliestAvailableICTCommitTs = getInCommitTimestamp(\n          deltaLog,\n          enablementCommit.version + 2)\n        assert(commit.timestamp == earliestAvailableICTCommitTs)\n      }\n    }\n  }\n\n  // Catalog Owned will also automatically enable ICT.\n  testWithDefaultCommitCoordinatorUnset(\"DeltaHistoryManager.getHistory --- \" +\n      \"works correctly when the history has both ICT and non-ICT commits\") {\n    withSQLConf(\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> false.toString) {\n      withTempTable(createTable = false) { tableName =>\n        spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n        val numNonICTCommits = 6\n        val numICTCommits = 5\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        for (i <- 1 to (numNonICTCommits - 1)) {\n          deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n        }\n\n        // Enable ICT.\n        spark.sql(\n          s\"ALTER TABLE $tableName \" +\n            s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n\n        for (i <- 1 to (numICTCommits - 1)) {\n          deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n        }\n        val currentVersion = deltaLog.update().version\n        val ictEnablementVersion = numNonICTCommits\n\n        // Fetch the entire history.\n        val history = deltaLog.history.getHistory(None)\n        assert(history.length == currentVersion + 1)\n        val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n        history.reverse.zipWithIndex.foreach { case (hist, version) =>\n          assert(hist.getVersion == version)\n          val expectedTimestamp = if (version < ictEnablementVersion) {\n            fs.getFileStatus(FileNames.unsafeDeltaFile(deltaLog.logPath, version))\n              .getModificationTime\n          } else {\n            getInCommitTimestamp(deltaLog, version)\n          }\n          assert(hist.timestamp.getTime == expectedTimestamp)\n        }\n        // Try fetching only the non-ICT commits.\n        val nonICTHistory =\n          deltaLog.history.getHistory(start = 0, end = Some(ictEnablementVersion - 1))\n        assert(nonICTHistory.length == ictEnablementVersion)\n        nonICTHistory.reverse.zipWithIndex.foreach { case (hist, version) =>\n          assert(hist.getVersion == version)\n          val expectedTimestamp =\n            fs.getFileStatus(FileNames.unsafeDeltaFile(deltaLog.logPath, version))\n              .getModificationTime\n          assert(hist.timestamp.getTime == expectedTimestamp)\n        }\n        // Try fetching only the ICT commits.\n        val ictHistory = deltaLog.history.getHistory(start = ictEnablementVersion, end = None)\n        assert(ictHistory.length == currentVersion - ictEnablementVersion + 1)\n        ictHistory\n          .reverse\n          .zip(ictEnablementVersion to currentVersion.toInt)\n          .foreach { case (hist, version) =>\n            assert(hist.getVersion == version)\n            assert(hist.timestamp.getTime == getInCommitTimestamp(deltaLog, version))\n          }\n        // Try fetching some non-ICT + some ICT commits.\n        val mixedHistory = deltaLog.history.getHistory(start = 2, end = Some(6))\n        assert(mixedHistory.length == 5)\n        mixedHistory\n          .reverse\n          .zip(2 to 6)\n          .foreach { case (hist, version) =>\n            assert(hist.getVersion == version)\n            val expectedTimestamp = if (version < ictEnablementVersion) {\n                fs.getFileStatus(FileNames.unsafeDeltaFile(deltaLog.logPath, version))\n                  .getModificationTime\n              } else {\n                getInCommitTimestamp(deltaLog, version)\n              }\n            assert(hist.timestamp.getTime == expectedTimestamp)\n        }\n      }\n    }\n  }\n\n  test(\"DeltaHistoryManager.getActiveCommitAtTimeFromICTRange -- boundary cases\" ) {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val startTime = System.currentTimeMillis()\n      val clock = new ManualClock(startTime)\n      val deltaLog = getDeltaLogWithClock(tableName, clock)\n      val commit0 = DeltaHistoryManager.Commit(0, deltaLog.snapshot.timestamp)\n      val commitTimeDelta = 10\n      val numberAdditionalCommits = 10\n      assert(clock eq deltaLog.clock)\n      for (i <- 1 to numberAdditionalCommits) {\n        clock.setTime(startTime + i * commitTimeDelta)\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n      }\n      def getICTCommit(version: Long): DeltaHistoryManager.Commit =\n        DeltaHistoryManager.Commit(version, startTime + commitTimeDelta * version)\n\n      val deltaCommitFileProvider = DeltaCommitFileProvider(deltaLog.update())\n\n      // Degenerate case: start + 1 == end.\n      var commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(2).timestamp,\n          getICTCommit(2),\n          end = 2 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(2))\n\n      // start + 1 == end, search for a timestamp that is after the window.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n        getICTCommit(5).timestamp,\n        getICTCommit(2),\n          end = 2 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(2))\n\n      // start + 1 == end, search for a timestamp that is before the window.\n      val commitOpt = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(1).timestamp,\n          getICTCommit(2),\n          end = 2 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider)\n      assert(commitOpt.isEmpty)\n\n      // window size is exactly equal to `numChunks`.\n      // Search for an intermediate commit.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(7).timestamp,\n          getICTCommit(5),\n          end = 9 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 5,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(7))\n      // Search for the last commit.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(9).timestamp,\n          getICTCommit(5),\n          end = 9 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 5,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(9))\n\n      // Delete the last few commits in the window.\n      val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n      fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, 5), false)\n      fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, 6), false)\n      // Search for the commit just before the deleted commits.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(4).timestamp,\n          getICTCommit(2),\n          end = 6 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(4))\n\n      // Search with the first couple of commits in the window deleted.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(8).timestamp,\n          // Commits 5 and 6 have been deleted. We start from commit 5,\n          // which does not exist anymore.\n          getICTCommit(5),\n          end = 10 + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 3,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(8))\n\n      // Make one chunk in the first iteration completely empty.\n      // Window -> [0, 11)\n      // numChunks = 5, chunkSize = (11-0)/5 = 2\n      // chunks -> [0, 2), [2, 4), [4, 6), [6, 8), [8, 10), [10, 11)\n      fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, 4), false)\n      // 4, 5, 6 have been deleted, so window [4, 6) is completely empty.\n\n      // Search for the commit 6.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(6).timestamp,\n          commit0,\n          end = 11,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 5,\n          spark,\n          deltaCommitFileProvider).get\n      // [4,6] have been deleted, so we should get the commit at version 3.\n      assert(commit == getICTCommit(3))\n\n      // Search for a commit just after the deleted chunk (7).\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(7).timestamp,\n          commit0,\n          end = 11,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 5,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(7))\n\n      // Scenario with many empty chunks.\n      fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, 8), false)\n      fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, 9), false)\n\n      // Window -> [0, 11)\n      // numChunks = 11, chunkSize = (11-0)/11 = 1\n      // chunks -> [0, 1), [1, 2), [2, 3), ... [9, 10), [10, 11)\n      // 4, 5, 6, 8, 9 have been deleted.\n      // [4, 6), [5, 6) and [8, 9) are completely empty.\n\n      // Search for a commit in between empty chunks (7).\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(7).timestamp,\n          commit0,\n          end = 11,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 11,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(7))\n\n      // Search for a commit just after the last deleted chunk (10).\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          getICTCommit(10).timestamp,\n          commit0,\n          end = 11,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 11,\n          spark,\n          deltaCommitFileProvider).get\n      assert(commit == getICTCommit(10))\n\n      fs.delete(FileNames.unsafeDeltaFile(deltaLog.logPath, 10), false)\n      // Everything after and including `end` does not exist.\n      commit = DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n        getICTCommit(7).timestamp,\n        commit0,\n        // The last commit in the table is at version 9. But our\n        // search window here is [7, 11).\n        end = 11,\n        deltaLog.newDeltaHadoopConf(),\n        deltaLog.logPath,\n        deltaLog.store,\n        numChunks = 3,\n        spark,\n        deltaCommitFileProvider).get\n      assert(commit == getICTCommit(7))\n    }\n  }\n\n  test(\"DeltaHistoryManager.getHistory --- all ICT commits\") {\n    withTempTable(createTable = false) { tableName =>\n      spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val numberAdditionalCommits = 4\n      for (i <- 1 to numberAdditionalCommits) {\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n      }\n      val history = deltaLog.history.getHistory(None)\n      assert(history.length == numberAdditionalCommits + 1)\n      history.reverse.zipWithIndex.foreach { case (hist, version) =>\n        assert(hist.timestamp.getTime == getInCommitTimestamp(deltaLog, version))\n      }\n      // Try fetching a limited subset of the history.\n      val historySubset = deltaLog.history.getHistory(start = 2, end = Some(2))\n      assert(historySubset.length == 1)\n      assert(historySubset.head.timestamp.getTime == getInCommitTimestamp(deltaLog, 2))\n    }\n  }\n\n  for (ictEnablementVersion <- Seq(1, 4, 7))\n  testWithDefaultCommitCoordinatorUnset(s\"CDC read with all commits being ICT \" +\n    s\"[ictEnablementVersion = $ictEnablementVersion]\") {\n    withSQLConf(\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> \"false\"\n    ) {\n      withTempTable(createTable = false) { tableName =>\n        for (i <- 0 to 7) {\n          if (i == ictEnablementVersion) {\n            spark.sql(\n              s\"ALTER TABLE $tableName \" +\n                s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n          } else {\n            spark.range(i, i + 1).write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n          }\n        }\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        val result = spark.read\n          .format(\"delta\")\n          .option(\"startingVersion\", \"1\")\n          .option(\"endingVersion\", \"7\")\n          .option(\"readChangeFeed\", \"true\")\n          .table(tableName)\n          .select(\"_commit_timestamp\", \"_commit_version\")\n          .collect()\n        val fileTimestampsMap = getFileModificationTimesMap(deltaLog, 0, 7)\n        result.foreach { row =>\n          val v = row.getAs[Long](\"_commit_version\")\n          val expectedTimestamp = if (v >= ictEnablementVersion) {\n            getInCommitTimestamp(deltaLog, v)\n          } else {\n            fileTimestampsMap(v)\n          }\n          assert(row.getAs[Timestamp](\"_commit_timestamp\").getTime == expectedTimestamp)\n        }\n      }\n    }\n  }\n\n  for (ictEnablementVersion <- Seq(1, 4, 7))\n  testWithDefaultCommitCoordinatorUnset(s\"Streaming query + CDC \" +\n    s\"[ictEnablementVersion = $ictEnablementVersion]\") {\n    withSQLConf(\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\",\n      DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> \"false\"\n    ) {\n      withTempTable(createTable = false) { sourceTableName =>\n        withTempDir { checkpointDir =>\n          withTempTable(createTable = false) { sinkTableName =>\n        spark.range(0).write.format(\"delta\").mode(\"append\").saveAsTable(sourceTableName)\n\n        val sourceDeltaLog = DeltaLog.forTable(spark, TableIdentifier(sourceTableName))\n        val streamingQuery = spark.readStream\n          .format(\"delta\")\n          .option(\"readChangeFeed\", \"true\")\n          .table(sourceTableName)\n          .select(\n            col(\"_commit_timestamp\").alias(\"source_commit_timestamp\"),\n            col(\"_commit_version\").alias(\"source_commit_version\"))\n          .writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n          .toTable(sinkTableName)\n        for (i <- 1 to 7) {\n          if (i == ictEnablementVersion) {\n            spark.sql(s\"ALTER TABLE $sourceTableName \" +\n              s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n          } else {\n            spark.range(i, i + 1).write.format(\"delta\").mode(\"append\").saveAsTable(sourceTableName)\n          }\n        }\n        streamingQuery.processAllAvailable()\n        val fileTimestampsMap = getFileModificationTimesMap(sourceDeltaLog, 0, 7)\n        val result = spark.read.format(\"delta\")\n          .table(sinkTableName)\n          .collect()\n        result.foreach { row =>\n          val v = row.getAs[Long](\"source_commit_version\")\n          val expectedTimestamp = if (v >= ictEnablementVersion) {\n            getInCommitTimestamp(sourceDeltaLog, v)\n          } else {\n            fileTimestampsMap(v)\n          }\n          assert(\n            row.getAs[Timestamp](\"source_commit_timestamp\").getTime == expectedTimestamp)\n        }\n      }}}\n    }\n  }\n\n  private def testICTEnablementPropertyRetention(\n      expectRetention: Boolean,\n      expectICTEnabled: Option[Boolean] = None)(runCommand: (String) => Unit): Unit = {\n    val ictConfOpt =\n      spark.conf.getOption(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey)\n    try {\n      spark.conf.unset(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey)\n      withTempTable(createTable = false) { tableName =>\n        spark.range(1).write.format(\"delta\").saveAsTable(tableName)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        // Enable ICT at version 1 instead of 0 so that we can test the retention of\n        // enablement provenance properties as well.\n        spark.sql(\n          s\"ALTER TABLE $tableName \" +\n            s\"SET TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n        val enablementVersion =\n          DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(\n            deltaLog.snapshot.metadata)\n        val enablementTimestamp =\n          DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(\n            deltaLog.snapshot.metadata)\n        assert(enablementVersion.contains(1))\n        assert(enablementTimestamp.isDefined)\n\n        spark.range(2, 3).write.format(\"delta\").mode(\"overwrite\").saveAsTable(tableName)\n\n        // Run the REPLACE/CLONE command.\n        runCommand(tableName)\n\n        val metadataAfterReplace = deltaLog.update().metadata\n        assert(\n          DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(\n            metadataAfterReplace) == expectICTEnabled.getOrElse(expectRetention))\n        if (expectRetention) {\n          assert(\n            DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.fromMetaData(\n              metadataAfterReplace) == enablementTimestamp)\n          assert(\n            DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.fromMetaData(\n              metadataAfterReplace) == enablementVersion)\n        } else {\n          Seq(\n            DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP.key,\n            DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION.key\n          ).foreach { key =>\n            assert(!metadataAfterReplace.configuration.contains(key))\n          }\n        }\n      }\n    } finally {\n      ictConfOpt.foreach { ictConf =>\n        spark.conf.set(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey, ictConf)\n      }\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\n    \"ICT enablement properties remain unchanged after a REPLACE with explicit enablement\") {\n    testICTEnablementPropertyRetention(expectRetention = true) { tableName =>\n      sql(s\"REPLACE TABLE $tableName USING delta \" +\n        s\"TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true') \" +\n        \"AS SELECT * FROM range(3, 4)\")\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\n    \"ICT enablement properties are dropped after a REPLACE with explicit enablement \" +\n      s\"when the ${DeltaSQLConf.IN_COMMIT_TIMESTAMP_RETAIN_ENABLEMENT_INFO_FIX_ENABLED.key} \" +\n      s\"is disabled\") {\n    withSQLConf(\n      DeltaSQLConf.IN_COMMIT_TIMESTAMP_RETAIN_ENABLEMENT_INFO_FIX_ENABLED.key -> \"false\"\n    ) {\n      testICTEnablementPropertyRetention(\n        expectRetention = false, expectICTEnabled = Some(true)) { tableName =>\n          sql(\n            s\"REPLACE TABLE $tableName USING delta \" +\n              s\"TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true') \" +\n              \"AS SELECT * FROM range(3, 4)\")\n      }\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\n    \"ICT enablement properties are dropped after a REPLACE with explicit disablement\") {\n    testICTEnablementPropertyRetention(expectRetention = false) { tableName =>\n      sql(s\"REPLACE TABLE $tableName USING delta \" +\n        s\"TBLPROPERTIES ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'false') \" +\n        \"AS SELECT * FROM range(3, 4)\")\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\n    \"ICT is completely dropped after a REPLACE with no explicit disablement\") {\n    testICTEnablementPropertyRetention(expectRetention = false) { tableName =>\n      sql(s\"REPLACE TABLE $tableName USING delta AS SELECT * FROM range(3, 4)\")\n    }\n  }\n}\n\nclass InCommitTimestampWithCatalogOwnedBatch1Suite extends InCommitTimestampSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass InCommitTimestampWithCatalogOwnedBatch2Suite extends InCommitTimestampSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass InCommitTimestampWithCatalogOwnedBatch5Suite extends InCommitTimestampSuite {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey, \"true\")\n  }\n\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(5)\n\n  test(\"getActiveCommitAtTime works correctly within catalog owned range\") {\n    CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n    CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n      catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING,\n      commitCoordinatorBuilder = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10)\n    )\n    withTempTable(createTable = false) { tableName =>\n      spark.range(10).write.format(\"delta\").saveAsTable(tableName)\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      val commit0 = DeltaHistoryManager.Commit(0, snapshot.timestamp)\n      val tableCommitCoordinatorClient =\n        CatalogOwnedTableUtils.populateTableCommitCoordinatorFromCatalog(\n          spark,\n          catalogTableOpt = deltaLog.initialCatalogTable,\n          snapshot\n        ).get\n      val numberAdditionalCommits = 4\n      // Create 4 unbackfilled commits.\n      for (i <- 1 to numberAdditionalCommits) {\n        deltaLog.startTransaction().commit(Seq(createTestAddFile(i.toString)), ManualUpdate)\n      }\n      val commitFileProvider = DeltaCommitFileProvider(deltaLog.update())\n      val unbackfilledCommits =\n        tableCommitCoordinatorClient\n          .getCommits(Some(1L))\n          .getCommits.asScala\n          .map { commit => DeltaHistoryManager.Commit(commit.getVersion, commit.getCommitTimestamp)}\n      val commits = (Seq(commit0) ++ unbackfilledCommits).toList\n      // Search for the exact timestamp.\n      for (commit <- commits) {\n        val resCommit = deltaLog.history.getActiveCommitAtTime(\n          new Timestamp(commit.timestamp), catalogTableOpt = None, canReturnLastCommit = false)\n        assert(resCommit.version == commit.version)\n        assert(resCommit.timestamp == commit.timestamp)\n      }\n\n      // getActiveCommitAtTimeFromICTRange should throw an IllegalStateException\n      // if it does not manage to find an unbackfilled commit.\n\n      // Delete the target unbackfilled commit:\n      val fs = deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf())\n      val commit3Path = commitFileProvider.deltaFile(3)\n      fs.delete(commit3Path, false)\n      val commit3Timestamp = unbackfilledCommits(2).timestamp\n      var errorOpt = Option.empty[org.apache.spark.SparkException]\n      try {\n        DeltaHistoryManager.getActiveCommitAtTimeFromICTRange(\n          commit3Timestamp,\n          commit0,\n          numberAdditionalCommits + 1,\n          deltaLog.newDeltaHadoopConf(),\n          deltaLog.logPath,\n          deltaLog.store,\n          numChunks = 5,\n          spark,\n          commitFileProvider)\n      } catch {\n        case e: org.apache.spark.SparkException => errorOpt = Some(e)\n          e.getStackTrace.exists(_.toString.contains(\n            s\"Could not find commit 3 which was expected to be at \" +\n              s\"path ${commit3Path.toString}.\"))\n      }\n      assert(errorOpt.isDefined)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/InCommitTimestampTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Action, CommitInfo}\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, FileNames, JsonUtils}\nimport org.apache.hadoop.fs.Path\n\nobject InCommitTimestampTestUtils {\n  /**\n   * Overwrites the in-commit-timestamp in the delta file with the given timestamp.\n   * It will also set operationParameters to an empty map because operationParameters\n   * serialization/deserialization is broken.\n   */\n  def overwriteICTInDeltaFile(deltaLog: DeltaLog, filePath: Path, ts: Option[Long]): Unit = {\n    val updatedActionsList = deltaLog.store\n      .readAsIterator(filePath, deltaLog.newDeltaHadoopConf())\n      .map(Action.fromJson)\n      .map {\n        case ci: CommitInfo =>\n          // operationParameters serialization/deserialization is broken as it uses a custom\n          // serializer but a default deserializer.\n          ci.copy(inCommitTimestamp = ts, operationParameters = Map.empty).json\n        case other => other.json\n      }.toList\n    deltaLog.store.write(\n      filePath, updatedActionsList.toIterator, overwrite = true, deltaLog.newDeltaHadoopConf())\n  }\n\n  /**\n   * Overwrites the in-commit-timestamp in the given CRC file with the given timestamp.\n   */\n  def overwriteICTInCrc(deltaLog: DeltaLog, version: Long, ts: Option[Long]): Unit = {\n    val crcPath = FileNames.checksumFile(deltaLog.logPath, version)\n    val latestCrc = JsonUtils.fromJson[VersionChecksum](\n      deltaLog.store.read(crcPath, deltaLog.newDeltaHadoopConf()).mkString(\"\"))\n    val checksumWithNoICT = latestCrc.copy(inCommitTimestampOpt = ts)\n    deltaLog.store.write(\n      crcPath,\n      Iterator(JsonUtils.toJson(checksumWithNoICT)),\n      overwrite = true,\n      deltaLog.newDeltaHadoopConf())\n  }\n\n  /**\n   * Retrieves the in-commit timestamp for a specific version of the Delta Log.\n   */\n  def getInCommitTimestamp(deltaLog: DeltaLog, version: Long): Long = {\n    val deltaFile = DeltaCommitFileProvider(deltaLog.unsafeVolatileSnapshot).deltaFile(version)\n    val commitInfo = DeltaHistoryManager.getCommitInfoOpt(\n      deltaLog.store,\n      deltaFile,\n      deltaLog.newDeltaHadoopConf())\n    assert(commitInfo.isDefined, s\"CommitInfo should exist for version $version\")\n    assert(commitInfo.get.inCommitTimestamp.isDefined,\n      s\"InCommitTimestamp should exist for CommitInfo's version $version\")\n    commitInfo.get.inCommitTimestamp.get\n  }\n\n  /**\n   * Retrieves a map of file modification times for Delta Log versions within a specified version\n   * range.\n   */\n  def getFileModificationTimesMap(\n      deltaLog: DeltaLog, start: Long, end: Long): Map[Long, Long] = {\n    deltaLog.store.listFrom(\n        FileNames.listingPrefix(deltaLog.logPath, start), deltaLog.newDeltaHadoopConf())\n      .collect { case FileNames.DeltaFile(fs, v) => v -> fs.getModificationTime }\n      .takeWhile(_._1 <= end)\n      .toMap\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/LastCheckpointInfoSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.databind.JsonNode\nimport com.fasterxml.jackson.databind.exc.MismatchedInputException\nimport com.google.common.io.{ByteStreams, Closeables}\nimport org.apache.commons.codec.digest.DigestUtils\nimport org.apache.commons.io.IOUtils\n\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, StructType}\n\nclass LastCheckpointInfoSuite extends SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  // same checkpoint schema for tests\n  private val checkpointSchema = Some(new StructType().add(\"c1\", IntegerType, nullable = false))\n\n  private def jsonStringToChecksum(jsonStr: String): String = {\n    val rootNode = JsonUtils.mapper.readValue(jsonStr, classOf[JsonNode])\n    LastCheckpointInfo.treeNodeToChecksum(rootNode)\n  }\n\n  test(\"test json to checksum conversion with maps\") {\n    // test with different ordering and spaces, with different value data types\n    val s1 = \"\"\"{\"k1\":\"v1\",\"k4\":\"v4\",\"k3\":23.45,\"k2\":123}\"\"\"\n    val s2 = \"\"\"{\"k1\":\"v1\",\"k3\":23.45,\"k2\":123,      \"k4\":\"v4\"}\"\"\"\n    assert(jsonStringToChecksum(s1) === jsonStringToChecksum(s2))\n\n    // test json with nested maps\n    val s3 =\n      \"\"\"{\"k1\":\"v1\",\"k4\":{\"k41\":\"v41\",\"k40\":{\"k401\":401,\"k402\":\"402\"}},\"k3\":23.45,\"k2\":123}\"\"\"\n    val s4 =\n      \"\"\"{\"k1\":\"v1\",\"k4\":{\"k40\":{\"k401\":401,\"k402\":\"402\"},   \"k41\":\"v41\"},\"k3\":23.45,\"k2\":123}\"\"\"\n    assert(jsonStringToChecksum(s3) === jsonStringToChecksum(s4))\n\n    // test empty json\n    val s5 = \"\"\"{    }\"\"\"\n    val s6 = \"\"\"{}\"\"\"\n    assert(jsonStringToChecksum(s5) === jsonStringToChecksum(s6))\n\n    // negative test: value for a specific key k4 is not same.\n    val s7 = \"\"\"{\"k1\":\"v1\",\"k4\":\"v4\",\"k3\":23.45,\"k2\":123}\"\"\"\n    val s8 = \"\"\"{\"k1\":\"v1\",\"k4\":\"v1\",\"k3\":23.45,\"k2\":123}\"\"\"\n    assert(jsonStringToChecksum(s7) != jsonStringToChecksum(s8))\n  }\n\n  test(\"test json to checksum conversion with array\") {\n    // has top level array and array values are json objects\n    val s1 = \"\"\"[{\"id\":\"j1\",\"stuff\":\"things\"},{\"stuff\":\"t2\",\"id\":\"j2\"}]\"\"\"\n    val s2 = \"\"\"[{\"id\" : \"j1\", \"stuff\" : \"things\"}, {\"id\" : \"j2\", \"stuff\" : \"t2\"}]\"\"\"\n    assert(jsonStringToChecksum(s1) === jsonStringToChecksum(s2))\n\n    // array as part of value for a json key and array value has single json object\n    val s3 = \"\"\"{\"id\":\"j1\",\"stuff\":[{\"hello\": \"world\", \"hello1\": \"world1\"}]}\"\"\"\n    val s4 = \"\"\"{\"id\":   \"j1\",\"stuff\":[{\"hello1\": \"world1\", \"hello\": \"world\"}]}\"\"\"\n    assert(jsonStringToChecksum(s3) === jsonStringToChecksum(s4))\n\n    // array as part of value for a json key and array values are multiple json objects\n    val s5 = \"\"\"{\"id\":\"j1\",\"stuff\":[{\"hello\": \"world\"}, {\"hello1\": \"world1\"}]}\"\"\"\n    val s6 = \"\"\"{\"id\":   \"j1\",\"stuff\":[{\"hello\":\"world\"},{\"hello1\":\"world1\"}]}\"\"\"\n    assert(jsonStringToChecksum(s5) === jsonStringToChecksum(s6))\n\n    // Negative case: array as part of value for a json key and array values are multiple json\n    // objects with different order.\n    val s7 = \"\"\"{\"id\":\"j1\",\"stuff\":[{\"hello1\": \"world1\"}, {\"hello\": \"world\"}]}\"\"\"\n    val s8 = \"\"\"{\"id\":   \"j1\",\"stuff\":[{\"hello\":\"world\"},{\"hello1\":\"world1\"}]}\"\"\"\n    assert(jsonStringToChecksum(s7) != jsonStringToChecksum(s8))\n\n    // array has scalar string values\n    val s9 = \"\"\"{\"id\":\"j1\",\"stuff\":[\"a\", \"b\"]}\"\"\"\n    val s10 = \"\"\"{\"stuff\":[\"a\",\"b\"], \"id\":   \"j1\"}\"\"\"\n    assert(jsonStringToChecksum(s9) === jsonStringToChecksum(s10))\n\n    // array has scalar int values\n    val s11 = \"\"\"{\"id\":\"j1\",\"stuff\":[1, 2]}\"\"\"\n    val s12 = \"\"\"{\"stuff\":[1,2], \"id\":   \"j1\"}\"\"\"\n    assert(jsonStringToChecksum(s11) === jsonStringToChecksum(s12))\n\n    // Negative case: array has scalar values in different order\n    val s13 = \"\"\"{\"id\":\"j1\",\"stuff\":[\"a\", \"b\", \"c\"]}\"\"\"\n    val s14 = \"\"\"{\"id\":\"j1\",\"stuff\":[\"c\", \"a\", \"b\"]}\"\"\"\n    assert(jsonStringToChecksum(s13) != jsonStringToChecksum(s14))\n  }\n\n  // scalastyle:off line.size.limit\n  test(\"test json normalization\") {\n    // test with different data types\n    val s1 = \"\"\"{\"k1\":\"v1\",\"k4\":\"v4\",\"k3\":23.45,\"k2\":123,\"k6\":null,\"k5\":true}\"\"\"\n    val normalizedS1 = \"\"\"\"k1\"=\"v1\",\"k2\"=123,\"k3\"=23.45,\"k4\"=\"v4\",\"k5\"=true,\"k6\"=null\"\"\"\n    assert(jsonStringToChecksum(s1) === DigestUtils.md5Hex(normalizedS1))\n\n    // test json with nested maps\n    val s2 =\n      \"\"\"{\"k1\":\"v1\",\"k4\":{\"k41\":\"v41\",\"k40\":{\"k401\":401,\"k402\":\"402\"}},\"k3\":23.45,\"k2\":123}\"\"\"\n    val normalizedS2 = \"\"\"\"k1\"=\"v1\",\"k2\"=123,\"k3\"=23.45,\"k4\"+\"k40\"+\"k401\"=401,\"k4\"+\"k40\"+\"k402\"=\"402\",\"k4\"+\"k41\"=\"v41\"\"\"\"\n    assert(jsonStringToChecksum(s2) === DigestUtils.md5Hex(normalizedS2))\n\n    // test with arrays\n    val s3 = \"\"\"{\"stuff\":[{\"hx\": \"wx\",\"h1\":\"w1\"}, {\"h2\": \"w2\"}],\"id\":1}\"\"\"\n    val normalizedS3 = \"\"\"\"id\"=1,\"stuff\"+0+\"h1\"=\"w1\",\"stuff\"+0+\"hx\"=\"wx\",\"stuff\"+1+\"h2\"=\"w2\"\"\"\"\n    assert(jsonStringToChecksum(s3) === DigestUtils.md5Hex(normalizedS3))\n\n    // test top level `checksum` key is ignored in canonicalization\n    val s4 = \"\"\"{\"k1\":\"v1\",\"checksum\":\"daswefdssfd\",\"k3\":23.45,\"k2\":123}\"\"\"\n    val normalizedS4 = \"\"\"\"k1\"=\"v1\",\"k2\"=123,\"k3\"=23.45\"\"\"\n    assert(jsonStringToChecksum(s4) === DigestUtils.md5Hex(normalizedS4))\n\n    // test empty json\n    val s5 = \"\"\"{    }\"\"\"\n    val normalizedS5 = \"\"\"\"\"\"\n    assert(jsonStringToChecksum(s5) === DigestUtils.md5Hex(normalizedS5))\n\n    // test with complex strings\n    val s6 = \"\"\"{\"k0\":\"normal\",\"k1\":\"'v1'\",\"k4\":\"'v4\",\"k3\":\":hello\",\"k2\":\"\\\"double quote str\\\"\"}\"\"\"\n    val normalizedS6 = \"\"\"\"k0\"=\"normal\",\"k1\"=\"%27v1%27\",\"k2\"=\"%22double%20quote%20str%22\",\"k3\"=\"%3Ahello\",\"k4\"=\"%27v4\"\"\"\"\n    assert(jsonStringToChecksum(s6) === DigestUtils.md5Hex(normalizedS6))\n\n    // test covering different ASCII characters\n    val s7 = \"\"\"{\"k0\":\"normal\",\"k1\":\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%'`~!@#$%^&*()_+-={[}]|\\\\;:'\\\"\\/?.>,<\"}\"\"\"\n    val normalizedS7 = \"\"\"\"k0\"=\"normal\",\"k1\"=\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%25%27%60~%21%40%23%24%25%5E%26%2A%28%29_%2B-%3D%7B%5B%7D%5D%7C%5C%3B%3A%27%22%2F%3F.%3E%2C%3C\"\"\"\"\n    assert(jsonStringToChecksum(s7) === DigestUtils.md5Hex(normalizedS7))\n\n    // test with nested maps and arrays\n    // This example is also part of Delta's PROTOCOL.md. We should keep these two in sync.\n    val s8 = \"\"\"{\"k0\":\"'v 0'\", \"checksum\": \"adsaskfljadfkjadfkj\", \"k1\":{\"k2\": 2, \"k3\": [\"v3\", [1, 2], {\"k4\": \"v4\", \"k5\": [\"v5\", \"v6\", \"v7\"]}]}}\"\"\"\n    val normalizedS8 = \"\"\"\"k0\"=\"%27v%200%27\",\"k1\"+\"k2\"=2,\"k1\"+\"k3\"+0=\"v3\",\"k1\"+\"k3\"+1+0=1,\"k1\"+\"k3\"+1+1=2,\"k1\"+\"k3\"+2+\"k4\"=\"v4\",\"k1\"+\"k3\"+2+\"k5\"+0=\"v5\",\"k1\"+\"k3\"+2+\"k5\"+1=\"v6\",\"k1\"+\"k3\"+2+\"k5\"+2=\"v7\"\"\"\"\n    assert(jsonStringToChecksum(s8) === DigestUtils.md5Hex(normalizedS8))\n    assert(jsonStringToChecksum(s8) === \"6a92d155a59bf2eecbd4b4ec7fd1f875\")\n\n    // test non-ASCII character\n    // scalastyle:off nonascii\n    val s9 = s\"\"\"{\"k0\":\"normal\",\"k1\":\"a€+\"}\"\"\"\n    val normalizedS9 = \"\"\"\"k0\"=\"normal\",\"k1\"=\"a%E2%82%AC%2B\"\"\"\"\n    assert(jsonStringToChecksum(s9) === DigestUtils.md5Hex(normalizedS9))\n    // scalastyle:on nonascii\n  }\n  // scalastyle:on line.size.limit\n\n  test(\"test LastCheckpointInfo checksum\") {\n    val ci1 = LastCheckpointInfo(version = 1, size = 2, parts = Some(3),\n      sizeInBytes = Some(20L), numOfAddFiles = Some(2L), checkpointSchema = checkpointSchema)\n    val (stored1, actual1) =\n      LastCheckpointInfo.getChecksums(LastCheckpointInfo.serializeToJson(ci1, addChecksum = true))\n    assert(stored1 === Some(actual1))\n\n    // checksum mismatch when version changes.\n    val ci2 = LastCheckpointInfo(version = 2, size = 2, parts = Some(3),\n      sizeInBytes = Some(20L), numOfAddFiles = Some(2L),\n      checkpointSchema = checkpointSchema)\n    val (stored2, actual2) =\n      LastCheckpointInfo.getChecksums(LastCheckpointInfo.serializeToJson(ci2, addChecksum = true))\n    assert(stored2 === Some(actual2))\n    assert(stored2 != stored1)\n\n    // `checksum` doesn't participate in `actualChecksum` calculation.\n    val ci3 = LastCheckpointInfo(version = 1, size = 2, parts = Some(3),\n      checksum = Some(\"XYZ\"), sizeInBytes = Some(20L), numOfAddFiles = Some(2L),\n      checkpointSchema = checkpointSchema)\n    val (stored3, actual3) =\n      LastCheckpointInfo.getChecksums(LastCheckpointInfo.serializeToJson(ci3, addChecksum = true))\n    assert(stored3 === Some(actual3))\n    assert(stored3 === stored1)\n\n    // checksum doesn't depend on spaces and order of field\n    val json1 = \"\"\"{\"version\":1,\"size\":2,\"parts\":3}\"\"\"\n    val json2 = \"\"\"{\"version\":1 ,\"parts\":3,\"size\":2}\"\"\"\n    assert(jsonStringToChecksum(json1) === jsonStringToChecksum(json2))\n    // `checksum` is ignored while calculating json\n    val json3 = \"\"\"{\"version\":1 ,\"parts\":3,\"size\":2,\"checksum\":\"xyz\"}\"\"\"\n    assert(jsonStringToChecksum(json1) === jsonStringToChecksum(json3))\n    // Change in any value changes the checksum\n    val json4 = \"\"\"{\"version\":4,\"size\":2,\"parts\":3}\"\"\"\n    assert(jsonStringToChecksum(json1) != jsonStringToChecksum(json4))\n\n  }\n\n  test(\"test backward compatibility - json without checksum is deserialized properly\") {\n    val jsonStr = \"\"\"{\"version\":1,\"size\":2,\"parts\":3,\"sizeInBytes\":20,\"numOfAddFiles\":2,\"\"\" +\n     \"\"\"\"checkpointSchema\":{\"type\":\"struct\",\"fields\":[{\"name\":\"c1\",\"type\":\"integer\"\"\"\" +\n     \"\"\",\"nullable\":false,\"metadata\":{}}]}}\"\"\"\n    val expectedLastCheckpointInfo = LastCheckpointInfo(\n      version = 1, size = 2, parts = Some(3), sizeInBytes = Some(20), numOfAddFiles = Some(2),\n      checkpointSchema = Some(new StructType().add(\"c1\", IntegerType, nullable = false)))\n    assert(LastCheckpointInfo.deserializeFromJson(jsonStr, validate = true) ===\n      expectedLastCheckpointInfo)\n  }\n\n  test(\"LastCheckpointInfo - serialize/deserialize\") {\n    val ci1 = LastCheckpointInfo(version = 1, size = 2, parts = Some(3),\n      checksum = Some(\"XYZ\"), sizeInBytes = Some(20L), numOfAddFiles = Some(2L),\n      checkpointSchema = checkpointSchema)\n    val ci2 = LastCheckpointInfo(version = 1, size = 2, parts = Some(3), checksum = None,\n      sizeInBytes = Some(20L), numOfAddFiles = Some(2L),\n      checkpointSchema = checkpointSchema)\n\n    val actualChecksum = LastCheckpointInfo.getChecksums(\n      LastCheckpointInfo.serializeToJson(ci1, addChecksum = true))._2\n    val ciWithCorrectChecksum = ci1.copy(checksum = Some(actualChecksum))\n\n    for(ci <- Seq(ci1, ci2)) {\n      val json = LastCheckpointInfo.serializeToJson(ci, addChecksum = true)\n      assert(LastCheckpointInfo.deserializeFromJson(json, validate = true)\n        === ciWithCorrectChecksum)\n      // The below assertion also validates that fields version/size/parts are in the beginning of\n      // the json.\n      assert(LastCheckpointInfo.serializeToJson(ci, addChecksum = true) ===\n        \"\"\"{\"version\":1,\"size\":2,\"parts\":3,\"sizeInBytes\":20,\"numOfAddFiles\":2,\"\"\" +\n          s\"\"\"\"checkpointSchema\":${JsonUtils.toJson(checkpointSchema)},\"\"\" +\n          \"\"\"\"checksum\":\"524d4e2226f3c3f923df4ee42dae347e\"}\"\"\")\n    }\n\n    assert(LastCheckpointInfo.serializeToJson(ci1, addChecksum = true)\n      === LastCheckpointInfo.serializeToJson(ci2, addChecksum = true))\n  }\n\n  test(\"LastCheckpointInfo - json with duplicate keys should fail\") {\n    val jsonString =\n      \"\"\"{\"version\":1,\"size\":3,\"parts\":3,\"checksum\":\"d84a0aa11c93304d57feca6acaceb7fb\",\"size\":2}\"\"\"\n    intercept[MismatchedInputException] {\n      LastCheckpointInfo.deserializeFromJson(jsonString, validate = true)\n    }\n    // Deserialization shouldn't fail when validate is false and the last `size` overrides the\n    // previous size.\n    assert(LastCheckpointInfo.deserializeFromJson(jsonString, validate = false).size === 2)\n  }\n\n  test(\"LastCheckpointInfo - test checksum is written only when config is enabled\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      val log = DeltaLog.forTable(spark, dir)\n\n      def readLastCheckpointFile(): String = {\n        val fs = log.LAST_CHECKPOINT.getFileSystem(log.newDeltaHadoopConf())\n        val is = fs.open(log.LAST_CHECKPOINT)\n        try {\n          IOUtils.toString(is, \"UTF-8\")\n        } finally {\n          is.close()\n        }\n      }\n\n      withSQLConf(DeltaSQLConf.LAST_CHECKPOINT_CHECKSUM_ENABLED.key -> \"true\") {\n        DeltaLog.forTable(spark, dir).checkpoint()\n        assert(readLastCheckpointFile().contains(\"checksum\"))\n      }\n\n      spark.range(10).write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n      withSQLConf(DeltaSQLConf.LAST_CHECKPOINT_CHECKSUM_ENABLED.key -> \"false\") {\n        DeltaLog.forTable(spark, dir).checkpoint()\n        assert(!readLastCheckpointFile().contains(\"checksum\"))\n      }\n    }\n  }\n\n  test(\"Suppress optional fields in _last_checkpoint\") {\n    val expectedStr = \"\"\"{\"version\":1,\"size\":2,\"parts\":3}\"\"\"\n    val info = LastCheckpointInfo(\n      version = 1, size = 2, parts = Some(3), sizeInBytes = Some(20), numOfAddFiles = Some(2),\n      checkpointSchema = Some(new StructType().add(\"c1\", IntegerType, nullable = false)))\n    val serializedJson = LastCheckpointInfo.serializeToJson(\n      info, addChecksum = true, suppressOptionalFields = true)\n    assert(serializedJson === expectedStr)\n\n    val expectedStrNoPart = \"\"\"{\"version\":1,\"size\":2}\"\"\"\n    val serializedJsonNoPart = LastCheckpointInfo.serializeToJson(\n      info.copy(parts = None), addChecksum = true, suppressOptionalFields = true)\n    assert(serializedJsonNoPart === expectedStrNoPart)\n  }\n\n  test(\"read and write _last_checkpoint with optional fields suppressed\") {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.SUPPRESS_OPTIONAL_LAST_CHECKPOINT_FIELDS.key -> \"true\") {\n        // Create a Delta table with a checkpoint.\n        spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n        DeltaLog.forTable(spark, dir).checkpoint()\n        DeltaLog.clearCache()\n\n        val log = DeltaLog.forTable(spark, dir)\n        val metadata = log.readLastCheckpointFile().get\n        val trimmed = metadata.productIterator.drop(3).forall {\n          case o: Option[_] => o.isEmpty\n        }\n        assert(trimmed, s\"Unexpected fields in _last_checkpoint: $metadata\")\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/LogStoreProviderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.storage.{DelegatingLogStore, LogStore, LogStoreAdaptor}\n\nimport org.apache.spark.{SparkConf, SparkContext, SparkFunSuite}\nimport org.apache.spark.sql.{AnalysisException, SparkSession}\nimport org.apache.spark.sql.LocalSparkSession._\n\nclass LogStoreProviderSuite extends SparkFunSuite {\n\n\n  private val customLogStoreClassName = classOf[CustomPublicLogStore].getName\n  private def fakeSchemeWithNoDefault = \"fake\"\n  private def withoutSparkPrefix(key: String) = key.stripPrefix(\"spark.\")\n\n  private def constructSparkConf(confs: Seq[(String, String)]): SparkConf = {\n    val sparkConf = new SparkConf(loadDefaults = false).setMaster(\"local\")\n    confs.foreach { case (key, value) => sparkConf.set(key, value) }\n    sparkConf\n  }\n\n  /**\n   * Test with class conf set and scheme conf unset using `scheme`. Test using class conf key both\n   * with and without 'spark.' prefix.\n   */\n  private def testLogStoreClassConfNoSchemeConf(scheme: String) {\n    for (classKeys <- Seq(\n      // set only prefixed key\n      Seq(LogStore.logStoreClassConfKey),\n      // set only non-prefixed key\n      Seq(withoutSparkPrefix(LogStore.logStoreClassConfKey)),\n      // set both spark-prefixed key and non-spark prefixed key\n      Seq(LogStore.logStoreClassConfKey, withoutSparkPrefix(LogStore.logStoreClassConfKey))\n    )) {\n      val sparkConf = constructSparkConf(classKeys.map((_, customLogStoreClassName)))\n      withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n        assert(LogStore(spark).isInstanceOf[LogStoreAdaptor])\n        assert(LogStore(spark).asInstanceOf[LogStoreAdaptor]\n          .logStoreImpl.getClass.getName == customLogStoreClassName)\n      }\n    }\n  }\n\n  /**\n   * Test with class conf set and scheme conf set using `scheme`. This tests\n   * checkLogStoreConfConflicts. Test conf keys both with and without 'spark.' prefix.\n   */\n  private def testLogStoreClassConfAndSchemeConf(scheme: String, classConf: String,\n      schemeConf: String) {\n    val schemeKey = LogStore.logStoreSchemeConfKey(scheme)\n    // we test with both the spark-prefixed and non-prefixed keys\n    val schemeConfKeys = Seq(schemeKey, withoutSparkPrefix(schemeKey))\n    val classConfKeys = Seq(LogStore.logStoreClassConfKey,\n      withoutSparkPrefix(LogStore.logStoreClassConfKey))\n\n    schemeConfKeys.foreach { schemeKey =>\n      classConfKeys.foreach { classKey =>\n        val sparkConf = constructSparkConf(Seq((schemeKey, schemeConf), (classKey, classConf)))\n        val e = intercept[AnalysisException](\n          withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n            LogStore(spark)\n          }\n        )\n        assert(e.getMessage.contains(\n          s\"(`$classKey`) and (`$schemeKey`) cannot be set at the same time\"))\n      }\n    }\n  }\n\n  test(\"class-conf = set, scheme has no default, scheme-conf = not set\") {\n    testLogStoreClassConfNoSchemeConf(fakeSchemeWithNoDefault)\n  }\n\n  test(\"class-conf = set, scheme has no default, scheme-conf = set\") {\n    testLogStoreClassConfAndSchemeConf(fakeSchemeWithNoDefault, customLogStoreClassName,\n      DelegatingLogStore.defaultAzureLogStoreClassName)\n  }\n\n  test(\"class-conf = set, scheme has default, scheme-conf = not set\") {\n    testLogStoreClassConfNoSchemeConf(\"s3a\")\n  }\n\n  test(\"class-conf = set, scheme has default, scheme-conf = set\") {\n    testLogStoreClassConfAndSchemeConf(\"s3a\", customLogStoreClassName,\n      DelegatingLogStore.defaultAzureLogStoreClassName)\n  }\n\n  test(\"verifyLogStoreConfs - scheme conf keys \") {\n    Seq(\n      fakeSchemeWithNoDefault, // scheme with no default\n      \"s3a\" // scheme with default\n    ).foreach { scheme =>\n      val schemeConfKey = LogStore.logStoreSchemeConfKey(scheme)\n      for (confs <- Seq(\n        // set only non-prefixed key\n        Seq((withoutSparkPrefix(schemeConfKey), customLogStoreClassName)),\n        // set only prefixed key\n        Seq((schemeConfKey, customLogStoreClassName)),\n        // set both spark-prefixed key and non-spark prefixed key to same value\n        Seq((withoutSparkPrefix(schemeConfKey), customLogStoreClassName),\n          (schemeConfKey, customLogStoreClassName))\n      )) {\n        val sparkConf = constructSparkConf(confs)\n        withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n          // no error is thrown\n          LogStore(spark)\n        }\n      }\n\n      // set both spark-prefixed key and non-spark-prefixed key to inconsistent values\n      val sparkConf = constructSparkConf(\n        Seq((withoutSparkPrefix(schemeConfKey), customLogStoreClassName),\n          (schemeConfKey, DelegatingLogStore.defaultAzureLogStoreClassName)))\n      val e = intercept[IllegalArgumentException](\n        withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n          LogStore(spark)\n        }\n      )\n      assert(e.getMessage.contains(\n        s\"(${withoutSparkPrefix(schemeConfKey)} = $customLogStoreClassName, \" +\n          s\"$schemeConfKey = ${DelegatingLogStore.defaultAzureLogStoreClassName}) cannot be set \" +\n          s\"to different values. Please only set one of them, or set them to the same value.\"\n      ))\n    }\n  }\n\n  test(\"verifyLogStoreConfs - class conf keys\") {\n    val classConfKey = LogStore.logStoreClassConfKey\n    for (confs <- Seq(\n      // set only non-prefixed key\n      Seq((withoutSparkPrefix(classConfKey), customLogStoreClassName)),\n      // set only prefixed key\n      Seq((classConfKey, customLogStoreClassName)),\n      // set both spark-prefixed key and non-spark prefixed key to same value\n      Seq((withoutSparkPrefix(classConfKey), customLogStoreClassName),\n        (classConfKey, customLogStoreClassName))\n    )) {\n      val sparkConf = constructSparkConf(confs)\n      withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n        // no error is thrown\n      LogStore(spark)\n      }\n    }\n\n    // set both spark-prefixed key and non-spark-prefixed key to inconsistent values\n    val sparkConf = constructSparkConf(\n      Seq((withoutSparkPrefix(classConfKey), customLogStoreClassName),\n        (classConfKey, DelegatingLogStore.defaultAzureLogStoreClassName)))\n    val e = intercept[IllegalArgumentException](\n      withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark =>\n        LogStore(spark)\n      }\n    )\n    assert(e.getMessage.contains(\n      s\"(${withoutSparkPrefix(classConfKey)} = $customLogStoreClassName, \" +\n        s\"$classConfKey = ${DelegatingLogStore.defaultAzureLogStoreClassName})\" +\n        s\" cannot be set to different values. Please only set one of them, or set them to the \" +\n        s\"same value.\"\n    ))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/LogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, IOException}\nimport java.net.URI\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.collection.mutable.ArrayBuffer\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, FSDataOutputStream, Path, RawLocalFileSystem}\n\nimport org.apache.spark.{SparkConf, SparkFunSuite}\nimport org.apache.spark.sql.{LocalSparkSession, QueryTest, SparkSession}\nimport org.apache.spark.sql.LocalSparkSession.withSparkSession\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\n///////////////////////////\n// Child-specific traits //\n///////////////////////////\n\ntrait AzureLogStoreSuiteBase extends LogStoreSuiteBase {\n\n  testHadoopConf(\n    expectedErrMsg = \".*No FileSystem for scheme.*fake.*\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = true\n}\n\ntrait HDFSLogStoreSuiteBase extends LogStoreSuiteBase {\n\n  // HDFSLogStore is based on FileContext APIs and hence requires AbstractFileSystem-based\n  // implementations.\n  testHadoopConf(\n    expectedErrMsg = \".*No FileSystem for scheme.*fake.*\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  import testImplicits._\n\n  test(\"writes on systems without AbstractFileSystem implemented\") {\n    withSQLConf(\"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n      \"fs.fake.impl.disable.cache\" -> \"true\") {\n      val tempDir = Utils.createTempDir()\n      // scalastyle:off pathfromuri\n      val path = new Path(new URI(s\"fake://${tempDir.toURI.getRawPath}/1.json\"))\n      // scalastyle:on pathfromuri\n      val e = intercept[IOException] {\n        createLogStore(spark)\n          .write(path, Iterator(\"zero\", \"none\"), overwrite = false, sessionHadoopConf)\n      }\n      assert(e.getMessage\n        .contains(\"The error typically occurs when the default LogStore implementation\"))\n    }\n  }\n\n  test(\"reads should work on systems without AbstractFileSystem implemented\") {\n    withTempDir { tempDir =>\n      val writtenFile = new File(tempDir, \"1\")\n      val store = createLogStore(spark)\n      store.write(\n        new Path(writtenFile.getCanonicalPath),\n        Iterator(\"zero\", \"none\"),\n        overwrite = false,\n        sessionHadoopConf)\n      withSQLConf(\"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n        \"fs.fake.impl.disable.cache\" -> \"true\") {\n        val read = createLogStore(spark)\n          .read(new Path(\"fake://\" + writtenFile.getCanonicalPath), sessionHadoopConf)\n        assert(read === ArrayBuffer(\"zero\", \"none\"))\n      }\n    }\n  }\n\n  test(\n    \"No AbstractFileSystem - end to end test using data frame\") {\n    // Writes to the fake file system will fail\n    withTempDir { tempDir =>\n      val fakeFSLocation = s\"fake://${tempDir.getCanonicalFile}\"\n      withSQLConf(\"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n        \"fs.fake.impl.disable.cache\" -> \"true\") {\n        val e = intercept[IOException] {\n          Seq(1, 2, 4).toDF().write.format(\"delta\").save(fakeFSLocation)\n        }\n        assert(e.getMessage\n          .contains(\"The error typically occurs when the default LogStore implementation\"))\n      }\n    }\n    // Reading files written by other systems will work.\n    withTempDir { tempDir =>\n      Seq(1, 2, 4).toDF().write.format(\"delta\").save(tempDir.getAbsolutePath)\n      withSQLConf(\"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n        \"fs.fake.impl.disable.cache\" -> \"true\") {\n        val fakeFSLocation = s\"fake://${tempDir.getCanonicalFile}\"\n        checkAnswer(spark.read.format(\"delta\").load(fakeFSLocation), Seq(1, 2, 4).toDF())\n      }\n    }\n  }\n\n  test(\"if fc.rename() fails, it should throw java.nio.file.FileAlreadyExistsException\") {\n    withTempDir { tempDir =>\n      withSQLConf(\n        \"fs.AbstractFileSystem.fake.impl\" -> classOf[FailingRenameAbstractFileSystem].getName,\n        \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n        \"fs.fake.impl.disable.cache\" -> \"true\") {\n        val store = createLogStore(spark)\n        val commit0 = new Path(s\"fake://${tempDir.getCanonicalPath}/00000.json\")\n\n        intercept[java.nio.file.FileAlreadyExistsException] {\n          store.write(commit0, Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n        }\n      }\n    }\n  }\n\n  test(\"Read after write consistency with msync\") {\n     withTempDir { tempDir =>\n      val tsFSLocation = s\"ts://${tempDir.getCanonicalFile}\"\n      // Use the file scheme so that it uses a different FileSystem cached object\n      withSQLConf(\n        (\"fs.ts.impl\", classOf[TimestampLocalFileSystem].getCanonicalName),\n        (\"fs.AbstractFileSystem.ts.impl\",\n          classOf[TimestampAbstractFileSystem].getCanonicalName)) {\n        val store = createLogStore(spark)\n        val path = new Path(tsFSLocation, \"1.json\")\n\n        // Initialize the TimestampLocalFileSystem object which will be reused later due to the\n        // FileSystem cache\n        assert(store.listFrom(path, sessionHadoopConf).length == 0)\n\n        store.write(path, Iterator(\"zero\", \"none\"), overwrite = false, sessionHadoopConf)\n        // Verify `msync` is called by checking whether `listFrom` returns the latest result.\n        // Without the `msync` call, the TimestampLocalFileSystem would not see this file.\n        assert(store.listFrom(path, sessionHadoopConf).length == 1)\n      }\n    }\n  }\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = true\n}\n\ntrait LocalLogStoreSuiteBase extends LogStoreSuiteBase {\n  testHadoopConf(\n    expectedErrMsg = \".*No FileSystem for scheme.*fake.*\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = true\n}\n\ntrait GCSLogStoreSuiteBase extends LogStoreSuiteBase {\n\n  testHadoopConf(\n    expectedErrMsg = \".*No FileSystem for scheme.*fake.*\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = false\n\n  test(\"gcs write should happen in a new thread\") {\n    withTempDir { tempDir =>\n      // Use `FakeGCSFileSystemValidatingCommits` to verify we write in the correct thread.\n      withSQLConf(\n        \"fs.gs.impl\" -> classOf[FakeGCSFileSystemValidatingCommits].getName,\n        \"fs.gs.impl.disable.cache\" -> \"true\") {\n        val store = createLogStore(spark)\n        store.write(\n          new Path(s\"gs://${tempDir.getCanonicalPath}\", \"1.json\"),\n          Iterator(\"foo\"),\n          overwrite = false,\n          sessionHadoopConf)\n      }\n    }\n  }\n\n  test(\"handles precondition failure\") {\n    withTempDir { tempDir =>\n      withSQLConf(\n        \"fs.gs.impl\" -> classOf[FailingGCSFileSystem].getName,\n        \"fs.gs.impl.disable.cache\" -> \"true\") {\n        val store = createLogStore(spark)\n\n        assertThrows[java.nio.file.FileAlreadyExistsException] {\n          store.write(\n            new Path(s\"gs://${tempDir.getCanonicalPath}\", \"1.json\"),\n            Iterator(\"foo\"),\n            overwrite = false,\n            sessionHadoopConf)\n        }\n\n        store.write(\n          new Path(s\"gs://${tempDir.getCanonicalPath}\", \"1.json\"),\n          Iterator(\"foo\"),\n          overwrite = true,\n          sessionHadoopConf)\n      }\n    }\n  }\n}\n\n////////////////////////////////\n// Concrete child test suites //\n////////////////////////////////\n\nclass HDFSLogStoreSuite extends HDFSLogStoreSuiteBase {\n  override val logStoreClassName: String = classOf[HDFSLogStore].getName\n}\n\nclass AzureLogStoreSuite extends AzureLogStoreSuiteBase {\n  override val logStoreClassName: String = classOf[AzureLogStore].getName\n}\n\nclass LocalLogStoreSuite extends LocalLogStoreSuiteBase {\n  override val logStoreClassName: String = classOf[LocalLogStore].getName\n}\n\n////////////////////////////////\n// File System Helper Classes //\n////////////////////////////////\n\n/** A fake file system to test whether GCSLogStore properly handles precondition failures. */\nclass FailingGCSFileSystem extends RawLocalFileSystem {\n  override def getScheme: String = \"gs\"\n  override def getUri: URI = URI.create(\"gs:/\")\n\n  override def create(f: Path, overwrite: Boolean): FSDataOutputStream = {\n    throw new IOException(\"412 Precondition Failed\");\n  }\n}\n\n/**\n * A fake AbstractFileSystem to test whether session Hadoop configuration will be picked up.\n * This is a wrapper around [[FakeFileSystem]].\n */\nclass FakeAbstractFileSystem(uri: URI, conf: org.apache.hadoop.conf.Configuration)\n  extends org.apache.hadoop.fs.DelegateToFileSystem(\n    uri,\n    new FakeFileSystem,\n    conf,\n    FakeFileSystem.scheme,\n    false) {\n\n  // Implementation copied from RawLocalFs\n  import org.apache.hadoop.fs.local.LocalConfigKeys\n  import org.apache.hadoop.fs._\n\n  override def getUriDefaultPort(): Int = -1\n  override def getServerDefaults(): FsServerDefaults = LocalConfigKeys.getServerDefaults\n  override def isValidName(src: String): Boolean = true\n}\n\n/**\n * A file system allowing to track how many times `rename` is called.\n * `TrackingRenameFileSystem.numOfRename` should be reset to 0 before starting to trace.\n */\nclass TrackingRenameFileSystem extends RawLocalFileSystem {\n  override def rename(src: Path, dst: Path): Boolean = {\n    TrackingRenameFileSystem.renameCounter.incrementAndGet()\n    super.rename(src, dst)\n  }\n}\n\nobject TrackingRenameFileSystem {\n  val renameCounter = new AtomicInteger(0)\n  def resetCounter(): Unit = renameCounter.set(0)\n}\n\n/**\n * A fake AbstractFileSystem to ensure FileSystem.renameInternal(), and thus FileContext.rename(),\n * fails. This will be used to test HDFSLogStore.writeInternal corner case.\n */\nclass FailingRenameAbstractFileSystem(uri: URI, conf: org.apache.hadoop.conf.Configuration)\n  extends FakeAbstractFileSystem(uri, conf) {\n\n  override def renameInternal(src: Path, dst: Path, overwrite: Boolean): Unit = {\n    throw new org.apache.hadoop.fs.FileAlreadyExistsException(s\"$dst path already exists\")\n  }\n}\n\n////////////////////////////////////////////////////////////////////\n// Public LogStore (Java) suite tests from delta-storage artifact //\n////////////////////////////////////////////////////////////////////\n\nabstract class PublicLogStoreSuite extends LogStoreSuiteBase {\n\n  protected val publicLogStoreClassName: String\n\n  // The actual type of LogStore created will be LogStoreAdaptor.\n  override val logStoreClassName: String = classOf[LogStoreAdaptor].getName\n\n  protected override def sparkConf = {\n    super.sparkConf.set(logStoreClassConfKey, publicLogStoreClassName)\n  }\n\n  protected override def testInitFromSparkConf(): Unit = {\n    test(\"instantiation through SparkConf\") {\n      assert(spark.sparkContext.getConf.get(logStoreClassConfKey) == publicLogStoreClassName)\n      assert(LogStore(spark).getClass.getName == logStoreClassName)\n      assert(LogStore(spark).asInstanceOf[LogStoreAdaptor]\n        .logStoreImpl.getClass.getName == publicLogStoreClassName)\n\n    }\n  }\n}\n\nclass PublicHDFSLogStoreSuite extends PublicLogStoreSuite with HDFSLogStoreSuiteBase {\n  override protected val publicLogStoreClassName: String =\n    classOf[io.delta.storage.HDFSLogStore].getName\n}\n\nclass PublicS3SingleDriverLogStoreSuite\n  extends PublicLogStoreSuite\n  with S3SingleDriverLogStoreSuiteBase {\n\n  override protected val publicLogStoreClassName: String =\n    classOf[io.delta.storage.S3SingleDriverLogStore].getName\n\n  override protected def canInvalidateCache: Boolean = false\n}\n\nclass PublicAzureLogStoreSuite extends PublicLogStoreSuite with AzureLogStoreSuiteBase {\n  override protected val publicLogStoreClassName: String =\n    classOf[io.delta.storage.AzureLogStore].getName\n}\n\nclass PublicLocalLogStoreSuite extends PublicLogStoreSuite with LocalLogStoreSuiteBase {\n  override protected val publicLogStoreClassName: String =\n    classOf[io.delta.storage.LocalLogStore].getName\n}\n\nclass PublicGCSLogStoreSuite extends PublicLogStoreSuite with GCSLogStoreSuiteBase {\n  override protected val publicLogStoreClassName: String =\n    classOf[io.delta.storage.GCSLogStore].getName\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/LogStoreSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, IOException}\nimport java.net.URI\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, FSDataOutputStream, Path, RawLocalFileSystem}\n\nimport org.apache.spark.{SparkConf, SparkFunSuite}\nimport org.apache.spark.sql.{LocalSparkSession, QueryTest, SparkSession}\nimport org.apache.spark.sql.LocalSparkSession.withSparkSession\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\n/////////////////////\n// Base Test Suite //\n/////////////////////\n\nabstract class LogStoreSuiteBase extends QueryTest\n  with LogStoreProvider\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  def logStoreClassName: String\n\n  protected override def sparkConf = {\n    super.sparkConf.set(logStoreClassConfKey, logStoreClassName)\n  }\n\n  // scalastyle:off deltahadoopconfiguration\n  def sessionHadoopConf: Configuration = spark.sessionState.newHadoopConf\n  // scalastyle:on deltahadoopconfiguration\n\n  protected def testInitFromSparkConf(): Unit = {\n    test(\"instantiation through SparkConf\") {\n      assert(spark.sparkContext.getConf.get(logStoreClassConfKey) == logStoreClassName)\n      assert(LogStore(spark).getClass.getName == logStoreClassName)\n    }\n  }\n\n  testInitFromSparkConf()\n\n  protected def withTempLogDir(f: File => Unit): Unit = {\n    val dir = Utils.createTempDir()\n    val deltaLogDir = new File(dir, \"_delta_log\")\n    deltaLogDir.mkdir()\n    try f(deltaLogDir) finally {\n      Utils.deleteRecursively(dir)\n    }\n  }\n\n  test(\"read / write\") {\n    def assertNoLeakedCrcFiles(dir: File): Unit = {\n      // crc file should not be leaked when origin file doesn't exist.\n      // The implementation of Hadoop filesystem may filter out checksum file, so\n      // listing files from local filesystem.\n      val fileNames = dir.listFiles().toSeq.filter(p => p.isFile).map(p => p.getName)\n      val crcFiles = fileNames.filter(n => n.startsWith(\".\") && n.endsWith(\".crc\"))\n      val originFileNamesForExistingCrcFiles = crcFiles.map { name =>\n        // remove first \".\" and last \".crc\"\n        name.substring(1, name.length - 4)\n      }\n\n      // Check all origin files exist for all crc files.\n      assert(originFileNamesForExistingCrcFiles.toSet.subsetOf(fileNames.toSet),\n        s\"Some of origin files for crc files don't exist - crc files: $crcFiles / \" +\n          s\"expected origin files: $originFileNamesForExistingCrcFiles / actual files: $fileNames\")\n    }\n\n    def pathToFileStatus(path: Path): FileStatus =\n      path.getFileSystem(sessionHadoopConf).getFileStatus(path)\n\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n      val deltas = Seq(0, 1)\n        .map(i => new File(tempLogDir, i.toString)).map(_.toURI).map(new Path(_))\n      store.write(deltas.head, Iterator(\"zero\", \"none\"), overwrite = false, sessionHadoopConf)\n      store.write(deltas(1), Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n\n      // Test Path based read APIs\n      assert(store.read(deltas.head, sessionHadoopConf) == Seq(\"zero\", \"none\"))\n      assert(store.readAsIterator(deltas.head, sessionHadoopConf).toSeq == Seq(\"zero\", \"none\"))\n      assert(store.read(deltas(1), sessionHadoopConf) == Seq(\"one\"))\n      assert(store.readAsIterator(deltas(1), sessionHadoopConf).toSeq == Seq(\"one\"))\n      // Test FileStatus based read APIs\n      assert(store.read(pathToFileStatus(deltas.head), sessionHadoopConf) == Seq(\"zero\", \"none\"))\n      assert(store.readAsIterator(pathToFileStatus(deltas.head), sessionHadoopConf).toSeq ==\n        Seq(\"zero\", \"none\"))\n      assert(store.read(pathToFileStatus(deltas(1)), sessionHadoopConf) == Seq(\"one\"))\n      assert(store.readAsIterator(pathToFileStatus(deltas(1)), sessionHadoopConf).toSeq ==\n        Seq(\"one\"))\n\n      assertNoLeakedCrcFiles(tempLogDir)\n    }\n\n  }\n\n  test(\"detects conflict\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n      val deltas = Seq(0, 1)\n        .map(i => new File(tempLogDir, i.toString)).map(_.toURI).map(new Path(_))\n      store.write(deltas.head, Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n      store.write(deltas(1), Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n\n      intercept[java.nio.file.FileAlreadyExistsException] {\n        store.write(deltas(1), Iterator(\"uno\"), overwrite = false, sessionHadoopConf)\n      }\n    }\n\n  }\n\n  test(\"listFrom\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n\n      val deltas =\n        Seq(0, 1, 2, 3, 4).map(i => new File(tempLogDir, i.toString)).map(_.toURI).map(new Path(_))\n      store.write(deltas(1), Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n      store.write(deltas(2), Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n      store.write(deltas(3), Iterator(\"two\"), overwrite = false, sessionHadoopConf)\n\n      assert(\n        store.listFrom(deltas.head, sessionHadoopConf)\n          .map(_.getPath.getName).toArray === Seq(1, 2, 3).map(_.toString))\n      assert(\n        store.listFrom(deltas(1), sessionHadoopConf)\n          .map(_.getPath.getName).toArray === Seq(1, 2, 3).map(_.toString))\n      assert(store.listFrom(deltas(2), sessionHadoopConf)\n        .map(_.getPath.getName).toArray === Seq(2, 3).map(_.toString))\n      assert(store.listFrom(deltas(3), sessionHadoopConf)\n        .map(_.getPath.getName).toArray === Seq(3).map(_.toString))\n      assert(store.listFrom(deltas(4), sessionHadoopConf).map(_.getPath.getName).toArray === Nil)\n    }\n  }\n\n  test(\"simple log store test\") {\n    val tempDir = Utils.createTempDir()\n    val log1 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n    assert(log1.store.getClass.getName == logStoreClassName)\n\n    val txn = log1.startTransaction()\n    txn.commitManually(createTestAddFile())\n    log1.checkpoint()\n\n    DeltaLog.clearCache()\n    val log2 = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n    assert(log2.store.getClass.getName == logStoreClassName)\n\n    assert(log2.readLastCheckpointFile().map(_.version) === Some(0L))\n    assert(log2.snapshot.allFiles.count == 1)\n  }\n\n  protected def testHadoopConf(expectedErrMsg: String, fsImplConfs: (String, String)*): Unit = {\n    test(\"should pick up fs impl conf from session Hadoop configuration\") {\n      withTempDir { tempDir =>\n        // scalastyle:off pathfromuri\n        val path = new Path(new URI(s\"fake://${tempDir.toURI.getRawPath}/1.json\"))\n        // scalastyle:on pathfromuri\n\n        // Make sure it will fail without FakeFileSystem\n        val e = intercept[IOException] {\n          createLogStore(spark).listFrom(path, sessionHadoopConf)\n        }\n        assert(e.getMessage.matches(expectedErrMsg))\n        withSQLConf(fsImplConfs: _*) {\n          createLogStore(spark).listFrom(path, sessionHadoopConf)\n        }\n      }\n    }\n  }\n\n  /**\n   * Whether the log store being tested should use rename to write checkpoint or not. The following\n   * test is using this method to verify the behavior of `checkpoint`.\n   */\n  protected def shouldUseRenameToWriteCheckpoint: Boolean\n\n  test(\n    \"use isPartialWriteVisible to decide whether use rename\") {\n    withTempDir { tempDir =>\n      import testImplicits._\n      // Write 5 files to delta table\n      (1 to 100).toDF().repartition(5).write.format(\"delta\").save(tempDir.getCanonicalPath)\n      withSQLConf(\n          \"fs.file.impl\" -> classOf[TrackingRenameFileSystem].getName,\n          \"fs.file.impl.disable.cache\" -> \"true\") {\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getCanonicalPath)\n        TrackingRenameFileSystem.renameCounter.set(0)\n        deltaLog.checkpoint()\n        val expectedNumOfRename = if (shouldUseRenameToWriteCheckpoint) 1 else 0\n        assert(TrackingRenameFileSystem.renameCounter.get() === expectedNumOfRename)\n\n        withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"9\") {\n          // Write 5 more files to the delta table\n          (1 to 100).toDF().repartition(5).write\n            .format(\"delta\").mode(\"append\").save(tempDir.getCanonicalPath)\n          // At this point table has total 10 files, which won't fit in 1 checkpoint part file (as\n          // DELTA_CHECKPOINT_PART_SIZE is set to 9 in this test). So this will end up generating\n          // 2 PART files.\n          TrackingRenameFileSystem.renameCounter.set(0)\n          deltaLog.checkpoint()\n          val expectedNumOfRename = if (shouldUseRenameToWriteCheckpoint) 2 else 0\n          assert(TrackingRenameFileSystem.renameCounter.get() === expectedNumOfRename)\n        }\n      }\n    }\n  }\n\n  test(\"readAsIterator should be lazy\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n      val testFile = new File(tempLogDir, \"readAsIterator\").getCanonicalPath\n      store.write(new Path(testFile), Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n\n      withSQLConf(\n          \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n          \"fs.fake.impl.disable.cache\" -> \"true\") {\n        val fsStats = FileSystem.getStatistics(\"fake\", classOf[FakeFileSystem])\n        fsStats.reset()\n        val iter = store.readAsIterator(new Path(s\"fake:///$testFile\"), sessionHadoopConf)\n        try {\n          // We should not read any date when creating the iterator.\n          assert(fsStats.getBytesRead == 0)\n          assert(iter.toList == \"foo\" :: \"bar\" :: Nil)\n          // Verify we are using the correct Statistics instance.\n          assert(fsStats.getBytesRead == 8)\n        } finally {\n          iter.close()\n        }\n      }\n    }\n  }\n\n  test(\"LogStoreInverseAdaptor is equivalent to base LogStore\") {\n    withTempLogDir { tempLogDir =>\n      val scalaStore = createLogStore(spark)\n      val javaStore = new LogStoreInverseAdaptor(scalaStore, sessionHadoopConf)\n\n      // Write with scala, read as java.\n      val testFile = new File(tempLogDir, \"readAsIteratorScala\").getCanonicalPath\n      scalaStore.write(\n        new Path(testFile), Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n\n      val contents = javaStore.read(new Path(testFile), sessionHadoopConf)\n      assert(contents.next() == \"foo\")\n      assert(contents.next() == \"bar\")\n      assert(!contents.hasNext)\n      contents.close()\n\n      // Write with java, read as scala.\n      val testFile2 = new File(tempLogDir, \"readAsIteratorJava\").getCanonicalPath\n      javaStore.write(\n        new Path(testFile2), Iterator(\"foo\", \"bar\").asJava, overwrite = false, sessionHadoopConf)\n\n      val contents2 = scalaStore.readAsIterator(new Path(testFile), sessionHadoopConf)\n      assert(contents2.toList == \"foo\" :: \"bar\" :: Nil)\n      contents2.close()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MaterializePartitionColumnsFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.jdk.CollectionConverters._\n\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.hadoop.ParquetFileReader\nimport org.apache.parquet.hadoop.util.HadoopInputFile\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass MaterializePartitionColumnsFeatureSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  private def validateWriterFeatureEnabled(deltaLog: DeltaLog, isEnabled: Boolean): Unit = {\n    val protocol = deltaLog.update().protocol\n    assert(protocol.isFeatureSupported(MaterializePartitionColumnsTableFeature) == isEnabled)\n    assert(protocol.writerFeatures.getOrElse(Set.empty).contains(\"materializePartitionColumns\")\n      == isEnabled)\n    assert(!protocol.readerFeatures.getOrElse(Set.empty).contains(\"materializePartitionColumns\"))\n  }\n\n  private def validateAddedFilesFromLastOperationMaterializedPartitionColumn(\n      deltaLog: DeltaLog, expectedMaterialized: Boolean, partCol: Seq[String]): Unit = {\n    val snapshot = deltaLog.update()\n    val currentVersion = snapshot.version\n    val addedFiles = deltaLog.getChanges(currentVersion).flatMap(_._2).collect {\n      case a: AddFile => a\n    }\n    assert(addedFiles.nonEmpty)\n    val logicalToPhysicalNameMap = DeltaColumnMapping.getLogicalNameToPhysicalNameMap(\n      snapshot.schema)\n    val physicalPartCol = logicalToPhysicalNameMap(partCol).head\n\n    addedFiles.foreach { file =>\n      val filePath = DeltaFileOperations.absolutePath(deltaLog.dataPath.toString, file.path)\n      val path = new Path(filePath.toString)\n      val fileReader = ParquetFileReader.open(HadoopInputFile.fromPath(path, new Configuration()))\n      val parquetSchema = try {\n        val metaData = fileReader.getFooter\n        metaData.getFileMetaData.getSchema\n      } finally {\n        fileReader.close()\n      }\n      val fieldNames = parquetSchema.getFields.asScala.map(_.getName).toSet\n      assert(fieldNames.contains(physicalPartCol) == expectedMaterialized)\n    }\n  }\n\n  Seq(true, false).foreach { enable =>\n    test(\"MaterializePartitionColumnsTableFeature is auto-enabled when table property is set - \" +\n        s\"enable=$enable\") {\n      val tbl = \"tbl\"\n      withTable(tbl) {\n        sql(\n          s\"\"\"CREATE TABLE $tbl (id LONG, partCol INT)\n             |USING DELTA\n             |PARTITIONED BY (partCol)\n             |\"\"\".stripMargin)\n        sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES ('${DeltaConfigs\n          .ENABLE_MATERIALIZE_PARTITION_COLUMNS_FEATURE.key}' = '$enable')\")\n\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n        validateWriterFeatureEnabled(deltaLog, isEnabled = enable)\n      }\n    }\n  }\n\n  test(\"DROP / Add back feature for materializePartitionColumns removes / adds feature and \" +\n      \"stops / starts materializing columns\") {\n    val tbl = \"tbl\"\n    withTable(tbl) {\n      // Create table with materializePartitionColumns feature enabled\n      sql(\n        s\"\"\"CREATE TABLE $tbl (id LONG, partCol INT)\n           |USING DELTA\n           |PARTITIONED BY (partCol)\n           |\"\"\".stripMargin)\n      sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES ('${DeltaConfigs\n        .ENABLE_MATERIALIZE_PARTITION_COLUMNS_FEATURE.key}' = 'true')\")\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tbl))\n\n      // Verify feature is enabled\n      validateWriterFeatureEnabled(deltaLog, isEnabled = true)\n\n      // Insert data - partition columns should be materialized\n      sql(s\"INSERT INTO $tbl VALUES (1, 100), (2, 200)\")\n      validateAddedFilesFromLastOperationMaterializedPartitionColumn(\n        deltaLog, expectedMaterialized = true, partCol = Seq(\"partCol\"))\n\n      // Drop the feature\n      sql(s\"ALTER TABLE $tbl DROP FEATURE materializePartitionColumns\")\n\n      // Verify feature is removed from protocol\n      validateWriterFeatureEnabled(deltaLog, isEnabled = false)\n\n      // Insert more data - new files should NOT have materialized partition columns\n      sql(s\"INSERT INTO $tbl VALUES (3, 300), (4, 400)\")\n      validateAddedFilesFromLastOperationMaterializedPartitionColumn(\n        deltaLog, expectedMaterialized = false, partCol = Seq(\"partCol\"))\n\n      // Add table feature back and verify partition columns are materialized again\n      sql(s\"ALTER TABLE $tbl SET TBLPROPERTIES ('${DeltaConfigs\n        .ENABLE_MATERIALIZE_PARTITION_COLUMNS_FEATURE.key}' = 'true')\")\n      validateWriterFeatureEnabled(deltaLog, isEnabled = true)\n\n      // Insert data - partition columns should be materialized again, all the files\n      // including old and new should have partition columns\n      sql(s\"INSERT INTO $tbl VALUES (5, 500), (6, 600)\")\n      validateAddedFilesFromLastOperationMaterializedPartitionColumn(\n        deltaLog, expectedMaterialized = true, partCol = Seq(\"partCol\"))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoAccumulatorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.atomic.AtomicReference\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.commands.MergeIntoCommandBase\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.scheduler.{SparkListener, SparkListenerEvent, SparkListenerNodeExcluded, SparkListenerTaskEnd}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.status.TaskDataWrapper\nimport org.apache.spark.util.JsonProtocol\n\n/**\n * Tests how the accumulator used by the MERGE command reacts with other Spark components such as\n * Spark UI. These tests stay in a separated file so that we can use the package name\n * `org.apache.spark.sql.delta` to access `private[spark]` APIs.\n */\nclass MergeIntoAccumulatorSuite\n  extends SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  private def runTestMergeCommand(): Unit = {\n    // Run a simple merge command\n    withTempView(\"source\") {\n      withTempDir { tempDir =>\n        val tempPath = tempDir.getCanonicalPath\n        Seq((1, 1), (0, 3)).toDF(\"key\", \"value\").createOrReplaceTempView(\"source\")\n        Seq((2, 2), (1, 4)).toDF(\"key\", \"value\").write.format(\"delta\").save(tempPath)\n        spark.sql(s\"\"\"\n          |MERGE INTO delta.`$tempPath` target\n          |USING source src\n          |ON src.key = target.key\n          |WHEN MATCHED THEN UPDATE SET *\n          |WHEN NOT MATCHED THEN INSERT *\n          |\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"accumulators used by MERGE should not be tracked by Spark UI\") {\n    runTestMergeCommand()\n\n    // Make sure all Spark events generated by the above command have been processed\n    spark.sparkContext.listenerBus.waitUntilEmpty(30000)\n\n    val store = spark.sparkContext.statusStore.store\n    val iter = store.view(classOf[TaskDataWrapper]).closeableIterator()\n    try {\n      // Collect all accumulator names tracked by Spark UI.\n      val accumNames = iter.asScala.toVector.flatMap { task =>\n        task.accumulatorUpdates.map(_.name)\n      }.toSet\n      // Verify accumulators used by MergeIntoCommand are not tracked.\n      assert(!accumNames.contains(MergeIntoCommandBase.TOUCHED_FILES_ACCUM_NAME))\n    } finally {\n      iter.close()\n    }\n  }\n\n  test(\"accumulators used by MERGE should not fail Spark event log generation\") {\n    // Register a listener to convert `SparkListenerTaskEnd` to json and catch failures.\n    val failure = new AtomicReference[Throwable]()\n    val listener = new SparkListener {\n      override def onTaskEnd(taskEnd: SparkListenerTaskEnd): Unit = {\n        try JsonProtocol.sparkEventToJsonString(taskEnd) catch {\n          case t: Throwable => failure.compareAndSet(null, t)\n        }\n      }\n    }\n    spark.sparkContext.listenerBus.addToSharedQueue(listener)\n    try {\n      runTestMergeCommand()\n\n      // Make sure all Spark events generated by the above command have been processed\n      spark.sparkContext.listenerBus.waitUntilEmpty(30000)\n      // Converting `SparkListenerEvent` to json should not fail\n      assert(failure.get == null)\n    } finally {\n      spark.sparkContext.listenerBus.removeListener(listener)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoDVsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.cdc.MergeCDCMixin\nimport org.apache.spark.sql.delta.commands.{DeletionVectorBitmapGenerator, DMLWithDeletionVectorsHelper}\nimport org.apache.spark.sql.delta.files.TahoeBatchFileIndex\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.functions.col\n\ntrait MergeIntoDVsMixin extends MergeIntoSQLMixin with DeletionVectorsTestUtils {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark, merge = true)\n  }\n\n  override def excluded: Seq[String] = {\n    val miscFailures = Seq(\n      \"basic case - merge to view on a Delta table, \" +\n        \"partitioned: true skippingEnabled: false useSqlView: true\",\n      \"basic case - merge to view on a Delta table, \" +\n        \"partitioned: true skippingEnabled: false useSqlView: false\",\n      \"basic case - merge to view on a Delta table, \" +\n        \"partitioned: false skippingEnabled: false useSqlView: true\",\n      \"basic case - merge to view on a Delta table, \" +\n        \"partitioned: false skippingEnabled: false useSqlView: false\",\n      \"basic case - merge to Delta table, isPartitioned: false skippingEnabled: false\",\n      \"basic case - merge to Delta table, isPartitioned: true skippingEnabled: false\",\n      \"not matched by source - all 3 clauses - no changes - \" +\n        \"isPartitioned: true - cdcEnabled: true\",\n      \"not matched by source - all 3 clauses - no changes - \" +\n        \"isPartitioned: false - cdcEnabled: true\",\n      \"test merge on temp view - view with too many internal aliases - Dataset TempView\"\n    )\n\n    super.excluded ++ miscFailures\n  }\n\n  protected override lazy val expectedOpTypes: Set[String] = Set(\n    \"delta.dml.merge.materializeSource\",\n    \"delta.dml.merge.findTouchedFiles\",\n    \"delta.dml.merge.writeModifiedRowsOnly\",\n    \"delta.dml.merge.writeDeletionVectors\",\n    \"delta.dml.merge\")\n}\n\ntrait MergeIntoDVsTests extends MergeIntoDVsMixin {\n  import testImplicits._\n\n  private def assertOperationalDVMetrics(\n      tablePath: String,\n      numDeletedRows: Long,\n      numUpdatedRows: Long,\n      numCopiedRows: Long,\n      numTargetFilesRemoved: Long,\n      numDeletionVectorsAdded: Long,\n      numDeletionVectorsRemoved: Long,\n      numDeletionVectorsUpdated: Long): Unit = {\n    val table = io.delta.tables.DeltaTable.forPath(tablePath)\n    val mergeMetrics = DeltaMetricsUtils.getLastOperationMetrics(table)\n    assert(mergeMetrics.getOrElse(\"numTargetRowsDeleted\", -1) === numDeletedRows)\n    assert(mergeMetrics.getOrElse(\"numTargetRowsUpdated\", -1) === numUpdatedRows)\n    assert(mergeMetrics.getOrElse(\"numTargetRowsCopied\", -1) === numCopiedRows)\n    assert(mergeMetrics.getOrElse(\"numTargetFilesRemoved\", -1) === numTargetFilesRemoved)\n    assert(mergeMetrics.getOrElse(\"numTargetDeletionVectorsAdded\", -1) === numDeletionVectorsAdded)\n    assert(\n      mergeMetrics.getOrElse(\"numTargetDeletionVectorsRemoved\", -1) === numDeletionVectorsRemoved)\n    assert(\n      mergeMetrics.getOrElse(\"numTargetDeletionVectorsUpdated\", -1) === numDeletionVectorsUpdated)\n  }\n\n  test(s\"Merge with DVs metrics - Incremental Updates\") {\n    withTempDir { dir =>\n      val sourcePath = s\"$dir/source\"\n\n      spark.range(0, 10, 2).write.format(\"delta\").save(sourcePath)\n      append(spark.range(10).toDF())\n\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier t\",\n        src = s\"delta.`$sourcePath` s\",\n        cond = \"t.id = s.id\",\n        clauses = updateNotMatched(set = \"id = t.id * 10\"))\n\n      checkAnswer(readDeltaTableByIdentifier(), Seq(0, 10, 2, 30, 4, 50, 6, 70, 8, 90).toDF(\"id\"))\n\n      assertOperationalDVMetrics(\n        deltaLog.dataPath.toString,\n        numDeletedRows = 0,\n        numUpdatedRows = 5,\n        numCopiedRows = 0,\n        numTargetFilesRemoved = 0, // No files were fully deleted.\n        numDeletionVectorsAdded = 2,\n        numDeletionVectorsRemoved = 0,\n        numDeletionVectorsUpdated = 0)\n\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier t\",\n        src = s\"delta.`$sourcePath` s\",\n        cond = \"t.id = s.id\",\n        clauses = delete(condition = \"t.id = 2\"))\n\n      checkAnswer(readDeltaTableByIdentifier(), Seq(0, 10, 30, 4, 50, 6, 70, 8, 90).toDF(\"id\"))\n\n      assertOperationalDVMetrics(\n        deltaLog.dataPath.toString,\n        numDeletedRows = 1,\n        numUpdatedRows = 0,\n        numCopiedRows = 0,\n        numTargetFilesRemoved = 0,\n        numDeletionVectorsAdded = 1, // Updating a DV equals removing and adding.\n        numDeletionVectorsRemoved = 1, // Updating a DV equals removing and adding.\n        numDeletionVectorsUpdated = 1)\n\n      // Delete all rows from a file.\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier t\",\n        src = s\"delta.`$sourcePath` s\",\n        cond = \"t.id = s.id\",\n        clauses = delete(condition = \"t.id < 5\"))\n\n      checkAnswer(readDeltaTableByIdentifier(), Seq(10, 30, 50, 6, 70, 8, 90).toDF(\"id\"))\n\n      assertOperationalDVMetrics(\n        deltaLog.dataPath.toString,\n        numDeletedRows = 2,\n        numUpdatedRows = 0,\n        numCopiedRows = 0,\n        numTargetFilesRemoved = 1,\n        numDeletionVectorsAdded = 0,\n        numDeletionVectorsRemoved = 1,\n        numDeletionVectorsUpdated = 0)\n    }\n  }\n\n  test(s\"Merge with DVs metrics - delete entire file\") {\n    withTempDir { dir =>\n      val sourcePath = s\"$dir/source\"\n\n      spark.range(0, 7).write.format(\"delta\").save(sourcePath)\n      append(spark.range(10).toDF())\n\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier t\",\n        src = s\"delta.`$sourcePath` s\",\n        cond = \"t.id = s.id\",\n        clauses = update(set = \"id = t.id * 10\"))\n\n      checkAnswer(readDeltaTableByIdentifier(), Seq(0, 10, 20, 30, 40, 50, 60, 7, 8, 9).toDF(\"id\"))\n\n      assertOperationalDVMetrics(\n        deltaLog.dataPath.toString,\n        numDeletedRows = 0,\n        numUpdatedRows = 7,\n        numCopiedRows = 0, // No rows were copied.\n        numTargetFilesRemoved = 1, // 1 file was removed entirely.\n        numDeletionVectorsAdded = 1, // 1 file was deleted partially.\n        numDeletionVectorsRemoved = 0,\n        numDeletionVectorsUpdated = 0)\n    }\n  }\n\n  test(s\"Verify error is produced when paths are not joined correctly\") {\n    withTempDir { dir =>\n      val sourcePath = s\"$dir/source\"\n      val targetPath = s\"$dir/target\"\n\n      spark.range(0, 10, 2).write.format(\"delta\").save(sourcePath)\n      spark.range(10).write.format(\"delta\").save(targetPath)\n\n      // Execute buildRowIndexSetsForFilesMatchingCondition with a corrupted touched files list.\n      val sourceDF = io.delta.tables.DeltaTable.forPath(sourcePath).toDF\n      val targetDF = io.delta.tables.DeltaTable.forPath(targetPath).toDF\n      val targetLog = DeltaLog.forTable(spark, targetPath)\n      val condition = col(\"s.id\") === col(\"t.id\")\n      val allFiles = targetLog.update().allFiles.collect().toSeq\n      assert(allFiles.size === 2)\n      val corruptedFiles = Seq(\n        allFiles.head,\n        allFiles.last.copy(path = \"corruptedPath\"))\n      val txn = targetLog.startTransaction(catalogTableOpt = None)\n\n      val fileIndex = new TahoeBatchFileIndex(\n        spark,\n        actionType = \"merge\",\n        addFiles = allFiles,\n        deltaLog = targetLog,\n        path = targetLog.dataPath,\n        snapshot = txn.snapshot)\n\n      val targetDFWithMetadata = DMLWithDeletionVectorsHelper.createTargetDfForScanningForMatches(\n        spark,\n        targetDF.queryExecution.logical,\n        fileIndex)\n      val e = intercept[SparkException] {\n        DeletionVectorBitmapGenerator.buildRowIndexSetsForFilesMatchingCondition(\n          spark,\n          txn,\n          tableHasDVs = true,\n          targetDf = sourceDF.as(\"s\").join(targetDFWithMetadata.as(\"t\"), condition),\n          candidateFiles = corruptedFiles,\n          condition = condition.expr,\n          fileNameColumnOpt = Option(col(\"s._metadata.file_name\")),\n          rowIndexColumnOpt = Option(col(\"s._metadata.row_index\"))\n        )\n      }\n      assert(e.getCause.getMessage.contains(\"Encountered a non matched file path.\"))\n    }\n  }\n}\n\ntrait MergeCDCWithDVsMixin extends QueryTest with MergeCDCMixin with DeletionVectorsTestUtils {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark, merge = true)\n  }\n\n  override def excluded: Seq[String] = {\n    /**\n     * Merge commands that result to no actions do not generate a new commit when DVs are enabled.\n     * We correct affected tests by changing the expected CDC result (Create table CDC).\n     */\n    val miscFailures = \"merge CDC - all conditions failed for all rows\"\n\n    super.excluded :+ miscFailures\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoMaterializeSourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable\nimport scala.concurrent.duration._\nimport scala.reflect.ClassTag\nimport scala.util.control.NonFatal\n\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions, UsageRecord}\nimport org.apache.spark.sql.delta.DeltaTestUtils._\nimport org.apache.spark.sql.delta.commands.merge.{MergeIntoMaterializeSourceError, MergeIntoMaterializeSourceErrorType, MergeIntoMaterializeSourceReason, MergeStats}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.scalactic.source.Position\nimport org.scalatest.Tag\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.expressions.{AttributeReference, EqualTo, Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.execution.{FilterExec, LogicalRDD, RDDScanExec, SQLExecution}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.storage.StorageLevel\nimport org.apache.spark.util.Utils\n\ntrait MergeIntoMaterializeSourceMixin\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils\n    with DeltaTestUtilsBase {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // trigger source materialization in all tests\n    spark.conf.set(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key, \"all\")\n  }\n\n  // Runs a merge query with source materialization, while a killer thread tries to unpersist it.\n  protected def testMergeMaterializedSourceUnpersist(\n      tblName: String, numKills: Int): Seq[UsageRecord] = {\n    val maxAttempts = spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_MAX_ATTEMPTS)\n\n    // when we ask to join the killer thread, it should exit in the next iteration.\n    val killerThreadJoinTimeoutMs = 10000\n    // sleep between attempts to unpersist\n    val killerIntervalMs = 1\n\n    // Data does not need to be big; there is enough latency to unpersist even with small data.\n    val targetDF = spark.range(100).toDF(\"id\")\n    targetDF.write.format(\"delta\").saveAsTable(tblName)\n    spark.range(90, 120).toDF(\"id\").createOrReplaceTempView(\"s\")\n    val mergeQuery =\n      s\"MERGE INTO $tblName t USING s ON t.id = s.id \" +\n      \"WHEN MATCHED THEN DELETE WHEN NOT MATCHED THEN INSERT *\"\n\n    // Killer thread tries to unpersist any persisted mergeMaterializedSource RDDs,\n    // until it has seen more than numKills distinct ones (from distinct Merge retries)\n    @volatile var finished = false\n    @volatile var invalidStorageLevel: Option[String] = None\n    val killerThread = new Thread() {\n      override def run(): Unit = {\n        val seenSources = mutable.Set[Int]()\n        while (!finished) {\n          sparkContext.getPersistentRDDs.foreach { case (rddId, rdd) =>\n            if (rdd.name == \"mergeMaterializedSource\") {\n              if (!seenSources.contains(rddId)) {\n                logInfo(s\"First time seeing mergeMaterializedSource with id=$rddId\")\n                seenSources.add(rddId)\n              }\n              if (seenSources.size > numKills) {\n                // already unpersisted numKills different source materialization attempts,\n                // the killer can retire\n                logInfo(s\"seenSources.size=${seenSources.size}. Proceeding to finish.\")\n                finished = true\n              } else {\n                // Need to wait until it is actually checkpointed, otherwise if we try to unpersist\n                // before it starts to actually persist it fails with\n                // java.lang.AssertionError: assumption failed:\n                // Storage level StorageLevel(1 replicas) is not appropriate for local checkpointing\n                // (this wouldn't happen in real world scenario of losing the block because executor\n                // was lost; there nobody manipulates with StorageLevel; if failure happens during\n                // computation of the materialized rdd, the task would be reattempted using the\n                // regular task retry mechanism)\n                if (rdd.isCheckpointed) {\n                  // Use this opportunity to test if the source has the correct StorageLevel.\n                  val expectedStorageLevel = StorageLevel.fromString(\n                    if (seenSources.size == 1) {\n                      spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL)\n                    } else if (seenSources.size == 2) {\n                      spark.conf.get(\n                        DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL_FIRST_RETRY)\n                    } else {\n                      spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL_RETRY)\n                    }\n                  )\n                  val rddStorageLevel = rdd.getStorageLevel\n                  if (rddStorageLevel != expectedStorageLevel) {\n                    invalidStorageLevel =\n                      Some(s\"For attempt ${seenSources.size} of materialized source expected \" +\n                        s\"$expectedStorageLevel but got ${rddStorageLevel}\")\n                    finished = true\n                  }\n                  logInfo(s\"Unpersisting mergeMaterializedSource with id=$rddId\")\n                  // don't make it blocking, so that the killer turns around quickly and is ready\n                  // for the next kill when Merge retries\n                  rdd.unpersist(blocking = false)\n                }\n              }\n            }\n          }\n          Thread.sleep(killerIntervalMs)\n        }\n        logInfo(s\"seenSources.size=${seenSources.size}. Proceeding to finish.\")\n      }\n    }\n    killerThread.start()\n\n    val events = Log4jUsageLogger.track {\n      try {\n        sql(mergeQuery)\n      } catch {\n        case NonFatal(ex) =>\n          if (numKills < maxAttempts) {\n            // The merge should succeed with retries\n            throw ex\n          }\n      } finally {\n        finished = true // put the killer to rest, if it didn't retire already\n        killerThread.join(killerThreadJoinTimeoutMs)\n        assert(!killerThread.isAlive)\n      }\n    }.filter(_.metric == MetricDefinitions.EVENT_TAHOE.name)\n\n    // If killer thread recorded an invalid StorageLevel, throw it here\n    assert(invalidStorageLevel.isEmpty, invalidStorageLevel.toString)\n\n    events\n  }\n}\n\ntrait MergeIntoMaterializeSourceErrorTests extends MergeIntoMaterializeSourceMixin {\n  import testImplicits._\n\n  // Test error message that we check if blocks of materialized source RDD were evicted.\n  test(\"missing RDD blocks error message\") {\n    val checkpointedDf = sql(\"select * from range(10)\")\n      .localCheckpoint(eager = false)\n    val rdd = checkpointedDf.queryExecution.analyzed.asInstanceOf[LogicalRDD].rdd\n    checkpointedDf.collect() // trigger lazy materialization\n    rdd.unpersist()\n    val ex = intercept[Exception] {\n      checkpointedDf.collect()\n    }\n    assert(ex.isInstanceOf[SparkException], ex)\n    val sparkEx = ex.asInstanceOf[SparkException]\n    assert(\n      sparkEx.getErrorClass == \"CHECKPOINT_RDD_BLOCK_ID_NOT_FOUND\" &&\n        sparkEx.getMessageParameters.get(\"rddBlockId\").contains(s\"rdd_${rdd.id}\"))\n  }\n\n  for {\n    materialized <- BOOLEAN_DOMAIN\n  } test(s\"merge logs out of disk errors - materialized=$materialized\") {\n    import DeltaSQLConf.MergeMaterializeSource\n    withSQLConf(\n        DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key ->\n          (if (materialized) MergeMaterializeSource.AUTO else MergeMaterializeSource.NONE)) {\n      val injectEx = new java.io.IOException(\"No space left on device\")\n      testWithCustomErrorInjected[SparkException](injectEx) { (thrownEx, errorOpt) =>\n        // Compare messages instead of instances, since the equals method for these exceptions\n        // takes more into account.\n        assert(thrownEx.getCause.getMessage === injectEx.getMessage)\n        if (materialized) {\n          assert(errorOpt.isDefined)\n          val error = errorOpt.get\n          assert(error.errorType == MergeIntoMaterializeSourceErrorType.OUT_OF_DISK.toString)\n          assert(error.attempt == 1)\n          val storageLevel = StorageLevel.fromString(\n              spark.conf.get(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_RDD_STORAGE_LEVEL))\n          assert(error.materializedSourceRDDStorageLevel == storageLevel.toString)\n        } else {\n          assert(errorOpt.isEmpty)\n        }\n      }\n    }\n  }\n\n  test(\"merge rethrows arbitrary errors\") {\n    val injectEx = new RuntimeException(\"test\")\n    testWithCustomErrorInjected[SparkException](injectEx) { (thrownEx, error) =>\n      // Compare messages instead of instances, since the equals method for these exceptions\n      // takes more into account.\n      assert(thrownEx.getCause.getMessage === injectEx.getMessage)\n      assert(error.isEmpty)\n    }\n  }\n\n  private def testWithCustomErrorInjected[Intercept >: Null <: Exception with AnyRef : ClassTag](\n      inject: Exception)(\n      handle: (Intercept, Option[MergeIntoMaterializeSourceError]) => Unit): Unit = {\n    {\n      val tblName = \"target\"\n      withTable(tblName) {\n        val targetDF = spark.range(10).toDF(\"id\").withColumn(\"value\", rand())\n        targetDF.write.format(\"delta\").saveAsTable(tblName)\n        spark\n          .range(10)\n          .mapPartitions { x =>\n            throw inject\n            x\n          }\n          .toDF(\"id\")\n          .withColumn(\"value\", rand())\n          .createOrReplaceTempView(\"s\")\n        var thrownException: Intercept = null\n        val events = Log4jUsageLogger\n          .track {\n            thrownException = intercept[Intercept] {\n              sql(s\"MERGE INTO $tblName t USING s ON t.id = s.id \" +\n                s\"WHEN MATCHED THEN DELETE WHEN NOT MATCHED THEN INSERT *\")\n            }\n          }\n          .filter { e =>\n            e.metric == MetricDefinitions.EVENT_TAHOE.name &&\n            e.tags.get(\"opType\").contains(MergeIntoMaterializeSourceError.OP_TYPE)\n          }\n        val error = events.headOption\n          .map(e => JsonUtils.fromJson[MergeIntoMaterializeSourceError](e.blob))\n        handle(thrownException, error)\n      }\n    }\n  }\n\n  private def testMergeMaterializeSourceUnpersistRetries = {\n    val maxAttempts = DeltaSQLConf.MERGE_MATERIALIZE_SOURCE_MAX_ATTEMPTS.defaultValue.get\n    val tblName = \"target\"\n\n    // For 1 to maxAttempts - 1 RDD block lost failures, merge should retry and succeed.\n    for {\n      kills <- 1 to maxAttempts - 1\n    } {\n      test(s\"materialize source unpersist with $kills kill attempts succeeds\") {\n        withTable(tblName) {\n          val allDeltaEvents = testMergeMaterializedSourceUnpersist(tblName, kills)\n          val events =\n            allDeltaEvents.filter(_.tags.get(\"opType\").contains(\"delta.dml.merge.stats\"))\n          assert(events.length == 1, s\"allDeltaEvents:\\n$allDeltaEvents\")\n          val mergeStats = JsonUtils.fromJson[MergeStats](events(0).blob)\n          assert(mergeStats.materializeSourceAttempts.isDefined, s\"MergeStats:\\n$mergeStats\")\n          assert(\n            mergeStats.materializeSourceAttempts.get == kills + 1,\n            s\"MergeStats:\\n$mergeStats\")\n\n          // Check query result after merge\n          val tab = sql(s\"select * from $tblName order by id\")\n            .collect()\n            .map(row => row.getLong(0))\n            .toSeq\n          assert(tab == (0L until 90L) ++ (100L until 120L))\n        }\n      }\n    }\n\n    // Eventually it should fail after exceeding maximum number of attempts.\n    test(s\"materialize source unpersist with $maxAttempts kill attempts fails\") {\n      withTable(tblName) {\n        val allDeltaEvents = testMergeMaterializedSourceUnpersist(tblName, maxAttempts)\n        val events = allDeltaEvents\n          .filter(_.tags.get(\"opType\").contains(MergeIntoMaterializeSourceError.OP_TYPE))\n        assert(events.length == 1, s\"allDeltaEvents:\\n$allDeltaEvents\")\n        val error = JsonUtils.fromJson[MergeIntoMaterializeSourceError](events(0).blob)\n        assert(error.errorType == MergeIntoMaterializeSourceErrorType.RDD_BLOCK_LOST.toString)\n        assert(error.attempt == maxAttempts)\n      }\n    }\n  }\n  testMergeMaterializeSourceUnpersistRetries\n}\n\ntrait MergeIntoMaterializeSourceTests extends MergeIntoMaterializeSourceMixin {\n  import testImplicits._\n\n  private def getHints(df: => DataFrame): Seq[(Seq[ResolvedHint], JoinHint)] = {\n    val plans = withAllPlansCaptured(spark) {\n      df\n    }\n    var plansWithMaterializedSource = 0\n    val hints = plans.flatMap { p =>\n      val materializedSourceExists = p.analyzed.exists {\n        case l: LogicalRDD if l.rdd.name == \"mergeMaterializedSource\" => true\n        case _ => false\n      }\n      if (materializedSourceExists) {\n        // If it is a plan with materialized source, there should be exactly one join\n        // of target and source. We collect resolved hints from analyzed plans, and the hint\n        // applied to the join from optimized plan.\n        plansWithMaterializedSource += 1\n        val hints = p.analyzed.collect {\n          case h: ResolvedHint => h\n        }\n        val joinHints = p.optimized.collect {\n          case j: Join => j.hint\n        }\n        assert(joinHints.length == 1, s\"Got $joinHints\")\n        val joinHint = joinHints.head\n\n        // Only preserve join strategy hints, because we are testing with these.\n        // Other hints may be added by MERGE internally, e.g. hints to force DFP/DPP, that\n        // we don't want to be considering here.\n        val retHints = hints\n          .filter(_.hints.strategy.nonEmpty)\n        def retJoinHintInfo(hintInfo: Option[HintInfo]): Option[HintInfo] = hintInfo match {\n          case Some(h) if h.strategy.nonEmpty => Some(HintInfo(strategy = h.strategy))\n          case _ => None\n        }\n        val retJoinHint = joinHint.copy(\n          leftHint = retJoinHintInfo(joinHint.leftHint),\n          rightHint = retJoinHintInfo(joinHint.rightHint)\n        )\n\n        Some((retHints, retJoinHint))\n      } else {\n        None\n      }\n    }\n    assert(plansWithMaterializedSource == 2,\n      s\"2 plans should have materialized source, but got: $plans\")\n    hints\n  }\n\n  test(s\"materialize source preserves dataframe hints\") {\n    withTable(\"A\", \"B\", \"T\") {\n      sql(\"select id, id as v from range(50000)\").write.format(\"delta\").saveAsTable(\"T\")\n      sql(\"select id, id+2 as v from range(10000)\").write.format(\"csv\").saveAsTable(\"A\")\n      sql(\"select id, id*2 as v from range(1000)\").write.format(\"csv\").saveAsTable(\"B\")\n\n      // Manually added broadcast hint will mess up the expected hints hence disable it\n      withSQLConf(\n        SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> \"-1\") {\n        // Simple BROADCAST hint\n        val hSimple = getHints(\n          sql(\"MERGE INTO T USING (SELECT /*+ BROADCAST */ * FROM A) s ON T.id = s.id\" +\n            \" WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *\")\n        )\n        hSimple.foreach { case (hints, joinHint) =>\n          assert(hints.length == 1)\n          assert(hints.head.hints == HintInfo(strategy = Some(BROADCAST)))\n          assert(joinHint == JoinHint(Some(HintInfo(strategy = Some(BROADCAST))), None))\n        }\n\n        // Simple MERGE hint\n        val hSimpleMerge = getHints(\n          sql(\"MERGE INTO T USING (SELECT /*+ MERGE */ * FROM A) s ON T.id = s.id\" +\n            \" WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *\")\n        )\n        hSimpleMerge.foreach { case (hints, joinHint) =>\n          assert(hints.length == 1)\n          assert(hints.head.hints == HintInfo(strategy = Some(SHUFFLE_MERGE)))\n          assert(joinHint == JoinHint(Some(HintInfo(strategy = Some(SHUFFLE_MERGE))), None))\n        }\n\n        // Aliased hint\n        val hAliased = getHints(\n          sql(\"MERGE INTO T USING \" +\n            \"(SELECT /*+ BROADCAST(FOO) */ * FROM (SELECT * FROM A) FOO) s ON T.id = s.id\" +\n            \" WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *\")\n        )\n        hAliased.foreach { case (hints, joinHint) =>\n          assert(hints.length == 1)\n          assert(hints.head.hints == HintInfo(strategy = Some(BROADCAST)))\n          assert(joinHint == JoinHint(Some(HintInfo(strategy = Some(BROADCAST))), None))\n        }\n\n        // Aliased hint - hint propagation does not work from under an alias\n        // (remove if this ever gets implemented in the hint framework)\n        val hAliasedInner = getHints(\n          sql(\"MERGE INTO T USING \" +\n            \"(SELECT /*+ BROADCAST(A) */ * FROM (SELECT * FROM A) FOO) s ON T.id = s.id\" +\n            \" WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *\")\n        )\n        hAliasedInner.foreach { case (hints, joinHint) =>\n          assert(hints.length == 0)\n          assert(joinHint == JoinHint(None, None))\n        }\n\n        // This hint applies to the join inside the source, not to the source as a whole\n        val hJoinInner = getHints(\n          sql(\"MERGE INTO T USING \" +\n            \"(SELECT /*+ BROADCAST(A) */ A.* FROM A JOIN B WHERE A.id = B.id) s ON T.id = s.id\" +\n            \" WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *\")\n        )\n        hJoinInner.foreach { case (hints, joinHint) =>\n          assert(hints.length == 0)\n          assert(joinHint == JoinHint(None, None))\n        }\n\n        // Two hints - top one takes effect\n        val hTwo = getHints(\n          sql(\"MERGE INTO T USING (SELECT /*+ BROADCAST, MERGE */ * FROM A) s ON T.id = s.id\" +\n            \" WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *\")\n        )\n        hTwo.foreach { case (hints, joinHint) =>\n          assert(hints.length == 2)\n          assert(hints(0).hints == HintInfo(strategy = Some(BROADCAST)))\n          assert(hints(1).hints == HintInfo(strategy = Some(SHUFFLE_MERGE)))\n          // top one takes effect\n          assert(joinHint == JoinHint(Some(HintInfo(strategy = Some(BROADCAST))), None))\n        }\n      }\n    }\n  }\n\n  test(\"materialize source for non-deterministic source formats\") {\n    val targetSchema = StructType(Array(\n      StructField(\"id\", IntegerType, nullable = false),\n      StructField(\"value\", StringType, nullable = true)))\n    val targetData = Seq(\n      Row(1, \"update\"),\n      Row(2, \"skip\"),\n      Row(3, \"delete\"))\n    val sourceData = Seq(1, 3, 4).toDF(\"id\")\n    val expectedResult = Seq(\n      Row(1, \"new\"), // Updated\n      Row(2, \"skip\"), // Copied\n      // 3 is deleted\n      Row(4, \"new\")) // Inserted\n\n    // There are more, but these are easiest to test for.\n    val nonDeterministicFormats = List(\"parquet\", \"json\")\n\n    // Return MergeIntoMaterializeSourceReason string\n    def executeMerge(sourceDf: DataFrame): String = {\n      val sourceDfWithAction = sourceDf.withColumn(\"value\", lit(\"new\"))\n      var materializedSource: String = \"\"\n      withTable(\"target\") {\n        val targetRdd = spark.sparkContext.parallelize(targetData)\n        val targetDf = spark.createDataFrame(targetRdd, targetSchema)\n        targetDf.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"target\")\n        val targetTable = io.delta.tables.DeltaTable.forName(\"target\")\n\n        val events: Seq[UsageRecord] = Log4jUsageLogger.track {\n          targetTable.merge(sourceDfWithAction, col(\"target.id\") === sourceDfWithAction(\"id\"))\n            .whenMatched(col(\"target.value\") === lit(\"update\")).updateAll()\n            .whenMatched(col(\"target.value\") === lit(\"delete\")).delete()\n            .whenNotMatched().insertAll()\n            .execute()\n        }\n\n        // Can't return values out of withTable.\n        materializedSource = mergeSourceMaterializeReason(events)\n\n        checkAnswer(\n          spark.read.format(\"delta\").table(\"target\"),\n          expectedResult)\n      }\n      materializedSource\n    }\n\n    def checkSourceMaterialization(\n        format: String,\n        reason: String): Unit = {\n      // Test once by name and once using path, as they produce different plans.\n      withTable(\"source\") {\n        sourceData.write.format(format).saveAsTable(\"source\")\n        val sourceDf = spark.read.format(format).table(\"source\")\n        assert(executeMerge(sourceDf) == reason, s\"Wrong materialization reason for $format\")\n      }\n\n      withTempPath { sourcePath =>\n        sourceData.write.format(format).save(sourcePath.toString)\n        val sourceDf = spark.read.format(format).load(sourcePath.toString)\n        assert(executeMerge(sourceDf) == reason, s\"Wrong materialization reason for $format\")\n      }\n    }\n\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n      for (format <- nonDeterministicFormats) {\n        checkSourceMaterialization(\n          format,\n          reason = MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA.toString)\n      }\n\n      // Delta should not materialize source.\n      checkSourceMaterialization(\n        \"delta\", reason = MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO.toString)\n    }\n\n    // Test with non-Delta sources in subqueries.\n    def checkSourceMaterializationSubquery(\n        delta: String,\n        filterSub: String,\n        projectSub: String,\n        nestedFilterSub: String,\n        nestedProjectSub: String,\n        testType: String,\n        reason: String): Unit = {\n      val df = spark.sql(\n        s\"\"\"\n           |SELECT\n           |  CASE WHEN id IN\n           |    (SELECT id kk FROM $projectSub WHERE id IN (SELECT * FROM $nestedFilterSub))\n           |  THEN id ELSE -1 END AS id,\n           |  0.5 AS value\n           |FROM $delta\n           |WHERE id IN\n           |  (SELECT CASE WHEN id IN (SELECT * FROM $nestedProjectSub) THEN id ELSE -1 END kk\n           |   FROM $filterSub)\n           |\"\"\".stripMargin)\n      assert(executeMerge(df) == reason, s\"Wrong materialization reason with $testType subquery\")\n    }\n\n    def checkSourceMaterializationSubqueries(deltaSource: String, nonDeltaSource: String): Unit = {\n      checkSourceMaterializationSubquery(\n        delta = deltaSource,\n        filterSub = deltaSource,\n        projectSub = deltaSource,\n        nestedFilterSub = deltaSource,\n        nestedProjectSub = deltaSource,\n        testType = \"all Delta\",\n        reason = MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO.toString)\n\n      checkSourceMaterializationSubquery(\n        delta = deltaSource,\n        filterSub = nonDeltaSource,\n        projectSub = deltaSource,\n        nestedFilterSub = deltaSource,\n        nestedProjectSub = deltaSource,\n        testType = \"non-Delta filter\",\n        reason = MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA.toString)\n\n      checkSourceMaterializationSubquery(\n        delta = deltaSource,\n        filterSub = deltaSource,\n        projectSub = nonDeltaSource,\n        nestedFilterSub = deltaSource,\n        nestedProjectSub = deltaSource,\n        testType = \"non-Delta project\",\n        reason = MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA.toString)\n\n      checkSourceMaterializationSubquery(\n        delta = deltaSource,\n        filterSub = deltaSource,\n        projectSub = deltaSource,\n        nestedFilterSub = nonDeltaSource,\n        nestedProjectSub = deltaSource,\n        testType = \"non-Delta nested filter\",\n        reason = MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA.toString)\n\n      checkSourceMaterializationSubquery(\n        delta = deltaSource,\n        filterSub = deltaSource,\n        projectSub = deltaSource,\n        nestedFilterSub = deltaSource,\n        nestedProjectSub = nonDeltaSource,\n        testType = \"non-Delta nested project\",\n        reason = MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA.toString)\n    }\n\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n      // Test once by name and once using path, as they produce different plans.\n      withTable(\"deltaSource\", \"nonDeltaSource\") {\n        sourceData.write.format(\"delta\").saveAsTable(\"deltaSource\")\n        sourceData.write.format(\"parquet\").saveAsTable(\"nonDeltaSource\")\n        checkSourceMaterializationSubqueries(\"deltaSource\", \"nonDeltaSource\")\n      }\n\n      withTempPath { deltaSourcePath =>\n        sourceData.write.format(\"delta\").save(deltaSourcePath.toString)\n        withTempPath { nonDeltaSourcePath =>\n          sourceData.write.format(\"parquet\").save(nonDeltaSourcePath.toString)\n          checkSourceMaterializationSubqueries(\n            s\"delta.`$deltaSourcePath`\", s\"parquet.`$nonDeltaSourcePath`\")\n        }\n      }\n    }\n\n    // Mixed safe/unsafe queries should materialize source.\n    def checkSourceMaterializationForMixedSources(\n        format1: String,\n        format2: String,\n        shouldMaterializeSource: Boolean): Unit = {\n\n      def checkWithSources(source1Df: DataFrame, source2Df: DataFrame): Unit = {\n        val sourceDf = source1Df.union(source2Df)\n        val materializeReason = executeMerge(sourceDf)\n        if (shouldMaterializeSource) {\n          assert(materializeReason ==\n            MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_NON_DELTA.toString,\n            s\"$format1 union $format2 are not deterministic as a source and should materialize.\")\n        } else {\n          assert(materializeReason ==\n            MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO.toString,\n            s\"$format1 union $format2 is deterministic as a source and should not materialize.\")\n        }\n      }\n\n      // Test once by name and once using path, as they produce different plans.\n      withTable(\"source1\", \"source2\") {\n        sourceData.filter(col(\"id\") < 2).write.format(format1).saveAsTable(\"source1\")\n        val source1Df = spark.read.format(format1).table(\"source1\")\n        sourceData.filter(col(\"id\") >= 2).write.format(format2).saveAsTable(\"source2\")\n        val source2Df = spark.read.format(format2).table(\"source2\")\n        checkWithSources(source1Df, source2Df)\n      }\n\n      withTempPaths(2) { case Seq(source1, source2) =>\n        sourceData.filter(col(\"id\") < 2).write\n          .mode(\"overwrite\").format(format1).save(source1.toString)\n        val source1Df = spark.read.format(format1).load(source1.toString)\n        sourceData.filter(col(\"id\") >= 2).write\n          .mode(\"overwrite\").format(format2).save(source2.toString)\n        val source2Df = spark.read.format(format2).load(source2.toString)\n        checkWithSources(source1Df, source2Df)\n      }\n    }\n\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n      val allFormats = \"delta\" :: nonDeterministicFormats\n      // Try all combinations\n      for {\n        format1 <- allFormats\n        format2 <- allFormats\n      } checkSourceMaterializationForMixedSources(\n        format1 = format1,\n        format2 = format2,\n        shouldMaterializeSource = !(format1 == \"delta\" && format2 == \"delta\"))\n    }\n\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"none\") {\n      // With \"none\", it should not materialize, even though parquet is non-deterministic.\n      checkSourceMaterialization(\n        \"parquet\",\n        reason = MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_NONE.toString)\n    }\n\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"all\") {\n      // With \"all\"\", it should materialize, even though Delta is deterministic.\n      checkSourceMaterialization(\n        \"delta\",\n        reason = MergeIntoMaterializeSourceReason.MATERIALIZE_ALL.toString)\n    }\n  }\n\n  test(\"materialize source for non-deterministic source queries - udf\") {\n    {\n      val targetSchema = StructType(Array(\n        StructField(\"id\", IntegerType, nullable = false),\n        StructField(\"value\", IntegerType, nullable = true)))\n      val targetData = Seq(\n        Row(1, 0),\n        Row(2, 0),\n        Row(3, 0))\n      val sourceData = Seq(1, 3).toDF(\"id\")\n      withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n        withTable(\"target\", \"source\") {\n          val targetRdd = spark.sparkContext.parallelize(targetData)\n          val targetDf = spark.createDataFrame(targetRdd, targetSchema)\n          targetDf.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"target\")\n          val targetTable = io.delta.tables.DeltaTable.forName(\"target\")\n\n          sourceData.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"source\")\n          val f = udf { () => 1L }\n          val sourceDf = spark.table(\"source\").withColumn(\"value\", f())\n\n          val events: Seq[UsageRecord] = Log4jUsageLogger.track {\n            targetTable\n              .merge(sourceDf, col(\"target.id\") === sourceDf(\"id\"))\n              .whenMatched(col(\"target.value\") > sourceDf(\"value\")).delete()\n              .whenMatched().updateAll()\n              .whenNotMatched().insertAll()\n              .execute()\n          }\n\n          val materializeReason = mergeSourceMaterializeReason(events)\n          assert(materializeReason == MergeIntoMaterializeSourceReason.\n            NON_DETERMINISTIC_SOURCE_WITH_DETERMINISTIC_UDF.toString,\n            \"Source has a udf and merge should have materialized the source.\")\n        }\n      }\n    }\n  }\n\n  test(\"materialize source for non-deterministic source queries - rand expr\") {\n    val targetSchema = StructType(Array(\n      StructField(\"id\", IntegerType, nullable = false),\n      StructField(\"value\", FloatType, nullable = true)))\n    val targetData = Seq(\n      Row(1, 0.5f),\n      Row(2, 0.3f),\n      Row(3, 0.8f))\n    val sourceData = Seq(1, 3).toDF(\"id\")\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n\n      def executeMerge(sourceDf: DataFrame): Unit = {\n        val nonDeterministicSourceDf = sourceDf.withColumn(\"value\", rand())\n        withTable(\"target\") {\n          val targetRdd = spark.sparkContext.parallelize(targetData)\n          val targetDf = spark.createDataFrame(targetRdd, targetSchema)\n          targetDf.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"target\")\n          val targetTable = io.delta.tables.DeltaTable.forName(\"target\")\n\n          val events: Seq[UsageRecord] = Log4jUsageLogger.track {\n            targetTable\n              .merge(nonDeterministicSourceDf, col(\"target.id\") === nonDeterministicSourceDf(\"id\"))\n              .whenMatched(col(\"target.value\") > nonDeterministicSourceDf(\"value\")).delete()\n              .whenMatched().updateAll()\n              .whenNotMatched().insertAll()\n              .execute()\n          }\n\n          val materializeReason = mergeSourceMaterializeReason(events)\n          assert(materializeReason ==\n              MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_OPERATORS.toString,\n            \"Source has non deterministic operations and should have materialized source.\")\n        }\n      }\n\n      // Test once by name and once using path, as they produce different plans.\n      withTable(\"source\") {\n        sourceData.write.format(\"delta\").saveAsTable(\"source\")\n        val sourceDf = spark.read.format(\"delta\").table(\"source\")\n        executeMerge(sourceDf)\n      }\n\n      withTempPath { sourcePath =>\n        sourceData.write.format(\"delta\").save(sourcePath.toString)\n        val sourceDf = spark.read.format(\"delta\").load(sourcePath.toString)\n        executeMerge(sourceDf)\n      }\n    }\n  }\n\n  test(\"don't materialize source for deterministic source queries with current_date\") {\n    val targetSchema = StructType(Array(\n      StructField(\"id\", IntegerType, nullable = false),\n      StructField(\"date\", DateType, nullable = true)))\n    val targetData = Seq(\n      Row(1, java.sql.Date.valueOf(\"2022-01-01\")),\n      Row(2, java.sql.Date.valueOf(\"2022-02-01\")),\n      Row(3, java.sql.Date.valueOf(\"2022-03-01\")))\n    val sourceData = Seq(1, 3).toDF(\"id\")\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n\n      def executeMerge(sourceDf: DataFrame): Unit = {\n        val nonDeterministicSourceDf = sourceDf.withColumn(\"date\", current_date())\n        withTable(\"target\") {\n          val targetRdd = spark.sparkContext.parallelize(targetData)\n          val targetDf = spark.createDataFrame(targetRdd, targetSchema)\n          targetDf.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"target\")\n          val targetTable = io.delta.tables.DeltaTable.forName(\"target\")\n\n          val events: Seq[UsageRecord] = Log4jUsageLogger.track {\n            targetTable\n              .merge(nonDeterministicSourceDf, col(\"target.id\") === nonDeterministicSourceDf(\"id\"))\n              .whenMatched(col(\"target.date\") < nonDeterministicSourceDf(\"date\")).delete()\n              .whenMatched().updateAll()\n              .whenNotMatched().insertAll()\n              .execute()\n          }\n\n          val materializeReason = mergeSourceMaterializeReason(events)\n          assert(materializeReason ==\n            MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO.toString,\n            \"Source query is deterministic and should not be materialized.\")\n        }\n      }\n\n      // Test once by name and once using path, as they produce different plans.\n      withTable(\"source\") {\n        sourceData.write.format(\"delta\").saveAsTable(\"source\")\n        val sourceDf = spark.read.format(\"delta\").table(\"source\")\n        executeMerge(sourceDf)\n      }\n\n      withTempPath { sourcePath =>\n        sourceData.write.format(\"delta\").save(sourcePath.toString)\n        val sourceDf = spark.read.format(\"delta\").load(sourcePath.toString)\n        executeMerge(sourceDf)\n      }\n    }\n  }\n\n  test(\"materialize source for non-deterministic source queries - subquery\") {\n    val sourceDataFrame = spark.range(0, 10)\n      .toDF(\"id\")\n      .withColumn(\"value\", rand())\n\n    val targetDataFrame = spark.range(0, 5)\n      .toDF(\"id\")\n      .withColumn(\"value\", rand())\n\n    withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n\n      // Return MergeIntoMaterializeSourceReason\n      def executeMerge(sourceDf: DataFrame, clue: String): Unit = {\n        withTable(\"target\") {\n          targetDataFrame.write\n            .format(\"delta\")\n            .saveAsTable(\"target\")\n          val targetTable = io.delta.tables.DeltaTable.forName(\"target\")\n\n          val events: Seq[UsageRecord] = Log4jUsageLogger.track {\n            targetTable.merge(sourceDf, col(\"target.id\") === sourceDf(\"id\"))\n              .whenMatched(col(\"target.value\") > sourceDf(\"value\")).delete()\n              .whenMatched().updateAll()\n              .whenNotMatched().insertAll()\n              .execute()\n          }\n\n          val materializeReason = mergeSourceMaterializeReason(events)\n          assert(materializeReason ==\n            MergeIntoMaterializeSourceReason.NON_DETERMINISTIC_SOURCE_OPERATORS.toString,\n            s\"Source query has non deterministic subqueries and should materialize ($clue).\")\n        }\n      }\n\n      def checkSubquery(from: String, subquery: String): Unit = {\n        // check subquery in filter\n        val sourceDfFilterSubquery = spark.sql(\n          s\"\"\"\n             |SELECT id, 0.5 AS value\n             |FROM $from WHERE id IN ($subquery)\n             |\"\"\".stripMargin)\n        executeMerge(sourceDfFilterSubquery,\n          s\"reading from `$from`, subquery `$subquery` in filter\")\n\n        // check subquery in project\n        val sourceDfProjectSubquery = spark.sql(\n          s\"\"\"\n             |SELECT CASE WHEN id IN ($subquery) THEN id ELSE -1 END AS id, 0.5 AS value\n             |FROM $from\n             |\"\"\".stripMargin)\n        executeMerge(sourceDfProjectSubquery,\n          s\"reading from `$from`, subquery `$subquery` in project\")\n      }\n\n      def checkSubqueries(from: String): Unit = {\n        // check non-deterministic plan\n        checkSubquery(from, s\"SELECT id FROM $from WHERE id < rand() * 10\")\n\n        // check too complex plan in subquery, even though plan.deterministic is true\n        val subqueryComplex = s\"SELECT A.id kk FROM $from A JOIN $from B ON A.id = B.id\"\n        assert(spark.sql(subqueryComplex).queryExecution.analyzed.deterministic,\n          \"We want the subquery plan to be deterministic for this test.\")\n        checkSubquery(from, subqueryComplex)\n\n        // check nested subquery\n        val subqueryNestedFilter = s\"SELECT id AS kk FROM $from WHERE id IN ($subqueryComplex)\"\n        checkSubquery(from, subqueryNestedFilter)\n        val subqueryNestedProject =\n          s\"SELECT CASE WHEN id IN ($subqueryComplex) THEN id ELSE -1 END AS kk FROM $from\"\n        checkSubquery(from, subqueryNestedProject)\n\n        // check correlated subquery\n        val subqueryCorrelated = s\"SELECT kk FROM (SELECT id AS kk from $from) WHERE kk = id\"\n        checkSubquery(from, subqueryCorrelated)\n      }\n\n      // Test once by name and once using path, as they produce different plans.\n      withTable(\"source\") {\n        sourceDataFrame.write.format(\"delta\").saveAsTable(\"source\")\n        checkSubqueries(\"source\")\n      }\n\n      withTempPath { sourcePath =>\n        sourceDataFrame.write.format(\"delta\").save(sourcePath.toString)\n        checkSubqueries(s\"delta.`${sourcePath.toString}`\")\n      }\n    }\n  }\n\n  test(\"don't materialize insert only merge\") {\n    val tblName = \"mergeTarget\"\n    withTable(tblName) {\n      val targetDF = spark.range(100).toDF(\"id\")\n      targetDF.write.format(\"delta\").saveAsTable(tblName)\n      spark.range(90, 120).toDF(\"id\").createOrReplaceTempView(\"s\")\n      val mergeQuery =\n        s\"MERGE INTO $tblName t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT *\"\n      val events: Seq[UsageRecord] = Log4jUsageLogger.track {\n        withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n          sql(mergeQuery)\n        }\n      }\n\n      assert(mergeSourceMaterializeReason(events) ==\n        MergeIntoMaterializeSourceReason.NOT_MATERIALIZED_AUTO_INSERT_ONLY.toString)\n\n      checkAnswer(\n        spark.read.format(\"delta\").table(tblName),\n        (0 until 120).map(i => Row(i.toLong)))\n    }\n  }\n\n  test(\"don't unpersist locally checkpointed RDDs\") {\n    val tblName = \"mergeTarget\"\n\n    withTable(tblName) {\n      val targetDF = Seq(\n        (\"2023-01-01\", \"trade1\", 100.0, \"buy\", \"user1\", \"2023-01-01 10:00:00\"),\n        (\"2023-01-02\", \"trade2\", 200.0, \"sell\", \"user2\", \"2023-01-02 11:00:00\")\n      ).toDF(\"block_date\", \"unique_trade_id\", \"transaction_amount\", \"transaction_type\",\n        \"user_id\", \"timestamp\")\n      targetDF.write.format(\"delta\").saveAsTable(tblName)\n\n      Seq(\n        (\"2023-01-01\", \"trade1\", 150.0, \"buy\", \"user1_updated\", \"2023-01-01 12:00:00\"),\n        (\"2023-01-03\", \"trade3\", 300.0, \"buy\", \"user3\", \"2023-01-03 10:00:00\")\n      ).toDF(\"block_date\", \"unique_trade_id\", \"transaction_amount\", \"transaction_type\",\n        \"user_id\", \"timestamp\").createOrReplaceTempView(\"s\")\n\n      val mergeQuery =\n        s\"\"\"MERGE INTO $tblName t USING s\n           |ON t.block_date = s.block_date AND t.unique_trade_id = s.unique_trade_id\n           |WHEN MATCHED THEN UPDATE SET *\n           |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin\n\n      Log4jUsageLogger.track {\n        withSQLConf(DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> \"auto\") {\n          sql(mergeQuery)\n        }\n      }\n\n      // Check if the source RDDs have been locally checkpointed and not unpersisted\n      assert(sparkContext.getPersistentRDDs.values.nonEmpty, \"Source RDDs\" +\n        \" should be locally checkpointed\")\n\n      checkAnswer(\n        spark.read.format(\"delta\").table(tblName),\n        Seq(\n          Row(\"2023-01-01\", \"trade1\", 150.0, \"buy\", \"user1_updated\", \"2023-01-01 12:00:00\"),\n          Row(\"2023-01-02\", \"trade2\", 200.0, \"sell\", \"user2\", \"2023-01-02 11:00:00\"),\n          Row(\"2023-01-03\", \"trade3\", 300.0, \"buy\", \"user3\", \"2023-01-03 10:00:00\"))\n      )\n    }\n  }\n\n  private def mergeStats(events: Seq[UsageRecord]): MergeStats = {\n    val mergeStats = events.filter { e =>\n      e.metric == MetricDefinitions.EVENT_TAHOE.name &&\n        e.tags.get(\"opType\").contains(\"delta.dml.merge.stats\")\n    }\n    assert(mergeStats.size == 1)\n    JsonUtils.fromJson[MergeStats](mergeStats.head.blob)\n  }\n\n  private def mergeSourceMaterializeReason(events: Seq[UsageRecord]): String = {\n    val stats = mergeStats(events)\n    assert(stats.materializeSourceReason.isDefined)\n    stats.materializeSourceReason.get\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoMetricsBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.functions.expr\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Tests for the metrics of MERGE INTO command in Delta log.\n *\n * This test suite checks the values of metrics that are emitted in Delta log by MERGE INTO command,\n * with Changed Data Feed (CDF) enabled/disabled.\n *\n * Metrics related with number of affected rows are deterministic and so the expected values are\n * explicitly checked. Metrics related with number of affected files and execution times are not\n * deterministic, and so we check only their presence and some invariants.\n *\n */\ntrait MergeIntoMetricsBase\n  extends QueryTest\n  with SharedSparkSession { self: DescribeDeltaHistorySuiteBase =>\n\n  import MergeIntoMetricsBase._\n  import testImplicits._\n\n  ///////////////////////\n  // container classes //\n  ///////////////////////\n\n  private case class MergeTestConfiguration(partitioned: Boolean, cdfEnabled: Boolean) {\n\n    /** Return a [[MetricValue]] for this config with the provided default value. */\n    def metricValue(defaultValue: Int): MetricValue = {\n      new MetricValue(this, defaultValue)\n    }\n  }\n\n  /**\n   * Helper class to compute values of metrics that depend on the configuration.\n   *\n   * Objects are initialized with a test configuration and a default value. The value can then be\n   * overwritten with helper methods that check the test config and value() can be called to\n   * retrieve the final expected value for a test.\n   */\n  private class MetricValue(testConfig: MergeTestConfiguration, defaultValue: Int) {\n    private var currentValue: Int = defaultValue\n\n    def value: Int = currentValue\n\n    // e.g. ifCDF\n  }\n\n  ////////////////\n  // test utils //\n  ////////////////\n\n  val testsToIgnore = Seq(\n    // The below tests fail due to incorrect numTargetRowsCopied metric.\n    \"delete-only with condition\",\n    \"delete-only with update with unsatisfied condition\",\n    \"delete-only with unsatisfied condition\",\n    \"delete-only with target-only condition\",\n    \"delete-only with source-only condition\",\n    \"match-only with unsatisfied condition\"\n  )\n\n  // Helper to generate tests with different configurations.\n  private def testMergeMetrics(name: String)(testFn: MergeTestConfiguration => Unit): Unit = {\n    for {\n      partitioned <- Seq(true, false)\n      cdfEnabled <- Seq(true, false)\n    } {\n      val testConfig = MergeTestConfiguration(partitioned = partitioned, cdfEnabled = cdfEnabled)\n      val testName = s\"merge-metrics: $name - Partitioned = $partitioned, CDF = $cdfEnabled\"\n\n      if (testsToIgnore.contains(name)) {\n        // Currently multiple metrics are wrong for Merge. We have added tests for these scenarios\n        // but we need to ignore the failing tests until the metrics are fixed.\n        ignore(testName) { testFn(testConfig) }\n      } else {\n        test(testName) { testFn(testConfig) }\n      }\n    }\n  }\n\n  /**\n   * Check invariants for row metrics of MERGE INTO command.\n   *\n   * @param metrics The merge operation metrics from the Delta history.\n   */\n  private def checkMergeOperationRowMetricsInvariants(metrics: Map[String, String]): Unit = {\n    assert(\n      metrics(\"numTargetRowsUpdated\").toLong ===\n        metrics(\"numTargetRowsMatchedUpdated\").toLong +\n        metrics(\"numTargetRowsNotMatchedBySourceUpdated\").toLong)\n    assert(\n      metrics(\"numTargetRowsDeleted\").toLong ===\n        metrics(\"numTargetRowsMatchedDeleted\").toLong +\n        metrics(\"numTargetRowsNotMatchedBySourceDeleted\").toLong)\n  }\n\n  /**\n   * Check invariants for file metrics of MERGE INTO command.\n   *\n   * @param metrics The merge operation metrics from the Delta history.\n   */\n  private def checkMergeOperationFileMetricsInvariants(metrics: Map[String, String]): Unit = {\n    // numTargetFilesAdded should have a positive value if rows were added and be zero\n    // otherwise.\n    {\n      val numFilesAdded = metrics(\"numTargetFilesAdded\").toLong\n      val numBytesAdded = metrics(\"numTargetBytesAdded\").toLong\n      val numRowsWritten =\n        metrics(\"numTargetRowsInserted\").toLong +\n          metrics(\"numTargetRowsUpdated\").toLong +\n          metrics(\"numTargetRowsCopied\").toLong\n      lazy val assertMsgNumFiles = {\n        val expectedNumFilesAdded =\n          if (numRowsWritten == 0) \"0\" else s\"between 1 and $numRowsWritten\"\n        s\"\"\"Unexpected value for numTargetFilesAdded metric.\n           | Expected: $expectedNumFilesAdded\n           | Actual: $numFilesAdded\n           | numRowsWritten: $numRowsWritten\n           | Metrics: ${metrics.toString}\n           |\"\"\".stripMargin\n\n      }\n      lazy val assertMsgBytes = {\n        val expected = if (numRowsWritten == 0) \"0\" else \"greater than 0\"\n        s\"\"\"Unexpected value for numTargetBytesAdded metric.\n           | Expected: $expected\n           | Actual: $numBytesAdded\n           | numRowsWritten: $numRowsWritten\n           | numFilesAdded: $numFilesAdded\n           | Metrics: ${metrics.toString}\n           |\"\"\".stripMargin\n      }\n      if (numRowsWritten == 0) {\n        assert(numFilesAdded === 0, assertMsgNumFiles)\n        assert(numBytesAdded === 0, assertMsgBytes)\n      } else {\n        assert(numFilesAdded > 0 && numFilesAdded <= numRowsWritten, assertMsgNumFiles)\n        assert(numBytesAdded > 0, assertMsgBytes)\n      }\n    }\n\n    // numTargetFilesRemoved should have a positive value if rows were updated or deleted and be\n    // zero otherwise.  In case of classic merge we also count copied rows as changed, because if\n    // match clauses have conditions we may end up copying rows even if no other rows are\n    // updated/deleted.\n    {\n      val numFilesRemoved = metrics(\"numTargetFilesRemoved\").toLong\n      val numBytesRemoved = metrics(\"numTargetBytesRemoved\").toLong\n      val numRowsTouched =\n        metrics(\"numTargetRowsDeleted\").toLong +\n          metrics(\"numTargetRowsUpdated\").toLong +\n          metrics(\"numTargetRowsCopied\").toLong\n      lazy val assertMsgNumFiles = {\n        val expectedNumFilesRemoved =\n          if (numRowsTouched == 0) \"0\" else s\"between 1 and $numRowsTouched\"\n        s\"\"\"Unexpected value for numTargetFilesRemoved metric.\n           | Expected: $expectedNumFilesRemoved\n           | Actual: $numFilesRemoved\n           | numRowsTouched: $numRowsTouched\n           | Metrics: ${metrics.toString}\n           |\"\"\".stripMargin\n      }\n      lazy val assertMsgBytes = {\n        val expectedNumBytesRemoved =\n          if (numRowsTouched == 0) \"0\" else \"greater than 0\"\n        s\"\"\"Unexpected value for numTargetBytesRemoved metric.\n           | Expected: $expectedNumBytesRemoved\n           | Actual: $numBytesRemoved\n           | numRowsTouched: $numRowsTouched\n           | Metrics: ${metrics.toString}\n           |\"\"\".stripMargin\n      }\n\n      if (numRowsTouched == 0) {\n        assert(numFilesRemoved === 0, assertMsgNumFiles)\n        assert(numBytesRemoved === 0, assertMsgBytes)\n      } else {\n        assert(numFilesRemoved > 0 && numFilesRemoved <= numRowsTouched, assertMsgNumFiles)\n        assert(numBytesRemoved > 0, assertMsgBytes)\n      }\n    }\n  }\n\n  /**\n   * Helper method to create a target table with the desired options, run a merge command and check\n   * the operation metrics in the Delta history.\n   *\n   * For operation metrics the following checks are performed:\n   * a) The operation metrics in Delta history must match [[DeltaOperationMetrics.MERGE]] schema,\n   *    i.e. no metrics can be missing or unknown metrics can exist.\n   * b) All operation metrics must have a non-negative values.\n   * c) The values of metrics that are specified in 'expectedOpMetrics' argument must match the\n   * operation metrics. Metrics with a value of -1 are ignored, to allow callers always specify\n   * metrics that don't exist under some configurations.\n   * d) Row-related operation metrics that are not specified in 'expectedOpMetrics' must be zero.\n   * e) File/Time-related operation metrics that are not specified in 'expectedOpMetrics' can have\n   * non-zero values. These metrics are not deterministic and so this method only checks that\n   * some invariants hold.\n   *\n   * @param targetDf The DataFrame to generate the target table for the merge command.\n   * @param sourceDf The DataFrame to generate the source table for the merge command.\n   * @param mergeCmdFn The function that actually runs the merge command.\n   * @param expectedOpMetrics A map with values for expected operation metrics.\n   * @param testConfig The configuration options for this test\n   * @param overrideExpectedOpMetrics Sequence of expected operation metric values to override from\n   *                                  those provided in expectedOpMetrics for specific\n   *                                  configurations of partitioned and cdfEnabled. Elements\n   *                                  provided as:\n   *                                  ((partitioned, cdfEnabled), (metric_name, metric_value))\n   */\n  private def runMergeCmdAndTestMetrics(\n      targetDf: DataFrame,\n      sourceDf: DataFrame,\n      mergeCmdFn: MergeCmd,\n      expectedOpMetrics: Map[String, Int],\n      testConfig: MergeTestConfiguration,\n      overrideExpectedOpMetrics: Seq[((Boolean, Boolean), (String, Int))] = Seq.empty\n  ): Unit = {\n    withSQLConf(\n      DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\",\n      DeltaSQLConf.DELTA_SKIP_RECORDING_EMPTY_COMMITS.key -> \"false\",\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> testConfig.cdfEnabled.toString\n    ) {\n      withTempDir { tempDir =>\n        def addExtraColumns(tableDf: DataFrame): DataFrame = {\n          // Add a column to be used for data partitioning and one extra column for filters in\n          // queries.\n          val numRows = tableDf.count()\n          val numPartitions = tableDf.rdd.getNumPartitions\n          val numRowsPerPart = if (numRows > 0) numRows / numPartitions else 1\n          tableDf.withColumn(\"partCol\", expr(s\"floor(id / $numRowsPerPart)\"))\n            .withColumn(\"extraCol\", expr(s\"$numRows - id\"))\n        }\n\n        // Add extra columns and create target table.\n        val tempPath = tempDir.getAbsolutePath\n        val partitionBy = if (testConfig.partitioned) Seq(\"partCol\") else Seq()\n        val targetDfWithExtraCols = addExtraColumns(targetDf)\n        targetDfWithExtraCols\n          .write\n          .partitionBy(partitionBy: _*)\n          .format(\"delta\")\n          .save(tempPath)\n        val targetTable = io.delta.tables.DeltaTable.forPath(tempPath)\n\n        // Also add extra columns in source to be able to call updateAll()/insertAll().\n        val sourceDfWithExtraCols = addExtraColumns(sourceDf)\n\n        // Run MERGE INTO command\n        val mergeResultDf = mergeCmdFn(targetTable, sourceDfWithExtraCols)\n\n        checkMergeResultMetrics(mergeResultDf, expectedOpMetrics)\n\n        // Query the operation metrics from the Delta log history.\n        val operationMetrics: Map[String, String] = getOperationMetrics(targetTable.history(1))\n\n        // Get the default row operation metrics and override them with the provided ones.\n        val metricsWithDefaultZeroValue = mergeRowMetrics.map(_ -> \"0\").toMap\n        var expectedOpMetricsWithDefaults = metricsWithDefaultZeroValue ++\n          expectedOpMetrics.filter(m => m._2 >= 0).mapValues(_.toString)\n\n        overrideExpectedOpMetrics.foreach { case ((partitioned, cdfEnabled), (metric, value)) =>\n          if (partitioned == testConfig.partitioned && cdfEnabled == testConfig.cdfEnabled) {\n            expectedOpMetricsWithDefaults = expectedOpMetricsWithDefaults +\n              (metric -> value.toString)\n          }\n        }\n\n        // Check that all operation metrics are positive numbers.\n        for ((metricName, metricValue) <- operationMetrics) {\n          assert(metricValue.toLong >= 0,\n            s\"Invalid negative value for metric $metricName = $metricValue\")\n        }\n\n        // Check that operation metrics match the schema and that values match the expected ones.\n        checkOperationMetrics(\n          expectedOpMetricsWithDefaults,\n          operationMetrics,\n          DeltaOperationMetrics.MERGE\n        )\n        // Check row metrics invariants.\n        checkMergeOperationRowMetricsInvariants(operationMetrics)\n        // Check file metrics invariants.\n        checkMergeOperationFileMetricsInvariants(operationMetrics)\n        // Check time metrics invariants.\n        checkOperationTimeMetricsInvariant(mergeTimeMetrics, operationMetrics)\n        // Check CDF metrics invariants.\n        checkMergeOperationCdfMetricsInvariants(operationMetrics, testConfig.cdfEnabled)\n      }\n    }\n  }\n\n  private def checkMergeResultMetrics(\n      mergeResultDf: DataFrame,\n      metrics: Map[String, Int]): Unit = {\n    val numRowsUpdated = metrics.get(\"numTargetRowsUpdated\").map(_.toLong).getOrElse(0L)\n    val numRowsDeleted = metrics.get(\"numTargetRowsDeleted\").map(_.toLong).getOrElse(0L)\n    val numRowsInserted = metrics.get(\"numTargetRowsInserted\").map(_.toLong).getOrElse(0L)\n    val numRowsTouched =\n      numRowsUpdated +\n        numRowsDeleted +\n        numRowsInserted\n\n    assert(mergeResultDf.collect() ===\n      Array(Row(numRowsTouched,\n        numRowsUpdated,\n        numRowsDeleted,\n        numRowsInserted)))\n  }\n\n  /////////////////////////////\n  // insert-only merge tests //\n  /////////////////////////////\n\n  testMergeMetrics(\"insert-only\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable.as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 50,\n      \"numTargetRowsInserted\" -> 50\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with skipping\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 100, end = 200, step = 1, numPartitions = 5).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable.as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id and t.partCol >= 2\")\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 100,\n      \"numTargetRowsInserted\" -> 100\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatched(\"s.id >= 125\")\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 25,\n      \"numTargetRowsInserted\" -> 25\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only when all rows match\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 200, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with unsatisfied condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatched(\"s.id > 150\")\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with empty source\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 200, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(0).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 0\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with empty target\") { testConfig => {\n    val targetDf = spark.range(0).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 100,\n      \"numTargetRowsInserted\" -> 100\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with disjoint tables\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 100, end = 200, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .updateAll()\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 100,\n      \"numTargetRowsInserted\" -> 100\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"insert-only with update/delete with unsatisfied conditions\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 50, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id + t.id > 200\")\n        .updateAll()\n        .whenMatched(\"s.id + t.id < 0\")\n        .delete()\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    // In classic merge we are copying all rows from job1.\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 150,\n      \"numOutputRows\" -> 150,\n      \"numTargetRowsInserted\" -> 100,\n      \"numTargetRowsCopied\" -> 50,\n      \"numTargetFilesRemoved\" -> 5\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  /////////////////////////////\n  // delete-only merge tests //\n  /////////////////////////////\n\n  testMergeMetrics(\"delete-only\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 100,\n      \"numTargetRowsDeleted\" -> 50,\n      \"numTargetRowsMatchedDeleted\" -> 50,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numOutputRows\" -> 10,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> -1\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with skipping\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id and t.partCol >= 2\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 10,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetRowsDeleted\" -> 50,\n      \"numTargetRowsMatchedDeleted\" -> 50,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with disjoint tables\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 100, end = 200, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numTargetFilesAdded\" -> 0,\n      \"numTargetFilesRemoved\" -> 0\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only delete all rows\") { testConfig => {\n    val targetDf = spark.range(start = 100, end = 200, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 300, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 300,\n      \"numOutputRows\" -> 0,\n      \"numTargetRowsCopied\" -> 0,\n      \"numTargetRowsDeleted\" -> 100,\n      \"numTargetRowsMatchedDeleted\" -> 100,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 0,\n      \"numTargetFilesRemoved\" -> 5\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id + t.id < 50\")\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 150,\n      \"numOutputRows\" -> 15,\n      \"numTargetRowsCopied\" -> 15,\n      \"numTargetRowsDeleted\" -> 25,\n      \"numTargetRowsMatchedDeleted\" -> 25,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 2\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with update with unsatisfied condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id + t.id > 1000\")\n        .updateAll()\n        .whenMatched(\"s.id + t.id < 50\")\n        .delete()\n        .execute()\n    }\n    // In case of partitioned tables, files are mixed-in even though finally there are no matches.\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 15,\n      \"numTargetRowsCopied\" -> 15,\n      \"numTargetRowsDeleted\" -> 25,\n      \"numTargetRowsMatchedDeleted\" -> 25,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 2\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with condition on delete and insert with no matching rows\") {\n    testConfig => {\n      val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n      val sourceDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 10).toDF()\n      val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n        targetTable\n          .as(\"t\")\n          .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n          .whenMatched(\"s.id + t.id < 50\")\n          .delete()\n          .whenNotMatched()\n          .insertAll()\n          .execute()\n      }\n      // In classic merge we are copying all rows from job1.\n      // In case of partitioned tables, files are mixed-in even though finally there are no matches.\n      val expectedOpMetrics = Map[String, Int](\n        \"numSourceRows\" -> 100,\n        \"numOutputRows\" -> 75,\n        \"numTargetRowsCopied\" -> 75,\n        \"numTargetRowsDeleted\" -> 25,\n        \"numTargetRowsMatchedDeleted\" -> 25,\n        \"numTargetRowsRemoved\" -> -1,\n        \"numTargetFilesRemoved\" -> 5\n      )\n      runMergeCmdAndTestMetrics(\n        targetDf = targetDf,\n        sourceDf = sourceDf,\n        mergeCmdFn = mergeCmdFn,\n        expectedOpMetrics = expectedOpMetrics,\n        testConfig = testConfig\n      )\n    }\n  }\n\n  testMergeMetrics(\"delete-only with unsatisfied condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 150, step = 1, numPartitions = 15).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id + t.id > 1000\")\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 150,\n      \"numTargetFilesAdded\" -> 0,\n      \"numTargetFilesRemoved\" -> 0\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with target-only condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 150, step = 1, numPartitions = 15).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"t.id >= 45\")\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 150,\n      \"numOutputRows\" -> 5,\n      \"numTargetRowsCopied\" -> 5,\n      \"numTargetRowsDeleted\" -> 55,\n      \"numTargetRowsMatchedDeleted\" -> 55,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with source-only condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 100).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id >= 70\")\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 10,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetRowsDeleted\" -> 30,\n      \"numTargetRowsMatchedDeleted\" -> 30,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 2\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with empty source\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 4).toDF()\n    val sourceDf = spark.range(0).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"t.id > 25\")\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 0,\n      \"numTargetFilesAdded\" -> 0,\n      \"numTargetFilesRemoved\" -> 0\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with empty target\") { testConfig => {\n    val targetDf = spark.range(0).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 3).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      // This actually goes through a special code path in MERGE because the optimizer optimizes\n      // away the join to the source table entirely if the target table is empty.\n      \"numSourceRows\" -> 100,\n      \"numTargetFilesAdded\" -> 0,\n      \"numTargetFilesRemoved\" -> 0\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only without join empty source\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(0).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"t.id >= 50\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 0,\n      \"numTargetFilesAdded\" -> 0,\n      \"numTargetFilesRemoved\" -> 0\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only without join with source with 1 row\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 1, step = 1, numPartitions = 1).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"t.id >= 50\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 1,\n      \"numOutputRows\" -> 10,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetRowsDeleted\" -> 50,\n      \"numTargetRowsMatchedDeleted\" -> 50,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only without join\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 0, end = 200, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"t.id >= 50\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 200,\n      \"numOutputRows\" -> 10,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetRowsDeleted\" -> 50,\n      \"numTargetRowsMatchedDeleted\" -> 50,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetFilesAdded\" -> 1,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"delete-only with duplicates\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    // This will cause duplicates due to rounding.\n    val sourceDf = spark\n      .range(start = 50, end = 150, step = 1, numPartitions = 2)\n      .toDF()\n      .select(floor($\"id\" / 2).as(\"id\"))\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 10,\n      \"numTargetRowsDeleted\" -> 50,\n      \"numTargetRowsMatchedDeleted\" -> 50,\n      \"numTargetRowsRemoved\" -> -1,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetFilesAdded\" -> 2,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig,\n      // When cdf=true in this test we hit the corner case where there are duplicate matches with a\n      // delete clause and we generate duplicate cdc data. This is further detailed in\n      // MergeIntoCommand at the definition of isDeleteWithDuplicateMatchesAndCdc. Our fix for this\n      // scenario includes deduplicating the output data which reshuffles the output data.\n      // Thus when the table is not partitioned, the data is rewritten into 1 new file rather than 2\n      overrideExpectedOpMetrics = Seq(\n        ((false, true), (\"numTargetFilesAdded\", 1)),\n        ((false, false), (\n          \"numTargetFilesAdded\",\n          1)\n        )\n      )\n    )\n  }}\n\n  /////////////////////////////\n  // match-only merge tests  //\n  /////////////////////////////\n  testMergeMetrics(\"match-only\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .updateAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 60,\n      \"numTargetRowsUpdated\" -> 50,\n      \"numTargetRowsMatchedUpdated\" -> 50,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"match-only with skipping\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id and t.partCol >= 2\")\n        .whenMatched()\n        .updateAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 60,\n      \"numTargetRowsUpdated\" -> 50,\n      \"numTargetRowsMatchedUpdated\" -> 50,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"match-only with update/delete with unsatisfied conditions\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id + t.id > 1000\")\n        .delete()\n        .whenMatched(\"s.id + t.id < 1000\")\n        .updateAll()\n        .whenNotMatched(\"s.id > 1000\")\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 60,\n      \"numTargetRowsUpdated\" -> 50,\n      \"numTargetRowsMatchedUpdated\" -> 50,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"match-only with unsatisfied condition\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"s.id + t.id > 1000\")\n        .updateAll()\n        .execute()\n    }\n\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  /////////////////////////////////////////////\n  // not matched by source only merge tests  //\n  /////////////////////////////////////////////\n  testMergeMetrics(\"not matched by source update only\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenNotMatchedBySource(\"t.id < 20\")\n        .updateExpr(Map(\"t.extraCol\" -> \"t.extraCol + 1\"))\n        .execute()\n    }\n    val expectedOpMetrics = Map[String, Int](\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 100,\n      \"numTargetRowsUpdated\" -> 20,\n      \"numTargetRowsNotMatchedBySourceUpdated\" -> 20,\n      \"numTargetRowsCopied\" -> 80,\n      \"numTargetFilesRemoved\" -> 5\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n   }}\n\n  /////////////////////////////\n  //    full merge tests     //\n  /////////////////////////////\n  testMergeMetrics(\"upsert\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .updateAll()\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 110,\n      \"numTargetRowsInserted\" -> 50,\n      \"numTargetRowsUpdated\" -> 50,\n      \"numTargetRowsMatchedUpdated\" -> 50,\n      \"numTargetRowsCopied\" -> 10,\n      \"numTargetFilesRemoved\" -> 3\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"replace target with source\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched()\n        .updateAll()\n        .whenNotMatched()\n        .insertAll()\n        .whenNotMatchedBySource()\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 100,\n      \"numTargetRowsInserted\" -> 50,\n      \"numTargetRowsUpdated\" -> 50,\n      \"numTargetRowsMatchedUpdated\" -> 50,\n      \"numTargetRowsDeleted\" -> 50,\n      \"numTargetRowsNotMatchedBySourceDeleted\" -> 50,\n      \"numTargetRowsCopied\" -> 0,\n      \"numTargetFilesRemoved\" -> 5\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\"upsert and delete with conditions\") { testConfig => {\n    val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 10).toDF()\n    val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 3).toDF()\n    val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n        .as(\"t\")\n        .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n        .whenMatched(\"t.id >= 55 and t.id < 60\")\n        .updateAll()\n        .whenMatched(\"t.id < 70\")\n        .delete()\n        .whenNotMatched()\n        .insertAll()\n        .whenNotMatchedBySource(\"t.id < 10\")\n        .updateExpr(Map(\"t.extraCol\" -> \"t.extraCol + 1\"))\n        .whenNotMatchedBySource(\"t.id >= 45\")\n        .delete()\n        .execute()\n    }\n    val expectedOpMetrics = Map(\n      \"numSourceRows\" -> 100,\n      \"numOutputRows\" -> 130,\n      \"numTargetRowsInserted\" -> 50,\n      \"numTargetRowsUpdated\" -> 15,\n      \"numTargetRowsMatchedUpdated\" -> 5,\n      \"numTargetRowsNotMatchedBySourceUpdated\" -> 10,\n      \"numTargetRowsDeleted\" -> 20,\n      \"numTargetRowsMatchedDeleted\" -> 15,\n      \"numTargetRowsNotMatchedBySourceDeleted\" -> 5,\n      \"numTargetRowsCopied\" -> 65,\n      \"numTargetFilesRemoved\" -> 10\n    )\n    runMergeCmdAndTestMetrics(\n      targetDf = targetDf,\n      sourceDf = sourceDf,\n      mergeCmdFn = mergeCmdFn,\n      expectedOpMetrics = expectedOpMetrics,\n      testConfig = testConfig\n    )\n  }}\n\n  testMergeMetrics(\n    \"update/delete/insert with some unsatisfied conditions\") {\n    testConfig => {\n      val targetDf = spark.range(start = 0, end = 100, step = 1, numPartitions = 5).toDF()\n      val sourceDf = spark.range(start = 50, end = 150, step = 1, numPartitions = 10).toDF()\n      val mergeCmdFn: MergeCmd = (targetTable, sourceDf) => {\n      targetTable\n          .as(\"t\")\n          .merge(sourceDf.as(\"s\"), \"s.id = t.id\")\n          .whenMatched(\"s.id + t.id > 1000\")\n          .delete()\n          .whenNotMatchedBySource(\"t.id > 1000\")\n          .delete()\n          .whenNotMatchedBySource(\"t.id < 1000\")\n          .updateExpr(Map(\"t.extraCol\" -> \"t.extraCol + 1\"))\n          .whenNotMatched(\"s.id > 1000\")\n          .insertAll()\n          .execute()\n      }\n      val expectedOpMetrics = Map[String, Int](\n        \"numSourceRows\" -> 100,\n        \"numOutputRows\" -> 100,\n        \"numTargetRowsUpdated\" -> 50,\n        \"numTargetRowsNotMatchedBySourceUpdated\" -> 50,\n        \"numTargetRowsCopied\" -> 50,\n        \"numTargetFilesRemoved\" -> 5\n      )\n      runMergeCmdAndTestMetrics(\n        targetDf = targetDf,\n        sourceDf = sourceDf,\n        mergeCmdFn = mergeCmdFn,\n        expectedOpMetrics = expectedOpMetrics,\n        testConfig = testConfig\n      )\n   }}\n}\n\nobject MergeIntoMetricsBase extends QueryTest with SharedSparkSession {\n\n  ///////////////////////\n  // helpful constants //\n  ///////////////////////\n\n  // Metrics related with affected number of rows. Values should always be deterministic.\n  val mergeRowMetrics = Set(\n    \"numSourceRows\",\n    \"numTargetRowsInserted\",\n    \"numTargetRowsUpdated\",\n    \"numTargetRowsMatchedUpdated\",\n    \"numTargetRowsNotMatchedBySourceUpdated\",\n    \"numTargetRowsDeleted\",\n    \"numTargetRowsMatchedDeleted\",\n    \"numTargetRowsNotMatchedBySourceDeleted\",\n    \"numTargetRowsCopied\",\n    \"numOutputRows\"\n  )\n  // Metrics related with affected number of files. Values depend on the file layout.\n  val mergeFileMetrics = Set(\n    \"numTargetFilesAdded\", \"numTargetFilesRemoved\", \"numTargetBytesAdded\", \"numTargetBytesRemoved\")\n  // Metrics related with execution times.\n  val mergeTimeMetrics = Set(\n    \"executionTimeMs\", \"materializeSourceTimeMs\", \"scanTimeMs\", \"rewriteTimeMs\")\n  // Metrics related with CDF. Available only when CDF is available.\n  val mergeCdfMetrics = Set(\"numTargetChangeFilesAdded\")\n  // DV Metrics.\n  val mergeDVMetrics = Set(\n    \"numTargetDeletionVectorsAdded\",\n    \"numTargetDeletionVectorsUpdated\",\n    \"numTargetDeletionVectorsRemoved\")\n\n  // Ensure that all metrics are properly copied here.\n  assert(\n    DeltaOperationMetrics.MERGE.size ==\n      mergeRowMetrics.size +\n      mergeFileMetrics.size +\n      mergeTimeMetrics.size +\n      mergeCdfMetrics.size +\n      mergeDVMetrics.size\n  )\n\n  ///////////////////\n  // helpful types //\n  ///////////////////\n\n  type MergeCmd = (io.delta.tables.DeltaTable, DataFrame) => DataFrame\n\n  /////////////////////\n  // helpful methods //\n  /////////////////////\n\n  /**\n   * Check invariants for the CDF metrics of MERGE INTO command. Checking the actual values\n   * is avoided since they depend on the file layout and the type of merge.\n   *\n   * @param metrics The merge operation metrics from the Delta history.\n   * @param cdfEnabled Whether CDF was enabled or not.\n   */\n  def checkMergeOperationCdfMetricsInvariants(\n      metrics: Map[String, String],\n      cdfEnabled: Boolean): Unit = {\n    val numRowsUpdated = metrics(\"numTargetRowsUpdated\").toLong\n    val numRowsDeleted = metrics(\"numTargetRowsDeleted\").toLong\n    val numRowsInserted = metrics(\"numTargetRowsInserted\").toLong\n    val numRowsChanged = numRowsUpdated + numRowsDeleted + numRowsInserted\n    val numTargetChangeFilesAdded = metrics(\"numTargetChangeFilesAdded\").toLong\n\n    lazy val assertMsg =\n      s\"\"\"Unexpected value for numTargetChangeFilesAdded metric:\n         | Expected : ${if (numRowsChanged == 0) 0 else \"Positive integer value\"}\n         | Actual : $numTargetChangeFilesAdded\n         | cdfEnabled: $cdfEnabled\n         | numRowsChanged: $numRowsChanged\n         | Metrics: ${metrics.toString}\n         |\"\"\".stripMargin\n\n    if (!cdfEnabled || numRowsChanged == 0) {\n      assert(numTargetChangeFilesAdded === 0, assertMsg)\n    } else {\n      // In case of insert-only merges where only new files are added, CDF data are not required\n      // since the CDF reader can read the corresponding added files. However, there are cases\n      // where we produce CDF data even in insert-only merges (see 'insert-only-dynamic-predicate'\n      // testcase for an example). Here we skip the assertion, since both behaviours can be\n      // considered valid.\n      val isInsertOnly = numRowsInserted > 0 && numRowsChanged == numRowsInserted\n      if (!isInsertOnly) {\n        assert(numTargetChangeFilesAdded > 0, assertMsg)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoNotMatchedBySourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.Row\n\ntrait MergeIntoNotMatchedBySourceWithCDCMixin extends MergeIntoSuiteBaseMixin {\n  import testImplicits._\n\n  /**\n   * Variant of `testExtendedMerge` that runs a MERGE INTO command, checks the expected result and\n   * additionally validate that the CDC produced is correct.\n   */\n  protected def testExtendedMergeWithCDC(\n      name: String,\n      namePrefix: String = \"not matched by source\")(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      mergeOn: String,\n      mergeClauses: MergeClause*)(\n      result: Seq[(Int, Int)],\n      cdc: Seq[(Int, Int, String)]): Unit = {\n\n    for {\n      isPartitioned <- BOOLEAN_DOMAIN\n      cdcEnabled <- BOOLEAN_DOMAIN\n    } {\n      test(s\"$namePrefix - $name - isPartitioned: $isPartitioned - cdcEnabled: $cdcEnabled\") {\n        withSQLConf(\n          DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> cdcEnabled.toString) {\n          withKeyValueData(source, target, isPartitioned) { case (sourceName, targetName) =>\n            withSQLConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"true\") {\n              executeMerge(s\"$targetName t\", s\"$sourceName s\", mergeOn, mergeClauses: _*)\n            }\n            checkAnswer(readDeltaTableByIdentifier(targetName),\n              result.map { case (k, v) => Row(k, v) })\n          }\n          if (cdcEnabled) {\n            checkAnswer(getCDCForLatestOperation(deltaLog, DeltaOperations.OP_MERGE), cdc.toDF())\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait MergeIntoNotMatchedBySourceCDCPart1Tests extends MergeIntoNotMatchedBySourceWithCDCMixin {\n  // Test correctness with NOT MATCHED BY SOURCE clauses.\n  testExtendedMergeWithCDC(\"all 3 types of match clauses without conditions\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (2, 20) :: (1, 10) :: (5, 50) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(set = \"*\"),\n    insert(values = \"*\"),\n    deleteNotMatched())(\n    result = Seq(\n      (0, 0), // No matched by target, inserted\n      (1, 1), // Matched, updated\n      // (2, 20) Not matched by source, deleted\n      (5, 5) // Matched, updated\n    ),\n    cdc = Seq(\n      (0, 0, \"insert\"),\n      (1, 10, \"update_preimage\"),\n      (1, 1, \"update_postimage\"),\n      (2, 20, \"delete\"),\n      (5, 50, \"update_preimage\"),\n      (5, 5, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"all 3 types of match clauses with conditions\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: (6, 6) :: Nil,\n    target = (1, 10) :: (2, 20) :: (5, 50) :: (7, 70) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(set = \"*\", condition = \"t.value < 30\"),\n    insert(values = \"*\", condition = \"s.value < 4\"),\n    deleteNotMatched(condition = \"t.value > 40\"))(\n    result = Seq(\n      (0, 0), // Not matched by target, inserted\n      (1, 1), // Matched, updated\n      (2, 20), // Not matched by source, no change\n      (5, 50) // Matched, not updated\n      // (6, 6) Not matched by target, no change\n      // (7, 7) Not matched by source, deleted\n    ),\n    cdc = Seq(\n      (0, 0, \"insert\"),\n      (1, 10, \"update_preimage\"),\n      (1, 1, \"update_postimage\"),\n      (7, 70, \"delete\")))\n\n  testExtendedMergeWithCDC(\"unconditional delete only when not matched by source\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (2, 20) :: (1, 10) :: (5, 50) :: (6, 60) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched())(\n    result = Seq(\n      (1, 10), // Matched, no change\n      // (2, 20) Not matched by source, deleted\n      (5, 50) // Matched, no change\n      // (6, 60) Not matched by source, deleted\n    ),\n    cdc = Seq((2, 20, \"delete\"), (6, 60, \"delete\")))\n\n  testExtendedMergeWithCDC(\"conditional delete only when not matched by source\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (5, 50) :: (6, 60) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(condition = \"t.value > 40\"))(\n    result = Seq(\n      (1, 10), // Matched, no change\n      (2, 20), // Not matched by source, no change\n      (5, 50) // Matched, no change\n      // (6, 60) Not matched by source, deleted\n    ),\n    cdc = Seq((6, 60, \"delete\")))\n\n  testExtendedMergeWithCDC(\"delete only matched and not matched by source\")(\n    source = (1, 1) :: (2, 2) :: (5, 5) :: (6, 6) :: Nil,\n    target = (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(\"s.value % 2 = 0\"),\n    deleteNotMatched(\"t.value % 20 = 0\"))(\n    result = Seq(\n      (1, 10), // Matched, no change\n      // (2, 20) Matched, deleted\n      (3, 30) // Not matched by source, no change\n      // (4, 40) Not matched by source, deleted\n    ),\n    cdc = Seq((2, 20, \"delete\"), (4, 40, \"delete\")))\n\n  testExtendedMergeWithCDC(\"unconditionally delete matched and not matched by source\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: (6, 6) :: Nil,\n    target = (1, 10) :: (2, 20) :: (5, 50) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(),\n    deleteNotMatched())(\n    result = Seq.empty,\n    cdc = Seq((1, 10, \"delete\"), (2, 20, \"delete\"), (5, 50, \"delete\")))\n\n  testExtendedMergeWithCDC(\"unconditional not matched by source update\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (4, 40) :: (5, 50) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (1, 10), // Matched, no change\n      (2, 21), // Not matched by source, updated\n      (4, 41), // Not matched by source, updated\n      (5, 50) // Matched, no change\n    ),\n    cdc = Seq(\n      (2, 20, \"update_preimage\"),\n      (2, 21, \"update_postimage\"),\n      (4, 40, \"update_preimage\"),\n      (4, 41, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"conditional not matched by source update\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (4, 40) :: (5, 50) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"t.value = 20\", set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (1, 10), // Matched, no change\n      (2, 21), // Not matched by source, updated\n      (4, 40), // Not matched by source, no change\n      (5, 50) // Matched, no change\n    ),\n    cdc = Seq((2, 20, \"update_preimage\"), (2, 21, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"not matched by source update and delete with skipping\")(\n    source = (0, 0) :: (1, 1) :: (2, 2) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (4, 40) :: (5, 50) :: Nil,\n    mergeOn = \"s.key = t.key and t.key > 4\",\n    updateNotMatched(condition = \"t.key = 1\", set = \"t.value = t.value + 1\"),\n    deleteNotMatched(condition = \"t.key = 4\"))(\n    result = Seq(\n      (1, 11), // Not matched by source based on merge condition, updated\n      (2, 20), // Not matched by source based on merge condition, no change\n      // (4, 40), Not matched by source, deleted\n      (5, 50) // Matched, no change\n    ),\n    cdc = Seq(\n      (1, 10, \"update_preimage\"),\n      (1, 11, \"update_postimage\"),\n      (4, 40, \"delete\")))\n\n  testExtendedMergeWithCDC(\n    \"matched delete and not matched by source update with skipping\")(\n    source = (0, 0) :: (1, 1) :: (2, 2) :: (5, 5) :: (6, 6) :: Nil,\n    target = (1, 10) :: (2, 20) :: (4, 40) :: (5, 50) :: (6, 60) :: Nil,\n    mergeOn = \"s.key = t.key and t.key > 4\",\n    delete(condition = \"t.key = 5\"),\n    updateNotMatched(condition = \"t.key = 1\", set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (1, 11), // Not matched by source based on merge condition, updated\n      (2, 20), // Not matched by source based on merge condition, no change\n      (4, 40), // Not matched by source, no change\n      // (5, 50), Matched, deleted\n      (6, 60) // Matched, no change\n    ),\n    cdc = Seq(\n      (1, 10, \"update_preimage\"),\n      (1, 11, \"update_postimage\"),\n      (5, 50, \"delete\")))\n}\n\ntrait MergeIntoNotMatchedBySourceCDCPart2Tests extends MergeIntoNotMatchedBySourceWithCDCMixin {\n  testExtendedMergeWithCDC(\"not matched by source update + delete clauses\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (7, 70) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(\"t.value % 20 = 0\"),\n    updateNotMatched(set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (1, 10), // Matched, no change\n      // (2, 20) Not matched by source, deleted\n      (7, 71) // Not matched by source, updated\n    ),\n    cdc = Seq((2, 20, \"delete\"), (7, 70, \"update_preimage\"), (7, 71, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"unconditional not matched by source update + not matched insert\")(\n    source = (0, 0) :: (1, 1) :: (4, 4) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (4, 40) :: (7, 70) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(\"*\"),\n    updateNotMatched(set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (0, 0), // Not matched by target, inserted\n      (1, 10), // Matched, no change\n      (2, 21), // Not matched by source, updated\n      (4, 40), // Matched, no change\n      (5, 5), // Not matched by target, inserted\n      (7, 71) // Not matched by source, updated\n    ),\n    cdc = Seq(\n      (0, 0, \"insert\"),\n      (2, 20, \"update_preimage\"),\n      (2, 21, \"update_postimage\"),\n      (5, 5, \"insert\"),\n      (7, 70, \"update_preimage\"),\n      (7, 71, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"not matched by source delete + not matched insert\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (1, 10) :: (2, 20) :: (7, 70) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(\"*\"),\n    deleteNotMatched(\"t.value % 20 = 0\"))(\n    result = Seq(\n      (0, 0), // Not matched by target, inserted\n      (1, 10), // Matched, no change\n      // (2, 20), Not matched by source, deleted\n      (5, 5), // Not matched by target, inserted\n      (7, 70) // Not matched by source, no change\n    ),\n    cdc = Seq((0, 0, \"insert\"), (2, 20, \"delete\"), (5, 5, \"insert\")))\n\n  testExtendedMergeWithCDC(\"multiple not matched by source clauses\")(\n    source = (0, 0) :: (1, 1) :: (5, 5) :: Nil,\n    target = (6, 6) :: (7, 7) :: (8, 8) :: (9, 9) :: (10, 10) :: (11, 11) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"t.key % 6 = 0\", set = \"t.value = t.value + 5\"),\n    updateNotMatched(condition = \"t.key % 6 = 1\", set = \"t.value = t.value + 4\"),\n    updateNotMatched(condition = \"t.key % 6 = 2\", set = \"t.value = t.value + 3\"),\n    updateNotMatched(condition = \"t.key % 6 = 3\", set = \"t.value = t.value + 2\"),\n    updateNotMatched(condition = \"t.key % 6 = 4\", set = \"t.value = t.value + 1\"),\n    deleteNotMatched())(\n    result = Seq(\n      (6, 11), // Not matched by source, updated\n      (7, 11), // Not matched by source, updated\n      (8, 11), // Not matched by source, updated\n      (9, 11), // Not matched by source, updated\n      (10, 11) // Not matched by source, updated\n      // (11, 11) Not matched by source, deleted\n    ),\n    cdc = Seq(\n      (6, 6, \"update_preimage\"),\n      (6, 11, \"update_postimage\"),\n      (7, 7, \"update_preimage\"),\n      (7, 11, \"update_postimage\"),\n      (8, 8, \"update_preimage\"),\n      (8, 11, \"update_postimage\"),\n      (9, 9, \"update_preimage\"),\n      (9, 11, \"update_postimage\"),\n      (10, 10, \"update_preimage\"),\n      (10, 11, \"update_postimage\"),\n      (11, 11, \"delete\")))\n\n  testExtendedMergeWithCDC(\"not matched by source update + conditional insert\")(\n    source = (1, 1) :: (0, 2) :: (5, 5) :: Nil,\n    target = (2, 2) :: (1, 4) :: (7, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.value % 2 = 0\", values = \"*\"),\n    updateNotMatched(set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (0, 2), // Not matched (by target), inserted\n      (2, 3), // Not matched by source, updated\n      (1, 4), // Matched, no change\n      // (5, 5) // Not matched (by target), not inserted\n      (7, 4) // Not matched by source, updated\n    ),\n    cdc = Seq(\n      (0, 2, \"insert\"),\n      (2, 2, \"update_preimage\"),\n      (2, 3, \"update_postimage\"),\n      (7, 3, \"update_preimage\"),\n      (7, 4, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"not matched by source delete + conditional insert\")(\n    source = (1, 1) :: (0, 2) :: (5, 5) :: Nil,\n    target = (2, 2) :: (1, 4) :: (7, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.value % 2 = 0\", values = \"*\"),\n    deleteNotMatched(condition = \"t.value > 2\"))(\n    result = Seq(\n      (0, 2), // Not matched (by target), inserted\n      (2, 2), // Not matched by source, no change\n      (1, 4) // Matched, no change\n      // (5, 5) // Not matched (by target), not inserted\n      // (7, 3) Not matched by source, deleted\n    ),\n    cdc = Seq((0, 2, \"insert\"), (7, 3, \"delete\")))\n\n  testExtendedMergeWithCDC(\"when not matched by source updates all rows\")(\n    source = (1, 1) :: (0, 2) :: (5, 5) :: Nil,\n    target = (3, 3) :: (4, 4) :: (6, 6) :: (7, 7) :: (8, 8) :: (9, 9) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(set = \"t.value = t.value + 1\"))(\n    result = Seq(\n      (3, 4), // Not matched by source, updated\n      (4, 5), // Not matched by source, updated\n      (6, 7), // Not matched by source, updated\n      (7, 8), // Not matched by source, updated\n      (8, 9), // Not matched by source, updated\n      (9, 10) // Not matched by source, updated\n    ),\n    cdc = Seq(\n      (3, 3, \"update_preimage\"),\n      (3, 4, \"update_postimage\"),\n      (4, 4, \"update_preimage\"),\n      (4, 5, \"update_postimage\"),\n      (6, 6, \"update_preimage\"),\n      (6, 7, \"update_postimage\"),\n      (7, 7, \"update_preimage\"),\n      (7, 8, \"update_postimage\"),\n      (8, 8, \"update_preimage\"),\n      (8, 9, \"update_postimage\"),\n      (9, 9, \"update_preimage\"),\n      (9, 10, \"update_postimage\")))\n\n  testExtendedMergeWithCDC(\"insert only with dummy not matched by source\")(\n    source = (1, 1) :: (0, 2) :: (5, 5) :: Nil,\n    target = (2, 2) :: (1, 4) :: (7, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.value % 2 = 0\", values = \"*\"),\n    deleteNotMatched(condition = \"t.value > 10\"))(\n    result = Seq(\n      (0, 2), // Not matched (by target), inserted\n      (2, 2), // Not matched by source, no change\n      (1, 4), // Matched, no change\n      // (5, 5) // Not matched (by target), not inserted\n      (7, 3) // Not matched by source, no change\n    ),\n    cdc = Seq((0, 2, \"insert\")))\n\n  testExtendedMergeWithCDC(\"empty source\")(\n    source = Nil,\n    target = (2, 2) :: (1, 4) :: (7, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"t.key = 2\", set = \"value = t.value + 1\"),\n    deleteNotMatched(condition = \"t.key = 7\"))(\n    result = Seq(\n      (2, 3), // Not matched by source, updated\n      (1, 4) // Not matched by source, no change\n      // (7, 3) Not matched by source, deleted\n    ),\n    cdc = Seq(\n      (2, 2, \"update_preimage\"),\n      (2, 3, \"update_postimage\"),\n      (7, 3, \"delete\")))\n\n  testExtendedMergeWithCDC(\"empty source delete only\")(\n    source = Nil,\n    target = (2, 2) :: (1, 4) :: (7, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(condition = \"t.key = 7\"))(\n    result = Seq(\n      (2, 2), // Not matched by source, no change\n      (1, 4) // Not matched by source, no change\n      // (7, 3) Not matched by source, deleted\n    ),\n    cdc = Seq((7, 3, \"delete\")))\n\n  testExtendedMergeWithCDC(\"all 3 clauses - no changes\")(\n    source = (1, 1) :: (0, 2) :: (5, 5) :: Nil,\n    target = (2, 2) :: (1, 4) :: (7, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"t.value > 10\", set = \"*\"),\n    insert(condition = \"s.value > 10\", values = \"*\"),\n    deleteNotMatched(condition = \"t.value > 10\"))(\n    result = Seq(\n      (2, 2), // Not matched by source, no change\n      (1, 4), // Matched, no change\n      (7, 3) // Not matched by source, no change\n    ),\n    cdc = Seq.empty)\n}\n\ntrait MergeIntoNotMatchedBySourceSuite extends MergeIntoSuiteBaseMixin {\n  import testImplicits._\n\n  // Test analysis errors with NOT MATCHED BY SOURCE clauses.\n  testMergeAnalysisException(\n    \"error on multiple not matched by source update clauses without condition\")(\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"t.key == 3\", set = \"value = 2 * value\"),\n    updateNotMatched(set = \"value = 3 * value\"),\n    updateNotMatched(set = \"value = 4 * value\"))(\n    expectedErrorClass = \"NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\n    \"error on multiple not matched by source update/delete clauses without condition\")(\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"t.key == 3\", set = \"value = 2 * value\"),\n    deleteNotMatched(),\n    updateNotMatched(set = \"value = 4 * value\"))(\n    expectedErrorClass = \"NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\n    \"error on non-empty condition following empty condition in not matched by source \" +\n      \"update clauses\")(\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(set = \"value = 2 * value\"),\n    updateNotMatched(condition = \"t.key < 3\", set = \"value = value\"))(\n    expectedErrorClass = \"NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\n    \"error on non-empty condition following empty condition in not matched by source \" +\n      \"delete clauses\")(\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(),\n    deleteNotMatched(condition = \"t.key < 3\"))(\n    expectedErrorClass = \"NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\"update not matched condition - unknown reference\")(\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"unknownAttrib > 1\", set = \"tgtValue = tgtValue + 1\"))(\n    expectedErrorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n    expectedMessageParameters = Map(\n      \"sqlExpr\" -> \"unknownAttrib\",\n      \"clause\" -> \"UPDATE condition\",\n      \"cols\" -> \"t.key, t.tgtValue\"))\n\n  testMergeAnalysisException(\"update not matched condition - aggregation function\")(\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"max(0) > 0\", set = \"tgtValue = tgtValue + 1\"))(\n    expectedErrorClass = \"DELTA_AGGREGATION_NOT_SUPPORTED\",\n    expectedMessageParameters = Map(\n      \"operation\" -> \"UPDATE condition of MERGE operation\",\n      \"predicate\" -> \"(condition = (max(0) > 0))\"))\n\n  testMergeAnalysisException(\"update not matched condition - subquery\")(\n    mergeOn = \"s.key = t.key\",\n    updateNotMatched(condition = \"tgtValue in (select value from t)\", set = \"tgtValue = 1\"))(\n    expectedErrorClass = \"TABLE_OR_VIEW_NOT_FOUND\",\n    expectedMessageParameters = Map(\"relationName\" -> \"`t`\"))\n\n  testMergeAnalysisException(\"delete not matched condition - unknown reference\")(\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(condition = \"unknownAttrib > 1\"))(\n    expectedErrorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n    expectedMessageParameters = Map(\n      \"sqlExpr\" -> \"unknownAttrib\",\n      \"clause\" -> \"DELETE condition\",\n      \"cols\" -> \"t.key, t.tgtValue\"))\n\n  testMergeAnalysisException(\"delete not matched condition - aggregation function\")(\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(condition = \"max(0) > 0\"))(\n    expectedErrorClass = \"DELTA_AGGREGATION_NOT_SUPPORTED\",\n    expectedMessageParameters = Map(\n      \"operation\" -> \"DELETE condition of MERGE operation\",\n      \"predicate\" -> \"(condition = (max(0) > 0))\"))\n\n  testMergeAnalysisException(\"delete not matched condition - subquery\")(\n    mergeOn = \"s.key = t.key\",\n    deleteNotMatched(condition = \"tgtValue in (select tgtValue from t)\"))(\n    expectedErrorClass = \"TABLE_OR_VIEW_NOT_FOUND\",\n    expectedMessageParameters = Map(\"relationName\" -> \"`t`\"))\n\n  test(\"special character in path - not matched by source delete\", NameBasedAccessIncompatible) {\n    withTempDir { tempDir =>\n      val source = s\"$tempDir/sou rce^\"\n      val target = s\"$tempDir/tar get=\"\n      spark.range(0, 10, 2).write.format(\"delta\").save(source)\n      spark.range(10).write.format(\"delta\").save(target)\n      executeMerge(\n        tgt = s\"delta.`$target` t\",\n        src = s\"delta.`$source` s\",\n        cond = \"t.id = s.id\",\n        clauses = deleteNotMatched())\n      checkAnswer(readDeltaTableByIdentifier(s\"delta.`$target`\"), Seq(0, 2, 4, 6, 8).toDF(\"id\"))\n    }\n  }\n\n  test(\"special character in path - not matched by source update\", NameBasedAccessIncompatible) {\n    withTempDir { tempDir =>\n      val source = s\"$tempDir/sou rce@\"\n      val target = s\"$tempDir/tar get#\"\n      spark.range(0, 10, 2).write.format(\"delta\").save(source)\n      spark.range(10).write.format(\"delta\").save(target)\n      executeMerge(\n        tgt = s\"delta.`$target` t\",\n        src = s\"delta.`$source` s\",\n        cond = \"t.id = s.id\",\n        clauses = updateNotMatched(set = \"id = t.id * 10\"))\n      checkAnswer(\n        readDeltaTableByIdentifier(s\"delta.`$target`\"),\n        Seq(0, 10, 2, 30, 4, 50, 6, 70, 8, 90).toDF(\"id\"))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.scalatest.matchers.must.Matchers.be\nimport org.scalatest.matchers.should.Matchers.noException\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.analysis.{Analyzer, ResolveSessionCatalog}\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.catalyst.plans.logical.{DeltaMergeInto, LogicalPlan}\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.execution.FileSourceScanExec\nimport org.apache.spark.sql.functions.udf\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{IntegerType, StructField, StructType}\n\ntrait MergeIntoSQLMixin extends MergeIntoSuiteBaseMixin\n  with MergeIntoSQLTestUtils\n  with DeltaSQLCommandTest\n  with DeltaTestUtilsForTempViews {\n\n  override def excluded: Seq[String] = super.excluded ++ Seq(\n    // Schema evolution SQL syntax is not yet supported\n    \"schema evolution enabled for the current command\"\n  )\n}\n\ntrait MergeIntoSQLNondeterministicOrderTests extends MergeIntoSQLMixin {\n  private def testNondeterministicOrder(insertOnly: Boolean): Unit = {\n    withTable(\"target\") {\n      // For the spark sql random() function the seed is fixed for both invocations\n      val trueRandom = () => Math.random()\n      val trueRandomUdf = udf(trueRandom)\n      spark.udf.register(\"trueRandom\", trueRandomUdf.asNondeterministic())\n\n      sql(\"CREATE TABLE target(`trgKey` INT, `trgValue` INT) using delta\")\n      sql(\"INSERT INTO target VALUES (1,2), (3,4)\")\n      // This generates different data sets on every execution\n      val sourceSql =\n        s\"\"\"\n           |(SELECT r.id AS srcKey, r.id AS srcValue\n           | FROM range(1, 100000) as r\n           |  JOIN (SELECT trueRandom() * 100000 AS bound) ON r.id < bound\n           |) AS source\n           |\"\"\".stripMargin\n\n      if (insertOnly) {\n        sql(s\"\"\"\n           |MERGE INTO target\n           |USING ${sourceSql}\n           |ON srcKey = trgKey\n           |WHEN NOT MATCHED THEN\n           |  INSERT (trgValue, trgKey) VALUES (srcValue, srcKey)\n           |\"\"\".stripMargin)\n      } else {\n        sql(s\"\"\"\n           |MERGE INTO target\n           |USING ${sourceSql}\n           |ON srcKey = trgKey\n           |WHEN MATCHED THEN\n           |  UPDATE SET trgValue = srcValue\n           |WHEN NOT MATCHED THEN\n           |  INSERT (trgValue, trgKey) VALUES (srcValue, srcKey)\n           |\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(s\"detect nondeterministic source - flag on\") {\n    withSQLConf(\n      // materializing source would fix determinism\n      DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> DeltaSQLConf.MergeMaterializeSource.NONE,\n      DeltaSQLConf.MERGE_FAIL_IF_SOURCE_CHANGED.key -> \"true\"\n    ) {\n      val e = intercept[UnsupportedOperationException](\n        testNondeterministicOrder(insertOnly = false)\n      )\n      assert(e.getMessage.contains(\"source dataset is not deterministic\"))\n    }\n  }\n\n  test(s\"detect nondeterministic source - flag on - insertOnly\") {\n    withSQLConf(\n        // materializing source would fix determinism\n        DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> DeltaSQLConf.MergeMaterializeSource.NONE,\n        DeltaSQLConf.MERGE_FAIL_IF_SOURCE_CHANGED.key -> \"true\") {\n      testNondeterministicOrder(insertOnly = true)\n    }\n  }\n\n  test(\"detect nondeterministic source - flag off\") {\n    withSQLConf(\n      // materializing source would fix determinism\n      DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> DeltaSQLConf.MergeMaterializeSource.NONE,\n      DeltaSQLConf.MERGE_FAIL_IF_SOURCE_CHANGED.key -> \"false\"\n    ) {\n      testNondeterministicOrder(insertOnly = false)\n    }\n  }\n\n  test(\"detect nondeterministic source - flag on, materialized\") {\n    withSQLConf(\n      // materializing source fixes determinism, so the source is no longer nondeterministic\n      DeltaSQLConf.MERGE_MATERIALIZE_SOURCE.key -> DeltaSQLConf.MergeMaterializeSource.ALL,\n      DeltaSQLConf.MERGE_FAIL_IF_SOURCE_CHANGED.key -> \"true\"\n    ) {\n      testNondeterministicOrder(insertOnly = false)\n    }\n  }\n}\n\ntrait MergeIntoSQLTests extends MergeIntoSQLMixin {\n\n  import testImplicits._\n\n  test(\"CTE as a source in MERGE\") {\n    withTable(\"source\") {\n      Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").write.format(\"delta\").saveAsTable(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      val merge = basicMergeStmt(\n        cte = Some(\"WITH cte1 AS (SELECT key1 + 2 AS key3, value FROM source)\"),\n        target = s\"$tableSQLIdentifier as target\",\n        source = \"cte1 src\",\n        condition = \"src.key3 = target.key2\",\n        update = \"key2 = 20 + src.key3, value = 20 + src.value\",\n        insert = \"(key2, value) VALUES (src.key3 - 10, src.value + 10)\")\n\n      QueryTest.checkAnswer(sql(merge), Seq(Row(2, 1, 0, 1)))\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(1, 4) :: // No change\n        Row(22, 23) :: // Update\n        Row(-7, 11) :: // Insert\n        Nil)\n    }\n  }\n\n  test(\"inline tables with set operations in source query\") {\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trg\",\n        source =\n          \"\"\"\n            |( SELECT * FROM VALUES (1, 6, \"a\") as t1(key1, value, others)\n            |  UNION\n            |  SELECT * FROM VALUES (0, 3, \"b\") as t2(key1, value, others)\n            |) src\n          \"\"\".stripMargin,\n        condition = \"src.key1 = trg.key2\",\n        update = \"trg.key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(trg.key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(21, 26) :: // Update\n          Row(-10, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  testNestedDataSupport(\"conflicting assignments between two nested fields\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 0 } } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1 } } }\"\"\",\n    update = \"value.a.x = 2\" :: \"value.a.x = 3\" :: Nil,\n    errorStrs = \"There is a conflict from these SET columns\" :: Nil)\n\n  test(\"Negative case - basic syntax analysis SQL\") {\n    withTable(\"source\") {\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      // duplicate column names in update clause\n      var e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 1, key2 = 2\",\n          insert = \"(key2, value) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"There is a conflict from these SET columns\")\n\n      // duplicate column names in insert clause\n      e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 1, value = 2\",\n          insert = \"(key2, key2) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"Duplicate column names in INSERT clause\")\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"no column is used from source table - column pruning, isPartitioned: $isPartitioned\") {\n      withTable(\"source\") {\n        val partitions = if (isPartitioned) \"key2\" :: Nil else Nil\n        append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), partitions)\n        Seq((1, 1, \"a\"), (0, 3, \"b\")).toDF(\"key1\", \"value\", \"col1\")\n          .createOrReplaceTempView(\"source\")\n\n        // filter pushdown can cause empty join conditions and cross-join being used\n        withCrossJoinEnabled {\n          val merge = basicMergeStmt(\n            target = tableSQLIdentifier,\n            source = \"source src\",\n            condition = \"key2 < 0\", // no row match\n            update = \"key2 = 20, value = 20\",\n            insert = \"(key2, value) VALUES (10, 10)\")\n\n          val df = sql(merge)\n\n          val readSchema: Seq[StructType] = df.queryExecution.executedPlan.collect {\n            case f: FileSourceScanExec => f.requiredSchema\n          }\n          assert(readSchema.flatten.isEmpty, \"column pruning does not work\")\n        }\n\n        checkAnswer(readDeltaTableByIdentifier(),\n          Row(2, 2) :: // No change\n          Row(1, 4) :: // No change\n          Row(10, 10) :: // Insert\n          Row(10, 10) :: // Insert\n          Nil)\n      }\n    }\n  }\n\n  test(\"negative case - omit multiple insert conditions\") {\n    withTable(\"source\") {\n      Seq((1, 1), (0, 3)).toDF(\"srcKey\", \"srcValue\").write.saveAsTable(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"trgKey\", \"trgValue\"))\n\n      // only the last NOT MATCHED clause can omit the condition\n      val e = intercept[ParseException](\n        sql(s\"\"\"\n          |MERGE INTO $tableSQLIdentifier\n          |USING source\n          |ON srcKey = trgKey\n          |WHEN NOT MATCHED THEN\n          |  INSERT (trgValue, trgKey) VALUES (srcValue, srcKey + 1)\n          |WHEN NOT MATCHED THEN\n          |  INSERT (trgValue, trgKey) VALUES (srcValue, srcKey)\n        \"\"\".stripMargin))\n      assert(e.getMessage.contains(\n        \"only the last NOT MATCHED [BY TARGET] clause can omit the condition\"))\n    }\n  }\n\n  test(\"detect nondeterministic merge condition\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicCondition = \"rand() > 0.5\"\n        val e = intercept[AnalysisException](\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key AND $nonDeterministicCondition\",\n            update(condition = \"s.key < 2\", set = \"key = s.key, value = s.value\")))\n        assert(e.getMessage.contains(\"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\"))\n    }\n  }\n\n  test(\"detect nondeterministic update condition in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicCondition = \"rand() > 0.5\"\n        val e = intercept[AnalysisException](\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            update(\n              condition = nonDeterministicCondition,\n              set = \"key = s.key, value = s.value\")))\n        assert(e.getMessage.contains(\"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\"))\n    }\n  }\n\n  test(\"detect nondeterministic delete condition in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicCondition = \"rand() > 0.5\"\n        val e = intercept[AnalysisException](\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            delete(condition = nonDeterministicCondition)))\n        assert(e.getMessage.contains(\"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\"))\n    }\n  }\n\n  test(\"detect nondeterministic insert condition in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicCondition = \"rand() > 0.5\"\n        val e = intercept[AnalysisException](\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            insert(\n              condition = nonDeterministicCondition,\n              values = \"(key, value) VALUES (s.key, s.value)\")))\n        assert(e.getMessage.contains(\"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\"))\n    }\n  }\n\n  test(\"detect nondeterministic updateNotMatched condition in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicCondition = \"rand() > 0.5\"\n        val e = intercept[AnalysisException](\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            updateNotMatched(\n              condition = nonDeterministicCondition,\n              set = \"key = t.key, value = t.value + 1\")))\n        assert(e.getMessage.contains(\"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\"))\n    }\n  }\n\n  test(\"detect nondeterministic deleteNotMatched condition in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicCondition = \"rand() > 0.5\"\n        val e = intercept[AnalysisException](\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            deleteNotMatched(condition = nonDeterministicCondition)))\n        assert(e.getMessage.contains(\"DELTA_NON_DETERMINISTIC_FUNCTION_NOT_SUPPORTED\"))\n    }\n  }\n\n  test(\"allow nondeterministic update action in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicAction = \"rand()\"\n        noException should be thrownBy {\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            update(\n              condition = \"s.key < 2\",\n              set = s\"key = s.key, value = $nonDeterministicAction\"))\n        }\n    }\n  }\n\n  test(\"allow nondeterministic insert action in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicAction = \"rand()\"\n        noException should be thrownBy {\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            insert(\n              condition = \"s.key < 2\",\n              values = s\"(key, value) VALUES (s.key, $nonDeterministicAction)\"))\n        }\n    }\n  }\n\n  test(\"allow nondeterministic updateNotMatched action in merge\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n      target = (-1, -10) :: (1, 1) :: (2, 2) :: Nil\n    ) {\n      case (sourceName, targetName) =>\n        val nonDeterministicAction = \"rand()\"\n        noException should be thrownBy {\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = s\"s.key = t.key\",\n            updateNotMatched(\n              condition = \"t.key < 2\",\n              set = s\"key = t.key, value = t.value + $nonDeterministicAction\"))\n        }\n    }\n  }\n\n  test(\"merge into a dataset temp views with star\") {\n    withTempView(\"v\") {\n      def testMergeWithView(testClue: String): Unit = {\n        withClue(testClue) {\n          withTempView(\"src\") {\n            sql(\"CREATE TEMP VIEW src AS SELECT * FROM VALUES (10, 1), (20, 2) AS t(value, key)\")\n            sql(\n              s\"\"\"\n                 |MERGE INTO v\n                 |USING src\n                 |ON src.key = v.key\n                 |WHEN MATCHED THEN\n                 |  UPDATE SET *\n                 |WHEN NOT MATCHED THEN\n                 |  INSERT *\n                 |\"\"\".stripMargin)\n            checkAnswer(spark.sql(s\"select * from v\"), Seq(Row(0, 0), Row(1, 10), Row(2, 20)))\n          }\n        }\n      }\n\n      // View on path-based table\n      append(Seq((0, 0), (1, 1)).toDF(\"key\", \"value\"))\n      readDeltaTableByIdentifier().createOrReplaceTempView(\"v\")\n      testMergeWithView(\"with path-based table\")\n\n      // View on catalog table\n      withTable(\"tab\") {\n        Seq((0, 0), (1, 1)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        spark.table(\"tab\").as(\"name\").createOrReplaceTempView(\"v\")\n        testMergeWithView(tableSQLIdentifier)\n      }\n    }\n  }\n\n\n  testWithTempView(\"Update specific column does not work in temp views\") { isSQLTempView =>\n    withJsonData(\n      \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1 } } }\"\"\",\n      \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 2 } } }\"\"\"\n    ) { (sourceName, targetName) =>\n      createTempViewFromTable(targetName, isSQLTempView)\n      val fieldNames = spark.table(targetName).schema.fieldNames\n      val fieldNamesStr = fieldNames.mkString(\"`\", \"`, `\", \"`\")\n      val e = intercept[DeltaAnalysisException] {\n        executeMerge(\n          target = \"v t\",\n          source = s\"$sourceName s\",\n          condition = \"s.key = t.key\",\n          update = \"value.a.x = s.value.a.x\",\n          insert = s\"($fieldNamesStr) VALUES ($fieldNamesStr)\")\n      }\n      assert(e.getMessage.contains(\"Unexpected assignment key\"))\n    }\n  }\n\n  test(\"Complex Data Type - Array of Struct\") {\n    withTable(\"source\") {\n      withTable(\"target\") {\n        // scalastyle:off line.size.limit\n        sql(\"CREATE TABLE source(`smtUidNr` STRING,`evt` ARRAY<STRUCT<`busLinCd`: STRING, `cmyHdrOidNr`: STRING, `cmyLinNr`: STRING, `coeOidNr`: STRING, `dclOidNr`: STRING, `evtCd`: STRING, `evtDclUidNr`: STRING, `evtDscTe`: STRING, `evtDt`: STRING, `evtLclTmZnNa`: STRING, `evtLclTs`: STRING, `evtOidNr`: STRING, `evtRef`: ARRAY<STRUCT<`refDt`: STRING, `refNr`: STRING, `refTypCd`: STRING, `refTypDscTe`: STRING>>, `evtShu`: ARRAY<STRUCT<`ledPkgIr`: STRING, `shuNr`: STRING, `shuRef`: ARRAY<STRUCT<`shuRefDscTe`: STRING, `shuRefDt`: STRING, `shuRefEffDt`: STRING, `shuRefNr`: STRING, `shuRefTe`: STRING, `shuRefTypCd`: STRING>>>>, `evtTypCd`: STRING, `evtUsrNr`: STRING, `evtUtcTcfQy`: STRING, `evtUtcTs`: STRING, `evtWstNa`: STRING, `loc`: ARRAY<STRUCT<`adCnySdvCd`: STRING, `adMunNa`: STRING, `adPslCd`: STRING, `al1Te`: STRING, `al2Te`: STRING, `al3Te`: STRING, `locAdCnyCd`: STRING, `locOgzNr`: STRING, `locXcpDclPorCd`: STRING, `upsDisNr`: STRING, `upsRegNr`: STRING>>, `mltDelOdrNr`: STRING, `mltPrfOfDelNa`: STRING, `mltSmtConNr`: STRING, `mnfOidNr`: STRING, `rpnEntLinNr`: STRING, `rpnEntLvlStsCd`: STRING, `rpnGovAcoTe`: STRING, `rpnInfSrcCrtLclTmZnNa`: STRING, `rpnInfSrcCrtLclTs`: STRING, `rpnInfSrcCrtUtcTcfQy`: STRING, `rpnInfSrcCrtUtcTs`: STRING, `rpnLinLvlStsCd`: STRING, `rpnPgaLinNr`: STRING, `smtDcvDt`: STRING, `smtNr`: STRING, `smtUidNr`: STRING, `xcpCtmDspCd`: STRING, `xcpGovAcoTe`: STRING, `xcpPgmCd`: STRING, `xcpRlvCd`: STRING, `xcpRlvDscTe`: STRING, `xcpRlvLclTmZnNa`: STRING, `xcpRlvLclTs`: STRING, `xcpRlvUtcTcfQy`: STRING, `xcpRlvUtcTs`: STRING, `xcpRsnCd`: STRING, `xcpRsnDscTe`: STRING, `xcpStsCd`: STRING, `xcpStsDscTe`: STRING>>,`msgTs` TIMESTAMP) using delta\")\n        sql(\"CREATE TABLE target(`smtUidNr` STRING,`evt` ARRAY<STRUCT<`busLinCd`: STRING, `dclOidNr`: STRING, `evtCd`: STRING, `evtDclUidNr`: STRING, `evtDscTe`: STRING, `evtDt`: STRING, `evtLclTmZnNa`: STRING, `evtLclTs`: STRING, `evtOidNr`: STRING, `evtRef`: ARRAY<STRUCT<`refDt`: STRING, `refNr`: STRING, `refTypCd`: STRING, `refTypDscTe`: STRING>>, `evtShu`: ARRAY<STRUCT<`ledPkgIr`: STRING, `shuNr`: STRING, `shuRef`: ARRAY<STRUCT<`shuRefDscTe`: STRING, `shuRefDt`: STRING, `shuRefEffDt`: STRING, `shuRefNr`: STRING, `shuRefTe`: STRING, `shuRefTypCd`: STRING>>>>, `evtTypCd`: STRING, `evtUsrNr`: STRING, `evtUtcTcfQy`: STRING, `evtUtcTs`: STRING, `evtWstNa`: STRING, `loc`: ARRAY<STRUCT<`adCnySdvCd`: STRING, `adMunNa`: STRING, `adPslCd`: STRING, `al1Te`: STRING, `al2Te`: STRING, `al3Te`: STRING, `locAdCnyCd`: STRING, `locOgzNr`: STRING, `locXcpDclPorCd`: STRING, `upsDisNr`: STRING, `upsRegNr`: STRING>>, `mltDelOdrNr`: STRING, `mltPrfOfDelNa`: STRING, `mltSmtConNr`: STRING, `mnfOidNr`: STRING, `rpnEntLinNr`: STRING, `rpnEntLvlStsCd`: STRING, `rpnGovAcoTe`: STRING, `rpnInfSrcCrtLclTmZnNa`: STRING, `rpnInfSrcCrtLclTs`: STRING, `rpnInfSrcCrtUtcTcfQy`: STRING, `rpnInfSrcCrtUtcTs`: STRING, `rpnLinLvlStsCd`: STRING, `smtDcvDt`: STRING, `smtNr`: STRING, `smtUidNr`: STRING, `xcpCtmDspCd`: STRING, `xcpRlvCd`: STRING, `xcpRlvDscTe`: STRING, `xcpRlvLclTmZnNa`: STRING, `xcpRlvLclTs`: STRING, `xcpRlvUtcTcfQy`: STRING, `xcpRlvUtcTs`: STRING, `xcpRsnCd`: STRING, `xcpRsnDscTe`: STRING, `xcpStsCd`: STRING, `xcpStsDscTe`: STRING, `cmyHdrOidNr`: STRING, `cmyLinNr`: STRING, `coeOidNr`: STRING, `rpnPgaLinNr`: STRING, `xcpGovAcoTe`: STRING, `xcpPgmCd`: STRING>>,`msgTs` TIMESTAMP) using delta\")\n        // scalastyle:on line.size.limit\n        sql(\n          s\"\"\"\n             |MERGE INTO target as r\n             |USING source as u\n             |ON u.smtUidNr = r.smtUidNr\n             |WHEN MATCHED and u.msgTs > r.msgTs THEN\n             |  UPDATE SET *\n             |WHEN NOT MATCHED THEN\n             |  INSERT *\n             \"\"\".stripMargin)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { partitioned =>\n    test(s\"User defined _change_type column doesn't get dropped - partitioned=$partitioned\") {\n      withTable(\"target\") {\n        sql(\n          s\"\"\"CREATE TABLE target USING DELTA\n             |${if (partitioned) \"PARTITIONED BY (part) \" else \"\"}\n             |TBLPROPERTIES (delta.enableChangeDataFeed = false)\n             |AS SELECT id, int(id / 10) AS part, 'foo' as _change_type\n             |FROM RANGE(1000)\n             |\"\"\".stripMargin)\n        executeMerge(\n          target = \"target as t\",\n          source =\n            \"\"\"(\n              |  SELECT id * 42 AS id, int(id / 10) AS part, 'bar' as _change_type FROM RANGE(33)\n              |) s\"\"\".stripMargin,\n          condition = \"t.id = s.id\",\n          update = \"*\",\n          insert = \"*\")\n\n        sql(\"SELECT id, _change_type FROM target\").collect().foreach { row =>\n          val _change_type = row.getString(1)\n          assert(_change_type === \"foo\" || _change_type === \"bar\",\n            s\"Invalid _change_type for id=${row.get(0)}\")\n        }\n      }\n    }\n  }\n\n  test(\"SET * with schema evolution\") {\n    withTable(\"tgt\", \"src\") {\n      withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n        sql(\"create table tgt(id int, delicious string, dummy_col string) using delta\")\n        sql(\"create table src(id int, delicious string) using parquet\")\n        // Make sure this MERGE command can resolve\n        sql(\n          \"\"\"\n            |merge into tgt as target\n            |using (select * from src) as source on target.id=source.id\n            |when matched then update set *\n            |when not matched then insert *;\n            |\"\"\".stripMargin)\n      }\n    }\n  }\n}\n\ntrait MergeIntoSQLColumnMappingOverrides extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests: Seq[String] =\n    Seq(\"schema evolution - new nested column with update non-* and insert * - \" +\n      \"array of struct - longer target\")\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoScalaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.Locale\n\nimport org.apache.spark.sql.delta.actions.SetTransaction\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaExcludedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.plans.Inner\nimport org.apache.spark.sql.catalyst.plans.logical.{Assignment, DeltaMergeIntoClause, Join}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.types.StructType\n\ntrait MergeIntoScalaMixin extends MergeIntoSuiteBaseMixin\n  with MergeIntoScalaTestUtils\n  with DeltaSQLCommandTest\n  with DeltaDMLTestUtilsPathBased\n  with DeltaTestUtilsForTempViews\n  with DeltaExcludedTestMixin {\n\n  // Maps expected error classes to actual error classes. Used to handle error classes that are\n  // different when running using SQL vs. Scala.\n  override protected val mappedErrorClasses: Map[String, String] = Map(\n   \"NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\" -> \"DELTA_NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\",\n   \"NON_LAST_NOT_MATCHED_BY_TARGET_CLAUSE_OMIT_CONDITION\" ->\n     \"DELTA_NON_LAST_NOT_MATCHED_CLAUSE_OMIT_CONDITION\",\n   \"NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\" ->\n     \"DELTA_NON_LAST_NOT_MATCHED_BY_SOURCE_CLAUSE_OMIT_CONDITION\"\n  )\n\n  // scalastyle:off argcount\n  override def testNestedDataSupport(name: String, namePrefix: String = \"nested data support\")(\n      source: String,\n      target: String,\n      update: Seq[String],\n      insert: String = null,\n      targetSchema: StructType = null,\n      sourceSchema: StructType = null,\n      result: String = null,\n      errorStrs: Seq[String] = null,\n      confs: Seq[(String, String)] = Seq.empty): Unit = {\n    // scalastyle:on argcount\n\n    require(result == null ^ errorStrs == null, \"either set the result or the error strings\")\n\n    val testName =\n      if (result != null) s\"$namePrefix - $name\" else s\"$namePrefix - analysis error - $name\"\n\n    test(testName) {\n      withSQLConf(confs: _*) {\n        withJsonData(source, target, targetSchema, sourceSchema) { case (sourceName, targetName) =>\n          val pathOrName = parsePath(targetName)\n          val fieldNames = readDeltaTable(pathOrName).schema.fieldNames\n          val keyName = s\"`${fieldNames.head}`\"\n\n          def execMerge() = {\n            val t = DeltaTestUtils.getDeltaTableForIdentifierOrPath(\n                spark,\n                DeltaTestUtils.getTableIdentifierOrPath(targetName))\n            val m = t.as(\"t\")\n              .merge(\n                spark.table(sourceName).as(\"s\"),\n                s\"s.$keyName = t.$keyName\")\n            val withUpdate = if (update == Seq(\"*\")) {\n              m.whenMatched().updateAll()\n            } else {\n              val updateColExprMap = parseUpdate(update)\n              m.whenMatched().updateExpr(updateColExprMap)\n            }\n\n            if (insert == \"*\") {\n              withUpdate.whenNotMatched().insertAll().execute()\n            } else {\n              val insertExprMaps = if (insert != null) {\n                parseInsert(insert, None)\n              } else {\n                fieldNames.map { f => s\"t.`$f`\" -> s\"s.`$f`\" }.toMap\n              }\n\n              withUpdate.whenNotMatched().insertExpr(insertExprMaps).execute()\n            }\n          }\n\n          if (result != null) {\n            execMerge()\n            val expectedDf = readFromJSON(strToJsonSeq(result), targetSchema)\n            checkAnswer(readDeltaTable(pathOrName), expectedDf)\n          } else {\n            val e = intercept[AnalysisException] {\n              execMerge()\n            }\n            errorStrs.foreach { s => errorContains(e.getMessage, s) }\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait MergeIntoScalaTests extends MergeIntoScalaMixin {\n\n  import testImplicits._\n\n  test(\"basic scala API\") {\n    withTable(\"source\") {\n      append(Seq((1, 10), (2, 20)).toDF(\"key1\", \"value1\"), Nil)  // target\n      val source = Seq((1, 100), (3, 30)).toDF(\"key2\", \"value2\")  // source\n\n      io.delta.tables.DeltaTable.forPath(spark, tempPath)\n        .merge(source, \"key1 = key2\")\n        .whenMatched().updateExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n        .whenNotMatched().insertExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n        .execute()\n\n      checkAnswer(\n        readDeltaTable(tempPath),\n        Row(1, 100) ::    // Update\n          Row(2, 20) ::     // No change\n          Row(3, 30) ::     // Insert\n          Nil)\n    }\n  }\n\n\n  // test created to validate a fix for a bug where merge command was\n  // resulting in a empty target table when statistics collection is disabled\n  test(\"basic scala API - without stats\") {\n    withSQLConf((DeltaSQLConf.DELTA_COLLECT_STATS.key, \"false\")) {\n      withTable(\"source\") {\n        append(Seq((1, 10), (2, 20)).toDF(\"key1\", \"value1\"), Nil) // target\n        val source = Seq((1, 100), (3, 30)).toDF(\"key2\", \"value2\") // source\n\n        io.delta.tables.DeltaTable.forPath(spark, tempPath)\n          .merge(source, \"key1 = key2\")\n          .whenMatched().updateExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n          .whenNotMatched().insertExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n          .execute()\n\n        checkAnswer(\n          readDeltaTable(tempPath),\n          Row(1, 100) :: // Update\n            Row(2, 20) :: // No change\n            Row(3, 30) :: // Insert\n            Nil)\n      }\n    }\n  }\n\n  test(\"extended scala API\") {\n    withTable(\"source\") {\n      append(Seq((1, 10), (2, 20), (4, 40)).toDF(\"key1\", \"value1\"), Nil)  // target\n      val source = Seq((1, 100), (3, 30), (4, 41)).toDF(\"key2\", \"value2\")  // source\n\n      io.delta.tables.DeltaTable.forPath(spark, tempPath)\n        .merge(source, \"key1 = key2\")\n        .whenMatched(\"key1 = 4\").delete()\n        .whenMatched(\"key2 = 1\").updateExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n        .whenNotMatched(\"key2 = 3\").insertExpr(Map(\"key1\" -> \"key2\", \"value1\" -> \"value2\"))\n        .execute()\n\n      checkAnswer(\n        readDeltaTable(tempPath),\n        Row(1, 100) ::    // Update\n          Row(2, 20) ::     // No change\n          Row(3, 30) ::     // Insert\n          Nil)\n    }\n  }\n\n  test(\"extended scala API with Column\") {\n    withTable(\"source\") {\n      append(Seq((1, 10), (2, 20), (4, 40)).toDF(\"key1\", \"value1\"), Nil)  // target\n      val source = Seq((1, 100), (3, 30), (4, 41)).toDF(\"key2\", \"value2\")  // source\n\n      io.delta.tables.DeltaTable.forPath(spark, tempPath)\n        .merge(source, functions.expr(\"key1 = key2\"))\n        .whenMatched(functions.expr(\"key1 = 4\")).delete()\n        .whenMatched(functions.expr(\"key2 = 1\"))\n        .update(Map(\"key1\" -> functions.col(\"key2\"), \"value1\" -> functions.col(\"value2\")))\n        .whenNotMatched(functions.expr(\"key2 = 3\"))\n        .insert(Map(\"key1\" -> functions.col(\"key2\"), \"value1\" -> functions.col(\"value2\")))\n        .execute()\n\n      checkAnswer(\n        readDeltaTable(tempPath),\n        Row(1, 100) ::    // Update\n          Row(2, 20) ::     // No change\n          Row(3, 30) ::     // Insert\n          Nil)\n    }\n  }\n\n  test(\"updateAll and insertAll\") {\n    withTable(\"source\") {\n      append(Seq((1, 10), (2, 20), (4, 40), (5, 50)).toDF(\"key\", \"value\"), Nil)\n      val source = Seq((1, 100), (3, 30), (4, 41), (5, 51), (6, 60))\n        .toDF(\"key\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        target = s\"delta.`$tempPath` as t\",\n        source = \"source s\",\n        condition = \"s.key = t.key\",\n        update = \"*\",\n        insert = \"*\")\n\n      checkAnswer(\n        readDeltaTable(tempPath),\n        Row(1, 100) ::    // Update\n          Row(2, 20) ::     // No change\n          Row(3, 30) ::     // Insert\n          Row(4, 41) ::     // Update\n          Row(5, 51) ::     // Update\n          Row(6, 60) ::     // Insert\n          Nil)\n    }\n  }\n\n  test(\"updateAll and insertAll with columns containing dot\") {\n    withTable(\"source\") {\n      append(Seq((1, 10), (2, 20), (4, 40)).toDF(\"key\", \"the.value\"), Nil) // target\n      val source = Seq((1, 100), (3, 30), (4, 41)).toDF(\"key\", \"the.value\") // source\n\n      io.delta.tables.DeltaTable.forPath(spark, tempPath).as(\"t\")\n        .merge(source.as(\"s\"), \"t.key = s.key\")\n        .whenMatched()\n        .updateAll()\n        .whenNotMatched()\n        .insertAll()\n        .execute()\n\n      checkAnswer(\n        readDeltaTable(tempPath),\n        Row(1, 100) :: // Update\n          Row(2, 20) :: // No change\n          Row(4, 41) :: // Update\n          Row(3, 30) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"update with empty map should do nothing\") {\n    append(Seq((1, 10), (2, 20)).toDF(\"trgKey\", \"trgValue\"), Nil) // target\n    val source = Seq((1, 100), (3, 30)).toDF(\"srcKey\", \"srcValue\") // source\n    io.delta.tables.DeltaTable.forPath(spark, tempPath)\n      .merge(source, \"srcKey = trgKey\")\n      .whenMatched().updateExpr(Map[String, String]())\n      .whenNotMatched().insertExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n      .execute()\n\n    checkAnswer(\n      readDeltaTable(tempPath),\n      Row(1, 10) ::       // Not updated since no update clause\n      Row(2, 20) ::       // No change due to merge condition\n      Row(3, 30) ::       // Not updated since no update clause\n      Nil)\n\n    // match condition should not be ignored when map is empty\n    io.delta.tables.DeltaTable.forPath(spark, tempPath)\n      .merge(source, \"srcKey = trgKey\")\n      .whenMatched(\"trgKey = 1\").updateExpr(Map[String, String]())\n      .whenMatched().delete()\n      .whenNotMatched().insertExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n      .execute()\n\n    checkAnswer(\n      readDeltaTable(tempPath),\n      Row(1, 10) ::     // Neither updated, nor deleted (condition is not ignored)\n      Row(2, 20) ::     // No change due to merge condition\n      Nil)              // Deleted (3, 30)\n  }\n\n  // Checks specific to the APIs that are automatically handled by parser for SQL\n  test(\"check invalid merge API calls\") {\n    withTable(\"source\") {\n      append(Seq((1, 10), (2, 20)).toDF(\"trgKey\", \"trgValue\"), Nil) // target\n      val source = Seq((1, 100), (3, 30)).toDF(\"srcKey\", \"srcValue\") // source\n\n      // There must be at least one WHEN clause in a MERGE statement\n      var e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.forPath(spark, tempPath)\n          .merge(source, \"srcKey = trgKey\")\n          .execute()\n      }\n      errorContains(e.getMessage, \"There must be at least one WHEN clause in a MERGE statement\")\n\n      // When there are multiple MATCHED clauses in a MERGE statement,\n      // the first MATCHED clause must have a condition\n      e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.forPath(spark, tempPath)\n          .merge(source, \"srcKey = trgKey\")\n          .whenMatched().delete()\n          .whenMatched(\"trgKey = 1\").updateExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n          .whenNotMatched().insertExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n          .execute()\n      }\n      errorContains(e.getMessage, \"When there are more than one MATCHED clauses in a MERGE \" +\n        \"statement, only the last MATCHED clause can omit the condition.\")\n\n      e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.forPath(spark, tempPath)\n          .merge(source, \"srcKey = trgKey\")\n          .whenMatched().updateExpr(Map(\"trgKey\" -> \"srcKey\", \"*\" -> \"*\"))\n          .whenNotMatched().insertExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n          .execute()\n      }\n      errorContains(e.getMessage, \"cannot resolve `*` in UPDATE clause\")\n\n      e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.forPath(spark, tempPath)\n          .merge(source, \"srcKey = trgKey\")\n          .whenMatched().updateExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n          .whenNotMatched().insertExpr(Map(\"*\" -> \"*\"))\n          .execute()\n      }\n      errorContains(e.getMessage, \"cannot resolve `*` in INSERT clause\")\n\n      e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.forPath(spark, tempPath)\n          .merge(source, \"srcKey = trgKey\")\n          .whenNotMatchedBySource().updateExpr(Map(\"*\" -> \"*\"))\n          .execute()\n      }\n      errorContains(e.getMessage, \"cannot resolve `*` in UPDATE clause\")\n    }\n  }\n\n  test(\"merge after schema change\") {\n    withSQLConf((DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\")) {\n      withTempPath { targetDir =>\n        val targetPath = targetDir.getCanonicalPath\n        spark.range(10).write.format(\"delta\").save(targetPath)\n        val t = io.delta.tables.DeltaTable.forPath(spark, targetPath).as(\"t\")\n        assert(t.toDF.schema == StructType.fromDDL(\"id LONG\"))\n\n        // Do one merge to change the schema.\n        t.merge(Seq((11L, \"newVal11\")).toDF(\"id\", \"newCol1\").as(\"s\"), \"t.id = s.id\")\n          .whenMatched().updateAll()\n          .whenNotMatched().insertAll()\n          .execute()\n        // assert(t.toDF.schema == StructType.fromDDL(\"id LONG, newCol1 STRING\"))\n\n        // SC-35564 - ideally this shouldn't throw an error, but right now we can't fix it without\n        // causing a regression.\n        val ex = intercept[Exception] {\n          t.merge(Seq((12L, \"newVal12\")).toDF(\"id\", \"newCol2\").as(\"s\"), \"t.id = s.id\")\n            .whenMatched().updateAll()\n            .whenNotMatched().insertAll()\n            .execute()\n        }\n        ex.getMessage.contains(\"schema of your Delta table has changed in an incompatible way\")\n      }\n    }\n  }\n\n  test(\"merge without table alias\") {\n    withTempDir { dir =>\n      val location = dir.getAbsolutePath\n      Seq((1, 1, 1), (2, 2, 2)).toDF(\"part\", \"id\", \"n\").write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(location)\n      val table = io.delta.tables.DeltaTable.forPath(spark, location)\n      val data1 = Seq((2, 2, 4, 2), (9, 3, 6, 9), (3, 3, 9, 3)).toDF(\"part\", \"id\", \"n\", \"part2\")\n      table.alias(\"t\").merge(\n        data1,\n        \"t.part = part2\")\n        .whenMatched().updateAll()\n        .whenNotMatched().insertAll()\n        .execute()\n    }\n  }\n\n  test(\"pre-resolved exprs: should work in all expressions in absence of duplicate refs\") {\n    withTempDir { dir =>\n      val location = dir.getAbsolutePath\n      Seq((1, 1), (2, 2)).toDF(\"key\", \"value\").write\n        .format(\"delta\")\n        .save(location)\n      val table = io.delta.tables.DeltaTable.forPath(spark, location)\n      val target = table.toDF\n      val source = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n\n      table.merge(source, target(\"key\") === source(\"key\"))\n        .whenMatched(target(\"key\") === lit(1) && source(\"value\") === lit(10))\n        .update(Map(\"value\" -> (target(\"value\") + source(\"value\"))))\n        .whenMatched(target(\"key\") === lit(2) && source(\"value\") === lit(20))\n        .delete()\n        .whenNotMatched(source(\"key\") === lit(3) && source(\"value\") === lit(30))\n        .insert(Map(\"key\" -> source(\"key\"), \"value\" -> source(\"value\")))\n        .execute()\n\n      checkAnswer(table.toDF, Seq((1, 11), (3, 30)).toDF(\"key\", \"value\"))\n    }\n  }\n\n  test(\"pre-resolved exprs: negative cases with refs resolved to wrong Dataframes\") {\n    withTempDir { dir =>\n      val location = dir.getAbsolutePath\n      Seq((1, 1), (2, 2)).toDF(\"key\", \"value\").write\n        .format(\"delta\")\n        .save(location)\n      val table = io.delta.tables.DeltaTable.forPath(spark, location)\n      val target = table.toDF\n      val source = Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\")\n      val dummyDF = Seq((0, 0)).toDF(\"key\", \"value\")\n\n      def checkError(f: => Unit): Unit = {\n        val e = intercept[AnalysisException] { f }\n        Seq(\"Resolved attribute\", \"missing from\").foreach { m =>\n          assert(e.getMessage.toLowerCase(Locale.ROOT).contains(m.toLowerCase(Locale.ROOT)))\n        }\n      }\n      // Merge condition\n      checkError {\n        table.merge(source, target(\"key\") === dummyDF(\"key\"))\n          .whenMatched().delete().execute()\n      }\n\n      // Matched clauses\n      checkError {\n        table.merge(source, target(\"key\") === source(\"key\"))\n          .whenMatched(dummyDF(\"key\") === lit(1)).updateAll().execute()\n      }\n\n      checkError {\n        table.merge(source, target(\"key\") === source(\"key\"))\n          .whenMatched().update(Map(\"key\" -> dummyDF(\"key\"))).execute()\n      }\n\n      // Not matched clauses\n      checkError {\n        table.merge(source, target(\"key\") === source(\"key\"))\n          .whenNotMatched(dummyDF(\"key\") === lit(1)).insertAll().execute()\n      }\n      checkError {\n        table.merge(source, target(\"key\") === source(\"key\"))\n          .whenNotMatched().insert(Map(\"key\" -> dummyDF(\"key\"))).execute()\n      }\n    }\n  }\n\n  /** Make sure the joins generated by merge do not have the duplicate AttributeReferences */\n  private def verifyNoDuplicateRefsAcrossSourceAndTarget(f: => Unit): Unit = {\n    val executedPlans = DeltaTestUtils.withLogicalPlansCaptured(spark, optimizedPlan = true) { f }\n    val plansWithInnerJoin = executedPlans.filter { p =>\n      p.collect { case b: Join if b.joinType == Inner => b }.nonEmpty\n    }\n    assert(plansWithInnerJoin.size == 1,\n      \"multiple plans found with inner join\\n\" + plansWithInnerJoin.mkString(\"\\n\"))\n    val join = plansWithInnerJoin.head.collect { case j: Join => j }.head\n    assert(join.left.outputSet.intersect(join.right.outputSet).isEmpty)\n  }\n\n  test(\"self-merge: duplicate attrib refs should be removed\") {\n    withTempDir { tempDir =>\n      val df = spark.range(5).selectExpr(\"id as key\", \"id as value\")\n      df.write.format(\"delta\").save(tempDir.toString)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.toString)\n      val target = deltaTable.toDF\n      val source = target.filter(\"key = 4\")\n\n      val duplicateRefs =\n        target.queryExecution.analyzed.outputSet.intersect(source.queryExecution.analyzed.outputSet)\n      require(duplicateRefs.nonEmpty, \"source and target were expected to have duplicate refs\")\n\n      verifyNoDuplicateRefsAcrossSourceAndTarget {\n        deltaTable.as(\"t\")\n          .merge(source.as(\"s\"), \"t.key = s.key\")\n          .whenMatched()\n          .delete()\n          .execute()\n      }\n      checkAnswer(deltaTable.toDF, spark.range(4).selectExpr(\"id as key\", \"id as value\"))\n    }\n  }\n\n  test(\n    \"self-merge + pre-resolved exprs: merge condition fails with pre-resolved, duplicate refs\") {\n    withTempDir { tempDir =>\n      val df = spark.range(5).selectExpr(\"id as key\", \"id as value\")\n      df.write.format(\"delta\").save(tempDir.toString)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.toString)\n      val target = deltaTable.toDF\n      val source = target.filter(\"key = 4\")\n      val e = intercept[AnalysisException] {\n        deltaTable.merge(source, target(\"key\") === source(\"key\"))  // this is ambiguous\n          .whenMatched()\n          .delete()\n          .execute()\n      }\n      assert(e.getMessage.toLowerCase(Locale.ROOT).contains(\"ambiguous\"))\n    }\n  }\n\n  test(\n    \"self-merge + pre-resolved exprs: duplicate refs should resolve in not-matched clauses\") {\n    withTempDir { tempDir =>\n      val df = spark.range(5).selectExpr(\"id as key\", \"id as value\")\n      df.write.format(\"delta\").save(tempDir.toString)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.toString)\n      val target = deltaTable.toDF\n      val source = target.filter(\"key = 4\")\n\n      // Insert clause can refer to only source attributes, so pre-resolved references,\n      // even when written as`target(\"column\")`, are actually unambiguous\n      verifyNoDuplicateRefsAcrossSourceAndTarget {\n        deltaTable.as(\"t\")\n          .merge(source.as(\"s\"), \"t.key = s.key\")\n          .whenNotMatched(source(\"value\") > 0 && target(\"key\") > 0)\n          .insert(Map(\"key\" -> source(\"key\"), \"value\" -> target(\"value\")))\n          .whenMatched().update(Map(\"key\" -> $\"s.key\")) // no-op\n          .execute()\n      }\n      // nothing should be inserted as source matches completely with target\n      checkAnswer(deltaTable.toDF, spark.range(5).selectExpr(\"id as key\", \"id as value\"))\n    }\n  }\n\n  test(\n    \"self-merge + pre-resolved exprs: non-duplicate but pre-resolved refs should still resolve\") {\n    withTempDir { tempDir =>\n      val df = spark.range(5).selectExpr(\"id as key\", \"id as value\")\n      df.write.format(\"delta\").save(tempDir.toString)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.toString)\n      val target = deltaTable.toDF\n      val source = target.filter(\"key = 0\").drop(\"value\")\n        .withColumn(\"value\", col(\"key\") + lit(0))\n        .withColumn(\"other\", lit(0))\n      // source is just one row (key, value, other) = (4, 4, 0)\n\n      // `value` should not be duplicate ref as its recreated in the source and have different\n      // exprIds than the target value.\n      val duplicateRefs =\n        target.queryExecution.analyzed.outputSet.intersect(source.queryExecution.analyzed.outputSet)\n      require(duplicateRefs.map(_.name).toSet == Set(\"key\"),\n        \"unexpected duplicate refs, should be only 'key': \" + duplicateRefs)\n\n      // So both `source(\"value\")` and `target(\"value\")` are not ambiguous.\n      // `source(\"other\")` is obviously not ambiguous.\n      verifyNoDuplicateRefsAcrossSourceAndTarget {\n        deltaTable.as(\"t\")\n          .merge(\n            source.as(\"s\"),\n            expr(\"t.key = s.key\") && source(\"other\") === 0 && target(\"value\") === 4)\n          .whenMatched(source(\"value\") > 0 && target(\"value\") > 0 && source(\"other\") === 0)\n          .update(Map(\n            \"key\" -> expr(\"s.key\"),\n            \"value\" -> (target(\"value\") + source(\"value\") + source(\"other\"))))\n          .whenNotMatched(source(\"value\") > 0 && source(\"other\") === 0)\n          .insert(Map(\n            \"key\" -> expr(\"s.key\"),\n            \"value\" -> (source(\"value\") + source(\"other\"))))\n          .execute()\n      }\n      // key = 4 should be updated to same values, and nothing should be inserted\n      checkAnswer(deltaTable.toDF, spark.range(5).selectExpr(\"id as key\", \"id as value\"))\n    }\n  }\n\n  test(\"self-merge + pre-resolved exprs: negative cases in matched clauses with duplicate refs\") {\n    // Only matched clauses can have attribute references from both source and target, hence\n    // pre-resolved expression can be ambiguous in presence of duplicate references from self-merge\n    withTempDir { tempDir =>\n      val df = spark.range(5).selectExpr(\"id as key\", \"id as value\")\n      df.write.format(\"delta\").save(tempDir.toString)\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(tempDir.toString)\n      val target = deltaTable.toDF\n      val source = target.filter(\"key = 4\")\n\n      def checkError(f: => Unit): Unit = {\n        val e = intercept[AnalysisException] { f }\n        assert(e.getMessage.toLowerCase(Locale.ROOT).contains(\"ambiguous\"))\n      }\n\n      checkError {\n        deltaTable\n          .merge(source, target(\"key\") === source(\"key\"))  // this is ambiguous\n          .whenMatched()\n          .delete()\n          .execute()\n      }\n\n      // Update\n      checkError {\n        deltaTable.as(\"t\").merge(source.as(\"s\"), \"t.key = s.key\")\n          .whenMatched(target(\"key\") === functions.lit(4))  // can map to either key column\n          .updateAll()\n          .execute()\n      }\n\n      checkError {\n        deltaTable.as(\"t\").merge(source.as(\"s\"), \"t.key = s.key\")\n          .whenMatched()\n          .update(Map(\"value\" -> target(\"value\").plus(1)))  // can map to either value column\n          .execute()\n      }\n\n      // Delete\n      checkError {\n        deltaTable.as(\"t\").merge(source.as(\"s\"), \"t.key = s.key\")\n          .whenMatched(target(\"key\") === functions.lit(4))  // can map to either key column\n          .delete()\n          .execute()\n      }\n    }\n  }\n\n  test(\"merge clause matched and not matched can interleave\") {\n    append(Seq((1, 10), (2, 20)).toDF(\"trgKey\", \"trgValue\"), Nil) // target\n    val source = Seq((1, 100), (2, 200), (3, 300), (4, 400)).toDF(\"srcKey\", \"srcValue\") // source\n    io.delta.tables.DeltaTable.forPath(spark, tempPath)\n      .merge(source, \"srcKey = trgKey\")\n      .whenMatched(\"trgKey = 1\").updateExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n      .whenNotMatched(\"srcKey = 3\").insertExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n      .whenMatched().delete()\n      .whenNotMatched().insertExpr(Map(\"trgKey\" -> \"srcKey\", \"trgValue\" -> \"srcValue\"))\n      .execute()\n\n    checkAnswer(\n      readDeltaTable(tempPath),\n      Row(1, 100) ::  // Update (1, 10)\n                      // Delete (2, 20)\n      Row(3, 300) ::  // Insert (3, 300)\n      Row(4, 400) ::  // Insert (4, 400)\n      Nil)\n  }\n\n  test(\"schema evolution with multiple update clauses\") {\n    withSQLConf((\"spark.databricks.delta.schema.autoMerge.enabled\", \"true\")) {\n      withTable(\"target\", \"src\") {\n        Seq((1, \"a\"), (2, \"b\"), (3, \"c\")).toDF(\"id\", \"targetValue\")\n          .write.format(\"delta\").saveAsTable(\"target\")\n        val source = Seq((1, \"x\"), (2, \"y\"), (4, \"z\")).toDF(\"id\", \"srcValue\")\n\n        io.delta.tables.DeltaTable.forName(\"target\")\n          .merge(source, col(\"target.id\") === source.col(\"id\"))\n          .whenMatched(\"target.id = 1\").updateExpr(Map(\"targetValue\" -> \"srcValue\"))\n          .whenMatched(\"target.id = 2\").updateAll()\n          .whenNotMatched().insertAll()\n          .execute()\n        checkAnswer(\n          sql(\"select * from target\"),\n          Row(1, \"x\", null) +: Row(2, \"b\", \"y\") +: Row(3, \"c\", null) +: Row(4, null, \"z\") +: Nil)\n      }\n    }\n  }\n\n\n  /* Exclude tempViews, because DeltaTable.forName does not resolve them correctly, so no one can\n   * use them anyway with the Scala API.\n\n  // Scala API won't hit the resolution exception.\n  testWithTempView(\"Update specific column works fine in temp views\") { isSQLTempView =>\n    withJsonData(\n      \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 } } }\"\"\",\n      \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 2, \"y\": 1 } } }\"\"\"\n    ) { (sourceName, targetName) =>\n      createTempViewFromTable(targetName, isSQLTempView)\n      val fieldNames = spark.table(targetName).schema.fieldNames\n      val fieldNamesStr = fieldNames.mkString(\"`\", \"`, `\", \"`\")\n      executeMerge(\n        target = \"v t\",\n        source = s\"$sourceName s\",\n        condition = \"s.key = t.key\",\n        update = \"value.a.x = s.value.a.x\",\n        insert = s\"($fieldNamesStr) VALUES ($fieldNamesStr)\")\n      checkAnswer(\n        spark.read.format(\"delta\").table(\"v\"),\n        spark.read.json(\n          strToJsonSeq(\"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 1 } } }\"\"\").toDS)\n      )\n    }\n  }\n   */\n\n  test(\"delta merge into clause with invalid data type.\") {\n    import org.apache.spark.sql.catalyst.dsl.expressions._\n    intercept[DeltaAnalysisException] {\n      DeltaMergeIntoClause.toActions(Seq(Assignment(\"1\".expr, \"1\".expr)))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoSchemaEvolutionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions.{array, lit, struct}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{ArrayType, DateType, IntegerType, LongType, MapType, NullType, StringType, StructField, StructType}\nimport org.apache.spark.util.Utils\n\n/**\n * Trait collecting schema evolution test runner methods and other helpers.\n */\ntrait MergeIntoSchemaEvolutionMixin extends QueryTest {\n  self: SharedSparkSession with MergeIntoTestUtils =>\n\n  protected implicit def strToJsonSeq(str: String): Seq[String] = {\n    str.split(\"\\n\").filter(_.trim.length > 0)\n  }\n\n  /**\n   * Helper method similar to [[testEvolution()]] but without aliasing the target and source tables\n   * as 't' and 's'. Used to check that attribute resolution works correctly with schema evolution\n   * when using column name qualified with the actual table name: `table_name.column`.\n   */\n  def testEvolutionWithoutTableAliases(name: String)(\n      targetData: => DataFrame,\n      sourceData: => DataFrame,\n      clauses: MergeClause*)(\n      expected: => Seq[Row] = Seq.empty,\n      expectErrorContains: String = null,\n      expectErrorWithoutEvolutionContains: String = null): Unit =\n    for (schemaEvolutionEnabled <- BOOLEAN_DOMAIN)\n    test(s\"schema evolution - $name - schemaEvolutionEnabled= $schemaEvolutionEnabled\") {\n      withTable(\"target\", \"source\") {\n        targetData.write.format(\"delta\").saveAsTable(\"target\")\n        sourceData.write.format(\"delta\").saveAsTable(\"source\")\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> schemaEvolutionEnabled.toString) {\n          if (!schemaEvolutionEnabled && expectErrorWithoutEvolutionContains != null) {\n            val ex = intercept[AnalysisException] {\n              executeMerge(tgt = \"target\", src = \"source\", cond = \"1 = 1\", clauses: _*)\n            }\n            errorContains(ex.getMessage, expectErrorWithoutEvolutionContains)\n          } else if (schemaEvolutionEnabled && expectErrorContains != null) {\n            val ex = intercept[AnalysisException] {\n              executeMerge(tgt = \"target\", src = \"source\", cond = \"1 = 1\", clauses: _*)\n            }\n            errorContains(ex.getMessage, expectErrorContains)\n          } else {\n            executeMerge(tgt = \"target\", src = \"source\", cond = \"1 = 1\", clauses: _*)\n            checkAnswer(spark.read.table(\"target\"), expected)\n          }\n        }\n      }\n    }\n\n  /**\n   * Test runner used by most non-nested schema evolution tests. Runs the MERGE operation once with\n   * schema evolution disabled then with schema evolution enabled. Tests must provide for each case\n   * either the expected result or the expected error message but not both.\n   */\n  // scalastyle:off argcount\n  protected def testEvolution(name: String)(\n      targetData: => DataFrame,\n      sourceData: => DataFrame,\n      cond: String = \"t.key = s.key\",\n      clauses: Seq[MergeClause] = Seq.empty,\n      expected: => DataFrame = null,\n      expectedWithoutEvolution: => DataFrame = null,\n      expectedSchema: StructType = null,\n      expectedSchemaWithoutEvolution: StructType = null,\n      expectErrorContains: String = null,\n      expectErrorWithoutEvolutionContains: String = null,\n      confs: Seq[(String, String)] = Seq(),\n      partitionCols: Seq[String] = Seq.empty): Unit = {\n\n    def executeMergeAndAssert(df: DataFrame, schema: StructType, error: String): Unit = {\n      append(targetData, partitionCols)\n      withTempView(\"source\") {\n        sourceData.createOrReplaceTempView(\"source\")\n\n        if (error != null) {\n          val ex = intercept[AnalysisException] {\n            executeMerge(s\"$tableSQLIdentifier t\", \"source s\", cond, clauses: _*)\n          }\n          errorContains(Utils.exceptionString(ex), error)\n        } else {\n          executeMerge(s\"$tableSQLIdentifier t\", \"source s\", cond, clauses: _*)\n          checkAnswer(readDeltaTableByIdentifier(), df.collect())\n          if (schema != null) {\n            assert(readDeltaTableByIdentifier().schema === schema)\n          } else {\n            // Check against the schema of the expected result df if no explicit schema was\n            // provided. Nullability of fields will vary depending on the actual data in the df so\n            // we ignore it.\n            assert(readDeltaTableByIdentifier().schema.asNullable ===\n              df.schema.asNullable)\n          }\n        }\n      }\n    }\n\n    test(s\"schema evolution - $name - with evolution disabled\") {\n      withSQLConf(confs :+ (DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"false\"): _*) {\n        executeMergeAndAssert(expectedWithoutEvolution, expectedSchemaWithoutEvolution,\n          expectErrorWithoutEvolutionContains)\n      }\n    }\n\n    test(s\"schema evolution - $name\") {\n      withSQLConf((confs :+ (DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\")): _*) {\n        executeMergeAndAssert(expected, expectedSchema, expectErrorContains)\n      }\n    }\n  }\n  // scalastyle:on argcount\n\n   /**\n   * Test runner used by most nested schema evolution tests. Similar to `testEvolution()` except\n   * that the target & source data and expected results are parsed as JSON strings for convenience.\n   */\n  // scalastyle:off argcount\n  protected def testNestedStructsEvolution(name: String)(\n      target: Seq[String],\n      source: Seq[String],\n      targetSchema: StructType,\n      sourceSchema: StructType,\n      cond: String = \"t.key = s.key\",\n      clauses: Seq[MergeClause] = Seq.empty,\n      result: Seq[String] = null,\n      resultSchema: StructType = null,\n      resultWithoutEvolution: Seq[String] = null,\n      expectErrorContains: String = null,\n      expectErrorWithoutEvolutionContains: String = null,\n      confs: Seq[(String, String)] = Seq()): Unit = {\n    testEvolution(name) (\n      targetData = readFromJSON(target, targetSchema),\n      sourceData = readFromJSON(source, sourceSchema),\n      cond,\n      clauses = clauses,\n      expected =\n        if (result != null ) {\n          val schema = if (resultSchema != null) resultSchema else targetSchema\n          readFromJSON(result, schema)\n        } else {\n          null\n        },\n      expectedSchema = resultSchema,\n      expectErrorContains = expectErrorContains,\n      expectedWithoutEvolution =\n        if (resultWithoutEvolution != null) {\n          readFromJSON(resultWithoutEvolution, targetSchema)\n        } else {\n          null\n        },\n      expectedSchemaWithoutEvolution = targetSchema,\n      expectErrorWithoutEvolutionContains = expectErrorWithoutEvolutionContains,\n      confs = confs\n    )\n  }\n  // scalastyle:on argcount\n}\n\n/**\n * Trait collecting a subset of tests providing core coverage for schema evolution. Mix this trait\n * in other suites to get basic test coverage for schema evolution in combination with other\n * features, e.g. CDF, DVs.\n */\ntrait MergeIntoSchemaEvolutionCoreTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import testImplicits._\n\n  testEvolution(\"new column with only insert *\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(\"*\") :: Nil,\n    expected =\n      ((0, 0, null) +: (3, 30, null) +: // unchanged\n        (1, 10, null) +:  // not updated\n        (2, 2, \"extra2\") +: Nil // newly inserted\n      ).toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution =\n      ((0, 0) +: (3, 30) +: (1, 10) +: (2, 2) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"new column with only update *\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"*\") :: Nil,\n    expected =\n      ((0, 0, null) +: (3, 30, null) +:\n        (1, 1, \"extra1\") +: // updated\n        Nil // row 2 not inserted\n      ).toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = ((0, 0) +: (3, 30) +: (1, 1) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"new column with insert * and delete not matched by source\")(\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    clauses = insert(\"*\") ::\n      deleteNotMatched() :: Nil,\n    expected = Seq(\n      // (0, 0) Not matched by source, deleted\n      (1, 10, null), // Matched, updated\n      (2, 2, \"extra2\") // Not matched by target, inserted\n      // (3, 30) Not matched by source, deleted\n    ).toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = Seq((1, 10), (2, 2)).toDF(\"key\", \"value\"))\n\n  testNestedStructsEvolution(\"new nested source field added when updating top-level column\")(\n    target = Seq(\"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\"),\n    source = Seq(\"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2, \"b\": 3 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    clauses = update(\"value = s.value\") :: Nil,\n    result = Seq(\"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2, \"b\": 3 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n}\n\n/**\n * Trait collecting all base and new column tests for schema evolution.\n */\ntrait MergeIntoSchemaEvolutionBaseNewColumnTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils\n    with SharedSparkSession =>\n\n  import testImplicits._\n\n  // Schema evolution with UPDATE SET alone\n  testEvolution(\"new column with update set\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(set = \"key = s.key, value = s.value, extra = s.extra\") :: Nil,\n    expected = ((0, 0, null) +: (3, 30, null) +: (1, 1, \"extra1\") +: Nil)\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\")\n\n  testEvolution(\"new column updated with value from existing column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, -1), (2, 2, -2))\n      .toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(set = \"extra = s.value\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, 1) +: (3, 30, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\")\n\n  // Schema evolution with INSERT alone\n  testEvolution(\"new column with insert values\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(values = \"(key, value, extra) VALUES (s.key, s.value, s.extra)\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, null) +: (3, 30, null) +: (2, 2, \"extra2\") +: Nil)\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in INSERT clause\")\n\n   testEvolution(\"new column inserted with value from existing column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, -1), (2, 2, -2))\n      .toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(values = \"(key, extra) VALUES (s.key, s.value)\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, null) +: (3, 30, null) +: (2, null, 2) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in INSERT clause\")\n\n  // Schema evolution (UPDATE) with two new columns in the source but only one added to the target.\n  testEvolution(\"new column with update set and column not updated\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\", \"unused1\"), (2, 2, \"extra2\", \"unused2\"))\n      .toDF(\"key\", \"value\", \"extra\", \"unused\"),\n    clauses = update(set = \"extra = s.extra\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, \"extra1\") +: (3, 30, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, String)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\")\n\n  testEvolution(\"new column updated from other new column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\", \"unused1\"), (2, 2, \"extra2\", \"unused2\"))\n      .toDF(\"key\", \"value\", \"extra\", \"unused\"),\n    clauses = update(set = \"extra = s.unused\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, \"unused1\") +: (3, 30, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, String)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\")\n\n  // Schema evolution (INSERT) with two new columns in the source but only one added to the target.\n  testEvolution(\"new column with insert values and column not inserted\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\", \"unused1\"), (2, 2, \"extra2\", \"unused2\"))\n      .toDF(\"key\", \"value\", \"extra\", \"unused\"),\n    clauses = insert(values = \"(key, extra) VALUES (s.key, s.extra)\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, null) +: (3, 30, null) +: (2, null, \"extra2\") +: Nil)\n      .asInstanceOf[List[(Integer, Integer, String)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in INSERT clause\")\n\n  testEvolution(\"new column inserted from other new column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\", \"unused1\"), (2, 2, \"extra2\", \"unused2\"))\n      .toDF(\"key\", \"value\", \"extra\", \"unused\"),\n    clauses = insert(values = \"(key, extra) VALUES (s.key, s.unused)\") :: Nil,\n    expected = ((0, 0, null) +: (1, 10, null) +: (3, 30, null) +: (2, null, \"unused2\") +: Nil)\n      .asInstanceOf[List[(Integer, Integer, String)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in INSERT clause\")\n\n  // Schema evolution with two new columns added by UPDATE and INSERT resp.\n  testEvolution(\"new column added by insert and other new column added by update\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\", \"other1\"), (2, 2, \"extra2\", \"other2\"))\n      .toDF(\"key\", \"value\", \"extra\", \"other\"),\n    clauses = update(set = \"extra = s.extra\") ::\n      insert(values = \"(key, other) VALUES (s.key, s.other)\") :: Nil,\n    expected =\n      ((0, 0, null, null) +:\n       (1, 10, \"extra1\", null) +:\n       (3, 30, null, null) +:\n       (2, null, null, \"other2\") +: Nil)\n      .asInstanceOf[List[(Integer, Integer, String, String)]]\n      .toDF(\"key\", \"value\", \"extra\", \"other\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\")\n\n  testEvolution(\"new column with insert existing column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(values = \"(key) VALUES (s.key)\") :: Nil,\n    expected = ((0, 0) +: (1, 10) +: (2, null) +: (3, 30) +: Nil)\n      .asInstanceOf[List[(Integer, Integer)]]\n      .toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 10) +: (2, null) +: (3, 30) +: Nil)\n      .asInstanceOf[List[(Integer, Integer)]]\n      .toDF(\"key\", \"value\"))\n\n  testEvolution(\"new column with update set and update *\")(\n    targetData = Seq((0, 0), (1, 10), (2, 20)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(condition = \"s.key < 2\", set = \"value = s.value\") :: update(\"*\") :: Nil,\n    expected =\n      ((0, 0, null) +:\n        (1, 1, null) +: // updated by first clause\n        (2, 2, \"extra2\") +: // updated by second clause\n        Nil\n      ).toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 1) +: (2, 2) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"new column with update non-* and insert *\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, 1), (2, 2, 2)).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"key = s.key, value = s.value\") :: insert(\"*\") :: Nil,\n    expected = ((0, 0, null) +: (2, 2, 2) +: (3, 30, null) +:\n      // null because `extra` isn't an update action, even though it's 1 in the source data\n      (1, 1, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]].toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = ((0, 0) +: (2, 2) +: (3, 30) +: (1, 1) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"new column with update * and insert non-*\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, 1), (2, 2, 2)).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"*\") :: insert(\"(key, value) VALUES (s.key, s.value)\") :: Nil,\n    expected = ((0, 0, null) +: (1, 1, 1) +: (3, 30, null) +:\n      // null because `extra` isn't an insert action, even though it's 2 in the source data\n      (2, 2, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]].toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = ((0, 0) +: (2, 2) +: (3, 30) +: (1, 1) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"evolve partitioned table\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((0, 0, null) +: (1, 1, \"extra1\") +: (2, 2, \"extra2\") +: (3, 30, null) +: Nil)\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = ((0, 0) +: (2, 2) +: (3, 30) +: (1, 1) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"star expansion with names including dots\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value.with.dotted.name\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\n      \"key\", \"value.with.dotted.name\", \"extra.dotted\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((0, 0, null) +: (1, 1, \"extra1\") +: (2, 2, \"extra2\") +: (3, 30, null) +: Nil)\n      .toDF(\"key\", \"value.with.dotted.name\", \"extra.dotted\"),\n    expectedWithoutEvolution = ((0, 0) +: (2, 2) +: (3, 30) +: (1, 1) +: Nil)\n      .toDF(\"key\", \"value.with.dotted.name\")\n  )\n\n  testEvolution(\"extra nested column in source - insert\")(\n    targetData = Seq((1, (1, 10))).toDF(\"key\", \"x\"),\n    sourceData = Seq((2, (2, 20, 30))).toDF(\"key\", \"x\"),\n    clauses = insert(\"*\") :: Nil,\n    expected = ((1, (1, 10, null)) +: (2, (2, 20, 30)) +: Nil)\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  testEvolution(\"add non-nullable column to target schema\")(\n    targetData = Seq(1, 2).toDF(\"key\"),\n    sourceData = Seq((1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((1, 10) :: (2, null) :: (3, 30) :: Nil)\n      .asInstanceOf[List[(Integer, Integer)]].toDF(\"key\", \"value\"),\n    expectedSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", IntegerType, nullable = true),\n    expectedWithoutEvolution = Seq(1, 2, 3).toDF(\"key\")\n  )\n\n  testEvolution(\"extra nested column in source - update - single target partition\")(\n    targetData = Seq((1, (1, 10)), (2, (2, 2000))).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'c', x._2) as x\").repartition(1),\n    sourceData = Seq((1, (10, 100, 1000))).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    clauses = update(\"*\") :: Nil,\n    expected = ((1, (10, 100, 1000)) +: (2, (2, null, 2000)) +: Nil)\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'c', x._3, 'b', x._2) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  testEvolution(\"multiple clauses\")(\n    // 1 and 2 should be updated from the source, 3 and 4 should be deleted. Only 5 is unchanged\n    targetData = Seq((1, \"a\"), (2, \"b\"), (3, \"c\"), (4, \"d\"), (5, \"e\")).toDF(\"key\", \"targetVal\"),\n    // 1 and 2 should be updated into the target, 6 and 7 should be inserted. 8 should be ignored\n    sourceData = Seq((1, \"t\"), (2, \"u\"), (3, \"v\"), (4, \"w\"), (6, \"x\"), (7, \"y\"), (8, \"z\"))\n      .toDF(\"key\", \"srcVal\"),\n    clauses =\n      update(\"targetVal = srcVal\", \"s.key = 1\") :: update(\"*\", \"s.key = 2\") ::\n      delete(\"s.key = 3\") :: delete(\"s.key = 4\") ::\n      insert(\"(key) VALUES (s.key)\", \"s.key = 6\") :: insert(\"*\", \"s.key = 7\") :: Nil,\n    expected =\n      ((1, \"t\", null) :: (2, \"b\", \"u\") :: (5, \"e\", null) ::\n        (6, null, null) :: (7, null, \"y\") :: Nil)\n        .asInstanceOf[List[(Integer, String, String)]].toDF(\"key\", \"targetVal\", \"srcVal\"),\n    // The UPDATE * clause won't resolve without evolution because the source and target columns\n    // don't match.\n    expectErrorWithoutEvolutionContains = \"cannot resolve targetVal\"\n  )\n\n  testEvolution(\"multiple INSERT * clauses with UPDATE\")(\n    // 1 and 2 should be updated from the source, 3 and 4 should be deleted. Only 5 is unchanged\n    targetData = Seq((1, \"a\"), (2, \"b\"), (3, \"c\"), (4, \"d\"), (5, \"e\")).toDF(\"key\", \"targetVal\"),\n    // 1 and 2 should be updated into the target, 6 and 7 should be inserted. 8 should be ignored\n    sourceData = Seq((1, \"t\"), (2, \"u\"), (3, \"v\"), (4, \"w\"), (6, \"x\"), (7, \"y\"), (8, \"z\"))\n      .toDF(\"key\", \"srcVal\"),\n    clauses =\n      update(\"targetVal = srcVal\", \"s.key = 1\") :: update(\"*\", \"s.key = 2\") ::\n      delete(\"s.key = 3\") :: delete(\"s.key = 4\") ::\n      insert(\"*\", \"s.key = 6\") :: insert(\"*\", \"s.key = 7\") :: Nil,\n    expected =\n      ((1, \"t\", null) :: (2, \"b\", \"u\") :: (5, \"e\", null) ::\n        (6, null, \"x\") :: (7, null, \"y\") :: Nil)\n        .asInstanceOf[List[(Integer, String, String)]].toDF(\"key\", \"targetVal\", \"srcVal\"),\n    // The UPDATE * clause won't resolve without evolution because the source and target columns\n    // don't match.\n    expectErrorWithoutEvolutionContains = \"cannot resolve targetVal\"\n  )\n\n  testEvolution(\"array of struct should work with containsNull as false\")(\n    targetData = Seq(500000).toDF(\"key\"),\n    sourceData = Seq(500000, 100000).toDF(\"key\")\n      .withColumn(\"generalDeduction\",\n        struct(\n          lit(\"2024-11-08\").cast(DateType).as(\"date\"),\n          array(struct(lit(0d).as(\"data\"))))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq(500000, 100000).toDF(\"key\")\n      .withColumn(\"generalDeduction\",\n        struct(\n          lit(\"2024-11-08\").cast(DateType).as(\"date\"),\n          array(struct(lit(0d).as(\"data\"))))),\n    expectedWithoutEvolution = Seq(500000, 100000).toDF(\"key\")\n  )\n\n  testEvolution(\"test array_union with schema evolution\")(\n    targetData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2010 to 2019).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i\").as(\"location\")\n            )\n          }: _*)),\n    sourceData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2020 to 8020).map { i =>\n            struct(\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\")\n            )\n          }: _*)),\n    clauses = update(set = \"openings = array_union(s.openings, s.openings)\") :: insert(\"*\") :: Nil,\n    expected = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2020 to 8020).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(null).cast(StringType).as(\"location\")\n            )\n          }: _*)),\n    expectErrorWithoutEvolutionContains = \"All nested columns must match\"\n  )\n\n  testEvolution(\"test array_intersect with schema evolution\")(\n    targetData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2010 to 2019).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i\").as(\"location\")\n            )\n          }: _*)),\n    sourceData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2020 to 8020).map { i =>\n            struct(\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\")\n            )\n          }: _*)),\n    clauses =\n      update(set = \"openings = array_intersect(s.openings, s.openings)\") :: insert(\"*\") :: Nil,\n    expected = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2020 to 8020).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(null).cast(StringType).as(\"location\")\n            )\n          }: _*)),\n    expectErrorWithoutEvolutionContains = \"All nested columns must match\"\n  )\n\n  testEvolution(\"test array_except with schema evolution\")(\n    targetData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2010 to 2020).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i\").as(\"location\")\n            )\n          }: _*)),\n    sourceData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2020 to 8020).map { i =>\n            struct(\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\")\n            )\n          }: _*)),\n    clauses =\n      update(set = \"openings = array_except(s.openings, s.openings)\") :: insert(\"*\") :: Nil,\n    expected = Seq(1).toDF(\"key\")\n      .withColumn(\n        \"openings\",\n        array().cast(\n          new ArrayType(\n            new StructType()\n              .add(\"opened_at\", StringType)\n              .add(\"opened_with\", StringType)\n              .add(\"location\", StringType),\n            true\n          )\n        )\n      ),\n    expectErrorWithoutEvolutionContains = \"All nested columns must match\"\n  )\n\n  testEvolution(\"test array_remove with schema evolution\")(\n    targetData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2010 to 2019).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i\").as(\"location\")\n            )\n          }: _*)),\n    sourceData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2020 to 8020).map { i =>\n            struct(\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\")\n            )\n          }: _*)),\n    clauses = update(\n      set = \"openings = array_remove(s.openings,\" +\n        \"named_struct('opened_with', cast(null as string),\" +\n        \"'opened_at', '2020-01-19T09:29:00.000+0000'))\") :: insert(\"*\") :: Nil,\n    expected = Seq(1).toDF(\"key\")\n      .withColumn(\n        \"openings\",\n        array((2021 to 8020).map { i =>\n          struct(\n            lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n            lit(null).cast(StringType).as(\"opened_with\"),\n            lit(null).cast(StringType).as(\"location\")\n          )\n        }: _*)),\n    expectErrorWithoutEvolutionContains = \"All nested columns must match\"\n  )\n\n  testEvolution(\"test array_distinct with schema evolution\")(\n    targetData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          (2010 to 2019).map { i =>\n            struct(\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i\").as(\"location\")\n            )\n          }: _*\n        )),\n    sourceData = Seq(1).toDF(\"key\")\n      .withColumn(\"openings\",\n        array(\n          ((2020 to 8020) ++ (2020 to 8020)).map { i =>\n            struct(\n              lit(null).cast(StringType).as(\"opened_with\"),\n              lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\")\n            )\n          }: _*\n        )),\n    clauses = update(set = \"openings = array_distinct(s.openings)\") :: insert(\"*\") :: Nil,\n    expected = Seq(1).toDF(\"key\")\n      .withColumn(\n        \"openings\",\n        array((2020 to 8020).map { i =>\n          struct(\n            lit(s\"$i-01-19T09:29:00.000+0000\").as(\"opened_at\"),\n            lit(null).cast(StringType).as(\"opened_with\"),\n            lit(null).cast(StringType).as(\"location\")\n          )\n        }: _*)),\n    expectErrorWithoutEvolutionContains = \"All nested columns must match\"\n  )\n\n  testEvolution(\"void columns are not allowed\")(\n    targetData = Seq((1, 1)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 100, null), (2, 200, null)).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"Cannot add column `extra` with type VOID\",\n    expectedWithoutEvolution = Seq((1, 100), (2, 200)).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"top-level column assignment qualified with source alias\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(set = \"s.value = s.value\") :: Nil,\n    // Assigning to the source is just wrong and should fail.\n    expected = ((0, 0) +: (3, 30) +: (1, 1) +: Nil)\n      .toDF(\"key\", \"value\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve s.value in UPDATE clause\")\n\n  test(\"schema evolution enabled for the current command\") {\n    withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"false\") {\n      withTable(\"target\", \"source\") {\n        Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\")\n          .write.format(\"delta\").saveAsTable(\"target\")\n        Seq((1, 1, 1), (2, 2, 2)).toDF(\"key\", \"value\", \"extra\")\n          .write.format(\"delta\").saveAsTable(\"source\")\n\n        // Should fail without schema evolution\n        val e = intercept[org.apache.spark.sql.AnalysisException] {\n          executeMerge(\n            \"target\",\n            \"source\",\n            \"target.key = source.key\",\n            update(\"extra = -1\"), insert(\"*\"))\n        }\n        assert(e.getErrorClass === \"DELTA_MERGE_UNRESOLVED_EXPRESSION\")\n        assert(e.getMessage.contains(\"resolve extra in UPDATE clause\"))\n\n        // Should succeed with schema evolution\n        executeMergeWithSchemaEvolution(\n          \"target\",\n          \"source\",\n          \"target.key = source.key\",\n          update(\"extra = -1\"), insert(\"*\"))\n        checkAnswer(\n          spark.table(\"target\"),\n          Seq[(Integer, Integer, Integer)]((0, 0, null), (1, 10, -1), (2, 2, 2), (3, 30, null))\n            .toDF(\"key\", \"value\", \"extra\"))\n      }\n    }\n  }\n\n  testEvolutionWithoutTableAliases(\"new top-level column assignment qualified with target name\")(\n    targetData = Seq((0, 1)).toDF(\"a\", \"nested_a\")\n        .selectExpr(\"a\", \"named_struct('a', nested_a) as target\"),\n    sourceData = Seq((2, 3, 4, 5)).toDF(\"a\", \"b\", \"nested_a\", \"nested_b\")\n        .selectExpr(\"a\", \"b\", \"named_struct('a', nested_a, 'b', nested_b) as target\"),\n    clauses = update(\"target.b = source.b\"))(\n    expected = Seq(Row(0, Row(1, 3))),\n    expectErrorWithoutEvolutionContains = \"No such struct field `b` in `a\")\n\n  testEvolutionWithoutTableAliases(\"new nested field assignment qualified with target name\")(\n    targetData = Seq((0, 1)).toDF(\"a\", \"nested_a\")\n        .selectExpr(\"a\", \"named_struct('a', nested_a) as target\"),\n    sourceData = Seq((2, 3, 4, 5)).toDF(\"a\", \"b\", \"nested_a\", \"nested_b\")\n        .selectExpr(\"a\", \"b\", \"named_struct('a', nested_a, 'b', nested_b) as target\"),\n    clauses = update(\"target.target.b = source.target.b\"))(\n    // target.target.b gets resolved to source struct target, accessing nested field target.target.b\n    // which doesn't exist.\n    expectErrorContains = \"No such struct field `target` in `a`, `b`\",\n    // target.target.b: target.target gets resolved to target table 'target' column with nested\n    // field b which doesn't exist.\n    expectErrorWithoutEvolutionContains = \"No such struct field `b` in `a`\")\n}\n\n/**\n * Trait collecting all base and existing column tests for schema evolution.\n */\ntrait MergeIntoSchemaEvolutionBaseExistingColumnTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils\n    with SharedSparkSession =>\n\n  import testImplicits._\n\n  // No schema evolution\n  testEvolution(\"old column updated from new column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, -1), (2, 2, -2))\n      .toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(set = \"value = s.extra\") :: Nil,\n    expected = ((0, 0) +: (1, -1) +: (3, 30) +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, -1) +: (3, 30) +: Nil).toDF(\"key\", \"value\"))\n\n  testEvolution(\"old column inserted from new column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, -1), (2, 2, -2))\n      .toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(values = \"(key) VALUES (s.extra)\") :: Nil,\n    expected = ((0, 0) +: (1, 10) +: (3, 30) +: (-2, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer)]]\n      .toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 10) +: (3, 30) +: (-2, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer)]]\n      .toDF(\"key\", \"value\"))\n\n  // Column doesn't exist with UPDATE/INSERT alone.\n  testEvolution(\"update set nonexistent column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(set = \"nonexistent = s.extra\") :: Nil,\n    expectErrorContains = \"cannot resolve nonexistent in UPDATE clause\",\n    expectErrorWithoutEvolutionContains = \"cannot resolve nonexistent in UPDATE clause\")\n\n  testEvolution(\"insert values nonexistent column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(values = \"(nonexistent) VALUES (s.extra)\") :: Nil,\n    expectErrorContains = \"cannot resolve nonexistent in INSERT clause\",\n    expectErrorWithoutEvolutionContains = \"cannot resolve nonexistent in INSERT clause\")\n\n  testEvolution(\"update * with column not in source\")(\n    targetData = Seq((0, 0, 0), (1, 10, 10), (3, 30, 30)).toDF(\"key\", \"value\", \"extra\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: Nil,\n    // update went through even though `extra` wasn't there\n    expected = ((0, 0, 0) +: (1, 1, 10) +: (3, 30, 30) +: Nil).toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\"\n  )\n\n  testEvolution(\"insert * with column not in source\")(\n    targetData = Seq((0, 0, 0), (1, 10, 10), (3, 30, 30)).toDF(\"key\", \"value\", \"extra\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"value\"),\n    clauses = insert(\"*\") :: Nil,\n    // insert went through even though `extra` wasn't there\n    expected = ((0, 0, 0) +: (1, 10, 10) +: (2, 2, null) +: (3, 30, 30) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in INSERT clause\"\n  )\n\n  testEvolution(\"explicitly insert subset of columns\")(\n    targetData = Seq((0, 0, 0), (1, 10, 10), (3, 30, 30)).toDF(\"key\", \"value\", \"extra\"),\n    sourceData = Seq((1, 1, 1), (2, 2, 2)).toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(\"(key, value) VALUES (s.key, s.value)\") :: Nil,\n    // 2 should have extra = null, since extra wasn't in the insert spec.\n    expected = ((0, 0, 0) +: (1, 10, 10) +: (2, 2, null) +: (3, 30, 30) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]]\n      .toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = ((0, 0, 0) +: (1, 10, 10) +: (2, 2, null) +: (3, 30, 30) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, Integer)]]\n      .toDF(\"key\", \"value\", \"extra\")\n  )\n\n  testEvolution(\"explicitly update one column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, 1), (2, 2, 2)).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"value = s.value\") :: Nil,\n    // Both results should be the same - we're checking that no evolution logic triggers\n    // even though there's an extra source column.\n    expected = ((0, 0) +: (1, 1) +: (3, 30) +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 1) +: (3, 30) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(s\"case-insensitive insert\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"VALUE\"),\n    clauses = insert(\"(key, value, VALUE) VALUES (s.key, s.value, s.VALUE)\") :: Nil,\n    expected = ((0, 0) +: (1, 10) +: (3, 30) +: (2, 2) +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 10) +: (3, 30) +: (2, 2) +: Nil).toDF(\"key\", \"value\"),\n    confs = Seq(SQLConf.CASE_SENSITIVE.key -> \"false\")\n  )\n\n  // TODO: Add a test for case-sensitive insert and column not in target\n\n  testEvolution(\"case-sensitive insert, column not in source\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"VALUE\"),\n    clauses = insert(\"(key, value) VALUES (s.key, s.value)\") :: Nil,\n    expectErrorContains = \"Cannot resolve s.value in INSERT clause\",\n    expectErrorWithoutEvolutionContains = \"Cannot resolve s.value in INSERT clause\",\n    confs = Seq(SQLConf.CASE_SENSITIVE.key -> \"true\")\n  )\n\n  // Note that incompatible types are those where a cast to the target type can't resolve - any\n  // valid cast will be permitted.\n  testEvolution(\"incompatible types in update *\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, Array[Byte](1)), (2, Array[Byte](2))).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: Nil,\n    expectErrorContains =\n      \"Failed to merge incompatible data types IntegerType and BinaryType\",\n    expectErrorWithoutEvolutionContains = \"cannot cast\"\n  )\n\n  testEvolution(\"incompatible types in insert *\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, Array[Byte](1)), (2, Array[Byte](2))).toDF(\"key\", \"value\"),\n    clauses = insert(\"*\") :: Nil,\n    expectErrorContains = \"Failed to merge incompatible data types IntegerType and BinaryType\",\n    expectErrorWithoutEvolutionContains = \"cannot cast\"\n  )\n\n  // All integral types other than long can be upcasted to integer.\n  testEvolution(\"upcast numeric source types into integer target\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1.toByte, 1.toShort), (2.toByte, 2.toShort)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((0, 0), (1, 1), (2, 2), (3, 30)).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = Seq((0, 0), (1, 1), (2, 2), (3, 30)).toDF(\"key\", \"value\")\n  )\n\n  // Delta's automatic schema evolution allows converting table columns with a numeric type narrower\n  // than integer to integer, because in the underlying Parquet they're all stored as ints.\n  testEvolution(\"upcast numeric target types from integer source\")(\n    targetData = Seq((0.toByte, 0.toShort), (1.toByte, 10.toShort)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected =\n      ((0.toByte, 0.toShort) +:\n        (1.toByte, 1.toShort) +:\n        (2.toByte, 2.toShort) +: Nil\n        ).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution =\n      ((0.toByte, 0.toShort) +:\n        (1.toByte, 1.toShort) +:\n        (2.toByte, 2.toShort) +: Nil\n        ).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"upcast int source type into long target\")(\n    targetData = Seq((0, 0L), (1, 10L), (3, 30L)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((0, 0L) +: (1, 1L) +: (2, 2L) +: (3, 30L) +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution =\n      ((0, 0L) +: (1, 1L) +: (2, 2L) +: (3, 30L) +: Nil).toDF(\"key\", \"value\")\n  )\n\n  testEvolution(\"write string into int column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, \"1\"), (2, \"2\"), (5, \"notANumber\")).toDF(\"key\", \"value\"),\n    clauses = insert(\"*\") :: Nil,\n    expected = ((0, 0) +: (1, 10) +: (2, 2) +: (3, 30) +: (5, null) +: Nil)\n      .asInstanceOf[List[(Integer, Integer)]].toDF(\"key\", \"value\"),\n    expectedWithoutEvolution =\n      ((0, 0) +: (1, 10) +: (2, 2) +: (3, 30) +: (5, null) +: Nil)\n        .asInstanceOf[List[(Integer, Integer)]].toDF(\"key\", \"value\"),\n    // Disable ANSI as this test needs to cast string \"notANumber\" to int\n    confs = Seq(SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"LEGACY\")\n  )\n\n  // This is kinda bug-for-bug compatibility. It doesn't really make sense that infinity is casted\n  // to int as Int.MaxValue, but that's the behavior.\n  testEvolution(\"write double into int column\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1.1), (2, 2.2), (5, Double.PositiveInfinity)).toDF(\"key\", \"value\"),\n    clauses = insert(\"*\") :: Nil,\n    expected =\n      ((0, 0) +: (1, 10) +: (2, 2) +: (3, 30) +: (5, Int.MaxValue) +: Nil)\n        .asInstanceOf[List[(Integer, Integer)]].toDF(\"key\", \"value\"),\n    expectedWithoutEvolution =\n      ((0, 0) +: (1, 10) +: (2, 2) +: (3, 30) +: (5, Int.MaxValue) +: Nil)\n        .asInstanceOf[List[(Integer, Integer)]].toDF(\"key\", \"value\"),\n    // Disable ANSI as this test needs to cast Double.PositiveInfinity to int\n    confs = Seq(SQLConf.STORE_ASSIGNMENT_POLICY.key -> \"LEGACY\")\n  )\n\n  testEvolution(\"missing nested column in source - insert\")(\n    targetData = Seq((1, (1, 2, 3))).toDF(\"key\", \"x\"),\n    sourceData = Seq((2, (2, 3))).toDF(\"key\", \"x\"),\n    clauses = insert(\"*\") :: Nil,\n    expected = ((1, (1, 2, 3)) +: (2, (2, 3, null)) +: Nil)\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  testEvolution(\"missing nested column resolved by name - insert\")(\n    targetData = Seq((1, 1, 2, 3)).toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'b', b, 'c', c) as x\"),\n    sourceData = Seq((2, 2, 4)).toDF(\"key\", \"a\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'c', c) as x\"),\n    clauses = insert(\"*\") :: Nil,\n    expected = ((1, (1, 2, 3)) +: (2, (2, null, 4)) +: Nil)\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  testEvolutionWithoutTableAliases(\n    \"existing top-level column assignment qualified with target name\")(\n    targetData = Seq((0, 1)).toDF(\"a\", \"nested_a\")\n        .selectExpr(\"a\", \"named_struct('a', nested_a) as target\"),\n    sourceData = Seq((2, 3)).toDF(\"a\", \"nested_a\")\n        .selectExpr(\"a\", \"named_struct('a', nested_a) as target\"),\n    clauses = update(\"target.a = source.a\"))(\n    expected = Seq(Row(2, Row(1))))\n\n  testEvolutionWithoutTableAliases(\"existing nested field assignment qualified with target name\")(\n    targetData = Seq((0, 1)).toDF(\"a\", \"nested_a\")\n        .selectExpr(\"a\", \"named_struct('a', nested_a) as target\"),\n    sourceData = Seq((2, 3)).toDF(\"a\", \"nested_a\")\n        .selectExpr(\"a\", \"named_struct('a', nested_a) as target\"),\n    clauses = update(\"target.target.a = source.target.a\"))(\n    expected = Seq(Row(0, Row(3))))\n}\n\ntrait MergeIntoSchemaEvoStoreAssignmentPolicyTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n  import testImplicits._\n\n  // Upcasting is always allowed.\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values)\n  testEvolution(\"upcast int source type into long target, storeAssignmentPolicy = \" +\n    s\"$storeAssignmentPolicy\")(\n    targetData = Seq((0, 0L), (1, 1L), (3, 3L)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1), (2, 2)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected =\n      ((0, 0L) +: (1, 1L) +: (2, 2L) +: (3, 3L) +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution =\n      ((0, 0L) +: (1, 1L) +: (2, 2L) +: (3, 3L) +: Nil).toDF(\"key\", \"value\"),\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\")\n  )\n\n  // Casts that are not valid implicit casts (e.g. string -> boolean) are never allowed with\n  // schema evolution enabled and allowed only when storeAssignmentPolicy is LEGACY or ANSI when\n  // schema evolution is disabled.\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values - StoreAssignmentPolicy.STRICT)\n  testEvolution(\"invalid implicit cast string source type into boolean target, \" +\n    s\"storeAssignmentPolicy = $storeAssignmentPolicy\")(\n    targetData = Seq((0, true), (1, false), (3, true)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, \"true\"), (2, \"false\")).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"Failed to merge incompatible data types BooleanType and StringType\",\n    expectedWithoutEvolution = ((0, true) +: (1, true) +: (2, false) +: (3, true) +: Nil)\n      .toDF(\"key\", \"value\"),\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\")\n  )\n\n  // Casts that are not valid implicit casts (e.g. string -> boolean) are not allowed with\n  // storeAssignmentPolicy = STRICT.\n  testEvolution(\"invalid implicit cast string source type into boolean target, \" +\n   s\"storeAssignmentPolicy = ${StoreAssignmentPolicy.STRICT}\")(\n    targetData = Seq((0, true), (1, false), (3, true)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, \"true\"), (2, \"false\")).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"Failed to merge incompatible data types BooleanType and StringType\",\n    expectErrorWithoutEvolutionContains = \"cannot up cast s.value from \\\"string\\\" to \\\"boolean\\\"\",\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\")\n  )\n\n  // Valid implicit casts that are not upcasts (e.g. string -> int) are allowed with\n  // storeAssignmentPolicy = LEGACY or ANSI.\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values - StoreAssignmentPolicy.STRICT)\n  testEvolution(\"valid implicit cast string source type into int target, \" +\n   s\"storeAssignmentPolicy = ${storeAssignmentPolicy}\")(\n    targetData = Seq((0, 0), (1, 1), (3, 3)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, \"1\"), (2, \"2\")).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((0, 0)+: (1, 1) +: (2, 2) +: (3, 3)  +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 1) +: (2, 2) +: (3, 3) +: Nil).toDF(\"key\", \"value\"),\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\")\n  )\n\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values - StoreAssignmentPolicy.STRICT)\n  testEvolution(\"valid implicit cast long source type into int target, \" +\n   s\"storeAssignmentPolicy = $storeAssignmentPolicy\")(\n    targetData = Seq((0, 0), (1, 1), (3, 3)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1L), (2, 2L)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((0, 0)+: (1, 1) +: (2, 2) +: (3, 3)  +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = ((0, 0) +: (1, 1) +: (2, 2) +: (3, 3) +: Nil).toDF(\"key\", \"value\"),\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\")\n  )\n\n  // Valid implicit casts that are not upcasts (e.g. string -> int) are rejected with\n  // storeAssignmentPolicy = STRICT.\n  testEvolution(\"valid implicit cast string source type into int target, \" +\n   s\"storeAssignmentPolicy = ${StoreAssignmentPolicy.STRICT}\")(\n    targetData = Seq((0, 0), (1, 1), (3, 3)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, \"1\"), (2, \"2\")).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"cannot up cast s.value from \\\"string\\\" to \\\"int\\\"\",\n    expectErrorWithoutEvolutionContains = \"cannot up cast s.value from \\\"string\\\" to \\\"int\\\"\",\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\")\n  )\n\n  testEvolution(\"multiple casts with storeAssignmentPolicy = STRICT\")(\n    targetData = Seq((0L, \"0\"), (1L, \"10\"), (3L, \"30\")).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1L), (2, 2L)).toDF(\"key\", \"value\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected =\n      ((0L, \"0\") +: (1L, \"1\") +: (2L, \"2\") +: (3L, \"30\") +: Nil).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution =\n      ((0L, \"0\") +: (1L, \"1\") +: (2L, \"2\") +: (3L, \"30\") +: Nil).toDF(\"key\", \"value\"),\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\"))\n\n  testEvolution(\"new column with storeAssignmentPolicy = STRICT\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"one\"), (2, 2, \"two\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"value = CAST(s.value AS short)\") :: insert(\"*\") :: Nil,\n    expected =\n      ((0, 0, null) +: (1, 1, null) +: (2, 2, \"two\") +: (3, 30, null) +: Nil)\n        .toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution =\n      ((0, 0) +: (1, 1) +: (2, 2) +: (3, 30) +: Nil).toDF(\"key\", \"value\"),\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\"))\n\n}\n\n/**\n * Trait collecting tests for schema evolution with a NOT MATCHED BY SOURCE clause.\n */\ntrait MergeIntoSchemaEvolutionNotMatchedBySourceTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import testImplicits._\n\n  // Test schema evolution with NOT MATCHED BY SOURCE clauses.\n  testEvolution(\"new column with insert * and conditional update not matched by source\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = insert(\"*\") ::\n      updateNotMatched(condition = \"key > 0\", set = \"value = value + 1\") :: Nil,\n    expected = Seq(\n      (0, 0, null), // Not matched by source, no change\n      (1, 10, null), // Matched, no change\n      (2, 2, \"extra2\"), // Not matched by target, inserted\n      (3, 31, null) // Not matched by source, updated\n    ).toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = Seq((0, 0), (1, 10), (2, 2), (3, 31)).toDF(\"key\", \"value\"))\n\n  testEvolution(\"new column not inserted and conditional update not matched by source\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = updateNotMatched(condition = \"key > 0\", set = \"value = value + 1\") :: Nil,\n    expected = Seq(\n      (0, 0), // Not matched by source, no change\n      (1, 10), // Matched, no change\n      (3, 31) // Not matched by source, updated\n    ).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = Seq((0, 0), (1, 10), (3, 31)).toDF(\"key\", \"value\"))\n\n  testEvolution(\"new column referenced in matched condition but not inserted\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = delete(condition = \"extra = 'extra1'\") ::\n      updateNotMatched(condition = \"key > 0\", set = \"value = value + 1\") :: Nil,\n    expected = Seq(\n      (0, 0), // Not matched by source, no change\n      // (1, 10), Matched, deleted\n      (3, 31) // Not matched by source, updated\n    ).toDF(\"key\", \"value\"),\n    expectedWithoutEvolution = Seq((0, 0), (3, 31)).toDF(\"key\", \"value\"))\n\n  testEvolution(\"matched update * and conditional update not matched by source\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra1\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = update(\"*\") ::\n      updateNotMatched(condition = \"key > 0\", set = \"value = value + 1\") :: Nil,\n    expected = Seq(\n      (0, 0, null), // Not matched by source, no change\n      (1, 1, \"extra1\"), // Matched, updated\n      (3, 31, null) // Not matched by source, updated\n    ).toDF(\"key\", \"value\", \"extra\"),\n    expectedWithoutEvolution = Seq((0, 0), (1, 1), (3, 31)).toDF(\"key\", \"value\"))\n\n  // Migrating new column via WHEN NOT MATCHED BY SOURCE is not allowed.\n  testEvolution(\"update new column with not matched by source fails\")(\n    targetData = Seq((0, 0), (1, 10), (3, 30)).toDF(\"key\", \"value\"),\n    sourceData = Seq((1, 1, \"extra3\"), (2, 2, \"extra2\")).toDF(\"key\", \"value\", \"extra\"),\n    clauses = updateNotMatched(\"extra = s.extra\") :: Nil,\n    expectErrorContains = \"cannot resolve extra in UPDATE clause\",\n    expectErrorWithoutEvolutionContains = \"cannot resolve extra in UPDATE clause\")\n}\n\n/**\n * Trait collecting all tests for nested struct evolution.\n */\ntrait MergeIntoNestedStructInMapEvolutionTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import testImplicits._\n\n  // scalastyle:off line.size.limit\n  // Struct evolution inside of map values.\n  testNestedStructsEvolution(\"new source column in map struct value\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 1 } } }\n         { \"key\": \"C\", \"map\": { \"key\": { \"a\": 3 } } }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 2, \"b\": 2 } } }\n         { \"key\": \"B\", \"map\": { \"key\": { \"a\": 1, \"b\": 2 } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 2, \"b\": 2 } } }\n         { \"key\": \"B\", \"map\": { \"key\": { \"a\": 1, \"b\": 2 } } }\n         { \"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\": null } } }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new source column in nested map struct value\")(\n    target =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"innerKey\": { \"a\": 1 } } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"innerKey\": { \"a\": 3 } } } }\"\"\",\n    source =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2, \"b\": 3 } } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2, \"b\": 3 } } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          MapType(StringType, new StructType().add(\"a\", IntegerType)))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          MapType(StringType, new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          MapType(StringType, new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2, \"b\": 3 } } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2, \"b\": 3 } } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"innerKey\": { \"a\": 3, \"b\": null } } } }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"source map struct value contains less columns than target\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 1, \"b\": 1 } } }\n         { \"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\": 1 } } }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 2 } } }\n         { \"key\": \"B\", \"map\": { \"key\": { \"a\": 1 } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 2, \"b\": null } } }\n         { \"key\": \"B\", \"map\": { \"key\": { \"a\": 1, \"b\": null } } }\n         { \"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\": 1 } } }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"source nested map struct value contains less columns than target\")(\n    target =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"innerKey\": { \"a\": 1, \"b\": 1 } } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"innerKey\": { \"a\": 3, \"b\": 1 } } } }\"\"\",\n    source =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2 } } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2 } } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          MapType(StringType, new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          MapType(StringType, new StructType().add(\"a\", IntegerType)))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          MapType(StringType, new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2, \"b\": null } } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"innerKey\": { \"a\": 2, \"b\": null } } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"innerKey\": { \"a\": 3, \"b\": 1 } } } }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"source nested map struct value contains different type than target\")(\n    target =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"a\": 1, \"b\" : 1 } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\" : 1 } } }\"\"\",\n    source =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"a\": 1, \"b\" : \"2\" } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"a\": 2, \"b\" : \"2\" } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"a\": 1, \"b\" : 2 } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"a\": 2, \"b\" : 2 } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\" : 1 } } }\"\"\",\n    resultWithoutEvolution =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"a\": 1, \"b\" : 2 } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"a\": 2, \"b\" : 2 } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\" : 1 } } }\"\"\")\n\n  testNestedStructsEvolution(\"source nested map struct value in different order\")(\n    target =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"a\" : 1, \"b\" : 1 } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"a\" : 3, \"b\" : 1 } } }\"\"\",\n    source =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"b\" : 2, \"a\" : 1, \"c\" : 3 } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"b\" : 2, \"a\" : 2, \"c\" : 4 } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"b\", IntegerType).add(\"a\", IntegerType).add(\"c\", IntegerType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType).add(\"c\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{\"key\": \"A\", \"map\": { \"key\": { \"a\": 1, \"b\" : 2, \"c\" : 3 } } }\n         {\"key\": \"B\", \"map\": { \"key\": { \"a\": 2, \"b\" : 2, \"c\" : 4 } } }\n         {\"key\": \"C\", \"map\": { \"key\": { \"a\": 3, \"b\" : 1, \"c\" : null } } }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"source map struct value to map array value\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": [ 1, 2 ] } }\n         { \"key\": \"C\", \"map\": { \"key\": [ 3, 4 ] } }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": { \"a\": 2 } } }\n         { \"key\": \"B\", \"map\": { \"key\": { \"a\": 1 } } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          ArrayType(IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          new StructType().add(\"a\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"Failed to merge incompatible data types\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"source struct nested in map array values contains more columns in different order\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": [ { \"a\": 1, \"b\": 2 } ] } }\n         { \"key\": \"C\", \"map\": { \"key\": [ { \"a\": 3, \"b\": 4 } ] } }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": [ { \"b\": 6, \"c\": 7, \"a\": 5 } ] } }\n         { \"key\": \"B\", \"map\": { \"key\": [ { \"b\": 9, \"c\": 10, \"a\": 8 } ] } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          ArrayType(\n            new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          ArrayType(\n          new StructType().add(\"b\", IntegerType).add(\"c\", IntegerType).add(\"a\", IntegerType)))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"map\", MapType(\n          StringType,\n          ArrayType(\n          new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType).add(\"c\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"map\": { \"key\": [ { \"a\": 5, \"b\": 6, \"c\": 7 } ] } }\n         { \"key\": \"B\", \"map\": { \"key\": [ { \"a\": 8, \"b\": 9, \"c\": 10 } ] } }\n         { \"key\": \"C\", \"map\": { \"key\": [ { \"a\": 3, \"b\": 4, \"c\": null } ] } }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  // Struct evolution inside of map keys.\n  testEvolution(\"new source column in map struct key\")(\n    targetData = Seq((1, 2, 3, 4), (3, 5, 6, 7)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\"),\n    sourceData = Seq((1, 10, 30, 50, 1), (2, 20, 40, 60, 2)).toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), value) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 50, 1), (2, 20, 40, 60, 2), (3, 5, 6, null, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), value) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  testEvolution(\"source nested map struct key contains less columns than target\")(\n    targetData = Seq((1, 2, 3, 4, 5), (3, 6, 7, 8, 9)).toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), value) as x\"),\n    sourceData = Seq((1, 10, 50, 1), (2, 20, 60, 2)).toDF(\"key\", \"a\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'c', c), value) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, null, 50, 1), (2, 20, null, 60, 2), (3, 6, 7, 8, 9))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), value) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  testEvolution(\"source nested map struct key contains different type than target\")(\n    targetData = Seq((1, 2, 3, 4), (3, 5, 6, 7)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\"),\n    sourceData = Seq((1, 10, \"30\", 1), (2, 20, \"40\", 2)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 1), (2, 20, 40, 2), (3, 5, 6, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\"),\n    expectedWithoutEvolution = Seq((1, 10, 30, 1), (2, 20, 40, 2), (3, 5, 6, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\")\n  )\n\n  testEvolution(\"source nested map struct key in different order\")(\n    targetData = Seq((1, 2, 3, 4), (3, 5, 6, 7)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\"),\n    sourceData = Seq((1, 10, 30, 1), (2, 20, 40, 2)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('b', b, 'a', a), value) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 1), (2, 20, 40, 2), (3, 5, 6, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\"),\n    expectedWithoutEvolution = Seq((1, 10, 30, 1), (2, 20, 40, 2), (3, 5, 6, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), value) as x\")\n  )\n\n  testEvolution(\"struct nested in map array keys contains more columns\")(\n    targetData = Seq((1, 2, 3, 4), (3, 5, 6, 7)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(array(named_struct('a', a, 'b', b)), value) as x\"),\n    sourceData = Seq((1, 10, 30, 50, 1), (2, 20, 40, 60, 2)).toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(array(named_struct('a', a, 'b', b, 'c', c)), value) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 50, 1), (2, 20, 40, 60, 2), (3, 5, 6, null, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(array(named_struct('a', a, 'b', b, 'c', c)), value) as x\"),\n    expectErrorWithoutEvolutionContains = \"cannot cast\"\n  )\n\n  testEvolution(\"update-only struct nested in map array keys contains more columns\")(\n    targetData = Seq((1, 2, 3, 4), (3, 5, 6, 7)).toDF(\"key\", \"a\", \"b\", \"value\")\n      .selectExpr(\"key\", \"map(array(named_struct('a', a, 'b', b)), value) as x\"),\n    sourceData = Seq((1, 10, 30, 50, 1), (2, 20, 40, 60, 2)).toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(array(named_struct('a', a, 'b', b, 'c', c)), value) as x\"),\n    clauses = update(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 50, 1), (3, 5, 6, null, 7))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"value\")\n      .selectExpr(\"key\", \"map(array(named_struct('a', a, 'b', b, 'c', c)), value) as x\"),\n    expectErrorWithoutEvolutionContains = \"cannot cast\"\n  )\n\n  testEvolution(\"struct evolution in both map keys and values\")(\n    targetData = Seq((1, 2, 3, 4, 5), (3, 6, 7, 8, 9)).toDF(\"key\", \"a\", \"b\", \"d\", \"e\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), named_struct('d', d, 'e', e)) as x\"),\n    sourceData = Seq((1, 10, 30, 50, 70, 90, 110), (2, 20, 40, 60, 80, 100, 120))\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), named_struct('d', d, 'e', e, 'f', f)) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 50, 70, 90, 110), (2, 20, 40, 60, 80, 100, 120), (3, 6, 7, null, 8, 9, null))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), named_struct('d', d, 'e', e, 'f', f)) as x\"),\n    expectErrorWithoutEvolutionContains = \"cannot cast\"\n  )\n\n  testEvolution(\"update only struct evolution in both map keys and values\")(\n    targetData = Seq((1, 2, 3, 4, 5), (3, 6, 7, 8, 9)).toDF(\"key\", \"a\", \"b\", \"d\", \"e\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b), named_struct('d', d, 'e', e)) as x\"),\n    sourceData = Seq((1, 10, 30, 50, 70, 90, 110), (2, 20, 40, 60, 80, 100, 120))\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), named_struct('d', d, 'e', e, 'f', f)) as x\"),\n    clauses = update(\"*\") :: Nil,\n    expected = Seq((1, 10, 30, 50, 70, 90, 110), (3, 6, 7, null, 8, 9, null))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\")\n      .selectExpr(\"key\", \"map(named_struct('a', a, 'b', b, 'c', c), named_struct('d', d, 'e', e, 'f', f)) as x\"),\n    expectErrorWithoutEvolutionContains = \"cannot cast\"\n  )\n  // scalastyle:on line.size.limit\n}\n\ntrait MergeIntoNestedStructEvolutionInsertTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import testImplicits._\n\n  // Nested Schema evolution with INSERT alone\n  testNestedStructsEvolution(\"new nested source field added when inserting top-level column\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    source = \"\"\"{ \"key\": \"B\", \"value\": { \"a\": 2, \"b\": 3 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    clauses = insert(\"(value) VALUES (s.value)\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1, \"b\": null } }\n       { \"key\": null, \"value\": { \"a\": 2, \"b\": 3 } }\"\"\".stripMargin,\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"insert new nested source field not supported\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2, \"b\": 3, \"c\": 4 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)\n        .add(\"c\", IntegerType)),\n    clauses = insert(\"(value.b) VALUES (s.value.b)\") :: Nil,\n    expectErrorContains = \"Nested field is not supported in the INSERT clause of MERGE operation\",\n    expectErrorWithoutEvolutionContains = \"No such struct field\")\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"new nested column with update non-* and insert * - array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1, \"c\": 2 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } }, { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 20 } }, { \"b\": \"4\", \"a\": { \"y\": 30, \"x\": 20 } } ] }\n           { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } }, { \"b\": \"4\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType))\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2, \"c\": null}, { \"a\": { \"x\": 20, \"y\": 30}, \"b\": 3, \"c\": null }, { \"a\": { \"x\": 20, \"y\": 30}, \"b\": 4, \"c\": null } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30 }, \"b\": 3, \"c\": null }, { \"a\": { \"x\": 40, \"y\": 30}, \"b\": 4, \"c\": null } ] }\"\"\".stripMargin,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new nested column with update non-* and insert * - array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1, \"c\": 2 }, { \"a\": { \"x\": 3, \"y\": 2 }, \"b\": 2, \"c\": 2 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } } ] }\n           { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType))\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20}, \"b\": 2, \"c\": null } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30}, \"b\": 3, \"c\": null } ] }\"\"\".stripMargin,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new nested column with update non-* and insert * - nested array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 } ] }, \"b\": 1, \"c\": 4 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": {\"x\": [ { \"d\": \"30\", \"c\": 10 }, { \"d\": \"20\", \"c\": 10 }, { \"d\": \"20\", \"c\": 10 } ], \"y\": 20 } } ] }\n          { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": {\"x\": [ { \"d\": \"50\", \"c\": 20 }, { \"d\": \"20\", \"c\": 10 } ], \"y\": 60 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n            )))\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"d\", StringType)\n                .add(\"c\", IntegerType)\n            ))\n            .add(\"y\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30 }, { \"c\": 10, \"d\": 20 }, { \"c\": 10, \"d\": 20 } ] }, \"b\": 2, \"c\": null}] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50 }, { \"c\": 10, \"d\": 20 } ] }, \"b\": 3, \"c\": null } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new nested column with update non-* and insert * - nested array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3}, { \"c\": 2, \"d\": 3 } ] }, \"b\": 1, \"c\": 4 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": {\"x\": [ { \"d\": \"30\", \"c\": 10 } ], \"y\": 20 } } ] }\n          { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": {\"x\": [ { \"d\": \"50\", \"c\": 20 } ], \"y\": 60 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n            )))\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"d\", StringType)\n                .add(\"c\", IntegerType)\n            ))\n            .add(\"y\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30} ] }, \"b\": 2, \"c\": null}] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50 } ] }, \"b\": 3, \"c\": null } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n  // scalastyle:on line.size.limit\n\n  testEvolution(\"new nested-nested column with update non-* and insert *\")(\n    targetData = Seq((1, 1, 2, 3)).toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"named_struct('y', named_struct('a', a, 'b', b, 'c', c)) as x\"),\n    sourceData = Seq((1, 10, 30), (2, 20, 40)).toDF(\"key\", \"a\", \"c\")\n      .selectExpr(\"key\", \"named_struct('y', named_struct('a', a, 'c', c)) as x\"),\n    clauses = update(\"x.y.a = s.x.y.a\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 2, 3), (2, 20, null, 40))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"named_struct('y', named_struct('a', a, 'b', b, 'c', c)) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"new nested-nested column with update non-* and insert * - array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"z\": 3 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } }, { \"b\": \"3\", \"a\": { \"y\": 20, \"x\": 30 } }, { \"b\": \"4\", \"a\": { \"y\": 20, \"x\": 30 } } ] }\n           { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } }, { \"b\": \"4\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": null }, \"b\": 2 }, { \"a\": { \"x\": 30, \"y\": 20, \"z\": null }, \"b\": 3}, { \"a\": { \"x\": 30, \"y\": 20, \"z\": null }, \"b\": 4 } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 3 }, { \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 4 } ] }\"\"\".stripMargin,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new nested-nested column with update non-* and insert * - array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"z\": 3 }, \"b\": 1 }, { \"a\": { \"x\": 2, \"y\": 3, \"z\": 4 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } } ] }\n           { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 10, \"y\": 20, \"z\": null }, \"b\": 2 }] }\n           { \"key\": \"B\", \"value\": [{ \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 3 }] }\"\"\".stripMargin,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new nested-nested column with update non-* and insert * - nested array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"e\": 1 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": {\"x\": [ { \"d\": \"30\", \"c\": 10 }, { \"d\": \"30\", \"c\": 40 }, { \"d\": \"30\", \"c\": 50 } ], \"y\": 20 } } ] }\n          { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": {\"x\": [ { \"d\": \"50\", \"c\": 20 }, { \"d\": \"50\", \"c\": 30 } ], \"y\": 60 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"d\", StringType)\n                .add(\"c\", IntegerType)\n            ))\n            .add(\"y\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"e\": null }, { \"c\": 40, \"d\": 30, \"e\": null }, { \"c\": 50, \"d\": 30, \"e\": null } ] }, \"b\": 2 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"e\": null }, { \"c\": 30, \"d\": 50, \"e\": null } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"new nested-nested column with update non-* and insert * - nested array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"e\": 1 }, { \"c\": 2, \"d\": 3, \"e\": 4 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"x\": [ { \"d\": \"30\", \"c\": 10 } ], \"y\": 20 } } ] }\n          { \"key\": \"B\", \"value\": [ { \"b\": \"3\", \"a\": { \"x\": [ { \"d\": \"50\", \"c\": 20 } ], \"y\": 60 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"d\", StringType)\n                .add(\"c\", IntegerType)\n            ))\n            .add(\"y\", IntegerType)))),\n    clauses = update(\"value = s.value\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"e\": null } ] }, \"b\": 2 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"e\": null } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  // scalastyle:on line.size.limit\n\n  // Note that the obvious dual of this test, \"update * and insert non-*\", doesn't exist\n  // because nested columns can't be explicitly INSERTed to.\n  testEvolution(\"new nested column with update non-* and insert *\")(\n    targetData = Seq((1, 1, 2, 3)).toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'b', b, 'c', c) as x\"),\n    sourceData = Seq((1, 10, 30), (2, 20, 40)).toDF(\"key\", \"a\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'c', c) as x\"),\n    clauses = update(\"x.a = s.x.a\") :: insert(\"*\") :: Nil,\n    expected = Seq((1, 10, 2, 3), (2, 20, null, 40))\n      .asInstanceOf[List[(Integer, Integer, Integer, Integer)]]\n      .toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'b', b, 'c', c) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"missing nested column resolved by name - insert - array of struct\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"z\": 1 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"z\": 20 }, \"b\": \"2\" } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"z\": 30 }, \"b\": \"3\" }, { \"a\": { \"x\": 40, \"z\": 30 }, \"b\": \"4\" }, { \"a\": { \"x\": 40, \"z\": 30 }, \"b\": \"5\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"z\": 1 }, \"b\": 1 } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": null, \"z\": 30 }, \"b\": 3 }, { \"a\": { \"x\": 40, \"y\": null, \"z\": 30 }, \"b\": 4 }, { \"a\": { \"x\": 40, \"y\": null, \"z\": 30 }, \"b\": 5 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"missing nested column resolved by name - insert - nested array of struct\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"e\": 1 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"e\": \"30\" } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ {\"a\": {\"y\": 60, \"x\": [ { \"c\": 20, \"e\": \"50\" }, { \"c\": 20, \"e\": \"60\" }, { \"c\": 20, \"e\": \"80\" } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"e\", StringType)\n            )))\n          .add(\"b\", StringType))),\n    clauses = insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"e\": 1 } ] }, \"b\": 1 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": null, \"e\": 50 }, { \"c\": 20, \"d\": null, \"e\": 60 }, { \"c\": 20, \"d\": null, \"e\": 80 } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n  // scalastyle:on line.size.limit\n\n  testEvolution(\"additional nested column in source resolved by name - insert\")(\n    targetData = Seq((1, 10, 30)).toDF(\"key\", \"a\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'c', c) as x\"),\n    sourceData = Seq((2, 20, 30, 40)).toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"named_struct('a', a, 'b', b, 'c', c) as x\"),\n    clauses = insert(\"*\") :: Nil,\n    expected = ((1, (10, null, 30)) +: ((2, (20, 30, 40)) +: Nil))\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'c', x._3, 'b', x._2) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"additional nested column in source resolved by name - insert - array of struct\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"z\": 2 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2 \"} ] }\n           { \"key\": \"B\", \"value\": [ {\"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"3\" }, {\"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"4\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"z\": 2, \"y\": null }, \"b\": 1 } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"z\": 3, \"y\": 30 }, \"b\": 3 }, { \"a\": { \"x\": 40, \"z\": 3, \"y\": 30 }, \"b\": 4 } ] }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"z\", IntegerType)\n            .add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"additional nested column in source resolved by name - insert - nested array of struct\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"e\": 3 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": \"50\", \"e\": 2 }, { \"c\": 20, \"d\": \"50\", \"e\": 3 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"e\", IntegerType)\n                .add(\"d\", StringType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"e\": 3, \"d\": null } ] }, \"b\": 1 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"e\": 2, \"d\": \"50\" }, { \"c\": 20, \"e\": 3, \"d\": \"50\" } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n  // scalastyle:on line.size.limit\n\n  testEvolution(\"nested columns resolved by name with same column count but different names\")(\n    targetData = Seq((1, 1, 2, 3)).toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"struct(a, b, c) as x\"),\n    sourceData = Seq((1, 10, 20, 30), (2, 20, 30, 40)).toDF(\"key\", \"a\", \"b\", \"d\")\n      .selectExpr(\"key\", \"struct(a, b, d) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    // We evolve to the schema (key, x.{a, b, c, d}).\n    expected = ((1, (10, 20, 3, 30)) +: (2, (20, 30, null, 40)) +: Nil)\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer, Integer))]]\n      .toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3, 'd', x._4) as x\"),\n    expectErrorWithoutEvolutionContains = \"All nested columns must match.\"\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"nested columns resolved by name with same column count but different names - array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"o\": 4 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2\" }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"3\" }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"4\" } ] }\n           { \"key\": \"B\", \"value\": [ {\"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"3\" }, {\"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"4\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"o\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"o\": null, \"z\": 2 }, \"b\": 2 }, { \"a\": { \"x\": 10, \"y\": 20, \"o\": null, \"z\": 2 }, \"b\": 3 }, { \"a\": { \"x\": 10, \"y\": 20, \"o\": null, \"z\": 2 }, \"b\": 4 } ] }\n           { \"key\": \"B\", \"value\": [ {\"a\": { \"x\": 40, \"y\": 30, \"o\": null, \"z\": 3 }, \"b\": 3 }, {\"a\": { \"x\": 40, \"y\": 30, \"o\": null, \"z\": 3 }, \"b\": 4 } ] }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"o\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"nested columns resolved by name with same column count but different names - array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"o\": 4 }, \"b\": 1 }, { \"a\": { \"x\": 1, \"y\": 2, \"o\": 4 }, \"b\": 2 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2\" } ] }\n           { \"key\": \"B\", \"value\": [ {\"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"o\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"o\": null, \"z\": 2 }, \"b\": 2 } ] }\n           { \"key\": \"B\", \"value\": [ {\"a\": { \"x\": 40, \"y\": 30, \"o\": null, \"z\": 3 }, \"b\": 3 } ] }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"o\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"nested columns resolved by name with same column count but different names - nested array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"f\": 4 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 }, { \"c\": 10, \"d\": \"30\", \"e\": 2 }, { \"c\": 10, \"d\": \"30\", \"e\": 3 } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": \"50\", \"e\": 2 }, { \"c\": 20, \"d\": \"50\", \"e\": 3 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"f\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"f\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"f\": null, \"e\": 1 }, { \"c\": 10, \"d\": 30, \"f\": null, \"e\": 2 }, { \"c\": 10, \"d\": 30, \"f\": null, \"e\": 3 } ] }, \"b\": 2 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"f\": null, \"e\": 2 }, { \"c\": 20, \"d\": 50, \"f\": null, \"e\": 3 } ] }, \"b\": 3} ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"nested columns resolved by name with same column count but different names - nested array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"f\": 4 }, { \"c\": 1, \"d\": 3, \"f\": 4 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": \"50\", \"e\": 2 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"f\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"f\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"f\": null, \"e\": 1 } ] }, \"b\": 2 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"f\": null, \"e\": 2 } ] }, \"b\": 3} ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n  // scalastyle:on line.size.limit\n\n  testEvolution(\"nested columns resolved by position with same column count but different names\")(\n    targetData = Seq((1, 1, 2, 3)).toDF(\"key\", \"a\", \"b\", \"c\")\n      .selectExpr(\"key\", \"struct(a, b, c) as x\"),\n    sourceData = Seq((1, 10, 20, 30), (2, 20, 30, 40)).toDF(\"key\", \"a\", \"b\", \"d\")\n      .selectExpr(\"key\", \"struct(a, b, d) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"cannot cast\",\n    expectedWithoutEvolution = ((1, (10, 20, 30)) +: (2, (20, 30, 40)) +: Nil)\n      .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]]\n      .toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"nested columns resolved by position with same column count but different names - array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 1, \"y\": 2, \"o\": 4 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2\" }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 3 }, \"b\": \"2\" }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 3 }, \"b\": \"3\" } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"3\" }, { \"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"4\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"o\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    resultWithoutEvolution =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"o\": 2 }, \"b\": 2 }, { \"a\": { \"x\": 10, \"y\": 20, \"o\": 3 }, \"b\": 2 }, { \"a\": { \"x\": 10, \"y\": 20, \"o\": 3 }, \"b\": 3 } ] }\n           { \"key\": \"B\", \"value\": [ {\"a\": { \"x\": 40, \"y\": 30, \"o\": 3 }, \"b\": 3 }, {\"a\": { \"x\": 40, \"y\": 30, \"o\": 3 }, \"b\": 4 } ] }\"\"\",\n    expectErrorContains = \"cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"nested columns resolved by position with same column count but different names - array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 1, \"y\": 2, \"o\": 4 }, \"b\": 1}, { \"a\": { \"x\": 1, \"y\": 2, \"o\": 4 }, \"b\": 2}] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2\" } ] }\n           { \"key\": \"B\", \"value\": [{\"a\": { \"x\": 40, \"y\": 30, \"z\": 3 }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"o\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    resultWithoutEvolution =\n      \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 10, \"y\": 20, \"o\": 2}, \"b\": 2}] }\n           { \"key\": \"B\", \"value\": [{\"a\": { \"x\": 40, \"y\": 30, \"o\": 3}, \"b\": 3}] }\"\"\",\n    expectErrorContains = \"cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"nested columns resolved by position with same column count but different names - nested array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"f\": 4 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 }, { \"c\": 10, \"d\": \"30\", \"e\": 2 }, { \"c\": 10, \"d\": \"30\", \"e\": 3} ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": \"50\", \"e\": 2 }, { \"c\": 20, \"d\": \"50\", \"e\": 3 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"f\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    resultWithoutEvolution =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": {\"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"f\": 1 }, { \"c\": 10, \"d\": 30, \"f\": 2 }, { \"c\": 10, \"d\": 30, \"f\": 3 } ] }, \"b\": 2 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": {\"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"f\": 2 }, { \"c\": 20, \"d\": 50, \"f\": 3 } ] }, \"b\": 3}]}\"\"\",\n    expectErrorContains = \"cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"nested columns resolved by position with same column count but different names - nested array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"f\": 5 }, { \"c\": 1, \"d\": 3, \"f\": 6 } ] }, \"b\": 1}] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": \"50\", \"e\": 2 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"f\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    resultWithoutEvolution =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": {\"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"f\": 1 } ] }, \"b\": 2 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": {\"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"f\": 2 } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorContains = \"cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n  // scalastyle:on line.size.limit\n\n  testEvolution(\"struct in different order\")(\n    targetData = Seq((1, (1, 10, 100)), (2, (2, 20, 200))).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    sourceData = Seq((1, (1111, 111, 11)), (3, (3333, 333, 33))).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('c', x._1, 'b', x._2, 'a', x._3) as x\"),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expected = ((1, (11, 111, 1111)) :: (2, (2, 20, 200)) :: (3, (33, 333, 3333)) :: Nil)\n      .toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    expectedWithoutEvolution =\n      ((1, (11, 111, 1111)) :: (2, (2, 20, 200)) :: (3, (33, 333, 3333)) :: Nil)\n      .toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\")\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"struct in different order - array of struct\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 }] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [{ \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10}}, { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40}}] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 }, { \"a\": { \"y\": 30, \"x\": 40}, \"b\": 3 }] }\"\"\",\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 }, { \"a\": { \"y\": 30, \"x\": 40}, \"b\": 3 }] }\"\"\")\n\n  testNestedStructsEvolution(\"struct in different order - nested array of struct\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"y\": 2, \"x\": [{ \"c\": 1, \"d\": 3}]}, \"b\": 1 }] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [{ \"b\": \"2\", \"a\": {\"x\": [{ \"d\": \"30\", \"c\": 10}, { \"d\": \"40\", \"c\": 3}], \"y\": 20}}]}\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"d\", StringType)\n                .add(\"c\", IntegerType)\n            ))\n            .add(\"y\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"y\": 20, \"x\": [{ \"c\": 10, \"d\": 30}, { \"c\": 3, \"d\": 40}]}, \"b\": 2 }]}\"\"\",\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"y\": 20, \"x\": [{ \"c\": 10, \"d\": 30}, { \"c\": 3, \"d\": 40}]}, \"b\": 2 }]}\"\"\")\n  // scalastyle:on line.size.limit\n\n  testNestedStructsEvolution(\"array of struct with same columns but in different order\" +\n    \" which can be casted implicitly - by name\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1, \"b\": 2 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": 4, \"a\": 3 } ] }\n          { \"key\": \"B\", \"value\": [ { \"b\": 2, \"a\": 5 } ] }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"a\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 3, \"b\": 4 } ] }\n                { \"key\": \"B\", \"value\": [ { \"a\": 5, \"b\": 2 } ] }\"\"\".stripMargin,\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 3, \"b\": 4 } ] }\n                { \"key\": \"B\", \"value\": [ { \"a\": 5, \"b\": 2 } ] }\"\"\".stripMargin)\n\n  testNestedStructsEvolution(\"array of struct with same columns but in different order\" +\n    \" which can be casted implicitly - by position\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1, \"b\": 2 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": 4, \"a\": 3 } ] }\n                  { \"key\": \"B\", \"value\": [ { \"b\": 2, \"a\": 5 } ] }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"a\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 4, \"b\": 3 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": 2, \"b\": 5 } ] }\"\"\".stripMargin,\n    resultWithoutEvolution =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 4, \"b\": 3 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": 2, \"b\": 5 } ] }\"\"\".stripMargin,\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"array of struct with same column count but all different names\" +\n    \" - by name\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1, \"b\": 2 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"c\": 4, \"d\": 3 } ] }\n          { \"key\": \"B\", \"value\": [ { \"c\": 2, \"d\": 5 } ] }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"c\", IntegerType)\n          .add(\"d\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType)\n          .add(\"d\", IntegerType))),\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": null, \"b\": null, \"c\": 4, \"d\": 3 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": null, \"b\": null, \"c\": 2, \"d\": 5 } ] }\"\"\".stripMargin,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"array of struct with same column count but all different names\" +\n    \" - by position\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1, \"b\": 2 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"c\": 4, \"d\": 3 } ] }\n                  { \"key\": \"B\", \"value\": [ { \"c\": 2, \"d\": 5 } ] }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"c\", IntegerType)\n          .add(\"d\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    resultWithoutEvolution =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 4, \"b\": 3 } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": 2, \"b\": 5 } ] }\"\"\".stripMargin,\n    expectErrorContains = \" cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"array of struct with same columns but in different order\" +\n    \" which cannot be casted implicitly - by name\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": {\"c\" : 1}, \"b\": 2 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": 4, \"a\": {\"c\" : 3 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"c\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"a\", new StructType().add(\"c\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"c\" : 3 }, \"b\": 4 } ] }\"\"\",\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"c\" : 3 }, \"b\": 4 } ] }\"\"\")\n\n  testNestedStructsEvolution(\"array of struct with same columns but in different order\" +\n    \" which cannot be casted implicitly - by position\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": {\"c\" : 1}, \"b\": 2 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": 4, \"a\": {\"c\" : 3 } } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"c\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"a\", new StructType().add(\"c\", IntegerType)))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \" cannot cast\",\n    expectErrorWithoutEvolutionContains = \" cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"array of struct with additional column in target - by name\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1, \"b\": 2, \"c\": 3 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": 4, \"a\": 3 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"a\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 3, \"b\": 4, \"c\": null } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"array of struct with additional column in target - by position\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1, \"b\": 2, \"c\": 3 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": 4, \"a\": 3 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"a\", IntegerType))),\n    clauses = update(\"*\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \" cannot cast\",\n    expectErrorWithoutEvolutionContains = \"cannot cast\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n\n  testNestedStructsEvolution(\"struct with extra source column not used in update, without fix\")(\n    target = \"\"\"{ \"key\": 1, \"value\": { \"a\": 10 } }\"\"\",\n    source =\n      \"\"\"{ \"key\": 1, \"value\": { \"a\": 11, \"b\": 21 } }\n       { \"key\": 2, \"value\": { \"a\": 12, \"b\": 22 } }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    cond = \"t.key = s.key\",\n    clauses = update(set = \"key = 0\") :: insert(\"*\") :: Nil,\n    expectErrorContains = \"data type mismatch\",\n    expectErrorWithoutEvolutionContains = \"cannot cast\",\n    confs = Seq(\n      DeltaSQLConf.DELTA_MERGE_SCHEMA_EVOLUTION_FIX_NESTED_STRUCT_ALIGNMENT.key -> \"false\")\n  )\n\n  testNestedStructsEvolution(\"struct with extra source column not used in update\")(\n    target = \"\"\"{ \"key\": 1, \"value\": { \"a\": 10 } }\"\"\",\n    source =\n    \"\"\"{ \"key\": 1, \"value\": { \"a\": 11, \"b\": 21 } }\n       { \"key\": 2, \"value\": { \"a\": 12, \"b\": 22 } }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    cond = \"t.key = s.key\",\n    clauses = update(set = \"key = 0\") :: insert(\"*\") :: Nil,\n    result =\n    \"\"\"{ \"key\": 0, \"value\": { \"a\": 10, \"b\": null } }\n       { \"key\": 2, \"value\": { \"a\": 12, \"b\": 22 } }\"\"\".stripMargin,\n    resultSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"cannot cast\",\n    confs = Seq(\n      DeltaSQLConf.DELTA_MERGE_SCHEMA_EVOLUTION_FIX_NESTED_STRUCT_ALIGNMENT.key -> \"true\")\n  )\n\n  testNestedStructsEvolution(\"array struct with extra source column not used in update\")(\n    target = \"\"\"{ \"key\": 1, \"value\": [ { \"a\": 10 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": 1, \"value\": [ { \"a\": 11, \"b\": 21 } ] }\n       { \"key\": 2, \"value\": [ { \"a\": 12, \"b\": 22 } ] }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))),\n    cond = \"t.key = s.key\",\n    clauses = update(set = \"key = 0\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": 0, \"value\": [ { \"a\": 10, \"b\": null } ] }\n       { \"key\": 2, \"value\": [ { \"a\": 12, \"b\": 22 } ] }\"\"\".stripMargin,\n    resultSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"cannot cast\",\n    confs = Seq(\n      DeltaSQLConf.DELTA_MERGE_SCHEMA_EVOLUTION_FIX_NESTED_STRUCT_ALIGNMENT.key -> \"true\")\n  )\n\n  testNestedStructsEvolution(\"nested struct with extra source column not used in update\")(\n    target = \"\"\"{ \"key\": 1, \"value\": { \"a\": { \"aa\": 1 } } }\"\"\",\n    source =\n      \"\"\"{ \"key\": 1, \"value\": { \"a\": { \"aa\": 11, \"bb\": 31 }, \"b\": 21 } }\n       { \"key\": 2, \"value\": { \"a\": { \"aa\": 12, \"bb\": 32 }, \"b\": 22 } }\"\"\".stripMargin,\n    targetSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType()\n        .add(\"aa\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType()\n        .add(\"aa\", IntegerType)\n        .add(\"bb\", IntegerType))\n        .add(\"b\", IntegerType)),\n    cond = \"t.key = s.key\",\n    clauses = update(set = \"key = 0\") :: insert(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": 0, \"value\": { \"a\": { \"aa\": 1, \"bb\": null }, \"b\": null } }\n       { \"key\": 2, \"value\": { \"a\": { \"aa\": 12, \"bb\": 32 }, \"b\": 22 } }\"\"\".stripMargin,\n    resultSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType()\n        .add(\"aa\", IntegerType)\n        .add(\"bb\", IntegerType))\n        .add(\"b\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"cannot cast\",\n    confs = Seq(\n      DeltaSQLConf.DELTA_MERGE_SCHEMA_EVOLUTION_FIX_NESTED_STRUCT_ALIGNMENT.key -> \"true\")\n  )\n}\n\ntrait MergeIntoNestedStructEvolutionUpdateOnlyTests extends MergeIntoSchemaEvolutionMixin {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import testImplicits._\n\n  // Nested Schema evolution with UPDATE alone\n  testNestedStructsEvolution(\"new nested source field not in update is ignored\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2, \"b\": 3 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    clauses = update(\"value.a = s.value.a\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2 } }\"\"\",\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2 } }\"\"\")\n\n  testNestedStructsEvolution(\"two new nested source fields with update: one added, one ignored\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 2, \"b\": 3, \"c\": 4 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)\n        .add(\"c\", IntegerType)),\n    clauses = update(\"value.b = s.value.b\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1, \"b\": 3 } }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"No such struct field\")\n\n  testNestedStructsEvolution(\"nested void columns are not allowed\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1 }, \"b\": 1 } }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 2, \"z\": null } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\",\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType))\n          .add(\"b\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\",\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"z\", NullType))),\n    clauses = update(\"*\") :: Nil,\n    expectErrorContains = \"Cannot add column `value`.`a`.`z` with type VOID\",\n    expectErrorWithoutEvolutionContains = \"All nested columns must match\")\n  for (isPartitioned <- BOOLEAN_DOMAIN)\n    testEvolution(s\"extra nested column in source - update, isPartitioned=$isPartitioned\")(\n      targetData = Seq((1, (1, 10)), (2, (2, 2000))).toDF(\"key\", \"x\")\n        .selectExpr(\"key\", \"named_struct('a', x._1, 'c', x._2) as x\"),\n      sourceData = Seq((1, (10, 100, 1000))).toDF(\"key\", \"x\")\n        .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n      clauses = update(\"*\") :: Nil,\n      expected = ((1, (10, 100, 1000)) +: (2, (2, null, 2000)) +: Nil)\n        .asInstanceOf[List[(Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"x\")\n        .selectExpr(\"key\", \"named_struct('a', x._1, 'c', x._3, 'b', x._2) as x\"),\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      partitionCols = if (isPartitioned) Seq(\"key\") else Seq.empty\n    )\n\n  testEvolution(\"extra nested column in source - update, partition on unused column\")(\n    targetData = Seq((1, 2, (1, 10)), (2, 2, (2, 2000))).toDF(\"key\", \"part\", \"x\")\n      .selectExpr(\"part\", \"key\", \"named_struct('a', x._1, 'c', x._2) as x\"),\n    sourceData = Seq((1, 2, (10, 100, 1000))).toDF(\"key\", \"part\", \"x\")\n      .selectExpr(\"key\", \"part\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    clauses = update(\"*\") :: Nil,\n    expected = ((1, 2, (10, 100, 1000)) +: (2, 2, (2, null, 2000)) +: Nil)\n      .asInstanceOf[List[(Integer, Integer, (Integer, Integer, Integer))]].toDF(\"key\", \"part\", \"x\")\n      .selectExpr(\"part\", \"key\", \"named_struct('a', x._1, 'c', x._3, 'b', x._2) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    partitionCols = Seq(\"part\")\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"extra nested column in source - update - array of struct - longer source\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30 }, \"b\": 3 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2\" }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"3\" }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"4\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": 2 }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": 3 }, { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": 4 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 3 } ] }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"extra nested column in source - update - array of struct - longer target\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 }, { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 2 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30 }, \"b\": 3 }, { \"a\": { \"x\": 40, \"y\": 30 }, \"b\": 4 }, { \"a\": { \"x\": 40, \"y\": 30 }, \"b\": 5 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": \"2\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"y\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20, \"z\": 2 }, \"b\": 2 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 3 }, { \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 4 }, { \"a\": { \"x\": 40, \"y\": 30, \"z\": null }, \"b\": 5 } ] }\"\"\".stripMargin,\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"extra nested column in source - update - nested array of struct - longer source\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 } ] }, \"b\": 1 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50 } ] }, \"b\": 3 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 }, { \"c\": 10, \"d\": \"30\", \"e\": 2 }, { \"c\": 10, \"d\": \"30\", \"e\": 3 } ] }, \"b\": 2 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"e\": 1 }, { \"c\": 10, \"d\": 30, \"e\": 2 }, { \"c\": 10, \"d\": 30, \"e\": 3 } ] }, \"b\": 2 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"e\": null } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"extra nested column in source - update - nested array of struct - longer target\")(\n    target =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 }, { \"c\": 1, \"d\": 2 } ] }, \"b\": 1 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50 }, { \"c\": 20, \"d\": 40 }, { \"c\": 20, \"d\": 60 } ] }, \"b\": 3 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\", \"e\": 1 } ] }, \"b\": \"2\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30, \"e\": 1 } ] }, \"b\": 2 } ] }\n         { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"d\": 50, \"e\": null }, { \"c\": 20, \"d\": 40, \"e\": null }, { \"c\": 20, \"d\": 60, \"e\": null } ] }, \"b\": 3 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n  // scalastyle:on line.size.limit\n\n  testEvolution(\"missing nested column in source - update\")(\n    targetData = Seq((1, (1, 10, 100)), (2, (2, 20, 200))).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    sourceData = Seq((1, (0, 0))).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'c', x._2) as x\"),\n    clauses = update(\"*\") :: Nil,\n    expected = ((1, (0, 10, 0)) +: (2, (2, 20, 200)) +: Nil).toDF(\"key\", \"x\")\n      .selectExpr(\"key\", \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\"),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\"\n  )\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"missing nested column in source - update - array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"z\": 3 }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"z\": 2 }, \"b\": \"2\" }, { \"a\": { \"x\": 10, \"z\": 2 }, \"b\": \"3\" }, { \"a\": { \"x\": 10, \"z\": 2 }, \"b\": \"4\" } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"z\": 3 }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": null, \"z\": 2 }, \"b\": 2 }, { \"a\": { \"x\": 10, \"y\": null, \"z\": 2 }, \"b\": 3 }, { \"a\": { \"x\": 10, \"y\": null, \"z\": 2 }, \"b\": 4 } ] }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\"missing nested column in source - update - array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2, \"z\": 3 }, \"b\": 1 }, { \"a\": { \"x\": 1, \"y\": 2, \"z\": 3 }, \"b\": 2 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"z\": 2 }, \"b\": \"2\" } ] }\n           { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 40, \"z\": 3 }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType).add(\"z\", IntegerType))\n          .add(\"b\", StringType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": null, \"z\": 2 }, \"b\": 2 } ] }\"\"\",\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"x\", IntegerType)\n            .add(\"y\", IntegerType)\n            .add(\"z\", IntegerType))\n          .add(\"b\", IntegerType))),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"missing nested column in source - update - nested array of struct - longer source\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"e\": 4 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": {\"y\": 20, \"x\": [ { \"c\": 10, \"e\": 1 }, { \"c\": 10, \"e\": 2 }, { \"c\": 10, \"e\": 3 } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": {\"y\": 60, \"x\":  [{ \"c\": 20, \"e\": 2 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": null, \"e\": 1 }, { \"c\": 10, \"d\": null, \"e\": 2 }, { \"c\": 10, \"d\": null, \"e\": 3} ] }, \"b\": 2 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n\n  testNestedStructsEvolution(\"missing nested column in source - update - nested array of struct - longer target\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3, \"e\": 4 }, { \"c\": 1, \"d\": 3, \"e\": 5 } ] }, \"b\": 1 } ] }\"\"\",\n    source =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"e\": 1 } ] }, \"b\": \"2\" } ] }\n          { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 60, \"x\": [ { \"c\": 20, \"e\": 2 } ] }, \"b\": \"3\" } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", StringType))),\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType)\n                .add(\"e\", IntegerType)\n            )))\n          .add(\"b\", IntegerType))),\n    clauses = update(\"*\") :: Nil,\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": null, \"e\": 1 } ] }, \"b\": 2 } ] }\"\"\",\n    expectErrorWithoutEvolutionContains = \"Cannot cast\")\n  // scalastyle:on line.size.limit\n\n  testNestedStructsEvolution(\"add non-nullable struct field to target schema\")(\n    target = \"\"\"{ \"key\": \"A\" }\"\"\",\n    source = \"\"\"{ \"key\": \"B\", \"value\": 4}\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", IntegerType, nullable = false),\n    clauses = update(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": null }\"\"\".stripMargin,\n    // Even though `value` is non-nullable in the source, it must be nullable in the target as\n    // existing rows will contain null values.\n    resultSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", IntegerType, nullable = true),\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\" }\"\"\")\n\n  testNestedStructsEvolution(\"struct in array with storeAssignmentPolicy = STRICT\")(\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1 } ] }\"\"\",\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 2 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\",\n        ArrayType(new StructType()\n          .add(\"a\", LongType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\",\n        ArrayType(new StructType()\n          .add(\"a\", IntegerType))),\n    clauses = update(\"*\") :: Nil,\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 2 } ] }\"\"\",\n    resultWithoutEvolution = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 2 } ] }\"\"\",\n    confs = Seq(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\"))\n\n  testNestedStructsEvolution(\"nested field assignment qualified with source alias\")(\n    target = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 2 } }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 3, \"t\": { \"a\": 5 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    cond = \"1 = 1\",\n    clauses = update(\"s.t.a = s.t.a\") :: Nil,\n    // Assigning to the source is just wrong and should fail.\n    result = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 5 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"cannot resolve s.t.a in UPDATE\")\n\n  testNestedStructsEvolution(\"existing top-level column assignment qualified with target alias\")(\n    target = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 2 } }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 3, \"t\": { \"a\": 5 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    cond = \"1 = 1\",\n    // This succeeds and updates 'a': the fully qualified column name 't.a' gets precedence over\n    // the unqualified struct field name '(t.)t.a' to resolve the ambiguity.\n    clauses = update(\"t.a = s.a\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": 3, \"t\": { \"a\": 2 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    resultWithoutEvolution = Seq(\"\"\"{ \"a\": 3, \"t\": { \"a\": 2 } }\"\"\"))\n\n  testNestedStructsEvolution(\"existing nested field assignment qualified with target alias\")(\n    target = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 2 } }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 3, \"t\": { \"a\": 5 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    cond = \"1 = 1\",\n    // This is unambiguous: and resolves to the struct field 't.t.a' during resolution\n    clauses = update(\"t.t.a = s.t.a\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 5 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    resultWithoutEvolution = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 5 } }\"\"\"))\n\n  testNestedStructsEvolution(\"new top-level column assignment qualified with target alias\")(\n    target = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 2 } }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 3, \"b\": 4, \"t\": { \"a\": 5, \"b\": 6 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    cond = \"1 = 1\",\n    clauses = update(\"t.b = s.b\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 2, \"b\": 4 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"No such struct field `b` in `a`\")\n\n  testNestedStructsEvolution(\"new nested field assignment qualified with target alias\")(\n    target = Seq(\"\"\"{ \"a\": 1, \"t\": { \"a\": 2 } }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 3, \"b\": 4, \"t\": { \"a\": 5, \"b\": 6 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    cond = \"1 = 1\",\n    clauses = update(\"t.t.b = s.t.b\") :: Nil,\n    // t.t.b gets resolved to source struct t, accessing nested field t.t.b which doesn't exist.\n    expectErrorContains = \"No such struct field `t` in `a`, `b`\",\n    resultSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"t\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)),\n    // t.t.b: t.t gets resolved to target t with nested field b which doesn't exist in fields (a)\n    expectErrorWithoutEvolutionContains = \"No such struct field `b` in `a`\")\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoStructEvolutionNullnessSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}\nimport com.fasterxml.jackson.module.scala.{ClassTagExtensions, DefaultScalaModule}\n\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{ArrayType, IntegerType, StructType}\n\n/** Trait containing common utility methods for struct evolution nullness tests. */\ntrait MergeIntoStructEvolutionNullnessTestUtils extends MergeHelpers {\n\n  /** Whether to preserve null source structs for struct evolution tests. */\n  protected def preserveNullSourceStructs: Boolean = true\n\n  /** Whether to preserve null source structs for UPDATE * specifically. */\n  protected def preserveNullSourceStructsUpdateStar: Boolean = true\n\n  /** Configurations for preserving null source structs. */\n  protected val preserveNullStructsConfs: Seq[(String, String)] = Seq(\n    DeltaSQLConf.DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS.key -> preserveNullSourceStructs.toString,\n    DeltaSQLConf.DELTA_MERGE_PRESERVE_NULL_SOURCE_STRUCTS_UPDATE_STAR.key ->\n      preserveNullSourceStructsUpdateStar.toString)\n\n  // `SourceType`, `TargetType` and `ActionType` assume that the source and target tables\n  // have a single top-level column `col` of struct type.\n  protected object SourceType extends Enumeration {\n    case class Val(displayName: String) extends super.Val\n    val NonNullLeaves: Val = Val(\"non-null source leaves\")\n    val NullLeaves: Val = Val(\"null source leaves\")\n    val NullNestedStruct: Val = Val(\"null source nested struct\")\n    val NullNestedArray: Val = Val(\"null source nested array\")\n    val NullNestedMap: Val = Val(\"null source nested map\")\n    val NullCol: Val = Val(\"null source col\")\n\n    def getName(sourceType: Value): String =\n      sourceType.asInstanceOf[Val].displayName\n  }\n\n  protected object TargetType extends Enumeration {\n    case class Val(displayName: String) extends super.Val\n    val NonNullLeaves: Val = Val(\"non-null target leaves\")\n    val NullLeaves: Val = Val(\"null target leaves\")\n    val NullNestedStruct: Val = Val(\"null target nested struct\")\n    val NullNestedArray: Val = Val(\"null source nested array\")\n    val NullNestedMap: Val = Val(\"null source nested map\")\n    val NullCol: Val = Val(\"null target col\")\n    val Empty: Val = Val(\"empty target\")\n\n    def getName(targetType: Value): String =\n      targetType.asInstanceOf[Val].displayName\n  }\n\n  protected object ActionType extends Enumeration {\n    case class Val(displayName: String, clause: MergeClause) extends super.Val\n    val UpdateStar: Val = Val(\"UPDATE *\", update(\"*\"))\n    val UpdateCol: Val = Val(\"UPDATE t.col = s.col\", update(\"t.col = s.col\"))\n    val UpdateColY: Val = Val(\"UPDATE t.col.y = s.col.y\", update(\"t.col.y = s.col.y\"))\n    val InsertStar: Val = Val(\"INSERT *\", insert(\"*\"))\n    val InsertCol: Val = Val(\"INSERT col\", insert(\"(key, col) VALUES (s.key, s.col)\"))\n\n    implicit def toVal(v: Value): Val = v.asInstanceOf[Val]\n\n    def getName(actionType: Value): String =\n      actionType.asInstanceOf[Val].displayName\n\n    def getClause(actionType: Value): MergeClause =\n      actionType.asInstanceOf[Val].clause\n\n    def isWholeStructAssignment(actionType: Value): Boolean =\n      Seq(ActionType.UpdateCol, ActionType.InsertStar, ActionType.InsertCol).contains(actionType)\n  }\n\n  /** Casts Any to Map[String, Any]. Returns null if `value` is null. */\n  protected def castToMap(value: Any): Map[String, Any] =\n    if (value == null) null else value.asInstanceOf[Map[String, Any]]\n\n  /** Gets a value from a map. Returns null if the map is null. */\n  protected def getNestedValue(map: Map[String, Any], key: String): Any = {\n    if (map == null) null else map(key)\n  }\n\n  /**\n   * JSON mapper that preserves null values during serialization.\n   * This is necessary because the default JsonUtils uses Include.NON_ABSENT which filters out\n   * null values, which is not what we want for the nullness tests.\n   * Uses ClassTagExtensions instead of deprecated ScalaObjectMapper for Scala 3 compatibility.\n   */\n  private val jsonMapper = {\n    val mapper = new ObjectMapper() with ClassTagExtensions\n    mapper.setSerializationInclusion(Include.ALWAYS)  // Preserve null values\n    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n    mapper.registerModule(DefaultScalaModule)\n    mapper\n  }\n\n  /**\n   * Converts a Scala object to JSON string, preserving null values.\n   * Unlike org.apache.spark.sql.util.JsonUtils.toJson(), this method uses Include.ALWAYS\n   * to ensure null values are preserved in the JSON output.\n   * Uses ClassTag instead of deprecated Manifest for Scala 3 compatibility.\n   */\n  protected def toJsonWithNulls[T: scala.reflect.ClassTag](obj: T): String = {\n    jsonMapper.writeValueAsString(obj)\n  }\n\n  /** Parses a JSON string to a Map[String, Any]. */\n  protected def fromJsonToMap(jsonStr: String): Map[String, Any] = {\n    jsonMapper.readValue[Map[String, Any]](jsonStr)\n  }\n\n  /**\n   * Determines whether the target struct should be overwritten with null.\n   *\n   * Conditions to set the target struct to NULL:\n   * - For UPDATE *, the source struct is null AND the target struct is null.\n   * - For whole-struct assignment (UPDATE col = s.col, INSERT), the source struct is null.\n   *\n   * @param sourceCol The source column value (can be null)\n   * @param targetColOpt Optional target column value corresponding to sourceCol\n   * @param actionType The action type\n   * @return true if target should be overwritten with null, false otherwise\n   */\n  protected def shouldOverwriteWithNull(\n      sourceCol: Map[String, Any],\n      targetColOpt: Option[Map[String, Any]],\n      actionType: ActionType.Value): Boolean = {\n    sourceCol == null && preserveNullSourceStructs && (\n      // `targetColOpt` being None means it's an INSERT\n      targetColOpt.isEmpty ||\n      ActionType.isWholeStructAssignment(actionType) ||\n      (actionType == ActionType.UpdateStar && preserveNullSourceStructsUpdateStar &&\n        targetColOpt.get == null)\n    )\n  }\n\n  /**\n   * Checks if null source struct preservation is enabled for UPDATE SET * operations.\n   * @return true if both preserveNullSourceStructs and preserveNullSourceStructsUpdateStar are true\n   */\n  protected def shouldPreserveNullSourceStructsForUpdateStar: Boolean = {\n    shouldPreserveNullSourceStructsForWholeStructAssignment && preserveNullSourceStructsUpdateStar\n  }\n\n  /**\n   * Checks if null source struct preservation is enabled for whole-struct assignments.\n   * @return true if preserveNullSourceStructs is true\n   */\n  protected def shouldPreserveNullSourceStructsForWholeStructAssignment: Boolean = {\n    preserveNullSourceStructs\n  }\n\n  /** Represents a struct evolution nullness test case. */\n  protected case class StructEvolutionNullnessTestCase(\n    testName: String,\n    sourceSchema: StructType,\n    targetSchema: StructType,\n    sourceData: String,\n    targetData: Seq[String],\n    actionClause: MergeClause,\n    resultSchema: StructType,\n    expectedResult: String,\n    confs: Seq[(String, String)])\n\n  /**\n   * Generates test cases for struct evolution nullness tests.\n   *\n   * @param testNamePrefix Prefix for test names\n   * @param sourceSchema Source table schema\n   * @param targetSchema Target table schema\n   * @param sourceTypes Source types to test\n   * @param updateTargetTypes Target types to use for UPDATE operations\n   * @param actionTypes Action types to test\n   * @param generateResultSchemaFn Function to determine result schema based on action type\n   * @param generateSourceRowFn Function to generate source row\n   * @param generateTargetRowFn Function to generate target row\n   * @param generateExpectedResultFn Function to generate expected result\n   */\n  protected def generateStructEvolutionNullnessTests(\n      testNamePrefix: String,\n      sourceSchema: StructType,\n      targetSchema: StructType,\n      sourceTypes: Seq[SourceType.Value],\n      updateTargetTypes: Seq[TargetType.Value],\n      actionTypes: Seq[ActionType.Value],\n      generateResultSchemaFn: ActionType.Value => StructType,\n      generateSourceRowFn: SourceType.Value => String,\n      generateTargetRowFn: TargetType.Value => Option[String],\n      generateExpectedResultFn: (String, Option[String], ActionType.Value) => String)\n  : Seq[StructEvolutionNullnessTestCase] = {\n    for {\n      actionType <- actionTypes\n      sourceType <- sourceTypes\n      // For INSERT, only use Empty target; for UPDATE, use specified target types.\n      targetType <- {\n        if (actionType == ActionType.InsertStar || actionType == ActionType.InsertCol) {\n          Seq(TargetType.Empty)\n        } else {\n          updateTargetTypes\n        }\n      }\n    } yield {\n      val sourceRowJson = generateSourceRowFn(sourceType)\n      val targetRowJsonOpt = generateTargetRowFn(targetType)\n      val expectedResultJson = generateExpectedResultFn(sourceRowJson, targetRowJsonOpt, actionType)\n\n      StructEvolutionNullnessTestCase(\n        testName =\n          s\"$testNamePrefix${SourceType.getName(sourceType)}, \" +\n          s\"${TargetType.getName(targetType)}, \" +\n          s\"${ActionType.getName(actionType)}\",\n        sourceSchema = sourceSchema,\n        targetSchema = targetSchema,\n        sourceData = sourceRowJson,\n        targetData = targetRowJsonOpt.toSeq,\n        actionClause = ActionType.getClause(actionType),\n        resultSchema = generateResultSchemaFn(actionType),\n        expectedResult = expectedResultJson,\n        confs = preserveNullStructsConfs\n      )\n    }\n  }\n}\n\n/**\n * Trait collecting tests verifying the nullness of the results for top-level struct evolution.\n */\ntrait MergeIntoTopLevelStructEvolutionNullnessTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  private val testNamePrefix = \"top-level struct - \"\n\n  private val topLevelStructTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType))\n\n  private val topLevelStructSourceSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"x\", IntegerType)\n      .add(\"y\", IntegerType))\n\n  private val topLevelStructResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)\n      .add(\"x\", IntegerType))\n\n  private def generateTopLevelStructSourceRow(sourceType: SourceType.Value): String = {\n    sourceType match {\n      case SourceType.NonNullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":1,\"y\":1}}\"\"\"\n      case SourceType.NullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":null,\"y\":null}}\"\"\"\n      case SourceType.NullCol =>\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n  }\n\n  private def generateTopLevelStructTargetRow(\n      targetType: TargetType.Value): Option[String] = {\n    targetType match {\n      case TargetType.NonNullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":2,\"z\":2}}\"\"\")\n      case TargetType.NullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\")\n      case TargetType.NullCol =>\n        Some(\"\"\"{\"key\":1,\"col\":null}\"\"\")\n      case TargetType.Empty =>\n        None\n    }\n  }\n\n  /**\n   * Generates the expected result based on `sourceRowJson`, `targetRowJsonOpt`, and `actionType`.\n   * Semantics:\n   * - UPDATE *: field-level merge, preserves target-only fields (col.z).\n   * - UPDATE t.col = s.col, INSERT: whole-struct assignment, nulls target-only fields.\n   */\n  private def generateTopLevelStructExpectedResult(\n      sourceRowJson: String,\n      targetRowJsonOpt: Option[String],\n      actionType: ActionType.Value): String = {\n    val sourceRow = fromJsonToMap(sourceRowJson)\n    val targetRowOpt = targetRowJsonOpt.map(fromJsonToMap)\n\n    val sourceCol = castToMap(sourceRow(\"col\"))\n    val targetColOpt = targetRowOpt.map(row => castToMap(row(\"col\")))\n\n    if (shouldOverwriteWithNull(sourceCol, targetColOpt, actionType)) {\n      return \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n\n    val sourceX = getNestedValue(sourceCol, \"x\")\n    val sourceY = getNestedValue(sourceCol, \"y\")\n\n    val (resultX, resultY, resultZ) = actionType match {\n      case ActionType.UpdateStar =>\n        // UPDATE SET * preserves target-only field (col.z).\n        val targetZ = getNestedValue(targetColOpt.get, \"z\")\n        (sourceX, sourceY, targetZ)\n\n      case ActionType.UpdateCol | ActionType.InsertStar | ActionType.InsertCol =>\n        // Whole-struct assignments null out target-only field (col.z).\n        (sourceX, sourceY, null)\n    }\n\n    val resultMap = Map(\n      \"key\" -> 1,\n      \"col\" -> Map(\"y\" -> resultY, \"z\" -> resultZ, \"x\" -> resultX))\n\n    toJsonWithNulls(resultMap)\n  }\n\n  private def generateTopLevelStructNullnessTests(): Seq[StructEvolutionNullnessTestCase] = {\n    generateStructEvolutionNullnessTests(\n      testNamePrefix = testNamePrefix,\n      sourceSchema = topLevelStructSourceSchema,\n      targetSchema = topLevelStructTargetSchema,\n      sourceTypes = Seq(SourceType.NonNullLeaves, SourceType.NullLeaves, SourceType.NullCol),\n      updateTargetTypes =\n        Seq(TargetType.NonNullLeaves, TargetType.NullLeaves, TargetType.NullCol),\n      actionTypes =\n        Seq(\n          ActionType.UpdateStar, ActionType.UpdateCol, ActionType.InsertStar, ActionType.InsertCol),\n      generateResultSchemaFn = _ => topLevelStructResultSchema,\n      generateSourceRowFn = generateTopLevelStructSourceRow,\n      generateTargetRowFn = generateTopLevelStructTargetRow,\n      generateExpectedResultFn = generateTopLevelStructExpectedResult\n    )\n  }\n\n  generateTopLevelStructNullnessTests().foreach { testCase =>\n    testNestedStructsEvolution(testCase.testName)(\n      target = testCase.targetData,\n      source = Seq(testCase.sourceData),\n      targetSchema = testCase.targetSchema,\n      sourceSchema = testCase.sourceSchema,\n      clauses = testCase.actionClause :: Nil,\n      result = Seq(testCase.expectedResult),\n      resultSchema = testCase.resultSchema,\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      confs = testCase.confs)\n  }\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = topLevelStructTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"\n      }\n    ),\n    resultSchema = topLevelStructResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with non-null target col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = topLevelStructTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"),\n    resultSchema = topLevelStructResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // The following tests verify that we overwrite the target struct with NULL if there\n  // are no target-only fields.\n  private val topLevelStructTargetSchemaWithoutTargetOnlyField = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"Y\", IntegerType)) // Use uppercase to verify case-insensitive comparison.\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - \" +\n        s\"UPDATE * with non-null target col without target-only field\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"Y\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = topLevelStructTargetSchemaWithoutTargetOnlyField,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"x\":null}}\"\"\"\n      }\n    ),\n    resultSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"col\", new StructType()\n        .add(\"Y\", IntegerType)\n        .add(\"x\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  private val expectedResult = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n    \"\"\"{\"key\":1,\"col\":null}\"\"\"\n  } else {\n    \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"\n  }\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - UPDATE t.col = s.col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":2,\"z\":2}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = topLevelStructTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelStructResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT *\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = topLevelStructTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = insert(\"*\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelStructResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT (key, col)\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = topLevelStructTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = insert(\"(key, col) VALUES (s.key, s.col)\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelStructResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n    s\"${testNamePrefix}non-nullable target struct becomes nullable\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":2,\"z\":2}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    // Target schema has non-nullable struct\n    targetSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"col\", new StructType()\n        .add(\"y\", IntegerType)\n        .add(\"z\", IntegerType), nullable = false),\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n      \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    } else {\n      \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"\n    }),\n    // Result schema has nullable struct\n    resultSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"col\", new StructType()\n        .add(\"y\", IntegerType)\n        .add(\"z\", IntegerType)\n        .add(\"x\", IntegerType), nullable = true),\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // Tests for multiple target-only fields to verify the original values of all target-only\n  // fields are preserved.\n  private val multiTargetOnlyFieldTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType) // target-only\n      .add(\"w\", IntegerType) // target-only\n    )\n\n  private val multiTargetOnlyFieldResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)\n      .add(\"w\", IntegerType)\n      .add(\"x\", IntegerType))\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}multiple target-only fields - UPDATE * with all target-only fields null\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":2,\"z\":null,\"w\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = multiTargetOnlyFieldTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"w\":null,\"x\":null}}\"\"\"),\n    resultSchema = multiTargetOnlyFieldResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}multiple target-only fields - UPDATE * with a non-null target-only field\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":2,\"z\":5,\"w\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = multiTargetOnlyFieldTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":5,\"w\":null,\"x\":null}}\"\"\"),\n    resultSchema = multiTargetOnlyFieldResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}multiple target-only fields - UPDATE * with non-null target-only fields\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":2,\"z\":5,\"w\":6}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = multiTargetOnlyFieldTargetSchema,\n    sourceSchema = topLevelStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":5,\"w\":6,\"x\":null}}\"\"\"),\n    resultSchema = multiTargetOnlyFieldResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n\n/**\n * Trait collecting tests verifying the nullness of the results for nested struct evolution.\n */\ntrait MergeIntoNestedStructEvolutionNullnessTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  private val testNamePrefix = \"nested struct - \"\n\n  private val nestedStructTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType))\n      .add(\"z\", new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType)))\n\n  private val nestedStructSourceSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))\n      .add(\"y\", new StructType()\n        .add(\"c\", IntegerType)\n        .add(\"d\", IntegerType)))\n\n  // Result schema for UPDATE * and UPDATE t.col = s.col: adds both col.x and col.y.c\n  private val nestedStructColEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType))\n      .add(\"z\", new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType))\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)))\n\n  // Result schema for UPDATE t.col.y = s.col.y: only adds col.y.c (no col.x)\n  private val nestedStructColYEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType))\n      .add(\"z\", new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType)))\n\n  private def generateNestedStructSourceRow(sourceType: SourceType.Value): String = {\n    sourceType match {\n      case SourceType.NonNullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":{\"a\":10,\"b\":20},\"y\":{\"c\":30,\"d\":40}}}\"\"\"\n      case SourceType.NullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":{\"a\":null,\"b\":null},\"y\":{\"c\":null,\"d\":null}}}\"\"\"\n      case SourceType.NullNestedStruct =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":null,\"y\":null}}\"\"\"\n      case SourceType.NullCol =>\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n  }\n\n  private def generateNestedStructTargetRow(\n      targetType: TargetType.Value): Option[String] = {\n    targetType match {\n      case TargetType.NonNullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":4,\"e\":5},\"z\":{\"f\":6,\"g\":7}}}\"\"\")\n      case TargetType.NullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null},\"z\":{\"f\":null,\"g\":null}}}\"\"\")\n      case TargetType.NullNestedStruct =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\")\n      case TargetType.NullCol =>\n        Some(\"\"\"{\"key\":1,\"col\":null}\"\"\")\n      case TargetType.Empty =>\n        None\n    }\n  }\n\n  /**\n   * Generates the expected result based on `sourceRowJson`, `targetRowJsonOpt`, and `actionType`.\n   * Semantics:\n   * - UPDATE *: field-level merge, preserves target-only fields.\n   * - UPDATE t.col = s.col, INSERT: whole-struct assignment, nulls target-only fields.\n   * - UPDATE t.col.y = s.col.y: whole-struct assignment, nulls target-only fields.\n   */\n  private def generateNestedStructExpectedResult(\n      sourceRowJson: String,\n      targetRowJsonOpt: Option[String],\n      actionType: ActionType.Value): String = {\n    val sourceRow = fromJsonToMap(sourceRowJson)\n    val targetRowOpt = targetRowJsonOpt.map(fromJsonToMap)\n\n    val sourceCol = castToMap(sourceRow(\"col\"))\n    val targetColOpt = targetRowOpt.map(row => castToMap(row(\"col\")))\n\n    if (shouldOverwriteWithNull(sourceCol, targetColOpt, actionType)) {\n      return \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n\n    val sourceX = castToMap(getNestedValue(sourceCol, \"x\"))\n\n    // col.x is source-only.\n    val resultXOpt: Option[Any] = actionType match {\n      case ActionType.UpdateStar =>\n        if (sourceX == null) {\n          if (shouldPreserveNullSourceStructsForUpdateStar) {\n            Some(null)\n          } else {\n            Some(Map(\"a\" -> null, \"b\" -> null))\n          }\n        } else {\n          // Keep struct as is.\n          Some(sourceX)\n        }\n\n      case ActionType.UpdateCol | ActionType.InsertStar | ActionType.InsertCol =>\n        // Whole-struct assignment: null stays null, struct stays struct.\n        Some(sourceX)\n\n      case ActionType.UpdateColY =>\n        // col.x is not added in result.\n        None\n    }\n\n    // col.y exists in both the source and target.\n    val sourceY = castToMap(getNestedValue(sourceCol, \"y\"))\n    val targetY = targetColOpt.map(col => castToMap(getNestedValue(col, \"y\")))\n    val resultY: Any = {\n      if (shouldOverwriteWithNull(sourceY, targetY, actionType)) {\n        null\n      } else {\n        val sourceD = getNestedValue(sourceY, \"d\")\n        val sourceC = getNestedValue(sourceY, \"c\")\n        actionType match {\n          case ActionType.UpdateStar =>\n            // Update * preserve target-only field (col.y.e)\n            val targetE = getNestedValue(targetY.get, \"e\")\n            Map(\"d\" -> sourceD, \"e\" -> targetE, \"c\" -> sourceC)\n\n          case ActionType.UpdateCol | ActionType.UpdateColY |\n               ActionType.InsertStar | ActionType.InsertCol =>\n            // Whole-struct assignment nulls out target-only field (col.y.e).\n            if (sourceY == null && shouldPreserveNullSourceStructsForWholeStructAssignment) {\n              null\n            } else {\n              Map(\"d\" -> sourceD, \"e\" -> null, \"c\" -> sourceC)\n            }\n        }\n      }\n    }\n\n    // col.z is target-only.\n    val resultZ: Any = actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateColY =>\n        val targetCol = targetColOpt.get\n        val targetZ = castToMap(getNestedValue(targetCol, \"z\"))\n        // UPDATE * preserves target-only field (col.z);\n        // UPDATE col.y = s.col.y does not change t.col.z.\n        targetZ\n\n      case ActionType.UpdateCol | ActionType.InsertStar | ActionType.InsertCol =>\n        // Whole-struct assignment nulls out target-only field (col.z).\n        null\n    }\n\n    val colMap = resultXOpt match {\n      case Some(resultX) =>\n        Map(\"y\" -> resultY, \"z\" -> resultZ, \"x\" -> resultX)\n      case None =>\n        Map(\"y\" -> resultY, \"z\" -> resultZ)\n    }\n\n    val resultMap = Map(\"key\" -> 1, \"col\" -> colMap)\n    toJsonWithNulls(resultMap)\n  }\n\n  private def getNestedStructResultSchema(actionType: ActionType.Value): StructType = {\n    actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateCol |\n           ActionType.InsertStar | ActionType.InsertCol =>\n        nestedStructColEvolutionResultSchema\n      case ActionType.UpdateColY =>\n        nestedStructColYEvolutionResultSchema\n    }\n  }\n\n  private def generateNestedStructNullnessTests(): Seq[StructEvolutionNullnessTestCase] = {\n    generateStructEvolutionNullnessTests(\n      testNamePrefix = testNamePrefix,\n      sourceSchema = nestedStructSourceSchema,\n      targetSchema = nestedStructTargetSchema,\n      sourceTypes =\n        Seq(\n          SourceType.NonNullLeaves,\n          SourceType.NullLeaves,\n          SourceType.NullNestedStruct,\n          SourceType.NullCol),\n      updateTargetTypes =\n        Seq(\n          TargetType.NonNullLeaves,\n          TargetType.NullLeaves,\n          TargetType.NullNestedStruct,\n          TargetType.NullCol),\n      actionTypes =\n        Seq(\n          ActionType.UpdateStar,\n          ActionType.UpdateCol,\n          ActionType.UpdateColY,\n          ActionType.InsertStar,\n          ActionType.InsertCol),\n      generateResultSchemaFn = getNestedStructResultSchema,\n      generateSourceRowFn = generateNestedStructSourceRow,\n      generateTargetRowFn = generateNestedStructTargetRow,\n      generateExpectedResultFn = generateNestedStructExpectedResult\n    )\n  }\n\n  generateNestedStructNullnessTests().foreach { testCase =>\n    testNestedStructsEvolution(testCase.testName)(\n      target = testCase.targetData,\n      source = Seq(testCase.sourceData),\n      targetSchema = testCase.targetSchema,\n      sourceSchema = testCase.sourceSchema,\n      clauses = testCase.actionClause :: Nil,\n      result = Seq(testCase.expectedResult),\n      resultSchema = testCase.resultSchema,\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      confs = testCase.confs)\n  }\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchema,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"z\":null,\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\n    s\"${testNamePrefix}null expansion - UPDATE * with null target nested structs\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchema,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"z\":null,\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target leaves\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null},\"z\":{\"f\":null,\"g\":null}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchema,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"z\":{\"f\":null,\"g\":null},\"x\":null}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"z\":{\"f\":null,\"g\":null},\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // The following tests verify that we don't overwrite the target if the target struct\n  // has extra nested fields (t.col.y.e) and the target is not NULL.\n  private val nestedStructTargetSchemaWithoutZ = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType))) // col.y.e is target-only\n\n  private val nestedStructResultSchemaWithoutZ = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType))\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)))\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target nested structs without col.z\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchemaWithoutZ,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"x\":null}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructResultSchemaWithoutZ,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target leaves without col.z\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchemaWithoutZ,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"x\":null}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructResultSchemaWithoutZ,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // The following tests verify that we overwrite the target struct with NULL if the\n  // target has no extra fields.\n  private val nestedStructTargetSchemaWithoutTargetOnlyFields = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)))\n\n  private val nestedStructResultSchemaWithoutTargetOnlyFields = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"c\", IntegerType))\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)))\n\n  testNestedStructsEvolution(\n    s\"${testNamePrefix}null expansion - UPDATE * with null target without target-only fields\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchemaWithoutTargetOnlyFields,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"c\":null},\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructResultSchemaWithoutTargetOnlyFields,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target nested structs without target-only fields\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchemaWithoutTargetOnlyFields,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"c\":null},\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructResultSchemaWithoutTargetOnlyFields,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target leaves without target-only fields\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchemaWithoutTargetOnlyFields,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"c\":null},\"x\":{\"a\":null,\"b\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedStructResultSchemaWithoutTargetOnlyFields,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n  // scalastyle:on line.size.limit\n\n  private val expectedResult = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n    \"\"\"{\"key\":1,\"col\":null}\"\"\"\n  } else {\n    \"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":null,\"e\":null,\"c\":null},\"z\":null,\"x\":null}}\"\"\"\n  }\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - UPDATE t.col = s.col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":{\"d\":2,\"e\":2},\"z\":{\"f\":2,\"g\":2}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchema,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedStructColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT *\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchema,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = insert(\"*\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedStructColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT (key, col)\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedStructTargetSchema,\n    sourceSchema = nestedStructSourceSchema,\n    clauses = insert(\"(key, col) VALUES (s.key, s.col)\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedStructColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n\n/**\n * Trait collecting tests verifying the nullness of the results for array-of-struct evolution.\n */\ntrait MergeIntoTopLevelArrayStructEvolutionNullnessTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import org.apache.spark.sql.types.ArrayType\n\n  private val testNamePrefix = \"top-level array-of-struct - \"\n\n  private val topLevelArrayStructTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", ArrayType(new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)))\n\n  private val topLevelArrayStructSourceSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", ArrayType(new StructType()\n      .add(\"x\", IntegerType)\n      .add(\"y\", IntegerType)))\n\n  // Result schema: adds col[].x\n  private val topLevelArrayStructEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", ArrayType(new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)\n      .add(\"x\", IntegerType)))\n\n  private def generateTopLevelArrayStructSourceRow(\n      sourceType: SourceType.Value): String = {\n    sourceType match {\n      case SourceType.NonNullLeaves =>\n        \"\"\"{\"key\":1,\"col\":[{\"x\":10,\"y\":20},{\"x\":30,\"y\":40}]}\"\"\"\n      case SourceType.NullLeaves =>\n        \"\"\"{\"key\":1,\"col\":[{\"x\":null,\"y\":null},{\"x\":null,\"y\":null}]}\"\"\"\n      case SourceType.NullNestedStruct =>\n        \"\"\"{\"key\":1,\"col\":[null,null]}\"\"\"\n      case SourceType.NullCol =>\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n  }\n\n  private def generateTopLevelArrayStructTargetRow(\n      targetType: TargetType.Value): Option[String] = {\n    targetType match {\n      case TargetType.NonNullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":[{\"y\":2,\"z\":3},{\"y\":4,\"z\":5}]}\"\"\")\n      case TargetType.Empty =>\n        None\n    }\n  }\n\n  /**\n   * Generates the expected result based on `sourceRowJson`, `targetRowJsonOpt`, and `actionType`.\n   * Semantics: Entire arrays are overwritten, and structs within the array evolve.\n   * Note: `targetRowJsonOpt` and `actionType` are not used since arrays are always overwritten.\n   *       They are added to match the data type of\n   *       `generateStructEvolutionNullnessTests.generateExpectedResultFn`.\n   *\n   */\n  private def generateTopLevelArrayStructExpectedResult(\n      sourceRowJson: String,\n      targetRowJsonOpt: Option[String],\n      actionType: ActionType.Value): String = {\n    val sourceRow = fromJsonToMap(sourceRowJson)\n\n    val sourceCol = sourceRow(\"col\")\n    val resultCol = if (sourceCol == null) {\n      null\n    } else {\n      val sourceArray = sourceCol.asInstanceOf[Seq[Any]]\n      sourceArray.map { elem =>\n        if (elem == null) {\n          if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n            null\n          } else {\n            Map(\"y\" -> null, \"z\" -> null, \"x\" -> null)\n          }\n        } else {\n          val sourceStruct = elem.asInstanceOf[Map[String, Any]]\n          Map(\n            \"y\" -> sourceStruct(\"y\"),\n            // Target-only field `z` in array element structs is added as null.\n            \"z\" -> null,\n            \"x\" -> sourceStruct(\"x\")\n          )\n        }\n      }\n    }\n\n    val resultMap = Map(\"key\" -> 1, \"col\" -> resultCol)\n    toJsonWithNulls(resultMap)\n  }\n\n  /**\n   * Generates test cases for combinations of source type, target type, and action type.\n   */\n  private def generateTopLevelArrayStructNullnessTests(): Seq[StructEvolutionNullnessTestCase] = {\n    generateStructEvolutionNullnessTests(\n      testNamePrefix = testNamePrefix,\n      sourceSchema = topLevelArrayStructSourceSchema,\n      targetSchema = topLevelArrayStructTargetSchema,\n      sourceTypes = Seq(\n        SourceType.NonNullLeaves,\n        SourceType.NullLeaves,\n        SourceType.NullNestedStruct,\n        SourceType.NullCol),\n      updateTargetTypes = Seq(TargetType.NonNullLeaves),\n      actionTypes = Seq(\n        ActionType.UpdateStar,\n        ActionType.UpdateCol,\n        ActionType.InsertStar,\n        ActionType.InsertCol),\n      generateResultSchemaFn = _ => topLevelArrayStructEvolutionResultSchema,\n      generateSourceRowFn = generateTopLevelArrayStructSourceRow,\n      generateTargetRowFn = generateTopLevelArrayStructTargetRow,\n      generateExpectedResultFn = generateTopLevelArrayStructExpectedResult\n    )\n  }\n\n  generateTopLevelArrayStructNullnessTests().foreach { testCase =>\n    testNestedStructsEvolution(testCase.testName)(\n      target = testCase.targetData,\n      source = Seq(testCase.sourceData),\n      targetSchema = testCase.targetSchema,\n      sourceSchema = testCase.sourceSchema,\n      clauses = testCase.actionClause :: Nil,\n      result = Seq(testCase.expectedResult),\n      resultSchema = testCase.resultSchema,\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      confs = testCase.confs)\n  }\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE *\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":[{\"y\":2,\"z\":2}]}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":[null]}\"\"\"),\n    targetSchema = topLevelArrayStructTargetSchema,\n    sourceSchema = topLevelArrayStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n        \"\"\"{\"key\":1,\"col\":[null]}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":[{\"y\":null,\"z\":null,\"x\":null}]}\"\"\"\n      }\n    ),\n    resultSchema = topLevelArrayStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  private val expectedResult = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n    \"\"\"{\"key\":1,\"col\":[null]}\"\"\"\n  } else {\n    \"\"\"{\"key\":1,\"col\":[{\"y\":null,\"z\":null,\"x\":null}]}\"\"\"\n  }\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - UPDATE t.col = s.col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":[{\"y\":2,\"z\":2}]}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":[null]}\"\"\"),\n    targetSchema = topLevelArrayStructTargetSchema,\n    sourceSchema = topLevelArrayStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelArrayStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT *\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":[null]}\"\"\"),\n    targetSchema = topLevelArrayStructTargetSchema,\n    sourceSchema = topLevelArrayStructSourceSchema,\n    clauses = insert(\"*\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelArrayStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT (key, col)\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":[null]}\"\"\"),\n    targetSchema = topLevelArrayStructTargetSchema,\n    sourceSchema = topLevelArrayStructSourceSchema,\n    clauses = insert(\"(key, col) VALUES (s.key, s.col)\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelArrayStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n\n/**\n * Trait collecting tests verifying the nullness of the results for nested array-of-struct\n * evolution.\n */\ntrait MergeIntoNestedArrayStructEvolutionNullnessTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import org.apache.spark.sql.types.ArrayType\n\n  private val testNamePrefix = \"nested array-of-struct - \"\n\n  // Nested arrays: col is a struct containing multiple array-of-struct fields.\n  private val nestedArrayStructTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", ArrayType(new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)))\n      .add(\"z\", ArrayType(new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType))))\n\n  private val nestedArrayStructSourceSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"x\", ArrayType(new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)))\n      .add(\"y\", ArrayType(new StructType()\n        .add(\"c\", IntegerType)\n        .add(\"d\", IntegerType))))\n\n  // Result schema for UPDATE * and UPDATE t.col = s.col: adds both col.x and col.y[].c.\n  private val nestedArrayColEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", ArrayType(new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType)))\n      .add(\"z\", ArrayType(new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType)))\n      .add(\"x\", ArrayType(new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))))\n\n  // Result schema for UPDATE t.col.y = s.col.y: only adds col.y[].c (no col.x).\n  private val nestedArrayColYEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", ArrayType(new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType)))\n      .add(\"z\", ArrayType(new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType))))\n\n  private def generateNestedArraySourceRow(sourceType: SourceType.Value): String = {\n    sourceType match {\n      case SourceType.NonNullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":[{\"a\":10,\"b\":20}],\"y\":[{\"c\":30,\"d\":40}]}}\"\"\"\n      case SourceType.NullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":[{\"a\":null,\"b\":null}],\"y\":[{\"c\":null,\"d\":null}]}}\"\"\"\n      case SourceType.NullNestedStruct =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":[null],\"y\":[null]}}\"\"\"\n      case SourceType.NullNestedArray =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":null,\"y\":null}}\"\"\"\n      case SourceType.NullCol =>\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n  }\n\n  private def generateNestedArrayTargetRow(\n      targetType: TargetType.Value): Option[String] = {\n    targetType match {\n      case TargetType.NonNullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":[{\"d\":4,\"e\":5}],\"z\":[{\"f\":6,\"g\":7}]}}\"\"\")\n      case TargetType.NullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":[{\"d\":null,\"e\":null}],\"z\":[{\"f\":null,\"g\":null}]}}\"\"\")\n      case TargetType.NullNestedStruct =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":[null],\"z\":[null]}}\"\"\")\n      case TargetType.NullNestedArray =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\")\n      case TargetType.NullCol =>\n        Some(\"\"\"{\"key\":1,\"col\":null}\"\"\")\n      case TargetType.Empty =>\n        None\n    }\n  }\n\n  /**\n   * Generates the expected result based on `sourceRowJson`, `targetRowJsonOpt`, and `actionType`.\n   * Semantics: col struct evolves, nested arrays are overwritten, and structs within the\n   * array evolve.\n   */\n  private def generateNestedArrayExpectedResult(\n      sourceRowJson: String,\n      targetRowJsonOpt: Option[String],\n      actionType: ActionType.Value): String = {\n    val sourceRow = fromJsonToMap(sourceRowJson)\n    val targetRowOpt = targetRowJsonOpt.map(fromJsonToMap)\n\n    val sourceCol = castToMap(sourceRow(\"col\"))\n    val targetColOpt = targetRowOpt.map(row => castToMap(row(\"col\")))\n\n    if (shouldOverwriteWithNull(sourceCol, targetColOpt, actionType)) {\n      return \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n\n    // col.x is source-only.\n    val sourceX = getNestedValue(sourceCol, \"x\")\n    val resultXOpt: Option[Any] = actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateCol |\n           ActionType.InsertStar | ActionType.InsertCol =>\n        // col.x is kept as is.\n        Some(sourceX)\n\n      case ActionType.UpdateColY =>\n        // col.x is not added in the result schema.\n        None\n    }\n\n    val sourceY = getNestedValue(sourceCol, \"y\")\n    // col.y exists in both the source and target.\n    // Semantics: replace entire array and evolve struct within the array.\n    val resultY: Any = if (sourceY == null) {\n      null\n    } else {\n      val sourceYArray = sourceY.asInstanceOf[Seq[Any]]\n      sourceYArray.map { elem =>\n        if (elem == null) {\n          if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n            null\n          } else {\n            Map(\"d\" -> null, \"e\" -> null, \"c\" -> null)\n          }\n        } else {\n          val sourceYStruct = elem.asInstanceOf[Map[String, Any]]\n          Map(\n            \"d\" -> sourceYStruct(\"d\"),\n            // Target-only field `e` in array element structs is added as null.\n            \"e\" -> null,\n            \"c\" -> sourceYStruct(\"c\")\n          )\n        }\n      }\n    }\n\n    // col.z is target-only.\n    val resultZ: Any = actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateColY =>\n        val targetCol = targetColOpt.get\n        val targetZ = getNestedValue(targetCol, \"z\")\n        // UPDATE * preserves target-only field (col.z).\n        // UPDATE col.y = s.col.y preserves fields not in assignment (col.z).\n        targetZ\n\n      case ActionType.UpdateCol | ActionType.InsertStar | ActionType.InsertCol =>\n        // Whole-struct assignment nulls target-only field (col.z).\n        null\n    }\n\n    val colMap = resultXOpt match {\n      case Some(resultX) =>\n        Map(\"y\" -> resultY, \"z\" -> resultZ, \"x\" -> resultX)\n      case None =>\n        Map(\"y\" -> resultY, \"z\" -> resultZ)\n    }\n\n    val resultMap = Map(\"key\" -> 1, \"col\" -> colMap)\n    toJsonWithNulls(resultMap)\n  }\n\n  private def getNestedArrayResultSchema(actionType: ActionType.Value): StructType = {\n    actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateCol |\n           ActionType.InsertStar | ActionType.InsertCol =>\n        nestedArrayColEvolutionResultSchema\n      case ActionType.UpdateColY =>\n        nestedArrayColYEvolutionResultSchema\n    }\n  }\n\n  /**\n   * Generates test cases for combinations of source type, target type, and action type.\n   */\n  private def generateNestedArrayStructNullnessTests(): Seq[StructEvolutionNullnessTestCase] = {\n    generateStructEvolutionNullnessTests(\n      testNamePrefix = testNamePrefix,\n      sourceSchema = nestedArrayStructSourceSchema,\n      targetSchema = nestedArrayStructTargetSchema,\n      sourceTypes = Seq(\n        SourceType.NonNullLeaves,\n        SourceType.NullLeaves,\n        SourceType.NullNestedStruct,\n        SourceType.NullNestedArray,\n        SourceType.NullCol),\n      updateTargetTypes = Seq(\n        TargetType.NonNullLeaves,\n        TargetType.NullLeaves,\n        TargetType.NullNestedStruct,\n        TargetType.NullNestedArray,\n        TargetType.NullCol),\n      actionTypes = Seq(\n        ActionType.UpdateStar,\n        ActionType.UpdateCol,\n        ActionType.UpdateColY,\n        ActionType.InsertStar,\n        ActionType.InsertCol),\n      generateResultSchemaFn = getNestedArrayResultSchema,\n      generateSourceRowFn = generateNestedArraySourceRow,\n      generateTargetRowFn = generateNestedArrayTargetRow,\n      generateExpectedResultFn = generateNestedArrayExpectedResult\n    )\n  }\n\n  generateNestedArrayStructNullnessTests().foreach { testCase =>\n    testNestedStructsEvolution(testCase.testName)(\n      target = testCase.targetData,\n      source = Seq(testCase.sourceData),\n      targetSchema = testCase.targetSchema,\n      sourceSchema = testCase.sourceSchema,\n      clauses = testCase.actionClause :: Nil,\n      result = Seq(testCase.expectedResult),\n      resultSchema = testCase.resultSchema,\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      confs = testCase.confs)\n  }\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target col struct\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedArrayStructTargetSchema,\n    sourceSchema = nestedArrayStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"\n      }\n    ),\n    resultSchema = nestedArrayColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with non-null target col struct\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedArrayStructTargetSchema,\n    sourceSchema = nestedArrayStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"),\n    resultSchema = nestedArrayColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null structs nested in source array\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":[{\"d\":1,\"e\":2},{\"d\":null,\"e\":null}],\"z\":[null]}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":{\"x\":[null],\"y\":[null]}}\"\"\"),\n    targetSchema = nestedArrayStructTargetSchema,\n    sourceSchema = nestedArrayStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      // The original array value should be overwritten by source.\n      if (preserveNullSourceStructs) {\n        \"\"\"{\"key\":1,\"col\":{\"y\":[null],\"z\":[null],\"x\":[null]}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":[{\"d\":null,\"e\":null,\"c\":null}],\"z\":[null],\"x\":[null]}}\"\"\"\n      }\n    ),\n    resultSchema = nestedArrayColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n  // scalastyle:on line.size.limit\n\n  private val expectedResult = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n    \"\"\"{\"key\":1,\"col\":null}\"\"\"\n  } else {\n    \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"\n  }\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - UPDATE t.col = s.col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":[{\"d\":2,\"e\":2}],\"z\":[{\"f\":2,\"g\":2}]}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedArrayStructTargetSchema,\n    sourceSchema = nestedArrayStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedArrayColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT *\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedArrayStructTargetSchema,\n    sourceSchema = nestedArrayStructSourceSchema,\n    clauses = insert(\"*\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedArrayColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT (key, col)\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedArrayStructTargetSchema,\n    sourceSchema = nestedArrayStructSourceSchema,\n    clauses = insert(\"(key, col) VALUES (s.key, s.col)\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedArrayColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n\n/**\n * Trait collecting tests verifying the nullness of the results for map-of-struct evolution.\n */\ntrait MergeIntoTopLevelMapStructEvolutionNullnessTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import org.apache.spark.sql.types.{MapType, StringType}\n\n  private val testNamePrefix = \"top-level map-of-struct - \"\n\n  private val topLevelMapStructTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", MapType(StringType, new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)))\n\n  private val topLevelMapStructSourceSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", MapType(StringType, new StructType()\n      .add(\"x\", IntegerType)\n      .add(\"y\", IntegerType)))\n\n  // Result schema: adds col{}.x\n  private val topLevelMapStructEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", MapType(StringType, new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)\n      .add(\"x\", IntegerType)))\n\n  private def generateTopLevelMapStructSourceRow(\n      sourceType: SourceType.Value): String = {\n    sourceType match {\n      case SourceType.NonNullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"k1\":{\"x\":10,\"y\":20},\"k2\":{\"x\":30,\"y\":40}}}\"\"\"\n      case SourceType.NullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"k1\":{\"x\":null,\"y\":null},\"k2\":{\"x\":null,\"y\":null}}}\"\"\"\n      case SourceType.NullNestedStruct =>\n        \"\"\"{\"key\":1,\"col\":{\"k1\":null,\"k2\":null}}\"\"\"\n      case SourceType.NullCol =>\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n  }\n\n  private def generateTopLevelMapStructTargetRow(\n      targetType: TargetType.Value): Option[String] = {\n    targetType match {\n      case TargetType.NonNullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"k2\":{\"y\":2,\"z\":3},\"k3\":{\"y\":4,\"z\":5}}}\"\"\")\n      case TargetType.Empty =>\n        None\n    }\n  }\n\n  /**\n   * Generates the expected result based on `sourceRowJson`, `targetRowJsonOpt`, and `actionType`.\n   * Semantics: Entire maps are overwritten, and structs within the map evolve.\n   * Note: `targetRowJsonOpt` and `actionType` are not used since maps are always overwritten.\n   *       They are added to match the data type of\n   *       `generateStructEvolutionNullnessTests.generateExpectedResultFn`.\n   *\n   */\n  private def generateTopLevelMapStructExpectedResult(\n      sourceRowJson: String,\n      targetRowJsonOpt: Option[String],\n      actionType: ActionType.Value): String = {\n    val sourceRow = fromJsonToMap(sourceRowJson)\n\n    val sourceCol = sourceRow(\"col\")\n    val resultCol = if (sourceCol == null) {\n      null\n    } else {\n      val sourceMap = sourceCol.asInstanceOf[Map[String, Any]]\n      sourceMap.map { case (key, value) =>\n        val resultValue = if (value == null) {\n          if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n            null\n          } else {\n            Map(\"y\" -> null, \"z\" -> null, \"x\" -> null)\n          }\n        } else {\n          val sourceStruct = value.asInstanceOf[Map[String, Any]]\n          Map(\n            \"y\" -> sourceStruct(\"y\"),\n            // Target-only field `z` in map value structs is added as null.\n            \"z\" -> null,\n            \"x\" -> sourceStruct(\"x\")\n          )\n        }\n        key -> resultValue\n      }\n    }\n\n    val resultMap = Map(\"key\" -> 1, \"col\" -> resultCol)\n    toJsonWithNulls(resultMap)\n  }\n\n  /**\n   * Generates test cases for combinations of source type, target type, and action type.\n   */\n  private def generateTopLevelMapStructNullnessTests(): Seq[StructEvolutionNullnessTestCase] = {\n    generateStructEvolutionNullnessTests(\n      testNamePrefix = testNamePrefix,\n      sourceSchema = topLevelMapStructSourceSchema,\n      targetSchema = topLevelMapStructTargetSchema,\n      sourceTypes = Seq(\n        SourceType.NonNullLeaves,\n        SourceType.NullLeaves,\n        SourceType.NullNestedStruct,\n        SourceType.NullCol),\n      updateTargetTypes = Seq(TargetType.NonNullLeaves),\n      actionTypes = Seq(\n        ActionType.UpdateStar,\n        ActionType.UpdateCol,\n        ActionType.InsertStar,\n        ActionType.InsertCol),\n      generateResultSchemaFn = _ => topLevelMapStructEvolutionResultSchema,\n      generateSourceRowFn = generateTopLevelMapStructSourceRow,\n      generateTargetRowFn = generateTopLevelMapStructTargetRow,\n      generateExpectedResultFn = generateTopLevelMapStructExpectedResult\n    )\n  }\n\n  generateTopLevelMapStructNullnessTests().foreach { testCase =>\n    testNestedStructsEvolution(testCase.testName)(\n      target = testCase.targetData,\n      source = Seq(testCase.sourceData),\n      targetSchema = testCase.targetSchema,\n      sourceSchema = testCase.sourceSchema,\n      clauses = testCase.actionClause :: Nil,\n      result = Seq(testCase.expectedResult),\n      resultSchema = testCase.resultSchema,\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      confs = testCase.confs)\n  }\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE *\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"k1\":{\"y\":2,\"z\":2}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":{\"k1\":null}}\"\"\"),\n    targetSchema = topLevelMapStructTargetSchema,\n    sourceSchema = topLevelMapStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n        \"\"\"{\"key\":1,\"col\":{\"k1\":null}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"k1\":{\"y\":null,\"z\":null,\"x\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = topLevelMapStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  private val expectedResult = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n    \"\"\"{\"key\":1,\"col\":{\"k1\":null}}\"\"\"\n  } else {\n    \"\"\"{\"key\":1,\"col\":{\"k1\":{\"y\":null,\"z\":null,\"x\":null}}}\"\"\"\n  }\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - UPDATE t.col = s.col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"k1\":{\"y\":2,\"z\":2}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":{\"k1\":null}}\"\"\"),\n    targetSchema = topLevelMapStructTargetSchema,\n    sourceSchema = topLevelMapStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelMapStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT *\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":{\"k1\":null}}\"\"\"),\n    targetSchema = topLevelMapStructTargetSchema,\n    sourceSchema = topLevelMapStructSourceSchema,\n    clauses = insert(\"*\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelMapStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT (key, col)\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":{\"k1\":null}}\"\"\"),\n    targetSchema = topLevelMapStructTargetSchema,\n    sourceSchema = topLevelMapStructSourceSchema,\n    clauses = insert(\"(key, col) VALUES (s.key, s.col)\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = topLevelMapStructEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n\n/**\n * Trait collecting tests verifying the nullness of the results for nested map-of-struct\n * evolution.\n */\ntrait MergeIntoNestedMapStructEvolutionNullnessTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  import org.apache.spark.sql.types.{MapType, StringType}\n\n  private val testNamePrefix = \"nested map-of-struct - \"\n\n  // Nested maps: col is a struct containing multiple map-of-struct fields.\n  private val nestedMapStructTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", MapType(StringType, new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)))\n      .add(\"z\", MapType(StringType, new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType))))\n\n  private val nestedMapStructSourceSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"x\", MapType(StringType, new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType)))\n      .add(\"y\", MapType(StringType, new StructType()\n        .add(\"c\", IntegerType)\n        .add(\"d\", IntegerType))))\n\n  // Result schema for UPDATE * and UPDATE t.col = s.col: adds both col.x and col.y{}.c.\n  private val nestedMapColEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", MapType(StringType, new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType)))\n      .add(\"z\", MapType(StringType, new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType)))\n      .add(\"x\", MapType(StringType, new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))))\n\n  // Result schema for UPDATE t.col.y = s.col.y: only adds col.y{}.c (no col.x).\n  private val nestedMapColYEvolutionResultSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", MapType(StringType, new StructType()\n        .add(\"d\", IntegerType)\n        .add(\"e\", IntegerType)\n        .add(\"c\", IntegerType)))\n      .add(\"z\", MapType(StringType, new StructType()\n        .add(\"f\", IntegerType)\n        .add(\"g\", IntegerType))))\n\n  private def generateNestedMapSourceRow(sourceType: SourceType.Value): String = {\n    sourceType match {\n      case SourceType.NonNullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":{\"k1\":{\"a\":10,\"b\":20}},\"y\":{\"k1\":{\"c\":30,\"d\":40}}}}\"\"\"\n      case SourceType.NullLeaves =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":{\"k1\":{\"a\":null,\"b\":null}},\"y\":{\"k1\":{\"c\":null,\"d\":null}}}}\"\"\"\n      case SourceType.NullNestedStruct =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":{\"k1\":null},\"y\":{\"k1\":null}}}\"\"\"\n      case SourceType.NullNestedMap =>\n        \"\"\"{\"key\":1,\"col\":{\"x\":null,\"y\":null}}\"\"\"\n      case SourceType.NullCol =>\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n  }\n\n  private def generateNestedMapTargetRow(\n      targetType: TargetType.Value): Option[String] = {\n    targetType match {\n      case TargetType.NonNullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":{\"k2\":{\"d\":4,\"e\":5}},\"z\":{\"k2\":{\"f\":6,\"g\":7}}}}\"\"\")\n      case TargetType.NullLeaves =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":{\"k2\":{\"d\":null,\"e\":null}},\"z\":{\"k2\":{\"f\":null,\"g\":null}}}}\"\"\")\n      case TargetType.NullNestedStruct =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":{\"k2\":null},\"z\":{\"k2\":null}}}\"\"\")\n      case TargetType.NullNestedMap =>\n        Some(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\")\n      case TargetType.NullCol =>\n        Some(\"\"\"{\"key\":1,\"col\":null}\"\"\")\n      case TargetType.Empty =>\n        None\n    }\n  }\n\n  /**\n   * Generates the expected result based on `sourceRowJson`, `targetRowJsonOpt`, and `actionType`.\n   * Semantics: col struct evolves, nested maps are overwritten, and structs within the\n   * map evolve.\n   */\n  private def generateNestedMapExpectedResult(\n      sourceRowJson: String,\n      targetRowJsonOpt: Option[String],\n      actionType: ActionType.Value): String = {\n    val sourceRow = fromJsonToMap(sourceRowJson)\n    val targetRowOpt = targetRowJsonOpt.map(fromJsonToMap)\n\n    val sourceCol = castToMap(sourceRow(\"col\"))\n    val targetColOpt = targetRowOpt.map(row => castToMap(row(\"col\")))\n\n    if (shouldOverwriteWithNull(sourceCol, targetColOpt, actionType)) {\n      return \"\"\"{\"key\":1,\"col\":null}\"\"\"\n    }\n\n    // col.x is source-only.\n    val sourceX = getNestedValue(sourceCol, \"x\")\n    val resultXOpt: Option[Any] = actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateCol |\n           ActionType.InsertStar | ActionType.InsertCol =>\n        // col.x is kept as is.\n        Some(sourceX)\n\n      case ActionType.UpdateColY =>\n        // col.x is not added in the result schema.\n        None\n    }\n\n    val sourceY = getNestedValue(sourceCol, \"y\")\n    // col.y exists in both the source and target.\n    // Semantics: replace entire map and evolve struct within the map.\n    val resultY: Any = if (sourceY == null) {\n      null\n    } else {\n      val sourceYMap = sourceY.asInstanceOf[Map[String, Any]]\n      sourceYMap.map { case (key, value) =>\n        val resultValue = if (value == null) {\n          if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n            null\n          } else {\n            Map(\"d\" -> null, \"e\" -> null, \"c\" -> null)\n          }\n        } else {\n          val sourceYStruct = value.asInstanceOf[Map[String, Any]]\n          Map(\n            \"d\" -> sourceYStruct(\"d\"),\n            // Target-only field `e` in map value structs is added as null.\n            \"e\" -> null,\n            \"c\" -> sourceYStruct(\"c\")\n          )\n        }\n        key -> resultValue\n      }\n    }\n\n    // col.z is target-only.\n    val resultZ: Any = actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateColY =>\n        val targetCol = targetColOpt.get\n        val targetZ = getNestedValue(targetCol, \"z\")\n        // UPDATE * preserves target-only field (col.z).\n        // UPDATE col.y = s.col.y preserves fields not in assignment (col.z).\n        targetZ\n\n      case ActionType.UpdateCol | ActionType.InsertStar | ActionType.InsertCol =>\n        // Whole-struct assignment nulls target-only field (col.z).\n        null\n    }\n\n    val colMap = resultXOpt match {\n      case Some(resultX) =>\n        Map(\"y\" -> resultY, \"z\" -> resultZ, \"x\" -> resultX)\n      case None =>\n        Map(\"y\" -> resultY, \"z\" -> resultZ)\n    }\n\n    val resultMap = Map(\"key\" -> 1, \"col\" -> colMap)\n    toJsonWithNulls(resultMap)\n  }\n\n  private def getNestedMapResultSchema(actionType: ActionType.Value): StructType = {\n    actionType match {\n      case ActionType.UpdateStar | ActionType.UpdateCol |\n           ActionType.InsertStar | ActionType.InsertCol =>\n        nestedMapColEvolutionResultSchema\n      case ActionType.UpdateColY =>\n        nestedMapColYEvolutionResultSchema\n    }\n  }\n\n  /**\n   * Generates test cases for combinations of source type, target type, and action type.\n   */\n  private def generateNestedMapStructNullnessTests(): Seq[StructEvolutionNullnessTestCase] = {\n    generateStructEvolutionNullnessTests(\n      testNamePrefix = testNamePrefix,\n      sourceSchema = nestedMapStructSourceSchema,\n      targetSchema = nestedMapStructTargetSchema,\n      sourceTypes = Seq(\n        SourceType.NonNullLeaves,\n        SourceType.NullLeaves,\n        SourceType.NullNestedStruct,\n        SourceType.NullNestedMap,\n        SourceType.NullCol),\n      updateTargetTypes = Seq(\n        TargetType.NonNullLeaves,\n        TargetType.NullLeaves,\n        TargetType.NullNestedStruct,\n        TargetType.NullNestedMap,\n        TargetType.NullCol),\n      actionTypes = Seq(\n        ActionType.UpdateStar,\n        ActionType.UpdateCol,\n        ActionType.UpdateColY,\n        ActionType.InsertStar,\n        ActionType.InsertCol),\n      generateResultSchemaFn = getNestedMapResultSchema,\n      generateSourceRowFn = generateNestedMapSourceRow,\n      generateTargetRowFn = generateNestedMapTargetRow,\n      generateExpectedResultFn = generateNestedMapExpectedResult\n    )\n  }\n\n  generateNestedMapStructNullnessTests().foreach { testCase =>\n    testNestedStructsEvolution(testCase.testName)(\n      target = testCase.targetData,\n      source = Seq(testCase.sourceData),\n      targetSchema = testCase.targetSchema,\n      sourceSchema = testCase.sourceSchema,\n      clauses = testCase.actionClause :: Nil,\n      result = Seq(testCase.expectedResult),\n      resultSchema = testCase.resultSchema,\n      expectErrorWithoutEvolutionContains = \"Cannot cast\",\n      confs = testCase.confs)\n  }\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null target col struct\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedMapStructTargetSchema,\n    sourceSchema = nestedMapStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      if (shouldPreserveNullSourceStructsForUpdateStar) {\n        \"\"\"{\"key\":1,\"col\":null}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\": null,\"z\": null, \"x\": null}}\"\"\"\n      }\n    ),\n    resultSchema = nestedMapColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with non-null target col struct\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedMapStructTargetSchema,\n    sourceSchema = nestedMapStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null}}\"\"\"),\n    resultSchema = nestedMapColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  // scalastyle:off line.size.limit\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}null expansion - UPDATE * with null structs nested in source map\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":{\"k2\":{\"d\":1,\"e\":2},\"k3\":{\"d\":null,\"e\":null}},\"z\":{\"k2\":null}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":{\"x\":{\"k1\":null},\"y\":{\"k1\":null}}}\"\"\"),\n    targetSchema = nestedMapStructTargetSchema,\n    sourceSchema = nestedMapStructSourceSchema,\n    clauses = update(\"*\") :: Nil,\n    result = Seq(\n      // The original map value should be overwritten by source.\n      if (preserveNullSourceStructs) {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"k1\":null},\"z\":{\"k2\":null},\"x\":{\"k1\":null}}}\"\"\"\n      } else {\n        \"\"\"{\"key\":1,\"col\":{\"y\":{\"k1\":{\"d\":null,\"e\":null,\"c\":null}},\"z\":{\"k2\":null},\"x\":{\"k1\":null}}}\"\"\"\n      }\n    ),\n    resultSchema = nestedMapColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n  // scalastyle:on line.size.limit\n\n  private val expectedResult = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n    \"\"\"{\"key\":1,\"col\":null}\"\"\"\n  } else {\n    \"\"\"{\"key\":1,\"col\":{\"y\": null,\"z\": null, \"x\": null}}\"\"\"\n  }\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - UPDATE t.col = s.col\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":{\"y\":{\"k1\":{\"d\":2,\"e\":2}},\"z\":{\"k1\":{\"f\":2,\"g\":2}}}}\"\"\"),\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedMapStructTargetSchema,\n    sourceSchema = nestedMapStructSourceSchema,\n    clauses = update(\"t.col = s.col\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedMapColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT *\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedMapStructTargetSchema,\n    sourceSchema = nestedMapStructSourceSchema,\n    clauses = insert(\"*\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedMapColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}null expansion - INSERT (key, col)\")(\n    target = Seq.empty,\n    source = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    targetSchema = nestedMapStructTargetSchema,\n    sourceSchema = nestedMapStructSourceSchema,\n    clauses = insert(\"(key, col) VALUES (s.key, s.col)\") :: Nil,\n    result = Seq(expectedResult),\n    resultSchema = nestedMapColEvolutionResultSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n\n/**\n * Trait collecting tests with multiple MERGE clauses for struct evolution nullness behavior.\n *\n * When multiple clauses have different actions, fields excluded in one clause may still be\n * added to the final evolved schema by another clause. The tests verify the nullness of the\n * results in these scenarios.\n */\ntrait MergeIntoStructEvolutionNullnessMultiClauseTests\n    extends MergeIntoSchemaEvolutionMixin\n    with MergeIntoStructEvolutionNullnessTestUtils {\n  self: MergeIntoTestUtils with SharedSparkSession =>\n\n  private val testNamePrefix = s\"multiple clauses - \" +\n    s\"preserveNullSourceStructs=$preserveNullSourceStructs - \" +\n    s\"preserveNullSourceStructsUpdateStar=$preserveNullSourceStructsUpdateStar - \"\n\n  private val targetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType))\n\n  private val sourceSchemaWithTopLevelExtra = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"x\", IntegerType)\n      .add(\"y\", IntegerType))\n    .add(\"extra\", new StructType()\n      .add(\"val\", IntegerType)\n      .add(\"val2\", IntegerType))\n\n  private val fullyEvolvedTargetSchema = new StructType()\n    .add(\"key\", IntegerType)\n    .add(\"col\", new StructType()\n      .add(\"y\", IntegerType)\n      .add(\"z\", IntegerType)\n      .add(\"x\", IntegerType))\n    .add(\"extra\", new StructType()\n      .add(\"val\", IntegerType)\n      .add(\"val2\", IntegerType))\n\n  // The following tests cover UPDATE SET col = s.col combined with the other action\n  // which adds new column `extra` to the target schema:\n  // - UPDATE SET col = s.col, UPDATE SET extra = s.extra\n  // - UPDATE SET col = s.col, UPDATE SET extra.val = s.extra.val\n  // - UPDATE SET col = s.col, UPDATE SET *\n  // - UPDATE SET col = s.col, INSERT (key, col, extra) VALUES (s.key, s.col, s.extra)\n  // - UPDATE SET col = s.col, INSERT *\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}UPDATE SET col = s.col, UPDATE SET extra = s.extra\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\", \"\"\"{\"key\":2,\"col\":null}\"\"\"),\n    source = Seq(\n      \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n      \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n    ),\n    targetSchema = targetSchema,\n    sourceSchema = sourceSchemaWithTopLevelExtra,\n    clauses = update(condition = \"s.key = 1\", set = \"col = s.col\") ::\n              update(condition = \"s.key = 2\", set = \"extra = s.extra\") :: Nil,\n    result = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n        \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n      )\n    } else {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null,\"val2\":null}}\"\"\",\n        \"\"\"{\"key\":2,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":null}\"\"\"\n      )\n    },\n    resultSchema = fullyEvolvedTargetSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot resolve\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}UPDATE SET col = s.col, UPDATE SET extra.val = s.extra.val\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\", \"\"\"{\"key\":2,\"col\":null}\"\"\"),\n    source = Seq(\n      \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n      \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n    ),\n    targetSchema = targetSchema,\n    sourceSchema = sourceSchemaWithTopLevelExtra,\n    clauses = update(condition = \"s.key = 1\", set = \"col = s.col\") ::\n              update(condition = \"s.key = 2\", set = \"extra.val = s.extra.val\") :: Nil,\n    result = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n        \"\"\"{\"key\":2,\"col\":null,\"extra\":{\"val\":null}}\"\"\"\n      )\n    } else {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null}}\"\"\",\n        \"\"\"{\"key\":2,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null}}\"\"\"\n      )\n    },\n    resultSchema = new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"col\", new StructType()\n        .add(\"y\", IntegerType)\n        .add(\"z\", IntegerType)\n        .add(\"x\", IntegerType))\n      .add(\"extra\", new StructType()\n        .add(\"val\", IntegerType)),\n    expectErrorWithoutEvolutionContains = \"Cannot resolve\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}UPDATE SET col = s.col, UPDATE SET *\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\", \"\"\"{\"key\":2,\"col\":null}\"\"\"),\n    source = Seq(\n      \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n      \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n    ),\n    targetSchema = targetSchema,\n    sourceSchema = sourceSchemaWithTopLevelExtra,\n    clauses = update(condition = \"s.key = 1\", set = \"col = s.col\") ::\n              update(condition = \"s.key = 2\", set = \"*\") :: Nil,\n    result = if (shouldPreserveNullSourceStructsForUpdateStar) {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n        \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n      )\n    } else if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n        \"\"\"{\"key\":2,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null,\"val2\":null}}\"\"\"\n      )\n    } else {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null,\"val2\":null}}\"\"\",\n        \"\"\"{\"key\":2,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null,\"val2\":null}}\"\"\"\n      )\n    },\n    resultSchema = fullyEvolvedTargetSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(\n      s\"${testNamePrefix}UPDATE SET col = s.col, INSERT (key, col, extra)\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    source = Seq(\n      \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n      \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n    ),\n    targetSchema = targetSchema,\n    sourceSchema = sourceSchemaWithTopLevelExtra,\n    clauses = update(condition = \"s.key = 1\", set = \"col = s.col\") ::\n              insert(values = \"(key, col, extra) VALUES (s.key, s.col, s.extra)\") :: Nil,\n    result = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n        \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n      )\n    } else {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null,\"val2\":null}}\"\"\",\n        \"\"\"{\"key\":2,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":null}\"\"\"\n      )\n    },\n    resultSchema = fullyEvolvedTargetSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot resolve\",\n    confs = preserveNullStructsConfs)\n\n  testNestedStructsEvolution(s\"${testNamePrefix}UPDATE SET col = s.col, INSERT *\")(\n    target = Seq(\"\"\"{\"key\":1,\"col\":null}\"\"\"),\n    source = Seq(\n      \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n      \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n    ),\n    targetSchema = targetSchema,\n    sourceSchema = sourceSchemaWithTopLevelExtra,\n    clauses = update(condition = \"s.key = 1\", set = \"col = s.col\") ::\n              insert(values = \"*\") :: Nil,\n    result = if (shouldPreserveNullSourceStructsForWholeStructAssignment) {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":null,\"extra\":null}\"\"\",\n        \"\"\"{\"key\":2,\"col\":null,\"extra\":null}\"\"\"\n      )\n    } else {\n      Seq(\n        \"\"\"{\"key\":1,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":{\"val\":null,\"val2\":null}}\"\"\",\n        \"\"\"{\"key\":2,\"col\":{\"y\":null,\"z\":null,\"x\":null},\"extra\":null}\"\"\"\n      )\n    },\n    resultSchema = fullyEvolvedTargetSchema,\n    expectErrorWithoutEvolutionContains = \"Cannot cast\",\n    confs = preserveNullStructsConfs)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.lang.{Integer => JInt}\n\nimport scala.language.implicitConversions\n\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions, UsageRecord}\nimport org.apache.spark.sql.delta.commands.MergeIntoCommand\nimport org.apache.spark.sql.delta.commands.merge.MergeStats\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.ScanReportHelper\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.QueryContext\nimport org.apache.spark.sql.{functions, AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.{GenericInternalRow, UnsafeArrayData}\nimport org.apache.spark.sql.catalyst.plans.logical.{SubqueryAlias, View}\nimport org.apache.spark.sql.execution.adaptive.DisableAdaptiveExecution\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\ntrait MergeIntoSuiteBaseMixin\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLTestUtils\n    with ScanReportHelper\n    with MergeIntoTestUtils\n    with MergeIntoSchemaEvolutionMixin {\n  import testImplicits._\n\n  // Maps expected error classes to actual error classes. Used to handle error classes that are\n  // different when running using SQL vs. Scala.\n  protected val mappedErrorClasses: Map[String, String] = Map.empty\n\n  // scalastyle:off argcount\n  def testNestedDataSupport(name: String, namePrefix: String = \"nested data support\")(\n      source: String,\n      target: String,\n      update: Seq[String],\n      insert: String = null,\n      targetSchema: StructType = null,\n      sourceSchema: StructType = null,\n      result: String = null,\n      errorStrs: Seq[String] = null,\n      confs: Seq[(String, String)] = Seq.empty): Unit = {\n    // scalastyle:on argcount\n\n    require(result == null ^ errorStrs == null, \"either set the result or the error strings\")\n\n    val testName =\n      if (result != null) s\"$namePrefix - $name\" else s\"$namePrefix - analysis error - $name\"\n\n    test(testName) {\n      withSQLConf(confs: _*) {\n        withJsonData(source, target, targetSchema, sourceSchema) { case (sourceName, targetName) =>\n          val fieldNames = spark.table(targetName).schema.fieldNames\n          val fieldNamesStr = fieldNames.mkString(\"`\", \"`, `\", \"`\")\n          val keyName = s\"`${fieldNames.head}`\"\n\n          def execMerge() = executeMerge(\n            target = s\"$targetName t\",\n            source = s\"$sourceName s\",\n            condition = s\"s.$keyName = t.$keyName\",\n            update = update.mkString(\", \"),\n            insert = Option(insert).getOrElse(s\"($fieldNamesStr) VALUES ($fieldNamesStr)\"))\n\n          if (result != null) {\n            execMerge()\n            val expectedDf = readFromJSON(strToJsonSeq(result), targetSchema)\n            checkAnswer(spark.table(targetName), expectedDf)\n          } else {\n            val e = intercept[AnalysisException] {\n              execMerge()\n            }\n            errorStrs.foreach { s => errorContains(e.getMessage, s) }\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Test runner to cover analysis exception in MERGE INTO.\n   */\n  protected def testMergeAnalysisException(\n      name: String)(\n      mergeOn: String,\n      mergeClauses: MergeClause*)(\n      expectedErrorClass: String,\n      expectedMessageParameters: Map[String, String]): Unit = {\n    test(s\"analysis errors - $name\") {\n      withKeyValueData(\n        source = Seq.empty,\n        target = Seq.empty,\n        sourceKeyValueNames = (\"key\", \"srcValue\"),\n        targetKeyValueNames = (\"key\", \"tgtValue\")) { case (sourceName, targetName) =>\n        val ex = intercept[AnalysisException] {\n          executeMerge(s\"$targetName t\", s\"$sourceName s\", mergeOn, mergeClauses: _*)\n        }\n\n        // Spark 3.5 and below uses QueryContext, Spark 4.0 and above uses ExpectedContext.\n        // Implicitly convert to ExpectedContext when needed for compatibility.\n        implicit def toExpectedContext(ctxs: Array[QueryContext]): Array[ExpectedContext] =\n          ctxs.map { ctx =>\n            ExpectedContext(ctx.fragment(), ctx.startIndex(), ctx.stopIndex())\n          }\n\n        checkError(\n          exception = ex,\n          mappedErrorClasses.getOrElse(expectedErrorClass, expectedErrorClass),\n          parameters = expectedMessageParameters,\n          queryContext = ex.getQueryContext\n        )\n      }\n    }\n  }\n\n  protected def withJsonData(\n      source: Seq[String],\n      target: Seq[String],\n      schema: StructType = null,\n      sourceSchema: StructType = null)(\n      thunk: (String, String) => Unit): Unit = {\n\n    def toDF(strs: Seq[String]) = {\n      if (sourceSchema != null && strs == source) {\n        spark.read.schema(sourceSchema).json(strs.toDS)\n      } else if (schema != null) {\n        spark.read.schema(schema).json(strs.toDS)\n      } else {\n        spark.read.json(strs.toDS)\n      }\n    }\n    append(toDF(target), Nil)\n    withTempView(\"source\") {\n      toDF(source).createOrReplaceTempView(\"source\")\n      thunk(\"source\", tableSQLIdentifier)\n    }\n  }\n\n  protected def insertOnlyMergeFeatureFlagOff(sourceName: String, targetName: String): Unit = {\n    executeMerge(\n      tgt = s\"$targetName t\",\n      src = s\"$sourceName s\",\n      cond = \"s.key = t.key\",\n      insert(values = \"(key, value) VALUES (s.key, s.value)\"))\n\n    checkAnswer(sql(s\"SELECT key, value FROM $targetName\"),\n      Row(1, 1) :: Row(3, 30) :: Nil)\n\n    val metrics = spark.sql(s\"DESCRIBE HISTORY $targetName LIMIT 1\")\n      .select(\"operationMetrics\")\n      .collect().head.getMap(0).asInstanceOf[Map[String, String]]\n    assert(metrics.contains(\"numTargetFilesRemoved\"))\n    // If insert-only code path is not used, then the general code path will rewrite existing\n    // target files when DVs are not enabled.\n    if (!spark.conf.get(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS)) {\n      assert(metrics(\"numTargetFilesRemoved\").toInt > 0)\n    }\n  }\n\n  def testMergeWithRepartition(\n      name: String,\n      partitionColumns: Seq[String],\n      srcRange: Range,\n      expectLessFilesWithRepartition: Boolean,\n      clauses: MergeClause*): Unit = {\n    test(s\"merge with repartition - $name\",\n      DisableAdaptiveExecution(\"AQE coalese would partition number\")) {\n      withTempView(\"source\") {\n        withTempDir { basePath =>\n          val tgt1 = basePath + \"target\"\n          val tgt2 = basePath + \"targetRepartitioned\"\n\n          val df = spark.range(100).withColumn(\"part1\", 'id % 5).withColumn(\"part2\", 'id % 3)\n          df.write.format(\"delta\").partitionBy(partitionColumns: _*).save(tgt1)\n          df.write.format(\"delta\").partitionBy(partitionColumns: _*).save(tgt2)\n          val cond = \"src.id = t.id\"\n          val src = srcRange.toDF(\"id\")\n            .withColumn(\"part1\", 'id % 5)\n            .withColumn(\"part2\", 'id % 3)\n            .createOrReplaceTempView(\"source\")\n          // execute merge without repartition\n          withSQLConf(DeltaSQLConf.MERGE_REPARTITION_BEFORE_WRITE.key -> \"false\") {\n            executeMerge(\n              tgt = s\"delta.`$tgt1` as t\",\n              src = \"source src\",\n              cond = cond,\n              clauses = clauses: _*)\n          }\n          // execute merge with repartition - default behavior\n          executeMerge(\n            tgt = s\"delta.`$tgt2` as t\",\n            src = \"source src\",\n            cond = cond,\n            clauses = clauses: _*)\n          checkAnswer(\n            io.delta.tables.DeltaTable.forPath(tgt2).toDF,\n            io.delta.tables.DeltaTable.forPath(tgt1).toDF\n          )\n          val filesAfterNoRepartition = DeltaLog.forTable(spark, tgt1).snapshot.numOfFiles\n          val filesAfterRepartition = DeltaLog.forTable(spark, tgt2).snapshot.numOfFiles\n          // check if there are fewer are number of files for merge with repartition\n          if (expectLessFilesWithRepartition) {\n            assert(filesAfterNoRepartition > filesAfterRepartition)\n          } else {\n            assert(filesAfterNoRepartition === filesAfterRepartition)\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Test whether data skipping on matched predicates of a merge command is performed.\n   * @param name The name of the test case.\n   * @param source The source for merge.\n   * @param target The target for merge.\n   * @param dataSkippingOnTargetOnly The boolean variable indicates whether\n   *                                 when matched clauses are on target fields only.\n   *                                 Data Skipping should be performed before inner join if\n   *                                 this variable is true.\n   * @param isMatchedOnly The boolean variable indicates whether the merge command only\n   *                      contains when matched clauses.\n   * @param mergeClauses Merge Clauses.\n   */\n  protected def testMergeDataSkippingOnMatchPredicates(\n      name: String)(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      dataSkippingOnTargetOnly: Boolean,\n      isMatchedOnly: Boolean,\n      mergeClauses: MergeClause*)(\n      result: Seq[(Int, Int)]): Unit = {\n    test(s\"data skipping with matched predicates - $name\") {\n      withKeyValueData(source, target) { case (sourceName, targetName) =>\n        val stats = performMergeAndCollectStatsForDataSkippingOnMatchPredicates(\n          sourceName,\n          targetName,\n          result,\n          mergeClauses)\n        // Data skipping on match predicates should only be performed when it's a\n        // matched only merge.\n        if (isMatchedOnly) {\n          // The number of files removed/added should be 0 because of the additional predicates.\n          assert(stats.targetFilesRemoved == 0)\n          assert(stats.targetFilesAdded == 0)\n          // Verify that the additional predicates on data skipping\n          // before inner join filters file out for match predicates only\n          // on target.\n          if (dataSkippingOnTargetOnly) {\n            assert(stats.targetBeforeSkipping.files.get > stats.targetAfterSkipping.files.get)\n          }\n        } else {\n          if (!spark.conf.get(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS)) {\n            assert(stats.targetFilesRemoved > 0)\n          }\n          // If there is no insert clause and the flag is enabled, data skipping should be\n          // performed on targetOnly predicates.\n          // However, with insert clauses, it's expected that no additional data skipping\n          // is performed on matched clauses.\n          assert(stats.targetBeforeSkipping.files.get == stats.targetAfterSkipping.files.get)\n          assert(stats.targetRowsUpdated == 0)\n        }\n      }\n    }\n  }\n\n  protected def performMergeAndCollectStatsForDataSkippingOnMatchPredicates(\n      sourceName: String,\n      targetName: String,\n      result: Seq[(Int, Int)],\n      mergeClauses: Seq[MergeClause]): MergeStats = {\n    var events: Seq[UsageRecord] = Seq.empty\n    // Perform merge on merge condition with matched clauses.\n    events = Log4jUsageLogger.track {\n      executeMerge(s\"$targetName t\", s\"$sourceName s\", \"s.key = t.key\", mergeClauses: _*)\n    }\n\n    checkAnswer(\n      readDeltaTableByIdentifier(targetName),\n      result.map { case (k, v) => Row(k, v) })\n\n    // Verify merge stats from usage events\n    val mergeStats = events.filter { e =>\n      e.metric == MetricDefinitions.EVENT_TAHOE.name &&\n        e.tags.get(\"opType\").contains(\"delta.dml.merge.stats\")\n    }\n\n    assert(mergeStats.size == 1)\n\n    JsonUtils.fromJson[MergeStats](mergeStats.head.blob)\n  }\n\n  /**\n   * @param function the unsupported function.\n   * @param functionType The type of the unsupported expression to be tested.\n   * @param sourceData the data in the source table.\n   * @param targetData the data in the target table.\n   * @param mergeCondition the merge condition containing the unsupported expression.\n   * @param clauseCondition the clause condition containing the unsupported expression.\n   * @param clauseAction the clause action containing the unsupported expression.\n   * @param expectExceptionInAction whether expect exception thrown in action.\n   * @param customConditionErrorRegex the customized error regex for condition.\n   * @param customActionErrorRegex the customized error regex for action.\n   */\n  def testUnsupportedExpression(\n      function: String,\n      functionType: String,\n      sourceData: => DataFrame,\n      targetData: => DataFrame,\n      mergeCondition: String,\n      clauseCondition: String,\n      clauseAction: String,\n      expectExceptionInAction: Option[Boolean] = None,\n      customConditionErrorRegex: Option[String] = None,\n      customActionErrorRegex: Option[String] = None) {\n    test(s\"$functionType functions in merge\" +\n      s\" - expect exception in action: ${expectExceptionInAction.getOrElse(true)}\") {\n      withTable(\"source\", \"target\") {\n        sourceData.write.format(\"delta\").saveAsTable(\"source\")\n        targetData.write.format(\"delta\").saveAsTable(\"target\")\n\n        val expectedErrorRegex = \"(?s).*(?i)unsupported.*(?i).*Invalid expressions.*\"\n\n        def checkExpression(\n            expectException: Boolean,\n            condition: Option[String] = None,\n            clause: Option[MergeClause] = None,\n            expectedRegex: Option[String] = None) {\n          if (expectException) {\n            val dataBeforeException = spark.read.format(\"delta\").table(\"target\").collect()\n            val e = intercept[Exception] {\n              executeMerge(\n                tgt = \"target as t\",\n                src = \"source as s\",\n                cond = condition.getOrElse(\"s.a = t.a\"),\n                clause.getOrElse(update(set = \"b = s.b\"))\n              )\n            }\n\n            def extractErrorClass(e: Throwable): String =\n              e match {\n                case dt: DeltaThrowable => s\"\\\\[${dt.getErrorClass}\\\\] \"\n                case _ => \"\"\n              }\n\n            val (message, errorClass) = if (e.getCause != null) {\n              (e.getCause.getMessage, extractErrorClass(e.getCause))\n            } else (e.getMessage, extractErrorClass(e))\n            assert(message.matches(errorClass + expectedRegex.getOrElse(expectedErrorRegex)))\n            checkAnswer(spark.read.format(\"delta\").table(\"target\"), dataBeforeException)\n          } else {\n            executeMerge(\n              tgt = \"target as t\",\n              src = \"source as s\",\n              cond = condition.getOrElse(\"s.a = t.a\"),\n              clause.getOrElse(update(set = \"b = s.b\"))\n            )\n          }\n        }\n\n        // on merge condition\n        checkExpression(\n          expectException = true,\n          condition = Option(mergeCondition),\n          expectedRegex = customConditionErrorRegex\n        )\n\n        // on update condition\n        checkExpression(\n          expectException = true,\n          clause = Option(update(condition = clauseCondition, set = \"b = s.b\")),\n          expectedRegex = customConditionErrorRegex\n        )\n\n        // on update action\n        checkExpression(\n          expectException = expectExceptionInAction.getOrElse(true),\n          clause = Option(update(set = s\"b = $clauseAction\")),\n          expectedRegex = customActionErrorRegex\n        )\n\n        // on insert condition\n        checkExpression(\n          expectException = true,\n          clause = Option(\n            insert(values = \"(a, b, c) VALUES (s.a, s.b, s.c)\", condition = clauseCondition)),\n          expectedRegex = customConditionErrorRegex\n        )\n\n        sql(\"update source set a = 2\")\n        // on insert action\n        checkExpression(\n          expectException = expectExceptionInAction.getOrElse(true),\n          clause = Option(insert(values = s\"(a, b, c) VALUES ($clauseAction, s.b, s.c)\")),\n          expectedRegex = customActionErrorRegex\n        )\n      }\n    }\n  }\n\n  protected def testExtendedMerge(\n      name: String,\n      namePrefix: String = \"extended syntax\")(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      mergeOn: String,\n      mergeClauses: MergeClause*)(\n      result: Seq[(Int, Int)]): Unit = {\n    Seq(true, false).foreach { isPartitioned =>\n      test(s\"$namePrefix - $name - isPartitioned: $isPartitioned \") {\n        withKeyValueData(source, target, isPartitioned) { case (sourceName, targetName) =>\n          withSQLConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"true\") {\n            executeMerge(s\"$targetName t\", s\"$sourceName s\", mergeOn, mergeClauses: _*)\n          }\n          checkAnswer(\n            readDeltaTableByIdentifier(targetName),\n            result.map { case (k, v) => Row(k, v) })\n        }\n      }\n    }\n  }\n\n  protected lazy val expectedOpTypes: Set[String] = Set(\n    \"delta.dml.merge.materializeSource\",\n    \"delta.dml.merge.findTouchedFiles\",\n    \"delta.dml.merge.writeAllChanges\",\n    \"delta.dml.merge\")\n\n  protected lazy val expectedOpTypesInsertOnly: Set[String] = Set(\n    \"delta.dml.merge.materializeSource\",\n    \"delta.dml.merge.findTouchedFiles\",\n    \"delta.dml.merge.writeInsertsOnlyWhenNoMatches\",\n    \"delta.dml.merge\")\n}\n\ntrait MergeIntoBasicTests extends MergeIntoSuiteBaseMixin {\n  import testImplicits._\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic case - merge to Delta table by path, isPartitioned: $isPartitioned\",\n        NameBasedAccessIncompatible) {\n      withTable(\"source\") {\n        val partitions = if (isPartitioned) \"key2\" :: Nil else Nil\n        append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), partitions)\n        Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n        executeMerge(\n          target = tableSQLIdentifier,\n          source = \"source src\",\n          condition = \"src.key1 = key2\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n        checkAnswer(readDeltaTableByIdentifier(),\n          Row(2, 2) :: // No change\n            Row(21, 21) :: // Update\n            Row(-10, 13) :: // Insert\n            Nil)\n      }\n    }\n  }\n\n  Seq(true, false).foreach { skippingEnabled =>\n    Seq(true, false).foreach { isPartitioned =>\n     test(\"basic case - merge to Delta table, \" +\n         s\"isPartitioned: $isPartitioned skippingEnabled: $skippingEnabled\") {\n        withTable(\"source\") {\n          withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> skippingEnabled.toString) {\n            Seq((1, 1), (0, 3), (1, 6)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n            val partitions = if (isPartitioned) \"key2\" :: Nil else Nil\n            append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), partitions)\n\n            executeMerge(\n              target = s\"$tableSQLIdentifier tgt\",\n              source = \"source src\",\n              condition = \"src.key1 = key2 AND src.value < tgt.value\",\n              update = \"key2 = 20 + key1, value = 20 + src.value\",\n              insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n            checkAnswer(readDeltaTableByIdentifier(),\n              Row(2, 2) :: // No change\n                Row(21, 21) :: // Update\n                Row(-10, 13) :: // Insert\n                Row(-9, 16) :: // Insert\n                Nil)\n            }\n        }\n      }\n    }\n  }\n\n  test(\"basic case - update value from both source and target table\") {\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trgNew\",\n        source = \"source src\",\n        condition = \"src.key1 = key2\",\n        update = \"key2 = 20 + key2, value = trgNew.value + src.value\",\n        insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(21, 5) :: // Update\n          Row(-10, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"basic case - columns are specified in wrong order\") {\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trgNew\",\n        source = \"source src\",\n        condition = \"src.key1 = key2\",\n        update = \"value = trgNew.value + src.value, key2 = 20 + key2\",\n        insert = \"(value, key2) VALUES (src.value + 10, key1 - 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(21, 5) :: // Update\n          Row(-10, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"basic case - not all columns are specified in update\") {\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trgNew\",\n        source = \"source src\",\n        condition = \"src.key1 = key2\",\n        update = \"value = trgNew.value + 3\",\n        insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(1, 7) :: // Update\n          Row(-10, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"basic case - multiple inserts\") {\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      Seq((1, 1), (0, 3), (3, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier as trgNew\",\n        src = \"source src\",\n        cond = \"src.key1 = key2\",\n        insert(condition = \"key1 = 0\", values = \"(key2, value) VALUES (src.key1, src.value + 3)\"),\n        insert(values = \"(key2, value) VALUES (src.key1 - 10, src.value + 10)\"))\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(1, 4) :: // No change\n          Row(0, 6) :: // Insert\n          Row(-7, 15) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"basic case - upsert with only rows inserted\") {\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier as trgNew\",\n        src = \"source src\",\n        cond = \"src.key1 = key2\",\n        update(condition = \"key2 = 5\", set = \"value = src.value + 3\"),\n        insert(values = \"(key2, value) VALUES (src.key1 - 10, src.value + 10)\"))\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(1, 4) :: // No change\n          Row(-10, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  private def testNullCase(name: String)(\n      target: Seq[(JInt, JInt)],\n      source: Seq[(JInt, JInt)],\n      condition: String,\n      expectedResults: Seq[(JInt, JInt)]) = {\n    Seq(true, false).foreach { isPartitioned =>\n      test(s\"basic case - null handling - $name, isPartitioned: $isPartitioned\") {\n        withView(\"sourceView\") {\n          val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n          append(target.toDF(\"key\", \"value\"), partitions)\n          source.toDF(\"key\", \"value\").createOrReplaceTempView(\"sourceView\")\n\n          executeMerge(\n            target = s\"$tableSQLIdentifier as t\",\n            source = \"sourceView s\",\n            condition = condition,\n            update = \"t.value = s.value\",\n            insert = \"(t.key, t.value) VALUES (s.key, s.value)\")\n\n          checkAnswer(\n            readDeltaTableByIdentifier(),\n            expectedResults.map { r => Row(r._1, r._2) }\n          )\n        }\n      }\n    }\n  }\n\n  testNullCase(\"null value in target\")(\n    target = Seq((null, null), (1, 1)),\n    source = Seq((1, 10), (2, 20)),\n    condition = \"s.key = t.key\",\n    expectedResults = Seq(\n      (null, null),   // No change\n      (1, 10),        // Update\n      (2, 20)         // Insert\n    ))\n\n  testNullCase(\"null value in source\")(\n    target = Seq((1, 1)),\n    source = Seq((1, 10), (2, 20), (null, null)),\n    condition = \"s.key = t.key\",\n    expectedResults = Seq(\n      (1, 10),        // Update\n      (2, 20),        // Insert\n      (null, null)    // Insert\n    ))\n\n  testNullCase(\"null value in both source and target\")(\n    target = Seq((1, 1), (null, null)),\n    source = Seq((1, 10), (2, 20), (null, 0)),\n    condition = \"s.key = t.key\",\n    expectedResults = Seq(\n      (null, null),   // No change as null in source does not match null in target\n      (1, 10),        // Update\n      (2, 20),        // Insert\n      (null, 0)       // Insert\n    ))\n\n  testNullCase(\"null value in both source and target + IS NULL in condition\")(\n    target = Seq((1, 1), (null, null)),\n    source = Seq((1, 10), (2, 20), (null, 0)),\n    condition = \"s.key = t.key AND s.key IS NULL\",\n    expectedResults = Seq(\n      (null, null),   // No change as s.key != t.key\n      (1, 1),         // No change as s.key is not null\n      (null, 0),      // Insert\n      (1, 10),        // Insert\n      (2, 20)         // Insert\n    ))\n\n  testNullCase(\"null value in both source and target + IS NOT NULL in condition\")(\n    target = Seq((1, 1), (null, null)),\n    source = Seq((1, null), (2, 20), (null, 0)),\n    condition = \"s.key = t.key AND t.value IS NOT NULL\",\n    expectedResults = Seq(\n      (null, null),   // No change as t.value is null\n      (1, null),      // Update as t.value is not null\n      (null, 0),      // Insert\n      (2, 20)         // Insert\n    ))\n\n  testNullCase(\"null value in both source and target + <=> in condition\")(\n    target = Seq((1, 1), (null, null)),\n    source = Seq((1, 10), (2, 20), (null, 0)),\n    condition = \"s.key <=> t.key\",\n    expectedResults = Seq(\n      (null, 0),      // Update\n      (1, 10),        // Update\n      (2, 20)         // Insert\n    ))\n\n  testNullCase(\"NULL in condition\")(\n    target = Seq((1, 1), (null, null)),\n    source = Seq((1, 10), (2, 20), (null, 0)),\n    condition = \"s.key = t.key AND NULL\",\n    expectedResults = Seq(\n      (null, null),   // No change as NULL condition did not match anything\n      (1, 1),         // No change as NULL condition did not match anything\n      (null, 0),      // Insert\n      (1, 10),        // Insert\n      (2, 20)         // Insert\n    ))\n\n  test(\"basic case - only insert\") {\n    withTable(\"source\") {\n      Seq((5, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq.empty[(Int, Int)].toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as target\",\n        source = \"source src\",\n        condition = \"src.key1 = target.key2\",\n        update = \"key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(-5, 15) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"basic case - both source and target are empty\") {\n    withTable(\"source\") {\n      Seq.empty[(Int, Int)].toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq.empty[(Int, Int)].toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as target\",\n        source = \"source src\",\n        condition = \"src.key1 = target.key2\",\n        update = \"key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(), Nil)\n    }\n  }\n\n  test(\"basic case - only update\") {\n    withTable(\"source\") {\n      Seq((1, 5), (2, 9)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as target\",\n        source = \"source src\",\n        condition = \"src.key1 = target.key2\",\n        update = \"key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(21, 25) ::   // Update\n          Row(22, 29) :: // Update\n          Nil)\n    }\n  }\n\n  private def testLocalPredicates(name: String)(\n      target: Seq[(String, String, String)],\n      source: Seq[(String, String)],\n      condition: String,\n      expectedResults: Seq[(String, String, String)],\n      numFilesPerPartition: Int = 2) = {\n    Seq(true, false).foreach { isPartitioned =>\n      test(s\"$name, isPartitioned: $isPartitioned\") { withTable(\"source\") {\n        val partitions = if (isPartitioned) \"key2\" :: Nil else Nil\n        append(target.toDF(\"key2\", \"value\", \"op\").repartition(numFilesPerPartition), partitions)\n        source.toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n\n        // Local predicates are likely to be pushed down leading empty join conditions\n        // and cross-join being used\n        withCrossJoinEnabled { executeMerge(\n          target = s\"$tableSQLIdentifier trg\",\n          source = \"source src\",\n          condition = condition,\n          update = \"key2 = src.key1, value = src.value, op = 'update'\",\n          insert = \"(key2, value, op) VALUES (src.key1, src.value, 'insert')\")\n        }\n\n        checkAnswer(\n          readDeltaTableByIdentifier(),\n          expectedResults.map { r => Row(r._1, r._2, r._3) }\n        )\n      }\n    }}\n  }\n\n  testLocalPredicates(\"basic case - local predicates - predicate has no matches, only inserts\")(\n    target = Seq((\"2\", \"2\", \"noop\"), (\"1\", \"4\", \"noop\"), (\"3\", \"2\", \"noop\"), (\"4\", \"4\", \"noop\")),\n    source = Seq((\"1\", \"8\"), (\"0\", \"3\")),\n    condition = \"src.key1 = key2 and key2 != '1'\",\n    expectedResults =\n      (\"2\", \"2\", \"noop\") ::\n      (\"1\", \"4\", \"noop\") ::\n      (\"3\", \"2\", \"noop\") ::\n      (\"4\", \"4\", \"noop\") ::\n      (\"1\", \"8\", \"insert\") ::\n      (\"0\", \"3\", \"insert\") ::\n      Nil)\n\n  testLocalPredicates(\"basic case - local predicates - predicate has matches, updates and inserts\")(\n    target = Seq((\"1\", \"2\", \"noop\"), (\"1\", \"4\", \"noop\"), (\"3\", \"2\", \"noop\"), (\"4\", \"4\", \"noop\")),\n    source = Seq((\"1\", \"8\"), (\"0\", \"3\")),\n    condition = \"src.key1 = key2 and key2 < '3'\",\n    expectedResults =\n      (\"3\", \"2\", \"noop\") ::\n      (\"4\", \"4\", \"noop\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"0\", \"3\", \"insert\") ::\n      Nil)\n\n  testLocalPredicates(\"basic case - local predicates - predicate has matches, only updates\")(\n    target = Seq((\"1\", \"2\", \"noop\"), (\"1\", \"4\", \"noop\"), (\"3\", \"2\", \"noop\"), (\"4\", \"4\", \"noop\")),\n    source = Seq((\"1\", \"8\")),\n    condition = \"key2 < '3'\",\n    expectedResults =\n      (\"3\", \"2\", \"noop\") ::\n      (\"4\", \"4\", \"noop\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"1\", \"8\", \"update\") ::\n      Nil)\n\n  testLocalPredicates(\"basic case - local predicates - always false predicate, only inserts\")(\n      target = Seq((\"1\", \"2\", \"noop\"), (\"1\", \"4\", \"noop\"), (\"3\", \"2\", \"noop\"), (\"4\", \"4\", \"noop\")),\n      source = Seq((\"1\", \"8\"), (\"0\", \"3\")),\n      condition = \"1 != 1\",\n      expectedResults =\n        (\"1\", \"2\", \"noop\") ::\n        (\"1\", \"4\", \"noop\") ::\n        (\"3\", \"2\", \"noop\") ::\n        (\"4\", \"4\", \"noop\") ::\n        (\"1\", \"8\", \"insert\") ::\n        (\"0\", \"3\", \"insert\") ::\n        Nil)\n\n  testLocalPredicates(\"basic case - local predicates - always true predicate, all updated\")(\n    target = Seq((\"1\", \"2\", \"noop\"), (\"1\", \"4\", \"noop\"), (\"3\", \"2\", \"noop\"), (\"4\", \"4\", \"noop\")),\n    source = Seq((\"1\", \"8\")),\n    condition = \"1 = 1\",\n    expectedResults =\n      (\"1\", \"8\", \"update\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"1\", \"8\", \"update\") ::\n      Nil)\n\n  testLocalPredicates(\"basic case - local predicates - single file, updates and inserts\")(\n    target = Seq((\"1\", \"2\", \"noop\"), (\"1\", \"4\", \"noop\"), (\"3\", \"2\", \"noop\"), (\"4\", \"4\", \"noop\")),\n    source = Seq((\"1\", \"8\"), (\"3\", \"10\"), (\"0\", \"3\")),\n    condition = \"src.key1 = key2 and key2 < '3'\",\n    expectedResults =\n      (\"3\", \"2\", \"noop\") ::\n      (\"4\", \"4\", \"noop\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"1\", \"8\", \"update\") ::\n      (\"0\", \"3\", \"insert\") ::\n      (\"3\", \"10\", \"insert\") ::\n      Nil,\n    numFilesPerPartition = 1\n  )\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic case - column pruning, isPartitioned: $isPartitioned\") {\n      withTable(\"source\") {\n        val partitions = if (isPartitioned) \"key2\" :: Nil else Nil\n        append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), partitions)\n        Seq((1, 1, \"a\"), (0, 3, \"b\")).toDF(\"key1\", \"value\", \"col1\")\n          .createOrReplaceTempView(\"source\")\n\n        executeMerge(\n          target = s\"$tableSQLIdentifier\",\n          source = \"source src\",\n          condition = \"src.key1 = key2\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n        checkAnswer(readDeltaTableByIdentifier(),\n          Row(2, 2) :: // No change\n            Row(21, 21) :: // Update\n            Row(-10, 13) :: // Insert\n            Nil)\n      }\n    }\n  }\n\n  private def testNullCaseInsertOnly(name: String)(\n    target: Seq[(JInt, JInt)],\n    source: Seq[(JInt, JInt)],\n    condition: String,\n    expectedResults: Seq[(JInt, JInt)],\n    insertCondition: Option[String] = None) = {\n    Seq(true, false).foreach { isPartitioned =>\n      test(s\"basic case - null handling - $name, isPartitioned: $isPartitioned\") {\n        withView(\"sourceView\") {\n          val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n          append(target.toDF(\"key\", \"value\"), partitions)\n          source.toDF(\"key\", \"value\").createOrReplaceTempView(\"sourceView\")\n          withSQLConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"true\") {\n            if (insertCondition.isDefined) {\n              executeMerge(\n                s\"$tableSQLIdentifier as t\",\n                \"sourceView s\",\n                condition,\n                insert(\"(t.key, t.value) VALUES (s.key, s.value)\",\n                  condition = insertCondition.get))\n            } else {\n              executeMerge(\n                s\"$tableSQLIdentifier as t\",\n                \"sourceView s\",\n                condition,\n                insert(\"(t.key, t.value) VALUES (s.key, s.value)\"))\n            }\n          }\n          checkAnswer(\n            readDeltaTableByIdentifier(),\n            expectedResults.map { r => Row(r._1, r._2) }\n          )\n        }\n      }\n    }\n  }\n\n  testNullCaseInsertOnly(\"insert only merge - null in source\") (\n    target = Seq((1, 1)),\n    source = Seq((1, 10), (2, 20), (null, null)),\n    condition = \"s.key = t.key\",\n    expectedResults = Seq(\n      (1, 1),         // Existing value\n      (2, 20),        // Insert\n      (null, null)    // Insert\n    ))\n\n  testNullCaseInsertOnly(\"insert only merge - null value in both source and target\")(\n    target = Seq((1, 1), (null, null)),\n    source = Seq((1, 10), (2, 20), (null, 0)),\n    condition = \"s.key = t.key\",\n    expectedResults = Seq(\n      (null, null),   // No change as null in source does not match null in target\n      (1, 1),         // Existing value\n      (2, 20),        // Insert\n      (null, 0)       // Insert\n    ))\n\n  testNullCaseInsertOnly(\"insert only merge - null in insert clause\")(\n    target = Seq((1, 1), (2, 20)),\n    source = Seq((1, 10), (3, 30), (null, 0)),\n    condition = \"s.key = t.key\",\n    expectedResults = Seq(\n      (1, 1),         // Existing value\n      (2, 20),        // Existing value\n      (null, 0)       // Insert\n    ),\n    insertCondition = Some(\"s.key IS NULL\")\n  )\n}\n\ntrait MergeIntoTempViewsTests extends MergeIntoSuiteBaseMixin with DeltaTestUtilsForTempViews {\n  import testImplicits._\n\n  Seq(true, false).foreach { skippingEnabled =>\n    Seq(true, false).foreach { partitioned =>\n      Seq(true, false).foreach { useSQLView =>\n        test(\"basic case - merge to view on a Delta table, \" +\n          s\"partitioned: $partitioned skippingEnabled: $skippingEnabled useSqlView: $useSQLView\") {\n          withTable(\"delta_target\", \"source\") {\n            withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> skippingEnabled.toString) {\n              Seq((1, 1), (0, 3), (1, 6)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n              val partitions = if (partitioned) \"key2\" :: Nil else Nil\n              append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), partitions)\n              if (useSQLView) {\n                sql(s\"CREATE OR REPLACE TEMP VIEW delta_target AS \" +\n                  s\"SELECT * FROM $tableSQLIdentifier t\")\n              } else {\n                readDeltaTableByIdentifier()\n                  .createOrReplaceTempView(\"delta_target\")\n              }\n\n              executeMerge(\n                target = \"delta_target\",\n                source = \"source src\",\n                condition = \"src.key1 = key2 AND src.value < delta_target.value\",\n                update = \"key2 = 20 + key1, value = 20 + src.value\",\n                insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n              checkAnswer(sql(\"SELECT key2, value FROM delta_target\"),\n                Row(2, 2) :: // No change\n                  Row(21, 21) :: // Update\n                  Row(-10, 13) :: // Insert\n                  Row(-9, 16) :: // Insert\n                  Nil)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Negative case - more operations between merge and delta target\") {\n    withTempView(\"source\", \"target\") {\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      readDeltaTableByIdentifier().filter(\"value <> 0\").createTempView(\"target\")\n\n      val e = intercept[AnalysisException] {\n        executeMerge(\n          target = \"target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n      }.getMessage\n      errorContains(e, \"Expect a full scan of Delta sources, but found a partial scan\")\n    }\n  }\n\n  /**\n   * Ensure we can successfully remove the temp view from the target plan during MERGE analysis.\n   * Failing to do so can cause MERGE execution to fail later on as it is assumed the target plan is\n   * a simple logical relation by then without projections.\n   */\n  private def checkStripViewFromTarget(target: String): Unit = {\n    val targetViewPlan = sql(s\"SELECT * FROM $target\").queryExecution.analyzed.collect {\n      case v: View => v\n    }\n    assert(targetViewPlan.size === 1,\n      s\"Expected 1 view in target plan, got ${targetViewPlan.size}\")\n    DeltaViewHelper.stripTempViewForMerge(targetViewPlan.head, conf) match {\n      case SubqueryAlias(_, _: LogicalRelation) =>\n      case _ =>\n        fail(s\"DeltaViewHelper.stripTempViewForMerge doesn't correctly handle\" +\n          s\"removing the view from plan:\\n${targetViewPlan.head}\")\n    }\n  }\n\n  private def testTempViews(name: String)(\n      text: String,\n      mergeCondition: String,\n      expectedResult: ExpectedResult[Seq[Row]],\n      checkViewStripped: Boolean = true): Unit = {\n    testWithTempView(s\"test merge on temp view - $name\") { isSQLTempView =>\n      withTable(\"tab\") {\n        withTempView(\"src\") {\n          Seq((0, 3), (1, 2)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n          createTempViewFromSelect(text, isSQLTempView)\n          sql(\"CREATE TEMP VIEW src AS SELECT * FROM VALUES (1, 2), (3, 4) AS t(a, b)\")\n\n          def runMerge(): Unit =\n            executeMerge(\n              target = \"v\",\n              source = \"src\",\n              condition = mergeCondition,\n              update = \"v.value = src.b + 1\",\n              insert = \"(v.key, v.value) VALUES (src.a, src.b)\")\n\n          expectedResult match {\n            case ExpectedResult.Failure(checkError) =>\n              val ex = intercept[AnalysisException] {\n                runMerge()\n              }\n              checkError(ex)\n            case ExpectedResult.Success(expectedRows: Seq[Row]) =>\n              if (checkViewStripped) {\n                checkStripViewFromTarget(target = \"v\")\n              }\n              runMerge()\n              checkAnswer(spark.table(\"v\"), expectedRows)\n          }\n        }\n      }\n    }\n  }\n\n  testTempViews(\"basic\")(\n    text = \"SELECT * FROM tab\",\n    mergeCondition = \"src.a = v.key AND src.b = v.value\",\n    expectedResult = ExpectedResult.Success(Seq(Row(0, 3), Row(1, 3), Row(3, 4)))\n  )\n\n  testTempViews(\"basic - merge condition references subset of target cols\")(\n    text = \"SELECT * FROM tab\",\n    mergeCondition = \"src.a = v.key\",\n    expectedResult = ExpectedResult.Success(Seq(Row(0, 3), Row(1, 3), Row(3, 4)))\n  )\n\n  testTempViews(\"subset cols\")(\n    text = \"SELECT key FROM tab\",\n    mergeCondition = \"src.a = v.key AND src.b = v.value\",\n    expectedResult = ExpectedResult.Failure { ex =>\n      assert(ex.getErrorClass === \"UNRESOLVED_COLUMN.WITH_SUGGESTION\")\n    }\n  )\n\n  testTempViews(\"superset cols\")(\n    text = \"SELECT key, value, 1 FROM tab\",\n    mergeCondition = \"src.a = v.key AND src.b = v.value\",\n    // The analyzer can't tell whether the table originally had the extra column or not.\n    expectedResult = ExpectedResult.Failure { ex =>\n      checkErrorMatchPVals(\n        ex,\n        \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\",\n        parameters = Map(\n          \"schemaDiff\" -> \"(?s)Latest schema is missing field.*\",\n          \"legacyFlagMessage\" -> \"\"\n      ))\n    }\n  )\n\n  testTempViews(\"nontrivial projection\")(\n    text = \"SELECT value as key, key as value FROM tab\",\n    mergeCondition = \"src.a = v.key AND src.b = v.value\",\n    expectedResult = ExpectedResult.Success(Seq(Row(2, 1), Row(2, 1), Row(3, 0), Row(4, 3))),\n    // The view doesn't get stripped by DeltaViewHelper.stripTempViewForMerge during analysis in\n    // this case, doing it would be incorrect since the view is not a simple projection.\n    // We really shouldn't support this use case altogether but due to historical reasons we have to\n    // keep supporting it.\n    checkViewStripped = false\n  )\n\n\n  testTempViews(\"view with too many internal aliases\")(\n    text = \"SELECT * FROM (SELECT * FROM tab AS t1) AS t2\",\n    mergeCondition = \"src.a = v.key AND src.b = v.value\",\n    expectedResult = ExpectedResult.Success(Seq(Row(0, 3), Row(1, 3), Row(3, 4)))\n  )\n\n  testTempViews(\"view with too many internal aliases - merge condition references subset of \" +\n      s\"target cols\")(\n    text = \"SELECT * FROM (SELECT * FROM tab AS t1) AS t2\",\n    mergeCondition = \"src.a = v.key\",\n    expectedResult = ExpectedResult.Success(Seq(Row(0, 3), Row(1, 3), Row(3, 4)))\n  )\n}\n\ntrait MergeIntoNestedDataTests extends MergeIntoSuiteBaseMixin {\n  testNestedDataSupport(\"no update when not matched, only insert\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X3\", \"y\": 3}, \"value\": { \"a\": 300, \"b\": \"B300\" } }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\": { \"a\": 2,   \"b\": \"B2\" } }\"\"\",\n    update = \"value.b = 'UPDATED'\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\": { \"a\": 2,   \"b\": \"B2\"      } }\n        { \"key\": { \"x\": \"X3\", \"y\": 3}, \"value\": { \"a\": 300, \"b\": \"B300\"    } }\"\"\")\n\n  testNestedDataSupport(\"update entire nested column\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 100, \"b\": \"B100\" } }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\": { \"a\": 2,   \"b\": \"B2\" } }\"\"\",\n    update = \"value = s.value\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 100, \"b\": \"B100\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\": { \"a\": 2, \"b\": \"B2\"   } }\"\"\")\n\n  testNestedDataSupport(\"update one nested field\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 100, \"b\": \"B100\" } }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\": { \"a\": 2,   \"b\": \"B2\" } }\"\"\",\n    update = \"value.b = s.value.b\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\": { \"a\": 1, \"b\": \"B100\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\": { \"a\": 2, \"b\": \"B2\"   } }\"\"\")\n\n  testNestedDataSupport(\"update multiple fields at different levels\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": { \"i\": 1.0 } }, \"value\": { \"a\": 100, \"b\": \"B100\" } }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": { \"i\": 1.0 } }, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": { \"i\": 2.0 } }, \"value\": { \"a\": 2,   \"b\": \"B2\" } }\"\"\",\n    update =\n      \"key.x = 'XXX'\" :: \"key.y.i = 9000\" ::\n      \"value = named_struct('a', 9000, 'b', s.value.b)\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"XXX\", \"y\": { \"i\": 9000 } }, \"value\": { \"a\": 9000, \"b\": \"B100\" } }\n        { \"key\": { \"x\": \"X2\" , \"y\": { \"i\": 2.0  } }, \"value\": { \"a\": 2, \"b\": \"B2\" } }\"\"\")\n\n  testNestedDataSupport(\"update multiple fields at different levels to NULL\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": { \"i\": 1.0 } }, \"value\": { \"a\": 100, \"b\": \"B100\" } }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": { \"i\": 1.0 } }, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": { \"i\": 2.0 } }, \"value\": { \"a\": 2,   \"b\": \"B2\" } }\"\"\",\n    update = \"value = NULL\" :: \"key.x = NULL\" :: \"key.y.i = NULL\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": null, \"y\": { \"i\" : null } }, \"value\": null }\n        { \"key\": { \"x\": \"X2\" , \"y\": { \"i\" : 2.0  } }, \"value\": { \"a\": 2, \"b\": \"B2\" } }\"\"\")\n\n  testNestedDataSupport(\"update multiple fields at different levels with implicit casting\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": { \"i\": 1.0 } }, \"value\": { \"a\": 100, \"b\": \"B100\" } }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": { \"i\": 1.0 } }, \"value\": { \"a\": 1,   \"b\": \"B1\" } }\n        { \"key\": { \"x\": \"X2\", \"y\": { \"i\": 2.0 } }, \"value\": { \"a\": 2,   \"b\": \"B2\" } }\"\"\",\n    update =\n      \"key.x = 'XXX' \" :: \"key.y.i = '9000'\" ::\n      \"value = named_struct('a', '9000', 'b', s.value.b)\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"XXX\", \"y\": { \"i\": 9000 } }, \"value\": { \"a\": 9000, \"b\": \"B100\" } }\n        { \"key\": { \"x\": \"X2\" , \"y\": { \"i\": 2.0  } }, \"value\": { \"a\": 2, \"b\": \"B2\" } }\"\"\")\n\n  testNestedDataSupport(\"update array fields at different levels\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": [ 1, 11 ] }, \"value\": [ -1, -10 , -100 ] }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": [ 1, 11 ] }, \"value\": [ 1, 10 , 100 ]} }\n        { \"key\": { \"x\": \"X2\", \"y\": [ 2, 22 ] }, \"value\": [ 2, 20 , 200 ]} }\"\"\",\n    update = \"value = array(-9000)\" :: \"key.y = array(-1, -11)\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y\": [ -1, -11 ] }, \"value\": [ -9000 ]} }\n        { \"key\": { \"x\": \"X2\", \"y\": [ 2, 22 ] }, \"value\": [ 2, 20 , 200 ]} }\"\"\")\n\n  testNestedDataSupport(\"update using quoted names at different levels\", \"dotted name support\")(\n    source = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y.i\": 1.0 }, \"value.a\": \"A\" }\"\"\",\n    target = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y.i\": 1.0 }, \"value.a\": \"A1\" }\n        { \"key\": { \"x\": \"X2\", \"y.i\": 2.0 }, \"value.a\": \"A2\" }\"\"\",\n    update = \"`t`.key.`y.i` = 9000\" ::  \"t.`value.a` = 'UPDATED'\" :: Nil,\n    result = \"\"\"\n        { \"key\": { \"x\": \"X1\", \"y.i\": 9000 }, \"value.a\": \"UPDATED\" }\n        { \"key\": { \"x\": \"X2\", \"y.i\" : 2.0 }, \"value.a\": \"A2\" }\"\"\")\n\n  testNestedDataSupport(\"unknown nested field\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 0 } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    update = \"value.c = 'UPDATED'\" :: Nil,\n    errorStrs = \"No such struct field\" :: Nil)\n\n  testNestedDataSupport(\"assigning simple type to struct field\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1 } } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1 } } }\"\"\",\n    update = \"value.a = 'UPDATED'\" :: Nil,\n    errorStrs = \"data type mismatch\" :: Nil)\n\n  testNestedDataSupport(\"conflicting assignments between two nested fields at different levels\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 0 } } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1 } } }\"\"\",\n    update = \"value.a.x = 2\" :: \"value.a = named_struct('x', 3)\" :: Nil,\n    errorStrs = \"There is a conflict from these SET columns\" :: Nil)\n\n  testNestedDataSupport(\"conflicting assignments between nested field and top-level column\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 0 } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    update = \"value.a = 2\" :: \"value = named_struct('a', 3)\" :: Nil,\n    errorStrs = \"There is a conflict from these SET columns\" :: Nil)\n\n  testNestedDataSupport(\"nested field not supported in INSERT\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 0 } }\"\"\",\n    target = \"\"\"{ \"key\": \"B\", \"value\": { \"a\": 1 } }\"\"\",\n    update = \"value.a = 2\" :: Nil,\n    insert = \"\"\"(key, value.a) VALUES (s.key, s.value.a)\"\"\",\n    errorStrs = \"Nested field is not supported in the INSERT clause\" :: Nil)\n\n  testNestedDataSupport(\"updating map type\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 0 } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": 1 } }\"\"\",\n    update = \"value.a = 2\" :: Nil,\n    targetSchema =\n      new StructType().add(\"key\", StringType).add(\"value\", MapType(StringType, IntegerType)),\n    errorStrs = \"Updating nested fields is only supported for StructType\" :: Nil)\n\n  testNestedDataSupport(\"updating array type\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 0 } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": 1 } ] }\"\"\",\n    update = \"value.a = 2\" :: Nil,\n    targetSchema =\n      new StructType().add(\"key\", StringType).add(\"value\", MapType(StringType, IntegerType)),\n    errorStrs = \"Updating nested fields is only supported for StructType\" :: Nil)\n\n  testNestedDataSupport(\"resolution by name - update specific column\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"b\": 2, \"a\": { \"y\": 20, \"x\": 10} } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 }}\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n        .add(\"b\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType))),\n    update = \"value.a = s.value.a\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 1 } }\"\"\")\n\n  // scalastyle:off line.size.limit\n  testNestedDataSupport(\"resolution by name - update specific column - array of struct - longer source\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } }, { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    update = \"value = s.value\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 }, { \"a\": { \"y\": 30, \"x\": 40}, \"b\": 3 } ] }\"\"\")\n\n  testNestedDataSupport(\"resolution by name - update specific column - array of struct - longer target\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 }, { \"a\": { \"x\": 2, \"y\": 3 }, \"b\": 2 }] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    update = \"value = s.value\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 } ] }\"\"\")\n\n  testNestedDataSupport(\"resolution by name - update specific column - nested array of struct - longer source\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\" }, { \"c\": 3, \"d\": \"40\" } ] } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3} ] }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n            ))))),\n    update = \"value = s.value\",\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30 }, { \"c\": 3, \"d\": 40 } ] }, \"b\": 2 } ] }\"\"\")\n\n  testNestedDataSupport(\"resolution by name - update specific column - nested array of struct - longer target\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\" } ] } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 }, { \"c\": 2, \"d\": 4 } ] }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n            ))))),\n    update = \"value = s.value\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30}]}, \"b\": 2 } ] }\"\"\")\n  // scalastyle:on line.size.limit\n\n  testNestedDataSupport(\"resolution by name - update *\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"b\": 2, \"a\": { \"y\": 20, \"x\": 10} } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 }}\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n        .add(\"b\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType))),\n    update = \"*\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 10, \"y\": 20 } , \"b\": 2} }\"\"\")\n\n  // scalastyle:off line.size.limit\n  testNestedDataSupport(\"resolution by name - update * - array of struct - longer source\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } }, { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    update = \"*\",\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 }, { \"a\": { \"y\": 30, \"x\": 40}, \"b\": 3 } ] }\"\"\".stripMargin)\n\n  testNestedDataSupport(\"resolution by name - update * - array of struct - longer target\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 }, { \"a\": { \"x\": 2, \"y\": 3 }, \"b\": 4 }] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    update = \"*\",\n    result =\n      \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 } ] }\"\"\".stripMargin)\n\n  testNestedDataSupport(\"resolution by name - update * - nested array of struct - longer source\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": [{ \"c\": 10, \"d\": \"30\"}, { \"c\": 3, \"d\": \"40\" } ] } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 } ] }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n            ))))),\n    update = \"*\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30}, { \"c\": 3, \"d\": 40 } ] }, \"b\": 2 } ] }\"\"\")\n\n  testNestedDataSupport(\"resolution by name - update * - nested array of struct - longer target\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\" } ] } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3}, { \"c\": 2, \"d\": 4} ] }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n            ))))),\n    update = \"*\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30 } ] }, \"b\": 2 } ] }\"\"\")\n  // scalastyle:on line.size.limit\n\n  testNestedDataSupport(\"resolution by name - insert specific column\")(\n    source = \"\"\"{ \"key\": \"B\", \"value\": { \"b\": 2, \"a\": { \"y\": 20, \"x\": 10 } } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n        .add(\"b\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType))),\n    update = \"*\",\n    insert = \"(key, value) VALUES (s.key, s.value)\",\n    result =\n      \"\"\"\n        |{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } },\n        |{ \"key\": \"B\", \"value\": { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 } }\"\"\".stripMargin)\n\n  // scalastyle:off line.size.limit\n  testNestedDataSupport(\"resolution by name - insert specific column - array of struct\")(\n    source = \"\"\"{ \"key\": \"B\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10 } }, { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    update = \"*\",\n    insert = \"(key, value) VALUES (s.key, s.value)\",\n    result =\n      \"\"\" { \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] },\n            { \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 }, { \"a\": { \"y\": 30, \"x\": 40}, \"b\": 3 } ] }\"\"\")\n\n  testNestedDataSupport(\"resolution by name - insert specific column - nested array of struct\")(\n    source = \"\"\"{ \"key\": \"B\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\" }, { \"c\": 3, \"d\": \"40\" } ] } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 } ] }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n            ))))),\n    update = \"*\",\n    insert = \"(key, value) VALUES (s.key, s.value)\",\n    result =\n      \"\"\"\n       { \"key\": \"A\", \"value\": [ { \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3 } ] }, \"b\": 1 } ] },\n        { \"key\": \"B\", \"value\": [ { \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": 30 }, { \"c\": 3, \"d\": 40 } ] }, \"b\": 2 } ] }\"\"\")\n  // scalastyle:on line.size.limit\n\n  testNestedDataSupport(\"resolution by name - insert *\")(\n    source = \"\"\"{ \"key\": \"B\", \"value\": { \"b\": 2, \"a\": { \"y\": 20, \"x\": 10} } }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n        .add(\"b\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType))),\n    update = \"*\",\n    insert = \"*\",\n    result =\n      \"\"\"\n        |{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } },\n        |{ \"key\": \"B\", \"value\": { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 } }\"\"\".stripMargin)\n\n  // scalastyle:off line.size.limit\n  testNestedDataSupport(\"resolution by name - insert * - array of struct\")(\n    source = \"\"\"{ \"key\": \"B\", \"value\": [ { \"b\": \"2\", \"a\": { \"y\": 20, \"x\": 10} }, { \"b\": \"3\", \"a\": { \"y\": 30, \"x\": 40 } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n        .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n        .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n        .add(\"b\", StringType)\n        .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType)))),\n    update = \"*\",\n    insert = \"*\",\n    result =\n      \"\"\"\n        |{ \"key\": \"A\", \"value\": [ { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } ] },\n        |{ \"key\": \"B\", \"value\": [ { \"a\": { \"x\": 10, \"y\": 20 }, \"b\": 2 }, { \"a\": { \"y\": 30, \"x\": 40}, \"b\": 3 } ] }\"\"\".stripMargin)\n\n  testNestedDataSupport(\"resolution by name - insert * - nested array of struct\")(\n    source = \"\"\"{ \"key\": \"B\", \"value\": [{ \"b\": \"2\", \"a\": { \"y\": 20, \"x\": [ { \"c\": 10, \"d\": \"30\"}, { \"c\": 3, \"d\": \"40\"} ] } } ] }\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": [{ \"a\": { \"y\": 2, \"x\": [ { \"c\": 1, \"d\": 3} ] }, \"b\": 1 }] }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"a\", new StructType()\n              .add(\"y\", IntegerType)\n              .add(\"x\", ArrayType(\n                new StructType()\n                  .add(\"c\", IntegerType)\n                  .add(\"d\", IntegerType))))\n          .add(\"b\", IntegerType))),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", ArrayType(\n        new StructType()\n          .add(\"b\", StringType)\n          .add(\"a\", new StructType()\n            .add(\"y\", IntegerType)\n            .add(\"x\", ArrayType(\n              new StructType()\n                .add(\"c\", IntegerType)\n                .add(\"d\", StringType)\n            ))))),\n    update = \"*\",\n    insert = \"*\",\n    result =\n      \"\"\"\n        |{ \"key\": \"A\", \"value\": [{ \"a\": { \"y\": 2, \"x\": [{ \"c\": 1, \"d\": 3}]}, \"b\": 1 }] },\n        |{ \"key\": \"B\", \"value\": [{ \"a\": { \"y\": 20, \"x\": [{ \"c\": 10, \"d\": 30}, { \"c\": 3, \"d\": 40}]}, \"b\": 2 }]}\"\"\".stripMargin)\n  // scalastyle:on line.size.limit\n\n  // Note that value.b has to be in the right position for this test to avoid throwing an error\n  // trying to write its integer value into the value.a struct.\n  testNestedDataSupport(\"update resolution by position with conf\")(\n    source = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"y\": 20, \"x\": 10}, \"b\": 2 }}\"\"\",\n    target = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 1, \"y\": 2 }, \"b\": 1 } }\"\"\",\n    targetSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType().add(\"x\", IntegerType).add(\"y\", IntegerType))\n        .add(\"b\", IntegerType)),\n    sourceSchema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", new StructType()\n        .add(\"a\", new StructType().add(\"y\", IntegerType).add(\"x\", IntegerType))\n        .add(\"b\", IntegerType)),\n    update = \"*\",\n    insert = \"(key, value) VALUES (s.key, s.value)\",\n    result = \"\"\"{ \"key\": \"A\", \"value\": { \"a\": { \"x\": 20, \"y\": 10 }, \"b\": 2 } }\"\"\",\n    confs = (DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\") +: Nil)\n}\n\ntrait MergeIntoUnlimitedMergeClausesTests extends MergeIntoSuiteBaseMixin {\n  private def testUnlimitedClauses(\n      name: String)(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      mergeOn: String,\n      mergeClauses: MergeClause*)(\n      result: Seq[(Int, Int)]): Unit =\n    testExtendedMerge(name, \"unlimited clauses\")(source, target, mergeOn, mergeClauses : _*)(result)\n\n  testUnlimitedClauses(\"two conditional update + two conditional delete + insert\")(\n    source = (0, 0) :: (1, 100) :: (3, 300) :: (4, 400) :: (5, 500) :: Nil,\n    target = (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.key < 2\"),\n    delete(condition = \"s.key > 4\"),\n    update(condition = \"s.key == 3\", set = \"key = s.key, value = s.value\"),\n    update(condition = \"s.key == 4\", set = \"key = s.key, value = 2 * s.value\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),    // insert (0, 0)\n                 // delete (1, 10)\n      (2, 20),   // neither updated nor deleted as it didn't match\n      (3, 300),  // update (3, 30)\n      (4, 800),  // update (4, 40)\n      (5, 500)   // insert (5, 500)\n    ))\n\n  testUnlimitedClauses(\"two conditional delete + conditional update + update + insert\")(\n    source = (0, 0) :: (1, 100) :: (2, 200) :: (3, 300) :: (4, 400) :: Nil,\n    target = (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.key < 2\"),\n    delete(condition = \"s.key > 3\"),\n    update(condition = \"s.key == 2\", set = \"key = s.key, value = s.value\"),\n    update(condition = null, set = \"key = s.key, value = 2 * s.value\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),   // insert (0, 0)\n                // delete (1, 10)\n      (2, 200), // update (2, 20)\n      (3, 600)  // update (3, 30)\n                // delete (4, 40)\n    ))\n\n  testUnlimitedClauses(\"conditional delete + two conditional update + two conditional insert\")(\n    source = (1, 100) :: (2, 200) :: (3, 300) :: (4, 400) :: (6, 600) :: Nil,\n    target = (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.key < 2\"),\n    update(condition = \"s.key == 2\", set = \"key = s.key, value = s.value\"),\n    update(condition = \"s.key == 3\", set = \"key = s.key, value = 2 * s.value\"),\n    insert(condition = \"s.key < 5\", values = \"(key, value) VALUES (s.key, s.value)\"),\n    insert(condition = \"s.key > 5\", values = \"(key, value) VALUES (s.key, 1 + s.value)\"))(\n    result = Seq(\n                // delete (1, 10)\n      (2, 200), // update (2, 20)\n      (3, 600), // update (3, 30)\n      (4, 400), // insert (4, 400)\n      (6, 601)  // insert (6, 600)\n    ))\n\n  testUnlimitedClauses(\"conditional update + update + conditional delete + conditional insert\")(\n    source = (1, 100) :: (2, 200) :: (3, 300) :: (4, 400) :: (5, 500) :: Nil,\n    target = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key < 2\", set = \"key = s.key, value = s.value\"),\n    update(condition = \"s.key < 3\", set = \"key = s.key, value = 2 * s.value\"),\n    delete(condition = \"s.key < 4\"),\n    insert(condition = \"s.key > 4\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),   // no change\n      (1, 100), // (1, 10) updated by matched_0\n      (2, 400), // (2, 20) updated by matched_1\n                // (3, 30) deleted by matched_2\n      (5, 500)  // (5, 500) inserted\n    ))\n\n  testUnlimitedClauses(\"conditional insert + insert\")(\n    source = (1, 100) :: (2, 200) :: (3, 300) :: (4, 400) :: (5, 500) :: Nil,\n    target = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.key < 5\", values = \"(key, value) VALUES (s.key, s.value)\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, s.value + 1)\"))(\n    result = Seq(\n      (0, 0),   // no change\n      (1, 10),  // no change\n      (2, 20),  // no change\n      (3, 30),  // no change\n      (4, 400), // (4, 400) inserted by notMatched_0\n      (5, 501)  // (5, 501) inserted by notMatched_1\n    ))\n\n  testUnlimitedClauses(\"2 conditional inserts\")(\n    source = (1, 100) :: (2, 200) :: (3, 300) :: (4, 400) :: (5, 500) :: (6, 600) :: Nil,\n    target = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.key < 5\", values = \"(key, value) VALUES (s.key, s.value)\"),\n    insert(condition = \"s.key = 5\", values = \"(key, value) VALUES (s.key, s.value + 1)\"))(\n    result = Seq(\n      (0, 0),   // no change\n      (1, 10),  // no change\n      (2, 20),  // no change\n      (3, 30),  // no change\n      (4, 400), // (4, 400) inserted by notMatched_0\n      (5, 501)  // (5, 501) inserted by notMatched_1\n                // (6, 600) not inserted as not insert condition matched\n    ))\n\n  testUnlimitedClauses(\"update/delete (no matches) + conditional insert + insert\")(\n    source = (4, 400) :: (5, 500) :: Nil,\n    target = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"t.key = 0\", set = \"key = s.key, value = s.value\"),\n    delete(condition = null),\n    insert(condition = \"s.key < 5\", values = \"(key, value) VALUES (s.key, s.value)\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, s.value + 1)\"))(\n    result = Seq(\n      (0, 0),   // no change\n      (1, 10),  // no change\n      (2, 20),  // no change\n      (3, 30),  // no change\n      (4, 400), // (4, 400) inserted by notMatched_0\n      (5, 501)  // (5, 501) inserted by notMatched_1\n    ))\n\n  testUnlimitedClauses(\"update/delete (no matches) + 2 conditional inserts\")(\n    source = (4, 400) :: (5, 500) :: (6, 600)  :: Nil,\n    target = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"t.key = 0\", set = \"key = s.key, value = s.value\"),\n    delete(condition = null),\n    insert(condition = \"s.key < 5\", values = \"(key, value) VALUES (s.key, s.value)\"),\n    insert(condition = \"s.key = 5\", values = \"(key, value) VALUES (s.key, s.value + 1)\"))(\n    result = Seq(\n      (0, 0),   // no change\n      (1, 10),  // no change\n      (2, 20),  // no change\n      (3, 30),  // no change\n      (4, 400), // (4, 400) inserted by notMatched_0\n      (5, 501)  // (5, 501) inserted by notMatched_1\n                // (6, 600) not inserted as not insert condition matched\n    ))\n\n  testUnlimitedClauses(\"2 update + 2 delete + 4 insert\")(\n    source = (1, 100) :: (2, 200) :: (3, 300) :: (4, 400) :: (5, 500) :: (6, 600) :: (7, 700) ::\n      (8, 800) :: (9, 900) :: Nil,\n    target = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key == 1\", set = \"key = s.key, value = s.value\"),\n    delete(condition = \"s.key == 2\"),\n    update(condition = \"s.key == 3\", set = \"key = s.key, value = 2 * s.value\"),\n    delete(condition = null),\n    insert(condition = \"s.key == 5\", values = \"(key, value) VALUES (s.key, s.value)\"),\n    insert(condition = \"s.key == 6\", values = \"(key, value) VALUES (s.key, 1 + s.value)\"),\n    insert(condition = \"s.key == 7\", values = \"(key, value) VALUES (s.key, 2 + s.value)\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, 3 + s.value)\"))(\n    result = Seq(\n      (0, 0),    // no change\n      (1, 100),  // (1, 10) updated by matched_0\n                 // (2, 20) deleted by matched_1\n      (3, 600),  // (3, 30) updated by matched_2\n                 // (4, 40) deleted by matched_3\n      (5, 500),  // (5, 500) inserted by notMatched_0\n      (6, 601),  // (6, 600) inserted by notMatched_1\n      (7, 702),  // (7, 700) inserted by notMatched_2\n      (8, 803),  // (8, 800) inserted by notMatched_3\n      (9, 903)   // (9, 900) inserted by notMatched_3\n    ))\n\n  testMergeAnalysisException(\"error on multiple insert clauses without condition\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key == 3\", set = \"key = s.key, value = 2 * srcValue\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, srcValue)\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, 1 + srcValue)\"))(\n    expectedErrorClass = \"NON_LAST_NOT_MATCHED_BY_TARGET_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\"error on multiple update clauses without condition\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key == 3\", set = \"key = s.key, value = 2 * srcValue\"),\n    update(condition = null, set = \"key = s.key, value = 3 * srcValue\"),\n    update(condition = null, set = \"key = s.key, value = 4 * srcValue\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, srcValue)\"))(\n    expectedErrorClass = \"NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\"error on multiple update/delete clauses without condition\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key == 3\", set = \"key = s.key, value = 2 * srcValue\"),\n    delete(condition = null),\n    update(condition = null, set = \"key = s.key, value = 4 * srcValue\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, srcValue)\"))(\n    expectedErrorClass = \"NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\n    \"error on non-empty condition following empty condition for update clauses\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = null, set = \"key = s.key, value = 2 * srcValue\"),\n    update(condition = \"s.key < 3\", set = \"key = s.key, value = srcValue\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, srcValue)\"))(\n    expectedErrorClass = \"NON_LAST_MATCHED_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n\n  testMergeAnalysisException(\n    \"error on non-empty condition following empty condition for insert clauses\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = null, set = \"key = s.key, value = srcValue\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, srcValue)\"),\n    insert(condition = \"s.key < 3\", values = \"(key, value) VALUES (s.key, srcValue)\"))(\n    expectedErrorClass = \"NON_LAST_NOT_MATCHED_BY_TARGET_CLAUSE_OMIT_CONDITION\",\n    expectedMessageParameters = Map.empty)\n}\n\ntrait MergeIntoAnalysisExceptionTests extends MergeIntoSuiteBaseMixin {\n  testMergeAnalysisException(\"update condition - ambiguous reference\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"key > 1\", set = \"tgtValue = srcValue\"))(\n    expectedErrorClass = \"AMBIGUOUS_REFERENCE\",\n    expectedMessageParameters = Map(\n      \"name\" -> \"`key`\",\n      \"referenceNames\" -> \"[`s`.`key`, `t`.`key`]\"))\n\n  testMergeAnalysisException(\"update condition - unknown reference\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"unknownAttrib > 1\", set = \"tgtValue = srcValue\"))(\n    // Should show unknownAttrib as invalid ref and (key, tgtValue, srcValue) as valid column names.\n    expectedErrorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n    expectedMessageParameters = Map(\n      \"sqlExpr\" -> \"unknownAttrib\",\n      \"clause\" -> \"UPDATE condition\",\n      \"cols\" -> \"t.key, t.tgtValue, s.key, s.srcValue\"))\n\n  testMergeAnalysisException(\"update condition - aggregation function\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"max(0) > 0\", set = \"tgtValue = srcValue\"))(\n    expectedErrorClass = \"DELTA_AGGREGATION_NOT_SUPPORTED\",\n    expectedMessageParameters = Map(\n      \"operation\" -> \"UPDATE condition of MERGE operation\",\n      \"predicate\" -> \"(condition = (max(0) > 0))\"))\n\n  testMergeAnalysisException(\"update condition - subquery\")(\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.srcValue in (select value from t)\", set = \"tgtValue = srcValue\"))(\n    expectedErrorClass = \"TABLE_OR_VIEW_NOT_FOUND\",\n    expectedMessageParameters = Map(\"relationName\" -> \"`t`\"))\n\n  testMergeAnalysisException(\"delete condition - ambiguous reference\")(\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"key > 1\"))(\n    expectedErrorClass = \"AMBIGUOUS_REFERENCE\",\n    expectedMessageParameters = Map(\n      \"name\" -> \"`key`\",\n      \"referenceNames\" -> \"[`s`.`key`, `t`.`key`]\"))\n\n  testMergeAnalysisException(\"delete condition - unknown reference\")(\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"unknownAttrib > 1\"))(\n    // Should show unknownAttrib as invalid ref and (key, tgtValue, srcValue) as valid column names.\n    expectedErrorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n    expectedMessageParameters = Map(\n      \"sqlExpr\" -> \"unknownAttrib\",\n      \"clause\" -> \"DELETE condition\",\n      \"cols\" -> \"t.key, t.tgtValue, s.key, s.srcValue\"))\n\n  testMergeAnalysisException(\"delete condition - aggregation function\")(\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"max(0) > 0\"))(\n    expectedErrorClass = \"DELTA_AGGREGATION_NOT_SUPPORTED\",\n    expectedMessageParameters = Map(\n      \"operation\" -> \"DELETE condition of MERGE operation\",\n      \"predicate\" -> \"(condition = (max(0) > 0))\"))\n\n  testMergeAnalysisException(\"delete condition - subquery\")(\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.srcValue in (select tgtValue from t)\"))(\n    expectedErrorClass = \"TABLE_OR_VIEW_NOT_FOUND\",\n    expectedMessageParameters = Map(\"relationName\" -> \"`t`\"))\n\n  testMergeAnalysisException(\"insert condition - unknown reference\")(\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"unknownAttrib > 1\", values = \"(key, tgtValue) VALUES (s.key, s.srcValue)\"))(\n    // Should show unknownAttrib as invalid ref and (key, srcValue) as valid column names,\n    // but not show tgtValue as a valid name as target columns cannot be present in insert clause.\n    expectedErrorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n    expectedMessageParameters = Map(\n      \"sqlExpr\" -> \"unknownAttrib\",\n      \"clause\" -> \"INSERT condition\",\n      \"cols\" -> \"s.key, s.srcValue\"))\n\n  testMergeAnalysisException(\"insert condition - reference to target table column\")(\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"tgtValue > 1\", values = \"(key, tgtValue) VALUES (s.key, s.srcValue)\"))(\n    // Should show tgtValue as invalid ref and (key, srcValue) as valid column names\n    expectedErrorClass = \"DELTA_MERGE_UNRESOLVED_EXPRESSION\",\n    expectedMessageParameters = Map(\n      \"sqlExpr\" -> \"tgtValue\",\n      \"clause\" -> \"INSERT condition\",\n      \"cols\" -> \"s.key, s.srcValue\"))\n\n  testMergeAnalysisException(\"insert condition - aggregation function\")(\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"max(0) > 0\", values = \"(key, tgtValue) VALUES (s.key, s.srcValue)\"))(\n    expectedErrorClass = \"DELTA_AGGREGATION_NOT_SUPPORTED\",\n    expectedMessageParameters = Map(\n      \"operation\" -> \"INSERT condition of MERGE operation\",\n      \"predicate\" -> \"(condition = (max(0) > 0))\"))\n\n  testMergeAnalysisException(\"insert condition - subquery\")(\n    mergeOn = \"s.key = t.key\",\n    insert(\n      condition = \"s.srcValue in (select srcValue from s)\",\n      values = \"(key, tgtValue) VALUES (s.key, s.srcValue)\"))(\n    expectedErrorClass = \"TABLE_OR_VIEW_NOT_FOUND\",\n    expectedMessageParameters = Map(\"relationName\" -> \"`s`\"))\n}\n\ntrait MergeIntoExtendedSyntaxTests extends MergeIntoSuiteBaseMixin {\n  import testImplicits._\n\n  private def testMergeErrorOnMultipleMatches(\n      name: String,\n      confs: Seq[(String, String)] = Seq.empty)(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      mergeOn: String,\n      mergeClauses: MergeClause*): Unit = {\n    test(s\"extended syntax - $name\") {\n      withSQLConf(confs: _*) {\n        withKeyValueData(source, target) { case (sourceName, targetName) =>\n          val docURL = \"/delta-update.html#upsert-into-a-table-using-merge\"\n\n          checkError(\n            exception = intercept[DeltaUnsupportedOperationException] {\n              executeMerge(s\"$targetName t\", s\"$sourceName s\", mergeOn, mergeClauses: _*)\n            },\n            \"DELTA_MULTIPLE_SOURCE_ROW_MATCHING_TARGET_ROW_IN_MERGE\",\n            parameters = Map(\n              \"usageReference\" -> DeltaErrors.generateDocsLink(\n                spark.sparkContext.getConf, docURL, skipValidation = true))\n          )\n        }\n      }\n    }\n  }\n\n  testExtendedMerge(\"only update\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2)  :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(set = \"key = s.key, value = s.value\"))(\n    result = Seq(\n      (1, 10),  // (1, 1) updated\n      (2, 2)\n    ))\n\n  testMergeErrorOnMultipleMatches(\"only update with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (1, 11) :: (2, 20) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(set = \"key = s.key, value = s.value\"))\n\n  testExtendedMerge(\"only conditional update\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: (3, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.value <> 20 AND t.value <> 3\", set = \"key = s.key, value = s.value\"))(\n    result = Seq(\n      (1, 10),  // updated\n      (2, 2),   // not updated due to source-only condition `s.value <> 20`\n      (3, 3)    // not updated due to target-only condition `t.value <> 3`\n    ))\n\n  testMergeErrorOnMultipleMatches(\"only conditional update with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (1, 11) :: (2, 20) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.value = 10\", set = \"key = s.key, value = s.value\"))\n\n  testExtendedMerge(\"only delete\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete())(\n    result = Seq(\n      (2, 2)    // (1, 1) deleted\n    ))          // (3, 30) not inserted as not insert clause\n\n  // This is not ambiguous even when there are multiple matches\n  testExtendedMerge(s\"only delete with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (1, 100) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete())(\n    result = Seq(\n      (2, 2)  // (1, 1) matches multiple source rows but unambiguously deleted\n    )\n  )\n\n  testExtendedMerge(\"only conditional delete\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: (3, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.value <> 20 AND t.value <> 3\"))(\n    result = Seq(\n      (2, 2),   // not deleted due to source-only condition `s.value <> 20`\n      (3, 3)    // not deleted due to target-only condition `t.value <> 3`\n    ))          // (1, 1) deleted\n\n  testMergeErrorOnMultipleMatches(\"only conditional delete with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (1, 100) :: (2, 20) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.value = 10\"))\n\n  testExtendedMerge(\"conditional update + delete\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: Nil,\n    target = (1, 1) :: (2, 2) :: (3, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key <> 1\", set = \"key = s.key, value = s.value\"),\n    delete())(\n    result = Seq(\n      (2, 20),  // (2, 2) updated, (1, 1) deleted as it did not match update condition\n      (3, 3)\n    ))\n\n  testMergeErrorOnMultipleMatches(\"conditional update + delete with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (2, 200) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.value = 20\", set = \"key = s.key, value = s.value\"),\n    delete())\n\n  testExtendedMerge(\"conditional update + conditional delete\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: (3, 3) :: (4, 4) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key <> 1\", set = \"key = s.key, value = s.value\"),\n    delete(condition = \"s.key <> 2\"))(\n    result = Seq(\n      (2, 20),  // (2, 2) updated as it matched update condition\n      (3, 30),  // (3, 3) updated even though it matched update and delete conditions, as update 1st\n      (4, 4)\n    ))          // (1, 1) deleted as it matched delete condition\n\n  testMergeErrorOnMultipleMatches(\n    \"conditional update + conditional delete with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (1, 100) :: (2, 20) :: (2, 200) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.value = 20\", set = \"key = s.key, value = s.value\"),\n    delete(condition = \"s.value = 10\"))\n\n  testExtendedMerge(\"conditional delete + conditional update (order matters)\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: (3, 3) :: (4, 4) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(condition = \"s.key <> 2\"),\n    update(condition = \"s.key <> 1\", set = \"key = s.key, value = s.value\"))(\n    result = Seq(\n      (2, 20),  // (2, 2) updated as it matched update condition\n      (4, 4)    // (4, 4) unchanged\n    ))          // (1, 1) and (3, 3) deleted as they matched delete condition (before update cond)\n\n  testExtendedMerge(\"only insert\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),   // (0, 0) inserted\n      (1, 1),   // (1, 1) not updated as no update clause\n      (2, 2),   // (2, 2) not updated as no update clause\n      (3, 30)   // (3, 30) inserted\n    ))\n\n  testExtendedMerge(\"only conditional insert\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.value <> 30\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),   // (0, 0) inserted by condition but not (3, 30)\n      (1, 1),\n      (2, 2)\n    ))\n\n  testExtendedMerge(\"update + conditional insert\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(\"key = s.key, value = s.value\"),\n    insert(condition = \"s.value <> 30\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),   // (0, 0) inserted by condition but not (3, 30)\n      (1, 10),  // (1, 1) updated\n      (2, 2)\n    ))\n\n  testExtendedMerge(\"conditional update + conditional insert\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key > 1\", set = \"key = s.key, value = s.value\"),\n    insert(condition = \"s.key > 1\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (1, 1),   // (1, 1) not updated by condition\n      (2, 20),  // (2, 2) updated by condition\n      (3, 30)   // (3, 30) inserted by condition but not (0, 0)\n    ))\n\n  // This is specifically to test the MergeIntoDeltaCommand.writeOnlyInserts code paths\n  testExtendedMerge(\"update + conditional insert clause with data to only insert, no updates\")(\n    source = (0, 0) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(\"key = s.key, value = s.value\"),\n    insert(condition = \"s.value <> 30\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (0, 0),   // (0, 0) inserted by condition but not (3, 30)\n      (1, 1),\n      (2, 2)\n    ))\n\n  testExtendedMerge(s\"delete + insert with multiple matches for both\") (\n    source = (1, 10) :: (1, 100) :: (3, 30) :: (3, 300) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    delete(),\n    insert(values = \"(key, value) VALUES (s.key, s.value)\")) (\n    result = Seq(\n               // (1, 1) matches multiple source rows but unambiguously deleted\n      (2, 2),  // existed previously\n      (3, 30), // inserted\n      (3, 300) // inserted\n    )\n  )\n\n  testExtendedMerge(\"conditional update + conditional delete + conditional insert\")(\n    source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n    target = (1, 1) :: (2, 2) :: (3, 3) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.key < 2\", set = \"key = s.key, value = s.value\"),\n    delete(condition = \"s.key < 3\"),\n    insert(condition = \"s.key > 1\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (1, 10),  // (1, 1) updated by condition, but not (2, 2) or (3, 3)\n      (3, 3),   // neither updated nor deleted as it matched neither condition\n      (4, 40)   // (4, 40) inserted by condition, but not (0, 0)\n    ))          // (2, 2) deleted by condition but not (1, 1) or (3, 3)\n\n  testMergeErrorOnMultipleMatches(\n    \"conditional update + conditional delete + conditional insert with multiple matches\")(\n    source = (0, 0) :: (1, 10) :: (1, 100) :: (2, 20) :: (2, 200) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    update(condition = \"s.value = 20\", set = \"key = s.key, value = s.value\"),\n    delete(condition = \"s.value = 10\"),\n    insert(condition = \"s.value = 0\", values = \"(key, value) VALUES (s.key, s.value)\"))\n\n  // complex merge condition = has target-only and source-only predicates\n  testExtendedMerge(\n    \"conditional update + conditional delete + conditional insert + complex merge condition \")(\n    source = (-1, -10) :: (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: (5, 50) :: Nil,\n    target = (-1, -1) :: (1, 1) :: (2, 2) :: (3, 3) :: (5, 5) :: Nil,\n    mergeOn = \"s.key = t.key AND t.value > 0 AND s.key < 5\",\n    update(condition = \"s.key < 2\", set = \"key = s.key, value = s.value\"),\n    delete(condition = \"s.key < 3\"),\n    insert(condition = \"s.key > 1\", values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq(\n      (-1, -1), // (-1, -1) not matched with (-1, -10) by target-only condition 't.value > 0', so\n                // not updated, But (-1, -10) not inserted as insert condition is 's.key > 1'\n                // (0, 0) not matched any target but not inserted as insert condition is 's.key > 1'\n      (1, 10),  // (1, 1) matched with (1, 10) and updated as update condition is 's.key < 2'\n                // (2, 2) matched with (2, 20) and deleted as delete condition is 's.key < 3'\n      (3, 3),   // (3, 3) matched with (3, 30) but neither updated nor deleted as it did not\n                // satisfy update or delete condition\n      (4, 40),  // (4, 40) not matched any target, so inserted as insert condition is 's.key > 1'\n      (5, 5),   // (5, 5) not matched with (5, 50) by source-only condition 's.key < 5', no update\n      (5, 50)   // (5, 50) inserted as inserted as insert condition is 's.key > 1'\n    ))\n\n  test(\"extended syntax - different # cols in source than target\") {\n    val sourceData =\n      (0, 0, 0) :: (1, 10, 100) :: (2, 20, 200) :: (3, 30, 300) :: (4, 40, 400) :: Nil\n    val targetData = (1, 1) :: (2, 2) :: (3, 3) :: Nil\n\n    withTempView(\"source\") {\n      append(targetData.toDF(\"key\", \"value\"), Nil)\n      sourceData.toDF(\"key\", \"value\", \"extra\").createOrReplaceTempView(\"source\")\n      executeMerge(\n        s\"$tableSQLIdentifier t\",\n        \"source s\",\n        cond = \"s.key = t.key\",\n        update(condition = \"s.key < 2\", set = \"key = s.key, value = s.value + s.extra\"),\n        delete(condition = \"s.key < 3\"),\n        insert(condition = \"s.key > 1\", values = \"(key, value) VALUES (s.key, s.value + s.extra)\"))\n\n      checkAnswer(\n        readDeltaTableByIdentifier(),\n        Seq(\n          Row(1, 110),  // (1, 1) updated by condition, but not (2, 2) or (3, 3)\n          Row(3, 3),    // neither updated nor deleted as it matched neither condition\n          Row(4, 440)   // (4, 40) inserted by condition, but not (0, 0)\n        ))              // (2, 2) deleted by condition but not (1, 1) or (3, 3)\n    }\n  }\n\n  test(\"extended syntax - nested data - conditions and actions\") {\n    withJsonData(\n      source =\n        \"\"\"{ \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\" : { \"a\": 100, \"b\": \"B100\" } }\n          { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\" : { \"a\": 200, \"b\": \"B200\" } }\n          { \"key\": { \"x\": \"X3\", \"y\": 3}, \"value\" : { \"a\": 300, \"b\": \"B300\" } }\n          { \"key\": { \"x\": \"X4\", \"y\": 4}, \"value\" : { \"a\": 400, \"b\": \"B400\" } }\"\"\",\n      target =\n        \"\"\"{ \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\" : { \"a\": 1,   \"b\": \"B1\" } }\n          { \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\" : { \"a\": 2,   \"b\": \"B2\" } }\"\"\"\n    ) { case (sourceName, targetName) =>\n      executeMerge(\n        s\"$targetName t\",\n        s\"$sourceName s\",\n        cond = \"s.key = t.key\",\n        update(condition = \"s.key.y < 2\", set = \"key = s.key, value = s.value\"),\n        insert(condition = \"s.key.x < 'X4'\", values = \"(key, value) VALUES (s.key, s.value)\"))\n\n      checkAnswer(\n        readDeltaTableByIdentifier(),\n        spark.read.json(Seq(\n          \"\"\"{ \"key\": { \"x\": \"X1\", \"y\": 1}, \"value\" : { \"a\": 100, \"b\": \"B100\" } }\"\"\", // updated\n          \"\"\"{ \"key\": { \"x\": \"X2\", \"y\": 2}, \"value\" : { \"a\": 2,   \"b\": \"B2\"   } }\"\"\", // not updated\n          \"\"\"{ \"key\": { \"x\": \"X3\", \"y\": 3}, \"value\" : { \"a\": 300, \"b\": \"B300\" } }\"\"\"  // inserted\n        ).toDS))\n    }\n  }\n\n  testExtendedMerge(\"insert only merge\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2)  :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(values = \"*\"))(\n    result = Seq(\n      (0, 0), // inserted\n      (1, 1), // existed previously\n      (2, 2), // existed previously\n      (3, 30) // inserted\n    ))\n\n  testExtendedMerge(\"insert only merge with insert condition on source\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2)  :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(values = \"*\", condition = \"s.key = s.value\"))(\n    result = Seq(\n      (0, 0), // inserted\n      (1, 1), // existed previously\n      (2, 2)  // existed previously\n    ))\n\n  testExtendedMerge(\"insert only merge with predicate insert\")(\n    source = (0, 0) :: (1, 10) :: (3, 30) :: Nil,\n    target = (1, 1) :: (2, 2)  :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(values = \"(t.key, t.value) VALUES (s.key + 10, s.value + 10)\"))(\n    result = Seq(\n      (10, 10), // inserted\n      (1, 1), // existed previously\n      (2, 2), // existed previously\n      (13, 40) // inserted\n    ))\n\n  testExtendedMerge(s\"insert only merge with multiple matches\") (\n    source = (0, 0) :: (1, 10) :: (1, 100) :: (3, 30) :: (3, 300) :: Nil,\n    target = (1, 1) :: (2, 2) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(values = \"(key, value) VALUES (s.key, s.value)\")) (\n    result = Seq(\n      (0, 0), // inserted\n      (1, 1), // existed previously\n      (2, 2), // existed previously\n      (3, 30), // inserted\n      (3, 300) // key exists but still inserted\n    )\n  )\n\n  testMergeErrorOnMultipleMatches(\n    \"unconditional insert only merge - multiple matches when feature flag off\",\n    confs = Seq(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"false\"))(\n    source = (1, 10) :: (1, 100) :: (2, 20) :: Nil,\n    target = (1, 1) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(values = \"(key, value) VALUES (s.key, s.value)\"))\n\n  testMergeErrorOnMultipleMatches(\n    \"conditional insert only merge - multiple matches when feature flag off\",\n    confs = Seq(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"false\"))(\n    source = (1, 10) :: (1, 100) :: (2, 20) :: (2, 200) :: Nil,\n    target = (1, 1) :: Nil,\n    mergeOn = \"s.key = t.key\",\n    insert(condition = \"s.value = 20\", values = \"(key, value) VALUES (s.key, s.value)\"))\n}\n\ntrait MergeIntoSuiteBaseMiscTests extends MergeIntoSuiteBaseMixin {\n  import testImplicits._\n\n  test(\"same column names in source and target\") {\n    withTable(\"source\") {\n      Seq((1, 5), (2, 9)).toDF(\"key\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as target\",\n        source = \"source src\",\n        condition = \"src.key = target.key\",\n        update = \"target.key = 20 + src.key, target.value = 20 + src.value\",\n        insert = \"(key, value) VALUES (src.key - 10, src.value + 10)\")\n\n      checkAnswer(\n        readDeltaTableByIdentifier(),\n        Row(21, 25) :: // Update\n          Row(22, 29) :: // Update\n          Nil)\n    }\n  }\n\n  test(\"Source is a query\") {\n    withTable(\"source\") {\n      Seq((1, 6, \"a\"), (0, 3, \"b\")).toDF(\"key1\", \"value\", \"others\")\n        .createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trg\",\n        source = \"(SELECT key1, value, others FROM source) src\",\n        condition = \"src.key1 = trg.key2\",\n        update = \"trg.key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(trg.key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(\n        readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(21, 26) :: // Update\n          Row(-10, 13) :: // Insert\n          Nil)\n\n      withCrossJoinEnabled {\n        executeMerge(\n        target = s\"$tableSQLIdentifier as trg\",\n        source = \"(SELECT 5 as key1, 5 as value) src\",\n        condition = \"src.key1 = trg.key2\",\n        update = \"trg.key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(trg.key2, value) VALUES (key1 - 10, src.value + 10)\")\n      }\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) ::\n          Row(21, 26) ::\n          Row(-10, 13) ::\n          Row(-5, 15) :: // new row\n          Nil)\n    }\n  }\n\n  test(\"self merge\") {\n    append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n    executeMerge(\n      target = s\"$tableSQLIdentifier as target\",\n      source = s\"$tableSQLIdentifier as src\",\n      condition = \"src.key2 = target.key2\",\n      update = \"key2 = 20 + src.key2, value = 20 + src.value\",\n      insert = \"(key2, value) VALUES (src.key2 - 10, src.value + 10)\")\n\n    checkAnswer(readDeltaTableByIdentifier(),\n      Row(22, 22) :: // UPDATE\n        Row(21, 24) :: // UPDATE\n        Nil)\n  }\n\n  test(\"order by + limit in source query #1\") {\n    withTable(\"source\") {\n      Seq((1, 6, \"a\"), (0, 3, \"b\")).toDF(\"key1\", \"value\", \"others\")\n        .createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trg\",\n        source = \"(SELECT key1, value, others FROM source order by key1 limit 1) src\",\n        condition = \"src.key1 = trg.key2\",\n        update = \"trg.key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(trg.key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(1, 4) :: // No change\n          Row(2, 2) :: // No change\n          Row(-10, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  test(\"order by + limit in source query #2\") {\n    withTable(\"source\") {\n      Seq((1, 6, \"a\"), (0, 3, \"b\")).toDF(\"key1\", \"value\", \"others\")\n        .createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trg\",\n        source = \"(SELECT key1, value, others FROM source order by value DESC limit 1) src\",\n        condition = \"src.key1 = trg.key2\",\n        update = \"trg.key2 = 20 + key1, value = 20 + src.value\",\n        insert = \"(trg.key2, value) VALUES (key1 - 10, src.value + 10)\")\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(2, 2) :: // No change\n          Row(21, 26) :: // UPDATE\n          Nil)\n    }\n  }\n\n  testQuietly(\"Negative case - more than one source rows match the same target row\") {\n    withTable(\"source\") {\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      val e = intercept[Exception] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n      }.toString\n\n      val expectedEx = DeltaErrors.multipleSourceRowMatchingTargetRowInMergeException(spark)\n      assert(e.contains(expectedEx.getMessage))\n    }\n  }\n\n  test(\"More than one target rows match the same source row\") {\n    withTable(\"source\") {\n      Seq((1, 5), (2, 9)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), Seq(\"key2\"))\n\n      withCrossJoinEnabled {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"key1 = 1\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n      }\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(-8, 19) :: // Insert\n          Row(21, 25) :: // Update\n          Row(21, 25) :: // Update\n          Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"Merge table using different data types - implicit casting, parts: $isPartitioned\") {\n      withTable(\"source\") {\n        Seq((1, \"5\"), (3, \"9\"), (3, \"a\")).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n        val partitions = if (isPartitioned) \"key2\" :: Nil else Nil\n        append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"), partitions)\n\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"key1 = key2\",\n          update = \"key2 = 33 + cast(key2 as double), value = '20'\",\n          insert = \"(key2, value) VALUES ('44', try_cast(src.value as double) + 10)\")\n\n        checkAnswer(readDeltaTableByIdentifier(),\n          Row(44, 19) :: // Insert\n          // NULL is generated when the type casting does not work for some values)\n          Row(44, null) :: // Insert\n          Row(34, 20) :: // Update\n          Row(2, 2) :: // No change\n          Nil)\n      }\n    }\n  }\n\n  test(\"Negative case - basic syntax analysis\") {\n    withTable(\"source\") {\n      Seq((1, 1), (0, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n\n      // insert expressions have target table reference\n      var e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = key1, value = src.value\",\n          insert = \"(key2, value) VALUES (3, src.value + key2)\")\n      }.getMessage\n\n      errorContains(e, \"cannot resolve key2\")\n\n      // to-update columns have source table reference\n      e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key1 = 1, value = 2\",\n          insert = \"(key2, value) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"Cannot resolve key1 in UPDATE clause\")\n      errorContains(e, \"key2\") // should show key2 as a valid name in target columns\n\n      // to-insert columns have source table reference\n      e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 1, value = 2\",\n          insert = \"(key1, value) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"Cannot resolve key1 in INSERT clause\")\n      errorContains(e, \"key2\") // should contain key2 as a valid name in target columns\n\n      // ambiguous reference\n      e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 1, value = value\",\n          insert = \"(key2, value) VALUES (3, 4)\")\n      }.getMessage\n\n      Seq(\"value\", \"is ambiguous\", \"could be\").foreach(x => errorContains(e, x))\n\n      // non-deterministic search condition\n      e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2 and rand() > 0.5\",\n          update = \"key2 = 1, value = 2\",\n          insert = \"(key2, value) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"Non-deterministic functions are not supported in the search condition\")\n\n      // aggregate function\n      e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2 and max(target.key2) > 20\",\n          update = \"key2 = 1, value = 2\",\n          insert = \"(key2, value) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"Aggregate functions are not supported in the search condition\")\n    }\n  }\n\n  test(\"Merge should use the same SparkSession consistently\", NameBasedAccessIncompatible) {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"false\") {\n        val r = dir.getCanonicalPath\n        val sourcePath = s\"$r/source\"\n        val targetPath = s\"$r/target\"\n        val numSourceRecords = 20\n        spark.range(numSourceRecords)\n          .withColumn(\"x\", $\"id\")\n          .withColumn(\"y\", $\"id\")\n          .write.mode(\"overwrite\").format(\"delta\").save(sourcePath)\n        spark.range(1)\n          .withColumn(\"x\", $\"id\")\n          .write.mode(\"overwrite\").format(\"delta\").save(targetPath)\n        val spark2 = spark.newSession\n        spark2.conf.set(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\")\n        val target = io.delta.tables.DeltaTable.forPath(spark2, targetPath)\n        val source = spark.read.format(\"delta\").load(sourcePath).alias(\"s\")\n        val merge = target.alias(\"t\")\n          .merge(source, \"t.id = s.id\")\n          .whenMatched.updateExpr(Map(\"t.x\" -> \"t.x + 1\"))\n          .whenNotMatched.insertAll()\n          .execute()\n        // The target table should have the same number of rows as the source after the merge\n        assert(spark.read.format(\"delta\").load(targetPath).count() == numSourceRecords)\n      }\n    }\n  }\n\n  test(\"Variant type\") {\n    withTable(\"source\") {\n      // Insert (\"0\", 0), (\"1\", 1)\n      val dstDf = sql(\n        \"\"\"SELECT parse_json(cast(id as string)) v, id i\n        FROM range(2)\"\"\")\n      append(dstDf)\n      // Insert (\"1\", 2), (\"2\", 3)\n      // The first row will update, the second will insert.\n      sql(\n        s\"\"\"SELECT parse_json(cast(id as string)) v, id + 1 i\n              FROM range(1, 3)\"\"\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trgNew\",\n        source = \"source src\",\n        condition = \"to_json(src.v) = to_json(trgNew.v)\",\n        update = \"i = 10 + src.i + trgNew.i, v = trgNew.v\",\n        insert = \"\"\"(i, v) VALUES (i + 100, parse_json('\"inserted\"'))\"\"\")\n\n      checkAnswer(readDeltaTableByIdentifier().selectExpr(\"i\", \"to_json(v)\"),\n        Row(0, \"0\") :: // No change\n          Row(13, \"1\") :: // Update\n          Row(103, \"\\\"inserted\\\"\") :: // Insert\n          Nil)\n    }\n  }\n\n  // Enable this test in OSS when Spark has the change to report better errors\n  // when MERGE is not supported.\n  ignore(\"Negative case - non-delta target\") {\n    withTable(\"source\", \"target\") {\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key2\", \"value\").write.saveAsTable(\"target\")\n\n      val e = intercept[AnalysisException] {\n        executeMerge(\n          target = \"target\",\n          source = \"source src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n      }.getMessage\n      assert(e.contains(\"does not support MERGE\") ||\n        // The MERGE Scala API is for Delta only and reports error differently.\n        e.contains(\"is not a Delta table\") ||\n        e.contains(\"MERGE destination only supports Delta sources\"))\n    }\n  }\n\n  test(\"Negative case - update assignments conflict because \" +\n    \"same column with different references\") {\n    withTable(\"source\") {\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      val e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"$tableSQLIdentifier as t\",\n          source = \"source s\",\n          condition = \"s.key1 = t.key2\",\n          update = \"key2 = key1, t.key2 = key1\",\n          insert = \"(key2, value) VALUES (3, 4)\")\n      }.getMessage\n\n      errorContains(e, \"there is a conflict from these set columns\")\n    }\n  }\n\n  test(\"Negative case - MERGE to the child directory\", NameBasedAccessIncompatible) {\n    withTempDir { tempDir =>\n      val tempPath = tempDir.getCanonicalPath\n      val df = Seq((1, 1), (0, 3), (1, 5)).toDF(\"key2\", \"value\")\n      val partitions = \"key2\" :: Nil\n      df.write.format(\"delta\").mode(\"append\").partitionBy(partitions: _*).save(tempPath)\n\n      val e = intercept[AnalysisException] {\n        executeMerge(\n          target = s\"delta.`$tempPath/key2=1` target\",\n          source = \"(SELECT 5 as key1, 5 as value) src\",\n          condition = \"src.key1 = target.key2\",\n          update = \"key2 = 20 + key1, value = 20 + src.value\",\n          insert = \"(key2, value) VALUES (key1 - 10, src.value + 10)\")\n      }.getMessage\n      errorContains(e, \"Expect a full scan of Delta sources, but found a partial scan\")\n    }\n  }\n\n  test(s\"special character in path - matched delete\", NameBasedAccessIncompatible) {\n    withTempDir { tempDir =>\n      val source = s\"$tempDir/sou rce~\"\n      val target = s\"$tempDir/tar get>\"\n      spark.range(0, 10, 2).write.format(\"delta\").save(source)\n      spark.range(10).write.format(\"delta\").save(target)\n      executeMerge(\n        tgt = s\"delta.`$target` t\",\n        src = s\"delta.`$source` s\",\n        cond = \"t.id = s.id\",\n        clauses = delete())\n      checkAnswer(readDeltaTableByIdentifier(s\"delta.`$target`\"), Seq(1, 3, 5, 7, 9).toDF(\"id\"))\n  }\n  }\n\n  test(s\"special character in path - matched update\", NameBasedAccessIncompatible) {\n    withTempDir { tempDir =>\n      val source = s\"$tempDir/sou rce(\"\n      val target = s\"$tempDir/tar get*\"\n      spark.range(0, 10, 2).write.format(\"delta\").save(source)\n      spark.range(10).write.format(\"delta\").save(target)\n      executeMerge(\n        tgt = s\"delta.`$target` t\",\n        src = s\"delta.`$source` s\",\n        cond = \"t.id = s.id\",\n        clauses = update(set = \"id = t.id * 10\"))\n      checkAnswer(readDeltaTableByIdentifier(s\"delta.`$target`\"),\n        Seq(0, 1, 20, 3, 40, 5, 60, 7, 80, 9).toDF(\"id\"))\n  }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"single file, isPartitioned: $isPartitioned\") {\n      withTable(\"source\") {\n        val df = spark.range(5).selectExpr(\"id as key1\", \"id as key2\", \"id as col1\").repartition(1)\n\n        val partitions = if (isPartitioned) \"key1\" :: \"key2\" :: Nil else Nil\n        append(df, partitions)\n\n        df.createOrReplaceTempView(\"source\")\n\n        executeMerge(\n          target = s\"$tableSQLIdentifier target\",\n          source = \"(SELECT key1 as srcKey, key2, col1 FROM source where key1 < 3) AS source\",\n          condition = \"srcKey = target.key1\",\n          update = \"target.key1 = srcKey - 1000, target.key2 = source.key2 + 1000, \" +\n            \"target.col1 = source.col1\",\n          insert = \"(key1, key2, col1) VALUES (srcKey, source.key2, source.col1)\")\n\n        checkAnswer(readDeltaTableByIdentifier(),\n          Row(-998, 1002, 2) :: // Update\n            Row(-999, 1001, 1) :: // Update\n            Row(-1000, 1000, 0) :: // Update\n            Row(4, 4, 4) :: // No change\n            Row(3, 3, 3) :: // No change\n            Nil)\n      }\n    }\n  }\n\n  test(\"merge into cached table\") {\n    // Merge with a cached target only works in the join-based implementation right now\n    withTable(\"source\") {\n      append(Seq((2, 2), (1, 4)).toDF(\"key2\", \"value\"))\n      Seq((1, 1), (0, 3), (3, 3)).toDF(\"key1\", \"value\").createOrReplaceTempView(\"source\")\n      spark.table(tableSQLIdentifier).cache()\n      spark.table(tableSQLIdentifier).collect()\n\n      append(Seq((100, 100), (3, 5)).toDF(\"key2\", \"value\"))\n      // cache is in effect, as the above change is not reflected\n      checkAnswer(spark.table(tableSQLIdentifier),\n        Row(2, 2) :: Row(1, 4) :: Row(100, 100) :: Row(3, 5) :: Nil)\n\n      executeMerge(\n        target = s\"$tableSQLIdentifier as trgNew\",\n        source = \"source src\",\n        condition = \"src.key1 = key2\",\n        update = \"value = trgNew.value + 3\",\n        insert = \"(key2, value) VALUES (key1, src.value + 10)\")\n\n      checkAnswer(spark.table(tableSQLIdentifier),\n        Row(100, 100) :: // No change (newly inserted record)\n          Row(2, 2) :: // No change\n          Row(1, 7) :: // Update\n          Row(3, 8) :: // Update (on newly inserted record)\n          Row(0, 13) :: // Insert\n          Nil)\n    }\n  }\n\n  def testStar(\n      name: String)(\n      source: Seq[String],\n      target: Seq[String],\n      mergeClauses: MergeClause*)(\n      result: Seq[String] = null,\n      errorStrs: Seq[String] = null) {\n\n    require(result == null ^ errorStrs == null, \"either set the result or the error strings\")\n    val testName =\n      if (result != null) s\"star syntax - $name\" else s\"star syntax - analysis error - $name\"\n\n    test(testName) {\n      withJsonData(source, target) { case (sourceName, targetName) =>\n        def execMerge() =\n          executeMerge(s\"$targetName t\", s\"$sourceName s\", \"s.key = t.key\", mergeClauses: _*)\n        if (result != null) {\n          execMerge()\n          checkAnswer(\n            readDeltaTableByIdentifier(targetName),\n            readFromJSON(result))\n        } else {\n          val e = intercept[AnalysisException] { execMerge() }\n          errorStrs.foreach { s => errorContains(e.getMessage, s) }\n        }\n      }\n    }\n  }\n\n  testStar(\"basic star expansion\")(\n    source =\n      \"\"\"{ \"key\": \"a\", \"value\" : 10 }\n         { \"key\": \"c\", \"value\" : 30 }\"\"\",\n    target =\n      \"\"\"{ \"key\": \"a\", \"value\" : 1 }\n         { \"key\": \"b\", \"value\" : 2 }\"\"\",\n    update(set = \"*\"),\n    insert(values = \"*\"))(\n    result =\n      \"\"\"{ \"key\": \"a\", \"value\" : 10 }\n         { \"key\": \"b\", \"value\" : 2   }\n         { \"key\": \"c\", \"value\" : 30 }\"\"\")\n\n  testStar(\"multiples columns and extra columns in source\")(\n    source =\n      \"\"\"{ \"key\": \"a\", \"value\" : 10, \"value2\" : 100, \"value3\" : 1000 }\n         { \"key\": \"c\", \"value\" : 30, \"value2\" : 300, \"value3\" : 3000 }\"\"\",\n    target =\n      \"\"\"{ \"key\": \"a\", \"value\" : 1, \"value2\" : 1 }\n         { \"key\": \"b\", \"value\" : 2, \"value2\" : 2 }\"\"\",\n    update(set = \"*\"),\n    insert(values = \"*\"))(\n    result =\n      \"\"\"{ \"key\": \"a\", \"value\" : 10, \"value2\" : 100 }\n         { \"key\": \"b\", \"value\" : 2,  \"value2\" : 2  }\n         { \"key\": \"c\", \"value\" : 30, \"value2\" : 300 }\"\"\")\n\n  test(\"insert only merge - turn off feature flag\") {\n    withSQLConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"false\") {\n      withKeyValueData(\n        source = (1, 10) :: (3, 30) :: Nil,\n        target = (1, 1) :: Nil\n      ) { case (sourceName, targetName) =>\n        insertOnlyMergeFeatureFlagOff(sourceName, targetName)\n      }\n    }\n  }\n\n  testMergeWithRepartition(\n    name = \"partition on multiple columns\",\n    partitionColumns = Seq(\"part1\", \"part2\"),\n    srcRange = Range(80, 110),\n    expectLessFilesWithRepartition = true,\n    update(\"t.part2 = 1\"),\n    insert(\"(id, part1, part2) VALUES (id, part1, part2)\")\n  )\n\n  testMergeWithRepartition(\n    name = \"insert only merge\",\n    partitionColumns = Seq(\"part1\"),\n    srcRange = Range(110, 150),\n    expectLessFilesWithRepartition = true,\n    insert(\"(id, part1, part2) VALUES (id, part1, part2)\")\n  )\n\n  testMergeWithRepartition(\n    name = \"non partitioned table\",\n    partitionColumns = Seq(),\n    srcRange = Range(80, 180),\n    expectLessFilesWithRepartition = false,\n    update(\"t.part2 = 1\"),\n    insert(\"(id, part1, part2) VALUES (id, part1, part2)\")\n  )\n\n  protected def testMatchedOnlyOptimization(\n      name: String)(\n      source: Seq[(Int, Int)],\n      target: Seq[(Int, Int)],\n      mergeOn: String,\n      mergeClauses: MergeClause*) (\n      result: Seq[(Int, Int)]): Unit = {\n    Seq(true, false).foreach { matchedOnlyEnabled =>\n      Seq(true, false).foreach { isPartitioned =>\n        val s = if (matchedOnlyEnabled) \"enabled\" else \"disabled\"\n        test(s\"matched only merge - $s - $name - isPartitioned: $isPartitioned \") {\n          withKeyValueData(source, target, isPartitioned) { case (sourceName, targetName) =>\n            withSQLConf(DeltaSQLConf.MERGE_MATCHED_ONLY_ENABLED.key -> s\"$matchedOnlyEnabled\") {\n              executeMerge(s\"$targetName t\", s\"$sourceName s\", mergeOn, mergeClauses: _*)\n            }\n            checkAnswer(\n              readDeltaTableByIdentifier(targetName),\n              result.map { case (k, v) => Row(k, v) })\n          }\n        }\n      }\n    }\n  }\n\n  testMatchedOnlyOptimization(\"with update\") (\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    mergeOn = \"s.key = t.key\",\n    update(\"t.key = s.key, t.value = s.value\")) (\n    result = Seq(\n      (1, 100), // updated\n      (2, 20), // existed previously\n      (3, 300) // updated\n    )\n  )\n\n  testMatchedOnlyOptimization(\"with delete\") (\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    mergeOn = \"s.key = t.key\",\n    delete()) (\n    result = Seq(\n      (2, 20) // existed previously\n    )\n  )\n\n  testMatchedOnlyOptimization(\"with update and delete\")(\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (3, 30), (5, 30)),\n    mergeOn = \"s.key = t.key\",\n    update(\"t.value = s.value\", \"t.key < 3\"), delete(\"t.key > 3\")) (\n    result = Seq(\n      (1, 100), // updated\n      (3, 30)   // existed previously\n    )\n  )\n\n  protected def testNullCaseMatchedOnly(name: String) (\n      source: Seq[(JInt, JInt)],\n      target: Seq[(JInt, JInt)],\n      mergeOn: String,\n      result: Seq[(JInt, JInt)]) = {\n    Seq(true, false).foreach { isPartitioned =>\n      withSQLConf(DeltaSQLConf.MERGE_MATCHED_ONLY_ENABLED.key -> \"true\") {\n        test(s\"matched only merge - null handling - $name, isPartitioned: $isPartitioned\") {\n          withView(\"sourceView\") {\n            val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n            append(target.toDF(\"key\", \"value\"), partitions)\n            source.toDF(\"key\", \"value\").createOrReplaceTempView(\"sourceView\")\n\n            executeMerge(\n              tgt = s\"$tableSQLIdentifier as t\",\n              src = \"sourceView s\",\n              cond = mergeOn,\n              update(\"t.value = s.value\"))\n\n            checkAnswer(\n              readDeltaTableByIdentifier(),\n              result.map { r => Row(r._1, r._2) }\n            )\n          }\n        }\n      }\n    }\n  }\n\n  testNullCaseMatchedOnly(\"null in source\") (\n    source = Seq((1, 10), (2, 20), (null, null)),\n    target = Seq((1, 1)),\n    mergeOn = \"s.key = t.key\",\n    result = Seq(\n      (1, 10) // update\n    )\n  )\n\n  testNullCaseMatchedOnly(\"null value in both source and target\") (\n    source = Seq((1, 10), (2, 20), (null, 0)),\n    target = Seq((1, 1), (null, null)),\n    mergeOn = \"s.key = t.key\",\n    result = Seq(\n      (null, null), // No change as null in source does not match null in target\n      (1, 10) // update\n    )\n  )\n\n  test(\"data skipping - target-only condition\") {\n    withKeyValueData(\n      source = (1, 10) :: Nil,\n      target = (1, 1) :: (2, 2) :: Nil,\n      isKeyPartitioned = true) { case (sourceName, targetName) =>\n\n      val report = getScanReport {\n        executeMerge(\n          target = s\"$targetName t\",\n          source = s\"$sourceName s\",\n          condition = \"s.key = t.key AND t.key <= 1\",\n          update = \"t.key = s.key, t.value = s.value\",\n          insert = \"(key, value) VALUES (s.key, s.value)\")\n      }.head\n\n      checkAnswer(readDeltaTableByIdentifier(),\n        Row(1, 10) ::  // Updated\n        Row(2, 2) ::   // File should be skipped\n        Nil)\n\n      assert(report.size(\"scanned\").bytesCompressed != report.size(\"total\").bytesCompressed)\n    }\n  }\n\n  test(\"insert only merge - target data skipping\") {\n    val tblName = \"merge_target\"\n    withTable(tblName) {\n      spark.range(10).withColumn(\"part\", 'id % 5).withColumn(\"value\", 'id + 'id)\n        .write.format(\"delta\").partitionBy(\"part\").mode(\"append\").saveAsTable(tblName)\n\n      val source = \"source\"\n      withTable(source) {\n        spark.range(20).withColumn(\"part\", functions.lit(1)).withColumn(\"value\", 'id + 'id)\n          .write.format(\"delta\").saveAsTable(source)\n\n        val scans = getScanReport {\n          withSQLConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"true\") {\n            executeMerge(\n              s\"$tblName t\",\n              s\"$source s\",\n              \"s.id = t.id AND t.part = 1\",\n              insert(condition = \"s.id % 5 = s.part\", values = \"*\"))\n          }\n        }\n        checkAnswer(\n          spark.table(tblName).where(\"part = 1\"),\n          Row(1, 1, 2) :: Row(6, 1, 12) :: Row(11, 1, 22) :: Row(16, 1, 32) :: Nil\n        )\n\n        assert(scans.length === 2, \"We should scan the source and target \" +\n          \"data once in an insert only optimization\")\n\n        // check if the source and target tables are scanned just once\n        val sourceRoot = DeltaTableUtils.findDeltaTableRoot(\n          spark, new Path(spark.table(source).inputFiles.head)).get.toString\n        val targetRoot = DeltaTableUtils.findDeltaTableRoot(\n          spark, new Path(spark.table(tblName).inputFiles.head)).get.toString\n        assert(scans.map(_.path).toSet == Set(sourceRoot, targetRoot))\n\n        // check scanned files\n        val targetScans = scans.find(_.path == targetRoot)\n        val deltaLog = DeltaLog.forTable(spark, targetScans.get.path)\n        val numTargetFiles = deltaLog.snapshot.numOfFiles\n        assert(targetScans.get.metrics(\"numFiles\") < numTargetFiles)\n        // check scanned sizes\n        val scanSizes = targetScans.head.size\n        assert(scanSizes(\"total\").bytesCompressed.get > scanSizes(\"scanned\").bytesCompressed.get,\n          \"Should have partition pruned target table\")\n      }\n    }\n  }\n\n  testMergeDataSkippingOnMatchPredicates(\"match conditions on target fields only\")(\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    dataSkippingOnTargetOnly = true,\n    isMatchedOnly = true,\n    update(condition = \"t.key == 10\", set = \"*\"),\n    update(condition = \"t.value == 100\", set = \"*\"))(\n    result = Seq((1, 10), (2, 20), (3, 30))\n  )\n\n  testMergeDataSkippingOnMatchPredicates(\"match conditions on source fields only\")(\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    dataSkippingOnTargetOnly = false,\n    isMatchedOnly = true,\n    update(condition = \"s.key == 10\", set = \"*\"),\n    update(condition = \"s.value == 10\", set = \"*\"))(\n    result = Seq((1, 10), (2, 20), (3, 30))\n  )\n\n  testMergeDataSkippingOnMatchPredicates(\"match on source and target fields\")(\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    dataSkippingOnTargetOnly = false,\n    isMatchedOnly = true,\n    update(condition = \"s.key == 10\", set = \"*\"),\n    update(condition = \"s.value == 10\", set = \"*\"),\n    delete(condition = \"t.key == 4\"))(\n    result = Seq((1, 10), (2, 20), (3, 30))\n  )\n\n  testMergeDataSkippingOnMatchPredicates(\"with insert clause\")(\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    dataSkippingOnTargetOnly = false,\n    isMatchedOnly = false,\n    update(condition = \"t.key == 10\", set = \"*\"),\n    insert(condition = null, values = \"(key, value) VALUES (s.key, s.value)\"))(\n    result = Seq((1, 10), (2, 20), (3, 30), (5, 500))\n  )\n\n  testMergeDataSkippingOnMatchPredicates(\"when matched and conjunction\")(\n    source = Seq((1, 100), (3, 300), (5, 500)),\n    target = Seq((1, 10), (2, 20), (3, 30)),\n    dataSkippingOnTargetOnly = true,\n    isMatchedOnly = true,\n    update(condition = \"t.key == 1 AND t.value == 5\", set = \"*\"))(\n    result = Seq((1, 10), (2, 20), (3, 30)))\n\n  test(\"SC-70829 - prevent re-resolution with star and schema evolution\") {\n    val source = \"source\"\n    val target = \"target\"\n    withTable(source, target) {\n\n      sql(s\"\"\"CREATE TABLE $source (id string, new string, old string, date DATE) USING delta\"\"\")\n      sql(s\"\"\"CREATE TABLE $target (id string, old string, date DATE) USING delta\"\"\")\n\n      withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n        executeMerge(\n          tgt = s\"$target t\",\n          src = s\"$source s\",\n          // functions like date_sub requires additional work to resolve\n          cond = \"s.id = t.id AND t.date >= date_sub(current_date(), 3)\",\n          update(set = \"*\"),\n          insert(values = \"*\"))\n      }\n    }\n  }\n\n  testUnsupportedExpression(\n    function = \"row_number\",\n    functionType = \"Window\",\n    sourceData = Seq((1, 2, 3)).toDF(\"a\", \"b\", \"c\"),\n    targetData = Seq((1, 5, 6)).toDF(\"a\", \"b\", \"c\"),\n    mergeCondition = \"(row_number() over (order by s.c)) = (row_number() over (order by t.c))\",\n    clauseCondition = \"row_number() over (order by s.c) > 1\",\n    clauseAction = \"row_number() over (order by s.c)\"\n  )\n\n  testUnsupportedExpression(\n    function = \"max\",\n    functionType = \"Aggregate\",\n    sourceData = Seq((1, 2, 3)).toDF(\"a\", \"b\", \"c\"),\n    targetData = Seq((1, 5, 6)).toDF(\"a\", \"b\", \"c\"),\n    mergeCondition = \"t.a = max(s.a)\",\n    clauseCondition = \"max(s.b) > 1\",\n    clauseAction = \"max(s.c)\",\n    customConditionErrorRegex =\n      Option(\"Aggregate functions are not supported in the .* condition of MERGE operation.*\")\n  )\n\n  test(\"merge correctly handle field metadata\") {\n    withTable(\"source\", \"target\") {\n      // Create a target table with user metadata (comments) and internal metadata (column mapping\n      // information) on both a top-level column and a nested field.\n      sql(\n        \"\"\"\n          |CREATE TABLE target(\n          |  key int not null COMMENT 'data column',\n          |  value int not null,\n          |  cstruct struct<foo int COMMENT 'foo field'>)\n          |USING DELTA\n          |TBLPROPERTIES (\n          |  'delta.minReaderVersion' = '2',\n          |  'delta.minWriterVersion' = '5',\n          |  'delta.columnMapping.mode' = 'name')\n        \"\"\".stripMargin\n      )\n      sql(s\"INSERT INTO target VALUES (0, 0, null)\")\n\n      sql(\"CREATE TABLE source (key int not null, value int not null) USING DELTA\")\n      sql(s\"INSERT INTO source VALUES (1, 1)\")\n\n      executeMerge(\n        tgt = \"target\",\n        src = \"source\",\n        cond = \"source.key = target.key\",\n        update(condition = \"target.key = 1\", set = \"target.value = 42\"),\n        updateNotMatched(condition = \"target.key = 100\", set = \"target.value = 22\"))\n    }\n  }\n\n  test(\"UDT Data Types - simple and nested\") {\n    withTable(\"source\") {\n      withTable(\"target\") {\n        // scalastyle:off line.size.limit\n        val targetData = Seq(\n          Row(SimpleTest(0), ComplexTest(10, Array(1, 2, 3))),\n          Row(SimpleTest(1), ComplexTest(20, Array(4, 5))),\n          Row(SimpleTest(2), ComplexTest(30, Array(6, 7, 8))))\n        val sourceData = Seq(\n          Row(SimpleTest(0), ComplexTest(40, Array(9, 10))),\n          Row(SimpleTest(3), ComplexTest(50, Array(11))))\n        val resultData = Seq(\n          Row(SimpleTest(0), ComplexTest(40, Array(9, 10))),\n          Row(SimpleTest(1), ComplexTest(20, Array(4, 5))),\n          Row(SimpleTest(2), ComplexTest(30, Array(6, 7, 8))),\n          Row(SimpleTest(3), ComplexTest(50, Array(11))))\n\n        val schema = StructType(Array(\n          StructField(\"id\", new SimpleTestUDT),\n          StructField(\"complex\", new ComplexTestUDT)))\n\n        val df = spark.createDataFrame(sparkContext.parallelize(targetData), schema)\n        df.collect()\n\n        spark.createDataFrame(sparkContext.parallelize(targetData), schema)\n          .write.format(\"delta\").saveAsTable(\"target\")\n\n        spark.createDataFrame(sparkContext.parallelize(sourceData), schema)\n          .write.format(\"delta\").saveAsTable(\"source\")\n        // scalastyle:on line.size.limit\n        sql(\n          s\"\"\"\n             |MERGE INTO target as t\n             |USING source as s\n             |ON t.id = s.id\n             |WHEN MATCHED THEN\n             |  UPDATE SET *\n             |WHEN NOT MATCHED THEN\n             |  INSERT *\n             \"\"\".stripMargin)\n\n        checkAnswer(sql(\"select * from target\"), resultData)\n      }\n    }\n  }\n  test(\"recorded operations - write all changes\") {\n    var events: Seq[UsageRecord] = Seq.empty\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n      target = (1, 1) :: (2, 2) :: (3, 3) :: (5, 5) :: (6, 6) :: Nil,\n      isKeyPartitioned = true) { case (sourceName, targetName) =>\n\n      events = Log4jUsageLogger.track {\n        executeMerge(\n          tgt = s\"$targetName t\",\n          src = s\"$sourceName s\",\n          cond = \"s.key = t.key\",\n          update(condition = \"s.key > 1\", set = \"key = s.key, value = s.value\"),\n          insert(condition = \"s.key < 1\", values = \"(key, value) VALUES (s.key, s.value)\"),\n          deleteNotMatched(condition = \"t.key > 5\"))\n      }\n\n      checkAnswer(readDeltaTableByIdentifier(targetName), Seq(\n        Row(0, 0),   // inserted\n        Row(1, 1),   // existed previously\n        Row(2, 20),  // updated\n        Row(3, 30),  // updated\n        Row(5, 5)    // existed previously\n        // Row(6, 6)    deleted\n      ))\n    }\n\n    // Get recorded operations from usage events\n    val opTypes = events.filter { e =>\n      e.metric == \"sparkOperationDuration\" && e.opType.get.typeName.contains(\"delta.dml.merge\")\n    }.map(_.opType.get.typeName).toSet\n\n    assert(opTypes == expectedOpTypes)\n  }\n\n  test(\"insert only merge - recorded operation\") {\n    var events: Seq[UsageRecord] = Seq.empty\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n      target = (1, 1) :: (2, 2) :: (3, 3) :: Nil,\n      isKeyPartitioned = true) { case (sourceName, targetName) =>\n\n      withSQLConf(DeltaSQLConf.MERGE_INSERT_ONLY_ENABLED.key -> \"true\") {\n        events = Log4jUsageLogger.track {\n          executeMerge(\n            tgt = s\"$targetName t\",\n            src = s\"$sourceName s\",\n            cond = \"s.key = t.key AND t.key > 1\",\n            insert(condition = \"s.key = 4\", values = \"(key, value) VALUES (s.key, s.value)\"))\n        }\n      }\n\n      checkAnswer(readDeltaTableByIdentifier(targetName), Seq(\n        Row(1, 1),  // existed previously\n        Row(2, 2),  // existed previously\n        Row(3, 3),  // existed previously\n        Row(4, 40)  // inserted\n      ))\n    }\n\n    // Get recorded operations from usage events\n    val opTypes = events.filter { e =>\n      e.metric == \"sparkOperationDuration\" && e.opType.get.typeName.contains(\"delta.dml.merge\")\n    }.map(_.opType.get.typeName).toSet\n\n    assert(opTypes == Set(\n      \"delta.dml.merge\",\n      \"delta.dml.merge.materializeSource\",\n      \"delta.dml.merge.writeInsertsOnlyWhenNoMatchedClauses\"))\n  }\n\n  test(\"recorded operations - write inserts only\") {\n    var events: Seq[UsageRecord] = Seq.empty\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: (2, 20) :: (3, 30) :: (4, 40) :: Nil,\n      target = (1, 1) :: (2, 2) :: (3, 3) :: Nil,\n      isKeyPartitioned = true) { case (sourceName, targetName) =>\n\n      events = Log4jUsageLogger.track {\n        executeMerge(\n          tgt = s\"$targetName t\",\n          src = s\"$sourceName s\",\n          cond = \"s.key = t.key AND s.key > 5\",\n          update(condition = \"s.key > 10\", set = \"key = s.key, value = s.value\"),\n          insert(condition = \"s.key < 1\", values = \"(key, value) VALUES (s.key, s.value)\"))\n      }\n\n      checkAnswer(readDeltaTableByIdentifier(targetName), Seq(\n        Row(0, 0),   // inserted\n        Row(1, 1),   // existed previously\n        Row(2, 2),   // existed previously\n        Row(3, 3)    // existed previously\n      ))\n    }\n\n    // Get recorded operations from usage events\n    val opTypes = events.filter { e =>\n      e.metric == \"sparkOperationDuration\" && e.opType.get.typeName.contains(\"delta.dml.merge\")\n    }.map(_.opType.get.typeName).toSet\n\n    assert(opTypes == expectedOpTypesInsertOnly)\n  }\n\n  test(\"merge execution is recorded with QueryExecutionListener\") {\n    withKeyValueData(\n      source = (0, 0) :: (1, 10) :: Nil,\n      target = (1, 1) :: (2, 2) :: Nil) { case (sourceName, targetName) =>\n      val plans = withLogicalPlansCaptured(spark, optimizedPlan = false) {\n        executeMerge(\n          tgt = s\"$targetName t\",\n          src = s\"$sourceName s\",\n          cond = \"s.key = t.key\",\n          update(set = \"*\"))\n      }\n      val mergeCommands = plans.collect {\n        case m: MergeIntoCommand => m\n      }\n      assert(mergeCommands.size === 1,\n        \"Merge command wasn't properly recorded by QueryExecutionListener\")\n    }\n  }\n\n  test(\"merge on partitioned table with special chars\") {\n    withTable(\"source\") {\n      val part1 = \"part%1\"\n      val part2 = \"part%2\"\n      val part3 = \"part%3\"\n      val part4 = \"part%4\"\n\n      for (part <- Seq(part1, part2, part3)) {\n        writeTable(\n          spark.range(0, 3, 1, 1)\n            .toDF(\"key\")\n            .withColumn(\"value\", functions.lit(part))\n            .write.format(\"delta\")\n            .partitionBy(\"value\")\n            .mode(\"append\"),\n          tableSQLIdentifier)\n      }\n\n      Seq(\n        (0, part1),\n        (0, part2), (1, part2),\n        (0, part3), (1, part3), (2, part3),\n        (0, part4)\n      ).toDF(\"key\", \"value\").createOrReplaceTempView(\"source\")\n\n      executeMerge(\n        tgt = s\"$tableSQLIdentifier t\",\n        src = \"source s\",\n        cond = \"t.key = s.key AND t.value = s.value\",\n        delete(condition = s\"s.value = '$part2'\"),\n        update(set = s\"t.key = -1\"),\n        insert(\"*\")\n      )\n      checkAnswer(\n        readDeltaTableByIdentifier(),\n        Row(-1, part1) :: Row(1, part1) :: Row(2, part1) ::\n          Row(2, part2) ::\n          Row(-1, part3) :: Row(-1, part3) :: Row(-1, part3) ::\n          Row(0, part4) ::\n          Nil)\n    }\n  }\n}\n\n@SQLUserDefinedType(udt = classOf[SimpleTestUDT])\ncase class SimpleTest(value: Int)\n\nclass SimpleTestUDT extends UserDefinedType[SimpleTest] {\n  override def sqlType: DataType = IntegerType\n\n  override def serialize(input: SimpleTest): Any = input.value\n\n  override def deserialize(datum: Any): SimpleTest = datum match {\n    case a: Int => SimpleTest(a)\n  }\n\n  override def userClass: Class[SimpleTest] = classOf[SimpleTest]\n}\n\n@SQLUserDefinedType(udt = classOf[ComplexTestUDT])\ncase class ComplexTest(key: Int, values: Array[Int])\n\nclass ComplexTestUDT extends UserDefinedType[ComplexTest] {\n  override def sqlType: DataType = StructType(Seq(\n    StructField(\"key\", IntegerType),\n    StructField(\"values\", ArrayType(IntegerType, containsNull = false))))\n\n  override def serialize(input: ComplexTest): Any = {\n    val row = new GenericInternalRow(2)\n    row.setInt(0, input.key)\n    row.update(1, UnsafeArrayData.fromPrimitiveArray(input.values))\n    row\n  }\n\n  override def deserialize(datum: Any): ComplexTest = datum match {\n    case row: InternalRow =>\n      ComplexTest(row.getInt(0), row.getArray(1).toIntArray())\n  }\n\n  override def userClass: Class[ComplexTest] = classOf[ComplexTest]\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport io.delta.tables._\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Base trait collecting helper methods to run MERGE tests. Merge test suite will want to mix in\n * either [[MergeIntoSQLTestUtils]] or [[MergeIntoScalaTestUtils]] to run merge tests using the SQL\n * or Scala API resp.\n */\ntrait MergeIntoTestUtils extends DeltaDMLTestUtils with MergeHelpers {\n  self: SharedSparkSession =>\n\n  protected def executeMerge(\n      target: String,\n      source: String,\n      condition: String,\n      update: String,\n      insert: String): Unit\n\n  protected def executeMerge(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): Unit\n\n  protected def executeMergeWithSchemaEvolution(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): Unit\n\n  protected def withCrossJoinEnabled(body: => Unit): Unit = {\n    withSQLConf(SQLConf.CROSS_JOINS_ENABLED.key -> \"true\") { body }\n  }\n}\n\ntrait MergeIntoSQLTestUtils extends DeltaSQLTestUtils with MergeIntoTestUtils {\n  self: SharedSparkSession =>\n\n  protected def basicMergeStmt(\n      cte: Option[String] = None,\n      target: String,\n      source: String,\n      condition: String,\n      update: String,\n      insert: String): String = {\n    basicMergeStmt(\n      cte = cte,\n      target = target,\n      source = source,\n      condition = condition,\n      withSchemaEvolution = false,\n      super.update(set = update),\n      super.insert(values = insert))\n  }\n\n  protected def basicMergeStmt(\n      cte: Option[String],\n      target: String,\n      source: String,\n      condition: String,\n      withSchemaEvolution: Boolean,\n      clauses: MergeClause*): String = {\n    val clausesStr = clauses.map(_.sql).mkString(\"\\n\")\n    val schemaEvolutionStr = if (withSchemaEvolution) \"WITH SCHEMA EVOLUTION\" else \"\"\n    s\"\"\"\n     |${cte.getOrElse(\"\")}\n     |MERGE $schemaEvolutionStr INTO $target\n     |USING $source\n     |ON $condition\n     |$clausesStr\n     \"\"\".stripMargin\n  }\n\n  override protected def executeMerge(\n      target: String,\n      source: String,\n      condition: String,\n      update: String,\n      insert: String): Unit =\n    spark.sql(basicMergeStmt(cte = None, target, source, condition, update, insert))\n\n  override protected def executeMerge(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): Unit = {\n    spark.sql(basicMergeStmt(cte = None, tgt, src, cond, withSchemaEvolution = false, clauses: _*))\n  }\n\n  override protected def executeMergeWithSchemaEvolution(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): Unit = {\n    throw new UnsupportedOperationException(\n      \"The SQL syntax [WITH SCHEMA EVOLUTION] is not yet supported.\")\n  }\n}\n\ntrait MergeIntoScalaTestUtils extends MergeIntoTestUtils {\n  self: SharedSparkSession =>\n\n  override protected def executeMerge(\n      target: String,\n      source: String,\n      condition: String,\n      update: String,\n      insert: String): Unit = {\n    executeMerge(\n      tgt = target,\n      src = source,\n      cond = condition,\n      this.update(set = update),\n      this.insert(values = insert))\n  }\n\n  override protected def executeMerge(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): Unit =\n    getMergeBuilder(tgt, src, cond, clauses: _*).execute()\n\n  override protected def executeMergeWithSchemaEvolution(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): Unit =\n    getMergeBuilder(tgt, src, cond, clauses: _*).withSchemaEvolution().execute()\n\n  private def getMergeBuilder(\n      tgt: String,\n      src: String,\n      cond: String,\n      clauses: MergeClause*): DeltaMergeBuilder = {\n    def buildClause(clause: MergeClause, mergeBuilder: DeltaMergeBuilder)\n      : DeltaMergeBuilder = clause match {\n      case _: MatchedClause =>\n        val actionBuilder: DeltaMergeMatchedActionBuilder =\n          if (clause.condition != null) mergeBuilder.whenMatched(clause.condition)\n          else mergeBuilder.whenMatched()\n        if (clause.action.startsWith(\"DELETE\")) {   // DELETE clause\n          actionBuilder.delete()\n        } else {                                    // UPDATE clause\n          val setColExprStr = clause.action.trim.stripPrefix(\"UPDATE SET\")\n          if (setColExprStr.trim == \"*\") {          // UPDATE SET *\n            actionBuilder.updateAll()\n          } else if (setColExprStr.contains(\"array_\")) { // UPDATE SET x = array_union(..)\n            val setColExprPairs = parseUpdate(Seq(setColExprStr))\n            actionBuilder.updateExpr(setColExprPairs)\n          } else {                                 // UPDATE SET x = a, y = b, z = c\n            val setColExprPairs = parseUpdate(setColExprStr.split(\",\"))\n            actionBuilder.updateExpr(setColExprPairs)\n          }\n        }\n      case _: NotMatchedClause =>                     // INSERT clause\n        val actionBuilder: DeltaMergeNotMatchedActionBuilder =\n          if (clause.condition != null) mergeBuilder.whenNotMatched(clause.condition)\n          else mergeBuilder.whenNotMatched()\n        val valueStr = clause.action.trim.stripPrefix(\"INSERT\")\n        if (valueStr.trim == \"*\") {                   // INSERT *\n          actionBuilder.insertAll()\n        } else {                                      // INSERT (x, y, z) VALUES (a, b, c)\n          val valueColExprsPairs = parseInsert(valueStr, Some(clause))\n          actionBuilder.insertExpr(valueColExprsPairs)\n        }\n      case _: NotMatchedBySourceClause =>\n        val actionBuilder: DeltaMergeNotMatchedBySourceActionBuilder =\n          if (clause.condition != null) mergeBuilder.whenNotMatchedBySource(clause.condition)\n          else mergeBuilder.whenNotMatchedBySource()\n        if (clause.action.startsWith(\"DELETE\")) { // DELETE clause\n          actionBuilder.delete()\n        } else { // UPDATE clause\n          val setColExprStr = clause.action.trim.stripPrefix(\"UPDATE SET\")\n          if (setColExprStr.contains(\"array_\")) { // UPDATE SET x = array_union(..)\n            val setColExprPairs = parseUpdate(Seq(setColExprStr))\n            actionBuilder.updateExpr(setColExprPairs)\n          } else { // UPDATE SET x = a, y = b, z = c\n            val setColExprPairs = parseUpdate(setColExprStr.split(\",\"))\n            actionBuilder.updateExpr(setColExprPairs)\n          }\n        }\n      }\n\n    val deltaTable = DeltaTestUtils.getDeltaTableForIdentifierOrPath(\n      spark,\n      DeltaTestUtils.getTableIdentifierOrPath(tgt))\n\n    val sourceDataFrame: DataFrame = {\n      val (tableOrQuery, optionalAlias) = DeltaTestUtils.parseTableAndAlias(src)\n      var df =\n        if (tableOrQuery.startsWith(\"(\")) spark.sql(tableOrQuery) else spark.table(tableOrQuery)\n      optionalAlias.foreach { alias => df = df.as(alias) }\n      df\n    }\n\n    var mergeBuilder = deltaTable.merge(sourceDataFrame, cond)\n    clauses.foreach { clause =>\n      mergeBuilder = buildClause(clause, mergeBuilder)\n    }\n    mergeBuilder\n  }\n\n  protected def parseUpdate(update: Seq[String]): Map[String, String] = {\n    update.map { _.split(\"=\").toList }.map {\n      case setCol :: setExpr :: Nil => setCol.trim -> setExpr.trim\n      case _ => fail(\"error parsing update actions \" + update)\n    }.toMap\n  }\n\n  protected def parseInsert(valueStr: String, clause: Option[MergeClause]): Map[String, String] = {\n    valueStr.split(\"VALUES\").toList match {\n      case colsStr :: exprsStr :: Nil =>\n        def parse(str: String): Seq[String] = {\n          str.trim.stripPrefix(\"(\").stripSuffix(\")\").split(\",\").map(_.trim)\n        }\n        val cols = parse(colsStr)\n        val exprs = parse(exprsStr)\n        require(cols.size == exprs.size,\n          s\"Invalid insert action ${clause.get.action}: cols = $cols, exprs = $exprs\")\n        cols.zip(exprs).toMap\n\n      case list =>\n        fail(s\"Invalid insert action ${clause.get.action} split into $list\")\n    }\n  }\n\n  protected def parsePath(nameOrPath: String): String = {\n    if (nameOrPath.startsWith(\"delta.`\")) {\n      nameOrPath.stripPrefix(\"delta.`\").stripSuffix(\"`\")\n    } else nameOrPath\n  }\n}\n\ntrait MergeHelpers {\n  /** A simple representative of a any WHEN clause in a MERGE statement */\n  protected sealed trait MergeClause {\n    def condition: String\n    def action: String\n    def clause: String\n    def sql: String = {\n      assert(action != null, \"action not specified yet\")\n      val cond = if (condition != null) s\"AND $condition\" else \"\"\n      s\"WHEN $clause $cond THEN $action\"\n    }\n  }\n\n  protected case class MatchedClause(condition: String, action: String) extends MergeClause {\n    override def clause: String = \"MATCHED\"\n  }\n\n  protected case class NotMatchedClause(condition: String, action: String) extends MergeClause {\n    override def clause: String = \"NOT MATCHED\"\n  }\n\n  protected case class NotMatchedBySourceClause(condition: String, action: String)\n    extends MergeClause {\n    override def clause: String = \"NOT MATCHED BY SOURCE\"\n  }\n\n  protected def update(set: String = null, condition: String = null): MergeClause = {\n    MatchedClause(condition, s\"UPDATE SET $set\")\n  }\n\n  protected def delete(condition: String = null): MergeClause = {\n    MatchedClause(condition, s\"DELETE\")\n  }\n\n  protected def insert(values: String = null, condition: String = null): MergeClause = {\n    NotMatchedClause(condition, s\"INSERT $values\")\n  }\n\n  protected def updateNotMatched(set: String = null, condition: String = null): MergeClause = {\n    NotMatchedBySourceClause(condition, s\"UPDATE SET $set\")\n  }\n\n  protected def deleteNotMatched(condition: String = null): MergeClause = {\n    NotMatchedBySourceClause(condition, s\"DELETE\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/MergeIntoTimestampConsistencySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Timestamp\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.expressions.{CurrentTimestamp, Now}\nimport org.apache.spark.sql.functions.{current_timestamp, lit, timestamp_seconds}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\nclass MergeIntoTimestampConsistencySuite extends MergeIntoTimestampConsistencySuiteBase {\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key, \"false\")\n  }\n}\n\n\nabstract class MergeIntoTimestampConsistencySuiteBase extends QueryTest\n    with SharedSparkSession with DeltaSQLCommandTest {\n  private def withTestTables(block: => Unit): Unit = {\n    def setupTablesAndRun(): Unit = {\n      spark.range(0, 5)\n        .toDF(\"id\")\n        .withColumn(\"updated\", lit(false))\n        .withColumn(\"timestampOne\", timestamp_seconds(lit(1)))\n        .withColumn(\"timestampTwo\", timestamp_seconds(lit(1337)))\n        .write\n        .format(\"delta\")\n        .saveAsTable(\"target\")\n      spark.range(0, 10)\n        .toDF(\"id\")\n        .withColumn(\"updated\", lit(true))\n        .withColumn(\"timestampOne\", current_timestamp())\n        .withColumn(\"timestampTwo\", current_timestamp())\n        .createOrReplaceTempView(\"source\")\n\n      block\n    }\n\n    Utils.tryWithSafeFinally(setupTablesAndRun) {\n      sql(\"DROP VIEW IF EXISTS source\")\n      sql(\"DROP TABLE IF EXISTS target\")\n    }\n  }\n\n  test(\"Consistent timestamps between source and ON condition\") {\n    withTestTables {\n      sql(s\"\"\"MERGE INTO target t\n            | USING source s\n            | ON s.id = t.id AND s.timestampOne = now()\n            | WHEN MATCHED THEN UPDATE SET *\"\"\".stripMargin)\n\n      assertAllRowsAreUpdated()\n    }\n  }\n\n  test(\"Consistent timestamps between source and WHEN MATCHED condition\") {\n    withTestTables {\n      sql(s\"\"\"MERGE INTO target t\n            | USING source s\n            | ON s.id = t.id\n            | WHEN MATCHED AND s.timestampOne = now() AND s.timestampTwo = now()\n            |   THEN UPDATE SET *\"\"\".stripMargin)\n\n      assertAllRowsAreUpdated()\n    }\n  }\n\n  test(\"Consistent timestamps between source and UPDATE SET\") {\n    withTestTables {\n      sql(\n        s\"\"\"MERGE INTO target t\n          | USING source s\n          | ON s.id = t.id\n          | WHEN MATCHED THEN UPDATE\n          |   SET updated = s.updated, t.timestampOne = s.timestampOne, t.timestampTwo = now()\n          |\"\"\".stripMargin)\n\n      assertUpdatedTimestampsInTargetAreAllEqual()\n    }\n  }\n\n  test(\"Consistent timestamps between source and WHEN NOT MATCHED condition\") {\n    withTestTables {\n      sql(s\"\"\"MERGE INTO target t\n            | USING source s\n            | ON s.id = t.id\n            | WHEN NOT MATCHED AND s.timestampOne = now() AND s.timestampTwo = now()\n            |   THEN INSERT *\n            |\"\"\".stripMargin)\n\n      assertNewSourceRowsInserted()\n    }\n  }\n\n  test(\"Consistent timestamps between source and INSERT VALUES\") {\n    withTestTables {\n      sql(\n        s\"\"\"MERGE INTO target t\n          | USING source s\n          | ON s.id = t.id\n          | WHEN NOT MATCHED THEN INSERT (id, updated, timestampOne, timestampTwo)\n          |   VALUES (s.id, s.updated, s.timestampOne, now())\n          |\"\"\".stripMargin)\n\n      assertUpdatedTimestampsInTargetAreAllEqual()\n    }\n  }\n\n  test(\"Consistent timestamps with subquery in source\") {\n    withTestTables {\n      val sourceWithSubqueryTable = \"source_with_subquery\"\n      withTempView(s\"$sourceWithSubqueryTable\") {\n        sql(\n          s\"\"\"CREATE OR REPLACE TEMPORARY VIEW $sourceWithSubqueryTable\n            | AS SELECT * FROM source WHERE timestampOne IN (SELECT now())\n            |\"\"\".stripMargin).collect()\n\n        sql(s\"\"\"MERGE INTO target t\n              | USING $sourceWithSubqueryTable s\n              | ON s.id = t.id\n              | WHEN MATCHED THEN UPDATE SET *\"\"\".stripMargin)\n\n        assertAllRowsAreUpdated()\n      }\n    }\n  }\n\n\n  private def assertAllRowsAreUpdated(): Unit = {\n    val nonUpdatedRowsCount = sql(\"SELECT * FROM target WHERE updated = FALSE\").count()\n    assert(0 === nonUpdatedRowsCount, \"Un-updated rows in target table\")\n  }\n\n  private def assertNewSourceRowsInserted(): Unit = {\n    val numNotInsertedSourceRows =\n      sql(\"SELECT * FROM source s LEFT ANTI JOIN target t ON s.id = t.id\").count()\n    assert(0 === numNotInsertedSourceRows, \"Un-inserted rows in source table\")\n  }\n\n  private def assertUpdatedTimestampsInTargetAreAllEqual(): Unit = {\n    import testImplicits._\n\n    val timestampCombinations =\n      sql(s\"\"\"SELECT timestampOne, timestampTwo\n            | FROM target WHERE updated = TRUE GROUP BY timestampOne, timestampTwo\n            |\"\"\".stripMargin)\n    val rows = timestampCombinations.as[(Timestamp, Timestamp)].collect()\n    assert(1 === rows.length, \"Multiple combinations of timestamp values in target table\")\n    assert(rows(0)._1 === rows(0)._2,\n      \"timestampOne and timestampTwo are not equal in target table\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/NonFateSharingFutureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.concurrent.duration._\nimport scala.util.control.ControlThrowable\n\nimport org.apache.spark.sql.delta.util.threads.DeltaThreadPool\n\nimport org.apache.spark.{SparkException, SparkFunSuite}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass NonFateSharingFutureSuite extends SparkFunSuite with SharedSparkSession {\n  test(\"function only runs once on success\") {\n    val count = new AtomicInteger\n    val future = DeltaThreadPool(\"test\", 1).submitNonFateSharing { _ => count.incrementAndGet }\n    assert(future.get(10.seconds) === 1)\n    assert(future.get(10.seconds) === 1)\n    spark.cloneSession().withActive {\n      assert(future.get(10.seconds) === 1)\n    }\n  }\n\n  test(\"non-fatal exception in future is ignored\") {\n    val count = new AtomicInteger\n    val future = DeltaThreadPool(\"test\", 1).submitNonFateSharing { _ =>\n      count.incrementAndGet match {\n        case 1 => throw new Exception\n        case i => i\n      }\n    }\n\n    // Make sure the future already failed before waiting on it. This should happen ~immediately\n    // unless the test runner is horribly overloaded/slow/etc, and stabilizes the assertions below.\n    eventually(timeout(100.seconds)) {\n      assert(count.get == 1)\n    }\n\n    spark.cloneSession().withActive {\n      assert(future.get(1.seconds) === 2)\n    }\n    assert(future.get(1.seconds) === 3)\n\n    spark.cloneSession().withActive {\n      assert(future.get(1.seconds) === 4)\n    }\n    assert(future.get(1.seconds) === 5)\n  }\n\n  test(\"fatal exception in future only propagates once, and only to owning session\") {\n    val count = new AtomicInteger\n    val future = DeltaThreadPool(\"test\", 1).submitNonFateSharing { _ =>\n      count.incrementAndGet match {\n        case 1 => throw new InternalError\n        case i => i\n      }\n    }\n\n    // Make sure the future already failed before waiting on it. This should happen ~immediately\n    // unless the test runner is horribly overloaded/slow/etc, and stabilizes the assertions below.\n    eventually(timeout(100.seconds)) {\n      assert(count.get == 1)\n    }\n\n    spark.cloneSession().withActive {\n      assert(future.get(1.seconds) === 2)\n    }\n    intercept[InternalError] {\n      future.get(1.seconds)\n    }\n    spark.cloneSession().withActive {\n      assert(future.get(1.seconds) === 3)\n    }\n    assert(future.get(1.seconds) === 4)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/OptimisticTransactionLegacyTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile, FileAction, Metadata, RemoveFile, SetTransaction}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{StringType, StructField, StructType}\n\n// These tests are potentially a subset of the tests already in OptimisticTransactionSuite.\n// These tests can potentially be removed but only after confirming that these tests are\n// truly a subset of the tests in OptimisticTransactionSuite.\ntrait OptimisticTransactionLegacyTests\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  private val addA = createTestAddFile(encodedPath = \"a\")\n  private val addB = createTestAddFile(encodedPath = \"b\")\n  private val addC = createTestAddFile(encodedPath = \"c\")\n\n  import testImplicits._\n\n  test(\"block append against metadata change\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log.\n      log.startTransaction().commitManually()\n\n      val txn = log.startTransaction()\n      val winningTxn = log.startTransaction()\n      winningTxn.commit(Metadata() :: Nil, ManualUpdate)\n      intercept[MetadataChangedException] {\n        txn.commit(addA :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block read+append against append\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log.\n      log.startTransaction().commitManually()\n\n      val txn = log.startTransaction()\n      // reads the table\n      txn.filterFiles()\n      val winningTxn = log.startTransaction()\n      winningTxn.commit(addA :: Nil, ManualUpdate)\n      intercept[ConcurrentAppendException] {\n        txn.commit(addB :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"allow blind-append against any data change\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log and add data.\n      log.startTransaction().commitManually(addA)\n\n      val txn = log.startTransaction()\n      val winningTxn = log.startTransaction()\n      winningTxn.commit(addA.remove :: addB :: Nil, ManualUpdate)\n      txn.commit(addC :: Nil, ManualUpdate)\n      checkAnswer(log.update().allFiles.select(\"path\"), Row(\"b\") :: Row(\"c\") :: Nil)\n    }\n  }\n\n  test(\"allow read+append+delete against no data change\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log and add data. ManualUpdate is just a no-op placeholder.\n      log.startTransaction().commitManually(addA)\n\n      val txn = log.startTransaction()\n      txn.filterFiles()\n      val winningTxn = log.startTransaction()\n      winningTxn.commit(Nil, ManualUpdate)\n      txn.commit(addA.remove :: addB :: Nil, ManualUpdate)\n      checkAnswer(log.update().allFiles.select(\"path\"), Row(\"b\") :: Nil)\n    }\n  }\n\n\n  val A_P1 = \"part=1/a\"\n  val B_P1 = \"part=1/b\"\n  val C_P1 = \"part=1/c\"\n  val C_P2 = \"part=2/c\"\n  val D_P2 = \"part=2/d\"\n  val E_P3 = \"part=3/e\"\n  val F_P3 = \"part=3/f\"\n  val G_P4 = \"part=4/g\"\n\n  private val addA_P1 = AddFile(A_P1, Map(\"part\" -> \"1\"), 1, 1, dataChange = true)\n  private val addB_P1 = AddFile(B_P1, Map(\"part\" -> \"1\"), 1, 1, dataChange = true)\n  private val addC_P1 = AddFile(C_P1, Map(\"part\" -> \"1\"), 1, 1, dataChange = true)\n  private val addC_P2 = AddFile(C_P2, Map(\"part\" -> \"2\"), 1, 1, dataChange = true)\n  private val addD_P2 = AddFile(D_P2, Map(\"part\" -> \"2\"), 1, 1, dataChange = true)\n  private val addE_P3 = AddFile(E_P3, Map(\"part\" -> \"3\"), 1, 1, dataChange = true)\n  private val addF_P3 = AddFile(F_P3, Map(\"part\" -> \"3\"), 1, 1, dataChange = true)\n  private val addG_P4 = AddFile(G_P4, Map(\"part\" -> \"4\"), 1, 1, dataChange = true)\n\n  test(\"allow concurrent commit on disjoint partitions\") {\n    withLog(addA_P1 :: addE_P3 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 reads P3 (but not P1)\n      val tx1Read = tx1.filterFiles(('part === 3).expr :: Nil)\n      assert(tx1Read.map(_.path) == E_P3 :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies only P1\n      tx2.commit(addB_P1 :: Nil, ManualUpdate)\n\n      // free to commit because P1 modified by TX2 was not read\n      tx1.commit(addC_P2 :: addE_P3.remove :: Nil, ManualUpdate)\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(A_P1) :: // start (E_P3 was removed by TX1)\n        Row(B_P1) :: // TX2\n        Row(C_P2) :: Nil) // TX1\n    }\n  }\n\n  test(\"allow concurrent commit on disjoint partitions reading all partitions\") {\n    withLog(addA_P1 :: addD_P2 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 read P1\n      tx1.filterFiles(('part isin 1).expr :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addC_P2 :: addD_P2.remove :: Nil, ManualUpdate)\n\n      tx1.commit(addE_P3 :: addF_P3 :: Nil, ManualUpdate)\n\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(A_P1) :: // start\n        Row(C_P2) :: // TX2\n        Row(E_P3) :: Row(F_P3) :: Nil) // TX1\n    }\n  }\n\n  test(\"block concurrent commit when read partition was appended to by concurrent write\") {\n    withLog(addA_P1 :: addD_P2 :: addE_P3 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 reads only P1\n      val tx1Read = tx1.filterFiles(('part === 1).expr :: Nil)\n      assert(tx1Read.map(_.path) == A_P1 :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies only P1\n      tx2.commit(addB_P1 :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // P1 was modified\n        tx1.commit(addC_P2 :: addE_P3 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent commit on full table scan\") {\n    withLog(addA_P1 :: addD_P2 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 full table scan\n      tx1.filterFiles()\n      tx1.filterFiles(('part === 1).expr :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addC_P2 :: addD_P2.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        tx1.commit(addE_P3 :: addF_P3 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  val A_1_1 = \"a=1/b=1/a\"\n  val B_1_2 = \"a=1/b=2/b\"\n  val C_2_1 = \"a=2/b=1/c\"\n  val D_3_1 = \"a=3/b=1/d\"\n\n  val addA_1_1_nested = AddFile(\n    A_1_1, Map(\"a\" -> \"1\", \"b\" -> \"1\"),\n    1, 1, dataChange = true)\n  val addB_1_2_nested = AddFile(\n    B_1_2, Map(\"a\" -> \"1\", \"b\" -> \"2\"),\n    1, 1, dataChange = true)\n  val addC_2_1_nested = AddFile(\n    C_2_1, Map(\"a\" -> \"2\", \"b\" -> \"1\"),\n    1, 1, dataChange = true)\n  val addD_3_1_nested = AddFile(\n    D_3_1, Map(\"a\" -> \"3\", \"b\" -> \"1\"),\n    1, 1, dataChange = true)\n\n  test(\"allow concurrent adds to disjoint nested partitions when read is disjoint from write\") {\n    withLog(addA_1_1_nested :: Nil, partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 reads a=1/b=1\n      val tx1Read = tx1.filterFiles(('a === 1 and 'b === 1).expr :: Nil)\n      assert(tx1Read.map(_.path) == A_1_1 :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 reads all partitions and modifies only a=1/b=2\n      tx2.commit(addB_1_2_nested :: Nil, ManualUpdate)\n\n      // TX1 reads a=1/b=1 which was not modified by TX2, hence TX1 can write to a=2/b=1\n      tx1.commit(addC_2_1_nested :: Nil, ManualUpdate)\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(A_1_1) :: // start\n        Row(B_1_2) :: // TX2\n        Row(C_2_1) :: Nil) // TX1\n    }\n  }\n\n  test(\"allow concurrent adds to same nested partitions when read is disjoint from write\") {\n    withLog(addA_1_1_nested :: Nil, partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 reads a=1/b=1\n      val tx1Read = tx1.filterFiles(('a === 1 and 'b === 1).expr :: Nil)\n      assert(tx1Read.map(_.path) == A_1_1 :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies a=1/b=2\n      tx2.commit(addB_1_2_nested :: Nil, ManualUpdate)\n\n      // TX1 reads a=1/b=1 which was not modified by TX2, hence TX1 can write to a=2/b=1\n      val add = AddFile(\n        \"a=1/b=2/x\", Map(\"a\" -> \"1\", \"b\" -> \"2\"),\n        1, 1, dataChange = true)\n      tx1.commit(add :: Nil, ManualUpdate)\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(A_1_1) :: // start\n        Row(B_1_2) :: // TX2\n        Row(\"a=1/b=2/x\") :: Nil) // TX1\n    }\n  }\n\n  test(\"allow concurrent add when read at lvl1 partition is disjoint from concur. write at lvl2\") {\n    withLog(\n      addA_1_1_nested :: addB_1_2_nested :: Nil,\n      partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 reads a=1\n      val tx1Read = tx1.filterFiles(('a === 1).expr :: Nil)\n      assert(tx1Read.map(_.path).toSet == Set(A_1_1, B_1_2))\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies only a=2/b=1\n      tx2.commit(addC_2_1_nested :: Nil, ManualUpdate)\n\n      // free to commit a=2/b=1\n      tx1.commit(addD_3_1_nested :: Nil, ManualUpdate)\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(A_1_1) :: Row(B_1_2) :: // start\n        Row(C_2_1) ::               // TX2\n        Row(D_3_1) :: Nil)          // TX1\n    }\n  }\n\n  test(\"block commit when read at lvl1 partition reads lvl2 file concur. deleted\") {\n    withLog(\n      addA_1_1_nested :: addB_1_2_nested :: Nil,\n      partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n\n      val tx1 = log.startTransaction()\n      // TX1 reads a=1\n      val tx1Read = tx1.filterFiles(('a === 1).expr :: Nil)\n      assert(tx1Read.map(_.path).toSet == Set(A_1_1, B_1_2))\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies a=1/b=1\n      tx2.commit(addA_1_1_nested.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteReadException] {\n        // TX2 modified a=1, which was read by TX1\n        tx1.commit(addD_3_1_nested :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block commit when full table read conflicts with concur. write in lvl2 nested partition\") {\n    withLog(addA_1_1_nested :: Nil, partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 full table scan\n      tx1.filterFiles()\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies only a=1/b=2\n      tx2.commit(addB_1_2_nested :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // TX2 modified table all of which was read by TX1\n        tx1.commit(addC_2_1_nested :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block commit when part. range read conflicts with concur. write in lvl2 nested partition\") {\n    withLog(\n      addA_1_1_nested :: Nil,\n      partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n\n      val tx1 = log.startTransaction()\n      // TX1 reads multiple nested partitions a >= 1 or b > 1\n      val tx1Read = tx1.filterFiles(('a >= 1 or 'b > 1).expr :: Nil)\n      assert(tx1Read.map(_.path).toSet == Set(A_1_1))\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies a=1/b=2\n      tx2.commit(addB_1_2_nested :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // partition a=1/b=2 conflicts with our read a >= 1 or 'b > 1\n        tx1.commit(addD_3_1_nested :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block commit with concurrent removes on same file\") {\n    withLog(addB_1_2_nested :: Nil, partitionCols = \"a\" :: \"b\" :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // TX1 reads a=2 so that read is disjoint with write partition.\n      tx1.filterFiles(('a === 2).expr :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      // TX2 modifies a=1/b=2\n      tx2.commit(addB_1_2_nested.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteDeleteException] {\n        // TX1 read does not conflict with TX2 as disjoint partitions\n        // But TX2 removed the same file that TX1 is trying to remove\n        tx1.commit(addB_1_2_nested.remove:: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block commit when full table read conflicts with add in any partition\") {\n    withLog(addA_P1 :: addC_P2 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      tx1.filterFiles()\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addC_P2.remove :: addB_P1 :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // TX1 read whole table but TX2 concurrently modified partition P2\n        tx1.commit(addD_P2 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block commit when full table read conflicts with delete in any partition\") {\n    withLog(addA_P1 :: addC_P2 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      tx1.filterFiles()\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addA_P1.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteReadException] {\n        // TX1 read whole table but TX2 concurrently modified partition P1\n        tx1.commit(addB_P1.remove :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent replaceWhere initial empty\") {\n    withLog(addA_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // replaceWhere (part >= 2) -> empty read\n      val tx1Read = tx1.filterFiles(('part >= 2).expr :: Nil)\n      assert(tx1Read.isEmpty)\n\n      val tx2 = log.startTransaction()\n      // replaceWhere (part >= 2) -> empty read\n      val tx2Read = tx2.filterFiles(('part >= 2).expr :: Nil)\n      assert(tx2Read.isEmpty)\n      tx2.commit(addE_P3 :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // Tx2 have modified P2 which conflicts with our read (part >= 2)\n        tx1.commit(addC_P2 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"allow concurrent replaceWhere disjoint partitions initial empty\") {\n    withLog(addA_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // replaceWhere (part > 2 and part <= 3) -> empty read\n      val tx1Read = tx1.filterFiles(('part > 1 and 'part <= 3).expr :: Nil)\n      assert(tx1Read.isEmpty)\n\n      val tx2 = log.startTransaction()\n      // replaceWhere (part > 3) -> empty read\n      val tx2Read = tx2.filterFiles(('part > 3).expr :: Nil)\n      assert(tx2Read.isEmpty)\n\n      tx1.commit(addC_P2 :: Nil, ManualUpdate)\n      // P2 doesn't conflict with read predicate (part > 3)\n      tx2.commit(addG_P4 :: Nil, ManualUpdate)\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(A_P1) :: // start\n        Row(C_P2) :: // TX1\n        Row(G_P4) :: Nil) // TX2\n    }\n  }\n\n  test(\"block concurrent replaceWhere NOT empty but conflicting predicate\") {\n    withLog(addA_P1 :: addG_P4 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // replaceWhere (part <= 3) -> read P1\n      val tx1Read = tx1.filterFiles(('part <= 3).expr :: Nil)\n      assert(tx1Read.map(_.path) == A_P1 :: Nil)\n      val tx2 = log.startTransaction()\n      // replaceWhere (part >= 2) -> read P4\n      val tx2Read = tx2.filterFiles(('part >= 2).expr :: Nil)\n      assert(tx2Read.map(_.path) == G_P4 :: Nil)\n\n      tx1.commit(addA_P1.remove :: addC_P2 :: Nil, ManualUpdate)\n      intercept[ConcurrentAppendException] {\n        // Tx1 have modified P2 which conflicts with our read (part >= 2)\n        tx2.commit(addG_P4.remove :: addE_P3 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent commit on read & add conflicting partitions\") {\n    withLog(addA_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // read P1\n      val tx1Read = tx1.filterFiles(('part === 1).expr :: Nil)\n      assert(tx1Read.map(_.path) == A_P1 :: Nil)\n\n      // tx2 commits before tx1\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addB_P1 :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // P1 read by TX1 was modified by TX2\n        tx1.commit(addE_P3 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent commit on read & delete conflicting partitions\") {\n    withLog(addA_P1 :: addB_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // read P1\n      tx1.filterFiles(('part === 1).expr :: Nil)\n\n      // tx2 commits before tx1\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addA_P1.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteReadException] {\n        // P1 read by TX1 was removed by TX2\n        tx1.commit(addE_P3 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block 2 concurrent replaceWhere transactions\") {\n    withLog(addA_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // read P1\n      tx1.filterFiles(('part === 1).expr :: Nil)\n\n      val tx2 = log.startTransaction()\n      // read P1\n      tx2.filterFiles(('part === 1).expr :: Nil)\n\n      // tx1 commits before tx2\n      tx1.commit(addA_P1.remove :: addB_P1 :: Nil, ManualUpdate)\n\n      intercept[ConcurrentAppendException] {\n        // P1 read & deleted by TX1 is being modified by TX2\n        tx2.commit(addA_P1.remove :: addC_P1 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block 2 concurrent replaceWhere transactions changing partitions\") {\n    withLog(addA_P1 :: addC_P2 :: addE_P3 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      // read P3\n      tx1.filterFiles(('part === 3 or 'part === 1).expr :: Nil)\n\n      val tx2 = log.startTransaction()\n      // read P3\n      tx2.filterFiles(('part === 3 or 'part === 2).expr :: Nil)\n\n      // tx1 commits before tx2\n      tx1.commit(addA_P1.remove :: addE_P3.remove :: addB_P1 :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteReadException] {\n        // P3 read & deleted by TX1 is being modified by TX2\n        tx2.commit(addC_P2.remove :: addE_P3.remove :: addD_P2 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent full table scan after concurrent write completes\") {\n    withLog(addA_P1 :: addE_P3 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addC_P2 :: Nil, ManualUpdate)\n\n      tx1.filterFiles(('part === 1).expr :: Nil)\n      // full table scan\n      tx1.filterFiles()\n\n      intercept[ConcurrentAppendException] {\n        tx1.commit(addA_P1.remove :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent commit mixed metadata and data predicate\") {\n    withLog(addA_P1 :: addE_P3 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addC_P2 :: Nil, ManualUpdate)\n\n      // actually a full table scan\n      tx1.filterFiles(('part === 1 or 'year > 2019).expr :: Nil)\n\n      intercept[ConcurrentAppendException] {\n        tx1.commit(addA_P1.remove :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  test(\"block concurrent read (2 scans) and add when read partition was changed by concur. write\") {\n    withLog(addA_P1 :: addE_P3 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      tx1.filterFiles(('part === 1).expr :: Nil)\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addC_P2 :: Nil, ManualUpdate)\n\n      tx1.filterFiles(('part > 1 and 'part < 3).expr :: Nil)\n\n      intercept[ConcurrentAppendException] {\n        // P2 added by TX2 conflicts with our read condition 'part > 1 and 'part < 3\n        tx1.commit(addA_P1.remove :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  def setDataChangeFalse(fileActions: Seq[FileAction]): Seq[FileAction] = {\n    fileActions.map {\n      case a: AddFile => a.copy(dataChange = false)\n      case r: RemoveFile => r.copy(dataChange = false)\n      case cdc: AddCDCFile => cdc // change files are always dataChange = false\n    }\n  }\n\n  test(\"no data change: allow data rearrange when new files concurrently added\") {\n    withLog(addA_P1 :: addB_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      tx1.filterFiles()\n\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(\n        addE_P3 :: Nil,\n        ManualUpdate)\n\n      // tx1 rearranges files\n      tx1.commit(\n        setDataChangeFalse(addA_P1.remove :: addB_P1.remove :: addC_P1 :: Nil),\n        ManualUpdate)\n\n      checkAnswer(\n        log.update().allFiles.select(\"path\"),\n        Row(C_P1) :: Row(E_P3) ::  Nil)\n    }\n  }\n\n  test(\"no data change: block data rearrange when concurrently delete removes same file\") {\n    withLog(addA_P1 :: addB_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      tx1.filterFiles()\n\n      // tx2 removes file\n      val tx2 = log.startTransaction()\n      tx2.filterFiles()\n      tx2.commit(addA_P1.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteReadException] {\n        // tx1 reads to rearrange the same file that tx2 deleted\n        tx1.commit(\n          setDataChangeFalse(addA_P1.remove :: addB_P1.remove :: addC_P1 :: Nil),\n          ManualUpdate)\n      }\n    }\n  }\n\n  test(\"readWholeTable should block concurrent delete\") {\n    withLog(addA_P1 :: Nil) { log =>\n      val tx1 = log.startTransaction()\n      tx1.readWholeTable()\n\n      // tx2 removes file\n      val tx2 = log.startTransaction()\n      tx2.commit(addA_P1.remove :: Nil, ManualUpdate)\n\n      intercept[ConcurrentDeleteReadException] {\n        // tx1 reads the whole table but tx2 removes files before tx1 commits\n        tx1.commit(addB_P1 :: Nil, ManualUpdate)\n      }\n    }\n  }\n\n  def withLog(\n      actions: Seq[Action],\n      partitionCols: Seq[String] = \"part\" :: Nil)(\n      test: DeltaLog => Unit): Unit = {\n\n    val schema = StructType(partitionCols.map(p => StructField(p, StringType)).toArray)\n    val metadata = Metadata(partitionColumns = partitionCols, schemaString = schema.json)\n    val actionWithMetaData = actions :+ metadata\n\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log and add data. ManualUpdate is just a no-op placeholder.\n      log.startTransaction().commit(Seq(metadata), ManualUpdate)\n      log.startTransaction().commitManually(actionWithMetaData: _*)\n      test(log)\n    }\n  }\n\n  test(\"allow concurrent set-txns with different app ids\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log.\n      log.startTransaction().commitManually()\n\n      val txn = log.startTransaction()\n      txn.txnVersion(\"t1\")\n      val winningTxn = log.startTransaction()\n      winningTxn.commit(SetTransaction(\"t2\", 1, Some(1234L)) :: Nil, ManualUpdate)\n      txn.commit(Nil, ManualUpdate)\n\n      assert(log.update().transactions === Map(\"t2\" -> 1))\n    }\n  }\n\n  test(\"block concurrent set-txns with the same app id\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir)\n      // Initialize the log.\n      log.startTransaction().commitManually()\n\n      val txn = log.startTransaction()\n      txn.txnVersion(\"t1\")\n      val winningTxn = log.startTransaction()\n      winningTxn.commit(SetTransaction(\"t1\", 1, Some(1234L)) :: Nil, ManualUpdate)\n\n      intercept[ConcurrentTransactionException] {\n        txn.commit(Nil, ManualUpdate)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/OptimisticTransactionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.nio.file.FileAlreadyExistsException\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.DeltaOperations.{ManualUpdate, Truncate}\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, CommitInfo, Metadata, Protocol, RemoveFile, SetTransaction}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CommitCoordinatorBuilder, CommitCoordinatorProvider, InMemoryCommitCoordinator, InMemoryCommitCoordinatorBuilder, TableCommitCoordinatorClient}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{CommitCoordinatorClient, CommitFailedException, CommitResponse, TableDescriptor, UpdatedActions}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{Row, SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.dsl.expressions._\nimport org.apache.spark.sql.catalyst.expressions.{EqualTo, Literal}\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.types.{IntegerType, StructType}\nimport org.apache.spark.util.ManualClock\n\n\nclass OptimisticTransactionSuite\n  extends OptimisticTransactionLegacyTests\n  with OptimisticTransactionSuiteBase {\n\n  import testImplicits._\n\n  // scalastyle:off: removeFile\n  private val addA = createTestAddFile(encodedPath = \"a\")\n  private val addB = createTestAddFile(encodedPath = \"b\")\n\n  /* ************************** *\n   * Allowed concurrent actions *\n   * ************************** */\n\n  check(\n    \"append / append\",\n    conflicts = false,\n    reads = Seq(\n      t => t.metadata\n    ),\n    concurrentWrites = Seq(\n      addA),\n    actions = Seq(\n      addB))\n\n  check(\n    \"disjoint txns\",\n    conflicts = false,\n    reads = Seq(\n      t => t.txnVersion(\"t1\")\n    ),\n    concurrentWrites = Seq(\n      SetTransaction(\"t2\", 0, Some(1234L))),\n    actions = Nil)\n\n  check(\n    \"disjoint delete / read\",\n    conflicts = false,\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\")),\n      AddFile(\"a\", Map(\"x\" -> \"2\"), 1, 1, dataChange = true)\n    ),\n    reads = Seq(\n      t => t.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n    ),\n    concurrentWrites = Seq(\n      RemoveFile(\"a\", Some(4))),\n    actions = Seq())\n\n  check(\n    \"disjoint add / read\",\n    conflicts = false,\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\"))\n    ),\n    reads = Seq(\n      t => t.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n    ),\n    concurrentWrites = Seq(\n      AddFile(\"a\", Map(\"x\" -> \"2\"), 1, 1, dataChange = true)),\n    actions = Seq())\n\n  /* ***************************** *\n   * Disallowed concurrent actions *\n   * ***************************** */\n\n  check(\n    \"delete / delete\",\n    conflicts = true,\n    reads = Nil,\n    concurrentWrites = Seq(\n      RemoveFile(\"a\", Some(4))),\n    actions = Seq(\n      RemoveFile(\"a\", Some(5))))\n\n  check(\n    \"add / read + write\",\n    conflicts = true,\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\"))\n    ),\n    reads = Seq(\n      t => t.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n    ),\n    concurrentWrites = Seq(\n      AddFile(\"a\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)),\n    actions = Seq(AddFile(\"b\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)),\n    // commit info should show operation as truncate, because that's the operation used by the\n    // harness\n    expectedErrorClass = Some(\"DELTA_CONCURRENT_APPEND.WITH_PARTITION_HINT\"),\n    expectedErrorMessageParameters = Some(Map(\n      \"operation\" -> \"TRUNCATE\",\n      \"version\" -> \"1\",\n      \"partitionValues\" -> \"\\\\[x=1\\\\]\",\n      \"docLink\" -> \".*\"\n    )))\n\n  check(\n    \"add / read + no write\",  // no write = no real conflicting change even though data was added\n    conflicts = false,        // so this should not conflict\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\"))\n    ),\n    reads = Seq(\n      t => t.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n    ),\n    concurrentWrites = Seq(\n      AddFile(\"a\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)),\n    actions = Seq())\n\n  check(\n    \"add in part=2 / read from part=1,2 and write to part=1\",\n    conflicts = true,\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\"))\n    ),\n    reads = Seq(\n      t => {\n        // Filter files twice - once for x=1 and again for x=2\n        t.filterFiles(Seq(EqualTo('x, Literal(1))))\n        t.filterFiles(Seq(EqualTo('x, Literal(2))))\n      }\n    ),\n    concurrentWrites = Seq(\n      AddFile(\n        path = \"a\",\n        partitionValues = Map(\"x\" -> \"1\"),\n        size = 1,\n        modificationTime = 1,\n        dataChange = true)\n    ),\n    actions = Seq(\n      AddFile(\n        path = \"b\",\n        partitionValues = Map(\"x\" -> \"2\"),\n        size = 1,\n        modificationTime = 1,\n        dataChange = true)\n    ))\n\n  check(\n    \"delete / read\",\n    conflicts = true,\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\")),\n      AddFile(\"a\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)\n    ),\n    reads = Seq(\n      t => t.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n    ),\n    concurrentWrites = Seq(\n      RemoveFile(\"a\", Some(4))),\n    actions = Seq(),\n    expectedErrorClass = Some(\"DELTA_CONCURRENT_DELETE_READ.WITH_PARTITION_HINT\"),\n    expectedErrorMessageParameters = Some(Map(\n      \"operation\" -> \"TRUNCATE\",\n      \"version\" -> \"2\",\n      \"partitionValues\" -> \"\\\\[x=1\\\\]\",\n      \"docLink\" -> \".*\"\n    )))\n\n  check(\n    \"schema change\",\n    conflicts = true,\n    reads = Seq(\n      t => t.metadata\n    ),\n    concurrentWrites = Seq(\n      Metadata()),\n    actions = Nil)\n\n  check(\n    \"conflicting txns\",\n    conflicts = true,\n    reads = Seq(\n      t => t.txnVersion(\"t1\")\n    ),\n    concurrentWrites = Seq(\n      SetTransaction(\"t1\", 0, Some(1234L))),\n    actions = Nil)\n\n  check(\n    \"upgrade / upgrade\",\n    conflicts = true,\n    reads = Seq(\n      t => t.metadata\n    ),\n    concurrentWrites = Seq(\n      Action.supportedProtocolVersion(featuresToExclude = Seq(CatalogOwnedTableFeature))),\n    actions = Seq(\n      Action.supportedProtocolVersion(featuresToExclude = Seq(CatalogOwnedTableFeature))))\n\n  check(\n    \"taint whole table\",\n    conflicts = true,\n    setup = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\")),\n      AddFile(\"a\", Map(\"x\" -> \"2\"), 1, 1, dataChange = true)\n    ),\n    reads = Seq(\n      t => t.filterFiles(EqualTo('x, Literal(1)) :: Nil),\n      // `readWholeTable` should disallow any concurrent change, even if the change\n      // is disjoint with the earlier filter\n      t => t.readWholeTable()\n    ),\n    concurrentWrites = Seq(\n      AddFile(\"b\", Map(\"x\" -> \"3\"), 1, 1, dataChange = true)),\n    actions = Seq(\n      AddFile(\"c\", Map(\"x\" -> \"4\"), 1, 1, dataChange = true)))\n\n  check(\n    \"taint whole table + concurrent remove\",\n    conflicts = true,\n    setup = Seq(\n      Metadata(schemaString = new StructType().add(\"x\", IntegerType).json),\n      AddFile(\"a\", Map.empty, 1, 1, dataChange = true)\n    ),\n    reads = Seq(\n      // `readWholeTable` should disallow any concurrent `RemoveFile`s.\n      t => t.readWholeTable()\n    ),\n    concurrentWrites = Seq(\n      RemoveFile(\"a\", Some(4L))),\n    actions = Seq(\n      AddFile(\"b\", Map.empty, 1, 1, dataChange = true)))\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n  }\n\n  test(\"initial commit without metadata should fail\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      val txn = log.startTransaction()\n      withSQLConf(DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED.key -> \"true\") {\n        val e = intercept[DeltaIllegalStateException] {\n          txn.commit(Nil, ManualUpdate)\n        }\n        assert(e.getMessage == DeltaErrors.metadataAbsentException().getMessage)\n      }\n    }\n  }\n\n  test(\"initial commit with multiple metadata actions should fail\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getAbsolutePath))\n      val txn = log.startTransaction()\n      val e = intercept[AssertionError] {\n        txn.commit(Seq(Metadata(), Metadata()), ManualUpdate)\n      }\n      assert(e.getMessage.contains(\"Cannot change the metadata more than once in a transaction.\"))\n    }\n  }\n\n  test(\"enabling Coordinated Commits on an existing table should create commit dir\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getAbsolutePath))\n      val metadata = Metadata()\n      log.startTransaction().commit(Seq(metadata), ManualUpdate)\n      val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n      val commitDir = FileNames.commitDirPath(log.logPath)\n      // Delete commit directory.\n      fs.delete(commitDir)\n      assert(!fs.exists(commitDir))\n      // With no Coordinated Commits conf, commit directory should not be created.\n      log.startTransaction().commit(Seq(metadata), ManualUpdate)\n      assert(!fs.exists(commitDir))\n      // Enabling Coordinated Commits on an existing table should create the commit dir.\n      CommitCoordinatorProvider.registerBuilder(InMemoryCommitCoordinatorBuilder(3))\n      val newMetadata = metadata.copy(configuration =\n        (metadata.configuration ++\n          Map(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> \"in-memory\")).toMap)\n      log.startTransaction().commit(Seq(newMetadata), ManualUpdate)\n      assert(fs.exists(commitDir))\n      log.update().ensureCommitFilesBackfilled()\n      // With no new Coordinated Commits conf, commit directory should not be created and so the\n      // transaction should fail because of corrupted dir.\n      fs.delete(commitDir)\n      assert(!fs.exists(commitDir))\n      intercept[java.io.FileNotFoundException] {\n        log.startTransaction().commit(Seq(newMetadata), ManualUpdate)\n      }\n    }\n  }\n\n  test(\"concurrent feature enablement with failConcurrentTransactionsAtUpgrade should conflict\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_CONFLICT_CHECKER_ENFORCE_FEATURE_ENABLEMENT_VALIDATION.key -> \"true\") {\n      withTempDir { tempDir =>\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n        val metadata = Metadata(\n          schemaString = new StructType().add(\"x\", IntegerType).json,\n          partitionColumns = Seq(\"x\"))\n        val protocol = Protocol(3, 7)\n        log.startTransaction().commit(Seq(metadata, protocol), ManualUpdate)\n\n        // Start a transaction that will write to the table concurrently with feature enablement.\n        val txn = log.startTransaction()\n\n        // Concurrently, enable the MaterializePartitionColumns feature\n        // This feature has failConcurrentTransactionsAtUpgrade = true\n        val newProtocol = txn.snapshot.protocol.withFeatures(\n          Set(MaterializePartitionColumnsTableFeature))\n\n        log.startTransaction().commit(Seq(newProtocol), ManualUpdate)\n\n        // The original transaction should fail when trying to commit\n        // because a feature with failConcurrentTransactionsAtUpgrade=true was added\n        val e = intercept[io.delta.exceptions.ProtocolChangedException] {\n          txn.commit(\n            Seq(AddFile(\"test\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)),\n            ManualUpdate)\n        }\n\n        // Verify the error message\n        assert(e.getMessage.contains(\"The protocol version of the Delta table has been changed\"))\n      }\n    }\n  }\n\n  test(\"AddFile with different partition schema compared to metadata should fail\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getAbsolutePath))\n      val initTxn = log.startTransaction()\n      initTxn.updateMetadataForNewTable(Metadata(\n        schemaString = StructType.fromDDL(\"col2 string, a int\").json,\n        partitionColumns = Seq(\"col2\")))\n      initTxn.commit(Seq(), ManualUpdate)\n      withSQLConf(DeltaSQLConf.DELTA_COMMIT_VALIDATION_ENABLED.key -> \"true\") {\n        val e = intercept[IllegalStateException] {\n          log.startTransaction().commit(Seq(AddFile(\n            log.dataPath.toString, Map(\"col3\" -> \"1\"), 12322, 0L, true, null, null)), ManualUpdate)\n        }\n        assert(e.getMessage == DeltaErrors.addFilePartitioningMismatchException(\n          Seq(\"col3\"), Seq(\"col2\")).getMessage)\n      }\n    }\n  }\n\n  test(\"isolation level shouldn't be null\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n      log.startTransaction().commit(Seq(Metadata()), ManualUpdate)\n\n      val txn = log.startTransaction()\n      txn.commit(addA :: Nil, ManualUpdate)\n\n      val isolationLevels = log.history.getHistory(Some(10)).map(_.isolationLevel)\n      assert(isolationLevels.size == 2)\n      assert(isolationLevels(0).exists(_.contains(\"Serializable\")))\n      assert(isolationLevels(0).exists(_.contains(\"Serializable\")))\n    }\n  }\n\n  test(\"every transaction should use a unique identifier in the commit\") {\n    withTempDir { tempDir =>\n      // Initialize delta table.\n      val clock = new ManualClock()\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath), clock)\n      log.startTransaction().commit(Seq(Metadata()), ManualUpdate)\n      clock.advance(100)\n\n      // Start two transactions which commits at same time with same content.\n      val txn1 = log.startTransaction()\n      val txn2 = log.startTransaction()\n      clock.advance(100)\n      val version1 = txn1.commit(Seq(), ManualUpdate)\n      val version2 = txn2.commit(Seq(), ManualUpdate)\n\n      // Validate that actions in both transactions are not exactly same.\n      def readActions(version: Long): Seq[Action] = {\n        log.store.read(FileNames.unsafeDeltaFile(log.logPath, version), log.newDeltaHadoopConf())\n          .map(Action.fromJson)\n      }\n      def removeTxnIdAndMetricsFromActions(actions: Seq[Action]): Seq[Action] = actions.map {\n        case c: CommitInfo => c.copy(txnId = None, operationMetrics = None)\n        case other => other\n      }\n      val actions1 = readActions(version1)\n      val actions2 = readActions(version2)\n      val actionsWithoutTxnId1 = removeTxnIdAndMetricsFromActions(actions1)\n      val actionsWithoutTxnId2 = removeTxnIdAndMetricsFromActions(actions2)\n      assert(actions1 !== actions2)\n      // Without the txn id, the actions are same as of today but they need not be in future. In\n      // future we might have other fields which may make these actions from two different\n      // transactions different. In that case, the below assertion can be removed.\n      assert(actionsWithoutTxnId1 === actionsWithoutTxnId2)\n    }\n  }\n\n  test(\"pre-command actions committed\") {\n    withTempDir { tempDir =>\n      // Initialize delta table.\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      log.startTransaction().commit(Seq(Metadata()), ManualUpdate)\n\n      val txn = log.startTransaction()\n      txn.updateSetTransaction(\"TestAppId\", 1L, None)\n      val version = txn.commit(Seq(), ManualUpdate)\n\n      def readActions(version: Long): Seq[Action] = {\n        log.store.read(FileNames.unsafeDeltaFile(log.logPath, version), log.newDeltaHadoopConf())\n          .map(Action.fromJson)\n      }\n      val actions = readActions(version)\n      assert(actions.collectFirst {\n        case SetTransaction(\"TestAppId\", 1L, _) =>\n      }.isDefined)\n    }\n  }\n\n  test(\"has SetTransaction version conflicts\") {\n    withTempDir { tempDir =>\n      // Initialize delta table.\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      log.startTransaction().commit(Seq(Metadata()), ManualUpdate)\n\n      val txn = log.startTransaction()\n      txn.updateSetTransaction(\"TestAppId\", 1L, None)\n      val e = intercept[IllegalArgumentException] {\n        txn.commit(Seq(SetTransaction(\"TestAppId\", 2L, None)), ManualUpdate)\n      }\n      assert(e.getMessage == DeltaErrors.setTransactionVersionConflict(\"TestAppId\", 2L, 1L)\n        .getMessage)\n    }\n  }\n\n  test(\"conflict event logs winningTxnOperation for observability\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      // Initialize delta table with partitioned schema.\n      val metadata = Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\"),\n        // Set isolation level to SERIALIZABLE to ensure conflict detection.\n        configuration = Map(DeltaConfigs.ISOLATION_LEVEL.key -> Serializable.toString))\n      log.startTransaction().commit(Seq(metadata), ManualUpdate)\n\n      // Start a transaction that reads partition x=1.\n      val txn = log.startTransaction()\n      txn.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n\n      // Commit a concurrent write to the same partition that will cause conflict.\n      log.startTransaction().commit(\n        Seq(AddFile(\"a\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)),\n        Truncate())\n\n      // Attempt to write to the same partition - should conflict.\n      val conflictLogs = Log4jUsageLogger.track {\n        intercept[DeltaConcurrentModificationException] {\n          txn.commit(\n            Seq(AddFile(\"b\", Map(\"x\" -> \"1\"), 1, 1, dataChange = true)),\n            Truncate())\n        }\n      }.filter(usageLog =>\n        usageLog.metric == \"tahoeEvent\" &&\n          usageLog.tags.getOrElse(\"opType\", \"\").startsWith(\"delta.commit.conflict\"))\n\n      // Verify the conflict event is logged with winningTxnOperation.\n      assert(conflictLogs.size == 1, \"Expected exactly one conflict event to be logged\")\n      val conflictBlob = JsonUtils.fromJson[Map[String, Any]](conflictLogs.head.blob)\n      assert(conflictBlob.get(\"winningTxnOperation\")\n        .exists(_.toString == \"TRUNCATE\"))\n    }\n  }\n\n  test(\"removes duplicate SetTransactions\") {\n    withTempDir { tempDir =>\n      // Initialize delta table.\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      log.startTransaction().commit(Seq(Metadata()), ManualUpdate)\n\n      val txn = log.startTransaction()\n      txn.updateSetTransaction(\"TestAppId\", 1L, None)\n      val version = txn.commit(Seq(SetTransaction(\"TestAppId\", 1L, None)), ManualUpdate)\n      def readActions(version: Long): Seq[Action] = {\n        log.store.read(FileNames.unsafeDeltaFile(log.logPath, version), log.newDeltaHadoopConf())\n          .map(Action.fromJson)\n      }\n      assert(readActions(version).collectFirst {\n        case SetTransaction(\"TestAppId\", 1L, _) =>\n      }.isDefined)\n    }\n  }\n\n  test(\"preCommitLogSegment is updated during conflict checking\") {\n    withTempDir { tempDir =>\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      log.startTransaction().commit(Seq(Metadata()), ManualUpdate)\n      sql(s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` \" +\n        s\"SET TBLPROPERTIES (${DeltaConfigs.CHECKPOINT_INTERVAL.key} = 10)\")\n      val testTxn = log.startTransaction()\n      val testTxnStartTs = System.currentTimeMillis()\n      for (_ <- 1 to 11) {\n        log.startTransaction().commit(Seq.empty, ManualUpdate)\n      }\n      val testTxnEndTs = System.currentTimeMillis()\n\n      // preCommitLogSegment should not get updated until a commit is triggered\n      assert(testTxn.preCommitLogSegment.version == 1)\n      assert(testTxn.preCommitLogSegment.lastCommitFileModificationTimestamp < testTxnStartTs)\n      assert(testTxn.preCommitLogSegment.deltas.size == 2)\n      assert(testTxn.preCommitLogSegment.checkpointProvider.isEmpty)\n\n      testTxn.commit(Seq.empty, ManualUpdate)\n\n      // preCommitLogSegment should get updated to the version right before the txn commits\n      assert(testTxn.preCommitLogSegment.version == 12)\n      assert(testTxn.preCommitLogSegment.lastCommitFileModificationTimestamp < testTxnEndTs)\n      assert(testTxn.preCommitLogSegment.deltas.size == 2)\n      assert(testTxn.preCommitLogSegment.checkpointProvider.version == 10)\n    }\n  }\n\n  test(\"Limited retries for non-conflict retryable CommitFailedExceptions\") {\n    val commitCoordinatorName = \"retryable-non-conflict-commit-coordinator\"\n    var commitAttempts = 0\n    val numRetries = \"100\"\n    val numNonConflictRetries = \"10\"\n    val initialNonConflictErrors = 5\n    val initialConflictErrors = 5\n\n    object RetryableNonConflictCommitCoordinatorBuilder$ extends CommitCoordinatorBuilder {\n\n      override def getName: String = commitCoordinatorName\n\n      val commitCoordinatorClient: InMemoryCommitCoordinator = {\n        new InMemoryCommitCoordinator(batchSize = 1000L) {\n          override def commit(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              commitVersion: Long,\n              actions: java.util.Iterator[String],\n              updatedActions: UpdatedActions): CommitResponse = {\n            // Fail all commits except first one\n            if (commitVersion == 0) {\n              return super.commit(\n                logStore,\n                hadoopConf,\n                tableDesc,\n                commitVersion,\n                actions,\n                updatedActions)\n            }\n            commitAttempts += 1\n            throw new CommitFailedException(\n              true,\n              commitAttempts > initialNonConflictErrors &&\n                commitAttempts <= (initialNonConflictErrors + initialConflictErrors),\n              \"\")\n          }\n        }\n      }\n      override def build(\n          spark: SparkSession,\n          conf: Map[String, String]): CommitCoordinatorClient = commitCoordinatorClient\n    }\n\n    CommitCoordinatorProvider.registerBuilder(RetryableNonConflictCommitCoordinatorBuilder$)\n\n    withSQLConf(\n        DeltaSQLConf.DELTA_MAX_RETRY_COMMIT_ATTEMPTS.key -> numRetries,\n        DeltaSQLConf.DELTA_MAX_NON_CONFLICT_RETRY_COMMIT_ATTEMPTS.key -> numNonConflictRetries) {\n      withTempDir { tempDir =>\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        val conf =\n          Map(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> commitCoordinatorName)\n        log.startTransaction().commit(Seq(Metadata(configuration = conf)), ManualUpdate)\n        val testTxn = log.startTransaction()\n        intercept[CommitFailedException] { testTxn.commit(Seq.empty, ManualUpdate) }\n        // num-attempts = 1 + num-retries\n        assert(commitAttempts ==\n          (initialNonConflictErrors + initialConflictErrors + numNonConflictRetries.toInt + 1))\n      }\n    }\n  }\n\n  test(\"No retries for FileAlreadyExistsException with commit-coordinator\") {\n    val commitCoordinatorName = \"file-already-exists-commit-coordinator\"\n    var commitAttempts = 0\n\n    object FileAlreadyExistsCommitCoordinatorBuilder extends CommitCoordinatorBuilder {\n\n      override def getName: String = commitCoordinatorName\n\n      lazy val commitCoordinatorClient: CommitCoordinatorClient = {\n        new InMemoryCommitCoordinator(batchSize = 1000L) {\n          override def commit(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              commitVersion: Long,\n              actions: java.util.Iterator[String],\n              updatedActions: UpdatedActions): CommitResponse = {\n            // Fail all commits except first one\n            if (commitVersion == 0) {\n              return super.commit(\n                logStore,\n                hadoopConf,\n                tableDesc,\n                commitVersion,\n                actions,\n                updatedActions)\n            }\n            commitAttempts += 1\n            throw new FileAlreadyExistsException(\"Commit-File Already Exists\")\n          }\n        }\n      }\n      override def build(\n          spark: SparkSession,\n          conf: Map[String, String]): CommitCoordinatorClient = commitCoordinatorClient\n    }\n\n    CommitCoordinatorProvider.registerBuilder(FileAlreadyExistsCommitCoordinatorBuilder)\n\n    withSQLConf(\n        DeltaSQLConf.DELTA_MAX_RETRY_COMMIT_ATTEMPTS.key -> \"100\",\n        DeltaSQLConf.DELTA_MAX_NON_CONFLICT_RETRY_COMMIT_ATTEMPTS.key -> \"10\") {\n      withTempDir { tempDir =>\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        val conf =\n          Map(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> commitCoordinatorName)\n        log.startTransaction().commit(Seq(Metadata(configuration = conf)), ManualUpdate)\n        val testTxn = log.startTransaction()\n        intercept[FileAlreadyExistsException] { testTxn.commit(Seq.empty, ManualUpdate) }\n        // Test that there are no retries for the FileAlreadyExistsException in\n        // CommitCoordinatorClient.commit()\n        // num-attempts(1) = 1 + num-retries(0)\n        assert(commitAttempts == 1)\n      }\n    }\n  }\n\n  /**\n   * Here we test whether ConflictChecker correctly resolves conflicts when using\n   * OptimisticTransaction.filterFiles(partitions) to perform dynamic partition overwrites.\n   *\n   */\n  private def testDynamicPartitionOverwrite(\n    caseName: String,\n    concurrentActions: String => Seq[Action],\n    expectedErrorClass: Option[String] = None,\n    expectedErrorMessageParameters: String => Map[String, String] = _ => Map.empty) = {\n\n    // We test with a partition column named \"partitionValues\" to make sure we correctly skip\n    // rewriting the filters\n    for (partCol <- Seq(\"part\", \"partitionValues\")) {\n      test(\"filterFiles(partitions) correctly updates readPredicates and ConflictChecker \" +\n        s\"correctly detects conflicts for $caseName with partition column [$partCol]\") {\n        withTempDir { tempDir =>\n\n            val tablePath = tempDir.getCanonicalPath\n            val log = DeltaLog.forTable(spark, tablePath)\n            // set up\n            log.startTransaction.commit(Seq(\n              Metadata(\n                schemaString = new StructType()\n                  .add(partCol, IntegerType)\n                  .add(\"value\", IntegerType).json,\n                partitionColumns = Seq(partCol))\n            ), ManualUpdate)\n            log.startTransaction.commit(\n              Seq(AddFile(\"a\", Map(partCol -> \"0\"), 1, 1, dataChange = true),\n                AddFile(\"b\", Map(partCol -> \"1\"), 1, 1, dataChange = true)),\n              ManualUpdate)\n\n\n            // new data we want to overwrite dynamically to the table\n            val newData = Seq(AddFile(\"x\", Map(partCol -> \"0\"), 1, 1, dataChange = true))\n\n            // txn1: read files in partitions of our new data (part=0)\n            val txn = log.startTransaction()\n            val addFiles =\n                txn.filterFiles(newData.map(_.partitionValues).toSet)\n\n            // txn2\n            log.startTransaction().commit(concurrentActions(partCol), ManualUpdate)\n\n            // txn1: remove files read in the partition and commit newData\n            def commitTxn1 = {\n                txn.commit(addFiles.map(_.remove) ++ newData, ManualUpdate)\n            }\n\n            if (expectedErrorClass.isDefined) {\n              val e = intercept[DeltaConcurrentModificationException] {\n                commitTxn1\n              }\n              checkError(\n                e.asInstanceOf[DeltaThrowable],\n                expectedErrorClass.get,\n                Some(\"2D521\"),\n                expectedErrorMessageParameters(partCol)\n                  ++ Map(\"tableName\" -> s\"delta.`${log.dataPath}`\"),\n                matchPVals = true\n              )\n            } else {\n              commitTxn1\n            }\n        }\n      }\n    }\n  }\n\n  testDynamicPartitionOverwrite(\n    caseName = \"concurrent append in same partition\",\n    concurrentActions = partCol => Seq(AddFile(\"y\", Map(partCol -> \"0\"), 1, 1, dataChange = true)),\n    expectedErrorClass = Some(\"DELTA_CONCURRENT_APPEND.WITH_PARTITION_HINT\"),\n    expectedErrorMessageParameters = partCol => Map(\n      \"operation\" -> \"Manual Update\",\n      \"partitionValues\" -> s\"\\\\[$partCol=0\\\\]\",\n      \"version\" -> \"2\",\n      \"docLink\" -> \".*\"\n    )\n  )\n\n  testDynamicPartitionOverwrite(\n    caseName = \"concurrent append in different partition\",\n    concurrentActions = partCol => Seq(AddFile(\"y\", Map(partCol -> \"1\"), 1, 1, dataChange = true))\n  )\n\n  testDynamicPartitionOverwrite(\n    caseName = \"concurrent delete in same partition\",\n    concurrentActions = partCol => Seq(\n      RemoveFile(\"a\", None, partitionValues = Map(partCol -> \"0\"))),\n    expectedErrorClass = Some(\"DELTA_CONCURRENT_DELETE_DELETE.WITH_PARTITION_HINT\"),\n    expectedErrorMessageParameters = partCol => Map(\n      \"operation\" -> \"Manual Update\",\n      \"partitionValues\" -> s\"\\\\[$partCol=0\\\\]\",\n      \"version\" -> \"2\",\n      \"docLink\" -> \".*\"\n    )\n  )\n\n  testDynamicPartitionOverwrite(\n    caseName = \"concurrent delete in different partition\",\n    concurrentActions = partCol => Seq(\n      RemoveFile(\"b\", None, partitionValues = Map(partCol -> \"1\")))\n  )\n\n  test(\"can set partition columns in first commit\") {\n    withTempDir { tableDir =>\n      val partitionColumns = Array(\"part\")\n      val exampleAddFile = AddFile(\n        path = \"test-path\",\n        partitionValues = Map(\"part\" -> \"one\"),\n        size = 1234,\n        modificationTime = 5678,\n        dataChange = true,\n        stats = \"\"\"{\"numRecords\": 1}\"\"\",\n        tags = Map.empty)\n      val deltaLog = DeltaLog.forTable(spark, tableDir)\n      val schema = new StructType()\n        .add(\"id\", \"long\")\n        .add(\"part\", \"string\")\n      deltaLog.withNewTransaction { txn =>\n        val protocol = Action.supportedProtocolVersion(\n          featuresToExclude = Seq(CatalogOwnedTableFeature))\n        val metadata = Metadata(\n          schemaString = schema.json,\n          partitionColumns = partitionColumns)\n        txn.commit(Seq(protocol, metadata, exampleAddFile), DeltaOperations.ManualUpdate)\n      }\n      val snapshot = deltaLog.update()\n      assert(snapshot.metadata.partitionColumns.sameElements(partitionColumns))\n    }\n  }\n\n  test(\"only single Protocol action per commit - implicit\") {\n    withTempDir { tableDir =>\n      val deltaLog = DeltaLog.forTable(spark, tableDir)\n      val schema = new StructType()\n        .add(\"id\", \"long\")\n        .add(\"col\", \"string\")\n      val e = intercept[java.lang.AssertionError] {\n        deltaLog.withNewTransaction { txn =>\n          val protocol = Protocol(2, 3)\n          val metadata = Metadata(\n            schemaString = schema.json,\n            configuration = Map(\"delta.enableChangeDataFeed\" -> \"true\"))\n          txn.commit(Seq(protocol, metadata), DeltaOperations.ManualUpdate)\n        }\n      }\n      assert(e.getMessage.contains(\n        \"assertion failed: Cannot change the protocol more than once in a transaction.\"))\n    }\n  }\n\n  test(\"only single Protocol action per commit - explicit\") {\n    withTempDir { tableDir =>\n      val deltaLog = DeltaLog.forTable(spark, tableDir)\n      val e = intercept[java.lang.AssertionError] {\n        deltaLog.withNewTransaction { txn =>\n          val protocol1 = Protocol(2, 3)\n          val protocol2 = Protocol(1, 4)\n          txn.commit(Seq(protocol1, protocol2), DeltaOperations.ManualUpdate)\n        }\n      }\n      assert(e.getMessage.contains(\n        \"assertion failed: Cannot change the protocol more than once in a transaction.\"))\n    }\n  }\n\n  test(\"DVs cannot be added to files without numRecords stat\") {\n    withTempPath { tempPath =>\n      val path = tempPath.getPath\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val firstFile = writeDuplicateActionsData(path).head\n      enableDeletionVectorsInTable(deltaLog)\n      val (addFileWithDV, removeFile) = addDVToFileInTable(path, firstFile)\n      val addFileWithDVWithoutStats = addFileWithDV.copy(stats = null)\n      testRuntimeErrorOnCommit(Seq(addFileWithDVWithoutStats, removeFile), deltaLog) { e =>\n        val expErrorClass = \"DELTA_DELETION_VECTOR_MISSING_NUM_RECORDS\"\n        assert(e.getErrorClass == expErrorClass)\n        assert(e.getSqlState == \"2D521\")\n      }\n    }\n  }\n\n  test(\"commitInfo tags\") {\n    withTempDir { tableDir =>\n      val deltaLog = DeltaLog.forTable(spark, tableDir)\n      val schema = new StructType().add(\"id\", \"long\")\n\n      def checkLastCommitTags(expectedTags: Option[Map[String, String]]): Unit = {\n        val ci = deltaLog.getChanges(deltaLog.update().version).map(_._2).flatten.collectFirst {\n          case ci: CommitInfo => ci\n        }.head\n        assert(ci.tags === expectedTags)\n      }\n\n      val metadata = Metadata(schemaString = schema.json)\n      // Check empty tags\n      deltaLog.withNewTransaction { txn =>\n        txn.commit(metadata :: Nil, DeltaOperations.ManualUpdate, tags = Map.empty)\n      }\n      checkLastCommitTags(expectedTags = None)\n\n      deltaLog.withNewTransaction { txn =>\n        txn.commit(addA :: Nil, DeltaOperations.Write(SaveMode.Append), tags = Map.empty)\n      }\n      checkLastCommitTags(expectedTags = None)\n\n      // Check non-empty tags\n      val tags1 = Map(\"testTag1\" -> \"testValue1\")\n      deltaLog.withNewTransaction { txn =>\n        txn.commit(metadata :: Nil, DeltaOperations.ManualUpdate, tags = tags1)\n      }\n      checkLastCommitTags(expectedTags = Some(tags1))\n\n      val tags2 = Map(\"testTag1\" -> \"testValue1\", \"testTag2\" -> \"testValue2\")\n      deltaLog.withNewTransaction { txn =>\n        txn.commit(addB :: Nil, DeltaOperations.Write(SaveMode.Append), tags = tags2)\n      }\n      checkLastCommitTags(expectedTags = Some(tags2))\n    }\n  }\n\n\n  test(\"empty commits are elided on write by default\") {\n    withTempDir { tableDir =>\n      val df = Seq((1, 0), (2, 1)).toDF(\"key\", \"value\")\n      df.write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n      val deltaLog = DeltaLog.forTable(spark, tableDir)\n\n      val expectedSnapshot = deltaLog.update()\n      val expectedDeltaVersion = expectedSnapshot.version\n\n      val emptyDf = Seq.empty[(Integer, Integer)].toDF(\"key\", \"value\")\n      emptyDf.write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n      val actualSnapshot = deltaLog.update()\n      val actualDeltaVersion = actualSnapshot.version\n\n      checkAnswer(spark.read.format(\"delta\").load(tableDir.getCanonicalPath),\n        Row(1, 0) :: Row(2, 1) :: Nil)\n\n      assert(expectedDeltaVersion === actualDeltaVersion)\n    }\n  }\n\n  Seq(true, false).foreach { skip =>\n    test(s\"Elide empty commits when requested - skipRecordingEmptyCommits=$skip\") {\n      withSQLConf(DeltaSQLConf.DELTA_SKIP_RECORDING_EMPTY_COMMITS.key -> skip.toString) {\n        withTempDir { tableDir =>\n          val df = Seq((1, 0), (2, 1)).toDF(\"key\", \"value\")\n          df.write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n          val deltaLog = DeltaLog.forTable(spark, tableDir)\n\n          val expectedSnapshot = deltaLog.update()\n          val expectedDeltaVersion = if (skip) {\n            expectedSnapshot.version\n          } else {\n            expectedSnapshot.version + 1\n          }\n\n          val emptyDf = Seq.empty[(Integer, Integer)].toDF(\"key\", \"value\")\n          emptyDf.write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n          val actualSnapshot = deltaLog.update()\n          val actualDeltaVersion = actualSnapshot.version\n\n          checkAnswer(spark.read.format(\"delta\").load(tableDir.getCanonicalPath),\n            Row(1, 0) :: Row(2, 1) :: Nil)\n\n          assert(expectedDeltaVersion === actualDeltaVersion)\n        }\n      }\n    }\n  }\n\n  BOOLEAN_DOMAIN.foreach { conflict =>\n    test(s\"commitLarge should handle Commit Failed Exception with conflict: $conflict\") {\n      withTempDir { tempDir =>\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n        val commitCoordinatorName = \"retryable-conflict-commit-coordinator\"\n        class RetryableConflictCommitCoordinatorClient\n          extends InMemoryCommitCoordinator(batchSize = 5) {\n          override def commit(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              commitVersion: Long,\n              actions: java.util.Iterator[String],\n              updatedActions: UpdatedActions): CommitResponse = {\n            if (updatedActions.getCommitInfo.asInstanceOf[CommitInfo].operation\n                == DeltaOperations.OP_RESTORE) {\n              deltaLog.startTransaction().commit(addB :: Nil, ManualUpdate)\n              throw new CommitFailedException(true, conflict, \"\")\n            }\n            super.commit(logStore, hadoopConf, tableDesc, commitVersion, actions, updatedActions)\n          }\n        }\n        object RetryableConflictCommitCoordinatorBuilder$ extends CommitCoordinatorBuilder {\n          lazy val commitCoordinatorClient = new RetryableConflictCommitCoordinatorClient()\n          override def getName: String = commitCoordinatorName\n          override def build(\n              spark: SparkSession,\n              conf: Map[String, String]): CommitCoordinatorClient = commitCoordinatorClient\n        }\n        CommitCoordinatorProvider.registerBuilder(RetryableConflictCommitCoordinatorBuilder$)\n        val conf =\n          Map(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> commitCoordinatorName)\n        deltaLog.startTransaction().commit(Seq(Metadata(configuration = conf)), ManualUpdate)\n        deltaLog.startTransaction().commit(addA :: Nil, ManualUpdate)\n        val records = Log4jUsageLogger.track {\n          // commitLarge must fail because of a conflicting commit at version-2.\n          val e = intercept[Exception] {\n            deltaLog.startTransaction().commitLarge(\n              spark,\n              nonProtocolMetadataActions = (addB :: Nil).iterator,\n              newProtocolOpt = None,\n              op = DeltaOperations.Restore(Some(0), None),\n              context = Map.empty,\n              metrics = Map.empty)\n          }\n          if (conflict) {\n            assert(e.isInstanceOf[ConcurrentWriteException])\n            assert(\n              e.getMessage.contains(\n                \"A concurrent transaction has written new data since the current transaction \" +\n                  s\"read the table. Please try the operation again\"))\n          } else {\n              assert(e.isInstanceOf[CommitFailedException])\n          }\n          assert(deltaLog.update().version == 2)\n        }\n        val failureRecord = filterUsageRecords(records, \"delta.commitLarge.failure\")\n        assert(failureRecord.size == 1)\n        val data = JsonUtils.fromJson[Map[String, Any]](failureRecord.head.blob)\n        assert(data(\"fromCoordinatedCommits\") == true)\n        assert(data(\"fromCoordinatedCommitsConflict\") == conflict)\n        assert(data(\"fromCoordinatedCommitsRetryable\") == true)\n      }\n    }\n  }\n\n  test(\"Append does not trigger snapshot state computation\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_WRITE_CHECKSUM_ENABLED.key -> \"false\",\n      DeltaSQLConf.INCREMENTAL_COMMIT_ENABLED.key -> \"true\",\n      DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key -> \"false\",\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_VERIFICATION_MODE_ENABLED.key -> \"false\",\n      DeltaSQLConf.DELTA_ALL_FILES_IN_CRC_FORCE_VERIFICATION_MODE_FOR_NON_UTC_ENABLED.key ->\n        \"false\",\n      DeltaSQLConf.INCREMENTAL_COMMIT_FORCE_VERIFY_IN_TESTS.key -> \"false\"\n    ) {\n      withTempDir { tableDir =>\n        val df = Seq((1, 0), (2, 1)).toDF(\"key\", \"value\")\n        df.write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n        val deltaLog = DeltaLog.forTable(spark, tableDir)\n        val preCommitSnapshot = deltaLog.update()\n        assert(!preCommitSnapshot.stateReconstructionTriggered)\n\n        df.write.format(\"delta\").mode(\"append\").save(tableDir.getCanonicalPath)\n\n        val postCommitSnapshot = deltaLog.update()\n        assert(!preCommitSnapshot.stateReconstructionTriggered)\n        assert(!postCommitSnapshot.stateReconstructionTriggered)\n      }\n    }\n  }\n\n\n  test(\"partition column changes not thrown for valid CREATE/REPLACE operations\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n\n      sql(s\"\"\"CREATE TABLE delta.`$tablePath`\n              USING delta\n              PARTITIONED BY (part)\n              AS SELECT id, id % 3 as part FROM range(10)\"\"\")\n\n      sql(s\"\"\"CREATE OR REPLACE TABLE delta.`$tablePath`\n              USING delta\n              PARTITIONED BY (newpart)\n              AS SELECT id, id % 5 as newpart FROM range(10)\"\"\")\n\n      sql(s\"\"\"REPLACE TABLE delta.`$tablePath`\n              USING delta\n              PARTITIONED BY (newpart)\n              AS SELECT id, id % 5 as newpart FROM range(10)\"\"\")\n    }\n  }\n\n  Seq((\"path\", true), (\"catalog\", false)).foreach { case (tableType, isPath) =>\n    Seq(\"dfv1\", \"dfv2\", \"sql\").foreach { method =>\n      Seq(\"error\", \"overwrite\")\n        .filterNot(mode => method == \"dfv2\" && mode == \"overwrite\")\n        .foreach { mode =>\n          test(s\"partition column changes not thrown for $method $mode on new $tableType table\") {\n            withTempDir { tempDir =>\n              val pathOrTable = getPathOrTable(tempDir, isPath, s\"${method}_${mode}_${tableType}\")\n\n              try {\n                writePartitionedTable(\n                  pathOrTable, isPath, \"part\", 10, mode = mode, method = method)\n                assertPartitionColumnsForTest(pathOrTable, isPath, Seq(\"part\"))\n              } finally {\n                if (!isPath) sql(s\"DROP TABLE IF EXISTS $pathOrTable\")\n              }\n            }\n          }\n        }\n    }\n  }\n\n  Seq((\"path\", true), (\"catalog\", false)).foreach { case (tableType, isPath) =>\n    Seq(\"sql\", \"dfv1\", \"dfv2\").foreach { method =>\n      test(s\"partition column changes not thrown for $method append on $tableType\") {\n        withTempDir { tempDir =>\n          val pathOrTable = getPathOrTable(tempDir, isPath, s\"${method}_append_${tableType}\")\n\n          try {\n            writePartitionedTable(\n              pathOrTable, isPath, \"part\", 10, mode = \"error\", method = method)\n            writePartitionedTable(\n              pathOrTable, isPath, \"part\", 10, mode = \"append\",\n              rangeStart = 10, rangeEnd = 20, method = method)\n            assertPartitionColumnsForTest(pathOrTable, isPath, Seq(\"part\"))\n          } finally {\n            if (!isPath) sql(s\"DROP TABLE IF EXISTS $pathOrTable\")\n          }\n        }\n      }\n    }\n  }\n\n  // Among others, this includes a test containing an overwrite using .saveAsTable() which\n  // creates a ReplaceTable operation under the hood.\n  Seq(\n    (\"path\", true),\n    (\"catalog\", false)\n  ).foreach { case (tableType, isPath) =>\n    Seq(\"dfv1\", \"dfv2\", \"sql\").foreach { method =>\n      Seq(\"overwrite\", \"createOrReplace\").foreach { mode =>\n        // Skip unsupported combinations\n        if (!(method == \"dfv1\" && mode == \"createOrReplace\")) {\n          test(s\"partition col changes allowed for $method $tableType $mode\") {\n            withTempDir { tempDir =>\n              val pathOrTable = if (isPath) {\n                tempDir.getAbsolutePath\n              } else {\n                s\"test_table_${method}_${tableType}_${mode}\"\n              }\n\n              // Create initial table\n              writeThreeColumnTable(pathOrTable, \"col1\")\n\n              if (isPath) {\n                assertPartitionColumns(pathOrTable, Seq(\"col1\"))\n              } else {\n                assertPartitionColumns(new TableIdentifier(pathOrTable), Seq(\"col1\"))\n              }\n\n              // Overwrite with different partition column\n              writeThreeColumnTable(pathOrTable, \"col2\", method = method, mode = mode)\n\n              val expectedAnswer = spark.range(10)\n                .withColumn(\"col1\", col(\"id\") % 3).withColumn(\"col2\", col(\"id\") % 5)\n\n              if (isPath) {\n                assertPartitionColumns(pathOrTable, Seq(\"col2\"))\n                checkAnswer(spark.read.format(\"delta\").load(pathOrTable), expectedAnswer)\n              } else {\n                assertPartitionColumns(new TableIdentifier(pathOrTable), Seq(\"col2\"))\n                checkAnswer(spark.table(pathOrTable), expectedAnswer)\n                sql(s\"DROP TABLE IF EXISTS $pathOrTable\")\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"partition column changes validation modes for UPDATE operations\") {\n    val testCases = Seq(\n      (\"true\", Some(classOf[DeltaAnalysisException]), true),\n      (\"log-only\", None, true),\n      (\"false\", None, false)\n    )\n\n    testCases.foreach { case (mode, exceptionClassOpt, expectLogEvent) =>\n      withSQLConf(DeltaSQLConf.DELTA_PARTITION_COLUMN_CHANGE_CHECK.key -> mode) {\n        withTempDir { tempDir =>\n          val tablePath = tempDir.getAbsolutePath\n          writeThreeColumnTable(tablePath, \"col1\")\n\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          val txn = deltaLog.startTransaction()\n          val newMetadata = txn.metadata.copy(partitionColumns = Seq(\"col2\"))\n\n          val logRecords = Log4jUsageLogger.track {\n            exceptionClassOpt match {\n              case Some(_) =>\n                checkError(\n                  intercept[DeltaAnalysisException] {\n                    txn.commit(\n                      Seq(newMetadata),\n                      DeltaOperations.Update(predicate = Some(EqualTo(Literal(1), Literal(1))))\n                    )\n                  },\n                  condition = \"DELTA_UNSUPPORTED_PARTITION_COLUMN_CHANGE\",\n                  sqlState = \"42P10\",\n                  parameters = Map(\n                    \"operation\" -> \"UPDATE\",\n                    \"oldPartitionColumns\" -> \"col1\",\n                    \"newPartitionColumns\" -> \"col2\"\n                  )\n                )\n              case None =>\n                // Should succeed without throwing\n                txn.commit(\n                  Seq(newMetadata),\n                  DeltaOperations.Update(predicate = Some(EqualTo(Literal(1), Literal(1))))\n                )\n            }\n          }\n\n          // Check for log event if expected\n          if (expectLogEvent) {\n            val matchingRecords =\n              filterUsageRecords(logRecords, \"delta.metadataCheck.illegalPartitionColumnChange\")\n            assert(matchingRecords.nonEmpty,\n              \"Expected to find log event 'delta.metadataCheck.illegalPartitionColumnChange' \" +\n                \"but it was not logged\")\n          }\n\n          // Verify final state only if commit succeeded\n          if (exceptionClassOpt.isEmpty) {\n            assertPartitionColumns(tablePath, Seq(\"col2\"))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"partition column changes validation default mode blocks UPDATE operations\") {\n    // Test that the default behavior (without explicit config) is to block partition column changes\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      writeThreeColumnTable(tablePath, \"col1\")\n\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      val txn = deltaLog.startTransaction()\n      val newMetadata = txn.metadata.copy(partitionColumns = Seq(\"col2\"))\n\n      checkError(\n        intercept[DeltaAnalysisException] {\n          txn.commit(\n            Seq(newMetadata),\n            DeltaOperations.Update(predicate = Some(EqualTo(Literal(1), Literal(1))))\n          )\n        },\n        condition = \"DELTA_UNSUPPORTED_PARTITION_COLUMN_CHANGE\",\n        sqlState = \"42P10\",\n        parameters = Map(\n          \"operation\" -> \"UPDATE\",\n          \"oldPartitionColumns\" -> \"col1\",\n          \"newPartitionColumns\" -> \"col2\"\n        )\n      )\n      assertPartitionColumns(tablePath, Seq(\"col1\"))\n    }\n  }\n\n  Seq(\n    // Recreation of running DFv1 .save() overwrite changing partition cols.\n    (\"blocked for Write(Overwrite, partitionBy)\", \"WRITE\",\n      (_: Metadata) => DeltaOperations.Write(SaveMode.Overwrite, partitionBy = Some(Seq(\"col2\")))),\n\n    // Recreation of running DFv1 .save() replaceWhere changing partition cols.\n    (\"blocked for Write(Overwrite, partitionBy, predicate)\", \"WRITE\",\n      (_: Metadata) => DeltaOperations.Write(SaveMode.Overwrite, partitionBy = Some(Seq(\"col2\")),\n        predicate = Some(\"col1=0\"))),\n\n    // Recreation of running DFv1 .save() DPO changing partition cols.\n    (\"blocked for Write(Overwrite, partitionBy, DPO)\", \"WRITE\",\n      (_: Metadata) => DeltaOperations.Write(SaveMode.Overwrite, partitionBy = Some(Seq(\"col2\")),\n        isDynamicPartitionOverwrite = Some(true))),\n\n    // Recreation of running DFv1 .saveAsTable() overwrite changing partition cols.\n    (\"blocked for ReplaceTable(isV1SaveAsTableOverwrite=true)\", \"CREATE OR REPLACE TABLE AS SELECT\",\n      (newMeta: Metadata) => DeltaOperations.ReplaceTable(\n        metadata = newMeta,\n        isManaged = true,\n        orCreate = true,\n        asSelect = true,\n        isV1SaveAsTableOverwrite = Some(true)))\n  ).foreach { case (testSuffix, expectedOpName, mkOp) =>\n    test(s\"partition column changes $testSuffix\") {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        writeThreeColumnTable(tablePath, \"col1\")\n\n        val deltaLog = DeltaLog.forTable(spark, tablePath)\n        val txn = deltaLog.startTransaction()\n        val newMetadata = txn.metadata.copy(partitionColumns = Seq(\"col2\"))\n\n        checkError(\n          intercept[DeltaAnalysisException] {\n            txn.commit(Seq(newMetadata), mkOp(newMetadata))\n          },\n          condition = \"DELTA_UNSUPPORTED_PARTITION_COLUMN_CHANGE\",\n          sqlState = \"42P10\",\n          parameters = Map(\n            \"operation\" -> expectedOpName,\n            \"oldPartitionColumns\" -> \"col1\",\n            \"newPartitionColumns\" -> \"col2\"\n          )\n        )\n        assertPartitionColumns(tablePath, Seq(\"col1\"))\n      }\n    }\n  }\n\n  test(\"partition column changes allowed for CLONE operations\") {\n    withTempDir { sourceDir =>\n      withTempDir { targetDir =>\n        val sourcePath = sourceDir.getAbsolutePath\n        val targetPath = targetDir.getAbsolutePath\n\n        writePartitionedTable(sourcePath, isPath = true, \"part\", 10)\n\n        // Test SHALLOW CLONE\n        sql(s\"CREATE TABLE delta.`$targetPath` SHALLOW CLONE delta.`$sourcePath`\")\n        assertPartitionColumns(targetPath, Seq(\"part\"))\n      }\n    }\n  }\n\n  test(\"partition column changes allowed for RenameColumn when partition column renamed\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      writePartitionedTable(tablePath, isPath = true, \"part\", 10)\n\n      sql(s\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\")\n      sql(s\"ALTER TABLE delta.`$tablePath` RENAME COLUMN part TO renamed_part\")\n\n      assertPartitionColumns(tablePath, Seq(\"renamed_part\"))\n    }\n  }\n\n  Seq((\"path\", true), (\"catalog\", false)).foreach { case (tableType, isPath) =>\n    test(s\"dfv1 overwrite without overwriteSchema blocks partition column changes - $tableType\") {\n      withTempDir { tempDir =>\n        val pathOrTable = getPathOrTable(tempDir, isPath, s\"dfv1_overwrite_${tableType}\")\n\n        try {\n          writeThreeColumnTable(pathOrTable, \"col1\")\n          assertPartitionColumnsForTest(pathOrTable, isPath, Seq(\"col1\"))\n\n          val df = spark.range(10)\n            .withColumn(\"col1\", col(\"id\") % 3)\n            .withColumn(\"col2\", col(\"id\") % 5)\n\n          val ex = intercept[DeltaAnalysisException] {\n            val writer = df.write.format(\"delta\").mode(\"overwrite\").partitionBy(\"col2\")\n            if (isPath) writer.save(pathOrTable) else writer.saveAsTable(pathOrTable)\n          }\n\n          assert(ex.getMessage.contains(\"overwriteSchema\") ||\n            ex.getMessage.contains(\"incompatible\"))\n          assertPartitionColumnsForTest(pathOrTable, isPath, Seq(\"col1\"))\n        } finally {\n          if (!isPath) sql(s\"DROP TABLE IF EXISTS $pathOrTable\")\n        }\n      }\n    }\n  }\n\n  // Helper methods\n  private def writePartitionedTable(\n    pathOrTable: String,\n    isPath: Boolean,\n    partitionCol: String,\n    range: Int,\n    mode: String = \"error\",\n    rangeStart: Int = 0,\n    rangeEnd: Int = -1,\n    method: String = \"dfv1\"): Unit = {\n\n    val end = if (rangeEnd == -1) range else rangeEnd\n    val df = spark.range(rangeStart, end)\n      .withColumn(partitionCol, col(\"id\") % 3)\n\n    val tableRef = if (isPath) s\"delta.`$pathOrTable`\" else pathOrTable\n    val v2WriteDf = df.writeTo(tableRef).using(\"delta\").partitionedBy(col(partitionCol))\n\n    method match {\n      case \"dfv1\" =>\n        val writer = df.write.format(\"delta\").mode(mode).partitionBy(partitionCol)\n        if (isPath) {\n          writer.save(pathOrTable)\n        } else {\n          writer.saveAsTable(pathOrTable)\n        }\n      case \"dfv2\" =>\n        mode match {\n          case \"error\" | \"errorifexists\" =>\n            v2WriteDf.create()\n          case \"overwrite\" =>\n            v2WriteDf.replace()\n          case \"append\" =>\n            df.writeTo(tableRef).append()\n          case \"createOrReplace\" =>\n            v2WriteDf.createOrReplace()\n        }\n      case \"sql\" =>\n        val tempView = s\"temp_view_${System.nanoTime()}\"\n        df.createOrReplaceTempView(tempView)\n        val stmt = mode match {\n          case \"append\" => s\"INSERT INTO $tableRef SELECT * FROM $tempView\"\n          case _ =>\n            s\"\"\"CREATE OR REPLACE TABLE $tableRef\n               |USING delta PARTITIONED BY ($partitionCol)\n               |AS SELECT * FROM $tempView\"\"\".stripMargin\n        }\n        sql(stmt)\n    }\n  }\n\n  private def writeThreeColumnTable(\n    pathOrTable: String,\n    partitionCol: String,\n    method: String = \"dfv1\",\n    mode: String = \"error\"): Unit = {\n\n    val df = spark.range(10)\n      .withColumn(\"col1\", col(\"id\") % 3)\n      .withColumn(\"col2\", col(\"id\") % 5)\n\n    val isPath = pathOrTable.contains(\"/\")\n\n    method match {\n      case \"dfv1\" =>\n        val writer = df.write.format(\"delta\").partitionBy(partitionCol)\n        if (mode == \"overwrite\") {\n          writer.option(\"overwriteSchema\", \"true\")\n        }\n        if (isPath) {\n          writer.mode(mode).save(pathOrTable)\n        } else {\n          writer.mode(mode).saveAsTable(pathOrTable)\n        }\n\n      case \"dfv2\" =>\n        val tableRef = if (isPath) s\"delta.`$pathOrTable`\" else pathOrTable\n        mode match {\n          case \"error\" =>\n            df.writeTo(tableRef).using(\"delta\").partitionedBy(col(partitionCol)).create()\n          case \"overwrite\" =>\n            df.writeTo(tableRef).using(\"delta\").partitionedBy(col(partitionCol)).replace()\n          case \"createOrReplace\" =>\n            df.writeTo(tableRef).using(\"delta\").partitionedBy(col(partitionCol))\n              .createOrReplace()\n        }\n\n      case \"sql\" =>\n        val tempView = s\"temp_view_${System.nanoTime()}\"\n        df.createOrReplaceTempView(tempView)\n\n        val tableRef = if (isPath) s\"delta.`$pathOrTable`\" else pathOrTable\n        val stmt = mode match {\n          case \"append\" => s\"INSERT INTO $tableRef SELECT * FROM $tempView\"\n          case _ =>\n            s\"\"\"CREATE OR REPLACE TABLE $tableRef\n               |USING delta PARTITIONED BY ($partitionCol)\n               |AS SELECT * FROM $tempView\"\"\".stripMargin\n        }\n        sql(stmt)\n    }\n  }\n\n  private def assertPartitionColumns(pathOrTableId: Any, expected: Seq[String]): Unit = {\n    val deltaLog = pathOrTableId match {\n      case path: String => DeltaLog.forTable(spark, path)\n      case tableId: TableIdentifier => DeltaLog.forTable(spark, tableId)\n    }\n    assert(deltaLog.update().metadata.partitionColumns === expected)\n  }\n\n  // Helper to generate path or table name based on test context\n  private def getPathOrTable(\n      tempDir: File,\n      isPath: Boolean,\n      testSuffix: String): String = {\n    if (isPath) {\n      tempDir.getAbsolutePath\n    } else {\n      s\"test_table_$testSuffix\"\n    }\n  }\n\n  // Helper to assert partition columns for either path or table\n  private def assertPartitionColumnsForTest(\n      pathOrTable: String,\n      isPath: Boolean,\n      expected: Seq[String]): Unit = {\n    if (isPath) {\n      assertPartitionColumns(pathOrTable, expected)\n    } else {\n      assertPartitionColumns(new TableIdentifier(pathOrTable), expected)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/OptimisticTransactionSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.ConcurrentModificationException\n\nimport org.apache.spark.sql.delta.DeltaOperations.{ManualUpdate, Truncate}\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, FileAction, Metadata, RemoveFile}\nimport org.apache.spark.sql.delta.deletionvectors.RoaringBitmapArray\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\ntrait OptimisticTransactionSuiteBase\n  extends QueryTest\n    with SharedSparkSession\n    with DeltaTestUtilsBase\n    with DeletionVectorsTestUtils {\n\n\n  /**\n   * Check whether the test transaction conflict with the concurrent writes by executing the\n   * given params in the following order:\n   *  - setup (including setting table isolation level\n   *  - reads\n   *  - concurrentWrites\n   *  - actions\n   *\n   * When `conflicts` == true, this function checks to make sure the commit of `actions` fails with\n   * [[ConcurrentModificationException]], otherwise checks that the commit is successful.\n   *\n   * @param name                test name\n   * @param conflicts           should test transaction is expected to conflict or not\n   * @param setup               sets up the initial delta log state (set schema, partitioning, etc.)\n   * @param reads               reads made in the test transaction\n   * @param concurrentWrites    writes made by concurrent transactions after the test txn reads\n   * @param actions             actions to be committed by the test transaction\n   * @param expectedErrorClass  Expected error class for the exception\n   * @param expectedErrorMessageParameters Expected parameter map for error message validation\n   * @param exceptionClass      A substring to expect in the exception class name\n   */\n  protected def check(\n      name: String,\n      conflicts: Boolean,\n      setup: Seq[Action] = Seq(Metadata(), Action.supportedProtocolVersion(\n        featuresToExclude = Seq(CatalogOwnedTableFeature))),\n      reads: Seq[OptimisticTransaction => Unit],\n      concurrentWrites: Seq[Action],\n      actions: Seq[Action],\n      expectedErrorClass: Option[String] = None,\n      expectedErrorMessageParameters: Option[Map[String, String]] = None,\n      exceptionClass: Option[String] = None): Unit = {\n\n    val concurrentTxn: OptimisticTransaction => Unit =\n      (opt: OptimisticTransaction) => opt.commit(concurrentWrites, Truncate())\n\n    def initialSetup(log: DeltaLog): Unit = {\n      // Setup the log\n      setup.foreach { action =>\n        log.startTransaction().commit(Seq(action), ManualUpdate)\n      }\n    }\n    check(\n      name,\n      conflicts,\n      initialSetup _,\n      reads,\n      Seq(concurrentTxn),\n      actions,\n      operation = Truncate(), // a data-changing operation\n      expectedErrorClass = expectedErrorClass,\n      expectedErrorMessageParameters = expectedErrorMessageParameters,\n      exceptionClass = exceptionClass,\n      additionalSQLConfs = Seq.empty\n    )\n  }\n\n  /**\n   * Check whether the test transaction conflict with the concurrent writes by executing the\n   * given params in the following order:\n   *  - sets up the initial delta log state using `initialSetup` (set schema, partitioning, etc.)\n   *  - reads\n   *  - concurrentWrites\n   *  - actions\n   *\n   * When `conflicts` == true, this function checks to make sure the commit of `actions` fails with\n   * [[ConcurrentModificationException]], otherwise checks that the commit is successful.\n   *\n   * @param name                test name\n   * @param conflicts           should test transaction is expected to conflict or not\n   * @param initialSetup        sets up the initial delta log state (set schema, partitioning, etc.)\n   * @param reads               reads made in the test transaction\n   * @param concurrentTxns      concurrent txns that may write data after the test txn reads\n   * @param actions             actions to be committed by the test transaction\n   * @param expectedErrorClass  Expected error class for the exception\n   * @param expectedErrorMessageParameters Expected parameter map for error message validation\n   * @param exceptionClass      A substring to expect in the exception class name\n   */\n  // scalastyle:off argcount\n  protected def check(\n      name: String,\n      conflicts: Boolean,\n      initialSetup: DeltaLog => Unit,\n      reads: Seq[OptimisticTransaction => Unit],\n      concurrentTxns: Seq[OptimisticTransaction => Unit],\n      actions: Seq[Action],\n      operation: DeltaOperations.Operation,\n      expectedErrorClass: Option[String],\n      expectedErrorMessageParameters: Option[Map[String, String]],\n      exceptionClass: Option[String],\n      additionalSQLConfs: Seq[(String, String)]): Unit = {\n    // scalastyle:on argcount\n    val conflict = if (conflicts) \"should conflict\" else \"should not conflict\"\n    test(s\"$name - $conflict\") {\n      withSQLConf(additionalSQLConfs: _*) {\n        val tempDir = Utils.createTempDir()\n        val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n        // Setup the log\n        initialSetup(log)\n\n        // Perform reads\n        val txn = log.startTransaction()\n        reads.foreach(_ (txn))\n\n        // Execute concurrent txn while current transaction is active\n        concurrentTxns.foreach(txn => txn(log.startTransaction()))\n\n        // Try commit and check expected conflict behavior\n        if (conflicts) {\n          val e = intercept[ConcurrentModificationException] {\n            txn.commit(actions, operation)\n          }\n          if (expectedErrorClass.isDefined) {\n            checkError(\n              e.asInstanceOf[DeltaThrowable],\n              expectedErrorClass.get,\n              parameters = expectedErrorMessageParameters.get\n                ++ Map(\"tableName\" -> s\"delta.`${log.dataPath}`\"),\n              matchPVals = true\n            )\n          }\n          if (exceptionClass.nonEmpty) {\n            assert(e.getClass.getName.contains(exceptionClass.get))\n          }\n        } else {\n          txn.commit(actions, operation)\n        }\n      }\n    }\n  }\n\n  /**\n   * Write 3 files at target path and return AddFiles.\n   */\n  protected def writeDuplicateActionsData(path: String): Seq[AddFile] = {\n    val deltaLog = DeltaLog.forTable(spark, path)\n    spark.range(start = 0, end = 6, step = 1, numPartitions = 3)\n      .write.format(\"delta\").save(path)\n    val files = deltaLog.update().allFiles.collect().sortBy(_.insertionTime)\n    for (file <- files) {\n      assert(file.numPhysicalRecords.isDefined)\n    }\n    files\n  }\n\n  protected def addDVToFileInTable(path: String, file: AddFile): (AddFile, RemoveFile) = {\n    val deltaLog = DeltaLog.forTable(spark, path)\n    val dv = writeDV(deltaLog, RoaringBitmapArray(0L))\n    updateFileDV(file, dv)\n  }\n\n  protected def testRuntimeErrorOnCommit(\n      actions: Seq[FileAction],\n      deltaLog: DeltaLog)(\n      checkErrorFun: DeltaRuntimeException => Unit): Unit = {\n    val operation = DeltaOperations.Optimize(Seq.empty, zOrderBy = Seq.empty)\n    val txn = deltaLog.startTransaction()\n    val e = intercept[DeltaRuntimeException] {\n      withSQLConf(DeltaSQLConf.DELTA_DUPLICATE_ACTION_CHECK_ENABLED.key -> \"true\") {\n        txn.commit(actions, operation)\n      }\n    }\n    checkErrorFun(e)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ProtocolMetadataAdapterSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, LongType, MetadataBuilder, StringType, StructField, StructType}\n\n/**\n * Abstract base class for testing ProtocolMetadataAdapter implementations.\n */\nabstract class ProtocolMetadataAdapterSuiteBase\n  extends SparkFunSuite\n  with SharedSparkSession {\n\n  /**\n   * Creates a wrapper instance based on different parameters.\n   *\n   * @param minReaderVersion Protocol reader version\n   * @param minWriterVersion Protocol writer version\n   * @param readerFeatures Optional set of reader features\n   * @param writerFeatures Optional set of writer features\n   * @param schema Table schema\n   * @param configuration Table properties/configuration\n   */\n  protected def createWrapper(\n      minReaderVersion: Int = 1,\n      minWriterVersion: Int = 2,\n      readerFeatures: Option[Set[String]] = None,\n      writerFeatures: Option[Set[String]] = None,\n      schema: StructType = new StructType().add(\"id\", IntegerType),\n      configuration: Map[String, String] = Map.empty): ProtocolMetadataAdapter\n\n  Seq[(DeltaColumnMappingMode, Map[String, String])](\n    (NoMapping, Map.empty),\n    (NameMapping, Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"name\")),\n    (IdMapping, Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> \"id\"))\n  ).foreach { case (expectedMode, config) =>\n    test(s\"columnMappingMode with $expectedMode\") {\n      val wrapper = createWrapper(configuration = config)\n      assert(wrapper.columnMappingMode === expectedMode)\n    }\n  }\n\n  Seq[(String, StructType)](\n    // Empty schema: table with no columns\n    (\"empty schema\", new StructType()),\n    // Simple schema: flat structure with primitive types\n    (\"simple schema\", new StructType().add(\"id\", IntegerType).add(\"name\", StringType)),\n    // Nested schema: struct within struct\n    (\"nested schema\", new StructType()\n      .add(\"user\", new StructType()\n        .add(\"id\", IntegerType)\n        .add(\"name\", StringType))\n      .add(\"timestamp\", LongType))\n  ).foreach { case (testCaseName, schema) =>\n    test(s\"getReferenceSchema with $testCaseName\") {\n      val wrapper = createWrapper(schema = schema)\n      assert(wrapper.getReferenceSchema === schema)\n    }\n  }\n\n  Seq[(String, Boolean, Map[String, String], Option[Set[String]], Option[Set[String]])](\n    // Row tracking enabled by setting table features\n    (\"enabled\", true,\n      Map(DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"true\"),\n      Some(Set(RowTrackingFeature.name)), Some(Set(RowTrackingFeature.name))),\n    // Row tracking disabled by default\n    (\"disabled\", false, Map.empty, None,\n     None),\n    // Row tracking explicitly disabled via config\n    (\"explicitly disabled\", false,\n      Map(DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"false\"),\n     None, None)\n  ).foreach { case (testCaseName, expectedRowIdEnabled, config, readerFeatures, writerFeatures) =>\n    test(s\"isRowIdEnabled when $testCaseName\") {\n      val wrapper = createWrapper(\n        minReaderVersion = if (readerFeatures.isDefined) 3 else 1,\n        minWriterVersion = if (writerFeatures.isDefined) 7 else 2,\n        readerFeatures = readerFeatures,\n        writerFeatures = writerFeatures,\n        configuration = config)\n      assert(wrapper.isRowIdEnabled === expectedRowIdEnabled)\n    }\n  }\n\n  Seq[(String, Boolean, Option[Set[String]], Option[Set[String]])](\n    // Deletion vectors enabled via table features\n    (\"enabled\", true,\n      Some(Set(DeletionVectorsTableFeature.name)),\n      Some(Set(DeletionVectorsTableFeature.name))),\n    // Deletion vectors disabled by default\n    (\"disabled\", false, None,\n     None)\n  ).foreach { case (testCaseName, expectedDeletionVectorReadable, readerFeatures, writerFeatures) =>\n    test(s\"isDeletionVectorReadable when $testCaseName\") {\n      val wrapper = createWrapper(\n        minReaderVersion = if (readerFeatures.isDefined) 3 else 1,\n        minWriterVersion = if (writerFeatures.isDefined) 7 else 2,\n        readerFeatures = readerFeatures,\n        writerFeatures = writerFeatures)\n      assert(wrapper.isDeletionVectorReadable === expectedDeletionVectorReadable)\n    }\n  }\n\n  Seq[(String, Boolean, Map[String, String])](\n    // IcebergCompat V1 enabled\n    (\"v1 enabled\", true,\n      Map(DeltaConfigs.ICEBERG_COMPAT_V1_ENABLED.key -> \"true\")),\n    // IcebergCompat V2 enabled\n    (\"v2 enabled\", true,\n      Map(DeltaConfigs.ICEBERG_COMPAT_V2_ENABLED.key -> \"true\")),\n    // No IcebergCompat enabled\n    (\"disabled\", false, Map.empty)\n  ).foreach { case (testCaseName, expectedIcebergCompatEnabled, config) =>\n    test(s\"isIcebergCompatAnyEnabled when $testCaseName\") {\n      val wrapper = createWrapper(\n        configuration = config)\n\n      assert(wrapper.isIcebergCompatAnyEnabled === expectedIcebergCompatEnabled)\n    }\n  }\n\n  Seq[(String, Map[String, String], Seq[(Int, Boolean)])](\n    // IcebergCompat V1 enabled: only version 1 should return true\n    (\"v1 enabled\", Map(DeltaConfigs.ICEBERG_COMPAT_V1_ENABLED.key -> \"true\"),\n      Seq[(Int, Boolean)]((1, true), (2, false), (3, false))),\n    // V2 enabled: version 1 and 2 should return true\n    (\"v2 enabled\", Map(DeltaConfigs.ICEBERG_COMPAT_V2_ENABLED.key -> \"true\"),\n      Seq[(Int, Boolean)]((1, true), (2, true), (3, false))),\n    // No version enabled: all versions should return false\n    (\"disabled\", Map.empty,\n      Seq[(Int, Boolean)]((1, false), (2, false), (3, false)))\n  ).foreach { case (testCaseName, config, versionChecks) =>\n    test(s\"isIcebergCompatGeqEnabled when $testCaseName\") {\n      val wrapper = createWrapper(\n        configuration = config)\n\n      versionChecks.foreach { case (version, expectedEnabled) =>\n        assert(wrapper.isIcebergCompatGeqEnabled(version) === expectedEnabled,\n          s\"version $version check failed\")\n      }\n    }\n  }\n\n  Seq[(String, Option[org.apache.spark.sql.types.Metadata], Boolean, Map[String, String],\n    Option[Set[String]], Option[Set[String]])](\n    // Table with no special features should be readable\n    (\"readable table\", None, true, Map.empty,\n     None, None),\n    // Table with unsupported type widening (string -> integer), should not be readable\n    (\"table with unsupported type widening\",\n      Some(new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          new MetadataBuilder()\n            .putLong(\"tableVersion\", 1L)\n            .putString(\"fromType\", \"string\")\n            .putString(\"toType\", \"integer\")\n            .build()\n        ))\n        .build()),\n     false,\n      Map(DeltaConfigs.ENABLE_TYPE_WIDENING.key -> \"true\"),\n      Some(Set(TypeWideningTableFeature.name)),\n      Some(Set(TypeWideningTableFeature.name)))\n  ).foreach { case (testCaseName, typeChangeMetadata, tableReadable, config,\n    readerFeatures, writerFeatures) =>\n    test(s\"assertTableReadable with $testCaseName\") {\n      val schema = typeChangeMetadata match {\n        case Some(metadata) =>\n          new StructType().add(\"col1\", IntegerType, nullable = true, metadata = metadata)\n        case None =>\n          new StructType().add(\"id\", IntegerType)\n      }\n\n      val wrapper = createWrapper(\n        minReaderVersion = if (readerFeatures.isDefined) 3 else 1,\n        minWriterVersion = if (writerFeatures.isDefined) 7 else 2,\n        readerFeatures = readerFeatures,\n        writerFeatures = writerFeatures,\n        schema = schema,\n        configuration = config)\n\n      if (tableReadable) {\n        // Should not throw\n        wrapper.assertTableReadable(spark)\n      } else {\n        // Should throw exception\n        intercept[Exception] {\n          wrapper.assertTableReadable(spark)\n        }\n      }\n    }\n  }\n\n  Seq[(String, Map[String, String], Option[Set[String]], Option[Set[String]],\n    Boolean, Boolean)](\n    // Row tracking disabled: should return no fields\n    (\"row tracking disabled\", Map.empty, None, None,\n     false, false),\n    (\"row tracking enabled with constant or generated metadata col non nullable\",\n      Map(\n        DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"true\",\n        \"delta.rowTracking.materializedRowIdColumnName\" -> \"_row_id_col\",\n        \"delta.rowTracking.materializedRowCommitVersionColumnName\" -> \"_row_commit_version_col\"),\n      Some(Set(RowTrackingFeature.name)), Some(Set(RowTrackingFeature.name)),\n     false, false),\n    (\"row tracking enabled with constant or generated metadata col nullable\",\n      Map(\n        DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"true\",\n        \"delta.rowTracking.materializedRowIdColumnName\" -> \"_row_id_col\",\n        \"delta.rowTracking.materializedRowCommitVersionColumnName\" -> \"_row_commit_version_col\"),\n      Some(Set(RowTrackingFeature.name)), Some(Set(RowTrackingFeature.name)),\n     true, true)\n  ).foreach { case (testCaseName, config, readerFeatures, writerFeatures,\n    nullableConstant, nullableGenerated) =>\n    test(s\"createRowTrackingMetadataFields when $testCaseName\") {\n      val wrapper = createWrapper(\n        minReaderVersion = if (readerFeatures.isDefined) 3 else 1,\n        minWriterVersion = if (writerFeatures.isDefined) 7 else 2,\n        readerFeatures = readerFeatures,\n        writerFeatures = writerFeatures,\n        configuration = config)\n\n      val fields = wrapper.createRowTrackingMetadataFields(\n        nullableConstant, nullableGenerated).toSeq\n\n      val expectedFields =\n        if (!config.get(DeltaConfigs.ROW_TRACKING_ENABLED.key).contains(\"true\")) {\n          // Row tracking disabled: no fields\n          Seq.empty[StructField]\n        } else {\n          Seq[StructField](\n            StructField(\"row_id\", LongType, nullableGenerated),\n            StructField(\"base_row_id\", LongType, nullableConstant),\n            StructField(\"default_row_commit_version\", LongType, nullableConstant),\n            StructField(\"row_commit_version\", LongType, nullableGenerated))\n        }\n\n      val actualSimplified = fields.map(f => StructField(f.name, f.dataType, f.nullable))\n      assert(actualSimplified === expectedFields)\n    }\n  }\n\n}\n\n/**\n * Unit tests for ProtocolMetadataAdapterV1.\n *\n * This suite tests the V1 wrapper implementation that adapts delta-spark's Protocol and Metadata\n * to the ProtocolMetadataAdapter interface.\n */\nclass ProtocolMetadataAdapterV1Suite extends ProtocolMetadataAdapterSuiteBase {\n\n  override protected def createWrapper(\n      minReaderVersion: Int = 1,\n      minWriterVersion: Int = 2,\n      readerFeatures: Option[Set[String]] = None,\n      writerFeatures: Option[Set[String]] = None,\n      schema: StructType = new StructType().add(\"id\", IntegerType),\n      configuration: Map[String, String] = Map.empty): ProtocolMetadataAdapter = {\n\n    val protocol = Protocol(\n      minReaderVersion = minReaderVersion,\n      minWriterVersion = minWriterVersion,\n      readerFeatures = readerFeatures,\n      writerFeatures = writerFeatures)\n\n    val metadata = Metadata(\n      schemaString = schema.json,\n      configuration = configuration)\n\n    ProtocolMetadataAdapterV1(protocol, metadata)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/RestoreTableSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame}\n\n/** Restore tests using the SQL. */\nclass RestoreTableSQLSuite extends RestoreTableSuiteBase {\n\n  override def restoreTableToVersion(\n      tblId: String,\n      version: Int,\n      isTable: Boolean,\n      expectNoOp: Boolean = false): DataFrame = {\n    val identifier = if (isTable) {\n      tblId\n    } else {\n      s\"delta.`$tblId`\"\n    }\n    spark.sql(s\"RESTORE TABLE $identifier VERSION AS OF ${version}\")\n  }\n\n  override def restoreTableToTimestamp(\n      tblId: String,\n      timestamp: String,\n      isTable: Boolean,\n      expectNoOp: Boolean = false): DataFrame = {\n    val identifier = if (isTable) {\n      tblId\n    } else {\n      s\"delta.`$tblId`\"\n    }\n    spark.sql(s\"RESTORE $identifier TO TIMESTAMP AS OF '${timestamp}'\")\n  }\n\n  test(\"restoring a table that doesn't exist\") {\n    val ex = intercept[AnalysisException] {\n      sql(s\"RESTORE TABLE not_exists VERSION AS OF 0\")\n    }\n    assert(ex.getMessage.contains(\"Table not found\")\n      || ex.getMessage.contains(\"TABLE_OR_VIEW_NOT_FOUND\"))\n  }\n\n  test(\"restoring a view\") {\n    withTempView(\"tmp\") {\n      sql(\"CREATE OR REPLACE TEMP VIEW tmp AS SELECT * FROM range(10)\")\n      val ex = intercept[AnalysisException] {\n        sql(s\"RESTORE tmp TO VERSION AS OF 0\")\n      }\n      assert(ex.getMessage.contains(\"only supported for Delta tables\"))\n    }\n  }\n\n  test(\"restoring a view over a Delta table\") {\n    withTable(\"delta_table\") {\n      withView(\"tmp\") {\n        sql(\"CREATE TABLE delta_table USING delta AS SELECT * FROM range(10)\")\n        sql(\"CREATE VIEW tmp AS SELECT * FROM delta_table\")\n        val ex = intercept[AnalysisException] {\n          sql(s\"RESTORE TABLE tmp VERSION AS OF 0\")\n        }\n        assert(ex.getMessage.contains(\"only supported for Delta tables\"))\n      }\n    }\n  }\n}\n\nclass RestoreTableSQLWithCatalogOwnedBatch1Suite extends RestoreTableSQLSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass RestoreTableSQLWithCatalogOwnedBatch2Suite extends RestoreTableSQLSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass RestoreTableSQLWithCatalogOwnedBatch100Suite extends RestoreTableSQLSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\n\nclass RestoreTableSQLNameColumnMappingSuite extends RestoreTableSQLSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  import testImplicits._\n\n  override protected def runOnlyTests = Seq(\n    \"path based table\",\n    \"metastore based table\"\n  )\n\n\n  test(\"restore prior to column mapping upgrade should fail\") {\n    withTempDir { tempDir =>\n      val df1 = Seq(1, 2, 3).toDF(\"id\")\n      val df2 = Seq(4, 5, 6).toDF(\"id\")\n\n      def deltaLog: DeltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n\n      withColumnMappingConf(\"none\") {\n        df1.write.format(\"delta\").save(tempDir.getAbsolutePath)\n        require(deltaLog.update().version == 0)\n\n        df2.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        assert(deltaLog.update().version == 1)\n      }\n\n      // upgrade to column mapping mode\n      sql(\n        s\"\"\"\n           |ALTER TABLE delta.`$tempDir`\n           |SET TBLPROPERTIES (\n           |  ${DeltaConfigs.COLUMN_MAPPING_MODE.key} = '$columnMappingModeString',\n           |  ${DeltaConfigs.MIN_READER_VERSION.key} = '2',\n           |  ${DeltaConfigs.MIN_WRITER_VERSION.key} = '5'\n           |)\n           |\"\"\".stripMargin)\n\n      assert(deltaLog.update().version == 2)\n\n      // try restore back to version 1 before column mapping should fail\n      intercept[ColumnMappingUnsupportedException] {\n        restoreTableToVersion(tempDir.getAbsolutePath, version = 1, isTable = false)\n      }\n    }\n  }\n\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/RestoreTableScalaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.sql.Date\n\nimport scala.concurrent.duration._\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.test.DeltaExcludedTestMixin\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{DataFrame, Row}\nimport org.apache.spark.util.Utils\n\n/** Restore tests using the Scala APIs. */\nclass RestoreTableScalaSuite extends RestoreTableSuiteBase {\n\n  override def restoreTableToVersion(\n      tblId: String,\n      version: Int,\n      isTable: Boolean,\n      expectNoOp: Boolean = false): DataFrame = {\n    val deltaTable = if (isTable) {\n      io.delta.tables.DeltaTable.forName(spark, tblId)\n    } else {\n      io.delta.tables.DeltaTable.forPath(spark, tblId)\n    }\n\n    deltaTable.restoreToVersion(version)\n  }\n\n  override def restoreTableToTimestamp(\n      tblId: String,\n      timestamp: String,\n      isTable: Boolean,\n      expectNoOp: Boolean = false): DataFrame = {\n    val deltaTable = if (isTable) {\n      io.delta.tables.DeltaTable.forName(spark, tblId)\n    } else {\n      io.delta.tables.DeltaTable.forPath(spark, tblId)\n    }\n\n    deltaTable.restoreToTimestamp(timestamp)\n  }\n}\n\nclass RestoreTableScalaWithCatalogOwnedBatch1Suite extends RestoreTableScalaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass RestoreTableScalaWithCatalogOwnedBatch2Suite extends RestoreTableScalaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass RestoreTableScalaWithCatalogOwnedBatch100Suite extends RestoreTableScalaSuite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n\nclass RestoreTableScalaDeletionVectorSuite\n    extends RestoreTableScalaSuite\n    with DeletionVectorsTestUtils\n    with DeltaExcludedTestMixin  {\n\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark.conf)\n  }\n  override def excluded: Seq[String] = super.excluded ++\n    Seq(\n      // These tests perform a delete to produce a file to vacuum, but with persistent DVs enabled,\n      // we actually just add a DV to the file instead, so there's no unreferenced file for vacuum.\n      \"restore after vacuum\",\n      \"restore after vacuum - cloned table\",\n      // These rely on the new-table protocol version to be lower than the latest,\n      // but this isn't true for DVs.\n      \"restore downgrade protocol (allowed=true)\",\n      \"restore downgrade protocol (allowed=false)\",\n      \"restore downgrade protocol with table features (allowed=true)\",\n      \"restore downgrade protocol with table features (allowed=false)\",\n      \"cdf + RESTORE with write amplification reduction\",\n      \"RESTORE doesn't account for session defaults\"\n    )\n\n  case class RestoreAndCheckArgs(versionToRestore: Int, expectedResult: DataFrame)\n  type RestoreAndCheckFunction = RestoreAndCheckArgs => Unit\n\n  /**\n   * Tests `testFun` once by restoring to version and once to timestamp.\n   *\n   * `testFun` is expected to perform setup before executing the `RestoreAndTestFunction` and\n   * cleanup afterwards.\n   */\n  protected def testRestoreByTimestampAndVersion\n      (testName: String)\n      (testFun: (String, RestoreAndCheckFunction) => Unit): Unit = {\n    for (restoreToVersion <- BOOLEAN_DOMAIN) {\n      val restoringTo = if (restoreToVersion) \"version\" else \"timestamp\"\n      test(testName + s\" - restoring to $restoringTo\") {\n        withTempDir{ dir =>\n          val path = dir.toString\n          val restoreAndCheck: RestoreAndCheckFunction = (args: RestoreAndCheckArgs) => {\n            val deltaLog = DeltaLog.forTable(spark, path)\n            if (restoreToVersion) {\n              restoreTableToVersion(path, args.versionToRestore, isTable = false)\n            } else {\n              // Set a custom timestamp for the commit\n              val desiredDateS = new Date(System.currentTimeMillis() - 3.days.toMillis).toString\n              setTimestampToCommitFileAtVersion(\n                deltaLog,\n                version = args.versionToRestore,\n                date = desiredDateS)\n              // Set all previous versions to something lower, so we don't error out.\n              for (version <- 0 until args.versionToRestore) {\n                val previousDateS = new Date(System.currentTimeMillis() - 5.days.toMillis).toString\n                setTimestampToCommitFileAtVersion(\n                  deltaLog,\n                  version = version,\n                  date = previousDateS)\n              }\n\n              restoreTableToTimestamp(path, desiredDateS, isTable = false)\n            }\n            checkAnswer(spark.read.format(\"delta\").load(path), args.expectedResult)\n          }\n          testFun(path, restoreAndCheck)\n        }\n      }\n    }\n  }\n\n  testRestoreByTimestampAndVersion(\n    \"Restoring table with persistent DVs to version without DVs\") { (path, restoreAndCheck) =>\n    val deltaLog = DeltaLog.forTable(spark, path)\n    val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\")\n    val values2 = Seq(6, 7, 8, 9, 10)\n    val df2 = values2.toDF(\"id\")\n\n    // Write all values into version 0.\n    df1.union(df2).coalesce(1).write.format(\"delta\").save(path) // version 0\n    checkAnswer(spark.read.format(\"delta\").load(path), expectedAnswer = df1.union(df2))\n    val snapshotV0 = deltaLog.update()\n    assert(snapshotV0.version === 0)\n\n    // Delete values 2 so that version 1 is `df1`.\n    spark.sql(s\"DELETE FROM delta.`$path` WHERE id IN (${values2.mkString(\", \")})\") // version 1\n    assert(getFilesWithDeletionVectors(deltaLog).size > 0)\n    checkAnswer(spark.read.format(\"delta\").load(path), expectedAnswer = df1)\n    val snapshotV1 = deltaLog.snapshot\n    assert(snapshotV1.version === 1)\n\n    restoreAndCheck(RestoreAndCheckArgs(versionToRestore = 0, expectedResult = df1.union(df2)))\n    assert(getFilesWithDeletionVectors(deltaLog).size === 0)\n  }\n\n  testRestoreByTimestampAndVersion(\n    \"Restoring table with persistent DVs to version with DVs\") { (path, restoreAndCheck) =>\n    val deltaLog = DeltaLog.forTable(spark, path)\n    val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\")\n    val values2 = Seq(6, 7)\n    val df2 = values2.toDF(\"id\")\n    val values3 = Seq(8, 9, 10)\n    val df3 = values3.toDF(\"id\")\n\n    // Write all values into version 0.\n    df1.union(df2).union(df3).coalesce(1).write.format(\"delta\").save(path) // version 0\n\n    // Delete values 2 and 3 in reverse order, so that version 1 is `df1.union(df2)`.\n    spark.sql(s\"DELETE FROM delta.`$path` WHERE id IN (${values3.mkString(\", \")})\") // version 1\n    assert(getFilesWithDeletionVectors(deltaLog).size > 0)\n    checkAnswer(spark.read.format(\"delta\").load(path), expectedAnswer = df1.union(df2))\n    spark.sql(s\"DELETE FROM delta.`$path` WHERE id IN (${values2.mkString(\", \")})\") // version 2\n    assert(getFilesWithDeletionVectors(deltaLog).size > 0)\n\n    restoreAndCheck(RestoreAndCheckArgs(versionToRestore = 1, expectedResult = df1.union(df2)))\n    assert(getFilesWithDeletionVectors(deltaLog).size > 0)\n  }\n\n  testRestoreByTimestampAndVersion(\"Restoring table with persistent DVs to version \" +\n      \"without persistent DVs enabled\") { (path, restoreAndCheck) =>\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"false\",\n        // Disable the log clean up. Tests sets the timestamp on commit files to long back\n        // in time that triggers the commit file clean up as part of the [[MetadataCleanup]]\n        DeltaConfigs.ENABLE_EXPIRED_LOG_CLEANUP.defaultTablePropertyKey -> \"false\") {\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\")\n      val values2 = Seq(6, 7, 8, 9, 10)\n      val df2 = values2.toDF(\"id\")\n\n      // Write all values into version 0.\n      df1.union(df2).coalesce(1).write.format(\"delta\").save(path) // version 0\n      checkAnswer(spark.read.format(\"delta\").load(path), expectedAnswer = df1.union(df2))\n      val snapshotV0 = deltaLog.update()\n      assert(snapshotV0.version === 0)\n      assert(!DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(snapshotV0.metadata))\n\n      // Upgrade to us DVs\n      spark.sql(s\"ALTER TABLE delta.`$path` SET TBLPROPERTIES \" +\n        s\"(${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key} = true)\")\n      val snapshotV1 = deltaLog.update()\n      assert(snapshotV1.version === 1)\n      assert(DeletionVectorUtils.deletionVectorsReadable(snapshotV1))\n      assert(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(snapshotV1.metadata))\n\n      // Delete values 2 so that version 1 is `df1`.\n      spark.sql(s\"DELETE FROM delta.`$path` WHERE id IN (${values2.mkString(\", \")})\") // version 2\n      assert(getFilesWithDeletionVectors(deltaLog).size > 0)\n      checkAnswer(spark.read.format(\"delta\").load(path), expectedAnswer = df1)\n      val snapshotV2 = deltaLog.update()\n      assert(snapshotV2.version === 2)\n\n      // Restore to before the version upgrade. Protocol version should be retained (to make the\n      // history readable), but DV creation should be disabled again.\n      restoreAndCheck(RestoreAndCheckArgs(versionToRestore = 0, expectedResult = df1.union(df2)))\n      val snapshotV3 = deltaLog.update()\n      assert(getFilesWithDeletionVectors(deltaLog).size === 0)\n      assert(DeletionVectorUtils.deletionVectorsReadable(snapshotV3))\n      assert(!DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.fromMetaData(snapshotV3.metadata))\n      // Check that we can still read versions that did have DVs.\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"versionAsOf\", \"2\").load(path),\n        expectedAnswer = df1)\n    }\n  }\n  test(\"CDF + DV + RESTORE\") {\n    withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n      withTempDir { tempDir =>\n        val df0 = Seq(0, 1).toDF(\"id\") // version 0 = [0, 1]\n        df0.write.format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val df1 = Seq(2).toDF(\"id\") // version 1: append to df0 = [0, 1, 2]\n        df1.write.mode(\"append\").format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n        deltaTable.delete(\"id < 1\") // version 2: delete (0) = [1, 2]\n\n        deltaTable.updateExpr(\n          \"id > 1\",\n          Map(\"id\" -> \"4\")\n        ) // version 3: update 2 --> 4 = [1, 4]\n\n        // version 4: restore to version 2 (delete 4, insert 2) = [1, 2]\n        restoreTableToVersion(tempDir.getAbsolutePath, 2, false)\n        checkAnswer(\n          CDCReader.changesToBatchDF(DeltaLog.forTable(spark, tempDir), 4, 4, spark)\n            .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n          Row(4, \"delete\", 4) :: Row(2, \"insert\", 4) :: Nil\n        )\n\n        // version 5: restore to version 1 (insert 0) = [0, 1, 2]\n        restoreTableToVersion(tempDir.getAbsolutePath, 1, false)\n        checkAnswer(\n          CDCReader.changesToBatchDF(DeltaLog.forTable(spark, tempDir), 5, 5, spark)\n            .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n          Row(0, \"insert\", 5) :: Nil\n        )\n\n        // version 6: restore to version 0 (delete 2) = [0, 1]\n        restoreTableToVersion(tempDir.getAbsolutePath, 0, false)\n        checkAnswer(\n          CDCReader.changesToBatchDF(DeltaLog.forTable(spark, tempDir), 6, 6, spark)\n            .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n          Row(2, \"delete\", 6) :: Nil\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/RestoreTableSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\nimport java.sql.Date\nimport java.sql.Timestamp\nimport java.text.SimpleDateFormat\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.actions.{Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\n/** Base suite containing the restore tests. */\ntrait RestoreTableSuiteBase\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite {\n\n  import testImplicits._\n\n  // Will be overridden in sub-class\n  /**\n   * @param tblId            - the table identifier either table name or path\n   * @param version          - version to restore to\n   * @param isMetastoreTable - whether its a path based table or metastore table\n   * @param expectNoOp       - whether the restore is no-op or not\n   */\n  protected def restoreTableToVersion(\n     tblId: String,\n     version: Int,\n     isMetastoreTable: Boolean,\n     expectNoOp: Boolean = false): DataFrame\n\n  /**\n   * @param tblId            - the table identifier either table name or path\n   * @param timestamp        - timestamp to restore to\n   * @param isMetastoreTable - whether its a path based table or a metastore table.\n   * @param expectNoOp       - whether the restore is no-op or not\n   */\n  protected def restoreTableToTimestamp(\n     tblId: String,\n     timestamp: String,\n     isMetastoreTable: Boolean,\n     expectNoOp: Boolean = false): DataFrame\n\n  /**\n   * Override the timestamp of the commit file at the given version.\n   * If CC is enabled, then the timestamp generated by InCommitTimestamp will be\n   * overridden since ICT is a dependent feature of CC.\n   * Otherwise, the file modification time will be overridden.\n   *\n   * NOTE: When CC is enabled, this method will only override the commit timestamp\n   *       for the backfilled/published commit files.\n   *\n   * @param deltaLog The DeltaLog for the table.\n   * @param version The specific version of the commit file to override.\n   * @param timestamp The timestamp to set as the commit timestamp.\n   */\n  protected def overrideTimestampOfCommitFile(\n      deltaLog: DeltaLog,\n      version: Int,\n      timestamp: Long): Unit = {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      // If CC is enabled, then timestamp generated by ICT will need to be overridden.\n      InCommitTimestampTestUtils.overwriteICTInDeltaFile(\n        deltaLog,\n        filePath = FileNames.unsafeDeltaFile(path = deltaLog.logPath, version),\n        ts = Some(timestamp)\n      )\n    } else {\n      // Otherwise we manually modify the file modification time.\n      setTimestampToCommitFileAtVersion(deltaLog, version, timestamp)\n    }\n  }\n\n  test(\"path based table\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getAbsolutePath\n      val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\")\n      val df2 = Seq(6, 7).toDF(\"id\")\n      val df3 = Seq(8, 9, 10).toDF(\"id\")\n\n      // write version 0 of the table\n      df1.write.format(\"delta\").save(path) // version 0\n\n      val deltaLog = DeltaLog.forTable(spark, path)\n      require(deltaLog.snapshot.version == 0)\n\n      // append df2 to the table\n      df2.write.format(\"delta\").mode(\"append\").save(path) // version 1\n\n      // append df3 to the table\n      df3.write.format(\"delta\").mode(\"append\").save(path) // version 2\n\n      // check if the table has all the three dataframes written\n      checkAnswer(spark.read.format(\"delta\").load(path), df1.union(df2).union(df3))\n\n      // restore by version to version 1\n      restoreTableToVersion(path, 1, false)\n      checkAnswer(spark.read.format(\"delta\").load(path), df1.union(df2))\n\n      // Set a custom timestamp for the commit\n      val desiredDate = new Date(System.currentTimeMillis() - 5.days.toMillis)\n      overrideTimestampOfCommitFile(\n        deltaLog,\n        version = 0,\n        timestamp = dateStringToTimestamp(date = desiredDate.toString)\n      )\n\n      // restore by timestamp to version 0\n      restoreTableToTimestamp(path, desiredDate.toString, false)\n      checkAnswer(spark.read.format(\"delta\").load(path), df1)\n    }\n  }\n\n  protected def dateStringToTimestamp(date: String): Long = {\n    val format = new java.text.SimpleDateFormat(\"yyyy-MM-dd\")\n    format.parse(date).getTime\n  }\n\n  protected def timeStringToTimestamp(time: String): Long = {\n    val format = new java.text.SimpleDateFormat(\"yyyy-MM-dd hh:mm:ss Z\")\n    format.parse(time).getTime\n  }\n\n  protected def setTimestampToCommitFileAtVersion(\n      deltaLog: DeltaLog,\n      version: Int,\n      date: String): Unit = {\n    val timestamp = dateStringToTimestamp(date)\n    setTimestampToCommitFileAtVersion(deltaLog, version, timestamp)\n  }\n\n  protected def setTimestampToCommitFileAtVersion(\n      deltaLog: DeltaLog,\n      version: Int,\n      timestamp: Long): Unit = {\n    val file = new File(FileNames.unsafeDeltaFile(deltaLog.logPath, version).toUri)\n    file.setLastModified(timestamp)\n  }\n\n  test(\"metastore based table\") {\n    val identifier = \"tbl\"\n    withTable(identifier) {\n\n      val df1 = Seq(1, 2, 3, 4, 5).toDF(\"id\")\n      val df2 = Seq(6, 7).toDF(\"id\")\n\n      // write first version of the table\n      df1.write.format(\"delta\").saveAsTable(identifier) // version 0\n\n      val deltaLog = DeltaLog.forTable(spark, new TableIdentifier(identifier))\n      require(deltaLog.snapshot.version == 0)\n\n      // append df2 to the table\n      df2.write.format(\"delta\").mode(\"append\").saveAsTable(identifier) // version  1\n\n      // check if the table has all the three dataframes written\n      checkAnswer(spark.read.format(\"delta\").table(identifier), df1.union(df2))\n\n\n      // restore by version to version 0\n      restoreTableToVersion(identifier, 0, true)\n      checkAnswer(spark.read.format(\"delta\").table(identifier), df1)\n    }\n  }\n\n  test(\"restore a restore back to pre-restore version\") {\n    withTempDir { tempDir =>\n      val df1 = Seq(1, 2, 3).toDF(\"id\")\n      val df2 = Seq(4, 5, 6).toDF(\"id\")\n      val df3 = Seq(7, 8, 9).toDF(\"id\")\n      df1.write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      require(deltaLog.snapshot.version == 0)\n\n      df2.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n      assert(deltaLog.update().version == 1)\n\n      df3.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n      assert(deltaLog.update().version == 2)\n\n      // we have three versions now, let's restore to version 1 first\n      restoreTableToVersion(tempDir.getAbsolutePath, 1, false)\n\n      checkAnswer(spark.read.format(\"delta\").load(tempDir.getAbsolutePath), df1.union(df2))\n      assert(deltaLog.update().version == 3)\n\n      restoreTableToVersion(tempDir.getAbsolutePath, 2, false)\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir.getAbsolutePath), df1.union(df2).union(df3))\n\n      assert(deltaLog.update().version == 4)\n    }\n  }\n\n  test(\"restore to a restored version\") {\n    withTempDir { tempDir =>\n      val df1 = Seq(1, 2, 3).toDF(\"id\")\n      val df2 = Seq(4, 5, 6).toDF(\"id\")\n      val df3 = Seq(7, 8, 9).toDF(\"id\")\n      df1.write.format(\"delta\").save(tempDir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n      require(deltaLog.update().version == 0)\n\n      df2.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n      assert(deltaLog.update().version == 1)\n\n      // we have two versions now, let's restore to version 0 first\n      restoreTableToVersion(tempDir.getAbsolutePath, 0, false)\n\n      checkAnswer(spark.read.format(\"delta\").load(tempDir.getAbsolutePath), df1)\n      assert(deltaLog.update().version == 2)\n\n      df3.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n      assert(deltaLog.update().version == 3)\n\n      // now we restore a restored version\n      restoreTableToVersion(tempDir.getAbsolutePath, 2, false)\n      checkAnswer(spark.read.format(\"delta\").load(tempDir.getAbsolutePath), df1)\n      assert(deltaLog.update().version == 4)\n    }\n  }\n\n  for (downgradeAllowed <- DeltaTestUtils.BOOLEAN_DOMAIN)\n  test(s\"restore downgrade protocol (allowed=$downgradeAllowed)\") {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"CatalogOwned is not compatible w/ the test since protocol version would be \" +\n        \"set to (3, 7) by default for CC tables.\")\n    }\n    withTempDir { tempDir =>\n      val path = tempDir.getAbsolutePath\n      spark.range(5).write.format(\"delta\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val oldProtocolVersion = deltaLog.snapshot.protocol\n      // Update table to latest version.\n      deltaLog.upgradeProtocol(\n        oldProtocolVersion.merge(Protocol().withFeature(TestReaderWriterFeature)))\n      val newProtocolVersion = deltaLog.snapshot.protocol\n      assert(newProtocolVersion.minReaderVersion > oldProtocolVersion.minReaderVersion &&\n        newProtocolVersion.minWriterVersion > oldProtocolVersion.minWriterVersion,\n        s\"newProtocolVersion=$newProtocolVersion is not strictly greater than\" +\n          s\" oldProtocolVersion=$oldProtocolVersion\")\n\n      withSQLConf(DeltaSQLConf.RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED.key ->\n          downgradeAllowed.toString) {\n        // Restore to before the upgrade.\n        restoreTableToVersion(path, version = 0, isMetastoreTable = false)\n      }\n      val restoredProtocolVersion = deltaLog.snapshot.protocol\n      if (downgradeAllowed) {\n        assert(restoredProtocolVersion === oldProtocolVersion)\n      } else {\n        assert(restoredProtocolVersion === newProtocolVersion.merge(oldProtocolVersion))\n      }\n    }\n  }\n\n  for (downgradeAllowed <- DeltaTestUtils.BOOLEAN_DOMAIN)\n    test(\n      s\"restore downgrade protocol with table features (allowed=$downgradeAllowed)\") {\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        cancel(\"CatalogOwned is not compatible w/ the test since protocol version would be \" +\n          \"set to (3, 7) by default for CC tables.\")\n      }\n      withTempDir { tempDir =>\n        val path = tempDir.getAbsolutePath\n        spark.range(5).write.format(\"delta\").save(path)\n        val deltaLog = DeltaLog.forTable(spark, path)\n        val oldProtocolVersion = deltaLog.snapshot.protocol\n        // Update table to latest version.\n        deltaLog.upgradeProtocol(\n          Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n            .withFeatures(Seq(TestLegacyReaderWriterFeature))\n            .withFeatures(oldProtocolVersion.implicitlyAndExplicitlySupportedFeatures))\n        val newProtocolVersion = deltaLog.snapshot.protocol\n        assert(\n          newProtocolVersion.minReaderVersion > oldProtocolVersion.minReaderVersion &&\n            newProtocolVersion.minWriterVersion >= oldProtocolVersion.minWriterVersion,\n          s\"newProtocolVersion=$newProtocolVersion is not strictly greater than\" +\n            s\" oldProtocolVersion=$oldProtocolVersion\")\n\n        withSQLConf(\n          DeltaSQLConf.RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED.key ->\n            downgradeAllowed.toString) {\n          // Restore to before the upgrade.\n          restoreTableToVersion(path, version = 0, isMetastoreTable = false)\n        }\n        val restoredProtocolVersion = deltaLog.snapshot.protocol\n        if (downgradeAllowed) {\n          assert(restoredProtocolVersion === oldProtocolVersion)\n        } else {\n          assert(restoredProtocolVersion ===\n            newProtocolVersion.merge(oldProtocolVersion))\n        }\n      }\n    }\n\n  test(\"RESTORE doesn't account for session defaults\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"1\") {\n      if (catalogOwnedDefaultCreationEnabledInTests) {\n        cancel(\"CatalogOwned is not compatible w/ the test since protocol version would be \" +\n          \"set to (3, 7) by default for CC tables.\")\n      }\n      withTempDir { dir =>\n        spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n        spark\n          .range(start = 10, end = 20)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        val oldProtocol = log.update().protocol\n        assert(oldProtocol === Protocol(1, 1))\n        withSQLConf(\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"2\",\n          DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"2\",\n          TableFeatureProtocolUtils.defaultPropertyKey(TestWriterFeature) -> \"enabled\") {\n          restoreTableToVersion(dir.getAbsolutePath, 0, isMetastoreTable = false)\n        }\n        val newProtocol = log.update().protocol\n        assert(newProtocol === oldProtocol)\n      }\n    }\n  }\n\n  test(\"restore operation metrics in Delta table history\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        val df1 = Seq(1, 2, 3).toDF(\"id\")\n        val df2 = Seq(4, 5, 6).toDF(\"id\")\n        df1.write.format(\"delta\").save(tempDir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n        df2.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        assert(deltaLog.update().version == 1)\n\n        // we have two versions now, let's restore to version 0 first\n        restoreTableToVersion(tempDir.getAbsolutePath, 0, false)\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n\n        val actualOperationMetrics = deltaTable.history(1).select(\"operationMetrics\")\n            .take(1)\n            .head\n            .getMap(0)\n            .asInstanceOf[Map[String, String]]\n\n        // File sizes are flaky due to differences in order of data (=> encoding size differences)\n        assert(actualOperationMetrics.get(\"tableSizeAfterRestore\").isDefined)\n        assert(actualOperationMetrics.get(\"numOfFilesAfterRestore\").get == \"2\")\n        assert(actualOperationMetrics.get(\"numRemovedFiles\").get == \"2\")\n        assert(actualOperationMetrics.get(\"numRestoredFiles\").get == \"0\")\n        // File sizes are flaky due to differences in order of data (=> encoding size differences)\n        assert(actualOperationMetrics.get(\"removedFilesSize\").isDefined)\n        assert(actualOperationMetrics.get(\"restoredFilesSize\").get == \"0\")\n      }\n    }\n  }\n\n  test(\"restore command output metrics\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        val df1 = Seq(1, 2, 3).toDF(\"id\")\n        val df2 = Seq(4, 5, 6).toDF(\"id\")\n        df1.write.format(\"delta\").save(tempDir.getAbsolutePath)\n        val deltaLog = DeltaLog.forTable(spark, tempDir.getAbsolutePath)\n        df2.write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        assert(deltaLog.update().version == 1)\n\n        // we have two versions now, let's restore to version 0 first\n        val actualOutputMetrics = restoreTableToVersion(tempDir.getAbsolutePath, 0, false)\n\n        // verify the schema\n        val expectedRestoreOutputSchema = StructType(Seq(\n          StructField(\"table_size_after_restore\", LongType),\n          StructField(\"num_of_files_after_restore\", LongType),\n          StructField(\"num_removed_files\", LongType),\n          StructField(\"num_restored_files\", LongType),\n          StructField(\"removed_files_size\", LongType),\n          StructField(\"restored_files_size\", LongType)\n        ))\n        assert(actualOutputMetrics.schema == expectedRestoreOutputSchema)\n\n        val outputRow = actualOutputMetrics.take(1).head\n        // File sizes are flaky due to differences in order of data (=> encoding size differences)\n        assert(outputRow.getLong(0) > 0L) // table_size_after_restore\n        assert(outputRow.getLong(1) == 2L) // num_of_files_after_restore\n        assert(outputRow.getLong(2) == 2L) // num_removed_files\n        assert(outputRow.getLong(3) == 0L) // num_restored_files\n        // File sizes are flaky due to differences in order of data (=> encoding size differences)\n        assert(outputRow.getLong(4) > 0L) // removed_files_size\n        assert(outputRow.getLong(5) == 0L) // restored_files_size\n      }\n    }\n  }\n\n  test(\"cdf + RESTORE\") {\n    withSQLConf(\n        DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> \"true\") {\n      withTempDir { tempDir =>\n        val df0 = Seq(0, 1).toDF(\"id\") // version 0 = [0, 1]\n        df0.write.format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val df1 = Seq(2).toDF(\"id\") // version 1: append to df0 = [0, 1, 2]\n        df1.write.mode(\"append\").format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n        deltaTable.delete(\"id < 1\") // version 2: delete (0) = [1, 2]\n\n        deltaTable.updateExpr(\n          \"id > 1\",\n          Map(\"id\" -> \"4\")\n        ) // version 3: update 2 --> 4 = [1, 4]\n\n        // version 4: restore to version 2 (delete 4, insert 2) = [1, 2]\n        restoreTableToVersion(tempDir.getAbsolutePath, 2, false)\n        checkAnswer(\n          CDCReader.changesToBatchDF(DeltaLog.forTable(spark, tempDir), 4, 4, spark)\n            .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n          Row(4, \"delete\", 4) :: Row(2, \"insert\", 4) :: Nil\n        )\n\n        // version 5: restore to version 1 (insert 0) = [0, 1, 2]\n        restoreTableToVersion(tempDir.getAbsolutePath, 1, false)\n        checkAnswer(\n          CDCReader.changesToBatchDF(DeltaLog.forTable(spark, tempDir), 5, 5, spark)\n            .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n          Row(0, \"insert\", 5) :: Nil\n        )\n\n        // version 6: restore to version 0 (delete 2) = [0, 1]\n        restoreTableToVersion(tempDir.getAbsolutePath, 0, false)\n        checkAnswer(\n          CDCReader.changesToBatchDF(DeltaLog.forTable(spark, tempDir), 6, 6, spark)\n            .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n          Row(2, \"delete\", 6) :: Nil\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/S3LikeLocalFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.net.URI\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.RawLocalFileSystem\n\n/**\n * A local filesystem on scheme s3. Useful for testing paths on non-default schemes.\n */\nclass S3LikeLocalFileSystem extends RawLocalFileSystem {\n  private var uri: URI = _\n  override def getScheme: String = \"s3\"\n\n  override def initialize(name: URI, conf: Configuration): Unit = {\n    uri = URI.create(name.getScheme + \":///\")\n    super.initialize(name, conf)\n  }\n\n  override def getUri(): URI = if (uri == null) {\n    // RawLocalFileSystem's constructor will call this one before `initialize` is called.\n    // Just return the super's URI to avoid NPE.\n    super.getUri\n  } else {\n    uri\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/S3SingleDriverLogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.storage.{HDFSLogStore, LogStore, S3SingleDriverLogStore}\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\ntrait S3SingleDriverLogStoreSuiteBase extends LogStoreSuiteBase {\n\n  private def checkLogStoreList(\n      store: LogStore,\n      path: Path,\n      expectedVersions: Seq[Int],\n      hadoopConf: Configuration): Unit = {\n    assert(store.listFrom(path, hadoopConf).map(FileNames.deltaVersion).toSeq === expectedVersions)\n  }\n\n  private def checkFileSystemList(fs: FileSystem, path: Path, expectedVersions: Seq[Int]): Unit = {\n    val fsList = fs.listStatus(path.getParent).filter(_.getPath.getName >= path.getName)\n    assert(fsList.map(FileNames.deltaVersion).sorted === expectedVersions)\n  }\n\n  testHadoopConf(\n    \".*No FileSystem for scheme.*fake.*\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\")\n\n  test(\"file system has priority over cache\") {\n    withTempDir { dir =>\n      val store = createLogStore(spark)\n      val deltas = Seq(0, 1, 2).map(i => FileNames.unsafeDeltaFile(new Path(dir.toURI), i))\n      store.write(deltas(0), Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n      store.write(deltas(1), Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n      store.write(deltas(2), Iterator(\"two\"), overwrite = false, sessionHadoopConf)\n\n      // delete delta file 2 and its checksum from file system\n      val fs = new Path(dir.getCanonicalPath).getFileSystem(sessionHadoopConf)\n      val delta2CRC = FileNames.checksumFile(new Path(dir.toURI), 2)\n      fs.delete(deltas(2), true)\n      fs.delete(delta2CRC, true)\n\n      // magically create a different version of file 2 in the FileSystem only\n      val hackyStore = new HDFSLogStore(sparkConf, sessionHadoopConf)\n      hackyStore.write(deltas(2), Iterator(\"foo\"), overwrite = true, sessionHadoopConf)\n\n      // we should see \"foo\" (FileSystem value) instead of \"two\" (cache value)\n      assert(store.read(deltas(2), sessionHadoopConf).head == \"foo\")\n    }\n  }\n\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = false\n\n  /**\n   * S3SingleDriverLogStore.scala can invalidate cache\n   * S3SingleDriverLogStore.java cannot invalidate cache\n   */\n  protected def canInvalidateCache: Boolean\n}\n\nclass S3SingleDriverLogStoreSuite extends S3SingleDriverLogStoreSuiteBase {\n  override val logStoreClassName: String = classOf[S3SingleDriverLogStore].getName\n\n  override protected def canInvalidateCache: Boolean = true\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/SchemaValidationSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.util.concurrent.CountDownLatch\n\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * This Suite tests the behavior of Delta commands when a schema altering commit is run after the\n * command completes analysis but before the command starts the transaction. We want to make sure\n * That we do not corrupt tables.\n */\nclass SchemaValidationSuite\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  class BlockingRule(\n      blockActionLatch: CountDownLatch,\n      startConcurrentUpdateLatch: CountDownLatch) extends Rule[LogicalPlan] {\n    override def apply(plan: LogicalPlan): LogicalPlan = {\n      startConcurrentUpdateLatch.countDown()\n      blockActionLatch.await()\n      plan\n    }\n  }\n\n  /**\n   * Blocks the thread with the help of an optimizer rule until end of scope.\n   * We need two latches to ensure that the thread executing the query is blocked until\n   * the other thread concurrently updates the metadata. `blockActionLatch` blocks the action\n   * until it is counted down by the thread updating the metadata. `startConcurrentUpdateLatch`\n   * will block the concurrent update to happen until it is counted down by the action reaches the\n   * optimizer rule.\n  */\n  private def withBlockedExecution(\n      t: Thread,\n      blockActionLatch: CountDownLatch,\n      startConcurrentUpdateLatch: CountDownLatch)(f: => Unit): Unit = {\n    t.start()\n    startConcurrentUpdateLatch.await()\n    try {\n      f\n    } finally {\n      blockActionLatch.countDown()\n      t.join()\n    }\n  }\n\n  def cloneSession(spark: SparkSession): SparkSession = spark.cloneSession()\n\n  /**\n   * Common base method for both the path based and table name based tests.\n   */\n  private def testConcurrentChangeBase(identifier: String)(\n      createTable: (SparkSession, String) => Unit,\n      actionToTest: (SparkSession, String) => Unit,\n      concurrentChange: (SparkSession, String) => Unit): Unit = {\n    createTable(spark, identifier)\n\n    // Clone the session to run the query in a separate thread.\n    val newSession = cloneSession(spark)\n    val blockActionLatch = new CountDownLatch(1)\n    val startConcurrentUpdateLatch = new CountDownLatch(1)\n    val rule = new BlockingRule(blockActionLatch, startConcurrentUpdateLatch)\n    newSession.experimental.extraOptimizations :+= rule\n\n    var actionException: Exception = null\n    val actionToTestThread = new Thread() {\n      override def run(): Unit = {\n        try {\n          actionToTest(newSession, identifier)\n        } catch {\n          case e: Exception =>\n            actionException = e\n        }\n      }\n    }\n    withBlockedExecution(actionToTestThread, blockActionLatch, startConcurrentUpdateLatch) {\n      concurrentChange(spark, identifier)\n    }\n    if (actionException != null) {\n      throw actionException\n    }\n }\n\n  /**\n   * tests the behavior of concurrent changes to schema on a blocked command.\n   * @param testName - name of the test\n   * @param createTable - method that creates a table given an identifier and spark session.\n   * @param actionToTest - the method we want to test.\n   * @param concurrentChange - the concurrent query that updates the schema of the table\n   *\n   * All the above methods take SparkSession and the table path as parameters\n   */\n  def testConcurrentChange(testName: String, testTags: org.scalatest.Tag*)(\n    createTable: (SparkSession, String) => Unit,\n    actionToTest: (SparkSession, String) => Unit,\n    concurrentChange: (SparkSession, String) => Unit): Unit = {\n\n    test(testName, testTags: _*) {\n      withTempDir { tempDir =>\n        testConcurrentChangeBase(tempDir.getCanonicalPath)(\n          createTable,\n          actionToTest,\n          concurrentChange\n        )\n      }\n    }\n  }\n\n  /**\n   * tests the behavior of concurrent changes pf schema on a blocked command with metastore tables.\n   * @param testName - name of the test\n   * @param createTable - method that creates a table given an identifier and spark session.\n   * @param actionToTest - the method we want to test.\n   * @param concurrentChange - the concurrent query that updates the schema of the table\n   *\n   * All the above methods take SparkSession and the table name as parameters\n   */\n  def testConcurrentChangeWithTable(testName: String)(\n    createTable: (SparkSession, String) => Unit,\n    actionToTest: (SparkSession, String) => Unit,\n    concurrentChange: (SparkSession, String) => Unit): Unit = {\n\n    val tblName = \"metastoreTable\"\n    test(testName) {\n      withTable(tblName) {\n        testConcurrentChangeBase(tblName)(\n          createTable,\n          actionToTest,\n          concurrentChange\n        )\n      }\n    }\n  }\n\n  /**\n   * Creates a method to remove a column from the table by taking column as an argument.\n   */\n  def dropColFromSampleTable(col: String): (SparkSession, String) => Unit = {\n    (spark: SparkSession, tblPath: String) => {\n      spark.read.format(\"delta\").load(tblPath)\n        .drop(col)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .option(\"overwriteSchema\", \"true\")\n        .save(tblPath)\n    }\n  }\n\n  /**\n   * Adding a column to the schema will result in the blocked thread appending to the table\n   * with null values for the new column.\n   */\n  testConcurrentChange(\"write - add a column concurrently\")(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).write.format(\"delta\").save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      spark.range(11, 20).write.format(\"delta\")\n        .mode(\"append\")\n        .save(tblPath)\n\n      val appendedCol2Values = spark.read.format(\"delta\")\n        .load(tblPath)\n        .filter(col(\"id\") <= 20)\n        .select(\"col2\")\n        .distinct()\n        .collect()\n        .toList\n      assert(appendedCol2Values == List(Row(null)))\n    },\n    concurrentChange = (spark: SparkSession, tblPath: String) => {\n      spark.range(21, 30).withColumn(\"col2\", lit(2)).write\n        .format(\"delta\")\n        .mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .save(tblPath)\n    }\n  )\n\n  /**\n   * Removing a column while a query is in running should throw an analysis\n   * exception\n   */\n  testConcurrentChange(\"write - remove a column concurrently\")(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val e = intercept[AnalysisException] {\n        spark.range(11, 20)\n          .withColumn(\"col2\", lit(1)).write.format(\"delta\")\n          .mode(\"append\")\n          .save(tblPath)\n      }\n      assert(e.getMessage.contains(\n        \"A schema mismatch detected when writing to the Delta table\"))\n    },\n    concurrentChange = dropColFromSampleTable(\"col2\")\n  )\n\n  /**\n   * Removing a column while performing a delete should be caught while\n   * writing the deleted files(i.e files with rows that were not deleted).\n   */\n  testConcurrentChange(\"delete - remove a column concurrently\")(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tblPath)\n      val e = intercept[Exception] {\n        deltaTable.delete(col(\"id\") === 1)\n      }\n      assert(e.getMessage.contains(s\"Can't resolve column col2\"))\n    },\n    concurrentChange = dropColFromSampleTable(\"col2\")\n  )\n\n  /**\n   * Removing a column(referenced in condition) while performing a delete will\n   * result in a no-op.\n   */\n  testConcurrentChange(\"test delete query against a concurrent query which removes the\" +\n    \" delete condition column\"\n  )(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .repartition(2)\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tblPath)\n      deltaTable.delete(col(\"id\") === 1)\n      // check if delete is no-op\n      checkAnswer(\n        sql(s\"SELECT * FROM delta.`$tblPath`\"),\n        Seq(Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1)))\n    },\n    concurrentChange = dropColFromSampleTable(\"id\")\n  )\n\n  /**\n   * An update command that has to rewrite files will have the old schema,\n   * we catch the outdated schema during the write.\n   */\n  testConcurrentChange(\"update - remove a column concurrently\")(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tblPath)\n      val e = intercept[AnalysisException] {\n        deltaTable.update(col(\"id\") =!= 1, Map(\"col2\" -> lit(-1)))\n      }\n      assert(e.getMessage.contains(s\"Can't resolve column col2\"))\n    },\n    concurrentChange = dropColFromSampleTable(\"col2\")\n  )\n\n  /**\n   * Removing a column(referenced in condition) while performing a update will\n   * result in a no-op.\n   */\n    testConcurrentChange(\"test update query against a concurrent query which removes the\" +\n      \" update condition column\"\n    )(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .repartition(2)\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tblPath)\n      deltaTable.update(col(\"id\") === 1, Map(\"id\" -> lit(\"2\")))\n      // check if update is no-op\n      checkAnswer(\n        sql(s\"SELECT * FROM delta.`$tblPath`\"),\n        Seq(Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1), Row(1)))\n    },\n    concurrentChange = dropColFromSampleTable(\"id\")\n  )\n\n  /**\n   * Concurrently drop column in merge condition. Merge command detects the schema change while\n   * resolving the target and throws a DeltaAnalysisException\n   */\n  testConcurrentChange(\"merge - remove a column in merge condition concurrently\")(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tblPath)\n      val sourceDf = spark.range(10).withColumn(\"col2\", lit(2))\n      val e = intercept[DeltaAnalysisException] {\n        deltaTable.as(\"t1\")\n          .merge(sourceDf.as(\"t2\"), \"t1.id == t2.id\")\n          .whenNotMatched()\n          .insertAll()\n          .whenMatched()\n          .updateAll()\n          .execute()\n      }\n\n      checkErrorMatchPVals(\n        e,\n        \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\",\n        parameters = Map(\n          \"schemaDiff\" -> \".*id.*\",\n          \"legacyFlagMessage\" -> \"\"\n        )\n      )\n    },\n    concurrentChange = dropColFromSampleTable(\"id\")\n  )\n\n  /**\n   * Concurrently drop column not in merge condition but in target. Merge command detects the schema\n   * change while resolving the target and throws a DeltaAnalysisException\n   */\n  testConcurrentChange(\"merge - remove a column not in merge condition concurrently\")(\n    createTable = (spark: SparkSession, tblPath: String) => {\n      spark.range(10).withColumn(\"col2\", lit(1))\n        .write\n        .format(\"delta\")\n        .save(tblPath)\n    },\n    actionToTest = (spark: SparkSession, tblPath: String) => {\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tblPath)\n      val sourceDf = spark.range(10).withColumn(\"col2\", lit(2))\n      val e = intercept[DeltaAnalysisException] {\n        deltaTable.as(\"t1\")\n          .merge(sourceDf.as(\"t2\"), \"t1.id == t2.id\")\n          .whenNotMatched()\n          .insertAll()\n          .whenMatched()\n          .updateAll()\n          .execute()\n      }\n      checkErrorMatchPVals(\n        e,\n        \"DELTA_SCHEMA_CHANGE_SINCE_ANALYSIS\",\n        parameters = Map(\n          \"schemaDiff\" -> \".*col2.*\",\n          \"legacyFlagMessage\" -> \"\"\n        )\n      )\n    },\n    concurrentChange = dropColFromSampleTable(\"col2\")\n  )\n\n  /**\n   * Alter table to add a column and at the same time add a column concurrently.\n   */\n  testConcurrentChangeWithTable(\"alter table add column - remove column and add same column\")(\n    createTable = (spark: SparkSession, tblName: String) => {\n      spark.range(10).write.format(\"delta\").saveAsTable(tblName)\n    },\n    actionToTest = (spark: SparkSession, tblName: String) => {\n      val e = intercept[AnalysisException] {\n        spark.sql(s\"ALTER TABLE `$tblName` ADD COLUMNS (col2 string)\")\n      }\n      assert(e.getMessage.contains(\"Found duplicate column(s) in adding columns: col2\"))\n    },\n    concurrentChange = (spark: SparkSession, tblName: String) => {\n      spark.read.format(\"delta\").table(tblName)\n        .withColumn(\"col2\", lit(1))\n        .write\n        .format(\"delta\")\n        .option(\"overwriteSchema\", \"true\")\n        .mode(\"overwrite\")\n        .saveAsTable(tblName)\n    }\n  )\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/ShowDeltaTableColumnsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions.struct\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\nclass ShowDeltaTableColumnsSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaTestUtilsForTempViews {\n\n  import testImplicits._\n\n  private val outputColumnNames = Seq(\"col_name\")\n  private val outputColumnValues = Seq(Seq(\"column1\"), Seq(\"column2\"))\n\n  protected def checkResult(\n      result: DataFrame,\n      expected: Seq[Seq[Any]],\n      columns: Seq[String]): Unit = {\n    checkAnswer(\n      result.select(columns.head, columns.tail: _*),\n      expected.map { x => Row(x: _*)})\n    assert(result.columns.toSeq == outputColumnNames)\n  }\n\n  private def showDeltaColumnsTest(\n      fileToTableNameMapper: File => String,\n      schemaName: Option[String] = None): Unit = {\n    withDatabase(\"delta\") {\n      val tempDir = Utils.createTempDir()\n      Seq(1 -> 1)\n        .toDF(\"column1\", \"column2\")\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tempDir.toString)\n\n      val finalSchema = if (schemaName.nonEmpty) s\"FROM ${schemaName.get}\" else \"\"\n      checkResult(sql(s\"SHOW COLUMNS IN ${fileToTableNameMapper(tempDir)} $finalSchema\"),\n        outputColumnValues,\n        outputColumnNames)\n    }\n  }\n\n  test(\"delta table: table identifier\") {\n    showDeltaColumnsTest(f => s\"delta.`${f.toString}`\")\n  }\n\n  test(\"delta table: table name with separated schema name\") {\n    showDeltaColumnsTest(f => s\"`${f.toString}`\", schemaName = Some(\"delta\"))\n  }\n\n  test(\"non-delta table: table identifier with catalog table\") {\n    // Non-Delta table represent by catalog identifier (e.g.: sales.line_ite) is supported in\n    // SHOW COLUMNS command.\n    withTable(\"show_columns\") {\n      sql(s\"\"\"\n             |CREATE TABLE show_columns(column1 INT, column2 INT)\n             |USING parquet\n             |COMMENT \"describe a non delta table\"\n      \"\"\".stripMargin)\n      checkResult(sql(\"SHOW COLUMNS IN show_columns\"), outputColumnValues, outputColumnNames)\n    }\n  }\n\n  test(\"delta table: table name not found\") {\n    val fakeTableName = s\"test_table\"\n    val schemaName = s\"delta\"\n    showDeltaColumnsTest(f => s\"$schemaName.`${f.toString}`\")\n    val e = intercept[AnalysisException] {\n      sql(s\"SHOW COLUMNS IN `$fakeTableName` IN $schemaName\")\n    }\n    assert(e.getMessage().contains(s\"Table or view not found: $schemaName.$fakeTableName\") ||\n      e.getMessage().contains(s\"table or view `$schemaName`.`$fakeTableName` cannot be found\"))\n  }\n\n  test(\"delta table: check duplicated schema name\") {\n    // When `schemaName` and `tableIdentity.database` both exists, we will throw error if they are\n    // not the same.\n    val schemaName = s\"default\"\n    val tableName = s\"test_table\"\n    val fakeSchemaName = s\"epsilon\"\n    withTable(tableName) {\n      sql(s\"\"\"\n             |CREATE TABLE $tableName(column1 INT, column2 INT)\n             |USING delta\n        \"\"\".stripMargin)\n\n      // when no schema name provided, default schema name is `default`.\n      checkResult(\n        sql(s\"SHOW COLUMNS IN $tableName\"),\n        outputColumnValues,\n        outputColumnNames)\n      checkResult(\n        sql(s\"SHOW COLUMNS IN $schemaName.$tableName\"),\n        outputColumnValues,\n        outputColumnNames)\n\n      var e = intercept[AnalysisException] {\n        sql(s\"SHOW COLUMNS IN $tableName IN $fakeSchemaName\")\n      }\n      assert(e\n        .getMessage()\n        .contains(s\"Table or view not found: $fakeSchemaName.$tableName\") ||\n        e.getMessage()\n          .contains(s\"table or view `$fakeSchemaName`.`$tableName` cannot be found\"))\n\n      e = intercept[AnalysisException] {\n        sql(s\"SHOW COLUMNS IN $fakeSchemaName.$tableName IN $schemaName\")\n      }\n      assert(e\n        .getMessage()\n        .contains(s\"Table or view not found: $fakeSchemaName.$tableName\") ||\n        e.getMessage()\n          .contains(s\"table or view `$fakeSchemaName`.`$tableName` cannot be found\"))\n\n      checkShowColumns(fakeSchemaName, schemaName, intercept[AnalysisException] {\n        sql(s\"SHOW COLUMNS IN $schemaName.$tableName IN $fakeSchemaName\")\n      })\n    }\n  }\n\n  testWithTempView(s\"show columns on temp view should fallback to Spark\") { isSQLTempView =>\n    val tableName = \"test_table_2\"\n    withTable(tableName) {\n      Seq(1 -> 1)\n        .toDF(\"column1\", \"column2\")\n        .write\n        .format(\"delta\")\n        .saveAsTable(tableName)\n      val viewName = \"v\"\n      createTempViewFromTable(tableName, isSQLTempView)\n      checkResult(sql(s\"SHOW COLUMNS IN $viewName\"), outputColumnValues, outputColumnNames)\n    }\n  }\n\n  test(s\"delta table: show columns on a nested column\") {\n    withTempDir { tempDir =>\n      (70.to(79).seq ++ 75.to(79).seq)\n        .toDF(\"id\")\n        .withColumn(\"nested\", struct(struct('id + 2 as \"b\", 'id + 3 as \"c\") as \"sub\"))\n        .write\n        .format(\"delta\")\n        .save(tempDir.toString)\n      checkResult(\n        sql(s\"SHOW COLUMNS IN delta.`${tempDir.toString}`\"),\n        Seq(Seq(\"id\"), Seq(\"nested\")),\n        outputColumnNames)\n    }\n  }\n\n  test(\"delta table: respect the Spark configuration on whether schema name is case sensitive\") {\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n      checkShowColumns(\"DELTA\", \"delta\", intercept[AnalysisException] {\n        showDeltaColumnsTest(f => s\"delta.`${f.toString}`\", schemaName = Some(\"DELTA\"))\n      })\n\n      checkShowColumns(\"delta\", \"DELTA\", intercept[AnalysisException] {\n        showDeltaColumnsTest(f => s\"DELTA.`${f.toString}`\", schemaName = Some(\"delta\"))\n      })\n    }\n\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      showDeltaColumnsTest(f => s\"delta.`${f.toString}`\", schemaName = Some(\"DELTA\"))\n      showDeltaColumnsTest(f => s\"DELTA.`${f.toString}`\", schemaName = Some(\"delta\"))\n    }\n  }\n\n  private def checkShowColumns(schema1: String, schema2: String, e: AnalysisException): Unit = {\n    val expectedMessage = Seq(\n      s\"SHOW COLUMNS with conflicting databases: '$schema1' != '$schema2'\",  // SPARK-3.5\n      s\"SHOW COLUMNS with conflicting namespaces: `$schema1` != `$schema2`\") // SPARK-4.0\n    assert(expectedMessage.exists(e.getMessage().contains))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/SnapshotManagementSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.{File, RandomAccessFile}\nimport java.util.concurrent.CountDownLatch\n\nimport scala.collection.mutable\n\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME\nimport org.apache.spark.sql.delta.DeltaTestUtils.{verifyBackfilled, verifyUnbackfilled, BOOLEAN_DOMAIN}\nimport org.apache.spark.sql.delta.coordinatedcommits.{CommitCoordinatorBuilder, CommitCoordinatorProvider, CoordinatedCommitsBaseSuite, CoordinatedCommitsUsageLogs, InMemoryCommitCoordinator}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LocalLogStore\nimport org.apache.spark.sql.delta.storage.LogStore.logStoreClassConfKey\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, FileNames, JsonUtils}\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{Commit, CommitCoordinatorClient, GetCommitsResponse, TableDescriptor}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.storage.StorageLevel\n\nclass SnapshotManagementSuite extends QueryTest with DeltaSQLTestUtils with SharedSparkSession\n  with DeltaSQLCommandTest with CoordinatedCommitsBaseSuite {\n\n  protected override def sparkConf = {\n    // Disable loading protocol and metadata from checksum file. Otherwise, creating a Snapshot\n    // won't touch the checkpoint file and we won't be able to retry.\n    super.sparkConf\n      .set(DeltaSQLConf.USE_PROTOCOL_AND_METADATA_FROM_CHECKSUM_ENABLED.key, \"false\")\n  }\n\n  /**\n   * Truncate an existing checkpoint file to create a corrupt file.\n   *\n   * @param path the Delta table path\n   * @param checkpointVersion the checkpoint version to be updated\n   * @param shouldBeEmpty whether to create an empty checkpoint file\n   */\n  private def makeCorruptCheckpointFile(\n      path: String,\n      checkpointVersion: Long,\n      shouldBeEmpty: Boolean,\n      multipart: Option[(Int, Int)] = None): Unit = {\n    if (multipart.isDefined) {\n      val (part, totalParts) = multipart.get\n      val checkpointFile = FileNames.checkpointFileWithParts(new Path(path, \"_delta_log\"),\n        checkpointVersion, totalParts)(part - 1).toString\n      assert(new File(checkpointFile).exists)\n      val cp = new RandomAccessFile(checkpointFile, \"rw\")\n      cp.setLength(if (shouldBeEmpty) 0 else 10)\n      cp.close()\n    } else {\n      val checkpointFile =\n        FileNames.checkpointFileSingular(new Path(path, \"_delta_log\"), checkpointVersion).toString\n      assert(new File(checkpointFile).exists)\n      val cp = new RandomAccessFile(checkpointFile, \"rw\")\n      cp.setLength(if (shouldBeEmpty) 0 else 10)\n      cp.close()\n    }\n  }\n\n  private def deleteLogVersion(path: String, version: Long): Unit = {\n    val deltaFile = new File(\n      FileNames.unsafeDeltaFile(new Path(path, \"_delta_log\"), version).toString)\n    assert(deltaFile.exists(), s\"Could not find $deltaFile\")\n    assert(deltaFile.delete(), s\"Failed to delete $deltaFile\")\n  }\n\n  private def deleteCheckpointVersion(path: String, version: Long): Unit = {\n    val deltaFile = new File(\n      FileNames.checkpointFileSingular(new Path(path, \"_delta_log\"), version).toString)\n    assert(deltaFile.exists(), s\"Could not find $deltaFile\")\n    assert(deltaFile.delete(), s\"Failed to delete $deltaFile\")\n  }\n\n  private def testWithAndWithoutMultipartCheckpoint(name: String)(f: (Option[Int]) => Unit) = {\n    testQuietly(name) {\n      withSQLConf(DeltaSQLConf.DELTA_CHECKPOINT_PART_SIZE.key -> \"1\") {\n        f(Some(1))\n        f(Some(2))\n      }\n      f(None)\n    }\n  }\n\n  testWithAndWithoutMultipartCheckpoint(\"recover from a corrupt checkpoint: previous checkpoint \" +\n      \"doesn't exist\") { partToCorrupt =>\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(path)\n      var deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n\n      DeltaLog.clearCache()\n      deltaLog = DeltaLog.forTable(spark, path)\n      val checkpointParts = deltaLog.snapshot.logSegment.checkpointProvider.topLevelFiles.size\n      val multipart = partToCorrupt.map((_, checkpointParts))\n\n      // We have different code paths for empty and non-empty checkpoints\n      for (testEmptyCheckpoint <- Seq(true, false)) {\n        makeCorruptCheckpointFile(path, checkpointVersion = 0,\n          shouldBeEmpty = testEmptyCheckpoint, multipart = multipart)\n        DeltaLog.clearCache()\n        // Checkpoint 0 is corrupted. Verify that we can still create the snapshot using\n        // existing json files.\n        DeltaLog.forTable(spark, path).snapshot\n      }\n    }\n  }\n\n  testWithAndWithoutMultipartCheckpoint(\"recover from a corrupt checkpoint: previous checkpoint \" +\n      \"exists\") { partToCorrupt =>\n    withTempDir { tempDir =>\n      // Create checkpoint 0 and 1\n      val path = tempDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(path)\n      var deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      deltaLog.update()\n      deltaLog.checkpoint()\n\n      DeltaLog.clearCache()\n      deltaLog = DeltaLog.forTable(spark, path)\n      val checkpointParts = deltaLog.snapshot.logSegment.checkpointProvider.topLevelFiles.size\n      val multipart = partToCorrupt.map((_, checkpointParts))\n\n      // We have different code paths for empty and non-empty checkpoints\n      for (testEmptyCheckpoint <- Seq(true, false)) {\n        makeCorruptCheckpointFile(path, checkpointVersion = 1,\n          shouldBeEmpty = testEmptyCheckpoint, multipart = multipart)\n        // Checkpoint 1 is corrupted. Verify that we can still create the snapshot using\n        // checkpoint 0.\n        DeltaLog.clearCache()\n        DeltaLog.forTable(spark, path).snapshot\n      }\n    }\n  }\n\n  testWithAndWithoutMultipartCheckpoint(\"should not recover when the current checkpoint is \" +\n      \"broken but we don't have the entire history\") { partToCorrupt =>\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(path)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      DeltaLog.forTable(spark, path).checkpoint()\n      deleteLogVersion(path, version = 0)\n      DeltaLog.clearCache()\n\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val checkpointParts = deltaLog.snapshot.logSegment.checkpointProvider.topLevelFiles.size\n      val multipart = partToCorrupt.map((_, checkpointParts))\n\n      DeltaLog.clearCache()\n\n      // We have different code paths for empty and non-empty checkpoints, and also different\n      // code paths when listing with or without a checkpoint hint.\n      for (testEmptyCheckpoint <- Seq(true, false)) {\n        makeCorruptCheckpointFile(path, checkpointVersion = 1,\n          shouldBeEmpty = testEmptyCheckpoint, multipart = multipart)\n        // When finding a Delta log for the first time, we rely on _last_checkpoint hint\n        val e = intercept[Exception] { DeltaLog.forTable(spark, path).snapshot }\n        if (testEmptyCheckpoint) {\n          // - checkpoint 1 is NOT in the list result\n          // - try to get an alternative LogSegment in `getLogSegmentForVersion`\n          // - fail to get an alternative LogSegment\n          // - throw the below exception\n          assert(e.isInstanceOf[IllegalStateException] && e.getMessage.contains(\n            \"Couldn't find all part files of the checkpoint version: 1\"))\n        } else {\n          // - checkpoint 1 is in the list result\n          // - Snapshot creation triggers state reconstruction\n          // - fail to read protocol+metadata from checkpoint 1\n          // - throw FileReadException\n          // - fail to get an alternative LogSegment\n          // - cannot find log file 0 so throw the above checkpoint 1 read failure\n          // Guava cache wraps the root cause\n          assert(e.isInstanceOf[SparkException] &&\n            e.getMessage.contains(\"0001.checkpoint\") &&\n            e.getMessage.contains(\"Encountered error while reading file\"))\n        }\n      }\n    }\n  }\n\n  testWithAndWithoutMultipartCheckpoint(\"should not recover when both the current and previous \" +\n      \"checkpoints are broken\") { partToCorrupt =>\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      val staleLog = DeltaLog.forTable(spark, path)\n      DeltaLog.clearCache()\n\n      spark.range(10).write.format(\"delta\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n      DeltaLog.clearCache()\n      val checkpointParts0 =\n        DeltaLog.forTable(spark, path).snapshot.logSegment.checkpointProvider.topLevelFiles.size\n\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      deltaLog.update()\n      deltaLog.checkpoint()\n      deleteLogVersion(path, version = 0)\n\n      DeltaLog.clearCache()\n      val checkpointParts1 =\n        DeltaLog.forTable(spark, path).snapshot.logSegment.checkpointProvider.topLevelFiles.size\n\n      makeCorruptCheckpointFile(path, checkpointVersion = 0, shouldBeEmpty = false,\n        multipart = partToCorrupt.map((_, checkpointParts0)))\n\n      val multipart = partToCorrupt.map((_, checkpointParts1))\n\n      // We have different code paths for empty and non-empty checkpoints\n      for (testEmptyCheckpoint <- Seq(true, false)) {\n        makeCorruptCheckpointFile(path, checkpointVersion = 1,\n          shouldBeEmpty = testEmptyCheckpoint, multipart = multipart)\n\n        // The code paths are different, but the error and message end up being the same:\n        //\n        // testEmptyCheckpoint = true:\n        // - checkpoint 1 is NOT in the list result.\n        // - fallback to load version 0 using checkpoint 0\n        // - fail to read checkpoint 0\n        // - cannot find log file 0 so throw the above checkpoint 0 read failure\n        //\n        // testEmptyCheckpoint = false:\n        // - checkpoint 1 is in the list result.\n        // - Snapshot creation triggers state reconstruction\n        // - fail to read protocol+metadata from checkpoint 1\n        // - fallback to load version 0 using checkpoint 0\n        // - fail to read checkpoint 0\n        // - cannot find log file 0 so throw the original checkpoint 1 read failure\n        val e = intercept[SparkException] { staleLog.update() }\n        val version = if (testEmptyCheckpoint) 0 else 1\n        assert(e.getMessage.contains(f\"$version%020d.checkpoint\") &&\n          e.getMessage.contains(\"Encountered error while reading file\"))\n      }\n    }\n  }\n\n  test(\"should throw a clear exception when checkpoint exists but its corresponding delta file \" +\n    \"doesn't exist\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      val staleLog = DeltaLog.forTable(spark, path)\n      DeltaLog.clearCache()\n\n      spark.range(10).write.format(\"delta\").save(path)\n      DeltaLog.forTable(spark, path).checkpoint()\n      // Delete delta files\n      new File(tempDir, \"_delta_log\").listFiles().filter(_.getName.endsWith(\".json\"))\n        .foreach(_.delete())\n      val e = intercept[IllegalStateException] {\n        staleLog.update()\n      }\n      assert(e.getMessage.contains(\"Could not find any delta files for version 0\"))\n    }\n  }\n\n  test(\"should throw an exception when trying to load a non-existent version\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      val staleLog = DeltaLog.forTable(spark, path)\n      DeltaLog.clearCache()\n\n      spark.range(10).write.format(\"delta\").save(path)\n      DeltaLog.forTable(spark, path).checkpoint()\n      val e = intercept[IllegalStateException] {\n        staleLog.getSnapshotAt(2)\n      }\n      assert(e.getMessage.contains(\"Trying to load a non-existent version 2\"))\n    }\n  }\n\n  test(\"should throw a clear exception when the checkpoint is corrupt \" +\n    \"but could not find any delta files\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      val staleLog = DeltaLog.forTable(spark, path)\n      DeltaLog.clearCache()\n\n      spark.range(10).write.format(\"delta\").save(path)\n      DeltaLog.forTable(spark, path).checkpoint()\n      // Delete delta files\n      new File(tempDir, \"_delta_log\").listFiles().filter(_.getName.endsWith(\".json\"))\n        .foreach(_.delete())\n      if (coordinatedCommitsEnabledInTests) {\n        new File(new File(tempDir, \"_delta_log\"), \"_staged_commits\")\n          .listFiles()\n          .filter(_.getName.endsWith(\".json\"))\n          .foreach(_.delete())\n      }\n      makeCorruptCheckpointFile(path, checkpointVersion = 0, shouldBeEmpty = false)\n      val e = intercept[IllegalStateException] {\n        staleLog.update()\n      }\n      assert(e.getMessage.contains(\"Could not find any delta files for version 0\"))\n    }\n  }\n\n  test(\"verifyDeltaVersions\") {\n    import SnapshotManagement.verifyDeltaVersions\n    // empty array\n    verifyDeltaVersions(\n      spark,\n      versions = Array.empty,\n      expectedStartVersion = None,\n      expectedEndVersion = None,\n      cachedSnapshot = None)\n    // contiguous versions\n    verifyDeltaVersions(\n      spark,\n      versions = Array(1, 2, 3),\n      expectedStartVersion = None,\n      expectedEndVersion = None,\n      cachedSnapshot = None)\n    // contiguous versions with correct `expectedStartVersion` and `expectedStartVersion`\n    verifyDeltaVersions(\n      spark,\n      versions = Array(1, 2, 3),\n      expectedStartVersion = None,\n      expectedEndVersion = Some(3),\n      cachedSnapshot = None)\n    verifyDeltaVersions(\n      spark,\n      versions = Array(1, 2, 3),\n      expectedStartVersion = Some(1),\n      expectedEndVersion = None,\n      cachedSnapshot = None)\n    verifyDeltaVersions(\n      spark,\n      versions = Array(1, 2, 3),\n      expectedStartVersion = Some(1),\n      expectedEndVersion = Some(3),\n      cachedSnapshot = None)\n    // `expectedStartVersion` or `expectedEndVersion` doesn't match\n    intercept[IllegalArgumentException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array(1, 2),\n        expectedStartVersion = Some(0),\n        expectedEndVersion = None,\n        cachedSnapshot = None)\n    }\n    intercept[IllegalArgumentException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array(1, 2),\n        expectedStartVersion = None,\n        expectedEndVersion = Some(3),\n        cachedSnapshot = None)\n    }\n    intercept[IllegalArgumentException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array.empty,\n        expectedStartVersion = Some(0),\n        expectedEndVersion = None,\n        cachedSnapshot = None)\n    }\n    intercept[IllegalArgumentException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array.empty,\n        expectedStartVersion = None,\n        expectedEndVersion = Some(3),\n        cachedSnapshot = None)\n    }\n    // non contiguous versions\n    intercept[IllegalStateException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array(1, 3),\n        expectedStartVersion = None,\n        expectedEndVersion = None,\n        cachedSnapshot = None)\n    }\n    // duplicates in versions\n    intercept[IllegalStateException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array(1, 2, 2, 3),\n        expectedStartVersion = None,\n        expectedEndVersion = None,\n        cachedSnapshot = None)\n    }\n    // unsorted versions\n    intercept[IllegalStateException] {\n      verifyDeltaVersions(\n        spark,\n        versions = Array(3, 2, 1),\n        expectedStartVersion = None,\n        expectedEndVersion = None,\n        cachedSnapshot = None)\n    }\n\n    // -----------------------------------------------------\n    // | Usage logs validation for non-contiguous versions |\n    // -----------------------------------------------------\n\n    /**\n     * Helper function to validate the usage log properties for the\n     * given `usageLogs` and `expected*` values.\n     */\n    def validateUsageLogProperties(\n        usageLogs: Seq[UsageRecord],\n        expectedStartVersion: Long,\n        expectedEndVersion: Long,\n        expectedVersionToLoad: Long,\n        expectedLatestSnapshotVersion: Long,\n        expectedLatestCheckpointVersion: Long,\n        shouldChecksumOptPresent: Boolean): Unit = {\n      assert(usageLogs.size == 1)\n      val usageLog = usageLogs.head\n      // `tags.opType` should be \"delta.exceptions.deltaVersionsNotContiguous\"\n      assert(usageLog.tags.getOrElse(\"opType\", \"null\") ==\n        \"delta.exceptions.deltaVersionsNotContiguous\")\n      val blob = JsonUtils.fromJson[Map[String, Any]](usageLog.blob)\n      // `blob` validation\n      assert(blob.get(\"startVersion\").exists(_.toString.toLong == expectedStartVersion))\n      assert(blob.get(\"endVersion\").exists(_.toString.toLong == expectedEndVersion))\n      assert(blob.get(\"versionToLoad\").exists(_.toString.toLong == expectedVersionToLoad))\n      assert(\n        blob.get(\"unsafeVolatileSnapshot.latestSnapshotVersion\").exists(_.toString.toLong ==\n          expectedLatestSnapshotVersion))\n      assert(\n        blob.get(\"unsafeVolatileSnapshot.latestCheckpointVersion\").exists(_.toString.toLong ==\n          expectedLatestCheckpointVersion))\n      // `stackTrace` should contain the entire stack trace,\n      // here we verify the starting of the stack trace.\n      assert(blob.get(\"stackTrace\").exists(_.toString.startsWith(\n          \"org.apache.spark.sql.delta.SnapshotManagement$.verifyDeltaVersions\")))\n      // Check whether `unsafeVolatileSnapshot.checksumOpt` is present or not\n      assert(blob.contains(\"unsafeVolatileSnapshot.checksumOpt\") == shouldChecksumOptPresent)\n    }\n\n    // 1. Basic usage log validation.\n    val usageLogs = Log4jUsageLogger.track {\n        intercept[IllegalStateException] {\n          verifyDeltaVersions(\n            spark,\n            versions = Array(1, 3),\n            expectedStartVersion = None,\n            expectedEndVersion = None,\n            cachedSnapshot = None)\n        }\n      }.filter(_.metric == \"tahoeEvent\")\n    validateUsageLogProperties(\n      usageLogs,\n      expectedStartVersion = 1,\n      expectedEndVersion = 3,\n      expectedVersionToLoad = -1,\n      expectedLatestSnapshotVersion = -1,\n      expectedLatestCheckpointVersion = -1,\n      shouldChecksumOptPresent = false)\n\n    // 2. Usage log validation with `expectedStartVersion`, `expectedEndVersion`\n    //    and `cachedSnapshot`.\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      import testImplicits._\n      // Commit 0 - to trigger the initial snapshot construction for version 0\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(path)\n      val snapshot = DeltaLog.forTable(spark, path).update()\n      val usageLogs = Log4jUsageLogger.track {\n        intercept[IllegalStateException] {\n          verifyDeltaVersions(\n            spark,\n            versions = Array(1, 3),\n            expectedStartVersion = Some(1),\n            expectedEndVersion = Some(4),\n            cachedSnapshot = Some(snapshot))\n        }\n      }.filter(_.metric == \"tahoeEvent\")\n      validateUsageLogProperties(\n        usageLogs,\n        expectedStartVersion = 1,\n        expectedEndVersion = 3,\n        expectedVersionToLoad = 4,\n        expectedLatestSnapshotVersion = 0,\n        expectedLatestCheckpointVersion = -1,\n        shouldChecksumOptPresent = true)\n    }\n  }\n\n  test(\"configurable snapshot cache storage level\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(path)\n      DeltaLog.clearCache()\n      // Corrupted snapshot tests leave a cached snapshot not tracked by the DeltaLog cache\n      sparkContext.getPersistentRDDs.foreach(_._2.unpersist())\n      assert(sparkContext.getPersistentRDDs.isEmpty)\n\n      withSQLConf(DeltaSQLConf.DELTA_SNAPSHOT_CACHE_STORAGE_LEVEL.key -> \"DISK_ONLY\") {\n        DeltaLog.forTable(spark, path).snapshot.stateDS.collect()\n        val persistedRDDs = sparkContext.getPersistentRDDs\n        assert(persistedRDDs.size == 1)\n        assert(persistedRDDs.values.head.getStorageLevel == StorageLevel.DISK_ONLY)\n      }\n\n      DeltaLog.clearCache()\n      assert(sparkContext.getPersistentRDDs.isEmpty)\n\n      withSQLConf(DeltaSQLConf.DELTA_SNAPSHOT_CACHE_STORAGE_LEVEL.key -> \"NONE\") {\n        DeltaLog.forTable(spark, path).snapshot.stateDS.collect()\n        val persistedRDDs = sparkContext.getPersistentRDDs\n        assert(persistedRDDs.size == 1)\n        assert(persistedRDDs.values.head.getStorageLevel == StorageLevel.NONE)\n      }\n\n      DeltaLog.clearCache()\n      assert(sparkContext.getPersistentRDDs.isEmpty)\n\n      withSQLConf(DeltaSQLConf.DELTA_SNAPSHOT_CACHE_STORAGE_LEVEL.key -> \"invalid\") {\n        intercept[IllegalArgumentException] {\n          spark.read.format(\"delta\").load(path).collect()\n        }\n      }\n    }\n  }\n\n  test(\"SerializableFileStatus json serialization/deserialization\") {\n    val testCases = Seq(\n      SerializableFileStatus(path = \"xyz\", length = -1, isDir = true, modificationTime = 0)\n        -> \"\"\"{\"path\":\"xyz\",\"length\":-1,\"isDir\":true,\"modificationTime\":0}\"\"\",\n      SerializableFileStatus(\n        path = \"s3://a.b/pq\", length = 123L, isDir = false, modificationTime = 246L)\n        -> \"\"\"{\"path\":\"s3://a.b/pq\",\"length\":123,\"isDir\":false,\"modificationTime\":246}\"\"\"\n    )\n    for ((obj, json) <- testCases) {\n      assert(JsonUtils.toJson(obj) == json)\n      val status = JsonUtils.fromJson[SerializableFileStatus](json)\n      assert(status.modificationTime === obj.modificationTime)\n      assert(status.isDir === obj.isDir)\n      assert(status.length === obj.length)\n      assert(status.path === obj.path)\n    }\n  }\n\n  test(\"getLogSegmentAfterCommit can find specified commit\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      val log = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      val oldLogSegment = log.snapshot.logSegment\n      spark.range(10).write.format(\"delta\").save(path)\n      val newLogSegment = log.snapshot.logSegment\n      assert(log.getLogSegmentAfterCommit(\n        log.snapshot.tableCommitCoordinatorClientOpt,\n        catalogTableOpt = None,\n        oldLogSegment.checkpointProvider) === newLogSegment)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n      val commitFileProvider = DeltaCommitFileProvider(log.snapshot)\n      intercept[IllegalArgumentException] {\n        val commitFile = fs.getFileStatus(commitFileProvider.deltaFile(1))\n        val commit = new Commit(1, commitFile, 0)\n        // Version exists, but not contiguous with old logSegment\n        log.getLogSegmentAfterCommit(\n          1, None, oldLogSegment, commit, None, None, EmptyCheckpointProvider)\n      }\n      intercept[IllegalArgumentException] {\n        val commitFile = fs.getFileStatus(commitFileProvider.deltaFile(0))\n        val commit = new Commit(0, commitFile, 0)\n\n        // Version exists, but newLogSegment already contains it\n        log.getLogSegmentAfterCommit(\n          0, None, newLogSegment, commit, None, None, EmptyCheckpointProvider)\n      }\n      assert(log.getLogSegmentAfterCommit(\n        log.snapshot.tableCommitCoordinatorClientOpt,\n        catalogTableOpt = None,\n        oldLogSegment.checkpointProvider) === log.snapshot.logSegment)\n    }\n  }\n\n  testQuietly(\"checkpoint/json not found when executor restart \" +\n    \"after expired checkpoints in the snapshot cache are cleaned up\") {\n    withTempDir { tempDir =>\n      // Create checkpoint 1 and 3\n      val path = tempDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(path)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      deltaLog.checkpoint()\n      // simulate checkpoint 1 expires and is cleaned up\n      deleteCheckpointVersion(path, 1)\n      // simulate executor hangs and restart, cache invalidation\n      deltaLog.snapshot.uncache()\n\n      spark.read.format(\"delta\").load(path).collect()\n    }\n  }\n\n  test(\"getUpdatedLogSegment without new files returns the original log segment\") {\n    withTempDir { tempDir =>\n      // Create checkpoint 1 and 3\n      val path = tempDir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").save(path)\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      deltaLog.checkpoint()\n      val snapshot = deltaLog.update()\n      val (updatedLogSegment, _) = deltaLog.getUpdatedLogSegment(\n        snapshot.logSegment,\n        tableCommitCoordinatorClientOpt = None,\n        catalogTableOpt = None\n      )\n      assert(updatedLogSegment === snapshot.logSegment)\n    }\n  }\n}\n\nclass SnapshotManagementWithCoordinatedCommitsBatch1Suite extends SnapshotManagementSuite {\n  override def coordinatedCommitsBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass SnapshotManagementWithCoordinatedCommitsBatch2Suite extends SnapshotManagementSuite {\n  override def coordinatedCommitsBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass SnapshotManagementWithCoordinatedCommitsBatch100Suite extends SnapshotManagementSuite {\n  override def coordinatedCommitsBackfillBatchSize: Option[Int] = Some(100)\n}\n\nclass CountDownLatchLogStore(sparkConf: SparkConf, hadoopConf: Configuration)\n    extends LocalLogStore(sparkConf, hadoopConf) {\n  override def listFrom(path: Path, hadoopConf: Configuration): Iterator[FileStatus] = {\n    val files = super.listFrom(path, hadoopConf).toSeq\n    if (ConcurrentBackfillCommitCoordinatorClient.beginConcurrentBackfills) {\n      CountDownLatchLogStore.listFromCalled.countDown()\n    }\n    files.iterator\n  }\n}\nobject CountDownLatchLogStore {\n  val listFromCalled = new CountDownLatch(1)\n}\n\ncase class ConcurrentBackfillCommitCoordinatorClient(\n    synchronousBackfillThreshold: Long,\n    override val batchSize: Long\n) extends InMemoryCommitCoordinator(batchSize) {\n  private val deferredBackfills: mutable.Map[Long, () => Unit] = mutable.Map.empty\n  override def getCommits(\n      tableDesc: TableDescriptor,\n      startVersion: java.lang.Long,\n      endVersion: java.lang.Long): GetCommitsResponse = {\n    if (ConcurrentBackfillCommitCoordinatorClient.beginConcurrentBackfills) {\n      CountDownLatchLogStore.listFromCalled.await()\n      logInfo(s\"Finishing pending backfills concurrently: ${deferredBackfills.keySet}\")\n      deferredBackfills.keys.toSeq.sorted.foreach((version: Long) => deferredBackfills(version)())\n      deferredBackfills.clear()\n    }\n    super.getCommits(tableDesc, startVersion, endVersion)\n  }\n  override def backfill(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      logPath: Path,\n      version: Long,\n      fileStatus: FileStatus): Unit = {\n    if (version > synchronousBackfillThreshold &&\n        ConcurrentBackfillCommitCoordinatorClient.deferBackfills) {\n      deferredBackfills(version) = () =>\n        super.backfill(logStore, hadoopConf, logPath, version, fileStatus)\n    } else {\n      super.backfill(logStore, hadoopConf, logPath, version, fileStatus)\n    }\n  }\n}\nobject ConcurrentBackfillCommitCoordinatorClient {\n  var deferBackfills = false\n  var beginConcurrentBackfills = false\n}\n\nobject ConcurrentBackfillCommitCoordinatorBuilder extends CommitCoordinatorBuilder {\n  val batchSize = 5\n  private lazy val concurrentBackfillCommitCoordinatorClient =\n    ConcurrentBackfillCommitCoordinatorClient(synchronousBackfillThreshold = 2, batchSize)\n  override def getName: String = \"awaiting-commit-coordinator\"\n  override def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n    concurrentBackfillCommitCoordinatorClient\n  }\n}\n\n/**\n * Setup (Assuming batch size = 5 & synchronousBackfillThreshold = 2):\n * - LogStore contains backfilled commits [0, 2]\n * - CommitCoordinatorClient contains unbackfilled commits [3, ...]\n * - Backfills are pending for versions [3, 5]\n *\n * Goal: Create a gap for versions [3, 5] in the LogStore and CommitCoordinatorClient listings.\n *\n * Step 1: LogStore retrieves delta files for versions [0, 2] from the file system.\n * Step 2: Wait on the latch to ensure step (1) is completed before step (3) begins.\n * Step 3: Backfill commits [3, 5] from CommitCoordinatorClient to LogStore using deferredBackfills\n * map.\n * Step 4: CommitCoordinatorClient returns commits [6, ...] (if valid).\n *\n * Test that the code correctly handles the gap in the LogStore and CommitCoordinatorClient listings\n * by making an additional call to LogStore to fetch versions [3, 5].\n */\nclass SnapshotManagementParallelListingSuite extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest {\n\n  override protected def sparkConf: SparkConf =\n    super.sparkConf.set(logStoreClassConfKey, classOf[CountDownLatchLogStore].getName)\n\n  override protected def beforeEach(): Unit = {\n    super.beforeEach()\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n    CommitCoordinatorProvider.registerBuilder(ConcurrentBackfillCommitCoordinatorBuilder)\n    ConcurrentBackfillCommitCoordinatorClient.beginConcurrentBackfills = false\n    ConcurrentBackfillCommitCoordinatorClient.deferBackfills = false\n  }\n\n  private def writeDeltaData(path: String, endVersion: Long): Unit = {\n    spark.range(10).write.format(\"delta\").save(path)\n    (1L to endVersion).foreach(\n      _ => spark.range(10).write.format(\"delta\").mode(\"append\").save(path))\n  }\n\n  private def captureUsageRecordsAndGetSnapshot(dataPath: Path): (Snapshot, Seq[UsageRecord]) = {\n    var snapshot: Snapshot = null\n    val records = Log4jUsageLogger.track {\n      snapshot = DeltaLog.forTable(spark, dataPath).update()\n    }\n    (snapshot, records)\n  }\n\n  private def verifyUsageRecords(\n      records: Seq[UsageRecord],\n      expectedNeedAdditionalFsListingCount: Int): Unit = {\n    val filteredLogs = DeltaTestUtils.filterUsageRecords(\n      records, CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_ADDITIONAL_LISTING_REQUIRED)\n    assert(filteredLogs.size === expectedNeedAdditionalFsListingCount)\n  }\n\n  private def verifySnapshotBackfills(snapshot: Snapshot, backfillUntilInclusive: Long): Unit = {\n    snapshot.logSegment.deltas.zipWithIndex.foreach { case (delta, index) =>\n      if (index <= backfillUntilInclusive) {\n        verifyBackfilled(delta)\n      } else {\n        verifyUnbackfilled(delta)\n      }\n    }\n  }\n\n  /**\n   * concurrentBackfills: Whether to defer backfills for versions > synchronousBackfillThreshold to\n   *                      simulate concurrent backfills to test addition file-system listing.\n   * tryIncludeGapAtTheEnd: Whether to include a gap in listing at end of the version range or\n   *                        somewhere in the middle.\n   */\n  BOOLEAN_DOMAIN.foreach { concurrentBackfills =>\n    BOOLEAN_DOMAIN.foreach { tryIncludeGapAtTheEnd =>\n      test(\n        s\"Backfills are properly reconciled with concurrentBackfills: $concurrentBackfills, \" +\n          s\"tryIncludeGapAtTheEnd: $tryIncludeGapAtTheEnd\") {\n        ConcurrentBackfillCommitCoordinatorClient.deferBackfills = concurrentBackfills\n        val batchSize = ConcurrentBackfillCommitCoordinatorBuilder.batchSize\n        val endVersion = if (tryIncludeGapAtTheEnd) { batchSize } else { batchSize + 3 }\n        withSQLConf(\n            COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey ->\n              ConcurrentBackfillCommitCoordinatorBuilder.getName,\n            DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS.key -> \"false\") {\n          withTempDir { tempDir =>\n            val path = tempDir.getCanonicalPath\n            val dataPath = new Path(path)\n\n            writeDeltaData(path, endVersion)\n\n            // Invalidate cache to ensure re-listing.\n            DeltaLog.invalidateCache(spark, dataPath)\n            ConcurrentBackfillCommitCoordinatorClient.beginConcurrentBackfills = true\n\n            val (snapshot, records) = captureUsageRecordsAndGetSnapshot(dataPath)\n            val expectedNeedAdditionalFsListingCount = if (concurrentBackfills) { 1 } else { 0 }\n            verifyUsageRecords(records, expectedNeedAdditionalFsListingCount)\n            verifySnapshotBackfills(snapshot, backfillUntilInclusive = batchSize)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"throws exception when additional listing also can't reconcile\") {\n    val batchSize = ConcurrentBackfillCommitCoordinatorBuilder.batchSize\n    val endVersion = batchSize + 3\n    withSQLConf(\n        COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey ->\n          ConcurrentBackfillCommitCoordinatorBuilder.getName,\n        DeltaSQLConf.DELTALOG_MINOR_COMPACTION_USE_FOR_READS.key -> \"false\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getCanonicalPath\n        val dataPath = new Path(path)\n\n        writeDeltaData(path, endVersion)\n\n        // Delete 5.json to create a permanent gap between file-system i.e. [0, 4] and\n        // commit-store [6, 8] which would even an additional listing won't be able to reconcile.\n        val deltaLog = DeltaLog.forTable(spark, dataPath)\n        deltaLog.logPath.getFileSystem(deltaLog.newDeltaHadoopConf()).delete(\n          FileNames.unsafeDeltaFile(deltaLog.logPath, batchSize), true)\n\n        // Invalidate cache to ensure re-listing.\n        DeltaLog.invalidateCache(spark, dataPath)\n\n        val e = intercept[IllegalStateException] {\n          DeltaLog.forTable(spark, dataPath).update()\n        }\n        assert(e.getMessage.contains(\"unexpectedly still requires additional file-system listing\"))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/TableRedirectSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.io.File\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsBaseSuite\nimport org.apache.spark.sql.delta.redirect.{\n  DropRedirectInProgress,\n  EnableRedirectInProgress,\n  NoRedirectRule,\n  PathBasedRedirectSpec,\n  RedirectFeature,\n  RedirectReaderWriter,\n  RedirectReady,\n  RedirectSpec,\n  RedirectState,\n  RedirectWriterOnly,\n  TableRedirect,\n  TableRedirectConfiguration\n}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.commons.text.StringEscapeUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{QueryTest, SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass TableRedirectSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with CoordinatedCommitsBaseSuite\n  with DeltaCheckpointTestUtils\n  with DeltaSQLTestUtils {\n\n  private def validateState(\n      deltaLog: DeltaLog,\n      redirectState: RedirectState,\n      sourceTablePath: File,\n      destTablePath: File,\n      feature: TableRedirect\n  ): Unit = {\n    val snapshot = deltaLog.update()\n    assert(feature.isFeatureSet(snapshot.metadata))\n    val redirectConfig = feature.getRedirectConfiguration(snapshot.metadata).get\n    val protocol = snapshot.protocol\n    if (feature != RedirectWriterOnly) {\n      assert(protocol.readerFeatureNames.contains(RedirectReaderWriterFeature.name))\n      assert(protocol.writerFeatureNames.contains(RedirectReaderWriterFeature.name))\n    } else {\n      assert(!protocol.readerFeatureNames.contains(RedirectWriterOnlyFeature.name))\n      assert(protocol.writerFeatureNames.contains(RedirectWriterOnlyFeature.name))\n    }\n    assert(redirectConfig.redirectState == redirectState)\n    assert(redirectConfig.`type` == PathBasedRedirectSpec.REDIRECT_TYPE)\n    val srcPath = sourceTablePath.getCanonicalPath\n    val dstPath = destTablePath.getCanonicalPath\n    val expectedSpecValue = s\"\"\"{\"sourcePath\":\"$srcPath\",\"destPath\":\"$dstPath\"}\"\"\"\n    assert(redirectConfig.specValue == expectedSpecValue)\n    val redirectSpec = redirectConfig.spec.asInstanceOf[PathBasedRedirectSpec]\n    assert(redirectSpec.sourcePath == srcPath)\n    assert(redirectSpec.destPath == dstPath)\n  }\n\n  private def validateRemovedState(deltaLog: DeltaLog, feature: TableRedirect): Unit = {\n    val snapshot = deltaLog.update()\n    val protocol = snapshot.protocol\n    assert(!feature.isFeatureSet(snapshot.metadata))\n    if (feature != RedirectWriterOnly) {\n      assert(protocol.readerFeatureNames.contains(RedirectReaderWriterFeature.name))\n      assert(protocol.writerFeatureNames.contains(RedirectReaderWriterFeature.name))\n    } else {\n      assert(!protocol.readerFeatureNames.contains(RedirectWriterOnlyFeature.name))\n      assert(protocol.writerFeatureNames.contains(RedirectWriterOnlyFeature.name))\n    }\n  }\n\n  def redirectTest(\n      label: String, enableRedirect: Boolean\n  )(f: (DeltaLog, File, File, CatalogTable) => Unit): Unit = {\n    test(s\"basic table redirect: $label\") {\n      withTempDir { sourceTablePath =>\n        withTempDir { destTablePath =>\n          withSQLConf(DeltaSQLConf.ENABLE_TABLE_REDIRECT_FEATURE.key -> enableRedirect.toString) {\n            withTable(\"t1\", \"t2\") {\n              sql(s\"CREATE external TABLE t1(c0 long) USING delta LOCATION '$sourceTablePath';\")\n              val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(\"t1\"))\n              val deltaLog = DeltaLog.forTable(spark, new Path(sourceTablePath.getCanonicalPath))\n              f(deltaLog, sourceTablePath, destTablePath, catalogTable)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  Seq(RedirectReaderWriter, RedirectWriterOnly).foreach { feature =>\n    val featureName = feature.config.key\n    Seq(true, false).foreach { hasCatalogTable =>\n      redirectTest(s\"basic redirect: $featureName - \" +\n        s\"hasCatalogTable: $hasCatalogTable\", enableRedirect = false) {\n        case (deltaLog, source, dest, catalogTable) =>\n        val snapshot = deltaLog.update()\n        assert(!feature.isFeatureSet(snapshot.metadata))\n        val redirectSpec = new PathBasedRedirectSpec(\n          source.getCanonicalPath,\n          dest.getCanonicalPath\n        )\n        val catalogTableOpt = if (hasCatalogTable) Some(catalogTable) else None\n        val redirectType = PathBasedRedirectSpec.REDIRECT_TYPE\n        // Step-1: Initiate table redirection and set to EnableRedirectInProgress state.\n        feature.add(deltaLog, catalogTableOpt, redirectType, redirectSpec)\n        validateState(deltaLog, EnableRedirectInProgress, source, dest, feature)\n        // Step-2: Complete table redirection and set to RedirectReady state.\n        feature.update(deltaLog, catalogTableOpt, RedirectReady, redirectSpec)\n        validateState(deltaLog, RedirectReady, source, dest, feature)\n        // Step-3: Start dropping table redirection and set to DropRedirectInProgress state.\n        feature.update(deltaLog, catalogTableOpt, DropRedirectInProgress, redirectSpec)\n        validateState(deltaLog, DropRedirectInProgress, source, dest, feature)\n        // Step-4: Finish dropping table redirection and remove the property completely.\n        feature.remove(deltaLog, catalogTableOpt)\n        validateRemovedState(deltaLog, feature)\n        // Step-5: Initiate table redirection and set to EnableRedirectInProgress state one\n        // more time.\n        withTempDir { destTablePath2 =>\n          val redirectSpec = new PathBasedRedirectSpec(\n            source.getCanonicalPath,\n            destTablePath2.getCanonicalPath\n          )\n          feature.add(deltaLog, catalogTableOpt, redirectType, redirectSpec)\n          validateState(deltaLog, EnableRedirectInProgress, source, destTablePath2, feature)\n          // Step-6: Finish dropping table redirection and remove the property completely.\n          feature.remove(deltaLog, catalogTableOpt)\n          validateRemovedState(deltaLog, feature)\n        }\n      }\n\n      redirectTest(s\"Redirect $featureName: empty no redirect rules - \" +\n        s\"hasCatalogTable: $hasCatalogTable\", enableRedirect = false) {\n        case (deltaLog, source, dest, catalogTable) =>\n          val snapshot = deltaLog.update()\n          assert(!feature.isFeatureSet(snapshot.metadata))\n          val redirectSpec = new PathBasedRedirectSpec(\n            source.getCanonicalPath,\n            dest.getCanonicalPath\n          )\n          val catalogTableOpt = if (hasCatalogTable) Some(catalogTable) else None\n          val redirectType = PathBasedRedirectSpec.REDIRECT_TYPE\n          // 0. Initialize table redirection by setting table to EnableRedirectInProgress state.\n          feature.add(deltaLog, catalogTableOpt, redirectType, redirectSpec)\n          validateState(deltaLog, EnableRedirectInProgress, source, dest, feature)\n\n          // 1. INSERT should hit DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE because table is in\n          //    EnableRedirectInProgress, which doesn't allow any DML and DDL.\n          val exception1 = intercept[DeltaIllegalStateException] {\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          }\n          assert(exception1.getErrorClass == \"DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE\")\n\n          // 2. DDL should hit DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE because table is in\n          //    EnableRedirectInProgress, which doesn't allow any DML and DDL.\n          val exception2 = intercept[DeltaIllegalStateException] {\n            sql(s\"alter table delta.`$source` add column c3 long\")\n          }\n          assert(exception2.getErrorClass == \"DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE\")\n\n          // 3. Move to RedirectReady state.\n          feature.update(deltaLog, catalogTableOpt, RedirectReady, redirectSpec)\n\n          // 4. INSERT should hit DELTA_NO_REDIRECT_RULES_VIOLATED since the\n          //    no-redirect-rules is empty.\n          validateState(deltaLog, RedirectReady, source, dest, feature)\n          val exception3 = intercept[DeltaIllegalStateException] {\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          }\n          assert(exception3.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 5. DDL should hit DELTA_NO_REDIRECT_RULES_VIOLATED since the\n          //    no-redirect-rules is empty.\n          val exception4 = intercept[DeltaIllegalStateException] {\n            sql(s\"alter table delta.`$source` add column c3 long\")\n          }\n          assert(exception4.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 6. Move to DropRedirectInProgress state.\n          feature.update(deltaLog, catalogTableOpt, DropRedirectInProgress, redirectSpec)\n\n          // 7. INSERT should hit DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE because table is in\n          //    DropRedirectInProgress, which doesn't allow any DML and DDL.\n          validateState(deltaLog, DropRedirectInProgress, source, dest, feature)\n          val exception5 = intercept[DeltaIllegalStateException] {\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          }\n          assert(exception5.getErrorClass == \"DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE\")\n\n          // 8. DDL should hit DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE because table is in\n          //    DropRedirectInProgress, which doesn't allow any DML and DDL.\n          val exception6 = intercept[DeltaIllegalStateException] {\n            sql(s\"alter table delta.`$source` add column c3 long\")\n          }\n          assert(exception6.getErrorClass == \"DELTA_COMMIT_INTERMEDIATE_REDIRECT_STATE\")\n      }\n\n      redirectTest(s\"Redirect $featureName: no redirect rules - \" +\n        s\"hasCatalogTable: $hasCatalogTable\", enableRedirect = false) {\n        case (deltaLog, source, dest, catalogTable) =>\n          val snapshot = deltaLog.update()\n          assert(!feature.isFeatureSet(snapshot.metadata))\n          val redirectSpec = new PathBasedRedirectSpec(\n            source.getCanonicalPath,\n            dest.getCanonicalPath\n          )\n          val catalogTableOpt = if (hasCatalogTable) Some(catalogTable) else None\n          val redirectType = PathBasedRedirectSpec.REDIRECT_TYPE\n          sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          feature.add(deltaLog, catalogTableOpt, redirectType, redirectSpec)\n          validateState(deltaLog, EnableRedirectInProgress, source, dest, feature)\n          // 1. Move table redirect to RedirectReady state with no redirect rules that\n          // allows WRITE, DELETE, UPDATE.\n          var noRedirectRules = Set(\n            NoRedirectRule(\n              appName = None,\n              allowedOperations = Set(\n                DeltaOperations.OP_WRITE,\n                DeltaOperations.OP_DELETE,\n                DeltaOperations.OP_UPDATE\n              )\n            )\n          )\n          feature.update(deltaLog, catalogTableOpt, RedirectReady, redirectSpec, noRedirectRules)\n          validateState(deltaLog, RedirectReady, source, dest, feature)\n          sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          sql(s\"update delta.`$source` set c0 = 100\")\n          sql(s\"delete from delta.`$source` where c0 = 1\")\n\n          // 2. Move table redirect to RedirectReady state with no-redirect-rules that\n          //    allows UPDATE.\n          noRedirectRules = Set(\n            NoRedirectRule(\n              appName = None, allowedOperations = Set(DeltaOperations.Update(None).name)\n            )\n          )\n          feature.update(deltaLog, catalogTableOpt, RedirectReady, redirectSpec, noRedirectRules)\n          validateState(deltaLog, RedirectReady, source, dest, feature)\n          // 2.1. WRITE should be aborted because no-redirect-rules only allow UPDATE.\n          val exception1 = intercept[DeltaIllegalStateException] {\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          }\n          assert(exception1.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 2.2. UPDATE should pass because no-redirect-rules is fulfilled.\n          sql(s\"update delta.`$source` set c0 = 100\")\n\n          // 2.3. DELETE should be aborted because no-redirect-rules only allow UPDATE.\n          val exception3 = intercept[DeltaIllegalStateException] {\n            sql(s\"delete from delta.`$source` where c0 = 1\")\n          }\n          assert(exception3.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 2.4. Disabling SKIP_REDIRECT_FEATURE should allow all DMLs to pass.\n          withSQLConf(DeltaSQLConf.SKIP_REDIRECT_FEATURE.key -> \"true\") {\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n            sql(s\"delete from delta.`$source` where c0 = 1\")\n          }\n\n          // 3. Move table redirect to RedirectReady state with no-redirect-rules that\n          // allows Write on appName \"etl\" .\n          noRedirectRules = Set(\n            NoRedirectRule(\n              appName = Some(\"etl\"),\n              allowedOperations = Set(DeltaOperations.Write(SaveMode.Append).name)\n            )\n          )\n          feature.update(deltaLog, catalogTableOpt, RedirectReady, redirectSpec, noRedirectRules)\n          validateState(deltaLog, RedirectReady, source, dest, feature)\n\n          // 3.1. The WRITE of appName \"dummy\" would be aborted because no-redirect-rules\n          //      only allow WRITE on application \"etl\".\n          val exception4 = intercept[DeltaIllegalStateException] {\n            spark.conf.set(\"spark.app.name\", \"dummy\")\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n          }\n          assert(exception4.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 3.1. WRITE should pass\n          spark.conf.set(\"spark.app.name\", \"etl\")\n          sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n\n          // 3.2. UPDATE should be aborted because no-redirect-rules only allow WRITE.\n          val exception5 = intercept[DeltaIllegalStateException] {\n            sql(s\"update delta.`$source` set c0 = 100\")\n          }\n          assert(exception5.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 3.3. DELETE should be aborted because no-redirect-rules only allow WRITE.\n          val exception6 = intercept[DeltaIllegalStateException] {\n            sql(s\"delete from delta.`$source` where c0 = 1\")\n          }\n          assert(exception6.getErrorClass == \"DELTA_NO_REDIRECT_RULES_VIOLATED\")\n\n          // 3.4. Disabling SKIP_REDIRECT_FEATURE should allow all DMLs to pass.\n          withSQLConf(DeltaSQLConf.SKIP_REDIRECT_FEATURE.key -> \"true\") {\n            sql(s\"insert into delta.`$source` values(1),(2),(3),(4),(5),(6)\")\n            sql(s\"update delta.`$source` set c0 = 100\")\n            sql(s\"delete from delta.`$source` where c0 = 1\")\n          }\n      }\n    }\n\n    def alterRedirect(\n        table: String,\n        redirectType: String,\n        redirectState: RedirectState,\n        spec: RedirectSpec,\n        noRedirectRules: Set[NoRedirectRule]\n    ): Unit = {\n      val enableConfig = TableRedirectConfiguration(\n        redirectType,\n        redirectState.name,\n        JsonUtils.toJson(spec),\n        noRedirectRules\n      )\n      val enableConfigJson = StringEscapeUtils.escapeJson(JsonUtils.toJson(enableConfig))\n      sql(s\"alter table $table set TBLPROPERTIES('$featureName' = '$enableConfigJson')\")\n    }\n\n    redirectTest(s\"Redirect $featureName: modify table property\", enableRedirect = true) {\n      case (deltaLog, source, dest, catalogTable) =>\n          val redirectSpec = new PathBasedRedirectSpec(\n            source.getCanonicalPath,\n            dest.getCanonicalPath\n          )\n          val redirectType = PathBasedRedirectSpec.REDIRECT_TYPE\n          val destPath = dest.toString\n          val srcPath = source.toString\n          sql(s\"CREATE external TABLE t2(c0 long) USING delta LOCATION '$dest';\")\n          sql(s\"insert into t2 values(1),(2),(3),(4),(5)\")\n          val destTable = s\"delta.`$destPath`\"\n          val srcTable = s\"delta.`$srcPath`\"\n          // Initialize the redirection by moving table into EnableRedirectInProgress state.\n          alterRedirect(srcTable, redirectType, EnableRedirectInProgress, redirectSpec, Set.empty)\n          alterRedirect(destTable, redirectType, EnableRedirectInProgress, redirectSpec, Set.empty)\n          // Delta log is cloned, then moves both redirect destination table and redirect source\n          // table to RedirectReady state.\n          alterRedirect(srcTable, redirectType, RedirectReady, redirectSpec, Set.empty)\n          alterRedirect(destTable, redirectType, RedirectReady, redirectSpec, Set.empty)\n          sql(s\"insert into $srcTable values(1), (2), (3)\")\n          sql(s\"insert into $destTable values(1), (2), (3)\")\n          sql(s\"insert into t1 values(1), (2), (3)\")\n          sql(s\"insert into t2 values(1), (2), (3)\")\n\n          var result = sql(\"select * from t1\").collect()\n          assert(result.length == 17)\n          result = sql(\"select * from t2\").collect()\n          assert(result.length == 17)\n          result = sql(s\"select * from $srcTable \").collect()\n          assert(result.length == 17)\n          result = sql(s\"select * from $destTable \").collect()\n          assert(result.length == 17)\n          val root = new Path(catalogTable.location)\n          val fs = root.getFileSystem(deltaLog.newDeltaHadoopConf)\n          var files = fs.listStatus(new Path(srcPath + \"/_delta_log\"))\n            .filter(_.getPath.toString.endsWith(\".json\"))\n          assert(files.length == 3)\n          files = fs.listStatus(new Path(destPath + \"/_delta_log\"))\n            .filter(_.getPath.toString.endsWith(\".json\"))\n          assert(files.length == 8)\n          // Drop redirection by moving both redirect destination table and redirect source table to\n          // DropRedirectInProgress.\n          alterRedirect(destTable, redirectType, DropRedirectInProgress, redirectSpec, Set.empty)\n          alterRedirect(srcTable, redirectType, DropRedirectInProgress, redirectSpec, Set.empty)\n          // Remove table redirect feature from redirect source table and verify table content.\n          sql(s\"alter table $srcTable unset TBLPROPERTIES('$featureName')\")\n          result = sql(\"select * from t1\").collect()\n          assert(result.length == 0)\n          sql(\"insert into t1 values(1), (2), (3), (4)\")\n          result = sql(\"select * from t1\").collect()\n          assert(result.length == 4)\n    }\n  }\n\n  test(\"test getRedirectConfiguration\") {\n    val redirectSpec = new PathBasedRedirectSpec(\"sourcePath\", \"targetPath\")\n    val properties1 = RedirectReaderWriter.generateRedirectMetadata(\n      PathBasedRedirectSpec.REDIRECT_TYPE,\n      EnableRedirectInProgress,\n      redirectSpec,\n      noRedirectRules = Set.empty)\n    val properties2 = RedirectWriterOnly.generateRedirectMetadata(\n      PathBasedRedirectSpec.REDIRECT_TYPE,\n      RedirectReady,\n      redirectSpec,\n      noRedirectRules = Set.empty)\n\n    val configuration = RedirectFeature.getRedirectConfiguration(properties1 ++ properties2)\n    assert(configuration.isDefined)\n    // redirect-reader-writer should be preferred over redirect-writer-only.\n    assert(JsonUtils.toJson(configuration.get) ==\n      properties1(DeltaConfigs.REDIRECT_READER_WRITER.key))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/TightBoundsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport scala.collection.mutable.ArrayBuffer\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.DeltaStatistics.{MIN, NULL_COUNT, NUM_RECORDS, TIGHT_BOUNDS}\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.databind.node.ObjectNode\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions.{col, lit, map_values, when}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass TightBoundsSuite\n    extends QueryTest\n    with SharedSparkSession\n    with DeletionVectorsTestUtils\n    with DeltaSQLCommandTest {\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark.conf)\n  }\n\n  test(\"Validate TIGHT_BOUND column\") {\n    val targetDF = createTestDF(0, 100, 2)\n    val sourceDF = targetDF\n\n    def runDelete(target: io.delta.tables.DeltaTable): Int = {\n      target.delete(\"id >= 75\")\n      2 // Expected number of files.\n    }\n\n    val operations = ArrayBuffer[io.delta.tables.DeltaTable => Int](runDelete)\n    for {\n      // Make sure it works for all operations that add DVs\n      runOperation <- operations\n      // Make sure tightBounds update is backwards compatible\n      tightBoundDisabled <- BOOLEAN_DOMAIN\n    } {\n      val conf = Seq(\n        DeltaSQLConf.TIGHT_BOUND_COLUMN_ON_FILE_INIT_DISABLED.key -> tightBoundDisabled.toString)\n\n      withSQLConf(conf: _*) {\n        withTempDeltaTable(targetDF) { (targetTable, targetLog) =>\n          val snapshotBeforeOperation = targetLog.update()\n          val statsColumnName = snapshotBeforeOperation.getBaseStatsColumnName\n          val tightBoundsValuesBeforeOperation = snapshotBeforeOperation.withStatsDeduplicated\n            .select(col(s\"${statsColumnName}.$TIGHT_BOUNDS\"))\n            .collect()\n\n          assert(tightBoundsValuesBeforeOperation.length === 2)\n          val expectedTightBoundsValue = if (tightBoundDisabled) \"[null]\" else \"[true]\"\n          tightBoundsValuesBeforeOperation\n            .foreach(r => assert(r.toString == expectedTightBoundsValue))\n\n          val expectedNumberOfFiles = runOperation(targetTable())\n          // All operations only touch the second file.\n          assert(getFilesWithDeletionVectors(targetLog).size == 1)\n\n          val snapshotAfterOperation = targetLog.update()\n          val tightBoundsValuesAfterOperation = snapshotAfterOperation.withStatsDeduplicated\n            // Order by returns non-null DVs last. Thus, the file with the wide bounds\n            // should be the last one.\n            .orderBy(col(\"deletionVector\").asc_nulls_first)\n            .select(col(s\"${statsColumnName}.$TIGHT_BOUNDS\"))\n            .collect()\n\n          // Make sure tightsBounds is generated even for files that initially\n          // did not contain the column. Note, we expect 2 files each from merge and delete\n          // operations and three from update. This is because update creates a new file for the\n          // updated rows.\n          assert(tightBoundsValuesAfterOperation.length === expectedNumberOfFiles)\n          assert(tightBoundsValuesAfterOperation.head.toString === expectedTightBoundsValue)\n          assert(tightBoundsValuesAfterOperation.last.toString === \"[false]\")\n        }\n      }\n    }\n  }\n\n  test(\"Verify exception is thrown if we commit files with DVs and tight bounds\") {\n    val targetDF = createTestDF(0, 100, 2)\n    withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n      // Remove one record from each file.\n      targetTable().delete(\"id in (0, 50)\")\n      verifyDVsExist(targetLog, 2)\n\n      // Commit actions with DVs and tight bounds.\n      val txn = targetLog.startTransaction()\n      val addFiles = txn.snapshot.allFiles.collect().toSeq.map { action =>\n        action.copy(stats =\n          s\"\"\"{\"${NUM_RECORDS}\":${action.numPhysicalRecords.get},\n             | \"${TIGHT_BOUNDS}\":true}\"\"\".stripMargin)\n      }\n\n      val exception = intercept[DeltaIllegalStateException] {\n        txn.commitActions(DeltaOperations.TestOperation(), addFiles: _*)\n      }\n      assert(exception.getErrorClass ===\n        \"DELTA_ADDING_DELETION_VECTORS_WITH_TIGHT_BOUNDS_DISALLOWED\")\n    }\n  }\n\n  protected def getStats(snapshot: Snapshot, statName: String): Array[Row] = {\n    val statsColumnName = snapshot.getBaseStatsColumnName\n    snapshot\n      .withStatsDeduplicated\n      .select(s\"$statsColumnName.$statName\")\n      .collect()\n  }\n\n  protected def getStatFromLastFile(snapshot: Snapshot, statName: String): Row = {\n    val statsColumnName = snapshot.getBaseStatsColumnName\n    snapshot\n      .withStatsDeduplicated\n      .select(s\"$statsColumnName.$statName\")\n      .orderBy(s\"$statsColumnName.$MIN\")\n      .collect()\n      .last\n  }\n\n  protected def getStatFromLastFileWithDVs(snapshot: Snapshot, statName: String): Row = {\n    val statsColumnName = snapshot.getBaseStatsColumnName\n    snapshot\n      .withStatsDeduplicated\n      .filter(\"isNotNull(deletionVector)\")\n      .select(s\"$statsColumnName.$statName\")\n      .collect()\n      .last\n  }\n\n  /**\n   * Helper method that returns stats for every file in the snapshot as row objects.\n   *\n   * Return value schema is {\n   *  numRecords: Int,\n   *  RminValues: Row(Int, Int, ...), // Min value for each column\n   *  maxValues: Row(Int, Int, ...), // Max value for each column\n   *  nullCount: Row(Int, Int, ...), // Null count for each column\n   *  tightBounds: boolean\n   * }\n   */\n  protected def getStatsInPartitionOrder(snapshot: Snapshot): Array[Row] = {\n    val statsColumnName = snapshot.getBaseStatsColumnName\n    snapshot\n      .withStatsDeduplicated\n      .orderBy(map_values(col(\"partitionValues\")))\n      .select(s\"$statsColumnName.*\")\n      .collect()\n  }\n\n  protected def getNullCountFromFirstFileWithDVs(snapshot: Snapshot): Row = {\n    // Note, struct columns in Spark are returned with datatype Row.\n    getStatFromLastFile(snapshot, NULL_COUNT)\n      .getAs[Row](NULL_COUNT)\n  }\n\n  test(\"NULL COUNT is updated correctly when all values are nulls\"\n  ) {\n    val targetDF = spark.range(0, 100, 1, 2)\n      .withColumn(\"value\", when(col(\"id\") < 25, col(\"id\"))\n        .otherwise(null))\n\n      withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n        targetTable().delete(\"id >= 80\")\n        assert(getNullCountFromFirstFileWithDVs(targetLog.update()) === Row(0, 50))\n\n        targetTable().delete(\"id >= 70\")\n        assert(getNullCountFromFirstFileWithDVs(targetLog.update()) === Row(0, 50))\n      }\n  }\n\n  test(\"NULL COUNT is updated correctly where there are no nulls\"\n  ) {\n    val targetDF = spark.range(0, 100, 1, 2)\n      .withColumn(\"value\", col(\"id\"))\n\n      withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n        val expectedResult = Row(0, 0)\n        targetTable().delete(\"id >= 80\")\n        assert(getNullCountFromFirstFileWithDVs(targetLog.update()) === expectedResult)\n\n        targetTable().delete(\"id >= 70\")\n        assert(getNullCountFromFirstFileWithDVs(targetLog.update()) === expectedResult)\n      }\n  }\n\n  test(\"NULL COUNT is updated correctly when some values are nulls\"\n  ) {\n    val targetDF = spark.range(0, 100, 1, 2)\n      .withColumn(\"value\", when(col(\"id\") < 75, col(\"id\"))\n        .otherwise(null))\n\n      withTempDeltaTable(targetDF, enableDVs = true) { (targetTable, targetLog) =>\n        targetTable().delete(\"id >= 80\")\n        assert(getNullCountFromFirstFileWithDVs(targetLog.update()) === Row(0, 25))\n\n        targetTable().delete(\"id >= 70\")\n        assert(getNullCountFromFirstFileWithDVs(targetLog.update()) === Row(0, 25))\n      }\n  }\n\n  test(\"DML operations fetch stats on tables with partial stats\") {\n    val targetDF = createTestDF(0, 200, 4)\n      .withColumn(\"v\", col(\"id\"))\n      .withColumn(\"partCol\", (col(\"id\") / lit(50)).cast(\"Int\"))\n\n    val conf = Seq(DeltaSQLConf.DELTA_COLLECT_STATS.key -> false.toString)\n    withTempDeltaTable(targetDF, Seq(\"partCol\"), conf = conf) { (targetTable, targetLog) =>\n      val statsBeforeFirstDelete = getStatsInPartitionOrder(targetLog.update())\n      val expectedStatsBeforeFirstDelete = Seq(\n        Row(null, null, null, null, null), // File 1.\n        Row(null, null, null, null, null), // File 2.\n        Row(null, null, null, null, null), // File 3.\n        Row(null, null, null, null, null) // File 4.\n      )\n      assert(statsBeforeFirstDelete === expectedStatsBeforeFirstDelete)\n\n      // This operation touches files 2 and 3. Files 1 and 4 should still have not stats.\n      targetTable().delete(\"id in (50, 100)\")\n\n      // Expect the stats for every file that got a DV added to it with tightBounds = false\n      val statsAfterFirstDelete = getStatsInPartitionOrder(targetLog.update())\n      val expectedStatsAfterFirstDelete = Seq(\n        Row(null, null, null, null, null), // File 1.\n        Row(50, Row(50, 50), Row(99, 99), Row(0, 0), false), // File 2.\n        Row(50, Row(100, 100), Row(149, 149), Row(0, 0), false), // File 3.\n        Row(null, null, null, null, null) // File 4.\n      )\n      assert(statsAfterFirstDelete === expectedStatsAfterFirstDelete)\n    }\n  }\n\n  test(\"Update file without minValue and maxValue stats to wide bounds\") {\n    // The table has only binary columns, for which Delta does not collect minValue or maxValue\n    // stats. The file stats should still include numRecords, nullCount, and tightBounds.\n    withTempDeltaTable(\n      dataDF = spark.range(0, 10, 1, 1).toDF(\"id\")\n        .select(col(\"id\").cast(\"string\").cast(\"binary\").as(\"b\")),\n      enableDVs = true\n    ) { (targetTable, targetLog) =>\n      val statsBeforeDelete = getStatsInPartitionOrder(targetLog.update())\n      val expectedStatsBeforeDelete = Seq(Row(10, Row(0), true))\n      assert(statsBeforeDelete === expectedStatsBeforeDelete)\n\n      // The DELETE command updates file stats to wide bounds.\n      targetTable().delete(col(\"b\") === lit(\"1\").cast(\"string\").cast(\"binary\"))\n\n      val statsAfterDelete = getStatsInPartitionOrder(targetLog.update())\n      val expectedStatsAfterDelete = Seq(Row(10, Row(0), false))\n      assert(statsAfterDelete === expectedStatsAfterDelete)\n    }\n  }\n\n  test(\"Update file without column stats to wide bounds\") {\n    // We disable gathering stats for any of the columns in this table.\n    // In this case, the file stats should include numRecords and tightBounds only,\n    // but not minValue, maxValue or nullCount.\n    withTempDeltaTable(\n      dataDF = spark.range(0, 10, 1, 1).toDF(\"id\"),\n      conf = Map(DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.defaultTablePropertyKey -> \"0\").toSeq,\n      enableDVs = true\n    ) { (targetTable, targetLog) =>\n      val statsBeforeDelete = getStatsInPartitionOrder(targetLog.update())\n      val expectedStatsBeforeDelete = Seq(Row(10, true))\n      assert(statsBeforeDelete === expectedStatsBeforeDelete)\n\n      // The DELETE command updates file stats to wide bounds.\n      targetTable().delete(\"id = 1\")\n\n      val statsAfterDelete = getStatsInPartitionOrder(targetLog.update())\n      val expectedStatsAfterDelete = Seq(Row(10, false))\n      assert(statsAfterDelete === expectedStatsAfterDelete)\n    }\n  }\n\n  def tableAddDVAndTightStats(\n      targetTable: () => io.delta.tables.DeltaTable,\n      targetLog: DeltaLog,\n      deleteCond: String): Unit = {\n    // Add DVs. Stats should have tightBounds = false afterwards.\n    targetTable().delete(deleteCond)\n    val initialStats = getStats(targetLog.update(), \"*\")\n    assert(initialStats.forall(_.get(4) === false)) // tightBounds\n\n    // Other systems may support Compute Stats that recomputes tightBounds stats on tables with DVs.\n    // Simulate this with a manual update commit that introduces tight stats.\n    val txn = targetLog.startTransaction()\n    val addFiles = txn.snapshot.allFiles.collect().toSeq.map { action =>\n      val node = JsonUtils.mapper.readTree(action.stats).asInstanceOf[ObjectNode]\n      assert(node.has(\"numRecords\"))\n      val numRecords = node.get(\"numRecords\").asInt()\n      action.copy(stats = s\"\"\"{ \"numRecords\" : $numRecords, \"tightBounds\" : true }\"\"\")\n    }\n    txn.commitActions(DeltaOperations.ManualUpdate, addFiles: _*)\n  }\n\n  test(\"CLONE on table with DVs and tightBound stats\") {\n    val targetDF = spark.range(0, 100, 1, 1).toDF()\n    withTempDeltaTable(targetDF) { (targetTable, targetLog) =>\n      val targetPath = targetLog.dataPath.toString\n      tableAddDVAndTightStats(targetTable, targetLog, \"id >= 80\")\n      // CLONE shouldn't throw\n      // DELTA_ADDING_DELETION_VECTORS_WITH_TIGHT_BOUNDS_DISALLOWED\n      withTempPath(\"cloned\") { clonedPath =>\n        sql(s\"CREATE TABLE delta.`$clonedPath` SHALLOW CLONE delta.`$targetPath`\")\n      }\n    }\n  }\n\n  test(\"RESTORE TABLE on table with DVs and tightBound stats\") {\n    val targetDF = spark.range(0, 100, 1, 1).toDF()\n    withTempDeltaTable(targetDF) { (targetTable, targetLog) =>\n      val targetPath = targetLog.dataPath.toString\n      // adds version 1 (delete) and 2 (compute stats)\n      tableAddDVAndTightStats(targetTable, targetLog, \"id >= 80\")\n      // adds version 3 (delete more)\n      targetTable().delete(\"id < 20\")\n      // Restore back to version 2 (after compute stats)\n      // After 2nd delete, new DVs are added to the file, so the restore will\n      // have to recommit the file with old DVs.\n      targetTable().restoreToVersion(2)\n      // Verify that the restored table has DVs and tight bounds.\n      val stats = getStatFromLastFileWithDVs(targetLog.update(), \"*\")\n      assert(stats.get(4) === true) // tightBounds\n    }\n  }\n\n  test(\"Row Tracking backfill on table with DVs and tightBound stats\") {\n    // Enabling Row Tracking and backfill shouldn't throw\n    // DELTA_ADDING_DELETION_VECTORS_WITH_TIGHT_BOUNDS_DISALLOWED\n    withSQLConf(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"false\") {\n      val targetDF = spark.range(0, 100, 1, 1).toDF()\n      withTempDeltaTable(targetDF) { (targetTable, targetLog) =>\n        val targetPath = targetLog.dataPath.toString\n        tableAddDVAndTightStats(targetTable, targetLog, \"id >= 80\")\n        // Make sure that we start with no RowTracking feature.\n        assert(!RowTracking.isSupported(targetLog.unsafeVolatileSnapshot.protocol))\n        assert(!RowId.isEnabled(targetLog.unsafeVolatileSnapshot.protocol,\n          targetLog.unsafeVolatileSnapshot.metadata))\n\n        sql(s\"ALTER TABLE delta.`$targetPath` SET TBLPROPERTIES \" +\n          \"('delta.enableRowTracking' = 'true')\")\n        assert(targetLog.history.getHistory(None)\n          .count(_.operation == DeltaOperations.ROW_TRACKING_BACKFILL_OPERATION_NAME) == 1)\n      }\n    }\n  }\n}\n\nclass TightBoundsColumnMappingSuite extends TightBoundsSuite with DeltaColumnMappingEnableIdMode\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/TimestampLocalFileSystem.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport java.net.URI\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{DelegateToFileSystem, Path, RawLocalFileSystem}\nimport org.apache.hadoop.fs.FileStatus\n\n/**\n * This custom fs implementation is used for testing the msync calling in HDFSLogStore writes.\n * If `msync` is not called, `listStatus` will return stale results.\n */\nclass TimestampLocalFileSystem extends RawLocalFileSystem {\n\n  private var uri: URI = _\n  private var latestTimestamp: Long = 0\n\n  override def getScheme: String = TimestampLocalFileSystem.scheme\n\n  override def initialize(name: URI, conf: Configuration): Unit = {\n    uri = URI.create(name.getScheme + \":///\")\n    super.initialize(name, conf)\n  }\n\n  override def getUri(): URI = if (uri == null) {\n    // RawLocalFileSystem's constructor will call this one before `initialize` is called.\n    // Just return the super's URI to avoid NPE.\n    super.getUri\n  } else {\n    uri\n  }\n\n  override def listStatus(path: Path): Array[FileStatus] = {\n    super.listStatus(path).filter(_.getModificationTime <= latestTimestamp)\n  }\n\n  override def msync(): Unit = {\n    latestTimestamp = System.currentTimeMillis()\n  }\n}\n\nclass TimestampAbstractFileSystem(uri: URI, conf: Configuration)\n    extends DelegateToFileSystem(\n      uri,\n      new TimestampLocalFileSystem,\n      conf,\n      TimestampLocalFileSystem.scheme,\n      false)\n\n/**\n * Singleton for BlockWritesLocalFileSystem used to initialize the file system countdown latch.\n */\nobject TimestampLocalFileSystem {\n  val scheme = \"ts\"\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/UCManagedTableKillSwitchSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.DomainMetadata\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumn}\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\n/**\n * Unit tests for the kill switch that blocks clustering column changes on UC-managed\n * CatalogOwned tables. These tests bypass UCSingleCatalog (only available in sparkUnityCatalog)\n * by overriding [[OptimisticTransaction.isUCManagedTable]] in a test-local subclass.\n *\n * The kill switch fires in [[OptimisticTransaction.commitLarge]], which is used by RESTORE TABLE\n * and bypasses prepareCommit.\n */\nclass UCManagedTableKillSwitchSuite\n  extends CatalogOwnedTestBaseSuite\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest {\n\n  // Enable CatalogOwned by default so every CREATE TABLE produces a CatalogOwned table.\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n\n  /**\n   * Test subclass that pretends it targets a UC-managed table, bypassing the real\n   * [[CatalogOwnedTableUtils.getCatalogName]] check that requires UCSingleCatalog.\n   */\n  private class UCManagedTxn(log: DeltaLog, snap: Snapshot)\n      extends OptimisticTransaction(log, None, snap) {\n    override protected[delta] lazy val isUCManagedTable: Boolean = true\n  }\n\n  private val clusteringOnId: Seq[DomainMetadata] =\n    Seq(ClusteredTableUtils.createDomainMetadata(Seq(ClusteringColumn(Seq(\"id\")))))\n\n  private val clusteringOnName: Seq[DomainMetadata] =\n    Seq(ClusteredTableUtils.createDomainMetadata(Seq(ClusteringColumn(Seq(\"name\")))))\n\n  /** Creates a CatalogOwned clustered-by-id table and returns its (log, snapshot). */\n  private def createClusteredTable(tableName: String): (DeltaLog, Snapshot) = {\n    sql(s\"CREATE TABLE $tableName (id INT, name STRING) USING delta \" +\n      s\"CLUSTER BY (id) TBLPROPERTIES ('delta.feature.catalogManaged' = 'supported')\")\n    sql(s\"INSERT INTO $tableName VALUES (1, 'a')\")\n    val (log, snap) =\n      DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n    assert(snap.isCatalogOwned, \"table should be CatalogOwned\")\n    (log, snap)\n  }\n\n  test(\"commitLarge blocks clustering change on UC-managed CatalogOwned table\") {\n    withTable(\"tbl\") {\n      val (log, snap) = createClusteredTable(\"tbl\")\n      val ex = intercept[DeltaAnalysisException] {\n        new UCManagedTxn(log, snap).commitLarge(\n          spark,\n          clusteringOnName.iterator,\n          newProtocolOpt = None,\n          op = DeltaOperations.Restore(Some(0L), None),\n          context = Map.empty,\n          metrics = Map.empty)\n      }\n      assert(ex.getMessage.contains(\"Clustering column changes on Unity Catalog managed tables\"))\n    }\n  }\n\n  test(\"commit with unchanged clustering is allowed on UC-managed CatalogOwned table\") {\n    withTable(\"tbl\") {\n      val (log, snap) = createClusteredTable(\"tbl\")\n      // Commit the same clustering DomainMetadata that the snapshot already has - must not throw.\n      new UCManagedTxn(log, snap).commit(clusteringOnId, DeltaOperations.ManualUpdate)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/UniversalFormatSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.Utils.try_element_at\n\nimport org.apache.spark.sql.{DataFrameWriter, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SQLTestUtils\n\ntrait UniversalFormatTestHelper {\n  val allCompatObjects: Seq[IcebergCompatBase] =\n    Seq(\n      IcebergCompatV1,\n      IcebergCompatV2\n    )\n  def compatObjectFromVersion(version: Int): IcebergCompatBase =\n    allCompatObjects(version - 1)\n\n  def getCompatVersionOtherThan(version: Int): Int = {\n    val targetVersion = getCompatVersionsOtherThan(version).head\n    assert(targetVersion != version)\n    targetVersion\n  }\n\n  def getCompatVersionsOtherThan(version: Int): Seq[Int] = {\n    allCompatObjects\n      .filter(_.version != version)\n      .map(_.version.toInt)\n  }\n}\n\ntrait UniversalFormatSuiteBase extends IcebergCompatUtilsBase\n  with UniversalFormatTestHelper {\n\n  protected def assertUniFormIcebergProtocolAndProperties(\n      tableId: String, compatVersion: Int = compatVersion): Unit = {\n    assertIcebergCompatProtocolAndProperties(tableId, compatObjectFromVersion(compatVersion))\n\n    val snapshot = DeltaLog.forTable(spark, TableIdentifier(tableId)).update()\n    assert(UniversalFormat.icebergEnabled(snapshot.metadata))\n  }\n\n  protected def getDfWriter(\n      colName: String,\n      mode: String,\n      enableUniform: Boolean = true): DataFrameWriter[Row] = {\n    var df = spark.range(10)\n      .toDF(colName)\n      .write\n      .mode(mode)\n      .format(\"delta\")\n    df = if (mode == \"overwrite\") df.option(\"overwriteSchema\", \"true\") else df\n    if (enableUniform) {\n      df.option(s\"delta.enableIcebergCompatV$compatVersion\", \"true\")\n      df.option(\"delta.universalFormat.enabledFormats\", \"iceberg\")\n    } else {\n      df\n    }\n  }\n\n  protected def assertAddFileIcebergCompatVersion(\n      snapshot: Snapshot,\n      icebergCompatVersion: Int,\n      count: Int): Unit = {\n    val addFilesWithTagCount = snapshot.allFiles\n      .select(\"tags\")\n      .where(try_element_at(col(\"tags\"), AddFile.Tags.ICEBERG_COMPAT_VERSION.name)\n        === s\"$icebergCompatVersion\")\n      .count()\n    assert(addFilesWithTagCount == count)\n  }\n\n  protected def runReorgTableForUpgradeUniform(\n      tableId: String,\n      icebergCompatVersion: Int = compatVersion): Unit = {\n    executeSql(s\"\"\"\n           | REORG TABLE $tableId APPLY\n           | (UPGRADE UNIFORM (ICEBERG_COMPAT_VERSION = $icebergCompatVersion))\n           |\"\"\".stripMargin)\n  }\n\n  protected def checkFileNotRewritten(\n      prevSnapshot: Snapshot,\n      currSnapshot: Snapshot): Unit = {\n    val prevFiles = prevSnapshot.allFiles.collect().map(f => (f.path, f.modificationTime))\n    val currFiles = currSnapshot.allFiles.collect().map(f => (f.path, f.modificationTime))\n\n    val unchangedFiles = currFiles.filter { case (path, time) =>\n      prevFiles.find(_._1 == path).exists(_._2 == time)\n    }\n    assert(unchangedFiles.length == currFiles.length)\n  }\n\n  test(\"create new UniForm table while manually enabling IcebergCompat\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n               |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n               |  'delta.universalFormat.enabledFormats' = 'iceberg',\n               |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n               |  'delta.minReaderVersion' = $r,\n               |  'delta.minWriterVersion' = $w\n               |)\"\"\".stripMargin)\n\n        assertUniFormIcebergProtocolAndProperties(id)\n      }\n    }\n  }\n\n  test(\"create new UniForm table while manually enabling IcebergCompat with no rw version\") {\n    withTempTableAndDir { case (id, _) =>\n      executeSql(s\"\"\"\n             |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n             |  'delta.universalFormat.enabledFormats' = 'iceberg',\n             |  'delta.enableIcebergCompatV$compatVersion' = 'true'\n             |)\"\"\".stripMargin)\n      assertUniFormIcebergProtocolAndProperties(id)\n    }\n  }\n\n  test(\"create new UniForm table via clone\") {\n    withTempTableAndDir { case (id, loc) =>\n      executeSql(s\"\"\"\n              |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n              | 'delta.columnMapping.mode' = 'name')\n              | \"\"\".stripMargin)\n      executeSql(s\"\"\"\n              |INSERT INTO $id values (1) \"\"\".stripMargin)\n      withTempTableAndDir { case (cloneId, _) =>\n        executeSql(s\"\"\"\n              |CREATE TABLE $cloneId SHALLOW CLONE $id TBLPROPERTIES (\n              |  'delta.universalFormat.enabledFormats' = 'iceberg',\n              |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n              |  'delta.columnMapping.mode' = 'name'\n              |) \"\"\".stripMargin)\n        assertUniFormIcebergProtocolAndProperties(cloneId)\n      }\n    }\n  }\n\n  test(\"enable UniForm on existing table with IcebergCompat enabled\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n               |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n               |  'delta.minReaderVersion' = $r,\n               |  'delta.minWriterVersion' = $w,\n               |  'delta.enableIcebergCompatV$compatVersion' = true\n               |)\"\"\".stripMargin)\n\n        executeSql(s\"ALTER TABLE $id SET TBLPROPERTIES \" +\n          s\"('delta.universalFormat.enabledFormats' = 'iceberg')\")\n\n        assertUniFormIcebergProtocolAndProperties(id)\n      }\n    }\n  }\n\n  test(\"enable UniForm on existing table without IcebergCompat\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n          |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n          |  'delta.minReaderVersion' = $r,\n          |  'delta.minWriterVersion' = $w\n          |)\"\"\".stripMargin)\n\n        executeSql(s\"ALTER TABLE $id SET TBLPROPERTIES \" +\n          s\"('delta.universalFormat.enabledFormats' = 'iceberg',\" +\n          s\" 'delta.columnMapping.mode' = 'name', \" +\n          s\" 'delta.enableIcebergCompatV$compatVersion' = true) \")\n\n        assertUniFormIcebergProtocolAndProperties(id)\n      }\n    }\n  }\n\n  test(\"enable UniForm on existing table with ColumnMapping\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n          |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n          |  'delta.minReaderVersion' = $r,\n          |  'delta.minWriterVersion' = $w,\n          |  'delta.columnMapping.mode' = 'name'\n          |)\"\"\".stripMargin)\n\n        executeSql(s\"ALTER TABLE $id SET TBLPROPERTIES \" +\n          s\"('delta.universalFormat.enabledFormats' = 'iceberg',\" +\n          s\" 'delta.enableIcebergCompatV$compatVersion' = true) \")\n        assertUniFormIcebergProtocolAndProperties(id)\n      }\n    }\n  }\n\n  test(\"enable UniForm on existing table but IcebergCompat isn't enabled - fail\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n               |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n               |  'delta.minReaderVersion' = $r,\n               |  'delta.minWriterVersion' = $w,\n               |  'delta.enableIcebergCompatV$compatVersion' = false,\n               |  'delta.feature.icebergCompatV$compatVersion' = 'supported'\n               |)\"\"\".stripMargin)\n\n        val e = intercept[DeltaUnsupportedOperationException] {\n          executeSql(s\"ALTER TABLE $id SET TBLPROPERTIES \" +\n            s\"('delta.universalFormat.enabledFormats' = 'iceberg')\")\n        }\n        assert(e.getErrorClass === \"DELTA_UNIVERSAL_FORMAT_VIOLATION\")\n      }\n    }\n  }\n\n  test(\"disabling UniForm will not disable IcebergCompat\") {\n    withTempTableAndDir { case (id, _) =>\n      executeSql(\n        s\"\"\"\n           |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'iceberg',\n           |  'delta.enableIcebergCompatV$compatVersion' = 'true'\n           |)\"\"\".stripMargin)\n\n      assertUniFormIcebergProtocolAndProperties(id)\n\n      executeSql(s\"ALTER TABLE $id UNSET TBLPROPERTIES ('delta.universalFormat.enabledFormats')\")\n\n      assert(getProperties(id)(s\"delta.enableIcebergCompatV$compatVersion\").toBoolean)\n    }\n  }\n\n  test(\"disabling IcebergCompat will disable UniForm if enabled\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n               |CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n               |  'delta.minReaderVersion' = $r,\n               |  'delta.minWriterVersion' = $w,\n               |  'delta.universalFormat.enabledFormats' = 'iceberg',\n               |  'delta.enableIcebergCompatV$compatVersion' = true\n               |)\"\"\".stripMargin)\n        var tableprops = getProperties(id)\n        assert(tableprops(\"delta.universalFormat.enabledFormats\") === \"iceberg\")\n        assert(tableprops(s\"delta.enableIcebergCompatV$compatVersion\").toBoolean)\n\n        executeSql(s\"\"\"\n               |ALTER TABLE $id SET TBLPROPERTIES (\n               |'delta.enableIcebergCompatV$compatVersion' = false)\n               |\"\"\".stripMargin)\n\n        tableprops = getProperties(id)\n        assert(!tableprops.contains(\"delta.universalFormat.enabledFormats\"))\n        assert(!tableprops(s\"delta.enableIcebergCompatV$compatVersion\").toBoolean)\n      }\n    }\n  }\n}\n\ntrait UniFormWithIcebergCompatV1SuiteBase extends UniversalFormatSuiteBase {\n  protected override val compatObject: IcebergCompatBase = IcebergCompatV1\n\n  test(\"enable UniForm and V1 on existing table\") {\n    withTempTableAndDir { case (id, loc) =>\n      executeSql(s\"CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc\")\n\n      executeSql(s\"\"\"\n             |ALTER TABLE $id SET TBLPROPERTIES (\n             |  'delta.minReaderVersion' = 2,\n             |  'delta.minWriterVersion' = 5,\n             |  'delta.universalFormat.enabledFormats' = 'iceberg',\n             |  'delta.enableIcebergCompatV1' = true,\n             |  'delta.columnMapping.mode' = 'name'\n             |)\"\"\".stripMargin)\n\n      assertUniFormIcebergProtocolAndProperties(id)\n    }\n  }\n\n  test(\"REORG TABLE for table from icebergCompatVx to icebergCompatV1, should skip rewrite\") {\n    getCompatVersionsOtherThan(1).foreach(originalVersion => {\n      withTempTableAndDir { case (id, _) =>\n        executeSql(s\"\"\"\n               | CREATE TABLE $id (ID INT) USING DELTA TBLPROPERTIES (\n               |  'delta.universalFormat.enabledFormats' = 'iceberg',\n               |  'delta.enableIcebergCompatV$originalVersion' = 'true'\n               |)\n               | \"\"\".stripMargin)\n        executeSql(s\"\"\"\n               | INSERT INTO TABLE $id (ID)\n               | VALUES (1),(2),(3),(4),(5),(6),(7)\"\"\".stripMargin)\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(id))\n        val snapshot = deltaLog.update()\n        assertAddFileIcebergCompatVersion(\n          snapshot, icebergCompatVersion = originalVersion, count = 1)\n\n        runReorgTableForUpgradeUniform(id, icebergCompatVersion = 1)\n        val updatedSnapshot = deltaLog.update()\n        assert(updatedSnapshot.getProperties(\"delta.enableIcebergCompatV1\") === \"true\")\n        assertAddFileIcebergCompatVersion(\n          deltaLog.update(), icebergCompatVersion = originalVersion, count = 1)\n        checkFileNotRewritten(snapshot, updatedSnapshot)\n      }\n    })\n  }\n}\n\ntrait UniFormWithIcebergCompatV2SuiteBase extends UniversalFormatSuiteBase {\n  override val compatObject: IcebergCompatBase = IcebergCompatV2\n\n  test(\"can downgrade from V2 to V1 with ALTER with UniForm enabled\") {\n    withTempTableAndDir {\n      case (id, loc) =>\n        executeSql(s\"\"\"\n               |CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc TBLPROPERTIES (\n               |  'delta.enableIcebergCompatV2' = 'true',\n               |  'delta.universalFormat.enabledFormats' = 'iceberg'\n               |)\"\"\".stripMargin)\n        executeSql(s\"\"\"\n               |ALTER TABLE $id SET TBLPROPERTIES (\n               |  'delta.enableIcebergCompatV1' = true,\n               |  'delta.enableIcebergCompatV2' = false\n               |)\"\"\".stripMargin)\n        assertUniFormIcebergProtocolAndProperties(id, 1)\n    }\n  }\n\n  test(\"REORG TABLE for table from icebergCompatVx to icebergCompatV2\") {\n    val originalVersion = 1\n    withTempTableAndDir { case (id, loc) =>\n      executeSql(s\"\"\"\n           | CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc TBLPROPERTIES (\n           |  'delta.universalFormat.enabledFormats' = 'iceberg',\n           |  'delta.enableIcebergCompatV$originalVersion' = 'true'\n           |)\"\"\".stripMargin)\n      executeSql(s\"\"\"\n           | INSERT INTO TABLE $id (ID)\n           | VALUES (1),(2),(3),(4),(5),(6),(7)\"\"\".stripMargin)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(id))\n      val snapshot1 = deltaLog.update()\n      assert(snapshot1.allFiles.collect().nonEmpty)\n      assertAddFileIcebergCompatVersion(snapshot1, icebergCompatVersion = 2, count = 0)\n\n      runReorgTableForUpgradeUniform(id, icebergCompatVersion = 2)\n      val snapshot2 = deltaLog.update()\n      assert(snapshot2.getProperties(\"delta.enableIcebergCompatV2\") === \"true\")\n      assert(snapshot2.getProperties(\"delta.enableDeletionVectors\") === \"false\")\n      assertAddFileIcebergCompatVersion(snapshot2, icebergCompatVersion = 2, count = 1)\n    }\n  }\n\n  test(\n    \"REORG TABLE: new files would have ICEBERG_COMPAT_VERSION tag if enableIcebergCompat is on\") {\n    withTempTableAndDir { case (id, loc) =>\n      executeSql(\n        s\"\"\"\n           | CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc TBLPROPERTIES (\n           | 'delta.columnMapping.mode' = 'name'\n           |)\n           | \"\"\".stripMargin)\n      executeSql(\n        s\"\"\"\n           | INSERT INTO TABLE $id (ID)\n           | VALUES (1),(2),(3),(4),(5),(6),(7)\"\"\".stripMargin)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(id))\n      val txn = deltaLog.startTransaction()\n      val metadata = txn.metadata\n      val enableIcebergCompatConf = Map(\n        DeltaConfigs.ICEBERG_COMPAT_V1_ENABLED.key -> \"false\",\n        DeltaConfigs.ICEBERG_COMPAT_V2_ENABLED.key -> \"true\")\n      val newMetadata = metadata.copy(\n        configuration = metadata.configuration ++ enableIcebergCompatConf)\n      txn.updateMetadata(newMetadata)\n      txn.commit(\n        Nil,\n        DeltaOperations.UpgradeUniformProperties(enableIcebergCompatConf)\n      )\n      assertAddFileIcebergCompatVersion(\n        deltaLog.update(), icebergCompatVersion = 2, count = 0)\n\n      // The new file would have the ICEBERG_COMPAT_VERSION tag while the exist files would not\n      executeSql(s\"\"\"\n             | INSERT INTO TABLE $id (ID)\n             | VALUES (8),(9),(10)\"\"\".stripMargin)\n      assertAddFileIcebergCompatVersion(\n        deltaLog.update(), icebergCompatVersion = 2, count = 1)\n\n      // After REORG TABLE command, all the exist files would have ICEBERG_COMPAT_VERSION tag\n      runReorgTableForUpgradeUniform(id, 2)\n      val finalSnapshot = deltaLog.update()\n      assert(finalSnapshot.getProperties(\"delta.enableIcebergCompatV2\") === \"true\")\n      assertAddFileIcebergCompatVersion(finalSnapshot, icebergCompatVersion = 2, count = 2)\n    }\n  }\n}\n\ntrait UniversalFormatMiscSuiteBase extends IcebergCompatUtilsBase with UniversalFormatTestHelper {\n  test(\"enforceInvariantsAndDependenciesForCTAS\") {\n    withTempTableAndDir { case (id, _) =>\n      executeSql(s\"CREATE TABLE $id (id INT) USING DELTA\")\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(id))\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(id))\n      var configurationUnderTest = Map(\"dummykey1\" -> \"dummyval1\", \"dummykey2\" -> \"dummyval2\")\n      // The enforce is not lossy. It will do nothing if there is no Universal related key.\n\n      def getUpdatedConfiguration(conf: Map[String, String]): Map[String, String] =\n        UniversalFormat.enforceDependenciesInConfiguration(spark,\n            catalogTable = catalogTable, conf, snapshot)\n\n      var updatedConfiguration = getUpdatedConfiguration(configurationUnderTest)\n      assert(configurationUnderTest == configurationUnderTest)\n\n      configurationUnderTest = Map(\n        \"delta.universalFormat.enabledFormats\" -> \"iceberg\",\n        \"dummykey\" -> \"dummyvalue\"\n      )\n      val e = intercept[DeltaUnsupportedOperationException] {\n        updatedConfiguration = getUpdatedConfiguration(configurationUnderTest)\n      }\n      assert(e.getErrorClass == \"DELTA_UNIVERSAL_FORMAT_VIOLATION\")\n\n      for (icv <- allCompatObjects.map(_.version)) {\n        configurationUnderTest = Map(\n          s\"delta.enableIcebergCompatV$icv\" -> \"true\",\n          \"delta.universalFormat.enabledFormats\" -> \"iceberg\",\n          \"dummykey\" -> \"dummyvalue\"\n        )\n        updatedConfiguration = getUpdatedConfiguration(configurationUnderTest)\n\n        assert(updatedConfiguration.size == 5)\n        assert(updatedConfiguration(\"dummykey\") == \"dummyvalue\")\n        assert(updatedConfiguration(\"delta.universalFormat.enabledFormats\") == \"iceberg\")\n        assert(updatedConfiguration(\"delta.columnMapping.mode\") == \"name\")\n        assert(updatedConfiguration(s\"delta.enableIcebergCompatV$icv\") == \"true\")\n        assert(updatedConfiguration(\"delta.columnMapping.maxColumnId\") == \"1\")\n\n        configurationUnderTest = Map(\n          s\"delta.enableIcebergCompatV$icv\" -> \"true\",\n          \"delta.universalFormat.enabledFormats\" -> \"iceberg\",\n          \"dummykey\" -> \"dummyvalue\",\n          \"delta.columnMapping.mode\" -> \"id\"\n        )\n        updatedConfiguration = getUpdatedConfiguration(configurationUnderTest)\n        assert(updatedConfiguration.size == 4)\n        assert(updatedConfiguration(\"dummykey\") == \"dummyvalue\")\n        assert(updatedConfiguration(\"delta.columnMapping.mode\") == \"id\")\n        assert(updatedConfiguration(\"delta.universalFormat.enabledFormats\") == \"iceberg\")\n        assert(updatedConfiguration(s\"delta.enableIcebergCompatV$icv\") == \"true\")\n      }\n    }\n  }\n\n  test(\"UniForm config validation\") {\n    Seq(\"ICEBERG\", \"iceberg,iceberg\", \"iceber\", \"paimon\").foreach { invalidConf =>\n      withTempTableAndDir { case (id, loc) =>\n        val errMsg = intercept[IllegalArgumentException] {\n          executeSql(s\"\"\"\n                 |CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc TBLPROPERTIES (\n                 |  'delta.universalFormat.enabledFormats' = '$invalidConf',\n                 |  'delta.enableIcebergCompatV1' = 'true',\n                 |  'delta.columnMapping.mode' = 'name'\n                 |)\"\"\".stripMargin)\n        }.getMessage\n        assert(\n          errMsg.contains(\"Must be a comma-separated list of formats from the list\"),\n          errMsg\n        )\n      }\n    }\n  }\n\n  test(\"create new UniForm table without manually enabling IcebergCompat - fail\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, loc) =>\n        val e = intercept[DeltaUnsupportedOperationException] {\n          executeSql(s\"\"\"\n                 |CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc TBLPROPERTIES (\n                 |  'delta.universalFormat.enabledFormats' = 'iceberg',\n                 |  'delta.minReaderVersion' = $r,\n                 |  'delta.minWriterVersion' = $w\n                 |)\"\"\".stripMargin)\n        }\n        assert(e.getErrorClass == \"DELTA_UNIVERSAL_FORMAT_VIOLATION\")\n\n        val e1 = intercept[DeltaUnsupportedOperationException] {\n          executeSql(s\"\"\"\n                 |CREATE TABLE $id USING DELTA LOCATION $loc TBLPROPERTIES (\n                 |  'delta.universalFormat.enabledFormats' = 'iceberg',\n                 |  'delta.minReaderVersion' = $r,\n                 |  'delta.minWriterVersion' = $w\n                 |) AS SELECT 1\"\"\".stripMargin)\n        }\n        assert(e1.getErrorClass == \"DELTA_UNIVERSAL_FORMAT_VIOLATION\")\n      }\n    }\n  }\n\n  test(\"enable UniForm on existing table but IcebergCompat isn't enabled - fail\") {\n    allReaderWriterVersions.foreach { case (r, w) =>\n      withTempTableAndDir { case (id, loc) =>\n        executeSql(s\"\"\"\n               |CREATE TABLE $id (ID INT) USING DELTA LOCATION $loc TBLPROPERTIES (\n               |  'delta.minReaderVersion' = $r,\n               |  'delta.minWriterVersion' = $w\n               |)\"\"\".stripMargin)\n\n        val e = intercept[DeltaUnsupportedOperationException] {\n          executeSql(s\"ALTER TABLE $id SET TBLPROPERTIES \" +\n            s\"('delta.universalFormat.enabledFormats' = 'iceberg')\")\n        }\n        assert(e.getErrorClass === \"DELTA_UNIVERSAL_FORMAT_VIOLATION\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/UpdateMetricsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.DatabricksLogging\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{Dataset, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.expr\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Tests for metrics of Delta UPDATE command.\n */\nclass UpdateMetricsSuite extends QueryTest\n  with SharedSparkSession\n  with DatabricksLogging\n  with DeltaSQLCommandTest {\n\n\n  /**\n   * Case class to parameterize tests.\n   */\n  case class TestConfiguration(\n      partitioned: Boolean,\n      cdfEnabled: Boolean\n  )\n\n  /**\n   * Case class to parameterize metric results.\n   */\n  case class TestMetricResults(\n      operationMetrics: Map[String, Long]\n  )\n\n  /**\n   * Helper to generate tests for all configuration parameters.\n   */\n  protected def testUpdateMetrics(name: String)(testFn: TestConfiguration => Unit): Unit = {\n    for {\n      partitioned <- BOOLEAN_DOMAIN\n      cdfEnabled <- Seq(false)\n    } {\n      val testConfig =\n        TestConfiguration(partitioned = partitioned,\n          cdfEnabled = cdfEnabled\n        )\n      var testName =\n        s\"update-metrics: $name - Partitioned = $partitioned, cdfEnabled = $cdfEnabled\"\n      test(testName) {\n        testFn(testConfig)\n      }\n    }\n  }\n\n\n  /**\n   * Create a table from the provided dataset.\n   *\n   * If an partitioned table is needed, then we create one data partition per Spark partition,\n   * i.e. every data partition will contain one file.\n   *\n   * Also an extra column is added to be used in non-partition filters.\n   */\n  protected def createTempTable(\n      table: Dataset[_],\n      tableName: String,\n      testConfig: TestConfiguration): Unit = {\n    val numRows = table.count()\n    val numPartitions = table.rdd.getNumPartitions\n    val numRowsPerPart = if (numRows > 0 && numPartitions < numRows) {\n      numRows / numPartitions\n    } else {\n      1\n    }\n    val partitionBy = if (testConfig.partitioned) {\n      Seq(\"partCol\")\n    } else {\n      Seq()\n    }\n    table.withColumn(\"partCol\", expr(s\"floor(id / $numRowsPerPart)\"))\n      .withColumn(\"extraCol\", expr(s\"$numRows - id\"))\n      .write\n      .partitionBy(partitionBy: _*)\n      .format(\"delta\")\n      .saveAsTable(tableName)\n  }\n\n  /**\n   * Run an update command and capture operation metrics from Delta log.\n   *\n   */\n  private def runUpdateAndCaptureMetrics(\n      table: Dataset[_],\n      where: String,\n      testConfig: TestConfiguration): TestMetricResults = {\n    val tableName = \"target\"\n    val whereClause = if (where.nonEmpty) {\n      s\"WHERE $where\"\n    } else {\n      \"\"\n    }\n    var operationMetrics: Map[String, Long] = null\n    import testImplicits._\n    withSQLConf(\n      DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\",\n      DeltaSQLConf.DELTA_SKIP_RECORDING_EMPTY_COMMITS.key -> \"false\",\n      DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> testConfig.cdfEnabled.toString) {\n      withTable(tableName) {\n        createTempTable(table, tableName, testConfig)\n          val resultDf = spark.sql(s\"UPDATE $tableName SET id = -1 $whereClause\")\n        operationMetrics = DeltaMetricsUtils.getLastOperationMetrics(tableName)\n\n        // Check operation metrics against commit actions.\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        DeltaMetricsUtils.checkOperationMetricsAgainstCommitActions(\n          deltaLog, deltaLog.update().version, operationMetrics)\n      }\n    }\n    TestMetricResults(\n      operationMetrics\n    )\n  }\n\n  /**\n   * Run a update command and check all available metrics.\n   * We allow some metrics to be missing, by setting their value to -1.\n   */\n  private def runUpdateAndCheckMetrics(\n      table: Dataset[_],\n      where: String,\n      expectedOperationMetrics: Map[String, Long],\n      testConfig: TestConfiguration): Unit = {\n    // Run the update capture and get all metrics.\n    val results = runUpdateAndCaptureMetrics(table, where, testConfig)\n\n    // Check operation metrics schema.\n    val unknownKeys = results.operationMetrics.keySet -- DeltaOperationMetrics.UPDATE --\n      DeltaOperationMetrics.WRITE\n    assert(unknownKeys.isEmpty,\n      s\"Unknown operation metrics for UPDATE command: ${unknownKeys.mkString(\", \")}\")\n\n    // Check values of expected operation metrics. For all unspecified deterministic metrics,\n    // we implicitly expect a zero value.\n    val requiredMetrics = Set(\n      \"numCopiedRows\",\n      \"numUpdatedRows\",\n      \"numAddedFiles\",\n      \"numRemovedFiles\",\n      \"numAddedChangeFiles\")\n    val expectedMetricsWithDefaults =\n      requiredMetrics.map(k => k -> 0L).toMap ++ expectedOperationMetrics\n    val expectedMetricsFiltered = expectedMetricsWithDefaults.filter(_._2 >= 0)\n    DeltaMetricsUtils.checkOperationMetrics(\n      expectedMetrics = expectedMetricsFiltered,\n      operationMetrics = results.operationMetrics)\n\n\n    // Check time operation metrics.\n    val expectedTimeMetrics =\n      Set(\"scanTimeMs\", \"rewriteTimeMs\", \"executionTimeMs\").filter(\n        k => expectedOperationMetrics.get(k).forall(_ >= 0)\n      )\n    DeltaMetricsUtils.checkOperationTimeMetrics(\n      operationMetrics = results.operationMetrics,\n      expectedMetrics = expectedTimeMetrics)\n  }\n\n\n  for (whereClause <- Seq(\"\", \"1 = 1\")) {\n    testUpdateMetrics(s\"update all with where = '$whereClause'\") { testConfig =>\n      val numFiles = 5\n      val numRows = 100\n      val numAddedChangeFiles = if (testConfig.partitioned && testConfig.cdfEnabled) {\n        5\n      } else if (testConfig.cdfEnabled) {\n        2\n      } else {\n        0\n      }\n      runUpdateAndCheckMetrics(\n        table = spark.range(start = 0, end = numRows, step = 1, numPartitions = numFiles),\n        where = whereClause,\n        expectedOperationMetrics = Map(\n          \"numCopiedRows\" -> 0,\n          \"numUpdatedRows\" -> -1,\n          \"numOutputRows\" -> -1,\n          \"numFiles\" -> -1,\n          \"numAddedFiles\" -> -1,\n          \"numRemovedFiles\" -> numFiles,\n          \"numAddedChangeFiles\" -> numAddedChangeFiles\n        ),\n        testConfig = testConfig\n      )\n    }\n  }\n\n  testUpdateMetrics(\"update with false predicate\") { testConfig =>\n    runUpdateAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"1 != 1\",\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numUpdatedRows\" -> 0,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> 0,\n        \"numAddedChangeFiles\" -> 0,\n        \"scanTimeMs\" -> -1,\n        \"rewriteTimeMs\" -> -1,\n        \"executionTimeMs\" -> -1\n      ),\n      testConfig = testConfig\n    )\n  }\n\n  testUpdateMetrics(\"update with unsatisfied static predicate\") { testConfig =>\n    runUpdateAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id < 0 or id > 100\",\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numUpdatedRows\" -> 0,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> 0,\n        \"numAddedChangeFiles\" -> 0,\n        \"scanTimeMs\" -> -1,\n        \"rewriteTimeMs\" -> -1,\n        \"executionTimeMs\" -> -1\n      ),\n      testConfig = testConfig\n    )\n  }\n\n  testUpdateMetrics(\"update with unsatisfied dynamic predicate\") { testConfig =>\n    runUpdateAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id / 200 > 1 \",\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numUpdatedRows\" -> 0,\n        \"numAddedFiles\" -> 0,\n        \"numRemovedFiles\" -> 0,\n        \"numAddedChangeFiles\" -> 0,\n        \"scanTimeMs\" -> -1,\n        \"rewriteTimeMs\" -> -1,\n        \"executionTimeMs\" -> -1\n      ),\n      testConfig = testConfig\n    )\n  }\n\n  for (whereClause <- Seq(\"id = 0\", \"id >= 49 and id < 50\")) {\n    testUpdateMetrics(s\"update one row with where = `$whereClause`\") { testConfig =>\n      var numCopiedRows = 19\n      val numAddedFiles = 1\n      var numRemovedFiles = 1\n      runUpdateAndCheckMetrics(\n        table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n        where = whereClause,\n        expectedOperationMetrics = Map(\n          \"numCopiedRows\" -> numCopiedRows,\n          \"numUpdatedRows\" -> 1,\n          \"numAddedFiles\" -> numAddedFiles,\n          \"numRemovedFiles\" -> numRemovedFiles,\n          \"numAddedChangeFiles\" -> {\n            if (testConfig.cdfEnabled) {\n              1\n            } else {\n              0\n            }\n          }\n        ),\n        testConfig = testConfig\n      )\n    }\n  }\n\n  testUpdateMetrics(\"update one file\") { testConfig =>\n    runUpdateAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = 5),\n      where = \"id < 20\",\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> 0,\n        \"numUpdatedRows\" -> 20,\n        \"numAddedFiles\" -> 1,\n        \"numRemovedFiles\" -> 1,\n        \"numAddedChangeFiles\" -> {\n          if (testConfig.cdfEnabled) {\n            1\n          } else {\n            0\n          }\n        }\n      ),\n      testConfig = testConfig\n    )\n  }\n\n  testUpdateMetrics(\"update one row per file\") { testConfig =>\n    val numPartitions = 5\n    var numCopiedRows = 95\n    val numAddedFiles = if (testConfig.partitioned) 5 else 2\n    var numRemovedFiles = 5\n    var unpartitionedNumAddFiles = 2\n    runUpdateAndCheckMetrics(\n      table = spark.range(start = 0, end = 100, step = 1, numPartitions = numPartitions),\n      where = \"id in (5, 25, 45, 65, 85)\",\n      expectedOperationMetrics = Map(\n        \"numCopiedRows\" -> numCopiedRows,\n        \"numUpdatedRows\" -> 5,\n        \"numAddedFiles\" -> {\n          if (testConfig.partitioned) {\n            5\n          } else {\n            unpartitionedNumAddFiles\n          }\n        },\n        \"numRemovedFiles\" -> numRemovedFiles,\n        \"numAddedChangeFiles\" -> {\n          if (testConfig.cdfEnabled) {\n            if (testConfig.partitioned) {\n              5\n            } else {\n              unpartitionedNumAddFiles\n            }\n          } else {\n            0\n          }\n        }\n      ),\n      testConfig = testConfig\n    )\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/UpdateSQLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.{AddFile, FileAction, RemoveFile}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaExcludedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.errors.QueryExecutionErrors.toSQLType\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy\n\ntrait UpdateSQLMixin extends UpdateBaseMixin\n  with DeltaSQLCommandTest\n  with DeltaDMLTestUtils {\n\n  override protected def executeUpdate(\n      target: String,\n      set: String,\n      where: String = null): Unit = {\n    val whereClause = Option(where).map(c => s\"WHERE $c\").getOrElse(\"\")\n    sql(s\"UPDATE $target SET $set $whereClause\")\n  }\n}\n\ntrait UpdateSQLTests extends UpdateSQLMixin {\n  import testImplicits._\n\n  test(\"explain\") {\n    append(Seq((2, 2)).toDF(\"key\", \"value\"))\n    val df = sql(s\"EXPLAIN UPDATE $tableSQLIdentifier SET key = 1, value = 2 WHERE key = 2\")\n    val outputs = df.collect().map(_.mkString).mkString\n    assert(outputs.contains(\"Delta\"))\n    assert(!outputs.contains(\"index\") && !outputs.contains(\"ActionLog\"))\n    // no change should be made by explain\n    checkAnswer(readDeltaTableByIdentifier(), Row(2, 2))\n  }\n\n  test(\"SC-11376: Update command should check target columns during analysis, same key\") {\n    val targetDF = spark.read.json(\n      \"\"\"\n        {\"a\": {\"c\": {\"d\": 'random', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\"\n        .split(\"\\n\").toSeq.toDS())\n\n    testAnalysisException(\n      targetDF,\n      set = \"z = 30\" :: \"z = 40\" :: Nil,\n      errMsgs = \"There is a conflict from these SET columns\" :: Nil)\n\n    testAnalysisException(\n      targetDF,\n      set = \"a.c.d = 'rand'\" :: \"a.c.d = 'RANDOM2'\" :: Nil,\n      errMsgs = \"There is a conflict from these SET columns\" :: Nil)\n  }\n\n  test(\"update a dataset temp view\") {\n    withTable(\"tab\") {\n      withTempView(\"v\") {\n        Seq((0, 3)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        spark.table(\"tab\").as(\"name\").createTempView(\"v\")\n        sql(\"UPDATE v SET key = 1 WHERE key = 0 AND value = 3\")\n        checkAnswer(spark.table(\"tab\"), Row(1, 3))\n      }\n    }\n  }\n\n  test(\"update a SQL temp view\") {\n    withTable(\"tab\") {\n      withTempView(\"v\") {\n        Seq((0, 3)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        sql(\"CREATE TEMP VIEW v AS SELECT * FROM tab\")\n        QueryTest.checkAnswer(sql(\"UPDATE v SET key = 1 WHERE key = 0 AND value = 3\"), Seq(Row(1)))\n        checkAnswer(spark.table(\"tab\"), Row(1, 3))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { partitioned =>\n    test(s\"User defined _change_type column doesn't get dropped - partitioned=$partitioned\") {\n      withTable(\"tab\") {\n        sql(\n          s\"\"\"CREATE TABLE tab USING DELTA\n             |${if (partitioned) \"PARTITIONED BY (part) \" else \"\"}\n             |TBLPROPERTIES (delta.enableChangeDataFeed = false)\n             |AS SELECT id, int(id / 10) AS part, 'foo' as _change_type\n             |FROM RANGE(1000)\n             |\"\"\".stripMargin)\n        val rowsToUpdate = (1 to 1000 by 42).mkString(\"(\", \", \", \")\")\n        executeUpdate(\"tab\", \"_change_type = 'bar'\", s\"id in $rowsToUpdate\")\n        sql(\"SELECT id, _change_type FROM tab\").collect().foreach { row =>\n          val _change_type = row.getString(1)\n          assert(_change_type === \"foo\" || _change_type === \"bar\",\n            s\"Invalid _change_type for id=${row.get(0)}\")\n        }\n      }\n    }\n  }\n\n  // The following two tests are run only against the SQL API because using the Scala API\n  // incorrectly triggers the analyzer rule [[ResolveRowLevelCommandAssignments]] which allows\n  // the casts without respecting the value of `storeAssignmentPolicy`.\n\n  // Casts that are not valid upcasts (e.g. string -> boolean) are not allowed with\n  // storeAssignmentPolicy = STRICT.\n  test(\"invalid implicit cast string source type into boolean target, \" +\n   s\"storeAssignmentPolicy = ${StoreAssignmentPolicy.STRICT}\") {\n    append(Seq((99, true), (100, false), (101, true)).toDF(\"key\", \"value\"))\n    withSQLConf(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\") {\n    checkError(\n      intercept[AnalysisException] {\n        executeUpdate(target = tableSQLIdentifier, set = \"value = 'false'\")\n      },\n      \"CANNOT_UP_CAST_DATATYPE\",\n      parameters = Map(\n        \"expression\" -> \"'false'\",\n        \"sourceType\" -> toSQLType(\"STRING\"),\n        \"targetType\" -> toSQLType(\"BOOLEAN\"),\n        \"details\" -> (\"The type path of the target object is:\\n\\nYou can either add an explicit \" +\n          \"cast to the input data or choose a higher precision type of the field in the target \" +\n          \"object\")))\n    }\n  }\n\n  // Implicit casts that are not upcasts are not allowed with storeAssignmentPolicy = STRICT.\n  test(\"valid implicit cast string source type into int target, \" +\n     s\"storeAssignmentPolicy = ${StoreAssignmentPolicy.STRICT}\") {\n    append(Seq((99, 2), (100, 4), (101, 3)).toDF(\"key\", \"value\"))\n    withSQLConf(\n        SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.STRICT.toString,\n        DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\") {\n    checkError(\n      intercept[AnalysisException] {\n        executeUpdate(target = tableSQLIdentifier, set = \"value = '5'\")\n      },\n      \"CANNOT_UP_CAST_DATATYPE\",\n      parameters = Map(\n        \"expression\" -> \"'5'\",\n        \"sourceType\" -> toSQLType(\"STRING\"),\n        \"targetType\" -> toSQLType(\"INT\"),\n        \"details\" -> (\"The type path of the target object is:\\n\\nYou can either add an explicit \" +\n          \"cast to the input data or choose a higher precision type of the field in the target \" +\n          \"object\")))\n    }\n  }\n}\n\ntrait UpdateSQLWithDeletionVectorsMixin extends UpdateSQLMixin\n  with DeltaExcludedTestMixin\n  with DeletionVectorsTestUtils {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark, update = true)\n  }\n\n  override def excluded: Seq[String] = super.excluded ++\n    Seq(\n      // The following two tests must fail when DV is used. Covered by another test case:\n      // \"throw error when non-pinned TahoeFileIndex snapshot is used\".\n      \"data and partition predicates - Partition=true Skipping=false\",\n      \"data and partition predicates - Partition=false Skipping=false\",\n      // The scan schema contains additional row index filter columns.\n      \"schema pruning on finding files to update\",\n      \"nested schema pruning on finding files to update\"\n    )\n}\n\ntrait UpdateSQLWithDeletionVectorsTests extends UpdateSQLWithDeletionVectorsMixin {\n  test(\"repeated UPDATE produces deletion vectors\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val log = DeltaLog.forTable(spark, path)\n      spark.range(0, 10, 1, numPartitions = 2).write.format(\"delta\").save(path)\n\n      // scalastyle:off argcount\n      def updateAndCheckLog(\n          where: String,\n          expectedAnswer: Seq[Row],\n\n          numAddFilesWithDVs: Int,\n          sumNumRowsInAddFileWithDV: Int,\n          sumNumRowsInAddFileWithoutDV: Int,\n          sumDvCardinalityInAddFile: Long,\n\n          numRemoveFilesWithDVs: Int,\n          sumNumRowsInRemoveFileWithDV: Int,\n          sumNumRowsInRemoveFileWithoutDV: Int,\n          sumDvCardinalityInRemoveFile: Long): Unit = {\n        executeUpdate(s\"delta.`$path`\", \"id = -1\", where)\n        checkAnswer(sql(s\"SELECT * FROM delta.`$path`\"), expectedAnswer)\n\n        val fileActions = log.getChanges(log.update().version).flatMap(_._2)\n          .collect { case f: FileAction => f }\n          .toSeq\n        val addFiles = fileActions.collect { case f: AddFile => f }\n        val removeFiles = fileActions.collect { case f: RemoveFile => f }\n\n        val (addFilesWithDV, addFilesWithoutDV) = addFiles.partition(_.deletionVector != null)\n        assert(addFilesWithDV.size === numAddFilesWithDVs)\n        assert(\n          addFilesWithDV.map(_.numPhysicalRecords.getOrElse(0L)).sum ===\n            sumNumRowsInAddFileWithDV)\n        assert(\n          addFilesWithDV.map(_.deletionVector.cardinality).sum ===\n            sumDvCardinalityInAddFile)\n        assert(\n          addFilesWithoutDV.map(_.numPhysicalRecords.getOrElse(0L)).sum ===\n            sumNumRowsInAddFileWithoutDV)\n\n        val (removeFilesWithDV, removeFilesWithoutDV) =\n          removeFiles.partition(_.deletionVector != null)\n        assert(removeFilesWithDV.size === numRemoveFilesWithDVs)\n        assert(\n          removeFilesWithDV.map(_.numPhysicalRecords.getOrElse(0L)).sum ===\n            sumNumRowsInRemoveFileWithDV)\n        assert(\n          removeFilesWithDV.map(_.deletionVector.cardinality).sum ===\n            sumDvCardinalityInRemoveFile)\n        assert(\n          removeFilesWithoutDV.map(_.numPhysicalRecords.getOrElse(0L)).sum ===\n            sumNumRowsInRemoveFileWithoutDV)\n      }\n      // scalastyle:on argcount\n\n      def assertDVMetrics(\n          numUpdatedRows: Long = 0,\n          numCopiedRows: Long = 0,\n          numDeletionVectorsAdded: Long = 0,\n          numDeletionVectorsRemoved: Long = 0,\n          numDeletionVectorsUpdated: Long = 0): Unit = {\n        val table = io.delta.tables.DeltaTable.forPath(path)\n        val updateMetrics = DeltaMetricsUtils.getLastOperationMetrics(table)\n        assert(updateMetrics.getOrElse(\"numUpdatedRows\", -1) === numUpdatedRows)\n        assert(updateMetrics.getOrElse(\"numCopiedRows\", -1) === numCopiedRows)\n        assert(updateMetrics.getOrElse(\"numDeletionVectorsAdded\", -1) === numDeletionVectorsAdded)\n        assert(\n          updateMetrics.getOrElse(\"numDeletionVectorsRemoved\", -1) === numDeletionVectorsRemoved)\n        assert(\n          updateMetrics.getOrElse(\"numDeletionVectorsUpdated\", -1) === numDeletionVectorsUpdated)\n      }\n\n      // DV created. 4 rows updated.\n      updateAndCheckLog(\n        \"id % 3 = 0\",\n        Seq(-1, 1, 2, -1, 4, 5, -1, 7, 8, -1).map(Row(_)),\n        numAddFilesWithDVs = 2,\n        sumNumRowsInAddFileWithDV = 10,\n        sumNumRowsInAddFileWithoutDV = 4,\n        sumDvCardinalityInAddFile = 4,\n\n        numRemoveFilesWithDVs = 0,\n        sumNumRowsInRemoveFileWithDV = 0,\n        sumNumRowsInRemoveFileWithoutDV = 10,\n        sumDvCardinalityInRemoveFile = 0)\n\n      assertDVMetrics(numUpdatedRows = 4, numDeletionVectorsAdded = 2)\n\n      // DV updated. 2 rows from the original file updated.\n      updateAndCheckLog(\n        \"id % 4 = 0\",\n        Seq(-1, 1, 2, -1, -1, 5, -1, 7, -1, -1).map(Row(_)),\n        numAddFilesWithDVs = 2,\n        sumNumRowsInAddFileWithDV = 10,\n        sumNumRowsInAddFileWithoutDV = 2,\n        sumDvCardinalityInAddFile = 6,\n        numRemoveFilesWithDVs = 2,\n        sumNumRowsInRemoveFileWithDV = 10,\n        sumNumRowsInRemoveFileWithoutDV = 0,\n        sumDvCardinalityInRemoveFile = 4)\n\n      assertDVMetrics(\n        numUpdatedRows = 2,\n        numDeletionVectorsAdded = 2,\n        numDeletionVectorsRemoved = 2,\n        numDeletionVectorsUpdated = 2)\n\n      // Original files DV removed, because all rows in the SECOND FILE are deleted.\n      updateAndCheckLog(\n        \"id IN (5, 7)\",\n        Seq(-1, 1, 2, -1, -1, -1, -1, -1, -1, -1).map(Row(_)),\n        numAddFilesWithDVs = 0,\n        sumNumRowsInAddFileWithDV = 0,\n        sumNumRowsInAddFileWithoutDV = 2,\n        sumDvCardinalityInAddFile = 0,\n        numRemoveFilesWithDVs = 1,\n        sumNumRowsInRemoveFileWithDV = 5,\n        sumNumRowsInRemoveFileWithoutDV = 0,\n        sumDvCardinalityInRemoveFile = 3)\n\n      assertDVMetrics(numUpdatedRows = 2, numDeletionVectorsRemoved = 1)\n    }\n  }\n\n  test(\"UPDATE a whole partition do not produce DVs\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      val log = DeltaLog.forTable(spark, path)\n      spark.range(10).withColumn(\"part\", col(\"id\") % 2)\n        .write\n        .format(\"delta\")\n        .partitionBy(\"part\")\n        .save(path)\n\n      executeUpdate(s\"delta.`$path`\", \"id = -1\", where = \"part = 0\")\n      checkAnswer(\n        sql(s\"SELECT * FROM delta.`$path`\"),\n        Row(-1, 0) :: Row(1, 1) :: Row(-1, 0) ::\n          Row(3, 1) :: Row(-1, 0) :: Row(5, 1) :: Row(-1, 0) ::\n          Row(7, 1) :: Row(-1, 0) :: Row(9, 1) :: Nil)\n\n      val fileActions = log.getChanges(log.update().version).flatMap(_._2)\n        .collect { case f: FileAction => f }\n        .toSeq\n      val addFiles = fileActions.collect { case f: AddFile => f }\n      val removeFiles = fileActions.collect { case f: RemoveFile => f }\n      assert(addFiles.map(_.numPhysicalRecords.getOrElse(0L)).sum === 5)\n      assert(removeFiles.map(_.numPhysicalRecords.getOrElse(0L)).sum === 5)\n      for (a <- addFiles) assert(a.deletionVector === null)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/UpdateScalaSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.{DeltaExcludedTestMixin, DeltaSQLCommandTest}\n\nimport org.apache.spark.sql.{functions, Row}\n\ntrait UpdateScalaMixin extends UpdateBaseMixin\n  with DeltaSQLCommandTest\n  with DeltaExcludedTestMixin\n  with DeltaDMLTestUtilsPathBased {\n\n  override protected def executeUpdate(\n      target: String,\n      set: String,\n      where: String = null): Unit = {\n    executeUpdate(target, set.split(\",\"), where)\n  }\n\n  override protected def executeUpdate(\n      target: String,\n      set: Seq[String],\n      where: String): Unit = {\n\n    val deltaTable = DeltaTestUtils.getDeltaTableForIdentifierOrPath(\n      spark,\n      DeltaTestUtils.getTableIdentifierOrPath(target))\n\n    val setColumns = set.map { assign =>\n      val kv = assign.split(\"=\")\n      require(kv.size == 2)\n      kv(0).trim -> kv(1).trim\n    }.toMap\n\n    if (where == null) {\n      deltaTable.updateExpr(setColumns)\n    } else {\n      deltaTable.updateExpr(where, setColumns)\n    }\n  }\n}\n\ntrait UpdateScalaTests extends UpdateScalaMixin {\n  import testImplicits._\n\n  test(\"update usage test - without condition\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.updateExpr(Map(\"key\" -> \"100\"))\n    checkAnswer(readDeltaTable(tempPath),\n      Row(100, 10) :: Row(100, 20) :: Row(100, 30) :: Row(100, 40) :: Nil)\n  }\n\n  test(\"update usage test - without condition, using Column\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.update(Map(\"key\" -> functions.expr(\"100\")))\n    checkAnswer(readDeltaTable(tempPath),\n      Row(100, 10) :: Row(100, 20) :: Row(100, 30) :: Row(100, 40) :: Nil)\n  }\n\n  test(\"update usage test - with condition\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.updateExpr(\"key = 1 or key = 2\", Map(\"key\" -> \"100\"))\n    checkAnswer(readDeltaTable(tempPath),\n      Row(100, 10) :: Row(100, 20) :: Row(3, 30) :: Row(4, 40) :: Nil)\n  }\n\n  test(\"update usage test - with condition, using Column\") {\n    append(Seq((1, 10), (2, 20), (3, 30), (4, 40)).toDF(\"key\", \"value\"))\n    val table = io.delta.tables.DeltaTable.forPath(tempPath)\n    table.update(functions.expr(\"key = 1 or key = 2\"),\n      Map(\"key\" -> functions.expr(\"100\"), \"value\" -> functions.expr(\"101\")))\n    checkAnswer(readDeltaTable(tempPath),\n      Row(100, 101) :: Row(100, 101) :: Row(3, 30) :: Row(4, 40) :: Nil)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/UpdateSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.Locale\n\nimport scala.language.implicitConversions\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.shims.UnsupportedTableOperationErrorShims\n\nimport org.apache.spark.{SparkThrowable, SparkUnsupportedOperationException}\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.execution.FileSourceScanExec\nimport org.apache.spark.sql.execution.datasources.FileFormat\nimport org.apache.spark.sql.functions.{lit, struct}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\ntrait UpdateBaseMixin\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaDMLTestUtils\n  with DeltaSQLTestUtils\n  with DeltaTestUtilsForTempViews {\n  import testImplicits._\n\n  protected def executeUpdate(target: String, set: Seq[String], where: String): Unit = {\n    executeUpdate(target, set.mkString(\", \"), where)\n  }\n\n  protected def executeUpdate(target: String, set: String, where: String = null): Unit\n\n  implicit def jsonStringToSeq(json: String): Seq[String] = json.split(\"\\n\")\n\n  protected def checkUpdate(\n      condition: Option[String],\n      setClauses: String,\n      expectedResults: Seq[Row],\n      tableName: Option[String] = None,\n      prefix: String = \"\"): Unit = {\n    val target = tableName.getOrElse(tableSQLIdentifier)\n    executeUpdate(target, setClauses, where = condition.orNull)\n    checkAnswer(\n      readDeltaTableByIdentifier(target).select(s\"${prefix}key\", s\"${prefix}value\"),\n      expectedResults)\n  }\n\n  protected def checkUpdateJson(\n      target: Seq[String],\n      source: Seq[String] = Nil,\n      updateWhere: String,\n      set: Seq[String],\n      expected: Seq[String]): Unit = {\n    withTempView(\"source\") {\n      def toDF(jsonStrs: Seq[String]) = spark.read.json(jsonStrs.toDS())\n      append(toDF(target))\n      if (source.nonEmpty) {\n        toDF(source).createOrReplaceTempView(\"source\")\n      }\n      executeUpdate(tableSQLIdentifier, set, updateWhere)\n      checkAnswer(readDeltaTableByIdentifier(), toDF(expected))\n      dropTable()\n    }\n  }\n\n  protected def testAnalysisException(\n      targetDF: DataFrame,\n      set: Seq[String],\n      where: String = null,\n      errMsgs: Seq[String] = Nil): Unit = {\n    dropTable()\n    append(targetDF)\n    val e = intercept[AnalysisException] {\n      executeUpdate(target = tableSQLIdentifier, set, where)\n    }\n    errMsgs.foreach { msg =>\n      assert(e.getMessage.toLowerCase(Locale.ROOT).contains(msg.toLowerCase(Locale.ROOT)))\n    }\n  }\n}\n\ntrait UpdateBaseTempViewTests extends UpdateBaseMixin {\n  import testImplicits._\n\n  test(\"different variations of column references - TempView\") {\n    append(Seq((99, 2), (100, 4), (101, 3), (102, 5)).toDF(\"key\", \"value\"))\n\n    readDeltaTableByIdentifier().createOrReplaceTempView(\"tblName\")\n\n    checkUpdate(\n      condition = Some(\"tblName.key = 101\"),\n      setClauses = \"tblName.value = -1\",\n      expectedResults = Row(99, 2) :: Row(100, 4) :: Row(101, -1) :: Row(102, 5) :: Nil,\n      tableName = Some(\"tblName\"))\n    checkUpdate(\n      condition = Some(\"`tblName`.`key` = 102\"),\n      setClauses = \"`tblName`.`value` = -1\",\n      expectedResults = Row(99, 2) :: Row(100, 4) :: Row(101, -1) :: Row(102, -1) :: Nil,\n      tableName = Some(\"tblName\"))\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    val testName = s\"test update on temp view - basic - Partition=$isPartitioned\"\n    testWithTempView(testName) { isSQLTempView =>\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n      createTempViewFromTable(tableSQLIdentifier, isSQLTempView)\n        checkUpdate(\n          condition = Some(\"key >= 1\"),\n          setClauses = \"value = key + value, key = key + 1\",\n          expectedResults = Row(0, 3) :: Row(2, 5) :: Row(2, 2) :: Row(3, 4) :: Nil,\n          tableName = Some(\"v\"))\n    }\n  }\n\n  private def testInvalidTempViews(name: String)(\n      text: String,\n      expectedErrorMsgForSQLTempView: String = null,\n      expectedErrorMsgForDataSetTempView: String = null,\n      expectedErrorClassForSQLTempView: String = null,\n      expectedErrorClassForDataSetTempView: String = null): Unit = {\n    testWithTempView(s\"test update on temp view - $name\") { isSQLTempView =>\n      withTable(\"tab\") {\n        Seq((0, 3), (1, 2)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n        createTempViewFromSelect(text, isSQLTempView)\n        val ex = intercept[AnalysisException] {\n          executeUpdate(\n            \"v\",\n            where = \"key >= 1 and value < 3\",\n            set = \"value = key + value, key = key + 1\"\n          )\n        }\n        testErrorMessageAndClass(\n          isSQLTempView,\n          ex,\n          expectedErrorMsgForSQLTempView,\n          expectedErrorMsgForDataSetTempView,\n          expectedErrorClassForSQLTempView,\n          expectedErrorClassForDataSetTempView)\n      }\n    }\n  }\n\n  testInvalidTempViews(\"subset cols\")(\n    text = \"SELECT key FROM tab\",\n    expectedErrorClassForSQLTempView = \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n    expectedErrorClassForDataSetTempView = \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n  )\n\n  testInvalidTempViews(\"superset cols\")(\n    text = \"SELECT key, value, 1 FROM tab\",\n    // The analyzer can't tell whether the table originally had the extra column or not.\n    expectedErrorMsgForSQLTempView = \"Can't resolve column 1 in root\",\n    expectedErrorMsgForDataSetTempView = \"Can't resolve column 1 in root\"\n  )\n\n  private def testComplexTempViews(name: String)(text: String, expectedResult: Seq[Row]) = {\n    testWithTempView(s\"test update on temp view - $name\") { isSQLTempView =>\n        withTable(\"tab\") {\n          Seq((0, 3), (1, 2)).toDF(\"key\", \"value\").write.format(\"delta\").saveAsTable(\"tab\")\n          createTempViewFromSelect(text, isSQLTempView)\n          executeUpdate(\n            \"v\",\n            where = \"key >= 1 and value < 3\",\n            set = \"value = key + value, key = key + 1\"\n          )\n          checkAnswer(spark.read.format(\"delta\").table(\"v\"), expectedResult)\n        }\n      }\n  }\n\n  testComplexTempViews(\"nontrivial projection\")(\n    text = \"SELECT value as key, key as value FROM tab\",\n    expectedResult = Seq(Row(3, 0), Row(3, 3))\n  )\n\n  testComplexTempViews(\"view with too many internal aliases\")(\n    text = \"SELECT * FROM (SELECT * FROM tab AS t1) AS t2\",\n    expectedResult = Seq(Row(0, 3), Row(2, 3))\n  )\n}\n\ntrait UpdateBaseMiscTests extends UpdateBaseMixin {\n  import testImplicits._\n\n  val fileFormat: String = \"parquet\"\n\n  test(\"basic case\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    checkUpdate(condition = None, setClauses = \"key = 1, value = 2\",\n      expectedResults = Row(1, 2) :: Row(1, 2) :: Row(1, 2) :: Row(1, 2) :: Nil)\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(\n        condition = Some(\"key >= 1\"),\n        setClauses = \"value = key + value, key = key + 1\",\n        expectedResults = Row(0, 3) :: Row(2, 5) :: Row(2, 2) :: Row(3, 4) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - Delta table by name - Partition=$isPartitioned\") {\n      withTable(\"delta_table\") {\n        val partitionByClause = if (isPartitioned) \"PARTITIONED BY (key)\" else \"\"\n        sql(s\"\"\"\n             |CREATE TABLE delta_table(key INT, value INT) USING delta\n             |$partitionByClause\n           \"\"\".stripMargin)\n\n        Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(\"delta_table\")\n\n        checkUpdate(\n          condition = Some(\"key >= 1\"),\n          setClauses = \"value = key + value, key = key + 1\",\n          expectedResults = Row(0, 3) :: Row(2, 5) :: Row(2, 2) :: Row(3, 4) :: Nil,\n          tableName = Some(\"delta_table\"))\n      }\n    }\n  }\n\n  Seq(true, false).foreach { skippingEnabled =>\n    Seq(true, false).foreach { isPartitioned =>\n      test(s\"data and partition predicates - Partition=$isPartitioned Skipping=$skippingEnabled\") {\n        withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> skippingEnabled.toString) {\n          val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n          append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n          checkUpdate(condition = Some(\"key >= 1 and value != 4\"),\n            setClauses = \"value = key + value, key = key + 5\",\n            expectedResults = Row(0, 3) :: Row(7, 4) :: Row(1, 4) :: Row(6, 2) :: Nil)\n        }\n      }\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"SC-12276: table has null values - partitioned=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((\"a\", 1), (null, 2), (null, 3), (\"d\", 4)).toDF(\"key\", \"value\"), partitions)\n\n      // predicate evaluates to null; no-op\n      checkUpdate(condition = Some(\"key = null\"),\n        setClauses = \"value = -1\",\n        expectedResults = Row(\"a\", 1) :: Row(null, 2) :: Row(null, 3) :: Row(\"d\", 4) :: Nil)\n\n      checkUpdate(condition = Some(\"key = 'a'\"),\n        setClauses = \"value = -1\",\n        expectedResults = Row(\"a\", -1) :: Row(null, 2) :: Row(null, 3) :: Row(\"d\", 4) :: Nil)\n\n      checkUpdate(condition = Some(\"key is null\"),\n        setClauses = \"value = -2\",\n        expectedResults = Row(\"a\", -1) :: Row(null, -2) :: Row(null, -2) :: Row(\"d\", 4) :: Nil)\n\n      checkUpdate(condition = Some(\"key is not null\"),\n        setClauses = \"value = -3\",\n        expectedResults = Row(\"a\", -3) :: Row(null, -2) :: Row(null, -2) :: Row(\"d\", -3) :: Nil)\n\n      checkUpdate(condition = Some(\"key <=> null\"),\n        setClauses = \"value = -4\",\n        expectedResults = Row(\"a\", -3) :: Row(null, -4) :: Row(null, -4) :: Row(\"d\", -3) :: Nil)\n    }\n  }\n\n  test(\"basic case - condition is false\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    checkUpdate(condition = Some(\"1 != 1\"), setClauses = \"key = 1, value = 2\",\n      expectedResults = Row(2, 2) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n  }\n\n  test(\"basic case - condition is true\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    checkUpdate(condition = Some(\"1 = 1\"), setClauses = \"key = 1, value = 2\",\n      expectedResults = Row(1, 2) :: Row(1, 2) :: Row(1, 2) :: Row(1, 2) :: Nil)\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - without where - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = None, setClauses = \"key = 1, value = 2\",\n        expectedResults = Row(1, 2) :: Row(1, 2) :: Row(1, 2) :: Row(1, 2) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - without where and partial columns - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = None, setClauses = \"key = 1\",\n        expectedResults = Row(1, 1) :: Row(1, 2) :: Row(1, 3) :: Row(1, 4) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - without where and out-of-order columns - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = None, setClauses = \"value = 3, key = 1\",\n        expectedResults = Row(1, 3) :: Row(1, 3) :: Row(1, 3) :: Row(1, 3) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - without where and complex input - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = None, setClauses = \"value = key + 3, key = key + 1\",\n        expectedResults = Row(1, 3) :: Row(2, 4) :: Row(2, 4) :: Row(3, 5) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - with where - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = Some(\"key = 1\"), setClauses = \"value = 3, key = 1\",\n        expectedResults = Row(1, 3) :: Row(2, 2) :: Row(0, 3) :: Row(1, 3) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - with where and complex input - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = Some(\"key >= 1\"), setClauses = \"value = key + value, key = key + 1\",\n        expectedResults = Row(0, 3) :: Row(2, 5) :: Row(2, 2) :: Row(3, 4) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - with where and no row matched - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = Some(\"key >= 10\"), setClauses = \"value = key + value, key = key + 1\",\n        expectedResults = Row(0, 3) :: Row(1, 1) :: Row(1, 4) :: Row(2, 2) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"type mismatch - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = Some(\"key >= 1\"),\n        setClauses = \"value = key + cast(value as double), key = cast(key as double) + 1\",\n        expectedResults = Row(0, 3) :: Row(2, 5) :: Row(3, 4) :: Row(2, 2) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"set to null - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(condition = Some(\"key >= 1\"),\n        setClauses = \"value = key, key = null + 1D\",\n        expectedResults = Row(0, 3) :: Row(null, 1) :: Row(null, 1) :: Row(null, 2) :: Nil)\n    }\n  }\n\n  Seq(true, false).foreach { isPartitioned =>\n    test(s\"basic update - TypeCoercion twice - Partition=$isPartitioned\") {\n      val partitions = if (isPartitioned) \"key\" :: Nil else Nil\n      append(Seq((99, 2), (100, 4), (101, 3)).toDF(\"key\", \"value\"), partitions)\n\n      checkUpdate(\n        condition = Some(\"cast(key as long) * cast('1.0' as decimal(38, 18)) > 100\"),\n        setClauses = \"value = -3\",\n        expectedResults = Row(100, 4) :: Row(101, -3) :: Row(99, 2) :: Nil)\n    }\n  }\n\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values)\n  test(\"upcast int source type into long target, storeAssignmentPolicy = \" +\n    s\"$storeAssignmentPolicy\") {\n    append(Seq((99, 2L), (100, 4L), (101, 3L)).toDF(\"key\", \"value\"))\n    withSQLConf(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\") {\n      checkUpdate(\n        condition = None,\n        setClauses = \"value = 4\",\n        expectedResults = Row(100, 4) :: Row(101, 4) :: Row(99, 4) :: Nil)\n    }\n  }\n\n  // Casts that are not valid implicit casts (e.g. string -> boolean) are allowed only when\n  // storeAssignmentPolicy is LEGACY or ANSI. STRICT is tested in [[UpdateSQLTests]] only due to\n  // limitations when using the Scala API.\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values - StoreAssignmentPolicy.STRICT)\n  test(\"invalid implicit cast string source type into boolean target, \" +\n    s\"storeAssignmentPolicy = $storeAssignmentPolicy\") {\n    append(Seq((99, true), (100, false), (101, true)).toDF(\"key\", \"value\"))\n    withSQLConf(\n      SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n      DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\") {\n      checkUpdate(\n        condition = None,\n        setClauses = \"value = 'false'\",\n        expectedResults = Row(100, false) :: Row(101, false) :: Row(99, false) :: Nil)\n    }\n  }\n\n  // Valid implicit casts that are not upcasts (e.g. string -> int) are allowed only when\n  // storeAssignmentPolicy is LEGACY or ANSI. STRICT is tested in [[UpdateSQLTests]] only due to\n  // limitations when using the Scala API.\n  for (storeAssignmentPolicy <- StoreAssignmentPolicy.values - StoreAssignmentPolicy.STRICT)\n  test(\"valid implicit cast string source type into int target, \" +\n     s\"storeAssignmentPolicy = ${storeAssignmentPolicy}\") {\n    append(Seq((99, 2), (100, 4), (101, 3)).toDF(\"key\", \"value\"))\n    withSQLConf(\n        SQLConf.STORE_ASSIGNMENT_POLICY.key -> storeAssignmentPolicy.toString,\n        DeltaSQLConf.UPDATE_AND_MERGE_CASTING_FOLLOWS_ANSI_ENABLED_FLAG.key -> \"false\") {\n      checkUpdate(\n        condition = None,\n        setClauses = \"value = '5'\",\n        expectedResults = Row(100, 5) :: Row(101, 5) :: Row(99, 5) :: Nil)\n    }\n  }\n\n  test(\"update cached table\") {\n    append(Seq((2, 2), (1, 4)).toDF(\"key\", \"value\"))\n\n    readDeltaTableByIdentifier().cache()\n    readDeltaTableByIdentifier().collect()\n\n    executeUpdate(tableSQLIdentifier, set = \"key = 3\")\n    checkAnswer(readDeltaTableByIdentifier(), Row(3, 2) :: Row(3, 4) :: Nil)\n  }\n\n  test(\"different variations of column references\") {\n    append(Seq((99, 2), (100, 4), (101, 3), (102, 5)).toDF(\"key\", \"value\"))\n\n    checkUpdate(\n      condition = Some(\"key = 99\"),\n      setClauses = \"value = -1\",\n      expectedResults = Row(99, -1) :: Row(100, 4) :: Row(101, 3) :: Row(102, 5) :: Nil)\n    checkUpdate(\n      condition = Some(\"`key` = 100\"),\n      setClauses = \"`value` = -1\",\n      expectedResults = Row(99, -1) :: Row(100, -1) :: Row(101, 3) :: Row(102, 5) :: Nil)\n  }\n\n  test(\"target columns can have db and table qualifiers\") {\n    withTable(\"target\") {\n      spark.read.json(\"\"\"\n          {\"a\": {\"b.1\": 1, \"c.e\": 'random'}, \"d\": 1}\n          {\"a\": {\"b.1\": 3, \"c.e\": 'string'}, \"d\": 2}\"\"\"\n        .split(\"\\n\").toSeq.toDS()).write.format(\"delta\").saveAsTable(\"`target`\")\n\n      executeUpdate(\n        target = \"target\",\n        set = \"`default`.`target`.a.`b.1` = -1, target.a.`c.e` = 'RANDOM'\",\n        where = \"d = 1\")\n\n      checkAnswer(spark.table(\"target\"),\n        spark.read.json(\"\"\"\n            {\"a\": {\"b.1\": -1, \"c.e\": 'RANDOM'}, \"d\": 1}\n            {\"a\": {\"b.1\": 3, \"c.e\": 'string'}, \"d\": 2}\"\"\"\n          .split(\"\\n\").toSeq.toDS()))\n    }\n  }\n\n  test(\"Negative case - non-delta target\") {\n    writeTable(\n      Seq((1, 1), (0, 3), (1, 5)).toDF(\"key1\", \"value\").write.mode(\"overwrite\").format(\"parquet\"),\n      tableSQLIdentifier)\n    intercept[SparkThrowable] {\n      executeUpdate(target = tableSQLIdentifier, set = \"key1 = 3\")\n    } match {\n      // Thrown when running with path-based SQL\n      case e: DeltaAnalysisException if e.getCondition == \"DELTA_TABLE_NOT_FOUND\" =>\n        checkError(e, \"DELTA_TABLE_NOT_FOUND\",\n          parameters = Map(\"tableName\" -> tableSQLIdentifier.stripPrefix(\"delta.\")))\n      case e: DeltaAnalysisException if e.getCondition == \"DELTA_MISSING_TRANSACTION_LOG\" =>\n        checkErrorMatchPVals(e, \"DELTA_MISSING_TRANSACTION_LOG\",\n          parameters = Map(\"operation\" -> \"read from\", \"path\" -> \".*\", \"docLink\" -> \"https://.*\"))\n      // Thrown when running with path-based Scala API\n      case e: DeltaAnalysisException if e.getCondition == \"DELTA_MISSING_DELTA_TABLE\" =>\n        checkError(e, \"DELTA_MISSING_DELTA_TABLE\",\n          parameters = Map(\"tableName\" -> tableSQLIdentifier.stripPrefix(\"delta.\")))\n      // Thrown when running with name-based SQL\n      case e: SparkUnsupportedOperationException =>\n        checkError(e, UnsupportedTableOperationErrorShims.UNSUPPORTED_TABLE_OPERATION_ERROR_CODE,\n          parameters = UnsupportedTableOperationErrorShims.updateTableErrorParameters(\n            tableSQLIdentifier))\n    }\n  }\n\n  test(\"Negative case - check target columns during analysis\") {\n    withTable(\"table\") {\n      sql(\"CREATE TABLE table (s int, t string) USING delta PARTITIONED BY (s)\")\n      var ae = intercept[AnalysisException] {\n        executeUpdate(\"table\", set = \"column_doesnt_exist = 'San Francisco'\", where = \"t = 'a'\")\n      }\n      // The error class is renamed from MISSING_COLUMN to UNRESOLVED_COLUMN in Spark 3.4\n      assert(ae.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n        || ae.getErrorClass == \"MISSING_COLUMN\" )\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n        executeUpdate(target = \"table\", set = \"S = 1, T = 'b'\", where = \"T = 'a'\")\n        ae = intercept[AnalysisException] {\n          executeUpdate(target = \"table\", set = \"S = 1, s = 'b'\", where = \"s = 1\")\n        }\n        assert(ae.message.contains(\"There is a conflict from these SET columns\"))\n      }\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n        ae = intercept[AnalysisException] {\n          executeUpdate(target = \"table\", set = \"S = 1\", where = \"t = 'a'\")\n        }\n        // The error class is renamed from MISSING_COLUMN to UNRESOLVED_COLUMN in Spark 3.4\n        assert(ae.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n          || ae.getErrorClass == \"MISSING_COLUMN\" )\n\n        ae = intercept[AnalysisException] {\n          executeUpdate(target = \"table\", set = \"S = 1, s = 'b'\", where = \"s = 1\")\n        }\n        // The error class is renamed from MISSING_COLUMN to UNRESOLVED_COLUMN in Spark 3.4\n        assert(ae.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n          || ae.getErrorClass == \"MISSING_COLUMN\" )\n\n        // unresolved column in condition\n        ae = intercept[AnalysisException] {\n          executeUpdate(target = \"table\", set = \"s = 1\", where = \"T = 'a'\")\n        }\n        // The error class is renamed from MISSING_COLUMN to UNRESOLVED_COLUMN in Spark 3.4\n        assert(ae.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\"\n          || ae.getErrorClass == \"MISSING_COLUMN\" )\n      }\n    }\n  }\n\n  test(\"Negative case - UPDATE the child directory\",\n      NameBasedAccessIncompatible) {\n    withTempDir { dir =>\n      val tempPath = dir.getCanonicalPath\n      val df = Seq((2, 2), (3, 2)).toDF(\"key\", \"value\")\n      df.write.format(\"delta\").partitionBy(\"key\").save(tempPath)\n\n      val e = intercept[AnalysisException] {\n        executeUpdate(\n          target = s\"delta.`$tempPath/key=2`\",\n          set = \"key = 1, value = 2\",\n          where = \"value = 2\")\n      }.getMessage\n      assert(e.contains(\"Expect a full scan of Delta sources, but found a partial scan\"))\n    }\n  }\n\n  test(\"Negative case - do not support subquery test\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"c\", \"d\").createOrReplaceTempView(\"source\")\n\n    // basic subquery\n    val e0 = intercept[AnalysisException] {\n      executeUpdate(target = tableSQLIdentifier,\n        set = \"key = 1\",\n        where = \"key < (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e0.contains(\"Subqueries are not supported\"))\n\n    // subquery with EXISTS\n    val e1 = intercept[AnalysisException] {\n      executeUpdate(target = tableSQLIdentifier,\n        set = \"key = 1\",\n        where = \"EXISTS (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e1.contains(\"Subqueries are not supported\"))\n\n    // subquery with NOT EXISTS\n    val e2 = intercept[AnalysisException] {\n      executeUpdate(target = tableSQLIdentifier,\n        set = \"key = 1\",\n        where = \"NOT EXISTS (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e2.contains(\"Subqueries are not supported\"))\n\n    // subquery with IN\n    val e3 = intercept[AnalysisException] {\n      executeUpdate(target = tableSQLIdentifier,\n        set = \"key = 1\",\n        where = \"key IN (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e3.contains(\"Subqueries are not supported\"))\n\n    // subquery with NOT IN\n    val e4 = intercept[AnalysisException] {\n      executeUpdate(target = tableSQLIdentifier,\n        set = \"key = 1\",\n        where = \"key NOT IN (SELECT max(c) FROM source)\")\n    }.getMessage\n    assert(e4.contains(\"Subqueries are not supported\"))\n  }\n\n  test(\"nested data support\") {\n    // set a nested field\n    checkUpdateJson(target = \"\"\"\n        {\"a\": {\"c\": {\"d\": 'random', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"z = 10\",\n      set = \"a.c.d = 'RANDOM'\" :: Nil,\n      expected = \"\"\"\n        {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\")\n\n    // do nothing as condition has no match\n    val unchanged = \"\"\"\n        {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\"\n    checkUpdateJson(target = unchanged,\n      updateWhere = \"z = 30\",\n      set = \"a.c.d = 'RANDOMMMMM'\" :: Nil,\n      expected = unchanged)\n\n    // set multiple nested fields at different levels\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"z = 20\",\n      set = \"a.c.d = 'RANDOM2'\" :: \"a.c.e = 'STR2'\" :: \"a.g = -2\" :: \"z = -20\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'RANDOM2', \"e\": 'STR2'}, \"g\": -2}, \"z\": -20}\"\"\")\n\n    // set nested fields to null\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"a.c.d = 'random2'\",\n      set = \"a.c = null\" :: \"a.g = null\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": null, \"g\": null}, \"z\": 20}\"\"\")\n\n    // set a top struct type column to null\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"a.c.d = 'random2'\",\n      set = \"a = null\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": null, \"z\": 20}\"\"\")\n\n    // set a nested field using named_struct\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"a.g = 2\",\n      set = \"a.c = named_struct('d', 'RANDOM2', 'e', 'STR2')\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'RANDOM2', \"e\": 'STR2'}, \"g\": 2}, \"z\": 20}\"\"\")\n\n    // set an integer nested field with a string that can be casted into an integer\n    checkUpdateJson(\n      target = \"\"\"\n        {\"a\": {\"c\": {\"d\": 'random', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"z = 10\",\n      set = \"a.g = '-1'\" :: \"z = '30'\" :: Nil,\n      expected = \"\"\"\n        {\"a\": {\"c\": {\"d\": 'random', \"e\": 'str'}, \"g\": -1}, \"z\": 30}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\")\n\n    // set the nested data that has an Array field\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'random', \"e\": [1, 11]}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'RANDOM2', \"e\": [2, 22]}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"z = 20\",\n      set = \"a.c.d = 'RANDOM22'\" :: \"a.g = -2\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'random', \"e\": [1, 11]}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'RANDOM22', \"e\": [2, 22]}, \"g\": -2}, \"z\": 20}\"\"\")\n\n    // set an array field\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'random', \"e\": [1, 11]}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'RANDOM22', \"e\": [2, 22]}, \"g\": -2}, \"z\": 20}\"\"\",\n      updateWhere = \"z = 10\",\n      set = \"a.c.e = array(-1, -11)\" ::  \"a.g = -1\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'random', \"e\": [-1, -11]}, \"g\": -1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'RANDOM22', \"e\": [2, 22]}, \"g\": -2}, \"z\": 20}\"\"\")\n\n    // set an array field as a top-level attribute\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": [1, 11], \"b\": 'Z'}\n          {\"a\": [2, 22], \"b\": 'Y'}\"\"\",\n      updateWhere = \"b = 'Z'\",\n      set = \"a = array(-1, -11, -111)\" :: Nil,\n      expected = \"\"\"\n          {\"a\": [-1, -11, -111], \"b\": 'Z'}\n          {\"a\": [2, 22], \"b\": 'Y'}\"\"\")\n  }\n\n  test(\"nested data resolution order\") {\n    // By default, resolve by name.\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"a.g = 2\",\n      set = \"a = named_struct('g', 20, 'c', named_struct('e', 'str0', 'd', 'randomNew'))\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'randomNew', \"e\": 'str0'}, \"g\": 20}, \"z\": 20}\"\"\")\n    checkUpdateJson(\n      target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n      updateWhere = \"a.g = 2\",\n      set = \"a.c = named_struct('e', 'str0', 'd', 'randomNew')\" :: Nil,\n      expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'randomNew', \"e\": 'str0'}, \"g\": 2}, \"z\": 20}\"\"\")\n\n    // With the legacy conf, resolve by position.\n    withSQLConf((DeltaSQLConf.DELTA_RESOLVE_MERGE_UPDATE_STRUCTS_BY_NAME.key, \"false\")) {\n      checkUpdateJson(\n        target = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n        updateWhere = \"a.g = 2\",\n        set = \"a.c = named_struct('e', 'str0', 'd', 'randomNew')\" :: Nil,\n        expected = \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'str0', \"e\": 'randomNew'}, \"g\": 2}, \"z\": 20}\"\"\")\n\n      val e = intercept[AnalysisException] {\n        checkUpdateJson(\n          target =\n            \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\",\n          updateWhere = \"a.g = 2\",\n          set =\n            \"a = named_struct('g', 20, 'c', named_struct('e', 'str0', 'd', 'randomNew'))\" :: Nil,\n          expected =\n            \"\"\"\n          {\"a\": {\"c\": {\"d\": 'RANDOM', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n          {\"a\": {\"c\": {\"d\": 'randomNew', \"e\": 'str0'}, \"g\": 20}, \"z\": 20}\"\"\")\n      }\n\n      assert(e.getMessage.contains(\"cannot cast\"))\n    }\n  }\n\n  testQuietly(\"nested data - negative case\") {\n    val targetDF = spark.read.json(\"\"\"\n        {\"a\": {\"c\": {\"d\": 'random', \"e\": 'str'}, \"g\": 1}, \"z\": 10}\n        {\"a\": {\"c\": {\"d\": 'random2', \"e\": 'str2'}, \"g\": 2}, \"z\": 20}\"\"\"\n      .split(\"\\n\").toSeq.toDS())\n\n    testAnalysisException(\n      targetDF,\n      set = \"a.c = 'RANDOM2'\" :: Nil,\n      where = \"z = 10\",\n      errMsgs = \"data type mismatch\" :: Nil)\n\n    testAnalysisException(\n      targetDF,\n      set = \"a.c.z = 'RANDOM2'\" :: Nil,\n      errMsgs = \"No such struct field\" :: Nil)\n\n    testAnalysisException(\n      targetDF,\n      set = \"a.c = named_struct('d', 'rand', 'e', 'str')\" :: \"a.c.d = 'RANDOM2'\" :: Nil,\n      errMsgs = \"There is a conflict from these SET columns\" :: Nil)\n\n    testAnalysisException(\n      targetDF,\n      set = Seq(\"a = named_struct('c', named_struct('d', 'rand', 'e', 'str'), 'g', 3)\",\n        \"a.c.d = 'RANDOM2'\"),\n      errMsgs = \"There is a conflict from these SET columns\" :: Nil)\n\n    val schema = new StructType().add(\"a\", MapType(StringType, IntegerType))\n    val mapData = spark.read.schema(schema).json(Seq(\"\"\"{\"a\": {\"b\": 1}}\"\"\").toDS())\n    testAnalysisException(\n      mapData,\n      set = \"a.b = -1\" :: Nil,\n      errMsgs = \"Updating nested fields is only supported for StructType\" :: Nil)\n\n    // Updating an ArrayStruct is not supported\n    val arrayStructData = spark.read.json(Seq(\"\"\"{\"a\": [{\"b\": 1}, {\"b\": 2}]}\"\"\").toDS())\n    testAnalysisException(\n      arrayStructData,\n      set = \"a.b = array(-1)\" :: Nil,\n      errMsgs = \"Updating nested fields is only supported for StructType\" :: Nil)\n  }\n\n  test(\"schema pruning on finding files to update\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\"))\n    // Start from a cached snapshot state\n    deltaLog.update().stateDF\n\n    val executedPlans = DeltaTestUtils.withPhysicalPlansCaptured(spark) {\n      checkUpdate(condition = Some(\"key = 2\"), setClauses = \"key = 1, value = 3\",\n        expectedResults = Row(1, 3) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil)\n    }\n\n    val scans = executedPlans.flatMap(_.collect {\n      case f: FileSourceScanExec => f\n    })\n    // The first scan is for finding files to update. We only are matching against the key\n    // so that should be the only field in the schema.\n    assert(scans.head.schema == StructType(\n      Seq(\n        StructField(\"key\", IntegerType)\n      )\n    ))\n  }\n\n  test(\"nested schema pruning on finding files to update\") {\n    append(Seq((2, 2), (1, 4), (1, 1), (0, 3)).toDF(\"key\", \"value\")\n      .select(struct(\"key\", \"value\").alias(\"nested\")))\n    // Start from a cached snapshot state\n    deltaLog.update().stateDF\n\n    val executedPlans = DeltaTestUtils.withPhysicalPlansCaptured(spark) {\n      checkUpdate(condition = Some(\"nested.key = 2\"),\n        setClauses = \"nested.key = 1, nested.value = 3\",\n        expectedResults = Row(1, 3) :: Row(1, 4) :: Row(1, 1) :: Row(0, 3) :: Nil,\n        prefix = \"nested.\")\n    }\n\n    val scans = executedPlans.flatMap(_.collect {\n      case f: FileSourceScanExec => f\n    })\n\n    assert(scans.head.schema == StructType.fromDDL(\"nested STRUCT<key: int>\"))\n  }\n\n  /**\n   * @param function the unsupported function.\n   * @param functionType The type of the unsupported expression to be tested.\n   * @param data the data in the table.\n   * @param set the set action containing the unsupported expression.\n   * @param where the where clause containing the unsupported expression.\n   * @param expectException whether an exception is expected to be thrown\n   * @param customErrorRegex customized error regex.\n   */\n  private def testUnsupportedExpression(\n      function: String,\n      functionType: String,\n      data: => DataFrame,\n      set: String,\n      where: String,\n      expectException: Boolean,\n      customErrorRegex: Option[String] = None) {\n    test(s\"$functionType functions in update - expect exception: $expectException\") {\n      withTable(\"deltaTable\") {\n        data.write.format(\"delta\").saveAsTable(\"deltaTable\")\n\n        val expectedErrorRegex = \"(?s).*(?i)unsupported.*(?i).*Invalid expressions.*\"\n\n        def checkExpression(\n            setOption: Option[String] = None,\n            whereOption: Option[String] = None) {\n          var catchException = if (functionType.equals(\"Generate\") && setOption.nonEmpty) {\n            expectException\n          } else true\n\n          var errorRegex = if (functionType.equals(\"Generate\") && whereOption.nonEmpty) {\n            \".*Subqueries are not supported in the UPDATE.*\"\n          } else customErrorRegex.getOrElse(expectedErrorRegex)\n\n\n          if (catchException) {\n            val dataBeforeException = spark.read.format(\"delta\").table(\"deltaTable\").collect()\n            val e = intercept[Exception] {\n              executeUpdate(\n                \"deltaTable\",\n                setOption.getOrElse(\"b = 4\"),\n                whereOption.getOrElse(\"a = 1\"))\n            }\n            val message = if (e.getCause != null) {\n              e.getCause.getMessage\n            } else e.getMessage\n            assert(message.matches(errorRegex))\n            checkAnswer(spark.read.format(\"delta\").table(\"deltaTable\"), dataBeforeException)\n          } else {\n            executeUpdate(\n              \"deltaTable\",\n              setOption.getOrElse(\"b = 4\"),\n              whereOption.getOrElse(\"a = 1\"))\n          }\n        }\n\n        // on set\n        checkExpression(setOption = Option(set))\n\n        // on condition\n        checkExpression(whereOption = Option(where))\n      }\n    }\n  }\n\n  testUnsupportedExpression(\n    function = \"row_number\",\n    functionType = \"Window\",\n    data = Seq((1, 2, 3)).toDF(\"a\", \"b\", \"c\"),\n    set = \"b = row_number() over (order by c)\",\n    where = \"row_number() over (order by c) > 1\",\n    expectException = true\n  )\n\n  testUnsupportedExpression(\n    function = \"max\",\n    functionType = \"Aggregate\",\n    data = Seq((1, 2, 3)).toDF(\"a\", \"b\", \"c\"),\n    set = \"b = max(c)\",\n    where = \"b > max(c)\",\n    expectException = true\n  )\n\n  // Explode functions are supported in set and where if there's only one row generated.\n  testUnsupportedExpression(\n    function = \"explode\",\n    functionType = \"Generate\",\n    data = Seq((1, 2, List(3))).toDF(\"a\", \"b\", \"c\"),\n    set = \"b = (select explode(c) from deltaTable)\",\n    where = \"b = (select explode(c) from deltaTable)\",\n    expectException = false // only one row generated, no exception.\n  )\n\n  // Explode functions are supported in set and where but if there's more than one row generated,\n  // it will throw an exception.\n  testUnsupportedExpression(\n    function = \"explode\",\n    functionType = \"Generate\",\n    data = Seq((1, 2, List(3, 4))).toDF(\"a\", \"b\", \"c\"),\n    set = \"b = (select explode(c) from deltaTable)\",\n    where = \"b = (select explode(c) from deltaTable)\",\n    expectException = true, // more than one generated, expect exception.\n    customErrorRegex =\n      Some(\".*ore than one row returned by a subquery used as an expression(?s).*\")\n  )\n\n  test(\"Variant type\") {\n    val df = sql(\n      \"\"\"SELECT parse_json(cast(id as string)) v, id i\n        FROM range(2)\"\"\")\n    append(df)\n    executeUpdate(target = tableSQLIdentifier,\n        where = \"to_json(v) = '1'\", set = \"i = 10, v = parse_json('123')\")\n    checkAnswer(readDeltaTableByIdentifier().selectExpr(\"i\", \"to_json(v)\"),\n        Seq(Row(0, \"0\"), Row(10, \"123\")))\n  }\n\n  test(\"update on partitioned table with special chars\") {\n    val partA = \"part%one\"\n    val partB = \"part%two\"\n    append(spark.range(0, 3, 1, 1).toDF(\"key\").withColumn(\"value\", lit(partA)), \"value\")\n    checkUpdate(\n      condition = Some(s\"value = '$partA' AND key = 1\"),\n      setClauses = s\"value = '$partB'\",\n      expectedResults = Row(0, partA) :: Row(1, partB) :: Row(2, partA) :: Nil\n    )\n    checkUpdate(\n      condition = Some(s\"value = '$partA' AND key = 2\"),\n      setClauses = s\"value = '$partB'\",\n      expectedResults = Row(0, partA) :: Row(1, partB) :: Row(2, partB) :: Nil\n    )\n    checkUpdate(\n      condition = Some(s\"value = '$partA'\"),\n      setClauses = s\"value = '$partB'\",\n      expectedResults = Row(0, partB) :: Row(1, partB) :: Row(2, partB) :: Nil\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/VersionChecksumHistogramCompatSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.FileSizeHistogram\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Tests for backward-compatible deserialization of the VersionChecksum histogram field.\n *\n * Delta spec and Kernel (Java/Rust) write CRC files using \"fileSizeHistogram\" as the JSON field\n * name, while Delta-Spark historically used \"histogramOpt\". The `@JsonAlias` on\n * [[VersionChecksum.histogramOpt]] allows reading both field names so that CRC files written by\n * either Kernel or Delta-Spark are compatible.\n */\nclass VersionChecksumHistogramCompatSuite\n  extends QueryTest\n  with DeltaSQLCommandTest\n  with SharedSparkSession {\n\n  import testImplicits._\n\n  test(\"CRC with spec-compliant fileSizeHistogram field (Kernel format) is readable\") {\n    // Delta spec and Kernel (Java/Rust) use \"fileSizeHistogram\" as the JSON field name.\n    // Delta-Spark historically used \"histogramOpt\". This test verifies that Delta-Spark\n    // can read CRC files written by Kernel (i.e., JSON with \"fileSizeHistogram\" key).\n\n    // Part 1: hardcoded JSON (unit-level deserialization check)\n    val kernelWrittenJson =\n      \"\"\"{\n        |  \"txnId\": \"kernel-txn-id\",\n        |  \"tableSizeBytes\": 2000,\n        |  \"numFiles\": 5,\n        |  \"numDeletedRecordsOpt\": null,\n        |  \"numDeletionVectorsOpt\": null,\n        |  \"numMetadata\": 1,\n        |  \"numProtocol\": 1,\n        |  \"inCommitTimestampOpt\": null,\n        |  \"setTransactions\": null,\n        |  \"domainMetadata\": null,\n        |  \"metadata\": {\"id\": \"kernel-test-table-id\", \"format\": {\"provider\": \"parquet\"},\n        |    \"partitionColumns\": [], \"configuration\": {}},\n        |  \"protocol\": {\"minReaderVersion\": 1, \"minWriterVersion\": 2},\n        |  \"fileSizeHistogram\": {\n        |    \"sortedBinBoundaries\": [0, 1024, 10240, 102400, 1048576, 10485760],\n        |    \"fileCounts\": [2, 1, 0, 1, 1, 0],\n        |    \"totalBytes\": [1000, 5000, 0, 200000, 2000000, 0]\n        |  },\n        |  \"deletedRecordCountsHistogramOpt\": null,\n        |  \"allFiles\": null\n        |}\"\"\".stripMargin\n\n    val parsedChecksum = JsonUtils.mapper.readValue[VersionChecksum](kernelWrittenJson)\n    assert(parsedChecksum.histogramOpt.isDefined,\n      \"histogramOpt should be populated from the fileSizeHistogram JSON field\")\n    val parsedHistogram = parsedChecksum.histogramOpt.get\n    assert(parsedHistogram.sortedBinBoundaries ===\n      IndexedSeq(0L, 1024L, 10240L, 102400L, 1048576L, 10485760L))\n    assert(parsedHistogram.fileCounts.toSeq === Seq(2L, 1L, 0L, 1L, 1L, 0L))\n    assert(parsedHistogram.totalBytes.toSeq === Seq(1000L, 5000L, 0L, 200000L, 2000000L, 0L))\n\n    // Part 2: integration check via real Delta table and DeltaLog\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val version = log.snapshot.version\n\n      // Scenario A: persist the hardcoded test JSON as the CRC and read it back via DeltaLog.\n      // Re-serialize as compact JSON (single line) because store.read() splits by newline and\n      // readChecksum takes only the first line.\n      log.store.write(\n        FileNames.checksumFile(log.logPath, version),\n        Iterator(JsonUtils.toJson(parsedChecksum)),\n        overwrite = true)\n      DeltaLog.clearCache()\n      val snapshotA = DeltaLog.forTable(spark, dir.getAbsolutePath).snapshot\n      assert(snapshotA.checksumOpt.isDefined)\n      assert(snapshotA.checksumOpt.get.histogramOpt.isDefined,\n        \"Scenario A: histogramOpt should be populated from the fileSizeHistogram JSON field\")\n      assert(snapshotA.checksumOpt.get.histogramOpt.get === parsedHistogram)\n\n      // Scenario B: read the real CRC produced by Delta-Spark, replace \"histogramOpt\" key\n      // with \"fileSizeHistogram\" (simulating a CRC rewritten by Kernel), and read it back\n      val realChecksum = log.readChecksum(version).get\n      assert(realChecksum.histogramOpt.isDefined, \"expected histogram in real CRC\")\n      val realHistogram = realChecksum.histogramOpt.get\n      val kernelFormatJson = JsonUtils.toJson(realChecksum)\n        .replace(\"\\\"histogramOpt\\\":\", \"\\\"fileSizeHistogram\\\":\")\n      log.store.write(\n        FileNames.checksumFile(log.logPath, version),\n        Iterator(kernelFormatJson),\n        overwrite = true)\n      DeltaLog.clearCache()\n      val snapshotB = DeltaLog.forTable(spark, dir.getAbsolutePath).snapshot\n      assert(snapshotB.checksumOpt.isDefined)\n      assert(snapshotB.checksumOpt.get.histogramOpt.isDefined,\n        \"Scenario B: histogramOpt should be populated from the fileSizeHistogram JSON field\")\n      assert(snapshotB.checksumOpt.get.histogramOpt.get === realHistogram)\n    }\n  }\n\n  test(\"CRC missing both histogramOpt and fileSizeHistogram fields deserializes without error\") {\n    // CRC files written before histogram support was added have neither field.\n    // Readers must gracefully return None for histogramOpt.\n    val noHistogramJson =\n      \"\"\"{\n        |  \"txnId\": \"old-txn-id\",\n        |  \"tableSizeBytes\": 500,\n        |  \"numFiles\": 2,\n        |  \"numDeletedRecordsOpt\": null,\n        |  \"numDeletionVectorsOpt\": null,\n        |  \"numMetadata\": 1,\n        |  \"numProtocol\": 1,\n        |  \"inCommitTimestampOpt\": null,\n        |  \"setTransactions\": null,\n        |  \"domainMetadata\": null,\n        |  \"metadata\": null,\n        |  \"protocol\": null,\n        |  \"deletedRecordCountsHistogramOpt\": null,\n        |  \"allFiles\": null\n        |}\"\"\".stripMargin\n\n    val checksum = JsonUtils.mapper.readValue[VersionChecksum](noHistogramJson)\n    assert(checksum.histogramOpt.isEmpty,\n      \"histogramOpt should be None when neither histogramOpt nor fileSizeHistogram is present\")\n  }\n\n  test(\"CRC with both histogramOpt and fileSizeHistogram - last field in JSON takes priority\") {\n    // In practice a CRC will only contain one of these fields (Delta-Spark writes\n    // histogramOpt, Kernel writes fileSizeHistogram). However, if both are present,\n    // Jackson maps both to the same\n    // VersionChecksum.histogramOpt field (via @JsonAlias) and processes them sequentially,\n    // so the LAST occurrence in the JSON wins. This test documents that behavior.\n\n    // Part 1: hardcoded JSON (unit-level deserialization check)\n    // histogramOpt fileCounts = [10, 20, 30] - distinguishable \"Delta-Spark value\"\n    // fileSizeHistogram fileCounts = [1, 2, 3] - used as a distinguishable \"Kernel value\"\n\n    // Case 1: fileSizeHistogram appears last -> fileSizeHistogram value wins\n    val fileSizeHistogramLast =\n      \"\"\"{\n        |  \"txnId\": \"txn-1\",\n        |  \"tableSizeBytes\": 1000,\n        |  \"numFiles\": 3,\n        |  \"numDeletedRecordsOpt\": null,\n        |  \"numDeletionVectorsOpt\": null,\n        |  \"numMetadata\": 1,\n        |  \"numProtocol\": 1,\n        |  \"inCommitTimestampOpt\": null,\n        |  \"setTransactions\": null,\n        |  \"domainMetadata\": null,\n        |  \"metadata\": {\"id\": \"kernel-test-table-id\", \"format\": {\"provider\": \"parquet\"},\n        |    \"partitionColumns\": [], \"configuration\": {}},\n        |  \"protocol\": {\"minReaderVersion\": 1, \"minWriterVersion\": 2},\n        |  \"histogramOpt\": {\n        |    \"sortedBinBoundaries\": [0, 1024, 10240],\n        |    \"fileCounts\": [10, 20, 30],\n        |    \"totalBytes\": [100, 200, 300]\n        |  },\n        |  \"fileSizeHistogram\": {\n        |    \"sortedBinBoundaries\": [0, 1024, 10240],\n        |    \"fileCounts\": [1, 2, 3],\n        |    \"totalBytes\": [10, 20, 30]\n        |  },\n        |  \"deletedRecordCountsHistogramOpt\": null,\n        |  \"allFiles\": null\n        |}\"\"\".stripMargin\n\n    val checksumCase1 = JsonUtils.mapper.readValue[VersionChecksum](fileSizeHistogramLast)\n    assert(checksumCase1.histogramOpt.get.fileCounts.toSeq === Seq(1L, 2L, 3L),\n      \"fileSizeHistogram (last in JSON) should win over histogramOpt\")\n\n    // Case 2: histogramOpt appears last -> histogramOpt value wins\n    val histogramOptLast =\n      \"\"\"{\n        |  \"txnId\": \"txn-2\",\n        |  \"tableSizeBytes\": 1000,\n        |  \"numFiles\": 3,\n        |  \"numDeletedRecordsOpt\": null,\n        |  \"numDeletionVectorsOpt\": null,\n        |  \"numMetadata\": 1,\n        |  \"numProtocol\": 1,\n        |  \"inCommitTimestampOpt\": null,\n        |  \"setTransactions\": null,\n        |  \"domainMetadata\": null,\n        |  \"metadata\": {\"id\": \"kernel-test-table-id\", \"format\": {\"provider\": \"parquet\"},\n        |    \"partitionColumns\": [], \"configuration\": {}},\n        |  \"protocol\": {\"minReaderVersion\": 1, \"minWriterVersion\": 2},\n        |  \"fileSizeHistogram\": {\n        |    \"sortedBinBoundaries\": [0, 1024, 10240],\n        |    \"fileCounts\": [1, 2, 3],\n        |    \"totalBytes\": [10, 20, 30]\n        |  },\n        |  \"histogramOpt\": {\n        |    \"sortedBinBoundaries\": [0, 1024, 10240],\n        |    \"fileCounts\": [10, 20, 30],\n        |    \"totalBytes\": [100, 200, 300]\n        |  },\n        |  \"deletedRecordCountsHistogramOpt\": null,\n        |  \"allFiles\": null\n        |}\"\"\".stripMargin\n\n    val checksumCase2 = JsonUtils.mapper.readValue[VersionChecksum](histogramOptLast)\n    assert(checksumCase2.histogramOpt.get.fileCounts.toSeq === Seq(10L, 20L, 30L),\n      \"histogramOpt (last in JSON) should win over fileSizeHistogram\")\n\n    // Part 2: integration check via real Delta table and DeltaLog\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val version = log.snapshot.version\n\n      // Scenario A: persist the hardcoded test JSON (both fields, fileSizeHistogram last)\n      // and verify fileSizeHistogram wins when read back via DeltaLog.\n      // Use readTree->writeValueAsString to compact to a single line (store.read() splits\n      // by newline and readChecksum takes only .head), while preserving both JSON fields.\n      val compactBothFields = JsonUtils.mapper.writeValueAsString(\n        JsonUtils.mapper.readTree(fileSizeHistogramLast))\n      log.store.write(\n        FileNames.checksumFile(log.logPath, version),\n        Iterator(compactBothFields),\n        overwrite = true)\n      DeltaLog.clearCache()\n      val snapshotA = DeltaLog.forTable(spark, dir.getAbsolutePath).snapshot\n      assert(snapshotA.checksumOpt.isDefined)\n      assert(snapshotA.checksumOpt.get.histogramOpt.get.fileCounts.toSeq === Seq(1L, 2L, 3L),\n        \"Scenario A: fileSizeHistogram (last in JSON) should win over histogramOpt\")\n\n      // Scenario B: read the real CRC produced by Delta-Spark, inject both fields by\n      // appending \"fileSizeHistogram\" (with bumped fileCounts) after the existing\n      // \"histogramOpt\", and verify fileSizeHistogram wins when read back via DeltaLog\n      val realChecksum = log.readChecksum(version).get\n      assert(realChecksum.histogramOpt.isDefined, \"expected histogram in real CRC\")\n      val realHistogramJson = JsonUtils.toJson(realChecksum.histogramOpt.get)\n      val altHistogram = realChecksum.histogramOpt.get.copy(\n        fileCounts = realChecksum.histogramOpt.get.fileCounts.map(_ + 1))\n      val altHistogramJson = JsonUtils.toJson(altHistogram)\n      // Insert fileSizeHistogram after histogramOpt so it appears last and wins\n      val bothFieldsFSHLast = JsonUtils.toJson(realChecksum).replace(\n        s\"\"\"\"histogramOpt\":$realHistogramJson\"\"\",\n        s\"\"\"\"histogramOpt\":$realHistogramJson,\"fileSizeHistogram\":$altHistogramJson\"\"\")\n      log.store.write(\n        FileNames.checksumFile(log.logPath, version),\n        Iterator(bothFieldsFSHLast),\n        overwrite = true)\n      DeltaLog.clearCache()\n      val snapshotB = DeltaLog.forTable(spark, dir.getAbsolutePath).snapshot\n      assert(snapshotB.checksumOpt.isDefined)\n      assert(snapshotB.checksumOpt.get.histogramOpt.get === altHistogram,\n        \"Scenario B: fileSizeHistogram (last in JSON) should win over histogramOpt\")\n    }\n  }\n\n\n  test(\"writeChecksumFile writes correct field name based on conf\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val testHistogram = FileSizeHistogram(\n        sortedBinBoundaries = Vector(0L, 1024L, 10240L),\n        fileCounts = Array(5L, 10L, 15L),\n        totalBytes = Array(100L, 200L, 300L))\n      val checksum = deltaLog.snapshot.checksumOpt.get.copy(histogramOpt = Some(testHistogram))\n\n      val currentSpark = spark\n      val currentLog = deltaLog\n      val writer = new RecordChecksum {\n        override val deltaLog: DeltaLog = currentLog\n        override protected def spark: org.apache.spark.sql.SparkSession = currentSpark\n        def writeChecksum(version: Long, cs: VersionChecksum): Unit =\n          writeChecksumFile(version, cs)\n      }\n\n      // Write with flag OFF (default) -- should use histogramOpt\n      val versionOff = deltaLog.snapshot.version + 1\n      withSQLConf(DeltaSQLConf.DELTA_CHECKSUM_HISTOGRAM_FIELD_FOLLOWS_PROTOCOL.key -> \"false\") {\n        writer.writeChecksum(versionOff, checksum)\n      }\n      val crcJsonOff =\n        deltaLog.store.read(FileNames.checksumFile(deltaLog.logPath, versionOff)).head\n      assert(crcJsonOff.contains(\"\\\"histogramOpt\\\":\"),\n        \"Flag OFF: CRC should contain histogramOpt\")\n      assert(!crcJsonOff.contains(\"\\\"fileSizeHistogram\\\":\"),\n        \"Flag OFF: CRC should not contain fileSizeHistogram\")\n\n      // Write with flag ON -- should use fileSizeHistogram\n      val versionOn = versionOff + 1\n      withSQLConf(DeltaSQLConf.DELTA_CHECKSUM_HISTOGRAM_FIELD_FOLLOWS_PROTOCOL.key -> \"true\") {\n        writer.writeChecksum(versionOn, checksum)\n      }\n      val crcJsonOn =\n        deltaLog.store.read(FileNames.checksumFile(deltaLog.logPath, versionOn)).head\n      assert(crcJsonOn.contains(\"\\\"fileSizeHistogram\\\":\"),\n        \"Flag ON: CRC should contain fileSizeHistogram\")\n      assert(!crcJsonOn.contains(\"\\\"histogramOpt\\\":\"),\n        \"Flag ON: CRC should not contain histogramOpt\")\n\n      // Both CRCs should be readable and produce the same histogram\n      val checksumOff = JsonUtils.mapper.readValue[VersionChecksum](crcJsonOff)\n      val checksumOn = JsonUtils.mapper.readValue[VersionChecksum](crcJsonOn)\n      assert(checksumOff.histogramOpt.get === testHistogram,\n        \"Flag OFF: read-back histogram should match the test histogram\")\n      assert(checksumOn.histogramOpt.get === testHistogram,\n        \"Flag ON: read-back histogram should match the test histogram\")\n    }\n  }\n\n  test(\"VersionChecksumProtocolCompliant fields match VersionChecksum\") {\n    // Use reflection to ensure the two classes stay in sync. If someone adds a field to\n    // VersionChecksum but forgets VersionChecksumProtocolCompliant, this test will catch it.\n    val checksumFields = classOf[VersionChecksum].getDeclaredFields\n      .map(f => (f.getName, f.getType)).toSet\n    val protocolCompliantFields = classOf[VersionChecksumProtocolCompliant].getDeclaredFields\n      .map(f => (f.getName, f.getType)).toSet\n\n    // The only difference should be histogramOpt vs fileSizeHistogram (same type)\n    val expectedOnlyInChecksum = Set((\"histogramOpt\", classOf[Option[_]]))\n    val expectedOnlyInProtocolCompliant = Set((\"fileSizeHistogram\", classOf[Option[_]]))\n\n    val onlyInChecksum = checksumFields -- protocolCompliantFields\n    val onlyInProtocolCompliant = protocolCompliantFields -- checksumFields\n\n    assert(onlyInChecksum === expectedOnlyInChecksum,\n      s\"Unexpected fields only in VersionChecksum: $onlyInChecksum. \" +\n        \"Did you add a new field to VersionChecksum without updating \" +\n        \"VersionChecksumProtocolCompliant?\")\n    assert(onlyInProtocolCompliant === expectedOnlyInProtocolCompliant,\n      s\"Unexpected fields only in VersionChecksumProtocolCompliant: $onlyInProtocolCompliant. \" +\n        \"Did you add a new field to VersionChecksumProtocolCompliant without updating \" +\n        \"VersionChecksum?\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/actions/AddFileSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, DeltaRuntimeException}\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.expressions.{Cast, Literal}\nimport org.apache.spark.sql.errors.QueryErrorsBase\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nclass AddFileSuite extends SparkFunSuite with SharedSparkSession with DeltaSQLCommandTest\n    with QueryErrorsBase {\n\n  private def withJvmTimeZone[T](tzId: String)(block: => T): T = {\n    val originalTz = java.util.TimeZone.getDefault\n    try {\n      java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(tzId))\n      block\n    } finally {\n      java.util.TimeZone.setDefault(originalTz)\n    }\n  }\n\n  private def createAddFileWithPartitionValue(partitionValues: Map[String, String]): AddFile = {\n    AddFile(\n      path = \"test.parquet\",\n      partitionValues = partitionValues,\n      size = 100,\n      modificationTime = 0,\n      dataChange = true)\n  }\n\n  private def timestampLiteral(value: String, tz: String = \"UTC\"): Literal = {\n    Literal.create(\n      Cast(Literal(value), TimestampType, Some(tz), ansiEnabled = false).eval(),\n      TimestampType)\n  }\n\n  private def dateLiteral(value: String): Literal = {\n    Literal.create(\n      Cast(Literal(value), DateType, None, ansiEnabled = false).eval(),\n      DateType)\n  }\n\n  private def timestampNTZLiteral(value: String): Literal = {\n    Literal.create(\n      Cast(Literal(value), TimestampNTZType, None, ansiEnabled = false).eval(),\n      TimestampNTZType)\n  }\n\n  test(\"normalizedPartitionValues for non-timestamp partitions returns typed literals\") {\n    withSQLConf(DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\") {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"strCol\", StringType),\n            StructField(\"intCol\", IntegerType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"strCol\", \"intCol\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        val file = createAddFileWithPartitionValue(Map(\"strCol\" -> \"value1\", \"intCol\" -> \"42\"))\n        val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n        assert(normalized(\"strCol\") == Literal(\"value1\"))\n        assert(normalized(\"intCol\") == Literal.create(42, IntegerType))\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues for timestamp partitions with well formatted values\") {\n    withSQLConf(DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\") {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"tsCol\", TimestampType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        val file = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T20:00:00.000000Z\"))\n        val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n        assert(normalized(\"tsCol\") == timestampLiteral(\"2000-01-01T20:00:00.000000Z\"))\n      }\n    }\n  }\n\n  for (enableNormalization <- BOOLEAN_DOMAIN) {\n    test(\"normalizedPartitionValues for UTC timestamps partitions with different string formats, \" +\n      s\"enableNormalization=$enableNormalization\") {\n      withJvmTimeZone(\"UTC\") {\n        withSQLConf(\n          DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key ->\n            enableNormalization.toString,\n          \"spark.sql.session.timeZone\" -> \"UTC\") {\n          withTempDir { tempDir =>\n            // Create empty Delta table with tsCol as partition column\n            spark.createDataFrame(\n              spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n              StructType(Seq(\n                StructField(\"data\", StringType),\n                StructField(\"tsCol\", TimestampType)\n              ))\n            ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n            val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n            val fileNonUtc = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01 12:00:00\"))\n            val fileUtc =\n              createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T12:00:00.000000Z\"))\n            val normalizedNonUtc = fileNonUtc.normalizedPartitionValues(spark, deltaTxn)\n            val normalizedUtc = fileUtc.normalizedPartitionValues(spark, deltaTxn)\n\n            if (enableNormalization) {\n              assert(normalizedNonUtc == normalizedUtc)\n            } else {\n              assert(normalizedNonUtc != normalizedUtc)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  for (enableNormalization <- BOOLEAN_DOMAIN) {\n    test(\"normalizedPartitionValues for TimestampNTZ partitions returns the correct literal, \" +\n      s\"enableNormalization=$enableNormalization\") {\n      // Per Delta protocol, TimestampNTZ values should be stored as\n      // \"{year}-{month}-{day} {hour}:{minute}:{second}\" without any time zone conversion\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> enableNormalization.toString\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampNTZType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          val file = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01 12:00:00\"))\n          val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n          if (enableNormalization) {\n            assert(normalized(\"tsCol\") == timestampNTZLiteral(\"2000-01-01 12:00:00\"))\n          } else {\n            assert(normalized(\"tsCol\") == Literal(\"2000-01-01 12:00:00\"))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues preserves null partition values\") {\n    withSQLConf(DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\") {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"tsCol\", TimestampType),\n            StructField(\"strCol\", StringType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"tsCol\", \"strCol\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        val file = createAddFileWithPartitionValue(Map(\"tsCol\" -> null, \"strCol\" -> \"value\"))\n        val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n        assert(normalized(\"tsCol\") == timestampLiteral(null))\n        assert(normalized(\"strCol\") == Literal(\"value\"))\n      }\n    }\n  }\n\n  for (enableNormalization <- BOOLEAN_DOMAIN) {\n    test(\"normalizedPartitionValues with mixed timestamp and non-timestamp partitions, \" +\n      s\"enableNormalization=$enableNormalization\") {\n      withJvmTimeZone(\"UTC\") {\n        withSQLConf(\n          DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key ->\n            enableNormalization.toString,\n          \"spark.sql.session.timeZone\" -> \"UTC\"\n        ) {\n          withTempDir { tempDir =>\n            spark.createDataFrame(\n              spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n              StructType(Seq(\n                StructField(\"data\", StringType),\n                StructField(\"tsCol\", TimestampType),\n                StructField(\"strCol\", StringType),\n                StructField(\"intCol\", IntegerType)\n              ))\n            ).write.format(\"delta\")\n              .partitionBy(\"tsCol\", \"strCol\", \"intCol\")\n              .save(tempDir.getCanonicalPath)\n            val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n            val file1 = createAddFileWithPartitionValue(\n              Map(\"tsCol\" -> \"2000-01-01 12:00:00\", \"strCol\" -> \"value\", \"intCol\" -> \"42\"))\n            val file2 = createAddFileWithPartitionValue(\n              Map(\"tsCol\" -> \"2000-01-01T12:00:00.000000Z\", \"strCol\" -> \"value\", \"intCol\" -> \"42\"))\n            val normalized1 = file1.normalizedPartitionValues(spark, deltaTxn)\n            val normalized2 = file2.normalizedPartitionValues(spark, deltaTxn)\n\n            if (enableNormalization) {\n              // Timestamp columns should normalize to same value (same microseconds)\n              assert(normalized1(\"tsCol\") == normalized2(\"tsCol\"))\n              // Non-timestamp columns should be typed literals\n              assert(normalized1(\"strCol\") == Literal(\"value\"))\n              assert(normalized1(\"intCol\") == Literal.create(42, IntegerType))\n            } else {\n              // Without normalization the partition values are different string literals\n              assert(normalized1 != normalized2)\n              // Normalized partition values should be string literals of original values\n              assert(normalized1(\"tsCol\") == Literal(\"2000-01-01 12:00:00\"))\n              assert(normalized2(\"tsCol\") == Literal(\"2000-01-01T12:00:00.000000Z\"))\n            }\n          }\n        }\n      }\n    }\n  }\n\n  for (enableNormalization <- BOOLEAN_DOMAIN) {\n    test(\"normalizedPartitionValues for equal timestamps with different time zone offsets, \" +\n      s\"enableNormalization=$enableNormalization\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key ->\n          enableNormalization.toString,\n        \"spark.sql.session.timeZone\" -> \"UTC\"\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          // All three represent the same instant: 2000-01-01 12:00:00 UTC\n          val fileUtc =\n            createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T12:00:00.000+0000\"))\n          // EST is UTC-5, so 07:00 EST = 12:00 UTC\n          val fileEst =\n            createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T07:00:00.000-0500\"))\n          // PST is UTC-8, so 04:00 PST = 12:00 UTC\n          val filePst =\n            createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T04:00:00.000-0800\"))\n\n          val normalizedUtc = fileUtc.normalizedPartitionValues(spark, deltaTxn)\n          val normalizedEst = fileEst.normalizedPartitionValues(spark, deltaTxn)\n          val normalizedPst = filePst.normalizedPartitionValues(spark, deltaTxn)\n\n          if (enableNormalization) {\n            // All should normalize to the same value since they represent the same moment\n            assert(normalizedUtc == normalizedEst)\n            assert(normalizedUtc == normalizedPst)\n            assert(normalizedEst == normalizedPst)\n          } else {\n            // Without normalization, returns string literals of original values\n            assert(normalizedUtc(\"tsCol\") == Literal(\"2000-01-01T12:00:00.000+0000\"))\n            assert(normalizedEst(\"tsCol\") == Literal(\"2000-01-01T07:00:00.000-0500\"))\n            assert(normalizedPst(\"tsCol\") == Literal(\"2000-01-01T04:00:00.000-0800\"))\n            // The normalized values should be different since they're just string literals\n            assert(normalizedUtc != normalizedEst)\n            assert(normalizedUtc != normalizedPst)\n            assert(normalizedEst != normalizedPst)\n          }\n        }\n      }\n    }\n  }\n\n  for (enableNormalization <- BOOLEAN_DOMAIN) {\n    test(\"normalizedPartitionValues for same timestamp with different time zone notations, \" +\n      s\"enableNormalization=$enableNormalization\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key ->\n          enableNormalization.toString,\n        \"spark.sql.session.timeZone\" -> \"UTC\"\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          // All three represent the same instant: 2000-01-15 12:00:00 UTC\n          // CET time zone abbreviation (UTC+1)\n          val fileCet = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-15 13:00:00 CET\"))\n          // Europe/Berlin time zone name (UTC+1 in winter)\n          val fileEuropeBerlin =\n            createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-15 13:00:00 Europe/Berlin\"))\n          // Numeric offset notation: +0100\n          val fileNumericOffset =\n            createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-15T13:00:00.000+0100\"))\n\n          val normalizedCet = fileCet.normalizedPartitionValues(spark, deltaTxn)\n          val normalizedEuropeBerlin = fileEuropeBerlin.normalizedPartitionValues(spark, deltaTxn)\n          val normalizedNumeric = fileNumericOffset.normalizedPartitionValues(spark, deltaTxn)\n\n          if (enableNormalization) {\n            // All should normalize to the same value since they represent the same moment\n            assert(normalizedCet == normalizedEuropeBerlin,\n              s\"CET and Europe/Berlin should match: $normalizedCet vs $normalizedEuropeBerlin\")\n            assert(normalizedCet == normalizedNumeric,\n              s\"CET and numeric offset should match: $normalizedCet vs $normalizedNumeric\")\n          } else {\n            // Without normalization, returns string literals of the original values\n            assert(normalizedCet(\"tsCol\") == Literal(\"2000-01-15 13:00:00 CET\"))\n            assert(normalizedEuropeBerlin(\"tsCol\") == Literal(\"2000-01-15 13:00:00 Europe/Berlin\"))\n            assert(normalizedNumeric(\"tsCol\") == Literal(\"2000-01-15T13:00:00.000+0100\"))\n            // The normalized values should be different since they're just string literals\n            assert(normalizedCet != normalizedEuropeBerlin)\n            assert(normalizedCet != normalizedNumeric)\n            assert(normalizedNumeric != normalizedEuropeBerlin)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues for DateType should return the original date string\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\"\n    ) {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"dateCol\", DateType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"dateCol\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        val originalDateString = \"2000-01-01\"\n        val file = createAddFileWithPartitionValue(Map(\"dateCol\" -> originalDateString))\n\n        val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n        val normalizedDateValue = normalized(\"dateCol\")\n\n        assert(normalizedDateValue == dateLiteral(originalDateString))\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues should handle __HIVE_DEFAULT_PARTITION__\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\"\n    ) {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"foo\", IntegerType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"data\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        // Tombstone value __HIVE_DEFAULT_PARTITION__ should be preserved as a string for AddFiles\n        val file = createAddFileWithPartitionValue(Map(\"data\" -> \"__HIVE_DEFAULT_PARTITION__\"))\n        val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n        assert(normalized(\"data\") == Literal.create(\"__HIVE_DEFAULT_PARTITION__\", StringType))\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues preserves escaped characters in AddFile partition values\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\"\n    ) {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"foo\", IntegerType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"data\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        // Escaped characters like %aa should be preserved as-is in AddFile partition values\n        // since they are not unescaped in AddFile partition values.\n        val escapedValue = \"test%aa%20value\"\n        val file = createAddFileWithPartitionValue(Map(\"data\" -> escapedValue))\n        val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n        assert(normalized(\"data\") == Literal.create(escapedValue, StringType))\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues with a non UTC session time zone gets converted to UTC\") {\n    withJvmTimeZone(\"Europe/Berlin\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\",\n        \"spark.sql.session.timeZone\" -> \"Europe/Berlin\" // UTC + 1 in winter time\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          val file = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01 12:00:00\"))\n          // The normalized timestamp should be 11:00 UTC\n          // Parsed in Europe/Berlin (UTC+1), so 12:00 Berlin = 11:00 UTC\n          val normalizedTimestamp = file.normalizedPartitionValues(spark, deltaTxn)(\"tsCol\")\n\n          assert(normalizedTimestamp == timestampLiteral(\"2000-01-01 12:00:00\", \"Europe/Berlin\"))\n          assert(normalizedTimestamp == timestampLiteral(\"2000-01-01 11:00:00\", \"UTC\"))\n        }\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues of timestamps strings with time zone offsets \" +\n    \"and a non UTC session time zone gets converted to UTC.\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\",\n      \"spark.sql.session.timeZone\" -> \"America/Los_Angeles\" // UTC - 8 in winter time\n    ) {\n      withTempDir { tempDir =>\n        spark.createDataFrame(\n          spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n          StructType(Seq(\n            StructField(\"data\", StringType),\n            StructField(\"tsCol\", TimestampType)\n          ))\n        ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n        val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n        // Timestamp at 17:30 with a +05:30 offset (India Standard Time) is 12:00 UTC\n        val fileWithIstOffset =\n          createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T17:30:00.000+0530\"))\n        // Timestamp at 13:00 in a Europe/Berlin time zone (UTC+1 in winter) is 12:00 UTC\n        val fileWithCetOffset =\n          createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01 13:00:00 Europe/Berlin\"))\n\n        val normalizedIstTimestamp =\n          fileWithIstOffset.normalizedPartitionValues(spark, deltaTxn)(\"tsCol\")\n        val normalizedCetTimestamp =\n          fileWithCetOffset.normalizedPartitionValues(spark, deltaTxn)(\"tsCol\")\n        // Both should represent 2000-01-01 12:00:00 UTC\n        val expectedTimestamp = timestampLiteral(\"2000-01-01 12:00:00\", \"UTC\")\n\n        assert(normalizedIstTimestamp == expectedTimestamp)\n        assert(normalizedCetTimestamp == expectedTimestamp)\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues with missing leading zeroes in timestamp are accepted\") {\n    withJvmTimeZone(\"UTC\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\",\n        \"spark.sql.session.timeZone\" -> \"UTC\"\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          def getNormalizedTimestamp(tsValue: String): Literal =\n            createAddFileWithPartitionValue(Map(\"tsCol\" -> tsValue))\n              .normalizedPartitionValues(spark, deltaTxn)(\"tsCol\")\n\n          // Missing leading zero in hours: \"1:00:00\" vs \"01:00:00\"\n          val hoursWithout = getNormalizedTimestamp(\"2000-01-01 1:00:00\")\n          val hoursWith = getNormalizedTimestamp(\"2000-01-01 01:00:00\")\n          val expectedHours = timestampLiteral(\"2000-01-01 01:00:00\", \"UTC\")\n          assert(hoursWithout == hoursWith)\n          assert(hoursWith == expectedHours)\n\n          // Missing leading zero in minutes: \"01:2:00\" vs \"01:02:00\"\n          val minutesWithout = getNormalizedTimestamp(\"2000-01-01 01:2:00\")\n          val minutesWith = getNormalizedTimestamp(\"2000-01-01 01:02:00\")\n          val expectedMinutes = timestampLiteral(\"2000-01-01 01:02:00\", \"UTC\")\n          assert(minutesWithout == minutesWith)\n          assert(minutesWith == expectedMinutes)\n\n          // Missing leading zero in seconds: \"01:02:3\" vs \"01:02:03\"\n          val secondsWithout = getNormalizedTimestamp(\"2000-01-01 01:02:3\")\n          val secondsWith = getNormalizedTimestamp(\"2000-01-01 01:02:03\")\n          val expectedSeconds = timestampLiteral(\"2000-01-01 01:02:03\", \"UTC\")\n          assert(secondsWithout == secondsWith)\n          assert(secondsWith == expectedSeconds)\n\n          // All missing leading zeroes: \"1:2:3\" vs \"01:02:03\"\n          val allWithout = getNormalizedTimestamp(\"2000-01-01 1:2:3\")\n          val allWith = getNormalizedTimestamp(\"2000-01-01 01:02:03\")\n          val expectedAll = timestampLiteral(\"2000-01-01 01:02:03\", \"UTC\")\n          assert(allWithout == allWith)\n          assert(allWith == expectedAll)\n        }\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues with ISO 8601 format with T separator but no time zone\") {\n    withJvmTimeZone(\"Europe/Berlin\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\",\n        \"spark.sql.session.timeZone\" -> \"Europe/Berlin\" // UTC + 1 in winter time\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          // ISO 8601 format with 'T' separator but no time zone should use the JVM time zone.\n          val file = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01T12:00:00\"))\n          // The normalized timestamp should be 11:00 UTC (12:00 Berlin = 11:00 UTC)\n          val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n          assert(normalized(\"tsCol\") == timestampLiteral(\"2000-01-01 12:00:00\", \"Europe/Berlin\"))\n        }\n      }\n    }\n  }\n\n  test(\"normalizedPartitionValues should also use the JVM timezone on read\") {\n    withJvmTimeZone(\"America/Los_Angeles\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_NORMALIZE_PARTITION_VALUES_ON_READ.key -> \"true\",\n        \"spark.sql.session.timeZone\" -> \"UTC\"\n      ) {\n        withTempDir { tempDir =>\n          spark.createDataFrame(\n            spark.sparkContext.emptyRDD[org.apache.spark.sql.Row],\n            StructType(Seq(\n              StructField(\"data\", StringType),\n              StructField(\"tsCol\", TimestampType)\n            ))\n          ).write.format(\"delta\").partitionBy(\"tsCol\").save(tempDir.getCanonicalPath)\n          val deltaTxn = DeltaLog.forTable(spark, tempDir.getCanonicalPath).startTransaction()\n\n          // ON WRITE we use the JVM timezone, parsing this as an America/Los_Angeles timestamp.\n          val file = createAddFileWithPartitionValue(Map(\"tsCol\" -> \"2000-01-01 12:00:00\"))\n          val normalized = file.normalizedPartitionValues(spark, deltaTxn)\n\n          // ON READ we also need to use the JVM timezone again, reading it again as an\n          // America/Los_Angeles timestamp.\n          assert(\n            normalized(\"tsCol\") == timestampLiteral(\"2000-01-01 12:00:00\", \"America/Los_Angeles\"))\n          assert(normalized(\"tsCol\") != timestampLiteral(\"2000-01-01 12:00:00\", \"UTC\"))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/actions/DeletionVectorDescriptorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.actions\n\nimport java.util.UUID\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.paths.SparkPath\n// scalastyle:on import.ordering.noEmptyLine\n\n/**\n * Test: DV descriptor creation, created DV descriptor properties and utility methods are\n * working as expected.\n */\nclass DeletionVectorDescriptorSuite extends SparkFunSuite {\n  test(\"Inline DV\") {\n    val dv = inlineInLog(testDVData, cardinality = 3)\n\n    // Make sure the metadata (type, size etc.) in the DV is as expected\n    assert(!dv.isOnDisk && dv.isInline, s\"Incorrect DV storage type: $dv\")\n    assertCardinality(dv, 3)\n\n    val encodedDVData = \"0rJua\"\n    assert(dv.pathOrInlineDv === encodedDVData)\n    assert(dv.sizeInBytes === testDVData.size)\n    assert(dv.inlineData === testDVData)\n    assert(dv.estimatedSerializedSize === 18)\n\n    assert(dv.offset.isEmpty) // There shouldn't be an offset for inline DV\n\n    // Unique id to identify the DV\n    assert(dv.uniqueId === s\"i$encodedDVData\")\n    assert(dv.uniqueFileId === s\"i$encodedDVData\")\n\n    // There is no on-disk file name for an inline DV\n    intercept[IllegalArgumentException] { dv.absolutePath(testTablePath) }\n\n    // Copy as on-disk DV with absolute path and relative path -\n    // expect the returned DV is same as input, since this is inline\n    // so paths are irrelevant.\n    assert(dv.copyWithAbsolutePath(testTablePath) === dv)\n    assert(dv.copyWithNewRelativePath(UUID.randomUUID(), \"predix2\") === dv)\n  }\n\n  for (offset <- Seq(None, Some(25))) {\n    test(s\"On disk DV with absolute path with offset=$offset\") {\n      val dv = onDiskWithAbsolutePath(testDVAbsPath, sizeInBytes = 15, cardinality = 10, offset)\n\n      // Make sure the metadata (type, size etc.) in the DV is as expected\n      assert(dv.isOnDisk && !dv.isInline, s\"Incorrect DV storage type: $dv\")\n      assertCardinality(dv, 10)\n\n      assert(dv.pathOrInlineDv === testDVAbsPath)\n      assert(dv.sizeInBytes === 15)\n      intercept[Exception] { dv.inlineData }\n      assert(dv.estimatedSerializedSize === (if (offset.isDefined) 4 else 0) + 37)\n      assert(dv.offset === offset)\n\n      // Unique id to identify the DV\n      val offsetSuffix = offset.map(o => s\"@$o\").getOrElse(\"\")\n      assert(dv.uniqueId === s\"p$testDVAbsPath$offsetSuffix\")\n      assert(dv.uniqueFileId === s\"p$testDVAbsPath\")\n\n      // Given the input already has an absolute path, it should return the path in DV\n      assert(dv.absolutePath(testTablePath) === new Path(testDVAbsPath))\n\n      // Given the input already has an absolute path, expect the output to be same as input\n      assert(dv.copyWithAbsolutePath(testTablePath) === dv)\n\n      // Copy DV as a relative path DV\n      val uuid = UUID.randomUUID()\n      val dvCopyWithRelativePath = dv.copyWithNewRelativePath(uuid, \"prefix\")\n      assert(dvCopyWithRelativePath.isRelative)\n      assert(dvCopyWithRelativePath.isOnDisk)\n      assert(dvCopyWithRelativePath.pathOrInlineDv === encodeUUID(uuid, \"prefix\"))\n    }\n  }\n\n  for (offset <- Seq(None, Some(25))) {\n    test(s\"On-disk DV with relative path with offset=$offset\") {\n      val uuid = UUID.randomUUID()\n      val dv = onDiskWithRelativePath(\n        uuid, randomPrefix = \"prefix\", sizeInBytes = 15, cardinality = 25, offset)\n\n      // Make sure the metadata (type, size etc.) in the DV is as expected\n      assert(dv.isOnDisk && !dv.isInline, s\"Incorrect DV storage type: $dv\")\n      assertCardinality(dv, 25)\n\n      assert(dv.pathOrInlineDv === encodeUUID(uuid, \"prefix\"))\n      assert(dv.sizeInBytes === 15)\n      intercept[Exception] { dv.inlineData }\n      assert(dv.estimatedSerializedSize === (if (offset.isDefined) 4 else 0) + 39)\n      assert(dv.offset === offset)\n\n      // Unique id to identify the DV\n      val offsetSuffix = offset.map(o => s\"@$o\").getOrElse(\"\")\n      val encodedUUID = encodeUUID(uuid, \"prefix\")\n      assert(dv.uniqueId === s\"u$encodedUUID$offsetSuffix\")\n      assert(dv.uniqueFileId === s\"u$encodedUUID\")\n\n      // Expect the DV final path to be under the given table path\n      assert(dv.absolutePath(testTablePath) ===\n        new Path(s\"$testTablePath/prefix/${DELETION_VECTOR_FILE_NAME_CORE}_$uuid.bin\"))\n\n      // Copy DV with an absolute path location\n      val dvCopyWithAbsPath = dv.copyWithAbsolutePath(testTablePath)\n      assert(dvCopyWithAbsPath.isAbsolute)\n      assert(dvCopyWithAbsPath.isOnDisk)\n      // pathOrInlineDV is URL-encoded.\n      assert(\n        SparkPath.fromUrlString(dvCopyWithAbsPath.pathOrInlineDv).toPath.toString ===\n        s\"$testTablePath/prefix/${DELETION_VECTOR_FILE_NAME_CORE}_$uuid.bin\")\n\n      // Copy DV as a relative path DV - expect to return the same DV as the current\n      // DV already contains relative path.\n      assert(dv.copyWithNewRelativePath(UUID.randomUUID(), \"predix2\") === dv)\n    }\n  }\n\n  private def assertCardinality(dv: DeletionVectorDescriptor, expSize: Int): Unit = {\n    if (expSize == 0) {\n      assert(dv.isEmpty, s\"Expected DV to be empty: $dv\")\n    } else {\n      assert(!dv.isEmpty && dv.cardinality == expSize, s\"Invalid size expected: $expSize, $dv\")\n    }\n  }\n\n  private val testTablePath = new Path(\"s3a://table/test\")\n  private val testDVAbsPath = \"s3a://table/test/dv1.bin\"\n  private val testDVData: Array[Byte] = Array(1, 2, 3, 4)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/cdc/CDCReaderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.cdc\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.Delete\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.{Action, AddCDCFile, AddFile}\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader._\nimport org.apache.spark.sql.delta.files.DelayedCommitProtocol\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.{Row, SaveMode}\nimport org.apache.spark.sql.execution.{LogicalRDD, SQLExecution}\nimport org.apache.spark.sql.execution.datasources.FileFormatWriter\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass CDCReaderSuite\n  extends QueryTest\n  with CheckCDCAnswer\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaColumnMappingTestUtils {\n\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")\n\n  /**\n   * Write a commit with just CDC data. Returns the committed version.\n   */\n  private def writeCdcData(\n      log: DeltaLog,\n      data: DataFrame,\n      extraActions: Seq[Action] = Seq.empty): Long = {\n    log.withNewTransaction { txn =>\n      val qe = data.queryExecution\n      val basePath = log.dataPath.toString\n\n      // column mapped mode forces to use random file prefix\n      val randomPrefixes = if (columnMappingEnabled) {\n        Some(DeltaConfigs.RANDOM_PREFIX_LENGTH.fromMetaData(log.snapshot.metadata))\n      } else {\n        None\n      }\n      // we need to convert to physical name in column mapping mode\n      val mappedOutput = if (columnMappingEnabled) {\n        val metadata = log.snapshot.metadata\n        DeltaColumnMapping.createPhysicalAttributes(\n          qe.analyzed.output, metadata.schema, metadata.columnMappingMode\n        )\n      } else {\n        qe.analyzed.output\n      }\n\n      SQLExecution.withNewExecutionId(qe) {\n        var committer = new DelayedCommitProtocol(\"delta\", basePath, randomPrefixes, None)\n        FileFormatWriter.write(\n          sparkSession = spark,\n          plan = qe.executedPlan,\n          fileFormat = log.fileFormat(log.snapshot.protocol, log.unsafeVolatileMetadata),\n          committer = committer,\n          outputSpec = FileFormatWriter.OutputSpec(basePath, Map.empty, mappedOutput),\n          hadoopConf = log.newDeltaHadoopConf(),\n          partitionColumns = Seq.empty,\n          bucketSpec = None,\n          statsTrackers = Seq.empty,\n          options = Map.empty)\n\n        val cdc = committer.addedStatuses.map { a =>\n          AddCDCFile(a.path, Map.empty, a.size)\n        }\n        txn.commit(extraActions ++ cdc, DeltaOperations.ManualUpdate)\n      }\n    }\n  }\n\n\n  def createCDFDF(start: Long, end: Long, commitVersion: Long, changeType: String): DataFrame = {\n    spark.range(start, end)\n      .withColumn(CDC_TYPE_COLUMN_NAME, lit(changeType))\n      .withColumn(CDC_COMMIT_VERSION, lit(commitVersion))\n  }\n\n  test(\"simple CDC scan\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val data = spark.range(10)\n      val cdcData = spark.range(20, 25).withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n\n      data.write.format(\"delta\").save(dir.getAbsolutePath)\n      sql(s\"DELETE FROM delta.`${dir.getAbsolutePath}`\")\n      writeCdcData(log, cdcData)\n\n      // For this basic test, we check each of the versions individually in addition to the full\n      // range to try and catch weird corner cases.\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 0, 0, spark),\n        data.withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(0))\n      )\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 1, 1, spark),\n        data.withColumn(CDC_TYPE_COLUMN_NAME, lit(\"delete\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(1))\n      )\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 2, 2, spark),\n        cdcData.withColumn(CDC_COMMIT_VERSION, lit(2))\n      )\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 0, 2, spark),\n        data.withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(0))\n          .unionAll(data\n            .withColumn(CDC_TYPE_COLUMN_NAME, lit(\"delete\"))\n            .withColumn(CDC_COMMIT_VERSION, lit(1)))\n          .unionAll(cdcData.withColumn(CDC_COMMIT_VERSION, lit(2)))\n      )\n    }\n  }\n\n  test(\"CDC has correct stats\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val data = spark.range(10)\n      val cdcData = spark.range(20, 25).withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n\n      data.write.format(\"delta\").save(dir.getAbsolutePath)\n      sql(s\"DELETE FROM delta.`${dir.getAbsolutePath}`\")\n      writeCdcData(log, cdcData)\n\n      assert(\n        CDCReader\n          .changesToBatchDF(log, 0, 2, spark)\n          .queryExecution\n          .optimizedPlan\n          .collectLeaves()\n          .exists {\n            case l: LogicalRDD => l.stats.sizeInBytes == 0 && !l.isStreaming\n            case _ => false\n          }\n      )\n    }\n  }\n\n  test(\"cdc update ops\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val data = spark.range(10)\n\n      data.write.format(\"delta\").save(dir.getAbsolutePath)\n      writeCdcData(\n        log,\n        spark.range(20, 25).toDF().withColumn(CDC_TYPE_COLUMN_NAME, lit(\"update_pre\")))\n      writeCdcData(\n        log,\n        spark.range(30, 35).toDF().withColumn(CDC_TYPE_COLUMN_NAME, lit(\"update_post\")))\n\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 0, 2, spark),\n        data.withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(0))\n          .unionAll(spark.range(20, 25).withColumn(CDC_TYPE_COLUMN_NAME, lit(\"update_pre\"))\n              .withColumn(CDC_COMMIT_VERSION, lit(1))\n          )\n          .unionAll(spark.range(30, 35).withColumn(CDC_TYPE_COLUMN_NAME, lit(\"update_post\"))\n              .withColumn(CDC_COMMIT_VERSION, lit(2))\n          )\n      )\n    }\n  }\n\n  test(\"dataChange = false operations ignored\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val data = spark.range(10)\n\n      data.write.format(\"delta\").save(dir.getAbsolutePath)\n      sql(s\"OPTIMIZE delta.`${dir.getAbsolutePath}`\")\n\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 0, 1, spark),\n        data.withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(0))\n      )\n    }\n  }\n\n  test(\"range with start and end equal\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      val data = spark.range(10)\n      val cdcData = spark.range(0, 5).withColumn(CDC_TYPE_COLUMN_NAME, lit(\"delete\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(1))\n\n      data.write.format(\"delta\").save(dir.getAbsolutePath)\n      writeCdcData(log, cdcData)\n\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 0, 0, spark),\n        data.withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(0))\n      )\n\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 1, 1, spark),\n        cdcData)\n    }\n  }\n\n  test(\"range past the end of the log\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n\n      checkCDCAnswer(\n        log,\n        CDCReader.changesToBatchDF(log, 0, 1, spark),\n        spark.range(10).withColumn(CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDC_COMMIT_VERSION, lit(0))\n      )\n    }\n  }\n\n  test(\"invalid range - end before start\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      spark.range(20).write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n\n      intercept[IllegalArgumentException] {\n        CDCReader.changesToBatchDF(log, 1, 0, spark)\n      }\n    }\n  }\n\n  testQuietly(\"invalid range - start after last version of CDF\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n      spark.range(20).write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n\n      val e = intercept[DeltaIllegalArgumentException] {\n        spark.read.format(\"delta\")\n          .option(\"readChangeFeed\", \"true\")\n          .option(\"startingVersion\", Long.MaxValue)\n          .option(\"endingVersion\", Long.MaxValue)\n          .load(dir.toString)\n          .count()\n      }\n      checkError(\n        e,\n        condition = \"DELTA_CDC_START_VERSION_AFTER_LATEST\",\n        sqlState = \"22003\",\n        parameters = Map(\"start\" -> Long.MaxValue.toString, \"latest\" -> \"1\")\n      )\n    }\n  }\n\n  test(\"partition filtering of removes and cdc files\") {\n    withTempDir { dir =>\n      withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n        val path = dir.getAbsolutePath\n        val log = DeltaLog.forTable(spark, path)\n        spark.range(6).selectExpr(\"id\", \"'old' as text\", \"id % 2 as part\")\n          .write.format(\"delta\").partitionBy(\"part\").save(path)\n\n        // Generate some CDC files.\n        withTempView(\"source\") {\n          spark.range(4).createOrReplaceTempView(\"source\")\n          sql(\n            s\"\"\"MERGE INTO delta.`$path` t USING source s ON s.id = t.id\n               |WHEN MATCHED AND s.id = 1 THEN UPDATE SET text = 'new'\n               |WHEN MATCHED AND s.id = 3 THEN DELETE\"\"\".stripMargin)\n        }\n\n        // This will generate just remove files due to the partition delete optimization.\n        sql(s\"DELETE FROM delta.`$path` WHERE part = 0\")\n\n        checkCDCAnswer(\n          log,\n          CDCReader.changesToBatchDF(log, 0, 2, spark).filter(\"_change_type = 'insert'\"),\n          Range(0, 6).map { i => Row(i, \"old\", i % 2, \"insert\", 0) })\n        checkCDCAnswer(\n          log,\n          CDCReader.changesToBatchDF(log, 0, 2, spark).filter(\"_change_type = 'delete'\"),\n          Seq(0, 2, 3, 4).map { i => Row(i, \"old\", i % 2, \"delete\", if (i % 2 == 0) 2 else 1) })\n        checkCDCAnswer(\n          log,\n          CDCReader.changesToBatchDF(log, 0, 2, spark).filter(\"_change_type = 'update_preimage'\"),\n          Row(1, \"old\", 1, \"update_preimage\", 1) :: Nil)\n        checkCDCAnswer(\n          log,\n          CDCReader.changesToBatchDF(log, 0, 2, spark).filter(\"_change_type = 'update_postimage'\"),\n          Row(1, \"new\", 1, \"update_postimage\", 1) :: Nil)\n      }\n    }\n  }\n\n  test(\"file layout - unpartitioned\") {\n    withTempDir { dir =>\n      withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n        val path = dir.getAbsolutePath\n        spark.range(10).repartition(1).write.format(\"delta\").save(path)\n        sql(s\"DELETE FROM delta.`$path` WHERE id < 5\")\n\n        val log = DeltaLog.forTable(spark, path)\n        // The data path should contain four files: the delta log, the CDC folder `__is_cdc=true`,\n        // and two data files with randomized names from before and after the DELETE command. The\n        // commit protocol should have stripped out __is_cdc=false.\n        val baseDirFiles =\n          log.logPath.getFileSystem(log.newDeltaHadoopConf()).listStatus(log.dataPath)\n        assert(baseDirFiles.length == 4)\n        assert(baseDirFiles.exists { f => f.isDirectory && f.getPath.getName == \"_delta_log\"})\n        assert(baseDirFiles.exists { f => f.isDirectory && f.getPath.getName == CDC_LOCATION})\n        assert(!baseDirFiles.exists { f => f.getPath.getName.contains(CDC_PARTITION_COL) })\n      }\n    }\n  }\n\n  test(\"file layout - partitioned\") {\n    withTempDir { dir =>\n      withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n        val path = dir.getAbsolutePath\n        spark.range(10).withColumn(\"part\", col(\"id\") % 2)\n          .repartition(1).write.format(\"delta\").partitionBy(\"part\").save(path)\n        sql(s\"DELETE FROM delta.`$path` WHERE id < 5\")\n\n        val log = DeltaLog.forTable(spark, path)\n        // The data path should contain four directories: the delta log, the CDC folder\n        // `__is_cdc=true`, and the two partition folders. The commit protocol\n        // should have stripped out __is_cdc=false.\n        val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n        val baseDirFiles = fs.listStatus(log.dataPath)\n        baseDirFiles.foreach { f => assert(f.isDirectory) }\n        assert(baseDirFiles.map(_.getPath.getName).toSet ==\n          Set(\"_delta_log\", CDC_LOCATION, \"part=0\", \"part=1\"))\n\n        // Each partition folder should contain only two data files from before and after the read.\n        // In particular, they should not contain any __is_cdc folder - that should always be the\n        // top level partition.\n        for (partitionFolder <- Seq(\"part=0\", \"part=1\")) {\n          val files = fs.listStatus(new Path(log.dataPath, partitionFolder))\n          assert(files.length === 2)\n          files.foreach { f =>\n            assert(!f.isDirectory)\n            assert(!f.getPath.getName.startsWith(CDC_LOCATION))\n          }\n        }\n\n        // The CDC folder should also contain the two partitions.\n        val cdcPartitions = fs.listStatus(new Path(log.dataPath, CDC_LOCATION))\n        cdcPartitions.foreach { f => assert(f.isDirectory, s\"$f was not a directory\") }\n        assert(cdcPartitions.map(_.getPath.getName).toSet == Set(\"part=0\", \"part=1\"))\n      }\n    }\n  }\n\n  test(\"for CDC add backtick in column name with dot [.] \") {\n    import testImplicits._\n\n    withTempDir { dir =>\n      withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n        val path = dir.getAbsolutePath\n        // 0th commit\n        Seq(2, 4).toDF(\"id.num\")\n          .withColumn(\"id.num`s\", lit(10))\n          .withColumn(\"struct_col\", struct(lit(1).as(\"field\"), lit(2).as(\"field.one\")))\n          .write.format(\"delta\").save(path)\n        // 1st commit\n        Seq(1, 3, 5).toDF(\"id.num\")\n          .withColumn(\"id.num`s\", lit(10))\n          .withColumn(\"struct_col\", struct(lit(1).as(\"field\"), lit(2).as(\"field.one\")))\n          .write.format(\"delta\").mode(SaveMode.Append).save(path)\n        // Reading from 0th version\n        val actual = spark.read.format(\"delta\")\n          .option(\"readChangeFeed\", \"true\").option(\"startingVersion\", 0)\n          .load(path).drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n\n        val expected = spark.range(1, 6).toDF(\"id.num\").withColumn(\"id.num`s\", lit(10))\n          .withColumn(\"struct_col\", struct(lit(1).as(\"field\"), lit(2).as(\"field.one\")))\n          .withColumn(CDCReader.CDC_TYPE_COLUMN_NAME, lit(\"insert\"))\n          .withColumn(CDCReader.CDC_COMMIT_VERSION, col(\"`id.num`\") % 2)\n        checkAnswer(actual, expected)\n      }\n    }\n  }\n\n  for (cdfEnabled <- BOOLEAN_DOMAIN)\n  test(s\"Coarse-grained CDF, cdfEnabled=$cdfEnabled\") {\n    withSQLConf(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey -> cdfEnabled.toString) {\n      withTempDir { dir =>\n        val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n        // commit 0: 2 inserts\n        spark.range(start = 0, end = 2, step = 1, numPartitions = 1)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        var df = CDCReader.changesToBatchDF(\n          log, 0, 1, spark, catalogTableOpt = None, useCoarseGrainedCDC = true)\n        checkAnswer(df.drop(CDC_COMMIT_TIMESTAMP),\n          createCDFDF(start = 0, end = 2, commitVersion = 0, changeType = \"insert\"))\n\n        // commit 1: 2 inserts\n        spark.range(start = 2, end = 4)\n          .write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n        df = CDCReader.changesToBatchDF(\n          log, 1, 2, spark, catalogTableOpt = None, useCoarseGrainedCDC = true)\n        checkAnswer(df.drop(CDC_COMMIT_TIMESTAMP),\n          createCDFDF(start = 2, end = 4, commitVersion = 1, changeType = \"insert\"))\n\n        // commit 2\n        sql(s\"DELETE FROM delta.`$dir` WHERE id = 0\")\n        df = CDCReader.changesToBatchDF(\n          log, 2, 3, spark, catalogTableOpt = None, useCoarseGrainedCDC = true)\n          .drop(CDC_COMMIT_TIMESTAMP)\n\n        // Using only Add and RemoveFiles should generate 2 deletes and 1 insert. Even when CDF\n        // is enabled, we want to use only Add and RemoveFiles.\n        val dfWithDeletesFirst = df.sort(CDC_TYPE_COLUMN_NAME)\n        val expectedAnswer =\n          createCDFDF(start = 0, end = 2, commitVersion = 2, changeType = \"delete\")\n            .union(\n              createCDFDF(start = 1, end = 2, commitVersion = 2, changeType = \"insert\"))\n        checkAnswer(dfWithDeletesFirst, expectedAnswer)\n      }\n    }\n  }\n\n  test(\"Logs are generated for changesToDF\") {\n    withTempDir { dir =>\n      val events = Log4jUsageLogger.track {\n        val log = DeltaLog.forTable(spark, dir.getAbsolutePath)\n        val data = spark.range(10)\n\n        data.write.format(\"delta\").save(dir.getAbsolutePath)\n        sql(s\"DELETE FROM delta.`${dir.getAbsolutePath}`\")\n        CDCReader.changesToBatchDF(log, 0, 1, spark)\n      }\n\n      assert(events.exists(event => event.metric == \"tahoeEvent\" &&\n        event.tags.get(\"opType\") == Option(\"delta.changeDataFeed.changesToDF\")))\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/cdc/CDCWorkloadSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.cdc\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Small end to end tests of workloads using CDC from Delta.\n */\nclass CDCWorkloadSuite extends QueryTest with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  test(\"replication workload\") {\n    withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n      withTempPaths(2) { paths =>\n        // Create an empty table at `path` we're going to replicate from, and a replication\n        // destination at `replicatedPath`. The destination contains a subset of the final keys,\n        // but with out-of-date enrichment data.\n        val path = paths.head.getAbsolutePath\n        val replicatedPath = paths(1).getAbsolutePath\n        spark.range(0).selectExpr(\"id\", \"'none' as text\").write.format(\"delta\").save(path)\n        spark.range(50)\n          .selectExpr(\"id\", \"'oldEnrichment' as text\")\n          .filter(\"id % 4 = 0\")\n          .write.format(\"delta\").save(replicatedPath)\n\n        // Add data to the replication source in overlapping batches, so we produce both insert and\n        // update events.\n        for (i <- 0 to 8) {\n          withTempView(\"source\") {\n            spark.range(i * 5, i * 5 + 10)\n              .selectExpr(\"id\", \"'newEnrichment' as text\")\n              .createOrReplaceTempView(\"source\")\n            sql(\n              s\"\"\"MERGE INTO delta.`$path` t USING source s ON s.id = t.id\n                 |WHEN MATCHED THEN UPDATE SET *\n                 |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin)\n          }\n        }\n\n        // Delete some data too.\n        sql(s\"DELETE FROM delta.`$path` WHERE id < 5\")\n\n        for (v <- 0 to 10) {\n          withTempView(\"cdcSource\") {\n            val changes = spark.read.format(\"delta\")\n              .option(\"readChangeFeed\", \"true\")\n              .option(\"startingVersion\", v)\n              .option(\"endingVersion\", v)\n              .load(path)\n            // Filter out the preimage so the update events only have the final row, as required by\n            // our merge API.\n            changes.filter(\"_change_type != 'update_preimage'\").createOrReplaceTempView(\"cdcSource\")\n            sql(\n              s\"\"\"MERGE INTO delta.`$replicatedPath` t USING cdcSource s ON s.id = t.id\n                 |WHEN MATCHED AND s._change_type = 'update_postimage' OR s._change_type = 'insert'\n                 |  THEN UPDATE SET *\n                 |WHEN MATCHED AND s._change_type = 'delete' THEN DELETE\n                 |WHEN NOT MATCHED THEN INSERT *\"\"\".stripMargin)\n          }\n        }\n\n        // We should have all the rows, all with the new enrichment data from the replication\n        // source, except for 0 to 5 which were deleted.\n        val expected = spark.range(5, 50).selectExpr(\"id\", \"'newEnrichment' as text\")\n        checkAnswer(spark.read.format(\"delta\").load(replicatedPath), expected)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/cdc/DeleteCDCSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.cdc\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.Dataset\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.lit\n\ntrait DeleteCDCMixin extends DeleteSQLMixin with CDCEnabled {\n  protected def testCDCDelete(\n      name: String)(\n      initialData: => Dataset[_],\n      partitionColumns: Seq[String] = Seq.empty,\n      deleteCondition: String,\n      expectedData: => Dataset[_],\n      expectedChangeDataWithoutVersion: => Dataset[_]\n    ): Unit = {\n    test(s\"CDC - $name\") {\n      withSQLConf(\n          (DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n        append(initialData.toDF(), partitionColumns)\n\n        executeDelete(tableSQLIdentifier, deleteCondition)\n\n        checkAnswer(\n          readDeltaTableByIdentifier(),\n          expectedData.toDF())\n\n        checkAnswer(\n          getCDCForLatestOperation(deltaLog, operation = \"DELETE\"),\n          expectedChangeDataWithoutVersion.toDF())\n      }\n    }\n  }\n\n}\n\ntrait DeleteCDCTests extends DeleteCDCMixin\n  with CDCTestMixin {\n  import testImplicits._\n\n  testCDCDelete(\"unconditional\")(\n    initialData = spark.range(0, 10, step = 1, numPartitions = 3),\n    deleteCondition = \"\",\n    expectedData = spark.range(0),\n    expectedChangeDataWithoutVersion = spark.range(10)\n      .withColumn(CDC_TYPE_COLUMN_NAME, lit(\"delete\"))\n  )\n\n  testCDCDelete(\"conditional covering all rows\")(\n    initialData = spark.range(0, 10, step = 1, numPartitions = 3),\n    deleteCondition = \"id < 100\",\n    expectedData = spark.range(0),\n    expectedChangeDataWithoutVersion = spark.range(10)\n      .withColumn(CDC_TYPE_COLUMN_NAME, lit(\"delete\"))\n  )\n\n  testCDCDelete(\"two random rows\")(\n    initialData = spark.range(0, 10, step = 1, numPartitions = 3),\n    deleteCondition = \"id = 2 OR id = 8\",\n    expectedData = Seq(0, 1, 3, 4, 5, 6, 7, 9).toDF(),\n    expectedChangeDataWithoutVersion = Seq(2, 8).toDF()\n      .withColumn(CDC_TYPE_COLUMN_NAME, lit(\"delete\"))\n  )\n\n  testCDCDelete(\"delete unconditionally - partitioned table\")(\n    initialData = spark.range(0, 100, step = 1, numPartitions = 10)\n      .selectExpr(\"id % 10 as part\", \"id\"),\n    partitionColumns = Seq(\"part\"),\n    deleteCondition = \"\",\n    expectedData = Seq.empty[(Long, Long)].toDF(\"part\", \"id\"),\n    expectedChangeDataWithoutVersion =\n      spark.range(100)\n        .selectExpr(\"id % 10 as part\", \"id\", \"'delete' as _change_type\")\n  )\n\n  testCDCDelete(\"delete all rows by condition - partitioned table\")(\n    initialData = spark.range(0, 100, step = 1, numPartitions = 10)\n      .selectExpr(\"id % 10 as part\", \"id\"),\n    partitionColumns = Seq(\"part\"),\n    deleteCondition = \"id < 1000\",\n    expectedData = Seq.empty[(Long, Long)].toDF(\"part\", \"id\"),\n    expectedChangeDataWithoutVersion =\n      spark.range(100)\n        .selectExpr(\"id % 10 as part\", \"id\", \"'delete' as _change_type\")\n  )\n\n\n  testCDCDelete(\"partition-optimized delete\")(\n    initialData = spark.range(0, 100, step = 1, numPartitions = 10)\n      .selectExpr(\"id % 10 as part\", \"id\"),\n    partitionColumns = Seq(\"part\"),\n    deleteCondition = \"part = 3\",\n    expectedData =\n      spark.range(100).selectExpr(\"id % 10 as part\", \"id\").where(\"part != 3\"),\n    expectedChangeDataWithoutVersion =\n      Range(0, 10).map(x => x * 10 + 3).toDF(\"id\")\n        .selectExpr(\"3 as part\", \"id\", \"'delete' as _change_type\"))\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/cdc/MergeCDCSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.cdc\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, StructField, StructType}\n\n\ntrait CDCEnabled extends SharedSparkSession {\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")\n}\n\ntrait MergeCDCMixin extends SharedSparkSession\n  with MergeIntoSQLTestUtils\n  with DeltaColumnMappingTestUtils\n  with DeltaSQLCommandTest\n  with MergePersistentDVDisabled\n\n/**\n * Tests for MERGE INTO in CDC output mode.\n *\n */\ntrait MergeCDCTests extends QueryTest\n  with CDCEnabled\n  with MergeCDCMixin\n  with CDCTestMixin {\n\n  import testImplicits._\n\n  // scalastyle:off argcount\n  /**\n   * Utility method for simpler test writing when there's at most clause of each type.\n   */\n  private def testMergeCdc(name: String)(\n      target: => DataFrame,\n      source: => DataFrame,\n      deleteWhen: String = null,\n      update: String = null,\n      insert: String = null,\n      expectedTableData: => DataFrame = null,\n      expectedCdcDataWithoutVersion: => DataFrame = null,\n      expectErrorContains: String = null,\n      confs: Seq[(String, String)] = Seq()): Unit = {\n    val updateClauses = Option(update).map(u => this.update(set = u)).toSeq\n    val insertClauses = Option(insert).map(i => this.insert(values = i)).toSeq\n    val deleteClauses = Option(deleteWhen).map(d => this.delete(condition = d)).toSeq\n    testMergeCdcUnlimitedClauses(name)(\n      target = target,\n      source = source,\n      clauses = deleteClauses ++ updateClauses ++ insertClauses,\n      expectedTableData = expectedTableData,\n      expectedCdcDataWithoutVersion = expectedCdcDataWithoutVersion,\n      expectErrorContains = expectErrorContains,\n      confs = confs)\n  }\n  // scalastyle:on argcount\n\n  private def testMergeCdcUnlimitedClauses(name: String)(\n      target: => DataFrame,\n      source: => DataFrame,\n      mergeCondition: String = \"s.key = t.key\",\n      clauses: Seq[MergeClause],\n      expectedTableData: => DataFrame = null,\n      expectedCdcDataWithoutVersion: => DataFrame = null,\n      expectErrorContains: String = null,\n      confs: Seq[(String, String)] = Seq(),\n      targetTableSchema: Option[StructType] = None): Unit = {\n    test(s\"merge CDC - $name\") {\n      withSQLConf(confs: _*) {\n        targetTableSchema.foreach { schema =>\n          io.delta.tables.DeltaTable.create(spark)\n            .tableName(tableSQLIdentifier)\n            .location(deltaLog.dataPath.toUri.getPath)\n            .addColumns(schema)\n            .execute()\n        }\n        append(target)\n        withTempView(\"source\") {\n          source.createOrReplaceTempView(\"source\")\n\n          if (expectErrorContains != null) {\n            val ex = intercept[Exception] {\n              executeMerge(s\"$tableSQLIdentifier t\", \"source s\", mergeCondition,\n                clauses.toSeq: _*)\n            }\n            assert(ex.getMessage.contains(expectErrorContains))\n          } else {\n            executeMerge(s\"$tableSQLIdentifier t\", \"source s\", mergeCondition,\n              clauses.toSeq: _*)\n            checkAnswer(\n              readDeltaTableByIdentifier(),\n              expectedTableData)\n\n            // Craft expected CDC data\n            val latestVersion = deltaLog.snapshot.version\n            val expectedCdcData = expectedCdcDataWithoutVersion\n              .withColumn(CDCReader.CDC_COMMIT_VERSION, lit(latestVersion))\n\n            // The timestamp is nondeterministic so we drop it when comparing results.\n            checkAnswer(\n              computeCDC(spark, deltaLog, latestVersion, latestVersion)\n                .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n              expectedCdcData)\n          }\n        }\n      }\n    }\n  }\n\n  testMergeCdc(\"insert only\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (2, 2)  :: Nil).toDF(\"key\", \"n\"),\n    insert = \"*\",\n    expectedTableData = ((0, 0) :: (1, 10) :: (2, 2) :: (3, 30) :: Nil).toDF(),\n    expectedCdcDataWithoutVersion = ((2, 2, \"insert\") :: Nil).toDF()\n  )\n\n  testMergeCdc(\"update only\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (2, 2)  :: Nil).toDF(\"key\", \"n\"),\n    update = \"*\",\n    expectedTableData = ((0, 0) :: (1, 1) :: (3, 30) :: Nil).toDF(),\n    expectedCdcDataWithoutVersion = (\n      (1, 10, \"update_preimage\") :: (1, 1, \"update_postimage\") :: Nil).toDF()\n  )\n\n  testMergeCdc(\"delete only\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (2, 2)  :: Nil).toDF(\"key\", \"n\"),\n    deleteWhen = \"true\",\n    expectedTableData = ((0, 0) :: (3, 30) :: Nil).toDF(),\n    expectedCdcDataWithoutVersion = ((1, 10, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdc(\"delete only with duplicate matches\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (1, 2) :: (2, 3)  :: Nil).toDF(\"key\", \"n\"),\n    deleteWhen = \"true\",\n    expectErrorContains = \"attempted to modify the same\\ntarget row\"\n  )\n\n  testMergeCdc(\"update + delete + insert together\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (2, 2) :: (3, -1) :: Nil).toDF(\"key\", \"n\"),\n    insert = \"*\",\n    update = \"*\",\n    deleteWhen = \"s.key = 3\",\n    expectedTableData = ((0, 0) :: (1, 1) :: (2, 2) :: Nil).toDF(),\n    expectedCdcDataWithoutVersion = (\n      (2, 2, \"insert\") ::\n        (1, 10, \"update_preimage\") :: (1, 1, \"update_postimage\") ::\n        (3, 30, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"unlimited clauses - conditional final branch\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: (4, 40) :: (6, 60) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (2, 2) :: (3, -1) :: (4, 4) :: (5, 0) :: (6, 0) :: Nil).toDF(\"key\", \"n\"),\n    clauses =\n      update(\"*\", \"s.key = 1\") :: update(\"n = 400\", \"s.key = 4\") ::\n      delete(\"s.key = 3\") :: delete(\"s.key = 6\") ::\n      insert(\"*\", \"s.key = 2\") :: insert(\"(key, n) VALUES (50, 50)\", \"s.key = 5\") :: Nil,\n    expectedTableData = ((0, 0) :: (1, 1) :: (2, 2) :: (4, 400) :: (50, 50) :: Nil).toDF(),\n    expectedCdcDataWithoutVersion = (\n      (2, 2, \"insert\") :: (50, 50, \"insert\") ::\n        (1, 10, \"update_preimage\") :: (1, 1, \"update_postimage\") ::\n        (4, 40, \"update_preimage\") :: (4, 400, \"update_postimage\") ::\n        (3, 30, \"delete\") :: (6, 60, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"unlimited clauses - unconditional final branch\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: (4, 40) :: (6, 60) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1) :: (2, 2) :: (3, -1) :: (4, 4) :: (5, 0) :: (6, 0) :: Nil).toDF(\"key\", \"n\"),\n    clauses =\n      update(\"*\", \"s.key = 1\") :: update(\"n = 400\", \"s.key = 4\") ::\n        delete(\"s.key = 3\") :: delete(condition = null) ::\n        insert(\"*\", \"s.key = 2\") :: insert(\"(key, n) VALUES (50, 50)\", condition = null) :: Nil,\n    expectedTableData = ((0, 0) :: (1, 1) :: (2, 2) :: (4, 400) :: (50, 50) :: Nil).toDF(),\n    expectedCdcDataWithoutVersion = (\n      (2, 2, \"insert\") :: (50, 50, \"insert\") ::\n        (1, 10, \"update_preimage\") :: (1, 1, \"update_postimage\") ::\n        (4, 40, \"update_preimage\") :: (4, 400, \"update_postimage\") ::\n        (3, 30, \"delete\") :: (6, 60, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdc(\"basic schema evolution\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1, \"a\") :: (2, 2, \"b\") :: (3, -1, \"c\") :: Nil).toDF(\"key\", \"n\", \"text\"),\n    insert = \"*\",\n    update = \"*\",\n    deleteWhen = \"s.key = 3\",\n    expectedTableData = ((0, 0, null) :: (1, 1, \"a\") :: (2, 2, \"b\") :: Nil)\n      .asInstanceOf[Seq[(Int, Int, String)]].toDF(),\n    expectedCdcDataWithoutVersion = (\n        (1, 10, null, \"update_preimage\") ::\n        (1, 1, \"a\", \"update_postimage\") ::\n        (2, 2, \"b\", \"insert\") ::\n        (3, 30, null, \"delete\") :: Nil)\n      .asInstanceOf[List[(Integer, Integer, String, String)]]\n      .toDF(\"key\", \"targetVal\", \"srcVal\", \"_change_type\"),\n    confs = (DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\") :: Nil\n  )\n\n  testMergeCdcUnlimitedClauses(\"schema evolution with non-nullable schema\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1, \"a\") :: (2, 2, \"b\") :: (3, -1, \"c\") :: Nil).toDF(\"key\", \"n\", \"text\"),\n    mergeCondition = \"t.key = s.key\",\n    clauses = delete(condition = \"s.key = 3\") :: update(\"*\") :: insert(\"*\") :: Nil,\n    expectedTableData = ((0, 0, null) :: (1, 1, \"a\") :: (2, 2, \"b\") :: Nil)\n      .asInstanceOf[Seq[(Int, Int, String)]].toDF(),\n    expectedCdcDataWithoutVersion = (\n      (1, 10, null, \"update_preimage\") ::\n        (1, 1, \"a\", \"update_postimage\") ::\n        (2, 2, \"b\", \"insert\") ::\n        (3, 30, null, \"delete\") :: Nil)\n      .asInstanceOf[List[(Integer, Integer, String, String)]]\n      .toDF(\"key\", \"targetVal\", \"srcVal\", \"_change_type\"),\n    confs = (DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\") :: Nil,\n    targetTableSchema = Some(StructType(Seq(\n      StructField(\"key\", IntegerType, nullable = false),\n      StructField(\"n\", IntegerType, nullable = false))))\n  )\n\n  testMergeCdcUnlimitedClauses(\"schema evolution with non-nullable schema - matched only\")(\n    target = ((0, 0) :: (1, 10) :: (3, 30) :: Nil).toDF(\"key\", \"n\"),\n    source = ((1, 1, \"a\") :: (2, 2, \"b\") :: (3, -1, \"c\") :: Nil).toDF(\"key\", \"n\", \"text\"),\n    mergeCondition = \"t.key = s.key\",\n    clauses = delete(condition = \"s.key = 3\") :: update(\"*\") :: Nil,\n    expectedTableData = ((0, 0, null) :: (1, 1, \"a\") :: Nil)\n      .asInstanceOf[Seq[(Int, Int, String)]].toDF(),\n    expectedCdcDataWithoutVersion = (\n      (1, 10, null, \"update_preimage\") ::\n        (1, 1, \"a\", \"update_postimage\") ::\n        (3, 30, null, \"delete\") :: Nil)\n      .asInstanceOf[List[(Integer, Integer, String, String)]]\n      .toDF(\"key\", \"targetVal\", \"srcVal\", \"_change_type\"),\n    confs = (DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\") :: Nil,\n    targetTableSchema = Some(StructType(Seq(\n      StructField(\"key\", IntegerType, nullable = false),\n      StructField(\"n\", IntegerType, nullable = false))))\n  )\n\n  testMergeCdcUnlimitedClauses(\"unconditional delete only with duplicate matches\")(\n    target = Seq(0, 1).toDF(\"value\"),\n    source = Seq(1, 1).toDF(\"value\"),\n    mergeCondition = \"t.value = s.value\",\n    clauses = delete() :: Nil,\n    expectedTableData = Seq(0).toDF(),\n    expectedCdcDataWithoutVersion = ((1, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\n    \"unconditional delete only with duplicate matches without duplicates rows in the source\")(\n    target = Seq(0).toDF(\"value\"),\n    source = ((0, 0) :: (0, 1) :: Nil).toDF(\"col1\", \"col2\"),\n    mergeCondition = \"t.value = s.col1\",\n    clauses = delete() :: Nil,\n    expectedTableData =\n      Nil.asInstanceOf[List[Integer]]\n        .toDF(\"value\"),\n    expectedCdcDataWithoutVersion = ((0, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\n    \"unconditional delete only with duplicate matches with duplicates in the target\")(\n    target = Seq(0, 1, 1).toDF(\"value\"),\n    source = Seq(1, 1).toDF(\"value\"),\n    mergeCondition = \"t.value = s.value\",\n    clauses = delete() :: Nil,\n    expectedTableData = Seq(0).toDF(),\n    expectedCdcDataWithoutVersion = ((1, \"delete\") :: (1, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"unconditional delete only with target-only merge condition\")(\n    target = Seq(0, 1).toDF(\"value\"),\n    source = Seq(0, 1).toDF(\"value\"),\n    mergeCondition = \"t.value > 0\",\n    clauses = delete() :: Nil,\n    expectedTableData = Seq(0).toDF(),\n    expectedCdcDataWithoutVersion = ((1, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\n    \"unconditional delete only with target-only merge condition with duplicates in the target\")(\n    target = Seq(0, 1, 1).toDF(\"value\"),\n    source = Seq(0, 1).toDF(\"value\"),\n    mergeCondition = \"t.value > 0\",\n    clauses = delete() :: Nil,\n    expectedTableData = Seq(0).toDF(),\n    expectedCdcDataWithoutVersion = ((1, \"delete\") :: (1, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"unconditional delete only with source-only merge condition\")(\n    target = Seq(0, 1).toDF(\"value\"),\n    source = Seq(0, 1).toDF(\"value\"),\n    mergeCondition = \"s.value < 2\",\n    clauses = delete() :: Nil,\n    expectedTableData =\n      Nil.asInstanceOf[List[Integer]]\n      .toDF(\"value\"),\n    expectedCdcDataWithoutVersion = ((0, \"delete\") :: (1, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\n    \"unconditional delete only with source-only merge condition with duplicates in the target\")(\n    target = Seq(0, 1, 1).toDF(\"value\"),\n    source = Seq(0, 1).toDF(\"value\"),\n    mergeCondition = \"s.value < 2\",\n    clauses = delete() :: Nil,\n    expectedTableData =\n      Nil.asInstanceOf[List[Integer]]\n        .toDF(\"value\"),\n    expectedCdcDataWithoutVersion = ((0, \"delete\") :: (1, \"delete\") :: (1, \"delete\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"unconditional delete with duplicate matches + insert\")(\n    target = ((1, 1) :: (2, 2) :: Nil).toDF(\"key\", \"value\"),\n    source = ((1, 10) :: (1, 100) :: (3, 30) :: (3, 300) :: Nil).toDF(\"key\", \"value\"),\n    mergeCondition = \"s.key = t.key\",\n    clauses = delete() ::\n      insert(values = \"(key, value) VALUES (s.key, s.value)\") :: Nil,\n    expectedTableData = ((2, 2) :: (3, 30) :: (3, 300) :: Nil).toDF(\"key\", \"value\"),\n    expectedCdcDataWithoutVersion =\n      ((1, 1, \"delete\") :: (3, 30, \"insert\") :: (3, 300, \"insert\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\n    \"unconditional delete with duplicate matches + insert with duplicate rows\")(\n    target = ((1, 1) :: (2, 2) :: Nil).toDF(\"key\", \"value\"),\n    source = ((1, 10) :: (1, 100) :: (3, 30) :: (3, 300) :: (3, 300) :: Nil).toDF(\"key\", \"value\"),\n    mergeCondition = \"s.key = t.key\",\n    clauses = delete() ::\n      insert(values = \"(key, value) VALUES (s.key, s.value)\") :: Nil,\n    expectedTableData = ((2, 2) :: (3, 30) :: (3, 300) :: (3, 300) :: Nil).toDF(\"key\", \"value\"),\n    expectedCdcDataWithoutVersion =\n      ((1, 1, \"delete\") :: (3, 30, \"insert\") :: (3, 300, \"insert\") ::\n        (3, 300, \"insert\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"unconditional delete with duplicate matches \" +\n      \"+ insert a duplicate of the unmatched target rows\")(\n    target = Seq(1, 2).toDF(\"value\"),\n    source = ((1, 10) :: (1, 100) :: (3, 2) :: Nil).toDF(\"col1\", \"col2\"),\n    mergeCondition = \"s.col1 = t.value\",\n    clauses = delete() ::\n      insert(values = \"(value) VALUES (col2)\") :: Nil,\n    expectedTableData = Seq(2, 2).toDF(),\n    expectedCdcDataWithoutVersion =\n      ((1, \"delete\") :: (2, \"insert\") :: Nil).toDF()\n  )\n\n  testMergeCdcUnlimitedClauses(\"all conditions failed for all rows\")(\n    target = Seq((1, \"a\"), (2, \"b\")).toDF(\"key\", \"val\"),\n    source = Seq((1, \"t\"), (2, \"u\")).toDF(\"key\", \"val\"),\n    clauses =\n      update(\"t.val = s.val\", \"s.key = 10\") :: insert(\"*\", \"s.key = 11\") :: Nil,\n    expectedTableData =\n      Seq((1, \"a\"), (2, \"b\")).asInstanceOf[List[(Integer, String)]].toDF(\"key\", \"targetVal\"),\n    expectedCdcDataWithoutVersion =\n      Nil.asInstanceOf[List[(Integer, String, String)]]\n      .toDF(\"key\", \"targetVal\", \"_change_type\")\n  )\n\n  testMergeCdcUnlimitedClauses(\"unlimited clauses schema evolution\")(\n    // 1 and 2 should be updated from the source, 3 and 4 should be deleted. Only 5 is unchanged\n    target = Seq((1, \"a\"), (2, \"b\"), (3, \"c\"), (4, \"d\"), (5, \"e\")).toDF(\"key\", \"targetVal\"),\n    // 1 and 2 should be updated into the target, 6 and 7 should be inserted. 8 should be ignored\n    source = Seq((1, \"t\"), (2, \"u\"), (3, \"v\"), (4, \"w\"), (6, \"x\"), (7, \"y\"), (8, \"z\"))\n      .toDF(\"key\", \"srcVal\"),\n    clauses =\n      update(\"targetVal = srcVal\", \"s.key = 1\") :: update(\"*\", \"s.key = 2\") ::\n        delete(\"s.key = 3\") :: delete(\"s.key = 4\") ::\n        insert(\"(key) VALUES (s.key)\", \"s.key = 6\") :: insert(\"*\", \"s.key = 7\") :: Nil,\n    expectedTableData =\n      ((1, \"t\", null) :: (2, \"b\", \"u\") :: (5, \"e\", null) ::\n        (6, null, null) :: (7, null, \"y\") :: Nil)\n        .asInstanceOf[List[(Integer, String, String)]].toDF(\"key\", \"targetVal\", \"srcVal\"),\n    expectedCdcDataWithoutVersion = (\n        (1, \"a\", null, \"update_preimage\") ::\n        (1, \"t\", null, \"update_postimage\") ::\n        (2, \"b\", null, \"update_preimage\") ::\n        (2, \"b\", \"u\", \"update_postimage\") ::\n        (3, \"c\", null, \"delete\") ::\n        (4, \"d\", null, \"delete\") ::\n        (6, null, null, \"insert\") ::\n        (7, null, \"y\", \"insert\") :: Nil)\n      .asInstanceOf[List[(Integer, String, String, String)]]\n      .toDF(\"key\", \"targetVal\", \"srcVal\", \"_change_type\"),\n    confs = (DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\") :: Nil\n  )\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/cdc/UpdateCDCSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.cdc\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddCDCFile, AddFile, RemoveFile}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\ntrait UpdateCDCTests  extends UpdateSQLMixin\n  with DeltaColumnMappingTestUtils\n  with DeltaDMLTestUtilsPathBased\n  with CDCTestMixin {\n  import testImplicits._\n\n  test(\"CDC for unconditional update\") {\n    append(Seq((1, 1), (2, 2), (3, 3), (4, 4)).toDF(\"key\", \"value\"))\n\n    checkUpdate(\n      condition = None,\n      setClauses = \"value = -1\",\n      expectedResults = Row(1, -1) :: Row(2, -1) :: Row(3, -1) :: Row(4, -1) :: Nil)\n\n    val latestVersion = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion, latestVersion)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(1, 1, \"update_preimage\", latestVersion) ::\n        Row(1, -1, \"update_postimage\", latestVersion) ::\n        Row(2, 2, \"update_preimage\", latestVersion) ::\n        Row(2, -1, \"update_postimage\", latestVersion) ::\n        Row(3, 3, \"update_preimage\", latestVersion) ::\n        Row(3, -1, \"update_postimage\", latestVersion) ::\n        Row(4, 4, \"update_preimage\", latestVersion) ::\n        Row(4, -1, \"update_postimage\", latestVersion) ::\n        Nil)\n  }\n\n  test(\"CDC for conditional update on all rows\") {\n    append(Seq((1, 1), (2, 2), (3, 3), (4, 4)).toDF(\"key\", \"value\"))\n\n    checkUpdate(\n      condition = Some(\"key < 10\"),\n      setClauses = \"value = -1\",\n      expectedResults = Row(1, -1) :: Row(2, -1) :: Row(3, -1) :: Row(4, -1) :: Nil)\n\n    val latestVersion = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion, latestVersion)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(1, 1, \"update_preimage\", latestVersion) ::\n        Row(1, -1, \"update_postimage\", latestVersion) ::\n        Row(2, 2, \"update_preimage\", latestVersion) ::\n        Row(2, -1, \"update_postimage\", latestVersion) ::\n        Row(3, 3, \"update_preimage\", latestVersion) ::\n        Row(3, -1, \"update_postimage\", latestVersion) ::\n        Row(4, 4, \"update_preimage\", latestVersion) ::\n        Row(4, -1, \"update_postimage\", latestVersion) ::\n        Nil)\n  }\n\n  test(\"CDC for point update\") {\n    append(Seq((1, 1), (2, 2), (3, 3), (4, 4)).toDF(\"key\", \"value\"))\n\n    checkUpdate(\n      condition = Some(\"key = 1\"),\n      setClauses = \"value = -1\",\n      expectedResults = Row(1, -1) :: Row(2, 2) :: Row(3, 3) :: Row(4, 4) :: Nil)\n\n    val latestVersion = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion, latestVersion)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(1, 1, \"update_preimage\", latestVersion) ::\n        Row(1, -1, \"update_postimage\", latestVersion) ::\n        Nil)\n  }\n\n  test(\"CDC for repeated point update\") {\n    append(Seq((1, 1), (2, 2), (3, 3), (4, 4)).toDF(\"key\", \"value\"))\n\n    checkUpdate(\n      condition = Some(\"key = 1\"),\n      setClauses = \"value = -1\",\n      expectedResults = Row(1, -1) :: Row(2, 2) :: Row(3, 3) :: Row(4, 4) :: Nil)\n\n    val latestVersion1 = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion1, latestVersion1)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(1, 1, \"update_preimage\", latestVersion1) ::\n        Row(1, -1, \"update_postimage\", latestVersion1) ::\n        Nil)\n\n    checkUpdate(\n      condition = Some(\"key = 3\"),\n      setClauses = \"value = -3\",\n      expectedResults = Row(1, -1) :: Row(2, 2) :: Row(3, -3) :: Row(4, 4) :: Nil)\n\n    val latestVersion2 = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion1, latestVersion2)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(1, 1, \"update_preimage\", latestVersion1) ::\n        Row(1, -1, \"update_postimage\", latestVersion1) ::\n        Row(3, 3, \"update_preimage\", latestVersion2) ::\n        Row(3, -3, \"update_postimage\", latestVersion2) ::\n        Nil)\n  }\n\n  test(\"CDC for partition-optimized update\") {\n    append(\n      Seq((1, 1, 1), (2, 2, 0), (3, 3, 1), (4, 4, 0)).toDF(\"key\", \"value\", \"part\"),\n      partitionBy = Seq(\"part\"))\n\n    checkUpdate(\n      condition = Some(\"part = 1\"),\n      setClauses = \"value = -1\",\n      expectedResults = Row(1, -1) :: Row(2, 2) :: Row(3, -1) :: Row(4, 4) :: Nil)\n\n    val latestVersion = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion, latestVersion)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(1, 1, 1, \"update_preimage\", latestVersion) ::\n        Row(1, -1, 1, \"update_postimage\", latestVersion) ::\n        Row(3, 3, 1, \"update_preimage\", latestVersion) ::\n        Row(3, -1, 1, \"update_postimage\", latestVersion) ::\n        Nil)\n  }\n\n\n  test(\"update a partitioned CDC enabled table to set the partition column to null\") {\n    val tableName = \"part_table_test\"\n    withTable(tableName) {\n      Seq((0, 0, 0), (1, 1, 1), (2, 2, 2))\n        .toDF(\"key\", \"partition_column\", \"value\")\n        .write\n        .partitionBy(\"partition_column\")\n        .format(\"delta\")\n        .saveAsTable(tableName)\n      sql(s\"INSERT INTO $tableName VALUES (4, 4, 4)\")\n      sql(s\"UPDATE $tableName SET partition_column = null WHERE partition_column = 4\")\n      checkAnswer(\n        computeCDC(spark,\n          DeltaLog.forTable(\n            spark,\n            spark.sessionState.sqlParser.parseTableIdentifier(tableName)\n          ), 1, 2)\n          .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n        Row(4, 4, 4, \"insert\", 1) ::\n          Row(4, 4, 4, \"update_preimage\", 2) ::\n          Row(4, null, 4, \"update_postimage\", 2)  :: Nil)\n    }\n  }\n}\n\ntrait UpdateCDCWithDeletionVectorsTests extends UpdateSQLWithDeletionVectorsMixin\n  with CDCTestMixin {\n  test(\"UPDATE with DV write CDC files explicitly\") {\n    append(spark.range(0, 10, 1, numPartitions = 2).toDF())\n    executeUpdate(tableSQLIdentifier, \"id = -1\", \"id % 4 = 0\")\n\n    val latestVersion = deltaLog.update().version\n    checkAnswer(\n      computeCDC(spark, deltaLog, latestVersion, latestVersion)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP),\n      Row(0, \"update_preimage\", latestVersion) ::\n        Row(-1, \"update_postimage\", latestVersion) ::\n        Row(4, \"update_preimage\", latestVersion) ::\n        Row(-1, \"update_postimage\", latestVersion) ::\n        Row(8, \"update_preimage\", latestVersion) ::\n        Row(-1, \"update_postimage\", latestVersion) ::\n        Nil)\n\n    val allActions = deltaLog.getChanges(latestVersion).flatMap(_._2).toSeq\n    val addActions = allActions.collect { case f: AddFile => f }\n    val removeActions = allActions.collect { case f: RemoveFile => f }\n    val cdcActions = allActions.collect { case f: AddCDCFile => f }\n\n    assert(addActions.count(_.deletionVector != null) === 2)\n    assert(removeActions.size === 2)\n    assert(cdcActions.nonEmpty)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/clustering/ClusteredTableClusteringSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.clustering\n\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtils\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass ClusteredTableClusteringSuite extends SparkFunSuite\n  with SharedSparkSession\n  with ClusteredTableTestUtils\n  with DeltaSQLCommandTest {\n  import testImplicits._\n\n  private val table: String = \"test_table\"\n\n  // Ingest data to create numFiles files with one row in each file.\n  private def addFiles(table: String, numFiles: Int): Unit = {\n    val df = (1 to numFiles).map(i => (i, i)).toDF(\"col1\", \"col2\")\n    withSQLConf(SQLConf.MAX_RECORDS_PER_FILE.key -> \"1\") {\n      df.write.format(\"delta\").mode(\"append\").saveAsTable(table)\n    }\n  }\n\n  private def getFiles(table: String): Set[AddFile] = {\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n    deltaLog.update().allFiles.collect().toSet\n  }\n\n  private def assertClustered(files: Set[AddFile]): Unit = {\n    assert(files.forall(_.clusteringProvider.contains(ClusteredTableUtils.clusteringProvider)))\n  }\n\n  private def assertNotClustered(files: Set[AddFile]): Unit = {\n    assert(files.forall(_.clusteringProvider.isEmpty))\n  }\n\n  test(\"optimize clustered table\") {\n    withSQLConf(SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\") {\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1, col2\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n        assertNotClustered(files0)\n\n        // Optimize should cluster the data into two 2 files since MAX_RECORDS_PER_FILE is 2.\n        runOptimize(table) { metrics =>\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n\n        val files1 = getFiles(table)\n        assert(files1.size == 2)\n        assertClustered(files1)\n      }\n    }\n  }\n\n  test(\"cluster by 1 column\") {\n    withSQLConf(SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\") {\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n        assertNotClustered(files0)\n\n        // Optimize should cluster the data into two 2 files since MAX_RECORDS_PER_FILE is 2.\n        runOptimize(table) { metrics =>\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n\n        val files1 = getFiles(table)\n        assert(files1.size == 2)\n        assertClustered(files1)\n      }\n    }\n  }\n\n  test(\"optimize clustered table with batching\") {\n    Seq((\"1\", 2), (\"1g\", 1)).foreach { case (batchSize, optimizeCommits) =>\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1, col2\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n        assertNotClustered(files0)\n\n        val totalSize = files0.toSeq.map(_.size).sum\n        val halfSize = totalSize / 2\n\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_BATCH_SIZE.key -> batchSize,\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> halfSize.toString,\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_TARGET_CUBE_SIZE.key -> halfSize.toString) {\n          // Optimize should create 2 cubes, which will be in separate batches if the batch size\n          // is small enough\n          runOptimize(table) { metrics =>\n            assert(metrics.numFilesRemoved == 4)\n            assert(metrics.numFilesAdded == 2)\n          }\n\n          val files1 = getFiles(table)\n          assert(files1.size == 2)\n          assertClustered(files1)\n\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n\n          val commits = deltaLog.history.getHistory(None)\n          assert(commits.filter(_.operation == \"OPTIMIZE\").length == optimizeCommits)\n        }\n      }\n    }\n  }\n\n  test(\"optimize clustered table with batching on an empty table\") {\n    withClusteredTable(\n      table = table,\n      schema = \"col1 int, col2 int\",\n      clusterBy = \"col1, col2\") {\n      withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_BATCH_SIZE.key -> \"1g\") {\n        runOptimize(table) { metrics =>\n          assert(metrics.numFilesRemoved == 0)\n          assert(metrics.numFilesAdded == 0)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/clustering/ClusteringMetadataDomainSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.clustering\n\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteringColumn\n\nimport org.apache.spark.SparkFunSuite\n\nclass ClusteringMetadataDomainSuite extends SparkFunSuite {\n  test(\"serialized string follows the spec\") {\n    val clusteringColumns = Seq(ClusteringColumn(Seq(\"col1\", \"`col2,col3`\", \"`col4.col5`,col6\")))\n    val clusteringMetadataDomain = ClusteringMetadataDomain.fromClusteringColumns(clusteringColumns)\n    val serializedString = clusteringMetadataDomain.toDomainMetadata.json\n    assert(serializedString ===\n      \"\"\"|{\"domainMetadata\":{\"domain\":\"delta.clustering\",\"configuration\":\n         |\"{\\\"clusteringColumns\\\":[[\\\"col1\\\",\\\"`col2,col3`\\\",\\\"`col4.col5`,col6\\\"]],\n         |\\\"domainName\\\":\\\"delta.clustering\\\"}\",\"removed\":false}}\"\"\".stripMargin.replace(\"\\n\", \"\"))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/clustering/ClusteringTableFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.clustering\n\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions}\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtils\nimport org.apache.spark.sql.delta.skipping.clustering.ClusteredTableUtils\nimport org.apache.spark.sql.delta.{ClusteringTableFeature, DeltaAnalysisException, DeltaLog, TableFeature}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass ClusteringTableFeatureSuite extends SparkFunSuite\n  with SharedSparkSession\n  with ClusteredTableTestUtils\n  with DeltaSQLCommandTest {\n  import testImplicits._\n\n  test(\"create table without cluster by clause cannot set clustering table properties\") {\n    withTable(\"tbl\") {\n      val e = intercept[DeltaAnalysisException] {\n        sql(\"CREATE TABLE tbl(a INT, b STRING) USING DELTA \" +\n          \"TBLPROPERTIES('delta.feature.clustering' = 'supported')\")\n      }\n      checkError(\n        e,\n        \"DELTA_CREATE_TABLE_SET_CLUSTERING_TABLE_FEATURE_NOT_ALLOWED\",\n        parameters = Map(\"tableFeature\" -> \"clustering\"))\n    }\n  }\n\n  test(\"use alter table set table properties to enable clustering is not allowed.\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(a INT, b STRING) USING DELTA\")\n      val e = intercept[DeltaAnalysisException] {\n        sql(\"ALTER TABLE tbl SET TBLPROPERTIES ('delta.feature.clustering' = 'supported')\")\n      }\n      checkError(\n        e,\n        \"DELTA_ALTER_TABLE_SET_CLUSTERING_TABLE_FEATURE_NOT_ALLOWED\",\n        parameters = Map(\"tableFeature\" -> \"clustering\"))\n    }\n  }\n\n  test(\"alter table cluster by partitioned tables is not allowed.\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl(a INT, b STRING) USING DELTA PARTITIONED BY (a)\")\n      val e1 = intercept[DeltaAnalysisException] {\n        sql(\"ALTER TABLE tbl CLUSTER BY (a)\")\n      }\n      checkError(\n        e1,\n        \"DELTA_ALTER_TABLE_CLUSTER_BY_ON_PARTITIONED_TABLE_NOT_ALLOWED\",\n        parameters = Map.empty)\n\n      val e2 = intercept[DeltaAnalysisException] {\n        sql(\"ALTER TABLE tbl CLUSTER BY NONE\")\n      }\n      checkError(\n        e2,\n        \"DELTA_ALTER_TABLE_CLUSTER_BY_ON_PARTITIONED_TABLE_NOT_ALLOWED\",\n        parameters = Map.empty)\n    }\n  }\n\n   test(\"alter table cluster by unpartitioned tables is supported.\") {\n    val table = \"tbl\"\n    withTable(table) {\n      sql(s\"CREATE TABLE $table (a INT, b STRING) USING DELTA\")\n      val (_, startingSnapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n      assert(!ClusteredTableUtils.isSupported(startingSnapshot.protocol))\n      val clusterByLogs = Log4jUsageLogger.track {\n        sql(s\"ALTER TABLE $table CLUSTER BY (a)\")\n      }.filter { e =>\n        e.metric == MetricDefinitions.EVENT_TAHOE.name &&\n          e.tags.get(\"opType\").contains(\"delta.ddl.alter.clusterBy\")\n      }\n      assert(clusterByLogs.nonEmpty)\n      val clusterByLogJson = JsonUtils.fromJson[Map[String, Any]](clusterByLogs.head.blob)\n      assert(!clusterByLogJson(\"isClusterByNoneSkipped\").asInstanceOf[Boolean])\n      val (_, finalSnapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n      assert(ClusteredTableUtils.isSupported(finalSnapshot.protocol))\n      val dependentFeatures = TableFeature.getDependentFeatures(ClusteringTableFeature)\n      dependentFeatures.foreach { feature =>\n        assert(finalSnapshot.protocol.isFeatureSupported(feature))\n      }\n\n      withSQLConf(SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\") {\n        val df = (1 to 4).map(i => (i, i.toString)).toDF(\"a\", \"b\")\n        withSQLConf(SQLConf.MAX_RECORDS_PER_FILE.key -> \"1\") {\n          df.write.format(\"delta\").mode(\"append\").saveAsTable(table)\n        }\n\n        // Optimize should cluster the data into two 2 files since MAX_RECORDS_PER_FILE is 2.\n        runOptimize(table) { metrics =>\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n        val files1 = deltaLog.update().allFiles.collect().toSet\n        assert(files1.size == 2)\n        assert(files1.forall(_.clusteringProvider.contains(ClusteredTableUtils.clusteringProvider)))\n\n        // Check if min-max intervals of 'a' are sorted\n        val minMaxIntervals = files1.map { file =>\n          val stats = JsonUtils.mapper.readTree(file.stats)\n          (stats.get(\"minValues\").get(\"a\").asInt, stats.get(\"maxValues\").get(\"a\").asInt)\n        }\n\n        val sortedAsc = minMaxIntervals.sliding(2).forall {\n          case Seq((_, maxA1), (minA2, _)) => maxA1.asInstanceOf[Int] < minA2.asInstanceOf[Int]\n          case _ => true\n        }\n\n        val sortedDesc = minMaxIntervals.sliding(2).forall {\n          case Seq((minA1, _), (_, maxA2)) => minA1.asInstanceOf[Int] > maxA2.asInstanceOf[Int]\n          case _ => true\n        }\n\n        assert(sortedAsc || sortedDesc, \"Min-max intervals for column 'a' are not sorted.\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/columnmapping/DropColumnMappingFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.columnmapping\n\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaConfigs._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.util.ManualClock\n\n/**\n * Test dropping column mapping feature from a table.\n */\nclass DropColumnMappingFeatureSuite extends RemoveColumnMappingSuiteUtils {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // All the drop feature tests below are based on the drop feature with history truncation\n    // implementation. The fast drop feature implementation does not require any waiting time.\n    // The fast drop feature implementation is tested extensively in the DeltaFastDropFeatureSuite.\n    spark.conf.set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, false.toString)\n  }\n\n  val clock = new ManualClock(System.currentTimeMillis())\n  test(\"column mapping cannot be dropped without the feature flag\") {\n    withSQLConf(ALLOW_COLUMN_MAPPING_REMOVAL.key -> \"false\") {\n      sql(s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |        'delta.minReaderVersion' = '3',\n         |        'delta.minWriterVersion' = '7')\n         |AS SELECT 1 as a\n         |\"\"\".stripMargin)\n\n      intercept[DeltaColumnMappingUnsupportedException] {\n        dropColumnMappingTableFeature()\n      }\n    }\n  }\n\n  test(\"table without column mapping enabled\") {\n    sql(s\"\"\"CREATE TABLE $testTableName\n           |USING delta\n           |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none')\n           |AS SELECT 1 as a\n           |\"\"\".stripMargin)\n\n    val e = intercept[DeltaTableFeatureException] {\n      dropColumnMappingTableFeature()\n    }\n    checkError(e,\n      DeltaErrors.dropTableFeatureFeatureNotSupportedByProtocol(\".\").getErrorClass,\n      parameters = Map(\"feature\" -> \"columnMapping\"))\n  }\n\n  test(\"invalid column names\") {\n    val invalidColName1 = colName(\"col1\")\n    val invalidColName2 = colName(\"col2\")\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (a INT, `$invalidColName1` INT, `$invalidColName2` INT)\n         |USING delta\n         |TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\n         |\"\"\".stripMargin)\n    val e = intercept[DeltaAnalysisException] {\n      // Try to drop column mapping.\n      dropColumnMappingTableFeature()\n    }\n    checkError(e,\n      \"DELTA_INVALID_COLUMN_NAMES_WHEN_REMOVING_COLUMN_MAPPING\",\n      parameters = Map(\"invalidColumnNames\" ->\n        \"col1 with special chars ,;{}()\\n\\t=, col2 with special chars ,;{}()\\n\\t=\"))\n  }\n\n  test(\"drop column mapping from a table without table feature\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |        '${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = 'false',\n         |        'delta.minReaderVersion' = '3',\n         |        'delta.minWriterVersion' = '7')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testDroppingColumnMapping()\n  }\n\n  test(\"drop column mapping from a table with table feature\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |        '${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = 'false',\n         |        'delta.minReaderVersion' = '3',\n         |        'delta.minWriterVersion' = '7')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testDroppingColumnMapping()\n  }\n\n  test(\"drop column mapping from a table without column mapping table property\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |        '${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = 'false',\n         |        'delta.minReaderVersion' = '3',\n         |        'delta.minWriterVersion' = '7')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    unsetColumnMappingProperty(useUnset = true)\n    val e = intercept[DeltaTableFeatureException] {\n      dropColumnMappingTableFeature()\n    }\n    checkError(\n      e,\n      \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n      parameters = Map(\n        \"feature\" -> \"columnMapping\",\n        \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n        \"logRetentionPeriod\" -> \"30 days\",\n        \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\")\n    )\n  }\n\n  test(\"drop column mapping in id mode\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'id',\n         |        '${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = 'false',\n         |        'delta.minReaderVersion' = '3',\n         |        'delta.minWriterVersion' = '7')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testDroppingColumnMapping()\n  }\n\n  def testDroppingColumnMapping(): Unit = {\n    withSQLConf(\n      \"spark.databricks.delta.vacuum.enforceDeletedFileAndLogRetentionDurationCompatibility\" ->\n        \"false\") {\n      // Verify the input data is as expected.\n      val originalData = spark.table(tableName = testTableName).select(logicalColumnName).collect()\n      // Add a schema comment and verify it is preserved after the rewrite.\n      val comment = \"test comment\"\n      sql(s\"ALTER TABLE $testTableName ALTER COLUMN $logicalColumnName COMMENT '$comment'\")\n\n      val table = DeltaTableV2(spark, TableIdentifier(tableName = testTableName), \"\")\n      val originalSnapshot = table.initialSnapshot\n\n      assert(originalSnapshot.schema.head.getComment().get == comment,\n        \"Renamed column should preserve comment.\")\n      val originalFiles = getFiles(originalSnapshot)\n      val startingVersion = originalSnapshot.version\n\n      val e = intercept[DeltaTableFeatureException] {\n        dropColumnMappingTableFeature()\n      }\n      checkError(\n        e,\n        \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n        parameters = Map(\n          \"feature\" -> \"columnMapping\",\n          \"logRetentionPeriodKey\" -> \"delta.logRetentionDuration\",\n          \"logRetentionPeriod\" -> \"30 days\",\n          \"truncateHistoryLogRetentionPeriod\" -> \"24 hours\")\n      )\n\n      verifyRewrite(\n        unsetTableProperty = true,\n        table,\n        originalFiles,\n        startingVersion,\n        originalData = originalData,\n        droppedFeature = true)\n      // Verify the schema comment is preserved after the rewrite.\n      assert(deltaLog.update().schema.head.getComment().get == comment,\n        \"Should preserve the schema comment.\")\n      verifyDropFeatureTruncateHistory()\n    }\n  }\n\n  protected def verifyDropFeatureTruncateHistory() = {\n    val deltaLog1 = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName), clock)\n    // Populate the delta cache with the delta log with the right data path so it stores the clock.\n    // This is currently the only way to make sure the drop feature command uses the clock.\n    DeltaLog.clearCache()\n    DeltaLog.forTable(spark, deltaLog1.dataPath, clock)\n    // Set the log retention to 0 so that we can test truncate history.\n    sql(\n      s\"\"\"\n         |ALTER TABLE $testTableName SET TBLPROPERTIES (\n         |  '${TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION.key}' = '0 hours',\n         |  '${LOG_RETENTION.key}' = '0 hours')\n         |\"\"\".stripMargin)\n    // Pretend enough time has passed for the history to be truncated.\n    clock.advance(TimeUnit.MINUTES.toMillis(5))\n    sql(\n      s\"\"\"\n         |ALTER TABLE $testTableName DROP FEATURE ${ColumnMappingTableFeature.name} TRUNCATE HISTORY\n         |\"\"\".stripMargin)\n    val newSnapshot = deltaLog.update()\n    assert(newSnapshot.protocol.readerAndWriterFeatures.isEmpty, \"Should drop the feature.\")\n    assert(newSnapshot.protocol.minWriterVersion == 1)\n    assert(newSnapshot.protocol.minReaderVersion == 1)\n  }\n\n  protected def dropColumnMappingTableFeature(): Unit = {\n    sql(\n      s\"\"\"\n         |ALTER TABLE $testTableName DROP FEATURE ${ColumnMappingTableFeature.name}\n         |\"\"\".stripMargin)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/columnmapping/RemoveColumnMappingCDCSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.columnmapping\n\nimport org.apache.spark.sql.delta.DeltaConfigs\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.DeltaUnsupportedOperationException\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\n/**\n * Test suite for removing column mapping(CM) from a table with CDC enabled. Test different\n * scenarios with respect to table schema at different points of time. There are a few events we\n * are interested in: UP: enable column mapping DOWN: disable column mapping RE, DROP: rename,\n * drop a column\n *\n * And there are two parameters for reading CDC: START: starting version END: ending version\n *\n * We test all the possible combinations of these events and parameters.\n */\nclass RemoveColumnMappingCDCSuite extends RemoveColumnMappingSuiteUtils {\n\n  // These two cases will fail because latest schema will be used to read CDC.\n  // Table doesn't have column mapping enabled in any of the start, end or latest versions.\n  // So it defaults to the non-column mapping behavior.\n  runScenario(Start, End, Upgrade, Rename, Downgrade)(ReadCDCIncompatibleDataSchema)\n  runScenario(Start, End, Upgrade, Drop, Downgrade)(ReadCDCIncompatibleDataSchema)\n\n  runScenario(Start, End, Upgrade, Rename, Downgrade, Upgrade)(ReadCDCSuccess)\n  runScenario(Start, End, Upgrade, Drop, Downgrade, Upgrade)(ReadCDCSuccess)\n\n  // This will use the endVersion schema to read CDC because endVersion has columnampping enabled.\n  runScenario(Start, Upgrade, End, Rename, Downgrade)(ReadCDCSuccess)\n  runScenario(Start, Upgrade, End, Drop, Downgrade)(ReadCDCSuccess)\n  runScenario(Start, Upgrade, End, Rename, Downgrade, Upgrade)(ReadCDCSuccess)\n  runScenario(Start, Upgrade, End, Drop, Downgrade, Upgrade)(ReadCDCSuccess)\n\n  // Reading across non-additive schema change.\n  runScenario(Start, Upgrade, Rename, End, Downgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Drop, End, Downgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Rename, End, Downgrade, Upgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Drop, End, Downgrade, Upgrade)(ReadCDCIncompatibleSchemaChange)\n\n  runScenario(Start, Upgrade, Rename, Downgrade, End)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Drop, Downgrade, End)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Rename, Downgrade, End, Upgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Drop, Downgrade, End, Upgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Rename, Downgrade, Upgrade, End)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Start, Upgrade, Drop, Downgrade, Upgrade, End)(ReadCDCIncompatibleSchemaChange)\n\n  runScenario(Upgrade, Start, End, Rename, Downgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Start, End, Drop, Downgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Start, End, Rename, Downgrade, Upgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Start, End, Drop, Downgrade, Upgrade)(ReadCDCSuccess)\n\n  // Reading across non-additive schema change.\n  runScenario(Upgrade, Start, Rename, End, Downgrade)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Start, Drop, End, Downgrade)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Start, Rename, End, Downgrade, Upgrade)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Start, Drop, End, Downgrade, Upgrade)(ReadCDCIncompatibleDataSchema)\n\n  // Reading across non-additive schema change.\n  runScenario(Upgrade, Start, Rename, Downgrade, End)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Upgrade, Start, Drop, Downgrade, End)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Upgrade, Start, Rename, Downgrade, End, Upgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Upgrade, Start, Drop, Downgrade, End, Upgrade)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Upgrade, Start, Rename, Downgrade, Upgrade, End)(ReadCDCIncompatibleSchemaChange)\n  runScenario(Upgrade, Start, Drop, Downgrade, Upgrade, End)(ReadCDCIncompatibleDataSchema)\n\n  runScenario(Upgrade, Rename, Start, End, Downgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Drop, Start, End, Downgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Rename, Start, End, Downgrade, Upgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Drop, Start, End, Downgrade, Upgrade)(ReadCDCSuccess)\n\n  // Reading across downgrade.\n  runScenario(Upgrade, Rename, Start, Downgrade, End)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Drop, Start, Downgrade, End)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Rename, Start, Downgrade, End, Upgrade)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Drop, Start, Downgrade, End, Upgrade)(ReadCDCIncompatibleDataSchema)\n  runScenario(Upgrade, Rename, Start, Downgrade, Upgrade, End)(ReadCDCIncompatibleDataSchema)\n  // Schema is readable in this range.\n  runScenario(Upgrade, Drop, Start, Downgrade, Upgrade, End)(ReadCDCSuccess)\n\n  runScenario(Upgrade, Rename, Downgrade, Start, End)(ReadCDCSuccess)\n  runScenario(Upgrade, Drop, Downgrade, Start, End)(ReadCDCSuccess)\n  runScenario(Upgrade, Rename, Downgrade, Start, End, Upgrade)(ReadCDCSuccess)\n  runScenario(Upgrade, Drop, Downgrade, Start, End, Upgrade)(ReadCDCSuccess)\n\n  runScenario(Upgrade, Rename, Downgrade, Start, Upgrade, End)(ReadCDCSuccess)\n  runScenario(Upgrade, Drop, Downgrade, Start, Upgrade, End)(ReadCDCSuccess)\n\n  runScenario(Upgrade, Rename, Downgrade, Upgrade, Start, End)(ReadCDCSuccess)\n  runScenario(Upgrade, Drop, Downgrade, Upgrade, Start, End)(ReadCDCSuccess)\n\n  private def runScenario(operations: Operation*)(readCDC: ReadCDC): Unit = {\n    val testName = operations.map { _.toString }.mkString(\", \") + \" - \" + readCDC.toString\n    var startVersion = 0L\n    var endVersion: Option[Long] = None\n    test(testName) {\n      createTable()\n      operations.foreach {\n        case op @ Start =>\n          startVersion = deltaLog.update().version\n          op.runOperation()\n        case op @ End =>\n          op.runOperation()\n          endVersion = Some(deltaLog.update().version)\n        case op => op.runOperation()\n      }\n      readCDC.runReadCDC(startVersion, endVersion)\n    }\n  }\n\n  private abstract class Operation {\n    def runOperation(): Unit\n  }\n\n  private case object Start extends Operation {\n    override def runOperation(): Unit = {\n      insertMoreRows()\n    }\n  }\n\n  private case object End extends Operation {\n    override def runOperation(): Unit = {\n      insertMoreRows()\n    }\n  }\n\n  private case object Upgrade extends Operation {\n    override def runOperation(): Unit = {\n      enableColumnMapping()\n      insertMoreRows()\n    }\n  }\n\n  private case object Downgrade extends Operation {\n    override def runOperation(): Unit = {\n      unsetColumnMappingProperty(useUnset = false)\n      insertMoreRows()\n    }\n  }\n\n  private case object Rename extends Operation {\n    override def runOperation(): Unit = {\n      renameColumn()\n      insertMoreRows()\n    }\n  }\n\n  private case object Drop extends Operation {\n    override def runOperation(): Unit = {\n      dropColumn()\n      insertMoreRows()\n    }\n  }\n\n  private abstract class ReadCDC {\n    def runReadCDC(start: Long, end: Option[Long]): Unit\n  }\n\n  private case object ReadCDCSuccess extends ReadCDC {\n    override def runReadCDC(start: Long, end: Option[Long]): Unit = {\n      val changes = getChanges(start, end)\n      assert(changes.length > 0, \"should have read some changes\")\n      changes.foreach { row =>\n        assert(!row.anyNull, \"None of the values should be null\")\n      }\n    }\n  }\n\n  private case object ReadCDCIncompatibleSchemaChange extends ReadCDC {\n    override def runReadCDC(start: Long, end: Option[Long]): Unit = {\n      getCDCAndFailIncompatibleSchemaChange(start, end)\n    }\n  }\n\n  private case object ReadCDCIncompatibleDataSchema extends ReadCDC {\n    override def runReadCDC(start: Long, end: Option[Long]): Unit = {\n      getCDCAndFailIncompatibleDataSchema(start, end)\n    }\n  }\n\n  private def createTable(): Unit = {\n    val columnMappingMode = \"none\"\n    sql(s\"\"\"CREATE TABLE $testTableName\n             |USING delta\n             |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '$columnMappingMode',\n             |  '${DeltaConfigs.CHANGE_DATA_FEED.key}' = 'true'\n             |)\n             |AS SELECT id as $firstColumn, id + 1 as $secondColumn, id + 2 as $thirdColumn\n             |  FROM RANGE(0, $totalRows, 1, $numFiles)\n             |\"\"\".stripMargin)\n  }\n\n  private def insertMoreRows(): Unit = {\n    sql(s\"INSERT INTO $testTableName SELECT * FROM $testTableName LIMIT $totalRows\")\n  }\n\n  private def getCDCAndFailIncompatibleSchemaChange(\n      startVersion: Long,\n      endVersion: Option[Long]) = {\n    val e = intercept[DeltaUnsupportedOperationException] {\n      getChanges(startVersion, endVersion)\n    }\n    assert(e.getErrorClass === \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_SCHEMA_CHANGE\")\n  }\n\n  private def getCDCAndFailIncompatibleDataSchema(\n      startVersion: Long,\n      endVersion: Option[Long]) = {\n    val e = intercept[DeltaUnsupportedOperationException] {\n      getChanges(startVersion, endVersion)\n    }\n    assert(e.getErrorClass === \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_DATA_SCHEMA\")\n  }\n\n  private def getChanges(startVersion: Long, endVersion: Option[Long]): Array[Row] = {\n    val endVersionStr = if (endVersion.isDefined) s\", ${endVersion.get}\" else \"\"\n    sql(s\"SELECT * FROM table_changes('$testTableName', $startVersion$endVersionStr)\")\n      .collect()\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/columnmapping/RemoveColumnMappingRowTrackingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.columnmapping\n\nimport org.apache.spark.sql.delta.DeltaConfigs\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.RowId\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.Snapshot\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\nclass RemoveColumnMappingRowTrackingSuite extends RemoveColumnMappingSuiteUtils {\n  test(\"row ids are preserved\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES (\n         |'${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true'\n         |)\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName))\n\n    val snapshot = deltaLog.update()\n    val originalDf = spark.read.table(testTableName)\n    val originalRowIds = originalDf.select(logicalColumnName, RowId.QUALIFIED_COLUMN_NAME)\n      .collect()\n    val originalDomainMetadata = RowTrackingMetadataDomain.fromSnapshot(snapshot).get\n\n    testRemovingColumnMapping()\n\n    verifyRowIdsStayTheSame(originalRowIds)\n    verifyDomainMetadata(deltaLog.update(), originalDomainMetadata, diffRows = totalRows)\n\n    // Add back column mapping and remove it again. Row ids should stay the same\n    sql(\n      s\"\"\"ALTER TABLE $testTableName\n         |SET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\"\"\".stripMargin)\n    // Update a row from each file to force materialize the row ids and metadata.\n    val predicate = s\"$logicalColumnName % $rowsPerFile == 0\"\n    withSQLConf(UPDATE_USE_PERSISTENT_DELETION_VECTORS.key -> \"false\") {\n      sql(s\"UPDATE $testTableName SET $secondColumn = -1 WHERE $predicate \")\n    }\n    testRemovingColumnMapping()\n    verifyRowIdsStayTheSame(originalRowIds)\n    // High watermark increased 3 times from the original value. Rewrite, UPDATE, Rewrite.\n    verifyDomainMetadata(deltaLog.update(), originalDomainMetadata, diffRows = totalRows * 3)\n  }\n\n  private def verifyRowIdsStayTheSame(originalRowIds: Array[Row]) = {\n    val newRowIds = spark.read.table(testTableName)\n      .select(logicalColumnName, RowId.QUALIFIED_COLUMN_NAME)\n    checkAnswer(newRowIds, originalRowIds)\n  }\n\n  private def verifyDomainMetadata(\n      snapshot: Snapshot,\n      originalDomainMetadata: RowTrackingMetadataDomain,\n      diffRows: Int) = {\n    val newDomainMetadata = RowTrackingMetadataDomain.fromSnapshot(snapshot).get\n    assert(newDomainMetadata.rowIdHighWaterMark ===\n      originalDomainMetadata.rowIdHighWaterMark + diffRows,\n      \"Should increase the high watermark by the number of rewritten rows.\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/columnmapping/RemoveColumnMappingStreamingReadSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.columnmapping\n\nimport org.apache.spark.sql.delta.ColumnMappingStreamingTestUtils\nimport org.apache.spark.sql.delta.DeltaConfigs\nimport org.apache.spark.sql.delta.DeltaOptions\nimport org.apache.spark.sql.delta.DeltaRuntimeException\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.streaming.StreamTest\n\n/**\n * Test suite for removing column mapping(CM) from a streaming table. Test different\n * scenarios with respect to table schema at different points of time. There are a few events we\n * are interested in:\n * Upgrade: enable column mapping\n * Downgrade: disable column mapping\n * Rename, Drop: rename, drop a column\n *\n * And we can decide when we start the streaming read with StartStreamRead.\n *\n * We test all the possible combinations of these events.\n *\n * Additionally, we test each scenario with schema tracking enabled which in general results in\n * a failure as schema tracking prohibits reading across an Upgrade.\n */\nclass RemoveColumnMappingStreamingReadSuite\n  extends RemoveColumnMappingSuiteUtils\n  with StreamTest\n  with ColumnMappingStreamingTestUtils {\n\n  // Here the physical/logical names don't change between start and the end\n  // so it succeeds without schema tracking. Schema tracking prohibits reading across an upgrade.\n  runScenario(StartStreamRead, Upgrade, Downgrade, SuccessAndFailSchemaTracking)\n  // Here the physical names do change. We start with existing physical names but end without them.\n  runScenario(Upgrade, StartStreamRead, Downgrade, FailNonAdditiveChange)\n  // This is just reading from a normal table.\n  runScenario(Upgrade, Downgrade, StartStreamRead, Success)\n\n  // In all of this cases there is a non-additive change between the start of the stream and\n  // the end.\n  runScenario(StartStreamRead, Upgrade, Rename, Downgrade, FailNonAdditiveChange)\n  runScenario(StartStreamRead, Upgrade, Drop, Downgrade, FailNonAdditiveChange)\n  runScenario(StartStreamRead, Upgrade, Rename, Downgrade, Upgrade, FailNonAdditiveChange)\n  runScenario(StartStreamRead, Upgrade, Drop, Downgrade, Upgrade, FailNonAdditiveChange)\n\n  runScenario(Upgrade, StartStreamRead, Rename, Downgrade, FailNonAdditiveChange)\n  runScenario(Upgrade, StartStreamRead, Drop, Downgrade, FailNonAdditiveChange)\n  runScenario(Upgrade, StartStreamRead, Rename, Downgrade, Upgrade, FailNonAdditiveChange)\n  runScenario(Upgrade, StartStreamRead, Drop, Downgrade, Upgrade, FailNonAdditiveChange)\n\n  // In these cases  schema pinned at the start of the stream is different from the end schema on\n  // the physical level.\n  // Essentially, prohibit reading across the downgrade.\n  runScenario(Upgrade, Rename, StartStreamRead, Downgrade, FailNonAdditiveChange)\n  runScenario(Upgrade, Rename, StartStreamRead, Downgrade, Upgrade, FailNonAdditiveChange)\n  runScenario(Upgrade, Drop, StartStreamRead, Downgrade, FailNonAdditiveChange)\n\n  // Here the schema at the end version has different physical names than at the start version.\n  runScenario(Upgrade, Drop, StartStreamRead, Downgrade, Upgrade, FailNonAdditiveChange)\n\n  // This is just reading from a table without column mapping.\n  runScenario(Upgrade, Rename, Downgrade, StartStreamRead, Success)\n  runScenario(Upgrade, Drop, Downgrade, StartStreamRead, Success)\n\n  // Reading across the upgrade is fine without schema tracking.\n  runScenario(Upgrade, Rename, Downgrade, StartStreamRead, Upgrade, SuccessAndFailSchemaTracking)\n  runScenario(Upgrade, Drop, Downgrade, StartStreamRead, Upgrade, SuccessAndFailSchemaTracking)\n\n  private def runScenario(operations: Operation*): Unit = {\n    // Run each scenario with and without schema tracking.\n    for (shouldTrackSchema <- Seq(true, false)) {\n      withTempPath { tempPath =>\n        val metadataLocation = tempPath.getCanonicalPath\n        val testName = generateTestName(operations, shouldTrackSchema)\n        val schemaTrackingLocation = if (shouldTrackSchema) {\n          Some(metadataLocation)\n        } else {\n          None\n        }\n        test(testName) {\n          createTable()\n          // Run all actions before the stream starts\n          val streamStartIndex = operations.indexWhere(_ == StartStreamRead)\n          operations.take(streamStartIndex).foreach(_.runOperation())\n          // Run the rest as stream actions\n          val remainingActions = operations.takeRight(operations.size - streamStartIndex)\n          testStream(testTableStreamDf(schemaTrackingLocation))(\n            remainingActions.flatMap {\n              // Add an explicit StartStream so we can pass the checkpoint location.\n              case StartStreamRead => Seq(StartStream(checkpointLocation = metadataLocation))\n              // Fail scenarios when schema tracking is enabled.\n              case FailNonAdditiveChange | SuccessAndFailSchemaTracking if shouldTrackSchema =>\n                FailSchemaEvolution.toStreamActions\n              case op: StreamActionLike => op.toStreamActions\n              case op => Seq(\n                Execute { _ =>\n                  op.runOperation()\n                })\n            }: _*\n          )\n        }\n      }\n    }\n  }\n\n  private def generateTestName(operations: Seq[Operation], shouldTrackSchema: Boolean) = {\n    val testNameSuffix = if (shouldTrackSchema) {\n      \" with schema tracking\"\n    } else {\n      \"\"\n    }\n    val testName = operations.map(_.toString).mkString(\", \") + testNameSuffix\n    testName\n  }\n\n  private abstract class Operation {\n    def runOperation(): Unit = {}\n  }\n\n  private case object StartStreamRead extends Operation\n\n  private case object Upgrade extends Operation {\n    override def runOperation(): Unit = {\n      enableColumnMapping()\n    }\n  }\n\n  private case object Downgrade extends Operation {\n    override def runOperation(): Unit = {\n      unsetColumnMappingProperty(useUnset = false)\n      insertMoreRows()\n    }\n  }\n\n  private case object Rename extends Operation {\n    override def runOperation(): Unit = {\n      renameColumn()\n    }\n  }\n\n  private case object Drop extends Operation {\n    override def runOperation(): Unit = {\n      dropColumn()\n    }\n  }\n\n  private case object FailNonAdditiveChange extends Operation with StreamActionLike {\n    override def toStreamActions: Seq[StreamAction] = Seq(\n      ProcessAllAvailableIgnoreError,\n      ExpectInStreamSchemaChangeFailure\n    )\n  }\n\n  private case object FailSchemaEvolution extends Operation with StreamActionLike {\n    override def toStreamActions: Seq[StreamAction] = Seq(\n      ProcessAllAvailableIgnoreError,\n      ExpectMetadataEvolutionException\n    )\n  }\n\n\n  private trait CheckAnswerStreamActionLike extends Operation with StreamActionLike {\n    override def toStreamActions: Seq[StreamAction] = Seq(\n      ProcessAllAvailable(),\n      // The end state should be the original consecutive rows and then -1s.\n      CheckAnswer(\n        (0 until totalRows)\n          .map( i => Row((0 until currentNumCols)\n            .map(colInd => i + colInd): _*)) ++\n          (totalRows until totalRows * 2).map(_ => Row(List.fill(currentNumCols)(-1): _*))\n          : _*\n      )\n    )\n  }\n\n  // Expected to succeed and check the rows in the sink.\n  private case object Success extends CheckAnswerStreamActionLike\n  // Expected to fail with schema tracking enabled. Schema tracking in general puts more limitations\n  // on which operations are permitted during a streaming read.\n  private case object SuccessAndFailSchemaTracking extends CheckAnswerStreamActionLike\n\n  protected val ExpectMetadataEvolutionException =\n    ExpectFailure[DeltaRuntimeException](e =>\n      assert(\n        e.asInstanceOf[DeltaRuntimeException].getErrorClass ==\n          \"DELTA_STREAMING_METADATA_EVOLUTION\" &&\n          e.getStackTrace.exists(\n            _.toString.contains(\"updateMetadataTrackingLogAndFailTheStreamIfNeeded\"))\n      )\n    )\n\n  trait StreamActionLike {\n    def toStreamActions: Seq[StreamAction]\n  }\n\n  private def testTableStreamDf(schemaTrackingLocation: Option[String]) = {\n    var streamReader = spark.readStream.format(\"delta\")\n    schemaTrackingLocation.foreach { loc =>\n      streamReader = streamReader\n        .option(DeltaOptions.SCHEMA_TRACKING_LOCATION, loc)\n    }\n    streamReader.table(testTableName)\n  }\n\n  private def createTable(columnMappingMode: String = \"none\"): Unit = {\n    sql(s\"\"\"CREATE TABLE $testTableName\n           |USING delta\n           |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = '$columnMappingMode',\n           |  '${DeltaConfigs.CHANGE_DATA_FEED.key}' = 'true'\n           |)\n           |AS SELECT id as $firstColumn, id + 1 as $secondColumn, id + 2 as $thirdColumn\n           |  FROM RANGE(0, $totalRows, 1, $numFiles)\n           |\"\"\".stripMargin)\n  }\n\n  private def insertMoreRows(v: Int = -1): Unit = {\n    val values = List.fill(currentNumCols)(v.toString).mkString(\", \")\n    sql(s\"INSERT INTO $testTableName SELECT $values FROM $testTableName LIMIT $totalRows\")\n  }\n\n  private def currentNumCols = deltaLog.update().schema.length\n  override protected def isCdcTest: Boolean = false\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/columnmapping/RemoveColumnMappingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.columnmapping\n\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.schema.DeltaInvariantViolationException\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\n/**\n * Test removing column mapping from a table.\n */\nclass RemoveColumnMappingSuite extends RemoveColumnMappingSuiteUtils {\n\n  test(\"column mapping cannot be removed without the feature flag\") {\n    withSQLConf(ALLOW_COLUMN_MAPPING_REMOVAL.key -> \"false\") {\n      sql(s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n         |AS SELECT 1 as a\n         |\"\"\".stripMargin)\n\n      intercept[DeltaColumnMappingUnsupportedException] {\n        sql(s\"\"\"\n             |ALTER TABLE $testTableName\n             |SET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none')\n             |\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"table without column mapping enabled\") {\n    sql(s\"\"\"CREATE TABLE $testTableName\n           |USING delta\n           |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none')\n           |AS SELECT 1 as a\n           |\"\"\".stripMargin)\n\n    unsetColumnMappingProperty(useUnset = true)\n  }\n\n  test(\"invalid column names\") {\n    val invalidColName1 = colName(\"col1\")\n    val invalidColName2 = colName(\"col2\")\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (a INT, `$invalidColName1` INT, `$invalidColName2` INT)\n         |USING delta\n         |TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\n         |\"\"\".stripMargin)\n    val e = intercept[DeltaAnalysisException] {\n      // Try to remove column mapping.\n      unsetColumnMappingProperty(useUnset = true)\n    }\n    checkError(e, \"DELTA_INVALID_COLUMN_NAMES_WHEN_REMOVING_COLUMN_MAPPING\", \"42K05\",\n      Map(\"invalidColumnNames\" -> s\"$invalidColName1, $invalidColName2\"))\n  }\n\n  test(\"ALTER TABLE with multiple table properties\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (a INT, b INT, c INT)\n         |USING delta\n         |TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\n         |\"\"\".stripMargin)\n    // Remove column mapping and set another property.\n    val myProperty = (\"acme\", \"1234\")\n    sql(s\"ALTER TABLE $testTableName SET TBLPROPERTIES \" +\n      s\"('delta.columnMapping.mode' = 'none', '${myProperty._1}' = '${myProperty._2}')\")\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName))\n    assert(deltaLog.update().metadata.configuration.get(myProperty._1).contains(myProperty._2))\n  }\n\n  test(\"ALTER TABLE UNSET column mapping\") {\n    val propertyToKeep = \"acme\"\n    val propertyToUnset = \"acme2\"\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (a INT, b INT, c INT)\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |'$propertyToKeep' = '1234', '$propertyToUnset' = '1234')\n         |\"\"\".stripMargin)\n    sql(s\"ALTER TABLE $testTableName UNSET TBLPROPERTIES \" +\n      s\"('delta.columnMapping.mode', '$propertyToKeep')\")\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName))\n    assert(!deltaLog.update()\n      .metadata.configuration.contains(DeltaConfigs.COLUMN_MAPPING_MODE.key))\n    assert(!deltaLog.update().metadata.configuration.contains(propertyToKeep))\n    assert(deltaLog.update().metadata.configuration.contains(propertyToUnset))\n  }\n\n  test(\"ALTER TABLE UNSET column mapping with invalid column names\") {\n    val invalidColName1 = colName(\"col1\")\n    val invalidColName2 = colName(\"col2\")\n    val propertyToKeep = \"acme\"\n    val propertyToUnset = \"acme2\"\n    sql(\n      s\"\"\"CREATE TABLE $testTableName (a INT, `$invalidColName1` INT, `$invalidColName2` INT)\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |'$propertyToKeep' = '1234', '$propertyToUnset' = '1234')\n         |\"\"\".stripMargin)\n    val e = intercept[DeltaAnalysisException] {\n      // Try to remove column mapping.\n      sql(s\"ALTER TABLE $testTableName UNSET TBLPROPERTIES \" +\n        s\"('delta.columnMapping.mode', '$propertyToKeep')\")\n    }\n    checkError(e, \"DELTA_INVALID_COLUMN_NAMES_WHEN_REMOVING_COLUMN_MAPPING\", \"42K05\",\n      Map(\"invalidColumnNames\" -> s\"$invalidColName1, $invalidColName2\"))\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName))\n    // Column mapping property should stay the same.\n    assert(deltaLog.update()\n      .metadata.configuration.contains(DeltaConfigs.COLUMN_MAPPING_MODE.key))\n    // Both other properties should stay the same.\n    assert(deltaLog.update().metadata.configuration.contains(propertyToKeep))\n    assert(deltaLog.update().metadata.configuration.contains(propertyToUnset))\n  }\n\n  test(\"remove column mapping from a table\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testRemovingColumnMapping()\n  }\n\n  test(\"remove column mapping using unset\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testRemovingColumnMapping(unsetTableProperty = true)\n  }\n\n  test(\"remove column mapping from a partitioned table\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |PARTITIONED BY (part)\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn, id % 2 as part\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testRemovingColumnMapping()\n  }\n\n  test(\"remove column mapping from a partitioned table with two part columns\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |PARTITIONED BY (part1, part2)\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn, id % 2 as part1,\n         |id % 3 as part2\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testRemovingColumnMapping()\n  }\n\n  test(\"remove column mapping from a table with only logical names\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    // Add column mapping without renaming any columns.\n    // That is, the column names in the table should be the same as the logical column names.\n    sql(\n      s\"\"\"ALTER TABLE $testTableName\n         |SET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         'delta.minReaderVersion' = '2',\n         'delta.minWriterVersion' = '5'\n         |)\"\"\".stripMargin)\n    testRemovingColumnMapping()\n  }\n\n  test(\"dropped column is added back\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    // Add column mapping without renaming any columns.\n    // That is, the column names in the table should be the same as the logical column names.\n    sql(\n      s\"\"\"ALTER TABLE $testTableName\n         |SET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         'delta.minReaderVersion' = '2',\n         'delta.minWriterVersion' = '5'\n         |)\"\"\".stripMargin)\n    // Drop the second column.\n    sql(s\"ALTER TABLE $testTableName DROP COLUMN $secondColumn\")\n    // Remove column mapping, this should rewrite the table to physically remove the dropped column.\n    testRemovingColumnMapping()\n    // Add the same column back.\n    sql(s\"ALTER TABLE $testTableName ADD COLUMN $secondColumn BIGINT\")\n    // Read from the table, ensure none of the original values of secondColumn are present.\n    assert(sql(s\"SELECT $secondColumn FROM $testTableName WHERE $secondColumn IS NOT NULL\").count()\n      == 0)\n  }\n\n  test(\"remove column mapping from a table with deletion vectors\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES (\n         |  '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n         |  '${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key}' = true)\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    sql(s\"DELETE FROM $testTableName WHERE $logicalColumnName % 2 = 0\")\n    testRemovingColumnMapping()\n  }\n\n  test(\"remove column mapping from a table with a generated column\") {\n    // Note: generate expressions are using logical column names and renaming referenced columns\n    // is forbidden.\n    DeltaTable.create(spark)\n      .tableName(testTableName)\n      .addColumn(logicalColumnName, \"LONG\")\n      .addColumn(\n        DeltaTable.columnBuilder(secondColumn)\n          .dataType(\"LONG\")\n          .generatedAlwaysAs(s\"$logicalColumnName + 1\")\n          .build())\n      .property(DeltaConfigs.COLUMN_MAPPING_MODE.key, \"name\")\n      .execute()\n    // Insert data into the table.\n    spark.range(totalRows)\n      .selectExpr(s\"id as $logicalColumnName\")\n      .writeTo(testTableName)\n      .append()\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName))\n    assert(GeneratedColumn.getGeneratedColumns(deltaLog.update()).head.name == secondColumn)\n    testRemovingColumnMapping()\n    // Verify the generated column is still there.\n    assert(GeneratedColumn.getGeneratedColumns(deltaLog.update()).head.name == secondColumn)\n    // Insert more rows.\n    spark.range(totalRows)\n      .selectExpr(s\"id + $totalRows as $logicalColumnName\")\n      .writeTo(testTableName)\n      .append()\n    // Verify the generated column values are correct.\n    checkAnswer(sql(s\"SELECT $logicalColumnName, $secondColumn FROM $testTableName\"),\n      (0 until totalRows * 2).map(i => Row(i, i + 1)))\n  }\n\n  test(\"column constraints are preserved\") {\n    // Note: constraints are using logical column names and renaming is forbidden until\n    // constraint is dropped.\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES (\n         |  '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    val constraintName = \"secondcolumnaddone\"\n    val constraintExpr = s\"$secondColumn = $logicalColumnName + 1\"\n    sql(s\"ALTER TABLE $testTableName ADD CONSTRAINT \" +\n      s\"$constraintName CHECK ($constraintExpr)\")\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName = testTableName))\n    assert(deltaLog.update().metadata.configuration(s\"delta.constraints.$constraintName\") ==\n      constraintExpr)\n    testRemovingColumnMapping()\n    // Verify the constraint is still there.\n    assert(deltaLog.update().metadata.configuration(s\"delta.constraints.$constraintName\") ==\n      constraintExpr)\n    // Verify the constraint is still enforced.\n    intercept[DeltaInvariantViolationException] {\n      sql(s\"INSERT INTO $testTableName VALUES (0, 0)\")\n    }\n  }\n\n  test(\"remove column mapping in id mode\") {\n    sql(\n      s\"\"\"CREATE TABLE $testTableName\n         |USING delta\n         |TBLPROPERTIES (\n         |  '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'id')\n         |AS SELECT id as $logicalColumnName, id + 1 as $secondColumn\n         |  FROM RANGE(0, $totalRows, 1, $numFiles)\n         |\"\"\".stripMargin)\n    testRemovingColumnMapping()\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/columnmapping/RemoveColumnMappingSuiteUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.columnmapping\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.RemoveColumnMapping\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport com.fasterxml.jackson.databind.ObjectMapper\n\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\n\n/**\n * A base trait for testing removing column mapping.\n * Takes care of setting basic SQL configs and dropping the [[testTableName]] after each test.\n */\ntrait RemoveColumnMappingSuiteUtils extends QueryTest with DeltaColumnMappingSuiteUtils {\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(ALLOW_COLUMN_MAPPING_REMOVAL.key, \"true\")\n  }\n\n  override protected def afterEach(): Unit = {\n    sql(s\"DROP TABLE IF EXISTS $testTableName\")\n    super.afterEach()\n  }\n\n  protected val numFiles = 10\n  protected val totalRows = 100\n  protected val rowsPerFile = totalRows / numFiles\n  protected val logicalColumnName = \"logical_column_name\"\n  protected val secondColumn = \"second_column_name\"\n  protected val firstColumn = \"first_column_name\"\n  protected val thirdColumn = \"third_column_name\"\n  protected val renamedThirdColumn = \"renamed_third_column_name\"\n\n  protected val testTableName: String = \"test_table_\" + this.getClass.getSimpleName\n  protected def deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n\n  import testImplicits._\n\n  protected def testRemovingColumnMapping(unsetTableProperty: Boolean = false): Any = {\n    // Verify the input data is as expected.\n    val originalData = spark.table(tableName = testTableName).select(logicalColumnName).collect()\n    // Add a schema comment and verify it is preserved after the rewrite.\n    val comment = \"test comment\"\n    sql(s\"ALTER TABLE $testTableName ALTER COLUMN $logicalColumnName COMMENT '$comment'\")\n\n    val table = DeltaTableV2(spark, TableIdentifier(tableName = testTableName))\n    val originalSnapshot = table.update()\n\n    assert(originalSnapshot.schema.head.getComment().get == comment,\n      \"Renamed column should preserve comment.\")\n    val originalFiles = getFiles(originalSnapshot)\n    val startingVersion = table.update().version\n\n    unsetColumnMappingProperty(useUnset = unsetTableProperty)\n\n    verifyRewrite(\n      unsetTableProperty = unsetTableProperty,\n      table,\n      originalFiles,\n      startingVersion,\n      originalData = originalData)\n    // Verify the schema comment is preserved after the rewrite.\n    assert(deltaLog.update().schema.head.getComment().get == comment,\n      \"Should preserve the schema comment.\")\n  }\n\n  /**\n   * Verify the table still contains the same data after the rewrite, column mapping is removed\n   * from table properties and the operation recorded properly.\n   */\n  protected def verifyRewrite(\n      unsetTableProperty: Boolean,\n      table: DeltaTableV2,\n      originalFiles: Array[AddFile],\n      startingVersion: Long,\n      originalData: Array[Row],\n      droppedFeature: Boolean = false): Unit = {\n    checkAnswer(\n      spark.table(tableName = testTableName).select(logicalColumnName),\n      originalData)\n    val newSnapshot = table.update()\n    val versionsAddedByRewrite = if (droppedFeature) {\n      2\n    } else {\n      1\n    }\n    assert(newSnapshot.version - startingVersion == versionsAddedByRewrite,\n      s\"Should rewrite the table in $versionsAddedByRewrite commits.\")\n\n    val rewriteVersion = newSnapshot.version - versionsAddedByRewrite + 1\n    val history =\n      table.deltaLog.history.getHistory(rewriteVersion, Some(rewriteVersion), table.catalogTable)\n    verifyColumnMappingOperationIsRecordedInHistory(history)\n\n    assert(newSnapshot.schema.head.name == logicalColumnName, \"Should rename the first column.\")\n\n    verifyColumnMappingSchemaMetadataIsRemoved(newSnapshot)\n\n    verifyColumnMappingTablePropertiesAbsent(newSnapshot, unsetTableProperty || droppedFeature)\n    assert(originalFiles.map(_.numLogicalRecords.get).sum ==\n      newSnapshot.allFiles.map(_.numLogicalRecords.get).collect().sum,\n      \"Should have the same number of records.\")\n  }\n\n  protected def unsetColumnMappingProperty(useUnset: Boolean): Unit = {\n    val unsetStr = if (useUnset) {\n      s\"UNSET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}')\"\n    } else {\n      s\"SET TBLPROPERTIES ('${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'none')\"\n    }\n    sql(\n      s\"\"\"\n         |ALTER TABLE $testTableName $unsetStr\n         |\"\"\".stripMargin)\n  }\n\n  protected def enableColumnMapping(): Unit = {\n    sql(\n      s\"\"\"ALTER TABLE $testTableName\n        SET TBLPROPERTIES (\n        '${DeltaConfigs.COLUMN_MAPPING_MODE.key}' = 'name',\n        'delta.minReaderVersion' = '2',\n        'delta.minWriterVersion' = '5')\"\"\")\n  }\n\n  protected def renameColumn(): Unit = {\n    sql(s\"ALTER TABLE $testTableName RENAME COLUMN $thirdColumn TO $renamedThirdColumn\")\n  }\n\n  protected def dropColumn(): Unit = {\n    sql(s\"ALTER TABLE $testTableName DROP COLUMN $thirdColumn\")\n  }\n\n  /**\n   * Get all files in snapshot.\n   */\n  protected def getFiles(snapshot: Snapshot): Array[AddFile] = snapshot.allFiles.collect()\n\n  protected def verifyColumnMappingOperationIsRecordedInHistory(history: Seq[DeltaHistory]) = {\n    val op = RemoveColumnMapping()\n    assert(history.head.operation === op.name)\n    assert(history.head.operationParameters === op.parameters.mapValues(_.toString).toMap)\n  }\n\n  protected def verifyColumnMappingSchemaMetadataIsRemoved(newSnapshot: Snapshot) = {\n    SchemaMergingUtils.explode(newSnapshot.schema).foreach { case(_, col) =>\n      assert(!DeltaColumnMapping.hasPhysicalName(col))\n      assert(!DeltaColumnMapping.hasColumnId(col))\n    }\n  }\n\n  protected def verifyColumnMappingTablePropertiesAbsent(\n      newSnapshot: Snapshot,\n      unsetTablePropertyUsed: Boolean) = {\n    val columnMappingPropertyKey = DeltaConfigs.COLUMN_MAPPING_MODE.key\n    val columnMappingMaxIdPropertyKey = DeltaConfigs.COLUMN_MAPPING_MAX_ID.key\n    val newColumnMappingModeOpt = newSnapshot.metadata.configuration.get(columnMappingPropertyKey)\n    if (unsetTablePropertyUsed) {\n      assert(newColumnMappingModeOpt.isEmpty)\n    } else {\n      assert(newColumnMappingModeOpt.contains(\"none\"))\n    }\n    assert(!newSnapshot.metadata.configuration.contains(columnMappingMaxIdPropertyKey))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/commands/DeltaCommandInvariantsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands\n\nimport scala.util.{Failure, Success, Try}\n\nimport com.databricks.spark.util.Log4jUsageLogger\n\nimport org.apache.spark.{SparkFunSuite, SparkThrowable}\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.DeltaOperations.EmptyCommit\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nclass DeltaCommandInvariantsSuite extends SparkFunSuite with DeltaSQLCommandTest {\n\n  for {\n    shouldSucceed <- BOOLEAN_DOMAIN\n    shouldThrow <- BOOLEAN_DOMAIN\n  } test(\"command invariant check - \" +\n    s\"shouldSucceed=$shouldSucceed, shouldThrow=$shouldThrow\") {\n    withTempDir { dir =>\n      val path = dir.toString\n      spark.range(10).write.format(\"delta\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n      val opType =\n        \"delta.assertions.unreliable.commandInvariantViolated\"\n      val events = Log4jUsageLogger.track {\n        val result = Try {\n          withSQLConf(\n            DeltaSQLConf.COMMAND_INVARIANT_CHECKS_THROW.key -> shouldThrow.toString) {\n            // Create an anonymous class, since checkCommandInvariant is protected here.\n            new DeltaCommand {\n              checkCommandInvariant(\n                invariant = () => shouldSucceed,\n                label = \"shouldSucceed\",\n                op = EmptyCommit,\n                deltaLog = deltaLog,\n                parameters = Map(\"unused\" -> 123),\n                additionalInfo = Map(\"shouldSucceed\" -> shouldSucceed.toString))\n            }\n          }\n        }\n        if (!shouldSucceed && shouldThrow) {\n          result match {\n            case Failure(e: SparkThrowable) =>\n              checkErrorMatchPVals(\n                e,\n                \"DELTA_COMMAND_INVARIANT_VIOLATION\",\n                parameters = Map(\n                  \"operation\" -> \"Empty Commit\",\n                  \"uuid\" -> \".*\" // Doesn't matter\n                )\n              )\n            case Failure(e) => throw e\n            case Success(_) => fail(\"Expected Failure but got Success\")\n          }\n        } else {\n          assert(result.isSuccess)\n        }\n      }\n      val violationEvents =\n        events.filter(_.tags.get(\"opType\").contains(opType))\n      if (shouldSucceed) {\n        assert(violationEvents.isEmpty)\n      } else {\n        assert(violationEvents.size === 1)\n        val violationEvent = violationEvents.head\n        val violationEventInfo = JsonUtils.fromJson[CommandInvariantCheckInfo](violationEvent.blob)\n        assert(violationEventInfo === CommandInvariantCheckInfo(\n          exceptionThrown = shouldThrow,\n          id = violationEventInfo.id, // Don't check this, it's random.\n          invariantExpression = \"shouldSucceed\",\n          invariantParameters = Map(\"unused\" -> 123),\n          operation = \"Empty Commit\",\n          operationParameters = Map.empty,\n          additionalInfo = Map(\"shouldSucceed\" -> shouldSucceed.toString)))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingBackfillBackfillConflictsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport java.util.concurrent.ExecutionException\n\nimport org.apache.spark.sql.delta.{DeltaOperations, RowId}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nclass RowTrackingBackfillBackfillConflictsSuite extends RowTrackingBackfillConflictsTestBase {\n\n  /**\n   * Concurrent backfill starts after the main backfill enabled the table feature and tries to\n   * commit its only batch and the metadata update after the main backfill is finished.\n   */\n  test(\"Two Concurrent backfills\") {\n    withTestTable {\n      withTrackedBackfillCommits {\n        // Start main backfill and set table feature.\n        val mainBackfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n        prepareSingleBackfillBatchCommit()\n\n        // Start concurrent backfill. Table feature is already enabled.\n        val concurrentBackfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n        prepareSingleBackfillBatchCommit()\n\n        // Finish the main backfill which only requires one commit.\n        commitPreparedBackfillBatchCommit()\n        mainBackfillFuture.get()\n\n        // Commit the batch of the concurrent backfill.\n        commitPreparedBackfillBatchCommit()\n\n        // Finish the concurrent backfill. It will commit 0 file.\n        concurrentBackfillFuture.get()\n\n        // Concurrent backfill does not upgrade the protocol again.\n        assert(deltaLog.history.getHistory(None).count(_.operation == \"UPGRADE PROTOCOL\") === 1)\n        validateResult(() => tableCreationDF)\n      }\n    }\n  }\n\n  test(\"A second backfill after a failed backfill\") {\n    withTestTable {\n      withTrackedBackfillCommits {\n        // Start a first backfill and set table feature.\n        val firstBackfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n        prepareSingleBackfillBatchCommit()\n\n        // Concurrenty update the metadata.\n        val updatedMetadata = latestSnapshot.metadata\n          .copy(configuration = Map(\"foo\" -> \"bar\"))\n        deltaLog.startTransaction().commit(Seq(updatedMetadata), DeltaOperations.ManualUpdate)\n\n        // Committing the batch and completing the command will fail because of the metadata update.\n        commitPreparedBackfillBatchCommit()\n        val e = intercept[ExecutionException] {\n          firstBackfillFuture.get()\n        }\n        assertAbortedBecauseOfMetadataChange(e)\n        // We need to remove that expected error or we'll fail below when checking the sink.\n        BackgroundErrorSink.clear()\n        assert(!RowId.isEnabled(latestSnapshot.protocol, latestSnapshot.metadata))\n\n        // Launch a second backfill to finish the aborted backfill.\n        val secondBackfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n        commitSingleBackfillBatch()\n        secondBackfillFuture.get()\n\n        validateResult(() => tableCreationDF)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingBackfillCloneConflictsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport org.apache.spark.sql.delta.{MetadataChangedException, RowTracking}\nimport org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver => TransactionObserver}\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.util.ThreadUtils\n\n/**\n * Clone uses commitLarge which does not do conflict checking nor retry. So when there is\n * a concurrent conflict, either Clone will fail from concurrent modification or\n * Backfill will fail from the metadata change.\n */\n\n/*\n * Tests for conflict detection with Backfill and Clone. Note that each backfill in this case has\n * 2 stages:\n * - Upgrade the protocol to support the Row Tracking Table Feature\n * - Mark Row Tracking active by updating the table metadata\n *\n * -------------------------------------------> TIME -------------------------------------------->\n *\n * Backfill         Row Tracking              Row Tracking               Row Tracking\n * Command          Protocol upgrade -------- metadata update ---------- metadata update\n * Thread           prepare + commit          prepare                    commit\n *\n *\n * Clone\n *\n * Scenario 1                        prepare ----------------- commit\n * Scenario 2                        prepare -------------------------------------------- commit\n *\n * -------------------------------------------> TIME -------------------------------------------->\n */\nclass RowTrackingBackfillCloneConflictsSuite extends RowTrackingBackfillConflictsTestBase {\n  override val testTableName = \"BackfillCloneTarget\"\n\n  val sourceTableName = \"CloneSource\"\n\n  private def withSourceTable(testBlock: => Unit): Unit = {\n    withTable(sourceTableName) {\n      withRowTrackingEnabled(enabled = false) {\n        tableCreationAfterInsert().write.format(\"delta\").saveAsTable(sourceTableName)\n        testBlock\n      }\n    }\n  }\n\n  private def createUpdateMetadataBackfillObserver(): TransactionObserver = {\n    // This observes the transaction in [[alterDeltaTableCommands]] after the backfill\n    // that tries to update the table's metadata.\n    new TransactionObserver(\n      OptimisticTransactionPhases.forName(\"update-metadata-backfill-observer\"))\n  }\n\n  test(\"Backfill fails from conflict with CLONE\") {\n    val cloneTransaction =\n      () => sql(s\"CREATE OR REPLACE TABLE \" +\n        s\"$testTableName SHALLOW CLONE $sourceTableName\").collect()\n\n    withTrackedBackfillCommits {\n      withSourceTable {\n        withEmptyTestTable {\n          // Row tracking will be enabled by the backfill.\n          validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 0) {\n            val Seq(backfillFuture) =\n              runFunctionsWithOrderingFromObserver(Seq(backfillTransaction)) {\n                // The upgradeProtocolBackfillObserver observes the transaction in\n                // [[RowTrackingBackfillCommands]] that adds the Row tracking table feature support.\n                case (upgradeProtocolBackfillObserver :: Nil) =>\n                  val updateMetadataBackfillObserver = createUpdateMetadataBackfillObserver()\n                  upgradeProtocolBackfillObserver.setNextObserver(\n                    updateMetadataBackfillObserver, autoAdvance = true)\n\n                  // Prepare and commit Row Tracking table feature support transaction.\n                  prepareAndCommitWithNextObserverSet(upgradeProtocolBackfillObserver)\n\n                  val Seq(cloneFuture) =\n                    runFunctionsWithOrderingFromObserver(Seq(cloneTransaction)) {\n                      case (cloneTransactionObserver :: Nil) =>\n                        // Prepare Clone commit.\n                        unblockUntilPreCommit(cloneTransactionObserver)\n                        waitForPrecommit(cloneTransactionObserver)\n\n                        // Prepare Row Tracking metadata update commit.\n                        unblockUntilPreCommit(updateMetadataBackfillObserver)\n                        waitForPrecommit(updateMetadataBackfillObserver)\n\n                        // Commit Clone.\n                        unblockCommit(cloneTransactionObserver)\n                        waitForCommit(cloneTransactionObserver)\n\n                        // Commit Row Tracking metadata update.\n                        unblockCommit(updateMetadataBackfillObserver)\n                        waitForCommit(updateMetadataBackfillObserver)\n                    }\n\n                  ThreadUtils.awaitResult(cloneFuture, timeout)\n                  checkAnswer(spark.table(testTableName), spark.table(sourceTableName))\n              }\n\n            val ex = intercept[SparkException] {\n              ThreadUtils.awaitResult(backfillFuture, timeout)\n            }\n            assertAbortedBecauseOfMetadataChange(ex)\n            assert(!RowTracking.isEnabled(latestSnapshot.protocol, latestSnapshot.metadata))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Clone fails from conflict with Backfill\") {\n    val cloneTransaction =\n      () => sql(s\"CREATE OR REPLACE TABLE \" +\n        s\"$testTableName SHALLOW CLONE $sourceTableName\").collect()\n\n    withTrackedBackfillCommits {\n      withSourceTable {\n        withEmptyTestTable {\n          // Row tracking will be enabled by the backfill.\n          validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 0) {\n            val Seq(backfillFuture) =\n              runFunctionsWithOrderingFromObserver(Seq(backfillTransaction)) {\n                // The upgradeProtocolBackfillObserver observes the transaction in\n                // [[RowTrackingBackfillCommands]] that adds the Row tracking table feature support.\n                case (upgradeProtocolBackfillObserver :: Nil) =>\n                  val updateMetadataBackfillObserver = createUpdateMetadataBackfillObserver()\n                  upgradeProtocolBackfillObserver.setNextObserver(\n                    updateMetadataBackfillObserver, autoAdvance = true)\n\n                  // Prepare and commit Row Tracking table feature support transaction.\n                  prepareAndCommitWithNextObserverSet(upgradeProtocolBackfillObserver)\n\n                  val Seq(cloneFuture) =\n                    runFunctionsWithOrderingFromObserver(Seq(cloneTransaction)) {\n                      case (cloneTransactionObserver :: Nil) =>\n                        // Prepare Clone commit.\n                        unblockUntilPreCommit(cloneTransactionObserver)\n                        waitForPrecommit(cloneTransactionObserver)\n\n                        // Prepare and commit Row Tracking metadata update commit.\n                        prepareAndCommit(updateMetadataBackfillObserver)\n\n                        // Commit Clone.\n                        unblockCommit(cloneTransactionObserver)\n                        waitForCommit(cloneTransactionObserver)\n                  }\n\n                  val ex = intercept[SparkException] {\n                    ThreadUtils.awaitResult(cloneFuture, timeout)\n                  }\n                  assertAbortedBecauseOfConcurrentWrite(ex)\n              }\n\n            ThreadUtils.awaitResult(backfillFuture, timeout)\n            assert(RowTracking.isEnabled(latestSnapshot.protocol, latestSnapshot.metadata))\n            checkAnswer(spark.table(testTableName), Seq.empty)\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/commands/backfill/RowTrackingBackfillConflictsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.commands.backfill\n\nimport java.util.concurrent.{ConcurrentLinkedDeque, ExecutionException, Future, TimeUnit}\n\nimport scala.annotation.tailrec\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.{Action, AddFile}\nimport org.apache.spark.sql.delta.commands.DeletionVectorUtils\nimport org.apache.spark.sql.delta.concurrency.{PhaseLockingTestMixin, TransactionExecutionTestMixin}\nimport org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver}\nimport org.apache.spark.sql.delta.fuzzer.AtomicBarrier.State\nimport org.apache.spark.sql.delta.rowid.RowIdTestUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport io.delta.exceptions.MetadataChangedException\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.{DataFrame, Dataset, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ThreadUtils\n\n/*\n * Tests for conflict detection with Backfill. Test suites have to override 'concurrentTransaction'\n * before testing the following scenarios. All scenarios include the execution one transaction\n * concurrently to a backfill command. Note that each backfill has 3 stages\n * - Upgrade the protocol to support the Row Tracking Table Feature\n * - Add baseRowId (done in multiple batches in parallel)\n * - Mark Row Tracking active by updating the table metadata\n *\n * -------------------------------------------> TIME -------------------------------------------->\n *                  RT           RT                                           RT\n * Backfill         protocol     protocol                                     metadata\n * Command          upgrade ---- upgrade -+--------------------------------+- update\n * Thread           prepare      commit    \\                              /   prepare + commit\n *                                          \\                            /\n * Backfill                                  \\  BaseRowId     BaseRowId /\n * Threads                                    - Backfill ---- Backfill-/\n *                                              prepare       commit\n *\n\n *\n * Concurrent transaction\n *\n * Scenario 1  prepare --- commit\n * Scenario 2  prepare ----------------- commit\n * Scenario 3  prepare ------------------------------------------------ commit\n * Scenario 4  prepare --------------------------------------------------------------------- commit\n * Scenario 5                            prepare ------- commit\n * Scenario 6                            prepare ---------------------- commit\n * Scenario 7                            prepare ------------------------------------------- commit\n *\n * -------------------------------------------> TIME -------------------------------------------->\n */\ntrait RowTrackingBackfillConflictsTestBase extends RowIdTestUtils\n  with TransactionExecutionTestMixin\n  with PhaseLockingTestMixin\n  with SharedSparkSession {\n\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key, \"true\")\n    .set(DeltaSQLConf.FEATURE_ENABLEMENT_CONFLICT_RESOLUTION_ENABLED.key, \"true\")\n\n  protected val usePersistentDeletionVectors = false\n\n  protected val testTableName = \"target\"\n  protected val colName = \"id\"\n  protected val partitionColumnName = \"partition\"\n\n  protected def tableCreationDF: DataFrame =\n    withPartitionColumn(spark.range(end = numRows).toDF(colName))\n\n\n  protected def insertedRowDF: DataFrame = {\n    val insertedRow = Seq((1337, 1))\n    spark.createDataFrame(insertedRow)\n  }\n\n  protected def tableCreationAfterInsert(): Dataset[Row] = {\n    tableCreationDF.union(insertedRowDF)\n  }\n\n  protected def withPartitionColumn(dataFrame: DataFrame): DataFrame = {\n    dataFrame.withColumn(partitionColumnName, (col(colName) % numPartitions).cast(\"int\"))\n  }\n\n  protected val numPartitions: Int = 4\n  private val numFilesPerPartition: Int = 2\n  private val numRowsPerFile: Int = 10\n  protected val numFiles: Int = numPartitions * numFilesPerPartition\n  protected val numRows: Int = numFiles * numRowsPerFile\n\n  protected def deltaLog: DeltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n\n  protected def latestSnapshot: Snapshot = deltaLog.update()\n\n  protected def backfillTransaction(): Array[Row] = {\n    sql(s\"\"\"ALTER TABLE $testTableName\n           |SET TBLPROPERTIES('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true')\"\"\".stripMargin)\n      .collect()\n  }\n\n  // All observers for backfill threads will be added to the Deque.\n  protected val backfillObservers =\n    new ConcurrentLinkedDeque[PhaseLockingTransactionExecutionObserver]\n\n  // Collects errors from background threads so busyWaitFor can fail fast with useful messages.\n  object BackgroundErrorSink {\n    private val errors = new ConcurrentLinkedDeque[Throwable]()\n\n    def recordError(t: Throwable): Unit = errors.addLast(t)\n\n    def isEmpty: Boolean = errors.isEmpty()\n\n    def hasErrors: Boolean = !errors.isEmpty()\n\n    def clear(): Unit = errors.clear()\n\n    def checkAndThrow(): Unit = {\n      if (hasErrors) {\n        import scala.jdk.CollectionConverters._\n        val errorList = errors.iterator().asScala.toList\n        val summaries = errorList.map { t =>\n          val sw = new java.io.StringWriter()\n          t.printStackTrace(new java.io.PrintWriter(sw))\n          sw.toString\n        }\n        fail(s\"Background thread(s) failed:\\n${summaries.mkString(\"\\n---\\n\")}\")\n      }\n    }\n  }\n\n  // An observer that adds transaction observers to `backfillObservers` for all batches.\n  object TrackingBackfillExecutionObserver extends BackfillExecutionObserver {\n    override def executeBatch[T](f: => T): T = {\n      val observer = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"backfill-observer\"))\n      backfillObservers.addLast(observer)\n      TransactionExecutionObserver.withObserver(observer)(f)\n    }\n  }\n\n  // Wait for one backfill thread to be ready and unblock it. The thread is chosen at random.\n  protected def commitSingleBackfillBatch(): Unit = {\n    prepareSingleBackfillBatchCommit()\n    commitPreparedBackfillBatchCommit()\n  }\n\n  // Wait for one backfill thread to be ready and unblock it up until pre commit. The thread will\n  // be kept at the front of 'backfillObservers'.\n  protected def prepareSingleBackfillBatchCommit(): Unit = {\n    var nextBlockedObserver: Option[PhaseLockingTransactionExecutionObserver] = None\n    def foundNextBlockedObserver(): Boolean = {\n      // Fail fast if background thread died.\n      BackgroundErrorSink.checkAndThrow()\n      backfillObservers.forEach { observer =>\n        val prepareEntryState = observer.phases.preparePhase.entryBarrier.load()\n        if (nextBlockedObserver.isEmpty &&\n            Seq(State.Blocked, State.Requested).contains(prepareEntryState)) {\n          nextBlockedObserver = Some(observer)\n        }\n      }\n      nextBlockedObserver.isDefined\n    }\n    busyWaitFor(foundNextBlockedObserver, timeout)\n    val observerToWaitOn = nextBlockedObserver.get\n    unblockUntilPreCommit(observerToWaitOn)\n    busyWaitForPreparePhase(observerToWaitOn)\n  }\n\n  // Commit the backfill batch that has been prepared by 'prepareSingleBackfillBatchCommit'.\n  protected def commitPreparedBackfillBatchCommit(): Unit = {\n    require(!backfillObservers.isEmpty)\n    val observer = backfillObservers.removeFirst()\n    require(observer.phases.preparePhase.hasEntered)\n    unblockCommit(observer)\n    waitForCommit(observer)\n  }\n\n  // Wait for an observer to enter the prepare phase, with fail-fast on background errors.\n  protected def busyWaitForPreparePhase(\n      observer: PhaseLockingTransactionExecutionObserver): Unit = {\n    def hasPrepared(): Boolean = {\n      BackgroundErrorSink.checkAndThrow()\n      observer.phases.preparePhase.hasEntered\n    }\n    busyWaitFor(hasPrepared, timeout)\n  }\n\n  // Launch the backfill command and wait until the table feature has been committed.\n  protected def launchBackFillAndBlockAfterFeatureIsCommitted(): Future[_] = {\n    val backfillFuture = launchBackfillInBackgroundThread()\n    busyWaitFor(RowTracking.isSupported(latestSnapshot.protocol), timeout)\n    backfillFuture\n  }\n\n  // Launch the backfill in a separate thread to run in parallel to `concurrentTransaction`.\n  private def launchBackfillInBackgroundThread(): Future[_] = {\n    val threadPool = ThreadUtils.newDaemonSingleThreadExecutor(\"backfill-thread-pool\")\n    val backfillRunnable: Runnable = () => {\n      try {\n        backfillTransaction()\n      } catch {\n        case t: Throwable =>\n          BackgroundErrorSink.recordError(t)\n          throw t\n      }\n    }\n    threadPool.submit(backfillRunnable)\n  }\n\n  protected def withTrackedBackfillCommits(testBlock: => Unit): Unit = {\n    assert(backfillObservers.isEmpty)\n    assert(BackgroundErrorSink.isEmpty)\n    try {\n      BackfillExecutionObserver.withObserver(TrackingBackfillExecutionObserver) {\n        testBlock\n      }\n    } finally {\n      backfillObservers.clear()\n      BackgroundErrorSink.clear()\n    }\n  }\n\n  protected def withEmptyTestTable(testBlock: => Unit): Unit = {\n    withTable(testTableName) {\n      // Row tracking will be enabled by the backfill.\n      withRowTrackingEnabled(enabled = false) {\n        spark.range(0)\n          .write.format(\"delta\").saveAsTable(testTableName)\n\n        testBlock\n      }\n    }\n  }\n\n  /**\n   * Sets explicit insertion times on files to guarantee deterministic ordering for backfill.\n   * Files in partitions 0-1 get early insertion time (batch 1).\n   * Files in partitions 2-3 get later insertion time (batch 2).\n   */\n  private def setDeterministicInsertionTimes(): Unit = {\n    val log = deltaLog\n    val snapshot = log.update()\n    val allFiles = snapshot.allFiles.collect()\n\n    // Partitions 0-1 get insertion time 1000, partitions 2-3 get insertion time 2000\n    val earlyTime = \"1000\"\n    val lateTime = \"2000\"\n\n    val updatedFiles = allFiles.map { file =>\n      val partition = file.partitionValues(partitionColumnName).toInt\n      val insertionTime = if (partition < 2) earlyTime else lateTime\n      val existingTags = Option(file.tags).getOrElse(Map.empty[String, String])\n      val newTags = existingTags + (AddFile.Tags.INSERTION_TIME.name -> insertionTime)\n      file.copy(tags = newTags)\n    }\n\n    // Replace files with updated versions using ManualUpdate\n    log.startTransaction(None).commit(updatedFiles, ManualUpdate)\n  }\n\n  protected def withTestTable(testBlock: => Unit): Unit = {\n    withTable(testTableName) {\n      // Row tracking will be enabled by the backfill.\n      withRowTrackingEnabled(enabled = false) {\n        withSQLConf(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey ->\n           usePersistentDeletionVectors.toString) {\n          tableCreationDF\n            .repartitionByRange(numFilesPerPartition, col(colName))\n            .write.format(\"delta\").partitionBy(partitionColumnName).saveAsTable(testTableName)\n\n          // Set explicit insertion times to guarantee deterministic ordering:\n          // - Partitions 0-1 get early insertion time (will be batch 1)\n          // - Partitions 2-3 get later insertion time (will be batch 2)\n          setDeterministicInsertionTimes()\n        }\n\n        val tableDF = spark.table(testTableName)\n        val tableFiles: Array[AddFile] = latestSnapshot.allFiles.collect()\n        assert(numPartitions === tableDF.select(partitionColumnName).distinct().count())\n        tableFiles.groupBy(_.partitionValues.get(partitionColumnName)).foreach {\n          case (_, filesInPartition) => assert(filesInPartition.length === numFilesPerPartition)\n        }\n        assert(tableFiles.forall(_.numLogicalRecords.get == numRowsPerFile))\n        assert(numFiles === tableFiles.length)\n        assert(numRows === tableDF.count())\n\n        testBlock\n      }\n    }\n  }\n\n  protected def validateResult(expectedResult: () => DataFrame): Unit = {\n    assert(RowId.isEnabled(latestSnapshot.protocol, latestSnapshot.metadata))\n    assertRowIdsAreValid(deltaLog)\n    checkAnswer(spark.table(testTableName), expectedResult())\n  }\n\n  private def causedByMetadataUpdate(exception: Throwable): Boolean = {\n    exception match {\n      case _: MetadataChangedException => true\n      case null => false\n      case same if same.getCause == same => false\n      case other => causedByMetadataUpdate(other.getCause)\n    }\n  }\n\n  protected def assertAbortedBecauseOfMetadataChange(exception: ExecutionException): Unit =\n    assert(causedByMetadataUpdate(exception), s\"Unexpected abort: ${exception.getMessage}\")\n\n  protected def assertAbortedBecauseOfMetadataChange(exception: SparkException): Unit =\n    assert(causedByMetadataUpdate(exception.getCause), s\"Unexpected abort: ${exception.getMessage}\")\n\n  protected def assertAbortedBecauseOfConcurrentWrite(\n      exception: SparkException): Unit = {\n    @tailrec\n    def causedByConcurrentModification(exception: Throwable): Boolean = {\n      exception match {\n        case _: ConcurrentWriteException => true\n        case null => false\n        case same if same.getCause == same => false\n        case other => causedByConcurrentModification(other.getCause)\n      }\n    }\n    assert(causedByConcurrentModification(exception.getCause),\n      s\"Unexpected abort: ${exception.getMessage}\")\n  }\n\n  /**\n   * This is a modification of scenario 5 in [[RowTrackingBackfillConflictsSuite]] with an\n   * extra insert and failure expectations for commitLarge test.\n   */\n  protected def testScenario5WithCommitLarge(\n      concurrentTransaction: () => Array[Row],\n      expectedResult: DataFrame): Unit = {\n    withTestTable {\n      withTrackedBackfillCommits {\n        // Commit Row Tracking feature.\n        val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n        // Add some data to bump the table version. A RESTORE to the current table version is a NOOP\n        // and will not even create the RestoreTableCommand object. We need a dummy commit in order\n        // for a RESTORE to take place.\n        insertedRowDF.write.insertInto(testTableName)\n        assert(latestSnapshot.version > 1)\n\n        val Seq(concurrentTransactionFuture) =\n          runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n            case (concurrentTransactionObserver :: Nil) =>\n              // Prepare concurrent transaction.\n              unblockUntilPreCommit(concurrentTransactionObserver)\n              busyWaitForPreparePhase(concurrentTransactionObserver)\n\n              // Prepare the commit of one backfill batch.\n              prepareSingleBackfillBatchCommit()\n\n              // Commit concurrent transaction.\n              unblockCommit(concurrentTransactionObserver)\n              waitForCommit(concurrentTransactionObserver)\n\n              // Try to finish the backfill, which only has one batch. This will fail because the\n              // concurrent txn does a metadata update.\n              commitPreparedBackfillBatchCommit()\n              val e = intercept[ExecutionException] {\n                backfillFuture.get(timeout.toSeconds, TimeUnit.SECONDS)\n              }\n              assertAbortedBecauseOfMetadataChange(e)\n          }\n\n        ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n        checkAnswer(spark.table(testTableName), expectedResult)\n      }\n    }\n  }\n\n  /**\n   * This is a modification of scenario 6 in [[RowTrackingBackfillConflictsSuite]] with an extra\n   * insert and failure expectations for commitLarge test.\n   */\n  protected def testScenario6WithCommitLarge(concurrentTransaction: () => Array[Row]): Unit = {\n    withTrackedBackfillCommits {\n      withTestTable {\n        // We enforce the backfill to use two commits such that we can stop the process before\n        // the metadata update commit.\n        withSQLConf(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key ->\n          math.ceil(numFiles / 2.0).toInt.toString) {\n          validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 2) {\n            val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n            // Add some data to bump the table version. A RESTORE to the current table version is a\n            // NOOP and will not even create the RestoreTableCommand object. We need a dummy commit\n            // in order for a RESTORE to take place.\n            insertedRowDF.write.insertInto(testTableName)\n            assert(latestSnapshot.version > 1)\n\n            val Seq(concurrentTransactionFuture) =\n              runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n                case (concurrentTransactionObserver :: Nil) =>\n                  // Prepare concurrent commit.\n                  unblockUntilPreCommit(concurrentTransactionObserver)\n                  busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                  // Commit one backfill.\n                  commitSingleBackfillBatch()\n\n                  // Try to commit concurrent transaction.\n                  unblockCommit(concurrentTransactionObserver)\n                  waitForCommit(concurrentTransactionObserver)\n\n                  // Finish the backfill command, which has 2 batches.\n                  commitSingleBackfillBatch()\n                  backfillFuture.get(timeout.toSeconds, TimeUnit.SECONDS)\n              }\n            val e = intercept[SparkException] {\n              ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n            }\n            assertAbortedBecauseOfConcurrentWrite(e)\n            validateResult(tableCreationAfterInsert)\n          }\n        }\n      }\n    }\n  }\n}\n\nclass RowTrackingBackfillConflictsSuite extends RowTrackingBackfillConflictsTestBase {\n  private def testAllScenarios(\n      concurrentTransactionName: String)(\n      concurrentTransaction: () => Array[Row])(\n      expectedResult: () => DataFrame): Unit = {\n    /**\n     * Scenario 1: Commit concurrent transaction in parallel to the protocol upgrade.\n     */\n    test(s\"$concurrentTransactionName - Scenario 1\") {\n      withTestTable {\n        validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n          val Seq(concurrentTransactionFuture, backfillFuture) =\n            runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction, backfillTransaction)) {\n              case (concurrentTransactionObserver :: backfillObserver :: Nil) =>\n                // Prepare concurrent transaction commit.\n                unblockUntilPreCommit(concurrentTransactionObserver)\n                busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                // Prepare table feature commit.\n                unblockUntilPreCommit(backfillObserver)\n                busyWaitForPreparePhase(backfillObserver)\n\n                // Commit concurrent transaction.\n                unblockCommit(concurrentTransactionObserver)\n                waitForCommit(concurrentTransactionObserver)\n\n                // Commit table feature and unblock further backfill commits. We replace the\n                // txnObserver on the main thread with a NoOp txnObserver, which means the remaining\n                // transactions on the main thread (the parent txn object of backfill and the\n                // metadata update) will not be observed.\n                backfillObserver.setNextObserver(\n                  NoOpTransactionExecutionObserver, autoAdvance = true)\n                unblockCommit(backfillObserver)\n                backfillObserver.phases.postCommitPhase.exitBarrier.unblock()\n                waitForCommit(backfillObserver)\n            }\n\n          ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n          ThreadUtils.awaitResult(backfillFuture, timeout)\n          validateResult(expectedResult)\n        }\n      }\n    }\n\n    /**\n     * Scenario 2: Commit concurrent transaction after enabling Table feature requiring\n     * conflict resolution.\n     */\n    test(s\"$concurrentTransactionName - Scenario 2\") {\n      withTrackedBackfillCommits {\n        withTestTable {\n          validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n            val Seq(concurrentTransactionFuture) =\n              runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n                case (concurrentTransactionObserver :: Nil) =>\n                  // Prepare concurrent commit.\n                  unblockUntilPreCommit(concurrentTransactionObserver)\n                  busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                  val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n                  busyWaitFor(backfillObservers.size() > 0, timeout)\n\n                  // Commit concurrent transaction.\n                  unblockCommit(concurrentTransactionObserver)\n                  waitForCommit(concurrentTransactionObserver)\n\n                  // Finish the backfill, which only has one batch.\n                  commitSingleBackfillBatch()\n                  backfillFuture.get(timeout.toSeconds, TimeUnit.SECONDS)\n              }\n            ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n            validateResult(expectedResult)\n          }\n        }\n      }\n    }\n\n    /**\n     * Scenario 3: Prepare the concurrent commit before the table feature is enabled\n     * and commit after at least one backfill committed and before the metadata update commit.\n     *\n     * The concurrent transaction touches partitions 1 and 2 (one from batch 1, one from batch 2).\n     * Partition 3 is not touched, guaranteeing batch 2 always has work to do.\n     */\n    test(s\"$concurrentTransactionName - Scenario 3\") {\n      withTrackedBackfillCommits {\n        withTestTable {\n          // Enforce the backfill to use two commits to stop the process after one commit.\n          withSQLConf(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key ->\n              math.ceil(numFiles / 2.0).toInt.toString) {\n            validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 2) {\n              val Seq(concurrentTransactionFuture) =\n                runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n                  case (concurrentTransactionObserver :: Nil) =>\n                    // Prepare concurrent commit.\n                    unblockUntilPreCommit(concurrentTransactionObserver)\n                    busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                    val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n\n                    // Commit one backfill batch (processes partitions 0-1).\n                    commitSingleBackfillBatch()\n\n                    // Commit concurrent transaction (touches partitions 1 and 2).\n                    unblockCommit(concurrentTransactionObserver)\n                    waitForCommit(concurrentTransactionObserver)\n\n                    // Commit second backfill batch (processes partitions 2-3).\n                    // Partition 3 is untouched, so there's always work for batch 2.\n                    commitSingleBackfillBatch()\n\n                    backfillFuture.get(timeout.toSeconds, TimeUnit.SECONDS)\n                }\n              ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n              validateResult(expectedResult)\n            }\n          }\n        }\n      }\n    }\n\n    /**\n     * Scenario 4: The concurrent operations starts before the backfill and ends after it.\n     */\n    test(s\"$concurrentTransactionName - Scenario 4\") {\n      withTestTable {\n        validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n          val concurrentTransactionFuture =\n            runTxnsWithOrder__A_Start__B__A_end_without_observer_on_B(\n              concurrentTransaction, backfillTransaction)\n\n          ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n          validateResult(expectedResult)\n        }\n      }\n    }\n\n    /**\n     * Scenario 5: The concurrent transaction commits after the table feature has been enabled\n     * and concurrently to one backfill batch, requiring conflict resolution on the backfill.\n     */\n    test(s\"$concurrentTransactionName - Scenario 5\") {\n      withTestTable {\n        withTrackedBackfillCommits {\n          validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n            val Seq(concurrentTransactionFuture) =\n              runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n                case (concurrentTransactionObserver :: Nil) =>\n                  // Commit Row Tracking feature.\n                  val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n\n                  // Prepare concurrent transaction.\n                  unblockUntilPreCommit(concurrentTransactionObserver)\n                  busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                  // Prepare the commit of one backfill batch.\n                  prepareSingleBackfillBatchCommit()\n\n                  // Commit concurrent transaction.\n                  unblockCommit(concurrentTransactionObserver)\n                  waitForCommit(concurrentTransactionObserver)\n\n                  // Finish the backfill, which only has one batch.\n                  commitPreparedBackfillBatchCommit()\n                  ThreadUtils.awaitResult(backfillFuture, timeout)\n              }\n            ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n            validateResult(expectedResult)\n          }\n        }\n      }\n    }\n\n    /**\n     * Scenario 6: The concurrent transaction starts after the table feature is enabled and commits\n     * after one backfill has been committed concurrently, requiring conflict resolution on the\n     * concurrent transaction.\n     *\n     * The concurrent transaction touches partitions 1 and 2 (one from batch 1, one from batch 2).\n     * Partition 3 is not touched, guaranteeing batch 2 always has work to do.\n     */\n    test(s\"$concurrentTransactionName - Scenario 6\") {\n      withTrackedBackfillCommits {\n        withTestTable {\n          // We enforce the backfill to use two commits such that we can stop the process before\n          // the metadata update commit.\n          withSQLConf(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key ->\n              math.ceil(numFiles / 2.0).toInt.toString) {\n            validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 2) {\n              val Seq(concurrentTransactionFuture) =\n                runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n                  case (concurrentTransactionObserver :: Nil) =>\n                    val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n\n                    // Prepare concurrent commit.\n                    unblockUntilPreCommit(concurrentTransactionObserver)\n                    busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                    // Commit one backfill batch (processes partitions 0-1).\n                    commitSingleBackfillBatch()\n\n                    // Commit concurrent transaction (touches partitions 1 and 2).\n                    unblockCommit(concurrentTransactionObserver)\n                    waitForCommit(concurrentTransactionObserver)\n\n                    // Commit second backfill batch (processes partitions 2-3).\n                    // Partition 3 is untouched, so there's always work for batch 2.\n                    commitSingleBackfillBatch()\n\n                    ThreadUtils.awaitResult(backfillFuture, timeout)\n                }\n              ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n              validateResult(expectedResult)\n            }\n          }\n        }\n      }\n    }\n\n    /**\n     * Scenario 7: The concurrent transaction starts after the table feature is enabled and\n     * commits after the metadata update.\n     */\n    test(s\"$concurrentTransactionName - Scenario 7\") {\n      withTrackedBackfillCommits {\n        withTestTable {\n          validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n            val Seq(concurrentTransactionFuture) =\n              runFunctionsWithOrderingFromObserver(Seq(concurrentTransaction)) {\n                case (concurrentTransactionObserver :: Nil) =>\n                  val backfillFuture = launchBackFillAndBlockAfterFeatureIsCommitted()\n\n                  // Prepare concurrent commit.\n                  unblockUntilPreCommit(concurrentTransactionObserver)\n                  busyWaitForPreparePhase(concurrentTransactionObserver)\n\n                  // Finish the backfill, which only has one batch.\n                  commitSingleBackfillBatch()\n                  backfillFuture.get(timeout.toSeconds, TimeUnit.SECONDS)\n\n                  // Unlock commit of concurrent transaction.\n                  unblockCommit(concurrentTransactionObserver)\n                  waitForCommit(concurrentTransactionObserver)\n              }\n\n            ThreadUtils.awaitResult(concurrentTransactionFuture, timeout)\n            validateResult(expectedResult)\n          }\n        }\n      }\n    }\n  }\n\n  testAllScenarios(\"INSERT\") { () =>\n    sql(s\"INSERT INTO $testTableName($colName, $partitionColumnName) VALUES(1337, 1)\").collect()\n  } { () =>\n    val insertedRow = Seq((1337, 1))\n    tableCreationDF.union(spark.createDataFrame(insertedRow))\n  }\n\n  // DELETE touches partitions 1 and 2 (id % 4 == 1 or 2)\n  testAllScenarios(\"DELETE\") { () =>\n    sql(s\"DELETE FROM $testTableName WHERE $colName IN (1, 2)\").collect()\n  } { () =>\n    assert(!usePersistentDeletionVectors ||\n      !DeletionVectorUtils.isTableDVFree(latestSnapshot))\n    tableCreationDF.where(s\"$colName NOT IN (1, 2)\")\n  }\n\n  // UPDATE touches partitions 1 and 2 (id % 4 == 1 or 2)\n  testAllScenarios(\"UPDATE\") { () =>\n    sql(s\"UPDATE $testTableName SET $colName = $colName + 1000 WHERE $colName IN (1, 2)\").collect()\n  } { () =>\n    assert(\n      !usePersistentDeletionVectors || !DeletionVectorUtils.isTableDVFree(latestSnapshot)\n    )\n    // id=1 becomes 1001 (partition 1), id=2 becomes 1002 (partition 2)\n    val updatedRows = Seq((1001, 1), (1002, 2))\n    tableCreationDF.where(\"id NOT IN (1, 2)\").union(spark.createDataFrame(updatedRows))\n  }\n\n  // DF to create the view used as a source for MERGEs. When joining on 'colName', the lower half\n  // of the rows in 'testTableName' is unmatched, the upper half of 'testTableName' is matched\n  // by the lower half of 'mergeSourceDF', and the upper half of 'mergeSourceDF' is unmatched.\n  private lazy val mergeSourceDF: DataFrame =\n    withPartitionColumn(tableCreationDF.select(col(colName) + (numRows / 2) as colName))\n\n  // Create a temporary view used as the source for MERGEs based on 'mergeSourceDF'.\n  private def withMergeSource(testBlock: String => Array[Row]): Array[Row] = {\n    val sourceViewName = \"source\"\n    mergeSourceDF.createTempView(sourceViewName)\n    try {\n      testBlock(sourceViewName)\n    } finally {\n      sql(s\"DROP VIEW $sourceViewName\")\n    }\n  }\n\n  // MERGE touches partitions 1 and 2 only via partition conditions on WHEN clauses\n  testAllScenarios(\"MERGE with not matched and not matched by source\") { () =>\n    withMergeSource { sourceViewName =>\n      val mergeStatement =\n        s\"\"\"MERGE INTO $testTableName t\n           |USING $sourceViewName s\n           |ON s.$colName = t.$colName\n           |WHEN NOT MATCHED AND s.$partitionColumnName IN (1, 2) THEN INSERT *\n           |WHEN NOT MATCHED BY SOURCE AND t.$partitionColumnName IN (1, 2) THEN DELETE\n           |\"\"\".stripMargin\n      sql(mergeStatement).collect()\n    }\n  } { () =>\n    // Target has ids [0, numRows), source has ids [numRows/2, numRows + numRows/2)\n    // per mergeSourceDF definition.\n    // MATCHED: [numRows/2, numRows)\n    // NOT MATCHED BY SOURCE: [0, numRows/2)\n    // NOT MATCHED: [numRows, ...)\n    // With partition filter, we DELETE [0, numRows/2) in partitions 1,2 and\n    // INSERT [numRows, ...) in partitions 1,2.\n    // targetRowsToKeep: outside partitions 1,2 (untouched) OR id >= numRows/2\n    // (matched, not deleted).\n    val targetRowsToKeep = tableCreationDF.where(\n      s\"$partitionColumnName NOT IN (1, 2) OR $colName >= ${numRows / 2}\")\n    val sourceRowsToInsert = mergeSourceDF.where(\n      s\"$partitionColumnName IN (1, 2) AND $colName >= $numRows\")\n    targetRowsToKeep.union(sourceRowsToInsert)\n  }\n\n  // MERGE touches partitions 1 and 2 only via partition conditions on WHEN clauses\n  testAllScenarios(\"MERGE with matched and not matched\") { () =>\n    withMergeSource { sourceViewName =>\n      val mergeStatement =\n        s\"\"\"MERGE INTO $testTableName t\n           |USING $sourceViewName s\n           |ON s.$colName = t.$colName\n           |WHEN MATCHED AND t.$partitionColumnName IN (1, 2) THEN UPDATE SET *\n           |WHEN NOT MATCHED AND s.$partitionColumnName IN (1, 2) THEN INSERT *\n           |\"\"\".stripMargin\n      sql(mergeStatement).collect()\n    }\n  } { () =>\n    // Target has ids [0, numRows), source has ids [numRows/2, numRows + numRows/2)\n    // per mergeSourceDF definition.\n    // MATCHED: [numRows/2, numRows)\n    // NOT MATCHED: [numRows, ...)\n    // With partition filter, we UPDATE [numRows/2, numRows) in partitions 1,2 and\n    // INSERT [numRows, ...) in partitions 1,2.\n    // targetRowsNotUpdated: outside partitions 1,2 (untouched) OR id < numRows/2 (unmatched).\n    val targetRowsNotUpdated = tableCreationDF.where(\n      s\"$partitionColumnName NOT IN (1, 2) OR $colName < ${numRows / 2}\")\n    val updatedRows = mergeSourceDF.where(\n      s\"$partitionColumnName IN (1, 2) AND $colName < $numRows\")\n    val insertedRows = mergeSourceDF.where(\n      s\"$partitionColumnName IN (1, 2) AND $colName >= $numRows\")\n    targetRowsNotUpdated.union(updatedRows).union(insertedRows)\n  }\n\n  // OPTIMIZE touches partitions 1 and 2 only\n  testAllScenarios(\"OPTIMIZE\") { () =>\n    sql(s\"OPTIMIZE $testTableName WHERE $partitionColumnName IN (1, 2)\").collect()\n  } { () =>\n    tableCreationDF\n  }\n\n  /**\n   * RESTORE uses commitLarge which does not do conflict checking nor retry. So when there is\n   * a concurrent conflict, either RESTORE will fail from concurrent modification or\n   * Backfill will fail from the metadata change.\n   */\n  test(\"Backfill fails from conflict with RESTORE\") {\n    // This tries to undo the insert.\n    val concurrentTransaction = () => {\n      sql(s\"RESTORE TABLE $testTableName TO VERSION AS OF 1\").collect()\n    }\n\n    testScenario5WithCommitLarge(concurrentTransaction, tableCreationDF)\n  }\n\n  test(\"RESTORE fails from conflict with Backfill\") {\n    // This tries to undo the insert.\n    val concurrentTransaction = () => {\n      sql(s\"RESTORE TABLE $testTableName TO VERSION AS OF 1\").collect()\n    }\n\n    testScenario6WithCommitLarge(concurrentTransaction)\n  }\n}\n\nclass RowTrackingBackfillConflictsDVSuite extends RowTrackingBackfillConflictsSuite {\n override val usePersistentDeletionVectors = true\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/concurrency/PhaseLockingTestMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.concurrency\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.ConcurrencyHelpers\nimport org.apache.spark.sql.delta.fuzzer.AtomicBarrier\n\nimport org.apache.spark.SparkFunSuite\n\ntrait PhaseLockingTestMixin { self: SparkFunSuite =>\n  /** Keep checking if `barrier` in `state` until it's the case or `waitTime` expires. */\n  def busyWaitForState(\n      barrier: AtomicBarrier,\n      state: AtomicBarrier.State,\n      waitTime: FiniteDuration): Unit =\n    busyWaitFor(\n      barrier.load() == state,\n      waitTime,\n      s\"Exceeded deadline waiting for $barrier to transition to state $state\")\n\n  /**\n   * Keep checking if `check` return `true` until it's the case or `waitTime` expires.\n   *\n   * Optionally provide a custom error `message`.\n   */\n  def busyWaitFor(\n      check: => Boolean,\n      timeout: FiniteDuration,\n      // lazy evaluate so closed over states are evaluated at time of failure not invocation\n      message: => String = \"Exceeded deadline waiting for check to become true.\"): Unit = {\n    if (!ConcurrencyHelpers.busyWaitFor(check, timeout)) {\n      fail(message)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/concurrency/TransactionExecutionObserverSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.concurrency\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.fuzzer.{AtomicBarrier, IllegalStateTransitionException, OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport io.delta.tables.{DeltaTable => IODeltaTable}\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Check that [[TransactionExecutionObserver]] is invoked correctly by transactions\n * and commands.\n *\n * Also check the testing tools that use this API.\n */\nclass TransactionExecutionObserverSuite extends QueryTest with SharedSparkSession\n  with DeltaSQLCommandTest\n  with PhaseLockingTestMixin\n  with TransactionExecutionTestMixin {\n\n  override val timeout: FiniteDuration = 10000.millis\n\n  test(\"Phase Locking - sequential\") {\n    withTempDir { tempFile =>\n\n      val tempPath = tempFile.toString\n\n      spark.range(100).write.format(\"delta\").save(tempPath)\n\n      val observer = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"test-txn\"))\n      val deltaLog = DeltaLog.forTable(spark, tempPath)\n\n      // get things started\n      observer.phases.initialPhase.entryBarrier.unblock()\n\n      assert(!observer.phases.initialPhase.hasEntered)\n      TransactionExecutionObserver.withObserver(observer) {\n        deltaLog.withNewTransaction { txn =>\n          assert(observer.phases.initialPhase.hasEntered)\n          assert(observer.phases.initialPhase.hasLeft)\n          assert(!observer.phases.preparePhase.hasEntered)\n          assert(!observer.phases.commitPhase.hasEntered)\n          assert(!observer.phases.backfillPhase.hasEntered)\n          assert(!observer.phases.postCommitPhase.hasEntered)\n\n          // allow things to progress\n          observer.phases.preparePhase.entryBarrier.unblock()\n          observer.phases.commitPhase.entryBarrier.unblock()\n          observer.phases.backfillPhase.entryBarrier.unblock()\n          observer.phases.postCommitPhase.entryBarrier.unblock()\n          val removedFiles = txn.snapshot.allFiles.collect().map(_.remove).toSeq\n          txn.commit(removedFiles, DeltaOperations.ManualUpdate)\n\n          assert(observer.phases.preparePhase.hasEntered)\n          assert(observer.phases.preparePhase.hasLeft)\n          assert(observer.phases.commitPhase.hasEntered)\n          assert(observer.phases.commitPhase.hasLeft)\n          assert(observer.phases.backfillPhase.hasEntered)\n          assert(observer.phases.backfillPhase.hasLeft)\n          assert(observer.phases.postCommitPhase.hasEntered)\n          assert(observer.phases.postCommitPhase.hasLeft)\n        }\n      }\n      val res = spark.read.format(\"delta\").load(tempPath).collect()\n      assert(res.isEmpty)\n    }\n  }\n\n  test(\"Phase Locking - parallel\") {\n    withTempDir { tempFile =>\n\n      val tempPath = tempFile.toString\n\n      spark.range(100).write.format(\"delta\").save(tempPath)\n\n      val observer = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"test-txn\"))\n      val deltaLog = DeltaLog.forTable(spark, tempPath)\n\n      val testThread = new Thread(() => {\n        // make sure the transaction will use our observer\n        TransactionExecutionObserver.withObserver(observer) {\n          deltaLog.withNewTransaction { txn =>\n            val removedFiles = txn.snapshot.allFiles.collect().map(_.remove).toSeq\n            txn.commit(removedFiles, DeltaOperations.ManualUpdate)\n          }\n        }\n      })\n      testThread.start()\n\n      busyWaitForState(\n        observer.phases.initialPhase.entryBarrier, AtomicBarrier.State.Requested, timeout)\n\n      // get things started\n      observer.phases.initialPhase.entryBarrier.unblock()\n\n      busyWaitFor(observer.phases.initialPhase.hasEntered, timeout)\n      busyWaitFor(observer.phases.initialPhase.hasLeft, timeout)\n      assert(!observer.phases.preparePhase.hasEntered)\n\n      observer.phases.preparePhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.preparePhase.hasEntered, timeout)\n      busyWaitFor(observer.phases.preparePhase.hasLeft, timeout)\n      assert(!observer.phases.commitPhase.hasEntered)\n\n      observer.phases.commitPhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.commitPhase.hasEntered, timeout)\n      busyWaitFor(observer.phases.commitPhase.hasLeft, timeout)\n\n      observer.phases.backfillPhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.backfillPhase.hasEntered, timeout)\n      busyWaitFor(observer.phases.backfillPhase.hasLeft, timeout)\n\n      observer.phases.postCommitPhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.postCommitPhase.hasEntered, timeout)\n      busyWaitFor(observer.phases.postCommitPhase.hasLeft, timeout)\n      testThread.join(timeout.toMillis)\n      assert(!testThread.isAlive) // should have passed the barrier and completed\n\n      val res = spark.read.format(\"delta\").load(tempPath).collect()\n      assert(res.isEmpty)\n    }\n  }\n\n  test(\"Phase Locking - no reusing observer\") {\n    withTempDir { tempFile =>\n\n      val tempPath = tempFile.toString\n\n      spark.range(100).write.format(\"delta\").save(tempPath)\n\n      val observer = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"test-txn\"))\n      val deltaLog = DeltaLog.forTable(spark, tempPath)\n\n      // get things started\n      observer.phases.initialPhase.entryBarrier.unblock()\n\n      assert(!observer.phases.initialPhase.hasEntered)\n      TransactionExecutionObserver.withObserver(observer) {\n        deltaLog.withNewTransaction { txn =>\n          // allow things to progress\n          observer.phases.preparePhase.entryBarrier.unblock()\n          observer.phases.commitPhase.entryBarrier.unblock()\n          observer.phases.backfillPhase.entryBarrier.unblock()\n          observer.phases.postCommitPhase.entryBarrier.unblock()\n          val removedFiles = txn.snapshot.allFiles.collect().map(_.remove).toSeq\n          txn.commit(removedFiles, DeltaOperations.ManualUpdate)\n        }\n        // Check that we fail trying to re-unblock the barrier\n        assertThrows[IllegalStateTransitionException] {\n          deltaLog.withNewTransaction { txn =>\n            // allow things to progress\n            observer.phases.preparePhase.entryBarrier.unblock()\n            observer.phases.commitPhase.entryBarrier.unblock()\n            observer.phases.backfillPhase.entryBarrier.unblock()\n            observer.phases.postCommitPhase.entryBarrier.unblock()\n            val removedFiles = txn.snapshot.allFiles.collect().map(_.remove).toSeq\n            txn.commit(removedFiles, DeltaOperations.ManualUpdate)\n          }\n        }\n        // Check that we fail just waiting on the passed barrier\n        assertThrows[IllegalStateTransitionException] {\n          deltaLog.withNewTransaction { txn =>\n            val removedFiles = txn.snapshot.allFiles.collect().map(_.remove).toSeq\n            txn.commit(removedFiles, DeltaOperations.ManualUpdate)\n          }\n        }\n      }\n      val res = spark.read.format(\"delta\").load(tempPath).collect()\n      assert(res.isEmpty)\n    }\n  }\n\n  test(\"Phase Locking - delete command\") {\n    withTempDir { tempFile =>\n\n      val tempPath = tempFile.toString\n\n      spark.range(100).write.format(\"delta\").save(tempPath)\n\n      val observer = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"test-txn\"))\n      val deltaLog = DeltaLog.forTable(spark, tempPath)\n      val deltaTable = IODeltaTable.forPath(spark, tempPath)\n\n      def assertOperationNotVisible(): Unit =\n        assert(deltaTable.toDF.count() === 100)\n\n      val testThread = new Thread(() => {\n        // make sure the transaction will use our observer\n        TransactionExecutionObserver.withObserver(observer) {\n          deltaTable.delete()\n        }\n      })\n      testThread.start()\n\n      busyWaitForState(\n        observer.phases.initialPhase.entryBarrier, AtomicBarrier.State.Requested, timeout)\n\n      assertOperationNotVisible()\n\n      // get things started\n      observer.phases.initialPhase.entryBarrier.unblock()\n\n      busyWaitFor(observer.phases.initialPhase.hasLeft, timeout)\n\n      assertOperationNotVisible()\n\n      observer.phases.preparePhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.preparePhase.hasLeft, timeout)\n      assert(!observer.phases.commitPhase.hasEntered)\n      assert(!observer.phases.backfillPhase.hasEntered)\n      assert(!observer.phases.postCommitPhase.hasEntered)\n\n      assertOperationNotVisible()\n\n      observer.phases.commitPhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.commitPhase.hasLeft, timeout)\n      observer.phases.backfillPhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.backfillPhase.hasLeft, timeout)\n      observer.phases.postCommitPhase.entryBarrier.unblock()\n      busyWaitFor(observer.phases.postCommitPhase.hasLeft, timeout)\n      testThread.join(timeout.toMillis)\n      assert(!testThread.isAlive) // should have passed the barrier and completed\n\n      val res = spark.read.format(\"delta\").load(tempPath).collect()\n      assert(res.isEmpty)\n    }\n  }\n\n  test(\"Phase Locking - set next observer after commit\") {\n    withTempDir { tempFile =>\n      val tempPath = tempFile.toString\n\n      spark.range(end = 1).write.format(\"delta\").save(tempPath)\n\n      val observer = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"test-txn\"))\n      val deltaLog = DeltaLog.forTable(spark, tempPath)\n      val initialTableVersion = deltaLog.update().version\n\n      // get things started\n      val replacementObserver = new PhaseLockingTransactionExecutionObserver(\n        OptimisticTransactionPhases.forName(\"test-replacement-txn\"))\n\n      observer.setNextObserver(replacementObserver, autoAdvance = true)\n      unblockAllPhases(observer)\n\n      TransactionExecutionObserver.withObserver(observer) {\n        deltaLog.withNewTransaction { txn =>\n          observer.phases.postCommitPhase.exitBarrier.unblock()\n          val removedFiles = txn.snapshot.allFiles.collect().map(_.remove).toSeq\n          txn.commit(removedFiles, DeltaOperations.ManualUpdate)\n        }\n        val tableVersionAfterFirstTxn = deltaLog.update().version\n        assert(tableVersionAfterFirstTxn === initialTableVersion + 1,\n          \"expected a successful commit\")\n        // Check that we cannot re-use the old observer, with unblocks.\n        assertThrows[IllegalStateTransitionException] {\n          observer.phases.preparePhase.entryBarrier.unblock()\n        }\n\n        // Check that we can use the replaced observer to control a subsequent commit on the same\n        // thread.\n        val oldMetadata = deltaLog.update().metadata\n        val newMetadata = oldMetadata.copy(configuration = Map(\"foo\" -> \"bar\"))\n        unblockAllPhases(replacementObserver)\n        deltaLog.withNewTransaction { txn =>\n          txn.commit(Seq(newMetadata), DeltaOperations.ManualUpdate)\n        }\n        assert(deltaLog.update().version === tableVersionAfterFirstTxn + 1,\n          \"expected a successful commit\")\n        assert(replacementObserver.allPhasesHavePassed)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/concurrency/TransactionExecutionTestMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.concurrency\n\nimport java.util.concurrent.{ExecutorService, TimeUnit}\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration._\n\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.{ConcurrencyHelpers, OptimisticTransaction, TransactionExecutionObserver}\nimport org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver => TransactionObserver}\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.internal.Logging\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ThreadUtils\n\ntrait TransactionExecutionTestMixin {\n  self: PhaseLockingTestMixin with SharedSparkSession with Logging =>\n\n  /**\n   * Timeout used when waiting for individual phases of instrumented operations to complete.\n   */\n  val timeout: FiniteDuration = 120.seconds\n\n  /** Run a given function `fn` inside the given `executor` within a [[TransactionObserver]] */\n  private[delta] def runFunctionWithObserver(\n      name: String,\n      executorService: ExecutorService,\n      fn: () => Array[Row]): (TransactionObserver, Future[Array[Row]]) = {\n    val observer =\n      new TransactionObserver(OptimisticTransactionPhases.forName(s\"observer-txn-$name\"))\n    implicit val ec = ExecutionContext.fromExecutorService(executorService)\n    val txn = OptimisticTransaction.getActive()\n    val future = Future {\n      ConcurrencyHelpers.withOptimisticTransaction(txn) {\n        spark.withActive(\n          try {\n            TransactionExecutionObserver.withObserver(observer) {\n              fn()\n            }\n          } catch {\n            case ex: Exception =>\n              logError(s\"Error on test thread\", ex)\n              throw ex\n          }\n        )\n      }\n    }\n    (observer, future)\n  }\n\n  /** Run a given `queryString` inside the given `executor` */\n  def runQueryWithObserver(name: String, executor: ExecutorService, queryString: String)\n    : (TransactionObserver, Future[Array[Row]]) = {\n    def fn(): Array[Row] = spark.sql(queryString).collect()\n    runFunctionWithObserver(name, executor, fn)\n  }\n\n  /**\n   * Run `functions` with the ordering defined by `observerOrdering` function.\n   * This function returns futures for each of the query results.\n   */\n  private[delta] def runFunctionsWithOrderingFromObserver\n      (functions: Seq[() => Array[Row]])\n      (observerOrdering: (Seq[TransactionObserver]) => Unit)\n      : Seq[Future[Array[Row]]] = {\n    val executors = functions.zipWithIndex.map { case (_, index) =>\n      ThreadUtils.newDaemonSingleThreadExecutor(threadName = s\"executor-txn-$index\")\n    }\n    try {\n      val (observers, futures) = functions.zipWithIndex.map { case (fn, index) =>\n        runFunctionWithObserver(name = s\"query-$index\", executors(index), fn)\n      }.unzip\n\n      // Run the observer ordering function.\n      observerOrdering(observers)\n\n      // wait for futures to succeed or fail\n      for (future <- futures) {\n        try {\n          ThreadUtils.awaitResult(future, timeout)\n        } catch {\n          case _: SparkException =>\n            // pass\n            true\n        }\n      }\n\n      futures\n    } finally {\n      for (executor <- executors) {\n        executor.shutdownNow()\n        executor.awaitTermination(timeout.toMillis, TimeUnit.MILLISECONDS)\n      }\n    }\n  }\n\n  /** Unblocks all phases before the `commitPhase` for [[TransactionObserver]] */\n  def unblockUntilPreCommit(observer: TransactionObserver): Unit = {\n    observer.phases.initialPhase.entryBarrier.unblock()\n    observer.phases.preparePhase.entryBarrier.unblock()\n  }\n\n  /**\n   * Unblocks the `commitPhase` and `backfillPhase` for [[TransactionObserver]].\n   */\n  def unblockCommit(observer: TransactionObserver): Unit = {\n    observer.phases.commitPhase.entryBarrier.unblock()\n    observer.phases.backfillPhase.entryBarrier.unblock()\n    observer.phases.postCommitPhase.entryBarrier.unblock()\n  }\n\n  /** Unblocks all phases for [[TransactionObserver]] so that corresponding query can finish. */\n  def unblockAllPhases(observer: TransactionObserver): Unit = {\n    observer.phases.initialPhase.entryBarrier.unblock()\n    observer.phases.preparePhase.entryBarrier.unblock()\n    observer.phases.commitPhase.entryBarrier.unblock()\n    observer.phases.backfillPhase.entryBarrier.unblock()\n    observer.phases.postCommitPhase.entryBarrier.unblock()\n  }\n\n  def waitForPrecommit(observer: TransactionObserver): Unit =\n    busyWaitFor(observer.phases.preparePhase.hasEntered, timeout)\n\n  def waitForCommit(observer: TransactionObserver): Unit = {\n    busyWaitFor(observer.phases.commitPhase.hasLeft, timeout)\n    busyWaitFor(observer.phases.backfillPhase.hasLeft, timeout)\n    busyWaitFor(observer.phases.postCommitPhase.hasLeft, timeout)\n  }\n\n  /**\n   * Prepare and commit the transaction managed by the given observer.\n   * If nextObserver is set, we need to manually call backfillPhase.leave() to advance to the\n   * nextObserver. Details in [[TransactionObserver.waitForCommitPhaseAndAdvanceToNextObserver]].\n   */\n  private def prepareAndCommitBase(\n      observer: TransactionObserver, hasNextObserver: Boolean): Unit = {\n    unblockUntilPreCommit(observer)\n    waitForPrecommit(observer)\n    unblockCommit(observer)\n    if (hasNextObserver) {\n      observer.phases.postCommitPhase.leave()\n    }\n    waitForCommit(observer)\n  }\n\n  /**\n   * Prepare and commit the transaction managed by the given observer.\n   */\n  def prepareAndCommit(observer: TransactionObserver): Unit = {\n    prepareAndCommitBase(observer, hasNextObserver = false)\n  }\n\n  /**\n   * Prepare and commit the transaction managed by the given observer which has nextObserver set.\n   */\n  def prepareAndCommitWithNextObserverSet(observer: TransactionObserver): Unit = {\n    prepareAndCommitBase(observer, hasNextObserver = true)\n  }\n\n  /**\n   * Run 2 transactions A, B with following order:\n   *\n   * t1 -------------------------------------- TxnA starts\n   * t2 --------- TxnB starts and commits (no transaction observer)\n   * t6 -------------------------------------- TxnA commits\n   *\n   * This function returns futures for each of the query runs.\n   */\n  def runTxnsWithOrder__A_Start__B__A_end_without_observer_on_B(\n      txnA: () => Array[Row],\n      txnB: () => Array[Row]): Future[Array[Row]] = {\n    val Seq(futureA) =\n      runFunctionsWithOrderingFromObserver(Seq(txnA)) {\n        case (observerA :: Nil) =>\n          // A starts\n          unblockUntilPreCommit(observerA)\n          busyWaitFor(observerA.phases.preparePhase.hasEntered, timeout)\n\n          // B starts and finishes\n          txnB()\n\n          // A commits\n          unblockCommit(observerA)\n          waitForCommit(observerA)\n      }\n    futureA\n  }\n\n  /**\n   * Run 2 transactions A, B with following order:\n   *\n   * t1 -------------------------------------- TxnA starts\n   * t2 --------- TxnB starts\n   * t3 --------- TxnB commits\n   * t6 -------------------------------------- TxnA commits\n   *\n   * This function returns futures for each of the query runs.\n   */\n  def runTxnsWithOrder__A_Start__B__A_End(txnA: () => Array[Row], txnB: () => Array[Row])\n      : (Future[Array[Row]], Future[Array[Row]]) = {\n    val Seq(futureA, futureB) =\n      runFunctionsWithOrderingFromObserver(Seq(txnA, txnB)) {\n        case (observerA :: observerB :: Nil) =>\n          // A starts\n          unblockUntilPreCommit(observerA)\n          busyWaitFor(observerA.phases.preparePhase.hasEntered, timeout)\n\n          // B starts and commits\n          unblockAllPhases(observerB)\n          busyWaitFor(observerB.phases.postCommitPhase.hasLeft, timeout)\n\n          // A commits\n          observerA.phases.commitPhase.entryBarrier.unblock()\n          busyWaitFor(observerA.phases.commitPhase.hasLeft, timeout)\n          observerA.phases.backfillPhase.entryBarrier.unblock()\n          busyWaitFor(observerA.phases.backfillPhase.hasLeft, timeout)\n          observerA.phases.postCommitPhase.entryBarrier.unblock()\n          busyWaitFor(observerA.phases.postCommitPhase.hasLeft, timeout)\n      }\n    (futureA, futureB)\n  }\n\n  /**\n   * Run 3 queries A, B, C with following order:\n   *\n   * t1 -------------------------------------- TxnA starts\n   * t2 --------- TxnB starts\n   * t3 --------- TxnB commits\n   * t4 ----------------- TxnC starts\n   * t5 ----------------- TxnC commits\n   * t6 -------------------------------------- TxnA commits\n   *\n   * This function returns futures for each of the query runs.\n   */\n  def runTxnsWithOrder__A_Start__B__C__A_End(\n      txnA: () => Array[Row],\n      txnB: () => Array[Row],\n      txnC: () => Array[Row])\n      : (Future[Array[Row]], Future[Array[Row]], Future[Array[Row]]) = {\n\n    val Seq(futureA, futureB, futureC) =\n      runFunctionsWithOrderingFromObserver(Seq(txnA, txnB, txnC)) {\n        case (observerA :: observerB :: observerC :: Nil) =>\n          // A starts\n          unblockUntilPreCommit(observerA)\n          busyWaitFor(observerA.phases.preparePhase.hasEntered, timeout)\n\n          // B starts and commits\n          unblockAllPhases(observerB)\n          busyWaitFor(observerB.phases.postCommitPhase.hasLeft, timeout)\n\n          // C starts and commits\n          unblockAllPhases(observerC)\n          busyWaitFor(observerC.phases.postCommitPhase.hasLeft, timeout)\n\n          // A commits\n          observerA.phases.commitPhase.entryBarrier.unblock()\n          busyWaitFor(observerA.phases.commitPhase.hasLeft, timeout)\n          observerA.phases.backfillPhase.entryBarrier.unblock()\n          busyWaitFor(observerA.phases.backfillPhase.hasLeft, timeout)\n          observerA.phases.postCommitPhase.entryBarrier.unblock()\n          busyWaitFor(observerA.phases.postCommitPhase.hasLeft, timeout)\n      }\n    (futureA, futureB, futureC)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CatalogManagedStreamingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.streaming.StreamTest\n\n/**\n * General note on streaming tests: by default, a streaming query uses the ProcessingTime trigger\n * with a 0-second interval, so the query will be executed as fast as possible, and the read path\n * will be triggered periodically. Because of this, things like asserting the number of getCommits\n * on the tracking client will not be deterministic.\n */\ntrait CatalogManagedStreamingSuiteBase\n  extends StreamTest\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with CatalogOwnedTestBaseSuite {\n\n\n  import testImplicits._\n  import org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\n  protected def assertNumCommitsCalled(expectedNumCommits: Int): Unit = {\n    getTrackingClient.foreach { trackingClient =>\n      assert(trackingClient.numCommitsCalled.get === expectedNumCommits)\n    }\n  }\n\n  protected def assertNumGetCommitsCalled(expectedNumGetCommits: Int): Unit = {\n    getTrackingClient.foreach { trackingClient =>\n      assert(trackingClient.numGetCommitsCalled.get >= expectedNumGetCommits)\n    }\n  }\n\n  protected def getTrackingClient: Option[TrackingCommitCoordinatorClient] =\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      Some(getCatalogOwnedCommitCoordinatorClient(\n        CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING)\n          .asInstanceOf[TrackingCommitCoordinatorClient])\n    } else {\n      None\n    }\n\n  protected def resetTrackingClient(): Unit = {\n    getTrackingClient.foreach(_.reset())\n  }\n\n  protected override def beforeEach(): Unit = {\n    super.beforeEach()\n    resetTrackingClient()\n  }\n\n  test(\"stream from delta source\") {\n    withTempTable(createTable = false) { sourceTableName =>\n      sql(s\"CREATE TABLE $sourceTableName (value INT) USING delta\")\n\n      val df = spark.readStream\n        .format(\"delta\")\n        .table(sourceTableName)\n\n      resetTrackingClient()\n\n      testStream(df)(\n        Execute{ _ =>\n          Seq(1, 2).toDF().write.format(\"delta\").mode(\"append\").saveAsTable(sourceTableName)\n        },\n        ProcessAllAvailable(),\n        CheckAnswer(1, 2),\n        Execute { _ => assertNumCommitsCalled(1) },\n        // At least one read from the commit and one from checking the result\n        Execute { _ => assertNumGetCommitsCalled(2) }\n      )\n    }\n  }\n\n  test(\"stream to delta sink\") {\n    // The dir is only used as the checkpoint location and doesn't imply a path-based access.\n    withTempDir { tempDir =>\n      withTempTable(createTable = false) { sinkTableName =>\n        var expectedNumCommits = 0\n        var expectedNumGetCommits = 0\n\n        val inputData = MemoryStream[Int]\n        val query = inputData\n          .toDF()\n          .writeStream\n          .format(\"delta\")\n          .option(\"checkpointLocation\", tempDir.getAbsolutePath)\n          .toTable(sinkTableName)\n        query.processAllAvailable()\n\n        try {\n          inputData.addData(1)\n          query.processAllAvailable()\n          expectedNumCommits += 1\n          expectedNumGetCommits += 1\n\n          // Loading after the first write to ensure the delta log is created.\n          val outputDf = spark.read.format(\"delta\").table(sinkTableName)\n          checkDatasetUnorderly(outputDf.as[Int], 1)\n          expectedNumGetCommits += 1\n          assertNumCommitsCalled(expectedNumCommits)\n          assertNumGetCommitsCalled(expectedNumGetCommits)\n\n          inputData.addData(2)\n          query.processAllAvailable()\n          expectedNumCommits += 1\n          expectedNumGetCommits += 1\n\n          checkDatasetUnorderly(outputDf.as[Int], 1, 2)\n          expectedNumGetCommits += 1\n          assertNumCommitsCalled(expectedNumCommits)\n          assertNumGetCommitsCalled(expectedNumGetCommits)\n        } finally {\n          query.stop()\n        }\n      }\n    }\n  }\n\n  test(\"stream from delta source to delta sink with shared commit coordinator\") {\n    withTempDir { tempDir =>\n      withTempTable(createTable = false) { sourceTableName =>\n        withTempTable(createTable = false) { sinkTableName =>\n          var expectedNumCommits = 0\n          var expectedNumGetCommits = 0\n\n          sql(s\"CREATE TABLE $sourceTableName (value INT) USING delta\")\n\n          resetTrackingClient()\n\n          val query = spark.readStream\n            .format(\"delta\")\n            .table(sourceTableName)\n            .toDF()\n            .writeStream\n            .format(\"delta\")\n            .option(\"checkpointLocation\", tempDir.getAbsolutePath)\n            .toTable(sinkTableName)\n          query.processAllAvailable()\n          expectedNumCommits += 1\n          expectedNumGetCommits += 1\n          assertNumCommitsCalled(expectedNumCommits)\n          assertNumGetCommitsCalled(expectedNumGetCommits)\n\n          try {\n            Seq(1).toDF().write.format(\"delta\").mode(\"append\").saveAsTable(sourceTableName)\n            query.processAllAvailable()\n            // One commit to the source table and one commit to the sink table\n            expectedNumCommits += 2\n            expectedNumGetCommits += 2\n\n            // Loading after the first write to ensure the delta log is created.\n            val outputDf = spark.read.format(\"delta\").table(sinkTableName)\n            checkDatasetUnorderly(outputDf.as[Int], 1)\n            expectedNumGetCommits += 1\n            assertNumCommitsCalled(expectedNumCommits)\n            assertNumGetCommitsCalled(expectedNumGetCommits)\n\n            Seq(2).toDF().write.format(\"delta\").mode(\"append\").saveAsTable(sourceTableName)\n            query.processAllAvailable()\n            // One commit to the source table and one commit to the sink table\n            expectedNumCommits += 2\n            expectedNumGetCommits += 2\n\n            checkDatasetUnorderly(outputDf.as[Int], 1, 2)\n            expectedNumGetCommits += 1\n            assertNumCommitsCalled(expectedNumCommits)\n            assertNumGetCommitsCalled(expectedNumGetCommits)\n          } finally {\n            query.stop()\n          }\n        }\n      }\n    }\n  }\n}\n\nclass CatalogManagedStreamingSuite extends CatalogManagedStreamingSuiteBase {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(10)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CatalogOwnedEnablementSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.io.File\nimport java.util.UUID\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.test.{DeltaExceptionTestUtils, DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.commons.lang3.NotImplementedException\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\nclass CatalogOwnedEnablementSuite\n  extends QueryTest\n  with CatalogOwnedTestBaseSuite\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest\n  with DeltaTestUtilsBase\n  with DeltaExceptionTestUtils {\n\n  override protected def beforeEach(): Unit = {\n    super.beforeEach()\n    CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n    CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n      catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING,\n      commitCoordinatorBuilder = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 3)\n    )\n  }\n\n  private val ICT_ENABLED_KEY = DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key\n\n  /**\n   * Validate that the table has the expected enablement of Catalog-Owned table feature.\n   * Specifically, [[CatalogOwnedTableFeature]] and its dependent features should be present\n   * in the table protocol.\n   *\n   * @param tableName The name of the table to validate.\n   * @param expectEnabled Whether the Catalog-Owned table feature should be enabled or not.\n   */\n  private def validateCatalogOwnedCompleteEnablement(\n      tableName: String,\n      expectEnabled: Boolean): Unit = {\n    val (_, snapshot) = getDeltaLogWithSnapshot(TableIdentifier(tableName))\n    assert(snapshot.isCatalogOwned == expectEnabled)\n    Seq(\n      CatalogOwnedTableFeature,\n      VacuumProtocolCheckTableFeature,\n      InCommitTimestampTableFeature\n    ).foreach { feature =>\n      assert(snapshot.protocol.writerFeatures.exists(_.contains(feature.name)) == expectEnabled)\n    }\n    Seq(\n      CatalogOwnedTableFeature,\n      VacuumProtocolCheckTableFeature\n    ).foreach { feature =>\n      assert(snapshot.protocol.readerFeatures.exists(_.contains(feature.name)) == expectEnabled)\n    }\n  }\n\n  protected def commitCoordinatorName: String =\n    CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING\n\n  protected def createTable(\n      tableName: String,\n      clusterBy: Seq[String] = Seq.empty,\n      properties: Map[String, String] = Map.empty): Unit = {\n    var stmt = s\"CREATE TABLE $tableName (id INT) USING delta \"\n    if (clusterBy.nonEmpty) {\n      stmt += s\"CLUSTER BY (${clusterBy.mkString(\", \")}) \"\n    }\n    if (properties.nonEmpty) {\n      val kv = properties.map { case (k, v) => s\"'$k' = '$v'\" }.mkString(\", \")\n      stmt += s\"TBLPROPERTIES ($kv)\"\n    }\n    sql(stmt)\n  }\n\n  /**\n   * Helper function to set up and validate the initial table to be tested\n   * for Catalog Owned enablement.\n   *\n   * @param tableName The name of the table to be created for the test.\n   * @param createCatalogOwnedTableAtInit Whether to enable Catalog-Owned table feature\n   *                                      at the time of table creation.\n   */\n  private def testCatalogOwnedEnablementSetup(\n      tableName: String,\n      createCatalogOwnedTableAtInit: Boolean): Unit = {\n    if (createCatalogOwnedTableAtInit) {\n      createTable(tableName, properties = Map(\n        s\"delta.feature.${CatalogOwnedTableFeature.name}\" -> \"supported\"))\n    } else {\n      createTable(tableName)\n    }\n    // Insert initial data to the table\n    spark.sql(s\"INSERT INTO $tableName VALUES 1\") // commit 1\n    spark.sql(s\"INSERT INTO $tableName VALUES 2\") // commit 2\n    validateCatalogOwnedCompleteEnablement(\n      tableName,\n      expectEnabled = createCatalogOwnedTableAtInit)\n  }\n\n  /**\n   * Helper function to create a table and run the test.\n   *\n   * @param f The test function to run with the random-generated table name.\n   */\n  private def withRandomTable(\n      createCatalogOwnedTableAtInit: Boolean)(f: String => Unit): Unit = {\n    val randomTableName = s\"testTable_${UUID.randomUUID().toString.replace(\"-\", \"\")}\"\n    withTable(randomTableName) {\n      testCatalogOwnedEnablementSetup(randomTableName, createCatalogOwnedTableAtInit)\n      f(randomTableName)\n    }\n  }\n\n  /**\n   * Validate the usage log blob.\n   *\n   * @param usageLogBlob The usage log blob to validate.\n   * @param expectedPresentFields The fields that should be present in the usage log blob.\n   * @param expectedAbsentFields The fields that should not be present in the usage log blob.\n   * @param expectedValues The expected values for the fields.\n   */\n  private def validateUsageLogBlob(\n      usageLogBlob: Map[String, Any],\n      expectedPresentFields: Seq[String] = Seq.empty,\n      expectedAbsentFields: Seq[String] = Seq.empty,\n      expectedValues: Map[String, Any] = Map.empty): Unit = {\n    expectedPresentFields.foreach { field =>\n      assert(usageLogBlob.contains(field), s\"Field '$field' should be present in usage log blob\")\n    }\n    expectedAbsentFields.foreach { field =>\n      assert(!usageLogBlob.contains(field),\n        s\"Field '$field' should not be present in usage log blob\")\n    }\n    assert(expectedValues.size === expectedPresentFields.size,\n      \"The size of `expectedValues` should match the size of `expectedPresentFields`.\")\n    expectedValues.foreach { case (field, expectedValue) =>\n      if (field == \"stackTrace\") {\n        // Only validate the start of the stack trace.\n        assert(\n          usageLogBlob(field).asInstanceOf[String].startsWith(expectedValue.asInstanceOf[String]),\n          s\"Field '$field' should start with '$expectedValue' but was '${usageLogBlob(field)}'\"\n        )\n      } else if (field == \"checksumOpt\" || field == \"properties\") {\n        // Validate a portion of the entire `checksumOpt` or `properties` map.\n        if (expectedValue == null) {\n          assert(usageLogBlob(field) == null,\n            s\"Field '$field' should be null but was '${usageLogBlob(field)}'\")\n          return\n        }\n        val properties = expectedValue.asInstanceOf[Map[String, Any]]\n        properties.foreach { case (key, value) =>\n          assert(\n            usageLogBlob(field).asInstanceOf[Map[String, Any]].get(key).contains(value),\n            s\"Field '$field' should contain '$key' with value '$value' \" +\n              s\"but was '${usageLogBlob(field)}'\"\n          )\n        }\n      } else {\n        assert(usageLogBlob(field) === expectedValue,\n          s\"Field '$field' should have value '$expectedValue' but was '${usageLogBlob(field)}'\")\n      }\n    }\n  }\n\n  /**\n   * Helper function to validate usage log for commit coordinator population when path-based access\n   * is blocked.\n   */\n  private def validateInvalidPathBasedAccessUsageLog(\n      tempDir: File,\n      expectedVersion: String,\n      expectedChecksumOpt: Any,\n      sqlConf: Map[String, String] = Map.empty): Unit = {\n    val log = DeltaLog.forTable(spark, tempDir.getCanonicalPath)\n\n    val usageLog = Log4jUsageLogger.track {\n      val error = intercept[DeltaIllegalStateException] {\n        withSQLConf(sqlConf.toSeq: _*) {\n          sql(s\"INSERT INTO TABLE delta.`$tempDir` VALUES (1), (2), (3)\")\n        }\n      }\n      checkError(\n        error,\n        \"DELTA_PATH_BASED_ACCESS_TO_CATALOG_MANAGED_TABLE_BLOCKED\",\n        sqlState = \"KD00G\",\n        parameters = Map(\"path\" -> log.logPath.toString))\n    }.filter { log =>\n      log.metric == \"tahoeEvent\" &&\n        log.tags.getOrElse(\"opType\", null) ==\n          CatalogOwnedUsageLogs.COMMIT_COORDINATOR_POPULATION_INVALID_PATH_BASED_ACCESS\n    }\n\n    assert(usageLog.nonEmpty, \"Should have usage log for INVALID_PATH_BASED_ACCESS scenario\")\n    val usageLogBlob = JsonUtils.fromJson[Map[String, Any]](usageLog.head.blob)\n\n    val logStore =\n      \"org.apache.spark.sql.delta.storage.DelegatingLogStore\"\n\n    validateUsageLogBlob(\n      usageLogBlob,\n      expectedPresentFields = Seq(\n        \"path\",\n        \"version\",\n        \"stackTrace\",\n        \"latestCheckpointVersion\",\n        \"checksumOpt\",\n        \"properties\",\n        \"logStore\"\n      ),\n      expectedAbsentFields = Seq(\n        \"catalogTable.identifier\",\n        \"catalogTable.tableType\",\n        \"commitCoordinator.getClass\"\n      ),\n      expectedValues = Map(\n        \"path\" -> log.logPath.toString,\n        \"version\" -> expectedVersion,\n        \"stackTrace\" ->\n          (\"org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTableUtils$\" +\n            \".recordCommitCoordinatorPopulationUsageLog\"),\n        \"latestCheckpointVersion\" -> -1,\n        // Only check for certain fields of `checksumOpt` since the entire map\n        // is too large to validate.\n        \"checksumOpt\" -> expectedChecksumOpt,\n        \"properties\" -> Map(\n          \"delta.minReaderVersion\" -> \"3\",\n          \"delta.minWriterVersion\" -> \"7\",\n          \"delta.feature.appendOnly\" -> \"supported\",\n          \"delta.feature.invariants\" -> \"supported\",\n          // To avoid potential naming change in the future.\n          s\"delta.feature.${CatalogOwnedTableFeature.name}\" -> \"supported\",\n          \"delta.feature.inCommitTimestamp\" -> \"supported\",\n          \"delta.feature.vacuumProtocolCheck\" -> \"supported\",\n          \"delta.enableInCommitTimestamps\" -> \"true\"\n        ),\n        \"logStore\" -> logStore\n      )\n    )\n  }\n\n  test(\"Quality of life table feature should be enabled by CREATE CLONE for target table\") {\n    withRandomTable(createCatalogOwnedTableAtInit = false) { tableName =>\n      withTable(\"t1\", \"t2\") {\n        val qolTableFeatures =\n          CatalogOwnedTableUtils.QOL_TABLE_FEATURES_AND_PROPERTIES.map(_._1)\n            .toSet\n        withSQLConf(defaultCatalogOwnedFeatureEnabledKey -> \"supported\") {\n          sql(s\"CREATE TABLE t1 SHALLOW CLONE $tableName\")\n        }\n        sql(s\"CREATE TABLE t2 SHALLOW CLONE $tableName TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n        Seq(\"t1\", \"t2\").foreach { t =>\n          validateOnlySpecifiedQoLTableFeaturesAndMetadataPresent(\n            tableName = t,\n            supportedTableFeatures = qolTableFeatures\n          )\n          validateCatalogOwnedCompleteEnablement(tableName = t, expectEnabled = true)\n          sql(s\"INSERT INTO $t VALUES (3), (4), (5), (6)\")\n          checkAnswer(sql(s\"SELECT * FROM $t\"),\n            Seq(Row(1), Row(2), Row(3), Row(4), Row(5), Row(6)))\n        }\n      }\n    }\n  }\n\n  test(\"Quality of life table features and corresponding metadata should be \" +\n      \"automatically enabled for CatalogOwned table\") {\n    withRandomTable(createCatalogOwnedTableAtInit = true) { tableName =>\n      // [[RowTrackingFeature]], [[DeletionVectorsTableFeature]],\n      // and [[V2CheckpointTableFeature]] should be enabled by default.\n      validateQoLFeaturesEnablement(tableName, expected = true)\n    }\n\n    // QoL features should also be enabled w/ other additional features enablement\n    // during CatalogOwned creation.\n    withTable(\"t1\") {\n      // Enable [[ClusteringTableFeature]] and [[ChangeDataFeedTableFeature]] during CO creation.\n      createTable(\"t1\", clusterBy = Seq(\"id\"), properties = Map(\n        s\"delta.feature.${CatalogOwnedTableFeature.name}\" -> \"supported\",\n        DeltaConfigs.CHANGE_DATA_FEED.key -> \"true\"))\n      sql(\"INSERT INTO t1 VALUES (1), (2)\")\n      validateQoLFeaturesEnablement(tableName = \"t1\", expected = true)\n      val (_, snapshot) = getDeltaLogWithSnapshot(TableIdentifier(\"t1\"))\n      val readerAndWriterFeatureNames = snapshot.protocol.readerAndWriterFeatureNames\n      assert(readerAndWriterFeatureNames.contains(ChangeDataFeedTableFeature.name) &&\n        readerAndWriterFeatureNames.contains(ClusteringTableFeature.name))\n      validateCatalogOwnedCompleteEnablement(tableName = \"t1\", expectEnabled = true)\n    }\n  }\n\n  test(\"ALTER TABLE should be blocked if attempts to disable ICT\") {\n    withRandomTable(createCatalogOwnedTableAtInit = true) { tableName =>\n      val error = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        spark.sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES ('$ICT_ENABLED_KEY' = 'false')\")\n      }\n      checkError(\n        error,\n        \"DELTA_CANNOT_MODIFY_CATALOG_MANAGED_DEPENDENCIES\",\n        sqlState = \"42616\",\n        parameters = Map[String, String]())\n    }\n  }\n\n  test(\"ALTER TABLE should be blocked if attempts to unset ICT\") {\n    withRandomTable(createCatalogOwnedTableAtInit = true) { tableName =>\n      val error = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        spark.sql(s\"ALTER TABLE $tableName UNSET TBLPROPERTIES ('$ICT_ENABLED_KEY')\")\n      }\n      checkError(\n        error,\n        \"DELTA_CANNOT_MODIFY_CATALOG_MANAGED_DEPENDENCIES\",\n        sqlState = \"42616\",\n        parameters = Map[String, String]())\n    }\n  }\n\n  test(\"ALTER TABLE should be blocked if attempts to downgrade Catalog-Owned\") {\n    withRandomTable(createCatalogOwnedTableAtInit = true)  { tableName =>\n      val error = intercept[DeltaTableFeatureException] {\n        spark.sql(s\"ALTER TABLE $tableName DROP FEATURE '${CatalogOwnedTableFeature.name}'\")\n      }\n      checkError(\n        error,\n        \"DELTA_FEATURE_DROP_UNSUPPORTED_CLIENT_FEATURE\",\n        sqlState = \"0AKDC\",\n        parameters = Map(\"feature\" -> CatalogOwnedTableFeature.name))\n    }\n  }\n\n  test(\"Upgrade should be blocked since it is not supported yet\") {\n    withRandomTable(\n      // Do not enable Catalog-Owned at the beginning when creating table\n      createCatalogOwnedTableAtInit = false\n    ) { tableName =>\n      val error = intercept[NotImplementedException] {\n        spark.sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      }\n      assert(error.getMessage.contains(\"Upgrading to CatalogOwned table is not yet supported.\"))\n    }\n  }\n\n  test(\"Dropping CatalogOwned dependent features should be blocked\") {\n    withRandomTable(createCatalogOwnedTableAtInit = true) { tableName =>\n      val error1 = intercept[DeltaTableFeatureException] {\n        spark.sql(s\"ALTER TABLE $tableName DROP FEATURE '${InCommitTimestampTableFeature.name}'\")\n      }\n      checkError(\n        error1,\n        \"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\",\n        sqlState = \"55000\",\n        parameters = Map(\n          \"feature\" -> InCommitTimestampTableFeature.name,\n          \"dependentFeatures\" -> CatalogOwnedTableFeature.name))\n\n      val error2 = intercept[DeltaTableFeatureException] {\n        spark.sql(s\"ALTER TABLE $tableName DROP FEATURE '${VacuumProtocolCheckTableFeature.name}'\")\n      }\n      checkError(\n        error2,\n        \"DELTA_FEATURE_DROP_DEPENDENT_FEATURE\",\n        sqlState = \"55000\",\n        parameters = Map(\n          \"feature\" -> VacuumProtocolCheckTableFeature.name,\n          \"dependentFeatures\" -> CatalogOwnedTableFeature.name))\n    }\n  }\n\n  test(\"CO_COMMIT should be recorded in usage_log for normal CO commit\") {\n    withRandomTable(createCatalogOwnedTableAtInit = true) { tableName =>\n      val usageLog = Log4jUsageLogger.track {\n        sql(s\"INSERT INTO $tableName VALUES 3\")\n      }\n      val commitStatsUsageLog = filterUsageRecords(usageLog, \"delta.commit.stats\")\n      val commitStats = JsonUtils.fromJson[CommitStats](commitStatsUsageLog.head.blob)\n      assert(commitStats.coordinatedCommitsInfo ===\n        CoordinatedCommitsStats(\n          coordinatedCommitsType = CoordinatedCommitType.CO_COMMIT.toString,\n          commitCoordinatorName = commitCoordinatorName,\n          commitCoordinatorConf = Map.empty))\n    }\n  }\n\n  test(\"FS_TO_CO_UPGRADE_COMMIT should be recorded in usage_log when creating \" +\n       \"CatalogOwned table\") {\n    withTable(\"t1\") {\n      val usageLog = Log4jUsageLogger.track {\n        createTable(\"t1\", properties = Map(\n          s\"delta.feature.${CatalogOwnedTableFeature.name}\" -> \"supported\"))\n      }\n      val commitStatsUsageLog = filterUsageRecords(usageLog, \"delta.commit.stats\")\n      val commitStats = JsonUtils.fromJson[CommitStats](commitStatsUsageLog.head.blob)\n      assert(commitStats.coordinatedCommitsInfo ===\n        CoordinatedCommitsStats(\n          coordinatedCommitsType = CoordinatedCommitType.FS_TO_CO_UPGRADE_COMMIT.toString,\n          // catalogTable is not available for FS_TO_CO_UPGRADE_COMMIT\n          commitCoordinatorName = \"CATALOG_MISSING\",\n          commitCoordinatorConf = Map.empty))\n    }\n  }\n\n  test(\"Testing usage log for commit coordinator population for invalid path-based access\") {\n    // Clear all potential CC builders so that we are not entering the UT-only path when\n    // populating the commit coordinator.\n    clearBuilders()\n    withTempDir { tempDir =>\n      // Create a path-based table so that we can simulate the scenario\n      // where catalog table is not available.\n      createTable(s\"delta.`$tempDir`\", properties = Map(\n        s\"delta.feature.${CatalogOwnedTableFeature.name}\" -> \"supported\"))\n      validateInvalidPathBasedAccessUsageLog(\n        tempDir,\n        expectedVersion = \"0\",\n        // Only check for certain fields of `checksumOpt` since the entire map\n        // is too large to validate.\n        expectedChecksumOpt = Map(\n          \"tableSizeBytes\" -> 0,\n          \"numFiles\" -> 0,\n          \"numMetadata\" -> 1,\n          \"allFiles\" -> List.empty\n        )\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CatalogOwnedPropertySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.UUID\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\n// scalastyle:on import.ordering.noEmptyLine\n\nclass CatalogOwnedPropertySuite extends QueryTest\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils\n  with CatalogOwnedTestBaseSuite {\n\n  // Register the mock commit coordinator builder for testing, but don't enable\n  // CatalogOwned by default (tests explicitly enable it when needed).\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(3)\n  override def catalogOwnedDefaultCreationEnabledInTests: Boolean = false\n\n  /**\n   * Validate if the table is catalog owned or not by checking the following:\n   * 1) [[Snapshot.isCatalogOwned]] === expected\n   * 2) [[Metadata.configuration]] contains [[UCCommitCoordinatorClient.UC_TABLE_ID_KEY]]\n   * 3) [[Protocol.readerAndWriterFeatureNames]] contains [[CatalogOwnedTableFeature.name]]\n   *    and its dependent features.\n   *\n   * @param tableName The name of the table to validate.\n   * @param expected The expected value of whether the table is catalog owned or not.\n   */\n  private def validateCatalogOwnedAndUCTableId(tableName: String, expected: Boolean): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(tableName))\n    assert(snapshot.isCatalogOwned === expected)\n    assert(snapshot.metadata.configuration.contains(UCCommitCoordinatorClient.UC_TABLE_ID_KEY)\n      === expected)\n    // CatalogOwned enabled table should have the protocol version of (3, 7),\n    // since the table can't have a [[ReaderWriterTableFeature]] w/o this version.\n    if (expected) {\n      // Only verify protocol version for expected CatalogOwned table.\n      validateProtocolMinReaderWriterVersion(\n        tableName,\n        expectedMinReaderVersion = 3,\n        expectedMinWriterVersion = 7)\n      // Check dependent features as well.\n      CatalogOwnedTableFeature.requiredFeatures.foreach { feature =>\n        assert(snapshot.protocol.readerAndWriterFeatureNames.contains(feature.name),\n          s\"Table $tableName should have ${feature.name} in protocol.\")\n      }\n    }\n  }\n\n  private def createTableAndValidateCatalogOwned(\n      tableName: String,\n      withCatalogOwned: Boolean): Unit = {\n    val createTableSQLStr = if (withCatalogOwned) {\n      s\"CREATE TABLE $tableName (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\"\n    } else {\n      s\"CREATE TABLE $tableName (id LONG) USING delta\"\n    }\n    sql(createTableSQLStr)\n    // Manually insert UC_TABLE_ID to mock UC integration behavior.\n    if (withCatalogOwned) {\n      val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))\n      val deltaLog = DeltaLog.forTable(spark, catalogTable)\n      val snapshot = deltaLog.update(catalogTableOpt = Some(catalogTable))\n      val newMetadata = snapshot.metadata.copy(\n        configuration = snapshot.metadata.configuration +\n          (UCCommitCoordinatorClient.UC_TABLE_ID_KEY -> java.util.UUID.randomUUID().toString)\n      )\n      deltaLog.startTransaction(Some(catalogTable))\n        .commit(Seq(newMetadata), DeltaOperations.ManualUpdate)\n    }\n    validateCatalogOwnedAndUCTableId(tableName, expected = withCatalogOwned)\n  }\n\n  // Helper to manually insert UC_TABLE_ID to mock UC integration behavior.\n  // This is needed in OSS tests since there's no real UC integration.\n  private def mockUCTableIdInsertion(tableName: String): Unit = {\n    val catalogTable = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))\n    val deltaLog = DeltaLog.forTable(spark, catalogTable)\n    val snapshot = deltaLog.update(catalogTableOpt = Some(catalogTable))\n    val newMetadata = snapshot.metadata.copy(\n      configuration = snapshot.metadata.configuration +\n        (UCCommitCoordinatorClient.UC_TABLE_ID_KEY -> java.util.UUID.randomUUID().toString)\n    )\n    deltaLog.startTransaction(Some(catalogTable))\n      .commit(Seq(newMetadata), DeltaOperations.ManualUpdate)\n  }\n\n  private def getUCTableIdFromTable(tableName: String): String = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(tableName))\n    val ucTableId = snapshot.metadata.configuration.get(UCCommitCoordinatorClient.UC_TABLE_ID_KEY)\n    assert(ucTableId.isDefined,\n      s\"Table $tableName should have `ucTableId` in metadata.\")\n    ucTableId.get\n  }\n\n  private def getSnapshotVersion(tableName: String): Long = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(tableName))\n    snapshot.version\n  }\n\n  private def validateInCommitTimestampTableFeature(tableName: String, expected: Boolean): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(tableName))\n    val writerFeatureNames = snapshot.protocol.writerFeatureNames\n    assert(writerFeatureNames.contains(InCommitTimestampTableFeature.name) === expected)\n  }\n\n  private def validateInCommitTimestampTableProperties(\n      tableName: String,\n      expectedEnabled: Boolean,\n      expectedEnablementInfo: Boolean): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(tableName))\n    val conf = snapshot.metadata.configuration\n    assert(conf.contains(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key) === expectedEnabled)\n    // In certain cases, we can't verify all three ICT table properties here.\n    // This is because we only need to persist the ICT enablement info\n    // if there are non-ICT commits in the Delta log.\n    // I.e., [[DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_VERSION]] and\n    //       [[DeltaConfigs.IN_COMMIT_TIMESTAMP_ENABLEMENT_TIMESTAMP]].\n    // E.g., If the 0th commit enables ICT, then we will not need the above\n    //       two properties for the table in subsequent commits.\n    // See [[InCommitTimestampUtils.getUpdatedMetadataWithICTEnablementInfo]] for details.\n    CatalogOwnedTableUtils.ICT_TABLE_PROPERTY_KEYS.filter { k =>\n      k != DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key\n    }.foreach { key =>\n      assert(conf.contains(key) === expectedEnablementInfo)\n    }\n  }\n\n  private def validateInCommitTimestampPresent(\n      tableName: String,\n      expected: Boolean,\n      expectedEnablementInfo: Boolean): Unit = {\n    // Validate ICT table feature is present in [[Protocol]] first.\n    validateInCommitTimestampTableFeature(tableName, expected)\n    // Then validate ICT table properties are present in [[Metadata.configuration]].\n    validateInCommitTimestampTableProperties(tableName, expected, expectedEnablementInfo)\n  }\n\n  private def validateProtocolMinReaderWriterVersion(\n      tableName: String,\n      expectedMinReaderVersion: Int,\n      expectedMinWriterVersion: Int): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(tableName))\n    assert(snapshot.protocol.minReaderVersion === expectedMinReaderVersion)\n    assert(snapshot.protocol.minWriterVersion === expectedMinWriterVersion)\n  }\n\n  test(\"[REPLACE] table UUID & table feature should retain for target \" +\n      \"catalog-owned table during REPLACE TABLE\") {\n    withTable(\"t1\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableId1 = getUCTableIdFromTable(tableName = \"t1\")\n\n      // Target table should remain a catalog-owned table with same `ucTableId`\n      sql(\"REPLACE TABLE t1 (id LONG) USING delta\")\n      val ucTableId2 = getUCTableIdFromTable(tableName = \"t1\")\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n\n      // [[UCCommitCoordinatorClient.UC_TABLE_ID_KEY]] should be the same before and after REPLACE\n      assert(ucTableId1 === ucTableId2)\n    }\n  }\n\n  test(\"[REPLACE] Specifying table UUID for target table should be blocked \" +\n      \"during REPLACE TABLE\") {\n    withTable(\"t1\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n\n      val error = intercept[DeltaUnsupportedOperationException] {\n        sql(\"REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n          s\"('${UCCommitCoordinatorClient.UC_TABLE_ID_KEY}' = '${UUID.randomUUID().toString}')\")\n      }\n      checkError(error, \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\", \"42939\",\n        Map(\"prop\" -> \"io.unitycatalog.tableId\"))\n    }\n  }\n\n  test(\"[RTAS] Specifying table UUID when creating catalog-owned \" +\n      \" table via RTAS should be blocked\") {\n    withTable(\"t1\", \"t2\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = false)\n      sql(\"INSERT INTO t1 VALUES (1)\")\n      sql(\"INSERT INTO t1 VALUES (2)\")\n\n      createTableAndValidateCatalogOwned(tableName = \"t2\", withCatalogOwned = true)\n      val ucTableIdOriginal = getUCTableIdFromTable(tableName = \"t2\")\n\n      val error = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"REPLACE TABLE t2 USING delta TBLPROPERTIES \" +\n          s\"('${UCCommitCoordinatorClient.UC_TABLE_ID_KEY}' = '${UUID.randomUUID().toString}') \" +\n          s\"AS SELECT * FROM t1\")\n      }\n      checkError(error, \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\", \"42939\",\n        Map(\"prop\" -> \"io.unitycatalog.tableId\"))\n\n      // Normal RTAS should not be blocked\n      sql(s\"REPLACE TABLE t2 USING delta AS SELECT * FROM t1\")\n      validateCatalogOwnedAndUCTableId(tableName = \"t2\", expected = true)\n      val ucTableIdAfterRTAS = getUCTableIdFromTable(tableName = \"t2\")\n      assert(ucTableIdOriginal === ucTableIdAfterRTAS)\n      checkAnswer(sql(\"SELECT * FROM t2\"), Seq(Row(1), Row(2)))\n    }\n  }\n\n  test(\"[REPLACE] Specifying CatalogManaged for non-CatalogManaged table should \" +\n      \"be blocked during REPLACE TABLE\") {\n    withTable(\"t1\") {\n      // Normal delta table.\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = false)\n\n      val error = intercept[IllegalStateException] {\n        sql(\"REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      }\n      assert(error.getMessage.contains(\n        \"Specifying CatalogManaged in REPLACE TABLE command is not supported\"))\n    }\n  }\n\n  test(\"[REPLACE] Specifying CatalogManaged for existing CatalogManaged table should \" +\n      \"succeed as a no-op during REPLACE TABLE\") {\n    withTable(\"t1\") {\n      // CatalogManaged enabled table.\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      // Specifying CatalogManaged on an already CatalogManaged table should succeed.\n      // The CatalogManaged properties are treated as a no-op.\n      sql(\"REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n\n      // Validate the table is still CatalogManaged with the same ucTableId.\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdAfter = getUCTableIdFromTable(tableName = \"t1\")\n      assert(ucTableIdBefore === ucTableIdAfter)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore + 1)\n    }\n  }\n\n  test(\"[RTAS] Specifying CatalogManaged for non-CatalogManaged table should \" +\n      \"be blocked during RTAS\") {\n    withTable(\"t1\", \"t2\") {\n      // Normal delta table.\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = false)\n\n      // Source RTAS table.\n      createTableAndValidateCatalogOwned(tableName = \"t2\", withCatalogOwned = false)\n      sql(\"INSERT INTO t2 VALUES (1), (2)\")\n\n      val error = intercept[IllegalStateException] {\n        sql(\"REPLACE TABLE t1 USING delta TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported') \" +\n          s\"AS SELECT * FROM t2\")\n      }\n      assert(error.getMessage.contains(\n        \"Specifying CatalogManaged in REPLACE TABLE command is not supported\"))\n    }\n  }\n\n  test(\"[RTAS] Specifying CatalogManaged for existing CatalogManaged table should \" +\n      \"succeed as a no-op during RTAS\") {\n    withTable(\"t1\", \"t2\") {\n      // CatalogManaged enabled table.\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      // Source RTAS table.\n      createTableAndValidateCatalogOwned(tableName = \"t2\", withCatalogOwned = false)\n      sql(\"INSERT INTO t2 VALUES (1), (2)\")\n\n      // Specifying CatalogManaged on an already CatalogManaged table should succeed.\n      sql(\"REPLACE TABLE t1 USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported') \" +\n        s\"AS SELECT * FROM t2\")\n\n      // Validate the table is still CatalogManaged with the same ucTableId.\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdAfter = getUCTableIdFromTable(tableName = \"t1\")\n      assert(ucTableIdBefore === ucTableIdAfter)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore + 1)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1), Row(2)))\n    }\n  }\n\n  test(\"[RTAS] failed replace preserves existing catalog-managed table data and version\") {\n    withTable(\"t1\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (1), (2)\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      intercept[Exception] {\n        sql(\n          \"\"\"REPLACE TABLE t1 USING delta AS\n            |SELECT IF(id = 2L, CAST(raise_error('boom') AS BIGINT), id) AS id\n            |FROM VALUES (1L), (2L) AS src(id)\n            |\"\"\".stripMargin)\n      }\n\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      assert(getUCTableIdFromTable(tableName = \"t1\") === ucTableIdBefore)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1), Row(2)))\n    }\n  }\n\n  test(\"[RTAS] replacing an existing catalog-managed table preserves UC identity\") {\n    withTable(\"t1\", \"t2\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (10)\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      createTableAndValidateCatalogOwned(tableName = \"t2\", withCatalogOwned = false)\n      sql(\"INSERT INTO t2 VALUES (1), (2)\")\n\n      sql(\"REPLACE TABLE t1 USING delta AS SELECT * FROM t2\")\n\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      assert(getUCTableIdFromTable(tableName = \"t1\") === ucTableIdBefore)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore + 1)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1), Row(2)))\n    }\n  }\n\n  test(\"[CREATE OR REPLACE] with CatalogManaged on non-existing table should succeed\") {\n    withTable(\"t1\") {\n      // CREATE OR REPLACE on non-existing table with CatalogManaged should create a CC table.\n      sql(\"CREATE OR REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      // Mock UC integration behavior by inserting UC_TABLE_ID.\n      mockUCTableIdInsertion(\"t1\")\n\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      sql(\"INSERT INTO t1 VALUES (1), (2)\")\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1), Row(2)))\n    }\n  }\n\n  test(\"[CREATE OR REPLACE] with CatalogManaged on existing CatalogManaged table \" +\n      \"should succeed as a no-op\") {\n    withTable(\"t1\") {\n      // Create a CatalogManaged table first.\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (1)\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      // CREATE OR REPLACE with CatalogManaged on existing CatalogManaged table should succeed.\n      sql(\"CREATE OR REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n\n      // Validate the table is still CatalogManaged with the same ucTableId.\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdAfter = getUCTableIdFromTable(tableName = \"t1\")\n      assert(ucTableIdBefore === ucTableIdAfter)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore + 1)\n    }\n  }\n\n  test(\"[CREATE OR REPLACE] replacing an existing catalog-managed table is atomic\") {\n    withTable(\"t1\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (1)\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      sql(\"CREATE OR REPLACE TABLE t1 USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported') AS \" +\n        \"SELECT 2 AS id\")\n\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      assert(getUCTableIdFromTable(tableName = \"t1\") === ucTableIdBefore)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore + 1)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(2)))\n    }\n  }\n\n  test(\"[REPLACE] replacing an existing catalog-managed table preserves UC identity\") {\n    withTable(\"t1\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (1)\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      sql(\"REPLACE TABLE t1 (id LONG) USING delta\")\n\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      assert(getUCTableIdFromTable(tableName = \"t1\") === ucTableIdBefore)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore + 1)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq.empty)\n    }\n  }\n\n  test(\"[CREATE OR REPLACE] specifying table UUID for existing catalog-managed table \" +\n      \"should be blocked\") {\n    withTable(\"t1\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBefore = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (1)\")\n      val versionBefore = getSnapshotVersion(tableName = \"t1\")\n\n      val error = intercept[DeltaUnsupportedOperationException] {\n        sql(\"CREATE OR REPLACE TABLE t1 USING delta TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported', \" +\n          s\"'${UCCommitCoordinatorClient.UC_TABLE_ID_KEY}' = '${UUID.randomUUID().toString}') \" +\n          \"AS SELECT 2 AS id\")\n      }\n      checkError(error, \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\", \"42939\",\n        Map(\"prop\" -> \"io.unitycatalog.tableId\"))\n\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      assert(getUCTableIdFromTable(tableName = \"t1\") === ucTableIdBefore)\n      assert(getSnapshotVersion(tableName = \"t1\") === versionBefore)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1)))\n    }\n  }\n\n  test(\"[CREATE OR REPLACE] with CatalogManaged on existing non-CatalogManaged table \" +\n      \"should be blocked\") {\n    withTable(\"t1\") {\n      // Create a non-CatalogManaged table first.\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = false)\n      sql(\"INSERT INTO t1 VALUES (1)\")\n\n      // CREATE OR REPLACE with CatalogManaged on existing non-CatalogManaged table should fail.\n      val error = intercept[IllegalStateException] {\n        sql(\"CREATE OR REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      }\n      assert(error.getMessage.contains(\n        \"Specifying CatalogManaged in REPLACE TABLE command is not supported\"))\n\n      // Original table should remain unchanged.\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = false)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1)))\n    }\n  }\n\n  test(\"[CREATE OR REPLACE] repeated same-schema CREATE OR REPLACE with CatalogManaged \" +\n      \"should succeed repeatedly\") {\n    withTable(\"t1\") {\n      // First CREATE OR REPLACE creates the table.\n      sql(\"CREATE OR REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      // Mock UC integration behavior by inserting UC_TABLE_ID.\n      mockUCTableIdInsertion(\"t1\")\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdFirst = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"INSERT INTO t1 VALUES (1)\")\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1)))\n\n      // Second CREATE OR REPLACE should succeed as a no-op for CC properties.\n      sql(\"CREATE OR REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdSecond = getUCTableIdFromTable(tableName = \"t1\")\n      assert(ucTableIdFirst === ucTableIdSecond)\n\n      // Third CREATE OR REPLACE should also succeed when the metadata is unchanged.\n      sql(\"CREATE OR REPLACE TABLE t1 (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdThird = getUCTableIdFromTable(tableName = \"t1\")\n      assert(ucTableIdFirst === ucTableIdThird)\n\n      // Verify table is functional.\n      sql(\"INSERT INTO t1 VALUES (2)\")\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(2)))\n    }\n  }\n  test(\"[CREATE LIKE] Specifying table UUID should be blocked\") {\n    withTable(\"t1\", \"t2\", \"t3\") {\n      // Source catalog-owned table\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n\n      val error1 = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"CREATE TABLE t2 LIKE t1 TBLPROPERTIES \" +\n          s\"('${UCCommitCoordinatorClient.UC_TABLE_ID_KEY}' = '${UUID.randomUUID().toString}')\")\n      }\n      val error2 = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"CREATE TABLE t3 LIKE t1 TBLPROPERTIES \" +\n          s\"('${UCCommitCoordinatorClient.UC_TABLE_ID_KEY}' = '${UUID.randomUUID().toString}')\")\n      }\n      Seq(error1, error2).foreach { error =>\n        checkError(error, \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\", \"42939\",\n          Map(\"prop\" -> \"io.unitycatalog.tableId\"))\n      }\n    }\n  }\n\n  test(\"[REPLACE] Protocol should not be downgraded when adding new table feature \" +\n      \"to existing CatalogOwned enabled table\") {\n    withTable(\"t1\", \"t2\") {\n      createTableAndValidateCatalogOwned(tableName = \"t1\", withCatalogOwned = true)\n      val ucTableIdBeforeReplace = getUCTableIdFromTable(tableName = \"t1\")\n      sql(\"CREATE TABLE t2 (col1 LONG) USING delta\")\n      sql(\"INSERT INTO t2 VALUES (1)\")\n      sql(\"INSERT INTO t2 VALUES (2)\")\n      // Adding [[ClusteringTableFeature]] when replacing `t1`\n      sql(\"REPLACE TABLE t1 USING delta CLUSTER BY (col1) \" +\n        \"TBLPROPERTIES ('delta.dataSkippingNumIndexedCols' = '1') \" +\n        \"AS SELECT * FROM t2\")\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = true)\n      val ucTableIdAfterReplace = getUCTableIdFromTable(tableName = \"t1\")\n      assert(ucTableIdBeforeReplace === ucTableIdAfterReplace)\n      checkAnswer(sql(\"SELECT * FROM t1\"), Seq(Row(1), Row(2)))\n      val log = DeltaLog.forTable(spark, new TableIdentifier(\"t1\"))\n      val readerWriterFeatureNames =\n        log.unsafeVolatileSnapshot.protocol.readerAndWriterFeatureNames\n      assert(readerWriterFeatureNames.contains(ClusteringTableFeature.name) &&\n        readerWriterFeatureNames.contains(CatalogOwnedTableFeature.name))\n    }\n  }\n\n  test(\"[REPLACE] ICT should not be present after replacing existing normal delta \" +\n      \"table w/o ICT w/ default spark configuration\") {\n    withSQLConf(\n        TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature) -> \"supported\") {\n      withTable(\"t1\", \"t2\") {\n        withoutDefaultCCTableFeature {\n          sql(\"CREATE TABLE t1 (id LONG) USING delta\")\n          sql(\"INSERT INTO t1 VALUES (1), (2)\")\n          sql(\"CREATE TABLE t2 (id LONG) USING delta\")\n          validateInCommitTimestampPresent(\n            tableName = \"t2\",\n            expected = false,\n            expectedEnablementInfo = false)\n        }\n        sql(\"REPLACE TABLE t2 USING delta AS SELECT * FROM t1\")\n        checkAnswer(sql(\"SELECT * FROM t2\"), Seq(Row(1), Row(2)))\n        validateInCommitTimestampPresent(\n          tableName = \"t2\",\n          expected = false,\n          expectedEnablementInfo = false)\n        validateCatalogOwnedAndUCTableId(tableName = \"t2\", expected = false)\n        sql(\"INSERT INTO t2 VALUES (3), (4)\")\n        checkAnswer(sql(\"SELECT * FROM t2\"), Seq(Row(1), Row(2), Row(3), Row(4)))\n      }\n    }\n  }\n\n  test(\"[REPLACE] ICT-related properties should not be present after replacing existing \" +\n      \"normal delta table w/ ICT w/ default spark configuration\") {\n    withSQLConf(\n        TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature) -> \"supported\") {\n      withTable(\"t1\", \"t2\", \"t3\") {\n        withoutDefaultCCTableFeature {\n          sql(\"CREATE TABLE t1 (id LONG) USING delta\")\n          sql(\"INSERT INTO t1 VALUES (1), (2)\")\n          sql(\"CREATE TABLE t2 (id LONG) USING delta TBLPROPERTIES \" +\n            s\"('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n          validateInCommitTimestampPresent(\n            tableName = \"t2\",\n            expected = true,\n            expectedEnablementInfo = false)\n          sql(\"CREATE TABLE t3 (id LONG) USING delta\")\n          sql(\"INSERT INTO t3 VALUES (1), (2)\")\n          sql(\"ALTER TABLE t3 SET TBLPROPERTIES \" +\n            s\"('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\")\n          validateInCommitTimestampPresent(\n            tableName = \"t3\",\n            expected = true,\n            expectedEnablementInfo = true)\n        }\n        sql(\"REPLACE TABLE t2 USING delta AS SELECT * FROM t1\")\n        sql(\"REPLACE TABLE t3 USING delta AS SELECT * FROM t1\")\n        sql(\"INSERT INTO t2 VALUES (3)\")\n        sql(\"INSERT INTO t3 VALUES (3), (4)\")\n        checkAnswer(sql(\"SELECT * FROM t2\"), Seq(Row(1), Row(2), Row(3)))\n        checkAnswer(sql(\"SELECT * FROM t3\"), Seq(Row(1), Row(2), Row(3), Row(4)))\n        Seq(\"t2\", \"t3\").foreach { tableName =>\n          // ICT-related properties should *NOT* be present after replacing existing normal\n          // delta table w/o explicit overrides or default spark configuration.\n          validateInCommitTimestampTableProperties(\n            tableName = tableName,\n            expectedEnabled = false,\n            // All three ICT properties should not be present,\n            // though currently there is only one for `t2`.\n            expectedEnablementInfo = false)\n          // ICT table feature will be preserved after REPLACE.\n          validateInCommitTimestampTableFeature(tableName = tableName, expected = true)\n          validateCatalogOwnedAndUCTableId(tableName = tableName, expected = false)\n        }\n        sql(\"INSERT INTO t2 VALUES (4), (5)\")\n        sql(\"INSERT INTO t3 VALUES (5)\")\n        checkAnswer(sql(\"SELECT * FROM t2\"), Seq(Row(1), Row(2), Row(3), Row(4), Row(5)))\n        checkAnswer(sql(\"SELECT * FROM t3\"), Seq(Row(1), Row(2), Row(3), Row(4), Row(5)))\n      }\n    }\n  }\n\n  test(\"[REPLACE] ICT should be present after replacing existing normal delta \" +\n      \"table w/o ICT w/ default spark configuration w/ explicitly overrides\") {\n    withSQLConf(\n        TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature) -> \"supported\") {\n      withTable(\"t1\", \"t2\", \"t3\") {\n        withoutDefaultCCTableFeature {\n          sql(\"CREATE TABLE t1 (id LONG) USING delta\")\n          sql(\"INSERT INTO t1 VALUES (1), (2)\")\n          sql(\"CREATE TABLE t2 (id LONG) USING delta\")\n          sql(\"CREATE TABLE t3 (id LONG) USING delta\")\n          sql(\"INSERT INTO t3 VALUES (1), (2)\")\n          Seq(\"t2\", \"t3\").foreach { tableName =>\n            validateInCommitTimestampPresent(\n              tableName = tableName,\n              expected = false,\n              expectedEnablementInfo = false)\n          }\n        }\n        Seq(\"t2\", \"t3\").foreach { tableName =>\n          sql(s\"\"\"\n                 | REPLACE TABLE $tableName USING delta TBLPROPERTIES\n                 | ('${DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'true')\n                 | AS SELECT * FROM t1\n              \"\"\".stripMargin)\n          checkAnswer(sql(s\"SELECT * FROM $tableName\"), Seq(Row(1), Row(2)))\n          validateCatalogOwnedAndUCTableId(tableName = tableName, expected = false)\n          sql(s\"INSERT INTO $tableName VALUES (3), (4)\")\n          checkAnswer(sql(s\"SELECT * FROM $tableName\"), Seq(Row(1), Row(2), Row(3), Row(4)))\n          // ICT enablement info is present in both `t2` and `t3`.\n          validateInCommitTimestampPresent(\n            tableName = \"t2\",\n            expected = true,\n            expectedEnablementInfo = true)\n        }\n      }\n    }\n  }\n\n  test(\"[REPLACE] ICT should be present after replacing existing normal delta \" +\n     \"table w/ ICT enabled via default spark configuration\") {\n    withTable(\"t1\", \"t2\", \"t3\") {\n      sql(\"CREATE TABLE t1 (id LONG) USING delta\")\n      sql(\"INSERT INTO t1 VALUES (1), (2)\")\n\n      sql(\"CREATE TABLE t2 (id LONG) USING delta\")\n      sql(\"INSERT INTO t2 VALUES (1), (2)\")\n      sql(\"CREATE TABLE t3 (id LONG) USING delta\")\n      Seq(\"t2\", \"t3\").foreach { tableName =>\n        validateInCommitTimestampPresent(\n          tableName = tableName,\n          expected = false,\n          expectedEnablementInfo = false)\n        validateCatalogOwnedAndUCTableId(tableName, expected = false)\n      }\n\n      // w/ default CO enabled\n      withSQLConf(\n          TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature) -> \"supported\",\n          DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> \"true\") {\n        sql(\"REPLACE TABLE t2 USING delta AS SELECT * FROM t1\")\n        validateInCommitTimestampPresent(\n          tableName = \"t2\",\n          expected = true,\n          expectedEnablementInfo = true)\n        validateCatalogOwnedAndUCTableId(tableName = \"t2\", expected = false)\n      }\n\n      // w/o default CO enabled\n      withSQLConf(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> \"true\") {\n        sql(\"REPLACE TABLE t3 USING delta AS SELECT * FROM t1\")\n        validateInCommitTimestampPresent(\n          tableName = \"t3\",\n          expected = true,\n          expectedEnablementInfo = true)\n        validateCatalogOwnedAndUCTableId(tableName = \"t3\", expected = false)\n      }\n\n      Seq(\"t1\", \"t2\", \"t3\").foreach { tableName =>\n        sql(s\"INSERT INTO $tableName VALUES (3), (4)\")\n        checkAnswer(sql(s\"SELECT * FROM $tableName\"), Seq(Row(1), Row(2), Row(3), Row(4)))\n      }\n    }\n  }\n\n  test(\"Table protocol should be kept intact before and after REPLACE \" +\n      \"regardless of default CC enablement\") {\n    def getReaderAndWriterFeatureNamesFromTable(tableName: String): Set[String] = {\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      snapshot.protocol.readerAndWriterFeatureNames\n    }\n    withTable(\"t1\", \"t2\", \"source\") {\n      // Source table.\n      sql(\"CREATE TABLE source (id LONG) USING delta\")\n      sql(\"INSERT INTO source VALUES (3), (4), (5)\")\n\n      // Create a normal delta table w/ protocol (1, 2).\n      sql(\"CREATE TABLE t1 (id LONG) USING delta\")\n      val featuresBeforeReplaceT1 = getReaderAndWriterFeatureNamesFromTable(tableName = \"t1\")\n      validateProtocolMinReaderWriterVersion(\n        tableName = \"t1\",\n        expectedMinReaderVersion = 1,\n        expectedMinWriterVersion = 2)\n      validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = false)\n      CatalogOwnedTableFeature.requiredFeatures.foreach { feature =>\n        assert(!featuresBeforeReplaceT1.contains(feature.name))\n      }\n\n      // Create a CC table w/ protocol (3, 7).\n      createTableAndValidateCatalogOwned(tableName = \"t2\", withCatalogOwned = true)\n      val featuresBeforeReplaceT2 = getReaderAndWriterFeatureNamesFromTable(tableName = \"t2\")\n\n      // Insert initial data.\n      Seq(\"t1\", \"t2\").foreach { tableName =>\n        sql(s\"INSERT INTO $tableName VALUES (1), (2)\")\n      }\n\n      // Enable CC by default\n      withDefaultCCTableFeature {\n        Seq(\"t1\", \"t2\").foreach { tableName =>\n          sql(s\"REPLACE TABLE $tableName USING delta AS SELECT * FROM source\")\n          checkAnswer(sql(s\"SELECT * FROM $tableName\"), Seq(Row(3), Row(4), Row(5)))\n        }\n        // REPLACE TABLE should not change the protocol.\n        val featuresAfterReplaceT1 = getReaderAndWriterFeatureNamesFromTable(tableName = \"t1\")\n        validateProtocolMinReaderWriterVersion(\n          tableName = \"t1\",\n          expectedMinReaderVersion = 1,\n          expectedMinWriterVersion = 2)\n        assert(featuresBeforeReplaceT1 === featuresAfterReplaceT1)\n        validateCatalogOwnedAndUCTableId(tableName = \"t1\", expected = false)\n\n        val featuresAfterReplaceT2 = getReaderAndWriterFeatureNamesFromTable(tableName = \"t2\")\n        assert(featuresBeforeReplaceT2 === featuresAfterReplaceT2)\n        validateCatalogOwnedAndUCTableId(tableName = \"t2\", expected = true)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CommitCoordinatorClientImplSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.io.File\nimport java.util.concurrent.{Executors, TimeUnit}\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.{Action, CommitInfo, Metadata, Protocol}\nimport org.apache.spark.sql.delta.storage.{LogStore, LogStoreProvider}\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.delta.util.threads.DeltaThreadPool\nimport io.delta.dynamodbcommitcoordinator.DynamoDBCommitCoordinatorClient\nimport io.delta.storage.commit.{Commit => JCommit, CommitFailedException => JCommitFailedException, GetCommitsResponse => JGetCommitsResponse}\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.{ThreadUtils, Utils}\n\ntrait CommitCoordinatorClientImplSuiteBase extends QueryTest\n    with SharedSparkSession\n    with LogStoreProvider\n    with CoordinatedCommitsTestUtils\n    with DeltaSQLTestUtils\n    with DeltaSQLCommandTest {\n\n  /**\n   * Needs to be overwritten by implementing classes to provide a [[TableCommitCoordinatorClient]]\n   * wrapping the commit coordinator client that should be tested.\n   */\n  protected def createTableCommitCoordinatorClient(deltaLog: DeltaLog): TableCommitCoordinatorClient\n\n  /**\n   * Needs to be overwritten by implementing classes to provide an implementation\n   * of backfill registration.\n   */\n  protected def registerBackfillOp(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      deltaLog: DeltaLog,\n      version: Long): Unit\n\n  /**\n   * Needs to be overwritten by implementing classes to provide a way of validating\n   * that the commit coordinator client under test performs backfilling as expected at\n   * the specified version.\n   */\n  protected def validateBackfillStrategy(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      logPath: Path,\n      version: Long): Unit\n\n  /**\n   * Needs to be overwritten by implementing classes to provide a way of validating\n   * the results of a getCommits call with the specified start and end versions,\n   * where maxVersion is the current latest version of the table.\n   */\n  protected def validateGetCommitsResult(\n    response: JGetCommitsResponse,\n    startVersion: Option[Long],\n    endVersion: Option[Long],\n    maxVersion: Long): Unit\n\n  /**\n   * Checks that the commit coordinator state is correct in terms of\n   *  - The latest table version in the commit coordinator is correct\n   *  - All supposedly backfilled commits are indeed backfilled\n   *  - The contents of the backfilled commits are correct (verified\n   *     if commitTimestampOpt is provided)\n   *\n   * This can be overridden by implementing classes to implement\n   * more specific invariants.\n   */\n  protected def assertInvariants(\n       logPath: Path,\n       tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n       commitTimestampsOpt: Option[Array[Long]] = None): Unit = {\n    val maxUntrackedVersion: Int = {\n      val commitResponse = tableCommitCoordinatorClient.getCommits()\n      if (commitResponse.getCommits.isEmpty) {\n        commitResponse.getLatestTableVersion.toInt\n      } else {\n        assert(commitResponse.getCommits.asScala.last.getVersion ==\n            commitResponse.getLatestTableVersion,\n          s\"Max commit tracked by the commit coordinator \" +\n            s\"${commitResponse.getCommits.asScala.last} must \" +\n            s\"match latestTableVersion tracked by the commit coordinator \" +\n            s\"${commitResponse.getLatestTableVersion}.\"\n        )\n        val minVersion = commitResponse.getCommits.asScala.head.getVersion\n        assert(\n          commitResponse.getLatestTableVersion - minVersion + 1 == commitResponse.getCommits.size,\n          \"Commit map should have a contiguous range of unbackfilled commits.\"\n        )\n        minVersion.toInt - 1\n      }\n    }\n    (0 to maxUntrackedVersion).foreach { version =>\n      assertBackfilled(version, logPath, commitTimestampsOpt.map(_(version)))\n    }\n  }\n\n  protected def writeCommitZero(logPath: Path): Unit = {\n    val commitInfo = CommitInfo.empty(version = Some(0)).withTimestamp(0)\n      .copy(inCommitTimestamp = Some(0))\n    val actions = Iterator(commitInfo.json, Metadata().json, Protocol().json)\n    store.write(FileNames.unsafeDeltaFile(logPath, 0), actions, overwrite = false)\n  }\n\n  /**\n   * The metadata that should be passed to the registerTable call. By default, this\n   * is empty but implementing classes can overwrite this method to provide custom\n   * metadata.\n   */\n  protected def initMetadata(): Metadata = Metadata()\n\n  // scalastyle:off deltahadoopconfiguration\n  protected def sessionHadoopConf: Configuration = spark.sessionState.newHadoopConf()\n  // scalastyle:on deltahadoopconfiguration\n\n  protected def store: LogStore = createLogStore(spark)\n\n  protected def withTempTableDir(f: File => Unit): Unit = {\n    val dir = Utils.createTempDir()\n    val deltaLogDir = new File(dir, DeltaLog.LOG_DIR_NAME)\n    deltaLogDir.mkdir()\n    val commitLogDir = new File(deltaLogDir, FileNames.COMMIT_SUBDIR)\n    commitLogDir.mkdir()\n    try f(dir)\n    finally {\n      Utils.deleteRecursively(dir)\n    }\n  }\n\n  protected def commit(\n      version: Long,\n      timestamp: Long,\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      tableIdentifier: Option[TableIdentifier] = None): JCommit = {\n    val commitInfo = CommitInfo.empty(version = Some(version)).withTimestamp(timestamp)\n      .copy(inCommitTimestamp = Some(timestamp))\n    val updatedActions = if (version == 0) {\n      getUpdatedActionsForZerothCommit(commitInfo)\n    } else {\n      getUpdatedActionsForNonZerothCommit(commitInfo)\n    }\n    tableCommitCoordinatorClient.commit(\n      version,\n      Iterator(commitInfo.json),\n      updatedActions,\n      tableIdentifier).getCommit\n  }\n\n  protected def assertBackfilled(\n      version: Long,\n      logPath: Path,\n      timestampOpt: Option[Long] = None): Unit = {\n    val delta = FileNames.unsafeDeltaFile(logPath, version)\n    if (timestampOpt.isDefined) {\n      val commitInfo = CommitInfo.empty(version = Some(version))\n        .withTimestamp(timestampOpt.get)\n        .copy(inCommitTimestamp = timestampOpt)\n      assert(store.read(delta, sessionHadoopConf).head == commitInfo.json)\n    } else {\n      assert(Action.fromJson(store.read(delta, sessionHadoopConf).head)\n        .isInstanceOf[CommitInfo])\n    }\n  }\n\n  protected def assertCommitFail(\n      currentVersion: Long,\n      expectedVersion: Long,\n      retryable: Boolean,\n      commitFunc: => JCommit): Unit = {\n    val e = intercept[JCommitFailedException] {\n      commitFunc\n    }\n    assert(e.getRetryable == retryable)\n    assert(e.getConflict == retryable)\n    val expectedMessage = if (currentVersion == 0) {\n      \"Commit version 0 must go via filesystem.\"\n    } else {\n      s\"Commit version $currentVersion is not valid. Expected version: $expectedVersion.\"\n    }\n    assert(e.getMessage === expectedMessage)\n  }\n\n  protected def assertResponseEquals(\n      resp1: JGetCommitsResponse,\n      resp2: JGetCommitsResponse): Unit = {\n    assert(resp1.getLatestTableVersion == resp2.getLatestTableVersion)\n    assert(resp1.getCommits == resp2.getCommits)\n  }\n\n  test(\"test basic commit and backfill functionality\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n\n      val e = intercept[JCommitFailedException] {\n        commit(version = 0, timestamp = 0, tableCommitCoordinatorClient)\n      }\n      assert(e.getMessage === \"Commit version 0 must go via filesystem.\")\n      writeCommitZero(logPath)\n      assertResponseEquals(tableCommitCoordinatorClient.getCommits(),\n        new JGetCommitsResponse(Seq.empty.asJava, -1))\n      assertBackfilled(version = 0, logPath, Some(0L))\n\n      // Test backfilling functionality for commits 1 - 8\n      (1 to 8).foreach { version =>\n        commit(version, version, tableCommitCoordinatorClient)\n        validateBackfillStrategy(tableCommitCoordinatorClient, logPath, version)\n        assert(tableCommitCoordinatorClient.getCommits().getLatestTableVersion == version)\n      }\n\n      // Test that out-of-order backfill is rejected\n      intercept[IllegalArgumentException] {\n        registerBackfillOp(tableCommitCoordinatorClient, log, 10)\n      }\n      assertInvariants(logPath, tableCommitCoordinatorClient)\n    }\n  }\n\n  test(\"startVersion and endVersion are respected in getCommits\") {\n    def runGetCommitsAndValidate(\n        client: TableCommitCoordinatorClient,\n        startVersion: Option[Long],\n        endVersion: Option[Long],\n        maxVersion: Long): Unit = {\n      val result = client.getCommits(startVersion, endVersion)\n      validateGetCommitsResult(result, startVersion, endVersion, maxVersion)\n    }\n\n    withTempTableDir { tempDir =>\n      // prepare a table with 15 commits\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n      writeCommitZero(logPath)\n      val maxVersion = 15\n      (1 to maxVersion).foreach { version =>\n        commit(version, version, tableCommitCoordinatorClient)\n      }\n\n      runGetCommitsAndValidate(tableCommitCoordinatorClient, None, None, maxVersion)\n      runGetCommitsAndValidate(tableCommitCoordinatorClient, Some(9), None, maxVersion)\n      runGetCommitsAndValidate(tableCommitCoordinatorClient, Some(11), Some(14), maxVersion)\n      runGetCommitsAndValidate(tableCommitCoordinatorClient, Some(12), Some(12), maxVersion)\n      runGetCommitsAndValidate(tableCommitCoordinatorClient, None, Some(14), maxVersion)\n    }\n  }\n\n  test(\"test out-of-order backfills are rejected\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.getPath)\n      val logPath = log.logPath\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n      // commit-0 must be file system based\n      writeCommitZero(logPath)\n      (1 to 3).foreach(i => commit(i, i, tableCommitCoordinatorClient))\n\n      // Test that backfilling is idempotent for already-backfilled commits.\n      registerBackfillOp(tableCommitCoordinatorClient, log, 2)\n      registerBackfillOp(tableCommitCoordinatorClient, log, 2)\n\n      // Test that backfilling uncommited commits fail.\n      intercept[IllegalArgumentException] {\n        registerBackfillOp(tableCommitCoordinatorClient, log, 4)\n      }\n    }\n  }\n\n  test(\"test out-of-order commits are rejected\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n\n      // commit-0 must be file system based\n      writeCommitZero(logPath)\n      // Verify that conflict-checker rejects out-of-order commits.\n      (1 to 4).foreach(i => commit(i, i, tableCommitCoordinatorClient))\n      // A retry of commit 0 fails from commit coordinator client with a conflict and it can't be\n      // retried as commit 0 is upgrading the commit coordinator client.\n      assertCommitFail(0, 5, retryable = false, commit(0, 5, tableCommitCoordinatorClient))\n      assertCommitFail(4, 5, retryable = true, commit(4, 6, tableCommitCoordinatorClient))\n\n      commit(5, 5, tableCommitCoordinatorClient)\n      validateGetCommitsResult(tableCommitCoordinatorClient.getCommits(), None, None, 5)\n      assertCommitFail(5, 6, retryable = true, commit(5, 5, tableCommitCoordinatorClient))\n      assertCommitFail(7, 6, retryable = false, commit(7, 7, tableCommitCoordinatorClient))\n\n      assertInvariants(logPath, tableCommitCoordinatorClient)\n    }\n  }\n\n  test(\"should handle concurrent readers and writers\") {\n    withTempTableDir { tempDir =>\n      val tablePath = new Path(tempDir.getCanonicalPath)\n      val logPath = new Path(tablePath, DeltaLog.LOG_DIR_NAME)\n      val tcs = createTableCommitCoordinatorClient(DeltaLog.forTable(spark, tablePath))\n\n      val numberOfWriters = 11\n      val numberOfCommitsPerWriter = 11\n      // scalastyle:off sparkThreadPools\n      val executor = DeltaThreadPool(\"commitCoordinatorSuite\", numberOfWriters)\n      // scalastyle:on sparkThreadPools\n      val runningTimestamp = new AtomicInteger(0)\n      val commitFailedExceptions = new AtomicInteger(0)\n      // commit-0 must be file system based\n      writeCommitZero(logPath)\n\n      try {\n        val tasks = (0 until numberOfWriters).map { i =>\n          executor.submit(spark) {\n              var currentWriterCommits = 0\n              while (currentWriterCommits < numberOfCommitsPerWriter) {\n                val nextVersion = math.max(tcs.getCommits().getLatestTableVersion + 1, 1)\n                try {\n                  val currentTimestamp = runningTimestamp.getAndIncrement()\n                  val commitResponse = commit(nextVersion, currentTimestamp, tcs)\n                  currentWriterCommits += 1\n                  assert(commitResponse.getCommitTimestamp == currentTimestamp)\n                  assert(commitResponse.getVersion == nextVersion)\n                } catch {\n                  case e: JCommitFailedException =>\n                    assert(e.getConflict)\n                    assert(e.getRetryable)\n                    commitFailedExceptions.getAndIncrement()\n                } finally {\n                  assertInvariants(logPath, tcs)\n                }\n              }\n            }\n        }\n        tasks.foreach(ThreadUtils.awaitResult(_, 150.seconds))\n      } catch {\n        case e: InterruptedException =>\n          fail(\"Test interrupted: \" + e.getMessage)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CommitCoordinatorClientSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\nimport scala.reflect.runtime.universe._\n\nimport org.apache.spark.sql.delta.{CoordinatedCommitsTableFeature, DeltaConfigs, DeltaLog, DeltaOperations}\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{CommitCoordinatorClient, CommitResponse, GetCommitsResponse => JGetCommitsResponse, TableDescriptor, TableIdentifier, UpdatedActions}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.{QueryTest, SparkSession}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass CommitCoordinatorClientSuite extends QueryTest with DeltaSQLTestUtils with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  private trait TestCommitCoordinatorClientBase extends CommitCoordinatorClient {\n    override def commit(\n        logStore: LogStore,\n        hadoopConf: Configuration,\n        tableDesc: TableDescriptor,\n        commitVersion: Long,\n        actions: java.util.Iterator[String],\n        updatedActions: UpdatedActions): CommitResponse = {\n      throw new UnsupportedOperationException(\"Not implemented\")\n    }\n\n    override def getCommits(\n        tableDesc: TableDescriptor,\n        startVersion: java.lang.Long,\n        endVersion: java.lang.Long): JGetCommitsResponse =\n      new JGetCommitsResponse(Seq.empty.asJava, -1)\n\n    override def backfillToVersion(\n        logStore: LogStore,\n        hadoopConf: Configuration,\n        tableDesc: TableDescriptor,\n        version: Long,\n        lastKnownBackfilledVersion: java.lang.Long): Unit = {}\n\n    override def registerTable(\n        logPath: Path,\n        tableIdentifier: Optional[TableIdentifier],\n        currentVersion: Long,\n        currentMetadata: AbstractMetadata,\n        currentProtocol: AbstractProtocol): java.util.Map[String, String] =\n      Map.empty[String, String].asJava\n\n    override def semanticEquals(other: CommitCoordinatorClient): Boolean = this == other\n  }\n\n  private class TestCommitCoordinatorClient1 extends TestCommitCoordinatorClientBase\n  private class TestCommitCoordinatorClient2 extends TestCommitCoordinatorClientBase\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n    CommitCoordinatorProvider.registerBuilder(InMemoryCommitCoordinatorBuilder(batchSize = 1))\n  }\n\n  test(\"registering multiple commit-coordinator builders with same name\") {\n    object Builder1 extends CommitCoordinatorBuilder {\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = null\n      override def getName: String = \"builder-1\"\n    }\n    object BuilderWithSameName extends CommitCoordinatorBuilder {\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = null\n      override def getName: String = \"builder-1\"\n    }\n    object Builder3 extends CommitCoordinatorBuilder {\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = null\n      override def getName: String = \"builder-3\"\n    }\n    CommitCoordinatorProvider.registerBuilder(Builder1)\n    intercept[Exception] {\n      CommitCoordinatorProvider.registerBuilder(BuilderWithSameName)\n    }\n    CommitCoordinatorProvider.registerBuilder(Builder3)\n  }\n\n  test(\"getCommitCoordinator - builder returns same object\") {\n    object Builder1 extends CommitCoordinatorBuilder {\n      val cs1 = new TestCommitCoordinatorClient1()\n      val cs2 = new TestCommitCoordinatorClient2()\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n        conf.getOrElse(\"url\", \"\") match {\n          case \"url1\" => cs1\n          case \"url2\" => cs2\n          case _ => throw new IllegalArgumentException(\"Invalid url\")\n        }\n      }\n      override def getName: String = \"cs-x\"\n    }\n    CommitCoordinatorProvider.registerBuilder(Builder1)\n    val cs1 =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-x\", Map(\"url\" -> \"url1\"), spark)\n    assert(cs1.isInstanceOf[TestCommitCoordinatorClient1])\n    val cs1_again =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-x\", Map(\"url\" -> \"url1\"), spark)\n    assert(cs1 eq cs1_again)\n    val cs2 = CommitCoordinatorProvider\n      .getCommitCoordinatorClient(\"cs-x\", Map(\"url\" -> \"url2\", \"a\" -> \"b\"), spark)\n    assert(cs2.isInstanceOf[TestCommitCoordinatorClient2])\n    // If builder receives a config which doesn't have expected params, then it can throw exception.\n    intercept[IllegalArgumentException] {\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-x\", Map(\"url\" -> \"url3\"), spark)\n    }\n  }\n\n  test(\"getCommitCoordinatorClient - builder returns new object each time\") {\n    object Builder1 extends CommitCoordinatorBuilder {\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n        conf.getOrElse(\"url\", \"\") match {\n          case \"url1\" => new TestCommitCoordinatorClient1()\n          case _ => throw new IllegalArgumentException(\"Invalid url\")\n        }\n      }\n      override def getName: String = \"cs-name\"\n    }\n    CommitCoordinatorProvider.registerBuilder(Builder1)\n    val cs1 =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-name\", Map(\"url\" -> \"url1\"), spark)\n    assert(cs1.isInstanceOf[TestCommitCoordinatorClient1])\n    val cs1_again =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-name\", Map(\"url\" -> \"url1\"), spark)\n    assert(cs1 ne cs1_again)\n  }\n\n  test(\"COORDINATED_COMMITS_PROVIDER_CONF\") {\n    val m1 = Metadata(\n      configuration = Map(\n        DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key ->\n          \"\"\"{\"key1\": \"string_value\", \"key2Int\": 2, \"key3ComplexStr\": \"\\\"hello\\\"\"}\"\"\")\n    )\n    assert(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.fromMetaData(m1) ===\n      Map(\"key1\" -> \"string_value\", \"key2Int\" -> \"2\", \"key3ComplexStr\" -> \"\\\"hello\\\"\"))\n\n    val m2 = Metadata(\n      configuration = Map(\n        DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key ->\n          \"\"\"{\"key1\": \"string_value\", \"key2Int\": \"2\"\"\")\n    )\n    intercept[com.fasterxml.jackson.core.JsonParseException] {\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.fromMetaData(m2)\n    }\n  }\n\n  test(\"Commit fails if we try to put bad value for COORDINATED_COMMITS_PROVIDER_CONF\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val deltaLog = DeltaLog.forTable(spark, path)\n\n      val metadataWithCorrectConf = Metadata(\n        configuration = Map(\n          DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key ->\n            \"\"\"{\"key1\": \"string_value\", \"key2Int\": 2, \"key3ComplexStr\": \"\\\"hello\\\"\"}\"\"\")\n      )\n      val metadataWithIncorrectConf = Metadata(\n        configuration = Map(\n          DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key ->\n            \"\"\"{\"key1\": \"string_value\", \"key2Int\": \"2\"\"\")\n      )\n\n      intercept[com.fasterxml.jackson.core.JsonParseException] {\n        deltaLog.startTransaction().commit(\n          Seq(metadataWithIncorrectConf), DeltaOperations.ManualUpdate)\n      }\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.fromMetaData(metadataWithCorrectConf)\n    }\n  }\n\n  test(\n    \"Adding COORDINATED_COMMITS_PROVIDER_NAME table property automatically upgrades the Protocol\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(10).write.format(\"delta\").mode(\"append\").save(path)\n      val metadata = Metadata(\n          configuration = Map(DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> \"in-memory\"))\n      val deltaLog = DeltaLog.forTable(spark, path)\n\n      def getWriterFeatures(log: DeltaLog): Set[String] = {\n        log.update().protocol.writerFeatures.getOrElse(Set.empty)\n      }\n\n      assert(!getWriterFeatures(deltaLog).contains(CoordinatedCommitsTableFeature.name))\n      deltaLog.startTransaction().commit(Seq(metadata), DeltaOperations.ManualUpdate)\n      assert(getWriterFeatures(deltaLog).contains(CoordinatedCommitsTableFeature.name))\n    }\n  }\n\n  test(\"Semantic Equality works as expected on CommitCoordinatorClients\") {\n    class TestCommitCoordinatorClient(val key: String) extends TestCommitCoordinatorClientBase {\n      override def semanticEquals(other: CommitCoordinatorClient): Boolean =\n        other.isInstanceOf[TestCommitCoordinatorClient] &&\n          other.asInstanceOf[TestCommitCoordinatorClient].key == key\n    }\n    object Builder1 extends CommitCoordinatorBuilder {\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n        new TestCommitCoordinatorClient(conf(\"key\"))\n      }\n      override def getName: String = \"cs-name\"\n    }\n    CommitCoordinatorProvider.registerBuilder(Builder1)\n\n    // Different CommitCoordinator with same keys should be semantically equal.\n    val obj1 =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-name\", Map(\"key\" -> \"url1\"), spark)\n    val obj2 =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-name\", Map(\"key\" -> \"url1\"), spark)\n    assert(obj1 != obj2)\n    assert(obj1.semanticEquals(obj2))\n\n    // Different CommitCoordinator with different keys should be semantically unequal.\n    val obj3 =\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\"cs-name\", Map(\"key\" -> \"url2\"), spark)\n    assert(obj1 != obj3)\n    assert(!obj1.semanticEquals(obj3))\n  }\n\n  private def checkMissing[Interface: TypeTag, Class: TypeTag](): Set[String] = {\n    val fields = typeOf[Class].decls.collect {\n      case m: MethodSymbol if m.isCaseAccessor => m.name.toString\n    }\n\n    val getters = typeOf[Interface].decls.collect {\n      case m: MethodSymbol if m.isAbstract => m.name.toString\n    }.toSet\n\n    fields.filterNot { field =>\n      getters.contains(s\"get${field.capitalize}\")\n    }.toSet\n  }\n\n  /**\n   * We expect the Protocol action to have the same fields as AbstractProtocol (part of the\n   * CommitCoordinatorClient interface). With this if any change has happened in the Protocol of the\n   * table, the same change is propagated to the CommitCoordinatorClient as AbstractProtocol. The\n   * CommitCoordinatorClient can access the changes using getters and decide to act on the changes\n   * based on the spec of the commit coordinator.\n   *\n   * This test case ensures that any new field added in the Protocol action is also accessible in\n   * the CommitCoordinatorClient via the getter. If the new field is something which we do not\n   * expect to be passed to the CommitCoordinatorClient, the test needs to be modified accordingly.\n   */\n  test(\"AbstractProtocol should have getter methods for all fields in Protocol\") {\n    val missingFields = checkMissing[AbstractProtocol, Protocol]()\n    val expectedMissingFields = Set.empty[String]\n    assert(missingFields == expectedMissingFields,\n      s\"Missing getter methods in AbstractProtocol\")\n  }\n\n  /**\n   * We expect the Metadata action to have the same fields as AbstractMetadata (part of the\n   * CommitCoordinatorClient interface). With this if any change has happened in the Metadata of the\n   * table, the same change is propagated to the CommitCoordinatorClient as AbstractMetadata. The\n   * CommitCoordinatorClient can access the changes using getters and decide to act on the changes\n   * based on the spec of the commit coordinator.\n   *\n   * This test case ensures that any new field added in the Metadata action is also accessible in\n   * the CommitCoordinatorClient via the getter. If the new field is something which we do not\n   * expect to be passed to the CommitCoordinatorClient, the test needs to be modified accordingly.\n   */\n  test(\"BaseMetadata should have getter methods for all fields in Metadata\") {\n    val missingFields = checkMissing[AbstractMetadata, Metadata]()\n    val expectedMissingFields = Set(\"format\")\n    assert(missingFields == expectedMissingFields,\n      s\"Missing getter methods in AbstractMetadata\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsEnablementSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\n\nclass CoordinatedCommitsEnablementSuite\n  extends CoordinatedCommitsBaseSuite\n    with DeltaSQLTestUtils\n    with DeltaSQLCommandTest\n    with CoordinatedCommitsTestUtils {\n\n  override def coordinatedCommitsBackfillBatchSize: Option[Int] = Some(3)\n\n  import testImplicits._\n\n  private def validateCoordinatedCommitsCompleteEnablement(\n      snapshot: Snapshot, expectEnabled: Boolean): Unit = {\n    assert(\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.fromMetaData(snapshot.metadata).isDefined\n      == expectEnabled)\n    Seq(\n      CoordinatedCommitsTableFeature,\n      VacuumProtocolCheckTableFeature,\n      InCommitTimestampTableFeature)\n      .foreach { feature =>\n        assert(snapshot.protocol.writerFeatures.exists(_.contains(feature.name)) == expectEnabled)\n      }\n    assert(\n      snapshot.protocol.readerFeatures.exists(_.contains(VacuumProtocolCheckTableFeature.name))\n        == expectEnabled)\n    assert(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.fromMetaData(snapshot.metadata)\n      == expectEnabled)\n  }\n\n  // ---- Tests START: Enablement at commit 0 ----\n  test(\"enablement at commit 0: CC should enable ICT and VacuumProtocolCheck\" +\n    \" --- writeintodelta api\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      val log = DeltaLog.forTable(spark, tablePath)\n      validateCoordinatedCommitsCompleteEnablement(log.snapshot, expectEnabled = true)\n    }\n  }\n\n  test(\"enablement at commit 0: CC should enable ICT and VacuumProtocolCheck\" +\n    \" --- simple create table\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      sql(s\"CREATE TABLE delta.`$tablePath` (id LONG) USING delta\")\n      val log = DeltaLog.forTable(spark, tablePath)\n      validateCoordinatedCommitsCompleteEnablement(log.snapshot, expectEnabled = true)\n    }\n  }\n\n  test(\"enablement at commit 0: CC should enable ICT and VacuumProtocolCheck\" +\n    \" --- create or replace\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      val log = DeltaLog.forTable(spark, tablePath)\n      validateCoordinatedCommitsCompleteEnablement(log.snapshot, expectEnabled = true)\n    }\n  }\n  // ---- Tests END: Enablement at commit 0 ----\n\n  // ---- Tests START: Enablement after commit 0 ----\n  testWithDefaultCommitCoordinatorUnset(\n    \"enablement after commit 0: CC should enable ICT and VacuumProtocolCheck\" +\n      \" --- update tblproperty\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath) // commit 0\n      Seq(1).toDF().write.format(\"delta\").mode(\"append\").save(tablePath) // commit 1\n      val log = DeltaLog.forTable(spark, tablePath)\n      validateCoordinatedCommitsCompleteEnablement(log.snapshot, expectEnabled = false)\n      sql(s\"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES \" + // Enable CC\n        s\"('${DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key}' = 'tracking-in-memory', \" +\n        s\"'${DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '{}')\")\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath) // commit 3\n      validateCoordinatedCommitsCompleteEnablement(log.update(), expectEnabled = true)\n    }\n  }\n  // ---- Tests END: Enablement after commit 0 ----\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsPropertySuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport org.apache.spark.sql.delta.{DeltaIllegalArgumentException, DeltaLog}\nimport org.apache.spark.sql.delta.DeltaConfigs.{COORDINATED_COMMITS_COORDINATOR_CONF, COORDINATED_COMMITS_COORDINATOR_NAME, COORDINATED_COMMITS_TABLE_CONF}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport io.delta.storage.commit.CommitCoordinatorClient\n\nimport org.apache.spark.sql.{QueryTest, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait CoordinatedCommitsPropertySuiteBase extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with CoordinatedCommitsTestUtils {\n\n  private def getRandomTableName: String = scala.util.Random.alphanumeric.take(10).mkString(\"\")\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    target = getRandomTableName\n    source = getRandomTableName\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n    CommitCoordinatorProvider.registerBuilder(CommitCoordinatorBuilder1())\n    CommitCoordinatorProvider.registerBuilder(CommitCoordinatorBuilder2())\n  }\n\n  protected val command: String\n\n  protected val cc1: String = \"commit-coordinator-1\"\n\n  private case class CommitCoordinatorBuilder1() extends CommitCoordinatorBuilder {\n    private val commitCoordinator = new InMemoryCommitCoordinator(batchSize = 1000L)\n    override def getName: String = cc1\n    override def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient =\n      commitCoordinator\n  }\n\n  protected val cc2: String = \"commit-coordinator-2\"\n\n  private case class CommitCoordinatorBuilder2() extends CommitCoordinatorBuilder {\n    private val commitCoordinator = new InMemoryCommitCoordinator(batchSize = 1000L)\n    override def getName: String = cc2\n    override def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient =\n      commitCoordinator\n  }\n\n  protected var target: String = getRandomTableName\n  protected var source: String = getRandomTableName\n\n  protected val coordinatorNameKey: String = COORDINATED_COMMITS_COORDINATOR_NAME.key\n  protected val coordinatorConfKey: String = COORDINATED_COMMITS_COORDINATOR_CONF.key\n  protected val tableConfKey: String = COORDINATED_COMMITS_TABLE_CONF.key\n\n  protected val coordinatorNameDefaultKey: String =\n    COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey\n  protected val coordinatorConfDefaultKey: String =\n    COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey\n  protected val tableConfDefaultKey: String =\n    COORDINATED_COMMITS_TABLE_CONF.defaultTablePropertyKey\n\n  protected val randomCoordinatorConf: String =\n    JsonUtils.toJson(Map(\"randomCoordinatorConf\" -> \"randomCoordinatorConfValue\"))\n  protected val randomTableConf: String =\n    JsonUtils.toJson(Map(\"randomTableConf\" -> \"randomTableConfValue\"))\n\n  def getCCPropertiesClause(properties: Seq[(String, String)]): String = {\n    if (properties.nonEmpty) {\n      \" TBLPROPERTIES (\" +\n        properties.map { case (k, v) => s\"'$k' = '$v'\" }.mkString(\", \") +\n        \")\"\n    } else {\n      \"\"\n    }\n  }\n\n  def verifyCommitCoordinator(table: String, expectedCoordinator: Option[String]): Unit = {\n    assert(DeltaLog.forTable(spark, TableIdentifier(table))\n      .update().metadata.coordinatedCommitsCoordinatorName == expectedCoordinator)\n  }\n\n  def testImpl(\n    commandConfs: Seq[(String, String)] = Seq(),\n    defaultConfs: Seq[(String, String)] = Seq(),\n    targetConfs: Seq[(String, String)] = Seq(),\n    sourceConfs: Seq[(String, String)] = Seq(),\n    expectedCoordinator: Option[String] = None): Unit\n}\n\ntrait CoordinatedCommitsPropertyCreateTableSuiteBase extends CoordinatedCommitsPropertySuiteBase {\n\n  test(\"Commit coordinators are picked from command specification.\") {\n    testImpl(\n      commandConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n\n  test(\"Commit coordinators are picked from default configurations if not specified in command.\") {\n    testImpl(\n      defaultConfs = Seq(\n        coordinatorNameDefaultKey -> cc1,\n        coordinatorConfDefaultKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n\n  test(\"Command-specified commit coordinators take precedence over default configurations.\") {\n    testImpl(\n      commandConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      defaultConfs = Seq(\n        coordinatorNameDefaultKey -> cc2,\n        coordinatorConfDefaultKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n\n  test(\"Illegal command-specified property combinations throw an exception.\") {\n    var e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        commandConfs = Seq(coordinatorNameKey -> cc1))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_COMMAND\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> coordinatorConfKey))\n\n    e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        commandConfs = Seq(coordinatorConfKey -> randomCoordinatorConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_COMMAND\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> coordinatorNameKey))\n\n    e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        commandConfs = Seq(\n          coordinatorNameKey -> cc1,\n          coordinatorConfKey -> randomCoordinatorConf,\n          tableConfKey -> randomTableConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> tableConfKey))\n  }\n\n  test(\"Illegal default property combinations throw an exception if none specified in command.\") {\n    var e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        defaultConfs = Seq(coordinatorNameDefaultKey -> cc1))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_SESSION\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> coordinatorConfDefaultKey))\n\n    e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        defaultConfs = Seq(coordinatorConfDefaultKey -> randomCoordinatorConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_SESSION\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> coordinatorNameDefaultKey))\n\n    e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        defaultConfs = Seq(\n          coordinatorNameDefaultKey -> cc1,\n          coordinatorConfDefaultKey -> randomCoordinatorConf,\n          tableConfDefaultKey -> randomTableConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_SESSION\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> tableConfDefaultKey))\n  }\n\n  test(\"Illegal default property combinations are ignored if command specifications are valid.\") {\n    testImpl(\n      commandConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      defaultConfs = Seq(\n        coordinatorNameDefaultKey -> cc2,\n        coordinatorConfDefaultKey -> randomCoordinatorConf,\n        tableConfDefaultKey -> randomTableConf),\n      expectedCoordinator = Some(cc1))\n  }\n  test(\"Illegal command-specified property combinations throw an exception even if default \" +\n      \"configurations are valid.\") {\n    val e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        commandConfs = Seq(\n          coordinatorNameKey -> cc1,\n          coordinatorConfKey -> randomCoordinatorConf,\n          tableConfKey -> randomTableConf),\n        defaultConfs = Seq(\n          coordinatorNameDefaultKey -> cc2,\n          coordinatorConfDefaultKey -> randomCoordinatorConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\",\n      sqlState = \"42616\",\n      parameters = Map(\"command\" -> command, \"configuration\" -> tableConfKey))\n  }\n}\n\nclass CoordinatedCommitsPropertyCreateTableSuite\n  extends CoordinatedCommitsPropertyCreateTableSuiteBase {\n\n  override protected val command: String = \"CREATE\"\n\n  override def testImpl(\n      commandConfs: Seq[(String, String)],\n      defaultConfs: Seq[(String, String)],\n      targetConfs: Seq[(String, String)],\n      sourceConfs: Seq[(String, String)],\n      expectedCoordinator: Option[String]): Unit = {\n    withTable(target) {\n      withSQLConf(defaultConfs: _*) {\n        sql(s\"CREATE TABLE $target (id LONG) USING delta\" + getCCPropertiesClause(commandConfs))\n      }\n      verifyCommitCoordinator(target, expectedCoordinator)\n    }\n  }\n}\n\nclass CoordinatedCommitsPropertyCreateTableAsSelectSuite\n  extends CoordinatedCommitsPropertyCreateTableSuiteBase {\n\n  override protected val command: String = \"CREATE\"\n\n  override def testImpl(\n      commandConfs: Seq[(String, String)],\n      defaultConfs: Seq[(String, String)],\n      targetConfs: Seq[(String, String)],\n      sourceConfs: Seq[(String, String)],\n      expectedCoordinator: Option[String]): Unit = {\n    withTable(target, source) {\n      sql(s\"CREATE TABLE $source (id LONG) USING delta\")\n      sql(s\"INSERT INTO $source VALUES (1)\")\n      withSQLConf(defaultConfs: _*) {\n        sql(s\"CREATE TABLE $target USING delta\" +\n          getCCPropertiesClause(commandConfs) + s\" AS SELECT * FROM $source\")\n      }\n      verifyCommitCoordinator(target, expectedCoordinator)\n    }\n  }\n}\n\nclass CoordinatedCommitsPropertyCreateTableWithShallowCloneSuite\n  extends CoordinatedCommitsPropertyCreateTableSuiteBase {\n\n  override protected val command: String = \"CREATE with CLONE\"\n\n  override def testImpl(\n      commandConfs: Seq[(String, String)] = Seq(),\n      defaultConfs: Seq[(String, String)] = Seq(),\n      targetConfs: Seq[(String, String)] = Seq(),\n      sourceConfs: Seq[(String, String)] = Seq(),\n      expectedCoordinator: Option[String] = None): Unit = {\n    withTable(target, source) {\n      sql(s\"CREATE TABLE $source (id LONG) USING delta\" + getCCPropertiesClause(sourceConfs))\n      withSQLConf(defaultConfs: _*) {\n        sql(s\"CREATE TABLE $target SHALLOW CLONE $source\" + getCCPropertiesClause(commandConfs))\n      }\n      verifyCommitCoordinator(target, expectedCoordinator)\n    }\n  }\n\n  test(\"Source table's commit coordinator should never be copied to the target table: no commit \" +\n      \"coordinators are specified\") {\n    testImpl(\n      sourceConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = None)\n  }\n\n  test(\"Source table's commit coordinator should never be copied to the target table: command \" +\n      \"specifies a commit coordinator\") {\n    testImpl(\n      commandConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      sourceConfs = Seq(\n        coordinatorNameKey -> cc2,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n\n  test(\"Source table's commit coordinator should never be copied to the target table: default \" +\n      \"configurations specify a commit coordinator\") {\n    testImpl(\n      defaultConfs = Seq(\n        coordinatorNameDefaultKey -> cc1,\n        coordinatorConfDefaultKey -> randomCoordinatorConf),\n      sourceConfs = Seq(\n        coordinatorNameKey -> cc2,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n}\n\ntrait CoordinatedCommitsPropertyReplaceTableSuiteBase extends CoordinatedCommitsPropertySuiteBase {\n\n  test(\"Any command-specified Coordinated Commits overrides throw an exception\") {\n    var e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        commandConfs = Seq(\n          coordinatorNameKey -> cc1,\n          coordinatorConfKey -> randomCoordinatorConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\",\n      sqlState = \"42616\",\n      parameters = Map(\"Command\" -> command))\n\n    e = intercept[DeltaIllegalArgumentException] {\n      testImpl(\n        commandConfs = Seq(\n          coordinatorNameKey -> cc1,\n          coordinatorConfKey -> randomCoordinatorConf,\n          tableConfKey -> randomTableConf))\n    }\n    checkError(\n      exception = e,\n      \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\",\n      sqlState = \"42616\",\n      parameters = Map(\"Command\" -> command))\n  }\n\n  test(\"Default Coordinated Commits configurations from SparkSession are ignored\") {\n    testImpl(\n      defaultConfs = Seq(\n        coordinatorNameDefaultKey -> cc1,\n        coordinatorConfDefaultKey -> randomCoordinatorConf),\n      expectedCoordinator = None)\n\n    testImpl(\n      defaultConfs = Seq(\n        coordinatorNameDefaultKey -> cc1,\n        coordinatorConfDefaultKey -> randomCoordinatorConf,\n        tableConfDefaultKey -> randomTableConf),\n      expectedCoordinator = None)\n  }\n\n  test(\"Existing Coordinated Commits configurations from the target table are retained.\") {\n    testImpl(\n      targetConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n}\n\nclass CoordinatedCommitsPropertyReplaceTableSuite\n  extends CoordinatedCommitsPropertyReplaceTableSuiteBase {\n\n  override protected val command: String = \"REPLACE\"\n\n  override def testImpl(\n      commandConfs: Seq[(String, String)],\n      defaultConfs: Seq[(String, String)],\n      targetConfs: Seq[(String, String)],\n      sourceConfs: Seq[(String, String)],\n      expectedCoordinator: Option[String]): Unit = {\n    withTable(target) {\n      sql(s\"CREATE TABLE $target (id LONG) USING delta\" + getCCPropertiesClause(targetConfs))\n      withSQLConf(defaultConfs: _*) {\n        sql(s\"REPLACE TABLE $target (id STRING) USING delta\" + getCCPropertiesClause(commandConfs))\n      }\n      verifyCommitCoordinator(target, expectedCoordinator)\n    }\n  }\n}\n\nclass CoordinatedCommitsPropertyReplaceTableAsSelectSuite\n  extends CoordinatedCommitsPropertyReplaceTableSuiteBase {\n\n  override protected val command: String = \"REPLACE\"\n\n  override def testImpl(\n      commandConfs: Seq[(String, String)],\n      defaultConfs: Seq[(String, String)],\n      targetConfs: Seq[(String, String)],\n      sourceConfs: Seq[(String, String)],\n      expectedCoordinator: Option[String]): Unit = {\n    withTable(target, source) {\n      sql(s\"CREATE TABLE $source (id LONG) USING delta\")\n      sql(s\"INSERT INTO $source VALUES (1)\")\n      sql(s\"CREATE TABLE $target (id LONG) USING delta\" + getCCPropertiesClause(targetConfs))\n      withSQLConf(defaultConfs: _*) {\n        sql(s\"REPLACE TABLE $target USING delta\" +\n          getCCPropertiesClause(commandConfs) + s\" AS SELECT * FROM $source\")\n      }\n      verifyCommitCoordinator(target, expectedCoordinator)\n    }\n  }\n}\n\nclass CoordinatedCommitsPropertyReplaceTableWithShallowCloneSuite\n  extends CoordinatedCommitsPropertyReplaceTableSuiteBase {\n\n  override protected val command: String = \"REPLACE with CLONE\"\n\n  override def testImpl(\n      commandConfs: Seq[(String, String)] = Seq(),\n      defaultConfs: Seq[(String, String)] = Seq(),\n      targetConfs: Seq[(String, String)] = Seq(),\n      sourceConfs: Seq[(String, String)] = Seq(),\n      expectedCoordinator: Option[String] = None): Unit = {\n    withTable(target, source) {\n      sql(s\"CREATE TABLE $target (id LONG) USING delta\" + getCCPropertiesClause(targetConfs))\n      sql(s\"CREATE TABLE $source (id LONG) USING delta\" + getCCPropertiesClause(sourceConfs))\n      withSQLConf(defaultConfs: _*) {\n        sql(s\"REPLACE TABLE $target SHALLOW CLONE $source\" + getCCPropertiesClause(commandConfs))\n      }\n      verifyCommitCoordinator(target, expectedCoordinator)\n    }\n  }\n\n  test(\"Source table's commit coordinator should never be copied to the target table: target \" +\n      \"table does not have any coordinator\") {\n    testImpl(\n      sourceConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = None)\n  }\n\n  test(\"Source table's commit coordinator should never be copied to the target table: target \" +\n      \"table has a coordinator\") {\n    testImpl(\n      targetConfs = Seq(\n        coordinatorNameKey -> cc1,\n        coordinatorConfKey -> randomCoordinatorConf),\n      sourceConfs = Seq(\n        coordinatorNameKey -> cc2,\n        coordinatorConfKey -> randomCoordinatorConf),\n      expectedCoordinator = Some(cc1))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.io.File\nimport java.lang.{Long => JLong}\nimport java.util.{Iterator => JIterator, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.ArrayBuffer\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport com.databricks.spark.util.UsageRecord\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, CheckpointPolicy, CommitCoordinatorGetCommitsFailedException, CommitStats, CoordinatedCommitsStats, CoordinatedCommitsTableFeature, DeltaIllegalArgumentException, DeltaOperations, DeltaTestUtilsBase, DeltaUnsupportedOperationException, V2CheckpointTableFeature}\nimport org.apache.spark.sql.delta.CoordinatedCommitType._\nimport org.apache.spark.sql.delta.DeltaConfigs\nimport org.apache.spark.sql.delta.DeltaConfigs.{CHECKPOINT_INTERVAL, CHECKPOINT_POLICY, COORDINATED_COMMITS_COORDINATOR_CONF, COORDINATED_COMMITS_COORDINATOR_NAME, COORDINATED_COMMITS_TABLE_CONF, IN_COMMIT_TIMESTAMPS_ENABLED}\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.DummySnapshot\nimport org.apache.spark.sql.delta.LogSegment\nimport org.apache.spark.sql.delta.Snapshot\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaExceptionTestUtils\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.spark.sql.delta.util.FileNames.{CompactedDeltaFile, DeltaFile, UnbackfilledDeltaFile}\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{CommitCoordinatorClient, CommitResponse, CoordinatedCommitsUtils => JCoordinatedCommitsUtils, GetCommitsResponse => JGetCommitsResponse, TableDescriptor, TableIdentifier, UpdatedActions}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.{TableIdentifier => CatalystTableIdentifier}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ManualClock\n\nclass CoordinatedCommitsSuite\n  extends CommitCoordinatorSuiteBase\n  with CoordinatedCommitsBaseSuite {\n\n  import testImplicits._\n\n  override def sparkConf: SparkConf = {\n    // Make sure all new tables in tests use tracking-in-memory commit-coordinator by default.\n    super.sparkConf\n      .set(COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey, \"tracking-in-memory\")\n      .set(COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey, JsonUtils.toJson(Map()))\n  }\n\n  test(\"helper method that recovers config from abstract metadata works properly\") {\n    val m1 = Metadata(\n      configuration = Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> \"string_value\")\n    )\n    assert(JCoordinatedCommitsUtils.getCoordinatorName(m1) === Optional.of(\"string_value\"))\n\n    val m2 = Metadata(\n      configuration = Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> \"\")\n    )\n    assert(JCoordinatedCommitsUtils.getCoordinatorName(m2)=== Optional.of(\"\"))\n\n    val m3 = Metadata(\n      configuration = Map(\n        COORDINATED_COMMITS_COORDINATOR_CONF.key ->\n          \"\"\"{\"key1\": \"string_value\", \"key2Int\": 2, \"key3ComplexStr\": \"\\\"hello\\\"\"}\"\"\")\n    )\n    assert(JCoordinatedCommitsUtils.getCoordinatorConf(m3) ===\n      Map(\"key1\" -> \"string_value\", \"key2Int\" -> \"2\", \"key3ComplexStr\" -> \"\\\"hello\\\"\").asJava)\n\n    val m4 = Metadata()\n    assert(JCoordinatedCommitsUtils.getCoordinatorConf(m4) === Map.empty.asJava)\n  }\n\n  test(\"During ALTER, overriding Coordinated Commits configurations throws an exception.\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n    registerBuilder(InMemoryCommitCoordinatorBuilder(1))\n\n    withTempDir { tempDir =>\n      sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('${COORDINATED_COMMITS_COORDINATOR_NAME.key}' = 'tracking-in-memory', \" +\n        s\"'${COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '${JsonUtils.toJson(Map())}')\")\n      val e = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        sql(s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` SET TBLPROPERTIES \" +\n          s\"('${COORDINATED_COMMITS_COORDINATOR_NAME.key}' = 'in-memory', \" +\n          s\"'${COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '${JsonUtils.toJson(Map())}')\")\n      }\n      checkError(\n        e,\n        \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\",\n        sqlState = \"42616\",\n        parameters = Map(\"Command\" -> \"ALTER\"))\n    }\n  }\n\n  test(\"During ALTER, unsetting Coordinated Commits configurations throws an exception.\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n\n    withTempDir { tempDir =>\n      sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('${COORDINATED_COMMITS_COORDINATOR_NAME.key}' = 'tracking-in-memory', \" +\n        s\"'${COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '${JsonUtils.toJson(Map())}')\")\n      val e = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        sql(s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` UNSET TBLPROPERTIES \" +\n          s\"('${COORDINATED_COMMITS_COORDINATOR_NAME.key}', \" +\n          s\"'${COORDINATED_COMMITS_COORDINATOR_CONF.key}')\")\n      }\n      checkError(\n        e,\n        \"DELTA_CANNOT_UNSET_COORDINATED_COMMITS_CONFS\",\n        sqlState = \"42616\",\n        parameters = Map[String, String]())\n    }\n  }\n\n  test(\"tableConf returned from registration API is recorded in deltaLog and passed \" +\n    \"to CommitCoordinatorClient in future for all the APIs\") {\n    val tableConf = Map(\"tableID\" -> \"random-u-u-i-d\", \"1\" -> \"2\").asJava\n    val trackingCommitCoordinatorClient = new TrackingCommitCoordinatorClient(\n      new InMemoryCommitCoordinator(batchSize = 10) {\n        override def registerTable(\n            logPath: Path,\n            tableIdentifier: Optional[TableIdentifier],\n            currentVersion: Long,\n            currentMetadata: AbstractMetadata,\n            currentProtocol: AbstractProtocol): java.util.Map[String, String] = {\n          super.registerTable(\n            logPath, tableIdentifier, currentVersion, currentMetadata, currentProtocol)\n          tableConf\n        }\n\n        override def getCommits(\n            tableDesc: TableDescriptor,\n            startVersion: java.lang.Long,\n            endVersion: java.lang.Long): JGetCommitsResponse = {\n          assert(tableDesc.getTableConf === tableConf)\n          super.getCommits(tableDesc, startVersion, endVersion)\n        }\n\n        override def commit(\n            logStore: LogStore,\n            hadoopConf: Configuration,\n            tableDesc: TableDescriptor,\n            commitVersion: Long,\n            actions: java.util.Iterator[String],\n            updatedActions: UpdatedActions): CommitResponse = {\n          assert(tableDesc.getTableConf === tableConf)\n          super.commit(logStore, hadoopConf, tableDesc, commitVersion, actions, updatedActions)\n        }\n\n        override def backfillToVersion(\n            logStore: LogStore,\n            hadoopConf: Configuration,\n            tableDesc: TableDescriptor,\n            version: Long,\n            lastKnownBackfilledVersionOpt: java.lang.Long): Unit = {\n          assert(tableDesc.getTableConf === tableConf)\n          super.backfillToVersion(\n            logStore,\n            hadoopConf,\n            tableDesc,\n            version,\n            lastKnownBackfilledVersionOpt)\n        }\n      }\n    )\n    val builder = TrackingInMemoryCommitCoordinatorBuilder(\n      batchSize = 10, Some(trackingCommitCoordinatorClient))\n    registerBuilder(builder)\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val log = DeltaLog.forTable(spark, tablePath)\n      val commitCoordinatorConf = Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder.getName)\n      val newMetadata = Metadata().copy(configuration = commitCoordinatorConf)\n      log.startTransaction().commitManually(newMetadata)\n      assert(log.unsafeVolatileSnapshot.version === 0)\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf.asJava === tableConf)\n\n      log.startTransaction().commitManually(createTestAddFile(\"f1\"))\n      log.startTransaction().commitManually(createTestAddFile(\"f2\"))\n      log.checkpoint()\n      log.startTransaction().commitManually(createTestAddFile(\"f2\"))\n\n      assert(trackingCommitCoordinatorClient.numCommitsCalled.get > 0)\n      assert(trackingCommitCoordinatorClient.numGetCommitsCalled.get > 0)\n      assert(trackingCommitCoordinatorClient.numBackfillToVersionCalled.get > 0)\n    }\n  }\n\n  test(\"transfer from one commit-coordinator to another commit-coordinator fails \" +\n    \"[CC-1 -> CC-2 fails]\") {\n    clearBuilders()\n    val builder1 = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10)\n    val builder2 = new TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10) {\n      override def getName: String = \"tracking-in-memory-2\"\n    }\n    Seq(builder1, builder2).foreach(registerBuilder(_))\n\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val log = DeltaLog.forTable(spark, tablePath)\n      // A new table will automatically get `tracking-in-memory` as the whole suite is configured to\n      // use it as default commit-coordinator via\n      // [[COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey]].\n      log.startTransaction().commitManually(Metadata())\n      assert(log.unsafeVolatileSnapshot.version === 0L)\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n\n      // Change commit-coordinator\n      val newCommitCoordinatorConf =\n        Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder2.getName)\n      val oldMetadata = log.unsafeVolatileSnapshot.metadata\n      val newMetadata = oldMetadata.copy(\n        configuration = oldMetadata.configuration ++ newCommitCoordinatorConf)\n      val ex = intercept[IllegalStateException] {\n        log.startTransaction().commitManually(newMetadata)\n      }\n      assert(ex.getMessage.contains(\n        \"from one commit-coordinator to another commit-coordinator is not allowed\"))\n    }\n  }\n\n  // This test has the following setup:\n  // Setup:\n  // 1. Make 2 commits on the table with CS1 as owner.\n  // 2. Make 2 new commits to change the owner back to FS and then from FS to CS2.\n  // 3. Do cold read from table and confirm we can construct snapshot v3 automatically. This will\n  //    need multiple snapshot update internally and both CS1 and CS2 will be contacted one\n  //    after the other.\n  // 4. Write commit 4/5 using new commit-coordinator.\n  // 5. Read the table again and make sure right APIs are called:\n  //    a) If read query is run in scala, we do listing 2 times. So CS2.getCommits will be called\n  //       twice. We should not be contacting CS1 anymore.\n  //    b) If read query is run on SQL, we do listing only once. So CS2.getCommits will be called\n  //       only once.\n  test(\"snapshot is updated properly when owner changes multiple times\") {\n    val batchSize = 10\n    val cs1 = new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize))\n    val cs2 = new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize))\n\n    case class TrackingInMemoryCommitCoordinatorBuilder(\n        name: String,\n        commitCoordinatorClient: CommitCoordinatorClient) extends CommitCoordinatorBuilder {\n      var numBuildCalled = 0\n      override def build(\n          spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n        numBuildCalled += 1\n        commitCoordinatorClient\n      }\n\n      override def getName: String = name\n    }\n    val builder1 = TrackingInMemoryCommitCoordinatorBuilder(name = \"tracking-in-memory-1\", cs1)\n    val builder2 = TrackingInMemoryCommitCoordinatorBuilder(name = \"tracking-in-memory-2\", cs2)\n    Seq(builder1, builder2).foreach(CommitCoordinatorProvider.registerBuilder)\n\n    def resetMetrics(): Unit = {\n      Seq(builder1, builder2).foreach { b => b.numBuildCalled = 0 }\n      Seq(cs1, cs2).foreach(_.reset())\n    }\n\n    withSQLConf(\n        COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey -> builder1.name) {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        // Step-1: Make 2 commits on the table with CS1 as owner.\n        Seq(0).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 0\n        Seq(1).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 1\n        DeltaLog.clearCache()\n        checkAnswer(sql(s\"SELECT * FROM delta.`$tablePath`\"), Seq(Row(0), Row(1)))\n\n        // Step-2: Add commit 2: change the table owner from \"tracking-in-memory-1\" to FS.\n        //         Add commit 3: change the table owner from FS to \"tracking-in-memory-2\".\n        // Both of these commits should be FS based as the spec mandates an atomic backfill when\n        // the commit-coordinator changes.\n        {\n          val log = DeltaLog.forTable(spark, tablePath)\n          val conf = log.newDeltaHadoopConf()\n          val segment = log.unsafeVolatileSnapshot.logSegment\n          (0 to 1).foreach { version =>\n            assert(FileNames.deltaVersion(segment.deltas(version).getPath) === version)\n          }\n          val oldMetadata = log.unsafeVolatileMetadata\n          val oldMetadataConf = oldMetadata.configuration\n          val newMetadata1 = oldMetadata.copy(\n            configuration = oldMetadataConf - COORDINATED_COMMITS_COORDINATOR_NAME.key)\n          val newMetadata2 = oldMetadata.copy(\n            configuration = oldMetadataConf + (\n              COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder2.name))\n          log.startTransaction().commitManually(newMetadata1)\n          log.startTransaction().commitManually(newMetadata2)\n\n          // Also backfill commit 0, 1 -- which the spec mandates when the commit-coordinator\n          // changes.\n          // commit 0 should already be backfilled\n          assert(segment.deltas(0).getPath.getName === \"00000000000000000000.json\")\n          log.store.write(\n            path = FileNames.unsafeDeltaFile(log.logPath, 1),\n            actions = log.store.read(segment.deltas(1).getPath, conf).toIterator,\n            overwrite = true,\n            conf)\n        }\n\n        // Step-3: Do cold read from table and confirm we can construct snapshot v3 automatically.\n        // This will update snapshot twice and both CS1 and CS2 will be contacted one after the\n        // other. Do cold read from the table and confirm things works as expected.\n        DeltaLog.clearCache()\n        resetMetrics()\n        checkAnswer(sql(s\"SELECT * FROM delta.`$tablePath`\"), Seq(Row(0), Row(1)))\n        assert(builder1.numBuildCalled == 0)\n        assert(builder2.numBuildCalled == 1)\n        val snapshotV3 = DeltaLog.forTable(spark, tablePath).unsafeVolatileSnapshot\n        assert(\n          snapshotV3.tableCommitCoordinatorClientOpt.map(_.commitCoordinatorClient) === Some(cs2))\n        assert(snapshotV3.version === 3)\n\n        // Step-4: Write more commits using new owner\n        resetMetrics()\n        Seq(2).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 4\n        Seq(3).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 5\n        assert((cs1.numCommitsCalled.get, cs2.numCommitsCalled.get) === (0, 2))\n        assert((cs1.numGetCommitsCalled.get, cs2.numGetCommitsCalled.get) === (0, 2))\n\n        // Step-5: Read the table again and assert that the right APIs are used\n        resetMetrics()\n        assert(\n          sql(s\"SELECT * FROM delta.`$tablePath`\").collect().toSet === (0 to 3).map(Row(_)).toSet)\n        // since this was hot query, so no new snapshot was created as part of this\n        // deltaLog.update() and so commit-coordinator is not initialized again.\n        assert((builder1.numBuildCalled, builder2.numBuildCalled) === (0, 0))\n        // Since this is dataframe read, so we invoke deltaLog.update() twice and so GetCommits API\n        // is called twice.\n        assert((cs1.numGetCommitsCalled.get, cs2.numGetCommitsCalled.get) === (0, 2))\n\n        // Step-6: Clear cache and simulate cold read again.\n        // We will firstly create snapshot from listing: 0.json, 1.json, 2.json.\n        // We create Snapshot v2 and find about owner CS2.\n        // Then we contact CS2 to update snapshot and find that v3, v4 exist.\n        // We create snapshot v4 and find that owner doesn't change.\n        DeltaLog.clearCache()\n        resetMetrics()\n        assert(\n          sql(s\"SELECT * FROM delta.`$tablePath`\").collect().toSet === (0 to 3).map(Row(_)).toSet)\n        assert((cs1.numGetCommitsCalled.get, cs2.numGetCommitsCalled.get) === (0, 2))\n        assert((builder1.numBuildCalled, builder2.numBuildCalled) === (0, 2))\n      }\n    }\n  }\n}\n\nclass CatalogOwnedSuite\n  extends CommitCoordinatorSuiteBase\n  with CatalogOwnedTestBaseSuite {\n\n  override def sparkConf: SparkConf = {\n    // Make sure all new tables in tests use CatalogOwned table feature by default.\n    super.sparkConf.set(defaultCatalogOwnedFeatureEnabledKey, \"supported\")\n  }\n}\n\nabstract class CommitCoordinatorSuiteBase\n  extends QueryTest\n  with CommitCoordinatorUtilBase\n  with DeltaSQLTestUtils\n  with DeltaTestUtilsBase\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaExceptionTestUtils {\n\n  import testImplicits._\n\n  test(\"0th commit happens via filesystem\") {\n    val commitCoordinatorName = \"tracking-in-memory\"\n    object NoBackfillingCommitCoordinatorBuilder$\n        extends CatalogOwnedCommitCoordinatorBuilder {\n\n      override def getName: String = commitCoordinatorName\n\n      override def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient =\n        new InMemoryCommitCoordinator(batchSize = 5) {\n          override def commit(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              commitVersion: Long,\n              actions: JIterator[String],\n              updatedActions: UpdatedActions): CommitResponse = {\n            throw new IllegalStateException(\"Fail commit request\")\n          }\n        }\n\n      override def buildForCatalog(spark: SparkSession, name: String): CommitCoordinatorClient =\n        new InMemoryCommitCoordinator(batchSize = 5) {\n          override def commit(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              commitVersion: Long,\n              actions: JIterator[String],\n              updatedActions: UpdatedActions): CommitResponse = {\n            throw new IllegalStateException(\"Fail commit request\")\n          }\n        }\n    }\n\n    registerBuilder(NoBackfillingCommitCoordinatorBuilder$)\n    withDefaultCCTableFeature {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        Seq(1).toDF.write.format(\"delta\").save(tablePath)\n        val log = DeltaLog.forTable(spark, tablePath)\n        assert(log.store.listFrom(FileNames.listingPrefix(log.logPath, 0L)).exists { f =>\n          f.getPath.getName === \"00000000000000000000.json\"\n        })\n      }\n    }\n  }\n\n  test(\"basic write\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(batchSize = 2))\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // version 0\n      Seq(2).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // version 1\n      Seq(3).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 2\n\n      val log = DeltaLog.forTable(spark, tablePath)\n      val commitsDir = new File(FileNames.commitDirPath(log.logPath).toUri)\n      val unbackfilledCommitVersions =\n        commitsDir\n          .listFiles()\n          .filterNot(f => f.getName.startsWith(\".\") && f.getName.endsWith(\".crc\"))\n          .map(_.getAbsolutePath)\n          .sortBy(path => path).map { commitPath =>\n            assert(FileNames.isDeltaFile(new Path(commitPath)))\n            FileNames.deltaVersion(new Path(commitPath))\n          }\n      assert(unbackfilledCommitVersions === Array(1, 2))\n      checkAnswer(sql(s\"SELECT * FROM delta.`$tablePath`\"), Seq(Row(2), Row(3)))\n    }\n  }\n\n  test(\"cold snapshot initialization\") {\n    val builder = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10)\n    val commitCoordinatorClient =\n      builder.build(spark, Map.empty).asInstanceOf[TrackingCommitCoordinatorClient]\n    registerBuilder(builder)\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // version 0\n      DeltaLog.clearCache()\n      val usageLogs1 = Log4jUsageLogger.track {\n        checkAnswer(sql(s\"SELECT * FROM delta.`$tablePath`\"), Seq(Row(1)))\n      }\n      val getCommitsUsageLogs1 = filterUsageRecords(\n        usageLogs1,\n        CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_CLIENT_GET_COMMITS)\n      val getCommitsEventData1 = JsonUtils.fromJson[Map[String, Any]](getCommitsUsageLogs1(0).blob)\n      assert(getCommitsEventData1(\"startVersion\") === 0)\n      assert(getCommitsEventData1(\"versionToLoad\") === -1)\n      assert(getCommitsEventData1(\"async\") === \"true\")\n      assert(getCommitsEventData1(\"responseCommitsSize\") === 0)\n      assert(getCommitsEventData1(\"responseLatestTableVersion\") === -1)\n\n      Seq(2).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // version 1\n      Seq(3).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 2\n      DeltaLog.clearCache()\n      commitCoordinatorClient.numGetCommitsCalled.set(0)\n      import testImplicits._\n      val result1 = sql(s\"SELECT * FROM delta.`$tablePath`\").collect()\n      assert(result1.length === 2 && result1.toSet === Set(Row(2), Row(3)))\n      assert(commitCoordinatorClient.numGetCommitsCalled.get === 2)\n    }\n  }\n\n  // Test commit-coordinator changed on concurrent cluster\n  testWithDefaultCommitCoordinatorUnset(\"snapshot is updated recursively when FS table\" +\n      \" is converted to commit-coordinator table on a concurrent cluster\") {\n    if (isCatalogOwnedTest) {\n      // TODO: CatalogOwned table cannot change its catalog, hence modify below to\n      // test race upgrade from normal table after implementing upgrade.\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    val commitCoordinatorClient =\n      new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize = 10))\n    val builder =\n      TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10, Some(commitCoordinatorClient))\n    registerBuilder(builder)\n\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val deltaLog1 = DeltaLog.forTable(spark, tablePath)\n      deltaLog1.startTransaction().commitManually(Metadata())\n      deltaLog1.startTransaction().commitManually(createTestAddFile(\"f1\"))\n      deltaLog1.startTransaction().commitManually()\n      val snapshotV2 = deltaLog1.update()\n      assert(snapshotV2.version === 2)\n      assert(snapshotV2.tableCommitCoordinatorClientOpt.isEmpty)\n      DeltaLog.clearCache()\n\n      // Add new commit to convert FS table to coordinated-commits table\n      val deltaLog2 = DeltaLog.forTable(spark, tablePath)\n      upgradeLogWithCCTableFeature(deltaLog2, commitCoordinator = \"tracking-in-memory\")\n      deltaLog2.startTransaction().commitManually(createTestAddFile(\"f2\"))\n      deltaLog2.startTransaction().commitManually()\n      val snapshotV5 = deltaLog2.unsafeVolatileSnapshot\n      assert(snapshotV5.version === 5)\n      assert(snapshotV5.tableCommitCoordinatorClientOpt.nonEmpty)\n      // only delta 4/5 will be un-backfilled and should have two dots in filename (x.uuid.json)\n      assert(snapshotV5.logSegment.deltas.count(_.getPath.getName.count(_ == '.') == 2) === 2)\n\n      val usageRecords = Log4jUsageLogger.track {\n        val newSnapshotV5 = deltaLog1.update()\n        assert(newSnapshotV5.version === 5)\n        assert(newSnapshotV5.logSegment.deltas === snapshotV5.logSegment.deltas)\n      }\n      assert(filterUsageRecords(usageRecords, \"delta.readChecksum\").size === 2)\n    }\n  }\n\n  test(\"update works correctly with InitialSnapshot\") {\n    registerBuilder(\n      TrackingInMemoryCommitCoordinatorBuilder(batchSize = 2))\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val clock = new ManualClock(System.currentTimeMillis())\n      val log = DeltaLog.forTable(spark, new Path(tablePath), clock)\n      assert(log.unsafeVolatileSnapshot.isInstanceOf[DummySnapshot])\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.isEmpty)\n      assert(log.getCapturedSnapshot().updateTimestamp == clock.getTimeMillis())\n      clock.advance(500)\n      log.update()\n      assert(log.unsafeVolatileSnapshot.isInstanceOf[DummySnapshot])\n      assert(log.getCapturedSnapshot().updateTimestamp == clock.getTimeMillis())\n    }\n  }\n\n  // This test has the following setup:\n  // 1. Table is created with CS1 as commit-coordinator.\n  // 2. Do another commit (v1) on table.\n  // 3. Take a reference to current DeltaLog and clear the cache. This deltaLog object currently\n  //    points to the latest table snapshot i.e. v1.\n  // 4. Do commit v2 on the table.\n  // 5. Do commit v3 on table. As part of this, change commit-coordinator to FS. Do v4 on table and\n  //    change owner to CS2.\n  // 6. Do commit v5 on table. This will happen via CS2.\n  // 7. Invoke deltaLog.update() on the old deltaLog object which is still pointing to v1.\n  //    - While doing this, we will inject failure in CS2 so that it fails twice when cs2.getCommits\n  //      is called.\n  //    - Because of this old delta log will firstly contact cs1 and get newer commits i.e. v2/v3\n  //      and create a snapshot out of it. Then it will contact cs2 and fail. So deltaLog.update()\n  //      won't succeed and throw exception. It also won't install the intermediate snapshot. The\n  //      older delta log will still be pointing to v1.\n  // 8. Invoke deltaLog.update() two more times. 3rd attempt will succeed.\n  //    - the recorded timestamp for this should be clock timestamp.\n  test(\"failures inside getCommits, correct timestamp is added in CapturedSnapshot\") {\n    if (isCatalogOwnedTest) {\n      // TODO: This test is important to test the robustness of the ability to resolve\n      // stale snapshot status interaction with upgrade/downgrade. Implement this suite\n      // for catalog owned tables once we enable upgrade.\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    val batchSize = 10\n    val cs1 = new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize))\n    val cs2 = new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize)) {\n      var failAttempts = Set[Int]()\n\n      override def getCommits(\n          tableDesc: TableDescriptor,\n          startVersion: java.lang.Long,\n          endVersion: java.lang.Long): JGetCommitsResponse = {\n        if (failAttempts.contains(numGetCommitsCalled.get + 1)) {\n          numGetCommitsCalled.incrementAndGet()\n          throw new IllegalStateException(\"Injected failure\")\n        }\n        super.getCommits(tableDesc, startVersion, endVersion)\n      }\n    }\n    case class TrackingInMemoryCommitCoordinatorClientBuilder(\n        name: String,\n        commitCoordinatorClient: CommitCoordinatorClient) extends CommitCoordinatorBuilder {\n      override def build(\n          spark: SparkSession,\n          conf: Map[String, String]): CommitCoordinatorClient = commitCoordinatorClient\n      override def getName: String = name\n    }\n    val builder1 = TrackingInMemoryCommitCoordinatorClientBuilder(name = \"in-memory-1\", cs1)\n    val builder2 = TrackingInMemoryCommitCoordinatorClientBuilder(name = \"in-memory-2\", cs2)\n    Seq(builder1, builder2).foreach(CommitCoordinatorProvider.registerBuilder)\n\n    def resetMetrics(): Unit = {\n      cs1.reset()\n      cs2.reset()\n      cs2.failAttempts = Set()\n    }\n\n    withSQLConf(COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey -> \"in-memory-1\") {\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        // Step-1\n        Seq(0).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 0\n        DeltaLog.clearCache()\n\n        // Step-2\n        Seq(1).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 1\n\n        // Step-3\n        DeltaLog.clearCache()\n        val clock = new ManualClock(System.currentTimeMillis())\n        val oldDeltaLog = DeltaLog.forTable(spark, new Path(tablePath), clock)\n        DeltaLog.clearCache()\n\n        // Step-4\n        Seq(2).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 2\n\n        // Step-5\n        val log = DeltaLog.forTable(spark, new Path(tablePath), clock)\n        val oldMetadata = log.update().metadata\n        assert(log.unsafeVolatileSnapshot.version === 2)\n        val oldMetadataConf = log.update().metadata.configuration\n        val newMetadata1 = oldMetadata.copy(\n          configuration = oldMetadataConf - COORDINATED_COMMITS_COORDINATOR_NAME.key)\n        val newMetadata2 = oldMetadata.copy(\n          configuration = oldMetadataConf + (\n            COORDINATED_COMMITS_COORDINATOR_NAME.key -> \"in-memory-2\"))\n        assert(log.update().tableCommitCoordinatorClientOpt.get.commitCoordinatorClient === cs1)\n        log.startTransaction().commitManually(newMetadata1) // version 3\n        (1 to 3).foreach { v =>\n          // backfill commit 1 and 2 also as 3/4 are written directly to FS.\n          val segment = log.unsafeVolatileSnapshot.logSegment\n          log.store.write(\n            path = FileNames.unsafeDeltaFile(log.logPath, v),\n            actions = log.store.read(segment.deltas(v).getPath).toIterator,\n            overwrite = true)\n        }\n        assert(log.update().tableCommitCoordinatorClientOpt === None)\n        log.startTransaction().commitManually(newMetadata2) // version 4\n        assert(log.update().tableCommitCoordinatorClientOpt.get.commitCoordinatorClient === cs2)\n\n        // Step-6\n        Seq(4).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 5\n        assert(log.unsafeVolatileSnapshot.version === 5L)\n        assert(FileNames.deltaVersion(log.unsafeVolatileSnapshot.logSegment.deltas(5)) === 5)\n\n        // Step-7\n        // Invoke deltaLog.update() on older copy of deltaLog which is still pointing to version 1\n        // Attempt-1\n        assert(oldDeltaLog.unsafeVolatileSnapshot.version === 1)\n        clock.setTime(System.currentTimeMillis())\n        resetMetrics()\n        cs2.failAttempts = Set(1, 2) // fail 0th and 1st attempt, 2nd attempt will succeed.\n        val ex1 = intercept[CommitCoordinatorGetCommitsFailedException] { oldDeltaLog.update() }\n        assert((cs1.numGetCommitsCalled.get, cs2.numGetCommitsCalled.get) === (1, 1))\n        assert(ex1.getMessage.contains(\"Injected failure\"))\n        assert(oldDeltaLog.unsafeVolatileSnapshot.version == 1)\n        assert(oldDeltaLog.getCapturedSnapshot().updateTimestamp != clock.getTimeMillis())\n\n        // Attempt-2\n        // 2nd update also fails\n        val ex2 = intercept[CommitCoordinatorGetCommitsFailedException] { oldDeltaLog.update() }\n        assert((cs1.numGetCommitsCalled.get, cs2.numGetCommitsCalled.get) === (2, 2))\n        assert(ex2.getMessage.contains(\"Injected failure\"))\n        assert(oldDeltaLog.unsafeVolatileSnapshot.version == 1)\n        assert(oldDeltaLog.getCapturedSnapshot().updateTimestamp != clock.getTimeMillis())\n\n        // Attempt-3: 3rd update succeeds\n        clock.advance(500)\n        assert(oldDeltaLog.update().version === 5)\n        assert((cs1.numGetCommitsCalled.get, cs2.numGetCommitsCalled.get) === (3, 3))\n        assert(oldDeltaLog.getCapturedSnapshot().updateTimestamp == clock.getTimeMillis())\n      }\n    }\n  }\n\n  testWithDifferentBackfillInterval(\"post commit snapshot creation\") { backfillInterval =>\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n\n      def getDeltasInPostCommitSnapshot(log: DeltaLog): Seq[String] = {\n        log\n          .unsafeVolatileSnapshot\n          .logSegment.deltas\n          .map(_.getPath.getName.replace(\"0000000000000000000\", \"\"))\n      }\n\n      // Commit 0\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      val log = DeltaLog.forTable(spark, tablePath)\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\"))\n      assert(log.unsafeVolatileSnapshot.getLastKnownBackfilledVersion == 0)\n      var snapshot = log.update()\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\"))\n      assert(snapshot.getLastKnownBackfilledVersion == 0)\n\n      // Commit 1\n      Seq(2).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 1\n      // Note: The assert for backfillInterval = 1 only works because with a batch\n      // size of 1, the AbstractBatchBackfillingCommitCoordinator synchronously\n      // backfills a commit and then directly returns the backfilled commit information\n      // from the commit() call so the post commit snapshot creation directly appends\n      // the backfilled commit to the LogSegment.\n      //\n      // The expected behavior before update() is:\n      // Backfill interval 1 : post commit snapshot contains 0.json, 1.json, lkbv = 1\n      // Backfill interval 2 : post commit snapshot contains 0.json, 1.uuid.json, lkbv = 0\n      // Backfill interval 10: post commit snapshot contains 0.json, 1.uuid.json, lkbv = 0\n      val commit1 = if (backfillInterval < 2) \"1.json\" else \"1.uuid-1.json\"\n      var backfillVersion = if (backfillInterval < 2) 1 else 0\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\", commit1))\n      assert(log.unsafeVolatileSnapshot.getLastKnownBackfilledVersion == backfillVersion)\n      snapshot = log.update()\n      // The expected behavior after update() is:\n      // Backfill interval 1 : post commit snapshot contains 0.json, 1.json, lkbv = 1\n      // Backfill interval 2 : post commit snapshot contains 0.json, 1.uuid.json, lkbv = 0\n      // Backfill interval 10: post commit snapshot contains 0.json, 1.uuid.json, lkbv = 0\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\", commit1))\n      assert(snapshot.getLastKnownBackfilledVersion == backfillVersion)\n\n      // Commit 2\n      Seq(3).toDF.write.format(\"delta\").mode(\"append\").save(tablePath) // version 2\n      // The expected behavior before update() is:\n      // Backfill interval 1 : post commit snapshot contains\n      //   0.json, 1.json, 2.json lkbv = 2\n      // Backfill interval 2 : post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json lkbv = 0\n      // Backfill interval 10: post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json, lkbv = 0\n      val commit2 = if (backfillInterval < 2) \"2.json\" else \"2.uuid-2.json\"\n      backfillVersion = if (backfillInterval < 2) 2 else 0\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\", commit1, commit2))\n      assert(log.unsafeVolatileSnapshot.getLastKnownBackfilledVersion == backfillVersion)\n      snapshot = log.update()\n      // backfill would have happened at commit 2 for batchSize = 2 but we do not swap\n      // the snapshot that contains the unbackfilled commits with the updated snapshot\n      // (which contains the backfilled commits) during update because they are identical\n      // and swapping would lead to losing the cached state. However, we update the\n      // effective last known backfilled version on the snapshot.\n      //\n      // The expected behavior after update is\n      // Backfill interval 1 : post commit snapshot contains\n      //   0.json, 1.json, 2.json lkbv = 2\n      // Backfill interval 2 : post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json lkbv = 0 lkbv = 2\n      // Backfill interval 10: post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json lkbv = 0\n      backfillVersion = if (backfillInterval <= 2) 2 else 0\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\", commit1, commit2))\n      assert(snapshot.getLastKnownBackfilledVersion == backfillVersion)\n\n      // Commit 3\n      Seq(4).toDF.write.format(\"delta\").mode(\"append\").save(tablePath)\n      val commit3 = if (backfillInterval < 2) \"3.json\" else \"3.uuid-3.json\"\n      // The post commit snapshot is a new snapshot and so its lastKnownBackfilledVersion\n      // member is calculated from the LogSegment. Given that the LogSegment for\n      // batchInterval > 1 only contains 0 as the only backfilled commit, we need\n      // to set the expected backfillVersion to 0 here for backfillIntervals > 1.\n      //\n      // The expected behavior before update() is:\n      // Backfill interval 1 : post commit snapshot contains\n      //   0.json, 1.json, 2.json, 3.json lkbv = 3\n      // Backfill interval 2 : post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json, 3.uuid.json lkbv = 0\n      // Backfill interval 10: post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json, 3.uuid.json lkbv = 0\n      backfillVersion = if (backfillInterval < 2) 3 else 0\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\", commit1, commit2, commit3))\n      assert(log.unsafeVolatileSnapshot.getLastKnownBackfilledVersion == backfillVersion)\n      snapshot = log.update()\n      // The expected behavior after update() is:\n      // Backfill interval 1 : post commit snapshot contains\n      //   0.json, 1.json, 2.json, 3.json lkbv = 3\n      // Backfill interval 2 : post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json, 3.uuid.json lkbv = 2\n      // Backfill interval 10: post commit snapshot contains\n      //   0.json, 1.uuid.json, 2.uuid.json, 3.uuid.json lkbv = 0\n      backfillVersion = if (backfillInterval < 2) 3 else if (backfillInterval == 2) 2 else 0\n      assert(getDeltasInPostCommitSnapshot(log) === Seq(\"0.json\", commit1, commit2, commit3))\n      assert(snapshot.getLastKnownBackfilledVersion == backfillVersion)\n\n      checkAnswer(sql(s\"SELECT * FROM delta.`$tablePath`\"), Seq(Row(1), Row(2), Row(3), Row(4)))\n    }\n  }\n\n  testWithDifferentBackfillInterval(\"Snapshot.ensureCommitFilesBackfilled\") { _ =>\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n\n      // Add 10 commits to the table\n      Seq(1).toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      2 to 10 foreach { i =>\n        Seq(i).toDF().write.format(\"delta\").mode(\"append\").save(tablePath)\n      }\n      val log = DeltaLog.forTable(spark, tablePath)\n      val snapshot = log.update()\n      snapshot.ensureCommitFilesBackfilled()\n\n      val commitFiles = log.listFrom(0).filter(FileNames.isDeltaFile).map(_.getPath)\n      val backfilledCommitFiles = (0 to 9).map(\n        version => FileNames.unsafeDeltaFile(log.logPath, version))\n      assert(commitFiles.toSeq == backfilledCommitFiles)\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\"DeltaLog.getSnapshotAt\") {\n    val commitCoordinatorClient =\n      new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize = 10))\n    val builder =\n      TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10, Some(commitCoordinatorClient))\n    registerBuilder(builder)\n    def checkGetSnapshotAt(\n        deltaLog: DeltaLog,\n        version: Long,\n        expectedUpdateCount: Int,\n        expectedListingCount: Int): Snapshot = {\n      var snapshot: Snapshot = null\n\n      val usageRecords = Log4jUsageLogger.track {\n        snapshot = deltaLog.getSnapshotAt(version)\n        assert(snapshot.version === version)\n      }\n      assert(filterUsageRecords(usageRecords, \"deltaLog.update\").size === expectedUpdateCount)\n      // deltaLog.update() will internally do listing\n      assert(filterUsageRecords(usageRecords, \"delta.deltaLog.listDeltaAndCheckpointFiles\").size\n        === expectedListingCount)\n      val versionsInLogSegment = if (version < 6) {\n        snapshot.logSegment.deltas.map(FileNames.deltaVersion(_))\n      } else {\n        snapshot.logSegment.deltas.flatMap {\n          case DeltaFile(_, deltaVersion) => Seq(deltaVersion)\n          case CompactedDeltaFile(_, startVersion, endVersion) => (startVersion to endVersion)\n        }\n      }\n      assert(versionsInLogSegment === (0L to version))\n      snapshot\n    }\n\n    withTempDir { dir =>\n      val tablePath = dir.getAbsolutePath\n      // Part-1: Validate getSnapshotAt API works as expected for non-coordinated commits tables\n      // commit 0, 1, 2 on FS table\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v0\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v1\n      val deltaLog1 = DeltaLog.forTable(spark, tablePath)\n      DeltaLog.clearCache()\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v2\n      assert(deltaLog1.unsafeVolatileSnapshot.version === 1)\n\n      checkGetSnapshotAt(deltaLog1, version = 1, expectedUpdateCount = 0, expectedListingCount = 0)\n      // deltaLog1 still points to version 1. So, we will do listing to get v0.\n      checkGetSnapshotAt(deltaLog1, version = 0, expectedUpdateCount = 0, expectedListingCount = 1)\n      // deltaLog1 still points to version 1 although we are asking for v2 So we do a\n      // deltaLog.update - the update will internally do listing.Since the updated snapshot is same\n      // as what we want, so we won't create another snapshot and do another listing.\n      checkGetSnapshotAt(deltaLog1, version = 2, expectedUpdateCount = 1, expectedListingCount = 1)\n      var deltaLog2 = DeltaLog.forTable(spark, tablePath)\n      Seq(deltaLog1, deltaLog2).foreach { log => assert(log.unsafeVolatileSnapshot.version === 2) }\n      DeltaLog.clearCache()\n\n      // Part-2: Validate getSnapshotAt API works as expected for coordinated commits tables when\n      // the switch is made\n      // commit 3\n      upgradeLogWithCCTableFeature(DeltaLog.forTable(spark, tablePath), \"tracking-in-memory\")\n      // commit 4\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      // the old deltaLog objects still points to version 2\n      Seq(deltaLog1, deltaLog2).foreach { log => assert(log.unsafeVolatileSnapshot.version === 2) }\n      // deltaLog1 points to version 2. So, we will do listing to get v1. Snapshot update not\n      // needed as what we are looking for is less than what deltaLog1 points to.\n      checkGetSnapshotAt(deltaLog1, version = 1, expectedUpdateCount = 0, expectedListingCount = 1)\n      // deltaLog1.unsafeVolatileSnapshot.version points to v2 - return it directly.\n      checkGetSnapshotAt(deltaLog1, version = 2, expectedUpdateCount = 0, expectedListingCount = 0)\n      // We are asking for v3 although the deltaLog1.unsafeVolatileSnapshot is for v2. So this will\n      // need deltaLog.update() to get the latest snapshot first - this update itself internally\n      // will do 2 round of listing as we are discovering a commit-coordinator after first round of\n      // listing. Once the update finishes, deltaLog1 will point to v4. So we need another round of\n      // listing to get just v3.\n      checkGetSnapshotAt(deltaLog1, version = 3, expectedUpdateCount = 1, expectedListingCount = 3)\n      // Ask for v3 again - this time deltaLog1.unsafeVolatileSnapshot points to v4.\n      // So we don't need deltaLog.update as version which we are asking is less than pinned\n      // version. Just do listing and get the snapshot.\n      checkGetSnapshotAt(deltaLog1, version = 3, expectedUpdateCount = 0, expectedListingCount = 1)\n      // deltaLog1.unsafeVolatileSnapshot.version points to v4 - return it directly.\n      checkGetSnapshotAt(deltaLog1, version = 4, expectedUpdateCount = 0, expectedListingCount = 0)\n      // We are asking for v3 although the deltaLog2.unsafeVolatileSnapshot is for v2. So this will\n      // need deltaLog.update() to get the latest snapshot first - this update itself internally\n      // will do 2 round of listing as we are discovering a commit-coordinator after first round of\n      // listing. Once the update finishes, deltaLog2 will point to v4. It can be returned directly.\n      checkGetSnapshotAt(deltaLog2, version = 4, expectedUpdateCount = 1, expectedListingCount = 2)\n\n      // Part-2: Validate getSnapshotAt API works as expected for coordinated commits tables\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v5\n      deltaLog2 = DeltaLog.forTable(spark, tablePath)\n      DeltaLog.clearCache()\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v6\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v7\n      assert(deltaLog2.unsafeVolatileSnapshot.version === 5)\n      checkGetSnapshotAt(deltaLog2, version = 1, expectedUpdateCount = 0, expectedListingCount = 1)\n      checkGetSnapshotAt(deltaLog2, version = 2, expectedUpdateCount = 0, expectedListingCount = 1)\n      checkGetSnapshotAt(deltaLog2, version = 4, expectedUpdateCount = 0, expectedListingCount = 1)\n      checkGetSnapshotAt(deltaLog2, version = 5, expectedUpdateCount = 0, expectedListingCount = 0)\n      checkGetSnapshotAt(deltaLog2, version = 6, expectedUpdateCount = 1, expectedListingCount = 2)\n    }\n  }\n\n  private def upgradeLogWithCCTableFeature(deltaLog: DeltaLog, commitCoordinator: String): Unit = {\n    if (isCatalogOwnedTest) {\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    val oldMetadata = deltaLog.update().metadata\n    val commitCoordinatorConf = (COORDINATED_COMMITS_COORDINATOR_NAME.key -> commitCoordinator)\n    val newMetadata =\n      oldMetadata.copy(configuration = oldMetadata.configuration + commitCoordinatorConf)\n    deltaLog.startTransaction().commitManually(newMetadata)\n  }\n\n  for (upgradeExistingTable <- BOOLEAN_DOMAIN)\n  testWithDifferentBackfillInterval(\"upgrade + downgrade [FS -> CC1 -> FS -> CC2],\" +\n      s\" upgradeExistingTable = $upgradeExistingTable\") { backfillInterval =>\n    if (isCatalogOwnedTest) {\n      // TODO: Once upgrade is supported, this unit test can only test\n      // first upgrade part (FS -> CC1) because there is no CC1 -> FS -> CC2 transition\n      // in CatalogOwned table feature. Note that only one Catalog can exist for\n      // each table identifier.\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    withoutDefaultCCTableFeature {\n      clearBuilders()\n      val builder1 = TrackingInMemoryCommitCoordinatorBuilder(batchSize = backfillInterval)\n      val builder2 = new TrackingInMemoryCommitCoordinatorBuilder(batchSize = backfillInterval) {\n        override def getName: String = \"tracking-in-memory-2\"\n      }\n\n      Seq(builder1, builder2).foreach(registerBuilder(_))\n      val cs1 = builder1\n        .trackingInMemoryCommitCoordinatorClient\n        .asInstanceOf[TrackingCommitCoordinatorClient]\n      val cs2 = builder2\n        .trackingInMemoryCommitCoordinatorClient\n        .asInstanceOf[TrackingCommitCoordinatorClient]\n\n      withTempDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        val log = DeltaLog.forTable(spark, tablePath)\n        val fs = log.logPath.getFileSystem(log.newDeltaHadoopConf())\n\n        var upgradeStartVersion = 0L\n        // Create a non-coordinated commits table if we are testing upgrade for existing tables\n        if (upgradeExistingTable) {\n          log.startTransaction().commitManually(Metadata())\n          assert(log.unsafeVolatileSnapshot.version === 0)\n          log.startTransaction().commitManually(createTestAddFile(\"1\"))\n          assert(log.unsafeVolatileSnapshot.version === 1)\n          assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.isEmpty)\n          upgradeStartVersion = 2L\n        }\n\n        // Upgrade the table\n        // [upgradeExistingTable = false] Commit-0\n        // [upgradeExistingTable = true] Commit-2\n        val commitCoordinatorConf =\n          Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder1.getName)\n        val newMetadata = Metadata().copy(configuration = commitCoordinatorConf)\n        val usageLogs1 = Log4jUsageLogger.track {\n          log.startTransaction().commitManually(newMetadata)\n        }\n        assert(log.unsafeVolatileSnapshot.version === upgradeStartVersion)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName ===\n          Some(builder1.getName))\n        assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n        // upgrade commit always filesystem based\n        assert(fs.exists(FileNames.unsafeDeltaFile(log.logPath, upgradeStartVersion)))\n        assert(Seq(cs1, cs2).map(_.numCommitsCalled.get) == Seq(0, 0))\n        assert(Seq(cs1, cs2).map(_.numRegisterTableCalled.get) == Seq(1, 0))\n        // Check usage logs for upgrade commit\n        val commitStatsUsageLogs1 = filterUsageRecords(usageLogs1, \"delta.commit.stats\")\n        val commitStats1 = JsonUtils.fromJson[CommitStats](commitStatsUsageLogs1.head.blob)\n        assert(commitStats1.coordinatedCommitsInfo ===\n          CoordinatedCommitsStats(FS_TO_CC_UPGRADE_COMMIT.toString, builder1.getName, Map.empty))\n\n        // Do couple of commits on the coordinated-commits table\n        // [upgradeExistingTable = false] Commit-1/2\n        // [upgradeExistingTable = true] Commit-3/4\n        (1 to 2).foreach { versionOffset =>\n          val version = upgradeStartVersion + versionOffset\n          val usageLogs2 = Log4jUsageLogger.track {\n            log.startTransaction().commitManually(createTestAddFile(s\"$versionOffset\"))\n          }\n          assert(log.unsafeVolatileSnapshot.version === version)\n          assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n          assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName.nonEmpty)\n          assert(\n            log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorConf === Map.empty)\n          assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n          assert(cs1.numCommitsCalled.get === versionOffset)\n          val backfillExpected = if (version % backfillInterval == 0) true else false\n          assert(fs.exists(FileNames.unsafeDeltaFile(log.logPath, version)) == backfillExpected)\n          // Check usage logs for INSERT commits on this coordinated-commits table.\n          val commitStatsUsageLogs2 = filterUsageRecords(usageLogs2, \"delta.commit.stats\")\n          val commitStats2 = JsonUtils.fromJson[CommitStats](commitStatsUsageLogs2.head.blob)\n          assert(commitStats2.coordinatedCommitsInfo ===\n            CoordinatedCommitsStats(CC_COMMIT.toString, builder1.getName, Map.empty))\n        }\n\n        // Downgrade the table\n        // [upgradeExistingTable = false] Commit-3\n        // [upgradeExistingTable = true] Commit-5\n        val commitCoordinatorConfKeys = Seq(\n          COORDINATED_COMMITS_COORDINATOR_NAME.key,\n          COORDINATED_COMMITS_COORDINATOR_CONF.key,\n          COORDINATED_COMMITS_TABLE_CONF.key\n        )\n        val newConfig = log.snapshot.metadata.configuration\n          .filterKeys(!commitCoordinatorConfKeys.contains(_)) ++ Map(\"downgraded_at\" -> \"v2\")\n        val newMetadata2 = log.snapshot.metadata.copy(configuration = newConfig.toMap)\n        val usageLogs3 = Log4jUsageLogger.track {\n          log.startTransaction().commitManually(newMetadata2)\n        }\n        assert(log.unsafeVolatileSnapshot.version === upgradeStartVersion + 3)\n        assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.isEmpty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName.isEmpty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorConf === Map.empty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n        assert(log.unsafeVolatileSnapshot.metadata === newMetadata2)\n        // This must have increased by 1 as downgrade commit happens via CommitCoordinatorClient.\n        assert(Seq(cs1, cs2).map(_.numCommitsCalled.get) == Seq(3, 0))\n        assert(Seq(cs1, cs2).map(_.numRegisterTableCalled.get) == Seq(1, 0))\n        (0 to 3).foreach { version =>\n          assert(fs.exists(FileNames.unsafeDeltaFile(log.logPath, version)))\n        }\n        // Check usage logs for downgrade commit\n        val commitStatsUsageLogs3 = filterUsageRecords(usageLogs3, \"delta.commit.stats\")\n        val commitStats3 = JsonUtils.fromJson[CommitStats](commitStatsUsageLogs3.head.blob)\n        assert(commitStats3.coordinatedCommitsInfo ===\n          CoordinatedCommitsStats(CC_TO_FS_DOWNGRADE_COMMIT.toString, builder1.getName, Map.empty))\n\n\n        // Do commit after downgrade is over\n        // [upgradeExistingTable = false] Commit-4\n        // [upgradeExistingTable = true] Commit-6\n        val usageLogs4 = Log4jUsageLogger.track {\n          log.startTransaction().commitManually(createTestAddFile(\"post-upgrade-file\"))\n        }\n        assert(log.unsafeVolatileSnapshot.version === upgradeStartVersion + 4)\n        // no commit-coordinator after downgrade\n        assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.isEmpty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n        // Metadata is same as what we added at time of downgrade\n        assert(log.unsafeVolatileSnapshot.metadata === newMetadata2)\n        // State reconstruction should give correct results\n        var expectedFileNames = Set(\"1\", \"2\", \"post-upgrade-file\")\n        assert(log.unsafeVolatileSnapshot.allFiles.collect().toSet ===\n          expectedFileNames.map(name => createTestAddFile(name, dataChange = false)))\n        // commit-coordinator should not be invoked for commit API.\n        // Register table API should not be called until the end\n        assert(Seq(cs1, cs2).map(_.numCommitsCalled.get) == Seq(3, 0))\n        assert(Seq(cs1, cs2).map(_.numRegisterTableCalled.get) == Seq(1, 0))\n        // 4th file is directly written to FS in backfilled way.\n        assert(fs.exists(FileNames.unsafeDeltaFile(log.logPath, upgradeStartVersion + 4)))\n        // Check usage logs for normal FS commit\n        val commitStatsUsageLogs4 = filterUsageRecords(usageLogs4, \"delta.commit.stats\")\n        val commitStats4 = JsonUtils.fromJson[CommitStats](commitStatsUsageLogs4.head.blob)\n        assert(commitStats4.coordinatedCommitsInfo ===\n          CoordinatedCommitsStats(FS_COMMIT.toString, \"NONE\", Map.empty))\n\n        // Now transfer the table to another commit-coordinator\n        // [upgradeExistingTable = false] Commit-5\n        // [upgradeExistingTable = true] Commit-7\n        val commitCoordinatorConf2 =\n          Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder2.getName)\n        val oldMetadata3 = log.unsafeVolatileSnapshot.metadata\n        val newMetadata3 = oldMetadata3.copy(\n          configuration = oldMetadata3.configuration ++ commitCoordinatorConf2)\n        val usageLogs5 = Log4jUsageLogger.track {\n          log.startTransaction().commitManually(newMetadata3, createTestAddFile(\"upgrade-2-file\"))\n        }\n        assert(log.unsafeVolatileSnapshot.version === upgradeStartVersion + 5)\n        assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName ===\n          Some(builder2.getName))\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorConf === Map.empty)\n        assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n        expectedFileNames = Set(\"1\", \"2\", \"post-upgrade-file\", \"upgrade-2-file\")\n        assert(log.unsafeVolatileSnapshot.allFiles.collect().toSet ===\n          expectedFileNames.map(name => createTestAddFile(name, dataChange = false)))\n        assert(Seq(cs1, cs2).map(_.numCommitsCalled.get) == Seq(3, 0))\n        assert(Seq(cs1, cs2).map(_.numRegisterTableCalled.get) == Seq(1, 1))\n        // Check usage logs for 2nd upgrade commit\n        val commitStatsUsageLogs5 = filterUsageRecords(usageLogs5, \"delta.commit.stats\")\n        val commitStats5 = JsonUtils.fromJson[CommitStats](commitStatsUsageLogs5.head.blob)\n        assert(commitStats5.coordinatedCommitsInfo ===\n          CoordinatedCommitsStats(FS_TO_CC_UPGRADE_COMMIT.toString, builder2.getName, Map.empty))\n\n        // Make 1 more commit, this should go to new owner\n        log.startTransaction().commitManually(newMetadata3, createTestAddFile(\"4\"))\n        expectedFileNames = Set(\"1\", \"2\", \"post-upgrade-file\", \"upgrade-2-file\", \"4\")\n        assert(log.unsafeVolatileSnapshot.allFiles.collect().toSet ===\n          expectedFileNames.map(name => createTestAddFile(name, dataChange = false)))\n        assert(Seq(cs1, cs2).map(_.numCommitsCalled.get) == Seq(3, 1))\n        assert(Seq(cs1, cs2).map(_.numRegisterTableCalled.get) == Seq(1, 1))\n        assert(log.unsafeVolatileSnapshot.version === upgradeStartVersion + 6)\n      }\n    }\n  }\n\n\n  testWithDefaultCommitCoordinatorUnset(\"FS -> CC upgrade is not retried on a conflict\") {\n    if (isCatalogOwnedTest) {\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    val builder = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10)\n    registerBuilder(builder)\n\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val log = DeltaLog.forTable(spark, tablePath)\n      val commitCoordinatorConf = Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder.getName)\n      val newMetadata = Metadata().copy(configuration = commitCoordinatorConf)\n      val txn = log.startTransaction() // upgrade txn started\n      log.startTransaction().commitManually(createTestAddFile(\"f1\"))\n      intercept[io.delta.exceptions.ConcurrentWriteException] {\n        txn.commitManually(newMetadata) // upgrade txn committed\n      }\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\"FS -> CC upgrade with commitLarge API\") {\n    if (isCatalogOwnedTest) {\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    val builder = TrackingInMemoryCommitCoordinatorBuilder(batchSize = 10)\n    val cs =\n      builder.trackingInMemoryCommitCoordinatorClient.asInstanceOf[TrackingCommitCoordinatorClient]\n    registerBuilder(builder)\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF.write.format(\"delta\").save(tablePath)\n      Seq(1).toDF.write.mode(\"overwrite\").format(\"delta\").save(tablePath)\n      var log = DeltaLog.forTable(spark, tablePath)\n      assert(log.unsafeVolatileSnapshot.version === 1L)\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.isEmpty)\n\n      val commitCoordinatorConf = Map(COORDINATED_COMMITS_COORDINATOR_NAME.key -> builder.getName)\n      val oldMetadata = log.unsafeVolatileSnapshot.metadata\n      val newMetadata = oldMetadata.copy(\n        configuration = oldMetadata.configuration ++ commitCoordinatorConf)\n      val oldProtocol = log.unsafeVolatileSnapshot.protocol\n      assert(!oldProtocol.readerAndWriterFeatures.contains(V2CheckpointTableFeature))\n      val newProtocol =\n        oldProtocol.copy(\n          minReaderVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION,\n          minWriterVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION,\n          readerFeatures =\n            Some(oldProtocol.readerFeatures.getOrElse(Set.empty) + V2CheckpointTableFeature.name),\n          writerFeatures =\n            Some(\n              oldProtocol.writerFeatures.getOrElse(Set.empty) + CoordinatedCommitsTableFeature.name)\n            )\n      assert(cs.numRegisterTableCalled.get === 0)\n      assert(cs.numCommitsCalled.get === 0)\n\n      val txn = log.startTransaction()\n      txn.updateMetadataForNewTable(newMetadata)\n      txn.commitLarge(\n        spark,\n        Seq(SetTransaction(\"app-1\", 1, None)).toIterator,\n        Some(newProtocol),\n        DeltaOperations.TestOperation(\"TEST\"),\n        Map.empty,\n        Map.empty)\n      log = DeltaLog.forTable(spark, tablePath)\n      assert(cs.numRegisterTableCalled.get === 1)\n      assert(cs.numCommitsCalled.get === 0)\n      assert(log.unsafeVolatileSnapshot.version === 2L)\n\n      Seq(V2CheckpointTableFeature, CoordinatedCommitsTableFeature).foreach { feature =>\n        assert(log.unsafeVolatileSnapshot.protocol.isFeatureSupported(feature))\n      }\n\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorName ===\n        Some(builder.getName))\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsCoordinatorConf === Map.empty)\n      assert(log.unsafeVolatileSnapshot.metadata.coordinatedCommitsTableConf === Map.empty)\n\n      Seq(3).toDF.write.mode(\"append\").format(\"delta\").save(tablePath)\n      assert(cs.numRegisterTableCalled.get === 1)\n      assert(cs.numCommitsCalled.get === 1)\n      assert(log.unsafeVolatileSnapshot.version === 3L)\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n\n    }\n  }\n\n  test(\"Incomplete backfills are handled properly by next commit after CC to FS conversion\") {\n    if (isCatalogOwnedTest) {\n      cancel(\"Downgrade is not yet supported for catalog owned tables\")\n    }\n    val batchSize = 10\n    val neverBackfillingCommitCoordinator =\n      new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(batchSize) {\n        override def backfillToVersion(\n          logStore: LogStore,\n          hadoopConf: Configuration,\n          tableDesc: TableDescriptor,\n          version: Long,\n          lastKnownBackfilledVersionOpt: JLong): Unit = { }\n      })\n    clearBuilders()\n    val builder =\n      TrackingInMemoryCommitCoordinatorBuilder(batchSize, Some(neverBackfillingCommitCoordinator))\n    registerBuilder(builder)\n\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v0\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v1\n      Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath) // v2\n\n      val log = DeltaLog.forTable(spark, tablePath)\n      assert(log.unsafeVolatileSnapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n      assert(log.unsafeVolatileSnapshot.version === 2)\n      assert(\n        log.unsafeVolatileSnapshot.logSegment.deltas.count(FileNames.isUnbackfilledDeltaFile) == 2)\n\n      val oldMetadata = log.unsafeVolatileSnapshot.metadata\n      val downgradeMetadata = oldMetadata.copy(\n        configuration = oldMetadata.configuration - COORDINATED_COMMITS_COORDINATOR_NAME.key)\n      log.startTransaction().commitManually(downgradeMetadata)\n      log.update()\n      val snapshotAfterDowngrade = log.unsafeVolatileSnapshot\n      assert(snapshotAfterDowngrade.version === 3)\n      assert(snapshotAfterDowngrade.tableCommitCoordinatorClientOpt.isEmpty)\n      assert(snapshotAfterDowngrade.logSegment.deltas.count(FileNames.isUnbackfilledDeltaFile) == 3)\n\n      val records = Log4jUsageLogger.track {\n        // commit 4\n        Seq(1).toDF.write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n      }\n      val filteredUsageLogs = filterUsageRecords(\n        records, \"delta.coordinatedCommits.backfillWhenCoordinatedCommitsSupportedAndDisabled\")\n      assert(filteredUsageLogs.size === 1)\n      val usageObj = JsonUtils.fromJson[Map[String, Any]](filteredUsageLogs.head.blob)\n      assert(usageObj(\"numUnbackfilledFiles\").asInstanceOf[Int] === 3)\n      assert(usageObj(\"numAlreadyBackfilledFiles\").asInstanceOf[Int] === 0)\n    }\n  }\n\n  test(\"LogSegment comparison does not swap snapshots that only differ in \" +\n    \"backfilled/unbackfilled commits\") {\n    // Use a batch size of two so we don't immediately backfill in\n    // the AbstractBatchBackfillingCommitCoordinatorClient and so the\n    // CommitResponse contains the UUID-based commit.\n    registerBuilder(\n      TrackingInMemoryCommitCoordinatorBuilder(batchSize = 2))\n\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val log = DeltaLog.forTable(spark, tablePath)\n\n      withSQLConf(\n        CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.Classic.name) {\n        // Version 0 -- backfilled by default\n        makeCommitAndAssertSnapshotState(\n          data = Seq(0),\n          expectedLastKnownBackfilledVersion = 0,\n          expectedNumUnbackfilledCommits = 0,\n          expectedLastKnownBackfilledFile = FileNames.unsafeDeltaFile(log.logPath, 0),\n          log, tablePath)\n        // Version 1 -- not backfilled immediately because of batchSize = 2\n        makeCommitAndAssertSnapshotState(\n          data = Seq(1),\n          expectedLastKnownBackfilledVersion = 0,\n          expectedNumUnbackfilledCommits = 1,\n          expectedLastKnownBackfilledFile = FileNames.unsafeDeltaFile(log.logPath, 0),\n          log, tablePath)\n        // Version 2 -- backfills versions 1 and 2\n        makeCommitAndAssertSnapshotState(\n          data = Seq(2),\n          expectedLastKnownBackfilledVersion = 2,\n          expectedNumUnbackfilledCommits = 2,\n          expectedLastKnownBackfilledFile = FileNames.unsafeDeltaFile(log.logPath, 0),\n          log, tablePath)\n        // Version 3 -- not backfilled immediately because of batchSize = 2\n        makeCommitAndAssertSnapshotState(\n          data = Seq(3),\n          expectedLastKnownBackfilledVersion = 2,\n          expectedNumUnbackfilledCommits = 3,\n          expectedLastKnownBackfilledFile = FileNames.unsafeDeltaFile(log.logPath, 0),\n          log, tablePath)\n        // Version 4 -- backfills versions 3 and 4\n        makeCommitAndAssertSnapshotState(\n          data = Seq(4),\n          expectedLastKnownBackfilledVersion = 4,\n          expectedNumUnbackfilledCommits = 4,\n          expectedLastKnownBackfilledFile = FileNames.unsafeDeltaFile(log.logPath, 0),\n          log, tablePath)\n        // Trigger a checkpoint\n        log.checkpoint(log.update())\n        // Version 5 -- not backfilled immediately because of batchSize 2\n        makeCommitAndAssertSnapshotState(\n          data = Seq(5),\n          expectedLastKnownBackfilledVersion = 4,\n          expectedNumUnbackfilledCommits = 1,\n          expectedLastKnownBackfilledFile = FileNames.checkpointFileSingular(log.logPath, 4),\n          log, tablePath)\n        // Version 6 -- backfills versions 5 and 6\n        makeCommitAndAssertSnapshotState(\n          data = Seq(6),\n          expectedLastKnownBackfilledVersion = 6,\n          expectedNumUnbackfilledCommits = 2,\n          expectedLastKnownBackfilledFile = FileNames.checkpointFileSingular(log.logPath, 4),\n          log, tablePath)\n      }\n    }\n  }\n\n  private def makeCommitAndAssertSnapshotState(\n      data: Seq[Long],\n      expectedLastKnownBackfilledVersion: Long,\n      expectedNumUnbackfilledCommits: Long,\n      expectedLastKnownBackfilledFile: Path,\n      log: DeltaLog,\n      tablePath: String): Unit = {\n    data.toDF().write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n    val snapshot = log.update()\n    val segment = snapshot.logSegment\n    var numUnbackfilledCommits = 0\n    segment.deltas.foreach {\n      case UnbackfilledDeltaFile(_, _, _) => numUnbackfilledCommits += 1\n      case _ => // do nothing\n    }\n    assert(snapshot.getLastKnownBackfilledVersion == expectedLastKnownBackfilledVersion)\n    assert(numUnbackfilledCommits == expectedNumUnbackfilledCommits)\n    val lastKnownBackfilledFile = CoordinatedCommitsUtils\n      .getLastBackfilledFile(segment.deltas).getOrElse(\n        segment.checkpointProvider.topLevelFiles.head\n      )\n    assert(lastKnownBackfilledFile.getPath == expectedLastKnownBackfilledFile,\n      s\"$lastKnownBackfilledFile did not equal $expectedLastKnownBackfilledFile\")\n  }\n\n  for (ignoreMissingCCImpl <- BOOLEAN_DOMAIN)\n  test(s\"missing coordinator implementation [ignoreMissingCCImpl = $ignoreMissingCCImpl]\") {\n    if (isCatalogOwnedTest) {\n      cancel(\"Error message is not yet customized for CatalogOwned table.\")\n    }\n    clearBuilders()\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(batchSize = 2))\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      Seq(0).toDF.write.format(\"delta\").save(tablePath)\n      (1 to 3).foreach { v =>\n        Seq(v).toDF.write.mode(\"append\").format(\"delta\").save(tablePath)\n      }\n      // The table has 3 backfilled commits [0, 1, 2] and 1 unbackfilled commit [3]\n      clearBuilders()\n\n      def getUsageLogsAndEnsurePresenceOfMissingCCImplLog(\n          expectedFailIfImplUnavailable: Boolean)(f: => Unit): Seq[UsageRecord] = {\n        val usageLogs = Log4jUsageLogger.track {\n          f\n        }\n        val filteredLogs = filterUsageRecords(\n          usageLogs, CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_MISSING_IMPLEMENTATION)\n        assert(filteredLogs.nonEmpty)\n        val usageObj = JsonUtils.fromJson[Map[String, Any]](filteredLogs.head.blob)\n        assert(usageObj(\"commitCoordinatorName\") === \"tracking-in-memory\")\n        assert(usageObj(\"registeredCommitCoordinators\") ===\n          CommitCoordinatorProvider.getRegisteredCoordinatorNames.mkString(\", \"))\n        assert(usageObj(\"failIfImplUnavailable\") === expectedFailIfImplUnavailable.toString)\n        usageLogs\n      }\n      withSQLConf(\n        DeltaSQLConf.COORDINATED_COMMITS_IGNORE_MISSING_COORDINATOR_IMPLEMENTATION.key ->\n          ignoreMissingCCImpl.toString) {\n        DeltaLog.clearCache()\n        if (!ignoreMissingCCImpl) {\n          getUsageLogsAndEnsurePresenceOfMissingCCImplLog(expectedFailIfImplUnavailable = true) {\n            val e = intercept[IllegalArgumentException] {\n              DeltaLog.forTable(spark, tablePath)\n            }\n            assert(e.getMessage.contains(\"Unknown commit-coordinator\"))\n          }\n        } else {\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          assert(deltaLog.snapshot.tableCommitCoordinatorClientOpt.isEmpty)\n          // This will create a stale deltaLog as the commit-coordinator is missing.\n          assert(deltaLog.snapshot.version === 2L)\n          DeltaLog.clearCache()\n          getUsageLogsAndEnsurePresenceOfMissingCCImplLog(expectedFailIfImplUnavailable = false) {\n            checkAnswer(spark.read.format(\"delta\").load(tablePath), Seq(0, 1, 2).toDF())\n          }\n          // Writes and checkpoints should still fail.\n          val createCheckpointFn = () => (deltaLog.checkpoint())\n          val writeDataFn =\n            () => Seq(4).toDF.write.format(\"delta\").mode(\"append\").save(tablePath)\n          for (tableMutationFn <- Seq(createCheckpointFn, writeDataFn)) {\n            DeltaLog.clearCache()\n            val usageLogs = Log4jUsageLogger.track {\n              val e = intercept[DeltaUnsupportedOperationException] {\n                tableMutationFn()\n              }\n              checkError(e,\n                \"DELTA_UNSUPPORTED_WRITES_WITHOUT_COORDINATOR\",\n                sqlState = \"0AKDC\",\n                parameters = Map(\"coordinatorName\" -> \"tracking-in-memory\")\n              )\n              assert(e.getMessage.contains(\n                \"no implementation of this coordinator is available in the current environment\"))\n            }\n            val filteredLogs = filterUsageRecords(\n              usageLogs,\n              CoordinatedCommitsUsageLogs.COMMIT_COORDINATOR_MISSING_IMPLEMENTATION_WRITE)\n            val usageObj = JsonUtils.fromJson[Map[String, Any]](filteredLogs.head.blob)\n            assert(usageObj(\"commitCoordinatorName\") === \"tracking-in-memory\")\n            assert(usageObj(\"readVersion\") === \"2\")\n          }\n        }\n      }\n    }\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////\n  //            Test coordinated-commits with DeltaLog.getChangeLogFile API starts           //\n  /////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Helper method which generates a delta table with `totalCommits`.\n   * The `upgradeToCoordinatedCommitsVersion`th commit version upgrades this table to coordinated\n   * commits and it uses `backfillInterval` for backfilling.\n   * This method returns a mapping of version to DeltaLog for the versions in\n   * `requiredDeltaLogVersions`. Each of this deltaLog object has a Snapshot as per what is\n   * mentioned in the `requiredDeltaLogVersions`.\n   */\n  private def generateDataForGetChangeLogFilesTest(\n      dir: File,\n      totalCommits: Int,\n      upgradeToCoordinatedCommitsVersion: Int,\n      backfillInterval: Int,\n      requiredDeltaLogVersions: Set[Int]): Map[Int, DeltaLog] = {\n    if (isCatalogOwnedTest) {\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    val commitCoordinatorClient =\n      new TrackingCommitCoordinatorClient(new InMemoryCommitCoordinator(backfillInterval))\n    val builder =\n      TrackingInMemoryCommitCoordinatorBuilder(backfillInterval, Some(commitCoordinatorClient))\n    registerBuilder(builder)\n    val versionToDeltaLogMapping = collection.mutable.Map.empty[Int, DeltaLog]\n    withSQLConf(\n      CHECKPOINT_INTERVAL.defaultTablePropertyKey -> \"100\") {\n      val tablePath = dir.getAbsolutePath\n\n      (0 to totalCommits).foreach { v =>\n        if (v === upgradeToCoordinatedCommitsVersion) {\n          val deltaLog = DeltaLog.forTable(spark, tablePath)\n          val oldMetadata = deltaLog.unsafeVolatileSnapshot.metadata\n          val commitCoordinator = (COORDINATED_COMMITS_COORDINATOR_NAME.key -> \"tracking-in-memory\")\n          val newMetadata =\n            oldMetadata.copy(configuration = oldMetadata.configuration + commitCoordinator)\n          deltaLog.startTransaction().commitManually(newMetadata)\n        } else {\n          Seq(v).toDF().write.format(\"delta\").mode(\"append\").save(tablePath)\n        }\n        if (requiredDeltaLogVersions.contains(v)) {\n          versionToDeltaLogMapping.put(v, DeltaLog.forTable(spark, tablePath))\n          DeltaLog.clearCache()\n        }\n      }\n    }\n    versionToDeltaLogMapping.toMap\n  }\n\n  def runGetChangeLogFiles(\n      deltaLog: DeltaLog,\n      startVersion: Long,\n      endVersionOpt: Option[Long] = None,\n      totalCommitsOnTable: Long = 8,\n      expectedLastBackfilledCommit: Long,\n      updateExpected: Boolean): Unit = {\n    val usageRecords = Log4jUsageLogger.track {\n      val iter = endVersionOpt match {\n        case Some(endVersion) =>\n          deltaLog.getChangeLogFiles(\n            startVersion, endVersion, catalogTableOpt = None, failOnDataLoss = false)\n        case None =>\n          deltaLog.getChangeLogFiles(startVersion)\n      }\n      val paths = iter.map(_._2.getPath).toIndexedSeq\n      val (backfilled, unbackfilled) = paths.partition(_.getParent === deltaLog.logPath)\n      val expectedBackfilledCommits = startVersion to expectedLastBackfilledCommit\n      val expectedUnbackfilledCommits = {\n        val firstUnbackfilledVersion = (expectedLastBackfilledCommit + 1).max(startVersion)\n        val expectedEndVersion = endVersionOpt.getOrElse(totalCommitsOnTable)\n        firstUnbackfilledVersion to expectedEndVersion\n      }\n      assert(backfilled.map(FileNames.deltaVersion) === expectedBackfilledCommits)\n      assert(unbackfilled.map(FileNames.deltaVersion) === expectedUnbackfilledCommits)\n    }\n    val updateCountEvents = if (updateExpected) 1 else 0\n    assert(filterUsageRecords(usageRecords, \"deltaLog.update\").size === updateCountEvents)\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\"DeltaLog.getChangeLogFile with and\" +\n    \" without endVersion [No Coordinated Commits]\") {\n    withTempDir { dir =>\n      val versionsToDeltaLogMapping = generateDataForGetChangeLogFilesTest(\n        dir,\n        totalCommits = 4,\n        upgradeToCoordinatedCommitsVersion = -1,\n        backfillInterval = -1,\n        requiredDeltaLogVersions = Set(2, 4))\n\n      // We are asking for changes between 0 and 0 to a DeltaLog(unsafeVolatileSnapshot = 2).\n      // So we should not need an update() as all the required files are on filesystem.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(2),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        endVersionOpt = Some(0),\n        expectedLastBackfilledCommit = 0,\n        updateExpected = false)\n\n      // We are asking for changes between 0 to `end` to a DeltaLog(unsafeVolatileSnapshot = 2).\n      // Since the commits in filesystem are more than what unsafeVolatileSnapshot has, we should\n      // need an update() to get the latest snapshot and see if coordinated commits was enabled on\n      // the table concurrently.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(2),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        expectedLastBackfilledCommit = 4,\n        updateExpected = true)\n\n      // We are asking for changes between 0 to `end` to a DeltaLog(unsafeVolatileSnapshot = 4).\n      // The latest commit from filesystem listing is 4 -- same as unsafeVolatileSnapshot and this\n      // unsafeVolatileSnapshot doesn't have coordinated commits enabled. So we should not need an\n      // update().\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(4),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        expectedLastBackfilledCommit = 4,\n        updateExpected = false)\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\"DeltaLog.getChangeLogFile with and\" +\n    \" without endVersion [Coordinated Commits backfill size 1]\") {\n    withTempDir { dir =>\n      val versionsToDeltaLogMapping = generateDataForGetChangeLogFilesTest(\n        dir,\n        totalCommits = 4,\n        upgradeToCoordinatedCommitsVersion = 2,\n        backfillInterval = 1,\n        requiredDeltaLogVersions = Set(0, 2, 4))\n\n      // We are asking for changes between 0 and 0 to a DeltaLog(unsafeVolatileSnapshot = 2).\n      // So we should not need an update() as all the required files are on filesystem.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(2),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        endVersionOpt = Some(0),\n        expectedLastBackfilledCommit = 0,\n        updateExpected = false)\n\n      // We are asking for changes between 0 to `end` to a DeltaLog(unsafeVolatileSnapshot = 2).\n      // Since the commits in filesystem are more than what unsafeVolatileSnapshot has, we should\n      // need an update() to get the latest snapshot and see if coordinated commits was enabled on\n      // the table concurrently.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(2),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        expectedLastBackfilledCommit = 4,\n        updateExpected = true)\n\n      // We are asking for changes between 0 to 4 to a DeltaLog(unsafeVolatileSnapshot = 4).\n      // Since the commits in filesystem are between 0 to 4, so we don't need to update() to get\n      // the latest snapshot and see if coordinated commits was enabled on the table concurrently.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(4),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        endVersionOpt = Some(4),\n        expectedLastBackfilledCommit = 4,\n        updateExpected = false)\n\n      // We are asking for changes between 0 to `end` to a DeltaLog(unsafeVolatileSnapshot = 4).\n      // The latest commit from filesystem listing is 4 -- same as unsafeVolatileSnapshot and this\n      // unsafeVolatileSnapshot has coordinated commits enabled. So we should need an update() to\n      // find out latest commits from Commit Coordinator.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(4),\n        totalCommitsOnTable = 4,\n        startVersion = 0,\n        expectedLastBackfilledCommit = 4,\n        updateExpected = true)\n    }\n  }\n\n  testWithDefaultCommitCoordinatorUnset(\"DeltaLog.getChangeLogFile with and\" +\n    \" without endVersion [Coordinated Commits backfill size 10]\") {\n    withTempDir { dir =>\n      val versionsToDeltaLogMapping = generateDataForGetChangeLogFilesTest(\n        dir,\n        totalCommits = 8,\n        upgradeToCoordinatedCommitsVersion = 2,\n        backfillInterval = 10,\n        requiredDeltaLogVersions = Set(2, 3, 4, 8))\n\n      // We are asking for changes between 0 and 1 to a DeltaLog(unsafeVolatileSnapshot = 2/4).\n      // So we should not need an update() as all the required files are on filesystem.\n      Seq(2, 3, 4).foreach { version =>\n        runGetChangeLogFiles(\n          versionsToDeltaLogMapping(version),\n          totalCommitsOnTable = 8,\n          startVersion = 0,\n          endVersionOpt = Some(1),\n          expectedLastBackfilledCommit = 1,\n          updateExpected = false)\n      }\n\n      // We are asking for changes between 0 to `end` to a DeltaLog(unsafeVolatileSnapshot = 2/4).\n      // Since the unsafeVolatileSnapshot has coordinated-commits enabled, so we need to trigger an\n      // update to find the latest commits from Commit Coordinator.\n      Seq(2, 3, 4).foreach { version =>\n        runGetChangeLogFiles(\n          versionsToDeltaLogMapping(version),\n          totalCommitsOnTable = 8,\n          startVersion = 0,\n          expectedLastBackfilledCommit = 2,\n          updateExpected = true)\n      }\n\n      // We are asking for changes between 0 to `4` to a DeltaLog(unsafeVolatileSnapshot = 8).\n      // The filesystem has only commit 0/1/2.\n      // After that we need to rely on deltaLog.update() to get the latest snapshot and return the\n      // files until 8.\n      // Ideally the unsafeVolatileSnapshot should have the info to generate files from 0 to 4 and\n      // an update() should not be needed. This is an optimization that can be done in future.\n      runGetChangeLogFiles(\n        versionsToDeltaLogMapping(8),\n        totalCommitsOnTable = 8,\n        startVersion = 0,\n        endVersionOpt = Some(4),\n        expectedLastBackfilledCommit = 2,\n        updateExpected = true)\n    }\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////\n  //            Test coordinated-commits with DeltaLog.getChangeLogFile API ENDS             //\n  /////////////////////////////////////////////////////////////////////////////////////////////\n\n  test(\"During ALTER, overriding ICT configurations on (potential) Coordinated Commits \" +\n      \"or Catalog Owned tables throws an exception.\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n\n    // For a table that had Coordinated Commits enabled before the ALTER command.\n    withTempDir { tempDir =>\n      sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta TBLPROPERTIES \" +\n        propertiesString)\n      val e = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        sql(s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` SET TBLPROPERTIES \" +\n          s\"('${IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'false')\")\n      }\n      if (isCatalogOwnedTest) {\n        checkError(\n          e,\n          \"DELTA_CANNOT_MODIFY_CATALOG_MANAGED_DEPENDENCIES\",\n          sqlState = \"42616\",\n          parameters = Map[String, String]())\n      } else {\n        checkError(\n        e,\n        \"DELTA_CANNOT_MODIFY_COORDINATED_COMMITS_DEPENDENCIES\",\n        sqlState = \"42616\",\n        parameters = Map(\"Command\" -> \"ALTER\"))\n      }\n    }\n\n    if (isCatalogOwnedTest) {\n      cancel(\"Upgrade is not yet supported for catalog owned tables\")\n    }\n    // For a table that is about to enable Coordinated Commits during the same ALTER command.\n    withoutDefaultCCTableFeature {\n      withTempDir { tempDir =>\n        sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta\")\n        val e = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n          sql(s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` SET TBLPROPERTIES \" +\n            s\"('${COORDINATED_COMMITS_COORDINATOR_NAME.key}' = 'tracking-in-memory', \" +\n            s\"'${COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '${JsonUtils.toJson(Map())}', \" +\n            s\"'${IN_COMMIT_TIMESTAMPS_ENABLED.key}' = 'false')\")\n        }\n        checkError(\n          e,\n          \"DELTA_CANNOT_SET_COORDINATED_COMMITS_DEPENDENCIES\",\n          sqlState = \"42616\",\n          parameters = Map(\"Command\" -> \"ALTER\"))\n      }\n    }\n  }\n\n  test(\"During ALTER, unsetting ICT configurations on Coordinated Commits tables throws an \" +\n      \"exception.\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n\n    withTempDir { tempDir =>\n      sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta TBLPROPERTIES \" +\n        propertiesString)\n      val e = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        sql(s\"ALTER TABLE delta.`${tempDir.getAbsolutePath}` UNSET TBLPROPERTIES \" +\n          s\"('${IN_COMMIT_TIMESTAMPS_ENABLED.key}')\")\n      }\n      if (isCatalogOwnedTest) {\n        checkError(\n          e,\n          \"DELTA_CANNOT_MODIFY_CATALOG_MANAGED_DEPENDENCIES\",\n          sqlState = \"42616\",\n          parameters = Map[String, String]())\n      } else {\n        checkError(\n          e,\n          \"DELTA_CANNOT_MODIFY_COORDINATED_COMMITS_DEPENDENCIES\",\n          sqlState = \"42616\",\n          parameters = Map(\"Command\" -> \"ALTER\"))\n      }\n    }\n  }\n\n  test(\"During REPLACE, for non-CC tables, default CC configurations are ignored, but default \" +\n      \"ICT confs are retained, and existing ICT confs are discarded\") {\n    // Non-CC table, REPLACE with default CC and ICT confs => Non-CC, but with ICT confs.\n    withTempDir { tempDir =>\n      withoutDefaultCCTableFeature {\n        sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta\")\n      }\n      withSQLConf(IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> \"true\") {\n        sql(s\"REPLACE TABLE delta.`${tempDir.getAbsolutePath}` (id STRING) USING delta\")\n      }\n      assert(DeltaLog.forTable(spark, tempDir).snapshot.tableCommitCoordinatorClientOpt.isEmpty)\n      assert(!DeltaLog.forTable(spark, tempDir).snapshot.isCatalogOwned)\n      assert(DeltaLog.forTable(spark, tempDir).snapshot.metadata.configuration.contains(\n        IN_COMMIT_TIMESTAMPS_ENABLED.key))\n    }\n\n    // Non-CC table with ICT confs, REPLACE with only default CC confs => Non-CC, also no ICT confs.\n    withTempDir { tempDir =>\n      withoutDefaultCCTableFeature {\n        withSQLConf(IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey -> \"true\") {\n          sql(s\"CREATE TABLE delta.`${tempDir.getAbsolutePath}` (id LONG) USING delta\")\n        }\n      }\n      sql(s\"REPLACE TABLE delta.`${tempDir.getAbsolutePath}` (id STRING) USING delta\")\n      val snapshot = DeltaLog.forTable(spark, tempDir).unsafeVolatileSnapshot\n      assert(snapshot.tableCommitCoordinatorClientOpt.isEmpty)\n      assert(!snapshot.isCatalogOwned)\n      assert(!snapshot.metadata.configuration.contains(IN_COMMIT_TIMESTAMPS_ENABLED.key))\n    }\n  }\n\n  test(\"During REPLACE, for CC tables, existing CC and ICT configurations are both retained.\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n\n    withTable(\"t1\") {\n      if (isCatalogOwnedTest) {\n        sql(s\"CREATE TABLE t1 (id LONG) USING delta\")\n        sql(s\"INSERT INTO t1 VALUES (0)\")\n      } else {\n        withoutDefaultCCTableFeature {\n          sql(s\"CREATE TABLE t1 (id LONG) USING delta\")\n          sql(s\"INSERT INTO t1 VALUES (0)\")\n          sql(s\"ALTER TABLE t1 SET TBLPROPERTIES \" + propertiesString)\n        }\n      }\n      withoutDefaultCCTableFeature {\n        // All three ICT configurations should be set because CC feature is enabled later.\n        // REPLACE w/o default CC confs => CC, and all ICT confs.\n        sql(s\"REPLACE TABLE t1 (id STRING) USING delta\")\n        val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, CatalystTableIdentifier(\"t1\"))\n        if (isCatalogOwnedTest) {\n          assert(snapshot.isCatalogOwned)\n          // Only [[IN_COMMIT_TIMESTAMPS_ENABLED]] should be set for CatalogOwned\n          // since we don't support upgrade yet.\n          assert(snapshot.metadata.configuration.contains(IN_COMMIT_TIMESTAMPS_ENABLED.key))\n        } else {\n          assert(snapshot.tableCommitCoordinatorClientOpt.nonEmpty)\n          CoordinatedCommitsUtils.ICT_TABLE_PROPERTY_KEYS.foreach { key =>\n            assert(snapshot.metadata.configuration.contains(key))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"CREATE LIKE does not copy commit coordinated related feature config \" +\n    \"from the source table.\") {\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n\n    val source = \"sourcetable\"\n    val target = \"targettable\"\n    sql(s\"CREATE TABLE $source (id LONG) USING delta TBLPROPERTIES\" + propertiesString)\n    sql(s\"CREATE TABLE $target LIKE $source\")\n    val snapshot = DeltaLog.forTable(spark, target).unsafeVolatileSnapshot\n    assert(snapshot.tableCommitCoordinatorClientOpt.isEmpty)\n    assert(!snapshot.isCatalogOwned)\n  }\n\n  test(\"CREATE an external table in a location with an existing table works correctly.\") {\n    if (isCatalogOwnedTest) {\n      cancel(\"Creating an external table is not yet supported for CatalogOwned.\")\n    }\n    registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(1))\n\n    // When the existing table has a commit coordinator, omitting CC configurations in the command\n    // should not throw an exception, and the commit coordinator should be retained, so should ICT.\n    withTempDir { dir =>\n      val tableName = \"testtable\"\n      val tablePath = dir.getAbsolutePath\n      sql(s\"CREATE TABLE delta.`${dir.getAbsolutePath}` (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('foo' = 'bar', \" + propertiesString.substring(1))\n      sql(s\"CREATE TABLE $tableName (id LONG) USING delta TBLPROPERTIES \" +\n        s\"('foo' = 'bar') LOCATION '${dir.getAbsolutePath}'\")\n      val snapshot = DeltaLog.forTable(spark, tablePath).snapshot\n      assert(snapshot.tableCommitCoordinatorClientOpt.nonEmpty || snapshot.isCatalogOwned)\n      assert(snapshot.metadata.configuration.contains(IN_COMMIT_TIMESTAMPS_ENABLED.key))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.util.Optional\nimport java.util.concurrent.atomic.AtomicInteger\nimport scala.collection.mutable\nimport scala.util.control.NonFatal\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, CheckpointPolicy, DeltaColumnMappingMode, DeltaConfig, DeltaConfigs, DeltaLog, DeltaTestUtilsBase, DomainMetadataTableFeature, MaterializedRowCommitVersion, MaterializedRowId, RowTrackingFeature, Snapshot, TableFeature}\nimport org.apache.spark.sql.delta.actions.{CommitInfo, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.util.{DeltaCommitFileProvider, JsonUtils}\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{CommitCoordinatorClient, CommitResponse, TableDescriptor, TableIdentifier, UpdatedActions, GetCommitsResponse => JGetCommitsResponse}\nimport io.delta.storage.commit.actions.{AbstractMetadata, AbstractProtocol}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.{SparkConf, SparkFunSuite}\nimport org.apache.spark.sql.{QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.{TableIdentifier => CatalystTableIdentifier}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\n\n// This trait is built to serve as a base trait for tests built for both CatalogOwned\n// and commit-coordinators table feature.\ntrait CommitCoordinatorUtilBase {\n  /**\n   * Runs a specific test with commit coordinator feature unset.\n   */\n  def testWithDefaultCommitCoordinatorUnset(testName: String)(f: => Unit)\n\n  /**\n   * Runs the function `f` with commit coordinator table feature unset.\n   * Any table created in function `f` have CatalogOwned/CoordinatedCommits disabled by default.\n   */\n  def withoutDefaultCCTableFeature(f: => Unit): Unit\n\n  /**\n   * Runs the function `f` with commit coordinator table feature set.\n   * Any table created in function `f` have CatalogOwned/CoordinatedCommits enabled by default.`\n   */\n  def withDefaultCCTableFeature(f: => Unit): Unit\n\n  /** Run the test with different backfill batch sizes: 1, 2, 10 */\n  def testWithDifferentBackfillInterval(testName: String)(f: Int => Unit): Unit\n\n  /** Register a builder to the appropriate builder provider. */\n  def registerBuilder(builder: CommitCoordinatorBuilder): Unit\n\n  /** Clear relevant table feature commit coordinator builders that are registered. */\n  def clearBuilders(): Unit\n\n  /** Returns the properties string to be used in the table creation for test. */\n  def propertiesString: String\n\n  /**\n   * Returns true if this test is about CatalogOwned table feature.\n   * Returns false if this test is about CoordinatedCommits tabel feature.\n   */\n  def isCatalogOwnedTest: Boolean\n\n  /** Keeps track of the number of table names pointing to the location. */\n  protected val locRefCount: mutable.Map[String, Int] = mutable.Map.empty\n}\n\ntrait CatalogOwnedTestBaseSuite\n  extends SparkFunSuite\n  with DeltaTestUtilsBase\n  with CommitCoordinatorUtilBase\n  with SharedSparkSession {\n\n  val defaultCatalogOwnedFeatureEnabledKey: String =\n    TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature)\n\n  // If this config is not overridden, newly created table is not CatalogOwned by default.\n  def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = None\n\n  def catalogOwnedDefaultCreationEnabledInTests: Boolean =\n    catalogOwnedCoordinatorBackfillBatchSize.nonEmpty\n\n  /**\n   * Returns the commit coordinator client for the specified catalog.\n   *\n   * @param catalogName The name of the catalog to get the commit coordinator client for.\n   * @return The commit coordinator client for the specified catalog.\n   */\n  protected def getCatalogOwnedCommitCoordinatorClient(\n      catalogName: String): CommitCoordinatorClient = {\n    CatalogOwnedCommitCoordinatorProvider.getBuilder(catalogName).getOrElse {\n      throw new IllegalStateException(\n        s\"Commit coordinator builder is not available for the specified catalog: $catalogName\")\n    }.buildForCatalog(spark, catalogName)\n  }\n\n  override protected def sparkConf: SparkConf = {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      super.sparkConf.set(defaultCatalogOwnedFeatureEnabledKey, \"supported\")\n    } else {\n      super.sparkConf\n    }\n  }\n\n  override def clearBuilders(): Unit = {\n    CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n  }\n\n  override def propertiesString: String =\n    s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\"\n\n  override protected def beforeEach(): Unit = {\n    super.beforeEach()\n    CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n    catalogOwnedCoordinatorBackfillBatchSize.foreach { batchSize =>\n      CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n        catalogName = CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING,\n        commitCoordinatorBuilder = TrackingInMemoryCommitCoordinatorBuilder(batchSize)\n      )\n    }\n    DeltaLog.clearCache()\n  }\n\n  override def testWithDefaultCommitCoordinatorUnset(testName: String)(f: => Unit): Unit = {\n    test(testName) {\n      withoutDefaultCCTableFeature {\n        f\n      }\n    }\n  }\n\n  override def withDefaultCCTableFeature(f: => Unit): Unit = {\n    val oldConfig = spark.conf.getOption(defaultCatalogOwnedFeatureEnabledKey)\n    spark.conf.set(defaultCatalogOwnedFeatureEnabledKey, \"supported\")\n    try { f } finally {\n      if (oldConfig.isDefined) {\n        spark.conf.set(defaultCatalogOwnedFeatureEnabledKey, oldConfig.get)\n      } else {\n        spark.conf.unset(defaultCatalogOwnedFeatureEnabledKey)\n      }\n    }\n  }\n\n  override def withoutDefaultCCTableFeature(f: => Unit): Unit = {\n    val oldConfig = spark.conf.getOption(defaultCatalogOwnedFeatureEnabledKey)\n    spark.conf.unset(defaultCatalogOwnedFeatureEnabledKey)\n    try { f } finally {\n      if (oldConfig.isDefined) {\n        spark.conf.set(defaultCatalogOwnedFeatureEnabledKey, oldConfig.get)\n      }\n    }\n  }\n\n  override def testWithDifferentBackfillInterval(testName: String)(f: Int => Unit): Unit = {\n    Seq(1, 2, 10).foreach { backfillBatchSize =>\n      test(s\"$testName [Backfill batch size: $backfillBatchSize]\") {\n        CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n        CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n          \"spark_catalog\", TrackingInMemoryCommitCoordinatorBuilder(batchSize = backfillBatchSize))\n        f(backfillBatchSize)\n      }\n    }\n  }\n\n  /**\n   * Run the test against a [[TrackingCommitCoordinatorClient]] with backfill batch size =\n   * `batchBackfillSize`\n   */\n  def testWithCatalogOwned(backfillBatchSize: Int)(testName: String)(f: => Unit): Unit = {\n    test(s\"$testName [Backfill batch size: $backfillBatchSize]\") {\n      CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n      CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n        CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING,\n        TrackingInMemoryCommitCoordinatorBuilder(batchSize = backfillBatchSize))\n      withDefaultCCTableFeature {\n        f\n      }\n    }\n  }\n\n  override def registerBuilder(builder: CommitCoordinatorBuilder): Unit = {\n    assert(builder.isInstanceOf[CatalogOwnedCommitCoordinatorBuilder],\n      s\"builder $builder(${builder.getName}) must be CatalogOwnedCommitCoordinatorBuilder\")\n    CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n      \"spark_catalog\", builder.asInstanceOf[CatalogOwnedCommitCoordinatorBuilder])\n  }\n\n  override def isCatalogOwnedTest: Boolean = true\n\n  def deleteCatalogOwnedTableFromCommitCoordinator(tableName: String): Unit = {\n    val location = try {\n      spark.sql(s\"describe detail $tableName\")\n        .select(\"location\")\n        .first()\n        .getAs[String](0)\n    } catch {\n      case NonFatal(_) =>\n        // Ignore if the table does not exist/broken.\n        return\n    }\n    deleteCatalogOwnedTableFromCommitCoordinator(path = new Path(location))\n  }\n\n  def deleteCatalogOwnedTableFromCommitCoordinator(path: Path): Unit = {\n    val catalogName = \"spark_catalog\"\n    val cc = CatalogOwnedCommitCoordinatorProvider.getBuilder(catalogName).getOrElse {\n      throw new IllegalStateException(\n        s\"Unable to get CatalogOwnedCommitCoordinatorBuilder for table at path: ${path.toString}\")\n    }.buildForCatalog(spark, catalogName)\n\n    assert(\n      cc.isInstanceOf[TrackingCommitCoordinatorClient],\n      s\"Please implement delete/drop method for coordinator: ${cc.getClass.getName}\")\n\n    val locKey = path.toString.stripPrefix(\"file:\")\n    if (locRefCount.contains(locKey)) {\n      locRefCount(locKey) -= 1\n    }\n    // When we create an external table in a location where some table already existed, two table\n    // names could be pointing to the same location. We should only clean up the table data in the\n    // commit coordinator when the last table name pointing to the location is dropped.\n    if (locRefCount.getOrElse(locKey, 0) == 0) {\n      val logPath = new Path(path, \"_delta_log\")\n      cc.asInstanceOf[TrackingCommitCoordinatorClient]\n        .delegatingCommitCoordinatorClient\n        .asInstanceOf[InMemoryCommitCoordinator]\n        .dropTable(logPath)\n    }\n    DeltaLog.clearCache()\n  }\n\n  /**\n   * Constructs the specific table properties for Catalog Owned tables.\n   *\n   * @param spark The Spark session.\n   * @param metadata The metadata of the CC table.\n   * @return A map of CC specific table properties.\n   */\n  def constructCatalogOwnedSpecificTableProperties(\n      spark: SparkSession,\n      metadata: Metadata): Map[String, String] = {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      val qolConfs = CatalogOwnedTableUtils.QOL_TABLE_FEATURES_AND_PROPERTIES\n        .collect {\n          case (feature, config, value)\n          => config.key -> value\n        }\n        .toMap\n      // RowTracking specific properties.\n      qolConfs ++ Map(\n        MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP ->\n          metadata.configuration.getOrElse(\n            MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP,\n            fail(s\"Failed to get ${MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP}.\")\n          ),\n        MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP ->\n          metadata.configuration.getOrElse(\n            MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP,\n            fail(s\"Failed to get ${MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP}.\")\n          )\n      ) ++\n      Map(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"true\")\n    } else {\n      Map.empty\n    }\n  }\n\n  /**\n   * Returns the properties that are expected to show up in the table properties of a Delta table\n   * when catalog owned is enabled in tests.\n   */\n  def extractCatalogOwnedSpecificPropertiesIfEnabled(\n      metadata: Metadata): Iterable[(String, String)] = {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      val CATALOG_OWNED_TABLE_QOL_PROPERTY_KEYS =\n        CatalogOwnedTableUtils.QOL_TABLE_FEATURES_AND_PROPERTIES\n          .map { case (_, config, _) => config.key }\n          .filterNot(Set(\n            DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key\n          )) ++\n          Seq(\n            MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP,\n            MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP\n          )\n      CATALOG_OWNED_TABLE_QOL_PROPERTY_KEYS.map { key =>\n        key -> metadata.configuration.getOrElse(key,\n          fail( s\"Expected $key to be defined in the table properties\"))\n      } ++\n      Option(DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.key -> \"true\")\n    } else {\n      Seq.empty\n    }\n  }\n\n  protected def withClassicCheckpointPolicyForCatalogOwned(f: => Unit): Unit = {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      withSQLConf(\n        DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.Classic.name) {\n        f\n      }\n    } else {\n      f\n    }\n  }\n\n  protected def getDeltaLogWithSnapshot(\n      tableIdentifier: CatalystTableIdentifier): (DeltaLog, Snapshot) = {\n    DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n  }\n\n  protected def isICTEnabledForNewTablesCatalogOwned: Boolean = {\n    catalogOwnedCoordinatorBackfillBatchSize.nonEmpty ||\n      spark.conf.getOption(\n        DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey).contains(\"true\")\n  }\n\n  protected def validateTableFeatureAndMetadata(\n      tableName: String,\n      tableFeature: TableFeature,\n      tableFeatureShouldPresent: Boolean,\n      metadataShouldPresent: Boolean,\n      config: DeltaConfig[_],\n      targetValue: String): Unit = {\n    val (_, snapshot) = getDeltaLogWithSnapshot(CatalystTableIdentifier(tableName))\n    assert(snapshot.protocol.readerAndWriterFeatureNames.contains(tableFeature.name)\n      === tableFeatureShouldPresent, s\"expected table feature \" +\n      s\"${tableFeature.name} to be ${if (tableFeatureShouldPresent) \"present\" else \"absent\"}\")\n    val metadataValue: String = if (config.key == DeltaConfigs.COLUMN_MAPPING_MODE.key) {\n      config.fromMetaData(snapshot.metadata).asInstanceOf[DeltaColumnMappingMode].name\n    } else if (config.key == DeltaConfigs.CHECKPOINT_POLICY.key) {\n      config.fromMetaData(snapshot.metadata).asInstanceOf[CheckpointPolicy.Policy].name\n    } else {\n      config.fromMetaData(snapshot.metadata).toString\n    }\n    assert((metadataValue == targetValue) === metadataShouldPresent,\n      s\"expected the metadata configuration of ${tableFeature.name} to be \" +\n        s\"${if (metadataShouldPresent) \"present\" else \"absent\"}\")\n  }\n\n  protected def validateOnlySpecifiedQoLTableFeaturesAndMetadataPresent(\n      tableName: String,\n      supportedTableFeatures: Set[TableFeature]): Unit = {\n    CatalogOwnedTableUtils.qolTableFeatureAndProperties.foreach {\n      case (t, config, value) =>\n        val isSpecifiedTableFeature = supportedTableFeatures.contains(t)\n        validateTableFeatureAndMetadata(\n          tableName,\n          tableFeature = t,\n          tableFeatureShouldPresent = isSpecifiedTableFeature,\n          metadataShouldPresent = isSpecifiedTableFeature,\n          config,\n          targetValue = value)\n    }\n    validateRowTrackingEnablement(\n      tableName, expected = supportedTableFeatures.contains(RowTrackingFeature))\n  }\n\n  protected def validateRowTrackingEnablement(tableName: String, expected: Boolean): Unit = {\n    // [[DomainMetadataTableFeature]] is a dependent feature of\n    // [[RowTrackingFeature]] and would be enabled at the same time.\n    // Note: [[DomainMetadataTableFeature]] does not have the corresponding metadata.\n    val (_, snapshot) = getDeltaLogWithSnapshot(CatalystTableIdentifier(tableName))\n    if (expected) {\n      // If row tracking is enabled, we expect domain metadata to be added.\n      // But when row tracking is not enabled, other features\n      // such as Iceberg V2 could still add domain metadata.\n      assert(\n        snapshot.protocol.readerAndWriterFeatureNames.contains(DomainMetadataTableFeature.name))\n    }\n    // All [[AddFiles]] should have `baseRowId` properly propagated if RowTracking is enabled.\n    if (expected) {\n      assert(snapshot.allFiles.where(col(\"baseRowId\").isNull).isEmpty)\n    } else {\n      assert(snapshot.allFiles.where(col(\"baseRowId\").isNotNull).isEmpty)\n    }\n  }\n\n  protected def validateQoLFeaturesEnablement(tableName: String, expected: Boolean): Unit = {\n    CatalogOwnedTableUtils.QOL_TABLE_FEATURES_AND_PROPERTIES.foreach { case (t, config, value) =>\n      validateTableFeatureAndMetadata(\n        tableName,\n        tableFeature = t,\n        tableFeatureShouldPresent = expected,\n        metadataShouldPresent = expected,\n        config,\n        targetValue = value)\n    }\n    sql(s\"INSERT INTO $tableName VALUES (3), (4), (5), (6)\")\n    QueryTest.checkAnswer(sql(s\"SELECT * FROM $tableName\"),\n      Seq(Row(1), Row(2), Row(3), Row(4), Row(5), Row(6)))\n    validateRowTrackingEnablement(\n      tableName,\n      expected)\n  }\n}\n\ntrait CoordinatedCommitsTestUtils\n  extends DeltaTestUtilsBase\n  with CommitCoordinatorUtilBase { self: SparkFunSuite with SharedSparkSession =>\n\n  protected val defaultCommitsCoordinatorName = \"tracking-in-memory\"\n  protected val defaultCommitsCoordinatorConf = Map(\"randomConf\" -> \"randomConfValue\")\n\n  override def testWithDefaultCommitCoordinatorUnset(testName: String)(f: => Unit): Unit = {\n    test(testName) {\n      withoutDefaultCCTableFeature {\n        f\n      }\n    }\n  }\n\n  override def withDefaultCCTableFeature(f: => Unit): Unit = {\n    val confJson = JsonUtils.toJson(defaultCommitsCoordinatorConf)\n    withSQLConf(\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey ->\n        defaultCommitsCoordinatorName,\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey -> confJson) {\n      f\n    }\n  }\n\n  override def withoutDefaultCCTableFeature(f: => Unit): Unit = {\n    val defaultCoordinatedCommitsConfs = CoordinatedCommitsUtils\n      .getDefaultCCConfigurations(spark, withDefaultKey = true)\n    defaultCoordinatedCommitsConfs.foreach { case (defaultKey, _) =>\n      spark.conf.unset(defaultKey)\n    }\n    try { f } finally {\n      defaultCoordinatedCommitsConfs.foreach { case (defaultKey, oldValue) =>\n        spark.conf.set(defaultKey, oldValue)\n      }\n    }\n  }\n\n  def withCustomCoordinatedCommitsTableProperties(\n      commitCoordinatorName: String,\n      conf: Map[String, String] = Map(\"randomConf\" -> \"randomConfValue\"))(f: => Unit): Unit = {\n    withSQLConf(\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey ->\n        commitCoordinatorName,\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey ->\n        JsonUtils.toJson(conf)) {\n      f\n    }\n  }\n\n  override def testWithDifferentBackfillInterval(testName: String)(f: Int => Unit): Unit = {\n    Seq(1, 2, 10).foreach { backfillBatchSize =>\n      test(s\"$testName [Backfill batch size: $backfillBatchSize]\") {\n        CommitCoordinatorProvider.clearNonDefaultBuilders()\n        CommitCoordinatorProvider.registerBuilder(\n          TrackingInMemoryCommitCoordinatorBuilder(backfillBatchSize))\n        CommitCoordinatorProvider.registerBuilder(\n          InMemoryCommitCoordinatorBuilder(backfillBatchSize))\n        f(backfillBatchSize)\n      }\n    }\n  }\n\n  override def registerBuilder(builder: CommitCoordinatorBuilder): Unit = {\n    CommitCoordinatorProvider.registerBuilder(builder)\n  }\n\n  override def clearBuilders(): Unit = {\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n  }\n\n  override def propertiesString: String = {\n    val coordinatedCommitsConfJson = JsonUtils.toJson(defaultCommitsCoordinatorConf)\n    s\"('${DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key}' =\" +\n      s\"'$defaultCommitsCoordinatorName', \" +\n      s\"'${DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.key}' = '$coordinatedCommitsConfJson')\"\n  }\n\n  override def isCatalogOwnedTest: Boolean = false\n\n  /** Run the test with:\n   * 1. Without coordinated-commits\n   * 2. With coordinated-commits with different backfill batch sizes\n   */\n  def testWithDifferentBackfillIntervalOptional(testName: String)(f: Option[Int] => Unit): Unit = {\n    test(s\"$testName [Backfill batch size: None]\") {\n      f(None)\n    }\n    testWithDifferentBackfillInterval(testName) { backfillBatchSize =>\n      val coordinatedCommitsCoordinatorJson = JsonUtils.toJson(defaultCommitsCoordinatorConf)\n      withSQLConf(\n          DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey ->\n            defaultCommitsCoordinatorName,\n          DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey ->\n            coordinatedCommitsCoordinatorJson) {\n        f(Some(backfillBatchSize))\n      }\n    }\n  }\n\n  def getUpdatedActionsForZerothCommit(\n      commitInfo: CommitInfo,\n      oldMetadata: Metadata = Metadata()): UpdatedActions = {\n    val newMetadataConfiguration =\n      oldMetadata.configuration +\n        (DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.key -> defaultCommitsCoordinatorName)\n    val newMetadata = oldMetadata.copy(configuration = newMetadataConfiguration)\n    new UpdatedActions(commitInfo, newMetadata, Protocol(), oldMetadata, Protocol())\n  }\n\n  def getUpdatedActionsForNonZerothCommit(commitInfo: CommitInfo): UpdatedActions = {\n    val updatedActions = getUpdatedActionsForZerothCommit(commitInfo)\n    new UpdatedActions(\n      updatedActions.getCommitInfo,\n      updatedActions.getNewMetadata,\n      updatedActions.getNewProtocol,\n      updatedActions.getNewMetadata,\n      updatedActions.getOldProtocol\n    )\n  }\n\n}\n\ncase class TrackingInMemoryCommitCoordinatorBuilder(\n    batchSize: Long,\n    defaultCommitCoordinatorClientOpt: Option[CommitCoordinatorClient] = None,\n    defaultCommitCoordinatorName: String = \"tracking-in-memory\")\n  extends CatalogOwnedCommitCoordinatorBuilder {\n  lazy val trackingInMemoryCommitCoordinatorClient =\n    defaultCommitCoordinatorClientOpt.getOrElse {\n      new TrackingCommitCoordinatorClient(\n        new PredictableUuidInMemoryCommitCoordinatorClient(batchSize))\n    }\n\n  override def getName: String = defaultCommitCoordinatorName\n  override def build(spark: SparkSession, conf: Map[String, String]): CommitCoordinatorClient = {\n    trackingInMemoryCommitCoordinatorClient\n  }\n\n  override def buildForCatalog(\n      spark: SparkSession, catalogName: String): CommitCoordinatorClient = {\n    trackingInMemoryCommitCoordinatorClient\n  }\n}\n\nclass PredictableUuidInMemoryCommitCoordinatorClient(batchSize: Long)\n  extends InMemoryCommitCoordinator(batchSize) {\n\n  var nextUuidSuffix = 1L\n  override def generateUUID(): String = {\n    nextUuidSuffix += 1\n    s\"uuid-${nextUuidSuffix - 1}\"\n  }\n}\n\nobject TrackingCommitCoordinatorClient {\n  private val insideOperation = new ThreadLocal[Boolean] {\n    override def initialValue(): Boolean = false\n  }\n}\n\nclass TrackingCommitCoordinatorClient(\n    val delegatingCommitCoordinatorClient: CommitCoordinatorClient)\n  extends CommitCoordinatorClient {\n\n  val numCommitsCalled = new AtomicInteger(0)\n  val numGetCommitsCalled = new AtomicInteger(0)\n  val numBackfillToVersionCalled = new AtomicInteger(0)\n  val numRegisterTableCalled = new AtomicInteger(0)\n\n  def recordOperation[T](op: String)(f: => T): T = {\n    val oldInsideOperation = TrackingCommitCoordinatorClient.insideOperation.get()\n    try {\n      if (!TrackingCommitCoordinatorClient.insideOperation.get()) {\n        op match {\n          case \"commit\" => numCommitsCalled.incrementAndGet()\n          case \"getCommits\" => numGetCommitsCalled.incrementAndGet()\n          case \"backfillToVersion\" => numBackfillToVersionCalled.incrementAndGet()\n          case \"registerTable\" => numRegisterTableCalled.incrementAndGet()\n          case _ => ()\n        }\n      }\n      TrackingCommitCoordinatorClient.insideOperation.set(true)\n      f\n    } finally {\n      TrackingCommitCoordinatorClient.insideOperation.set(oldInsideOperation)\n    }\n  }\n\n  override def commit(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      tableDesc: TableDescriptor,\n      commitVersion: Long,\n      actions: java.util.Iterator[String],\n      updatedActions: UpdatedActions): CommitResponse = recordOperation(\"commit\") {\n    delegatingCommitCoordinatorClient.commit(\n      logStore,\n      hadoopConf,\n      tableDesc,\n      commitVersion,\n      actions,\n      updatedActions)\n  }\n\n  override def getCommits(\n      tableDesc: TableDescriptor,\n      startVersion: java.lang.Long,\n      endVersion: java.lang.Long): JGetCommitsResponse = recordOperation(\"getCommits\") {\n    delegatingCommitCoordinatorClient.getCommits(tableDesc, startVersion, endVersion)\n  }\n\n  override def backfillToVersion(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      tableDesc: TableDescriptor,\n      version: Long,\n      lastKnownBackfilledVersion: java.lang.Long): Unit = recordOperation(\"backfillToVersion\") {\n    delegatingCommitCoordinatorClient.backfillToVersion(\n      logStore,\n      hadoopConf,\n      tableDesc,\n      version,\n      lastKnownBackfilledVersion)\n  }\n\n  override def semanticEquals(other: CommitCoordinatorClient): Boolean = {\n    other match {\n      case otherTracking: TrackingCommitCoordinatorClient =>\n        delegatingCommitCoordinatorClient.semanticEquals(\n          otherTracking.delegatingCommitCoordinatorClient)\n      case _ =>\n        delegatingCommitCoordinatorClient.semanticEquals(other)\n    }\n  }\n\n  def reset(): Unit = {\n    numCommitsCalled.set(0)\n    numGetCommitsCalled.set(0)\n    numBackfillToVersionCalled.set(0)\n  }\n\n  override def registerTable(\n      logPath: Path,\n      tableIdentifier: Optional[TableIdentifier],\n      currentVersion: Long,\n      currentMetadata: AbstractMetadata,\n      currentProtocol: AbstractProtocol): java.util.Map[String, String] =\n    recordOperation(\"registerTable\") {\n      delegatingCommitCoordinatorClient.registerTable(\n        logPath, tableIdentifier, currentVersion, currentMetadata, currentProtocol)\n    }\n}\n\n/**\n * A helper class which enables coordinated-commits for the test suite based on the given\n * `coordinatedCommitsBackfillBatchSize` conf.\n */\ntrait CoordinatedCommitsBaseSuite\n  extends SparkFunSuite\n  with SharedSparkSession\n  with CoordinatedCommitsTestUtils {\n\n  // If this config is not overridden, coordinated commits are disabled.\n  def coordinatedCommitsBackfillBatchSize: Option[Int] = None\n\n  final def coordinatedCommitsEnabledInTests: Boolean = coordinatedCommitsBackfillBatchSize.nonEmpty\n\n  // In case some tests reuse the table path/name with DROP table, this method can be used to\n  // clean the table data in the commit coordinator. Note that we should call this before\n  // the table actually gets DROP.\n  def deleteTableFromCommitCoordinator(tableName: String): Unit = {\n    val location = try {\n      spark.sql(s\"describe detail $tableName\")\n        .select(\"location\")\n        .first()\n        .getAs[String](0)\n    } catch {\n      case NonFatal(_) =>\n        // Ignore if the table does not exist/broken.\n        return\n    }\n    deleteTableFromCommitCoordinator(new Path(location))\n  }\n\n  def deleteTableFromCommitCoordinator(path: Path): Unit = {\n    val cc = CommitCoordinatorProvider.getCommitCoordinatorClient(\n      defaultCommitsCoordinatorName, defaultCommitsCoordinatorConf, spark)\n    assert(\n      cc.isInstanceOf[TrackingCommitCoordinatorClient],\n      s\"Please implement delete/drop method for coordinator: ${cc.getClass.getName}\")\n\n    val locKey = path.toString.stripPrefix(\"file:\")\n    if (locRefCount.contains(locKey)) {\n      locRefCount(locKey) -= 1\n    }\n    // When we create an external table in a location where some table already existed, two table\n    // names could be pointing to the same location. We should only clean up the table data in the\n    // commit coordinator when the last table name pointing to the location is dropped.\n    if (locRefCount.getOrElse(locKey, 0) == 0) {\n      val logPath = new Path(path, \"_delta_log\")\n      cc.asInstanceOf[TrackingCommitCoordinatorClient]\n        .delegatingCommitCoordinatorClient\n        .asInstanceOf[InMemoryCommitCoordinator]\n        .dropTable(logPath)\n    }\n    DeltaLog.clearCache()\n  }\n\n  override protected def sparkConf: SparkConf = {\n    if (coordinatedCommitsBackfillBatchSize.nonEmpty) {\n      val coordinatedCommitsCoordinatorJson = JsonUtils.toJson(defaultCommitsCoordinatorConf)\n      super.sparkConf\n        .set(\n          DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey,\n          defaultCommitsCoordinatorName)\n        .set(\n          DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey,\n          coordinatedCommitsCoordinatorJson)\n    } else {\n      super.sparkConf\n    }\n  }\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    CommitCoordinatorProvider.clearNonDefaultBuilders()\n    coordinatedCommitsBackfillBatchSize.foreach { batchSize =>\n      CommitCoordinatorProvider.registerBuilder(TrackingInMemoryCommitCoordinatorBuilder(batchSize))\n    }\n    DeltaLog.clearCache()\n  }\n\n  protected def isICTEnabledForNewTables: Boolean = {\n    spark.conf.getOption(\n      DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey).nonEmpty ||\n      spark.conf.getOption(\n        DeltaConfigs.IN_COMMIT_TIMESTAMPS_ENABLED.defaultTablePropertyKey).contains(\"true\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/CoordinatedCommitsUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport scala.jdk.CollectionConverters._\n\nimport org.apache.spark.sql.delta.DeltaConfigs.{COORDINATED_COMMITS_COORDINATOR_CONF, COORDINATED_COMMITS_COORDINATOR_NAME, COORDINATED_COMMITS_TABLE_CONF}\nimport org.apache.spark.sql.delta.DeltaIllegalArgumentException\nimport org.apache.spark.sql.delta.test.shims.GridTestShim\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass CoordinatedCommitsUtilsSuite extends QueryTest\n  with GridTestShim\n  with SharedSparkSession\n  with CoordinatedCommitsTestUtils {\n\n  /////////////////////////////////////////////////////////////////////////////////////////////\n  //     Test CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl STARTS    //\n  /////////////////////////////////////////////////////////////////////////////////////////////\n\n  private val cNameKey = COORDINATED_COMMITS_COORDINATOR_NAME.key\n  private val cConfKey = COORDINATED_COMMITS_COORDINATOR_CONF.key\n  private val tableConfKey = COORDINATED_COMMITS_TABLE_CONF.key\n  private val cName = cNameKey -> \"some-cc-name\"\n  private val cConf = cConfKey -> \"some-cc-conf\"\n  private val tableConf = tableConfKey -> \"some-table-conf\"\n\n  private val cNameDefaultKey = COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey\n  private val cConfDefaultKey = COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey\n  private val tableConfDefaultKey = COORDINATED_COMMITS_TABLE_CONF.defaultTablePropertyKey\n  private val cNameDefault = cNameDefaultKey -> \"some-cc-name\"\n  private val cConfDefault = cConfDefaultKey -> \"some-cc-conf\"\n  private val tableConfDefault = tableConfDefaultKey -> \"some-table-conf\"\n\n  private val command = \"CLONE\"\n\n  private def errCannotOverride = new DeltaIllegalArgumentException(\n    \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\", Array(command))\n\n  private def errMissingConfInCommand(key: String) = new DeltaIllegalArgumentException(\n    \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_COMMAND\", Array(command, key))\n\n  private def errMissingConfInSession(key: String) = new DeltaIllegalArgumentException(\n    \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_SESSION\", Array(command, key))\n\n  private def errTableConfInCommand = new DeltaIllegalArgumentException(\n    \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\", Array(command, tableConfKey))\n\n  private def errTableConfInSession = new DeltaIllegalArgumentException(\n    \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_SESSION\",\n    Array(command, tableConfDefaultKey, tableConfDefaultKey))\n\n  private def testValidationForCreateDeltaTableCommand(\n      tableExists: Boolean,\n      propertyOverrides: Map[String, String],\n      defaultConfs: Seq[(String, String)],\n      errorOpt: Option[DeltaIllegalArgumentException]): Unit = {\n    withoutDefaultCCTableFeature {\n      withSQLConf(defaultConfs: _*) {\n        if (errorOpt.isDefined) {\n          val e = intercept[DeltaIllegalArgumentException] {\n            CoordinatedCommitsUtils.validateConfigurationsForCreateDeltaTableCommandImpl(\n              spark, propertyOverrides, tableExists, command)\n          }\n          checkError(\n            e,\n            errorOpt.get.getErrorClass,\n            sqlState = errorOpt.get.getSqlState,\n            parameters = errorOpt.get.getMessageParameters.asScala.toMap)\n        } else {\n          CoordinatedCommitsUtils.validateConfigurationsForCreateDeltaTableCommandImpl(\n            spark, propertyOverrides, tableExists, command)\n        }\n      }\n    }\n  }\n\n  // tableExists: True\n  //            | False\n  //\n  // propertyOverrides: Map.empty\n  //                  | Map(cName)\n  //                  | Map(cName, cConf)\n  //                  | Map(cName, cConf, tableConf)\n  //                  | Map(tableConf)\n  //\n  // defaultConf: Seq.empty\n  //            | Seq(cNameDefault)\n  //            | Seq(cNameDefault, cConfDefault)\n  //            | Seq(cNameDefault, cConfDefault, tableConfDefault)\n  //            | Seq(tableConfDefault)\n  //\n  // errorOpt: None\n  //         | Some(errCannotOverride)\n  //         | Some(errMissingConfInCommand(cConfKey))\n  //         | Some(errMissingConfInSession(cConfKey))\n  //         | Some(errTableConfInCommand)\n  //         | Some(errTableConfInSession)\n\n  gridTest(\"During CLONE, CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl \" +\n      \"passes for existing target tables with no explicit Coordinated Commits Configurations.\") (\n    Seq(\n      Seq.empty,\n      // Not having any explicit Coordinated Commits configurations, but having an illegal\n      // combination of Coordinated Commits configurations in default: pass.\n      // This is because we don't consider default configurations when the table exists.\n      Seq(cNameDefault),\n      Seq(cNameDefault, cConfDefault),\n      Seq(cNameDefault, cConfDefault, tableConfDefault),\n      Seq(tableConfDefault)\n    )\n  ) { defaultConfs: Seq[(String, String)] =>\n    testValidationForCreateDeltaTableCommand(\n      tableExists = true,\n      propertyOverrides = Map.empty,\n      defaultConfs,\n      errorOpt = None)\n  }\n\n  gridTest(\"During CLONE, CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl \" +\n      \"fails for existing target tables with any explicit Coordinated Commits Configurations.\") (\n    Seq(\n      (Map(cName), Seq.empty),\n      (Map(cName), Seq(cNameDefault)),\n      (Map(cName), Seq(cNameDefault, cConfDefault)),\n      (Map(cName), Seq(cNameDefault, cConfDefault, tableConfDefault)),\n      (Map(cName), Seq(tableConfDefault)),\n\n      (Map(cName, cConf), Seq.empty),\n      (Map(cName, cConf), Seq(cNameDefault)),\n      (Map(cName, cConf), Seq(cNameDefault, cConfDefault)),\n      (Map(cName, cConf), Seq(cNameDefault, cConfDefault, tableConfDefault)),\n      (Map(cName, cConf), Seq(tableConfDefault)),\n\n      (Map(cName, cConf, tableConf), Seq.empty),\n      (Map(cName, cConf, tableConf), Seq(cNameDefault)),\n      (Map(cName, cConf, tableConf), Seq(cNameDefault, cConfDefault)),\n      (Map(cName, cConf, tableConf), Seq(cNameDefault, cConfDefault, tableConfDefault)),\n      (Map(cName, cConf, tableConf), Seq(tableConfDefault)),\n\n      (Map(tableConf), Seq.empty),\n      (Map(tableConf), Seq(cNameDefault)),\n      (Map(tableConf), Seq(cNameDefault, cConfDefault)),\n      (Map(tableConf), Seq(cNameDefault, cConfDefault, tableConfDefault)),\n      (Map(tableConf), Seq(tableConfDefault))\n    )\n  ) { case (\n      propertyOverrides: Map[String, String],\n      defaultConfs: Seq[(String, String)]) =>\n    testValidationForCreateDeltaTableCommand(\n      tableExists = true,\n      propertyOverrides,\n      defaultConfs,\n      errorOpt = Some(errCannotOverride))\n  }\n\n  gridTest(\"During CLONE, CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl \" +\n      \"works correctly for new target tables with default Coordinated Commits Configurations.\") (\n    Seq(\n      (Seq.empty, None),\n      (Seq(cNameDefault), Some(errMissingConfInSession(cConfDefaultKey))),\n      (Seq(cNameDefault, cConfDefault), None),\n      (Seq(cNameDefault, cConfDefault, tableConfDefault), Some(errTableConfInSession)),\n      (Seq(tableConfDefault), Some(errTableConfInSession))\n    )\n  ) { case (\n      defaultConfs: Seq[(String, String)],\n      errorOpt: Option[DeltaIllegalArgumentException]) =>\n    testValidationForCreateDeltaTableCommand(\n      tableExists = false,\n      propertyOverrides = Map.empty,\n      defaultConfs,\n      errorOpt)\n  }\n\n  gridTest(\"During CLONE, CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl \" +\n      \"fails for new target tables with any illegal explicit Coordinated Commits Configurations.\") (\n    Seq(\n      (Map(cName), Seq.empty, Some(errMissingConfInCommand(cConfKey))),\n      (Map(cName), Seq(cNameDefault), Some(errMissingConfInCommand(cConfKey))),\n      (Map(cName), Seq(cNameDefault, cConfDefault), Some(errMissingConfInCommand(cConfKey))),\n      (Map(cName), Seq(cNameDefault, cConfDefault, tableConfDefault),\n        Some(errMissingConfInCommand(cConfKey))),\n      (Map(cName), Seq(tableConfDefault), Some(errMissingConfInCommand(cConfKey))),\n\n      (Map(cName, cConf, tableConf), Seq.empty, Some(errTableConfInCommand)),\n      (Map(cName, cConf, tableConf), Seq(cNameDefault), Some(errTableConfInCommand)),\n      (Map(cName, cConf, tableConf), Seq(cNameDefault, cConfDefault), Some(errTableConfInCommand)),\n      (Map(cName, cConf, tableConf), Seq(cNameDefault, cConfDefault, tableConfDefault),\n        Some(errTableConfInCommand)),\n      (Map(cName, cConf, tableConf), Seq(tableConfDefault), Some(errTableConfInCommand)),\n\n      (Map(tableConf), Seq.empty, Some(errTableConfInCommand)),\n      (Map(tableConf), Seq(cNameDefault), Some(errTableConfInCommand)),\n      (Map(tableConf), Seq(cNameDefault, cConfDefault), Some(errTableConfInCommand)),\n      (Map(tableConf), Seq(cNameDefault, cConfDefault, tableConfDefault),\n        Some(errTableConfInCommand)),\n      (Map(tableConf), Seq(tableConfDefault), Some(errTableConfInCommand))\n    )\n  ) { case (\n      propertyOverrides: Map[String, String],\n      defaultConfs: Seq[(String, String)],\n      errorOpt: Option[DeltaIllegalArgumentException]) =>\n    testValidationForCreateDeltaTableCommand(\n      tableExists = false,\n      propertyOverrides,\n      defaultConfs,\n      errorOpt)\n  }\n\n  gridTest(\"During CLONE, CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl \" +\n      \"passes for new target tables with legal explicit Coordinated Commits Configurations.\") (\n    Seq(\n      // Having exactly Coordinator Name and Coordinator Conf explicitly, but having an illegal\n      // combination of Coordinated Commits configurations in default: pass.\n      // This is because we don't consider default configurations when explicit ones are provided.\n      Seq.empty,\n      Seq(cNameDefault),\n      Seq(cNameDefault, cConfDefault),\n      Seq(cNameDefault, cConfDefault, tableConfDefault),\n      Seq(tableConfDefault)\n    )\n  ) { defaultConfs: Seq[(String, String)] =>\n    testValidationForCreateDeltaTableCommand(\n      tableExists = false,\n      propertyOverrides = Map(cName, cConf),\n      defaultConfs,\n      errorOpt = None)\n  }\n\n  private def testValidateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n      existingConfs: Map[String, String],\n      propertyOverrides: Map[String, String],\n      errorOpt: Option[DeltaIllegalArgumentException]): Unit = {\n    if (errorOpt.isDefined) {\n      val e = intercept[DeltaIllegalArgumentException] {\n        CoordinatedCommitsUtils.validateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n          existingConfs, propertyOverrides)\n      }\n      checkError(\n        e,\n        errorOpt.get.getErrorClass,\n        sqlState = errorOpt.get.getSqlState,\n        parameters = errorOpt.get.getMessageParameters.asScala.toMap)\n    } else {\n      CoordinatedCommitsUtils.validateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n        existingConfs, propertyOverrides)\n    }\n  }\n\n  gridTest(\"During ALTER, `validateConfigurationsForAlterTableSetPropertiesDeltaCommand` \" +\n      \"works correctly for tables without Coordinated Commits configurations.\") {\n    Seq(\n      (Map.empty, None),\n      (Map(cName), Some(new DeltaIllegalArgumentException(\n        \"DELTA_MUST_SET_ALL_COORDINATED_COMMITS_CONFS_IN_COMMAND\", Array(\"ALTER\", cConfKey)))),\n      (Map(cName, cConf), None),\n      (Map(cName, cConf, tableConf), Some(new DeltaIllegalArgumentException(\n        \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\", Array(\"ALTER\", tableConfKey)))),\n      (Map(tableConf), Some(new DeltaIllegalArgumentException(\n        \"DELTA_CONF_OVERRIDE_NOT_SUPPORTED_IN_COMMAND\", Array(\"ALTER\", tableConfKey))))\n    )\n  } { case (\n      propertyOverrides: Map[String, String],\n      errorOpt: Option[DeltaIllegalArgumentException]) =>\n    testValidateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n      existingConfs = Map.empty,\n      propertyOverrides,\n      errorOpt)\n  }\n\n  test(\"During ALTER, `validateConfigurationsForAlterTableSetPropertiesDeltaCommand` \" +\n    \"passes with no overrides for tables with Coordinated Commits configurations.\") {\n    testValidateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n      existingConfs = Map(cName, cConf, tableConf),\n      propertyOverrides = Map.empty,\n      errorOpt = None)\n  }\n\n  gridTest(\"During ALTER, `validateConfigurationsForAlterTableSetPropertiesDeltaCommand` \" +\n    \"fails with overrides for tables with Coordinated Commits configurations.\") (\n    Seq(\n      Map(cName),\n      Map(cName, cConf),\n      Map(cName, cConf, tableConf),\n      Map(tableConf)\n    )\n  ) { propertyOverrides: Map[String, String] =>\n    testValidateConfigurationsForAlterTableSetPropertiesDeltaCommand(\n      existingConfs = Map(cName, cConf, tableConf),\n      propertyOverrides,\n      errorOpt = Some(new DeltaIllegalArgumentException(\n        \"DELTA_CANNOT_OVERRIDE_COORDINATED_COMMITS_CONFS\", Array(\"ALTER\"))))\n  }\n\n  private def errCannotUnset = new DeltaIllegalArgumentException(\n    \"DELTA_CANNOT_UNSET_COORDINATED_COMMITS_CONFS\", Array.empty)\n\n  private def testValidateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n      existingConfs: Map[String, String],\n      propKeysToUnset: Seq[String],\n      errorOpt: Option[DeltaIllegalArgumentException]): Unit = {\n    if (errorOpt.isDefined) {\n      val e = intercept[DeltaIllegalArgumentException] {\n        CoordinatedCommitsUtils.validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n          existingConfs, propKeysToUnset)\n      }\n      checkError(\n        e,\n        errorOpt.get.getErrorClass,\n        sqlState = errorOpt.get.getSqlState,\n        parameters = errorOpt.get.getMessageParameters.asScala.toMap)\n    } else {\n      CoordinatedCommitsUtils.validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n        existingConfs, propKeysToUnset)\n    }\n  }\n\n  gridTest(\"During ALTER, `validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand` \" +\n    \"fails with overrides for tables with Coordinated Commits configurations.\") {\n    Seq(\n      Seq(cNameKey),\n      Seq(cNameKey, cConfKey),\n      Seq(cNameKey, cConfKey, tableConfKey),\n      Seq(tableConfKey)\n    )\n  } { propKeysToUnset: Seq[String] =>\n    testValidateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n      existingConfs = Map(cName, cConf, tableConf),\n      propKeysToUnset,\n      errorOpt = Some(errCannotUnset))\n  }\n\n  gridTest(\"During ALTER, `validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand` \" +\n    \"passes with no overrides for tables with or without Coordinated Commits configurations.\") {\n    Seq(\n      Map.empty,\n      Map(cName, cConf, tableConf)\n    )\n  } { case existingConfs: Map[String, String] =>\n    testValidateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n      existingConfs,\n      propKeysToUnset = Seq.empty,\n      errorOpt = None)\n  }\n\n  gridTest(\"During ALTER, `validateConfigurationsForAlterTableUnsetPropertiesDeltaCommand` \" +\n    \"passes with overrides for tables without Coordinated Commits configurations.\") {\n    Seq(\n      Seq(cNameKey),\n      Seq(cNameKey, cConfKey),\n      Seq(cNameKey, cConfKey, tableConfKey),\n      Seq(tableConfKey)\n    )\n  } { propKeysToUnset: Seq[String] =>\n    testValidateConfigurationsForAlterTableUnsetPropertiesDeltaCommand(\n      existingConfs = Map.empty,\n      propKeysToUnset,\n      errorOpt = None)\n  }\n\n  /////////////////////////////////////////////////////////////////////////////////////////////\n  //      Test CoordinatedCommitsUtils.validateCoordinatedCommitsConfigurationsImpl ENDS     //\n  /////////////////////////////////////////////////////////////////////////////////////////////\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/DynamoDBCommitCoordinatorClientSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.util.Optional\nimport java.util.concurrent.locks.ReentrantReadWriteLock\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport com.amazonaws.services.dynamodbv2.{AbstractAmazonDynamoDB, AmazonDynamoDB, AmazonDynamoDBClient}\nimport com.amazonaws.services.dynamodbv2.model.{AttributeValue, ConditionalCheckFailedException, CreateTableRequest, CreateTableResult, DescribeTableResult, GetItemRequest, GetItemResult, PutItemRequest, PutItemResult, ResourceInUseException, ResourceNotFoundException, TableDescription, UpdateItemRequest, UpdateItemResult}\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog}\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport io.delta.dynamodbcommitcoordinator.{DynamoDBCommitCoordinatorClient, DynamoDBCommitCoordinatorClientBuilder}\nimport io.delta.storage.commit.{CommitCoordinatorClient, CommitFailedException => JCommitFailedException, GetCommitsResponse => JGetCommitsResponse}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.SparkSession\n\n/**\n * An in-memory implementation of DynamoDB client for testing. Only the methods used by\n * `DynamoDBCommitCoordinatorClient` are implemented.\n */\nclass InMemoryDynamoDBClient extends AbstractAmazonDynamoDB {\n  /**\n   * The db has multiple tables (outer map). Each table has multiple entries (inner map).\n   */\n  val db: mutable.Map[String, mutable.Map[String, PerEntryData]] = mutable.Map.empty\n  case class PerEntryData(\n      lock: ReentrantReadWriteLock,\n      data: mutable.Map[String, AttributeValue])\n\n  private def getTableData(tableName: String): mutable.Map[String, PerEntryData] = {\n    db.getOrElse(tableName, throw new ResourceNotFoundException(\"table does not exist\"))\n  }\n\n  override def createTable(createTableRequest: CreateTableRequest): CreateTableResult = {\n    val tableName = createTableRequest.getTableName\n    if (db.contains(tableName)) {\n      throw new ResourceInUseException(\"Table already exists\")\n    }\n    db.getOrElseUpdate(tableName, mutable.Map.empty)\n    new CreateTableResult().withTableDescription(\n      new TableDescription().withTableName(tableName));\n  }\n\n  override def describeTable(tableName: String): DescribeTableResult = {\n    if (!db.contains(tableName)) {\n      throw new ResourceNotFoundException(\"table does not exist\")\n    }\n    val tableDesc =\n      new TableDescription().withTableName(tableName).withTableStatus(\"ACTIVE\")\n    new DescribeTableResult().withTable(tableDesc)\n  }\n\n  override def getItem(getItemRequest: GetItemRequest): GetItemResult = {\n    val table = getTableData(getItemRequest.getTableName)\n    val tableId = getItemRequest.getKey.values().iterator().next();\n    val entry = table.getOrElse(tableId.getS,\n      throw new ResourceNotFoundException(\"table does not exist\"))\n    val lock = entry.lock.readLock()\n    try {\n      lock.lock()\n      val result = new GetItemResult()\n      getItemRequest.getAttributesToGet.forEach(attr => {\n        entry.data.get(attr).foreach(result.addItemEntry(attr, _))\n      })\n      result\n    } finally {\n      lock.unlock()\n    }\n  }\n\n  override def putItem(putItemRequest: PutItemRequest): PutItemResult = {\n    val table = getTableData(putItemRequest.getTableName)\n    val item = putItemRequest.getItem\n    val tableId = item.get(\"tableId\").getS\n    if (table.contains(tableId)) {\n      throw new ResourceInUseException(\"table already exists\")\n    }\n    val entry = PerEntryData(new ReentrantReadWriteLock(), item.asScala)\n    // This is not really safe, but tableId is a UUID, so it should be fine.\n    table.put(tableId, entry)\n    new PutItemResult()\n  }\n\n  override def updateItem(request: UpdateItemRequest): UpdateItemResult = {\n    val table = getTableData(request.getTableName)\n    val tableId = request.getKey.values().iterator().next();\n    val entry = table.getOrElse(tableId.getS,\n      throw new ResourceNotFoundException(\"table does not exist\"))\n    val lock = entry.lock.writeLock()\n    try {\n      lock.lock()\n      request.getExpected.forEach((attr, expectedVal) => {\n        val actualVal = entry.data.getOrElse(attr,\n          throw new ConditionalCheckFailedException(\"Expected attr not found\"))\n        if (actualVal != expectedVal.getValue) {\n          throw new ConditionalCheckFailedException(\"Value does not match\")\n        }\n      })\n      request.getAttributeUpdates.forEach((attr, update) => {\n        if (attr != \"commits\") {\n          entry.data.put(attr, update.getValue)\n        } else {\n          val commits = update.getValue.getL.asScala\n          if (update.getAction == \"ADD\") {\n            val existingCommits =\n              entry.data.get(\"commits\").map(_.getL.asScala).getOrElse(List())\n            entry.data.put(\n              \"commits\", new AttributeValue().withL((existingCommits ++ commits).asJava))\n          } else if (update.getAction == \"PUT\") {\n            entry.data.put(\"commits\", update.getValue)\n          } else {\n            throw new IllegalArgumentException(\"Unsupported action\")\n          }\n        }\n      })\n      new UpdateItemResult()\n    } finally {\n      lock.unlock()\n    }\n  }\n}\n\ncase class TestDynamoDBCommitCoordinatorBuilder(batchSize: Long) extends CommitCoordinatorBuilder {\n    override def getName: String = \"test-dynamodb\"\n    override def build(\n        spark: SparkSession, config: Map[String, String]): CommitCoordinatorClient = {\n        new DynamoDBCommitCoordinatorClient(\n          \"testTable\",\n          \"test-endpoint\",\n          new InMemoryDynamoDBClient(),\n          batchSize)\n    }\n}\n\nabstract class DynamoDBCommitCoordinatorClientSuite(batchSize: Long)\n  extends CommitCoordinatorClientImplSuiteBase {\n\n  override protected def createTableCommitCoordinatorClient(\n      deltaLog: DeltaLog): TableCommitCoordinatorClient = {\n    val cs = TestDynamoDBCommitCoordinatorBuilder(batchSize = batchSize).build(spark, Map.empty)\n    val tableConf = cs.registerTable(\n      deltaLog.logPath, Optional.empty(), -1L, Metadata(), Protocol(1, 1))\n    TableCommitCoordinatorClient(cs, deltaLog, tableConf.asScala.toMap)\n  }\n\n  override protected def registerBackfillOp(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      deltaLog: DeltaLog,\n      version: Long): Unit = {\n    tableCommitCoordinatorClient.backfillToVersion(version)\n  }\n\n  override protected def validateBackfillStrategy(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      logPath: Path,\n      version: Long): Unit = {\n    val lastExpectedBackfilledVersion = (version - (version % batchSize)).toInt\n    val unbackfilledCommitVersionsAll = tableCommitCoordinatorClient\n      .getCommits().getCommits.asScala.map(_.getVersion)\n    val expectedVersions = lastExpectedBackfilledVersion + 1 to version.toInt\n\n    assert(unbackfilledCommitVersionsAll == expectedVersions)\n    (0 to lastExpectedBackfilledVersion).foreach { v =>\n      assertBackfilled(v, logPath, Some(v))\n    }\n  }\n\n  protected def validateGetCommitsResult(\n      result: JGetCommitsResponse,\n      startVersion: Option[Long],\n      endVersion: Option[Long],\n      maxVersion: Long): Unit = {\n    val commitVersions = result.getCommits.asScala.map(_.getVersion)\n    val lastExpectedBackfilledVersion = (maxVersion - (maxVersion % batchSize)).toInt\n    val expectedVersions = lastExpectedBackfilledVersion + 1 to maxVersion.toInt\n    assert(commitVersions == expectedVersions)\n    assert(result.getLatestTableVersion == maxVersion)\n  }\n\n  for (skipPathCheck <- Seq(true, false))\n  test(s\"skipPathCheck should work correctly [skipPathCheck = $skipPathCheck]\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      writeCommitZero(logPath)\n      val dynamoDB = new InMemoryDynamoDBClient();\n      val commitCoordinator = new DynamoDBCommitCoordinatorClient(\n        \"testTable\",\n        \"test-endpoint\",\n        dynamoDB,\n        batchSize,\n        1, // readCapacityUnits\n        1, // writeCapacityUnits\n        skipPathCheck)\n      val tableConf = commitCoordinator.registerTable(\n        logPath, Optional.empty(), -1L, Metadata(), Protocol(1, 1))\n      val wrongTablePath = new Path(logPath.getParent, \"wrongTable\")\n      val wrongLogPath = new Path(wrongTablePath, logPath.getName)\n      val fs = wrongLogPath.getFileSystem(log.newDeltaHadoopConf())\n      fs.mkdirs(wrongTablePath)\n      fs.mkdirs(FileNames.commitDirPath(wrongLogPath))\n      val wrongTablePathTableCommitCoordinator = new TableCommitCoordinatorClient(\n        commitCoordinator,\n        wrongLogPath,\n        tableConf.asScala.toMap,\n        log.newDeltaHadoopConf(),\n        log.store\n      )\n      if (skipPathCheck) {\n        // This should succeed because we are skipping the path check.\n        val resp = commit(1L, 1L, wrongTablePathTableCommitCoordinator)\n        assert(resp.getVersion == 1L)\n      } else {\n        val e = intercept[JCommitFailedException] {\n          commit(1L, 1L, wrongTablePathTableCommitCoordinator)\n        }\n        assert(e.getMessage.contains(\"while the table is registered at\"))\n      }\n    }\n  }\n\n  test(\"builder should read dynamic configs from sparkSession\") {\n    class TestDynamoDBCommitCoordinatorBuilder extends DynamoDBCommitCoordinatorClientBuilder {\n      override def getName: String = \"dynamodb-test\"\n      override def createAmazonDDBClient(\n          endpoint: String,\n          credentialProviderName: String,\n          hadoopConf: Configuration): AmazonDynamoDB = {\n        assert(endpoint == \"endpoint-1224\")\n        assert(credentialProviderName == \"creds-1225\")\n        new InMemoryDynamoDBClient()\n      }\n\n      override def getDynamoDBCommitCoordinatorClient(\n          coordinatedCommitsTableName: String,\n          dynamoDBEndpoint: String,\n          ddbClient: AmazonDynamoDB,\n          backfillBatchSize: Long,\n          readCapacityUnits: Int,\n          writeCapacityUnits: Int,\n          skipPathCheck: Boolean): DynamoDBCommitCoordinatorClient = {\n        assert(coordinatedCommitsTableName == \"tableName-1223\")\n        assert(dynamoDBEndpoint == \"endpoint-1224\")\n        assert(backfillBatchSize == 1)\n        assert(readCapacityUnits == 1226)\n        assert(writeCapacityUnits == 1227)\n        assert(skipPathCheck)\n        new DynamoDBCommitCoordinatorClient(\n          coordinatedCommitsTableName,\n          dynamoDBEndpoint,\n          ddbClient,\n          backfillBatchSize,\n          readCapacityUnits,\n          writeCapacityUnits,\n          skipPathCheck)\n      }\n    }\n    val commitCoordinatorConf = JsonUtils.toJson(Map(\n      \"dynamoDBTableName\" -> \"tableName-1223\",\n      \"dynamoDBEndpoint\" -> \"endpoint-1224\"\n    ))\n    withSQLConf(\n        DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_NAME.defaultTablePropertyKey ->\n          \"dynamodb-test\",\n        DeltaConfigs.COORDINATED_COMMITS_COORDINATOR_CONF.defaultTablePropertyKey ->\n          commitCoordinatorConf,\n        DeltaSQLConf.COORDINATED_COMMITS_DDB_AWS_CREDENTIALS_PROVIDER_NAME.key -> \"creds-1225\",\n        DeltaSQLConf.COORDINATED_COMMITS_DDB_SKIP_PATH_CHECK.key -> \"true\",\n        DeltaSQLConf.COORDINATED_COMMITS_DDB_READ_CAPACITY_UNITS.key -> \"1226\",\n        DeltaSQLConf.COORDINATED_COMMITS_DDB_WRITE_CAPACITY_UNITS.key -> \"1227\") {\n      // clear default builders\n      CommitCoordinatorProvider.clearNonDefaultBuilders()\n      CommitCoordinatorProvider.registerBuilder(new TestDynamoDBCommitCoordinatorBuilder())\n      withTempTableDir { tempDir =>\n        val tablePath = tempDir.getAbsolutePath\n        spark.range(1).write.format(\"delta\").mode(\"overwrite\").save(tablePath)\n        val log = DeltaLog.forTable(spark, tempDir.toString)\n        val tableCommitCoordinatorClient = log.snapshot.tableCommitCoordinatorClientOpt.get\n        assert(tableCommitCoordinatorClient\n          .commitCoordinatorClient.isInstanceOf[DynamoDBCommitCoordinatorClient])\n        assert(tableCommitCoordinatorClient.tableConf.contains(\"tableId\"))\n      }\n    }\n  }\n}\n\nclass DynamoDBCommitCoordinatorClient5BackfillSuite extends DynamoDBCommitCoordinatorClientSuite(5)\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/InMemoryCommitCoordinatorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport io.delta.storage.commit.{GetCommitsResponse => JGetCommitsResponse}\nimport org.apache.hadoop.fs.Path\n\nabstract class InMemoryCommitCoordinatorSuite(batchSize: Int)\n  extends CommitCoordinatorClientImplSuiteBase {\n\n  override protected def createTableCommitCoordinatorClient(\n      deltaLog: DeltaLog): TableCommitCoordinatorClient = {\n    val cs = InMemoryCommitCoordinatorBuilder(batchSize).build(spark, Map.empty)\n    val conf = cs.registerTable(\n      deltaLog.logPath, Optional.empty(), -1L, initMetadata, Protocol(1, 1))\n    TableCommitCoordinatorClient(cs, deltaLog, conf.asScala.toMap)\n  }\n\n  override protected def registerBackfillOp(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      deltaLog: DeltaLog,\n      version: Long): Unit = {\n    val commitCoordinatorClient = tableCommitCoordinatorClient.commitCoordinatorClient\n    val inMemoryCS = commitCoordinatorClient.asInstanceOf[InMemoryCommitCoordinator]\n    inMemoryCS.registerBackfill(deltaLog.logPath, version)\n  }\n\n  override protected def validateBackfillStrategy(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      logPath: Path,\n      version: Long): Unit = {\n    val lastExpectedBackfilledVersion = (version - (version % batchSize)).toInt\n    val unbackfilledCommitVersionsAll = tableCommitCoordinatorClient\n      .getCommits().getCommits.asScala.map(_.getVersion)\n    val expectedVersions = lastExpectedBackfilledVersion + 1 to version.toInt\n\n    assert(unbackfilledCommitVersionsAll == expectedVersions)\n    (0 to lastExpectedBackfilledVersion).foreach { v =>\n      assertBackfilled(v, logPath, Some(v))\n    }\n  }\n\n  protected def validateGetCommitsResult(\n      result: JGetCommitsResponse,\n      startVersion: Option[Long],\n      endVersion: Option[Long],\n      maxVersion: Long): Unit = {\n    val commitVersions = result.getCommits.asScala.map(_.getVersion)\n    val lastExpectedBackfilledVersion = (maxVersion - (maxVersion % batchSize)).toInt\n    val expectedVersions = lastExpectedBackfilledVersion + 1 to maxVersion.toInt\n    assert(commitVersions == expectedVersions)\n    assert(result.getLatestTableVersion == maxVersion)\n  }\n\n  test(\"InMemoryCommitCoordinatorBuilder works as expected\") {\n    val builder1 = InMemoryCommitCoordinatorBuilder(5)\n    val cs1 = builder1.build(spark, Map.empty)\n    assert(cs1.isInstanceOf[InMemoryCommitCoordinator])\n    assert(cs1.asInstanceOf[InMemoryCommitCoordinator].batchSize == 5)\n\n    val cs1_again = builder1.build(spark, Map.empty)\n    assert(cs1_again.isInstanceOf[InMemoryCommitCoordinator])\n    assert(cs1 == cs1_again)\n\n    val builder2 = InMemoryCommitCoordinatorBuilder(10)\n    val cs2 = builder2.build(spark, Map.empty)\n    assert(cs2.isInstanceOf[InMemoryCommitCoordinator])\n    assert(cs2.asInstanceOf[InMemoryCommitCoordinator].batchSize == 10)\n    assert(cs2 ne cs1)\n\n    val builder3 = InMemoryCommitCoordinatorBuilder(10)\n    val cs3 = builder3.build(spark, Map.empty)\n    assert(cs3.isInstanceOf[InMemoryCommitCoordinator])\n    assert(cs3.asInstanceOf[InMemoryCommitCoordinator].batchSize == 10)\n    assert(cs3 ne cs2)\n  }\n\n  test(\"test commit > 1 is rejected as first commit\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      val tcs = createTableCommitCoordinatorClient(log)\n\n      // Anything other than version-0 or version-1 should be rejected as the first commit\n      // version-0 will be directly backfilled and won't be recorded in InMemoryCommitCoordinator.\n      // version-1 is what commit-coordinator is accepting.\n      assertCommitFail(2, 1, retryable = false, commit(2, 0, tcs))\n    }\n  }\n}\n\nclass InMemoryCommitCoordinator1Suite extends InMemoryCommitCoordinatorSuite(1)\nclass InMemoryCommitCoordinator5Suite extends InMemoryCommitCoordinatorSuite(5)\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/UCCommitCoordinatorBuilderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport io.delta.storage.commit.uccommitcoordinator.{UCClient, UCCommitCoordinatorClient}\nimport org.mockito.{Mock, Mockito}\nimport org.mockito.ArgumentMatchers.{any, eq => meq}\nimport org.mockito.Mockito.{mock, never, times, verify, when}\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass UCCommitCoordinatorBuilderSuite extends SparkFunSuite with SharedSparkSession {\n\n  @Mock\n  private val mockFactory: UCClientFactory = mock(classOf[UCClientFactory])\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    Mockito.reset(mockFactory)\n    CommitCoordinatorProvider.clearAllBuilders()\n    UCCommitCoordinatorBuilder.ucClientFactory = mockFactory\n    UCCommitCoordinatorBuilder.clearCache()\n    CommitCoordinatorProvider.registerBuilder(UCCommitCoordinatorBuilder)\n  }\n\n  case class CatalogTestConfig(\n    name: String,\n    uri: Option[String] = None,\n    configMap: Map[String, String] = Map.empty,\n    metastoreId: Option[String] = None,\n    path: Option[String] = Some(\"io.unitycatalog.spark.UCSingleCatalog\")\n  )\n\n  def setupCatalogs(configs: CatalogTestConfig*)(testCode: => Unit): Unit = {\n    val allConfigs = configs.flatMap { config =>\n      val baseConfigs = Seq(\n        config.path.map(p => s\"spark.sql.catalog.${config.name}\" -> p),\n        config.uri.map(uri => s\"spark.sql.catalog.${config.name}.uri\" -> uri)\n      ).flatten\n\n      // Add all additional configurations from configMap (without any prefix)\n      val additionalConfigs = config.configMap.map { case (key, value) =>\n        s\"spark.sql.catalog.${config.name}.$key\" -> value\n      }\n\n      baseConfigs ++ additionalConfigs\n    }\n\n    withSQLConf(allConfigs: _*) {\n      configs.foreach { config =>\n        (config.uri, config.configMap.isEmpty, config.metastoreId) match {\n          case (Some(uri), false, Some(id)) =>\n            registerMetastoreId(uri, config.configMap, id)\n          case (Some(uri), false, None) =>\n            registerMetastoreIdException(\n              uri,\n              config.configMap,\n              new RuntimeException(\"Invalid metastore ID\"))\n          case _ => // Do nothing for incomplete configs\n        }\n      }\n      testCode\n    }\n  }\n\n  test(\"build with valid configuration\") {\n    val expectedMetastoreId = \"test-metastore-id\"\n    val catalog1 = CatalogTestConfig(\n      name = \"catalog1\",\n      uri = Some(\"https://test-uri-1.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token-1\"),\n      metastoreId = Some(expectedMetastoreId)\n    )\n    val catalog2 = CatalogTestConfig(\n      name = \"catalog2\",\n      uri = Some(\"https://test-uri-2.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token-2\"),\n      metastoreId = Some(\"different-metastore-id\")\n    )\n\n    setupCatalogs(catalog1, catalog2) {\n      val result = getCommitCoordinatorClient(expectedMetastoreId)\n\n      assert(result.isInstanceOf[UCCommitCoordinatorClient])\n      verify(mockFactory, times(2)).createUCClient(catalog1.uri.get, catalog1.configMap)\n      verify(mockFactory).createUCClient(catalog2.uri.get, catalog2.configMap)\n      verify(mockFactory.createUCClient(catalog1.uri.get, catalog1.configMap))\n        .getMetastoreId\n      verify(mockFactory.createUCClient(catalog2.uri.get, catalog2.configMap))\n        .getMetastoreId\n      verify(mockFactory.createUCClient(catalog2.uri.get, catalog2.configMap)).close()\n      verify(mockFactory.createUCClient(catalog1.uri.get, catalog1.configMap)).close()\n    }\n  }\n\n  test(\"token based rest client factory default app versions\") {\n    val defaults = UCTokenBasedRestClientFactory.defaultAppVersions\n    assert(defaults(\"Delta\") === io.delta.VERSION)\n    assert(defaults(\"Spark\") === org.apache.spark.SPARK_VERSION)\n    assert(defaults(\"Scala\") === scala.util.Properties.versionNumberString)\n    assert(defaults(\"Java\") === System.getProperty(\"java.version\"))\n  }\n\n  test(\"createUCClientWithVersions passes custom app versions to UCClient\") {\n    val customVersions = Map(\n      \"Delta\" -> io.delta.VERSION,\n      \"Kernel\" -> \"4.0.0\",\n      \"Delta V2 connector\" -> \"true\"\n    )\n    val defaults = UCTokenBasedRestClientFactory.defaultAppVersions\n    val merged = defaults ++ customVersions\n    assert(merged(\"Kernel\") === \"4.0.0\")\n    assert(merged(\"Delta V2 connector\") === \"true\")\n    assert(merged(\"Delta\") === io.delta.VERSION)\n    assert(merged(\"Spark\") === org.apache.spark.SPARK_VERSION)\n  }\n\n  test(\"build with missing metastore ID\") {\n    val exception = intercept[IllegalArgumentException] {\n      CommitCoordinatorProvider.getCommitCoordinatorClient(\n        UCCommitCoordinatorBuilder.getName,\n        Map.empty,\n        spark)\n    }\n    assert(exception.getMessage.contains(\"UC metastore ID not found\"))\n  }\n\n  test(\"build with no matching catalog\") {\n    val metastoreId = \"test-metastore-id\"\n    val catalog = CatalogTestConfig(\n      name = \"catalog\",\n      uri = Some(\"https://test-uri.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token\"),\n      metastoreId = Some(\"different-metastore-id\")\n    )\n\n    setupCatalogs(catalog) {\n      val exception = intercept[IllegalStateException] {\n        getCommitCoordinatorClient(metastoreId)\n      }\n      assert(exception.getMessage.contains(\"No matching catalog found\"))\n      verify(mockFactory).createUCClient(catalog.uri.get, catalog.configMap)\n      verify(mockFactory.createUCClient(catalog.uri.get, catalog.configMap)).getMetastoreId\n      verify(mockFactory.createUCClient(catalog.uri.get, catalog.configMap)).close()\n    }\n  }\n\n  test(\"build with multiple matching catalogs\") {\n    val metastoreId = \"test-metastore-id\"\n    val catalog1 = CatalogTestConfig(\n      name = \"catalog1\",\n      uri = Some(\"https://test-uri1.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token-1\"),\n      metastoreId = Some(metastoreId)\n    )\n    val catalog2 = CatalogTestConfig(\n      name = \"catalog2\",\n      uri = Some(\"https://test-uri2.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token-2\"),\n      metastoreId = Some(metastoreId)\n    )\n\n    setupCatalogs(catalog1, catalog2) {\n      val exception = intercept[IllegalStateException] {\n        getCommitCoordinatorClient(metastoreId)\n      }\n      assert(exception.getMessage.contains(\"Found multiple catalogs\"))\n      verify(mockFactory).createUCClient(catalog1.uri.get, catalog1.configMap)\n      verify(mockFactory).createUCClient(catalog2.uri.get, catalog2.configMap)\n      verify(mockFactory.createUCClient(catalog1.uri.get, catalog1.configMap))\n        .getMetastoreId\n      verify(mockFactory.createUCClient(catalog2.uri.get, catalog2.configMap))\n        .getMetastoreId\n      verify(mockFactory.createUCClient(catalog1.uri.get, catalog1.configMap)).close()\n      verify(mockFactory.createUCClient(catalog2.uri.get, catalog2.configMap)).close()\n    }\n  }\n\n  test(\"build with mixed valid and invalid catalog configurations\") {\n    val expectedMetastoreId = \"test-metastore-id\"\n    val validCatalog = CatalogTestConfig(\n      name = \"valid-catalog\",\n      uri = Some(\"https://valid-uri.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"valid-token\"),\n      metastoreId = Some(expectedMetastoreId)\n    )\n    val invalidCatalog1 = CatalogTestConfig(\n      name = \"invalid-catalog-1\",\n      uri = Some(\"https://invalid-uri.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"invalid-token\"),\n      metastoreId = None\n    )\n    val invalidCatalog2 = CatalogTestConfig(\n      name = \"invalid-catalog-2\",\n      uri = Some(\"random-uri\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"invalid-token\")\n    )\n    val incompleteCatalog = CatalogTestConfig(\n      name = \"incomplete-catalog\",\n      path = None\n    )\n\n    setupCatalogs(validCatalog, invalidCatalog1, invalidCatalog2, incompleteCatalog) {\n      val result = getCommitCoordinatorClient(expectedMetastoreId)\n\n      assert(result.isInstanceOf[UCCommitCoordinatorClient])\n      verify(mockFactory, times(2)).createUCClient(\n        validCatalog.uri.get,\n        validCatalog.configMap\n      )\n      verify(mockFactory.createUCClient(validCatalog.uri.get, validCatalog.configMap),\n        times(1)).close()\n    }\n  }\n\n  test(\"build caching behavior\") {\n    val metastoreId = \"test-metastore-id\"\n    val catalog = CatalogTestConfig(\n      name = \"catalog\",\n      uri = Some(\"https://test-uri.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token\"),\n      metastoreId = Some(metastoreId)\n    )\n\n    setupCatalogs(catalog) {\n      val result1 = getCommitCoordinatorClient(metastoreId)\n      val result2 = getCommitCoordinatorClient(metastoreId)\n      assert(result1 eq result2)\n    }\n  }\n\n  test(\"build with multiple catalogs pointing to the same URI, token, and metastore\") {\n    val metastoreId = \"shared-metastore-id\"\n    val sharedUri = \"https://shared-test-uri.com\"\n    val sharedConfigMap = Map(\"type\" -> \"static\", \"token\" -> \"shared-test-token\")\n    val catalog1 = CatalogTestConfig(\n      name = \"catalog1\",\n      uri = Some(sharedUri),\n      configMap = sharedConfigMap,\n      metastoreId = Some(metastoreId)\n    )\n    val catalog2 = CatalogTestConfig(\n      name = \"catalog2\",\n      uri = Some(sharedUri),\n      configMap = sharedConfigMap,\n      metastoreId = Some(metastoreId)\n    )\n    val catalog3 = CatalogTestConfig(\n      name = \"catalog3\",\n      uri = Some(sharedUri),\n      configMap = sharedConfigMap,\n      metastoreId = Some(metastoreId)\n    )\n\n    setupCatalogs(catalog1, catalog2, catalog3) {\n      val result = getCommitCoordinatorClient(metastoreId)\n\n      assert(result.isInstanceOf[UCCommitCoordinatorClient])\n      verify(mockFactory, times(2)).createUCClient(sharedUri, sharedConfigMap)\n      verify(mockFactory.createUCClient(sharedUri, sharedConfigMap)).getMetastoreId\n      verify(mockFactory.createUCClient(sharedUri, sharedConfigMap)).close()\n    }\n  }\n\n  test(\"build with a catalog having invalid path but valid URI and token\") {\n    val metastoreId = \"test-metastore-id\"\n    val catalog = CatalogTestConfig(\n      name = \"invalid-path-catalog\",\n      uri = Some(\"https://test-uri.com\"),\n      configMap = Map(\"type\" -> \"static\", \"token\" -> \"test-token\"),\n      metastoreId = Some(metastoreId),\n      path = Some(\"invalid-catalog-path\")\n    )\n\n    setupCatalogs(catalog) {\n      assert(UCCommitCoordinatorBuilder.getCatalogConfigs(spark).isEmpty)\n      val e = intercept[IllegalStateException] {\n        getCommitCoordinatorClient(metastoreId)\n      }\n      assert(e.getMessage.contains(\"No matching catalog found\"))\n      verify(mockFactory, never()).createUCClient(catalog.uri.get, catalog.configMap)\n    }\n  }\n\n  private def registerMetastoreId(\n      uri: String,\n      configMap: Map[String, String],\n      metastoreId: String): Unit = {\n    val mockClient = org.mockito.Mockito.mock(classOf[UCClient])\n    when(mockClient.getMetastoreId).thenReturn(metastoreId)\n    when(mockFactory.createUCClient(meq(uri), meq(configMap))).thenReturn(mockClient)\n  }\n\n  private def registerMetastoreIdException(\n      uri: String,\n      configMap: Map[String, String],\n      exception: Throwable): Unit = {\n    val mockClient = org.mockito.Mockito.mock(classOf[UCClient])\n    when(mockClient.getMetastoreId).thenThrow(exception)\n    when(mockFactory.createUCClient(meq(uri), meq(configMap))).thenReturn(mockClient)\n  }\n\n  test(\"getCatalogConfigs with legacy token format\") {\n    val catalogName = \"legacy_catalog\"\n    val uri = \"https://test-uri.com\"\n    val token = \"test-token\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.token\" -> token\n    ) {\n      val configs = UCCommitCoordinatorBuilder.getCatalogConfigs(spark)\n      assert(configs.length == 1)\n\n      val (name, catalogUri, authConfigMap) = configs.head\n      assert(name == catalogName)\n      assert(catalogUri == uri)\n\n      // Legacy token should be converted to new format\n      assert(authConfigMap.contains(\"type\"))\n      assert(authConfigMap(\"type\") == \"static\")\n      assert(authConfigMap.contains(\"token\"))\n      assert(authConfigMap(\"token\") == token)\n    }\n  }\n\n  test(\"getCatalogConfigs with new auth.* format\") {\n    val catalogName = \"new_catalog\"\n    val uri = \"https://test-uri.com\"\n    val token = \"test-token\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.auth.type\" -> \"static\",\n      s\"spark.sql.catalog.$catalogName.auth.token\" -> token\n    ) {\n      val configs = UCCommitCoordinatorBuilder.getCatalogConfigs(spark)\n      assert(configs.length == 1)\n\n      val (name, catalogUri, authConfigMap) = configs.head\n      assert(name == catalogName)\n      assert(catalogUri == uri)\n      assert(authConfigMap(\"type\") == \"static\")\n      assert(authConfigMap(\"token\") == token)\n    }\n  }\n\n  test(\"getCatalogConfigs with nested auth.* configurations\") {\n    val catalogName = \"oauth_catalog\"\n    val uri = \"https://test-uri.com\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.auth.type\" -> \"oauth\",\n      s\"spark.sql.catalog.$catalogName.auth.oauth.uri\" -> \"https://oauth.example.com\",\n      s\"spark.sql.catalog.$catalogName.auth.oauth.client_id\" -> \"client123\",\n      s\"spark.sql.catalog.$catalogName.auth.oauth.client_secret\" -> \"secret456\"\n    ) {\n      val configs = UCCommitCoordinatorBuilder.getCatalogConfigs(spark)\n      assert(configs.length == 1)\n\n      val (name, catalogUri, authConfigMap) = configs.head\n      assert(name == catalogName)\n      assert(catalogUri == uri)\n      assert(authConfigMap(\"type\") == \"oauth\")\n      assert(authConfigMap(\"oauth.uri\") == \"https://oauth.example.com\")\n      assert(authConfigMap(\"oauth.client_id\") == \"client123\")\n      assert(authConfigMap(\"oauth.client_secret\") == \"secret456\")\n    }\n  }\n\n  test(\"getCatalogConfigs skips catalog with no auth configurations\") {\n    val catalogName = \"no_auth_catalog\"\n    val uri = \"https://test-uri.com\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri\n    ) {\n      val configs = UCCommitCoordinatorBuilder.getCatalogConfigs(spark)\n      assert(configs.isEmpty, \"Catalog without auth config should be skipped\")\n    }\n  }\n\n  test(\"getCatalogConfigs prefers new auth.* format over legacy token\") {\n    val catalogName = \"mixed_catalog\"\n    val uri = \"https://test-uri.com\"\n    val legacyToken = \"legacy-token\"\n    val newToken = \"new-token\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.token\" -> legacyToken,\n      s\"spark.sql.catalog.$catalogName.auth.type\" -> \"static\",\n      s\"spark.sql.catalog.$catalogName.auth.token\" -> newToken\n    ) {\n      val configs = UCCommitCoordinatorBuilder.getCatalogConfigs(spark)\n      assert(configs.length == 1)\n\n      val (name, catalogUri, authConfigMap) = configs.head\n      assert(name == catalogName)\n      assert(catalogUri == uri)\n      // New format should take precedence\n      assert(authConfigMap(\"type\") == \"static\")\n      assert(authConfigMap(\"token\") == newToken)\n      assert(!authConfigMap.contains(legacyToken))\n    }\n  }\n\n  test(\"getCatalogConfigs handles multiple catalogs with mixed formats\") {\n    withSQLConf(\n      \"spark.sql.catalog.catalog1\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      \"spark.sql.catalog.catalog1.uri\" -> \"https://uri1.com\",\n      \"spark.sql.catalog.catalog1.token\" -> \"token1\",\n      \"spark.sql.catalog.catalog2\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      \"spark.sql.catalog.catalog2.uri\" -> \"https://uri2.com\",\n      \"spark.sql.catalog.catalog2.auth.type\" -> \"static\",\n      \"spark.sql.catalog.catalog2.auth.token\" -> \"token2\",\n      \"spark.sql.catalog.catalog3\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      \"spark.sql.catalog.catalog3.uri\" -> \"https://uri3.com\"\n    ) {\n      val configs = UCCommitCoordinatorBuilder.getCatalogConfigs(spark)\n      // Only catalog1 and catalog2 should be included (catalog3 has no auth)\n      assert(configs.length == 2)\n\n      val catalog1 = configs.find(_._1 == \"catalog1\")\n      assert(catalog1.isDefined)\n      assert(catalog1.get._3(\"type\") == \"static\")\n      assert(catalog1.get._3(\"token\") == \"token1\")\n\n      val catalog2 = configs.find(_._1 == \"catalog2\")\n      assert(catalog2.isDefined)\n      assert(catalog2.get._3(\"type\") == \"static\")\n      assert(catalog2.get._3(\"token\") == \"token2\")\n    }\n  }\n\n  test(\"buildForCatalog with legacy token format\") {\n    val catalogName = \"test_catalog\"\n    val uri = \"https://test-uri.com\"\n    val token = \"test-token\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.token\" -> token\n    ) {\n      val result = UCCommitCoordinatorBuilder.buildForCatalog(spark, catalogName)\n      assert(result.isInstanceOf[UCCommitCoordinatorClient])\n\n      // Verify that createUCClient was called with the converted auth config\n      verify(mockFactory).createUCClient(\n        meq(uri),\n        any[Map[String, String]]()\n      )\n    }\n  }\n\n  test(\"buildForCatalog with new auth.* format\") {\n    val catalogName = \"test_catalog\"\n    val uri = \"https://test-uri.com\"\n    val token = \"test-token\"\n\n    withSQLConf(\n      s\"spark.sql.catalog.$catalogName\" -> \"io.unitycatalog.spark.UCSingleCatalog\",\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.auth.type\" -> \"static\",\n      s\"spark.sql.catalog.$catalogName.auth.token\" -> token\n    ) {\n      val result = UCCommitCoordinatorBuilder.buildForCatalog(spark, catalogName)\n      assert(result.isInstanceOf[UCCommitCoordinatorClient])\n\n      verify(mockFactory).createUCClient(\n        meq(uri),\n        any[Map[String, String]]()\n      )\n    }\n  }\n\n  test(\"buildForCatalog with non-existent catalog\") {\n    val exception = intercept[IllegalArgumentException] {\n      UCCommitCoordinatorBuilder.buildForCatalog(spark, \"non_existent_catalog\")\n    }\n    assert(exception.getMessage.contains(\"not found\"))\n  }\n\n  private def getCommitCoordinatorClient(metastoreId: String) = {\n    CommitCoordinatorProvider.getCommitCoordinatorClient(\n      UCCommitCoordinatorBuilder.getName,\n      Map(UCCommitCoordinatorClient.UC_METASTORE_ID_KEY -> metastoreId),\n      spark)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/UCCommitCoordinatorClientSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.io.IOException\nimport java.lang.{Long => JLong}\nimport java.util.{List => JList, Optional}\n\nimport scala.collection.JavaConverters._\nimport scala.reflect.ClassTag\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.{Log4jUsageLogger, UsageRecord}\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaIllegalArgumentException, DeltaLog, LogSegment, Snapshot}\nimport org.apache.spark.sql.delta.CommitCoordinatorGetCommitsFailedException\nimport org.apache.spark.sql.delta.DeltaConfigs.{COORDINATED_COMMITS_COORDINATOR_CONF, COORDINATED_COMMITS_COORDINATOR_NAME, COORDINATED_COMMITS_TABLE_CONF}\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{CommitInfo, Metadata, Protocol}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LogStoreInverseAdaptor\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.{\n  Commit => JCommit,\n  CommitFailedException => JCommitFailedException,\n  CoordinatedCommitsUtils => JCoordinatedCommitsUtils,\n  TableDescriptor,\n  UpdatedActions\n}\nimport io.delta.storage.commit.uccommitcoordinator.{\n  UCCommitCoordinatorClient,\n  UCCoordinatedCommitsUsageLogs}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileStatus, FileSystem, LocalFileSystem, Path}\nimport org.mockito.ArgumentMatchers.anyString\nimport org.mockito.Mockito\nimport org.mockito.Mockito.{mock, when}\nimport org.scalatest.PrivateMethodTester\nimport org.scalatest.time.SpanSugar._\n\nimport org.apache.spark.{SparkConf, SparkException}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.util.SystemClock\n\nclass UCCommitCoordinatorClientSuite extends UCCommitCoordinatorClientSuiteBase\n    with PrivateMethodTester\n{\n  protected override def sparkConf = super.sparkConf\n      .set(\"spark.sql.catalog.main\", \"io.unitycatalog.spark.UCSingleCatalog\")\n      .set(\"spark.sql.catalog.main.uri\", \"https://test-uri.com\")\n      .set(\"spark.sql.catalog.main.token\", \"test-token\")\n      .set(\"spark.hadoop.fs.file.impl\", classOf[LocalFileSystem].getCanonicalName)\n\n  override protected def commit(\n      version: Long,\n      timestamp: Long,\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      tableIdentifier: Option[TableIdentifier] = None): JCommit = {\n    val commitResult = super.commit(\n      version, timestamp, tableCommitCoordinatorClient, tableIdentifier)\n    // As backfilling for UC happens after every commit asynchronously, we block here until\n    // the current in-progress backfill has completed in order to make tests deterministic.\n    waitForBackfill(version, tableCommitCoordinatorClient)\n    commitResult\n  }\n  protected def assertUsageLogsContains(usageLogs: Seq[UsageRecord], opType: String): Unit = {\n    assert(usageLogs.exists { record =>\n      record.tags.get(\"opType\").contains(opType)\n    })\n  }\n\n  test(\"incorrect last known backfilled version\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n      tableCommitCoordinatorClient.commitCoordinatorClient.registerTable(\n        logPath, Optional.empty(), -1L, initMetadata, Protocol(1, 1))\n      // Write 11 commits.\n      writeCommitZero(logPath)\n      (1 to 10).foreach(i => commit(i, i, tableCommitCoordinatorClient))\n      // Now delete some backfilled versions\n      val fs = logPath.getFileSystem(log.newDeltaHadoopConf())\n      fs.delete(FileNames.unsafeDeltaFile(logPath, 8), false)\n      fs.delete(FileNames.unsafeDeltaFile(logPath, 9), false)\n      fs.delete(FileNames.unsafeDeltaFile(logPath, 10), false)\n      // Backfill with the wrong specified last version\n      val e = intercept[IllegalStateException] {\n        tableCommitCoordinatorClient.backfillToVersion(10L, Some(9L))\n      }\n      assert(e.getMessage.contains(\"Last known backfilled version 9 doesn't exist\"))\n      // Backfill with the correct version\n      tableCommitCoordinatorClient.backfillToVersion(10L, Some(7L))\n      // Everything should be backfilled now\n      validateBackfillStrategy(tableCommitCoordinatorClient, logPath, 10)\n    }\n  }\n\n  test(\"test getLastKnownBackfilledVersion\") {\n    withTempTableDir { tempDir =>\n      val backfillListingOffset = 5\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      UCCommitCoordinatorClient.BACKFILL_LISTING_OFFSET = backfillListingOffset\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n      tableCommitCoordinatorClient.commitCoordinatorClient.registerTable(\n        logPath, Optional.empty(), -1L, initMetadata, Protocol(1, 1))\n      val hadoopConf = log.newDeltaHadoopConf()\n      val fs = logPath.getFileSystem(hadoopConf)\n\n      writeCommitZero(logPath)\n      val backfillThreshold = 5\n      (1 to backfillThreshold + backfillListingOffset + 5).foreach {\n          commitVersion =>\n        commit(commitVersion, commitVersion, tableCommitCoordinatorClient)\n        if (commitVersion > backfillThreshold) {\n          // After x = backfillThreshold commits, delete all backfilled files to simulate\n          // backfill failing. This means UC should keep track of all commits starting\n          // from x and nothing >= x should be backfilled.\n          (backfillThreshold + 1 to commitVersion).foreach { deleteVersion =>\n              fs.delete(FileNames.unsafeDeltaFile(logPath, deleteVersion), false)\n            }\n          val tableDesc = new TableDescriptor(\n            logPath, Optional.empty(), tableCommitCoordinatorClient.tableConf.asJava)\n\n          val ucCommitCoordinatorClient = tableCommitCoordinatorClient.commitCoordinatorClient\n            .asInstanceOf[UCCommitCoordinatorClient]\n          assert(\n            ucCommitCoordinatorClient.getLastKnownBackfilledVersion(\n              commitVersion,\n              hadoopConf,\n              LogStoreInverseAdaptor(log.store, hadoopConf),\n              tableDesc\n            ) == backfillThreshold\n          )\n        }\n      }\n    }\n  }\n\n  test(\"commit-limit-reached exception handling\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val logPath = log.logPath\n      // Create a client that does not register backfills to keep accumulating\n      // commits in the commit coordinator.\n      val noBackfillRegistrationClient =\n        new UCCommitCoordinatorClient(Map.empty[String, String].asJava, ucClient)\n          with DeltaLogging {\n          override def backfillToVersion(\n              logStore: LogStore,\n              hadoopConf: Configuration,\n              tableDesc: TableDescriptor,\n              version: Long,\n              lastKnownBackfilledVersion: JLong): Unit = {\n            throw new IOException(\"Simulated exception\")\n          }\n\n          override protected def recordDeltaEvent(opType: String, data: Any, path: Path): Unit = {\n            data match {\n              case ref: AnyRef =>\n                recordDeltaEvent(null, opType = opType, data = ref, path = Some(path))\n            }\n          }\n        }\n      // Client 1 performs backfills correctly.\n      val tcc1 = createTableCommitCoordinatorClient(log)\n      // Client 2 does not backfill.\n      val tcc2 = tcc1.copy(commitCoordinatorClient = noBackfillRegistrationClient)\n\n      // Write 10 commits to fill up the commit coordinator (MAX_NUM_COMMITS is set to 10\n      // in the InMemoryUCCommitCoordinator).\n      writeCommitZero(logPath)\n      // We use super.commit here because tco2 does not backfill so the local override of\n      // commit would fail waiting for the commits to be backfilled. This also applies\n      // to the retry of commit 11 with tco2 below.\n      (1 to 10).foreach(i =>\n        super.commit(version = i, timestamp = i, tableCommitCoordinatorClient = tcc2)\n      )\n      // Commit 11 should trigger an exception and a full backfill should be attempted.\n      // With tcc2, this backfill attempt should again fail, leading to a user facing\n      // CommitLimitReachedException, along with the usage logs.\n      var usageLogs = Log4jUsageLogger.track {\n        val e1 = intercept[JCommitFailedException] {\n          super.commit(version = 11, timestamp = 11, tableCommitCoordinatorClient = tcc2)\n        }\n        val tableId = tcc2.tableConf(UCCommitCoordinatorClient.UC_TABLE_ID_KEY)\n        assert(e1.getMessage.contains(s\"Too many unbackfilled commits for $tableId.\"))\n        assert(e1.getMessage.contains(s\"A full backfill attempt failed due to: \" +\n          \"java.io.IOException: Simulated exception\"))\n      }\n      assertUsageLogsContains(\n        usageLogs, UCCoordinatedCommitsUsageLogs.UC_FULL_BACKFILL_ATTEMPT_FAILED)\n      // Retry commit 11 with tcc1. This should again trigger an exception and a full\n      // backfill should be attempted but the backfill should succeed this time. The\n      // commit is then retried automatically and should succeed. We use the local\n      // override of commit here to ensure that we only return once commit 11 has\n      // been backfilled and the remaining asserts pass.\n      usageLogs = Log4jUsageLogger.track {\n        commit(version = 11, timestamp = 11, tableCommitCoordinatorClient = tcc1)\n      }\n      assertUsageLogsContains(usageLogs, UCCoordinatedCommitsUsageLogs.UC_ATTEMPT_FULL_BACKFILL)\n      validateBackfillStrategy(tcc1, logPath, version = 11)\n    }\n  }\n\n  test(\"usage logs in commit calls are emitted correctly\") {\n    withTempTableDir { tempDir =>\n      val log = DeltaLog.forTable(spark, tempDir.toString)\n      val eventLoggerClient =\n        new UCCommitCoordinatorClient(Map.empty[String, String].asJava, ucClient)\n          with DeltaLogging {\n          override protected def recordDeltaEvent(opType: String, data: Any, path: Path): Unit = {\n            data match {\n              case ref: AnyRef =>\n                recordDeltaEvent(null, opType = opType, data = ref, path = Some(path))\n            }\n          }\n        }\n      val logPath = log.logPath\n      val tableCommitCoordinatorClient = createTableCommitCoordinatorClient(log)\n        .copy(commitCoordinatorClient = eventLoggerClient)\n      writeCommitZero(logPath)\n      // A normal commit should emit one usage log.\n      val usageLogs = Log4jUsageLogger.track {\n        commit(version = 1, timestamp = 1, tableCommitCoordinatorClient)\n      }\n      assertUsageLogsContains(usageLogs, UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS)\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/coordinatedcommits/UCCommitCoordinatorClientSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.coordinatedcommits\n\nimport java.io.File\nimport java.net.URI\nimport java.util.{Collections, Optional, UUID}\n\nimport scala.collection.JavaConverters._\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaConfigs.{COORDINATED_COMMITS_COORDINATOR_CONF, COORDINATED_COMMITS_COORDINATOR_NAME, COORDINATED_COMMITS_TABLE_CONF}\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol}\nimport org.apache.spark.sql.delta.metering.DeltaLogging\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport io.delta.storage.commit.{\n  CoordinatedCommitsUtils => JCoordinatedCommitsUtils,\n  GetCommitsResponse => JGetCommitsResponse\n}\nimport io.delta.storage.commit.uccommitcoordinator.{UCClient, UCCommitCoordinatorClient}\nimport org.apache.hadoop.fs.Path\nimport org.mockito.ArgumentMatchers.{any, anyString}\nimport org.mockito.Mock\nimport org.mockito.Mockito\nimport org.mockito.Mockito.{mock, when}\nimport org.scalatest.time.SpanSugar._\n\nimport org.apache.spark.sql.types.{IntegerType, StringType, StructField}\n\ntrait UCCommitCoordinatorClientSuiteBase extends CommitCoordinatorClientImplSuiteBase\n  {\n  /**\n   * A unique table ID for each test.\n   */\n  protected var tableUUID = UUID.randomUUID()\n\n  /**\n   * A unique metastore ID for each test.\n   */\n  protected var metastoreId = UUID.randomUUID()\n\n  protected var ucClient: UCClient = _\n\n  @Mock\n  protected var mockFactory: UCClientFactory = _\n\n  protected var ucCommitCoordinator: InMemoryUCCommitCoordinator = _\n\n  protected override def beforeAll(): Unit = {\n    val tmpDirName = System.getProperty(\"java.io.tmpdir\")\n    val tmpDir = new File(tmpDirName)\n    if (!tmpDir.exists()) {\n      tmpDir.mkdirs()\n    }\n    super.beforeAll()\n    mockFactory = mock(classOf[UCClientFactory])\n  }\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    tableUUID = UUID.randomUUID()\n    UCCommitCoordinatorClient.BACKFILL_LISTING_OFFSET = 100\n    metastoreId = UUID.randomUUID()\n    DeltaLog.clearCache()\n    Mockito.reset(mockFactory)\n    CommitCoordinatorProvider.clearAllBuilders()\n    UCCommitCoordinatorBuilder.ucClientFactory = mockFactory\n    UCCommitCoordinatorBuilder.clearCache()\n    CommitCoordinatorProvider.registerBuilder(UCCommitCoordinatorBuilder)\n    ucCommitCoordinator = new InMemoryUCCommitCoordinator()\n    ucClient = new InMemoryUCClient(metastoreId.toString, ucCommitCoordinator)\n    when(mockFactory.createUCClient(anyString(), any[Map[String, String]]())).thenReturn(ucClient)\n  }\n  override protected def createTableCommitCoordinatorClient(\n      deltaLog: DeltaLog): TableCommitCoordinatorClient = {\n    var commitCoordinatorClient = UCCommitCoordinatorBuilder\n      .build(spark, Map(UCCommitCoordinatorClient.UC_METASTORE_ID_KEY -> metastoreId.toString))\n      .asInstanceOf[UCCommitCoordinatorClient]\n    commitCoordinatorClient = new UCCommitCoordinatorClient(\n      commitCoordinatorClient.conf,\n      commitCoordinatorClient.ucClient) with DeltaLogging {\n      override def recordDeltaEvent(opType: String, data: Any, path: Path): Unit = {\n        data match {\n          case ref: AnyRef => recordDeltaEvent(null, opType = opType, data = ref, path = Some(path))\n          case _ => super.recordDeltaEvent(opType, data, path)\n        }\n      }\n    }\n    // Initialize table ID for the calling test\n    // tableUUID = UUID.randomUUID().toString\n    commitCoordinatorClient.registerTable(\n      deltaLog.logPath, Optional.empty(), -1L, initMetadata(), Protocol(1, 1))\n    TableCommitCoordinatorClient(\n      commitCoordinatorClient,\n      deltaLog,\n      Map(UCCommitCoordinatorClient.UC_TABLE_ID_KEY -> tableUUID.toString)\n    )\n  }\n\n  override protected def registerBackfillOp(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      deltaLog: DeltaLog,\n      version: Long): Unit = {\n    ucClient.commit(\n      tableUUID.toString,\n      JCoordinatedCommitsUtils.getTablePath(deltaLog.logPath).toUri,\n      Optional.empty(),\n      Optional.of(version),\n      false,\n      Optional.empty(),\n      Optional.empty(),\n      Optional.empty() /* icebergMetadata */)\n  }\n\n  override protected def validateBackfillStrategy(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient,\n      logPath: Path,\n      version: Long): Unit = {\n    val response = tableCommitCoordinatorClient.getCommits()\n    assert(response.getCommits.size == 1)\n    assert(response.getCommits.asScala.head.getVersion == version)\n    assert(response.getLatestTableVersion == version)\n  }\n\n  protected def validateGetCommitsResult(\n      response: JGetCommitsResponse,\n      startVersion: Option[Long],\n      endVersion: Option[Long],\n      maxVersion: Long): Unit = {\n    val expectedVersions = endVersion.map { _ => Seq.empty }.getOrElse(Seq(maxVersion))\n    assert(response.getCommits.asScala.map(_.getVersion) == expectedVersions)\n    assert(response.getLatestTableVersion == maxVersion)\n  }\n\n  override protected def initMetadata(): Metadata = {\n    // Ensure that the metadata that is passed to registerTable has the\n    // correct table conf set.\n    Metadata(configuration = Map(\n      COORDINATED_COMMITS_TABLE_CONF.key ->\n        JsonUtils.toJson(Map(UCCommitCoordinatorClient.UC_TABLE_ID_KEY -> tableUUID.toString)),\n      COORDINATED_COMMITS_COORDINATOR_NAME.key -> UCCommitCoordinatorBuilder.getName,\n      COORDINATED_COMMITS_COORDINATOR_CONF.key ->\n        JsonUtils.toJson(\n          Map(UCCommitCoordinatorClient.UC_METASTORE_ID_KEY -> metastoreId.toString))))\n  }\n\n  protected def waitForBackfill(\n      version: Long,\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient): Unit = {\n    eventually(timeout(10.seconds)) {\n      val logPath = tableCommitCoordinatorClient.logPath\n      val log = DeltaLog.forTable(spark, JCoordinatedCommitsUtils.getTablePath(logPath))\n      val fs = logPath.getFileSystem(log.newDeltaHadoopConf())\n      assert(fs.exists(FileNames.unsafeDeltaFile(logPath, version)))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/deletionvectors/DeletionVectorsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.deletionvectors\n\nimport java.io.{File, FileNotFoundException}\nimport java.net.URISyntaxException\n\nimport org.apache.spark.sql.delta.{DeletionVectorsTableFeature, DeletionVectorsTestUtils, DeltaChecksumException, DeltaConfigs, DeltaLog, DeltaMetricsUtils, DeltaTestUtilsForTempViews}\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor, RemoveFile}\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor.EMPTY\nimport org.apache.spark.sql.delta.deletionvectors.DeletionVectorsSuite._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaExceptionTestUtils, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport io.delta.tables.DeltaTable\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.format.converter.ParquetMetadataConverter\nimport org.apache.parquet.hadoop.ParquetFileReader\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.plans.logical.{AppendData, Subquery}\nimport org.apache.spark.sql.execution.FileSourceScanExec\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeletionVectorsSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeletionVectorsTestUtils\n  with DeltaTestUtilsForTempViews\n  with DeltaExceptionTestUtils {\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key, \"false\")\n  }\n\n  protected def hadoopConf(): Configuration = {\n    // scalastyle:off hadoopconfiguration\n    // This is to generate a Parquet file with two row groups\n    spark.sparkContext.hadoopConfiguration\n    // scalastyle:on hadoopconfiguration\n  }\n\n  test(s\"read Delta table with deletion vectors\") {\n    def verifyVersion(version: Int, expectedData: Seq[Int]): Unit = {\n      checkAnswer(\n        spark.read.format(\"delta\").option(\"versionAsOf\", version.toString).load(table1Path),\n        expectedData.toDF())\n    }\n    // Verify all versions of the table\n    verifyVersion(0, expectedTable1DataV0)\n    verifyVersion(1, expectedTable1DataV1)\n    verifyVersion(2, expectedTable1DataV2)\n    verifyVersion(3, expectedTable1DataV3)\n    verifyVersion(4, expectedTable1DataV4)\n  }\n\n  test(s\"read partitioned Delta table with deletion vectors\") {\n    def verify(version: Int, expectedData: Seq[Int], filterExp: String = \"true\"): Unit = {\n      val query = spark.read.format(\"delta\")\n          .option(\"versionAsOf\", version.toString)\n          .load(table3Path)\n          .filter(filterExp)\n      val expected = expectedData.toDF(\"id\")\n          .withColumn(\"partCol\", col(\"id\") % 10)\n          .filter(filterExp)\n\n      checkAnswer(query, expected)\n    }\n    // Verify all versions of the table\n    verify(0, expectedTable3DataV0)\n    verify(1, expectedTable3DataV1)\n    verify(2, expectedTable3DataV2)\n    verify(3, expectedTable3DataV3)\n    verify(4, expectedTable3DataV4)\n\n    verify(4, expectedTable3DataV4, filterExp = \"partCol = 3\")\n    verify(3, expectedTable3DataV3, filterExp = \"partCol = 3 and id > 25\")\n    verify(1, expectedTable3DataV1, filterExp = \"id > 25\")\n  }\n\n  test(\"select metadata columns from a Delta table with deletion vectors\") {\n    assert(spark.read.format(\"delta\").load(table1Path)\n      .select(\"_metadata.file_path\").distinct().count() == 22)\n  }\n\n  test(\"throw error when non-pinned TahoeFileIndex snapshot is used\") {\n    // Corner case where we still have non-pinned TahoeFileIndex when data skipping is disabled\n    withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> \"false\") {\n      def assertError(dataFrame: DataFrame): Unit = {\n        val ex = intercept[IllegalArgumentException] {\n          dataFrame.collect()\n        }\n        assert(ex.getMessage contains\n          \"Cannot work with a non-pinned table snapshot of the TahoeFileIndex\")\n      }\n      assertError(spark.read.format(\"delta\").load(table1Path))\n      assertError(spark.read.format(\"delta\").option(\"versionAsOf\", \"2\").load(table1Path))\n    }\n  }\n\n  test(\"read Delta table with deletion vectors with a filter\") {\n    checkAnswer(\n      spark.read.format(\"delta\").load(table1Path).where(\"value in (300, 787, 239)\"),\n      // 300 is removed in the final table\n      Seq(787, 239).toDF())\n  }\n\n  test(\"read Delta table with DV for a select files\") {\n    val deltaLog = DeltaLog.forTable(spark, table1Path)\n    val snapshot = deltaLog.unsafeVolatileSnapshot\n\n    // Select a subset of files with DVs and specific value range, this is just to test\n    // that reading these files will respect the DVs\n    var rowCount = 0L\n    var deletedRowCount = 0L\n    val selectFiles = snapshot.allFiles.collect().filter(\n      addFile => {\n        val stats = JsonUtils.mapper.readTree(addFile.stats).asInstanceOf[ObjectNode]\n        // rowCount += stats.get(\"rowCount\")\n        val min = stats.get(\"minValues\").get(\"value\").toString\n        val max = stats.get(\"maxValues\").get(\"value\").toString\n        val selected = (min == \"18\" && max == \"1988\") ||\n            (min == \"33\" && max == \"1995\") || (min == \"13\" && max == \"1897\")\n        // TODO: these steps will be easier and also change (depending upon tightBounds value) once\n        // we expose more methods on AddFile as part of the data skipping changes with DVs\n        if (selected) {\n          rowCount += stats.get(\"numRecords\").asInt(0)\n          deletedRowCount += Option(addFile.deletionVector).getOrElse(EMPTY).cardinality\n        }\n        selected\n      }\n      ).toSeq\n    assert(selectFiles.filter(_.deletionVector != null).size > 1) // make at least one file has DV\n\n    assert(deltaLog.createDataFrame(snapshot, selectFiles).count() == rowCount - deletedRowCount)\n  }\n\n  for (optimizeMetadataQuery <- BOOLEAN_DOMAIN)\n    test(\"read Delta tables with DVs in subqueries: \" +\n      s\"metadataQueryOptimizationEnabled=$optimizeMetadataQuery\") {\n      withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key ->\n        optimizeMetadataQuery.toString) {\n        val table1 = s\"delta.`${new File(table1Path).getAbsolutePath}`\"\n        val table2 = s\"delta.`${new File(table2Path).getAbsolutePath}`\"\n\n        def assertQueryResult(query: String, expected1: Int, expected2: Int): Unit = {\n          val df = spark.sql(query)\n          assertPlanContains(df, Subquery.getClass.getSimpleName.stripSuffix(\"$\"))\n          val actual = df.collect()(0) // fetch only row in the result\n          assert(actual === Row(expected1, expected2))\n        }\n\n        // same table used twice in the query\n        val query1 = s\"SELECT (SELECT COUNT(*) FROM $table1), (SELECT COUNT(*) FROM $table1)\"\n        assertQueryResult(query1, expectedTable1DataV4.size, expectedTable1DataV4.size)\n\n        // two tables used in the query\n        val query2 = s\"SELECT (SELECT COUNT(*) FROM $table1), (SELECT COUNT(*) FROM $table2)\"\n        assertQueryResult(query2, expectedTable1DataV4.size, expectedTable2DataV1.size)\n      }\n    }\n\n  test(\"insert into Delta table with DVs\") {\n    withTempDir { tempDir =>\n      val source1 = new File(table1Path)\n      val source2 = new File(table2Path)\n      val target = new File(tempDir, \"insertTest\")\n\n      // Copy the source2 DV table to a temporary directory\n      FileUtils.copyDirectory(source1, target)\n\n      // Insert data from source2 into source1 (copied to target)\n      // This blind append generates a plan with `V2WriteCommand` which is a corner\n      // case in `PrepareDeltaScan` rule\n      val insertDf = spark.sql(s\"INSERT INTO TABLE delta.`${target.getAbsolutePath}` \" +\n        s\"SELECT * FROM delta.`${source2.getAbsolutePath}`\")\n      // [[AppendData]] is one of the [[V2WriteCommand]] subtypes\n      assertPlanContains(insertDf, AppendData.getClass.getSimpleName.stripSuffix(\"$\"))\n\n      val dataInTarget = spark.sql(s\"SELECT * FROM delta.`${target.getAbsolutePath}`\")\n\n      // Make sure the number of rows is correct.\n      for (metadataQueryOptimization <- BOOLEAN_DOMAIN) {\n        withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key ->\n          metadataQueryOptimization.toString) {\n          assert(dataInTarget.count() == expectedTable2DataV1.size + expectedTable1DataV4.size)\n        }\n      }\n\n      // Make sure the contents are the same\n      checkAnswer(\n        dataInTarget,\n        spark.sql(\n          s\"SELECT * FROM delta.`${source1.getAbsolutePath}` UNION ALL \" +\n          s\"SELECT * FROM delta.`${source2.getAbsolutePath}`\")\n      )\n    }\n  }\n\n  test(\"DELETE with DVs - on a table with no prior DVs\") {\n    withDeletionVectorsEnabled() {\n      withTempDir { dirName =>\n        // Create table with 500 files of 2 rows each.\n        val numFiles = 500\n        val path = dirName.getAbsolutePath\n        spark.range(0, 1000, step = 1, numPartitions = numFiles).write.format(\"delta\").save(path)\n        val tableName = s\"delta.`$path`\"\n\n        val log = DeltaLog.forTable(spark, path)\n        val beforeDeleteFilesWithStats = log.update().allFiles.collect()\n        val beforeDeleteFiles = beforeDeleteFilesWithStats.map(_.path)\n\n        val numFilesWithDVs = 100\n        val numDeletedRows = numFilesWithDVs * 1\n        spark.sql(s\"DELETE FROM $tableName WHERE id % 2 = 0 AND id < 200\")\n\n        val snapshotAfterDelete = log.update()\n        val afterDeleteFilesWithStats = snapshotAfterDelete.allFiles.collect()\n        val afterDeleteFilesWithDVs = afterDeleteFilesWithStats.filter(_.deletionVector != null)\n        val afterDeleteFiles = afterDeleteFilesWithStats.map(_.path)\n\n        // Verify the expected no. of deletion vectors and deleted rows according to DV cardinality\n        assert(afterDeleteFiles.length === numFiles)\n        assert(afterDeleteFilesWithDVs.length === numFilesWithDVs)\n        assert(afterDeleteFilesWithDVs.map(_.deletionVector.cardinality).sum == numDeletedRows)\n\n        // Expect all DVs are written in one file\n          assert(\n          afterDeleteFilesWithDVs\n            .map(_.deletionVector.absolutePath(new Path(path)))\n            .toSet\n            .size === 1)\n\n        // Verify \"tightBounds\" is false for files that have DVs\n        for (f <- afterDeleteFilesWithDVs) {\n          assert(f.tightBounds.get === false)\n        }\n\n        // Verify all stats are the same except \"tightBounds\".\n        // Drop \"tightBounds\" and convert the rest to JSON.\n        val dropTightBounds: (AddFile => String) =\n          _.stats.replaceAll(\"\\\"tightBounds\\\":(false|true)\", \"\")\n        val beforeStats = beforeDeleteFilesWithStats.map(dropTightBounds).sorted\n        val afterStats = afterDeleteFilesWithStats.map(dropTightBounds).sorted\n        assert(beforeStats === afterStats)\n\n        // make sure the data file list is the same\n        assert(beforeDeleteFiles === afterDeleteFiles)\n\n        // Contents after the DELETE are as expected\n        checkAnswer(\n          spark.sql(s\"SELECT * FROM $tableName\"),\n          Seq.range(0, 1000).filterNot(Seq.range(start = 0, end = 200, step = 2).contains(_)).toDF()\n        )\n      }\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { mode =>\n    test(s\"DELETE with DVs with column mapping mode=$mode\") {\n      withSQLConf(\"spark.databricks.delta.properties.defaults.columnMapping.mode\" -> mode) {\n        withTempDir { dirName =>\n          val path = dirName.getAbsolutePath\n          val data = (0 until 50).map(x => (x % 10, x, s\"foo${x % 5}\"))\n          data.toDF(\"part\", \"col1\", \"col2\").write.format(\"delta\").partitionBy(\n            \"part\").save(path)\n          val tableLog = DeltaLog.forTable(spark, path)\n          enableDeletionVectorsInTable(tableLog, true)\n          spark.sql(s\"DELETE FROM delta.`$path` WHERE col1 = 2\")\n          checkAnswer(spark.sql(s\"select * from delta.`$path` WHERE col1 = 2\"), Seq())\n          verifyDVsExist(tableLog, 1)\n        }\n      }\n    }\n\n    test(s\"variant types DELETE with DVs with column mapping mode=$mode\") {\n      withSQLConf(\"spark.databricks.delta.properties.defaults.columnMapping.mode\" -> mode) {\n        withTempDir { dirName =>\n          val path = dirName.getAbsolutePath\n          val df = spark.range(0, 50).selectExpr(\n            \"id % 10 as part\",\n            \"id\",\n            \"parse_json(cast(id as string)) as v\"\n          )\n          df.write.format(\"delta\").partitionBy(\"part\").save(path)\n          val tableLog = DeltaLog.forTable(spark, path)\n          enableDeletionVectorsInTable(tableLog, true)\n          spark.sql(s\"DELETE FROM delta.`$path` WHERE v::int = 2\")\n          checkAnswer(spark.sql(s\"select * from delta.`$path` WHERE v::int = 2\"), Seq())\n          verifyDVsExist(tableLog, 1)\n        }\n      }\n    }\n  }\n\n  test(\"DELETE with DVs - existing table already has DVs\") {\n    withSQLConf(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> \"true\") {\n      withTempDir { tempDir =>\n        val source = new File(table1Path)\n        val target = new File(tempDir, \"deleteTest\")\n\n        // Copy the source DV table to a temporary directory\n        FileUtils.copyDirectory(source, target)\n\n        val targetPath = s\"delta.`${target.getAbsolutePath}`\"\n        val dataToRemove = Seq(1999, 299, 7, 87, 867, 456)\n        val existingDVs = getFilesWithDeletionVectors(DeltaLog.forTable(spark, target))\n\n        spark.sql(s\"DELETE FROM $targetPath WHERE value in (${dataToRemove.mkString(\",\")})\")\n\n        // Check new DVs are created\n        val newDVs = getFilesWithDeletionVectors(DeltaLog.forTable(spark, target))\n        // expect the new DVs contain extra entries for the deleted rows.\n        assert(\n          existingDVs.map(_.deletionVector.cardinality).sum + dataToRemove.size ===\n          newDVs.map(_.deletionVector.cardinality).sum\n        )\n        for (f <- newDVs) {\n          assert(f.tightBounds.get === false)\n        }\n\n        // Check the data is valid\n        val expectedTable1DataV5 = expectedTable1DataV4.filterNot(e => dataToRemove.contains(e))\n        checkAnswer(spark.sql(s\"SELECT * FROM $targetPath\"), expectedTable1DataV5.toDF())\n      }\n    }\n  }\n\n  test(\"Metrics when deleting with DV\") {\n    withDeletionVectorsEnabled() {\n      val tableName = \"tbl\"\n      withTable(tableName) {\n        spark.range(0, 10, 1, numPartitions = 2)\n          .write.format(\"delta\").saveAsTable(tableName)\n\n        {\n          // Delete one row from the first file, and the whole second file.\n          val result = sql(s\"DELETE FROM $tableName WHERE id >= 4\")\n          assert(result.collect() === Array(Row(6)))\n          val opMetrics = DeltaMetricsUtils.getLastOperationMetrics(tableName)\n          assert(opMetrics.getOrElse(\"numDeletedRows\", -1) === 6)\n          assert(opMetrics.getOrElse(\"numRemovedFiles\", -1) === 1)\n          assert(opMetrics.getOrElse(\"numDeletionVectorsAdded\", -1) === 1)\n          assert(opMetrics.getOrElse(\"numDeletionVectorsRemoved\", -1) === 0)\n          assert(opMetrics.getOrElse(\"numDeletionVectorsUpdated\", -1) === 0)\n        }\n\n        {\n          // Delete one row again.\n          sql(s\"DELETE FROM $tableName WHERE id = 3\")\n          val opMetrics = DeltaMetricsUtils.getLastOperationMetrics(tableName)\n          assert(opMetrics.getOrElse(\"numDeletedRows\", -1) === 1)\n          assert(opMetrics.getOrElse(\"numRemovedFiles\", -1) === 0)\n          val initialNumDVs = 0\n          val numDVUpdated = 1\n          // An \"updated\" DV is \"deleted\" then \"added\" again.\n          // We increment the count for \"updated\", \"added\", and \"deleted\".\n          assert(\n            opMetrics.getOrElse(\"numDeletionVectorsAdded\", -1) ===\n              initialNumDVs + numDVUpdated)\n          assert(\n            opMetrics.getOrElse(\"numDeletionVectorsRemoved\", -1) ===\n              initialNumDVs + numDVUpdated)\n          assert(\n            opMetrics.getOrElse(\"numDeletionVectorsUpdated\", -1) ===\n              numDVUpdated)\n        }\n\n        {\n          // Delete all renaming rows.\n          sql(s\"DELETE FROM $tableName WHERE id IN (0, 1, 2)\")\n          val opMetrics = DeltaMetricsUtils.getLastOperationMetrics(tableName)\n          assert(opMetrics.getOrElse(\"numDeletedRows\", -1) === 3)\n          assert(opMetrics.getOrElse(\"numRemovedFiles\", -1) === 1)\n          assert(opMetrics.getOrElse(\"numDeletionVectorsAdded\", -1) === 0)\n          assert(opMetrics.getOrElse(\"numDeletionVectorsRemoved\", -1) === 1)\n          assert(opMetrics.getOrElse(\"numDeletionVectorsUpdated\", -1) === 0)\n        }\n      }\n    }\n  }\n\n  for(targetDVFileSize <- Seq(2, 200, 2000000)) {\n    test(s\"DELETE with DVs - packing multiple DVs into one file: target max DV file \" +\n      s\"size=$targetDVFileSize\") {\n      withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\",\n        DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> \"true\",\n        DeltaSQLConf.DELETION_VECTOR_PACKING_TARGET_SIZE.key -> targetDVFileSize.toString) {\n        withTempDir { dirName =>\n          // Create table with 100 files of 2 rows each.\n          val numFiles = 100\n          val path = dirName.getAbsolutePath\n          spark.range(0, 200, step = 1, numPartitions = numFiles)\n            .write.format(\"delta\").save(path)\n          val tableName = s\"delta.`$path`\"\n\n          val beforeDeleteFiles = DeltaLog.forTable(spark, path)\n            .unsafeVolatileSnapshot.allFiles.collect().map(_.path)\n\n          val numFilesWithDVs = 10\n          val numDeletedRows = numFilesWithDVs * 1\n          spark.sql(s\"DELETE FROM $tableName WHERE id % 2 = 0 AND id < 20\")\n\n          // Verify the expected number of AddFiles with DVs\n          val allFiles = DeltaLog.forTable(spark, path).unsafeVolatileSnapshot.allFiles.collect()\n          assert(allFiles.size === numFiles)\n          val addFilesWithDV = allFiles.filter(_.deletionVector != null)\n          assert(addFilesWithDV.size === numFilesWithDVs)\n          assert(addFilesWithDV.map(_.deletionVector.cardinality).sum == numDeletedRows)\n\n          val expectedDVFileCount = targetDVFileSize match {\n            // Each AddFile will have its own DV file\n            case 2 => numFilesWithDVs\n            // Each DV size is about 34bytes according the latest format, plus 4 bytes for\n            // checksum and another 4 bytes for data length.\n            case 200 => (numFilesWithDVs.toDouble / (200 / (34 + 4 + 4)).toDouble).ceil.toInt\n            // Expect all DVs in one file\n            case 2000000 => 1\n            case default =>\n              throw new IllegalStateException(s\"Unknown target DV file size: $default\")\n          }\n          // Expect all DVs are written in one file\n          assert(\n            addFilesWithDV.map(_.deletionVector.absolutePath(new Path(path))).toSet.size ===\n            expectedDVFileCount)\n\n          val afterDeleteFiles = allFiles.map(_.path)\n          // make sure the data file list is the same\n          assert(beforeDeleteFiles === afterDeleteFiles)\n\n          // Contents after the DELETE are as expected\n          checkAnswer(\n            spark.sql(s\"SELECT * FROM $tableName\"),\n            Seq.range(0, 200).filterNot(\n              Seq.range(start = 0, end = 20, step = 2).contains(_)).toDF())\n        }\n      }\n    }\n  }\n\n  test(\"JOIN with DVs - self-join a table with DVs\") {\n    val tableDf = spark.read.format(\"delta\").load(table2Path)\n    val leftDf = tableDf.withColumn(\"key\", col(\"value\") % 2)\n    val rightDf = tableDf.withColumn(\"key\", col(\"value\") % 2 + 1)\n\n    checkAnswer(\n      leftDf.as(\"left\").join(rightDf.as(\"right\"), \"key\").drop(\"key\"),\n      Seq(1, 3, 5, 7).flatMap(l => Seq(2, 4, 6, 8).map(r => (l, r))).toDF()\n    )\n  }\n\n  test(\"JOIN with DVs - non-DV table joins DV table\") {\n    val tableDf = spark.read.format(\"delta\").load(table2Path)\n    val tableDfV0 = spark.read.format(\"delta\").option(\"versionAsOf\", \"0\").load(table2Path)\n    val leftDf = tableDf.withColumn(\"key\", col(\"value\") % 2)\n    val rightDf = tableDfV0.withColumn(\"key\", col(\"value\") % 2 + 1)\n\n    // Right has two more rows 0 and 9. 0 will be left in the join result.\n    checkAnswer(\n      leftDf.as(\"left\").join(rightDf.as(\"right\"), \"key\").drop(\"key\"),\n      Seq(1, 3, 5, 7).flatMap(l => Seq(0, 2, 4, 6, 8).map(r => (l, r))).toDF()\n    )\n  }\n\n  test(\"MERGE with DVs - merge into DV table\") {\n    withTempDir { tempDir =>\n      val source = new File(table1Path)\n      val target = new File(tempDir, \"mergeTest\")\n      FileUtils.copyDirectory(new File(table2Path), target)\n\n      DeltaTable.forPath(spark, target.getAbsolutePath).as(\"target\")\n        .merge(\n          spark.read.format(\"delta\").load(source.getAbsolutePath).as(\"source\"),\n          \"source.value = target.value\")\n        .whenMatched()\n        .updateExpr(Map(\"value\" -> \"source.value + 10000\"))\n        .whenNotMatched()\n        .insertExpr(Map(\"value\" -> \"source.value\"))\n        .execute()\n\n      val snapshot = DeltaLog.forTable(spark, target).update()\n      val allFiles = snapshot.allFiles.collect()\n      val tombstones = snapshot.tombstones.collect()\n      // DVs are removed\n      for (ts <- tombstones) {\n        assert(ts.deletionVector != null)\n      }\n      // target log should not contain DVs\n      for (f <- allFiles) {\n        assert(f.deletionVector == null)\n        assert(f.tightBounds.get)\n      }\n\n      // Target table should contain \"table2 records + 10000\" and \"table1 records \\ table2 records\".\n      checkAnswer(\n        spark.read.format(\"delta\").load(target.getAbsolutePath),\n        (expectedTable2DataV1.map(_ + 10000) ++\n          expectedTable1DataV4.filterNot(expectedTable2DataV1.contains)).toDF()\n      )\n    }\n  }\n\n  test(\"UPDATE with DVs - update rewrite files with DVs\") {\n    withTempDir { tempDir =>\n      FileUtils.copyDirectory(new File(table2Path), tempDir)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n      DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n        .update(col(\"value\") === 1, Map(\"value\" -> (col(\"value\") + 1)))\n\n      val snapshot = deltaLog.update()\n      val allFiles = snapshot.allFiles.collect()\n      val tombstones = snapshot.tombstones.collect()\n      // DVs are removed\n      for (ts <- tombstones) {\n        assert(ts.deletionVector != null)\n      }\n      // target log should contain two files, one with and one without DV\n      assert(allFiles.count(_.deletionVector != null) === 1)\n      assert(allFiles.count(_.deletionVector == null) === 1)\n    }\n  }\n\n  test(\"UPDATE with DVs - update deleted rows updates nothing\") {\n    withTempDir { tempDir =>\n      FileUtils.copyDirectory(new File(table2Path), tempDir)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n      val snapshotBeforeUpdate = deltaLog.update()\n      val allFilesBeforeUpdate = snapshotBeforeUpdate.allFiles.collect()\n\n      DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n        .update(col(\"value\")  === 0, Map(\"value\" -> (col(\"value\") + 1)))\n\n      val snapshot = deltaLog.update()\n      val allFiles = snapshot.allFiles.collect()\n      val tombstones = snapshot.tombstones.collect()\n      // nothing changed\n      assert(tombstones.length === 0)\n      assert(allFiles === allFilesBeforeUpdate)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir.getAbsolutePath),\n        expectedTable2DataV1.toDF()\n      )\n    }\n  }\n\n  test(\"INSERT + DELETE + MERGE + UPDATE with DVs\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getAbsolutePath\n      val deltaLog = DeltaLog.forTable(spark, path)\n\n      def checkTableContents(rows: DataFrame): Unit =\n        checkAnswer(sql(s\"SELECT * FROM delta.`$path`\"), rows)\n\n      // Version 0: DV is enabled on table\n      {\n        withSQLConf(\n          DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\") {\n          spark.range(0, 10, 1, numPartitions = 2).write.format(\"delta\").save(path)\n        }\n        val snapshot = deltaLog.update()\n        assert(snapshot.protocol.isFeatureSupported(DeletionVectorsTableFeature))\n        for (f <- snapshot.allFiles.collect()) {\n          assert(f.tightBounds.get)\n        }\n      }\n      // Version 1: DELETE one row from each file\n      {\n        withSQLConf(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> \"true\") {\n          sql(s\"DELETE FROM delta.`$path` WHERE id IN (1, 8)\")\n        }\n        val (add, _) = getFileActionsInLastVersion(deltaLog)\n        for (a <- add) {\n          assert(a.deletionVector !== null)\n          assert(a.deletionVector.cardinality === 1)\n          assert(a.numPhysicalRecords.get === a.numLogicalRecords.get + 1)\n          assert(a.tightBounds.get === false)\n        }\n\n        checkTableContents(Seq(0, 2, 3, 4, 5, 6, 7, 9).toDF())\n      }\n      // Version 2: UPDATE one row in the first file\n      {\n        sql(s\"UPDATE delta.`$path` SET id = -1 WHERE id = 0\")\n        val (added, removed) = getFileActionsInLastVersion(deltaLog)\n        assert(added.length === 2)\n        assert(removed.length === 1)\n        // Added files must be two, one containing DV and one not\n        assert(added.count(_.deletionVector != null) === 1)\n        assert(added.count(_.deletionVector == null) === 1)\n        // Removed files must contain DV\n        for (r <- removed) {\n          assert(r.deletionVector !== null)\n        }\n\n        checkTableContents(Seq(-1, 2, 3, 4, 5, 6, 7, 9).toDF())\n      }\n      // Version 3: MERGE into the table using table2\n      {\n        DeltaTable.forPath(spark, path).as(\"target\")\n          .merge(\n            spark.read.format(\"delta\").load(table2Path).as(\"source\"),\n            \"source.value = target.id\")\n          .whenMatched()\n          .updateExpr(Map(\"id\" -> \"source.value\"))\n          .whenNotMatchedBySource().delete().execute()\n        val (added, removed) = getFileActionsInLastVersion(deltaLog)\n        assert(removed.length === 3)\n        for (a <- added) {\n          assert(a.deletionVector === null)\n          assert(a.tightBounds.get)\n        }\n        // Two of three removed files have DV\n        assert(removed.count(_.deletionVector != null) === 2)\n\n        // -1 and 9 are deleted by \"when not matched by source\"\n        checkTableContents(Seq(2, 3, 4, 5, 6, 7).toDF())\n      }\n      // Version 4: DELETE one row again\n      {\n        withSQLConf(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> \"true\") {\n          sql(s\"DELETE FROM delta.`$path` WHERE id IN (4)\")\n        }\n        val (add, _) = getFileActionsInLastVersion(deltaLog)\n        for (a <- add) {\n          assert(a.deletionVector !== null)\n          assert(a.deletionVector.cardinality === 1)\n          assert(a.numPhysicalRecords.get === a.numLogicalRecords.get + 1)\n          assert(a.tightBounds.get === false)\n        }\n\n        checkTableContents(Seq(2, 3, 5, 6, 7).toDF())\n      }\n    }\n  }\n\n  test(\"huge table: read from tables of 2B rows with existing DV of many zeros\") {\n    val canonicalTable5Path = new File(table5Path).getCanonicalPath\n\n    val predicatePushDownEnabled =\n      spark.conf.get(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX)\n\n    try {\n      checkCountAndSum(\"value\", table5Count, table5Sum, canonicalTable5Path)\n    } catch {\n      // TODO(SPARK-47731): Known issue. To be fixed in Spark 3.5 and/or Spark 4.0.\n      case e: SparkException if predicatePushDownEnabled &&\n        (e.getMessage.contains(\"More than Int.MaxValue elements\") ||\n          e.getCause.getMessage.contains(\"More than Int.MaxValue elements\")) => () // Ignore.\n    }\n  }\n\n  test(\"sanity check for non-incremental DV update\") {\n    val addFile = createTestAddFile()\n    def bitmapToDvDescriptor(bitmap: RoaringBitmapArray): DeletionVectorDescriptor = {\n      DeletionVectorDescriptor.inlineInLog(\n        bitmap.serializeAsByteArray(RoaringBitmapArrayFormat.Portable),\n        bitmap.cardinality)\n    }\n    val dv0 = bitmapToDvDescriptor(RoaringBitmapArray())\n    val dv1 = bitmapToDvDescriptor(RoaringBitmapArray(0L, 1L))\n    val dv2 = bitmapToDvDescriptor(RoaringBitmapArray(0L, 2L))\n    val dv3 = bitmapToDvDescriptor(RoaringBitmapArray(3L))\n\n    def removeRows(a: AddFile, dv: DeletionVectorDescriptor): (AddFile, RemoveFile) = {\n      a.removeRows(\n        deletionVector = dv,\n        updateStats = true\n      )\n    }\n\n    // Adding an empty DV to a file is allowed.\n    removeRows(addFile, dv0)\n    // Updating with the same DV is allowed.\n    val (addFileWithDV1, _) = removeRows(addFile, dv1)\n    removeRows(addFileWithDV1, dv1)\n    // Updating with a different DV with the same cardinality and different rows should not be\n    // allowed, but is expensive to detect it.\n    removeRows(addFileWithDV1, dv2)\n\n    // Updating with a DV with lower cardinality should throw.\n    for (dv <- Seq(dv0, dv3)) {\n      assertThrows[DeltaChecksumException] {\n        removeRows(addFileWithDV1, dv)\n      }\n    }\n  }\n\n  test(\"Check no resource leak when DV files are missing (table corrupted)\") {\n    withTempDir { tempDir =>\n      val source = new File(table2Path)\n      val target = new File(tempDir, \"resourceLeakTest\")\n      val targetPath = target.getAbsolutePath\n\n      // Copy the source DV table to a temporary directory\n      FileUtils.copyDirectory(source, target)\n\n      val filesWithDvs = getFilesWithDeletionVectors(DeltaLog.forTable(spark, target))\n      assert(filesWithDvs.size > 0)\n      deleteDVFile(targetPath, filesWithDvs(0))\n\n      val se = intercept[SparkException] {\n        spark.sql(s\"SELECT * FROM delta.`$targetPath`\").collect()\n      }\n      assert(findIfResponsible[FileNotFoundException](se).nonEmpty,\n        s\"Expected a file not found exception as the cause, but got: [${se}]\")\n    }\n  }\n\n  test(\"absolute DV path with encoded special characters\") {\n    // This test uses hand-crafted path with special characters.\n    // Do not test with a prefix that needs URL standard escaping.\n    withTempDir(prefix = \"spark\") { dir =>\n      writeTableHavingSpecialCharInDVPath(dir, pathIsEncoded = true)\n      checkAnswer(\n        spark.read.format(\"delta\").load(dir.getCanonicalPath),\n        Seq(1, 3, 5, 7, 9).toDF())\n    }\n  }\n\n  test(\"absolute DV path with not-encoded special characters\") {\n    // This test uses hand-crafted path with special characters.\n    // Do not test with a prefix that needs URL standard escaping.\n    withTempDir(prefix = \"spark\") { dir =>\n      writeTableHavingSpecialCharInDVPath(dir, pathIsEncoded = false)\n      val e = interceptWithUnwrapping[URISyntaxException] {\n        spark.read.format(\"delta\").load(dir.getCanonicalPath).collect()\n      }\n      assert(e.getMessage.contains(\"Malformed escape pair\"))\n    }\n  }\n\n  private sealed case class DeleteUsingDVWithResults(\n      scale: String,\n      sqlRule: String,\n      count: Long,\n      sum: Long)\n  private val deleteUsingDvSmallScale = DeleteUsingDVWithResults(\n    \"small\",\n    \"value = 1\",\n    table5CountByValues.filterKeys(_ != 1).values.sum,\n    table5SumByValues.filterKeys(_ != 1).values.sum)\n  private val deleteUsingDvMediumScale = DeleteUsingDVWithResults(\n    \"medium\",\n    \"value > 10\",\n    table5CountByValues.filterKeys(_ <= 10).values.sum,\n    table5SumByValues.filterKeys(_ <= 10).values.sum)\n  private val deleteUsingDvLargeScale = DeleteUsingDVWithResults(\n    \"large\",\n    \"value != 21\",\n    table5CountByValues(21),\n    table5SumByValues(21))\n\n  // deleteUsingDvMediumScale and deleteUsingDvLargeScale runs too slow thus disabled.\n  for (deleteSpec <- Seq(deleteUsingDvSmallScale)) {\n    test(\n      s\"huge table: delete a ${deleteSpec.scale} number of rows from tables of 2B rows with DVs\") {\n      val predicatePushDownEnabled =\n        spark.conf.get(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX)\n      withTempDir { dir =>\n        try {\n          FileUtils.copyDirectory(new File(table5Path), dir)\n          val log = DeltaLog.forTable(spark, dir)\n\n          withDeletionVectorsEnabled() {\n            sql(s\"DELETE FROM delta.`${dir.getCanonicalPath}` WHERE ${deleteSpec.sqlRule}\")\n          }\n          val (added, _) = getFileActionsInLastVersion(log)\n          assert(added.forall(_.deletionVector != null))\n\n          checkCountAndSum(\"value\", deleteSpec.count, deleteSpec.sum, dir.getCanonicalPath)\n        } catch {\n          // TODO(SPARK-47731): Known issue. To be fixed in Spark 3.5 and/or Spark 4.0.\n          case e: SparkException if predicatePushDownEnabled &&\n            (e.getMessage.contains(\"More than Int.MaxValue elements\") ||\n              e.getCause.getMessage.contains(\"More than Int.MaxValue elements\")) => () // Ignore.\n        }\n      }\n    }\n  }\n\n  private def checkCountAndSum(column: String, count: Long, sum: Long, tableDir: String): Unit = {\n    checkAnswer(\n      sql(s\"SELECT count($column), sum($column) FROM delta.`$tableDir`\"),\n      Seq((count, sum)).toDF())\n  }\n\n  private def assertPlanContains(queryDf: DataFrame, expected: String): Unit = {\n    val optimizedPlan = queryDf.queryExecution.analyzed.toString()\n    assert(optimizedPlan.contains(expected), s\"Plan is missing `$expected`: $optimizedPlan\")\n  }\n}\n\nobject DeletionVectorsSuite {\n  val table1Path = \"src/test/resources/delta/table-with-dv-large\"\n  // Table at version 0: contains [0, 2000)\n  val expectedTable1DataV0 = Seq.range(0, 2000)\n  // Table at version 1: removes rows with id = 0, 180, 300, 700, 1800\n  val v1Removed = Set(0, 180, 300, 700, 1800)\n  val expectedTable1DataV1 = expectedTable1DataV0.filterNot(e => v1Removed.contains(e))\n  // Table at version 2: inserts rows with id = 300, 700\n  val v2Added = Set(300, 700)\n  val expectedTable1DataV2 = expectedTable1DataV1 ++ v2Added\n  // Table at version 3: removes rows with id = 300, 250, 350, 900, 1353, 1567, 1800\n  val v3Removed = Set(300, 250, 350, 900, 1353, 1567, 1800)\n  val expectedTable1DataV3 = expectedTable1DataV2.filterNot(e => v3Removed.contains(e))\n  // Table at version 4: inserts rows with id = 900, 1567\n  val v4Added = Set(900, 1567)\n  val expectedTable1DataV4 = expectedTable1DataV3 ++ v4Added\n\n  val table2Path = \"src/test/resources/delta/table-with-dv-small\"\n  // Table at version 0: contains 0 - 9\n  val expectedTable2DataV0 = Seq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)\n  // Table at version 1: removes rows 0 and 9\n  val expectedTable2DataV1 = Seq(1, 2, 3, 4, 5, 6, 7, 8)\n\n  val table3Path = \"src/test/resources/delta/partitioned-table-with-dv-large\"\n  // Table at version 0: contains [0, 2000)\n  val expectedTable3DataV0 = Seq.range(0, 2000)\n  // Table at version 1: removes rows with id = (0, 180, 308, 225, 756, 1007, 1503)\n  val table3V1Removed = Set(0, 180, 308, 225, 756, 1007, 1503)\n  val expectedTable3DataV1 = expectedTable3DataV0.filterNot(e => table3V1Removed.contains(e))\n  // Table at version 2: inserts rows with id = 308, 756\n  val table3V2Added = Set(308, 756)\n  val expectedTable3DataV2 = expectedTable3DataV1 ++ table3V2Added\n  // Table at version 3: removes rows with id = (300, 257, 399, 786, 1353, 1567, 1800)\n  val table3V3Removed = Set(300, 257, 399, 786, 1353, 1567, 1800)\n  val expectedTable3DataV3 = expectedTable3DataV2.filterNot(e => table3V3Removed.contains(e))\n  // Table at version 4: inserts rows with id = 1353, 1567\n  val table3V4Added = Set(1353, 1567)\n  val expectedTable3DataV4 = expectedTable3DataV3 ++ table3V4Added\n\n  // Table with DV table feature as supported but no DVs\n  val table4Path = \"src/test/resources/delta/table-with-dv-feature-enabled\"\n  val expectedTable4DataV0 = Seq(1L)\n\n  // Table with DV, (1<<31)+10=2147483658 rows in total including 2147484 rows deleted. Parquet is\n  // generated by:\n  //   spark.range(0, (1L << 31) + 10, 1, numPartitions = 1)\n  //     .withColumn(\n  //       \"value\",\n  //       when($\"id\" % 1000 === 0, 1).otherwise(($\"id\" / 100000000).cast(IntegerType)))\n  // All \"id % 1000 = 0\" rows are marked as deleted.\n  // Column \"value\" ranges from 0 to 21.\n  // 99900000 rows with values 0 to 20 each, and 47436174 rows with value 21.\n  val table5Path = \"src/test/resources/delta/table-with-dv-gigantic\"\n  val table5Count = 2145336174L\n  val table5Sum = 21975159654L\n  val table5CountByValues = (0 to 20).map(_ -> 99900000L).toMap + (21 -> 47436174L)\n  val table5SumByValues = (0 to 20).map(v => v -> v * 99900000L).toMap + (21 -> 21 * 47436174L)\n\n  // Generate a table with special characters in DV path.\n  // Content of this table is range(0, 10) with all even numbers deleted.\n  def writeTableHavingSpecialCharInDVPath(path: File, pathIsEncoded: Boolean): Unit = {\n    val tableHavingSpecialCharInDVTemplate = \"src/test/resources/delta/table-with-dv-special-char\"\n    FileUtils.copyDirectory(new File(tableHavingSpecialCharInDVTemplate), path)\n    val fullPath = new File(\n      path,\n      if (pathIsEncoded) \"folder&with%25special%20char\" else \"folder&with%special char\")\n      .getCanonicalPath\n    val logJson = new File(path, \"_delta_log/00000000000000000000.json\")\n    val logJsonContent = FileUtils.readFileToString(logJson, \"UTF-8\")\n    val newLogJsonContent = logJsonContent.replace(\n      \"{{FOLDER_WITH_SPECIAL_CHAR}}\", fullPath)\n    FileUtils.delete(logJson)\n    FileUtils.write(logJson, newLogJsonContent, \"UTF-8\")\n  }\n}\n\nclass DeletionVectorsWithPredicatePushdownSuite extends DeletionVectorsSuite {\n  // ~4MBs. Should contain 2 row groups.\n  val multiRowgroupTable = \"multiRowgroupTable\"\n  val multiRowgroupTableRowsNum = 1000000\n\n  def assertParquetHasMultipleRowGroups(filePath: Path): Unit = {\n    val parquetMetadata = ParquetFileReader.readFooter(\n      hadoopConf,\n      filePath,\n      ParquetMetadataConverter.NO_FILTER)\n    assert(parquetMetadata.getBlocks.size() > 1)\n  }\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // 2MB rowgroups.\n    hadoopConf().set(\"parquet.block.size\", (2 * 1024 * 1024).toString)\n\n    spark.range(0, multiRowgroupTableRowsNum, 1, 1).toDF(\"id\")\n      .write\n      .option(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, true.toString)\n      .format(\"delta\")\n      .saveAsTable(multiRowgroupTable)\n\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(multiRowgroupTable))\n    val files = deltaLog.update().allFiles.collect()\n\n    assert(files.length === 1)\n    assertParquetHasMultipleRowGroups(files.head.absolutePath(deltaLog))\n\n    spark.conf.set(DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key, \"true\")\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    sql(s\"DROP TABLE IF EXISTS $multiRowgroupTable\")\n  }\n\n  private def testPredicatePushDown(\n      deletePredicates: Seq[String],\n      selectPredicate: Option[String],\n      expectedNumRows: Long,\n      validationPredicate: String,\n      vectorizedReaderEnabled: Boolean,\n      readColumnarBatchAsRows: Boolean): Unit = {\n    withTempDir { dir =>\n      // This forces the code generator to not use codegen. As a result, Spark sets options to get\n      // rows instead of columnar batches from the Parquet reader. This allows to test the relevant\n      // code path in DeltaParquetFileFormat.\n      val codeGenMaxFields = if (readColumnarBatchAsRows) \"0\" else \"100\"\n      withSQLConf(\n          SQLConf.WHOLESTAGE_MAX_NUM_FIELDS.key -> codeGenMaxFields,\n          SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> vectorizedReaderEnabled.toString,\n          SQLConf.FILES_MAX_PARTITION_BYTES.key -> \"2MB\") {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` SHALLOW CLONE $multiRowgroupTable\")\n\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.getCanonicalPath)\n        val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)\n        val files = deltaLog.update().allFiles.collect()\n        assert(files.length === 1)\n\n        // Execute multiple delete statements. These require to reconsile the metadata column\n        // between DV writing and scanning operations.\n        deletePredicates.foreach(targetTable.delete)\n\n        val targetTableDF = selectPredicate.map(targetTable.toDF.filter).getOrElse(targetTable.toDF)\n        assertPredicatesArePushedDown(targetTableDF)\n        // Make sure there are multiple row groups.\n        assertParquetHasMultipleRowGroups(files.head.toPath)\n        // Make sure we have 2 splits.\n        assert(targetTableDF.rdd.partitions.size === 2)\n\n        assert(targetTableDF.count() === expectedNumRows)\n        // The deleted/filtered rows should not exist.\n        assert(targetTableDF.filter(validationPredicate).count() === 0)\n      }\n    }\n  }\n\n  for {\n    vectorizedReaderEnabled <- BOOLEAN_DOMAIN\n    readColumnarBatchAsRows <- if (vectorizedReaderEnabled) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"PredicatePushdown: Single deletion at the first row group. \" +\n    s\"vectorizedReaderEnabled: $vectorizedReaderEnabled \" +\n    s\"readColumnarBatchAsRows: $readColumnarBatchAsRows\") {\n    testPredicatePushDown(\n      deletePredicates = Seq(\"id == 100\"),\n      selectPredicate = None,\n      expectedNumRows = multiRowgroupTableRowsNum - 1,\n      validationPredicate = \"id == 100\",\n      vectorizedReaderEnabled = vectorizedReaderEnabled,\n      readColumnarBatchAsRows = readColumnarBatchAsRows)\n  }\n\n  for {\n    vectorizedReaderEnabled <- BOOLEAN_DOMAIN\n    readColumnarBatchAsRows <- if (vectorizedReaderEnabled) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"PredicatePushdown: Single deletion at the second row group. \" +\n    s\"vectorizedReaderEnabled: $vectorizedReaderEnabled \" +\n    s\"readColumnarBatchAsRows: $readColumnarBatchAsRows\") {\n    testPredicatePushDown(\n      deletePredicates = Seq(\"id == 900000\"),\n      selectPredicate = None,\n      expectedNumRows = multiRowgroupTableRowsNum - 1,\n      // (rowId, Expected value).\n      validationPredicate = \"id == 900000\",\n      vectorizedReaderEnabled = vectorizedReaderEnabled,\n      readColumnarBatchAsRows = readColumnarBatchAsRows)\n  }\n\n  for {\n    vectorizedReaderEnabled <- BOOLEAN_DOMAIN\n    readColumnarBatchAsRows <- if (vectorizedReaderEnabled) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"PredicatePushdown: Single delete statement with multiple ids. \" +\n    s\"vectorizedReaderEnabled: $vectorizedReaderEnabled \" +\n    s\"readColumnarBatchAsRows: $readColumnarBatchAsRows\") {\n    testPredicatePushDown(\n      deletePredicates = Seq(\"id in (20, 200, 2000, 900000)\"),\n      selectPredicate = None,\n      expectedNumRows = multiRowgroupTableRowsNum - 4,\n      validationPredicate = \"id in (20, 200, 2000, 900000)\",\n      vectorizedReaderEnabled = vectorizedReaderEnabled,\n      readColumnarBatchAsRows = readColumnarBatchAsRows)\n  }\n\n  for {\n    vectorizedReaderEnabled <- BOOLEAN_DOMAIN\n    readColumnarBatchAsRows <- if (vectorizedReaderEnabled) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"PredicatePushdown: Multiple delete statements. \" +\n    s\"vectorizedReaderEnabled: $vectorizedReaderEnabled \" +\n    s\"readColumnarBatchAsRows: $readColumnarBatchAsRows\") {\n    testPredicatePushDown(\n      deletePredicates = Seq(\"id = 20\", \"id = 200\", \"id = 2000\", \"id = 900000\"),\n      selectPredicate = None,\n      expectedNumRows = multiRowgroupTableRowsNum - 4,\n      validationPredicate = \"id in (20, 200, 2000, 900000)\",\n      vectorizedReaderEnabled = vectorizedReaderEnabled,\n      readColumnarBatchAsRows = readColumnarBatchAsRows)\n  }\n\n  for {\n    vectorizedReaderEnabled <- BOOLEAN_DOMAIN\n    readColumnarBatchAsRows <- if (vectorizedReaderEnabled) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"PredicatePushdown: Scan with predicates. \" +\n    s\"vectorizedReaderEnabled: $vectorizedReaderEnabled \" +\n    s\"readColumnarBatchAsRows: $readColumnarBatchAsRows\") {\n    testPredicatePushDown(\n      deletePredicates = Seq(\"id = 20\", \"id = 2000\"),\n      selectPredicate = Some(\"id not in (200, 900000)\"),\n      expectedNumRows = multiRowgroupTableRowsNum - 4,\n      validationPredicate = \"id in (20, 200, 2000, 900000)\",\n      vectorizedReaderEnabled = vectorizedReaderEnabled,\n      readColumnarBatchAsRows = readColumnarBatchAsRows)\n  }\n\n  for {\n    vectorizedReaderEnabled <- BOOLEAN_DOMAIN\n    readColumnarBatchAsRows <- if (vectorizedReaderEnabled) BOOLEAN_DOMAIN else Seq(false)\n  } test(\"PredicatePushdown: Scan with predicates - no deletes. \" +\n    s\"vectorizedReaderEnabled: $vectorizedReaderEnabled \" +\n    s\"readColumnarBatchAsRows: $readColumnarBatchAsRows\") {\n    testPredicatePushDown(\n      deletePredicates = Seq.empty,\n      selectPredicate = Some(\"id not in (20, 200, 2000, 900000)\"),\n      expectedNumRows = multiRowgroupTableRowsNum - 4,\n      validationPredicate = \"id in (20, 200, 2000, 900000)\",\n      vectorizedReaderEnabled = vectorizedReaderEnabled,\n      readColumnarBatchAsRows = readColumnarBatchAsRows)\n  }\n\n  test(\"Predicate pushdown works on queries that select metadata fields\") {\n    withTempDir { dir =>\n      withSQLConf(SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> true.toString) {\n        sql(s\"CREATE TABLE delta.`${dir.getCanonicalPath}` SHALLOW CLONE $multiRowgroupTable\")\n\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.getCanonicalPath)\n        targetTable.delete(\"id == 900000\")\n\n        val r1 = targetTable.toDF.select(\"id\", \"_metadata.row_index\").count()\n        assert(r1 === multiRowgroupTableRowsNum - 1)\n\n        val r2 = targetTable.toDF.select(\"id\", \"_metadata.row_index\", \"_metadata.file_path\").count()\n        assert(r2 === multiRowgroupTableRowsNum - 1)\n\n        val r3 = targetTable\n          .toDF\n          .select(\"id\", \"_metadata.file_block_start\", \"_metadata.file_path\").count()\n        assert(r3 === multiRowgroupTableRowsNum - 1)\n      }\n    }\n  }\n\n  private def assertPredicatesArePushedDown(df: DataFrame): Unit = {\n    val scan = df.queryExecution.executedPlan.collectFirst {\n      case scan: FileSourceScanExec => scan\n    }\n    assert(scan.map(_.dataFilters.nonEmpty).getOrElse(true))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/deletionvectors/RoaringBitmapArraySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.deletionvectors\n\nimport java.nio.{ByteBuffer, ByteOrder}\n\nimport scala.collection.immutable.TreeSet\n\nimport com.google.common.primitives.Ints\n\nimport org.apache.spark.SparkFunSuite\n\nclass RoaringBitmapArraySuite extends SparkFunSuite {\n\n  final val BITMAP2_NUMBER = Int.MaxValue.toLong * 3L\n  /** RoaringBitmap containers mostly use `Char` constants internally, so this is consistent. */\n  final val CONTAINER_BOUNDARY = Char.MaxValue.toLong + 1L\n  final val BITMAP_BOUNDARY = 0xFFFFFFFFL + 1L\n\n  private def testEquality(referenceResult: Seq[Long])(\n    testOps: (RoaringBitmapArray => Unit)*): Unit = {\n    val referenceBitmap = RoaringBitmapArray(referenceResult: _*)\n    val testBitmap = RoaringBitmapArray()\n    testOps.foreach(op => op(testBitmap))\n    assert(testBitmap === referenceBitmap)\n    assert(testBitmap.## === referenceBitmap.##)\n    assert(testBitmap.toArray === referenceBitmap.toArray)\n  }\n\n  test(\"equality\") {\n    testEquality(Seq(1))(_.add(1))\n    testEquality(Nil)(_.add(1), _.remove(1))\n    testEquality(Seq(1))(_.add(1), _.add(1))\n    testEquality(Nil)(_.add(1), _.add(1), _.remove(1))\n    testEquality(Nil)(_.add(1), _.remove(1), _.remove(1))\n    testEquality(Nil)(_.add(1), _.add(1), _.remove(1), _.remove(1))\n    testEquality(Seq(1))(_.add(1), _.remove(1), _.add(1))\n    testEquality(Nil)(_.add(1), _.remove(1), _.add(1), _.remove(1))\n\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.remove(BITMAP2_NUMBER))\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(BITMAP2_NUMBER), _.add(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.add(BITMAP2_NUMBER), _.remove(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.remove(BITMAP2_NUMBER), _.remove(BITMAP2_NUMBER))\n    testEquality(Nil)(\n      _.add(BITMAP2_NUMBER),\n      _.add(BITMAP2_NUMBER),\n      _.remove(BITMAP2_NUMBER),\n      _.remove(BITMAP2_NUMBER))\n    testEquality(Seq(BITMAP2_NUMBER))(\n      _.add(BITMAP2_NUMBER),\n      _.remove(BITMAP2_NUMBER),\n      _.add(BITMAP2_NUMBER))\n    testEquality(Nil)(\n      _.add(BITMAP2_NUMBER),\n      _.remove(BITMAP2_NUMBER),\n      _.add(BITMAP2_NUMBER),\n      _.remove(BITMAP2_NUMBER))\n\n    testEquality(Seq(1, BITMAP2_NUMBER))(_.add(1), _.add(BITMAP2_NUMBER))\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(1), _.add(BITMAP2_NUMBER), _.remove(1))\n    testEquality(Seq(1, BITMAP2_NUMBER))(_.add(BITMAP2_NUMBER), _.add(1))\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(BITMAP2_NUMBER), _.add(1), _.remove(1))\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(1), _.remove(1), _.add(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(1), _.remove(1), _.add(BITMAP2_NUMBER), _.remove(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(1), _.add(BITMAP2_NUMBER), _.remove(1), _.remove(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.add(1), _.remove(1), _.remove(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.add(1), _.remove(BITMAP2_NUMBER), _.remove(1))\n\n    val denseSequence = 1L to (3L * CONTAINER_BOUNDARY)\n    def addAll(v: Long): RoaringBitmapArray => Unit = rb => rb.add(v)\n    testEquality(denseSequence)(denseSequence.map(addAll): _*)\n    testEquality(denseSequence)(denseSequence.reverse.map(addAll): _*)\n\n    val sparseSequence = 1L to BITMAP2_NUMBER by CONTAINER_BOUNDARY\n    testEquality(sparseSequence)(sparseSequence.map(addAll): _*)\n    testEquality(sparseSequence)(sparseSequence.reverse.map(addAll): _*)\n  }\n\n  /**\n   * A [[RoaringBitmapArray]] that contains all 3 container types\n   * in two [[org.roaringbitmap.RoaringBitmap]] instances.\n   */\n  lazy val allContainerTypesBitmap: RoaringBitmapArray = {\n    val bitmap = RoaringBitmapArray()\n    // RoaringBitmap 1 Container 1 (Array)\n    bitmap.addAll(1L, 17L, 63000L, CONTAINER_BOUNDARY - 1)\n    // RoaringBitmap 1 Container 2 (RLE)\n    bitmap.addRange((CONTAINER_BOUNDARY + 500L) until (CONTAINER_BOUNDARY + 1200L))\n    // RoaringBitmap 1 Container 3 (Bitset)\n    bitmap.addRange((2L * CONTAINER_BOUNDARY) until (3L * CONTAINER_BOUNDARY - 1L) by 3L)\n\n    // RoaringBitmap 2 Container 1 (Array)\n    bitmap.addAll(\n      BITMAP_BOUNDARY, BITMAP_BOUNDARY + 17L,\n      BITMAP_BOUNDARY + 63000L,\n      BITMAP_BOUNDARY + CONTAINER_BOUNDARY - 1)\n    // RoaringBitmap 2 Container 2 (RLE)\n    bitmap.addRange((BITMAP_BOUNDARY + CONTAINER_BOUNDARY + 500L) until\n      (BITMAP_BOUNDARY + CONTAINER_BOUNDARY + 1200L))\n    // RoaringBitmap 2 Container 3 (Bitset)\n    bitmap.addRange((BITMAP_BOUNDARY + 2L * CONTAINER_BOUNDARY) until\n      (BITMAP_BOUNDARY + 3L * CONTAINER_BOUNDARY - 1L) by 3L)\n\n    // Check that RLE containers are actually created.\n    assert(bitmap.runOptimize())\n\n    bitmap\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(s\"serialization - $serializationFormat\") {\n      checkSerializeDeserialize(RoaringBitmapArray(), serializationFormat)\n      checkSerializeDeserialize(RoaringBitmapArray(1L), serializationFormat)\n      checkSerializeDeserialize(RoaringBitmapArray(BITMAP2_NUMBER), serializationFormat)\n      checkSerializeDeserialize(RoaringBitmapArray(1L, BITMAP2_NUMBER), serializationFormat)\n      checkSerializeDeserialize(allContainerTypesBitmap, serializationFormat)\n    }\n  }\n\n  private def checkSerializeDeserialize(\n      input: RoaringBitmapArray,\n      format: RoaringBitmapArrayFormat.Value): Unit = {\n    val serializedSize = Ints.checkedCast(input.serializedSizeInBytes(format))\n    val buffer = ByteBuffer.allocate(serializedSize).order(ByteOrder.LITTLE_ENDIAN)\n    input.serialize(buffer, format)\n    val output = RoaringBitmapArray()\n    buffer.flip()\n    output.deserialize(buffer)\n    assert(input === output)\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(\n      s\"serialization and deserialization with big endian buffers throws - $serializationFormat\") {\n      val roaringBitmapArray = RoaringBitmapArray(1L)\n      val bigEndianBuffer = ByteBuffer\n        .allocate(roaringBitmapArray.serializedSizeInBytes(serializationFormat).toInt)\n        .order(ByteOrder.BIG_ENDIAN)\n\n      assertThrows[IllegalArgumentException] {\n        roaringBitmapArray.serialize(bigEndianBuffer, serializationFormat)\n      }\n\n      assertThrows[IllegalArgumentException] {\n        roaringBitmapArray.deserialize(bigEndianBuffer)\n      }\n    }\n  }\n\n  test(\"empty\") {\n    val bitmap = RoaringBitmapArray()\n    assert(bitmap.isEmpty)\n    assert(bitmap.cardinality === 0L)\n    assert(!bitmap.contains(0L))\n    assert(bitmap.toArray === Array.empty[Long])\n    var hadValue = false\n    bitmap.forEach(_ => hadValue = true)\n    assert(!hadValue)\n  }\n\n  test(\"special values\") {\n    testSpecialValue(0L)\n    testSpecialValue(Int.MaxValue.toLong)\n    testSpecialValue(CONTAINER_BOUNDARY - 1L)\n    testSpecialValue(CONTAINER_BOUNDARY)\n    testSpecialValue(BITMAP_BOUNDARY - 1L)\n    testSpecialValue(BITMAP_BOUNDARY)\n    testSpecialValue(3L * BITMAP_BOUNDARY + 42L)\n  }\n\n  private def testSpecialValue(value: Long): Unit = {\n    val bitmap = RoaringBitmapArray(value)\n    assert(bitmap.cardinality === 1L)\n    assert(bitmap.contains(value))\n    assert(bitmap.toArray === Array(value))\n    var valueCount = 0\n    bitmap.forEach { v =>\n      valueCount += 1\n      assert(v === value)\n    }\n    assert(valueCount === 1)\n    bitmap.remove(value)\n    assert(!bitmap.contains(value))\n    assert(bitmap.cardinality === 0L)\n  }\n\n  test(\"negative numbers\") {\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray()\n      bitmap.add(-1L)\n    }\n    assertThrows[IllegalArgumentException] {\n      RoaringBitmapArray(-1L)\n    }\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray(1L)\n      bitmap.remove(-1L)\n    }\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray()\n      bitmap.add(Long.MaxValue)\n    }\n    assertThrows[IllegalArgumentException] {\n      RoaringBitmapArray(Long.MaxValue)\n    }\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray(1L)\n      bitmap.remove(Long.MaxValue)\n    }\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray()\n      bitmap.addAll(-1L, 1L)\n    }\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray()\n      bitmap.addRange(-3 to 1)\n    }\n    assertThrows[IllegalArgumentException] {\n      val bitmap = RoaringBitmapArray()\n      bitmap.addRange(-3L to 1L)\n    }\n  }\n\n  private def testContainsButNoSimilarValues(value: Long, bitmap: RoaringBitmapArray): Unit = {\n    assert(bitmap.contains(value))\n    for (i <- 1 to 3) {\n      assert(!bitmap.contains(value + i * CONTAINER_BOUNDARY))\n      assert(!bitmap.contains(value + i * BITMAP_BOUNDARY))\n    }\n  }\n\n  test(\"small integers\") {\n    val bitmap = RoaringBitmapArray(\n      3L, 4L, CONTAINER_BOUNDARY - 1L, CONTAINER_BOUNDARY, Int.MaxValue.toLong)\n    assert(bitmap.cardinality === 5L)\n    testContainsButNoSimilarValues(3L, bitmap)\n    testContainsButNoSimilarValues(4L, bitmap)\n    testContainsButNoSimilarValues(CONTAINER_BOUNDARY - 1L, bitmap)\n    testContainsButNoSimilarValues(CONTAINER_BOUNDARY, bitmap)\n    testContainsButNoSimilarValues(Int.MaxValue.toLong, bitmap)\n    assert(bitmap.toArray ===\n      Array(3L, 4L, CONTAINER_BOUNDARY - 1L, CONTAINER_BOUNDARY, Int.MaxValue.toLong))\n    var values: List[Long] = Nil\n    bitmap.forEach { value =>\n      values ::= value\n    }\n    assert(values.reverse ===\n      List(3L, 4L, CONTAINER_BOUNDARY - 1L, CONTAINER_BOUNDARY, Int.MaxValue.toLong))\n    bitmap.remove(CONTAINER_BOUNDARY)\n    assert(!bitmap.contains(CONTAINER_BOUNDARY))\n    assert(bitmap.cardinality === 4L)\n    testContainsButNoSimilarValues(3L, bitmap)\n    testContainsButNoSimilarValues(4L, bitmap)\n    testContainsButNoSimilarValues(CONTAINER_BOUNDARY - 1L, bitmap)\n    testContainsButNoSimilarValues(Int.MaxValue.toLong, bitmap)\n  }\n\n  test(\"large integers\") {\n    val container1Number = Int.MaxValue.toLong + 1L\n    val container3Number = 2 * BITMAP_BOUNDARY + 1L\n    val bitmap = RoaringBitmapArray(\n      3L, 4L, container1Number, BITMAP_BOUNDARY, BITMAP2_NUMBER, container3Number)\n    assert(bitmap.cardinality === 6L)\n    testContainsButNoSimilarValues(3L, bitmap)\n    testContainsButNoSimilarValues(4L, bitmap)\n    testContainsButNoSimilarValues(container1Number, bitmap)\n    testContainsButNoSimilarValues(BITMAP_BOUNDARY, bitmap)\n    testContainsButNoSimilarValues(BITMAP2_NUMBER, bitmap)\n    testContainsButNoSimilarValues(container3Number, bitmap)\n    assert(bitmap.toArray ===\n      Array(3L, 4L, container1Number, BITMAP_BOUNDARY, BITMAP2_NUMBER, container3Number))\n    var values: List[Long] = Nil\n    bitmap.forEach { value =>\n      values ::= value\n    }\n    assert(values.reverse ===\n      List(3L, 4L, container1Number, BITMAP_BOUNDARY, BITMAP2_NUMBER, container3Number))\n    bitmap.remove(BITMAP_BOUNDARY)\n    assert(!bitmap.contains(BITMAP_BOUNDARY))\n    assert(bitmap.cardinality === 5L)\n    testContainsButNoSimilarValues(3L, bitmap)\n    testContainsButNoSimilarValues(4L, bitmap)\n    testContainsButNoSimilarValues(container1Number, bitmap)\n    testContainsButNoSimilarValues(BITMAP2_NUMBER, bitmap)\n    testContainsButNoSimilarValues(container3Number, bitmap)\n  }\n\n  test(\"add/remove round-trip\") {\n    // Single value in the second bitmap\n    val bitmap = RoaringBitmapArray(BITMAP2_NUMBER)\n    assert(bitmap.contains(BITMAP2_NUMBER))\n    bitmap.remove(BITMAP2_NUMBER)\n    assert(!bitmap.contains(BITMAP2_NUMBER))\n    bitmap.add(BITMAP2_NUMBER)\n    assert(bitmap.contains(BITMAP2_NUMBER))\n\n    // Two values in two bitmaps\n    bitmap.add(CONTAINER_BOUNDARY)\n    assert(bitmap.contains(CONTAINER_BOUNDARY))\n    assert(bitmap.contains(BITMAP2_NUMBER))\n    bitmap.remove(CONTAINER_BOUNDARY)\n    assert(!bitmap.contains(CONTAINER_BOUNDARY))\n    assert(bitmap.contains(BITMAP2_NUMBER))\n    bitmap.add(CONTAINER_BOUNDARY)\n    assert(bitmap.contains(CONTAINER_BOUNDARY))\n    assert(bitmap.contains(BITMAP2_NUMBER))\n  }\n\n  test(\"or\") {\n    testOr(left = TreeSet.empty, right = TreeSet.empty)\n    testOr(left = TreeSet(1L), right = TreeSet.empty)\n    testOr(left = TreeSet.empty, right = TreeSet(1L))\n    testOr(left = TreeSet(0L, CONTAINER_BOUNDARY), right = TreeSet(1L, BITMAP_BOUNDARY - 1L))\n    testOr(\n      left = TreeSet(0L, CONTAINER_BOUNDARY, BITMAP2_NUMBER),\n      right = TreeSet(1L, BITMAP_BOUNDARY - 1L))\n    testOr(\n      left = TreeSet(0L, CONTAINER_BOUNDARY),\n      right = TreeSet(1L, BITMAP_BOUNDARY - 1L, BITMAP2_NUMBER))\n  }\n\n  private def testOr(left: TreeSet[Long], right: TreeSet[Long]): Unit = {\n    val leftBitmap = RoaringBitmapArray(left.toSeq: _*)\n    val rightBitmap = RoaringBitmapArray(right.toSeq: _*)\n\n    val expected = left.union(right).toSeq\n\n    leftBitmap.or(rightBitmap)\n\n    assert(leftBitmap.toArray.toSeq === expected)\n  }\n\n  test(\"andNot\") {\n    testAndNot(left = TreeSet.empty, right = TreeSet.empty)\n    testAndNot(left = TreeSet(1L), right = TreeSet.empty)\n    testAndNot(left = TreeSet.empty, right = TreeSet(1L))\n    testAndNot(left = TreeSet(0L, CONTAINER_BOUNDARY), right = TreeSet(1L, BITMAP_BOUNDARY - 1L))\n    testAndNot(\n      left = TreeSet(0L, CONTAINER_BOUNDARY, BITMAP2_NUMBER),\n      right = TreeSet(1L, BITMAP_BOUNDARY - 1L))\n    testAndNot(\n      left = TreeSet(0L, CONTAINER_BOUNDARY),\n      right = TreeSet(1L, BITMAP_BOUNDARY - 1L, BITMAP2_NUMBER))\n  }\n\n  private def testAndNot(left: TreeSet[Long], right: TreeSet[Long]): Unit = {\n    val leftBitmap = RoaringBitmapArray()\n    left.foreach(leftBitmap.add)\n    val rightBitmap = RoaringBitmapArray()\n    right.foreach(rightBitmap.add)\n\n    val expected = left.diff(right).toArray\n\n    leftBitmap.andNot(rightBitmap)\n\n    assert(leftBitmap.toArray === expected)\n  }\n\n  test(\"and\") {\n    // Empty result\n    testAnd(left = TreeSet.empty, right = TreeSet.empty)\n    testAnd(left = TreeSet.empty, right = TreeSet(1L))\n    testAnd(left = TreeSet.empty, right = TreeSet(1L, BITMAP_BOUNDARY - 1L))\n    testAnd(left = TreeSet.empty, right = TreeSet(0L, CONTAINER_BOUNDARY, BITMAP2_NUMBER))\n    testAnd(left = TreeSet(1L), right = TreeSet.empty)\n    testAnd(left = TreeSet(1L), right = TreeSet(BITMAP_BOUNDARY))\n    testAnd(left = TreeSet(1L), right = TreeSet(CONTAINER_BOUNDARY))\n    testAnd(left = TreeSet(1L, BITMAP_BOUNDARY - 1L), right = TreeSet.empty)\n    testAnd(left = TreeSet(0L, CONTAINER_BOUNDARY, BITMAP2_NUMBER), right = TreeSet.empty)\n    testAnd(\n      left = TreeSet(0L, CONTAINER_BOUNDARY, BITMAP2_NUMBER),\n      right = TreeSet(1L, BITMAP_BOUNDARY - 1L))\n    testAnd(\n      left = TreeSet(0L, CONTAINER_BOUNDARY),\n      right = TreeSet(1L, BITMAP_BOUNDARY - 1L, BITMAP2_NUMBER))\n\n    // Non empty result\n    testAnd(left = TreeSet(0L, 5L, 10L), right = TreeSet(5L, 15L))\n    testAnd(\n      left = TreeSet(0L, CONTAINER_BOUNDARY, BITMAP2_NUMBER),\n      right = TreeSet(1L, BITMAP2_NUMBER))\n    testAnd(\n      left = TreeSet(1L, BITMAP_BOUNDARY, CONTAINER_BOUNDARY),\n      right = TreeSet(1L, BITMAP_BOUNDARY, CONTAINER_BOUNDARY))\n  }\n\n  private def testAnd(left: TreeSet[Long], right: TreeSet[Long]): Unit = {\n    val leftBitmap = RoaringBitmapArray()\n    leftBitmap.addAll(left.toSeq: _*)\n    val rightBitmap = RoaringBitmapArray()\n    rightBitmap.addAll(right.toSeq: _*)\n\n    leftBitmap.and(rightBitmap)\n    val expected = left.intersect(right)\n    assert(leftBitmap.toArray === expected.toArray)\n  }\n\n  test(\"clear\") {\n    testEquality(Nil)(_.add(1), _.clear())\n    testEquality(Nil)(_.add(1), _.add(1), _.clear())\n    testEquality(Nil)(_.add(1), _.clear(), _.clear())\n    testEquality(Nil)(_.add(1), _.add(1), _.clear(), _.clear())\n    testEquality(Seq(1))(_.add(1), _.clear(), _.add(1))\n    testEquality(Nil)(_.add(1), _.clear(), _.add(1), _.clear())\n\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.clear())\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.add(BITMAP2_NUMBER), _.clear())\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.clear(), _.clear())\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.add(BITMAP2_NUMBER), _.clear(), _.clear())\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(BITMAP2_NUMBER), _.clear(), _.add(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.clear(), _.add(BITMAP2_NUMBER), _.clear())\n\n    testEquality(Nil)(_.add(1), _.add(BITMAP2_NUMBER), _.clear())\n    testEquality(Nil)(_.add(BITMAP2_NUMBER), _.add(1), _.clear())\n    testEquality(Seq(BITMAP2_NUMBER))(_.add(1), _.clear(), _.add(BITMAP2_NUMBER))\n    testEquality(Nil)(_.add(1), _.clear(), _.add(BITMAP2_NUMBER), _.clear())\n    testEquality(Nil)(_.add(1), _.add(BITMAP2_NUMBER), _.clear(), _.clear())\n\n    val denseSequence = 1L to (3L * CONTAINER_BOUNDARY)\n    testEquality(Nil)(_.addAll(denseSequence: _*), _.clear())\n\n    val sparseSequence = 1L to BITMAP2_NUMBER by CONTAINER_BOUNDARY\n    testEquality(Nil)(_.addAll(sparseSequence: _*), _.clear())\n  }\n\n  test(\"bulk adds\") {\n\n    def testArrayEquality(referenceResult: Seq[Long], command: RoaringBitmapArray => Unit): Unit = {\n      val testBitmap = RoaringBitmapArray()\n      command(testBitmap)\n      assert(testBitmap.toArray.toSeq === referenceResult)\n    }\n\n    val bitmap = RoaringBitmapArray(1L, 5L, CONTAINER_BOUNDARY, BITMAP_BOUNDARY)\n    assert(bitmap.toArray.toSeq === Seq(1L, 5L, CONTAINER_BOUNDARY, BITMAP_BOUNDARY))\n\n    testArrayEquality(\n      referenceResult = Seq(1L, 5L, CONTAINER_BOUNDARY, BITMAP_BOUNDARY),\n      command = _.addAll(1L, 5L, CONTAINER_BOUNDARY, BITMAP_BOUNDARY))\n\n    testArrayEquality(\n      referenceResult = (CONTAINER_BOUNDARY - 5L) to (CONTAINER_BOUNDARY + 5L),\n      command = _.addRange((CONTAINER_BOUNDARY - 5L) to (CONTAINER_BOUNDARY + 5L)))\n\n    testArrayEquality(\n      referenceResult = (CONTAINER_BOUNDARY - 5L) to (CONTAINER_BOUNDARY + 5L) by 3L,\n      command = _.addRange((CONTAINER_BOUNDARY - 5L) to (CONTAINER_BOUNDARY + 5L) by 3L))\n\n    // Int ranges call a different method.\n    testArrayEquality(\n      referenceResult = (CONTAINER_BOUNDARY - 5L) to (CONTAINER_BOUNDARY + 5L),\n      command = _.addRange((CONTAINER_BOUNDARY - 5L).toInt to (CONTAINER_BOUNDARY + 5L).toInt))\n\n    testArrayEquality(\n      referenceResult = (CONTAINER_BOUNDARY - 5L) to (CONTAINER_BOUNDARY + 5L) by 3L,\n      command = _.addRange((CONTAINER_BOUNDARY - 5L).toInt to (CONTAINER_BOUNDARY + 5L).toInt by 3))\n\n    testArrayEquality(\n      referenceResult = (BITMAP_BOUNDARY - 5L) to BITMAP_BOUNDARY,\n      command = _.addRange((BITMAP_BOUNDARY - 5L) to BITMAP_BOUNDARY))\n\n    testArrayEquality(\n      referenceResult = (BITMAP_BOUNDARY - 5L) to (BITMAP_BOUNDARY + 5L),\n      command = _.addRange((BITMAP_BOUNDARY - 5L) to (BITMAP_BOUNDARY + 5L)))\n\n    testArrayEquality(\n      referenceResult = BITMAP_BOUNDARY to (BITMAP_BOUNDARY + 5L),\n      command = _.addRange(BITMAP_BOUNDARY to (BITMAP_BOUNDARY + 5L)))\n  }\n\n  test(\"large cardinality\") {\n    val bitmap = RoaringBitmapArray()\n    // We can't produce ranges in Scala whose lengths would be greater than Int.MaxValue\n    // so we add them in stages of Int.MaxValue / 2 instead.\n    for (index <- 0 until 6) {\n      val start = index.toLong * Int.MaxValue.toLong / 2L\n      val end = (index.toLong + 1L) * Int.MaxValue.toLong / 2L\n      bitmap.addRange(start until end)\n    }\n    assert(bitmap.cardinality === (3L * Int.MaxValue.toLong))\n    for (index <- 0 until 6) {\n      val start = index.toLong * Int.MaxValue.toLong / 2L\n      val end = (index.toLong + 1L) * Int.MaxValue.toLong / 2L\n      val stride = 1023\n      for (pos <- start until end by stride) {\n        assert(bitmap.contains(pos))\n      }\n    }\n    assert(!bitmap.contains(3L * Int.MaxValue.toLong))\n    assert(!bitmap.contains(3L * Int.MaxValue.toLong + 42L))\n  }\n\n  test(\"first/last\") {\n    {\n      val bitmap = RoaringBitmapArray()\n      assert(bitmap.first.isEmpty)\n      assert(bitmap.last.isEmpty)\n    }\n    // Single value bitmaps.\n    val valuesOfInterest = Seq(0L, 1L, 64L, CONTAINER_BOUNDARY, BITMAP_BOUNDARY, BITMAP2_NUMBER)\n    for (v <- valuesOfInterest) {\n      val bitmap = RoaringBitmapArray(v)\n      assert(bitmap.first === Some(v))\n      assert(bitmap.last === Some(v))\n    }\n    // Two value bitmaps.\n    for {\n      start <- valuesOfInterest\n      end <- valuesOfInterest\n      if start < end\n    } {\n      val bitmap = RoaringBitmapArray(start, end)\n      assert(bitmap.first === Some(start))\n      assert(bitmap.last === Some(end))\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/deletionvectors/RowIndexMarkingFiltersSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.deletionvectors\n\nimport org.apache.spark.sql.delta.RowIndexFilter\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor\nimport org.apache.spark.sql.delta.actions.DeletionVectorDescriptor._\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore._\nimport org.apache.spark.sql.delta.util.PathWithFileSystem\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.execution.vectorized.{OnHeapColumnVector, WritableColumnVector}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.ByteType\nimport org.apache.spark.util.Utils\n\nclass RowIndexMarkingFiltersSuite extends QueryTest with SharedSparkSession {\n\n  test(\"empty deletion vector (drop filter)\") {\n    val rowIndexFilter = DropMarkedRowsFilter.createInstance(\n      DeletionVectorDescriptor.EMPTY,\n      newHadoopConf,\n      tablePath = None)\n\n    assert(getMarked(rowIndexFilter, start = 0, end = 20) === Seq.empty)\n    assert(getMarked(rowIndexFilter, start = 20, end = 200) === Seq.empty)\n    assert(getMarked(rowIndexFilter, start = 200, end = 2000) === Seq.empty)\n  }\n\n  test(\"empty deletion vector (keep filter)\") {\n    val rowIndexFilter = KeepMarkedRowsFilter.createInstance(\n      DeletionVectorDescriptor.EMPTY,\n      newHadoopConf,\n      tablePath = None)\n\n    assert(getMarked(rowIndexFilter, start = 0, end = 20) === 0.until(20))\n    assert(getMarked(rowIndexFilter, start = 20, end = 200) === 20.until(200))\n    assert(getMarked(rowIndexFilter, start = 200, end = 2000) === 200.until(2000))\n  }\n\n  private val filtersToBeTested =\n    Seq((DropMarkedRowsFilter, \"drop\"), (KeepMarkedRowsFilter, \"keep\"))\n\n  for {\n    (filterType, filterName) <- filtersToBeTested\n    isInline <- BOOLEAN_DOMAIN\n  } {\n    test(s\"deletion vector single row marked (isInline=$isInline) ($filterName filter)\") {\n      withTempDir { tableDir =>\n        val tablePath = unescapedStringToPath(tableDir.toString)\n        val dv = createDV(isInline, tablePath, 25)\n\n        val rowIndexFilter = filterType.createInstance(dv, newHadoopConf, Some(tablePath))\n\n        def correctValues(range: Seq[Long]): Seq[Long] = filterName match {\n          case \"drop\" => range.filter(_ == 25)\n          case \"keep\" => range.filterNot(_ == 25)\n          case _ => throw new RuntimeException(\"unreachable code reached\")\n        }\n\n        for ((start, end) <- Seq((0, 20), (20, 35), (35, 325))) {\n          val actual = getMarked(rowIndexFilter, start, end)\n          val correct = correctValues(start.toLong.until(end))\n          assert(actual === correct)\n        }\n      }\n    }\n  }\n\n  for {\n    (filterType, filterName) <- filtersToBeTested\n    isInline <- BOOLEAN_DOMAIN\n  } {\n    test(s\"deletion vector with multiple rows marked (isInline=$isInline) ($filterName filter)\") {\n      withTempDir { tableDir =>\n        val tablePath = unescapedStringToPath(tableDir.toString)\n        val markedRows = Seq[Long](0, 25, 35, 2000, 50000)\n        val dv = createDV(isInline, tablePath, markedRows: _*)\n\n        val rowIndexFilter = filterType.createInstance(dv, newHadoopConf, Some(tablePath))\n\n        def correctValues(range: Seq[Long]): Seq[Long] = filterName match {\n          case \"drop\" => range.filter(markedRows.contains(_))\n          case \"keep\" => range.filterNot(markedRows.contains(_))\n          case _ => throw new RuntimeException(\"unreachable code reached\")\n        }\n\n        for ((start, end) <- Seq(\n          (0, 20), (20, 35), (35, 325), (325, 1000), (1000, 60000), (60000, 800000))) {\n          val actual = getMarked(rowIndexFilter, start, end)\n          val correct = correctValues(start.toLong.until(end))\n          assert(actual === correct)\n        }\n      }\n    }\n  }\n\n  private def newBatch(capacity: Int): WritableColumnVector =\n    new OnHeapColumnVector(capacity, ByteType)\n\n  protected def newHadoopConf: Configuration = {\n    // scalastyle:off deltahadoopconfiguration\n    spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  /**\n   * Helper method that creates DV with the given deleted row ids and returns\n   * a [[DeletionVectorDescriptor]]. DV created can be an in-line or on disk\n   */\n  protected def createDV(\n      isInline: Boolean, tablePath: Path, markedRows: Long*): DeletionVectorDescriptor = {\n    val bitmap = RoaringBitmapArray(markedRows: _*)\n    val serializedBitmap = bitmap.serializeAsByteArray(RoaringBitmapArrayFormat.Portable)\n    val cardinality = markedRows.size\n    if (isInline) {\n      inlineInLog(serializedBitmap, cardinality)\n    } else {\n      val tableWithFS = PathWithFileSystem.withConf(tablePath, newHadoopConf).makeQualified()\n      val dvPath = dvStore.generateUniqueNameInTable(tableWithFS)\n      val dvRange = Utils.tryWithResource(dvStore.createWriter(dvPath)) { writer =>\n        writer.write(serializedBitmap)\n      }\n      onDiskWithAbsolutePath(\n        pathToEscapedString(dvPath.path), dvRange.length, cardinality, Some(dvRange.offset))\n    }\n  }\n\n  /** Evaluate the given row index filter instance and return sequence of marked rows indexes */\n  protected def getMarked(rowIndexFilter: RowIndexFilter, start: Long, end: Long): Seq[Long] = {\n    val batchSize = (end - start + 1).toInt\n    val batch = newBatch(batchSize)\n    rowIndexFilter.materializeIntoVector(start, end, batch)\n    batch.getBytes(0, batchSize).toSeq\n      .zip(Seq.range(start, end))\n      .filter(_._1 == RowIndexFilter.DROP_ROW_VALUE) // filter out marked rows\n      .map(_._2) // select only the row id\n      .toSeq\n  }\n\n  lazy val dvStore: DeletionVectorStore = DeletionVectorStore.createInstance(newHadoopConf)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/DecodeNestedZ85EncodedVariantSuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport java.util.Arrays\n\nimport org.apache.spark.sql.{Column, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.expressions.variant.VariantExpressionEvalUtils\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.DeltaStatsJsonUtils\nimport org.apache.spark.sql.functions.{col, from_json}\nimport org.apache.spark.sql.types.{IntegerType, LongType, StringType, StructField, StructType, VariantType}\nimport org.apache.spark.types.variant.Variant\nimport org.apache.spark.unsafe.types.{UTF8String, VariantVal}\n\nclass DecodeNestedZ85EncodedVariantSuite extends QueryTest with DeltaSQLCommandTest {\n\n  test(\"RoundTrip alternateVariantEncoding Z85\") {\n    val jsonValues = Seq(\n      \"21\",\n      \"1021\",\n      \"-29183652\",\n      \"[1, null, true, {\\\"a\\\": 1}]\",\n      \"{\\\"key1\\\": \\\"value_1\\\", \\\"key_2\\\": [\\\"value2\\\", 1385731029.1236421], \\\"key3\\\": false}\"\n    )\n\n    jsonValues.foreach { json =>\n      val inputVariant = VariantExpressionEvalUtils.parseJson(UTF8String.fromString(json))\n      val variant = new Variant(inputVariant.getValue, inputVariant.getMetadata)\n\n      // Encode as Z85\n      val z85 = DeltaStatsJsonUtils.encodeVariantAsZ85(variant)\n\n      // Create a DataFrame with the Z85 string\n      val df = spark.range(1).selectExpr(s\"\"\"'{\"v\":\"$z85\"}' as z85_string\"\"\")\n\n      // Parse as JSON (this creates a VariantVal containing the Z85 string)\n      val statsSchema = StructType(Seq(StructField(\"v\", VariantType)))\n      val parsedDf = df.withColumn(\"parsed\", from_json(col(\"z85_string\"), statsSchema))\n\n      // Apply DecodeNestedZ85EncodedVariant\n      val decodedDf = parsedDf.withColumn(\n        \"decoded\",\n        Column(DecodeNestedZ85EncodedVariant(col(\"parsed\").expr))\n      )\n\n      // Extract the decoded variant and verify\n      val result = decodedDf.select(\"decoded.v\").head().get(0)\n      val decodedVariant = result.asInstanceOf[VariantVal]\n\n      assert(Arrays.equals(inputVariant.getMetadata, decodedVariant.getMetadata),\n        s\"Metadata mismatch for JSON: $json\")\n      assert(Arrays.equals(inputVariant.getValue, decodedVariant.getValue),\n        s\"Value mismatch for JSON: $json\")\n    }\n  }\n\n  test(\"DecodeNestedZ85EncodedVariantSuite with nested struct and mixed types\") {\n    val json1 = \"{\\\"id\\\": 100, \\\"name\\\": \\\"test\\\"}\"\n    val inputVariant1 = VariantExpressionEvalUtils.parseJson(UTF8String.fromString(json1))\n    val variant1 = new Variant(inputVariant1.getValue, inputVariant1.getMetadata)\n    val z85_1 = DeltaStatsJsonUtils.encodeVariantAsZ85(variant1)\n\n    val json2 = \"{\\\"count\\\": 42}\"\n    val inputVariant2 = VariantExpressionEvalUtils.parseJson(UTF8String.fromString(json2))\n    val variant2 = new Variant(inputVariant2.getValue, inputVariant2.getMetadata)\n    val z85_2 = DeltaStatsJsonUtils.encodeVariantAsZ85(variant2)\n\n    // Create stats schema with nested variant, non-variant fields, and nullable variant\n    val statsSchema = StructType(Seq(\n      StructField(\"numRecords\", LongType, nullable = true),\n      StructField(\"minValues\", StructType(Seq(\n        StructField(\"intCol\", IntegerType, nullable = true),\n        StructField(\"stringCol\", StringType, nullable = true),\n        StructField(\"v\", VariantType, nullable = true),\n        StructField(\"v2\", VariantType, nullable = true),\n        StructField(\"missingField\", StringType, nullable = true)\n      )), nullable = true),\n      StructField(\"maxValues\", StructType(Seq(\n        StructField(\"intCol\", IntegerType, nullable = true),\n        StructField(\"stringCol\", StringType, nullable = true),\n        StructField(\"v\", VariantType, nullable = true),\n        StructField(\"v2\", VariantType, nullable = true)\n      )), nullable = true)\n    ))\n\n    val statsJson = s\"\"\"{\"numRecords\": 1000,\"\"\" +\n      s\"\"\"\"minValues\": {\"intCol\": 1, \"stringCol\": \"a\", \"v\": \"$z85_1\"},\"\"\" +\n      s\"\"\"\"maxValues\": {\"intCol\": 100, \"stringCol\": \"z\", \"v\": \"$z85_1\", \"v2\": \"$z85_2\"}\"\"\" +\n      s\"\"\"}\"\"\"\n\n    val df = spark.range(1).selectExpr(s\"\"\"'${statsJson}' as stats\"\"\")\n\n    val parsedDf = df.withColumn(\"parsed\", from_json(col(\"stats\"), statsSchema))\n    val decodedDf = parsedDf.withColumn(\n      \"decoded\",\n      Column(DecodeNestedZ85EncodedVariant(col(\"parsed\").expr))\n    )\n\n    val result = decodedDf.select(\n      \"decoded.numRecords\",\n      \"decoded.minValues.intCol\",\n      \"decoded.minValues.stringCol\",\n      \"decoded.minValues.v\",\n      \"decoded.minValues.v2\",\n      \"decoded.minValues.missingField\",\n      \"decoded.maxValues.intCol\",\n      \"decoded.maxValues.stringCol\",\n      \"decoded.maxValues.v\",\n      \"decoded.maxValues.v2\"\n    ).head()\n\n    // Check non-variant fields pass through unchanged\n    assert(result.getLong(0) == 1000L)\n    assert(result.getInt(1) == 1)\n    assert(result.getString(2) == \"a\")\n\n    // Check decoded variant\n    val decodedVariant1Min = result.get(3).asInstanceOf[VariantVal]\n    assert(Arrays.equals(inputVariant1.getMetadata, decodedVariant1Min.getMetadata))\n    assert(Arrays.equals(inputVariant1.getValue, decodedVariant1Min.getValue))\n\n    // Check null variant (v2 in minValues)\n    assert(result.isNullAt(4))\n\n    // Check missing field returns null\n    assert(result.isNullAt(5))\n\n    // Check maxValues\n    assert(result.getInt(6) == 100)\n    assert(result.getString(7) == \"z\")\n\n    val decodedVariant1Max = result.get(8).asInstanceOf[VariantVal]\n    assert(Arrays.equals(inputVariant1.getMetadata, decodedVariant1Max.getMetadata))\n    assert(Arrays.equals(inputVariant1.getValue, decodedVariant1Max.getValue))\n\n    val decodedVariant2Max = result.get(9).asInstanceOf[VariantVal]\n    assert(Arrays.equals(inputVariant2.getMetadata, decodedVariant2Max.getMetadata))\n    assert(Arrays.equals(inputVariant2.getValue, decodedVariant2Max.getValue))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/HilbertIndexSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport java.util\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.delta.test.shims.GridTestShim\n\nclass HilbertIndexSuite extends SparkFunSuite with GridTestShim {\n\n  /**\n   * Represents a test case. Each n-k pair will verify the continuity of the mapping,\n   * and the reversibility of it.\n   * @param n The number of dimensions\n   * @param k The number of bits in each dimension\n   */\n  case class TestCase(n: Int, k: Int)\n  val testCases = Seq(\n    TestCase(2, 10),\n    TestCase(3, 6),\n    TestCase(4, 5),\n    TestCase(5, 4),\n    TestCase(6, 3)\n  )\n\n  gridTest(\"HilbertStates caches states\")(2 to 9) { n =>\n    val start = System.nanoTime()\n    HilbertStates.getStateList(n)\n    val end = System.nanoTime()\n\n    HilbertStates.getStateList(n)\n    val end2 = System.nanoTime()\n    assert(end2 - end < end - start)\n  }\n\n  gridTest(\"Hilbert Mapping is continuous (long keys)\")(testCases) { case TestCase(n, k) =>\n    val generator = HilbertIndex.getStateGenerator(n)\n\n    val stateList = generator.generateStateList()\n\n    val states = stateList.getDKeyToNPointStateMap\n\n    val maxDKeys = 1L << (k * n)\n    var d = 0\n    var lastPoint = new Array[Int](n)\n    while (d < maxDKeys) {\n      val point = states.translateDKeyToNPoint(d, k)\n      if (d != 0) {\n        assert(HilbertUtils.manhattanDist(lastPoint, point) == 1)\n      }\n\n      lastPoint = point\n      d += 1\n    }\n\n  }\n\n  gridTest(\"Hilbert Mapping is 1 to 1 (long keys)\")(testCases) { case TestCase(n, k) =>\n    val generator = HilbertIndex.getStateGenerator(n)\n    val stateList = generator.generateStateList()\n\n    val d2p = stateList.getDKeyToNPointStateMap\n    val p2d = stateList.getNPointToDKeyStateMap\n\n    val maxDKeys = 1L << (k * n)\n    var d = 0\n    while (d < maxDKeys) {\n      val point = d2p.translateDKeyToNPoint(d, k)\n      val d2 = p2d.translateNPointToDKey(point, k)\n      assert(d == d2)\n      d += 1\n    }\n  }\n\n  gridTest(\"Hilbert Mapping is continuous (array keys)\")(testCases) { case TestCase(n, k) =>\n    val generator = HilbertIndex.getStateGenerator(n)\n\n    val stateList = generator.generateStateList()\n\n    val states = stateList.getDKeyToNPointStateMap\n\n    val maxDKeys = 1L << (k * n)\n    val d = new Array[Byte](((k * n) / 8) + 1)\n    var lastPoint = new Array[Int](n)\n    var i = 0\n    while (i < maxDKeys) {\n      val point = states.translateDKeyArrayToNPoint(d, k)\n      if (i != 0) {\n        assert(HilbertUtils.manhattanDist(lastPoint, point) == 1,\n          s\"$i ${d.toSeq.map(_.toBinaryString.takeRight(8))} ${lastPoint.toSeq} to ${point.toSeq}\")\n      }\n\n      lastPoint = point\n      i += 1\n      HilbertUtils.addOne(d)\n    }\n\n  }\n\n  gridTest(\"Hilbert Mapping is 1 to 1 (array keys)\")(testCases) { case TestCase(n, k) =>\n    val generator = HilbertIndex.getStateGenerator(n)\n    val stateList = generator.generateStateList()\n\n    val d2p = stateList.getDKeyToNPointStateMap\n    val p2d = stateList.getNPointToDKeyStateMap\n\n    val maxDKeys = 1L << (k * n)\n    val d = new Array[Byte](((k * n) / 8) + 1)\n    var i = 0\n    while (i < maxDKeys) {\n      val point = d2p.translateDKeyArrayToNPoint(d, k)\n      val d2 = p2d.translateNPointToDKeyArray(point, k)\n      assert(util.Arrays.equals(d, d2), s\"$i ${d.toSeq}, ${d2.toSeq}\")\n      i += 1\n      HilbertUtils.addOne(d)\n    }\n  }\n\n  gridTest(\"continuous and 1 to 1 for all spaces\")((2 to 9).map(n => TestCase(n, 15 - n))) {\n      case TestCase(n, k) =>\n    val generator = HilbertIndex.getStateGenerator(n)\n    val stateList = generator.generateStateList()\n\n    val d2p = stateList.getDKeyToNPointStateMap\n    val p2d = stateList.getNPointToDKeyStateMap\n\n    val numBits = k * n\n    val numBytes = (numBits + 7) / 8\n\n    // test 1000 contiguous 1000 point blocks to make sure the mapping is continuous and one to one\n\n    val maxDKeys = 1L << (k * n)\n    val step = maxDKeys / 1000\n    var x = 0L\n    for (_ <- 0 until 1000) {\n      var dLong = x\n      val bigIntArray = BigInt(dLong).toByteArray\n      val dArray = new Array[Byte](numBytes)\n\n      System.arraycopy(\n        bigIntArray,\n        math.max(0, bigIntArray.length - dArray.length),\n        dArray,\n        math.max(0, dArray.length - bigIntArray.length),\n        math.min(bigIntArray.length, dArray.length)\n      )\n\n      var lastPoint: Array[Int] = null\n\n      for (_ <- 0 until 1000) {\n        val pArray = d2p.translateDKeyArrayToNPoint(dArray, k)\n        val pLong = d2p.translateDKeyToNPoint(dLong, k)\n        assert(util.Arrays.equals(pArray, pLong), s\"points should be the same at $dLong\")\n\n        if (lastPoint != null) {\n          assert(HilbertUtils.manhattanDist(lastPoint, pLong) == 1,\n            s\"distance between point and last point should be the same at $dLong\")\n        }\n\n        val dArray2 = p2d.translateNPointToDKeyArray(pArray, k)\n        val dLong2 = p2d.translateNPointToDKey(pLong, k)\n\n        assert(dLong == dLong2, s\"reversing the points should map correctly at $dLong != $dLong2\")\n\n        assert(util.Arrays.equals(dArray, dArray2),\n          s\"reversing the points should map correctly at $dLong\")\n\n        lastPoint = pLong\n\n        dLong += 1\n        HilbertUtils.addOne(dArray)\n      }\n\n      x += step\n    }\n\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/HilbertUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport java.util\n\nimport org.apache.spark.sql.delta.expressions.HilbertUtils.HilbertMatrix\n\nimport org.apache.spark.SparkFunSuite\n\nclass HilbertUtilsSuite extends SparkFunSuite {\n\n  test(\"circularLeftShift\") {\n    assert(\n      (0 until (1 << 10) by 7).forall(i => HilbertUtils.circularLeftShift(10, i, 0) == i),\n      \"Shift by 0 should be a no op\"\n    )\n    assert(\n      (0 until (1 << 10) by 7).forall(i => HilbertUtils.circularLeftShift(10, i, 10) == i),\n      \"Shift by n should be a no op\"\n    )\n    // 0111 (<< 2) => 1101\n    assert(\n      HilbertUtils.circularLeftShift(4, 7, 2) == 13,\n      \"handle wrapping\"\n    )\n    assert(\n      (0 until (1 << 5)).forall(HilbertUtils.circularLeftShift(5, _, 5) <= (1 << 5)),\n      \"always mask values based on n\"\n    )\n  }\n\n  test(\"circularRightShift\") {\n    assert(\n      (0 until (1 << 10) by 7).forall(i => HilbertUtils.circularRightShift(10, i, 0) == i),\n      \"Shift by 0 should be a no op\"\n    )\n    assert(\n      (0 until (1 << 10) by 7).forall(i => HilbertUtils.circularRightShift(10, i, 10) == i),\n      \"Shift by n should be a no op\"\n    )\n    // 0111 (>> 2) => 1101\n    assert(\n      HilbertUtils.circularRightShift(4, 7, 2) == 13,\n      \"handle wrapping\"\n    )\n    assert(\n      (0 until (1 << 5)).forall(HilbertUtils.circularRightShift(5, _, 5) <= (1 << 5)),\n      \"always mask values based on n\"\n    )\n  }\n\n  test(\"getSetColumn should return the column that is set\") {\n    (0 until 16) foreach { i =>\n      assert(HilbertUtils.getSetColumn(16, 1 << i) ==  16 - 1 - i)\n    }\n  }\n\n  test(\"HilbertMatrix makes sense\") {\n    val identityMatrix = HilbertMatrix.identity(10)\n    (0 until (1 << 10) by 7) foreach { i =>\n      assert(identityMatrix.transform(i) == i, s\"$i transformed by the identity should be $i\")\n    }\n\n    identityMatrix.multiply(HilbertMatrix.identity(10)) == identityMatrix\n\n    val shift5 = HilbertMatrix(10, 0, 5)\n    assert(shift5.multiply(shift5) == identityMatrix, \"shift by 5 twice should equal identity\")\n  }\n\n  test(\"HilbertUtils.getBits\") {\n    assert(HilbertUtils.getBits(Array(0, 0, 1), 22, 2) == 1)\n    val array = Array[Byte](0, 0, -1, 0)\n    assert(HilbertUtils.getBits(array, 16, 4) == 15)\n    assert(HilbertUtils.getBits(array, 18, 3) == 7)\n    assert(HilbertUtils.getBits(array, 23, 1) == 1)\n    assert(HilbertUtils.getBits(array, 23, 2) == 2)\n    assert(HilbertUtils.getBits(array, 23, 8) == 128)\n    assert(HilbertUtils.getBits(array, 16, 3) == 7)\n    assert(HilbertUtils.getBits(array, 16, 2) == 3)\n    assert(HilbertUtils.getBits(array, 16, 1) == 1)\n    assert(HilbertUtils.getBits(array, 15, 2) == 1)\n    assert(HilbertUtils.getBits(array, 15, 1) == 0)\n    assert(HilbertUtils.getBits(array, 12, 8) == 15)\n    assert(HilbertUtils.getBits(array, 12, 12) == 255)\n    assert(HilbertUtils.getBits(array, 12, 13) == (255 << 1))\n\n    assert(HilbertUtils.getBits(Array(0, 1, 0), 6, 6) == 0)\n    assert(HilbertUtils.getBits(Array(0, 1, 0), 12, 6) == 4)\n    assert(HilbertUtils.getBits(Array(0, 1, 0), 18, 6) == 0)\n  }\n\n  def check(received: Array[Byte], expected: Array[Byte]): Unit = {\n    assert(util.Arrays.equals(expected, received),\n      s\"${expected.toSeq.map(_.toBinaryString.takeRight(8))} \" +\n      s\"${received.toSeq.map(_.toBinaryString.takeRight(8))}\")\n  }\n\n  test(\"HilbertUtils.setBits\") {\n    check(HilbertUtils.setBits(Array(0, 0, 0), 7, 8, 4), Array(1, 0, 0))\n    check(HilbertUtils.setBits(Array(0, 0, 0), 7, 12, 4), Array(1, (1.toByte << 7).toByte, 0))\n    check(HilbertUtils.setBits(Array(8, 0, 5), 7, 12, 4), Array(9, (1.toByte << 7).toByte, 5))\n    check(HilbertUtils.setBits(Array(8, 0, 2), 7, -1, 12),\n      Array(9, -1, ((7.toByte << 5).toByte | 2).toByte))\n    check(HilbertUtils.setBits(Array(8, 14, 2), 15, 1, 1), Array(8, 15, 2))\n  }\n\n  test(\"addOne\") {\n    check(HilbertUtils.addOne(Array(0, 0, 0)), Array(0, 0, 1))\n    check(HilbertUtils.addOne(Array(0, 0, -1)), Array(0, 1, 0))\n    check(HilbertUtils.addOne(Array(0, 0, -2)), Array(0, 0, -1))\n    check(HilbertUtils.addOne(Array(0, -1, -1)), Array(1, 0, 0))\n    check(HilbertUtils.addOne(Array(-1, -1, -1)), Array(0, 0, 0))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/InterleaveBitsBenchmark.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport org.apache.spark.benchmark.{Benchmark, BenchmarkBase}\nimport org.apache.spark.sql.catalyst.{CatalystTypeConverters, InternalRow}\nimport org.apache.spark.sql.catalyst.dsl.expressions._\nimport org.apache.spark.sql.catalyst.expressions.Expression\n\n/**\n * Benchmark to measure performance for interleave bits.\n * To run this benchmark:\n * {{{\n *   build/sbt \"core/test:runMain org.apache.spark.sql.delta.expressions.InterleaveBitsBenchmark\"\n * }}}\n */\nobject InterleaveBitsBenchmark extends BenchmarkBase {\n\n  private val numRows = 1 * 1000 * 1000\n\n  private def seqInt(numColumns: Int): Seq[Array[Int]] = {\n    (1 to numRows).map { l =>\n      val arr = new Array[Int](numColumns)\n      (0 until numColumns).foreach(col => arr(col) = l)\n      arr\n    }\n  }\n\n  private def randomInt(numColumns: Int): Seq[Array[Int]] = {\n    (1 to numRows).map { l =>\n      val arr = new Array[Int](numColumns)\n      (0 until numColumns).foreach(col => arr(col) = scala.util.Random.nextInt())\n      arr\n    }\n  }\n\n  private def createExpression(numColumns: Int): Expression = {\n    val inputs = (0 until numColumns).map { i =>\n      $\"c_$i\".int.at(i)\n    }\n    InterleaveBits(inputs)\n  }\n\n  protected def create_row(values: Any*): InternalRow = {\n    InternalRow.fromSeq(values.map(CatalystTypeConverters.convertToCatalyst))\n  }\n\n  override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {\n    val benchmark =\n      new Benchmark(s\"$numRows rows interleave bits benchmark\", numRows, output = output)\n    benchmark.addCase(\"sequence - 1 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(1)\n      seqInt(1).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\"sequence - 2 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(2)\n      seqInt(2).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\"sequence - 3 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(3)\n      seqInt(3).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\"sequence - 4 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(4)\n      seqInt(4).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\"random - 1 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(1)\n      randomInt(1).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\"random - 2 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(2)\n      randomInt(2).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\"random - 3 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(3)\n      randomInt(3).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n\n    benchmark.addCase(\" random - 4 int columns benchmark\", 3) { _ =>\n      val interleaveBits = createExpression(4)\n      randomInt(4).foreach { input =>\n        interleaveBits.eval(create_row(input: _*))\n      }\n    }\n    benchmark.run()\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/InterleaveBitsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport java.nio.ByteBuffer\n\nimport scala.util.Random\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.analysis.TypeCheckResult.TypeCheckSuccess\nimport org.apache.spark.sql.catalyst.expressions.{Expression, ExpressionEvalHelper, Literal}\nimport org.apache.spark.sql.types.IntegerType\n\n\nclass InterleaveBitsSuite extends SparkFunSuite with ExpressionEvalHelper {\n\n  def intToBinary(x: Int): Array[Byte] = {\n    ByteBuffer.allocate(4).putInt(x).array()\n  }\n\n  def checkInterleaving(input: Seq[Expression], expectedOutput: Any): Unit = {\n    Seq(\"true\", \"false\").foreach { flag =>\n      withSQLConf(DeltaSQLConf.FAST_INTERLEAVE_BITS_ENABLED.key -> flag) {\n        checkEvaluation(InterleaveBits(input), expectedOutput)\n      }\n    }\n  }\n\n  test(\"0 inputs\") {\n    checkInterleaving(Seq.empty[Expression], Array.empty[Byte])\n  }\n\n  test(\"1 input\") {\n    for { i <- 1.to(10) } {\n      val r = Random.nextInt()\n      checkInterleaving(Seq(Literal(r)), intToBinary(r))\n    }\n  }\n\n  test(\"2 inputs\") {\n    checkInterleaving(\n      input = Seq(\n        0x000ff0ff,\n        0xfff00f00\n      ).map(Literal(_)),\n      expectedOutput =\n        Array(0x55, 0x55, 0x55, 0xaa, 0xaa, 0x55, 0xaa, 0xaa)\n          .map(_.toByte))\n  }\n\n  test(\"3 inputs\") {\n    checkInterleaving(\n      input = Seq(\n        0xff00,\n        0x00ff,\n        0x0000\n      ).map(Literal(_)),\n      expectedOutput =\n        Array(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x49, 0x24, 0x49, 0x24, 0x92)\n          .map(_.toByte))\n  }\n\n  test(\"9 inputs\") {\n    val result = Array(\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0x00000000,\n      0xffffff92,\n      0x00000049,\n      0x00000024,\n      0xffffff92,\n      0x00000049,\n      0x00000024,\n      0xffffff92,\n      0x00000049,\n      0x00000024,\n      0x00000049,\n      0x00000024,\n      0xffffff92,\n      0x00000049,\n      0x00000024,\n      0xffffff92,\n      0x00000049,\n      0x00000024,\n      0xffffff92\n    )\n    checkInterleaving(\n      input = Seq(\n        0xff00,\n        0x00ff,\n        0x0000,\n        0xff00,\n        0x00ff,\n        0x0000,\n        0xff00,\n        0x00ff,\n        0x0000\n      ).map(Literal(_)),\n      expectedOutput = result.map(_.toByte)\n    )\n  }\n\n  test(\"nulls\") {\n    val ones = 0xffffffff\n    checkInterleaving(\n      Seq(Literal(ones), Literal.create(null, IntegerType)), Array.fill(8)(0xaa.toByte))\n    checkInterleaving(\n      Seq(Literal.create(null, IntegerType), Literal(ones)), Array.fill(8)(0x55.toByte))\n\n    for { i <- 0.to(6) } {\n      checkInterleaving(\n        Seq.fill(i)(Literal.create(null, IntegerType)), Array.fill(i * 4)(0x00.toByte))\n    }\n  }\n\n  test(\"consistency\") {\n    for { num_inputs <- 1 to 10 } {\n      checkConsistencyBetweenInterpretedAndCodegen(InterleaveBits(_), IntegerType, num_inputs)\n    }\n  }\n\n  test(\"supported types\") {\n    // only int for now\n    InterleaveBits(Seq(Literal(0))).checkInputDataTypes() == TypeCheckSuccess\n    // nothing else\n    InterleaveBits(Seq(Literal(false))).checkInputDataTypes() != TypeCheckSuccess\n    InterleaveBits(Seq(Literal(0.toLong))).checkInputDataTypes() != TypeCheckSuccess\n    InterleaveBits(Seq(Literal(0.toDouble))).checkInputDataTypes() != TypeCheckSuccess\n    InterleaveBits(Seq(Literal(0.toString))).checkInputDataTypes() != TypeCheckSuccess\n  }\n\n  test(\"randomization interleave bits\") {\n    val numIters = sys.env\n      .get(\"NUMBER_OF_ITERATIONS_TO_INTERLEAVE_BITS\")\n      .map(_.toInt)\n      .getOrElse(1000000)\n    var i = 0\n    while (i < numIters) {\n      // generate n columns where 1 <= n <= 8\n      val numCols = Random.nextInt(8) + 1\n      val input = new Array[Int](numCols)\n      var j = 0\n      while (j < numCols) {\n        input(j) = Random.nextInt()\n        j += 1\n      }\n      val r1 = InterleaveBits.interleaveBits(input, true)\n      val r2 = InterleaveBits.interleaveBits(input, false)\n      assert(java.util.Arrays.equals(r1, r2), s\"input: ${input.mkString(\",\")}\")\n      i += 1\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/RangePartitionIdSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions\n\nimport scala.reflect.ClassTag\n\nimport org.apache.spark.{Partitioner, RangePartitioner, SparkFunSuite, SparkThrowable}\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\n\nclass RangePartitionIdSuite\n  extends SparkFunSuite with ExpressionEvalHelper with SharedSparkSession {\n\n  def getPartitioner[T : Ordering : ClassTag](data: Seq[T], partitions: Int): Partitioner = {\n    implicit val ordering = new Ordering[GenericInternalRow] {\n      override def compare(x: GenericInternalRow, y: GenericInternalRow): Int = {\n        def getValue0AsT(row: GenericInternalRow): T = row.values.head.asInstanceOf[T]\n        val orderingT = implicitly[Ordering[T]]\n        orderingT.compare(getValue0AsT(x), getValue0AsT(y))\n      }\n    }\n\n    val rdd =\n      spark.sparkContext.parallelize(data).filter(_ != null)\n        .map(key => (new GenericInternalRow(Array[Any](key)), null))\n\n    new RangePartitioner(partitions, rdd)\n  }\n\n  def testRangePartitionerExpr[T : Ordering : ClassTag](\n    data: Seq[T], partitions: Int, childExpr: Expression, expected: Any): Unit = {\n    val rangePartitioner = getPartitioner(data, partitions)\n    checkEvaluation(PartitionerExpr(childExpr, rangePartitioner), expected)\n  }\n\n  test(\"RangePartitionerExpr: test basic\") {\n    val data = 0.until(12)\n    for { numPartitions <- Seq(2, 3, 4, 6) } {\n      val rangePartitioner = getPartitioner(data, numPartitions)\n      data.foreach { i =>\n        val expected = i / (data.size / numPartitions)\n        checkEvaluation(PartitionerExpr(Literal(i), rangePartitioner), expected)\n      }\n    }\n  }\n\n  test(\"RangePartitionerExpr: null values\") {\n    testRangePartitionerExpr(\n      data = 0.until(10),\n      partitions = 2,\n      childExpr = Literal(null),\n      expected = 0)\n  }\n\n  test(\"RangePartitionerExpr: null data\") {\n    testRangePartitionerExpr(\n      data = 0.until(10).map(_ => null),\n      partitions = 2,\n      childExpr = Literal(\"asd\"),\n      expected = 0)\n  }\n\n  test(\"RangePartitionId: unevaluable\") {\n    intercept[Exception with SparkThrowable] {\n      evaluateWithoutCodegen(RangePartitionId(Literal(2), 10))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/expressions/aggregation/BitmapAggregatorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.expressions.aggregation\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.catalyst.expressions.aggregation.BitmapAggregator\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.InternalRow\nimport org.apache.spark.sql.catalyst.expressions.BoundReference\nimport org.apache.spark.sql.types.LongType\n\nclass BitmapAggregatorSuite extends SparkFunSuite {\n\n  import BitmapAggregatorSuite._\n\n  private val childExpression = BoundReference(0, LongType, nullable = true)\n\n  /** Creates a bitmap aggregate expression, using the child expression defined above. */\n  private def newBitmapAgg(format: RoaringBitmapArrayFormat.Value): BitmapAggregator =\n    new BitmapAggregator(childExpression, format)\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values)\n  test(s\"Bitmap serialization - $serializationFormat\") {\n    val bitmapSet = fillSetWithAggregator(newBitmapAgg(serializationFormat), Array(1L, 2L, 3L, 4L))\n    val serialized = bitmapSet.serializeAsByteArray(serializationFormat)\n    val deserialized = RoaringBitmapArray.readFrom(serialized)\n    assert(bitmapSet === deserialized)\n    assert(bitmapSet.## === deserialized.##)\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values)\n  test(s\"Aggregator serialization - $serializationFormat\") {\n    val aggregator = newBitmapAgg(serializationFormat)\n    val bitmapSet = fillSetWithAggregator(aggregator, Array(1L, 2L, 3L, 4L))\n    val deserialized = aggregator.deserialize(aggregator.serialize(bitmapSet))\n    assert(bitmapSet === deserialized)\n    assert(bitmapSet.## === deserialized.##)\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values)\n  test(s\"Bitmap Aggregator merge no duplicates - $serializationFormat\") {\n    val (dataset1, dataset2) = createDatasetsNoDuplicates\n\n    val finalResult =\n      fillSetWithAggregatorAndMerge(\n        newBitmapAgg(serializationFormat),\n        dataset1,\n        dataset2)\n\n    verifyContainsAll(finalResult, dataset1)\n    verifyContainsAll(finalResult, dataset2)\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values)\n  test(s\"Bitmap Aggregator with duplicates - $serializationFormat\") {\n    val (dataset1, dataset2) = createDatasetsWithDuplicates\n\n    val finalResult =\n      fillSetWithAggregatorAndMerge(\n        newBitmapAgg(serializationFormat),\n        dataset1,\n        dataset2)\n\n    verifyContainsAll(finalResult, dataset1)\n    verifyContainsAll(finalResult, dataset2)\n  }\n\n  private lazy val createDatasetsNoDuplicates: (List[Long], List[Long]) = {\n    val primeSet = primes(DATASET_SIZE).toSet\n    val notPrime = (0 until DATASET_SIZE).filterNot(primeSet.contains).toList\n    (primeSet.map(_.toLong).toList, notPrime.map(_.toLong))\n  }\n\n  private def createDatasetsWithDuplicates: (List[Long], List[Long]) = {\n    var (primes, notPrimes) = createDatasetsNoDuplicates\n    // duplicate all powers of 3 (powers of 2 might align with container boundaries)\n    notPrimes ::= 3L\n    var value = 3L\n    while (value < DATASET_SIZE.toLong) {\n      value *= 3L\n      primes ::= value\n    }\n    (primes, notPrimes)\n  }\n\n  // List the first primes smaller than `end`\n  private def primes(end: Int): List[Int] = {\n    // scalastyle:off\n    // Basically https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes#Algorithm_and_variants\n    // but concretely the implementation is adapted from:\n    // https://medium.com/coding-with-clarity/functional-vs-iterative-prime-numbers-in-scala-7e22447146f0\n    // scalastyle:on\n    val primeIndices = mutable.ArrayBuffer.fill((end + 1) / 2)(true)\n\n    val intSqrt = Math.sqrt(end).toInt\n    for {\n      i <- 3 to end by 2 if i <= intSqrt\n      nonPrime <- i * i to end by 2 * i\n    } primeIndices.update(nonPrime / 2, false)\n\n\n    (for (i <- primeIndices.indices if primeIndices(i)) yield 2 * i + 1).tail.toList\n  }\n\n  private def fillSetWithAggregatorAndMerge(\n    aggregator: BitmapAggregator,\n    dataset1: Seq[Long],\n    dataset2: Seq[Long]): RoaringBitmapArray = {\n    val buffer1 = fillSetWithAggregator(aggregator, dataset1)\n    val buffer2 = fillSetWithAggregator(aggregator, dataset2)\n    val merged = aggregator.merge(buffer1, buffer2)\n    val fieldIndex = aggregator.dataType.fieldIndex(\"bitmap\")\n    val result = aggregator.eval(merged).getBinary(fieldIndex)\n    RoaringBitmapArray.readFrom(result)\n  }\n\n  private def fillSetWithAggregator(\n    aggregator: BitmapAggregator,\n    dataset: Seq[Long]): RoaringBitmapArray = {\n    val buffer = aggregator.createAggregationBuffer()\n    for (entry <- dataset) {\n      val row = InternalRow(entry)\n      aggregator.update(buffer, row)\n    }\n    buffer\n  }\n\n  private def verifyContainsAll(\n    aggregator: RoaringBitmapArray,\n    dataset: Seq[Long]): Unit = {\n    for (entry <- dataset) {\n      assert(aggregator.contains(entry),\n        s\"Aggregator did not contain file $entry\")\n    }\n  }\n}\n\nobject BitmapAggregatorSuite {\n  // Pick something over 64k to make sure we fill a few different bitmap containers\n  val DATASET_SIZE: Int = 100000\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/files/TransactionalWriteSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.files\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.functions.column\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{StringType, StructType}\n\nclass TransactionalWriteSuite extends QueryTest with SharedSparkSession with DeltaSQLCommandTest {\n\n  test(\"writing out an empty dataframe produces no AddFiles\") {\n    withTempDir { dir =>\n      spark.range(100).write.format(\"delta\").save(dir.getCanonicalPath)\n\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      val schema = new StructType().add(\"id\", StringType)\n      val emptyDf = spark.createDataFrame(spark.sparkContext.emptyRDD[Row], schema)\n      assert(log.startTransaction().writeFiles(emptyDf).isEmpty)\n    }\n  }\n\n  test(\"write data files to the data subdir\") {\n    withSQLConf(DeltaSQLConf.WRITE_DATA_FILES_TO_SUBDIR.key -> \"true\") {\n      def validateDataSubdir(tablePath: String): Unit = {\n        val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, tablePath)\n        snapshot.allFiles.collect().foreach { f =>\n          assert(f.path.startsWith(\"data/\"))\n        }\n      }\n\n      withTempDir { dir =>\n        spark.range(100).toDF(\"id\").write.format(\"delta\").save(dir.getCanonicalPath)\n        validateDataSubdir(dir.getCanonicalPath)\n      }\n\n      withTempDir { dir =>\n        spark.range(100).toDF(\"id\").withColumn(\"id1\", column(\"id\")).write.format(\"delta\")\n          .partitionBy(\"id\").save(dir.getCanonicalPath)\n        validateDataSubdir(dir.getCanonicalPath)\n      }\n    }\n\n    withSQLConf(DeltaSQLConf.WRITE_DATA_FILES_TO_SUBDIR.key -> \"false\") {\n      withTempDir { dir =>\n        spark.range(100).toDF(\"id\").write.format(\"delta\").save(dir.getCanonicalPath)\n        val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, dir.getCanonicalPath)\n        snapshot.allFiles.collect().foreach { f =>\n          assert(!f.path.startsWith(\"data/\"))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/fuzzer/AtomicBarrierSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.fuzzer\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.concurrency.PhaseLockingTestMixin\n\nimport org.apache.spark.SparkFunSuite\n\nclass AtomicBarrierSuite extends SparkFunSuite\n  with PhaseLockingTestMixin {\n\n  val timeout: FiniteDuration = 5000.millis\n\n  test(\"Atomic Barrier - wait before unblock\") {\n    val barrier = new AtomicBarrier\n    assert(AtomicBarrier.State.Blocked === barrier.load())\n    val thread = new Thread(() => {\n      barrier.waitToPass()\n    })\n    assert(AtomicBarrier.State.Blocked === barrier.load())\n    thread.start()\n    busyWaitForState(barrier, AtomicBarrier.State.Requested, timeout)\n    assert(thread.isAlive) // should be stuck waiting for unblock\n    barrier.unblock()\n    busyWaitForState(barrier, AtomicBarrier.State.Passed, timeout)\n    thread.join(timeout.toMillis) // shouldn't take long\n    assert(!thread.isAlive) // should have passed the barrier and completed\n  }\n\n  test(\"Atomic Barrier - unblock before wait\") {\n    val barrier = new AtomicBarrier\n    assert(AtomicBarrier.State.Blocked === barrier.load())\n    val thread = new Thread(() => {\n      barrier.waitToPass()\n    })\n    assert(AtomicBarrier.State.Blocked === barrier.load())\n    barrier.unblock()\n    assert(AtomicBarrier.State.Unblocked === barrier.load())\n    thread.start()\n    busyWaitForState(barrier, AtomicBarrier.State.Passed, timeout)\n    thread.join(timeout.toMillis) // shouldn't take long\n    assert(!thread.isAlive) // should have passed the barrier and completed\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesDeleteBaseTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass DeleteBaseSQLNameBasedSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass DeleteBaseSQLPathBasedCDCOnSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with DeleteCDCMixin\n\nclass DeleteBaseSQLPathBasedColMapIdModeSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n\nclass DeleteBaseSQLPathBasedColMapNameModeSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with DeleteSQLNameColumnMappingMixin\n\nclass DeleteBaseSQLPathBasedDVPredPushOffSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeleteSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass DeleteBaseSQLPathBasedDVPredPushOnSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeleteSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n\nclass DeleteBaseSQLPathBasedSuite\n  extends DeleteBaseTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass DeleteBaseScalaSuite extends DeleteBaseTests with DeleteScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesDeleteCDCTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass DeleteCDCSQLPathBasedCDCOnSuite\n  extends DeleteCDCTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with DeleteCDCMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesDeleteSQLTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass DeleteSQLSQLNameBasedSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass DeleteSQLSQLPathBasedCDCOnSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with DeleteCDCMixin\n\nclass DeleteSQLSQLPathBasedColMapIdModeSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n\nclass DeleteSQLSQLPathBasedColMapNameModeSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with DeleteSQLNameColumnMappingMixin\n\nclass DeleteSQLSQLPathBasedDVPredPushOffSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeleteSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass DeleteSQLSQLPathBasedDVPredPushOnSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeleteSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n\nclass DeleteSQLSQLPathBasedSuite\n  extends DeleteSQLTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesDeleteScalaTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass DeleteScalaScalaSuite extends DeleteScalaTests with DeleteScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesDeleteTempViewTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass DeleteTempViewSQLNameBasedSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass DeleteTempViewSQLPathBasedCDCOnSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with DeleteCDCMixin\n\nclass DeleteTempViewSQLPathBasedColMapIdModeSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n\nclass DeleteTempViewSQLPathBasedColMapNameModeSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with DeleteSQLNameColumnMappingMixin\n\nclass DeleteTempViewSQLPathBasedDVPredPushOffSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeleteSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass DeleteTempViewSQLPathBasedDVPredPushOnSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeleteSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n\nclass DeleteTempViewSQLPathBasedSuite\n  extends DeleteTempViewTests\n  with DeleteSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesRowTrackingDeleteDvBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass RowTrackingDeleteDvBaseCDCOnDVOnColMapIdModeSuite\n  extends RowTrackingDeleteDvBase\n  with CDCEnabled\n  with PersistentDVEnabled\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingDeleteDvBaseCDCOnDVOnColMapNameModeSuite\n  extends RowTrackingDeleteDvBase\n  with CDCEnabled\n  with PersistentDVEnabled\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingDeleteDvBaseCDCOnDVOnSuite\n  extends RowTrackingDeleteDvBase\n  with CDCEnabled\n  with PersistentDVEnabled\n\nclass RowTrackingDeleteDvBaseDVOnSuite extends RowTrackingDeleteDvBase with PersistentDVEnabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/DeleteSuitesRowTrackingDeleteSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass RowTrackingDeleteSuiteBaseCDCOnDVOffSuite\n  extends RowTrackingDeleteSuiteBase\n  with CDCEnabled\n  with PersistentDVDisabled\n\nclass RowTrackingDeleteSuiteBaseCDCOnDVOnColMapIdModeSuite\n  extends RowTrackingDeleteSuiteBase\n  with CDCEnabled\n  with PersistentDVEnabled\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingDeleteSuiteBaseCDCOnDVOnColMapNameModeSuite\n  extends RowTrackingDeleteSuiteBase\n  with CDCEnabled\n  with PersistentDVEnabled\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingDeleteSuiteBaseCDCOnDVOnSuite\n  extends RowTrackingDeleteSuiteBase\n  with CDCEnabled\n  with PersistentDVEnabled\n\nclass RowTrackingDeleteSuiteBaseDVOffColMapIdModeSuite\n  extends RowTrackingDeleteSuiteBase\n  with PersistentDVDisabled\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingDeleteSuiteBaseDVOffColMapNameModeSuite\n  extends RowTrackingDeleteSuiteBase\n  with PersistentDVDisabled\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingDeleteSuiteBaseDVOffSuite\n  extends RowTrackingDeleteSuiteBase\n  with PersistentDVDisabled\n\nclass RowTrackingDeleteSuiteBaseDVOnSuite\n  extends RowTrackingDeleteSuiteBase\n  with PersistentDVEnabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/InsertSuitesDeltaInsertIntoImplicitCastStreamingWriteTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\n\nclass DeltaInsertIntoImplicitCastStreamingWriteSuite\n  extends DeltaInsertIntoImplicitCastStreamingWriteTests\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/InsertSuitesDeltaInsertIntoImplicitCastTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nclass DeltaInsertIntoImplicitCastSuite extends DeltaInsertIntoImplicitCastTests\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeCDCTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeCDCSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeCDCTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeCDCSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeCDCTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeCDCSQLPathBasedCDCOnSuite\n  extends MergeCDCTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoAnalysisExceptionTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoAnalysisExceptionSQLNameBasedSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoAnalysisExceptionSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoAnalysisExceptionSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoAnalysisExceptionSQLPathBasedCDCOnSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoAnalysisExceptionSQLPathBasedColMapIdModeSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoAnalysisExceptionSQLPathBasedColMapNameModeSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoAnalysisExceptionSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoAnalysisExceptionSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoAnalysisExceptionSQLPathBasedSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoAnalysisExceptionScalaSuite\n  extends MergeIntoAnalysisExceptionTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoBasicTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoBasicSQLNameBasedSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoBasicSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoBasicSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoBasicSQLPathBasedCDCOnSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoBasicSQLPathBasedColMapIdModeSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoBasicSQLPathBasedColMapNameModeSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoBasicSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoBasicSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoBasicSQLPathBasedSuite\n  extends MergeIntoBasicTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoBasicScalaSuite extends MergeIntoBasicTests with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoDVsTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoDVsSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoDVsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoDVsSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoDVsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoDVsSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoDVsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoDVsSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoDVsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoExtendedSyntaxTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoExtendedSyntaxSQLNameBasedSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoExtendedSyntaxSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoExtendedSyntaxSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoExtendedSyntaxSQLPathBasedCDCOnSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoExtendedSyntaxSQLPathBasedColMapIdModeSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoExtendedSyntaxSQLPathBasedColMapNameModeSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoExtendedSyntaxSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoExtendedSyntaxSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoExtendedSyntaxSQLPathBasedSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoExtendedSyntaxScalaSuite\n  extends MergeIntoExtendedSyntaxTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoMaterializeSourceErrorTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoMaterializeSourceErrorMergePersistentDVOffSuite\n  extends MergeIntoMaterializeSourceErrorTests\n  with MergePersistentDVDisabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoMaterializeSourceTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoMaterializeSourceMergePersistentDVOffSuite\n  extends MergeIntoMaterializeSourceTests\n  with MergePersistentDVDisabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedArrayStructEvolutionNullnessTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedArrayStructEvolutionNullnessSQLNameBasedSuite\n  extends MergeIntoNestedArrayStructEvolutionNullnessTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedDataTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedDataSQLNameBasedSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNestedDataSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedDataSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedDataSQLPathBasedCDCOnSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNestedDataSQLPathBasedColMapIdModeSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedDataSQLPathBasedColMapNameModeSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedDataSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNestedDataSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNestedDataSQLPathBasedSuite\n  extends MergeIntoNestedDataTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNestedDataScalaSuite extends MergeIntoNestedDataTests with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedMapStructEvolutionNullnessTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedMapStructEvolutionNullnessSQLNameBasedSuite\n  extends MergeIntoNestedMapStructEvolutionNullnessTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedStructEvolutionInsertTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedStructEvolutionInsertSQLNameBasedSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedCDCOnSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedColMapIdModeSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedColMapNameModeSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNestedStructEvolutionInsertSQLPathBasedSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNestedStructEvolutionInsertScalaSuite\n  extends MergeIntoNestedStructEvolutionInsertTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedStructEvolutionNullnessTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedStructEvolutionNullnessSQLNameBasedSuite\n  extends MergeIntoNestedStructEvolutionNullnessTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedStructEvolutionUpdateOnlyTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLNameBasedSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedCDCOnSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedColMapIdModeSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedColMapNameModeSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNestedStructEvolutionUpdateOnlySQLPathBasedSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNestedStructEvolutionUpdateOnlyScalaSuite\n  extends MergeIntoNestedStructEvolutionUpdateOnlyTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNestedStructInMapEvolutionTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNestedStructInMapEvolutionSQLNameBasedSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedCDCOnSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedColMapIdModeSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedColMapNameModeSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNestedStructInMapEvolutionSQLPathBasedSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNestedStructInMapEvolutionScalaSuite\n  extends MergeIntoNestedStructInMapEvolutionTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNotMatchedBySourceCDCPart1Tests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLNameBasedSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedCDCOnSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedColMapIdModeSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedColMapNameModeSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNotMatchedBySourceCDCPart1SQLPathBasedSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNotMatchedBySourceCDCPart1ScalaSuite\n  extends MergeIntoNotMatchedBySourceCDCPart1Tests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNotMatchedBySourceCDCPart2Tests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLNameBasedSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedCDCOnSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedColMapIdModeSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedColMapNameModeSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNotMatchedBySourceCDCPart2SQLPathBasedSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNotMatchedBySourceCDCPart2ScalaSuite\n  extends MergeIntoNotMatchedBySourceCDCPart2Tests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoNotMatchedBySourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoNotMatchedBySourceSQLNameBasedSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedCDCOnSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedColMapIdModeSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedColMapNameModeSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoNotMatchedBySourceSQLPathBasedSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoNotMatchedBySourceScalaSuite\n  extends MergeIntoNotMatchedBySourceSuite\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSQLNondeterministicOrderTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSQLNondeterministicOrderSQLNameBasedSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedCDCOnSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSQLNondeterministicOrderSQLPathBasedSuite\n  extends MergeIntoSQLNondeterministicOrderTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSQLTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSQLSQLNameBasedSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSQLSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSQLSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSQLSQLPathBasedCDCOnSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSQLSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSQLSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSQLSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSQLSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSQLSQLPathBasedSuite\n  extends MergeIntoSQLTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoScalaTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoScalaScalaSuite extends MergeIntoScalaTests with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSchemaEvoStoreAssignmentPolicyTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLNameBasedSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedCDCOnSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedColMapIdModeSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedColMapNameModeSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicySQLPathBasedSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoSchemaEvoStoreAssignmentPolicyScalaSuite\n  extends MergeIntoSchemaEvoStoreAssignmentPolicyTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSchemaEvolutionBaseExistingColumnTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLNameBasedSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedCDCOnSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnSQLPathBasedSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoSchemaEvolutionBaseExistingColumnScalaSuite\n  extends MergeIntoSchemaEvolutionBaseExistingColumnTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSchemaEvolutionBaseNewColumnTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLNameBasedSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedCDCOnSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSchemaEvolutionBaseNewColumnSQLPathBasedSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoSchemaEvolutionBaseNewColumnScalaSuite\n  extends MergeIntoSchemaEvolutionBaseNewColumnTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSchemaEvolutionCoreTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSchemaEvolutionCoreSQLNameBasedSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedCDCOnSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSchemaEvolutionCoreSQLPathBasedSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoSchemaEvolutionCoreScalaSuite\n  extends MergeIntoSchemaEvolutionCoreTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSchemaEvolutionNotMatchedBySourceTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLNameBasedSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedCDCOnSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceSQLPathBasedSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoSchemaEvolutionNotMatchedBySourceScalaSuite\n  extends MergeIntoSchemaEvolutionNotMatchedBySourceTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoStructEvolutionNullnessMultiClauseTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoStructEvolutionNullnessMultiClauseSQLNameBasedSuite\n  extends MergeIntoStructEvolutionNullnessMultiClauseTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoSuiteBaseMiscTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoSuiteBaseMiscSQLNameBasedSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedCDCOnSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedColMapIdModeSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedColMapNameModeSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoSuiteBaseMiscSQLPathBasedSuite\n  extends MergeIntoSuiteBaseMiscTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoSuiteBaseMiscScalaSuite extends MergeIntoSuiteBaseMiscTests with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoTempViewsTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoTempViewsSQLNameBasedSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoTempViewsSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoTempViewsSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoTempViewsSQLPathBasedCDCOnSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoTempViewsSQLPathBasedColMapIdModeSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoTempViewsSQLPathBasedColMapNameModeSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoTempViewsSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoTempViewsSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoTempViewsSQLPathBasedSuite\n  extends MergeIntoTempViewsTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoTopLevelArrayStructEvolutionNullnessTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoTopLevelArrayStructEvolutionNullnessSQLNameBasedSuite\n  extends MergeIntoTopLevelArrayStructEvolutionNullnessTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoTopLevelMapStructEvolutionNullnessTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoTopLevelMapStructEvolutionNullnessSQLNameBasedSuite\n  extends MergeIntoTopLevelMapStructEvolutionNullnessTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoTopLevelStructEvolutionNullnessTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoTopLevelStructEvolutionNullnessSQLNameBasedSuite\n  extends MergeIntoTopLevelStructEvolutionNullnessTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesMergeIntoUnlimitedMergeClausesTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass MergeIntoUnlimitedMergeClausesSQLNameBasedSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedCDCOnDVsPredPushOffSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedCDCOnDVsPredPushOnSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n  with MergeCDCMixin\n  with MergeCDCWithDVsMixin\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedCDCOnSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with MergeCDCMixin\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedColMapIdModeSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableIdMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedColMapNameModeSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with DeltaColumnMappingEnableNameMode\n  with MergeIntoSQLColumnMappingOverrides\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedDVsPredPushOffSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownDisabled\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedDVsPredPushOnSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with MergeIntoDVsMixin\n  with PredicatePushdownEnabled\n\nclass MergeIntoUnlimitedMergeClausesSQLPathBasedSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass MergeIntoUnlimitedMergeClausesScalaSuite\n  extends MergeIntoUnlimitedMergeClausesTests\n  with MergeIntoScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/MergeSuitesRowTrackingMergeCommonTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\n\nclass RowTrackingMergeCommonNameBasedCDCOnDVOffMergePersistentDVOffSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with CDCEnabled\n  with PersistentDVDisabled\n  with MergePersistentDVDisabled\n\nclass RowTrackingMergeCommonNameBasedCDCOnRowTrackingMergeDVSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with CDCEnabled\n  with RowTrackingMergeDVMixin\n\nclass RowTrackingMergeCommonNameBasedCDCOnSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with CDCEnabled\n\nclass RowTrackingMergeCommonNameBasedColMapIdModeCDCOnRowTrackingMergeDVSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with DeltaColumnMappingEnableIdMode\n  with CDCEnabled\n  with RowTrackingMergeDVMixin\n\nclass RowTrackingMergeCommonNameBasedColMapIdModeSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingMergeCommonNameBasedColMapNameModeCDCOnRowTrackingMergeDVSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with DeltaColumnMappingEnableNameMode\n  with CDCEnabled\n  with RowTrackingMergeDVMixin\n\nclass RowTrackingMergeCommonNameBasedColMapNameModeSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingMergeCommonNameBasedDVOffMergePersistentDVOffSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with PersistentDVDisabled\n  with MergePersistentDVDisabled\n\nclass RowTrackingMergeCommonNameBasedRowTrackingMergeDVSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n  with RowTrackingMergeDVMixin\n\nclass RowTrackingMergeCommonNameBasedSuite\n  extends RowTrackingMergeCommonTests\n  with DeltaDMLTestUtilsNameBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesRowTrackingUpdateCommonTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass RowTrackingUpdateCommonCDCOnColMapIdModeSuite\n  extends RowTrackingUpdateCommonTests\n  with CDCEnabled\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingUpdateCommonCDCOnColMapNameModeSuite\n  extends RowTrackingUpdateCommonTests\n  with CDCEnabled\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingUpdateCommonCDCOnSuite extends RowTrackingUpdateCommonTests with CDCEnabled\n\nclass RowTrackingUpdateCommonColMapIdModeSuite\n  extends RowTrackingUpdateCommonTests\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingUpdateCommonColMapNameModeSuite\n  extends RowTrackingUpdateCommonTests\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingUpdateCommonRowTrackingUpdateDVCDCOnColMapIdModeSuite\n  extends RowTrackingUpdateCommonTests\n  with RowTrackingUpdateDVMixin\n  with CDCEnabled\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingUpdateCommonRowTrackingUpdateDVCDCOnColMapNameModeSuite\n  extends RowTrackingUpdateCommonTests\n  with RowTrackingUpdateDVMixin\n  with CDCEnabled\n  with DeltaColumnMappingEnableNameMode\n\nclass RowTrackingUpdateCommonRowTrackingUpdateDVCDCOnSuite\n  extends RowTrackingUpdateCommonTests\n  with RowTrackingUpdateDVMixin\n  with CDCEnabled\n\nclass RowTrackingUpdateCommonRowTrackingUpdateDVSuite\n  extends RowTrackingUpdateCommonTests\n  with RowTrackingUpdateDVMixin\n\nclass RowTrackingUpdateCommonSuite extends RowTrackingUpdateCommonTests\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateBaseMiscTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateBaseMiscSQLNameBasedSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass UpdateBaseMiscSQLPathBasedCDCOnDVSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with UpdateSQLWithDeletionVectorsMixin\n\nclass UpdateBaseMiscSQLPathBasedCDCOnRowTrackingOffSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingDisabled\n\nclass UpdateBaseMiscSQLPathBasedCDCOnRowTrackingOnSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateBaseMiscSQLPathBasedCDCOnSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n\nclass UpdateBaseMiscSQLPathBasedDVPredPushOffSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass UpdateBaseMiscSQLPathBasedDVPredPushOnSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n\nclass UpdateBaseMiscSQLPathBasedRowTrackingOffSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with RowTrackingDisabled\n\nclass UpdateBaseMiscSQLPathBasedRowTrackingOnSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateBaseMiscSQLPathBasedSuite\n  extends UpdateBaseMiscTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n\nclass UpdateBaseMiscScalaSuite extends UpdateBaseMiscTests with UpdateScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateBaseTempViewTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateBaseTempViewSQLNameBasedSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass UpdateBaseTempViewSQLPathBasedCDCOnDVSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with UpdateSQLWithDeletionVectorsMixin\n\nclass UpdateBaseTempViewSQLPathBasedCDCOnRowTrackingOffSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingDisabled\n\nclass UpdateBaseTempViewSQLPathBasedCDCOnRowTrackingOnSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateBaseTempViewSQLPathBasedCDCOnSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n\nclass UpdateBaseTempViewSQLPathBasedDVPredPushOffSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass UpdateBaseTempViewSQLPathBasedDVPredPushOnSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n\nclass UpdateBaseTempViewSQLPathBasedRowTrackingOffSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with RowTrackingDisabled\n\nclass UpdateBaseTempViewSQLPathBasedRowTrackingOnSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateBaseTempViewSQLPathBasedSuite\n  extends UpdateBaseTempViewTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateCDCTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateCDCSQLPathBasedCDCOnDVSuite\n  extends UpdateCDCTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with UpdateSQLWithDeletionVectorsMixin\n\nclass UpdateCDCSQLPathBasedCDCOnRowTrackingOffSuite\n  extends UpdateCDCTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingDisabled\n\nclass UpdateCDCSQLPathBasedCDCOnRowTrackingOnSuite\n  extends UpdateCDCTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateCDCSQLPathBasedCDCOnSuite\n  extends UpdateCDCTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateCDCWithDeletionVectorsTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateCDCWithDeletionVectorsSQLPathBasedCDCOnDVSuite\n  extends UpdateCDCWithDeletionVectorsTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with UpdateSQLWithDeletionVectorsMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateSQLTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateSQLSQLNameBasedSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsNameBased\n\nclass UpdateSQLSQLPathBasedCDCOnDVSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with UpdateSQLWithDeletionVectorsMixin\n\nclass UpdateSQLSQLPathBasedCDCOnRowTrackingOffSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingDisabled\n\nclass UpdateSQLSQLPathBasedCDCOnRowTrackingOnSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateSQLSQLPathBasedCDCOnSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n\nclass UpdateSQLSQLPathBasedDVPredPushOffSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass UpdateSQLSQLPathBasedDVPredPushOnSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n\nclass UpdateSQLSQLPathBasedRowTrackingOffSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with RowTrackingDisabled\n\nclass UpdateSQLSQLPathBasedRowTrackingOnSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with RowTrackingEnabled\n  with UpdateWithRowTrackingOverrides\n\nclass UpdateSQLSQLPathBasedSuite\n  extends UpdateSQLTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateSQLWithDeletionVectorsTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateSQLWithDeletionVectorsSQLPathBasedCDCOnDVSuite\n  extends UpdateSQLWithDeletionVectorsTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with CDCEnabled\n  with UpdateSQLWithDeletionVectorsMixin\n\nclass UpdateSQLWithDeletionVectorsSQLPathBasedDVPredPushOffSuite\n  extends UpdateSQLWithDeletionVectorsTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownDisabled\n\nclass UpdateSQLWithDeletionVectorsSQLPathBasedDVPredPushOnSuite\n  extends UpdateSQLWithDeletionVectorsTests\n  with UpdateSQLMixin\n  with DeltaDMLTestUtilsPathBased\n  with UpdateSQLWithDeletionVectorsMixin\n  with PredicatePushdownEnabled\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/generatedsuites/UpdateSuitesUpdateScalaTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// ***********************************************************************************\n// * This file is automatically generated. Manual modification is not allowed.       *\n// * There is a unit test that should prevent merging a manual change.               *\n// *                                                                                 *\n// * To make changes to the suites, modify the generator script config at            *\n// * SuiteGeneratorConfig.scala and run it. The generator can be run via the         *\n// * sbt command deltaSuiteGenerator / run.                                          *\n// *                                                                                 *\n// * DO NOT TOUCH ANYTHING IN THIS FILE!                                             *\n// ***********************************************************************************\n\n// scalastyle:off line.size.limit\npackage org.apache.spark.sql.delta.generatedsuites\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.cdc._\nimport org.apache.spark.sql.delta.rowid._\nimport org.apache.spark.sql.delta.rowtracking._\n\nclass UpdateScalaScalaSuite extends UpdateScalaTests with UpdateScalaMixin\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/logging/DeltaStructuredLoggingSuite.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.logging\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.Files\nimport java.util.regex.Pattern\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport org.apache.logging.log4j.Level\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.internal.{LogEntry, Logging, LogKey, MDC}\n\nclass DeltaStructuredLoggingSuite extends SparkFunSuite with Logging {\n  private def className: String = classOf[DeltaStructuredLoggingSuite].getSimpleName\n  private def logFilePath: String = \"target/structured.log\"\n\n  private lazy val logFile: File = {\n    val pwd = new File(\".\").getCanonicalPath\n    new File(pwd + \"/\" + logFilePath)\n  }\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    Logging.enableStructuredLogging()\n  }\n\n  override def afterAll(): Unit = {\n    Logging.disableStructuredLogging()\n    super.afterAll()\n  }\n\n  private val jsonMapper = new ObjectMapper().registerModule(DefaultScalaModule)\n  private def compactAndToRegexPattern(json: String): String = {\n    jsonMapper.readTree(json).toString.\n      replace(\"<timestamp>\", \"\"\"[^\"]+\"\"\").\n      replace(\"\"\"\"<stacktrace>\"\"\"\", \"\"\".*\"\"\").\n      replace(\"{\", \"\"\"\\{\"\"\") + \"\\n\"\n  }\n\n  // Return the newly added log contents in the log file after executing the function `f`\n  private def captureLogOutput(f: () => Unit): String = {\n    val content = if (logFile.exists()) {\n      new String(Files.readAllBytes(logFile.toPath), StandardCharsets.UTF_8)\n    } else {\n      \"\"\n    }\n    f()\n    val newContent =\n      new String(Files.readAllBytes(logFile.toPath), StandardCharsets.UTF_8)\n    newContent.substring(content.length)\n  }\n\n  private def basicMsg: String = \"This is a log message\"\n\n  private def msgWithMDC: LogEntry = log\"Lost executor ${MDC(DeltaLogKeys.EXECUTOR_ID, \"1\")}.\"\n\n  private def msgWithMDCValueIsNull: LogEntry =\n    log\"Lost executor ${MDC(DeltaLogKeys.EXECUTOR_ID, null)}.\"\n\n  private def msgWithMDCAndException: LogEntry =\n    log\"Error in executor ${MDC(DeltaLogKeys.EXECUTOR_ID, \"1\")}.\"\n\n  private def msgWithConcat: LogEntry = log\"Min Size: ${MDC(DeltaLogKeys.MIN_SIZE, \"2\")}, \" +\n    log\"Max Size: ${MDC(DeltaLogKeys.MAX_SIZE, \"4\")}. \" +\n    log\"Please double check.\"\n\n  private val customLog = log\"${MDC(CustomLogKeys.CUSTOM_LOG_KEY, \"Custom log message.\")}\"\n\n  def expectedPatternForBasicMsg(level: Level): String = {\n    compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"This is a log message\",\n          \"logger\": \"$className\"\n        }\"\"\")\n  }\n\n  def expectedPatternForBasicMsgWithException(level: Level): String = {\n    compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"This is a log message\",\n          \"exception\": {\n            \"class\": \"java.lang.RuntimeException\",\n            \"msg\": \"OOM\",\n            \"stacktrace\": \"<stacktrace>\"\n          },\n          \"logger\": \"$className\"\n        }\"\"\")\n  }\n\n  def expectedPatternForMsgWithMDC(level: Level): String = {\n    compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"Lost executor 1.\",\n          \"context\": {\n             \"executor_id\": \"1\"\n          },\n          \"logger\": \"$className\"\n        }\"\"\")\n  }\n\n  def expectedPatternForMsgWithMDCValueIsNull(level: Level): String = {\n    compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"Lost executor null.\",\n          \"context\": {\n             \"executor_id\": null\n          },\n          \"logger\": \"$className\"\n        }\"\"\")\n  }\n\n  def expectedPatternForMsgWithMDCAndException(level: Level): String = {\n    compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"Error in executor 1.\",\n          \"context\": {\n            \"executor_id\": \"1\"\n          },\n          \"exception\": {\n            \"class\": \"java.lang.RuntimeException\",\n            \"msg\": \"OOM\",\n            \"stacktrace\": \"<stacktrace>\"\n          },\n          \"logger\": \"$className\"\n        }\"\"\")\n  }\n\n  def expectedPatternForCustomLogKey(level: Level): String = {\n    compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"Custom log message.\",\n          \"logger\": \"$className\"\n        }\"\"\"\n    )\n  }\n\n  def verifyMsgWithConcat(level: Level, logOutput: String): Unit = {\n    val pattern1 = compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"Min Size: 2, Max Size: 4. Please double check.\",\n          \"context\": {\n            \"min_size\": \"2\",\n            \"max_size\": \"4\"\n          },\n          \"logger\": \"$className\"\n        }\"\"\")\n\n    val pattern2 = compactAndToRegexPattern(\n      s\"\"\"\n        {\n          \"ts\": \"<timestamp>\",\n          \"level\": \"$level\",\n          \"msg\": \"Min Size: 2, Max Size: 4. Please double check.\",\n          \"context\": {\n            \"max_size\": \"4\",\n            \"min_size\": \"2\"\n          },\n          \"logger\": \"$className\"\n        }\"\"\")\n    assert(Pattern.compile(pattern1).matcher(logOutput).matches() ||\n      Pattern.compile(pattern2).matcher(logOutput).matches())\n  }\n\n  test(\"Basic logging\") {\n    Seq(\n      (Level.ERROR, () => logError(basicMsg)),\n      (Level.WARN, () => logWarning(basicMsg)),\n      (Level.INFO, () => logInfo(basicMsg))).foreach { case (level, logFunc) =>\n      val logOutput = captureLogOutput(logFunc)\n      assert(Pattern.compile(expectedPatternForBasicMsg(level)).matcher(logOutput).matches())\n    }\n  }\n\n  test(\"Basic logging with Exception\") {\n    val exception = new RuntimeException(\"OOM\")\n    Seq(\n      (Level.ERROR, () => logError(basicMsg, exception)),\n      (Level.WARN, () => logWarning(basicMsg, exception)),\n      (Level.INFO, () => logInfo(basicMsg, exception))).foreach { case (level, logFunc) =>\n      val logOutput = captureLogOutput(logFunc)\n      assert(\n        Pattern.compile(expectedPatternForBasicMsgWithException(level)).matcher(logOutput)\n          .matches())\n    }\n  }\n\n  test(\"Logging with MDC\") {\n    Seq(\n      (Level.ERROR, () => logError(msgWithMDC)),\n      (Level.WARN, () => logWarning(msgWithMDC)),\n      (Level.INFO, () => logInfo(msgWithMDC))).foreach {\n      case (level, logFunc) =>\n        val logOutput = captureLogOutput(logFunc)\n        assert(\n          Pattern.compile(expectedPatternForMsgWithMDC(level)).matcher(logOutput).matches())\n    }\n  }\n\n  test(\"Logging with MDC(the value is null)\") {\n    Seq(\n      (Level.ERROR, () => logError(msgWithMDCValueIsNull)),\n      (Level.WARN, () => logWarning(msgWithMDCValueIsNull)),\n      (Level.INFO, () => logInfo(msgWithMDCValueIsNull))).foreach {\n      case (level, logFunc) =>\n        val logOutput = captureLogOutput(logFunc)\n        assert(\n          Pattern.compile(expectedPatternForMsgWithMDCValueIsNull(level)).matcher(logOutput)\n            .matches())\n    }\n  }\n\n  test(\"Logging with MDC and Exception\") {\n    val exception = new RuntimeException(\"OOM\")\n    Seq(\n      (Level.ERROR, () => logError(msgWithMDCAndException, exception)),\n      (Level.WARN, () => logWarning(msgWithMDCAndException, exception)),\n      (Level.INFO, () => logInfo(msgWithMDCAndException, exception))).foreach {\n      case (level, logFunc) =>\n        val logOutput = captureLogOutput(logFunc)\n        assert(\n          Pattern.compile(expectedPatternForMsgWithMDCAndException(level)).matcher(logOutput)\n            .matches())\n    }\n  }\n\n  test(\"Logging with custom LogKey\") {\n    Seq(\n      (Level.ERROR, () => logError(customLog)),\n      (Level.WARN, () => logWarning(customLog)),\n      (Level.INFO, () => logInfo(customLog))).foreach {\n      case (level, logFunc) =>\n        val logOutput = captureLogOutput(logFunc)\n        assert(Pattern.compile(expectedPatternForCustomLogKey(level)).matcher(logOutput).matches())\n    }\n  }\n\n  test(\"Logging with concat\") {\n    Seq(\n      (Level.ERROR, () => logError(msgWithConcat)),\n      (Level.WARN, () => logWarning(msgWithConcat)),\n      (Level.INFO, () => logInfo(msgWithConcat))).foreach {\n      case (level, logFunc) =>\n        val logOutput = captureLogOutput(logFunc)\n        verifyMsgWithConcat(level, logOutput)\n    }\n  }\n}\n\nobject CustomLogKeys {\n  // Custom `LogKey` must extend LogKey\n  case object CUSTOM_LOG_KEY extends DeltaLogKey\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/logging/LogThrottlingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.logging\n\nimport scala.concurrent.duration._\n\nimport org.apache.spark.sql.delta.metering.{DeadlineWithTimeSource, LogThrottler, NanoTimeTimeSource}\n\nimport org.apache.spark.SparkFunSuite\n\nclass LogThrottlingSuite extends SparkFunSuite {\n\n  // Make sure that the helper works right.\n  test(\"time control\") {\n    val nanoTimeControl = new MockedNanoTime\n    assert(nanoTimeControl.nanoTime() === 0L)\n    assert(nanoTimeControl.nanoTime() === 0L)\n\n    nanoTimeControl.advance(112L.nanos)\n    assert(nanoTimeControl.nanoTime() === 112L)\n    assert(nanoTimeControl.nanoTime() === 112L)\n  }\n\n  test(\"deadline with time control\") {\n    val nanoTimeControl = new MockedNanoTime\n    assert(DeadlineWithTimeSource.now(nanoTimeControl).isOverdue())\n    val deadline = DeadlineWithTimeSource.now(nanoTimeControl) + 5.nanos\n    assert(!deadline.isOverdue())\n    nanoTimeControl.advance(5.nanos)\n    assert(deadline.isOverdue())\n    nanoTimeControl.advance(5.nanos)\n    assert(deadline.isOverdue())\n    // Check addition.\n    assert(deadline + 0.nanos === deadline)\n    val increasedDeadline = deadline + 10.nanos\n    assert(!increasedDeadline.isOverdue())\n    nanoTimeControl.advance(5.nanos)\n    assert(increasedDeadline.isOverdue())\n    // Ensure that wrapping keeps throwing this exact exception, since we rely on it in\n    // LogThrottler.tryRecoverTokens\n    assertThrows[IllegalArgumentException] {\n      deadline + Long.MaxValue.nanos\n    }\n    // Check difference and ordering.\n    assert(deadline - deadline === 0.nanos)\n    assert(increasedDeadline - deadline === 10.nanos)\n    assert(increasedDeadline - deadline > 9.nanos)\n    assert(increasedDeadline - deadline < 11.nanos)\n    assert(deadline - increasedDeadline === -10.nanos)\n    assert(deadline - increasedDeadline < -9.nanos)\n    assert(deadline - increasedDeadline > -11.nanos)\n  }\n\n  test(\"unthrottled, no burst\") {\n    val nanotTimeControl = new MockedNanoTime\n    val throttler = new LogThrottler(\n      bucketSize = 1,\n      tokenRecoveryInterval = 5.nanos,\n      timeSource = nanotTimeControl)\n    val numInvocations = 100\n    var timesExecuted = 0\n    for (i <- 0 until numInvocations) {\n      throttler.throttled { skipped =>\n        assert(skipped === 0L)\n        timesExecuted += 1\n      }\n      nanotTimeControl.advance(5.nanos)\n    }\n    assert(timesExecuted === numInvocations)\n  }\n\n  test(\"unthrottled, burst\") {\n    val nanotTimeControl = new MockedNanoTime\n    val throttler = new LogThrottler(\n      bucketSize = 100,\n      tokenRecoveryInterval = 1000000.nanos, // Just to make it obvious that it's a large number.\n      timeSource = nanotTimeControl)\n    val numInvocations = 100\n    var timesExecuted = 0\n    for (_ <- 0 until numInvocations) {\n      throttler.throttled { skipped =>\n        assert(skipped === 0L)\n        timesExecuted += 1\n      }\n      nanotTimeControl.advance(5.nanos)\n    }\n    assert(timesExecuted === numInvocations)\n  }\n\n  test(\"throttled, no burst\") {\n    val nanoTimeControl = new MockedNanoTime\n    val throttler = new LogThrottler(\n      bucketSize = 1,\n      tokenRecoveryInterval = 5.nanos,\n      timeSource = nanoTimeControl)\n    val numInvocations = 100\n    var timesExecuted = 0\n    for (i <- 0 until numInvocations) {\n      throttler.throttled { skipped =>\n        if (timesExecuted == 0) {\n          assert(skipped === 0L)\n        } else {\n          assert(skipped === 4L)\n        }\n        timesExecuted += 1\n      }\n      nanoTimeControl.advance(1.nanos)\n    }\n    assert(timesExecuted === numInvocations / 5)\n  }\n\n  test(\"throttled, single burst\") {\n    val nanoTimeControl = new MockedNanoTime\n    val throttler = new LogThrottler(\n      bucketSize = 5,\n      tokenRecoveryInterval = 10.nanos,\n      timeSource = nanoTimeControl)\n    val numInvocations = 100\n    var timesExecuted = 0\n    for (i <- 0 until numInvocations) {\n      throttler.throttled { skipped =>\n        if (i < 5) {\n          // First burst\n          assert(skipped === 0L)\n        } else if (i == 10) {\n          // First token recovery\n          assert(skipped === 5L)\n        } else {\n          // All other token recoveries\n          assert(skipped === 9L)\n        }\n        timesExecuted += 1\n      }\n      nanoTimeControl.advance(1.nano)\n    }\n    // A burst of 5 and then 1 every 10ns/invocations.\n    assert(timesExecuted === 5 + (numInvocations - 10) / 10)\n  }\n\n  test(\"throttled, bursty\") {\n    val nanoTimeControl = new MockedNanoTime\n    val throttler = new LogThrottler(\n      bucketSize = 5,\n      tokenRecoveryInterval = 10.nanos,\n      timeSource = nanoTimeControl)\n    val numBursts = 10\n    val numInvocationsPerBurst = 10\n    var timesExecuted = 0\n    for (burst <- 0 until numBursts) {\n      for (i <- 0 until numInvocationsPerBurst) {\n        throttler.throttled { skipped =>\n          if (i == 0 && burst != 0) {\n            // first after recovery\n            assert(skipped === 5L)\n          } else {\n            // either first burst, or post-recovery on every other burst.\n            assert(skipped === 0L)\n          }\n          timesExecuted += 1\n        }\n        nanoTimeControl.advance(1.nano)\n      }\n      nanoTimeControl.advance(100.nanos)\n    }\n    // Bursts of 5.\n    assert(timesExecuted === 5 * numBursts)\n  }\n\n  test(\"wraparound\") {\n    val nanoTimeControl = new MockedNanoTime\n    val throttler = new LogThrottler(\n      bucketSize = 1,\n      tokenRecoveryInterval = 100.nanos,\n      timeSource = nanoTimeControl)\n    def executeThrottled(expectedSkipped: Long = 0L): Boolean = {\n      var executed = false\n      throttler.throttled { skipped =>\n        assert(skipped === expectedSkipped)\n        executed = true\n      }\n      executed\n    }\n    assert(executeThrottled())\n    assert(!executeThrottled())\n\n    // Move to 2 ns before wrapping.\n    nanoTimeControl.advance((Long.MaxValue - 1L).nanos)\n    assert(executeThrottled(expectedSkipped = 1L))\n    assert(!executeThrottled())\n\n    nanoTimeControl.advance(1.nano)\n    assert(!executeThrottled())\n\n    // Wrapping\n    nanoTimeControl.advance(1.nano)\n    assert(!executeThrottled())\n\n    // Recover\n    nanoTimeControl.advance(100.nanos)\n    assert(executeThrottled(expectedSkipped = 3L))\n  }\n}\n\n/**\n * Use a mocked object to replace calls to `System.nanoTime()` with a custom value that can be\n * controlled by calling `advance(nanos)` on an instance of this class.\n */\nclass MockedNanoTime extends NanoTimeTimeSource {\n  private var currentTimeNs: Long = 0L\n\n  override def nanoTime(): Long = currentTimeNs\n\n  def advance(time: FiniteDuration): Unit = {\n    currentTimeNs += time.toNanos\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/metric/IncrementMetricSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.metric\n\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, Column, DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.execution.SparkPlan\nimport org.apache.spark.sql.execution.metric.SQLMetrics\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.BooleanType\n\nabstract class IncrementMetricSuiteBase\n  extends QueryTest with SharedSparkSession with DeltaSQLCommandTest {\n  import testImplicits._\n  import SQLMetrics._\n\n  val ROWS_IN_DF = 1000\n\n  protected override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.range(ROWS_IN_DF).toDF(\"a\")\n      .withColumn(\"gb\", rand(0).multiply(10).cast(\"integer\"))\n      .write\n      .format(\"parquet\")\n      .mode(\"overwrite\")\n      .save(\"test-df\")\n  }\n\n  def testDf: DataFrame = spark.read.format(\"parquet\").load(\"test-df\")\n\n  test(\"Increment the same metric\") {\n    val metric = createMetric(sparkContext, \"metric\")\n    val increment = IncrementMetric(Literal(true), metric)\n    val groupByKey = IncrementMetric(UnresolvedAttribute(\"gb\"), metric)\n    val havingIncrement = IncrementMetric(\n      GreaterThan(UnresolvedAttribute(\"s\"), Literal(10)), metric)\n    val df = testDf\n      .filter(Column(increment))\n      .groupBy(Column(groupByKey).as(\"gby\"))\n      .agg(sum(\"a\").as(\"s\"))\n      .filter(Column(havingIncrement))\n    val numGroups = df.collect().size\n    validatePlan(df.queryExecution.executedPlan)\n\n    assert(metric.value === 2 * ROWS_IN_DF + numGroups)\n  }\n\n  test(\"Increment with filter and conditional\") {\n    val trueBranchCount = createMetric(sparkContext, \"true\")\n    val falseBranchCount = createMetric(sparkContext, \"false\")\n    val incrementTrueBranch = IncrementMetric(Literal(true), trueBranchCount)\n    val incrementFalseBranch = IncrementMetric(Literal(false), falseBranchCount)\n    val incrementMetric = createMetric(sparkContext, \"increment\")\n    val increment = IncrementMetric(Literal(true), incrementMetric)\n    val incrementPreFilterMetric = createMetric(sparkContext, \"incrementPreFilter\")\n    val incrementPreFilter = IncrementMetric(Literal(true), incrementPreFilterMetric)\n    val ifCondition: Expression = ('a < Literal(20)).expr\n    val conditional = If(ifCondition, incrementTrueBranch, incrementFalseBranch)\n    val df = testDf\n      .filter(Column(incrementPreFilter))\n      .filter('a < 25)\n      .filter(Column(increment))\n      .filter(Column(conditional))\n    val numRows = df.collect().size\n    validatePlan(df.queryExecution.executedPlan)\n\n    assert(incrementPreFilterMetric.value === ROWS_IN_DF)\n    assert(trueBranchCount.value === numRows)\n    assert(falseBranchCount.value + numRows === incrementMetric.value)\n  }\n\n  test(\"ConditionalIncrementMetric with mixed conditions in filters\") {\n    val divisibleBy3Metric = createMetric(sparkContext, \"divisibleBy3\")\n    val oneMod7Metric = createMetric(sparkContext, \"divisibleBy7\")\n    val rangeMetric = createMetric(sparkContext, \"inRange\")\n    val largeEvenMetric = createMetric(sparkContext, \"largeEven\")\n\n    // Count rows divisible by 3 (metric condition) while filtering for divisible by 5\n    // (filter inside ConditionalIncrementMetric).\n    val divisibleBy5Filter =\n      ConditionalIncrementMetric(\n        EqualTo(Pmod(UnresolvedAttribute(\"a\"), Literal(5)), Literal(0)),\n        EqualTo(Pmod(UnresolvedAttribute(\"a\"), Literal(3)), Literal(0)),\n        divisibleBy3Metric)\n\n    // Count numbers where a % 7 == 1 while filtering for a > 10\n    // (filter outside ConditionalIncrementMetric).\n    val divisibleBy7Condition = EqualTo(Pmod(UnresolvedAttribute(\"a\"), Literal(7)), Literal(1))\n    val divisibleBy7Increment =\n      ConditionalIncrementMetric(\n        UnresolvedAttribute(\"a\"),\n        EqualTo(Pmod(UnresolvedAttribute(\"a\"), Literal(7)), Literal(1)),\n        oneMod7Metric)\n    val greaterThan10Filter = GreaterThan(divisibleBy7Increment, Literal(10))\n\n    // Count rows in range 30-70 while filtering for < 50.\n    val rangeCondition = And(\n      GreaterThanOrEqual(UnresolvedAttribute(\"a\"), Literal(30)),\n      LessThanOrEqual(UnresolvedAttribute(\"a\"), Literal(70))\n    )\n    val rangeIncrement =\n      ConditionalIncrementMetric(\n        UnresolvedAttribute(\"a\"),\n        rangeCondition,\n        rangeMetric)\n    val lessThan50Filter = LessThan(rangeIncrement, Literal(50))\n\n    // Count even numbers >= 20 while selecting column a.\n    val largeEvenCondition = And(\n      GreaterThanOrEqual(UnresolvedAttribute(\"a\"), Literal(20)),\n      EqualTo(Pmod(UnresolvedAttribute(\"a\"), Literal(2)), Literal(0))\n    )\n    val largeEvenIncrement =\n      ConditionalIncrementMetric(UnresolvedAttribute(\"a\"), largeEvenCondition, largeEvenMetric)\n\n    val df = testDf\n      .filter(Column(divisibleBy5Filter))    // Filter: a % 5 == 0, Metric: counts a % 3 == 0\n      .filter(Column(greaterThan10Filter))   // Filter: a > 10, Metric: counts a % 7 == 1\n      .filter(Column(lessThan50Filter))      // Filter: a < 80, Metric: counts 30 <= a <= 70\n      .select(\n        Column(largeEvenIncrement).as(\"result\"), // Metric inside Project: counts even a >= 20\n        col(\"a\")\n      )\n      .filter(col(\"a\") >= 30)  // Additional filter after select.\n    val results = df.collect()\n    val numRows = results.size\n    validatePlan(df.queryExecution.executedPlan)\n    // divisibleBy3Metric counts values divisible by 3 among ALL rows (0-999)\n    // The ConditionalIncrementMetric outputs (a % 5 == 0) as boolean for filtering,\n    // but internally counts when (a % 3 == 0).\n    // Divisible by 3: 0,3,6,9,12,15,...,999\n    // Count: 1000/3 = 333.33, so 334 values (includes 0)\n    assert(divisibleBy3Metric.value === 334)\n    // oneMod7Metric counts a % 7 == 1 among rows that pass divisible by 5 filter.\n    // Divisible by 5: 0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,...,995 (200 values)\n    // Among these, a % 7 == 1: 15,50,85,...,995\n    // Pattern: starts at 15, then every 35 (LCM of 5 and 7)\n    // Count: (995-15)/35 + 1 = 29 values\n    assert(oneMod7Metric.value === 29)\n\n    // After divisible by 5 and > 10 filters: 15,20,25,30,35,40,45,50,55,60,65,70,75,...\n    // Among these, in range [30,70]: 30,35,40,45,50,55,60,65,70 = 9 values\n    assert(rangeMetric.value === 9)\n\n    // After divisible by 5, > 10, < 50: 15,20,25,30,35,40,45\n    // Among these, even and >= 20: 20,30,40 = 3 values.\n    assert(largeEvenMetric.value === 3)\n\n    // Final result: divisible by 5, > 10, < 50, >= 30\n    // Values: 30,35,40,45 = 4 rows.\n    assert(numRows === 4)\n  }\n\n  test(\"ConditionalIncrementMetric with mixed conditions in projections\") {\n    val trueMetric = createMetric(sparkContext, \"conditionTrue\")\n    val falseMetric = createMetric(sparkContext, \"conditionFalse\")\n    val equalMetric = createMetric(sparkContext, \"conditionEqual\")\n\n    val trueCondition = LessThan(UnresolvedAttribute(\"a\"), Literal(500))\n    val falseCondition = GreaterThan(UnresolvedAttribute(\"a\"), Literal(ROWS_IN_DF))\n    val equalCondition = EqualTo(UnresolvedAttribute(\"a\"), Literal(42))\n\n    val trueIncrement = ConditionalIncrementMetric(UnresolvedAttribute(\"a\"), trueCondition,\n      trueMetric)\n    val falseIncrement = ConditionalIncrementMetric(UnresolvedAttribute(\"a\"), falseCondition,\n      falseMetric)\n    val equalIncrement = ConditionalIncrementMetric(UnresolvedAttribute(\"a\"), equalCondition,\n      equalMetric)\n\n    val df = testDf\n      .select(\n        Column(trueIncrement).as(\"true_result\"),\n        Column(falseIncrement).as(\"false_result\"),\n        Column(equalIncrement).as(\"equal_result\")\n      )\n    val numRows = df.collect().size\n    validatePlan(df.queryExecution.executedPlan)\n\n    assert(trueMetric.value === 500) // a < 500: rows 0-499\n    assert(falseMetric.value === 0)  // a > 1000: no rows\n    assert(equalMetric.value === 1)  // a == 42: exactly 1 row\n    assert(numRows === ROWS_IN_DF)\n  }\n\n  test(\"ConditionalIncrementMetric with nullable condition\") {\n    val metric = createMetric(sparkContext, \"nullable_condition\")\n\n    // Create a DataFrame with nullable boolean values.\n    val df = spark.range(10).selectExpr(\n      \"id\",\n      \"CASE WHEN id % 3 = 0 THEN null WHEN id % 3 = 1 THEN true ELSE false END AS nullable_bool\"\n    )\n\n    // Create condition that can be null.\n    val conditionExpr = UnresolvedAttribute(\"nullable_bool\")\n    val incrementExpr = ConditionalIncrementMetric(\n      UnresolvedAttribute(\"id\"),\n      conditionExpr,\n      metric\n    )\n\n    val resultDf = df.select(Column(incrementExpr).as(\"result\"))\n    val numRows = resultDf.collect().size\n    validatePlan(resultDf.queryExecution.executedPlan)\n\n    // The metric should only count rows where condition is true (not null and not false).\n    // id=1,4,7 have nullable_bool=true (3 rows)\n    // id=0,3,6,9 have nullable_bool=null (4 rows) - should NOT be counted\n    // id=2,5,8 have nullable_bool=false (3 rows) - should NOT be counted\n    assert(metric.value === 3)\n    assert(numRows === 10)\n  }\n\n  test(\"ConditionalIncrementMetric with invalid condition type\") {\n    val metric = createMetric(sparkContext, \"invalidType\")\n    val nonBooleanCondition = UnresolvedAttribute(\"a\")  // Integer type\n    val increment =\n      ConditionalIncrementMetric(UnresolvedAttribute(\"a\"), nonBooleanCondition, metric)\n\n    // This should fail during analysis due to non-boolean condition type.\n    val exception = intercept[AnalysisException] {\n      val df = testDf.select(Column(increment).as(\"result\"))\n      df.collect()\n    }\n    assert(exception.getErrorClass == \"DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE\")\n  }\n\n  for (enabled <- BOOLEAN_DOMAIN) {\n    test(s\"ConditionalIncrementMetric optimization - literal conditions - enabled=$enabled\") {\n      withSQLConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_CONDITIONAL_INCREMENT_METRIC_ENABLED.key -> enabled.toString) {\n        val trueMetric = createMetric(sparkContext, s\"literalTrue$enabled\")\n        val falseMetric = createMetric(sparkContext, s\"literalFalse$enabled\")\n        val nullMetric = createMetric(sparkContext, s\"literalNull$enabled\")\n        val nonConstMetric = createMetric(sparkContext, s\"nonConst$enabled\")\n\n        val childExpr = UnresolvedAttribute(\"a\")\n        val trueCondition = Literal(true, BooleanType)\n        val falseCondition = Literal(false, BooleanType)\n        val nullCondition = Literal(null, BooleanType)\n        val nonConstCondition = GreaterThan(UnresolvedAttribute(\"a\"), Literal(100))\n\n        val trueExpr = ConditionalIncrementMetric(childExpr, trueCondition, trueMetric)\n        val falseExpr = ConditionalIncrementMetric(childExpr, falseCondition, falseMetric)\n        val nullExpr = ConditionalIncrementMetric(childExpr, nullCondition, nullMetric)\n        val nonConstExpr = ConditionalIncrementMetric(childExpr, nonConstCondition, nonConstMetric)\n\n        val df = testDf.select(\n          Column(trueExpr).as(\"true_result\"),\n          Column(falseExpr).as(\"false_result\"),\n          Column(nullExpr).as(\"null_result\"),\n          Column(nonConstExpr).as(\"nonconst_result\")\n        )\n\n        // Check optimized plan transformations.\n        val optimizedPlan = df.queryExecution.optimizedPlan\n        var conditionalCount = 0\n        var nonConditionalCount = 0\n        optimizedPlan.foreach {\n          _.transformExpressions {\n            case e: ConditionalIncrementMetric =>\n              conditionalCount += 1\n              e\n            case e: IncrementMetric =>\n              nonConditionalCount += 1\n              e\n          }\n        }\n\n        if (enabled) {\n          assert(conditionalCount === 1,\n            s\"ConditionalIncrementMetric with non-const cond should remain unchanged.\" +\n              s\"\\n$optimizedPlan\")\n          assert(nonConditionalCount === 1,\n            s\"ConditionalIncrementMetric with cond true should become IncrementMetric\" +\n              s\"\\n$optimizedPlan\")\n          // ConditionalIncrementMetric with condition false or null have gotten removed.\n        } else {\n          assert(conditionalCount === 4,\n            s\"All ConditionalIncrementMetric expressions should remain unchanged when \" +\n              s\"optimization is disabled.\\n$optimizedPlan\")\n          assert(nonConditionalCount === 0,\n            s\"No ConditionalIncrementMetric should be converted to IncrementMetric when \" +\n              s\"optimization is disabled.\\n$optimizedPlan\")\n        }\n\n        // Verify metrics work correctly regardless of optimization state.\n        df.collect()\n\n        validatePlan(df.queryExecution.executedPlan)\n\n        assert(trueMetric.value === ROWS_IN_DF)  // All rows counted (true condition)\n        assert(falseMetric.value === 0)          // No rows counted (false condition)\n        assert(nullMetric.value === 0)           // No rows counted (null condition)\n        assert(nonConstMetric.value === 899)   // Rows where a > 100 (101-999)\n      }\n    }\n  }\n\n  test(\"ConditionalIncrementMetric with all-null condition column\") {\n    val metric = createMetric(sparkContext, \"allNullCondition\")\n\n    // Create a DataFrame where the condition column is always null.\n    val df = spark.range(10).selectExpr(\"id\", \"try_divide(id, 0) < 0 as null_condition\")\n\n    // Use the null condition column (not a literal, but all values are null).\n    val incrementExpr = ConditionalIncrementMetric(\n      UnresolvedAttribute(\"id\"),\n      UnresolvedAttribute(\"null_condition\"),\n      metric)\n\n    val resultDf = df.select(Column(incrementExpr).as(\"result\"))\n\n    // This should NOT be optimized away since it's not a literal condition.\n    val optimizedPlan = resultDf.queryExecution.optimizedPlan\n    var conditionalCount = 0\n    optimizedPlan.foreach {\n      _.transformExpressions {\n        case e: ConditionalIncrementMetric =>\n          conditionalCount += 1\n          e\n      }\n    }\n\n    assert(conditionalCount === 1,\n      s\"Non-literal all-null condition should preserve ConditionalIncrementMetric\\n$optimizedPlan\")\n\n    val numRows = resultDf.collect().size\n    validatePlan(resultDf.queryExecution.executedPlan)\n\n    // The metric should be 0 since all condition values are null.\n    assert(metric.value === 0, \"All-null condition should count 0 rows\")\n    assert(numRows === 10)\n  }\n\n  protected def validatePlan(plan: SparkPlan): Unit = {}\n\n}\n\nclass IncrementMetricSuite extends IncrementMetricSuiteBase {}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/optimize/CompactionTestHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.optimize\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.hooks.AutoCompact\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.SparkSession\n\n/**\n * A trait used by unit tests to trigger compaction over a table.\n */\nprivate[delta] trait CompactionTestHelper extends QueryTest with DeltaSQLTestUtils {\n\n  /**\n   * Compact files under the given `tablePath` using AutoCompaction/OPTIMIZE and\n   * returns the [[OptimizeMetrics]]\n   */\n  def compactAndGetMetrics(tablePath: String, where: String = \"\"): OptimizeMetrics\n\n  /** config controlling the min file size required for compaction */\n  val minFileSizeConf: String\n\n  /** config controlling the target file size for compaction */\n  val maxFileSizeConf: String\n\n  /** Create `numFilePartitions` partitions and each partition has `numFilesPerPartition` files. */\n  def createFilesToPartitions(\n      numFilePartitions: Int, numFilesPerPartition: Int, dir: String)\n      (implicit spark: SparkSession): Unit = {\n    val totalNumFiles = numFilePartitions * numFilesPerPartition\n    spark.range(start = 0, end = totalNumFiles, step = 1, numPartitions = totalNumFiles)\n      .selectExpr(s\"id % $numFilePartitions as c0\", \"id as c1\")\n      .write\n      .format(\"delta\")\n      .partitionBy(\"c0\")\n      .mode(\"append\")\n      .save(dir)\n  }\n\n  /** Create `numFiles` files without any partition. */\n  def createFilesWithoutPartitions(\n      numFiles: Int, dir: String)(implicit spark: SparkSession): Unit = {\n    spark.range(start = 0, end = numFiles, step = 1, numPartitions = numFiles)\n      .selectExpr(\"id as c0\", \"id as c1\", \"id as c2\")\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(dir)\n  }\n}\n\nprivate[delta] trait CompactionTestHelperForOptimize extends CompactionTestHelper {\n\n  override def compactAndGetMetrics(tablePath: String, where: String = \"\"): OptimizeMetrics = {\n    import testImplicits._\n    val whereClause = if (where != \"\") s\"WHERE $where\" else \"\"\n    val res = spark.sql(s\"OPTIMIZE tahoe.`$tablePath` $whereClause\")\n    val metrics: OptimizeMetrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n    metrics\n  }\n\n  override val minFileSizeConf: String = DELTA_OPTIMIZE_MIN_FILE_SIZE.key\n\n  override val maxFileSizeConf: String = DELTA_OPTIMIZE_MAX_FILE_SIZE.key\n}\n\nprivate[delta] trait CompactionTestHelperForAutoCompaction extends CompactionTestHelper {\n\n  override def compactAndGetMetrics(tablePath: String, where: String = \"\"): OptimizeMetrics = {\n    // Set min num files to 2 - so that even if two small files are present in a partition, then\n    // also they are compacted.\n    var metrics: Option[OptimizeMetrics] = None\n    withSQLConf(DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"2\") {\n        metrics =\n          Some(\n            AutoCompact.compact(\n              spark,\n              DeltaLog.forTable(spark, tablePath)\n            ).head\n          )\n    }\n    metrics.get\n  }\n\n  override val minFileSizeConf: String = DELTA_AUTO_COMPACT_MIN_FILE_SIZE.key\n\n  override val maxFileSizeConf: String = DELTA_AUTO_COMPACT_MAX_FILE_SIZE.key\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/optimize/DeltaReorgSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.optimize\n\nimport org.apache.spark.sql.delta.{DeletionVectorsTestUtils, DeltaColumnMapping, DeltaLog, DeltaUnsupportedOperationException}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.commands.VacuumCommand.generateCandidateFileMap\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport io.delta.tables.DeltaTable\nimport org.apache.hadoop.fs.{FileStatus, Path}\nimport org.apache.parquet.hadoop.Footer\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.SerializableConfiguration\n\nclass DeltaReorgSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest\n  with DeltaSQLTestUtils\n  with DeletionVectorsTestUtils {\n\n  import testImplicits._\n\n  def executePurge(table: String, condition: Option[String] = None): Unit = {\n    condition match {\n      case Some(cond) => sql(s\"REORG TABLE delta.`$table` WHERE $cond APPLY (PURGE)\")\n      case None => sql(s\"REORG TABLE delta.`$table` APPLY (PURGE)\")\n    }\n  }\n\n  test(\"Purge DVs will combine small files\") {\n    val targetDf = spark.range(0, 100, 1, numPartitions = 5).toDF\n    withTempDeltaTable(targetDf) { (_, log) =>\n      val path = log.dataPath.toString\n\n      sql(s\"DELETE FROM delta.`$path` WHERE id IN (0, 99)\")\n      assert(log.update().allFiles.filter(_.deletionVector != null).count() === 2)\n      withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> \"1073741824\") { // 1gb\n        executePurge(path)\n      }\n      val (addFiles, _) = getFileActionsInLastVersion(log)\n      assert(addFiles.size === 1, \"files should be combined\")\n      assert(addFiles.forall(_.deletionVector === null))\n      checkAnswer(\n        sql(s\"SELECT * FROM delta.`$path`\"),\n        (1 to 98).toDF())\n\n      // Verify commit history and operation metrics\n      checkOpHistory(\n        tablePath = path,\n        expOpParams = Map(\"applyPurge\" -> \"true\", \"predicate\" -> \"[]\"),\n        numFilesRemoved = 2,\n        numFilesAdded = 1)\n    }\n  }\n\n  test(\"Purge DVs\") {\n    val targetDf = spark.range(0, 100, 1, numPartitions = 5).toDF()\n    withTempDeltaTable(targetDf) { (_, log) =>\n      val path = log.dataPath.toString\n\n      sql(s\"DELETE FROM delta.`$path` WHERE id IN (0, 99)\")\n      assert(log.update().allFiles.filter(_.deletionVector != null).count() === 2)\n\n      // First purge\n      executePurge(path)\n      val (addFiles, _) = getFileActionsInLastVersion(log)\n      assert(addFiles.size === 1) // two files are combined\n      assert(addFiles.forall(_.deletionVector === null))\n      checkAnswer(\n        sql(s\"SELECT * FROM delta.`$path`\"),\n        (1 to 98).toDF())\n\n      // Verify commit history and operation metrics\n      checkOpHistory(\n        tablePath = path,\n        expOpParams = Map(\"applyPurge\" -> \"true\", \"predicate\" -> \"[]\"),\n        numFilesRemoved = 2,\n        numFilesAdded = 1)\n\n      // Second purge is a noop\n      val versionBefore = log.update().version\n      executePurge(path)\n      val versionAfter = log.update().version\n      assert(versionBefore === versionAfter)\n    }\n  }\n\n  test(\"Purge a non-DV table is a noop\") {\n    val targetDf = spark.range(0, 100, 1, numPartitions = 5).toDF()\n    withTempDeltaTable(targetDf, enableDVs = false) { (_, log) =>\n      val versionBefore = log.update().version\n      executePurge(log.dataPath.toString)\n      val versionAfter = log.update().version\n      assert(versionBefore === versionAfter)\n    }\n  }\n\n  test(\"Purge some partitions of a table with DV\") {\n    val targetDf = spark.range(0, 100, 1, numPartitions = 1)\n      .withColumn(\"part\", col(\"id\") % 4)\n      .toDF()\n    withTempDeltaTable(targetDf, partitionBy = Seq(\"part\")) { (_, log) =>\n      val path = log.dataPath\n      // Delete one row from each partition\n      sql(s\"DELETE FROM delta.`$path` WHERE id IN (48, 49, 50, 51)\")\n      val (addFiles1, _) = getFileActionsInLastVersion(log)\n      assert(addFiles1.size === 4)\n      assert(addFiles1.forall(_.deletionVector !== null))\n      // PURGE two partitions\n      sql(s\"REORG TABLE delta.`$path` WHERE part IN (0, 2) APPLY (PURGE)\")\n      val (addFiles2, _) = getFileActionsInLastVersion(log)\n      assert(addFiles2.size === 2)\n      assert(addFiles2.forall(_.deletionVector === null))\n\n      // Verify commit history and operation metrics\n      checkOpHistory(\n        tablePath = path.toString,\n        expOpParams = Map(\"applyPurge\" -> \"true\", \"predicate\" -> \"[\\\"'part IN (0,2)\\\"]\"),\n        numFilesRemoved = 2,\n        numFilesAdded = 2)\n    }\n  }\n\n  private def checkOpHistory(\n      tablePath: String,\n      expOpParams: Map[String, String],\n      numFilesRemoved: Long,\n      numFilesAdded: Long): Unit = {\n    val (opName, opParams, opMetrics) = DeltaTable.forPath(tablePath)\n      .history(1)\n      .select(\"operation\", \"operationParameters\", \"operationMetrics\")\n      .as[(String, Map[String, String], Map[String, String])]\n      .head()\n    assert(opName === \"REORG\")\n    assert(opParams === expOpParams)\n    assert(opMetrics(\"numAddedFiles\").toLong === numFilesAdded)\n    assert(opMetrics(\"numRemovedFiles\").toLong === numFilesRemoved)\n    // Because each deleted file has a DV associated it which gets rewritten as part of PURGE\n    assert(opMetrics(\"numDeletionVectorsRemoved\").toLong === numFilesRemoved)\n  }\n\n  /**\n   * Get all parquet footers for the input `files`, used only for testing.\n   *\n   * @param files the sequence of `AddFile` used to read the parquet footers\n   *              by the data file path in each `AddFile`.\n   * @param log the delta log used to get the configuration and data path.\n   * @return the sequence of the corresponding parquet footers, corresponds to\n   *         the sequence of `AddFile`.\n   */\n  private def getParquetFooters(\n      files: Seq[AddFile],\n      log: DeltaLog): Seq[Footer] = {\n    val serializedConf = new SerializableConfiguration(log.newDeltaHadoopConf())\n    val dataPath = new Path(log.dataPath.toString)\n    val nameToAddFileMap = generateCandidateFileMap(dataPath, files)\n    val fileStatuses = nameToAddFileMap.map { case (absPath, addFile) =>\n      new FileStatus(\n        /* length */ addFile.size,\n        /* isDir */ false,\n        /* blockReplication */ 0,\n        /* blockSize */ 1,\n        /* modificationTime */ addFile.modificationTime,\n        new Path(absPath)\n      )\n    }\n    DeltaFileOperations.readParquetFootersInParallel(\n      serializedConf.value,\n      fileStatuses.toList,\n      ignoreCorruptFiles = false\n    )\n  }\n\n  test(\"Purge dropped columns of a table without DV\") {\n    val targetDf = spark.range(0, 100, 1, numPartitions = 5)\n      .withColumn(\"id_dropped\", col(\"id\") % 4)\n      .toDF()\n    withTempDeltaTable(targetDf) { (_, log) =>\n      val path = log.dataPath.toString\n\n      val (addFiles1, _) = getFileActionsInLastVersion(log)\n      assert(addFiles1.size === 5)\n      val footers1 = getParquetFooters(addFiles1, log)\n      footers1.foreach { footer =>\n        val fields = footer.getParquetMetadata.getFileMetaData.getSchema.getFields\n        assert(fields.size == 2)\n        assert(fields.toArray.map { _.toString }.contains(\"optional int64 id_dropped\"))\n      }\n\n      // enable column-mapping first\n      sql(\n        s\"\"\"\n           | ALTER TABLE delta.`$path`\n           | SET TBLPROPERTIES (\n           |   'delta.columnMapping.mode' = 'name'\n           | )\n           |\"\"\".stripMargin\n      )\n      // drop the extra column by alter table and run REORG PURGE\n      sql(\n        s\"\"\"\n           | ALTER TABLE delta.`$path`\n           | DROP COLUMN id_dropped\n           |\"\"\".stripMargin\n      )\n      executePurge(path)\n\n      val (addFiles2, _) = getFileActionsInLastVersion(log)\n      assert(addFiles2.size === 1)\n      val footers2 = getParquetFooters(addFiles2, log)\n      footers2.foreach { footer =>\n        val fields = footer.getParquetMetadata.getFileMetaData.getSchema.getFields\n        assert(fields.size == 1)\n        assert(!fields.toArray.map { _.toString }.contains(\"optional int64 id_dropped\"))\n      }\n    }\n  }\n\n  test(\"Columns being renamed should not be purged\") {\n    val targetDf = spark.range(0, 100, 1, numPartitions = 5)\n      .withColumn(\"id_before_rename\", col(\"id\") % 4)\n      .withColumn(\"id_dropped\", col(\"id\") % 5)\n      .toDF()\n    withTempDeltaTable(targetDf) { (_, log) =>\n      val path = log.dataPath.toString\n\n      val (addFiles1, _) = getFileActionsInLastVersion(log)\n      assert(addFiles1.size === 5)\n      val footers1 = getParquetFooters(addFiles1, log)\n      footers1.foreach { footer =>\n        val fields = footer.getParquetMetadata.getFileMetaData.getSchema.getFields\n        assert(fields.size == 3)\n        assert(fields.toArray.map { _.toString }.contains(\"optional int64 id_dropped\"))\n        assert(fields.toArray.map { _.toString }.contains(\"optional int64 id_before_rename\"))\n      }\n\n      // enable column-mapping first\n      sql(\n        s\"\"\"\n           | ALTER TABLE delta.`$path`\n           | SET TBLPROPERTIES (\n           |   'delta.columnMapping.mode' = 'name'\n           | )\n           |\"\"\".stripMargin\n      )\n      // drop `id_dropped` and rename `id_before_rename` via alter table and run REORG PURGE,\n      // this should remove `id_dropped` but keep `id_after_rename` in the parquet files.\n      sql(\n        s\"\"\"\n           | ALTER TABLE delta.`$path`\n           | DROP COLUMN id_dropped\n           |\"\"\".stripMargin\n      )\n      sql(\n        s\"\"\"\n           | ALTER TABLE delta.`$path`\n           | RENAME COLUMN id_before_rename TO id_after_rename\n           |\"\"\".stripMargin\n      )\n      executePurge(path)\n\n      val tableSchema = log.update().schema\n      val tablePhysicalSchema = DeltaColumnMapping.renameColumns(tableSchema)\n      val beforeRenameColStr = \"StructField(id_before_rename,LongType,true)\"\n      val afterRenameColStr = \"StructField(id_after_rename,LongType,true)\"\n      assert(tableSchema.fields.length == 2 &&\n        tableSchema.map { _.toString }.contains(afterRenameColStr))\n      assert(tablePhysicalSchema.fields.length == 2 &&\n        tablePhysicalSchema.map { _.toString }.contains(beforeRenameColStr))\n\n      val (addFiles2, _) = getFileActionsInLastVersion(log)\n      assert(addFiles2.size === 1)\n      val footers2 = getParquetFooters(addFiles2, log)\n      footers2.foreach { footer =>\n        val fields = footer.getParquetMetadata.getFileMetaData.getSchema.getFields\n        assert(fields.size == 2)\n        assert(!fields.toArray.map { _.toString }.contains(\"optional int64 id_dropped = 3\"))\n        // do note that the actual name for the column will not be\n        // changed in parquet file level\n        assert(fields.toArray.map { _.toString }.contains(\"optional int64 id_before_rename = 2\"))\n      }\n    }\n  }\n\n  test(\"reorg on a catalog managed table should fail\") {\n    withCatalogManagedTable() { tableName =>\n      checkError(\n        intercept[DeltaUnsupportedOperationException] {\n          spark.sql(s\"REORG TABLE $tableName APPLY (PURGE)\")\n        },\n        \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\",\n        parameters = Map(\"operation\" -> \"OPTIMIZE\")\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/optimize/OptimizeCompactionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.optimize\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport io.delta.tables.DeltaTable\n\nimport org.scalatest.concurrent.TimeLimits.failAfter\nimport org.scalatest.time.SpanSugar._\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.LongType\n\n/**\n * Base class containing tests for Delta table Optimize (file compaction)\n */\ntrait OptimizeCompactionSuiteBase extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeletionVectorsTestUtils\n  with DeltaColumnMappingTestUtils {\n\n  import testImplicits._\n\n  def executeOptimizeTable(table: String, condition: Option[String] = None)\n  def executeOptimizePath(path: String, condition: Option[String] = None)\n\n  test(\"optimize command: with database and table name\") {\n    withTempDir { tempDir =>\n      val dbName = \"delta_db\"\n      val tableName = s\"$dbName.delta_optimize\"\n      withDatabase(dbName) {\n        spark.sql(s\"create database $dbName\")\n        withTable(tableName) {\n          appendToDeltaTable(Seq(1, 2, 3).toDF(), tempDir.toString, partitionColumns = None)\n          appendToDeltaTable(Seq(4, 5, 6).toDF(), tempDir.toString, partitionColumns = None)\n\n          spark.sql(s\"create table $tableName using delta location '$tempDir'\")\n\n          val deltaLog = DeltaLog.forTable(spark, tempDir)\n          val versionBeforeOptimize = deltaLog.snapshot.version\n          executeOptimizeTable(tableName)\n\n          deltaLog.update()\n          assert(deltaLog.snapshot.version === versionBeforeOptimize + 1)\n          checkDatasetUnorderly(spark.table(tableName).as[Int], 1, 2, 3, 4, 5, 6)\n        }\n      }\n    }\n  }\n\n  test(\"optimize command\") {\n    withTempDir { tempDir =>\n      appendToDeltaTable(Seq(1, 2, 3).toDF(), tempDir.toString, partitionColumns = None)\n      appendToDeltaTable(Seq(4, 5, 6).toDF(), tempDir.toString, partitionColumns = None)\n\n      def data: DataFrame = spark.read.format(\"delta\").load(tempDir.toString)\n\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      val versionBeforeOptimize = deltaLog.snapshot.version\n      executeOptimizePath(tempDir.getCanonicalPath)\n      deltaLog.update()\n      assert(deltaLog.snapshot.version === versionBeforeOptimize + 1)\n      checkDatasetUnorderly(data.toDF().as[Int], 1, 2, 3, 4, 5, 6)\n\n      // Make sure thread pool is shut down\n      assert(Thread.getAllStackTraces.keySet.asScala\n        .filter(_.getName.startsWith(\"OptimizeJob\")).isEmpty)\n    }\n  }\n\n  test(\"optimize command: predicate on non-partition column\") {\n    withTempDir { tempDir =>\n      val path = new File(tempDir, \"testTable\").getCanonicalPath\n      val partitionColumns = Some(Seq(\"id\"))\n      appendToDeltaTable(\n        Seq(1, 2, 3).toDF(\"value\").withColumn(\"id\", 'value % 2),\n        path,\n        partitionColumns)\n\n      val e = intercept[AnalysisException] {\n        // Should fail when predicate is on a non-partition column\n        executeOptimizePath(path, Some(\"value < 4\"))\n      }\n      assert(e.getMessage.contains(\"Predicate references non-partition column 'value'. \" +\n                                       \"Only the partition columns may be referenced: [id]\"))\n    }\n  }\n\n  test(\"optimize command: on partitioned table - all partitions\") {\n    withTempDir { tempDir =>\n      val path = new File(tempDir, \"testTable\").getCanonicalPath\n      val partitionColumns = Some(Seq(\"id\"))\n      appendToDeltaTable(\n        Seq(1, 2, 3).toDF(\"value\").withColumn(\"id\", 'value % 2),\n        path,\n        partitionColumns)\n\n      appendToDeltaTable(\n        Seq(4, 5, 6).toDF(\"value\").withColumn(\"id\", 'value % 2),\n        path,\n        partitionColumns)\n\n      val deltaLogBefore = DeltaLog.forTable(spark, path)\n      val txnBefore = deltaLogBefore.startTransaction();\n      val fileListBefore = txnBefore.filterFiles();\n      val versionBefore = deltaLogBefore.snapshot.version\n\n      val id = \"id\".phy(deltaLogBefore)\n\n      // Expect each partition have more than one file\n      (0 to 1).foreach(partId =>\n        assert(fileListBefore.count(_.partitionValues === Map(id -> partId.toString)) > 1))\n\n      executeOptimizePath(path)\n\n      val deltaLogAfter = DeltaLog.forTable(spark, path)\n      val txnAfter = deltaLogAfter.startTransaction();\n      val fileListAfter = txnAfter.filterFiles();\n\n      (0 to 1).foreach(partId =>\n        assert(fileListAfter.count(_.partitionValues === Map(id -> partId.toString)) === 1))\n\n      // version is incremented\n      assert(deltaLogAfter.snapshot.version === versionBefore + 1)\n\n      // data should remain the same after the OPTIMIZE\n      checkDatasetUnorderly(\n        spark.read.format(\"delta\").load(path).select(\"value\").as[Long],\n        (1L to 6L): _*)\n    }\n  }\n\n  test(\n    s\"optimize command with DVs\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getAbsolutePath\n      withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\") {\n        // Create 10 files each with 1000 records\n        spark.range(start = 0, end = 10000, step = 1, numPartitions = 10)\n          .toDF(\"id\")\n          .withColumn(colName = \"extra\", lit(\"just a random text to fill up the space.....\"))\n          .write.format(\"delta\").mode(\"append\").save(path) // v0\n\n        val deltaLog = DeltaLog.forTable(spark, path)\n        val filesV0 = deltaLog.unsafeVolatileSnapshot.allFiles.collect()\n        assert(filesV0.size == 10)\n\n        // Default `optimize.maxDeletedRowsRatio` is 0.05.\n        // Delete slightly more than threshold ration in two files, less in one of the file\n        val file0 = filesV0(1)\n        val file1 = filesV0(4)\n        val file2 = filesV0(8)\n        deleteRows(deltaLog, file0, approxPhyRows = 1000, ratioOfRowsToDelete = 0.06d) // v1\n        deleteRows(deltaLog, file1, approxPhyRows = 1000, ratioOfRowsToDelete = 0.06d) // v2\n        deleteRows(deltaLog, file2, approxPhyRows = 1000, ratioOfRowsToDelete = 0.01d) // v3\n\n        // Add a one small file, so that the file selection is based on both the file size and\n        // deleted rows ratio\n        spark.range(start = 1, end = 2, step = 1, numPartitions = 1)\n            .toDF(\"id\").withColumn(colName = \"extra\", lit(\"\"))\n          .write.format(\"delta\").mode(\"append\").save(path) // v4\n        val smallFiles = addedFiles(\n          deltaLog.getChanges(startVersion = 4, catalogTableOpt = None).next()._2)\n        assert(smallFiles.size == 1)\n\n        // Save the data before optimize for comparing it later with optimize\n        val data = spark.read.format(\"delta\").load(path)\n\n        // Set a low value for minFileSize so that the criteria for file selection is based on DVs\n        // and not based on the file size.\n        val targetSmallSize = smallFiles(0).size +  10 // A number just higher than the `smallFile`\n        withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_MIN_FILE_SIZE.key -> targetSmallSize.toString) {\n          executeOptimizePath(path) // v5\n        }\n        val changes = deltaLog.getChanges(startVersion = 5, catalogTableOpt = None).next()._2\n\n        // We expect the two files containing more than the threshold rows to be compacted.\n        var expectedRemoveFiles = Set(file0.path, file1.path)\n        // Expect the small file also to be compacted always\n        expectedRemoveFiles += smallFiles(0).path\n\n        assert(removedFiles(changes).map(_.path).toSet === expectedRemoveFiles)\n\n        assert(addedFiles(changes).size == 1) // Expect one new file added\n\n        // Verify the final data after optimization hasn't changed.\n        checkAnswer(spark.read.format(\"delta\").load(path), data)\n      }\n    }\n  }\n\n  private def removedFiles(actions: Seq[Action]): Seq[RemoveFile] = {\n    actions.filter(_.isInstanceOf[RemoveFile]).map(_.asInstanceOf[RemoveFile])\n  }\n\n  private def addedFiles(actions: Seq[Action]): Seq[AddFile] = {\n    actions.filter(_.isInstanceOf[AddFile]).map(_.asInstanceOf[AddFile])\n  }\n\n  def appendRowsToDeltaTable(\n      path: String,\n      numFiles: Int,\n      numRowsPerFiles: Int,\n      partitionColumns: Option[Seq[String]],\n      partitionValues: Seq[Int]): Unit = {\n    partitionValues.foreach { partition =>\n      (0 until numFiles).foreach { _ =>\n        appendToDeltaTable(\n          (0 until numRowsPerFiles).toDF(\"value\").withColumn(\"id\", lit(partition)),\n          path,\n          partitionColumns)\n      }\n    }\n  }\n\n  def testOptimizeCompactWithLargeFile(\n      name: String, unCompactablePartitions: Seq[Int], compactablePartitions: Seq[Int]) {\n    test(name) {\n      withTempDir { tempDir =>\n          val path = new File(tempDir, \"testTable\").getCanonicalPath\n          val partitionColumns = Some(Seq(\"id\"))\n          // Create un-compactable partitions.\n          appendRowsToDeltaTable(\n            path, numFiles = 1, numRowsPerFiles = 200, partitionColumns, unCompactablePartitions)\n          // Create compactable partitions with 5 files\n          appendRowsToDeltaTable(\n            path, numFiles = 5, numRowsPerFiles = 10, partitionColumns, compactablePartitions)\n\n          val deltaLogBefore = DeltaLog.forTable(spark, path)\n          val txnBefore = deltaLogBefore.startTransaction()\n          val fileListBefore = txnBefore.filterFiles()\n          val versionBefore = deltaLogBefore.snapshot.version\n\n          val id = \"id\".phy(deltaLogBefore)\n          unCompactablePartitions.foreach(partId =>\n            assert(fileListBefore.count(_.partitionValues === Map(id -> partId.toString)) == 1))\n          compactablePartitions.foreach(partId =>\n            assert(fileListBefore.count(_.partitionValues === Map(id -> partId.toString)) == 5))\n          // Optimize compact all partitions\n          spark.sql(s\"OPTIMIZE '$path'\")\n\n          val deltaLogAfter = DeltaLog.forTable(spark, path)\n          val txnAfter = deltaLogAfter.startTransaction();\n          val fileListAfter = txnAfter.filterFiles();\n          // All partitions should only contains single file.\n          (unCompactablePartitions ++ compactablePartitions).foreach(partId =>\n            assert(fileListAfter.count(_.partitionValues === Map(id -> partId.toString)) === 1))\n          // version is incremented\n          assert(deltaLogAfter.snapshot.version === versionBefore + 1)\n      }\n    }\n  }\n  testOptimizeCompactWithLargeFile(\n    \"optimize command: interleaves compactable/un-compactable partitions\",\n    unCompactablePartitions = Seq(1, 3, 5),\n    compactablePartitions = Seq(2, 4, 6))\n\n  testOptimizeCompactWithLargeFile(\n    \"optimize command: first two and last two partitions are un-compactable\",\n    unCompactablePartitions = Seq(1, 2, 5, 6),\n    compactablePartitions = Seq(3, 4))\n\n  testOptimizeCompactWithLargeFile(\n    \"optimize command: only first and last partition are compactable\",\n    unCompactablePartitions = Seq(2, 3, 4, 5),\n    compactablePartitions = Seq(1, 6))\n\n  testOptimizeCompactWithLargeFile(\n    \"optimize command: only first partition is un-compactable\",\n    unCompactablePartitions = Seq(1),\n    compactablePartitions = Seq(2, 3, 4, 5, 6))\n\n  testOptimizeCompactWithLargeFile(\n    \"optimize command: only first partition is compactable\",\n    unCompactablePartitions = Seq(2, 3, 4, 5, 6),\n    compactablePartitions = Seq(1))\n\n  test(\"optimize command: on partitioned table - selected partitions\") {\n    withTempDir { tempDir =>\n      val path = new File(tempDir, \"testTable\").getCanonicalPath\n      val partitionColumns = Some(Seq(\"id\"))\n      appendToDeltaTable(\n        Seq(1, 2, 3).toDF(\"value\").withColumn(\"id\", 'value % 2),\n        path,\n        partitionColumns)\n\n      appendToDeltaTable(\n        Seq(4, 5, 6).toDF(\"value\").withColumn(\"id\", 'value % 2),\n        path,\n        partitionColumns)\n\n      val deltaLogBefore = DeltaLog.forTable(spark, path)\n      val txnBefore = deltaLogBefore.startTransaction();\n      val fileListBefore = txnBefore.filterFiles()\n\n      val id = \"id\".phy(deltaLogBefore)\n\n      assert(fileListBefore.length >= 3)\n      assert(fileListBefore.count(_.partitionValues === Map(id -> \"0\")) > 1)\n\n      val versionBefore = deltaLogBefore.snapshot.version\n      executeOptimizePath(path, Some(\"id = 0\"))\n\n      val deltaLogAfter = DeltaLog.forTable(spark, path)\n      val txnAfter = deltaLogBefore.startTransaction();\n      val fileListAfter = txnAfter.filterFiles()\n\n      assert(fileListBefore.length > fileListAfter.length)\n      // Optimized partition should contain only one file\n      assert(fileListAfter.count(_.partitionValues === Map(id -> \"0\")) === 1)\n\n      // File counts in partitions that are not part of the OPTIMIZE should remain the same\n      assert(fileListAfter.count(_.partitionValues === Map(id -> \"1\")) ===\n                 fileListAfter.count(_.partitionValues === Map(id -> \"1\")))\n\n      // version is incremented\n      assert(deltaLogAfter.snapshot.version === versionBefore + 1)\n\n      // data should remain the same after the OPTIMIZE\n      checkDatasetUnorderly(\n        spark.read.format(\"delta\").load(path).select(\"value\").as[Long],\n        (1L to 6L): _*)\n    }\n  }\n\n  test(\"optimize command: on null partition columns\") {\n    withTempDir { tempDir =>\n      val path = new File(tempDir, \"testTable\").getCanonicalPath\n      val partitionColumn = \"part\"\n\n      (1 to 5).foreach { _ =>\n        appendToDeltaTable(\n          Seq((\"a\", 1), (\"b\", 2), (null.asInstanceOf[String], 3), (\"\", 4))\n              .toDF(partitionColumn, \"value\"),\n          path,\n          Some(Seq(partitionColumn)))\n      }\n\n      val deltaLogBefore = DeltaLog.forTable(spark, path)\n      val txnBefore = deltaLogBefore.startTransaction();\n      val fileListBefore = txnBefore.filterFiles()\n      val versionBefore = deltaLogBefore.snapshot.version\n\n      val partitionColumnPhysicalName = partitionColumn.phy(deltaLogBefore)\n\n      // we have only 1 partition here\n      val filesInEachPartitionBefore = groupInputFilesByPartition(\n        fileListBefore.map(_.toPath.toString).toArray, deltaLogBefore)\n\n      // There exist at least one file in each partition\n      assert(filesInEachPartitionBefore.forall(_._2.length > 1))\n\n      // And there is a partition for null values\n      assert(filesInEachPartitionBefore.keys.exists(\n        _ === (partitionColumnPhysicalName, nullPartitionValue)))\n\n      executeOptimizePath(path)\n\n      val deltaLogAfter = DeltaLog.forTable(spark, path)\n      val txnAfter = deltaLogBefore.startTransaction();\n      val fileListAfter = txnAfter.filterFiles()\n\n      // Number of files is less than before optimize\n      assert(fileListBefore.length > fileListAfter.length)\n\n      // Optimized partition should contain only one file in null partition\n      assert(fileListAfter.count(\n        _.partitionValues === Map[String, String](partitionColumnPhysicalName -> null)) === 1)\n\n      // version is incremented\n      assert(deltaLogAfter.snapshot.version === versionBefore + 1)\n\n      // data should remain the same after the OPTIMIZE\n      checkAnswer(\n        spark.read.format(\"delta\").load(path).groupBy(partitionColumn).count(),\n        Seq(Row(\"a\", 5), Row(\"b\", 5), Row(null, 10)))\n    }\n  }\n\n  test(\"optimize command: on table with multiple partition columns\") {\n    withTempDir { tempDir =>\n      val path = new File(tempDir, \"testTable\").getCanonicalPath\n      val partitionColumns = Seq(\"date\", \"part\")\n\n      Seq(10, 100).foreach { count =>\n        appendToDeltaTable(\n          spark.range(count)\n              .select('id, lit(\"2017-10-10\").cast(\"date\") as \"date\", 'id % 5 as \"part\"),\n          path,\n          Some(partitionColumns))\n      }\n\n      val deltaLogBefore = DeltaLog.forTable(spark, path)\n      val txnBefore = deltaLogBefore.startTransaction();\n      val fileListBefore = txnBefore.filterFiles()\n      val versionBefore = deltaLogBefore.snapshot.version\n\n      val date = \"date\".phy(deltaLogBefore)\n      val part = \"part\".phy(deltaLogBefore)\n\n      val fileCountInTestPartitionBefore = fileListBefore\n          .count(_.partitionValues === Map[String, String](date -> \"2017-10-10\", part -> \"3\"))\n\n      executeOptimizePath(path, Some(\"date = '2017-10-10' and part = 3\"))\n\n      val deltaLogAfter = DeltaLog.forTable(spark, path)\n      val txnAfter = deltaLogBefore.startTransaction();\n      val fileListAfter = txnAfter.filterFiles()\n\n      // Number of files is less than before optimize\n      assert(fileListBefore.length > fileListAfter.length)\n\n      // Optimized partition should contain only one file in null partition and less number\n      // of files than before optimize\n      val fileCountInTestPartitionAfter = fileListAfter\n          .count(_.partitionValues === Map[String, String](date -> \"2017-10-10\", part -> \"3\"))\n      assert(fileCountInTestPartitionAfter === 1L)\n      assert(fileCountInTestPartitionBefore > fileCountInTestPartitionAfter,\n             \"Expected the partition to count less number of files after optimzie.\")\n\n      // version is incremented\n      assert(deltaLogAfter.snapshot.version === versionBefore + 1)\n    }\n  }\n\n  test(\"optimize - multiple jobs start executing at once \") {\n    // The idea here is to make sure multiple optimize jobs execute concurrently. We can\n    // block the writes of one batch with a countdown latch that will unblock only\n    // after the second batch also tries to write.\n\n    val numPartitions = 2\n    withTempDir { tempDir =>\n      spark.range(100)\n          .withColumn(\"pCol\", 'id % numPartitions)\n          .repartition(10)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"pCol\")\n          .save(tempDir.getAbsolutePath)\n\n      // We have two partitions so we would have two tasks. We can make sure we have two batches\n      withSQLConf(\n        (\"fs.AbstractFileSystem.block.impl\",\n          classOf[BlockWritesAbstractFileSystem].getCanonicalName),\n        (\"fs.block.impl\", classOf[BlockWritesLocalFileSystem].getCanonicalName)) {\n\n        val path = s\"block://${tempDir.getAbsolutePath}\"\n        val deltaLog = DeltaLog.forTable(spark, path)\n        require(deltaLog.snapshot.numOfFiles === 20) // 10 files in each partition\n        // block the first write until the second batch can attempt to write.\n        BlockWritesLocalFileSystem.blockUntilConcurrentWrites(numPartitions)\n        failAfter(60.seconds) {\n          executeOptimizePath(path)\n        }\n        assert(deltaLog.snapshot.numOfFiles === numPartitions) // 1 file per partition\n      }\n    }\n  }\n\n  test(\"optimize command with multiple partition predicates\") {\n    withTempDir { tempDir =>\n      def writeData(count: Int): Unit = {\n        spark.range(count).select('id, lit(\"2017-10-10\").cast(\"date\") as \"date\", 'id % 5 as \"part\")\n            .write\n            .partitionBy(\"date\", \"part\")\n            .format(\"delta\")\n            .mode(\"append\")\n            .save(tempDir.getAbsolutePath)\n      }\n\n      writeData(10)\n      writeData(100)\n\n      executeOptimizePath(tempDir.getAbsolutePath, Some(\"date = '2017-10-10' and part = 3\"))\n\n      val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n      val part = \"part\".phy(deltaLog)\n      val files = groupInputFilesByPartition(df.inputFiles, deltaLog)\n      assert(files.filter(_._1._1 == part).minBy(_._2.length)._1 === (part, \"3\"),\n        \"part 3 should have been optimized and have least amount of files\")\n    }\n  }\n\n  test(\"optimize command with multiple partition predicates with multiple where\") {\n    withTempDir { tempDir =>\n      def writeData(count: Int): Unit = {\n        spark.range(count).select('id, lit(\"2017-10-10\").cast(\"date\") as \"date\", 'id % 5 as \"part\")\n          .write\n          .partitionBy(\"date\", \"part\")\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.getAbsolutePath)\n      }\n\n      writeData(10)\n      writeData(100)\n\n      DeltaTable.forPath(tempDir.getAbsolutePath).optimize()\n        .where(\"part = 3\")\n        .where(\"date = '2017-10-10'\")\n        .executeCompaction()\n\n      val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n      val part = \"part\".phy(deltaLog)\n      val files = groupInputFilesByPartition(df.inputFiles, deltaLog)\n      assert(files.filter(_._1._1 == part).minBy(_._2.length)._1 === (part, \"3\"),\n        \"part 3 should have been optimized and have least amount of files\")\n    }\n  }\n\n  def optimizeWithBatching(\n      batchSize: String,\n      expectedCommits: Int,\n      condition: Option[String],\n      partitionFileCount: Map[String, Int]): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_BATCH_SIZE.key -> batchSize) {\n      withTempDir { tempDir =>\n        def writeData(count: Int): Unit = {\n          spark.range(count).select('id, 'id % 5 as \"part\")\n            .coalesce(1)\n            .write\n            .partitionBy(\"part\")\n            .format(\"delta\")\n            .mode(\"append\")\n            .save(tempDir.getAbsolutePath)\n        }\n\n        writeData(10)\n        writeData(100)\n\n        val data = spark.read.format(\"delta\").load(tempDir.getAbsolutePath()).collect()\n\n        executeOptimizePath(tempDir.getAbsolutePath, condition)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n        checkAnswer(df, data)\n\n        val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n\n        val commits = deltaLog.history.getHistory(None)\n        assert(commits.filter(_.operation == \"OPTIMIZE\").length == expectedCommits)\n\n        val files = groupInputFilesByPartition(df.inputFiles, deltaLog)\n        for ((part, fileCount) <- partitionFileCount) {\n          assert(files((\"part\", part)).length == fileCount)\n        }\n      }\n    }\n  }\n\n  test(\"optimize command with batching\") {\n    // Batch size of 1 byte means each bin will run in its own batch, and lead to 5 batches,\n    // one for each partition.\n    Seq((\"1\", 5), (\"1g\", 1)).foreach { case (batchSize, optimizeCommits) =>\n      // All partitions should be one file after optimizing\n      val partitionFileCount = (0 to 4).map(_.toString -> 1).toMap\n\n      optimizeWithBatching(batchSize, optimizeCommits, None, partitionFileCount)\n    }\n  }\n\n  test(\"optimize command with where clause and batching\") {\n    // Batch size of 1 byte means each bin will run in its own batch, and lead to 2 batches\n    // for the two partitions we are optimizing.\n    Seq((\"1\", 2), (\"1g\", 1)).foreach { case (batchSize, optimizeCommits) =>\n      // First two partitions should have 1 file, last 3 should have two\n      val partitionFileCount = Map(\n        \"0\" -> 1,\n        \"1\" -> 1,\n        \"2\" -> 2,\n        \"3\" -> 2,\n        \"4\" -> 2\n      )\n      val files = optimizeWithBatching(batchSize, optimizeCommits, Some(\"part <= 1\"),\n        partitionFileCount)\n    }\n  }\n\n  test(\"optimize an empty table with batching\") {\n    // Batch size of 1 byte means each bin will run in its own batch\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_BATCH_SIZE.key -> \"1\") {\n      withTempDir { tempDir =>\n        DeltaTable.create(spark)\n          .location(tempDir.getAbsolutePath())\n          .addColumn(\"id\", LongType)\n          .addColumn(\"part\", LongType)\n          .partitionedBy(\"part\")\n          .execute()\n\n        // Just make sure it succeeds\n        executeOptimizePath(tempDir.getAbsolutePath)\n\n        assert(spark.read.format(\"delta\").load(tempDir.getAbsolutePath()).count() == 0)\n      }\n    }\n  }\n\n  /**\n   * Utility method to append the given data to the Delta table located at the given path.\n   * Optionally partitions the data.\n   */\n  protected def appendToDeltaTable[T](\n      data: Dataset[T], tablePath: String, partitionColumns: Option[Seq[String]] = None): Unit = {\n    var df = data.repartition(1).write;\n    partitionColumns.map(columns => {\n      df = df.partitionBy(columns: _*)\n    })\n    df.format(\"delta\").mode(\"append\").save(tablePath)\n  }\n\n  test(\"optimize on a catalog managed table should fail\") {\n    withCatalogManagedTable() { tableName =>\n      checkError(\n        intercept[DeltaUnsupportedOperationException] {\n          spark.sql(s\"OPTIMIZE $tableName\")\n        },\n        \"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\",\n        parameters = Map(\"operation\" -> \"OPTIMIZE\")\n      )\n    }\n  }\n}\n\n/**\n * Runs optimize compaction tests using OPTIMIZE SQL\n */\nclass OptimizeCompactionSQLSuite extends OptimizeCompactionSuiteBase\n    with DeltaSQLCommandTest {\n  import testImplicits._\n\n  def executeOptimizeTable(table: String, condition: Option[String] = None): Unit = {\n    val conditionClause = condition.map(c => s\"WHERE $c\").getOrElse(\"\")\n    spark.sql(s\"OPTIMIZE $table $conditionClause\")\n  }\n\n  def executeOptimizePath(path: String, condition: Option[String] = None): Unit = {\n    executeOptimizeTable(s\"'$path'\", condition)\n  }\n\n  test(\"optimize command: missing path\") {\n    val e = intercept[ParseException] {\n      spark.sql(s\"OPTIMIZE\")\n    }\n    assert(e.getMessage.contains(\"OPTIMIZE\"))\n  }\n\n  test(\"optimize command: missing predicate on path\") {\n    val e = intercept[ParseException] {\n      spark.sql(s\"OPTIMIZE /doesnt/exist WHERE\")\n    }\n    assert(e.getMessage.contains(\"OPTIMIZE\"))\n  }\n\n  test(\"optimize command: non boolean expression\") {\n    val e = intercept[ParseException] {\n      spark.sql(s\"OPTIMIZE /doesnt/exist WHERE 1+1\")\n    }\n    assert(e.getMessage.contains(\"OPTIMIZE\"))\n  }\n\n  test(\"optimize with partition value containing space\") {\n    withTempDir { tempDir =>\n      val baseDf = Seq((\"a space\", 1), (\"other\", 2)).toDF(\"name\", \"value\")\n\n      def write(): Unit = {\n        baseDf.write\n          .format(\"delta\")\n          .partitionBy(\"name\")\n          .mode(\"append\")\n          .save(tempDir.getAbsolutePath)\n      }\n\n      write()\n      write()\n\n      sql(s\"optimize '${tempDir.getAbsolutePath}'\")\n      val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      assert(df.inputFiles.length === 2, \"2 files for 2 partitions\")\n      checkAnswer(\n        df,\n        baseDf.union(baseDf))\n    }\n  }\n\n  test(\"optimize command: subquery predicate\") {\n    val tableName = \"myTable\"\n    withTable(tableName) {\n      spark.sql(s\"create table $tableName (p int, id int) using delta partitioned by(p)\")\n      val e = intercept[DeltaAnalysisException] {\n        spark.sql(s\"optimize $tableName where p >= (select p from $tableName where id > 5)\")\n      }\n      checkError(e, \"DELTA_UNSUPPORTED_SUBQUERY_IN_PARTITION_PREDICATES\",\n        \"0AKDC\", Map.empty[String, String])\n    }\n  }\n}\n\n/**\n * Runs optimize compaction tests using OPTIMIZE Scala APIs\n */\nclass OptimizeCompactionScalaSuite extends OptimizeCompactionSuiteBase\n    with DeltaSQLCommandTest {\n  def executeOptimizeTable(table: String, condition: Option[String] = None): Unit = {\n    if (condition.isDefined) {\n      DeltaTable.forName(table).optimize().where(condition.get).executeCompaction()\n    } else {\n      DeltaTable.forName(table).optimize().executeCompaction()\n    }\n  }\n\n  def executeOptimizePath(path: String, condition: Option[String] = None): Unit = {\n    if (condition.isDefined) {\n      DeltaTable.forPath(path).optimize().where(condition.get).executeCompaction()\n    } else {\n      DeltaTable.forPath(path).optimize().executeCompaction()\n    }\n  }\n}\n\ntrait OptimizeCompactionColumnMappingSuiteBase extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runOnlyTests = Seq(\n    \"optimize command: on table with multiple partition columns\",\n    \"optimize command: on null partition columns\"\n  )\n}\n\nclass OptimizeCompactionIdColumnMappingSuite extends OptimizeCompactionSQLSuite\n  with DeltaColumnMappingEnableIdMode\n  with OptimizeCompactionColumnMappingSuiteBase {\n}\n\nclass OptimizeCompactionNameColumnMappingSuite extends OptimizeCompactionSQLSuite\n  with DeltaColumnMappingEnableNameMode\n  with OptimizeCompactionColumnMappingSuiteBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/optimize/OptimizeConflictSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.optimize\n\nimport java.io.File\n\nimport scala.concurrent.duration.Duration\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.concurrency.PhaseLockingTestMixin\nimport org.apache.spark.sql.delta.concurrency.TransactionExecutionTestMixin\nimport org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ThreadUtils\n\nclass OptimizeConflictSuite extends QueryTest\n  with SharedSparkSession\n  with PhaseLockingTestMixin\n  with TransactionExecutionTestMixin\n  with DeltaSQLCommandTest {\n\n  protected def appendRows(dir: File, numRows: Int, numFiles: Int): Unit = {\n    spark.range(start = 0, end = numRows, step = 1, numPartitions = numFiles)\n      .write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n  }\n\n  test(\"conflict handling between Optimize and Business Txn\") {\n    withTempDir { tempDir =>\n\n      // Create table with 100 rows.\n      appendRows(tempDir, numRows = 100, numFiles = 10)\n\n      // Enable DVs.\n      sql(s\"ALTER TABLE delta.`${tempDir.toString}` \" +\n        \"SET TBLPROPERTIES ('delta.enableDeletionVectors' = true);\")\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n\n      def optimizeTxn(): Array[Row] = {\n        deltaTable.optimize().executeCompaction()\n        Array.empty\n      }\n\n      def deleteTxn(): Array[Row] = {\n        // Delete 50% of the rows.\n        sql(s\"DELETE FROM delta.`${tempDir}` WHERE id%2 = 0\").collect()\n      }\n\n      val Seq(future) = runFunctionsWithOrderingFromObserver(Seq(optimizeTxn)) {\n        case (optimizeObserver :: Nil) =>\n          // Create a replacement observer for the retry thread of Optimize.\n          val retryObserver = new PhaseLockingTransactionExecutionObserver(\n            OptimisticTransactionPhases.forName(\"test-replacement-txn\"))\n\n          // Block Optimize during the first commit attempt.\n          optimizeObserver.setNextObserver(retryObserver, autoAdvance = true)\n          unblockUntilPreCommit(optimizeObserver)\n          busyWaitFor(optimizeObserver.phases.preparePhase.hasEntered, timeout)\n\n          // Delete starts and finishes\n          deleteTxn()\n\n          // Allow Optimize to resume.\n          unblockCommit(optimizeObserver)\n          busyWaitFor(optimizeObserver.phases.commitPhase.hasLeft, timeout)\n          optimizeObserver.phases.postCommitPhase.exitBarrier.unblock()\n\n          // The first txn will not commit as there was a conflict commit\n          // (deleteTxn). Optimize will attempt to auto resolve and retry\n          // Wait for the retry txn to finish.\n          // Resume the retry txn.\n          unblockAllPhases(retryObserver)\n      }\n      val e = intercept[SparkException] {\n        ThreadUtils.awaitResult(future, timeout)\n      }\n      // The retry txn should fail as the same files are modified(DVs added) by\n      // the delete txn.\n      assert(e.getCause.getMessage.contains(\"DELTA_CONCURRENT_DELETE_READ\"))\n      assert(sql(s\"SELECT * FROM delta.`${tempDir}`\").count() == 50)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/optimize/OptimizeMetricsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.optimize\n\n// scalastyle:off import.ordering.noEmptyLine\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands.optimize.{FileSizeStats, OptimizeMetrics, ZOrderStats}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.functions.floor\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\n/** Tests that run optimize and verify the returned output (metrics) is expected. */\ntrait OptimizeMetricsSuiteBase extends QueryTest\n    with SharedSparkSession\n    with DeletionVectorsTestUtils {\n\n  import testImplicits._\n\n  test(\"optimize metrics\") {\n    withTempDir { tempDir =>\n      val skewedRightSeq =\n        0.to(79).seq ++ 40.to(79).seq ++ 60.to(79).seq ++ 70.to(79).seq ++ 75.to(79).seq\n      skewedRightSeq.toDF().withColumn(\"p\", floor('value / 10)).repartition(4)\n        .write.partitionBy(\"p\").format(\"delta\").save(tempDir.toString)\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      val startCount = deltaLog.unsafeVolatileSnapshot.numOfFiles\n      val startSizes = deltaLog.unsafeVolatileSnapshot.allFiles.select('size).as[Long].collect()\n      val res = spark.sql(s\"OPTIMIZE delta.`${tempDir.toString}`\")\n      val metrics: OptimizeMetrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n      val finalSizes = deltaLog.unsafeVolatileSnapshot.allFiles\n        .select('size).collect().map(_.getLong(0))\n      val finalNumFiles = deltaLog.unsafeVolatileSnapshot.numOfFiles\n      assert(metrics.numFilesAdded == finalNumFiles)\n      assert(metrics.numFilesRemoved == startCount)\n      assert(metrics.filesAdded.min.get == finalSizes.min)\n      assert(metrics.filesAdded.max.get == finalSizes.max)\n      assert(metrics.filesAdded.totalSize == finalSizes.sum)\n      assert(metrics.filesAdded.totalFiles == finalSizes.length)\n      assert(metrics.filesRemoved.max.get == startSizes.max)\n      assert(metrics.filesRemoved.min.get == startSizes.min)\n      assert(metrics.filesRemoved.totalSize == startSizes.sum)\n      assert(metrics.filesRemoved.totalFiles == startSizes.length)\n      assert(metrics.totalConsideredFiles == startCount)\n      assert(metrics.totalFilesSkipped == 0)\n      assert(metrics.numTableColumns == 2)\n      assert(metrics.numTableColumnsWithStats == 2)\n    }\n  }\n\n\n  /**\n   * Ensure public API for metrics persists\n   */\n  test(\"optimize command output schema\") {\n\n    val zOrderFileStatsSchema = StructType(Seq(\n      StructField(\"num\", LongType, nullable = false),\n      StructField(\"size\", LongType, nullable = false)\n    ))\n\n    val zOrderStatsSchema = StructType(Seq(\n      StructField(\"strategyName\", StringType, nullable = true),\n      StructField(\"inputCubeFiles\", zOrderFileStatsSchema, nullable = true),\n      StructField(\"inputOtherFiles\", zOrderFileStatsSchema, nullable = true),\n      StructField(\"inputNumCubes\", LongType, nullable = false),\n      StructField(\"mergedFiles\", zOrderFileStatsSchema, nullable = true),\n      StructField(\"numOutputCubes\", LongType, nullable = false),\n      StructField(\"mergedNumCubes\", LongType, nullable = true)\n    ))\n\n    val clusteringFileStatsSchema = StructType(Seq(\n      StructField(\"numFiles\", LongType, nullable = false),\n      StructField(\"size\", LongType, nullable = false)))\n\n    val clusteringStatsSchema = StructType(Seq(\n      StructField(\"inputZCubeFiles\", clusteringFileStatsSchema, nullable = true),\n      StructField(\"inputOtherFiles\", clusteringFileStatsSchema, nullable = true),\n      StructField(\"inputNumZCubes\", LongType, nullable = false),\n      StructField(\"mergedFiles\", clusteringFileStatsSchema, nullable = true),\n      StructField(\"numOutputZCubes\", LongType, nullable = false)))\n\n    val fileSizeMetricsSchema = StructType(Seq(\n      StructField(\"min\", LongType, nullable = true),\n      StructField(\"max\", LongType, nullable = true),\n      StructField(\"avg\", DoubleType, nullable = false),\n      StructField(\"totalFiles\", LongType, nullable = false),\n      StructField(\"totalSize\", LongType, nullable = false)\n    ))\n\n    val parallelismMetricsSchema = StructType(Seq(\n      StructField(\"maxClusterActiveParallelism\", LongType, nullable = true),\n      StructField(\"minClusterActiveParallelism\", LongType, nullable = true),\n      StructField(\"maxSessionActiveParallelism\", LongType, nullable = true),\n      StructField(\"minSessionActiveParallelism\", LongType, nullable = true)\n    ))\n    val dvMetricsSchema = StructType(Seq(\n      StructField(\"numDeletionVectorsRemoved\", LongType, nullable = false),\n      StructField(\"numDeletionVectorRowsRemoved\", LongType, nullable = false)\n    ))\n\n    val optimizeMetricsSchema = StructType(Seq(\n      StructField(\"numFilesAdded\", LongType, nullable = false),\n      StructField(\"numFilesRemoved\", LongType, nullable = false),\n      StructField(\"filesAdded\", fileSizeMetricsSchema, nullable = true),\n      StructField(\"filesRemoved\", fileSizeMetricsSchema, nullable = true),\n      StructField(\"partitionsOptimized\", LongType, nullable = false),\n      StructField(\"zOrderStats\", zOrderStatsSchema, nullable = true),\n      StructField(\"clusteringStats\", clusteringStatsSchema, nullable = true),\n      StructField(\"numBins\", LongType, nullable = false),\n      StructField(\"numBatches\", LongType, nullable = false),\n      StructField(\"totalConsideredFiles\", LongType, nullable = false),\n      StructField(\"totalFilesSkipped\", LongType, nullable = false),\n      StructField(\"preserveInsertionOrder\", BooleanType, nullable = false),\n      StructField(\"numFilesSkippedToReduceWriteAmplification\", LongType, nullable = false),\n      StructField(\"numBytesSkippedToReduceWriteAmplification\", LongType, nullable = false),\n      StructField(\"startTimeMs\", LongType, nullable = false),\n      StructField(\"endTimeMs\", LongType, nullable = false),\n      StructField(\"totalClusterParallelism\", LongType, nullable = false),\n      StructField(\"totalScheduledTasks\", LongType, nullable = false),\n      StructField(\"autoCompactParallelismStats\", parallelismMetricsSchema, nullable = true),\n      StructField(\"deletionVectorStats\", dvMetricsSchema, nullable = true),\n      StructField(\"numTableColumns\", LongType, nullable = false),\n      StructField(\"numTableColumnsWithStats\", LongType, nullable = false)\n    ))\n    val optimizeSchema = StructType(Seq(\n      StructField(\"path\", StringType, nullable = true),\n      StructField(\"metrics\", optimizeMetricsSchema, nullable = true)\n    ))\n    withTempDir { tempDir =>\n      spark.range(0, 10).write.format(\"delta\").save(tempDir.toString)\n      val res = sql(s\"OPTIMIZE delta.`${tempDir.toString}`\")\n      assert(res.schema == optimizeSchema)\n    }\n  }\n\n  test(\"optimize operation metrics in Delta table history\") {\n    withSQLConf(DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        val sampleData =\n          0.to(79).seq ++ 40.to(79).seq ++ 60.to(79).seq ++ 70.to(79).seq ++ 75.to(79).seq\n\n        // partition the data and write to test table\n        sampleData.toDF().withColumn(\"p\", floor('value / 10)).repartition(4)\n            .write.partitionBy(\"p\").format(\"delta\").save(tempDir.toString)\n\n        spark.sql(s\"OPTIMIZE delta.`${tempDir.toString}`\") // run optimize on the table\n\n        val actualOperationMetricsAndName = DeltaTable.forPath(spark, tempDir.getAbsolutePath)\n          .history(1)\n          .select(\"operationMetrics\", \"operation\")\n          .head\n\n        val actualOperationMetrics = actualOperationMetricsAndName\n          .getMap(0)\n          .asInstanceOf[Map[String, String]]\n\n        // File sizes depend on the order of how they are merged (=> compression). In order to avoid\n        // flaky test, just test that the metric exists.\n        Seq(\n          \"numAddedFiles\",\n          \"numAddedBytes\",\n          \"numRemovedBytes\",\n          \"numRemovedFiles\",\n          \"numRemovedBytes\",\n          \"minFileSize\",\n          \"maxFileSize\",\n          \"p25FileSize\",\n          \"p50FileSize\",\n          \"p75FileSize\",\n          \"numDeletionVectorsRemoved\"\n        ).foreach(metric => assert(actualOperationMetrics.get(metric).isDefined))\n\n        val operationName = actualOperationMetricsAndName(1).asInstanceOf[String]\n        assert(operationName === DeltaOperations.OPTIMIZE_OPERATION_NAME)\n      }\n    }\n  }\n\n  test(\"optimize metrics on idempotent operations\") {\n    val tblName = \"tblName\"\n    withTable(tblName) {\n      // Create Delta table\n      spark.range(10).write.format(\"delta\").saveAsTable(tblName)\n\n      // First Optimize\n      spark.sql(s\"OPTIMIZE $tblName\")\n\n      // Second Optimize\n      val res = spark.sql(s\"OPTIMIZE $tblName\")\n      val actMetrics: OptimizeMetrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n      var preserveInsertionOrder = false\n\n      val expMetrics = OptimizeMetrics(\n        numFilesAdded = 0,\n        numFilesRemoved = 0,\n        filesAdded = FileSizeStats().toFileSizeMetrics,\n        filesRemoved = FileSizeStats().toFileSizeMetrics,\n        partitionsOptimized = 0,\n        zOrderStats = None,\n        numBins = 0,\n        numBatches = 1,\n        totalConsideredFiles = 1,\n        totalFilesSkipped = 1,\n        preserveInsertionOrder = preserveInsertionOrder,\n        startTimeMs = actMetrics.startTimeMs,\n        endTimeMs = actMetrics.endTimeMs,\n        totalClusterParallelism = 2,\n        totalScheduledTasks = 0,\n        numTableColumns = 1,\n        numTableColumnsWithStats = 1)\n\n      assert(actMetrics === expMetrics)\n    }\n  }\n\n  test(\"optimize metrics when certain table columns have no stats\") {\n    val tblName = \"tblName\"\n    withTable(tblName) {\n      // Create Delta table with 5 columns\n      spark.range(10)\n        .withColumn(\"col2\", 'id * 2)\n        .withColumn(\"col3\", 'id * 3)\n        .withColumn(\"col4\", 'id * 4)\n        .withColumn(\"col5\", 'id * 5)\n        .write.format(\"delta\").saveAsTable(tblName)\n\n      // Set to only collect data skipping stats on 3 columns\n      spark.sql(s\"\"\"\n                  |ALTER TABLE $tblName\n                  |SET TBLPROPERTIES (\n                  |  'delta.dataSkippingNumIndexedCols' = '3'\n                  |)\"\"\".stripMargin)\n\n      // Optimize\n      val res = spark.sql(s\"OPTIMIZE $tblName\")\n      val actMetrics: OptimizeMetrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n\n      // The table has 5 columns\n      assert(actMetrics.numTableColumns == 5)\n      // There are only 3 columns to collect stats because of the dataSkippingNumIndexedCols config\n      assert(actMetrics.numTableColumnsWithStats == 3)\n    }\n  }\n\n\n  test(\"optimize ZOrderBy operation metrics in Delta table history\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_HISTORY_METRICS_ENABLED.key -> \"true\") {\n      withTempDir { tempDir =>\n        // create a partitioned table with each partition containing multiple files\n        0.to(100).seq.toDF()\n          .withColumn(\"col1\", floor('value % 7))\n          .withColumn(\"col2\", floor('value % 27))\n          .withColumn(\"p\", floor('value % 10))\n          .repartition(4).write.partitionBy(\"p\").format(\"delta\").save(tempDir.toString)\n\n        val startSizes = DeltaLog.forTable(spark, tempDir)\n          .unsafeVolatileSnapshot.allFiles.select('size).as[Long].collect().sorted\n\n        spark.sql(s\"OPTIMIZE delta.`${tempDir.toString}` ZORDER BY (col1, col2)\").show()\n\n        val finalSizes = DeltaLog.forTable(spark, tempDir)\n          .unsafeVolatileSnapshot.allFiles.select('size).collect().map(_.getLong(0)).sorted\n\n        val actualOperation = DeltaTable.forPath(spark, tempDir.getAbsolutePath).history(1)\n          .select(\n            \"operationParameters.zOrderBy\",\n            \"operationMetrics\",\n            \"operation\")\n          .head\n\n        // Verify ZOrder operation parameters\n        val actualOpParameters = actualOperation.getString(0)\n        assert(actualOpParameters === \"[\\\"col1\\\",\\\"col2\\\"]\")\n\n        // Verify metrics records in commit log.\n        val actualMetrics = actualOperation\n          .getMap(1)\n          .asInstanceOf[Map[String, String]]\n\n        val expMetricsJson =\n          s\"\"\"{\n            |  \"numRemovedFiles\" : \"37\",\n            |  \"numAddedFiles\" : \"10\",\n            |  \"numAddedBytes\" : \"${finalSizes.sum}\",\n            |  \"numRemovedBytes\" : \"${startSizes.sum}\",\n            |  \"minFileSize\" : \"${finalSizes.min}\",\n            |  \"maxFileSize\" : \"${finalSizes.max}\",\n            |  \"p25FileSize\" : \"${finalSizes(finalSizes.length / 4)}\",\n            |  \"p50FileSize\" : \"${finalSizes(finalSizes.length / 2)}\",\n            |  \"p75FileSize\" : \"${finalSizes(3 * finalSizes.length / 4)}\",\n            |  \"numDeletionVectorsRemoved\" : \"0\"\n            |}\"\"\".stripMargin.trim\n\n        val expMetrics = JsonUtils.fromJson[Map[String, String]](expMetricsJson)\n        assert(actualMetrics === expMetrics)\n\n        val operationName = actualOperation(2).asInstanceOf[String]\n        assert(operationName === DeltaOperations.OPTIMIZE_OPERATION_NAME)\n      }\n    }\n  }\n\n  test(\"optimize ZOrderBy operation metrics in command output\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> \"1000000\") {\n      withTempDir { tempDir =>\n        // create a partitioned table with each partition containing multiple files\n        0.to(100).seq.toDF()\n          .withColumn(\"col1\", floor('value % 7))\n          .withColumn(\"col2\", floor('value % 27))\n          .withColumn(\"p\", floor('value % 10))\n          .repartition(4).write.partitionBy(\"p\").format(\"delta\").save(tempDir.toString)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val startCount = deltaLog.unsafeVolatileSnapshot.allFiles.count()\n        val startSizes = deltaLog.unsafeVolatileSnapshot.allFiles.select('size).as[Long].collect()\n\n        val result = spark.sql(s\"OPTIMIZE delta.`${tempDir.toString}` ZORDER BY (col1, col2)\")\n        val metrics: OptimizeMetrics = result.select($\"metrics.*\").as[OptimizeMetrics].head()\n\n        val finalSizes = deltaLog.unsafeVolatileSnapshot.allFiles\n          .select('size).collect().map(_.getLong(0))\n        val finalNumFiles = deltaLog.unsafeVolatileSnapshot.allFiles.collect().length\n\n        assert(metrics.filesAdded.totalFiles === finalNumFiles)\n        assert(metrics.filesRemoved.totalFiles === startCount)\n        assert(metrics.filesAdded.min.get === finalSizes.min)\n        assert(metrics.filesAdded.max.get === finalSizes.max)\n        assert(metrics.filesRemoved.max.get === startSizes.max)\n        assert(metrics.filesRemoved.min.get === startSizes.min)\n        assert(metrics.totalFilesSkipped === 0)\n        assert(metrics.totalConsideredFiles === metrics.numFilesRemoved)\n\n        val expZOrderMetrics = s\"\"\"{\n          |  \"strategyName\" : \"all\",\n          |  \"inputCubeFiles\" : {\n          |    \"num\" : 0,\n          |    \"size\" : 0\n          |  },\n          |  \"inputOtherFiles\" : {\n          |    \"num\" : $startCount,\n          |    \"size\" : ${startSizes.sum}\n          |  },\n          |  \"inputNumCubes\" : 0,\n          |  \"mergedFiles\" : {\n          |    \"num\" : $startCount,\n          |    \"size\" : ${startSizes.sum}\n          |  },\n          |  \"numOutputCubes\" : 10\n          |}\"\"\".stripMargin\n\n        assert(metrics.zOrderStats === Some(JsonUtils.fromJson[ZOrderStats](expZOrderMetrics)))\n      }\n    }\n  }\n\n  val optimizeCommands = Seq(\"optimize\", \"zorder\", \"purge\")\n  for (cmd <- optimizeCommands) {\n    testWithDVs(s\"deletion vector metrics - $cmd\") {\n      withTempDir { dirName =>\n        // Create table with 100 files of 10 rows each.\n        val numFiles = 100\n        val path = dirName.getAbsolutePath\n        spark.range(0, 1000, step = 1, numPartitions = numFiles)\n          .write.format(\"delta\").save(path)\n        val tableName = s\"delta.`$path`\"\n        val deltaTable = DeltaTable.forPath(spark, path)\n        val deltaLog = DeltaLog.forTable(spark, path)\n\n        var allFiles = deltaLog.unsafeVolatileSnapshot.allFiles.collect().toSeq\n        // Delete two rows each from 5 files to create Deletion Vectors.\n        val numFilesWithDVs = 5\n        val numDeletedRows = numFilesWithDVs * 2\n        allFiles.take(numFilesWithDVs).foreach(\n          file => removeRowsFromFile(deltaLog, file, Seq(1, 5)))\n\n        allFiles = deltaLog.unsafeVolatileSnapshot.allFiles.collect().toSeq\n        assert(allFiles.size === numFiles)\n        assert(allFiles.filter(_.deletionVector != null).size === numFilesWithDVs)\n\n        var expOpName = DeltaOperations.OPTIMIZE_OPERATION_NAME\n        val metrics: Seq[OptimizeMetrics] = cmd match {\n          case \"optimize\" =>\n            spark.sql(s\"OPTIMIZE $tableName\")\n              .select(\"metrics.*\").as[OptimizeMetrics].collect().toSeq\n          case \"zorder\" =>\n            spark.sql(s\"OPTIMIZE $tableName ZORDER BY (id)\")\n              .select(\"metrics.*\").as[OptimizeMetrics].collect().toSeq\n          case \"purge\" =>\n            expOpName = DeltaOperations.REORG_OPERATION_NAME\n            spark.sql(s\"REORG TABLE $tableName APPLY (PURGE)\")\n              .select(\"metrics.*\").as[OptimizeMetrics].collect().toSeq\n          case unknown => throw new IllegalArgumentException(s\"Unknown command: $unknown\")\n        }\n\n        // Check DV metrics in the result.\n        assert(metrics.length === 1)\n        val dvStats = metrics.head.deletionVectorStats\n        assert(dvStats.get.numDeletionVectorsRemoved === numFilesWithDVs)\n        assert(dvStats.get.numDeletionVectorRowsRemoved === numDeletedRows)\n\n        // Check DV metrics in the Delta history.\n        val opMetricsAndName = deltaTable.history.select(\"operationMetrics\", \"operation\")\n          .head\n\n        val opMetrics = opMetricsAndName\n          .getMap(0)\n          .asInstanceOf[Map[String, String]]\n        val dvMetrics = opMetrics.keys.filter(_.contains(\"DeletionVector\"))\n        assert(dvMetrics === Set(\"numDeletionVectorsRemoved\"))\n        assert(opMetrics(\"numDeletionVectorsRemoved\") === numFilesWithDVs.toString)\n\n        val operationName = opMetricsAndName(1).asInstanceOf[String]\n        assert(operationName === expOpName)\n      }\n    }\n  }\n}\n\nclass OptimizeMetricsSuite extends OptimizeMetricsSuiteBase\n  with DeltaSQLCommandTest\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/optimize/OptimizeZOrderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.optimize\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils, TestsStatistics}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport io.delta.tables.DeltaTable\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions.{col, floor, lit, max, struct}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.LongType\n\ntrait OptimizePartitionTableHelper extends QueryTest {\n  def testPartition(str: String)(testFun: => Any): Unit = {\n    test(\"partitioned table - \" + str) {\n      testFun\n    }\n  }\n}\n\n/** Tests for Optimize Z-Order by */\ntrait OptimizeZOrderSuiteBase extends OptimizePartitionTableHelper\n  with TestsStatistics\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaColumnMappingTestUtils {\n  import testImplicits._\n\n\n  def executeOptimizeTable(table: String, zOrderBy: Seq[String],\n    condition: Option[String] = None): DataFrame\n  def executeOptimizePath(path: String, zOrderBy: Seq[String],\n    condition: Option[String] = None): DataFrame\n\n  test(\"optimize command: checks existence of interleaving columns\") {\n    withTempDir { tempDir =>\n      Seq(1, 2, 3).toDF(\"value\")\n        .select('value, 'value % 2 as \"id\", 'value % 3 as \"id2\")\n        .write\n        .format(\"delta\")\n        .save(tempDir.toString)\n      val e = intercept[IllegalArgumentException] {\n        executeOptimizePath(tempDir.getCanonicalPath, Seq(\"id\", \"id3\"))\n      }\n      assert(Seq(\"id3\", \"data schema\").forall(e.getMessage.contains))\n    }\n  }\n\n  test(\"optimize command: interleaving columns can't be partitioning columns\") {\n    withTempDir { tempDir =>\n      Seq(1, 2, 3).toDF(\"value\")\n        .select('value, 'value % 2 as \"id\", 'value % 3 as \"id2\")\n        .write\n        .format(\"delta\")\n        .partitionBy(\"id\")\n        .save(tempDir.toString)\n      val e = intercept[IllegalArgumentException] {\n        executeOptimizePath(tempDir.getCanonicalPath, Seq(\"id\", \"id2\"))\n      }\n      assert(e.getMessage === DeltaErrors.zOrderingOnPartitionColumnException(\"id\").getMessage)\n    }\n  }\n\n  test(\"optimize command: interleaving with nested columns\") {\n    withTempDir { tempDir =>\n      val df = spark.read.json(Seq(\"\"\"{\"a\":1,\"b\":{\"c\":2,\"d\":3}}\"\"\").toDS())\n      df.write.format(\"delta\").save(tempDir.toString)\n      executeOptimizePath(tempDir.getCanonicalPath, Seq(\"a\", \"b.c\"))\n    }\n  }\n\n  testPartition(\"optimize on null partition column\") {\n    withTempDir { tempDir =>\n      (1 to 5).foreach { _ =>\n        Seq((\"a\", 1), (\"b\", 2), (null.asInstanceOf[String], 3), (\"\", 4)).toDF(\"part\", \"value\")\n          .write\n          .partitionBy(\"part\")\n          .format(\"delta\")\n          .mode(\"append\")\n          .save(tempDir.getAbsolutePath)\n      }\n\n      var df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n      val part = \"part\".phy(deltaLog)\n      var preOptInputFiles = groupInputFilesByPartition(df.inputFiles, deltaLog)\n      assert(preOptInputFiles.forall(_._2.length > 1))\n      assert(preOptInputFiles.keys.exists(_ == (part, nullPartitionValue)))\n\n      executeOptimizePath(tempDir.getAbsolutePath, Seq(\"value\"))\n\n      df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      preOptInputFiles = groupInputFilesByPartition(df.inputFiles, deltaLog)\n      assert(preOptInputFiles.forall(_._2.length == 1))\n      assert(preOptInputFiles.keys.exists(_ == (part, nullPartitionValue)))\n\n      checkAnswer(\n        df.groupBy('part).count(),\n        Seq(Row(\"a\", 5), Row(\"b\", 5), Row(null, 10))\n      )\n    }\n  }\n\n  test(\"optimize: Zorder on col name containing dot\") {\n    withTempDir { tempDir =>\n        (0.to(79).seq ++ 40.to(79).seq ++ 60.to(79).seq ++ 70.to(79).seq ++ 75.to(79).seq)\n          .toDF(\"id\")\n          .withColumn(\"flat.a\", $\"id\" + 1)\n          .write\n          .format(\"delta\")\n          .save(tempDir.toString)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val numFilesBefore = deltaLog.snapshot.numOfFiles\n        val res = executeOptimizePath(tempDir.getCanonicalPath, Seq(\"`flat.a`\"))\n        val metrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n        val numFilesAfter = deltaLog.snapshot.numOfFiles\n        assert(metrics.numFilesAdded === numFilesAfter)\n        assert(metrics.numFilesRemoved === numFilesBefore)\n    }\n  }\n\n  test(\"optimize: Zorder on a nested column\") {\n    withTempDir { tempDir =>\n        (0.to(79).seq ++ 40.to(79).seq ++ 60.to(79).seq ++ 70.to(79).seq ++ 75.to(79).seq)\n          .toDF(\"id\")\n          .withColumn(\"nested\", struct(struct('id + 2 as \"b\", 'id + 3 as \"c\") as \"sub\"))\n          .write\n          .format(\"delta\")\n          .save(tempDir.toString)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val numFilesBefore = deltaLog.snapshot.numOfFiles\n        val res = executeOptimizePath(tempDir.getCanonicalPath, Seq(\"nested.sub.c\"))\n        val metrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n        val numFilesAfter = deltaLog.snapshot.numOfFiles\n        assert(metrics.numFilesAdded === numFilesAfter)\n        assert(metrics.numFilesRemoved === numFilesBefore)\n    }\n  }\n\n  test(\"optimize: ZOrder on a column without stats\") {\n    withTempDir { tempDir =>\n      withSQLConf(\"spark.databricks.delta.properties.defaults.dataSkippingNumIndexedCols\" ->\n        \"1\", DELTA_OPTIMIZE_ZORDER_COL_STAT_CHECK.key -> \"true\") {\n        val data = Seq(1, 2, 3).toDF(\"id\")\n        data.withColumn(\"nested\",\n          struct(struct('id + 1 as \"p1\", 'id + 2 as \"p2\") as \"a\", 'id + 3 as \"b\"))\n          .write\n          .format(\"delta\")\n          .save(tempDir.getAbsolutePath)\n        val e1 = intercept[AnalysisException] {\n          executeOptimizeTable(s\"delta.`${tempDir.getPath}`\", Seq(\"nested.b\"))\n        }\n        assert(e1.getMessage == DeltaErrors\n          .zOrderingOnColumnWithNoStatsException(Seq[String](\"nested.b\"), spark)\n          .getMessage)\n        val e2 = intercept[AnalysisException] {\n          executeOptimizeTable(s\"delta.`${tempDir.getPath}`\", Seq(\"nested.a.p1\"))\n        }\n        assert(e2.getMessage == DeltaErrors\n          .zOrderingOnColumnWithNoStatsException(Seq[String](\"nested.a.p1\"), spark)\n          .getMessage)\n        val e3 = intercept[AnalysisException] {\n          executeOptimizeTable(s\"delta.`${tempDir.getPath}`\",\n            Seq(\"nested.a.p1\", \"nested.b\"))\n        }\n        assert(e3.getMessage == DeltaErrors\n          .zOrderingOnColumnWithNoStatsException(\n            Seq[String](\"nested.a.p1\", \"nested.b\"), spark)\n          .getMessage)\n      }\n    }\n  }\n\n  def optimizeWithBatching(\n      batchSize: String,\n      expectedCommits: Int,\n      condition: Option[String],\n      partitionFileCount: Map[String, Int]): Unit = {\n    withSQLConf(DELTA_OPTIMIZE_BATCH_SIZE.key -> batchSize) {\n      withTempDir { tempDir =>\n        def writeData(count: Int): Unit = {\n          spark.range(count).select('id, 'id % 5 as \"part\")\n            .coalesce(1)\n            .write\n            .partitionBy(\"part\")\n            .format(\"delta\")\n            .mode(\"append\")\n            .save(tempDir.getAbsolutePath)\n        }\n\n        writeData(10)\n        writeData(100)\n\n        val data = spark.read.format(\"delta\").load(tempDir.getAbsolutePath()).collect()\n\n        executeOptimizePath(tempDir.getAbsolutePath, Seq(\"id\"), condition)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n        checkAnswer(df, data)\n\n        val deltaLog = loadDeltaLog(tempDir.getAbsolutePath)\n\n        val commits = deltaLog.history.getHistory(None)\n        assert(commits.filter(_.operation == \"OPTIMIZE\").length == expectedCommits)\n\n        val files = groupInputFilesByPartition(df.inputFiles, deltaLog)\n        for ((part, fileCount) <- partitionFileCount) {\n          assert(files((\"part\", part)).length == fileCount)\n        }\n      }\n    }\n  }\n\n  test(\"optimize command with batching\") {\n    // Batch size of 1 byte means each bin will run in its own batch, and lead to 5 batches,\n    // one for each partition.\n    Seq((\"1\", 5), (\"1g\", 1)).foreach { case (batchSize, optimizeCommits) =>\n      // All partitions should be one file after optimizing\n      val partitionFileCount = (0 to 4).map(_.toString -> 1).toMap\n\n      optimizeWithBatching(batchSize, optimizeCommits, None, partitionFileCount)\n    }\n  }\n\n  test(\"optimize command with where clause and batching\") {\n    // Batch size of 1 byte means each bin will run in its own batch, and lead to 2 batches\n    // for the two partitions we are optimizing.\n    Seq((\"1\", 2), (\"1g\", 1)).foreach { case (batchSize, optimizeCommits) =>\n      // First two partitions should have 1 file, last 3 should have two\n      val partitionFileCount = Map(\n        \"0\" -> 1,\n        \"1\" -> 1,\n        \"2\" -> 2,\n        \"3\" -> 2,\n        \"4\" -> 2\n      )\n      val files = optimizeWithBatching(batchSize, optimizeCommits, Some(\"part <= 1\"),\n        partitionFileCount)\n    }\n  }\n\n  test(\"optimize an empty table with batching\") {\n    // Batch size of 1 byte means each bin will run in its own batch\n    withSQLConf(DELTA_OPTIMIZE_BATCH_SIZE.key -> \"1\") {\n      withTempDir { tempDir =>\n        DeltaTable.create(spark)\n          .location(tempDir.getAbsolutePath())\n          .addColumn(\"id\", LongType)\n          .addColumn(\"part\", LongType)\n          .partitionedBy(\"part\")\n          .execute()\n\n        // Just make sure it succeeds\n        executeOptimizePath(tempDir.getAbsolutePath, Seq(\"id\"))\n\n        assert(spark.read.format(\"delta\").load(tempDir.getAbsolutePath()).count() == 0)\n      }\n    }\n  }\n\n  statsTest(\"optimize command: interleaving\") {\n    def statsDF(deltaLog: DeltaLog): DataFrame = {\n      val (c1, c2, c3) = (\"c1\".phy(deltaLog), \"c2\".phy(deltaLog), \"c3\".phy(deltaLog))\n      getStatsDf(deltaLog, Seq(\n        $\"numRecords\",\n        struct($\"minValues.`$c1`\", $\"minValues.`$c2`\", $\"minValues.`$c3`\"),\n        struct($\"maxValues.`$c1`\", $\"maxValues.`$c2`\", $\"maxValues.`$c3`\")))\n    }\n\n    withTempDir { tempDir =>\n      val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n      {\n        val df = spark.range(100)\n            .map(i => (i, 99 - i, (i + 50) % 100))\n            .toDF(\"c1\", \"c2\", \"c3\")\n\n        df.repartitionByRange(4, $\"c1\", $\"c2\", $\"c3\")\n            .write\n            .format(\"delta\")\n            .save(tempDir.toString)\n      }\n      assert(deltaLog.snapshot.allFiles.count() == 4)\n      checkAnswer(statsDF(deltaLog), Seq(\n        Row(25, Row(0, 75, 50), Row(24, 99, 74)),\n        Row(25, Row(25, 50, 75), Row(49, 74, 99)),\n        Row(25, Row(50, 25, 0), Row(74, 49, 24)),\n        Row(25, Row(75, 0, 25), Row(99, 24, 49))))\n\n      withSQLConf(\n        DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> \"1000000\"\n      ) {\n        val res = executeOptimizePath(tempDir.getCanonicalPath, Seq(\"c1\", \"c2\", \"c3\"))\n        val metrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n        assert(metrics.zOrderStats.get.mergedFiles.num == 4)\n        assert(deltaLog.snapshot.allFiles.count() == 1)\n        checkAnswer(statsDF(deltaLog),\n                    Row(100, Row(0, 0, 0), Row(99, 99, 99)))\n      }\n\n      // I want to get 4 files again, in order for this to be comparable to the initial scenario\n      val maxFileSize = deltaLog.snapshot.allFiles.head().size / 4\n      withSQLConf(\n        DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> maxFileSize.toString\n      ) {\n        val res = executeOptimizePath(tempDir.getCanonicalPath, Seq(\"c1\", \"c2\", \"c3\"))\n        val metrics = res.select($\"metrics.*\").as[OptimizeMetrics].head()\n        val expectedFileCount = 4\n        val expectedStats: Seq[Row] = Seq(\n          Row(25, Row(0, 50, 50), Row(49, 99, 99)),\n          Row(25, Row(16, 20, 18), Row(79, 83, 85)),\n          Row(25, Row(36, 36, 0), Row(63, 63, 96)),\n          Row(25, Row(64, 0, 14), Row(99, 35, 49)))\n        assert(metrics.zOrderStats.get.mergedFiles.num == 1)\n        assert(deltaLog.snapshot.allFiles.count() == expectedFileCount)\n        checkAnswer(statsDF(deltaLog), expectedStats)\n      }\n    }\n  }\n}\n\n/**\n * Runs optimize compaction tests using OPTIMIZE SQL\n */\nclass OptimizeZOrderSQLSuite extends OptimizeZOrderSuiteBase\n  with DeltaSQLCommandTest {\n  import testImplicits._\n\n  def executeOptimizeTable(table: String, zOrderBy: Seq[String],\n      condition: Option[String] = None): DataFrame = {\n    val conditionClause = condition.map(c => s\"WHERE $c\").getOrElse(\"\")\n    val zOrderClause = s\"ZORDER BY (${zOrderBy.mkString(\", \")})\"\n    spark.sql(s\"OPTIMIZE $table $conditionClause $zOrderClause\")\n  }\n\n  def executeOptimizePath(path: String, zOrderBy: Seq[String],\n      condition: Option[String] = None): DataFrame = {\n    executeOptimizeTable(s\"'$path'\", zOrderBy, condition)\n  }\n\n  test(\"optimize command: no need for parenthesis\") {\n    withTempDir { tempDir =>\n      val df = spark.read.json(Seq(\"\"\"{\"a\":1,\"b\":{\"c\":2,\"d\":3}}\"\"\").toDS())\n      df.write.format(\"delta\").save(tempDir.toString)\n      spark.sql(s\"OPTIMIZE '${tempDir.getCanonicalPath}' ZORDER BY a, b.c\")\n    }\n  }\n}\n\n/**\n * Runs optimize compaction tests using OPTIMIZE Scala APIs\n */\nclass OptimizeZOrderScalaSuite extends OptimizeZOrderSuiteBase\n    with DeltaSQLCommandTest {\n\n\n  def executeOptimizeTable(table: String, zOrderBy: Seq[String],\n      condition: Option[String] = None): DataFrame = {\n    if (condition.isDefined) {\n      DeltaTable.forName(table).optimize().where(condition.get).executeZOrderBy(zOrderBy: _*)\n    } else {\n      DeltaTable.forName(table).optimize().executeZOrderBy(zOrderBy: _*)\n    }\n  }\n\n  def executeOptimizePath(path: String, zOrderBy: Seq[String],\n      condition: Option[String] = None): DataFrame = {\n    if (condition.isDefined) {\n      DeltaTable.forPath(path).optimize().where(condition.get).executeZOrderBy(zOrderBy: _*)\n    } else {\n      DeltaTable.forPath(path).optimize().executeZOrderBy(zOrderBy: _*)\n    }\n  }\n}\n\nclass OptimizeZOrderNameColumnMappingSuite extends OptimizeZOrderSQLSuite\n  with DeltaColumnMappingEnableNameMode\n  with DeltaColumnMappingTestUtils\n\nclass OptimizeZOrderIdColumnMappingSuite extends OptimizeZOrderSQLSuite\n  with DeltaColumnMappingEnableIdMode\n  with DeltaColumnMappingTestUtils\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/perf/OptimizeGeneratedColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.perf\n\nimport java.sql.Timestamp\nimport java.util.Locale\nimport java.util.concurrent.{CountDownLatch, TimeUnit}\n\nimport scala.util.matching.Regex\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\n\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.{DELTA_COLLECT_STATS, GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED}\nimport org.apache.spark.sql.delta.stats.PrepareDeltaScanBase\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{DataFrame, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.execution.{FileSourceScanExec, QueryExecution}\nimport org.apache.spark.sql.types.TimestampType\nimport org.apache.spark.util.ThreadUtils\nimport org.apache.spark.util.Utils\n\nclass OptimizeGeneratedColumnSuite extends GeneratedColumnTest {\n  import testImplicits._\n\n  private val regex = new Regex(s\"(\\\\S+)\\\\s(\\\\S+)\\\\sGENERATED\\\\sALWAYS\\\\sAS\\\\s\\\\((.*)\\\\s?\\\\)\",\n  \"col_name\", \"data_type\", \"generated_as\")\n\n  private def getPushedPartitionFilters(queryExecution: QueryExecution): Seq[Expression] = {\n    queryExecution.executedPlan.collectFirst {\n      case scan: FileSourceScanExec => scan.partitionFilters\n    }.getOrElse(Nil)\n  }\n\n  protected def insertInto(path: String, df: DataFrame) = {\n    df.write.format(\"delta\").mode(\"append\").save(path)\n  }\n\n  /**\n   * Verify we can recognize an `OptimizablePartitionExpression` and generate corresponding\n   * partition filters correctly.\n   *\n   * @param dataSchema DDL of the data columns\n   * @param partitionSchema DDL of the partition columns\n   * @param generatedColumns a map of generated partition columns defined using the above data\n   *                         columns\n   * @param expectedPartitionExpr the expected `OptimizablePartitionExpression` to be recognized\n   * @param auxiliaryTestName string to append to the generated test name\n   * @param expressionKey key to check for the optmizable expression if not the default first\n   *                      word in the data schema\n   * @param skipNested Whether to skip the nested variant of the test\n   * @param filterTestCases test cases for partition filters. The key is the data filter, and the\n   *                        value is the partition filters we should generate.\n   */\n  private def testOptimizablePartitionExpression(\n      dataSchema: String,\n      partitionSchema: String,\n      generatedColumns: Map[String, String],\n      expectedPartitionExpr: OptimizablePartitionExpression,\n      auxiliaryTestName: Option[String] = None,\n      expressionKey: Option[String] = None,\n      skipNested: Boolean = false,\n      filterTestCases: Seq[(String, Seq[String])]): Unit = {\n    test(expectedPartitionExpr.toString + auxiliaryTestName.getOrElse(\"\")) {\n      val normalCol = dataSchema.split(\" \")(0)\n\n      withTableName(\"optimizable_partition_expression\") { table =>\n        createTable(\n          table,\n          None,\n          s\"$dataSchema, $partitionSchema\",\n          generatedColumns,\n          generatedColumns.keys.toSeq\n        )\n\n        val metadata = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))._2.metadata\n        assert(metadata.optimizablePartitionExpressions(expressionKey.getOrElse(\n          normalCol).toLowerCase(Locale.ROOT)) == expectedPartitionExpr :: Nil)\n        filterTestCases.foreach { filterTestCase =>\n          val partitionFilters = getPushedPartitionFilters(\n            sql(s\"SELECT * from $table where ${filterTestCase._1}\").queryExecution)\n          assert(partitionFilters.map(_.sql) == filterTestCase._2)\n        }\n      }\n    }\n\n    if (!skipNested) {\n      test(expectedPartitionExpr.toString + auxiliaryTestName.getOrElse(\"\") + \" nested\") {\n        val normalCol = dataSchema.split(\" \")(0)\n        val nestedSchema = s\"nested struct<${dataSchema.replace(\" \", \": \")}>\"\n        val updatedGeneratedColumns =\n          generatedColumns.mapValues(v => v.replaceAll(s\"(?i)($normalCol)\", \"nested.$1\")).toMap\n\n        withTableName(\"optimizable_partition_expression\") { table =>\n          createTable(\n            table,\n            None,\n            s\"$nestedSchema, $partitionSchema\",\n            updatedGeneratedColumns,\n            updatedGeneratedColumns.keys.toSeq\n          )\n\n          val metadata = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))._2.metadata\n          val nestedColPath =\n            s\"nested.${expressionKey.getOrElse(normalCol).toLowerCase(Locale.ROOT)}\"\n          assert(metadata.optimizablePartitionExpressions(nestedColPath)\n            == expectedPartitionExpr :: Nil)\n          filterTestCases.foreach { filterTestCase =>\n            val updatedFilter = filterTestCase._1.replaceAll(s\"(?i)($normalCol)\", \"nested.$1\")\n            val partitionFilters = getPushedPartitionFilters(\n              sql(s\"SELECT * from $table where $updatedFilter\").queryExecution)\n            assert(partitionFilters.map(_.sql) == filterTestCase._2)\n          }\n        }\n      }\n    }\n  }\n\n  /** Format a human readable SQL filter into Spark's compact SQL format */\n  private def compactFilter(filter: String): String = {\n    filter.replaceAllLiterally(\"\\n\", \"\")\n      .replaceAll(\"(?<=\\\\)) +(?=\\\\))\", \"\")\n      .replaceAll(\"(?<=\\\\() +(?=\\\\()\", \"\")\n      .replaceAll(\"\\\\) +OR +\\\\(\", \") OR (\")\n      .replaceAll(\"\\\\) +AND +\\\\(\", \") AND (\")\n  }\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"date DATE\",\n    Map(\"date\" -> \"CAST(eventTime AS DATE)\"),\n    expectedPartitionExpr = DatePartitionExpr(\"date\"),\n    auxiliaryTestName = Option(\" from cast(timestamp)\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime <= '2021-01-01 18:00:00'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime = '2021-01-01 18:00:00'\" ->\n        Seq(\"((date = DATE '2021-01-01') \" +\n          \"OR ((date = DATE '2021-01-01') IS NULL))\"),\n      \"eventTime > '2021-01-01 18:00:00'\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime >= '2021-01-01 18:00:00'\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime is null\" -> Seq(\"(date IS NULL)\"),\n      // Verify we can reverse the order\n      \"'2021-01-01 18:00:00' > eventTime\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' >= eventTime\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' = eventTime\" ->\n        Seq(\"((date = DATE '2021-01-01') \" +\n          \"OR ((date = DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' < eventTime\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' <= eventTime\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      // Verify date type literal. In theory, the best filter should be date < DATE '2021-01-01'.\n      // But Spark's analyzer converts eventTime < '2021-01-01' to\n      // `eventTime` < TIMESTAMP '2021-01-01 00:00:00'. So it's the same as\n      // eventTime < '2021-01-01 18:00:00' for `OptimizeGeneratedColumn`.\n      \"eventTime < '2021-01-01'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventDate DATE\",\n    \"date DATE\",\n    Map(\"date\" -> \"CAST(eventDate AS DATE)\"),\n    expectedPartitionExpr = DatePartitionExpr(\"date\"),\n    auxiliaryTestName = Option(\" from cast(date)\"),\n    filterTestCases = Seq(\n      \"eventDate < '2021-01-01 18:00:00'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"eventDate <= '2021-01-01 18:00:00'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"eventDate = '2021-01-01 18:00:00'\" ->\n        Seq(\"((date = DATE '2021-01-01') \" +\n          \"OR ((date = DATE '2021-01-01') IS NULL))\"),\n      \"eventDate > '2021-01-01 18:00:00'\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"eventDate >= '2021-01-01 18:00:00'\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"eventDate is null\" -> Seq(\"(date IS NULL)\"),\n      // Verify we can reverse the order\n      \"'2021-01-01 18:00:00' > eventDate\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' >= eventDate\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' = eventDate\" ->\n        Seq(\"((date = DATE '2021-01-01') \" +\n          \"OR ((date = DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' < eventDate\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"'2021-01-01 18:00:00' <= eventDate\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      // Verify date type literal. In theory, the best filter should be date < DATE '2021-01-01'.\n      // But Spark's analyzer converts eventTime < '2021-01-01' to\n      // `eventTime` < TIMESTAMP '2021-01-01 00:00:00'. So it's the same as\n      // eventTime < '2021-01-01 18:00:00' for `OptimizeGeneratedColumn`.\n      \"eventDate < '2021-01-01'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n          \"OR ((date <= DATE '2021-01-01') IS NULL))\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"year INT, month INT, day INT, hour INT\",\n    Map(\n      \"year\" -> \"YEAR(eventTime)\",\n      \"month\" -> \"MONTH(eventTime)\",\n      \"day\" -> \"DAY(eventTime)\",\n      \"hour\" -> \"HOUR(eventTime)\"\n    ),\n    expectedPartitionExpr = YearMonthDayHourPartitionExpr(\"year\", \"month\", \"day\", \"hour\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (\n            |      (year < 2021)\n            |      OR\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month < 1)\n            |      )\n            |    )\n            |    OR\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day < 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day = 1)\n            |    )\n            |    AND\n            |    (hour <= 18)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime <= '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (\n            |      (year < 2021)\n            |      OR\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month < 1)\n            |      )\n            |    )\n            |    OR\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day < 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day = 1)\n            |    )\n            |    AND\n            |    (hour <= 18)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime = '2021-01-01 18:00:00'\" -> Seq(\n        \"(year = 2021)\",\n        \"(month = 1)\",\n        \"(day = 1)\",\n        \"(hour = 18)\"\n      ),\n      \"eventTime > '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (\n            |      (year > 2021)\n            |      OR\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month > 1)\n            |      )\n            |    )\n            |    OR\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day > 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day = 1)\n            |    )\n            |    AND\n            |    (hour >= 18)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime >= '2021-01-01 18:00:00'\" ->Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (\n            |      (year > 2021)\n            |      OR\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month > 1)\n            |      )\n            |    )\n            |    OR\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day > 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (\n            |        (year = 2021)\n            |        AND\n            |        (month = 1)\n            |      )\n            |      AND\n            |      (day = 1)\n            |    )\n            |    AND\n            |    (hour >= 18)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime is null\" -> Seq(\n        \"(year IS NULL)\",\n        \"(month IS NULL)\",\n        \"(day IS NULL)\",\n        \"(hour IS NULL)\"\n      )\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"year INT, month INT, day INT\",\n    Map(\n      \"year\" -> \"YEAR(eventTime)\",\n      \"month\" -> \"MONTH(eventTime)\",\n      \"day\" -> \"DAY(eventTime)\"\n    ),\n    expectedPartitionExpr = YearMonthDayPartitionExpr(\"year\", \"month\", \"day\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (year < 2021)\n            |    OR\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month < 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month = 1)\n            |    )\n            |    AND\n            |    (day <= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime <= '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (year < 2021)\n            |    OR\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month < 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month = 1)\n            |    )\n            |    AND\n            |    (day <= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime = '2021-01-01 18:00:00'\" -> Seq(\n        \"(year = 2021)\",\n        \"(month = 1)\",\n        \"(day = 1)\"\n      ),\n      \"eventTime > '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (year > 2021)\n            |    OR\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month > 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month = 1)\n            |    )\n            |    AND\n            |    (day >= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime >= '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (\n            |    (year > 2021)\n            |    OR\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month > 1)\n            |    )\n            |  )\n            |  OR\n            |  (\n            |    (\n            |      (year = 2021)\n            |      AND\n            |      (month = 1)\n            |    )\n            |    AND\n            |    (day >= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime is null\" -> Seq(\n        \"(year IS NULL)\",\n        \"(month IS NULL)\",\n        \"(day IS NULL)\"\n      )\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"year INT, month INT\",\n    // Use different cases to verify we can recognize the same column using different cases in\n    // generation expressions.\n    Map(\n      \"year\" -> \"YEAR(EVENTTIME)\",\n      \"month\" -> \"MONTH(eventTime)\"\n    ),\n    expectedPartitionExpr = YearMonthPartitionExpr(\"year\", \"month\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (year < 2021)\n            |  OR\n            |  (\n            |    (year = 2021)\n            |    AND\n            |    (month <= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime <= '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (year < 2021)\n            |  OR\n            |  (\n            |    (year = 2021)\n            |    AND\n            |    (month <= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime = '2021-01-01 18:00:00'\" -> Seq(\n        \"(year = 2021)\",\n        \"(month = 1)\"\n      ),\n      \"eventTime > '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (year > 2021)\n            |  OR\n            |  (\n            |    (year = 2021)\n            |    AND\n            |    (month >= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime >= '2021-01-01 18:00:00'\" -> Seq(\n        compactFilter(\n          \"\"\"(\n            |  (year > 2021)\n            |  OR\n            |  (\n            |    (year = 2021)\n            |    AND\n            |    (month >= 1)\n            |  )\n            |)\n            |\"\"\".stripMargin)),\n      \"eventTime is null\" -> Seq(\"(year IS NULL)\", \"(month IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"year INT\",\n    Map(\"year\" -> \"YEAR(eventTime)\"),\n    expectedPartitionExpr = YearPartitionExpr(\"year\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" ->\n        Seq(\"((year <= 2021) \" +\n          \"OR ((year <= 2021) IS NULL))\"),\n      \"eventTime <= '2021-01-01 18:00:00'\" ->\n        Seq(\"((year <= 2021) \" +\n          \"OR ((year <= 2021) IS NULL))\"),\n      \"eventTime = '2021-01-01 18:00:00'\" ->\n        Seq(\"((year = 2021) \" +\n          \"OR ((year = 2021) IS NULL))\"),\n      \"eventTime > '2021-01-01 18:00:00'\" ->\n        Seq(\"((year >= 2021) \" +\n          \"OR ((year >= 2021) IS NULL))\"),\n      \"eventTime >= '2021-01-01 18:00:00'\" ->\n        Seq(\"((year >= 2021) \" +\n          \"OR ((year >= 2021) IS NULL))\"),\n      \"eventTime is null\" -> Seq(\"(year IS NULL)\")\n    )\n  )\n\n  Seq((\"YEAR(eventDate)\", \" from year(date)\"),\n    (\"YEAR(CAST(eventDate AS DATE))\", \" from year(cast(date))\"))\n    .foreach { case (partCol, auxTestName) =>\n      testOptimizablePartitionExpression(\n        \"eventDate DATE\",\n        \"year INT\",\n        Map(\"year\" -> partCol),\n        expectedPartitionExpr = YearPartitionExpr(\"year\"),\n        auxiliaryTestName = Option(auxTestName),\n        filterTestCases = Seq(\n          \"eventDate < '2021-01-01'\" ->\n            Seq(\"((year <= 2021) \" +\n              \"OR ((year <= 2021) IS NULL))\"),\n          \"eventDate <= '2021-01-01'\" ->\n            Seq(\"((year <= 2021) \" +\n              \"OR ((year <= 2021) IS NULL))\"),\n          \"eventDate = '2021-01-01'\" ->\n            Seq(\"((year = 2021) \" +\n              \"OR ((year = 2021) IS NULL))\"),\n          \"eventDate > '2021-01-01'\" ->\n            Seq(\"((year >= 2021) \" +\n              \"OR ((year >= 2021) IS NULL))\"),\n          \"eventDate >= '2021-01-01'\" ->\n            Seq(\"((year >= 2021) \" +\n              \"OR ((year >= 2021) IS NULL))\"),\n          \"eventDate is null\" -> Seq(\"(year IS NULL)\")\n        )\n      )\n    }\n\n  testOptimizablePartitionExpression(\n    \"value STRING\",\n    \"substr STRING\",\n    Map(\"substr\" -> \"SUBSTRING(value, 2, 3)\"),\n    expectedPartitionExpr = SubstringPartitionExpr(\"substr\", 2, 3),\n    filterTestCases = Seq(\n      \"value < 'foo'\" -> Nil,\n      \"value <= 'foo'\" -> Nil,\n      \"value = 'foo'\" -> Seq(\"((substr IS NULL) OR (substr = 'oo'))\"),\n      \"value > 'foo'\" -> Nil,\n      \"value >= 'foo'\" -> Nil,\n      \"value is null\" -> Seq(\"(substr IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"value STRING\",\n    \"substr STRING\",\n    Map(\"substr\" -> \"SUBSTRING(value, 0, 3)\"),\n    expectedPartitionExpr = SubstringPartitionExpr(\"substr\", 0, 3),\n    filterTestCases = Seq(\n      \"value < 'foo'\" -> Seq(\"((substr IS NULL) OR (substr <= 'foo'))\"),\n      \"value <= 'foo'\" -> Seq(\"((substr IS NULL) OR (substr <= 'foo'))\"),\n      \"value = 'foo'\" -> Seq(\"((substr IS NULL) OR (substr = 'foo'))\"),\n      \"value > 'foo'\" -> Seq(\"((substr IS NULL) OR (substr >= 'foo'))\"),\n      \"value >= 'foo'\" -> Seq(\"((substr IS NULL) OR (substr >= 'foo'))\"),\n      \"value is null\" -> Seq(\"(substr IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"value STRING\",\n    \"substr STRING\",\n    Map(\"substr\" -> \"SUBSTRING(value, 1, 3)\"),\n    expectedPartitionExpr = SubstringPartitionExpr(\"substr\", 1, 3),\n    filterTestCases = Seq(\n      \"value < 'foo'\" -> Seq(\"((substr IS NULL) OR (substr <= 'foo'))\"),\n      \"value <= 'foo'\" -> Seq(\"((substr IS NULL) OR (substr <= 'foo'))\"),\n      \"value = 'foo'\" -> Seq(\"((substr IS NULL) OR (substr = 'foo'))\"),\n      \"value > 'foo'\" -> Seq(\"((substr IS NULL) OR (substr >= 'foo'))\"),\n      \"value >= 'foo'\" -> Seq(\"((substr IS NULL) OR (substr >= 'foo'))\"),\n      \"value is null\" -> Seq(\"(substr IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"value STRING\",\n    \"`my.substr` STRING\",\n    Map(\"my.substr\" -> \"SUBSTRING(value, 1, 3)\"),\n    expectedPartitionExpr = SubstringPartitionExpr(\"my.substr\", 1, 3),\n    filterTestCases = Seq(\n      \"value < 'foo'\" -> Seq(\"((`my.substr` IS NULL) OR (`my.substr` <= 'foo'))\"),\n      \"value <= 'foo'\" -> Seq(\"((`my.substr` IS NULL) OR (`my.substr` <= 'foo'))\"),\n      \"value = 'foo'\" -> Seq(\"((`my.substr` IS NULL) OR (`my.substr` = 'foo'))\"),\n      \"value > 'foo'\" -> Seq(\"((`my.substr` IS NULL) OR (`my.substr` >= 'foo'))\"),\n      \"value >= 'foo'\" -> Seq(\"((`my.substr` IS NULL) OR (`my.substr` >= 'foo'))\"),\n      \"value is null\" -> Seq(\"(`my.substr` IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"outer struct<inner struct<nested: struct<value:STRING>>>\",\n    \"substr STRING\",\n    Map(\"substr\" -> \"SUBSTRING(outer.inner.nested.value, 1, 3)\"),\n    expectedPartitionExpr = SubstringPartitionExpr(\"substr\", 1, 3),\n    auxiliaryTestName = Some(\" deeply nested\"),\n    expressionKey = Some(\"outer.inner.nested.value\"),\n    skipNested = true,\n    filterTestCases = Seq(\n      \"outer.inner.nested.value < 'foo'\" ->\n        Seq(\"((substr IS NULL) OR (substr <= 'foo'))\"),\n      \"outer.inner.nested.value <= 'foo'\" ->\n        Seq(\"((substr IS NULL) OR (substr <= 'foo'))\"),\n      \"outer.inner.nested.value = 'foo'\" ->\n        Seq(\"((substr IS NULL) OR (substr = 'foo'))\"),\n      \"outer.inner.nested.value > 'foo'\" ->\n        Seq(\"((substr IS NULL) OR (substr >= 'foo'))\"),\n      \"outer.inner.nested.value >= 'foo'\" ->\n        Seq(\"((substr IS NULL) OR (substr >= 'foo'))\"),\n      \"outer.inner.nested.value is null\" -> Seq(\"(substr IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"eventTimeTrunc TIMESTAMP\",\n    Map(\"eventTimeTrunc\" -> \"date_trunc('YEAR', eventTime)\"),\n    expectedPartitionExpr = TimestampTruncPartitionExpr(\"YEAR\", \"eventTimeTrunc\"),\n    auxiliaryTestName = Option(\" from date_trunc(timestamp)\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventTime <= '2021-01-01 18:00:00'\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventTime = '2021-01-01 18:00:00'\" ->\n        Seq(\"((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventTime > '2021-01-01 18:00:00'\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventTime >= '2021-01-01 18:00:00'\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventTime is null\" -> Seq(\"(eventTimeTrunc IS NULL)\"),\n      // Verify we can reverse the order\n      \"'2021-01-01 18:00:00' > eventTime\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01 18:00:00' >= eventTime\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01 18:00:00' = eventTime\" ->\n        Seq(\"((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01 18:00:00' < eventTime\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01 18:00:00' <= eventTime\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n          \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventDate DATE\",\n    \"eventTimeTrunc TIMESTAMP\",\n    Map(\"eventTimeTrunc\" -> \"date_trunc('DD', eventDate)\"),\n    expectedPartitionExpr = TimestampTruncPartitionExpr(\"DD\", \"eventTimeTrunc\"),\n    auxiliaryTestName = Option(\" from date_trunc(cast(date))\"),\n    filterTestCases = Seq(\n      \"eventDate < '2021-01-01'\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventDate <= '2021-01-01'\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventDate = '2021-01-01'\" ->\n        Seq(\"((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventDate > '2021-01-01'\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventDate >= '2021-01-01'\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"eventDate is null\" -> Seq(\"(eventTimeTrunc IS NULL)\"),\n      // Verify we can reverse the order\n      \"'2021-01-01' > eventDate\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01' >= eventDate\" ->\n        Seq(\"((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc <= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01' = eventDate\" ->\n        Seq(\"((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc = TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01' < eventDate\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\"),\n      \"'2021-01-01' <= eventDate\" ->\n        Seq(\"((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') \" +\n      \"OR ((eventTimeTrunc >= TIMESTAMP '2021-01-01 00:00:00') IS NULL))\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"value STRING\",\n    \"part STRING\",\n    Map(\"part\" -> \"value\"),\n    expectedPartitionExpr = IdentityPartitionExpr(\"part\"),\n    expressionKey = Some(\"value\"),\n    filterTestCases = Seq(\n      \"value < 'foo'\" -> Seq(\"((part IS NULL) OR (part < 'foo'))\"),\n      \"value <= 'foo'\" -> Seq(\"((part IS NULL) OR (part <= 'foo'))\"),\n      \"value = 'foo'\" -> Seq(\"((part IS NULL) OR (part = 'foo'))\"),\n      \"value > 'foo'\" -> Seq(\"((part IS NULL) OR (part > 'foo'))\"),\n      \"value >= 'foo'\" -> Seq(\"((part IS NULL) OR (part >= 'foo'))\"),\n      \"value is null\" -> Seq(\"(part IS NULL)\")\n    )\n  )\n\n  /**\n   * In order to distinguish between field names with periods and nested field names,\n   * fields with periods must be escaped. Otherwise in the example below, there's\n   * no way to tell whether a filter on nested.value should be applied to part1 or part2.\n   */\n  testOptimizablePartitionExpression(\n    \"`nested.value` STRING, nested struct<value: STRING>\",\n    \"part1 STRING, part2 STRING\",\n    Map(\"part1\" -> \"`nested.value`\", \"part2\" -> \"nested.value\"),\n    auxiliaryTestName = Some(\" escaped field names\"),\n    expectedPartitionExpr = IdentityPartitionExpr(\"part1\"),\n    expressionKey = Some(\"`nested.value`\"),\n    skipNested = true,\n    filterTestCases = Seq(\n      \"`nested.value` < 'foo'\" -> Seq(\"((part1 IS NULL) OR (part1 < 'foo'))\"),\n      \"`nested.value` <= 'foo'\" -> Seq(\"((part1 IS NULL) OR (part1 <= 'foo'))\"),\n      \"`nested.value` = 'foo'\" -> Seq(\"((part1 IS NULL) OR (part1 = 'foo'))\"),\n      \"`nested.value` > 'foo'\" -> Seq(\"((part1 IS NULL) OR (part1 > 'foo'))\"),\n      \"`nested.value` >= 'foo'\" -> Seq(\"((part1 IS NULL) OR (part1 >= 'foo'))\"),\n      \"`nested.value` is null\" -> Seq(\"(part1 IS NULL)\")\n    )\n  )\n\n  test(\"end-to-end optimizable partition expression\") {\n    withTempDir { tempDir =>\n      withTableName(\"optimizable_partition_expression\") { table =>\n\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 INT, c2 TIMESTAMP, c3 DATE\",\n          Map(\"c3\" -> \"CAST(c2 AS DATE)\"),\n          Seq(\"c3\")\n        )\n\n          Seq(\n            Tuple2(1, \"2020-12-31 11:00:00\"),\n            Tuple2(2, \"2021-01-01 12:00:00\"),\n            Tuple2(3, \"2021-01-02 13:00:00\")\n          ).foreach { values =>\n            insertInto(\n              tempDir.getCanonicalPath,\n              Seq(values).toDF(\"c1\", \"c2\")\n                .withColumn(\"c2\", $\"c2\".cast(TimestampType))\n            )\n          }\n        assert(tempDir.listFiles().map(_.getName).toSet ==\n          Set(\"c3=2021-01-01\", \"c3=2021-01-02\", \"c3=2020-12-31\", \"_delta_log\"))\n        // Delete folders which should not be read if we generate the partition filters correctly\n        tempDir.listFiles().foreach { f =>\n          if (f.getName != \"c3=2021-01-01\" && f.getName != \"_delta_log\") {\n            Utils.deleteRecursively(f)\n          }\n        }\n        assert(tempDir.listFiles().map(_.getName).toSet == Set(\"c3=2021-01-01\", \"_delta_log\"))\n        checkAnswer(\n          sql(s\"select * from $table where \" +\n            s\"c2 >= '2021-01-01 12:00:00' AND c2 <= '2021-01-01 18:00:00'\"),\n          Row(2, sqlTimestamp(\"2021-01-01 12:00:00\"), sqlDate(\"2021-01-01\")))\n        // Verify `OptimizeGeneratedColumn` doesn't mess up Projects.\n        checkAnswer(\n          sql(s\"select c1 from $table where \" +\n            s\"c2 >= '2021-01-01 12:00:00' AND c2 <= '2021-01-01 18:00:00'\"),\n          Row(2))\n\n        // Check both projection orders to make sure projection orders are handled correctly\n        checkAnswer(\n          sql(s\"select c1, c2 from $table where \" +\n            s\"c2 >= '2021-01-01 12:00:00' AND c2 <= '2021-01-01 18:00:00'\"),\n          Row(2, Timestamp.valueOf(\"2021-01-01 12:00:00\")))\n        checkAnswer(\n          sql(s\"select c2, c1 from $table where \" +\n            s\"c2 >= '2021-01-01 12:00:00' AND c2 <= '2021-01-01 18:00:00'\"),\n          Row(Timestamp.valueOf(\"2021-01-01 12:00:00\"), 2))\n\n        // Verify the optimization works for limit.\n        val limitQuery = sql(\n          s\"\"\"select * from $table\n             |where c2 >= '2021-01-01 12:00:00' AND c2 <= '2021-01-01 18:00:00'\n             |limit 10\"\"\".stripMargin)\n        val expectedPartitionFilters = Seq(\n          \"((c3 >= DATE '2021-01-01') OR ((c3 >= DATE '2021-01-01') IS NULL))\",\n          \"((c3 <= DATE '2021-01-01') OR ((c3 <= DATE '2021-01-01') IS NULL))\"\n        )\n        assert(expectedPartitionFilters ==\n          getPushedPartitionFilters(limitQuery.queryExecution).map(_.sql))\n        checkAnswer(limitQuery, Row(2, sqlTimestamp(\"2021-01-01 12:00:00\"), sqlDate(\"2021-01-01\")))\n      }\n    }\n  }\n\n  test(\"empty string and null ambiguity in a partition column\") {\n    withTempDir { tempDir =>\n      withTableName(\"optimizable_partition_expression\") { table =>\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 STRING, c2 STRING\",\n          Map(\"c2\" -> \"SUBSTRING(c1, 1, 4)\"),\n          Seq(\"c2\")\n        )\n        insertInto(\n          tempDir.getCanonicalPath,\n          Seq(Tuple1(\"\")).toDF(\"c1\")\n        )\n        checkAnswer(\n          sql(s\"select * from $table where c1 = ''\"),\n          Row(\"\", null))\n        // The following check shows the weird behavior of SPARK-24438 and confirms the generated\n        // partition filter doesn't impact the answer.\n        withSQLConf(GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED.key -> \"false\") {\n          checkAnswer(\n            sql(s\"select * from $table where c1 = ''\"),\n            Row(\"\", null))\n        }\n      }\n    }\n  }\n\n  test(\"substring on multibyte characters\") {\n    withTempDir { tempDir =>\n      withTableName(\"multibyte_characters\") { table =>\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 STRING, c2 STRING\",\n          Map(\"c2\" -> \"SUBSTRING(c1, 1, 2)\"),\n          Seq(\"c2\")\n        )\n        // scalastyle:off nonascii\n        insertInto(\n          tempDir.getCanonicalPath,\n          Seq(Tuple1(\"一二三四\")).toDF(\"c1\")\n        )\n        val testQuery = s\"select * from $table where c1 > 'abcd'\"\n        assert(\"((c2 IS NULL) OR (c2 >= 'ab'))\" :: Nil ==\n          getPushedPartitionFilters(sql(testQuery).queryExecution).map(_.sql))\n        checkAnswer(\n          sql(testQuery),\n          Row(\"一二三四\", \"一二\"))\n        // scalastyle:on nonascii\n      }\n    }\n  }\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"month STRING\",\n    Map(\"month\" -> \"DATE_FORMAT(eventTime, 'yyyy-MM')\"),\n    expectedPartitionExpr = DateFormatPartitionExpr(\"month\", \"yyyy-MM\"),\n    auxiliaryTestName = Option(\" from timestamp\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) IS NULL))\"),\n      \"eventTime <= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) IS NULL))\"),\n      \"eventTime = '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') = 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') = 1622530800L) IS NULL))\"),\n      \"eventTime > '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) IS NULL))\"),\n      \"eventTime >= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) IS NULL))\"),\n      \"eventTime is null\" -> Seq(\"(month IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventDate DATE\",\n    \"month STRING\",\n    Map(\"month\" -> \"DATE_FORMAT(eventDate, 'yyyy-MM')\"),\n    expectedPartitionExpr = DateFormatPartitionExpr(\"month\", \"yyyy-MM\"),\n    auxiliaryTestName = Option(\" from cast(date)\"),\n    filterTestCases = Seq(\n      \"eventDate < '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) IS NULL))\"),\n      \"eventDate <= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') <= 1622530800L) IS NULL))\"),\n      \"eventDate = '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') = 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') = 1622530800L) IS NULL))\"),\n      \"eventDate > '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) IS NULL))\"),\n      \"eventDate >= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) \" +\n          \"OR ((unix_timestamp(month, 'yyyy-MM') >= 1622530800L) IS NULL))\"),\n      \"eventDate is null\" -> Seq(\"(month IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"day STRING\",\n    Map(\"day\" -> \"DATE_FORMAT(eventTime, 'yyyy-MM-dd')\"),\n    expectedPartitionExpr = DateFormatPartitionExpr(\"day\", \"yyyy-MM-dd\"),\n    auxiliaryTestName = Option(\" from timestamp\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(day, 'yyyy-MM-dd') <= 1624863600L) \" +\n          \"OR ((unix_timestamp(day, 'yyyy-MM-dd') <= 1624863600L) IS NULL))\"),\n      \"eventTime <= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(day, 'yyyy-MM-dd') <= 1624863600L) \" +\n          \"OR ((unix_timestamp(day, 'yyyy-MM-dd') <= 1624863600L) IS NULL))\"),\n      \"eventTime = '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(day, 'yyyy-MM-dd') = 1624863600L) \" +\n          \"OR ((unix_timestamp(day, 'yyyy-MM-dd') = 1624863600L) IS NULL))\"),\n      \"eventTime > '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(day, 'yyyy-MM-dd') >= 1624863600L) \" +\n          \"OR ((unix_timestamp(day, 'yyyy-MM-dd') >= 1624863600L) IS NULL))\"),\n      \"eventTime >= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(day, 'yyyy-MM-dd') >= 1624863600L) \" +\n          \"OR ((unix_timestamp(day, 'yyyy-MM-dd') >= 1624863600L) IS NULL))\"),\n      \"eventTime is null\" -> Seq(\"(day IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"hour STRING\",\n    Map(\"hour\" -> \"(DATE_FORMAT(eventTime, 'yyyy-MM-dd-HH'))\"),\n    expectedPartitionExpr = DateFormatPartitionExpr(\"hour\", \"yyyy-MM-dd-HH\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(hour, 'yyyy-MM-dd-HH') <= 1624928400L) \" +\n          \"OR ((unix_timestamp(hour, 'yyyy-MM-dd-HH') <= 1624928400L) IS NULL))\"),\n      \"eventTime <= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(hour, 'yyyy-MM-dd-HH') <= 1624928400L) \" +\n          \"OR ((unix_timestamp(hour, 'yyyy-MM-dd-HH') <= 1624928400L) IS NULL))\"),\n      \"eventTime = '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(hour, 'yyyy-MM-dd-HH') = 1624928400L) \" +\n          \"OR ((unix_timestamp(hour, 'yyyy-MM-dd-HH') = 1624928400L) IS NULL))\"),\n      \"eventTime > '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(hour, 'yyyy-MM-dd-HH') >= 1624928400L) \" +\n          \"OR ((unix_timestamp(hour, 'yyyy-MM-dd-HH') >= 1624928400L) IS NULL))\"),\n      \"eventTime >= '2021-06-28 18:00:00'\" ->\n        Seq(\"((unix_timestamp(hour, 'yyyy-MM-dd-HH') >= 1624928400L) \" +\n          \"OR ((unix_timestamp(hour, 'yyyy-MM-dd-HH') >= 1624928400L) IS NULL))\"),\n      \"eventTime is null\" -> Seq(\"(hour IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventTime TIMESTAMP\",\n    \"date DATE\",\n    Map(\"date\" -> \"(trunc(eventTime, 'year'))\"),\n    expectedPartitionExpr = TruncDatePartitionExpr(\"date\", \"year\"),\n    filterTestCases = Seq(\n      \"eventTime < '2021-01-01 18:00:00'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n        \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime <= '2021-01-01 18:00:00'\" ->\n        Seq(\"((date <= DATE '2021-01-01') \" +\n        \"OR ((date <= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime = '2021-01-01 18:00:00'\" ->\n        Seq(\"((date = DATE '2021-01-01') \" +\n          \"OR ((date = DATE '2021-01-01') IS NULL))\"),\n      \"eventTime > '2021-01-01 18:00:00'\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime >= '2021-01-01 18:00:00'\" ->\n        Seq(\"((date >= DATE '2021-01-01') \" +\n          \"OR ((date >= DATE '2021-01-01') IS NULL))\"),\n      \"eventTime is null\" ->\n        Seq(\"(date IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventDate DATE\",\n    \"date DATE\",\n    Map(\"date\" -> \"(trunc(eventDate, 'month'))\"),\n    expectedPartitionExpr = TruncDatePartitionExpr(\"date\", \"month\"),\n    filterTestCases = Seq(\n      \"eventDate < '2021-12-01'\" ->\n        Seq(\"((date <= DATE '2021-12-01') \" +\n          \"OR ((date <= DATE '2021-12-01') IS NULL))\"),\n      \"eventDate <= '2021-12-01'\" ->\n        Seq(\"((date <= DATE '2021-12-01') \" +\n          \"OR ((date <= DATE '2021-12-01') IS NULL))\"),\n      \"eventDate = '2021-12-01'\" ->\n        Seq(\"((date = DATE '2021-12-01') \" +\n          \"OR ((date = DATE '2021-12-01') IS NULL))\"),\n      \"eventDate > '2021-12-01'\" ->\n        Seq(\"((date >= DATE '2021-12-01') \" +\n          \"OR ((date >= DATE '2021-12-01') IS NULL))\"),\n      \"eventDate >= '2021-12-01'\" ->\n        Seq(\"((date >= DATE '2021-12-01') \" +\n          \"OR ((date >= DATE '2021-12-01') IS NULL))\"),\n      \"eventDate is null\" ->\n        Seq(\"(date IS NULL)\")\n    )\n  )\n\n  testOptimizablePartitionExpression(\n    \"eventDateStr STRING\",\n    \"date DATE\",\n    Map(\"date\" -> \"(trunc(eventDateStr, 'quarter'))\"),\n    expectedPartitionExpr = TruncDatePartitionExpr(\"date\", \"quarter\"),\n    filterTestCases = Seq(\n      \"eventDateStr < '2022-04-01'\" ->\n        Seq(\"((date <= DATE '2022-04-01') \" +\n          \"OR ((date <= DATE '2022-04-01') IS NULL))\"),\n      \"eventDateStr <= '2022-04-01'\" ->\n        Seq(\"((date <= DATE '2022-04-01') \" +\n          \"OR ((date <= DATE '2022-04-01') IS NULL))\"),\n      \"eventDateStr = '2022-04-01'\" ->\n        Seq(\"((date = DATE '2022-04-01') \" +\n          \"OR ((date = DATE '2022-04-01') IS NULL))\"),\n      \"eventDateStr > '2022-04-01'\" ->\n        Seq(\"((date >= DATE '2022-04-01') \" +\n          \"OR ((date >= DATE '2022-04-01') IS NULL))\"),\n      \"eventDateStr >= '2022-04-01'\" ->\n        Seq(\"((date >= DATE '2022-04-01') \" +\n          \"OR ((date >= DATE '2022-04-01') IS NULL))\"),\n      \"eventDateStr is null\" ->\n        Seq(\"(date IS NULL)\")\n    )\n  )\n\n  test(\"five digits year in a year month day partition column\") {\n    withTempDir { tempDir =>\n      withTableName(\"optimizable_partition_expression\") { table =>\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 TIMESTAMP, c2 INT, c3 INT, c4 INT\",\n          Map(\n            \"c2\" -> \"YEAR(c1)\",\n            \"c3\" -> \"MONTH(c1)\",\n            \"c4\" -> \"DAY(c1)\"\n          ),\n          Seq(\"c2\", \"c3\", \"c4\")\n        )\n        insertInto(\n          tempDir.getCanonicalPath,\n          Seq(Tuple1(\"12345-07-15 18:00:00\"))\n            .toDF(\"c1\")\n            .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n        )\n\n        checkAnswer(\n          sql(s\"select * from $table where c1 = CAST('12345-07-15 18:00:00' as timestamp)\"),\n          Row(new Timestamp(327420320400000L), 12345, 7, 15))\n        withSQLConf(GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED.key -> \"false\") {\n          checkAnswer(\n            sql(s\"select * from $table where c1 = CAST('12345-07-15 18:00:00' as timestamp)\"),\n            Row(new Timestamp(327420320400000L), 12345, 7, 15))\n        }\n      }\n    }\n  }\n\n  test(\"five digits year in a date_format yyyy-MM partition column\") {\n    withTempDir { tempDir =>\n      withTableName(\"optimizable_partition_expression\") { table =>\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 TIMESTAMP, c2 STRING\",\n          Map(\"c2\" -> \"DATE_FORMAT(c1, 'yyyy-MM')\"),\n          Seq(\"c2\")\n        )\n        insertInto(\n          tempDir.getCanonicalPath,\n          Seq(Tuple1(\"12345-07-15 18:00:00\"))\n            .toDF(\"c1\")\n            .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n        )\n\n        checkAnswer(\n          sql(s\"select * from $table where c1 = CAST('12345-07-15 18:00:00' as timestamp)\"),\n          Row(new Timestamp(327420320400000L), \"+12345-07\"))\n        withSQLConf(GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED.key -> \"false\") {\n          checkAnswer(\n            sql(s\"select * from $table where c1 = CAST('12345-07-15 18:00:00' as timestamp)\"),\n            Row(new Timestamp(327420320400000L), \"+12345-07\"))\n        }\n      }\n    }\n  }\n\n  test(\"five digits year in a date_format yyyy-MM-dd-HH partition column\") {\n    withTempDir { tempDir =>\n      withTableName(\"optimizable_partition_expression\") { table =>\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 TIMESTAMP, c2 STRING\",\n          Map(\"c2\" -> \"DATE_FORMAT(c1, 'yyyy-MM-dd-HH')\"),\n          Seq(\"c2\")\n        )\n        insertInto(\n          tempDir.getCanonicalPath,\n          Seq(Tuple1(\"12345-07-15 18:00:00\"))\n            .toDF(\"c1\")\n            .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n        )\n\n        checkAnswer(\n          sql(s\"select * from $table where c1 = CAST('12345-07-15 18:00:00' as timestamp)\"),\n          Row(new Timestamp(327420320400000L), \"+12345-07-15-18\"))\n        withSQLConf(GENERATED_COLUMN_PARTITION_FILTER_OPTIMIZATION_ENABLED.key -> \"false\") {\n          checkAnswer(\n            sql(s\"select * from $table where c1 = CAST('12345-07-15 18:00:00' as timestamp)\"),\n            Row(new Timestamp(327420320400000L), \"+12345-07-15-18\"))\n        }\n      }\n    }\n  }\n\n  test(\"end-to-end test of behaviors of write/read null on partition column\") {\n    //              unix_timestamp('12345-12', 'yyyy-MM') | unix_timestamp('+12345-12', 'yyyy-MM')\n    //  EXCEPTION               fail                     |           327432240000\n    //  CORRECTED               null                     |           327432240000\n    //  LEGACY               327432240000                |               null\n    withTempDir { tempDir =>\n      withTableName(\"optimizable_partition_expression\") { table =>\n        createTable(\n          table,\n          Some(tempDir.getCanonicalPath),\n          \"c1 TIMESTAMP, c2 STRING\",\n          Map(\"c2\" -> \"DATE_FORMAT(c1, 'yyyy-MM')\"),\n          Seq(\"c2\")\n        )\n\n        // write in LEGACY\n        withSQLConf(\n        \"spark.sql.legacy.timeParserPolicy\" -> \"CORRECTED\"\n        ) {\n          insertInto(\n            tempDir.getCanonicalPath,\n            Seq(Tuple1(\"12345-07-01 00:00:00\"))\n              .toDF(\"c1\")\n              .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n          )\n          insertInto(\n            tempDir.getCanonicalPath,\n            Seq(Tuple1(\"+23456-07-20 18:30:00\"))\n              .toDF(\"c1\")\n              .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n          )\n        }\n\n        // write in LEGACY\n        withSQLConf(\n          \"spark.sql.legacy.timeParserPolicy\" -> \"LEGACY\"\n        ) {\n          insertInto(\n            tempDir.getCanonicalPath,\n            Seq(Tuple1(\"+12349-07-01 00:00:00\"))\n              .toDF(\"c1\")\n              .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n          )\n          insertInto(\n            tempDir.getCanonicalPath,\n            Seq(Tuple1(\"+30000-12-30 20:00:00\"))\n              .toDF(\"c1\")\n              .withColumn(\"c1\", $\"c1\".cast(TimestampType))\n          )\n        }\n\n        // we have partitions based on CORRECTED + LEGACY parser (with +)\n        assert(tempDir.listFiles().map(_.getName).toSet ==\n          Set(\"c2=+23456-07\", \"c2=12349-07\", \"c2=30000-12\", \"c2=+12345-07\", \"_delta_log\"))\n\n        // read behaviors in CORRECTED, we still can query correctly\n        withSQLConf(\"spark.sql.legacy.timeParserPolicy\" -> \"CORRECTED\") {\n          checkAnswer(\n            sql(s\"select (unix_timestamp('+20000-01', 'yyyy-MM')) as value\"),\n            Row(568971849600L)\n          )\n          withSQLConf(\"spark.sql.ansi.enabled\" -> \"false\") {\n            checkAnswer(\n              sql(s\"select (unix_timestamp('20000-01', 'yyyy-MM')) as value\"),\n              Row(null)\n            )\n            checkAnswer(\n              sql(s\"select * from $table where \" +\n                s\"c1 >= '20000-01-01 12:00:00'\"),\n              // 23456-07-20 18:30:00\n              Row(new Timestamp(678050098200000L), \"+23456-07\") ::\n                // 30000-12-30 20:00:00\n                Row(new Timestamp(884572891200000L), \"30000-12\") :: Nil\n            )\n          }\n        }\n\n        // read behaviors in LEGACY, we still can query correctly\n        withSQLConf(\"spark.sql.legacy.timeParserPolicy\" -> \"LEGACY\") {\n          checkAnswer(\n            sql(s\"select (unix_timestamp('20000-01', 'yyyy-MM')) as value\"),\n            Row(568971849600L)\n          )\n          withSQLConf(\"spark.sql.ansi.enabled\" -> \"false\") {\n            checkAnswer(\n              sql(s\"select (unix_timestamp('+20000-01', 'yyyy-MM')) as value\"),\n              Row(null)\n            )\n            checkAnswer(\n              sql(s\"select * from $table where \" +\n                s\"c1 >= '20000-01-01 12:00:00'\"),\n              // 23456-07-20 18:30:00\n              Row(new Timestamp(678050098200000L), \"+23456-07\") ::\n                // 30000-12-30 20:00:00\n                Row(new Timestamp(884572891200000L), \"30000-12\") :: Nil\n            )\n          }\n        }\n      }\n    }\n  }\n\n  test(\"generated partition filters should avoid conflicts\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      withTableName(\"avoid_conflicts\") { table =>\n        createTable(\n          table,\n          Some(path),\n          \"eventTime TIMESTAMP, date DATE\",\n          Map(\"date\" -> \"CAST(eventTime AS DATE)\"),\n          Seq(\"date\")\n        )\n        insertInto(\n          path,\n          Seq(Tuple1(\"2021-01-01 00:00:00\"), Tuple1(\"2021-01-02 00:00:00\"))\n            .toDF(\"eventTime\")\n            .withColumn(\"eventTime\", $\"eventTime\".cast(TimestampType))\n        )\n\n        val unblockQueries = new CountDownLatch(1)\n        val waitForAllQueries = new CountDownLatch(2)\n\n        PrepareDeltaScanBase.withCallbackOnGetDeltaScanGenerator(_ => {\n          waitForAllQueries.countDown()\n          assert(\n            unblockQueries.await(30, TimeUnit.SECONDS),\n            \"the main thread didn't wake up queries\")\n        }) {\n          val threadPool = ThreadUtils.newDaemonFixedThreadPool(2, \"test\")\n          try {\n            // Run two queries that should not conflict with each other if we generate the partition\n            // filter correctly.\n            val f1 = threadPool.submit(() => {\n              spark.read.format(\"delta\").load(path).where(\"eventTime = '2021-01-01 00:00:00'\")\n                .write.mode(\"append\").format(\"delta\").save(path)\n              true\n            })\n            val f2 = threadPool.submit(() => {\n              spark.read.format(\"delta\").load(path).where(\"eventTime = '2021-01-02 00:00:00'\")\n                .write.mode(\"append\").format(\"delta\").save(path)\n              true\n            })\n            assert(\n              waitForAllQueries.await(30, TimeUnit.SECONDS),\n              \"queries didn't finish before timeout\")\n            unblockQueries.countDown()\n            f1.get(30, TimeUnit.SECONDS)\n            f2.get(30, TimeUnit.SECONDS)\n          } finally {\n            threadPool.shutdownNow()\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/perf/OptimizeMetadataOnlyDeltaQuerySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.perf\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.{DeletionVectorsTestUtils, DeltaColumnMappingEnableIdMode, DeltaColumnMappingEnableNameMode, DeltaLog, DeltaTestUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.PrepareDeltaScanBase\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\nimport org.apache.spark.sql.delta.test.DeltaColumnMappingSelectedTestMixin\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport io.delta.tables.DeltaTable\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.BeforeAndAfterAll\n\nimport org.apache.spark.sql.{DataFrame, Dataset, QueryTest, Row, SaveMode}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.plans.logical.LocalRelation\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass OptimizeMetadataOnlyDeltaQuerySuite\n  extends QueryTest\n    with SharedSparkSession\n    with BeforeAndAfterAll\n    with DeltaSQLCommandTest\n    with DeletionVectorsTestUtils {\n  val testTableName = \"table_basic\"\n  val noStatsTableName = \" table_nostats\"\n  val mixedStatsTableName = \" table_mixstats\"\n\n  var dfPart1: DataFrame = null\n  var dfPart2: DataFrame = null\n\n  var totalRows: Long = -1\n  var minId: Long = -1\n  var maxId: Long = -1\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    dfPart1 = generateRowsDataFrame(spark.range(1L, 6L))\n    dfPart2 = generateRowsDataFrame(spark.range(6L, 11L))\n\n    withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n      dfPart1.write.format(\"delta\").mode(SaveMode.Overwrite).saveAsTable(noStatsTableName)\n      dfPart1.write.format(\"delta\").mode(SaveMode.Overwrite).saveAsTable(mixedStatsTableName)\n\n      spark.sql(s\"DELETE FROM $noStatsTableName WHERE id = 1\")\n      spark.sql(s\"DELETE FROM $mixedStatsTableName WHERE id = 1\")\n\n      dfPart2.write.format(\"delta\").mode(\"append\").saveAsTable(noStatsTableName)\n    }\n\n    withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n      dfPart1.write.format(\"delta\").mode(SaveMode.Overwrite).saveAsTable(testTableName)\n\n      spark.sql(s\"DELETE FROM $testTableName WHERE id = 1\")\n\n      dfPart2.write.format(\"delta\").mode(SaveMode.Append).saveAsTable(testTableName)\n      dfPart2.write.format(\"delta\").mode(SaveMode.Append).saveAsTable(mixedStatsTableName)\n\n      // Run updates to generate more Delta Log and trigger a checkpoint\n      // and make sure stats works after checkpoints\n      for (a <- 1 to 10) {\n        spark.sql(s\"UPDATE $testTableName SET data='$a' WHERE id = 7\")\n      }\n      spark.sql(s\"UPDATE $testTableName SET data=NULL WHERE id = 7\")\n\n      // Creates an empty (numRecords == 0) AddFile record\n      generateRowsDataFrame(spark.range(11L, 12L))\n        .write.format(\"delta\").mode(\"append\").saveAsTable(testTableName)\n      spark.sql(s\"DELETE FROM $testTableName WHERE id = 11\")\n    }\n\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"false\") {\n      val result = spark.sql(s\"SELECT COUNT(*), MIN(id), MAX(id) FROM $testTableName\").head\n      totalRows = result.getLong(0)\n      minId = result.getLong(1)\n      maxId = result.getLong(2)\n    }\n  }\n\n  /** Class to hold test parameters */\n  case class ScalaTestParams(name: String, queryScala: () => DataFrame, expectedPlan: String)\n\n  Seq(\n    new ScalaTestParams(\n      name = \"count - simple query\",\n      queryScala = () => spark.read.format(\"delta\").table(testTableName)\n        .agg(count(col(\"*\"))),\n      expectedPlan = \"LocalRelation [none#0L]\"),\n    new ScalaTestParams(\n      name = \"min-max - simple query\",\n      queryScala = () => spark.read.format(\"delta\").table(testTableName)\n      .agg(min(col(\"id\")), max(col(\"id\")),\n        min(col(\"TinyIntColumn\")), max(col(\"TinyIntColumn\")),\n        min(col(\"SmallIntColumn\")), max(col(\"SmallIntColumn\")),\n        min(col(\"IntColumn\")), max(col(\"IntColumn\")),\n        min(col(\"BigIntColumn\")), max(col(\"BigIntColumn\")),\n        min(col(\"FloatColumn\")), max(col(\"FloatColumn\")),\n        min(col(\"DoubleColumn\")), max(col(\"DoubleColumn\")),\n        min(col(\"DateColumn\")), max(col(\"DateColumn\"))),\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2, none#3, none#4, none#5, none#6\" +\n      \", none#7, none#8L, none#9L, none#10, none#11, none#12, none#13, none#14, none#15]\"))\n    .foreach { testParams =>\n      test(s\"optimization supported - Scala - ${testParams.name}\") {\n        checkResultsAndOptimizedPlan(testParams.queryScala, testParams.expectedPlan)\n    }\n  }\n\n  /** Class to hold test parameters */\n  case class SqlTestParams(\n    name: String,\n    querySql: String,\n    expectedPlan: String,\n    querySetup: Option[Seq[String]] = None)\n\n  Seq(\n    new SqlTestParams(\n      name = \"count - simple query\",\n      querySql = s\"SELECT COUNT(*) FROM $testTableName\",\n      expectedPlan = \"LocalRelation [none#0L]\"),\n    new SqlTestParams(\n      name = \"min-max - simple query\",\n      querySql = s\"SELECT MIN(id), MAX(id)\" +\n        s\", MIN(TinyIntColumn), MAX(TinyIntColumn)\" +\n        s\", MIN(SmallIntColumn), MAX(SmallIntColumn)\" +\n        s\", MIN(IntColumn), MAX(IntColumn)\" +\n        s\", MIN(BigIntColumn), MAX(BigIntColumn)\" +\n        s\", MIN(FloatColumn), MAX(FloatColumn)\" +\n        s\", MIN(DoubleColumn), MAX(DoubleColumn)\" +\n        s\", MIN(DateColumn), MAX(DateColumn)\" +\n        s\"FROM $testTableName\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2, none#3, none#4, none#5, none#6\" +\n        \", none#7, none#8L, none#9L, none#10, none#11, none#12, none#13, none#14, none#15]\"),\n    new SqlTestParams(\n      name = \"min-max - column name non-matching case\",\n      querySql = s\"SELECT MIN(ID), MAX(iD)\" +\n        s\", MIN(tINYINTCOLUMN), MAX(tinyintcolumN)\" +\n        s\", MIN(sMALLINTCOLUMN), MAX(smallintcolumN)\" +\n        s\", MIN(iNTCOLUMN), MAX(intcolumN)\" +\n        s\", MIN(bIGINTCOLUMN), MAX(bigintcolumN)\" +\n        s\", MIN(fLOATCOLUMN), MAX(floatcolumN)\" +\n        s\", MIN(dOUBLECOLUMN), MAX(doublecolumN)\" +\n        s\", MIN(dATECOLUMN), MAX(datecolumN)\" +\n        s\"FROM $testTableName\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2, none#3, none#4, none#5, none#6\" +\n        \", none#7, none#8L, none#9L, none#10, none#11, none#12, none#13, none#14, none#15]\"),\n    new SqlTestParams(\n      name = \"count with column name alias\",\n      querySql = s\"SELECT COUNT(*) as MyCount FROM $testTableName\",\n      expectedPlan = \"LocalRelation [none#0L]\"),\n    new SqlTestParams(\n      name = \"count-min-max with column name alias\",\n      querySql = s\"SELECT COUNT(*) as MyCount, MIN(id) as MyMinId, MAX(id) as MyMaxId\" +\n        s\" FROM $testTableName\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L]\"),\n    new SqlTestParams(\n      name = \"count-min-max - table name with alias\",\n      querySql = s\"SELECT COUNT(*), MIN(id), MAX(id) FROM $testTableName MyTable\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L]\"),\n    new SqlTestParams(\n      name = \"count-min-max - query using time travel - version 0\",\n      querySql = s\"SELECT COUNT(*), MIN(id), MAX(id) \" +\n      s\"FROM $testTableName VERSION AS OF 0\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L]\"),\n    new SqlTestParams(\n      name = \"count-min-max - query using time travel - version 1\",\n      querySql = s\"SELECT COUNT(*), MIN(id), MAX(id) \" +\n      s\"FROM $testTableName VERSION AS OF 1\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L]\"),\n    new SqlTestParams(\n      name = \"count-min-max - query using time travel - version 2\",\n      querySql = s\"SELECT COUNT(*), MIN(id), MAX(id) \" +\n      s\"FROM $testTableName VERSION AS OF 2\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L]\"),\n    new SqlTestParams(\n      name = \"count - sub-query\",\n      querySql = s\"SELECT (SELECT COUNT(*) FROM $testTableName)\",\n      expectedPlan = \"Project [scalar-subquery#0 [] AS #0L]\\n\" +\n        \":  +- LocalRelation [none#0L]\\n+- OneRowRelation\"),\n    new SqlTestParams(\n      name = \"min - sub-query\",\n      querySql = s\"SELECT (SELECT MIN(id) FROM $testTableName)\",\n      expectedPlan = \"Project [scalar-subquery#0 [] AS #0L]\\n\" +\n        \":  +- LocalRelation [none#0L]\\n+- OneRowRelation\"),\n    new SqlTestParams(\n      name = \"max - sub-query\",\n      querySql = s\"SELECT (SELECT MAX(id) FROM $testTableName)\",\n      expectedPlan = \"Project [scalar-subquery#0 [] AS #0L]\\n\" +\n        \":  +- LocalRelation [none#0L]\\n+- OneRowRelation\"),\n    new SqlTestParams(\n      name = \"count - sub-query filter\",\n      querySql = s\"SELECT 'ABC' WHERE\" +\n        s\" (SELECT COUNT(*) FROM $testTableName) = $totalRows\",\n      expectedPlan = \"Project [ABC AS #0]\\n+- Filter (scalar-subquery#0 [] = \" +\n        totalRows + \")\\n   :  +- LocalRelation [none#0L]\\n   +- OneRowRelation\"),\n    new SqlTestParams(\n      name = \"min - sub-query filter\",\n      querySql = s\"SELECT 'ABC' WHERE\" +\n        s\" (SELECT MIN(id) FROM $testTableName) = $minId\",\n      expectedPlan = \"Project [ABC AS #0]\\n+- Filter (scalar-subquery#0 [] = \" +\n        minId + \")\\n   :  +- LocalRelation [none#0L]\\n   +- OneRowRelation\"),\n    new SqlTestParams(\n      name = \"max - sub-query filter\",\n      querySql = s\"SELECT 'ABC' WHERE\" +\n        s\" (SELECT MAX(id) FROM $testTableName) = $maxId\",\n      expectedPlan = \"Project [ABC AS #0]\\n+- Filter (scalar-subquery#0 [] = \" +\n        maxId + \")\\n   :  +- LocalRelation [none#0L]\\n   +- OneRowRelation\"),\n    // Limit doesn't affect aggregation results\n    new SqlTestParams(\n      name = \"count-min-max - query with limit\",\n      querySql = s\"SELECT COUNT(*), MIN(id), MAX(id) FROM $testTableName LIMIT 3\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L]\"),\n    new SqlTestParams(\n      name = \"count-min-max - duplicated functions\",\n      querySql = s\"SELECT COUNT(*), COUNT(*), MIN(id), MIN(id), MAX(id), MAX(id)\" +\n        s\" FROM $testTableName\",\n      expectedPlan = \"LocalRelation [none#0L, none#1L, none#2L, none#3L, none#4L, none#5L]\"),\n    new SqlTestParams(\n      name = \"count - empty table\",\n      querySetup = Some(Seq(\"CREATE TABLE TestEmpty (c1 int) USING DELTA\")),\n      querySql = \"SELECT COUNT(*) FROM TestEmpty\",\n      expectedPlan = \"LocalRelation [none#0L]\"),\n    /** Dates are stored as Int in literals. This test make sure Date columns works\n     * and NULL are handled correctly\n     */\n    new SqlTestParams(\n      name = \"min-max - date columns\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestDateValues\" +\n        \" (Column1 DATE, Column2 DATE, Column3 DATE) USING DELTA;\",\n        \"INSERT INTO TestDateValues\" +\n        \" (Column1, Column2, Column3) VALUES (NULL, current_date(), current_date());\",\n        \"INSERT INTO TestDateValues\" +\n        \" (Column1, Column2, Column3) VALUES (NULL, NULL, current_date());\")),\n      querySql = \"SELECT COUNT(*), MIN(Column1), MAX(Column1), MIN(Column2)\" +\n      \", MAX(Column2), MIN(Column3), MAX(Column3) FROM TestDateValues\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4, none#5, none#6]\"),\n    new SqlTestParams(\n      name = \"min-max - floating point infinity\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestFloatInfinity (FloatColumn Float, DoubleColumn Double) USING DELTA\",\n        \"INSERT INTO TestFloatInfinity (FloatColumn, DoubleColumn) VALUES (1, 1);\",\n        \"INSERT INTO TestFloatInfinity (FloatColumn, DoubleColumn) VALUES (NULL, NULL);\",\n        \"INSERT INTO TestFloatInfinity (FloatColumn, DoubleColumn) VALUES \" +\n          \"(float('inf'), double('inf'))\" +\n          \", (float('+inf'), double('+inf'))\" +\n          \", (float('infinity'), double('infinity'))\" +\n          \", (float('+infinity'), double('+infinity'))\" +\n          \", (float('-inf'), double('-inf'))\" +\n          \", (float('-infinity'), double('-infinity'))\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(FloatColumn), MAX(FloatColumn), MIN(DoubleColumn)\" +\n        \", MAX(DoubleColumn) FROM TestFloatInfinity\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4]\"),\n    // NaN is larger than any other value, including Infinity\n    new SqlTestParams(\n      name = \"min-max - floating point NaN values\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestFloatNaN (FloatColumn Float, DoubleColumn Double) USING DELTA\",\n        \"INSERT INTO TestFloatNaN (FloatColumn, DoubleColumn) VALUES (1, 1);\",\n        \"INSERT INTO TestFloatNaN (FloatColumn, DoubleColumn) VALUES (NULL, NULL);\",\n        \"INSERT INTO TestFloatNaN (FloatColumn, DoubleColumn) VALUES \" +\n          \"(float('inf'), double('inf'))\" +\n          \", (float('+inf'), double('+inf'))\" +\n          \", (float('infinity'), double('infinity'))\" +\n          \", (float('+infinity'), double('+infinity'))\" +\n          \", (float('-inf'), double('-inf'))\" +\n          \", (float('-infinity'), double('-infinity'))\",\n        \"INSERT INTO TestFloatNaN (FloatColumn, DoubleColumn) VALUES \" +\n      \"(float('NaN'), double('NaN'));\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(FloatColumn), MAX(FloatColumn), MIN(DoubleColumn)\" +\n        \", MAX(DoubleColumn) FROM TestFloatNaN\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4]\"),\n    new SqlTestParams(\n      name = \"min-max - floating point min positive value\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestFloatPrecision (FloatColumn Float, DoubleColumn Double) USING DELTA\",\n        \"INSERT INTO TestFloatPrecision (FloatColumn, DoubleColumn) VALUES \" +\n      \"(CAST('1.4E-45' as FLOAT), CAST('4.9E-324' as DOUBLE))\" +\n      \", (CAST('-1.4E-45' as FLOAT), CAST('-4.9E-324' as DOUBLE))\" +\n      \", (0, 0);\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(FloatColumn), MAX(FloatColumn), MIN(DoubleColumn)\" +\n        \", MAX(DoubleColumn) FROM TestFloatPrecision\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4]\"),\n    new SqlTestParams(\n      name = \"min-max - NULL and non-NULL values\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestNullValues (Column1 INT, Column2 INT, Column3 INT) USING DELTA\",\n        \"INSERT INTO TestNullValues (Column1, Column2, Column3) VALUES (NULL, 1, 1);\",\n        \"INSERT INTO TestNullValues (Column1, Column2, Column3) VALUES (NULL, NULL, 1);\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(Column1), MAX(Column1),\" +\n        \"MIN(Column2), MAX(Column2) FROM TestNullValues\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4]\"),\n    new SqlTestParams(\n      name = \"min-max - only NULL values\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestOnlyNullValues (Column1 INT, Column2 INT, Column3 INT) USING DELTA\",\n        \"INSERT INTO TestOnlyNullValues (Column1, Column2, Column3) VALUES (NULL, NULL, 1);\",\n        \"INSERT INTO TestOnlyNullValues (Column1, Column2, Column3) VALUES (NULL, NULL, 2);\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(Column1), MAX(Column1), MIN(Column2), MAX(Column2), \" +\n        \"MIN(Column3), MAX(Column3) FROM TestOnlyNullValues\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4, none#5, none#6]\"),\n    new SqlTestParams(\n      name = \"min-max - all supported data types\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestMinMaxValues (\" +\n      \"TINYINTColumn TINYINT, SMALLINTColumn SMALLINT, INTColumn INT, BIGINTColumn BIGINT, \" +\n      \"FLOATColumn FLOAT, DOUBLEColumn DOUBLE, DATEColumn DATE) USING DELTA\",\n        \"INSERT INTO TestMinMaxValues (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n      \" FLOATColumn, DOUBLEColumn, DATEColumn)\" +\n      \" VALUES (-128, -32768, -2147483648, -9223372036854775808,\" +\n      \" -3.4028235E38, -1.7976931348623157E308, CAST('1582-10-15' AS DATE));\",\n        \"INSERT INTO TestMinMaxValues (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n      \" FLOATColumn, DOUBLEColumn, DATEColumn)\" +\n      \" VALUES (127, 32767, 2147483647, 9223372036854775807,\" +\n      \" 3.4028235E38, 1.7976931348623157E308, CAST('9999-12-31' AS DATE));\"\n      )),\n      querySql = \"SELECT COUNT(*),\" +\n        \"MIN(TINYINTColumn), MAX(TINYINTColumn)\" +\n        \", MIN(SMALLINTColumn), MAX(SMALLINTColumn)\" +\n        \", MIN(INTColumn), MAX(INTColumn)\" +\n        \", MIN(BIGINTColumn), MAX(BIGINTColumn)\" +\n        \", MIN(FLOATColumn), MAX(FLOATColumn)\" +\n        \", MIN(DOUBLEColumn), MAX(DOUBLEColumn)\" +\n        \", MIN(DATEColumn), MAX(DATEColumn)\" +\n        \" FROM TestMinMaxValues\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4, none#5, none#6\" +\n        \", none#7L, none#8L, none#9, none#10, none#11, none#12, none#13, none#14]\"),\n    new SqlTestParams(\n      name = \"count-min-max - partitioned table - simple query\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestPartitionedTable (Column1 INT, Column2 INT, Column3 INT, Column4 INT)\" +\n        \" USING DELTA PARTITIONED BY (Column2, Column3)\",\n        \"INSERT INTO TestPartitionedTable\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (1, 2, 3, 4);\",\n        \"INSERT INTO TestPartitionedTable\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (2, 2, 3, 5);\",\n        \"INSERT INTO TestPartitionedTable\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (3, 3, 2, 6);\",\n        \"INSERT INTO TestPartitionedTable\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (4, 3, 2, 7);\"\n      )),\n      querySql = \"SELECT COUNT(*)\" +\n        \", MIN(Column1), MAX(Column1)\" +\n        \", MIN(Column2), MAX(Column2)\" +\n        \", MIN(Column3), MAX(Column3)\" +\n        \", MIN(Column4), MAX(Column4)\" +\n        \" FROM TestPartitionedTable\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3,\" +\n        \" none#4, none#5, none#6, none#7, none#8]\"),\n    /** Partitioned columns should be able to return MIN and MAX data\n     * even when there are no column stats */\n    new SqlTestParams(\n      name = \"count-min-max - partitioned table - no stats\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestPartitionedTableNoStats\" +\n        \" (Column1 INT, Column2 INT, Column3 INT, Column4 INT)\" +\n        \" USING DELTA PARTITIONED BY (Column2, Column3)\" +\n        \" TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 0)\",\n        \"INSERT INTO TestPartitionedTableNoStats\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (1, 2, 3, 4);\",\n        \"INSERT INTO TestPartitionedTableNoStats\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (2, 2, 3, 5);\",\n        \"INSERT INTO TestPartitionedTableNoStats\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (3, 3, 2, 6);\",\n        \"INSERT INTO TestPartitionedTableNoStats\" +\n        \" (Column1, Column2, Column3, Column4) VALUES (4, 3, 2, 7);\"\n      )),\n      querySql = \"SELECT COUNT(*)\" +\n        \", MIN(Column2), MAX(Column2)\" +\n        \", MIN(Column3), MAX(Column3)\" +\n        \" FROM TestPartitionedTableNoStats\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4]\"),\n    new SqlTestParams(\n      name = \"min-max - partitioned table - all supported data types\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestAllTypesPartitionedTable (\" +\n        \"TINYINTColumn TINYINT, SMALLINTColumn SMALLINT, INTColumn INT, BIGINTColumn BIGINT, \" +\n        \"FLOATColumn FLOAT, DOUBLEColumn DOUBLE, DATEColumn DATE, Data INT) USING DELTA\" +\n        \"  PARTITIONED BY (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n        \" FLOATColumn, DOUBLEColumn, DATEColumn)\",\n        \"INSERT INTO TestAllTypesPartitionedTable\" +\n        \" (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n        \" FLOATColumn, DOUBLEColumn, DATEColumn, Data)\" +\n        \" VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);\",\n        \"INSERT INTO TestAllTypesPartitionedTable\" +\n        \" (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n        \" FLOATColumn, DOUBLEColumn, DATEColumn, Data)\" +\n        \" VALUES (-128, -32768, -2147483648, -9223372036854775808,\" +\n        \" -3.4028235E38, -1.7976931348623157E308, CAST('1582-10-15' AS DATE), 1);\"\n      )),\n      querySql = \"SELECT COUNT(*),\" +\n        \"MIN(TINYINTColumn), MAX(TINYINTColumn)\" +\n        \", MIN(SMALLINTColumn), MAX(SMALLINTColumn)\" +\n        \", MIN(INTColumn), MAX(INTColumn)\" +\n        \", MIN(BIGINTColumn), MAX(BIGINTColumn)\" +\n        \", MIN(FLOATColumn), MAX(FLOATColumn)\" +\n        \", MIN(DOUBLEColumn), MAX(DOUBLEColumn)\" +\n        \", MIN(DATEColumn), MAX(DATEColumn)\" +\n        \", MIN(Data), MAX(Data)\" +\n        \" FROM TestAllTypesPartitionedTable\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4, none#5, none#6, \" +\n        \"none#7L, none#8L, none#9, none#10, none#11, none#12, none#13, none#14, none#15, none#16]\"),\n    new SqlTestParams(\n      name = \"min-max - partitioned table - only NULL values\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestOnlyNullValuesPartitioned (\" +\n        \"TINYINTColumn TINYINT, SMALLINTColumn SMALLINT, INTColumn INT, BIGINTColumn BIGINT, \" +\n        \"FLOATColumn FLOAT, DOUBLEColumn DOUBLE, DATEColumn DATE, Data INT) USING DELTA\" +\n        \"  PARTITIONED BY (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n        \" FLOATColumn, DOUBLEColumn, DATEColumn)\",\n        \"INSERT INTO TestOnlyNullValuesPartitioned\" +\n        \" (TINYINTColumn, SMALLINTColumn, INTColumn, BIGINTColumn,\" +\n        \" FLOATColumn, DOUBLEColumn, DATEColumn, Data)\" +\n        \" VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);\"\n      )),\n      querySql = \"SELECT COUNT(*),\" +\n        \"MIN(TINYINTColumn), MAX(TINYINTColumn)\" +\n        \", MIN(SMALLINTColumn), MAX(SMALLINTColumn)\" +\n        \", MIN(INTColumn), MAX(INTColumn)\" +\n        \", MIN(BIGINTColumn), MAX(BIGINTColumn)\" +\n        \", MIN(FLOATColumn), MAX(FLOATColumn)\" +\n        \", MIN(DOUBLEColumn), MAX(DOUBLEColumn)\" +\n        \", MIN(DATEColumn), MAX(DATEColumn)\" +\n        \", MIN(Data), MAX(Data)\" +\n        \" FROM TestOnlyNullValuesPartitioned\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4, none#5, none#6, \" +\n        \"none#7L, none#8L, none#9, none#10, none#11, none#12, none#13, none#14, none#15, none#16]\"),\n    new SqlTestParams(\n      name = \"min-max - partitioned table - NULL and NON-NULL values\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestNullPartitioned (Column1 INT, Column2 INT, Column3 INT)\" +\n        \" USING DELTA PARTITIONED BY (Column2, Column3)\",\n        \"INSERT INTO TestNullPartitioned (Column1, Column2, Column3) VALUES (NULL, NULL, 1);\",\n        \"INSERT INTO TestNullPartitioned (Column1, Column2, Column3) VALUES (NULL, NULL, NULL);\",\n        \"INSERT INTO TestNullPartitioned (Column1, Column2, Column3) VALUES (NULL, NULL, 2);\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(Column1), MAX(Column1), MIN(Column2), MAX(Column2), \" +\n        \"MIN(Column3), MAX(Column3) FROM TestNullPartitioned\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3, none#4, none#5, none#6]\"),\n    new SqlTestParams(\n      name = \"min-max - column name containing punctuation\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestPunctuationColumnName (`My.!?Column` INT) USING DELTA\",\n        \"INSERT INTO TestPunctuationColumnName (`My.!?Column`) VALUES (1), (2), (3);\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(`My.!?Column`), MAX(`My.!?Column`)\" +\n        \" FROM TestPunctuationColumnName\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2]\"),\n    new SqlTestParams(\n      name = \"min-max - partitioned table - column name containing punctuation\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestPartitionedPunctuationColumnName (`My.!?Column` INT, Data INT)\" +\n        \" USING DELTA PARTITIONED BY (`My.!?Column`)\",\n        \"INSERT INTO TestPartitionedPunctuationColumnName\" +\n        \" (`My.!?Column`, Data) VALUES (1, 1), (2, 1), (3, 1);\"\n      )),\n      querySql = \"SELECT COUNT(*), MIN(`My.!?Column`), MAX(`My.!?Column`)\" +\n        \" FROM TestPartitionedPunctuationColumnName\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2]\"),\n    new SqlTestParams(\n      name = \"min-max - partitioned table - special characters in column name\",\n      querySetup = Some(Seq(\n        \"CREATE TABLE TestColumnMappingPartitioned\" +\n        \" (Column1 INT, Column2 INT, `Column3 .,;{}()\\n\\t=` INT, Column4 INT)\" +\n        \" USING DELTA PARTITIONED BY (Column2, `Column3 .,;{}()\\n\\t=`)\" +\n        \" TBLPROPERTIES('delta.columnMapping.mode' = 'name')\",\n        \"INSERT INTO TestColumnMappingPartitioned\" +\n        \" (Column1, Column2, `Column3 .,;{}()\\n\\t=`, Column4)\" +\n        \" VALUES (1, 2, 3, 4);\",\n        \"INSERT INTO TestColumnMappingPartitioned\" +\n        \" (Column1, Column2, `Column3 .,;{}()\\n\\t=`, Column4)\" +\n        \" VALUES (2, 2, 3, 5);\",\n        \"INSERT INTO TestColumnMappingPartitioned\" +\n        \" (Column1, Column2, `Column3 .,;{}()\\n\\t=`, Column4)\" +\n        \" VALUES (3, 3, 2, 6);\",\n        \"INSERT INTO TestColumnMappingPartitioned\" +\n        \" (Column1, Column2, `Column3 .,;{}()\\n\\t=`, Column4)\" +\n        \" VALUES (4, 3, 2, 7);\")),\n      querySql = \"SELECT COUNT(*)\" +\n        \", MIN(Column1), MAX(Column1)\" +\n        \", MIN(Column2), MAX(Column2)\" +\n        \", MIN(`Column3 .,;{}()\\n\\t=`), MAX(`Column3 .,;{}()\\n\\t=`)\" +\n        \", MIN(Column4), MAX(Column4)\" +\n        \" FROM TestColumnMappingPartitioned\",\n      expectedPlan = \"LocalRelation [none#0L, none#1, none#2, none#3,\" +\n        \" none#4, none#5, none#6, none#7, none#8]\"))\n    .foreach { testParams =>\n      test(s\"optimization supported - SQL - ${testParams.name}\") {\n        if (testParams.querySetup.isDefined) {\n          testParams.querySetup.get.foreach(spark.sql)\n        }\n        checkResultsAndOptimizedPlan(testParams.querySql, testParams.expectedPlan)\n    }\n  }\n\n  test(\"count-min-max - external table\") {\n    withTempDir { dir =>\n      val testTablePath = dir.getCanonicalPath\n      dfPart1.write.format(\"delta\").mode(\"overwrite\").save(testTablePath)\n      DeltaTable.forPath(spark, testTablePath).delete(\"id = 1\")\n      dfPart2.write.format(\"delta\").mode(SaveMode.Append).save(testTablePath)\n\n      checkResultsAndOptimizedPlan(\n        s\"SELECT COUNT(*), MIN(id), MAX(id) FROM delta.`$testTablePath`\",\n        \"LocalRelation [none#0L, none#1L, none#2L]\")\n    }\n  }\n\n  test(\"min-max - partitioned column stats disabled\") {\n    withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n      val tableName = \"TestPartitionedNoStats\"\n\n      spark.sql(s\"CREATE TABLE $tableName (Column1 INT, Column2 INT)\" +\n        \" USING DELTA PARTITIONED BY (Column2)\")\n\n      spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (1, 3);\")\n      spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (2, 4);\")\n\n      // Has no stats, including COUNT\n      checkOptimizationIsNotTriggered(\n        s\"SELECT COUNT(*), MIN(Column2), MAX(Column2) FROM $tableName\")\n\n      // Should work for partitioned columns even without stats\n      checkResultsAndOptimizedPlan(\n        s\"SELECT MIN(Column2), MAX(Column2) FROM $tableName\",\n        \"LocalRelation [none#0, none#1]\")\n    }\n  }\n\n  test(\"min-max - recompute column missing stats\") {\n    val tableName = \"TestRecomputeMissingStat\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT, Column2 INT) USING DELTA\" +\n      s\" TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 0)\")\n\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (1, 4);\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (2, 5);\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (3, 6);\")\n\n    checkOptimizationIsNotTriggered(s\"SELECT COUNT(*), MIN(Column1), MAX(Column1) FROM $tableName\")\n\n    spark.sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 1);\")\n\n    StatisticsCollection.recompute(\n      spark,\n      DeltaLog.forTable(spark, TableIdentifier(tableName)),\n      DeltaTableV2(spark, TableIdentifier(tableName)).catalogTable)\n\n    checkResultsAndOptimizedPlan(\n      s\"SELECT COUNT(*), MIN(Column1), MAX(Column1) FROM $tableName\",\n      \"LocalRelation [none#0L, none#1, none#2]\")\n\n    checkOptimizationIsNotTriggered(s\"SELECT COUNT(*), MIN(Column2), MAX(Column2) FROM $tableName\")\n\n    spark.sql(s\"ALTER TABLE $tableName SET TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 2);\")\n\n    StatisticsCollection.recompute(\n      spark,\n      DeltaLog.forTable(spark, TableIdentifier(tableName)),\n      DeltaTableV2(spark, TableIdentifier(tableName)).catalogTable)\n\n    checkResultsAndOptimizedPlan(\n      s\"SELECT COUNT(*), MIN(Column2), MAX(Column2) FROM $tableName\",\n      \"LocalRelation [none#0L, none#1, none#2]\")\n  }\n\n  test(\"min-max - recompute added column\") {\n    val tableName = \"TestRecomputeAddedColumn\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT) USING DELTA\")\n    spark.sql(s\"INSERT INTO $tableName (Column1) VALUES (1);\")\n\n    spark.sql(s\"ALTER TABLE $tableName ADD COLUMN (Column2 INT)\")\n\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (2, 5);\")\n\n    checkResultsAndOptimizedPlan(\n      s\"SELECT COUNT(*), MIN(Column1), MAX(Column1) FROM $tableName\",\n      \"LocalRelation [none#0L, none#1, none#2]\")\n\n    checkOptimizationIsNotTriggered(s\"SELECT COUNT(*), \" +\n      s\"MIN(Column1), MAX(Column1), MIN(Column2), MAX(Column2) FROM $tableName\")\n\n    StatisticsCollection.recompute(\n      spark,\n      DeltaLog.forTable(spark, TableIdentifier(tableName)),\n      DeltaTableV2(spark, TableIdentifier(tableName)).catalogTable)\n\n    checkResultsAndOptimizedPlan(s\"SELECT COUNT(*), \" +\n      s\"MIN(Column1), MAX(Column1), MIN(Column2), MAX(Column2) FROM $tableName\",\n      \"LocalRelation [none#0L, none#1, none#2, none#3, none#4]\")\n  }\n\n  test(\"Select Count: snapshot isolation\") {\n    sql(s\"CREATE TABLE TestSnapshotIsolation (c1 int) USING DELTA\")\n    spark.sql(\"INSERT INTO TestSnapshotIsolation VALUES (1)\")\n\n    val scannedVersions = mutable.ArrayBuffer[Long]()\n    val query = \"SELECT (SELECT COUNT(*) FROM TestSnapshotIsolation), \" +\n      \"(SELECT COUNT(*) FROM TestSnapshotIsolation)\"\n\n    checkResultsAndOptimizedPlan(\n      query,\n      \"Project [scalar-subquery#0 [] AS #0L, scalar-subquery#0 [] AS #1L]\\n\" +\n        \":  :- LocalRelation [none#0L]\\n\" +\n        \":  +- LocalRelation [none#0L]\\n\" +\n        \"+- OneRowRelation\")\n\n    PrepareDeltaScanBase.withCallbackOnGetDeltaScanGenerator(scanGenerator => {\n      // Record the scanned version and make changes to the table. We will verify changes in the\n      // middle of the query are not visible to the query.\n      scannedVersions += scanGenerator.snapshotToScan.version\n      // Insert a row after each call to get scanGenerator\n      // to test if the count doesn't change in the same query\n      spark.sql(\"INSERT INTO TestSnapshotIsolation VALUES (1)\")\n    }) {\n      val result = spark.sql(query).collect()(0)\n      val c1 = result.getLong(0)\n      val c2 = result.getLong(1)\n      assertResult(c1, \"Snapshot isolation should guarantee the results are always the same\")(c2)\n      assert(\n        scannedVersions.toSet.size == 1,\n        s\"Scanned multiple versions of the same table in one query: ${scannedVersions.toSet}\")\n    }\n  }\n\n  test(\".collect() and .show() both use this optimization\") {\n    var resultRow: Row = null\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"false\") {\n      resultRow = spark.sql(s\"SELECT COUNT(*), MIN(id), MAX(id) FROM $testTableName\").head\n    }\n\n    val totalRows = resultRow.getLong(0)\n    val minId = resultRow.getLong(1)\n    val maxId = resultRow.getLong(2)\n\n    val collectPlans = DeltaTestUtils.withLogicalPlansCaptured(spark, optimizedPlan = true) {\n      spark.sql(s\"SELECT COUNT(*) FROM $testTableName\").collect()\n    }\n    val collectResultData = collectPlans.collect { case x: LocalRelation => x.data }\n    assert(collectResultData.size === 1)\n    assert(collectResultData.head.head.getLong(0) === totalRows)\n\n    val showPlans = DeltaTestUtils.withLogicalPlansCaptured(spark, optimizedPlan = true) {\n      spark.sql(s\"SELECT COUNT(*) FROM $testTableName\").show()\n    }\n    val showResultData = showPlans.collect { case x: LocalRelation => x.data }\n    assert(showResultData.size === 1)\n    assert(showResultData.head.head.getString(0).toLong === totalRows)\n\n    val showMultAggPlans = DeltaTestUtils.withLogicalPlansCaptured(spark, optimizedPlan = true) {\n      spark.sql(s\"SELECT COUNT(*), MIN(id), MAX(id) FROM $testTableName\").show()\n    }\n\n    val showMultipleAggResultData = showMultAggPlans.collect { case x: LocalRelation => x.data }\n    assert(showMultipleAggResultData.size === 1)\n    val firstRow = showMultipleAggResultData.head.head\n    assert(firstRow.getString(0).toLong === totalRows)\n    assert(firstRow.getString(1).toLong === minId)\n    assert(firstRow.getString(2).toLong === maxId)\n  }\n\n  test(\"min-max .show() - only NULL values\") {\n    val tableName = \"TestOnlyNullValuesShow\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT) USING DELTA\")\n    spark.sql(s\"INSERT INTO $tableName (Column1) VALUES (NULL);\")\n\n    val showMultAggPlans = DeltaTestUtils.withLogicalPlansCaptured(spark, optimizedPlan = true) {\n      spark.sql(s\"SELECT MIN(Column1), MAX(Column1) FROM $tableName\").show()\n    }\n\n    val showMultipleAggResultData = showMultAggPlans.collect { case x: LocalRelation => x.data }\n    assert(showMultipleAggResultData.size === 1)\n    val firstRow = showMultipleAggResultData.head.head\n    assert(firstRow.getString(0) === \"NULL\")\n    assert(firstRow.getString(1) === \"NULL\")\n  }\n\n  test(\"min-max .show() - Date Columns\") {\n    val tableName = \"TestDateColumnsShow\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 DATE, Column2 DATE) USING DELTA\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES \" +\n      s\"(CAST('1582-10-15' AS DATE), NULL);\")\n\n    val showMultAggPlans = DeltaTestUtils.withLogicalPlansCaptured(spark, optimizedPlan = true) {\n      spark.sql(s\"SELECT MIN(Column1), MIN(Column2) FROM $tableName\").show()\n    }\n\n    val showMultipleAggResultData = showMultAggPlans.collect { case x: LocalRelation => x.data }\n    assert(showMultipleAggResultData.size === 1)\n    val firstRow = showMultipleAggResultData.head.head\n    assert(firstRow.getString(0) === \"1582-10-15\")\n    assert(firstRow.getString(1) === \"NULL\")\n  }\n\n  test(\"count - dv-enabled\") {\n    withTempDir { dir =>\n      val tempPath = dir.getCanonicalPath\n      spark.range(1, 10, 1, 1).write.format(\"delta\").save(tempPath)\n\n      enableDeletionVectorsInTable(new Path(tempPath), true)\n      DeltaTable.forPath(spark, tempPath).delete(\"id = 1\")\n      assert(!getFilesWithDeletionVectors(DeltaLog.forTable(spark, new Path(tempPath))).isEmpty)\n\n      checkResultsAndOptimizedPlan(\n        s\"SELECT COUNT(*) FROM delta.`$tempPath`\",\n        \"LocalRelation [none#0L]\")\n    }\n  }\n\n  test(\"count - zero rows AddFile\") {\n    withTempDir { dir =>\n      val tempPath = dir.getCanonicalPath\n      val df = spark.range(1, 10)\n      val expectedResult = df.count()\n      df.write.format(\"delta\").save(tempPath)\n\n      // Creates AddFile entries with non-existing files\n      // The query should read only the delta log and not the parquet files\n      val log = DeltaLog.forTable(spark, tempPath)\n      val txn = log.startTransaction()\n      txn.commitManually(\n        DeltaTestUtils.createTestAddFile(encodedPath = \"1.parquet\", stats = \"{\\\"numRecords\\\": 0}\"),\n        DeltaTestUtils.createTestAddFile(encodedPath = \"2.parquet\", stats = \"{\\\"numRecords\\\": 0}\"),\n        DeltaTestUtils.createTestAddFile(encodedPath = \"3.parquet\", stats = \"{\\\"numRecords\\\": 0}\"))\n\n      withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"true\") {\n        val queryDf = spark.sql(s\"SELECT COUNT(*) FROM delta.`$tempPath`\")\n        val optimizedPlan = queryDf.queryExecution.optimizedPlan.canonicalized.toString()\n\n        assert(queryDf.head().getLong(0) === expectedResult)\n\n        assertResult(\"LocalRelation [none#0L]\") {\n          optimizedPlan.trim\n        }\n      }\n    }\n  }\n\n  // Tests to validate the optimizer won't incorrectly change queries it can't correctly handle\n\n  Seq((s\"SELECT COUNT(*) FROM $mixedStatsTableName\", \"missing stats\"),\n    (s\"SELECT COUNT(*) FROM $noStatsTableName\", \"missing stats\"),\n    (s\"SELECT MIN(id), MAX(id) FROM $mixedStatsTableName\", \"missing stats\"),\n    (s\"SELECT MIN(id), MAX(id) FROM $noStatsTableName\", \"missing stats\"),\n    (s\"SELECT group, COUNT(*) FROM $testTableName GROUP BY group\", \"group by\"),\n    (s\"SELECT group, MIN(id), MAX(id) FROM $testTableName GROUP BY group\", \"group by\"),\n    (s\"SELECT COUNT(*) + 1 FROM $testTableName\", \"plus literal\"),\n    (s\"SELECT MAX(id) + 1 FROM $testTableName\", \"plus literal\"),\n    (s\"SELECT COUNT(DISTINCT data) FROM $testTableName\", \"distinct count\"),\n    (s\"SELECT COUNT(*) FROM $testTableName WHERE id > 0\", \"filter\"),\n    (s\"SELECT MAX(id) FROM $testTableName WHERE id > 0\", \"filter\"),\n    (s\"SELECT (SELECT COUNT(*) FROM $testTableName WHERE id > 0)\", \"sub-query with filter\"),\n    (s\"SELECT (SELECT MAX(id) FROM $testTableName WHERE id > 0)\", \"sub-query with filter\"),\n    (s\"SELECT COUNT(ALL data) FROM $testTableName\", \"count non-null\"),\n    (s\"SELECT COUNT(data) FROM $testTableName\", \"count non-null\"),\n    (s\"SELECT COUNT(*) FROM $testTableName A, $testTableName B\", \"join\"),\n    (s\"SELECT MAX(A.id) FROM $testTableName A, $testTableName B\", \"join\"),\n    (s\"SELECT COUNT(*) OVER() FROM $testTableName LIMIT 1\", \"over\"),\n    ( s\"SELECT MAX(id) OVER() FROM $testTableName LIMIT 1\", \"over\")\n    )\n    .foreach { case (query, desc) =>\n      test(s\"optimization not supported - $desc - $query\") {\n        checkOptimizationIsNotTriggered(query)\n    }\n  }\n\n  test(\"optimization not supported - min-max unsupported data types\") {\n    val tableName = \"TestUnsupportedTypes\"\n\n    spark.sql(s\"CREATE TABLE $tableName \" +\n      s\"(STRINGColumn STRING, DECIMALColumn DECIMAL(38,0)\" +\n      s\", TIMESTAMPColumn TIMESTAMP, BINARYColumn BINARY, \" +\n      s\"BOOLEANColumn BOOLEAN, ARRAYColumn ARRAY<INT>, MAPColumn MAP<INT, INT>, \" +\n      s\"STRUCTColumn STRUCT<Id: INT, Name: STRING>) USING DELTA\")\n\n    spark.sql(s\"INSERT INTO $tableName\" +\n      s\" (STRINGColumn, DECIMALColumn, TIMESTAMPColumn, BINARYColumn\" +\n      s\", BOOLEANColumn, ARRAYColumn, MAPColumn, STRUCTColumn) VALUES \" +\n      s\"('A', -99999999999999999999999999999999999999, CAST('1900-01-01 00:00:00.0' AS TIMESTAMP)\" +\n      s\", X'1ABF', TRUE, ARRAY(1, 2, 3), MAP(1, 10, 2, 20), STRUCT(1, 'Spark'));\")\n\n    val columnNames = List(\"STRINGColumn\", \"DECIMALColumn\", \"TIMESTAMPColumn\",\n      \"BINARYColumn\", \"BOOLEANColumn\", \"ARRAYColumn\", \"STRUCTColumn\")\n\n    columnNames.foreach(colName =>\n      checkOptimizationIsNotTriggered(s\"SELECT MAX($colName) FROM $tableName\")\n    )\n  }\n\n  test(\"optimization not supported - min-max column without stats\") {\n    val tableName = \"TestColumnWithoutStats\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT, Column2 INT) USING DELTA\" +\n      s\" TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 1)\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (1, 2);\")\n\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MAX(Column2) FROM $tableName\")\n  }\n\n  // For empty tables the stats won't be found and the query should not be optimized\n  test(\"optimization not supported - min-max empty table\") {\n    val tableName = \"TestMinMaxEmptyTable\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT) USING DELTA\")\n\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MIN(Column1), MAX(Column1) FROM $tableName\")\n  }\n\n  test(\"optimization not supported - min-max dv-enabled\") {\n    withTempDir { dir =>\n      val tempPath = dir.getCanonicalPath\n      spark.range(1, 10, 1, 1).write.format(\"delta\").save(tempPath)\n      val querySql = s\"SELECT MIN(id), MAX(id) FROM delta.`$tempPath`\"\n      checkResultsAndOptimizedPlan(querySql, \"LocalRelation [none#0L, none#1L]\")\n\n      enableDeletionVectorsInTable(new Path(tempPath), true)\n      DeltaTable.forPath(spark, tempPath).delete(\"id = 1\")\n      assert(!getFilesWithDeletionVectors(DeltaLog.forTable(spark, new Path(tempPath))).isEmpty)\n      checkOptimizationIsNotTriggered(querySql)\n    }\n  }\n\n  test(\"optimization not supported - filter on partitioned column\") {\n    val tableName = \"TestPartitionedFilter\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT, Column2 INT)\" +\n      \" USING DELTA PARTITIONED BY (Column2)\")\n\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (1, 2);\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (2, 2);\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (3, 3);\")\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2) VALUES (4, 3);\")\n\n    // Filter by partition column\n    checkOptimizationIsNotTriggered(\n      \"SELECT COUNT(*)\" +\n        \", MIN(Column1), MAX(Column1)\" +\n        \", MIN(Column2), MAX(Column2)\" +\n        s\" FROM $tableName WHERE Column2 = 2\")\n\n    // Filter both partition and data columns\n    checkOptimizationIsNotTriggered(\n      \"SELECT COUNT(*)\" +\n        \", MIN(Column1), MAX(Column1)\" +\n        \", MIN(Column2), MAX(Column2)\" +\n        s\" FROM $tableName WHERE Column1 = 2 AND Column2 = 2\")\n  }\n\n  test(\"optimization not supported - sub-query with column alias\") {\n    val tableName = \"TestColumnAliasSubQuery\"\n\n    spark.sql(s\"CREATE TABLE $tableName (Column1 INT, Column2 INT, Column3 INT) USING DELTA\")\n\n    spark.sql(s\"INSERT INTO $tableName (Column1, Column2, Column3) VALUES (1, 2, 3);\")\n\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MAX(Column2) FROM (SELECT Column1 AS Column2 FROM $tableName)\")\n\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MAX(Column1), MAX(Column2), MAX(Column3) FROM \" +\n        s\"(SELECT Column1 AS Column2, Column2 AS Column3, Column3 AS Column1 FROM $tableName)\")\n  }\n\n  test(\"optimization not supported - nested columns\") {\n    val tableName = \"TestNestedColumns\"\n\n    spark.sql(s\"CREATE TABLE $tableName \" +\n      s\"(Column1 STRUCT<Id: INT>, \" +\n      s\"`Column1.Id` INT) USING DELTA\")\n\n    spark.sql(s\"INSERT INTO $tableName\" +\n      s\" (Column1, `Column1.Id`) VALUES \" +\n      s\"(STRUCT(1), 2);\")\n\n    // Nested Column\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MAX(Column1.Id) FROM $tableName\")\n\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MAX(Column1.Id) AS XYZ FROM $tableName\")\n\n    // Validate the scenario where all the columns are read\n    // since it creates a different query plan\n    checkOptimizationIsNotTriggered(\n      s\"SELECT MAX(Column1.Id), \" +\n        s\"MAX(`Column1.Id`) FROM $tableName\")\n\n    // The optimization for columns with dots should still work\n    checkResultsAndOptimizedPlan(s\"SELECT MAX(`Column1.Id`) FROM $tableName\",\n      \"LocalRelation [none#0]\")\n  }\n\n  private def generateRowsDataFrame(source: Dataset[java.lang.Long]): DataFrame = {\n    import testImplicits._\n\n    source.select('id,\n      'id.cast(\"tinyint\") as 'TinyIntColumn,\n      'id.cast(\"smallint\") as 'SmallIntColumn,\n      'id.cast(\"int\") as 'IntColumn,\n      'id.cast(\"bigint\") as 'BigIntColumn,\n      ('id / 3.3).cast(\"float\") as 'FloatColumn,\n      ('id / 3.3).cast(\"double\") as 'DoubleColumn,\n      date_add(lit(\"2022-08-31\").cast(\"date\"), col(\"id\").cast(\"int\")) as 'DateColumn,\n      ('id % 2).cast(\"integer\") as 'group,\n      'id.cast(\"string\") as 'data)\n  }\n\n  /** Validate the results of the query is the same with the flag\n   * DELTA_OPTIMIZE_METADATA_QUERY_ENABLED enabled and disabled.\n   * And the expected Optimized Query Plan with the flag enabled */\n  private def checkResultsAndOptimizedPlan(\n    query: String,\n    expectedOptimizedPlan: String): Unit = {\n    checkResultsAndOptimizedPlan(() => spark.sql(query), expectedOptimizedPlan)\n  }\n\n  /** Validate the results of the query is the same with the flag\n   * DELTA_OPTIMIZE_METADATA_QUERY_ENABLED enabled and disabled.\n   * And the expected Optimized Query Plan with the flag enabled. */\n  private def checkResultsAndOptimizedPlan(\n    generateQueryDf: () => DataFrame,\n    expectedOptimizedPlan: String): Unit = {\n    var expectedAnswer: scala.Seq[Row] = null\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"false\") {\n      expectedAnswer = generateQueryDf().collect()\n    }\n\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"true\") {\n      val queryDf = generateQueryDf()\n      val optimizedPlan = queryDf.queryExecution.optimizedPlan.canonicalized.toString()\n\n      assert(queryDf.collect().sameElements(expectedAnswer))\n\n      assertResult(expectedOptimizedPlan.trim) {\n        optimizedPlan.trim\n      }\n    }\n  }\n\n  /**\n   * Verify the query plans and results are the same with/without metadata query optimization.\n   * This method can be used to verify cases that we shouldn't trigger optimization\n   * or cases that we can potentially improve.\n   * @param query\n   */\n  private def checkOptimizationIsNotTriggered(query: String) {\n    var expectedOptimizedPlan: String = null\n    var expectedAnswer: scala.Seq[Row] = null\n\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"false\") {\n\n      val generateQueryDf = spark.sql(query)\n      expectedOptimizedPlan = generateQueryDf.queryExecution.optimizedPlan\n        .canonicalized.toString()\n      expectedAnswer = generateQueryDf.collect()\n    }\n\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_METADATA_QUERY_ENABLED.key -> \"true\") {\n\n      val generateQueryDf = spark.sql(query)\n      val optimizationEnabledQueryPlan = generateQueryDf.queryExecution.optimizedPlan\n        .canonicalized.toString()\n\n      assert(generateQueryDf.collect().sameElements(expectedAnswer))\n\n      assertResult(expectedOptimizedPlan) {\n        optimizationEnabledQueryPlan\n      }\n    }\n  }\n}\n\ntrait OptimizeMetadataOnlyDeltaQueryColumnMappingSuiteBase\n  extends DeltaColumnMappingSelectedTestMixin {\n  override protected def runAllTests = true\n}\n\nclass OptimizeMetadataOnlyDeltaQueryIdColumnMappingSuite\n  extends OptimizeMetadataOnlyDeltaQuerySuite\n  with DeltaColumnMappingEnableIdMode\n  with OptimizeMetadataOnlyDeltaQueryColumnMappingSuiteBase\n\nclass OptimizeMetadataOnlyDeltaQueryNameColumnMappingSuite\n  extends OptimizeMetadataOnlyDeltaQuerySuite\n  with DeltaColumnMappingEnableNameMode\n  with OptimizeMetadataOnlyDeltaQueryColumnMappingSuiteBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/perf/OptimizedWritesSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.perf\n\nimport java.io.File\n\nimport scala.language.implicitConversions\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, DeltaOptions, DeltaTestUtils}\nimport org.apache.spark.sql.delta.CommitStats\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.streaming.StreamingQuery\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{LongType, StructType}\n\nabstract class OptimizedWritesSuiteBase extends QueryTest\n  with SharedSparkSession {\n\n  import testImplicits._\n\n  protected def writeTest(testName: String)(f: String => Unit): Unit = {\n    test(testName) {\n      withTempDir { dir =>\n        withSQLConf(DeltaConfigs.OPTIMIZE_WRITE.defaultTablePropertyKey -> \"true\") {\n          f(dir.getCanonicalPath)\n        }\n      }\n    }\n  }\n\n  protected def checkResult(df: DataFrame, numFileCheck: Long => Boolean, dir: String): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, dir)\n    val files = snapshot.numOfFiles\n    assert(numFileCheck(files), s\"file check failed: received $files\")\n\n    checkAnswer(\n      spark.read.format(\"delta\").load(dir),\n      df\n    )\n  }\n\n  protected implicit def fileToPathString(dir: File): String = dir.getCanonicalPath\n\n  writeTest(\"non-partitioned write - table config\") { dir =>\n    val df = spark.range(0, 100, 1, 4).toDF()\n    df.write.format(\"delta\").save(dir)\n    checkResult(\n      df,\n      numFileCheck = _ === 1,\n      dir)\n  }\n\n  test(\"non-partitioned write - table config compatibility\") {\n    withTempDir { tempDir =>\n      val dir = tempDir.getCanonicalPath\n      // When table property is not set, we use session conf value.\n      // Writes 1 file instead of 4 when OW is enabled\n      withSQLConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"true\") {\n        val df = spark.range(0, 100, 1, 4).toDF()\n        val commitStats = Log4jUsageLogger.track {\n          df.write.format(\"delta\").mode(\"append\").save(dir)\n        }.filter(_.tags.get(\"opType\") === Some(\"delta.commit.stats\"))\n        assert(commitStats.length >= 1)\n        checkResult(\n          df,\n          numFileCheck = _ === 1,\n          dir)\n      }\n    }\n\n    // Test order of precedence between table property \"delta.autoOptimize.optimizeWrite\" and\n    // session conf.\n    for {\n      sqlConf <- DeltaTestUtils.BOOLEAN_DOMAIN\n      tableProperty <- DeltaTestUtils.BOOLEAN_DOMAIN\n    } {\n      withTempDir { tempDir =>\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> sqlConf.toString) {\n          val dir = tempDir.getCanonicalPath\n          // Write one file to be able to set tblproperties\n          spark.range(10).coalesce(1).write.format(\"delta\")\n            .mode(\"append\").save(dir)\n\n          sql(s\"ALTER TABLE delta.`$dir` SET TBLPROPERTIES\" +\n            s\" (delta.autoOptimize.optimizeWrite = ${tableProperty.toString})\")\n\n          val df = spark.range(0, 100, 1, 4).toDF()\n          // OW adds one file vs non-OW adds 4 files\n          val expectedNumberOfFiles = if (sqlConf) 2 else 5\n          df.write.format(\"delta\").mode(\"append\").save(dir)\n          checkResult(\n            df.union(spark.range(10).toDF()),\n            numFileCheck = _ === expectedNumberOfFiles,\n            dir)\n        }\n      }\n    }\n  }\n\n  test(\"non-partitioned write - data frame config\") {\n    withTempDir { dir =>\n      val df = spark.range(0, 100, 1, 4).toDF()\n      df.write.format(\"delta\")\n        .option(DeltaOptions.OPTIMIZE_WRITE_OPTION, \"true\").save(dir)\n      checkResult(\n        df,\n        numFileCheck = _ === 1,\n        dir)\n    }\n  }\n\n  writeTest(\"non-partitioned write - data frame config trumps table config\") { dir =>\n    val df = spark.range(0, 100, 1, 4).toDF()\n    df.write.format(\"delta\").option(DeltaOptions.OPTIMIZE_WRITE_OPTION, \"false\").save(dir)\n    checkResult(\n      df,\n      numFileCheck = _ === 4,\n      dir)\n  }\n\n  writeTest(\"partitioned write - table config\") { dir =>\n    val df = spark.range(0, 100, 1, 4)\n      .withColumn(\"part\", 'id % 5)\n\n    df.write.partitionBy(\"part\").format(\"delta\").save(dir)\n    checkResult(\n      df,\n      numFileCheck = _ <= 5,\n      dir)\n  }\n\n  test(\"partitioned write - data frame config\") {\n    withTempDir { dir =>\n      val df = spark.range(0, 100, 1, 4)\n        .withColumn(\"part\", 'id % 5)\n\n      df.write.partitionBy(\"part\").option(DeltaOptions.OPTIMIZE_WRITE_OPTION, \"true\")\n        .format(\"delta\").save(dir)\n\n      checkResult(\n        df,\n        numFileCheck = _ <= 5,\n        dir)\n    }\n  }\n\n  writeTest(\"partitioned write - data frame config trumps table config\") { dir =>\n    val df = spark.range(0, 100, 1, 4)\n      .withColumn(\"part\", 'id % 5)\n\n    df.write.partitionBy(\"part\").format(\"delta\")\n      .option(DeltaOptions.OPTIMIZE_WRITE_OPTION, \"false\").save(dir)\n\n    checkResult(\n      df,\n      numFileCheck = _ === 20,\n      dir)\n  }\n\n  writeTest(\"multi-partitions - table config\") { dir =>\n    val df = spark.range(0, 100, 1, 4)\n      .withColumn(\"part\", 'id % 5)\n      .withColumn(\"part2\", ('id / 20).cast(\"int\"))\n\n    df.write.partitionBy(\"part\", \"part2\").format(\"delta\").save(dir)\n\n    checkResult(\n      df,\n      numFileCheck = _ <= 25,\n      dir)\n  }\n\n  test(\"multi-partitions - data frame config\") {\n    withTempDir { dir =>\n      val df = spark.range(0, 100, 1, 4)\n        .withColumn(\"part\", 'id % 5)\n        .withColumn(\"part2\", ('id / 20).cast(\"int\"))\n\n      df.write.partitionBy(\"part\", \"part2\")\n        .option(DeltaOptions.OPTIMIZE_WRITE_OPTION, \"true\").format(\"delta\").save(dir)\n\n      checkResult(\n        df,\n        numFileCheck = _ <= 25,\n        dir)\n    }\n  }\n\n  test(\"optimized writes used if enabled when a stream starts\") {\n    withTempDir { f =>\n      // Write some data into the table so it already exists\n      Seq(1).toDF().write.format(\"delta\").save(f)\n\n      // Use optimized writes just when starting the stream\n      val inputData = MemoryStream[Int]\n\n      val df = inputData.toDF().repartition(10)\n      var stream: StreamingQuery = null\n\n      // Start the stream with optimized writes enabled, and then reset the conf\n      withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"true\") {\n        val checkpoint = new File(f, \"checkpoint\").getCanonicalPath\n        stream = df.writeStream.format(\"delta\").option(\"checkpointLocation\", checkpoint).start(f)\n      }\n      try {\n        inputData.addData(1 to 100)\n        stream.processAllAvailable()\n      } finally {\n        stream.stop()\n      }\n\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, f)\n      assert(snapshot.numOfFiles == 2, \"Optimized writes were not used\")\n    }\n  }\n\n  writeTest(\"multi-partitions - data frame config trumps table config\") { dir =>\n    val df = spark.range(0, 100, 1, 4)\n      .withColumn(\"part\", 'id % 5)\n      .withColumn(\"part2\", ('id / 20).cast(\"int\"))\n\n    df.write.partitionBy(\"part\", \"part2\")\n      .option(DeltaOptions.OPTIMIZE_WRITE_OPTION, \"false\").format(\"delta\").save(dir)\n\n    checkResult(\n      df,\n      numFileCheck = _ > 25,\n      dir)\n  }\n\n  writeTest(\"optimize should not leverage optimized writes\") { dir =>\n    val df = spark.range(0, 10, 1, 2)\n\n    val logs1 = Log4jUsageLogger.track {\n      df.write.format(\"delta\").mode(\"append\").save(dir)\n      df.write.format(\"delta\").mode(\"append\").save(dir)\n    }.filter(_.metric == \"tahoeEvent\")\n\n    assert(logs1.count(_.tags.get(\"opType\") === Some(\"delta.optimizeWrite.planned\")) === 2)\n\n    val logs2 = Log4jUsageLogger.track {\n      sql(s\"optimize delta.`$dir`\")\n    }.filter(_.metric == \"tahoeEvent\")\n\n    assert(logs2.count(_.tags.get(\"opType\") === Some(\"delta.optimizeWrite.planned\")) === 0)\n  }\n\n  writeTest(\"map task with more partitions than target shuffle blocks - non-partitioned\") { dir =>\n    val df = spark.range(0, 20, 1, 4)\n\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS.key -> \"2\") {\n      df.write.format(\"delta\").mode(\"append\").save(dir)\n    }\n\n    checkResult(\n      df.toDF(),\n      numFileCheck = _ === 1,\n      dir)\n  }\n\n  writeTest(\"map task with more partitions than target shuffle blocks - partitioned\") { dir =>\n    val df = spark.range(0, 20, 1, 4).withColumn(\"part\", 'id % 5)\n\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS.key -> \"2\") {\n      df.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(dir)\n    }\n\n    checkResult(\n      df,\n      numFileCheck = _ === 5,\n      dir)\n  }\n\n  writeTest(\"zero partition dataframe write\") { dir =>\n    val df = spark.range(0, 20, 1, 4).withColumn(\"part\", 'id % 5)\n    df.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(dir)\n    val schema = new StructType().add(\"id\", LongType).add(\"part\", LongType)\n\n    spark.createDataFrame(sparkContext.emptyRDD[Row], schema).write.format(\"delta\")\n      .partitionBy(\"part\").mode(\"append\").save(dir)\n\n    checkResult(\n      df,\n      numFileCheck = _ === 5,\n      dir)\n  }\n\n  test(\"OptimizedWriterBlocks is not serializable\") {\n    assert(!new OptimizedWriterBlocks(Array.empty).isInstanceOf[Serializable],\n      \"The blocks should not be serializable so that they don't get shipped to executors.\")\n  }\n\n  writeTest(\"single partition dataframe write\") { dir =>\n    val df = spark.range(0, 20).repartition(1).withColumn(\"part\", 'id % 5)\n    val logs1 = Log4jUsageLogger.track {\n      df.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(dir)\n    }.filter(_.metric == \"tahoeEvent\")\n\n    // doesn't use optimized writes\n    assert(logs1.count(_.tags.get(\"opType\") === Some(\"delta.optimizeWrite.planned\")) === 0)\n\n    checkResult(\n      df,\n      numFileCheck = _ === 5,\n      dir)\n  }\n\n  writeTest(\"do not create tons of shuffle partitions during optimized writes\") { dir =>\n    // 50M shuffle blocks would've led to 25M shuffle partitions\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS.key -> \"50000000\") {\n      val df = spark.range(0, 20).repartition(2).withColumn(\"part\", 'id % 5)\n      val logs1 = Log4jUsageLogger.track {\n        df.write.format(\"delta\").partitionBy(\"part\").mode(\"append\").save(dir)\n      }.filter(_.metric == \"tahoeEvent\")\n        .filter(_.tags.get(\"opType\") === Some(\"delta.optimizeWrite.planned\"))\n\n      assert(logs1.length === 1)\n      val blob = JsonUtils.fromJson[Map[String, Any]](logs1.head.blob)\n      assert(blob(\"outputPartitions\") === 5)\n      assert(blob(\"originalPartitions\") === 2)\n      assert(blob(\"numShuffleBlocks\") === 50000000)\n      assert(blob(\"shufflePartitions\") ===\n        spark.conf.get(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_MAX_SHUFFLE_PARTITIONS))\n\n      checkResult(\n        df,\n        numFileCheck = _ === 5,\n        dir)\n    }\n  }\n}\n\nclass OptimizedWritesSuite extends OptimizedWritesSuiteBase with DeltaSQLCommandTest {}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/ConflictCheckerRowIdSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport java.io.File\n\nimport scala.concurrent.duration.Duration\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.DeltaTestUtils.createTestAddFile\nimport org.apache.spark.sql.delta.concurrency.PhaseLockingTestMixin\nimport org.apache.spark.sql.delta.concurrency.TransactionExecutionTestMixin\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ThreadUtils\n\nclass ConflictCheckerRowIdSuite extends QueryTest\n  with SharedSparkSession\n  with PhaseLockingTestMixin\n  with TransactionExecutionTestMixin\n  with RowIdTestUtils {\n\n  protected def appendRows(dir: File, numRows: Int, numFiles: Int): Unit = {\n    withRowTrackingEnabled(enabled = true) {\n      spark.range(start = 0, end = numRows, step = 1, numPartitions = numFiles)\n        .write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n    }\n  }\n\n  protected def setIsolationLevel(tableDir: File): Unit = {\n    spark.sql(\n      s\"\"\"ALTER TABLE delta.`${tableDir.getAbsolutePath}`\n         |SET TBLPROPERTIES ('${DeltaConfigs.ISOLATION_LEVEL.key}' = '$Serializable')\n         |\"\"\".stripMargin\n    )\n  }\n\n  test(\"concurrent transactions do not assign overlapping row IDs\") {\n    withTempDir { tempDir =>\n      appendRows(tempDir, numRows = 100, numFiles = 1)\n\n      setIsolationLevel(tempDir)\n\n      def txnA(): Array[Row] = {\n        appendRows(tempDir, numRows = 1000, numFiles = 2)\n        Array.empty\n      }\n\n      def txnB(): Array[Row] = {\n        appendRows(tempDir, numRows = 1500, numFiles = 3)\n        Array.empty\n      }\n\n      val (futureA, futureB) = runTxnsWithOrder__A_Start__B__A_End(txnA, txnB)\n      ThreadUtils.awaitResult(futureA, Duration.Inf)\n      ThreadUtils.awaitResult(futureB, Duration.Inf)\n\n      val log = DeltaLog.forTable(spark, tempDir)\n      assertRowIdsAreValid(log)\n    }\n  }\n\n  test(\"re-added files keep their row ids\") {\n    withTempDir { tempDir =>\n      appendRows(tempDir, numRows = 100, numFiles = 3)\n\n      setIsolationLevel(tempDir)\n\n      val log = DeltaLog.forTable(spark, tempDir)\n      val filesBefore = log.update().allFiles.collect()\n      val baseRowIdsBefore = filesBefore.map(f => f.path -> f.baseRowId.get).toMap\n\n      def txnA(): Array[Row] = {\n        log.startTransaction().commit(filesBefore, ManualUpdate)\n        Array.empty\n      }\n\n      def txnB(): Array[Row] = {\n        appendRows(tempDir, numRows = 113, numFiles = 2)\n        Array.empty\n      }\n\n      val (futureA, futureB) = runTxnsWithOrder__A_Start__B__A_End(txnA, txnB)\n      ThreadUtils.awaitResult(futureA, Duration.Inf)\n      ThreadUtils.awaitResult(futureB, Duration.Inf)\n\n      assertRowIdsAreValid(log)\n      val filesAfter = log.update().allFiles.collect()\n      val baseRowIdsAfter = filesAfter.map(f => f.path -> f.baseRowId.get).toMap\n      filesBefore.foreach { file =>\n        assert(baseRowIdsBefore(file.path) === baseRowIdsAfter(file.path))\n      }\n    }\n  }\n\n  test(\"Re-added files keep their row IDs after conflict with txn not \" +\n    \"updating high watermark\") {\n    withTempDir { dir =>\n      appendRows(dir, numRows = 10, numFiles = 2)\n      setIsolationLevel(dir)\n\n      val log = DeltaLog.forTable(spark, dir)\n      val filesBefore = log.update().allFiles.collect()\n      assert(filesBefore.length === 2)\n\n      // Adds one file that will change the high water mark, and one file that was\n      // in the table before.\n      def txnA(): Array[Row] = {\n        val file1 = createTestAddFile()\n        val oldFile = filesBefore.last\n        log.startTransaction().commit(Seq(oldFile, file1), ManualUpdate)\n        Array.empty\n      }\n\n      // Adds another file that was in the table before, so we have a conflict\n      // with a txn that does not change the high water mark.\n      def txnB(): Array[Row] = {\n        log.startTransaction().commit(Seq(filesBefore.head), ManualUpdate)\n        Array.empty\n      }\n\n      // One more transaction to change the high water mark, which will lead to txnA reassigning\n      // its row IDs.\n      def txnC(): Array[Row] = {\n        appendRows(dir, numRows = 30, numFiles = 3)\n        Array.empty\n      }\n\n      val (futureA, futureB, futureC) = runTxnsWithOrder__A_Start__B__C__A_End(txnA, txnB, txnC)\n      ThreadUtils.awaitResult(futureA, Duration.Inf)\n      ThreadUtils.awaitResult(futureB, Duration.Inf)\n      ThreadUtils.awaitResult(futureC, Duration.Inf)\n      assertRowIdsAreValid(log)\n\n      val baseRowIdsAfter = log.update().allFiles.collect().map(f => f.path -> f.baseRowId).toMap\n      filesBefore.foreach { file =>\n        assert(file.baseRowId === baseRowIdsAfter(file.path))\n      }\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/GenerateRowIDsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport org.apache.spark.sql.delta.{RowCommitVersion, RowId}\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\n\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.expressions.{Add, Alias, AttributeReference, Coalesce, EqualTo, Expression, FileSourceMetadataAttribute, GetStructField, MetadataAttributeWithLogicalName}\nimport org.apache.spark.sql.catalyst.plans.logical.{Filter, Join, LogicalPlan, Project}\nimport org.apache.spark.sql.execution.datasources.LogicalRelation\nimport org.apache.spark.sql.types.StructType\n\n/**\n * This test suite checks the optimized logical plans produced after applying the [[GenerateRowIDs]]\n * rule. It ensures that the rule is correctly applied to all Delta scans in different scenarios and\n * that the optimizer is able to remove redundant expressions or nodes when possible.\n */\nclass GenerateRowIDsSuite extends QueryTest with RowIdTestUtils {\n  protected val testTable: String = \"generateRowIDsTestTable\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    withRowTrackingEnabled(enabled = true) {\n      spark.range(start = 0, end = 20)\n        .toDF(\"id\")\n        .write\n        .format(\"delta\")\n        .saveAsTable(testTable)\n    }\n  }\n\n  override def afterAll(): Unit = {\n    sql(s\"DROP TABLE IF EXISTS $testTable\")\n    super.afterAll()\n  }\n\n  /**\n   * Test runner checking that the optimized plan for the given dataframe matches the expected plan.\n   * The expected plan is defined as a partial function `check`, e.g.:\n   * check = {\n   *   case Project(_, LogicalRelation) => // Do additional checks\n   * }\n   *\n   * Note: Pass `df` by name to avoid evaluating anything before test setup.\n   */\n  protected def testRowIdPlan(\n      testName: String, df: => DataFrame, rowTrackingEnabled: Boolean = true)(\n      check: PartialFunction[LogicalPlan, Unit]): Unit = {\n    test(testName) {\n      withRowTrackingEnabled(enabled = rowTrackingEnabled) {\n        check.applyOrElse(df.queryExecution.optimizedPlan, { plan: LogicalPlan =>\n          fail(s\"Unexpected optimized plan: $plan\")\n        })\n      }\n    }\n  }\n\n  /**\n   * Checks that the given expression corresponds to the expression used to generate Row IDs:\n   *   coalesce(_metadata.row_id, _metadata.base_row_id + _metadata.row_index).\n   */\n  protected def checkRowIdExpr(expr: Expression): Unit = {\n    expr match {\n      case Coalesce(\n            Seq(\n              GetStructField(FileSourceMetadataAttribute(_), _, _),\n              Add(\n                GetStructField(FileSourceMetadataAttribute(_), _, _),\n                GetStructField(FileSourceMetadataAttribute(_), _, _),\n                _))) => ()\n      case Alias(aliasedExpr, RowId.ROW_ID) => checkRowIdExpr(aliasedExpr)\n      case _ => fail(s\"Expression didn't match expected Row ID expression: $expr\")\n    }\n  }\n\n  /**\n   * Checks that the given expression corresponds to the an expression used to generate Row commit\n   * versions:\n   *   coalesce(_metadata.row_commit_version, _metadata.default_row_commit_version).\n   */\n  protected def checkRowCommitVersionExpr(expr: Expression): Unit = expr match {\n    case Coalesce(\n          Seq(\n            GetStructField(FileSourceMetadataAttribute(_), _, _),\n            GetStructField(FileSourceMetadataAttribute(_), _, _))) => ()\n    case Alias(aliasedExpr, RowCommitVersion.METADATA_STRUCT_FIELD_NAME) =>\n      checkRowCommitVersionExpr(aliasedExpr)\n    case _ => fail(s\"Expression didn't match expected Row commit version expression: $expr\")\n  }\n\n  /**\n   * Checks that a metadata column is present in `output` and that it contains the given fields and\n   * only these.\n   */\n  protected def checkMetadataFieldsPresent(\n      output: Seq[AttributeReference],\n      expectedFieldNames: Seq[String])\n    : Unit = {\n    val metadataSchema = output.collect {\n      case FileSourceMetadataAttribute(\n        MetadataAttributeWithLogicalName(\n          AttributeReference(_, schema: StructType, _, _), _)) => schema\n    }\n    assert(metadataSchema.nonEmpty, s\"No metadata column present in output: $output\")\n    assert(metadataSchema.head.fieldNames === expectedFieldNames,\n      \"Unexpected metadata fields present in the metadata output.\")\n  }\n\n  for (rowTrackingEnabled <- BOOLEAN_DOMAIN)\n  testRowIdPlan(s\"Regular column selected, rowTrackingEnabled: $rowTrackingEnabled\",\n      sql(s\"SELECT id FROM $testTable\"), rowTrackingEnabled) {\n    // No projection is added when no metadata column is selected.\n    case lr: LogicalRelation =>\n      assert(lr.output.map(_.name) === Seq(\"id\"), \"Scan list didn't match\")\n  }\n\n  for (rowTrackingEnabled <- BOOLEAN_DOMAIN)\n  testRowIdPlan(s\"Metadata column selected, rowTrackingEnabled: $rowTrackingEnabled\",\n      sql(s\"SELECT _metadata.file_path FROM $testTable\"), rowTrackingEnabled) {\n    // Selecting a metadata column adds a projection to unpack metadata fields (unrelated to Row\n    // IDs). Row IDs don't introduce an extra projection.\n    case Project(projectList, lr: LogicalRelation) =>\n      assert(projectList.map(_.name) === Seq(\"file_path\"), \"Project list didn't match\")\n      assert(lr.output.map(_.name) === Seq(\"id\", \"_metadata\"), \"Scan list didn't match\")\n      checkMetadataFieldsPresent(lr.output, Seq(\"file_path\"))\n  }\n\n  testRowIdPlan(\"Row ID column selected\", sql(s\"SELECT _metadata.row_id FROM $testTable\")) {\n    // Selecting Row IDs injects an expression to generate default Row IDs.\n    case Project(Seq(rowIdExpr), lr: LogicalRelation) =>\n      assert(rowIdExpr.name == RowId.ROW_ID)\n      checkRowIdExpr(rowIdExpr)\n      assert(lr.output.map(_.name) === Seq(\"id\", \"_metadata\"))\n      checkMetadataFieldsPresent(lr.output, Seq(\"row_index\", \"row_id\", \"base_row_id\"))\n  }\n\n  testRowIdPlan(\"Filter on Row ID column\",\n      sql(s\"SELECT * FROM $testTable WHERE _metadata.row_id = 5\")) {\n    // Filtering on Row IDs injects an expression to generate default Row IDs in the filter.\n    case Project(projectList, Filter(EqualTo(rowIdExpr, _), lr: LogicalRelation)) =>\n      assert(projectList.map(_.name) === Seq(\"id\"), \"Project list didn't match\")\n      checkRowIdExpr(rowIdExpr)\n      assert(lr.output.map(_.name) === Seq(\"id\", \"_metadata\"), \"Scan list didn't match\")\n      checkMetadataFieldsPresent(lr.output, Seq(\"row_index\", \"row_id\", \"base_row_id\"))\n  }\n\n  testRowIdPlan(\"Filter on Row ID in subquery\",\n      sql(s\"SELECT * FROM $testTable WHERE _metadata.row_id IN (SELECT id FROM $testTable)\")) {\n    // Filtering on Row IDs using a subquery injects an expression to generate default Row IDs in\n    // the subquery.\n    case Project(\n        projectList,\n        Join(right: LogicalRelation, left: LogicalPlan, _, joinCond, _)) =>\n      assert(projectList.map(_.name) === Seq(\"id\"), \"Project list didn't match\")\n      assert(right.output.map(_.name) === Seq(\"id\", \"_metadata\"), \"Outer scan output didn't match\")\n      checkMetadataFieldsPresent(right.output, Seq(\"row_index\", \"row_id\", \"base_row_id\"))\n      assert(left.output.map(_.name) === Seq(\"id\"), \"Subquery scan output didn't match\")\n      joinCond match {\n        case Some(EqualTo(rowIdExpr, _)) =>\n          checkRowIdExpr(rowIdExpr)\n        case _ => fail(s\"Subquery was transformed into a join with an unexpected condition.\")\n      }\n  }\n\n  testRowIdPlan(\"Row commit version column selected\",\n    sql(s\"SELECT _metadata.row_commit_version FROM $testTable\")) {\n    // Selecting Row commit versions injects an expression to generate default Row commit versions.\n    case Project(Seq(rowIdExpr), lr: LogicalRelation) =>\n      assert(rowIdExpr.name == RowCommitVersion.METADATA_STRUCT_FIELD_NAME)\n      checkRowCommitVersionExpr(rowIdExpr)\n      assert(lr.output.map(_.name) === Seq(\"id\", \"_metadata\"))\n      checkMetadataFieldsPresent(lr.output, Seq(\"default_row_commit_version\", \"row_commit_version\"))\n  }\n\n  testRowIdPlan(\"Filter on Row commit version column\",\n      sql(s\"SELECT * FROM $testTable WHERE _metadata.row_commit_version = 5\")) {\n    // Filtering on Row commit version injects an expression to generate default Row commit version\n    // in the filter.\n    case Project(projectList, Filter(EqualTo(rowIdExpr, _), lr: LogicalRelation)) =>\n      assert(projectList.map(_.name) === Seq(\"id\"), \"Project list didn't match\")\n      checkRowCommitVersionExpr(rowIdExpr)\n      assert(lr.output.map(_.name) === Seq(\"id\", \"_metadata\"), \"Scan list didn't match\")\n      checkMetadataFieldsPresent(lr.output, Seq(\"default_row_commit_version\", \"row_commit_version\"))\n  }\n\n  testRowIdPlan(\"Filter on Row commit version in subquery\",\n      sql(s\"SELECT * FROM $testTable WHERE _metadata.row_commit_version IN (SELECT id FROM \" +\n        s\"$testTable)\")) {\n    // Filtering on Row commit versions using a subquery injects an expression to generate default\n    // Row commit versions in the subquery.\n    case Project(\n        projectList,\n        Join(right: LogicalRelation, left: LogicalPlan, _, joinCond, _)) =>\n      assert(projectList.map(_.name) === Seq(\"id\"), \"Project list didn't match\")\n      assert(right.output.map(_.name) === Seq(\"id\", \"_metadata\"), \"Outer scan output didn't match\")\n      checkMetadataFieldsPresent(right.output,\n        Seq(\"default_row_commit_version\", \"row_commit_version\"))\n      assert(left.output.map(_.name) === Seq(\"id\"), \"Subquery scan output didn't match\")\n      joinCond match {\n        case Some(EqualTo(rowIdExpr, _)) =>\n          checkRowCommitVersionExpr(rowIdExpr)\n        case _ => fail(s\"Subquery was transformed into a join with an unexpected condition.\")\n      }\n  }\n\n  testRowIdPlan(\"Rename metadata column\",\n      sql(s\"SELECT renamed_metadata FROM (SELECT _metadata AS renamed_metadata FROM $testTable)\"\n    )) {\n    case Project(projectList, lr: LogicalRelation) =>\n      assert(projectList.map(_.name) === Seq(\"renamed_metadata\"), \"Project list didn't match\")\n      assert(lr.output.map(_.name) === Seq(\"id\", \"_metadata\"))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowIdCloneSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaIllegalStateException, DeltaLog, DeltaUnsupportedOperationException, RowId}\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass RowIdCloneSuite\n  extends QueryTest\n  with SharedSparkSession\n  with RowIdTestUtils {\n\n\n  val numRows = 10\n\n  test(\"clone assigns fresh row IDs when explicitly adding row IDs support\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = false, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = false, tableState = TableState.NON_EXISTING)) {\n      cloneTable(\n        targetTableName = \"target\",\n        sourceTableName = \"source\",\n        tblProperties = s\"'$rowTrackingFeatureName' = 'supported'\" :: Nil)\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(targetLog)\n      assert(RowId.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  test(\"clone a table with row tracking enabled into non-existing target \" +\n    \"enables row tracking even if disabled by default\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = true, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = false, tableState = TableState.NON_EXISTING)) {\n      withRowTrackingEnabled(enabled = false) {\n        cloneTable(targetTableName = \"target\", sourceTableName = \"source\")\n\n        val (targetLog, snapshot) =\n          DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n        assertRowIdsAreValid(targetLog)\n        assert(RowId.isSupported(targetLog.update().protocol))\n        assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n      }\n    }\n  }\n\n  for (enableRowIdsForSource <- BOOLEAN_DOMAIN)\n  test(\"self-clone an empty table does not change the table's Row Tracking \" +\n    s\"enablement and does not set Row IDs, enableRowIdsForSource=$enableRowIdsForSource\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = enableRowIdsForSource, tableState = TableState.EMPTY)) {\n      cloneTable(targetTableName = \"source\", sourceTableName = \"source\")\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"source\"))\n      assertRowIdsAreNotSet(targetLog)\n      assert(RowId.isSupported(targetLog.update().protocol) === enableRowIdsForSource)\n      assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata) === enableRowIdsForSource)\n    }\n  }\n\n  for {\n    rowIdsEnabledOnSource <- BOOLEAN_DOMAIN\n    targetTableState <- Seq(TableState.EMPTY, TableState.NON_EXISTING)\n  } {\n    test(\"clone from empty source into an empty or non-existing target \" +\n      s\"does not assign row IDs, rowIdsEnabledOnSource=$rowIdsEnabledOnSource, \" +\n      s\"targetTableState=$targetTableState\") {\n      withTables(\n        TableSetupInfo(tableName = \"source\",\n          rowIdsEnabled = rowIdsEnabledOnSource, tableState = TableState.EMPTY),\n        TableSetupInfo(tableName = \"target\",\n          rowIdsEnabled = false, tableState = targetTableState)) {\n        cloneTable(targetTableName = \"target\", sourceTableName = \"source\")\n\n        val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n        assertRowIdsAreNotSet(targetLog)\n        assert(RowId.isSupported(snapshot.protocol) === rowIdsEnabledOnSource)\n        assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata) === rowIdsEnabledOnSource)\n      }\n    }\n  }\n\n  for (targetTableState <- Seq(TableState.EMPTY, TableState.NON_EXISTING))\n  test(\"clone from empty source into an empty or non-existing target \" +\n    s\"using property override does not assign row IDs, targetTableState=$targetTableState\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = false, tableState = TableState.EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = false, tableState = targetTableState)) {\n\n      cloneTable(\n        targetTableName = \"target\",\n        sourceTableName = \"source\",\n        tblProperties = s\"'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = true\" :: Nil)\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreNotSet(targetLog)\n      assert(RowId.isSupported(snapshot.protocol))\n      assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  test(\"clone that add row ID feature using table property override \" +\n    \"doesn't enable row IDs on target\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = false, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = false, tableState = TableState.EMPTY)) {\n\n      cloneTable(\n        targetTableName = \"target\",\n        sourceTableName = \"source\",\n        tblProperties = s\"'$rowTrackingFeatureName' = 'supported'\" ::\n                        s\"'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION\" :: Nil)\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(targetLog)\n      assert(RowId.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  test(\"clone can disable row IDs using property override\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = true, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = true, tableState = TableState.EMPTY)) {\n\n      cloneTable(\n        targetTableName = \"target\",\n        sourceTableName = \"source\",\n        tblProperties = s\"'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = false\" :: Nil)\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(targetLog)\n      assert(RowId.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  test(\"clone throws error when assigning row IDs without stats\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n      withTable(\"source\", \"target\") {\n        withRowTrackingEnabled(enabled = false) {\n          spark.range(end = 10)\n            .write.format(\"delta\").saveAsTable(\"source\")\n        }\n        withRowTrackingEnabled(enabled = true) {\n          // enable stats to create table with row IDs\n          withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n            spark.range(0).write.format(\"delta\").saveAsTable(\"target\")\n          }\n          val err = intercept[DeltaUnsupportedOperationException] {\n            cloneTable(targetTableName = \"target\", sourceTableName = \"source\")\n          }\n          checkError(err, \"DELTA_CLONE_WITH_ROW_TRACKING_WITHOUT_STATS\")\n        }\n      }\n    }\n  }\n\n  test(\"clone can enable row tracking on empty target using property override\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = false, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = false, tableState = TableState.EMPTY)) {\n\n      cloneTable(\n        targetTableName = \"target\",\n        sourceTableName = \"source\",\n        tblProperties = s\"'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = true\" :: Nil)\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(targetLog)\n      assert(RowId.isSupported(snapshot.protocol))\n      assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  test(\"clone assigns fresh row IDs for empty target\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = false, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = true, tableState = TableState.EMPTY)) {\n      cloneTable(targetTableName = \"target\", sourceTableName = \"source\")\n\n      val (targetLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(targetLog)\n      assert(RowId.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  test(\"clone can't assign row IDs for non-empty target\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = false, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = true, tableState = TableState.NON_EMPTY)) {\n      assert(intercept[DeltaIllegalStateException] {\n        cloneTable(targetTableName = \"target\", sourceTableName = \"source\")\n      }.getErrorClass === \"DELTA_UNSUPPORTED_NON_EMPTY_CLONE\")\n    }\n  }\n\n  test(\"clone from source with row tracking enabled into existing empty target \" +\n    \"without row tracking enables row tracking\") {\n    withTables(\n      TableSetupInfo(tableName = \"source\",\n        rowIdsEnabled = true, tableState = TableState.NON_EMPTY),\n      TableSetupInfo(tableName = \"target\",\n        rowIdsEnabled = false, tableState = TableState.EMPTY)) {\n      cloneTable(targetTableName = \"target\", sourceTableName = \"source\")\n\n      val (targetLog, snapshot) =\n        DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(targetLog)\n      assert(RowId.isSupported(targetLog.update().protocol))\n      assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  def cloneTable(\n      targetTableName: String,\n      sourceTableName: String,\n      tblProperties: Seq[String] = Seq.empty): Unit = {\n    val tblPropertiesStr = if (tblProperties.nonEmpty) {\n      s\"TBLPROPERTIES ${tblProperties.mkString(\"(\", \",\", \")\")}\"\n    } else {\n      \"\"\n    }\n    sql(\n      s\"\"\"\n         |CREATE OR REPLACE TABLE $targetTableName\n         |SHALLOW CLONE $sourceTableName\n         |$tblPropertiesStr\n         |\"\"\".stripMargin)\n  }\n\n  final object TableState extends Enumeration {\n    type TableState = Value\n    val NON_EMPTY, EMPTY, NON_EXISTING = Value\n  }\n\n  case class TableSetupInfo(\n      tableName: String,\n      rowIdsEnabled: Boolean,\n      tableState: TableState.TableState)\n\n  private def withTables(tables: TableSetupInfo*)(f: => Unit): Unit = {\n    if (tables.isEmpty) {\n      f\n    } else {\n      val firstTable = tables.head\n      withTable(firstTable.tableName) {\n        firstTable.tableState match {\n          case TableState.NON_EMPTY | TableState.EMPTY =>\n            val rows = if (firstTable.tableState == TableState.NON_EMPTY) {\n              spark.range(start = 0, end = numRows, step = 1, numPartitions = 1)\n            } else {\n              spark.range(0)\n            }\n            withRowTrackingEnabled(enabled = firstTable.rowIdsEnabled) {\n              rows.write.format(\"delta\").saveAsTable(firstTable.tableName)\n            }\n          case TableState.NON_EXISTING =>\n        }\n\n        withTables(tables.drop(1): _*)(f)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowIdCreateReplaceTableSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog}\nimport org.apache.spark.sql.delta.RowId.extractHighWatermark\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass RowIdCreateReplaceTableSuite extends QueryTest\n  with SharedSparkSession with RowIdTestUtils {\n\n  private val numSourceRows = 50\n\n  test(\"Create or replace table with values list\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(\"target\") {\n        writeTargetTestData(withRowIds = true)\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n\n        val highWaterMarkBefore = extractHighWatermark(snapshot).get\n        createReplaceTargetTable(\n          commandName = \"CREATE OR REPLACE\",\n          query = \"SELECT * FROM VALUES (0, 0), (1, 1)\")\n\n        assertHighWatermarkIsCorrectAfterUpdate(\n          log, highWaterMarkBefore, expectedNumRecordsWritten = 2)\n        assertRowIdsAreLargerThanValue(log, highWaterMarkBefore)\n      }\n    }\n  }\n\n  test(\"Create or replace table with other delta table\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(\"source\", \"target\") {\n        writeTargetTestData(withRowIds = true)\n\n        writeSourceTestData(withRowIds = true)\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n\n        val highWaterMarkBefore = extractHighWatermark(snapshot).get\n        createReplaceTargetTable(commandName = \"CREATE OR REPLACE\", query = \"SELECT * FROM source\")\n\n        assertHighWatermarkIsCorrectAfterUpdate(\n          log, highWaterMarkBefore, expectedNumRecordsWritten = numSourceRows)\n        assertRowIdsAreLargerThanValue(log, highWaterMarkBefore)\n      }\n    }\n  }\n\n  test(\"Replace table with values list\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(\"target\") {\n        writeTargetTestData(withRowIds = true)\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n\n        val highWaterMarkBefore = extractHighWatermark(snapshot).get\n        createReplaceTargetTable(commandName = \"REPLACE\", query = \"SELECT * FROM VALUES (0), (1)\")\n\n        assertHighWatermarkIsCorrectAfterUpdate(\n          log, highWaterMarkBefore, expectedNumRecordsWritten = 2)\n        assertRowIdsAreLargerThanValue(log, highWaterMarkBefore)\n      }\n    }\n  }\n\n  test(\"Replace table with another delta table\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(\"source\", \"target\") {\n        writeTargetTestData(withRowIds = true)\n        val log = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n\n        writeSourceTestData(withRowIds = true)\n\n        val highWaterMarkBefore = extractHighWatermark(log.update()).get\n        createReplaceTargetTable(commandName = \"REPLACE\", query = \"SELECT * FROM source\")\n\n        assertHighWatermarkIsCorrectAfterUpdate(\n          log, highWaterMarkBefore, expectedNumRecordsWritten = numSourceRows)\n        assertRowIdsAreLargerThanValue(log, highWaterMarkBefore)\n      }\n    }\n  }\n\n  test(\"Replace table with row IDs with table without row IDs assigns new row IDs\") {\n    withTable(\"source\", \"target\") {\n      writeTargetTestData(withRowIds = true)\n      val log = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n\n      writeSourceTestData(withRowIds = false)\n\n      val highWaterMarkBefore = extractHighWatermark(log.update()).get\n      withRowTrackingEnabled(enabled = false) {\n        createReplaceTargetTable(commandName = \"REPLACE\", query = \"SELECT * FROM source\")\n      }\n\n      assertHighWatermarkIsCorrectAfterUpdate(\n        log, highWaterMarkBefore, expectedNumRecordsWritten = numSourceRows)\n    }\n  }\n\n  test(\"Replacing a table without row IDs with row IDs enabled assigns new row IDs\") {\n    withTable(\"source\", \"target\") {\n      writeTargetTestData(withRowIds = false)\n      writeSourceTestData(withRowIds = true)\n\n      val log = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreNotSet(log)\n\n      withRowTrackingEnabled(enabled = true) {\n        createReplaceTargetTable(\n          commandName = \"REPLACE\",\n          query = \"SELECT * FROM source\",\n          tblProperties = s\"'$rowTrackingFeatureName' = 'supported'\" ::\n            s\"'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION\" :: Nil)\n      }\n\n      assertRowIdsAreValid(log)\n\n      val df = spark.read.table(\"target\").select(\"*\", \"_metadata.row_id\")\n      checkAnswer(df, (0 until 50).map(i => Row(i, i)))\n    }\n  }\n\n  test(\"CREATE OR REPLACE on existing table without row IDs assigns new row IDs when enabling \" +\n    \"row IDs\") {\n    withTable(\"target\") {\n      writeTargetTestData(withRowIds = false)\n\n      val log = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreNotSet(log)\n\n      withRowTrackingEnabled(enabled = true) {\n        createReplaceTargetTable(\n          commandName = \"CREATE OR REPLACE\",\n          query = \"SELECT * FROM VALUES (0), (1)\",\n          tblProperties = s\"${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\" :: Nil)\n      }\n\n      assertRowIdsAreValid(log)\n\n      val df = spark.read.table(\"target\").select(\"*\", \"_metadata.row_id\")\n      checkAnswer(df, Seq(Row(0, 0), Row(1, 1)))\n    }\n  }\n\n  test(\"CTAS assigns new row IDs when immediately enabling row IDs\") {\n    withTable(\"target\") {\n      createReplaceTargetTable(\n        commandName = \"CREATE\",\n        query = \"SELECT * FROM VALUES (0), (1)\",\n        tblProperties = s\"${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\" :: Nil)\n\n      val log = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n      assertRowIdsAreValid(log)\n\n      val df = spark.read.table(\"target\").select(\"*\", \"_metadata.row_id\")\n      checkAnswer(df, Seq(Row(0, 0), Row(1, 1)))\n    }\n  }\n\n  test(\"CTAS assigns new row IDs when row IDs are by default enabled\") {\n    withTable(\"target\") {\n      withSQLConf(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"true\") {\n        createReplaceTargetTable(\n          commandName = \"CREATE\",\n          query = \"SELECT * FROM VALUES (0), (1)\")\n\n        val log = DeltaLog.forTable(spark, TableIdentifier(\"target\"))\n        assertRowIdsAreValid(log)\n\n        val df = spark.read.table(\"target\").select(\"*\", \"_metadata.row_id\")\n        checkAnswer(df, Seq(Row(0, 0), Row(1, 1)))\n      }\n    }\n  }\n\n  def createReplaceTargetTable(\n      commandName: String, query: String, tblProperties: Seq[String] = Seq.empty): Unit = {\n    val tblPropertiesStr = if (tblProperties.nonEmpty) {\n      s\"TBLPROPERTIES ${tblProperties.mkString(\"(\", \",\", \")\")}\"\n    } else {\n      \"\"\n    }\n    sql(\n      s\"\"\"\n         |$commandName TABLE target\n         |USING delta\n         |$tblPropertiesStr\n         |AS $query\n         |\"\"\".stripMargin)\n  }\n\n  def writeTargetTestData(withRowIds: Boolean): Unit = {\n    withRowTrackingEnabled(enabled = withRowIds) {\n      spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n        .write.format(\"delta\").saveAsTable(\"target\")\n    }\n  }\n\n  def writeSourceTestData(withRowIds: Boolean): Unit = {\n    withRowTrackingEnabled(enabled = withRowIds) {\n      spark.range(start = 0, end = numSourceRows, step = 1, numPartitions = 1)\n        .write.format(\"delta\").saveAsTable(\"source\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowIdSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DataFrameUtils, DeltaConfigs, DeltaIllegalStateException, DeltaLog, DeltaOperations, DeltaTableUtils, MaterializedRowCommitVersion, MaterializedRowId, RowCommitVersion, RowId, RowTrackingFeature, Serializable, SnapshotIsolation}\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.actions.{CommitInfo, Protocol}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.parquet.column.Encoding\nimport org.apache.parquet.column.ParquetProperties\nimport org.apache.parquet.hadoop.ParquetOutputFormat\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.execution.{FileSourceScanExec, SparkPlan}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{LongType, MetadataBuilder, StructField, StructType}\n\nclass RowIdSuite extends QueryTest\n    with SharedSparkSession\n    with ParquetTest\n    with RowIdTestUtils {\n  test(\"Enabling row IDs on existing table does not set row IDs as readable\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTable(\"tbl\") {\n        spark.range(10).write.format(\"delta\")\n          .saveAsTable(\"tbl\")\n\n        sql(\n          s\"\"\"\n             |ALTER TABLE tbl\n             |SET TBLPROPERTIES (\n             |'$rowTrackingFeatureName' = 'supported',\n             |'delta.minWriterVersion' = $TABLE_FEATURES_MIN_WRITER_VERSION)\"\"\".stripMargin)\n\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"tbl\"))\n        assert(RowId.isSupported(snapshot.protocol))\n        assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n      }\n    }\n  }\n\n  test(\"row ids are assigned when they are enabled\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreValid(log)\n\n        spark.range(start = 1000, end = 1500, step = 1, numPartitions = 3)\n          .write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n        assertRowIdsAreValid(log)\n      }\n    }\n  }\n\n  test(\"row ids are not assigned when they are disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreNotSet(log)\n\n        spark.range(start = 1000, end = 1500, step = 1, numPartitions = 3)\n          .write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n        assertRowIdsAreNotSet(log)\n      }\n    }\n  }\n\n  test(\"row ids can be disabled\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreValid(log)\n\n        sql(s\"ALTER TABLE delta.`${dir.getAbsolutePath}` \" +\n          s\"SET TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = false)\")\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getAbsolutePath),\n          (0 until 1000).map(Row(_)))\n      }\n    }\n  }\n\n  test(\"high watermark survives checkpointing\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log1 = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreValid(log1)\n\n        // Force a checkpoint and add an empty commit, so that we can delete the first commit\n        log1.checkpoint(log1.update())\n        log1.startTransaction().commit(Nil, ManualUpdate)\n        DeltaLog.clearCache()\n\n        // Delete the first commit and all checksum files to force the next read to read the high\n        // watermark from the checkpoint.\n        val fs = log1.logPath.getFileSystem(log1.newDeltaHadoopConf())\n        fs.delete(FileNames.unsafeDeltaFile(log1.logPath, version = 0), true)\n        fs.delete(FileNames.checksumFile(log1.logPath, version = 0), true)\n        fs.delete(FileNames.checksumFile(log1.logPath, version = 1), true)\n\n        spark.range(start = 1000, end = 1500, step = 1, numPartitions = 3)\n          .write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n        val log2 = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreValid(log2)\n      }\n    }\n  }\n\n  test(\"re-added files keep their row ids\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreValid(log)\n\n        val filesBefore = log.update().allFiles.collect()\n        val baseRowIdsBefore = filesBefore.map(f => f.path -> f.baseRowId.get).toMap\n\n        log.startTransaction().commit(filesBefore, ManualUpdate)\n        assertRowIdsAreValid(log)\n\n        val filesAfter = log.update().allFiles.collect()\n        val baseRowIdsAfter = filesAfter.map(f => f.path -> f.baseRowId.get).toMap\n\n        assert(baseRowIdsBefore == baseRowIdsAfter)\n      }\n    }\n  }\n\n  test(\"RESTORE retains high watermark\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        // version 0: high watermark = 9\n        spark.range(start = 0, end = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        val deltaTable = io.delta.tables.DeltaTable.forPath(spark, dir.getAbsolutePath)\n\n        // version 1: high watermark = 19\n        spark.range(start = 10, end = 20)\n          .write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n        val highWatermarkBeforeRestore = RowId.extractHighWatermark(log.update())\n\n        // back to version 0: high watermark should be still equal to before the restore.\n        deltaTable.restoreToVersion(0)\n\n        val highWatermarkAfterRestore = RowId.extractHighWatermark(log.update())\n        assert(highWatermarkBeforeRestore == highWatermarkAfterRestore)\n        assertRowIdsDoNotOverlap(log)\n\n        // version 1 (overridden): high watermark = 29\n        spark.range(start = 10, end = 20)\n          .write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n        assertHighWatermarkIsCorrectAfterUpdate(\n          log,\n          highWatermarkBeforeUpdate = highWatermarkAfterRestore.get,\n          expectedNumRecordsWritten = 10)\n        assertRowIdsDoNotOverlap(log)\n        val highWatermarkWithNewData = RowId.extractHighWatermark(log.update())\n\n        // back to version 0: high watermark should still be 29.\n        deltaTable.restoreToVersion(0)\n\n        val highWatermarkWithNewDataAfterRestore =\n          RowId.extractHighWatermark(log.update())\n        assert(highWatermarkWithNewData == highWatermarkWithNewDataAfterRestore)\n        assertRowIdsDoNotOverlap(log)\n      }\n    }\n  }\n\n  for (downgradeAllowed <- BOOLEAN_DOMAIN) {\n    test(s\"RESTORE with potential row tracking downgrade, downgradeAllowed=$downgradeAllowed\") {\n      withTempDir { dir =>\n        withRowTrackingEnabled(enabled = false) {\n          spark.range(5).write.format(\"delta\").save(dir.toString)\n        }\n        val log = DeltaLog.forTable(spark, dir)\n        val oldProtocolVersion = log.update().protocol\n        assert(!oldProtocolVersion.isFeatureSupported(RowTrackingFeature))\n        val protocolWithRowTracking = Protocol(minWriterVersion = TABLE_FEATURES_MIN_WRITER_VERSION)\n          .withFeature(RowTrackingFeature)\n        val newProtocolVersion = oldProtocolVersion.merge(protocolWithRowTracking)\n        log.upgradeProtocol(newProtocolVersion)\n        withSQLConf(\n          DeltaSQLConf.RESTORE_TABLE_PROTOCOL_DOWNGRADE_ALLOWED.key -> downgradeAllowed.toString) {\n          sql(s\"RESTORE TABLE delta.`$dir` VERSION AS OF 0\")\n        }\n        val restoredProtocolVersion = log.update().protocol\n        if (downgradeAllowed) {\n          assert(restoredProtocolVersion === oldProtocolVersion)\n        } else {\n          assert(restoredProtocolVersion === newProtocolVersion)\n        }\n      }\n    }\n  }\n\n  test(\"Check missing High Watermark for newly created empty table\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 0)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n        val log = DeltaLog.forTable(spark, dir)\n        assert(RowId.extractHighWatermark(log.update()) === None)\n        assertRowIdsAreNotSet(log)\n\n        spark.range(start = 0, end = 10)\n          .write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n        assert(RowId.extractHighWatermark(log.update()) === Some(9))\n        assertRowIdsAreValid(log)\n      }\n    }\n  }\n\n  test(\"row_id column with row ids disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 5)\n          .select((col(\"id\") + 10000L).as(\"row_id\"))\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getAbsolutePath),\n          (0 until 1000).map(i => Row(i + 10000L))\n        )\n      }\n    }\n  }\n\n  test(\"Throws error when assigning row IDs without stats\") {\n    withSQLConf(\n      DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"true\",\n      DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n      withTable(\"target\") {\n        val err = intercept[DeltaIllegalStateException] {\n          spark.range(end = 10).write.format(\"delta\").saveAsTable(\"target\")\n        }\n        checkError(err, \"DELTA_ROW_ID_ASSIGNMENT_WITHOUT_STATS\")\n      }\n    }\n  }\n\n  test(\"manually setting row ID high watermark is not allowed\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n\n        val log = DeltaLog.forTable(spark, dir)\n\n        val exception = intercept[IllegalStateException] {\n          log.startTransaction().commit(\n            Seq(RowTrackingMetadataDomain(rowIdHighWaterMark = 9001).toDomainMetadata),\n            ManualUpdate)\n        }\n        assert(exception.getMessage.contains(\n          \"Manually setting the Row ID high water mark is not allowed\"))\n      }\n    }\n  }\n\n  for (prevIsolationLevel <- Seq(\n    Serializable))\n  test(s\"Maintenance operations can downgrade to snapshot isolation, \" +\n    s\"previousIsolationLevel = $prevIsolationLevel\") {\n    withTable(\"table\") {\n      withSQLConf(\n        DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"true\",\n        DeltaConfigs.ISOLATION_LEVEL.defaultTablePropertyKey -> prevIsolationLevel.toString) {\n        // Create two files that will be picked up by OPTIMIZE\n        spark.range(10).repartition(2).write.format(\"delta\").saveAsTable(\"table\")\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"table\"))\n        val versionBeforeOptimize = snapshot.version\n\n        spark.sql(\"OPTIMIZE table\").collect()\n\n        val commitInfos = log.getChanges(versionBeforeOptimize + 1).flatMap(_._2).flatMap {\n          case commitInfo: CommitInfo => Some(commitInfo)\n          case _ => None\n        }.toList\n        assert(commitInfos.size == 1)\n        assert(commitInfos.forall(_.isolationLevel.get == SnapshotIsolation.toString))\n      }\n    }\n  }\n\n  test(\"ALTER TABLE cannot enable Row IDs on existing table\") {\n    withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key -> \"false\") {\n      withRowTrackingEnabled(enabled = false) {\n        withTable(\"tbl\") {\n          spark.range(10).write.format(\"delta\").saveAsTable(\"tbl\")\n\n          val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"tbl\"))\n          assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n\n          val err = intercept[UnsupportedOperationException] {\n            sql(s\"ALTER TABLE tbl \" +\n              s\"SET TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = true)\")\n          }\n          assert(err.getMessage === \"Cannot enable Row IDs on an existing table.\")\n        }\n      }\n    }\n  }\n\n  test(s\"CONVERT TO DELTA assigns row ids\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(10).repartition(1)\n          .write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n\n        sql(s\"CONVERT TO DELTA parquet.`$dir`\")\n\n        val log = DeltaLog.forTable(spark, dir)\n        assertRowIdsAreValid(log)\n        assert(extractMaterializedRowIdColumnName(log).isDefined)\n\n        val df = spark.read.format(\"delta\").load(dir.toString)\n        checkAnswer(\n          df.select(\"id\", \"_metadata.row_id\"),\n          (0 until 10).map(i => Row(i, i)))\n      }\n    }\n  }\n\n  test(s\"CONVERT TO DELTA NO STATISTICS throws error\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(10)\n          .write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n\n        val err = intercept[DeltaIllegalStateException] {\n          sql(s\"CONVERT TO DELTA parquet.`$dir` NO STATISTICS\")\n        }\n        checkError(err, \"DELTA_CONVERT_TO_DELTA_ROW_TRACKING_WITHOUT_STATS\",\n          parameters = Map(\n            \"statisticsCollectionPropertyKey\" -> DeltaSQLConf.DELTA_COLLECT_STATS.key,\n            \"rowTrackingTableFeatureDefaultKey\" -> defaultRowTrackingFeatureProperty,\n            \"rowTrackingDefaultPropertyKey\" ->\n              DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey))\n      }\n    }\n  }\n\n  test(s\"CONVERT TO DELTA without stats collection enabled throws error\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { dir =>\n        spark.range(10)\n          .write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n          val err = intercept[DeltaIllegalStateException] {\n            sql(s\"CONVERT TO DELTA parquet.`$dir`\")\n          }\n          checkError(err, \"DELTA_CONVERT_TO_DELTA_ROW_TRACKING_WITHOUT_STATS\",\n            parameters = Map(\n              \"statisticsCollectionPropertyKey\" -> DeltaSQLConf.DELTA_COLLECT_STATS.key,\n              \"rowTrackingTableFeatureDefaultKey\" -> defaultRowTrackingFeatureProperty,\n              \"rowTrackingDefaultPropertyKey\" ->\n                DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey))\n        }\n      }\n    }\n  }\n\n  test(\"Base Row ID metadata field has the expected type\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 20).toDF(\"id\")\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n          .select(QUALIFIED_BASE_ROW_ID_COLUMN_NAME)\n\n        val expectedBaseRowIdMetadata = new MetadataBuilder()\n          .putBoolean(\"__base_row_id_metadata_col\", value = true)\n          .build()\n\n        val expectedBaseRowIdField = StructField(\n          RowId.BASE_ROW_ID,\n          LongType,\n          nullable = false,\n          metadata = expectedBaseRowIdMetadata)\n\n        Seq(df.schema, df.queryExecution.analyzed.schema, df.queryExecution.optimizedPlan.schema)\n          .foreach { schema =>\n            assert(schema === new StructType().add(expectedBaseRowIdField))\n          }\n      }\n    }\n  }\n\n  test(\"Base Row IDs can be read with conflicting metadata column name\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"_metadata\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"_metadata\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath).select(\"_metadata\")\n\n        val dfWithConflict = df\n          .select(\n            col(\"_metadata\"),\n            df.metadataColumn(\"_metadata\")\n              .getField({RowId.BASE_ROW_ID})\n              .as(\"real_base_row_id\"))\n          .where(\"real_base_row_id % 5 = 0\")\n\n        checkAnswer(dfWithConflict,\n          (0 until 20).map(Row(_, 0)) ++\n            (20 until 30).map(Row(_, 20)))\n      }\n    }\n  }\n\n  test(\"Base Row IDs can be read through the Scala syntax\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n          .select(\"id\", QUALIFIED_BASE_ROW_ID_COLUMN_NAME)\n\n        checkAnswer(df,\n          (0 until 20).map(Row(_, 0)) ++\n            (20 until 30).map(Row(_, 20)))\n      }\n    }\n  }\n\n  test(\"Base Row IDs can be read through the SQL syntax\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val rows = sql(\n          s\"\"\"\n             |SELECT id, $QUALIFIED_BASE_ROW_ID_COLUMN_NAME FROM delta.`${tempDir.getAbsolutePath}`\n          \"\"\".stripMargin\n        )\n\n        checkAnswer(rows,\n          (0 until 20).map(Row(_, 0)) ++\n            (20 until 30).map(Row(_, 20)))\n      }\n    }\n  }\n\n  test(\"Filter by base Row IDs\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 3 files with base Row ID 0, 10 and 20 resp.\n        spark.range(start = 0, end = 10).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 10, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n          .where(col(QUALIFIED_BASE_ROW_ID_COLUMN_NAME) === 10)\n\n        checkAnswer(df, (10 until 20).map(Row(_)))\n      }\n    }\n  }\n\n  test(\"Base Row IDs can be read in subquery\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val rows = sql(\n          s\"\"\"\n             |SELECT * FROM delta.`${tempDir.getAbsolutePath}`\n             |WHERE id IN (\n             |  SELECT $QUALIFIED_BASE_ROW_ID_COLUMN_NAME\n             |  FROM delta.`${tempDir.getAbsolutePath}`)\n          \"\"\".stripMargin)\n\n        checkAnswer(rows, Seq(Row(0), Row(20)))\n      }\n    }\n  }\n\n  test(\"Filter by base Row IDs in subquery\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val rows = sql(\n          s\"\"\"\n             |SELECT * FROM delta.`${tempDir.getAbsolutePath}`\n             |WHERE id IN (\n             |  SELECT id\n             |  FROM delta.`${tempDir.getAbsolutePath}`\n             |  WHERE $QUALIFIED_BASE_ROW_ID_COLUMN_NAME = 20)\n          \"\"\".stripMargin)\n\n        checkAnswer(rows, (20 until 30).map(Row(_)))\n      }\n    }\n  }\n\n  test(\"row ids cannot be read when they are disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTempDir { dir =>\n        spark.range(start = 0, end = 1000, step = 1, numPartitions = 10)\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n\n        withAllParquetReaders {\n          val err = intercept[AnalysisException] {\n            spark.read.format(\"delta\").load(dir.toString).select(\"_metadata.row_id\").collect()\n          }\n          assert(err.getMessage.contains(\"No such struct field\"))\n        }\n      }\n    }\n  }\n\n\n  // Although readers don't have any row-id specific implementation, we still check that we\n  // are able to read row IDs to check that we switch to a reader that supports row IDs if the\n  // selected reader isn't able to.\n  test(\"row ids can be read back\") {\n    withRowTrackingEnabled(enabled = true) {\n      withAllParquetReaders {\n        assertRowIdsCanBeReadWithRowGroupSkipping(start = 50)\n        // Column mapping\n        withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\") {\n          assertRowIdsCanBeRead(start = 100, numRows = 100)\n        }\n      }\n    }\n  }\n\n  test(\"Can read both row id and row index\") {\n    withRowTrackingEnabled(enabled = true) {\n      withAllParquetReaders {\n        withTempDir { dir =>\n          val start = 10\n          val recordsPerFile = 5\n          spark.range(start = start, end = 20, step = 1, numPartitions = 2)\n            .toDF(\"value\")\n            .write\n            .format(\"delta\")\n            .save(dir.getAbsolutePath)\n          val df1 = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n            .select(RowId.QUALIFIED_COLUMN_NAME, \"value\", \"_metadata.row_index\")\n          checkAnswer(df1, (0 until 10).map(i => Row(i, start + i, i % recordsPerFile)))\n        }\n      }\n    }\n  }\n\n  test(\"Row ID metadata field has the expected type\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 20).toDF(\"id\")\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n          .select(RowId.QUALIFIED_COLUMN_NAME)\n\n        val expectedRowIdMetadata = new MetadataBuilder()\n          .putBoolean(\"__row_id_metadata_col\", value = true)\n          .build()\n\n        val expectedRowIdField = StructField(\n          RowId.ROW_ID,\n          LongType,\n          nullable = false,\n          metadata = expectedRowIdMetadata)\n\n        Seq(df.schema, df.queryExecution.analyzed.schema, df.queryExecution.optimizedPlan.schema)\n          .foreach { schema =>\n            assert(schema === new StructType().add(expectedRowIdField))\n          }\n      }\n    }\n  }\n\n  test(\"Row IDs can be read in subquery\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val rows = sql(\n          s\"\"\"\n             |SELECT * FROM delta.`${tempDir.getAbsolutePath}`\n             |WHERE id IN (\n             |  SELECT ${RowId.QUALIFIED_COLUMN_NAME}\n             |  FROM delta.`${tempDir.getAbsolutePath}`)\n           \"\"\".stripMargin)\n        checkAnswer(rows, (0 until 30).map(Row(_)))\n      }\n    }\n  }\n\n  test(\"Filter by Row IDs\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 100, end = 110).toDF(\"id\")\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n\n        val rows = spark.read.format(\"delta\")\n          .load(tempDir.getAbsolutePath).filter(\"_metadata.row_id % 2 = 0\")\n\n        checkAnswer(rows, (100.until(end = 110, step = 2)).map(Row(_)))\n      }\n    }\n  }\n\n  test(\"Filter by Row IDs in subquery\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        // Generate 2 files with base Row ID 0 and 20 resp.\n        spark.range(start = 0, end = 20).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").save(tempDir.getAbsolutePath)\n        spark.range(start = 20, end = 30).toDF(\"id\").repartition(1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val rows = sql(\n          s\"\"\"\n             |SELECT * FROM delta.`${tempDir.getAbsolutePath}`\n             |WHERE id IN (\n             |  SELECT id\n             |  FROM delta.`${tempDir.getAbsolutePath}`\n             |  WHERE ${RowId.QUALIFIED_COLUMN_NAME} % 5 = 0)\n           \"\"\".stripMargin)\n        checkAnswer(rows, Seq(Row(0), Row(5), Row(10), Row(15), Row(20), Row(25)))\n      }\n    }\n  }\n\n  test(\"Row IDs cannot be read if the table property is not enabled\") {\n    withRowTrackingEnabled(enabled = true) {\n      withAllParquetReaders {\n        withTable(\"target\") {\n          spark.range(10).repartition(1).write.format(\"delta\").saveAsTable(\"target\")\n          var df = spark.read.table(\"target\")\n          val expected = (0 until 10).map(i => Row(i, i))\n          // Check that row IDs can be read while table property is enabled\n          checkAnswer(df.select(\"id\", \"_metadata.row_id\"), expected)\n\n          sql(\n            s\"\"\"\n               |ALTER TABLE target\n               |SET TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = false)\n               |\"\"\".stripMargin)\n\n          df = spark.read.format(\"delta\").table(\"target\")\n          val err = intercept[AnalysisException] {\n            checkAnswer(df.select(\"id\", \"_metadata.row_id\"), expected)\n          }\n          assert(err.getMessage.contains(\"No such struct field\"))\n          // can still read other columns when table property disabled\n          checkAnswer(df.select(\"id\"), (0 until 10).map(Row(_)))\n        }\n      }\n    }\n  }\n\n  test(\"No row-group skipping on _metadata.row_id\") {\n    withAllParquetReaders {\n      withRowTrackingEnabled(enabled = true) {\n        withTempPath { path =>\n          val numRows = ParquetProperties.DEFAULT_MINIMUM_RECORD_COUNT_FOR_CHECK\n          val materializedColName = \"materialized_rowid_col\"\n\n          val df = spark.range(start = 0, end = numRows, step = 1, numPartitions = 1)\n            .toDF(\"value\")\n            .withColumn(materializedColName,\n              when(col(\"value\") < (numRows / 2), col(\"value\"))\n                .otherwise(lit(null)))\n          writeParquetWithMinimalRowGroupSize(df, path.toString)\n\n          sql(s\"CONVERT TO DELTA parquet.`$path`\")\n\n          setRowIdMaterializedColumnName(\n            DeltaLog.forTable(spark, path), colName = materializedColName)\n\n          checkFileLayout(\n            path,\n            numFiles = 1,\n            numRowGroupsPerFile = 1,\n            rowCountPerRowGroup = numRows)\n\n          // Filter by row IDs that are not part of the materialized column. If we don't take fresh\n          // row IDs into account, the row group will be skipped and the test will fail.\n          val dfWithSkippingOnRowId = spark.read.format(\"delta\").load(path.toString).select(\"value\")\n            .where(col(RowId.QUALIFIED_COLUMN_NAME) >= (numRows / 2))\n          checkAnswer(dfWithSkippingOnRowId, ((numRows / 2) until numRows).map(Row(_)))\n          checkScanMetrics(\n            dfWithSkippingOnRowId.queryExecution.executedPlan,\n            expectedNumOfRows = numRows)\n        }\n      }\n    }\n  }\n\n  test(\"No dictionary filtering on _metadata.row_id\") {\n    withAllParquetReaders {\n      withRowTrackingEnabled(enabled = true) {\n        withTempPath { path =>\n          val numRows = ParquetProperties.DEFAULT_MINIMUM_RECORD_COUNT_FOR_CHECK\n          val materializedColName = \"materialized_rowid_col\"\n\n          val df = spark.range(start = 0, end = numRows, step = 1, numPartitions = 1)\n            .toDF(\"value\")\n            .withColumn(materializedColName,\n              // This will cause dictionary encoding to be used, as the column has few unique\n              // values. Normally this shouldn't happen with row IDs, but we want to ensure that\n              // we can still read row IDs correctly if dictionary encoding is used.\n              when(col(\"value\") > 0, lit(1L))\n                .otherwise(lit(null)))\n          writeParquetWithMinimalRowGroupSize(df, path.toString)\n\n          sql(s\"CONVERT TO DELTA parquet.`$path`\")\n\n          setRowIdMaterializedColumnName(\n            DeltaLog.forTable(spark, path), colName = materializedColName)\n\n          checkFileLayout(\n            path,\n            numFiles = 1,\n            numRowGroupsPerFile = 1,\n            rowCountPerRowGroup = numRows)\n\n          // We can't check directly whether dictionary filtering will take place, but we can ensure\n          // that the row ID column is dictionary encoded, which should mean that the\n          // optimization is applied.\n          readRowGroupsPerFile(path).flatten.foreach { block =>\n            val rowIdColChunk = block.getColumns.asScala.find(\n              _.getPath.asScala.exists(_ == materializedColName)).get\n            assert(rowIdColChunk.getEncodings.contains(Encoding.PLAIN_DICTIONARY))\n          }\n\n          // Filter by row IDs that are not part of the materialized column. If we don't take fresh\n          // row IDs into account, the row group will be skipped and the test will fail.\n          val dfWithSkippingOnRowId = spark.read.format(\"delta\").load(path.toString).select(\"value\")\n            .where(col(RowId.QUALIFIED_COLUMN_NAME).equalTo(0))\n          checkAnswer(dfWithSkippingOnRowId, Row(0))\n          checkScanMetrics(\n            dfWithSkippingOnRowId.queryExecution.executedPlan,\n            expectedNumOfRows = numRows)\n        }\n      }\n    }\n  }\n\n  test(\"Reading row IDs when file is split and splits are recombined\") {\n    withSQLConf(\n      DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"true\",\n      // 10 byte partition sizes\n      SQLConf.FILES_MAX_PARTITION_BYTES.key -> \"10B\") {\n      withTempDir { dir =>\n        spark.range(end = 10).repartition(1)\n          // Add some more random columns, leads to multiple splits being recombined into a single\n          // partition\n          .selectExpr(\"id\", \"id as id2\", \"id as id3\", \"id as id4\")\n          .write.format(\"delta\").save(dir.toString)\n        val log = DeltaLog.forTable(spark, dir)\n        // Make sure we would create at least two splits of a single file\n        val necessarySplitSizeBytes = 20\n        assert(log.update().allFiles.collect().forall(_.size > necessarySplitSizeBytes))\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.toString).select(\"id\", RowId.QUALIFIED_COLUMN_NAME),\n          (0 until 10).map(i => Row(i, i)))\n      }\n    }\n  }\n\n  test(\"missing base row ids and default row commit versions\") {\n    val tableName = \"my_table\"\n    withTable(tableName) {\n      // Create a table with some rows without row tracking enabled.\n      spark.range(start = 0, end = 10).repartition(1).sortWithinPartitions(\"id\")\n        .write.format(\"delta\").mode(\"overwrite\").saveAsTable(tableName)\n\n      // Hack to enable row tracking without triggering a backfill.\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val snapshot = deltaLog.update()\n      val actions = Seq(\n        snapshot.metadata.copy(\n          configuration = snapshot.metadata.configuration ++ Map(\n            DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"true\",\n            MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP -> \"x\",\n            MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP -> \"y\"\n          )\n        )\n      )\n      deltaLog.startTransaction().commit(actions, DeltaOperations.ManualUpdate)\n\n      // Append some rows with base row ids and default row commit set on the files.\n      spark.range(start = 10, end = 20).repartition(1).sortWithinPartitions(\"id\")\n        .write.format(\"delta\").mode(\"append\").saveAsTable(tableName)\n\n      // Ensure that we cannot read the row id and row commit version by default.\n      intercept[SparkException] {\n        spark.read.table(tableName).select(\"id\", RowId.QUALIFIED_COLUMN_NAME).collect()\n      }\n      intercept[SparkException] {\n        spark.read.table(tableName).select(\"id\", RowCommitVersion.QUALIFIED_COLUMN_NAME).collect()\n      }\n\n      // Create a dataframe that allows reading missing row ids and row commit versions.\n      val originalPlan = spark.read.table(tableName).queryExecution.analyzed\n      val transformedPlan = DeltaTableUtils.transformFileFormat(originalPlan) {\n        case format =>\n          format.copy(\n            nullableRowTrackingConstantFields = true,\n            nullableRowTrackingGeneratedFields = true\n          )\n      }\n      val df = DataFrameUtils.ofRows(spark, transformedPlan)\n\n      checkAnswer(\n        df.select(\"id\", RowId.QUALIFIED_COLUMN_NAME, RowCommitVersion.QUALIFIED_COLUMN_NAME),\n        (0 until 10).map(i => Row(i, null, null)) ++\n          (0 until 10).map(i => Row(10 + i, i, 2))\n      )\n    }\n  }\n\n  protected def assertRowIdsCanBeRead(start: Int, numRows: Int): Unit = {\n    withTempDir { dir =>\n      spark.range(start, end = start + numRows, step = 1, numPartitions = 3)\n        .toDF(\"value\")\n        .write\n        .format(\"delta\")\n        .save(dir.getAbsolutePath)\n\n      val df1 = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        .select(RowId.QUALIFIED_COLUMN_NAME, \"value\")\n      checkAnswer(df1, (0L until numRows).map(i => Row(i, start + i)))\n\n      val df2 = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        .select(\"value\", RowId.QUALIFIED_COLUMN_NAME)\n      checkAnswer(df2, (0L until numRows).map(i => Row(start + i, i)))\n    }\n  }\n\n  protected def writeParquetWithMinimalRowGroupSize(df: DataFrame, path: String): Unit = {\n    df.write\n      .format(\"parquet\")\n      // The minimum row count in a row group is\n      // `ParquetProperties.DEFAULT_MINIMUM_RECORD_COUNT_FOR_CHECK`, if we specify a\n      // block size that can't accommodate the minimum row count, we'll write exactly\n      // the minimum row count per row group.\n      .option(ParquetOutputFormat.BLOCK_SIZE, 0)\n      .save(path)\n  }\n\n  protected def setRowIdMaterializedColumnName(log: DeltaLog, colName: String): Unit = {\n    val metadata = log.update().metadata\n    val configWithUpdatedRowIdColName = metadata.configuration + (\n      MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP -> colName)\n    // We need to remove the column from the schema as we are not allowed to have the\n    // materialized row ID column be part of the schema.\n    val schemaFieldsWithoutRowIdCol = metadata.schema.filterNot(_.name == colName)\n    val updatedMetadata = metadata.copy(\n      configuration = configWithUpdatedRowIdColName,\n      schemaString = metadata.schema.copy(fields = schemaFieldsWithoutRowIdCol.toArray).json)\n    log.startTransaction().commit(Seq(updatedMetadata), DeltaOperations.ManualUpdate)\n  }\n\n  protected def checkScanMetrics(plan: SparkPlan, expectedNumOfRows: Long): Unit = {\n    var numOutputRows = 0L\n    plan.foreach {\n      case f: FileSourceScanExec =>\n        numOutputRows += f.metrics(\"numOutputRows\").value\n      case _ => // Not a scan node, do nothing.\n    }\n    assert(expectedNumOfRows === numOutputRows)\n  }\n\n  private def assertRowIdsCanBeReadWithRowGroupSkipping(start: Int): Unit = {\n    val rowGroupRowCount = ParquetProperties.DEFAULT_MINIMUM_RECORD_COUNT_FOR_CHECK\n    // write at least two row groups\n    val numRows = rowGroupRowCount * 2\n    withTempPath { path =>\n      val df = spark.range(start, end = start + numRows, step = 1, numPartitions = 1).toDF(\"value\")\n      writeParquetWithMinimalRowGroupSize(df, path.toString)\n      sql(s\"CONVERT TO DELTA parquet.`$path`\")\n\n      import testImplicits._\n      checkFileLayout(\n        path,\n        numFiles = 1,\n        numRowGroupsPerFile = 2,\n        rowCountPerRowGroup = rowGroupRowCount)\n\n      val rowGroups = readRowGroupsPerFile(path).head\n      val minValueSecondRowGroup = rowGroups(1).getColumns.get(0).getStatistics.genericGetMin()\n\n      val df1 = spark.read.format(\"delta\").load(path.getAbsolutePath)\n        .filter($\"value\" >= minValueSecondRowGroup)\n        .select(RowId.QUALIFIED_COLUMN_NAME, \"value\")\n      checkAnswer(df1, (rowGroupRowCount until numRows).map(i => Row(i, start + i)))\n      checkScanMetrics(df1.queryExecution.executedPlan, expectedNumOfRows = rowGroupRowCount)\n\n      val df2 = spark.read.format(\"delta\").load(path.getAbsolutePath)\n        .filter($\"value\" >= minValueSecondRowGroup)\n        .select(\"value\", RowId.QUALIFIED_COLUMN_NAME)\n      checkAnswer(df2, (rowGroupRowCount until numRows).map(i => Row(start + i, i)))\n      checkScanMetrics(df2.queryExecution.executedPlan, expectedNumOfRows = rowGroupRowCount)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowIdTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{AddFile, CommitInfo, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.commands.backfill.{BackfillBatchStats, BackfillCommandStats}\nimport org.apache.spark.sql.delta.rowtracking.RowTrackingTestUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.hadoop.fs.Path\nimport org.apache.parquet.hadoop.metadata.BlockMetaData\n\nimport org.apache.spark.sql.{Column, DataFrame}\nimport org.apache.spark.sql.execution.datasources.FileFormat\n\ntrait RowIdTestUtils extends RowTrackingTestUtils with DeltaSQLCommandTest {\n  val QUALIFIED_BASE_ROW_ID_COLUMN_NAME = s\"${FileFormat.METADATA_NAME}.${RowId.BASE_ROW_ID}\"\n\n  protected def getRowIdRangeInclusive(f: AddFile): (Long, Long) = {\n    val min = f.baseRowId.get\n    val max = min + f.numPhysicalRecords.get - 1L\n    (min, max)\n  }\n\n  def assertRowIdsDoNotOverlap(log: DeltaLog): Unit = {\n    val files = log.update().allFiles.collect()\n\n    val sortedRanges = files\n      .map(f => (f.path, getRowIdRangeInclusive(f)))\n      .sortBy { case (_, (min, _)) => min }\n\n    for (i <- sortedRanges.indices.dropRight(1)) {\n      val (curPath, (_, curMax)) = sortedRanges(i)\n      val (nextPath, (nextMin, _)) = sortedRanges(i + 1)\n      assert(curMax < nextMin, s\"$curPath and $nextPath have overlapping row IDs\")\n    }\n  }\n\n  def assertHighWatermarkIsCorrect(log: DeltaLog): Unit = {\n    val snapshot = log.update()\n    val files = snapshot.allFiles.collect()\n\n    val highWatermarkOpt = RowId.extractHighWatermark(snapshot)\n    if (files.isEmpty) {\n      assert(highWatermarkOpt.isDefined)\n    } else {\n      val maxAssignedRowId = files\n        .map(a => a.baseRowId.get + a.numPhysicalRecords.get - 1L)\n        .max\n      assert(highWatermarkOpt.get == maxAssignedRowId)\n    }\n  }\n\n  def assertRowIdsAreValid(log: DeltaLog): Unit = {\n    assertRowIdsDoNotOverlap(log)\n    assertHighWatermarkIsCorrect(log)\n  }\n\n  def assertHighWatermarkIsCorrectAfterUpdate(\n      log: DeltaLog, highWatermarkBeforeUpdate: Long, expectedNumRecordsWritten: Long): Unit = {\n    val highWaterMarkAfterUpdate = RowId.extractHighWatermark(log.update()).get\n    assert((highWatermarkBeforeUpdate + expectedNumRecordsWritten) === highWaterMarkAfterUpdate)\n    assertRowIdsAreValid(log)\n  }\n\n  def assertRowIdsAreNotSet(log: DeltaLog): Unit = {\n    val snapshot = log.update()\n\n    val highWatermarks = RowId.extractHighWatermark(snapshot)\n    assert(highWatermarks.isEmpty)\n\n    val files = snapshot.allFiles.collect()\n    assert(files.forall(_.baseRowId.isEmpty))\n  }\n\n  def assertRowIdsAreLargerThanValue(log: DeltaLog, value: Long): Unit = {\n    log.update().allFiles.collect().foreach { f =>\n      val minRowId = getRowIdRangeInclusive(f)._1\n      assert(minRowId > value, s\"${f.toString} has a row id smaller or equal than $value\")\n    }\n  }\n\n  def extractMaterializedRowIdColumnName(log: DeltaLog): Option[String] = {\n    log.update().metadata.configuration.get(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP)\n  }\n\n  protected def readRowGroupsPerFile(dir: File): Seq[Seq[BlockMetaData]] = {\n    assert(dir.isDirectory)\n    readAllFootersWithoutSummaryFiles(\n      // scalastyle:off deltahadoopconfiguration\n      new Path(dir.getAbsolutePath), spark.sessionState.newHadoopConf())\n      // scalastyle:on deltahadoopconfiguration\n      .map(_.getParquetMetadata.getBlocks.asScala.toSeq)\n  }\n\n  protected def checkFileLayout(\n      dir: File,\n      numFiles: Int,\n      numRowGroupsPerFile: Int,\n      rowCountPerRowGroup: Int): Unit = {\n    val rowGroupsPerFile = readRowGroupsPerFile(dir)\n    assert(numFiles === rowGroupsPerFile.size)\n    for (rowGroups <- rowGroupsPerFile) {\n      assert(numRowGroupsPerFile === rowGroups.size)\n      for (rowGroup <- rowGroups) {\n        assert(rowCountPerRowGroup === rowGroup.getRowCount)\n      }\n    }\n  }\n\n  // easily add a rowid column to a dataframe by calling [[df.withMaterializedRowIdColumn]]\n  implicit class DataFrameRowIdColumn(df: DataFrame) {\n    def withMaterializedRowIdColumn(\n        materializedColumnName: String, rowIdColumn: Column): DataFrame =\n      RowId.preserveRowIdsUnsafe(\n        df, materializedColumnName, rowIdColumn, shouldSetIcebergReservedFieldId = false)\n\n    def withMaterializedRowCommitVersionColumn(\n        materializedColumnName: String, rowCommitVersionColumn: Column): DataFrame =\n      RowCommitVersion.preserveRowCommitVersionsUnsafe(\n        df, materializedColumnName, rowCommitVersionColumn, shouldSetIcebergReservedFieldId = false)\n  }\n\n  def extractMaterializedRowCommitVersionColumnName(log: DeltaLog): Option[String] = {\n    log.update().metadata.configuration\n      .get(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP)\n  }\n\n  /** Returns a Map of file path to base row ID from the AddFiles in a Snapshot. */\n  private def getAddFilePathToBaseRowIdMap(snapshot: Snapshot): Map[String, Long] = {\n    val allAddFiles = snapshot.allFiles.collect()\n    allAddFiles.foreach(addFile => assert(addFile.baseRowId.isDefined,\n      \"Every AddFile should have a base row ID\"))\n    allAddFiles.map(a => a.path -> a.baseRowId.get).toMap\n  }\n\n  /** Returns a Map of file path to base row ID from the RemoveFiles in a Snapshot. */\n  private def getRemoveFilePathToBaseRowIdMap(snapshot: Snapshot): Map[String, Long] = {\n    val removeFiles = snapshot.tombstones.collect()\n    removeFiles.foreach(removeFile => assert(removeFile.baseRowId.isDefined,\n      \"Every RemoveFile should have a base row ID\"))\n    removeFiles.map(r => r.path -> r.baseRowId.get).toMap\n  }\n\n  /** Check that the high watermark does not get updated if there aren't any new files */\n  def checkHighWatermarkBeforeAndAfterOperation(log: DeltaLog)(operation: => Unit): Unit = {\n    val prevSnapshot = log.update()\n    val prevHighWatermark = RowId.extractHighWatermark(prevSnapshot)\n    val prevAddFiles = getAddFilePathToBaseRowIdMap(prevSnapshot).keySet\n\n    operation\n\n    val newAddFiles = getAddFilePathToBaseRowIdMap(log.update()).keySet\n    val newFilesAdded = newAddFiles.diff(prevAddFiles).nonEmpty\n    val newHighWatermark = RowId.extractHighWatermark(log.update())\n\n    if (newFilesAdded) {\n      assert(prevHighWatermark.get < newHighWatermark.get,\n        \"The high watermark should have been updated after creating new files\")\n    } else {\n      assert(prevHighWatermark === newHighWatermark,\n        \"The high watermark should not be updated when there are no new file\")\n    }\n  }\n\n  /**\n   * Check that file actions do not violate Row ID invariants after an operation.\n   * More specifically:\n   *  - We do not reassign the base row ID to the same AddFile.\n   *  - RemoveFiles have the same base row ID as the corresponding AddFile\n   *    with the same file path.\n   */\n  def checkFileActionInvariantBeforeAndAfterOperation(log: DeltaLog)(operation: => Unit): Unit = {\n    val prevAddFilePathToBaseRowId = getAddFilePathToBaseRowIdMap(log.update())\n\n    operation\n\n    val snapshot = log.update()\n    val newAddFileBaseRowIdsMap = getAddFilePathToBaseRowIdMap(snapshot)\n    val newRemoveFileBaseRowIds = getRemoveFilePathToBaseRowIdMap(snapshot)\n\n    prevAddFilePathToBaseRowId.foreach { case (path, prevRowId) =>\n      if (newAddFileBaseRowIdsMap.contains(path)) {\n        val currRowId = newAddFileBaseRowIdsMap(path)\n        assert(currRowId === prevRowId,\n          \"We should not reassign base row IDs if it's the same AddFile\")\n      } else if (newRemoveFileBaseRowIds.contains(path)) {\n        assert(newRemoveFileBaseRowIds(path) === prevRowId,\n          \"No new base row ID should be assigned to RemoveFiles\")\n      }\n    }\n  }\n\n  /**\n   * Checks whether Row tracking is marked as preserved on the [[CommitInfo]] action\n   * committed during `operation`.\n   */\n  def rowTrackingMarkedAsPreservedForCommit(log: DeltaLog)(operation: => Unit): Boolean = {\n    val versionPriorToCommit = log.update().version\n\n    operation\n\n    val versionOfCommit = log.update().version\n    assert(versionPriorToCommit < versionOfCommit)\n    val commitInfos = log.getChanges(versionOfCommit).flatMap(_._2).flatMap {\n      case commitInfo: CommitInfo => Some(commitInfo)\n      case _ => None\n    }.toList\n    assert(commitInfos.size === 1)\n    commitInfos.forall { commitInfo =>\n      commitInfo.tags\n        .getOrElse(Map.empty)\n        .getOrElse(DeltaCommitTag.PreservedRowTrackingTag.key, \"false\").toBoolean\n    }\n  }\n\n  def checkRowTrackingMarkedAsPreservedForCommit(log: DeltaLog)(operation: => Unit): Unit = {\n    assert(rowTrackingMarkedAsPreservedForCommit(log)(operation))\n  }\n\n  /**\n   * Capture backfill related metrics for basic validation.\n   */\n  def validateSuccessfulBackfillMetrics(\n      expectedNumSuccessfulBatches: Int,\n      nameOfTriggeringOperation: String = DeltaOperations.OP_SET_TBLPROPERTIES)\n      (testBlock: => Unit): Unit = {\n    val backfillUsageRecords = Log4jUsageLogger.track {\n      testBlock\n    }.filter(_.metric == \"tahoeEvent\")\n\n    val backfillRecords = backfillUsageRecords\n      .filter(_.tags.get(\"opType\").contains(DeltaUsageLogsOpTypes.BACKFILL_COMMAND))\n    assert(backfillRecords.size === 1, \"Row Tracking Backfill should have \" +\n      \"only been executed once.\")\n\n    val backfillStats = JsonUtils.fromJson[BackfillCommandStats](backfillRecords.head.blob)\n    assert(backfillStats.wasSuccessful)\n    assert(backfillStats.numFailedBatches === 0)\n    assert(backfillStats.totalExecutionTimeMs > 0)\n    assert(backfillStats.numSuccessfulBatches === expectedNumSuccessfulBatches)\n    assert(backfillStats.nameOfTriggeringOperation === nameOfTriggeringOperation)\n\n    val parentTxnId = backfillStats.transactionId\n\n    val backfillBatchRecords = backfillUsageRecords\n      .filter(_.tags.get(\"opType\").contains(DeltaUsageLogsOpTypes.BACKFILL_BATCH))\n    val backfillBatchStats = backfillBatchRecords.map { backfillBatchRecord =>\n      JsonUtils.fromJson[BackfillBatchStats](backfillBatchRecord.blob)\n    }\n    // Sanity check that the individual child commits were successful.\n    backfillBatchStats.foreach { backfillBatchStat =>\n      assert(backfillBatchStat.wasSuccessful)\n      assert(backfillBatchStat.totalExecutionTimeInMs > 0)\n      assert(backfillBatchStat.initialNumFiles > 0)\n      assert(backfillBatchStat.parentTransactionId === parentTxnId)\n    }\n  }\n\n  /**\n   * This triggers backfill on the test table in this suite by calling the user-facing syntax\n   * `ALTER TABLE t SET TBLPROPERTIES()`. We check for proper protocol upgrade (if any) and\n   * that the table has valid row IDs afterwards.\n   */\n  def triggerBackfillOnTestTableUsingAlterTable(\n      targetTableName: String,\n      numRowsInTable: Int,\n      log: DeltaLog): Unit = {\n    val prevMinReaderVersion = log.update().protocol.minReaderVersion\n    val prevMinWriterVersion = log.update().protocol.minWriterVersion\n\n    val rowIdPropertyKey = DeltaConfigs.ROW_TRACKING_ENABLED.key\n\n    spark.sql(s\"ALTER TABLE $targetTableName SET TBLPROPERTIES ('$rowIdPropertyKey'=true)\")\n    assert(lastCommitHasRowTrackingEnablementOnlyTag(log))\n\n    // Check the protocol upgrade is as expected. We should only bump the minWriterVersion if\n    // necessary and add the table feature support for row IDs.\n    val snapshot = log.update()\n    val newProtocol = snapshot.protocol\n    assert(newProtocol.isFeatureSupported(RowTrackingFeature))\n    assert(newProtocol.minReaderVersion === prevMinReaderVersion,\n      \"The reader version does not need to be upgraded\")\n    val expectedMinWriterVersion = Math.max(\n      prevMinWriterVersion, TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n    assert(newProtocol.minWriterVersion === expectedMinWriterVersion)\n\n    // Tables should have the table property enabled at the end of ALTER TABLE command.\n    assert(RowId.isEnabled(newProtocol, snapshot.metadata))\n    val highWaterMarkBefore = -1L\n    assertRowIdsAreValid(log)\n    assertRowIdsAreLargerThanValue(log, highWaterMarkBefore)\n    assertHighWatermarkIsCorrectAfterUpdate(log, highWaterMarkBefore, numRowsInTable)\n  }\n\n  /**\n   * Returns a Boolean indicating whether the last commit on a Delta table has the tag\n   * [[DeltaCommitTag.RowTrackingEnablementOnlyTag.key]].\n   */\n  def lastCommitHasRowTrackingEnablementOnlyTag(log: DeltaLog): Boolean = {\n    val lastTableVersion = log.update().version\n    val (_, lastCommitActions) = log.getChanges(lastTableVersion).toList.last\n    val findRowTrackingEnablementOnlyTag = lastCommitActions.collectFirst {\n      case commitInfo: CommitInfo => DeltaCommitTag.getTagValueFromCommitInfo(\n        Some(commitInfo), DeltaCommitTag.RowTrackingEnablementOnlyTag.key)\n    }.flatten\n\n    findRowTrackingEnablementOnlyTag.exists(_.toBoolean)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingBackfillSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.concurrent.ExecutionException\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.{AddFile, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.AlterTableSetPropertiesDeltaCommand\nimport org.apache.spark.sql.delta.commands.backfill.{BackfillBatch, BackfillBatchStats, BackfillCommandStats, RowTrackingBackfillBatch, RowTrackingBackfillCommand, RowTrackingBackfillExecutor}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/** Test-only object that overrides a method to force a failure. */\ncase class FailingRowTrackingBackfillBatch(filesInBatch: Seq[AddFile])\n  extends BackfillBatch {\n  override val backfillBatchStatsOpType = DeltaUsageLogsOpTypes.BACKFILL_BATCH\n  override def prepareFilesAndCommit(\n      spark: SparkSession,\n      txn: OptimisticTransaction,\n      batchId: Int): Long = {\n    throw new IllegalStateException(\"mock exception for test\")\n  }\n}\n\nclass RowTrackingBackfillSuite\n    extends RowIdTestUtils\n    with SharedSparkSession {\n  protected val initialNumRows = 1000\n  protected val testTableName = \"target\"\n  protected val numFilesInTable = 10\n\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key, \"true\")\n\n  protected def createTable(tableName: String): Unit = {\n    // We disable Optimize Write to ensure the right number of files are created.\n    withSQLConf(\n      DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"false\"\n    ) {\n      spark.range(start = 0, end = initialNumRows, step = 1, numPartitions = numFilesInTable)\n        .write\n        .format(\"delta\")\n        .saveAsTable(tableName)\n\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      assert(snapshot.allFiles.count() === numFilesInTable)\n    }\n  }\n\n  /** Create the default test table used by this suite, which has row IDs disabled. */\n  protected def withTestTableWithNoRowTracking()(f: => Unit): Unit = {\n    withRowTrackingEnabled(enabled = false) {\n      withTable(testTableName) {\n        createTable(testTableName)\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n        assertRowIdsAreNotSet(log)\n        assert(!RowTracking.isSupported(snapshot.protocol))\n        assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n        f\n      }\n    }\n  }\n\n  protected def withTestTableWithRowTrackingDisabled()(f: => Unit): Unit = {\n    withTable(testTableName) {\n      // Do not create baseRowIds.\n      withRowTrackingEnabled(enabled = false) {\n        createTable(testTableName)\n      }\n      val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      AlterTableSetPropertiesDeltaCommand(\n        table = DeltaTableV2(spark, log.dataPath),\n        configuration = Map(\n          s\"delta.feature.${RowTrackingFeature.name}\" -> \"supported\",\n          DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"false\"))\n        .run(spark)\n      val snapshot = log.update()\n      assert(RowTracking.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n      f\n    }\n  }\n\n  /** Check the number of backfill commits in the Delta history. */\n  protected def assertNumBackfillCommits(expectedNumCommits: Int): Unit = {\n    val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n    val actualNumBackfillCommits = log.history.getHistory(None)\n      .filter(_.operation == DeltaOperations.ROW_TRACKING_BACKFILL_OPERATION_NAME)\n    assert(actualNumBackfillCommits.size === expectedNumCommits)\n  }\n\n  /** Check the protocol, the number of backfill commits and the table property. */\n  protected def assertBackfillWasSuccessful(expectedNumCommits: Int): Unit = {\n    val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n    assert(RowTracking.isSupported(snapshot.protocol))\n    assertNumBackfillCommits(expectedNumCommits)\n    assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n  }\n\n  test(\"getCandidateFilesToBackfill returns right files on tables with row IDs disabled\") {\n    withTestTableWithNoRowTracking() {\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n      val initialFilesInTable = snapshot.allFiles.collect().toSet\n      assert(initialFilesInTable.size === numFilesInTable,\n        \"We expect the AddFiles to be unique in this table.\")\n      // ALl files in a table with row ID disabled should require backfill.\n      val filesToBackfillBeforeTableFeatureSupport =\n        RowTrackingBackfillExecutor.getCandidateFilesToBackfill(log.update()).collect()\n      assert(filesToBackfillBeforeTableFeatureSupport.toSet === initialFilesInTable)\n      // Let's add table feature support without enabling row ID, i.e., force new files to have\n      // base row IDs. This is not the same as enabling the table property. This does not trigger\n      // backfill commits.\n      sql(\n        s\"\"\"ALTER TABLE $testTableName\n           |SET TBLPROPERTIES('$rowTrackingFeatureName' = 'supported')\"\"\".stripMargin)\n      val snapshotAfterTableSupport = log.update()\n      assert(RowTracking.isSupported(snapshotAfterTableSupport.protocol))\n      assert(!RowId.isEnabled(\n        snapshotAfterTableSupport.protocol, snapshotAfterTableSupport.metadata))\n      spark.range(end = 1).write.mode(\"append\").insertInto(testTableName)\n      val snapshotAfterInsert = log.update()\n      assert(snapshotAfterInsert.allFiles.count() === numFilesInTable + 1)\n      // Only the files before the table feature support should need backfill.\n      val filesToBackfillAfterTableFeatureSupport =\n        RowTrackingBackfillExecutor.getCandidateFilesToBackfill(snapshotAfterInsert).collect()\n      assert(filesToBackfillAfterTableFeatureSupport.toSet === initialFilesInTable)\n    }\n  }\n\n  test(\"Trigger backfill by calling command directly\") {\n    // No one should be calling this directly. We just want to unit test outside of ALTER TABLE.\n    withTestTableWithNoRowTracking() {\n      val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      RowTrackingBackfillCommand(\n        log,\n        nameOfTriggeringOperation = DeltaOperations.OP_SET_TBLPROPERTIES,\n        None).run(spark)\n\n      val snapshot = log.update()\n      assert(RowTracking.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n      assertNumBackfillCommits(expectedNumCommits = 1)\n    }\n  }\n\n  test(\"Calling the command directly should not trigger backfill when \" +\n    \"Row Tracking Backfill is not enabled\") {\n    withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key -> \"false\") {\n      // No one should be calling this directly. We just want to unit test outside of ALTER TABLE.\n      withTestTableWithNoRowTracking() {\n        val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n\n        val ex = intercept[UnsupportedOperationException] {\n          RowTrackingBackfillCommand(\n            log,\n            nameOfTriggeringOperation = DeltaOperations.OP_SET_TBLPROPERTIES,\n            None).run(spark)\n        }\n\n        assert(ex.getMessage === \"Cannot enable Row IDs on an existing table.\")\n      }\n    }\n  }\n\n  test(\"Trigger backfill using ALTER TABLE\") {\n    withTestTableWithNoRowTracking() {\n      val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n        triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n      }\n      assertBackfillWasSuccessful(expectedNumCommits = 1)\n\n      val snapshot = log.update()\n      assertRowIdsAreValid(log)\n      assert(RowTracking.isSupported(snapshot.protocol))\n      assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n\n      val prevHighWatermark = RowId.extractHighWatermark(log.update()).get\n\n      // Commits after the backfill command should have row IDs.\n      val numNewRows = 100\n      spark.range(end = numNewRows).write.insertInto(testTableName)\n      assertRowIdsAreValid(log)\n      assertHighWatermarkIsCorrectAfterUpdate(log, prevHighWatermark, numNewRows)\n    }\n  }\n\n  test(\"Backfill respects the max file limit per commit\") {\n    val maxNumFilesPerCommit = 3\n    withTestTableWithNoRowTracking() {\n      withSQLConf(\n        DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key ->\n          maxNumFilesPerCommit.toString) {\n        val expectedNumBackfillCommits =\n          Math.ceil(numFilesInTable.toDouble / maxNumFilesPerCommit.toDouble).toInt\n        validateSuccessfulBackfillMetrics(expectedNumBackfillCommits) {\n          val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n          triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n        }\n        assertBackfillWasSuccessful(expectedNumBackfillCommits)\n      }\n    }\n  }\n\n  test(\"Backfill on table with row tracking already enabled\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(testTableName) {\n        createTable(testTableName)\n        val (log, snapshot1) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n        var snapshot = snapshot1\n        assert(RowTracking.isSupported(snapshot.protocol))\n        assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n\n        // ALTER TABLE should do nothing other than set the table properties.\n        validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 0) {\n          triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n          assertNumBackfillCommits(expectedNumCommits = 0)\n        }\n\n        // Now, let's test UNSET TBLPROPERTIES\n        val rowTrackingKey = DeltaConfigs.ROW_TRACKING_ENABLED.key\n        sql(s\"ALTER TABLE $testTableName UNSET TBLPROPERTIES('$rowTrackingKey')\")\n        // This should not downgrade the table and it should not trigger any backfill.\n        // It will only change the table property.\n        snapshot = log.update()\n        assertNumBackfillCommits(expectedNumCommits = 0)\n        assert(RowTracking.isSupported(snapshot.protocol))\n        assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n        val prevMaterializedColumnName = extractMaterializedRowIdColumnName(log)\n        // If we re-enable the table property again, we should expect the materialized column name\n        // to be the same.\n        validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 0) {\n          triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n        }\n        assert(prevMaterializedColumnName === extractMaterializedRowIdColumnName(log))\n      }\n    }\n  }\n\n  test(\"Backfill on table that enabled the table feature separately\") {\n    withTestTableWithNoRowTracking() {\n      val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      // User decides to enable the table feature separately first.\n      sql(\n        s\"\"\"ALTER TABLE $testTableName\n           |SET TBLPROPERTIES('$rowTrackingFeatureName' = 'supported')\"\"\".stripMargin)\n      val snapshotAfterTableSupport = log.update()\n      assert(RowTracking.isSupported(snapshotAfterTableSupport.protocol))\n      assert(!RowId.isEnabled(\n        snapshotAfterTableSupport.protocol, snapshotAfterTableSupport.metadata))\n\n      val deltaHistory = log.history.getHistory(None)\n      // 1 commit to create table, 1 commit to upgrade protocol\n      assert(deltaHistory.size === 2)\n\n      // New data is inserted into the table. The new files should have base row ID.\n      val numNewRowsInserted = 1\n      spark.range(end = numNewRowsInserted).write.mode(\"append\").insertInto(testTableName)\n      val snapshotAfterInsert = log.update()\n      assert(snapshotAfterInsert.allFiles.count() === numFilesInTable + 1)\n      val numRowsInTableAfterInsert = initialNumRows + numNewRowsInserted\n\n      // Trigger backfill. Only the files before the table feature support should need backfill.\n      validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n        triggerBackfillOnTestTableUsingAlterTable(testTableName, numRowsInTableAfterInsert, log)\n        assertBackfillWasSuccessful(expectedNumCommits = 1)\n      }\n\n      // We should not try to upgrade the protocol again.\n      val deltaHistory2 = log.history.getHistory(None)\n      // We should have 3 more commits since the protocol upgrade:\n      // 1 commit to insert, 1 commit to backfill, 1 commit to set tbl properties.\n      assert(deltaHistory2.size === 5)\n      assertRowIdsAreValid(log)\n    }\n  }\n\n  test(\"Backfill should be idempotent\") {\n    withTestTableWithNoRowTracking() {\n      val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n        triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n      }\n\n      val snapshot = log.update()\n      assert(RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n\n      val materializedColumnName = extractMaterializedRowIdColumnName(log)\n      val deltaHistory = log.history.getHistory(None)\n      // 1 commit to create table, 1 commit to upgrade protocol, 1 commit for Backfill,\n      // 1 commit to set row tracking to enabled.\n      assert(deltaHistory.size === 4)\n\n      // We should not upgrade the protocol again and we should not backfill.\n      validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 0) {\n        triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n      }\n      assert(extractMaterializedRowIdColumnName(log) === materializedColumnName,\n        \"the materialized column name should not change\")\n      val deltaHistory2 = log.history.getHistory(None)\n      // 1 more commit to SET TBLPROPERTIES, nothing else.\n      assert(deltaHistory2.size === 5)\n      assert(deltaHistory2.head.operation === DeltaOperations.OP_SET_TBLPROPERTIES)\n    }\n  }\n\n  test(\"ALTER TABLE that don't enable row tracking should not backfill\") {\n    withTestTableWithNoRowTracking() {\n      val rowTrackingKey = DeltaConfigs.ROW_TRACKING_ENABLED.key\n      sql(s\"ALTER TABLE $testTableName SET TBLPROPERTIES('$rowTrackingKey' = false)\")\n      // This should not upgrade the table protocol and it should not trigger any backfill.\n      // It will only set the table property.\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n      assertNumBackfillCommits(expectedNumCommits = 0)\n      assert(!RowTracking.isSupported(snapshot.protocol))\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n      assert(!lastCommitHasRowTrackingEnablementOnlyTag(log),\n          \"RowTrackingEnablementOnly tag should not be set if the table property value is false\")\n    }\n  }\n\n  test(\"Trigger backfill using ALTER TABLE, but another property fails\") {\n    withTestTableWithNoRowTracking() {\n      // Enable column mapping\n      val columnMappingMode = DeltaConfigs.COLUMN_MAPPING_MODE.key\n      val minReaderKey = DeltaConfigs.MIN_READER_VERSION.key\n      val minWriterKey = DeltaConfigs.MIN_WRITER_VERSION.key\n      val rowTrackingKey = DeltaConfigs.ROW_TRACKING_ENABLED.key\n      sql(s\"\"\"ALTER TABLE $testTableName SET TBLPROPERTIES(\n             |'$minReaderKey' = '2',\n             |'$minWriterKey' = '5',\n             |'$columnMappingMode'='name')\"\"\".stripMargin)\n\n      val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      assert(!lastCommitHasRowTrackingEnablementOnlyTag(log),\n          \"RowTrackingEnablementOnly tag should not be set for other table properties\")\n\n      // Try to enable row IDs at the same time as we set column mapping mode to id.\n      // This should fail due to illegal column mapping mode change.\n      intercept[ColumnMappingUnsupportedException] {\n        sql(s\"ALTER TABLE $testTableName SET \" +\n          s\"TBLPROPERTIES('$columnMappingMode'='id', '$rowTrackingKey'=true)\")\n      }\n      // Despite the failure, there are side effects: the protocol is still upgraded and\n      // backfill commits occurred. However, row IDs should still be disabled because we were unable\n      // to set the property.\n      val snapshot = log.update()\n      assert(RowTracking.isSupported(snapshot.protocol))\n      assertNumBackfillCommits(expectedNumCommits = 1)\n      assert(!RowId.isEnabled(snapshot.protocol, snapshot.metadata))\n    }\n  }\n\n  /**\n   * Validate that if a user triggers backfill with other protocol changes within the ALTER TABLE\n   * command, we end up with a correct final protocol object.\n   *\n   * @param tableBelowTableFeatureLevel : Boolean indicating whether the test table has a protocol\n   *                                    writer version below table feature support prior to calling\n   *                                    the ALTER TABLE command that triggers backfill.\n   * @param isOtherTableFeatureLegacy   : Boolean indicating whether the other table feature being\n   *                                    supported along row tracking enablement is legacy (i.e. it\n   *                                    requires minWriterVersion and minReaderVersion below table\n   *                                    feature level).\n   */\n  private def checkProtocolUpgradeWithBackfill(\n      tableBelowTableFeatureLevel: Boolean,\n      isOtherTableFeatureLegacy: Boolean): Unit = {\n    val initialMinReaderVersion = ColumnMappingTableFeature.minReaderVersion\n    val initialMinWriterVersion = ColumnMappingTableFeature.minWriterVersion\n    withSQLConf(\n      DeltaConfigs.MIN_WRITER_VERSION.defaultTablePropertyKey -> initialMinWriterVersion.toString,\n      DeltaConfigs.MIN_READER_VERSION.defaultTablePropertyKey -> initialMinReaderVersion.toString\n    ) {\n      withTestTableWithNoRowTracking() {\n        val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n        val prevProtocol = snapshot.protocol\n        val expectedInitialProtocol = Protocol(initialMinReaderVersion, initialMinWriterVersion)\n        assert(prevProtocol === expectedInitialProtocol)\n\n        // Build the TBLPROPERTIES String for the ALTER TABLE command.\n        val rowTrackingKey = DeltaConfigs.ROW_TRACKING_ENABLED.key\n        var propertiesMap: Map[String, String] = Map(rowTrackingKey -> \"true\")\n        if (isOtherTableFeatureLegacy) {\n          val columnMappingMode = DeltaConfigs.COLUMN_MAPPING_MODE.key\n          propertiesMap = propertiesMap ++ Map(columnMappingMode -> \"name\")\n        } else {\n          val deletionVectorsKey = DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key\n          propertiesMap = propertiesMap ++ Map(deletionVectorsKey -> \"true\")\n        }\n        val tblPropertiesString = propertiesMap.map {\n          case (key, value) => s\"'$key'='$value'\"\n        }.mkString(\", \")\n\n        sql(s\"ALTER TABLE $testTableName SET TBLPROPERTIES($tblPropertiesString)\")\n        assert(!lastCommitHasRowTrackingEnablementOnlyTag(log),\n            \"RowTrackingEnablementOnly tag should not be set if ALTER TABLE is changing\" +\n            \" multiple table properties\")\n\n        val expectedFinalProtocol = if (isOtherTableFeatureLegacy) {\n          Protocol(\n            minReaderVersion = ColumnMappingTableFeature.minReaderVersion,\n            minWriterVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n            .withFeature(RowTrackingFeature)\n            .withFeature(ColumnMappingTableFeature)\n            .merge(prevProtocol)\n        } else {\n          Protocol(\n            minReaderVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_READER_VERSION,\n            minWriterVersion = TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n            .withFeature(RowTrackingFeature)\n            .withFeature(DeletionVectorsTableFeature)\n            .merge(prevProtocol)\n        }\n\n        assertBackfillWasSuccessful(expectedNumCommits = 1)\n        val finalSnapshot = log.update()\n        assert(finalSnapshot.protocol === expectedFinalProtocol)\n      }\n    }\n  }\n\n  for {\n    tableBelowTableFeatureLevel <- BOOLEAN_DOMAIN\n    isOtherTableFeatureLegacy <- BOOLEAN_DOMAIN\n  } {\n    test(\"ALTER TABLE does other protocol upgrade with backfill, \" +\n      s\"tableBelowTableFeatureLevel=$tableBelowTableFeatureLevel, \" +\n      s\"isOtherTableFeatureLegacy=$isOtherTableFeatureLegacy\") {\n      checkProtocolUpgradeWithBackfill(tableBelowTableFeatureLevel, isOtherTableFeatureLegacy)\n    }\n  }\n\n  test(\"lower MIN_WRITER_VERSION along with Row ID prop can upgrade protocol\") {\n    withTestTableWithNoRowTracking() {\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n      val prevProtocol = snapshot.protocol\n      assert(prevProtocol.minWriterVersion <\n        TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n\n      // Try to enable row IDs at the same time as we enable column mapping mode.\n      val columnMappingMode = DeltaConfigs.COLUMN_MAPPING_MODE.key\n      val minReaderKey = DeltaConfigs.MIN_READER_VERSION.key\n      val minWriterKey = DeltaConfigs.MIN_WRITER_VERSION.key\n      val rowTrackingKey = DeltaConfigs.ROW_TRACKING_ENABLED.key\n      // If we specify lower minWriterKey along with the row tracking prop, we will\n      // do an implicit upgrade to minWriterKey = 7.\n      sql(\n        s\"\"\"ALTER TABLE $testTableName SET TBLPROPERTIES(\n           |'$minReaderKey' = '2',\n           |'$minWriterKey' = '5',\n           |'$columnMappingMode'='name',\n           |'$rowTrackingKey'=true\n           |)\"\"\".stripMargin)\n      val afterProtocol = log.update().protocol\n      assert(afterProtocol.minReaderVersion === 2)\n      assert(\n        afterProtocol.minWriterVersion ===\n          TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION)\n      assert(afterProtocol.readerFeatures === None)\n      assert(\n        afterProtocol.writerFeatures === Some((\n          prevProtocol.implicitlyAndExplicitlySupportedFeatures ++\n            Protocol(2, 5).implicitlySupportedFeatures ++\n            Set(\n              ColumnMappingTableFeature,\n              DomainMetadataTableFeature, // Required by Row Tracking\n              RowTrackingFeature))\n          .map(_.name)))\n    }\n  }\n\n  test(\"BackfillCommandStats metrics are correct in case of failure\") {\n    withTestTableWithRowTrackingDisabled() {\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n\n      val allFiles = snapshot.allFiles.collect()\n      val numFilesInSuccessfulBackfillBatch = 3\n      val filesInSuccessfulBackfill = allFiles.take(numFilesInSuccessfulBackfillBatch)\n      val numFilesInFailingBackfillBatch = 2\n      val filesInFailingBackfill = allFiles.takeRight(numFilesInFailingBackfillBatch)\n\n      val txn = log.startTransaction()\n      val backfillStats = BackfillCommandStats(\n        transactionId = txn.txnId,\n        nameOfTriggeringOperation = DeltaOperations.OP_SET_TBLPROPERTIES)\n      val backfillExecutor = new RowTrackingBackfillExecutor(\n        spark,\n        log,\n        catalogTableOpt = None,\n        txn.txnId,\n        backfillStats\n      ) {\n        var batchIdx = 0\n        override def constructBatch(files: Seq[AddFile]): BackfillBatch = {\n          batchIdx += 1\n          if (batchIdx == 1) {\n            RowTrackingBackfillBatch(filesInSuccessfulBackfill)\n          } else {\n            FailingRowTrackingBackfillBatch(filesInFailingBackfill)\n          }\n        }\n      }\n\n      val backfillUsageRecords = Log4jUsageLogger.track {\n        intercept[IllegalStateException] {\n          backfillExecutor.run(maxNumFilesPerCommit = 3)\n        }\n      }.filter(_.metric == \"tahoeEvent\")\n\n      val backfillBatchRecords = backfillUsageRecords\n        .filter(_.tags.get(\"opType\").contains(DeltaUsageLogsOpTypes.BACKFILL_BATCH))\n      val backfillBatchStatsSeq = backfillBatchRecords.map { backfillBatchRecord =>\n        JsonUtils.fromJson[BackfillBatchStats](backfillBatchRecord.blob)\n      }\n\n      // Check parent backfill stats. The total execution time and wasSuccessful are manipulated in\n      // RowTrackingBackfillCommand, not BackfillExecutor so it is still 0 and false respectively.\n      assert(backfillStats.numFailedBatches === 1)\n      assert(backfillStats.numSuccessfulBatches === 1)\n      assert(backfillStats.transactionId === txn.txnId)\n      assert(backfillStats.nameOfTriggeringOperation === DeltaOperations.OP_SET_TBLPROPERTIES)\n\n      // Check the individual batch stats\n      assert(backfillBatchStatsSeq.size === 2)\n      backfillBatchStatsSeq.foreach { backfillBatchStat =>\n        backfillBatchStat.batchId match {\n          case 0 =>\n            assert(backfillBatchStat.wasSuccessful)\n            assert(backfillBatchStat.initialNumFiles === numFilesInSuccessfulBackfillBatch)\n            assert(backfillBatchStat.totalExecutionTimeInMs > 0)\n          case 1 =>\n            assert(!backfillBatchStat.wasSuccessful)\n            assert(backfillBatchStat.initialNumFiles === numFilesInFailingBackfillBatch)\n            // Failing batch can have totalExecutionTimeInMs be 0 because it ends faster.\n            assert(backfillBatchStat.totalExecutionTimeInMs >= 0)\n          case id => fail(s\"Unexpected batch id $id for RowTrackingBackfillBatch.\")\n        }\n        assert(backfillBatchStat.parentTransactionId === txn.txnId)\n      }\n    }\n  }\n\n  test(\"BackfillBatchStats failure leads to correct metrics\") {\n    withTestTableWithNoRowTracking() {\n      val table = DeltaTableV2(spark, TableIdentifier(testTableName))\n\n      val filesInBackfillBatch = table.snapshot.allFiles.head(2)\n      val batch = FailingRowTrackingBackfillBatch(filesInBackfillBatch)\n      val batchId = 17\n      val numSuccessfulBatch = new AtomicInteger(0)\n      val numFailedBatch = new AtomicInteger(0)\n\n      val backfillTxnId = \"backfill-txn-id\"\n      val txn = table.startTransactionWithInitialSnapshot()\n      val backfillUsageRecords = Log4jUsageLogger.track {\n        intercept[IllegalStateException] {\n          batch.execute(spark, backfillTxnId, batchId, txn, numSuccessfulBatch, numFailedBatch)\n        }\n      }.filter(_.metric == \"tahoeEvent\")\n\n      val backfillBatchRecords = backfillUsageRecords\n        .filter(_.tags.get(\"opType\").contains(DeltaUsageLogsOpTypes.BACKFILL_BATCH))\n      assert(backfillBatchRecords.size === 1, \"Row Tracking Backfill should have \" +\n        \"only been executed once.\")\n      val backfillBatchStats =\n        JsonUtils.fromJson[BackfillBatchStats](backfillBatchRecords.head.blob)\n\n      assert(numSuccessfulBatch.get() === 0)\n      assert(numFailedBatch.get() === 1)\n      assert(backfillBatchStats.batchId === batchId)\n      assert(!backfillBatchStats.wasSuccessful)\n      assert(backfillBatchStats.initialNumFiles === filesInBackfillBatch.length)\n      // Failing batch can have totalExecutionTimeInMs be 0 because it ends faster.\n      assert(backfillBatchStats.totalExecutionTimeInMs >= 0)\n      assert(backfillBatchStats.parentTransactionId === backfillTxnId)\n      assert(backfillBatchStats.transactionId != null && backfillBatchStats.transactionId.nonEmpty)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingCompactionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.hooks.AutoCompact\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute\nimport org.apache.spark.sql.catalyst.expressions.{EqualTo, Literal}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait RowTrackingCompactionTestsBase\n  extends QueryTest\n  with SharedSparkSession\n  with RowIdTestUtils {\n\n  protected def commandName: String\n\n  protected val numSoftDeletedRows: Int = 0\n\n  protected def createTable(\n      dir: File,\n      rowTrackingEnabled: Boolean,\n      partitioned: Boolean,\n      withMaterializedRowTrackingColumns: Boolean): Unit = {\n    withRowTrackingEnabled(rowTrackingEnabled) {\n      val partitionClause = if (partitioned) \"PARTITIONED BY (key)\" else \"\"\n      spark.sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getAbsolutePath}` (key LONG, value LONG)\n           |USING DELTA\n           |$partitionClause\"\"\".stripMargin)\n\n      def writeValues(start: Long, end: Long): Unit = {\n        val df = spark.range(start, end, step = 1, numPartitions = 1)\n          .select(col(\"id\") % 10 as \"key\", col(\"id\") as \"value\")\n        writeDf(dir, partitioned, withMaterializedRowTrackingColumns, df)\n      }\n      // Write 3 times to create 3 commits with different versions\n      writeValues(start = 0, end = 100)\n      writeValues(start = 100, end = 200)\n      writeValues(start = 200, end = 300)\n    }\n  }\n\n  protected def writeDf(\n      dir: File,\n      partitioned: Boolean,\n      withMaterializedRowTrackingColumns: Boolean,\n      _df: DataFrame): Unit = {\n    var df = _df\n    if (withMaterializedRowTrackingColumns) {\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val snapshot = deltaLog.update()\n      val materializedRowIdColName = MaterializedRowId.getMaterializedColumnNameOrThrow(\n        snapshot.protocol, snapshot.metadata, deltaLog.unsafeVolatileTableId)\n      df = df.withMaterializedRowIdColumn(materializedRowIdColName, col(\"value\"))\n      val materializedRowCommitVersionColName =\n        MaterializedRowCommitVersion.getMaterializedColumnNameOrThrow(\n          snapshot.protocol, snapshot.metadata, deltaLog.unsafeVolatileTableId)\n      df = df.withMaterializedRowCommitVersionColumn(\n        materializedRowCommitVersionColName, col(\"value\"))\n    }\n\n    var writer = df.write.format(\"delta\").mode(\"append\")\n    if (partitioned) {\n      writer = writer.partitionBy(\"key\")\n    }\n    writer.save(dir.getAbsolutePath)\n  }\n\n  protected def runStatement(statement: String): OptimizeMetrics = {\n    import testImplicits._\n    spark.sql(statement)\n      .select(col(\"metrics.*\"))\n      .as[OptimizeMetrics]\n      .head()\n  }\n\n  protected def runCompaction(dir: File, applyFilter: Boolean): OptimizeMetrics\n\n  protected def checkCompactionRowTrackingPreservation(dir: File, applyFilter: Boolean): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n    def readWithRowTrackingColumns(): DataFrame = {\n      spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        .select(\"value\", RowId.QUALIFIED_COLUMN_NAME, RowCommitVersion.QUALIFIED_COLUMN_NAME)\n    }\n\n    val rowsBeforeFirstCompaction = readWithRowTrackingColumns().collect()\n\n    // Run compaction, check that at least one file in the table was rewritten, and check that\n    // the fresh row IDs have been preserved.\n    checkRowTrackingMarkedAsPreservedForCommit(deltaLog) {\n      val metrics = runCompaction(dir, applyFilter)\n      assert(metrics.numFilesRemoved > 0)\n    }\n    checkAnswer(readWithRowTrackingColumns(), rowsBeforeFirstCompaction)\n\n    assertRowIdsAreValid(deltaLog)\n  }\n\n  protected def runTest(\n      partitioned: Boolean,\n      withMaterializedRowTrackingColumns: Boolean,\n      applyFilter: Boolean): Unit = {\n    withTempDir { dir =>\n      createTable(dir, rowTrackingEnabled = true, partitioned, withMaterializedRowTrackingColumns)\n      checkCompactionRowTrackingPreservation(dir, applyFilter)\n    }\n  }\n}\n\ntrait RowTrackingCompactionTests extends RowTrackingCompactionTestsBase {\n  test(s\"$commandName unpartitioned table with fresh row IDs\") {\n    runTest(partitioned = false, withMaterializedRowTrackingColumns = false, applyFilter = false)\n  }\n\n  test(s\"$commandName unpartitioned table with stable row IDs\") {\n    runTest(partitioned = false, withMaterializedRowTrackingColumns = true, applyFilter = false)\n  }\n\n  test(s\"$commandName partitioned table with fresh row IDs\") {\n    runTest(partitioned = true, withMaterializedRowTrackingColumns = false, applyFilter = false)\n  }\n\n  test(s\"$commandName partitioned table with stable row IDs\") {\n    runTest(partitioned = true, withMaterializedRowTrackingColumns = true, applyFilter = false)\n  }\n\n  test(s\"$commandName partitioned table with fresh row IDs and filter\") {\n    runTest(partitioned = true, withMaterializedRowTrackingColumns = false, applyFilter = true)\n  }\n\n  test(s\"$commandName partitioned table with stable row IDs and filter\") {\n    runTest(partitioned = true, withMaterializedRowTrackingColumns = true, applyFilter = true)\n  }\n\n  test(\"Row tracking marked as not preserved when row tracking disabled\") {\n    withTempDir { dir =>\n      withRowTrackingEnabled(enabled = false) {\n        createTable(\n          dir,\n          rowTrackingEnabled = false,\n          partitioned = false,\n          withMaterializedRowTrackingColumns = false)\n      }\n      val log = DeltaLog.forTable(spark, dir)\n      assert(!rowTrackingMarkedAsPreservedForCommit(log)(runCompaction(dir, applyFilter = false)))\n    }\n  }\n\n  test(s\"$commandName preserves row tracking on backfill enabled tables\") {\n    withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key -> \"true\") {\n      withTempDir { dir =>\n        createTable(\n          dir,\n          rowTrackingEnabled = false,\n          partitioned = false,\n          withMaterializedRowTrackingColumns = false)\n\n        val log = DeltaLog.forTable(spark, dir)\n        val snapshot = log.update()\n        assert(!RowTracking.isEnabled(snapshot.protocol, snapshot.metadata))\n\n        val numRows = spark.read.format(\"delta\").load(dir.getAbsolutePath).count()\n        validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n          triggerBackfillOnTestTableUsingAlterTable(\n            targetTableName = s\"delta.`${dir.getAbsolutePath}`\",\n            numRowsInTable = numRows.toInt + numSoftDeletedRows,\n            log)\n        }\n\n        checkCompactionRowTrackingPreservation(dir, applyFilter = false)\n      }\n    }\n  }\n}\n\ntrait RowTrackingOptimizeTests extends RowTrackingCompactionTests {\n  override protected def commandName: String = \"optimize\"\n\n  override def runCompaction(dir: File, applyFilter: Boolean): OptimizeMetrics = {\n    runStatement(\n      s\"\"\"OPTIMIZE delta.`${dir.getAbsolutePath}`\n         |${if (applyFilter) \"WHERE key = 5\" else \"\"}\"\"\".stripMargin)\n  }\n}\n\ntrait RowTrackingZorderTests extends RowTrackingCompactionTests {\n  override protected def commandName: String = \"z-order\"\n\n  override def runCompaction(dir: File, applyFilter: Boolean): OptimizeMetrics = {\n    runStatement(\n      s\"\"\"OPTIMIZE delta.`${dir.getAbsolutePath}`\n         |${if (applyFilter) \"WHERE key = 5\" else \"\"}\n         |ZORDER BY (value)\"\"\".stripMargin)\n  }\n}\n\ntrait RowTrackingAutoCompactionTests extends RowTrackingCompactionTests {\n  override protected def commandName: String = \"auto-compact\"\n\n  override def runCompaction(dir: File, applyFilter: Boolean): OptimizeMetrics = {\n    val log = DeltaLog.forTable(spark, dir)\n    val partitionPredicates = if (applyFilter) {\n      Seq(EqualTo(UnresolvedAttribute(\"key\"), Literal(5L)))\n    } else {\n      Nil\n    }\n\n    var metrics: OptimizeMetrics = null\n    withSQLConf(DeltaSQLConf.DELTA_AUTO_COMPACT_MIN_NUM_FILES.key -> \"0\") {\n      metrics = AutoCompact.compact(\n        spark,\n        log,\n        partitionPredicates).head\n    }\n    metrics\n  }\n}\n\ntrait RowTrackingPurgeTests extends RowTrackingCompactionTests with PersistentDVEnabled {\n\n  override protected val numSoftDeletedRows: Int = 3\n\n  override protected def commandName: String = \"purge\"\n\n  override protected def createTable(\n      dir: File,\n      rowTrackingEnabled: Boolean,\n      partitioned: Boolean,\n      withMaterializedRowTrackingColumns: Boolean): Unit = {\n    withRowTrackingEnabled(enabled = rowTrackingEnabled) {\n      val partitionClause = if (partitioned) \"PARTITIONED BY (key)\" else \"\"\n      spark.sql(\n        s\"\"\"CREATE TABLE delta.`${dir.getAbsolutePath}` (key LONG, value LONG, value2 LONG)\n           |USING DELTA\n           |$partitionClause\"\"\".stripMargin)\n\n      def writeValues(start: Long, end: Long): Unit = {\n        val df = spark.range(start, end, step = 1, numPartitions = 1)\n          .select(col(\"id\") % 10 as \"key\", col(\"id\") as \"value\", col(\"id\") as \"value2\")\n        writeDf(dir, partitioned, withMaterializedRowTrackingColumns, df)\n      }\n      // Write 3 times to create 3 commits with different versions\n      writeValues(start = 0, end = 100)\n      writeValues(start = 100, end = 200)\n      writeValues(start = 200, end = 300)\n\n      // Add Deletion Vectors to the table so that we can trigger purge.\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      removeRowsFromAllFilesInLog(deltaLog, numRowsToRemovePerFile = 1)\n    }\n  }\n\n  override def runCompaction(dir: File, applyFilter: Boolean): OptimizeMetrics = {\n    val statement =\n      s\"\"\"REORG TABLE delta.`${dir.getAbsolutePath}`\n         |${if (applyFilter) \"WHERE key = 5\" else \"\"}\n         |APPLY (PURGE)\"\"\".stripMargin\n\n    val metricsFirstRun = runStatement(statement)\n\n    // Check that a second of run of PURGE does not modify the table.\n    // This could be an issue with row IDs, as the materialized row ID is not part of the schema\n    // of the table.\n    val metricsSecondRun = runStatement(statement)\n    assert(metricsSecondRun.numFilesRemoved === 0)\n    assert(metricsSecondRun.numFilesAdded === 0)\n\n    metricsFirstRun\n  }\n}\n\ntrait RowTrackingCompactionTestsWithNameColumnMapping\n  extends RowTrackingCompactionTestsBase\n  with DeltaColumnMappingEnableNameMode\n\ntrait RowIdCompactionTestsWithIdColumnMapping\n  extends RowTrackingCompactionTestsBase\n  with DeltaColumnMappingEnableIdMode\n\nclass RowTrackingOptimizeSuite extends RowTrackingOptimizeTests\nclass RowTrackingOptimizeSuiteWithIdColumnMapping extends RowTrackingOptimizeTests\n  with RowIdCompactionTestsWithIdColumnMapping\nclass RowTrackingOptimizeSuiteWithNameColumnMapping extends RowTrackingOptimizeTests\n  with RowTrackingCompactionTestsWithNameColumnMapping\n\nclass RowTrackingZorderSuite extends RowTrackingZorderTests\nclass RowTrackingZorderSuiteWithIdColumnMapping extends RowTrackingZorderTests\n  with RowIdCompactionTestsWithIdColumnMapping\nclass RowTrackingZorderSuiteWithNameColumnMapping extends RowTrackingZorderTests\n  with RowTrackingCompactionTestsWithNameColumnMapping\n\nclass RowTrackingAutoCompactionSuite extends RowTrackingAutoCompactionTests\nclass RowTrackingAutoCompactionSuiteWithIdColumnMapping extends RowTrackingAutoCompactionTests\n  with RowIdCompactionTestsWithIdColumnMapping\nclass RowTrackingAutoCompactionSuiteWithNameColumnMapping extends RowTrackingAutoCompactionTests\n  with RowTrackingCompactionTestsWithNameColumnMapping\n\nclass RowTrackingPurgeSuite extends RowTrackingPurgeTests\nclass RowTrackingPurgeSuiteWithIdColumnMapping extends RowTrackingPurgeTests\n  with RowIdCompactionTestsWithIdColumnMapping\nclass RowTrackingPurgeSuiteWithNameColumnMapping extends RowTrackingPurgeTests\n  with RowTrackingCompactionTestsWithNameColumnMapping\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingDeleteSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.col\n\ntrait RowTrackingDeleteTestDimension\n  extends QueryTest\n  with RowIdTestUtils {\n  val testTableName = \"rowIdDeleteTable\"\n  val initialNumRows = 5000\n\n  /**\n   * Create a table and validate that it has Row IDs and the expected number of files.\n   */\n  def createTestTable(\n      tableName: String,\n      isPartitioned: Boolean,\n      multipleFilesPerPartition: Boolean): Unit = {\n    // We disable Optimize Write to ensure the right number of files are created.\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"false\") {\n      val numFilesPerPartition = if (isPartitioned && multipleFilesPerPartition) 2 else 1\n      val numRowsPerPartition = 100\n      val expectedNumFiles = if (isPartitioned) {\n        numFilesPerPartition * (initialNumRows / numRowsPerPartition)\n      } else {\n        10\n      }\n      val partitionColumnValue = (col(\"id\") / numRowsPerPartition).cast(\"int\")\n\n      val df = spark.range(0, initialNumRows, 1, expectedNumFiles)\n                    .withColumn(\"part\", partitionColumnValue)\n      if (isPartitioned) {\n        df.repartition(numFilesPerPartition)\n          .write\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .saveAsTable(tableName)\n      } else {\n        df.write\n          .format(\"delta\")\n          .saveAsTable(tableName)\n      }\n\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      assert(snapshot.allFiles.count() === expectedNumFiles)\n    }\n  }\n\n  def withRowIdTestTable(isPartitioned: Boolean)(f: => Unit): Unit = {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(testTableName) {\n        createTestTable(testTableName, isPartitioned, multipleFilesPerPartition = false)\n        f\n      }\n    }\n  }\n\n  /**\n   * Read the stable row IDs before and after the DELETE operation.\n   * Validate the row IDs are the same.\n   */\n  def deleteAndValidateStableRowId(whereCondition: Option[String]): Unit = {\n    val expectedRows: Array[Row] = spark.table(testTableName)\n      .select(\"id\", RowId.QUALIFIED_COLUMN_NAME, RowCommitVersion.QUALIFIED_COLUMN_NAME)\n      .where(s\"NOT (${whereCondition.getOrElse(\"true\")})\")\n      .collect()\n\n    val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n    checkRowTrackingMarkedAsPreservedForCommit(log) {\n      checkFileActionInvariantBeforeAndAfterOperation(log) {\n        checkHighWatermarkBeforeAndAfterOperation(log) {\n          executeDelete(whereCondition)\n        }\n      }\n    }\n\n    val actualDF = spark.table(testTableName)\n      .select(\"id\", RowId.QUALIFIED_COLUMN_NAME, RowCommitVersion.QUALIFIED_COLUMN_NAME)\n    checkAnswer(actualDF, expectedRows)\n  }\n\n  def executeDelete(whereCondition: Option[String]): Unit = {\n    val whereClause = whereCondition.map(cond => s\" WHERE $cond\").getOrElse(\"\")\n    spark.sql(s\"DELETE FROM $testTableName$whereClause\")\n  }\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"false\")\n  }\n}\n\ntrait RowTrackingDeleteSuiteBase extends RowTrackingDeleteTestDimension {\n  val subqueryTableName = \"subqueryTable\"\n\n  for {\n    isPartitioned <- BOOLEAN_DOMAIN\n    whereClause <- Seq(\n      \"id IN (5, 7, 11, 57, 66, 77, 79, 88, 91, 95)\", // 0.2%, 10 rows match\n      \"part = 5\", // 10%, 500 rows match\n      \"id % 20 = 0\", // 20%, 1000 rows match\n      \"id >= 0\" // 100%, 5000 rows match\n    )\n  } {\n    test(s\"DELETE preserves Row IDs, isPartitioned=$isPartitioned, whereClause=`$whereClause`\") {\n      withRowIdTestTable(isPartitioned) {\n        deleteAndValidateStableRowId(Some(whereClause))\n      }\n    }\n  }\n\n  test(\"Preserving Row Tracking - Subqueries are not supported in DELETE\") {\n    withRowIdTestTable(isPartitioned = false) {\n      withTable(subqueryTableName) {\n        createTestTable(subqueryTableName, isPartitioned = false, multipleFilesPerPartition = false)\n        val ex = intercept[AnalysisException] {\n          deleteAndValidateStableRowId(Some(\n            s\"id in (SELECT id FROM $subqueryTableName WHERE id = 7 OR id = 11)\"))\n        }.getMessage\n        assert(ex.contains(\"Subqueries are not supported in the DELETE\"))\n      }\n    }\n  }\n\n  for (isPartitioned <- BOOLEAN_DOMAIN)  {\n    test(s\"Multiple DELETEs preserve Row IDs, isPartitioned=$isPartitioned\") {\n      withRowIdTestTable(isPartitioned) {\n        val whereClause1 = \"id % 20 = 0\"\n        deleteAndValidateStableRowId(Some(whereClause1))\n        val whereClause2 = \"id % 10 = 0\"\n        deleteAndValidateStableRowId(Some(whereClause2))\n      }\n    }\n  }\n\n  for (isPartitioned <- BOOLEAN_DOMAIN) {\n    test(s\"Insert after DELETE on whole table, isPartitioned=$isPartitioned\") {\n      withRowIdTestTable(isPartitioned) {\n        // Delete whole table.\n        deleteAndValidateStableRowId(whereCondition = None)\n\n        spark.sql(s\"INSERT INTO $testTableName VALUES (1, 0), (2, 0), (3, 0), (4, 0)\")\n\n        // The new rows should have new row IDs.\n        val actualDF = spark.table(testTableName)\n          .select(\"id\", RowId.QUALIFIED_COLUMN_NAME)\n        assert(actualDF.filter(s\"row_id < $initialNumRows\").count() <= 0)\n      }\n    }\n  }\n\n  for {\n    isPartitioned <- BOOLEAN_DOMAIN\n  } {\n    test(s\"DELETE with optimized writes preserves Row ID, isPartitioned=$isPartitioned\") {\n      withRowTrackingEnabled(enabled = true) {\n        withTable(testTableName) {\n          createTestTable(testTableName, isPartitioned, multipleFilesPerPartition = true)\n          val whereClause = \"id % 20 = 0\"\n          withSQLConf(\n              DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"true\"\n          ) {\n            deleteAndValidateStableRowId(whereCondition = Some(whereClause))\n\n            val (log, snapshot) =\n              DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n            val currentNumFiles = snapshot.allFiles.count()\n\n            val deletionVectorEnabled = spark.conf\n              .getOption(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey)\n              .contains(\"true\")\n            val expectedNumFiles = if (deletionVectorEnabled) {\n              if (isPartitioned) 100 else 10\n            } else {\n              if (isPartitioned) 53 else 1\n            }\n\n            assert(currentNumFiles === expectedNumFiles,\n              s\"The current num files $currentNumFiles is unexpected for optimized writes\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Row tracking marked as not preserved when row tracking disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTable(testTableName) {\n        createTestTable(testTableName, isPartitioned = false, multipleFilesPerPartition = false)\n        val log = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n        assert(\n          !rowTrackingMarkedAsPreservedForCommit(log)(\n            executeDelete(whereCondition = Some(\"id = 5\"))))\n      }\n    }\n  }\n\n  for {\n    isPartitioned <- BOOLEAN_DOMAIN\n  } {\n    test(\"DELETE preserves Row ID on tables with row IDs enabled using backfill,\"\n      + s\"isPartitioned=$isPartitioned\") {\n      withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key -> \"true\") {\n        withRowTrackingEnabled(enabled = false) {\n          withTable(testTableName) {\n            createTestTable(testTableName, isPartitioned, multipleFilesPerPartition = false)\n            val (log, snapshot) = DeltaLog.\n              forTableWithSnapshot(spark, TableIdentifier(testTableName))\n            assert(!RowTracking.isEnabled(snapshot.protocol, snapshot.metadata))\n            validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n              triggerBackfillOnTestTableUsingAlterTable(testTableName, initialNumRows, log)\n            }\n            deleteAndValidateStableRowId(whereCondition = Some(\"id % 10 = 4\"))\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait RowTrackingDeleteDvBase\n  extends RowTrackingDeleteTestDimension\n  with PersistentDVEnabled {\n\n  for (isPartitioned <- BOOLEAN_DOMAIN) {\n    test(s\"DELETE with persistent DVs disabled, isPartitioned=$isPartitioned\") {\n      val whereClause = \"id % 20 = 0\"\n      withDeletionVectorsEnabled(enabled = false) {\n        withRowIdTestTable(isPartitioned) {\n          deleteAndValidateStableRowId(whereCondition = Some(whereClause))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingMergeSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{Dataset, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{col, lit}\n\ntrait RowTrackingMergeSuiteBase extends RowIdTestUtils\n  with DeltaDMLTestUtils\n  with MergeHelpers {\n  import testImplicits._\n\n  protected val SOURCE_TABLE_NAME = \"source\"\n\n  protected val numRows = 4000\n  protected val numUnmatchedRows = 2000\n\n  // Source table with 4000 rows with 'key' 2000 until 6000.\n  protected def createSourceTable(tableName: String, lastModifiedVersion: Long): Unit = {\n    spark.range(start = numUnmatchedRows, end = numUnmatchedRows + numRows).toDF(\"key\")\n      .withColumn(\"stored_id\", col(\"key\"))\n      .withColumn(\"last_modified_version\", lit(lastModifiedVersion))\n      .write.format(\"delta\").saveAsTable(tableName)\n  }\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey, value = \"true\")\n\n    createSourceTable(SOURCE_TABLE_NAME, lastModifiedVersion = 1L)\n  }\n\n  override protected def afterAll(): Unit = {\n    sql(s\"DROP TABLE IF EXISTS $SOURCE_TABLE_NAME\")\n  }\n\n  protected def withTestTable(\n      partitionedTarget: Boolean,\n      lastModifiedVersion: Long = 0L)(f: => Unit): Unit = {\n    val targetCreationDF = spark.range(end = numRows)\n      .toDF(\"key\")\n      .withColumn(\"stored_id\", col(\"key\"))\n      .withColumn(\"last_modified_version\", lit(lastModifiedVersion))\n\n    if (partitionedTarget) {\n      append(targetCreationDF\n        .withColumn(\"partition\", lit(0))\n        .repartition(numPartitions = 2), Seq(\"partition\"))\n    } else {\n      append(targetCreationDF.repartition(numPartitions = 2))\n    }\n\n    f\n  }\n\n  protected def executeMerge(\n      targetReference: String,\n      sourceReference: String,\n      clauses: MergeClause*): Unit = {\n    val mergeSQL =\n      s\"\"\"MERGE INTO $targetReference AS t\n         |USING $sourceReference AS s\n         |ON s.key = t.key\n         |${clauses.map(_.sql).mkString(\"\\n\")}\n         |\"\"\".stripMargin\n    sql(mergeSQL)\n  }\n\n  /**\n   * Create a test validating stable Row IDs and Row Commit Versions in MERGE. The test uses a fixed\n   * source table and a modifiable target table. By default the source and the target table have\n   * rows not matched in a join on 'key'.\n   *\n   *                  source table                                   target table\n   *\n   *                                                  |  key  | stored_id | last_modified_version |\n   *                                                  |  0    |   0       |           0           |\n   *                                                  |  1    |   1       |           0           |\n   *                                                  |  ...  |   ...     |          ...          |\n   *   |  key  | stored_id | last_modified_version |  |  1999 |   1999    |           0           |\n   *   |  2000 |   2000    |           1           |  |  2000 |   2000    |           0           |\n   *   |  2001 |   2001    |           1           |  |  2001 |   2001    |           0           |\n   *   |  ...  |   ...     |          ...          |  |  ...  |   ...     |          ...          |\n   *   |  3999 |   3999    |           1           |  |  3999 |   3999    |           0           |\n   *   |  4000 |   4000    |           1           |\n   *   |  ...  |   ...     |          ...          |\n   *   |  5999 |   5999    |           1           |\n   *\n   * Tests also include CDF validation, which only works if 'key' is not changed in update clauses.\n   */\n  protected def rowTrackingMergeTests(\n      name: String)(\n      partitionedTarget: Boolean = false,\n      targetAsView: Boolean = false,\n      source: => Option[String] = None,\n      targetTablePostSetupAction: Option[() => Unit] = None,\n      sqlConfs: Seq[(String, String)] = Seq.empty)(\n      clauses: MergeClause*)(\n      expected: Seq[Row],\n      numFilesAfterMerge: Option[Int] = None): Unit = {\n    test(name) {\n      withTestTable(partitionedTarget) {\n        // Post setup actions can be used to modify the target table, for example by inserting\n        // more rows into it.\n        targetTablePostSetupAction.foreach(_.apply())\n\n        val preMergeRowIdMapping = getPreMergeRowIdMapping\n\n        val sourceReference = source.getOrElse {\n          if (partitionedTarget) {\n            s\"(SELECT *, 0 AS partition FROM $SOURCE_TABLE_NAME)\"\n          } else {\n            SOURCE_TABLE_NAME\n          }\n        }\n\n        val targetReference = if (targetAsView) {\n          sql(s\"CREATE TEMPORARY VIEW target_view AS SELECT * FROM $tableSQLIdentifier\")\n          \"target_view\"\n        } else {\n          tableSQLIdentifier\n        }\n\n        withSQLConf(sqlConfs: _*) {\n          checkRowTrackingMarkedAsPreservedForCommit(deltaLog) {\n            checkFileActionInvariantBeforeAndAfterOperation(deltaLog) {\n              checkHighWatermarkBeforeAndAfterOperation(deltaLog) {\n                executeMerge(targetReference, sourceReference, clauses: _*)\n              }\n            }\n          }\n        }\n\n        checkAnswer(sql(s\"SELECT key, last_modified_version FROM $tableSQLIdentifier\"), expected)\n\n        validateRowIdsPostMerge(preMergeRowIdMapping)\n        validateRowCommitVersionsPostMerge()\n\n        if (numFilesAfterMerge.isDefined) {\n          val targetTableFiles = deltaLog.update().allFiles\n          assert(targetTableFiles.count() === numFilesAfterMerge.get,\n            s\"Expected ${numFilesAfterMerge.get} but got ${targetTableFiles.collect().mkString}\")\n        }\n\n        val cdfEnabled = spark.conf\n          .getOption(DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey)\n          .contains(\"true\")\n        if (cdfEnabled && targetTablePostSetupAction.isEmpty) {\n          assert(deltaLog.update().version === 1, \"Table has been modified more than once.\")\n\n          // The tableIdentifier will be overridden as a path identifier if a class/trait also mixes\n          // in DeltaDMLTestUtilsPathBased. So we need a check to ensure it's a name identifier\n          // before using it to get the catalog table.\n          val catalogTableOpt = if (DeltaTableIdentifier.isDeltaPath(spark, tableIdentifier)) {\n            None\n          } else {\n            Some(spark.sessionState.catalog.getTableMetadata(tableIdentifier))\n          }\n          // Only read CDF from version 1 (the version after the MERGE)\n          val cdfResult = CDCReader.changesToBatchDF(\n              deltaLog,\n              start = 1,\n              end = 1,\n              spark,\n              catalogTableOpt,\n              useCoarseGrainedCDC = true)\n              .select(\"stored_id\",\n                \"key\",\n                \"last_modified_version\",\n                \"_change_type\")\n              .collect()\n\n          val initialTableDf = spark.read.format(\"delta\")\n            .option(\"versionAsOf\", 0)\n            .table(tableSQLIdentifier)\n            .select(\"*\", \"_metadata.row_id\")\n            .alias(\"initial\")\n\n          val postMergeTableDf = spark.read.format(\"delta\")\n            .option(\"versionAsOf\", 1)\n            .table(tableSQLIdentifier)\n            .select(\"*\", \"_metadata.row_id\")\n            .alias(\"postMerge\")\n\n          // Outer join the table at the state before the merge with the state after the MERGE on\n          // 'key' under the assumption that 'key' is not altered by the MERGE.\n          val joinedInitialAndPost = initialTableDf\n            .join(postMergeTableDf, usingColumn = \"key\", joinType = \"fullouter\")\n            .select(\n              \"initial.key\",\n              \"initial.stored_id\",\n              \"initial.last_modified_version\",\n              \"initial.row_id\",\n              \"postMerge.key\",\n              \"postMerge.stored_id\",\n              \"postMerge.last_modified_version\",\n              \"postMerge.row_id\")\n            .collect()\n\n          joinedInitialAndPost.foreach {\n            case Row(_, storedIdInitial, lastModifiedVersionInitial, rowIdInitial,\n                _, storedIdPostMerge, lastModifiedVersionPostMerge, rowIdPostMerge) =>\n              if (lastModifiedVersionPostMerge == null) { // Row has been deleted.\n                val cdfEntries =\n                  cdfResult.filter(row => row.getAs(\"stored_id\") == storedIdInitial)\n\n                assert(cdfEntries.length === 1,\n                  s\"Invalid number of CDF entries for deleted row with stored_id = \" +\n                    s\"$storedIdInitial. ${cdfEntries.mkString}\")\n                assert(cdfEntries.head.getAs[String](\"_change_type\") === \"delete\",\n                s\"Invalid _change_type (!= delete) for inserted row with stored_id = \" +\n                  s\" $storedIdInitial\")\n                assert(rowIdInitial.asInstanceOf[Long] < numRows,\n                  \"Row ID for delete row not from initial range\")\n              } else if (lastModifiedVersionInitial == null) { // Row has been inserted.\n                val cdfEntries =\n                  cdfResult.filter(row => row.getAs(\"stored_id\") == storedIdPostMerge)\n\n                assert(cdfEntries.length === 1,\n                  s\"Invalid number of CDF entries for inserted row with stored_id = \" +\n                    s\" $storedIdPostMerge. ${cdfEntries.mkString}\")\n                assert(cdfEntries.head.getAs[String](\"_change_type\") === \"insert\",\n                  s\"Invalid _change_type (!= insert) for row with stored_id = $storedIdPostMerge\")\n                assert(rowIdPostMerge.asInstanceOf[Long] >= numRows,\n                  \"Row ID for inserted row from initial range\")\n              } else { // Row has been updated or is unchanged.\n                val cdfEntries =\n                  cdfResult.filter(row => row.getAs(\"stored_id\") == storedIdPostMerge)\n\n                if (lastModifiedVersionInitial != lastModifiedVersionPostMerge) {\n                  // Row has been updated\n                  assert(cdfEntries.length === 2,\n                    s\"Invalid number of CDF entries for updated/copied row with \" +\n                      s\"stored_id = $storedIdPostMerge. ${cdfEntries.mkString}\")\n                } else { // Row is untouched or copied.\n                  assert(Seq(0, 2).contains(cdfEntries.length),\n                    s\"Invalid number of CDF entries for updated/copied row with \" +\n                      s\"stored_id = $storedIdPostMerge. ${cdfEntries.mkString}\")\n                }\n              }\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * This method retrieves the mapping of stored_id to row_id from the target table\n   * before the merge operation.\n   * It groups the collected data by stored_id and ensures that each stored_id\n   * is associated with only row_id.\n   *\n   * @return A Map of stored_id to row_id before the merge operation.\n   */\n  protected def getPreMergeRowIdMapping: Map[Long, Long] = {\n    spark.table(tableSQLIdentifier)\n      .select(\"stored_id\", RowId.QUALIFIED_COLUMN_NAME)\n      .as[(Long, Long)]\n      .collect()\n      .groupBy(_._1)\n      .mapValues { values =>\n        assert(values.length === 1)\n        values.head._2\n      }.toMap\n  }\n\n  /**\n   * This method validates the row ids after the merge operation.\n   * It ensures that the rows that existed before the merge\n   * operation have kept their original row ids.\n   * For the newly inserted rows, it checks that they have been assigned fresh row ids\n   * that are greater than any row id before the merge operation.\n   *\n   * @param preMergeRowIdMapping The mapping of stored_id to row_id before the merge operation.\n   */\n  def validateRowIdsPostMerge(preMergeRowIdMapping: Map[Long, Long]): Unit = {\n    val highestRowIdPreMerge = preMergeRowIdMapping.values.max\n\n    val rowsAfterMerge = spark.read.table(tableSQLIdentifier)\n      .select(\"stored_id\", RowId.QUALIFIED_COLUMN_NAME)\n      .as[(Long, Long)]\n      .collect()\n\n    val (otherRows, insertedRows) =\n      rowsAfterMerge.partition { case (storedId, _) => preMergeRowIdMapping.contains(storedId) }\n\n    // Validate that rows kept their stable Row IDs.\n    otherRows.foreach { case (storedId, rowId) =>\n      assert(preMergeRowIdMapping(storedId) === rowId,\n        s\"Row ID has change for row with stored_id = $storedId\")\n    }\n\n    assert(insertedRows.length === insertedRows.map { case (_, rowId) => rowId }.distinct.length,\n      s\"Row IDs are not unique for inserted rows: ${insertedRows.mkString}\")\n\n    // Validate that inserted rows received a fresh Row ID.\n    insertedRows.foreach { case (storedId, rowId) =>\n      assert(rowId > highestRowIdPreMerge,\n        s\"Row ID not fresh for inserted row with stored_id $storedId\")\n    }\n  }\n\n  /**\n   * This method validates the row commit versions after the merge operation.\n   * It ensures that the row commit version for each row in the target table\n   * matches its last_modified_version.\n   * This is to ensure that the row commit version is updated correctly during\n   * the merge operation.\n   */\n  def validateRowCommitVersionsPostMerge(): Unit = {\n    val rowsAfterMerge = spark.read.table(tableSQLIdentifier)\n      .select(\"stored_id\", \"last_modified_version\", RowCommitVersion.QUALIFIED_COLUMN_NAME)\n      .as[(Long, Long, Long)]\n      .collect()\n\n    rowsAfterMerge.foreach { case (storedId, lastModifiedVersion, rowCommitVersion) =>\n      assert(rowCommitVersion === lastModifiedVersion,\n        s\"row commit version does not match for row with stored_id $storedId\")\n    }\n  }\n}\n\ntrait RowTrackingMergeCommonTests extends RowTrackingMergeSuiteBase {\n\n  rowTrackingMergeTests(\"INSERT NOT MATCHED only MERGE\")()(\n    clauses = insert(\"*\"))(\n    // The old rows that are in the target initially and the added rows.\n    expected = (0 until numRows).map(Row(_, 0L))\n      ++ (0 until numUnmatchedRows).map(id => Row(numRows + id, 1L))\n  )\n\n  rowTrackingMergeTests(\"DELETE MATCHED only MERGE\")()(\n    clauses = delete())(\n    // All unmatched rows.\n    expected = (0 until numUnmatchedRows).map(Row(_, 0L))\n  )\n\n  rowTrackingMergeTests(\"UPDATE MATCHED only MERGE\")()(\n    clauses = update(\"*\"))(\n    // Matched rows updated, other rows untouched.\n    expected = (0 until numUnmatchedRows).map(Row(_, 0L))\n      ++ (numUnmatchedRows until numRows).map(Row(_, 1L))\n  )\n\n  rowTrackingMergeTests(\"DELETE WHEN NOT MATCHED BY SOURCE only MERGE\")()(\n    clauses = deleteNotMatched())(\n    // Unmatched rows only.\n    expected = (numUnmatchedRows until numRows).map(Row(_, 0L))\n  )\n\n  rowTrackingMergeTests(\"UPDATE only WHEN NOT MATCHED BY SOURCE MERGE\")()(\n    clauses = updateNotMatched(\"last_modified_version = 1\"))(\n    // All rows not matched by source updated.\n    expected = (0 until numUnmatchedRows).map(Row(_, 1L))\n      ++ (numUnmatchedRows until numRows).map(Row(_, 0L))\n  )\n\n  rowTrackingMergeTests(\"UPDATE + DELETE WHEN NOT MATCHED BY SOURCE MERGE\")()(\n    clauses =\n      deleteNotMatched(\"t.stored_id % 2 = 0\"), updateNotMatched(\"last_modified_version = 1\"))(\n    expected = (0 until numUnmatchedRows).filter(_ % 2 == 1).map(Row(_, 1L)) ++\n               (numUnmatchedRows until numRows).map(Row(_, 0L)))\n\n  rowTrackingMergeTests(\"UPDATE only with source rows matching multiple target rows\")(\n    // Duplicate all target rows.\n    targetTablePostSetupAction = Some(() => {\n      append(spark.read.table(tableSQLIdentifier)\n        .withColumn(\"stored_id\", col(\"stored_id\") + numRows)\n        .withColumn(\"last_modified_version\", lit(1L))) }))(\n    clauses = update(\"t.last_modified_version = 2\"))(\n    // Updated 'key' and 'last_modified_version' for matched rows.\n    expected = (0 until numUnmatchedRows).flatMap(id => Seq(Row(id, 0L), Row(id, 1L)))\n      ++ (numUnmatchedRows until numRows).flatMap(id => Seq(Row(id, 2L), Row(id, 2L)))\n  )\n\n  rowTrackingMergeTests(\"DELETE only with source rows matching multiple target rows\")(\n    // Duplicate all target rows.\n    targetTablePostSetupAction = Some(() => {\n      append(spark.read.table(tableSQLIdentifier)\n        .withColumn(\"stored_id\", col(\"stored_id\") + numRows)\n        .withColumn(\"last_modified_version\", lit(1L))) }))(\n    clauses = delete())(\n    // Deleted all matches (2 target rows per source row).\n    expected = (0 until numUnmatchedRows).flatMap(id => Seq(Row(id, 0L), Row(id, 1L)))\n  )\n\n  rowTrackingMergeTests(\"Target is accessed through a view\")(targetAsView = true)(\n    clauses = update(\"*\"))(\n    expected = (0 until numUnmatchedRows).map(Row(_, 0L)) // Untouched.\n      ++ (numUnmatchedRows until numRows).map(Row(_, 1L)) // Updated.\n  )\n\n  rowTrackingMergeTests(\"Optimized writes on partitioned table\")(partitionedTarget = true)(\n    clauses = update(\"*\", \"s.key % 2 = 0\"), delete(), insert(\"*\"), deleteNotMatched())(\n    expected = (numUnmatchedRows.until(numRows, step = 2)).map(Row(_, 1L)) // Updated.\n      ++ (numRows until numRows + numUnmatchedRows).map(Row(_, 1L)), // Inserted.\n    numFilesAfterMerge = Some(1)\n  )\n\n  rowTrackingMergeTests(\"Optimized writes disabled on partitioned table\")(\n    partitionedTarget = true,\n    sqlConfs = Seq(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"false\"))(\n    clauses = update(\"*\", \"s.key % 2 = 0\"), delete(), insert(\"*\"), deleteNotMatched())(\n    expected = (numUnmatchedRows.until(numRows, step = 2)).map(Row(_, 1L)) // Updated.\n      ++ (numRows until numRows + numUnmatchedRows).map(Row(_, 1L)), // Inserted.\n    numFilesAfterMerge = Some(1)\n  )\n\n  rowTrackingMergeTests(\"Optimized writes on un-partitioned table\")(\n    sqlConfs = Seq(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"true\"))(\n    clauses = update(\"*\", \"s.stored_id % 2 = 0\"), delete(), insert(\"*\"), deleteNotMatched())(\n    expected = (numUnmatchedRows.until(numRows, step = 2)).map(Row(_, 1L)) // Updated.\n        ++ (numRows until numRows + numUnmatchedRows).map(Row(_, 1L)), // Inserted.\n    numFilesAfterMerge = Some(1)\n  )\n\n  rowTrackingMergeTests(\"Source and target referencing to the same table\")(\n    source = Some(\n      s\"(SELECT key, stored_id, 1L as last_modified_version FROM $tableSQLIdentifier)\"))(\n    clauses = update(\"*\"))(\n    // All rows updated.\n    expected = (0 until numRows).map(Row(_, 1L))\n  )\n\n  test(\"Multiple merges into the same table\") {\n    val numMerges = 5\n    require(numMerges <= numUnmatchedRows)\n\n    // Create the target table using half the rows from the source table.\n    append(spark.table(SOURCE_TABLE_NAME)\n      .withColumn(\"last_modified_version\", lit(0L))\n      .filter(\"key % 2 = 0\")\n      .repartition(numPartitions = 2))\n\n    val preMergeRowIdMapping = getPreMergeRowIdMapping\n\n    // Give the target the same rows as the source table, one row at a time.\n    for (i <- 0 until numMerges) {\n      executeMerge(\n        tableSQLIdentifier,\n        sourceReference = s\"(SELECT ${numUnmatchedRows + i} AS key)\",\n        clauses =\n          update(s\"last_modified_version = ${i + 1}\"),\n          insert(s\"(key, stored_id, last_modified_version) VALUES (key, key, ${i + 1})\"))\n    }\n\n    checkAnswer(sql(s\"SELECT key, last_modified_version FROM $tableSQLIdentifier\"),\n      // Updated rows.\n      (0 until numMerges).map(i => Row(numUnmatchedRows + i, i + 1))\n        // Untouched rows.\n        ++ (numUnmatchedRows + numMerges + 1).until(numRows + numUnmatchedRows, step = 2)\n          .map(Row(_, 0L))\n    )\n\n    validateRowIdsPostMerge(preMergeRowIdMapping)\n    validateRowCommitVersionsPostMerge()\n  }\n\n  test(\"Row tracking marked as not preserved when row tracking disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTestTable(partitionedTarget = false) {\n        assert(!rowTrackingMarkedAsPreservedForCommit(deltaLog)(\n          executeMerge(\n            tableSQLIdentifier,\n            SOURCE_TABLE_NAME,\n            clauses = update(\"*\"), insert(\"*\"))))\n      }\n    }\n  }\n\n  test(\"schema evolution, extra nested column in source - update\") {\n    import testImplicits._\n    val targetData = Seq((0L, 0L, 0L, (1, 10)), (1L, 1L, 0L, (2, 2000)))\n      .toDF(\"key\", \"stored_id\", \"last_modified_version\", \"x\")\n      .selectExpr(\n        \"key\",\n        \"stored_id\",\n        \"last_modified_version\",\n        \"named_struct('a', x._1, 'c', x._2) as x\")\n    append(targetData.repartition(1))\n\n    val sourceData = Seq((0L, 0L, 1L, (10, 100, 10000)))\n      .toDF(\"key\", \"stored_id\", \"last_modified_version\", \"x\")\n      .selectExpr(\n        \"key\",\n        \"stored_id\",\n        \"last_modified_version\",\n        \"named_struct('a', x._1, 'b', x._2, 'c', x._3) as x\")\n\n    val preMergeRowIdMapping = getPreMergeRowIdMapping\n    withTempView(\"src\") {\n      sourceData.createOrReplaceTempView(\"src\")\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        executeMerge(\n          tableSQLIdentifier,\n          sourceReference = \"src\",\n          clauses = update(\"*\"))\n      }\n    }\n    checkAnswer(sql(s\"SELECT stored_id, last_modified_version FROM $tableSQLIdentifier\"),\n      Seq(Row(0L, 1L), Row(1L, 0L)))\n    validateRowIdsPostMerge(preMergeRowIdMapping)\n    validateRowCommitVersionsPostMerge()\n  }\n\n  test(\"MERGE preserves Row Tracking on tables enabled using backfill\") {\n    withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key -> \"true\") {\n      val SOURCE_TABLE_NAME_FOR_BACKFILL_TEST = \"backfilled_source\"\n      withTable(SOURCE_TABLE_NAME_FOR_BACKFILL_TEST) {\n        createSourceTable(SOURCE_TABLE_NAME_FOR_BACKFILL_TEST, lastModifiedVersion = 4L)\n\n        withRowTrackingEnabled(enabled = false) {\n          withTestTable(partitionedTarget = false, lastModifiedVersion = 2L) {\n            val snapshot = deltaLog.update()\n            assert(!RowTracking.isEnabled(snapshot.protocol, snapshot.metadata))\n            validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n              triggerBackfillOnTestTableUsingAlterTable(tableSQLIdentifier, numRows, deltaLog)\n            }\n            val preMergeRowIdMapping = getPreMergeRowIdMapping\n\n            executeMerge(\n              tableSQLIdentifier,\n              SOURCE_TABLE_NAME_FOR_BACKFILL_TEST,\n              clauses = update(\"*\"), insert(\"*\"))\n\n            validateRowIdsPostMerge(preMergeRowIdMapping)\n            validateRowCommitVersionsPostMerge()\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait RowTrackingMergeDVMixin extends RowTrackingMergeSuiteBase\n  with DeletionVectorsTestUtils {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    enableDeletionVectors(spark, delete = true, update = true, merge = true)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingRemovalConcurrencySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport java.io.File\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.spark.sql.delta.{ConflictResolutionTestUtils, DeltaConfigs, DeltaErrors, DeltaIllegalStateException, DeltaLog, DeltaOperations, DeltaTableFeatureException, ProtocolChangedException, RemovableFeature, RowTrackingFeature, TableFeature}\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.actions.{AddFile, DropTableFeatureUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.AlterTableSetPropertiesDeltaCommand\nimport org.apache.spark.sql.delta.commands.backfill.{BackfillCommandStats, BackfillExecutionObserver, BackfillExecutor, RowTrackingBackfillCommand, RowTrackingBackfillExecutor, RowTrackingUnBackfillCommand, RowTrackingUnBackfillExecutor}\nimport org.apache.spark.sql.delta.concurrency.{PhaseLockingTestMixin, TransactionExecutionTestMixin}\nimport org.apache.spark.sql.delta.fuzzer.{AtomicBarrier, OptimisticTransactionPhases, PhaseLockingTransactionExecutionObserver => TransactionObserver}\nimport org.apache.spark.sql.delta.implicits._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.{SparkConf, SparkThrowable}\nimport org.apache.spark.sql.Row\nimport org.apache.spark.util.ThreadUtils\n\nclass RowTrackingRemovalConcurrencySuite\n    extends RowTrackingRemovalSuiteBase\n    with ConflictResolutionTestUtils\n    with TransactionExecutionTestMixin\n    with PhaseLockingTestMixin {\n\n  protected val areDVsEnabled = true\n  private val ignoreSuspensionConf =\n    DeltaSQLConf.DELTA_ROW_TRACKING_IGNORE_SUSPENSION.key ->  \"true\"\n\n  protected override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey,\n      areDVsEnabled.toString)\n\n  protected def dropRowTrackingTransaction(tableName: String): Array[Row] = {\n    sql(s\"\"\"ALTER TABLE $tableName DROP FEATURE ${RowTrackingFeature.name}\"\"\").collect()\n  }\n\n  protected def enableRowTrackingTransaction(tableName: String): Array[Row] = {\n    sql(s\"\"\"ALTER TABLE $tableName\n           |SET TBLPROPERTIES('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true')\n           |\"\"\".stripMargin)\n      .collect()\n  }\n\n  protected def backfillTransaction(deltaLog: DeltaLog): Seq[Row] = {\n    RowTrackingBackfillCommand(\n      deltaLog,\n      nameOfTriggeringOperation = \"TEST\",\n      catalogTableOpt = None).run(spark)\n  }\n\n  protected def unBackfillTransaction(deltaLog: DeltaLog): Seq[Row] = {\n    RowTrackingUnBackfillCommand(\n      deltaLog,\n      nameOfTriggeringOperation = \"TEST\",\n      catalogTableOpt = None).run(spark)\n  }\n\n  /**\n   * Represents a delete transaction by a third party writer that does not respect\n   * property `delta.rowTrackingSuspended`.\n   */\n  case class ThirdPartyDelete(\n      condition: String) extends TestTransaction(Map(ignoreSuspensionConf)) {\n\n    override val name: String = s\"THIRD PARTY DELETE($condition)($sqlConfStr)\"\n    override def dataChange: Boolean = true\n    override def toSQL(tableName: String): String = s\"DELETE FROM $tableName WHERE $condition\"\n  }\n\n  /**\n   * Represents an update transaction by a third party writer that does not respect\n   * property `delta.rowTrackingSuspended`.\n   */\n  case class ThirdPartyUpdate(\n      set: String,\n      condition: String) extends TestTransaction(Map(ignoreSuspensionConf)) {\n\n    override val name: String = s\"THIRD PARTY UPDATE($set)($condition)($sqlConfStr)\"\n    override def dataChange: Boolean = true\n    override def toSQL(tableName: String): String = s\"UPDATE $tableName SET $set WHERE $condition\"\n  }\n\n  /**\n   * Represents an insert transaction by a third party writer that does not respect\n   * property `delta.rowTrackingSuspended`.\n   */\n  case class ThirdPartyInsert(\n      start: Long,\n      end: Long,\n      numPartitions: Int = 2,\n      sqlConf: Map[String, String] = Map(ignoreSuspensionConf)) extends TestTransaction(sqlConf) {\n\n    override val name: String = s\"THIRD PARTY INSERT($start-$end)($sqlConfStr)\"\n    override def dataChange: Boolean = true\n    override def toSQL(tableName: String): String = {\n      throw new UnsupportedOperationException(\"No SQL implementation for ThirdPartyInsert\")\n    }\n\n    override def executeImpl(ctx: TestContext): Unit = {\n      withSQLConf(sqlConf.toSeq: _*) {\n        // This should generate baseRowIds.\n        spark.range(start, end, step = 1, numPartitions)\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .insertInto(s\"delta.`${ctx.deltaLog.dataPath}`\")\n      }\n    }\n  }\n\n  /**\n   * Test transaction that performs a protocol downgrade for a given feature.\n   * Note, it does not add the checkpointProtection feature.\n   */\n  case class DowngradeProtocol(\n      feature: TableFeature with RemovableFeature) extends TestTransaction(Map.empty) {\n\n    override val name: String = s\"Downgrade(${feature.name})($sqlConfStr)\"\n    override def dataChange: Boolean = false\n\n    override def toSQL(tableName: String): String = {\n      throw new UnsupportedOperationException(\"No SQL implementation for DowngradeProtocol\")\n    }\n\n    override def executeImpl(ctx: TestContext): Unit = {\n      val deltaLog = ctx.deltaLog\n      val table = DeltaTableV2(spark, deltaLog.dataPath)\n      val txn = deltaLog.startTransaction(catalogTableOpt = None)\n\n      if (!feature.validateDropInvariants(table, txn.snapshot)) {\n        throw DeltaErrors.dropTableFeatureConflictRevalidationFailed()\n      }\n\n      txn.updateProtocol(txn.protocol.removeFeature(feature))\n      val metadataWithNewConfiguration = DropTableFeatureUtils\n        .getDowngradedProtocolMetadata(feature, txn.metadata)\n      txn.updateMetadata(metadataWithNewConfiguration)\n      val commitActions = feature.actionsToIncludeAtDowngradeCommit(txn.snapshot)\n      txn.commit(commitActions, DeltaOperations.DropTableFeature(feature.name, false))\n    }\n  }\n\n  /** Test implementation of backfill batch. */\n  private def backfillBatchImplementation(\n      deltaLog: DeltaLog,\n      executor: BackfillExecutor): Unit = {\n    BackfillExecutionObserver.getObserver.executeBatch {\n      val txn = deltaLog.startTransaction(catalogTableOpt = None)\n      val filesInBatch = executor\n        .filesToBackfill(txn.snapshot)\n        .collect()\n      if (filesInBatch.isEmpty) {\n        return\n      }\n\n      val batch = executor.constructBatch(filesInBatch)\n      txn.trackFilesRead(filesInBatch)\n      batch.execute(\n        spark,\n        backfillTxnId = executor.backfillTxnId,\n        batchId = 0,\n        txn = txn,\n        numSuccessfulBatch = new AtomicInteger(0),\n        numFailedBatch = new AtomicInteger(0))\n    }\n  }\n\n  /**\n   * Test transaction that unbackfill baseRowIDs. It assumes all files can be\n   * unbackfilled in a single commit.\n   */\n  case class UnbackfillBatch() extends TestTransaction(Map.empty) {\n    override val name: String = \"UNBACKFILL\"\n    override def dataChange: Boolean = false\n\n    override def toSQL(tableName: String): String = {\n      throw new UnsupportedOperationException(\"No SQL implementation for Unbackfill\")\n    }\n\n    override def executeImpl(ctx: TestContext): Unit = {\n      val deltaLog = ctx.deltaLog\n      val propertyKey = DeltaSQLConf.DELTA_ROW_TRACKING_IGNORE_SUSPENSION.key\n      withSQLConf(propertyKey -> \"false\") {\n        val backfillStats = BackfillCommandStats(\n          transactionId = \"test-backfill-batch\",\n          \"TEST\"\n        )\n        backfillBatchImplementation(\n          deltaLog,\n          new RowTrackingUnBackfillExecutor(\n            spark,\n            deltaLog,\n            catalogTableOpt = None,\n            backfillTxnId = backfillStats.transactionId,\n            backfillStats = backfillStats\n          ))\n      }\n    }\n  }\n\n  /**\n   * Test transaction that backfills baseRowIDs. It ignores\n   * `rowTrackingSuspended` property. However,it assumes the third party\n   * client uses ROW_TRACKING_BACKFILL_OPERATION_NAME. Finally, it assumes all files can be\n   * backfilled in a single commit.\n   */\n  case class ThirdPartyBackfillBatch() extends TestTransaction(Map.empty) {\n    override val name: String = \"BACKFILL\"\n    override def dataChange: Boolean = false\n\n    override def toSQL(tableName: String): String = {\n      throw new UnsupportedOperationException(\"No SQL implementation for Backfill\")\n    }\n\n    override def executeImpl(ctx: TestContext): Unit = {\n      val deltaLog = ctx.deltaLog\n      withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_IGNORE_SUSPENSION.key -> \"true\") {\n        val backfillStats = BackfillCommandStats(\n          transactionId = \"test-backfill-batch\",\n          \"TEST\"\n        )\n        backfillBatchImplementation(\n          deltaLog,\n          new RowTrackingBackfillExecutor(\n            spark,\n            deltaLog,\n            catalogTableOpt = None,\n            backfillTxnId = backfillStats.transactionId,\n            backfillStats = backfillStats\n          ))\n      }\n    }\n  }\n\n  private def createTestTable(\n      dir: File,\n      numPartitions: Int = 2,\n      rowTrackingEnabled: Boolean = true): DeltaLog = {\n    sql(\n      s\"\"\"CREATE TABLE delta.`${dir.getCanonicalPath}` (id bigint)\n         |USING delta\n         |TBLPROPERTIES(\n         |'${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = '${rowTrackingEnabled.toString}'\n         |)\"\"\".stripMargin)\n\n    spark.range(start = 0, end = 100, step = 1, numPartitions)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(dir.getAbsolutePath)\n\n    val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n    if (rowTrackingEnabled) {\n      validateRowTrackingState(deltaLog, isPresent = true)\n    }\n    val expectedFileCountWithRowIDs = if (rowTrackingEnabled) numPartitions else 0\n    validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs)\n    deltaLog\n  }\n\n  private def validateRowTrackingMetadataInAddFiles(\n      deltaLog: DeltaLog,\n      expectedFileCountWithRowIDs: Int): Unit = {\n    val snapshot = deltaLog.update()\n    val filesWithRowIDsCount = snapshot\n      .allFiles\n      .filter(\"baseRowId IS NOT NULL or defaultRowCommitVersion IS NOT NULL\")\n      .count()\n    assert(filesWithRowIDsCount === expectedFileCountWithRowIDs)\n  }\n\n  private def disableRowTracking(tablePath: String): Unit = {\n    // Fist stage of row tracking removal: disable the feature.\n    val propertiesToSet = Map(\n      DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"false\",\n      DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"true\")\n    val table = DeltaTableV2(spark, new Path(tablePath))\n    AlterTableSetPropertiesDeltaCommand(table, propertiesToSet).run(spark)\n  }\n\n  private def waitForTransactionStart(\n      observer: TransactionObserver): Unit = {\n    def transactionStart: Boolean = {\n      observer.phases.initialPhase.entryBarrier.load() ==\n        AtomicBarrier.State.Requested\n    }\n    busyWaitFor(transactionStart, timeout)\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Drop Feature\n   * Command        Metadata upgrade -------- Unbackfill batches ---------------- Downgrade Commit\n   *                prepare + commit          prepare + commit                    prepare + commit\n   *\n   *\n   * Business Txn                                                prepare + commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for (op <- Seq(\"insert\", \"update\", \"delete\"))\n  test(s\"$op interleaves between the last unbackfill and the protocol downgrade\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir)\n      val ctx = new TestContext(deltaLog)\n      val table = s\"delta.`${dir.getAbsolutePath}`\"\n      val dropRowTrackingFn = () => dropRowTrackingTransaction(table)\n\n      val Seq(dropFuture) = runFunctionsWithOrderingFromObserver(Seq(dropRowTrackingFn)) {\n        case (updateMetadataDropObserver :: Nil) =>\n          val unbackfillDropObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"unbackfill-drop-txn\"))\n          val downgradeDropObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"downgrade-drop-txn\"))\n\n          updateMetadataDropObserver.setNextObserver(\n            unbackfillDropObserver, autoAdvance = true)\n          unbackfillDropObserver.setNextObserver(\n            downgradeDropObserver, autoAdvance = true)\n\n          prepareAndCommitWithNextObserverSet(updateMetadataDropObserver)\n          prepareAndCommitWithNextObserverSet(unbackfillDropObserver)\n\n          waitForTransactionStart(downgradeDropObserver)\n\n          val businessTxn = op match {\n            case \"insert\" => ThirdPartyInsert(start = 100, end = 200)\n            case \"update\" => ThirdPartyUpdate(set = \"id = 200\", condition = \"id = 90\")\n            case \"delete\" => ThirdPartyDelete(\"id = 90\")\n          }\n\n          businessTxn.execute(ctx)\n\n          val expectedFileCountWithRowIDs = op match {\n            case \"insert\" => 2\n            case \"update\" => if (areDVsEnabled) 2 else 1\n            case \"delete\" => 1\n          }\n          validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs)\n\n          prepareAndCommit(downgradeDropObserver)\n      }\n      ThreadUtils.awaitResult(dropFuture, timeout)\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n      validateRowTrackingState(deltaLog, isPresent = false)\n\n      val targetTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n      val expectedValues = op match {\n        case \"insert\" => (0 to 199)\n        case \"update\" => (0 to 99).filterNot(_ == 90) :+ 200\n        case \"delete\" => (0 to 99).filterNot(_ == 90)\n      }\n      checkAnswer(targetTable.toDF, expectedValues.map(Row(_)))\n    }\n  }\n\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Drop Feature\n   * Command        Metadata upgrade -------- Unbackfill -------------- Unbackfill --- Downgrade\n   *                prepare + commit          Batch 1                   Batch 2        Commit\n   *                                          prep+commit               prep+commit    prep+commit\n   *\n   *\n   * Business Txn                                          prep+commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  test(\"Business txn interleaves between two unbackfill batches\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir, numPartitions = 3)\n      val ctx = new TestContext(deltaLog)\n      val table = s\"delta.`${dir.getAbsolutePath}`\"\n      val dropRowTrackingFn = () => dropRowTrackingTransaction(table)\n\n      withSQLConf(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key -> \"2\") {\n        val Seq(dropFuture) = runFunctionsWithOrderingFromObserver(Seq(dropRowTrackingFn)) {\n          case (updateMetadataDropObserver :: Nil) =>\n            val unbackfillBatch1DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-1-drop-txn\"))\n            val unbackfillBatch2DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-2-drop-txn\"))\n            val downgradeDropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"downgrade-drop-txn\"))\n\n            updateMetadataDropObserver.setNextObserver(\n              unbackfillBatch1DropObserver, autoAdvance = true)\n            unbackfillBatch1DropObserver.setNextObserver(\n              unbackfillBatch2DropObserver, autoAdvance = true)\n            unbackfillBatch2DropObserver.setNextObserver(\n              downgradeDropObserver, autoAdvance = true)\n\n            prepareAndCommitWithNextObserverSet(updateMetadataDropObserver)\n\n            // Block unbackfill batch 1 right after commit.\n            unblockUntilPreCommit(unbackfillBatch1DropObserver)\n            waitForPrecommit(unbackfillBatch1DropObserver)\n            unblockCommit(unbackfillBatch1DropObserver)\n            busyWaitFor(unbackfillBatch1DropObserver.phases.commitPhase.hasLeft, timeout)\n            busyWaitFor(unbackfillBatch1DropObserver.phases.backfillPhase.hasLeft, timeout)\n\n            // We unbackfilled 2 of the 3 files.\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 1)\n\n            ThirdPartyInsert(start = 100, end = 110, numPartitions = 1).execute(ctx)\n\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n            // Allow to proceed to batch 2.\n            unbackfillBatch1DropObserver.phases.postCommitPhase.leave()\n\n            // Batch 2 picked the single backfilled file by the interleaved txn.\n            prepareAndCommitWithNextObserverSet(unbackfillBatch2DropObserver)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n\n            prepareAndCommit(downgradeDropObserver)\n        }\n        ThreadUtils.awaitResult(dropFuture, timeout)\n        validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n        validateRowTrackingState(deltaLog, isPresent = false)\n\n        val targetTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n        val expectedValues = (0 to 109)\n        checkAnswer(targetTable.toDF, expectedValues.map(Row(_)))\n      }\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Drop Feature\n   * Command        Metadata upgrade -------- Unbackfill ---- Unbackfill --------------- Downgrade\n   *                prepare + commit          Batch 1         Batch 2                    Commit\n   *                                          prep+commit     prepare             commit prep+commit\n   *\n   *\n   * Enable Row Tracking\n   * Scenario 1:                                                        Metadata\n   *                                                                    upgrade\n   * Scenario 2:                                                        Backfill\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for (isScenario1 <- BOOLEAN_DOMAIN)\n  test(s\"Enable row tracking during unbackfill - isScenario1: $isScenario1\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir, numPartitions = 3)\n      val table = s\"delta.`${dir.getAbsolutePath}`\"\n      val dropRowTrackingFn = () => dropRowTrackingTransaction(table)\n\n      withSQLConf(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key -> \"2\") {\n        val Seq(dropFuture) = runFunctionsWithOrderingFromObserver(Seq(dropRowTrackingFn)) {\n          case (updateMetadataDropObserver :: Nil) =>\n            val unbackfillBatch1DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-1-drop-txn\"))\n            val unbackfillBatch2DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-2-drop-txn\"))\n            val downgradeDropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"downgrade-drop-txn\"))\n\n            updateMetadataDropObserver.setNextObserver(\n              unbackfillBatch1DropObserver, autoAdvance = true)\n            unbackfillBatch1DropObserver.setNextObserver(\n              unbackfillBatch2DropObserver, autoAdvance = true)\n            unbackfillBatch2DropObserver.setNextObserver(\n              downgradeDropObserver, autoAdvance = true)\n\n            prepareAndCommitWithNextObserverSet(updateMetadataDropObserver)\n            prepareAndCommitWithNextObserverSet(unbackfillBatch1DropObserver)\n\n            unblockUntilPreCommit(unbackfillBatch2DropObserver)\n            waitForPrecommit(unbackfillBatch2DropObserver)\n\n            if (isScenario1) {\n              // Trying to re-enable row tracking during removal causes the alter table command\n              // to fail.\n              val e = intercept[DeltaIllegalStateException] {\n                enableRowTrackingTransaction(table)\n              }\n              assert(e.getErrorClass === \"DELTA_ROW_TRACKING_ILLEGAL_PROPERTY_COMBINATION\")\n            } else {\n              // Backfill fails if run together backfill.\n              assertThrows[IllegalStateException] {\n                backfillTransaction(deltaLog)\n              }\n            }\n\n            // Commit batch 2.\n            unblockCommit(unbackfillBatch2DropObserver)\n            unbackfillBatch2DropObserver.phases.postCommitPhase.leave()\n            waitForCommit(unbackfillBatch2DropObserver)\n\n            prepareAndCommit(downgradeDropObserver)\n        }\n        ThreadUtils.awaitResult(dropFuture, timeout)\n        validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n        validateRowTrackingState(deltaLog, isPresent = false)\n      }\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Drop Feature\n   * Command        Metadata upgrade -------- Unbackfill ---------- Downgrade --------------------\n   *                                          Batch 1               Protocol\n   *                prepare + commit          prep+commit           prep+commit\n   *\n   *\n   * Business Txn                                         prepare                  commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for (op <- Seq(\"insert\", \"update\", \"delete\"))\n  test(s\"Interleaved $op right after protocol downgrade should abort due to protocol change\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir)\n      val table = s\"delta.`${dir.getAbsolutePath}`\"\n      val ctx = new TestContext(deltaLog)\n      val dropRowTrackingFn = () => dropRowTrackingTransaction(table)\n\n      val Seq(dropFuture) = runFunctionsWithOrderingFromObserver(Seq(dropRowTrackingFn)) {\n        case (updateMetadataDropObserver :: Nil) =>\n          val unbackfillDropObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"unbackfill-drop-txn\"))\n          val downgradeDropObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"downgrade-drop-txn\"))\n\n          updateMetadataDropObserver.setNextObserver(\n            unbackfillDropObserver, autoAdvance = true)\n          unbackfillDropObserver.setNextObserver(\n            downgradeDropObserver, autoAdvance = true)\n\n          prepareAndCommitWithNextObserverSet(updateMetadataDropObserver)\n          prepareAndCommitWithNextObserverSet(unbackfillDropObserver)\n\n          val businessTxn = op match {\n            case \"insert\" => ThirdPartyInsert(start = 100, end = 200)\n            case \"update\" => ThirdPartyUpdate(set = \"id = 200\", condition = \"id = 10\")\n            case \"delete\" => ThirdPartyDelete(\"id = 10\")\n          }\n          val businessTxnFn = () => {\n            businessTxn.execute(ctx)\n            Array.empty[Row]\n          }\n\n          val Seq(businessTxnFuture) = runFunctionsWithOrderingFromObserver(Seq(businessTxnFn)) {\n            case (businessTxnObserver :: Nil) =>\n              unblockUntilPreCommit(businessTxnObserver)\n              waitForPrecommit(businessTxnObserver)\n\n              prepareAndCommit(downgradeDropObserver)\n\n              unblockCommit(businessTxnObserver)\n          }\n          val e = intercept[org.apache.spark.SparkException] {\n            ThreadUtils.awaitResult(businessTxnFuture, timeout)\n          }\n          assert(e.getCause.isInstanceOf[ProtocolChangedException])\n          validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n      }\n      ThreadUtils.awaitResult(dropFuture, timeout)\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n      validateRowTrackingState(deltaLog, isPresent = false)\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Unbackfill Batch:      prepare                         commit\n   * Business Txn:                      prepare + commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for (op <- Seq(\"insert\", \"update\", \"delete\"))\n  test(s\"Third party $op that interleaves with unbackfill is resolved\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir)\n      val ctx = new TestContext(deltaLog)\n\n      // Fist stage of row tracking removal: disable the feature.\n      disableRowTracking(dir.getAbsolutePath)\n\n      val txnA = UnbackfillBatch()\n      val txnB = op match {\n        case \"insert\" => ThirdPartyInsert(start = 100, end = 200)\n        case \"update\" => ThirdPartyUpdate(\"id = 200\", \"id = 90\")\n        case \"delete\" => ThirdPartyDelete(\"id = 90\")\n      }\n\n      txnA.start(ctx)\n      txnA.observer.foreach(o => busyWaitFor(o.phases.commitPhase.hasReached, timeout))\n\n      txnB.execute(ctx)\n\n      val expectedFileCountWithRowIDs = op match {\n        case \"insert\" => 4\n        case \"update\" => if (areDVsEnabled) 3 else 2\n        case \"delete\" => 2\n      }\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs)\n      txnA.commit(ctx)\n\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n\n      // Unbackfill conflict should be resolved and the downgrade should proceed.\n      DowngradeProtocol(RowTrackingFeature).execute(ctx)\n      validateRowTrackingState(deltaLog, isPresent = false)\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Unbackfill Batch                       prepare + commit\n   * Business Txn               prepare                           commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for (op <- Seq(\"insert\", \"update\", \"delete\"))\n  test(s\"Single Unbackfill batch interleaves $op\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir, numPartitions = 3)\n      val ctx = new TestContext(deltaLog)\n      disableRowTracking(dir.getAbsolutePath)\n\n      val businessTxn = op match {\n        case \"insert\" => ThirdPartyInsert(start = 100, end = 200)\n        case \"update\" => ThirdPartyUpdate(set = \"id = 200\", condition = \"id = 10\")\n        case \"delete\" => ThirdPartyDelete(\"id = 10\")\n      }\n\n      businessTxn.start(ctx)\n      UnbackfillBatch().execute(ctx)\n      businessTxn.commit(ctx)\n\n      val expectedFileCountWithRowIDs = op match {\n        case \"insert\" => 2\n        case \"update\" => if (areDVsEnabled) 2 else 1\n        case \"delete\" => 1\n      }\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs)\n\n      val targetTable = io.delta.tables.DeltaTable.forPath(dir.getAbsolutePath)\n      val expectedValues = op match {\n        case \"insert\" => (0 to 199)\n        case \"update\" => (0 to 99).filterNot(_ == 10) :+ 200\n        case \"delete\" => (0 to 99).filterNot(_ == 10)\n      }\n      checkAnswer(targetTable.toDF, expectedValues.map(Row(_)))\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Drop Feature\n   * Command        Metadata upgrade -------- Unbackfill ----- Downgrade -------------------------\n   *                                          Batch 1          Protocol\n   *                prepare + commit          prep+commit      Prepare                 Commit\n   *\n   *\n   * Business Txn                                                         prep+commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  for (op <- Seq(\"insert\", \"update\", \"delete\"))\n  test(s\"Drop feature conflict resolves unbackfills addFiles of interleaved commits ($op)\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir)\n      val table = s\"delta.`${dir.getAbsolutePath}`\"\n      val ctx = new TestContext(deltaLog)\n      val dropRowTrackingFn = () => dropRowTrackingTransaction(table)\n\n      val Seq(dropFuture) = runFunctionsWithOrderingFromObserver(Seq(dropRowTrackingFn)) {\n        case (updateMetadataDropObserver :: Nil) =>\n          val unbackfillDropObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"unbackfill-drop-txn\"))\n          val downgradeDropObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"downgrade-drop-txn\"))\n\n          updateMetadataDropObserver.setNextObserver(\n            unbackfillDropObserver, autoAdvance = true)\n          unbackfillDropObserver.setNextObserver(\n            downgradeDropObserver, autoAdvance = true)\n\n          prepareAndCommitWithNextObserverSet(updateMetadataDropObserver)\n          prepareAndCommitWithNextObserverSet(unbackfillDropObserver)\n          validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n\n          unblockUntilPreCommit(downgradeDropObserver)\n          waitForPrecommit(downgradeDropObserver)\n\n          val businessTxn = op match {\n            case \"insert\" => ThirdPartyInsert(start = 100, end = 200)\n            case \"update\" => ThirdPartyUpdate(set = \"id = 200\", condition = \"id = 10\")\n            case \"delete\" => ThirdPartyDelete(\"id = 10\")\n          }\n          businessTxn.execute(ctx)\n\n          val expectedFileCountWithRowIDs = op match {\n            case \"insert\" => 2\n            case \"update\" => if (areDVsEnabled) 2 else 1\n            case \"delete\" => 1\n          }\n          validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs)\n\n          unblockCommit(downgradeDropObserver)\n          waitForCommit(downgradeDropObserver)\n      }\n      ThreadUtils.awaitResult(dropFuture, timeout)\n      validateRowTrackingState(deltaLog, isPresent = false)\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Unbackfill Batch:      prepare                         commit\n   * Backfill Batch:                    prepare + commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  test(\"Backfill interleaves unbackfill\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir)\n      val ctx = new TestContext(deltaLog)\n\n      // Fist stage of row tracking removal: disable the feature.\n      disableRowTracking(dir.getAbsolutePath)\n\n      // Add some more data without row IDs.\n      addData(dir.getAbsolutePath, start = 100, end = 200)\n\n      val txnA = UnbackfillBatch()\n      val txnB = ThirdPartyBackfillBatch()\n\n      txnA.start(ctx)\n      txnB.execute(ctx)\n      val e = intercept[org.apache.spark.SparkException] {\n        txnA.commit(ctx)\n      }\n      checkError(\n        e.getCause.asInstanceOf[SparkThrowable],\n        \"DELTA_ROW_TRACKING_BACKFILL_RUNNING_CONCURRENTLY_WITH_UNBACKFILL\",\n        parameters = Map.empty)\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Backfill Batch:      prepare                         commit\n   * Unackfill Batch:                prepare + commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  test(\"Unbackfill interleaves backfill\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir)\n      val ctx = new TestContext(deltaLog)\n\n      // Fist stage of row tracking removal: disable the feature.\n      disableRowTracking(dir.getAbsolutePath)\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n      // Add some more data without row IDs.\n      addData(dir.getAbsolutePath, start = 100, end = 200)\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n      val txnA = ThirdPartyBackfillBatch()\n      val txnB = UnbackfillBatch()\n\n      txnA.start(ctx)\n      txnB.execute(ctx)\n      txnA.commit(ctx)\n      // Backfill backfilled the initial 2 files. The unbackfill unbackfilled the following 2 files.\n      // The backfill at conflict resolution did not find any common files with the conflicting\n      // unbackfill and proceeded.\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n      // Downgrade commit detects the issue and aborts.\n      val e = intercept[DeltaTableFeatureException] {\n        DowngradeProtocol(RowTrackingFeature).execute(ctx)\n      }\n      assert(e.getErrorClass === \"DELTA_FEATURE_DROP_CONFLICT_REVALIDATION_FAIL\")\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Unbackfill Command:\n   *                 Unbackfill ------------------------------ Unbackfill ------------------------\n   *                 Batch 1                                   Batch 2\n   *                 prepare + commit                          prepare + commit\n   *\n   *\n   * Business Txn A                      prepare + commit\n   * Business Txn B                                                               prepare + commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  test(s\"Unbackfill terminates when small competing txns run concurrently \") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir, numPartitions = 4)\n      val ctx = new TestContext(deltaLog)\n      disableRowTracking(dir.getAbsolutePath)\n      val unbackillFn = () => {\n        unBackfillTransaction(deltaLog)\n        Array.empty[Row]\n      }\n\n      withSQLConf(DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key -> \"3\") {\n        val Seq(unbackfillFuture) = runFunctionsWithOrderingFromObserver(Seq(unbackillFn)) {\n          case (unbackfillBatch1DropObserver :: Nil) =>\n            val unbackfillBatch2DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-2-drop-txn\"))\n\n            unbackfillBatch1DropObserver.setNextObserver(\n              unbackfillBatch2DropObserver, autoAdvance = true)\n\n            unblockUntilPreCommit(unbackfillBatch1DropObserver)\n            waitForPrecommit(unbackfillBatch1DropObserver)\n            unblockCommit(unbackfillBatch1DropObserver)\n            busyWaitFor(unbackfillBatch1DropObserver.phases.commitPhase.hasLeft, timeout)\n            busyWaitFor(unbackfillBatch1DropObserver.phases.backfillPhase.hasLeft, timeout)\n            // We started with 4 files and unbackfilled 3.\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 1)\n\n            // Adds 1 backfilled file.\n            ThirdPartyInsert(start = 100, end = 110, numPartitions = 1).execute(ctx)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n            unbackfillBatch1DropObserver.phases.postCommitPhase.leave()\n            // Finds both files and unbackfills them. It terminates since the number of found\n            // files is less than the max batch size.\n            prepareAndCommit(unbackfillBatch2DropObserver)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 0)\n\n            // Adds 1 backfilled file.\n            ThirdPartyInsert(start = 110, end = 120, numPartitions = 1).execute(ctx)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 1)\n        }\n        ThreadUtils.awaitResult(unbackfillFuture, timeout)\n        validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 1)\n      }\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Unbackfill Command:\n   *                 Unbackfill ------------- Unbackfill ------------ Unbackfill -----------------\n   *                 Batch 1                  Batch 2                 Batch 3\n   *                 prep+commit              prep+commit             prep+commit\n   *\n   *\n   * Business Txn A              prep+commit\n   * Business Txn B                                       prep+commit\n   * Business Txn C                                                                prep+commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  test(s\"Unbackfill terminates when large competing txns run concurrently\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir, numPartitions = 4)\n      val ctx = new TestContext(deltaLog)\n      disableRowTracking(dir.getAbsolutePath)\n      val unbackillFn = () => {\n        unBackfillTransaction(deltaLog)\n        Array.empty[Row]\n      }\n\n      withSQLConf(\n          DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_PER_COMMIT.key -> \"3\",\n          DeltaSQLConf.DELTA_BACKFILL_MAX_NUM_FILES_FACTOR.key -> \"2\") {\n        val Seq(unbackfillFuture) = runFunctionsWithOrderingFromObserver(Seq(unbackillFn)) {\n          case (unbackfillBatch1DropObserver :: Nil) =>\n            val unbackfillBatch2DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-2-drop-txn\"))\n            val unbackfillBatch3DropObserver = new TransactionObserver(\n              OptimisticTransactionPhases.forName(\"unbackfill-batch-3-drop-txn\"))\n\n            unbackfillBatch1DropObserver.setNextObserver(\n              unbackfillBatch2DropObserver, autoAdvance = true)\n            unbackfillBatch2DropObserver.setNextObserver(\n              unbackfillBatch3DropObserver, autoAdvance = true)\n\n            unblockUntilPreCommit(unbackfillBatch1DropObserver)\n            waitForPrecommit(unbackfillBatch1DropObserver)\n            unblockCommit(unbackfillBatch1DropObserver)\n            busyWaitFor(unbackfillBatch1DropObserver.phases.commitPhase.hasLeft, timeout)\n            busyWaitFor(unbackfillBatch1DropObserver.phases.backfillPhase.hasLeft, timeout)\n            // We started with 4 files and unbackfilled 3.\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 1)\n\n            // Adds 4 backfilled files (5 in total left).\n            ThirdPartyInsert(start = 100, end = 150, numPartitions = 4).execute(ctx)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 5)\n            unbackfillBatch1DropObserver.phases.postCommitPhase.leave()\n\n            // Start batch 2.\n            unblockUntilPreCommit(unbackfillBatch2DropObserver)\n            waitForPrecommit(unbackfillBatch2DropObserver)\n            unblockCommit(unbackfillBatch2DropObserver)\n            busyWaitFor(unbackfillBatch2DropObserver.phases.commitPhase.hasLeft, timeout)\n            busyWaitFor(unbackfillBatch2DropObserver.phases.backfillPhase.hasLeft, timeout)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n            // Adds 4 backfilled files (6 in total left).\n            ThirdPartyInsert(start = 150, end = 200, numPartitions = 4).execute(ctx)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 6)\n            unbackfillBatch2DropObserver.phases.postCommitPhase.leave()\n\n            // Start batch 3. Although there more than 3 files left to unbackfill the\n            // job terminates. This is because we reached the max number of files to unbackfill\n            // (Number of initial files in the table * 2).\n            prepareAndCommit(unbackfillBatch3DropObserver)\n            validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 3)\n\n            // Adds 2 backfilled files (5 in total left).\n            ThirdPartyInsert(start = 200, end = 220, numPartitions = 2).execute(ctx)\n        }\n        ThreadUtils.awaitResult(unbackfillFuture, timeout)\n        validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 5)\n      }\n    }\n  }\n\n  /*\n   * -------------------------------------------> TIME ------------------------------------------->\n   *\n   * Row Tracking Enablement:\n   *                 Protocol  ------------- Backfill ---------------------------- Alter Table ---\n   *                 Upgrade\n   *                 prepare + commit        prepare + commit                      prepare + commit\n   *\n   *\n   * Business Txn A                                             prepare + commit\n   *\n   * -------------------------------------------> TIME ------------------------------------------->\n   */\n  test(s\"Row tracking enablement fails when not all files are backfilled\") {\n    withTempDir { dir =>\n      val deltaLog = createTestTable(dir, rowTrackingEnabled = false)\n      val enablementFn = () => {\n        enableRowTrackingTransaction(s\"delta.`${dir.getAbsolutePath}`\")\n        Array.empty[Row]\n      }\n\n      val Seq(enablementFuture) = runFunctionsWithOrderingFromObserver(Seq(enablementFn)) {\n        case (protocolUpgradeObserver :: Nil) =>\n          val backfillObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"backfill-txn\"))\n          val alterTableObserver = new TransactionObserver(\n            OptimisticTransactionPhases.forName(\"alter-table-txn\"))\n\n          protocolUpgradeObserver.setNextObserver(backfillObserver, autoAdvance = true)\n          backfillObserver.setNextObserver(alterTableObserver, autoAdvance = true)\n\n          prepareAndCommitWithNextObserverSet(protocolUpgradeObserver)\n\n          unblockUntilPreCommit(backfillObserver)\n          waitForPrecommit(backfillObserver)\n          unblockCommit(backfillObserver)\n          busyWaitFor(backfillObserver.phases.commitPhase.hasLeft, timeout)\n          busyWaitFor(backfillObserver.phases.backfillPhase.hasLeft, timeout)\n          validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n\n          // Add 2 non-backfilled files.\n          val table = DeltaTableV2(spark, new Path(dir.getAbsolutePath))\n          val propertiesToSet = Map(DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"true\")\n          AlterTableSetPropertiesDeltaCommand(table, propertiesToSet).run(spark)\n          addData(dir.getAbsolutePath, start = 100, end = 200)\n          val propertiesToUnSet = Map(DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"false\")\n          AlterTableSetPropertiesDeltaCommand(table, propertiesToUnSet).run(spark)\n\n          // No row IDs were generated.\n          validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n          backfillObserver.phases.postCommitPhase.leave()\n\n          // Alter table should throw an exception since not all files were backfilled.\n          unblockUntilPreCommit(alterTableObserver)\n      }\n      val e = intercept[org.apache.spark.SparkException] {\n        ThreadUtils.awaitResult(enablementFuture, timeout)\n      }\n      assert(e.getCause.isInstanceOf[ProtocolChangedException])\n      validateRowTrackingMetadataInAddFiles(deltaLog, expectedFileCountWithRowIDs = 2)\n    }\n  }\n}\n\nclass RowTrackingRemovalConcurrencyWithoutDVsSuite extends RowTrackingRemovalConcurrencySuite {\n  override protected val areDVsEnabled = false\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingRemovalSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaIllegalStateException, DeltaLog, DeltaOperations, MaterializedRowCommitVersion, MaterializedRowId, RowId, RowTrackingFeature}\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.RowId.RowTrackingMetadataDomain\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.AlterTableSetPropertiesDeltaCommand\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport io.delta.tables.DeltaTable\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.functions.{expr, lit}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.ManualClock\n\ntrait RowTrackingRemovalSuiteBase\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest {\n\n  protected override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey, \"true\")\n\n  def validateRowTrackingState(deltaLog: DeltaLog, isPresent: Boolean): Unit = {\n    val snapshot = deltaLog.update()\n    val configuration = snapshot.metadata.configuration\n    val allFiles = snapshot.allFiles.collect()\n\n    assert(RowId.isSupported(snapshot.protocol) === isPresent)\n    assert(!configuration.contains(DeltaConfigs.ROW_TRACKING_SUSPENDED.key))\n\n    if (isPresent) {\n      assert(DeltaConfigs.ROW_TRACKING_ENABLED.fromMetaData(snapshot.metadata))\n      assert(configuration.contains(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP))\n      assert(configuration.contains(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP))\n      assert(RowTrackingMetadataDomain\n        .fromSnapshot(snapshot)\n        .forall(_.rowIdHighWaterMark > RowId.MISSING_HIGH_WATER_MARK))\n      assert(allFiles.forall(a => a.baseRowId.isDefined && a.defaultRowCommitVersion.isDefined))\n    } else {\n      assert(!configuration.contains(DeltaConfigs.ROW_TRACKING_ENABLED.key))\n      assert(!configuration.contains(DeltaConfigs.ROW_TRACKING_SUSPENDED.key))\n      assert(!configuration.contains(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP))\n      assert(!configuration.contains(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP))\n      assert(RowTrackingMetadataDomain.fromSnapshot(snapshot).isEmpty)\n      assert(allFiles.forall(a => a.baseRowId.isEmpty && a.defaultRowCommitVersion.isEmpty))\n    }\n  }\n\n  def dropRowTracking(deltaLog: DeltaLog, truncateHistory: Boolean = false): Unit = {\n    val sqlText =\n      s\"\"\"\n         |ALTER TABLE delta.`${deltaLog.dataPath}`\n         |DROP FEATURE ${RowTrackingFeature.name}\n         |${if (truncateHistory) \"TRUNCATE HISTORY\" else \"\"}\n         |\"\"\".stripMargin\n\n    sql(sqlText)\n  }\n\n  def addData(path: String, start: Long, end: Long): Unit = {\n    spark.range(start, end, step = 1, numPartitions = 2)\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .save(path)\n  }\n}\n\nclass RowTrackingRemovalSuite extends RowTrackingRemovalSuiteBase {\n\n  test(\"Basic row tracking removal\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      addData(dir.getAbsolutePath, 0, 10)\n      assert(RowId.isSupported(deltaLog.update().protocol))\n\n      dropRowTracking(deltaLog)\n      validateRowTrackingState(deltaLog, isPresent = false)\n    }\n  }\n\n  test(\"Remove row tracking and then re-enable it\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      addData(dir.getAbsolutePath, 0, 10)\n      addData(dir.getAbsolutePath, 10, 20)\n\n      val table = DeltaTable.forPath(spark, dir.getAbsolutePath)\n      table.update(expr(\"id == 2\"), Map(\"id\" -> lit(200)))\n\n      // Store the old materialized column names.\n      val configuration = deltaLog.update().metadata.configuration\n      val oldMaterializedRowIdName = configuration(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP)\n      val oldMaterializedRowCommitVersionName =\n        configuration(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP)\n\n      dropRowTracking(deltaLog)\n      addData(dir.getAbsolutePath, 20, 30)\n      validateRowTrackingState(deltaLog, isPresent = false)\n\n      // Re-enable row tracking.\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n           |SET TBLPROPERTIES(\n           |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\n           |)\"\"\".stripMargin)\n\n      addData(dir.getAbsolutePath, 30, 40)\n\n      validateRowTrackingState(deltaLog, isPresent = true)\n\n      // Make sure the materialized column names are different.\n      val newConfiguration = deltaLog.update().metadata.configuration\n      val newMaterializedRowIdName =\n        newConfiguration(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP)\n      val newMaterializedRowCommitVersionName =\n        newConfiguration(MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP)\n      assert(newMaterializedRowIdName != oldMaterializedRowIdName)\n      assert(newMaterializedRowCommitVersionName != oldMaterializedRowCommitVersionName)\n    }\n  }\n\n  // This test verifies we can recover a drop feature failure. this is for the scenario\n  // the user decides to re-enable row tracking instead of retrying drop feature.\n  test(\"Row tracking can recover from suspension\") {\n    import org.apache.spark.sql.delta.implicits._\n\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      def getMaterializedRowId(df: DataFrame, id: Long): Long = {\n         df\n          .filter(s\"id == $id\")\n          .select(RowId.QUALIFIED_COLUMN_NAME)\n          .as[Long]\n          .collect()\n          .head\n      }\n\n      def getHighWatermark(): Long = {\n        RowId.extractHighWatermark(deltaLog.update()).getOrElse {\n          throw new IllegalStateException(\"High watermark is missing\")\n        }\n      }\n\n      addData(dir.getAbsolutePath, 0, 10)\n\n      val table = DeltaTable.forPath(spark, dir.getAbsolutePath)\n\n      // These operations should materialize row IDs.\n      table.update(expr(\"id == 2\"), Map(\"id\" -> lit(200)))\n      table.delete(\"id == 4\")\n\n      val watermarkPreDisablement = getHighWatermark()\n      val materializedRowIdPreDisablement = getMaterializedRowId(table.toDF, 200)\n\n      AlterTableSetPropertiesDeltaCommand(\n        table = DeltaTableV2(spark, deltaLog.dataPath),\n        configuration = Map(\n          DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"false\",\n          DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"true\"))\n        .run(spark)\n\n      // Should not generate row IDs.\n      addData(dir.getAbsolutePath, 10, 15)\n      table.update(expr(\"id == 200\"), Map(\"id\" -> lit(300)))\n      assert(getHighWatermark() === watermarkPreDisablement)\n\n      // Lift row identity generation suspension.\n      AlterTableSetPropertiesDeltaCommand(\n        table = DeltaTableV2(spark, deltaLog.dataPath),\n        configuration = Map(DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"false\"))\n        .run(spark)\n\n      // Backfill.\n      AlterTableSetPropertiesDeltaCommand(\n        table = DeltaTableV2(spark, deltaLog.dataPath),\n        configuration = Map(DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"true\"))\n        .run(spark)\n\n      // Row tracking continued from previous high watermark.\n      assert(getHighWatermark() > watermarkPreDisablement)\n\n      // Row tracking does not guarantee the materialized row ID will be the same after\n      // re-enablement.\n      assert(getMaterializedRowId(table.toDF, 300) != materializedRowIdPreDisablement)\n\n      // All add files should have row IDs.\n      assert(deltaLog.update().allFiles.where(\"baseRowId IS NULL\").count() === 0)\n    }\n  }\n\n  for (dvsEnabled <- BOOLEAN_DOMAIN)\n  test(s\"Property `delta.rowTrackingSuspended` is respected - dvsEnabled: $dvsEnabled\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n      addData(dir.getAbsolutePath, 0, 10)\n\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n           |SET TBLPROPERTIES(\n           |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'false',\n           |${DeltaConfigs.ROW_TRACKING_SUSPENDED.key} = 'true',\n           |${DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key} = '$dvsEnabled'\n           |)\"\"\".stripMargin)\n\n      val filesWithRowIds = deltaLog.update().allFiles.collect().toSet\n\n      addData(dir.getAbsolutePath, 10, 15)\n\n      val targetTable = DeltaTable.forPath(dir.getAbsolutePath)\n      targetTable.update(expr(\"id IN (2, 12)\"), Map(\"id\" -> lit(200)))\n      targetTable.delete(\"id == 3\")\n\n      val allFiles = deltaLog.update()\n        .allFiles\n        .collect()\n        .filterNot(filesWithRowIds.contains)\n\n      assert(allFiles.forall(a => a.baseRowId.isEmpty && a.defaultRowCommitVersion.isEmpty))\n    }\n  }\n\n  test(s\"Cannot enable both configurations at the same time\") {\n    withTempDir { dir =>\n      addData(dir.getAbsolutePath, 0, 10)\n\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n           |SET TBLPROPERTIES(\n           |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\n           |)\"\"\".stripMargin)\n\n      assertThrows[IllegalStateException] {\n        sql(\n          s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n             |SET TBLPROPERTIES(\n             |${DeltaConfigs.ROW_TRACKING_SUSPENDED.key} = 'true'\n             |)\"\"\".stripMargin)\n      }\n\n      sql(\n        s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n           |SET TBLPROPERTIES(\n           |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'false',\n           |${DeltaConfigs.ROW_TRACKING_SUSPENDED.key} = 'true'\n           |)\"\"\".stripMargin)\n\n\n      assertThrows[IllegalStateException] {\n        sql(\n          s\"\"\"ALTER TABLE delta.`${dir.getAbsolutePath}`\n             |SET TBLPROPERTIES(\n             |${DeltaConfigs.ROW_TRACKING_ENABLED.key} = 'true'\n             |)\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"Third party writer enables row tracking without disabling suspension property\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir.getAbsolutePath)\n\n      addData(dir.getAbsolutePath, 0, 10)\n\n      // Third party writer messes up the configs.\n      val txn = deltaLog.startTransaction(catalogTableOpt = None)\n      val newConfiguration = txn.metadata.configuration ++ Map(\n        DeltaConfigs.ROW_TRACKING_ENABLED.key -> \"true\",\n        DeltaConfigs.ROW_TRACKING_SUSPENDED.key -> \"true\"\n      )\n      txn.updateMetadata(txn.metadata.copy(configuration = newConfiguration))\n      txn.commit(Seq.empty, DeltaOperations.ManualUpdate)\n\n\n      val e = intercept[DeltaIllegalStateException] {\n        addData(dir.getAbsolutePath, 10, 20)\n      }\n      checkError(\n        e,\n        \"DELTA_ROW_TRACKING_ILLEGAL_PROPERTY_COMBINATION\",\n        parameters = Map(\n          \"property1\" -> DeltaConfigs.ROW_TRACKING_ENABLED.key,\n          \"property2\" -> DeltaConfigs.ROW_TRACKING_SUSPENDED.key))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowid/RowTrackingUpdateSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowid\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaTestUtils.BOOLEAN_DOMAIN\nimport org.apache.spark.sql.delta.rowtracking.RowTrackingEnabled\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.functions.{col, lit}\n\ntrait RowTrackingUpdateSuiteBase extends RowIdTestUtils with RowTrackingEnabled {\n  protected def dvsEnabled: Boolean = false\n\n  protected val numRowsTarget = 3000\n  protected val numRowsPerFile = 250\n  protected val numFiles: Int = numRowsTarget / numRowsPerFile\n\n  protected val targetTableName = \"target\"\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey,\n        dvsEnabled.toString)\n      .set(DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key, dvsEnabled.toString)\n      .set(DeltaSQLConf.UPDATE_USE_PERSISTENT_DELETION_VECTORS.key, dvsEnabled.toString)\n      .set(DeltaSQLConf.MERGE_USE_PERSISTENT_DELETION_VECTORS.key, dvsEnabled.toString)\n  }\n\n  protected def writeTestTable(\n      tableName: String,\n      isPartitioned: Boolean,\n      lastModifiedVersion: Long = 0L): Unit = {\n    // Disable optimized writes to write out the specified number of files.\n    withSQLConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"false\") {\n      val df = spark.range(\n        start = 0, end = numRowsTarget, step = 1, numPartitions = numFiles)\n        .withColumn(\"last_modified_version\", lit(lastModifiedVersion))\n        .withColumn(\"partition\", (col(\"id\") / (numRowsTarget / 3)).cast(\"int\"))\n        .write.format(\"delta\")\n      if (isPartitioned) {\n        df.partitionBy(\"partition\").saveAsTable(tableName)\n      } else {\n        df.saveAsTable(tableName)\n      }\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      assert(snapshot.allFiles.count() === numFiles)\n    }\n  }\n\n  protected def withRowIdTestTable(isPartitioned: Boolean)(f: => Unit): Unit = {\n    withTable(targetTableName) {\n      writeTestTable(targetTableName, isPartitioned)\n      f\n    }\n  }\n\n  protected def checkAndExecuteUpdate(\n      tableName: String, condition: Option[String], newVersion: Long = 1L): Unit = {\n    val expectedRowIds =\n      spark.read.table(tableName).select(\"id\", RowId.QUALIFIED_COLUMN_NAME).collect()\n\n    val log = DeltaLog.forTable(spark, TableIdentifier(targetTableName))\n    checkRowTrackingMarkedAsPreservedForCommit(log) {\n      checkFileActionInvariantBeforeAndAfterOperation(log) {\n        executeUpdate(tableName, condition, newVersion)\n      }\n    }\n\n    val actualRowIds = spark.read.table(tableName).select(\"id\", RowId.QUALIFIED_COLUMN_NAME)\n    checkAnswer(actualRowIds, expectedRowIds)\n    assertRowIdsAreValid(log)\n\n    val actualRowCommitVersions =\n      spark.read.table(tableName).select(\"id\", RowCommitVersion.QUALIFIED_COLUMN_NAME)\n    val expectedRowCommitVersions =\n      spark.read.table(tableName).select(\"id\", \"last_modified_version\").collect()\n    checkAnswer(actualRowCommitVersions, expectedRowCommitVersions)\n  }\n\n  protected def executeUpdate(tableName: String, where: Option[String], newVersion: Long): Unit = {\n    val whereClause = where.map(c => s\"WHERE $c\").getOrElse(\"\")\n    sql(s\"\"\"UPDATE $tableName as t\n         |SET last_modified_version = $newVersion\n         |$whereClause\"\"\".stripMargin)\n  }\n}\n\ntrait RowTrackingUpdateCommonTests extends RowTrackingUpdateSuiteBase {\n\n  for {\n    isPartitioned <- BOOLEAN_DOMAIN\n    whereClause <- Seq(\n      Some(s\"id < ${(numFiles / 2) * numRowsPerFile}\"), // 50% of files match\n      Some(s\"id < ${numRowsPerFile / 2}\"), // One file matches\n      None // No condition, 100% of files match\n    )\n  } {\n    test(s\"Preserves row IDs, whereClause = $whereClause, isPartitioned = $isPartitioned\") {\n      withRowIdTestTable(isPartitioned = isPartitioned) {\n        checkAndExecuteUpdate(tableName = targetTableName, condition = whereClause)\n      }\n    }\n  }\n\n  for (isPartitioned <- BOOLEAN_DOMAIN)\n  test(s\"Preserves row IDs across multiple updates, isPartitioned = $isPartitioned\") {\n    withRowIdTestTable(isPartitioned = false) {\n      checkAndExecuteUpdate(targetTableName, condition = Some(\"id % 20 = 0\"))\n\n      checkAndExecuteUpdate(targetTableName, condition = Some(\"id % 10 = 0\"), newVersion = 2L)\n    }\n  }\n\n  test(\"Preserves row IDs in update on partition column, whole file update\") {\n    withRowIdTestTable(isPartitioned = true) {\n      checkAndExecuteUpdate(tableName = targetTableName, condition = Some(\"partition = 0\"))\n    }\n  }\n\n\n  test(s\"Preserves row IDs on unpartitioned table with optimized writes\") {\n    withRowIdTestTable(isPartitioned = false) {\n      val whereClause = Some(s\"id = 0 OR id = $numRowsTarget - 1\")\n      withSQLConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key -> \"true\") {\n        checkAndExecuteUpdate(targetTableName, condition = whereClause)\n      }\n\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(targetTableName))\n\n      val expectedNumFiles = if (dvsEnabled) numFiles + 1 else numFiles - 1\n      assert(snapshot.allFiles.count() === expectedNumFiles)\n    }\n  }\n\n  test(\"Row tracking marked as not preserved when row tracking disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withRowIdTestTable(isPartitioned = false) {\n        val log = DeltaLog.forTable(spark, TableIdentifier(targetTableName))\n        assert(\n          !rowTrackingMarkedAsPreservedForCommit(log)(executeUpdate(\n            targetTableName, where = None, newVersion = -1L)))\n      }\n    }\n  }\n\n  test(\"Preserving Row Tracking - Subqueries are not supported in UPDATE\") {\n    withRowTrackingEnabled(enabled = true) {\n      withRowIdTestTable(isPartitioned = false) {\n        val ex = intercept[AnalysisException] {\n          checkAndExecuteUpdate(\n            tableName = targetTableName,\n            condition = Some(\n              s\"\"\"id in (SELECT id FROM $targetTableName s\n              WHERE s.id = 0 OR s.id = $numRowsPerFile)\"\"\"))\n        }.getMessage\n        assert(ex.contains(\"Subqueries are not supported in the UPDATE\"))\n      }\n    }\n  }\n\n  for {\n    isPartitioned <- BOOLEAN_DOMAIN\n  } {\n    test(\"UPDATE preserves Row Tracking on tables enabled using backfill, \"\n        + s\"isPartitioned=$isPartitioned\") {\n      withSQLConf(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key -> \"true\") {\n        // This is the expected delta log history by the end of the test.\n        // version 0: Table Creation\n        // version 1: Protocol upgrade\n        // version 2: Backfill commit\n        // version 3: Metadata upgrade (tbl properties)\n        // version 4: Update\n        val backfillCommitVersion = 2L\n        withRowTrackingEnabled(enabled = false) {\n          withTable(targetTableName) {\n            writeTestTable(\n              targetTableName, isPartitioned, lastModifiedVersion = backfillCommitVersion)\n\n            val (log, snapshot) =\n              DeltaLog.forTableWithSnapshot(spark, TableIdentifier(targetTableName))\n            assert(!RowTracking.isEnabled(snapshot.protocol, snapshot.metadata))\n            validateSuccessfulBackfillMetrics(expectedNumSuccessfulBatches = 1) {\n              triggerBackfillOnTestTableUsingAlterTable(targetTableName, numRowsTarget, log)\n            }\n\n            val whereClause = s\"id < ${numRowsPerFile / 2}\"\n            // The newVersion should be 4, the commit associated with the UPDATE.\n            val newVersion = 4L\n            checkAndExecuteUpdate(\n              tableName = targetTableName, condition = Some(whereClause), newVersion)\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait RowTrackingUpdateDVMixin extends RowTrackingUpdateSuiteBase\n  with DeletionVectorsTestUtils {\n\n  override protected def dvsEnabled: Boolean = true\n}\n\n\n// Base trait for UPDATE tests with row tracking.\ntrait UpdateWithRowTrackingOverrides extends UpdateSQLMixin {\n  override def excluded: Seq[String] = super.excluded ++\n    Seq(\n      // TODO: UPDATE on views can't find metadata column\n      \"test update on temp view - view with too many internal aliases - Dataset TempView\",\n      \"test update on temp view - view with too many internal aliases - SQL TempView\",\n      \"test update on temp view - view with too many internal aliases \" +\n        \"with write amplification reduction - Dataset TempView\",\n      \"test update on temp view - view with too many internal aliases \" +\n        \"with write amplification reduction - SQL TempView\",\n      \"test update on temp view - basic - Partition=true - SQL TempView\",\n      \"test update on temp view - basic - Partition=false - SQL TempView\",\n      \"test update on temp view - superset cols - Dataset TempView\",\n      \"test update on temp view - superset cols - SQL TempView\",\n      \"test update on temp view - nontrivial projection - Dataset TempView\",\n      \"test update on temp view - nontrivial projection - SQL TempView\",\n      \"test update on temp view - nontrivial projection \" +\n        \"with write amplification reduction - Dataset TempView\",\n      \"test update on temp view - nontrivial projection \" +\n        \"with write amplification reduction - SQL TempView\",\n      \"update a SQL temp view\",\n      // Checks file size written out\n      \"usage metrics\"\n      )\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowtracking/DefaultRowCommitVersionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowtracking\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, RowTrackingFeature}\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol, RemoveFile}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.{TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION}\nimport org.apache.spark.sql.delta.rowid.RowIdTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DefaultRowCommitVersionSuite extends QueryTest\n  with SharedSparkSession\n  with ParquetTest\n  with RowIdTestUtils {\n  import testImplicits._\n\n  def expectedCommitVersionsForAllFiles(deltaLog: DeltaLog): Map[String, Long] = {\n    val commitVersionForFiles = mutable.Map.empty[String, Long]\n    deltaLog.getChanges(\n        startVersion = 0, catalogTableOpt = None).foreach { case (commitVersion, actions) =>\n      actions.foreach {\n        case a: AddFile if !commitVersionForFiles.contains(a.path) =>\n          commitVersionForFiles += a.path -> commitVersion\n        case r: RemoveFile if commitVersionForFiles.contains(r.path) =>\n          assert(r.defaultRowCommitVersion.contains(commitVersionForFiles(r.path)))\n        case _ =>\n          // Do nothing\n      }\n    }\n    commitVersionForFiles.toMap\n  }\n\n  test(\"defaultRowCommitVersion is not set when feature is disabled\") {\n    withRowTrackingEnabled(enabled = false) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"overwrite\").save(tempDir.getAbsolutePath)\n        spark.range(start = 100, end = 200, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        deltaLog.update().allFiles.collect().foreach { f =>\n          assert(f.defaultRowCommitVersion.isEmpty)\n        }\n      }\n    }\n  }\n\n  test(\"checkpoint preserves defaultRowCommitVersion\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 100, end = 200, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 200, end = 300, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val commitVersionForFiles = expectedCommitVersionsForAllFiles(deltaLog)\n\n        deltaLog.update().allFiles.collect().foreach { f =>\n          assert(f.defaultRowCommitVersion.contains(commitVersionForFiles(f.path)))\n        }\n\n        deltaLog.checkpoint(deltaLog.update())\n\n        deltaLog.update().allFiles.collect().foreach { f =>\n          assert(f.defaultRowCommitVersion.contains(commitVersionForFiles(f.path)))\n        }\n      }\n    }\n  }\n\n  test(\"data skipping reads defaultRowCommitVersion\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 100, end = 200, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 200, end = 300, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val commitVersionForFiles = expectedCommitVersionsForAllFiles(deltaLog)\n\n        val filters = Seq(col(\"id = 150\").expr)\n        val scan = deltaLog.update().filesForScan(filters)\n\n        scan.files.foreach { f =>\n          assert(f.defaultRowCommitVersion.contains(commitVersionForFiles(f.path)))\n        }\n      }\n    }\n  }\n\n  test(\"clone does not preserve default row commit versions\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { sourceDir =>\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(sourceDir.getAbsolutePath)\n        spark.range(start = 100, end = 200, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(sourceDir.getAbsolutePath)\n        spark.range(start = 200, end = 300, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(sourceDir.getAbsolutePath)\n\n        withTable(\"target\") {\n          spark.sql(s\"CREATE TABLE target SHALLOW CLONE delta.`${sourceDir.getAbsolutePath}` \" +\n              s\"TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true')\")\n\n          val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(\"target\"))\n          snapshot.allFiles.collect().foreach { f =>\n            assert(f.defaultRowCommitVersion.contains(0L))\n          }\n        }\n      }\n    }\n  }\n\n  test(\"restore does preserve default row commit versions\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 100, end = 200, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 200, end = 300, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        val deltaLog = DeltaLog.forTable(spark, tempDir)\n        val commitVersionForFiles = expectedCommitVersionsForAllFiles(deltaLog)\n\n        spark.sql(s\"RESTORE delta.`${tempDir.getAbsolutePath}` TO VERSION AS OF 1\")\n\n        deltaLog.update().allFiles.collect().foreach { f =>\n          assert(f.defaultRowCommitVersion.contains(commitVersionForFiles(f.path)))\n        }\n      }\n    }\n  }\n\n  test(\"default row commit versions are reassigned on conflict\") {\n    withTempDir { tempDir =>\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n      // Initial setup - version 0\n      val protocol = Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n        .withFeature(RowTrackingFeature)\n      val metadata = Metadata()\n      deltaLog.startTransaction().commit(Seq(protocol, metadata), ManualUpdate)\n\n      // Start a transaction\n      val txn = deltaLog.startTransaction()\n\n      // Commit two concurrent transactions - version 1 and 2\n      deltaLog.startTransaction().commit(Nil, ManualUpdate)\n      deltaLog.startTransaction().commit(Nil, ManualUpdate)\n\n      // Commit the transaction - version 3\n      val addA = AddFile(path = \"a\", partitionValues = Map.empty, size = 1, modificationTime = 1,\n        dataChange = true, stats = \"{\\\"numRecords\\\": 1}\")\n      val addB = AddFile(path = \"b\", partitionValues = Map.empty, size = 1, modificationTime = 1,\n        dataChange = true, stats = \"{\\\"numRecords\\\": 1}\")\n      txn.commit(Seq(addA, addB), ManualUpdate)\n\n      deltaLog.update().allFiles.collect().foreach { f =>\n        assert(f.defaultRowCommitVersion.contains(3))\n      }\n    }\n  }\n\n  test(\"default row commit versions are assigned when concurrent txn enables row tracking\") {\n    withTempDir { tempDir =>\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n\n      // Initial setup - version 0\n      val protocolWithoutRowTracking =\n        Protocol(TABLE_FEATURES_MIN_READER_VERSION, TABLE_FEATURES_MIN_WRITER_VERSION)\n      val metadata = Metadata()\n      deltaLog.startTransaction().commit(Seq(protocolWithoutRowTracking, metadata), ManualUpdate)\n\n      // Start a transaction\n      val txn = deltaLog.startTransaction()\n\n      // Commit concurrent transactions enabling row tracking - version 1 and 2\n      val protocolWithRowTracking = protocolWithoutRowTracking.withFeature(RowTrackingFeature)\n      deltaLog.startTransaction().commit(Seq(protocolWithRowTracking), ManualUpdate)\n      deltaLog.startTransaction().commit(Nil, ManualUpdate)\n\n      // Commit the transaction - version 3\n      val addA = AddFile(path = \"a\", partitionValues = Map.empty, size = 1, modificationTime = 1,\n        dataChange = true, stats = \"{\\\"numRecords\\\": 1}\")\n      val addB = AddFile(path = \"b\", partitionValues = Map.empty, size = 1, modificationTime = 1,\n        dataChange = true, stats = \"{\\\"numRecords\\\": 1}\")\n      txn.commit(Seq(addA, addB), ManualUpdate)\n\n      deltaLog.update().allFiles.collect().foreach { f =>\n        assert(f.defaultRowCommitVersion.contains(3))\n      }\n    }\n  }\n\n  test(\"can read default row commit versions\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTempDir { tempDir =>\n        spark.range(start = 0, end = 100, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 100, end = 200, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n        spark.range(start = 200, end = 300, step = 1, numPartitions = 1)\n          .write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n        withAllParquetReaders {\n          checkAnswer(\n            spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n              .select(\"id\", \"_metadata.default_row_commit_version\"),\n            (0L until 100L).map(Row(_, 0L)) ++\n              (100L until 200L).map(Row(_, 1L)) ++\n              (200L until 300L).map(Row(_, 2L)))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowtracking/MaterializedColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowtracking\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaIllegalStateException, DeltaLog, DeltaRuntimeException, MaterializedRowCommitVersion, MaterializedRowId, RowTracking}\nimport org.apache.spark.sql.delta.rowid.RowIdTestUtils\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\n\nclass MaterializedColumnSuite extends RowIdTestUtils\n  with ParquetTest {\n\n  private val testTableName = \"target\"\n  private val testDataColumnName = \"test_data\"\n\n  private def withTestTable(testFunction: => Unit): Unit = {\n    withTable(testTableName) {\n      spark.range(end = 10).toDF(testDataColumnName)\n        .write.format(\"delta\").saveAsTable(testTableName)\n      testFunction\n    }\n  }\n\n  private def getMaterializedRowIdColumnName(tableName: String): Option[String] = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n    snapshot.metadata.configuration.get(MaterializedRowId.MATERIALIZED_COLUMN_NAME_PROP)\n  }\n\n  private def getMaterializedRowCommitVersionColumnName(tableName: String): Option[String] = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n    snapshot.metadata.configuration.get(\n      MaterializedRowCommitVersion.MATERIALIZED_COLUMN_NAME_PROP)\n  }\n\n  for ((name, getMaterializedColumnName) <- Map(\n    \"row ids\" -> getMaterializedRowIdColumnName _,\n    \"row commit versions\" -> getMaterializedRowCommitVersionColumnName _\n  )) {\n    test(s\"materialized $name column name is stored when row tracking is enabled\") {\n      withRowTrackingEnabled(enabled = true) {\n        withTestTable {\n          assert(getMaterializedColumnName(testTableName).isDefined)\n        }\n      }\n    }\n\n    test(s\"materialized $name column name is not stored when row tracking is disabled\") {\n      withRowTrackingEnabled(enabled = false) {\n        withTestTable {\n          assert(getMaterializedColumnName(testTableName).isEmpty)\n        }\n      }\n    }\n\n    test(s\"adding a column with the same name as the materialized $name column name fails\") {\n      withRowTrackingEnabled(enabled = true) {\n        withTestTable {\n          val materializedColumnName = getMaterializedColumnName(testTableName).get\n          val error = intercept[DeltaRuntimeException] {\n            sql(s\"ALTER TABLE $testTableName ADD COLUMN (`$materializedColumnName` BIGINT)\")\n          }\n          checkError(error, \"DELTA_ADDING_COLUMN_WITH_INTERNAL_NAME_FAILED\",\n            parameters = Map(\"colName\" -> materializedColumnName))\n        }\n      }\n    }\n\n    test(s\"renaming a column to the materialized $name column name fails\") {\n      withRowTrackingEnabled(enabled = true) {\n        withSQLConf(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> \"name\") {\n          withTestTable {\n            val materializedColumnName = getMaterializedColumnName(testTableName).get\n            val error = intercept[DeltaRuntimeException] {\n              sql(s\"ALTER TABLE $testTableName \" +\n                s\"RENAME COLUMN $testDataColumnName TO `$materializedColumnName`\")\n            }\n            checkError(error, \"DELTA_ADDING_COLUMN_WITH_INTERNAL_NAME_FAILED\",\n              parameters = Map(\"colName\" -> materializedColumnName))\n          }\n        }\n      }\n    }\n\n    test(s\"cloning a table with a column equal to the materialized $name column name fails\") {\n      val targetName = \"target\"\n      val sourceName = \"source\"\n      withTable(targetName, sourceName) {\n        withRowTrackingEnabled(enabled = true) {\n          spark.range(0).toDF(\"val\")\n            .write.format(\"delta\").saveAsTable(targetName)\n\n          val materializedColumnName = getMaterializedColumnName(targetName).get\n          spark.range(0).toDF(materializedColumnName)\n            .write.format(\"delta\").saveAsTable(sourceName)\n\n          val error = intercept[DeltaRuntimeException] {\n            sql(s\"CREATE OR REPLACE TABLE $targetName SHALLOW CLONE $sourceName\")\n          }\n          checkError(error, \"DELTA_ADDING_COLUMN_WITH_INTERNAL_NAME_FAILED\",\n            parameters = Map(\"colName\" -> materializedColumnName))\n        }\n      }\n    }\n\n    test(s\"replace keeps the materialized $name column name\") {\n      withRowTrackingEnabled(enabled = true) {\n        withTestTable {\n          val materializedColumnNameBefore = getMaterializedColumnName(testTableName)\n          sql(\n            s\"\"\"\n               |CREATE OR REPLACE TABLE $testTableName\n               |USING delta AS\n               |SELECT * FROM VALUES (0), (1)\n               |\"\"\".stripMargin)\n          val materializedColumnNameAfter = getMaterializedColumnName(testTableName)\n          assert(materializedColumnNameBefore == materializedColumnNameAfter)\n        }\n      }\n    }\n\n    test(s\"restore keeps the materialized $name column name\") {\n      withRowTrackingEnabled(enabled = true) {\n        withTestTable {\n          spark.range(end = 100).toDF(testDataColumnName)\n            .write.format(\"delta\").mode(\"overwrite\").saveAsTable(testTableName)\n\n          val materializedColumnNameBefore = getMaterializedColumnName(testTableName)\n          io.delta.tables.DeltaTable.forName(testTableName).restoreToVersion(0)\n          val materializedColumnNameAfter = getMaterializedColumnName(testTableName)\n          assert(materializedColumnNameBefore == materializedColumnNameAfter)\n        }\n      }\n    }\n\n    test(s\"clone assigns a materialized $name column when table property is set\") {\n      val sourceTableName = \"source\"\n      val targetTableName = \"target\"\n\n      withTable(sourceTableName, targetTableName) {\n        withRowTrackingEnabled(enabled = false) {\n          spark.range(end = 1).write.format(\"delta\").saveAsTable(sourceTableName)\n          assert(getMaterializedColumnName(sourceTableName).isEmpty)\n\n          sql(s\"CREATE OR REPLACE TABLE $targetTableName SHALLOW CLONE $sourceTableName \" +\n            s\"TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true')\")\n\n          assert(getMaterializedColumnName(targetTableName).isDefined)\n        }\n      }\n    }\n\n    test(s\"clone assigns a materialized $name column when source enables row tracking\") {\n      val sourceTableName = \"source\"\n      val targetTableName = \"target\"\n\n      withTable(sourceTableName, targetTableName) {\n        withRowTrackingEnabled(enabled = true) {\n          spark.range(end = 1).toDF(\"col1\").write.format(\"delta\").saveAsTable(sourceTableName)\n\n          sql(s\"CREATE TABLE $targetTableName SHALLOW CLONE $sourceTableName\")\n\n          val sourceTableColumnName = getMaterializedColumnName(sourceTableName)\n          val targetTableColumnName = getMaterializedColumnName(targetTableName)\n\n          assert(sourceTableColumnName.isDefined)\n          assert(targetTableColumnName.isDefined)\n          assert(sourceTableColumnName !== targetTableColumnName)\n        }\n      }\n    }\n\n    test(s\"clone gives new materialized $name column name for existing empty target table\") {\n      val sourceTableName = \"source\"\n      val targetTableName = \"target\"\n\n      withTable(sourceTableName, targetTableName) {\n        withRowTrackingEnabled(enabled = true) {\n          spark.range(end = 1).toDF(\"col1\").write.format(\"delta\").saveAsTable(sourceTableName)\n          spark.range(end = 0).toDF(\"col2\").write.format(\"delta\").saveAsTable(targetTableName)\n          val oldMaterializedColumnName = getMaterializedColumnName(targetTableName).get\n\n          sql(s\"CREATE OR REPLACE TABLE $targetTableName SHALLOW CLONE $sourceTableName\")\n\n          val newMaterializedColumnName = getMaterializedColumnName(targetTableName).get\n          assert(oldMaterializedColumnName === newMaterializedColumnName)\n          val sourceMaterializedColName = getMaterializedColumnName(sourceTableName).get\n          assert(sourceMaterializedColName !== newMaterializedColumnName)\n        }\n      }\n    }\n\n    test(\"double clone from an empty source table maintains the same \" +\n      s\"materialized $name column name\") {\n      val sourceTableName = \"source\"\n      val targetTableName = \"target\"\n\n      withTable(sourceTableName, targetTableName) {\n        withRowTrackingEnabled(enabled = true) {\n          spark.range(end = 0).toDF(\"col1\").write.format(\"delta\").saveAsTable(sourceTableName)\n\n          sql(s\"CREATE OR REPLACE TABLE $targetTableName SHALLOW CLONE $sourceTableName \" +\n            s\"TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true')\")\n          val materializedColumnNameBefore = getMaterializedColumnName(targetTableName)\n          val sourceMaterializedColName = getMaterializedColumnName(sourceTableName)\n          assert(sourceMaterializedColName !== materializedColumnNameBefore)\n\n          sql(s\"CREATE OR REPLACE TABLE $targetTableName SHALLOW CLONE $sourceTableName\")\n          val materializedColumnNameAfter = getMaterializedColumnName(targetTableName)\n          assert(materializedColumnNameBefore === materializedColumnNameAfter)\n        }\n      }\n    }\n\n    test(s\"self clone of an empty table maintains the same materialized $name column name\") {\n      withRowTrackingEnabled(enabled = true) {\n        withTestTable {\n          spark.range(end = 0).toDF(testDataColumnName)\n            .write.format(\"delta\").mode(\"overwrite\").saveAsTable(testTableName)\n\n          val materializedColumnNameBefore = getMaterializedColumnName(testTableName)\n          sql(s\"CREATE OR REPLACE TABLE $testTableName SHALLOW CLONE $testTableName\")\n          val materializedColumnNameAfter = getMaterializedColumnName(testTableName)\n          assert(materializedColumnNameBefore === materializedColumnNameAfter)\n\n          val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTableName))\n          assert(RowTracking.isEnabled(snapshot.protocol, snapshot.metadata))\n        }\n      }\n    }\n\n    test(s\"can't clone materialized $name column name for existing non-empty target\") {\n      val sourceTableName = \"source\"\n      val targetTableName = \"target\"\n\n      withTable(sourceTableName, targetTableName) {\n        withRowTrackingEnabled(enabled = true) {\n          spark.range(end = 1).toDF(\"col1\").write.format(\"delta\").saveAsTable(sourceTableName)\n          spark.range(end = 1).toDF(\"col2\").write.format(\"delta\").saveAsTable(targetTableName)\n\n          assert(intercept[DeltaIllegalStateException] {\n            sql(s\"CREATE OR REPLACE TABLE $targetTableName SHALLOW CLONE $sourceTableName\")\n          }.getErrorClass === \"DELTA_UNSUPPORTED_NON_EMPTY_CLONE\")\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowtracking/RowTrackingConflictResolutionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowtracking\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.{ManualUpdate, Truncate}\nimport org.apache.spark.sql.delta.actions.{Action, AddFile}\nimport org.apache.spark.sql.delta.actions.{Metadata, Protocol, RemoveFile}\nimport org.apache.spark.sql.delta.commands.backfill.{BackfillCommandStats, RowTrackingBackfillExecutor}\nimport org.apache.spark.sql.delta.deletionvectors.RoaringBitmapArray\nimport org.apache.spark.sql.delta.rowid.RowIdTestUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport io.delta.exceptions.MetadataChangedException\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.dsl.expressions._\nimport org.apache.spark.sql.catalyst.expressions.{EqualTo, Literal}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{IntegerType, StructType}\n\nclass RowTrackingConflictResolutionSuite extends QueryTest\n  with DeletionVectorsTestUtils\n  with SharedSparkSession\n  with RowIdTestUtils {\n\n  override def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_ROW_TRACKING_BACKFILL_ENABLED.key, \"true\")\n    .set(DeltaSQLConf.FEATURE_ENABLEMENT_CONFLICT_RESOLUTION_ENABLED.key, \"true\")\n\n  private val testTableName = \"test_table\"\n\n  private def deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n  private def latestSnapshot = deltaLog.update()\n\n  private def withTestTable(testBlock: => Unit): Unit = {\n    withTable(testTableName) {\n      withRowTrackingEnabled(enabled = false) {\n        // Table is initially empty.\n        spark.range(end = 0).toDF().write.format(\"delta\").saveAsTable(testTableName)\n\n        testBlock\n      }\n    }\n  }\n\n  /** Create an AddFile action for testing purposes. */\n  private def addFile(path: String): AddFile = {\n    AddFile(\n      path = path,\n      partitionValues = Map.empty,\n      size = 1337,\n      modificationTime = 1,\n      dataChange = true,\n      stats = \"\"\"{ \"numRecords\": 1 }\"\"\"\n    )\n  }\n\n  /** Add Row tracking table feature support. */\n  private def activateRowTracking(): Unit = {\n    require(!latestSnapshot.protocol.isFeatureSupported(RowTrackingFeature))\n    val protocolWithRowTracking = Protocol(3, 7).withFeature(RowTrackingFeature)\n    deltaLog.upgradeProtocol(\n      None, latestSnapshot, latestSnapshot.protocol.merge(protocolWithRowTracking))\n  }\n\n  // Add 'numRecords' records to the table.\n  private def commitRecords(numRecords: Int): Unit = {\n    spark.range(numRecords).write.format(\"delta\").mode(\"append\").saveAsTable(testTableName)\n  }\n\n  test(\"Set baseRowId if table feature was committed concurrently\") {\n    withTestTable {\n      val txn = deltaLog.startTransaction()\n      activateRowTracking()\n      txn.commit(Seq(addFile(path = \"file_path\")), DeltaOperations.ManualUpdate)\n\n      assertRowIdsAreValid(deltaLog)\n    }\n  }\n\n  test(\"Set valid baseRowId if table feature and RowIdHighWaterMark are committed concurrently\") {\n    withTestTable {\n      val filePath = \"file_path\"\n      val numConcurrentRecords = 11\n\n      val txn = deltaLog.startTransaction()\n      activateRowTracking()\n      commitRecords(numConcurrentRecords)\n      txn.commit(Seq(addFile(filePath)), DeltaOperations.ManualUpdate)\n\n      assertRowIdsAreValid(deltaLog)\n      val committedAddFile = latestSnapshot.allFiles.collect().filter(_.path == filePath)\n      assert(committedAddFile.size === 1)\n      assert(committedAddFile.head.baseRowId === Some(numConcurrentRecords))\n    }\n  }\n\n  test(\"Conflict resolution if table feature and initial AddFiles are in the same commit\") {\n    withTestTable {\n      val filePath = \"file_path\"\n\n      val txn = deltaLog.startTransaction()\n      val protocolWithRowTracking = Protocol(3, 7).withFeature(RowTrackingFeature)\n      deltaLog.startTransaction().commit(\n        Seq(\n          latestSnapshot.protocol.merge(protocolWithRowTracking),\n          addFile(\"other_path\")\n        ), DeltaOperations.ManualUpdate)\n      txn.commit(Seq(addFile(filePath)), DeltaOperations.ManualUpdate)\n\n      assertRowIdsAreValid(deltaLog)\n      val committedAddFile = latestSnapshot.allFiles.collect().filter(_.path == filePath)\n      assert(committedAddFile.size === 1)\n      assert(committedAddFile.head.baseRowId === Some(1))\n    }\n  }\n\n  test(\"Conflict resolution with concurrent INSERT\") {\n    withTestTable {\n      val filePath = \"file_path\"\n      val numInitialRecords = 7\n      val numConcurrentRecords = 11\n\n      activateRowTracking()\n      commitRecords(numInitialRecords)\n      val txn = deltaLog.startTransaction()\n      commitRecords(numConcurrentRecords)\n      txn.commit(Seq(addFile(filePath)), DeltaOperations.ManualUpdate)\n\n      assertRowIdsAreValid(deltaLog)\n      val committedAddFile = latestSnapshot.allFiles.collect().filter(_.path == filePath)\n      assert(committedAddFile.size === 1)\n      assert(committedAddFile.head.baseRowId === Some(numInitialRecords + numConcurrentRecords))\n      val currentHighWaterMark = RowId.extractHighWatermark(latestSnapshot).get\n      assert(currentHighWaterMark === numInitialRecords + numConcurrentRecords)\n    }\n  }\n\n  test(\"Handle commits that do not bump the high water mark\") {\n    withTestTable {\n      val filePath = \"file_path\"\n      val numInitialRecords = 7\n      activateRowTracking()\n      commitRecords(numInitialRecords)\n\n      val txn = deltaLog.startTransaction()\n      val concurrentTxn = deltaLog.startTransaction()\n      val updatedProtocol = latestSnapshot.protocol\n      concurrentTxn.commit(Seq(updatedProtocol), DeltaOperations.ManualUpdate)\n      txn.commit(Seq(addFile(filePath)), DeltaOperations.ManualUpdate)\n\n      assertRowIdsAreValid(deltaLog)\n    }\n  }\n\n  /**\n   * Setup a test table with four files and return these files to the caller.\n   */\n  private def setupTableAndGetAllFiles(log: DeltaLog): (AddFile, AddFile, AddFile, AddFile) = {\n    val f1 = DeltaTestUtils.createTestAddFile(encodedPath = \"a\", partitionValues = Map(\"x\" -> \"1\"))\n    val f2 = DeltaTestUtils.createTestAddFile(encodedPath = \"b\", partitionValues = Map(\"x\" -> \"1\"))\n    val f3 = DeltaTestUtils.createTestAddFile(encodedPath = \"c\", partitionValues = Map(\"x\" -> \"2\"))\n    val f4 = DeltaTestUtils.createTestAddFile(encodedPath = \"d\", partitionValues = Map(\"x\" -> \"2\"))\n\n    val setupActions: Seq[Action] = Seq(\n      Metadata(\n        schemaString = new StructType().add(\"x\", IntegerType).json,\n        partitionColumns = Seq(\"x\")),\n      f1,\n      f2,\n      f3,\n      f4,\n      Action.supportedProtocolVersion(\n        featuresToExclude = Seq(CatalogOwnedTableFeature)).withFeature(RowTrackingFeature)\n    )\n\n    log.startTransaction().commit(setupActions, ManualUpdate)\n\n    (f1, f2, f3, f4)\n  }\n\n  /** Add a dummy DV to a file in a table. */\n  private def addDVToFileInTable(deltaLog: DeltaLog, file: AddFile): (AddFile, RemoveFile) = {\n    val dv = writeDV(deltaLog, RoaringBitmapArray(0L))\n    updateFileDV(file, dv)\n  }\n\n  /** Execute backfill on the table associated with the delta log passed in. */\n  private def executeBackfill(log: DeltaLog, backfillTxn: OptimisticTransaction): Unit = {\n    val backfillStats = BackfillCommandStats(\n      backfillTxn.txnId,\n      nameOfTriggeringOperation = DeltaOperations.OP_SET_TBLPROPERTIES)\n    val backfillExecutor = new RowTrackingBackfillExecutor(\n      spark,\n      log,\n      catalogTableOpt = None,\n      backfillTxn.txnId,\n      backfillStats\n    )\n    backfillExecutor.run(maxNumFilesPerCommit = 4)\n  }\n\n  /** Check if base row IDs and default row commit versions have been assigned. */\n  def assertBaseRowIDsAndDefaultRowCommitVersionsAssigned(finalFiles: Seq[AddFile]): Unit = {\n    finalFiles.foreach(addedFile => assert(addedFile.baseRowId.nonEmpty))\n    finalFiles.foreach(addedFile => assert(addedFile.defaultRowCommitVersion.nonEmpty))\n  }\n\n  test(\"Backfill conflict with a delete, Delete wins\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n      // Setup\n      val (file1, file2, file3, file4) = setupTableAndGetAllFiles(log)\n\n      // Start Backfill.\n      val backfillTxn = log.startTransaction()\n\n      // A delete occurs in parallel. Delete wins.\n      val deleteTxn = log.startTransaction()\n      deleteTxn.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n      val deleteActions = Seq(file1.remove, file2.remove)\n      // Truncate is a data-changing operation.\n      deleteTxn.commit(deleteActions, Truncate())\n\n      // Finish backfill.\n      executeBackfill(log, backfillTxn)\n\n      val finalFiles = log.update().allFiles.collect()\n      assertBaseRowIDsAndDefaultRowCommitVersionsAssigned(finalFiles)\n      assertRowIdsAreValid(log)\n      assert(finalFiles.map(_.path).toSet === Seq(file3, file4).map(_.path).toSet)\n    }\n  }\n\n  test(\"Backfill conflicts with a delete, Backfill wins\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      // Setup\n      val (file1, file2, file3, file4) = setupTableAndGetAllFiles(log)\n\n      // Start delete\n      val deleteTxn = log.startTransaction()\n      deleteTxn.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n\n      // Backfill occurs in parallel and wins.\n      val backfillTxn = log.startTransaction()\n      executeBackfill(log, backfillTxn)\n\n      val deleteActions = Seq(file1.remove, file2.remove)\n      // Truncate is a data-changing operation.\n      deleteTxn.commit(deleteActions, Truncate())\n\n      val finalFiles = log.update().allFiles.collect()\n      assertBaseRowIDsAndDefaultRowCommitVersionsAssigned(finalFiles)\n      assertRowIdsAreValid(log)\n      assert(finalFiles.map(_.path).toSet === Seq(file3, file4).map(_.path).toSet)\n    }\n  }\n\n  test(\"Backfill conflicts with a DV delete, Delete wins\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n\n      // Setup\n      val (file1, file2, file3, file4) = setupTableAndGetAllFiles(log)\n      enableDeletionVectorsInTable(log)\n\n      // Start Backfill\n      val backfillTxn = log.startTransaction()\n\n      // A delete occurs in parallel. Delete wins.\n      val deleteTxn = log.startTransaction()\n      deleteTxn.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n      val (addFile1WithDV, removeFile1) = addDVToFileInTable(log, file1)\n      val (addFile2WithDV, removeFile2) = addDVToFileInTable(log, file2)\n      val deleteActions = Seq(addFile1WithDV, removeFile1, addFile2WithDV, removeFile2)\n      // Truncate is a data-changing operation.\n      deleteTxn.commit(deleteActions, Truncate())\n\n      // Finish Backfill\n      executeBackfill(log, backfillTxn)\n\n      val finalFiles = log.update().allFiles.collect()\n      assertBaseRowIDsAndDefaultRowCommitVersionsAssigned(finalFiles)\n      assertRowIdsAreValid(log)\n      val allFiles = Seq(file1, file2, file3, file4)\n      assert(finalFiles.map(_.path).toSet === allFiles.map(_.path).toSet)\n    }\n  }\n\n  test(\"Backfill conflicts with a DV delete, Backfill wins\") {\n    withTempDir { dir =>\n      val log = DeltaLog.forTable(spark, dir.getCanonicalPath)\n      // Setup\n      val (file1, file2, file3, file4) = setupTableAndGetAllFiles(log)\n      enableDeletionVectorsInTable(log)\n\n      // Start delete\n      val deleteTxn = log.startTransaction()\n      deleteTxn.filterFiles(EqualTo('x, Literal(1)) :: Nil)\n\n      // Backfill occurs in parallel and wins.\n      val backfillTxn = log.startTransaction()\n      executeBackfill(log, backfillTxn)\n\n      val (addFile1WithDV, removeFile1) = addDVToFileInTable(log, file1)\n      val (addFile2WithDV, removeFile2) = addDVToFileInTable(log, file2)\n      val deleteActions = Seq(addFile1WithDV, removeFile1, addFile2WithDV, removeFile2)\n      // Truncate is a data-changing operation.\n      deleteTxn.commit(deleteActions, Truncate())\n\n      val finalFiles = log.update().allFiles.collect()\n      assertBaseRowIDsAndDefaultRowCommitVersionsAssigned(finalFiles)\n      assertRowIdsAreValid(log)\n      val allFiles = Seq(file1, file2, file3, file4)\n      assert(finalFiles.map(_.path).toSet === allFiles.map(_.path).toSet)\n    }\n  }\n\n  private def addRowTrackingEnabledConfigToMetadata(metadata: Metadata): Metadata = {\n    val newConfigs = metadata.configuration updated\n      (DeltaConfigs.ROW_TRACKING_ENABLED.key, \"true\")\n    metadata.copy(configuration = newConfigs)\n  }\n\n  private def enableRowTrackingOnlyMetadataUpdate(): Unit = {\n    val txn = deltaLog.startTransaction()\n    val updatedMetadata = addRowTrackingEnabledConfigToMetadata(latestSnapshot.metadata)\n    val tags = Map(DeltaCommitTag.RowTrackingEnablementOnlyTag.key -> \"true\")\n    txn.updateMetadata(updatedMetadata)\n    txn.commit(Nil, ManualUpdate, tags)\n  }\n\n  test(\"RowTrackingEnablementOnly metadata update does not fail txns that don't update metadata\") {\n    withTestTable {\n      withSQLConf(DeltaSQLConf.FEATURE_ENABLEMENT_CONFLICT_RESOLUTION_ENABLED.key -> \"false\") {\n        val txn = deltaLog.startTransaction()\n        activateRowTracking()\n        enableRowTrackingOnlyMetadataUpdate()\n\n        val rowTrackingPreserved = rowTrackingMarkedAsPreservedForCommit(deltaLog) {\n          txn.commit(Seq(addFile(path = \"file_path\")), DeltaOperations.ManualUpdate)\n        }\n\n        assert(!rowTrackingPreserved, \"Commits conflicting with a metadata update \" +\n          \"that enables row tracking only should have row tracking marked as not preserved.\")\n\n        assertRowIdsAreValid(deltaLog)\n        assert(RowTracking.isEnabled(latestSnapshot.protocol, latestSnapshot.metadata))\n      }\n    }\n  }\n\n  test(\"RowTrackingEnablementOnly metadata update fails transactions \"\n      + \"that perform a metadata update\") {\n    withTestTable {\n      activateRowTracking()\n      val numInitialRecords = 7\n      commitRecords(numInitialRecords)\n\n      val txn = deltaLog.startTransaction()\n      val newConfigs = Map(\"key\" -> \"value\")\n      val newMetadata = latestSnapshot.metadata.copy(configuration = newConfigs)\n      txn.updateMetadata(newMetadata)\n\n      enableRowTrackingOnlyMetadataUpdate()\n\n      val commitVersionBefore = latestSnapshot.version\n      intercept[MetadataChangedException] {\n        txn.commit(Nil, DeltaOperations.ManualUpdate)\n      }\n      assert(latestSnapshot.version === commitVersionBefore,\n        \"the commit should have failed\")\n    }\n  }\n\n  test(\"RowTrackingEnablementOnly metadata update fails another \" +\n      \"RowTrackingEnablementOnly metadata update\") {\n    withTestTable {\n      activateRowTracking()\n      val txn = deltaLog.startTransaction()\n      val newMetadata = addRowTrackingEnabledConfigToMetadata(latestSnapshot.metadata)\n      txn.updateMetadata(newMetadata)\n\n      enableRowTrackingOnlyMetadataUpdate()\n\n      val commitVersionBefore = latestSnapshot.version\n      intercept[MetadataChangedException] {\n        val tags = Map(DeltaCommitTag.RowTrackingEnablementOnlyTag.key -> \"true\")\n        txn.commit(Nil, DeltaOperations.ManualUpdate, tags)\n      }\n      assert(latestSnapshot.version === commitVersionBefore,\n        \"the commit should have failed\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowtracking/RowTrackingReadWriteSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowtracking\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, RowCommitVersion, RowId}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.rowid.RowIdTestUtils\n\nimport org.apache.spark.sql.{AnalysisException, Column, DataFrame, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.execution.datasources.FileFormat\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\nimport org.apache.spark.sql.functions.{col, lit, when}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types.{LongType, StructType}\n\nclass RowTrackingReadWriteSuite extends RowIdTestUtils\n  with ParquetTest {\n\n  private val testTableName = \"target\"\n\n  private val testDataColumnName = \"test_data\"\n\n  test(\"select star does not read materialized columns\") {\n    withAllParquetReaders {\n      withTable(testTableName) {\n        writeWithMaterializedRowCommitVersionColumns(\n          spark.range(100).toDF(testDataColumnName),\n          rowIdColumn = lit(1L),\n          rowCommitVersionColumn = lit(2L))\n\n        withAllParquetReaders {\n          val df = sql(s\"SELECT * FROM $testTableName\")\n          assert(df.schema.size === 1)\n          assert(df.schema.head.name === testDataColumnName)\n        }\n      }\n    }\n  }\n\n  test(\"write and read table without materialized columns\") {\n    withTable(testTableName) {\n      withRowTrackingEnabled(enabled = true) {\n        val numRows = 5L\n        spark.range(numRows).toDF(testDataColumnName)\n          .write.format(\"delta\").saveAsTable(testTableName)\n\n        try {\n          // Confirm that the materialized columns are not present in the Parquet file(s).\n            val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n            val parquetDataFrame = spark.read.parquet(deltaLog.dataPath.toString)\n            checkAnswer(parquetDataFrame, (0L until numRows).map(Row(_)))\n        } catch {\n          case e: Exception => Thread.sleep(1000 * 1000)\n        }\n\n        withAllParquetReaders {\n          val df = spark.read.table(testTableName)\n            .select(\n              testDataColumnName,\n              RowId.QUALIFIED_COLUMN_NAME,\n              RowCommitVersion.QUALIFIED_COLUMN_NAME)\n          checkAnswer(df, (0L until numRows).map(i => Row(i, i, 0)))\n        }\n      }\n    }\n  }\n\n  test(\"write and read table with all-null materialized columns\") {\n    withTable(testTableName) {\n      val numRows = 5L\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.range(numRows).toDF(testDataColumnName),\n        rowIdColumn = lit(null).cast(\"long\"),\n        rowCommitVersionColumn = lit(null).cast(\"long\"))\n\n      // Confirm that the materialized columns are present in the Parquet file(s).\n      withSQLConf(\n        // The function writeWithMaterializedRowCommitVersionColumns initially creates\n        // an empty table with only the testDataColumnName column. After that, we write\n        // some data to the Delta table, appending some Parquet files that include\n        // both the testDataColumnName and the materialized Row Tracking Columns.\n        //\n        // PARQUET_SCHEMA_MERGING_ENABLED by default is disabled, so what then happens\n        // is we pick a random data file to infer the schema of the underlying parquet\n        // DataFrame of the Delta table in [[ParquetUtils]].\n        //\n        // Thus, that inferred schema could either contains only the testDataColumnName\n        // column or all three columns, the former leads to wrong result for the\n        // checkAnswer below.\n        //\n        // This is the intended behavior as Delta doesn't give any guarantee\n        // that the Parquet files will all have the same schema, and normally we should\n        // not read raw Parquet files from a Delta table, we are only doing this for\n        // testing purpose.\n        SQLConf.PARQUET_SCHEMA_MERGING_ENABLED.key -> \"true\"\n      ) {\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n        val parquetDataFrame = spark.read.parquet(deltaLog.dataPath.toString)\n        checkAnswer(parquetDataFrame, (0L until numRows).map(Row(_, null, null)))\n      }\n\n      withAllParquetReaders {\n        val df = spark.read.table(testTableName)\n          .select(\n            testDataColumnName,\n            RowId.QUALIFIED_COLUMN_NAME,\n            RowCommitVersion.QUALIFIED_COLUMN_NAME)\n        checkAnswer(df, (0L until 5L).map(i => Row(i, i, 1)))\n      }\n    }\n  }\n\n  test(\"write and read table with no-nulls materialized columns\") {\n    withTable(testTableName) {\n      val numRows = 100\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.range(numRows).toDF(testDataColumnName),\n        rowIdColumn = col(testDataColumnName) + 10,\n        rowCommitVersionColumn = col(testDataColumnName) + 100)\n\n      withAllParquetReaders {\n        val df = spark.table(testTableName)\n          .select(\n            testDataColumnName,\n            RowId.QUALIFIED_COLUMN_NAME,\n            RowCommitVersion.QUALIFIED_COLUMN_NAME)\n        val expectedAnswer = (0L until numRows).map(i => Row(i, i + 10, i + 100))\n        checkAnswer(df, expectedAnswer)\n      }\n    }\n  }\n\n  test(\"write and read table with mixed materialized columns\") {\n    withTable(testTableName) {\n      val numRows = 100L\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.range(numRows).toDF(testDataColumnName),\n        rowIdColumn =\n          when(col(testDataColumnName) % 2 === 0, col(testDataColumnName) + 10)\n            .otherwise(null),\n        rowCommitVersionColumn =\n          when(col(testDataColumnName) % 3 === 0, col(testDataColumnName) + 100)\n            .otherwise(null))\n\n      withAllParquetReaders {\n        val df = spark.table(testTableName)\n          .select(\n            testDataColumnName,\n            RowId.QUALIFIED_COLUMN_NAME,\n            RowCommitVersion.QUALIFIED_COLUMN_NAME)\n        val expectedAnswer = (0L until numRows).map { i =>\n          Row(i, if (i % 2 == 0) i + 10 else i, if (i % 3 == 0) i + 100 else 1)\n        }\n        checkAnswer(df, expectedAnswer)\n      }\n    }\n  }\n\n  test(\"read mixed materialized columns with filter\") {\n    withTable(testTableName) {\n      val numRows = 100L\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.range(numRows).toDF(testDataColumnName),\n        rowIdColumn =\n          when(col(testDataColumnName) % 2 === 0, lit(1000L))\n            .otherwise(null),\n        rowCommitVersionColumn =\n          when(col(testDataColumnName) % 3 === 0, lit(2000L))\n            .otherwise(null))\n\n      withAllParquetReaders {\n        // Read the table with a filter that does not match any of the materialized row ids and\n        // row commit versions, but that does match the default-generated ids and versions.\n        val df = spark.table(testTableName)\n          .select(\n            testDataColumnName,\n            RowId.QUALIFIED_COLUMN_NAME,\n            RowCommitVersion.QUALIFIED_COLUMN_NAME)\n          .filter(col(RowId.QUALIFIED_COLUMN_NAME) <= 100 &&\n            col(RowCommitVersion.QUALIFIED_COLUMN_NAME) <= 100)\n        val expectedAnswer = (0L until numRows)\n          .filter(i => i % 2 != 0 && i % 3 != 0)\n          .map { i => Row(i, i, 1) }\n        checkAnswer(df, expectedAnswer)\n      }\n    }\n  }\n\n  test(\"writing to materialized column requires correct metadata\") {\n    withTable(testTableName) {\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.range(100).toDF(\"id\"),\n        rowIdColumn = lit(null),\n        rowCommitVersionColumn = lit(null))\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      val materializedRowIdColumnName =\n        extractMaterializedRowIdColumnName(deltaLog).get\n      val materializedRowCommitVersionColumnName =\n        extractMaterializedRowCommitVersionColumnName(deltaLog).get\n\n      val insertStmt1 = s\"INSERT INTO $testTableName (id, `$materializedRowIdColumnName`)\"\n      val errorRowIds = intercept[AnalysisException](sql(insertStmt1 + \" VALUES(1, 2)\"))\n      checkError(\n        errorRowIds,\n        \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n        parameters = errorRowIds.messageParameters,\n        queryContext = Array(ExpectedContext(insertStmt1, 0, insertStmt1.length - 1)))\n\n      val insertStmt2 =\n        s\"INSERT INTO $testTableName (id, `$materializedRowCommitVersionColumnName`)\"\n      val errorRowCommitVersions = intercept[AnalysisException](sql(insertStmt2 + \" VALUES(1, 2)\"))\n      checkError(\n        errorRowCommitVersions,\n        \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n        parameters = errorRowCommitVersions.messageParameters,\n        queryContext = Array(ExpectedContext(insertStmt2, 0, insertStmt2.length - 1)))\n    }\n  }\n\n  test(\"writing to materialized column requires correct name\") {\n    withRowTrackingEnabled(enabled = true) {\n      withTable(testTableName) {\n        writeWithMaterializedRowCommitVersionColumns(\n          spark.range(1).toDF(testDataColumnName),\n          rowIdColumn = lit(1L),\n          rowCommitVersionColumn = lit(2L))\n\n        checkWritingToMaterializedColumnsRequiresCorrectName()\n      }\n    }\n  }\n\n  test(\"writing to materialized column requires row tracking to be enabled\") {\n    withTable(testTableName) {\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.range(1).toDF(testDataColumnName),\n        rowIdColumn = lit(1L),\n        rowCommitVersionColumn = lit(2L))\n\n      spark.sql(s\"ALTER TABLE $testTableName \" +\n        s\"SET TBLPROPERTIES ('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'false')\")\n\n      checkWritingToMaterializedColumnsRequiresCorrectName()\n    }\n  }\n\n  private def checkWritingToMaterializedColumnsRequiresCorrectName(): Unit = {\n    val columnNames = Seq(\n      RowId.QUALIFIED_COLUMN_NAME,\n      s\"`${FileFormat.METADATA_NAME}`.`${RowId.ROW_ID}`\",\n      s\"`${RowId.QUALIFIED_COLUMN_NAME}`\",\n      RowId.QUALIFIED_COLUMN_NAME,\n      s\"`${FileFormat.METADATA_NAME}`.`${RowId.ROW_ID}`\",\n      s\"`${RowId.QUALIFIED_COLUMN_NAME}`\")\n\n    for (columnName <- columnNames) {\n      // Throw an error because only using the materialized column name is valid.\n      val error = intercept[AnalysisException] {\n        spark\n          .range(end = 1)\n          .toDF(testDataColumnName)\n          .withMaterializedRowIdColumn(columnName, lit(1L))\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(testTableName)\n      }\n      checkError(\n        error,\n        \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n        parameters = error.messageParameters)\n    }\n\n    for (columnName <- columnNames) {\n      // Throw an error because only using the materialized column name is valid.\n      val error = intercept[AnalysisException] {\n        spark\n          .range(end = 1)\n          .toDF(testDataColumnName)\n          .withMaterializedRowCommitVersionColumn(columnName, lit(2L))\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .saveAsTable(testTableName)\n      }\n      checkError(\n        error,\n        \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n        parameters = error.messageParameters)\n    }\n  }\n\n  test(\"write and read with column names similar to row tracking columns in the table schema\") {\n    val columnNames = Seq(\n      RowId.QUALIFIED_COLUMN_NAME,\n      RowCommitVersion.QUALIFIED_COLUMN_NAME)\n    for (columnName <- columnNames) {\n      withTable(testTableName) {\n        val numRows = 10L\n        writeWithMaterializedRowCommitVersionColumns(\n          spark.range(numRows).toDF(columnName),\n          rowIdColumn = lit(1L),\n          rowCommitVersionColumn = lit(2L))\n\n        withAllParquetReaders {\n          val df = spark.read.table(testTableName).select(\n            s\"`$columnName`\",\n            RowId.QUALIFIED_COLUMN_NAME,\n            RowCommitVersion.QUALIFIED_COLUMN_NAME)\n          val expectedAnswer = (0L until numRows).map(Row(_, 1L, 2L))\n          checkAnswer(df, expectedAnswer)\n        }\n      }\n    }\n  }\n\n  test(\"write and read with conflicting columns\") {\n    withTable(testTableName) {\n      val tableSchema = new StructType()\n        .add(\"id\", LongType)\n        .add(FileFormat.METADATA_NAME, new StructType()\n          .add(RowId.ROW_ID, LongType)\n          .add(RowCommitVersion.METADATA_STRUCT_FIELD_NAME, LongType))\n\n      writeWithMaterializedRowCommitVersionColumns(\n        spark.createDataFrame(\n          Seq(Row(1L, (11L, 111L)), Row(2L, (22L, 222L)), Row(3L, (33L, 333L))).asJava,\n          tableSchema),\n        rowIdColumn = lit(-1L),\n        rowCommitVersionColumn = lit(-2L))\n\n      withAllParquetReaders {\n        val table = spark.read.table(testTableName)\n        val metadataCol = table.metadataColumn(FileFormat.METADATA_NAME)\n        val userCol = table.col(FileFormat.METADATA_NAME)\n        val df = table.select(\n          col(\"id\"),\n          metadataCol.getField(RowId.ROW_ID),\n          metadataCol.getField(RowCommitVersion.METADATA_STRUCT_FIELD_NAME),\n          userCol.getField(RowId.ROW_ID),\n          userCol.getField(RowCommitVersion.METADATA_STRUCT_FIELD_NAME))\n        val expectedAnswer =\n          Seq(Row(1, -1, -2, 11, 111), Row(2, -1, -2, 22, 222), Row(3, -1, -2, 33, 333))\n        checkAnswer(df, expectedAnswer)\n      }\n    }\n  }\n\n  private def writeWithMaterializedRowCommitVersionColumns(\n      df: DataFrame,\n      rowIdColumn: Column,\n      rowCommitVersionColumn: Column): Unit = {\n    withRowTrackingEnabled(enabled = true) {\n      // Create the table if it does not exist already.\n      df.limit(n = 0)\n        .write.format(\"delta\").mode(\"append\").saveAsTable(testTableName)\n\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n      val materializedRowIdColumnName =\n        extractMaterializedRowIdColumnName(deltaLog).get\n      val materializedRowCommitVersionName =\n        extractMaterializedRowCommitVersionColumnName(deltaLog).get\n\n      df.withMaterializedRowIdColumn(\n          materializedRowIdColumnName, rowIdColumn)\n        .withMaterializedRowCommitVersionColumn(\n          materializedRowCommitVersionName, rowCommitVersionColumn)\n        .write\n        .mode(\"append\")\n        .format(\"delta\")\n        .saveAsTable(testTableName)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/rowtracking/RowTrackingTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.rowtracking\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, RowTrackingFeature}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait RowTrackingDisabled extends RowTrackingTestUtils {\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey, \"false\")\n    .set(defaultRowTrackingFeatureProperty, \"supported\")\n}\n\ntrait RowTrackingEnabled extends RowTrackingTestUtils {\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey, \"true\")\n}\n\ntrait RowTrackingTestUtils\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with ParquetTest {\n  lazy val rowTrackingFeatureName: String =\n    TableFeatureProtocolUtils.propertyKey(RowTrackingFeature)\n  lazy val defaultRowTrackingFeatureProperty: String =\n    TableFeatureProtocolUtils.defaultPropertyKey(RowTrackingFeature)\n\n  def withRowTrackingEnabled(enabled: Boolean)(f: => Unit): Unit = {\n    withSQLConf(DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> enabled.toString)(f)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/schema/CaseSensitivitySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.{StreamingQuery, StreamingQueryException}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass CaseSensitivitySuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  private def testWithCaseSensitivity(name: String)(f: => Unit): Unit = {\n    testQuietly(name) {\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n        f\n      }\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n        f\n      }\n    }\n  }\n\n  private def getPartitionValues(allFiles: Dataset[AddFile], colName: String): Array[String] = {\n    allFiles.select(col(s\"partitionValues.$colName\")).where(col(colName).isNotNull)\n      .distinct().as[String].collect()\n  }\n\n  testWithCaseSensitivity(\"case sensitivity of partition fields\") {\n    withTempDir { tempDir =>\n      val query = \"SELECT id + 1 as Foo, id as Bar FROM RANGE(1)\"\n      sql(query).write.partitionBy(\"foo\").format(\"delta\").save(tempDir.getAbsolutePath)\n      checkAnswer(\n        sql(query),\n        spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      )\n\n      val allFiles = DeltaLog.forTable(spark, tempDir.getAbsolutePath).snapshot.allFiles\n      assert(getPartitionValues(allFiles, \"Foo\") === Array(\"1\"))\n      checkAnswer(\n        spark.read.format(\"delta\").load(tempDir.getAbsolutePath),\n        Row(1L, 0L)\n      )\n    }\n  }\n\n  testQuietly(\"case sensitivity of partition fields (stream)\") {\n    // DataStreamWriter auto normalizes partition columns, therefore we don't need to check\n    // case sensitive case\n    withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n      withTempDir { tempDir =>\n        val memSource = MemoryStream[(Long, Long)]\n        val stream1 = startStream(memSource.toDF().toDF(\"Foo\", \"Bar\"), tempDir)\n        try {\n          memSource.addData((1L, 0L))\n          stream1.processAllAvailable()\n        } finally {\n          stream1.stop()\n        }\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getAbsolutePath),\n          Row(1L, 0L)\n        )\n\n        val allFiles = DeltaLog.forTable(spark, tempDir.getAbsolutePath).snapshot.allFiles\n        assert(getPartitionValues(allFiles, \"Foo\") === Array(\"1\"))\n      }\n    }\n  }\n\n  testWithCaseSensitivity(\"two fields with same name\") {\n    withTempDir { tempDir =>\n      intercept[AnalysisException] {\n        val query = \"SELECT id as Foo, id as foo FROM RANGE(1)\"\n        sql(query).write.partitionBy(\"foo\").format(\"delta\").save(tempDir.getAbsolutePath)\n      }\n    }\n  }\n\n  testWithCaseSensitivity(\"two fields with same name (stream)\") {\n    withTempDir { tempDir =>\n      val memSource = MemoryStream[(Long, Long)]\n      val stream1 = startStream(memSource.toDF().toDF(\"Foo\", \"foo\"), tempDir)\n      try {\n        val e = intercept[StreamingQueryException] {\n          memSource.addData((0L, 0L))\n          stream1.processAllAvailable()\n        }\n        assert(e.cause.isInstanceOf[AnalysisException])\n      } finally {\n        stream1.stop()\n      }\n    }\n  }\n\n  testWithCaseSensitivity(\"schema merging is case insenstive but preserves original case\") {\n    withTempDir { tempDir =>\n      val query1 = \"SELECT id as foo, id as bar FROM RANGE(1)\"\n      sql(query1).write.format(\"delta\").save(tempDir.getAbsolutePath)\n\n      val query2 = \"SELECT id + 1 as Foo, id as bar FROM RANGE(1)\" // notice how 'F' is capitalized\n      sql(query2).write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n      val query3 = \"SELECT id as bAr, id + 2 as Foo FROM RANGE(1)\" // changed order as well\n      sql(query3).write.format(\"delta\").mode(\"append\").save(tempDir.getAbsolutePath)\n\n      val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      checkAnswer(\n        df,\n        Row(0, 0) :: Row(1, 0) :: Row(2, 0) :: Nil\n      )\n      assert(df.schema.fieldNames === Seq(\"foo\", \"bar\"))\n    }\n  }\n\n  testWithCaseSensitivity(\"schema merging preserving column case (stream)\") {\n    withTempDir { tempDir =>\n      val memSource = MemoryStream[(Long, Long)]\n      val stream1 = startStream(memSource.toDF().toDF(\"Foo\", \"Bar\"), tempDir, None)\n      try {\n        memSource.addData((0L, 0L))\n        stream1.processAllAvailable()\n      } finally {\n        stream1.stop()\n      }\n      val stream2 = startStream(memSource.toDF().toDF(\"foo\", \"Bar\"), tempDir, None)\n      try {\n        memSource.addData((1L, 2L))\n        stream2.processAllAvailable()\n      } finally {\n        stream2.stop()\n      }\n\n      val df = spark.read.format(\"delta\").load(tempDir.getAbsolutePath)\n      checkAnswer(\n        df,\n        Row(0L, 0L) :: Row(1L, 2L) :: Nil\n      )\n      assert(df.schema.fieldNames === Seq(\"Foo\", \"Bar\"))\n    }\n  }\n\n  test(\"SC-12677: replaceWhere predicate should be case insensitive\") {\n    withTempDir { tempDir =>\n      val path = tempDir.getCanonicalPath\n      Seq((1, \"a\"), (2, \"b\")).toDF(\"Key\", \"val\").write\n        .partitionBy(\"key\").format(\"delta\").mode(\"append\").save(path)\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"false\") {\n        Seq((2, \"c\")).toDF(\"Key\", \"val\").write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"replaceWhere\", \"key = 2\") // note the different case\n          .save(path)\n      }\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        Row(1, \"a\") :: Row(2, \"c\") :: Nil\n      )\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n        val e = intercept[AnalysisException] {\n          Seq((2, \"d\")).toDF(\"Key\", \"val\").write\n            .format(\"delta\")\n            .mode(\"overwrite\")\n            .option(\"replaceWhere\", \"key = 2\") // note the different case\n            .save(path)\n        }\n        assert(e.getErrorClass == \"UNRESOLVED_COLUMN.WITHOUT_SUGGESTION\"\n          || e.getErrorClass == \"MISSING_COLUMN\"\n          || e.getErrorClass == \"UNRESOLVED_COLUMN.WITH_SUGGESTION\")\n      }\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        Row(1, \"a\") :: Row(2, \"c\") :: Nil\n      )\n    }\n  }\n\n  private def startStream(\n      df: Dataset[_],\n      tempDir: File,\n      partitionBy: Option[String] = Some(\"foo\")): StreamingQuery = {\n    val writer = df.writeStream\n      .option(\"checkpointLocation\", new File(tempDir, \"_checkpoint\").getAbsolutePath)\n      .format(\"delta\")\n    partitionBy.foreach(writer.partitionBy(_))\n    writer.start(tempDir.getAbsolutePath)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/schema/CheckConstraintsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{AllowedUserProvidedExpressions, DeltaConfigs, DeltaLog}\nimport org.apache.spark.sql.delta.constraints.CharVarcharConstraint\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.ValidateCheckConstraintsMode\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.parser.ParseException\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{ArrayType, BooleanType, IntegerType, MapType, MetadataBuilder, StringType, StructField, StructType}\n\nclass CheckConstraintsSuite extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils {\n\n\n  import testImplicits._\n\n  private def withTestTable(thunk: String => Unit) = {\n    withSQLConf(\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"3\") {\n      withTable(\"checkConstraintsTest\") {\n        Seq(\n          (1, \"a\"), (2, \"b\"), (3, \"c\"),\n          (4, \"d\"), (5, \"e\"), (6, \"f\")\n        ).toDF(\"num\", \"text\").write.format(\"delta\").saveAsTable(\"checkConstraintsTest\")\n        thunk(\"checkConstraintsTest\")\n      }\n    }\n  }\n\n  private def errorContains(errMsg: String, str: String): Unit = {\n    errMsg.contains(str)\n  }\n\n  test(\"can't add unparseable constraint\") {\n    withTestTable { table =>\n      val e = intercept[ParseException] {\n        sql(s\"ALTER TABLE $table\\nADD CONSTRAINT lessThan5 CHECK (id <)\")\n      }\n      // Make sure we're still getting a useful parse error, even though we do some complicated\n      // internal stuff to persist the constraint. Unfortunately this test may be a bit fragile.\n      errorContains(e.getMessage, \"Syntax error at or near end of input\")\n      errorContains(e.getMessage,\n        \"\"\"\n          |== SQL ==\n          |id <\n          |----^^^\n          |\"\"\".stripMargin)\n    }\n  }\n\n  test(\"Checking incorrect constraints added through table property in CREATE TABLE errors out\") {\n    val tableName = \"test_tbl\"\n    withTable(tableName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tableName (\n           |id INT,\n           |event_date DATE\n           |) USING DELTA\n           |TBLPROPERTIES('delta.constraints.ch' = 'event_date < 2025-06-12');\"\"\".stripMargin)\n\n      val e = intercept[AnalysisException] {\n        sql(s\"INSERT INTO $tableName VALUES(1, '2025-06-11')\")\n      }\n      errorContains(e.getMessage,\n        \"Cannot resolve \\\"(event_date < ((2025 - 6) - 12))\\\" due to data type mismatch\")\n    }\n  }\n\n  test(\"CREATE TABLE with check constraint referencing non-existent column fails at create time\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n        ValidateCheckConstraintsMode.ASSERT.toString) {\n      val tableName = \"test_create_invalid_constraint\"\n      withTable(tableName) {\n        checkError(\n          exception = intercept[AnalysisException] {\n            sql(\n              s\"\"\"\n                 |CREATE TABLE $tableName (\n                 |id INT,\n                 |value STRING\n                 |) USING DELTA\n                 |TBLPROPERTIES('delta.constraints.invalid' = 'non_existent_column > 0')\n                 |\"\"\".stripMargin)\n          },\n          \"DELTA_INVALID_CHECK_CONSTRAINT_REFERENCES\",\n          parameters = Map(\"colName\" -> \"`non_existent_column`\")\n        )\n      }\n    }\n  }\n\n  test(\"CREATE TABLE with non-boolean check constraint fails at create time\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n        ValidateCheckConstraintsMode.ASSERT.toString) {\n      val tableName = \"test_create_non_boolean_constraint\"\n      withTable(tableName) {\n        checkError(\n          exception = intercept[AnalysisException] {\n            sql(\n              s\"\"\"\n                 |CREATE TABLE $tableName (\n                 |id INT,\n                 |value STRING\n                 |) USING DELTA\n                 |TBLPROPERTIES('delta.constraints.nonbool' = 'id + 1')\n                 |\"\"\".stripMargin)\n          },\n          \"DELTA_NON_BOOLEAN_CHECK_CONSTRAINT\",\n          parameters = Map(\n            \"name\" -> \"nonbool\",\n            \"expr\" -> \"(id + 1)\"\n          )\n        )\n      }\n    }\n  }\n\n  test(\"CREATE TABLE with valid check constraint succeeds\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n        ValidateCheckConstraintsMode.ASSERT.toString) {\n      val tableName = \"test_create_valid_constraint\"\n      withTable(tableName) {\n        sql(\n          s\"\"\"\n             |CREATE TABLE $tableName (\n             |id INT,\n             |value STRING\n             |) USING DELTA\n             |TBLPROPERTIES('delta.constraints.positive_id' = 'id > 0')\n             |\"\"\".stripMargin)\n      }\n    }\n  }\n\n  test(\"constraint must be boolean\") {\n    withTestTable { table =>\n      checkError(\n        exception = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $table ADD CONSTRAINT integerVal CHECK (3)\")\n        },\n        \"DELTA_NON_BOOLEAN_CHECK_CONSTRAINT\",\n        parameters = Map(\n          \"name\" -> \"integerVal\",\n          \"expr\" -> \"3\"\n        )\n      )\n    }\n  }\n\n  test(\"can't add constraint referencing non-existent columns\") {\n    withTestTable { table =>\n      checkError(\n        intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $table ADD CONSTRAINT c CHECK (does_not_exist)\")\n        },\n        \"UNRESOLVED_COLUMN.WITH_SUGGESTION\",\n        parameters = Map(\n          \"objectName\" -> \"`does_not_exist`\",\n          \"proposal\" -> \"`text`, `num`\"\n        )\n      )\n    }\n  }\n\n  test(\"can't add constraint with duplicate name\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT trivial CHECK (true)\")\n      val e = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $table ADD CONSTRAINT trivial CHECK (true)\")\n      }\n      errorContains(e.getMessage,\n        s\"Constraint 'trivial' already exists as a CHECK constraint. Please delete the \" +\n          s\"old constraint first.\\nOld constraint:\\ntrue\")\n    }\n  }\n\n  test(\"can't add constraint with names that are reserved for internal usage\") {\n    withTestTable { table =>\n      val reservedName = CharVarcharConstraint.INVARIANT_NAME\n      val e = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $table ADD CONSTRAINT $reservedName CHECK (true)\")\n      }\n      errorContains(e.getMessage, s\"Cannot use '$reservedName' as the name of a CHECK constraint\")\n    }\n  }\n\n  test(\"duplicate constraint check is case insensitive\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT trivial CHECK (true)\")\n      val e = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $table ADD CONSTRAINT TRIVIAL CHECK (true)\")\n      }\n      errorContains(e.getMessage,\n        s\"Constraint 'TRIVIAL' already exists as a CHECK constraint. Please delete the \" +\n          s\"old constraint first.\\nOld constraint:\\ntrue\")\n    }\n  }\n\n  testQuietly(\"can't add already violated constraint\") {\n    withTestTable { table =>\n      val e = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $table ADD CONSTRAINT lessThan5 CHECK (num < 5 and text < 'd')\")\n      }\n      errorContains(e.getMessage,\n        s\"violate the new CHECK constraint (num < 5 and text < 'd')\")\n    }\n  }\n\n  testQuietly(\"can't add row violating constraint\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT lessThan10 CHECK (num < 10 and text < 'g')\")\n      sql(s\"INSERT INTO $table VALUES (5, 'a')\")\n      val e = intercept[InvariantViolationException] {\n        sql(s\"INSERT INTO $table VALUES (11, 'a')\")\n      }\n      errorContains(e.getMessage,\n        s\"CHECK constraint lessthan10 ((num < 10) AND (text < 'g')) violated\")\n    }\n  }\n\n  test(\"drop constraint that doesn't exist throws an exception\") {\n    withTestTable { table =>\n      intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $table DROP CONSTRAINT myConstraint\")\n      }\n    }\n\n    withSQLConf((DeltaSQLConf.DELTA_ASSUMES_DROP_CONSTRAINT_IF_EXISTS.key, \"false\")) {\n      withTestTable { table =>\n        val e = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE $table DROP CONSTRAINT myConstraint\")\n        }\n        assert(e.getErrorClass == \"DELTA_CONSTRAINT_DOES_NOT_EXIST\")\n        errorContains(e.getMessage,\n          \"nonexistent constraint myconstraint from table `default`.`checkconstraintstest`\")\n        errorContains(e.getMessage,\n          \"databricks.spark.delta.constraints.assumesDropIfExists.enabled to true\")\n      }\n    }\n  }\n\n  test(\"can drop constraint that doesn't exist with IF EXISTS\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table DROP CONSTRAINT IF EXISTS myConstraint\")\n    }\n\n    withSQLConf((DeltaSQLConf.DELTA_ASSUMES_DROP_CONSTRAINT_IF_EXISTS.key, \"true\")) {\n      withTestTable { table =>\n        sql(s\"ALTER TABLE $table DROP CONSTRAINT myConstraint\")\n      }\n    }\n  }\n\n\n  test(\"drop constraint is case insensitive\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT myConstraint CHECK (true)\")\n      sql(s\"ALTER TABLE $table DROP CONSTRAINT MYCONSTRAINT\")\n    }\n  }\n\n  testQuietly(\"add row violating constraint after it's dropped\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT lessThan10 CHECK (num < 10 and text < 'g')\")\n      intercept[InvariantViolationException] {\n        sql(s\"INSERT INTO $table VALUES (11, 'a')\")\n      }\n      sql(s\"ALTER TABLE $table DROP CONSTRAINT lessThan10\")\n      sql(s\"INSERT INTO $table VALUES (11, 'a')\")\n      checkAnswer(sql(s\"SELECT num FROM $table\"), Seq(1, 2, 3, 4, 5, 6, 11).toDF())\n    }\n  }\n\n  test(\"see constraints in table properties\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT toBeDropped CHECK (text < 'n')\")\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT trivial CHECK (true)\")\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT numLimit CHECK (num < 10)\")\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT combo CHECK (concat(num, text) != '9i')\")\n      sql(s\"ALTER TABLE $table DROP CONSTRAINT toBeDropped\")\n      val props =\n        sql(s\"DESCRIBE DETAIL $table\").selectExpr(\"properties\").head().getMap[String, String](0)\n      // We've round-tripped through the parser, so the text of the constraints stored won't exactly\n      // match what was originally given.\n      assert(props == Map(\n        \"delta.constraints.trivial\" -> \"true\",\n        \"delta.constraints.numlimit\" -> \"num < 10\",\n        \"delta.constraints.combo\" -> \"concat ( num , text ) != '9i'\"\n      ))\n    }\n  }\n\n  test(\"delta history for constraints\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT lessThan10 CHECK (num < 10)\")\n      checkAnswer(\n        sql(s\"DESCRIBE HISTORY $table\")\n          .where(\"operation = 'ADD CONSTRAINT'\")\n          .selectExpr(\"operation\", \"operationParameters\"),\n        Seq((\"ADD CONSTRAINT\", Map(\"name\" -> \"lessThan10\", \"expr\" -> \"num < 10\"))).toDF())\n\n      sql(s\"ALTER TABLE $table DROP CONSTRAINT IF EXISTS lessThan10\")\n      checkAnswer(\n        sql(s\"DESCRIBE HISTORY $table\")\n          .where(\"operation = 'DROP CONSTRAINT'\")\n          .selectExpr(\"operation\", \"operationParameters\"),\n        Seq((\n          \"DROP CONSTRAINT\",\n          Map(\"name\" -> \"lessThan10\", \"expr\" -> \"num < 10\", \"existed\" -> \"true\")\n        )).toDF())\n      sql(s\"ALTER TABLE $table DROP CONSTRAINT IF EXISTS lessThan10\")\n        checkAnswer(\n          sql(s\"DESCRIBE HISTORY $table\")\n            .where(\"operation = 'DROP CONSTRAINT'\")\n            .selectExpr(\"operation\", \"operationParameters\"),\n          Seq(\n            (\"DROP CONSTRAINT\",\n              Map(\"name\" -> \"lessThan10\", \"expr\" -> \"num < 10\", \"existed\" -> \"true\")),\n            (\"DROP CONSTRAINT\",\n              Map(\"name\" -> \"lessThan10\", \"existed\" -> \"false\"))\n          ).toDF())\n    }\n  }\n\n  testQuietly(\"constraint on builtin methods\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT textSize CHECK (LENGTH(text) < 10)\")\n      sql(s\"INSERT INTO $table VALUES (11, 'abcdefg')\")\n      val e = intercept[InvariantViolationException] {\n        sql(s\"INSERT INTO $table VALUES (12, 'abcdefghijklmnop')\")\n      }\n      errorContains(e.getMessage, \"constraint textsize (LENGTH(text) < 10) violated by row\")\n    }\n  }\n\n  testQuietly(\"constraint with implicit casts\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT maxWithImplicitCast CHECK (num < '10')\")\n      val e = intercept[InvariantViolationException] {\n        sql(s\"INSERT INTO $table VALUES (11, 'data')\")\n      }\n      errorContains(e.getMessage, \"constraint maxwithimplicitcast (num < '10') violated by row\")\n    }\n  }\n\n  testQuietly(\"constraint with nested parentheses\") {\n    withTestTable { table =>\n      sql(s\"ALTER TABLE $table ADD CONSTRAINT maxWithParens \" +\n        s\"CHECK (( (num < '10') AND ((LENGTH(text)) < 100) ))\")\n      val e = intercept[InvariantViolationException] {\n        sql(s\"INSERT INTO $table VALUES (11, 'data')\")\n      }\n      errorContains(e.getMessage,\n        \"constraint maxwithparens ((num < '10') AND (LENGTH(text) < 100)) violated by row\")\n    }\n  }\n\n  for (expression <- Seq(\"year(current_date())\", \"unix_timestamp()\"))\n  testQuietly(s\"constraint with analyzer-evaluated expressions. Expression: $expression\") {\n    // Explicitly block constraint validation since both functions are nondeterministic.\n    val disabled = ValidateCheckConstraintsMode.OFF.toString\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key -> disabled) {\n      withTestTable { table =>\n        // We use current_timestamp()/current_date() as the most convenient\n        // analyzer-evaluated expressions - of course in a realistic use case\n        // it'd probably not be right to add a constraint on a\n        // nondeterministic expression.\n        sql(s\"ALTER TABLE $table ADD CONSTRAINT maxWithAnalyzerEval \" +\n          s\"CHECK (num < $expression)\")\n        val e = intercept[InvariantViolationException] {\n          sql(s\"INSERT INTO $table VALUES (${Int.MaxValue}, 'data')\")\n        }\n        errorContains(e.getMessage,\n          s\"maxwithanalyzereval (num < $expression) violated by row\")\n      }\n    }\n  }\n\n  testQuietly(\"constraints with nulls\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"3\") {\n      withTable(\"checkConstraintsTest\") {\n        val rows = Range(0, 10).map { i =>\n          Row(\n            i,\n            null,\n            Row(\"constantWithinStruct\", Map(i -> i), Array(i, null, i + 2)))\n        }\n\n        val schema = new StructType(Array(\n          StructField(\"id\", IntegerType),\n          StructField(\"text\", StringType),\n          StructField(\"nested\", new StructType(Array(\n            StructField(\"constant\", StringType),\n            StructField(\"m\", MapType(IntegerType, IntegerType, valueContainsNull = true)),\n            StructField(\"arr\", ArrayType(IntegerType, containsNull = true)))))))\n        spark.createDataFrame(rows.toList.asJava, schema)\n          .write.format(\"delta\").saveAsTable(\"checkConstraintsTest\")\n\n        // Constraints checking for a null value should work.\n        sql(\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT textNull CHECK (text IS NULL)\")\n        sql(\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT arr1Null \" +\n          \"CHECK (nested.arr[1] IS NULL)\")\n\n        // Constraints incompatible with a null value will of course fail, but they should fail with\n        // the same clear error as normal.\n        var e: Exception = intercept[AnalysisException] {\n          sql(\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT arrLessThan5 \" +\n            \"CHECK (nested.arr[1] < 5)\")\n        }\n        errorContains(e.getMessage,\n          s\"10 rows in default.checkconstraintstest violate the new CHECK constraint \" +\n            s\"(nested . arr [ 1 ] < 5)\")\n\n        // Adding a null value into a constraint should fail similarly, even if it's null\n        // because a parent field is null.\n        sql(\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT arr0 \" +\n          \"CHECK (nested.arr[0] < 100)\")\n        val newRows = Seq(\n          Row(10, null, Row(\"c\", Map(10 -> null), Array(null, null, 12))),\n          Row(11, null, Row(\"c\", Map(11 -> null), null)),\n          Row(12, null, null))\n        newRows.foreach { r =>\n          e = intercept[InvariantViolationException] {\n            spark.createDataFrame(List(r).asJava, schema)\n              .write.format(\"delta\").mode(\"append\").saveAsTable(\"checkConstraintsTest\")\n          }\n          errorContains(e.getMessage,\n            \"CHECK constraint arr0 (nested.arr[0] < 100) violated by row\")\n        }\n\n        // On the other hand, existing constraints like arr1Null which do allow null values should\n        // permit new rows even if the value's parent is null.\n        sql(\"ALTER TABLE checkConstraintsTest DROP CONSTRAINT arr0\")\n        newRows.foreach { r =>\n          spark.createDataFrame(List(r).asJava, schema)\n            .write.format(\"delta\").mode(\"append\").saveAsTable(\"checkConstraintsTest\")\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").table(\"checkConstraintsTest\").select(\"id\"),\n          (0 to 12).toDF(\"id\"))\n      }\n    }\n  }\n\n  testQuietly(\"complex constraints\") {\n    withSQLConf(\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_READER_VERSION.key -> \"1\",\n      DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key -> \"3\") {\n      withTable(\"checkConstraintsTest\") {\n        val rows = Range(0, 10).map { i =>\n          Row(\n            i,\n            ('a' + i).toString,\n            Row(\"constantWithinStruct\", Map(i -> i), Array(i, i + 1, i + 2)))\n        }\n        val schema = new StructType(Array(\n          StructField(\"id\", IntegerType),\n          StructField(\"text\", StringType),\n          StructField(\"nested\", new StructType(Array(\n            StructField(\"constant\", StringType),\n            StructField(\"m\", MapType(IntegerType, IntegerType, valueContainsNull = false)),\n            StructField(\"arr\", ArrayType(IntegerType, containsNull = false)))))))\n        spark.createDataFrame(rows.toList.asJava, schema)\n          .write.format(\"delta\").saveAsTable(\"checkConstraintsTest\")\n        sql(\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT arrLen CHECK (SIZE(nested.arr) = 3)\")\n        sql(\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT mapIntegrity \" +\n          \"CHECK (nested.m[id] = id)\")\n        val e = intercept[AnalysisException] {\n          sql(s\"ALTER TABLE checkConstraintsTest ADD CONSTRAINT violated \" +\n            s\"CHECK (nested.arr[0] < id)\")\n        }\n        errorContains(e.getMessage,\n          s\"violate the new CHECK constraint (nested . arr [ 0 ] < id)\")\n      }\n    }\n  }\n\n\n  // TODO: https://github.com/delta-io/delta/issues/831\n  test(\"SET NOT NULL constraint fails\") {\n    withTable(\"my_table\") {\n      sql(\"CREATE TABLE my_table (id INT) USING DELTA;\")\n      sql(\"INSERT INTO my_table VALUES (1);\")\n      val e = intercept[AnalysisException] {\n        sql(\"ALTER TABLE my_table CHANGE COLUMN id SET NOT NULL;\")\n      }.getMessage()\n      assert(e.contains(\"Cannot change nullable column to non-nullable\"))\n    }\n  }\n\n  testQuietly(\"ending semi-colons no longer makes ADD, DROP constraint commands fail\") {\n    withTable(\"my_table\") {\n      sql(\"CREATE TABLE my_table (birthday DATE) USING DELTA;\")\n      sql(\"INSERT INTO my_table VALUES ('2021-11-11');\")\n\n      sql(\"ALTER TABLE my_table ADD CONSTRAINT aaa CHECK (birthday > '1900-01-01')\")\n      sql(\"ALTER TABLE my_table ADD CONSTRAINT bbb CHECK (birthday > '1900-02-02')\")\n      sql(\"ALTER TABLE my_table ADD CONSTRAINT ccc CHECK (birthday > '1900-03-03');\") // semi-colon\n\n      sql(\"ALTER TABLE my_table DROP CONSTRAINT aaa\")\n      sql(\"ALTER TABLE my_table DROP CONSTRAINT bbb;\") // semi-colon\n    }\n  }\n\n  test(\"validate check constraints on table with char/varchar columns\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n        ValidateCheckConstraintsMode.ASSERT.toString,\n      SQLConf.READ_SIDE_CHAR_PADDING.key -> \"true\") {\n      withTable(\"charVarcharConstraintTest\") {\n        sql(\n          \"\"\"CREATE TABLE charVarcharConstraintTest (\n            |  id INT,\n            |  name VARCHAR(50),\n            |  code CHAR(10)\n            |) USING DELTA\n            |TBLPROPERTIES('delta.constraints.positive_id' = 'id > 0')\n            |\"\"\".stripMargin)\n        sql(\"INSERT INTO charVarcharConstraintTest VALUES (1, 'test', 'ABC')\")\n        checkAnswer(\n          sql(\"SELECT id, name, code FROM charVarcharConstraintTest\"),\n          Seq(Row(1, \"test\", \"ABC       \")))\n      }\n    }\n  }\n\n  test(\"constraint induced by varchar\") {\n    withTable(\"table\") {\n      sql(\"CREATE TABLE table (id INT, value VARCHAR(12)) USING DELTA\")\n      sql(\"INSERT INTO table VALUES (1, 'short string')\")\n      val exception = intercept[DeltaInvariantViolationException] {\n        sql(\"INSERT INTO table VALUES (2, 'a very long string')\")\n      }\n      checkError(\n        exception,\n        \"DELTA_EXCEED_CHAR_VARCHAR_LIMIT\",\n        parameters = Map(\n          \"value\" -> \"a very long string\",\n          \"expr\" -> \"((value IS NULL) OR (length(value) <= 12))\"\n        )\n      )\n    }\n  }\n\n  test(\"drop table feature\") {\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> false.toString) {\n      withTable(\"table\") {\n        sql(\"CREATE TABLE table (a INT, b INT) USING DELTA \" +\n          \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n        sql(\"ALTER TABLE table ADD CONSTRAINT c1 CHECK (a > 0)\")\n        sql(\"ALTER TABLE table ADD CONSTRAINT c2 CHECK (b > 0)\")\n\n        val error1 = intercept[AnalysisException] {\n          sql(\"ALTER TABLE table DROP FEATURE checkConstraints\")\n        }\n        checkError(\n          error1,\n          \"DELTA_CANNOT_DROP_CHECK_CONSTRAINT_FEATURE\",\n          parameters = Map(\"constraints\" -> \"`c1`, `c2`\")\n        )\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"table\"))\n        val featureNames1 =\n          deltaLog.update().protocol.implicitlyAndExplicitlySupportedFeatures.map(_.name)\n        assert(featureNames1.contains(\"checkConstraints\"))\n\n        sql(\"ALTER TABLE table DROP CONSTRAINT c1\")\n        val error2 = intercept[AnalysisException] {\n          sql(\"ALTER TABLE table DROP FEATURE checkConstraints\")\n        }\n        checkError(\n          error2,\n          \"DELTA_CANNOT_DROP_CHECK_CONSTRAINT_FEATURE\",\n          parameters = Map(\"constraints\" -> \"`c2`\")\n        )\n        val featureNames2 =\n          deltaLog.update().protocol.implicitlyAndExplicitlySupportedFeatures.map(_.name)\n        assert(featureNames2.contains(\"checkConstraints\"))\n\n        sql(\"ALTER TABLE table DROP CONSTRAINT c2\")\n        sql(\"ALTER TABLE table DROP FEATURE checkConstraints\")\n        val featureNames3 =\n          deltaLog.update().protocol.implicitlyAndExplicitlySupportedFeatures.map(_.name)\n        assert(!featureNames3.contains(\"checkConstraints\"))\n      }\n    }\n  }\n\n  for (expression <- Seq(\"startsWith\", \"endsWith\", \"contains\")) {\n    test(s\"Creating constraints with expressions in the allowList should work for: $expression\") {\n      withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n        ValidateCheckConstraintsMode.ASSERT.toString) {\n        val testTable = \"tbl\"\n        withTable(testTable) {\n          sql(s\"CREATE TABLE $testTable (id STRING, value BOOLEAN) USING DELTA \" +\n            \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n          sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c1 CHECK (value == $expression(id, 'A'))\")\n          sql(s\"INSERT INTO $testTable VALUES ('ABA', true), ('DEF', false)\")\n        }\n      }\n    }\n  }\n\n  test(\"check constraints with LIKE ANY/ALL and NOT LIKE ANY/ALL expressions\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n      ValidateCheckConstraintsMode.ASSERT.toString) {\n      val testTable = \"like_any_all_test\"\n      withTable(testTable) {\n        sql(s\"CREATE TABLE $testTable (id INT, name STRING, code STRING) USING DELTA \" +\n          \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_like_any \" +\n          \"CHECK (name LIKE ANY ('%test%', '%prod%'))\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_not_like_any \" +\n          \"CHECK (name NOT LIKE ANY ('%forbidden%', '%blocked%'))\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_like_all \" +\n          \"CHECK (code LIKE ALL ('%A%', '%B%'))\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_not_like_all \" +\n          \"CHECK (code NOT LIKE ALL ('%X%', '%Y%'))\")\n        sql(s\"INSERT INTO $testTable VALUES (1, 'test_data', 'AB')\")\n        checkAnswer(\n          sql(s\"SELECT * FROM $testTable\"),\n          Seq(Row(1, \"test_data\", \"AB\")))\n      }\n    }\n  }\n\n  test(\"check constraints with array_size and array_compact expressions\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n      ValidateCheckConstraintsMode.ASSERT.toString) {\n      val testTable = \"array_funcs_test\"\n      withTable(testTable) {\n        sql(s\"CREATE TABLE $testTable (id INT, tags ARRAY<STRING>) USING DELTA \" +\n          \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_array_size \" +\n          \"CHECK (array_size(tags) > 0)\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_array_compact \" +\n          \"CHECK (array_size(array_compact(tags)) > 0)\")\n        sql(s\"INSERT INTO $testTable VALUES (1, array('a', 'b'))\")\n        checkAnswer(\n          sql(s\"SELECT id FROM $testTable\"),\n          Seq(Row(1)))\n      }\n    }\n  }\n\n  test(\"check constraints with array_append, array_prepend, array_insert expressions\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n      ValidateCheckConstraintsMode.ASSERT.toString) {\n      val testTable = \"array_modify_funcs_test\"\n      withTable(testTable) {\n        sql(s\"CREATE TABLE $testTable (id INT, tags ARRAY<STRING>) USING DELTA \" +\n          \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_array_append \" +\n          \"CHECK (array_size(array_append(tags, 'x')) > 1)\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_array_prepend \" +\n          \"CHECK (array_size(array_prepend(tags, 'y')) > 1)\")\n        sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c_array_insert \" +\n          \"CHECK (array_size(array_insert(tags, 1, 'z')) > 1)\")\n        sql(s\"INSERT INTO $testTable VALUES (1, array('a', 'b'))\")\n        checkAnswer(\n          sql(s\"SELECT id FROM $testTable\"),\n          Seq(Row(1)))\n      }\n    }\n  }\n\n  test(\"Creating constraints with expressions not in the allowList should throw an error\") {\n    withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key ->\n      ValidateCheckConstraintsMode.ASSERT.toString) {\n      val testTable = \"tbl\"\n      withTable(testTable) {\n        sql(s\"CREATE TABLE $testTable (id INT) USING DELTA \" +\n          \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n        checkError(\n          exception = intercept[AnalysisException] {\n            sql(s\"ALTER TABLE $testTable ADD CONSTRAINT c1 \" +\n              s\"CHECK (id > (SELECT max(id) FROM $testTable))\")\n          },\n          \"DELTA_UNSUPPORTED_EXPRESSION_CHECK_CONSTRAINT\",\n          parameters = Map(\"expression\" -> \"scalarsubquery()\")\n        )\n      }\n    }\n  }\n\n  for (isEnabled <- Seq(ValidateCheckConstraintsMode.OFF, ValidateCheckConstraintsMode.ASSERT)) {\n    test(s\"Reject CHECK constraints with external UDF calls when validation is ${isEnabled}\") {\n      withSQLConf(DeltaSQLConf.VALIDATE_CHECK_CONSTRAINTS.key -> isEnabled.toString) {\n        val testTable = \"check_external_udf_test\"\n        withTable(testTable) {\n          withUserDefinedFunction(\"external_udf\" -> true) {\n            sql(s\"CREATE TABLE $testTable (id INT, value INT) USING DELTA \" +\n              \"TBLPROPERTIES ('delta.feature.checkConstraints' = 'supported')\")\n\n            spark.udf.register(\"external_udf\", (x: Int) => x > 0)\n\n            val sqlText = s\"ALTER TABLE $testTable ADD CONSTRAINT check_external_udf \" +\n              \"CHECK (external_udf(value))\"\n\n            if (isEnabled == ValidateCheckConstraintsMode.ASSERT) {\n              checkError(\n                exception = intercept[AnalysisException] {\n                  sql(sqlText)\n                },\n                \"DELTA_UDF_IN_CHECK_CONSTRAINT\",\n                parameters = Map(\"expr\" -> \"external_udf(knownnotnull(value))\")\n              )\n            } else {\n              sql(sqlText)\n            }\n          }\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/schema/InvariantEnforcementSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.io.File\nimport java.sql.Date\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.{CheckConstraintsTableFeature, DeltaLog, DeltaOperations}\nimport org.apache.spark.sql.delta.actions.{Metadata, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.constraints.{Constraint, Constraints, Invariants}\nimport org.apache.spark.sql.delta.constraints.Constraints.NotNull\nimport org.apache.spark.sql.delta.constraints.Invariants.PersistedExpression\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\n\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.streaming.StreamingQueryException\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nclass InvariantEnforcementSuite extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils {\n\n\n  import testImplicits._\n\n  private def tableWithSchema(schema: StructType)(f: String => Unit): Unit = {\n    withTempDir { tempDir =>\n      val deltaLog = DeltaLog.forTable(spark, tempDir)\n      val txn = deltaLog.startTransaction()\n      txn.commit(Metadata(schemaString = schema.json) :: Nil, DeltaOperations.ManualUpdate)\n      spark.read.format(\"delta\")\n        .load(tempDir.getAbsolutePath)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .save(tempDir.getAbsolutePath)\n      f(tempDir.getAbsolutePath)\n    }\n  }\n\n  private def testBatchWriteRejection(\n      invariant: Constraint,\n      schema: StructType,\n      df: Dataset[_],\n      expectedErrors: String*): Unit = {\n    tableWithSchema(schema) { path =>\n      val e = intercept[InvariantViolationException] {\n        df.write.mode(\"append\").format(\"delta\").save(path)\n      }\n      checkConstraintException(e, (invariant.name +: expectedErrors): _*)\n    }\n  }\n\n  private def checkConstraintException(\n      e: InvariantViolationException, expectedErrors: String*): Unit = {\n    val error = e.getMessage\n    val allExpected = expectedErrors\n    allExpected.foreach { expected =>\n      assert(error.contains(expected), s\"$error didn't contain $expected\")\n    }\n  }\n\n  private def testStreamingWriteRejection[T: Encoder](\n      invariant: Constraint,\n      schema: StructType,\n      toDF: MemoryStream[T] => DataFrame,\n      data: Seq[T],\n      expectedErrors: String*): Unit = {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val txn = deltaLog.startTransaction()\n      txn.commit(Metadata(schemaString = schema.json) :: Nil, DeltaOperations.ManualUpdate)\n      val memStream = MemoryStream[T]\n      val stream = toDF(memStream).writeStream\n        .outputMode(\"append\")\n        .format(\"delta\")\n        .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n        .start(dir.getAbsolutePath)\n      try {\n        val e = intercept[StreamingQueryException] {\n          memStream.addData(data)\n          stream.processAllAvailable()\n        }\n        // Produce a good error if the cause isn't the right type - just an assert makes it hard to\n        // see what the wrong exception was.\n        intercept[InvariantViolationException] { throw e.getCause }\n\n        checkConstraintException(\n          e.getCause.asInstanceOf[InvariantViolationException],\n          (invariant.name +: expectedErrors): _*)\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  test(\"reject non-nullable top level column\") {\n    val schema = new StructType()\n      .add(\"key\", StringType, nullable = false)\n      .add(\"value\", IntegerType)\n    testBatchWriteRejection(\n      NotNull(Seq(\"key\")),\n      schema,\n      Seq[(String, Int)]((\"a\", 1), (null, 2)).toDF(\"key\", \"value\"),\n      \"key\"\n    )\n    testStreamingWriteRejection[(String, Int)](\n      NotNull(Seq(\"key\")),\n      schema,\n      _.toDF().toDF(\"key\", \"value\"),\n      Seq[(String, Int)]((\"a\", 1), (null, 2)),\n      \"key\"\n    )\n  }\n\n  test(\"reject non-nullable top level column - column doesn't exist\") {\n    val schema = new StructType()\n      .add(\"key\", StringType, nullable = false)\n      .add(\"value\", IntegerType)\n    testBatchWriteRejection(\n      NotNull(Seq(\"key\")),\n      schema,\n      Seq[Int](1, 2).toDF(\"value\"),\n      \"key\"\n    )\n    testStreamingWriteRejection[Int](\n      NotNull(Seq(\"key\")),\n      schema,\n      _.toDF().toDF(\"value\"),\n      Seq[Int](1, 2),\n      \"key\"\n    )\n  }\n\n  testQuietly(\"write empty DataFrame - zero rows\") {\n    val schema = new StructType()\n      .add(\"key\", StringType, nullable = false)\n      .add(\"value\", IntegerType)\n    tableWithSchema(schema) { path =>\n      spark.createDataFrame(Seq.empty[Row].asJava, schema.asNullable).write\n        .mode(\"append\").format(\"delta\").save(path)\n    }\n  }\n\n  test(\"write empty DataFrame - zero columns\") {\n    val schema = new StructType()\n      .add(\"key\", StringType, nullable = false)\n      .add(\"value\", IntegerType)\n    testBatchWriteRejection(\n      NotNull(Seq(\"key\")),\n      schema,\n      Seq[Int](1, 2).toDF(\"value\").drop(\"value\"),\n      \"key\"\n    )\n    testStreamingWriteRejection[Int](\n      NotNull(Seq(\"key\")),\n      schema,\n      _.toDF().toDF(\"value\").drop(\"value\"),\n      Seq[Int](1, 2),\n      \"key\"\n    )\n  }\n\n  testQuietly(\"reject non-nullable nested column\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"key\", StringType, nullable = false)\n        .add(\"value\", IntegerType))\n    testBatchWriteRejection(\n      NotNull(Seq(\"key\")),\n      schema,\n      spark.createDataFrame(Seq(Row(Row(\"a\", 1)), Row(Row(null, 2))).asJava, schema.asNullable),\n      \"top.key\"\n    )\n    testBatchWriteRejection(\n      NotNull(Seq(\"key\")),\n      schema,\n      spark.createDataFrame(Seq(Row(Row(\"a\", 1)), Row(null)).asJava, schema.asNullable),\n      \"top.key\"\n    )\n  }\n\n  testQuietly(\"reject non-nullable array column\") {\n    val schema = new StructType()\n      .add(\"top\", ArrayType(ArrayType(new StructType()\n        .add(\"key\", StringType)\n        .add(\"value\", IntegerType))), nullable = false)\n    testBatchWriteRejection(\n      NotNull(Seq(\"top\", \"value\")),\n      schema,\n      spark.createDataFrame(Seq(Row(Seq(Seq(Row(\"a\", 1)))), Row(null)).asJava, schema.asNullable),\n      \"top\"\n    )\n  }\n\n  test(\"reject expression invariant on top level column\") {\n    val expr = \"value < 3\"\n    val rule = Constraints.Check(\"\", spark.sessionState.sqlParser.parseExpression(expr))\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", IntegerType, nullable = true, metadata)\n    testBatchWriteRejection(\n      rule,\n      schema,\n      Seq[(String, Int)]((\"a\", 1), (null, 5)).toDF(\"key\", \"value\"),\n      \"value\", \"5\"\n    )\n    testStreamingWriteRejection[(String, Int)](\n      rule,\n      schema,\n      _.toDF().toDF(\"key\", \"value\"),\n      Seq[(String, Int)]((\"a\", 1), (null, 5)),\n      \"value\"\n    )\n  }\n\n  testQuietly(\"reject expression invariant on nested column\") {\n    val expr = \"top.value < 3\"\n    val rule = Constraints.Check(\"\", spark.sessionState.sqlParser.parseExpression(expr))\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"key\", StringType)\n        .add(\"value\", IntegerType, nullable = true, metadata))\n    testBatchWriteRejection(\n      rule,\n      schema,\n      spark.createDataFrame(Seq(Row(Row(\"a\", 1)), Row(Row(null, 5))).asJava, schema.asNullable),\n      \"top.value\", \"5\"\n    )\n  }\n\n  testQuietly(\"reject write on top level expression invariant when field is null\") {\n    val expr = \"value < 3\"\n    val rule = Constraints.Check(\"\", spark.sessionState.sqlParser.parseExpression(expr))\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", IntegerType, nullable = true, metadata)\n    testBatchWriteRejection(\n      rule,\n      schema,\n      Seq[String](\"a\", \"b\").toDF(\"key\"),\n      \" - value : null\"\n    )\n    testBatchWriteRejection(\n      rule,\n      schema,\n      Seq[(String, Integer)]((\"a\", 1), (\"b\", null)).toDF(\"key\", \"value\"),\n      \" - value : null\"\n    )\n  }\n\n  testQuietly(\"reject write on nested expression invariant when field is null\") {\n    val expr = \"top.value < 3\"\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val rule = Constraints.Check(\"\", spark.sessionState.sqlParser.parseExpression(expr))\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"key\", StringType)\n        .add(\"value\", IntegerType, nullable = true, metadata))\n    testBatchWriteRejection(\n      rule,\n      schema,\n      spark.createDataFrame(Seq(Row(Row(\"a\", 1)), Row(Row(\"b\", null))).asJava, schema.asNullable),\n      \" - top.value : null\"\n    )\n    val schema2 = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"key\", StringType))\n    testBatchWriteRejection(\n      rule,\n      schema,\n      spark.createDataFrame(Seq(Row(Row(\"a\")), Row(Row(\"b\"))).asJava, schema2.asNullable),\n      \" - top.value : null\"\n    )\n  }\n\n  testQuietly(\"is null on top level expression invariant when field is null\") {\n    val expr = \"value is null or value < 3\"\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", IntegerType, nullable = true, metadata)\n    tableWithSchema(schema) { path =>\n      Seq[String](\"a\", \"b\").toDF(\"key\").write\n        .mode(\"append\").format(\"delta\").save(path)\n      Seq[(String, Integer)]((\"a\", 1), (\"b\", null)).toDF(\"key\", \"value\").write\n        .mode(\"append\").format(\"delta\").save(path)\n    }\n  }\n\n  testQuietly(\"is null on nested expression invariant when field is null\") {\n    val expr = \"top.value is null or top.value < 3\"\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"key\", StringType)\n        .add(\"value\", IntegerType, nullable = true, metadata))\n    val schema2 = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"key\", StringType))\n    tableWithSchema(schema) { path =>\n      spark.createDataFrame(Seq(Row(Row(\"a\", 1)), Row(Row(\"b\", null))).asJava, schema.asNullable)\n        .write.mode(\"append\").format(\"delta\").save(path)\n      spark.createDataFrame(Seq(Row(Row(\"a\")), Row(Row(\"b\"))).asJava, schema2.asNullable)\n        .write.mode(\"append\").format(\"delta\").save(path)\n    }\n  }\n\n  testQuietly(\"complex expressions - AND\") {\n    val expr = \"value < 3 AND value > 0\"\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"key\", StringType)\n      .add(\"value\", IntegerType, nullable = true, metadata)\n    tableWithSchema(schema) { path =>\n      Seq(1, 2).toDF(\"value\").write.mode(\"append\").format(\"delta\").save(path)\n      intercept[InvariantViolationException] {\n        Seq(1, 4).toDF(\"value\").write.mode(\"append\").format(\"delta\").save(path)\n      }\n      intercept[InvariantViolationException] {\n        Seq(-1, 2).toDF(\"value\").write.mode(\"append\").format(\"delta\").save(path)\n      }\n    }\n  }\n\n  testQuietly(\"complex expressions - IN SET\") {\n    val expr = \"key in ('a', 'b', 'c')\"\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(expr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"key\", StringType, nullable = true, metadata)\n      .add(\"value\", IntegerType)\n    tableWithSchema(schema) { tempDir =>\n      Seq(\"a\", \"b\").toDF(\"key\").write.mode(\"append\").format(\"delta\").save(tempDir)\n      intercept[InvariantViolationException] {\n        Seq(\"a\", \"d\").toDF(\"key\").write.mode(\"append\").format(\"delta\").save(tempDir)\n      }\n      intercept[InvariantViolationException] {\n        Seq(\"e\").toDF(\"key\").write.mode(\"append\").format(\"delta\").save(tempDir)\n      }\n    }\n  }\n\n  test(\"CHECK constraint can't be created through SET TBLPROPERTIES\") {\n    withTable(\"noCheckConstraints\") {\n      spark.range(10).write.format(\"delta\").saveAsTable(\"noCheckConstraints\")\n      val ex = intercept[AnalysisException] {\n        spark.sql(\n          \"ALTER TABLE noCheckConstraints SET TBLPROPERTIES ('delta.constraints.mychk' = '1')\")\n      }\n      assert(ex.getMessage.contains(\"ALTER TABLE ADD CONSTRAINT\"))\n    }\n  }\n\n  for (writerVersion <- Seq(2, TableFeatureProtocolUtils.TABLE_FEATURES_MIN_WRITER_VERSION))\n  testQuietly(\"CHECK constraint is enforced if somehow created (writerVersion = \" +\n    s\"$writerVersion)\") {\n    withSQLConf((DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key, writerVersion.toString)) {\n      withTable(\"constraint\") {\n        spark.range(10).selectExpr(\"id AS valueA\", \"id AS valueB\", \"id AS valueC\")\n          .write.format(\"delta\").saveAsTable(\"constraint\")\n        val table = DeltaTableV2(spark, TableIdentifier(\"constraint\", None))\n        val txn = table.startTransactionWithInitialSnapshot()\n        val newMetadata = txn.metadata.copy(\n          configuration = txn.metadata.configuration +\n            (\"delta.constraints.mychk\" -> \"valueA < valueB\"))\n        txn.commit(Seq(newMetadata), DeltaOperations.ManualUpdate)\n        val protocol = table.deltaLog.update().protocol\n        assert(protocol.implicitlyAndExplicitlySupportedFeatures\n          .contains(CheckConstraintsTableFeature))\n        spark.sql(\"INSERT INTO constraint VALUES (50, 100, null)\")\n        val e = intercept[InvariantViolationException] {\n          spark.sql(\"INSERT INTO constraint VALUES (100, 50, null)\")\n        }\n        checkConstraintException(\n          e,\n          \"CHECK constraint mychk (valueA < valueB) violated by row with values:\",\n          \" - valueA : 100\",\n          \" - valueB : 50\")\n\n        val e2 = intercept[InvariantViolationException] {\n          spark.sql(\"INSERT INTO constraint VALUES (100, null, null)\")\n        }\n        checkConstraintException(\n          e2,\n          \"CHECK constraint mychk (valueA < valueB) violated by row with values:\",\n          \" - valueA : 100\",\n          \" - valueB : null\")\n      }\n    }\n  }\n\n  test(\"table with CHECK constraint accepts other metadata changes\") {\n    withSQLConf((DeltaSQLConf.DELTA_PROTOCOL_DEFAULT_WRITER_VERSION.key, \"3\")) {\n      withTable(\"constraint\") {\n        spark.range(10).selectExpr(\"id AS valueA\", \"id AS valueB\")\n          .write.format(\"delta\").saveAsTable(\"constraint\")\n        val table = DeltaTableV2(spark, TableIdentifier(\"constraint\", None))\n        val txn = table.startTransactionWithInitialSnapshot()\n        val newMetadata = txn.metadata.copy(\n          configuration = txn.metadata.configuration +\n            (\"delta.constraints.mychk\" -> \"valueA < valueB\"))\n        txn.commit(Seq(newMetadata), DeltaOperations.ManualUpdate)\n        spark.sql(\"ALTER TABLE constraint ADD COLUMN valueC INT\")\n      }\n    }\n  }\n\n  def testUnenforcedNestedConstraints(\n      testName: String,\n      schemaString: String,\n      expectedError: String,\n      data: Row): Unit = {\n    testQuietly(testName) {\n      val nullTable = \"nullTbl\"\n      withTable(nullTable) {\n        // Try creating the table with the check enabled first, which should fail, then create it\n        // for real with the check off which should succeed.\n        if (expectedError != null) {\n          val ex = intercept[AnalysisException] {\n            sql(s\"CREATE TABLE $nullTable ($schemaString) USING delta\")\n          }\n          assert(ex.getMessage.contains(expectedError))\n        }\n        withSQLConf((\"spark.databricks.delta.constraints.allowUnenforcedNotNull.enabled\", \"true\")) {\n          sql(s\"CREATE TABLE $nullTable ($schemaString) USING delta\")\n        }\n\n        // Once we've created the table, writes should succeed even if they violate the constraint.\n        spark.createDataFrame(\n          Seq(data).asJava,\n          spark.table(nullTable).schema\n        ).write.mode(\"append\").format(\"delta\").saveAsTable(nullTable)\n\n        if (expectedError != null) {\n          val ex = intercept[AnalysisException] {\n            sql(s\"REPLACE TABLE $nullTable ($schemaString) USING delta\")\n          }\n          assert(ex.getMessage.contains(expectedError))\n        }\n        withSQLConf((\"spark.databricks.delta.constraints.allowUnenforcedNotNull.enabled\", \"true\")) {\n          sql(s\"REPLACE TABLE $nullTable ($schemaString) USING delta\")\n        }\n      }\n    }\n  }\n\n  testUnenforcedNestedConstraints(\n    \"not null within array\",\n    schemaString = \"arr array<struct<name:string,mailbox:string NOT NULL>> NOT NULL\",\n    expectedError = \"The element type of the field arr contains a NOT NULL constraint.\",\n    data = Row(Seq(Row(\"myName\", null))))\n\n  testUnenforcedNestedConstraints(\n    \"not null within map key\",\n    schemaString = \"m map<struct<name:string,mailbox:string NOT NULL>, int> NOT NULL\",\n    expectedError = \"The key type of the field m contains a NOT NULL constraint.\",\n    data = Row(Map(Row(\"myName\", null) -> 1)))\n\n  testUnenforcedNestedConstraints(\n    \"not null within map value\",\n    schemaString = \"m map<int, struct<name:string,mailbox:string NOT NULL>> NOT NULL\",\n    expectedError = \"The value type of the field m contains a NOT NULL constraint.\",\n    data = Row(Map(1 -> Row(\"myName\", null))))\n\n  testUnenforcedNestedConstraints(\n    \"not null within nested array\",\n    schemaString =\n      \"s struct<n:int NOT NULL, arr:array<struct<name:string,mailbox:string NOT NULL>> NOT NULL>\",\n    expectedError = \"The element type of the field s.arr contains a NOT NULL constraint.\",\n    data = Row(Row(1, Seq(Row(\"myName\", null)))))\n\n\n  // Helper function to construct the full test name as \"RuntimeRepalceable: func\"\n  private def testReplaceableExpr(targetFunc: String, testTags: org.scalatest.Tag*)\n    (testFun: => Any)\n    (implicit pos: org.scalactic.source.Position): Unit = {\n    val fulLTestName = s\"RuntimeReplaceable: ${targetFunc}\"\n    // Suppress exceptions output for invariant violations\n    super.test(fulLTestName) {\n      testFun\n    }\n  }\n\n  private def testReplaceable[T: Encoder](\n    exprStr: String,\n    colType: DataType,\n    badValue: T) = {\n    val rule = Constraints.Check(\"\", spark.sessionState.sqlParser.parseExpression(exprStr))\n    val metadata = new MetadataBuilder()\n      .putString(Invariants.INVARIANTS_FIELD, PersistedExpression(exprStr).json)\n      .build()\n    val schema = new StructType()\n      .add(\"value\", colType, nullable = true, metadata)\n    val rows = Seq(Row(badValue))\n    testBatchWriteRejection(\n      rule,\n      schema,\n      spark.createDataFrame(rows.toList.asJava, schema),\n      \"violated by row with values\"\n    )\n    testStreamingWriteRejection[T](\n      rule,\n      schema,\n      _.toDF().toDF(\"value\"),\n      Seq[T](badValue),\n      \"violated by row with values\"\n    )\n  }\n\n  testReplaceableExpr(\"assert_true\") {\n    testReplaceable(\"assert_true(value < 2) is not null\", IntegerType, 1)\n  }\n\n  testReplaceableExpr(\"date_part\") {\n    testReplaceable(\"date_part('YEAR', value) < 2000\", DateType, Date.valueOf(\"2001-01-01\"))\n  }\n\n  testReplaceableExpr(\"decode\") {\n    testReplaceable(\"decode(encode(value, 'utf-8'), 'utf-8') = 'abc'\", StringType, \"a\")\n  }\n\n  testReplaceableExpr(\"extract\") {\n    testReplaceable(\"extract(YEAR FROM value) < 2000\", DateType, Date.valueOf(\"2001-01-01\"))\n  }\n\n  testReplaceableExpr(\"ifnull\") {\n    testReplaceable(\"ifnull(value, 1) = 1\", IntegerType, 2)\n  }\n\n  testReplaceableExpr(\"left\") {\n    testReplaceable(\"left(value, 1) = 'a'\", StringType, \"b\")\n  }\n\n  testReplaceableExpr(\"right\") {\n    testReplaceable(\"right(value, 1) = 'a'\", StringType, \"b\")\n  }\n\n  testReplaceableExpr(\"nullif\") {\n    testReplaceable(\"nullif(value, 1) = 2\", IntegerType, 1)\n  }\n\n  testReplaceableExpr(\"nvl\") {\n    testReplaceable(\"nvl(value, 1) = 1\", IntegerType, 2)\n  }\n\n  testReplaceableExpr(\"nvl2\") {\n    testReplaceable(\"nvl2(value, 1, 2) = 3\", IntegerType, 2)\n  }\n\n  testReplaceableExpr(\"to_date\") {\n    testReplaceable(\"to_date(value) = '2001-01-01'\", StringType, \"2002-01-01\")\n  }\n\n  testReplaceableExpr(\"to_timestamp\") {\n    testReplaceable(\n      \"to_timestamp(value) = '2001-01-01'\",\n      StringType,\n      \"2002-01-01 00:12:00\")\n  }\n\n\n  // Helper function to test with empty to null conf on and off.\n  private def testEmptyToNull(name: String)(f: => Any): Unit = {\n    // Suppress exceptions output for invariant violations\n    testQuietly(name) {\n      Seq(true, false).foreach { enabled =>\n        withSQLConf(\n          DeltaSQLConf.CONVERT_EMPTY_TO_NULL_FOR_STRING_PARTITION_COL.key -> enabled.toString) {\n          if (enabled) {\n            f\n          } else {\n            intercept[Exception](f)\n          }\n        }\n      }\n    }\n  }\n\n  testEmptyToNull(\"reject empty string for NOT NULL string partition column - create\") {\n    val tblName = \"empty_string_test\"\n    withTable(tblName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tblName (\n           |  c1 INT,\n           |  c2 STRING NOT NULL\n           |) USING delta\n           |PARTITIONED BY (c2)\n           |\"\"\".stripMargin)\n      val ex = intercept[InvariantViolationException] (\n        sql(\n          s\"\"\"\n             |INSERT INTO $tblName values (1, '')\n             |\"\"\".stripMargin)\n      )\n      assert(ex.getMessage.contains(\"violated\"))\n    }\n  }\n\n  testEmptyToNull(\"reject empty string for NOT NULL string partition column - multiple\") {\n    val tblName = \"empty_string_test\"\n    withTable(tblName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tblName (\n           |  c1 INT,\n           |  c2 STRING NOT NULL,\n           |  c3 STRING\n           |) USING delta\n           |PARTITIONED BY (c2, c3)\n           |\"\"\".stripMargin)\n      val ex = intercept[InvariantViolationException] (\n        sql(\n          s\"\"\"\n             |INSERT INTO $tblName values (1, '', 'a')\n             |\"\"\".stripMargin)\n      )\n      assert(ex.getMessage.contains(\"violated\"))\n      sql(\n        s\"\"\"\n           |INSERT INTO $tblName values (1, 'a', '')\n           |\"\"\".stripMargin)\n      checkAnswer(\n        sql(s\"SELECT COUNT(*) from $tblName where c3 IS NULL\"),\n        Row(1L)\n      )\n    }\n  }\n\n  testEmptyToNull(\"reject empty string for NOT NULL string partition column - multiple not null\") {\n    val tblName = \"empty_string_test\"\n    withTable(tblName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tblName (\n           |  c1 INT,\n           |  c2 STRING NOT NULL,\n           |  c3 STRING NOT NULL\n           |) USING delta\n           |PARTITIONED BY (c2, c3)\n           |\"\"\".stripMargin)\n      val ex1 = intercept[InvariantViolationException] (\n        sql(\n          s\"\"\"\n             |INSERT INTO $tblName values (1, '', 'a')\n             |\"\"\".stripMargin)\n      )\n      assert(ex1.getMessage.contains(\"violated\"))\n      val ex2 = intercept[InvariantViolationException] (\n        sql(\n          s\"\"\"\n             |INSERT INTO $tblName values (1, 'a', '')\n             |\"\"\".stripMargin)\n      )\n      assert(ex2.getMessage.contains(\"violated\"))\n      val ex3 = intercept[InvariantViolationException] (\n        sql(\n          s\"\"\"\n             |INSERT INTO $tblName values (1, '', '')\n             |\"\"\".stripMargin)\n      )\n      assert(ex3.getMessage.contains(\"violated\"))\n    }\n  }\n\n\n  testEmptyToNull(\"reject empty string in check constraint\") {\n    val tblName = \"empty_string_test\"\n    withTable(tblName) {\n      sql(\n        s\"\"\"\n           |CREATE TABLE $tblName (\n           |  c1 INT,\n           |  c2 STRING\n           |) USING delta\n           |PARTITIONED BY (c2);\n           |\"\"\".stripMargin)\n      sql(\n        s\"\"\"\n           |ALTER TABLE $tblName ADD CONSTRAINT test CHECK (c2 IS NOT NULL)\n           |\"\"\".stripMargin)\n      intercept[InvariantViolationException] (\n        sql(\n          s\"\"\"\n             |INSERT INTO ${tblName} VALUES (1, \"\")\n             |\"\"\".stripMargin)\n      )\n    }\n  }\n\n  test(\"streaming with additional project\") {\n    withSQLConf(DeltaSQLConf.CONVERT_EMPTY_TO_NULL_FOR_STRING_PARTITION_COL.key -> \"true\") {\n      val tblName = \"test\"\n      withTable(tblName) {\n        withTempDir { checkpointDir =>\n          sql(\n            s\"\"\"\n               |CREATE TABLE $tblName (\n               |  c1 INT,\n               |  c2 STRING\n               |) USING delta\n               |PARTITIONED BY (c2);\n               |\"\"\".stripMargin)\n          sql(\n            s\"\"\"\n               |ALTER TABLE $tblName ADD CONSTRAINT cons CHECK (c1 > 0)\n               |\"\"\".stripMargin)\n          val path = DeltaLog.forTable(spark, TableIdentifier(tblName)).dataPath.toString\n          val stream = MemoryStream[Int]\n          val q = stream.toDF()\n            .map(_ => Tuple2(1, \"a\"))\n            .toDF(\"c1\", \"c2\")\n            .writeStream\n            .option(\"checkpointLocation\", checkpointDir.getCanonicalPath)\n            .format(\"delta\")\n            .start(path)\n          stream.addData(1)\n          q.processAllAvailable()\n          q.stop()\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/schema/SchemaEnforcementSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\nimport java.io.File\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DeltaLog, DeltaOptions}\nimport org.apache.spark.sql.delta.actions.SingleAction\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims.MemoryStream\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.StreamingQueryException\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nsealed trait SaveOperation {\n  def apply(dfw: DataFrameWriter[_]): Unit\n}\n\ncase class SaveWithPath(path: String = null) extends SaveOperation {\n  override def apply(dfw: DataFrameWriter[_]): Unit = {\n    if (path == null) dfw.save() else dfw.save(path)\n  }\n}\n\ncase class SaveAsTable(tableName: String) extends SaveOperation {\n  override def apply(dfw: DataFrameWriter[_]): Unit = dfw.saveAsTable(tableName)\n}\n\nsealed trait SchemaEnforcementSuiteBase extends QueryTest\n    with SharedSparkSession {\n  protected def enableAutoMigration(f: => Unit): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n      f\n    }\n  }\n\n  protected def disableAutoMigration(f: => Unit): Unit = {\n    withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"false\") {\n      f\n    }\n  }\n}\n\nsealed trait BatchWriterTest extends SchemaEnforcementSuiteBase with SharedSparkSession {\n\n  def saveOperation: SaveOperation\n\n  implicit class RichDataFrameWriter(dfw: DataFrameWriter[_]) {\n    def append(path: File): Unit = {\n      saveOperation(dfw.format(\"delta\").mode(\"append\").option(\"path\", path.getAbsolutePath))\n    }\n\n    def overwrite(path: File): Unit = {\n      saveOperation(dfw.format(\"delta\").mode(\"overwrite\").option(\"path\", path.getAbsolutePath))\n    }\n  }\n\n  def equivalenceTest(testName: String)(f: => Unit): Unit = {\n    test(s\"batch: $testName\") {\n      saveOperation match {\n        case _: SaveWithPath => f\n        case SaveAsTable(tbl) => withTable(tbl) { f }\n      }\n    }\n  }\n}\n\ntrait AppendSaveModeNullTests extends BatchWriterTest {\n  import testImplicits._\n\n  equivalenceTest(\"JSON ETL workflow, NullType being only data column\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"id\":null}\"\"\"\n      withTempDir { dir =>\n        val schema1 = new StructType().add(\"key\", StringType).add(\"id\", NullType)\n        val e = intercept[AnalysisException] {\n          spark.read.schema(schema1).json(Seq(row1).toDS()).write.partitionBy(\"key\").append(dir)\n        }\n        assert(e.getMessage.contains(\"NullType have been dropped\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"JSON ETL workflow, schema merging NullTypes\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"id\":null,\"extra\":1}\"\"\"\n      val row2 = \"\"\"{\"key\":\"def\",\"id\":2,\"extra\":null}\"\"\"\n      val row3 = \"\"\"{\"key\":\"ghi\",\"id\":null,\"extra\":3}\"\"\"\n      withTempDir { dir =>\n        val schema1 = new StructType()\n          .add(\"key\", StringType).add(\"id\", NullType).add(\"extra\", IntegerType)\n        val schema2 = new StructType()\n          .add(\"key\", StringType).add(\"id\", IntegerType).add(\"extra\", NullType)\n        spark.read.schema(schema1).json(Seq(row1).toDS()).write.append(dir)\n\n        // NullType will be removed during the read\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getAbsolutePath),\n          Row(\"abc\", 1) :: Nil\n        )\n\n        spark.read.schema(schema2).json(Seq(row2).toDS()).write.append(dir)\n        spark.read.schema(schema1).json(Seq(row3).toDS()).write.append(dir)\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getAbsolutePath),\n          Row(\"abc\", null, 1) :: Row(\"def\", 2, null) :: Row(\"ghi\", null, 3) :: Nil\n        )\n      }\n    }\n  }\n\n  equivalenceTest(\"JSON ETL workflow, schema merging NullTypes - nested struct\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"top\":{\"id\":null,\"extra\":1}}\"\"\"\n      val row2 = \"\"\"{\"key\":\"def\",\"top\":{\"id\":2,\"extra\":null}}\"\"\"\n      val row3 = \"\"\"{\"key\":\"ghi\",\"top\":{\"id\":null,\"extra\":3}}\"\"\"\n      withTempDir { dir =>\n        val schema1 = new StructType().add(\"key\", StringType)\n          .add(\"top\", new StructType().add(\"id\", NullType).add(\"extra\", IntegerType))\n        val schema2 = new StructType().add(\"key\", StringType)\n          .add(\"top\", new StructType().add(\"id\", IntegerType).add(\"extra\", NullType))\n        val mergedSchema = new StructType().add(\"key\", StringType)\n          .add(\"top\", new StructType().add(\"id\", IntegerType).add(\"extra\", IntegerType))\n        spark.read.schema(schema1).json(Seq(row1).toDS()).write.append(dir)\n        // NullType will be removed during the read\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getAbsolutePath),\n          Row(\"abc\", Row(1)) :: Nil\n        )\n\n        spark.read.schema(schema2).json(Seq(row2).toDS()).write.append(dir)\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema === mergedSchema)\n        spark.read.schema(schema1).json(Seq(row3).toDS()).write.append(dir)\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema === mergedSchema)\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(dir.getAbsolutePath),\n          Row(\"abc\", Row(null, 1)) :: Row(\"def\", Row(2, null)) :: Row(\"ghi\", Row(null, 3)) :: Nil\n        )\n      }\n    }\n  }\n\n  equivalenceTest(\"JSON ETL workflow, schema merging NullTypes - throw error on complex types\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"top\":[]}\"\"\"\n      val row2 = \"\"\"{\"key\":\"abc\",\"top\":[{\"id\":null}]}\"\"\"\n      withTempDir { dir =>\n        val schema1 = new StructType().add(\"key\", StringType).add(\"top\", ArrayType(NullType))\n        val schema2 = new StructType().add(\"key\", StringType)\n          .add(\"top\", ArrayType(new StructType().add(\"id\", NullType)))\n        val e1 = intercept[AnalysisException] {\n          spark.read.schema(schema1).json(Seq(row1).toDS()).write.append(dir)\n        }\n        assert(e1.getMessage.contains(\"NullType\"))\n        val e2 = intercept[AnalysisException] {\n          spark.read.schema(schema2).json(Seq(row2).toDS()).write.append(dir)\n        }\n        assert(e2.getMessage.contains(\"NullType\"))\n      }\n    }\n  }\n}\n\ntrait AppendSaveModeTests extends BatchWriterTest {\n  import testImplicits._\n\n  equivalenceTest(\"reject schema changes by default\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(10).write.append(dir)\n        val e = intercept[AnalysisException] {\n          spark.range(10).withColumn(\"part\", 'id + 1).write.append(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n      }\n    }\n  }\n\n  equivalenceTest(\"allow schema changes when autoMigrate is enabled\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(10).write.append(dir)\n        spark.range(10).withColumn(\"part\", 'id + 1).write.append(dir)\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n      }\n    }\n  }\n\n  equivalenceTest(\"disallow schema changes when autoMigrate enabled but writer config disabled\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(10).write.append(dir)\n        val e = intercept[AnalysisException] {\n          spark.range(10).withColumn(\"part\", 'id + 1).write\n            .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"false\").append(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n      }\n    }\n  }\n\n  equivalenceTest(\"allow schema change with option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(10).write.append(dir)\n        spark.range(10).withColumn(\"part\", 'id + 1).write\n          .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"true\").append(dir)\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n      }\n    }\n  }\n\n  equivalenceTest(\"JSON ETL workflow, NullType partition column should fail\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"id\":null}\"\"\"\n      withTempDir { dir =>\n        val schema1 = new StructType().add(\"key\", StringType).add(\"id\", NullType)\n        intercept[AnalysisException] {\n          spark.read.schema(schema1).json(Seq(row1).toDS()).write.partitionBy(\"id\").append(dir)\n        }\n        intercept[AnalysisException] {\n          // check case sensitivity with regards to column dropping\n          spark.read.schema(schema1).json(Seq(row1).toDS()).write.partitionBy(\"iD\").append(dir)\n        }\n      }\n    }\n  }\n\n  equivalenceTest(\"reject columns that only differ by case - append\") {\n    withTempDir { dir =>\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n        intercept[AnalysisException] {\n          spark.range(10).withColumn(\"ID\", 'id + 1).write.append(dir)\n        }\n\n        intercept[AnalysisException] {\n          spark.range(10).withColumn(\"ID\", 'id + 1).write\n            .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"true\").append(dir)\n        }\n\n        intercept[AnalysisException] {\n          spark.range(10).withColumn(\"a\", 'id + 1).write\n            .partitionBy(\"a\", \"A\")\n            .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"true\").append(dir)\n        }\n      }\n    }\n  }\n\n  equivalenceTest(\"ensure schema mismatch error message contains table ID\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(10).write.append(dir)\n        val e = intercept[AnalysisException] {\n          spark.range(10).withColumn(\"part\", 'id + 1).write.append(dir)\n        }\n        assert(e.getMessage.contains(\"schema mismatch detected\"))\n        assert(e.getMessage.contains(\n          s\"Table ID: ${DeltaLog.forTable(spark, dir).unsafeVolatileTableId}\"))\n      }\n    }\n  }\n}\n\ntrait AppendOutputModeTests extends SchemaEnforcementSuiteBase with SharedSparkSession\n  with DeltaSQLTestUtils {\n  import testImplicits._\n\n  testQuietly(\"reject schema changes by default - streaming\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n\n      val memStream = MemoryStream[Long]\n      val stream = memStream.toDS().toDF(\"value1234\") // different column name\n        .writeStream\n        .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n        .format(\"delta\")\n        .start(dir.getAbsolutePath)\n      try {\n        disableAutoMigration {\n          val e = intercept[StreamingQueryException] {\n            memStream.addData(1L)\n            stream.processAllAvailable()\n          }\n          assert(e.cause.isInstanceOf[AnalysisException])\n          assert(e.cause.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n        }\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  testQuietly(\"reject schema changes when autoMigrate enabled but writer config disabled\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n\n      val memStream = MemoryStream[Long]\n      val stream = memStream.toDS().toDF(\"value1234\") // different column name\n        .writeStream\n        .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n        .format(\"delta\")\n        .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"false\")\n        .start(dir.getAbsolutePath)\n      try {\n        enableAutoMigration {\n          val e = intercept[StreamingQueryException] {\n            memStream.addData(1L)\n            stream.processAllAvailable()\n          }\n          assert(e.cause.isInstanceOf[AnalysisException])\n          assert(e.cause.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n        }\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  test(\"allow schema changes when autoMigrate is enabled - streaming\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n\n      enableAutoMigration {\n        val memStream = MemoryStream[Long]\n        val stream = memStream.toDS().toDF(\"value1234\") // different column name\n          .writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          memStream.addData(1L)\n          stream.processAllAvailable()\n\n          assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n        } finally {\n          stream.stop()\n        }\n      }\n    }\n  }\n\n  test(\"allow schema change with option - streaming\") {\n    withTempDir { dir =>\n      spark.range(10).write.format(\"delta\").save(dir.getAbsolutePath)\n\n      val memStream = MemoryStream[Long]\n      val stream = memStream.toDS().toDF(\"value1234\") // different column name\n        .writeStream\n        .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n        .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"true\")\n        .format(\"delta\")\n        .start(dir.getAbsolutePath)\n      try {\n        disableAutoMigration {\n          memStream.addData(1L)\n          stream.processAllAvailable()\n\n          assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n        }\n      } finally {\n        stream.stop()\n      }\n    }\n  }\n\n  testQuietly(\"JSON ETL workflow, reject NullTypes\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"id\":null}\"\"\"\n      withTempDir { dir =>\n        val schema = new StructType().add(\"key\", StringType).add(\"id\", NullType)\n\n        val memStream = MemoryStream[String]\n        val stream = memStream.toDS().select(from_json('value, schema).as(\"value\"))\n          .select($\"value.*\")\n          .writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n\n        try {\n          val e = intercept[StreamingQueryException] {\n            memStream.addData(row1)\n            stream.processAllAvailable()\n          }\n          assert(e.cause.isInstanceOf[AnalysisException])\n          assert(e.cause.getMessage.contains(\"NullType\"))\n        } finally {\n          stream.stop()\n        }\n      }\n    }\n  }\n\n  testQuietly(\"JSON ETL workflow, reject NullTypes on nested column\") {\n    enableAutoMigration {\n      val row1 = \"\"\"{\"key\":\"abc\",\"id\":{\"a\":null}}\"\"\"\n      withTempDir { dir =>\n        val schema = new StructType().add(\"key\", StringType)\n          .add(\"id\", new StructType().add(\"a\", NullType))\n\n        val memStream = MemoryStream[String]\n        val stream = memStream.toDS().select(from_json('value, schema).as(\"value\"))\n          .select($\"value.*\")\n          .writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n\n        try {\n          val e = intercept[StreamingQueryException] {\n            memStream.addData(row1)\n            stream.processAllAvailable()\n          }\n          assert(e.cause.isInstanceOf[AnalysisException])\n          assert(e.cause.getMessage.contains(\"NullType\"))\n        } finally {\n          stream.stop()\n        }\n      }\n    }\n  }\n}\n\ntrait OverwriteSaveModeTests extends BatchWriterTest {\n  import testImplicits._\n\n  equivalenceTest(\"reject schema overwrites by default\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(10).write.overwrite(dir)\n        val e = intercept[AnalysisException] {\n          spark.range(10).withColumn(\"part\", 'id + 1).write.overwrite(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.OVERWRITE_SCHEMA_OPTION))\n      }\n    }\n  }\n\n  equivalenceTest(\"can overwrite schema when using overwrite mode - option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").write.overwrite(dir)\n        spark.range(5).toDF(\"value\").write.option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .overwrite(dir)\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.fieldNames === Array(\"value\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"when autoMerge sqlConf is enabled, we merge schemas\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").write.overwrite(dir)\n        spark.range(5).toDF(\"value\").write.overwrite(dir)\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.fieldNames === Array(\"id\", \"value\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"reject migration when autoMerge sqlConf is enabled and writer config disabled\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").write.overwrite(dir)\n        intercept[AnalysisException] {\n          spark.range(5).toDF(\"value\").write.option(DeltaOptions.MERGE_SCHEMA_OPTION, \"false\")\n            .overwrite(dir)\n        }\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.fieldNames === Array(\"id\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"schema merging with replaceWhere - sqlConf\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n        Seq((1L, 0L), (2L, 0L)).toDF(\"value\", \"part\").write\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 0\")\n          .overwrite(dir)\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.fieldNames === Array(\"id\", \"part\", \"value\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"schema merging with replaceWhere - option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n        Seq((1L, 0L), (2L, 0L)).toDF(\"value\", \"part\").write\n          .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 0\")\n          .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"true\")\n          .overwrite(dir)\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.fieldNames === Array(\"id\", \"part\", \"value\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"schema merging with replaceWhere - option case insensitive\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n        Seq((1L, 0L), (2L, 0L)).toDF(\"value\", \"part\").write\n          .option(\"RePlAcEwHeRe\", \"part = 0\")\n          .option(\"mErGeScHeMa\", \"true\")\n          .overwrite(dir)\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.fieldNames === Array(\"id\", \"part\", \"value\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"reject schema merging with replaceWhere - overwrite option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n        val e = intercept[AnalysisException] {\n          Seq((1L, 0L), (2L, 0L)).toDF(\"value\", \"part\").write\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 0\")\n            .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n            .overwrite(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n      }\n    }\n  }\n\n  equivalenceTest(\"reject schema merging with replaceWhere - no option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n        val e = intercept[AnalysisException] {\n          Seq((1L, 0L), (2L, 0L)).toDF(\"value\", \"part\").write\n            .partitionBy(\"part\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 0\")\n            .overwrite(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n      }\n    }\n  }\n\n  equivalenceTest(\"reject schema merging with replaceWhere - option set to false, config true\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n        val e = intercept[AnalysisException] {\n          Seq((1L, 0L), (2L, 0L)).toDF(\"value\", \"part\").write\n            .partitionBy(\"part\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 0\")\n            .option(DeltaOptions.MERGE_SCHEMA_OPTION, \"false\")\n            .overwrite(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.MERGE_SCHEMA_OPTION))\n      }\n    }\n  }\n\n  equivalenceTest(\"reject change partitioning with overwrite - sqlConf\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").write\n          .overwrite(dir)\n        val e = intercept[AnalysisException] {\n          spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n            .partitionBy(\"part\")\n            .overwrite(dir)\n        }\n        assert(e.getMessage.contains(DeltaOptions.OVERWRITE_SCHEMA_OPTION))\n\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.snapshot.metadata.partitionColumns === Nil)\n        assert(deltaLog.snapshot.metadata.schema.fieldNames === Array(\"id\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"can change partitioning with overwrite - option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").write\n          .overwrite(dir)\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .overwrite(dir)\n\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.snapshot.metadata.partitionColumns === Seq(\"part\"))\n        assert(deltaLog.snapshot.metadata.schema.fieldNames === Array(\"id\", \"part\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"can't change partitioning with overwrite and replaceWhere - option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .partitionBy(\"part\")\n          .overwrite(dir)\n\n        intercept[AnalysisException] {\n          spark.range(5).toDF(\"id\").withColumn(\"part\", lit(0L)).withColumn(\"test\", 'id + 1).write\n            .partitionBy(\"part\", \"test\")\n            .option(DeltaOptions.REPLACE_WHERE_OPTION, \"part = 0\")\n            .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n            .overwrite(dir)\n        }\n      }\n    }\n  }\n\n  equivalenceTest(\"can drop columns with overwriteSchema\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        spark.range(5).toDF(\"id\").withColumn(\"part\", 'id % 2).write\n          .overwrite(dir)\n        spark.range(5).toDF(\"id\").write\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .overwrite(dir)\n\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        assert(deltaLog.snapshot.metadata.partitionColumns === Nil)\n        assert(deltaLog.snapshot.metadata.schema.fieldNames === Array(\"id\"))\n      }\n    }\n  }\n\n  equivalenceTest(\"can change column data type with overwriteSchema\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        spark.range(5).toDF(\"id\").write\n          .overwrite(dir)\n        assert(deltaLog.snapshot.metadata.schema.head === StructField(\"id\", LongType))\n        spark.range(5).toDF(\"id\").selectExpr(\"cast(id as string) as id\").write\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .overwrite(dir)\n        assert(deltaLog.snapshot.metadata.schema.head === StructField(\"id\", StringType))\n      }\n    }\n  }\n\n  equivalenceTest(\"reject columns that only differ by case - overwrite\") {\n    withTempDir { dir =>\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n        intercept[AnalysisException] {\n          spark.range(10).withColumn(\"ID\", 'id + 1).write.overwrite(dir)\n        }\n\n        intercept[AnalysisException] {\n          spark.range(10).withColumn(\"ID\", 'id + 1).write\n            .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n            .overwrite(dir)\n        }\n\n        intercept[AnalysisException] {\n          spark.range(10).withColumn(\"a\", 'id + 1).write\n            .partitionBy(\"a\", \"A\")\n            .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n            .overwrite(dir)\n        }\n      }\n    }\n  }\n}\n\ntrait CompleteOutputModeTests extends SchemaEnforcementSuiteBase with SharedSparkSession\n  with DeltaSQLTestUtils {\n  import testImplicits._\n\n  testQuietly(\"reject complete mode with new schema by default\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        val memStream = MemoryStream[Long]\n        val query = memStream.toDS().toDF(\"id\")\n          .withColumn(\"part\", 'id % 3)\n          .groupBy(\"part\")\n          .count()\n\n        val stream1 = query.writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .outputMode(\"complete\")\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          memStream.addData(1L)\n          stream1.processAllAvailable()\n        } finally {\n          stream1.stop()\n        }\n\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n\n        val stream2 = query.withColumn(\"test\", lit(\"abc\")).writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .outputMode(\"complete\")\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          val e = intercept[StreamingQueryException] {\n            memStream.addData(2L)\n            stream2.processAllAvailable()\n          }\n          assert(e.cause.isInstanceOf[AnalysisException])\n          assert(e.cause.getMessage.contains(DeltaOptions.OVERWRITE_SCHEMA_OPTION))\n\n        } finally {\n          stream2.stop()\n        }\n      }\n    }\n  }\n\n  test(\"complete mode can overwrite schema with option\") {\n    disableAutoMigration {\n      withTempDir { dir =>\n        val memStream = MemoryStream[Long]\n        val query = memStream.toDS().toDF(\"id\")\n          .withColumn(\"part\", 'id % 3)\n          .groupBy(\"part\")\n          .count()\n\n        val stream1 = query.writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .outputMode(\"complete\")\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          memStream.addData(1L)\n          stream1.processAllAvailable()\n        } finally {\n          stream1.stop()\n        }\n\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n\n        val stream2 = query.withColumn(\"test\", lit(\"abc\")).writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .option(DeltaOptions.OVERWRITE_SCHEMA_OPTION, \"true\")\n          .outputMode(\"complete\")\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          memStream.addData(2L)\n          stream2.processAllAvailable()\n\n          memStream.addData(3L)\n          stream2.processAllAvailable()\n        } finally {\n          stream2.stop()\n        }\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.length == 3)\n\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        val hadoopConf = deltaLog.newDeltaHadoopConf()\n        val lastCommitFile = deltaLog.store\n          .listFrom(FileNames.listingPrefix(deltaLog.logPath, 0L), hadoopConf)\n          .map(_.getPath).filter(FileNames.isDeltaFile).toArray.last\n        val lastCommitContainsMetadata = deltaLog.store.read(lastCommitFile, hadoopConf)\n          .exists(JsonUtils.mapper.readValue[SingleAction](_).metaData != null)\n\n        assert(!lastCommitContainsMetadata,\n          \"Metadata shouldn't be updated as long as schema doesn't change\")\n\n        checkAnswer(\n          df,\n          Row(0L, 1L, \"abc\") :: Row(1L, 1L, \"abc\") :: Row(2L, 1L, \"abc\") :: Nil)\n      }\n    }\n  }\n\n  test(\"complete mode behavior with autoMigrate enabled is to migrate schema\") {\n    enableAutoMigration {\n      withTempDir { dir =>\n        val memStream = MemoryStream[Long]\n        val query = memStream.toDS().toDF(\"id\")\n          .withColumn(\"part\", 'id % 3)\n          .groupBy(\"part\")\n          .count()\n\n        val stream1 = query.writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .outputMode(\"complete\")\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          memStream.addData(1L)\n          stream1.processAllAvailable()\n        } finally {\n          stream1.stop()\n        }\n\n        assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).schema.length == 2)\n\n        val stream2 = query.withColumn(\"test\", lit(\"abc\")).writeStream\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint\").getAbsolutePath)\n          .outputMode(\"complete\")\n          .format(\"delta\")\n          .start(dir.getAbsolutePath)\n        try {\n          memStream.addData(2L)\n          stream2.processAllAvailable()\n\n          memStream.addData(3L)\n          stream2.processAllAvailable()\n        } finally {\n          stream2.stop()\n        }\n\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        assert(df.schema.length == 3)\n\n        val deltaLog = DeltaLog.forTable(spark, dir)\n        val hadoopConf = deltaLog.newDeltaHadoopConf()\n        val lastCommitFile = deltaLog.store\n          .listFrom(FileNames.listingPrefix(deltaLog.logPath, 0L), hadoopConf)\n          .map(_.getPath).filter(FileNames.isDeltaFile).toArray.last\n        val lastCommitContainsMetadata = deltaLog.store.read(lastCommitFile, hadoopConf)\n          .exists(JsonUtils.mapper.readValue[SingleAction](_).metaData != null)\n\n        assert(!lastCommitContainsMetadata,\n          \"Metadata shouldn't be updated as long as schema doesn't change\")\n\n        checkAnswer(\n          df,\n          Row(0L, 1L, \"abc\") :: Row(1L, 1L, \"abc\") :: Row(2L, 1L, \"abc\") :: Nil)\n      }\n    }\n  }\n}\n\nclass SchemaEnforcementWithPathSuite\n  extends AppendSaveModeTests\n  with AppendSaveModeNullTests\n  with OverwriteSaveModeTests\n  with DeltaSQLCommandTest {\n  override val saveOperation = SaveWithPath()\n}\n\nclass SchemaEnforcementWithTableSuite\n  extends AppendSaveModeTests\n  with OverwriteSaveModeTests\n  with DeltaSQLCommandTest {\n\n  override val saveOperation = SaveAsTable(\"delta_schema_test\")\n}\n\nclass SchemaEnforcementStreamingSuite\n  extends AppendOutputModeTests\n  with CompleteOutputModeTests\n  with DeltaSQLCommandTest {\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/schema/SchemaUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.schema\n\n// scalastyle:off import.ordering.noEmptyLine\nimport java.util.Locale\nimport java.util.regex.Pattern\n\nimport scala.annotation.tailrec\n\nimport org.apache.spark.sql.delta.{DeltaAnalysisException, DeltaLog, DeltaTestUtils, TypeWideningMode}\nimport org.apache.spark.sql.delta.RowCommitVersion\nimport org.apache.spark.sql.delta.RowId\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.schema.SchemaMergingUtils._\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf.AllowAutomaticWideningMode\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport io.delta.tables.DeltaTable\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.sql.{AnalysisException, Column, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.{InternalRow, TableIdentifier}\nimport org.apache.spark.sql.catalyst.analysis.{caseInsensitiveResolution, caseSensitiveResolution, UnresolvedAttribute}\nimport org.apache.spark.sql.catalyst.expressions.{Alias, Cast, Expression}\nimport org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\nclass SchemaUtilsSuite extends QueryTest\n  with SharedSparkSession\n  with GivenWhenThen\n  with DeltaSQLTestUtils\n  with DeltaSQLCommandTest {\n  import SchemaUtils._\n  import TypeWideningMode._\n  import testImplicits._\n\n  private def expectFailure(shouldContain: String*)(f: => Unit): Unit = {\n    val e = intercept[AnalysisException] {\n      f\n    }\n    val msg = e.getMessage.toLowerCase(Locale.ROOT)\n    assert(shouldContain.map(_.toLowerCase(Locale.ROOT)).forall(msg.contains),\n      s\"Error message '$msg' didn't contain: $shouldContain\")\n  }\n\n  private def expectFailurePattern(shouldContainPatterns: String*)(f: => Unit): Unit = {\n    val e = intercept[AnalysisException] {\n      f\n    }\n    val patterns =\n      shouldContainPatterns.map(regex => Pattern.compile(regex, Pattern.CASE_INSENSITIVE))\n    assert(patterns.forall(_.matcher(e.getMessage).find()),\n      s\"Error message '${e.getMessage}' didn't contain the patterns: $shouldContainPatterns\")\n  }\n\n  private def expectAnalysisErrorClass(\n      errorClass: String,\n      params: Map[String, String],\n      matchPVals: Boolean = true)(\n      f: => Unit): Unit = {\n    val e = intercept[AnalysisException] {\n      f\n    }\n\n    @tailrec\n    def getError(ex: Throwable): Option[DeltaAnalysisException] = ex match {\n      case e: DeltaAnalysisException if e.getErrorClass() == errorClass => Some(e)\n      case e: AnalysisException => getError(e.getCause)\n      case _ => None\n    }\n\n    val err = getError(e)\n    assert(err.isDefined, \"exception with the error class not found\")\n    checkError(\n      err.get,\n      errorClass,\n      parameters = params,\n      matchPVals = matchPVals)\n  }\n\n  /////////////////////////////\n  // Duplicate Column Checks\n  /////////////////////////////\n\n  test(\"duplicate column name in top level\") {\n    val schema = new StructType()\n      .add(\"dupColName\", IntegerType)\n      .add(\"b\", IntegerType)\n      .add(\"dupColName\", StringType)\n    expectFailure(\"dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in top level - case sensitivity\") {\n    val schema = new StructType()\n      .add(\"dupColName\", IntegerType)\n      .add(\"b\", IntegerType)\n      .add(\"dupCOLNAME\", StringType)\n    expectFailure(\"dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name for nested column + non-nested column\") {\n    val schema = new StructType()\n      .add(\"dupColName\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))\n      .add(\"dupColName\", IntegerType)\n    expectFailure(\"dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name for nested column + non-nested column - case sensitivity\") {\n    val schema = new StructType()\n      .add(\"dupColName\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))\n      .add(\"dupCOLNAME\", IntegerType)\n    expectFailure(\"dupCOLNAME\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in nested level\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"dupColName\", IntegerType)\n        .add(\"b\", IntegerType)\n        .add(\"dupColName\", StringType)\n      )\n    expectFailure(\"top.dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in nested level - case sensitivity\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"dupColName\", IntegerType)\n        .add(\"b\", IntegerType)\n        .add(\"dupCOLNAME\", StringType)\n      )\n    expectFailure(\"top.dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in double nested level\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"b\", new StructType()\n          .add(\"dupColName\", StringType)\n          .add(\"c\", IntegerType)\n          .add(\"dupColName\", StringType))\n        .add(\"d\", IntegerType)\n      )\n    expectFailure(\"top.b.dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in double nested array\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"b\", ArrayType(ArrayType(new StructType()\n          .add(\"dupColName\", StringType)\n          .add(\"c\", IntegerType)\n          .add(\"dupColName\", StringType))))\n        .add(\"d\", IntegerType)\n      )\n    expectFailure(\"top.b.element.element.dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in double nested map\") {\n    val keyType = new StructType()\n      .add(\"dupColName\", IntegerType)\n      .add(\"d\", StringType)\n    expectFailure(\"top.b.key.dupColName\") {\n      val schema = new StructType()\n        .add(\"top\", new StructType()\n          .add(\"b\", MapType(keyType.add(\"dupColName\", StringType), keyType))\n        )\n      checkColumnNameDuplication(schema, \"\")\n    }\n    expectFailure(\"top.b.value.dupColName\") {\n      val schema = new StructType()\n        .add(\"top\", new StructType()\n          .add(\"b\", MapType(keyType, keyType.add(\"dupColName\", StringType)))\n        )\n      checkColumnNameDuplication(schema, \"\")\n    }\n    // This is okay\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"b\", MapType(keyType, keyType))\n      )\n    checkColumnNameDuplication(schema, \"\")\n  }\n\n  test(\"duplicate column name in nested array\") {\n    val schema = new StructType()\n      .add(\"top\", ArrayType(new StructType()\n        .add(\"dupColName\", IntegerType)\n        .add(\"b\", IntegerType)\n        .add(\"dupColName\", StringType))\n      )\n    expectFailure(\"top.element.dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column name in nested array - case sensitivity\") {\n    val schema = new StructType()\n      .add(\"top\", ArrayType(new StructType()\n        .add(\"dupColName\", IntegerType)\n        .add(\"b\", IntegerType)\n        .add(\"dupCOLNAME\", StringType))\n      )\n    expectFailure(\"top.element.dupColName\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"non duplicate column because of back tick\") {\n    val schema = new StructType()\n      .add(\"top\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))\n      .add(\"top.a\", IntegerType)\n    checkColumnNameDuplication(schema, \"\")\n  }\n\n  test(\"non duplicate column because of back tick - nested\") {\n    val schema = new StructType()\n      .add(\"first\", new StructType()\n        .add(\"top\", new StructType()\n          .add(\"a\", IntegerType)\n          .add(\"b\", IntegerType))\n        .add(\"top.a\", IntegerType))\n    checkColumnNameDuplication(schema, \"\")\n  }\n\n  test(\"duplicate column with back ticks - nested\") {\n    val schema = new StructType()\n      .add(\"first\", new StructType()\n        .add(\"top.a\", StringType)\n        .add(\"b\", IntegerType)\n        .add(\"top.a\", IntegerType))\n    expectFailure(\"first.`top.a`\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  test(\"duplicate column with back ticks - nested and case sensitivity\") {\n    val schema = new StructType()\n      .add(\"first\", new StructType()\n        .add(\"TOP.a\", StringType)\n        .add(\"b\", IntegerType)\n        .add(\"top.a\", IntegerType))\n    expectFailure(\"first.`top.a`\") { checkColumnNameDuplication(schema, \"\") }\n  }\n\n  /////////////////////////////\n  // Read Compatibility Checks\n  /////////////////////////////\n\n  /**\n   * Tests change of datatype within a schema.\n   *  - the make() function is a \"factory\" function to create schemas that vary only by the\n   *    given datatype in a specific position in the schema.\n   *  - other tests will call this method with different make() functions to test datatype\n   *    incompatibility in all the different places within a schema (in a top-level struct,\n   *    in a nested struct, as the element type of an array, etc.)\n   */\n  def testDatatypeChange(scenario: String)(make: DataType => StructType): Unit = {\n    val schemas = Map(\n      (\"int\", make(IntegerType)),\n      (\"string\", make(StringType)),\n      (\"struct\", make(new StructType().add(\"a\", StringType))),\n      (\"array\", make(ArrayType(IntegerType))),\n      (\"map\", make(MapType(StringType, FloatType)))\n    )\n    test(s\"change of datatype should fail read compatibility - $scenario\") {\n      for (a <- schemas.keys; b <- schemas.keys if a != b) {\n        assert(!isReadCompatible(schemas(a), schemas(b)),\n          s\"isReadCompatible should have failed for: ${schemas(a)}, ${schemas(b)}\")\n      }\n    }\n  }\n\n  /**\n   * Tests change of nullability within a schema (making a field nullable is not allowed,\n   * but making a nullable field non-nullable is ok).\n   *  - the make() function is a \"factory\" function to create schemas that vary only by the\n   *    nullability (of a field, array element, or map values) in a specific position in the schema.\n   *  - other tests will call this method with different make() functions to test nullability\n   *    incompatibility in all the different places within a schema (in a top-level struct,\n   *    in a nested struct, for the element type of an array, etc.)\n   */\n  def testNullability(scenario: String)(make: Boolean => StructType): Unit = {\n    val nullable = make(true)\n    val nonNullable = make(false)\n    Seq(true, false).foreach { forbidTightenNullability =>\n      val (blockedCase, blockedExisting, blockedRead) = if (forbidTightenNullability) {\n        (s\"tighten nullability should fail read compatibility \" +\n          s\"(forbidTightenNullability=$forbidTightenNullability) - $scenario\",\n          nullable, nonNullable)\n      } else {\n        (s\"relax nullability should fail read compatibility \" +\n          s\"(forbidTightenNullability=$forbidTightenNullability) - $scenario\",\n          nonNullable, nullable)\n      }\n      val (allowedCase, allowedExisting, allowedRead) = if (forbidTightenNullability) {\n        (s\"relax nullability should not fail read compatibility \" +\n          s\"(forbidTightenNullability=$forbidTightenNullability) - $scenario\",\n          nonNullable, nullable)\n      } else {\n        (s\"tighten nullability should not fail read compatibility \" +\n          s\"(forbidTightenNullability=$forbidTightenNullability) - $scenario\",\n          nullable, nonNullable)\n      }\n      test(blockedCase) {\n        assert(!isReadCompatible(blockedExisting, blockedRead, forbidTightenNullability))\n      }\n      test(allowedCase) {\n        assert(isReadCompatible(allowedExisting, allowedRead, forbidTightenNullability))\n      }\n    }\n  }\n\n  /**\n   * Tests for fields of a struct: adding/dropping fields, changing nullability, case variation\n   *  - The make() function is a \"factory\" method to produce schemas. It takes a function that\n   *    mutates a struct (for example, but adding a column, or it could just not make any change).\n   *  - Following tests will call this method with different factory methods, to mutate the\n   *    various places where a struct can appear (at the top-level, nested in another struct,\n   *    within an array, etc.)\n   *  - This allows us to have one shared code to test compatibility of a struct field in all the\n   *    different places where it may occur.\n   */\n  def testColumnVariations(scenario: String)\n      (make: (StructType => StructType) => StructType): Unit = {\n\n    // generate one schema without extra column, one with, one nullable, and one with mixed case\n    val withoutExtra = make(struct => struct) // produce struct WITHOUT extra field\n    val withExtraNullable = make(struct => struct.add(\"extra\", StringType))\n    val withExtraMixedCase = make(struct => struct.add(\"eXtRa\", StringType))\n    val withExtraNonNullable = make(struct => struct.add(\"extra\", StringType, nullable = false))\n\n    test(s\"dropping a field should fail read compatibility - $scenario\") {\n      assert(!isReadCompatible(withExtraNullable, withoutExtra))\n    }\n    test(s\"adding a nullable field should not fail read compatibility - $scenario\") {\n      assert(isReadCompatible(withoutExtra, withExtraNullable))\n    }\n    test(s\"adding a non-nullable field should not fail read compatibility - $scenario\") {\n      assert(isReadCompatible(withoutExtra, withExtraNonNullable))\n    }\n    test(s\"case variation of field name should fail read compatibility - $scenario\") {\n      assert(!isReadCompatible(withExtraNullable, withExtraMixedCase))\n    }\n    testNullability(scenario)(b => make(struct => struct.add(\"extra\", StringType, nullable = b)))\n    testDatatypeChange(scenario)(datatype => make(struct => struct.add(\"extra\", datatype)))\n  }\n\n  // --------------------------------------------------------------------\n  // tests for all kinds of places where a field can appear in a struct\n  // --------------------------------------------------------------------\n\n  testColumnVariations(\"top level\")(\n    f => f(new StructType().add(\"a\", IntegerType)))\n\n  testColumnVariations(\"nested struct\")(\n    f => new StructType()\n      .add(\"a\", f(new StructType().add(\"b\", IntegerType))))\n\n  testColumnVariations(\"nested in array\")(\n    f => new StructType()\n      .add(\"array\", ArrayType(\n        f(new StructType().add(\"b\", IntegerType)))))\n\n  testColumnVariations(\"nested in map key\")(\n    f => new StructType()\n      .add(\"map\", MapType(\n        f(new StructType().add(\"b\", IntegerType)),\n        StringType)))\n\n  testColumnVariations(\"nested in map value\")(\n    f => new StructType()\n      .add(\"map\", MapType(\n        StringType,\n        f(new StructType().add(\"b\", IntegerType)))))\n\n  // --------------------------------------------------------------------\n  // tests for data type change in places other than struct\n  // --------------------------------------------------------------------\n\n  testDatatypeChange(\"array element\")(\n    datatype => new StructType()\n      .add(\"array\", ArrayType(datatype)))\n\n  testDatatypeChange(\"map key\")(\n    datatype => new StructType()\n      .add(\"map\", MapType(datatype, StringType)))\n\n  testDatatypeChange(\"map value\")(\n    datatype => new StructType()\n      .add(\"map\", MapType(StringType, datatype)))\n\n  // --------------------------------------------------------------------\n  // tests for nullability change in places other than struct\n  // --------------------------------------------------------------------\n\n  testNullability(\"array contains null\")(\n    b => new StructType()\n      .add(\"array\", ArrayType(StringType, containsNull = b)))\n\n  testNullability(\"map contains null values\")(\n    b => new StructType()\n      .add(\"map\", MapType(IntegerType, StringType, valueContainsNull = b)))\n\n  testNullability(\"map nested in array\")(\n    b => new StructType()\n      .add(\"map\", ArrayType(\n        MapType(IntegerType, StringType, valueContainsNull = b))))\n\n  testNullability(\"array nested in map\")(\n    b => new StructType()\n      .add(\"map\", MapType(\n        IntegerType,\n        ArrayType(StringType, containsNull = b))))\n\n  ////////////////////////////\n  // reportDifference\n  ////////////////////////////\n\n  /**\n   * @param existing the existing schema to compare to\n   * @param specified the new specified schema\n   * @param expected an expected list of messages, each describing a schema difference.\n   *                 Every expected message is actually a regex patterns that is matched\n   *                 against all diffs that are returned. This is necessary to tolerate\n   *                 variance in ordering of field names, for example in a message such as\n   *                 \"Specified schema has additional field(s): x, y\", we cannot predict\n   *                 the order of x and y.\n   */\n  def testReportDifferences(testName: String)\n    (existing: StructType, specified: StructType, expected: String*): Unit = {\n    test(testName) {\n      val differences = SchemaUtils.reportDifferences(existing, specified)\n      // make sure every expected difference is reported\n      expected foreach ((exp: String) =>\n        assert(differences.exists(message => exp.r.findFirstMatchIn(message).isDefined),\n          s\"\"\"Difference not reported.\n             |Expected:\n             |- $exp\n             |Reported: ${differences.mkString(\"\\n- \", \"\\n- \", \"\")}\n            \"\"\".stripMargin))\n      // make sure there are no extra differences reported\n      assert(expected.size == differences.size,\n        s\"\"\"Too many differences reported.\n           |Expected: ${expected.mkString(\"\\n- \", \"\\n- \", \"\")}\n           |Reported: ${differences.mkString(\"\\n- \", \"\\n- \", \"\")}\n          \"\"\".stripMargin)\n    }\n  }\n\n  testReportDifferences(\"extra columns should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType),\n    expected = \"additional field[(]s[)]: b\"\n  )\n\n  testReportDifferences(\"missing columns should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType),\n    specified = new StructType()\n      .add(\"a\", IntegerType),\n    expected = \"missing field[(]s[)]: b\"\n  )\n\n  testReportDifferences(\"making a column nullable should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType, nullable = false)\n      .add(\"b\", StringType, nullable = true),\n    specified = new StructType()\n      .add(\"a\", IntegerType, nullable = true)\n      .add(\"b\", StringType, nullable = true),\n    expected = \"a is nullable in specified schema but non-nullable in existing schema\"\n  )\n\n  testReportDifferences(\"making a column non-nullable should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType, nullable = false)\n      .add(\"b\", StringType, nullable = true),\n    specified = new StructType()\n      .add(\"a\", IntegerType, nullable = false)\n      .add(\"b\", StringType, nullable = false),\n    expected = \"b is non-nullable in specified schema but nullable in existing schema\"\n  )\n\n  testReportDifferences(\"change in column metadata should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType, nullable = true, new MetadataBuilder().putString(\"x\", \"1\").build())\n      .add(\"b\", StringType),\n    specified = new StructType()\n      .add(\"a\", IntegerType, nullable = true, new MetadataBuilder().putString(\"x\", \"2\").build())\n      .add(\"b\", StringType),\n    expected = \"metadata for field a is different\"\n  )\n\n  testReportDifferences(\"change in generation expression for generated columns\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType, nullable = true,\n        new MetadataBuilder()\n          .putString(GENERATION_EXPRESSION_METADATA_KEY, \"b + 1\")\n          .putString(\"x\", \"1\").build())\n      .add(\"b\", StringType),\n    specified = new StructType()\n      .add(\"a\", IntegerType, nullable = true, new MetadataBuilder()\n        .putString(GENERATION_EXPRESSION_METADATA_KEY, \"1 + b\")\n        .putString(\"x\", \"1\").build())\n      .add(\"b\", StringType),\n    // Regex flags: DOTALL and MULTILINE\n    expected = \"(?sm)generation expression for field a is different\" +\n      // Not include\n      \"(?!.*metadata for field a is different)\"\n  )\n\n  testReportDifferences(\"change in column metadata for generated columns\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType, nullable = true,\n        new MetadataBuilder()\n          .putString(GENERATION_EXPRESSION_METADATA_KEY, \"b + 1\")\n          .putString(\"x\", \"1\").build())\n      .add(\"b\", StringType),\n    specified = new StructType()\n      .add(\"a\", IntegerType, nullable = true, new MetadataBuilder()\n        .putString(GENERATION_EXPRESSION_METADATA_KEY, \"b + 1\")\n        .putString(\"x\", \"2\").build())\n      .add(\"b\", StringType),\n    expected = \"metadata for field a is different\"\n  )\n\n  testReportDifferences(\"change in generation expression and metadata for generated columns\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType, nullable = true,\n        new MetadataBuilder()\n          .putString(GENERATION_EXPRESSION_METADATA_KEY, \"b + 1\")\n          .putString(\"x\", \"1\").build())\n      .add(\"b\", StringType),\n    specified = new StructType()\n      .add(\"a\", IntegerType, nullable = true, new MetadataBuilder()\n        .putString(GENERATION_EXPRESSION_METADATA_KEY, \"b + 2\")\n        .putString(\"x\", \"2\").build())\n      .add(\"b\", StringType),\n    // Regex flags: DOTALL and MULTILINE\n    expected = \"(?sm)generation expression for field a is different\" +\n      \".*metadata for field a is different\"\n  )\n\n  testReportDifferences(\"change of column type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(\n        StringType, containsNull = false)),\n    expected = \"type for b is different\"\n  )\n\n  testReportDifferences(\"change of array nullability should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(\n        new StructType().add(\"x\", LongType), containsNull = true)),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(\n        new StructType().add(\"x\", LongType), containsNull = false)),\n    expected = \"b\\\\[\\\\] can not contain null in specified schema but can in existing\"\n  )\n\n  testReportDifferences(\"change of element type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(LongType, containsNull = true)),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(StringType, containsNull = true)),\n    expected = \"type for b\\\\[\\\\] is different\"\n  )\n\n  testReportDifferences(\"change of element struct type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(\n        new StructType()\n          .add(\"x\", LongType),\n        containsNull = true)),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new ArrayType(\n        new StructType()\n          .add(\"x\", StringType),\n        containsNull = true)),\n    expected = \"type for b\\\\[\\\\].x is different\"\n  )\n\n  testReportDifferences(\"change of map value nullability should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new MapType(\n        StringType,\n        new StructType().add(\"x\", LongType), valueContainsNull = true)),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new MapType(\n        StringType,\n        new StructType().add(\"x\", LongType), valueContainsNull = false)),\n    expected = \"b can not contain null values in specified schema but can in existing\"\n  )\n\n  testReportDifferences(\"change of map key type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new MapType(LongType, StringType, valueContainsNull = true)),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new MapType(StringType, StringType, valueContainsNull = true)),\n    expected = \"type for b\\\\[key\\\\] is different\"\n  )\n\n  testReportDifferences(\"change of value struct type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new MapType(\n        StringType,\n        new StructType().add(\"x\", LongType),\n        valueContainsNull = true)),\n    specified = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", new MapType(\n        StringType,\n        new StructType().add(\"x\", FloatType),\n        valueContainsNull = true)),\n    expected = \"type for b\\\\[value\\\\].x is different\"\n  )\n\n  testReportDifferences(\"nested extra columns should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", StringType)\n        .add(\"c\", LongType)),\n    expected = \"additional field[(]s[)]: (x.b, x.c|x.c, x.b)\"\n  )\n\n  testReportDifferences(\"nested missing columns should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", StringType)\n        .add(\"c\", FloatType)),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)),\n    expected = \"missing field[(]s[)]: (x.b, x.c|x.c, x.b)\"\n  )\n\n  testReportDifferences(\"making a nested column nullable should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType, nullable = false)\n        .add(\"b\", StringType, nullable = true)),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType, nullable = true)\n        .add(\"b\", StringType, nullable = true)),\n    expected = \"x.a is nullable in specified schema but non-nullable in existing schema\"\n  )\n\n  testReportDifferences(\"making a nested column non-nullable should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType, nullable = false)\n        .add(\"b\", StringType, nullable = true)),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType, nullable = false)\n        .add(\"b\", StringType, nullable = false)),\n    expected = \"x.b is non-nullable in specified schema but nullable in existing schema\"\n  )\n\n  testReportDifferences(\"change in nested column metadata should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType, nullable = true, new MetadataBuilder().putString(\"x\", \"1\").build())\n        .add(\"b\", StringType)),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType, nullable = true, new MetadataBuilder().putString(\"x\", \"2\").build())\n        .add(\"b\", StringType)),\n    expected = \"metadata for field x.a is different\"\n  )\n\n  testReportDifferences(\"change of nested column type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", StringType)),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(\n          StringType, containsNull = false))),\n    expected = \"type for x.b is different\"\n  )\n\n  testReportDifferences(\"change of nested array nullability should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(\n          new StructType()\n            .add(\"x\", LongType),\n          containsNull = true))),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(\n          new StructType()\n            .add(\"x\", LongType),\n          containsNull = false))),\n    expected = \"x.b\\\\[\\\\] can not contain null in specified schema but can in existing\"\n  )\n\n  testReportDifferences(\"change of nested element type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(LongType, containsNull = true))),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(StringType, containsNull = true))),\n    expected = \"type for x.b\\\\[\\\\] is different\"\n  )\n\n  testReportDifferences(\"change of nested element struct type should be reported as a difference\")(\n    existing = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(\n          new StructType()\n            .add(\"x\", LongType),\n          containsNull = true))),\n    specified = new StructType()\n      .add(\"x\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", new ArrayType(\n          new StructType()\n            .add(\"x\", StringType),\n          containsNull = true))),\n    expected = \"type for x.b\\\\[\\\\].x is different\"\n  )\n\n  private val piiTrue = new MetadataBuilder().putBoolean(\"pii\", value = true).build()\n  private val piiFalse = new MetadataBuilder().putBoolean(\"pii\", value = false).build()\n\n  testReportDifferences(\"multiple differences should be reported\")(\n    existing = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType)\n      .add(\"c\", BinaryType)\n      .add(\"f\", LongType, nullable = true, piiTrue)\n      .add(\"g\", new MapType(\n        IntegerType,\n        new StructType()\n          .add(\"a\", IntegerType, nullable = false, piiFalse)\n          .add(\"b\", StringType)\n          .add(\"d\", new ArrayType(\n            LongType,\n            containsNull = false\n          )),\n        valueContainsNull = true))\n      .add(\"h\", new MapType(\n        LongType,\n        StringType,\n        valueContainsNull = true)),\n    specified = new StructType()\n      .add(\"a\", FloatType)\n      .add(\"d\", StringType)\n      .add(\"e\", LongType)\n      .add(\"f\", LongType, nullable = false, piiFalse)\n      .add(\"g\", new MapType(\n        StringType,\n        new StructType()\n          .add(\"a\", LongType, nullable = true)\n          .add(\"c\", StringType)\n          .add(\"d\", new ArrayType(\n            BooleanType,\n            containsNull = true\n          )),\n        valueContainsNull = false))\n      .add(\"h\", new MapType(\n        LongType,\n        new ArrayType(IntegerType, containsNull = false),\n        valueContainsNull = true)),\n    \"type for a is different\",\n    \"additional field[(]s[)]: (d, e|e, d)\",\n    \"missing field[(]s[)]: (b, c|c, b)\",\n    \"f is non-nullable in specified schema but nullable\",\n    \"metadata for field f is different\",\n    \"type for g\\\\[key\\\\] is different\",\n    \"g can not contain null values in specified schema but can in existing\",\n    \"additional field[(]s[)]: g\\\\[value\\\\].c\",\n    \"missing field[(]s[)]: g\\\\[value\\\\].b\",\n    \"type for g\\\\[value\\\\].a is different\",\n    \"g\\\\[value\\\\].a is nullable in specified schema but non-nullable in existing\",\n    \"metadata for field g\\\\[value\\\\].a is different\",\n    \"field g\\\\[value\\\\].d\\\\[\\\\] can contain null in specified schema but can not in existing\",\n    \"type for g\\\\[value\\\\].d\\\\[\\\\] is different\",\n    \"type for h\\\\[value\\\\] is different\"\n  )\n\n  ////////////////////////////\n  // findColumnPosition\n  ////////////////////////////\n\n  test(\"findColumnPosition\") {\n    val schema = new StructType()\n      .add(\"struct\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", IntegerType))\n      .add(\"array\", ArrayType(new StructType()\n        .add(\"c\", IntegerType)\n        .add(\"d\", IntegerType)))\n      .add(\"field\", StringType)\n      .add(\"map\", MapType(\n        new StructType()\n          .add(\"e\", IntegerType),\n        new StructType()\n          .add(\"f\", IntegerType)))\n      .add(\"mapStruct\", MapType(\n        IntegerType,\n        new StructType()\n          .add(\"g\", new StructType()\n          .add(\"h\", IntegerType))))\n      .add(\"arrayMap\", ArrayType(\n        MapType(\n          new StructType()\n            .add(\"i\", IntegerType),\n          new StructType()\n            .add(\"j\", IntegerType))))\n\n    val List(structIdx, arrayIdx, fieldIdx, mapIdx, mapStructIdx, arrayMapIdx) = (0 to 5).toList\n    val ARRAY_ELEMENT_INDEX = 0\n    val MAP_KEY_INDEX = 0\n    val MAP_VALUE_INDEX = 1\n\n    def checkPosition(column: Seq[String], position: Seq[Int]): Unit =\n      assert(SchemaUtils.findColumnPosition(column, schema) === position)\n\n    checkPosition(Seq(\"struct\"), Seq(structIdx))\n    checkPosition(Seq(\"STRucT\"), Seq(structIdx))\n    expectFailure(\"Couldn't find\", schema.treeString) {\n      SchemaUtils.findColumnPosition(Seq(\"struct\", \"array\"), schema)\n    }\n    checkPosition(Seq(\"struct\", \"a\"), Seq(structIdx, 0))\n    checkPosition(Seq(\"STRucT\", \"a\"), Seq(structIdx, 0))\n    checkPosition(Seq(\"struct\", \"A\"), Seq(structIdx, 0))\n    checkPosition(Seq(\"STRucT\", \"A\"), Seq(structIdx, 0))\n    checkPosition(Seq(\"struct\", \"b\"), Seq(structIdx, 1))\n    checkPosition(Seq(\"array\"), Seq(arrayIdx))\n    checkPosition(Seq(\"array\", \"element\", \"C\"), Seq(arrayIdx, ARRAY_ELEMENT_INDEX, 0))\n    checkPosition(Seq(\"array\", \"element\", \"d\"), Seq(arrayIdx, ARRAY_ELEMENT_INDEX, 1))\n    checkPosition(Seq(\"field\"), Seq(fieldIdx))\n    checkPosition(Seq(\"map\"), Seq(mapIdx))\n    checkPosition(Seq(\"map\", \"key\", \"e\"), Seq(mapIdx, MAP_KEY_INDEX, 0))\n    checkPosition(Seq(\"map\", \"value\", \"f\"), Seq(mapIdx, MAP_VALUE_INDEX, 0))\n    checkPosition(Seq(\"map\", \"value\", \"F\"), Seq(mapIdx, MAP_VALUE_INDEX, 0))\n    checkPosition(Seq(\"mapStruct\", \"key\"), Seq(mapStructIdx, MAP_KEY_INDEX))\n    checkPosition(Seq(\"mapStruct\", \"value\", \"g\"), Seq(mapStructIdx, MAP_VALUE_INDEX, 0))\n    checkPosition(Seq(\"mapStruct\", \"key\"), Seq(mapStructIdx, MAP_KEY_INDEX))\n    checkPosition(Seq(\"mapStruct\", \"value\"), Seq(mapStructIdx, MAP_VALUE_INDEX))\n    checkPosition(Seq(\"arrayMap\"), Seq(arrayMapIdx))\n    checkPosition(Seq(\"arrayMap\", \"element\"), Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX))\n    checkPosition(\n      Seq(\"arrayMap\", \"element\", \"key\"),\n      Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_KEY_INDEX))\n    checkPosition(\n      Seq(\"arrayMap\", \"element\", \"value\"),\n      Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_VALUE_INDEX))\n    checkPosition(\n      Seq(\"arrayMap\", \"element\", \"key\", \"i\"),\n      Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_KEY_INDEX, 0))\n    checkPosition(\n      Seq(\"arrayMap\", \"element\", \"value\", \"j\"),\n      Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_VALUE_INDEX, 0))\n\n    val resolver = org.apache.spark.sql.catalyst.analysis.caseSensitiveResolution\n    Seq(Seq(\"STRucT\", \"b\"), Seq(\"struct\", \"B\"), Seq(\"array\", \"element\", \"C\"),\n        Seq(\"map\", \"key\", \"E\")).foreach { column =>\n      expectFailure(\"Couldn't find\", schema.treeString) {\n        SchemaUtils.findColumnPosition(column, schema, resolver)\n      }\n    }\n  }\n\n  test(\"findColumnPosition that doesn't exist\") {\n    val schema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", MapType(StringType, StringType))\n      .add(\"c\", ArrayType(IntegerType))\n    expectFailure(\"Couldn't find\", schema.treeString) {\n      SchemaUtils.findColumnPosition(Seq(\"d\"), schema)\n    }\n    expectFailure(\"A MapType was found\", \"mapType\", schema.treeString) {\n      SchemaUtils.findColumnPosition(Seq(\"b\", \"c\"), schema)\n    }\n    expectFailure(\"An ArrayType was found\", \"arrayType\", schema.treeString) {\n      SchemaUtils.findColumnPosition(Seq(\"c\", \"b\"), schema)\n    }\n  }\n\n  ////////////////////////////\n  // getNestedFieldFromPosition\n  ////////////////////////////\n\n  test(\"getNestedFieldFromPosition\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", IntegerType)\n    val c = StructField(\"c\", IntegerType)\n    val d = StructField(\"d\", IntegerType)\n    val e = StructField(\"e\", IntegerType)\n    val f = StructField(\"f\", IntegerType)\n    val g = StructField(\"g\", IntegerType)\n\n    val field = StructField(\"field\", StringType)\n    val struct = StructField(\"struct\", new StructType().add(a).add(b))\n    val arrayElement = StructField(\"element\", new StructType().add(c))\n    val array = StructField(\"array\", ArrayType(arrayElement.dataType))\n    val mapKey = StructField(\"key\", new StructType().add(d))\n    val mapValue = StructField(\"value\", new StructType().add(e))\n    val map = StructField(\"map\", MapType(\n      keyType = mapKey.dataType,\n      valueType = mapValue.dataType))\n    val arrayMapKey = StructField(\"key\", new StructType().add(f))\n    val arrayMapValue = StructField(\"value\", new StructType().add(g))\n    val arrayMapElement = StructField(\"element\", MapType(\n      keyType = arrayMapKey.dataType,\n      valueType = arrayMapValue.dataType))\n    val arrayMap = StructField(\"arrayMap\", ArrayType(arrayMapElement.dataType))\n\n    val root = StructField(\"root\", StructType(Seq(field, struct, array, map, arrayMap)))\n\n    val List(fieldIdx, structIdx, arrayIdx, mapIdx, arrayMapIdx) = (0 to 4).toList\n    val ARRAY_ELEMENT_INDEX = 0\n    val MAP_KEY_INDEX = 0\n    val MAP_VALUE_INDEX = 1\n\n    def checkField(position: Seq[Int], expected: StructField): Unit =\n      assert(getNestedFieldFromPosition(root, position) === expected)\n\n    checkField(Seq.empty, root)\n    checkField(Seq(fieldIdx), field)\n    checkField(Seq(structIdx), struct)\n    checkField(Seq(structIdx, 0), a)\n    checkField(Seq(structIdx, 1), b)\n    checkField(Seq(arrayIdx), array)\n    checkField(Seq(arrayIdx, ARRAY_ELEMENT_INDEX), arrayElement)\n    checkField(Seq(arrayIdx, ARRAY_ELEMENT_INDEX, 0), c)\n    checkField(Seq(mapIdx), map)\n    checkField(Seq(mapIdx, MAP_KEY_INDEX), mapKey)\n    checkField(Seq(mapIdx, MAP_VALUE_INDEX), mapValue)\n    checkField(Seq(mapIdx, MAP_KEY_INDEX, 0), d)\n    checkField(Seq(mapIdx, MAP_VALUE_INDEX, 0), e)\n    checkField(Seq(arrayMapIdx), arrayMap)\n    checkField(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX), arrayMapElement)\n    checkField(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_KEY_INDEX), arrayMapKey)\n    checkField(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_VALUE_INDEX), arrayMapValue)\n    checkField(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_KEY_INDEX, 0), f)\n    checkField(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_VALUE_INDEX, 0), g)\n\n    def checkError(position: Seq[Int]): Unit =\n      assertThrows[IllegalArgumentException] {\n        getNestedFieldFromPosition(root, position)\n      }\n\n    checkError(Seq(-1))\n    checkError(Seq(fieldIdx, 0))\n    checkError(Seq(structIdx, -1))\n    checkError(Seq(structIdx, 2))\n    checkError(Seq(arrayIdx, ARRAY_ELEMENT_INDEX - 1))\n    checkError(Seq(arrayIdx, ARRAY_ELEMENT_INDEX + 1))\n    checkError(Seq(mapIdx, MAP_KEY_INDEX - 1))\n    checkError(Seq(mapIdx, MAP_VALUE_INDEX + 1))\n    checkError(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX - 1))\n    checkError(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX + 1))\n    checkError(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_KEY_INDEX - 1))\n    checkError(Seq(arrayMapIdx, ARRAY_ELEMENT_INDEX, MAP_VALUE_INDEX + 1))\n    checkError(Seq(arrayMapIdx + 1))\n  }\n\n  test(\"getNestedTypeFromPosition\") {\n    val schema = new StructType().add(\"a\", IntegerType)\n    assert(getNestedTypeFromPosition(schema, Seq.empty) === schema)\n    assert(getNestedTypeFromPosition(schema, Seq(0)) === IntegerType)\n    assertThrows[IllegalArgumentException] {\n      getNestedTypeFromPosition(schema, Seq(-1))\n    }\n    assertThrows[IllegalArgumentException] {\n      getNestedTypeFromPosition(schema, Seq(1))\n    }\n  }\n\n  ////////////////////////////\n  // addColumn\n  ////////////////////////////\n\n  test(\"addColumn - simple\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", StringType)\n    val schema = new StructType().add(a).add(b)\n\n    val x = StructField(\"x\", LongType)\n    assert(SchemaUtils.addColumn(schema, x, Seq(0)) === new StructType().add(x).add(a).add(b))\n    assert(SchemaUtils.addColumn(schema, x, Seq(1)) === new StructType().add(a).add(x).add(b))\n    assert(SchemaUtils.addColumn(schema, x, Seq(2)) === new StructType().add(a).add(b).add(x))\n\n    expectFailure(\"Index -1\", \"lower than 0\") {\n      SchemaUtils.addColumn(schema, x, Seq(-1))\n    }\n    expectFailure(\"Index 3\", \"larger than struct length: 2\") {\n      SchemaUtils.addColumn(schema, x, Seq(3))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, 0))\n    }\n  }\n\n  test(\"addColumn - nested struct\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", StringType)\n    val first = StructField(\"first\", new StructType().add(a).add(b))\n    val middle = StructField(\"middle\", new StructType().add(a).add(b))\n    val last = StructField(\"last\", new StructType().add(a).add(b))\n    val schema = new StructType().add(first).add(middle).add(last)\n\n    val x = StructField(\"x\", LongType)\n    assert(SchemaUtils.addColumn(schema, x, Seq(0)) ===\n      new StructType().add(x).add(first).add(middle).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(1)) ===\n      new StructType().add(first).add(x).add(middle).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(2)) ===\n      new StructType().add(first).add(middle).add(x).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(3)) ===\n      new StructType().add(first).add(middle).add(last).add(x))\n\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, 2)) ===\n      new StructType().add(\"first\", new StructType().add(a).add(b).add(x)).add(middle).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, 1)) ===\n      new StructType().add(\"first\", new StructType().add(a).add(x).add(b)).add(middle).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, 0)) ===\n      new StructType().add(\"first\", new StructType().add(x).add(a).add(b)).add(middle).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(1, 0)) ===\n      new StructType().add(first).add(\"middle\", new StructType().add(x).add(a).add(b)).add(last))\n    assert(SchemaUtils.addColumn(schema, x, Seq(2, 0)) ===\n      new StructType().add(first).add(middle).add(\"last\", new StructType().add(x).add(a).add(b)))\n\n    expectFailure(\"Index -1\", \"lower than 0\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, -1))\n    }\n    expectFailure(\"Index 3\", \"larger than struct length: 2\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, 3))\n    }\n    expectFailure(\"Struct not found at position 2\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, 2, 0))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, 0, 0))\n    }\n  }\n\n  test(\"addColumn - nested map\") {\n    val k = StructField(\"k\", IntegerType)\n    val v = StructField(\"v\", StringType)\n    val schema = new StructType().add(\"m\", MapType(\n      keyType = new StructType().add(k),\n      valueType = new StructType().add(v)))\n\n    val MAP_KEY_INDEX = 0\n    val MAP_VALUE_INDEX = 1\n\n    val x = StructField(\"x\", LongType)\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, MAP_KEY_INDEX, 0)) ===\n      new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(x).add(k),\n        valueType = new StructType().add(v))))\n\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, MAP_KEY_INDEX, 1)) ===\n      new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(k).add(x),\n        valueType = new StructType().add(v))))\n\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, MAP_VALUE_INDEX, 0)) ===\n      new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(k),\n        valueType = new StructType().add(x).add(v))))\n\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, MAP_VALUE_INDEX, 1)) ===\n      new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(k),\n        valueType = new StructType().add(v).add(x))))\n\n    // Adding to map key/value.\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, MAP_KEY_INDEX))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, MAP_VALUE_INDEX))\n    }\n    // Invalid map access.\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, MAP_KEY_INDEX - 1, 0))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, MAP_VALUE_INDEX + 1, 0))\n    }\n  }\n\n  test(\"addColumn - nested maps\") {\n    // Helper method to create a 2-level deep nested map of structs. The tests below each cover\n    // adding a field to one of the leaf struct.\n    def schema(\n        kk: StructType = new StructType().add(\"kk\", IntegerType),\n        kv: StructType = new StructType().add(\"kv\", IntegerType),\n        vk: StructType = new StructType().add(\"vk\", IntegerType),\n        vv: StructType = new StructType().add(\"vv\", IntegerType))\n      : StructType = new StructType().add(\"m\", MapType(\n        keyType = MapType(\n          keyType = kk,\n          valueType = kv),\n        valueType = MapType(\n          keyType = vk,\n          valueType = vv)))\n\n    val MAP_KEY_INDEX = 0\n    val MAP_VALUE_INDEX = 1\n\n    val x = StructField(\"x\", LongType)\n    // Add field `x` at the front of each leaf struct.\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX, 0)) ===\n      schema(kk = new StructType().add(x).add(\"kk\", IntegerType)))\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_VALUE_INDEX, MAP_KEY_INDEX, 0)) ===\n      schema(vk = new StructType().add(x).add(\"vk\", IntegerType)))\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX, 0)) ===\n      schema(kv = new StructType().add(x).add(\"kv\", IntegerType)))\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_VALUE_INDEX, MAP_VALUE_INDEX, 0)) ===\n      schema(vv = new StructType().add(x).add(\"vv\", IntegerType)))\n\n    // Add field `x` at the back of each leaf struct.\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX, 1)) ===\n      schema(kk = new StructType().add(\"kk\", IntegerType).add(x)))\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_VALUE_INDEX, MAP_KEY_INDEX, 1)) ===\n      schema(vk = new StructType().add(\"vk\", IntegerType).add(x)))\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX, 1)) ===\n      schema(kv = new StructType().add(\"kv\", IntegerType).add(x)))\n    assert(SchemaUtils.addColumn(schema(), x, Seq(0, MAP_VALUE_INDEX, MAP_VALUE_INDEX, 1)) ===\n      schema(vv = new StructType().add(\"vv\", IntegerType).add(x)))\n\n    // Adding to map key/value.\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX))\n    }\n    // Invalid map access.\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX - 1, 0))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX - 1, MAP_KEY_INDEX, 0))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema(), x, Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX + 1, 0))\n    }\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema(), x, Seq(0, MAP_VALUE_INDEX + 1, MAP_KEY_INDEX, 0))\n    }\n  }\n\n  test(\"addColumn - nested array\") {\n    val e = StructField(\"e\", IntegerType)\n    val schema = new StructType().add(\"a\", ArrayType(new StructType().add(e)))\n    val x = StructField(\"x\", LongType)\n\n    val ARRAY_ELEMENT_INDEX = 0\n\n    // Add field `x` at the front of the leaf struct.\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, 0)) ===\n      new StructType().add(\"a\", ArrayType(new StructType().add(x).add(e))))\n    // Add field `x` at the back of the leaf struct.\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, 1)) ===\n      new StructType().add(\"a\", ArrayType(new StructType().add(e).add(x))))\n\n    // Adding to array element.\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX))\n    }\n    // Invalid array access.\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX - 1, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX + 1, 0))\n    }\n  }\n\n  test(\"addColumn - nested arrays\") {\n    val e = StructField(\"e\", IntegerType)\n    val schema = new StructType().add(\"a\", ArrayType(ArrayType(new StructType().add(e))))\n    val x = StructField(\"x\", LongType)\n\n    val ARRAY_ELEMENT_INDEX = 0\n\n    // Add field `x` at the front of the leaf struct.\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX, 0)) ===\n      new StructType().add(\"a\", ArrayType(ArrayType(new StructType().add(x).add(e)))))\n    // Add field `x` at the back of the leaf struct.\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX, 1)) ===\n      new StructType().add(\"a\", ArrayType(ArrayType(new StructType().add(e).add(x)))))\n\n    // Adding to array element.\n    expectFailure(\"parent is not a structtype\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX))\n    }\n    // Invalid array access.\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX - 1, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX - 1, ARRAY_ELEMENT_INDEX, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX + 1, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.addColumn(schema, x, Seq(0, ARRAY_ELEMENT_INDEX + 1, ARRAY_ELEMENT_INDEX, 0))\n    }\n  }\n\n  test(\"addColumn - top level array\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", StringType)\n    val schema = ArrayType(new StructType().add(a).add(b))\n\n    val x = StructField(\"x\", LongType)\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, 1)) ===\n      ArrayType(new StructType().add(a).add(x).add(b)))\n  }\n\n  test(\"addColumn - top level map\") {\n    val k = StructField(\"k\", IntegerType)\n    val v = StructField(\"v\", StringType)\n    val schema = MapType(\n      keyType = new StructType().add(k),\n      valueType = new StructType().add(v))\n\n    val x = StructField(\"x\", LongType)\n    assert(SchemaUtils.addColumn(schema, x, Seq(0, 1)) ===\n      MapType(\n        keyType = new StructType().add(k).add(x),\n        valueType = new StructType().add(v)))\n\n    assert(SchemaUtils.addColumn(schema, x, Seq(1, 1)) ===\n      MapType(\n        keyType = new StructType().add(k),\n        valueType = new StructType().add(v).add(x)))\n  }\n\n  ////////////////////////////\n  // dropColumn\n  ////////////////////////////\n\n  test(\"dropColumn - simple\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", StringType)\n    val schema = new StructType().add(a).add(b)\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0)) === ((new StructType().add(b), a)))\n    assert(SchemaUtils.dropColumn(schema, Seq(1)) === ((new StructType().add(a), b)))\n\n    expectFailure(\"Index -1\", \"lower than 0\") {\n      SchemaUtils.dropColumn(schema, Seq(-1))\n    }\n    expectFailure(\"Index 2\", \"equals to or is larger than struct length: 2\") {\n      SchemaUtils.dropColumn(schema, Seq(2))\n    }\n    expectFailure(\"Can only drop nested columns from StructType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, 0))\n    }\n  }\n\n  test(\"dropColumn - nested struct\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", StringType)\n    val c = StructField(\"c\", StringType)\n    val first = StructField(\"first\", new StructType().add(a).add(b).add(c))\n    val middle = StructField(\"middle\", new StructType().add(a).add(b).add(c))\n    val last = StructField(\"last\", new StructType().add(a).add(b).add(c))\n    val schema = new StructType().add(first).add(middle).add(last)\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0)) ===\n      new StructType().add(middle).add(last) -> first)\n    assert(SchemaUtils.dropColumn(schema, Seq(1)) ===\n      new StructType().add(first).add(last) -> middle)\n    assert(SchemaUtils.dropColumn(schema, Seq(2)) ===\n      new StructType().add(first).add(middle) -> last)\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, 2)) ===\n      new StructType().add(\"first\", new StructType().add(a).add(b)).add(middle).add(last) -> c)\n    assert(SchemaUtils.dropColumn(schema, Seq(0, 1)) ===\n      new StructType().add(\"first\", new StructType().add(a).add(c)).add(middle).add(last) -> b)\n    assert(SchemaUtils.dropColumn(schema, Seq(0, 0)) ===\n      new StructType().add(\"first\", new StructType().add(b).add(c)).add(middle).add(last) -> a)\n    assert(SchemaUtils.dropColumn(schema, Seq(1, 0)) ===\n      new StructType().add(first).add(\"middle\", new StructType().add(b).add(c)).add(last) -> a)\n    assert(SchemaUtils.dropColumn(schema, Seq(2, 0)) ===\n      new StructType().add(first).add(middle).add(\"last\", new StructType().add(b).add(c)) -> a)\n\n    expectFailure(\"Index -1\", \"lower than 0\") {\n      SchemaUtils.dropColumn(schema, Seq(0, -1))\n    }\n    expectFailure(\"Index 3\", \"equals to or is larger than struct length: 3\") {\n      SchemaUtils.dropColumn(schema, Seq(0, 3))\n    }\n    expectFailure(\"Can only drop nested columns from StructType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, 0, 0))\n    }\n  }\n\n  test(\"dropColumn - nested map\") {\n    val a = StructField(\"a\", IntegerType)\n    val b = StructField(\"b\", StringType)\n    val c = StructField(\"c\", LongType)\n    val d = StructField(\"d\", DateType)\n    val schema = new StructType().add(\"m\", MapType(\n      keyType = new StructType().add(a).add(b),\n      valueType = new StructType().add(c).add(d)))\n\n    val MAP_KEY_INDEX = 0\n    val MAP_VALUE_INDEX = 1\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, MAP_KEY_INDEX, 0)) ===\n      (new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(b),\n        valueType = new StructType().add(c).add(d))),\n      a))\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, MAP_KEY_INDEX, 1)) ===\n      (new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(a),\n        valueType = new StructType().add(c).add(d))),\n      b))\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, MAP_VALUE_INDEX, 0)) ===\n      (new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(a).add(b),\n        valueType = new StructType().add(d))),\n      c))\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, MAP_VALUE_INDEX, 1)) ===\n      (new StructType().add(\"m\", MapType(\n        keyType = new StructType().add(a).add(b),\n        valueType = new StructType().add(c))),\n      d))\n\n    // Dropping map key/value.\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema, Seq(0, MAP_KEY_INDEX))\n    }\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema, Seq(0, MAP_VALUE_INDEX))\n    }\n    // Invalid map access.\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema, Seq(0, MAP_KEY_INDEX - 1, 0))\n    }\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema, Seq(0, MAP_VALUE_INDEX + 1, 0))\n    }\n  }\n\n  test(\"dropColumn - nested maps\") {\n    // Helper method to create a 2-level deep nested map of structs. The tests below each cover\n    // dropping a field to one of the leaf struct. Each test adds an extra field `a` at a specific\n    // position then drops it to end up with the default schema returned by `schema()`\n    def schema(\n        kk: StructType = new StructType().add(\"kk\", IntegerType),\n        kv: StructType = new StructType().add(\"kv\", IntegerType),\n        vk: StructType = new StructType().add(\"vk\", IntegerType),\n        vv: StructType = new StructType().add(\"vv\", IntegerType))\n      : StructType = new StructType().add(\"m\", MapType(\n        keyType = MapType(\n          keyType = kk,\n          valueType = kv),\n        valueType = MapType(\n          keyType = vk,\n          valueType = vv)))\n\n    val a = StructField(\"a\", LongType)\n\n    val MAP_KEY_INDEX = 0\n    val MAP_VALUE_INDEX = 1\n\n    def checkDrop(initialSchema: StructType, position: Seq[Int]): Unit =\n      assert(SchemaUtils.dropColumn(initialSchema, position) === (schema(), a))\n    // Drop field `a` from the front of each leaf struct.\n    checkDrop(\n      initialSchema = schema(kk = new StructType().add(a).add(\"kk\", IntegerType)),\n      position = Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX, 0))\n\n    checkDrop(\n      initialSchema = schema(kv = new StructType().add(a).add(\"kv\", IntegerType)),\n      position = Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX, 0))\n\n    checkDrop(\n      initialSchema = schema(vk = new StructType().add(a).add(\"vk\", IntegerType)),\n      position = Seq(0, MAP_VALUE_INDEX, MAP_KEY_INDEX, 0))\n\n    checkDrop(\n      initialSchema = schema(vv = new StructType().add(a).add(\"vv\", IntegerType)),\n      position = Seq(0, MAP_VALUE_INDEX, MAP_VALUE_INDEX, 0))\n\n    // Drop field `a` from the back of each leaf struct.\n    checkDrop(\n      initialSchema = schema(kk = new StructType().add(\"kk\", IntegerType).add(a)),\n      position = Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX, 1))\n\n    checkDrop(\n      initialSchema = schema(kv = new StructType().add(\"kv\", IntegerType).add(a)),\n      position = Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX, 1))\n\n    checkDrop(\n      initialSchema = schema(vk = new StructType().add(\"vk\", IntegerType).add(a)),\n      position = Seq(0, MAP_VALUE_INDEX, MAP_KEY_INDEX, 1))\n\n    checkDrop(\n      initialSchema = schema(vv = new StructType().add(\"vv\", IntegerType).add(a)),\n      position = Seq(0, MAP_VALUE_INDEX, MAP_VALUE_INDEX, 1))\n\n    // Dropping map key/value.\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema(), Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX))\n    }\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema(), Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX))\n    }\n    // Invalid map access.\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema(), Seq(0, MAP_KEY_INDEX, MAP_KEY_INDEX - 1, 0))\n    }\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema(), Seq(0, MAP_KEY_INDEX - 1, MAP_KEY_INDEX, 0))\n    }\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema(), Seq(0, MAP_KEY_INDEX, MAP_VALUE_INDEX + 1, 0))\n    }\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema(), Seq(0, MAP_VALUE_INDEX + 1, MAP_KEY_INDEX, 0))\n    }\n  }\n\n  test(\"dropColumn - nested array\") {\n    val e = StructField(\"e\", IntegerType)\n    val f = StructField(\"f\", IntegerType)\n    val schema = new StructType().add(\"a\", ArrayType(new StructType().add(e).add(f)))\n\n    val ARRAY_ELEMENT_INDEX = 0\n\n    // Drop field from the front of the leaf struct.\n    assert(SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, 0)) ===\n      (new StructType().add(\"a\", ArrayType(new StructType().add(f))), e))\n    // Drop field from the back of the leaf struct.\n    assert(SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, 1)) ===\n      (new StructType().add(\"a\", ArrayType(new StructType().add(e))), f))\n\n    // Dropping array element.\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX))\n    }\n    // Invalid array access.\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX - 1, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX + 1, 0))\n    }\n  }\n\n  test(\"dropColumn - nested arrays\") {\n    val e = StructField(\"e\", IntegerType)\n    val f = StructField(\"f\", IntegerType)\n    val schema = new StructType().add(\"a\", ArrayType(ArrayType(new StructType().add(e).add(f))))\n\n    val ARRAY_ELEMENT_INDEX = 0\n\n    // Drop field `x` from the front of the leaf struct.\n    assert(SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX, 0)) ===\n      (new StructType().add(\"a\", ArrayType(ArrayType(new StructType().add(f)))), e))\n    // Drop field `x` from the back of the leaf struct.\n    assert(SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX, 1)) ===\n      (new StructType().add(\"a\", ArrayType(ArrayType(new StructType().add(e)))), f))\n\n    // Dropping array element.\n    expectFailure(\"can only drop nested columns from structtype\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX))\n    }\n    // Invalid array access.\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX - 1, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX - 1, ARRAY_ELEMENT_INDEX, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX, ARRAY_ELEMENT_INDEX + 1, 0))\n    }\n    expectFailure(\"Incorrectly accessing an ArrayType\") {\n      SchemaUtils.dropColumn(schema, Seq(0, ARRAY_ELEMENT_INDEX + 1, ARRAY_ELEMENT_INDEX, 0))\n    }\n  }\n\n  test(\"dropColumn - top level array\") {\n    val schema = ArrayType(new StructType().add(\"a\", IntegerType).add(\"b\", StringType))\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, 0))._1 ===\n      ArrayType(new StructType().add(\"b\", StringType)))\n  }\n\n  test(\"dropColumn - top level map\") {\n    val schema = MapType(\n      keyType = new StructType().add(\"k\", IntegerType).add(\"k2\", StringType),\n      valueType = new StructType().add(\"v\", StringType).add(\"v2\", StringType))\n\n    assert(SchemaUtils.dropColumn(schema, Seq(0, 0))._1 ===\n      MapType(\n        keyType = new StructType().add(\"k2\", StringType),\n        valueType = new StructType().add(\"v\", StringType).add(\"v2\", StringType)))\n\n    assert(SchemaUtils.dropColumn(schema, Seq(1, 0))._1 ===\n      MapType(\n        keyType = new StructType().add(\"k\", IntegerType).add(\"k2\", StringType),\n        valueType = new StructType().add(\"v2\", StringType)))\n  }\n\n  /////////////////////////////////\n  // normalizeColumnNamesInDataType\n  /////////////////////////////////\n  private def runNormalizeColumnNamesInDataType(\n      sourceDataType: DataType,\n      tableDataType: DataType): DataType = {\n    normalizeColumnNamesInDataType(\n      deltaLog = null,\n      sourceDataType,\n      tableDataType,\n      sourceParentFields = Seq.empty,\n      tableSchema = new StructType())\n  }\n\n  test(\"normalize column names in data type - top-level atomic types\") {\n    val source = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType)\n      .add(\"c\", LongType)\n      .add(\"d\", DateType)\n    val table = new StructType()\n      .add(\"B\", StringType)\n      .add(\"A\", LongType) // LongType != IntegerType\n      .add(\"D\", DecimalType(10, 0)) // DecimalType != DateType\n      .add(\"C\", StringType) // StringType != LongType\n    val expected = new StructType()\n      .add(\"A\", IntegerType)\n      .add(\"B\", StringType)\n      .add(\"C\", LongType)\n      .add(\"D\", DateType)\n    assert(runNormalizeColumnNamesInDataType(source, table) == expected)\n  }\n\n  test(\"normalize column names in data type - incompatible top-level types\") {\n    val schema1a = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType)\n    val schema1b = new StructType()\n      .add(\"B\", StringType)\n      .add(\"A\", new StructType()) // StructType != IntegerType\n    intercept[AssertionError] {\n      runNormalizeColumnNamesInDataType(schema1a, schema1b)\n    }\n    intercept[AssertionError] {\n      runNormalizeColumnNamesInDataType(schema1b, schema1a)\n    }\n\n    val schema2a = new StructType()\n      .add(\"x\", StringType)\n      .add(\"y\", new StructType()\n        .add(\"z\", IntegerType)\n      )\n    val schema2b = new StructType()\n      .add(\"x\", StringType)\n      .add(\"Y\", new StructType()\n        .add(\"z\", ArrayType(IntegerType)) // ArrayType != IntegerType\n      )\n    intercept[AssertionError] {\n      runNormalizeColumnNamesInDataType(schema2a, schema2b)\n    }\n    intercept[AssertionError] {\n      runNormalizeColumnNamesInDataType(schema2b, schema2a)\n    }\n  }\n\n  test(\"normalize column names in data type - nested structs\") {\n    val source = new StructType()\n      .add(\"a1\", IntegerType)\n      .add(\"a2\", new StructType()\n        .add(\"b1\", IntegerType)\n        .add(\"b2\", new StructType()\n          .add(\"c1\", IntegerType)\n          .add(\"c2\", LongType)\n        )\n        .add(\"b3\", LongType)\n      )\n      .add(\"a3\", new StructType()\n        .add(\"d1\", IntegerType)\n        .add(\"d2\", LongType)\n      )\n    val table = new StructType()\n      .add(\"A3\", new StructType()\n        .add(\"D2\", LongType)\n        .add(\"D3x\", StringType)\n        .add(\"D1\", IntegerType)\n      )\n      .add(\"A2\", new StructType()\n        .add(\"B3\", LongType)\n        .add(\"B4x\", IntegerType)\n        .add(\"B1\", IntegerType)\n        .add(\"B2\", new StructType()\n          .add(\"C3\", LongType)\n          .add(\"C2\", LongType)\n          .add(\"C1\", IntegerType)\n        )\n      )\n      .add(\"A4x\", StringType)\n      .add(\"A1\", IntegerType)\n    val expected = new StructType()\n      .add(\"A1\", IntegerType)\n      .add(\"A2\", new StructType()\n        .add(\"B1\", IntegerType)\n        .add(\"B2\", new StructType()\n          .add(\"C1\", IntegerType)\n          .add(\"C2\", LongType))\n        .add(\"B3\", LongType)\n      )\n      .add(\"A3\", new StructType()\n        .add(\"D1\", IntegerType)\n        .add(\"D2\", LongType)\n      )\n    assert(runNormalizeColumnNamesInDataType(source, table) == expected)\n  }\n\n  test(\"normalize column names in data type - different atomic types in a map\") {\n    val source = new StructType()\n      .add(\"a\", new StructType()\n      .add(\"b\", new StructType()\n      .add(\"c\", MapType(StringType, IntegerType))))\n    val table = new StructType()\n      .add(\"A\", new StructType()\n      .add(\"B\", new StructType()\n      .add(\"C\", MapType(IntegerType, StringType))))\n    val expected = new StructType()\n      .add(\"A\", new StructType()\n      .add(\"B\", new StructType()\n      .add(\"C\", MapType(StringType, IntegerType))))\n    assert(runNormalizeColumnNamesInDataType(source, table) == expected)\n  }\n\n  test(\"normalize column names in data type - incompatible nested types\") {\n    val schema1 = new StructType()\n      .add(\"a\", new StructType()\n      .add(\"b\", new StructType()\n      .add(\"c\", IntegerType)))\n    val schema2 = new StructType()\n      .add(\"A\", new StructType()\n      .add(\"B\", new StructType()\n      .add(\"C\", ArrayType(IntegerType))))\n    val schema3 = new StructType()\n      .add(\"A\", new StructType()\n      .add(\"b\", new StructType()\n      .add(\"C\", new StructType())))\n    val schemas = Seq(schema1, schema2, schema3)\n\n    for (left <- schemas; right <- schemas) {\n      if (left == right) {\n        // Make sure there's no error when the schemas are the same.\n        assert(runNormalizeColumnNamesInDataType(left, right) == left)\n      } else {\n        intercept[AssertionError] {\n          runNormalizeColumnNamesInDataType(left, right)\n        }\n      }\n    }\n  }\n\n  test(\"normalize column names in data type - arrays, maps, structs\") {\n    val source = MapType(\n      new StructType()\n        .add(\"aa\", IntegerType)\n        .add(\"bb\", StringType),\n      ArrayType(new StructType()\n        .add(\"aa\", IntegerType)\n        .add(\"bb\", StringType)))\n    val table = MapType(\n      new StructType()\n        .add(\"aA\", IntegerType)\n        .add(\"bB\", StringType),\n      ArrayType(new StructType()\n        .add(\"Cc\", IntegerType)\n        .add(\"Aa\", IntegerType)\n        .add(\"Bb\", StringType)))\n    val expected = MapType(\n      new StructType()\n        .add(\"aA\", IntegerType)\n        .add(\"bB\", StringType),\n      ArrayType(new StructType()\n        .add(\"Aa\", IntegerType)\n        .add(\"Bb\", StringType)))\n    assert(runNormalizeColumnNamesInDataType(source, table) == expected)\n  }\n\n  test(\"normalize column names in data type - missing column\") {\n    val source = ArrayType(\n      new StructType()\n        .add(\"aa\", IntegerType)\n        .add(\"bb\", StringType)\n    )\n    val target = ArrayType(\n      new StructType()\n        .add(\"AA\", IntegerType)\n        .add(\"CC\", StringType) // \"bb\" != \"CC\"\n    )\n    val exception = intercept[DeltaAnalysisException] {\n      normalizeColumnNamesInDataType(deltaLog = null, source, target,\n        Seq(\"x\", \"Y\"), new StructType())\n    }\n    checkError(\n      exception,\n      \"DELTA_CANNOT_RESOLVE_COLUMN\",\n      sqlState = \"42703\",\n      parameters = Map(\"columnName\" -> \"x.Y.bb\", \"schema\" -> \"root\\n\")\n    )\n  }\n\n  test(\"normalize column names in data type - preserve nullability and comments\") {\n    val source = new StructType()\n      .add(\"a1\", IntegerType, nullable = true)\n      .add(\"a2\", new StructType()\n        .add(\"b1\", IntegerType, nullable = false)\n        .add(\"b2\", ArrayType(IntegerType, containsNull = true),\n          nullable = true, comment = \"comment for b2\")\n        .add(\"b3\", MapType(IntegerType, StringType, valueContainsNull = false),\n          nullable = true, comment = \"comment for b3\"),\n        nullable = false, comment = \"comment for a2\"\n      )\n    val table = new StructType()\n      .add(\"A1\", IntegerType, nullable = false, \"comment for A1\")\n      .add(\"A2\", new StructType()\n        .add(\"B1\", IntegerType, nullable = true)\n        .add(\"B2\", ArrayType(IntegerType, containsNull = false),\n          nullable = false, comment = \"comment for B2\")\n        .add(\"B3\", MapType(IntegerType, StringType, valueContainsNull = true),\n          nullable = false, comment = \"comment for B3\"),\n        nullable = true\n      )\n    val expected = new StructType()\n      .add(\"A1\", IntegerType, nullable = true)\n      .add(\"A2\", new StructType()\n        .add(\"B1\", IntegerType, nullable = false)\n        .add(\"B2\", ArrayType(IntegerType, containsNull = true),\n          nullable = true, comment = \"comment for b2\")\n        .add(\"B3\", MapType(IntegerType, StringType, valueContainsNull = false),\n          nullable = true, comment = \"comment for b3\"),\n        nullable = false, comment = \"comment for a2\"\n      )\n    assert(runNormalizeColumnNamesInDataType(source, table) == expected)\n  }\n\n  test(\"normalize column names in data type - empty source struct\") {\n    val source = new StructType()\n    val table = new StructType().add(\"a\", IntegerType)\n    val expected = new StructType()\n    assert(runNormalizeColumnNamesInDataType(source, table) == expected)\n  }\n\n  ////////////////////////////\n  // normalizeColumnNames\n  ////////////////////////////\n\n  /**\n   * SchemaUtils.normalizeColumnNames() introduces a Project operator where for each of the\n   * top-level columns:\n   * - If a top-level field name differs from the table schema, we correct it using an Alias.\n   * - If a nested field name differs from the table schema, we correct it using a Cast.\n   * This function verifies that the Casts are only introduced for the correct subset of top-level\n   * columns.\n   */\n  private def verifyColumnsWithCasts(df: DataFrame, columnsWithCasts: Seq[String]): Unit = {\n    @tailrec def isCast(expression: Expression): Boolean = expression match {\n      case _: Cast => true\n      case Alias(child, _) => isCast(child)\n      case _ => false\n    }\n\n    val plan = df.queryExecution.analyzed\n    val projections = plan.asInstanceOf[Project].projectList\n    for (projection <- projections) {\n      val expectedIsCast = columnsWithCasts.contains(projection.name)\n      val actualIsCast = isCast(projection)\n      assert(expectedIsCast === actualIsCast, s\"Verifying cast for ${projection.name}\")\n    }\n  }\n\n  test(\"normalize column names - different top-level ordering\") {\n    val df = Seq((1, 2, 3)).toDF(\"def\", \"gHi\", \"abC\")\n    val tableSchema = new StructType()\n      .add(\"abc\", IntegerType)\n      .add(\"Def\", IntegerType)\n      .add(\"ghi\", IntegerType)\n      // Add an extra column to the table schema to make sure it is not added, and does not cause\n      // an error.\n      .add(\"jkl\", StringType)\n    val expectedSchema = new StructType()\n      .add(\"Def\", IntegerType, false)\n      .add(\"ghi\", IntegerType, false)\n      .add(\"abc\", IntegerType, false)\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq.empty)\n    assert(normalized.schema == expectedSchema)\n  }\n\n  test(\"normalize column names - dots in the name\") {\n    val df = spark.read.json(Seq(\"\"\"{\"a.b\":1,\"c.d\":{\"x.y\":2, \"y.z\":1}}\"\"\").toDS())\n    val tableSchema = new StructType()\n      .add(\"c.D\", new StructType()\n        .add(\"y.z\", LongType)\n        .add(\"x.Y\", LongType)\n      )\n      .add(\"a.B\", LongType)\n    val expectedSchema = new StructType()\n      .add(\"a.B\", LongType, nullable = true)\n      .add(\"c.D\", new StructType()\n        .add(\"x.Y\", LongType, nullable = true)\n        .add(\"y.z\", LongType, nullable = true),\n        nullable = true\n      )\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq(\"c.D\"))\n    assert(normalized.schema === expectedSchema)\n  }\n\n  test(\"normalize column names - different case in struct\") {\n    // JSON schema inference does not preserve the order of columns, so we need an explicit schema.\n    val jsonSchema = new StructType()\n      .add(\"b\", new StructType()\n        .add(\"x\", LongType)\n        .add(\"y\", new StructType()\n          .add(\"T\", LongType)\n          .add(\"s\", LongType)\n        )\n      )\n      .add(\"a\", LongType)\n    val df = spark.read.schema(jsonSchema)\n      .json(Seq(\"\"\"{\"b\":{\"x\":1,\"y\":{\"T\":2, \"s\":1}}, \"a\":1}\"\"\").toDS())\n    val tableSchema = new StructType()\n      .add(\"a\", LongType)\n      .add(\"b\", new StructType()\n        .add(\"x\", LongType)\n        .add(\"y\", new StructType()\n          .add(\"s\", LongType)\n          .add(\"t\", LongType)\n        )\n      )\n    val expectedSchema = new StructType()\n      .add(\"b\", new StructType()\n        .add(\"x\", LongType)\n        .add(\"y\", new StructType()\n          .add(\"t\", LongType)\n          .add(\"s\", LongType)\n        )\n      )\n      .add(\"a\", LongType)\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq(\"b\"))\n    assert(normalized.schema === expectedSchema)\n  }\n\n  test(\"normalize column names - different case in array\") {\n    val df = spark.read.json(Seq(\"\"\"{\"X\":1,\"y\":[{\"Z\": \"alpha\"},{\"Z\":\"beta\"}]}\"\"\").toDS())\n    val tableSchema = new StructType()\n      .add(\"x\", LongType)\n      .add(\"y\", ArrayType(new StructType().add(\"z\", StringType)))\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq(\"y\"))\n    assert(normalized.schema == tableSchema)\n  }\n\n  test(\"normalize column names - different case in map\") {\n    val sourceMapType = MapType(StringType, new StructType().add(\"Z\", StringType))\n    val df = spark.range(1).toDF(\"X\")\n      .withColumn(\"y\", lit(null).cast(sourceMapType))\n      .select(col(\"y\"), col(\"X\"))\n    val tableSchema = new StructType()\n      .add(\"x\", LongType)\n      .add(\"y\", MapType(StringType, new StructType()\n        .add(\"z\", StringType)\n        // Add an extra nested column to the table schema to make sure it is not added.\n        .add(\"v\", IntegerType)))\n    val expectedSchema = new StructType()\n      .add(\"y\", MapType(StringType, new StructType().add(\"z\", StringType)))\n      .add(\"x\", LongType, nullable = false)\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq(\"y\"))\n    assert(normalized.schema === expectedSchema)\n  }\n\n  test(\"normalize column names - maintain nested column order\") {\n    val sourceStructColumnNames =\n      Seq(\"the\", \"quick\", \"brown\", \"fox\", \"jumps\", \"over\", \"them\", \"lazy\", \"dog\")\n    val sourceStructType = new StructType(\n      sourceStructColumnNames.map(StructField(_, IntegerType)).toArray)\n\n    // Nested columns in the table are all name in upper case, and listed in reverse order.\n    val tableStructColumnNames = Seq(\"LOOK\") ++\n      sourceStructColumnNames.reverse.map(_.toUpperCase(Locale.ROOT))\n    val tableSchema = new StructType()\n      .add(\"s\", new StructType(\n        tableStructColumnNames.map(StructField(_, IntegerType)).toArray))\n\n    // We expect the columns to maintain the same order as the source.\n    val expectedStructColumnNames = sourceStructColumnNames.map(_.toUpperCase(Locale.ROOT))\n    val expectedSchema = new StructType()\n      .add(\"s\", new StructType(\n        expectedStructColumnNames.map(StructField(_, IntegerType)).toArray))\n\n    val df = spark.range(1).toDF(\"id\")\n      .select(lit(null).cast(sourceStructType).as(\"s\"))\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq(\"s\"))\n    assert(normalized.schema === expectedSchema)\n  }\n\n  test(\"normalize column names - only top-level names of complex columns differ\") {\n    val structType = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType)\n    val mapType = MapType(structType, structType)\n    val arrayType = ArrayType(structType)\n\n    val df = spark.range(1).toDF(\"id\")\n      .select(lit(null).cast(structType).as(\"x\"),\n        lit(null).cast(mapType).as(\"y\"),\n        lit(null).cast(arrayType).as(\"z\"))\n    val tableSchema = new StructType()\n      .add(\"X\", structType)\n      .add(\"Y\", mapType)\n      .add(\"Z\", arrayType)\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    // If only top-level names differ, there is no need to cast complex types.\n    verifyColumnsWithCasts(normalized, Seq.empty)\n    assert(normalized.schema === tableSchema)\n  }\n\n  test(\"normalize column names - unmatched top-level column\") {\n    val df = spark.range(1).toDF(\"id\")\n      .select(lit(1L).as(\"one\"), lit(2L).as(\"two\"))\n    val tableSchema = new StructType()\n      .add(\"ONE\", LongType)\n      .add(\"THREE\", LongType)\n    val exception = intercept[DeltaAnalysisException] {\n      normalizeColumnNames(\n        deltaLog = null,\n        tableSchema,\n        df\n      )\n    }\n    checkError(\n      exception,\n      \"DELTA_CANNOT_RESOLVE_COLUMN\",\n      sqlState = \"42703\",\n      parameters = Map(\"columnName\" -> \"two\", \"schema\" -> tableSchema.treeString)\n    )\n  }\n\n  test(\"normalize column names - unmatched nested column\") {\n    val sourceStructType = new StructType()\n      .add(\"one\", LongType)\n      .add(\"two\", LongType)\n    val df = spark.range(1).toDF(\"id\")\n      .select(lit(null).cast(sourceStructType).as(\"s\"))\n    val tableSchema = new StructType()\n      .add(\"S\", new StructType()\n        .add(\"ONE\", LongType)\n        .add(\"THREE\", LongType)\n      )\n    val exception = intercept[DeltaAnalysisException] {\n      normalizeColumnNames(\n        deltaLog = null,\n        tableSchema,\n        df\n      )\n    }\n    checkError(\n      exception,\n      \"DELTA_CANNOT_RESOLVE_COLUMN\",\n      sqlState = \"42703\",\n      parameters = Map(\"columnName\" -> \"s.two\", \"schema\" -> tableSchema.treeString)\n    )\n  }\n\n  test(\"normalize column names - deeply nested schema\") {\n    // The only difference is the case of the most deeply nested column.\n    val structTypes = Seq(\"z\", \"Z\").map { finalColumnName =>\n      new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", MapType(StringType, new StructType()\n          .add(\"c\", IntegerType)\n          .add(\"d\", new StructType()\n            .add(\"e\", IntegerType)\n            .add(\"f\", IntegerType)\n            .add(\"g\", ArrayType(new StructType()\n              .add(\"h\", IntegerType)\n              .add(\"i\", IntegerType)\n              .add(\"j\", new StructType()\n                .add(\"k\", MapType(StringType, new StructType()\n                  .add(\"l\", ArrayType(new StructType()\n                    .add(\"m\", IntegerType)\n                    .add(finalColumnName, IntegerType)\n      ))))))))))\n    }.toArray\n\n    val sourceStructType = structTypes(0)\n    val df = spark.range(1).toDF(\"id\")\n      .select(lit(null).cast(sourceStructType).as(\"s\"))\n    val tableStructType = structTypes(1)\n    val tableSchema = new StructType()\n      .add(\"s\", tableStructType)\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq(\"s\"))\n    assert(normalized.schema === tableSchema)\n  }\n\n  test(\"normalize column names - can normalize row id column\") {\n    withTable(\"src\") {\n      spark.range(3).toDF(\"id\").write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .option(\"delta.enableRowTracking\", \"true\")\n        .saveAsTable(\"src\")\n\n      val df = spark.read.format(\"delta\").table(\"src\")\n        .select(\n          col(\"*\"),\n          col(\"_metadata.row_id\").as(\"row_id\")\n        )\n        .withMetadata(\n          \"row_id\",\n          RowId.RowIdMetadataStructField.metadata(\"name\", shouldSetIcebergReservedFieldId = false)\n        )\n\n      val tableSchema = new StructType().add(\"id\", LongType)\n      val normalized =\n        normalizeColumnNames(deltaLog = null, tableSchema, df)\n        assert(normalized.schema.fieldNames === Seq(\"id\", \"row_id\"))\n    }\n  }\n\n  test(\"normalize column names - can normalize both row id and commit version columns\") {\n    withTable(\"src\") {\n      spark.range(3).toDF(\"id\").write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .option(\"delta.enableRowTracking\", \"true\")\n        .saveAsTable(\"src\")\n\n      val df = spark.read.format(\"delta\").table(\"src\")\n        .select(\n          col(\"*\"),\n          col(\"_metadata.row_id\").as(\"row_id\"),\n          col(\"_metadata.row_commit_version\").as(\"row_commit_version\")\n        )\n        .withMetadata(\n          \"row_id\",\n          RowId.RowIdMetadataStructField.metadata(\"name\", shouldSetIcebergReservedFieldId = false))\n        .withMetadata(\n          \"row_commit_version\",\n          RowCommitVersion.MetadataStructField.metadata(\n            \"name\", shouldSetIcebergReservedFieldId = false)\n        )\n\n      val tableSchema = new StructType().add(\"id\", LongType)\n        val normalized =\n        normalizeColumnNames(deltaLog = null, tableSchema, df)\n        assert(normalized.schema.fieldNames === Seq(\"id\", \"row_id\", \"row_commit_version\"))\n    }\n  }\n\n  test(\"normalize column names - can normalize CDC type column\") {\n    val df = Seq((1, 2, 3, 4)).toDF(\"Abc\", \"def\", \"gHi\", CDCReader.CDC_TYPE_COLUMN_NAME)\n    val tableSchema = new StructType()\n      .add(\"abc\", IntegerType)\n      .add(\"Def\", IntegerType)\n      .add(\"ghi\", IntegerType)\n    val normalized = normalizeColumnNames(\n      deltaLog = null,\n      tableSchema,\n      df\n    )\n    verifyColumnsWithCasts(normalized, Seq.empty)\n    assert(normalized.schema.fieldNames ===\n      tableSchema.fieldNames :+ CDCReader.CDC_TYPE_COLUMN_NAME)\n  }\n\n  private def checkLatestStatsForOneRowFile(\n      tableName: String,\n      expectedStats: Map[String, Option[Any]]): Unit = {\n    val snapshot = DeltaLog.forTable(spark, TableIdentifier(tableName)).update()\n    val fileStats = snapshot.allFiles\n      .orderBy(desc(\"modificationTime\"))\n      .limit(1)\n      .withColumn(\"stats\", from_json(col(\"stats\"), snapshot.statsSchema))\n      .select(\"stats.*\")\n\n    val assertions = Seq(assert_true(col(\"numRecords\") === lit(1L))) ++\n        expectedStats.flatMap { case (columnName, columnValue) =>\n      columnValue match {\n        case Some(value) => Seq(\n          assert_true(col(\"minValues.\" + columnName) === lit(value)),\n          assert_true(col(\"maxValues.\" + columnName) === lit(value)),\n          assert_true(col(\"nullCount.\" + columnName) === lit(0L)))\n        case None => Seq(\n          assert_true(col(\"minValues.\" + columnName).isNull),\n          assert_true(col(\"maxValues.\" + columnName).isNull),\n          assert_true(col(\"nullCount.\" + columnName) === lit(1L)))\n      }\n    }\n    fileStats.select(assertions: _*).collect()\n  }\n\n  for (caseSensitive <- DeltaTestUtils.BOOLEAN_DOMAIN) {\n    test(s\"normalize column names - e2e nested struct (caseSensitive=$caseSensitive)\") {\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> caseSensitive.toString) {\n        val sourceData = Seq((105L, \"foo\", 205L, \"bar\",\n            Struct1(\"James\", 11, \"Smith\", 3000, \"Correct\")))\n        val sourceDf = sourceData.toDF(\"long1\", \"str1\", \"long2\", \"str2\", \"struct1\")\n        val sourceSchema = new StructType()\n          .add(\"long1\", LongType, nullable = false)\n          .add(\"str1\", StringType, nullable = true)\n          .add(\"long2\", LongType, nullable = false)\n          .add(\"str2\", StringType, nullable = true)\n          .add(\"struct1\", new StructType()\n            .add(\"firstname\", StringType, nullable = true)\n            .add(\"numberone\", LongType, nullable = false)\n            .add(\"lastname\", StringType, nullable = true)\n            .add(\"numbertwo\", LongType, nullable = false)\n            .add(\"CorrectCase\", StringType, nullable = true),\n            nullable = true\n          )\n        assert(sourceDf.schema === sourceSchema)\n        val createTableCommand =\n          \"\"\" CREATE TABLE t (\n            |  Long2 LONG, Str2 STRING, Long1 LONG, Str1 STRING, Int1 INT,\n            |  Struct1 STRUCT<LastName: STRING, NumberTwo: LONG, FirstName: STRING,\n            |    NumberOne: LONG, MissingNested: INT, CorrectCase: STRING>\n            | ) USING delta\n            |\"\"\".stripMargin\n\n        withTable(\"t\") {\n          sql(createTableCommand)\n          sourceDf.write.format(\"delta\").mode(\"append\").saveAsTable(\"t\")\n\n          // Make sure all the values were inserted into the right columns, and columns missing in\n          // the source were set to null.\n          spark.table(\"t\")\n            .select(\n              assert_true(col(\"Long2\") === 205L),\n              assert_true(col(\"Str2\") === \"bar\"),\n              assert_true(col(\"Long1\") === 105L),\n              assert_true(col(\"Str1\") === \"foo\"),\n              assert_true(col(\"Int1\").isNull),\n              assert_true(col(\"Struct1.LastName\") === \"Smith\"),\n              assert_true(col(\"Struct1.NumberTwo\") === 3000L),\n              assert_true(col(\"Struct1.FirstName\") === \"James\"),\n              assert_true(col(\"Struct1.NumberOne\") === 11L),\n              assert_true(col(\"Struct1.MissingNested\").isNull),\n              assert_true(col(\"Struct1.CorrectCase\") === \"Correct\")\n            ).collect()\n\n          // Make sure each of the columns stats was computed correctly.\n          checkLatestStatsForOneRowFile(\"t\", Map(\n            \"Long2\" -> Some(205L),\n            \"Str2\" -> Some(\"bar\"),\n            \"Long1\" -> Some(105L),\n            \"Str1\" -> Some(\"foo\"),\n            \"Int1\" -> None,\n            \"Struct1.LastName\" -> Some(\"Smith\"),\n            \"Struct1.NumberTwo\" -> Some(3000L),\n            \"Struct1.FirstName\" -> Some(\"James\"),\n            \"Struct1.NumberOne\" -> Some(11L),\n            \"Struct1.MissingNested\" -> None,\n            \"Struct1.CorrectCase\" -> Some(\"Correct\")\n          ))\n        }\n      }\n    }\n  }\n\n  ////////////////////////////\n  // mergeSchemas\n  ////////////////////////////\n\n  test(\"mergeSchemas: missing columns in df\") {\n    val base = new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)\n    val write = new StructType().add(\"a\", IntegerType)\n    assert(mergeSchemas(base, write) === base)\n  }\n\n  test(\"mergeSchemas: missing columns in df - case sensitivity\") {\n    val base = new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)\n    val write = new StructType().add(\"A\", IntegerType)\n    assert(mergeSchemas(base, write) === base)\n  }\n\n  test(\"new columns get added to the tail of the schema\") {\n    val base = new StructType().add(\"a\", IntegerType)\n    val write = new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)\n    val write2 = new StructType().add(\"b\", IntegerType).add(\"a\", IntegerType)\n    assert(mergeSchemas(base, write) === write)\n    assert(mergeSchemas(base, write2) === write)\n  }\n\n  test(\"new columns get added to the tail of the schema - nested\") {\n    val base = new StructType()\n      .add(\"regular\", StringType)\n      .add(\"struct\", new StructType()\n        .add(\"a\", IntegerType))\n\n    val write = new StructType()\n      .add(\"other\", StringType)\n      .add(\"struct\", new StructType()\n        .add(\"b\", DateType)\n        .add(\"a\", IntegerType))\n      .add(\"this\", StringType)\n\n    val expected = new StructType()\n      .add(\"regular\", StringType)\n      .add(\"struct\", new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", DateType))\n      .add(\"other\", StringType)\n      .add(\"this\", StringType)\n    assert(mergeSchemas(base, write) === expected)\n  }\n\n  test(\"schema merging of incompatible types\") {\n    val base = new StructType()\n      .add(\"top\", StringType)\n      .add(\"struct\", new StructType()\n        .add(\"a\", IntegerType))\n      .add(\"array\", ArrayType(new StructType()\n        .add(\"b\", DecimalType(18, 10))))\n      .add(\"map\", MapType(StringType, StringType))\n\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"StringType\", \"updateDataType\" -> \"IntegerType\")) {\n      mergeSchemas(base, new StructType().add(\"top\", IntegerType))\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"IntegerType\", \"updateDataType\" -> \"DateType\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"struct\", new StructType().add(\"a\", DateType)))\n    }\n    // StructType's toString is different between Scala 2.12 and 2.13.\n    // - In Scala 2.12, it extends `scala.collection.Seq` which returns\n    //   `StructType(StructField(a,IntegerType,true))`.\n    // - In Scala 2.13, it extends `scala.collection.immutable.Seq` which returns\n    //   `Seq(StructField(a,IntegerType,true))`.\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"(StructType|Seq)\\\\(.*\", \"updateDataType\" -> \"MapType\\\\(.*\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"struct\", MapType(StringType, IntegerType)))\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"DecimalType\\\\(.*\", \"updateDataType\" -> \"DoubleType\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"array\", ArrayType(new StructType().add(\"b\", DoubleType))))\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n      Map(\"decimalRanges\" -> \"scale.*\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"array\", ArrayType(new StructType().add(\"b\", DecimalType(18, 12)))))\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n      Map(\"decimalRanges\" -> \"precision.*\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"array\", ArrayType(new StructType().add(\"b\", DecimalType(16, 10)))))\n    }\n    // See the above comment about `StructType`\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"MapType\\\\(.*\", \"updateDataType\" -> \"(StructType|Seq)\\\\(.*\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"map\", new StructType().add(\"b\", StringType)))\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"StringType\", \"updateDataType\" -> \"IntegerType\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"map\", MapType(StringType, IntegerType)))\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"StringType\", \"updateDataType\" -> \"IntegerType\")) {\n      mergeSchemas(base, new StructType()\n        .add(\"map\", MapType(IntegerType, StringType)))\n    }\n  }\n\n  test(\"schema merging should pick current nullable and metadata\") {\n    val m = new MetadataBuilder().putDouble(\"a\", 0.2).build()\n    val base = new StructType()\n      .add(\"top\", StringType, nullable = false, m)\n      .add(\"struct\", new StructType()\n        .add(\"a\", IntegerType, nullable = false, m))\n      .add(\"array\", ArrayType(new StructType()\n        .add(\"b\", DecimalType(18, 10))), nullable = false, m)\n      .add(\"map\", MapType(StringType, StringType), nullable = false, m)\n\n    assert(mergeSchemas(base, new StructType().add(\"top\", StringType)) === base)\n    assert(mergeSchemas(base, new StructType().add(\"struct\", new StructType()\n      .add(\"a\", IntegerType))) === base)\n    assert(mergeSchemas(base, new StructType().add(\"array\", ArrayType(new StructType()\n      .add(\"b\", DecimalType(18, 10))))) === base)\n    assert(mergeSchemas(base, new StructType()\n      .add(\"map\", MapType(StringType, StringType))) === base)\n  }\n\n  test(\"schema merging null type\") {\n    val base = new StructType().add(\"top\", NullType)\n    val update = new StructType().add(\"top\", StringType)\n\n    assert(mergeSchemas(base, update) === update)\n    assert(mergeSchemas(update, base) === update)\n  }\n\n  test(\"schema merging performs upcast between ByteType, ShortType, and IntegerType\") {\n    val byteType = new StructType().add(\"top\", ByteType)\n    val shortType = new StructType().add(\"top\", ShortType)\n    val intType = new StructType().add(\"top\", IntegerType)\n\n    assert(mergeSchemas(byteType, shortType) === shortType)\n    assert(mergeSchemas(byteType, intType) === intType)\n    assert(mergeSchemas(shortType, intType) === intType)\n    assert(mergeSchemas(shortType, byteType) === shortType)\n    assert(mergeSchemas(intType, shortType) === intType)\n    assert(mergeSchemas(intType, byteType) === intType)\n\n    val structInt = new StructType().add(\"top\", new StructType().add(\"leaf\", IntegerType))\n    val structShort = new StructType().add(\"top\", new StructType().add(\"leaf\", ShortType))\n    assert(mergeSchemas(structInt, structShort) === structInt)\n\n    val map1 = new StructType().add(\"top\", new MapType(IntegerType, ShortType, true))\n    val map2 = new StructType().add(\"top\", new MapType(ShortType, IntegerType, true))\n    val mapMerged = new StructType().add(\"top\", new MapType(IntegerType, IntegerType, true))\n    assert(mergeSchemas(map1, map2) === mapMerged)\n\n    val arrInt = new StructType().add(\"top\", new ArrayType(IntegerType, true))\n    val arrShort = new StructType().add(\"top\", new ArrayType(ShortType, true))\n    assert(mergeSchemas(arrInt, arrShort) === arrInt)\n  }\n\n  test(\"schema merging allows upcasting to LongType with allowImplicitConversions\") {\n    val byteType = new StructType().add(\"top\", ByteType)\n    val shortType = new StructType().add(\"top\", ShortType)\n    val intType = new StructType().add(\"top\", IntegerType)\n    val longType = new StructType().add(\"top\", LongType)\n\n    Seq(byteType, shortType, intType).foreach { sourceType =>\n      assert(\n        longType === mergeSchemas(\n          longType, sourceType, allowImplicitConversions = true))\n      val e = intercept[DeltaAnalysisException] {\n          mergeSchemas(longType, sourceType)\n        }\n      checkError(\n        e.getCause.asInstanceOf[AnalysisException],\n        \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n        parameters = Map(\"currentDataType\" -> \"LongType\",\n          \"updateDataType\" -> sourceType.head.dataType.toString))\n    }\n  }\n\n  test(\"Upcast between ByteType, ShortType and IntegerType is OK for parquet\") {\n    import org.apache.spark.sql.functions._\n    def testParquetUpcast(): Unit = {\n      withTempDir { dir =>\n        val tempDir = dir.getCanonicalPath\n        spark.range(1.toByte).select(col(\"id\") cast ByteType).write.save(tempDir + \"/byte\")\n        spark.range(1.toShort).select(col(\"id\") cast ShortType).write.save(tempDir + \"/short\")\n        spark.range(1).select(col(\"id\") cast IntegerType).write.save(tempDir + \"/int\")\n\n        val shortSchema = new StructType().add(\"id\", ShortType)\n        val intSchema = new StructType().add(\"id\", IntegerType)\n\n        spark.read.schema(shortSchema).parquet(tempDir + \"/byte\").collect() === Seq(Row(1.toShort))\n        spark.read.schema(intSchema).parquet(tempDir + \"/short\").collect() === Seq(Row(1))\n        spark.read.schema(intSchema).parquet(tempDir + \"/byte\").collect() === Seq(Row(1))\n      }\n    }\n\n    testParquetUpcast()\n\n  }\n\n  test(\"schema merging non struct root type\") {\n    // Array root type\n    val base1 = ArrayType(new StructType().add(\"a\", IntegerType))\n    val update1 = ArrayType(new StructType().add(\"b\", IntegerType))\n    val mergedType1 =\n      mergeDataTypes(\n        current = base1,\n        update = update1,\n        allowImplicitConversions = false,\n        keepExistingType = false,\n        typeWideningMode = TypeWideningMode.NoTypeWidening,\n        caseSensitive = false,\n        allowOverride = false,\n        overrideMetadata = false)\n    assert(mergedType1 === ArrayType(new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType)))\n\n    // Map root type\n    val base2 = MapType(\n      new StructType().add(\"a\", IntegerType),\n      new StructType().add(\"b\", IntegerType)\n    )\n    val update2 = MapType(\n      new StructType().add(\"b\", IntegerType),\n      new StructType().add(\"c\", IntegerType)\n    )\n    val mergedType2 =\n      mergeDataTypes(\n        current = base2,\n        update = update2,\n        allowImplicitConversions = false,\n        keepExistingType = false,\n        typeWideningMode = TypeWideningMode.NoTypeWidening,\n        caseSensitive = false,\n        allowOverride = false,\n        overrideMetadata = false)\n    assert(mergedType2 ===\n      MapType(\n        new StructType().add(\"a\", IntegerType).add(\"b\", IntegerType),\n        new StructType().add(\"b\", IntegerType).add(\"c\", IntegerType)\n      ))\n  }\n\n  test(\"schema merging allow override\") {\n    // override root type\n    val base1 = new StructType().add(\"a\", IntegerType)\n    val update1 = ArrayType(LongType)\n    val mergedSchema1 =\n      mergeDataTypes(\n        current = base1,\n        update = update1,\n        allowImplicitConversions = false,\n        keepExistingType = false,\n        typeWideningMode = TypeWideningMode.NoTypeWidening,\n        caseSensitive = false,\n        allowOverride = true,\n        overrideMetadata = false)\n    assert(mergedSchema1 === ArrayType(LongType))\n\n    // override nested type\n    val base2 = ArrayType(new StructType().add(\"a\", IntegerType).add(\"b\", StringType))\n    val update2 = ArrayType(new StructType().add(\"a\", MapType(StringType, StringType)))\n    val mergedSchema2 =\n      mergeDataTypes(\n        current = base2,\n        update = update2,\n        allowImplicitConversions = false,\n        keepExistingType = false,\n        typeWideningMode = TypeWideningMode.NoTypeWidening,\n        caseSensitive = false,\n        allowOverride = true,\n        overrideMetadata = false)\n    assert(mergedSchema2 ===\n      ArrayType(new StructType().add(\"a\", MapType(StringType, StringType)).add(\"b\", StringType)))\n  }\n\n  test(\"keepExistingType and typeWideningMode both set allows both widening and \" +\n    \"preserving non-widenable existing types\") {\n    val base = new StructType()\n      .add(\"widened\", ShortType)\n      .add(\"struct\", new StructType()\n        .add(\"b\", ByteType)\n        .add(\"a\", StringType))\n      .add(\"map\", MapType(ShortType, IntegerType))\n      .add(\"array\", ArrayType(StringType))\n      .add(\"nonwidened\", IntegerType)\n\n    val update = new StructType()\n      .add(\"widened\", IntegerType)\n      .add(\"struct\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"a\", IntegerType))\n      .add(\"map\", MapType(IntegerType, StringType))\n      .add(\"array\", ArrayType(ByteType))\n      .add(\"nonwidened\", StringType)\n\n    val expected = new StructType()\n      .add(\"widened\", IntegerType)\n      .add(\"struct\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"a\", StringType))\n      .add(\"map\", MapType(IntegerType, IntegerType))\n      .add(\"array\", ArrayType(StringType))\n      .add(\"nonwidened\", IntegerType)\n\n    val mergedSchema =\n      mergeSchemas(\n        base,\n        update,\n        typeWideningMode = TypeWideningMode.TypeEvolution(\n          uniformIcebergCompatibleOnly = false,\n          allowAutomaticWidening = AllowAutomaticWideningMode.default),\n        keepExistingType = true\n      )\n    assert(mergedSchema === expected)\n  }\n\n  private val allTypeWideningModes = Set(\n    NoTypeWidening,\n    AllTypeWidening,\n    TypeEvolution(\n      uniformIcebergCompatibleOnly = false,\n      allowAutomaticWidening = AllowAutomaticWideningMode.default),\n    TypeEvolution(\n      uniformIcebergCompatibleOnly = true,\n      allowAutomaticWidening = AllowAutomaticWideningMode.default),\n    AllTypeWideningToCommonWiderType,\n    TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = false),\n    TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = true),\n    AllTypeWideningWithDecimalCoercion,\n    TypeEvolutionWithDecimalCoercion\n  )\n\n  test(\"typeWideningMode - byte->short->int is always allowed\") {\n    val narrow = new StructType()\n      .add(\"a\", ByteType)\n      .add(\"b\", ByteType)\n      .add(\"c\", ShortType)\n      .add(\"s\", new StructType().add(\"x\", ByteType))\n      .add(\"m\", MapType(ByteType, ShortType))\n      .add(\"ar\", ArrayType(ByteType))\n\n    val wide = new StructType()\n      .add(\"a\", ShortType)\n      .add(\"b\", IntegerType)\n      .add(\"c\", IntegerType)\n      .add(\"s\", new StructType().add(\"x\", IntegerType))\n      .add(\"m\", MapType(ShortType, IntegerType))\n      .add(\"ar\", ArrayType(IntegerType))\n\n    for (typeWideningMode <- allTypeWideningModes) {\n      // byte, short, int are all stored as INT64 in parquet, [[mergeSchemas]] always allows\n      // widening between them. This was already the case before typeWideningMode was introduced.\n      val merged1 = mergeSchemas(narrow, wide, typeWideningMode = typeWideningMode)\n      assert(merged1 === wide)\n      val merged2 = mergeSchemas(wide, narrow, typeWideningMode = typeWideningMode)\n      assert(merged2 === wide)\n    }\n  }\n\n  // These type changes will only be available once Delta uses Spark 4.0.\n  for ((fromType, toType) <- Seq(\n    IntegerType -> LongType,\n    new StructType().add(\"x\", IntegerType) -> new StructType().add(\"x\", LongType),\n    MapType(IntegerType, IntegerType) -> MapType(LongType, LongType),\n    ArrayType(IntegerType) -> ArrayType(LongType)\n  ))\n  test(s\"typeWideningMode ${fromType.sql} -> ${toType.sql}\") {\n    val narrow = new StructType().add(\"a\", fromType)\n    val wide = new StructType().add(\"a\", toType)\n\n    for (typeWideningMode <- Seq(\n        NoTypeWidening,\n        AllTypeWidening,\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = false,\n          allowAutomaticWidening = AllowAutomaticWideningMode.default),\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = true,\n          allowAutomaticWidening = AllowAutomaticWideningMode.default),\n        AllTypeWideningWithDecimalCoercion,\n        TypeEvolutionWithDecimalCoercion)) {\n      // Narrowing is not allowed.\n      expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n        Map(\"currentDataType\" -> \"LongType\", \"updateDataType\" -> \"IntegerType\")) {\n        mergeSchemas(wide, narrow, typeWideningMode = typeWideningMode)\n      }\n    }\n\n    for (typeWideningMode <- Seq(\n        AllTypeWideningToCommonWiderType,\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = false),\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = true))) {\n      // These modes don't enforce an order on the inputs, widening from second schema to first\n      // is allowed.\n      val merged = mergeSchemas(wide, narrow, typeWideningMode = typeWideningMode)\n      assert(merged === wide)\n    }\n\n    for (typeWideningMode <- allTypeWideningModes -- Set(NoTypeWidening)) {\n      // Widening is allowed, unless mode is NoTypeWidening.\n      val merged = mergeSchemas(narrow, wide, typeWideningMode = typeWideningMode)\n      assert(merged === wide)\n    }\n    expectAnalysisErrorClass(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n      Map(\"currentDataType\" -> \"LongType\", \"updateDataType\" -> \"IntegerType\")) {\n      mergeSchemas(wide, narrow, typeWideningMode = NoTypeWidening)\n    }\n  }\n\n  for ((fromType, toType) <- Seq(\n    ShortType -> DoubleType,\n    IntegerType -> DecimalType(10, 0)\n  ))\n  test(\n    s\"typeWideningMode - blocked type evolution ${fromType.sql} -> ${toType.sql}\") {\n    val narrow = new StructType().add(\"a\", fromType)\n    val wide = new StructType().add(\"a\", toType)\n\n    for (typeWideningMode <- Seq(\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = false,\n          allowAutomaticWidening = AllowAutomaticWideningMode.SAME_FAMILY_TYPE),\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = false),\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = true,\n          allowAutomaticWidening = AllowAutomaticWideningMode.SAME_FAMILY_TYPE),\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = true),\n        TypeEvolutionWithDecimalCoercion)) {\n      expectAnalysisErrorClass(\n        \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n        Map(\"currentDataType\" -> fromType.toString, \"updateDataType\" -> toType.toString),\n        matchPVals = false) {\n        mergeSchemas(narrow, wide, typeWideningMode = typeWideningMode)\n      }\n      expectAnalysisErrorClass(\n        \"DELTA_MERGE_INCOMPATIBLE_DATATYPE\",\n        Map(\"currentDataType\" -> toType.toString, \"updateDataType\" -> fromType.toString),\n        matchPVals = false) {\n        mergeSchemas(wide, narrow, typeWideningMode = typeWideningMode)\n      }\n    }\n  }\n\n  for ((fromType, toType) <- Seq(\n    DateType -> TimestampNTZType,\n    DecimalType(10, 2) -> DecimalType(12, 4)\n  ))\n  test(\n      s\"typeWideningMode - Uniform Iceberg compatibility ${fromType.sql} -> ${toType.sql}\") {\n    val narrow = new StructType().add(\"a\", fromType)\n    val wide = new StructType().add(\"a\", toType)\n\n    def checkAnalysisException(f: => Unit): Unit = {\n      val ex = intercept[DeltaAnalysisException](f).getCause.asInstanceOf[AnalysisException]\n      // Decimal scale increase return a slightly different error class.\n      assert(ex.errorClass.contains(\"DELTA_MERGE_INCOMPATIBLE_DATATYPE\") ||\n        ex.errorClass.contains(\"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\"))\n    }\n\n    for (typeWideningMode <- Seq(\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = false,\n          allowAutomaticWidening = AllowAutomaticWideningMode.ALWAYS),\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = false))) {\n        // Unsupported type changes by Iceberg are allowed without Iceberg compatibility.\n      val merged = mergeSchemas(narrow, wide, typeWideningMode = typeWideningMode)\n      assert(merged === wide)\n    }\n\n    for (typeWideningMode <- Seq(\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = true,\n          allowAutomaticWidening = AllowAutomaticWideningMode.ALWAYS),\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = true))) {\n      // Widening is blocked for unsupported type changes with Iceberg compatibility.\n      checkAnalysisException {\n        mergeSchemas(wide, narrow, typeWideningMode = typeWideningMode)\n      }\n    }\n\n    // These modes don't enforce an order on the inputs, widening from second schema to first\n    // is allowed without Iceberg compatibility.\n    val merged = mergeSchemas(wide, narrow,\n      typeWideningMode = TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = false))\n    assert(merged === wide)\n\n    for (typeWideningMode <- Seq(\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = true,\n          allowAutomaticWidening = AllowAutomaticWideningMode.default),\n        TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = true),\n        TypeEvolution(\n          uniformIcebergCompatibleOnly = true,\n          allowAutomaticWidening = AllowAutomaticWideningMode.default))) {\n      // Rejected either because this is a narrowing type change, or for the bidirectional mode,\n      // because it is not supported by Iceberg.\n      checkAnalysisException {\n        mergeSchemas(wide, narrow, typeWideningMode = typeWideningMode)\n      }\n    }\n  }\n\n  test(\n    s\"typeWideningMode - widen to common wider decimal\") {\n    val left = new StructType().add(\"a\", DecimalType(10, 2))\n    val right = new StructType().add(\"a\", DecimalType(5, 4))\n    val wider = new StructType().add(\"a\", DecimalType(12, 4))\n\n    val modesCanWidenToCommonWiderDecimal = Set(\n      // Increasing decimal scale isn't supported by Iceberg, so only possible when we don't enforce\n      // Iceberg compatibility.\n      TypeEvolutionToCommonWiderType(uniformIcebergCompatibleOnly = false),\n      AllTypeWideningToCommonWiderType,\n      AllTypeWideningWithDecimalCoercion,\n      TypeEvolutionWithDecimalCoercion\n    )\n\n    for (typeWideningMode <- modesCanWidenToCommonWiderDecimal) {\n      assert(mergeSchemas(left, right, typeWideningMode = typeWideningMode) === wider)\n      assert(mergeSchemas(right, left, typeWideningMode = typeWideningMode) === wider)\n    }\n\n    for (typeWideningMode <- allTypeWideningModes -- modesCanWidenToCommonWiderDecimal) {\n      expectAnalysisErrorClass(\n        \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n        Map(\"decimalRanges\" -> \"precision 10 and 5 & scale 2 and 4\"),\n        matchPVals = false) {\n        mergeSchemas(left, right, typeWideningMode = typeWideningMode)\n      }\n      expectAnalysisErrorClass(\n        \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n        Map(\"decimalRanges\" -> \"precision 5 and 10 & scale 4 and 2\"),\n        matchPVals = false) {\n        mergeSchemas(right, left, typeWideningMode = typeWideningMode)\n      }\n    }\n\n  }\n\n  test(\n    s\"typeWideningMode - widen to common wider decimal exceeds max decimal precision\") {\n    // We'd need a DecimalType(40, 19) to fit both types, which exceeds max decimal precision of 38.\n    val left = new StructType().add(\"a\", DecimalType(20, 19))\n    val right = new StructType().add(\"a\", DecimalType(21, 0))\n\n    for (typeWideningMode <- allTypeWideningModes) {\n      expectAnalysisErrorClass(\n        \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n        Map(\"decimalRanges\" -> \"precision 20 and 21 & scale 19 and 0\"),\n        matchPVals = false) {\n        mergeSchemas(left, right, typeWideningMode = typeWideningMode)\n      }\n      expectAnalysisErrorClass(\n        \"DELTA_MERGE_INCOMPATIBLE_DECIMAL_TYPE\",\n        Map(\"decimalRanges\" -> \"precision 21 and 20 & scale 0 and 19\"),\n        matchPVals = false) {\n        mergeSchemas(right, left, typeWideningMode = typeWideningMode)\n      }\n    }\n  }\n\n  test(s\"typeWideningMode - widen integral type to common wider decimal\") {\n    val left = new StructType()\n      .add(\"a\", ByteType)\n      .add(\"b\", ShortType)\n      .add(\"c\", IntegerType)\n      .add(\"d\", LongType)\n    val right = new StructType()\n      .add(\"a\", DecimalType(2, 1))\n      .add(\"b\", DecimalType(2, 1))\n      .add(\"c\", DecimalType(2, 1))\n      .add(\"d\", DecimalType(2, 1))\n    val wider = new StructType()\n      .add(\"a\", DecimalType(4, 1))\n      .add(\"b\", DecimalType(6, 1))\n      .add(\"c\", DecimalType(11, 1))\n      .add(\"d\", DecimalType(21, 1))\n\n    assert(mergeSchemas(left, right, typeWideningMode = AllTypeWideningWithDecimalCoercion)\n      == wider)\n    assert(mergeSchemas(left, right, typeWideningMode = AllTypeWideningToCommonWiderType)\n      == wider)\n\n    // check that flipping conf to false prevents integral type decimal coercion\n    // for `AllTypeWideningToCommonWiderType`\n    withSQLConf(DeltaSQLConf.DELTA_TYPE_WIDENING_ALLOW_INTEGRAL_DECIMAL_COERCION.key ->\n      \"false\") {\n      val exception = intercept[DeltaAnalysisException] {\n        mergeSchemas(left, right, typeWideningMode = AllTypeWideningToCommonWiderType)\n      }\n      checkError(\n        exception,\n        \"DELTA_FAILED_TO_MERGE_FIELDS\",\n        sqlState = \"22005\",\n        parameters = Map(\"currentField\" -> \"a\", \"updateField\" -> \"a\")\n      )\n    }\n  }\n\n  test(\"schema merging override field metadata\") {\n    val base1 = new StructType()\n      .add(\"a\", IntegerType)\n    val update1 = new StructType()\n      .add(\"a\", IntegerType, nullable = true, new MetadataBuilder().putString(\"x\", \"1\").build())\n    val mergedSchema1 =\n      mergeDataTypes(\n        current = base1,\n        update = update1,\n        allowImplicitConversions = false,\n        keepExistingType = false,\n        typeWideningMode = TypeWideningMode.NoTypeWidening,\n        caseSensitive = false,\n        allowOverride = false,\n        overrideMetadata = true\n      )\n    assert(mergedSchema1 ===\n      new StructType()\n        .add(\"a\", IntegerType, nullable = true,\n          new MetadataBuilder().putString(\"x\", \"1\").build()))\n\n    // override nested metadata\n    val base2 = ArrayType(new StructType()\n      .add(\"a\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"c\", IntegerType)))\n    val update2 = ArrayType(new StructType()\n      .add(\"a\", new StructType()\n        .add(\"b\", IntegerType)\n        .add(\"c\", IntegerType, nullable = true,\n          new MetadataBuilder().putString(\"c_metadata\", \"2\").build()),\n        nullable = true,\n        new MetadataBuilder().putString(\"a_metadata\", \"3\").build()))\n    val mergedSchema2 =\n      mergeDataTypes(\n        current = base2,\n        update = update2,\n        allowImplicitConversions = false,\n        keepExistingType = false,\n        typeWideningMode = TypeWideningMode.NoTypeWidening,\n        caseSensitive = false,\n        allowOverride = false,\n        overrideMetadata = true\n      )\n    assert(mergedSchema2 ===\n      ArrayType(new StructType()\n        .add(\"a\", new StructType()\n          .add(\"b\", IntegerType)\n          .add(\"c\", IntegerType, nullable = true,\n            new MetadataBuilder().putString(\"c_metadata\", \"2\").build()),\n          nullable = true,\n          new MetadataBuilder().putString(\"a_metadata\", \"3\").build())))\n  }\n\n  ////////////////////////////\n  // transformColumns\n  ////////////////////////////\n\n  test(\"transform columns - simple\") {\n    val base = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", StringType)\n    val update = new StructType()\n      .add(\"c\", IntegerType)\n      .add(\"b\", StringType)\n\n    // Identity.\n    var visitedFields = 0\n    val res1 = SchemaMergingUtils.transformColumns(base) {\n      case (Seq(), field, _) =>\n        visitedFields += 1\n        field\n    }\n    assert(visitedFields === 2)\n    assert(base === res1)\n\n    // Rename a -> c\n    visitedFields = 0\n    val res2 = SchemaMergingUtils.transformColumns(base) {\n      case (Seq(), field, _) =>\n        visitedFields += 1\n        val name = field.name\n        field.copy(name = if (name == \"a\") \"c\" else name)\n    }\n    assert(visitedFields === 2)\n    assert(update === res2)\n\n    // Rename a -> c; using input map.\n    visitedFields = 0\n    val res3 = transformColumns(base, (Seq(\"A\"), \"c\") :: Nil) {\n      case (Seq(), field, Seq((_, newName))) =>\n        visitedFields += 1\n        field.copy(name = newName)\n    }\n    assert(visitedFields === 1)\n    assert(update === res3)\n  }\n\n  test(\"transform element field type\") {\n    val base = new StructType()\n      .add(\"a\", new StructType()\n        .add(\"element\", StringType))\n\n    val update = new StructType()\n      .add(\"a\", new StructType()\n        .add(\"element\", IntegerType))\n\n    // Update type\n    var visitedFields = 0\n    val res = SchemaMergingUtils.transformColumns(base) { (path, field, _) =>\n      visitedFields += 1\n      val dataType = path :+ field.name match {\n        case Seq(\"a\", \"element\") => IntegerType\n        case _ => field.dataType\n      }\n      field.copy(dataType = dataType)\n    }\n    assert(visitedFields === 2)\n    assert(update === res)\n  }\n\n  test(\"transform array nested field type\") {\n    val nested = new StructType()\n      .add(\"s1\", IntegerType)\n      .add(\"s2\", LongType)\n    val base = new StructType()\n      .add(\"arr\", ArrayType(nested))\n\n    val updatedNested = new StructType()\n      .add(\"s1\", StringType)\n      .add(\"s2\", LongType)\n    val update = new StructType()\n      .add(\"arr\", ArrayType(updatedNested))\n\n    // Update type\n    var visitedFields = 0\n    val res = SchemaMergingUtils.transformColumns(base) { (path, field, _) =>\n      visitedFields += 1\n      val dataType = path :+ field.name match {\n        case Seq(\"arr\", \"element\", \"s1\") => StringType\n        case _ => field.dataType\n      }\n      field.copy(dataType = dataType)\n    }\n    assert(visitedFields === 3)\n    assert(update === res)\n  }\n\n  test(\"transform map nested field type\") {\n    val nested = new StructType()\n      .add(\"s1\", IntegerType)\n      .add(\"s2\", LongType)\n    val base = new StructType()\n      .add(\"m\", MapType(StringType, nested))\n\n    val updatedNested = new StructType()\n      .add(\"s1\", StringType)\n      .add(\"s2\", LongType)\n    val update = new StructType()\n      .add(\"m\", MapType(StringType, updatedNested))\n\n    // Update type\n    var visitedFields = 0\n    val res = SchemaMergingUtils.transformColumns(base) { (path, field, _) =>\n      visitedFields += 1\n      val dataType = path :+ field.name match {\n        case Seq(\"m\", \"value\", \"s1\") => StringType\n        case _ => field.dataType\n      }\n      field.copy(dataType = dataType)\n    }\n    assert(visitedFields === 3)\n    assert(update === res)\n  }\n\n  test(\"transform map type\") {\n    val base = new StructType()\n      .add(\"m\", MapType(StringType, IntegerType))\n    val update = new StructType()\n      .add(\"m\", MapType(StringType, StringType))\n\n    // Update type\n    var visitedFields = 0\n    val res = SchemaMergingUtils.transformColumns(base) { (path, field, _) =>\n      visitedFields += 1\n      val dataType = path :+ field.name match {\n        case Seq(\"m\") => MapType(field.dataType.asInstanceOf[MapType].keyType, StringType)\n        case _ => field.dataType\n      }\n      field.copy(dataType = dataType)\n    }\n    assert(visitedFields === 1)\n    assert(update === res)\n  }\n\n  test(\"transform columns - nested\") {\n    val nested = new StructType()\n      .add(\"s1\", IntegerType)\n      .add(\"s2\", LongType)\n    val base = new StructType()\n      .add(\"nested\", nested)\n      .add(\"arr\", ArrayType(nested))\n      .add(\"kvs\", MapType(nested, nested))\n    val update = new StructType()\n      .add(\"nested\",\n        new StructType()\n          .add(\"t1\", IntegerType)\n          .add(\"s2\", LongType))\n      .add(\"arr\", ArrayType(\n        new StructType()\n          .add(\"s1\", IntegerType)\n          .add(\"a2\", LongType)))\n      .add(\"kvs\", MapType(\n        new StructType()\n          .add(\"k1\", IntegerType)\n          .add(\"s2\", LongType),\n        new StructType()\n          .add(\"s1\", IntegerType)\n          .add(\"v2\", LongType)))\n\n    // Identity.\n    var visitedFields = 0\n    val res1 = SchemaMergingUtils.transformColumns(base) {\n      case (_, field, _) =>\n        visitedFields += 1\n        field\n    }\n    assert(visitedFields === 11)\n    assert(base === res1)\n\n    // Rename\n    visitedFields = 0\n    val res2 = SchemaMergingUtils.transformColumns(base) { (path, field, _) =>\n      visitedFields += 1\n      val name = path :+ field.name match {\n        case Seq(\"nested\", \"s1\") => \"t1\"\n        case Seq(\"arr\", \"element\", \"s2\") => \"a2\"\n        case Seq(\"kvs\", \"key\", \"s1\") => \"k1\"\n        case Seq(\"kvs\", \"value\", \"s2\") => \"v2\"\n        case _ => field.name\n      }\n      field.copy(name = name)\n    }\n    assert(visitedFields === 11)\n    assert(update === res2)\n\n    // Rename; using map\n    visitedFields = 0\n    val mapping = Seq(\n      Seq(\"nested\", \"s1\") -> \"t1\",\n      Seq(\"arr\", \"element\", \"s2\") -> \"a2\",\n      Seq(\"kvs\", \"key\", \"S1\") -> \"k1\",\n      Seq(\"kvs\", \"value\", \"s2\") -> \"v2\")\n    val res3 = transformColumns(base, mapping) {\n      case (_, field, Seq((_, name))) =>\n        visitedFields += 1\n        field.copy(name = name)\n    }\n    assert(visitedFields === 4)\n    assert(update === res3)\n  }\n\n  test(\"transform top level array type\") {\n    val at = ArrayType(\n      new StructType()\n        .add(\"s1\", IntegerType)\n    )\n\n    var visitedFields = 0\n    val updated = SchemaMergingUtils.transformColumns(at) {\n      case (_, field, _) =>\n        visitedFields += 1\n        field.copy(name = \"s1_1\", dataType = StringType)\n    }\n\n    assert(visitedFields === 1)\n    assert(updated === ArrayType(new StructType().add(\"s1_1\", StringType)))\n  }\n\n  test(\"transform top level map type\") {\n    val mt = MapType(\n      new StructType()\n        .add(\"k1\", IntegerType),\n      new StructType()\n        .add(\"v1\", IntegerType)\n    )\n\n    var visitedFields = 0\n    val updated = SchemaMergingUtils.transformColumns(mt) {\n      case (_, field, _) =>\n        visitedFields += 1\n        field.copy(name = field.name + \"_1\", dataType = StringType)\n    }\n\n    assert(visitedFields === 2)\n    assert(updated === MapType(\n      new StructType().add(\"k1_1\", StringType),\n      new StructType().add(\"v1_1\", StringType)\n    ))\n  }\n\n  ////////////////////////////\n  // pruneEmptyStructs\n  ////////////////////////////\n  test(\"prune empty structs\") {\n    val emptySchema = new StructType()\n    var schema = emptySchema\n    assert(SchemaMergingUtils.pruneEmptyStructs(schema).isEmpty)\n\n    val elementType = new StructType()\n      .add(\"a\", emptySchema)\n      .add(\"b\", new StructType().add(\"1\", emptySchema).add(\"2\", StringType))\n    val filteredElementType = new StructType().add(\"b\", new StructType().add(\"2\", StringType))\n\n    assert(SchemaMergingUtils.pruneEmptyStructs(elementType).get == filteredElementType)\n\n    // filter out array element type with empty schema\n    schema = new StructType().add(\"a\", new ArrayType(emptySchema, false))\n    assert(SchemaMergingUtils.pruneEmptyStructs(schema).isEmpty)\n\n    // nested empty schema\n    schema = new StructType().add(\"a\", new ArrayType(new StructType().add(\"a\", emptySchema), false))\n    assert(SchemaMergingUtils.pruneEmptyStructs(schema).isEmpty)\n\n    schema = new StructType().add(\"a\", new ArrayType(elementType, false))\n    assert(\n      SchemaMergingUtils.pruneEmptyStructs(schema).get ==\n        new StructType().add(\"a\", new ArrayType(filteredElementType, false))\n    )\n\n    schema = new StructType().add(\"a\", new MapType(emptySchema, StringType, false))\n    assert(SchemaMergingUtils.pruneEmptyStructs(schema).isEmpty)\n\n    schema = new StructType().add(\"a\", new MapType(StringType, emptySchema, false))\n    assert(SchemaMergingUtils.pruneEmptyStructs(schema).isEmpty)\n\n    schema = new StructType().add(\"a\", new MapType(StringType, elementType, false))\n    assert(\n      SchemaMergingUtils.pruneEmptyStructs(schema).get ==\n        new StructType().add(\"a\", new MapType(StringType, filteredElementType, false))\n    )\n  }\n\n  ////////////////////////////\n  // checkFieldNames\n  ////////////////////////////\n\n  test(\"check non alphanumeric column characters\") {\n    val badCharacters = \" ,;{}()\\n\\t=\"\n    val goodCharacters = \"#.`!@$%^&*~_<>?/:\"\n\n    badCharacters.foreach { char =>\n      Seq(s\"a${char}b\", s\"${char}ab\", s\"ab${char}\", char.toString).foreach { name =>\n        checkError(\n          intercept[AnalysisException] {\n            SchemaUtils.checkFieldNames(Seq(name))\n          },\n          \"DELTA_INVALID_CHARACTERS_IN_COLUMN_NAME\",\n          parameters = Map(\"columnName\" -> s\"$name\")\n        )\n      }\n    }\n\n    goodCharacters.foreach { char =>\n      // no issues here\n      SchemaUtils.checkFieldNames(Seq(s\"a${char}b\", s\"${char}ab\", s\"ab${char}\", char.toString))\n    }\n  }\n\n  test(\"fieldToColumn\") {\n    assert(SchemaUtils.fieldToColumn(StructField(\"a\", IntegerType)).expr ==\n      new UnresolvedAttribute(\"a\" :: Nil))\n    // Dot in the column name should be converted correctly\n    assert(SchemaUtils.fieldToColumn(StructField(\"a.b\", IntegerType)).expr ==\n      new UnresolvedAttribute(\"a.b\" :: Nil))\n  }\n\n  ////////////////////////////\n  // findNestedFieldIgnoreCase\n  ////////////////////////////\n\n  test(\"complex schema access\") {\n    val st = StringType\n    val it = IntegerType\n    def m(a: DataType, b: DataType): MapType = MapType(a, b)\n    def a(el: DataType): ArrayType = ArrayType(el)\n    def struct(el: DataType): StructType = new StructType().add(\"f1\", el)\n\n    val schema = new StructType()\n      .add(\"a\", it)\n      .add(\"b\", struct(st))\n      .add(\"c\", struct(struct(struct(st))))\n      .add(\"d\", a(it))\n      .add(\"e\", a(a(it)))\n      .add(\"f\", a(a(struct(st))))\n      .add(\"g\", m(m(st, it), m(st, it)))\n      .add(\"h\", m(a(st), a(it)))\n      .add(\"i\", m(a(struct(st)), a(struct(st))))\n      .add(\"j\", m(m(struct(st), struct(it)), m(struct(st), struct(it))))\n      .add(\"k\", m(struct(a(a(struct(a(struct(st)))))),\n                m(m(struct(st), struct(it)), m(struct(st), struct(it)))))\n\n    def find(names: Seq[String]): Option[StructField] =\n      SchemaUtils.findNestedFieldIgnoreCase(schema, names, true)\n\n    val checks = Map(\n      \"a\" -> it,\n      \"b\" -> struct(st),\n      \"b.f1\" -> st,\n      \"c.f1.f1.f1\" -> st,\n      \"d.element\" -> it,\n      \"e.element.element\" -> it,\n      \"f.element.element.f1\" -> st,\n      \"g.key.key\" -> st,\n      \"g.key.value\" -> it,\n      \"g.value.key\" -> st,\n      \"g.value.value\" -> it,\n      \"h.key.element\" -> st,\n      \"h.value.element\" -> it,\n      \"i.key.element.f1\" -> st,\n      \"i.value.element.f1\" -> st,\n      \"j.key.key.f1\" -> st,\n      \"j.key.value.f1\" -> it,\n      \"j.value.key.f1\" -> st,\n      \"j.value.value.f1\" -> it,\n      \"k.key.f1.element.element.f1.element.f1\" -> st,\n      \"k.value.key.key.f1\" -> st,\n      \"k.value.key.value.f1\" -> it,\n      \"k.value.value.key.f1\" -> st,\n      \"k.value.value.value.f1\" -> it\n    )\n\n    checks.foreach { pair =>\n      val (key, t) = pair\n      val path = key.split('.')\n      val f = find(path)\n      assert(f.isDefined, s\"cannot find $key\")\n      assert(f.get.name == path.last && f.get.dataType == t)\n    }\n\n    val negativeChecks = Seq(\n      \"x\",\n      \"b.f2\",\n      \"c.f1.f2\",\n      \"c.f1.f1.f2\",\n      \"d.f1\",\n      \"d.element.f1\",\n      \"e.element.element.f1\",\n      \"f.element.key.f1\",\n      \"g.key.element\",\n      \"g.key.keyy\",\n      \"g.key.valuee\",\n      \"h.key.element.f1\",\n      \"k.key.f1.element.element.f2.element.f1\",\n      \"k.value.value.f1\"\n    )\n\n    negativeChecks.foreach { key =>\n      val path = key.split('.')\n      val f = find(path)\n      assert(f.isEmpty, s\"$key should be empty\")\n    }\n\n  }\n\n  test(\"findUnsupportedDataTypes\") {\n    def assertUnsupportedDataType(\n        dataType: DataType,\n        expected: Seq[UnsupportedDataTypeInfo]): Unit = {\n      val schema = StructType(Seq(StructField(\"col\", dataType)))\n      assert(findUnsupportedDataTypes(schema) == expected)\n    }\n\n    assertUnsupportedDataType(NullType, Nil)\n    assertUnsupportedDataType(BooleanType, Nil)\n    assertUnsupportedDataType(ByteType, Nil)\n    assertUnsupportedDataType(ShortType, Nil)\n    assertUnsupportedDataType(IntegerType, Nil)\n    assertUnsupportedDataType(LongType, Nil)\n    assertUnsupportedDataType(\n      YearMonthIntervalType.DEFAULT,\n      Seq(UnsupportedDataTypeInfo(\"col\", YearMonthIntervalType.DEFAULT)))\n    assertUnsupportedDataType(\n      DayTimeIntervalType.DEFAULT,\n      Seq(UnsupportedDataTypeInfo(\"col\", DayTimeIntervalType.DEFAULT)))\n    assertUnsupportedDataType(FloatType, Nil)\n    assertUnsupportedDataType(DoubleType, Nil)\n    assertUnsupportedDataType(StringType, Nil)\n    assertUnsupportedDataType(DateType, Nil)\n    assertUnsupportedDataType(TimestampType, Nil)\n    assertUnsupportedDataType(\n      CalendarIntervalType,\n      Seq(UnsupportedDataTypeInfo(\"col\", CalendarIntervalType)))\n    assertUnsupportedDataType(BinaryType, Nil)\n    assertUnsupportedDataType(DataTypes.createDecimalType(), Nil)\n    assertUnsupportedDataType(\n      UnsupportedDataType,\n      Seq(UnsupportedDataTypeInfo(\"col\", UnsupportedDataType)))\n\n    // array\n    assertUnsupportedDataType(ArrayType(IntegerType, true), Nil)\n    assertUnsupportedDataType(\n      ArrayType(UnsupportedDataType, true),\n      Seq(UnsupportedDataTypeInfo(\"col[]\", UnsupportedDataType)))\n\n    // map\n    assertUnsupportedDataType(MapType(IntegerType, IntegerType, true), Nil)\n    assertUnsupportedDataType(\n      MapType(UnsupportedDataType, IntegerType, true),\n      Seq(UnsupportedDataTypeInfo(\"col[key]\", UnsupportedDataType)))\n    assertUnsupportedDataType(\n      MapType(IntegerType, UnsupportedDataType, true),\n      Seq(UnsupportedDataTypeInfo(\"col[value]\", UnsupportedDataType)))\n    assertUnsupportedDataType(\n      MapType(UnsupportedDataType, UnsupportedDataType, true),\n      Seq(\n        UnsupportedDataTypeInfo(\"col[key]\", UnsupportedDataType),\n        UnsupportedDataTypeInfo(\"col[value]\", UnsupportedDataType)))\n\n    // struct\n    assertUnsupportedDataType(StructType(StructField(\"f\", LongType) :: Nil), Nil)\n    assertUnsupportedDataType(\n      StructType(StructField(\"a\", LongType) :: StructField(\"dot.name\", UnsupportedDataType) :: Nil),\n      Seq(UnsupportedDataTypeInfo(\"col.`dot.name`\", UnsupportedDataType)))\n    val nestedStructType = StructType(Seq(\n      StructField(\"a\", LongType),\n      StructField(\"b\", StructType(Seq(\n        StructField(\"c\", LongType),\n        StructField(\"d\", UnsupportedDataType)\n      ))),\n      StructField(\"e\", StructType(Seq(\n        StructField(\"f\", LongType),\n        StructField(\"g\", UnsupportedDataType)\n      )))\n    ))\n    assertUnsupportedDataType(\n      nestedStructType,\n      Seq(\n        UnsupportedDataTypeInfo(\"col.b.d\", UnsupportedDataType),\n        UnsupportedDataTypeInfo(\"col.e.g\", UnsupportedDataType)))\n\n    // udt\n    assertUnsupportedDataType(new PointUDT, Nil)\n    assertUnsupportedDataType(\n      new UnsupportedUDT,\n      Seq(UnsupportedDataTypeInfo(\"col\", UnsupportedDataType)))\n  }\n\n  test(\"findUndefinedTypes: basic types\") {\n    val schema = StructType(Seq(\n      StructField(\"c1\", NullType),\n      StructField(\"c2\", BooleanType),\n      StructField(\"c3\", ByteType),\n      StructField(\"c4\", ShortType),\n      StructField(\"c5\", IntegerType),\n      StructField(\"c6\", LongType),\n      StructField(\"c7\", FloatType),\n      StructField(\"c8\", DoubleType),\n      StructField(\"c9\", StringType),\n      StructField(\"c10\", DateType),\n      StructField(\"c11\", TimestampType),\n      StructField(\"c12\", BinaryType),\n      StructField(\"c13\", DataTypes.createDecimalType()),\n      // undefined types\n      StructField(\"c14\", TimestampNTZType),\n      StructField(\"c15\", YearMonthIntervalType.DEFAULT),\n      StructField(\"c16\", DayTimeIntervalType.DEFAULT),\n      StructField(\"c17\", new PointUDT) // UserDefinedType\n    ))\n    val udts = findUndefinedTypes(schema)\n    assert(udts.map(_.getClass.getName.stripSuffix(\"$\")) ==\n      Seq(\n        classOf[TimestampNTZType],\n        classOf[YearMonthIntervalType],\n        classOf[DayTimeIntervalType],\n        classOf[PointUDT]\n      ).map(_.getName.stripSuffix(\"$\"))\n    )\n  }\n\n  test(\"findUndefinedTypes: complex types\") {\n    val schema = StructType(Seq(\n      StructField(\"c1\", new PointUDT),\n      StructField(\"c2\", ArrayType(new PointUDT, true)),\n      StructField(\"c3\", MapType(new PointUDT, new PointUDT, true)),\n      StructField(\"c4\", StructType(Seq(\n        StructField(\"c1\", new PointUDT),\n        StructField(\"c2\", ArrayType(new PointUDT, true)),\n        StructField(\"c3\", MapType(new PointUDT, new PointUDT, true))\n      )))\n    ))\n    val udts = findUndefinedTypes(schema)\n    assert(udts.size == 8)\n    assert(udts.map(_.getClass.getName).toSet == Set(classOf[PointUDT].getName))\n  }\n\n\n  test(\"check if column affects given dependent expressions\") {\n    val schema = StructType(Seq(\n      StructField(\"cArray\", ArrayType(StringType)),\n      StructField(\"cStruct\", StructType(Seq(\n        StructField(\"cMap\", MapType(IntegerType, ArrayType(BooleanType))),\n        StructField(\"cMapWithComplexKey\", MapType(StructType(Seq(\n          StructField(\"a\", ArrayType(StringType)),\n          StructField(\"b\", BooleanType)\n        )), IntegerType))\n      )))\n    ))\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cArray\"),\n        exprString = \"cast(cStruct.cMap as string) == '{}'\",\n        schema,\n        caseInsensitiveResolution) === false\n    )\n    // Extracting value from map uses key type as well.\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cStruct\", \"cMap\", \"key\"),\n        exprString = \"cStruct.cMap['random_key'] == 'string'\",\n        schema,\n        caseInsensitiveResolution) === true\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cstruct\"),\n        exprString = \"size(cStruct.cMap) == 0\",\n        schema,\n        caseSensitiveResolution) === false\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cStruct\", \"cMap\", \"key\"),\n        exprString = \"size(cArray) == 1\",\n        schema,\n        caseInsensitiveResolution) === false\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cStruct\", \"cMap\", \"key\"),\n        exprString = \"cStruct.cMapWithComplexKey[struct(cArray, false)] == 0\",\n        schema,\n        caseInsensitiveResolution) === false\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cArray\", \"element\"),\n        exprString = \"cStruct.cMapWithComplexKey[struct(cArray, false)] == 0\",\n        schema,\n        caseInsensitiveResolution) === true\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cStruct\", \"cMapWithComplexKey\", \"key\", \"b\"),\n        exprString = \"cStruct.cMapWithComplexKey[struct(cArray, false)] == 0\",\n        schema,\n        caseInsensitiveResolution) === true\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"cArray\", \"element\"),\n        exprString = \"concat_ws('', cArray) == 'string'\",\n        schema,\n        caseInsensitiveResolution) === true\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"CARRAY\"),\n        exprString = \"cArray[0] > 'a'\",\n        schema,\n        caseInsensitiveResolution) === true\n    )\n    assert(\n      SchemaUtils.containsDependentExpression(\n        spark,\n        columnToChange = Seq(\"CARRAY\", \"element\"),\n        exprString = \"cArray[0] > 'a'\",\n        schema,\n        caseSensitiveResolution) === false\n    )\n  }\n}\n\nobject UnsupportedDataType extends DataType {\n  override def defaultSize: Int = throw new UnsupportedOperationException(\"defaultSize\")\n  override def asNullable: DataType = throw new UnsupportedOperationException(\"asNullable\")\n  override def toString: String = \"UnsupportedDataType\"\n}\n\n@SQLUserDefinedType(udt = classOf[PointUDT])\ncase class Point(x: Int, y: Int)\n\nclass PointUDT extends UserDefinedType[Point] {\n  override def sqlType: DataType = StructType(Array(\n    StructField(\"x\", IntegerType, nullable = false),\n    StructField(\"y\", IntegerType, nullable = false)))\n\n  override def serialize(obj: Point): Any = InternalRow(obj.x, obj.y)\n\n  override def deserialize(datum: Any): Point = datum match {\n    case row: InternalRow => Point(row.getInt(0), row.getInt(1))\n  }\n\n  override def userClass: Class[Point] = classOf[Point]\n\n  override def toString: String = \"PointUDT\"\n}\n\nclass UnsupportedUDT extends PointUDT {\n  override def sqlType: DataType = UnsupportedDataType\n}\ncase class Struct1(\n    firstname: String,\n    numberone: Long,\n    lastname: String,\n    numbertwo: Long,\n    CorrectCase: String)\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlannedTableSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.sources.{And, EqualTo, Filter, GreaterThan, LessThan}\n\n/**\n * Tests for server-side planning with a mock client.\n */\nclass ServerSidePlannedTableSuite extends QueryTest with DeltaSQLCommandTest {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // Create test database and shared table once for all tests\n    sql(\"CREATE DATABASE IF NOT EXISTS test_db\")\n    sql(\"\"\"\n      CREATE TABLE test_db.shared_test (\n        id INT,\n        name STRING,\n        value INT,\n        a STRUCT<`b.c`: STRING>\n      ) USING parquet\n    \"\"\")\n    sql(\"\"\"\n      INSERT INTO test_db.shared_test (id, name, value, a) VALUES\n      (1, 'alpha', 10, struct('abc_1')),\n      (2, 'beta', 20, struct('abc_2')),\n      (3, 'gamma', 30, struct('abc_3'))\n    \"\"\")\n  }\n\n  /**\n   * Helper method to run tests with server-side planning enabled.\n   * Automatically sets up the test factory and config, then cleans up afterwards.\n   * This prevents test pollution from leaked configuration.\n   */\n  private def withServerSidePlanningEnabled(f: => Unit): Unit = {\n    withServerSidePlanningFactory(new TestServerSidePlanningClientFactory())(f)\n  }\n\n  /**\n   * Helper method to run tests with pushdown capturing enabled.\n   * TestServerSidePlanningClient captures pushdowns (filter, projection) passed to planScan().\n   */\n  private def withPushdownCapturingEnabled(f: => Unit): Unit = {\n    withServerSidePlanningFactory(new TestServerSidePlanningClientFactory()) {\n      try {\n        f\n      } finally {\n        TestServerSidePlanningClient.clearCaptured()\n      }\n    }\n  }\n\n  /**\n   * Common helper for setting up server-side planning with a specific factory.\n   */\n  private def withServerSidePlanningFactory(factory: ServerSidePlanningClientFactory)\n      (f: => Unit): Unit = {\n    val originalConfig = spark.conf.getOption(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key)\n    ServerSidePlanningClientFactory.setFactory(factory)\n    spark.conf.set(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key, \"true\")\n    try {\n      f\n    } finally {\n      // Reset factory\n      ServerSidePlanningClientFactory.clearFactory()\n      // Restore original config\n      originalConfig match {\n        case Some(value) => spark.conf.set(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key, value)\n        case None => spark.conf.unset(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key)\n      }\n    }\n  }\n\n  /**\n   * Extract all leaf filters from a filter tree.\n   * Spark may wrap filters with And and IsNotNull checks, so this flattens the tree.\n   */\n  private def collectLeafFilters(filter: Filter): Seq[Filter] = filter match {\n    case And(left, right) => collectLeafFilters(left) ++ collectLeafFilters(right)\n    case other => Seq(other)\n  }\n\n  test(\"full query through DeltaCatalog with server-side planning\") {\n    // This test verifies server-side planning works end-to-end by checking:\n    // (1) DeltaCatalog returns ServerSidePlannedTable (not normal table)\n    // (2) Query execution returns correct results\n    // If both are true, the server-side planning client worked correctly - that's the only way\n    // ServerSidePlannedTable can read data.\n\n    withServerSidePlanningEnabled {\n      // (1) Verify that DeltaCatalog actually returns ServerSidePlannedTable\n      val catalog = spark.sessionState.catalogManager.catalog(\"spark_catalog\")\n        .asInstanceOf[org.apache.spark.sql.connector.catalog.TableCatalog]\n      val loadedTable = catalog.loadTable(\n        org.apache.spark.sql.connector.catalog.Identifier.of(\n          Array(\"test_db\"), \"shared_test\"))\n      assert(loadedTable.isInstanceOf[ServerSidePlannedTable],\n        s\"Expected ServerSidePlannedTable but got ${loadedTable.getClass.getName}\")\n\n      // (2) Execute query - should go through full server-side planning stack\n      checkAnswer(\n        sql(\"SELECT id, name, value FROM test_db.shared_test ORDER BY id\"),\n        Seq(\n          Row(1, \"alpha\", 10),\n          Row(2, \"beta\", 20),\n          Row(3, \"gamma\", 30)\n        )\n      )\n    }\n  }\n\n  test(\"verify normal path unchanged when feature disabled\") {\n    // Explicitly disable server-side planning\n    spark.conf.set(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key, \"false\")\n\n    // Verify that DeltaCatalog returns normal table, not ServerSidePlannedTable\n    val catalog = spark.sessionState.catalogManager.catalog(\"spark_catalog\")\n      .asInstanceOf[org.apache.spark.sql.connector.catalog.TableCatalog]\n    val loadedTable = catalog.loadTable(\n      org.apache.spark.sql.connector.catalog.Identifier.of(\n        Array(\"test_db\"), \"shared_test\"))\n    assert(!loadedTable.isInstanceOf[ServerSidePlannedTable],\n      s\"Expected normal table but got ServerSidePlannedTable when config is disabled\")\n  }\n\n  test(\"shouldUseServerSidePlanning() decision logic\") {\n    // ============================================================\n    // Production mode: skipUCRequirementForTests = false\n    // Should return true ONLY when all three conditions are met:\n    // 1. Unity Catalog table\n    // 2. No credentials available\n    // 3. Enable flag is set\n    // ============================================================\n\n    assert(ServerSidePlannedTable.shouldUseServerSidePlanning(\n      isUnityCatalog = true,\n      hasCredentials = false,\n      enableServerSidePlanning = true,\n      skipUCRequirementForTests = false\n    ) == true, \"Production: UC without credentials + flag enabled should use SSP\")\n\n    // Group 1: flag disabled (even with valid UC setup)\n    assert(ServerSidePlannedTable.shouldUseServerSidePlanning(\n      isUnityCatalog = true,\n      hasCredentials = false,\n      enableServerSidePlanning = false,\n      skipUCRequirementForTests = false\n    ) == false, \"Production: UC without credentials but flag disabled should NOT use SSP\")\n\n    // Group 2: Has credentials (SSP not needed)\n    assert(ServerSidePlannedTable.shouldUseServerSidePlanning(\n      isUnityCatalog = true,\n      hasCredentials = true,\n      enableServerSidePlanning = true,\n      skipUCRequirementForTests = false\n    ) == false, \"Production: UC with credentials should NOT use SSP (has creds)\")\n\n    assert(ServerSidePlannedTable.shouldUseServerSidePlanning(\n      isUnityCatalog = true,\n      hasCredentials = true,\n      enableServerSidePlanning = false,\n      skipUCRequirementForTests = false\n    ) == false, \"Production: UC with credentials and no flag should NOT use SSP\")\n\n    // Group 3: Not Unity Catalog (always false, regardless of other params)\n    for (hasCreds <- Seq(true, false)) {\n      for (enableSSP <- Seq(true, false)) {\n        assert(ServerSidePlannedTable.shouldUseServerSidePlanning(\n          isUnityCatalog = false,\n          hasCredentials = hasCreds,\n          enableServerSidePlanning = enableSSP,\n          skipUCRequirementForTests = false\n        ) == false,\n          s\"Production: Non-UC should NOT use SSP (hasCreds=$hasCreds, enableSSP=$enableSSP)\")\n      }\n    }\n\n    // ============================================================\n    // Test mode: skipUCRequirementForTests = true\n    // Should return true if flag is enabled (UC/creds checks bypassed)\n    // Keep as loop since logic is simple and demonstrates bypass\n    // ============================================================\n    for (isUC <- Seq(true, false)) {\n      for (hasCreds <- Seq(true, false)) {\n        for (enableSSP <- Seq(true, false)) {\n          val description = s\"Test mode: isUC=$isUC, hasCreds=$hasCreds, enableSSP=$enableSSP\"\n          val expected = enableSSP  // In test mode, only the flag matters\n          val result = ServerSidePlannedTable.shouldUseServerSidePlanning(\n            isUnityCatalog = isUC,\n            hasCredentials = hasCreds,\n            enableServerSidePlanning = enableSSP,\n            skipUCRequirementForTests = true\n          )\n          assert(result == expected, s\"$description -> expected $expected but got $result\")\n        }\n      }\n    }\n  }\n\n  test(\"ServerSidePlannedTable is read-only\") {\n    withTable(\"readonly_test\") {\n      sql(\"\"\"\n        CREATE TABLE readonly_test (\n          id INT,\n          data STRING\n        ) USING parquet\n      \"\"\")\n\n      // First insert WITHOUT server-side planning should succeed\n      sql(\"INSERT INTO readonly_test VALUES (1, 'initial')\")\n      checkAnswer(\n        sql(\"SELECT * FROM readonly_test\"),\n        Seq(Row(1, \"initial\"))\n      )\n\n      // Try to insert WITH server-side planning enabled - should fail\n      withServerSidePlanningEnabled {\n        val exception = intercept[AnalysisException] {\n          sql(\"INSERT INTO readonly_test VALUES (2, 'should_fail')\")\n        }\n        assert(exception.getMessage.contains(\"does not support append\"))\n      }\n\n      // Verify data unchanged - second insert didn't happen\n      checkAnswer(\n        sql(\"SELECT * FROM readonly_test\"),\n        Seq(Row(1, \"initial\"))\n      )\n    }\n  }\n\n  test(\"ServerSidePlanningMetadata.fromTable returns empty defaults for non-UC catalogs\") {\n    import org.apache.spark.sql.connector.catalog.Identifier\n\n    // Create a simple identifier for testing\n    val ident = Identifier.of(Array(\"my_catalog\", \"my_schema\"), \"my_table\")\n\n    // Call fromTable with a null table (we only use the identifier for catalog name extraction)\n    val metadata = ServerSidePlanningMetadata.fromTable(\n      table = null,\n      spark = spark,\n      ident = ident,\n      isUnityCatalog = false\n    )\n\n    // Verify the metadata has expected defaults\n    assert(metadata.catalogName == \"my_catalog\")\n    assert(metadata.planningEndpointUri == \"\")\n    assert(metadata.authToken.isEmpty)\n    assert(metadata.tableProperties.isEmpty)\n  }\n\n  test(\"UnityCatalogMetadata constructs base IRC endpoint from UC URI\") {\n    val ucUri = \"https://unity-catalog-server.example.com\"\n    val metadata = UnityCatalogMetadata(\n      catalogName = \"test_catalog\",\n      ucUri = ucUri,\n      ucToken = \"test-token\",\n      tableProps = Map.empty\n    )\n\n    // UnityCatalogMetadata returns the base Iceberg REST path up to /v1.\n    // The IcebergRESTCatalogPlanningClient then calls config to get the prefix\n    // and constructs the full endpoint URL per the Iceberg REST catalog spec.\n    val expectedEndpoint =\n      \"https://unity-catalog-server.example.com/api/2.1/unity-catalog/\" +\n      \"iceberg-rest/v1\"\n    assert(metadata.planningEndpointUri == expectedEndpoint)\n  }\n\n  test(\"simple EqualTo filter pushed to planning client\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id, name, value FROM test_db.shared_test WHERE id = 2\").collect()\n\n      val capturedFilter = TestServerSidePlanningClient.getCapturedFilter\n      assert(capturedFilter.isDefined, \"Filter should be pushed down\")\n\n      // Extract leaf filters and find the EqualTo filter\n      val leafFilters = collectLeafFilters(capturedFilter.get)\n      val eqFilter = leafFilters.collectFirst {\n        case eq: EqualTo if eq.attribute == \"id\" => eq\n      }\n      assert(eqFilter.isDefined, \"Expected EqualTo filter on 'id'\")\n      assert(eqFilter.get.value == 2, s\"Expected EqualTo value 2, got ${eqFilter.get.value}\")\n    }\n  }\n\n  test(\"compound And filter pushed to planning client\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id, name, value FROM test_db.shared_test WHERE id > 1 AND value < 30\").collect()\n\n      val capturedFilter = TestServerSidePlanningClient.getCapturedFilter\n      assert(capturedFilter.isDefined, \"Filter should be pushed down\")\n\n      val filter = capturedFilter.get\n      assert(filter.isInstanceOf[And], s\"Expected And filter, got ${filter.getClass.getSimpleName}\")\n\n      // Extract all leaf filters from the And tree (Spark may add IsNotNull checks)\n      val leafFilters = collectLeafFilters(filter)\n\n      // Verify GreaterThan(id, 1) is present\n      val gtFilter = leafFilters.collectFirst {\n        case gt: GreaterThan if gt.attribute == \"id\" => gt\n      }\n      assert(gtFilter.isDefined, \"Expected GreaterThan filter on 'id'\")\n      assert(gtFilter.get.value == 1, s\"Expected GreaterThan value 1, got ${gtFilter.get.value}\")\n\n      // Verify LessThan(value, 30) is present\n      val ltFilter = leafFilters.collectFirst {\n        case lt: LessThan if lt.attribute == \"value\" => lt\n      }\n      assert(ltFilter.isDefined, \"Expected LessThan filter on 'value'\")\n      assert(ltFilter.get.value == 30, s\"Expected LessThan value 30, got ${ltFilter.get.value}\")\n    }\n  }\n\n  test(\"no filter pushed when no WHERE clause\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id, name, value FROM test_db.shared_test\").collect()\n\n      val capturedFilter = TestServerSidePlanningClient.getCapturedFilter\n      assert(capturedFilter.isEmpty, \"No filter should be pushed when there's no WHERE clause\")\n    }\n  }\n\n  test(\"projection pushed when selecting specific columns\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id, name FROM test_db.shared_test\").collect()\n\n      val capturedProjection = TestServerSidePlanningClient.getCapturedProjection\n      assert(capturedProjection.isDefined, \"Projection should be pushed down\")\n      assert(capturedProjection.get.toSet == Set(\"id\", \"name\"),\n        s\"Expected {id, name}, got {${capturedProjection.get.mkString(\", \")}}\")\n    }\n  }\n\n  test(\"no projection pushed when selecting all columns\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT * FROM test_db.shared_test\").collect()\n\n      val capturedProjection = TestServerSidePlanningClient.getCapturedProjection\n      assert(capturedProjection.isEmpty,\n        \"No projection should be pushed when selecting all columns\")\n    }\n  }\n\n  test(\"projection escaping with dotted column names\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT a.`b.c` FROM test_db.shared_test\").collect()\n\n      val capturedProjection = TestServerSidePlanningClient.getCapturedProjection\n      assert(capturedProjection.isDefined, \"Projection should be pushed down\")\n      assert(capturedProjection.get == Seq(\"a.`b.c`\"),\n        s\"Expected escaped [a.`b.c`], got ${capturedProjection.get}\")\n    }\n  }\n\n  test(\"projection and filter pushed together\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id FROM test_db.shared_test WHERE value > 10\").collect()\n\n      val capturedProjection = TestServerSidePlanningClient.getCapturedProjection\n      assert(capturedProjection.isDefined, \"Projection should be pushed down\")\n      val projectedFields = capturedProjection.get.toSet\n      assert(projectedFields == Set(\"id\"),\n        s\"Expected projection with only SELECT columns {id}, \" +\n        s\"got {${projectedFields.mkString(\", \")}}\")\n\n      // Verify filter was also pushed\n      val capturedFilter = TestServerSidePlanningClient.getCapturedFilter\n      assert(capturedFilter.isDefined, \"Filter should be pushed down\")\n\n      // Verify GreaterThan(value, 10) is in the filter\n      val leafFilters = collectLeafFilters(capturedFilter.get)\n      val gtFilter = leafFilters.collectFirst {\n        case gt: GreaterThan if gt.attribute == \"value\" => gt\n      }\n      assert(gtFilter.isDefined, \"Expected GreaterThan filter on 'value'\")\n      assert(gtFilter.get.value == 10, s\"Expected GreaterThan value 10, got ${gtFilter.get.value}\")\n    }\n  }\n\n  test(\"projection and limit pushed together\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id FROM test_db.shared_test LIMIT 5\").collect()\n\n      // Verify projection was pushed (only 'id' column)\n      val capturedProjection = TestServerSidePlanningClient.getCapturedProjection\n      assert(capturedProjection.isDefined, \"Projection should be pushed down\")\n      val projectedFields = capturedProjection.get.toSet\n      assert(projectedFields == Set(\"id\"),\n        s\"Expected projection with just {id}, got {${projectedFields.mkString(\", \")}}\")\n\n      // Verify limit was pushed\n      val capturedLimit = TestServerSidePlanningClient.getCapturedLimit\n      assert(capturedLimit.isDefined, \"Limit should be pushed down\")\n      assert(capturedLimit.get == 5, s\"Expected limit 5, got ${capturedLimit.get}\")\n    }\n  }\n\n  test(\"limit pushed to planning client\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id, name, value FROM test_db.shared_test LIMIT 2\").collect()\n\n      val capturedLimit = TestServerSidePlanningClient.getCapturedLimit\n      assert(capturedLimit.isDefined, \"Limit should be pushed down\")\n      assert(capturedLimit.get == 2, s\"Expected limit 2, got ${capturedLimit.get}\")\n    }\n  }\n\n  test(\"no limit pushed when no LIMIT clause\") {\n    withPushdownCapturingEnabled {\n      sql(\"SELECT id, name, value FROM test_db.shared_test\").collect()\n\n      val capturedLimit = TestServerSidePlanningClient.getCapturedLimit\n      assert(capturedLimit.isEmpty, \"No limit should be pushed when there's no LIMIT clause\")\n    }\n  }\n\n  test(\"filter and limit pushed together when all filters are convertible\") {\n    withPushdownCapturingEnabled {\n      // Query with convertible filter (GreaterThan) AND limit\n      sql(\"SELECT id FROM test_db.shared_test WHERE value > 10 LIMIT 5\").collect()\n\n      // Verify filter was captured\n      val capturedFilter = TestServerSidePlanningClient.getCapturedFilter\n      assert(capturedFilter.isDefined, \"Filter should be pushed to server\")\n\n      // Verify limit was captured (this is the key test - limit pushdown with filters)\n      val capturedLimit = TestServerSidePlanningClient.getCapturedLimit\n      assert(capturedLimit.isDefined,\n        \"Limit should be pushed to server when all filters are convertible\")\n      assert(capturedLimit.get == 5, s\"Expected limit 5, got ${capturedLimit.get}\")\n    }\n  }\n\n  test(\"limit NOT pushed when any filter is unconvertible\") {\n    withPushdownCapturingEnabled {\n      // Configure the test client to treat filters as unconvertible\n      // This simulates a scenario where the filter cannot be converted to server's native format\n      TestServerSidePlanningClient.setFiltersConvertible(false)\n\n      try {\n        // Query with filter AND limit\n        sql(\"SELECT id FROM test_db.shared_test WHERE value > 10 LIMIT 5\").collect()\n\n        // Verify filter was still captured (server receives it)\n        val capturedFilter = TestServerSidePlanningClient.getCapturedFilter\n        assert(capturedFilter.isDefined, \"Filter should still be sent to server\")\n\n        // Verify limit was NOT captured (because residual filters blocked pushdown)\n        val capturedLimit = TestServerSidePlanningClient.getCapturedLimit\n        assert(capturedLimit.isEmpty,\n          \"Limit should NOT be pushed when any filter is unconvertible\")\n      } finally {\n        // Reset to default (convertible) for other tests\n        TestServerSidePlanningClient.setFiltersConvertible(true)\n      }\n    }\n  }\n\n  test(\"avoid planInputPartitions call during Spark query planning\") {\n    withPushdownCapturingEnabled {\n      sql(\"EXPLAIN EXTENDED SELECT id, name FROM test_db.shared_test\").collect()\n      val capturedProjection = TestServerSidePlanningClient.getCapturedProjection\n      assert(capturedProjection.isEmpty, \"Should not fire a planTable request for EXPLAIN\")\n    }\n  }\n\n  test(\"ServerSidePlannedTable closes planning client on close\") {\n    var clientClosed = false\n    val trackingClient = new ServerSidePlanningClient {\n      override def planScan(\n          databaseName: String,\n          table: String,\n          filterOption: Option[Filter],\n          projectionOption: Option[Seq[String]],\n          limitOption: Option[Int]): ScanPlan = ScanPlan(Seq.empty)\n      override def canConvertFilters(filters: Array[Filter]): Boolean = true\n      override def close(): Unit = { clientClosed = true }\n    }\n\n    val table = new ServerSidePlannedTable(\n      spark, \"test_db\", \"test_table\", new org.apache.spark.sql.types.StructType(), trackingClient)\n    assert(!clientClosed, \"Client should not be closed before table.close()\")\n    table.close()\n    assert(clientClosed, \"Client should be closed after table.close()\")\n  }\n\n  test(\"planning client is closed after scan completes\") {\n    withPushdownCapturingEnabled {\n      assert(!TestServerSidePlanningClient.isClientClosed,\n        \"Client should not be closed before query execution\")\n\n      sql(\"SELECT id FROM test_db.shared_test\").collect()\n\n      assert(TestServerSidePlanningClient.isClientClosed,\n        \"Planning client should be closed after scan plan is obtained\")\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlanningClientFactorySuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Unit tests for ServerSidePlanningClientFactory core functionality.\n * Tests manual factory registration, state management, and lifecycle.\n */\nclass ServerSidePlanningClientFactorySuite extends QueryTest with SharedSparkSession {\n\n  override def afterEach(): Unit = {\n    try {\n      ServerSidePlanningClientFactory.clearFactory()\n    } finally {\n      super.afterEach()\n    }\n  }\n\n  // ========== Test Infrastructure ==========\n\n  /**\n   * Execute test block with clean factory state (setup + teardown).\n   * Ensures factory is cleared before and after the test.\n   */\n  private def withCleanFactory[T](testFn: => T): T = {\n    try {\n      ServerSidePlanningClientFactory.clearFactory()\n      testFn\n    } finally {\n      ServerSidePlanningClientFactory.clearFactory()\n    }\n  }\n\n  /**\n   * Assert that getFactory() throws IllegalStateException indicating no factory is registered.\n   */\n  private def assertNoFactory(context: String = \"\"): Unit = {\n    val prefix = if (context.nonEmpty) s\"[$context] \" else \"\"\n    val exception = intercept[IllegalStateException] {\n      ServerSidePlanningClientFactory.getFactory()\n    }\n    assert(\n      exception.getMessage.contains(\"No ServerSidePlanningClientFactory has been registered\"),\n      s\"${prefix}Expected 'No ServerSidePlanningClientFactory' message, \" +\n        s\"got: ${exception.getMessage}\")\n  }\n\n  // ========== Tests ==========\n\n  test(\"manual setFactory can replace existing factory\") {\n    withCleanFactory {\n      // Set first factory\n      val firstFactory = new TestServerSidePlanningClientFactory()\n      ServerSidePlanningClientFactory.setFactory(firstFactory)\n      assert(ServerSidePlanningClientFactory.getFactory() eq firstFactory,\n        \"Should return first factory\")\n\n      // Replace with second factory\n      val secondFactory = new TestServerSidePlanningClientFactory()\n      ServerSidePlanningClientFactory.setFactory(secondFactory)\n\n      // Verify replacement\n      val retrievedFactory = ServerSidePlanningClientFactory.getFactory()\n      assert(retrievedFactory eq secondFactory,\n        \"getFactory() should return the second factory after replacement\")\n      assert(!(retrievedFactory eq firstFactory),\n        \"Should not return the first factory\")\n    }\n  }\n\n  test(\"getFactory returns same instance across multiple calls\") {\n    withCleanFactory {\n      val testFactory = new TestServerSidePlanningClientFactory()\n      ServerSidePlanningClientFactory.setFactory(testFactory)\n\n      val factory1 = ServerSidePlanningClientFactory.getFactory()\n      val factory2 = ServerSidePlanningClientFactory.getFactory()\n      val factory3 = ServerSidePlanningClientFactory.getFactory()\n\n      assert(factory1 eq factory2, \"Second call should return same instance as first\")\n      assert(factory2 eq factory3, \"Third call should return same instance as second\")\n      assert(factory1 eq testFactory, \"Should return the originally set factory\")\n    }\n  }\n\n  test(\"clearFactory resets registration state\") {\n    withCleanFactory {\n      val testFactory = new TestServerSidePlanningClientFactory()\n      ServerSidePlanningClientFactory.setFactory(testFactory)\n\n      // Verify factory is registered by successfully retrieving it\n      assert(ServerSidePlanningClientFactory.getFactory() eq testFactory,\n        \"Factory should be registered and retrievable\")\n\n      // Clear factory\n      ServerSidePlanningClientFactory.clearFactory()\n\n      // Verify factory is no longer registered\n      assertNoFactory(\"after clearFactory\")\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/serverSidePlanning/TestServerSidePlanningClient.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.serverSidePlanning\n\nimport org.apache.hadoop.fs.Path\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.functions.input_file_name\nimport org.apache.spark.sql.sources.Filter\n\n/**\n * Implementation of ServerSidePlanningClient that uses Spark SQL with input_file_name()\n * to discover the list of files in a table. This allows end-to-end testing without\n * a real server that can do server-side planning.\n *\n * Also captures filter/projection parameters for test verification via companion object.\n */\nclass TestServerSidePlanningClient(spark: SparkSession) extends ServerSidePlanningClient {\n\n  override def planScan(\n      databaseName: String,\n      table: String,\n      filterOption: Option[Filter] = None,\n      projectionOption: Option[Seq[String]] = None,\n      limitOption: Option[Int] = None): ScanPlan = {\n    // Capture filter, projection, and limit for test verification\n    TestServerSidePlanningClient.capturedFilter = filterOption\n    TestServerSidePlanningClient.capturedProjection = projectionOption\n    TestServerSidePlanningClient.capturedLimit = limitOption\n    val fullTableName = s\"$databaseName.$table\"\n\n    // Temporarily disable server-side planning to avoid infinite recursion\n    // when this test client internally loads the table\n    val originalConfigValue = spark.conf.getOption(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key)\n    spark.conf.set(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key, \"false\")\n\n    try {\n      // Use input_file_name() to get the list of files\n      // Query: SELECT DISTINCT input_file_name() FROM table\n      val filesDF = spark.table(fullTableName)\n        .select(input_file_name().as(\"file_path\"))\n        .distinct()\n\n      // Collect file paths\n      val filePaths = filesDF.collect().map(_.getString(0))\n\n      // Get file metadata (size, format) from filesystem\n      // scalastyle:off deltahadoopconfiguration\n      // The rule prevents accessing Hadoop conf on executors where it could use wrong credentials\n      // for multi-catalog scenarios. Safe here: test-only code simulating server filesystem access.\n      val hadoopConf = spark.sessionState.newHadoopConf()\n      // scalastyle:on deltahadoopconfiguration\n      val files = filePaths.map { filePath =>\n        // input_file_name() returns URL-encoded paths, decode them\n        val decodedPath = java.net.URLDecoder.decode(filePath, \"UTF-8\")\n        val path = new Path(decodedPath)\n        val fs = path.getFileSystem(hadoopConf)\n        val fileStatus = fs.getFileStatus(path)\n\n        ScanFile(\n          filePath = decodedPath,\n          fileSizeInBytes = fileStatus.getLen,\n          fileFormat = getFileFormat(path)\n        )\n      }.toSeq\n\n      ScanPlan(files = files)\n    } finally {\n      // Restore original config value\n      originalConfigValue match {\n        case Some(value) => spark.conf.set(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key, value)\n        case None => spark.conf.unset(DeltaSQLConf.ENABLE_SERVER_SIDE_PLANNING.key)\n      }\n    }\n  }\n\n  override def canConvertFilters(filters: Array[Filter]): Boolean = {\n    // For testing: check if filters should be treated as convertible\n    // Tests can configure this via TestServerSidePlanningClient.setFiltersConvertible()\n    TestServerSidePlanningClient.filtersConvertible\n  }\n\n  override def close(): Unit = {\n    TestServerSidePlanningClient.clientClosed = true\n  }\n\n  private def getFileFormat(path: Path): String = \"parquet\"\n}\n\n/**\n * Companion object for TestServerSidePlanningClient.\n * Stores captured pushdown parameters (filter, projection, limit) for test verification.\n */\nobject TestServerSidePlanningClient {\n  private var capturedFilter: Option[Filter] = None\n  private var capturedProjection: Option[Seq[String]] = None\n  private var capturedLimit: Option[Int] = None\n  private var filtersConvertible: Boolean = true  // Default: all filters convertible\n  private[serverSidePlanning] var clientClosed: Boolean = false\n\n  def getCapturedFilter: Option[Filter] = capturedFilter\n  def getCapturedProjection: Option[Seq[String]] = capturedProjection\n  def getCapturedLimit: Option[Int] = capturedLimit\n  def isClientClosed: Boolean = clientClosed\n\n  /**\n   * Configure whether filters should be treated as convertible.\n   * Used for testing filter conversion failure scenarios.\n   */\n  def setFiltersConvertible(convertible: Boolean): Unit = {\n    filtersConvertible = convertible\n  }\n\n  def clearCaptured(): Unit = {\n    capturedFilter = None\n    capturedProjection = None\n    capturedLimit = None\n    filtersConvertible = true  // Reset to default\n    clientClosed = false\n  }\n}\n\n/**\n * Factory for creating TestServerSidePlanningClient instances.\n */\nclass TestServerSidePlanningClientFactory extends ServerSidePlanningClientFactory {\n  override def buildClient(\n      spark: SparkSession,\n      metadata: ServerSidePlanningMetadata): ServerSidePlanningClient = {\n    new TestServerSidePlanningClient(spark)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/ClusteredTableTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping\n\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumn, ClusteringColumnInfo}\nimport org.apache.spark.sql.delta.skipping.clustering.temp.ClusterBySpec\nimport org.apache.spark.sql.delta.{DeltaLog, Snapshot}\nimport org.apache.spark.sql.delta.DeltaOperations\nimport org.apache.spark.sql.delta.DeltaOperations.{CLUSTERING_PARAMETER_KEY, ZORDER_PARAMETER_KEY}\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedTableUtils, CatalogOwnedTestBaseSuite, CoordinatedCommitsBaseSuite}\nimport org.apache.spark.sql.delta.hooks.UpdateCatalog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.junit.Assert.assertEquals\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\ntrait ClusteredTableTestUtilsBase\n  extends SparkFunSuite\n  with SharedSparkSession\n  with CatalogOwnedTestBaseSuite {\n\n  import testImplicits._\n\n  /**\n   * Helper for running optimize on the table with different APIs.\n   * @param table the name of table\n   */\n  def optimizeTable(table: String): DataFrame = {\n    sql(s\"OPTIMIZE $table\")\n  }\n\n  /**\n   * Runs optimize on the table and calls postHook on the metrics.\n   * @param table the name of table\n   * @param postHook callback triggered with OptimizeMetrics returned by the OPTIMIZE command\n   */\n  def runOptimize(table: String)(postHook: OptimizeMetrics => Unit): Unit = {\n    // Verify Delta history operation parameters' clusterBy\n    val isPathBasedTable = table.startsWith(\"tahoe.\") || table.startsWith(\"delta.\")\n    var (deltaLog, snapshot) = if (isPathBasedTable) {\n      // Path based table e.g. delta.`path-to-directory` or tahoe.`path-to-directory`. Strip\n      // 6 characters to extract table path.\n      DeltaLog.forTableWithSnapshot(spark, table.drop(6).replace(\"`\", \"\"))\n    } else {\n      DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n    }\n    val beforeVersion = snapshot.version\n\n    postHook(optimizeTable(table).select($\"metrics.*\").as[OptimizeMetrics].head())\n    snapshot = deltaLog.update()\n    val afterVersion = snapshot.version\n\n    val shouldCheckFullStatus = deltaLog.history.getHistory(Some(1)).headOption.exists { h =>\n      Seq(DeltaOperations.OPTIMIZE_OPERATION_NAME\n      ).contains(h.operation)\n    }\n\n    // Note: Only expect isFull status when the table has non-empty clustering columns and\n    // clustering table feature, otherwise the OPTIMIZE will fall back to compaction and\n    // isFull status will not be relevant anymore.\n    val expectedOperationParameters = ClusteredTableUtils\n      .getClusteringColumnsOptional(snapshot)\n      .filter { cols =>\n        cols.nonEmpty &&\n          shouldCheckFullStatus &&\n          ClusteredTableUtils.isSupported(snapshot.protocol) &&\n          afterVersion > beforeVersion\n      }\n      .map(_ => Map(DeltaOperations.CLUSTERING_IS_FULL_KEY -> false))\n      .getOrElse(Map.empty)\n    verifyDescribeHistoryOperationParameters(\n      table, expectedOperationParameters = expectedOperationParameters)\n  }\n\n  /**\n   * Runs optimize full on the table and calls postHook on the metrics.\n   *\n   * @param table    the name of table\n   * @param postHook callback triggered with OptimizeMetrics returned by the OPTIMIZE command\n   */\n  def runOptimizeFull(table: String)(postHook: OptimizeMetrics => Unit): Unit = {\n    postHook(sql(s\"OPTIMIZE $table FULL\").select($\"metrics.*\").as[OptimizeMetrics].head())\n\n    // Verify Delta history operation parameters' clusterBy\n    verifyDescribeHistoryOperationParameters(table, expectedOperationParameters = Map(\n      DeltaOperations.CLUSTERING_IS_FULL_KEY -> true))\n  }\n\n  def verifyClusteringColumnsInDomainMetadata(\n      snapshot: Snapshot,\n      logicalColumnNames: Seq[String]): Unit = {\n    val expectedClusteringColumns = logicalColumnNames.map(ClusteringColumn(snapshot.schema, _))\n    val actualClusteringColumns =\n      ClusteredTableUtils.getClusteringColumnsOptional(snapshot).orNull\n    assert(expectedClusteringColumns == actualClusteringColumns)\n  }\n\n  // Verify the operation parameters of the last history event contains `clusterBy`.\n  protected def verifyDescribeHistoryOperationParameters(\n      table: String,\n      expectedOperationParameters: Map[String, Any] = Map.empty): Unit = {\n    val clusterBySupportedOperations = Set(\n      \"CREATE TABLE\",\n      \"REPLACE TABLE\",\n      \"CREATE OR REPLACE TABLE\",\n      \"CREATE TABLE AS SELECT\",\n      \"REPLACE TABLE AS SELECT\",\n      \"CREATE OR REPLACE TABLE AS SELECT\")\n\n    val isPathBasedTable = table.startsWith(\"tahoe.\") || table.startsWith(\"delta.\")\n    val (deltaLog, snapshot) = if (isPathBasedTable) {\n      // Path based table.\n      DeltaLog.forTableWithSnapshot(spark, table.drop(6).replace(\"`\", \"\"))\n    } else {\n      DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n    }\n    val isClusteredTable = ClusteredTableUtils.isSupported(snapshot.protocol)\n    val clusteringColumns =\n      ClusteringColumnInfo.extractLogicalNames(snapshot)\n    val expectedClusterBy = JsonUtils.toJson(clusteringColumns)\n    val expectClustering = isClusteredTable && clusteringColumns.nonEmpty\n\n    val lastEvent = deltaLog.history.getHistory(Some(1)).head\n    val lastOperationParameters = lastEvent.operationParameters\n\n    def doAssert(assertion: => Boolean): Unit = {\n      val debugMsg = \"verifyDescribeHistoryOperationParameters DEBUG: \" +\n        \"assert failed. Please check the expected behavior and \" +\n        \"add the operation to the appropriate case in \" +\n        \"verifyDescribeHistoryOperationParameters. \" +\n        s\"table: $table, lastOperation: ${lastEvent.operation} \" +\n        s\"lastOperationParameters: $lastOperationParameters \" +\n        s\"expectedOperationParameters: $expectedOperationParameters\"\n      try {\n        assert(assertion, debugMsg)\n      } catch {\n        case e: Throwable =>\n          throw new Throwable(debugMsg, e)\n      }\n    }\n\n    // Check clusterBy exists and matches the expected clusterBy.\n    def assertClusterByExists(): Unit = {\n      doAssert(lastOperationParameters(CLUSTERING_PARAMETER_KEY) === expectedClusterBy)\n    }\n\n    // Check clusterBy does not exist or is empty.\n    def assertClusterByEmptyOrNotExists(): Unit = {\n      doAssert(!lastOperationParameters.contains(CLUSTERING_PARAMETER_KEY) ||\n        lastOperationParameters(CLUSTERING_PARAMETER_KEY) === \"[]\")\n    }\n\n    // Check clusterBy does not exist.\n    def assertClusterByNotExist(): Unit = {\n      doAssert(!lastOperationParameters.contains(CLUSTERING_PARAMETER_KEY))\n    }\n\n    // Validate caller provided operator parameters from the last commit.\n    for ((operationParameterKey, value) <- expectedOperationParameters) {\n      // Convert value to string since value is stored as toString in operationParameters.\n      doAssert(lastOperationParameters(operationParameterKey) === value.toString)\n    }\n\n    // Check clusterBy\n    lastEvent.operation match {\n      case \"CLUSTER BY\" =>\n        // Operation is [[DeltaOperations.ClusterBy]] - ALTER TABLE CLUSTER BY\n        doAssert(\n          lastOperationParameters(\"newClusteringColumns\") === clusteringColumns.mkString(\",\")\n        )\n      case \"OPTIMIZE\" =>\n        if (expectClustering) {\n          doAssert(lastOperationParameters(CLUSTERING_PARAMETER_KEY) === expectedClusterBy)\n          doAssert(lastOperationParameters(ZORDER_PARAMETER_KEY) === \"[]\")\n        } else {\n          // If the table clusters by NONE, OPTIMIZE will be a regular compaction.\n          // In this case, both clustering and z-order parameters should be empty.\n          doAssert(lastOperationParameters(CLUSTERING_PARAMETER_KEY) === \"[]\")\n          doAssert(lastOperationParameters(ZORDER_PARAMETER_KEY) === \"[]\")\n        }\n      case \"CLONE\" =>\n        // CLUSTER BY not in operation parameters for CLONE - similar to PARTITION BY.\n        doAssert(!lastOperationParameters.contains(CLUSTERING_PARAMETER_KEY))\n      case o if clusterBySupportedOperations.contains(o) =>\n        if (expectClustering) {\n          assertClusterByExists()\n        } else if (isClusteredTable && clusteringColumns.isEmpty) {\n          assertClusterByEmptyOrNotExists()\n        } else {\n          assertClusterByNotExist()\n        }\n      case \"WRITE\" | \"RESTORE\" =>\n        // These are known operations from our tests that don't have clusterBy.\n        doAssert(!lastOperationParameters.contains(CLUSTERING_PARAMETER_KEY))\n      case _ =>\n        // Other operations are not tested yet. If the test fails here, please check the expected\n        // behavior and add the operation to the appropriate case.\n        doAssert(false)\n    }\n  }\n\n  protected def deleteTableFromCommitCoordinatorIfNeeded(table: String): Unit = {\n    // Clean up the table data in commit coordinator because DROP/REPLACE TABLE does not bother\n    // commit coordinator.\n    if (CatalogOwnedTableUtils.defaultCatalogOwnedEnabled(spark) &&\n        catalogOwnedDefaultCreationEnabledInTests) {\n      deleteCatalogOwnedTableFromCommitCoordinator(table)\n    }\n  }\n\n  override def withTable(tableNames: String*)(f: => Unit): Unit = {\n    Utils.tryWithSafeFinally(f) {\n      tableNames.foreach { name =>\n        deleteTableFromCommitCoordinatorIfNeeded(name)\n        spark.sql(s\"DROP TABLE IF EXISTS $name\")\n      }\n    }\n  }\n\n  def withClusteredTable[T](\n      table: String,\n      schema: String,\n      clusterBy: String,\n      tableProperties: Map[String, String] = Map.empty,\n      location: Option[String] = None)(f: => T): T = {\n    createOrReplaceClusteredTable(\"CREATE\", table, schema, clusterBy, tableProperties, location)\n\n    Utils.tryWithSafeFinally(f) {\n      deleteTableFromCommitCoordinatorIfNeeded(table)\n      spark.sql(s\"DROP TABLE IF EXISTS $table\")\n    }\n  }\n\n  /**\n   * Helper for creating or replacing table with different APIs.\n   * @param clause clause for SQL API ('CREATE', 'REPLACE', 'CREATE OR REPLACE')\n   * @param table the name of table\n   * @param schema comma separated list of \"colName dataType\"\n   * @param clusterBy comma separated list of clustering columns\n   */\n  def createOrReplaceClusteredTable(\n      clause: String,\n      table: String,\n      schema: String,\n      clusterBy: String,\n      tableProperties: Map[String, String] = Map.empty,\n      location: Option[String] = None): Unit = {\n    val locationClause = if (location.isEmpty) \"\" else s\"LOCATION '${location.get}'\"\n    val tablePropertiesClause = if (!tableProperties.isEmpty) {\n      val tablePropertiesString = tableProperties.map {\n        case (key, value) => s\"'$key' = '$value'\"\n      }.mkString(\",\")\n      s\"TBLPROPERTIES($tablePropertiesString)\"\n    } else {\n      \"\"\n    }\n    spark.sql(s\"$clause TABLE $table ($schema) USING delta CLUSTER BY ($clusterBy) \" +\n      s\"$tablePropertiesClause $locationClause\")\n    location.foreach { loc => locRefCount(loc) = locRefCount.getOrElse(loc, 0) + 1 }\n  }\n\n  protected def createOrReplaceAsSelectClusteredTable(\n      clause: String,\n      table: String,\n      srcTable: String,\n      clusterBy: String,\n      location: Option[String] = None): Unit = {\n    val locationClause = if (location.isEmpty) \"\" else s\"LOCATION '${location.get}'\"\n    spark.sql(s\"$clause TABLE $table USING delta CLUSTER BY ($clusterBy) \" +\n      s\"$locationClause AS SELECT * FROM $srcTable\")\n  }\n\n  def verifyClusteringColumns(\n      tableIdentifier: TableIdentifier,\n      expectedLogicalClusteringColumns: Seq[String],\n      skipCatalogCheck: Boolean = false\n    ): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n    verifyClusteringColumnsInternal(\n      snapshot,\n      tableIdentifier.table,\n      expectedLogicalClusteringColumns\n    )\n\n    if (skipCatalogCheck) {\n      return\n    }\n\n    val updateCatalogEnabled = spark.conf.get(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED)\n    assert(updateCatalogEnabled,\n      \"need to enable [[DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED]] to verify catalog updates.\")\n    UpdateCatalog.awaitCompletion(10000)\n    val catalog = spark.sessionState.catalog\n    catalog.refreshTable(tableIdentifier)\n    val table = catalog.getTableMetadata(tableIdentifier)\n\n    // Verify CatalogTable's clusterBySpec.\n    assert(ClusteredTableUtils.getClusterBySpecOptional(table).isDefined)\n    assertEquals(ClusterBySpec.fromColumnNames(expectedLogicalClusteringColumns),\n      ClusteredTableUtils.getClusterBySpecOptional(table).get)\n  }\n\n  def verifyClusteringColumns(\n      dataPath: String,\n      expectedLogicalClusteringColumns: Seq[String]): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, dataPath)\n    verifyClusteringColumnsInternal(\n      snapshot,\n      s\"delta.`$dataPath`\",\n      expectedLogicalClusteringColumns\n    )\n  }\n\n  def verifyClusteringColumnsInternal(\n      snapshot: Snapshot,\n      tableNameOrPath: String,\n      expectedLogicalClusteringColumns: Seq[String]\n    ): Unit = {\n    assert(ClusteredTableUtils.isSupported(snapshot.protocol) === true)\n    verifyClusteringColumnsInDomainMetadata(snapshot, expectedLogicalClusteringColumns)\n\n    // Verify Delta history operation parameters' clusterBy\n    verifyDescribeHistoryOperationParameters(\n      tableNameOrPath\n    )\n\n    // Verify DESCRIBE DETAIL's properties doesn't contain the \"clusteringColumns\" key.\n    val describeDetailProps = sql(s\"describe detail $tableNameOrPath\")\n      .select(\"properties\")\n      .first\n      .getAs[Map[String, String]](0)\n    assert(!describeDetailProps.contains(ClusteredTableUtils.PROP_CLUSTERING_COLUMNS))\n\n    // Verify SHOW TBLPROPERTIES contains the correct clustering columns.\n    val clusteringColumnsVal =\n      sql(s\"show tblproperties $tableNameOrPath\")\n        .filter($\"key\" === ClusteredTableUtils.PROP_CLUSTERING_COLUMNS)\n        .select(\"value\")\n        .first\n        .getString(0)\n    val clusterBySpec = ClusterBySpec.fromProperties(\n      Map(ClusteredTableUtils.PROP_CLUSTERING_COLUMNS -> clusteringColumnsVal)).get\n    assert(expectedLogicalClusteringColumns === clusterBySpec.columnNames.map(_.toString))\n  }\n}\n\ntrait ClusteredTableTestUtils extends ClusteredTableTestUtilsBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/MultiDimClusteringFunctionsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping\n\nimport java.nio.ByteBuffer\n\nimport scala.util.Random\n\nimport org.apache.spark.sql.delta.expressions.{HilbertByteArrayIndex, HilbertLongIndex}\nimport org.apache.spark.sql.delta.skipping.MultiDimClusteringFunctions._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.expressions.Cast\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/** Tests for [[MultiDimClusterFunctions]] */\nclass MultiDimClusteringFunctionsSuite extends QueryTest\n    with SharedSparkSession with DeltaSQLCommandTest {\n  import testImplicits._\n\n  test(\"range_partition_id(): simple\") {\n    val numTuples = 20\n    val data = 0.to(numTuples - 1)\n\n    for { div <- Seq(1, 2, 4, 5, 10, 20) } {\n      checkAnswer(\n        Random.shuffle(data).toDF(\"col\")\n          .withColumn(\"rpi\", range_partition_id($\"col\", data.size / div)),\n        data.map(i => Row(i, i / div))\n      )\n    }\n  }\n\n  test(\"range_partition_id(): two columns\") {\n    val data = Seq(\"a\" -> 10, \"b\" -> 20, \"c\" -> 30, \"d\" -> 40)\n\n    checkAnswer(\n      // randomize the order and expect the partition ids assigned correctly in sorted order\n      Random.shuffle(data).toDF(\"c1\", \"c2\")\n        .withColumn(\"r1\", range_partition_id($\"c1\", 2))\n        .withColumn(\"r2\", range_partition_id($\"c2\", 4)),\n      Seq(\n        // Column c1 has values (a, b, c, d). Splitting this value range into two partitions\n        // gets ranges [a, b] and [c, d]. Values in each range map to partition 0 and 1.\n        // Similarly column c2 has values (10, 20, 30, 40). Splitting this into four partitions\n        // gets ranges [10], [20], [30] and [40] which map to partition ids 0 to 3.\n        Row(\"a\", 10, 0, 0),\n        Row(\"b\", 20, 0, 1),\n        Row(\"c\", 30, 1, 2),\n        Row(\"d\", 40, 1, 3)))\n\n    checkAnswer(\n      Random.shuffle(data).toDF(\"c1\", \"c2\")\n        .withColumn(\"r1\", range_partition_id($\"c1\", 2))\n        .distinct\n        .withColumn(\"r2\", range_partition_id($\"c2\", 4)),\n      Seq(\n        Row(\"a\", 10, 0, 0),\n        Row(\"b\", 20, 0, 1),\n        Row(\"c\", 30, 1, 2),\n        Row(\"d\", 40, 1, 3)))\n\n    checkAnswer(\n      Random.shuffle(data).toDF(\"c1\", \"c2\")\n        .where(range_partition_id($\"c1\", 2) === 0)\n        .sort(range_partition_id($\"c2\", 4)),\n      Seq(\n        Row(\"a\", 10),\n        Row(\"b\", 20)))\n  }\n\n  testQuietly(\"range_partition_id(): corner cases\") {\n    // invalid number of partitions\n    val ex1 = intercept[IllegalArgumentException] {\n      spark.range(10).select(range_partition_id($\"id\", 0)).show\n    }\n    assert(ex1.getMessage contains \"expected the number partitions to be greater than zero\")\n\n    val ex2 = intercept[IllegalArgumentException] {\n      withSQLConf(SQLConf.RANGE_EXCHANGE_SAMPLE_SIZE_PER_PARTITION.key -> \"0\") {\n        spark.range(10).withColumn(\"rpi\", range_partition_id($\"id\", 10)).show\n      }\n    }\n    assert(ex2.getMessage contains \"Sample points per partition must be greater than 0 but found 0\")\n\n    // Number of partitions is way more than the cardinality of input column values\n    checkAnswer(\n      spark.range(1).withColumn(\"rpi\", range_partition_id($\"id\", 1000)),\n      Row(0, 0))\n\n    // compute range_partition_id on a dataframe with zero rows\n    checkAnswer(\n      spark.range(0).withColumn(\"rpi\", range_partition_id($\"id\", 1000)),\n      Seq.empty[Row])\n\n    // compute range_partition_id on column with null values\n    checkAnswer(\n      Seq(\"a\", null, \"b\", null).toDF(\"id\").withColumn(\"rpi\", range_partition_id($\"id\", 10)),\n      Seq(\n        Row(\"a\", 0),\n        Row(\"b\", 1),\n        Row(null, 0),\n        Row(null, 0)))\n\n    // compute range_partition_id on column with one value which is null\n    checkAnswer(\n      spark.range(1).withColumn(\"id\", lit(null)).withColumn(\"rpi\", range_partition_id($\"id\", 10)),\n      Row(null, 0))\n\n    // compute range_partition_id on array type column\n    checkAnswer(\n      spark.range(1).withColumn(\"id\", lit(Array(1, 2)))\n        .withColumn(\"rpi\", range_partition_id($\"id\", 10)),\n      Row(Array(1, 2), 0))\n  }\n\n  test(\"interleave_bits(): 1 input = cast to binary\") {\n    val data = Seq.fill(100)(Random.nextInt())\n    checkAnswer(\n      data.toDF(\"id\").select(interleave_bits($\"id\")),\n      data.map(i => Row(intToBinary(i)))\n    )\n  }\n\n  test(s\"interleave_bits(): arbitrary num inputs\") {\n    val n = 1 + Random.nextInt(7)\n    val zDF = spark.range(1).select()\n\n    // Output is an array with number of elements equal to 4 * num_of_input_columns to interleave\n\n    // Multiple columns each has value 0. Expect the final output an array of zeros\n    checkAnswer(\n      1.to(n).foldLeft(zDF)((df, i) => df.withColumn(s\"c$i\", lit(0x00000000)))\n        .select(interleave_bits(1.to(n).map(i => $\"c$i\"): _*)),\n      Row(Array.fill(n * 4)(0x00.toByte))\n    )\n\n    // Multiple column each has value 1. As the bits are interleaved expect the following output\n    // Inputs: c1=0x00000001, c2=0x00000001, c3=0x00000001, c4=0x00000001\n    // Output (divided into array of 4 bytes for readability)\n    //  [0x00, 0x00, 0x00, 0x00] [0x00, 0x00, 0x00, 0x00]\n    //  [0x00, 0x00, 0x00, 0x00] [0x00, 0x00, 0x00, 0x08]\n    // (Inputs have last bit as 1 as we are interleaving bits across columns, all these\n    // bits of value 1 they will end up as last 4 bits in the last byte of the output)\n    checkAnswer(\n      1.to(n).foldLeft(zDF)((df, i) => df.withColumn(s\"c$i\", lit(0x00000001)))\n        .select(interleave_bits(1.to(n).map(i => $\"c$i\"): _*)),\n      Row(Array.fill(n * 4 - 1)(0x00.toByte) :+ ((1 << n) - 1).toByte)\n    )\n\n    // Multiple columns each has value 0xFFFFFFFF. Expect the final output an array of 0xFF\n    checkAnswer(\n      1.to(n).foldLeft(zDF)((df, i) => df.withColumn(s\"c$i\", lit(0xffffffff)))\n        .select(interleave_bits(1.to(n).map(i => $\"c$i\"): _*)),\n      Row(Array.fill(n * 4)(0xff.toByte))\n    )\n  }\n\n  test(\"interleave_bits(): corner cases\") {\n    // null input\n    checkAnswer(\n      spark.range(1).select(interleave_bits(lit(null))),\n      Row(Array.fill(4)(0x00.toByte))\n    )\n\n    // no inputs to interleave_bits -> expect an empty row\n    checkAnswer(\n      spark.range(1).select(interleave_bits()),\n      Row(Array.empty[Byte])\n    )\n\n    // Non-integer type as input column\n    val ex = intercept[AnalysisException] {\n      Seq(false).toDF(\"col\").select(interleave_bits($\"col\")).show\n    }\n    assert(ex.getMessage contains \"\")\n\n    def invalidColumnTypeInput(df: DataFrame): Unit = {\n      val ex = intercept[AnalysisException] {\n        df.select(interleave_bits($\"col\")).show\n      }\n      assert(ex.getMessage contains \"\")\n    }\n\n    // Expect failure when a non-int type column is provided as input\n    invalidColumnTypeInput(Seq(0L).toDF(\"col\"))\n    invalidColumnTypeInput(Seq(0.0).toDF(\"col\"))\n    invalidColumnTypeInput(Seq(\"asd\").toDF(\"col\"))\n    invalidColumnTypeInput(Seq(Array(1, 2, 3)).toDF(\"col\"))\n  }\n\n  test(\"interleave_bits(range_partition_ids)\") {\n    // test the combination of range_partition_id and interleave\n    checkAnswer(\n      spark.range(100).select(interleave_bits(range_partition_id($\"id\", 10))),\n      0.until(100).map(i => Row(intToBinary(i / 10)))\n    )\n\n    // test the combination of range_partition_id and interleave on multiple columns\n    checkAnswer(\n      Seq(\n        (false, 0, \"0\"),\n        (true, 1, \"1\")\n      ).toDF(\"c1\", \"c2\", \"c3\")\n        .select(interleave_bits(\n          range_partition_id($\"c1\", 2),\n          range_partition_id($\"c2\", 2),\n          range_partition_id($\"c3\", 2)\n        )),\n      Seq(\n        Row(Array.fill(3 * 4)(0x00.toByte)),\n        Row(Array.fill(3 * 4 - 1)(0x00.toByte) :+ 0x07.toByte)\n      )\n    )\n  }\n\n  test(\"hilbert_index selects underlying expression correctly\") {\n    assert(hilbert_index(10, Seq($\"c1\", $\"c2\", $\"c3\", $\"c4\", $\"c5\", $\"c6\"): _*).expr\n      .isInstanceOf[HilbertLongIndex])\n    assert(\n      hilbert_index(\n        10,\n        Seq($\"c1\", $\"c2\", $\"c3\", $\"c4\", $\"c5\", $\"c6\", $\"c7\", $\"c8\", $\"c9\"): _*)\n      .expr.asInstanceOf[Cast].child.isInstanceOf[HilbertByteArrayIndex])\n    val e = intercept[SparkException](\n      hilbert_index(\n        11,\n        Seq($\"c1\", $\"c2\", $\"c3\", $\"c4\", $\"c5\", $\"c6\", $\"c7\", $\"c8\", $\"c9\", $\"c10\"): _*)\n      .expr.isInstanceOf[HilbertByteArrayIndex])\n    assert(e.getMessage.contains(\"Hilbert indexing can only be used on 9 or fewer columns.\"))\n  }\n\n  private def intToBinary(x: Int): Array[Byte] = {\n    ByteBuffer.allocate(4).putInt(x).array()\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/MultiDimClusteringSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping\n\nimport java.io.{File, FilenameFilter}\n\nimport scala.util.Random\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf._\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.functions.expr\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass MultiDimClusteringSuite extends QueryTest\n    with SharedSparkSession with DeltaSQLCommandTest {\n\n  private lazy val sparkSession = spark\n  // scalastyle:off sparkimplicits\n  import sparkSession.implicits._\n  // scalastyle:on sparkimplicits\n\n  test(\"Negative case - ZOrder clustering expression with zero columns\") {\n    val ex = intercept[AssertionError] {\n      ZOrderClustering.getClusteringExpression(Seq.empty, 20)\n    }\n    assert(ex.getMessage contains \"Cannot do Z-Order clustering by zero columns!\")\n  }\n\n  test(\"ZOrder clustering expression with one column\") {\n    val cluster = ZOrderClustering.getClusteringExpression(Seq(expr(\"col1\")), 20)\n    assert(cluster.expr.toString ===\n      \"cast(interleavebits(rangepartitionid('col1, 20)) as string)\")\n  }\n\n  test(\"ZOrder clustering expression with two column\") {\n    val cluster = ZOrderClustering.getClusteringExpression(Seq(expr(\"col1\"), expr(\"col2\")), 20)\n    assert(cluster.expr.toString ===\n      \"cast(interleavebits(rangepartitionid('col1, 20), rangepartitionid('col2, 20)) as string)\")\n  }\n\n  test(\"ensure records with close Z-order values are close in the output\") {\n    withTempDir { tempDir =>\n      withSQLConf(\n        MDC_NUM_RANGE_IDS.key -> \"4\",\n        MDC_ADD_NOISE.key -> \"false\") {\n        val data = Seq(\n          // \"c1\" -> \"c2\", // (rangeId_c1, rangeId_c2) -> ZOrder (decimal Z-Order)\n          \"a\" -> 20, \"a\" -> 20, // (0, 0) -> 0b000000 (0)\n          \"b\" -> 20, // (0, 0) -> 0b000000 (0)\n          \"c\" -> 30, // (1, 1) -> 0b000011 (3)\n          \"d\" -> 70, // (1, 2) -> 0b001011 (3)\n          \"e\" -> 90, \"e\" -> 90, \"e\" -> 90, // (1, 2) -> 0b001001 (9)\n          \"f\" -> 200, // (2, 3) -> 0b001110 (14)\n          \"g\" -> 10, // (3, 0) -> 0b000101 (5)\n          \"h\" -> 20) // (3, 0) -> 0b000101 (5)\n\n        // Randomize the data. Use seed for deterministic input.\n        val inputDf = new Random(seed = 101).shuffle(data)\n            .toDF(\"c1\", \"c2\")\n\n        // Cluster the data and range partition into four partitions\n        val outputDf = MultiDimClustering.cluster(\n          inputDf,\n          approxNumPartitions = 4,\n          colNames = Seq(\"c1\", \"c2\"),\n          curve = \"zorder\")\n        outputDf.write.parquet(new File(tempDir, \"source\").getCanonicalPath)\n\n        // Load the partition 0 and verify that it contains (a, 20), (a, 20), (b, 20)\n        checkAnswer(\n          Seq(\"a\" -> 20, \"a\" -> 20, \"b\" -> 20).toDF(\"c1\", \"c2\"),\n          sparkSession.read.parquet(new File(tempDir, \"source/part-00000*\").getCanonicalPath))\n\n        // partition 1\n        checkAnswer(\n          Seq(\"c\" -> 30, \"d\" -> 70, \"e\" -> 90, \"e\" -> 90, \"e\" -> 90).toDF(\"c1\", \"c2\"),\n          sparkSession.read.parquet(new File(tempDir, \"source/part-00001*\").getCanonicalPath))\n\n        // partition 2\n        checkAnswer(\n          Seq(\"h\" -> 20, \"g\" -> 10).toDF(\"c1\", \"c2\"),\n          sparkSession.read.parquet(new File(tempDir, \"source/part-00002*\").getCanonicalPath))\n\n        // partition 3\n        checkAnswer(\n          Seq(\"f\" -> 200).toDF(\"c1\", \"c2\"),\n          sparkSession.read.parquet(new File(tempDir, \"source/part-00003*\").getCanonicalPath))\n      }\n    }\n  }\n\n  test(\"ensure records with close Hilbert curve values are close in the output\") {\n    withTempDir { tempDir =>\n      withSQLConf(MDC_NUM_RANGE_IDS.key -> \"4\", MDC_ADD_NOISE.key -> \"false\") {\n        val data = Seq(\n          // \"c1\" -> \"c2\", // (rangeId_c1, rangeId_c2) -> Decimal Hilbert index\n          \"a\" -> 20, \"a\" -> 20, // (0, 0) -> 0\n          \"b\" -> 20, // (0, 0) -> 0\n          \"c\" -> 30, // (1, 1) -> 2\n          \"d\" -> 70, // (1, 2) -> 13\n          \"e\" -> 90, \"e\" -> 90, \"e\" -> 90, // (1, 2) -> 13\n          \"f\" -> 200, // (2, 3) -> 11\n          \"g\" -> 10, // (3, 0) -> 5\n          \"h\" -> 20) // (3, 0) -> 5\n\n        // Randomize the data. Use seed for deterministic input.\n        val inputDf = new Random(seed = 101)\n          .shuffle(data)\n          .toDF(\"c1\", \"c2\")\n\n        // Cluster the data and range partition into four partitions\n        val outputDf = MultiDimClustering.cluster(\n          inputDf,\n          approxNumPartitions = 2,\n          colNames = Seq(\"c1\", \"c2\"),\n          curve = \"hilbert\")\n        outputDf.write.parquet(new File(tempDir, \"source\").getCanonicalPath)\n\n        // Load the partition 0 and verify its records.\n        checkAnswer(\n          Seq(\"a\" -> 20, \"a\" -> 20, \"b\" -> 20, \"c\" -> 30, \"g\" -> 10, \"h\" -> 20).toDF(\"c1\", \"c2\"),\n          sparkSession.read.parquet(new File(tempDir, \"source/part-00000*\").getCanonicalPath)\n        )\n\n        // partition 1\n        checkAnswer(\n          Seq(\"d\" -> 70, \"e\" -> 90, \"e\" -> 90, \"e\" -> 90, \"f\" -> 200).toDF(\"c1\", \"c2\"),\n          sparkSession.read.parquet(new File(tempDir, \"source/part-00001*\").getCanonicalPath)\n        )\n      }\n    }\n  }\n\n  test(\"ensure records in each partition are sorted according to Z-order values\") {\n    withSQLConf(\n      MDC_SORT_WITHIN_FILES.key -> \"true\",\n      MDC_ADD_NOISE.key -> \"false\") {\n      val data = Seq(\n        // \"c1\" -> \"c2\", // (rangeId_c1, rangeId_c2) -> ZOrder (decimal Z-Order)\n        \"a\" -> 20, \"a\" -> 20, // (0, 1) -> 0x01 (1)\n        \"b\" -> 20, // (1, 1) -> 0x03 (3)\n        \"c\" -> 30, // (2, 2) -> 0x0C (12)\n        \"d\" -> 70, // (3, 3) -> 0x0F (15)\n        \"e\" -> 90, \"e\" -> 90, \"e\" -> 90, // (4, 4) -> 0x30 (48)\n        \"f\" -> 200, // (5, 5) -> 0x33 (51)\n        \"g\" -> 10, // (6, 0) -> 0x28 (40)\n        \"h\" -> 20) // (7, 1) -> 0x2B (43)\n\n      // Randomize the data. Use seed for deterministic input.\n      val inputDf = new Random(seed = 101).shuffle(data)\n          .toDF(\"c1\", \"c2\")\n\n      // Cluster the data, range partition into one partition, and sort.\n      val outputDf = MultiDimClustering.cluster(\n        inputDf,\n        approxNumPartitions = 1,\n        colNames = Seq(\"c1\", \"c2\"),\n        curve = \"zorder\")\n\n      // Check that dataframe is sorted.\n      checkAnswer(\n        outputDf,\n        Seq(\n          \"a\" -> 20, \"a\" -> 20,\n          \"b\" -> 20,\n          \"c\" -> 30,\n          \"d\" -> 70,\n          \"g\" -> 10,\n          \"h\" -> 20,\n          \"e\" -> 90, \"e\" -> 90, \"e\" -> 90,\n          \"f\" -> 200\n        ).toDF(\"c1\", \"c2\").collect())\n    }\n  }\n\n  test(\"ensure records in each partition are sorted according to Hilbert curve values\") {\n    withSQLConf(\n      MDC_SORT_WITHIN_FILES.key -> \"true\",\n      MDC_ADD_NOISE.key -> \"false\") {\n      val data = Seq(\n        // \"c1\" -> \"c2\", // (rangeId_c1, rangeId_c2) -> Decimal Hilbert index\n        \"a\" -> 20, \"a\" -> 20, // (0, 1) -> 3\n        \"b\" -> 20, // (1, 1) -> 2\n        \"c\" -> 30, // (2, 2) -> 8\n        \"d\" -> 70, // (3, 3) -> 10\n        \"e\" -> 90, \"e\" -> 90, \"e\" -> 90, // (4, 4) -> 32\n        \"f\" -> 200, // (5, 5) -> 34\n        \"g\" -> 10, // (6, 0) -> 20\n        \"h\" -> 20) // (7, 1) -> 22\n\n      // Randomize the data. Use seed for deterministic input.\n      val inputDf = new Random(seed = 101).shuffle(data)\n          .toDF(\"c1\", \"c2\")\n\n      // Cluster the data, range partition into one partition, and sort.\n      val outputDf = MultiDimClustering.cluster(\n        inputDf,\n        approxNumPartitions = 1,\n        colNames = Seq(\"c1\", \"c2\"),\n        curve = \"hilbert\")\n\n      // Check that dataframe is sorted.\n      checkAnswer(\n        outputDf,\n        Seq(\n          \"b\" -> 20,\n          \"a\" -> 20, \"a\" -> 20,\n          \"c\" -> 30,\n          \"d\" -> 70,\n          \"g\" -> 10,\n          \"h\" -> 20,\n          \"e\" -> 90, \"e\" -> 90, \"e\" -> 90,\n          \"f\" -> 200\n        ).toDF(\"c1\", \"c2\").collect())\n    }\n  }\n\n  test(\"noise is helpful in skew handling\") {\n    Seq(\"zorder\", \"hilbert\").foreach { curve =>\n      Seq(\"true\", \"false\").foreach { addNoise =>\n        withTempDir { tempDir =>\n          withSQLConf(\n            MDC_NUM_RANGE_IDS.key -> \"4\",\n            MDC_ADD_NOISE.key -> addNoise) {\n            val data = Array.fill(100)(20, 20) // all records have the same values\n            val inputDf = data.toSeq.toDF(\"c1\", \"c2\")\n\n            // Cluster the data and range partition into four partitions\n            val outputDf = MultiDimClustering.cluster(\n              inputDf,\n              approxNumPartitions = 4,\n              colNames = Seq(\"c1\", \"c2\"),\n              curve)\n\n            outputDf.write.parquet(new File(tempDir, \"source\").getCanonicalPath)\n\n            // If there is no noise added, expect only one partition, otherwise four partition\n            // as mentioned in the cluster command above.\n            val partCount = new File(tempDir, \"source\").listFiles(new FilenameFilter {\n              override def accept(dir: File, name: String): Boolean = {\n                name.startsWith(\"part-0000\")\n              }\n            }).length\n\n            if (\"true\".equals(addNoise)) {\n              assert(4 === partCount, s\"Incorrect number of partitions when addNoise=$addNoise\")\n            } else {\n              assert(1 === partCount, s\"Incorrect number of partitions when addNoise=$addNoise\")\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(s\"try clustering with different ranges and noise flag on/off\") {\n    Seq(\"zorder\", \"hilbert\").foreach { curve =>\n      Seq(\"true\", \"false\").foreach { addNoise =>\n        Seq(\"20\", \"100\", \"200\", \"1000\").foreach { numRanges =>\n          withSQLConf(MDC_NUM_RANGE_IDS.key -> numRanges, MDC_ADD_NOISE.key -> addNoise) {\n            val data = Seq.range(0, 100)\n            val inputDf = Random.shuffle(data).map(x => (x, x * 113 % 101)).toDF(\"col1\", \"col2\")\n            val outputDf = MultiDimClustering.cluster(\n              inputDf,\n              approxNumPartitions = 10,\n              colNames = Seq(\"col1\", \"col2\"),\n              curve)\n            // Underlying data shouldn't change\n            checkAnswer(outputDf, inputDf)\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/clustering/ClusteredTableDDLSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\nimport java.io.File\n\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions}\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtils\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, DeltaAnalysisException, DeltaColumnMappingEnableIdMode, DeltaColumnMappingEnableNameMode, DeltaConfigs, DeltaLog, DeltaUnsupportedOperationException, NoMapping}\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.clustering.ClusteringMetadataDomain\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.hooks.UpdateCatalog\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.SkippingEligibleDataType\nimport org.apache.spark.sql.delta.test.{DeltaColumnMappingSelectedTestMixin, DeltaSQLCommandTest}\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.connector.expressions.FieldReference\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.{ArrayType, IntegerType, StructField, StructType}\nimport org.apache.spark.util.Utils\n\ntrait ClusteredTableCreateOrReplaceDDLSuiteBase extends QueryTest\n  with SharedSparkSession\n  with ClusteredTableTestUtils {\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.conf.set(DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key, \"true\")\n  }\n\n  override def afterAll(): Unit = {\n    // Reset UpdateCatalog's thread pool to ensure it is re-initialized in the next test suite.\n    // This is necessary because the [[SparkThreadLocalForwardingThreadPoolExecutor]]\n    // retains a reference to the SparkContext. Without resetting, the new test suite would\n    // reuse the same SparkContext from the previous suite, despite it being stopped.\n    //\n    // This will force the UpdateCatalog's background thread to use the new SparkContext.\n    //\n    // scalastyle:off line.size.limit\n    // This is to avoid the following exception thrown from the UpdateCatalog's background thread:\n    //  java.lang.IllegalStateException: Cannot call methods on a stopped SparkContext.\n    //  This stopped SparkContext was created at:\n    //\n    //  org.apache.spark.sql.delta.skipping.clustering.ClusteredTableDDLDataSourceV2NameColumnMappingSuite.beforeAll\n    //\n    //  The currently active SparkContext was created at:\n    //\n    //  org.apache.spark.sql.delta.skipping.clustering.ClusteredTableDDLDataSourceV2Suite.beforeAll\n    // scalastyle:on line.size.limit\n    UpdateCatalog.tp = null\n\n    super.afterAll()\n  }\n\n  protected val testTable: String = \"test_ddl_table\"\n  protected val sourceTable: String = \"test_ddl_source\"\n  protected val targetTable: String = \"test_ddl_target\"\n\n  protected def isPathBased: Boolean = false\n\n  protected def supportedClauses: Seq[String]\n\n  testCtasRtasHelper(supportedClauses)\n  testClusteringColumnsPartOfStatsColumn(supportedClauses)\n  testColTypeValidation(\"CREATE\")\n\n  def testCtasRtasHelper(clauses: Seq[String]): Unit = {\n    Seq(\n      (\"\",\n        \"a INT, b STRING, ts TIMESTAMP\",\n        Seq(\"a\", \"b\")),\n      (\" multipart name\",\n        \"a STRUCT<b INT, c STRING>, ts TIMESTAMP\",\n        Seq(\"a.b\", \"ts\"))\n    ).foreach { case (testSuffix, columns, clusteringColumns) =>\n      test(s\"create/replace table$testSuffix\") {\n        withTable(testTable) {\n          clauses.foreach { clause =>\n            createOrReplaceClusteredTable(\n              clause, testTable, columns, clusteringColumns.mkString(\",\"))\n            verifyClusteringColumns(TableIdentifier(testTable), clusteringColumns)\n          }\n        }\n      }\n\n      test(s\"ctas/rtas$testSuffix\") {\n        withTable(sourceTable, targetTable) {\n          sql(s\"CREATE TABLE $sourceTable($columns) USING delta\")\n          withTempDirIfNecessary { location =>\n            clauses.foreach { clause =>\n              createOrReplaceAsSelectClusteredTable(\n                clause,\n                targetTable,\n                sourceTable,\n                clusteringColumns.mkString(\",\"),\n                location = location)\n              verifyClusteringColumns(targetTable, clusteringColumns, location)\n            }\n          }\n        }\n      }\n\n      if (clauses.contains(\"REPLACE\")) {\n        test(s\"Replace from non clustered table$testSuffix\") {\n          withTable(targetTable) {\n            sql(s\"CREATE TABLE $targetTable($columns) USING delta\")\n            createOrReplaceClusteredTable(\n              \"REPLACE\", targetTable, columns, clusteringColumns.mkString(\",\"))\n            verifyClusteringColumns(TableIdentifier(targetTable), clusteringColumns)\n          }\n        }\n      }\n    }\n  }\n\n  protected def createTableWithStatsColumns(\n      clause: String,\n      table: String,\n      clusterColumns: Seq[String],\n      numIndexedColumns: Int,\n      tableSchema: Option[String],\n      statsColumns: Seq[String] = Seq.empty,\n      location: Option[String] = None): Unit = {\n    val clusterSpec = clusterColumns.mkString(\",\")\n    val updatedTableProperties =\n      collection.mutable.Map(\"delta.dataSkippingNumIndexedCols\" -> s\"$numIndexedColumns\")\n    if (statsColumns.nonEmpty) {\n      updatedTableProperties(DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.key) =\n        statsColumns.mkString(\",\")\n    }\n    val tablePropertiesString = updatedTableProperties.map {\n      case (key, value) => s\"'$key' = '$value'\"\n    }.mkString(\",\")\n    val locationClause = if (location.isEmpty) \"\" else s\"LOCATION '${location.get}'\"\n    if (clause == \"REPLACE\") {\n      // Create the default before it can be replaced.\n      sql(s\"CREATE TABLE IF NOT EXISTS $table USING DELTA $locationClause\")\n    }\n    if (tableSchema.isEmpty) {\n      sql(\n        s\"\"\"\n           |$clause TABLE $table USING DELTA CLUSTER BY ($clusterSpec)\n           |TBLPROPERTIES($tablePropertiesString)\n           |$locationClause\n           |AS SELECT * FROM $sourceTable\n           |\"\"\".stripMargin)\n    } else {\n      createOrReplaceClusteredTable(\n        clause, table, tableSchema.get, clusterSpec, updatedTableProperties.toMap, location)\n    }\n  }\n\n  protected def testStatsCollectionHelper(\n      tableSchema: String,\n      numberOfIndexedCols: Int)(cb: => Unit): Unit = {\n    withTable(sourceTable) {\n      // Create a source table for CTAS.\n      sql(\n        s\"\"\"\n           | CREATE TABLE $sourceTable($tableSchema) USING DELTA\n           | TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = '$numberOfIndexedCols')\n           |\"\"\".stripMargin)\n      // Run additional steps.\n      cb\n    }\n  }\n\n  protected def testColTypeValidation(clause: String): Unit = {\n    test(s\"validate column datatype checking on $clause table\") {\n      withTable(\"srcTbl\", \"dstTbl\") {\n        // Create reference table for CTAS/RTAS.\n        val columnMappingMode =\n          sparkConf\n            .getOption(DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey)\n            .getOrElse(\"none\")\n        val columnMappingEnabled = columnMappingMode != NoMapping.name\n        val specialColName = \"`f@q`\"\n        val commaColSql = if (columnMappingEnabled) {\n          s\",$specialColName INT\"\n        } else {\n          \"\"\n        }\n        val schemaStr =\n          s\"\"\"\n            |a STRUCT<b INT, c INT>\n            |,d BOOLEAN\n            |,e MAP<INT, INT>\n            |$commaColSql\n            |\"\"\".stripMargin\n        sql(s\"CREATE table srcTbl ($schemaStr) USING delta\")\n\n        val data = (0 to 1000)\n          .map(i => Row(Row(i + 1, i * 10), i % 2 == 0, Map(i -> i), i % 2 == 1))\n        val schema = StructType(List(\n          StructField(\"a\", StructType(\n            Array(\n              StructField(\"b\", IntegerType),\n              StructField(\"c\", IntegerType)\n            )\n          ))))\n        spark.createDataFrame(spark.sparkContext.parallelize(data), StructType(schema))\n          .write.mode(\"append\").format(\"delta\").saveAsTable(\"srcTbl\")\n\n        val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, new TableIdentifier(\"srcTbl\"))\n        // Test multiple data types.\n        // Columns \"a\", \"d\" and \"e\" are all unsupported data skipping types.\n        // Columns \"a.b\" and \"`f@q`\" are eligible data skipping types.\n        val commaClusterColOpt = if (columnMappingEnabled) {\n          Some(specialColName)\n        } else None\n        (Seq(\"a\", \"d\", \"e\", \"a.b\") ++ commaClusterColOpt).foreach { colName =>\n          withTempDir { tmpDir =>\n            // Since validation happens both on create and replace, validate for both cases to\n            // ensure that datatype validation behaves consistently between the two.\n            if (clause == \"REPLACE\") {\n              sql(\"DROP TABLE IF EXISTS dstTbl\")\n              sql(s\"CREATE TABLE dstTbl LIKE srcTbl LOCATION '${tmpDir.getAbsolutePath}'\")\n            }\n\n            Seq(\n              // Scenario 1: Standard CREATE/REPLACE TABLE.\n              () => {\n                val schema = s\"a STRUCT<b INT, c INT>, d BOOLEAN, e MAP<INT, INT>, `f,q` INT\"\n                createOrReplaceClusteredTable(\n                  clause, \"dstTbl\", schemaStr, colName, location = Some(tmpDir.getAbsolutePath))\n              },\n              // Scenario 2: CTAS/RTAS.\n              () =>\n                createOrReplaceAsSelectClusteredTable(\n                clause, \"dstTbl\", \"srcTbl\", colName, location = Some(tmpDir.getAbsolutePath)))\n              .foreach { f =>\n                if (colName == \"a.b\" || colName == specialColName) {\n                  if (clause == \"CREATE\") {\n                    // Drop the table and delete the _delta_log directory to allow\n                    // external delta table creation.\n                    deleteTableFromCommitCoordinatorIfNeeded(\"dstTbl\")\n                    sql(\"DROP TABLE IF EXISTS dstTbl\")\n                    Utils.deleteRecursively(new File(tmpDir, \"_delta_log\"))\n                  }\n                  // Qualified data types and no exception is expected.\n                  f()\n                } else {\n                  val e = intercept[DeltaAnalysisException] {\n                    f()\n                  }\n                  val tableSchema =\n                    DeltaLog.forTable(spark, TableIdentifier(\"srcTbl\")).update().metadata.schema\n                  val dataTypeOpt = tableSchema\n                    .findNestedField(FieldReference(colName).fieldNames())\n                    .map(_._2.dataType)\n                  assert(dataTypeOpt.nonEmpty, s\"Can't find column $colName \" +\n                    s\"in schema ${tableSchema.treeString}\")\n                  checkError(\n                    e,\n                    \"DELTA_CLUSTERING_COLUMNS_DATATYPE_NOT_SUPPORTED\",\n                    parameters = Map(\"columnsWithDataTypes\" -> s\"$colName : ${dataTypeOpt.get.sql}\")\n                  )\n                }\n              }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"cluster by with more than 4 columns - create table\") {\n    val testTable = \"test_table\"\n    withTable(testTable) {\n      val e = intercept[DeltaAnalysisException] {\n        createOrReplaceClusteredTable(\n          \"CREATE\", testTable, \"a INT, b INT, c INT, d INT, e INT\", \"a, b, c, d, e\")\n      }\n      checkError(\n        e,\n        \"DELTA_CLUSTER_BY_INVALID_NUM_COLUMNS\",\n        parameters = Map(\"numColumnsLimit\" -> \"4\", \"actualNumColumns\" -> \"5\")\n      )\n    }\n  }\n\n  test(\"cluster by with more than 4 columns - ctas\") {\n    val testTable = \"test_table\"\n    val schema = \"a INT, b INT, c INT, d INT, e INT\"\n    withTempDirIfNecessary { location =>\n      withTable(sourceTable, testTable) {\n        sql(s\"CREATE TABLE $sourceTable($schema) USING delta\")\n        val e = intercept[DeltaAnalysisException] {\n          createOrReplaceAsSelectClusteredTable(\n            \"CREATE\", testTable, sourceTable, \"a, b, c, d, e\", location = location)\n        }\n        checkError(\n          e,\n          \"DELTA_CLUSTER_BY_INVALID_NUM_COLUMNS\",\n          parameters = Map(\"numColumnsLimit\" -> \"4\", \"actualNumColumns\" -> \"5\")\n        )\n      }\n    }\n  }\n\n  protected def verifyPartitionColumns(\n      tableIdentifier: TableIdentifier,\n      expectedPartitionColumns: Seq[String]): Unit = {\n    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n    assert(snapshot.metadata.partitionColumns === expectedPartitionColumns)\n  }\n\n  protected def verifyClusteringColumns(\n      table: String,\n      expectedLogicalClusteringColumns: Seq[String],\n      locationOpt: Option[String]): Unit = {\n    locationOpt.map { location =>\n      verifyClusteringColumns(\n        location, expectedLogicalClusteringColumns\n      )\n    }.getOrElse {\n      verifyClusteringColumns(TableIdentifier(table), expectedLogicalClusteringColumns)\n    }\n  }\n\n  def testClusteringColumnsPartOfStatsColumn(clauses: Seq[String]): Unit = {\n    clauses.foreach { clause =>\n      val mode = if (clause == \"CREATE\") \"create table\" else \"replace table\"\n      test(s\"Validate clustering columns part of stats columns - $mode\") {\n        val tableSchema = \"col0 int, col1 STRUCT<col11: int, col12: string>, col2 int\"\n        val indexedColumns = 2\n        testStatsCollectionHelper(\n          tableSchema = tableSchema,\n          numberOfIndexedCols = indexedColumns) {\n          withTable(targetTable) {\n            val deltaLogSrc = DeltaLog.forTable(spark, TableIdentifier(sourceTable))\n            // Validate the 3rd column `col1.col12` and 4th column `col2` can not be\n            // clustering columns.\n            val e = intercept[DeltaAnalysisException](\n              createTableWithStatsColumns(\n                clause,\n                targetTable,\n                \"col0\" :: \"col1.col11\" :: \"col1.col12\" :: \"col2\" :: Nil,\n                indexedColumns,\n                Some(tableSchema)))\n            checkError(\n              e,\n              \"DELTA_CLUSTERING_COLUMN_MISSING_STATS\",\n              parameters = Map(\n                \"columns\" -> \"col1.col12, col2\",\n                \"schema\" -> \"\"\"root\n                    | |-- col0: integer (nullable = true)\n                    | |-- col1: struct (nullable = true)\n                    | |    |-- col11: integer (nullable = true)\n                    |\"\"\".stripMargin)\n            )\n            // Validate the first two columns can be clustering columns.\n            createTableWithStatsColumns(\n              clause,\n              targetTable,\n              \"col0\" :: \"col1.col11\" :: Nil,\n              indexedColumns,\n              Some(tableSchema))\n          }\n        }\n      }\n    }\n\n    clauses.foreach { clause =>\n      val mode = if (clause == \"CREATE\") \"ctas\" else \"rtas\"\n      test(s\"Validate clustering columns part of stats columns - $mode\") {\n        // Add a suffix for the target table name to work around the issue that delta table's\n        // location isn't removed by the DROP TABLE from ctas/rtas test cases.\n        val table = targetTable + \"_\" + clause\n\n        val tableSchema = \"col0 int, col1 STRUCT<col11: int, col12: string>, col2 int\"\n        val indexedColumns = 2\n        testStatsCollectionHelper(\n          tableSchema = tableSchema,\n          numberOfIndexedCols = indexedColumns) {\n          withTable(table) {\n            withTempDir { dir =>\n              val deltaLogSrc = DeltaLog.forTable(spark, TableIdentifier(sourceTable))\n              val targetLog = DeltaLog.forTable(spark, s\"${dir.getPath}\")\n              val dataPath = new File(targetLog.dataPath.toString.replace(\"file:\", \"\"))\n              val initialNumFiles =\n                if (dataPath.listFiles() != null) { // Returns null if directory doesn't exist -> 0\n                  dataPath.listFiles().size\n                }\n                else {\n                  0\n                }\n              // Validate the 3rd column `col1.col12` and 4th column `col2` can not be\n              // clustering columns.\n              val e = intercept[DeltaAnalysisException](\n                createTableWithStatsColumns(\n                  clause,\n                  table,\n                  \"col0\" :: \"col1.col11\" :: \"col1.col12\" :: \"col2\" :: Nil,\n                  indexedColumns,\n                  None,\n                  location = Some(dir.getPath)))\n              checkError(\n                e,\n                \"DELTA_CLUSTERING_COLUMN_MISSING_STATS\",\n                parameters = Map(\n                  \"columns\" -> \"col1.col12, col2\",\n                  \"schema\" -> \"\"\"root\n                    | |-- col0: integer (nullable = true)\n                    | |-- col1: struct (nullable = true)\n                    | |    |-- col11: integer (nullable = true)\n                    |\"\"\".stripMargin)\n              )\n\n              // Validate the first two columns can be clustering columns.\n              createTableWithStatsColumns(\n                clause,\n                table,\n                \"col0\" :: \"col1.col11\" :: Nil,\n                indexedColumns,\n                None)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Validate clustering columns cannot be non-eligible data types\") {\n    val indexedColumns = 3\n    // Validate non-eligible column stat data type.\n    val nonEligibleType = ArrayType(IntegerType)\n    assert(!SkippingEligibleDataType(nonEligibleType))\n    val nonEligibleTableSchema = s\"col0 int, col1 STRUCT<col11: array<int>, col12: string>\"\n    testStatsCollectionHelper(\n      tableSchema = nonEligibleTableSchema,\n      numberOfIndexedCols = indexedColumns) {\n      withTable(targetTable) {\n        val deltaLogSrc = DeltaLog.forTable(spark, TableIdentifier(sourceTable))\n        // Validate the 2nd column `col1.col11` cannot be clustering column.\n        val e = intercept[DeltaAnalysisException](\n          createTableWithStatsColumns(\n            \"CREATE\",\n            targetTable,\n            \"col0\" :: \"col1.col11\" :: Nil,\n            indexedColumns,\n            Some(nonEligibleTableSchema)))\n        checkError(\n          e,\n          \"DELTA_CLUSTERING_COLUMNS_DATATYPE_NOT_SUPPORTED\",\n          parameters = Map(\"columnsWithDataTypes\" -> \"col1.col11 : ARRAY<INT>\")\n        )\n      }\n    }\n  }\n\n  test(\"Replace clustered table with non-clustered table\") {\n    import testImplicits._\n    withTable(sourceTable) {\n      sql(s\"CREATE TABLE $sourceTable(i int, s string) USING delta\")\n      spark.range(1000)\n        .map(i => (i.intValue(), \"string col\"))\n        .toDF(\"i\", \"s\")\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .saveAsTable(sourceTable)\n\n      // Validate REPLACE TABLE (AS SELECT).\n      Seq(\"REPLACE\", \"CREATE OR REPLACE\").foreach { clause =>\n        withClusteredTable(testTable, \"a int\", \"a\") {\n          verifyClusteringColumns(TableIdentifier(testTable), Seq(\"a\"))\n\n          Seq(true, false).foreach { isRTAS =>\n            val testQuery = if (isRTAS) {\n              s\"$clause TABLE $testTable USING delta AS SELECT * FROM $sourceTable\"\n            } else {\n              sql(s\"$clause TABLE $testTable (i int, s string) USING delta\")\n              s\"INSERT INTO $testTable SELECT * FROM $sourceTable\"\n            }\n            sql(testQuery)\n            // Note that clustering table feature are still retained after REPLACE TABLE.\n            verifyClusteringColumns(TableIdentifier(testTable), Seq.empty)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Replace clustered table with non-clustered table - dataframe writer\") {\n    import testImplicits._\n    withTable(sourceTable) {\n      sql(s\"CREATE TABLE $sourceTable(i int, s string) USING delta\")\n      spark.range(1000)\n        .map(i => (i.intValue(), \"string col\"))\n        .toDF(\"i\", \"s\")\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .saveAsTable(sourceTable)\n\n      withClusteredTable(testTable, \"a int\", \"a\") {\n        verifyClusteringColumns(TableIdentifier(testTable), Seq(\"a\"))\n\n        spark.table(sourceTable)\n          .write\n          .format(\"delta\")\n          .mode(\"overwrite\")\n          .option(\"overwriteSchema\", \"true\")\n          .saveAsTable(testTable)\n        // Note that clustering table feature are still retained after REPLACE TABLE.\n        verifyClusteringColumns(TableIdentifier(testTable), Seq.empty)\n      }\n    }\n  }\n\n  protected def withTempDirIfNecessary(f: Option[String] => Unit): Unit = {\n    if (isPathBased) {\n      withTempDir { dir =>\n        f(Some(dir.getAbsolutePath))\n      }\n    } else {\n      f(None)\n    }\n  }\n}\n\ntrait ClusteredTableDDLWithColumnMapping\n  extends ClusteredTableCreateOrReplaceDDLSuite\n    with DeltaColumnMappingSelectedTestMixin {\n\n  override protected def runOnlyTests: Seq[String] = Seq(\n    \"validate dropping clustering column is not allowed: single clustering column\",\n    \"validate dropping clustering column is not allowed: multiple clustering columns\",\n    \"validate dropping clustering column is not allowed: clustering column + \" +\n      \"non-clustering column\",\n    \"validate RESTORE on clustered table\"\n  )\n\n  test(\"validate dropping clustering column is not allowed: single clustering column\") {\n    withClusteredTable(testTable, \"col1 INT, col2 STRING, col3 LONG\", \"col1\") {\n      val e = intercept[DeltaAnalysisException] {\n        sql(s\"ALTER TABLE $testTable DROP COLUMNS (col1)\")\n      }\n      checkError(\n        e,\n        \"DELTA_UNSUPPORTED_DROP_CLUSTERING_COLUMN\",\n        parameters = Map(\"columnList\" -> \"col1\")\n      )\n      // Drop non-clustering columns are allowed.\n      sql(s\"ALTER TABLE $testTable DROP COLUMNS (col2)\")\n    }\n  }\n\n  test(\"validate dropping clustering column is not allowed: multiple clustering columns\") {\n    withClusteredTable(testTable, \"col1 INT, col2 STRING, col3 LONG\", \"col1, col2\") {\n      val e = intercept[DeltaAnalysisException] {\n        sql(s\"ALTER TABLE $testTable DROP COLUMNS (col1, col2)\")\n      }\n      checkError(\n        e,\n        \"DELTA_UNSUPPORTED_DROP_CLUSTERING_COLUMN\",\n        parameters = Map(\"columnList\" -> \"col1,col2\")\n      )\n    }\n  }\n\n  test(\"validate dropping clustering column is not allowed: clustering column + \" +\n    \"non-clustering column\") {\n    withClusteredTable(testTable, \"col1 INT, col2 STRING, col3 LONG\", \"col1, col2\") {\n      val e = intercept[DeltaAnalysisException] {\n        sql(s\"ALTER TABLE $testTable DROP COLUMNS (col1, col3)\")\n      }\n      checkError(\n        e,\n        \"DELTA_UNSUPPORTED_DROP_CLUSTERING_COLUMN\",\n        parameters = Map(\"columnList\" -> \"col1\")\n      )\n    }\n  }\n}\n\ntrait ClusteredTableDDLWithColumnMappingV2Base extends ClusteredTableDDLWithColumnMapping {\n  test(\"test clustering column names (alter table + create table) with spaces\") {\n    withClusteredTable(testTable, \"`col1 a` INT, col2 INT, col3 STRUCT<col4 INT, `col5 b` INT>, \" +\n      \"`col6 c` STRUCT<col7 INT, `col8 d.e` INT>, `col9.f` INT\", \"`col1 a`\") {\n      val tableIdentifier = TableIdentifier(testTable)\n      verifyClusteringColumns(tableIdentifier, Seq(\"`col1 a`\"))\n\n      // Test ALTER CLUSTER BY to change clustering columns away from names with spaces.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (col2)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"col2\"))\n\n      // Test ALTER CLUSTER BY to test with structs with spaces in varying places.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (col3.`col5 b`, `col6 c`.col7)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"col3.`col5 b`\", \"`col6 c`.col7\"))\n\n      // Test ALTER CLUSTER BY on structs with spaces on both entries and with no spaces in the same\n      // clustering spec, including cases where there is a '.' in the name.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (col3.col4, `col6 c`.`col8 d.e`, `col1 a`)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"col3.col4\", \"`col6 c`.`col8 d.e`\", \"`col1 a`\"))\n\n      // Test ALTER TABLE CLUSTER BY after renaming a column to include spaces in the name.\n      sql(s\"ALTER TABLE $testTable RENAME COLUMN col2 to `col2 e`\")\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (`col2 e`)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"`col2 e`\"))\n\n      // Test ALTER TABLE with '.' in the name.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (`col9.f`)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"`col9.f`\"))\n    }\n  }\n\n  test(\"validate create table with commas in the column name\") {\n    withClusteredTable(testTable, \"`col1,a` BIGINT\", \"`col1,a`\") {\n      verifyClusteringColumns(TableIdentifier(testTable), Seq(\"`col1,a`\"))\n    }\n    withClusteredTable(testTable, \"`,col1,a,` BIGINT\", \"`,col1,a,`\") {\n      verifyClusteringColumns(TableIdentifier(testTable), Seq(\"`,col1,a,`\"))\n    }\n    withClusteredTable(testTable, \"`,col1,a,` BIGINT, `col2` BIGINT\", \"`,col1,a,`, `col2`\") {\n      verifyClusteringColumns(TableIdentifier(testTable), Seq(\"`,col1,a,`\", \"col2\"))\n    }\n    withClusteredTable(testTable, \"`,col1,a,` BIGINT, col2 BIGINT\", \"col2\") {\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (`,col1,a,`)\")\n      verifyClusteringColumns(TableIdentifier(testTable), Seq(\"`,col1,a,`\"))\n    }\n  }\n}\n\ntrait ClusteredTableDDLWithColumnMappingV2\n  extends ClusteredTableDDLWithColumnMappingV2Base\n\ntrait ClusteredTableCreateOrReplaceDDLSuite\n  extends ClusteredTableCreateOrReplaceDDLSuiteBase\n\ntrait ClusteredTableDDLSuiteBase\n  extends ClusteredTableCreateOrReplaceDDLSuite\n    with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  test(\"cluster by with more than 4 columns - alter table\") {\n    val testTable = \"test_table\"\n    withClusteredTable(testTable, \"a INT, b INT, c INT, d INT, e INT\", \"a\") {\n      val e = intercept[DeltaAnalysisException] {\n        sql(s\"ALTER TABLE $testTable CLUSTER BY (a, b, c, d, e)\")\n      }\n      checkError(\n        e,\n        \"DELTA_CLUSTER_BY_INVALID_NUM_COLUMNS\",\n        parameters = Map(\n          \"numColumnsLimit\" -> \"4\",\n          \"actualNumColumns\" -> \"5\")\n      )\n    }\n  }\n\n  test(\"alter table cluster by - valid scenarios\") {\n    withClusteredTable(testTable, \"id INT, a STRUCT<b INT, c STRING>, name STRING\", \"id, name\") {\n      val tableIdentifier = TableIdentifier(testTable)\n      verifyClusteringColumns(tableIdentifier, Seq(\"id\", \"name\"))\n\n      // Change the clustering columns and verify that they are changed in both\n      // Delta logs and catalog.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (name)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"name\"))\n\n      // Nested column scenario.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (a.b, id)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"a.b\", \"id\"))\n    }\n  }\n\n  test(\"alter table cluster by - catalog reflects clustering columns when reordered\") {\n    withClusteredTable(testTable, \"id INT, a STRUCT<b INT, c STRING>, name STRING\", \"id, name\") {\n      val tableIdentifier = TableIdentifier(testTable)\n      verifyClusteringColumns(tableIdentifier, Seq(\"id\", \"name\"))\n\n      // Re-order the clustering keys and validate the catalog sees the correctly reordered keys.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (name, id)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"name\", \"id\"))\n    }\n  }\n\n  test(\"alter table cluster by - error scenarios\") {\n    withClusteredTable(testTable, \"id INT, id2 INT, name STRING\", \"id, name\") {\n      // Specify non-existing columns.\n      val e = intercept[AnalysisException] {\n        sql(s\"ALTER TABLE $testTable CLUSTER BY (invalid)\")\n      }\n      assert(e.getMessage.contains(\"Couldn't find column\"))\n\n      // Specify duplicate clustering columns.\n      val e2 = intercept[DeltaAnalysisException] {\n        sql(s\"ALTER TABLE $testTable CLUSTER BY (id, id)\")\n      }\n      assert(e2.getErrorClass == \"DELTA_DUPLICATE_COLUMNS_FOUND\")\n      assert(e2.getSqlState == \"42711\")\n      assert(e2.getMessageParametersArray === Array(\"in CLUSTER BY\", \"`id`\"))\n    }\n  }\n\n  test(\"alter table cluster by none\") {\n    withClusteredTable(testTable, \"id Int\", \"id\") {\n      val tableIdentifier = TableIdentifier(testTable)\n      verifyClusteringColumns(tableIdentifier, Seq(\"id\"))\n\n      sql(s\"ALTER TABLE $testTable CLUSTER BY NONE\")\n      verifyClusteringColumns(tableIdentifier, Seq.empty)\n    }\n  }\n\n  test(\"optimize clustered table and trigger regular compaction\") {\n    assume(!catalogOwnedDefaultCreationEnabledInTests,\n      \"OPTIMIZE is blocked on catalog-managed tables\")\n    withClusteredTable(testTable, \"a INT, b STRING\", \"a, b\") {\n      val tableIdentifier = TableIdentifier(testTable)\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\", \"b\"))\n\n      (1 to 1000).map(i => (i, i.toString)).toDF(\"a\", \"b\")\n        .write.mode(\"append\").format(\"delta\").saveAsTable(testTable)\n\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTable))\n      val targetFileSize = (snapshot.sizeInBytes / 10).toString\n      withSQLConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> targetFileSize,\n        DeltaSQLConf.DELTA_OPTIMIZE_MIN_FILE_SIZE.key -> targetFileSize) {\n        runOptimize(testTable) { metrics =>\n          assert(metrics.numFilesAdded > 0)\n          assert(metrics.numFilesRemoved > 0)\n          assert(metrics.clusteringStats.nonEmpty)\n          assert(metrics.clusteringStats.get.numOutputZCubes == 1)\n        }\n      }\n\n      // ALTER TABLE CLUSTER BY NONE and then OPTIMIZE to trigger regular compaction.\n      sql(s\"ALTER TABLE $testTable CLUSTER BY NONE\")\n      verifyClusteringColumns(tableIdentifier, Seq.empty)\n\n      (1001 to 2000).map(i => (i, i.toString)).toDF(\"a\", \"b\")\n        .repartition(10).write.mode(\"append\").format(\"delta\").saveAsTable(testTable)\n      val newSnapshot = deltaLog.update()\n      val newTargetFileSize = (newSnapshot.sizeInBytes / 10).toString\n      withSQLConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> newTargetFileSize,\n        DeltaSQLConf.DELTA_OPTIMIZE_MIN_FILE_SIZE.key -> newTargetFileSize) {\n        runOptimize(testTable) { metrics =>\n          assert(metrics.numFilesAdded > 0)\n          assert(metrics.numFilesRemoved > 0)\n          // No clustering or zorder stats indicates regular compaction.\n          assert(metrics.clusteringStats.isEmpty)\n          assert(metrics.zOrderStats.isEmpty)\n        }\n      }\n    }\n  }\n\n  test(\"optimize clustered table - error scenarios\") {\n    assume(!catalogOwnedDefaultCreationEnabledInTests,\n      \"OPTIMIZE is blocked on catalog-managed tables\")\n    withClusteredTable(testTable, \"a INT, b STRING\", \"a\") {\n      // Specify partition predicate.\n      val e = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"OPTIMIZE $testTable WHERE a > 0 and b = foo\")\n      }\n      checkError(\n        e,\n        \"DELTA_CLUSTERING_WITH_PARTITION_PREDICATE\",\n        parameters = Map(\"predicates\" -> \"a > 0 and b = foo\")\n      )\n\n      // Specify ZORDER BY.\n      val e2 = intercept[DeltaAnalysisException] {\n        sql(s\"OPTIMIZE $testTable ZORDER BY (a)\")\n      }\n      checkError(\n        e2,\n        \"DELTA_CLUSTERING_WITH_ZORDER_BY\",\n        parameters = Map(\"zOrderBy\" -> \"a\")\n      )\n    }\n  }\n\n  test(\"Validate stats collected - alter table\") {\n    val tableSchema = \"col0 int, col1 STRUCT<col11: int, col12: string>\"\n    val indexedColumns = 2\n    // Validate ALTER TABLE can not change to a missing stats column.\n    testStatsCollectionHelper(\n      tableSchema = tableSchema,\n      numberOfIndexedCols = indexedColumns) {\n      withTable(testTable) {\n        createTableWithStatsColumns(\n          \"CREATE\",\n          testTable,\n          \"col0\" :: \"col1.col11\" :: Nil,\n          indexedColumns,\n          Some(tableSchema))\n        // Try to alter to col1.col12 which is missing stats.\n        val e = intercept[DeltaAnalysisException] {\n          sql(\n            s\"\"\"\n               |ALTER TABLE $testTable\n               |CLUSTER BY (col0, col1.col12)\n               |\"\"\".stripMargin)\n        }\n        val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTable))\n        checkError(\n          e,\n          \"DELTA_CLUSTERING_COLUMN_MISSING_STATS\",\n          parameters = Map(\n            \"columns\" -> \"col1.col12\",\n            \"schema\" -> snapshot.statCollectionLogicalSchema.treeString)\n        )\n      }\n    }\n  }\n\n  Seq(\"true\", \"false\").foreach { checkEnabled =>\n    test(s\"Alter column after statement with stats schema update - checkEnabled=$checkEnabled\") {\n      withTable(testTable) {\n        withSQLConf(\n          DeltaSQLConf.DELTA_LIQUID_ALTER_COLUMN_AFTER_STATS_SCHEMA_CHECK.key -> checkEnabled) {\n          val tableSchema = \"c1 int, c2 int, c3 int, c4 int\"\n          val indexedColumns = 2\n\n          testStatsCollectionHelper(\n            tableSchema = tableSchema,\n            numberOfIndexedCols = indexedColumns) {\n\n            createTableWithStatsColumns(\n              \"CREATE\",\n              testTable,\n              Seq(\"c1\", \"c2\"),\n              indexedColumns,\n              Some(tableSchema))\n\n            // Insert data to ensure stats are collected\n            sql(s\"INSERT INTO $testTable VALUES(1, 2, 3, 4), (5, 6, 7, 8)\")\n\n            // ALTER TABLE ALTER COLUMN should succeed when checkEnabled=false\n            sql(s\"ALTER TABLE $testTable ALTER COLUMN c1 AFTER c3\")\n\n            // Verify the column order changed\n            val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTable))\n            assert(snapshot.schema.fieldNames.toSeq === Seq(\"c2\", \"c3\", \"c1\", \"c4\"))\n\n            // Try another ALTER - behavior depends on checkEnabled\n            if (checkEnabled == \"true\") {\n              // Should fail when validation is enabled\n              val e = intercept[DeltaAnalysisException] {\n                sql(s\"ALTER TABLE $testTable ALTER COLUMN c2 AFTER c3\")\n              }\n              assert(e.errorClass.contains(\"DELTA_CLUSTERING_COLUMN_MISSING_STATS\"))\n            } else {\n              // Should succeed when validation is disabled\n              sql(s\"ALTER TABLE $testTable ALTER COLUMN c2 AFTER c3\")\n              val (_, snapshot2) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(testTable))\n              assert(snapshot2.schema.fieldNames.toSeq === Seq(\"c3\", \"c2\", \"c1\", \"c4\"))\n            }\n          }\n        }\n      }\n    }\n  }\n\n\n  test(\"validate CLONE on clustered table\") {\n    assume(!catalogOwnedDefaultCreationEnabledInTests,\n      \"OPTIMIZE is blocked on catalog-managed tables\")\n    import testImplicits._\n    val srcTable = \"SrcTbl\"\n    val dstTable1 = \"DestTbl1\"\n    val dstTable2 = \"DestTbl2\"\n    val dstTable3 = \"DestTbl3\"\n\n    withTable(srcTable, dstTable1, dstTable2, dstTable3) {\n      // Create the source table.\n      sql(s\"CREATE TABLE $srcTable (col1 INT, col2 INT, col3 INT) \" +\n        s\"USING delta CLUSTER BY (col1, col2)\")\n      val tableIdent = new TableIdentifier(srcTable)\n      (1 to 100).map(i => (i, i + 1000, i + 100)).toDF(\"col1\", \"col2\", \"col3\")\n        .repartitionByRange(100, col(\"col1\"))\n        .write.format(\"delta\").mode(\"append\").saveAsTable(srcTable)\n\n      // Force clustering on the source table.\n      val (_, srcSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdent)\n      val ingestionSize = srcSnapshot.allFiles.collect().map(_.size).sum\n      withSQLConf(\n        DeltaSQLConf.DELTA_OPTIMIZE_MAX_FILE_SIZE.key -> (ingestionSize / 4).toString) {\n        runOptimize(srcTable) { res =>\n          assert(res.numFilesAdded === 4)\n          assert(res.numFilesRemoved === 100)\n        }\n      }\n\n      // Create destination table as a clone of the source table.\n      sql(s\"CREATE TABLE $dstTable1 SHALLOW CLONE $srcTable\")\n\n      // Validate clustering columns and that clustering columns in stats schema.\n      val (_, dstSnapshot1) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(dstTable1))\n      verifyClusteringColumns(TableIdentifier(dstTable1), Seq(\"col1\", \"col2\"))\n      ClusteredTableUtils.validateClusteringColumnsInStatsSchema(dstSnapshot1, Seq(\"col1\", \"col2\"))\n\n      // Change to CLUSTER BY NONE, then CLONE from earlier version to validate that the\n      // clustering column information is maintainted.\n      sql(s\"ALTER TABLE $srcTable CLUSTER BY NONE\")\n      sql(s\"CREATE TABLE $dstTable2 SHALLOW CLONE $srcTable VERSION AS OF 2\")\n      val (_, dstSnapshot2) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(dstTable2))\n      verifyClusteringColumns(TableIdentifier(dstTable2), Seq(\"col1\", \"col2\"))\n      ClusteredTableUtils.validateClusteringColumnsInStatsSchema(dstSnapshot2, Seq(\"col1\", \"col2\"))\n\n      // Validate CLONE after CLUSTER BY NONE\n      sql(s\"CREATE TABLE $dstTable3 SHALLOW CLONE $srcTable\")\n      val (_, dstSnapshot3) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(dstTable3))\n      verifyClusteringColumns(TableIdentifier(dstTable3), Seq.empty)\n      ClusteredTableUtils.validateClusteringColumnsInStatsSchema(dstSnapshot3, Seq.empty)\n\n    }\n  }\n\n  test(\"alter table cluster by none is a no-op on non-clustered tables\") {\n    withTable(testTable) {\n      sql(s\"CREATE TABLE $testTable (a INT, b STRING) USING delta\")\n      val tableIdentifier = TableIdentifier(testTable)\n      val (_, initialSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n\n      // Verify that ALTER TABLE CLUSTER BY NONE does not enable clustering and is a no-op.\n      val clusterByLogs = Log4jUsageLogger.track {\n        sql(s\"ALTER TABLE $testTable CLUSTER BY NONE\")\n      }.filter { e =>\n        e.metric == MetricDefinitions.EVENT_TAHOE.name &&\n          e.tags.get(\"opType\").contains(\"delta.ddl.alter.clusterBy\")\n      }\n      assert(clusterByLogs.nonEmpty)\n      val clusterByLogJson = JsonUtils.fromJson[Map[String, Any]](clusterByLogs.head.blob)\n      assert(clusterByLogJson(\"isClusterByNoneSkipped\").asInstanceOf[Boolean])\n      val (_, finalSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      assert(!ClusteredTableUtils.isSupported(finalSnapshot.protocol))\n\n      // Snapshot equality shows that no table features can be changed.\n      assert(initialSnapshot.version == finalSnapshot.version)\n      assert(initialSnapshot.protocol.readerAndWriterFeatureNames ==\n        finalSnapshot.protocol.readerAndWriterFeatureNames)\n    }\n  }\n\n  test(\"alter table set tbl properties not allowed for clusteringColumns\") {\n    withClusteredTable(testTable, \"a INT, b STRING\", \"a\") {\n      val e = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"\"\"\n           |ALTER TABLE $testTable SET TBLPROPERTIES\n           |('${ClusteredTableUtils.PROP_CLUSTERING_COLUMNS}' = '[[\\\"b\\\"]]')\n           |\"\"\".stripMargin)\n      }\n      checkError(\n        e,\n        \"DELTA_CANNOT_MODIFY_TABLE_PROPERTY\",\n        parameters = Map(\"prop\" -> \"clusteringColumns\"))\n    }\n  }\n\n  test(\"validate RESTORE on clustered table\") {\n    val tableIdentifier = TableIdentifier(testTable)\n    // Scenario 1: restore clustered table to unclustered version.\n    withTable(testTable) {\n      sql(s\"CREATE TABLE $testTable (a INT, b STRING) USING delta\")\n      val (_, startingSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      assert(!ClusteredTableUtils.isSupported(startingSnapshot.protocol))\n\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (a)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"))\n\n      sql(s\"RESTORE TABLE $testTable TO VERSION AS OF 0\")\n      val (_, currentSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      verifyClusteringColumns(tableIdentifier, Seq.empty, skipCatalogCheck = true)\n    }\n\n    // Scenario 2: restore clustered table to previous clustering columns.\n    withClusteredTable(testTable, \"a INT, b STRING\", \"a\") {\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"))\n\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (b)\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"b\"))\n\n      sql(s\"RESTORE TABLE $testTable TO VERSION AS OF 0\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"), skipCatalogCheck = true)\n    }\n\n    // Scenario 3: restore from table with clustering columns to non-empty clustering columns\n    withClusteredTable(testTable, \"a int\", \"a\") {\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"))\n\n      sql(s\"ALTER TABLE $testTable CLUSTER BY NONE\")\n      verifyClusteringColumns(tableIdentifier, Seq.empty)\n\n      sql(s\"RESTORE TABLE $testTable TO VERSION AS OF 0\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"), skipCatalogCheck = true)\n    }\n\n    // Scenario 4: restore to start version.\n    withClusteredTable(testTable, \"a int\", \"a\") {\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"))\n\n      sql(s\"INSERT INTO $testTable VALUES (1)\")\n\n      sql(s\"RESTORE TABLE $testTable TO VERSION AS OF 0\")\n      verifyClusteringColumns(tableIdentifier, Seq(\"a\"), skipCatalogCheck = true)\n    }\n\n    // Scenario 5: restore unclustered table to unclustered table.\n    withTable(testTable) {\n      sql(s\"CREATE TABLE $testTable (a INT) USING delta\")\n      val (_, startingSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      assert(!ClusteredTableUtils.isSupported(startingSnapshot.protocol))\n      assert(!startingSnapshot.domainMetadata.exists(_.domain ==\n        ClusteringMetadataDomain.domainName))\n\n      sql(s\"INSERT INTO $testTable VALUES (1)\")\n\n      sql(s\"RESTORE TABLE $testTable TO VERSION AS OF 0\").collect\n      val (_, currentSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      assert(!ClusteredTableUtils.isSupported(currentSnapshot.protocol))\n      assert(!currentSnapshot.domainMetadata.exists(_.domain ==\n        ClusteringMetadataDomain.domainName))\n    }\n\n    // Scenario 6: restore clustered table to unclustered table.\n    withTable(testTable) {\n      sql(s\"CREATE TABLE $testTable (a INT) USING delta\")\n      val (_, startingSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      assert(!ClusteredTableUtils.isSupported(startingSnapshot.protocol))\n      assert(!startingSnapshot.domainMetadata.exists(_.domain ==\n        ClusteringMetadataDomain.domainName))\n\n      sql(s\"ALTER TABLE $testTable CLUSTER BY (a)\")\n\n      sql(s\"RESTORE TABLE $testTable TO VERSION AS OF 0\")\n\n      val (_, currentSnapshot) = DeltaLog.forTableWithSnapshot(spark, tableIdentifier)\n      assert(ClusteredTableUtils.isSupported(currentSnapshot.protocol))\n      verifyClusteringColumns(tableIdentifier, Seq.empty[String], skipCatalogCheck = true)\n    }\n  }\n\n  test(\"Variant is not supported\") {\n    val e = intercept[DeltaAnalysisException] {\n      createOrReplaceClusteredTable(\"CREATE\", testTable, \"id long, v variant\", \"v\")\n    }\n    checkError(\n      e,\n      \"DELTA_CLUSTERING_COLUMNS_DATATYPE_NOT_SUPPORTED\",\n      parameters = Map(\"columnsWithDataTypes\" -> \"v : VARIANT\")\n    )\n  }\n}\n\ntrait ClusteredTableDDLSuite\n  extends ClusteredTableDDLSuiteBase\n  with CatalogOwnedTestBaseSuite\n\ntrait ClusteredTableDDLWithNameColumnMapping\n  extends ClusteredTableCreateOrReplaceDDLSuite with DeltaColumnMappingEnableNameMode\n\ntrait ClusteredTableDDLWithIdColumnMapping\n  extends ClusteredTableCreateOrReplaceDDLSuite with DeltaColumnMappingEnableIdMode\n\ntrait ClusteredTableDDLWithV2Base\n  extends ClusteredTableCreateOrReplaceDDLSuite\n    with SharedSparkSession {\n  override protected def supportedClauses: Seq[String] = Seq(\"CREATE\", \"REPLACE\")\n\n  testColTypeValidation(\"REPLACE\")\n\n  test(\"replace with different clustering columns\") {\n    withTable(sourceTable) {\n      sql(s\"CREATE TABLE $sourceTable(i int, s string) USING delta\")\n      // Validate REPLACE TABLE (AS SELECT).\n      Seq(\"REPLACE\", \"CREATE OR REPLACE\").foreach { clause =>\n        Seq(true, false).foreach { isRTAS =>\n          withTempDirIfNecessary { location =>\n            withClusteredTable(testTable, \"a int\", \"a\", location = location) {\n              if (isRTAS) {\n                createOrReplaceAsSelectClusteredTable(\n                  clause, testTable, sourceTable, \"i\", location = location)\n              } else {\n                createOrReplaceClusteredTable(\n                  clause, testTable, \"i int, b string\", \"i\", location = location)\n              }\n              verifyClusteringColumns(testTable, Seq(\"i\"), location)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Validate replacing clustered tables with partitioned tables is not allowed\") {\n    withTable(sourceTable) {\n      sql(s\"CREATE TABLE $sourceTable(i int, s string) USING delta\")\n\n      // Validate REPLACE TABLE (AS SELECT).\n      Seq(\"REPLACE\", \"CREATE OR REPLACE\").foreach { clause =>\n        withClusteredTable(testTable, \"a int\", \"a\") {\n          verifyClusteringColumns(TableIdentifier(testTable), Seq(\"a\"))\n\n          Seq(true, false).foreach { isRTAS =>\n            val e = intercept[DeltaAnalysisException] {\n              if (isRTAS) {\n                sql(s\"$clause TABLE $testTable USING delta PARTITIONED BY (i) \" +\n                  s\"AS SELECT * FROM $sourceTable\")\n              } else {\n                sql(s\"$clause TABLE $testTable (i int, b string) USING delta PARTITIONED BY (i)\")\n              }\n            }\n            checkError(\n              e,\n              \"DELTA_CLUSTERING_REPLACE_TABLE_WITH_PARTITIONED_TABLE\",\n              parameters = Map.empty\n            )\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Validate replacing partitioned tables with clustered tables is allowed\") {\n    withTable(sourceTable) {\n      sql(s\"CREATE TABLE $sourceTable(i int, s string) USING delta\")\n\n      // Validate REPLACE TABLE (AS SELECT).\n      Seq(\"REPLACE\", \"CREATE OR REPLACE\").foreach { clause =>\n        Seq(true, false).foreach { isRTAS =>\n          withTable(testTable) {\n            withTempDirIfNecessary { location =>\n              val locationClause = if (location.isEmpty) \"\" else s\"LOCATION '${location.get}'\"\n              sql(s\"CREATE TABLE $testTable USING delta PARTITIONED BY (i) $locationClause\" +\n                s\" SELECT 1 i, 'a' s\")\n              verifyPartitionColumns(TableIdentifier(testTable), Seq(\"i\"))\n              if (isRTAS) {\n                createOrReplaceAsSelectClusteredTable(\n                  clause, testTable, sourceTable, \"i\", location = location)\n              } else {\n                createOrReplaceClusteredTable(\n                  clause, testTable, \"i int, b string\", \"i\", location = location)\n              }\n              verifyClusteringColumns(testTable, Seq(\"i\"), location)\n              verifyPartitionColumns(TableIdentifier(testTable), Seq())\n            }\n          }\n        }\n      }\n    }\n  }\n\n  Seq(\n    (\"\",\n      \"a INT, b STRING, ts TIMESTAMP\",\n      Seq(\"a\", \"b\")),\n    (\" multipart name\",\n      \"a STRUCT<b INT, c STRING>, ts TIMESTAMP\",\n      Seq(\"a.b\", \"ts\"))\n  ).foreach { case (testSuffix, columns, clusteringColumns) =>\n    test(s\"create/replace table createOrReplace$testSuffix\") {\n      withTable(testTable) {\n        // Repeat two times to test both create and replace cases.\n        (1 to 2).foreach { _ =>\n          createOrReplaceClusteredTable(\n            \"CREATE OR REPLACE\", testTable, columns, clusteringColumns.mkString(\",\"))\n          verifyClusteringColumns(TableIdentifier(testTable), clusteringColumns)\n        }\n      }\n    }\n\n    test(s\"ctas/rtas createOrReplace$testSuffix\") {\n      withTable(sourceTable, targetTable) {\n        sql(s\"CREATE TABLE $sourceTable($columns) USING delta\")\n        withTempDirIfNecessary { location =>\n          // Repeat two times to test both create and replace cases.\n          (1 to 2).foreach { _ =>\n            createOrReplaceAsSelectClusteredTable(\n              \"CREATE OR REPLACE\",\n              targetTable,\n              sourceTable,\n              clusteringColumns.mkString(\",\"),\n              location = location)\n            verifyClusteringColumns(targetTable, clusteringColumns, location)\n          }\n        }\n      }\n    }\n  }\n}\n\ntrait ClusteredTableDDLWithV2\n  extends ClusteredTableDDLWithV2Base\n\ntrait ClusteredTableDDLDataSourceV2SuiteBase\n  extends ClusteredTableDDLWithV2\n    with ClusteredTableDDLSuite {\n  test(\"Create clustered table from external location, \" +\n    \"location has clustered table, schema not specified, cluster by not specified\") {\n    withTempDir { dir =>\n      // 1. Create a clustered table\n      sql(s\"create table delta.`${dir.getAbsolutePath}` (col1 int, col2 string) using delta \" +\n        \"cluster by (col1)\")\n\n      // 2. Create a clustered table from the external location.\n      withTable(\"clustered_table\") {\n        // When schema is not specified, the schema of the table is inferred from the external\n        // table.\n        sql(s\"CREATE EXTERNAL TABLE clustered_table USING delta LOCATION '${dir.getAbsolutePath}'\")\n        verifyClusteringColumns(TableIdentifier(\"clustered_table\"), Seq(\"col1\"))\n      }\n    }\n  }\n\n  test(\"create external non-clustered table: location has clustered table, schema specified, \" +\n    \"cluster by not specified\") {\n    val tableName = \"clustered_table\"\n    withTempDir { dir =>\n      // 1. Create a clustered table in the external location.\n      sql(s\"create table delta.`${dir.getAbsolutePath}` (col1 int, col2 string) using delta \" +\n        \"cluster by (col1)\")\n\n      // 2. Create a non-clustered table from the external location.\n      withTable(tableName) {\n        val e = intercept[DeltaAnalysisException] {\n          // When schema is specified, the schema has to match the schema of the external table.\n          sql(s\"CREATE EXTERNAL TABLE $tableName (col1 INT, col2 STRING) USING delta \" +\n            s\"LOCATION '${dir.getAbsolutePath}'\")\n        }\n        checkError(\n          e,\n          \"DELTA_CREATE_TABLE_WITH_DIFFERENT_CLUSTERING\",\n          parameters = Map(\n            \"path\" -> dir.toURI.toString.stripSuffix(\"/\"),\n            \"specifiedColumns\" -> \"\",\n            \"existingColumns\" -> \"col1\"))\n      }\n    }\n  }\n\n  test(\"create external clustered table: location has clustered table, schema specified, \" +\n    \"cluster by specified with different clustering column\") {\n    val tableName = \"clustered_table\"\n    withTempDir { dir =>\n      // 1. Create a clustered table in the external location.\n      sql(s\"create table delta.`${dir.getAbsolutePath}` (col1 int, col2 string) using delta \" +\n        \"cluster by (col1)\")\n\n      // 2. Create a clustered table from the external location.\n      withTable(tableName) {\n        val e = intercept[DeltaAnalysisException] {\n          sql(s\"CREATE EXTERNAL TABLE $tableName (col1 INT, col2 STRING) USING delta \" +\n            s\"CLUSTER BY (col2) LOCATION '${dir.getAbsolutePath}'\")\n        }\n        checkError(\n          e,\n          \"DELTA_CREATE_TABLE_WITH_DIFFERENT_CLUSTERING\",\n          parameters = Map(\n            \"path\" -> dir.toURI.toString.stripSuffix(\"/\"),\n            \"specifiedColumns\" -> \"col2\",\n            \"existingColumns\" -> \"col1\"))\n      }\n    }\n  }\n\n  test(\"create external clustered table: location has clustered table, schema specified, \" +\n    \"cluster by specified with same clustering column\") {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"CatalogOwned does not support external table creation.\")\n    }\n    val tableName = \"clustered_table\"\n    withTempDir { dir =>\n      // 1. Create a clustered table in the external location.\n      sql(s\"create table delta.`${dir.getAbsolutePath}` (col1 int, col2 string) using delta \" +\n        \"cluster by (col1)\")\n\n      // 2. Create a clustered table from the external location.\n      withTable(tableName) {\n        sql(s\"CREATE EXTERNAL TABLE $tableName (col1 INT, col2 STRING) USING delta \" +\n          s\"CLUSTER BY (col1) LOCATION '${dir.getAbsolutePath}'\")\n        verifyClusteringColumns(TableIdentifier(tableName), Seq(\"col1\"))\n      }\n    }\n  }\n\n  test(\"create external clustered table: location has non-clustered table, schema specified, \" +\n    \"cluster by specified\") {\n    if (catalogOwnedDefaultCreationEnabledInTests) {\n      cancel(\"CatalogOwned does not support external table creation.\")\n    }\n    val tableName = \"clustered_table\"\n    withTempDir { dir =>\n      // 1. Create a non-clustered table in the external location.\n      sql(s\"create table delta.`${dir.getAbsolutePath}` (col1 int, col2 string) using delta\")\n\n      // 2. Create a clustered table from the external location.\n      withTable(tableName) {\n        val e = intercept[DeltaAnalysisException] {\n          sql(s\"CREATE EXTERNAL TABLE $tableName (col1 INT, col2 STRING) USING delta \" +\n            s\"CLUSTER BY (col1) LOCATION '${dir.getAbsolutePath}'\")\n        }\n        checkError(\n          e,\n          \"DELTA_CREATE_TABLE_WITH_DIFFERENT_CLUSTERING\",\n          parameters = Map(\n            \"path\" -> dir.toURI.toString.stripSuffix(\"/\"),\n            \"specifiedColumns\" -> \"col1\",\n            \"existingColumns\" -> \"\"))\n      }\n    }\n  }\n}\n\nclass ClusteredTableDDLDataSourceV2Suite\n  extends ClusteredTableDDLDataSourceV2SuiteBase\n\nclass ClusteredTableDDLDataSourceV2IdColumnMappingSuite\n  extends ClusteredTableDDLWithIdColumnMapping\n    with ClusteredTableDDLWithV2\n    with ClusteredTableDDLWithColumnMappingV2\n    with ClusteredTableDDLSuite\n\nclass ClusteredTableDDLDataSourceV2NameColumnMappingSuite\n  extends ClusteredTableDDLWithNameColumnMapping\n    with ClusteredTableDDLWithV2\n    with ClusteredTableDDLWithColumnMappingV2\n    with ClusteredTableDDLSuite\n\nclass ClusteredTableDDLDataSourceV2WithCatalogOwnedBatch100Suite\n  extends ClusteredTableDDLDataSourceV2Suite {\n\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/clustering/ClusteringColumnSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtils\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\nclass ClusteringColumnSuite extends ClusteredTableTestUtils with DeltaSQLCommandTest {\n  test(\"ClusteringColumnInfo: validate logical column\") {\n    val table = \"test_table\"\n    withTable(table) {\n      // Create a table with nested dot name column.\n      sql(s\"CREATE TABLE $table(col0 int, col1 STRUCT<`x.y`: int, z: int>) USING DELTA\")\n      val col0 = \"col0\"\n      val dotColumnName = \"col1.`x.y`\"\n\n      val schema = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))._2.schema\n      val columnInfo0 = ClusteringColumnInfo(schema, ClusteringColumn(schema, col0))\n      val columnInfoDot = ClusteringColumnInfo(schema, ClusteringColumn(schema, dotColumnName))\n\n      assert(columnInfo0.logicalName === col0)\n      assert(columnInfoDot.logicalName === dotColumnName)\n    }\n  }\n\n  test(\"ClusteringColumnInfo: extractLogicalNames\") {\n    val table = \"test_table\"\n    // Create a table with nested dot name column.\n    withClusteredTable(\n      table,\n      \"col0 int, col1 STRUCT<`x.y`: int, z: int>\",\n      \"col0, col1.`x.y`\") {\n      val col0 = \"col0\"\n      val dotColumnName = \"col1.`x.y`\"\n\n      val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n      assert(ClusteringColumnInfo.extractLogicalNames(snapshot) == Seq(col0, dotColumnName))\n    }\n  }\n\n  test(\"ClusteringColumn: throws correct error when column not found\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl (a INT) USING DELTA\")\n      val schema = spark.table(\"tbl\").schema\n      val e = intercept[AnalysisException] {\n        ClusteringColumn(schema, \"b\")\n      }\n      checkError(\n        e,\n        \"DELTA_COLUMN_NOT_FOUND_IN_SCHEMA\",\n        parameters = Map(\n          \"columnName\" -> \"b\",\n          \"tableSchema\" -> schema.treeString))\n    }\n  }\n\n  test(\"ClusteringColumn: throws correct error when nested column not found\") {\n    withTable(\"tbl\") {\n      sql(\"CREATE TABLE tbl (a INT, b STRING) USING DELTA\")\n      val schema = spark.table(\"tbl\").schema\n      val e = intercept[AnalysisException] {\n        ClusteringColumn(schema, \"b.c\")\n      }\n      checkError(\n        e,\n        \"DELTA_COLUMN_NOT_FOUND_IN_SCHEMA\",\n        parameters = Map(\n          \"columnName\" -> \"b.c\",\n          \"tableSchema\" -> schema.treeString))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/clustering/ClusteringProviderSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, DeltaOperations}\nimport org.apache.spark.sql.delta.actions.{AddFile, Metadata}\nimport org.apache.spark.sql.delta.actions.SingleAction._\nimport org.apache.spark.sql.delta.stats.DataSkippingReader\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass ClusteringProviderSuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  private def testAddFileWithSnapshotReconstructionHelper(\n      prefix: String)(collectFiles: DeltaLog => Seq[AddFile]): Unit = {\n    for (checkpointPolicy <- Seq(\"none\", \"classic\", \"v2\")) {\n      test(s\"$prefix - Validate clusteringProvider in snapshot reconstruction, \" +\n        s\"checkpointPolicy = $checkpointPolicy\") {\n        val file = AddFile(\n          path = \"path\",\n          partitionValues = Map.empty,\n          size = 1,\n          modificationTime = 1,\n          dataChange = true,\n          clusteringProvider = Some(\"liquid\"))\n\n        withTempDir { dir =>\n          val log = DeltaLog.forTable(spark, new Path(dir.getCanonicalPath))\n          log.startTransaction(None).commit(Metadata() :: Nil, DeltaOperations.ManualUpdate)\n          log.startTransaction(None).commit(file :: Nil, DeltaOperations.ManualUpdate)\n\n          if (checkpointPolicy != \"none\") {\n            spark.sql(s\"ALTER TABLE delta.`${dir.getAbsolutePath}` SET TBLPROPERTIES \" +\n              s\"('${DeltaConfigs.CHECKPOINT_POLICY.key}' = '$checkpointPolicy')\")\n            log.checkpoint(log.update())\n            // clear cache to force the snapshot reconstruction.\n            DeltaLog.clearCache()\n          }\n          val files = collectFiles(log)\n          assert(files.size === 1)\n          assert(files.head.clusteringProvider === Some(\"liquid\"))\n        }\n      }\n    }\n  }\n\n  testAddFileWithSnapshotReconstructionHelper(\"Default snapshot reconstruction\") { log =>\n    log.update().allFiles.collect()\n  }\n\n  testAddFileWithSnapshotReconstructionHelper(\"AddFile with stats\") { log =>\n    val statsDF = log.update().withStats.withColumn(\"stats\", DataSkippingReader.nullStringLiteral)\n    statsDF.as[AddFile].collect()\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/skipping/clustering/IncrementalZCubeClusteringSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.skipping.clustering\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtilsBase\nimport org.apache.spark.sql.delta.skipping.clustering.{ClusteredTableUtils, ClusteringColumnInfo, ClusteringFileStats, ClusteringStats}\nimport org.apache.spark.sql.delta.{DeltaLog, DeltaOperations, DeltaUnsupportedOperationException}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.zorder.ZCubeInfo\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.internal.SQLConf\n\nclass IncrementalZCubeClusteringSuite extends QueryTest\n  with ClusteredTableTestUtilsBase\n  with DeltaSQLCommandTest {\n  import testImplicits._\n\n  private val table: String = \"test_table\"\n\n  // Ingest data to create numFiles files with one row in each file.\n  private def addFiles(table: String, numFiles: Int): Unit = {\n    val df = (1 to numFiles).map(i => (i, i)).toDF(\"col1\", \"col2\")\n    withSQLConf(SQLConf.MAX_RECORDS_PER_FILE.key -> \"1\") {\n      df.write.format(\"delta\").mode(\"append\").saveAsTable(table)\n    }\n  }\n\n  private def getFiles(table: String): Set[AddFile] = {\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n    deltaLog.update().allFiles.collect().toSet\n  }\n\n  private def assertClustered(table: String, files: Set[AddFile]): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(table))\n    val clusteringColumns =\n      ClusteringColumnInfo.extractLogicalNames(deltaLog.update())\n    assert(files.forall(_.clusteringProvider.contains(ClusteredTableUtils.clusteringProvider)))\n    assert(files.forall { file =>\n      val zCubeInfo = ZCubeInfo.getForFile(file)\n      if (zCubeInfo.isEmpty) {\n        logError(s\"File $file is missing ZCube info.\")\n        false\n      } else {\n        zCubeInfo.get.zOrderBy == clusteringColumns\n      }\n    })\n  }\n\n  // The sentinel value to signal skipping size validation in ClusteringStats. This is used for the\n  // cases where file size can not be predicated due to compression and encoding.\n  private val SKIP_CHECK_SIZE_VALUE: Long = Long.MinValue\n\n  private def validateClusteringMetrics(\n      actualMetrics: ClusteringStats, expectedMetrics: ClusteringStats): Unit = {\n    var finalActualMetrics = actualMetrics\n    if (expectedMetrics.inputZCubeFiles.size == SKIP_CHECK_SIZE_VALUE) {\n      val stats = finalActualMetrics.inputZCubeFiles\n      finalActualMetrics =\n        finalActualMetrics.copy(inputZCubeFiles = stats.copy(size = SKIP_CHECK_SIZE_VALUE))\n    }\n    if (expectedMetrics.inputOtherFiles.size == SKIP_CHECK_SIZE_VALUE) {\n      val stats = finalActualMetrics.inputOtherFiles\n      finalActualMetrics =\n        finalActualMetrics.copy(inputOtherFiles = stats.copy(size = SKIP_CHECK_SIZE_VALUE))\n    }\n    if (expectedMetrics.mergedFiles.size == SKIP_CHECK_SIZE_VALUE) {\n      val stats = finalActualMetrics.mergedFiles\n      finalActualMetrics =\n        finalActualMetrics.copy(mergedFiles = stats.copy(size = SKIP_CHECK_SIZE_VALUE))\n    }\n    assert(expectedMetrics === finalActualMetrics)\n  }\n\n  private def getZCubeIds(table: String): Set[String] = {\n    val files = getFiles(table)\n    files.map(ZCubeInfo.getForFile).collect {\n      case Some(ZCubeInfo(id, _)) => id\n    }\n  }\n\n  test(\"test incremental clustering\") {\n    withSQLConf(\n      SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\") {\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1, col2\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n\n        // Optimize should cluster the data into two 2 files since MAX_RECORDS_PER_FILE is 2.\n        runOptimize(table) { metrics =>\n          assert(metrics.clusteringStats.nonEmpty)\n          validateClusteringMetrics(\n            actualMetrics = metrics.clusteringStats.get,\n            expectedMetrics = ClusteringStats(\n              inputZCubeFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n              inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              inputNumZCubes = 0,\n              mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              numOutputZCubes = 1))\n\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n        val files1 = getFiles(table)\n        assert(files1.size == 2)\n        assertClustered(table, files1)\n        assert(getZCubeIds(table).size === 1)\n\n        // re-optimize is no-op if there is single ZCUBE in the whole table.\n        withSQLConf(\n          // Make the current ZCUBE big enough to include all input in a single ZCUBE.\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> Long.MaxValue.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.numFilesRemoved === 0)\n          }\n        }\n        assert(files1 == getFiles(table))\n\n        // Append some new data and only cluster new files.\n        addFiles(table, numFiles = 4)\n        val files2 = getFiles(table)\n        assert(files2.size === 6)\n\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> 1.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(2, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 1,\n                mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n\n            assert(metrics.numFilesRemoved === 4)\n            assert(metrics.numFilesAdded === 2)\n          }\n        }\n        val files3 = getFiles(table)\n        assert(files3.intersect(files2) === files1)\n        assert(getZCubeIds(table).size === 2)\n\n        // Now there are 2 ZCUBEs, increase ZCUBE size and stable ZCUBEs should be re-clustered.\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> Long.MaxValue.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 2,\n                mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n            assert(metrics.numFilesRemoved === 4)\n            // 2 records per file.\n            assert(metrics.numFilesAdded === 4)\n          }\n        }\n        val files4 = getFiles(table)\n        assertClustered(table, files4)\n        assert(getZCubeIds(table).size === 1)\n      }\n    }\n  }\n\n  test(\"test changing clustering columns\") {\n    withSQLConf(\n      SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\",\n      // Enable update catalog for verifyClusteringColumns.\n      DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"true\") {\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1, col2\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n        // Cluster the table into two ZCUBEs.\n        runOptimize(table) { metrics =>\n          assert(metrics.clusteringStats.nonEmpty)\n          validateClusteringMetrics(\n            actualMetrics = metrics.clusteringStats.get,\n            expectedMetrics = ClusteringStats(\n              inputZCubeFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n              inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              inputNumZCubes = 0,\n              mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              numOutputZCubes = 1))\n\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n        assert(getFiles(table).size == 2)\n\n        addFiles(table, numFiles = 4)\n        assert(getFiles(table).size == 6)\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> 1.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(2, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 1,\n                mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n            assert(metrics.numFilesRemoved == 4)\n            assert(metrics.numFilesAdded == 2)\n          }\n        }\n        val files1 = getFiles(table)\n        assert(files1.size === 4)\n        assertClustered(table, files1)\n        assert(getZCubeIds(table).size == 2)\n\n        sql(s\"ALTER TABLE $table CLUSTER BY (col2, col1)\")\n        verifyClusteringColumns(TableIdentifier(table), Seq(\"col2\", \"col1\"))\n        // Incremental clustering won't touch those clustered files with different clustering\n        // columns, so re-clustering should be a no-op.\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> Long.MaxValue.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            assert(metrics.numFilesRemoved == 0)\n          }\n        }\n        assert(getFiles(table) === files1)\n\n        // Add more files and only new files are clustered.\n        addFiles(table, numFiles = 4)\n        val files2 = getFiles(table)\n        assert(files2.size === 8)\n        withSQLConf(\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> Long.MaxValue.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                // 8 files: 4 files from previously clustered files with different cluster keys\n                // and 4 files from newly added 4 un-clustered files.\n                inputOtherFiles = ClusteringFileStats(8, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 0,\n                mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n            assert(metrics.numFilesRemoved == 4)\n            assert(metrics.numFilesAdded == 2)\n          }\n        }\n        val files3 = getFiles(table)\n        assert(files3.size === 6)\n        // files1 are files with old clustering columns 'col1'.\n        assert(files3.intersect(files2) === files1)\n      }\n    }\n  }\n\n  test(\"OPTIMIZE FULL - change cluster keys\") {\n    withSQLConf(\n      SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\",\n      // Enable update catalog for verifyClusteringColumns.\n      DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"true\") {\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1, col2\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n        // Cluster the table into two ZCUBEs.\n        runOptimize(table) { metrics =>\n          assert(metrics.clusteringStats.nonEmpty)\n          validateClusteringMetrics(\n            actualMetrics = metrics.clusteringStats.get,\n            expectedMetrics = ClusteringStats(\n              inputZCubeFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n              inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              inputNumZCubes = 0,\n              mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              numOutputZCubes = 1))\n\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n        val files1 = getFiles(table)\n        assert(files1.size === 2)\n\n        addFiles(table, numFiles = 4)\n        assert(getFiles(table).size == 6)\n\n        // Change the clustering columns and verify files with previous clustering columns\n        // are not clustered.\n        sql(s\"ALTER TABLE $table CLUSTER BY (col2, col1)\")\n        verifyClusteringColumns(TableIdentifier(table), Seq(\"col2\", \"col1\"))\n\n        withSQLConf(\n          // Set an extreme value to make all zcubes unstable.\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> Long.MaxValue.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            assert(metrics.numFilesRemoved == 4)\n            assert(metrics.numFilesAdded == 2)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(6, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 0,\n                mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n          }\n        }\n        val files2 = getFiles(table)\n        assert(files2.size === 4)\n        assert(files2.forall { file =>\n          val zCubeInfo = ZCubeInfo.getForFile(file)\n          zCubeInfo.nonEmpty\n        })\n        assert(getZCubeIds(table).size == 2)\n        // validate files clustered to previous clustering columns are not re-clustered.\n        assert(files2.intersect(files1) === files1)\n\n        // OPTIMIZE FULL should re-cluster previously clustered files.\n        withSQLConf(\n          // Force all zcubes stable\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> 1.toString) {\n          runOptimizeFull(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            // Only files with old cluster keys are rewritten.\n            assert(metrics.numFilesRemoved == 2)\n            assert(metrics.numFilesAdded == 2)\n\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 2,\n                mergedFiles = ClusteringFileStats(2, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n          }\n        }\n        // all files have same clustering keys.\n        assert(getFiles(table).forall { f =>\n          val zCubeInfo = ZCubeInfo.getForFile(f).get\n          val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n          val clusteringColumns = ClusteringColumnInfo.extractLogicalNames(snapshot)\n          zCubeInfo.zOrderBy == clusteringColumns\n        })\n\n        // Incremental OPTIMIZE to validate no files should be clustered.\n        withSQLConf(\n          // Force all zcubes stable\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> 1.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            assert(metrics.numFilesRemoved == 0)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 2,\n                mergedFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 0))\n          }\n        }\n\n        // OPTIMIZE FULL again and all clustered files have same clustering columns and\n        // all ZCUBEs are stable.\n        withSQLConf(\n          // Force all zcubes stable\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> 1.toString) {\n          runOptimizeFull(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 2,\n                mergedFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 0))\n            assert(metrics.numFilesRemoved == 0)\n            assert(metrics.numFilesAdded == 0)\n          }\n        }\n      }\n    }\n  }\n\n  test(\"OPTIMIZE FULL - change clustering provider\") {\n    withSQLConf(\n      SQLConf.MAX_RECORDS_PER_FILE.key -> \"2\",\n      // Enable update catalog for verifyClusteringColumns.\n      DeltaSQLConf.DELTA_UPDATE_CATALOG_ENABLED.key -> \"true\") {\n      withClusteredTable(\n        table = table,\n        schema = \"col1 int, col2 int\",\n        clusterBy = \"col1, col2\") {\n        addFiles(table, numFiles = 4)\n        val files0 = getFiles(table)\n        assert(files0.size === 4)\n        // Cluster the table into two ZCUBEs.\n        runOptimize(table) { metrics =>\n          assert(metrics.clusteringStats.nonEmpty)\n          validateClusteringMetrics(\n            actualMetrics = metrics.clusteringStats.get,\n            expectedMetrics = ClusteringStats(\n              inputZCubeFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n              inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              inputNumZCubes = 0,\n              mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n              numOutputZCubes = 1))\n\n          assert(metrics.numFilesRemoved == 4)\n          assert(metrics.numFilesAdded == 2)\n        }\n        var files1 = getFiles(table)\n        assert(files1.size === 2)\n        for (f <- files1) {\n          assert(f.clusteringProvider.contains(ClusteredTableUtils.clusteringProvider))\n        }\n        // Change the clusteringProvider and verify files with different clusteringProvider\n        // are not clustered.\n        val (deltaLog, _) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(table))\n        val txn = deltaLog.startTransaction(None)\n        files1 = files1.map(f => f.copy(clusteringProvider = Some(\"customProvider\")))\n        txn.commit(files1.toIndexedSeq, DeltaOperations.ManualUpdate)\n        files1 = getFiles(table)\n        assert(files1.size === 2)\n        for (f <- files1) {\n          assert(f.clusteringProvider.contains(\"customProvider\"))\n        }\n\n        addFiles(table, numFiles = 4)\n        assert(getFiles(table).size == 6)\n\n        withSQLConf(\n          // Set an extreme value to make all zcubes unstable.\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> Long.MaxValue.toString) {\n          runOptimize(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            assert(metrics.numFilesRemoved == 4)\n            assert(metrics.numFilesAdded == 2)\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(2, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 1,\n                mergedFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n          }\n        }\n        val files2 = getFiles(table)\n        assert(files2.size === 4)\n        assert(files2.forall { file =>\n          val zCubeInfo = ZCubeInfo.getForFile(file)\n          zCubeInfo.nonEmpty\n        })\n        assert(getZCubeIds(table).size == 2)\n        // validate files with different clusteringProvider are not re-clustered.\n        assert(files2.intersect(files1) === files1)\n\n        // OPTIMIZE FULL should re-cluster previously clustered files.\n        withSQLConf(\n          // Force all zcubes stable\n          DeltaSQLConf.DELTA_OPTIMIZE_CLUSTERING_MIN_CUBE_SIZE.key -> 1.toString) {\n          runOptimizeFull(table) { metrics =>\n            assert(metrics.clusteringStats.nonEmpty)\n            // Only files with old cluster keys are rewritten.\n            assert(metrics.numFilesRemoved == 2)\n            assert(metrics.numFilesAdded == 2)\n\n            validateClusteringMetrics(\n              actualMetrics = metrics.clusteringStats.get,\n              expectedMetrics = ClusteringStats(\n                inputZCubeFiles = ClusteringFileStats(4, SKIP_CHECK_SIZE_VALUE),\n                inputOtherFiles = ClusteringFileStats(0, SKIP_CHECK_SIZE_VALUE),\n                inputNumZCubes = 2,\n                mergedFiles = ClusteringFileStats(2, SKIP_CHECK_SIZE_VALUE),\n                numOutputZCubes = 1))\n          }\n        }\n        // all files have same clustering provider.\n        assert(getFiles(table).forall { f =>\n          f.clusteringProvider.contains(ClusteredTableUtils.clusteringProvider)\n        })\n      }\n    }\n  }\n\n  // Test to validate OPTIMIZE FULL is only applied to a clustered table with non-empty clustering\n  // columns.\n  test(\"OPTIMIZE FULL - error cases\") {\n    withTable(table) {\n      sql(s\"CREATE TABLE $table(col1 INT, col2 INT, col3 INT) using delta\")\n      val e = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"OPTIMIZE $table FULL\")\n      }\n      checkError(e, \"DELTA_OPTIMIZE_FULL_NOT_SUPPORTED\")\n    }\n\n    withClusteredTable(table, \"col1 INT, col2 INT, col3 INT\", \"col1\") {\n      sql(s\"ALTER TABLE $table CLUSTER BY NONE\")\n      val e = intercept[DeltaUnsupportedOperationException] {\n        sql(s\"OPTIMIZE $table FULL\")\n      }\n      checkError(e, \"DELTA_OPTIMIZE_FULL_NOT_SUPPORTED\")\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/sources/DeltaSourceMetadataEvolutionSupportSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.sources\n\nimport org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaOptions, DeltaTestUtilsBase, DeltaThrowable}\n\nimport org.apache.spark.{SparkConf, SparkFunSuite}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Unit tests covering `DeltaSourceMetadataEvolutionSupport`, which detects non-additive schema\n * changes when reading from a Delta source and checks user provided confs to decide whether to\n * allow resuming streaming processing.\n */\nclass DeltaSourceMetadataEvolutionSupportSuite\n  extends SparkFunSuite\n    with SharedSparkSession\n    with DeltaTestUtilsBase {\n\n  protected override def sparkConf: SparkConf =\n    super.sparkConf\n      .set(DeltaSQLConf.DELTA_ALLOW_TYPE_WIDENING_STREAMING_SOURCE.key, \"true\")\n      .set(DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK.key, \"false\")\n\n  private def persistedMetadata(\n      schemaDDL: String,\n      physicalNames: Map[Seq[String], String]): PersistedMetadata = {\n    var schemaWithPhysicalNames = StructType.fromDDL(schemaDDL)\n    schemaWithPhysicalNames = DeltaColumnMapping.setPhysicalNames(\n      schema = schemaWithPhysicalNames,\n      physicalNames\n    )\n    schemaWithPhysicalNames = DeltaColumnMapping.assignPhysicalNames(\n      schemaWithPhysicalNames,\n      reuseLogicalName = true)\n\n    PersistedMetadata(\n      tableId = \"tableId\",\n      deltaCommitVersion = 0,\n      dataSchemaJson = schemaWithPhysicalNames.json,\n      partitionSchemaJson = \"\",\n      sourceMetadataPath = \"sourceMetadataPath\"\n    )\n  }\n\n  private def expectColumnMappingChangeBlocked(opType: String): ExpectedResult[Nothing] =\n    ExpectedResult.Failure(ex => {\n      assert(\n        ex.getErrorClass === \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\")\n      assert(ex.getMessageParameters.get(\"opType\") === opType)\n    })\n\n  private def expectTypeWideningBlocked(wideningTypeChanges: Seq[String]): ExpectedResult[Nothing] =\n    ExpectedResult.Failure(ex => {\n      assert(\n        ex.getErrorClass === \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\")\n      assert(ex.getMessageParameters.get(\"columnChangeDetails\")\n        .contains(wideningTypeChanges.mkString(\"\\n\")))\n    })\n\n  private def expectNonWideningTypeChangeError: ExpectedResult[Nothing] =\n    ExpectedResult.Failure(ex => {\n      assert(\n        ex.getErrorClass === \"DELTA_SCHEMA_CHANGED_WITH_VERSION\")\n    })\n\n  private def withSQLConfUnblockedChanges(unblock: Seq[String])(f: => Unit): Unit = {\n    val confs = unblock.map( conf => s\"spark.databricks.delta.streaming.$conf\" -> \"always\")\n    withSQLConf(confs: _*) {\n      f\n    }\n  }\n\n  /**\n   * Unit test runner covering `validateIfSchemaChangeCanBeUnblocked()`. Takes as input\n   * an initial schema (from) and an updated schema (to) and checks that:\n   *   1. Non-additive schema changes are correctly detected: matches `expectedResult`\n   *   2. Setting SQL confs to unblock the changes allows the check to succeeds.\n   * @param name              Name of the test.\n   * @param fromDDL           Initial schema, as a DDL string: 'a INT'\n   * @param fromPhysicalNames Physical column/field names for the initial schema: assigning\n   *                          physical names that are different than the logical names in\n   *                          `fromDDL` allows simulating column mapping operations: DROP, RENAME.\n   * @param toDDL             Updated schema, as a DDL string.\n   * @param toPhysicalNames   Physical column/field names for the updated schema\n   * @param expectedResult    Expected result, either failure or success. In case of failure, a\n   *                          check to apply on the returned expression can be passed.\n   * @param unblock           SQL confs to unblock the schema change. Each entry is a set of SQL\n   *                          confs which together allow the schema change to be unblocked. There\n   *                          can be multiple such sets, e.g.\n   *                          [[allowSourceColumnDrop], [allowSourceColumnRenameAndDrop]] as both\n   *                          allow dropping columns independently.\n   * @param confs             Additional SQL confs to set when running the test.\n   */\n  private def testSchemaChange(\n      name: String,\n      fromDDL: String,\n      fromPhysicalNames: Map[Seq[String], String] = Map.empty,\n      toDDL: String,\n      toPhysicalNames: Map[Seq[String], String] = Map.empty,\n      expectedResult: ExpectedResult[Nothing],\n      unblock: Seq[Seq[String]] = Seq.empty,\n      confs: Seq[(String, String)] = Seq.empty): Unit =\n    test(s\"$name\") {\n      def validate(parameters: Map[String, String]): Unit =\n        DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked(\n          spark,\n          parameters,\n          metadataPath = \"sourceMetadataPath\",\n          currentSchema = persistedMetadata(toDDL, toPhysicalNames),\n          previousSchema = persistedMetadata(fromDDL, fromPhysicalNames)\n        )\n      withSQLConf(confs: _*) {\n        expectedResult match {\n          case ExpectedResult.Success(_) => validate(parameters = Map.empty)\n          case ExpectedResult.Failure(checkError) =>\n            // Run first without setting any configuration to unblock and check that the validation\n            // fails => column dropped, renamed or with changed type.\n            val ex = intercept[DeltaThrowable] {\n              validate(parameters = Map.empty)\n            }\n            checkError(ex)\n\n            // Verify that we can unblock using SQL confs\n            for (u <- unblock) {\n              withSQLConfUnblockedChanges(u) {\n                validate(parameters = Map.empty)\n              }\n            }\n            // Verify that we can unblock using dataframe reader options.\n            for (u <- unblock) {\n              val parameters = u.flatMap {\n                case \"allowSourceColumnRenameAndDrop\" =>\n                  Seq(DeltaOptions.ALLOW_SOURCE_COLUMN_RENAME -> \"always\",\n                    DeltaOptions.ALLOW_SOURCE_COLUMN_DROP -> \"always\")\n                case option => Seq(option -> \"always\")\n              }\n              validate(parameters.toMap)\n            }\n        }\n      }\n    }\n\n  testSchemaChange(\n    name = \"no schema change, use logical names\",\n    fromDDL = \"a int\",\n    toDDL = \"a int\",\n    expectedResult = ExpectedResult.Success()\n  )\n\n  testSchemaChange(\n    name = \"no schema change, use physical names\",\n    fromDDL = \"a int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\"),\n    toDDL = \"a int\",\n    toPhysicalNames = Map(Seq(\"a\") -> \"x\"),\n    expectedResult = ExpectedResult.Success()\n  )\n\n  testSchemaChange(\n    name = \"schema overwrite, different column name\",\n    fromDDL = \"a int\",\n    toDDL = \"b string\",\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"schema overwrite, same column name, non-widening type change\",\n    fromDDL = \"a int\",\n    toDDL = \"a string\",\n    toPhysicalNames = Map(Seq(\"a\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"schema overwrite, same column name, widening type change\",\n    fromDDL = \"a byte\",\n    toDDL = \"a int\",\n    toPhysicalNames = Map(Seq(\"a\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  /////////////////\n  // Rename column\n  /////////////////\n  testSchemaChange(\n    name = \"column rename, use logical names\",\n    fromDDL = \"a int\",\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"column rename, use physical names\",\n    fromDDL = \"a int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\"),\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"x\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"column rename with widening type change\",\n    fromDDL = \"a byte\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\"),\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"x\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"column rename with non-widening type change\",\n    fromDDL = \"a int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\"),\n    toDDL = \"b string\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"x\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"swap columns\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"b int, a int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\", Seq(\"a\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"swap columns with widening type change\",\n    fromDDL = \"a byte, b byte\",\n    toDDL = \"b byte, a int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\", Seq(\"a\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"swap columns with non-widening type change\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"b int, a string\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\", Seq(\"a\") -> \"b\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"swap columns with widening and non-widening type change\",\n    fromDDL = \"a byte, b int\",\n    toDDL = \"b int, a string\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\", Seq(\"a\") -> \"b\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  /////////////////\n  // Drop column\n  /////////////////\n  testSchemaChange(\n    name = \"drop column, use logical names\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"b int\",\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, use physical names\",\n    fromDDL = \"a int, b int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\", Seq(\"b\") -> \"y\"),\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"y\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column with widening type change\",\n    fromDDL = \"a byte, b byte\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\", Seq(\"b\") -> \"y\"),\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"y\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column with non-widening type change\",\n    fromDDL = \"a int, b int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"x\", Seq(\"b\") -> \"y\"),\n    toDDL = \"b string\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"y\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"drop column, swapped physical names\",\n    fromDDL = \"a int, b int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"b\", Seq(\"b\") -> \"a\"),\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, swapped physical names with widening type change\",\n    fromDDL = \"a byte, b byte\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"b\", Seq(\"b\") -> \"a\"),\n    toDDL = \"b int\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, swapped physical names with non-widening type change\",\n    fromDDL = \"a int, b int\",\n    fromPhysicalNames = Map(Seq(\"a\") -> \"b\", Seq(\"b\") -> \"a\"),\n    toDDL = \"b float\",\n    toPhysicalNames = Map(Seq(\"b\") -> \"a\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  //////////////////////////\n  // Drop and rename column\n  //////////////////////////\n  testSchemaChange(\n    name = \"drop column, rename to other column name\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"c int\",\n    toPhysicalNames = Map(Seq(\"c\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME AND DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnDrop\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, rename to other column name with widening type change\",\n    fromDDL = \"a byte, b byte\",\n    toDDL = \"c int\",\n    toPhysicalNames = Map(Seq(\"c\") -> \"b\"),\n    // We don't block the type change itself here as the column is also renamed\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME, DROP AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, rename to other column name with non-widening type change\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"c string\",\n    toPhysicalNames = Map(Seq(\"c\") -> \"b\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"drop column, rename to same column name\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"a int\",\n    toPhysicalNames = Map(Seq(\"a\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME AND DROP COLUMN\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnDrop\"),\n      Seq(\"allowSourceColumnRenameAndDrop\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, rename to same column name with widening type change\",\n    fromDDL = \"a byte, b byte\",\n    toDDL = \"a int\",\n    toPhysicalNames = Map(Seq(\"a\") -> \"b\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME, DROP AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"drop column, rename to same column name with non-widening type change\",\n    fromDDL = \"a int, b int\",\n    toDDL = \"a string\",\n    toPhysicalNames = Map(Seq(\"a\") -> \"b\"),\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  ////////////////\n  // Type changes\n  ////////////////\n  testSchemaChange(\n    name = \"widen single column\",\n    fromDDL = \"a byte\",\n    toDDL = \"a int\",\n    expectedResult = expectTypeWideningBlocked(Seq(\"'a': TINYINT -> INT\")),\n    unblock = Seq(\n      Seq(\"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widening and non-widening type changes\",\n    fromDDL = \"a byte, b int\",\n    toDDL = \"a int, b byte\",\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"widen single column with type widening disabled in Delta source\",\n    fromDDL = \"a byte\",\n    toDDL = \"a int\",\n    expectedResult = expectNonWideningTypeChangeError,\n    confs = Seq(DeltaSQLConf.DELTA_ALLOW_TYPE_WIDENING_STREAMING_SOURCE.key -> \"false\")\n  )\n\n  testSchemaChange(\n    name = \"widen single column with type change check disabled\",\n    fromDDL = \"a byte\",\n    toDDL = \"a int\",\n    expectedResult = ExpectedResult.Success(),\n    confs = Seq(DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK.key -> \"true\")\n  )\n\n  testSchemaChange(\n    name = \"widen single column with both type widening and type change check disabled\",\n    fromDDL = \"a byte\",\n    toDDL = \"a int\",\n    expectedResult = ExpectedResult.Success(),\n    confs = Seq(\n      DeltaSQLConf.DELTA_ALLOW_TYPE_WIDENING_STREAMING_SOURCE.key -> \"false\",\n      DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK.key -> \"true\"\n    )\n  )\n\n  testSchemaChange(\n    name = \"narrow single column\",\n    fromDDL = \"a long\",\n    toDDL = \"a int\",\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"narrow single column with type change check disabled\",\n    fromDDL = \"a long\",\n    toDDL = \"a int\",\n    expectedResult = ExpectedResult.Success(),\n    confs = Seq(DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK.key -> \"true\")\n  )\n\n  testSchemaChange(\n    name = \"change to nullable\",\n    fromDDL = \"a int not null\",\n    toDDL = \"a int\",\n    expectedResult = ExpectedResult.Success()\n  )\n  testSchemaChange(\n    name = \"change to non-nullable\",\n    fromDDL = \"a int\",\n    toDDL = \"a int not null\",\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"widen and change to nullable\",\n    fromDDL = \"a byte not null\",\n    toDDL = \"a int\",\n    expectedResult = expectTypeWideningBlocked(Seq(\"'a': TINYINT -> INT\")),\n    unblock = Seq(\n      Seq(\"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widen change to non-nullable\",\n    fromDDL = \"a byte\",\n    toDDL = \"a int not null\",\n    expectedResult = expectNonWideningTypeChangeError\n  )\n\n  testSchemaChange(\n    name = \"widen map\",\n    fromDDL = \"a map<byte, short>\",\n    toDDL = \"a map<short, int>\",\n    expectedResult = expectTypeWideningBlocked(Seq(\n      \"'a.key': TINYINT -> SMALLINT\",\n      \"'a.value': SMALLINT -> INT\"\n    )),\n    unblock = Seq(\n      Seq(\"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widen array\",\n    fromDDL = \"a array<byte>\",\n    toDDL = \"a array<short>\",\n    expectedResult = expectTypeWideningBlocked(Seq(\"'a.element': TINYINT -> SMALLINT\")),\n    unblock = Seq(\n      Seq(\"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widen struct\",\n    fromDDL = \"a struct<x: byte>\",\n    toDDL = \"a struct<x: short>\",\n    expectedResult = expectTypeWideningBlocked(Seq(\"'a.x': TINYINT -> SMALLINT\")),\n    unblock = Seq(\n      Seq(\"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widen struct and struct field rename\",\n    fromDDL = \"a struct<x: byte, y: int>\",\n    toDDL = \"a struct<x: short, z: int>\",\n    toPhysicalNames = Map(Seq(\"a\", \"z\") -> \"y\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widen struct and struct field drop\",\n    fromDDL = \"a struct<x: byte, y: int>\",\n    toDDL = \"a struct<x: short>\",\n    expectedResult = expectColumnMappingChangeBlocked(\"DROP AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  testSchemaChange(\n    name = \"widen struct and struct field rename and drop\",\n    fromDDL = \"a struct<x: byte, y: int, z: int>\",\n    toDDL = \"a struct<x: short, w: int>\",\n    toPhysicalNames = Map(Seq(\"a\", \"w\") -> \"y\"),\n    expectedResult = expectColumnMappingChangeBlocked(\"RENAME, DROP AND TYPE WIDENING\"),\n    unblock = Seq(\n      Seq(\"allowSourceColumnRename\", \"allowSourceColumnDrop\", \"allowSourceColumnTypeChange\"),\n      Seq(\"allowSourceColumnRenameAndDrop\", \"allowSourceColumnTypeChange\")\n    )\n  )\n\n  test(\"combining individual SQL confs to unblock is supported\") {\n    withSQLConfUnblockedChanges(Seq(\"allowSourceColumnRename\", \"allowSourceColumnDrop\")) {\n      DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked(\n        spark,\n        parameters = Map.empty,\n        metadataPath = \"sourceMetadataPath\",\n        currentSchema = persistedMetadata(\"a int\", Map(Seq(\"a\") -> \"b\")),\n        previousSchema = persistedMetadata(\"a int, b int\", Map.empty)\n      )\n    }\n  }\n\n  test(\"combining SQL confs and reader options to unblock is supported\") {\n    withSQLConfUnblockedChanges(Seq(\"allowSourceColumnRename\")) {\n      DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked(\n        spark,\n        parameters = Map(\"allowSourceColumnDrop\" -> \"always\"),\n        metadataPath = \"sourceMetadataPath\",\n        currentSchema = persistedMetadata(\"a int\", Map(Seq(\"a\") -> \"b\")),\n        previousSchema = persistedMetadata(\"a int, b int\", Map.empty)\n      )\n    }\n  }\n\n  test(\"unblocking column drop for specific version with reader option is supported\") {\n    DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked(\n      spark,\n      parameters = Map(\"allowSourceColumnDrop\" -> \"0\"),\n      metadataPath = \"sourceMetadataPath\",\n      currentSchema = persistedMetadata(\"a int\", Map.empty),\n      previousSchema = persistedMetadata(\"a int, b int\", Map.empty)\n    )\n  }\n\n  test(\"unblocking column rename for specific version with reader option is supported\") {\n    DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked(\n      spark,\n      parameters = Map(\"allowSourceColumnRename\" -> \"0\"),\n      metadataPath = \"sourceMetadataPath\",\n      currentSchema = persistedMetadata(\"b int\", Map(Seq(\"b\") -> \"a\")),\n      previousSchema = persistedMetadata(\"a int\", Map.empty)\n    )\n  }\n\n  test(\"unblocking column type change for specific version with reader option is supported\") {\n    DeltaSourceMetadataEvolutionSupport.validateIfSchemaChangeCanBeUnblocked(\n      spark,\n      parameters = Map(\"allowSourceColumnTypeChange\" -> \"0\"),\n      metadataPath = \"sourceMetadataPath\",\n      currentSchema = persistedMetadata(\"a int\", Map.empty),\n      previousSchema = persistedMetadata(\"a byte\", Map.empty)\n    )\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/stats/DataSkippingDeltaConstructDataFiltersSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.delta.DeltaLog\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.expressions._\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.types.StringType\n\nclass DataSkippingDeltaConstructDataFiltersSuite\n    extends QueryTest with SharedSparkSession with DeltaSQLCommandTest {\n  test(\"Verify constructDataFilters doesn't hang for expressions with Literal operands.\") {\n    val snapshot = DeltaLog.forTable(spark, \"dummy_path\").update()\n    val dataFilterBuilder = new snapshot.DataFiltersBuilder(\n      spark, DeltaDataSkippingType.dataSkippingOnlyV1)\n\n    val literal = Literal.create(\"foo\", StringType)\n    Seq(\n      EqualTo(literal, literal),\n      Not(EqualTo(literal, literal)),\n      EqualNullSafe(literal, literal),\n      Not(EqualNullSafe(literal, literal)),\n      LessThan(literal, literal),\n      LessThanOrEqual(literal, literal),\n      GreaterThan(literal, literal),\n      GreaterThanOrEqual(literal, literal),\n      Not(GreaterThanOrEqual(literal, literal)),\n      In(literal, Seq(literal)),\n      IsNull(literal),\n      IsNotNull(literal),\n      And(EqualTo(literal, literal), LessThan(literal, literal))\n    ).foreach { expression =>\n      assert(dataFilterBuilder.constructDataFilters(expression, isNullExpansionDepth = 0).isEmpty)\n    }\n  }\n\n  test(\"Test when the query contains EqualTo(Literal, Literal) in the filter.\") {\n    setup {\n      sql(\n        \"\"\"\n          |explain\n          |select\n          | *\n          |from\n          |  view1 c\n          |  join view2 cv on c.type=cv.type and c.key=cv.key\n          |  join tbl3 b on cv.name=b.name\n          |where\n          |  (\n          |       (b.name=\"name1\" and c.type=\"foo\")\n          |       or\n          |       (b.name=\"name2\" and c.type=\"bar\")\n          |  )\n          |\"\"\".stripMargin)\n    }\n  }\n\n  test(\"Verify areAllLeavesLiteral can't be recursively called b/c it can cause stack overflow\") {\n    val snapshot = DeltaLog.forTable(spark, \"dummy_path\").update()\n    val dataFilterBuilder = new snapshot.DataFiltersBuilder(\n      spark, DeltaDataSkippingType.dataSkippingOnlyV1)\n\n    // Create a deeply nested Alias expression: Alias(Alias(Alias(...), \"name3\"), \"name2\"), \"name1\")\n    val depth = 100000 // Deep enough to cause stack overflow with default JVM stack size\n    var nestedExpr: Expression = Literal.create(\"foo\", StringType)\n    for (i <- 1 to depth) {\n      nestedExpr = Alias(nestedExpr, s\"name$i\")()\n    }\n\n    assert(dataFilterBuilder.areAllLeavesLiteral(nestedExpr))\n\n    // This should cause a StackOverflowError when areAllLeavesLiteral is called recursively\n    def areAllLeavesLiteral(e: Expression): Boolean = e match {\n      case _: Literal => true\n      case _ if e.children.nonEmpty => e.children.forall(areAllLeavesLiteral)\n      case _ => false\n    }\n\n    intercept[StackOverflowError] {\n      areAllLeavesLiteral(nestedExpr)\n    }\n  }\n\n  private def setup(f: => Unit) {\n    withTable(\"tbl1_foo\", \"tbl1_bar\", \"tbl2_foo\", \"tbl2_bar\", \"tbl3\") {\n      Seq(\"foo\", \"bar\").foreach { tableType =>\n        sql(s\"CREATE TABLE tbl1_$tableType (key STRING) USING delta\")\n        sql(s\"CREATE TABLE tbl2_$tableType (key STRING, name STRING) USING delta\")\n      }\n      sql(\"CREATE TABLE tbl3 (name STRING) USING delta\")\n\n      withView(\"view1\", \"view2\") {\n        sql(\n          s\"\"\"\n             |CREATE VIEW view1 (type, key)\n             |AS (\n             |    select 'foo' as type, * from tbl1_foo\n             |    union all\n             |    select 'bar' as type, * from tbl1_bar\n             |)\n             |\"\"\".stripMargin\n        )\n\n        sql(\n          s\"\"\"\n             |CREATE VIEW view2 (type, key, name)\n             |AS (\n             |    select 'foo' as type, * from tbl2_foo\n             |    union all\n             |    select 'bar' as type, * from tbl2_bar\n             |)\n             |\"\"\".stripMargin\n        )\n\n        f\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/stats/DataSkippingDeltaTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.spark.sql.delta.coordinatedcommits.CatalogOwnedTestBaseSuite\nimport org.apache.spark.sql.delta.metering.ScanReport\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.test.ScanReportHelper\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.GivenWhenThen\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql._\nimport org.apache.spark.sql.catalyst.QueryPlanningTracker\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal, PredicateHelper}\nimport org.apache.spark.sql.catalyst.plans.logical.Filter\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\ntrait DataSkippingDeltaTestsBase extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLCommandTest\n    with DataSkippingDeltaTestsUtils\n    with GivenWhenThen\n    with ScanReportHelper\n    with CatalogOwnedTestBaseSuite\n    with DeltaSQLTestUtils {\n\n  val defaultNumIndexedCols = DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.fromString(\n    DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.defaultValue)\n\n  import testImplicits._\n\n  protected def checkpointAndCreateNewLogIfNecessary(log: DeltaLog): DeltaLog = log\n\n  protected val tableSchemaOnlyTag = org.scalatest.Tag(\"StatsCollectionWithTableSchemaOnly\")\n\n  /**\n   * Test stats collection using both the table schema and DataFrame schema (if applicable)\n   * TODO(lin): remove this after we remove the DELTA_COLLECT_STATS_USING_TABLE_SCHEMA flag\n   */\n  protected override def test(testName: String, testTags: org.scalatest.Tag*)\n                             (testFun: => Any)\n                             (implicit pos: org.scalactic.source.Position): Unit = {\n    super.test(testName, testTags : _*)(testFun)(pos)\n    if (!testTags.contains(tableSchemaOnlyTag)) {\n      super.test(testName + \" - old behavior with DataFrame schema\", testTags: _*) {\n        withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS_USING_TABLE_SCHEMA.key -> \"false\") {\n          testFun\n        }\n      }\n    }\n  }\n\n  testSkipping(\n    \"top level, single 1\",\n    \"\"\"{\"a\": 1}\"\"\",\n    hits = Seq(\n      \"True\", // trivial base case\n      \"a = 1\",\n      \"a <=> 1\",\n      \"a >= 1\",\n      \"a <= 1\",\n      \"a <= 2\",\n      \"a >= 0\",\n      \"1 = a\",\n      \"1 <=> a\",\n      \"1 <= a\",\n      \"1 >= a\",\n      \"2 >= a\",\n      \"0 <= a\",\n      \"NOT a <=> 2\"\n    ),\n    misses = Seq(\n      \"NOT a = 1\",\n      \"NOT a <=> 1\",\n      \"a = 2\",\n      \"a <=> 2\",\n      \"a != 1\",\n      \"2 = a\",\n      \"2 <=> a\",\n      \"1 != a\",\n      \"a > 1\",\n      \"a < 1\",\n      \"a >= 2\",\n      \"a <= 0\",\n      \"1 < a\",\n      \"1 > a\",\n      \"2 <= a\",\n      \"0 >= a\"\n    )\n  )\n\n  testSkipping(\n    \"nested, single 1\",\n    \"\"\"{\"a\": {\"b\": 1}}\"\"\",\n    hits = Seq(\n      \"a.b = 1\",\n      \"a.b >= 1\",\n      \"a.b <= 1\",\n      \"a.b <= 2\",\n      \"a.b >= 0\"\n    ),\n    misses = Seq(\n      \"a.b = 2\",\n      \"a.b > 1\",\n      \"a.b < 1\"\n    )\n  )\n\n  testSkipping(\n    \"double nested, single 1\",\n    \"\"\"{\"a\": {\"b\": {\"c\": 1}}}\"\"\",\n    hits = Seq(\n      \"a.b.c = 1\",\n      \"a.b.c >= 1\",\n      \"a.b.c <= 1\",\n      \"a.b.c <= 2\",\n      \"a.b.c >= 0\"\n    ),\n    misses = Seq(\n      \"a.b.c = 2\",\n      \"a.b.c > 1\",\n      \"a.b.c < 1\"\n    )\n  )\n\n  private def longString(str: String) = str * 1000\n\n  testSkipping(\n    \"long strings - long min\",\n    s\"\"\"\n       {\"a\": '${longString(\"A\")}'}\n       {\"a\": 'B'}\n       {\"a\": 'C'}\n     \"\"\",\n    hits = Seq(\n      \"a like 'A%'\",\n      s\"a = '${longString(\"A\")}'\",\n      \"a > 'BA'\",\n      \"a < 'AB'\"\n    ),\n    misses = Seq(\n      \"a < 'AA'\",\n      \"a > 'CD'\"\n    )\n  )\n\n  testSkipping(\n    \"long strings - long max\",\n    s\"\"\"\n       {\"a\": 'A'}\n       {\"a\": 'B'}\n       {\"a\": '${longString(\"C\")}'}\n     \"\"\",\n    hits = Seq(\n      \"a like 'A%'\",\n      \"a like 'C%'\",\n      s\"a = '${longString(\"C\")}'\",\n      \"a > 'BA'\",\n      \"a < 'AB'\",\n      \"a > 'CC'\"\n    ),\n    misses = Seq(\n      \"a >= 'D'\",\n      \"a > 'CD'\"\n    )\n  )\n\n  testSkipping(\n    \"starts with\",\n    \"\"\"\n      {\"a\": 'apple'}\n      {\"a\": 'microsoft'}\n    \"\"\",\n    hits = Seq(\n      \"a like 'a%'\",\n      \"a like 'ap%'\",\n      \"a like 'm%'\",\n      \"a like 'mic%'\",\n      \"a like '%'\"\n    ),\n    misses = Seq(\n      \"a like 'xyz%'\"\n    )\n  )\n\n  testSkipping(\n    \"starts with, nested\",\n    \"\"\"\n      {\"a\":{\"b\": 'apple'}}\n      {\"a\":{\"b\": 'microsoft'}}\n    \"\"\",\n    hits = Seq(\n      \"a.b like 'a%'\",\n      \"a.b like 'ap%'\",\n      \"a.b like 'm%'\",\n      \"a.b like 'mic%'\",\n      \"a.b like '%'\"\n    ),\n    misses = Seq(\n      \"a.b like 'xyz%'\"\n    )\n  )\n\n  testSkipping(\n    \"and statements - simple\",\n    \"\"\"\n      {\"a\": 1}\n      {\"a\": 2}\n    \"\"\",\n    hits = Seq(\n      \"a > 0 AND a < 3\",\n      \"a <= 1 AND a > -1\"\n    ),\n    misses = Seq(\n      \"a < 0 AND a > -2\"\n    )\n  )\n\n  testSkipping(\n    \"and statements - two fields\",\n    \"\"\"\n      {\"a\": 1, \"b\": \"2017-09-01\"}\n      {\"a\": 2, \"b\": \"2017-08-31\"}\n    \"\"\",\n    hits = Seq(\n      \"a > 0 AND b = '2017-09-01'\",\n      \"a = 2 AND b >= '2017-08-30'\",\n      \"a >= 2 AND b like '2017-08-%'\"\n    ),\n    misses = Seq(\n      \"a > 0 AND b like '2016-%'\"\n    )\n  )\n\n  // One side of AND by itself still has pruning power.\n  testSkipping(\n    \"and statements - one side unsupported\",\n    \"\"\"\n      {\"a\": 10, \"b\": 10}\n      {\"a\": 20: \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      \"a % 100 < 10 AND b % 100 > 20\"\n    ),\n    misses = Seq(\n      \"a < 10 AND b % 100 > 20\",\n      \"a % 100 < 10 AND b > 20\"\n    )\n  )\n\n  testSkipping(\n    \"or statements - simple\",\n    \"\"\"\n      {\"a\": 1}\n      {\"a\": 2}\n    \"\"\",\n    hits = Seq(\n      \"a > 0 or a < -3\",\n      \"a >= 2 or a < -1\"\n    ),\n    misses = Seq(\n      \"a > 5 or a < -2\"\n    )\n  )\n\n  testSkipping(\n    \"or statements - two fields\",\n    \"\"\"\n      {\"a\": 1, \"b\": \"2017-09-01\"}\n      {\"a\": 2, \"b\": \"2017-08-31\"}\n    \"\"\",\n    hits = Seq(\n      \"a < 0 or b = '2017-09-01'\",\n      \"a = 2 or b < '2017-08-30'\",\n      \"a < 2 or b like '2017-08-%'\",\n      \"a >= 2 or b like '2016-08-%'\"\n    ),\n    misses = Seq(\n      \"a < 0 or b like '2016-%'\"\n    )\n  )\n\n  // One side of OR by itself isn't powerful enough to prune any files.\n  testSkipping(\n    \"or statements - one side unsupported\",\n    \"\"\"\n      {\"a\": 10, \"b\": 10}\n      {\"a\": 20: \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      \"a % 100 < 10 OR b > 20\",\n      \"a < 10 OR b % 100 > 20\"\n    ),\n    misses = Seq(\n      \"a < 10 OR b > 20\"\n    )\n  )\n\n  testSkipping(\n    \"not statements - simple\",\n    \"\"\"\n      {\"a\": 1}\n      {\"a\": 2}\n    \"\"\",\n    hits = Seq(\n      \"not a < 0\"\n    ),\n    misses = Seq(\n      \"not a > 0\"\n    )\n  )\n\n  // NOT(AND(a, b)) === OR(NOT(a), NOT(b)) ==> One side by itself cannot prune.\n  testSkipping(\n    \"not statements - and\",\n    \"\"\"\n      {\"a\": 10, \"b\": 10}\n      {\"a\": 20: \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      \"NOT(a % 100 >= 10 AND b % 100 <= 20)\",\n      \"NOT(a >= 10 AND b % 100 <= 20)\",\n      \"NOT(a % 100 >= 10 AND b <= 20)\"\n    ),\n    misses = Seq(\n      \"NOT(a >= 10 AND b <= 20)\"\n    )\n  )\n\n  // NOT(OR(a, b)) === AND(NOT(a), NOT(b)) => One side by itself is enough to prune.\n  testSkipping(\n    \"not statements - or\",\n    \"\"\"\n      {\"a\": 1, \"b\": 10}\n      {\"a\": 2, \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      \"NOT(a < 1 OR b > 20)\",\n      \"NOT(a % 100 >= 1 OR b % 100 <= 20)\"\n    ),\n    misses = Seq(\n      \"NOT(a >= 1 OR b <= 20)\",\n      \"NOT(a % 100 >= 1 OR b <= 20)\",\n      \"NOT(a >= 1 OR b % 100 <= 20)\"\n    )\n  )\n\n  // If a column does not have stats, it does not participate in data skipping, which disqualifies\n  // that leg of whatever conjunct it was part of.\n  testSkipping(\n    \"missing stats columns\",\n    \"\"\"\n      {\"a\": 1, \"b\": 10}\n      {\"a\": 2, \"b\": 20}\n    \"\"\",\n    hits = Seq(\n      \"b < 10\",  // disqualified\n      \"a < 1 OR b < 10\",  // a disqualified by b (same conjunct)\n      \"a < 1 OR (a >= 1 AND b < 10)\"  // ==> a < 1 OR a >=1 ==> TRUE\n    ),\n    misses = Seq(\n      \"a < 1 AND b < 10\",  // ==> a < 1 ==> FALSE\n      \"a < 1 OR (a > 10 AND b < 10)\"  // ==> a < 1 OR a > 10 ==> FALSE\n    ),\n    indexedCols = 1\n  )\n\n  private def generateJsonData(numCols: Int): String = {\n    val fields = (0 until numCols).map(i => s\"\"\"\"col${\"%02d\".format(i)}\":$i\"\"\".stripMargin)\n\n    \"{\" + fields.mkString(\",\") + \"}\"\n  }\n\n  testSkipping(\n    \"more columns than indexed\",\n    generateJsonData(defaultNumIndexedCols + 1),\n    hits = Seq(\n      \"col00 = 0\",\n      s\"col$defaultNumIndexedCols = $defaultNumIndexedCols\",\n      s\"col$defaultNumIndexedCols = -1\"\n    ),\n    misses = Seq(\n      \"col00 = 1\"\n    )\n  )\n\n  testSkipping(\n    \"nested schema - # indexed column = 3\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      \"a = 1\",\n      \"b.c.d = 2\",\n      \"b.c.e = 3\",\n      // below matches due to missing stats\n      \"b.c.f.g < 0\",\n      \"b.c.f.i < 0\",\n      \"b.l < 0\"),\n    misses = Seq(\n      \"a < 0\",\n      \"b.c.d < 0\",\n      \"b.c.e < 0\"),\n    indexedCols = 3\n  )\n\n  testSkipping(\n    \"nested schema - # indexed column = 6\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      \"b.c.f.i = 6\",\n      // below matches are due to missing stats\n      \"b.c.j < 0\",\n      \"b.c.k < 0\",\n      \"b.l < 0\"),\n    misses = Seq(\n      \"a < 0\",\n      \"b.c.f.i < 0\"\n    ),\n    indexedCols = 6\n  )\n\n  testSkipping(\n    \"nested schema - # indexed column = 9\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      \"b.c.d = 2\",\n      \"b.c.f.i = 6\",\n      \"b.l = 9\",\n      // below matches are due to missing stats\n      \"m < 0\"),\n    misses = Seq(\n      \"b.l < 0\",\n      \"b.c.f.i < 0\"\n    ),\n    indexedCols = 9\n  )\n\n  testSkipping(\n    \"nested schema - # indexed column = 0\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      // all included due to missing stats\n      \"a < 0\",\n      \"b.c.d < 0\",\n      \"b.c.f.i < 0\",\n      \"b.l < 0\",\n      \"m < 0\"),\n    misses = Seq(),\n    indexedCols = 0\n  )\n\n  testSkipping(\n    \"indexed column names - empty list disables stats collection\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": 2,\n      \"c\": 3,\n      \"d\": 4\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      \"a < 0\",\n      \"b < 0\",\n      \"c < 0\",\n      \"d < 0\"\n    ),\n    misses = Seq(),\n    indexedCols = 3,\n    deltaStatsColNamesOpt = Some(\" \")\n  )\n\n  testSkipping(\n    \"indexed column names - naming a nested column indexes all leaf fields of that column\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      // these all have missing stats\n      \"a < 0\",\n      \"b.l < 0\",\n      \"m < 0\"\n    ),\n    misses = Seq(\n      \"b.c.d < 0\",\n      \"b.c.e < 0\",\n      \"b.c.f.g < 0\",\n      \"b.c.f.h < 0\",\n      \"b.c.f.i < 0\",\n      \"b.c.j < 0\",\n      \"b.c.k < 0\"\n    ),\n    indexedCols = 3,\n    deltaStatsColNamesOpt = Some(\"b.c\")\n  )\n\n  testSkipping(\n    \"indexed column names - naming a nested column allows nested complex types\",\n    \"\"\"{\n      \"a\": {\n        \"b\": [1, 2, 3],\n        \"c\": [4, 5, 6],\n        \"d\": 7,\n        \"e\": 8,\n        \"f\": {\n          \"g\": 9\n        }\n      },\n      \"i\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      \"i < 0\",\n      \"a.d > 6\",\n      \"a.f.g < 10\"\n    ),\n    misses = Seq(\n      \"a.d < 0\",\n      \"a.e < 0\",\n      \"a.f.g < 0\"\n    ),\n    deltaStatsColNamesOpt = Some(\"a\")\n  )\n\n  testSkipping(\n    \"indexed column names - index only a subset of leaf columns\",\n    \"\"\"{\n      \"a\": 1,\n      \"b\": {\n        \"c\": {\n          \"d\": 2,\n          \"e\": 3,\n          \"f\": {\n            \"g\": 4,\n            \"h\": 5,\n            \"i\": 6\n          },\n          \"j\": 7,\n          \"k\": 8\n        },\n        \"l\": 9\n      },\n      \"m\": 10\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      // these all have missing stats\n      \"a < 0\",\n      \"b.c.d < 0\",\n      \"b.c.f.g < 0\",\n      \"b.c.f.i < 0\",\n      \"b.c.j < 0\",\n      \"m < 0\"\n    ),\n    misses = Seq(\n      \"b.c.e < 0\",\n      \"b.c.f.h < 0\",\n      \"b.c.k < 0\",\n      \"b.l < 0\"\n    ),\n    indexedCols = 3,\n    deltaStatsColNamesOpt = Some(\"b.c.e, b.c.f.h, b.c.k, b.l\")\n  )\n\n  testSkipping(\n    \"indexed column names - backtick escapes work as expected\",\n    \"\"\"{\n      \"a\": 1,\n      \"b.c\": 2,\n      \"b\": {\n        \"c\": 3,\n        \"d\": 4\n      }\n    }\"\"\".replace(\"\\n\", \"\"),\n    hits = Seq(\n      \"b.c < 0\"\n    ),\n    misses = Seq(\n      \"a < 0\",\n      \"`b.c` < 0\",\n      \"b.d < 0\"\n    ),\n    indexedCols = 3,\n    deltaStatsColNamesOpt = Some(\"`a`, `b.c`, `b`.`d`\")\n  )\n\n  testSkipping(\n    \"boolean comparisons\",\n    \"\"\"{\"a\": false}\"\"\",\n    hits = Seq(\n      \"!a\",\n      \"NOT a\",\n      \"a\", // there is no skipping for BooleanValues\n      \"a = false\",\n      \"NOT a = false\",\n      \"a > true\",\n      \"a <= false\",\n      \"true = a\",\n      \"true < a\",\n      \"false = a or a\"\n    ),\n    misses = Seq()\n  )\n\n  // Data skipping by stats should still work even when the only data in file is null, in spite of\n  // the NULL min/max stats that result -- this is different to having no stats at all.\n  testSkipping(\n    \"nulls - only null in file\",\n    \"\"\"\n      {\"a\": null }\n    \"\"\",\n    schema = new StructType().add(new StructField(\"a\", IntegerType)),\n    hits = Seq(\n      \"a IS NULL\",\n      \"a = NULL\",  // Ideally this should not hit as it is always FALSE, but its correct to not skip\n      \"NOT a = NULL\", // Same as previous case\n      \"a <=> NULL\", // This is optimized to `IsNull(a)` by NullPropagation\n      \"TRUE\",\n      \"FALSE\",     // Ideally this should not hit, but its correct to not skip\n      \"NULL AND a = 1\", // This is optimized to FALSE by ReplaceNullWithFalse, so it's same as above\n      \"NOT a <=> 1\",\n      \"(a > 1) IS NULL\", // This pushes down the IS NULL to both sides of GreaterThan.\n      \"(a > 1 AND a > 0) IS NULL\", // Pushdown of IS NULL on AND.\n      \"(a > 1 OR a < 0) IS NULL\" // Pushdown of IS NULL on OR.\n    ),\n    misses = Seq(\n      // stats tell us a is always NULL, so any predicate that requires non-NULL a should skip\n      \"a IS NOT NULL\",\n      \"NOT a <=> NULL\", // This is optimized to `IsNotNull(a)`\n      \"a = 1\",\n      \"NOT a = 1\",\n      \"a > 1\",\n      \"a < 1\",\n      \"a <> 1\",\n      \"a <=> 1\",\n      \"NOT ((a > 1) IS NULL)\"\n    )\n  )\n\n  testSkipping(\n    \"nulls - only non-null in file\",\n    \"\"\"\n      {\"a\": 1, \"b\": 2}\n    \"\"\",\n    schema = new StructType()\n      .add(new StructField(\"a\", IntegerType))\n      .add(new StructField(\"b\", IntegerType)),\n    hits = Seq(),\n    misses = Seq(\n      \"(a > 0 AND b > 1) IS NULL\",\n      \"(a > 0 OR b > 1) IS NULL\"\n    )\n  )\n\n  testSkipping(\n    \"nulls - only non-null in file with enhanced pushdown disabled\",\n    \"\"\"\n      {\"a\": 1, \"b\": 2}\n    \"\"\",\n    schema = new StructType()\n      .add(new StructField(\"a\", IntegerType))\n      .add(new StructField(\"b\", IntegerType)),\n    hits = Seq(\n      \"(a > 0 AND b > 1) IS NULL\",\n      \"(a > 0 OR b > 1) IS NULL\"\n    ),\n    misses = Seq.empty,\n    sqlConfs = Seq((DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_ENABLED.key, \"false\"))\n  )\n\n  testSkipping(\n    \"nulls - null + not-null in same file\",\n    \"\"\"\n      {\"a\": null }\n      {\"a\": 1 }\n    \"\"\",\n    schema = new StructType().add(new StructField(\"a\", IntegerType)),\n    hits = Seq(\n      \"a IS NULL\",\n      \"a IS NOT NULL\",\n      \"a = NULL\", // Ideally this should not hit as it is always FALSE, but its correct to not skip\n      \"NOT a = NULL\", // Same as previous case\n      \"a <=> NULL\", // This is optimized to `IsNull(a)` by NullPropagation\n      \"NOT a <=> NULL\", // This is optimized to `IsNotNull(a)`\n      \"a = 1\",\n      \"a <=> 1\",\n      \"TRUE\",\n      \"FALSE\",    // Ideally this should not hit, but its correct to not skip\n      \"NULL AND a = 1\", // This is optimized to FALSE by ReplaceNullWithFalse, so it's same as above\n      \"NOT a <=> 1\",\n      \"(a > 0) IS NULL\",\n      \"(a < 0) IS NULL\",\n      \"(a > 1 AND a > 0) IS NULL\", // Pushdown of IS NULL on AND.\n      \"(a > 1 OR a < 0) IS NULL\", // Pushdown of IS NULL on OR.\n      \"NOT ((a > 0) IS NULL)\"\n    ),\n    misses = Seq(\n      \"a <> 1\",\n      \"a > 1\",\n      \"a < 1\",\n      \"NOT a = 1\"\n    )\n  )\n\n  testSkipping(\n    \"nulls - IsNull pushdown on complex expressions\",\n    \"\"\"\n      {\"a\": 1, \"b\": 2}\n    \"\"\",\n    schema = new StructType()\n      .add(new StructField(\"a\", IntegerType))\n      .add(new StructField(\"b\", IntegerType)),\n    hits = Seq(\n      \"(a > 0 OR a == -1 OR a == -2 OR b == -1 OR b == -2 OR b > 10 OR b == 7) IS NULL\"\n    ),\n    misses = Seq(\n      \"(a > 0 OR b > 1) IS NULL\"\n    ),\n    sqlConfs = Seq((DeltaSQLConf.DELTA_DATASKIPPING_ISNULL_PUSHDOWN_EXPRS_MAX_DEPTH.key -> \"1\"))\n  )\n\n  testSkipping(\n    \"nulls - non-nulls only in file\",\n    \"\"\"\n      {\"a\": 1 }\n    \"\"\",\n    schema = new StructType().add(new StructField(\"a\", IntegerType)),\n    hits = Seq(\n      \"NOT ((a > 0) IS NULL)\"\n    ),\n    misses = Seq(\n      \"(a > 0) IS NULL\",\n      \"(a < 0) IS NULL\",\n      \"(a > 1 AND a > 0) IS NULL\", // Pushdown of IS NULL on AND.\n      \"(a > 1 OR a < 0) IS NULL\" // Pushdown of IS NULL on OR.\n    )\n  )\n\n  testSkipping(\n    \"nulls - non-nulls only in file with partial column stats\",\n    \"\"\"\n      {\"a\": 1, \"b\": 2}\n    \"\"\",\n    hits = Seq(\n      \"NOT ((a > 0) IS NULL)\",\n      \"(b > 0) IS NULL\",\n      \"(b < 0) IS NULL\",\n      \"(b > 1 AND a > 0) IS NULL\", // Pushdown of IS NULL on AND.\n      \"(b > 1 OR a < 0) IS NULL\" // Pushdown of IS NULL on OR.\n    ),\n    misses = Seq(\n      \"(a > 0) IS NULL\",\n      \"(a < 0) IS NULL\",\n      \"(a > 1 AND a > 0) IS NULL\", // Pushdown of IS NULL on AND.\n      \"(a > 1 OR a < 0) IS NULL\" // Pushdown of IS NULL on OR.\n    ),\n    indexedCols = 1\n  )\n\n  testSkipping(\n    \"nulls - non-strict null-intolerant predicate returns hits for IS NULL\",\n    \"\"\"\n      {\"a\": [3, 4]}\n    \"\"\",\n    hits = Seq(\n      \"NOT (element_at(a, 3) IS NULL)\",\n      \"element_at(a, 3) IS NULL\"\n    ),\n    misses = Seq.empty\n  )\n\n  test(\"data skipping with missing stats\") {\n    val tempDir = Utils.createTempDir()\n    Seq(1, 2, 3).toDF().write.format(\"delta\").save(tempDir.toString)\n    val log = DeltaLog.forTable(spark, new Path(tempDir.toString))\n    val txn = log.startTransaction()\n    val noStats = txn.filterFiles(Nil).map(_.copy(stats = null))\n    txn.commit(noStats, DeltaOperations.ComputeStats(Nil))\n\n    val df = spark.read.format(\"delta\").load(tempDir.toString)\n    checkAnswer(df.where(\"value > 0\"), Seq(Row(1), Row(2), Row(3)))\n  }\n\n  test(\"data skipping stats before and after optimize\") {\n      assume(!catalogOwnedDefaultCreationEnabledInTests,\n        \"OPTIMIZE is blocked on catalog-managed tables\")\n      val tempDir = Utils.createTempDir()\n      var r = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n      val (numTuples, numFiles) = (10, 2)\n      val data = spark.range(0, numTuples, 1, 2).repartition(numFiles)\n      data.write.format(\"delta\").save(r.dataPath.toString)\n      r = checkpointAndCreateNewLogIfNecessary(r)\n      def rStats: DataFrame =\n        getStatsDf(r, $\"numRecords\", $\"minValues.id\".as(\"id_min\"), $\"maxValues.id\".as(\"id_max\"))\n\n      checkAnswer(rStats, Seq(Row(4, 0, 8), Row(6, 1, 9)))\n      val optimizeDf = sql(s\"OPTIMIZE '$tempDir'\")\n      checkAnswer(rStats, Seq(Row(10, 0, 9)))\n  }\n\n  test(\"number of indexed columns\") {\n    val numTotalCols = defaultNumIndexedCols + 5\n    val path = Utils.createTempDir().getCanonicalPath\n    var r = DeltaLog.forTable(spark, new Path(path))\n    val data = spark.range(10).select(Seq.tabulate(numTotalCols)(i => lit(i) as s\"col$i\"): _*)\n    data.coalesce(1).write.format(\"delta\").save(r.dataPath.toString)\n\n    def checkNumIndexedCol(numIndexedCols: Int): Unit = {\n      if (defaultNumIndexedCols != numTotalCols) {\n        setNumIndexedColumns(r.dataPath.toString, numIndexedCols)\n      }\n      data.coalesce(1).write.format(\"delta\").mode(\"overwrite\").save(r.dataPath.toString)\n      r = checkpointAndCreateNewLogIfNecessary(r)\n\n      if (numIndexedCols == 0) {\n        intercept[AnalysisException] {\n          getStatsDf(r, $\"numRecords\", $\"minValues.col0\").first()\n        }\n      } else if (numIndexedCols < numTotalCols) {\n        checkAnswer(\n          getStatsDf(r, $\"numRecords\", $\"minValues.col${numIndexedCols - 1}\"),\n          Seq(Row(10, numIndexedCols - 1)))\n        intercept[AnalysisException] {\n          getStatsDf(r, $\"minValues.col$numIndexedCols\").first()\n        }\n      } else {\n        checkAnswer(\n          getStatsDf(r, $\"numRecords\", $\"minValues.col${numTotalCols - 1}\"),\n          Seq(Row(10, numTotalCols - 1)))\n        intercept[AnalysisException] {\n          getStatsDf(r, $\"minValues.col$numTotalCols\").first()\n        }\n      }\n    }\n\n    checkNumIndexedCol(defaultNumIndexedCols)\n    checkNumIndexedCol(numTotalCols - 1)\n    checkNumIndexedCol(numTotalCols)\n    checkNumIndexedCol(numTotalCols + 1)\n    checkNumIndexedCol(0)\n  }\n\n  test(\"remove redundant stats column references in data skipping expression\") {\n    withTable(\"table\") {\n      val colNames = (0 to 100).map(i => s\"col_$i\")\n      sql(s\"\"\"CREATE TABLE `table` (${colNames.map(x => x + \" INT\").mkString(\", \")}) using delta\"\"\")\n      val conditions = colNames.map(i => s\"$i != 1\")\n      val whereClause = conditions.mkString(\"WHERE \", \" AND \", \"\")\n\n      // This query reproduces the issue raised by running TPC-DS q41. Basically the breaking\n      // condition is when the query involves a big boolean expression. As data skipping\n      // generates many redundant null checks on the non-leaf stats columns, e.g., stats\n      // and stats.minValues, the query complexity is amplified in the data skipping expression.\n      // This fix was to simply apply a distinct() on stats column references before generating\n      // the data skipping expression.\n      sql(s\"select col_0 from table $whereClause\").collect\n    }\n  }\n\n  test(\"data skipping shouldn't use expressions involving a subquery \") {\n    withTable(\"t1\", \"t2\") {\n      sql(s\"CREATE TABLE t1(i int, p string) USING delta partitioned by (i)\")\n      sql(\"INSERT INTO t1 SELECT 1, 'a1'\")\n      sql(\"INSERT INTO t1 SELECT 2, 'a2'\")\n      sql(\"INSERT INTO t1 SELECT 3, 'a3'\")\n      sql(\"INSERT INTO t1 SELECT 4, 'a4'\")\n\n      sql(\"CREATE TABLE t2(j int, q string) USING delta\")\n      sql(\"INSERT INTO t2 SELECT 1, 'b1'\")\n      sql(\"INSERT INTO t2 SELECT 2, 'b2'\")\n\n      // This query would fail before the fix, i.e., when skipping considers subquery filters.\n      checkAnswer(sql(\"SELECT i FROM t1 join t2 on i + 2 = j + 1 where q = 'b2'\"), Row(1))\n\n      // Partition filter with subquery should be ignored for skipping\n      val r1 = getScanReport { checkAnswer(\n        sql(\"SELECT p from t1 where i in (select j from t2 where q = 'b1')\"),\n        Seq(Row(\"a1\")))\n      }\n      assert(isFullScan(r1(0)))\n\n\n      // Partition filter with subquery should be ignored for skipping\n      val r3 = getScanReport { checkAnswer(\n        sql(\"SELECT p from t1 where i in (select j from t2 where q = 'b1') and p = 'a2'\"), Nil)\n      }\n      assert(r3(0).size(\"scanned\").rows === Some(1))\n    }\n  }\n\n  test(\"support case insensitivity for partitioning filters\") {\n    withTable(\"table\") {\n      sql(s\"CREATE TABLE table(Year int, P string, Y int) USING delta partitioned by (Year)\")\n      sql(\"INSERT INTO table SELECT 1999, 'a1', 1990\")\n      sql(\"INSERT INTO table SELECT 1989, 'a2', 1990\")\n\n      val Seq(r1) = getScanReport {\n        checkAnswer(sql(\"SELECT * from table where year > 1990\"), Row(1999, \"a1\", 1990))\n      }\n      assert(!isFullScan(r1))\n\n      val Seq(r2) = getScanReport {\n        checkAnswer(\n          sql(\"SELECT * from table where year > 1990 and p = 'a1'\"), Row(1999, \"a1\", 1990))\n      }\n      assert(!isFullScan(r2))\n\n      val Seq(r3) = getScanReport {\n        checkAnswer(sql(\"SELECT * from table where p = 'a1'\"), Row(1999, \"a1\", 1990))\n      }\n      assert(!isFullScan(r3))\n\n\n      checkAnswer(sql(\"SELECT * from table where year < y\"), Row(1989, \"a2\", 1990))\n\n      withSQLConf(SQLConf.CASE_SENSITIVE.key -> \"true\") {\n        intercept[AnalysisException] {\n          sql(\"SELECT * from table where year > 1990\")\n        }\n      }\n    }\n  }\n\n  test(\"Test file pruning metrics with data skipping\") {\n    withTempDir { tempDir =>\n      withTempView(\"t1\", \"t2\") {\n        val data = spark.range(10).toDF(\"col1\")\n          .withColumn(\"col2\", 'col1./(3).cast(DataTypes.IntegerType))\n        data.write.format(\"delta\").partitionBy(\"col1\")\n          .save(tempDir.getCanonicalPath)\n        spark.read.format(\"delta\").load(tempDir.getAbsolutePath).createTempView(\"t1\")\n        val deltaLog = DeltaLog.forTable(spark, tempDir.toString())\n\n        val query = \"SELECT * from t1 where col1 > 5\"\n        val Seq(r1) = getScanReport {\n          assert(sql(query).collect().length == 4)\n        }\n        val inputFiles = spark.sql(query).inputFiles\n        assert(deltaLog.snapshot.numOfFiles - inputFiles.length == 6)\n\n        val allQuery = \"SELECT * from t1\"\n        val Seq(r2) = getScanReport {\n          assert(sql(allQuery).collect().length == 10)\n        }\n      }\n    }\n  }\n\n  test(\"loading data from Delta to parquet should skip data\") {\n    withTempDir { dir =>\n      val path = dir.getCanonicalPath\n      spark.range(5).write.format(\"delta\").save(path)\n      spark.range(5, 10).write.format(\"delta\").mode(\"append\").save(path)\n\n      withTempDir { dir2 =>\n        val path2 = dir2.getCanonicalPath\n        val scans = getScanReport {\n          spark.read.format(\"delta\").load(path).where(\"id < 2\")\n            .write.format(\"parquet\").mode(\"overwrite\").save(path2)\n        }\n        assert(scans.size == 1)\n        assert(\n          scans.head.size(\"scanned\").bytesCompressed != scans.head.size(\"total\").bytesCompressed)\n      }\n    }\n  }\n\n  test(\"data skipping with a different DataFrame schema order\", tableSchemaOnlyTag) {\n    withTable(\"table\") {\n      sql(\"CREATE TABLE table (col1 Int, col2 Int, col3 Int) USING delta\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n      // Only index the first two columns\n      setNumIndexedColumns(r.dataPath.toString, 2)\n      val dataSeq = Seq((1, 2, 3))\n      // We should use the table schema to create stats and the DataFrame schema should be ignored\n      dataSeq.toDF(\"col1\", \"col2\", \"col3\")\n        .select(\"col2\", \"col3\", \"col1\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      var hits = Seq(\n        \"col3 = 10\",\n        \"col1 = 1\",\n        \"col2 = 2\",\n        \"col3 = 3\"\n      )\n      var misses = Seq(\n        \"col1 = 5\",\n        \"col1 = 5 AND col2 = 10\",\n        \"col1 = 5 and col3 = 10\",\n        \"col2 = 10\",\n        \"col2 = 5 and col3 = 10\",\n        \"col1 = 5 and col2 = 10 and col3 = 10\"\n      )\n\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n\n      // Change the statsSchema to 3 columns. But there are only two columns in the stats from\n      // the file\n      setNumIndexedColumns(r.dataPath.toString, 3)\n      hits = Seq(\n        \"col3 = 3\",  // 3 is in col3, but no stats\n        \"col3 = 10\",  // No stats on col3\n        // The data skipping filters will be generated but verifyStatsForFilter will invalidate\n        // the entire predicate\n        \"col1 = 5 and col3 = 10\"\n      )\n      misses = Seq(\n        \"col1 = 5\",\n        \"col1 = 5 AND col2 = 10\"\n      )\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n  }\n\n  test(\"data skipping with a different DataFrame schema and column name case\", tableSchemaOnlyTag) {\n    withTable(\"table\") {\n      sql(\"CREATE TABLE table (col1 Int, col2 Int, col3 Int) USING delta\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n      // Only index the first two columns\n      setNumIndexedColumns(r.dataPath.toString, 2)\n      val dataSeq = Seq((1, 2, 3))\n      // We should use the table schema to create stats and the DataFrame schema should be ignored\n      dataSeq.toDF(\"col1\", \"col2\", \"col3\")\n        .select(\"COL2\", \"Col3\", \"coL1\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      val hits = Seq(\n        \"col3 = 10\",  // No stats for col3\n        // These values should be in the columns\n        \"col1 = 1\",\n        \"col2 = 2\",\n        \"col3 = 3\"\n      )\n      val misses = Seq(\n        \"col1 = 5\",\n        \"col1 = 5 AND col2 = 10\",\n        \"col1 = 5 and col3 = 10\",\n        \"col2 = 10\",\n        \"col2 = 5 and col3 = 10\",\n        \"col1 = 5 and col2 = 10 and col3 = 10\"\n      )\n\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n  }\n\n  test(\"data skipping with a different DataFrame schema order and nested columns\",\n    tableSchemaOnlyTag) {\n    withTempDir { dir =>\n      val structureData = Seq(\n        Row(Row(\"James \", \"\", \"Smith\"), \"36636\", \"M\", 3100)\n      )\n\n      val structureDataSchema = new StructType()\n        .add(\"name\", new StructType()\n          .add(\"firstname\", StringType)\n          .add(\"middlename\", StringType)\n          .add(\"lastname\", StringType))\n        .add(\"id\", StringType)\n        .add(\"gender\", StringType)\n        .add(\"salary\", IntegerType)\n\n      val data = spark.createDataFrame(\n        spark.sparkContext.parallelize(structureData), structureDataSchema)\n\n      data.write.partitionBy(\"id\").format(\"delta\").save(dir.getAbsolutePath)\n      // Only index the first three columns (unnested), excluding partition column id\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getCanonicalPath))\n      setNumIndexedColumns(deltaLog.dataPath.toString, 3)\n\n      val structureDfData = Seq(\n        // The same content as previous row but different DataFrame schema order\n        Row(3100, \"M\", Row(\"James \", \"\", \"Smith\"), \"36636\")\n      )\n      val structureDfSchema = new StructType()\n        .add(\"salary\", IntegerType)\n        .add(\"gender\", StringType)\n        .add(\"name\", new StructType()\n          .add(\"firstname\", StringType)\n          .add(\"middlename\", StringType)\n          .add(\"lastname\", StringType))\n        .add(\"id\", StringType)\n\n      // middlename is missing, but we collect NULL_COUNT for it\n      val df = spark.createDataFrame(\n        spark.sparkContext.parallelize(structureDfData), structureDfSchema)\n      df.write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n\n      val hits = Seq(\n        // Can't skip them since stats schema only has three columns now\n        \"gender = 'M'\",\n        \"salary = 3100\"\n      )\n      val misses = Seq(\n        \"name.firstname = 'Michael'\",\n        \"name.middlename = 'L'\",\n        \"name.lastname = 'Miller'\",\n        \"id = '10000'\",\n        \"name.firstname = 'Robert' and name.middlename = ''\",\n        \"name.firstname = 'Robert' and salary = 3100\"\n      )\n      checkSkipping(deltaLog, hits, misses, structureDfData.toString(), false)\n    }\n  }\n\n  test(\"compatibility with the old behavior that collect stats based on DataFrame schema\",\n    tableSchemaOnlyTag) {\n    withTable(\"table\") {\n      sql(\"CREATE TABLE table (col2 Int, col3 Int, col1 Int) USING delta\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n      // Only index the first two columns\n      setNumIndexedColumns(r.dataPath.toString, 2)\n      val dataSeq = Seq((1, 2, 3))\n      // Only collect stats for col2 and col3\n      dataSeq.toDF(\"col1\", \"col2\", \"col3\")\n        .select(\"col2\", \"col3\", \"col1\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      // Change the schema to (col1, col2, col3). The final result would be the same as using the\n      // old approach to collect stats based on the DataFrame schema\n      sql(\"ALTER TABLE table ALTER COLUMN col1 FIRST\")\n\n      // Since the stats schema is (col1, col2), and we only have stats on col2 and col3, only\n      // the predicate on col2 can be used for filters\n      val hits = Seq(\n        \"col1 = 1\",\n        \"col2 = 2\",\n        \"col3 = 3\",\n        \"col1 = 5\",\n        \"col3 = 10\",\n        \"col1 = 5 AND col2 = 10\",\n        \"col1 = 5 and col3 = 10\",\n        \"col1 = 5 and col2 = 10 and col3 = 10\"\n      )\n      val misses = Seq(\n        \"col2 = 10\",\n        \"col2 = 5 and col3 = 10\"  // This can pass because stats also exists on col3\n      )\n\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n  }\n\n  // TODO(lin): remove this after we remove the DELTA_COLLECT_STATS_USING_TABLE_SCHEMA flag\n  test(\"old behavior with DELTA_COLLECT_STATS_USING_TABLE_SCHEMA set to false\") {\n    // This force the system restore the old stats collection behavior based on the DataFrame schema\n    withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS_USING_TABLE_SCHEMA.key -> \"false\") {\n      withTable(\"table\") {\n        sql(\"CREATE TABLE table (col1 Int, col2 Int, col3 Int) USING delta\")\n        val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n        // Only index the first two columns\n        setNumIndexedColumns(r.dataPath.toString, 2)\n        val dataSeq = Seq((1, 2, 3))\n        // Only collect stats for col2 and col3\n        dataSeq.toDF(\"col1\", \"col2\", \"col3\")\n          .select(\"col2\", \"col3\", \"col1\") // DataFrame schema order\n          .write.mode(\"append\").format(\"delta\")\n          .save(r.dataPath.toString)\n\n        // Since the stats schema is (col1, col2), and we only have stats on col2 and col3, only\n        // the predicate on col2 can be used for filters\n        val hits = Seq(\n          \"col1 = 1\",\n          \"col2 = 2\",\n          \"col3 = 3\",\n          \"col1 = 5\",\n          \"col3 = 10\",\n          \"col1 = 5 AND col2 = 10\",\n          \"col1 = 5 and col3 = 10\",\n          \"col1 = 5 and col2 = 10 and col3 = 10\"\n        )\n        val misses = Seq(\n          \"col2 = 10\",\n          \"col2 = 5 and col3 = 10\" // This can pass because stats also exists on col3\n        )\n\n        checkSkipping(r, hits, misses, dataSeq.toString(), false)\n      }\n    }\n  }\n\n  test(\"data skipping with missing columns in DataFrame\", tableSchemaOnlyTag) {\n    // case-1: dataframe schema has less columns than the dataSkippingNumIndexedCols\n    withTempTable(createTable = false) { tableName =>\n      sql(s\"CREATE TABLE $tableName (a Int, b Int, c Int, d Int, e Int) \" +\n        \"USING delta PARTITIONED BY(b)\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(tableName))\n      // Only index the first three columns, excluding partition column b\n      setNumIndexedColumns(r.dataPath.toString, 3)\n      val dataSeq = Seq((1, 2, 3, 4, 5))\n\n      dataSeq.toDF(\"a\", \"b\", \"c\", \"d\", \"e\")\n        .select(\"a\", \"b\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      val hits = Seq(\n        // These values are in the table\n        \"a = 1\",\n        \"b = 2\",\n        \"c <=> null\",\n        \"d is null\",\n        // No stats for e\n        \"e = 10\"\n      )\n      val misses = Seq(\n        \"a = 10\",\n        \"b = 10\",\n        \"c = 10\",\n        \"c is not null\",\n        \"d = 10\",\n        \"isnotnull(d)\"\n      )\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n\n    // case-2: dataframe schema lacks columns that are supposed to be part of the stats schema,\n    // but has an additional column that should not collect stats on\n    withTempTable(createTable = false) { tableName =>\n      sql(s\"CREATE TABLE $tableName (a Int, b Int, c Int, d Int, e Int) \" +\n        \"USING delta PARTITIONED BY(b)\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(tableName))\n      // Only index the first three columns, excluding partition column b\n      setNumIndexedColumns(r.dataPath.toString, 3)\n      val dataSeq = Seq((1, 2, 3, 4, 5))\n\n      dataSeq.toDF(\"a\", \"b\", \"c\", \"d\", \"e\")\n        .select(\"a\", \"b\", \"d\", \"e\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      val hits = Seq(\n        \"a = 1\", // In table\n        \"isnull(c)\", // In table\n        \"e = 20\" // No stats\n      )\n      val misses = Seq(\n        \"a = 20\",\n        \"b = 20\",\n        \"c = 20\",\n        \"d = 20\",\n        \"a = 20 and c = 20\",\n        \"a = 20 and e = 20\"\n      )\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n\n    // case-3: Structured data with some columns missing and some additional columns\n    withTempDir { dir =>\n      val structureData = Seq(\n        Row(Row(\"James \", \"\", \"Smith\"), \"36636\", \"M\", 3100)\n      )\n\n      val structureDataSchema = new StructType()\n        .add(\"name\", new StructType()\n          .add(\"firstname\", StringType)\n          .add(\"middlename\", StringType)\n          .add(\"lastname\", StringType))\n        .add(\"id\", StringType)\n        .add(\"gender\", StringType)\n        .add(\"salary\", IntegerType)\n\n      val data = spark.createDataFrame(\n        spark.sparkContext.parallelize(structureData), structureDataSchema)\n\n      data.write.partitionBy(\"id\").format(\"delta\").save(dir.getAbsolutePath)\n      // Only index the first three columns (unnested), excluding partition column id\n      val deltaLog = DeltaLog.forTable(spark, new Path(dir.getCanonicalPath))\n      setNumIndexedColumns(deltaLog.dataPath.toString, 3)\n\n      val structureDfData = Seq(\n        Row(2000, Row(\"Robert \", \"Johnson\"), \"40000\")\n      )\n      val structureDfSchema = new StructType()\n        .add(\"salary\", IntegerType)\n        .add(\"name\", new StructType()\n          .add(\"firstname\", StringType)\n          .add(\"lastname\", StringType))\n        .add(\"id\", StringType)\n\n      // middlename is missing, but we collect NULL_COUNT for it\n      val df = spark.createDataFrame(\n        spark.sparkContext.parallelize(structureDfData), structureDfSchema)\n      df.write.mode(\"append\").format(\"delta\").save(dir.getAbsolutePath)\n\n      val hits = Seq(\n        \"gender = 'M'\", // No stats\n        \"salary = 1000\" // No stats\n      )\n      val misses = Seq(\n        \"name.firstname = 'Michael'\",\n        \"name.middlename = 'L'\",\n        \"name.lastname = 'Miller'\",\n        \"id = '10000'\",\n        \"name.firstname = 'Robert' and name.middlename = 'L'\"\n      )\n      checkSkipping(deltaLog, hits, misses, structureDfData.toString(), false)\n    }\n\n    // case-4: dataframe schema does not have any columns within the first\n    // dataSkippingNumIndexedCols columns of the table schema\n    withTempTable(createTable = false) { tableName =>\n      sql(s\"CREATE TABLE $tableName (a Int, b Int, c Int, d Int, e Int) USING delta\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(tableName))\n      // Only index the first three columns\n      setNumIndexedColumns(r.dataPath.toString, 3)\n      val dataSeq = Seq((1, 2, 3, 4, 5))\n\n      dataSeq.toDF(\"a\", \"b\", \"c\", \"d\", \"e\")\n        .select(\"d\", \"e\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      val hits = Seq(\n        \"d = 40\", // No stats\n        \"e = 40\" // No stats\n      )\n      // We can still collect NULL_COUNT for a, b, and c\n      val misses = Seq(\n        \"a = 40\",\n        \"b = 40\",\n        \"c = 40\"\n      )\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n\n    // case-5: The first dataSkippingNumIndexedCols columns of the table schema has map or array\n    // types, which we only collect NULL_COUNT\n    withTempTable(createTable = false) { tableName =>\n      sql(s\"CREATE TABLE $tableName (a Int, b Map<String, Int>, c Array<Int>, d Int, e Int)\" +\n        \" USING delta\")\n      val r = DeltaLog.forTable(spark, new TableIdentifier(tableName))\n      // Only index the first three columns\n      setNumIndexedColumns(r.dataPath.toString, 3)\n      val dataSeq = Seq((1, Map(\"key\" -> 2), Seq(3, 3, 3), 4, 5))\n\n      dataSeq.toDF(\"a\", \"b\", \"c\", \"d\", \"e\")\n        .select(\"b\", \"c\", \"d\") // DataFrame schema order\n        .write.mode(\"append\").format(\"delta\")\n        .save(r.dataPath.toString)\n\n      val hits = Seq(\n        \"d = 50\", // No stats\n        \"e = 50\", // No stats\n        // No min/max stats for c. We couldn't check = for b since EqualTo does not support\n        // ordering on type maP\n        \"c = array(50, 50)\",\n        // b and c should have NULL_COUNT stats, but currently they're not SkippingEligibleColumn\n        // (since they're not AtomicType), we couldn't skip for them\n        \"isnull(b)\",\n        \"c is null\",\n        \"ELEMENT_AT(c, 10) IS NULL\" // Out-of-bounds access returns null.\n      )\n      val misses = Seq(\n        // a has NULL_COUNT stats since it's missing from DataFrame schema\n        \"a = 50\"\n      )\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n  }\n\n\n  test(\"data skipping with generated column\") {\n    withTable(\"table\") {\n      // OSS does not support the generated column syntax in SQL so we have to use table builder\n      val tableBuilder = io.delta.tables.DeltaTable.create(spark).tableName(\"table\")\n      // add regular columns\n      val col1 = io.delta.tables.DeltaTable.columnBuilder(spark, \"col1\")\n        .dataType(\"int\")\n        .build()\n      val col2 = io.delta.tables.DeltaTable.columnBuilder(spark, \"col2\")\n        .dataType(\"string\")\n        .build()\n      // add generated column\n      val genCol3 = io.delta.tables.DeltaTable.columnBuilder(spark, \"genCol3\")\n        .dataType(\"string\")\n        .generatedAlwaysAs(\"substring(col2, 3, 2)\")\n        .build()\n\n      tableBuilder\n        .addColumn(col1)\n        .addColumn(col2)\n        .addColumn(genCol3)\n        .execute()\n      // Only pass in two columns, and col3 will be generated as \"st\"\n      val tableData = Seq((1, \"test string\"))\n      tableData.toDF(\"col1\", \"col2\")\n        .write.format(\"delta\").mode(\"append\")\n        .saveAsTable(\"table\")\n\n      val hits = Seq(\n        \"genCol3 = 'st'\"\n      )\n      val misses = Seq(\n        \"col1 = 10\",\n        \"col2 = 'test'\",\n        \"genCol3 = 'test'\"\n      )\n\n      val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n      checkSkipping(r, hits, misses, tableData.toString(), false)\n    }\n  }\n\n  test(\"data skipping by partitions and data values - nulls\") {\n    val tableDir = Utils.createTempDir().getAbsolutePath\n    val dataSeqs = Seq( // each sequence produce a single file\n      Seq((null, null)),\n      Seq((null, \"a\")),\n      Seq((null, \"b\")),\n      Seq((\"a\", \"a\"), (\"a\", null)),\n      Seq((\"b\", null))\n    )\n    dataSeqs.foreach { seq =>\n      seq.toDF(\"key\", \"value\").coalesce(1)\n        .write.format(\"delta\").partitionBy(\"key\").mode(\"append\").save(tableDir)\n    }\n    val allData = dataSeqs.flatten\n\n    def checkResults(\n                      predicate: String,\n                      expResults: Seq[(String, String)],\n                      expNumPartitions: Int,\n                      expNumFiles: Long): Unit =\n      checkResultsWithPartitions(tableDir, predicate, expResults, expNumPartitions, expNumFiles)\n\n    // Trivial base case\n    checkResults(\n      predicate = \"True\",\n      expResults = allData,\n      expNumPartitions = 3,\n      expNumFiles = 5)\n\n    // Conditions on partition key\n    checkResults(\n      predicate = \"key IS NULL\",\n      expResults = allData.filter(_._1 == null),\n      expNumPartitions = 1,\n      expNumFiles = 3) // 3 files with key = null\n\n    checkResults(\n      predicate = \"key IS NOT NULL\",\n      expResults = allData.filter(_._1 != null),\n      expNumPartitions = 2,\n      expNumFiles = 2) // 2 files with key = 'a', and 1 file with key = 'b'\n\n    checkResults(\n      predicate = \"key <=> NULL\",\n      expResults = allData.filter(_._1 == null),\n      expNumPartitions = 1,\n      expNumFiles = 3) // 3 files with key = null\n\n    checkResults(\n      predicate = \"key = 'a'\",\n      expResults = allData.filter(_._1 == \"a\"),\n      expNumPartitions = 1,\n      expNumFiles = 1) // 1 files with key = 'a'\n\n    checkResults(\n      predicate = \"key <=> 'a'\",\n      expResults = allData.filter(_._1 == \"a\"),\n      expNumPartitions = 1,\n      expNumFiles = 1) // 1 files with key <=> 'a'\n\n    checkResults(\n      predicate = \"key = 'b'\",\n      expResults = allData.filter(_._1 == \"b\"),\n      expNumPartitions = 1,\n      expNumFiles = 1) // 1 files with key = 'b'\n\n    checkResults(\n      predicate = \"key <=> 'b'\",\n      expResults = allData.filter(_._1 == \"b\"),\n      expNumPartitions = 1,\n      expNumFiles = 1) // 1 files with key <=> 'b'\n\n    // Conditions on partitions keys and values\n    checkResults(\n      predicate = \"value IS NULL\",\n      expResults = allData.filter(_._2 == null),\n      expNumPartitions = 3,\n      expNumFiles = 3) // files with all non-NULL values get skipped\n\n    checkResults(\n      predicate = \"value IS NOT NULL\",\n      expResults = allData.filter(_._2 != null),\n      expNumPartitions = 2, // one of the partitions has no files left after data skipping\n      expNumFiles = 3) // files with all NULL values get skipped\n\n    checkResults(\n      predicate = \"value <=> NULL\",\n      expResults = allData.filter(_._2 == null),\n      expNumPartitions = 3,\n      expNumFiles = 3) // same as IS NULL case above\n\n    checkResults(\n      predicate = \"value = 'a'\",\n      expResults = allData.filter(_._2 == \"a\"),\n      expNumPartitions = 2, // one partition has no files left after data skipping\n      expNumFiles = 2) // only two files contain \"a\"\n\n    checkResults(\n      predicate = \"value <=> 'a'\",\n      expResults = allData.filter(_._2 == \"a\"),\n      expNumPartitions = 2, // one partition has no files left after data skipping\n      expNumFiles = 2) // only two files contain \"a\"\n\n    checkResults(\n      predicate = \"value <> 'a'\",\n      expResults = allData.filter(x => x._2 != \"a\" && x._2 != null), // i.e., only (null, b)\n      expNumPartitions = 1,\n      expNumFiles = 1) // only one file contains 'b'\n\n    checkResults(\n      predicate = \"value = 'b'\",\n      expResults = allData.filter(_._2 == \"b\"),\n      expNumPartitions = 1,\n      expNumFiles = 1) // same as previous case\n\n    checkResults(\n      predicate = \"value <=> 'b'\",\n      expResults = allData.filter(_._2 == \"b\"),\n      expNumPartitions = 1,\n      expNumFiles = 1) // same as previous case\n\n    // Conditions on both, partition keys and values\n    checkResults(\n      predicate = \"key IS NULL AND value = 'a'\",\n      expResults = Seq((null, \"a\")),\n      expNumPartitions = 1,\n      expNumFiles = 1) // only one file in the partition has (*, \"a\")\n\n    checkResults(\n      predicate = \"key IS NOT NULL AND value IS NOT NULL\",\n      expResults = Seq((\"a\", \"a\")),\n      expNumPartitions = 1,\n      expNumFiles = 1) // 1 file with (*, a)\n\n    checkResults(\n      predicate = \"key <=> NULL AND value <=> NULL\",\n      expResults = Seq((null, null)),\n      expNumPartitions = 1,\n      expNumFiles = 1) // 3 files with key = null, but only 1 with val = null.\n\n    checkResults(\n      predicate = \"key <=> NULL OR value <=> NULL\",\n      expResults = allData.filter(_ != ((\"a\", \"a\"))),\n      expNumPartitions = 3,\n      expNumFiles = 5) // all 5 files\n  }\n\n  // Note that we cannot use testSkipping here, because the JSON parsing bug we're working around\n  // prevents specifying a microsecond timestamp as input data.\n  for (timestampType <- Seq(\"TIMESTAMP\", \"TIMESTAMP_NTZ\")) {\n    test(s\"data skipping on $timestampType\") {\n      val data = \"2019-09-09 01:02:03.456789\"\n      val df = Seq(data).toDF(\"strTs\")\n        .selectExpr(\n          s\"CAST(strTs AS $timestampType) AS ts\",\n          s\"STRUCT(CAST(strTs AS $timestampType) AS ts) AS nested\")\n\n      val tempDir = Utils.createTempDir()\n      val log = DeltaLog.forTable(spark, tempDir)\n      df.coalesce(1).write.format(\"delta\").save(log.dataPath.toString)\n\n      checkSkipping(\n        log,\n        // Check to ensure that the value actually in the file is always in range queries.\n        hits = Seq(\n          s\"\"\"ts >= cast(\"2019-09-09 01:02:03.456789\" AS $timestampType)\"\"\",\n          s\"\"\"ts <= cast(\"2019-09-09 01:02:03.456789\" AS $timestampType)\"\"\",\n          s\"\"\"nested.ts >= cast(\"2019-09-09 01:02:03.456789\" AS $timestampType)\"\"\",\n          s\"\"\"nested.ts <= cast(\"2019-09-09 01:02:03.456789\" AS $timestampType)\"\"\",\n          s\"\"\"TS >= cast(\"2019-09-09 01:02:03.456789\" AS $timestampType)\"\"\",\n          s\"\"\"nEstED.tS >= cast(\"2019-09-09 01:02:03.456789\" AS $timestampType)\"\"\"),\n        // Check the range of values that are far enough away to be data skipped. Note that the\n        // values are aligned with millisecond boundaries because of the JSON serialization\n        // truncation.\n        misses = Seq(\n          s\"\"\"ts >= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\",\n          s\"\"\"ts <= cast(\"2019-09-04 01:02:03.455999\" AS $timestampType)\"\"\",\n          s\"\"\"nested.ts >= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\",\n          s\"\"\"nested.ts <= cast(\"2019-09-09 01:02:03.455999\" AS $timestampType)\"\"\",\n          s\"\"\"TS >= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\",\n          s\"\"\"nEstED.tS >= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\"),\n        data = data,\n        checkEmptyUnusedFiltersForHits = false)\n    }\n  }\n\n  for (timestampType <- Seq(\"TIMESTAMP\", \"TIMESTAMP_NTZ\")) {\n    test(s\"data skipping on $timestampType with Long.MaxValue\") {\n      val maxVal = \"294247-01-10 04:00:54.775807Z\"\n      val tempDir = Utils.createTempDir()\n      val log = DeltaLog.forTable(spark, tempDir)\n      Seq(maxVal).toDF(\"strTs\")\n        .selectExpr(s\"CAST(strTs AS $timestampType) AS ts\")\n        .coalesce(1)\n        .write\n        .format(\"delta\")\n        .save(log.dataPath.toString)\n\n      checkSkipping(\n        log,\n        hits = Seq(\n          s\"\"\"ts >= cast(\"$maxVal\" AS $timestampType)\"\"\",\n          s\"\"\"ts >= \"$maxVal\"\"\"\",\n          s\"\"\"ts >= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\",\n          // This still hits because of JSON truncation to milliseconds\n          s\"\"\"ts < cast(\"$maxVal\" AS $timestampType)\"\"\".stripMargin),\n        misses = Seq(\n          s\"\"\"ts <= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\",\n          s\"\"\"ts > cast(\"$maxVal\" AS $timestampType)\"\"\"),\n        data = maxVal,\n        checkEmptyUnusedFiltersForHits = false)\n    }\n  }\n\n  for (timestampType <- Seq(\"TIMESTAMP\", \"TIMESTAMP_NTZ\")) {\n    test(s\"data skipping on $timestampType near Long.MaxValue\") {\n      val tempDir = Utils.createTempDir()\n      val log = DeltaLog.forTable(spark, tempDir)\n\n      val nearMaxMicros = Long.MaxValue - 999L\n\n      // Create DataFrame with the near-max timestamp value\n      Seq(nearMaxMicros).toDF(\"microsSinceEpoch\")\n        .selectExpr(s\"TIMESTAMP_MICROS(microsSinceEpoch) AS ts\")\n        .selectExpr(s\"CAST(ts AS $timestampType) AS ts\")\n        .coalesce(1)\n        .write\n        .format(\"delta\")\n        .save(log.dataPath.toString)\n\n      checkSkipping(\n        log,\n        // maxValue of the stats on ts will be saturated to Long.MaxValue instead\n        // of being added 1000 microseconds, which will cause a long overflow.\n        hits = Seq(\n          s\"ts >= TIMESTAMP_MICROS($nearMaxMicros)\",\n          s\"ts >= TIMESTAMP_MICROS(${nearMaxMicros - 100})\",\n          s\"\"\"ts >= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\",\n          s\"ts >= TIMESTAMP_MICROS(${Long.MaxValue - 1000})\",\n          s\"ts < TIMESTAMP_MICROS($nearMaxMicros)\"),\n        misses = Seq(\n          s\"\"\"ts <= cast(\"2019-09-09 01:02:03.457001\" AS $timestampType)\"\"\"),\n        data = nearMaxMicros.toString,\n        checkEmptyUnusedFiltersForHits = false)\n    }\n  }\n\n  test(\"Ensure that we don't reuse scans when tables are different\") {\n    withTempDir { dir =>\n      val table1 = new File(dir, \"tbl1\")\n      val table1Dir = table1.getCanonicalPath\n      val table2 = new File(dir, \"tbl2\")\n      val table2Dir = table2.getCanonicalPath\n      spark.range(100).withColumn(\"part\", 'id % 5).withColumn(\"id2\", 'id)\n        .write.format(\"delta\").partitionBy(\"part\").save(table1Dir)\n\n      FileUtils.copyDirectory(table1, table2)\n\n      sql(s\"DELETE FROM delta.`$table2Dir` WHERE part = 0 and id < 65\")\n\n      val query = sql(s\"SELECT * FROM delta.`$table1Dir` WHERE part = 0 AND id2 < 85 AND \" +\n        s\"id NOT IN (SELECT id FROM delta.`$table2Dir` WHERE part = 0 AND id2 < 85)\")\n\n      checkAnswer(\n        query,\n        sql(s\"SELECT * FROM delta.`$table1Dir` WHERE part = 0 and id < 65\"))\n    }\n  }\n\n  test(\"Data skipping should always return files from latest commit version\") {\n    withTempDir { dir =>\n      // If this test is flacky it is broken\n      Seq(\"aaa\").toDF().write.format(\"delta\").save(dir.getCanonicalPath)\n      val (log, snapshot) = DeltaLog.forTableWithSnapshot(spark, dir.getPath)\n      val addFile = snapshot.allFiles.collect().head\n      val fileWithStat = snapshot.getSpecificFilesWithStats(Seq(addFile.path)).head\n      // Ensure the stats has actual stats, not {}\n      assert(fileWithStat.stats.size > 2)\n      log.startTransaction().commitManually(addFile.copy(stats = \"{}\"))\n\n      // Delta dedup should always keep AddFile from newer version so\n      // getSpecificFilesWithStats should return the AddFile with empty stats\n      log.update()\n      val newfileWithStat =\n        log.unsafeVolatileSnapshot.getSpecificFilesWithStats(Seq(addFile.path)).head\n      assert(newfileWithStat.stats === \"{}\")\n    }\n  }\n\n  Seq(\"create\", \"alter\").foreach { label =>\n    test(s\"Basic: Data skipping with delta statistic column $label\") {\n      withTable(\"table\") {\n        val tableProperty = if (label == \"create\") {\n          \"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c7,c10')\"\n        } else {\n          \"\"\n        }\n        sql(\n          s\"\"\"CREATE TABLE table(\n             |c1 long, c2 STRING, c3 FLOAT, c4 DOUBLE, c5 TIMESTAMP, c6 TIMESTAMP_NTZ, c7 DATE,\n             |c8 BINARY, c9 BOOLEAN, c10 DECIMAL(3, 2)\n             |) USING delta $tableProperty\"\"\".stripMargin)\n        if (label == \"alter\") {\n          sql(\n            s\"\"\"ALTER TABLE table\n               |SET TBLPROPERTIES (\n               |  'delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c7,c10'\n               |)\"\"\".stripMargin)\n        }\n        sql(\n          \"\"\"insert into table values\n            |(1, '1', 1.0, 1.0, TIMESTAMP'2001-01-01 01:00', TIMESTAMP_NTZ'2001-01-01 01:00',\n            |DATE'2001-01-01', '1111', true, 1.0),\n            |(2, '2', 2.0, 2.0, TIMESTAMP'2002-02-02 02:00', TIMESTAMP_NTZ'2002-02-02 02:00',\n            |DATE'2002-02-02', '2222', false, 2.0)\n            |\"\"\".stripMargin).count()\n        val hits = Seq(\n          \"c1 = 1\",\n          \"c2 = \\'2\\'\",\n          \"c3 < 1.5\",\n          \"c4 > 1.0\",\n          \"c5 >= \\\"2001-01-01 01:00:00\\\"\",\n          \"c6 >= \\\"2001-01-01 01:00:00\\\"\",\n          \"c7 = \\\"2002-02-02\\\"\",\n          \"c8 = HEX(\\\"1111\\\")\", // Binary Column doesn't support delta statistics.\n          \"c8 = HEX(\\\"3333\\\")\", // Binary Column doesn't support delta statistics.\n          \"c9 = true\",\n          \"c9 = false\",\n          \"c10 > 1.5\"\n        )\n        val misses = Seq(\n          \"c1 = 10\",\n          \"c2 = \\'4\\'\",\n          \"c3 < 0.5\",\n          \"c4 > 5.0\",\n          \"c5 >= \\\"2003-01-01 01:00:00\\\"\",\n          \"c6 >= \\\"2003-01-01 01:00:00\\\"\",\n          \"c7 = \\\"2003-02-02\\\"\",\n          \"c10 > 2.5\"\n        )\n        val dataSeq = Seq(\n          (1L, \"1\", 1.0f, 1.0d, \"2002-01-01 01:00\", \"2002-01-01 01:00\", \"2001-01-01\", \"1111\",\n            true, 1.0f),\n          (2L, \"2\", 2.0f, 2.0d, \"2002-02-02 02:00\", \"2002-02-02 02:00\", \"2002-02-02\", \"2222\",\n            false, 2.0f)\n        )\n        val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n        checkSkipping(r, hits, misses, dataSeq.toString(), false)\n      }\n    }\n  }\n\n  test(\"data skipping by stats - variant type\") {\n    Seq(false, true).foreach { pushVariantIntoScan =>\n      withSQLConf(SQLConf.PUSH_VARIANT_INTO_SCAN.key -> pushVariantIntoScan.toString) {\n        val tableName = s\"tbl_$pushVariantIntoScan\"\n        withTable(tableName) {\n          sql(s\"\"\"CREATE TABLE $tableName(v VARIANT,\n                  v_struct STRUCT<v: VARIANT>,\n                  null_v VARIANT,\n                  null_v_struct STRUCT<v: VARIANT>) USING DELTA\"\"\")\n          sql(s\"\"\"INSERT INTO $tableName (SELECT\n              parse_json(cast(id as string)),\n              named_struct('v', parse_json(cast(id as string))),\n              cast(null as variant),\n              named_struct('v', cast(null as variant))\n              FROM range(100))\"\"\")\n\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName, None, None))\n          val hits = Seq(\n            \"v IS NOT NULL\",\n            \"v_struct.v IS NOT NULL\",\n            \"null_v IS NULL\",\n            \"null_v_struct.v IS NULL\")\n          val misses = Seq(\n            \"v IS NULL\",\n            \"v_struct.v IS NULL\",\n            \"null_v IS NOT NULL\",\n            \"null_v_struct.v IS NOT NULL\")\n          val data = spark.sql(s\"select * from $tableName\").collect().toSeq.toString\n          checkSkipping(deltaLog, hits, misses, data, false)\n        }\n      }\n    }\n  }\n\n  test(s\"Data skipping with delta statistic column rename column\") {\n    withTable(\"table\") {\n      sql(\n        s\"\"\"CREATE TABLE table(\n           |c1 long, c2 STRING, c3 FLOAT, c4 DOUBLE, c5 TIMESTAMP, c6 TIMESTAMP_NTZ,\n           |c7 DATE, c8 BINARY, c9 BOOLEAN, c10 DECIMAL(3, 2)\n           |) USING delta\n           |TBLPROPERTIES(\n           |'delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c7,c10',\n           |'delta.columnMapping.mode' = 'name',\n           |'delta.minReaderVersion' = '2',\n           |'delta.minWriterVersion' = '5'\n           |)\n           |\"\"\".stripMargin)\n      (1 to 10).foreach { i =>\n        sql(s\"alter table table RENAME COLUMN c$i to cc$i\")\n      }\n      val newConfiguration = sql(\"SHOW TBLPROPERTIES table \")\n        .collect()\n        .map { row =>\n          row.getString(0) -> row.getString(1)\n        }\n        .filter(_._1 == \"delta.dataSkippingStatsColumns\")\n        .toSeq\n      assert(\n        newConfiguration == Seq(\n          (\"delta.dataSkippingStatsColumns\", \"cc1,cc2,cc3,cc4,cc5,cc6,cc7,cc10\"))\n      )\n      sql(\n        \"\"\"insert into table values\n          |(1, '1', 1.0, 1.0, TIMESTAMP'2001-01-01 01:00', TIMESTAMP_NTZ'2001-01-01 01:00',\n          |DATE'2001-01-01', '1111', true, 1.0),\n          |(2, '2', 2.0, 2.0, TIMESTAMP'2002-02-02 02:00', TIMESTAMP_NTZ'2002-02-02 02:00',\n          |DATE'2002-02-02', '2222', false, 2.0)\n          |\"\"\".stripMargin).count()\n      val hits = Seq(\n        \"cc1 = 1\",\n        \"cc2 = \\'2\\'\",\n        \"cc3 < 1.5\",\n        \"cc4 > 1.0\",\n        \"cc5 >= \\\"2001-01-01 01:00:00\\\"\",\n        \"cc6 >= \\\"2001-01-01 01:00:00\\\"\",\n        \"cc7 = \\\"2002-02-02\\\"\",\n        \"cc8 = HEX(\\\"1111\\\")\", // Binary Column doesn't support delta statistics.\n        \"cc8 = HEX(\\\"3333\\\")\", // Binary Column doesn't support delta statistics.\n        \"cc9 = true\",\n        \"cc9 = false\",\n        \"cc10 > 1.5\"\n      )\n      val misses = Seq(\n        \"cc1 = 10\",\n        \"cc2 = \\'4\\'\",\n        \"cc3 < 0.5\",\n        \"cc4 > 5.0\",\n        \"cc5 >= \\\"2003-01-01 01:00:00\\\"\",\n        \"cc6 >= \\\"2003-01-01 01:00:00\\\"\",\n        \"cc7 = \\\"2003-02-02\\\"\",\n        \"cc10 > 2.5\"\n      )\n      val dataSeq = Seq(\n        (1L, \"1\", 1.0f, 1.0d, \"2002-01-01 01:00\", \"2002-01-01 01:00\", \"2001-01-01\", \"1111\", true,\n          1.0f),\n        (2L, \"2\", 2.0f, 2.0d, \"2002-02-02 02:00\", \"2002-02-02 02:00\", \"2002-02-02\", \"2222\", false,\n          2.0f)\n      )\n      val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n  }\n\n  test(s\"Data skipping with delta statistic column drop column\") {\n    withTable(\"table\") {\n      sql(\n        s\"\"\"CREATE TABLE table(\n           |c1 long, c2 STRING, c3 FLOAT, c4 DOUBLE, c5 TIMESTAMP, c6 TIMESTAMP_NTZ,\n           |c7 DATE, c8 BINARY, c9 BOOLEAN, c10 DECIMAL(3, 2))\n           |USING delta\n           |TBLPROPERTIES(\n           |'delta.dataSkippingStatsColumns' = 'c1,c2,c3,c4,c5,c6,c7,c10',\n           |'delta.columnMapping.mode' = 'name',\n           |'delta.minReaderVersion' = '2',\n           |'delta.minWriterVersion' = '5'\n           |)\n           |\"\"\".stripMargin)\n      sql(s\"alter table table drop COLUMN c2\")\n      sql(s\"alter table table drop COLUMN c8\")\n      sql(s\"alter table table drop COLUMN c9\")\n      val newConfiguration = sql(\"SHOW TBLPROPERTIES table \")\n        .collect()\n        .map { row =>\n          row.getString(0) -> row.getString(1)\n        }\n        .filter(_._1 == \"delta.dataSkippingStatsColumns\")\n        .toSeq\n      assert(newConfiguration == Seq((\"delta.dataSkippingStatsColumns\", \"c1,c3,c4,c5,c6,c7,c10\")))\n      sql(\n        \"\"\"insert into table values\n          |(1, 1.0, 1.0, TIMESTAMP'2001-01-01 01:00', TIMESTAMP_NTZ'2001-01-01 01:00',\n          |DATE'2001-01-01', 1.0),\n          |(2, 2.0, 2.0, TIMESTAMP'2002-02-02 02:00', TIMESTAMP_NTZ'2002-02-02 02:00',\n          |DATE'2002-02-02', 2.0)\n          |\"\"\".stripMargin).count()\n      val hits = Seq(\n        \"c1 = 1\",\n        \"c3 < 1.5\",\n        \"c4 > 1.0\",\n        \"c5 >= \\\"2001-01-01 01:00:00\\\"\",\n        \"c6 >= \\\"2001-01-01 01:00:00\\\"\",\n        \"c7 = \\\"2002-02-02\\\"\",\n        \"c10 > 1.5\"\n      )\n      val misses = Seq(\n        \"c1 = 10\",\n        \"c3 < 0.5\",\n        \"c4 > 5.0\",\n        \"c5 >= \\\"2003-01-01 01:00:00\\\"\",\n        \"c6 >= \\\"2003-01-01 01:00:00\\\"\",\n        \"c7 = \\\"2003-02-02\\\"\",\n        \"c10 > 2.5\"\n      )\n      val dataSeq = Seq(\n        (1L, 1.0f, 1.0d, \"2002-01-01 01:00\", \"2002-01-01 01:00\", \"2001-01-01\", 1.0f),\n        (2L, 2.0f, 2.0d, \"2002-02-02 02:00\", \"2002-02-02 02:00\", \"2002-02-02\", 2.0f)\n      )\n      val r = DeltaLog.forTable(spark, new TableIdentifier(\"table\"))\n      checkSkipping(r, hits, misses, dataSeq.toString(), false)\n    }\n  }\n\n  protected def expectedStatsForFile(index: Int, colName: String, deltaLog: DeltaLog): String = {\n    if (deltaLog.unsafeVolatileSnapshot.protocol.isFeatureSupported(DeletionVectorsTableFeature)) {\n      s\"\"\"{\"numRecords\":1,\"minValues\":{\"$colName\":$index},\"maxValues\":{\"$colName\":$index},\"\"\" +\n        s\"\"\"\"nullCount\":{\"$colName\":0},\"tightBounds\":true}\"\"\".stripMargin\n    } else {\n      s\"\"\"{\"numRecords\":1,\"minValues\":{\"$colName\":$index},\"maxValues\":{\"$colName\":$index},\"\"\" +\n        s\"\"\"\"nullCount\":{\"$colName\":0}}\"\"\".stripMargin\n    }\n  }\n\n  test(\"data skipping get specific files with Stats API\") {\n    withTempDir { tempDir =>\n      val tableDirPath = tempDir.getCanonicalPath\n\n      val fileCount = 5\n      // Create 5 files each having 1 row - x=1/x=2/x=3/x=4/x=5\n      val data = spark.range(1, fileCount).toDF(\"x\").repartition(fileCount, col(\"x\"))\n      data.write.format(\"delta\").save(tableDirPath)\n\n      var deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n      // Get name of file corresponding to row x=1\n      val file1 = getFilesRead(deltaLog, \"x = 1\").head.path\n      // Get name of file corresponding to row x=2\n      val file2 = getFilesRead(deltaLog, \"x = 2\").head.path\n      // Get name of file corresponding to row x=3\n      val file3 = getFilesRead(deltaLog, \"x = 3\").head.path\n\n      deltaLog = checkpointAndCreateNewLogIfNecessary(deltaLog)\n      // Delete rows/files for x >= 3 from snapshot\n      sql(s\"DELETE FROM delta.`$tableDirPath` WHERE x >= 3\")\n      // Add another file with just one row x=6 in snapshot\n      sql(s\"INSERT INTO delta.`$tableDirPath` VALUES (6)\")\n\n      // We want the file from the INSERT VALUES (6) stmt. However, this `getFilesRead` call might\n      // also return the AddFile (due to data file re-writes) from the DELETE stmt above. Since\n      // they were committed in different commits, we can select the addFile with the higher\n      // version\n      val addPathToCommitVersion = deltaLog.getChanges(0).flatMap {\n        case (version, actions) => actions\n          .collect { case a: AddFile => a }\n          .map(a => (a.path, version))\n      }.toMap\n\n      val file6 = getFilesRead(deltaLog, \"x = 6\")\n        .map(_.path)\n        .maxBy(path => addPathToCommitVersion(path))\n\n      // At this point, our latest snapshot has only 3 rows: x=1, x=2, x=6 - all in\n      // different files\n\n      // Case-1: all passes files to the API exists in the snapshot\n      val result1 = deltaLog.snapshot.getSpecificFilesWithStats(Seq(file1, file2))\n        .map(addFile => (addFile.path, addFile)).toMap\n      assert(result1.size == 2)\n      assert(result1.keySet == Set(file1, file2))\n      assert(result1(file1).stats === expectedStatsForFile(1, \"x\", deltaLog))\n      assert(result1(file2).stats === expectedStatsForFile(2, \"x\", deltaLog))\n\n      // Case-2: few passes files exists in the snapshot and few don't exists\n      val result2 = deltaLog.snapshot.getSpecificFilesWithStats(Seq(file1, file2, file3))\n        .map(addFile => (addFile.path, addFile)).toMap\n      assert(result1 == result2)\n\n      // Case-3: all passed files don't exists in the snapshot\n      val result3 = deltaLog.snapshot.getSpecificFilesWithStats(Seq(file3, \"xyz\"))\n      assert(result3.isEmpty)\n\n      // Case-4: file3 doesn't exist and file6 exists in the latest commit\n      val result4 = deltaLog.snapshot.getSpecificFilesWithStats(Seq(file3, file6))\n        .map(addFile => (addFile.path, addFile)).toMap\n      assert(result4.size == 1)\n      assert(result4(file6).stats == expectedStatsForFile(6, \"x\", deltaLog))\n    }\n  }\n\n  test(\"File skipping with non-deterministic filters\") {\n    withTable(\"tbl\") {\n      // Create the table.\n      val df = spark.range(100).toDF()\n      df.write.mode(\"overwrite\").format(\"delta\").saveAsTable(\"tbl\")\n\n      // Append 9 times to the table.\n      for (i <- 1 to 9) {\n        val df = spark.range(i * 100, (i + 1) * 100).toDF()\n        df.write.mode(\"append\").format(\"delta\").insertInto(\"tbl\")\n      }\n\n      val query = \"SELECT count(*) FROM tbl WHERE rand(0) < 0.25\"\n      val result = sql(query).collect().head.getLong(0)\n      assert(result > 150, s\"Expected around 250 rows (~0.25 * 1000), got: $result\")\n\n      val predicates = sql(query).queryExecution.optimizedPlan.collect {\n        case Filter(condition, _) => condition\n      }.flatMap(splitConjunctivePredicates)\n      val scanResult = DeltaLog.forTable(spark, TableIdentifier(\"tbl\"))\n        .update().filesForScan(predicates)\n      assert(scanResult.unusedFilters.nonEmpty)\n    }\n  }\n\n  test(\"File skipping with non-deterministic filters on partitioned tables\") {\n    withTable(\"tbl_partitioned\") {\n      import org.apache.spark.sql.functions.col\n\n      // Create initial DataFrame and add a partition column.\n      val df = spark.range(100).toDF().withColumn(\"p\", col(\"id\") % 10)\n      df.write\n        .mode(\"overwrite\")\n        .format(\"delta\")\n        .partitionBy(\"p\")\n        .saveAsTable(\"tbl_partitioned\")\n\n      // Append 9 more times to the table.\n      for (i <- 1 to 9) {\n        val newDF = spark.range(i * 100, (i + 1) * 100).toDF().withColumn(\"p\", col(\"id\") % 10)\n        newDF.write.mode(\"append\").format(\"delta\").insertInto(\"tbl_partitioned\")\n      }\n\n      // Run query with a nondeterministic filter.\n      val query = \"SELECT count(*) FROM tbl_partitioned WHERE rand(0) < 0.25\"\n      val result = sql(query).collect().head.getLong(0)\n      // Assert that the row count is as expected (e.g., roughly 25% of rows).\n      assert(result > 150, s\"Expected a reasonable number of rows, got: $result\")\n\n      val predicates = sql(query).queryExecution.optimizedPlan.collect {\n        case Filter(condition, _) => condition\n      }.flatMap(splitConjunctivePredicates)\n      val scanResult = DeltaLog.forTable(spark, TableIdentifier(\"tbl_partitioned\"))\n        .update().filesForScan(predicates)\n      assert(scanResult.unusedFilters.nonEmpty)\n\n      // Assert that entries are fetched from all 10 partitions\n      val distinctPartitions =\n        sql(\"SELECT DISTINCT p FROM tbl_partitioned WHERE rand(0) < 0.25\")\n        .collect()\n        .length\n      assert(distinctPartitions == 10)\n    }\n  }\n\n  test(\"Data skipping handles aliasing for _metadata fields\") {\n    withTable(\"t\") {\n      // Create table with BIGINT file_name column\n      sql(\"create or replace table t(file_name BIGINT) using delta\")\n      sql(\"insert into t values (1), (2), (3)\")\n      sql(\"insert into t values (4), (5), (6)\")\n      val (fileName, fileCount) = {\n        val dataFilesDF = sql(\"select distinct _metadata.file_name from t\")\n        (dataFilesDF.first().getString(0), dataFilesDF.count())\n      }\n      // Filter rows by _metadata.file_name\n      val df = sql(s\"select * from t where _metadata.file_name = '$fileName'\")\n      // Verify the predicate is not used for data skipping\n      val predicates = df.queryExecution.optimizedPlan.collect {\n        case Filter(condition, _) => condition\n      }.flatMap(splitConjunctivePredicates)\n      val scanResult = DeltaLog.forTable(spark, TableIdentifier(\"t\")).update()\n        .filesForScan(predicates)\n      assert(scanResult.unusedFilters.nonEmpty,\n        \"Expected predicate to be ineligible for data skipping\")\n    }\n  }\n\n  protected def parse(deltaLog: DeltaLog, predicate: String): Seq[Expression] =\n    super.parse(spark, deltaLog, predicate)\n\n  protected def filesRead(\n      deltaLog: DeltaLog,\n      predicate: String,\n      checkEmptyUnusedFilters: Boolean = false): Int =\n    super.filesRead(spark, deltaLog, predicate, checkEmptyUnusedFilters)\n\n  protected def getFilesRead(\n      deltaLog: DeltaLog,\n      predicate: String,\n      checkEmptyUnusedFilters: Boolean = false): Seq[AddFile] =\n    super.getFilesRead(spark, deltaLog, predicate, checkEmptyUnusedFilters)\n\n  protected def checkResultsWithPartitions(\n    tableDir: String,\n    predicate: String,\n    expResults: Seq[(String, String)],\n    expNumPartitions: Int,\n    expNumFiles: Long): Unit = {\n    Given(predicate)\n    val df = spark.read.format(\"delta\").load(tableDir).where(predicate)\n    checkAnswer(df, expResults.toDF())\n\n    val files = getFilesRead(DeltaLog.forTable(spark, tableDir), predicate)\n    assert(files.size == expNumFiles, \"# files incorrect:\\n\\t\" + files.mkString(\"\\n\\t\"))\n\n    val partitionValues = files.map(_.partitionValues).distinct\n    assert(partitionValues.size == expNumPartitions,\n      \"# partitions incorrect:\\n\\t\" + partitionValues.mkString(\"\\n\\t\"))\n  }\n\n  protected def getStatsDf(deltaLog: DeltaLog, columns: Column*): DataFrame = {\n    deltaLog.snapshot.withStats.select(\"stats.*\").select(columns: _*)\n  }\n\n  protected def failPretty(error: String, predicate: String, data: String) = {\n    fail(\n      s\"\"\"$error\n         |\n         |== Data ==\n         |$data\n       \"\"\".stripMargin)\n  }\n\n  protected def setNumIndexedColumns(path: String, numIndexedCols: Int): Unit = {\n    sql(s\"\"\"\n          |ALTER TABLE delta.`$path`\n          |SET TBLPROPERTIES (\n          |  'delta.dataSkippingNumIndexedCols' = '$numIndexedCols'\n          |)\"\"\".stripMargin)\n  }\n\n  protected def setDeltaStatsColumns(path: String, deltaStatsColumns: String): Unit = {\n    sql(s\"\"\"\n           |ALTER TABLE delta.`$path`\n           |SET TBLPROPERTIES (\n           |  'delta.dataSkippingStatsColumns' = '$deltaStatsColumns'\n           |)\"\"\".stripMargin)\n  }\n\n  private def isFullScan(report: ScanReport): Boolean = {\n    report.size(\"scanned\").bytesCompressed === report.size(\"total\").bytesCompressed\n  }\n\n  protected def checkSkipping(\n      log: DeltaLog,\n      hits: Seq[String],\n      misses: Seq[String],\n      data: String,\n      checkEmptyUnusedFiltersForHits: Boolean): Unit = {\n    hits.foreach { predicate =>\n      Given(predicate)\n      if (filesRead(log, predicate, checkEmptyUnusedFiltersForHits) == 0) {\n        failPretty(s\"Expected hit but got miss for $predicate\", predicate, data)\n      }\n    }\n\n    misses.foreach { predicate =>\n      Given(predicate)\n      if (filesRead(log, predicate) != 0) {\n        failPretty(s\"Expected miss but got hit for $predicate\", predicate, data)\n      }\n    }\n    val schemaDiff = SchemaUtils.reportDifferences(\n      log.snapshot.statsSchema.asNullable,\n      log.snapshot.statsSchema)\n    if (schemaDiff.nonEmpty) {\n      fail(s\"The stats schema should be nullable. Differences:\\n${schemaDiff.mkString(\"\\n\")}\")\n    }\n  }\n  protected def getDataSkippingConfs(\n      indexedCols: Int,\n      deltaStatsColNamesOpt: Option[String]): TraversableOnce[(String, String)] = {\n    val numIndexedColsConfOpt = Option(indexedCols)\n      .filter(_ != defaultNumIndexedCols)\n      .map(DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.defaultTablePropertyKey -> _.toString)\n    val indexedColNamesConfOpt = deltaStatsColNamesOpt\n      .map(DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS.defaultTablePropertyKey -> _)\n    numIndexedColsConfOpt ++ indexedColNamesConfOpt\n  }\n\n  protected def testSkipping(\n      name: String,\n      data: String,\n      schema: StructType = null,\n      hits: Seq[String],\n      misses: Seq[String],\n      sqlConfs: Seq[(String, String)] = Nil,\n      indexedCols: Int = defaultNumIndexedCols,\n      deltaStatsColNamesOpt: Option[String] = None,\n      checkEmptyUnusedFiltersForHits: Boolean = false,\n      exceptionOpt: Option[Throwable] = None): Unit = {\n    test(s\"data skipping by stats - $name\") {\n      val allSQLConfs = sqlConfs ++ getDataSkippingConfs(indexedCols, deltaStatsColNamesOpt)\n      withSQLConf(allSQLConfs: _*) {\n        val jsonRecords = data.split(\"\\n\").toSeq\n        val reader = spark.read\n        if (schema != null) { reader.schema(schema) }\n        val df = reader.json(jsonRecords.toDS())\n\n        val tempDir = Utils.createTempDir()\n        val r = DeltaLog.forTable(spark, tempDir)\n        df.coalesce(1).write.format(\"delta\").save(r.dataPath.toString)\n\n        exceptionOpt.map { exception =>\n          val except = intercept[Throwable] {\n            deltaStatsColNamesOpt.foreach { deltaStatsColNames =>\n              setDeltaStatsColumns(r.dataPath.toString, deltaStatsColNames)\n              df.coalesce(1).write.format(\"delta\").mode(\"overwrite\").save(r.dataPath.toString)\n              if (indexedCols != defaultNumIndexedCols) {\n                setNumIndexedColumns(r.dataPath.toString, indexedCols)\n                df.coalesce(1).write.format(\"delta\").mode(\"overwrite\").save(r.dataPath.toString)\n              }\n              checkSkipping(r, hits, misses, data, checkEmptyUnusedFiltersForHits)\n            }\n          }\n          assert(except.getClass == exception.getClass &&\n            except.getMessage.contains(exception.getMessage))\n        }.getOrElse {\n          if (indexedCols != defaultNumIndexedCols) {\n            setNumIndexedColumns(r.dataPath.toString, indexedCols)\n            df.coalesce(1).write.format(\"delta\").mode(\"overwrite\").save(r.dataPath.toString)\n          }\n          deltaStatsColNamesOpt.foreach { deltaStatsColNames =>\n            setDeltaStatsColumns(r.dataPath.toString, deltaStatsColNames)\n            df.coalesce(1).write.format(\"delta\").mode(\"overwrite\").save(r.dataPath.toString)\n          }\n          checkSkipping(r, hits, misses, data, checkEmptyUnusedFiltersForHits)\n        }\n      }\n    }\n  }\n}\n\ntrait DataSkippingDeltaTestsUtils extends PredicateHelper {\n  protected def parse(\n      spark: SparkSession, deltaLog: DeltaLog, predicate: String): Seq[Expression] = {\n\n    // We produce a wrong filter in this case otherwise\n    if (predicate == \"True\") return Seq(Literal.TrueLiteral)\n\n    val filtered = spark.read.format(\"delta\").load(deltaLog.dataPath.toString).where(predicate)\n\n    val optimizedPlan = filtered.queryExecution.optimizedPlan\n\n    // When pushVariantIntoScan = true, the plan is transformed such that a projection is inserted\n    // at the top of the plan. Therefore, the filter node is lower in the plan.\n    val filterNode = optimizedPlan.collectFirst {\n      case f: Filter => f\n    }.getOrElse {\n      optimizedPlan\n    }\n    filterNode\n      .expressions\n      .flatMap(splitConjunctivePredicates)\n  }\n\n  /**\n   * Returns the number of files that should be included in a scan after applying the given\n   * predicate on a snapshot of the Delta log.\n   *\n   * @param deltaLog Delta log for a table.\n   * @param predicate Predicate to run on the Delta table.\n   * @param checkEmptyUnusedFilters If true, check if there were no unused filters, meaning\n   *                                the given predicate was used as data or partition filters.\n   * @return The number of files that should be included in a scan after applying the predicate.\n   */\n  protected def filesRead(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      predicate: String,\n      checkEmptyUnusedFilters: Boolean): Int =\n    getFilesRead(spark, deltaLog, predicate, checkEmptyUnusedFilters).size\n\n  /**\n   * Returns the files that should be included in a scan after applying the given predicate on\n   * a snapshot of the Delta log.\n   * @param deltaLog Delta log for a table.\n   * @param predicate Predicate to run on the Delta table.\n   * @param checkEmptyUnusedFilters If true, check if there were no unused filters, meaning\n   *                                the given predicate was used as data or partition filters.\n   * @return The files that should be included in a scan after applying the predicate.\n   */\n  protected def getFilesRead(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      predicate: String,\n      checkEmptyUnusedFilters: Boolean): Seq[AddFile] = {\n    val parsed = parse(spark, deltaLog, predicate)\n    val res = deltaLog.snapshot.filesForScan(parsed)\n    assert(res.total.files.get == deltaLog.snapshot.numOfFiles)\n    assert(res.total.bytesCompressed.get == deltaLog.snapshot.sizeInBytes)\n    assert(res.scanned.files.get == res.files.size)\n    assert(res.scanned.bytesCompressed.get == res.files.map(_.size).sum)\n    assert(!checkEmptyUnusedFilters || res.unusedFilters.isEmpty)\n    res.files\n  }\n}\n\n\ntrait DataSkippingDeltaTests extends DataSkippingDeltaTestsBase\n/** Tests code paths within DataSkippingReader.scala */\nclass DataSkippingDeltaV1Suite\n  extends DataSkippingDeltaTests\n{\n  import testImplicits._\n\n  test(\"data skipping flags\") {\n    val tempDir = Utils.createTempDir()\n    val r = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n    def rStats: DataFrame =\n      getStatsDf(r, $\"numRecords\", $\"minValues.id\".as(\"id_min\"), $\"maxValues.id\".as(\"id_max\"))\n\n    val data = spark.range(10).repartition(2)\n\n    Given(\"appending data without collecting stats\")\n    withSQLConf(\n        DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\",\n        DeltaConfigs.ROW_TRACKING_ENABLED.defaultTablePropertyKey -> \"false\") {\n      data.write.format(\"delta\").save(r.dataPath.toString)\n      checkAnswer(rStats, Seq(Row(null, null, null), Row(null, null, null)))\n    }\n\n    Given(\"appending data and collecting stats\")\n    withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n      data.write.format(\"delta\").mode(\"append\").save(r.dataPath.toString)\n      checkAnswer(rStats,\n        Seq(Row(null, null, null), Row(null, null, null), Row(4, 0, 8), Row(6, 1, 9)))\n    }\n\n    Given(\"querying reservoir without using stats\")\n    withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> \"false\") {\n      assert(filesRead(r, \"id = 0\") == 4)\n    }\n\n    Given(\"querying reservoir using stats\")\n    withSQLConf(DeltaSQLConf.DELTA_STATS_SKIPPING.key -> \"true\") {\n      assert(filesRead(r, \"id = 0\") == 3)\n    }\n  }\n}\n\n/**\n * Used to disable the tests with the old stats collection behavior on long-running suites to\n * avoid time-out\n * TODO(lin): remove this after we remove the DELTA_COLLECT_STATS_USING_TABLE_SCHEMA flag\n */\ntrait DataSkippingDisableOldStatsSchemaTests extends DataSkippingDeltaTests {\n\n  protected override def test(testName: String, testTags: org.scalatest.Tag*)\n                             (testFun: => Any)\n                             (implicit pos: org.scalactic.source.Position): Unit = {\n    // Adding the null check in case tableSchemaOnlyTag has not been initialized in base traits\n    val newTestTags = if (tableSchemaOnlyTag == null) testTags else tableSchemaOnlyTag +: testTags\n    super.test(testName, newTestTags: _*)(testFun)(pos)\n  }\n}\n\n/** DataSkipping tests under id column mapping */\ntrait DataSkippingDeltaIdColumnMappingTests extends DataSkippingDeltaTests\n  with DeltaColumnMappingTestUtils {\n\n  override def expectedStatsForFile(index: Int, colName: String, deltaLog: DeltaLog): String = {\n    val x = colName.phy(deltaLog)\n    if (deltaLog.unsafeVolatileSnapshot.protocol.isFeatureSupported(DeletionVectorsTableFeature)) {\n      s\"\"\"{\"numRecords\":1,\"minValues\":{\"$x\":$index},\"maxValues\":{\"$x\":$index},\"\"\" +\n        s\"\"\"\"nullCount\":{\"$x\":0},\"tightBounds\":true}\"\"\".stripMargin\n    } else {\n      s\"\"\"{\"numRecords\":1,\"minValues\":{\"$x\":$index},\"maxValues\":{\"$x\":$index},\"\"\" +\n        s\"\"\"\"nullCount\":{\"$x\":0}}\"\"\".stripMargin\n    }\n  }\n}\n\ntrait DataSkippingDeltaTestV1ColumnMappingMode extends DataSkippingDeltaIdColumnMappingTests {\n  override protected def getStatsDf(deltaLog: DeltaLog, columns: Column*): DataFrame = {\n    deltaLog.snapshot.withStats.select(\"stats.*\")\n      .select(convertToPhysicalColumns(columns, deltaLog): _*)\n  }\n}\n\nclass DataSkippingDeltaV1NameColumnMappingSuite\n  extends DataSkippingDeltaV1Suite\n    with DeltaColumnMappingEnableNameMode\n    with DataSkippingDeltaTestV1ColumnMappingMode {\n  override protected def runAllTests: Boolean = true\n}\n\nclass DataSkippingDeltaV1JsonCheckpointV2Suite extends DataSkippingDeltaV1Suite {\n  override def sparkConf: SparkConf = {\n    super.sparkConf.setAll(\n      Seq(\n        DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n        DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> V2Checkpoint.Format.JSON.name\n      )\n    )\n  }\n}\n\nclass DataSkippingDeltaV1ParquetCheckpointV2Suite extends DataSkippingDeltaV1Suite {\n  override def sparkConf: SparkConf = {\n    super.sparkConf.setAll(\n      Seq(\n        DeltaConfigs.CHECKPOINT_POLICY.defaultTablePropertyKey -> CheckpointPolicy.V2.name,\n        DeltaSQLConf.CHECKPOINT_V2_TOP_LEVEL_FILE_FORMAT.key -> V2Checkpoint.Format.PARQUET.name\n      )\n    )\n  }\n}\n\nclass DataSkippingDeltaV1WithCatalogOwnedBatch1Suite extends DataSkippingDeltaV1Suite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(1)\n}\n\nclass DataSkippingDeltaV1WithCatalogOwnedBatch2Suite extends DataSkippingDeltaV1Suite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(2)\n}\n\nclass DataSkippingDeltaV1WithCatalogOwnedBatch100Suite extends DataSkippingDeltaV1Suite {\n  override def catalogOwnedCoordinatorBackfillBatchSize: Option[Int] = Some(100)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/stats/PartitionLikeDataSkippingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.sql.{Date, Timestamp}\n\nimport org.apache.spark.sql.delta.skipping.ClusteredTableTestUtils\nimport org.apache.spark.sql.delta.{DeltaColumnMappingEnableIdMode, DeltaLog}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.scalatest.BeforeAndAfter\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.plans.logical.Filter\nimport org.apache.spark.sql.functions.{array, col, concat, lit, struct}\nimport org.apache.spark.sql.test.SharedSparkSession\n\ntrait PartitionLikeDataSkippingSuiteBase\n  extends QueryTest\n    with SharedSparkSession\n    with BeforeAndAfter\n    with DeltaSQLCommandTest\n    with ClusteredTableTestUtils {\n  import testImplicits._\n\n  // Disable write optimization to ensure close control over the partitioning of ingested files.\n  override protected def sparkConf: SparkConf = super.sparkConf\n    .set(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_ENABLED.key, \"false\")\n\n  protected val testTableName = \"test_table\"\n\n  private def longToTimestampMillis(i: Long): Long = {\n    i +                               // Ensure that there are some millis that will be truncated.\n      i * 1000L +                     // Add some seconds.\n      i * 60 * 1000 +                 // Add some minutes.\n      i * 60 * 60 * 1000 +            // Add some hours.\n      i * 24L * 60 * 60 * 1000 +      // Add some days.\n      i * 30L * 24 * 60 * 60 * 1000 + // Add some months.\n      i * 365L * 24 * 60 * 60 * 1000  // Add some years.\n  }\n\n  // Helper to validate the expected scan metrics of a query.\n  protected def validateExpectedScanMetrics(\n      tableName: String,\n      query: String,\n      expectedNumFiles: Int,\n      expectedNumPartitionLikeDataFilters: Int,\n      allPredicatesUsed: Boolean,\n      minNumFilesToApply: Long): Unit = {\n    // Execute the query without partition-like filters.\n    val baseResult = sql(query).collect()\n    withSQLConf(\n      DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ENABLED.key -> \"true\",\n      DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_THRESHOLD.key ->\n        minNumFilesToApply.toString) {\n      // Execute the query with partition-like filters and validate that the result matches.\n      val res = sql(query).collect()\n      assert(res.sortBy(_.toString).sameElements(baseResult.sortBy(_.toString)))\n\n      val predicates = sql(query).queryExecution.optimizedPlan.collect {\n        case Filter(condition, _) => condition\n      }.flatMap(splitConjunctivePredicates)\n      val scanResult = DeltaLog.forTable(spark, TableIdentifier(tableName))\n        .update().filesForScan(predicates)\n      assert(scanResult.files.length == expectedNumFiles)\n      assert(allPredicatesUsed == scanResult.unusedFilters.isEmpty)\n      assert(scanResult.partitionLikeDataFilters.size == expectedNumPartitionLikeDataFilters)\n    }\n  }\n\n  protected override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // Create a shared test table to be used by many tests.\n    sql(s\"CREATE TABLE $testTableName(s STRUCT<a INT, b STRING>, c DATE, d TIMESTAMP, e INT) \" +\n      s\"USING delta CLUSTER BY (s.a, s.b, c, d)\")\n\n    // Insert 10 files that each have the same value on all clustering columns.\n    val srcDF = (0L until 10L).map{ i =>\n      val timestampMillis = longToTimestampMillis(i)\n      val timestamp = new Timestamp(timestampMillis)\n      (9 - i.toInt, timestamp.toString, new Date(timestampMillis), timestamp, i.toInt)\n    }.toDF(\"a\", \"b\", \"c\", \"d\", \"e\")\n      .withColumn(\"s\", struct(col(\"a\"), col(\"b\")))\n      .select(\"s\", \"c\", \"d\", \"e\")\n    srcDF.repartitionByRange(10, col(\"s.a\"))\n      .write\n      .format(\"delta\")\n      .mode(\"append\")\n      .saveAsTable(testTableName)\n\n    // Insert 10 files that each have the same value on all clustering columns, but that also\n    // contain partial nulls.\n    (0 until 10).foreach { i =>\n      srcDF.where(col(\"a\") === i)\n        .union(\n          spark.range(1)\n            .withColumn(\"a\", lit(null).cast(\"int\"))\n            .withColumn(\"b\", lit(null).cast(\"string\"))\n            .withColumn(\"s\", struct(col(\"a\"), col(\"b\")))\n            .select(\"s\")\n            .withColumn(\"c\", lit(null).cast(\"date\"))\n            .withColumn(\"d\", lit(null).cast(\"timestamp\"))\n            .withColumn(\"e\", lit(null).cast(\"int\"))\n            .drop(\"id\"))\n        .coalesce(1)\n        .write.format(\"delta\").mode(\"append\").insertInto(testTableName)\n    }\n\n    // Insert 1 file that contains only nulls on each clustering column.\n    spark.range(1)\n      .withColumn(\"a\", lit(null).cast(\"int\"))\n      .withColumn(\"b\", lit(null).cast(\"string\"))\n      .withColumn(\"s\", struct(col(\"a\"), col(\"b\")))\n      .select(\"s\")\n      .withColumn(\"c\", lit(null).cast(\"date\"))\n      .withColumn(\"d\", lit(null).cast(\"timestamp\"))\n      .withColumn(\"e\", lit(null).cast(\"int\"))\n      .drop(\"id\")\n      .coalesce(1)\n      .write.format(\"delta\").mode(\"append\").insertInto(testTableName)\n\n    // Insert 1 file that does not have perfect clustering.\n    srcDF\n      .coalesce(1)\n      .write.format(\"delta\").mode(\"append\").insertInto(testTableName)\n\n    // Register a deterministic and nondeterministic UDF.\n    spark.udf.register(\"isEven\", (x: Int) => x % 2 == 0)\n    spark.udf.register(\"randIsEven\", (x: Int) => x % 2 == 0).asNondeterministic()\n  }\n\n  override protected def afterAll(): Unit = {\n    // Cleanup shared table.\n    sql(s\"DROP TABLE IF EXISTS $testTableName\")\n    super.afterAll()\n  }\n\n  // Test cases for partition-like data skipping on the shared table.\n  // Each test case has the format:\n  // (name, predicate, num matching files (out of 10), number of partition-like predicates,\n  //  all predicates used for data skipping)\n  private val partitionLikeTestCases = Seq(\n    // Valid partition-like predicates.\n    (\"COALESCE\", \"COALESCE(null, s.a, 1) = 1\", 2, 1, true),\n    (\"COALESCE\", \"COALESCE(null, s.a, 1) > 3\", 6, 1, true),\n    (\"COALESCE\", \"COALESCE(null, s.a, 1) != 1\", 9, 1, true),\n    (\"COALESCE\", \"COALESCE(s.A, 1) != 1\", 9, 1, true),\n    (\"COALESCE\", \"COALESCE(TO_DATE(S.b), c) = '1976-07-03'\", 1, 1, true),\n    (\"CAST\", \"CAST(s.A AS STRING) = '1'\", 1, 1, true),\n    (\"CAST\", \"CAST(s.A AS STRING) < '3'\", 3, 1, true),\n    (\"CAST\", \"CAST(s.A AS STRING) >= '3'\", 7, 1, true),\n    (\"CAST\", \"TO_DATE(s.b) = '1976-07-03'\", 1, 1, true),\n    (\"CAST\", \"CAST(s.a AS TIMESTAMP) = TIMESTAMP_SECONDS(1)\", 1, 1, true),\n    (\"YEAR\", \"YEAR(TIMESTAMP(s.b)) = 1969\", 1, 1, true),\n    (\"MONTH\", \"MONTH(TIMESTAMP(s.b)) = 1\", 1, 1, true),\n    (\"DAYOFYEAR\", \"DAYOFYEAR(TIMESTAMP(s.b)) = 31\", 1, 1, true),\n    (\"DAYOFMONTH\", \"DAYOFMONTH(TIMESTAMP(s.b)) = 2\", 2, 1, true),\n    (\"MINUTE\", \"MINUTE(TIMESTAMP(s.b)) = 5\", 1, 1, true),\n    (\"SECOND\", \"SECOND(TIMESTAMP(s.b)) = 5\", 1, 1, true),\n    (\"LIKE\", \"s.b LIKE '%7.007'\", 1, 1, true),\n    (\"DATE_FORMAT\", \"DATE_FORMAT(c, 'yyyy-MM') = '1976-07'\", 1, 1, true),\n    (\"ENDSWITH\", \"ENDSWITH(s.b, '7.007')\", 1, 1, true),\n    (\"LENGTH\", \"LENGTH(s.b) = 21\", 1, 1, true),\n    (\"TRIM\", \"TRIM(CONCAT('      ', s.b, '   ')) = '1971-01-31 17:01:01.001'\", 1, 1, true),\n    (\"LOWER\", \"LOWER(CONCAT('AAA', s.b)) = 'aaa1971-01-31 17:01:01.001'\", 1, 1, true),\n    (\"CONCAT\", \"CONCAT(s.b, CAST(s.a AS STRING)) = '1971-01-31 17:01:01.0018'\", 1, 1, true),\n    (\"FROM_UNIXTIME\", \"FROM_UNIXTIME(s.a) = '1969-12-31 16:00:00'\", 1, 1, true),\n    (\"TO_UNIX_TIMESTAMP\", \"TO_UNIX_TIMESTAMP(CAST(s.b AS TIMESTAMP)) = 0\", 1, 1, true),\n    (\"DATE_TRUNC\", \"DATE_TRUNC('MONTH', CAST(s.b AS DATE)) = '1976-07-01'\", 1, 1, true),\n    (\"TRUNC\", \"TRUNC(CAST(s.b AS DATE), 'MONTH') = '1976-07-01'\", 1, 1, true),\n    (\"DATE_FROM_UNIX_DATE\", \"DATE_FROM_UNIX_DATE(s.a) = '1970-01-05'\", 1, 1, true),\n    (\"ISNULL\", \"CAST(s.a AS STRING) IS NULL\", 1, 1, true),\n    (\"ISNOTNULL\", \"CAST(s.a AS STRING) IS NOT NULL\", 10, 1, true),\n    // Fully eligible compound predicates.\n    (\"NOT (valid)\", \"NOT CONTAINS(s.b, '7.007')\", 9, 1, true),\n    (\n      \"AND (both valid)\", \"(CONTAINS(s.b, '-03') AND CONTAINS(s.b, '04')) OR ENDSWITH(s.b, '008')\",\n      2,\n      1,\n      true\n    ),\n    (\"OR (both valid)\", \"ENDSWITH(s.b, '7.007') OR ENDSWITH(s.b, '8.008')\", 2, 1, true),\n    // Partially eligible compound predicates.\n    (\n      \"AND (one valid)\",\n      \"(ISEVEN(s.a) AND ENDSWITH(s.b, '7.007')) OR ENDSWITH(s.b, '008')\",\n      11,\n      0,\n      false\n    ),\n    (\n      \"OR (one valid)\",\n      \"DATE_FROM_UNIX_DATE(e) = '1977-01-05' OR ENDSWITH(s.b, '7.007')\",\n      11,\n      0,\n      false\n    ),\n    (\n      \"Inverted partially valid AND\",\n      \"STRING((ENDSWITH(s.b, '7.007') OR ENDSWITH(s.b, '001')) AND e > 5) = 'false'\",\n      11,\n      0,\n      false\n    ),\n    // Fully ineligible compound predicates.\n    (\"NOT (invalid)\", \"NOT ISEVEN(s.a)\", 11, 0, false),\n    (\n      \"AND (invalid)\",\n      \"(LEN(STRING(d)) = 23 AND STRING(e) = '1') OR ENDSWITH(s.b, '008')\",\n      11,\n      0,\n      false\n    ),\n    (\"OR (invalid)\", \"ISEVEN(s.a) OR STRING(e) = '1'\", 11, 0, false),\n    // Predicates on non-clustering columns.\n    (\"CAST\", \"CAST(e AS STRING) = '1'\", 10, 0, false),\n    (\"DATE_FROM_UNIX_DATE\", \"DATE_FROM_UNIX_DATE(e) = '1977-01-05'\", 10, 0, false),\n    // Predicates on timestamp column.\n    (\"CAST\", \"CAST(d AS STRING) = '1970-01-01 00:00:00.000'\", 10, 0, false),\n    // Unsupported expressions.\n    (\"RAND\", \"RAND(0) * s.a > 0.5\", 10, 0, false),\n    (\"UDF\", \"ISEVEN(s.a)\", 11, 0, false),\n    (\"UDF\", \"RANDISEVEN(s.a)\", 11, 0, false),\n    (\"SCALAR_SUBQUERY\", s\"DATE(s.b) = (SELECT(MAX(c)) FROM $testTableName)\", 10, 0, false),\n    (\"REGEX\", \"REGEXP_EXTRACT(s.b, '([0-9][0-9][0-9][0-9]).*') = '1970'\", 10, 0, false)\n  )\n\n  // Test cases for combinations of partition-like data skipping with normal data skipping on the\n  // shared table.\n  // Each test case has the format:\n  // (predicate, num matching files, number of partition-like predicates,\n  //  all predicates used for data skipping)\n  // For these test cases, the number of matching files will be the sum across 3 groups of files:\n  // 1. Files with the same min-max values and no nulls or all nulls (out of 11).\n  // 2. Files with the same min-max values, but some nulls (out of 10). Only traditional data\n  // skipping can affect these files.\n  // 3. Files with different min-max values (out of 1). This file will always be read because it has\n  //    the full range of all values across the clustering columns.\n  private val combinedDataSkippingTestCases = Seq(\n    // All partition-like filters are eligible.\n    (\"s.a > 5 AND COALESCE(null, s.a, 1) < 7\", 1 + 4 + 1, 1, true),\n    (\"e < 4 AND COALESCE(null, s.a, 1) < 7\", 1 + 4 + 1, 1, true),\n    (\"d < '1975-01-01' AND DATE(s.b) > '1974-01-01'\", 1 + 5 + 1, 1, true),\n    // Some partition-like filters are eligible.\n    (\"ISEVEN(s.a) AND ENDSWITH(s.b, '007') AND s.a < 5\", 1 + 5 + 1, 1, false),\n    (\"(ISEVEN(s.a) AND s.a > 5) OR ENDSWITH(s.b, '008')\", 11 + 10 + 1, 0, false),\n    (\n      \"((ISEVEN(s.a) AND ENDSWITH(s.b, '007')) OR ENDSWITH(s.b, '008')) AND s.a < 5\",\n      5 + 5 + 1,\n      0,\n      false\n    ),\n    // No partition-like filters are eligible.\n    (\"ISEVEN(s.a) AND s.a < 5\", 5 + 5 + 1, 0, false),\n    (\"STRING(e) = '1' AND s.a < 5\", 5 + 5 + 1, 0, false)\n  )\n\n  partitionLikeTestCases.foreach {\n    case (name, predicate, expectedNumFiles, expectedNumPredicates, allPredicatesUsed) =>\n      test(s\"partition-like data skipping for expression $name: $predicate\") {\n        validateExpectedScanMetrics(\n          tableName = testTableName,\n          query = s\"SELECT * FROM $testTableName WHERE $predicate\",\n          expectedNumFiles = 11 + expectedNumFiles,\n          expectedNumPartitionLikeDataFilters = expectedNumPredicates,\n          allPredicatesUsed = allPredicatesUsed,\n          minNumFilesToApply = 1L)\n      }\n  }\n\n  combinedDataSkippingTestCases.foreach {\n    case (predicate, expectedNumFiles, expectedNumPredicates, allPredicatesUsed) =>\n      test(s\"combined data skipping test: $predicate\") {\n        validateExpectedScanMetrics(\n          tableName = testTableName,\n          query = s\"SELECT * FROM $testTableName WHERE $predicate\",\n          expectedNumFiles = expectedNumFiles,\n          expectedNumPartitionLikeDataFilters = expectedNumPredicates,\n          allPredicatesUsed = allPredicatesUsed,\n          minNumFilesToApply = 1L)\n      }\n  }\n\n\n  test(\"partition-like data skipping not applied to truncated string column\") {\n    val tbl = \"tbl\"\n    withClusteredTable(tbl, \"a STRING, b BIGINT\", \"a, b\") {\n      // Insert 10 files with truncated string values.\n      spark.range(10)\n        .withColumnRenamed(\"id\", \"b\")\n        .withColumn(\"a\", concat(lit(\"abcde\" * 10), col(\"b\")))\n        .select(\"a\", \"b\") // Reorder columns to ensure the schema matches.\n        .repartitionByRange(10, col(\"a\"))\n        .write.format(\"delta\").mode(\"append\").insertInto(tbl)\n\n      // Insert 10 files with non-truncated string values.\n      spark.range(10)\n        .withColumnRenamed(\"id\", \"b\")\n        .withColumn(\"a\", concat(lit(\"fghij\" * 3), col(\"b\")))\n        .select(\"a\", \"b\") // Reorder columns to ensure the schema matches.\n        .repartitionByRange(10, col(\"a\"))\n        .write.format(\"delta\").mode(\"append\").insertInto(tbl)\n\n      // For a starts-with predicate (existing data skipping), we can skip files normally.\n      validateExpectedScanMetrics(\n        tbl, s\"SELECT * FROM $tbl WHERE STARTSWITH(a, 'fghij')\", 10, 0, true, 1L)\n\n      // For an ends-with predicate, we can only skip files that don't have truncated stats.\n      validateExpectedScanMetrics(\n        tbl, s\"SELECT * FROM $tbl WHERE ENDSWITH(a, '9')\", 11, 1, true, 1L)\n    }\n  }\n\n  test(\"partition-like data skipping evaluates file eligibility before skipping expression\") {\n    val tbl = \"tbl\"\n    withClusteredTable(tbl, \"a STRING, b BIGINT\", \"a\") {\n      spark.range(10)\n        .withColumnRenamed(\"id\", \"b\")\n        .withColumn(\"a\", concat(lit(\"abcde\" * 10), lit(\"--\"), col(\"b\")))\n        .select(\"a\", \"b\") // Reorder columns to ensure the schema matches.\n        .repartitionByRange(10, col(\"a\"))\n        .write.format(\"delta\").mode(\"append\").insertInto(tbl)\n\n      validateExpectedScanMetrics(\n        tbl, s\"SELECT * FROM $tbl WHERE SPLIT(a, '--')[1]=8\", 10, 1, true, 1L)\n    }\n  }\n\n  test(\"partition-like data skipping not applied to sufficiently small tables\") {\n    validateExpectedScanMetrics(\n      tableName = testTableName,\n      query = s\"SELECT * FROM $testTableName WHERE COALESCE(null, s.a, 1) = 1\",\n      expectedNumFiles = 22,\n      expectedNumPartitionLikeDataFilters = 0,\n      allPredicatesUsed = false,\n      minNumFilesToApply = 8000)\n  }\n\n  test(\"partition-like data skipping when predicate returns NULL\") {\n    // Predicate returns NULL both when the input attributes are NULL and when the input attributes\n    // are non-null.\n    val predicate = \"GET(ARRAY(1, 2, 3), INT(SUBSTR(s.b, 4, 1))) IN (1, 2)\"\n    validateExpectedScanMetrics(\n      tableName = testTableName,\n      query = s\"SELECT * FROM $testTableName WHERE $predicate\",\n      expectedNumFiles = 1 + 10 + 1,\n      expectedNumPartitionLikeDataFilters = 1,\n      allPredicatesUsed = true,\n      minNumFilesToApply = 1)\n  }\n\n  test(\"partition-like data skipping expression references non-skipping eligible columns\") {\n    val tbl = \"tbl\"\n    withClusteredTable(\n        table = tbl,\n        schema = \"a BIGINT, b ARRAY<BIGINT>, c STRUCT<d ARRAY<BIGINT>, e BIGINT>\",\n        clusterBy = \"a\") {\n      spark.range(10)\n        .withColumnRenamed(\"id\", \"a\")\n        .withColumn(\"b\", array(col(\"a\"), lit(0L)))\n        .withColumn(\"c\", struct(array(col(\"a\"), lit(0L)), lit(0L)))\n        .select(\"a\", \"b\", \"c\") // Reorder columns to ensure the schema matches.\n        .repartitionByRange(10, col(\"a\"))\n        .write.format(\"delta\").mode(\"append\").insertInto(tbl)\n\n      // All files should be read because the filters are on columns that aren't skipping eligible.\n      validateExpectedScanMetrics(\n        tableName = tbl,\n        query = s\"SELECT * FROM $tbl WHERE GET(b, 1) = 0\",\n        expectedNumFiles = 10,\n        expectedNumPartitionLikeDataFilters = 0,\n        allPredicatesUsed = false,\n        minNumFilesToApply = 1)\n      validateExpectedScanMetrics(\n        tableName = tbl,\n        query = s\"SELECT * FROM $tbl WHERE GET(c.d, 1) = 0\",\n        expectedNumFiles = 10,\n        expectedNumPartitionLikeDataFilters = 0,\n        allPredicatesUsed = false,\n        minNumFilesToApply = 1)\n    }\n  }\n\n  test(\"partition-like skipping can reference non-clustering columns via config\") {\n    withSQLConf(\n        DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_CLUSTERING_COLUMNS_ONLY.key ->\n          \"false\") {\n      validateExpectedScanMetrics(\n        tableName = testTableName,\n        query = s\"SELECT * FROM $testTableName WHERE CAST(e AS STRING) = '1'\",\n        expectedNumFiles = 12,\n        expectedNumPartitionLikeDataFilters = 1,\n        allPredicatesUsed = true,\n        minNumFilesToApply = 1L)\n    }\n  }\n\n  test(\"partition-like skipping whitelist can be expanded via config\") {\n    // Single additional supported expression.\n    withSQLConf(\n      DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ADDITIONAL_SUPPORTED_EXPRESSIONS.key ->\n        \"org.apache.spark.sql.catalyst.expressions.RegExpExtract\") {\n      val query = s\"SELECT * FROM $testTableName \" +\n        \"WHERE REGEXP_EXTRACT(s.b, '([0-9][0-9][0-9][0-9]).*') = '1971'\"\n      validateExpectedScanMetrics(\n        tableName = testTableName,\n        query = query,\n        expectedNumFiles = 12,\n        expectedNumPartitionLikeDataFilters = 1,\n        allPredicatesUsed = true,\n        minNumFilesToApply = 1L)\n    }\n\n    // Multiple additional supported expressions.\n    DeltaLog.clearCache() // Clear cache to avoid stale config reads.\n    withSQLConf(\n      DeltaSQLConf.DELTA_DATASKIPPING_PARTITION_LIKE_FILTERS_ADDITIONAL_SUPPORTED_EXPRESSIONS.key ->\n        (\"org.apache.spark.sql.catalyst.expressions.RegExpExtract,\" +\n        \"org.apache.spark.sql.catalyst.expressions.JsonToStructs\")) {\n      val query = s\"\"\"\n        |SELECT * FROM $testTableName\n        |WHERE (REGEXP_EXTRACT(s.b, '([0-9][0-9][0-9][0-9]).*') = '1971' OR\n        |FROM_JSON(CONCAT('{\"date\":\"', STRING(c), '\"}'), 'date STRING')['date'] = '1972-03-02')\n        |\"\"\".stripMargin\n      validateExpectedScanMetrics(\n        tableName = testTableName,\n        query = query,\n        expectedNumFiles = 13,\n        expectedNumPartitionLikeDataFilters = 1,\n        allPredicatesUsed = true,\n        minNumFilesToApply = 1L)\n    }\n  }\n}\n\nclass PartitionLikeDataSkippingSuite extends PartitionLikeDataSkippingSuiteBase\n\nclass PartitionLikeDataSkippingColumnMappingSuite\n  extends PartitionLikeDataSkippingSuiteBase with DeltaColumnMappingEnableIdMode {\n  override def runAllTests: Boolean = true\n\n  test(\"partition-like data skipping with special characters in column names\") {\n    val tbl = \"tbl\"\n    withTable(tbl) {\n      sql(s\"CREATE TABLE $tbl SHALLOW CLONE $testTableName\")\n      sql(s\"ALTER TABLE $tbl RENAME COLUMN c TO `a.b`\")\n      sql(s\"ALTER TABLE $tbl ADD COLUMN `s.a` STRUCT<b INT, c INT>\")\n      // Validate clustering columns are resolved with case-insensitive resolution.\n      validateExpectedScanMetrics(\n        tableName = tbl,\n        query = s\"SELECT * FROM $tbl WHERE DATE_FORMAT(`A.B`, 'yyyy-MM') = '1976-07'\",\n        expectedNumFiles = 12,\n        expectedNumPartitionLikeDataFilters = 1,\n        allPredicatesUsed = true,\n        minNumFilesToApply = 1L)\n\n      // Predicate not on a clustering column - should not be eligible for partition-like data\n      // skipping.\n      validateExpectedScanMetrics(\n        tableName = tbl,\n        query = s\"SELECT * FROM $tbl WHERE COALESCE(null, `s.a`.b, 1) = 1\",\n        expectedNumFiles = 22,\n        allPredicatesUsed = false,\n        expectedNumPartitionLikeDataFilters = 0,\n        minNumFilesToApply = 1L)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/stats/PreparedDeltaFileIndexRowCountSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.delta.{DeltaLog, DeltaTable}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport org.apache.spark.sql.{DataFrame, QueryTest}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Test suite to verify when preparedScan.scanned.rows is populated in PreparedDeltaFileIndex,\n * and the behavior of the DELTA_ALWAYS_COLLECT_STATS flag.\n */\nclass PreparedDeltaFileIndexRowCountSuite\n    extends QueryTest\n    with DeltaSQLCommandTest {\n\n  import testImplicits._\n\n  private def getDeltaScan(df: DataFrame): DeltaScan = {\n    val scans = df.queryExecution.optimizedPlan.collect {\n      case DeltaTable(prepared: PreparedDeltaFileIndex) => prepared.preparedScan\n    }\n    assert(scans.size == 1, s\"Expected 1 DeltaScan, found ${scans.size}\")\n    scans.head\n  }\n\n  /**\n   * Test utility that creates a partitioned Delta table and verifies scanned.rows and\n   * scanned.logicalRows behavior.\n   *\n   * @param alwaysCollectStats value of the DELTA_ALWAYS_COLLECT_STATS flag\n   * @param queryTransform function to transform the base DataFrame (apply filters)\n   * @param expectedRowsDefined whether scanned.rows should be defined\n   * @param expectedRowCount expected row count if defined (None to skip validation)\n   * @param expectedLogicalRowsDefined whether scanned.logicalRows should be defined\n   *                                   (defaults to same as expectedRowsDefined)\n   * @param expectedLogicalRowCount expected logical row count if defined (None to skip validation)\n   */\n  private def testRowCountBehavior(\n      alwaysCollectStats: Boolean,\n      queryTransform: DataFrame => DataFrame,\n      expectedRowsDefined: Boolean,\n      expectedRowCount: Option[Long] = None,\n      expectedLogicalRowsDefined: Option[Boolean] = None,\n      expectedLogicalRowCount: Option[Long] = None): Unit = {\n    withTempDir { dir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"true\") {\n        spark.range(100).toDF(\"id\")\n          .withColumn(\"part\", $\"id\" % 4)\n          .repartition(4)\n          .write.format(\"delta\").partitionBy(\"part\").save(dir.getAbsolutePath)\n      }\n\n      DeltaLog.clearCache()\n\n      withSQLConf(DeltaSQLConf.DELTA_ALWAYS_COLLECT_STATS.key -> alwaysCollectStats.toString) {\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        val scan = getDeltaScan(queryTransform(df))\n\n        if (expectedRowsDefined) {\n          assert(scan.scanned.rows.isDefined, \"scanned.rows should be defined\")\n          expectedRowCount.foreach { expected =>\n            assert(scan.scanned.rows.get == expected,\n              s\"Expected $expected rows, got ${scan.scanned.rows.get}\")\n          }\n        } else {\n          assert(scan.scanned.rows.isEmpty, \"scanned.rows should be None\")\n        }\n\n        // logicalRows should follow the same defined/undefined pattern as rows by default\n        val logicalDefined = expectedLogicalRowsDefined.getOrElse(expectedRowsDefined)\n        if (logicalDefined) {\n          assert(scan.scanned.logicalRows.isDefined, \"scanned.logicalRows should be defined\")\n          expectedLogicalRowCount.foreach { expected =>\n            assert(scan.scanned.logicalRows.get == expected,\n              s\"Expected $expected logical rows, got ${scan.scanned.logicalRows.get}\")\n          }\n        } else {\n          assert(scan.scanned.logicalRows.isEmpty, \"scanned.logicalRows should be None\")\n        }\n      }\n    }\n  }\n\n  // Define query cases: (name, transform function, always collects rows)\n  private val queryCases: Seq[(String, DataFrame => DataFrame, Boolean)] = Seq(\n    (\"no filter\", identity[DataFrame], false),\n    (\"TrueLiteral filter\", _.where(lit(true)), false),\n    (\"partition filter only\", _.where($\"part\" === 1), false),\n    (\"data filter\", _.where($\"id\" === 50), true),\n    (\"partition + data filter\", _.where($\"part\" === 1).where($\"id\" === 49), true)\n  )\n\n  // Grid test: all query cases x flag values\n  for {\n    (caseName, queryTransform, alwaysCollectsRows) <- queryCases\n    alwaysCollectStats <- Seq(false, true)\n  } {\n    val flagDesc = s\"alwaysCollectStats=$alwaysCollectStats\"\n    // If the query type always collects rows, rows is always defined; otherwise depends on flag\n    val expectedRowsDefined = alwaysCollectsRows || alwaysCollectStats\n\n    test(s\"$caseName - $flagDesc\") {\n      testRowCountBehavior(\n        alwaysCollectStats = alwaysCollectStats,\n        queryTransform = queryTransform,\n        expectedRowsDefined = expectedRowsDefined\n      )\n    }\n  }\n\n  test(\"alwaysCollectStats with missing stats returns None\") {\n    withTempDir { dir =>\n      // Create table without stats\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        spark.range(100).toDF(\"id\")\n          .write.format(\"delta\").save(dir.getAbsolutePath)\n      }\n\n      DeltaLog.clearCache()\n\n      withSQLConf(DeltaSQLConf.DELTA_ALWAYS_COLLECT_STATS.key -> \"true\") {\n        val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n        val scan = getDeltaScan(df)\n        assert(scan.scanned.rows.isEmpty, \"scanned.rows should be None when stats are missing\")\n        assert(scan.scanned.logicalRows.isEmpty,\n          \"scanned.logicalRows should be None when stats are missing\")\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/stats/StatsCollectionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport java.math.BigDecimal\nimport java.sql.Date\nimport java.time.LocalDateTime\n\n// scalastyle:off import.ordering.noEmptyLine\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaColumnMapping\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.schema.SchemaUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.stats.StatisticsCollection.{ASCII_MAX_CHARACTER, UTF8_MAX_CHARACTER}\nimport org.apache.spark.sql.delta.test.{DeltaExceptionTestUtils, DeltaSQLCommandTest, DeltaSQLTestUtils, TestsStatistics}\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{FileNames, JsonUtils}\nimport org.apache.hadoop.fs.Path\nimport org.scalatest.exceptions.TestFailedException\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.{GenericRow, GenericRowWithSchema}\nimport org.apache.spark.sql.functions._\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.types.{IntegerType, MetadataBuilder, StringType, StructType}\n\nclass StatsCollectionSuite\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaColumnMappingTestUtils\n    with TestsStatistics\n    with DeltaSQLCommandTest\n    with DeltaSQLTestUtils\n    with DeletionVectorsTestUtils\n    with DeltaExceptionTestUtils {\n\n  import testImplicits._\n\n\n  test(\"on write\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      val data = Seq(1, 2, 3).toDF().coalesce(1)\n      data.write.format(\"delta\").save(dir.getAbsolutePath)\n      val snapshot = deltaLog.update()\n      val statsJson = deltaLog.update().allFiles.head().stats\n\n      // convert data schema to physical name if possible\n      val dataRenamed = data.toDF(\n        data.columns.map(name => getPhysicalName(name, deltaLog.snapshot.schema)): _*)\n\n      val skipping = new StatisticsCollection {\n        override val spark = StatsCollectionSuite.this.spark\n        override def tableSchema: StructType = dataRenamed.schema\n        override def outputTableStatsSchema: StructType = dataRenamed.schema\n        override def outputAttributeSchema: StructType = dataRenamed.schema\n        override val statsColumnSpec = DeltaStatsColumnSpec(\n          None,\n          Some(\n            DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.fromString(\n              DeltaConfigs.DATA_SKIPPING_NUM_INDEXED_COLS.defaultValue)\n          )\n        )\n        override def columnMappingMode: DeltaColumnMappingMode = deltaLog.snapshot.columnMappingMode\n        override val protocol: Protocol = snapshot.protocol\n        override def getDataSkippingStringPrefixLength: Int =\n          StatsCollectionUtils.getDataSkippingStringPrefixLength(spark, snapshot.metadata)\n      }\n\n      val correctAnswer = dataRenamed\n        .select(skipping.statsCollector)\n        .select(to_json($\"stats\").as[String])\n        .collect()\n        .head\n\n      assert(statsJson === correctAnswer)\n    }\n  }\n\n  test(\"gather stats\") {\n    withTempDir { dir =>\n      val deltaLog = DeltaLog.forTable(spark, dir)\n\n      val data = spark.range(1, 10, 1, 10).withColumn(\"odd\", $\"id\" % 2 === 1)\n      data.write.partitionBy(\"odd\").format(\"delta\").save(dir.getAbsolutePath)\n\n      val df = spark.read.format(\"delta\").load(dir.getAbsolutePath)\n      withSQLConf(\"spark.sql.parquet.filterPushdown\" -> \"false\") {\n        assert(recordsScanned(df) == 9)\n        assert(recordsScanned(df.where(\"id = 1\")) == 1)\n      }\n    }\n  }\n\n  test(\"statistics re-computation throws error on Delta tables with DVs\") {\n    withDeletionVectorsEnabled() {\n      withTempDir { dir =>\n        val df = spark.range(start = 0, end = 20).toDF().repartition(numPartitions = 4)\n        df.write.format(\"delta\").save(dir.toString())\n\n        spark.sql(s\"DELETE FROM delta.`${dir.toString}` WHERE id in (2, 15)\")\n        val e = intercept[DeltaCommandUnsupportedWithDeletionVectorsException] {\n          val deltaLog = DeltaLog.forTable(spark, dir)\n          StatisticsCollection.recompute(spark, deltaLog)\n        }\n        assert(e.getErrorClass == \"DELTA_UNSUPPORTED_STATS_RECOMPUTE_WITH_DELETION_VECTORS\")\n        assert(e.getSqlState == \"0AKDD\")\n        assert(e.getMessage ==\n          \"[DELTA_UNSUPPORTED_STATS_RECOMPUTE_WITH_DELETION_VECTORS] \" +\n            \"Statistics re-computation on a Delta table with deletion \" +\n            \"vectors is not yet supported.\")\n      }\n    }\n  }\n\n  statsTest(\"recompute stats basic\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val df = spark.range(2).coalesce(1).toDF()\n        df.write.format(\"delta\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n\n        {\n          StatisticsCollection.recompute(spark, deltaLog)\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        val statsDf = statsDF(deltaLog)\n        assert(statsDf.where('numRecords.isNotNull).count() > 0)\n        // Make sure stats indicate 2 rows, min [0], max [1]\n        checkAnswer(statsDf, Row(2, Row(0), Row(1)))\n      }\n    }\n  }\n\n  statsTestSparkMasterOnly(\"recompute variant stats\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val df = spark.range(2)\n          .selectExpr(\n            \"case when id % 2 = 0 then parse_json(cast(id as string)) else null end as v\"\n          )\n          .coalesce(1)\n          .toDF()\n        df.write.format(\"delta\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n\n        assert(getStatsDf(deltaLog, Seq($\"numRecords\")).where('numRecords.isNotNull).count() == 0)\n\n        {\n          StatisticsCollection.recompute(spark, deltaLog)\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        val statsDf = getStatsDf(deltaLog, Seq($\"numRecords\", $\"nullCount\"))\n        assert(statsDf.where('numRecords.isNotNull).count() > 0)\n        // Make sure stats indicate 2 rows, nullCount [1]\n        checkAnswer(statsDf, Row(2, Row(1)))\n      }\n    }\n  }\n\n  statsTest(\"recompute stats multiple columns and files\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val df = spark.range(10, 20).withColumn(\"x\", 'id + 10).repartition(3)\n\n        df.write.format(\"delta\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n\n        {\n          StatisticsCollection.recompute(spark, deltaLog)\n        }\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        val statsDf = statsDF(deltaLog)\n        assert(statsDf.where('numRecords.isNotNull).count() > 0)\n        // scalastyle:off line.size.limit\n        val expectedStats = Seq(Row(3, Row(10, 20), Row(19, 29)), Row(4, Row(12, 22), Row(17, 27)), Row(3, Row(11, 21), Row(18, 28)))\n        // scalastyle:on line.size.limit\n        checkAnswer(statsDf, expectedStats)\n      }\n    }\n  }\n\n  statsTest(\"recompute stats on partitioned table\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val df = spark.range(15).toDF(\"a\")\n          .withColumn(\"b\", 'a % 3)\n          .withColumn(\"c\", 'a % 2)\n          .repartition(3, 'b)\n\n        df.write.format(\"delta\").partitionBy(\"b\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n\n        {\n          StatisticsCollection.recompute(spark, deltaLog)\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        val statsDf = statsDF(deltaLog)\n        assert(statsDf.where('numRecords.isNotNull).count() > 0)\n        checkAnswer(statsDf, Seq(\n          Row(5, Row(1, 0), Row(13, 1)),\n          Row(5, Row(0, 0), Row(12, 1)),\n          Row(5, Row(2, 0), Row(14, 1))))\n      }\n    }\n  }\n\n  statsTest(\"recompute stats with partition predicates\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val df = Seq(\n          (1, 0, 10), (1, 2, 20), (1, 4, 30), (2, 6, 40), (2, 8, 50), (3, 10, 60), (4, 12, 70))\n          .toDF(\"a\", \"b\", \"c\")\n\n        df.write.format(\"delta\").partitionBy(\"a\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n\n        {\n          StatisticsCollection.recompute(spark, deltaLog, Seq(('a > 1).expr, ('a < 4).expr))\n        }\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        val statsDf = statsDF(deltaLog)\n        assert(statsDf.where('numRecords.isNotNull).count() == 2)\n        checkAnswer(statsDf, Seq(\n          Row(null, Row(null, null), Row(null, null)),\n          Row(2, Row(6, 40), Row(8, 50)),\n          Row(1, Row(10, 60), Row(10, 60)),\n          Row(null, Row(null, null), Row(null, null))))\n      }\n    }\n  }\n\n  statsTest(\"recompute stats with invalid partition predicates\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        Seq((1, 0, 10), (1, 2, 20), (1, 4, 30), (2, 6, 40), (2, 8, 50), (3, 10, 60), (4, 12, 70))\n          .toDF(\"a\", \"b\", \"c\")\n          .write.format(\"delta\").partitionBy(\"a\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n\n        {\n          intercept[AnalysisException] {\n            StatisticsCollection.recompute(spark, deltaLog, Seq(('b > 1).expr))\n          }\n          intercept[AnalysisException] {\n            StatisticsCollection.recompute(spark, deltaLog, Seq(('a > 1).expr, ('c > 1).expr))\n          }\n        }\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n      }\n    }\n  }\n\n  statsTest(\"recompute stats on a table with corrupted stats\") {\n    withTempDir { tempDir =>\n      val df = Seq(\n        (1, 0, 10), (1, 2, 20), (1, 4, 30), (2, 6, 40), (2, 8, 50), (3, 10, 60), (4, 12, 70))\n        .toDF(\"a\", \"b\", \"c\")\n\n      df.write.format(\"delta\").partitionBy(\"a\").save(tempDir.toString())\n      val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n      val correctStats = statsDF(deltaLog)\n      assert(correctStats.where('numRecords.isNotNull).count() == 4)\n\n      // use physical names if possible\n      val (a, b, c) = (\n        getPhysicalName(\"a\", deltaLog.snapshot.schema),\n        getPhysicalName(\"b\", deltaLog.snapshot.schema),\n        getPhysicalName(\"c\", deltaLog.snapshot.schema)\n      )\n\n      {\n        // Corrupt stats on one of the files\n        val txn = deltaLog.startTransaction()\n        val f = deltaLog.snapshot.allFiles.filter(_.partitionValues(a) == \"1\").first()\n        val corrupted = f.copy(stats = f.stats.replace(\n          s\"\"\"maxValues\":{\"$b\":4,\"$c\":30}\"\"\",\n          s\"\"\"maxValues\":{\"$b\":-100,\"$c\":100}\"\"\"))\n        txn.commit(Seq(corrupted), DeltaOperations.ComputeStats(Nil))\n        intercept[TestFailedException] {\n          checkAnswer(statsDF(deltaLog), correctStats)\n        }\n\n        // Recompute stats and verify they match the original ones\n        StatisticsCollection.recompute(spark, deltaLog)\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        checkAnswer(statsDF(deltaLog), correctStats)\n      }\n    }\n  }\n\n  statsTest(\"recompute stats with file filter\") {\n    withTempDir { tempDir =>\n      withSQLConf(DeltaSQLConf.DELTA_COLLECT_STATS.key -> \"false\") {\n        val df = Seq(\n          (1, 0, 10), (1, 2, 20), (1, 4, 30), (2, 6, 40), (2, 8, 50), (3, 10, 60), (4, 12, 70))\n          .toDF(\"a\", \"b\", \"c\")\n\n        df.write.format(\"delta\").partitionBy(\"a\").save(tempDir.toString())\n        val deltaLog = DeltaLog.forTable(spark, new Path(tempDir.getCanonicalPath))\n        assert(statsDF(deltaLog).where('numRecords.isNotNull).count() == 0)\n\n        val biggest = deltaLog.snapshot.allFiles.agg(max('size)).first().getLong(0)\n\n        {\n          StatisticsCollection.recompute(\n            spark, deltaLog, catalogTable = None, fileFilter = _.size == biggest)\n        }\n\n        checkAnswer(\n          spark.read.format(\"delta\").load(tempDir.getCanonicalPath),\n          df\n        )\n        val statsDf = statsDF(deltaLog)\n        assert(statsDf.where('numRecords.isNotNull).count() == 1)\n        checkAnswer(statsDf, Seq(\n          Row(null, Row(null, null), Row(null, null)),\n          Row(null, Row(null, null), Row(null, null)),\n          Row(null, Row(null, null), Row(null, null)),\n          Row(3, Row(0, 10), Row(4, 30))))\n      }\n    }\n  }\n\n  test(\"Truncate min string\") {\n    // scalastyle:off nonascii\n    val inputToExpected = Seq(\n      (s\"abcd\", s\"abc\", 3),\n      (s\"abcdef\", s\"abcdef\", 6),\n      (s\"abcde�\", s\"abcde�\", 6),\n      (s\"$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER\",\n        s\"$UTF8_MAX_CHARACTER\",\n        1),\n      (s\"$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER\", s\"$UTF8_MAX_CHARACTER\", 1),\n      (s\"abcd\", null, 0)\n    )\n\n    inputToExpected.foreach {\n      case (input, expected, prefixLen) =>\n        val actual = StatisticsCollection.truncateMinStringAgg(prefixLen)(input)\n        val debugMsg = s\"input:$input, actual:$actual, expected:$expected\"\n        assert(actual == expected, debugMsg)\n        if (actual != null) {\n          assert(input.startsWith(actual), debugMsg)\n        }\n    }\n    // scalastyle:on nonascii\n  }\n\n  test(\"Truncate max string\") {\n    // scalastyle:off nonascii\n    val inputToExpected = Seq(\n      (s\"abcd\", null, 0),\n      (s\"a${UTF8_MAX_CHARACTER}d\", s\"a$UTF8_MAX_CHARACTER$ASCII_MAX_CHARACTER\", 2),\n      (s\"abcd\", s\"abcd\", 6),\n      (s\"abcdef\", s\"abcdef\", 6),\n      (s\"abcde�\", s\"abcde�\", 6),\n      (s\"abcd�abcd\", s\"abcd�a$ASCII_MAX_CHARACTER\", 6),\n      (s\"�abcd\", s\"�abcd\", 6),\n      (s\"abcdef�\", s\"abcdef$UTF8_MAX_CHARACTER\", 6),\n      (s\"abcdef��\", s\"abcdef$UTF8_MAX_CHARACTER\", 6),\n      (s\"abcdef-abcdef�\", s\"abcdef$ASCII_MAX_CHARACTER\", 6),\n      (s\"abcdef�abcdef\", s\"abcdef$UTF8_MAX_CHARACTER\", 6),\n      (s\"abcde�abcdef�abcdef�abcdef\", s\"abcde�$ASCII_MAX_CHARACTER\", 6),\n      (s\"漢字仮名한글தமி\", s\"漢字仮名한글$UTF8_MAX_CHARACTER\", 6),\n      (s\"漢字仮名한글��\", s\"漢字仮名한글$UTF8_MAX_CHARACTER\", 6),\n      (s\"漢字仮名한글\", s\"漢字仮名한글\", 6),\n      (s\"abcdef🚀\", s\"abcdef$UTF8_MAX_CHARACTER\", 6),\n      (s\"$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER\", null, 1),\n      (s\"$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER\",\n        s\"$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER$UTF8_MAX_CHARACTER\",\n        4),\n      (s\"����\", s\"��$UTF8_MAX_CHARACTER\", 2),\n      (s\"���\", s\"�$UTF8_MAX_CHARACTER\", 1),\n      (\"abcdefghijklm💞😉💕\\n🥀🌹💐🌺🌷🌼🌻🌷🥀\",\n        s\"abcdefghijklm💞😉💕\\n🥀🌹💐🌺🌷🌼$UTF8_MAX_CHARACTER\",\n        32)\n    )\n\n    inputToExpected.foreach {\n      case (input, expected, prefixLen) =>\n        val actual = StatisticsCollection.truncateMaxStringAgg(prefixLen)(input)\n        // `Actual` should be higher or equal than `input` in UTF-8 encoded binary order.\n        val debugMsg = s\"input:$input, actual:$actual, expected:$expected\"\n        assert(actual == expected, debugMsg)\n    }\n    // scalastyle:off nonascii\n  }\n\n\n  test(s\"Optimize Zorder for delta statistics column: table creation\") {\n    val tableName = \"delta_table\"\n    withTable(tableName) {\n      sql(\"create table delta_table (c1 long, c2 long) \" +\n        \"using delta \" +\n        \"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1,c2', \" +\n        \"'delta.dataSkippingNumIndexedCols' = 0)\")\n      for (_ <- 1 to 10) {\n        sql(\"insert into delta_table values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8)\")\n      }\n      sql(\"optimize delta_table zorder by (c1)\")\n      sql(\"optimize delta_table zorder by (c2)\")\n      sql(\"optimize delta_table zorder by (c1,c2)\")\n    }\n  }\n\n  test(s\"Optimize Zorder for delta statistics column: alter TBLPROPERTIES\") {\n    val tableName = \"delta_table\"\n    withTable(tableName) {\n      sql(\"create table delta_table (c1 long, c2 long) \" +\n        \"using delta TBLPROPERTIES('delta.dataSkippingNumIndexedCols' = 0)\")\n      intercept[DeltaAnalysisException] { sql(\"optimize delta_table zorder by (c1)\") }\n      intercept[DeltaAnalysisException] { sql(\"optimize delta_table zorder by (c2)\") }\n      intercept[DeltaAnalysisException] { sql(\"optimize delta_table zorder by (c1,c2)\") }\n      for (_ <- 1 to 10) {\n        sql(\"insert into delta_table values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8)\")\n      }\n      sql(\"ALTER TABLE delta_table SET TBLPROPERTIES ('delta.dataSkippingStatsColumns' = 'c1,c2')\")\n      sql(\"optimize delta_table zorder by (c1)\")\n      sql(\"optimize delta_table zorder by (c2)\")\n      sql(\"optimize delta_table zorder by (c1,c2)\")\n    }\n  }\n\n  test(s\"Delta statistic column: special characters\") {\n    val tableName = \"delta_table_1\"\n    withTable(tableName) {\n      sql(\n        s\"create table $tableName (`c1.` long, `c2*` long, `c3,` long, `c-4` long) using delta \" +\n        s\"TBLPROPERTIES(\" +\n        s\"'delta.dataSkippingStatsColumns'='`c1.`,`c2*`,`c3,`,`c-4`',\" +\n        s\"'delta.columnMapping.mode' = 'name')\"\n      )\n      val dataSkippingStatsColumns = sql(s\"SHOW TBLPROPERTIES $tableName\")\n        .collect()\n        .map { row => row.getString(0) -> row.getString(1) }\n        .filter(_._1 == \"delta.dataSkippingStatsColumns\")\n        .toSeq\n      val result1 = Seq((\"delta.dataSkippingStatsColumns\", \"`c1.`,`c2*`,`c3,`,`c-4`\"))\n      assert(dataSkippingStatsColumns == result1)\n    }\n  }\n\n  Seq(\"c1.\", \"c2*\", \"c3,\", \"c-4\").foreach { col =>\n    test(s\"Delta statistic column: invalid special characters $col\") {\n      val tableName = \"delta_table_1\"\n      withTable(tableName) {\n        val except = intercept[Exception] {\n          sql(\n            s\"create table $tableName (`c1.` long, `c2*` long, `c3,` long, c4 long) using delta \" +\n            s\"TBLPROPERTIES(\" +\n            s\"'delta.dataSkippingStatsColumns'='$col',\" +\n            s\"'delta.columnMapping.mode' = 'name')\"\n          )\n        }\n      }\n    }\n  }\n\n  Seq(\n    (\"BINARY\", \"BinaryType\"),\n    (\"BOOLEAN\", \"BooleanType\"),\n    (\"ARRAY<TINYINT>\", \"ArrayType(ByteType,true)\"),\n    (\"MAP<DATE, INT>\", \"MapType(DateType,IntegerType,true)\")\n  ).foreach { case (invalidType, typename) =>\n    val tableName1 = \"delta_table_1\"\n    val tableName2 = \"delta_table_2\"\n    test(s\"Delta statistic column: invalid data type $invalidType\") {\n      withTable(tableName1, tableName2) {\n        val columnName = \"c2\"\n        val exceptOne = intercept[DeltaIllegalArgumentException] {\n          sql(\n            s\"create table $tableName1 (c1 long, c2 $invalidType) using delta \" +\n            s\"TBLPROPERTIES('delta.dataSkippingStatsColumns'='c2')\"\n          )\n        }\n        assert(\n          exceptOne.getErrorClass == \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_TYPE\" &&\n          exceptOne.getMessageParametersArray.toSeq == Seq(columnName, typename)\n        )\n        sql(s\"create table $tableName2 (c1 long, c2 $invalidType) using delta\")\n        val exceptTwo = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n          sql(s\"ALTER TABLE $tableName2 SET TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c2')\")\n        }\n        assert(\n          exceptTwo.getErrorClass == \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_TYPE\" &&\n          exceptTwo.getMessageParametersArray.toSeq == Seq(columnName, typename)\n        )\n      }\n    }\n  }\n\n  test(s\"Delta statistic column: mix case column name\") {\n    val tableName = \"delta_table_1\"\n    withTable(tableName) {\n      sql(\n        s\"create table $tableName (cOl1 LONG, COL2 struct<COL20 INT, CoL21 LONG>, CoL3 LONG) \" +\n        s\"using delta TBLPROPERTIES\" +\n        s\"('delta.dataSkippingStatsColumns' = 'coL1, COL2.col20, COL2.col21, cOl3');\"\n      )\n      (1 to 10).foreach { _ =>\n        sql(\n          s\"\"\"insert into $tableName values\n             |(1, struct(1, 10), 1), (2, struct(2, 20), 2), (3, struct(3, 30), 3),\n             |(4, struct(4, 40), 4), (5, struct(5, 50), 5), (6, struct(6, 60), 6),\n             |(7, struct(7, 70), 7), (8, struct(8, 80), 8), (9, struct(9, 90), 9),\n             |(10, struct(10, 100), 10), (null, struct(null, null), null),\n             |(-1, struct(-1, -100), -1), (null, struct(null, null), null);\"\"\".stripMargin\n        )\n      }\n      sql(s\"optimize $tableName\")\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val df = deltaLog.update().withStatsDeduplicated\n      val analyzedDfPlan = df.queryExecution.analyzed.toString\n      val stats = if (analyzedDfPlan.indexOf(\"stats_parsed\") > 0) \"stats_parsed\" else \"stats\"\n      df.select(s\"$stats.numRecords\", s\"$stats.nullCount\", s\"$stats.minValues\", s\"$stats.maxValues\")\n        .collect()\n        .foreach { row =>\n          assert(row(0) == 130)\n          assert(row(1).asInstanceOf[GenericRow] == Row(20, Row(20, 20), 20))\n          assert(row(2) == Row(-1, Row(-1, -100), -1))\n          assert(row(3) == Row(10, Row(10, 100), 10))\n        }\n    }\n  }\n\n  Seq(\n    \"BIGINT\", \"DATE\", \"DECIMAL(3, 2)\", \"DOUBLE\", \"FLOAT\", \"INT\", \"SMALLINT\", \"STRING\",\n    \"TIMESTAMP\", \"TIMESTAMP_NTZ\", \"TINYINT\", \"STRUCT<c3: BIGINT>\",\n    \"STRUCT<c3: BIGINT, c4: ARRAY<BIGINT>>\"\n  ).foreach { validType =>\n    val tableName1 = \"delta_table_1\"\n    val tableName2 = \"delta_table_2\"\n    test(s\"Delta statistic column: valid data type $validType\") {\n      withTable(tableName1, tableName2) {\n        sql(\n          s\"create table $tableName1 (c1 long, c2 $validType) using delta \" +\n          s\"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c2')\"\n        )\n        sql(s\"create table $tableName2 (c1 long, c2 $validType) using delta\")\n        sql(s\"ALTER TABLE $tableName2 SET TBLPROPERTIES('delta.dataSkippingStatsColumns'='c2')\")\n      }\n    }\n\n    test(s\"Delta statistic column: valid data type $validType in nested column\") {\n      val tableName3 = \"delta_table_3\"\n      val tableName4 = \"delta_table_4\"\n      withTable(tableName1, tableName2, tableName3, tableName4) {\n        sql(\n          s\"create table $tableName1 (c1 long, c2 STRUCT<c20:INT, c21:$validType>) \" +\n          s\"using delta TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c2.c21')\"\n        )\n        sql(\n          s\"create table $tableName2 (c1 long, c2 STRUCT<c20:INT, c21:$validType>) \" +\n          s\"using delta TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c2')\"\n        )\n        sql(s\"create table $tableName3 (c1 long, c2 STRUCT<c20:INT, c21:$validType>) using delta\")\n        sql(s\"ALTER TABLE $tableName3 SET TBLPROPERTIES('delta.dataSkippingStatsColumns'='c2.c21')\")\n        sql(s\"create table $tableName4 (c1 long, c2 STRUCT<c20:INT, c21:$validType>) using delta\")\n        sql(s\"ALTER TABLE $tableName4 SET TBLPROPERTIES('delta.dataSkippingStatsColumns'='c2')\")\n      }\n    }\n  }\n\n  Seq(\"create\", \"alter\").foreach { label =>\n    val tableName = \"delta_table\"\n    val propertyName = \"delta.dataSkippingStatsColumns\"\n    test(s\"Delta statistics column with partition column: $label\") {\n      withTable(tableName) {\n        if (label == \"create\") {\n          val except = intercept[DeltaIllegalArgumentException] {\n            sql(\n              \"create table delta_table(c0 int, c1 int) using delta partitioned by(c1) \" +\n              \"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1')\"\n            )\n          }\n          assert(\n            except.getErrorClass == \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_PARTITIONED_COLUMN\" &&\n            except.getMessageParametersArray.toSeq == Seq(\"c1\")\n          )\n        } else {\n          sql(\"create table delta_table(c0 int, c1 int) using delta partitioned by(c1)\")\n          val except = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n            sql(\n              \"ALTER TABLE delta_table SET TBLPROPERTIES ('delta.dataSkippingStatsColumns' = 'c1')\"\n            )\n          }\n          assert(\n            except.getErrorClass == \"DELTA_COLUMN_DATA_SKIPPING_NOT_SUPPORTED_PARTITIONED_COLUMN\" &&\n            except.getMessageParametersArray.toSeq == Seq(\"c1\")\n          )\n        }\n      }\n    }\n\n    test(s\"Rename Nested Columns with delta statistics column: $label\") {\n      withTable(tableName) {\n        if (label == \"create\") {\n          sql(\n            \"create table delta_table (\" +\n            \" id long,\" +\n            \" info STRUCT <title: String, value: long, depart STRUCT <org: long, perf: long>>, \" +\n            \" prev_job STRUCT <title: String, depart STRUCT <org: long, perf: long>>)\" +\n            \" using delta TBLPROPERTIES(\" +\n            s\"'$propertyName' = 'info.title,info.depart.org,info.depart.perf',\" +\n            \"'delta.columnMapping.mode' = 'name', \" +\n            \"'delta.minReaderVersion' = '2', \" +\n            \"'delta.minWriterVersion' = '5')\"\n          )\n        } else {\n          sql(\n            \"create table delta_table (\" +\n            \" id long,\" +\n            \" info STRUCT <title: String, value: long, depart STRUCT <org: long, perf: long>>, \" +\n            \" prev_job STRUCT <title: String, depart STRUCT <org: long, perf: long>>)\" +\n            \" using delta TBLPROPERTIES(\" +\n            \"'delta.columnMapping.mode' = 'name', \" +\n            \"'delta.minReaderVersion' = '2', \" +\n            \"'delta.minWriterVersion' = '5')\"\n          )\n        }\n        if (label == \"alter\") {\n          sql(s\"alter table delta_table set TBLPROPERTIES(\" +\n            s\"'$propertyName' = 'info.title,info.depart.org,info.depart.perf')\")\n        }\n        // Rename nested column leaf.\n        sql(\"ALTER TABLE delta_table RENAME COLUMN info.title TO title_name;\")\n        var dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == propertyName)\n          .toSeq\n        val result1 = Seq((propertyName, \"info.title_name,info.depart.org,info.depart.perf\"))\n        assert(dataSkippingStatsColumns == result1)\n        // Rename nested column root.\n        sql(\"ALTER TABLE delta_table RENAME COLUMN info TO detail\")\n        dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == propertyName)\n          .toSeq\n        val result2 = Seq(\n          (propertyName, \"detail.title_name,detail.depart.org,detail.depart.perf\")\n        )\n        assert(dataSkippingStatsColumns == result2)\n        // Rename nested column intermediate node.\n        sql(\"ALTER TABLE delta_table RENAME COLUMN detail.DEPART TO organization\")\n        dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == propertyName)\n          .toSeq\n        val result3 = Seq(\n          (propertyName, \"detail.title_name,detail.organization.org,detail.organization.perf\")\n        )\n        assert(dataSkippingStatsColumns == result3)\n      }\n    }\n\n    test(s\"Drop Nested Columns with delta statistics column: $label\") {\n      withTable(tableName) {\n        if (label == \"create\") {\n          sql(\n            \"create table delta_table (\" +\n            \" id long, \" +\n            \" info STRUCT <title: String, value: long, depart STRUCT <org: long, perf: long>>, \" +\n            \" prev_job STRUCT <title: String, depart STRUCT <org: long, perf: long>>)\" +\n            \" using delta TBLPROPERTIES(\" +\n            s\"'$propertyName' = \" +\n            \"'info.title,info.depart.org,info.depart.perf,prev_job.title,prev_job.depart.perf', \" +\n            \"'delta.columnMapping.mode' = 'name', \" +\n            \"'delta.minReaderVersion' = '2', \" +\n            \"'delta.minWriterVersion' = '5')\"\n          )\n        } else {\n          sql(\n            \"create table delta_table (\" +\n            \" id long,\" +\n            \" info STRUCT<title: String, value: long, depart STRUCT<org: long, perf: long>>, \" +\n            \" prev_job STRUCT<title: String, depart STRUCT<org: long, perf: long>>)\" +\n            \" using delta TBLPROPERTIES(\" +\n            \"'delta.columnMapping.mode' = 'name', \" +\n            \"'delta.minReaderVersion' = '2', \" +\n            \"'delta.minWriterVersion' = '5')\"\n          )\n        }\n        if (label == \"alter\") {\n          sql(\n            s\"alter table delta_table set TBLPROPERTIES(\" +\n              s\"'$propertyName' = \" +\n              s\"'info.title,info.depart.org,info.depart.perf,prev_job.title,prev_job.depart.perf')\"\n          )\n        }\n        // Drop nested column leaf.\n        sql(\"ALTER TABLE delta_table DROP COLUMN info.title;\")\n        var dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == propertyName)\n          .toSeq\n        val result1 = Seq(\n          (propertyName, \"info.depart.org,info.depart.perf,prev_job.title,prev_job.depart.perf\")\n        )\n        assert(dataSkippingStatsColumns == result1)\n        // Drop nested column intermediate node.\n        sql(\"ALTER TABLE delta_table DROP COLUMN info.depart;\")\n        dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == propertyName)\n          .toSeq\n        val result3 = Seq((propertyName, \"prev_job.title,prev_job.depart.perf\"))\n        assert(dataSkippingStatsColumns == result3)\n\n        // Rename nested column root node.\n        sql(\"ALTER TABLE delta_table DROP COLUMN prev_job;\")\n        dataSkippingStatsColumns = sql(\"SHOW TBLPROPERTIES delta_table\")\n          .collect()\n          .map { row => row.getString(0) -> row.getString(1) }\n          .filter(_._1 == propertyName)\n          .toSeq\n        val result2 = Seq((propertyName, \"\"))\n        assert(dataSkippingStatsColumns == result2)\n      }\n    }\n  }\n\n  test(\"Change Columns with delta statistics column\") {\n    Seq(\n      \"BIGINT\", \"DATE\", \"DECIMAL(3, 2)\", \"DOUBLE\", \"FLOAT\", \"INT\", \"SMALLINT\", \"STRING\",\n      \"TIMESTAMP\", \"TIMESTAMP_NTZ\", \"TINYINT\"\n    ).foreach { validType =>\n      Seq(\n        \"BINARY\", \"BOOLEAN\", \"ARRAY<TINYINT>\", \"MAP<DATE, INT>\", \"STRUCT<c60:INT, c61:ARRAY<INT>>\"\n      ).foreach { invalidType =>\n        withTable(\"delta_table\") {\n          sql(\n            s\"create table delta_table (c0 long, c1 long, c2 $validType) using delta \" +\n            s\"TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c1,c2', \" +\n            \"'delta.columnMapping.mode' = 'name', \" +\n            \"'delta.minReaderVersion' = '2', \" +\n            \"'delta.minWriterVersion' = '5')\"\n          )\n          intercept[AnalysisException] {\n            sql(s\"ALTER TABLE delta_table Change c2 TYPE $invalidType;\")\n          }\n        }\n      }\n    }\n  }\n\n  test(\"Duplicated delta statistic columns: create\") {\n    Seq(\n      (\"'c0,c0'\", \"c0\"),\n      (\"'c1,c1.c11'\", \"c1.c11\"),\n      (\"'c1.c11,c1.c11'\", \"c1.c11\"),\n      (\"'c1,c1'\", \"c1.c11,c1.c12\")\n    ).foreach { case (statsColumns, duplicatedColumns) =>\n      val exception = intercept[DeltaIllegalArgumentException] {\n        sql(\n          s\"create table delta_table (c0 long, c1 struct<c11: long, c12 long>) using delta \" +\n          s\"TBLPROPERTIES('delta.dataSkippingStatsColumns' = $statsColumns, \" +\n          \"'delta.columnMapping.mode' = 'name')\"\n        )\n      }\n      assert(\n        exception.getErrorClass == \"DELTA_DUPLICATE_DATA_SKIPPING_COLUMNS\" &&\n        exception.getMessageParametersArray.toSeq == Seq(duplicatedColumns)\n      )\n    }\n  }\n\n  test(\"Duplicated delta statistic columns: alter\") {\n    sql(\n      s\"create table delta_table_t1 (c0 long, c1 struct<c11: long, c12 long>) using delta \" +\n      s\"TBLPROPERTIES('delta.columnMapping.mode' = 'name')\"\n    )\n    Seq(\n      (\"'c0,c0'\", \"c0\"),\n      (\"'c1,c1.c11'\", \"c1.c11\"),\n      (\"'c1.c11,c1.c11'\", \"c1.c11\"),\n      (\"'c1,c1'\", \"c1.c11,c1.c12\")\n    ).foreach { case (statsColumns, duplicatedColumns) =>\n      val exception = interceptWithUnwrapping[DeltaIllegalArgumentException] {\n        sql(\n          s\"ALTER TABLE delta_table_t1 \" +\n          s\"SET TBLPROPERTIES('delta.dataSkippingStatsColumns'=$statsColumns)\"\n        )\n      }\n      assert(\n        exception.getErrorClass == \"DELTA_DUPLICATE_DATA_SKIPPING_COLUMNS\" &&\n        exception.getMessageParametersArray.toSeq == Seq(duplicatedColumns)\n      )\n    }\n  }\n\n  test(\"handle special nested characters in column name\") {\n    withTable(\"t\") {\n      sql(\n        s\"create table t (`|` long, c struct<s struct<a int, b INT>, `s.a` int>) using delta \" +\n          s\"TBLPROPERTIES('delta.columnMapping.mode' = 'name')\")\n      // Have this test to make sure there are no collisions with c.`s.a` and c.s.a.\n      sql(\n        s\"ALTER TABLE t SET TBLPROPERTIES('delta.dataSkippingStatsColumns' = 'c')\")\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(\"t\"))\n      val tblProperty = DeltaConfigs.DATA_SKIPPING_STATS_COLUMNS\n        .fromMetaData(deltaLog.update().metadata)\n      assert(tblProperty.get == \"c\")\n    }\n  }\n\n  Seq(\"name\", \"id\").foreach { mappingModeName =>\n    val mappingMode = if (mappingModeName == \"name\") NameMapping else IdMapping\n    test(s\"Throw ColumnMappingException when missing physical name\" +\n        s\" - mappingModeName: $mappingModeName\") {\n      val fieldWithPhysicalName = StructField(\n        name = \"testColumn\",\n        dataType = StringType,\n        nullable = true,\n        metadata = new MetadataBuilder()\n          .putString(DeltaColumnMapping.COLUMN_MAPPING_PHYSICAL_NAME_KEY, \"col_12345_physical\")\n          .build())\n\n      val fieldWithoutPhysicalName = StructField(\n        name = \"testColumn\",\n        dataType = StringType,\n        nullable = true,\n        metadata = new MetadataBuilder().build())\n\n      // Use empty schemaNames so that schemaNames.contains(fullPath) returns false.\n      // This forces the function to proceed to the physical name check.\n      val emptySchemaNames = Seq.empty[String]\n      val fullPath = \"testColumn\"\n\n      val goodResult = StatisticsCollection.convertToPhysicalName(\n        fullPath = fullPath,\n        field = fieldWithPhysicalName,\n        schemaNames = emptySchemaNames,\n        mappingMode = mappingMode)\n\n      assert(goodResult.name == \"col_12345_physical\")\n      assert(goodResult.dataType == StringType)\n\n      // Test with a field that does not have a physical name, expect ColumnMappingException.\n      val exception = intercept[ColumnMappingException] {\n        StatisticsCollection.convertToPhysicalName(\n          fullPath = fullPath,\n          field = fieldWithoutPhysicalName,\n          schemaNames = emptySchemaNames,\n          mappingMode = mappingMode)\n      }\n\n      // Verify the exception contains the expected message and correct mapping mode\n      assert(exception.msg.contains(s\"Missing physical name in column mapping mode \" +\n        s\"`$mappingModeName`\"))\n      assert(exception.mode == mappingMode)\n    }\n  }\n\n  private def recordsScanned(df: DataFrame): Long = {\n    val scan = df.queryExecution.executedPlan.find {\n      case FileScanExecNode(_) => true\n      case _ => false\n    }.get\n\n    var executedScan = false\n\n    if (!executedScan) {\n      if (scan.supportsColumnar) {\n        scan.executeColumnar().count()\n      } else {\n        scan.execute().count()\n      }\n    }\n    scan.metrics.get(\"numOutputRows\").get.value\n  }\n\n  private def statsDF(deltaLog: DeltaLog): DataFrame = {\n    // use physical name if possible\n    val dataColumns = deltaLog.snapshot.metadata.dataSchema.map(DeltaColumnMapping.getPhysicalName)\n    val minValues = struct(dataColumns.map(c => $\"minValues.$c\"): _*)\n    val maxValues = struct(dataColumns.map(c => $\"maxValues.$c\"): _*)\n    val df = getStatsDf(deltaLog, Seq($\"numRecords\", minValues, maxValues))\n    val numRecordsCol = df.schema.head.name\n    df.withColumnRenamed(numRecordsCol, \"numRecords\")\n  }\n\n  /**\n   * Checks if the min/max values in the collected stats for the given string column are truncated\n   * to the expected length.\n   */\n  private def checkDataSkippingStringPrefixLength(\n      tableName: String,\n      columnName: String,\n      expectedLength: Int,\n      minValueRowContent: String,\n      maxValueRowContent: String): Unit = {\n    val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n    val physicalColumnName = DeltaColumnMapping.getPhysicalName(\n      SchemaUtils.findNestedFieldIgnoreCase(snapshot.schema, Seq(columnName)).get)\n\n    val statsDf = statsDF(deltaLog)\n    val minValue = statsDf\n      .select(min(s\"`struct(minValues.$physicalColumnName)`.$physicalColumnName\"))\n      .collect().head\n      .getString(0)\n    val maxValue = statsDf\n      .select(max(s\"`struct(maxValues.$physicalColumnName)`.$physicalColumnName\"))\n      .collect().head\n      .getString(0)\n\n    assert(minValue == minValueRowContent.take(expectedLength))\n    assert(maxValue == maxValueRowContent.take(expectedLength) + ASCII_MAX_CHARACTER)\n  }\n\n  statsTest(\"Data-skipping-string-prefix-length delta table property override: basic\") {\n    val tableName = \"delta_table\"\n    val strCol = \"strCol\"\n    withTable(tableName) {\n      val (a1000, b1000, c1000) = (\"a\" * 1000, \"b\" * 1000, \"c\" * 1000)\n\n      // Create a table with table property override.\n      sql(\n        s\"\"\"\n           | create table $tableName ($strCol string) using delta tblproperties\n           | ('${DeltaConfigs.DATA_SKIPPING_STRING_PREFIX_LENGTH.key}' = '64')\n           |\"\"\".stripMargin)\n      sql(s\"insert into $tableName values ('$a1000'), ('$b1000'), ('$c1000')\")\n      checkDataSkippingStringPrefixLength(\n        tableName,\n        columnName = strCol,\n        expectedLength = 64,\n        minValueRowContent = a1000,\n        maxValueRowContent = c1000\n      )\n    }\n  }\n\n  statsTest(\"Data-skipping-string-prefix-length delta table property override: recompute stats\") {\n    val tableName = \"delta_table\"\n    val strCol = \"strCol\"\n    withTable(tableName) {\n      val (a1000, b1000, c1000) = (\"a\" * 1000, \"b\" * 1000, \"c\" * 1000)\n\n      // Create a table without table property override.\n      sql(s\"create table $tableName ($strCol string) using delta\")\n      sql(s\"insert into $tableName values ('$a1000'), ('$b1000'), ('$c1000')\")\n      checkDataSkippingStringPrefixLength(\n        tableName,\n        columnName = strCol,\n        expectedLength = DeltaSQLConf.DATA_SKIPPING_STRING_PREFIX_LENGTH.defaultValue.get,\n        minValueRowContent = a1000,\n        maxValueRowContent = c1000\n      )\n\n      // Set the table property override and recompute stats.\n      sql(\n        s\"\"\"\n           | alter table $tableName set tblproperties\n           | ('${DeltaConfigs.DATA_SKIPPING_STRING_PREFIX_LENGTH.key}' = '64')\n           | \"\"\".stripMargin)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      StatisticsCollection.recompute(spark, deltaLog)\n      checkDataSkippingStringPrefixLength(\n        tableName,\n        columnName = strCol,\n        expectedLength = 64,\n        minValueRowContent = a1000,\n        maxValueRowContent = c1000\n      )\n    }\n  }\n\n  statsTest(\"Data-skipping-string-prefix-length delta table property override: RTAS\") {\n    val tableName = \"delta_table\"\n    val sourceTableName = \"source_table\"\n    val strCol = \"strCol\"\n    withTable(tableName, sourceTableName) {\n      val (a1000, b1000, c1000) = (\"a\" * 1000, \"b\" * 1000, \"c\" * 1000)\n      val (x1000, y1000, z1000) = (\"x\" * 1000, \"y\" * 1000, \"z\" * 1000)\n\n      // Create a source table.\n      sql(s\"create table $sourceTableName ($strCol string) using delta\")\n      sql(s\"insert into $sourceTableName values ('$a1000'), ('$b1000'), ('$c1000')\")\n\n      // Create a table without table property override.\n      sql(s\"create table $tableName (strCol string) using delta\")\n      sql(s\"insert into $tableName values ('$x1000'), ('$y1000'), ('$z1000')\")\n      checkDataSkippingStringPrefixLength(\n        tableName,\n        columnName = strCol,\n        expectedLength = DeltaSQLConf.DATA_SKIPPING_STRING_PREFIX_LENGTH.defaultValue.get,\n        minValueRowContent = x1000,\n        maxValueRowContent = z1000\n      )\n\n      // Replace the table with table property override by selecting from the source table.\n      sql(\n        s\"\"\"\n           | replace table $tableName using delta tblproperties\n           | ('${DeltaConfigs.DATA_SKIPPING_STRING_PREFIX_LENGTH.key}' = '64')\n           | as (select * from $sourceTableName)\n           | \"\"\".stripMargin)\n      checkDataSkippingStringPrefixLength(\n        tableName,\n        columnName = strCol,\n        expectedLength = 64,\n        minValueRowContent = a1000,\n        maxValueRowContent = c1000\n      )\n    }\n  }\n\n  statsTest(\"Data-skipping-string-prefix-length delta table property override: Add/Remove Files\") {\n    /**\n     * In the commit JSON file at the given version, checks if the min/max values for the given\n     * string column stored in the Add/Remove File action are truncated to the expected length.\n     */\n    def checkStringPrefixLengthInAction(\n        tableName: String,\n        version: Long,\n        action: String,\n        columnName: String,\n        expectedLength: Int,\n        minValueRowContent: String,\n        maxValueRowContent: String): Unit = {\n      val (deltaLog, snapshot) = DeltaLog.forTableWithSnapshot(spark, TableIdentifier(tableName))\n      val physicalColumnName = DeltaColumnMapping.getPhysicalName(\n        SchemaUtils.findNestedFieldIgnoreCase(snapshot.schema, Seq(columnName)).get)\n      val commit = spark.read.json(FileNames.unsafeDeltaFile(deltaLog.logPath, version).toString)\n      val actionFile = commit.filter(col(action).isNotNull).select(s\"$action.*\")\n      val minMaxStatsSchema = StructType(Seq(\n        StructField(\"minValues\", StructType(Seq(StructField(physicalColumnName, StringType)))),\n        StructField(\"maxValues\", StructType(Seq(StructField(physicalColumnName, StringType))))\n      ))\n      val actionFileWithParsedStats = actionFile.withColumn(\n        \"parsed_stats\", from_json(col(\"stats\"), minMaxStatsSchema))\n      val minValue = actionFileWithParsedStats\n        .select(min(col(s\"parsed_stats.minValues.$physicalColumnName\")))\n        .collect().head.getString(0)\n      val maxValue = actionFileWithParsedStats\n        .select(max(col(s\"parsed_stats.maxValues.$physicalColumnName\")))\n        .collect().head.getString(0)\n\n      assert(minValue == minValueRowContent.take(expectedLength))\n      assert(maxValue == maxValueRowContent.take(expectedLength) + ASCII_MAX_CHARACTER)\n    }\n\n    val tableName = \"delta_table\"\n    val strCol = \"strCol\"\n    withTable(tableName) {\n      // Create a table without table property override.\n      sql(s\"create table $tableName ($strCol string) using delta\")\n\n      val (a1000, b1000, c1000) = (\"a\" * 1000, \"b\" * 1000, \"c\" * 1000)\n\n      sql(s\"insert into $tableName values ('$a1000'), ('$b1000'), ('$c1000')\")\n      // [Add File] Min: \"a\" * 32, Max: \"c\" * 32\n      checkStringPrefixLengthInAction(\n        tableName,\n        version = 1,\n        action = \"add\",\n        columnName = strCol,\n        expectedLength = DeltaSQLConf.DATA_SKIPPING_STRING_PREFIX_LENGTH.defaultValue.get,\n        minValueRowContent = a1000,\n        maxValueRowContent = c1000\n      )\n\n      // Set the table property override.\n      sql(\n        s\"\"\"\n           | alter table $tableName set tblproperties\n           | ('${DeltaConfigs.DATA_SKIPPING_STRING_PREFIX_LENGTH.key}' = '64')\n           | \"\"\".stripMargin)\n\n      val (x1000, y1000, z1000) = (\"x\" * 1000, \"y\" * 1000, \"z\" * 1000)\n\n      sql(s\"insert into $tableName values ('$x1000'), ('$y1000'), ('$z1000')\")\n      // [Add File] Min: \"x\" * 64, Max: \"z\" * 64\n      checkStringPrefixLengthInAction(\n        tableName,\n        version = 3,\n        action = \"add\",\n        columnName = strCol,\n        expectedLength = 64,\n        minValueRowContent = x1000,\n        maxValueRowContent = z1000\n      )\n    }\n  }\n}\n\nclass StatsCollectionNameColumnMappingSuite extends StatsCollectionSuite\n  with DeltaColumnMappingEnableNameMode {\n\n  override protected def runOnlyTests = Seq(\n    \"on write\",\n    \"recompute stats with partition predicates\"\n  )\n}\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/stats/StatsUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.stats\n\nimport org.apache.spark.sql.delta.DeltaTable\n\nimport org.apache.spark.sql.DataFrame\n\ntrait StatsUtils {\n  protected def getStats(df: DataFrame): DeltaScan = {\n    val stats = df.queryExecution.optimizedPlan.collect {\n      case DeltaTable(prepared: PreparedDeltaFileIndex) =>\n        prepared.preparedScan\n    }\n    if (stats.size != 1) sys.error(s\"Found ${stats.size} scans!\")\n    stats.head\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/storage/LineClosableIteratorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage\n\nimport java.io.{Reader, StringReader}\n\nimport org.apache.spark.SparkFunSuite\n\nabstract class LineClosableIteratorSuiteBase extends SparkFunSuite {\n\n  protected def createIter(_reader: Reader): ClosableIterator[String]\n\n  test(\"empty\") {\n    var iter = createIter(new StringReader(\"\"))\n    assert(!iter.hasNext)\n    intercept[NoSuchElementException] { iter.next() }\n\n    iter = createIter(new StringReader(\"\"))\n    intercept[NoSuchElementException] { iter.next() }\n\n    iter = createIter(new StringReader(\"\"))\n    iter.close()\n    intercept[IllegalStateException] { iter.hasNext }\n    intercept[IllegalStateException] { iter.next() }\n  }\n\n  test(\"one elem\") {\n    var iter = createIter(new StringReader(\"foo\"))\n    assert(iter.hasNext)\n    assert(iter.next() == \"foo\")\n    assert(!iter.hasNext)\n    intercept[NoSuchElementException] { iter.next() }\n\n    iter = createIter(new StringReader(\"foo\"))\n    assert(iter.next() == \"foo\")\n    intercept[NoSuchElementException] { iter.next() }\n\n    iter = createIter(new StringReader(\"foo\"))\n    iter.close()\n    intercept[IllegalStateException] { iter.hasNext }\n    intercept[IllegalStateException] { iter.next() }\n  }\n\n  test(\"two elems\") {\n    var iter = createIter(new StringReader(\"foo\\nbar\"))\n    assert(iter.hasNext)\n    assert(iter.next() == \"foo\")\n    assert(iter.hasNext)\n    assert(iter.next() == \"bar\")\n    assert(!iter.hasNext)\n    intercept[NoSuchElementException] { iter.next() }\n\n    iter = createIter(new StringReader(\"foo\\nbar\"))\n    assert(iter.next() == \"foo\")\n    assert(iter.next() == \"bar\")\n    intercept[NoSuchElementException] { iter.next() }\n\n    iter = createIter(new StringReader(\"foo\\nbar\"))\n    assert(iter.next() == \"foo\")\n    iter.close()\n    intercept[IllegalStateException] { iter.hasNext }\n    intercept[IllegalStateException] { iter.next() }\n\n    iter = createIter(new StringReader(\"foo\\nbar\"))\n    assert(iter.hasNext) // Cache `nextValue`\n    iter.close()\n    // We should throw `IllegalStateException` even if there is a cached `nextValue`.\n    intercept[IllegalStateException] { iter.hasNext }\n    intercept[IllegalStateException] { iter.next() }\n  }\n\n  test(\"close should be called when the iterator reaches the end\") {\n    var closed = false\n    val reader = new StringReader(\"foo\") {\n      override def close(): Unit = {\n        super.close()\n        closed = true\n      }\n    }\n    val iter = createIter(reader)\n    assert(iter.toList == \"foo\" :: Nil)\n    assert(closed)\n  }\n\n  test(\"close should be called when the iterator is closed\") {\n    var closed = false\n    val reader = new StringReader(\"foo\") {\n      override def close(): Unit = {\n        super.close()\n        closed = true\n      }\n    }\n    val iter = createIter(reader)\n    iter.close()\n    assert(closed)\n  }\n\n  test(\"close should be called only once\") {\n    var closed = 0\n    val reader = new StringReader(\"foo\") {\n      override def close(): Unit = {\n        super.close()\n        closed += 1\n      }\n    }\n    val iter = createIter(reader)\n    assert(iter.toList == \"foo\" :: Nil)\n    iter.close()\n    assert(closed == 1)\n  }\n\n  test(\"flatMapWithClose does not open any iterators on creation\") {\n    var opened = 0\n    var closed = 0\n    val outerReader = new StringReader(\"b\\na\\nr\")\n    createIter(outerReader).flatMapWithClose(_ => {\n      val innerReader = new StringReader(\"f\\no\\no\") {\n        opened += 1\n        override def close(): Unit = {\n          super.close()\n          closed += 1\n        }\n      }\n      createIter(innerReader)\n    })\n    assert(opened == 0)\n    assert(closed == 0)\n  }\n\n  test(\"flatMapWithClose calls close only for opened iterators\") {\n    var opened = 0\n    var closed = 0\n    val outerReader = new StringReader(\"b\\na\\nr\")\n    val iter = createIter(outerReader).flatMapWithClose(_ => {\n      val innerReader = new StringReader(\"f\\no\\no\") {\n        opened += 1\n        override def close(): Unit = {\n          super.close()\n          closed += 1\n        }\n      }\n      createIter(innerReader)\n    })\n    assert(iter.take(5).toList == List(\"f\", \"o\", \"o\", \"f\", \"o\"))\n    iter.close()\n    assert(opened == 2)\n    assert(closed == 2)\n  }\n\n  test(\"flatMapWithClose calls close only for opened iterators - iter boundary\") {\n    var opened = 0\n    var closed = 0\n    val outerReader = new StringReader(\"b\\na\\nr\")\n    val iter = createIter(outerReader).flatMapWithClose(_ => {\n      val innerReader = new StringReader(\"f\\no\\no\") {\n        opened += 1\n        override def close(): Unit = {\n          super.close()\n          closed += 1\n        }\n      }\n      createIter(innerReader)\n    })\n    assert(iter.take(3).toList == List(\"f\", \"o\", \"o\"))\n    iter.close()\n    assert(opened == 1)\n    assert(closed == 1)\n  }\n}\n\nclass InternalLineClosableIteratorSuite extends LineClosableIteratorSuiteBase {\n  override protected def createIter(_reader: Reader): ClosableIterator[String] = {\n    new LineClosableIterator(_reader)\n  }\n}\n\nclass PublicLineClosableIteratorSuite extends LineClosableIteratorSuiteBase {\n  override protected def createIter(_reader: Reader): ClosableIterator[String] = {\n    val impl = new io.delta.storage.LineCloseableIterator(_reader)\n    new LineClosableIteratorAdaptor(impl)\n  }\n}\n\nprivate class LineClosableIteratorAdaptor(\n    impl: io.delta.storage.LineCloseableIterator) extends ClosableIterator[String] {\n\n  override def hasNext(): Boolean = impl.hasNext\n\n  override def next(): String = impl.next()\n\n  override def close(): Unit = impl.close()\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/storage/dv/DeletionVectorFileSizeSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage.dv\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor, RemoveFile}\nimport org.apache.spark.sql.delta.deletionvectors.DeletionVectorsSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeletionVectorFileSizeSuite extends QueryTest\n    with SharedSparkSession\n    with DeltaSQLTestUtils\n    with DeltaSQLCommandTest {\n  private def getAddAndRemoveFilesFromCommitVersion(\n      deltaLog: DeltaLog,\n      commitVersion: Long): (Seq[AddFile], Seq[RemoveFile]) = {\n    require(commitVersion <= deltaLog.update().version,\n      \"Commit version should be less than or equal to the current version\")\n    val changes = deltaLog.getChanges(\n      commitVersion, commitVersion, catalogTableOpt = None, failOnDataLoss = true)\n    val (changesItrForAddFiles, changesItrForRemoveFiles) = changes.duplicate\n    val addFiles: Seq[AddFile] =\n       changesItrForAddFiles.flatMap(_._2.collect { case a: AddFile => a }).toSeq\n    val removeFiles: Seq[RemoveFile] =\n       changesItrForRemoveFiles.flatMap(_._2.collect { case r: RemoveFile => r }).toSeq\n    (addFiles, removeFiles)\n  }\n\n  private def getDeletionVectorFilePath(\n    dvDescriptor: DeletionVectorDescriptor,\n    tableDataPath: String\n  ): Path = {\n    val path = dvDescriptor.absolutePath(new Path(tableDataPath))\n    path\n  }\n\n  private def getFileSizeInBytes(\n      absoluteFilePath: Path,\n      hadoopConf: org.apache.hadoop.conf.Configuration): Long = {\n    val file = new File(absoluteFilePath.toString)\n    assert(file.exists())\n\n    val fs = absoluteFilePath.getFileSystem(hadoopConf)\n    val fileStatus = fs.getFileStatus(absoluteFilePath)\n    fileStatus.getLen\n  }\n\n  test(\"Bin Packing should take the size of existing DVs into account\") {\n    withTempDir { tempDir =>\n      val source = new File(DeletionVectorsSuite.table1Path)\n      val target = new File(tempDir, \"deleteTest\")\n\n      // Copy the source table with existing table layout to a temporary directory\n      FileUtils.copyDirectory(source, target)\n\n      val (deltaLog, snapshot) =\n        DeltaLog.forTableWithSnapshot(spark, new Path(target.getAbsolutePath))\n      assert(snapshot.version === 4, \"Table should exist\")\n      // All existing individual DVs either have 34 or 36 bytes, corresponding 1 or 2 deleted rows.\n      val priorAddFiles = snapshot.allFiles.collect()\n      priorAddFiles.forall(a => a.deletionVector == null\n        || (a.deletionVector.sizeInBytes == 34 || a.deletionVector.sizeInBytes == 36))\n      assert(priorAddFiles.count(_.deletionVector != null) === 8,\n        \"8 AddFiles with DVs expected\")\n\n      val targetPackingFileSizeInBytes = 110\n      withSQLConf(\n          DeltaSQLConf.DELETION_VECTOR_PACKING_TARGET_SIZE.key ->\n          targetPackingFileSizeInBytes.toString) {\n        // Delete some rows to trigger the creation of new DVs.\n        sql(s\"DELETE FROM delta.`${target.getAbsolutePath}` WHERE value IN (255, 303, 707, 1905)\")\n        val (addFiles, removeFiles) =\n          getAddAndRemoveFilesFromCommitVersion(deltaLog, deltaLog.update().version)\n        assert(addFiles.size === 3, \"Deletion vector added to 3 different AddFiles\")\n        assert(addFiles.forall(_.deletionVector != null),\n          \"Deletion should have used DVs and not rewrite any files\")\n        val removeFilesPaths = removeFiles.map(_.path).toSet\n        assert(priorAddFiles.filter(a => removeFilesPaths.contains(a.path))\n          .count(_.deletionVector != null) === 2, \"2 of the AddFiles had existing DVs\")\n\n        // Get the file sizes of the DV files added by this commit.\n        val dvFileSizes = addFiles.map(_.deletionVector)\n          .map(dv => getDeletionVectorFilePath(dv, target.getAbsolutePath))\n          .toSet\n          .map(path => getFileSizeInBytes(path, deltaLog.newDeltaHadoopConf()))\n        assert(addFiles.forall(a => a.deletionVector.sizeInBytes <= targetPackingFileSizeInBytes),\n          \"the individual DVs can each fit in their own file if needed\")\n        assert(dvFileSizes.forall(_ <= targetPackingFileSizeInBytes),\n          \"The target file size should be respected when individual DVs fit under the file size\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/storage/dv/DeletionVectorStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.storage.dv\n\nimport java.io.{DataInputStream, DataOutputStream, File}\n\nimport org.apache.spark.sql.delta.{DeltaChecksumException, DeltaConfigs, DeltaLog}\nimport org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArray, RoaringBitmapArrayFormat}\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.dv.DeletionVectorStore.{getTotalSizeOfDVFieldsInFile, CHECKSUM_LEN}\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.PathWithFileSystem\nimport com.google.common.primitives.Ints\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.util.Utils\n\ntrait DeletionVectorStoreSuiteBase\n  extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLCommandTest {\n\n  lazy val dvStore: DeletionVectorStore =\n    DeletionVectorStore.createInstance(newHadoopConf)\n\n  protected def newHadoopConf: Configuration = {\n    // scalastyle:off deltahadoopconfiguration\n    spark.sessionState.newHadoopConf()\n    // scalastyle:on deltahadoopconfiguration\n  }\n\n  // Test bitmaps\n  protected lazy val simpleBitmap = {\n    val data = Seq(1L, 5L, 6L, 7L, 1000L, 8000000L, 8000001L)\n    RoaringBitmapArray(data: _*)\n  }\n\n  protected lazy val simpleBitmap2 = {\n    val data = Seq(78L, 256L, 998L, 1000002L, 22623423L)\n    RoaringBitmapArray(data: _*)\n  }\n\n\n  def withTempHadoopFileSystemPath[T](f: Path => T): T = {\n    val dir: File = Utils.createTempDir()\n    dir.delete()\n    val tempPath = DeletionVectorStore.unescapedStringToPath(dir.toString)\n    try f(tempPath) finally Utils.deleteRecursively(dir)\n  }\n\n  testWithAllSerializationFormats(\"Write simple DV directly to disk\") { serializationFormat =>\n    val readDV =\n      withTempHadoopFileSystemPath { tableDir =>\n        val tableWithFS = PathWithFileSystem.withConf(tableDir, newHadoopConf)\n        val dvPath = dvStore.generateUniqueNameInTable(tableWithFS)\n        val serializedBitmap = simpleBitmap.serializeAsByteArray(serializationFormat)\n        val dvRange = Utils.tryWithResource(dvStore.createWriter(dvPath)) { writer =>\n          writer.write(serializedBitmap)\n        }\n        assert(dvRange.offset === 1) // there's a version id at byte 0\n        assert(dvRange.length === serializedBitmap.length)\n        dvStore.read(dvPath.path, dvRange.offset, dvRange.length)\n      }\n    assert(simpleBitmap === readDV)\n  }\n\n\n  testWithAllSerializationFormats(\"Detect corrupted DV checksum \") { serializationFormat =>\n    withTempHadoopFileSystemPath { tableDir =>\n      val tableWithFS = PathWithFileSystem.withConf(tableDir, newHadoopConf)\n      val dvPath = dvStore.generateUniqueNameInTable(tableWithFS)\n      val dvBytes = simpleBitmap.serializeAsByteArray(serializationFormat)\n      val dvRange = Utils.tryWithResource(dvStore.createWriter(dvPath)) {\n        writer => writer.write(dvBytes)\n      }\n      assert(dvRange.offset === 1) // there's a version id at byte 0\n      assert(dvRange.length === dvBytes.length)\n      // corrupt 1 byte in the middle of the stored DV (after the checksum)\n      corruptByte(dvPath, byteToCorrupt = DeletionVectorStore.CHECKSUM_LEN + dvRange.length / 2)\n      val e = intercept[DeltaChecksumException] {\n        dvStore.read(dvPath.path, dvRange.offset, dvRange.length)\n      }\n      // make sure this is our exception not ChecksumFileSystem's\n      assert(e.getErrorClass == \"DELTA_DELETION_VECTOR_CHECKSUM_MISMATCH\")\n      assert(e.getSqlState == \"XXKDS\")\n      assert(e.getMessage == \"[DELTA_DELETION_VECTOR_CHECKSUM_MISMATCH] \" +\n        \"Could not verify deletion vector integrity, CRC checksum verification failed.\")\n    }\n  }\n\n  testWithAllSerializationFormats(\"Detect corrupted DV size\") { serializationFormat =>\n    withTempHadoopFileSystemPath { tableDir =>\n      val tableWithFS = PathWithFileSystem.withConf(tableDir, newHadoopConf)\n      val dvPath = dvStore.generateUniqueNameInTable(tableWithFS)\n      val dvBytes = simpleBitmap.serializeAsByteArray(serializationFormat)\n      val dvRange = Utils.tryWithResource(dvStore.createWriter(dvPath)) {\n        writer => writer.write(dvBytes)\n      }\n      assert(dvRange.offset === 1) // there's a version id at byte 0\n      assert(dvRange.length === dvBytes.length)\n\n      // Corrupt 1 byte in the part where the serialized DV size is stored.\n      // Format:<Version - 1byte> <SerializedDV Size> <SerializedDV Bytes> <DV Checksum>\n      corruptByte(dvPath, byteToCorrupt = 2)\n      val e = intercept[DeltaChecksumException] {\n        dvStore.read(dvPath.path, dvRange.offset, dvRange.length)\n      }\n      assert(e.getErrorClass == \"DELTA_DELETION_VECTOR_SIZE_MISMATCH\")\n      assert(e.getSqlState == \"XXKDS\")\n      assert(e.getMessage == \"[DELTA_DELETION_VECTOR_SIZE_MISMATCH] \" +\n        \"Deletion vector integrity check failed. Encountered a size mismatch.\")\n    }\n  }\n\n  testWithAllSerializationFormats(\"Multiple DVs in one file\") { serializationFormat =>\n    withTempHadoopFileSystemPath { tableDir =>\n      val tableWithFS = PathWithFileSystem.withConf(tableDir, newHadoopConf)\n      val dvPath = dvStore.generateUniqueNameInTable(tableWithFS)\n      val dvBytes1 = simpleBitmap.serializeAsByteArray(serializationFormat)\n      val dvBytes2 = simpleBitmap2.serializeAsByteArray(serializationFormat)\n      val (dvRange1, dvRange2) = Utils.tryWithResource(dvStore.createWriter(dvPath)) {\n        writer =>\n          (writer.write(dvBytes1), writer.write(dvBytes2))\n      }\n      assert(dvRange1.offset === 1) // there's a version id at byte 0\n      assert(dvRange1.length === dvBytes1.length)\n\n      // DV2 should be written immediately after the DV1\n      val totalDV1Size = getTotalSizeOfDVFieldsInFile(dvBytes1.length)\n      assert(dvRange2.offset === 1 + totalDV1Size) // 1byte for file format version\n      assert(dvRange2.length === dvBytes2.length)\n\n      // Read back DVs from the file and verify\n      assert(dvStore.read(dvPath.path, dvRange1.offset, dvRange1.length) === simpleBitmap)\n      assert(dvStore.read(dvPath.path, dvRange2.offset, dvRange2.length) === simpleBitmap2)\n    }\n  }\n\n  test(\"Exception is thrown for DVDescriptors with invalid maxRowIndex\") {\n    withSQLConf(\n        DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.defaultTablePropertyKey -> \"true\",\n        DeltaSQLConf.DELETE_USE_PERSISTENT_DELETION_VECTORS.key -> true.toString) {\n      withTempDir { dir =>\n        val path = dir.toString\n        spark.range(0, 50, 1, 1).write.format(\"delta\").save(path)\n        val targetTable = io.delta.tables.DeltaTable.forPath(path)\n        val deltaLog = DeltaLog.forTable(spark, path)\n        val tableName = s\"delta.`$path`\"\n        spark.sql(s\"DELETE FROM $tableName WHERE id = 3\")\n        val file = deltaLog.update().allFiles.first()\n        val dvDescriptorWithInvalidRowIndex = file.deletionVector.copy(maxRowIndex = Some(50))\n\n        val e = intercept[DeltaChecksumException] {\n          file.removeRows(\n            dvDescriptorWithInvalidRowIndex,\n            updateStats = false\n          )\n        }\n        assert(e.getErrorClass == \"DELTA_DELETION_VECTOR_INVALID_ROW_INDEX\")\n        assert(e.getSqlState == \"XXKDS\")\n        assert(e.getMessage == \"[DELTA_DELETION_VECTOR_INVALID_ROW_INDEX] \" +\n            \"Deletion vector integrity check failed. Encountered an invalid row index.\")\n      }\n    }\n  }\n\n  /** Helper method to run the test using all DV serialization formats */\n  protected def testWithAllSerializationFormats(name: String)\n      (func: RoaringBitmapArrayFormat.Value => Unit): Unit = {\n    for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n      test(s\"$name - $serializationFormat\") {\n        func(serializationFormat)\n      }\n    }\n  }\n\n  /** Helper to method to simulate data corruption in on-disk DV */\n  private def corruptByte(pathWithFS: PathWithFileSystem, byteToCorrupt: Int): Unit = {\n    val fs = pathWithFS.fs\n    val path = pathWithFS.path\n    val status = fs.getFileStatus(path)\n    val len = Ints.checkedCast(status.getLen)\n\n    val bytes = Utils.tryWithResource(fs.open(path)) { stream =>\n      val reader = new DataInputStream(stream)\n      // readAllBytes is not available in 1.8, yet\n      val buffer = new Array[Byte](len)\n      reader.readFully(buffer)\n      buffer\n    }\n    bytes(byteToCorrupt) = (bytes(byteToCorrupt) + 1).toByte\n    val overwrite = true\n    Utils.tryWithResource(fs.create(path, overwrite)) { stream =>\n      val writer = new DataOutputStream(stream)\n      writer.write(bytes)\n      writer.flush()\n    }\n  }\n}\n\nclass DeletionVectorStoreSuite\n  extends DeletionVectorStoreSuiteBase\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/CustomCatalogs.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport java.util\n\nimport scala.collection.{immutable, mutable}\nimport scala.collection.JavaConverters._\n\nimport org.apache.spark.sql.delta.catalog.{DeltaCatalog, DeltaTableV2}\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport org.apache.spark.sql.{Row, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.analysis.{NamespaceAlreadyExistsException, NoSuchTableException}\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.connector.catalog.{DelegatingCatalogExtension, Identifier, NamespaceChange, SupportsNamespaces, Table, TableCatalog, TableChange, V1Table}\nimport org.apache.spark.sql.connector.expressions.Transform\nimport org.apache.spark.sql.execution.datasources.v2.V2SessionCatalog\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\nimport org.apache.spark.util.Utils\n\n\n/**\n * A Utils class for custom catalog implementations that could be used for testing.\n */\nclass DummyCatalog extends TableCatalog {\n  private val spark: SparkSession = SparkSession.active\n  protected lazy val tempDir: Path = new Path(Utils.createTempDir().getAbsolutePath)\n  // scalastyle:off deltahadoopconfiguration\n  protected lazy val fs: FileSystem =\n    tempDir.getFileSystem(spark.sessionState.newHadoopConf())\n  // scalastyle:on deltahadoopconfiguration\n\n  override def name: String = \"dummy\"\n\n  def getTablePath(tableName: String): Path = {\n    new Path(tempDir.toString + \"/\" + tableName)\n  }\n  override def defaultNamespace(): Array[String] = Array(\"default\")\n\n  override def listTables(namespace: Array[String]): Array[Identifier] = {\n    val status = fs.listStatus(tempDir)\n    status.filter(_.isDirectory).map { dir =>\n      Identifier.of(namespace, dir.getPath.getName)\n    }\n  }\n\n  override def tableExists(ident: Identifier): Boolean = {\n    val tablePath = getTablePath(ident.name())\n    fs.exists(tablePath)\n  }\n\n  override def loadTable(ident: Identifier): Table = {\n    if (!tableExists(ident)) {\n      throw new NoSuchTableException(ident)\n    }\n    val tablePath = getTablePath(ident.name())\n    DeltaTableV2(spark = spark, path = tablePath, catalogTable = Some(createCatalogTable(ident)))\n  }\n\n  override def createTable(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: java.util.Map[String, String]): Table = {\n    val tablePath = getTablePath(ident.name())\n    // Create an empty Delta table on the tablePath\n    val part = partitions.map(_.arguments().head.toString)\n    spark.createDataFrame(List.empty[Row].asJava, schema)\n      .write.format(\"delta\").partitionBy(part: _*).save(tablePath.toString)\n    DeltaTableV2(spark = spark, path = tablePath, catalogTable = Some(createCatalogTable(ident)))\n  }\n\n  override def alterTable(ident: Identifier, changes: TableChange*): Table = {\n    // hack hack: no-op just for testing\n    loadTable(ident)\n  }\n\n  override def dropTable(ident: Identifier): Boolean = {\n    val tablePath = getTablePath(ident.name())\n    try {\n      fs.delete(tablePath, true)\n      true\n    } catch {\n      case _: Exception => false\n    }\n  }\n\n  override def renameTable(oldIdent: Identifier, newIdent: Identifier): Unit = {\n    throw new UnsupportedOperationException(\"Rename table operation is not supported.\")\n  }\n\n  override def initialize(name: String, options: CaseInsensitiveStringMap): Unit = {\n    // Initialize tempDir here\n    if (!fs.exists(tempDir)) {\n      fs.mkdirs(tempDir)\n    }\n  }\n\n  private def createCatalogTable(ident: Identifier): CatalogTable = {\n    val tablePath = getTablePath(ident.name())\n    CatalogTable(\n      identifier = TableIdentifier(ident.name(), defaultNamespace.headOption, Some(name)),\n      tableType = CatalogTableType.MANAGED,\n      storage = CatalogStorageFormat(Some(tablePath.toUri), None, None, None, false, Map.empty),\n      schema = spark.range(0).schema\n    )\n  }\n}\n\n// A dummy catalog that adds additional table storage properties after the table is loaded.\n// It's only used inside `DummySessionCatalog`.\nclass DummySessionCatalogInner extends DelegatingCatalogExtension {\n  override def loadTable(ident: Identifier): Table = {\n    val t = super.loadTable(ident).asInstanceOf[V1Table]\n    V1Table(t.v1Table.copy(\n      storage = t.v1Table.storage.copy(\n        properties = t.v1Table.storage.properties ++ Map(\"fs.myKey\" -> \"val\")\n      )\n    ))\n  }\n}\n\n// A dummy catalog that adds a layer between DeltaCatalog and the Spark SessionCatalog,\n// to attach additional table storage properties after the table is loaded, and generates location\n// for managed tables.\nclass DummySessionCatalog extends TableCatalog {\n  private var deltaCatalog: DeltaCatalog = null\n\n  override def initialize(name: String, options: CaseInsensitiveStringMap): Unit = {\n    val inner = new DummySessionCatalogInner()\n    inner.setDelegateCatalog(new V2SessionCatalog(\n      SparkSession.active.sessionState.catalogManager.v1SessionCatalog))\n    deltaCatalog = new DeltaCatalog()\n    deltaCatalog.setDelegateCatalog(inner)\n  }\n\n  override def name(): String = deltaCatalog.name()\n\n  override def listTables(namespace: Array[String]): Array[Identifier] = {\n    deltaCatalog.listTables(namespace)\n  }\n\n  override def loadTable(ident: Identifier): Table = deltaCatalog.loadTable(ident)\n\n  override def createTable(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: java.util.Map[String, String]): Table = {\n    if (!properties.containsKey(TableCatalog.PROP_EXTERNAL) &&\n      !properties.containsKey(TableCatalog.PROP_LOCATION)) {\n      val newProps = new java.util.HashMap[String, String]\n      newProps.putAll(properties)\n      newProps.put(TableCatalog.PROP_LOCATION, properties.get(\"fakeLoc\"))\n      newProps.put(TableCatalog.PROP_IS_MANAGED_LOCATION, \"true\")\n      deltaCatalog.createTable(ident, schema, partitions, newProps)\n    } else {\n      deltaCatalog.createTable(ident, schema, partitions, properties)\n    }\n  }\n\n  override def alterTable(ident: Identifier, changes: TableChange*): Table = {\n    deltaCatalog.alterTable(ident, changes: _*)\n  }\n\n  override def dropTable(ident: Identifier): Boolean = deltaCatalog.dropTable(ident)\n\n  override def renameTable(oldIdent: Identifier, newIdent: Identifier): Unit = {\n    deltaCatalog.renameTable(oldIdent, newIdent)\n  }\n}\n\n// This catalog always does a CASCADE on DROP SCHEMA ...\nclass DummyCatalogWithNamespace extends DummyCatalog with SupportsNamespaces {\n  private val spark: SparkSession = SparkSession.active\n  // To load a catalog into spark CatalogPlugin calls the Catalog's no-arg constructor and\n  // then Catalog.initialize. To have a consistent state across different invocations\n  // in the same test, this catalog impl uses a hard coded path.\n  override lazy val tempDir: Path = DummyCatalogWithNamespace.catalogDir\n  // scalastyle:off deltahadoopconfiguration\n  override lazy val fs: FileSystem =\n    tempDir.getFileSystem(spark.sessionState.newHadoopConf())\n  // scalastyle:on deltahadoopconfiguration\n\n  // Map each namespace to its metadata\n  protected val namespaces: util.Map[List[String], Map[String, String]] =\n    new util.HashMap[List[String], Map[String, String]]()\n\n  protected val tables: mutable.Map[Array[String], mutable.HashSet[Identifier]] =\n    new mutable.HashMap[Array[String], mutable.HashSet[Identifier]]()\n\n  override def name: String = \"test_catalog\"\n\n  override def getTablePath(tableName: String): Path = {\n    new Path(s\"${tempDir.toString}/$name.$tableName\")\n  }\n\n  override def tableExists(ident: Identifier): Boolean = {\n    val tablePath = getTablePath(ident.toString)\n    fs.exists(tablePath)\n  }\n\n  override def loadTable(ident: Identifier): Table = {\n    if (!tableExists(ident)) {\n      throw new NoSuchTableException(ident)\n    }\n    val tablePath = getTablePath(ident.toString)\n    DeltaTableV2(spark = spark, path = tablePath, catalogTable = Some(createCatalogTable(ident)))\n  }\n\n  override def createTable(\n      ident: Identifier,\n      schema: StructType,\n      partitions: Array[Transform],\n      properties: java.util.Map[String, String]): Table = {\n    val tablePath = getTablePath(ident.toString)\n    // Create an empty Delta table on the tablePath\n    val part = partitions.map(_.arguments().head.toString)\n    spark.createDataFrame(List.empty[Row].asJava, schema)\n      .write.format(\"delta\").partitionBy(part: _*).save(tablePath.toString)\n    val map = tables.getOrElseUpdate(ident.namespace(), new mutable.HashSet[Identifier]())\n    map.add(ident)\n    tables.put(ident.namespace(), map)\n    DeltaTableV2(spark = spark, path = tablePath, catalogTable = Some(createCatalogTable(ident)))\n  }\n\n  override def dropTable(ident: Identifier): Boolean = {\n    val tablePath = getTablePath(ident.toString)\n    try {\n      fs.delete(tablePath, true)\n      true\n    } catch {\n      case _: Exception => false\n    }\n  }\n\n  override def initialize(name: String, options: CaseInsensitiveStringMap): Unit = {\n    // Initialize tempDir here\n    if (!fs.exists(tempDir)) {\n      fs.mkdirs(tempDir)\n    }\n    fs.deleteOnExit(tempDir)\n  }\n\n  private def createCatalogTable(ident: Identifier): CatalogTable = {\n    val tablePath = getTablePath(ident.toString)\n    CatalogTable(\n      identifier = TableIdentifier(ident.toString, defaultNamespace.headOption, Some(name)),\n      tableType = CatalogTableType.MANAGED,\n      storage = CatalogStorageFormat(\n        Some(tablePath.toUri), None, None, None, false, immutable.Map.empty),\n      schema = spark.range(0).schema\n    )\n  }\n\n  override def createNamespace(\n      namespace: Array[String],\n      metadata: util.Map[String, String]): Unit = {\n    Option(namespaces.putIfAbsent(namespace.toList, metadata.asScala.toMap)) match {\n      case Some(_) =>\n        throw new NamespaceAlreadyExistsException(namespace)\n      case _ =>\n      // success\n    }\n  }\n\n  override def alterNamespace(namespace: Array[String], changes: NamespaceChange*): Unit = {\n    throw new UnsupportedOperationException(\"alter namespace metadata is not supported.\")\n  }\n\n  override def dropNamespace(namespace: Array[String], cascade: Boolean): Boolean = {\n    tables.getOrElse(namespace, mutable.HashSet.empty[Identifier]).foreach(dropTable)\n    Option(namespaces.remove(namespace.toList)).isDefined\n  }\n\n  override def namespaceExists(namespace: Array[String]): Boolean = {\n    namespaces.containsKey(namespace.toList)\n  }\n\n  override def listNamespaces(): Array[Array[String]] = {\n    throw new UnsupportedOperationException(\"List namespaces operation is not supported.\")\n  }\n\n  override def listNamespaces(namespace: Array[String]): Array[Array[String]] = {\n    throw new UnsupportedOperationException(\"List namespaces operation is not supported.\")\n  }\n\n  override def loadNamespaceMetadata(namespace: Array[String]): util.Map[String, String] = {\n    new util.HashMap[String, String]()\n  }\n}\n\nobject DummyCatalogWithNamespace {\n  val catalogDir: Path = new Path(Utils.createTempDir().getAbsolutePath)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaColumnMappingSelectedTestMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta.{DeltaColumnMappingTestUtils, DeltaConfigs, NoMapping}\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.scalactic.source.Position\nimport org.scalatest.Tag\nimport org.scalatest.exceptions.TestFailedException\n\nimport org.apache.spark.SparkFunSuite\n\n/**\n * A trait for selective enabling certain tests to run for column mapping modes\n */\ntrait DeltaColumnMappingSelectedTestMixin extends SparkFunSuite\n  with DeltaSQLTestUtils with DeltaColumnMappingTestUtils {\n\n  protected def skipTests: Seq[String] = Seq()\n\n  protected def runOnlyTests: Seq[String] = Seq()\n\n  /**\n   * If true, will run all tests.\n   * Requires that `runOnlyTests` is empty.\n   */\n  protected def runAllTests: Boolean = false\n\n  private val testsRun: mutable.Set[String] = mutable.Set.empty\n\n  override protected def test(\n      testName: String,\n      testTags: Tag*)(testFun: => Any)(implicit pos: Position): Unit = {\n    require(!runAllTests || runOnlyTests.isEmpty,\n      \"If `runAllTests` is true then `runOnlyTests` must be empty\")\n\n    if ((runAllTests || runOnlyTests.contains(testName)) && !skipTests.contains(testName)) {\n      super.test(s\"$testName - column mapping $columnMappingMode mode\", testTags: _*) {\n        testsRun.add(testName)\n        withSQLConf(\n          DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey -> columnMappingMode) {\n          testFun\n        }\n      }\n    } else {\n      super.ignore(s\"$testName - ignored by DeltaColumnMappingSelectedTestMixin\")(testFun)\n    }\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    val missingTests = runOnlyTests.toSet diff testsRun\n    if (missingTests.nonEmpty) {\n      throw new TestFailedException(\n        Some(\"Not all selected column mapping tests were run. Missing: \" +\n          missingTests.mkString(\", \")), None, 0)\n    }\n  }\n\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaExceptionTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport java.util.concurrent.ExecutionException\n\nimport scala.annotation.tailrec\nimport scala.reflect.ClassTag\n\nimport org.scalactic.source.Position\nimport org.scalatest.Assertions.intercept\n\nimport org.apache.spark.SparkException\n\ntrait DeltaExceptionTestUtils {\n\n  /**\n   * Handles a breaking change between Spark 3.5 and Spark Master (4.0) to improve error messaging\n   * in Spark. Previously, in Spark 3.5, when an executor would throw an exception, the driver would\n   * wrap it in a [[SparkException]]. Now, in Spark Master (4.0), the original executor exception is\n   * thrown directly.\n   *\n   * This method, which is Spark-version agnostic, executes [[f]] and unwraps it as needed to return\n   * the desired [[Throwable]] of type [[T]].\n   */\n  def interceptWithUnwrapping[T <: Throwable : ClassTag](\n      f: => Any)(implicit pos: Position): T = {\n    @tailrec\n    def unwrapIfNeeded(t: Throwable): T = {\n      t match {\n        case x: T => x\n        case _: SparkException | _: ExecutionException if t.getCause != null =>\n          unwrapIfNeeded(t.getCause)\n        case _ if t.getCause != null && t.getCause.isInstanceOf[T] =>\n          t.getCause.asInstanceOf[T]\n        case _ =>\n          throw t // allow unrecognized exceptions to directly fail the test\n      }\n    }\n\n    val ex = intercept[Throwable](f)\n    unwrapIfNeeded(ex)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaExcludedTestMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.sql.QueryTest\n\nimport org.scalactic.source.Position\nimport org.scalatest.Tag\n\ntrait DeltaExcludedTestMixin extends QueryTest {\n\n  /** Tests to be ignored by the runner. */\n  override def excluded: Seq[String] = Seq.empty\n\n  protected override def test(testName: String, testTags: Tag*)\n    (testFun: => Any)\n    (implicit pos: Position): Unit = {\n    if (excluded.contains(testName)) {\n      super.ignore(testName, testTags: _*)(testFun)\n    } else {\n      super.test(testName, testTags: _*)(testFun)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaHiveTest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport io.delta.sql.DeltaSparkSessionExtension\nimport org.scalatest.BeforeAndAfterAll\n\nimport org.apache.spark.{SparkContext, SparkFunSuite}\nimport org.apache.spark.sql.classic.SparkSession\nimport org.apache.spark.sql.hive.test.{TestHive, TestHiveContext}\nimport org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}\n\n/**\n * Test utility for initializing a SparkSession with a Hive Client and a Hive Catalog for testing\n * DDL operations. Typical tests leverage an in-memory catalog with a mock catalog client. Here we\n * use real Hive classes.\n */\ntrait DeltaHiveTest extends SparkFunSuite with BeforeAndAfterAll { self: DeltaSQLTestUtils =>\n\n  private var _session: SparkSession = _\n  private var _hiveContext: TestHiveContext = _\n  private var _sc: SparkContext = _\n\n  override def beforeAll(): Unit = {\n    val conf = TestHive.sparkSession.sparkContext.getConf.clone()\n    TestHive.sparkSession.stop()\n    conf.set(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key, classOf[DeltaCatalog].getName)\n    conf.set(StaticSQLConf.SPARK_SESSION_EXTENSIONS.key,\n      classOf[DeltaSparkSessionExtension].getName)\n    _sc = new SparkContext(\"local\", this.getClass.getName, conf)\n    _hiveContext = new TestHiveContext(_sc)\n    _session = _hiveContext.sparkSession\n    SparkSession.setActiveSession(_session)\n    super.beforeAll()\n  }\n\n  override protected def spark: SparkSession = _session\n\n  override def afterAll(): Unit = {\n    try {\n      _hiveContext.reset()\n    } finally {\n      _sc.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaSQLCommandTest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.sql.delta.catalog.DeltaCatalog\nimport io.delta.sql.DeltaSparkSessionExtension\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * A trait for tests that are testing a fully set up SparkSession with all of Delta's requirements,\n * such as the configuration of the DeltaCatalog and the addition of all Delta extensions.\n */\ntrait DeltaSQLCommandTest extends SharedSparkSession {\n\n  override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(StaticSQLConf.SPARK_SESSION_EXTENSIONS.key,\n        classOf[DeltaSparkSessionExtension].getName)\n      .set(SQLConf.V2_SESSION_CATALOG_IMPLEMENTATION.key,\n        classOf[DeltaCatalog].getName)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaSQLTestUtils.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport java.io.File\nimport java.util.UUID\n\nimport scala.util.Random\n\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, DeltaColumnMappingTestUtilsBase, DeltaLog, DeltaTable, Snapshot, TableFeature}\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.coordinatedcommits.{CatalogOwnedCommitCoordinatorProvider, CatalogOwnedTableUtils, TrackingInMemoryCommitCoordinatorBuilder}\nimport org.apache.spark.sql.delta.stats.{DeltaStatistics, PreparedDeltaFileIndex}\nimport com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SQLTestUtils\nimport org.apache.spark.sql.types._\nimport org.apache.spark.util.Utils\n\ntrait DeltaSQLTestUtils extends SQLTestUtils {\n  /**\n   * Override the temp dir/path creation methods from [[SQLTestUtils]] to:\n   * 1. Drop the call to `waitForTasksToFinish` which is a source of flakiness due to timeouts\n   *    without clear benefits.\n   * 2. Allow creating paths with special characters for better test coverage.\n   */\n\n  protected val defaultTempDirPrefix: String = \"spark%dir%prefix\"\n\n  override protected def withTempDir(f: File => Unit): Unit = {\n    withTempDir(prefix = defaultTempDirPrefix)(f)\n  }\n\n  override protected def withTempPaths(numPaths: Int)(f: Seq[File] => Unit): Unit = {\n    withTempPaths(numPaths, prefix = defaultTempDirPrefix)(f)\n  }\n\n  override def withTempPath(f: File => Unit): Unit = {\n    withTempPath(prefix = defaultTempDirPrefix)(f)\n  }\n\n  /**\n   * Creates a temporary directory, which is then passed to `f` and will be deleted after `f`\n   * returns.\n   */\n  def withTempDir(prefix: String)(f: File => Unit): Unit = {\n    val path = Utils.createTempDir(namePrefix = prefix)\n    try f(path) finally Utils.deleteRecursively(path)\n  }\n\n  /**\n   * Generates a temporary directory path without creating the actual directory, which is then\n   * passed to `f` and will be deleted after `f` returns.\n   */\n  def withTempPath(prefix: String)(f: File => Unit): Unit = {\n    val path = Utils.createTempDir(namePrefix = prefix)\n    path.delete()\n    try f(path) finally Utils.deleteRecursively(path)\n  }\n\n  /**\n   * Generates the specified number of temporary directory paths without creating the actual\n   * directories, which are then passed to `f` and will be deleted after `f` returns.\n   */\n  protected def withTempPaths(numPaths: Int, prefix: String)(f: Seq[File] => Unit): Unit = {\n    val files =\n      Seq.fill[File](numPaths)(Utils.createTempDir(namePrefix = prefix).getCanonicalFile)\n    files.foreach(_.delete())\n    try f(files) finally {\n      files.foreach(Utils.deleteRecursively)\n    }\n  }\n\n  /**\n   * Creates a temporary table with a unique name for testing and executes a function with it.\n   * The table is automatically cleaned up after the function completes.\n   *\n   * @param createTable Whether to create an empty table.\n   * @param f The function to execute with the generated table name.\n   */\n  protected def withTempTable(createTable: Boolean)(f: String => Unit): Unit = {\n    val tableName = s\"test_table_${UUID.randomUUID().toString.filterNot(_ == '-')}\"\n\n    withTable(tableName) {\n      if (createTable) {\n        spark.sql(s\"CREATE TABLE $tableName (id LONG) USING delta\")\n      }\n      f(tableName)\n    }\n  }\n\n  /**\n   * Creates a Catalog-Managed Delta table for tests.\n   *\n   * @param createTable Whether to create the table with CatalogOwnedTableFeature enabled.\n   * @param f The function to execute with the generated table name.\n   */\n  protected def withCatalogManagedTable(createTable: Boolean = true)(f: String => Unit): Unit = {\n    CatalogOwnedCommitCoordinatorProvider.clearBuilders()\n    CatalogOwnedCommitCoordinatorProvider.registerBuilder(\n      CatalogOwnedTableUtils.DEFAULT_CATALOG_NAME_FOR_TESTING,\n      TrackingInMemoryCommitCoordinatorBuilder(batchSize = 3))\n    withTempTable(createTable = false) { tableName =>\n      if (createTable) {\n        spark.sql(s\"CREATE TABLE $tableName (id INT) USING delta TBLPROPERTIES \" +\n          s\"('delta.feature.${CatalogOwnedTableFeature.name}' = 'supported')\")\n      }\n      f(tableName)\n    }\n  }\n\n  /** Returns random alphanumberic string to be used as a unique table name. */\n  def uniqueTableName: String = Random.alphanumeric.take(10).mkString\n\n  /** Gets the latest snapshot of the table. */\n  def getSnapshot(tableName: String): Snapshot = {\n    DeltaLog.forTable(spark, TableIdentifier(tableName)).update()\n  }\n\n  /** Gets the table protocol of the latest snapshot. */\n  def getProtocolForTable(tableName: String): Protocol = {\n    getSnapshot(tableName).protocol\n  }\n  /** Gets the `StructField` of `columnPath`. */\n  final def getColumnField(schema: StructType, columnPath: Seq[String]): StructField = {\n    schema.findNestedField(columnPath, includeCollections = true).get._2\n  }\n\n  /** Gets the `StructField` of `columnName`. */\n  def getColumnField(tableName: String, columnName: String): StructField = {\n    val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n    getColumnField(deltaLog.update().schema, columnName.split(\"\\\\.\"))\n  }\n\n  /** Gets the `DataType` of `columnPath`. */\n  def getColumnType(schema: StructType, columnPath: Seq[String]): DataType = {\n    getColumnField(schema, columnPath).dataType\n  }\n\n  /** Gets the `DataType` of `columnName`. */\n  def getColumnType(tableName: String, columnName: String): DataType = {\n    getColumnField(tableName, columnName).dataType\n  }\n\n  /**\n   * Gets the stats fields from the AddFiles of `snapshot`. The stats are ordered by the\n   * modification time of the files they are associated with.\n   */\n  def getUnvalidatedStatsOrderByFileModTime(snapshot: Snapshot): Array[JsonNode] = {\n    snapshot.allFiles\n      .orderBy(\"modificationTime\")\n      .collect()\n      .map(file => new ObjectMapper().readTree(file.stats))\n  }\n\n  /**\n   * Gets the stats fields from the AddFiles of `tableName`. The stats are ordered by the\n   * modification time of the files they are associated with.\n   */\n  def getUnvalidatedStatsOrderByFileModTime(tableName: String): Array[JsonNode] =\n    getUnvalidatedStatsOrderByFileModTime(getSnapshot(tableName))\n\n  /** Gets the physical column path if there is column mapping metadata in the schema. */\n  def getPhysicalColumnPath(tableSchema: StructType, columnName: String): Seq[String] = {\n    new DeltaColumnMappingTestUtilsBase {}.getPhysicalPathForStats(\n      columnName.split(\"\\\\.\"), tableSchema\n    ).get\n  }\n\n  /** Gets the value of a specified field from `stats` JSON node if it exists. */\n  def getStatFieldOpt(stats: JsonNode, path: Seq[String]): Option[JsonNode] =\n    path.foldLeft(Option(stats)) {\n      case (Some(node), key) if node.has(key) => Option(node.get(key))\n      case _ => None\n    }\n\n  /** Gets the min/max stats of `columnName` from `stats` if they exist. */\n  private def getMinMaxStatsOpt(\n      tableName: String,\n      stats: JsonNode,\n      columnName: String): (Option[String], Option[String]) = {\n    val schema = getSnapshot(tableName).schema\n\n    val physicalColumnPath = getPhysicalColumnPath(schema, columnName)\n    val minStatsPath = DeltaStatistics.MIN +: physicalColumnPath\n    val maxStatsPath = DeltaStatistics.MAX +: physicalColumnPath\n    (\n      getStatFieldOpt(stats, minStatsPath).map(_.asText()),\n      getStatFieldOpt(stats, maxStatsPath).map(_.asText()))\n  }\n\n  /** Gets the min/max stats of `columnName` from `stats`. */\n  def getMinMaxStats(\n      tableName: String,\n      stats: JsonNode,\n      columnName: String): (String, String) = {\n    val (minOpt, maxOpt) = getMinMaxStatsOpt(tableName, stats, columnName)\n    (minOpt.get, maxOpt.get)\n  }\n\n  /** Verifies whether there are min/max stats of `columnName` in `stats`. */\n  def assertMinMaxStatsPresence(\n      tableName: String,\n      stats: JsonNode,\n      columnName: String,\n      expectStats: Boolean): Unit = {\n    val (minStats, maxStats) = getMinMaxStatsOpt(tableName, stats, columnName)\n    assert(minStats.isDefined === expectStats)\n    assert(maxStats.isDefined === expectStats)\n  }\n\n  /** Verifies min/max stats values of `columnName` in `stats`. */\n  def assertMinMaxStats(\n      tableName: String,\n      stats: JsonNode,\n      columnName: String,\n      expectedMin: String,\n      expectedMax: String): Unit = {\n    val (min, max) =\n      getMinMaxStats(tableName, stats, columnName)\n    assert(min === expectedMin, s\"Expected $expectedMin, got $min\")\n    assert(max === expectedMax, s\"Expected $expectedMax, got $max\")\n  }\n\n  /** Verifies minReaderVersion and minWriterVersion of the protocol. */\n  def assertProtocolVersion(\n      protocol: Protocol,\n      minReaderVersion: Int,\n      minWriterVersion: Int): Unit = {\n    assert(protocol.minReaderVersion === minReaderVersion)\n    assert(protocol.minWriterVersion === minWriterVersion)\n  }\n\n  /** Verifies column is of expected data type. */\n  def assertColumnDataType(\n      tableName: String,\n      columnName: String,\n      expectedDataType: DataType): Unit = {\n    assert(getColumnType(tableName, columnName) === expectedDataType)\n  }\n\n  /** Verifies `columnName` does not exist in `tableName`. */\n  def assertColumnNotExist(tableName: String, columnName: String): Unit = {\n    val e = intercept[AnalysisException] {\n      sql(s\"SELECT $columnName FROM $tableName\")\n    }\n    assert(e.getMessage.contains(s\"`$columnName` cannot be resolved\"))\n  }\n\n  /**\n   * Runs `select` query on `tableName` with `predicate` and verifies the number of rows returned\n   * and files read.\n   */\n  def assertSelectQueryResults(\n      tableName: String,\n      predicate: String,\n      numRows: Int,\n      numFilesRead: Int): Unit = {\n    val query = sql(s\"SELECT * FROM $tableName WHERE $predicate\")\n    assertSelectQueryResults(query, numRows, numFilesRead)\n  }\n\n  /**\n   * Runs `query` and verifies the number of rows returned\n   * and files read.\n   */\n  def assertSelectQueryResults(\n      query: DataFrame,\n      numRows: Int,\n      numFilesRead: Int): Unit = {\n    assert(query.count() === numRows, s\"Expected $numRows rows, got ${query.count()}\")\n    val filesRead = getNumReadFiles(query)\n    assert(filesRead === numFilesRead, s\"Expected $numFilesRead files read, got $filesRead\")\n  }\n\n  /** Returns the number of read files by the query with given query text. */\n  def getNumReadFiles(queryText: String): Int = {\n    getNumReadFiles(sql(queryText))\n  }\n\n  /** Returns the number of read files by the given data frame query. */\n  def getNumReadFiles(df: DataFrame): Int = {\n    val deltaScans = df.queryExecution.optimizedPlan.collect {\n      case DeltaTable(prepared: PreparedDeltaFileIndex) => prepared.preparedScan\n    }\n    assert(deltaScans.size == 1)\n    deltaScans.head.files.length\n  }\n\n  /** Drops `columnName` from `tableName`. */\n  def dropColumn(tableName: String, columnName: String): Unit = {\n    sql(s\"ALTER TABLE $tableName DROP COLUMN $columnName\")\n    assertColumnNotExist(tableName, columnName)\n  }\n\n  /** Changes `columnName` to `newType` */\n  def alterColumnType(tableName: String, columnName: String, newType: String): Unit = {\n    sql(s\"ALTER TABLE $tableName ALTER COLUMN $columnName TYPE $newType\")\n  }\n\n  /** Whether the table protocol supports the given table feature. */\n  def isFeatureSupported(tableName: String, tableFeature: TableFeature): Boolean = {\n    val protocol = getProtocolForTable(tableName)\n    protocol.isFeatureSupported(tableFeature)\n  }\n\n  /** Whether the table protocol supports the given table feature. */\n  def isFeatureSupported(tableName: String, featureName: String): Boolean = {\n    val protocol = getProtocolForTable(tableName)\n    protocol.readerFeatureNames.contains(featureName) ||\n      protocol.writerFeatureNames.contains(featureName)\n  }\n\n  /** Enables table feature for `tableName` and given `featureName`. */\n  def enableTableFeature(tableName: String, featureName: String): Unit = {\n    sql(s\"\"\"\n           |ALTER TABLE $tableName\n           |SET TBLPROPERTIES('delta.feature.$featureName' = 'supported')\n           |\"\"\".stripMargin)\n    assert(isFeatureSupported(tableName, featureName))\n  }\n\n  /** Drops table feature for `tableName` and `featureName`. */\n  def dropTableFeature(tableName: String, featureName: String): Unit = {\n    sql(s\"ALTER TABLE $tableName DROP FEATURE `$featureName`\")\n    assert(!isFeatureSupported(tableName, featureName))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/DeltaTestImplicits.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport java.io.File\nimport java.sql.Timestamp\n\nimport org.apache.spark.sql.delta.{CatalogOwnedTableFeature, DeltaHistoryManager, DeltaLog, OptimisticTransaction, Snapshot}\nimport org.apache.spark.sql.delta.DeltaOperations.{ManualUpdate, Operation, Write}\nimport org.apache.spark.sql.delta.SnapshotDescriptor\nimport org.apache.spark.sql.delta.actions.{Action, AddFile, Metadata, Protocol, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.coordinatedcommits.TableCommitCoordinatorClient\nimport org.apache.spark.sql.delta.files.TahoeLogFileIndex\nimport org.apache.spark.sql.delta.hooks.AutoCompact\nimport org.apache.spark.sql.delta.stats.StatisticsCollection\nimport io.delta.storage.commit.{CommitResponse, GetCommitsResponse, UpdatedActions}\nimport org.apache.hadoop.fs.{FileStatus, Path}\n\nimport org.apache.spark.sql.{SaveMode, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.expressions.{Expression, Literal}\nimport org.apache.spark.util.Clock\n\n/**\n * Additional method definitions for Delta classes that are intended for use only in testing.\n */\nobject DeltaTestImplicits {\n  implicit class OptimisticTxnTestHelper(txn: OptimisticTransaction) {\n\n    /** Ensure that the initial commit of a Delta table always contains a Metadata action */\n    def commitActions(op: Operation, actions: Action*): Long = {\n      if (txn.readVersion == -1) {\n        val metadataOpt = actions.collectFirst { case m: Metadata => m }\n        val protocolOpt = actions.collectFirst { case p: Protocol => p }\n        val otherActions =\n          actions.filterNot(a => a.isInstanceOf[Metadata] || a.isInstanceOf[Protocol])\n        (metadataOpt, protocolOpt) match {\n          case (Some(metadata), Some(protocol)) =>\n            // When both metadata and protocol are explicitly passed, use them.\n            txn.updateProtocol(protocol)\n            // This will auto upgrade any required table features in the passed protocol as per\n            // given metadata.\n            txn.updateMetadataForNewTable(metadata)\n          case (Some(metadata), None) =>\n            // When just metadata is passed, use it.\n            // This will auto generate protocol as per metadata.\n            txn.updateMetadataForNewTable(metadata)\n          case (None, Some(protocol)) =>\n            txn.updateProtocol(protocol)\n            txn.updateMetadataForNewTable(Metadata())\n          case (None, None) =>\n            // If neither metadata nor protocol is explicitly passed, then use default Metadata and\n            // with the maximum protocol.\n            txn.updateMetadataForNewTable(Metadata())\n            val enableCatalogOwnedByDefault = SparkSession.active.conf.getOption(\n              TableFeatureProtocolUtils.defaultPropertyKey(CatalogOwnedTableFeature))\n                .contains(\"supported\")\n            if (enableCatalogOwnedByDefault) {\n              txn.updateProtocol(Action.supportedProtocolVersion())\n            } else {\n              txn.updateProtocol(Action.supportedProtocolVersion(\n                // CatalogOwnedTableFeature is enabled by protocol only without metadata, and should\n                // not be enabled by default.\n                featuresToExclude = Seq(CatalogOwnedTableFeature)))\n            }\n        }\n        txn.commit(otherActions, op)\n      } else {\n        txn.commit(actions, op)\n      }\n    }\n\n    def commitManually(actions: Action*): Long = {\n      commitActions(ManualUpdate, actions: _*)\n    }\n\n    def commitWriteAppend(actions: Action*): Long = {\n      commitActions(Write(SaveMode.Append), actions: _*)\n    }\n  }\n\n  /** Add test-only File overloads for DeltaTable.forPath */\n  implicit class DeltaLogObjectTestHelper(deltaLog: DeltaLog.type) {\n    def forTable(spark: SparkSession, dataPath: File): DeltaLog = {\n      DeltaLog.forTable(spark, new Path(dataPath.getCanonicalPath))\n    }\n\n    def forTable(spark: SparkSession, dataPath: File, clock: Clock): DeltaLog = {\n      DeltaLog.forTable(spark, new Path(dataPath.getCanonicalPath), clock)\n    }\n  }\n\n  implicit class DeltaHistoryManagerTestHelper(history: DeltaHistoryManager) {\n    def checkVersionExists(version: Long): Unit = {\n      history.checkVersionExists(version, catalogTableOpt = None)\n    }\n\n    def getActiveCommitAtTime(\n        timestamp: Long,\n        canReturnLastCommit: Boolean): DeltaHistoryManager.Commit = {\n      history.getActiveCommitAtTime(\n        new Timestamp(timestamp),\n        catalogTableOpt = None,\n        canReturnLastCommit)\n    }\n  }\n\n  /** Helper class for working with [[TableCommitCoordinatorClient]] */\n  implicit class TableCommitCoordinatorClientTestHelper(\n      tableCommitCoordinatorClient: TableCommitCoordinatorClient) {\n\n    def commit(\n        commitVersion: Long,\n        actions: Iterator[String],\n        updatedActions: UpdatedActions): CommitResponse = {\n      tableCommitCoordinatorClient.commit(\n        commitVersion, actions, updatedActions, tableIdentifierOpt = None)\n    }\n\n    def getCommits(\n        startVersion: Option[Long] = None,\n        endVersion: Option[Long] = None): GetCommitsResponse = {\n      tableCommitCoordinatorClient.getCommits(tableIdentifierOpt = None, startVersion, endVersion)\n    }\n\n    def backfillToVersion(\n        version: Long,\n        lastKnownBackfilledVersion: Option[Long] = None): Unit = {\n      tableCommitCoordinatorClient.backfillToVersion(\n        tableIdentifierOpt = None, version, lastKnownBackfilledVersion)\n    }\n  }\n\n\n  /** Helper class for working with [[Snapshot]] */\n  implicit class SnapshotTestHelper(snapshot: Snapshot) {\n    def ensureCommitFilesBackfilled(): Unit = {\n      snapshot.ensureCommitFilesBackfilled(catalogTableOpt = None)\n    }\n  }\n\n  /**\n   * Helper class for working with the most recent snapshot in the deltaLog\n   */\n  implicit class DeltaLogTestHelper(deltaLog: DeltaLog) {\n    def snapshot: Snapshot = {\n      deltaLog.unsafeVolatileSnapshot\n    }\n\n    def checkpoint(): Unit = {\n      deltaLog.checkpoint(snapshot)\n    }\n\n    def checkpointInterval(): Int = {\n      deltaLog.checkpointInterval(snapshot.metadata)\n    }\n\n    def deltaRetentionMillis(): Long = {\n      deltaLog.deltaRetentionMillis(snapshot.metadata)\n    }\n\n    def enableExpiredLogCleanup(): Boolean = {\n      deltaLog.enableExpiredLogCleanup(snapshot.metadata)\n    }\n\n    def upgradeProtocol(newVersion: Protocol): Unit = {\n      upgradeProtocol(deltaLog.unsafeVolatileSnapshot, newVersion)\n    }\n\n    def upgradeProtocol(snapshot: Snapshot, newVersion: Protocol): Unit = {\n      deltaLog.upgradeProtocol(None, snapshot, newVersion)\n    }\n\n    /**\n     * Test helper method for getChangeLogFiles that provides catalogTableOpt = None\n     * for backward compatibility with existing unit tests.\n     */\n    def getChangeLogFiles(startVersion: Long): Iterator[(Long, FileStatus)] = {\n      deltaLog.getChangeLogFiles(startVersion, catalogTableOpt = None)\n    }\n\n    /**\n     * Test helper method for getChanges that provides catalogTableOpt = None for backward\n     * compatibility with existing unit tests.\n     */\n    def getChanges(\n        startVersion: Long,\n        failOnDataLoss: Boolean = false): Iterator[(Long, Seq[Action])] = {\n      deltaLog.getChanges(startVersion, catalogTableOpt = None, failOnDataLoss)\n    }\n  }\n\n  implicit class DeltaTableV2ObjectTestHelper(dt: DeltaTableV2.type) {\n    /** Convenience overload that omits the cmd arg (which is not helpful in tests). */\n    def apply(spark: SparkSession, id: TableIdentifier): DeltaTableV2 =\n      dt.apply(spark, id, \"test\")\n\n    def apply(spark: SparkSession, tableDir: File): DeltaTableV2 =\n      dt.apply(spark, new Path(tableDir.getAbsolutePath))\n\n    def apply(spark: SparkSession, tableDir: File, clock: Clock): DeltaTableV2 = {\n      val tablePath = new Path(tableDir.getAbsolutePath)\n      DeltaTableV2.testOnlyApplyWithCustomDeltaLog(spark, tablePath, clock)\n    }\n  }\n\n  implicit class DeltaTableV2TestHelper(deltaTable: DeltaTableV2) {\n    /** For backward compatibility with existing unit tests */\n    def snapshot: Snapshot = deltaTable.initialSnapshot\n  }\n\n  implicit class TahoeLogFileIndexObjectTestHelper(index: TahoeLogFileIndex.type) {\n    def apply(spark: SparkSession, deltaLog: DeltaLog): TahoeLogFileIndex = {\n      index.apply(spark, deltaLog, catalogTableOpt = None)\n    }\n  }\n\n  implicit class AutoCompactObjectTestHelper(ac: AutoCompact.type) {\n    private[delta] def compact(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      partitionPredicates: Seq[Expression] = Nil,\n      opType: String = AutoCompact.OP_TYPE): Seq[OptimizeMetrics] = {\n      AutoCompact.compact(\n        spark, deltaLog, catalogTable = None,\n        partitionPredicates, opType)\n    }\n  }\n\n  implicit class StatisticsCollectionObjectTestHelper(sc: StatisticsCollection.type) {\n\n    /**\n     * This is an implicit helper required for backward compatibility with existing\n     * unit tests. It allows to call [[StatisticsCollection.recompute]] without a\n     * catalog table and in the actual call, sets it to [[None]].\n     */\n    def recompute(\n      spark: SparkSession,\n      deltaLog: DeltaLog,\n      predicates: Seq[Expression] = Seq(Literal(true)),\n      fileFilter: AddFile => Boolean = af => true): Unit = {\n      StatisticsCollection.recompute(\n        spark, deltaLog, catalogTable = None, predicates, fileFilter)\n    }\n  }\n\n  implicit class CDCReaderObjectTestHelper(cdcReader: CDCReader.type) {\n\n    /**\n     * Test helper method for changesToBatchDF that provides catalogTableOpt = None\n     * for backward compatibility with existing unit tests.\n     */\n    def changesToBatchDF(\n        deltaLog: DeltaLog,\n        start: Long,\n        end: Long,\n        spark: SparkSession,\n        readSchemaSnapshot: Option[Snapshot] = None,\n        useCoarseGrainedCDC: Boolean = false,\n        startVersionSnapshot: Option[SnapshotDescriptor] = None\n    ): org.apache.spark.sql.DataFrame = {\n      cdcReader.changesToBatchDF(\n        deltaLog,\n        start,\n        end,\n        spark,\n        catalogTableOpt = None,\n        readSchemaSnapshot,\n        useCoarseGrainedCDC,\n        startVersionSnapshot)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/ScanReportHelper.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport scala.util.control.NonFatal\n\nimport org.apache.spark.sql.delta.files.TahoeFileIndex\nimport org.apache.spark.sql.delta.metering.ScanReport\nimport org.apache.spark.sql.delta.stats.{DataSize, PreparedDeltaFileIndex}\nimport org.apache.spark.sql.execution.{FileSourceScanExec, QueryExecution, SparkPlan}\nimport org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper\nimport org.apache.spark.sql.execution.columnar.InMemoryTableScanExec\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.util.QueryExecutionListener\n\n/**\n * A helper trait used by test classes that want to collect the scans (i.e. [[FileSourceScanExec]])\n * generated by a given input query during query planning.\n *\n * This trait exposes a single public API [[getScanReport]].\n */\ntrait ScanReportHelper extends SharedSparkSession with AdaptiveSparkPlanHelper {\n\n  import ScanReportHelper._\n\n  /**\n   * Collect the scan leaves in the given SparkPlan.\n   */\n  private def collectScans(plan: SparkPlan): Seq[FileSourceScanExec] = {\n    collectWithSubqueries(plan)({\n      case fs: FileSourceScanExec => Seq(fs)\n      case cached: InMemoryTableScanExec => collectScans(cached.relation.cacheBuilder.cachedPlan)\n    }).flatten\n  }\n\n  /**\n   * Returns a new [[QueryExecutionListener]] that can be registered to the Spark listener bus\n   * to analyse and collect metrics during query execution.\n   *\n   * Specifically, this listener will check for any [[FileSourceScanExec]] generated during query\n   * planning, cast them into [[ScanReport]] (helper class to hold useful info about the scan), and\n   * append to the singleton [[ScanReportHelper.scans]]\n   */\n  private def getListener(): QueryExecutionListener = {\n    new QueryExecutionListener {\n      override def onSuccess(funcName: String, qe: QueryExecution, durationNs: Long): Unit = {\n        try qe.assertAnalyzed() catch {\n          case NonFatal(e) =>\n            logDebug(\"Not running Delta Metering because the query failed during analysis.\", e)\n            return\n        }\n\n        val fileScans = collectScans(qe.executedPlan)\n\n        for (scanExec <- fileScans) {\n          scanExec.relation.location match {\n            case deltaTable: PreparedDeltaFileIndex =>\n              val preparedScan = deltaTable.preparedScan\n              // The names of the partition columns that were used as filters in this scan.\n              // Convert this to a set first to avoid double-counting partition columns that might\n              // appear multiple times.\n              val usedPartitionColumns =\n              preparedScan.partitionFilters.map(_.references.map(_.name)).flatten.toSet.toSeq\n              val report = ScanReport(\n                tableId = deltaTable.metadata.id,\n                path = deltaTable.path.toString,\n                scanType = \"delta-query\",\n                deltaDataSkippingType = preparedScan.dataSkippingType.toString,\n                partitionFilters = preparedScan.partitionFilters.map(_.sql).toSeq,\n                partitionLikeDataFilters = preparedScan.partitionLikeDataFilters.map(_.sql).toSeq,\n                rewrittenPartitionLikeDataFilters =\n                  preparedScan.rewrittenPartitionLikeFilterSQL.toSeq,\n                dataFilters = preparedScan.dataFilters.map(_.sql).toSeq,\n                unusedFilters = preparedScan.unusedFilters.map(_.sql).toSeq,\n                size = Map(\n                  \"total\" -> preparedScan.total,\n                  \"partition\" -> preparedScan.partition,\n                  \"scanned\" -> preparedScan.scanned),\n                metrics = scanExec.metrics.mapValues(_.value).toMap +\n                  (\"scanDurationMs\" -> preparedScan.scanDurationMs),\n                annotations = Map.empty,\n                versionScanned = deltaTable.versionScanned,\n                usedPartitionColumns = usedPartitionColumns,\n                numUsedPartitionColumns = usedPartitionColumns.size,\n                allPartitionColumns = deltaTable.metadata.partitionColumns,\n                numAllPartitionColumns = deltaTable.metadata.partitionColumns.size,\n                parentFilterOutputRows = None\n              )\n\n              scans += report\n\n            case deltaTable: TahoeFileIndex =>\n              val report = ScanReport(\n                tableId = deltaTable.metadata.id,\n                path = deltaTable.path.toString,\n                scanType = \"delta-unknown\",\n                partitionFilters = Nil,\n                dataFilters = Nil,\n                partitionLikeDataFilters = Nil,\n                rewrittenPartitionLikeDataFilters = Nil,\n                unusedFilters = Nil,\n                size = Map(\n                  \"total\" -> DataSize(\n                    bytesCompressed = Some(deltaTable.deltaLog.unsafeVolatileSnapshot.sizeInBytes)),\n                  \"scanned\" -> DataSize(bytesCompressed = Some(deltaTable.sizeInBytes))\n                ),\n                metrics = scanExec.metrics.mapValues(_.value).toMap,\n                versionScanned = None,\n                annotations = Map.empty\n              )\n\n              scans += report\n\n            case _ => // ignore\n          }\n        }\n      }\n\n      override def onFailure(funcName: String, qe: QueryExecution, exception: Exception): Unit = { }\n    }\n  }\n\n  /**\n   * Execute function `f` and return the scans generated during query planning\n   */\n  def getScanReport(f: => Unit): Seq[ScanReport] = {\n    synchronized {\n      assert(scans == null, \"getScanReport does not support nested invocation.\")\n      scans = scala.collection.mutable.ArrayBuffer.empty[ScanReport]\n    }\n\n    val listener = getListener()\n    spark.listenerManager.register(listener)\n\n    var result: scala.collection.mutable.ArrayBuffer[ScanReport] = null\n    try {\n      f\n    } finally {\n      spark.sparkContext.listenerBus.waitUntilEmpty(15000)\n      spark.listenerManager.unregister(listener)\n\n      result = scans\n      synchronized {\n        scans = null\n      }\n    }\n\n    result.toSeq\n  }\n}\n\nobject ScanReportHelper {\n  @volatile var scans: scala.collection.mutable.ArrayBuffer[ScanReport] = null\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/test/TestsStatistics.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.execution.{ColumnarToRowExec, FileSourceScanExec, InputAdapter, SparkPlan}\nimport org.apache.spark.sql.functions.from_json\nimport org.apache.spark.sql.{Column, DataFrame}\n\n/**\n * Provides utilities for testing StatisticsCollection.\n */\ntrait TestsStatistics  { self: DeltaSQLTestUtils =>\n\n  /** A function to get the reconciled statistics DataFrame from the DeltaLog */\n  protected var getStatsDf: (DeltaLog, Seq[Column]) => DataFrame = _\n\n  /**\n   * Creates the correct `getStatsDf` to be used by the `testFun` and executes the `testFun`.\n   */\n  protected def statsTest(testName: String, testTags: org.scalatest.Tag*)(testFun: => Any): Unit = {\n    import testImplicits._\n\n    test(testName, testTags: _*) {\n      getStatsDf = (deltaLog, columns) => {\n        val snapshot = deltaLog.snapshot\n        snapshot.allFiles\n          .withColumn(\"stats\", from_json($\"stats\", snapshot.statsSchema))\n          .select(\"stats.*\")\n          .select(columns: _*)\n      }\n      testFun\n    }\n  }\n\n  /**\n   * Creates the correct `getStatsDf` to be used by the `testFun` and executes the `testFun`.\n   * Runs only against Spark master.\n   */\n  protected def statsTestSparkMasterOnly(\n      testName: String,\n      testTags: org.scalatest.Tag*)(testFun: => Any): Unit = {\n    import testImplicits._\n\n    test(testName, testTags: _*) {\n      getStatsDf = (deltaLog, columns) => {\n        val snapshot = deltaLog.snapshot\n        snapshot.allFiles\n          .withColumn(\"stats\", from_json($\"stats\", snapshot.statsSchema))\n          .select(\"stats.*\")\n          .select(columns: _*)\n      }\n      testFun\n    }\n  }\n\n  /**\n   * A util to match a physical file scan node.\n   */\n  object FileScanExecNode {\n    def unapply(plan: SparkPlan): Option[FileSourceScanExec] = plan match {\n      case f: FileSourceScanExec => Some(f)\n      case InputAdapter(f: FileSourceScanExec) => Some(f)\n      case ColumnarToRowExec(InputAdapter(f: FileSourceScanExec)) => Some(f)\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningAlterTableNestedSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.{AnalysisException, QueryTest}\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\nimport org.apache.spark.sql.types._\n\n/**\n * Suite providing additional coverage for widening nested fields using ALTER TABLE CHANGE COLUMN\n * TYPE.\n */\nclass TypeWideningAlterTableNestedSuite\n  extends QueryTest\n    with ParquetTest\n    with TypeWideningTestMixin\n    with TypeWideningAlterTableNestedTests\n\ntrait TypeWideningAlterTableNestedTests {\n  self: QueryTest with ParquetTest with TypeWideningTestMixin =>\n\n  import testImplicits._\n\n  /** Create a table with a struct, map and array for each test. */\n  protected def createNestedTable(): Unit = {\n    sql(s\"CREATE TABLE delta.`$tempPath` \" +\n      \"(s struct<a: byte>, m map<byte, short>, a array<short>) USING DELTA\")\n    append(Seq((1, 2, 3, 4))\n      .toDF(\"a\", \"b\", \"c\", \"d\")\n      .selectExpr(\n        \"named_struct('a', cast(a as byte)) as s\",\n        \"map(cast(b as byte), cast(c as short)) as m\",\n        \"array(cast(d as short)) as a\"))\n\n    assert(readDeltaTable(tempPath).schema === new StructType()\n      .add(\"s\", new StructType().add(\"a\", ByteType))\n      .add(\"m\", MapType(ByteType, ShortType))\n      .add(\"a\", ArrayType(ShortType)))\n  }\n\n  test(\"unsupported ALTER TABLE CHANGE COLUMN on non-leaf fields\") {\n    createNestedTable()\n    // Running ALTER TABLE CHANGE COLUMN on non-leaf fields is invalid.\n    var alterTableSql = s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN s TYPE struct<a: short>\"\n    checkError(\n      intercept[AnalysisException] { sql(alterTableSql) },\n      \"CANNOT_UPDATE_FIELD.STRUCT_TYPE\",\n      parameters = Map(\n        \"table\" -> s\"`spark_catalog`.`delta`.`$tempPath`\",\n        \"fieldName\" -> \"`s`\"\n      ),\n      context = ExpectedContext(\n        fragment = alterTableSql,\n        start = 0,\n        stop = alterTableSql.length - 1)\n    )\n\n    alterTableSql = s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN m TYPE map<int, int>\"\n    checkError(\n      intercept[AnalysisException] { sql(alterTableSql) },\n      \"CANNOT_UPDATE_FIELD.MAP_TYPE\",\n      parameters = Map(\n        \"table\" -> s\"`spark_catalog`.`delta`.`$tempPath`\",\n        \"fieldName\" -> \"`m`\"\n      ),\n      context = ExpectedContext(\n        fragment = alterTableSql,\n        start = 0,\n        stop = alterTableSql.length - 1)\n    )\n\n    alterTableSql = s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE array<int>\"\n    checkError(\n      intercept[AnalysisException] { sql(alterTableSql) },\n      \"CANNOT_UPDATE_FIELD.ARRAY_TYPE\",\n      parameters = Map(\n        \"table\" -> s\"`spark_catalog`.`delta`.`$tempPath`\",\n        \"fieldName\" -> \"`a`\"\n      ),\n      context = ExpectedContext(\n        fragment = alterTableSql,\n        start = 0,\n        stop = alterTableSql.length - 1)\n    )\n  }\n\n  test(\"type widening with ALTER TABLE on nested fields\") {\n    createNestedTable()\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN s.a TYPE short\")\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN m.key TYPE int\")\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN m.value TYPE int\")\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a.element TYPE int\")\n\n    assert(readDeltaTable(tempPath).schema === new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", ShortType))\n      .add(\"m\", MapType(IntegerType, IntegerType))\n      .add(\"a\", ArrayType(IntegerType)))\n\n    append(Seq((5, 6, 7, 8))\n      .toDF(\"a\", \"b\", \"c\", \"d\")\n        .selectExpr(\"named_struct('a', cast(a as short)) as s\", \"map(b, c) as m\", \"array(d) as a\"))\n\n    checkAnswer(\n      readDeltaTable(tempPath),\n      Seq((1, 2, 3, 4), (5, 6, 7, 8))\n        .toDF(\"a\", \"b\", \"c\", \"d\")\n        .selectExpr(\"named_struct('a', cast(a as short)) as s\", \"map(b, c) as m\", \"array(d) as a\"))\n  }\n\n  test(\"type widening using ALTER TABLE REPLACE COLUMNS on nested fields\") {\n    createNestedTable()\n    sql(s\"ALTER TABLE delta.`$tempPath` REPLACE COLUMNS \" +\n      \"(s struct<a: short>, m map<int, int>, a array<int>)\")\n    assert(readDeltaTable(tempPath).schema === new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", ShortType))\n      .add(\"m\", MapType(IntegerType, IntegerType))\n      .add(\"a\", ArrayType(IntegerType)))\n\n    append(Seq((5, 6, 7, 8))\n      .toDF(\"a\", \"b\", \"c\", \"d\")\n        .selectExpr(\"named_struct('a', cast(a as short)) as s\", \"map(b, c) as m\", \"array(d) as a\"))\n\n    checkAnswer(\n      readDeltaTable(tempPath),\n      Seq((1, 2, 3, 4), (5, 6, 7, 8))\n        .toDF(\"a\", \"b\", \"c\", \"d\")\n        .selectExpr(\"named_struct('a', cast(a as short)) as s\", \"map(b, c) as m\", \"array(d) as a\"))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningAlterTableSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.expressions.Cast\nimport org.apache.spark.sql.errors.QueryErrorsBase\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetTest\nimport org.apache.spark.sql.functions.lit\nimport org.apache.spark.sql.types._\n\n/**\n * Suite providing core coverage for type widening using ALTER TABLE CHANGE COLUMN TYPE.\n */\nclass TypeWideningAlterTableSuite\n  extends TypeWideningAlterTableTests\n    with ParquetTest\n    with TypeWideningTestMixin\n\ntrait TypeWideningAlterTableTests extends QueryTest\n    with QueryErrorsBase\n    with TypeWideningTestCases {\n  self: QueryTest with ParquetTest with TypeWideningTestMixin =>\n\n  import testImplicits._\n\n  for {\n    testCase <- supportedTestCases ++ restrictedAutomaticWideningTestCases\n    partitioned <- BOOLEAN_DOMAIN\n  } {\n    test(s\"type widening ${testCase.fromType.sql} -> ${testCase.toType.sql}, \" +\n      s\"partitioned=$partitioned\") {\n      def writeData(df: DataFrame): Unit = if (partitioned) {\n        // The table needs to have at least 1 non-partition column, use a dummy one.\n        append(df.withColumn(\"dummy\", lit(1)), partitionBy = Seq(\"value\"))\n      } else {\n        append(df)\n      }\n\n      writeData(testCase.initialValuesDF)\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN value TYPE ${testCase.toType.sql}\")\n      withAllParquetReaders {\n        assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.toType)\n        checkAnswerWithTolerance(\n          actualDf = readDeltaTable(tempPath).select(\"value\"),\n          expectedDf = testCase.initialValuesDF.select($\"value\".cast(testCase.toType)),\n          toType = testCase.toType\n        )\n      }\n      writeData(testCase.additionalValuesDF)\n      withAllParquetReaders {\n        checkAnswerWithTolerance(\n          actualDf = readDeltaTable(tempPath).select(\"value\"),\n          expectedDf = testCase.expectedResult.select($\"value\".cast(testCase.toType)),\n          toType = testCase.toType\n        )\n      }\n    }\n  }\n\n  for {\n    testCase <- unsupportedTestCases\n    partitioned <- BOOLEAN_DOMAIN\n  } {\n    test(s\"unsupported type changes ${testCase.fromType.sql} -> ${testCase.toType.sql}, \" +\n      s\"partitioned=$partitioned\") {\n      if (partitioned) {\n        // The table needs to have at least 1 non-partition column, use a dummy one.\n        append(testCase.initialValuesDF.withColumn(\"dummy\", lit(1)), partitionBy = Seq(\"value\"))\n      } else {\n        append(testCase.initialValuesDF)\n      }\n\n      val alterTableSql =\n        s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN value TYPE ${testCase.toType.sql}\"\n\n      // Type changes that aren't upcast are rejected early during analysis by Spark, while upcasts\n      // are rejected in Delta when the ALTER TABLE command is executed.\n      if (Cast.canUpCast(testCase.fromType, testCase.toType)) {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(alterTableSql)\n          },\n          \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n          sqlState = None,\n          parameters = Map(\n            \"fieldPath\" -> \"value\",\n            \"oldField\" -> testCase.fromType.sql,\n            \"newField\" -> testCase.toType.sql)\n        )\n      } else {\n        checkError(\n          intercept[AnalysisException] {\n            sql(alterTableSql)\n          },\n          \"NOT_SUPPORTED_CHANGE_COLUMN\",\n          sqlState = None,\n          parameters = Map(\n            \"table\" -> s\"`spark_catalog`.`delta`.`$tempPath`\",\n            \"originName\" -> toSQLId(\"value\"),\n            \"originType\" -> toSQLType(testCase.fromType),\n            \"newName\" -> toSQLId(\"value\"),\n            \"newType\" -> toSQLType(testCase.toType)),\n          context = ExpectedContext(\n            fragment = alterTableSql,\n            start = 0,\n            stop = alterTableSql.length - 1)\n        )\n      }\n    }\n  }\n\n  test(\"type widening using ALTER TABLE REPLACE COLUMNS\") {\n    append(Seq(1, 2).toDF(\"value\").select($\"value\".cast(ShortType)))\n    assert(readDeltaTable(tempPath).schema === new StructType().add(\"value\", ShortType))\n    sql(s\"ALTER TABLE delta.`$tempPath` REPLACE COLUMNS (value INT)\")\n    assert(readDeltaTable(tempPath).schema === new StructType().add(\"value\", IntegerType))\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2)))\n    append(Seq(3, 4).toDF(\"value\"))\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2), Row(3), Row(4)))\n  }\n\n  def withTimestampNTZDisabled(f: => Unit): Unit = {\n    val timestampNTZKey = TableFeatureProtocolUtils.defaultPropertyKey(TimestampNTZTableFeature)\n    conf.unsetConf(timestampNTZKey)\n    if (!conf.contains(timestampNTZKey)) return f\n\n    val timestampNTZSupported = conf.getConfString(timestampNTZKey)\n    conf.unsetConf(timestampNTZKey)\n    try {\n      f\n    } finally {\n      conf.setConfString(timestampNTZKey, timestampNTZSupported)\n    }\n  }\n\n  test(\n    \"widening Date -> TimestampNTZ rejected when TimestampNTZ feature isn't supported\") {\n    withTimestampNTZDisabled {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a date) USING DELTA\")\n      val currentProtocol = deltaLog.unsafeVolatileSnapshot.protocol\n      val currentFeatures = currentProtocol.implicitlyAndExplicitlySupportedFeatures\n        .map(_.name)\n        .toSeq\n        .sorted\n        .mkString(\", \")\n\n      checkError(\n        intercept[DeltaTableFeatureException] {\n          sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE TIMESTAMP_NTZ\")\n        },\n        \"DELTA_FEATURES_REQUIRE_MANUAL_ENABLEMENT\",\n        parameters = Map(\n          \"unsupportedFeatures\" -> \"timestampNtz\",\n          \"supportedFeatures\" -> currentFeatures\n        )\n      )\n    }\n  }\n\n  test(\"type widening type change metrics\") {\n    sql(s\"CREATE TABLE delta.`$tempDir` (a byte) USING DELTA\")\n    val usageLogs = Log4jUsageLogger.track {\n      sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n    }\n\n    val metrics = filterUsageRecords(usageLogs, \"delta.typeWidening.typeChanges\")\n      .map(r => JsonUtils.fromJson[Map[String, Seq[Map[String, String]]]](r.blob))\n      .head\n\n    assert(metrics(\"changes\") === Seq(\n      Map(\n        \"fromType\" -> \"TINYINT\",\n        \"toType\" -> \"INT\"\n      ))\n    )\n  }\n\n  test(\"type widening with user-defined type in table\") {\n    val dataWithUDT =\n      (1 to 10).map(x => Tuple2(x.toByte, new TestUDT.MyDenseVector(Array(x*0.5, x*2.0))))\n    append(dataWithUDT.toDF(\"a\", \"udt\"))\n    sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n  }\n\n  test(\"type widening with null type in table\") {\n    sql(s\"CREATE TABLE delta.`$tempDir` (a byte, n VOID) USING DELTA\")\n    sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningConstraintsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta.DeltaAnalysisException\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types._\n\n/**\n * Suite covering changing the type of columns referenced by constraints, e.g. CHECK constraints or\n * NOT NULL constraints.\n */\nclass TypeWideningConstraintsSuite\n  extends QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningConstraintsTests\n\ntrait TypeWideningConstraintsTests { self: QueryTest with TypeWideningTestMixin =>\n\n  test(\"not null constraint with type change\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (a byte NOT NULL) USING DELTA\")\n      sql(\"INSERT INTO t VALUES (1)\")\n      checkAnswer(sql(\"SELECT * FROM t\"), Row(1))\n\n      // Changing the type of a column with a NOT NULL constraint is allowed.\n      sql(\"ALTER TABLE t CHANGE COLUMN a TYPE SMALLINT\")\n      assert(sql(\"SELECT * FROM t\").schema(\"a\").dataType === ShortType)\n\n      sql(\"INSERT INTO t VALUES (2)\")\n      checkAnswer(sql(\"SELECT * FROM t\"), Seq(Row(1), Row(2)))\n    }\n  }\n\n  test(\"check constraint with type change\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (a byte, b byte) USING DELTA\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (hash(a) > 0)\")\n      sql(\"INSERT INTO t VALUES (2, 2)\")\n      checkAnswer(sql(\"SELECT hash(a) FROM t\"), Row(1765031574))\n\n      // Changing the type of a column that a CHECK constraint depends on is not allowed.\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN a TYPE SMALLINT\")\n        },\n        \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"a\",\n          \"constraints\" -> \"delta.constraints.ck -> hash ( a ) > 0\"\n      ))\n\n      // Changing the type of `b` is allowed as it's not referenced by the constraint.\n      sql(\"ALTER TABLE t CHANGE COLUMN b TYPE SMALLINT\")\n      assert(sql(\"SELECT * FROM t\").schema(\"b\").dataType === ShortType)\n      checkAnswer(sql(\"SELECT * FROM t\"), Row(2, 2))\n    }\n  }\n\n  test(\"check constraint on nested field with type change\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (a struct<x: byte, y: byte>) USING DELTA\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (hash(a.x) > 0)\")\n      sql(\"INSERT INTO t (a) VALUES (named_struct('x', 2, 'y', 3))\")\n      checkAnswer(sql(\"SELECT hash(a.x) FROM t\"), Row(1765031574))\n\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN a.x TYPE SMALLINT\")\n        },\n        \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"a.x\",\n          \"constraints\" -> \"delta.constraints.ck -> hash ( a . x ) > 0\"\n      ))\n\n      // Changing the type of a.y is allowed since it's not referenced by the CHECK constraint.\n      sql(\"ALTER TABLE t CHANGE COLUMN a.y TYPE SMALLINT\")\n      checkAnswer(sql(\"SELECT * FROM t\"), Row(Row(2, 3)))\n    }\n  }\n\n  test(\"check constraint on arrays and maps with type change\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (m map<byte, byte>, a array<byte>) USING DELTA\")\n      sql(\"INSERT INTO t VALUES (map(1, 2, 7, -3), array(1, -2, 3))\")\n\n      sql(\"ALTER TABLE t CHANGE COLUMN a.element TYPE SMALLINT\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ch1 CHECK (hash(a[1]) = -1160545675)\")\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN a.element TYPE INTEGER\")\n        },\n        \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"a.element\",\n          \"constraints\" -> \"delta.constraints.ch1 -> hash ( a [ 1 ] ) = - 1160545675\"\n        )\n      )\n\n      sql(\"ALTER TABLE t CHANGE COLUMN m.value TYPE SMALLINT\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ch2 CHECK (sign(m[7]) < 0)\")\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN m.value TYPE INTEGER\")\n        },\n        \"DELTA_CONSTRAINT_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"m.value\",\n          \"constraints\" -> \"delta.constraints.ch2 -> sign ( m [ 7 ] ) < 0\"\n        )\n      )\n    }\n  }\n\n  test(s\"check constraint with type evolution\") {\n    withTable(\"t\") {\n      sql(s\"CREATE TABLE t (a byte) USING DELTA\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (hash(a) > 0)\")\n      sql(\"INSERT INTO t VALUES (2)\")\n      checkAnswer(sql(\"SELECT hash(a) FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\"INSERT INTO t VALUES (200)\")\n          },\n          \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"a\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"constraints\" -> \"delta.constraints.ck -> hash ( a ) > 0\"\n        ))\n      }\n    }\n  }\n\n  test(\"check constraint on nested field with type evolution\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (a struct<x: byte, y: byte>) USING DELTA\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (hash(a.x) > 0)\")\n      sql(\"INSERT INTO t (a) VALUES (named_struct('x', 2, 'y', 3))\")\n      checkAnswer(sql(\"SELECT hash(a.x) FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\"INSERT INTO t (a) VALUES (named_struct('x', 200, 'y', CAST(5 AS byte)))\")\n          },\n          \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"a.x\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"constraints\" -> \"delta.constraints.ck -> hash ( a . x ) > 0\"\n          )\n        )\n\n        // changing the type of struct field `a.y` when it's not\n        // the field referenced by the CHECK constraint is allowed.\n        sql(\"INSERT INTO t (a) VALUES (named_struct('x', CAST(2 AS byte), 'y', 500))\")\n        checkAnswer(sql(\"SELECT hash(a.x) FROM t\"), Seq(Row(1765031574), Row(1765031574)))\n      }\n    }\n  }\n\n  test(\"check constraint on nested field with complex type evolution\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (a struct<x: struct<z: byte, h: byte>, y: byte>) USING DELTA\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (hash(a.x.z) > 0)\")\n      sql(\"INSERT INTO t (a) VALUES (named_struct('x', named_struct('z', 2, 'h', 3), 'y', 4))\")\n      checkAnswer(sql(\"SELECT hash(a.x.z) FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\n              s\"\"\"\n                 | INSERT INTO t (a) VALUES (\n                 |   named_struct('x', named_struct('z', 200, 'h', 3), 'y', 4)\n                 | )\n                 |\"\"\".stripMargin\n            )\n          },\n          \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"a.x.z\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"constraints\" -> \"delta.constraints.ck -> hash ( a . x . z ) > 0\"\n          )\n        )\n\n        // changing the type of struct field `a.y` and `a.x.h` when it's not\n        // the field referenced by the CHECK constraint is allowed.\n        sql(\n          \"\"\"\n            | INSERT INTO t (a) VALUES (\n            |   named_struct('x', named_struct('z', CAST(2 AS BYTE), 'h', 2002), 'y', 1030)\n            | )\n            |\"\"\".stripMargin\n        )\n        checkAnswer(sql(\"SELECT hash(a.x.z) FROM t\"), Seq(Row(1765031574), Row(1765031574)))\n      }\n    }\n  }\n\n  test(\"check constraint on arrays and maps with type evolution\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (s struct<arr array<map<byte, byte>>>) USING DELTA\")\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (s.arr[0][3] = 3)\")\n      sql(\"INSERT INTO t(s) VALUES (struct(struct(array(map(1, 1, 3, 3)))))\")\n      checkAnswer(sql(\"SELECT s.arr[0][3] FROM t\"), Row(3))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        // Insert by name is not supported by type evolution.\n        checkError(\n          intercept[DeltaAnalysisException] {\n            // Migrate map's key to int type.\n            spark.createDataFrame(Seq(Tuple1(Tuple1(Array(Map(999999 -> 1, 3 -> 3))))))\n              .toDF(\"s\").withColumn(\"s\", col(\"s\").cast(\"struct<arr:array<map<int,tinyint>>>\"))\n              .write.format(\"delta\").mode(\"append\").saveAsTable(\"t\")\n          },\n          \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"s.arr.element.key\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"constraints\" -> \"delta.constraints.ck -> s . arr [ 0 ] [ 3 ] = 3\"\n          )\n        )\n        checkError(\n          intercept[DeltaAnalysisException] {\n            // Migrate map's value to int type.\n            spark.createDataFrame(Seq(Tuple1(Tuple1(Array(Map(1 -> 999999, 3 -> 3))))))\n              .toDF(\"s\").withColumn(\"s\", col(\"s\").cast(\"struct<arr:array<map<tinyint,int>>>\"))\n              .write.format(\"delta\").mode(\"append\").saveAsTable(\"t\")\n          },\n          \"DELTA_CONSTRAINT_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"s.arr.element.value\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"constraints\" -> \"delta.constraints.ck -> s . arr [ 0 ] [ 3 ] = 3\"\n          )\n        )\n      }\n    }\n  }\n\n  test(\"add constraint after type change then RESTORE\") {\n    withTable(\"t\") {\n      sql(\"CREATE TABLE t (a byte) USING DELTA\")\n      sql(\"INSERT INTO t VALUES (2)\")\n      sql(\"ALTER TABLE t CHANGE COLUMN a TYPE INT\")\n      sql(\"INSERT INTO t VALUES (5)\")\n      checkAnswer(sql(\"SELECT a, hash(a) FROM t\"), Seq(Row(2, 1765031574), Row(5, 1023896466)))\n      sql(\"ALTER TABLE t ADD CONSTRAINT ck CHECK (hash(a) > 0)\")\n      // Constraints are stored in the table metadata, RESTORE removes the constraint so the type\n      // change can't get in the way.\n      sql(s\"RESTORE TABLE t VERSION AS OF 1\")\n      sql(\"INSERT INTO t VALUES (1)\")\n      checkAnswer(sql(\"SELECT a, hash(a) FROM t\"), Seq(Row(2, 1765031574), Row(1, -559580957)))\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningFeatureCompatibilitySuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.commands.cdc.CDCReader\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\n\nimport org.apache.spark.sql.{AnalysisException, DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types._\n\nclass TypeWideningFeatureCompatibilitySuite\n  extends QueryTest\n    with DeltaDMLTestUtils\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin\n    with TypeWideningCompatibilityTests\n    with TypeWideningColumnMappingTests\n\n/** Tests covering type widening compatibility with other delta features. */\ntrait TypeWideningCompatibilityTests {\n  self: TypeWideningTestMixin with QueryTest with DeltaDMLTestUtils =>\n\n  import testImplicits._\n\n  test(\"reading CDF with a type change\") {\n    withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a smallint) USING DELTA\")\n    }\n    append(Seq(1, 2).toDF(\"a\").select($\"a\".cast(ShortType)))\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n    append(Seq(3, 4).toDF(\"a\"))\n\n    def readCDF(start: Long, end: Long): DataFrame =\n      CDCReader\n        .changesToBatchDF(deltaLog, start, end, spark)\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n        .drop(CDCReader.CDC_COMMIT_VERSION)\n\n    checkErrorMatchPVals(\n      intercept[DeltaUnsupportedOperationException] {\n        readCDF(start = 1, end = 1).collect()\n      },\n      \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_DATA_SCHEMA\",\n      parameters = Map(\n        \"start\" -> \"1\",\n        \"end\" -> \"1\",\n        \"readSchema\" -> \".*\",\n        \"readVersion\" -> \"3\",\n        \"incompatibleVersion\" -> \"1\",\n        \"config\" -> \".*defaultSchemaModeForColumnMappingTable\"\n      )\n    )\n    checkAnswer(readCDF(start = 3, end = 3), Seq(Row(3, \"insert\"), Row(4, \"insert\")))\n  }\n\n  test(\"reading CDF with a type change using read schema from before the change\") {\n    withSQLConf((DeltaConfigs.CHANGE_DATA_FEED.defaultTablePropertyKey, \"true\")) {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a smallint) USING DELTA\")\n    }\n    append(Seq(1, 2).toDF(\"a\").select($\"a\".cast(ShortType)))\n    val readSchemaSnapshot = deltaLog.update()\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n    append(Seq(3, 4).toDF(\"a\"))\n\n    def readCDF(start: Long, end: Long): DataFrame =\n      CDCReader\n        .changesToBatchDF(\n          deltaLog,\n          start,\n          end,\n          spark,\n          catalogTableOpt = None,\n          readSchemaSnapshot = Some(readSchemaSnapshot)\n        )\n        .drop(CDCReader.CDC_COMMIT_TIMESTAMP)\n        .drop(CDCReader.CDC_COMMIT_VERSION)\n\n    checkAnswer(readCDF(start = 1, end = 1), Seq(Row(1, \"insert\"), Row(2, \"insert\")))\n    checkErrorMatchPVals(\n      intercept[DeltaUnsupportedOperationException] {\n        readCDF(start = 1, end = 3)\n      },\n      \"DELTA_CHANGE_DATA_FEED_INCOMPATIBLE_SCHEMA_CHANGE\",\n      parameters = Map(\n        \"start\" -> \"1\",\n        \"end\" -> \"3\",\n        \"readSchema\" -> \".*\",\n        \"readVersion\" -> \"1\",\n        \"incompatibleVersion\" -> \"2\"\n      )\n    )\n  }\n\n  test(\"time travel read before type change\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n    append(Seq(1).toDF(\"a\").select($\"a\".cast(ByteType)))\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE smallint\")\n    append(Seq(2).toDF(\"a\").select($\"a\".cast(ShortType)))\n\n    val previousVersion = sql(s\"SELECT a FROM delta.`$tempPath` VERSION AS OF 1\")\n    assert(previousVersion.schema(\"a\").dataType === ByteType)\n    checkAnswer(previousVersion, Seq(Row(1)))\n\n    val latestVersion = sql(s\"SELECT a FROM delta.`$tempPath`\")\n    assert(latestVersion.schema(\"a\").dataType === ShortType)\n    checkAnswer(latestVersion, Seq(Row(1), Row(2)))\n  }\n\n  test(\"compatibility with char/varchar columns\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte, c char(3), v varchar(3)) USING DELTA\")\n    append(Seq((1.toByte, \"abc\", \"def\")).toDF(\"a\", \"c\", \"v\"))\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1, \"abc\", \"def\")))\n\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE smallint\")\n    append(Seq((2.toShort, \"ghi\", \"jkl\")).toDF(\"a\", \"c\", \"v\"))\n    assert(readDeltaTable(tempPath).schema ===\n      new StructType()\n        .add(\"a\", ShortType)\n        .add(\"c\", StringType, nullable = true,\n          metadata = new MetadataBuilder()\n            .putString(\"__CHAR_VARCHAR_TYPE_STRING\", \"char(3)\")\n            .build()\n        )\n        .add(\"v\", StringType, nullable = true,\n          metadata = new MetadataBuilder()\n            .putString(\"__CHAR_VARCHAR_TYPE_STRING\", \"varchar(3)\")\n            .build()))\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1, \"abc\", \"def\"), Row(2, \"ghi\", \"jkl\")))\n\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN c TYPE string\")\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN v TYPE string\")\n    append(Seq((3.toShort, \"longer string 1\", \"longer string 2\")).toDF(\"a\", \"c\", \"v\"))\n    assert(readDeltaTable(tempPath).schema ===\n      new StructType()\n        .add(\"a\", ShortType)\n        .add(\"c\", StringType)\n        .add(\"v\", StringType))\n    checkAnswer(readDeltaTable(tempPath),\n      Seq(Row(1, \"abc\", \"def\"), Row(2, \"ghi\", \"jkl\"), Row(3, \"longer string 1\", \"longer string 2\")))\n  }\n\n  test(\"type widening with row tracking\") {\n    // Start with row tracking disabled.\n    sql(s\"CREATE TABLE $tableSQLIdentifier (a TINYINT) USING DELTA \" +\n        s\"TBLPROPERTIES('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'false')\")\n\n    append(Seq(1).toDF(\"a\").select($\"a\".cast(ByteType)))\n\n    sql(s\"ALTER TABLE $tableSQLIdentifier SET TBLPROPERTIES \" +\n        s\"('${DeltaConfigs.ROW_TRACKING_ENABLED.key}' = 'true')\")\n\n    def readWithRowTracking(): DataFrame =\n      readDeltaTable(tempPath).select(\n        \"a\",\n        \"_metadata.row_id\",\n        \"_metadata.base_row_id\",\n        \"_metadata.row_commit_version\",\n        \"_metadata.default_row_commit_version\"\n      )\n\n    // [base_]row_id starting at 0, [default_]row_commit_version set to 3 when\n    // Row Tracking got enabled.\n    checkAnswer(readWithRowTracking(), Seq(Row(1, 0, 0, 3, 3)))\n\n    sql(s\"UPDATE $tableSQLIdentifier SET a = 2 WHERE a = 1\")\n    // Existing row moved to new file: base_row_id = 1. Version updated to 5\n    // (4 is internal row tracking backfill).\n    checkAnswer(readWithRowTracking(), Seq(Row(2, 0, 1, 5, 5)))\n\n    sql(s\"ALTER TABLE $tableSQLIdentifier CHANGE COLUMN a TYPE INT\")\n    // No changes when enabling Type Widening.\n    checkAnswer(readWithRowTracking(), Seq(Row(2, 0, 1, 5, 5)))\n\n    append(Seq(Int.MaxValue).toDF(\"a\"))\n    // Adding new row in a new file [base_]row_id set to 2, [default_]row_commit_version set to 7.\n    checkAnswer(readWithRowTracking(), Seq(\n      Row(2, 0, 1, 5, 5),\n      Row(Int.MaxValue, 2, 2, 7, 7)\n    ))\n  }\n}\n\n/** Trait collecting tests covering type widening + column mapping. */\ntrait TypeWideningColumnMappingTests {\n    self: QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin =>\n\n  import testImplicits._\n\n  for (mappingMode <- Seq(IdMapping.name, NameMapping.name)) {\n    test(s\"change column type and rename it, mappingMode=$mappingMode\") {\n      withSQLConf((DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey, mappingMode)) {\n        sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n      }\n      // Add some data and change type of column `a`.\n      addSingleFile(Seq(1), ByteType)\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE smallint\")\n      addSingleFile(Seq(2), ShortType)\n      assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n      checkAnswer(sql(s\"SELECT a FROM delta.`$tempPath`\"), Seq(Row(1), Row(2)))\n\n      // Rename column `a` to `a (with reserved characters)`, add more data.\n      val newColumnName = \"a (with reserved characters)\"\n      sql(s\"ALTER TABLE delta.`$tempPath` RENAME COLUMN a TO `$newColumnName`\")\n      assert(readDeltaTable(tempPath).schema(newColumnName).dataType === ShortType)\n      checkAnswer(\n        sql(s\"SELECT `$newColumnName` FROM delta.`$tempPath`\"), Seq(Row(1), Row(2))\n      )\n      append(Seq(3).toDF(newColumnName).select(col(newColumnName).cast(ShortType)))\n      checkAnswer(\n        sql(s\"SELECT `$newColumnName` FROM delta.`$tempPath`\"), Seq(Row(1), Row(2), Row(3))\n      )\n\n      // Change column type again, add more data.\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN `$newColumnName` TYPE int\")\n      assert(\n        readDeltaTable(tempPath).schema(newColumnName).dataType === IntegerType)\n      append(Seq(4).toDF(newColumnName).select(col(newColumnName).cast(IntegerType)))\n      checkAnswer(\n        sql(s\"SELECT `$newColumnName` FROM delta.`$tempPath`\"),\n        Seq(Row(1), Row(2), Row(3), Row(4))\n      )\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        // All files except the last one should be rewritten.\n        expectedNumFilesRewritten = 3,\n        expectedColumnTypes = Map(newColumnName -> IntegerType)\n      )\n    }\n\n    test(s\"dropped column shouldn't cause files to be rewritten, mappingMode=$mappingMode\") {\n      withSQLConf((DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey, mappingMode)) {\n        sql(s\"CREATE TABLE delta.`$tempPath` (a byte, b byte) USING DELTA\")\n      }\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (1, 1)\")\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN b TYPE int\")\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (2, 2)\")\n      sql(s\"ALTER TABLE delta.`$tempPath` DROP COLUMN b\")\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> ByteType)\n      )\n    }\n\n    test(s\"swap column names and change type, mappingMode=$mappingMode\") {\n      withSQLConf((DeltaConfigs.COLUMN_MAPPING_MODE.defaultTablePropertyKey, mappingMode)) {\n        sql(s\"CREATE TABLE delta.`$tempPath` (a byte, b byte) USING DELTA\")\n      }\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (1, 1)\")\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN b TYPE int\")\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (2, 2)\")\n      sql(s\"ALTER TABLE delta.`$tempPath` RENAME COLUMN b TO c\")\n      sql(s\"ALTER TABLE delta.`$tempPath` RENAME COLUMN a TO b\")\n      sql(s\"ALTER TABLE delta.`$tempPath` RENAME COLUMN c TO a\")\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (3, 3)\")\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 1,\n        expectedColumnTypes = Map(\n          \"a\" -> IntegerType,\n          \"b\" -> ByteType\n        )\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningGeneratedColumnsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types._\n\n/**\n * Suite covering changing the type of columns referenced by generated columns.\n */\nclass TypeWideningGeneratedColumnsSuite\n  extends QueryTest\n    with TypeWideningTestMixin\n    with GeneratedColumnTest\n    with TypeWideningGeneratedColumnTests\n\ntrait TypeWideningGeneratedColumnTests extends GeneratedColumnTest {\n  self: QueryTest with TypeWideningTestMixin =>\n\n  test(\"generated column with type change\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a byte, b byte, gen int\",\n        generatedColumns = Map(\"gen\" -> \"hash(a)\"),\n        partitionColumns = Seq.empty\n      )\n      sql(\"INSERT INTO t (a, b) VALUES (2, 2)\")\n      checkAnswer(sql(\"SELECT hash(a) FROM t\"), Row(1765031574))\n\n      // Changing the type of a column that a generated column depends on is not allowed.\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN a TYPE SMALLINT\")\n        },\n        \"DELTA_GENERATED_COLUMNS_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"a\",\n          \"generatedColumns\" -> \"gen -> hash(a)\"\n        ))\n\n      // Changing the type of `b` is allowed as it's not referenced by the generated column.\n      sql(\"ALTER TABLE t CHANGE COLUMN b TYPE SMALLINT\")\n      assert(sql(\"SELECT * FROM t\").schema(\"b\").dataType === ShortType)\n      checkAnswer(sql(\"SELECT * FROM t\"), Row(2, 2, 1765031574))\n    }\n  }\n\n  test(\"generated column on nested field with type change\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a struct<x: byte, y: byte>, gen int\",\n        generatedColumns = Map(\"gen\" -> \"hash(a.x)\"),\n        partitionColumns = Seq.empty\n      )\n      sql(\"INSERT INTO t (a) VALUES (named_struct('x', 2, 'y', 3))\")\n      checkAnswer(sql(\"SELECT hash(a.x) FROM t\"), Row(1765031574))\n\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN a.x TYPE SMALLINT\")\n        },\n        \"DELTA_GENERATED_COLUMNS_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"a.x\",\n          \"generatedColumns\" -> \"gen -> hash(a.x)\"\n      ))\n\n      // Changing the type of a.y is allowed since it's not referenced by the CHECK constraint.\n      sql(\"ALTER TABLE t CHANGE COLUMN a.y TYPE SMALLINT\")\n      checkAnswer(sql(\"SELECT * FROM t\"), Row(Row(2, 3), 1765031574) :: Nil)\n    }\n  }\n\n  test(\"generated column on arrays and maps with type change\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a array<struct<f: byte, g: byte>>, gen tinyint\",\n        generatedColumns = Map(\"gen\" -> \"a[0].f\"),\n        partitionColumns = Seq.empty\n      )\n      sql(\"INSERT INTO t (a) VALUES (array(named_struct('f', 7, 'g', 8)))\")\n      checkAnswer(sql(\"SELECT gen FROM t\"), Row(7))\n\n      sql(\"ALTER TABLE t CHANGE COLUMN a.element.g TYPE SMALLINT\")\n      checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"ALTER TABLE t CHANGE COLUMN a.element.f TYPE SMALLINT\")\n        },\n        \"DELTA_GENERATED_COLUMNS_DEPENDENT_COLUMN_CHANGE\",\n        parameters = Map(\n          \"columnName\" -> \"a.element.f\",\n          \"generatedColumns\" -> \"gen -> a[0].f\"\n        ))\n\n      checkAnswer(sql(\"SELECT gen FROM t\"), Row(7))\n    }\n  }\n\n  test(\"generated column with type evolution\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a byte, gen int\",\n        generatedColumns = Map(\"gen\" -> \"hash(a)\"),\n        partitionColumns = Seq.empty\n      )\n      sql(\"INSERT INTO t (a) VALUES (2)\")\n      checkAnswer(sql(\"SELECT hash(a) FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        checkError(\n        intercept[DeltaAnalysisException] {\n          sql(\"INSERT INTO t (a) VALUES (200)\")\n        },\n        \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n        parameters = Map(\n          \"columnName\" -> \"a\",\n          \"columnType\" -> \"TINYINT\",\n          \"dataType\" -> \"INT\",\n          \"generatedColumns\" -> \"gen -> hash(a)\"\n        ))\n      }\n    }\n  }\n\n  test(\"generated column on nested field with type evolution\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a struct<x: byte, y: byte>, gen int\",\n        generatedColumns = Map(\"gen\" -> \"hash(a.x)\"),\n        partitionColumns = Seq.empty\n      )\n      sql(\"INSERT INTO t (a) VALUES (named_struct('x', 2, 'y', 3))\")\n      checkAnswer(sql(\"SELECT gen FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\"INSERT INTO t (a) VALUES (named_struct('x', 200, 'y', CAST(5 AS byte)))\")\n          },\n          \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"a.x\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"generatedColumns\" -> \"gen -> hash(a.x)\"\n          )\n        )\n\n        // changing the type of struct field `a.y` when it's not\n        // the field referenced by the generated column is allowed.\n        sql(\"INSERT INTO t (a) VALUES (named_struct('x', CAST(2 AS byte), 'y', 200))\")\n        checkAnswer(sql(\"SELECT gen FROM t\"), Seq(Row(1765031574), Row(1765031574)))\n      }\n    }\n  }\n\n  test(\"generated column on arrays and maps with type evolution\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a array<byte>, gen INT\",\n        generatedColumns = Map(\"gen\" -> \"hash(a[0])\"),\n        partitionColumns = Seq.empty\n      )\n      sql(\"INSERT INTO t (a) VALUES (array(2, 3))\")\n      checkAnswer(sql(\"SELECT gen FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        // Insert by name is not supported by type evolution.\n        checkError(\n          intercept[DeltaAnalysisException] {\n            spark.createDataFrame(Seq(Tuple1(Array(200000, 12345))))\n              .toDF(\"a\").withColumn(\"a\", col(\"a\").cast(\"array<int>\"))\n              .write.format(\"delta\").mode(\"append\").saveAsTable(\"t\")\n          },\n          \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"a.element\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"generatedColumns\" -> \"gen -> hash(a[0])\"\n          )\n        )\n\n        checkAnswer(sql(\"SELECT gen FROM t\"), Row(1765031574))\n      }\n    }\n  }\n\n  test(\"generated column on nested field with complex type evolution\") {\n    withTable(\"t\") {\n      createTable(\n        tableName = \"t\",\n        path = None,\n        schemaString = \"a struct<x: struct<z: byte, h: byte>, y: byte>, gen int\",\n        generatedColumns = Map(\"gen\" -> \"hash(a.x.z)\"),\n        partitionColumns = Seq.empty\n      )\n\n      sql(\"INSERT INTO t (a) VALUES (named_struct('x', named_struct('z', 2, 'h', 3), 'y', 4))\")\n      checkAnswer(sql(\"SELECT gen FROM t\"), Row(1765031574))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        checkError(\n          intercept[DeltaAnalysisException] {\n            sql(\n              s\"\"\"\n                 | INSERT INTO t (a) VALUES (\n                 |   named_struct('x', named_struct('z', 200, 'h', 3), 'y', 4)\n                 | )\n                 |\"\"\".stripMargin\n            )\n          },\n          \"DELTA_GENERATED_COLUMNS_DATA_TYPE_MISMATCH\",\n          parameters = Map(\n            \"columnName\" -> \"a.x.z\",\n            \"columnType\" -> \"TINYINT\",\n            \"dataType\" -> \"INT\",\n            \"generatedColumns\" -> \"gen -> hash(a.x.z)\"\n          )\n        )\n\n        // changing the type of struct field `a.y` when it's not\n        // the field referenced by the generated column is allowed.\n        sql(\n          \"\"\"\n            | INSERT INTO t (a) VALUES (\n            |   named_struct('x', named_struct('z', CAST(2 AS BYTE), 'h', 2002), 'y', 1030)\n            | )\n            |\"\"\".stripMargin\n        )\n        checkAnswer(sql(\"SELECT gen FROM t\"), Seq(Row(1765031574), Row(1765031574)))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningInsertSchemaEvolutionBasicSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.hadoop.fs.Path\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, Dataset, QueryTest, Row, SaveMode}\nimport org.apache.spark.sql.catalyst.plans.logical.{AppendData, LogicalPlan}\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n\n/**\n * Suite covering widening columns and fields type as part of automatic schema evolution in INSERT\n * when the type widening table feature is supported.\n */\nclass TypeWideningInsertSchemaEvolutionBasicSuite\n    extends QueryTest\n    with DeltaDMLTestUtils\n    with TypeWideningTestMixin\n    with TypeWideningInsertSchemaEvolutionBasicTests {\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\")\n  }\n}\n\n/**\n * Tests covering type widening during schema evolution in INSERT.\n */\ntrait TypeWideningInsertSchemaEvolutionBasicTests\n  extends DeltaInsertIntoTest\n  with TypeWideningTestCases {\n  self: QueryTest with TypeWideningTestMixin with DeltaDMLTestUtils =>\n\n  import testImplicits._\n  import scala.collection.JavaConverters._\n\n  for {\n    testCase <- restrictedAutomaticWideningTestCases ++ supportedTestCases\n  } {\n    test(s\"INSERT - always automatic type widening \" +\n      s\"${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n      append(testCase.initialValuesDF)\n\n      withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"always\") {\n        testCase.additionalValuesDF\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .insertInto(s\"delta.`$tempPath`\")\n      }\n\n      assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.toType)\n      checkAnswerWithTolerance(\n        actualDf = readDeltaTable(tempPath).select(\"value\"),\n        expectedDf = testCase.expectedResult.select($\"value\".cast(testCase.toType)),\n        toType = testCase.toType\n      )\n    }\n  }\n\n  for {\n    testCase <- restrictedAutomaticWideningTestCases ++ supportedTestCases\n  } {\n    test(s\"INSERT - never automatic type widening \" +\n      s\"${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n      append(testCase.initialValuesDF)\n\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString,\n        DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"never\") {\n        testCase.additionalValuesDF\n          .write\n          .format(\"delta\")\n          .mode(\"append\")\n          .option(\"mergeSchema\", \"true\")\n          .insertInto(s\"delta.`$tempPath`\")\n      }\n\n      assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.fromType)\n    }\n  }\n\n  test(s\"INSERT - logs for missed opportunity for conversion\") {\n    val testCase = restrictedAutomaticWideningTestCases.head\n\n    append(testCase.initialValuesDF)\n\n    val events = Log4jUsageLogger.track {\n      withSQLConf(\n          SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString,\n          DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"same_family_type\") {\n        testCase.additionalValuesDF\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .insertInto(s\"delta.`$tempPath`\")\n      }\n    }\n\n    assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.fromType)\n    assert(events.exists(event => event.metric == \"tahoeEvent\" &&\n      event.tags.get(\"opType\") == Option(\"delta.typeWidening.missedAutomaticWidening\")))\n  }\n\n  test(s\"INSERT - no logs for lack of missed opportunity for conversion\") {\n    val testCase = supportedTestCases.head\n    append(testCase.initialValuesDF)\n\n    val events = Log4jUsageLogger.track {\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString) {\n        testCase.additionalValuesDF\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .insertInto(s\"delta.`$tempPath`\")\n      }\n    }\n\n    assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.toType)\n    assert(!events.exists(event => event.metric == \"tahoeEvent\" &&\n      event.tags.get(\"opType\") == Option(\"delta.typeWidening.missedAutomaticWidening\")))\n  }\n\n  for {\n    testCase <- supportedTestCases ++ restrictedAutomaticWideningTestCases\n  } {\n    test(s\"INSERT - automatic type widening ${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n      append(testCase.initialValuesDF)\n      testCase.additionalValuesDF\n        .write\n        .mode(\"append\")\n        .insertInto(s\"delta.`$tempPath`\")\n\n      assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.toType)\n      checkAnswerWithTolerance(\n        actualDf = readDeltaTable(tempPath).select(\"value\"),\n        expectedDf = testCase.expectedResult.select($\"value\".cast(testCase.toType)),\n        toType = testCase.toType\n      )\n    }\n  }\n\n  for {\n    testCase <- unsupportedTestCases\n  } {\n    test(s\"INSERT - unsupported automatic type widening \" +\n      s\"${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n      append(testCase.initialValuesDF)\n      // Test cases for some of the unsupported type changes may overflow while others only have\n      // values that can be implicitly cast to the narrower type - e.g. double ->float.\n      // We set storeAssignmentPolicy to LEGACY to ignore overflows, this test only ensures\n      // that the table schema didn't evolve.\n      withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString) {\n        testCase.additionalValuesDF.write.mode(\"append\")\n          .insertInto(s\"delta.`$tempPath`\")\n        assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.fromType)\n      }\n    }\n  }\n\n  test(\"INSERT - type widening isn't applied when schema evolution is disabled\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n    withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"false\") {\n      // Insert integer values. This should succeed and downcast the values to short.\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (1), (2)\")\n      assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n      checkAnswer(readDeltaTable(tempPath),\n        Seq(1, 2).toDF(\"a\").select($\"a\".cast(ShortType)))\n    }\n\n    // Check that we would actually widen if schema evolution was enabled.\n    withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n      sql(s\"INSERT INTO delta.`$tempPath` VALUES (3), (4)\")\n      assert(readDeltaTable(tempPath).schema(\"a\").dataType === IntegerType)\n      checkAnswer(readDeltaTable(tempPath), Seq(1, 2, 3, 4).toDF(\"a\"))\n    }\n  }\n\n  test(\"INSERT - type widening isn't applied when it's disabled\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n    enableTypeWidening(tempPath, enabled = false)\n    sql(s\"INSERT INTO delta.`$tempPath` VALUES (1), (2)\")\n    assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n    checkAnswer(readDeltaTable(tempPath),\n      Seq(1, 2).toDF(\"a\").select($\"a\".cast(ShortType)))\n  }\n\n  test(\"INSERT - type widening is triggered when schema evolution is enabled via option\") {\n    val tableName = \"type_widening_insert_into_table\"\n    withTable(tableName) {\n      sql(s\"CREATE TABLE $tableName (a short) USING DELTA\")\n      Seq(1, 2).toDF(\"a\")\n        .write\n        .format(\"delta\")\n        .mode(\"append\")\n        .option(\"mergeSchema\", \"true\")\n        .insertInto(tableName)\n\n      val result = spark.read.format(\"delta\").table(tableName)\n      assert(result.schema(\"a\").dataType === IntegerType)\n      checkAnswer(result, Seq(1, 2).toDF(\"a\"))\n    }\n  }\n\n  /**\n   * Short-hand to create a logical plan to insert into the table. This captures the state of the\n   * table at the time the method is called, e.p. the type widening property value that will be used\n   * during analysis.\n   */\n  private def createInsertPlan(df: DataFrame): LogicalPlan = {\n    val relation = DataSourceV2Relation.create(\n      table = DeltaTableV2(spark, new Path(tempPath)),\n      catalog = None,\n      identifier = None,\n      options = new CaseInsensitiveStringMap(Map.empty[String, String].asJava)\n    )\n    AppendData.byPosition(relation, df.queryExecution.logical)\n  }\n\n  test(s\"INSERT - fail if type widening gets enabled by a concurrent transaction\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n    enableTypeWidening(tempPath, enabled = false)\n    val insert = createInsertPlan(Seq(1).toDF(\"a\"))\n    // Enabling type widening after analysis doesn't impact the insert operation: the data is\n    // already cast to conform to the current schema.\n    enableTypeWidening(tempPath, enabled = true)\n    DataFrameUtils.ofRows(spark, insert).collect()\n    assert(readDeltaTable(tempPath).schema == new StructType().add(\"a\", ShortType))\n    checkAnswer(readDeltaTable(tempPath), Row(1))\n  }\n\n  test(s\"INSERT - fail if type widening gets disabled by a concurrent transaction\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n    val insert = createInsertPlan(Seq(1).toDF(\"a\"))\n    // Disabling type widening after analysis results in inserting data with a wider type into the\n    // table while type widening is actually disabled during execution. We do actually widen the\n    // table schema in that case because `short` and `int` are both stored as INT32 in parquet.\n    enableTypeWidening(tempPath, enabled = false)\n    DataFrameUtils.ofRows(spark, insert).collect()\n    assert(readDeltaTable(tempPath).schema == new StructType().add(\"a\", IntegerType))\n    checkAnswer(readDeltaTable(tempPath), Row(1))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningInsertSchemaEvolutionExtendedSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.types._\n\nclass TypeWideningInsertSchemaEvolutionExtendedSuite\n  extends QueryTest\n  with DeltaDMLTestUtils\n  with TypeWideningTestMixin\n  with TypeWideningInsertSchemaEvolutionExtendedTests {\n}\n\ntrait TypeWideningInsertSchemaEvolutionExtendedTests\n  extends DeltaInsertIntoTest\n  with TypeWideningTestCases {\n  self: QueryTest with TypeWideningTestMixin with DeltaDMLTestUtils =>\n\n  testInserts(\"top-level type evolution\")(\n    initialData = TestData(\"a int, b short\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n    partitionBy = Seq(\"a\"),\n    overwriteWhere = \"a\" -> 1,\n    insertData = TestData(\"a int, b int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4 }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType)),\n    withSchemaEvolution = true\n  )\n\n  testInserts(\"top-level type evolution with column upcast\")(\n    initialData = TestData(\"a int, b short, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 2, \"c\": 3 }\"\"\")),\n    partitionBy = Seq(\"a\"),\n    overwriteWhere = \"a\" -> 1,\n    insertData = TestData(\"a int, b int, c short\", Seq(\"\"\"{ \"a\": 1, \"b\": 5, \"c\": 6 }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType)\n      .add(\"c\", IntegerType)),\n    withSchemaEvolution = true\n  )\n\n  testInserts(\"top-level type evolution with schema evolution\")(\n    initialData = TestData(\"a int, b short\", Seq(\"\"\"{ \"a\": 1, \"b\": 2 }\"\"\")),\n    partitionBy = Seq(\"a\"),\n    overwriteWhere = \"a\" -> 1,\n    insertData = TestData(\"a int, b int, c int\", Seq(\"\"\"{ \"a\": 1, \"b\": 4, \"c\": 5 }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType)\n      .add(\"c\", IntegerType)),\n    // SQL INSERT by name doesn't support schema evolution.\n    excludeInserts = insertsSQL.intersect(insertsByName),\n    withSchemaEvolution = true\n  )\n\n\n  testInserts(\"nested type evolution by position\")(\n    initialData = TestData(\n      \"key int, s struct<x: short, y: short>, m map<string, short>, a array<short>\",\n      Seq(\"\"\"{ \"key\": 1, \"s\": { \"x\": 1, \"y\": 2 }, \"m\": { \"p\": 3 }, \"a\": [4] }\"\"\")),\n    partitionBy = Seq(\"key\"),\n    overwriteWhere = \"key\" -> 1,\n    insertData = TestData(\n      \"key int, s struct<x: short, y: int>, m map<string, int>, a array<int>\",\n      Seq(\"\"\"{ \"key\": 1, \"s\": { \"x\": 4, \"y\": 5 }, \"m\": { \"p\": 6 }, \"a\": [7] }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"s\", new StructType()\n        .add(\"x\", ShortType)\n        .add(\"y\", IntegerType))\n      .add(\"m\", MapType(StringType, IntegerType))\n      .add(\"a\", ArrayType(IntegerType))),\n    withSchemaEvolution = true\n  )\n\n\n  testInserts(\"nested type evolution with struct evolution by position\")(\n    initialData = TestData(\n      \"key int, s struct<x: short, y: short>, m map<string, short>, a array<short>\",\n      Seq(\"\"\"{ \"key\": 1, \"s\": { \"x\": 1, \"y\": 2 }, \"m\": { \"p\": 3 }, \"a\": [4] }\"\"\")),\n    partitionBy = Seq(\"key\"),\n    overwriteWhere = \"key\" -> 1,\n    insertData = TestData(\n      \"key int, s struct<x: short, y: int, z: int>, m map<string, int>, a array<int>\",\n      Seq(\"\"\"{ \"key\": 1, \"s\": { \"x\": 4, \"y\": 5, \"z\": 8 }, \"m\": { \"p\": 6 }, \"a\": [7] }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"s\", new StructType()\n        .add(\"x\", ShortType)\n        .add(\"y\", IntegerType)\n        .add(\"z\", IntegerType))\n      .add(\"m\", MapType(StringType, IntegerType))\n      .add(\"a\", ArrayType(IntegerType))),\n    withSchemaEvolution = true\n  )\n\n\n  testInserts(\"nested struct type evolution with field upcast\")(\n    initialData = TestData(\n      \"key int, s struct<x: int, y: short>\",\n      Seq(\"\"\"{ \"key\": 1, \"s\": { \"x\": 1, \"y\": 2 } }\"\"\")),\n    partitionBy = Seq(\"key\"),\n    overwriteWhere = \"key\" -> 1,\n    insertData = TestData(\n      \"key int, s struct<x: short, y: int>\",\n      Seq(\"\"\"{ \"key\": 1, \"s\": { \"x\": 4, \"y\": 5 } }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"s\", new StructType()\n        .add(\"x\", IntegerType)\n        .add(\"y\", IntegerType))),\n    withSchemaEvolution = true\n  )\n\n  // Interestingly, we introduced a special case to handle schema evolution / casting for structs\n  // directly nested into an array. This doesn't always work with maps or with elements that\n  // aren't a struct (see other tests).\n  testInserts(\"nested struct type evolution with field upcast in array\")(\n    initialData = TestData(\n      \"key int, a array<struct<x: int, y: short>>\",\n      Seq(\"\"\"{ \"key\": 1, \"a\": [ { \"x\": 1, \"y\": 2 } ] }\"\"\")),\n    partitionBy = Seq(\"key\"),\n    overwriteWhere = \"key\" -> 1,\n    insertData = TestData(\n      \"key int, a array<struct<x: short, y: int>>\",\n      Seq(\"\"\"{ \"key\": 1, \"a\": [ { \"x\": 3, \"y\": 4 } ] }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"key\", IntegerType)\n      .add(\"a\", ArrayType(new StructType()\n        .add(\"x\", IntegerType)\n        .add(\"y\", IntegerType)))),\n    withSchemaEvolution = true\n  )\n\n  // maps now allow type evolution for INSERT by position and name in SQL and dataframe.\n  testInserts(\"nested struct type evolution with field upcast in map\")(\n    initialData = TestData(\n      \"key int, m map<string, struct<x: int, y: short>>\",\n      Seq(\"\"\"{ \"key\": 1, \"m\": { \"a\": { \"x\": 1, \"y\": 2 } } }\"\"\")),\n    partitionBy = Seq(\"key\"),\n    overwriteWhere = \"key\" -> 1,\n    insertData = TestData(\n      \"key int, m map<string, struct<x: short, y: int>>\",\n      Seq(\"\"\"{ \"key\": 1, \"m\": { \"a\": { \"x\": 3, \"y\": 4 } } }\"\"\")),\n    expectedResult = ExpectedResult.Success(new StructType()\n      .add(\"key\", IntegerType)\n      // Type evolution was applied in the map.\n      .add(\"m\", MapType(StringType, new StructType()\n        .add(\"x\", IntegerType)\n        .add(\"y\", IntegerType)))),\n    withSchemaEvolution = true\n  )\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningMergeIntoSchemaEvolutionSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy\nimport org.apache.spark.sql.types._\n\n/**\n * Suite covering widening columns and fields type as part of automatic schema evolution in MERGE\n * INTO when the type widening table feature is supported.\n */\nclass TypeWideningMergeIntoSchemaEvolutionSuite\n    extends TypeWideningMergeIntoSchemaEvolutionTests\n    with DeltaDMLTestUtils\n    with TypeWideningTestMixin {\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\")\n  }\n}\n\n/**\n * Tests covering type widening during schema evolution in MERGE INTO.\n */\ntrait TypeWideningMergeIntoSchemaEvolutionTests extends QueryTest\n    with MergeIntoSQLTestUtils\n    with MergeIntoSchemaEvolutionMixin\n    with TypeWideningTestCases {\n  self: QueryTest with TypeWideningTestMixin with DeltaDMLTestUtils =>\n\n  import testImplicits._\n\n  test(s\"MERGE - always automatic type widening TINYINT -> DOUBLE\") {\n    withTable(\"source\") {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n      sql(\"CREATE TABLE source (a double) USING DELTA\")\n      sql(\"INSERT INTO source VALUES (3.0), (-10.5)\")\n\n      withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"always\") {\n        // Merge double values. This should succeed and widen the short column to double.\n        executeMerge(\n          tgt = s\"delta.`$tempPath` t\",\n          src = \"source\",\n          cond = \"0 = 1\",\n          clauses = insert(\"*\")\n        )\n        assert(readDeltaTable(tempPath).schema(\"a\").dataType === DoubleType)\n        checkAnswer(readDeltaTable(tempPath),\n          Seq(3.0, -10.5).toDF(\"a\"))\n      }\n    }\n  }\n\n  test(s\"MERGE - never automatic type widening TINYINT -> INT\") {\n    withTable(\"source\") {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n      sql(\"CREATE TABLE source (a int) USING DELTA\")\n      sql(\"INSERT INTO source VALUES (1), (2)\")\n\n      withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"never\",\n        SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString) {\n        // Merge int values into short column. This should not widen the target schema.\n        executeMerge(\n          tgt = s\"delta.`$tempPath` t\",\n          src = \"source\",\n          cond = \"0 = 1\",\n          clauses = insert(\"*\")\n        )\n        assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n      }\n    }\n  }\n\n  for {\n    testCase <- supportedTestCases ++ restrictedAutomaticWideningTestCases\n  } {\n    test(s\"MERGE - automatic type widening ${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n      withTable(\"source\") {\n        testCase.additionalValuesDF.write.format(\"delta\").saveAsTable(\"source\")\n        append(testCase.initialValuesDF)\n\n        // We mainly want to ensure type widening is correctly applied to the schema. We use a\n        // trivial insert only merge to make it easier to validate results.\n        executeMerge(\n          tgt = s\"delta.`$tempPath` t\",\n          src = \"source\",\n          cond = \"0 = 1\",\n          clauses = insert(\"*\"))\n\n        assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.toType)\n        checkAnswerWithTolerance(\n          actualDf = readDeltaTable(tempPath).select(\"value\"),\n          expectedDf = testCase.expectedResult.select($\"value\".cast(testCase.toType)),\n          toType = testCase.toType\n        )\n      }\n    }\n  }\n\n  for {\n    testCase <- unsupportedTestCases\n  } {\n    test(s\"MERGE - unsupported automatic type widening \" +\n      s\"${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n      withTable(\"source\") {\n        testCase.additionalValuesDF.write.format(\"delta\").saveAsTable(\"source\")\n        append(testCase.initialValuesDF)\n\n        // Test cases for some of the unsupported type changes may overflow while others only have\n        // values that can be implicitly cast to the narrower type - e.g. double ->float.\n        // We set storeAssignmentPolicy to LEGACY to ignore overflows, this test only ensures\n        // that the table schema didn't evolve.\n        withSQLConf(SQLConf.STORE_ASSIGNMENT_POLICY.key -> StoreAssignmentPolicy.LEGACY.toString) {\n          executeMerge(\n            tgt = s\"delta.`$tempPath` t\",\n            src = \"source\",\n            cond = \"0 = 1\",\n            clauses = insert(\"*\"))\n          assert(readDeltaTable(tempPath).schema(\"value\").dataType === testCase.fromType)\n        }\n      }\n    }\n  }\n\n  test(\"MERGE - type widening isn't applied when it's disabled\") {\n    withTable(\"source\") {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n      sql(\"CREATE TABLE source (a int) USING DELTA\")\n      sql(\"INSERT INTO source VALUES (1), (2)\")\n      enableTypeWidening(tempPath, enabled = false)\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        // Merge integer values. This should succeed and downcast the values to short.\n        executeMerge(\n          tgt = s\"delta.`$tempPath` t\",\n          src = \"source\",\n          cond = \"0 = 1\",\n          clauses = insert(\"*\")\n        )\n        assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n        checkAnswer(readDeltaTable(tempPath),\n          Seq(1, 2).toDF(\"a\").select($\"a\".cast(ShortType)))\n      }\n    }\n  }\n\n  test(\"MERGE - type widening isn't applied when schema evolution is disabled\") {\n    withTable(\"source\") {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n      sql(\"CREATE TABLE source (a int) USING DELTA\")\n      sql(\"INSERT INTO source VALUES (1), (2)\")\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"false\") {\n        // Merge integer values. This should succeed and downcast the values to short.\n        executeMerge(\n          tgt = s\"delta.`$tempPath` t\",\n          src = \"source\",\n          cond = \"0 = 1\",\n          clauses = insert(\"*\")\n        )\n        assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n        checkAnswer(readDeltaTable(tempPath),\n          Seq(1, 2).toDF(\"a\").select($\"a\".cast(ShortType)))\n      }\n\n      // Check that we would actually widen if schema evolution was enabled.\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n        executeMerge(\n          tgt = s\"delta.`$tempPath` t\",\n          src = \"source\",\n          cond = \"0 = 1\",\n          clauses = insert(\"*\")\n        )\n        assert(readDeltaTable(tempPath).schema(\"a\").dataType === IntegerType)\n        checkAnswer(readDeltaTable(tempPath), Seq(1, 2, 1, 2).toDF(\"a\"))\n      }\n    }\n  }\n\n  /**\n   * Wrapper around testNestedStructsEvolution that constrains the result with and without schema\n   * evolution to be the same: the schema is different but the values should be the same.\n   */\n  protected def testTypeEvolution(name: String)(\n      target: Seq[String],\n      source: Seq[String],\n      targetSchema: StructType,\n      sourceSchema: StructType,\n      cond: String = \"t.key = s.key\",\n      clauses: Seq[MergeClause] = Seq.empty,\n      result: Seq[String],\n      resultSchema: StructType): Unit =\n    testNestedStructsEvolution(s\"MERGE - $name\")(\n      target,\n      source,\n      targetSchema,\n      sourceSchema,\n      cond,\n      clauses,\n      result,\n      resultWithoutEvolution = result,\n      resultSchema = resultSchema)\n\n\n  testTypeEvolution(\"change top-level column short -> int with update\")(\n    target = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 10 }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 20 }\"\"\"),\n    targetSchema = new StructType().add(\"a\", ShortType),\n    sourceSchema = new StructType().add(\"a\", IntegerType),\n    cond = \"t.a = s.a\",\n    clauses = update(\"a = s.a + 1\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": 1 }\"\"\", \"\"\"{ \"a\": 10 }\"\"\"),\n    resultSchema = new StructType().add(\"a\", IntegerType)\n  )\n\n  testTypeEvolution(\"change top-level column short -> int with insert\")(\n    target = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 10 }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 20 }\"\"\"),\n    targetSchema = new StructType().add(\"a\", ShortType),\n    sourceSchema = new StructType().add(\"a\", IntegerType),\n    cond = \"t.a = s.a\",\n    clauses = insert(\"(a) VALUES (s.a)\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 10 }\"\"\", \"\"\"{ \"a\": 20 }\"\"\"),\n    resultSchema = new StructType().add(\"a\", IntegerType)\n  )\n\n  testTypeEvolution(\"updating using narrower value doesn't evolve schema\")(\n    target = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 10 }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 0 }\"\"\", \"\"\"{ \"a\": 20 }\"\"\"),\n    targetSchema = new StructType().add(\"a\", IntegerType),\n    sourceSchema = new StructType().add(\"a\", ShortType),\n    cond = \"t.a = s.a\",\n    clauses = update(\"a = s.a + 1\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": 1 }\"\"\", \"\"\"{ \"a\": 10 }\"\"\"),\n    resultSchema = new StructType().add(\"a\", IntegerType)\n  )\n\n  testTypeEvolution(\"only columns in assignments are widened\")(\n    target = Seq(\"\"\"{ \"a\": 0, \"b\": 5 }\"\"\", \"\"\"{ \"a\": 10, \"b\": 15 }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 0, \"b\": 6 }\"\"\", \"\"\"{ \"a\": 20, \"b\": 16 }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", ShortType)\n      .add(\"b\", ShortType),\n    sourceSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", IntegerType),\n    cond = \"t.a = s.a\",\n    clauses = update(\"a = s.a + 1\") :: Nil,\n    result = Seq(\n      \"\"\"{ \"a\": 1, \"b\": 5 }\"\"\", \"\"\"{ \"a\": 10, \"b\": 15 }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", IntegerType)\n      .add(\"b\", ShortType)\n  )\n\n  testTypeEvolution(\"automatic widening of struct field with struct assignment\")(\n    target = Seq(\"\"\"{ \"s\": { \"a\": 1 } }\"\"\", \"\"\"{ \"s\": { \"a\": 10 } }\"\"\"),\n    source = Seq(\"\"\"{ \"s\": { \"a\": 1 } }\"\"\", \"\"\"{ \"s\": { \"a\": 20 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", ShortType)),\n    sourceSchema = new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", IntegerType)),\n    cond = \"t.s.a = s.s.a\",\n    clauses = update(\"t.s.a = s.s.a + 1\") :: Nil,\n    result = Seq(\"\"\"{ \"s\": { \"a\": 2 } }\"\"\", \"\"\"{ \"s\": { \"a\": 10 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", IntegerType))\n  )\n\n  testTypeEvolution(\"automatic widening of struct field with field assignment\")(\n    target = Seq(\"\"\"{ \"s\": { \"a\": 1 } }\"\"\", \"\"\"{ \"s\": { \"a\": 10 } }\"\"\"),\n    source = Seq(\"\"\"{ \"s\": { \"a\": 1 } }\"\"\", \"\"\"{ \"s\": { \"a\": 20 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", ShortType)),\n    sourceSchema = new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", IntegerType)),\n    cond = \"t.s.a = s.s.a\",\n    clauses = update(\"t.s.a = s.s.a + 1\") :: Nil,\n    result = Seq(\"\"\"{ \"s\": { \"a\": 2 } }\"\"\", \"\"\"{ \"s\": { \"a\": 10 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"s\", new StructType()\n        .add(\"a\", IntegerType))\n  )\n\n  testTypeEvolution(\"automatic widening of map value\")(\n    target = Seq(\"\"\"{ \"m\": { \"a\": 1 } }\"\"\"),\n    source = Seq(\"\"\"{ \"m\": { \"a\": 2 } }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"m\", MapType(StringType, ShortType)),\n    sourceSchema = new StructType()\n      .add(\"m\", MapType(StringType, IntegerType)),\n    // Can't compare maps\n    cond = \"1 = 1\",\n    clauses = update(\"t.m = s.m\") :: Nil,\n    result = Seq(\"\"\"{ \"m\": { \"a\": 2 } }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"m\", MapType(StringType, IntegerType))\n  )\n\n  testTypeEvolution(\"automatic widening of array element\")(\n    target = Seq(\"\"\"{ \"a\": [1, 2] }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": [3, 4] }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", ArrayType(ShortType)),\n    sourceSchema = new StructType()\n      .add(\"a\", ArrayType(IntegerType)),\n    cond = \"t.a != s.a\",\n    clauses = update(\"t.a = s.a\") :: Nil,\n    result = Seq(\"\"\"{ \"a\": [3, 4] }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", ArrayType(IntegerType))\n  )\n\n  testTypeEvolution(\"multiple automatic widening\")(\n    target = Seq(\"\"\"{ \"a\": 1, \"b\": 2  }\"\"\"),\n    source = Seq(\"\"\"{ \"a\": 1, \"b\": 4  }\"\"\", \"\"\"{ \"a\": 5, \"b\": 6  }\"\"\"),\n    targetSchema = new StructType()\n      .add(\"a\", ByteType)\n      .add(\"b\", ShortType),\n    sourceSchema = new StructType()\n      .add(\"a\", ShortType)\n      .add(\"b\", IntegerType),\n    cond = \"t.a = s.a\",\n    clauses = update(\"*\") :: insert(\"*\")  :: Nil,\n    result = Seq(\"\"\"{ \"a\": 1, \"b\": 4  }\"\"\", \"\"\"{ \"a\": 5, \"b\": 6  }\"\"\"),\n    resultSchema = new StructType()\n      .add(\"a\", ShortType)\n      .add(\"b\", IntegerType)\n  )\n\n  for (enabled <- BOOLEAN_DOMAIN)\n  test(s\"MERGE - fail if type widening gets ${if (enabled) \"enabled\" else \"disabled\"} by a \" +\n    \"concurrent transaction\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a short) USING DELTA\")\n    enableTypeWidening(tempPath, !enabled)\n    val target = io.delta.tables.DeltaTable.forPath(tempPath)\n    import testImplicits._\n    val merge = target.as(\"target\")\n      .merge(\n        source = Seq(1L).toDF(\"a\").as(\"source\"),\n        condition = \"target.a = source.a\")\n      .whenNotMatched().insertAll()\n\n    // The MERGE operation was created with the previous type widening value, which will apply\n    // during analysis. Toggle type widening so that the actual MERGE runs with a different setting.\n    enableTypeWidening(tempPath, enabled)\n    intercept[MetadataChangedException] {\n      merge.execute()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningMetadataSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport java.io.File\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.propertyKey\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.types._\n\n/**\n * Suite that covers recording type change metadata in the table schema.\n */\nclass TypeWideningMetadataSuite\n  extends QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningMetadataTests\n    with TypeWideningMetadataEndToEndTests\n    with TypeWideningLeakingMetadataTests\n\n/**\n * Tests covering the [[TypeWideningMetadata]] and [[TypeChange]] classes used to handle the\n * metadata recorded by the Type Widening table feature in the table schema.\n */\ntrait TypeWideningMetadataTests extends QueryTest with DeltaSQLCommandTest {\n  private val testTableName: String = \"delta_type_widening_metadata_test\"\n\n  /** A dummy transaction to be used by tests covering `addTypeWideningMetadata`. */\n  private lazy val txn: OptimisticTransaction = {\n    val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(testTableName))\n    DeltaLog.forTable(spark, TableIdentifier(testTableName))\n        .startTransaction(catalogTableOpt = Some(table))\n  }\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    sql(s\"CREATE TABLE $testTableName (a int) USING delta TBLPROPERTIES (\" +\n      s\"'${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'true', \" +\n      // Force the stable feature to be used by default in tests instead of the preview feature,\n      // which is the one currently enabled by default. Tests that cover aspects specific to the\n      // preview - e.p. populating the `tableVersion` field - explicitly enable the preview feature.\n      s\"'${propertyKey(TypeWideningTableFeature)}' = 'supported')\")\n  }\n\n  override protected def afterAll(): Unit = {\n    sql(s\"DROP TABLE IF EXISTS $testTableName\")\n    super.afterAll()\n  }\n\n  /**\n   * Short-hand to build the metadata for a type change to cut down on repetition.\n   */\n  private def typeChangeMetadata(\n      fromType: String,\n      toType: String,\n      path: String = \"\"): Metadata = {\n    val builder = new MetadataBuilder()\n      .putString(\"fromType\", fromType)\n      .putString(\"toType\", toType)\n    if (path.nonEmpty) {\n      builder.putString(\"fieldPath\", path)\n    }\n    builder.build()\n  }\n\n  test(\"toMetadata/fromMetadata with empty path\") {\n    val typeChange = TypeChange(version = None, IntegerType, LongType, Seq.empty)\n    assert(typeChange.toMetadata === typeChangeMetadata(\"integer\", \"long\"))\n    assert(TypeChange.fromMetadata(typeChange.toMetadata) === typeChange)\n  }\n\n  test(\"toMetadata/fromMetadata with non-empty path\") {\n    val typeChange =\n      TypeChange(version = None, DateType, TimestampNTZType, Seq(\"key\", \"element\"))\n    assert(typeChange.toMetadata ===\n      typeChangeMetadata(\"date\", \"timestamp_ntz\", \"key.element\"))\n    assert(TypeChange.fromMetadata(typeChange.toMetadata) === typeChange)\n  }\n\n  test(\"toMetadata/fromMetadata with tableVersion\") {\n    val typeChange = TypeChange(version = Some(1), ByteType, ShortType, Seq.empty)\n    val expectedMetadata = new MetadataBuilder()\n      .putLong(\"tableVersion\", 1)\n      .putString(\"fromType\", \"byte\")\n      .putString(\"toType\", \"short\")\n      .build()\n    assert(typeChange.toMetadata === expectedMetadata)\n    assert(TypeChange.fromMetadata(typeChange.toMetadata) === typeChange)\n  }\n\n  test(\"fromField with no type widening metadata\") {\n    val field = StructField(\"a\", IntegerType)\n    assert(TypeWideningMetadata.fromField(field) === None)\n  }\n\n  test(\"fromField with empty type widening metadata\") {\n    val field = StructField(\"a\", IntegerType, metadata = new MetadataBuilder()\n      .putMetadataArray(\"delta.typeChanges\", Array.empty[Metadata])\n      .build()\n    )\n    assert(TypeWideningMetadata.fromField(field) === Some(TypeWideningMetadata(Seq.empty)))\n    val otherField = StructField(\"a\", IntegerType)\n    // Empty type widening metadata is discarded.\n    assert(TypeWideningMetadata.fromField(field).get.appendToField(otherField) ===\n      StructField(\"a\", IntegerType))\n  }\n\n  test(\"fromField with single type change\") {\n    val field = StructField(\"a\", IntegerType, metadata = new MetadataBuilder()\n      .putMetadataArray(\"delta.typeChanges\", Array(\n        typeChangeMetadata(\"integer\", \"long\")\n      )).build()\n    )\n    assert(TypeWideningMetadata.fromField(field) ===\n      Some(TypeWideningMetadata(Seq(\n        TypeChange(version = None, IntegerType, LongType, Seq.empty)))))\n    val otherField = StructField(\"a\", IntegerType)\n    assert(TypeWideningMetadata.fromField(field).get.appendToField(otherField) === field)\n  }\n\n  test(\"fromField with multiple type changes\") {\n    val field = StructField(\"a\", IntegerType, metadata = new MetadataBuilder()\n      .putMetadataArray(\"delta.typeChanges\", Array(\n        typeChangeMetadata(\"integer\", \"long\"),\n        typeChangeMetadata(\"decimal(5,0)\", \"decimal(10,2)\", \"element.element\")\n      )).build()\n    )\n    assert(TypeWideningMetadata.fromField(field) ===\n      Some(TypeWideningMetadata(Seq(\n        TypeChange(version = None, IntegerType, LongType, Seq.empty),\n        TypeChange(\n          version = None, DecimalType(5, 0), DecimalType(10, 2), Seq(\"element\", \"element\"))))))\n    val otherField = StructField(\"a\", IntegerType)\n    assert(TypeWideningMetadata.fromField(field).get.appendToField(otherField) === field)\n  }\n\n  test(\"fromField with tableVersion\") {\n    val typeChange = new MetadataBuilder()\n      .putLong(\"tableVersion\", 1)\n      .putString(\"fromType\", \"integer\")\n      .putString(\"toType\", \"long\")\n      .build()\n    val field = StructField(\"a\", IntegerType, metadata = new MetadataBuilder()\n      .putMetadataArray(\"delta.typeChanges\", Array(typeChange))\n      .build()\n    )\n    assert(TypeWideningMetadata.fromField(field) ===\n      Some(TypeWideningMetadata(Seq(\n        TypeChange(version = Some(1), IntegerType, LongType, Seq.empty)))))\n    val otherField = StructField(\"a\", IntegerType)\n    assert(TypeWideningMetadata.fromField(field).get.appendToField(otherField) === field)\n  }\n\n  test(\"appendToField on field with no type widening metadata\") {\n    val field = StructField(\"a\", IntegerType)\n    // Adding empty type widening metadata should not change the field.\n    val emptyMetadata = TypeWideningMetadata(Seq.empty)\n    assert(emptyMetadata.appendToField(field) === field)\n    assert(TypeWideningMetadata.fromField(emptyMetadata.appendToField(field)).isEmpty)\n\n    // Adding single type change should add the metadata to the field and not otherwise change it.\n    val singleMetadata = TypeWideningMetadata(Seq(\n      TypeChange(version = None, IntegerType, LongType, Seq.empty)))\n    assert(singleMetadata.appendToField(field) === field.copy(metadata =\n      new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"integer\", \"long\")\n        )).build()\n      )\n    )\n    val singleMetadataFromField =\n      TypeWideningMetadata.fromField(singleMetadata.appendToField(field))\n    assert(singleMetadataFromField.contains(singleMetadata))\n\n    // Adding multiple type changes should add the metadata to the field and not otherwise change\n    // it.\n    val multipleMetadata = TypeWideningMetadata(Seq(\n      TypeChange(version = None, IntegerType, LongType, Seq.empty),\n      TypeChange(version = None, FloatType, DoubleType, Seq(\"value\"))))\n    assert(multipleMetadata.appendToField(field) === field.copy(metadata =\n      new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"integer\", \"long\"),\n          typeChangeMetadata(\"float\", \"double\", \"value\")\n        )).build()\n      )\n    )\n\n    val multipleMetadataFromField =\n      TypeWideningMetadata.fromField(multipleMetadata.appendToField(field))\n    assert(multipleMetadataFromField.contains(multipleMetadata))\n  }\n\n  test(\"appendToField on field with existing type widening metadata\") {\n    val field = StructField(\"a\", IntegerType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"integer\", \"long\")\n        )).build()\n    )\n    // Adding empty type widening metadata should not change the field.\n    val emptyMetadata = TypeWideningMetadata(Seq.empty)\n    assert(emptyMetadata.appendToField(field) === field)\n    assert(TypeWideningMetadata.fromField(emptyMetadata.appendToField(field)).contains(\n      TypeWideningMetadata(Seq(\n        TypeChange(version = None, IntegerType, LongType, Seq.empty)))\n    ))\n\n    // Adding single type change should add the metadata to the field and not otherwise change it.\n    val singleMetadata = TypeWideningMetadata(Seq(\n      TypeChange(version = None, DecimalType(18, 0), DecimalType(19, 0), Seq.empty)))\n\n    assert(singleMetadata.appendToField(field) === field.copy(\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"integer\", \"long\"),\n          typeChangeMetadata(\"decimal(18,0)\", \"decimal(19,0)\")\n        )).build()\n    ))\n    val singleMetadataFromField =\n      TypeWideningMetadata.fromField(singleMetadata.appendToField(field))\n\n    assert(singleMetadataFromField.contains(TypeWideningMetadata(Seq(\n      TypeChange(version = None, IntegerType, LongType, Seq.empty),\n      TypeChange(version = None, DecimalType(18, 0), DecimalType(19, 0), Seq.empty)))\n    ))\n\n    // Adding multiple type changes should add the metadata to the field and not otherwise change\n    // it.\n    val multipleMetadata = TypeWideningMetadata(Seq(\n      TypeChange(version = None, DecimalType(18, 0), DecimalType(19, 0), Seq.empty),\n      TypeChange(version = None, FloatType, DoubleType, Seq(\"value\"))))\n\n    assert(multipleMetadata.appendToField(field) === field.copy(\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"integer\", \"long\"),\n          typeChangeMetadata(\"decimal(18,0)\", \"decimal(19,0)\"),\n          typeChangeMetadata(\"float\", \"double\", \"value\")\n        )).build()\n    ))\n    val multipleMetadataFromField =\n      TypeWideningMetadata.fromField(multipleMetadata.appendToField(field))\n\n    assert(multipleMetadataFromField.contains(TypeWideningMetadata(Seq(\n      TypeChange(version = None, IntegerType, LongType, Seq.empty),\n      TypeChange(version = None, DecimalType(18, 0), DecimalType(19, 0), Seq.empty),\n      TypeChange(version = None, FloatType, DoubleType, Seq(\"value\"))))\n    ))\n  }\n\n  test(\"addTypeWideningMetadata/removeTypeWideningMetadata with no type changes\") {\n    for {\n      (oldSchema, newSchema) <- Seq(\n        (\"a short\", \"a short\"),\n        (\"a short\", \"a short NOT NULL\"),\n        (\"a short NOT NULL\", \"a short\"),\n        (\"a short NOT NULL\", \"a short COMMENT 'a comment'\"),\n        (\"a string, b int\", \"b int, a string\"),\n        (\"a struct<s1: date, s2: long>\", \"a struct<s2: long, s1: date>\"),\n        (\"a struct<s1: short COMMENT 'a comment'>\", \"a struct<s1: short>\"),\n        (\"a struct<s1: short>\", \"a struct<s1: short COMMENT 'a comment'>\"),\n        (\"a map<int, long>\", \"m map<int, long>\"),\n        (\"a array<timestamp>\", \"a array<timestamp>\"),\n        (\"a map<array<timestamp>, int>\", \"a map<array<timestamp>, int>\"),\n        (\"a array<struct<s1: byte>>\", \"a array<struct<s1: byte>>\")\n      ).map { case (oldStr, newStr) => StructType.fromDDL(oldStr) -> StructType.fromDDL(newStr) }\n    } {\n      withClue(s\"oldSchema = $oldSchema, newSchema = $newSchema\") {\n        val schema = TypeWideningMetadata.addTypeWideningMetadata(txn, newSchema, oldSchema)\n        assert(schema === newSchema)\n        assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) === schema -> Seq.empty)\n      }\n    }\n  }\n\n  test(\"addTypeWideningMetadata/removeTypeWideningMetadata on top-level fields\") {\n    val schemaWithoutMetadata =\n      StructType.fromDDL(\"i int, a array<int>, m map<short, int>\")\n    val firstOldSchema =\n      StructType.fromDDL(\"i byte, a array<byte>, m map<byte, int>\")\n    val secondOldSchema =\n      StructType.fromDDL(\"i short, a array<short>, m map<short, byte>\")\n\n    var schema =\n      TypeWideningMetadata.addTypeWideningMetadata(txn, schemaWithoutMetadata, firstOldSchema)\n\n    assert(schema(\"i\") === StructField(\"i\", IntegerType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\")\n        )).build()\n    ))\n\n    assert(schema(\"a\") === StructField(\"a\", ArrayType(IntegerType),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\", \"element\")\n        )).build()\n    ))\n\n    assert(schema(\"m\") === StructField(\"m\", MapType(ShortType, IntegerType),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"short\", \"key\")\n        )).build()\n    ))\n\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) ===\n      schemaWithoutMetadata -> Seq(\n        Seq.empty -> schema(\"i\"),\n        Seq.empty -> schema(\"a\"),\n        Seq.empty -> schema(\"m\")\n      ))\n    // Second type change on all fields.\n    schema = TypeWideningMetadata.addTypeWideningMetadata(txn, schema, secondOldSchema)\n\n    assert(schema(\"i\") === StructField(\"i\", IntegerType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\"),\n          typeChangeMetadata(\"short\", \"integer\")\n        )).build()\n    ))\n\n    assert(schema(\"a\") === StructField(\"a\", ArrayType(IntegerType),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\", \"element\"),\n          typeChangeMetadata(\"short\", \"integer\", \"element\")\n        )).build()\n    ))\n\n    assert(schema(\"m\") === StructField(\"m\", MapType(ShortType, IntegerType),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"short\", \"key\"),\n          typeChangeMetadata(\"byte\", \"integer\", \"value\")\n        )).build()\n    ))\n\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) ===\n      schemaWithoutMetadata -> Seq(\n        Seq.empty -> schema(\"i\"),\n        Seq.empty -> schema(\"a\"),\n        Seq.empty -> schema(\"m\")\n      ))\n  }\n\n  test(\"addTypeWideningMetadata/removeTypeWideningMetadata on nested fields\") {\n    val schemaWithoutMetadata = StructType.fromDDL(\n      \"s struct<i: int, a: array<map<int, int>>, m: map<map<int, int>, array<int>>>\")\n    val firstOldSchema = StructType.fromDDL(\n      \"s struct<i: byte, a: array<map<byte, int>>, m: map<map<short, int>, array<int>>>\")\n    val secondOldSchema = StructType.fromDDL(\n      \"s struct<i: short, a: array<map<int, short>>, m: map<map<int, int>, array<short>>>\")\n\n    // First type change on all struct fields.\n    var schema =\n      TypeWideningMetadata.addTypeWideningMetadata(txn, schemaWithoutMetadata, firstOldSchema)\n    var struct = schema(\"s\").dataType.asInstanceOf[StructType]\n\n    assert(struct(\"i\") === StructField(\"i\", IntegerType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\")\n        )).build()\n    ))\n\n    assert(struct(\"a\") === StructField(\"a\", ArrayType(MapType(IntegerType, IntegerType)),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\", \"element.key\")\n        )).build()\n    ))\n\n    assert(struct(\"m\") === StructField(\"m\",\n      MapType(MapType(IntegerType, IntegerType), ArrayType(IntegerType)),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"short\", \"integer\", \"key.key\")\n        )).build()\n    ))\n\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) ===\n      schemaWithoutMetadata -> Seq(\n        Seq(\"s\") -> struct(\"i\"),\n        Seq(\"s\") -> struct(\"a\"),\n        Seq(\"s\") -> struct(\"m\")\n      ))\n\n    // Second type change on all struct fields.\n    schema = TypeWideningMetadata.addTypeWideningMetadata(txn, schema, secondOldSchema)\n    struct = schema(\"s\").dataType.asInstanceOf[StructType]\n\n    assert(struct(\"i\") === StructField(\"i\", IntegerType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\"),\n          typeChangeMetadata(\"short\", \"integer\")\n        )).build()\n    ))\n\n    assert(struct(\"a\") === StructField(\"a\", ArrayType(MapType(IntegerType, IntegerType)),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"byte\", \"integer\", \"element.key\"),\n          typeChangeMetadata(\"short\", \"integer\", \"element.value\")\n        )).build()\n    ))\n\n    assert(struct(\"m\") === StructField(\"m\",\n      MapType(MapType(IntegerType, IntegerType), ArrayType(IntegerType)),\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"short\", \"integer\", \"key.key\"),\n          typeChangeMetadata(\"short\", \"integer\", \"value.element\")\n        )).build()\n    ))\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) ===\n      schemaWithoutMetadata -> Seq(\n        Seq(\"s\") -> struct(\"i\"),\n        Seq(\"s\") -> struct(\"a\"),\n        Seq(\"s\") -> struct(\"m\")\n      ))\n  }\n\n  test(\"addTypeWideningMetadata/removeTypeWideningMetadata with added and removed fields\") {\n    val newSchema = StructType.fromDDL(\"a int, b int, d int\")\n    val oldSchema = StructType.fromDDL(\"a int, b short, c int\")\n\n    val schema = TypeWideningMetadata.addTypeWideningMetadata(txn, newSchema, oldSchema)\n    assert(schema(\"a\") === StructField(\"a\", IntegerType))\n    assert(schema(\"d\") === StructField(\"d\", IntegerType))\n    assert(!schema.contains(\"c\"))\n\n    assert(schema(\"b\") === StructField(\"b\", IntegerType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"short\", \"integer\")\n        )).build()\n    ))\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) ===\n      newSchema -> Seq(Seq.empty -> schema(\"b\"))\n    )\n  }\n\n  test(\"addTypeWideningMetadata/removeTypeWideningMetadata with different field position\") {\n    val newSchema = StructType.fromDDL(\"a short, b int, s struct<c: int, d: long>\")\n    val oldSchema = StructType.fromDDL(\"b int, a short, s struct<d: long, c: int>\")\n\n    val schema = TypeWideningMetadata.addTypeWideningMetadata(txn, newSchema, oldSchema)\n    // No type widening metadata is added.\n    assert(schema(\"a\") === StructField(\"a\", ShortType))\n    assert(schema(\"b\") === StructField(\"b\", IntegerType))\n    assert(schema(\"s\") ===\n      StructField(\"s\", new StructType()\n        .add(\"c\", IntegerType)\n        .add(\"d\", LongType)))\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) === newSchema -> Seq.empty)\n  }\n\n  test(\"addTypeWideningMetadata/removeTypeWideningMetadata with preview feature\") {\n    val newSchema = StructType.fromDDL(\"a short\")\n    val oldSchema = StructType.fromDDL(\"a byte\")\n\n    // Create a new transaction with the preview feature supported.\n    val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(testTableName))\n    val txn = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n        .startTransaction(catalogTableOpt = Some(table))\n    txn.updateProtocol(txn.protocol.withFeature(TypeWideningPreviewTableFeature))\n    val schema = TypeWideningMetadata.addTypeWideningMetadata(txn, newSchema, oldSchema)\n\n    // Type widening metadata is added with field `tableVersion` populated as this uses the preview\n    // feature. That field is deprecated in the stable version of the feature.\n    assert(schema(\"a\") === StructField(\"a\", ShortType,\n      metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          new MetadataBuilder()\n          .putLong(\"tableVersion\", 1)\n          .putString(\"fromType\", \"byte\")\n          .putString(\"toType\", \"short\")\n          .build()\n        )).build()\n    ))\n    assert(TypeWideningMetadata.removeTypeWideningMetadata(schema) ===\n      newSchema -> Seq(Seq.empty -> schema(\"a\")))\n  }\n\n  test(\"updateTypeChangeVersion with no type changes\") {\n    val schema = new StructType().add(\"a\", IntegerType)\n    assert(TypeWideningMetadata.updateTypeChangeVersion(schema, 1, 4) === schema)\n  }\n\n  test(\"updateTypeChangeVersion with field with single type change\") {\n    val schema = new StructType()\n      .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          typeChangeMetadata(\"integer\", \"long\")\n        ))\n        .build()\n      )\n\n    assert(TypeWideningMetadata.updateTypeChangeVersion(schema, 1, 4) ===\n      new StructType()\n        .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"integer\", \"long\")\n          ))\n          .build()\n        )\n    )\n  }\n\n  test(\"updateTypeChangeVersion with field with multiple type changes\") {\n    val schema = new StructType()\n      .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"integer\", \"long\"),\n            typeChangeMetadata(\"float\", \"double\", \"value\")\n          ))\n        .build()\n      )\n\n    // Update matching one of the type changes.\n    assert(TypeWideningMetadata.updateTypeChangeVersion(schema, 1, 4) ===\n      new StructType()\n      .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"integer\", \"long\"),\n            typeChangeMetadata(\"float\", \"double\", \"value\")\n          ))\n        .build()\n      )\n    )\n\n    // Update doesn't match any of the recorded type changes.\n    assert(\n      TypeWideningMetadata.updateTypeChangeVersion(schema, 3, 4) === schema\n    )\n  }\n\n  test(\"updateTypeChangeVersion with multiple fields with a type change\") {\n    val schema = new StructType()\n      .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"integer\", \"long\")\n          ))\n        .build())\n      .add(\"b\", ArrayType(IntegerType), nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"short\", \"integer\", \"element\")\n          ))\n        .build())\n\n    // Update both type changes.\n    assert(TypeWideningMetadata.updateTypeChangeVersion(schema, 1, 4) ===\n      new StructType()\n      .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"integer\", \"long\")\n          ))\n        .build())\n      .add(\"b\", ArrayType(IntegerType), nullable = true, metadata = new MetadataBuilder()\n          .putMetadataArray(\"delta.typeChanges\", Array(\n            typeChangeMetadata(\"short\", \"integer\", \"element\")\n          ))\n        .build())\n    )\n\n    // Update doesn't match any of the recorded type changes.\n    assert(\n      TypeWideningMetadata.updateTypeChangeVersion(schema, 3, 4) === schema\n    )\n  }\n}\n\n/**\n * Tests that covers recording type change information as metadata in the table schema. For\n * lower-level tests, see [[TypeWideningMetadataTests]].\n */\ntrait TypeWideningMetadataEndToEndTests {\n  self: QueryTest with TypeWideningTestMixin =>\n\n  def testTypeWideningMetadata(name: String)(\n      initialSchema: String,\n      typeChanges: Seq[(String, String)],\n      expectedJsonSchema: String): Unit =\n    test(name) {\n      sql(s\"CREATE TABLE delta.`$tempPath` ($initialSchema) USING DELTA\")\n      typeChanges.foreach { case (fieldName, newType) =>\n        sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN $fieldName TYPE $newType\")\n      }\n\n      // Parse the schemas as JSON to ignore whitespaces and field order.\n      val actualSchema = JsonUtils.fromJson[Map[String, Any]](readDeltaTable(tempPath).schema.json)\n      val expectedSchema = JsonUtils.fromJson[Map[String, Any]](expectedJsonSchema)\n      assert(actualSchema === expectedSchema,\n        s\"${readDeltaTable(tempPath).schema.prettyJson} did not equal $expectedJsonSchema\"\n      )\n    }\n\n  testTypeWideningMetadata(\"change top-level column type short->int\")(\n    initialSchema = \"a short\",\n    typeChanges = Seq(\"a\" -> \"int\"),\n    expectedJsonSchema =\n      \"\"\"{\n      \"type\": \"struct\",\n      \"fields\": [{\n        \"name\": \"a\",\n        \"type\": \"integer\",\n        \"nullable\": true,\n        \"metadata\": {}\n      }]}\"\"\".stripMargin)\n\n  testTypeWideningMetadata(\"change top-level column type twice byte->short->int\")(\n    initialSchema = \"a byte\",\n    typeChanges = Seq(\"a\" -> \"short\", \"a\" -> \"int\"),\n    expectedJsonSchema =\n      \"\"\"{\n      \"type\": \"struct\",\n      \"fields\": [{\n        \"name\": \"a\",\n        \"type\": \"integer\",\n        \"nullable\": true,\n        \"metadata\": {}\n      }]}\"\"\".stripMargin)\n\n  testTypeWideningMetadata(\"change type in map key and in struct in map value\")(\n    initialSchema = \"a map<byte, struct<b: byte>>\",\n    typeChanges = Seq(\"a.key\" -> \"int\", \"a.value.b\" -> \"short\"),\n    expectedJsonSchema =\n      \"\"\"{\n      \"type\": \"struct\",\n      \"fields\": [{\n        \"name\": \"a\",\n        \"type\": {\n          \"type\": \"map\",\n          \"keyType\": \"integer\",\n          \"valueType\": {\n            \"type\": \"struct\",\n            \"fields\": [{\n              \"name\": \"b\",\n              \"type\": \"short\",\n              \"nullable\": true,\n              \"metadata\": {}\n            }]\n          },\n          \"valueContainsNull\": true\n        },\n        \"nullable\": true,\n        \"metadata\": {}\n      }\n    ]}\"\"\".stripMargin)\n\n\n  testTypeWideningMetadata(\"change type in array and in struct in array\")(\n    initialSchema = \"a array<byte>, b array<struct<c: short>>\",\n    typeChanges = Seq(\"a.element\" -> \"short\", \"b.element.c\" -> \"int\"),\n    expectedJsonSchema =\n      \"\"\"{\n      \"type\": \"struct\",\n      \"fields\": [{\n        \"name\": \"a\",\n        \"type\": {\n          \"type\": \"array\",\n          \"elementType\": \"short\",\n          \"containsNull\": true\n        },\n        \"nullable\": true,\n        \"metadata\": {}\n      },\n      {\n        \"name\": \"b\",\n        \"type\": {\n          \"type\": \"array\",\n          \"elementType\":{\n            \"type\": \"struct\",\n            \"fields\": [{\n              \"name\": \"c\",\n              \"type\": \"integer\",\n              \"nullable\": true,\n              \"metadata\": {}\n            }]\n          },\n          \"containsNull\": true\n        },\n        \"nullable\": true,\n        \"metadata\": { }\n      }\n    ]}\"\"\".stripMargin)\n}\n\n\ntrait TypeWideningLeakingMetadataTests {\n    self: QueryTest with TypeWideningTestMixin =>\n\n  test(\"stream read from type widening does not leak metadata\") {\n    val (t1, t2) = (\"type_widening_table_1\", \"type_widening_table_2\")\n    withTable(t1, t2) {\n      withTempDir { dir =>\n        sql(s\"CREATE TABLE $t1 (part BYTE, value SHORT) USING DELTA PARTITIONED BY (part)\")\n        sql(s\"INSERT INTO $t1 VALUES (1, 1), (2, 2)\")\n        // Change type of both partition and non-partition columns.\n        sql(s\"ALTER TABLE $t1 CHANGE COLUMN part TYPE INT\")\n        sql(s\"ALTER TABLE $t1 CHANGE COLUMN value TYPE INT\")\n        // Stream read from source table\n        val streamDf = spark.readStream.format(\"delta\").table(t1)\n        // Should not contain type widening metadata\n        assert(streamDf.schema.forall(_.metadata.json == \"{}\"))\n\n        // Create and write to another table\n        val q = streamDf.writeStream\n          .partitionBy(\"part\")\n          .trigger(org.apache.spark.sql.streaming.Trigger.AvailableNow())\n          .format(\"delta\")\n          .option(\"checkpointLocation\", new File(dir, \"_checkpoint1\").getCanonicalPath)\n          .toTable(t2)\n        q.awaitTermination()\n\n        // Check target table Delta log\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(t2))\n        assert(deltaLog.update().metadata.schema.forall(_.metadata.json == \"{}\"))\n\n        // Check target table data\n        checkAnswer(spark.table(t2), Seq(Row(1, 1), Row(2, 2)))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningStatsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\nimport org.apache.spark.sql.{QueryTest, Row}\nimport org.apache.spark.sql.catalyst.expressions.{AttributeReference, EqualTo, Expression, Literal}\nimport org.apache.spark.sql.catalyst.plans.logical.LocalRelation\nimport org.apache.spark.sql.types._\n\n/**\n * Suite covering stats and data skipping with type changes.\n */\nclass TypeWideningStatsSuite\n  extends QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningStatsTests\n\ntrait TypeWideningStatsTests { self: QueryTest with TypeWideningTestMixin =>\n\n  import testImplicits._\n\n  /**\n   * Helper to create a table and run tests while enabling/disabling storing stats as JSON string or\n   * strongly-typed structs in checkpoint files. Creates a\n   */\n  def testStats(\n      name: String,\n      partitioned: Boolean,\n      jsonStatsEnabled: Boolean,\n      structStatsEnabled: Boolean)(\n      body: => Unit): Unit =\n    test(s\"$name, partitioned=$partitioned, jsonStatsEnabled=$jsonStatsEnabled, \" +\n        s\"structStatsEnabled=$structStatsEnabled\") {\n      withSQLConf(\n        DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_JSON.defaultTablePropertyKey ->\n          jsonStatsEnabled.toString,\n        DeltaConfigs.CHECKPOINT_WRITE_STATS_AS_STRUCT.defaultTablePropertyKey ->\n          structStatsEnabled.toString\n      ) {\n        val partitionStr = if (partitioned) \"PARTITIONED BY (a)\" else \"\"\n        sql(s\"\"\"\n            |CREATE TABLE delta.`$tempPath` (a smallint, dummy int DEFAULT 1)\n            |USING DELTA\n            |$partitionStr\n            |TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported')\n          \"\"\".stripMargin)\n        body\n      }\n    }\n\n  /** Returns the latest checkpoint for the test table. */\n  def getLatestCheckpoint: LastCheckpointInfo =\n    deltaLog.readLastCheckpointFile().getOrElse {\n      fail(\"Expected the table to have a checkpoint but it didn't\")\n    }\n\n  /** Returns the type used to store JSON stats in the checkpoint if JSON stats are present. */\n  def getJsonStatsType(checkpoint: LastCheckpointInfo): Option[DataType] =\n    checkpoint.checkpointSchema.flatMap {\n      _.findNestedField(Seq(\"add\", \"stats\"))\n    }.map(_._2.dataType)\n\n  /**\n   * Returns the type used to store parsed partition values for the given column in the checkpoint\n   * if these are present.\n   */\n  def getPartitionValuesType(checkpoint: LastCheckpointInfo, colName: String)\n    : Option[DataType] = {\n    checkpoint.checkpointSchema.flatMap {\n      _.findNestedField(Seq(\"add\", \"partitionValues_parsed\", colName))\n    }.map(_._2.dataType)\n  }\n\n  /**\n   * Returns the type used to store parsed stat values for the given column in the checkpoint if\n   * these are present.\n   */\n  def getStructStatsType(checkpoint: LastCheckpointInfo, colName: String)\n    : Option[DataType] = {\n    checkpoint.checkpointSchema.flatMap {\n      _.findNestedField(Seq(\"add\", \"stats_parsed\", \"minValues\", colName))\n    }.map(_._2.dataType)\n  }\n\n  /**\n   * Checks that stats and parsed partition values are stored in the checkpoint when enabled and\n   * that their type matches the expected type.\n   */\n  def checkCheckpointStats(\n      checkpoint: LastCheckpointInfo,\n      colName: String,\n      colType: DataType,\n      partitioned: Boolean,\n      jsonStatsEnabled: Boolean,\n      structStatsEnabled: Boolean): Unit = {\n    val expectedJsonStatsType = if (jsonStatsEnabled) Some(StringType) else None\n    assert(getJsonStatsType(checkpoint) === expectedJsonStatsType)\n\n    val expectedPartitionStats = if (partitioned && structStatsEnabled) Some(colType) else None\n    assert(getPartitionValuesType(checkpoint, colName) === expectedPartitionStats)\n    val expectedStructStats = if (!partitioned && structStatsEnabled) Some(colType) else None\n    assert(getStructStatsType(checkpoint, colName) === expectedStructStats)\n  }\n\n  /**\n   * Reads the test table filtered by the given value and checks that files are skipped as expected.\n   */\n  def checkFileSkipping(filterValue: Any, expectedFilesRead: Long): Unit = {\n    val dataFilter: Expression =\n      EqualTo(AttributeReference(\"a\", IntegerType)(), Literal(filterValue))\n    val files = deltaLog.update().filesForScan(Seq(dataFilter), keepNumRecords = false).files\n    assert(files.size === expectedFilesRead, s\"Expected $expectedFilesRead files to be \" +\n      s\"read but read ${files.size} files.\")\n  }\n\n  for {\n    partitioned <- BOOLEAN_DOMAIN\n    jsonStatsEnabled <- BOOLEAN_DOMAIN\n    structStatsEnabled <- BOOLEAN_DOMAIN\n  }\n  testStats(s\"data skipping after type change\", partitioned, jsonStatsEnabled, structStatsEnabled) {\n    addSingleFile(Seq(1), ShortType)\n    addSingleFile(Seq(2), ShortType)\n    deltaLog.checkpoint()\n    assert(readDeltaTable(tempPath).schema(\"a\").dataType === ShortType)\n    val initialCheckpoint = getLatestCheckpoint\n    checkCheckpointStats(\n      initialCheckpoint, \"a\", ShortType, partitioned, jsonStatsEnabled, structStatsEnabled)\n\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    addSingleFile(Seq(Int.MinValue), IntegerType)\n\n    var checkpoint = getLatestCheckpoint\n    // Ensure there's no new checkpoint after the type change.\n    assert(getLatestCheckpoint.semanticEquals(initialCheckpoint))\n\n    // Struct stats can be used as fallback for non-partition values when json stats are disabled.\n    val canSkipFiles = jsonStatsEnabled || partitioned || structStatsEnabled\n\n    // The last file added isn't part of the checkpoint, it always has stats that can be used for\n    // skipping even when checkpoint stats can't be used for skipping.\n    checkFileSkipping(filterValue = 1, expectedFilesRead = if (canSkipFiles) 1 else 2)\n    checkAnswer(readDeltaTable(tempPath).filter(\"a = 1\"), Row(1, 1))\n\n    checkFileSkipping(filterValue = Int.MinValue, expectedFilesRead = if (canSkipFiles) 1 else 3)\n    checkAnswer(readDeltaTable(tempPath).filter(s\"a = ${Int.MinValue}\"), Row(Int.MinValue, 1))\n\n    // Trigger a new checkpoint after the type change and re-check data skipping.\n    deltaLog.checkpoint()\n    checkpoint = getLatestCheckpoint\n    assert(!checkpoint.semanticEquals(initialCheckpoint))\n    checkCheckpointStats(\n      checkpoint, \"a\", IntegerType, partitioned, jsonStatsEnabled, structStatsEnabled)\n    // When checkpoint stats are completely disabled, the last file added can't be skipped anymore.\n    checkFileSkipping(filterValue = 1, expectedFilesRead = if (canSkipFiles) 1 else 3)\n    checkFileSkipping(filterValue = Int.MinValue, expectedFilesRead = if (canSkipFiles) 1 else 3)\n  }\n\n  for {\n    partitioned <- BOOLEAN_DOMAIN\n    jsonStatsEnabled <- BOOLEAN_DOMAIN\n    structStatsEnabled <- BOOLEAN_DOMAIN\n  }\n  testStats(s\"metadata-only query\", partitioned, jsonStatsEnabled, structStatsEnabled) {\n    addSingleFile(Seq(1), ShortType)\n    addSingleFile(Seq(2), ShortType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    addSingleFile(Seq(Int.MinValue), IntegerType)\n    addSingleFile(Seq(Int.MaxValue), IntegerType)\n\n    // Check that collecting aggregates using a metadata-only query works after the type change.\n    val resultDf = sql(s\"SELECT MIN(a), MAX(a), COUNT(*) FROM delta.`$tempPath`\")\n    val isMetadataOnly = resultDf.queryExecution.optimizedPlan.collectFirst {\n      case l: LocalRelation => l\n    }.nonEmpty\n    assert(isMetadataOnly, \"Expected the query to be metadata-only\")\n    checkAnswer(resultDf, Row(Int.MinValue, Int.MaxValue, 4))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningStreamingSinkSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.Relocated.StreamExecution\nimport org.apache.spark.sql.delta.sources.{DeltaSink, DeltaSQLConf}\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.streaming.OutputMode\nimport org.apache.spark.sql.types._\n\n/**\n * Suite covering automatic type widening in the Delta streaming sink.\n */\nclass TypeWideningStreamingSinkSuite\n  extends DeltaSinkImplicitCastSuiteBase\n  with TypeWideningTestMixin {\n\n  import testImplicits._\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // Set by default confs to enable automatic type widening in all tests. Negative tests should\n    // explicitly disable these.\n    spark.conf.set(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key, \"true\")\n    spark.conf.set(DeltaConfigs.ENABLE_TYPE_WIDENING.defaultTablePropertyKey, \"true\")\n    spark.conf.set(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key, \"true\")\n    // Ensure we don't silently cast test inputs to null on overflow.\n    spark.conf.set(SQLConf.ANSI_ENABLED.key, \"true\")\n  }\n\n  test(\"type is widened if automatic widening set to always\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"CAST(value AS SHORT)\")\n      assert(stream.currentSchema(\"value\").dataType === ShortType)\n      checkAnswer(stream.read(), Row(17))\n\n      withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"always\") {\n        stream.write(2)(\"CAST(value AS DOUBLE)\")\n        assert(stream.currentSchema(\"value\").dataType === DoubleType)\n        checkAnswer(stream.read(), Row(17.0) :: Row(2.0) :: Nil)\n      }\n    }\n  }\n\n  test(\"type isn't widened if automatic widening set to never\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"CAST(value AS SHORT)\")\n      assert(stream.currentSchema(\"value\").dataType === ShortType)\n      checkAnswer(stream.read(), Row(17))\n\n      withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"never\") {\n        stream.write(100)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === ShortType)\n        checkAnswer(stream.read(), Row(17) :: Row(100) :: Nil)\n      }\n    }\n  }\n\n  test(\"type isn't widened if schema evolution is disabled\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"CAST(value AS SHORT)\")\n      assert(stream.currentSchema(\"value\").dataType === ShortType)\n      checkAnswer(stream.read(), Row(17))\n\n      withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"false\") {\n        stream.write(53)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === ShortType)\n        checkAnswer(stream.read(), Row(17) :: Row(53) :: Nil)\n      }\n    }\n  }\n\n  test(\"type isn't widened if type widening is disabled\") {\n    withDeltaStream[Int] { stream =>\n      withSQLConf(DeltaConfigs.ENABLE_TYPE_WIDENING.defaultTablePropertyKey -> \"false\") {\n        stream.write(17)(\"CAST(value AS SHORT)\")\n        assert(stream.currentSchema(\"value\").dataType === ShortType)\n        checkAnswer(stream.read(), Row(17))\n\n        stream.write(53)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === ShortType)\n        checkAnswer(stream.read(), Row(17) :: Row(53) :: Nil)\n      }\n    }\n  }\n\n  test(\"type is widened if type widening and schema evolution are enabled\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"CAST(value AS SHORT)\")\n      assert(stream.currentSchema(\"value\").dataType === ShortType)\n      checkAnswer(stream.read(), Row(17))\n\n      stream.write(Int.MaxValue)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(17) :: Row(Int.MaxValue) :: Nil)\n    }\n  }\n\n  test(\"type can be widened even if type casting is disabled in the sink\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(17)(\"CAST(value AS SHORT)\")\n      assert(stream.currentSchema(\"value\").dataType === ShortType)\n      checkAnswer(stream.read(), Row(17))\n\n      withSQLConf(DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> \"false\") {\n        stream.write(Int.MaxValue)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === IntegerType)\n        checkAnswer(stream.read(), Row(17) :: Row(Int.MaxValue) :: Nil)\n      }\n    }\n  }\n\n  test(\"type isn't changed if it's not a wider type\") {\n    withDeltaStream[Int] { stream =>\n      stream.write(Int.MaxValue)(\"CAST(value AS INT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(Int.MaxValue))\n\n      stream.write(17)(\"CAST(value AS SHORT)\")\n      assert(stream.currentSchema(\"value\").dataType === IntegerType)\n      checkAnswer(stream.read(), Row(Int.MaxValue) :: Row(17) :: Nil)\n    }\n  }\n\n  test(\"type isn't changed if it's not eligible for automatic widening: int -> decimal\") {\n    withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"same_family_type\") {\n      withDeltaStream[Int] { stream =>\n        stream.write(17)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === IntegerType)\n        checkAnswer(stream.read(), Row(17))\n\n        stream.write(567)(\"CAST(value AS DECIMAL(20, 0))\")\n        assert(stream.currentSchema(\"value\").dataType === IntegerType)\n        checkAnswer(stream.read(), Row(17) :: Row(567) :: Nil)\n      }\n    }\n  }\n\n  test(\"type isn't changed if it's not eligible for automatic widening: int -> double\") {\n    withSQLConf(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key -> \"same_family_type\") {\n      withDeltaStream[Int] { stream =>\n        stream.write(17)(\"CAST(value AS INT)\")\n        assert(stream.currentSchema(\"value\").dataType === IntegerType)\n        checkAnswer(stream.read(), Row(17))\n\n        stream.write(567)(\"CAST(value AS DOUBLE)\")\n        assert(stream.currentSchema(\"value\").dataType === IntegerType)\n        checkAnswer(stream.read(), Row(17) :: Row(567) :: Nil)\n      }\n    }\n  }\n\n  test(\"widen type and add a new column with schema evolution\") {\n    withDeltaStream[(Int, Int)] { stream =>\n      stream.write((17, -1))(\"CAST(_1 AS SHORT) AS a\")\n      assert(stream.currentSchema === new StructType().add(\"a\", ShortType))\n      checkAnswer(stream.read(), Row(17))\n\n      stream.write((12, 3456))(\"CAST(_1 AS INT) AS a\", \"CAST(_2 AS DECIMAL(10, 2)) AS b\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", DecimalType(10, 2)))\n      checkAnswer(stream.read(), Row(17, null) :: Row(12, 3456) :: Nil)\n    }\n  }\n\n  test(\"widen type during write with missing column\") {\n    withDeltaStream[(Int, Int)] { stream =>\n      stream.write((17, 45))(\"CAST(_1 AS SHORT) AS a\", \"CAST(_2 AS LONG) AS b\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", ShortType)\n        .add(\"b\", LongType))\n      checkAnswer(stream.read(), Row(17, 45))\n\n      stream.write((12, -1))(\"CAST(_1 AS INT) AS a\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", IntegerType)\n        .add(\"b\", LongType))\n      checkAnswer(stream.read(), Row(17, 45) :: Row(12, null) :: Nil)\n    }\n  }\n\n  test(\"widen type after column rename and drop\") {\n    withDeltaStream[(Int, Int)] { stream =>\n      stream.write((17, 45))(\"CAST(_1 AS SHORT) AS a\", \"CAST(_2 AS DECIMAL(10, 0)) AS b\")\n      assert(stream.currentSchema === new StructType()\n        .add(\"a\", ShortType)\n        .add(\"b\", DecimalType(10, 0)))\n      checkAnswer(stream.read(), Row(17, 45))\n\n      sql(\n        s\"\"\"\n           |ALTER TABLE delta.`${stream.deltaLog.dataPath}` SET TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.minReaderVersion' = '2',\n           |  'delta.minWriterVersion' = '5'\n           |)\n         \"\"\".stripMargin)\n      sql(s\"ALTER TABLE delta.`${stream.deltaLog.dataPath}` DROP COLUMN b\")\n      sql(s\"ALTER TABLE delta.`${stream.deltaLog.dataPath}` RENAME COLUMN a to c\")\n      assert(stream.currentSchema === new StructType().add(\"c\", ShortType))\n\n      stream.write((12, -1))(\"CAST(_1 AS INT) AS c\")\n      assert(stream.currentSchema === new StructType().add(\"c\", IntegerType))\n      checkAnswer(stream.read(), Row(17) :: Row(12) :: Nil)\n    }\n  }\n\n  test(\"type widening in addBatch\") {\n    withTempDir { tempDir =>\n      val tablePath = tempDir.getAbsolutePath\n      val deltaLog = DeltaLog.forTable(spark, tablePath)\n      sqlContext.sparkContext.setLocalProperty(StreamExecution.QUERY_ID_KEY, \"streaming_query\")\n      val sink = DeltaSink(\n        sqlContext,\n        path = deltaLog.dataPath,\n        partitionColumns = Seq.empty,\n        outputMode = OutputMode.Append(),\n        options = new DeltaOptions(options = Map.empty, conf = spark.sessionState.conf)\n      )\n\n      val schema = new StructType().add(\"value\", ShortType)\n\n      {\n        val data = Seq(0, 1).toDF(\"value\").selectExpr(\"CAST(value AS SHORT)\")\n        sink.addBatch(0, data)\n        val df = spark.read.format(\"delta\").load(tablePath)\n        assert(df.schema === schema)\n        checkAnswer(df, Row(0) :: Row(1) :: Nil)\n      }\n      {\n        val data = Seq(2, 3).toDF(\"value\").selectExpr(\"CAST(value AS INT)\")\n        sink.addBatch(1, data)\n        val df = spark.read.format(\"delta\").load(tablePath)\n        assert(df.schema === new StructType().add(\"value\", IntegerType))\n        checkAnswer(df, Row(0) :: Row(1) :: Row(2) :: Row(3) :: Nil)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningStreamingSourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport java.io.File\n\nimport com.databricks.spark.util.{Log4jUsageLogger, MetricDefinitions}\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\nimport org.apache.spark.sql.util.ScalaExtensions._\nimport org.scalatest.BeforeAndAfterAll\n\nimport org.apache.spark.{SparkException, SparkThrowable}\nimport org.apache.spark.SparkArithmeticException\nimport org.apache.spark.sql.{DataFrame, Row, SaveMode}\nimport org.apache.spark.sql.functions.{col, count, lit}\nimport org.apache.spark.sql.streaming._\nimport org.apache.spark.sql.test.SQLTestUtils\nimport org.apache.spark.sql.types._\n\n/**\n * Suite covering streaming reads from a Delta table that had a column type widened, **without**\n * schema tracking, i.e. widening type changes don't require users to set a SQL conf to proceed.\n */\nclass TypeWideningStreamingSourceSuite extends TypeWideningStreamingSourceTests\n  with TypeWideningStreamingSourceWithoutSchemaTrackingTests {\n\n  override protected val schemaTrackingEnabled: Boolean = false\n\n  // Changes are not blocked with schema tracking disabled, this is a no-op.\n  override protected def withUnblockedTypeChanges(fn: => Unit): Unit = fn\n}\n\n/**\n * Suite covering streaming reads from a Delta table that had a column type widened, **with**\n * schema tracking, i.e. users must manually acknowledge type changes by setting a SQL conf for\n * the stream to resume processing.\n */\nclass TypeWideningStreamingSourceSchemaTrackingSuite extends TypeWideningStreamingSourceTests\n  with TypeWideningStreamingSourceSchemaTrackingTests {\n\n  override protected val schemaTrackingEnabled: Boolean = true\n\n  override protected def withUnblockedTypeChanges(fn: => Unit): Unit =\n    withSQLConf(\"spark.databricks.delta.streaming.allowSourceColumnTypeChange\" -> \"always\")(fn)\n}\n\ntrait TypeWideningStreamingSourceTestMixin\n  extends TypeWideningTestMixin\n  with BeforeAndAfterAll { self: StreamTest =>\n\n  /** Whether the suite uses schema tracking to handle widening type changes. */\n  protected val schemaTrackingEnabled: Boolean\n\n  /** Unblocks the stream after a widening type change. */\n  protected def withUnblockedTypeChanges(fn: => Unit): Unit\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    spark.sessionState.conf.setConf(\n      DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING, schemaTrackingEnabled)\n    spark.udf.register(\"scala_udf\", (x: Int) => x + 1)\n  }\n\n  override def afterAll(): Unit = {\n    // The scala UDF is a temporary function, no need to drop it.\n    super.afterAll()\n  }\n\n  /** Short-hand to read a data stream from the Delta table at the given location. */\n  protected def readStream(\n      path: File,\n      checkpointDir: File,\n      options: Map[String, String] = Map.empty): DataFrame = {\n    val allOptions = options ++ Option.when(schemaTrackingEnabled)(\n      DeltaOptions.SCHEMA_TRACKING_LOCATION -> checkpointDir.toString\n    )\n    spark.readStream.format(\"delta\")\n      .options(allOptions)\n      .load(path.getCanonicalPath)\n  }\n\n  /** Test action checking that the stream fails due to a metadata change - typ. a schema change. */\n  object ExpectMetadataEvolutionException {\n    def apply(): StreamAction = if (schemaTrackingEnabled) {\n      ExpectFailure[DeltaRuntimeException] { ex =>\n        assert(ex.asInstanceOf[DeltaRuntimeException].getErrorClass ===\n          \"DELTA_STREAMING_METADATA_EVOLUTION\")\n        }\n    } else {\n     ExpectFailure[DeltaIllegalStateException] { ex =>\n       assert(ex.asInstanceOf[SparkThrowable].getErrorClass ===\n         \"DELTA_SCHEMA_CHANGED_WITH_VERSION\")\n      }\n    }\n  }\n\n  /** Test action checking that the stream fails due to a type change being blocked. */\n  object ExpectTypeChangeBlockedException {\n    def apply(): StreamAction =\n      ExpectFailure[DeltaRuntimeException] { ex =>\n        assert(ex.asInstanceOf[DeltaRuntimeException].getErrorClass ===\n          \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\")\n        assert(ex.asInstanceOf[DeltaRuntimeException].getMessage.contains(\"TYPE WIDENING\"))\n      }\n  }\n\n  /** Test action checking that the stream fails due to an unsupported type change. */\n  object ExpectIncompatibleSchemaChangeException {\n    def apply(): StreamAction =\n      ExpectFailure[DeltaIllegalStateException] { ex =>\n        assert(ex.asInstanceOf[DeltaIllegalStateException].getErrorClass ===\n          \"DELTA_SCHEMA_CHANGED_WITH_VERSION\")\n      }\n  }\n}\n\n/**\n * Common tests for type widening when streaming from a Delta source.\n * Can run both with and without schema tracking.\n */\ntrait TypeWideningStreamingSourceTests\n  extends StreamTest\n  with SQLTestUtils\n  with TypeWideningStreamingSourceTestMixin {\n\n  import testImplicits._\n\n  /**\n   * Test a streaming query with a type widening operation. Creates a Delta source with two columns\n   * `widened` and `other` of type `byte` and widens the `widened` column to `int`. The query under\n   * test is used to read from the table and checked against the expected result.\n   * @param name           Test name.\n   * @param query          Streaming query to apply on the source.\n   * @param expectedResult In case of success, checks the last batch of data written by the stream\n   *                       matches the expected result. In case of failure, the caller provides a\n   *                       check to perform on the exception.\n   * @param outputMode     Output mode of the streaming query. `Append` by default but can be\n   *                       overriden to e.g. `Complete` for aggregations.\n   */\n  private def testStreamTypeWidening(\n      name: String,\n      query: DataFrame => DataFrame,\n      partitionBy: Option[String] = None,\n      expectedResult: ExpectedResult[Seq[Row]],\n      outputMode: OutputMode = OutputMode.Append()): Unit = {\n    test(s\"type change - $name\") {\n      withTempDir { dir =>\n        val partitionByStr = partitionBy.map(p => s\"PARTITIONED BY ($p)\").getOrElse(\"\")\n        sql(s\"CREATE TABLE delta.`$dir` (widened byte, other byte) USING DELTA $partitionByStr\")\n        val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n        testStream(query(readStream(dir, checkpointDir)), outputMode)(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1, 1)\") },\n          ProcessAllAvailable(),\n          Execute { _ => sql(s\"ALTER TABLE delta.`$dir`ALTER COLUMN widened TYPE int\") },\n          ExpectMetadataEvolutionException()\n        )\n\n        val streamActions = expectedResult match {\n          case ExpectedResult.Success(rows: Seq[Row @unchecked]) =>\n            Seq(\n              Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789, 2)\") },\n              ProcessAllAvailable(),\n              CheckLastBatch(rows: _*)\n            )\n          case ExpectedResult.Failure(checkError) =>\n            Seq(AssertOnQuery { q =>\n              val ex = intercept[StreamingQueryException] {\n                q.processAllAvailable()\n              }\n              val cause = if (ex.getCause.getMessage.contains(\n                \"Provided schema doesn't match to the schema for existing state!\")) {\n                // State store schema mismatches were non-spark exception until Spark 3.5. We wrap\n                // them into a spark exception to be able to check them consistently across spark\n                // versions.\n                new SparkException(\n                  message = ex.getCause.getMessage,\n                  cause = ex,\n                  errorClass = Some(\"STATE_STORE_KEY_SCHEMA_NOT_COMPATIBLE\"),\n                  messageParameters = Map.empty\n                )\n              } else {\n                assert(ex.getCause.isInstanceOf[SparkThrowable])\n                ex.getCause.asInstanceOf[SparkThrowable]\n              }\n              checkError(cause)\n              true\n            })\n        }\n\n        // We need to unblock the type change to let the stream make progress.\n        withUnblockedTypeChanges {\n          testStream(query(readStream(dir, checkpointDir)), outputMode)(\n            StartStream(checkpointLocation = checkpointDir.toString) +:\n              streamActions: _*\n          )\n        }\n      }\n    }\n  }\n\n  testStreamTypeWidening(\"filter\",\n    query = _.where(col(\"widened\") > 10),\n    expectedResult = ExpectedResult.Success(Seq(Row(123456789, 2)))\n  )\n\n  testStreamTypeWidening(\"projection\",\n    query = _.withColumn(\"add\", col(\"widened\") + col(\"other\")),\n    expectedResult = ExpectedResult.Success(Seq(Row(123456789, 2, 123456791)))\n  )\n\n  testStreamTypeWidening(\"projection partition column\",\n    query = _.withColumn(\"add\", col(\"widened\") + col(\"other\")),\n    partitionBy = Some(\"widened\"),\n    expectedResult = ExpectedResult.Success(Seq(Row(123456789, 2, 123456791)))\n  )\n\n  testStreamTypeWidening(\"widen unused scala udf field\",\n    query = _.selectExpr(\"scala_udf(other)\"),\n    expectedResult = ExpectedResult.Success(Seq(Row(3)))\n  )\n\n  testStreamTypeWidening(\"widen scala udf argument\",\n    query = _.selectExpr(\"scala_udf(widened)\"),\n    expectedResult = ExpectedResult.Success(Seq(Row(123456790)))\n  )\n\n  testStreamTypeWidening(\"widen aggregation grouping key\",\n    query = _.groupBy(\"widened\").agg(count(col(\"other\"))),\n    expectedResult = ExpectedResult.Failure { ex =>\n      assert(ex.getErrorClass === \"STATE_STORE_KEY_SCHEMA_NOT_COMPATIBLE\")\n    },\n    outputMode = OutputMode.Complete()\n  )\n\n  testStreamTypeWidening(\"widen aggregation expression\",\n    query = _.groupBy(\"other\").agg(count(col(\"widened\"))),\n    expectedResult = ExpectedResult.Success(Seq(Row(1, 1), Row(2, 1))),\n    outputMode = OutputMode.Complete()\n  )\n\n  testStreamTypeWidening(\"widen aggregation expression partition column\",\n    query = _.groupBy(\"other\").agg(count(col(\"widened\"))),\n    partitionBy = Some(\"widened\"),\n    expectedResult = ExpectedResult.Success(Seq(Row(1, 1), Row(2, 1))),\n    outputMode = OutputMode.Complete()\n  )\n\n  testStreamTypeWidening(\"widen aggregation expression after projection\",\n    query = _.groupBy(col(\"widened\") + lit(1).cast(ByteType)).agg(count(col(\"other\"))),\n    expectedResult = ExpectedResult.Failure { ex =>\n      assert(ex.getErrorClass === \"STATE_STORE_KEY_SCHEMA_NOT_COMPATIBLE\")\n    },\n    outputMode = OutputMode.Complete()\n  )\n\n  testStreamTypeWidening(\"widen limit\",\n    query = _.select(\"widened\").limit(1),\n    expectedResult = ExpectedResult.Success(Seq.empty)\n  )\n\n  testStreamTypeWidening(\"widen distinct\",\n    query = _.select(\"widened\").distinct(),\n    expectedResult = ExpectedResult.Failure { ex =>\n      assert(ex.getErrorClass === \"STATE_STORE_KEY_SCHEMA_NOT_COMPATIBLE\")\n    }\n  )\n\n  testStreamTypeWidening(\"widen drop duplicates\",\n    query = _.select(\"widened\").dropDuplicates(),\n    expectedResult = ExpectedResult.Failure { ex =>\n      assert(ex.getErrorClass === \"STATE_STORE_KEY_SCHEMA_NOT_COMPATIBLE\")\n    },\n    outputMode = OutputMode.Update()\n  )\n\n  testStreamTypeWidening(\"widen drop duplicates with watermark\",\n    query = _.select(\"widened\")\n      .withColumn(\"watermark\", lit(\"2025-02-04\").cast(\"timestamp\"))\n      .withWatermark(\"watermark\", \"0 seconds\")\n      .dropDuplicatesWithinWatermark(),\n    expectedResult = ExpectedResult.Failure { ex =>\n      assert(ex.getErrorClass === \"STATE_STORE_KEY_SCHEMA_NOT_COMPATIBLE\")\n    },\n    outputMode = OutputMode.Update()\n  )\n\n  testStreamTypeWidening(\"widen flatMap groups with state\",\n    query = _.select(\"widened\").as[Int]\n      .groupByKey(x => x)\n        .flatMapGroupsWithState(\n          outputMode = OutputMode.Update,\n          timeoutConf = GroupStateTimeout.NoTimeout\n        )((key: Int, values: Iterator[Int], state: GroupState[Int]) => {\n          Iterator(values.max)\n        })\n      .toDF(),\n    expectedResult = ExpectedResult.Success(Seq(Row(123456789))),\n    outputMode = OutputMode.Update()\n  )\n\n  test(\"widening type change then restore back\") {\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE delta.`$dir` (a byte) USING DELTA\")\n      val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n      testStream(readStream(dir, checkpointDir))(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1)\") },\n        ProcessAllAvailable(),\n        Execute { _ => sql(s\"ALTER TABLE delta.`$dir`ALTER COLUMN a TYPE int\") },\n        // Widening a column type requires restarting the stream so that the new, wider schema is\n        // used to process the batch.\n        ExpectMetadataEvolutionException()\n      )\n\n      if (schemaTrackingEnabled) {\n        testStream(readStream(dir, checkpointDir))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          // The type change is blocked until the user reviews it and unblocks the stream.\n          ExpectTypeChangeBlockedException()\n        )\n      }\n\n      withUnblockedTypeChanges {\n        testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789)\") },\n          ProcessAllAvailable(),\n          CheckLastBatch(123456789),\n          // Restore will narrow the type back, the schema change fails the query.\n          Execute { _ => sql(s\"RESTORE delta.`$dir` VERSION AS OF 1\") },\n          if (schemaTrackingEnabled) {\n            // With schema tracking, the first try evolves the tracked schema. The unsupported\n            // type change is surfaced on the next retry.\n            ExpectMetadataEvolutionException()\n          } else {\n            ExpectIncompatibleSchemaChangeException()\n          }\n        )\n      }\n\n      // Retrying doesn't allow the narrowing type change to go through.\n      withUnblockedTypeChanges {\n        testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          ExpectIncompatibleSchemaChangeException()\n        )\n      }\n    }\n  }\n\n  for { (name: String, toType: DataType) <- Seq(\n    (\"narrowing\", ByteType),\n    (\"arbitrary\", StringType))\n  } {\n    test(s\"$name type changes are not supported\") {\n      withTempDir { dir =>\n        sql(s\"CREATE TABLE delta.`$dir` (a int) USING DELTA\")\n        val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n        testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1)\") },\n          ProcessAllAvailable(),\n          Execute { _ =>\n            // Overwrite the table schema to apply an arbitrary type change.\n            spark\n              .createDataFrame(\n                sparkContext.emptyRDD[Row],\n                StructType.fromDDL(s\"a ${toType.sql}\"))\n              .write\n              .format(\"delta\")\n              .mode(SaveMode.Overwrite)\n              .option(\"overwriteSchema\", \"true\")\n              .save(dir.getCanonicalPath)\n          },\n          if (schemaTrackingEnabled) {\n            // With schema tracking, the first try evolves the tracked schema. The unsupported\n            // type change is surfaced on the next retry.\n            ExpectMetadataEvolutionException()\n          } else {\n            ExpectIncompatibleSchemaChangeException()\n          }\n        )\n\n        // Try to restart the stream even though the error is not retryable and it will fail again.\n        testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (2)\") },\n          ExpectIncompatibleSchemaChangeException()\n        )\n      }\n    }\n  }\n\n  test(\"type change in delta source writing to a delta sink\") {\n    // End-to-end test with a delta source and a delta sink.\n    withTempDir { sourceDir =>\n      withTempDir { sinkDir =>\n        // The test mixin implicitly enables type widening on all tables, disable type widening on\n        // the sink initially for this test.\n        sql(s\"CREATE TABLE delta.`$sourceDir` (a byte) USING DELTA\")\n        sql(\n          s\"\"\"\n             |CREATE TABLE delta.`$sinkDir` (a byte) USING DELTA\n             |TBLPROPERTIES('delta.enableTypeWidening' = 'false')\n           \"\"\".stripMargin)\n        val checkpointDir = new File(sinkDir, \"checkpoint_dir\")\n\n        def runStream(mergeSchema: Boolean): Unit = try {\n          withUnblockedTypeChanges {\n            val q = readStream(sourceDir, checkpointDir)\n              .writeStream\n              .format(\"delta\")\n              .option(\"checkpointLocation\", checkpointDir.toString)\n              .option(\"mergeSchema\", mergeSchema.toString)\n              .start(sinkDir.getCanonicalPath)\n            q.processAllAvailable()\n            q.stop()\n          }\n        } catch {\n          case e: StreamingQueryException =>\n            // Unwrap the exception for convenience\n            throw e.getCause\n        }\n\n        // Start with no type change.\n        sql(s\"INSERT INTO delta.`$sourceDir` VALUES (1)\")\n        runStream(mergeSchema = false)\n        checkAnswer(readDeltaTable(sinkDir.toString), Seq(Row(1)))\n\n        // Change type of column 'a' and introduce a new column 'b'. Schema evolution is enabled\n        // so the new column 'b' is added to the sink, but type widening is disabled on the sink so\n        // the type of column 'a' remains INT: values are downcasted from INT to BYTE on write.\n        sql(s\"ALTER TABLE delta.`$sourceDir`ALTER COLUMN a TYPE int\")\n        sql(s\"ALTER TABLE delta.`$sourceDir`ADD COLUMN b int\")\n        sql(s\"INSERT INTO delta.`$sourceDir` VALUES (2, 2)\")\n\n        if (schemaTrackingEnabled) {\n          val evolutionException = intercept[DeltaRuntimeException] {\n            runStream(mergeSchema = true)\n          }\n          assert(evolutionException.getErrorClass === \"DELTA_STREAMING_METADATA_EVOLUTION\")\n        }\n        runStream(mergeSchema = true)\n        assert(readDeltaTable(sinkDir.toString).schema(\"a\").dataType === ByteType)\n        assert(readDeltaTable(sinkDir.toString).schema(\"b\").dataType === IntegerType)\n        checkAnswer(readDeltaTable(sinkDir.toString), Seq(Row(1, null), Row(2, 2)))\n\n        // Enable type widening on the sink and insert a value in 'a' that won't fit, first with\n        // schema evolution disabled: the type of column 'a' in the sink isn't automatically changed\n        // to INT and values are downcast: the value overflows and fails.\n        sql(s\"ALTER TABLE delta.`$sinkDir` SET TBLPROPERTIES('delta.enableTypeWidening' = 'true')\")\n        sql(s\"INSERT INTO delta.`$sourceDir` VALUES (${Int.MaxValue}, ${Int.MaxValue})\")\n\n        def getSparkArithmeticException(ex: Throwable): SparkArithmeticException = ex match {\n          case e: SparkArithmeticException => e\n          case e: Throwable if e.getCause != null => getSparkArithmeticException(e.getCause)\n          case e => fail(s\"Unexpected exception: $e\")\n        }\n\n        val ex = intercept[Throwable] {\n          runStream(mergeSchema = false)\n        }\n        assert(getSparkArithmeticException(ex).getErrorClass === \"CAST_OVERFLOW_IN_TABLE_INSERT\")\n\n        // Retry with schema evolution enabled. Type widening is also enabled on the sink, the type\n        // of column 'a' is widened to INT and the write succeeds.\n        runStream(mergeSchema = true)\n        assert(readDeltaTable(sinkDir.toString).schema(\"a\").dataType === IntegerType)\n        assert(readDeltaTable(sinkDir.toString).schema(\"b\").dataType === IntegerType)\n        checkAnswer(\n          readDeltaTable(sinkDir.toString),\n          Seq(Row(1, null), Row(2, 2), Row(Int.MaxValue, Int.MaxValue))\n        )\n      }\n    }\n  }\n}\n\n/** Tests that specifically cover type widening without schema tracking. */\ntrait TypeWideningStreamingSourceWithoutSchemaTrackingTests\n  extends StreamTest\n  with SQLTestUtils\n  with TypeWideningStreamingSourceTestMixin {\n\n  import testImplicits._\n\n  test(\"schema changed event is logged for type widening\") {\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE delta.`$dir` (widened byte) USING DELTA\")\n      val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n      val logs = Log4jUsageLogger.track {\n        testStream(readStream(dir, checkpointDir))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1)\") },\n          ProcessAllAvailable(),\n          Execute { _ => sql(s\"ALTER TABLE delta.`$dir` ALTER COLUMN widened TYPE int\") },\n          ExpectMetadataEvolutionException()\n        )\n\n        testStream(readStream(dir, checkpointDir))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789)\") },\n          ProcessAllAvailable(),\n          CheckLastBatch(Row(123456789))\n        )\n      }\n\n      // Filter for the schema changed event\n      val schemaChangedEvents = logs\n        .filter(_.metric == MetricDefinitions.EVENT_TAHOE.name)\n        .filter(_.tags.get(\"opType\").contains(\"delta.streaming.source.schemaChanged\"))\n\n      assert(schemaChangedEvents.size === 1, \"A single schema changed events should be logged\")\n\n      val eventData = JsonUtils.fromJson[Map[String, Any]](schemaChangedEvents.head.blob)\n      assert(eventData(\"currentVersion\") === 0)\n      assert(eventData(\"newVersion\") === 2)\n      assert(eventData(\"retryable\") === true)\n      assert(eventData(\"backfilling\") === false)\n      assert(eventData(\"readChangeDataFeed\") === false)\n      assert(eventData(\"typeWideningEnabled\") === true)\n      assert(eventData(\"enableSchemaTrackingForTypeWidening\") === false)\n      assert(eventData(\"containsWideningTypeChanges\") === true)\n    }\n  }\n\n  test(\"schema changed event is not logged when there are no schema changes\") {\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE delta.`$dir` (widened byte) USING DELTA\")\n      val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n      val logs = Log4jUsageLogger.track {\n        testStream(readStream(dir, checkpointDir))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1)\") },\n          // This doesn't do anything since the type is already `byte`.\n          Execute { _ => sql(s\"ALTER TABLE delta.`$dir` ALTER COLUMN widened TYPE byte\") },\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (100)\") },\n          ProcessAllAvailable(),\n          CheckAnswer(1, 100)\n        )\n      }\n\n      // Filter for the schema changed event\n      val schemaChangedEvents = logs\n        .filter(_.metric == MetricDefinitions.EVENT_TAHOE.name)\n        .filter(_.tags.get(\"opType\").contains(\"delta.streaming.source.schemaChanged\"))\n\n      assert(schemaChangedEvents.isEmpty, \"No schema changed events should be logged\")\n    }\n  }\n}\n\n/** Tests that specifically cover schema tracking for type widening. */\ntrait TypeWideningStreamingSourceSchemaTrackingTests\n  extends StreamTest\n  with SQLTestUtils\n  with TypeWideningStreamingSourceTestMixin {\n\n  import testImplicits._\n\n  test(\n    \"type change first without schemaTrackingLocation and unblock using schemaTrackingLocation\") {\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE delta.`$dir` (widened byte) USING DELTA\")\n      val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n      def readWithoutSchemaTrackingLog(): DataFrame =\n        spark.readStream.format(\"delta\").load(dir.getCanonicalPath)\n\n      testStream(readWithoutSchemaTrackingLog())(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1)\") },\n        ProcessAllAvailable(),\n        CheckAnswer(1)\n      )\n\n      testStream(readWithoutSchemaTrackingLog())(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        Execute { _ => sql(s\"ALTER TABLE delta.`$dir`ALTER COLUMN widened TYPE int\") },\n        Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789)\") },\n        ExpectFailure[DeltaStreamingNonAdditiveSchemaIncompatibleException]()\n      )\n\n      // First retry with schema log initializes it.\n      testStream(readStream(dir, checkpointDir))(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        ExpectMetadataEvolutionException()\n      )\n      // Second retry updates the schema log after the type change, then fails.\n      testStream(readStream(dir, checkpointDir))(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        ExpectMetadataEvolutionException()\n      )\n      // Third retry requests user action to unblock the stream.\n      testStream(readStream(dir, checkpointDir))(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        ExpectTypeChangeBlockedException()\n      )\n      // Unblocking the stream goes through.\n      withUnblockedTypeChanges {\n        testStream(readStream(dir, checkpointDir))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          ProcessAllAvailable(),\n          CheckAnswer(123456789)\n        )\n      }\n    }\n  }\n\n  for ((name, getSqlConf: (Int => String), value) <- Seq(\n    (\"unblock all\", (_: Int) => \"allowSourceColumnTypeChange\", \"always\"),\n    (\"unblock stream\", (hash: Int) => s\"allowSourceColumnTypeChange.ckpt_$hash\", \"always\"),\n    (\"unblock version\", (hash: Int) => s\"allowSourceColumnTypeChange.ckpt_$hash\", \"2\")\n  )) {\n    test(s\"unblocking stream with sql conf after type change - $name\") {\n      withTempDir { dir =>\n        sql(s\"CREATE TABLE delta.`$dir` (widened byte, other byte) USING DELTA\")\n        // Getting the checkpoint dir through the delta log to ensure the format is consistent with\n        // the path used internally to compute the hash of the checkpoint location to unblock the\n        // stream.\n        val deltaLog = DeltaLog.forTable(spark, dir.toString)\n        val checkpointDir = new File(deltaLog.dataPath.toString, \"sink_checkpoint\")\n\n        def readWithAgg(): DataFrame =\n          readStream(dir, checkpointDir)\n            .groupBy(\"other\")\n            .agg(count(col(\"widened\")))\n\n        testStream(readWithAgg(), outputMode = OutputMode.Complete())(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1, 1)\") },\n          Execute { _ => sql(s\"ALTER TABLE delta.`$dir`ALTER COLUMN widened TYPE int\") },\n          ExpectMetadataEvolutionException()\n        )\n\n        testStream(readWithAgg(), outputMode = OutputMode.Complete())(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          ExpectTypeChangeBlockedException()\n        )\n\n        val checkpointHash = s\"$checkpointDir/sources/0\".hashCode\n\n        withSQLConf(s\"spark.databricks.delta.streaming.${getSqlConf(checkpointHash)}\" -> value) {\n          testStream(readWithAgg(), outputMode = OutputMode.Complete())(\n            StartStream(checkpointLocation = checkpointDir.toString),\n            Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789, 1)\") },\n            ProcessAllAvailable(),\n            CheckLastBatch(Row(1, 2))\n          )\n        }\n      }\n    }\n  }\n\n  for ((name, optionValue) <- Seq(\n    (\"unblock stream\", \"always\"),\n    (\"unblock version\", \"2\")\n  )) {\n    test(s\"unblocking stream with reader option after type change - $name\") {\n      withTempDir { dir =>\n        sql(s\"CREATE TABLE delta.`$dir` (widened byte, other byte) USING DELTA\")\n        val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n        def readWithAgg(options: Map[String, String] = Map.empty): DataFrame =\n          readStream(dir, checkpointDir, options)\n            .groupBy(\"other\")\n            .agg(count(col(\"widened\")))\n\n        testStream(readWithAgg(), outputMode = OutputMode.Complete())(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1, 1)\") },\n          Execute { _ => sql(s\"ALTER TABLE delta.`$dir`ALTER COLUMN widened TYPE int\") },\n          ExpectMetadataEvolutionException()\n        )\n\n        testStream(readWithAgg(), outputMode = OutputMode.Complete())(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          ExpectTypeChangeBlockedException()\n        )\n\n        testStream(\n            readWithAgg(Map(\"allowSourceColumnTypeChange\" -> optionValue)),\n            outputMode = OutputMode.Complete())(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789, 1)\") },\n          ProcessAllAvailable(),\n          CheckLastBatch(Row(1, 2))\n        )\n      }\n    }\n  }\n\n  test(s\"overwrite schema with type change and dropped column\") {\n    withTempDir { dir =>\n      sql(s\"CREATE TABLE delta.`$dir` (a byte, b int) USING DELTA\")\n      val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n      testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1, 1)\") },\n        ProcessAllAvailable(),\n        Execute { _ =>\n          // Overwrite the table schema.\n          spark\n            .createDataFrame(\n              sparkContext.emptyRDD[Row],\n              StructType.fromDDL(s\"a INT\"))\n            .write\n            .format(\"delta\")\n            .mode(SaveMode.Overwrite)\n            .option(\"overwriteSchema\", \"true\")\n            .save(dir.getCanonicalPath)\n        },\n        ExpectMetadataEvolutionException()\n      )\n\n      testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n        StartStream(checkpointLocation = checkpointDir.toString),\n        ExpectFailure[DeltaRuntimeException] { ex =>\n          checkErrorMatchPVals(\n            exception = ex.asInstanceOf[DeltaRuntimeException],\n            \"DELTA_STREAMING_CANNOT_CONTINUE_PROCESSING_POST_SCHEMA_EVOLUTION\",\n            parameters = Map(\n              \"opType\" -> \"DROP AND TYPE WIDENING\",\n              \"previousSchemaChangeVersion\" -> \"0\",\n              \"currentSchemaChangeVersion\" -> \"2\",\n              \"columnChangeDetails\" ->\n                s\"\"\"Columns dropped:\n                   |'b'\n                   |Columns with widened types:\n                   |'a': TINYINT -> INT\n                   |\"\"\".stripMargin,\n              \"unblockChangeOptions\" ->\n                \".*allowSourceColumnDrop(.|\\\\n)*allowSourceColumnTypeChange.*\",\n              \"unblockStreamOptions\" ->\n                \".*allowSourceColumnDrop(.|\\\\n)*allowSourceColumnTypeChange.*\",\n              \"unblockChangeConfs\" ->\n                \".*allowSourceColumnDrop(.|\\\\n)*allowSourceColumnTypeChange.*\",\n              \"unblockStreamConfs\" ->\n                \".*allowSourceColumnDrop(.|\\\\n)*allowSourceColumnTypeChange.*\",\n              \"unblockAllConfs\" ->\n                \".*allowSourceColumnDrop(.|\\\\n)*allowSourceColumnTypeChange.*\"\n            ))\n        }\n      )\n      // Allowing both source column drop and type widening allows the stream to proceed\n      withSQLConf(\n          \"spark.databricks.delta.streaming.allowSourceColumnDrop\" -> \"always\",\n          \"spark.databricks.delta.streaming.allowSourceColumnTypeChange\" -> \"always\") {\n        testStream(readStream(dir, checkpointDir, options = Map(\"ignoreDeletes\" -> \"true\")))(\n          StartStream(checkpointLocation = checkpointDir.toString),\n          Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (2)\") },\n          ProcessAllAvailable()\n        )\n      }\n    }\n  }\n\n  test(\"disable schema tracking log using internal conf\") {\n     withTempDir { dir =>\n       sql(s\"CREATE TABLE delta.`$dir` (a byte) USING DELTA\")\n       val checkpointDir = new File(dir, \"sink_checkpoint\")\n\n       def readStream(): DataFrame =\n         spark.readStream.format(\"delta\").load(dir.getCanonicalPath)\n\n       // When we disable schema tracking for widening type changes, the stream should succeed\n       // without requiring the user to provide a schema tracking location or unblock the type\n       // change.\n       withSQLConf(\n         DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING.key -> \"false\") {\n         testStream(readStream())(\n           StartStream(checkpointLocation = checkpointDir.toString),\n           Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (1)\") },\n           ProcessAllAvailable(),\n           Execute { _ => sql(s\"ALTER TABLE delta.`$dir`ALTER COLUMN a TYPE int\") },\n           ExpectFailure[DeltaIllegalStateException] { ex =>\n             assert(ex.asInstanceOf[SparkThrowable].getErrorClass ===\n               \"DELTA_SCHEMA_CHANGED_WITH_VERSION\")\n           }\n         )\n\n         testStream(readStream())(\n           StartStream(checkpointLocation = checkpointDir.toString),\n           Execute { _ => sql(s\"INSERT INTO delta.`$dir` VALUES (123456789)\") },\n           ProcessAllAvailable(),\n           CheckLastBatch(123456789)\n         )\n       }\n     }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningTableFeatureSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport java.io.PrintWriter\n\nimport com.databricks.spark.util.Log4jUsageLogger\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.DeltaOperations.ManualUpdate\nimport org.apache.spark.sql.delta.actions.TableFeatureProtocolUtils.propertyKey\nimport org.apache.spark.sql.delta.rowtracking.RowTrackingTestUtils\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.util.JsonUtils\n\nimport org.apache.spark.SparkException\nimport org.apache.spark.sql.{AnalysisException, QueryTest, Row}\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.internal.SQLConf\nimport org.apache.spark.sql.types._\n\n/**\n * Test suite covering feature enablement and configuration tests.\n */\nclass TypeWideningTableFeatureEnablementSuite extends TypeWideningTableFeatureEnablementTests\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin\n\ntrait TypeWideningTableFeatureEnablementTests extends QueryTest\n    with TypeWideningTestCases {\n  self: QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin =>\n\n  import testImplicits._\n\n  test(\"enable type widening at table creation then disable it\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a int) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'true')\")\n    assert(isTypeWideningSupported)\n    assert(isTypeWideningEnabled)\n    enableTypeWidening(tempPath, enabled = false)\n    assert(isTypeWideningSupported)\n    assert(!isTypeWideningEnabled)\n  }\n\n  test(\"enable type widening after table creation then disable it\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a int) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n    assert(!isTypeWideningSupported)\n    assert(!isTypeWideningEnabled)\n    // Setting the property to false shouldn't add the table feature if it's not present.\n    enableTypeWidening(tempPath, enabled = false)\n    assert(!isTypeWideningSupported)\n    assert(!isTypeWideningEnabled)\n\n    enableTypeWidening(tempPath)\n    assert(isTypeWideningSupported)\n    assert(isTypeWideningEnabled)\n    enableTypeWidening(tempPath, enabled = false)\n    assert(isTypeWideningSupported)\n    assert(!isTypeWideningEnabled)\n  }\n\n  test(\"set table property to incorrect value\") {\n    val ex = intercept[IllegalArgumentException] {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a int) USING DELTA \" +\n        s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'bla')\")\n    }\n    assert(ex.getMessage.contains(\"For input string: \\\"bla\\\"\"))\n    sql(s\"CREATE TABLE delta.`$tempPath` (a int) USING DELTA \" +\n       s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n    checkError(\n      intercept[SparkException] {\n        sql(s\"ALTER TABLE delta.`$tempPath` \" +\n          s\"SET TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'bla')\")\n      },\n      \"_LEGACY_ERROR_TEMP_2045\",\n      parameters = Map(\n        \"message\" -> \"For input string: \\\"bla\\\"\"\n      )\n    )\n    assert(!isTypeWideningSupported)\n    assert(!isTypeWideningEnabled)\n  }\n\n  test(\"change column type without table feature\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a TINYINT) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n\n    checkError(\n      intercept[AnalysisException] {\n        sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE SMALLINT\")\n      },\n      \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n      parameters = Map(\n        \"fieldPath\" -> \"a\",\n        \"oldField\" -> \"TINYINT\",\n        \"newField\" -> \"SMALLINT\"\n      )\n    )\n  }\n\n  test(\"change column type with type widening table feature supported but table property set to \" +\n    \"false\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a SMALLINT) USING DELTA\")\n    sql(s\"ALTER TABLE delta.`$tempPath` \" +\n      s\"SET TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n\n    checkError(\n      intercept[AnalysisException] {\n        sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n      },\n      \"DELTA_UNSUPPORTED_ALTER_TABLE_CHANGE_COL_OP\",\n      parameters = Map(\n        \"fieldPath\" -> \"a\",\n        \"oldField\" -> \"SMALLINT\",\n        \"newField\" -> \"INT\"\n      )\n    )\n  }\n\n  test(\"no-op type changes are always allowed\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a int) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    enableTypeWidening(tempPath, enabled = true)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    enableTypeWidening(tempPath, enabled = false)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n  }\n}\n\n/**\n * Test suite covering feature removal, rewriting data files with the old type and removing type\n * widening metadata.\n */\nclass TypeWideningTableFeatureDropSuite\n  extends QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin\n    with TypeWideningTableFeatureDropTests\n\ntrait TypeWideningTableFeatureDropTests\n  extends RowTrackingTestUtils\n    with TypeWideningTestCases {\n  self: QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin =>\n\n  import testImplicits._\n\n  test(\"drop unused table feature on empty table\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n    dropTableFeature(\n      expectedOutcome = ExpectedOutcome.SUCCESS,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> ByteType)\n    )\n    checkAnswer(readDeltaTable(tempPath), Seq.empty)\n  }\n\n  test(\"drop feature using sql with multipart identifier\") {\n    withTempDatabase { databaseName =>\n      val tableName = \"test_table\"\n      withTable(tableName) {\n        sql(s\"CREATE TABLE $databaseName.$tableName (a byte) USING DELTA \" +\n          s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'true')\")\n        sql(s\"INSERT INTO  $databaseName.$tableName VALUES (1)\")\n        sql(s\"ALTER TABLE $databaseName.$tableName CHANGE COLUMN a TYPE INT\")\n        sql(s\"INSERT INTO  $databaseName.$tableName VALUES (2)\")\n\n        val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName, Some(databaseName)))\n\n        checkError(\n          intercept[DeltaTableFeatureException] {\n            sql(s\"ALTER TABLE $databaseName.$tableName \" +\n              s\"DROP FEATURE '${TypeWideningTableFeature.name}'\"\n            ).collect()\n          },\n          \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n          parameters = Map(\n            \"feature\" -> TypeWideningTableFeature.name,\n            \"logRetentionPeriodKey\" -> DeltaConfigs.LOG_RETENTION.key,\n            \"logRetentionPeriod\" -> DeltaConfigs.LOG_RETENTION\n              .fromMetaData(deltaLog.unsafeVolatileMetadata).toString,\n            \"truncateHistoryLogRetentionPeriod\" ->\n              DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION\n                .fromMetaData(deltaLog.unsafeVolatileMetadata).toString)\n        )\n      }\n    }\n  }\n\n  // Rewriting the data when dropping the table feature relies on the default row commit version\n  // being set even when row tracking isn't enabled.\n  for(rowTrackingEnabled <- BOOLEAN_DOMAIN) {\n    test(s\"drop unused table feature on table with data, rowTrackingEnabled=$rowTrackingEnabled\") {\n      sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n      addSingleFile(Seq(1, 2, 3), ByteType)\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.SUCCESS,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> ByteType)\n      )\n      checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2), Row(3)))\n    }\n\n    test(s\"drop unused table feature on table with data inserted before adding the table feature,\" +\n      s\"rowTrackingEnabled=$rowTrackingEnabled\") {\n      setupManualClock()\n      sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA \" +\n        s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n      addSingleFile(Seq(1, 2, 3), ByteType)\n      enableTypeWidening(tempPath)\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 1,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n\n      advancePastRetentionPeriod()\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.SUCCESS,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n    }\n\n    test(s\"drop table feature on table with data added only after type change, \" +\n      s\"rowTrackingEnabled=$rowTrackingEnabled\") {\n      setupManualClock()\n      sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n      sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n      addSingleFile(Seq(1, 2, 3), IntegerType)\n\n      // We could actually drop the table feature directly here instead of failing by checking that\n      // there were no files added before the type change. This may be an expensive check for a rare\n      // scenario so we don't do it.\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n\n      advancePastRetentionPeriod()\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.SUCCESS,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n      checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2), Row(3)))\n    }\n\n    test(s\"drop table feature on table with data added before type change, \" +\n      s\"rowTrackingEnabled=$rowTrackingEnabled\") {\n      setupManualClock()\n      sql(s\"CREATE TABLE delta.`$tempDir` (a byte) USING DELTA\")\n      addSingleFile(Seq(1, 2, 3), ByteType)\n      sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 1,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n\n      advancePastRetentionPeriod()\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.SUCCESS,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n      checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2), Row(3)))\n    }\n\n    test(s\"drop table feature on table with data added before type change and fully rewritten \" +\n      s\"after, rowTrackingEnabled=$rowTrackingEnabled\") {\n      setupManualClock()\n      sql(s\"CREATE TABLE delta.`$tempDir` (a byte) USING DELTA\")\n      addSingleFile(Seq(1, 2, 3), ByteType)\n      sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n      sql(s\"UPDATE delta.`$tempDir` SET a = a + 10\")\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        // The file was already rewritten in UPDATE.\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n\n      advancePastRetentionPeriod()\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.SUCCESS,\n        expectedNumFilesRewritten = 0,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n      checkAnswer(readDeltaTable(tempPath), Seq(Row(11), Row(12), Row(13)))\n    }\n\n    test(s\"drop table feature on table with data added before type change and partially \" +\n      s\"rewritten after, rowTrackingEnabled=$rowTrackingEnabled\") {\n      setupManualClock()\n      withRowTrackingEnabled(rowTrackingEnabled) {\n        sql(s\"CREATE TABLE delta.`$tempDir` (a byte) USING DELTA\")\n        addSingleFile(Seq(1, 2, 3), ByteType)\n        addSingleFile(Seq(4, 5, 6), ByteType)\n        sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n        sql(s\"UPDATE delta.`$tempDir` SET a = a + 10 WHERE a < 4\")\n\n        dropTableFeature(\n          expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n          // One file was already rewritten in UPDATE, leaving 1 file to rewrite.\n          expectedNumFilesRewritten = 1,\n          expectedColumnTypes = Map(\"a\" -> IntegerType)\n        )\n\n        dropTableFeature(\n          expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n          expectedNumFilesRewritten = 0,\n          expectedColumnTypes = Map(\"a\" -> IntegerType)\n        )\n\n        advancePastRetentionPeriod()\n        dropTableFeature(\n          expectedOutcome = ExpectedOutcome.SUCCESS,\n          expectedNumFilesRewritten = 0,\n          expectedColumnTypes = Map(\"a\" -> IntegerType)\n        )\n        checkAnswer(\n          readDeltaTable(tempPath),\n          Seq(Row(11), Row(12), Row(13), Row(4), Row(5), Row(6)))\n      }\n    }\n  }\n}\n\n/**\n * Additional tests covering e.g. unsupported type change check, CLONE, RESTORE.\n */\nclass TypeWideningTableFeatureAdvancedSuite\n  extends TypeWideningTableFeatureAdvancedTests\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin\n\ntrait TypeWideningTableFeatureAdvancedTests extends QueryTest\n    with TypeWideningTestCases {\n  self: QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin =>\n\n  import testImplicits._\n\n  for {\n    testCase <- supportedTestCases\n  }\n  test(s\"drop feature after type change ${testCase.fromType.sql} -> ${testCase.toType.sql}\") {\n    append(testCase.initialValuesDF.repartition(2))\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN value TYPE ${testCase.toType.sql}\")\n    append(testCase.additionalValuesDF.repartition(3))\n    dropTableFeature(\n      expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n      expectedNumFilesRewritten = 2,\n      expectedColumnTypes = Map(\"value\" -> testCase.toType)\n    )\n    checkAnswer(readDeltaTable(tempPath), testCase.expectedResult)\n  }\n\n  test(\"drop feature after a type change with schema evolution\") {\n    setupManualClock()\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n    addSingleFile(Seq(1), ByteType)\n\n    withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n      addSingleFile(Seq(1024), IntegerType)\n    }\n    assert(readDeltaTable(tempPath).schema(\"a\").dataType === IntegerType)\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(1024)))\n\n    dropTableFeature(\n      expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n      expectedNumFilesRewritten = 1,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n\n    advancePastRetentionPeriod()\n    dropTableFeature(\n      expectedOutcome = ExpectedOutcome.SUCCESS,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(1024)))\n  }\n\n  test(\"unsupported type changes applied to the table\") {\n    sql(s\"CREATE TABLE delta.`$tempDir` (a array<int>) USING DELTA\")\n    val metadata = new MetadataBuilder()\n      .putMetadataArray(\"delta.typeChanges\", Array(\n        new MetadataBuilder()\n          .putString(\"toType\", \"string\")\n          .putString(\"fromType\", \"int\")\n          .putLong(\"tableVersion\", 2)\n          .putString(\"fieldPath\", \"element\")\n          .build()\n      )).build()\n\n    // Add an unsupported type change to the table schema. Only an implementation that isn't\n    // compliant with the feature specification would allow this.\n    deltaLog.withNewTransaction { txn =>\n      txn.commit(\n        Seq(txn.snapshot.metadata.copy(\n          schemaString = new StructType()\n            .add(\"a\", StringType, nullable = true, metadata).json\n        )),\n        ManualUpdate)\n    }\n\n    checkError(\n      intercept[DeltaIllegalStateException] {\n        readDeltaTable(tempPath).collect()\n      },\n      \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_SCHEMA\",\n      parameters = Map(\n        \"fieldName\" -> \"a.element\",\n        \"fromType\" -> \"INT\",\n        \"toType\" -> \"STRING\"\n      )\n    )\n\n    // Validate that the internal table property can be used to bypass the check if needed.\n    withSQLConf(\n      DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_UNSUPPORTED_TYPE_CHANGE_CHECK.key -> \"true\") {\n      readDeltaTable(tempPath).collect()\n    }\n  }\n\n  test(\"unsupported type changes in nested structs\") {\n    sql(s\"CREATE TABLE delta.`$tempDir` (s struct<a: int>) USING DELTA\")\n    deltaLog.withNewTransaction { txn =>\n      txn.commit(\n        Seq(txn.snapshot.metadata.copy(\n          schemaString = new StructType()\n            .add(\"s\", new StructType()\n              .add(\"a\", BooleanType, nullable = true,\n                metadata = typeWideningMetadata(IntegerType, BooleanType)))\n          .json\n        )),\n        ManualUpdate)\n    }\n\n    checkError(\n      intercept[DeltaIllegalStateException] {\n        readDeltaTable(tempPath).collect()\n      },\n      \"DELTA_UNSUPPORTED_TYPE_CHANGE_IN_SCHEMA\",\n      parameters = Map(\n        \"fieldName\" -> \"s.a\",\n        \"fromType\" -> \"INT\",\n        \"toType\" -> \"BOOLEAN\"\n      )\n    )\n  }\n\n  test(\"char/varchar/string type changes don't trigger the unsupported type change check\") {\n    sql(\n      s\"\"\"\n        |CREATE TABLE delta.`$tempDir` (\n        |  a string, b string, c char(4), d char(4), e varchar(4), f varchar(4), s struct<x: string>\n        |) USING DELTA\n        |\"\"\".stripMargin)\n\n    // Add type change metadata for all string<->char<->varchar type changes and ensure the table\n    // can still be read.\n    // Note: compliant delta implementations shouldn't actually record these type changes in the\n    // table schema metadata. This test ensures that if a non-compliant implementation still does,\n    // we don't unnecessarily block reads.\n    deltaLog.withNewTransaction { txn =>\n      txn.commit(\n        Seq(txn.snapshot.metadata.copy(\n          schemaString = new StructType()\n            .add(\"a\", StringType, nullable = true,\n              metadata = typeWideningMetadata(StringType, CharType(4)))\n            .add(\"b\", StringType, nullable = true,\n              metadata = typeWideningMetadata(StringType, VarcharType(4)))\n            .add(\"c\", StringType, nullable = true,\n              metadata = typeWideningMetadata(CharType(4), StringType))\n            .add(\"d\", StringType, nullable = true,\n              metadata = typeWideningMetadata(CharType(4), VarcharType(4)))\n            .add(\"e\", StringType, nullable = true,\n              metadata = typeWideningMetadata(VarcharType(4), StringType))\n            .add(\"f\", StringType, nullable = true,\n              metadata = typeWideningMetadata(VarcharType(4), CharType(4)))\n            .add(\"s\", new StructType()\n              .add(\"x\", StringType, nullable = true,\n                metadata = typeWideningMetadata(StringType, CharType(4)))\n            )\n            .json\n        )),\n        ManualUpdate)\n    }\n    readDeltaTable(tempPath).collect()\n  }\n\n  test(\"type widening rewrite metrics\") {\n    sql(s\"CREATE TABLE delta.`$tempDir` (a byte) USING DELTA\")\n    addSingleFile(Seq(1, 2, 3), ByteType)\n    addSingleFile(Seq(4, 5, 6), ByteType)\n    sql(s\"ALTER TABLE delta.`$tempDir` CHANGE COLUMN a TYPE int\")\n    // Update a row from the second file to rewrite it. Only the first file still contains the old\n    // data type after this.\n    sql(s\"UPDATE delta.`$tempDir` SET a = a + 10 WHERE a < 4\")\n    val usageLogs = Log4jUsageLogger.track {\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 1,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n    }\n\n    val metrics = filterUsageRecords(usageLogs, \"delta.typeWidening.featureRemoval\")\n      .map(r => JsonUtils.fromJson[Map[String, String]](r.blob))\n      .head\n\n    assert(metrics(\"downgradeTimeMs\").toLong > 0L)\n    // Only the first file should get rewritten here since the second file was already rewritten\n    // during the UPDATE.\n    assert(metrics(\"numFilesRewritten\").toLong === 1L)\n    assert(metrics(\"metadataRemoved\").toBoolean)\n  }\n\n  test(\"dropping feature after CLONE correctly rewrite files with old type\") {\n    withTable(\"source\") {\n      sql(\"CREATE TABLE source (a byte) USING delta\")\n      sql(\"INSERT INTO source VALUES (1)\")\n      sql(\"INSERT INTO source VALUES (2)\")\n      sql(s\"ALTER TABLE source CHANGE COLUMN a TYPE INT\")\n      sql(\"INSERT INTO source VALUES (200)\")\n      sql(s\"CREATE OR REPLACE TABLE delta.`$tempPath` SHALLOW CLONE source\")\n      checkAnswer(readDeltaTable(tempPath),\n        Seq(Row(1), Row(2), Row(200)))\n\n      dropTableFeature(\n        expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n        expectedNumFilesRewritten = 2,\n        expectedColumnTypes = Map(\"a\" -> IntegerType)\n      )\n      checkAnswer(readDeltaTable(tempPath),\n        Seq(Row(1), Row(2), Row(200)))\n    }\n  }\n  test(\"RESTORE to before type change\") {\n    addSingleFile(Seq(1), ShortType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    sql(s\"UPDATE delta.`$tempPath` SET a = ${Int.MinValue} WHERE a = 1\")\n\n    // RESTORE to version 0, before the type change was applied.\n    sql(s\"RESTORE TABLE delta.`$tempPath` VERSION AS OF 0\")\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1)))\n    dropTableFeature(\n      // There should be no files to rewrite but versions before RESTORE still use the feature.\n      expectedOutcome = ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> ShortType)\n    )\n  }\n\n  test(\"dropping feature after RESTORE correctly rewrite files with old type\") {\n    addSingleFile(Seq(1), ShortType)\n    addSingleFile(Seq(2), ShortType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    // Delete the first file which will be added back again with RESTORE.\n    sql(s\"DELETE FROM delta.`$tempPath` WHERE a = 1\")\n    addSingleFile(Seq(Int.MinValue), IntegerType)\n\n    // RESTORE to version 2 -> ALTER TABLE CHANGE COLUMN TYPE.\n    // The type change is then still present and the first file initially added at version 0 is\n    // added back during RESTORE (version 5). That file contains the old type and must be rewritten\n    // when the feature is dropped.\n    sql(s\"RESTORE TABLE delta.`$tempPath` VERSION AS OF 2\")\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2)))\n    dropTableFeature(\n      expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n      // Both files added before the type change must be rewritten.\n      expectedNumFilesRewritten = 2,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    checkAnswer(readDeltaTable(tempPath), Seq(Row(1), Row(2)))\n  }\n\n  test(\"rewriting files fails if there are corrupted files\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA\")\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE INT\")\n    addSingleFile(Seq(2), IntegerType)\n    addSingleFile(Seq(3), IntegerType)\n    val filePath = deltaLog.update().allFiles.first().absolutePath(deltaLog).toUri.getPath\n    val pw = new PrintWriter(filePath)\n    pw.write(\"corrupted\")\n    pw.close()\n\n    // Rewriting files when dropping type widening should ignore this config, if the corruption is\n    // transient it will leave files behind that some clients can't read.\n    withSQLConf(SQLConf.IGNORE_CORRUPT_FILES.key -> \"true\") {\n      val ex = intercept[SparkException] {\n        sql(s\"ALTER TABLE delta.`$tempDir` DROP FEATURE '${TypeWideningTableFeature.name}'\")\n      }\n      assert(ex.getMessage.contains(\"Cannot seek after EOF\"))\n    }\n  }\n}\n\n/**\n * Test suite covering preview vs stable feature interactions.\n */\nclass TypeWideningTableFeaturePreviewSuite\n  extends TypeWideningTableFeatureVersionTests\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin\n\ntrait TypeWideningTableFeatureVersionTests extends QueryTest\n    with TypeWideningTestCases {\n  self: QueryTest\n    with TypeWideningTestMixin\n    with TypeWideningDropFeatureTestMixin =>\n\n  import testImplicits._\n\n  /**\n   * Directly add the preview/stable type widening table feature without using the type widening\n   * table property.\n   */\n  def addTableFeature(tablePath: String, feature: TypeWideningTableFeatureBase): Unit =\n    sql(s\"ALTER TABLE delta.`$tablePath` \" +\n      s\"SET TBLPROPERTIES ('${propertyKey(feature)}' = 'supported')\")\n\n  /** Validate whether the preview stable type widening table feature are supported or not. */\n  def assertFeatureSupported(preview: Boolean, stable: Boolean): Unit = {\n    val protocol = deltaLog.update().protocol\n    def supported(supported: Boolean): String = if (supported) \"supported\" else \"not supported\"\n\n    assert(protocol.isFeatureSupported(TypeWideningPreviewTableFeature) === preview,\n      s\"Expected the preview feature to be ${supported(preview)} but it is ${supported(!preview)}.\")\n    assert(protocol.isFeatureSupported(TypeWideningTableFeature) === stable,\n      s\"Expected the stable feature to be ${supported(stable)} but it is ${supported(!stable)}.\")\n    assert(TypeWidening.isSupported(protocol) === preview || stable,\n      s\"Expected type widening to be ${supported(preview || stable)} but it is \" +\n        s\"${supported(!(preview || stable))}.\")\n  }\n\n  test(\"automatically enabling the preview feature doesn't enable the stable feature\") {\n    setupManualClock()\n    // Create a new table with type widening enabled.\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'true')\")\n    addSingleFile(Seq(1), ByteType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n\n    // The preview feature isn't supported and can't be dropped.\n    assertFeatureSupported(preview = false, stable = true)\n    dropTableFeature(\n      feature = TypeWideningPreviewTableFeature,\n      expectedOutcome = ExpectedOutcome.FAIL_FEATURE_NOT_PRESENT,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> ByteType)\n    )\n\n    // The stable feature is supported and can be dropped.\n    dropTableFeature(\n      feature = TypeWideningTableFeature,\n      expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n      expectedNumFilesRewritten = 1,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = false, stable = true)\n\n    advancePastRetentionPeriod()\n    dropTableFeature(\n      feature = TypeWideningTableFeature,\n      expectedOutcome = ExpectedOutcome.SUCCESS,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = false, stable = false)\n  }\n\n  test(\"manually adding the stable and preview features and dropping them\") {\n    setupManualClock()\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n    assertFeatureSupported(preview = false, stable = false)\n    // This is undocumented for type widening but users can manually add the preview/stable feature\n    // to the table instead of using the table property.\n    addTableFeature(tempPath, TypeWideningTableFeature)\n    assertFeatureSupported(preview = false, stable = true)\n\n    // Users can manually add both features to the table that way: this is allowed, the two\n    // specifications are compatible and supported.\n    addTableFeature(tempPath, TypeWideningPreviewTableFeature)\n    assertFeatureSupported(preview = true, stable = true)\n\n    enableTypeWidening(tempPath)\n    addSingleFile(Seq(1), ByteType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n    // Dropping the stable feature doesn't also drop the preview feature.\n    dropTableFeature(\n      feature = TypeWideningTableFeature,\n      expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n      expectedNumFilesRewritten = 1,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = true, stable = true)\n\n    advancePastRetentionPeriod()\n    dropTableFeature(\n      feature = TypeWideningTableFeature,\n      expectedOutcome = ExpectedOutcome.SUCCESS,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = true, stable = false)\n\n    // Dropping the preview feature is now immediate since all traces have already been removed from\n    // the table history.\n    dropTableFeature(\n      feature = TypeWideningPreviewTableFeature,\n      expectedOutcome = ExpectedOutcome.SUCCESS,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = false, stable = false)\n  }\n\n  test(\"tables created with the preview feature aren't automatically enabling the stable feature\") {\n    setupManualClock()\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n\n    addTableFeature(tempPath, TypeWideningPreviewTableFeature)\n    assertFeatureSupported(preview = true, stable = false)\n\n    // Enable the table property, this should keep the preview feature but not add the stable one.\n    enableTypeWidening(tempPath)\n    assertFeatureSupported(preview = true, stable = false)\n\n    addSingleFile(Seq(1), ByteType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n\n    dropTableFeature(\n      feature = TypeWideningTableFeature,\n      expectedOutcome = ExpectedOutcome.FAIL_FEATURE_NOT_PRESENT,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> ByteType)\n    )\n    // The preview table feature can be dropped.\n    dropTableFeature(\n      feature = TypeWideningPreviewTableFeature,\n      expectedOutcome = ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE,\n      expectedNumFilesRewritten = 1,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = true, stable = false)\n\n    advancePastRetentionPeriod()\n    dropTableFeature(\n      feature = TypeWideningPreviewTableFeature,\n      expectedOutcome = ExpectedOutcome.SUCCESS,\n      expectedNumFilesRewritten = 0,\n      expectedColumnTypes = Map(\"a\" -> IntegerType)\n    )\n    assertFeatureSupported(preview = false, stable = false)\n  }\n\n  test(\"tableVersion metadata is correctly set and preserved when using the preview feature\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n\n    addTableFeature(tempPath, TypeWideningPreviewTableFeature)\n    enableTypeWidening(tempPath)\n    addSingleFile(Seq(1), ByteType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE short\")\n\n    assert(deltaLog.update().metadata.schema === new StructType()\n      .add(\"a\", ShortType, nullable = true, metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          new MetadataBuilder()\n            .putLong(\"tableVersion\", 4)\n            .putString(\"fromType\", \"byte\")\n            .putString(\"toType\", \"short\")\n            .build()\n        )).build()))\n\n    // It's allowed to manually add both the preview and stable feature to the same table - the\n    // specs are compatible. In that case, we still populate the `tableVersion` field.\n    addTableFeature(tempPath, TypeWideningTableFeature)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE int\")\n    assert(deltaLog.update().metadata.schema === new StructType()\n      .add(\"a\", IntegerType, nullable = true, metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          new MetadataBuilder()\n            .putLong(\"tableVersion\", 4)\n            .putString(\"fromType\", \"byte\")\n            .putString(\"toType\", \"short\")\n            .build(),\n          new MetadataBuilder()\n            .putLong(\"tableVersion\", 6)\n            .putString(\"fromType\", \"short\")\n            .putString(\"toType\", \"integer\")\n            .build()\n        )).build()))\n  }\n\n  test(\"tableVersion isn't set when using the stable feature\") {\n    sql(s\"CREATE TABLE delta.`$tempPath` (a byte) USING DELTA \" +\n      s\"TBLPROPERTIES ('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = 'false')\")\n\n    addTableFeature(tempPath, TypeWideningTableFeature)\n    enableTypeWidening(tempPath)\n    addSingleFile(Seq(1), ByteType)\n    sql(s\"ALTER TABLE delta.`$tempPath` CHANGE COLUMN a TYPE short\")\n    assert(deltaLog.update().metadata.schema === new StructType()\n      .add(\"a\", ShortType, nullable = true, metadata = new MetadataBuilder()\n        .putMetadataArray(\"delta.typeChanges\", Array(\n          new MetadataBuilder()\n            .putString(\"fromType\", \"byte\")\n            .putString(\"toType\", \"short\")\n            .build()\n        )).build()))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningTestCases.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.{DataFrame, Encoder, Row}\nimport org.apache.spark.sql.test.{SharedSparkSession, SQLTestUtils}\nimport org.apache.spark.sql.types._\n\n/**\n * Trait collecting supported and unsupported type change test cases.\n */\ntrait TypeWideningTestCases extends SQLTestUtils { self: SharedSparkSession =>\n  import testImplicits._\n\n  /**\n   * Represents the input of a type change test.\n   * @param fromType         The original type of the column 'value' in the test table.\n   * @param toType           The type to use when changing the type of column 'value'.\n   */\n  abstract class TypeEvolutionTestCase(\n      val fromType: DataType,\n      val toType: DataType) {\n    /** The initial values to insert with type `fromType` in column 'value' after table creation. */\n    def initialValuesDF: DataFrame\n    /** Additional values to insert after changing the type of the column 'value' to `toType`. */\n    def additionalValuesDF: DataFrame\n    /** Expected content of the table after inserting the additional values. */\n    def expectedResult: DataFrame\n  }\n\n  /**\n   * Represents the input of a supported type change test. Handles converting the test values from\n   * scala types to a dataframe.\n   */\n  case class SupportedTypeEvolutionTestCase[\n      FromType  <: DataType, ToType <: DataType,\n      FromVal: Encoder, ToVal: Encoder\n    ](\n      override val fromType: FromType,\n      override val toType: ToType,\n      initialValues: Seq[FromVal],\n      additionalValues: Seq[ToVal]\n  ) extends TypeEvolutionTestCase(fromType, toType) {\n    override def initialValuesDF: DataFrame =\n      initialValues.toDF(\"value\").select($\"value\".cast(fromType))\n\n    override def additionalValuesDF: DataFrame =\n      additionalValues.toDF(\"value\").select($\"value\".cast(toType))\n\n    override def expectedResult: DataFrame =\n      initialValuesDF.union(additionalValuesDF).select($\"value\".cast(toType))\n  }\n\n  /**\n   * Represents the input of an unsupported type change test. Handles converting the test values\n   * from scala types to a dataframe. Additional values to insert are always empty since the type\n   * change is expected to fail.\n   */\n  case class UnsupportedTypeEvolutionTestCase[\n    FromType  <: DataType, ToType <: DataType, FromVal : Encoder](\n      override val fromType: FromType,\n      override val toType: ToType,\n      initialValues: Seq[FromVal]) extends TypeEvolutionTestCase(fromType, toType) {\n    override def initialValuesDF: DataFrame =\n      initialValues.toDF(\"value\").select($\"value\".cast(fromType))\n\n    override def additionalValuesDF: DataFrame =\n      spark.createDataFrame(\n        sparkContext.emptyRDD[Row],\n        new StructType().add(StructField(\"value\", toType)))\n\n    override def expectedResult: DataFrame =\n      initialValuesDF.select($\"value\".cast(toType))\n  }\n\n  // Type changes that are supported by all Parquet readers. Byte, Short, Int are all stored as\n  // INT32 in parquet so these changes are guaranteed to be supported.\n  protected val supportedTestCases: Seq[TypeEvolutionTestCase] = Seq(\n    SupportedTypeEvolutionTestCase(ByteType, ShortType,\n      Seq(1, -1, Byte.MinValue, Byte.MaxValue, null.asInstanceOf[Byte]),\n      Seq(4, -4, Short.MinValue, Short.MaxValue, null.asInstanceOf[Short])),\n    SupportedTypeEvolutionTestCase(ByteType, IntegerType,\n      Seq(1, -1, Byte.MinValue, Byte.MaxValue, null.asInstanceOf[Byte]),\n      Seq(4, -4, Int.MinValue, Int.MaxValue, null.asInstanceOf[Int])),\n    SupportedTypeEvolutionTestCase(ShortType, IntegerType,\n      Seq(1, -1, Short.MinValue, Short.MaxValue, null.asInstanceOf[Short]),\n      Seq(4, -4, Int.MinValue, Int.MaxValue, null.asInstanceOf[Int])),\n    SupportedTypeEvolutionTestCase(ShortType, LongType,\n      Seq(1, -1, Short.MinValue, Short.MaxValue, null.asInstanceOf[Short]),\n      Seq(4L, -4L, Long.MinValue, Long.MaxValue, null.asInstanceOf[Long])),\n    SupportedTypeEvolutionTestCase(IntegerType, LongType,\n      Seq(1, -1, Int.MinValue, Int.MaxValue, null.asInstanceOf[Int]),\n      Seq(4L, -4L, Long.MinValue, Long.MaxValue, null.asInstanceOf[Long])),\n    SupportedTypeEvolutionTestCase(FloatType, DoubleType,\n      Seq(1234.56789f, -0f, 0f, Float.NaN, Float.NegativeInfinity, Float.PositiveInfinity,\n        Float.MinPositiveValue, Float.MinValue, Float.MaxValue, null.asInstanceOf[Float]),\n      Seq(987654321.987654321d, -0d, 0d, Double.NaN, Double.NegativeInfinity,\n        Double.PositiveInfinity, Double.MinPositiveValue, Double.MinValue, Double.MaxValue,\n        null.asInstanceOf[Double])),\n    SupportedTypeEvolutionTestCase(DateType, TimestampNTZType,\n      Seq(\"2020-01-01\", \"2024-02-29\", \"1312-02-27\"),\n      Seq(\"2020-03-17 15:23:15.123456\", \"2058-12-31 23:59:59.999\", \"0001-01-01 00:00:00\")),\n    // Larger precision.\n    SupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_INT_DIGITS, 2),\n      DecimalType(Decimal.MAX_LONG_DIGITS, 2),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"10.34\"), null.asInstanceOf[BigDecimal]),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_LONG_DIGITS - 2) + \".99\"),\n        null.asInstanceOf[BigDecimal])),\n    // Larger precision and scale, same physical type.\n    SupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_INT_DIGITS - 1, 2),\n      DecimalType(Decimal.MAX_INT_DIGITS, 3),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"10.34\"), null.asInstanceOf[BigDecimal]),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_INT_DIGITS - 3) + \".99\"),\n        null.asInstanceOf[BigDecimal])),\n    // Larger precision and scale, different physical types.\n    SupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_INT_DIGITS, 2),\n      DecimalType(Decimal.MAX_LONG_DIGITS + 1, 3),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"10.34\"), null.asInstanceOf[BigDecimal]),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_LONG_DIGITS - 2) + \".99\"),\n        null.asInstanceOf[BigDecimal]))\n  )\n\n  // Type changes that are only eligible for automatic widening when\n  // spark.databricks.delta.typeWidening.allowAutomaticWidening = ALWAYS.\n  protected val restrictedAutomaticWideningTestCases: Seq[TypeEvolutionTestCase] = Seq(\n    SupportedTypeEvolutionTestCase(IntegerType, DoubleType,\n      Seq(1, -1, Int.MinValue, Int.MaxValue, null.asInstanceOf[Int]),\n      Seq(987654321.987654321d, -0d, 0d, Double.NaN, Double.NegativeInfinity,\n        Double.PositiveInfinity, Double.MinPositiveValue, Double.MinValue, Double.MaxValue,\n        null.asInstanceOf[Double])),\n    SupportedTypeEvolutionTestCase(ByteType, DecimalType(10, 0),\n      Seq(1, -1, Byte.MinValue, Byte.MaxValue, null.asInstanceOf[Byte]),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"9\" * 10), null.asInstanceOf[BigDecimal])),\n    SupportedTypeEvolutionTestCase(ShortType, DecimalType(10, 0),\n      Seq(1, -1, Short.MinValue, Short.MaxValue, null.asInstanceOf[Short]),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"9\" * 10), null.asInstanceOf[BigDecimal])),\n    SupportedTypeEvolutionTestCase(IntegerType, DecimalType(10, 0),\n      Seq(1, -1, Int.MinValue, Int.MaxValue, null.asInstanceOf[Int]),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"9\" * 10), null.asInstanceOf[BigDecimal])),\n    SupportedTypeEvolutionTestCase(LongType, DecimalType(20, 0),\n      Seq(1L, -1L, Long.MinValue, Long.MaxValue, null.asInstanceOf[Int]),\n      Seq(BigDecimal(\"1.23\"), BigDecimal(\"9\" * 20), null.asInstanceOf[BigDecimal]))\n  )\n\n  // Test type changes that aren't supported.\n  protected val unsupportedTestCases: Seq[TypeEvolutionTestCase] = Seq(\n    UnsupportedTypeEvolutionTestCase(IntegerType, ByteType,\n      Seq(1, 2, Int.MinValue)),\n    UnsupportedTypeEvolutionTestCase(LongType, IntegerType,\n      Seq(4, 5, Long.MaxValue)),\n    UnsupportedTypeEvolutionTestCase(DoubleType, FloatType,\n      Seq(987654321.987654321d, Double.NaN, Double.NegativeInfinity,\n        Double.PositiveInfinity, Double.MinPositiveValue,\n        Double.MinValue, Double.MaxValue)),\n    UnsupportedTypeEvolutionTestCase(ByteType, DecimalType(2, 0),\n      Seq(1, -1, Byte.MinValue)),\n    UnsupportedTypeEvolutionTestCase(ShortType, DecimalType(4, 0),\n      Seq(1, -1, Short.MinValue)),\n    UnsupportedTypeEvolutionTestCase(IntegerType, DecimalType(9, 0),\n      Seq(1, -1, Int.MinValue)),\n    UnsupportedTypeEvolutionTestCase(LongType, DecimalType(19, 0),\n      Seq(1, -1, Long.MinValue)),\n    UnsupportedTypeEvolutionTestCase(TimestampNTZType, DateType,\n      Seq(\"2020-03-17 15:23:15\", \"2023-12-31 23:59:59\", \"0001-01-01 00:00:00\")),\n    // Reduce scale\n    UnsupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_INT_DIGITS, 2),\n      DecimalType(Decimal.MAX_INT_DIGITS, 3),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_INT_DIGITS - 2) + \".99\"))),\n    // Reduce precision\n    UnsupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_INT_DIGITS, 2),\n      DecimalType(Decimal.MAX_INT_DIGITS - 1, 2),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_INT_DIGITS - 2) + \".99\"))),\n    // Reduce precision & scale\n    UnsupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_LONG_DIGITS, 2),\n      DecimalType(Decimal.MAX_INT_DIGITS - 1, 1),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_LONG_DIGITS - 2) + \".99\"))),\n    // Increase scale more than precision\n    UnsupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_INT_DIGITS, 2),\n      DecimalType(Decimal.MAX_INT_DIGITS + 1, 4),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_INT_DIGITS - 2) + \".99\"))),\n    // Smaller scale and larger precision.\n    UnsupportedTypeEvolutionTestCase(DecimalType(Decimal.MAX_LONG_DIGITS, 2),\n      DecimalType(Decimal.MAX_INT_DIGITS + 3, 1),\n      Seq(BigDecimal(\"-67.89\"), BigDecimal(\"9\" * (Decimal.MAX_LONG_DIGITS - 2) + \".99\")))\n  )\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningTestMixin.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta._\nimport org.apache.spark.sql.delta.actions.{RemoveFile, TableFeatureProtocolUtils}\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2\nimport org.apache.spark.sql.delta.commands.AlterTableDropFeatureDeltaCommand\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.util.DeltaFileOperations\nimport com.google.common.math.DoubleMath\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.{DataFrame, Encoder, QueryTest}\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.internal.{LegacyBehaviorPolicy, SQLConf}\nimport org.apache.spark.sql.test.SharedSparkSession\nimport org.apache.spark.sql.types._\n\n/**\n * Test mixin that enables type widening by default for all tests in the suite.\n */\ntrait TypeWideningTestMixin\n  extends DeltaSQLCommandTest\n  with DeltaDMLTestUtilsPathBased { self: QueryTest =>\n\n  import testImplicits._\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaConfigs.ENABLE_TYPE_WIDENING.defaultTablePropertyKey, \"true\")\n      .set(DeltaSQLConf.DELTA_ALLOW_AUTOMATIC_WIDENING.key, \"always\")\n      .set(TableFeatureProtocolUtils.defaultPropertyKey(TimestampNTZTableFeature), \"supported\")\n      // Ensure we don't silently cast test inputs to null on overflow.\n      .set(SQLConf.ANSI_ENABLED.key, \"true\")\n      // Rebase mode must be set explicitly to allow writing dates before 1582-10-15.\n      .set(SQLConf.PARQUET_REBASE_MODE_IN_WRITE.key, LegacyBehaviorPolicy.CORRECTED.toString)\n      // All the drop feature tests below are based on the drop feature with history truncation\n      // implementation. The fast drop feature implementation does not require any waiting time.\n      // The fast drop feature implementation is tested extensively in the\n      // DeltaFastDropFeatureSuite.\n      .set(DeltaSQLConf.FAST_DROP_FEATURE_ENABLED.key, false.toString)\n  }\n\n  /** Enable (or disable) type widening for the table under the given path. */\n  protected def enableTypeWidening(tablePath: String, enabled: Boolean = true): Unit =\n    sql(s\"ALTER TABLE delta.`$tablePath` \" +\n          s\"SET TBLPROPERTIES('${DeltaConfigs.ENABLE_TYPE_WIDENING.key}' = '${enabled.toString}')\")\n\n  /** Whether the test table supports the type widening table feature. */\n  def isTypeWideningSupported: Boolean = {\n    TypeWidening.isSupported(deltaLog.update().protocol)\n  }\n\n  /** Whether the type widening table property is enabled on the test table. */\n  def isTypeWideningEnabled: Boolean = {\n    val snapshot = deltaLog.update()\n    TypeWidening.isEnabled(snapshot.protocol, snapshot.metadata)\n  }\n\n  /** Short-hand to create type widening metadata for struct fields. */\n  protected def typeWideningMetadata(\n      from: AtomicType,\n      to: AtomicType,\n      path: Seq[String] = Seq.empty): Metadata =\n    new MetadataBuilder()\n      .putMetadataArray(\n        \"delta.typeChanges\", Array(TypeChange(None, from, to, path).toMetadata))\n      .build()\n\n  def addSingleFile[T: Encoder](values: Seq[T], dataType: DataType): Unit =\n      append(values.toDF(\"a\").select(col(\"a\").cast(dataType)).repartition(1))\n\n  /**\n   * Similar to `QueryTest.checkAnswer` but using fuzzy equality for double values. This is needed\n   * because double partition values are serialized as string leading to loss of precision. Also\n   * `checkAnswer` treats -0f and 0f as different values without tolerance.\n   */\n  def checkAnswerWithTolerance(\n      actualDf: DataFrame,\n      expectedDf: DataFrame,\n      toType: DataType,\n      tolerance: Double = 0.001)\n    : Unit = {\n    // Widening to float isn't supported so only handle double here.\n    if (toType == DoubleType) {\n      val actual = actualDf.sort(\"value\").collect()\n      val expected = expectedDf.sort(\"value\").collect()\n      assert(actual.length === expected.length, s\"Wrong result: $actual did not equal $expected\")\n\n      actual.zip(expected).foreach { case (a, e) =>\n        val expectedValue = e.getAs[Double](\"value\")\n        val actualValue = a.getAs[Double](\"value\")\n        val absTolerance = if (expectedValue.isNaN || expectedValue.isInfinity) {\n          0\n        } else {\n          tolerance * Math.abs(expectedValue)\n        }\n        assert(\n          DoubleMath.fuzzyEquals(actualValue, expectedValue, absTolerance),\n          s\"$actualValue did not equal $expectedValue\"\n        )\n      }\n    } else {\n      checkAnswer(actualDf, expectedDf)\n    }\n  }\n}\n\n/**\n * Mixin trait containing helpers to test dropping the type widening table feature.\n */\ntrait TypeWideningDropFeatureTestMixin\n    extends QueryTest\n    with SharedSparkSession\n    with DeltaDMLTestUtils {\n\n  /** Expected outcome of dropping the type widening table feature. */\n  object ExpectedOutcome extends Enumeration {\n    val SUCCESS,\n    FAIL_CURRENT_VERSION_USES_FEATURE,\n    FAIL_HISTORICAL_VERSION_USES_FEATURE,\n    FAIL_FEATURE_NOT_PRESENT = Value\n  }\n\n  def getCatalogTableOpt: Option[CatalogTable] = {\n    if (DeltaTableIdentifier.isDeltaPath(spark, tableIdentifier)) {\n      None\n    } else {\n      Some(spark.sessionState.catalog.getTableMetadata(tableIdentifier))\n    }\n  }\n\n  /**\n   * Helper method to drop the type widening table feature and check for an expected outcome.\n   * Validates in particular that the right number of files were rewritten and that the rewritten\n   * files all contain the expected type for specified columns.\n   */\n  def dropTableFeature(\n      feature: TableFeature = TypeWideningTableFeature,\n      expectedOutcome: ExpectedOutcome.Value,\n      expectedNumFilesRewritten: Long,\n      expectedColumnTypes: Map[String, DataType]): Unit = {\n    val catalogTableOpt = getCatalogTableOpt\n    val snapshot = deltaLog.update(catalogTableOpt = catalogTableOpt)\n    // Need to directly call ALTER TABLE command to pass our deltaLog with manual clock.\n    val dropFeature =\n      AlterTableDropFeatureDeltaCommand(DeltaTableV2(spark, deltaLog.dataPath), feature.name)\n\n    expectedOutcome match {\n      case ExpectedOutcome.SUCCESS =>\n        dropFeature.run(spark)\n      case ExpectedOutcome.FAIL_CURRENT_VERSION_USES_FEATURE =>\n        checkError(\n          intercept[DeltaTableFeatureException] { dropFeature.run(spark) },\n          \"DELTA_FEATURE_DROP_WAIT_FOR_RETENTION_PERIOD\",\n          parameters = Map(\n            \"feature\" -> feature.name,\n            \"logRetentionPeriodKey\" -> DeltaConfigs.LOG_RETENTION.key,\n            \"logRetentionPeriod\" -> DeltaConfigs.LOG_RETENTION\n              .fromMetaData(snapshot.metadata).toString,\n            \"truncateHistoryLogRetentionPeriod\" ->\n              DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION\n                .fromMetaData(snapshot.metadata).toString)\n        )\n      case ExpectedOutcome.FAIL_HISTORICAL_VERSION_USES_FEATURE =>\n        checkError(\n          intercept[DeltaTableFeatureException] { dropFeature.run(spark) },\n          \"DELTA_FEATURE_DROP_HISTORICAL_VERSIONS_EXIST\",\n          parameters = Map(\n            \"feature\" -> feature.name,\n            \"logRetentionPeriodKey\" -> DeltaConfigs.LOG_RETENTION.key,\n            \"logRetentionPeriod\" -> DeltaConfigs.LOG_RETENTION\n              .fromMetaData(snapshot.metadata).toString,\n            \"truncateHistoryLogRetentionPeriod\" ->\n              DeltaConfigs.TABLE_FEATURE_DROP_TRUNCATE_HISTORY_LOG_RETENTION\n                .fromMetaData(snapshot.metadata).toString)\n        )\n      case ExpectedOutcome.FAIL_FEATURE_NOT_PRESENT =>\n        checkError(\n          intercept[DeltaTableFeatureException] { dropFeature.run(spark) },\n          \"DELTA_FEATURE_DROP_FEATURE_NOT_PRESENT\",\n          parameters = Map(\"feature\" -> feature.name)\n        )\n    }\n\n    if (expectedOutcome != ExpectedOutcome.FAIL_FEATURE_NOT_PRESENT) {\n      assert(!TypeWideningMetadata.containsTypeWideningMetadata(\n        deltaLog.update(catalogTableOpt = catalogTableOpt).schema))\n    }\n\n    // Check the number of files rewritten.\n    assert(getNumRemoveFilesSinceVersion(snapshot.version + 1) === expectedNumFilesRewritten,\n      s\"Expected $expectedNumFilesRewritten file(s) to be rewritten but found \" +\n        s\"${getNumRemoveFilesSinceVersion(snapshot.version + 1)} rewritten file(s).\")\n\n    // Check that all files now contain the expected data types.\n    expectedColumnTypes.foreach { case (colName, expectedType) =>\n      withSQLConf(\"spark.databricks.delta.formatCheck.enabled\" -> \"false\") {\n        deltaLog.update(catalogTableOpt = catalogTableOpt).filesForScan(\n            Seq.empty, keepNumRecords = false).files.foreach { file =>\n          val filePath = DeltaFileOperations.absolutePath(deltaLog.dataPath.toString, file.path)\n          val data = spark.read.parquet(filePath.toString)\n          val physicalColName = DeltaColumnMapping.getPhysicalName(snapshot.schema(colName))\n          assert(data.schema(physicalColName).dataType === expectedType,\n            s\"File with values ${data.collect().mkString(\", \")} wasn't rewritten.\")\n        }\n      }\n    }\n  }\n\n  /** Get the number of remove actions committed since the given table version (included). */\n  def getNumRemoveFilesSinceVersion(version: Long): Long =\n    deltaLog\n      .getChanges(startVersion = version, catalogTableOpt = getCatalogTableOpt)\n      .flatMap { case (_, actions) => actions }\n      .collect { case r: RemoveFile => r }\n      .size\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/typewidening/TypeWideningUniformTests.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.typewidening\n\nimport org.apache.spark.sql.delta.{DeltaInsertIntoTest, DeltaUnsupportedOperationException, IcebergCompatBase, IcebergCompatV1, IcebergCompatV2}\nimport org.apache.spark.sql.delta.DeltaErrors.toSQLType\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.scalatest.GivenWhenThen\n\nimport org.apache.spark.sql.{QueryTest, SaveMode}\nimport org.apache.spark.sql.functions.col\nimport org.apache.spark.sql.types.{DataType, DateType, DecimalType}\n\n/** Trait collecting tests covering type widening + Uniform Iceberg compatibility. */\ntrait TypeWideningUniformTests extends QueryTest\n  with TypeWideningTestMixin\n  with TypeWideningTestCases\n  with DeltaInsertIntoTest\n  with GivenWhenThen {\n\n  // Iceberg supports all base type changes eligible for widening during schema evolution except\n  // for date -> timestampNtz and decimal scale changes.\n  private val icebergSupportedTestCases = supportedTestCases.filter {\n    case SupportedTypeEvolutionTestCase(_ : DateType, _, _, _) => false\n    case SupportedTypeEvolutionTestCase(from: DecimalType, to: DecimalType, _, _) =>\n      from.scale == to.scale\n    case _ => true\n  }\n\n  // Unsupported type changes are all base changes that aren't supported above and all changes that\n  // are not eligible for schema evolution: int -> double, int -> decimal\n  private val icebergUnsupportedTestCases =\n    supportedTestCases.diff(icebergSupportedTestCases) ++ restrictedAutomaticWideningTestCases\n\n  /** Helper to enable Uniform with Iceberg compatibility on the given table. */\n  private def enableIcebergUniform(tableName: String, compat: IcebergCompatBase): Unit =\n    sql(\n      s\"\"\"\n         |ALTER TABLE $tableName SET TBLPROPERTIES (\n         |  'delta.columnMapping.mode' = 'name',\n         |  '${compat.config.key}' = 'true',\n         |  'delta.universalFormat.enabledFormats' = 'iceberg'\n         |)\n       \"\"\".stripMargin)\n\n  /** Helper to check that the given function violates Uniform compatibility with type widening. */\n  private def checkIcebergCompatViolation(\n      compat: IcebergCompatBase,\n      fromType: DataType,\n      toType: DataType)(f: => Unit): Unit = {\n    Given(s\"iceberg compat ${compat.getClass.getSimpleName}\")\n    checkError(\n      exception = intercept[DeltaUnsupportedOperationException] {\n        f\n      },\n      \"DELTA_ICEBERG_COMPAT_VIOLATION.UNSUPPORTED_TYPE_WIDENING\",\n      parameters = Map(\n        \"version\" -> compat.version.toString,\n        \"prevType\" -> toSQLType(fromType),\n        \"newType\" -> toSQLType(toType),\n        \"fieldPath\" -> \"a\"\n      )\n    )\n  }\n\n  test(\"apply supported type change then enable uniform\") {\n    for (testCase <- icebergSupportedTestCases) {\n      Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n      val tableName = \"type_widening_uniform_supported_table\"\n      withTable(tableName) {\n        sql(s\"CREATE TABLE $tableName (a ${testCase.fromType.sql}) USING DELTA\")\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN a TYPE ${testCase.toType.sql}\")\n        enableIcebergUniform(tableName, IcebergCompatV2)\n      }\n    }\n  }\n\n  test(\"apply unsupported type change then enable uniform\") {\n    for (testCase <- icebergUnsupportedTestCases) {\n      val tableName = \"type_widening_uniform_unsupported_table\"\n      withTable(tableName) {\n        Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n        sql(s\"CREATE TABLE $tableName (a ${testCase.fromType.sql}) USING DELTA\")\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN a TYPE ${testCase.toType.sql}\")\n        checkIcebergCompatViolation(IcebergCompatV1, testCase.fromType, testCase.toType) {\n          enableIcebergUniform(tableName, IcebergCompatV1)\n        }\n        checkIcebergCompatViolation(IcebergCompatV2, testCase.fromType, testCase.toType) {\n          enableIcebergUniform(tableName, IcebergCompatV2)\n        }\n      }\n    }\n  }\n\n  test(\"enable uniform then apply supported type change - ALTER TABLE\") {\n    for (testCase <- icebergSupportedTestCases) {\n      Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n      val tableName = \"type_widening_uniform_manual_supported_table\"\n      withTable(tableName) {\n        sql(s\"CREATE TABLE $tableName (a ${testCase.fromType.sql}) USING DELTA\")\n        enableIcebergUniform(tableName, IcebergCompatV2)\n        sql(s\"ALTER TABLE $tableName CHANGE COLUMN a TYPE ${testCase.toType.sql}\")\n      }\n    }\n  }\n\n  test(\"enable uniform then apply unsupported type change - ALTER TABLE\") {\n    for (testCase <- icebergUnsupportedTestCases) {\n      val tableName = \"type_widening_uniform_manual_unsupported_table\"\n      withTable(tableName) {\n        Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n        sql(s\"CREATE TABLE $tableName (a ${testCase.fromType.sql}) USING DELTA\")\n        enableIcebergUniform(tableName, IcebergCompatV2)\n        checkIcebergCompatViolation(IcebergCompatV2, testCase.fromType, testCase.toType) {\n          sql(s\"ALTER TABLE $tableName CHANGE COLUMN a TYPE ${testCase.toType.sql}\")\n        }\n      }\n    }\n  }\n\n\n  test(\"enable uniform then apply supported type change - MERGE\") {\n    for (testCase <- icebergSupportedTestCases) {\n      Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n      withTable(\"source\", \"target\") {\n        testCase.initialValuesDF.write.format(\"delta\").saveAsTable(\"target\")\n        testCase.additionalValuesDF.write.format(\"delta\").saveAsTable(\"source\")\n        enableIcebergUniform(\"target\", IcebergCompatV2)\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n          sql(\n            s\"\"\"\n               |MERGE INTO target\n               |USING source\n               |ON 0 = 1\n               |WHEN NOT MATCHED THEN INSERT *\n           \"\"\".stripMargin)\n        }\n        val result = sql(s\"SELECT * FROM target\")\n        assert(result.schema(\"value\").dataType === testCase.toType)\n        checkAnswer(result, testCase.expectedResult)\n      }\n    }\n  }\n\n  test(\"enable uniform then apply unsupported type change - MERGE\") {\n    for (testCase <- icebergUnsupportedTestCases) {\n      Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n      withTable(\"source\", \"target\") {\n        testCase.initialValuesDF.write.format(\"delta\").saveAsTable(\"target\")\n        // Here we use a source for MERGE that contains the same data that is already present in the\n        // target, except that it uses a wider type. Since Uniform is enabled and Iceberg doesn't\n        // support the given type change, we will keep the existing narrower type and downcast\n        // values. `testCase.additionalValueDF` contains values that would overflow, which would\n        // just fail, hence why we use `testCase.initialValueDF` instead.\n        testCase.initialValuesDF\n          .select(col(\"value\").cast(testCase.toType))\n          .write\n          .format(\"delta\")\n          .saveAsTable(\"source\")\n        enableIcebergUniform(\"target\", IcebergCompatV2)\n        withSQLConf(DeltaSQLConf.DELTA_SCHEMA_AUTO_MIGRATE.key -> \"true\") {\n          sql(\n            s\"\"\"\n               |MERGE INTO target\n               |USING source\n               |ON 0 = 1\n               |WHEN NOT MATCHED THEN INSERT *\n          \"\"\".stripMargin)\n        }\n        val result = sql(s\"SELECT * FROM target\")\n        val expected = testCase.initialValuesDF.union(testCase.initialValuesDF)\n        assert(result.schema(\"value\").dataType === testCase.fromType)\n        checkAnswer(result, expected)\n      }\n    }\n  }\n\n\n  for (insert <- Set(\n      // Cover only a subset of all INSERTs. There's little value in testing all of them and it\n      // quickly gets expensive.\n      SQLInsertByPosition(SaveMode.Append),\n      SQLInsertByName(SaveMode.Append),\n      DFv1InsertInto(SaveMode.Append),\n      StreamingInsert)) {\n    test(s\"enable uniform then apply supported type change - ${insert.name}\") {\n      for (testCase <- icebergSupportedTestCases) {\n        Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n        withTable(\"source\", \"target\") {\n          testCase.initialValuesDF.write.format(\"delta\").saveAsTable(\"target\")\n          testCase.additionalValuesDF.write.format(\"delta\").saveAsTable(\"source\")\n          enableIcebergUniform(\"target\", IcebergCompatV2)\n          insert.runInsert(\n            columns = Seq(\"value\"),\n            whereCol = \"value\",\n            whereValue = 1,\n            withSchemaEvolution = true)\n          val result = sql(s\"SELECT * FROM target\")\n          assert(result.schema(\"value\").dataType === testCase.toType)\n          checkAnswer(result, testCase.expectedResult)\n        }\n      }\n    }\n\n    test(s\"enable uniform then apply unsupported type change - ${insert.name}\") {\n      for (testCase <- icebergUnsupportedTestCases) {\n        Given(s\"changing ${testCase.fromType.sql} -> ${testCase.toType.sql}\")\n        withTable(\"source\", \"target\") {\n          testCase.initialValuesDF.write.format(\"delta\").saveAsTable(\"target\")\n          // Here we use a source for INSERT that contains the same data that is already present in\n          // the target, except that it uses a wider type. Since Uniform is enabled and Iceberg\n          // doesn't support the given type change, we will keep the existing narrower type and\n          // downcast values. `testCase.additionalValueDF` contains values that would overflow,\n          // which would just fail, hence why we use `testCase.initialValueDF` instead.\n          testCase.initialValuesDF\n            .select(col(\"value\").cast(testCase.toType))\n            .write\n            .format(\"delta\")\n            .saveAsTable(\"source\")\n          enableIcebergUniform(\"target\", IcebergCompatV2)\n          withSQLConf(\n            DeltaSQLConf.DELTA_STREAMING_SINK_ALLOW_IMPLICIT_CASTS.key -> \"true\"\n          ) {\n            insert.runInsert(\n              columns = Seq(\"value\"),\n              whereCol = \"value\",\n              whereValue = 1,\n              withSchemaEvolution = true)\n          }\n          val result = sql(s\"SELECT * FROM target\")\n          val expected = testCase.initialValuesDF.union(testCase.initialValuesDF)\n          assert(result.schema(\"value\").dataType === testCase.fromType)\n          checkAnswer(result, expected)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/IcebergCompatV2EnableUniformByAlterTableSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport org.apache.spark.sql.delta.{DeltaConfigs, DeltaLog, DeltaUnsupportedOperationException, Snapshot}\nimport org.apache.spark.sql.delta.actions.AddFile\nimport org.apache.parquet.hadoop.metadata.ParquetMetadata\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, SparkSession}\nimport org.apache.spark.sql.catalyst.TableIdentifier\n\ntrait IcebergCompatV2EnableUniformByAlterTableSuiteBase extends QueryTest {\n  override protected def spark: SparkSession\n\n  /**\n   * Assert the invariance between old and new parquet footers,\n   * this will check if the number of overlapped parquet footers\n   * is the same as expected and extract the newer portion of footers,\n   * i.e., the portion of parquet files present in `newParquetFooters`\n   * but not in `oldParquetFooters`; or the overlapped portion of footers,\n   * as specified by the flag `newerOrOverlapped`.\n   *\n   * This function is useful when, e.g.,\n   * - checking the invariance of parquet footers before and after\n   *   ALTER TABLE to enable UniForm, a portion of parquet footers\n   *   will stay the same, and the new portion of parquet footers\n   *   should be `IcebergCompatV2`.\n   * - after running REORG UPGRADE UNIFORM, there may be a portion of\n   *   parquet files that do not need to be rewritten, and the number\n   *   should be the same as expected.\n   *\n   * @param oldParquetFooters the old version of parquet footers.\n   * @param newParquetFooters the new version of parquet footers.\n   * @param expectedNumOfOverlappedParquetFiles the expected number of overlapped parquet footers.\n   * @param expectedNumOfAddedParquetFiles the expected number of added portion of parquet footers.\n   * @return a pair consists of (overlapped parquet footers, added parquet footers).\n   */\n  protected def assertInvarianceAndExtractParquetFooters(\n      oldParquetFooters: Seq[ParquetMetadata],\n      newParquetFooters: Seq[ParquetMetadata],\n      expectedNumOfOverlappedParquetFiles: Int,\n      expectedNumOfAddedParquetFiles: Int): (Seq[ParquetMetadata], Seq[ParquetMetadata]) = {\n    val oldParquetFootersInStr = oldParquetFooters.map { _.toString }\n    val newParquetFootersInStr = newParquetFooters.map { _.toString }\n    val overlappedParquetFootersInStr = oldParquetFootersInStr.filter { footer =>\n      newParquetFootersInStr.contains(footer)\n    }\n    assert(\n      overlappedParquetFootersInStr.length == expectedNumOfOverlappedParquetFiles,\n      s\"expect number of overlapped parquet footers to be $expectedNumOfOverlappedParquetFiles, \" +\n        s\"but get ${overlappedParquetFootersInStr.length}\"\n    )\n    val addedParquetFootersInStr = newParquetFootersInStr.filter { footer =>\n      !oldParquetFootersInStr.contains(footer)\n    }\n    assert(\n      addedParquetFootersInStr.length == expectedNumOfAddedParquetFiles,\n      s\"expect number of newer parquet footers to be $expectedNumOfAddedParquetFiles, \" +\n        s\"but get ${addedParquetFootersInStr.length}\"\n    )\n    val overlappedParquetFooters = oldParquetFooters.filter { footer =>\n      overlappedParquetFootersInStr.contains(footer.toString)\n    }\n    val addedParquetFooters = newParquetFooters.filter { footer =>\n      addedParquetFootersInStr.contains(footer.toString)\n    }\n    (overlappedParquetFooters, addedParquetFooters)\n  }\n\n\n  /**\n   * Assert the properties for old and new parquet footers.\n   * Specifically, first check the number of overlapped and added parquet footers\n   * to match with the expected numbers;\n   * then extract and assert whether each should be considered `IcebergCompatV2`\n   * by the expected values.\n   *\n   * @param oldParquetFooters the old version of parquet footers.\n   * @param newParquetFooters the new version of parquet footers.\n   * @param expectedNumOfOverlappedParquetFiles the expected number of overlapped parquet footers.\n   * @param expectedNumOfAddedParquetFiles the expected number of added parquet footers.\n   * @param isOverlappedIcebergCompatV2 whether the overlapped parquet footers is expected\n   *                                    to be `IcebergCompatV2`.\n   * @param isAddedIcebergCompatV2 whether the added parquet footers is expected to be\n   *                               `IcebergCompatV2`.\n   */\n  protected def assertParquetFootersProperties(\n      oldParquetFooters: Seq[ParquetMetadata],\n      newParquetFooters: Seq[ParquetMetadata],\n      expectedNumOfOverlappedParquetFiles: Int,\n      expectedNumOfAddedParquetFiles: Int,\n      isOverlappedIcebergCompatV2: Boolean,\n      isAddedIcebergCompatV2: Boolean): Unit = {\n    val (overlapped, added) = assertInvarianceAndExtractParquetFooters(\n      oldParquetFooters = oldParquetFooters,\n      newParquetFooters = newParquetFooters,\n      expectedNumOfOverlappedParquetFiles = expectedNumOfOverlappedParquetFiles,\n      expectedNumOfAddedParquetFiles = expectedNumOfAddedParquetFiles\n    )\n    assert(isParquetFootersIcebergCompatV2(overlapped) == isOverlappedIcebergCompatV2)\n    assert(isParquetFootersIcebergCompatV2(added) == isAddedIcebergCompatV2)\n  }\n\n  /** Check if `IcebergCompatV1` is enabled based on the provided snapshot */\n  protected def isIcebergCompatV1Enabled(snapshot: Snapshot): Boolean = {\n    snapshot\n      .getProperties(DeltaConfigs.ICEBERG_COMPAT_V1_ENABLED.key)\n      .contains(\"true\")\n  }\n\n  /** Check if `IcebergCompatV2` is enabled based on the provided snapshot */\n  protected def isIcebergCompatV2Enabled(snapshot: Snapshot): Boolean = {\n    snapshot\n      .getProperties(DeltaConfigs.ICEBERG_COMPAT_V2_ENABLED.key)\n      .contains(\"true\")\n  }\n\n  /**\n   * Insert three initial rows to the specified table.\n   *\n   * @param id the table id used for insertion.\n   */\n  protected def insertInitialRowsIntoTable(id: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | INSERT INTO TABLE $id\n         | VALUES\n         | (1, 'Alex', '2000-01-01'),\n         | (1, 'Cat', '2001-01-01'),\n         | (2, 'Michael', '2002-10-30')\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Insert two additional rows to the specified table.\n   *\n   * @param id the table id used for insertion.\n   */\n  protected def insertAdditionalRowsIntoTable(id: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | INSERT INTO TABLE $id\n         | VALUES\n         | (3, 'Cat', '2003-01-01'),\n         | (4, 'Cat', '2004-01-02')\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Create a vanilla delta table with a single partition column.\n   *\n   * @param id the table id used for creation.\n   * @param loc the table location.\n   */\n  protected def createVanillaDeltaTableWithDV(id: String, loc: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | CREATE TABLE $id (id INT, name STRING, date TIMESTAMP)\n         | USING DELTA\n         | PARTITIONED BY (id)\n         | LOCATION $loc\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Create a vanilla delta table with DV disabled and a single partition column.\n   *\n   * @param id the table id used for creation.\n   * @param loc the table location.\n   */\n  protected def createVanillaDeltaTableWithoutDV(id: String, loc: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | CREATE TABLE $id (id INT, name STRING, date TIMESTAMP)\n         | USING DELTA\n         | PARTITIONED BY (id)\n         | LOCATION $loc\n         | TBLPROPERTIES (\n         |   'delta.enableDeletionVectors' = 'false',\n         |   'delta.minReaderVersion' = '2',\n         |   'delta.minWriterVersion' = '7'\n         | )\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Create an `IcebergCompatV1` uniform table with a single partition column.\n   *\n   * @param id the table id used for creation.\n   * @param loc the table location.\n   */\n  protected def createIcebergCompatV1Table(id: String, loc: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | CREATE TABLE $id (id INT, name STRING, date TIMESTAMP)\n         | USING DELTA\n         | PARTITIONED BY (id)\n         | LOCATION $loc\n         | TBLPROPERTIES (\n         |   'delta.enableIcebergCompatV1' = 'true',\n         |   'delta.universalFormat.enabledFormats' = 'iceberg'\n         | )\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Create a delta table with liquid clustering enabled for a single column.\n   *\n   * @param id the table id used for creation.\n   * @param loc the table location.\n   */\n  protected def createLiquidDeltaTable(id: String, loc: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | CREATE TABLE $id (id INT, name STRING, date TIMESTAMP)\n         | USING DELTA\n         | CLUSTER BY (id)\n         | LOCATION $loc\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Create a delta table with nested types and column-mapping enabled.\n   *\n   * @param id the table id used for creation.\n   * @param loc the table location.\n   */\n  protected def createDeltaTableWithNestedTypesAndColumnMapping(id: String, loc: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | CREATE TABLE $id (\n         |   id INT,\n         |   listOfInt ARRAY<INT>,\n         |   listOfList ARRAY<ARRAY<INT>>,\n         |   listOfMap ARRAY<MAP<INT, STRING>>,\n         |   map MAP<INT, STRING>,\n         |   mapOfMap MAP<INT, MAP<INT, STRING>>,\n         |   mapOfList MAP<INT, ARRAY<STRING>>,\n         |   struct STRUCT<col1: INT, col2: STRING>,\n         |   structOfStruct STRUCT<col1: INT, col2: STRUCT<col1: INT, col2: STRING>>,\n         |   structOfListAndMap STRUCT<col1: INT, col2: ARRAY<INT>, col3: MAP<INT, STRING>>)\n         | USING DELTA\n         | LOCATION $loc\n         | TBLPROPERTIES (\n         |   'delta.columnMapping.mode' = 'name'\n         | )\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Insert a single row to the delta table with nested types and column-mapping enabled.\n   *\n   * @param id the table used for insertion.\n   */\n  protected def insertRowToDeltaTableWithNestedTypesAndColumnMapping(id: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | INSERT INTO TABLE $id VALUES\n         | (1,\n         |  ARRAY(1, 2, 3),\n         |  ARRAY(ARRAY(1, 2), ARRAY(3, 4)),\n         |  ARRAY(MAP(1, 'Alex'), MAP(2, 'Michael')),\n         |  MAP(1, 'Alex'),\n         |  MAP(1, MAP(2, 'Michael')),\n         |  MAP(3, ARRAY('Cat', 'Cat')),\n         |  STRUCT(2, 'Michael'),\n         |  STRUCT(1, STRUCT(2, 'Cat')),\n         |  STRUCT(1, ARRAY(4, 5, 6), MAP(7, 'Alex'))\n         | )\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Get all parquet footers of data files for the specified table.\n   *\n   * @param spark the spark session used to get the footers.\n   * @param id the table id from which to get all the parquet footers.\n   * @return all parquet metadata/footers of the parquet (data) files for this table.\n   */\n  protected def getParquetFooters(spark: SparkSession, id: String): Seq[ParquetMetadata] = {\n    val snapshot = DeltaLog.forTable(spark, new TableIdentifier(id)).update()\n    val basePath = snapshot.path.getParent.toString + \"/\"\n    val addFiles: Array[AddFile] = snapshot.allFiles.collect()\n    val parquetPaths: Array[String] = addFiles.map { basePath + _.toPath.toString }\n    parquetPaths.map { ParquetIcebergCompatV2Utils.getParquetFooter }\n  }\n\n  /**\n   * Check whether the current parquet footers are all `IcebergCompatV2`.\n   * This will check two properties for each parquet footer,\n   * see [[ParquetIcebergCompatV2Utils.isParquetIcebergCompatV2]] for details.\n   *\n   * @param parquetFooters the parquet footers to be checked.\n   * @return whether the footers are considered `IcebergCompatV2`\n   */\n  protected def isParquetFootersIcebergCompatV2(parquetFooters: Seq[ParquetMetadata]): Boolean = {\n    parquetFooters.forall { parquetFooter =>\n      ParquetIcebergCompatV2Utils.isParquetIcebergCompatV2(parquetFooter)\n    }\n  }\n\n  /** The wrapper function to execute sql */\n  protected def executeSql(sqlStr: String): DataFrame\n\n  /** The wrapper function to assert the protocol and properties for UniForm Iceberg */\n  protected def assertUniFormIcebergProtocolAndProperties(id: String): Unit\n\n  /** The wrapper function to generate a temporary table and directory */\n  protected def withTempTableAndDir(f: (String, String) => Unit): Unit\n\n  /**\n   * Helper function to enforce the properties that an `IcebergCompatV2`\n   * Delta Uniform requires.\n   * e.g., disable DV, ensure reader/writer versions, enable column-mapping.\n   *\n   * @param id the table to be altered.\n   */\n  protected def enforceDeltaUniformRequireProperties(id: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | ALTER TABLE $id\n         | SET TBLPROPERTIES (\n         |   'delta.columnMapping.mode' = 'name',\n         |   'delta.enableDeletionVectors' = 'false',\n         |   'delta.minReaderVersion' = '2',\n         |   'delta.minWriterVersion' = '7'\n         | )\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * Enable `IcebergCompatV2` by ALTER TABLE command for the table.\n   *\n   * @param id the table id used for ALTER TABLE to enable `IcebergCompatV2`.\n   */\n  protected def enableIcebergCompatV2ByAlterTable(id: String): Unit = {\n    executeSql(\n      s\"\"\"\n         | ALTER TABLE $id\n         | SET TBLPROPERTIES (\n         |   'delta.enableIcebergCompatV2' = 'true',\n         |   'delta.universalFormat.enabledFormats' = 'iceberg'\n         | )\n         |\"\"\".stripMargin\n    )\n  }\n\n  /**\n   * The basic test case for enable uniform by ALTER TABLE,\n   * this could be used as a prior setup for subsequent tests.\n   *\n   * @param id the table id.\n   * @param loc the table location.\n   */\n  protected def alterTableToEnableIcebergCompatV2BaseCase(id: String, loc: String): Unit = {\n    createVanillaDeltaTableWithDV(id, loc)\n    insertInitialRowsIntoTable(id)\n\n    val parquetFooters1 = getParquetFooters(spark, id)\n\n    enforceDeltaUniformRequireProperties(id)\n    enableIcebergCompatV2ByAlterTable(id)\n\n    insertAdditionalRowsIntoTable(id)\n\n    val parquetFooters2 = getParquetFooters(spark, id)\n    assertParquetFootersProperties(\n      oldParquetFooters = parquetFooters1,\n      newParquetFooters = parquetFooters2,\n      expectedNumOfOverlappedParquetFiles = 2,\n      expectedNumOfAddedParquetFiles = 2,\n      isOverlappedIcebergCompatV2 = false,\n      isAddedIcebergCompatV2 = true\n    )\n\n    assertUniFormIcebergProtocolAndProperties(id)\n  }\n\n  test(\"Enable IcebergCompatV2 By ALTER TABLE Base Case\") {\n    withTempTableAndDir { case (id, loc) =>\n      alterTableToEnableIcebergCompatV2BaseCase(id, loc)\n    }\n  }\n\n  test(\"Enable IcebergCompatV2 For Vanilla Table By ALTER TABLE With Deletion Vectors Disabled\") {\n    withTempTableAndDir { case (id, loc) =>\n      createVanillaDeltaTableWithoutDV(id, loc)\n      insertInitialRowsIntoTable(id)\n\n      executeSql(\n        s\"\"\"\n           | ALTER TABLE $id\n           | SET TBLPROPERTIES (\n           |   'delta.columnMapping.mode' = 'name'\n           | )\n           |\"\"\".stripMargin\n      )\n\n      val parquetFooters1 = getParquetFooters(spark, id)\n\n      // no need to manually disable DV\n      enableIcebergCompatV2ByAlterTable(id)\n\n      insertAdditionalRowsIntoTable(id)\n\n      val parquetFooters2 = getParquetFooters(spark, id)\n      assertParquetFootersProperties(\n        oldParquetFooters = parquetFooters1,\n        newParquetFooters = parquetFooters2,\n        expectedNumOfOverlappedParquetFiles = 2,\n        expectedNumOfAddedParquetFiles = 2,\n        isOverlappedIcebergCompatV2 = false,\n        isAddedIcebergCompatV2 = true\n      )\n\n      assertUniFormIcebergProtocolAndProperties(id)\n    }\n  }\n\n  test(\"Enable IcebergCompatV2 By ALTER TABLE With Purging Deletion Vectors\") {\n    withTempTableAndDir { case (id, loc) =>\n      createVanillaDeltaTableWithDV(id, loc)\n      insertInitialRowsIntoTable(id)\n\n      executeSql(\n        s\"\"\"\n           | DELETE FROM $id\n           | WHERE name = 'Alex'\n           |\"\"\".stripMargin\n      )\n\n      // purge the current table before running ALTER TABLE\n      // note: this may be different if `autoOptimize` has been enabled,\n      // which will automatically rewrite the parquet file with DV when\n      // the above DELETE FROM command is triggered.\n      executeSql(\n        s\"\"\"\n           | REORG TABLE $id APPLY (PURGE)\n           |\"\"\".stripMargin\n      )\n\n      val parquetFooters1 = getParquetFooters(spark, id)\n\n      enforceDeltaUniformRequireProperties(id)\n      enableIcebergCompatV2ByAlterTable(id)\n\n      insertAdditionalRowsIntoTable(id)\n\n      val parquetFooters2 = getParquetFooters(spark, id)\n      assertParquetFootersProperties(\n        oldParquetFooters = parquetFooters1,\n        newParquetFooters = parquetFooters2,\n        expectedNumOfOverlappedParquetFiles = 2,\n        expectedNumOfAddedParquetFiles = 2,\n        isOverlappedIcebergCompatV2 = false,\n        isAddedIcebergCompatV2 = true\n      )\n\n      assertUniFormIcebergProtocolAndProperties(id)\n    }\n  }\n\n  test(\"REORG UPGRADE UNIFORM Should Rewrite All Parquet Files To Be IcebergCompatV2\") {\n    withTempTableAndDir { case (id, loc) =>\n      alterTableToEnableIcebergCompatV2BaseCase(id, loc)\n\n      // run the REORG UPGRADE UNIFORM command to rewrite the portion of\n      // parquet files that are not `IcebergCompatV2`.\n      executeSql(\n        s\"\"\"\n           | REORG TABLE $id APPLY\n           | (UPGRADE UNIFORM (ICEBERG_COMPAT_VERSION = 2))\n           |\"\"\".stripMargin\n      )\n\n      // now all the parquet files must be `IcebergCompatV2`\n      val parquetFooters3 = getParquetFooters(spark, id)\n      assert(isParquetFootersIcebergCompatV2(parquetFooters3))\n\n      assertUniFormIcebergProtocolAndProperties(id)\n    }\n  }\n\n  // TODO: update this test when automatically disable V1 is supported when upgrading\n  //       from an existing `IcebergCompatV1` table.\n  test(\"Manually Enable V2 and Disable V1 When Upgrading From an IcebergCompatV1 Table\") {\n    withTempTableAndDir { case (id, loc) =>\n      createIcebergCompatV1Table(id, loc)\n      insertInitialRowsIntoTable(id)\n\n      val parquetFooters1 = getParquetFooters(spark, id)\n\n      val snapshot1 = DeltaLog.forTable(spark, new TableIdentifier(id)).update()\n      assert(\n        isIcebergCompatV1Enabled(snapshot1),\n        \"`IcebergCompatV1` should be enabled for the current table\"\n      )\n\n      enforceDeltaUniformRequireProperties(id)\n\n      // enable `IcebergCompatV2` for the `IcebergCompatV1` table.\n      // note that `IcebergCompatV1` needs to be disabled *manually* as for now.\n      executeSql(\n        s\"\"\"\n           | ALTER TABLE $id\n           | SET TBLPROPERTIES (\n           |   'delta.enableIcebergCompatV1' = 'false',\n           |   'delta.enableIcebergCompatV2' = 'true',\n           |   'delta.universalFormat.enabledFormats' = 'iceberg'\n           | )\n           |\"\"\".stripMargin\n      )\n\n      insertAdditionalRowsIntoTable(id)\n\n      val parquetFooters2 = getParquetFooters(spark, id)\n      assertParquetFootersProperties(\n        oldParquetFooters = parquetFooters1,\n        newParquetFooters = parquetFooters2,\n        expectedNumOfOverlappedParquetFiles = 2,\n        expectedNumOfAddedParquetFiles = 2,\n        isOverlappedIcebergCompatV2 = true,\n        isAddedIcebergCompatV2 = true\n      )\n\n      val snapshot2 = DeltaLog.forTable(spark, new TableIdentifier(id)).update()\n      assert(\n        !isIcebergCompatV1Enabled(snapshot2),\n        \"`IcebergCompatV1` should be disabled after enabling `IcebergCompatV2` by ALTER TABLE\"\n      )\n      assert(\n        isIcebergCompatV2Enabled(snapshot2),\n        \"`IcebergCompatV2` should be enabled after being enabled by ALTER TABLE\"\n      )\n\n      assertUniFormIcebergProtocolAndProperties(id)\n    }\n  }\n\n  test(\"Enabling V1 and V2 At The Same Time For Vanilla Delta Table Should Fail\") {\n    withTempTableAndDir { case (id, loc) =>\n      createVanillaDeltaTableWithDV(id, loc)\n      insertInitialRowsIntoTable(id)\n\n      enforceDeltaUniformRequireProperties(id)\n\n      val ex = intercept[DeltaUnsupportedOperationException](\n        executeSql(\n          s\"\"\"\n             | ALTER TABLE $id\n             | SET TBLPROPERTIES (\n             |   'delta.enableIcebergCompatV1' = 'true',\n             |   'delta.enableIcebergCompatV2' = 'true',\n             |   'delta.universalFormat.enabledFormats' = 'iceberg'\n             | )\n             |\"\"\".stripMargin\n        )\n      )\n      assertResult(\n        \"DELTA_ICEBERG_COMPAT_VIOLATION.VERSION_MUTUAL_EXCLUSIVE\"\n      )(ex.getErrorClass)\n    }\n  }\n\n  test(\"Enabling V1 and V2 At The Same Time For IcebergCompatV1 Delta Uniform Table Should Fail\") {\n    withTempTableAndDir { case (id, loc) =>\n      createIcebergCompatV1Table(id, loc)\n      insertInitialRowsIntoTable(id)\n\n      enforceDeltaUniformRequireProperties(id)\n\n      val ex = intercept[DeltaUnsupportedOperationException](\n        executeSql(\n          s\"\"\"\n             | ALTER TABLE $id\n             | SET TBLPROPERTIES (\n             |   'delta.enableIcebergCompatV1' = 'true',\n             |   'delta.enableIcebergCompatV2' = 'true',\n             |   'delta.universalFormat.enabledFormats' = 'iceberg'\n             | )\n             |\"\"\".stripMargin\n        )\n      )\n      assertResult(\n        \"DELTA_ICEBERG_COMPAT_VIOLATION.VERSION_MUTUAL_EXCLUSIVE\"\n      )(ex.getErrorClass)\n    }\n  }\n\n  test(\"Disable Column-Mapping When Enabling IcebergCompatV2 By ALTER TABLE Should Fail\") {\n    withTempTableAndDir { case (id, loc) =>\n      createVanillaDeltaTableWithDV(id, loc)\n      insertInitialRowsIntoTable(id)\n\n      enforceDeltaUniformRequireProperties(id)\n\n      // disable column-mapping when enabling `IcebergCompatV2`\n      // delta uniform by ALTER TABLE should fail\n      val ex = intercept[DeltaUnsupportedOperationException](\n        executeSql(\n          s\"\"\"\n             | ALTER TABLE $id\n             | SET TBLPROPERTIES (\n             |   'delta.columnMapping.mode' = 'none',\n             |   'delta.enableIcebergCompatV2' = 'true',\n             |   'delta.universalFormat.enabledFormats' = 'iceberg'\n             | )\n             |\"\"\".stripMargin\n        )\n      )\n      assertResult(\n        \"DELTA_ICEBERG_COMPAT_VIOLATION.WRONG_REQUIRED_TABLE_PROPERTY\"\n      )(ex.getErrorClass)\n    }\n  }\n\n  test(\"Enable UniForm With LIST, MAP, and Column-Mapping Enabled By ALTER TABLE\") {\n    withTempTableAndDir { case (id, loc) =>\n      createDeltaTableWithNestedTypesAndColumnMapping(id, loc)\n      insertRowToDeltaTableWithNestedTypesAndColumnMapping(id)\n\n      val parquetFooters1 = getParquetFooters(spark, id)\n\n      // only DV needs to be disabled here\n      executeSql(\n        s\"\"\"\n           | ALTER TABLE $id\n           | SET TBLPROPERTIES (\n           |   'delta.enableDeletionVectors' = 'false'\n           | )\n           |\"\"\".stripMargin\n      )\n\n      enableIcebergCompatV2ByAlterTable(id)\n\n      insertRowToDeltaTableWithNestedTypesAndColumnMapping(id)\n\n      val parquetFooters2 = getParquetFooters(spark, id)\n      assertParquetFootersProperties(\n        oldParquetFooters = parquetFooters1,\n        newParquetFooters = parquetFooters2,\n        expectedNumOfAddedParquetFiles = 1,\n        expectedNumOfOverlappedParquetFiles = 1,\n        isOverlappedIcebergCompatV2 = false,\n        isAddedIcebergCompatV2 = true\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/SparkSessionSwitch.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark\n\nimport org.apache.spark.sql.SparkSession\n\n/**\n * Helper for easily switch between multiple sessions in test\n */\ntrait SparkSessionSwitch {\n\n  private val knownSessions =\n    collection.mutable.HashMap[SparkSession, (Option[SparkContext], SparkEnv)]()\n\n  /**\n   * Create a SparkSession and save its context. Calling this will not change\n   * the current active SparkSession. Use [[withSession]] when you want to use\n   * the newly created session.\n   *\n   * @param factory used to create the session\n   * @return the newly created session\n   */\n  def newSession(factory: => SparkSession): SparkSession = {\n    registerActiveSession()\n    val old = SparkSession.getActiveSession\n    clear()\n    val created = factory\n    registerActiveSession()\n    old.foreach(restore)\n    created\n  }\n\n  /**\n   * Execute code with the given session.\n   * @param session session to use\n   * @param thunk code to execute within the specified session\n   */\n  def withSession[T](session: SparkSession)(thunk: SparkSession => T): T = {\n    val oldSession = SparkSession.getActiveSession\n    restore(session)\n    val result = thunk(session)\n    oldSession.foreach(restore)\n    result\n  }\n\n  /**\n   * Record the SparkContext/SparkEnv for current active session\n   */\n  private def registerActiveSession(): Unit = {\n    SparkSession.getActiveSession\n      .foreach(knownSessions.put(_, (SparkContext.getActive, SparkEnv.get)))\n  }\n\n  /**\n   * Restore the snapshot made for the given session\n   * @param session the session to be restore\n   */\n  private def restore(session: SparkSession): Unit = {\n    val (restoreContext, restoreEnv) = knownSessions.getOrElse(\n      session, throw new IllegalArgumentException(\"Unknown Session to restore\"))\n    SparkSession.setActiveSession(session)\n    SparkSession.setDefaultSession(session)\n\n    val oldContext = SparkContext.getActive\n    SparkContext.clearActiveContext()\n    restoreContext.foreach(SparkContext.setActiveContext)\n    // Synchronize the context\n    (oldContext, restoreContext) match {\n      case (Some(off), Some(on)) => syncContext(off, on)\n      case _ =>\n    }\n\n    SparkEnv.set(restoreEnv)\n  }\n\n  /**\n   * Clear the session related context. Necessary before creating new sessions\n   */\n  private def clear(): Unit = {\n    SparkSession.clearActiveSession()\n    SparkSession.clearDefaultSession()\n    SparkContext.clearActiveContext()\n    SparkEnv.set(null)\n  }\n\n  /**\n   * Synchronize local properties when switch SparkContext by merging\n   * and overwriting from off to on\n   * @param off the context to be deactivated\n   * @param on the context to be activated\n   */\n  private def syncContext(off: SparkContext, on: SparkContext): Unit = {\n    // NOTE: cannot use putAll due to a problem of Scala2 + JDK9+\n    // See https://github.com/scala/bug/issues/10418 for detail\n    val onProperties = on.localProperties.get()\n    off.localProperties.get().forEach((k, v) => onProperties.put(k, v))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/UniFormE2EIcebergSuiteBase.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport scala.collection.mutable\n\nimport org.apache.spark.sql.delta._\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.types._\n\n/**\n * This test suite base aims at testing the end-to-end behavior of UniForm.\n * It writes Delta tables, and reads the generated Iceberg tables to\n * perform verification.\n */\nabstract class UniFormE2EIcebergSuiteBase extends UniFormE2ETest {\n\n  val testTableName = \"delta_table\"\n\n  var compatVersions: Seq[Int] = Seq(1, 2)\n\n  def extraTableProperties(compatVersion: Int): String = {\n    val extraProps = mutable.HashMap[String, String]()\n    val compat = IcebergCompat.getForVersion(compatVersion)\n\n    if (compat.incompatibleTableFeatures.contains(DeletionVectorsTableFeature)) {\n      extraProps.put(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, \"false\")\n    }\n\n    extraProps.map(pair => s\", '${pair._1}' = '${pair._2}'\").mkString(\" \")\n  }\n\n  compatVersions.foreach { compatVersion =>\n    test(s\"Basic Insert - compatV$compatVersion\") {\n      withTable(testTableName) {\n        write(\n          s\"\"\"CREATE TABLE $testTableName (col1 INT) USING DELTA\n             |TBLPROPERTIES (\n             |  'delta.columnMapping.mode' = 'name',\n             |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n             |  'delta.universalFormat.enabledFormats' = 'iceberg'\n             |  ${extraTableProperties(compatVersion)}\n             |)\"\"\".stripMargin)\n        write(s\"INSERT INTO $testTableName VALUES (123)\")\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(123)))\n      }\n    }\n  }\n\n  compatVersions.foreach { compatVersion =>\n    test(s\"CIUD - compatV$compatVersion\") {\n      withTable(testTableName) {\n        write(\n          s\"\"\"CREATE TABLE `$testTableName` (col1 INT) USING DELTA\n             |TBLPROPERTIES (\n             |  'delta.columnMapping.mode' = 'name',\n             |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n             |  'delta.universalFormat.enabledFormats' = 'iceberg'\n             |  ${extraTableProperties(compatVersion)}\n             |)\"\"\".stripMargin)\n        write(s\"INSERT INTO `$testTableName` VALUES (123),(456),(567),(331)\")\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(123), Row(331), Row(456), Row(567)))\n        write(s\"UPDATE `$testTableName` SET col1 = 191 WHERE col1 = 567\")\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(123), Row(191), Row(331), Row(456)))\n        write(s\"DELETE FROM `$testTableName` WHERE col1 = 456\")\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(123), Row(191), Row(331)))\n      }\n    }\n  }\n\n  compatVersions.foreach { compatVersion =>\n    test(s\"CTAS - compatV$compatVersion\") {\n      withTable(testTableName, \"source\") {\n        write(\"CREATE TABLE source (col1 INT) USING DELTA\")\n        write(\"INSERT INTO source VALUES (1), (2), (3)\")\n        write(\n          s\"\"\"CREATE TABLE `$testTableName` USING DELTA\n             |TBLPROPERTIES (\n             |  'delta.columnMapping.mode' = 'name',\n             |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n             |  'delta.universalFormat.enabledFormats' = 'iceberg'\n             |  ${extraTableProperties(compatVersion)}\n             |) AS SELECT col1 FROM source\"\"\".stripMargin)\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(1), Row(2), Row(3)))\n        write(s\"UPDATE `$testTableName` SET col1 = 100 WHERE col1 = 1\")\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(2), Row(3), Row(100)))\n        write(s\"DELETE FROM `$testTableName` WHERE col1 = 3\")\n        readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(2), Row(100)))\n      }\n    }\n  }\n\n  compatVersions.foreach { compatVersion =>\n    test(s\"Table with partition - compatV$compatVersion\") {\n      withTable(testTableName) {\n        write(\n          s\"\"\"CREATE TABLE $testTableName (id INT, part STRING) USING delta\n             |PARTITIONED BY (part)\n             |TBLPROPERTIES (\n             |  'delta.columnMapping.mode' = 'name',\n             |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n             |  'delta.universalFormat.enabledFormats' = 'iceberg'\n             |  ${extraTableProperties(compatVersion)}\n             |)\"\"\".stripMargin)\n        write(s\"INSERT INTO `$testTableName` VALUES (123, 'p1'), (456, 'p2'), (789, 'p1')\")\n        readAndVerify(testTableName, \"id, part\", \"id\",\n          Seq(Row(123, \"p1\"), Row(456, \"p2\"), Row(789, \"p1\")))\n      }\n    }\n  }\n\n  compatVersions.foreach { compatVersion =>\n    test(s\"Nested struct schema test - compatV$compatVersion\") {\n      withTable(testTableName) {\n        write(\n          s\"\"\"CREATE TABLE $testTableName\n             | (col1 INT, col2 STRUCT<f1: STRUCT<f2: INT, f3: STRUCT<f4: INT, f5: INT>\n             | , f6: INT>, f7: INT>) USING DELTA\n             |TBLPROPERTIES (\n             |  'delta.columnMapping.mode' = 'name',\n             |  'delta.enableIcebergCompatV$compatVersion' = 'true',\n             |  'delta.universalFormat.enabledFormats' = 'iceberg'\n             |  ${extraTableProperties(compatVersion)}\n             |)\"\"\".stripMargin)\n\n        val data = Seq(\n          Row(1, Row(Row(2, Row(3, 4), 5), 6))\n        )\n\n        val innerStruct3 = StructType(\n          StructField(\"f4\", IntegerType) ::\n            StructField(\"f5\", IntegerType) :: Nil)\n\n        val innerStruct2 = StructType(\n          StructField(\"f2\", IntegerType) ::\n            StructField(\"f3\", innerStruct3) ::\n            StructField(\"f6\", IntegerType) :: Nil)\n\n        val innerStruct = StructType(\n          StructField(\"f1\", innerStruct2) ::\n            StructField(\"f7\", IntegerType) :: Nil)\n\n        val schema = StructType(\n          StructField(\"col1\", IntegerType) ::\n            StructField(\"col2\", innerStruct) :: Nil)\n\n        val tableFullName = tableNameForRead(testTableName)\n\n        spark.createDataFrame(spark.sparkContext.parallelize(data), schema)\n          .write.format(\"delta\").mode(\"append\")\n          .saveAsTable(testTableName)\n\n        readAndVerify(tableFullName, \"col1, col2\", \"col1\", data)\n      }\n    }\n  }\n\n  test(\"reorg from v1 to v2\") {\n    withTable(testTableName) {\n      write(\n        s\"\"\"CREATE TABLE $testTableName (col1 INT) USING DELTA\n           |TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.enableIcebergCompatV1' = 'true',\n           |  'delta.universalFormat.enabledFormats' = 'iceberg',\n           |  'delta.enableDeletionVectors' = 'false'\n           |)\"\"\".stripMargin)\n      write(s\"INSERT INTO $testTableName VALUES (1)\")\n      readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(1)))\n\n      write(s\"ALTER TABLE `$testTableName` UNSET TBLPROPERTIES \" +\n        s\"('delta.universalFormat.enabledFormats')\")\n      write(s\"\"\"\n               | REORG TABLE $testTableName APPLY\n               | (UPGRADE UNIFORM (ICEBERG_COMPAT_VERSION = 2))\n               |\"\"\".stripMargin)\n      write(s\"INSERT INTO $testTableName VALUES (2)\")\n      readAndVerify(testTableName, \"col1\", \"col1\", Seq(Row(1), Row(2)))\n    }\n  }\n\n  // TODO createReaderSparkSession is no longer supported.\n  // Please use readAndVerify and re-enable the cases\n  /*\n  test(\"Insert Partitioned Table\") {\n    val partitionColumns = Array(\n      \"str STRING\",\n      \"i INTEGER\",\n      \"l LONG\",\n      \"s SHORT\",\n      \"b BYTE\",\n      \"dt DATE\",\n      \"bin BINARY\",\n      \"bool BOOLEAN\",\n      \"ts_ntz TIMESTAMP_NTZ\",\n      \"ts TIMESTAMP\")\n\n    val partitionValues: Array[Any] = Array(\n      \"'some_value'\",\n      1,\n      1234567L,\n      1000,\n      119,\n      \"to_date('2016-12-31', 'yyyy-MM-dd')\",\n      \"'asdf'\",\n      true,\n      \"TIMESTAMP_NTZ'2021-12-06 00:00:00'\",\n      \"TIMESTAMP'2023-08-18 05:00:00UTC-7'\"\n    )\n\n    partitionColumns zip partitionValues map {\n      partitionColumnsAndValues =>\n        val partitionColumnName =\n          partitionColumnsAndValues._1.split(\" \")(0)\n        val tableName = testTableName + \"_\" + partitionColumnName\n        withTable(tableName) {\n          write(\n            s\"\"\"CREATE TABLE $tableName (${partitionColumnsAndValues._1}, col1 INT)\n               | USING DELTA\n               | PARTITIONED BY ($partitionColumnName)\n               | TBLPROPERTIES (\n               |  'delta.columnMapping.mode' = 'name',\n               |  'delta.enableIcebergCompatV2' = 'true',\n               |  'delta.universalFormat.enabledFormats' = 'iceberg'\n               |)\"\"\".stripMargin)\n          write(s\"INSERT INTO $tableName VALUES (${partitionColumnsAndValues._2}, 123)\")\n          val verificationQuery = s\"SELECT col1 FROM $tableName \" +\n            s\"where ${partitionColumnName}=${partitionColumnsAndValues._2}\"\n          // Verify against Delta read and Iceberg read\n          checkAnswer(spark.sql(verificationQuery), Seq(Row(123)))\n          checkAnswer(createReaderSparkSession.sql(verificationQuery), Seq(Row(123)))\n        }\n    }\n  }\n\n  test(\"Insert Partitioned Table - Multiple Partitions\") {\n    withTable(testTableName) {\n      write(\n        s\"\"\"CREATE TABLE $testTableName (id int, ts timestamp, col1 INT)\n           | USING DELTA\n           | PARTITIONED BY (id, ts)\n           | TBLPROPERTIES (\n           |  'delta.columnMapping.mode' = 'name',\n           |  'delta.enableIcebergCompatV2' = 'true',\n           |  'delta.universalFormat.enabledFormats' = 'iceberg'\n           |)\"\"\".stripMargin)\n      write(s\"INSERT INTO $testTableName VALUES (1, TIMESTAMP'2023-08-18 05:00:00UTC-7', 123)\")\n      val verificationQuery = s\"SELECT col1 FROM $testTableName \" +\n        s\"where id=1 and ts=TIMESTAMP'2023-08-18 05:00:00UTC-7'\"\n      // Verify against Delta read and Iceberg read\n      checkAnswer(spark.sql(verificationQuery), Seq(Row(123)))\n      checkAnswer(createReaderSparkSession.sql(verificationQuery), Seq(Row(123)))\n    }\n  }\n\n  test(\"Insert Partitioned Table - UTC Adjustment for Non-ISO Timestamp Partition values\") {\n    withTable(testTableName) {\n      withTimeZone(\"GMT-8\") {\n        withSQLConf(UTC_TIMESTAMP_PARTITION_VALUES.key -> \"false\") {\n          write(\n            s\"\"\"CREATE TABLE $testTableName (id int, ts timestamp)\n               | USING DELTA\n               | PARTITIONED BY (ts)\n               | TBLPROPERTIES (\n               |  'delta.columnMapping.mode' = 'name',\n               |  'delta.enableIcebergCompatV2' = 'true',\n               |  'delta.universalFormat.enabledFormats' = 'iceberg'\n               |)\"\"\".stripMargin)\n          write(s\"INSERT INTO $testTableName\" +\n            s\" VALUES (1, timestamp'2021-06-30 00:00:00.123456')\")\n\n          // Verify partition values in Delta Log\n          val deltaLog = DeltaLog.forTable(spark, TableIdentifier(testTableName))\n          val partitionColName = deltaLog.unsafeVolatileMetadata.physicalPartitionColumns.head\n          val partitionValues = deltaLog.update().allFiles.head.partitionValues\n          assert(partitionValues === Map(partitionColName -> \"2021-06-30 00:00:00.123456\"))\n\n          // Verify against Delta read and Iceberg read\n          val verificationQuery = s\"SELECT id FROM $testTableName \" +\n            s\"where ts=TIMESTAMP'2021-06-30 08:00:00.123456UTC'\"\n          checkAnswer(spark.sql(verificationQuery), Seq(Row(1)))\n          checkAnswer(createReaderSparkSession.sql(verificationQuery), Seq(Row(1)))\n        }\n      }\n    }\n  }\n*/\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/UniFormE2ETest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform\n\nimport org.apache.spark.sql.{DataFrame, QueryTest, Row}\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Base classes for all UniForm end-to-end test cases. Provides support to\n * write data with Delta SparkSession and read data for verification.\n *\n * People who need to write a new test suite should extend this class and\n * implement their test cases with [[write]] and [[readAndVerify]], which execute\n * with the writer and reader respectively.\n *\n * Implementing classes need to correctly set up the reader and writer environments.\n * See [[UniFormE2EIcebergSuiteBase]] for existing examples.\n */\ntrait UniFormE2ETest\n  extends QueryTest\n  with SharedSparkSession {\n\n  /**\n   * Execute write operations through the writer SparkSession\n   *\n   * @param sqlText write query to the UniForm table\n   */\n  protected def write(sqlText: String): DataFrame = sql(sqlText)\n\n  /**\n   * Verify the result by reading from the reader session and compare the result to the expected.\n   *\n   * @param table  write table name\n   * @param fields fields to verify, separated by comma. E.g., \"col1, col2\"\n   * @param orderBy fields to order the results, separated by comma.\n   * @param expect expected result\n   */\n  protected def readAndVerify(\n      table: String, fields: String, orderBy: String, expect: Seq[Row]): Unit =\n    throw new UnsupportedOperationException\n\n  /**\n   * Subclasses should override this method when the table name for reading\n   * is different from the table name used for writing. For example, when we\n   * write a table using the name `table1`, and then read it from another catalog\n   * `catalog_read`, this method should return `catalog_read.default.table1`\n   * for the input `table1`.\n   *\n   * @param tableName table name for writing (name only)\n   * @return table name for reading, default is no translation\n   */\n  protected def tableNameForRead(tableName: String): String = tableName\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/hms/EmbeddedHMS.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform.hms\n\nimport java.util.UUID\nimport java.io.{BufferedReader, File, IOException, InputStreamReader}\nimport java.net.ServerSocket\nimport java.nio.file.Files\nimport java.sql.{Connection, DriverManager}\n\nimport org.apache.commons.io.FileUtils\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.hive.conf.HiveConf\nimport org.apache.hadoop.hive.conf.HiveConf.ConfVars\n\n/**\n * EmbeddedHMS is an embedded Hive MetaStore for testing purposes.\n * Multiple EmbeddedHMS instances can be started in parallel on the same host\n * (see [[HMSTest]] for how to use it in the code).\n */\nclass EmbeddedHMS {\n  private var server: HMSServer = _\n  private var whFolder: String = _\n  private var dbName: String = _\n  private var started = false\n  private var port: Int = 0\n\n  /**\n   * Generate a random suffix for HMS warehouse/metastore to keep\n   * the directory unique for each suite if running concurrently.\n   */\n  def randomSuffix: String = {\n    UUID.randomUUID().toString\n  }\n\n  /**\n   * Start an EmbeddedHMS instance\n   */\n  def start(): Unit = {\n    if (started) return\n    port = EmbeddedHMS.firstAvailablePort()\n    val dbFolder = Files.createTempDirectory(\"ehms_metastore_\" + randomSuffix)\n    Files.delete(dbFolder) // Derby needs the folder to be non-existent\n    dbName = dbFolder.toString\n    whFolder = Files.createTempDirectory(\"ehms_warehouse_\" + randomSuffix).toString\n\n    initDatabase(dbName)\n\n    val innerConf = new HiveConf()\n    innerConf.set(ConfVars.HIVE_IN_TEST.varname, \"false\")\n    innerConf.set(ConfVars.METASTOREWAREHOUSE.varname, whFolder)\n    innerConf.set(ConfVars.METASTORECONNECTURLKEY.varname, s\"jdbc:derby:$dbName;create=true\")\n    server = new HMSServer(innerConf, port)\n    server.start()\n\n    started = true\n  }\n\n  /**\n   * Stop the instance and cleanup its resources\n   */\n  def stop(): Unit = {\n    if (!started) return\n    server.stop()\n    // Cleanup on exit\n    FileUtils.deleteDirectory(new File(dbName))\n    FileUtils.deleteDirectory(new File(whFolder))\n    started = false\n  }\n\n  /**\n   * Fetch the configuration used for clients to connect to the MetaStore\n   * @return conf containing thrift uri and warehouse location\n   */\n  def conf(): Configuration = {\n    if (!started) throw new IllegalStateException(\"Not started\")\n    val conf = new Configuration()\n    conf.set(ConfVars.METASTOREWAREHOUSE.varname, whFolder)\n    conf.set(ConfVars.METASTOREURIS.varname, s\"thrift://localhost:$port\")\n    conf\n  }\n\n  /**\n   * Load SQL scripts into Apache Derby instance to initialize the metastore\n   * schema. The script used here is copied from HMS official repo.\n   * @param dbFolder the folder to create the database, also the database name\n   */\n  private def initDatabase(dbFolder: String): Unit = {\n    // scalastyle:off classforname\n    // Register the Derby JDBC Driver\n    Class.forName(\"org.apache.derby.jdbc.EmbeddedDriver\").getConstructor().newInstance()\n    // scalastyle:on classforname\n    val con = DriverManager.getConnection(s\"jdbc:derby:$dbFolder;create=true\")\n    // May need to use another version when upgrading Hive dependencies\n    executeScript(con, \"hms/hive-schema-3.1.0.derby.sql\")\n    con.close()\n    // Shutdown the Derby instance properly, allowing it to clean up.\n    try {\n      DriverManager.getConnection(s\"jdbc:derby:$dbFolder;shutdown=true\")\n    } catch {\n      // From Derby doc:\n      // \"A successful shutdown always results in an SQLException to indicate\n      // that Derby has shut down and that there is no other exception.\"\n      // We thus ignore the exception here.\n      case _: java.sql.SQLException =>\n    }\n  }\n\n  /**\n   * Execute sql scripts in the given resource file\n   * @param con        database connection\n   * @param scriptFile the name of the resource location of the sql script\n   */\n  private def executeScript(con: Connection, scriptFile: String): Unit = {\n    val scriptIs = Thread.currentThread().getContextClassLoader.getResourceAsStream(scriptFile)\n    if (scriptIs == null) {\n      throw new RuntimeException(\"Make sure derby init script is in the classpath\")\n    }\n    val reader = new BufferedReader(new InputStreamReader(scriptIs))\n    var line: String = reader.readLine\n    val buffer: StringBuilder = new StringBuilder()\n    val stmt = con.createStatement()\n    while (line != null) {\n      line match {\n        case comment if comment.startsWith(\"--\") =>\n        case eos if eos.endsWith(\";\") =>\n          if (buffer.nonEmpty) buffer.append(\"\\n\")\n          buffer.append(eos)\n          buffer.deleteCharAt(buffer.length - 1) // Remove semicolon\n          stmt.addBatch(buffer.toString)\n          buffer.clear\n        case piece =>\n          if (buffer.nonEmpty) buffer.append(\"\\n\")\n          buffer.append(piece)\n      }\n      line = reader.readLine()\n    }\n    reader.close()\n    stmt.executeBatch()\n    stmt.close()\n  }\n}\n\nobject EmbeddedHMS {\n  var start = 9084\n\n  def firstAvailablePort(): Integer = this.synchronized {\n    for (port <- start until 65536) {\n      var ss: ServerSocket = null\n      try {\n        ss = new ServerSocket(port)\n        ss.setReuseAddress(true)\n        start = port + 1\n        return port\n      } catch {\n        case e: IOException =>\n      } finally {\n        if (ss != null) {\n          try ss.close()\n          catch {\n            case e: IOException =>\n          }\n        }\n      }\n    }\n    throw new RuntimeException(\"No port is available\")\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/hms/HMSServer.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform.hms\n\nimport java.net.InetSocketAddress\n\nimport org.apache.hadoop.hive.conf.HiveConf\nimport org.apache.hadoop.hive.metastore.HiveMetaStore.HMSHandler\nimport org.apache.hadoop.hive.metastore.RetryingHMSHandler\nimport org.apache.hadoop.hive.metastore.TSetIpAddressProcessor\nimport org.apache.thrift.protocol.{TBinaryProtocol, TProtocol, TProtocolFactory}\nimport org.apache.thrift.server.{ServerContext, TServer, TServerEventHandler, TThreadPoolServer}\nimport org.apache.thrift.transport.{TServerSocket, TTransport, TTransportFactory}\n\n/**\n * Start a Thrift Server that accepts standard HMS thrift client.\n *\n * @param conf including database connection and warehouse location\n * @param port the port this thrift server listens\n */\nclass HMSServer(val conf: HiveConf, val port: Int) {\n\n  private var tServer: TServer = _\n  private var serverThread: MetastoreThread = _\n\n  def start(): Unit = {\n    val maxMessageSize = 100L * 1024 * 1024\n\n    val protocolFactory: TProtocolFactory = new TBinaryProtocol.Factory\n    val inputProtoFactory: TProtocolFactory = new TBinaryProtocol.Factory(\n      true, true, maxMessageSize, maxMessageSize)\n    val hmsHandler = new HMSHandler(\"default\", conf)\n    val handler = RetryingHMSHandler.getProxy(conf, hmsHandler, false)\n    val transFactory = new TTransportFactory\n    val processor = new TSetIpAddressProcessor(handler)\n    val serverSocket = new TServerSocket(new InetSocketAddress(port))\n\n    val args = new TThreadPoolServer.Args(serverSocket)\n      .processor(processor)\n      .transportFactory(transFactory)\n      .protocolFactory(protocolFactory)\n      .inputProtocolFactory(inputProtoFactory)\n      .minWorkerThreads(5)\n      .maxWorkerThreads(5);\n\n    tServer = new TThreadPoolServer(args);\n\n    val tServerEventHandler = new TServerEventHandler() {\n      override def preServe(): Unit = ()\n\n      override def createContext(tProtocol: TProtocol, tProtocol1: TProtocol): ServerContext = null\n\n      override def deleteContext(\n          serverContext: ServerContext, tProtocol: TProtocol, tProtocol1: TProtocol): Unit = {\n        // If the IMetaStoreClient#close was called, HMSHandler#shutdown would have already\n        // cleaned up thread local RawStore. Otherwise, do it now.\n        HMSServer.cleanupRawStore()\n      }\n\n      override def processContext(\n          serverContext: ServerContext, tTransport: TTransport, tTransport1: TTransport): Unit = ()\n    }\n    tServer.setServerEventHandler(tServerEventHandler)\n\n    serverThread = new MetastoreThread\n    serverThread.start()\n\n    // Wait till the server is up\n    while (!tServer.isServing) {\n      Thread.sleep(100)\n    }\n  }\n\n  def stop(): Unit = {\n    HMSServer.cleanupRawStore()\n    tServer.stop()\n  }\n\n  /**\n   * The metastore thrift server will run in this thread\n   */\n  private class MetastoreThread extends Thread {\n    super.setDaemon(true)\n    super.setName(\"EmbeddedHMS Metastore Thread\")\n\n    override def run(): Unit = {\n      tServer.serve()\n    }\n  }\n}\n\nobject HMSServer {\n\n  private val localConfField = classOf[HMSHandler].getDeclaredField(\"threadLocalConf\")\n  localConfField.setAccessible(true)\n  private val localConf = localConfField.get().asInstanceOf[ThreadLocal[HiveConf]]\n\n  private def cleanupRawStore(): Unit = {\n    try {\n      val rs = HMSHandler.getRawStore\n      if (rs != null) {\n        rs.shutdown()\n      }\n    } finally {\n      HMSHandler.removeRawStore()\n      localConf.remove()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/uniform/hms/HMSTest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.uniform.hms\n\nimport java.util.concurrent.ConcurrentLinkedQueue\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.hive.conf.HiveConf.ConfVars._\nimport org.scalatest.{BeforeAndAfterAll, Suite}\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.SparkSession\n\nobject HMSPool {\n  private val pool = new ConcurrentLinkedQueue[EmbeddedHMS]()\n  private val maxInstances = 5\n\n  def acquire(): EmbeddedHMS = synchronized {\n    while (pool.isEmpty && pool.size >= maxInstances) {\n      wait()\n    }\n    if (pool.isEmpty) {\n      new EmbeddedHMS()\n    } else {\n      pool.poll()\n    }\n  }\n\n  def release(hms: EmbeddedHMS): Unit = synchronized {\n    pool.offer(hms)\n    notify()\n  }\n}\n\n/**\n * Provide support to testcases that need to use HMS.\n */\ntrait HMSTest extends Suite with BeforeAndAfterAll {\n  private var sharedHMS: EmbeddedHMS = _\n\n  def withMetaStore(thunk: (Configuration) => Unit): Unit = {\n    val conf = sharedHMS.conf()\n    thunk(conf)\n  }\n\n  protected override def beforeAll(): Unit = {\n    sharedHMS = HMSPool.acquire()\n    sharedHMS.start()\n    super.beforeAll()\n  }\n\n  protected override def afterAll(): Unit = {\n    super.afterAll()\n    releaseHMS()\n  }\n\n  protected def releaseHMS(): Unit = {\n    if (sharedHMS != null) {\n      HMSPool.release(sharedHMS)\n      sharedHMS = null\n    }\n  }\n\n  protected def setupSparkConfWithHMS(in: SparkConf): SparkConf = {\n    val conf = sharedHMS.conf()\n    in.set(\"spark.sql.warehouse.dir\", conf.get(METASTOREWAREHOUSE.varname))\n      .set(\"hive.metastore.uris\", conf.get(METASTOREURIS.varname))\n      .set(\"spark.sql.catalogImplementation\", \"hive\")\n  }\n\n  protected def createDeltaSparkSession: SparkSession = {\n    val conf = sharedHMS.conf()\n    val sparkSession = SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"DeltaSession\")\n      .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n      .config(\"spark.sql.warehouse.dir\", conf.get(METASTOREWAREHOUSE.varname))\n      .config(\"hive.metastore.uris\", conf.get(METASTOREURIS.varname))\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n    sparkSession\n  }\n\n  protected def createIcebergSparkSession: SparkSession = {\n    val conf = sharedHMS.conf()\n    val sparkSession = SparkSession.builder()\n      .master(\"local[*]\")\n      .appName(\"IcebergSession\")\n      .config(\"spark.sql.extensions\",\n        \"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions\")\n      .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.iceberg.spark.SparkSessionCatalog\")\n      .config(\"spark.sql.catalog.spark_catalog.cache-enabled\", \"false\")\n      .config(\"spark.sql.warehouse.dir\", conf.get(METASTOREWAREHOUSE.varname))\n      .config(\"hive.metastore.uris\", conf.get(METASTOREURIS.varname))\n      .config(\"spark.sql.catalogImplementation\", \"hive\")\n      .getOrCreate()\n    sparkSession\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/AnalysisHelperSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass AnalysisHelperSuite extends QueryTest with SharedSparkSession {\n\n  test(\"should not throw NullPointerException when Exception has null description\") {\n    class FakeAnalysisHelper extends AnalysisHelper {\n      def throwInterruptedException(): Unit = super.improveUnsupportedOpError {\n        throw new InterruptedException()\n      }\n    }\n\n    // Should throw original exception\n    assertThrows[InterruptedException] {\n      new FakeAnalysisHelper {}.throwInterruptedException()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/BinPackingIteratorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.collection.generic.Sizing\n\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.test.SharedSparkSession\n\ncase class IntArrayImplementingSizing(array: Seq[Int]) extends Sizing {\n  override def size: Int = array.size\n}\n\ncase class TestArbitrarySizing(size: Int) extends Sizing\n\nclass BinPackingIteratorSuite extends QueryTest\n  with SharedSparkSession {\n\n  test(\"Bin packing works\") {\n    val targetSize = 4\n    val testInput = Seq(\n      IntArrayImplementingSizing(Seq(1, 2, 3)),\n      IntArrayImplementingSizing(Seq(1, 2, 3)))\n\n    val binPackingIterator = new BinPackingIterator(testInput.iterator, targetSize)\n\n    assert(binPackingIterator.hasNext)\n    var count = 0\n    for (bin <- binPackingIterator) {\n      assert(bin.size === 1)\n      assert(bin.toSeq === Seq(IntArrayImplementingSizing(Seq(1, 2, 3))))\n      count += 1\n    }\n    assert(count === 2)\n  }\n\n  test(\"Bin packing can handle overflows to internal size tracking\") {\n    val targetSize = Int.MaxValue / 2\n    val testInput = Seq(\n      // 1st bin\n      TestArbitrarySizing(1),\n      TestArbitrarySizing(1),\n      TestArbitrarySizing(2),\n      // 2nd bin\n      TestArbitrarySizing(Int.MaxValue - 14),\n      // 3rd bin\n      TestArbitrarySizing(Int.MaxValue / 2),\n      // 4th bin\n      TestArbitrarySizing(Int.MaxValue / 2)\n    )\n    val binPackingIterator = new BinPackingIterator(testInput.iterator, targetSize)\n    assert(binPackingIterator.hasNext)\n    val firstBin = binPackingIterator.next()\n    assert(firstBin.size === 3)\n    assert(firstBin.toSeq === Seq(\n      TestArbitrarySizing(1),\n      TestArbitrarySizing(1),\n      TestArbitrarySizing(2)))\n    assert(binPackingIterator.hasNext)\n    val secondBin = binPackingIterator.next()\n    assert(secondBin.size === 1)\n    assert(secondBin.toSeq === Seq(TestArbitrarySizing(Int.MaxValue - 14)))\n    assert(binPackingIterator.hasNext)\n    val thirdBin = binPackingIterator.next()\n    assert(thirdBin.size === 1)\n    assert(thirdBin.toSeq === Seq(TestArbitrarySizing(Int.MaxValue / 2)))\n    assert(binPackingIterator.hasNext)\n    val fourthBin = binPackingIterator.next()\n    assert(fourthBin.size === 1)\n    assert(fourthBin.toSeq === Seq(TestArbitrarySizing(Int.MaxValue / 2)))\n    assert(!binPackingIterator.hasNext)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/BinPackingUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.SparkFunSuite\n\nclass BinPackingUtilsSuite extends SparkFunSuite {\n  test(\"test bin-packing\") {\n    val binSize = 5\n    val cases = Seq[(Seq[Int], Seq[Seq[Int]])](\n      (Seq(1, 2, 3, 4, 5), Seq(Seq(1, 2), Seq(3), Seq(4), Seq(5))),\n      (Seq(5, 4, 3, 2, 1), Seq(Seq(1, 2), Seq(3), Seq(4), Seq(5))),\n      // Naive coalescing returns 5 bins where sort-then-coalesce gets 4.\n      (Seq(4, 2, 4, 2, 5), Seq(Seq(2, 2), Seq(4), Seq(4), Seq(5))),\n      // The last element exceeds binSize and it's in its own bin.\n      (Seq(1, 2, 4, 5, 6), Seq(Seq(1, 2), Seq(4), Seq(5), Seq(6))))\n\n    for ((input, expect) <- cases) {\n      assert(BinPackingUtils.binPackBySize(input, (x: Int) => x, (x: Int) => x, binSize) == expect)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/BitmapAggregatorE2ESuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport java.io.{File, IOException}\nimport java.net.URI\nimport java.nio.{ByteBuffer, ByteOrder}\nimport java.nio.file.Files\n\nimport org.apache.spark.sql.catalyst.expressions.aggregation.BitmapAggregator\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.deletionvectors.{PortableRoaringBitmapArraySerializationFormat, RoaringBitmapArray, RoaringBitmapArrayFormat}\nimport org.apache.spark.sql.delta.test.DeltaSQLTestUtils\n\nimport org.apache.spark.sql.{Column, QueryTest}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass BitmapAggregatorE2ESuite extends QueryTest\n  with SharedSparkSession\n  with DeltaSQLTestUtils {\n\n  import BitmapAggregatorE2ESuite._\n  import testImplicits._\n  import org.apache.spark.sql.functions._\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(s\"DataFrame bitmap groupBy aggregate no duplicates - $serializationFormat\") {\n      dataFrameBitmapGroupByAggregateWithoutDuplicates(format = serializationFormat)\n    }\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(\"DataFrame bitmap groupBy aggregate no duplicates - invalid Int ids\" +\n        s\" - $serializationFormat\") {\n      dataFrameBitmapGroupByAggregateWithoutDuplicates(\n        offset = INVALID_INT_OFFSET,\n        format = serializationFormat)\n    }\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(\"DataFrame bitmap groupBy aggregate no duplicates - invalid unsigned Int ids\" +\n        s\" - $serializationFormat\") {\n      dataFrameBitmapGroupByAggregateWithoutDuplicates(\n        offset = UNSIGNED_INT_OFFSET,\n        format = serializationFormat)\n    }\n  }\n\n  private def dataFrameBitmapGroupByAggregateWithoutDuplicates(\n      offset: Long = 0L,\n      format: RoaringBitmapArrayFormat.Value): Unit = {\n    val baseDF = spark\n      .range(DATASET_SIZE)\n      .map { id =>\n        val newId = id + offset\n        // put 2 adjacent and one with gap\n        (newId % 6) match {\n          case 0 | 1 | 4 => (\"file1\" -> newId)\n          case 2 | 3 | 5 => (\"file2\" -> newId)\n        }\n      }\n      .toDF(\"file\", \"id\")\n      .cache()\n\n    val bitmapAgg = bitmapAggColumn(baseDF(\"id\"), format)\n    val aggregationOutput = baseDF\n      .groupBy(\"file\")\n      .agg(bitmapAgg)\n      .as[(String, (Long, Long, Array[Byte]))]\n      .collect()\n      .toMap\n      .mapValues(v => RoaringBitmapArray.readFrom(v._3))\n\n    val dfFile1 = baseDF\n      .select(\"id\")\n      .where(\"file = 'file1'\")\n      .as[Long]\n      .collect()\n    val dfFile2 = baseDF\n      .select(\"id\")\n      .where(\"file = 'file2'\")\n      .as[Long]\n      .collect()\n\n    assertEqualContents(aggregationOutput(\"file1\"), dfFile1)\n    assertEqualContents(aggregationOutput(\"file2\"), dfFile2)\n    baseDF.unpersist()\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(\"DataFrame bitmap groupBy aggregate with duplicates\" +\n        s\" - $serializationFormat\") {\n      dataFrameBitmapGroupAggregateWithDuplicates(format = serializationFormat)\n    }\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(\"DataFrame bitmap groupBy aggregate with duplicates - invalid Int ids\" +\n        s\" - $serializationFormat\") {\n      dataFrameBitmapGroupAggregateWithDuplicates(\n        offset = INVALID_INT_OFFSET,\n        format = serializationFormat)\n    }\n  }\n\n  for (serializationFormat <- RoaringBitmapArrayFormat.values) {\n    test(\"DataFrame bitmap groupBy aggregate with duplicates - invalid unsigned Int ids\" +\n        s\" - $serializationFormat\") {\n      dataFrameBitmapGroupAggregateWithDuplicates(\n        offset = UNSIGNED_INT_OFFSET,\n        format = serializationFormat)\n    }\n  }\n\n  def dataFrameBitmapGroupAggregateWithDuplicates(\n      offset: Long = 0L,\n      format: RoaringBitmapArrayFormat.Value) {\n    val baseDF = spark\n      .range(DATASET_SIZE)\n      .flatMap { id =>\n        val newId = id + offset\n        // put two adjacent and duplicate the one after a gap\n        (newId % 6) match {\n          case 0 | 1 => Seq(\"file1\" -> newId)\n          case 2 | 3 => Seq(\"file2\" -> newId)\n          case 4 => Seq(\"file1\" -> newId, \"file1\" -> newId) // duplicate in file1\n          case 5 => Seq(\"file2\" -> newId, \"file2\" -> newId) // duplicate in file2\n        }\n      }\n      .toDF(\"file\", \"id\")\n      .cache()\n\n    val bitmapAgg = bitmapAggColumn(baseDF(\"id\"), format)\n    // scalastyle:off countstring\n    val aggregationOutput = baseDF\n      .groupBy(\"file\")\n      .agg(bitmapAgg, count(\"id\"))\n      .as[(String, (Long, Long, Array[Byte]), Long)]\n      .collect()\n      .map(t => (t._1 -> (RoaringBitmapArray.readFrom(t._2._3), t._3)))\n      .toMap\n    // scalastyle:on countstring\n\n    val dfFile1 = baseDF\n      .select(\"id\")\n      .where(\"file = 'file1'\")\n      .distinct()\n      .as[Long]\n      .collect()\n    val dfFile2 = baseDF\n      .select(\"id\")\n      .where(\"file = 'file2'\")\n      .distinct()\n      .as[Long]\n      .collect()\n\n    val file1Value = aggregationOutput(\"file1\")\n    assert(file1Value._2 > file1Value._1.cardinality)\n    val file2Value = aggregationOutput(\"file2\")\n    assert(file2Value._2 > file2Value._1.cardinality)\n\n    assertEqualContents(file1Value._1, dfFile1)\n    assertEqualContents(file2Value._1, dfFile2)\n  }\n\n  // modulo ordering\n  private def assertEqualContents(aggregator: RoaringBitmapArray, dataset: Array[Long]): Unit = {\n    // make sure they are in the same order\n    val aggregatorArray = aggregator.values.sorted\n    assert(aggregatorArray === dataset.sorted)\n  }\n}\n\nobject BitmapAggregatorE2ESuite {\n  // Pick something large enough hat 2 files have at least 64k entries each\n  final val DATASET_SIZE: Long = 1000000L\n\n  // Cross the `isValidInt` threshold\n  final val INVALID_INT_OFFSET: Long = Int.MaxValue.toLong - DATASET_SIZE / 2\n\n  // Cross the 32bit threshold\n  final val UNSIGNED_INT_OFFSET: Long = (1L << 32) - DATASET_SIZE / 2\n\n  private[delta] def bitmapAggColumn(\n      column: Column,\n      format: RoaringBitmapArrayFormat.Value): Column = {\n    val func = new BitmapAggregator(expression(column), format);\n    Column(func.toAggregateExpression(isDistinct = false))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/CatalogTableTestUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.types.StructType\n\nimport scala.jdk.CollectionConverters._\n\n/**\n * Helpers for constructing [[CatalogTable]] instances inside Java tests.\n *\n * Spark's [[CatalogTable]] is defined in Scala and its constructor signature shifts between Spark\n * releases. Centralising the construction in Scala keeps the kernel tests insulated from those\n * binary changes and saves Java tests from manually wiring the many optional parameters.\n */\nobject CatalogTableTestUtils {\n\n  /**\n   * Creates a [[CatalogTable]] with configurable options.\n   *\n   * @param tableName table name (default: \"tbl\")\n   * @param catalogName optional catalog name for the identifier\n   * @param properties table properties (default: empty)\n   * @param storageProperties storage properties (default: empty)\n   * @param locationUri optional storage location URI\n   * @param nullStorage if true, sets storage to null (for edge case testing)\n   * @param nullStorageProperties if true, sets storage properties to null\n   */\n  def createCatalogTable(\n      tableName: String = \"tbl\",\n      catalogName: Option[String] = None,\n      properties: java.util.Map[String, String] = new java.util.HashMap[String, String](),\n      storageProperties: java.util.Map[String, String] = new java.util.HashMap[String, String](),\n      locationUri: Option[java.net.URI] = None,\n      nullStorage: Boolean = false,\n      nullStorageProperties: Boolean = false): CatalogTable = {\n\n    val scalaProps = properties.asScala.toMap\n    val scalaStorageProps =\n      if (nullStorageProperties) null else storageProperties.asScala.toMap\n\n    val identifier = catalogName match {\n      case Some(catalog) =>\n        TableIdentifier(tableName, Some(\"default\"), Some(catalog))\n      case None => TableIdentifier(tableName)\n    }\n\n    val storage = if (nullStorage) {\n      null\n    } else {\n      CatalogStorageFormat(\n        locationUri = locationUri,\n        inputFormat = None,\n        outputFormat = None,\n        serde = None,\n        compressed = false,\n        properties = scalaStorageProps)\n    }\n\n    CatalogTable(\n      identifier = identifier,\n      tableType = CatalogTableType.MANAGED,\n      storage = storage,\n      schema = new StructType(),\n      provider = None,\n      partitionColumnNames = Seq.empty,\n      bucketSpec = None,\n      properties = scalaProps)\n  }\n}\n\n\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/CodecSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport java.nio.charset.StandardCharsets.US_ASCII\nimport java.util.UUID\n\nimport scala.util.Random\n\nimport org.apache.spark.SparkFunSuite\n\nclass CodecSuite extends SparkFunSuite {\n\n  import CodecSuite._\n\n  // Z85 reference strings are generated by https://cryptii.com/pipes/z85-encoder\n  val testUuids = Seq[(UUID, String)](\n    new UUID(0L, 0L) -> \"00000000000000000000\",\n    new UUID(Long.MinValue, Long.MinValue) -> \"Fb/MH00000Fb/MH00000\",\n    new UUID(-1L, -1L) -> \"%nSc0%nSc0%nSc0%nSc0\",\n    new UUID(0L, Long.MinValue) -> \"0000000000Fb/MH00000\",\n    new UUID(0L, -1L) -> \"0000000000%nSc0%nSc0\",\n    new UUID(0L, Long.MaxValue) -> \"0000000000Fb/MG%nSc0\",\n    new UUID(Long.MinValue, 0L) -> \"Fb/MH000000000000000\",\n    new UUID(-1L, 0L) -> \"%nSc0%nSc00000000000\",\n    new UUID(Long.MaxValue, 0L) -> \"Fb/MG%nSc00000000000\",\n    new UUID(0L, 1L) -> \"00000000000000000001\",\n    // Just a few random ones, using literals for test determinism\n    new UUID(-4124158004264678669L, -6032951921472435211L) -> \"-(5oirYA.yTvx6v@H:L>\",\n    new UUID(6453181356142382984L, 8208554093199893996L) -> \"s=Mlx-0Pp@AQ6uw@k6=D\",\n    new UUID(6453181356142382984L, -8208554093199893996L) -> \"s=Mlx-0Pp@JUL=R13LuL\",\n    new UUID(-4124158004264678669L, 8208554093199893996L) -> \"-(5oirYA.yAQ6uw@k6=D\")\n\n  // From https://rfc.zeromq.org/spec/32/ - Test Case\n  test(\"Z85 spec reference value\") {\n    val inputBytes: Array[Byte] =\n      Array(0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B).map(_.toByte)\n    val expectedEncodedString = \"HelloWorld\"\n    val actualEncodedString = Codec.Base85Codec.encodeBytes(inputBytes)\n    assert(actualEncodedString === expectedEncodedString)\n    val outputBytes = Codec.Base85Codec.decodeAlignedBytes(actualEncodedString)\n    assert(outputBytes sameElements inputBytes)\n  }\n\n  test(\"Z85 reference implementation values\") {\n    for ((id, expectedEncodedString) <- testUuids) {\n      val actualEncodedString = Codec.Base85Codec.encodeUUID(id)\n      assert(actualEncodedString === expectedEncodedString)\n    }\n  }\n\n  test(\"Z85 spec character map\") {\n    assert(Codec.Base85Codec.ENCODE_MAP.length === 85)\n    val referenceBytes = Seq(\n      0x00, 0x09, 0x98, 0x62, 0x0f, 0xc7, 0x99, 0x43, 0x1f, 0x85,\n      0x9a, 0x24, 0x2f, 0x43, 0x9b, 0x05, 0x3f, 0x01, 0x9b, 0xe6,\n      0x4e, 0xbf, 0x9c, 0xc7, 0x5e, 0x7d, 0x9d, 0xa8, 0x6e, 0x3b,\n      0x9e, 0x89, 0x7d, 0xf9, 0x9f, 0x6a, 0x8d, 0xb7, 0xa0, 0x4b,\n      0x9d, 0x75, 0xa1, 0x2c, 0xad, 0x33, 0xa2, 0x0d, 0xbc, 0xf1,\n      0xa2, 0xee, 0xcc, 0xaf, 0xa3, 0xcf, 0xdc, 0x6d, 0xa4, 0xb0,\n      0xec, 0x2b, 0xa5, 0x91, 0xfb, 0xe9, 0xa6, 0x72)\n      .map(_.toByte).toArray\n    val referenceString = new String(Codec.Base85Codec.ENCODE_MAP, US_ASCII)\n    val encodedString = Codec.Base85Codec.encodeBytes(referenceBytes)\n    assert(encodedString === referenceString)\n    val decodedBytes = Codec.Base85Codec.decodeAlignedBytes(encodedString)\n    assert(decodedBytes sameElements referenceBytes)\n  }\n\n  test(\"Reject illegal Z85 input - unaligned string\") {\n    // Minimum string should 5 characters\n    val illegalEncodedString = \"abc\"\n    assertThrows[IllegalArgumentException] {\n      Codec.Base85Codec.decodeBytes(\n        illegalEncodedString,\n        // This value is irrelevant, any value should cause the failure.\n        outputLength = 3)\n    }\n  }\n\n  // scalastyle:off nonascii\n  test(s\"Reject illegal Z85 input - illegal character\") {\n    for (char <- Seq[Char]('î', 'π', '\"', 0x7F)) {\n      val illegalEncodedString = String.valueOf(Array[Char]('a', 'b', char, 'd', 'e'))\n      val ex = intercept[IllegalArgumentException] {\n        Codec.Base85Codec.decodeAlignedBytes(illegalEncodedString)\n      }\n      assert(ex.getMessage.contains(\"Input is not valid Z85\"))\n    }\n  }\n  // scalastyle:on nonascii\n\n  test(\"base85 codec uuid roundtrips\") {\n    for ((id, _) <- testUuids) {\n      val encodedString = Codec.Base85Codec.encodeUUID(id)\n      // 16 bytes always get encoded into 20 bytes with Base85.\n      assert(encodedString.length === Codec.Base85Codec.ENCODED_UUID_LENGTH)\n      val decodedId = Codec.Base85Codec.decodeUUID(encodedString)\n      assert(id === decodedId, s\"encodedString = $encodedString\")\n    }\n  }\n\n  test(\"base85 codec empty byte array\") {\n    val empty = Array.empty[Byte]\n    val encodedString = Codec.Base85Codec.encodeBytes(empty)\n    assert(encodedString === \"\")\n    val decodedArray = Codec.Base85Codec.decodeAlignedBytes(encodedString)\n    assert(decodedArray.isEmpty)\n    val decodedArray2 = Codec.Base85Codec.decodeBytes(encodedString, 0)\n    assert(decodedArray2.isEmpty)\n  }\n\n  test(\"base85 codec byte array random roundtrips\") {\n    val rand = new Random(1L) // Fixed seed for determinism\n    val arrayLengths = (1 to 20) ++ Seq(32, 56, 64, 128, 1022, 11 * 1024 * 1024)\n\n    for (len <- arrayLengths) {\n      val inputArray: Array[Byte] = Array.ofDim(len)\n      rand.nextBytes(inputArray)\n      val encodedString = Codec.Base85Codec.encodeBytes(inputArray)\n      val decodedArray = Codec.Base85Codec.decodeBytes(encodedString, len)\n      assert(decodedArray === inputArray, s\"encodedString = $encodedString\")\n    }\n  }\n\n  /**\n   * Execute `thunk` works for strings containing any of the possible base85 characters at either\n   * beginning, middle, or end positions.\n   */\n  private def forAllEncodedStrings(thunk: String => Unit): Unit = {\n    // Basically test that every possible character can occur at any\n    // position with a 20 character string.\n    val characterString = new String(Codec.Base85Codec.ENCODE_MAP, US_ASCII)\n    // Use this to fill in the remaining 17 characters.\n    val fillerChar = \"x\"\n\n    var count = 0\n    for {\n      firstChar <- characterString\n      middleChar <- characterString\n      finalChar <- characterString\n    } {\n      val sb = new StringBuilder\n      sb += firstChar\n      sb ++= fillerChar * 9\n      sb += middleChar\n      sb ++= fillerChar * 8\n      sb += finalChar\n      val encodedString = sb.toString()\n      assert(encodedString.length === 20)\n      thunk(encodedString)\n      count += 1\n    }\n    assert(count === 85 * 85 * 85)\n  }\n\n  test(\"base85 character set is JSON-safe\") {\n    forAllEncodedStrings { inputString =>\n      val inputObject = JsonRoundTripContainer(inputString)\n      val jsonString = JsonUtils.toJson(inputObject)\n      assert(jsonString.contains(inputString),\n        \"Some character from the input had to be escaped to be JSON-safe:\" +\n          s\"input = '$inputString' vs JSON = '$jsonString'\")\n      val outputObject = JsonUtils.fromJson[JsonRoundTripContainer](jsonString)\n      val outputString = outputObject.data\n      assert(inputString === outputString)\n    }\n  }\n\n}\n\nobject CodecSuite {\n  final case class JsonRoundTripContainer(data: String)\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/DatasetRefCacheSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.{QueryTest, SparkSession}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DatasetRefCacheSuite extends QueryTest with SharedSparkSession {\n\n  test(\"should create a new Dataset when the active session is changed\") {\n    val cache = new DatasetRefCache(() => spark.range(1, 10) )\n    val ref = cache.get\n    // Should reuse `Dataset` when the active session is the same\n    assert(ref eq cache.get)\n    SparkSession.setActiveSession(spark.newSession())\n    // Should create a new `Dataset` when the active session is changed\n    assert(ref ne cache.get)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/DeltaLogGroupingIteratorSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport org.apache.spark.sql.delta.SerializableFileStatus\n\nimport org.apache.spark.SparkFunSuite\n\nclass DeltaLogGroupingIteratorSuite extends SparkFunSuite {\n\n  test(\"DeltaLogGroupingIterator\") {\n    val paths = Seq(\n      // both checkpoint and commit file present for v1\n      \"file://a/b/_delta_log/1.checkpoint.parquet\",\n      \"file://a/b/_delta_log/1.json\",\n\n      // only json file present for v2\n      \"file://a/b/_delta_log/2.json\",\n      // v3 missing\n\n      // multiple types of checkpoint present for v4\n      \"file://a/b/_delta_log/4.checkpoint.parquet\",\n      \"file://a/b/_delta_log/4.checkpoint.uuid.parquet\",\n      \"file://a/b/_delta_log/4.checkpoint.json.parquet\",\n      \"file://a/b/_delta_log/4.checkpoint.0.1.parquet\",\n      \"file://a/b/_delta_log/4.checkpoint.1.1.parquet\",\n      \"file://a/b/_delta_log/4.json\",\n      // v5, v6 with single checkpoint file\n      \"file://a/b/_delta_log/5.checkpoint.parquet\",\n      \"file://a/b/_delta_log/5.json\",\n      \"file://a/b/_delta_log/6.checkpoint.parquet\",\n      // no checkpoint files in the end\n      \"file://a/b/_delta_log/6.json\",\n      \"file://a/b/_delta_log/7.json\",\n      \"file://a/b/_delta_log/8.json\",\n      \"file://a/b/_delta_log/9.json\",\n      \"file://a/b/_delta_log/11.checkpoint.0.1.parquet\",\n      \"file://a/b/_delta_log/11.checkpoint.1.1.parquet\",\n      \"file://a/b/_delta_log/11.checkpoint.uuid.parquet\",\n      \"file://a/b/_delta_log/12.checkpoint.parquet\",\n      \"file://a/b/_delta_log/14.json\"\n    )\n    val fileStatuses = paths.map { path =>\n      SerializableFileStatus(path, length = 10, isDir = false, modificationTime = 1).toFileStatus\n    }.toIterator\n    val groupedFileStatuses = new DeltaLogGroupingIterator(fileStatuses)\n    val groupedPaths = groupedFileStatuses.toIndexedSeq.map { case (version, files) =>\n      (version, files.map(_.getPath.toString).toList)\n    }\n    assert(groupedPaths === Seq(\n      1 -> List(\"file://a/b/_delta_log/1.checkpoint.parquet\", \"file://a/b/_delta_log/1.json\"),\n      2 -> List(\"file://a/b/_delta_log/2.json\"),\n      4 -> List(\n        \"file://a/b/_delta_log/4.checkpoint.parquet\",\n        \"file://a/b/_delta_log/4.checkpoint.uuid.parquet\",\n        \"file://a/b/_delta_log/4.checkpoint.json.parquet\",\n        \"file://a/b/_delta_log/4.checkpoint.0.1.parquet\",\n        \"file://a/b/_delta_log/4.checkpoint.1.1.parquet\",\n        \"file://a/b/_delta_log/4.json\"),\n      5 -> List(\"file://a/b/_delta_log/5.checkpoint.parquet\", \"file://a/b/_delta_log/5.json\"),\n      6 -> List(\"file://a/b/_delta_log/6.checkpoint.parquet\", \"file://a/b/_delta_log/6.json\"),\n      7 -> List(\"file://a/b/_delta_log/7.json\"),\n      8 -> List(\"file://a/b/_delta_log/8.json\"),\n      9 -> List(\"file://a/b/_delta_log/9.json\"),\n      11 -> List(\n        \"file://a/b/_delta_log/11.checkpoint.0.1.parquet\",\n        \"file://a/b/_delta_log/11.checkpoint.1.1.parquet\",\n        \"file://a/b/_delta_log/11.checkpoint.uuid.parquet\"),\n      12 -> List(\"file://a/b/_delta_log/12.checkpoint.parquet\"),\n      14 -> List(\"file://a/b/_delta_log/14.json\")\n    ))\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/JsonUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util\n\nimport scala.util.Random\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.actions.CommitInfo\nimport org.apache.spark.sql.delta.stats.DataSize\nimport com.fasterxml.jackson.core.StreamReadConstraints\n\nimport org.apache.spark.sql.QueryTest\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass JsonUtilsSuite\n  extends QueryTest\n  with SharedSparkSession {\n\n  test(\"DataSize json serialization\") {\n    val testCases = Seq(\n      DataSize() -> \"\"\"{}\"\"\",\n      DataSize(bytesCompressed = Some(816L)) -> \"\"\"{\"bytesCompressed\":816}\"\"\",\n      DataSize(rows = Some(111L)) -> \"\"\"{\"rows\":111}\"\"\",\n      DataSize(rows = Some(0)) -> \"\"\"{\"rows\":0}\"\"\",\n      DataSize(logicalRows = Some(111L)) -> \"\"\"{\"logicalRows\":111}\"\"\",\n      DataSize(logicalRows = Some(-1L)) -> \"\"\"{\"logicalRows\":-1}\"\"\",\n      DataSize(bytesCompressed = Some(816L), rows = Some(111L), logicalRows = Some(111L)) ->\n        \"\"\"{\"bytesCompressed\":816,\"rows\":111,\"logicalRows\":111}\"\"\"\n    )\n    for ((obj, json) <- testCases) {\n      assert(JsonUtils.toJson(obj) == json)\n      assert(JsonUtils.fromJson[DataSize](json) == obj)\n    }\n  }\n\n  test(\"Serialize and de-serialize commit info with large message\") {\n    val operationStringSize = StreamReadConstraints.DEFAULT_MAX_STRING_LEN * 10\n    assert(operationStringSize > StreamReadConstraints.DEFAULT_MAX_STRING_LEN)\n\n    val operation = Random.alphanumeric.take(operationStringSize).toString()\n    val commitInfo = CommitInfo(\n      time = System.currentTimeMillis(),\n      operation,\n      operationParameters = Map.empty,\n      commandContext = Map.empty,\n      readVersion = Some(1),\n      isolationLevel = None,\n      isBlindAppend = Some(false),\n      operationMetrics = None,\n      userMetadata = Some(\"I am a test and not a user\"),\n      tags = None,\n      txnId = Some(\"Transaction with a veryyyyyyy large commit info\")\n    )\n\n    val serialized = JsonUtils.toJson(commitInfo)\n    val deserialized = JsonUtils.fromJson[CommitInfo](serialized)\n\n    assert(commitInfo === deserialized)\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/threads/DeltaThreadPoolSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util.threads\n\nimport java.util.Properties\n\nimport org.apache.spark.{SparkFunSuite, TaskContext, TaskContextImpl}\nimport org.apache.spark.sql.test.SharedSparkSession\n\nclass DeltaThreadPoolSuite extends SparkFunSuite with SharedSparkSession {\n\n  val threadPool: DeltaThreadPool = DeltaThreadPool(\"test\", 1)\n\n  def makeTaskContext(id: Int): TaskContext = {\n    new TaskContextImpl(id, 0, 0, 0, attemptNumber = 45613, 0, null, new Properties(), null)\n  }\n\n  def testForwarding(testName: String, id: Int)(f: => Unit): Unit = {\n    test(testName) {\n      val prevTaskContext = TaskContext.get()\n      TaskContext.setTaskContext(makeTaskContext(id))\n      sparkContext.setLocalProperty(\"test\", id.toString)\n\n      try {\n        f\n      } finally {\n        TaskContext.setTaskContext(prevTaskContext)\n      }\n    }\n  }\n\n  def assertTaskAndProperties(id: Int): Unit = {\n    assert(TaskContext.get() !== null)\n    assert(TaskContext.get().stageId() === id)\n    assert(sparkContext.getLocalProperty(\"test\") === id.toString)\n  }\n\n  testForwarding(\"parallelMap captures TaskContext\", id = 0) {\n    threadPool.parallelMap(spark, 0 until 1) { _ =>\n      assertTaskAndProperties(id = 0)\n    }\n  }\n\n  testForwarding(\"submit captures TaskContext and local properties\", id = 1) {\n    threadPool.submit(spark) {\n      assertTaskAndProperties(id = 1)\n    }\n  }\n\n  testForwarding(\"submitNonFateSharing captures TaskContext and local properties\", id = 2) {\n    threadPool.submitNonFateSharing { _ =>\n      assertTaskAndProperties(id = 2)\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala/org/apache/spark/sql/delta/util/threads/SparkThreadLocalForwardingSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.util.threads\n\nimport java.util.Properties\nimport java.util.concurrent.{LinkedBlockingQueue, ThreadPoolExecutor, TimeUnit}\n\nimport scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}\nimport scala.concurrent.duration._\n\nimport org.apache.spark._\nimport org.apache.spark.util.ThreadUtils\nimport org.apache.spark.util.ThreadUtils.namedThreadFactory\n\nclass SparkThreadLocalForwardingSuite extends SparkFunSuite {\n\n  private def createThreadPool(nThreads: Int, prefix: String): ThreadPoolExecutor = {\n    val threadFactory = namedThreadFactory(prefix)\n    val keepAliveTimeSeconds = 60\n    val threadPool = new SparkThreadLocalForwardingThreadPoolExecutor(\n      nThreads,\n      nThreads,\n      keepAliveTimeSeconds,\n      TimeUnit.MILLISECONDS,\n      new LinkedBlockingQueue[Runnable],\n      threadFactory)\n    threadPool.allowCoreThreadTimeOut(true)\n    threadPool\n  }\n\n  test(\"SparkThreadLocalForwardingThreadPoolExecutor properly propagates\" +\n      \" TaskContext and Spark Local Properties\") {\n    val sc = SparkContext.getOrCreate(new SparkConf().setAppName(\"test\").setMaster(\"local\"))\n    val executor = createThreadPool(1, \"test-threads\")\n    implicit val executionContext: ExecutionContextExecutor =\n      ExecutionContext.fromExecutor(executor)\n\n    val prevTaskContext = TaskContext.get()\n    try {\n      // assert that each instance of submitting a task to the execution context captures the\n      // current task context\n      val futures = (1 to 10) map { i =>\n        setTaskAndProperties(i, sc)\n\n        Future {\n          checkTaskAndProperties(i, sc)\n        }(executionContext)\n      }\n\n      assert(ThreadUtils.awaitResult(Future.sequence(futures), 10.seconds).forall(identity))\n    } finally {\n      ThreadUtils.shutdown(executor)\n      TaskContext.setTaskContext(prevTaskContext)\n      sc.stop()\n    }\n  }\n\n  def makeTaskContext(id: Int): TaskContext = {\n    new TaskContextImpl(id, 0, 0, 0, attemptNumber = 45613, 0, null, new Properties(), null)\n  }\n\n  def setTaskAndProperties(i: Int, sc: SparkContext = SparkContext.getActive.get): Unit = {\n    val tc = makeTaskContext(i)\n    TaskContext.setTaskContext(tc)\n    sc.setLocalProperty(\"test\", i.toString)\n  }\n\n  def checkTaskAndProperties(i: Int, sc: SparkContext = SparkContext.getActive.get): Boolean = {\n    TaskContext.get() != null &&\n      TaskContext.get().stageId() == i &&\n      sc.getLocalProperty(\"test\") == i.toString\n  }\n\n  test(\"That CapturedSparkThreadLocals properly restores the existing state\") {\n    val sc = SparkContext.getOrCreate(new SparkConf().setAppName(\"test\").setMaster(\"local\"))\n    val prevTaskContext = TaskContext.get()\n    try {\n      setTaskAndProperties(10)\n      val capturedSparkThreadLocals = CapturedSparkThreadLocals()\n      setTaskAndProperties(11)\n      assert(!checkTaskAndProperties(10, sc))\n      assert(checkTaskAndProperties(11, sc))\n      capturedSparkThreadLocals.runWithCaptured {\n        assert(checkTaskAndProperties(10, sc))\n      }\n      assert(checkTaskAndProperties(11, sc))\n    } finally {\n      TaskContext.setTaskContext(prevTaskContext)\n      sc.stop()\n    }\n  }\n\n  test(\"That CapturedSparkThreadLocals properly restores the existing spark properties.\" +\n    \" Changes to local properties inside a task do not affect the original properties\") {\n    val sc = SparkContext.getOrCreate(new SparkConf().setAppName(\"test\").setMaster(\"local\"))\n    try {\n      sc.setLocalProperty(\"TestProp\", \"1\")\n      val capturedSparkThreadLocals = CapturedSparkThreadLocals()\n      assert(sc.getLocalProperty(\"TestProp\") == \"1\")\n      capturedSparkThreadLocals.runWithCaptured {\n        sc.setLocalProperty(\"TestProp\", \"2\")\n        assert(sc.getLocalProperty(\"TestProp\") == \"2\")\n      }\n      assert(sc.getLocalProperty(\"TestProp\") == \"1\")\n    } finally {\n      sc.stop()\n    }\n  }\n\n\n  test(\"captured spark thread locals are immutable\") {\n    val sc = SparkContext.getOrCreate(new SparkConf().setAppName(\"test\").setMaster(\"local\"))\n    try {\n      sc.setLocalProperty(\"test1\", \"good\")\n      sc.setLocalProperty(\"test2\", \"good\")\n      val threadLocals = CapturedSparkThreadLocals()\n      sc.setLocalProperty(\"test2\", \"bad\")\n      assert(sc.getLocalProperty(\"test1\") == \"good\")\n      assert(sc.getLocalProperty(\"test2\") == \"bad\")\n      threadLocals.runWithCaptured {\n        assert(sc.getLocalProperty(\"test1\") == \"good\")\n        assert(sc.getLocalProperty(\"test2\") == \"good\")\n        sc.setLocalProperty(\"test1\", \"bad\")\n        sc.setLocalProperty(\"test2\", \"maybe\")\n        assert(sc.getLocalProperty(\"test1\") == \"bad\")\n        assert(sc.getLocalProperty(\"test2\") == \"maybe\")\n      }\n      assert(sc.getLocalProperty(\"test1\") == \"good\")\n      assert(sc.getLocalProperty(\"test2\") == \"bad\")\n      threadLocals.runWithCaptured {\n        assert(sc.getLocalProperty(\"test1\") == \"good\")\n        assert(sc.getLocalProperty(\"test2\") == \"good\")\n      }\n    } finally {\n      sc.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.0/GridTestShim.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\nimport org.scalatest.Tag\nimport org.apache.spark.SparkFunSuite\n\n/**\n * Shim for SparkFunSuite as gridTest doesn't exist in Spark 4.0 but we rely on it\n * in tests.\n */\ntrait GridTestShim { self: SparkFunSuite =>\n  def gridTest[A](testNamePrefix: String, testTags: Tag*)(params: Seq[A])(\n    testFun: A => Unit): Unit = {\n    for (param <- params) {\n      test(testNamePrefix + s\" ($param)\", testTags: _*)(testFun(param))\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.0/InvalidDefaultValueErrorShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for INVALID_DEFAULT_VALUE error codes that changed between Spark versions.\n * In Spark 4.0, the error code is INVALID_DEFAULT_VALUE.NOT_CONSTANT\n */\nobject InvalidDefaultValueErrorShims {\n  val INVALID_DEFAULT_VALUE_ERROR_CODE: String = \"INVALID_DEFAULT_VALUE.NOT_CONSTANT\"\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.0/StreamingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shims for streaming classes that were relocated in Spark 4.1.\n * In Spark 4.0, these classes are in their original locations.\n */\nobject StreamingTestShims {\n  // MemoryStream\n  type MemoryStream[T] = org.apache.spark.sql.execution.streaming.MemoryStream[T]\n  val MemoryStream: org.apache.spark.sql.execution.streaming.MemoryStream.type =\n    org.apache.spark.sql.execution.streaming.MemoryStream\n\n  // MicroBatchExecution (class only, no companion object)\n  type MicroBatchExecution = org.apache.spark.sql.execution.streaming.MicroBatchExecution\n\n  // StreamingQueryWrapper (class only, no companion object)\n  type StreamingQueryWrapper = org.apache.spark.sql.execution.streaming.StreamingQueryWrapper\n\n  // StreamingExecutionRelation (class only, no companion object)\n  type StreamingExecutionRelation =\n    org.apache.spark.sql.execution.streaming.StreamingExecutionRelation\n\n  // OffsetSeqLog (class only, no companion object)\n  type OffsetSeqLog = org.apache.spark.sql.execution.streaming.OffsetSeqLog\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.0/UnsupportedTableOperationErrorShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for UNSUPPORTED_FEATURE.TABLE_OPERATION error codes that changed between Spark\n * versions. In Spark 4.0, the error code is _LEGACY_ERROR_TEMP_2096\n */\nobject UnsupportedTableOperationErrorShims {\n  val UNSUPPORTED_TABLE_OPERATION_ERROR_CODE: String = \"_LEGACY_ERROR_TEMP_2096\"\n\n  /**\n   * Returns the parameters map for UPDATE TABLE error in Spark 4.0\n   * @param tableSQLIdentifier Ignored in Spark 4.0, kept for API compatibility\n   */\n  def updateTableErrorParameters(tableSQLIdentifier: String = \"\"): Map[String, String] = {\n    Map(\"ddl\" -> \"UPDATE TABLE\")\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.0/VariantShreddingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for variant shredding to handle differences between Spark versions.\n * In Spark 4.0, VARIANT_INFER_SHREDDING_SCHEMA does not exist.\n */\nobject VariantShreddingTestShims {\n  /**\n   * Returns true if VARIANT_INFER_SHREDDING_SCHEMA config is supported in this Spark version.\n   * In Spark 4.0, this returns false.\n   */\n  val variantInferShreddingSchemaSupported: Boolean = false\n\n  /**\n   * Returns a dummy config key for VARIANT_INFER_SHREDDING_SCHEMA.\n   * In Spark 4.0, since this config doesn't exist, we return a dummy key that won't affect tests.\n   * This allows tests to compile but the config will have no effect.\n   */\n  val variantInferShreddingSchemaKey: String = \"spark.sql.dummy.variantInferShreddingSchema\"\n}\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.1/GridTestShim.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\nimport org.apache.spark.SparkFunSuite\n\n/**\n * Shim for SparkFunSuite as gridTest doesn't exist in Spark 4.0 but we rely on it\n * in tests. In Spark 4.1 it exists so we don't need to do anything.\n */\ntrait GridTestShim { self: SparkFunSuite =>\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.1/InvalidDefaultValueErrorShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for INVALID_DEFAULT_VALUE error codes that changed between Spark versions.\n * In Spark 4.1, the error code is INVALID_DEFAULT_VALUE.UNRESOLVED_EXPRESSION\n */\nobject InvalidDefaultValueErrorShims {\n  val INVALID_DEFAULT_VALUE_ERROR_CODE: String = \"INVALID_DEFAULT_VALUE.UNRESOLVED_EXPRESSION\"\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.1/StreamingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shims for streaming classes that were relocated in Spark 4.1.\n * In Spark 4.1, these classes moved to new package locations.\n */\nobject StreamingTestShims {\n  // MemoryStream - moved to runtime package\n  type MemoryStream[T] = org.apache.spark.sql.execution.streaming.runtime.MemoryStream[T]\n  val MemoryStream: org.apache.spark.sql.execution.streaming.runtime.MemoryStream.type =\n    org.apache.spark.sql.execution.streaming.runtime.MemoryStream\n\n  // MicroBatchExecution - moved to runtime package (class only, no companion object)\n  type MicroBatchExecution = org.apache.spark.sql.execution.streaming.runtime.MicroBatchExecution\n\n  // StreamingQueryWrapper - moved to runtime package (class only, no companion object)\n  type StreamingQueryWrapper =\n    org.apache.spark.sql.execution.streaming.runtime.StreamingQueryWrapper\n\n  // StreamingExecutionRelation - moved to runtime package (class only, no companion object)\n  type StreamingExecutionRelation =\n    org.apache.spark.sql.execution.streaming.runtime.StreamingExecutionRelation\n\n  // OffsetSeqLog - moved to checkpointing package (class only, no companion object)\n  type OffsetSeqLog = org.apache.spark.sql.execution.streaming.checkpointing.OffsetSeqLog\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.1/UnsupportedTableOperationErrorShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for UNSUPPORTED_FEATURE.TABLE_OPERATION error codes that changed between\n * Spark versions. In Spark 4.1, the error code is UNSUPPORTED_FEATURE.TABLE_OPERATION\n */\nobject UnsupportedTableOperationErrorShims {\n  val UNSUPPORTED_TABLE_OPERATION_ERROR_CODE: String = \"UNSUPPORTED_FEATURE.TABLE_OPERATION\"\n\n  /**\n   * Returns the parameters map for UPDATE TABLE error in Spark 4.1\n   * @param tableSQLIdentifier The table identifier (e.g., \"test_delta_table\")\n   */\n  def updateTableErrorParameters(tableSQLIdentifier: String): Map[String, String] = {\n    // Construct the full table name with catalog prefix\n    val fullTableName = s\"`spark_catalog`.`default`.`$tableSQLIdentifier`\"\n    Map(\n      \"tableName\" -> fullTableName,\n      \"operation\" -> \"UPDATE TABLE\")\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.1/VariantShreddingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Test shim for variant shredding to handle differences between Spark versions.\n * In Spark 4.1, VARIANT_INFER_SHREDDING_SCHEMA exists.\n */\nobject VariantShreddingTestShims {\n  /**\n   * Returns true if VARIANT_INFER_SHREDDING_SCHEMA config is supported in this Spark version.\n   * In Spark 4.1, this returns true.\n   */\n  val variantInferShreddingSchemaSupported: Boolean = true\n\n  /**\n   * Returns the config key for VARIANT_INFER_SHREDDING_SCHEMA.\n   * In Spark 4.1, this returns the actual SQLConf key.\n   */\n  val variantInferShreddingSchemaKey: String = SQLConf.VARIANT_INFER_SHREDDING_SCHEMA.key\n}\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.2/GridTestShim.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\nimport org.apache.spark.SparkFunSuite\n\n/**\n * Shim for SparkFunSuite as gridTest doesn't exist in Spark 4.0 but we rely on it\n * in tests. In Spark 4.2 it exists (same as 4.1) so we don't need to do anything.\n */\ntrait GridTestShim { self: SparkFunSuite =>\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.2/InvalidDefaultValueErrorShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for INVALID_DEFAULT_VALUE error codes that changed between Spark versions.\n * In Spark 4.2, the error code is INVALID_DEFAULT_VALUE.NOT_CONSTANT\n */\nobject InvalidDefaultValueErrorShims {\n  val INVALID_DEFAULT_VALUE_ERROR_CODE: String = \"INVALID_DEFAULT_VALUE.NOT_CONSTANT\"\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.2/StreamingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shims for streaming classes that were relocated in Spark 4.1+.\n * In Spark 4.2, these classes remain in the same package locations as Spark 4.1.\n */\nobject StreamingTestShims {\n  // MemoryStream - moved to runtime package\n  type MemoryStream[T] = org.apache.spark.sql.execution.streaming.runtime.MemoryStream[T]\n  val MemoryStream: org.apache.spark.sql.execution.streaming.runtime.MemoryStream.type =\n    org.apache.spark.sql.execution.streaming.runtime.MemoryStream\n\n  // MicroBatchExecution - moved to runtime package (class only, no companion object)\n  type MicroBatchExecution = org.apache.spark.sql.execution.streaming.runtime.MicroBatchExecution\n\n  // StreamingQueryWrapper - moved to runtime package (class only, no companion object)\n  type StreamingQueryWrapper =\n    org.apache.spark.sql.execution.streaming.runtime.StreamingQueryWrapper\n\n  // StreamingExecutionRelation - moved to runtime package (class only, no companion object)\n  type StreamingExecutionRelation =\n    org.apache.spark.sql.execution.streaming.runtime.StreamingExecutionRelation\n\n  // OffsetSeqLog - moved to checkpointing package (class only, no companion object)\n  type OffsetSeqLog = org.apache.spark.sql.execution.streaming.checkpointing.OffsetSeqLog\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.2/UnsupportedTableOperationErrorShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\n/**\n * Test shim for UNSUPPORTED_FEATURE.TABLE_OPERATION error codes that changed between\n * Spark versions. In Spark 4.2, the error code is UNSUPPORTED_FEATURE.TABLE_OPERATION\n * (same as Spark 4.1)\n */\nobject UnsupportedTableOperationErrorShims {\n  val UNSUPPORTED_TABLE_OPERATION_ERROR_CODE: String = \"UNSUPPORTED_FEATURE.TABLE_OPERATION\"\n\n  /**\n   * Returns the parameters map for UPDATE TABLE error in Spark 4.2 (same as Spark 4.1)\n   * @param tableSQLIdentifier The table identifier (e.g., \"test_delta_table\")\n   */\n  def updateTableErrorParameters(tableSQLIdentifier: String): Map[String, String] = {\n    // Construct the full table name with catalog prefix\n    val fullTableName = s\"`spark_catalog`.`default`.`$tableSQLIdentifier`\"\n    Map(\n      \"tableName\" -> fullTableName,\n      \"operation\" -> \"UPDATE TABLE\")\n  }\n}\n\n"
  },
  {
    "path": "spark/src/test/scala-shims/spark-4.2/VariantShreddingTestShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test.shims\n\nimport org.apache.spark.sql.internal.SQLConf\n\n/**\n * Test shim for variant shredding to handle differences between Spark versions.\n * In Spark 4.2, VARIANT_INFER_SHREDDING_SCHEMA exists.\n */\nobject VariantShreddingTestShims {\n  /**\n   * Returns true if VARIANT_INFER_SHREDDING_SCHEMA config is supported in this Spark version.\n   * In Spark 4.2, this returns true.\n   */\n  val variantInferShreddingSchemaSupported: Boolean = true\n\n  /**\n   * Returns the config key for VARIANT_INFER_SHREDDING_SCHEMA.\n   * In Spark 4.2, this returns the actual SQLConf key.\n   */\n  val variantInferShreddingSchemaKey: String = SQLConf.VARIANT_INFER_SHREDDING_SCHEMA.key\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/S3CredentialFileSystem.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FSDataInputStream;\nimport org.apache.hadoop.fs.FSDataOutputStream;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.hadoop.fs.RawLocalFileSystem;\nimport org.apache.hadoop.fs.permission.FsPermission;\nimport org.apache.hadoop.util.Progressable;\nimport software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;\nimport software.amazon.awssdk.auth.credentials.AwsSessionCredentials;\n\n/**\n * Fake S3 filesystem backed by local disk for integration testing. Maps {@code s3://bucket/path} to\n * local paths while verifying UC-vended credentials are correctly propagated.\n */\npublic class S3CredentialFileSystem extends RawLocalFileSystem {\n\n  private static final String SCHEME = \"s3:\";\n\n  // Same as org.apache.hadoop.fs.s3a.Constants#AWS_CREDENTIALS_PROVIDER.\n  private static final String S3A_CREDENTIALS_PROVIDER = \"fs.s3a.aws.credentials.provider\";\n\n  /** Set to {@code false} to skip credential assertions (useful for debugging). */\n  public static boolean credentialCheckEnabled = true;\n\n  private AwsCredentialsProvider provider;\n\n  @Override\n  protected void checkPath(Path path) {\n    // Accept any path without validation.\n  }\n\n  @Override\n  public FSDataOutputStream create(\n      Path f,\n      boolean overwrite,\n      int bufferSize,\n      short replication,\n      long blockSize,\n      Progressable progress)\n      throws IOException {\n    return super.create(toLocalPath(f), overwrite, bufferSize, replication, blockSize, progress);\n  }\n\n  @Override\n  public FileStatus getFileStatus(Path f) throws IOException {\n    if (!f.toString().startsWith(SCHEME)) return super.getFileStatus(f);\n    return restoreS3Path(f, super.getFileStatus(toLocalPath(f)));\n  }\n\n  @Override\n  public FSDataInputStream open(Path f) throws IOException {\n    return super.open(toLocalPath(f));\n  }\n\n  @Override\n  public FileStatus[] listStatus(Path f) throws IOException {\n    FileStatus[] files;\n    try {\n      files = super.listStatus(toLocalPath(f));\n    } catch (FileNotFoundException e) {\n      return new FileStatus[0]; // S3 returns empty for non-existent prefixes\n    }\n    FileStatus[] result = new FileStatus[files.length];\n    for (int i = 0; i < files.length; i++) {\n      result[i] = restoreS3Path(f, files[i]);\n    }\n    return result;\n  }\n\n  @Override\n  public boolean mkdirs(Path f, FsPermission permission) throws IOException {\n    return super.mkdirs(toLocalPath(f), permission);\n  }\n\n  @Override\n  public boolean rename(Path src, Path dst) throws IOException {\n    return super.rename(toLocalPath(src), toLocalPath(dst));\n  }\n\n  @Override\n  public boolean delete(Path f, boolean recursive) throws IOException {\n    return super.delete(toLocalPath(f), recursive);\n  }\n\n  /** Converts {@code s3://bucket/path} to local path, verifying credentials on the way. */\n  private Path toLocalPath(Path f) {\n    checkCredentials(f);\n    return new Path(f.toString().replaceAll(SCHEME + \"//.*?/\", \"file:///\"));\n  }\n\n  /** Replaces the file: scheme in a FileStatus with the original S3 prefix. */\n  private FileStatus restoreS3Path(Path originalS3Path, FileStatus status) {\n    String s3Prefix = SCHEME + \"//\" + originalS3Path.toUri().getHost();\n    String restored = status.getPath().toString().replace(\"file:\", s3Prefix);\n    return new FileStatus(\n        status.getLen(),\n        status.isDirectory(),\n        status.getReplication(),\n        status.getBlockSize(),\n        status.getModificationTime(),\n        new Path(restored));\n  }\n\n  private void checkCredentials(Path f) {\n    if (!credentialCheckEnabled) return;\n    String bucket = f.toUri().getHost();\n    assertThat(bucket).isEqualTo(UnityCatalogSupport.FAKE_S3_BUCKET);\n    assertCredentials();\n  }\n\n  /** Verifies UC-vended credentials via AwsCredentialsProvider or static Hadoop properties. */\n  private void assertCredentials() {\n    Configuration conf = getConf();\n    AwsCredentialsProvider p = resolveProvider(conf);\n    if (p != null) {\n      AwsSessionCredentials creds = (AwsSessionCredentials) p.resolveCredentials();\n      assertThat(creds.accessKeyId()).isEqualTo(\"fakeAccessKey\");\n      assertThat(creds.secretAccessKey()).isEqualTo(\"fakeSecretKey\");\n      assertThat(creds.sessionToken()).isEqualTo(\"fakeSessionToken\");\n    } else {\n      assertThat(conf.get(\"fs.s3a.access.key\")).isEqualTo(\"fakeAccessKey\");\n      assertThat(conf.get(\"fs.s3a.secret.key\")).isEqualTo(\"fakeSecretKey\");\n      assertThat(conf.get(\"fs.s3a.session.token\")).isEqualTo(\"fakeSessionToken\");\n    }\n  }\n\n  private synchronized AwsCredentialsProvider resolveProvider(Configuration conf) {\n    if (provider != null) return provider;\n    String clazz = conf.get(S3A_CREDENTIALS_PROVIDER);\n    if (clazz == null) return null;\n    try {\n      provider =\n          (AwsCredentialsProvider)\n              Class.forName(clazz).getConstructor(Configuration.class).newInstance(conf);\n    } catch (Exception e) {\n      throw new RuntimeException(\"Failed to instantiate credential provider: \" + clazz, e);\n    }\n    return provider;\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaStreamingTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.unitycatalog.client.ApiClient;\nimport io.unitycatalog.client.ApiException;\nimport io.unitycatalog.client.api.DeltaCommitsApi;\nimport io.unitycatalog.client.api.TablesApi;\nimport io.unitycatalog.client.model.DeltaGetCommits;\nimport io.unitycatalog.client.model.DeltaGetCommitsResponse;\nimport io.unitycatalog.client.model.TableInfo;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.file.Files;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport java.util.stream.LongStream;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Encoders;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.RowFactory;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.delta.test.shims.StreamingTestShims;\nimport org.apache.spark.sql.streaming.StreamingQuery;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.Metadata;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport scala.collection.JavaConverters;\nimport scala.collection.immutable.Seq;\n\n/**\n * Streaming test suite for Delta Lake operations through Unity Catalog.\n *\n * <p>Tests structured streaming write and read operations with Delta format tables managed by Unity\n * Catalog.\n */\npublic class UCDeltaStreamingTest extends UCDeltaTableIntegrationBaseTest {\n\n  /**\n   * Creates a local temporary directory for checkpoint location. Checkpoint must be on local\n   * filesystem since Spark doesn't have direct cloud storage credentials (credentials are managed\n   * by UC server for catalog-managed tables only).\n   */\n  public String createTempCheckpointDir() {\n    try {\n      return Files.createTempDirectory(\"spark-checkpoint-\").toFile().getAbsolutePath();\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  @TestAllTableTypes\n  public void testStreamingWriteToManagedTable(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_write_test\",\n        \"id BIGINT, value STRING\",\n        tableType,\n        (tableName) -> {\n          // Define schema for the stream\n          StructType schema =\n              new StructType(\n                  new StructField[] {\n                    new StructField(\"id\", DataTypes.LongType, false, Metadata.empty()),\n                    new StructField(\"value\", DataTypes.StringType, false, Metadata.empty())\n                  });\n\n          // Create MemoryStream - using Scala companion object with proper encoder via shims\n          var memoryStream =\n              StreamingTestShims.MemoryStream().apply(Encoders.row(schema), spark().sqlContext());\n\n          // Start streaming query writing to the Unity Catalog managed table\n          StreamingQuery query =\n              memoryStream\n                  .toDF()\n                  .writeStream()\n                  .format(\"delta\")\n                  .outputMode(\"append\")\n                  .option(\"checkpointLocation\", createTempCheckpointDir())\n                  .toTable(tableName);\n\n          // Assert that the query is active\n          assertTrue(query.isActive(), \"Streaming query should be active\");\n\n          // Let's do 3 rounds testing, and for every round, adding 1 row and waiting to be\n          // available, and finally verify the results and unity catalog latest version are\n          // expected.\n          ApiClient client = unityCatalogInfo().createApiClient();\n          for (long i = 1; i <= 3; i += 1) {\n            Seq<Row> batchRow = createRowsAsSeq(RowFactory.create(i, String.valueOf(i)));\n            memoryStream.addData(batchRow);\n\n            // Process all available data\n            query.processAllAvailable();\n\n            // Verify the content\n            check(\n                tableName,\n                LongStream.range(1, i + 1)\n                    .mapToObj(idx -> List.of(String.valueOf(idx), String.valueOf(idx)))\n                    .collect(Collectors.toUnmodifiableList()));\n\n            // The UC server should have the latest version, for managed table.\n            if (TableType.MANAGED == tableType) {\n              assertUCManagedTableVersion(i, tableName, client);\n            }\n          }\n\n          // Stop the stream.\n          query.stop();\n          query.awaitTermination();\n\n          // Assert that the query has stopped\n          assertFalse(query.isActive(), \"Streaming query should have stopped\");\n        });\n  }\n\n  @TestAllTableTypes\n  public void testStreamingReadFromTable(TableType tableType) throws Exception {\n    String uniqueTableName = \"streaming_read_test_\" + UUID.randomUUID().toString().replace(\"-\", \"\");\n    withNewTable(\n        uniqueTableName,\n        \"id BIGINT, value STRING\",\n        tableType,\n        (tableName) -> {\n          SparkSession spark = spark();\n          String queryName =\n              \"uc_streaming_read_\"\n                  + tableType.name().toLowerCase()\n                  + \"_\"\n                  + UUID.randomUUID().toString().replace(\"-\", \"\");\n          StreamingQuery query = null;\n\n          try {\n            List<List<String>> expected = new ArrayList<>();\n            // Seed an initial commit (required for managed tables, harmless for external).\n            spark.sql(String.format(\"INSERT INTO %s VALUES (0, 'seed')\", tableName)).collect();\n            expected.add(List.of(\"0\", \"seed\"));\n            Dataset<Row> input = spark.readStream().table(tableName);\n            // Start the streaming query into a memory sink\n            query =\n                input\n                    .writeStream()\n                    .format(\"memory\")\n                    .queryName(queryName)\n                    .option(\"checkpointLocation\", createTempCheckpointDir())\n                    .outputMode(\"append\")\n                    .start();\n\n            assertTrue(query.isActive(), \"Streaming query should be active\");\n\n            // Write a few batches and verify the stream consumes them.\n            for (long i = 1; i <= 3; i += 1) {\n              String value = \"value_\" + i;\n              spark\n                  .sql(String.format(\"INSERT INTO %s VALUES (%d, '%s')\", tableName, i, value))\n                  .collect();\n\n              query.processAllAvailable();\n              // Validate by checking if query and expected match.\n              expected.add(List.of(String.valueOf(i), value));\n              check(queryName, expected);\n            }\n          } finally {\n            if (query != null) {\n              // TODO: remove additional processAllAvailable once interrupt is handled gracefully\n              query.processAllAvailable();\n              query.awaitTermination(10000);\n              query.stop();\n              assertFalse(query.isActive(), \"Streaming query should have stopped\");\n            }\n            spark.sql(\"DROP VIEW IF EXISTS \" + queryName);\n          }\n        });\n  }\n\n  private void assertUCManagedTableVersion(long expectedVersion, String tableName, ApiClient client)\n      throws ApiException {\n    // Get the table info.\n    TablesApi tablesApi = new TablesApi(client);\n    TableInfo tableInfo = tablesApi.getTable(tableName, false, false);\n\n    // Get the latest UC commit version.\n    DeltaCommitsApi deltaCommitsApi = new DeltaCommitsApi(client);\n    DeltaGetCommitsResponse resp =\n        deltaCommitsApi.getCommits(\n            new DeltaGetCommits().tableId(tableInfo.getTableId()).startVersion(0L));\n    assertNotNull(resp, \"DeltaGetCommits response should not be null\");\n    assertNotNull(resp.getLatestTableVersion(), \"Latest table version should not be null\");\n\n    // The UC server should have the latest version.\n    assertEquals(expectedVersion, resp.getLatestTableVersion());\n  }\n\n  private static Seq<Row> createRowsAsSeq(Row... rows) {\n    return JavaConverters.asScalaIteratorConverter(Arrays.asList(rows).iterator())\n        .asScala()\n        .toSeq();\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableBlockMetadataUpdateTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport org.junit.jupiter.api.Assumptions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests that schema-changing and property-changing operations are blocked on Unity Catalog managed\n * (CatalogOwned) tables — regardless of which layer does the blocking.\n *\n * <p>There are two distinct layers of protection:\n *\n * <ol>\n *   <li><strong>Kill switch</strong> in {@code OptimisticTransaction.updateMetadata()}: blocks any\n *       commit that would change schema, partitions, description, or configuration on an existing\n *       CatalogOwned table. A second guard in {@code prepareCommit()} and {@code commitLarge()}\n *       blocks {@code delta.clustering} {@code DomainMetadata} changes (e.g. via RESTORE TABLE to a\n *       version written by an older client that had different clustering columns).\n *   <li><strong>UC catalog layer</strong> in {@code UCSingleCatalog}: {@code alterTable()} throws\n *       {@code UnsupportedOperationException} for all ALTER TABLE variants. INSERT OVERWRITE with\n *       {@code overwriteSchema=true} and {@code CREATE OR REPLACE TABLE} both route through REPLACE\n *       TABLE AS SELECT (RTAS) because {@code UCSingleCatalog} does not implement {@code\n *       StagingTableCatalog}; RTAS is not supported in OSS Delta.\n * </ol>\n *\n * <p>EXTERNAL tables are not CatalogOwned and are NOT affected by the kill switch; they continue to\n * allow schema evolution as before.\n */\npublic class UCDeltaTableBlockMetadataUpdateTest extends UCDeltaTableIntegrationBaseTest {\n\n  // Error produced by the kill switch in OptimisticTransaction.updateMetadata().\n  private static final String KILL_SWITCH_ERROR =\n      \"Metadata changes on Unity Catalog managed tables\";\n\n  // Error produced by the clustering kill switch in commitLarge().\n  private static final String CLUSTERING_KILL_SWITCH_ERROR =\n      \"Clustering column changes on Unity Catalog managed tables\";\n\n  // Error produced by UCSingleCatalog.alterTable() for all ALTER TABLE variants.\n  private static final String ALTER_TABLE_ERROR = \"Altering a table is not supported yet\";\n\n  // Error produced by OSS Delta when REPLACE TABLE AS SELECT (RTAS) is attempted.\n  // Triggered by CREATE OR REPLACE TABLE and DataFrame saveAsTable(overwrite+overwriteSchema)\n  // when the target catalog does not implement StagingTableCatalog.\n  private static final String RTAS_ERROR = \"REPLACE TABLE AS SELECT (RTAS) is not supported\";\n\n  // ---------------------------------------------------------------------------\n  // Kill-switch tests: operations blocked by OptimisticTransaction.updateMetadata()\n  // ---------------------------------------------------------------------------\n\n  /**\n   * INSERT with {@code autoMerge=true} and MERGE INTO with schema evolution must be blocked by the\n   * kill switch in {@code updateMetadata()} on CatalogOwned tables. The kill switch covers all\n   * metadata fields (schema, partitions, description, properties); schema evolution via autoMerge\n   * is the primary user-facing path that reaches it without going through ALTER TABLE.\n   */\n  @Test\n  public void testMetadataChangesViaWritesAreBlocked() throws Exception {\n    withNewTable(\n        \"block_schema_evolution_target\",\n        \"id INT, name STRING\",\n        TableType.MANAGED,\n        targetTable -> {\n          sql(\"INSERT INTO %s VALUES (1, 'initial')\", targetTable);\n          withNewTable(\n              \"block_schema_evolution_source\",\n              \"id INT, name STRING, extra STRING\",\n              TableType.EXTERNAL,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (2, 'new', 'extra_value')\", sourceTable);\n                sql(\"SET spark.databricks.delta.schema.autoMerge.enabled = true\");\n                try {\n                  // INSERT with autoMerge introduces a new column.\n                  assertThrowsWithCauseContaining(\n                      KILL_SWITCH_ERROR,\n                      () -> sql(\"INSERT INTO %s SELECT * FROM %s\", targetTable, sourceTable));\n\n                  // MERGE INTO with autoMerge introduces a new column from the source.\n                  assertThrowsWithCauseContaining(\n                      KILL_SWITCH_ERROR,\n                      () ->\n                          sql(\n                              \"MERGE INTO %s AS target \"\n                                  + \"USING %s AS source \"\n                                  + \"ON target.id = source.id \"\n                                  + \"WHEN NOT MATCHED THEN INSERT *\",\n                              targetTable, sourceTable));\n                } finally {\n                  sql(\"SET spark.databricks.delta.schema.autoMerge.enabled = false\");\n                }\n              });\n        });\n  }\n\n  // ---------------------------------------------------------------------------\n  // UC catalog layer tests: operations blocked by UCSingleCatalog before reaching Delta\n  // ---------------------------------------------------------------------------\n\n  /**\n   * All ALTER TABLE variants on a CatalogOwned table must be blocked by {@code\n   * UCSingleCatalog.alterTable()}, which throws {@code UnsupportedOperationException} for every\n   * table change regardless of the specific operation.\n   *\n   * <p>Covered operations: SET TBLPROPERTIES (configuration change), ADD COLUMNS (schema change),\n   * and CLUSTER BY (clustering change). All share the same managed table and each throws before\n   * modifying anything.\n   */\n  @Test\n  public void testAlterTableOperationsAreBlocked() throws Exception {\n    withNewTable(\n        \"block_alter_table_test\",\n        \"id INT, name STRING\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'initial')\", tableName);\n\n          // ALTER TABLE SET TBLPROPERTIES would change configuration.\n          assertThrowsWithCauseContaining(\n              ALTER_TABLE_ERROR,\n              () -> sql(\"ALTER TABLE %s SET TBLPROPERTIES ('custom.key' = 'value')\", tableName));\n\n          // ALTER TABLE ADD COLUMNS would change the schema.\n          assertThrowsWithCauseContaining(\n              ALTER_TABLE_ERROR, () -> sql(\"ALTER TABLE %s ADD COLUMNS (extra STRING)\", tableName));\n\n          // ALTER TABLE CLUSTER BY would change clustering columns.\n          assertThrowsWithCauseContaining(\n              ALTER_TABLE_ERROR, () -> sql(\"ALTER TABLE %s CLUSTER BY (id)\", tableName));\n        });\n  }\n\n  /**\n   * RESTORE TABLE to a version with unchanged clustering must succeed. The clustering kill switch\n   * only fires when clustering actually changes.\n   */\n  @Test\n  public void testRestoreTableWithUnchangedClusteringSucceeds() throws Exception {\n    String tableName = fullTableName(\"restore_unchanged_clustering_test\");\n    try {\n      sql(\n          \"CREATE TABLE %s (id INT, name STRING) USING DELTA CLUSTER BY (id)\"\n              + \" TBLPROPERTIES ('delta.feature.catalogManaged'='supported')\",\n          tableName);\n      sql(\"INSERT INTO %s VALUES (1, 'a'), (2, 'b')\", tableName);\n      long versionAfterInsert = currentVersion(tableName);\n      // Restore to version 0 (before the insert): clustering is unchanged, must succeed.\n      sql(\"RESTORE TABLE %s TO VERSION AS OF %d\", tableName, versionAfterInsert - 1);\n      check(tableName, List.of());\n    } finally {\n      sql(\"DROP TABLE IF EXISTS %s\", tableName);\n    }\n  }\n\n  /**\n   * RESTORE TABLE to a version whose clustering differs from the current version must be blocked by\n   * the kill switch in {@code commitLarge()}.\n   *\n   * <p>A table can have different clustering at different versions if an older client or another\n   * connector wrote a version before this guard was in place. This test simulates that by writing a\n   * Delta log entry with different clustering directly to the table's underlying storage (bypassing\n   * the kill switch), then attempting a RESTORE that would change clustering back.\n   *\n   * <p>This test is local-only: it needs direct filesystem access to write the fake commit, which\n   * is only available when backed by {@code S3CredentialFileSystem} (local UC).\n   */\n  @Test\n  public void testRestoreTableWithClusteringChangeIsBlocked() throws Exception {\n    Assumptions.assumeFalse(\n        isUCRemoteConfigured(), \"Requires local filesystem access (local UC only)\");\n\n    String tableName = fullTableName(\"restore_clustering_change_test\");\n    try {\n      sql(\n          \"CREATE TABLE %s (id INT, name STRING) USING DELTA CLUSTER BY (id)\"\n              + \" TBLPROPERTIES ('delta.feature.catalogManaged'='supported')\",\n          tableName);\n      sql(\"INSERT INTO %s VALUES (1, 'a'), (2, 'b')\", tableName);\n      long insertVersion = currentVersion(tableName);\n\n      // Simulate an older client writing a version with different clustering directly to the\n      // table's storage, bypassing the kill switch. This is how a real-world scenario could arise.\n      String s3Location =\n          sql(\"DESCRIBE FORMATTED %s\", tableName).stream()\n              .filter(r -> r.size() >= 2 && \"Location\".equalsIgnoreCase(r.get(0).trim()))\n              .map(r -> r.get(1).trim())\n              .findFirst()\n              .orElseThrow();\n      // Convert URI to a local filesystem path:\n      // - S3CredentialFileSystem maps s3://fakeS3Bucket/abs/path → /abs/path\n      // - Local UC may return a file: URI directly\n      String localTablePath;\n      if (s3Location.startsWith(\"s3://\")) {\n        localTablePath = s3Location.replaceAll(\"s3://[^/]+\", \"\");\n      } else if (s3Location.startsWith(\"file:\")) {\n        localTablePath = s3Location.replaceAll(\"^file:/+\", \"/\");\n      } else {\n        localTablePath = s3Location;\n      }\n      long hackedVersion = insertVersion + 1;\n      Path hackedCommitFile =\n          Paths.get(localTablePath, \"_delta_log\", String.format(\"%020d.json\", hackedVersion));\n      // Write a minimal Delta commit with delta.clustering on 'name' instead of 'id'.\n      String clusteringOnName = \"{\\\\\\\"clusteringColumns\\\\\\\":[[\\\\\\\"name\\\\\\\"]]}\";\n      Files.writeString(\n          hackedCommitFile,\n          \"{\\\"commitInfo\\\":{\\\"timestamp\\\":1000000000000,\\\"inCommitTimestamp\\\":1000000000000,\"\n              + \"\\\"operation\\\":\\\"MANUAL UPDATE\\\",\\\"operationParameters\\\":{},\\\"isBlindAppend\\\":false}}\\n\"\n              + \"{\\\"domainMetadata\\\":{\\\"domain\\\":\\\"delta.clustering\\\",\"\n              + \"\\\"configuration\\\":\\\"\"\n              + clusteringOnName\n              + \"\\\",\\\"removed\\\":false}}\\n\");\n\n      // RESTORE to a version before the hacked commit: the current snapshot now shows 'name'\n      // clustering, so restoring to 'id' clustering fires the kill switch.\n      assertThrowsWithCauseContaining(\n          CLUSTERING_KILL_SWITCH_ERROR,\n          () -> sql(\"RESTORE TABLE %s TO VERSION AS OF %d\", tableName, insertVersion - 1));\n    } finally {\n      sql(\"DROP TABLE IF EXISTS %s\", tableName);\n    }\n  }\n\n  /**\n   * INSERT OVERWRITE with {@code overwriteSchema=true} that would replace the schema of an existing\n   * CatalogOwned table must be blocked.\n   *\n   * <p>Because {@code UCSingleCatalog} does not implement {@code StagingTableCatalog}, Spark routes\n   * the overwrite-with-schema-change through REPLACE TABLE AS SELECT (RTAS), which OSS Delta does\n   * not support.\n   */\n  @Test\n  public void testInsertOverwriteWithOverwriteSchemaIsBlocked() throws Exception {\n    withNewTable(\n        \"block_overwrite_schema_target\",\n        \"id INT, name STRING\",\n        TableType.MANAGED,\n        targetTable -> {\n          sql(\"INSERT INTO %s VALUES (1, 'initial')\", targetTable);\n          withNewTable(\n              \"block_overwrite_schema_source\",\n              \"id INT, name STRING, extra STRING\",\n              TableType.EXTERNAL,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (2, 'new', 'extra_val')\", sourceTable);\n                assertThrowsWithCauseContaining(\n                    RTAS_ERROR,\n                    () ->\n                        spark()\n                            .read()\n                            .table(sourceTable)\n                            .write()\n                            .format(\"delta\")\n                            .mode(\"overwrite\")\n                            .option(\"overwriteSchema\", \"true\")\n                            .saveAsTable(targetTable));\n              });\n        });\n  }\n\n  /**\n   * {@code CREATE OR REPLACE TABLE} with a different schema on an existing CatalogOwned table must\n   * be blocked.\n   *\n   * <p>Because {@code UCSingleCatalog} does not implement {@code StagingTableCatalog}, Spark routes\n   * {@code CREATE OR REPLACE TABLE} through REPLACE TABLE AS SELECT (RTAS), which OSS Delta does\n   * not support.\n   */\n  @Test\n  public void testReplaceTableWithNewSchemaIsBlocked() throws Exception {\n    withNewTable(\n        \"block_replace_schema_test\",\n        \"id INT, name STRING\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'initial')\", tableName);\n          assertThrowsWithCauseContaining(\n              RTAS_ERROR,\n              () ->\n                  sql(\n                      \"CREATE OR REPLACE TABLE %s (id INT, name STRING, extra STRING) \"\n                          + \"USING DELTA \"\n                          + \"TBLPROPERTIES ('delta.feature.catalogManaged'='supported')\",\n                      tableName));\n        });\n  }\n\n  // ---------------------------------------------------------------------------\n  // Positive tests: operations that must still succeed\n  // ---------------------------------------------------------------------------\n\n  /** Normal INSERT with no metadata change must still succeed on CatalogOwned tables. */\n  @Test\n  public void testNormalInsertSucceedsForManagedTable() throws Exception {\n    withNewTable(\n        \"normal_insert_managed_test\",\n        \"id INT, name STRING\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'foo'), (2, 'bar')\", tableName);\n          check(tableName, List.of(List.of(\"1\", \"foo\"), List.of(\"2\", \"bar\")));\n        });\n  }\n\n  /**\n   * INSERT with {@code autoMerge=true} but no new columns must succeed -- {@code autoMerge} only\n   * triggers a schema update when the incoming data actually introduces extra columns.\n   */\n  @Test\n  public void testInsertWithAutoMergeAndNoSchemaChangeSucceeds() throws Exception {\n    withNewTable(\n        \"auto_merge_no_change_managed_test\",\n        \"id INT, name STRING\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'initial')\", tableName);\n          sql(\"SET spark.databricks.delta.schema.autoMerge.enabled = true\");\n          try {\n            sql(\"INSERT INTO %s VALUES (2, 'second')\", tableName);\n            check(tableName, List.of(List.of(\"1\", \"initial\"), List.of(\"2\", \"second\")));\n          } finally {\n            sql(\"SET spark.databricks.delta.schema.autoMerge.enabled = false\");\n          }\n        });\n  }\n\n  /**\n   * Schema evolution via INSERT with {@code autoMerge=true} must still work on EXTERNAL (non-\n   * CatalogOwned) tables. The kill switch must not affect tables that are not CatalogOwned.\n   */\n  @Test\n  public void testInsertWithMergeSchemaStillWorksForExternalTable() throws Exception {\n    withNewTable(\n        \"merge_schema_external_target\",\n        \"id INT, name STRING\",\n        TableType.EXTERNAL,\n        targetTable -> {\n          sql(\"INSERT INTO %s VALUES (1, 'initial')\", targetTable);\n          withNewTable(\n              \"merge_schema_external_source\",\n              \"id INT, name STRING, extra STRING\",\n              TableType.EXTERNAL,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (2, 'new', 'extra_value')\", sourceTable);\n                sql(\"SET spark.databricks.delta.schema.autoMerge.enabled = true\");\n                try {\n                  // Should succeed: EXTERNAL tables are not CatalogOwned.\n                  sql(\"INSERT INTO %s SELECT * FROM %s\", targetTable, sourceTable);\n                  // The target now has 3 columns; row 1 has null for 'extra'.\n                  check(\n                      targetTable,\n                      List.of(List.of(\"1\", \"initial\", \"null\"), List.of(\"2\", \"new\", \"extra_value\")));\n                } finally {\n                  sql(\"SET spark.databricks.delta.schema.autoMerge.enabled = false\");\n                }\n              });\n        });\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableCreationTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableMap;\nimport io.unitycatalog.client.ApiException;\nimport io.unitycatalog.client.api.TablesApi;\nimport io.unitycatalog.client.model.ColumnInfo;\nimport io.unitycatalog.client.model.DataSourceFormat;\nimport io.unitycatalog.client.model.TableInfo;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\nimport lombok.experimental.Accessors;\nimport org.apache.commons.lang3.tuple.Pair;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.log4j.Logger;\nimport org.apache.spark.sql.connector.catalog.TableCatalog;\nimport org.assertj.core.api.Assertions;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Assumptions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DynamicTest;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestFactory;\n\n/** Test suite for creating UC Delta Tables. */\npublic class UCDeltaTableCreationTest extends UCDeltaTableIntegrationBaseTest {\n\n  private static final Logger LOG = Logger.getLogger(UCDeltaTableCreationTest.class);\n\n  // Property constants related to managed table creation\n  private static final String UC_TABLE_ID_KEY = \"io.unitycatalog.tableId\";\n  private static final String UC_TABLE_ID_KEY_OLD = \"ucTableId\";\n  private static final String DELTA_CATALOG_MANAGED_KEY = \"delta.feature.catalogManaged\";\n  private static final String SUPPORTED = \"supported\";\n  private static final String MANAGED_TBLPROPERTIES_CLAUSE =\n      String.format(\"TBLPROPERTIES ('%s'='%s', 'Foo'='Bar')\", DELTA_CATALOG_MANAGED_KEY, SUPPORTED);\n  // In the table REPLACE test, a slightly different table property clause will be used to create\n  // the first table. Then the REPLACE command would use TBLPROPERTIES_CLAUSE. This is to make sure\n  // that the table properties are properly updated in the REPLACE command.\n  private static final String MANAGED_TBLPROPERTIES_CLAUSE_OTHER =\n      String.format(\n          \"TBLPROPERTIES ('%s'='%s', 'Foo2'='Bar2')\", DELTA_CATALOG_MANAGED_KEY, SUPPORTED);\n\n  // Expected table features to be enabled for managed tables\n  private static final List<String> EXPECTED_MANAGED_TABLE_FEATURES =\n      List.of(\n          \"delta.feature.appendOnly\",\n          DELTA_CATALOG_MANAGED_KEY,\n          \"delta.feature.deletionVectors\",\n          \"delta.feature.domainMetadata\",\n          \"delta.feature.inCommitTimestamp\",\n          \"delta.feature.invariants\",\n          \"delta.feature.rowTracking\",\n          \"delta.feature.v2Checkpoint\",\n          \"delta.feature.vacuumProtocolCheck\");\n  private static final Map<String, String> EXPECTED_MANAGED_TABLE_FEATURES_PROPERTIES =\n      EXPECTED_MANAGED_TABLE_FEATURES.stream()\n          .collect(Collectors.toMap(Function.identity(), k -> SUPPORTED));\n\n  private static final String EXTERNAL_TBLPROPERTIES_CLAUSE = \"TBLPROPERTIES ('Foo'='Bar')\";\n\n  /**\n   * Returns true if the Unity Catalog Spark version >0.4.0 so that it supports complex data types\n   * in columns and partition index.\n   */\n  private static boolean isUcSparkNewerThan040() {\n    final int[] VER_0_4_0 = {0, 4, 0};\n    int[] ucSparkVersion = getUnityCatalogSparkVersion();\n    return Arrays.compare(ucSparkVersion, VER_0_4_0) > 0;\n  }\n\n  String tempDir;\n  private Set<String> tablesToCleanUp = new HashSet<>();\n\n  @BeforeEach\n  public void setUp() {\n    tempDir = unityCatalogInfo().baseTableLocation() + \"/temp-\" + UUID.randomUUID();\n  }\n\n  @AfterEach\n  public void cleanUpTables() {\n    for (String fullTableName : tablesToCleanUp) {\n      try {\n        sql(\"DROP TABLE IF EXISTS %s\", fullTableName);\n      } catch (Exception e) {\n        // Ignore during clean up.\n      }\n    }\n    tablesToCleanUp.clear();\n  }\n\n  /** Helper class for controlling table creation options during tests. */\n  @Accessors(chain = true)\n  @Getter\n  @Setter\n  @ToString\n  private class TableSetupOptions {\n\n    private TableType tableType;\n    private String catalogName;\n    private String schemaName;\n    private String tableName;\n    private Optional<String> partitionColumn = Optional.empty();\n    private Optional<String> clusterColumn = Optional.empty();\n    private Optional<Pair<Integer, String>> asSelect = Optional.empty();\n    private Optional<String> comment = Optional.empty();\n    private boolean replaceTable = false;\n\n    public TableSetupOptions() {}\n\n    public TableSetupOptions setPartitionColumn(String column) {\n      Preconditions.checkArgument(List.of(\"i\", \"s\").contains(column));\n      Preconditions.checkState(\n          clusterColumn.isEmpty(), \"Can not have both PARTITIONED BY and CLUSTER BY.\");\n      partitionColumn = Optional.of(column);\n      return this;\n    }\n\n    public TableSetupOptions setClusterColumn(String column) {\n      Preconditions.checkArgument(List.of(\"i\", \"s\").contains(column));\n      Preconditions.checkState(\n          partitionColumn.isEmpty(), \"Can not have both PARTITIONED BY and CLUSTER BY.\");\n      clusterColumn = Optional.of(column);\n      return this;\n    }\n\n    public TableSetupOptions setAsSelect(int i, String s) {\n      asSelect = Optional.of(Pair.of(i, s));\n      return this;\n    }\n\n    public TableSetupOptions setComment(String c) {\n      comment = Optional.of(c);\n      return this;\n    }\n\n    public String partitionClause() {\n      return partitionColumn.map(c -> String.format(\"PARTITIONED BY (%s)\", c)).orElse(\"\");\n    }\n\n    public String clusterClause() {\n      return clusterColumn.map(c -> String.format(\"CLUSTER BY (%s)\", c)).orElse(\"\");\n    }\n\n    public String columnsClause() {\n      if (asSelect.isEmpty()) {\n        return \"(i INT, s STRING)\";\n      } else {\n        // \"AS SELECT\" can't specify columns\n        return \"\";\n      }\n    }\n\n    public String asSelectClause() {\n      return asSelect\n          .map(x -> String.format(\"AS SELECT %d AS i, '%s' AS s\", x.getLeft(), x.getRight()))\n          .orElse(\"\");\n    }\n\n    public String commentClause() {\n      return comment.map(c -> String.format(\"COMMENT '%s'\", c)).orElse(\"\");\n    }\n\n    public String ddlCommand() {\n      return replaceTable ? \"REPLACE\" : \"CREATE\";\n    }\n\n    private String createManagedTableSql() {\n      return String.format(\n          \"%s TABLE %s.%s.%s %s USING DELTA %s %s %s %s %s\",\n          ddlCommand(),\n          catalogName,\n          schemaName,\n          tableName,\n          columnsClause(),\n          partitionClause(),\n          clusterClause(),\n          MANAGED_TBLPROPERTIES_CLAUSE,\n          commentClause(),\n          asSelectClause());\n    }\n\n    public String getExternalTableLocation() {\n      return tempDir + \"/\" + tableName;\n    }\n\n    private String createExternalTableSql() {\n      return String.format(\n          \"%s TABLE %s.%s.%s %s USING DELTA %s %s %s %s LOCATION '%s' %s\",\n          ddlCommand(),\n          catalogName,\n          schemaName,\n          tableName,\n          columnsClause(),\n          partitionClause(),\n          clusterClause(),\n          EXTERNAL_TBLPROPERTIES_CLAUSE,\n          commentClause(),\n          getExternalTableLocation(),\n          asSelectClause());\n    }\n\n    public String createTableSql() {\n      if (tableType == TableType.MANAGED) {\n        return createManagedTableSql();\n      } else {\n        return createExternalTableSql();\n      }\n    }\n\n    public String fullTableName() {\n      return String.join(\".\", catalogName, schemaName, tableName);\n    }\n  }\n\n  @TestFactory\n  public Stream<DynamicTest> testCreateTable() {\n    int counter = 0;\n    List<DynamicTest> tests = new ArrayList<>();\n    for (TableType tableType : TableType.values()) {\n      for (boolean withPartition : List.of(true, false)) {\n        for (boolean withCluster : List.of(true, false)) {\n          if (withCluster && withPartition) {\n            // Can not have CLUSTER BY and PARTITIONED BY on the same table\n            continue;\n          }\n          for (boolean withAsSelect : List.of(true, false)) {\n            for (boolean replaceTable : List.of(true, false)) {\n              String displayName =\n                  String.format(\n                      \"tableType=%s, withPartition=%s, withCluster=%s, withAsSelect=%s, replaceTable=%s\",\n                      tableType, withPartition, withCluster, withAsSelect, replaceTable);\n              counter++;\n              int finalCounter = counter;\n              tests.add(\n                  DynamicTest.dynamicTest(\n                      displayName,\n                      () ->\n                          runTableCreationTest(\n                              finalCounter,\n                              tableType,\n                              withPartition,\n                              withCluster,\n                              withAsSelect,\n                              replaceTable)));\n            }\n          }\n        }\n      }\n    }\n    return tests.stream();\n  }\n\n  private void runTableCreationTest(\n      int count,\n      TableType tableType,\n      boolean withPartition,\n      boolean withCluster,\n      boolean withAsSelect,\n      boolean replaceTable)\n      throws Exception {\n    UnityCatalogInfo uc = unityCatalogInfo();\n    final String comment = \"This is comment.\";\n    // Test with unity catalog only (spark_catalog is not configured as UC catalog)\n    final String catalogName = uc.catalogName();\n    final String schemaName = uc.schemaName();\n    String tableName = \"test_delta_table_\" + count;\n\n    TableSetupOptions options =\n        new TableSetupOptions()\n            .setCatalogName(catalogName)\n            .setSchemaName(schemaName)\n            .setTableName(tableName)\n            .setTableType(tableType)\n            .setReplaceTable(replaceTable)\n            .setComment(comment);\n    if (withPartition) {\n      options.setPartitionColumn(\"i\");\n    }\n    if (withCluster) {\n      options.setClusterColumn(\"s\");\n    }\n    if (withAsSelect) {\n      options.setAsSelect(1, \"a\");\n    }\n    LOG.info(\"Running table creation test: \" + options);\n\n    String fullTableName = options.fullTableName();\n    if (replaceTable) {\n      // First, create a different table to replace.\n      sql(\n          \"CREATE TABLE %s USING DELTA %s AS SELECT %s AS col1\",\n          fullTableName,\n          MANAGED_TBLPROPERTIES_CLAUSE_OTHER,\n          // Older version UC Spark client can't support Decimal type\n          isUcSparkNewerThan040() ? \"0.1\" : \"1\");\n      tablesToCleanUp.add(fullTableName);\n    }\n\n    // TODO: Remove the block if UC and delta support the atomic RT and RTAS.\n    if (replaceTable) {\n      assertThatThrownBy(() -> sql(options.createTableSql()));\n      return;\n    }\n\n    // Create table\n    sql(options.createTableSql());\n    tablesToCleanUp.add(fullTableName);\n    // Basic read/write test\n    sql(\"INSERT INTO %s SELECT 2, 'b'\", fullTableName);\n    if (withAsSelect) {\n      check(fullTableName, List.of(List.of(\"1\", \"a\"), List.of(\"2\", \"b\")));\n    } else {\n      check(fullTableName, List.of(List.of(\"2\", \"b\")));\n    }\n\n    // Verify that table information maintained at the uc server side are expected.\n    // TODO: Remove the block when delta supports the CTAS in the correct way. Currently CTAS\n    //  is missing AbstractDeltaCatalog.translateUCTableIdProperty\n    if (!withAsSelect || replaceTable) {\n      assertUCTableInfo(\n          tableType,\n          fullTableName,\n          List.of(\"i\", \"s\"),\n          Map.of(\"Foo\", \"Bar\"),\n          comment,\n          options.getExternalTableLocation(),\n          withCluster,\n          options.getClusterColumn(),\n          options.getPartitionColumn());\n    }\n  }\n\n  @Test\n  public void testCreateManagedTableErrors() {\n    String tableName = \"test_delta_errors\";\n    UnityCatalogInfo uc = unityCatalogInfo();\n    String fullTableName = uc.catalogName() + \".\" + uc.schemaName() + \".\" + tableName;\n\n    // Test 1: Non-Delta managed tables are not supported\n    assertThatThrownBy(\n            () ->\n                sql(\n                    \"CREATE TABLE %s(name STRING) USING parquet %s\",\n                    fullTableName, MANAGED_TBLPROPERTIES_CLAUSE))\n        .hasMessageContaining(\"not support non-Delta managed table\");\n\n    // Test 2: Invalid property value 'disabled' for catalogManaged feature\n    assertThatThrownBy(\n            () ->\n                sql(\n                    \"CREATE TABLE %s(name STRING) USING delta TBLPROPERTIES ('%s' = 'disabled')\",\n                    fullTableName, DELTA_CATALOG_MANAGED_KEY))\n        .hasMessageContaining(\n            String.format(\"Invalid property value 'disabled' for '%s'\", DELTA_CATALOG_MANAGED_KEY));\n\n    // Test 3: Cannot set UC table ID manually\n    for (String ucTableIdProperty : List.of(UC_TABLE_ID_KEY, UC_TABLE_ID_KEY_OLD)) {\n      assertThatThrownBy(\n              () ->\n                  sql(\n                      \"CREATE TABLE %s(name STRING) USING delta TBLPROPERTIES ('%s' = 'some_id')\",\n                      fullTableName, ucTableIdProperty))\n          .hasMessageContaining(ucTableIdProperty);\n    }\n\n    // Test 4: Cannot set is_managed_location to false for managed tables\n    assertThatThrownBy(\n            () ->\n                sql(\n                    \"CREATE TABLE %s(name STRING) USING delta TBLPROPERTIES ('%s' = 'false')\",\n                    fullTableName, TableCatalog.PROP_IS_MANAGED_LOCATION))\n        .hasMessageContaining(\"is_managed_location\");\n\n    // Test 5: Managed table creation requires catalogManaged property\n    assertThatThrownBy(() -> sql(\"CREATE TABLE %s(name STRING) USING delta\", fullTableName))\n        .hasMessageContaining(\n            String.format(\n                \"Managed table creation requires table property '%s'='%s' to be set\",\n                DELTA_CATALOG_MANAGED_KEY, SUPPORTED));\n  }\n\n  @TestAllTableTypes\n  public void testCreateOrReplaceTable(TableType tableType) throws Exception {\n    UnityCatalogInfo uc = unityCatalogInfo();\n    String tableName = String.format(\"%s.%s.create_or_replace\", uc.catalogName(), uc.schemaName());\n    withTempDir(\n        (Path dir) -> {\n          try {\n            // TODO: Once the UC and delta support the stageCreateOrReplace, then we should remove\n            // the failure assertion. Please see https://github.com/delta-io/delta/issues/6013.\n            // CREATE OR REPLACE with new schema\n            if (tableType == TableType.MANAGED) {\n              assertThatThrownBy(\n                  () ->\n                      sql(\n                          \"CREATE OR REPLACE TABLE %s (id INT, name STRING) USING DELTA %s \",\n                          tableName, MANAGED_TBLPROPERTIES_CLAUSE));\n            } else {\n              assertThatThrownBy(\n                  () ->\n                      sql(\n                          \"CREATE OR REPLACE TABLE %s (id INT, name STRING) USING DELTA LOCATION '%s'\",\n                          tableName, dir.toString()));\n            }\n\n            // TODO: Uncommon those code once support the stageCreateOrReplace, as said above.\n\n            // Assert the unity catalog table information.\n            // assertUCTableInfo(\n            //     tableType, tableName, List.of(\"id\", \"name\"), Map.of(\"Foo\", \"Bar\"), null, null);\n\n            // Insert data to verify new schema\n            // sql(\"INSERT INTO %s VALUES (1, 'Alice')\", tableName);\n            // check(tableName, List.of(List.of(\"1\", \"Alice\")));\n          } finally {\n            sql(\"DROP TABLE IF EXISTS %s\", tableName);\n          }\n        });\n  }\n\n  @TestAllTableTypes\n  public void testTableWithSupportedDataTypes(TableType tableType) throws Exception {\n    Assumptions.assumeTrue(\n        isUcSparkNewerThan040() || tableType != TableType.MANAGED,\n        \"Older UC Spark package can't support uploading complex types to UC server for managed table\");\n    String schema =\n        // Numeric types\n        \"col_tinyint TINYINT, col_smallint SMALLINT, col_int INT, col_bigint BIGINT, \"\n            + \"col_float FLOAT, col_double DOUBLE, col_decimal DECIMAL(10,2), \"\n            // String and binary types\n            + \"col_string STRING, col_char CHAR(10), col_varchar VARCHAR(20), col_binary BINARY, \"\n            // Boolean type\n            + \"col_boolean BOOLEAN, \"\n            // Date and time types\n            + \"col_date DATE, col_timestamp TIMESTAMP, col_timestamp_ntz TIMESTAMP_NTZ\";\n\n    withNewTable(\n        \"supported_types_table\",\n        schema,\n        tableType,\n        tableName -> {\n          // Insert sample data\n          sql(\n              \"INSERT INTO %s VALUES (\"\n                  // Numeric values\n                  + \"CAST(1 AS TINYINT), CAST(100 AS SMALLINT), 1000, 100000, \"\n                  + \"2.5, 1.5, 123.45, \"\n                  // String and binary values\n                  + \"'test', 'char_test', 'varchar_test', X'CAFEBABE', \"\n                  // Boolean value\n                  + \"true, \"\n                  // Date and time values\n                  + \"DATE'2025-01-01', TIMESTAMP'2025-01-01 12:00:00', \"\n                  + \"TIMESTAMP_NTZ'2025-01-01 12:00:00')\",\n              tableName);\n\n          // Assert the unity catalog table information.\n          assertUCTableInfo(\n              tableType,\n              tableName,\n              List.of(\n                  \"col_tinyint\",\n                  \"col_smallint\",\n                  \"col_int\",\n                  \"col_bigint\",\n                  \"col_float\",\n                  \"col_double\",\n                  \"col_decimal\",\n                  \"col_string\",\n                  \"col_char\",\n                  \"col_varchar\",\n                  \"col_binary\",\n                  \"col_boolean\",\n                  \"col_date\",\n                  \"col_timestamp\",\n                  \"col_timestamp_ntz\"),\n              // This feature is automatically enabled due to use of TIMESTAMP_NTZ\n              Map.of(\"delta.feature.timestampNtz\", \"supported\"),\n              null,\n              null);\n\n          // Verify data can be queried - checking that each column type is correctly\n          // stored/retrieved\n          List<List<String>> results = sql(\"SELECT * FROM %s\", tableName);\n          assertThat(results).hasSize(1);\n          List<String> row = results.get(0);\n\n          // Verify each column value\n          assertThat(row.get(0)).isEqualTo(\"1\"); // TINYINT\n          assertThat(row.get(1)).isEqualTo(\"100\"); // SMALLINT\n          assertThat(row.get(2)).isEqualTo(\"1000\"); // INT\n          assertThat(row.get(3)).isEqualTo(\"100000\"); // BIGINT\n          assertThat(row.get(4)).isEqualTo(\"2.5\"); // FLOAT\n          assertThat(row.get(5)).isEqualTo(\"1.5\"); // DOUBLE\n          assertThat(row.get(6)).isEqualTo(\"123.45\"); // DECIMAL\n          assertThat(row.get(7)).isEqualTo(\"test\"); // STRING\n          assertThat(row.get(8)).isEqualTo(\"char_test \"); // CHAR (padded with space)\n          assertThat(row.get(9)).isEqualTo(\"varchar_test\"); // VARCHAR\n          assertThat(row.get(10)).startsWith(\"[B@\"); // BINARY (Java byte array object reference)\n          assertThat(row.get(11)).isEqualTo(\"true\"); // BOOLEAN\n          assertThat(row.get(12)).isEqualTo(\"2025-01-01\"); // DATE\n          assertThat(row.get(13)).isEqualTo(\"2025-01-01 12:00:00.0\"); // TIMESTAMP\n          assertThat(row.get(14)).isEqualTo(\"2025-01-01T12:00\"); // TIMESTAMP_NTZ\n        });\n  }\n\n  @TestAllTableTypes\n  public void testTableWithComplexTypes(TableType tableType) throws Exception {\n    Assumptions.assumeTrue(\n        isUcSparkNewerThan040() || tableType != TableType.MANAGED,\n        \"Older UC Spark package can't support uploading complex types to UC server for managed table\");\n    String schema =\n        \"id INT, arr ARRAY<INT>, \"\n            + \"map_col MAP<STRING, INT>, \"\n            + \"struct_col STRUCT<a: INT, b: STRING>\";\n\n    withNewTable(\n        \"complex_types_table\",\n        schema,\n        tableType,\n        tableName -> {\n          // Insert sample data\n          sql(\n              \"INSERT INTO %s VALUES (1, array(1, 2, 3), \"\n                  + \"map('key1', 10, 'key2', 20), \"\n                  + \"struct(42, 'test'))\",\n              tableName);\n\n          // Assert the unity catalog table information.\n          assertUCTableInfo(\n              tableType,\n              tableName,\n              List.of(\"id\", \"arr\", \"map_col\", \"struct_col\"),\n              Map.of(),\n              null,\n              null);\n\n          // Verify data can be queried\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"ArraySeq(1, 2, 3)\", \"Map(key1 -> 10, key2 -> 20)\", \"[42,test]\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testTableWithNotNullConstraints(TableType tableType) throws Exception {\n    withNewTable(\n        \"not_null_table\",\n        \"id INT NOT NULL, name STRING NOT NULL, optional STRING\",\n        tableType,\n        tableName -> {\n          // Insert valid data\n          sql(\"INSERT INTO %s VALUES (1, 'Alice', 'extra')\", tableName);\n          sql(\"INSERT INTO %s VALUES (2, 'Bob', NULL)\", tableName);\n\n          check(tableName, List.of(List.of(\"1\", \"Alice\", \"extra\"), List.of(\"2\", \"Bob\", \"null\")));\n\n          // Assert the unity catalog table information.\n          assertUCTableInfo(\n              tableType, tableName, List.of(\"id\", \"name\", \"optional\"), Map.of(), null, null);\n\n          // Attempting to insert NULL into NOT NULL column should fail\n          Assertions.assertThatThrownBy(\n                  () -> sql(\"INSERT INTO %s VALUES (NULL, 'Charlie', 'data')\", tableName))\n              .isInstanceOf(Exception.class);\n        });\n  }\n\n  private void assertUCTableInfo(\n      TableType tableType,\n      String fullTableName,\n      List<String> expectedColumns,\n      Map<String, String> customizedProps,\n      String comment,\n      String externalTableLocation)\n      throws ApiException {\n    assertUCTableInfo(\n        tableType,\n        fullTableName,\n        expectedColumns,\n        customizedProps,\n        comment,\n        externalTableLocation,\n        false,\n        Optional.empty(),\n        Optional.empty());\n  }\n\n  private void assertUCTableInfo(\n      TableType tableType,\n      String fullTableName,\n      List<String> expectedColumns,\n      Map<String, String> customizedProps,\n      String comment,\n      String externalTableLocation,\n      boolean withCluster,\n      Optional<String> clusterColumn,\n      Optional<String> partitionColumn)\n      throws ApiException {\n    UnityCatalogInfo uc = unityCatalogInfo();\n    String catalogName = uc.catalogName();\n    String schemaName = uc.schemaName();\n\n    // Verify that properties are set on server. This can not be done by DESC EXTENDED.\n    TablesApi tablesApi = new TablesApi(uc.createApiClient());\n    TableInfo tableInfo = tablesApi.getTable(fullTableName, false, false);\n    assertThat(tableInfo.getCatalogName()).isEqualTo(catalogName);\n    assertThat(tableInfo.getName()).isEqualTo(parseTableName(fullTableName));\n    assertThat(tableInfo.getSchemaName()).isEqualTo(schemaName);\n    assertThat(tableInfo.getTableType().name()).isEqualTo(tableType.name());\n    assertThat(tableInfo.getDataSourceFormat().name()).isEqualTo(DataSourceFormat.DELTA.name());\n    assertThat(tableInfo.getComment()).isEqualTo(comment);\n    if (tableType == TableType.EXTERNAL && externalTableLocation != null) {\n      assertThat(tableInfo.getStorageLocation()).isEqualTo(externalTableLocation);\n    }\n\n    // At this point table schema can not be sent to server yet because it won't be\n    // updated later and that would cause problem.\n    List<ColumnInfo> columns = tableInfo.getColumns();\n    assertThat(columns).isNotNull();\n\n    if (tableType == TableType.MANAGED) {\n      assertThat(columns).isNotEmpty();\n      List<String> columnNamesFromServer =\n          columns.stream().map(ColumnInfo::getName).collect(Collectors.toList());\n      assertThat(columnNamesFromServer).containsExactlyInAnyOrderElementsOf(expectedColumns);\n      // Partition index is only set after UC-Spark 0.4.0\n      if (isUcSparkNewerThan040() && partitionColumn.isPresent()) {\n        List<ColumnInfo> matchingColumns =\n            columns.stream()\n                .filter(c -> c.getName().equals(partitionColumn.get()))\n                .collect(Collectors.toList());\n        assertThat(matchingColumns).hasSize(1);\n        assertThat(matchingColumns.get(0).getPartitionIndex()).isEqualTo(0);\n      } else {\n        assertThat(columns.stream().anyMatch(c -> c.getPartitionIndex() != null)).isFalse();\n      }\n      // Delta sent properties of managed tables to server\n      Map<String, String> tablePropertiesFromServer = tableInfo.getProperties();\n      tablePropertiesFromServer.remove(\"table_type\", \"MANAGED\"); // New property by Spark 4.1\n\n      // CLUSTER BY has two extra properties\n      final Map<String, String> expectedClusteringProperties =\n          withCluster\n              ? ImmutableMap.<String, String>builder()\n                  .put(\"clusteringColumns\", \"[[\\\"\" + clusterColumn.get() + \"\\\"]]\")\n                  .put(\"delta.feature.clustering\", SUPPORTED)\n                  .build()\n              : ImmutableMap.of();\n      final Map<String, String> expectedOtherProperties =\n          ImmutableMap.<String, String>builder()\n              .put(\"delta.checkpointPolicy\", \"v2\")\n              .put(\"delta.enableDeletionVectors\", \"true\")\n              .put(\"delta.enableInCommitTimestamps\", \"true\")\n              .put(\"delta.enableRowTracking\", \"true\")\n              .put(\"delta.lastUpdateVersion\", \"0\")\n              .put(\"delta.minReaderVersion\", \"3\")\n              .put(\"delta.minWriterVersion\", \"7\")\n              .put(UC_TABLE_ID_KEY, tableInfo.getTableId())\n              // User specified custom table property is also sent.\n              .putAll(customizedProps)\n              .putAll(expectedClusteringProperties)\n              .build();\n      // The value of these properties aren't predictable. But at least we confirm their existence.\n      final Set<String> expectedPropertiesWithVariableValue =\n          Set.of(\n              \"delta.lastCommitTimestamp\",\n              \"delta.rowTracking.materializedRowCommitVersionColumnName\",\n              \"delta.rowTracking.materializedRowIdColumnName\");\n\n      // This is combination of expectedOtherProperties and\n      //  EXPECTED_MANAGED_TABLE_FEATURES_PROPERTIES.\n      Map<String, String> expectedProperties =\n          Stream.concat(\n                  EXPECTED_MANAGED_TABLE_FEATURES_PROPERTIES.entrySet().stream(),\n                  expectedOtherProperties.entrySet().stream())\n              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n\n      // Server has all the expected table properties\n      expectedProperties.forEach(\n          (key, value) -> assertThat(tablePropertiesFromServer).containsEntry(key, value));\n      expectedPropertiesWithVariableValue.forEach(\n          key -> assertThat(tablePropertiesFromServer).containsKey(key));\n\n      // Server doesn't have any unexpected table properties. If anyone introduces a new table\n      // property and this fails, update the list of expected properties.\n      Map<String, String> unexpectedTablePropertiesFromServer =\n          tablePropertiesFromServer.entrySet().stream()\n              .filter(\n                  entry ->\n                      !expectedProperties.containsKey(entry.getKey())\n                          && !expectedPropertiesWithVariableValue.contains(entry.getKey()))\n              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n      assertThat(unexpectedTablePropertiesFromServer).isEmpty();\n    } else {\n      assertThat(columns).isEmpty();\n    }\n\n    // Also verify table using DESC EXTENDED\n    List<List<String>> rows = sql(\"DESC EXTENDED %s\", fullTableName);\n    Map<String, String> describeResult = new HashMap<>();\n    for (List<String> row : rows) {\n      String key = row.get(0);\n      // Skip duplicate column names that appear in partition info\n      if (!expectedColumns.contains(key)) {\n        describeResult.put(key, row.get(1));\n      }\n    }\n\n    // Verify basic table properties\n    assertThat(describeResult.get(\"Name\")).isEqualTo(fullTableName);\n    assertThat(describeResult.get(\"Type\")).isEqualTo(tableType.name());\n    assertThat(describeResult.get(\"Provider\")).isEqualToIgnoringCase(\"delta\");\n    assertThat(describeResult.get(\"Is_managed_location\"))\n        .isEqualTo(tableType == TableType.MANAGED ? \"true\" : null);\n    assertThat(describeResult).containsKey(\"Table Properties\");\n    String tableProperties = describeResult.get(\"Table Properties\");\n    if (tableType == TableType.MANAGED) {\n      // Check for UC table ID\n      assertThat(tableProperties).contains(UC_TABLE_ID_KEY);\n      // Check for catalogManaged feature\n      assertThat(tableProperties)\n          .contains(String.format(\"%s=%s\", DELTA_CATALOG_MANAGED_KEY, SUPPORTED));\n    } else {\n      // Check for UC table ID\n      assertThat(tableProperties).doesNotContain(UC_TABLE_ID_KEY);\n      // Check for catalogManaged feature\n      assertThat(tableProperties).doesNotContain(DELTA_CATALOG_MANAGED_KEY);\n    }\n  }\n\n  private static String parseTableName(String fullTableName) {\n    String[] splits = fullTableName.split(\"\\\\.\");\n    assertThat(splits.length).isEqualTo(3);\n    return splits[splits.length - 1];\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableDMLTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport java.util.List;\n\n/**\n * DML test suite for Delta Table operations through Unity Catalog.\n *\n * <p>Covers INSERT, UPDATE, DELETE, and MERGE operations with various conditions and scenarios.\n * Tests are parameterized to support different table types (currently EXTERNAL only, as Delta does\n * not support MANAGED catalog-owned tables).\n */\npublic class UCDeltaTableDMLTest extends UCDeltaTableIntegrationBaseTest {\n\n  @TestAllTableTypes\n  public void testBasicInsertOperations(TableType tableType) throws Exception {\n    withNewTable(\n        \"insert_basic_test\",\n        \"id INT, name STRING, active BOOLEAN\",\n        tableType,\n        tableName -> {\n          // Single row INSERT\n          sql(\"INSERT INTO %s VALUES (1, 'initial', true)\", tableName);\n\n          // Verify single row\n          check(tableName, List.of(List.of(\"1\", \"initial\", \"true\")));\n\n          // Multiple rows in single INSERT\n          sql(\"INSERT INTO %s VALUES (2, 'User2', false), (3, 'User3', true)\", tableName);\n\n          // Multiple separate INSERT operations\n          sql(\"INSERT INTO %s VALUES (4, 'User4', false)\", tableName);\n\n          // Verify all inserts (appended data)\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"initial\", \"true\"),\n                  List.of(\"2\", \"User2\", \"false\"),\n                  List.of(\"3\", \"User3\", \"true\"),\n                  List.of(\"4\", \"User4\", \"false\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testAdvancedInsertOperations(TableType tableType) throws Exception {\n    // Test INSERT ... SELECT\n    withNewTable(\n        \"insert_select_target\",\n        \"id INT, category STRING\",\n        tableType,\n        targetTable -> {\n          withNewTable(\n              \"insert_select_source\",\n              \"id INT, name STRING\",\n              tableType,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (1, 'TypeA'), (2, 'TypeB'), (3, 'TypeA')\", sourceTable);\n                sql(\n                    \"INSERT INTO %s SELECT id, name FROM %s WHERE name = 'TypeA'\",\n                    targetTable, sourceTable);\n\n                check(targetTable, List.of(List.of(\"1\", \"TypeA\"), List.of(\"3\", \"TypeA\")));\n              });\n        });\n\n    // Test INSERT OVERWRITE\n    withNewTable(\n        \"insert_overwrite_test\",\n        \"id INT, status STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'old'), (2, 'old'), (3, 'old')\", tableName);\n          sql(\"INSERT OVERWRITE %s VALUES (4, 'new'), (5, 'new')\", tableName);\n\n          check(tableName, List.of(List.of(\"4\", \"new\"), List.of(\"5\", \"new\")));\n        });\n\n    // Test INSERT ... REPLACE WHERE\n    withNewTable(\n        \"insert_replace_test\",\n        \"id INT, status STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'pending'), (2, 'pending'), (3, 'completed')\", tableName);\n          sql(\n              \"INSERT INTO %s REPLACE WHERE id <= 2 VALUES (1, 'replaced'), (2, 'replaced')\",\n              tableName);\n\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"replaced\"), List.of(\"2\", \"replaced\"), List.of(\"3\", \"completed\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testInsertWithDynamicPartitionOverwrite(TableType tableType) throws Exception {\n    withNewTable(\n        \"insert_dynamic_partition_overwrite_test\",\n        \"id INT, name STRING, date STRING\",\n        \"date\",\n        tableType,\n        tableName -> {\n          // Setup initial data\n          sql(\"INSERT INTO %s PARTITION (date='2025-11-01') VALUES (1, 'AAA')\", tableName);\n          sql(\"INSERT INTO %s PARTITION (date='2025-11-01') VALUES (2, 'BBB')\", tableName);\n\n          // Verify the result before dynamic partition overwrite.\n          check(\n              tableName,\n              List.of(List.of(\"1\", \"AAA\", \"2025-11-01\"), List.of(\"2\", \"BBB\", \"2025-11-01\")));\n\n          // Enable dynamic partition overwrite\n          sql(\"SET spark.databricks.delta.dynamicPartitionOverwrite.enabled = true\");\n\n          try {\n            // Insert with dynamic partition overwrite\n            sql(\"INSERT OVERWRITE %s VALUES (3, 'CCC', '2025-11-01')\", tableName);\n\n            // Verify the result - should have replaced with the new value\n            check(tableName, List.of(List.of(\"3\", \"CCC\", \"2025-11-01\")));\n          } finally {\n            // Disable dynamic partition overwrite\n            sql(\"SET spark.databricks.delta.dynamicPartitionOverwrite.enabled = false\");\n          }\n        });\n  }\n\n  @TestAllTableTypes\n  public void testUpdateOperations(TableType tableType) throws Exception {\n    withNewTable(\n        \"update_test\",\n        \"id INT, priority INT, status STRING\",\n        tableType,\n        tableName -> {\n          // Setup data\n          sql(\n              \"INSERT INTO %s VALUES \"\n                  + \"(1, 1, 'pending'), (2, 5, 'pending'), (3, 10, 'pending'), (4, 2, 'completed')\",\n              tableName);\n\n          // Simple update: update specific row by id\n          sql(\"UPDATE %s SET status = 'processed' WHERE id = 1\", tableName);\n\n          // Verify simple update\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"1\", \"processed\"),\n                  List.of(\"2\", \"5\", \"pending\"),\n                  List.of(\"3\", \"10\", \"pending\"),\n                  List.of(\"4\", \"2\", \"completed\")));\n\n          // Complex update: update based on priority condition\n          sql(\"UPDATE %s SET status = 'urgent' WHERE priority >= 5\", tableName);\n\n          // Verify complex update\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"1\", \"processed\"),\n                  List.of(\"2\", \"5\", \"urgent\"),\n                  List.of(\"3\", \"10\", \"urgent\"),\n                  List.of(\"4\", \"2\", \"completed\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testDeleteOperations(TableType tableType) throws Exception {\n    withNewTable(\n        \"delete_test\",\n        \"id INT, category STRING, value INT, active BOOLEAN\",\n        tableType,\n        tableName -> {\n          // Setup data\n          sql(\n              \"INSERT INTO %s VALUES \"\n                  + \"(1, 'A', 10, true), (2, 'B', 20, false), (3, 'A', 30, true), \"\n                  + \"(4, 'C', 5, false), (5, 'B', 15, true)\",\n              tableName);\n\n          // Simple delete: single condition\n          sql(\"DELETE FROM %s WHERE active = false\", tableName);\n\n          // Verify simple delete\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"A\", \"10\", \"true\"),\n                  List.of(\"3\", \"A\", \"30\", \"true\"),\n                  List.of(\"5\", \"B\", \"15\", \"true\")));\n\n          // Complex delete: multiple conditions with OR\n          sql(\"DELETE FROM %s WHERE category = 'A' OR value < 10\", tableName);\n\n          // Verify complex delete\n          check(tableName, List.of(List.of(\"5\", \"B\", \"15\", \"true\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testMergeInsertOnly(TableType tableType) throws Exception {\n    withNewTable(\n        \"merge_insert_test\",\n        \"id INT, value STRING\",\n        tableType,\n        tableName -> {\n          // Setup target table with initial data\n          sql(\"INSERT INTO %s VALUES (1, 'existing1'), (2, 'existing2')\", tableName);\n\n          // Create source data and perform merge\n          withNewTable(\n              \"merge_source\",\n              \"id INT, value STRING\",\n              tableType,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (3, 'new3'), (4, 'new4')\", sourceTable);\n\n                sql(\n                    \"MERGE INTO %s AS target \"\n                        + \"USING %s AS source \"\n                        + \"ON target.id = source.id \"\n                        + \"WHEN NOT MATCHED THEN INSERT (id, value) VALUES (source.id, source.value)\",\n                    tableName, sourceTable);\n              });\n\n          // Verify merge result\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"existing1\"),\n                  List.of(\"2\", \"existing2\"),\n                  List.of(\"3\", \"new3\"),\n                  List.of(\"4\", \"new4\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testMergeUpdateOnly(TableType tableType) throws Exception {\n    withNewTable(\n        \"merge_update_test\",\n        \"id INT, value STRING\",\n        tableType,\n        tableName -> {\n          // Setup target table\n          sql(\"INSERT INTO %s VALUES (1, 'old1'), (2, 'old2'), (3, 'old3')\", tableName);\n\n          // Perform merge to update existing records\n          withNewTable(\n              \"merge_update_source\",\n              \"id INT, value STRING\",\n              tableType,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (2, 'updated2'), (3, 'updated3')\", sourceTable);\n\n                sql(\n                    \"MERGE INTO %s AS target \"\n                        + \"USING %s AS source \"\n                        + \"ON target.id = source.id \"\n                        + \"WHEN MATCHED THEN UPDATE SET value = source.value\",\n                    tableName, sourceTable);\n              });\n\n          // Verify merge result\n          check(\n              tableName,\n              List.of(List.of(\"1\", \"old1\"), List.of(\"2\", \"updated2\"), List.of(\"3\", \"updated3\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testMergeCombinedInsertAndUpdate(TableType tableType) throws Exception {\n    withNewTable(\n        \"merge_combined_test\",\n        \"id INT, name STRING, status STRING\",\n        tableType,\n        tableName -> {\n          // Setup target table\n          sql(\"INSERT INTO %s VALUES (1, 'Alice', 'active'), (2, 'Bob', 'inactive')\", tableName);\n\n          // Perform merge with both insert and update\n          withNewTable(\n              \"merge_combined_source\",\n              \"id INT, name STRING, status STRING\",\n              tableType,\n              sourceTable -> {\n                sql(\n                    \"INSERT INTO %s VALUES \"\n                        + \"(2, 'Bob', 'active'), (3, 'Charlie', 'active'), (4, 'Diana', 'pending')\",\n                    sourceTable);\n\n                sql(\n                    \"MERGE INTO %s AS target \"\n                        + \"USING %s AS source \"\n                        + \"ON target.id = source.id \"\n                        + \"WHEN MATCHED THEN UPDATE SET status = source.status \"\n                        + \"WHEN NOT MATCHED THEN INSERT (id, name, status) VALUES (source.id, source.name, source.status)\",\n                    tableName, sourceTable);\n              });\n\n          // Verify merge result\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"Alice\", \"active\"),\n                  List.of(\"2\", \"Bob\", \"active\"),\n                  List.of(\"3\", \"Charlie\", \"active\"),\n                  List.of(\"4\", \"Diana\", \"pending\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testMergeWithDeleteAction(TableType tableType) throws Exception {\n    withNewTable(\n        \"merge_delete_test\",\n        \"id INT, active BOOLEAN\",\n        tableType,\n        tableName -> {\n          // Setup target table\n          sql(\"INSERT INTO %s VALUES (1, true), (2, true), (3, false), (4, true)\", tableName);\n\n          // Perform merge with delete action\n          withNewTable(\n              \"merge_delete_source\",\n              \"id INT, active BOOLEAN\",\n              tableType,\n              sourceTable -> {\n                sql(\"INSERT INTO %s VALUES (2, false), (3, true), (5, true)\", sourceTable);\n\n                sql(\n                    \"MERGE INTO %s AS target \"\n                        + \"USING %s AS source \"\n                        + \"ON target.id = source.id \"\n                        + \"WHEN MATCHED AND source.active = false THEN DELETE \"\n                        + \"WHEN MATCHED THEN UPDATE SET active = source.active \"\n                        + \"WHEN NOT MATCHED THEN INSERT (id, active) VALUES (source.id, source.active)\",\n                    tableName, sourceTable);\n              });\n\n          // Verify merge result - record 2 should be deleted, record 3 should be updated\n          check(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"true\"), // not in source, no change\n                  List.of(\"3\", \"true\"), // matched and updated from false to true\n                  List.of(\"4\", \"true\"), // not in source, no change\n                  List.of(\"5\", \"true\") // not matched in target, inserted\n                  ));\n        });\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableDataFrameReadTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.apache.spark.sql.functions.col;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.apache.spark.sql.DataFrameReader;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Row;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * DataFrame read test suite for Delta Table operations through Unity Catalog.\n *\n * <p>Covers spark.table(), DataFrameReader, time travel, column pruning, and filter. Most tests run\n * against both EXTERNAL and MANAGED table types.\n */\npublic class UCDeltaTableDataFrameReadTest extends UCDeltaTableIntegrationBaseTest {\n\n  @TestAllTableTypes\n  public void testReadViaSparkTable(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_read_spark_table\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          assertThat(ids(spark().table(tableName).orderBy(\"id\"))).containsExactly(1, 2, 3);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testReadViaDataFrameReader(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_read_reader\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          assertThat(ids(deltaDFReader().table(tableName).orderBy(\"id\"))).containsExactly(1, 2, 3);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testTimeTravelByVersion(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_time_travel_version\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          long v1 = currentVersion(tableName);\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n          assertThat(ids(deltaDFReader().option(\"versionAsOf\", v1).table(tableName).orderBy(\"id\")))\n              .containsExactly(1, 2, 3);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testTimeTravelByTimestamp(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_time_travel_ts\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          String ts = currentTimestamp(tableName);\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n          assertThat(\n                  ids(deltaDFReader().option(\"timestampAsOf\", ts).table(tableName).orderBy(\"id\")))\n              .containsExactly(1, 2, 3);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testColumnPruning(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_column_pruning\",\n        \"id INT, name STRING, value INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'Alice', 100), (2, 'Bob', 200)\", tableName);\n          List<Row> rows =\n              spark().table(tableName).select(\"id\", \"name\").orderBy(\"id\").collectAsList();\n          assertThat(rows.get(0).schema().fieldNames()).containsExactly(\"id\", \"name\");\n          assertThat(rows.stream().map(r -> r.getInt(0)).collect(Collectors.toList()))\n              .containsExactly(1, 2);\n          assertThat(rows.stream().map(r -> r.getString(1)).collect(Collectors.toList()))\n              .containsExactly(\"Alice\", \"Bob\");\n        });\n  }\n\n  @Test\n  public void testReadViaPath() throws Exception {\n    withNewTable(\n        \"df_read_via_path\",\n        \"id INT\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          String tablePath =\n              sql(\"DESCRIBE EXTENDED %s\", tableName).stream()\n                  .filter(row -> row.size() >= 2 && \"Location\".equals(row.get(0)))\n                  .map(row -> row.get(1))\n                  .findFirst()\n                  .orElseThrow(() -> new AssertionError(\"Could not retrieve table location\"));\n          Assertions.assertThrows(\n              Exception.class,\n              () -> spark().read().format(\"delta\").load(tablePath).collect(),\n              \"Path-based access should fail for managed tables\");\n        });\n  }\n\n  @TestAllTableTypes\n  public void testChangeDataFeedViaDataFrameAPI(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_cdf_reader\",\n        \"id INT\",\n        null,\n        tableType,\n        \"'delta.enableChangeDataFeed'='true'\",\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n          long currentVersion = currentVersion(tableName);\n          List<Row> rows =\n              deltaDFReader()\n                  .option(\"readChangeFeed\", \"true\")\n                  .option(\"startingVersion\", currentVersion)\n                  .table(tableName)\n                  .orderBy(\"id\")\n                  .collectAsList();\n          assertThat(rows.stream().map(r -> r.getInt(0)).collect(Collectors.toList()))\n              .containsExactly(4, 5);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testEmptyTableRead(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_empty_read\",\n        \"id INT\",\n        tableType,\n        tableName -> assertThat(spark().table(tableName).collectAsList()).isEmpty());\n  }\n\n  @TestAllTableTypes\n  public void testFilter(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_filter\",\n        \"id INT, category STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'A'), (2, 'B'), (3, 'A'), (4, 'B')\", tableName);\n          assertThat(\n                  ids(spark().table(tableName).filter(col(\"category\").equalTo(\"A\")).orderBy(\"id\")))\n              .containsExactly(1, 3);\n        });\n  }\n\n  private DataFrameReader deltaDFReader() {\n    return spark().read().format(\"delta\");\n  }\n\n  private List<Integer> ids(Dataset<Row> df) {\n    return df.collectAsList().stream().map(r -> r.getInt(0)).collect(Collectors.toList());\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableDataFrameStreamingTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.apache.spark.api.java.function.VoidFunction2;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.streaming.DataStreamReader;\nimport org.apache.spark.sql.streaming.StreamingQuery;\nimport org.apache.spark.sql.streaming.Trigger;\nimport org.assertj.core.api.ThrowableAssert.ThrowingCallable;\nimport org.junit.jupiter.api.io.TempDir;\n\n/**\n * DataFrame streaming test suite for Delta Table operations through Unity Catalog.\n *\n * <p>Covers streaming read and write via Structured Streaming. Uses {@link Trigger#AvailableNow()}\n * for deterministic, testable streaming without manual termination. Tests run against both EXTERNAL\n * and MANAGED table types.\n */\npublic class UCDeltaTableDataFrameStreamingTest extends UCDeltaTableIntegrationBaseTest {\n\n  /** No-op foreachBatch sink used by negative tests that only need the stream to start. */\n  private static final VoidFunction2<Dataset<Row>, Long> NOOP_BATCH = (df, id) -> {};\n\n  @TempDir private Path tempDir;\n\n  private int checkpointCount;\n\n  @TestAllTableTypes\n  public void testStreamingReadWrite(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_rw_src\",\n        \"id INT\",\n        tableType,\n        srcName ->\n            withNewTable(\n                \"streaming_rw_sink\",\n                \"id INT\",\n                tableType,\n                sinkName -> {\n                  sql(\"INSERT INTO %s VALUES (1), (2), (3)\", srcName);\n                  String ck = checkpoint();\n\n                  // AvailableNow: process all existing data and terminate.\n                  spark()\n                      .readStream()\n                      .format(\"delta\")\n                      .table(srcName)\n                      .writeStream()\n                      .format(\"delta\")\n                      .outputMode(\"append\")\n                      .trigger(Trigger.AvailableNow())\n                      .option(\"checkpointLocation\", ck)\n                      .toTable(sinkName)\n                      .awaitTermination();\n                  check(sinkName, List.of(row(\"1\"), row(\"2\"), row(\"3\")));\n\n                  // Continuous: reuse same checkpoint so the query resumes from where\n                  // AvailableNow left off and only picks up newly inserted rows.\n                  StreamingQuery query =\n                      spark()\n                          .readStream()\n                          .format(\"delta\")\n                          .table(srcName)\n                          .writeStream()\n                          .format(\"delta\")\n                          .outputMode(\"append\")\n                          .option(\"checkpointLocation\", ck)\n                          .toTable(sinkName);\n                  try {\n                    sql(\"INSERT INTO %s VALUES (4), (5)\", srcName);\n                    query.processAllAvailable();\n                    check(sinkName, List.of(row(\"1\"), row(\"2\"), row(\"3\"), row(\"4\"), row(\"5\")));\n                  } finally {\n                    query.stop();\n                  }\n                }));\n  }\n\n  @TestAllTableTypes\n  public void testStreamingReadFromVersion(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_version_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          long v1 = currentVersion(tableName);\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n          List<Integer> result = new ArrayList<>();\n          spark()\n              .readStream()\n              .format(\"delta\")\n              .option(\"startingVersion\", v1 + 1)\n              .table(tableName)\n              .writeStream()\n              .trigger(Trigger.AvailableNow())\n              .option(\"checkpointLocation\", checkpoint())\n              .foreachBatch((VoidFunction2<Dataset<Row>, Long>) (df, id) -> result.addAll(ids(df)))\n              .start()\n              .awaitTermination();\n          assertThat(result).containsExactlyInAnyOrder(4, 5);\n        });\n  }\n\n  /**\n   * Verifies incremental streaming via a foreachBatch sink (in-memory accumulator). Complements\n   * {@link #testStreamingReadWrite}, which covers the same data-arrival scenario but writes to a\n   * Delta table sink and validates via SQL.\n   */\n  @TestAllTableTypes\n  public void testStreamingContinuous(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_continuous_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          List<Integer> result = new ArrayList<>();\n          StreamingQuery query =\n              spark()\n                  .readStream()\n                  .format(\"delta\")\n                  .table(tableName)\n                  .writeStream()\n                  .option(\"checkpointLocation\", checkpoint())\n                  .foreachBatch(\n                      (VoidFunction2<Dataset<Row>, Long>) (df, id) -> result.addAll(ids(df)))\n                  .start();\n          try {\n            query.processAllAvailable();\n            assertThat(result).containsExactlyInAnyOrder(1, 2, 3);\n\n            sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n            query.processAllAvailable();\n            assertThat(result).containsExactlyInAnyOrder(1, 2, 3, 4, 5);\n          } finally {\n            query.stop();\n          }\n        });\n  }\n\n  /**\n   * Verifies that {@code maxFilesPerTrigger=1} causes the stream to process each of the 3 separate\n   * commits as its own micro-batch, producing exactly 3 batches total under {@link\n   * Trigger#AvailableNow()}.\n   */\n  @TestAllTableTypes\n  public void testStreamingMaxFilesPerTrigger(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_max_files_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1)\", tableName);\n          sql(\"INSERT INTO %s VALUES (2)\", tableName);\n          sql(\"INSERT INTO %s VALUES (3)\", tableName);\n\n          List<Integer> result = new ArrayList<>();\n          List<Long> batchIds = new ArrayList<>();\n          spark()\n              .readStream()\n              .format(\"delta\")\n              .option(\"maxFilesPerTrigger\", 1)\n              .table(tableName)\n              .writeStream()\n              .trigger(Trigger.AvailableNow())\n              .option(\"checkpointLocation\", checkpoint())\n              .foreachBatch(\n                  (VoidFunction2<Dataset<Row>, Long>)\n                      (df, batchId) -> {\n                        result.addAll(ids(df));\n                        batchIds.add(batchId);\n                      })\n              .start()\n              .awaitTermination();\n          assertThat(result).containsExactlyInAnyOrder(1, 2, 3);\n          assertThat(batchIds).hasSize(3);\n        });\n  }\n\n  /**\n   * Verifies that streaming from a table that has rows deleted (RemoveFile with dataChange=true)\n   * fails with a clear error directing the user to the {@code ignoreDeletes} option.\n   */\n  @TestAllTableTypes\n  public void testStreamingDeleteFailsWithHelpfulError(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_delete_error_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          List<Integer> result = new ArrayList<>();\n          StreamingQuery query =\n              spark()\n                  .readStream()\n                  .format(\"delta\")\n                  .table(tableName)\n                  .writeStream()\n                  .option(\"checkpointLocation\", checkpoint())\n                  .foreachBatch(\n                      (VoidFunction2<Dataset<Row>, Long>) (df, id) -> result.addAll(ids(df)))\n                  .start();\n          try {\n            query.processAllAvailable();\n            assertThat(result).containsExactlyInAnyOrder(1, 2, 3);\n\n            sql(\"DELETE FROM %s WHERE id = 1\", tableName);\n            assertStreamingThrowsContaining(query::processAllAvailable, \"ignoreDeletes\");\n          } finally {\n            query.stop();\n          }\n        });\n  }\n\n  /**\n   * CDF streaming reads work for EXTERNAL tables but fail for MANAGED tables.\n   *\n   * <p>For EXTERNAL: verifies that inserts and a delete produce the expected typed change events.\n   * For MANAGED: verifies the stream fails with an error containing \"not supported\" and \"CDC\".\n   */\n  @TestAllTableTypes\n  public void testStreamingCDFRead(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_cdf_read_test\",\n        \"id INT\",\n        null,\n        tableType,\n        \"'delta.enableChangeDataFeed'='true'\",\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          long insertVersion = currentVersion(tableName);\n          sql(\"DELETE FROM %s WHERE id = 1\", tableName);\n\n          if (tableType == TableType.EXTERNAL) {\n            List<Row> changes = new ArrayList<>();\n            spark()\n                .readStream()\n                .format(\"delta\")\n                .option(\"readChangeFeed\", \"true\")\n                .option(\"startingVersion\", insertVersion)\n                .table(tableName)\n                .writeStream()\n                .trigger(Trigger.AvailableNow())\n                .option(\"checkpointLocation\", checkpoint())\n                .foreachBatch(\n                    (VoidFunction2<Dataset<Row>, Long>)\n                        (df, id) -> changes.addAll(df.select(\"id\", \"_change_type\").collectAsList()))\n                .start()\n                .awaitTermination();\n            assertThat(changes)\n                .extracting(r -> r.getString(1))\n                .containsExactlyInAnyOrder(\"insert\", \"insert\", \"insert\", \"delete\");\n            assertThat(changes)\n                .filteredOn(r -> \"delete\".equals(r.getString(1)))\n                .extracting(r -> r.getInt(0))\n                .containsExactly(1);\n          } else {\n            assertInvalidStreamOption(\n                tableName,\n                r -> r.option(\"readChangeFeed\", \"true\").option(\"startingVersion\", insertVersion),\n                \"not supported\",\n                \"CDC\");\n          }\n        });\n  }\n\n  /**\n   * Delta streaming sink does not support {@code complete} output mode. Verifies the stream fails\n   * with a clear error mentioning \"complete\".\n   */\n  @TestAllTableTypes\n  public void testStreamingWriteCompleteModeNotSupported(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_complete_mode_src\",\n        \"id INT\",\n        tableType,\n        srcName ->\n            withNewTable(\n                \"streaming_complete_mode_sink\",\n                \"id INT\",\n                tableType,\n                sinkName -> {\n                  sql(\"INSERT INTO %s VALUES (1), (2), (3)\", srcName);\n                  assertStreamingThrowsContaining(\n                      () ->\n                          spark()\n                              .readStream()\n                              .format(\"delta\")\n                              .table(srcName)\n                              .writeStream()\n                              .format(\"delta\")\n                              .outputMode(\"complete\")\n                              .trigger(Trigger.AvailableNow())\n                              .option(\"checkpointLocation\", checkpoint())\n                              .toTable(sinkName)\n                              .awaitTermination(),\n                      \"complete\");\n                }));\n  }\n\n  /**\n   * Verifies that invalid or unsupported streaming read options are rejected with clear errors.\n   *\n   * <ul>\n   *   <li>Negative {@code startingVersion} is rejected (both table types).\n   *   <li>{@code startingVersion} beyond the table history is rejected (both table types).\n   *   <li>{@code ignoreChanges} and {@code ignoreDeletes} fail with a \"not supported\" error for\n   *       MANAGED tables; EXTERNAL tables accept these options silently.\n   * </ul>\n   */\n  @TestAllTableTypes\n  public void testStreamingInvalidOptions(TableType tableType) throws Exception {\n    withNewTable(\n        \"streaming_invalid_options_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1)\", tableName);\n\n          assertInvalidStreamOption(\n              tableName, r -> r.option(\"startingVersion\", -1), \"startingVersion\");\n          assertInvalidStreamOption(tableName, r -> r.option(\"startingVersion\", 999999), \"999999\");\n\n          if (tableType == TableType.MANAGED) {\n            assertInvalidStreamOption(\n                tableName,\n                r -> r.option(\"ignoreChanges\", \"true\"),\n                \"not supported\",\n                \"ignoreChanges\");\n          }\n        });\n  }\n\n  /**\n   * Starts a streaming read with options applied by {@code configure}, then asserts the stream\n   * fails with a message containing all {@code fragments} (case-insensitive).\n   */\n  private void assertInvalidStreamOption(\n      String tableName,\n      Function<DataStreamReader, DataStreamReader> configure,\n      String... fragments) {\n    assertStreamingThrowsContaining(\n        () ->\n            configure\n                .apply(spark().readStream().format(\"delta\"))\n                .table(tableName)\n                .writeStream()\n                .trigger(Trigger.AvailableNow())\n                .option(\"checkpointLocation\", checkpoint())\n                .foreachBatch(NOOP_BATCH)\n                .start()\n                .awaitTermination(),\n        fragments);\n  }\n\n  /**\n   * Asserts that {@code action} throws an exception whose cause chain contains all the given {@code\n   * fragments} (case-insensitive).\n   */\n  private static void assertStreamingThrowsContaining(\n      ThrowingCallable action, String... fragments) {\n    assertThatThrownBy(action)\n        .satisfies(\n            e -> {\n              StringBuilder full = new StringBuilder();\n              for (Throwable t = e; t != null; t = t.getCause()) {\n                if (t.getMessage() != null) full.append(t.getMessage()).append(' ');\n              }\n              String msg = full.toString();\n              for (String fragment : fragments) {\n                assertThat(msg).containsIgnoringCase(fragment);\n              }\n            });\n  }\n\n  private String checkpoint() throws IOException {\n    Path ckDir = tempDir.resolve(\"ck-\" + checkpointCount++);\n    Files.createDirectory(ckDir);\n    return ckDir.toString();\n  }\n\n  private List<Integer> ids(Dataset<Row> df) {\n    return df.collectAsList().stream().map(r -> r.getInt(0)).collect(Collectors.toList());\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableDataFrameWriteTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.apache.spark.sql.functions.col;\nimport static org.apache.spark.sql.functions.lit;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.RowFactory;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.junit.jupiter.api.Assumptions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * DataFrame write test suite for Delta Table operations through Unity Catalog.\n *\n * <p>Covers DataFrame Writer V1 (insertInto, save) and Writer V2 (writeTo) operations. Tests run\n * against both EXTERNAL and MANAGED table types.\n */\npublic class UCDeltaTableDataFrameWriteTest extends UCDeltaTableIntegrationBaseTest {\n\n  // Writer V1: insertInto\n\n  @TestAllTableTypes\n  public void testInsertIntoAppend(TableType tableType) throws Exception {\n    withNewTable(\n        \"insert_into_append_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          intDf(4, 5).write().mode(\"append\").insertInto(tableName);\n          check(tableName, List.of(row(\"1\"), row(\"2\"), row(\"3\"), row(\"4\"), row(\"5\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testInsertIntoOverwrite(TableType tableType) throws Exception {\n    withNewTable(\n        \"insert_into_overwrite_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          intDf(9).write().mode(\"overwrite\").insertInto(tableName);\n          check(tableName, List.of(row(\"9\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testInsertIntoReplaceWhere(TableType tableType) throws Exception {\n    withNewTable(\n        \"insert_into_replace_where_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          intDf(9).write().mode(\"overwrite\").option(\"replaceWhere\", \"id > 1\").insertInto(tableName);\n          check(tableName, List.of(row(\"1\"), row(\"9\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testSaveAsTableAppend(TableType tableType) throws Exception {\n    withNewTable(\n        \"save_as_table_append_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          intDf(4, 5).write().format(\"delta\").mode(\"append\").saveAsTable(tableName);\n          check(tableName, List.of(row(\"1\"), row(\"2\"), row(\"3\"), row(\"4\"), row(\"5\")));\n        });\n  }\n\n  // TODO: Add saveAsTable overwrite/replaceWhere coverage once UCSingleCatalog supports REPLACE\n  // TABLE AS SELECT (RTAS). Currently, saveAsTable with mode(\"overwrite\") routes through Spark's\n  // V2 catalog path as RTAS, which throws UnsupportedOperationException in UCSingleCatalog.\n\n  @Test\n  public void testSaveByPathBlockedForManagedTable() throws Exception {\n    withNewTable(\n        \"save_path_blocked_test\",\n        \"id INT\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          String tablePath =\n              sql(\"DESCRIBE FORMATTED %s\", tableName).stream()\n                  .filter(r -> r.size() >= 2 && \"Location\".equalsIgnoreCase(r.get(0).trim()))\n                  .map(r -> r.get(1).trim())\n                  .findFirst()\n                  .orElseThrow();\n          assertThatThrownBy(() -> intDf(4).write().format(\"delta\").mode(\"append\").save(tablePath))\n              .satisfies(\n                  e ->\n                      assertThat(e.getMessage())\n                          .containsAnyOf(\n                              \"Unable to load credentials\",\n                              \"DELTA_PATH_BASED_ACCESS_TO_CATALOG_MANAGED_TABLE_BLOCKED\",\n                              \"Path-based access is not allowed\"));\n          check(tableName, List.of(row(\"1\"), row(\"2\"), row(\"3\")));\n        });\n  }\n\n  // Writer V2: writeTo\n\n  @TestAllTableTypes\n  public void testWriteToAppend(TableType tableType) throws Exception {\n    withNewTable(\n        \"write_to_append_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          intDf(4, 5).writeTo(tableName).append();\n          check(tableName, List.of(row(\"1\"), row(\"2\"), row(\"3\"), row(\"4\"), row(\"5\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testWriteToOverwrite(TableType tableType) throws Exception {\n    withNewTable(\n        \"write_to_overwrite_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          intDf(9).writeTo(tableName).overwrite(lit(true));\n          check(tableName, List.of(row(\"9\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testWriteToOverwriteWithCondition(TableType tableType) throws Exception {\n    withNewTable(\n        \"write_to_overwrite_condition_test\",\n        \"id INT, category STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'A'), (2, 'B'), (3, 'A')\", tableName);\n          spark()\n              .createDataFrame(\n                  List.of(RowFactory.create(9, \"A\")),\n                  new StructType()\n                      .add(\"id\", DataTypes.IntegerType)\n                      .add(\"category\", DataTypes.StringType))\n              .writeTo(tableName)\n              .overwrite(col(\"category\").equalTo(\"A\"));\n          check(tableName, List.of(row(\"2\", \"B\"), row(\"9\", \"A\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testWriteToOverwritePartitions(TableType tableType) throws Exception {\n    withNewTable(\n        \"write_to_overwrite_partitions_test\",\n        \"id INT, category STRING\",\n        \"category\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'A'), (2, 'A'), (3, 'B')\", tableName);\n          spark()\n              .createDataFrame(\n                  List.of(RowFactory.create(9, \"A\")),\n                  new StructType()\n                      .add(\"id\", DataTypes.IntegerType)\n                      .add(\"category\", DataTypes.StringType))\n              .writeTo(tableName)\n              .overwritePartitions();\n          // Only partition 'A' is replaced; partition 'B' remains untouched.\n          check(tableName, List.of(row(\"3\", \"B\"), row(\"9\", \"A\")));\n        });\n  }\n\n  @Test\n  public void testWriteToCreateNewManagedTable() throws Exception {\n    String tableName = fullTableName(\"write_to_create_test\");\n    try {\n      intDf(1, 2)\n          .writeTo(tableName)\n          .using(\"delta\")\n          .tableProperty(\"delta.feature.catalogManaged\", \"supported\")\n          .create();\n      check(tableName, List.of(row(\"1\"), row(\"2\")));\n    } finally {\n      sql(\"DROP TABLE IF EXISTS %s\", tableName);\n    }\n  }\n\n  @TestAllTableTypes\n  public void testMergeSchema(TableType tableType) throws Exception {\n    Assumptions.assumeFalse(\n        isUCRemoteConfigured(), \"mergeSchema not yet supported for UC managed tables remotely\");\n    if (tableType == TableType.MANAGED) {\n      // mergeSchema triggers updateMetadata() with a new schema, which the kill switch blocks\n      // on CatalogOwned tables. Assert the failure rather than skipping.\n      withNewTable(\n          \"merge_schema_blocked_test\",\n          \"id INT\",\n          tableType,\n          tableName -> {\n            sql(\"INSERT INTO %s VALUES (1), (2)\", tableName);\n            assertThrowsWithCauseContaining(\n                \"Metadata changes on Unity Catalog\",\n                () ->\n                    spark()\n                        .createDataFrame(\n                            List.of(RowFactory.create(3, \"extra\")),\n                            new StructType()\n                                .add(\"id\", DataTypes.IntegerType)\n                                .add(\"name\", DataTypes.StringType))\n                        .write()\n                        .format(\"delta\")\n                        .mode(\"append\")\n                        .option(\"mergeSchema\", \"true\")\n                        .saveAsTable(tableName));\n          });\n      return;\n    }\n    withNewTable(\n        \"merge_schema_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2)\", tableName);\n          spark()\n              .createDataFrame(\n                  List.of(RowFactory.create(3, \"extra\")),\n                  new StructType()\n                      .add(\"id\", DataTypes.IntegerType)\n                      .add(\"name\", DataTypes.StringType))\n              .write()\n              .format(\"delta\")\n              .mode(\"append\")\n              .option(\"mergeSchema\", \"true\")\n              .saveAsTable(tableName);\n          check(tableName, List.of(row(\"1\", \"null\"), row(\"2\", \"null\"), row(\"3\", \"extra\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testWriteToPartitionedTable(TableType tableType) throws Exception {\n    withNewTable(\n        \"df_partitioned_write_test\",\n        \"id INT, category STRING\",\n        \"category\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'A'), (2, 'B')\", tableName);\n          spark()\n              .createDataFrame(\n                  List.of(RowFactory.create(3, \"A\"), RowFactory.create(4, \"B\")),\n                  new StructType()\n                      .add(\"id\", DataTypes.IntegerType)\n                      .add(\"category\", DataTypes.StringType))\n              .write()\n              .mode(\"append\")\n              .insertInto(tableName);\n          check(tableName, List.of(row(\"1\", \"A\"), row(\"2\", \"B\"), row(\"3\", \"A\"), row(\"4\", \"B\")));\n        });\n  }\n\n  private Dataset<Row> intDf(Integer... ids) {\n    return spark()\n        .createDataFrame(\n            Arrays.stream(ids).map(RowFactory::create).collect(Collectors.toList()),\n            new StructType().add(\"id\", DataTypes.IntegerType));\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableDeltaAPITest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.apache.spark.sql.functions.col;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport io.delta.tables.DeltaTable;\nimport java.util.List;\nimport java.util.Map;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.RowFactory;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\n\n/**\n * Programmatic DeltaTable API test suite for Delta Table operations through Unity Catalog.\n *\n * <p>Covers DeltaTable.forName(), update(), delete(), merge(), history(), optimize(), and restore()\n * via the io.delta.tables.DeltaTable API. These go through different code paths than SQL\n * equivalents tested in UCDeltaTableDMLTest and UCDeltaUtilityTest. Tests run against both EXTERNAL\n * and MANAGED table types.\n */\npublic class UCDeltaTableDeltaAPITest extends UCDeltaTableIntegrationBaseTest {\n\n  @TestAllTableTypes\n  public void testForNameAndToDF(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_read\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          assertThat(forName(tableName).toDF().orderBy(\"id\").collectAsList())\n              .extracting(r -> r.getInt(0))\n              .containsExactly(1, 2, 3);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testUpdate(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_update\",\n        \"id INT, status STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'pending'), (2, 'pending'), (3, 'done')\", tableName);\n          forName(tableName).updateExpr(\"id = 1\", Map.of(\"status\", \"'processed'\"));\n          check(tableName, List.of(row(\"1\", \"processed\"), row(\"2\", \"pending\"), row(\"3\", \"done\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testDelete(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_delete\",\n        \"id INT, active BOOLEAN\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, true), (2, false), (3, true)\", tableName);\n          forName(tableName).delete(col(\"active\").equalTo(false));\n          check(tableName, List.of(row(\"1\", \"true\"), row(\"3\", \"true\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testMerge(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_merge\",\n        \"id INT, value STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'old1'), (2, 'old2')\", tableName);\n          Dataset<Row> source =\n              spark()\n                  .createDataFrame(\n                      List.of(RowFactory.create(2, \"updated2\"), RowFactory.create(3, \"new3\")),\n                      new StructType()\n                          .add(\"id\", DataTypes.IntegerType)\n                          .add(\"value\", DataTypes.StringType));\n          forName(tableName)\n              .as(\"target\")\n              .merge(source.as(\"source\"), \"target.id = source.id\")\n              .whenMatched()\n              .updateExpr(Map.of(\"value\", \"source.value\"))\n              .whenNotMatched()\n              .insertExpr(Map.of(\"id\", \"source.id\", \"value\", \"source.value\"))\n              .execute();\n          check(tableName, List.of(row(\"1\", \"old1\"), row(\"2\", \"updated2\"), row(\"3\", \"new3\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testHistory(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_history\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1)\", tableName);\n          sql(\"INSERT INTO %s VALUES (2)\", tableName);\n          // CREATE TABLE (v0) + 2 INSERTs (v1, v2)\n          assertThat(forName(tableName).history().collectAsList()).hasSize(3);\n        });\n  }\n\n  @TestAllTableTypes\n  public void testOptimize(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_optimize\",\n        \"id INT, category STRING\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1, 'A'), (2, 'B'), (3, 'A')\", tableName);\n          if (tableType == TableType.MANAGED) {\n            assertThatThrownBy(() -> forName(tableName).optimize().executeCompaction())\n                .hasMessageContaining(\"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\");\n          } else {\n            assertThat(forName(tableName).optimize().executeCompaction().collectAsList())\n                .isNotEmpty();\n            assertThat(forName(tableName).optimize().executeZOrderBy(\"category\").collectAsList())\n                .isNotEmpty();\n          }\n        });\n  }\n\n  @TestAllTableTypes\n  public void testRestoreToVersion(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_restore_version\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          long v1 = currentVersion(tableName);\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n          forName(tableName).restoreToVersion(v1);\n          check(tableName, List.of(row(\"1\"), row(\"2\"), row(\"3\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testRestoreToTimestamp(TableType tableType) throws Exception {\n    withNewTable(\n        \"dt_api_restore_ts\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n          String ts = currentTimestamp(tableName);\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n          forName(tableName).restoreToTimestamp(ts);\n          check(tableName, List.of(row(\"1\"), row(\"2\"), row(\"3\")));\n        });\n  }\n\n  private DeltaTable forName(String tableName) {\n    return DeltaTable.forName(spark(), tableName);\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableIntegrationBaseTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.SparkConf;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.SparkSession;\nimport org.assertj.core.api.ThrowableAssert.ThrowingCallable;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.DynamicContainer;\nimport org.junit.jupiter.api.DynamicTest;\nimport org.junit.jupiter.api.TestFactory;\nimport org.opentest4j.TestAbortedException;\n\n/**\n * Abstract base class for Unity Catalog + Delta Table integration tests.\n *\n * <p>This class provides a pluggable SQL execution framework via the SQLExecutor interface,\n * allowing tests to be written once and executed via different execution engines (e.g., Spark SQL,\n * JDBC, REST API, etc.).\n *\n * <p>Subclasses must provide an executor by implementing the getSqlExecutor method.\n */\npublic abstract class UCDeltaTableIntegrationBaseTest extends UnityCatalogSupport {\n  public static final List<TableType> ALL_TABLE_TYPES =\n      List.of(TableType.EXTERNAL, TableType.MANAGED);\n\n  /**\n   * Tests with this annotation will test against ALL_TABLE_TYPES. Example:\n   *\n   * <pre>{@code\n   * @TestAllTableTypes\n   * public void testAdvancedInsertOperations(TableType tableType)\n   * }</pre>\n   */\n  @Target(ElementType.METHOD)\n  @Retention(RetentionPolicy.RUNTIME)\n  public @interface TestAllTableTypes {}\n\n  /** Generate dynamic tests for all methods with @TestAllTableTypes to run with ALL_TABLE_TYPES. */\n  @TestFactory\n  Stream<DynamicContainer> allTableTypesTestsFactory() {\n    List<Method> methods =\n        Stream.of(this.getClass().getDeclaredMethods())\n            .filter(m -> m.isAnnotationPresent(TestAllTableTypes.class))\n            .collect(Collectors.toList());\n    List<DynamicContainer> containers = new ArrayList<>();\n    for (Method method : methods) {\n      List<DynamicTest> tests = new ArrayList<>();\n      for (TableType tableType : ALL_TABLE_TYPES) {\n        String testName = String.format(\"%s(%s)\", method.getName(), tableType);\n        tests.add(\n            DynamicTest.dynamicTest(\n                testName,\n                () -> {\n                  try {\n                    method.invoke(this, tableType);\n                  } catch (InvocationTargetException e) {\n                    // Unwrap so JUnit sees the original exception type. Without this,\n                    // TestAbortedException (thrown by Assumptions) gets wrapped and JUnit\n                    // treats the test as failed instead of skipped. Also unwrap\n                    // RuntimeException/Error so assertThrows() in individual tests still\n                    // matches the expected exception class rather than InvocationTargetException.\n                    Throwable cause = e.getCause();\n                    if (cause instanceof TestAbortedException) throw (TestAbortedException) cause;\n                    if (cause instanceof RuntimeException) throw (RuntimeException) cause;\n                    if (cause instanceof Error) throw (Error) cause;\n                    throw e;\n                  }\n                }));\n      }\n      containers.add(DynamicContainer.dynamicContainer(method.getName(), tests));\n    }\n    return containers.stream();\n  }\n\n  private SparkSession sparkSession;\n\n  /** Create the SparkSession before all tests. */\n  @BeforeAll\n  public void setUpSpark() {\n    // UC server is started by UnityCatalogSupport.setupServer()\n    // And the BeforeAll of parent class UnityCatalogSupport will be called before this method.\n\n    SparkConf conf =\n        new SparkConf()\n            .setAppName(\"UnityCatalog Integration Tests\")\n            .setMaster(\"local[2]\")\n            .set(\"spark.ui.enabled\", \"false\")\n            // Delta Lake required configurations\n            .set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n            .set(\n                \"spark.sql.catalog.spark_catalog\",\n                \"org.apache.spark.sql.delta.catalog.DeltaCatalog\");\n\n    // Configure with Unity Catalog\n    conf = configureSparkWithUnityCatalog(conf);\n\n    sparkSession = SparkSession.builder().config(conf).getOrCreate();\n  }\n\n  private SparkConf configureSparkWithUnityCatalog(SparkConf conf) {\n    // Use fake S3 filesystem for local testing; real S3A for remote UC.\n    if (isUCRemoteConfigured()) {\n      conf.set(\"spark.hadoop.fs.s3.impl\", \"org.apache.hadoop.fs.s3a.S3AFileSystem\");\n    } else {\n      conf.set(\"spark.hadoop.fs.s3.impl\", S3CredentialFileSystem.class.getName());\n    }\n\n    // Set the catalog specific configs.\n    UnityCatalogInfo uc = unityCatalogInfo();\n    String catalogName = uc.catalogName();\n    return conf.set(\"spark.sql.catalog.\" + catalogName, \"io.unitycatalog.spark.UCSingleCatalog\")\n        .set(\"spark.sql.catalog.\" + catalogName + \".uri\", uc.serverUri())\n        .set(\"spark.sql.catalog.\" + catalogName + \".token\", uc.serverToken());\n  }\n\n  /** Stop the SparkSession after all tests. */\n  @AfterAll\n  public void tearDownSpark() {\n    if (sparkSession != null) {\n      sparkSession.stop();\n      sparkSession = null;\n    }\n    // UC server is stopped by UnityCatalogSupport.tearDownServer()\n  }\n\n  /** Get the SparkSession for direct access (e.g., for streaming operations). */\n  protected SparkSession spark() {\n    return sparkSession;\n  }\n\n  /** Get the SQL executor. Private to force subclasses to use sql() and check() methods. */\n  private SQLExecutor getSqlExecutor() {\n    return new SparkSQLExecutor(sparkSession);\n  }\n\n  /**\n   * Execute SQL through the SQL executor and return results.\n   *\n   * <p>When called with arguments, formats the SQL query using String.format:\n   *\n   * <pre>\n   * sql(\"INSERT INTO %s VALUES (%d, '%s')\", tableName, 1, \"value\")\n   * </pre>\n   *\n   * <p>When called without arguments, executes the SQL as-is:\n   *\n   * <pre>\n   * sql(\"CREATE TABLE test (id INT)\")\n   * </pre>\n   *\n   * @param sqlQuery SQL query with optional format specifiers (e.g., \"SELECT * FROM %s WHERE id =\n   *     %d\")\n   * @param args Arguments to be formatted into the SQL query\n   * @return List of result rows, each row is a list of string values\n   */\n  protected List<List<String>> sql(String sqlQuery, Object... args) {\n    String formattedQuery = args.length > 0 ? String.format(sqlQuery, args) : sqlQuery;\n    return getSqlExecutor().runSQL(formattedQuery);\n  }\n\n  /**\n   * Verify table contents by selecting all rows ordered by the first column.\n   *\n   * @param tableName The fully qualified table name\n   * @param expected The expected results as a list of rows\n   */\n  protected void check(String tableName, List<List<String>> expected) {\n    getSqlExecutor().checkWithSQL(\"SELECT * FROM \" + tableName + \" ORDER BY 1\", expected);\n  }\n\n  /** Helper method to run code with a temporary directory that gets cleaned up. */\n  protected void withTempDir(TempDirCode code) throws Exception {\n    UnityCatalogInfo uc = unityCatalogInfo();\n    Path tempDir = new Path(uc.baseTableLocation(), \"temp-\" + UUID.randomUUID());\n    code.run(tempDir);\n  }\n\n  /** Table types for parameterized testing. */\n  public enum TableType {\n    EXTERNAL, // Requires LOCATION clause\n    MANAGED // No LOCATION clause (Spark manages the data)\n  }\n\n  /**\n   * Helper method to create a new Delta table, run test code, and clean up.\n   *\n   * @param tableName The simple table name (without catalog/schema prefix)\n   * @param tableSchema The table schema (e.g., \"id INT, name STRING\")\n   * @param partitionFields The partition fields (e.g., \"id, name\")\n   * @param tableType The type of table (EXTERNAL or MANAGED)\n   * @param tableProperties Additional table properties (e.g., \"delta.enableChangeDataFeed\"=\"true\")\n   * @param testCode The test function that receives the full table name\n   */\n  protected void withNewTable(\n      String tableName,\n      String tableSchema,\n      String partitionFields,\n      TableType tableType,\n      String tableProperties,\n      TestCode testCode)\n      throws Exception {\n    String fullTableName = fullTableName(tableName);\n\n    // Create th partition cause.\n    StringBuilder partitionCause = new StringBuilder();\n    if (partitionFields != null && !partitionFields.trim().isEmpty()) {\n      partitionCause.append(String.format(\"PARTITIONED BY (%s)\", partitionFields));\n    }\n\n    // Build table properties clause\n    StringBuilder tblPropertiesClause = new StringBuilder();\n    if (tableType == TableType.MANAGED) {\n      tblPropertiesClause.append(\"'delta.feature.catalogManaged'='supported'\");\n    }\n    if (tableProperties != null && !tableProperties.trim().isEmpty()) {\n      if (tblPropertiesClause.length() > 0) {\n        tblPropertiesClause.append(\", \");\n      }\n      tblPropertiesClause.append(tableProperties);\n    }\n\n    final String tblPropertiesSql;\n    if (tblPropertiesClause.length() > 0) {\n      tblPropertiesSql = \"TBLPROPERTIES (\" + tblPropertiesClause + \")\";\n    } else {\n      tblPropertiesSql = \"\";\n    }\n\n    if (tableType == TableType.EXTERNAL) {\n      // External table requires a location\n      withTempDir(\n          (Path dir) -> {\n            Path tablePath = new Path(dir, tableName);\n            sql(\n                \"CREATE TABLE %s (%s) USING DELTA %s %s LOCATION '%s'\",\n                fullTableName,\n                tableSchema,\n                partitionCause.toString(),\n                tblPropertiesSql,\n                tablePath.toString());\n\n            try {\n              testCode.run(fullTableName);\n            } finally {\n              sql(\"DROP TABLE IF EXISTS %s\", fullTableName);\n            }\n          });\n    } else {\n      // Managed table - Spark manages the location\n      // Unity Catalog requires 'delta.feature.catalogManaged'='supported' for managed tables\n      sql(\n          \"CREATE TABLE %s (%s) USING DELTA %s %s\",\n          fullTableName, tableSchema, partitionCause.toString(), tblPropertiesSql);\n\n      try {\n        testCode.run(fullTableName);\n      } finally {\n        sql(\"DROP TABLE IF EXISTS %s\", fullTableName);\n      }\n    }\n  }\n\n  /**\n   * Helper method to create a new Delta table, run test code, and clean up.\n   *\n   * @param tableName The simple table name (without catalog/schema prefix)\n   * @param tableSchema The table schema (e.g., \"id INT, name STRING\")\n   * @param partitionFields The partition fields (e.g., \"id, name\")\n   * @param tableType The type of table (EXTERNAL or MANAGED)\n   * @param testCode The test function that receives the full table name\n   */\n  protected void withNewTable(\n      String tableName,\n      String tableSchema,\n      String partitionFields,\n      TableType tableType,\n      TestCode testCode)\n      throws Exception {\n    withNewTable(tableName, tableSchema, partitionFields, tableType, null, testCode);\n  }\n\n  /**\n   * Helper method to create a new Delta table, run test code, and clean up.\n   *\n   * @param tableName The simple table name (without catalog/schema prefix)\n   * @param tableSchema The table schema (e.g., \"id INT, name STRING\")\n   * @param tableType The type of table (EXTERNAL or MANAGED)\n   * @param testCode The test function that receives the full table name\n   */\n  protected void withNewTable(\n      String tableName, String tableSchema, TableType tableType, TestCode testCode)\n      throws Exception {\n    withNewTable(tableName, tableSchema, null, tableType, testCode);\n  }\n\n  /** Returns the fully qualified table name for a given simple table name. */\n  protected String fullTableName(String simpleName) {\n    UnityCatalogInfo uc = unityCatalogInfo();\n    return uc.catalogName() + \".\" + uc.schemaName() + \".\" + simpleName;\n  }\n\n  /** Returns the current (latest) version of the table. */\n  protected long currentVersion(String tableName) {\n    return Long.parseLong(sql(\"DESCRIBE HISTORY %s LIMIT 1\", tableName).get(0).get(0));\n  }\n\n  /** Returns the timestamp of the current (latest) version. */\n  protected String currentTimestamp(String tableName) {\n    return sql(\"DESCRIBE HISTORY %s LIMIT 1\", tableName).get(0).get(1);\n  }\n\n  /**\n   * Asserts that the given operation throws an exception whose cause chain contains {@code\n   * expectedMessage}.\n   */\n  protected void assertThrowsWithCauseContaining(\n      String expectedMessage, ThrowingCallable operation) {\n    assertThatThrownBy(operation)\n        .satisfies(\n            e -> {\n              Throwable t = e;\n              while (t != null) {\n                if (t.getMessage() != null && t.getMessage().contains(expectedMessage)) {\n                  return;\n                }\n                t = t.getCause();\n              }\n              throw new AssertionError(\n                  \"Expected exception containing '\"\n                      + expectedMessage\n                      + \"' in cause chain, but none found. Top-level: \"\n                      + e,\n                  e);\n            });\n  }\n\n  /** Helper to build an expected row as a list of string values. */\n  protected static List<String> row(String... values) {\n    return List.of(values);\n  }\n\n  /** Functional interface for test code that takes a temporary directory. */\n  @FunctionalInterface\n  protected interface TempDirCode {\n\n    void run(Path dir) throws Exception;\n  }\n\n  /** Functional interface for test code that takes a table name parameter. */\n  @FunctionalInterface\n  protected interface TestCode {\n\n    void run(String tableName) throws Exception;\n  }\n\n  /**\n   * Interface defining the interface for executing SQL and verifying results.\n   *\n   * <p>This abstraction allows tests to be independent of the execution engine, making it easy to\n   * test the same logic via different interfaces (Spark SQL, JDBC, etc.).\n   */\n  public interface SQLExecutor {\n\n    /**\n     * Execute a SQL statement and return the results.\n     *\n     * @param sql The SQL statement to execute\n     * @return The query results as a list of rows, where each row is a list of strings\n     */\n    List<List<String>> runSQL(String sql);\n\n    /**\n     * Execute a SQL query and verify the results match the expected output.\n     *\n     * @param sql The SQL query to execute\n     * @param expected The expected results as a list of rows\n     */\n    void checkWithSQL(String sql, List<List<String>> expected);\n  }\n\n  /**\n   * Default SQL executor implementation using SparkSession.\n   *\n   * <p>This executor runs all SQL queries through Spark SQL and converts results to string lists\n   * for easy comparison.\n   */\n  public static class SparkSQLExecutor implements SQLExecutor {\n\n    private final SparkSession spark;\n\n    public SparkSQLExecutor(SparkSession spark) {\n      this.spark = spark;\n    }\n\n    @Override\n    public List<List<String>> runSQL(String sql) {\n      Dataset<Row> df = spark.sql(sql);\n      Row[] rows = (Row[]) df.collect();\n      return Arrays.stream(rows)\n          .map(\n              row -> {\n                List<String> cells = new java.util.ArrayList<>();\n                for (int i = 0; i < row.length(); i++) {\n                  cells.add(row.isNullAt(i) ? \"null\" : row.get(i).toString());\n                }\n                return cells;\n              })\n          .collect(Collectors.toList());\n    }\n\n    @Override\n    public void checkWithSQL(String sql, List<List<String>> expected) {\n      List<List<String>> actual = runSQL(sql);\n      if (!actual.equals(expected)) {\n        throw new AssertionError(\n            String.format(\n                \"Query results do not match.\\nSQL: %s\\n Expected: %s\\nActual: %s\",\n                sql, expected, actual));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaTableReadTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Assertions;\n\n/**\n * Read operation test suite for Delta Table operations through Unity Catalog.\n *\n * <p>Covers time travel, change data feed, and path-based access scenarios. Tests are parameterized\n * to support different table types (EXTERNAL and MANAGED).\n */\npublic class UCDeltaTableReadTest extends UCDeltaTableIntegrationBaseTest {\n\n  @TestAllTableTypes\n  public void testTimeTravelRead(TableType tableType) throws Exception {\n    withNewTable(\n        \"time_travel_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          // Setup initial data\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n\n          // Get current version and timestamp.\n          long currentVersion = currentVersion(tableName);\n          String currentTimestamp = currentTimestamp(tableName);\n\n          // Add more data\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n\n          // Test VERSION AS OF with SQL syntax\n          List<List<String>> versionResult =\n              sql(\"SELECT * FROM %s VERSION AS OF %d ORDER BY id\", tableName, currentVersion);\n          check(versionResult, List.of(List.of(\"1\"), List.of(\"2\"), List.of(\"3\")));\n\n          // Test TIMESTAMP AS OF with SQL syntax\n          List<List<String>> timestampResult =\n              sql(\"SELECT * FROM %s TIMESTAMP AS OF '%s' ORDER BY id\", tableName, currentTimestamp);\n          check(timestampResult, List.of(List.of(\"1\"), List.of(\"2\"), List.of(\"3\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testChangeDataFeed(TableType tableType) throws Exception {\n    withNewTable(\n        \"cdf_timestamp_test\",\n        \"id INT\",\n        null,\n        tableType,\n        \"'delta.enableChangeDataFeed'='true'\",\n        tableName -> {\n          // Setup initial data (creates version 0)\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n\n          // Add more data (creates version 1)\n          sql(\"INSERT INTO %s VALUES (4), (5)\", tableName);\n\n          // Get current version and timestamp (both for version 1)\n          long currentVersion = currentVersion(tableName);\n          String currentTimestamp = currentTimestamp(tableName);\n\n          // Query changes from version 1 (the second insert)\n          check(\n              sql(\n                  \"SELECT id, _change_type FROM table_changes('%s', %d) ORDER BY id\",\n                  tableName, currentVersion),\n              List.of(List.of(\"4\", \"insert\"), List.of(\"5\", \"insert\")));\n\n          // Query changes from the timestamp of version 1\n          check(\n              sql(\n                  \"SELECT id, _change_type FROM table_changes('%s', '%s') ORDER BY id\",\n                  tableName, currentTimestamp),\n              List.of(List.of(\"4\", \"insert\"), List.of(\"5\", \"insert\")));\n        });\n  }\n\n  @TestAllTableTypes\n  public void testDeltaTableForPath(TableType tableType) throws Exception {\n    withNewTable(\n        \"delta_table_for_path_test\",\n        \"id INT\",\n        tableType,\n        tableName -> {\n          // Setup initial data\n          sql(\"INSERT INTO %s VALUES (1), (2), (3)\", tableName);\n\n          // Get table path\n          List<List<String>> describeResult = sql(\"DESCRIBE EXTENDED %s\", tableName);\n\n          // Find the Location row in the describe output\n          String tablePath =\n              describeResult.stream()\n                  .filter(row -> row.size() >= 2 && \"Location\".equals(row.get(0)))\n                  .map(row -> row.get(1))\n                  .findFirst()\n                  .orElse(null);\n          Assertions.assertTrue(\n              tablePath != null && !tablePath.isEmpty(),\n              \"Could not retrieve table location from DESCRIBE EXTENDED\");\n\n          // Path-based access isn't supported for catalog-owned (MANAGED) tables.\n          if (tableType == TableType.MANAGED) {\n            Assertions.assertThrows(\n                Exception.class,\n                () -> sql(\"SELECT * FROM delta.`%s`\", tablePath),\n                \"For managed tables, path-based access should fail\");\n          } else {\n            // For EXTERNAL tables, path-based access should work\n            S3CredentialFileSystem.credentialCheckEnabled = false;\n            try {\n              check(\n                  sql(\"SELECT * FROM delta.`%s` ORDER BY id\", tablePath),\n                  List.of(List.of(\"1\"), List.of(\"2\"), List.of(\"3\")));\n            } finally {\n              S3CredentialFileSystem.credentialCheckEnabled = true;\n            }\n          }\n        });\n  }\n\n  private void check(List<List<String>> actual, List<List<String>> expected) {\n    if (!actual.equals(expected)) {\n      throw new AssertionError(\n          String.format(\"Query results do not match.\\nExpected: %s\\nActual: %s\", expected, actual));\n    }\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UCDeltaUtilityTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.assertj.core.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class UCDeltaUtilityTest extends UCDeltaTableIntegrationBaseTest {\n\n  @TestAllTableTypes\n  public void testDescribeHistory(TableType tableType) throws Exception {\n    withNewTable(\n        \"describe_history\",\n        \"id INT, name STRING\",\n        tableType,\n        tableName -> {\n          // Assert the initial history.\n          assertDescribeHistory(tableName, List.of(List.of(\"0\", \"CREATE TABLE\", \"Serializable\")));\n\n          // The 1st operation.\n          sql(\"INSERT INTO %s VALUES (1, 'AAA')\", tableName);\n          check(tableName, List.of(List.of(\"1\", \"AAA\")));\n          // Assert the history.\n          assertDescribeHistory(\n              tableName,\n              List.of(\n                  List.of(\"1\", \"WRITE\", \"Serializable\"),\n                  List.of(\"0\", \"CREATE TABLE\", \"Serializable\")));\n\n          // The 2nd operation.\n          sql(\"UPDATE %s SET name='BBB' WHERE id = 1\", tableName, tableName);\n          check(tableName, List.of(List.of(\"1\", \"BBB\")));\n          // Assert the history\n          assertDescribeHistory(\n              tableName,\n              List.of(\n                  List.of(\"2\", \"UPDATE\", \"Serializable\"),\n                  List.of(\"1\", \"WRITE\", \"Serializable\"),\n                  List.of(\"0\", \"CREATE TABLE\", \"Serializable\")));\n        });\n  }\n\n  private void assertDescribeHistory(String tableName, List<List<String>> expected) {\n    List<List<String>> results = sql(\"DESCRIBE HISTORY %s\", tableName);\n\n    // Only assert below columns, since other columns are null or undetermined (such as timestamp).\n    // index  0: version\n    // index  4: operation\n    // index 10: isolationLevel\n    List<List<String>> prunedResults = new ArrayList<>();\n    for (List<String> row : results) {\n      prunedResults.add(List.of(row.get(0), row.get(4), row.get(10)));\n    }\n\n    Assertions.assertThat(prunedResults).isEqualTo(expected);\n  }\n\n  @TestAllTableTypes\n  public void testFsPropertiesHiddenFromTableProperties(TableType tableType) throws Exception {\n    withNewTable(\n        \"fs_props_hidden\",\n        \"id INT, name STRING\",\n        null, // no partition\n        tableType,\n        \"'myCustomProp'='myCustomValue'\",\n        tableName -> {\n          // SHOW TBLPROPERTIES returns one row per property (key, value).\n          List<List<String>> propRows = sql(\"SHOW TBLPROPERTIES %s\", tableName);\n          List<String> propKeys = new ArrayList<>();\n          for (List<String> row : propRows) {\n            propKeys.add(row.get(0));\n          }\n\n          // Verify no key starts with option.fs. — these are internal catalog-vended\n          // credentials/metadata that should not be user-visible.\n          for (String key : propKeys) {\n            Assertions.assertThat(key)\n                .as(\"SHOW TBLPROPERTIES should not expose option.fs.* keys\")\n                .doesNotStartWith(\"option.fs.\");\n          }\n\n          // Verify that non-fs storage properties and user-set table properties ARE\n          // still present — confirming the filter is selective, not a blanket removal.\n          Assertions.assertThat(propKeys)\n              .as(\"User-set table properties should still be visible\")\n              .contains(\"myCustomProp\");\n          Assertions.assertThat(propKeys)\n              .as(\"Delta table properties should still be visible\")\n              .contains(\"delta.minReaderVersion\");\n\n          // DESCRIBE EXTENDED returns a \"Table Properties\" row with all properties\n          // in a single string like \"[key1=val1,key2=val2,...]\".\n          boolean foundTableProperties = false;\n          List<List<String>> descRows = sql(\"DESCRIBE EXTENDED %s\", tableName);\n          for (List<String> row : descRows) {\n            if (row.size() >= 2 && \"Table Properties\".equals(row.get(0))) {\n              foundTableProperties = true;\n              Assertions.assertThat(row.get(1))\n                  .as(\"DESCRIBE EXTENDED should not expose option.fs.* storage properties\")\n                  .doesNotContain(\"option.fs.\");\n              Assertions.assertThat(row.get(1))\n                  .as(\"DESCRIBE EXTENDED should not expose fs.* storage properties either\")\n                  .doesNotContain(\"fs.\");\n              Assertions.assertThat(row.get(1))\n                  .as(\"DESCRIBE EXTENDED should still show user-set properties\")\n                  .contains(\"myCustomProp=myCustomValue\");\n            }\n          }\n          Assertions.assertThat(foundTableProperties)\n              .as(\"DESCRIBE EXTENDED must include a 'Table Properties' row\")\n              .isTrue();\n\n          // Verify the data path still works — credentials still flow to the filesystem\n          // via CatalogTable.storage.properties even though they are hidden from properties().\n          sql(\"INSERT INTO %s VALUES (1, 'hello'), (2, 'world')\", tableName);\n          check(tableName, List.of(List.of(\"1\", \"hello\"), List.of(\"2\", \"world\")));\n          sql(\"INSERT INTO %s VALUES (3, 'foo')\", tableName);\n          check(\n              tableName,\n              List.of(List.of(\"1\", \"hello\"), List.of(\"2\", \"world\"), List.of(\"3\", \"foo\")));\n        });\n  }\n\n  @Test\n  public void testMaintenanceOpsBlockedOnManagedTable() throws Exception {\n    withNewTable(\n        \"maintenance_blocked\",\n        \"id INT\",\n        TableType.MANAGED,\n        tableName -> {\n          sql(\"INSERT INTO %s VALUES (1)\", tableName);\n\n          assertThatThrownBy(() -> sql(\"OPTIMIZE %s\", tableName))\n              .hasMessageContaining(\"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\")\n              .hasMessageContaining(\"OPTIMIZE\");\n\n          assertThatThrownBy(() -> sql(\"VACUUM %s\", tableName))\n              .hasMessageContaining(\"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\")\n              .hasMessageContaining(\"VACUUM\");\n\n          assertThatThrownBy(() -> sql(\"REORG TABLE %s APPLY (PURGE)\", tableName))\n              .hasMessageContaining(\"DELTA_UNSUPPORTED_CATALOG_MANAGED_TABLE_OPERATION\")\n              .hasMessageContaining(\"OPTIMIZE\");\n        });\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UnityCatalogSupport.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableMap;\nimport io.unitycatalog.client.ApiClient;\nimport io.unitycatalog.client.ApiClientBuilder;\nimport io.unitycatalog.client.VersionUtils;\nimport io.unitycatalog.client.api.CatalogsApi;\nimport io.unitycatalog.client.api.SchemasApi;\nimport io.unitycatalog.client.auth.TokenProvider;\nimport io.unitycatalog.client.model.CreateCatalog;\nimport io.unitycatalog.client.model.CreateSchema;\nimport io.unitycatalog.server.UnityCatalogServer;\nimport io.unitycatalog.server.utils.ServerProperties;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.ServerSocket;\nimport java.nio.file.Files;\nimport java.util.Properties;\nimport org.apache.commons.io.FileUtils;\nimport org.apache.log4j.Logger;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.TestInstance;\n\n/**\n * Abstract base class that provides Unity Catalog server integration for Delta tests.\n *\n * <p>Automatically starts a local Unity Catalog server before tests and stops it after. To use a\n * remote server instead, set {@code UC_REMOTE=true} and configure {@code UC_URI}, {@code UC_TOKEN},\n * {@code UC_CATALOG_NAME}, {@code UC_SCHEMA_NAME}, and {@code UC_BASE_TABLE_LOCATION}.\n *\n * <p>{@code unityCatalogInfo()} is the only API for subclasses, All other methods are internal\n * implementation details.\n *\n * <pre>\n * public class MyUCTest extends UnityCatalogSupport {\n *   {@literal @}Test\n *   public void myTest() {\n *     UnityCatalogInfo ucInfo = unityCatalogInfo();\n *     String tableName = ucInfo.catalogName() + \".\" + ucInfo.schemaName() + \".my_table\";\n *     spark.sql(\"CREATE TABLE \" + tableName + \" (id INT) USING DELTA\");\n *   }\n * }\n * </pre>\n */\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic abstract class UnityCatalogSupport {\n\n  private static final Logger LOG = Logger.getLogger(UnityCatalogSupport.class);\n\n  protected static class UnityCatalogInfo {\n\n    private final String serverUri;\n    private final String catalogName;\n    private final String serverToken;\n    private final String schemaName;\n    private final String baseTableLocation;\n\n    public UnityCatalogInfo(\n        String serverUri,\n        String catalogName,\n        String serverToken,\n        String schemaName,\n        String baseTableLocation) {\n      this.serverUri = serverUri;\n      this.catalogName = catalogName;\n      this.serverToken = serverToken;\n      this.schemaName = schemaName;\n      this.baseTableLocation = baseTableLocation;\n    }\n\n    public String serverUri() {\n      return serverUri;\n    }\n\n    public String catalogName() {\n      return catalogName;\n    }\n\n    public String serverToken() {\n      return serverToken;\n    }\n\n    public String schemaName() {\n      return schemaName;\n    }\n\n    public String baseTableLocation() {\n      return baseTableLocation;\n    }\n\n    /** Creates a configured Unity Catalog API client. */\n    public ApiClient createApiClient() {\n      return ApiClientBuilder.create()\n          .uri(serverUri)\n          .tokenProvider(\n              TokenProvider.create(ImmutableMap.of(\"type\", \"static\", \"token\", serverToken)))\n          .build();\n    }\n  }\n\n  public static final String UC_STATIC_TOKEN = \"static-token\";\n\n  /** The fake S3 bucket name used for local integration tests. */\n  static final String FAKE_S3_BUCKET = \"fakeS3Bucket\";\n\n  // Environment variables for configuring access to remote unity catalog server.\n  public static final String UC_REMOTE = \"UC_REMOTE\";\n  public static final String UC_URI = \"UC_URI\";\n  public static final String UC_TOKEN = \"UC_TOKEN\";\n  public static final String UC_CATALOG_NAME = \"UC_CATALOG_NAME\";\n  public static final String UC_SCHEMA_NAME = \"UC_SCHEMA_NAME\";\n  public static final String UC_BASE_TABLE_LOCATION = \"UC_BASE_TABLE_LOCATION\";\n\n  protected static boolean isUCRemoteConfigured() {\n    String ucRemote = System.getenv(UC_REMOTE);\n    return ucRemote != null && ucRemote.equalsIgnoreCase(\"true\");\n  }\n\n  /** The Unity Catalog info instance for subclasses access */\n  private UnityCatalogInfo ucInfo = null;\n\n  /** The Unity Catalog server instance. */\n  private UnityCatalogServer ucServer;\n\n  /** The port on which the UC server is running. */\n  private int ucServerPort;\n\n  /** The temporary directory for UC server data. */\n  private File ucServerDir;\n\n  /** The temporary directory for external table location */\n  private File ucBaseTableLocation = null;\n\n  /**\n   * Returns the Unity Catalog configuration for use in tests.\n   *\n   * <p>This is the primary method subclasses should use to access Unity Catalog connection details,\n   * authentication tokens, and storage locations.\n   *\n   * <p><strong>Note:</strong> This is the only public API intended for subclasses. All other\n   * methods are internal implementation details.\n   *\n   * @return the Unity Catalog configuration\n   * @see UnityCatalogInfo\n   */\n  protected synchronized UnityCatalogInfo unityCatalogInfo() {\n    Preconditions.checkNotNull(\n        ucInfo,\n        \"No UnityCatalogInfo available, please make sure the unity catalog server is available\");\n    return ucInfo;\n  }\n\n  private UnityCatalogInfo remoteUnityCatalogInfo() {\n    String serverUri = System.getenv(UC_URI);\n    String catalogName = System.getenv(UC_CATALOG_NAME);\n    String serverToken = System.getenv(UC_TOKEN);\n    String schemaName = System.getenv(UC_SCHEMA_NAME);\n    String baseTableLocation = System.getenv(UC_BASE_TABLE_LOCATION);\n    Preconditions.checkNotNull(serverUri, \"%s must be set when UC_REMOTE=true\", UC_URI);\n    Preconditions.checkNotNull(catalogName, \"%s must be set when UC_REMOTE=true\", UC_CATALOG_NAME);\n    Preconditions.checkNotNull(serverToken, \"%s must be set when UC_REMOTE=true\", UC_TOKEN);\n    Preconditions.checkNotNull(schemaName, \"%s must be set when UC_REMOTE=true\", UC_SCHEMA_NAME);\n    Preconditions.checkNotNull(\n        baseTableLocation, \"%s must be set when UC_REMOTE=true\", UC_BASE_TABLE_LOCATION);\n    return new UnityCatalogInfo(serverUri, catalogName, serverToken, schemaName, baseTableLocation);\n  }\n\n  private UnityCatalogInfo localUnityCatalogInfo() {\n    Preconditions.checkNotNull(ucServer, \"Local Unity Catalog Server is not configured\");\n    Preconditions.checkNotNull(\n        ucBaseTableLocation, \"Local Unity Catalog Temp Directory is not configured\");\n    // Use fake S3 bucket (backed by local filesystem via S3CredentialFileSystem).\n    return new UnityCatalogInfo(\n        String.format(\"http://localhost:%s/\", ucServerPort),\n        \"unity\",\n        UC_STATIC_TOKEN,\n        \"default\",\n        \"s3://\" + FAKE_S3_BUCKET + ucBaseTableLocation.getAbsolutePath());\n  }\n\n  /** Finds an available port for the UC server. */\n  private int findAvailablePort() throws IOException {\n    try (ServerSocket socket = new ServerSocket(0)) {\n      return socket.getLocalPort();\n    }\n  }\n\n  /**\n   * Starts the Unity Catalog server before all tests. IMPORTANT: Starts the server BEFORE calling\n   * other setup to ensure the server is running when SharedSparkSession creates the SparkSession.\n   */\n  @BeforeAll\n  public void setupServer() throws Exception {\n    if (isUCRemoteConfigured()) {\n      setUpRemoteServer();\n    } else {\n      setUpLocalServer();\n    }\n  }\n\n  private void setUpRemoteServer() {\n    // For remote UC, log the configuration\n    ucInfo = remoteUnityCatalogInfo();\n    LOG.info(\"Using remote Unity Catalog server at \" + ucInfo.serverUri());\n    LOG.info(\"Catalog: \" + ucInfo.catalogName() + \", Schema: \" + ucInfo.schemaName());\n    LOG.info(\"Base location: \" + ucInfo.baseTableLocation());\n    LOG.info(\n        \"Note: Schema '\"\n            + ucInfo.catalogName()\n            + \".\"\n            + ucInfo.schemaName()\n            + \"' must already exist in the remote UC server\");\n  }\n\n  private void setUpLocalServer() throws Exception {\n    // Create temporary directory for UC server data\n    ucServerDir = Files.createTempDirectory(\"unity-catalog-test-\").toFile();\n    ucServerDir.deleteOnExit();\n\n    // Create temporary directory for external tables testing.\n    ucBaseTableLocation = Files.createTempDirectory(\"base-table-location-\").toFile();\n    ucBaseTableLocation.deleteOnExit();\n\n    // Find an available port\n    ucServerPort = findAvailablePort();\n\n    // Set up server properties\n    Properties serverProps = new Properties();\n    serverProps.setProperty(\"server.env\", \"test\");\n    // Enable managed tables (experimental feature in Unity Catalog)\n    serverProps.setProperty(\"server.managed-table.enabled\", \"true\");\n    serverProps.setProperty(\n        \"storage-root.tables\", new File(ucServerDir, \"ucroot\").getAbsolutePath());\n\n    // Configure S3 credentials for the fake bucket (mirrors UC OSS BaseSparkIntegrationTest).\n    serverProps.setProperty(\"s3.bucketPath.0\", \"s3://\" + FAKE_S3_BUCKET);\n    serverProps.setProperty(\"s3.accessKey.0\", \"fakeAccessKey\");\n    serverProps.setProperty(\"s3.secretKey.0\", \"fakeSecretKey\");\n    serverProps.setProperty(\"s3.sessionToken.0\", \"fakeSessionToken\");\n\n    // Start UC server with configuration\n    ServerProperties initServerProperties = new ServerProperties(serverProps);\n\n    UnityCatalogServer server =\n        UnityCatalogServer.builder()\n            .port(ucServerPort)\n            .serverProperties(initServerProperties)\n            .build();\n\n    server.start();\n    ucServer = server;\n\n    // Poll for server readiness by checking if we can create an API client and query catalogs\n    int maxRetries = 30;\n    int retryDelayMs = 500;\n    boolean serverReady = false;\n    int retries = 0;\n\n    ucInfo = localUnityCatalogInfo();\n    while (!serverReady && retries < maxRetries) {\n      try {\n        CatalogsApi catalogsApi = new CatalogsApi(ucInfo.createApiClient());\n        catalogsApi.listCatalogs(null, null); // This will throw if server is not ready\n        serverReady = true;\n      } catch (Exception e) {\n        Thread.sleep(retryDelayMs);\n        retries++;\n      }\n    }\n\n    if (!serverReady) {\n      throw new RuntimeException(\n          \"Unity Catalog server did not become ready after \" + (maxRetries * retryDelayMs) + \"ms\");\n    }\n\n    // Create the catalog and default schema in the UC server\n    ApiClient client = ucInfo.createApiClient();\n\n    CatalogsApi catalogsApi = new CatalogsApi(client);\n    SchemasApi schemasApi = new SchemasApi(client);\n\n    // Create catalog\n    catalogsApi.createCatalog(\n        new CreateCatalog()\n            .name(ucInfo.catalogName())\n            .comment(\"Test catalog for Delta Lake integration\"));\n\n    // Create default schema\n    schemasApi.createSchema(new CreateSchema().name(\"default\").catalogName(ucInfo.catalogName()));\n\n    LOG.info(\"Unity Catalog server started and ready at \" + ucInfo.serverUri());\n    LOG.info(\"Created catalog '\" + ucInfo.catalogName() + \"' with schema 'default'\");\n  }\n\n  /** Stops the Unity Catalog server after all tests. */\n  @AfterAll\n  public void tearDownServer() {\n    if (isUCRemoteConfigured()) {\n      return;\n    }\n\n    if (ucServer != null) {\n      ucServer.stop();\n      LOG.info(\"Unity Catalog server stopped\");\n      ucServer = null;\n    }\n\n    // Clean up uc server temporary directory\n    if (ucServerDir != null && ucServerDir.exists()) {\n      deleteRecursively(ucServerDir);\n    }\n\n    // Clear up base table locations.\n    if (ucBaseTableLocation != null && ucBaseTableLocation.exists()) {\n      deleteRecursively(ucBaseTableLocation);\n    }\n  }\n\n  /** Recursively deletes a directory and all its contents. */\n  private void deleteRecursively(File file) {\n    FileUtils.deleteQuietly(file);\n  }\n\n  /** Returns Unity Catalog Spark version, like [0, 4, 0]. */\n  protected static int[] getUnityCatalogSparkVersion() {\n    String version = Preconditions.checkNotNull(VersionUtils.VERSION);\n    String[] parts = version.split(\"[.\\\\-]\", 4);\n    int major = Integer.parseInt(parts[0]);\n    int minor = Integer.parseInt(parts[1]);\n    int patch = Integer.parseInt(parts[2]);\n    return new int[] {major, minor, patch};\n  }\n}\n"
  },
  {
    "path": "spark/unitycatalog/src/test/java/io/sparkuctest/UnityCatalogSupportTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.sparkuctest;\n\nimport static io.sparkuctest.UnityCatalogSupport.UC_BASE_TABLE_LOCATION;\nimport static io.sparkuctest.UnityCatalogSupport.UC_CATALOG_NAME;\nimport static io.sparkuctest.UnityCatalogSupport.UC_REMOTE;\nimport static io.sparkuctest.UnityCatalogSupport.UC_SCHEMA_NAME;\nimport static io.sparkuctest.UnityCatalogSupport.UC_TOKEN;\nimport static io.sparkuctest.UnityCatalogSupport.UC_URI;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.collect.ImmutableMap;\nimport io.sparkuctest.UnityCatalogSupport.UnityCatalogInfo;\nimport java.lang.reflect.Field;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\n\npublic class UnityCatalogSupportTest {\n\n  private static final List<String> ALL_ENVS =\n      ImmutableList.of(\n          UC_REMOTE, UC_URI, UC_TOKEN, UC_CATALOG_NAME, UC_SCHEMA_NAME, UC_BASE_TABLE_LOCATION);\n\n  @Test\n  public void testUnityCatalogInfo() throws Exception {\n    withEnvTesting(\n        ImmutableMap.of(\n            UC_REMOTE,\n            \"true\",\n            UC_URI,\n            \"http://localhost:8080\",\n            UC_TOKEN,\n            \"TestRemoteToken\",\n            UC_CATALOG_NAME,\n            \"TestRemoteCatalog\",\n            UC_SCHEMA_NAME,\n            \"TestRemoteSchema\",\n            UC_BASE_TABLE_LOCATION,\n            \"s3://test-bucket/key\"),\n        () -> {\n          TestingUCSupport ucSupport = new TestingUCSupport();\n          UnityCatalogInfo uc = ucSupport.accessUnityCatalogInfo();\n          assertThat(uc.catalogName()).isEqualTo(\"TestRemoteCatalog\");\n          assertThat(uc.serverUri()).isEqualTo(\"http://localhost:8080\");\n          assertThat(uc.serverToken()).isEqualTo(\"TestRemoteToken\");\n          assertThat(uc.schemaName()).isEqualTo(\"TestRemoteSchema\");\n          assertThat(uc.baseTableLocation()).isEqualTo(\"s3://test-bucket/key\");\n        });\n  }\n\n  @Test\n  public void testNoUri() throws Exception {\n    withEnvTesting(\n        ImmutableMap.of(\n            UC_REMOTE,\n            \"true\",\n            UC_TOKEN,\n            \"TestRemoteToken\",\n            UC_CATALOG_NAME,\n            \"TestRemoteCatalog\",\n            UC_SCHEMA_NAME,\n            \"TestRemoteSchema\",\n            UC_BASE_TABLE_LOCATION,\n            \"s3://test-bucket/key\"),\n        () -> {\n          TestingUCSupport uc = new TestingUCSupport();\n          assertThatThrownBy(uc::accessUnityCatalogInfo)\n              .isInstanceOf(NullPointerException.class)\n              .hasMessageContaining(\"UC_URI must be set when UC_REMOTE=true\");\n        });\n  }\n\n  @Test\n  public void testNoCatalogName() throws Exception {\n    withEnvTesting(\n        ImmutableMap.of(\n            UC_REMOTE,\n            \"true\",\n            UC_URI,\n            \"http://localhost:8080\",\n            UC_TOKEN,\n            \"TestRemoteToken\",\n            UC_SCHEMA_NAME,\n            \"TestRemoteSchema\",\n            UC_BASE_TABLE_LOCATION,\n            \"s3://test-bucket/key\"),\n        () -> {\n          TestingUCSupport uc = new TestingUCSupport();\n          assertThatThrownBy(uc::accessUnityCatalogInfo)\n              .isInstanceOf(NullPointerException.class)\n              .hasMessageContaining(\"UC_CATALOG_NAME must be set when UC_REMOTE=true\");\n        });\n  }\n\n  @Test\n  public void testNoToken() throws Exception {\n    withEnvTesting(\n        ImmutableMap.of(\n            UC_REMOTE,\n            \"true\",\n            UC_URI,\n            \"http://localhost:8080\",\n            UC_CATALOG_NAME,\n            \"TestRemoteCatalog\",\n            UC_SCHEMA_NAME,\n            \"TestRemoteSchema\",\n            UC_BASE_TABLE_LOCATION,\n            \"s3://test-bucket/key\"),\n        () -> {\n          TestingUCSupport uc = new TestingUCSupport();\n          assertThatThrownBy(uc::accessUnityCatalogInfo)\n              .isInstanceOf(NullPointerException.class)\n              .hasMessageContaining(\"UC_TOKEN must be set when UC_REMOTE=true\");\n        });\n  }\n\n  @Test\n  public void testNoSchemaName() throws Exception {\n    withEnvTesting(\n        ImmutableMap.of(\n            UC_REMOTE,\n            \"true\",\n            UC_URI,\n            \"http://localhost:8080\",\n            UC_TOKEN,\n            \"TestRemoteToken\",\n            UC_CATALOG_NAME,\n            \"TestRemoteCatalog\",\n            UC_BASE_TABLE_LOCATION,\n            \"s3://test-bucket/key\"),\n        () -> {\n          TestingUCSupport uc = new TestingUCSupport();\n          assertThatThrownBy(uc::accessUnityCatalogInfo)\n              .isInstanceOf(NullPointerException.class)\n              .hasMessageContaining(\"UC_SCHEMA_NAME must be set when UC_REMOTE=true\");\n        });\n  }\n\n  @Test\n  public void testNoBaseTableLocation() throws Exception {\n    withEnvTesting(\n        ImmutableMap.of(\n            UC_REMOTE,\n            \"true\",\n            UC_URI,\n            \"http://localhost:8080\",\n            UC_TOKEN,\n            \"TestRemoteToken\",\n            UC_CATALOG_NAME,\n            \"TestRemoteCatalog\",\n            UC_SCHEMA_NAME,\n            \"TestRemoteSchema\"),\n        () -> {\n          TestingUCSupport uc = new TestingUCSupport();\n          assertThatThrownBy(uc::accessUnityCatalogInfo)\n              .isInstanceOf(NullPointerException.class)\n              .hasMessageContaining(\"UC_BASE_TABLE_LOCATION must be set when UC_REMOTE=true\");\n        });\n  }\n\n  public interface TestCall {\n\n    void call() throws Exception;\n  }\n\n  public void withEnvTesting(Map<String, String> envs, TestCall testCall) throws Exception {\n    // Clear all UC-related environment variables first to ensure clean state\n    ALL_ENVS.forEach(UnityCatalogSupportTest::removeEnv);\n    envs.forEach(UnityCatalogSupportTest::setEnv);\n    try {\n      testCall.call();\n    } finally {\n      // Clean up all UC-related environment variables after test\n      ALL_ENVS.forEach(UnityCatalogSupportTest::removeEnv);\n    }\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static void setEnv(String key, String value) {\n    try {\n      Map<String, String> env = System.getenv();\n      Field f = env.getClass().getDeclaredField(\"m\");\n      f.setAccessible(true);\n      ((Map<String, String>) f.get(env)).put(key, value);\n    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static void removeEnv(String key) {\n    try {\n      Map<String, String> env = System.getenv();\n      Field field = env.getClass().getDeclaredField(\"m\");\n      field.setAccessible(true);\n      ((Map<String, String>) field.get(env)).remove(key);\n    } catch (NoSuchFieldException e) {\n      // Ignore if field doesn't exist (different JVM implementation)\n    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  private static class TestingUCSupport extends UnityCatalogSupport {\n\n    public UnityCatalogInfo accessUnityCatalogInfo() throws Exception {\n      setupServer();\n      return unityCatalogInfo();\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/README.md",
    "content": "# Delta Spark V2 Connector\n\nThe **Delta Spark V2 Connector** enables Apache Spark to read Delta tables using the Delta Kernel.\nIt leverages Spark's DataSource V2 (DSV2) APIs to integrate Delta Kernel into Spark's query execution pipeline.\n\n---\n\n## High-Level Design\n\nThe **Delta Spark V2 Connector** sits between Spark and Delta Kernel, bridging the two:\n\n1. **Spark Driver** requests the table schema and pushes down both static and dynamic filters through the connector.\n2. **Delta Spark V2 Connector** translates these requests to Delta Kernel APIs:\n   - Requests table schema from Delta Kernel.\n   - Pushes down filters and fetches the list of files to scan.\n3. **Delta Kernel** resolves table state from Catalog and Delta logs, applies file skipping, and returns the necessary files.\n4. **Spark Engine** partitions the files (default 128MB splits) and executes the actual Parquet scans using Spark’s existing `ParquetFileFormat`.\n\n\n<p style=\"text-align: center;\">\n  <img width=\"600\" alt=\"Delta-kernel-connector\" src=\"docs/images/delta-kernel-connector.png\" />\n</p>\n\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/catalog/SparkTable.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.catalog;\n\nimport static io.delta.spark.internal.v2.utils.ScalaUtils.toJavaOptional;\nimport static io.delta.spark.internal.v2.utils.ScalaUtils.toScalaMap;\nimport static io.delta.spark.internal.v2.utils.StatsUtils.toV2Statistics;\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.spark.internal.v2.read.SparkScanBuilder;\nimport io.delta.spark.internal.v2.snapshot.DeltaSnapshotManager;\nimport io.delta.spark.internal.v2.snapshot.SnapshotManagerFactory;\nimport io.delta.spark.internal.v2.utils.SchemaUtils;\nimport java.util.*;\nimport java.util.function.Supplier;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.connector.catalog.*;\nimport org.apache.spark.sql.connector.expressions.Expressions;\nimport org.apache.spark.sql.connector.expressions.Transform;\nimport org.apache.spark.sql.connector.read.ScanBuilder;\nimport org.apache.spark.sql.connector.read.Statistics;\nimport org.apache.spark.sql.connector.write.LogicalWriteInfo;\nimport org.apache.spark.sql.connector.write.WriteBuilder;\nimport org.apache.spark.sql.delta.DeltaTableUtils;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\n\n/** DataSource V2 Table implementation for Delta Lake using the Delta Kernel API. */\npublic class SparkTable implements Table, SupportsRead, SupportsWrite {\n\n  private static final Set<TableCapability> CAPABILITIES =\n      Collections.unmodifiableSet(\n          EnumSet.of(\n              TableCapability.BATCH_READ,\n              TableCapability.MICRO_BATCH_READ,\n              TableCapability.BATCH_WRITE));\n\n  private final Identifier identifier;\n  private final String tablePath;\n  private final Map<String, String> options;\n  private final DeltaSnapshotManager snapshotManager;\n  /** Snapshot created during connector setup */\n  private final Snapshot initialSnapshot;\n\n  private final Configuration hadoopConf;\n\n  private final SchemaProvider schemaProvider;\n  private final Optional<CatalogTable> catalogTable;\n\n  /**\n   * Creates a SparkTable from a filesystem path without a catalog table.\n   *\n   * @param identifier logical table identifier used by Spark's catalog\n   * @param tablePath filesystem path to the Delta table root\n   * @throws NullPointerException if identifier or tablePath is null\n   */\n  public SparkTable(Identifier identifier, String tablePath) {\n    this(identifier, tablePath, Collections.emptyMap(), Optional.empty());\n  }\n\n  /**\n   * Creates a SparkTable from a filesystem path with options.\n   *\n   * @param identifier logical table identifier used by Spark's catalog\n   * @param tablePath filesystem path to the Delta table root\n   * @param options table options used to configure the Hadoop conf, table reads and writes\n   * @throws NullPointerException if identifier or tablePath is null\n   */\n  public SparkTable(Identifier identifier, String tablePath, Map<String, String> options) {\n    this(identifier, tablePath, options, Optional.empty());\n  }\n\n  /**\n   * Constructor that accepts a Spark CatalogTable and user-provided options. Extracts the table\n   * location and storage properties from the catalog table, then merges with user options. User\n   * options take precedence over catalog properties in case of conflicts.\n   *\n   * @param identifier logical table identifier used by Spark's catalog\n   * @param catalogTable the Spark CatalogTable containing table metadata including location\n   * @param options user-provided options to override catalog properties\n   */\n  public SparkTable(Identifier identifier, CatalogTable catalogTable, Map<String, String> options) {\n    this(\n        identifier,\n        getDecodedPath(requireNonNull(catalogTable, \"catalogTable is null\").location()),\n        options,\n        Optional.of(catalogTable));\n  }\n\n  /**\n   * Creates a SparkTable backed by a Delta Kernel snapshot manager and initializes Spark-facing\n   * metadata (schemas, partitioning, capabilities).\n   *\n   * <p>Side effects: - Initializes a SnapshotManager for the given tablePath. - Loads the latest\n   * snapshot via the manager. - Builds Hadoop configuration from options for subsequent I/O. -\n   * Derives data schema, partition schema, and full table schema from the snapshot.\n   *\n   * <p>Notes: - Partition column order from the snapshot is preserved for partitioning and appended\n   * after data columns in the public Spark schema, per Spark conventions. - Read-time scan options\n   * are later merged with these options.\n   */\n  private SparkTable(\n      Identifier identifier,\n      String tablePath,\n      Map<String, String> userOptions,\n      Optional<CatalogTable> catalogTable) {\n    this.identifier = requireNonNull(identifier, \"identifier is null\");\n    this.tablePath = requireNonNull(tablePath, \"tablePath is null\");\n    this.catalogTable = catalogTable;\n    // Merge options: file system options from catalog + user options (user takes precedence)\n    // This follows the same pattern as DeltaTableV2 in delta-spark\n    Map<String, String> merged = new HashMap<>();\n    // Only extract file system options from table storage properties\n    catalogTable.ifPresent(\n        table ->\n            scala.collection.JavaConverters.mapAsJavaMap(table.storage().properties())\n                .forEach(\n                    (key, value) -> {\n                      if (DeltaTableUtils.validDeltaTableHadoopPrefixes()\n                          .exists(prefix -> key.startsWith(prefix))) {\n                        merged.put(key, value);\n                      }\n                    }));\n    // User options override catalog properties\n    merged.putAll(userOptions);\n    this.options = Collections.unmodifiableMap(merged);\n\n    this.hadoopConf =\n        SparkSession.active().sessionState().newHadoopConfWithOptions(toScalaMap(options));\n    Engine kernelEngine = DefaultEngine.create(this.hadoopConf);\n    this.snapshotManager = SnapshotManagerFactory.create(tablePath, kernelEngine, catalogTable);\n    // Load the initial snapshot through the manager\n    this.initialSnapshot = snapshotManager.loadLatestSnapshot();\n\n    // Schema-related metadata is lazily computed on first access within SchemaProvider\n    this.schemaProvider = new SchemaProvider(SparkSession.active(), initialSnapshot);\n  }\n\n  /**\n   * Helper method to decode URI path handling URL-encoded characters correctly. E.g., converts\n   * \"spark%25dir%25prefix\" to \"spark%dir%prefix\"\n   *\n   * <p>Uses Hadoop's Path class to properly handle all URI schemes (file, s3, abfss, gs, hdfs,\n   * etc.), not just file:// URIs.\n   */\n  private static String getDecodedPath(java.net.URI location) {\n    Path hadoopPath = new Path(location);\n    // For local file system paths, return just the path component without the scheme\n    // to maintain consistency with path-based table construction where tablePath is a\n    // plain filesystem path string.\n    if (location.getScheme() == null || \"file\".equals(location.getScheme())) {\n      return hadoopPath.toUri().getPath();\n    }\n    return hadoopPath.toString();\n  }\n\n  /**\n   * Returns the CatalogTable if this SparkTable was created from a catalog table.\n   *\n   * @return Optional containing the CatalogTable, or empty if this table was created from a path\n   */\n  public Optional<CatalogTable> getCatalogTable() {\n    return catalogTable;\n  }\n\n  /**\n   * Returns the Path to the Delta table root.\n   *\n   * @return Path created from the table path\n   */\n  public Path getTablePath() {\n    return new Path(tablePath);\n  }\n\n  /**\n   * Returns the table name in a format compatible with DeltaTableV2.\n   *\n   * <p>For catalog-based tables, returns the fully qualified table name (e.g.,\n   * \"spark_catalog.default.table_name\"). For path-based tables, returns the path-based identifier\n   * (e.g., \"delta.`/path/to/table`\").\n   *\n   * @return the table name string\n   */\n  @Override\n  public String name() {\n    return catalogTable\n        .map(ct -> ct.identifier().unquotedString())\n        .orElse(\"delta.`\" + tablePath + \"`\");\n  }\n\n  @Override\n  public StructType schema() {\n    return schemaProvider.getPublicSchema();\n  }\n\n  @Override\n  public Column[] columns() {\n    return schemaProvider.getColumns();\n  }\n\n  @Override\n  public Transform[] partitioning() {\n    return schemaProvider.getPartitionTransforms();\n  }\n\n  @Override\n  public Map<String, String> properties() {\n    Map<String, String> props = new HashMap<>(initialSnapshot.getTableProperties());\n    return Collections.unmodifiableMap(props);\n  }\n\n  @Override\n  public Set<TableCapability> capabilities() {\n    return CAPABILITIES;\n  }\n\n  @Override\n  public ScanBuilder newScanBuilder(CaseInsensitiveStringMap scanOptions) {\n    Map<String, String> combined = new HashMap<>(this.options);\n    combined.putAll(scanOptions.asCaseSensitiveMap());\n    CaseInsensitiveStringMap merged = new CaseInsensitiveStringMap(combined);\n    Optional<Statistics> catalogStats =\n        catalogTable\n            .flatMap(ct -> toJavaOptional(ct.stats()))\n            .map(\n                stats ->\n                    toV2Statistics(\n                        stats,\n                        schemaProvider.getDataSchema(),\n                        schemaProvider.getPartitionSchema()));\n    return new SparkScanBuilder(\n        name(),\n        initialSnapshot,\n        snapshotManager,\n        schemaProvider.getDataSchema(),\n        schemaProvider.getPartitionSchema(),\n        schemaProvider.getRawSchema(),\n        catalogStats,\n        merged);\n  }\n\n  /**\n   * Batch write for Delta tables via the DSv2 connector is not yet supported.\n   *\n   * <p>The write entrypoint is intentionally present to advertise DSv2 write capability while\n   * follow-up changes land the full write implementation.\n   */\n  @Override\n  public WriteBuilder newWriteBuilder(LogicalWriteInfo info) {\n    requireNonNull(info, \"write info is null\");\n    throw new UnsupportedOperationException(\n        \"Batch write for Delta tables via the DSv2 connector is not yet supported.\");\n  }\n\n  @Override\n  public String toString() {\n    return \"SparkTable{identifier=\" + identifier + '}';\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    SparkTable that = (SparkTable) o;\n    return Objects.equals(identifier, that.identifier)\n        && Objects.equals(tablePath, that.tablePath)\n        && Objects.equals(options, that.options)\n        && Objects.equals(catalogTable, that.catalogTable)\n        && Objects.equals(initialSnapshot.getPath(), that.initialSnapshot.getPath())\n        && initialSnapshot.getVersion() == that.initialSnapshot.getVersion();\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(\n        identifier,\n        tablePath,\n        options,\n        catalogTable,\n        initialSnapshot.getPath(),\n        initialSnapshot.getVersion());\n  }\n\n  /**\n   * Private helper class that lazily computes and caches schema-related metadata.\n   *\n   * <p>This class encapsulates all schema computation logic including:\n   *\n   * <ul>\n   *   <li>Raw schema conversion from Kernel to Spark\n   *   <li>Public schema with internal metadata removed\n   *   <li>Data and partition schema derivation\n   *   <li>Column and partition transform creation\n   * </ul>\n   *\n   * <p>All schema computations are deferred until first access.\n   */\n  private static class SchemaProvider {\n    private final SparkSession sparkSession;\n    private final Snapshot snapshot;\n\n    // Lazily computed fields\n    private boolean initialized = false;\n    private StructType rawSchema;\n    private StructType publicSchema;\n    private List<String> partColNames;\n    private StructType dataSchema;\n    private StructType partitionSchema;\n    private Column[] columns;\n    private Transform[] partitionTransforms;\n\n    SchemaProvider(SparkSession sparkSession, Snapshot snapshot) {\n      this.sparkSession = sparkSession;\n      this.snapshot = snapshot;\n    }\n\n    private synchronized void ensureInitialized() {\n      if (initialized) {\n        return;\n      }\n\n      // Convert Kernel schema to Spark schema - keep all metadata for internal use\n      this.rawSchema = SchemaUtils.convertKernelSchemaToSparkSchema(snapshot.getSchema());\n\n      // Create public schema by removing internal metadata (for schema() method)\n      this.publicSchema =\n          DeltaTableUtils.removeInternalDeltaMetadata(\n              sparkSession, DeltaTableUtils.removeInternalWriterMetadata(sparkSession, rawSchema));\n\n      this.partColNames =\n          Collections.unmodifiableList(new ArrayList<>(snapshot.getPartitionColumnNames()));\n\n      final List<StructField> dataFields = new ArrayList<>();\n      final List<StructField> partitionFields = new ArrayList<>();\n\n      // Build a map for O(1) field lookups to improve performance\n      // Use rawSchema (with metadata) for deriving data and partition schemas\n      Map<String, StructField> fieldMap = new HashMap<>();\n      for (StructField field : rawSchema.fields()) {\n        fieldMap.put(field.name(), field);\n      }\n\n      // IMPORTANT: Add partition fields in the exact order specified by partColNames\n      // This is crucial because the order in partColNames may differ from the order\n      // in snapshotSchema, and we need to preserve the partColNames order for\n      // proper partitioning behavior\n      for (String partColName : partColNames) {\n        StructField field = fieldMap.get(partColName);\n        if (field != null) {\n          partitionFields.add(field);\n        }\n      }\n\n      // Add remaining fields as data fields (non-partition columns)\n      // These are fields that exist in the schema but are not partition columns\n      for (StructField field : rawSchema.fields()) {\n        if (!partColNames.contains(field.name())) {\n          dataFields.add(field);\n        }\n      }\n      this.dataSchema = new StructType(dataFields.toArray(new StructField[0]));\n      this.partitionSchema = new StructType(partitionFields.toArray(new StructField[0]));\n\n      // Use publicSchema (cleaned) for external API\n      this.columns = CatalogV2Util.structTypeToV2Columns(publicSchema);\n      this.partitionTransforms =\n          partColNames.stream().map(Expressions::identity).toArray(Transform[]::new);\n\n      this.initialized = true;\n    }\n\n    private <T> T withInit(Supplier<T> supplier) {\n      ensureInitialized();\n      return supplier.get();\n    }\n\n    StructType getPublicSchema() {\n      return withInit(() -> publicSchema);\n    }\n\n    StructType getDataSchema() {\n      return withInit(() -> dataSchema);\n    }\n\n    StructType getPartitionSchema() {\n      return withInit(() -> partitionSchema);\n    }\n\n    StructType getRawSchema() {\n      return withInit(() -> rawSchema);\n    }\n\n    Column[] getColumns() {\n      return withInit(() -> columns);\n    }\n\n    Transform[] getPartitionTransforms() {\n      return withInit(() -> partitionTransforms);\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/exception/VersionNotFoundException.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.exception;\n\n/** Exception thrown when a requested version is not available in the Delta log. */\npublic class VersionNotFoundException extends RuntimeException {\n\n  private final long userVersion;\n  private final long earliest;\n  private final long latest;\n\n  public VersionNotFoundException(long userVersion, long earliest, long latest) {\n    super(\n        String.format(\n            \"Cannot time travel Delta table to version %d. Available versions: [%d, %d].\",\n            userVersion, earliest, latest));\n    this.userVersion = userVersion;\n    this.earliest = earliest;\n    this.latest = latest;\n  }\n\n  public long getUserVersion() {\n    return userVersion;\n  }\n\n  public long getEarliest() {\n    return earliest;\n  }\n\n  public long getLatest() {\n    return latest;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/DeltaInputPartition.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport java.util.Objects;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.connector.read.HasPartitionKey;\nimport org.apache.spark.sql.connector.read.InputPartition;\nimport org.apache.spark.sql.execution.datasources.FilePartition;\n\n/**\n * A Delta-specific InputPartition that wraps a FilePartition and implements HasPartitionKey. This\n * enables Spark to leverage partition information for optimizations like shuffle elimination in\n * joins and aggregations when using KeyGroupedPartitioning.\n *\n * <p>Each DeltaInputPartition represents files from a single logical partition (i.e., all files\n * share the same partition values). The partition key is the InternalRow containing the partition\n * column values.\n */\npublic class DeltaInputPartition implements InputPartition, HasPartitionKey {\n  private final FilePartition filePartition;\n  private final InternalRow partitionKey;\n\n  /**\n   * Creates a new DeltaInputPartition.\n   *\n   * @param filePartition The underlying FilePartition containing the files to read.\n   * @param partitionKey The partition key (partition column values) for all files in this\n   *     partition. Must not be null.\n   */\n  public DeltaInputPartition(FilePartition filePartition, InternalRow partitionKey) {\n    this.filePartition = Objects.requireNonNull(filePartition, \"filePartition is null\");\n    this.partitionKey = Objects.requireNonNull(partitionKey, \"partitionKey is null\");\n  }\n\n  /**\n   * Returns the partition key (partition column values) associated with this partition. All files\n   * in this partition have the same partition key.\n   *\n   * @return The partition key as an InternalRow.\n   */\n  @Override\n  public InternalRow partitionKey() {\n    return partitionKey;\n  }\n\n  /**\n   * Returns the underlying FilePartition.\n   *\n   * @return The FilePartition containing the files to read.\n   */\n  public FilePartition getFilePartition() {\n    return filePartition;\n  }\n\n  @Override\n  public String[] preferredLocations() {\n    return filePartition.preferredLocations();\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (o == null || getClass() != o.getClass()) return false;\n    DeltaInputPartition that = (DeltaInputPartition) o;\n    return Objects.equals(filePartition, that.filePartition)\n        && Objects.equals(partitionKey, that.partitionKey);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(filePartition, partitionKey);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"DeltaInputPartition(partitionKey=%s, files=%d)\",\n        partitionKey, filePartition.files().length);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/DeltaParquetFileFormatV2.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport org.apache.spark.sql.delta.DeltaParquetFileFormatBase;\nimport scala.Option;\n\n/**\n * V2 implementation of DeltaParquetFileFormat using Kernel's Protocol and Metadata.\n *\n * <p>This class enables the V2 connector to reuse delta-spark-v1's DeltaParquetFileFormatBase for\n * reading Parquet files with Delta-specific features like column mapping.\n */\npublic class DeltaParquetFileFormatV2 extends DeltaParquetFileFormatBase {\n\n  private static final long serialVersionUID = 1L;\n\n  /**\n   * Creates a DeltaParquetFileFormatV2.\n   *\n   * @param protocol Kernel's Protocol\n   * @param metadata Kernel's Metadata\n   * @param nullableRowTrackingConstantFields if true, row tracking constant fields (e.g., base row\n   *     ID, default row commit version) will be created as nullable in the schema\n   * @param nullableRowTrackingGeneratedFields if true, row tracking generated fields will be\n   *     created as nullable in the schema\n   * @param optimizationsEnabled whether to enable optimizations (splits, predicate pushdown)\n   * @param tablePath table path for deletion vector support\n   * @param isCDCRead whether this is a CDC read\n   * @param useMetadataRowIndex V2: explicit control over _metadata.row_index usage for DV filtering\n   */\n  public DeltaParquetFileFormatV2(\n      Protocol protocol,\n      Metadata metadata,\n      boolean nullableRowTrackingConstantFields,\n      boolean nullableRowTrackingGeneratedFields,\n      boolean optimizationsEnabled,\n      Option<String> tablePath,\n      boolean isCDCRead,\n      Option<Boolean> useMetadataRowIndex) {\n    super(\n        new ProtocolMetadataAdapterV2(protocol, metadata),\n        nullableRowTrackingConstantFields,\n        nullableRowTrackingGeneratedFields,\n        optimizationsEnabled,\n        tablePath,\n        isCDCRead,\n        // Java's Option<Boolean> can't directly pass to Scala's Option[Boolean] parameter,\n        // because Scala compiles Option[Boolean] to Option<Object> in bytecode for primitive\n        // handling.\n        useMetadataRowIndex.map(x -> x));\n  }\n\n  @Override\n  public boolean equals(Object other) {\n    if (this == other) return true;\n    if (!(other instanceof DeltaParquetFileFormatV2)) return false;\n\n    DeltaParquetFileFormatV2 that = (DeltaParquetFileFormatV2) other;\n    return this.columnMappingMode().equals(that.columnMappingMode())\n        && this.referenceSchema().equals(that.referenceSchema())\n        && this.optimizationsEnabled() == that.optimizationsEnabled()\n        && this.tablePath().equals(that.tablePath())\n        && this.isCDCRead() == that.isCDCRead();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/IndexedFile.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\n\nimport io.delta.kernel.internal.actions.AddFile;\nimport org.apache.spark.sql.delta.sources.AdmittableFile;\n\n/**\n * Java version of IndexedFile.scala that uses Kernel's action classes.\n *\n * <p>File: represents a data file in Delta.\n *\n * <p>Indexed: refers to the index in DeltaSourceOffset, assigned by the streaming engine.\n */\npublic class IndexedFile implements AdmittableFile {\n  private final long version;\n  private final long index;\n  private final AddFile addFile;\n\n  public IndexedFile(long version, long index, AddFile addFile) {\n    this.version = version;\n    this.index = index;\n    this.addFile = addFile;\n  }\n\n  public long getVersion() {\n    return version;\n  }\n\n  public long getIndex() {\n    return index;\n  }\n\n  public AddFile getAddFile() {\n    return addFile;\n  }\n\n  @Override\n  public boolean hasFileAction() {\n    return addFile != null;\n  }\n\n  @Override\n  public long getFileSize() {\n    checkState(addFile != null, \"check hasFileAction() before calling getFileSize()\");\n    return addFile.getSize();\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"IndexedFile{\");\n    sb.append(\"version=\").append(version);\n    sb.append(\", index=\").append(index);\n    sb.append(\", addFile=\").append(addFile);\n    sb.append('}');\n    return sb.toString();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/ProtocolMetadataAdapterV2.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.rowtracking.RowTracking;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.spark.internal.v2.utils.RowTrackingUtils;\nimport io.delta.spark.internal.v2.utils.SchemaUtils;\nimport java.io.Serializable;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.delta.DeltaColumnMappingMode;\nimport org.apache.spark.sql.delta.IdMapping$;\nimport org.apache.spark.sql.delta.NameMapping$;\nimport org.apache.spark.sql.delta.NoMapping$;\nimport org.apache.spark.sql.delta.ProtocolMetadataAdapter;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport scala.jdk.javaapi.CollectionConverters;\n\n/**\n * Implementation of ProtocolMetadataAdapter for Delta Kernel's Protocol and Metadata.\n *\n * <p>This class adapts Kernel's Protocol and Metadata to the ProtocolMetadataAdapter interface,\n * enabling the V2 connector to reuse delta-spark-v1's DeltaParquetFileFormat for reading Parquet\n * files.\n *\n * <p>Key responsibilities:\n *\n * <ul>\n *   <li>Bridge Kernel's Protocol/Metadata to delta-spark's ProtocolMetadataAdapter interface\n *   <li>Convert column mapping modes between Kernel and delta-spark representations\n *   <li>Provide Delta-aware feature checks (deletion vectors, row tracking, Iceberg compatibility)\n *   <li>Convert schemas between Kernel and Spark formats\n * </ul>\n */\npublic class ProtocolMetadataAdapterV2 implements ProtocolMetadataAdapter, Serializable {\n  private static final long serialVersionUID = 1L;\n\n  private final Protocol protocol;\n  private final Metadata metadata;\n\n  public ProtocolMetadataAdapterV2(Protocol protocol, Metadata metadata) {\n    this.protocol = protocol;\n    this.metadata = metadata;\n  }\n\n  @Override\n  public DeltaColumnMappingMode columnMappingMode() {\n    ColumnMapping.ColumnMappingMode kernelMode =\n        ColumnMapping.getColumnMappingMode(metadata.getConfiguration());\n    switch (kernelMode) {\n      case NONE:\n        return NoMapping$.MODULE$;\n      case ID:\n        return IdMapping$.MODULE$;\n      case NAME:\n        return NameMapping$.MODULE$;\n      default:\n        throw new UnsupportedOperationException(\"Unsupported column mapping mode: \" + kernelMode);\n    }\n  }\n\n  @Override\n  public StructType getReferenceSchema() {\n    return SchemaUtils.convertKernelSchemaToSparkSchema(metadata.getSchema());\n  }\n\n  @Override\n  public boolean isRowIdEnabled() {\n    return RowTracking.isEnabled(protocol, metadata);\n  }\n\n  @Override\n  public boolean isDeletionVectorReadable() {\n    return protocol.supportsFeature(TableFeatures.DELETION_VECTORS_RW_FEATURE)\n        && \"parquet\".equalsIgnoreCase(metadata.getFormat().getProvider());\n  }\n\n  @Override\n  public boolean isIcebergCompatAnyEnabled() {\n    return TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(metadata)\n        || TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(metadata);\n  }\n\n  @Override\n  public boolean isIcebergCompatGeqEnabled(int version) {\n    boolean v2Enabled = TableConfig.ICEBERG_COMPAT_V2_ENABLED.fromMetadata(metadata);\n    boolean v3Enabled = TableConfig.ICEBERG_COMPAT_V3_ENABLED.fromMetadata(metadata);\n    // IcebergCompatV1 is not supported in Kernel, so V2 is the minimum version for v2 connector\n    // until kernel supports IcebergCompatV1.\n    // For version 1 or 2, we return true if V2 or V3 is enabled.\n    switch (version) {\n      case 1:\n      case 2:\n        return v2Enabled || v3Enabled;\n      case 3:\n        return v3Enabled;\n      default:\n        return false;\n    }\n  }\n\n  @Override\n  public void assertTableReadable(SparkSession sparkSession) {\n    // TODO(delta-io/delta#5649): Add type widening validation.\n  }\n\n  @Override\n  public scala.collection.Iterable<StructField> createRowTrackingMetadataFields(\n      boolean nullableRowTrackingConstantFields, boolean nullableRowTrackingGeneratedFields) {\n    // Use RowTrackingUtils.createMetadataStructFields which handles:\n    // - Checking if row tracking is enabled\n    // - Creating fields with proper Spark metadata attributes\n    // - Handling materialized column names\n    return CollectionConverters.asScala(\n            RowTrackingUtils.createMetadataStructFields(\n                protocol,\n                metadata,\n                nullableRowTrackingConstantFields,\n                nullableRowTrackingGeneratedFields))\n        .toSeq();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/SparkBatch.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.spark.internal.v2.utils.PartitionUtils;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.connector.read.Batch;\nimport org.apache.spark.sql.connector.read.InputPartition;\nimport org.apache.spark.sql.connector.read.PartitionReaderFactory;\nimport org.apache.spark.sql.execution.datasources.FilePartition;\nimport org.apache.spark.sql.execution.datasources.FilePartition$;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.internal.SQLConf;\nimport org.apache.spark.sql.sources.Filter;\nimport org.apache.spark.sql.types.StructType;\nimport scala.collection.JavaConverters;\n\npublic class SparkBatch implements Batch {\n  private final Snapshot snapshot;\n  private final StructType readDataSchema;\n  private final StructType dataSchema;\n  private final StructType partitionSchema;\n  private final Predicate[] pushedToKernelFilters;\n  private final Filter[] dataFilters;\n  private final Configuration hadoopConf;\n  private final SQLConf sqlConf;\n  private final long totalBytes;\n  private scala.collection.immutable.Map<String, String> scalaOptions;\n  private final List<PartitionedFile> partitionedFiles;\n\n  public SparkBatch(\n      Snapshot snapshot,\n      StructType dataSchema,\n      StructType partitionSchema,\n      StructType readDataSchema,\n      List<PartitionedFile> partitionedFiles,\n      Predicate[] pushedToKernelFilters,\n      Filter[] dataFilters,\n      long totalBytes,\n      scala.collection.immutable.Map<String, String> scalaOptions,\n      Configuration hadoopConf) {\n\n    this.snapshot = Objects.requireNonNull(snapshot, \"snapshot is null\");\n    this.dataSchema = Objects.requireNonNull(dataSchema, \"dataSchema is null\");\n    this.partitionSchema = Objects.requireNonNull(partitionSchema, \"partitionSchema is null\");\n    this.readDataSchema = Objects.requireNonNull(readDataSchema, \"readDataSchema is null\");\n    this.partitionedFiles =\n        java.util.Collections.unmodifiableList(\n            new ArrayList<>(Objects.requireNonNull(partitionedFiles, \"partitionedFiles is null\")));\n    this.pushedToKernelFilters =\n        pushedToKernelFilters != null\n            ? Arrays.copyOf(pushedToKernelFilters, pushedToKernelFilters.length)\n            : new Predicate[0];\n    this.dataFilters =\n        dataFilters != null ? Arrays.copyOf(dataFilters, dataFilters.length) : new Filter[0];\n    this.totalBytes = totalBytes;\n    this.scalaOptions = Objects.requireNonNull(scalaOptions, \"scalaOptions is null\");\n    this.hadoopConf = Objects.requireNonNull(hadoopConf, \"hadoopConf is null\");\n    this.sqlConf = SQLConf.get();\n  }\n\n  @Override\n  public InputPartition[] planInputPartitions() {\n    SparkSession sparkSession = SparkSession.active();\n    long maxSplitBytes =\n        PartitionUtils.calculateMaxSplitBytes(\n            sparkSession, totalBytes, partitionedFiles.size(), sqlConf);\n\n    // For non-partitioned tables, use simple file partitioning\n    if (partitionSchema.fields().length == 0) {\n      scala.collection.Seq<FilePartition> filePartitions =\n          FilePartition$.MODULE$.getFilePartitions(\n              sparkSession, JavaConverters.asScalaBuffer(partitionedFiles).toSeq(), maxSplitBytes);\n      return JavaConverters.seqAsJavaList(filePartitions).toArray(new InputPartition[0]);\n    }\n\n    // For partitioned tables, group files by partition values and wrap in DeltaInputPartition\n    // to support HasPartitionKey for KeyGroupedPartitioning optimizations\n    return planPartitionedInputPartitions(sparkSession, maxSplitBytes);\n  }\n\n  /**\n   * Plans input partitions for partitioned tables by grouping files by their partition values. Each\n   * resulting DeltaInputPartition implements HasPartitionKey, enabling Spark to leverage partition\n   * information for optimizations like shuffle elimination.\n   */\n  private InputPartition[] planPartitionedInputPartitions(\n      SparkSession sparkSession, long maxSplitBytes) {\n    // Note: Using InternalRow as map key relies on GenericInternalRow's value-based\n    // equals()/hashCode(), which is what PartitionUtils.getPartitionRow() returns.\n    Map<InternalRow, List<PartitionedFile>> filesByPartition = new LinkedHashMap<>();\n    for (PartitionedFile file : partitionedFiles) {\n      InternalRow partitionKey = file.partitionValues();\n      filesByPartition.computeIfAbsent(partitionKey, k -> new ArrayList<>()).add(file);\n    }\n\n    // Create DeltaInputPartitions for each partition group\n    List<InputPartition> result = new ArrayList<>();\n    int partitionIndex = 0;\n\n    for (Map.Entry<InternalRow, List<PartitionedFile>> entry : filesByPartition.entrySet()) {\n      InternalRow partitionKey = entry.getKey();\n      List<PartitionedFile> filesInPartition = entry.getValue();\n\n      // Split files within this partition based on maxSplitBytes\n      scala.collection.Seq<FilePartition> filePartitions =\n          FilePartition$.MODULE$.getFilePartitions(\n              sparkSession, JavaConverters.asScalaBuffer(filesInPartition).toSeq(), maxSplitBytes);\n\n      // Wrap each FilePartition in a DeltaInputPartition with the partition key.\n      // Re-index partitions with a global counter because getFilePartitions returns 0-based\n      // indices within each partition group, but we need unique indices across all groups.\n      for (FilePartition fp : JavaConverters.seqAsJavaList(filePartitions)) {\n        FilePartition reindexedPartition = new FilePartition(partitionIndex++, fp.files());\n        result.add(new DeltaInputPartition(reindexedPartition, partitionKey));\n      }\n    }\n\n    return result.toArray(new InputPartition[0]);\n  }\n\n  @Override\n  public PartitionReaderFactory createReaderFactory() {\n    return PartitionUtils.createDeltaParquetReaderFactory(\n        snapshot,\n        dataSchema,\n        partitionSchema,\n        readDataSchema,\n        dataFilters,\n        scalaOptions,\n        hadoopConf,\n        sqlConf);\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (this == obj) return true;\n    if (!(obj instanceof SparkBatch)) return false;\n\n    SparkBatch that = (SparkBatch) obj;\n    return Objects.equals(this.snapshot, that.snapshot)\n        && Objects.equals(this.readDataSchema, that.readDataSchema)\n        && Objects.equals(this.dataSchema, that.dataSchema)\n        && Objects.equals(this.partitionSchema, that.partitionSchema)\n        && Arrays.equals(this.pushedToKernelFilters, that.pushedToKernelFilters)\n        && Arrays.equals(this.dataFilters, that.dataFilters)\n        && partitionedFiles.size() == that.partitionedFiles.size();\n  }\n\n  @Override\n  public int hashCode() {\n    int result = snapshot.hashCode();\n    result = 31 * result + readDataSchema.hashCode();\n    result = 31 * result + dataSchema.hashCode();\n    result = 31 * result + partitionSchema.hashCode();\n    result = 31 * result + Arrays.hashCode(pushedToKernelFilters);\n    result = 31 * result + Arrays.hashCode(dataFilters);\n    result = 31 * result + Integer.hashCode(partitionedFiles.size());\n    return result;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/SparkMicroBatchStream.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.TYPE_WIDENING_RW_FEATURE;\nimport static io.delta.kernel.internal.tablefeatures.TableFeatures.TYPE_WIDENING_RW_PREVIEW_FEATURE;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.Scan;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.exceptions.UnsupportedTableFeatureException;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.kernel.internal.DeltaLogActionUtils.DeltaAction;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.RemoveFile;\nimport io.delta.kernel.internal.util.ColumnMapping;\nimport io.delta.kernel.internal.util.ColumnMapping.ColumnMappingMode;\nimport io.delta.kernel.internal.util.Preconditions;\nimport io.delta.kernel.internal.util.Utils;\nimport io.delta.kernel.internal.util.VectorUtils;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.spark.internal.v2.snapshot.DeltaSnapshotManager;\nimport io.delta.spark.internal.v2.utils.PartitionUtils;\nimport io.delta.spark.internal.v2.utils.ScalaUtils;\nimport io.delta.spark.internal.v2.utils.SchemaUtils;\nimport io.delta.spark.internal.v2.utils.StreamingHelper;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.channels.ClosedByInterruptException;\nimport java.sql.Timestamp;\nimport java.time.ZoneId;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.stream.Collectors;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.expressions.Literal$;\nimport org.apache.spark.sql.connector.read.InputPartition;\nimport org.apache.spark.sql.connector.read.PartitionReaderFactory;\nimport org.apache.spark.sql.connector.read.streaming.*;\nimport org.apache.spark.sql.delta.DeltaColumnMapping;\nimport org.apache.spark.sql.delta.DeltaErrors;\nimport org.apache.spark.sql.delta.DeltaOptions;\nimport org.apache.spark.sql.delta.DeltaStartingVersion;\nimport org.apache.spark.sql.delta.DeltaTimeTravelSpec;\nimport org.apache.spark.sql.delta.StartingVersion;\nimport org.apache.spark.sql.delta.StartingVersionLatest$;\nimport org.apache.spark.sql.delta.TypeWidening;\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf;\nimport org.apache.spark.sql.delta.sources.DeltaSource;\nimport org.apache.spark.sql.delta.sources.DeltaSourceOffset;\nimport org.apache.spark.sql.delta.sources.DeltaSourceOffset$;\nimport org.apache.spark.sql.delta.sources.DeltaStreamUtils;\nimport org.apache.spark.sql.execution.datasources.FilePartition;\nimport org.apache.spark.sql.execution.datasources.FilePartition$;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.internal.SQLConf;\nimport org.apache.spark.sql.sources.Filter;\nimport org.apache.spark.sql.types.DataType;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport scala.Option;\nimport scala.Some;\nimport scala.collection.JavaConverters;\nimport scala.collection.immutable.Seq;\nimport scala.collection.immutable.Seq$;\nimport scala.jdk.javaapi.CollectionConverters;\nimport scala.util.matching.Regex;\n\n// TODO(#5318): Use DeltaErrors error framework for consistent error handling.\npublic class SparkMicroBatchStream\n    implements MicroBatchStream, SupportsAdmissionControl, SupportsTriggerAvailableNow {\n\n  private static final Logger logger = LoggerFactory.getLogger(SparkMicroBatchStream.class);\n\n  private static final Set<DeltaAction> ACTION_SET =\n      Collections.unmodifiableSet(\n          new HashSet<>(Arrays.asList(DeltaAction.ADD, DeltaAction.REMOVE, DeltaAction.METADATA)));\n\n  private final Engine engine;\n  private final DeltaSnapshotManager snapshotManager;\n  private final DeltaOptions options;\n  private final boolean skipChangeCommits;\n  private final SnapshotImpl snapshotAtSourceInit;\n  private final String tableId;\n  private final StructType readSchemaAtSourceInit;\n  private final boolean shouldValidateOffsets;\n  private final Optional<Regex> excludeRegex;\n  private final SparkSession spark;\n  private final String tablePath;\n  private final StructType readDataSchema;\n  private final StructType dataSchema;\n  private final StructType partitionSchema;\n  private final Filter[] dataFilters;\n  private final Configuration hadoopConf;\n  private final SQLConf sqlConf;\n  private final scala.collection.immutable.Map<String, String> scalaOptions;\n\n  /**\n   * Tracks whether this is the first batch for this stream (no checkpointed offset).\n   *\n   * <p>- First batch: initialOffset() -> latestOffset(Offset, ReadLimit) - Set `isFirstBatch` to\n   * true in initialOffset() - in latestOffset(Offset, ReadLimit), use `isFirstBatch` to determine\n   * whether to return null vs previousOffset (when no data is available) - set `isFirstBatch` to\n   * false - Subsequent batches: latestOffset(Offset, ReadLimit)\n   */\n  private boolean isFirstBatch = false;\n\n  /**\n   * Configuration options for handling schema changes behavior. Controls unsafe operations like\n   * column mapping changes, partition column changes, nullability changes, and type widening.\n   */\n  private DeltaStreamUtils.SchemaReadOptions schemaReadOptions;\n\n  /**\n   * A global flag to mark whether we have done a per-stream start check for column mapping schema\n   * changes (rename / drop).\n   */\n  private volatile boolean hasCheckedReadIncompatibleSchemaChangesOnStreamStart = false;\n\n  /**\n   * When AvailableNow is used, this offset will be the upper bound where this run of the query will\n   * process up. We may run multiple micro batches, but the query will stop itself when it reaches\n   * this offset.\n   */\n  private Optional<DeltaSourceOffset> lastOffsetForTriggerAvailableNow = Optional.empty();\n\n  private boolean isLastOffsetForTriggerAvailableNowInitialized = false;\n\n  private boolean isTriggerAvailableNow = false;\n\n  // Cached starting version to ensure idempotent behavior for \"latest\" starting version.\n  // getStartingVersion() must return the same value across multiple calls.\n  private volatile Optional<Long> cachedStartingVersion = null;\n\n  // Cache for the initial snapshot files to avoid re-sorting on repeated access.\n  private static class InitialSnapshotCache {\n    final Long version;\n    final List<IndexedFile> files;\n\n    InitialSnapshotCache(Long version, List<IndexedFile> files) {\n      this.version = version;\n      this.files = files;\n    }\n  }\n\n  private final AtomicReference<InitialSnapshotCache> cachedInitialSnapshot =\n      new AtomicReference<>(null);\n\n  private final int maxInitialSnapshotFiles;\n\n  public SparkMicroBatchStream(\n      DeltaSnapshotManager snapshotManager,\n      Snapshot snapshotAtSourceInit,\n      Configuration hadoopConf,\n      SparkSession spark,\n      DeltaOptions options,\n      String tablePath,\n      StructType dataSchema,\n      StructType partitionSchema,\n      StructType readDataSchema,\n      Filter[] dataFilters,\n      scala.collection.immutable.Map<String, String> scalaOptions) {\n    this.snapshotManager = Objects.requireNonNull(snapshotManager, \"snapshotManager is null\");\n    this.hadoopConf = Objects.requireNonNull(hadoopConf, \"hadoopConf is null\");\n    this.spark = Objects.requireNonNull(spark, \"spark is null\");\n    this.engine = DefaultEngine.create(hadoopConf);\n    this.options = Objects.requireNonNull(options, \"options is null\");\n    this.skipChangeCommits = this.options.skipChangeCommits();\n    // Normalize tablePath to ensure it ends with \"/\" for consistent path construction\n    String normalizedTablePath = Objects.requireNonNull(tablePath, \"tablePath is null\");\n    this.tablePath =\n        normalizedTablePath.endsWith(\"/\") ? normalizedTablePath : normalizedTablePath + \"/\";\n    this.dataSchema = Objects.requireNonNull(dataSchema, \"dataSchema is null\");\n    this.partitionSchema = Objects.requireNonNull(partitionSchema, \"partitionSchema is null\");\n    this.readDataSchema = Objects.requireNonNull(readDataSchema, \"readDataSchema is null\");\n    this.dataFilters =\n        Arrays.copyOf(\n            Objects.requireNonNull(dataFilters, \"dataFilters is null\"), dataFilters.length);\n    this.sqlConf = SQLConf.get();\n    this.scalaOptions = Objects.requireNonNull(scalaOptions, \"scalaOptions is null\");\n\n    this.snapshotAtSourceInit = (SnapshotImpl) snapshotAtSourceInit;\n    this.tableId = this.snapshotAtSourceInit.getMetadata().getId();\n    // TODO(#5319): schema tracking for non-additive schema changes\n    this.readSchemaAtSourceInit =\n        Objects.requireNonNull(\n            SchemaUtils.convertKernelSchemaToSparkSchema(snapshotAtSourceInit.getSchema()),\n            \"readSchemaAtSourceInit is null\");\n    this.shouldValidateOffsets =\n        Objects.requireNonNull(\n            (Boolean)\n                spark.sessionState().conf().getConf(DeltaSQLConf.STREAMING_OFFSET_VALIDATION()),\n            \"shouldValidateOffsets is null\");\n    this.excludeRegex = ScalaUtils.toJavaOptional(options.excludeRegex());\n    this.maxInitialSnapshotFiles =\n        (Integer)\n            spark\n                .sessionState()\n                .conf()\n                .getConf(DeltaSQLConf.DELTA_STREAMING_INITIAL_SNAPSHOT_MAX_FILES());\n\n    boolean isStreamingFromColumnMappingTable =\n        ColumnMapping.getColumnMappingMode(\n                this.snapshotAtSourceInit.getMetadata().getConfiguration())\n            != ColumnMappingMode.NONE;\n    boolean isTypeWideningSupportedInProtocol =\n        this.snapshotAtSourceInit.getProtocol().supportsFeature(TYPE_WIDENING_RW_PREVIEW_FEATURE)\n            || this.snapshotAtSourceInit.getProtocol().supportsFeature(TYPE_WIDENING_RW_FEATURE);\n    this.schemaReadOptions =\n        Objects.requireNonNull(\n            DeltaStreamUtils.SchemaReadOptions$.MODULE$.fromSparkSession(\n                spark, isStreamingFromColumnMappingTable, isTypeWideningSupportedInProtocol),\n            \"schemaReadOptions is null\");\n    validateSchemaCompatibilityOnStartup(dataSchema, partitionSchema, readSchemaAtSourceInit);\n  }\n\n  @Override\n  public void prepareForTriggerAvailableNow() {\n    logger.info(\"The streaming query reports to use Trigger.AvailableNow.\");\n    isTriggerAvailableNow = true;\n  }\n\n  /**\n   * initialize the internal states for AvailableNow if this method is called first time after\n   * prepareForTriggerAvailableNow.\n   */\n  private void initForTriggerAvailableNowIfNeeded(DeltaSourceOffset startOffsetOpt) {\n    if (isTriggerAvailableNow && !isLastOffsetForTriggerAvailableNowInitialized) {\n      isLastOffsetForTriggerAvailableNowInitialized = true;\n      initLastOffsetForTriggerAvailableNow(startOffsetOpt);\n    }\n  }\n\n  private void initLastOffsetForTriggerAvailableNow(DeltaSourceOffset startOffsetOpt) {\n    lastOffsetForTriggerAvailableNow =\n        latestOffsetInternal(startOffsetOpt, ReadLimit.allAvailable());\n\n    lastOffsetForTriggerAvailableNow.ifPresent(\n        lastOffset ->\n            logger.info(\"lastOffset for Trigger.AvailableNow has set to \" + lastOffset.json()));\n  }\n\n  ////////////\n  // offset //\n  ////////////\n\n  /**\n   * Returns the initial offset for a streaming query to start reading from (if there's no\n   * checkpointed offset).\n   */\n  @Override\n  public Offset initialOffset() {\n    Optional<Long> startingVersionOpt = getStartingVersion();\n    long version;\n    boolean isInitialSnapshot;\n    isFirstBatch = true;\n\n    if (startingVersionOpt.isPresent()) {\n      version = startingVersionOpt.get();\n      isInitialSnapshot = false;\n    } else {\n      // No starting version specified in the options, use snapshot captured\n      // at source initialization.\n      version = snapshotAtSourceInit.getVersion();\n      isInitialSnapshot = true;\n    }\n\n    return DeltaSourceOffset.apply(\n        tableId, version, DeltaSourceOffset.BASE_INDEX(), isInitialSnapshot);\n  }\n\n  @Override\n  public Offset latestOffset() {\n    throw new IllegalStateException(\n        \"latestOffset() should not be called - use latestOffset(Offset, ReadLimit) instead\");\n  }\n\n  /**\n   * Get the latest offset with rate limiting (SupportsAdmissionControl).\n   *\n   * @param startOffset The starting offset\n   * @param limit The read limit for rate limiting\n   * @return The latest offset, or null if no data is available to read.\n   */\n  @Override\n  public Offset latestOffset(Offset startOffset, ReadLimit limit) {\n    Objects.requireNonNull(startOffset, \"startOffset should not be null for MicroBatchStream\");\n    Objects.requireNonNull(limit, \"limit should not be null for MicroBatchStream\");\n\n    try {\n      DeltaSourceOffset deltaStartOffset = DeltaSourceOffset.apply(tableId, startOffset);\n      initForTriggerAvailableNowIfNeeded(deltaStartOffset);\n      // Return null when no data is available for this batch.\n      DeltaSourceOffset endOffset = latestOffsetInternal(deltaStartOffset, limit).orElse(null);\n      isFirstBatch = false;\n      return endOffset;\n    } catch (Exception e) {\n      // Kernel's DefaultJsonHandler wraps ClosedByInterruptException (thrown by NIO\n      // channels on thread interrupt) inside KernelEngineException (a RuntimeException).\n      // Spark's StreamExecution.isInterruptionException recognizes\n      // ClosedByInterruptException and UncheckedIOException but not\n      // KernelEngineException. Re-wrap so Spark's isInterruptedByStop — which also\n      // verifies state == TERMINATED — handles it as a clean stream shutdown.\n      Optional<ClosedByInterruptException> interruptCause = findClosedByInterruptCause(e);\n      if (interruptCause.isPresent()) {\n        throw new UncheckedIOException(interruptCause.get());\n      }\n      throw e;\n    }\n  }\n\n  /**\n   * Internal implementation of latestOffset using DeltaSourceOffset directly, without null checks\n   * and state management.\n   */\n  private Optional<DeltaSourceOffset> latestOffsetInternal(\n      DeltaSourceOffset deltaStartOffset, ReadLimit limit) {\n    Optional<DeltaSource.AdmissionLimits> limits =\n        ScalaUtils.toJavaOptional(DeltaSource.AdmissionLimits$.MODULE$.apply(options, limit));\n    Optional<DeltaSourceOffset> endOffset =\n        getNextOffsetFromPreviousOffset(deltaStartOffset, limits, isFirstBatch);\n\n    if (shouldValidateOffsets && endOffset.isPresent()) {\n      DeltaSourceOffset.validateOffsets(deltaStartOffset, endOffset.get());\n    }\n\n    return endOffset;\n  }\n\n  @Override\n  public Offset deserializeOffset(String json) {\n    return DeltaSourceOffset$.MODULE$.apply(tableId, json);\n  }\n\n  @Override\n  public ReadLimit getDefaultReadLimit() {\n    return DeltaSource.AdmissionLimits$.MODULE$.toReadLimit(options);\n  }\n\n  /**\n   * Return the next offset when previous offset exists. Mimics\n   * DeltaSource.getNextOffsetFromPreviousOffset.\n   *\n   * @param previousOffset The previous offset\n   * @param limits Rate limits for this batch (Optional.empty() for no limits)\n   * @param isFirstBatch Whether this is the first batch for this stream\n   * @return The next offset, or the previous offset if no new data is available (except on the\n   *     initial batch where we return empty to match DSv1's\n   *     getStartingOffsetFromSpecificDeltaVersion behavior)\n   */\n  private Optional<DeltaSourceOffset> getNextOffsetFromPreviousOffset(\n      DeltaSourceOffset previousOffset,\n      Optional<DeltaSource.AdmissionLimits> limits,\n      boolean isFirstBatch) {\n    // TODO(#5319): Special handling for schema tracking.\n\n    CloseableIterator<IndexedFile> changes =\n        getFileChangesWithRateLimit(\n            previousOffset.reservoirVersion(),\n            previousOffset.index(),\n            previousOffset.isInitialSnapshot(),\n            limits);\n\n    Optional<IndexedFile> lastFileChange = Utils.iteratorLast(changes);\n\n    if (!lastFileChange.isPresent()) {\n      // For the first batch, return empty to match DSv1's\n      // getStartingOffsetFromSpecificDeltaVersion\n      if (isFirstBatch) {\n        return Optional.empty();\n      }\n      return Optional.of(previousOffset);\n    }\n    // Block latestOffset() from generating an invalid offset by proactively\n    // verifying incompatible schema changes under column mapping. See more details in the\n    // method java doc.\n    checkReadIncompatibleSchemaChangeOnStreamStartOnce(\n        previousOffset.reservoirVersion(), /* batchEndVersion= */ null);\n    IndexedFile lastFile = lastFileChange.get();\n    return Optional.of(\n        DeltaSource.buildOffsetFromIndexedFile(\n            tableId,\n            lastFile.getVersion(),\n            lastFile.getIndex(),\n            previousOffset.reservoirVersion(),\n            previousOffset.isInitialSnapshot()));\n  }\n\n  ////////////\n  /// data ///\n  ////////////\n\n  @Override\n  public InputPartition[] planInputPartitions(Offset start, Offset end) {\n    DeltaSourceOffset startOffset = (DeltaSourceOffset) start;\n    DeltaSourceOffset endOffset = (DeltaSourceOffset) end;\n\n    long fromVersion = startOffset.reservoirVersion();\n    long fromIndex = startOffset.index();\n    boolean isInitialSnapshot = startOffset.isInitialSnapshot();\n\n    List<PartitionedFile> partitionedFiles = new ArrayList<>();\n    long totalBytesToRead = 0;\n    try (CloseableIterator<IndexedFile> fileChanges =\n        getFileChanges(fromVersion, fromIndex, isInitialSnapshot, Optional.of(endOffset))) {\n      while (fileChanges.hasNext()) {\n        IndexedFile indexedFile = fileChanges.next();\n        if (!indexedFile.hasFileAction() || indexedFile.getAddFile() == null) {\n          continue;\n        }\n        AddFile addFile = indexedFile.getAddFile();\n        // TODO(#5319): Apply excludeRegex to RemoveFile/AddCDCFile when CDC is supported\n        if (excludeRegex.isPresent()\n            && excludeRegex.get().findFirstIn(addFile.getPath()).isDefined()) {\n          continue;\n        }\n        PartitionedFile partitionedFile =\n            PartitionUtils.buildPartitionedFile(\n                addFile, partitionSchema, tablePath, ZoneId.of(sqlConf.sessionLocalTimeZone()));\n\n        totalBytesToRead += addFile.getSize();\n        partitionedFiles.add(partitionedFile);\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(\n          String.format(\n              \"Failed to get file changes for table %s from version %d index %d to offset %s\",\n              tablePath, fromVersion, fromIndex, endOffset),\n          e);\n    } catch (RuntimeException e) {\n      // Same interrupt handling as latestOffset(): Kernel wraps ClosedByInterruptException\n      // in KernelEngineException (a RuntimeException). Re-wrap as UncheckedIOException so\n      // Spark's isInterruptedByStop recognizes it as a clean shutdown.\n      Optional<ClosedByInterruptException> interruptCause = findClosedByInterruptCause(e);\n      if (interruptCause.isPresent()) {\n        throw new UncheckedIOException(interruptCause.get());\n      }\n      throw e;\n    }\n\n    long maxSplitBytes =\n        PartitionUtils.calculateMaxSplitBytes(\n            spark, totalBytesToRead, partitionedFiles.size(), sqlConf);\n    // Partitions files into Spark FilePartitions.\n    Seq<FilePartition> filePartitions =\n        FilePartition$.MODULE$.getFilePartitions(\n            spark, JavaConverters.asScalaBuffer(partitionedFiles).toSeq(), maxSplitBytes);\n    return JavaConverters.seqAsJavaList(filePartitions).toArray(new InputPartition[0]);\n  }\n\n  @Override\n  public PartitionReaderFactory createReaderFactory() {\n    return PartitionUtils.createDeltaParquetReaderFactory(\n        snapshotAtSourceInit,\n        dataSchema,\n        partitionSchema,\n        readDataSchema,\n        dataFilters,\n        scalaOptions,\n        hadoopConf,\n        sqlConf);\n  }\n\n  ///////////////\n  // lifecycle //\n  ///////////////\n\n  @Override\n  public void commit(Offset end) {\n    // TODO(#5319): update metadata tracking log.\n  }\n\n  @Override\n  public void stop() {\n    cachedInitialSnapshot.set(null);\n  }\n\n  /**\n   * If the given exception wraps a {@link ClosedByInterruptException} as its direct cause, returns\n   * it. This occurs when Spark interrupts the micro-batch thread during stream shutdown and the\n   * thread is blocked inside Kernel's {@code DefaultJsonHandler} reading delta log files via NIO\n   * channels.\n   */\n  static Optional<ClosedByInterruptException> findClosedByInterruptCause(Throwable t) {\n    Throwable cause = t.getCause();\n    if (cause instanceof ClosedByInterruptException) {\n      return Optional.of((ClosedByInterruptException) cause);\n    }\n    return Optional.empty();\n  }\n\n  ///////////////////////\n  // getStartingVersion //\n  ///////////////////////\n\n  /**\n   * Extracts whether users provided the option to time travel a relation. If a query restarts from\n   * a checkpoint and the checkpoint has recorded the offset, this method should never be called.\n   *\n   * <p>Returns Optional.empty() if no starting version is provided.\n   *\n   * <p>This is the DSv2 Kernel-based implementation of DeltaSource.getStartingVersion.\n   */\n  synchronized Optional<Long> getStartingVersion() {\n    if (cachedStartingVersion != null) {\n      return cachedStartingVersion;\n    }\n    // Note: returning a version beyond latest snapshot version won't be a problem as callers\n    // of this function won't use the version to retrieve snapshot(refer to\n    // [[getStartingOffset]]).\n    // TODO(#5319): fetch spark config if CDF is supported.\n    boolean allowOutOfRange = false;\n\n    if (options.startingVersion().isDefined()) {\n      DeltaStartingVersion startingVersion = options.startingVersion().get();\n      if (startingVersion instanceof StartingVersionLatest$) {\n        Snapshot latestSnapshot = snapshotManager.loadLatestSnapshot();\n        // \"latest\": start reading from the next commit\n        cachedStartingVersion = Optional.of(latestSnapshot.getVersion() + 1);\n        return cachedStartingVersion;\n      } else if (startingVersion instanceof StartingVersion) {\n        long version = ((StartingVersion) startingVersion).version();\n        if (!validateProtocolAt(spark, snapshotManager, engine, version)) {\n          // When starting from a given version, we don't require that the snapshot of this\n          // version can be reconstructed, even though the input table is technically in an\n          // inconsistent state. If the snapshot cannot be reconstructed, then the protocol\n          // check is skipped, so this is technically not safe, but we keep it this way for\n          // historical reasons.\n          snapshotManager.checkVersionExists(\n              version, /* mustBeRecreatable= */ false, /* allowOutOfRange= */ false);\n        }\n        cachedStartingVersion = Optional.of(version);\n        return cachedStartingVersion;\n      }\n    } else if (options.startingTimestamp().isDefined()) {\n      // Set enforceRetention to true to align with V1 connector\n      Timestamp timestamp =\n          new DeltaTimeTravelSpec(\n                  /* timestamp= */ options.startingTimestamp().map(Literal$.MODULE$::apply),\n                  /* version= */ Option.empty(),\n                  /* creationSource= */ Some.apply(\"sparkMicroBatchStream\"),\n                  /* enforceRetention= */ true)\n              .getTimestamp(spark.sessionState().conf());\n      long startingVersion =\n          getStartingVersionFromTimestamp(\n              spark, snapshotManager, engine, timestamp, allowOutOfRange);\n      cachedStartingVersion = Optional.of(startingVersion);\n      return cachedStartingVersion;\n    }\n    cachedStartingVersion = Optional.empty();\n    return cachedStartingVersion;\n  }\n\n  /**\n   * Returns the earliest commit version whose timestamp is >= the provided timestamp.\n   *\n   * <p>This method fetches the commit at the given timestamp via\n   * [[DeltaSnapshotManager.getActiveCommitAtTime]], computes the starting version using\n   * [[DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp]], and validates the protocol at the\n   * returned version.\n   */\n  private static long getStartingVersionFromTimestamp(\n      SparkSession spark,\n      DeltaSnapshotManager snapshotManager,\n      Engine engine,\n      Timestamp timestamp,\n      boolean canExceedLatest) {\n    // TODO(#5999): optimize duplicate loadLatestSnapshot calls\n    DeltaHistoryManager.Commit commit =\n        snapshotManager.getActiveCommitAtTime(\n            timestamp.getTime(),\n            /* canReturnLastCommit= */ true,\n            /* mustBeRecreatable= */ false,\n            /* canReturnEarliestCommit= */ true);\n    long latestVersion = snapshotManager.loadLatestSnapshot().getVersion();\n    long startingVersion =\n        DeltaStreamUtils.getStartingVersionFromCommitAtTimestamp(\n            /* timeZone= */ spark.sessionState().conf().sessionLocalTimeZone(),\n            /* commitTimestamp= */ commit.getTimestamp(),\n            /* commitVersion= */ commit.getVersion(),\n            /* latestVersion= */ latestVersion,\n            /* timestamp= */ timestamp,\n            /* canExceedLatest= */ canExceedLatest);\n    if (startingVersion <= latestVersion) {\n      validateProtocolAt(spark, snapshotManager, engine, startingVersion);\n    }\n    return startingVersion;\n  }\n\n  /**\n   * Validate the protocol at a given version. If the snapshot reconstruction fails for any other\n   * reason than unsupported feature exception, we suppress it. This allows fallback to previous\n   * behavior where the starting version/timestamp was not mandatory to point to reconstructable\n   * snapshot.\n   *\n   * <p>This is the DSv2 Kernel-based implementation of DeltaSource.validateProtocolAt.\n   *\n   * <p>Returns true when the validation was performed and succeeded.\n   */\n  private static boolean validateProtocolAt(\n      SparkSession spark, DeltaSnapshotManager snapshotManager, Engine engine, long version) {\n    boolean alwaysValidateProtocol =\n        (Boolean)\n            spark\n                .sessionState()\n                .conf()\n                .getConf(DeltaSQLConf.FAST_DROP_FEATURE_STREAMING_ALWAYS_VALIDATE_PROTOCOL());\n    if (!alwaysValidateProtocol) {\n      return false;\n    }\n\n    try {\n      // Attempt to construct a snapshot at the startingVersion to validate the protocol\n      // If snapshot reconstruction fails, fall back to old behavior where the only\n      // requirement was for the commit to exist\n      snapshotManager.loadSnapshotAt(version);\n      return true;\n    } catch (UnsupportedTableFeatureException e) {\n      // Re-throw fatal unsupported table feature exceptions\n      throw e;\n    } catch (Exception e) {\n      // Suppress non-fatal exceptions\n      logger.warn(\"Protocol validation failed at version {} with: {}\", version, e.getMessage());\n      return false;\n    }\n  }\n\n  ////////////////////\n  // getFileChanges //\n  ////////////////////\n\n  /**\n   * Get file changes with rate limiting applied. Mimics DeltaSource.getFileChangesWithRateLimit.\n   *\n   * @param fromVersion The starting version (exclusive with fromIndex)\n   * @param fromIndex The starting index within fromVersion (exclusive)\n   * @param isInitialSnapshot Whether this is the initial snapshot\n   * @param limits Rate limits to apply (Optional.empty() for no limits)\n   * @return An iterator of IndexedFile with rate limiting applied\n   */\n  CloseableIterator<IndexedFile> getFileChangesWithRateLimit(\n      long fromVersion,\n      long fromIndex,\n      boolean isInitialSnapshot,\n      Optional<DeltaSource.AdmissionLimits> limits) {\n    // TODO(#5319): getFileChangesForCDC if CDC is enabled.\n\n    CloseableIterator<IndexedFile> changes =\n        getFileChanges(\n            fromVersion, fromIndex, isInitialSnapshot, /* endOffset= */ Optional.empty());\n\n    // Take each change until we've seen the configured number of addFiles. Some changes don't\n    // represent file additions; we retain them for offset tracking, but they don't count toward\n    // the maxFilesPerTrigger conf.\n    if (limits.isPresent()) {\n      DeltaSource.AdmissionLimits admissionLimits = limits.get();\n      changes = changes.takeWhile(admissionLimits::admit);\n    }\n\n    // TODO(#5318): Stop at schema change barriers\n    return changes;\n  }\n\n  /**\n   * Get file changes between fromVersion/fromIndex and endOffset. This is the Kernel-based\n   * implementation of DeltaSource.getFileChanges.\n   *\n   * <p>Package-private for testing.\n   *\n   * @param fromVersion The starting version (exclusive with fromIndex)\n   * @param fromIndex The starting index within fromVersion (exclusive)\n   * @param isInitialSnapshot Whether this is the initial snapshot\n   * @param endOffset The end offset (inclusive), or empty to read all available commits\n   * @return An iterator of IndexedFile representing the file changes\n   */\n  CloseableIterator<IndexedFile> getFileChanges(\n      long fromVersion,\n      long fromIndex,\n      boolean isInitialSnapshot,\n      Optional<DeltaSourceOffset> endOffset) {\n\n    CloseableIterator<IndexedFile> result;\n\n    if (isInitialSnapshot) {\n      // Lazily combine snapshot files with delta logs starting from fromVersion + 1.\n      // filterDeltaLogs handles the case when no commits exist after fromVersion.\n      CloseableIterator<IndexedFile> snapshotFiles = getSnapshotFiles(fromVersion);\n      CloseableIterator<IndexedFile> deltaChanges = filterDeltaLogs(fromVersion + 1, endOffset);\n      result = snapshotFiles.combine(deltaChanges);\n    } else {\n      result = filterDeltaLogs(fromVersion, endOffset);\n    }\n\n    // Check start boundary (exclusive)\n    result =\n        result.filter(\n            file ->\n                file.getVersion() > fromVersion\n                    || (file.getVersion() == fromVersion && file.getIndex() > fromIndex));\n\n    // If endOffset is provided, we are getting a batch on a constructed range so we should use\n    // the endOffset as the limit.\n    // Otherwise, we are looking for a new offset, so we try to use the latestOffset we found for\n    // Trigger.availableNow() as limit. We know endOffset <= lastOffsetForTriggerAvailableNow.\n    Optional<DeltaSourceOffset> lastOffsetForThisScan =\n        endOffset.or(() -> lastOffsetForTriggerAvailableNow);\n\n    // Check end boundary (inclusive)\n    if (lastOffsetForThisScan.isPresent()) {\n      DeltaSourceOffset bound = lastOffsetForThisScan.get();\n      result =\n          result.takeWhile(\n              file ->\n                  file.getVersion() < bound.reservoirVersion()\n                      || (file.getVersion() == bound.reservoirVersion()\n                          && file.getIndex() <= bound.index()));\n    }\n\n    return result;\n  }\n\n  private CloseableIterator<IndexedFile> filterDeltaLogs(\n      long startVersion, Optional<DeltaSourceOffset> endOffset) {\n    Optional<Long> endVersionOpt =\n        endOffset.isPresent() ? Optional.of(endOffset.get().reservoirVersion()) : Optional.empty();\n\n    if (endVersionOpt.isPresent()) {\n      // Cap endVersion to the latest available version. The Kernel's getTableChanges requires\n      // endVersion to be an actual existing version or empty.\n      long latestVersion = snapshotAtSourceInit.getVersion();\n      if (endVersionOpt.get() > latestVersion) {\n        // This could happen because:\n        // 1. data could be added after snapshotAtSourceInit was captured.\n        // 2. buildOffsetFromIndexedFile bumps the version up by one when we hit the END_INDEX.\n        // TODO(#5318): consider caching the latest version to avoid loading a new snapshot.\n        // TODO(#5318): kernel should ideally relax this constraint.\n        endVersionOpt = Optional.of(snapshotManager.loadLatestSnapshot().getVersion());\n      }\n\n      // After capping, check if startVersion is beyond the endVersion.\n      // This can happen when all files in the batch come from the initial snapshot\n      // (e.g., offset was bumped to next version due to END_INDEX, but no new commits exist).\n      if (startVersion > endVersionOpt.get()) {\n        return Utils.toCloseableIterator(Collections.emptyIterator());\n      }\n    } else {\n      // When endOffset is empty (offset discovery), check if startVersion exceeds the current\n      // latest version. We must load the current latest (not snapshotAtSourceInit) because new\n      // commits may have arrived since stream initialization.\n      long currentLatestVersion = snapshotManager.loadLatestSnapshot().getVersion();\n      if (startVersion > currentLatestVersion) {\n        return Utils.toCloseableIterator(Collections.emptyIterator());\n      }\n    }\n\n    CommitRange commitRange;\n    try {\n      commitRange = snapshotManager.getTableChanges(engine, startVersion, endVersionOpt);\n    } catch (io.delta.kernel.exceptions.CommitRangeNotFoundException e) {\n      // If the requested version range doesn't exist (e.g., we're asking for version 6 when\n      // the table only has versions 0-5).\n      return Utils.toCloseableIterator(Collections.emptyIterator());\n    }\n\n    // Use getCommitActionsFromRangeUnsafe instead of CommitRange.getCommitActions() because:\n    // 1. CommitRange.getCommitActions() requires a snapshot at exactly the startVersion, but when\n    //    startingVersion option is used, we may not be able to recreate that exact snapshot\n    //    (e.g., if log files have been cleaned up after checkpointing).\n    // 2. This matches DSv1 behavior which uses snapshotAtSourceInit's P&M to interpret all\n    //    AddFile actions and performs per-commit protocol validation.\n    CloseableIterator<CommitActions> commitsIterator =\n        StreamingHelper.getCommitActionsFromRangeUnsafe(\n            engine,\n            (io.delta.kernel.internal.commitrange.CommitRangeImpl) commitRange,\n            snapshotAtSourceInit.getPath(),\n            ACTION_SET);\n\n    return commitsIterator.flatMap(\n        commit -> processCommitToIndexedFiles(commit, startVersion, endOffset));\n  }\n\n  /**\n   * Processes a single commit and returns an iterator of IndexedFiles wrapped with BEGIN/END\n   * sentinels.\n   */\n  private CloseableIterator<IndexedFile> processCommitToIndexedFiles(\n      CommitActions commit, long startVersion, Optional<DeltaSourceOffset> endOffsetOpt) {\n    try {\n      long version = commit.getVersion();\n\n      // First pass: Validate the commit and decide whether to skip it.\n      //\n      // We must validate the ENTIRE commit before emitting ANY files. This is a correctness\n      // requirement: commits could contain both AddFiles and RemoveFiles.\n      // If we emitted AddFiles before discovering a RemoveFile(dataChange=true) later in the\n      // commit, downstream would produce incorrect results.\n      //\n      // TODO(#5318): consider caching the commit actions to avoid reading the same commit twice.\n      // TODO(#5319): don't verify metadata action when schema tracking is enabled\n      boolean shouldSkipCommit =\n          validateCommitAndDecideSkipping(\n              commit,\n              version,\n              startVersion,\n              snapshotAtSourceInit.getPath(),\n              endOffsetOpt,\n              /* verifyMetadataAction= */ true);\n\n      // Second pass: Build a lazy iterator of IndexedFiles.\n      //\n      //   BEGIN (BASE_INDEX) + actual file actions + END (END_INDEX)\n      //\n      // These sentinel IndexedFiles have null file actions and are used for proper offset\n      // tracking:\n      //   - BASE_INDEX: marks \"before any files in this version\", allowing the offset to\n      //                 reference the start of a version.\n      //   - END_INDEX:  marks end of version, triggers version advancement in\n      //                 buildOffsetFromIndexedFile to skip re-reading completed versions.\n      //\n      // See DeltaSource.addBeginAndEndIndexOffsetsForVersion for the Scala equivalent.\n      CloseableIterator<IndexedFile> fileActions =\n          shouldSkipCommit\n              ? Utils.toCloseableIterator(Collections.emptyIterator())\n              : getFilesFromCommit(commit, version);\n      CloseableIterator<IndexedFile> inner =\n          Utils.singletonCloseableIterator(\n                  new IndexedFile(version, DeltaSourceOffset.BASE_INDEX(), /* addFile= */ null))\n              .combine(fileActions)\n              .combine(\n                  Utils.singletonCloseableIterator(\n                      new IndexedFile(\n                          version, DeltaSourceOffset.END_INDEX(), /* addFile= */ null)));\n\n      // Wrap the iterator so that closing it also closes the CommitActions, releasing its\n      // internal ActionsIterator and any associated file handles / parsed data.\n      return wrapIteratorWithCommitClose(inner, commit);\n    } catch (Exception e) {\n      // commit is not a CloseableIterator, we need to close it manually.\n      Utils.closeCloseables(commit);\n      throw (e instanceof RuntimeException) ? (RuntimeException) e : new RuntimeException(e);\n    }\n  }\n\n  /**\n   * Wraps an iterator so that closing it also closes the given {@link CommitActions}, releasing its\n   * internal resources. This is necessary because {@link CommitActions} is an {@link AutoCloseable}\n   * but not a {@link CloseableIterator}, so closing the iterator chain alone does not close the\n   * {@link CommitActions} that produced it. Package-private for testing.\n   */\n  static CloseableIterator<IndexedFile> wrapIteratorWithCommitClose(\n      CloseableIterator<IndexedFile> inner, CommitActions commit) {\n    return new CloseableIterator<IndexedFile>() {\n      @Override\n      public boolean hasNext() {\n        return inner.hasNext();\n      }\n\n      @Override\n      public IndexedFile next() {\n        return inner.next();\n      }\n\n      @Override\n      public void close() throws IOException {\n        Utils.closeCloseables(inner, commit);\n      }\n    };\n  }\n\n  private CloseableIterator<IndexedFile> getFilesFromCommit(CommitActions commit, long version) {\n    // Assign each IndexedFile a unique index within the commit. We use a mutable array\n    // because variables captured by a lambda must be effectively final (never reassigned).\n    long[] fileIndex = {0};\n\n    return commit\n        .getActions()\n        .flatMap(\n            batch -> {\n              // Processing each batch eagerly because they are already loaded into memory.\n              List<IndexedFile> files = new ArrayList<>();\n              fileIndex[0] = addIndexedFilesAndReturnNextIndex(batch, version, fileIndex[0], files);\n              return Utils.toCloseableIterator(files.iterator());\n            });\n  }\n\n  /**\n   * Validates a commit, fail the stream if it's invalid and decides whether to skip it. Mimics\n   * DeltaSource.validateCommitAndDecideSkipping in Scala.\n   *\n   * @param commit the CommitActions representing a single commit\n   * @param version the commit version\n   * @param batchStartVersion Starting version of the batch being processed\n   * @param tablePath the path to the Delta table\n   * @param endOffsetOpt optional end offset for boundary checking\n   * @param verifyMetadataAction Whether to verify metadata action compatibility\n   * @return true if the commit should be skipped (no AddFiles emitted), false otherwise\n   * @throws RuntimeException if the commit is invalid.\n   */\n  private boolean validateCommitAndDecideSkipping(\n      CommitActions commit,\n      long version,\n      long batchStartVersion,\n      String tablePath,\n      Optional<DeltaSourceOffset> endOffsetOpt,\n      boolean verifyMetadataAction) {\n    // If endOffset is at the beginning of this version, exit early.\n    if (endOffsetOpt.isPresent()) {\n      DeltaSourceOffset endOffset = endOffsetOpt.get();\n      if (endOffset.reservoirVersion() == version\n          && endOffset.index() == DeltaSourceOffset.BASE_INDEX()) {\n        return false;\n      }\n    }\n\n    // TODO(#5319): Implement ignoreChanges & ignoreFileDeletion (deprecated)\n    // A check on the source table that disallows changes on the source data.\n    boolean shouldAllowChanges = skipChangeCommits;\n    // A check on the source table that disallows commits that only include deletes to the data.\n    boolean shouldAllowDeletes = shouldAllowChanges || options.ignoreDeletes();\n\n    boolean hasFileAdd = false;\n    boolean shouldSkipCommit = false;\n    Metadata metadataAction = null;\n    String removeFileActionPath = null;\n\n    try (CloseableIterator<ColumnarBatch> actionsIter = commit.getActions()) {\n      while (actionsIter.hasNext()) {\n        ColumnarBatch batch = actionsIter.next();\n        int numRows = batch.getSize();\n        for (int rowId = 0; rowId < numRows; rowId++) {\n          // Track AddFile(dataChange=true)\n          Optional<AddFile> addOpt = StreamingHelper.getAddFileWithDataChange(batch, rowId);\n          if (addOpt.isPresent()) {\n            hasFileAdd = true;\n          }\n\n          // Track RemoveFile(dataChange=true)\n          Optional<RemoveFile> removeOpt = StreamingHelper.getDataChangeRemove(batch, rowId);\n          if (removeOpt.isPresent()) {\n            // skip change commits include delete-only commits\n            shouldSkipCommit = skipChangeCommits;\n            if (removeFileActionPath == null) {\n              removeFileActionPath = removeOpt.get().getPath();\n            }\n          }\n\n          // Track Metadata for read-incompatible schema changes.\n          Optional<Metadata> metadataOpt = StreamingHelper.getMetadata(batch, rowId);\n          if (metadataOpt.isPresent()) {\n            Metadata metadata = metadataOpt.get();\n            Preconditions.checkArgument(\n                metadataAction == null,\n                \"Should not encounter two metadata actions in the same commit of version %d\",\n                version);\n            metadataAction = metadata;\n            Long batchEndVersion =\n                endOffsetOpt.map(DeltaSourceOffset::reservoirVersion).orElse(null);\n            if (verifyMetadataAction) {\n              checkReadIncompatibleSchemaChanges(\n                  metadata,\n                  version,\n                  batchStartVersion,\n                  batchEndVersion,\n                  /* validatedDuringStreamStart */ false);\n            }\n          }\n        }\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(\"Failed to process commit at version \" + version, e);\n    }\n\n    if (removeFileActionPath != null) {\n      if (hasFileAdd && !shouldAllowChanges) {\n        // Commit contains data changes (adds + removes) and changes are disallowed.\n        // TODO(#5319): log CommitInfo action's operation instead of path\n        throw (RuntimeException)\n            DeltaErrors.deltaSourceIgnoreChangesError(version, removeFileActionPath, tablePath);\n      } else if (!hasFileAdd && !shouldAllowDeletes) {\n        // Commit contains only removes (deletes) and deletes are disallowed.\n        throw (RuntimeException)\n            DeltaErrors.deltaSourceIgnoreDeleteError(version, removeFileActionPath, tablePath);\n      }\n    }\n\n    return shouldSkipCommit;\n  }\n\n  /**\n   * Narrow waist to verify a metadata action for read-incompatible schema changes, specifically: 1.\n   * Any column mapping related schema changes (rename / drop) columns 2. Standard\n   * read-compatibility changes including: a) No missing columns b) No data type changes c) No\n   * read-incompatible nullability changes If the check fails, we throw an exception to exit the\n   * stream. If lazy log initialization is required, we also run a one time scan to safely\n   * initialize the metadata tracking log upon any non-additive schema change failures.\n   *\n   * @param metadata Metadata that contains a potential schema change\n   * @param version Version for the metadata action\n   * @param batchStartVersion Starting version of the batch being processed\n   * @param batchEndVersion Ending version of the batch being processed, or null for the latest\n   * @param validatedDuringStreamStart Whether this check is being done during stream start.\n   */\n  private void checkReadIncompatibleSchemaChanges(\n      Metadata metadata,\n      long version,\n      long batchStartVersion,\n      Long batchEndVersion,\n      boolean validatedDuringStreamStart) {\n    logger.info(\n        \"checking read incompatibility with schema at version {}, inside batch[{}, {}].\",\n        version,\n        batchStartVersion,\n        batchEndVersion != null ? batchEndVersion : \"latest\");\n\n    Metadata newMetadata, oldMetadata;\n    if (version < snapshotAtSourceInit.getVersion()) {\n      newMetadata = snapshotAtSourceInit.getMetadata();\n      oldMetadata = metadata;\n    } else {\n      newMetadata = metadata;\n      oldMetadata = snapshotAtSourceInit.getMetadata();\n    }\n\n    // Table ID has changed during streaming\n    if (!Objects.equals(newMetadata.getId(), oldMetadata.getId())) {\n      throw (RuntimeException)\n          DeltaErrors.differentDeltaTableReadByStreamingSource(\n              newMetadata.getId(), oldMetadata.getId());\n    }\n\n    // Partition column change will be ignored if user enable the unsafe flag\n    Seq<String> newPartitionColumns, oldPartitionColumns;\n    if (schemaReadOptions.allowUnsafeStreamingReadOnPartitionColumnChanges()) {\n      newPartitionColumns = (Seq<String>) Seq$.MODULE$.empty();\n      oldPartitionColumns = (Seq<String>) Seq$.MODULE$.empty();\n    } else {\n      newPartitionColumns =\n          CollectionConverters.asScala(\n                  VectorUtils.toJavaList(newMetadata.getPartitionColumns()).stream()\n                      .map(Object::toString)\n                      .collect(Collectors.toList()))\n              .toSeq();\n      oldPartitionColumns =\n          CollectionConverters.asScala(\n                  VectorUtils.toJavaList(oldMetadata.getPartitionColumns()).stream()\n                      .map(Object::toString)\n                      .collect(Collectors.toList()))\n              .toSeq();\n    }\n\n    checkNonAdditiveSchemaChanges(\n        oldMetadata,\n        newMetadata,\n        oldPartitionColumns,\n        newPartitionColumns,\n        validatedDuringStreamStart);\n\n    // Other standard read compatibility changes\n    if (!validatedDuringStreamStart\n        || !schemaReadOptions\n            .forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart()) {\n\n      StructType schemaChange = SchemaUtils.convertKernelSchemaToSparkSchema(metadata.getSchema());\n\n      // There is a schema change. All files after this commit will use `schemaChange`. Hence, we\n      // check whether we can use `schema` (the fixed source schema we use in the same run of the\n      // query) to read these new files safely.\n      boolean backfilling = version < snapshotAtSourceInit.getVersion();\n\n      DeltaStreamUtils.SchemaCompatibilityResult checkResult =\n          DeltaStreamUtils.checkSchemaChangesWhenNoSchemaTracking(\n              schemaChange,\n              readSchemaAtSourceInit,\n              newPartitionColumns,\n              oldPartitionColumns,\n              backfilling,\n              schemaReadOptions);\n\n      if (!DeltaStreamUtils.SchemaCompatibilityResult$.MODULE$.isCompatible(checkResult)) {\n        boolean isRetryable =\n            DeltaStreamUtils.SchemaCompatibilityResult$.MODULE$.isRetryableIncompatible(\n                checkResult);\n        throw (RuntimeException)\n            DeltaErrors.schemaChangedException(\n                readSchemaAtSourceInit,\n                schemaChange,\n                isRetryable,\n                Some.apply(version),\n                options.containsStartingVersionOrTimestamp());\n      }\n    }\n  }\n\n  // TODO(#5319): schema tracking for non-additive schema changes\n  // TODO(#5319): Extract the entire non-additive schema check into a static utility and share it\n  // with v1 by refactoring DeltaColumnMapping.hasNoColumnMappingSchemaChanges so it can be reused\n  // by both v1 and v2.\n  // Non-additive schema changes include rename column, drop column and change column type\n  private void checkNonAdditiveSchemaChanges(\n      Metadata oldMetadata,\n      Metadata newMetadata,\n      Seq<String> oldPartitionColumns,\n      Seq<String> newPartitionColumns,\n      boolean validatedDuringStreamStart) {\n    StructType sparkNewSchema =\n        SchemaUtils.convertKernelSchemaToSparkSchema(newMetadata.getSchema());\n    StructType sparkOldSchema =\n        SchemaUtils.convertKernelSchemaToSparkSchema(oldMetadata.getSchema());\n\n    boolean shouldTrackSchema;\n    if (schemaReadOptions.typeWideningEnabled()\n        && schemaReadOptions.enableSchemaTrackingForTypeWidening()\n        && TypeWidening.containsWideningTypeChanges(sparkOldSchema, sparkNewSchema)) {\n      // If schema tracking is enabled for type widening, we will detect widening type changes and\n      // block the stream until the user sets `allowSourceColumnTypeChange` - similar to handling\n      // DROP/RENAME for column mapping.\n      shouldTrackSchema = true;\n    } else if (schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges()) {\n      shouldTrackSchema = false;\n    } else {\n      ColumnMappingMode NONE = ColumnMappingMode.NONE;\n      ColumnMappingMode oldMode =\n          ColumnMapping.getColumnMappingMode(oldMetadata.getConfiguration());\n      ColumnMappingMode newMode =\n          ColumnMapping.getColumnMappingMode(newMetadata.getConfiguration());\n      if (oldMode != NONE && newMode != NONE) {\n        Preconditions.checkArgument(oldMode == newMode, \"changing mode is not supported\");\n        shouldTrackSchema =\n            DeltaColumnMapping.hasColMappingOrPartitionSchemaChange(\n                sparkNewSchema,\n                sparkOldSchema,\n                newPartitionColumns,\n                oldPartitionColumns,\n                /* isBothColumnMappingEnabled */ true);\n      } else if (oldMode == NONE && newMode != NONE) {\n        // TODO(#5319): We should disallow user to upgrade column mapping mode for now since we\n        // don't support schema tracking\n        shouldTrackSchema = true;\n      } else {\n        // Prohibit reading across a downgrade.\n        shouldTrackSchema = oldMode != NONE && newMode == NONE;\n      }\n    }\n\n    if (shouldTrackSchema) {\n      throw (RuntimeException)\n          DeltaErrors.blockStreamingReadsWithIncompatibleNonAdditiveSchemaChanges(\n              spark,\n              sparkOldSchema,\n              sparkNewSchema,\n              !validatedDuringStreamStart,\n              /* isV2DataSource= */ true);\n    }\n  }\n\n  /**\n   * Check read-incompatible schema changes during stream (re)start so we could fail fast.\n   *\n   * <p>This is called ONCE during the first latestOffset call to catch edge cases that normal\n   * per-commit validation (checkReadIncompatibleSchemaChanges) misses.\n   *\n   * <p><b>Why needed?</b> Normal validation only checks commits with metadata actions. If a stream\n   * starts at version 1 with the latest version is version 3 and there is a schema change at\n   * version 2, validateCommits only validates version 2's schema against version 3 (SAME, passes).\n   * But version 1 files may have an incompatible older schema that was never checked since version\n   * 1 has no metadata action.\n   *\n   * <p>This method explicitly loads and validates the snapshot in the scan range, regardless of\n   * whether it has a metadata action, catching such incompatibilities before planInputPartitions\n   *\n   * <p>Skipped if schema tracking log is already initialized.\n   *\n   * @param batchStartVersion Start version we want to verify read compatibility against\n   * @param batchEndVersion Optionally, if we are checking against an existing constructed batch\n   *     during streaming initialization, we would also like to verify all schema changes in between\n   *     as well before we can lazily initialize the schema log if needed.\n   */\n  private void checkReadIncompatibleSchemaChangeOnStreamStartOnce(\n      long batchStartVersion, Long batchEndVersion) {\n    // TODO(#5319): skip if enable schema tracking log\n\n    if (hasCheckedReadIncompatibleSchemaChangesOnStreamStart) return;\n\n    SnapshotImpl startVersionSnapshot = null;\n    Exception err = null;\n    try {\n      startVersionSnapshot = (SnapshotImpl) snapshotManager.loadSnapshotAt(batchStartVersion);\n    } catch (Exception e) {\n      err = e;\n    }\n\n    // Cannot perfectly verify column mapping schema changes if we cannot compute a start snapshot.\n    if (!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges()\n        && schemaReadOptions.isStreamingFromColumnMappingTable()\n        && (err != null)) {\n      throw (RuntimeException)\n          DeltaErrors.failedToGetSnapshotDuringColumnMappingStreamingReadCheck(err);\n    }\n\n    // Perform schema check if we need to, considering all escape flags.\n    if (!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges()\n        || schemaReadOptions.typeWideningEnabled()\n        || !schemaReadOptions\n            .forceEnableStreamingReadOnReadIncompatibleSchemaChangesDuringStreamStart()) {\n      if (startVersionSnapshot != null) {\n        checkReadIncompatibleSchemaChanges(\n            startVersionSnapshot.getMetadata(),\n            startVersionSnapshot.getVersion(),\n            batchStartVersion,\n            batchEndVersion,\n            /* validatedDuringStreamStart= */ true);\n        // If end version is defined (i.e. we have a pending batch), let's also eagerly check all\n        // intermediate schema changes against the stream read schema to capture corners cases such\n        // as rename and rename back.\n        if (batchEndVersion != null) {\n          for (Map.Entry<Long, Metadata> entry :\n              StreamingHelper.collectMetadataActionsFromRangeUnsafe(\n                      batchStartVersion,\n                      Optional.of(batchEndVersion),\n                      snapshotManager,\n                      engine,\n                      snapshotAtSourceInit.getPath())\n                  .entrySet()) {\n            long version = entry.getKey();\n            Metadata metadata = entry.getValue();\n            checkReadIncompatibleSchemaChanges(\n                metadata,\n                version,\n                batchStartVersion,\n                batchEndVersion,\n                /* validatedDuringStreamStart= */ true);\n          }\n        }\n      }\n    }\n\n    // Mark as checked\n    hasCheckedReadIncompatibleSchemaChangesOnStreamStart = true;\n  }\n\n  /**\n   * Validates that the analysis-time schema matches the latest snapshot schema. This catches cases\n   * where the table schema changed after the streaming query was analyzed, and the user restarts\n   * the stream with a stale DataFrame.\n   *\n   * @param dataSchema data columns from analysis time (excludes partition columns)\n   * @param partitionSchema partition columns from analysis time\n   * @param snapshotSchema full table schema from the latest snapshot at stream start\n   */\n  private void validateSchemaCompatibilityOnStartup(\n      StructType dataSchema, StructType partitionSchema, StructType snapshotSchema) {\n    // Reconstruct the full analysis-time table schema from dataSchema + partitionSchema.\n    // StructType is immutable — add() returns a new instance without modifying the original.\n    StructType querySchema = dataSchema;\n    for (StructField field : partitionSchema.fields()) {\n      querySchema = querySchema.add(field);\n    }\n\n    // Compare the structural schema of the analysis-time schema and snapshot schema.\n    if (!DataType.equalsStructurally(querySchema, snapshotSchema, /* ignoreNullability */ false)) {\n      throw DeltaErrors.streamingSchemaMismatchOnRestart(querySchema, snapshotSchema);\n    }\n  }\n\n  /**\n   * Extracts IndexedFiles from a batch of actions for a given version and adds them to the output\n   * list. Assigns an index to each IndexedFile.\n   *\n   * @return The next available index after processing this batch\n   */\n  private long addIndexedFilesAndReturnNextIndex(\n      ColumnarBatch batch, long version, long startIndex, List<IndexedFile> output) {\n    long index = startIndex;\n    for (int rowId = 0; rowId < batch.getSize(); rowId++) {\n      // Only include AddFiles with dataChange=true. Skip changes that optimize or reorganize\n      // data without changing the logical content.\n      Optional<AddFile> addOpt = StreamingHelper.getAddFileWithDataChange(batch, rowId);\n      if (addOpt.isPresent()) {\n        AddFile addFile = addOpt.get();\n        output.add(new IndexedFile(version, index++, addFile));\n      }\n    }\n\n    return index;\n  }\n\n  /**\n   * Get all files from a snapshot at the specified version, sorted by modificationTime and path,\n   * with indices assigned sequentially, and wrapped with BEGIN/END sentinels.\n   *\n   * <p>Mimics DeltaSourceSnapshot in DSv1.\n   *\n   * @param version The snapshot version to read\n   * @return An iterator of IndexedFile representing the snapshot files\n   */\n  private CloseableIterator<IndexedFile> getSnapshotFiles(long version) {\n    InitialSnapshotCache cache = cachedInitialSnapshot.get();\n\n    if (cache != null && cache.version != null && cache.version == version) {\n      return Utils.toCloseableIterator(cache.files.iterator());\n    }\n\n    List<IndexedFile> indexedFiles = loadAndValidateSnapshot(version);\n\n    cachedInitialSnapshot.set(\n        new InitialSnapshotCache(version, Collections.unmodifiableList(indexedFiles)));\n\n    return Utils.toCloseableIterator(indexedFiles.iterator());\n  }\n\n  /** Loads snapshot files at the specified version. */\n  private List<IndexedFile> loadAndValidateSnapshot(long version) {\n    Snapshot snapshot = snapshotManager.loadSnapshotAt(version);\n\n    Scan scan = snapshot.getScanBuilder().build();\n\n    List<AddFile> addFiles = new ArrayList<>();\n    try (CloseableIterator<FilteredColumnarBatch> filesIter = scan.getScanFiles(engine)) {\n      while (filesIter.hasNext()) {\n        FilteredColumnarBatch filteredBatch = filesIter.next();\n\n        // Get all AddFiles from the batch. Include both dataChange=true and dataChange=false\n        // (checkpoint files) files. StreamingHelper.getAddFile respects the selection vector\n        // to filter out duplicate files (e.g., stats re-collection re-adds files with updated\n        // stats).\n        for (int rowId = 0; rowId < filteredBatch.getData().getSize(); rowId++) {\n          Optional<AddFile> addOpt = StreamingHelper.getAddFile(filteredBatch, rowId);\n          if (addOpt.isPresent()) {\n            addFiles.add(addOpt.get());\n\n            // Basic memory protection: each IndexedFile is ~1-2KB (path, stats, partition values,\n            // etc.).\n            // This limit aims to prevent OOM for large tables.\n            // TODO(#5318): support large tables and remove this limit.\n            if (addFiles.size() > maxInitialSnapshotFiles) {\n              throw (RuntimeException)\n                  DeltaErrors.initialSnapshotTooLargeForStreaming(\n                      version, addFiles.size(), maxInitialSnapshotFiles, tablePath);\n            }\n          }\n        }\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(\n          String.format(\"Failed to read snapshot files at version %d\", version), e);\n    }\n\n    // TODO(#5318): For large snapshots, consider external sorting.\n    // CRITICAL: Sort by modificationTime, then path for deterministic ordering\n    addFiles.sort(\n        Comparator.comparing(AddFile::getModificationTime).thenComparing(AddFile::getPath));\n\n    // Build IndexedFile list with sentinels\n    List<IndexedFile> indexedFiles = new ArrayList<>();\n\n    // Add BEGIN sentinel\n    indexedFiles.add(new IndexedFile(version, DeltaSourceOffset.BASE_INDEX(), null));\n\n    // Add data files with sequential indices starting from 0\n    for (int i = 0; i < addFiles.size(); i++) {\n      indexedFiles.add(new IndexedFile(version, i, addFiles.get(i)));\n    }\n\n    // Add END sentinel\n    indexedFiles.add(new IndexedFile(version, DeltaSourceOffset.END_INDEX(), null));\n\n    return indexedFiles;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/SparkPartitionReader.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.connector.read.PartitionReader;\nimport org.apache.spark.sql.execution.datasources.FilePartition;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport scala.Function1;\nimport scala.collection.Iterator;\n\npublic class SparkPartitionReader<T> implements PartitionReader<T> {\n  // Function that produces an Iterator for a given file.\n  private final Function1<PartitionedFile, Iterator<InternalRow>> readFunc;\n  private final FilePartition partition;\n\n  // Index of the next file to read within the partition.\n  private int currentFileIndex = 0;\n\n  // Current iterator for the file being read.\n  private Iterator<T> currentIterator = null;\n\n  public SparkPartitionReader(\n      Function1<PartitionedFile, Iterator<InternalRow>> readFunc, FilePartition partition) {\n    this.readFunc = java.util.Objects.requireNonNull(readFunc, \"readFunc\");\n    this.partition = java.util.Objects.requireNonNull(partition, \"partition\");\n  }\n\n  @Override\n  public boolean next() throws IOException {\n    // Advance to the next available record, opening readers as needed and closing exhausted ones.\n    while (true) {\n      if (currentIterator != null && currentIterator.hasNext()) {\n        return true;\n      }\n\n      closeCurrentIterator();\n\n      if (currentFileIndex >= partition.files().length) {\n        return false;\n      }\n\n      final PartitionedFile file = partition.files()[currentFileIndex++];\n      @SuppressWarnings(\"unchecked\")\n      Iterator<T> it = (Iterator<T>) readFunc.apply(file);\n      currentIterator = it;\n    }\n  }\n\n  @Override\n  public T get() {\n    if (currentIterator == null) {\n      throw new IllegalStateException(\"No current record. Call next() before get().\");\n    }\n    return currentIterator.next();\n  }\n\n  @Override\n  public void close() throws IOException {\n    closeCurrentIterator();\n  }\n\n  private void closeCurrentIterator() throws IOException {\n    if (currentIterator != null) {\n      if (currentIterator instanceof Closeable) {\n        ((Closeable) currentIterator).close();\n      }\n      currentIterator = null;\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/SparkReaderFactory.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.connector.read.InputPartition;\nimport org.apache.spark.sql.connector.read.PartitionReader;\nimport org.apache.spark.sql.connector.read.PartitionReaderFactory;\nimport org.apache.spark.sql.execution.datasources.FilePartition;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.vectorized.ColumnarBatch;\nimport scala.Function1;\nimport scala.collection.Iterator;\n\npublic class SparkReaderFactory implements PartitionReaderFactory {\n  private Function1<PartitionedFile, Iterator<InternalRow>> readFunc;\n  private boolean supportsColumnar;\n\n  public SparkReaderFactory(\n      Function1<PartitionedFile, Iterator<InternalRow>> readFunc, boolean supportsColumnar) {\n    this.readFunc = readFunc;\n    this.supportsColumnar = supportsColumnar;\n  }\n\n  @Override\n  public PartitionReader<ColumnarBatch> createColumnarReader(InputPartition partition) {\n    return new SparkPartitionReader<ColumnarBatch>(readFunc, extractFilePartition(partition));\n  }\n\n  @Override\n  public boolean supportColumnarReads(InputPartition partition) {\n    return supportsColumnar;\n  }\n\n  @Override\n  public PartitionReader<InternalRow> createReader(InputPartition partition) {\n    return new SparkPartitionReader<InternalRow>(readFunc, extractFilePartition(partition));\n  }\n\n  /**\n   * Extracts the FilePartition from the given InputPartition. Handles both DeltaInputPartition (for\n   * partitioned tables) and FilePartition (for non-partitioned tables).\n   */\n  private FilePartition extractFilePartition(InputPartition partition) {\n    if (partition instanceof DeltaInputPartition) {\n      return ((DeltaInputPartition) partition).getFilePartition();\n    } else if (partition instanceof FilePartition) {\n      return (FilePartition) partition;\n    } else {\n      throw new IllegalArgumentException(\n          \"Unexpected partition type: \"\n              + partition.getClass().getName()\n              + \". Expected DeltaInputPartition or FilePartition.\");\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/SparkScan.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static io.delta.spark.internal.v2.utils.ExpressionUtils.dsv2PredicateToCatalystExpression;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.data.ScanStateRow;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.spark.internal.v2.read.deletionvector.DeletionVectorSchemaContext;\nimport io.delta.spark.internal.v2.snapshot.DeltaSnapshotManager;\nimport io.delta.spark.internal.v2.utils.PartitionUtils;\nimport io.delta.spark.internal.v2.utils.ScalaUtils;\nimport java.io.IOException;\nimport java.time.ZoneId;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.catalyst.expressions.Expression;\nimport org.apache.spark.sql.catalyst.expressions.InterpretedPredicate;\nimport org.apache.spark.sql.connector.expressions.FieldReference;\nimport org.apache.spark.sql.connector.expressions.NamedReference;\nimport org.apache.spark.sql.connector.read.*;\nimport org.apache.spark.sql.connector.read.colstats.ColumnStatistics;\nimport org.apache.spark.sql.connector.read.partitioning.KeyGroupedPartitioning;\nimport org.apache.spark.sql.connector.read.partitioning.Partitioning;\nimport org.apache.spark.sql.connector.read.partitioning.UnknownPartitioning;\nimport org.apache.spark.sql.connector.read.streaming.MicroBatchStream;\nimport org.apache.spark.sql.delta.DeltaOptions;\nimport org.apache.spark.sql.execution.datasources.*;\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetUtils;\nimport org.apache.spark.sql.internal.SQLConf;\nimport org.apache.spark.sql.sources.Filter;\nimport org.apache.spark.sql.types.StringType;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\n\n/** Spark DSV2 Scan implementation backed by Delta Kernel. */\npublic class SparkScan\n    implements Scan,\n        SupportsReportStatistics,\n        SupportsRuntimeV2Filtering,\n        SupportsReportPartitioning {\n\n  /** Supported streaming options for the V2 connector. */\n  private static final List<String> SUPPORTED_STREAMING_OPTIONS =\n      Collections.unmodifiableList(\n          Arrays.asList(\n              DeltaOptions.STARTING_VERSION_OPTION(),\n              DeltaOptions.STARTING_TIMESTAMP_OPTION(),\n              DeltaOptions.MAX_FILES_PER_TRIGGER_OPTION(),\n              DeltaOptions.MAX_BYTES_PER_TRIGGER_OPTION(),\n              DeltaOptions.IGNORE_DELETES_OPTION(),\n              DeltaOptions.SKIP_CHANGE_COMMITS_OPTION(),\n              DeltaOptions.EXCLUDE_REGEX_OPTION()));\n\n  /**\n   * Block list of DeltaOptions that are not supported for streaming in V2 connector. Only\n   * startingVersion, startingTimestamp, maxFilesPerTrigger, maxBytesPerTrigger, ignoreDeletes,\n   * skipChangeCommits, and excludeRegex are supported. User-defined custom options (not in\n   * DeltaOptions) are allowed to pass through.\n   */\n  private static final Set<String> UNSUPPORTED_STREAMING_OPTIONS =\n      Collections.unmodifiableSet(\n          new HashSet<>(\n              Arrays.asList(\n                  DeltaOptions.IGNORE_FILE_DELETION_OPTION().toLowerCase(),\n                  DeltaOptions.IGNORE_CHANGES_OPTION().toLowerCase(),\n                  DeltaOptions.FAIL_ON_DATA_LOSS_OPTION().toLowerCase(),\n                  DeltaOptions.CDC_READ_OPTION().toLowerCase(),\n                  DeltaOptions.CDC_READ_OPTION_LEGACY().toLowerCase(),\n                  DeltaOptions.CDC_END_VERSION().toLowerCase(),\n                  DeltaOptions.CDC_END_TIMESTAMP().toLowerCase(),\n                  DeltaOptions.SCHEMA_TRACKING_LOCATION().toLowerCase(),\n                  DeltaOptions.SCHEMA_TRACKING_LOCATION_ALIAS().toLowerCase(),\n                  DeltaOptions.STREAMING_SOURCE_TRACKING_ID().toLowerCase(),\n                  DeltaOptions.ALLOW_SOURCE_COLUMN_DROP().toLowerCase(),\n                  DeltaOptions.ALLOW_SOURCE_COLUMN_RENAME().toLowerCase(),\n                  DeltaOptions.ALLOW_SOURCE_COLUMN_TYPE_CHANGE().toLowerCase())));\n\n  private final DeltaSnapshotManager snapshotManager;\n  private final Snapshot initialSnapshot;\n  private final StructType readDataSchema;\n  private final StructType dataSchema;\n  private final StructType partitionSchema;\n  private final Predicate[] pushedToKernelFilters;\n  private final Filter[] dataFilters;\n  private final io.delta.kernel.Scan kernelScan;\n  private final Optional<Statistics> catalogStats;\n  private final Configuration hadoopConf;\n  private final CaseInsensitiveStringMap options;\n  private final scala.collection.immutable.Map<String, String> scalaOptions;\n  private final SQLConf sqlConf;\n  private final ZoneId zoneId;\n\n  // Planned input files and stats\n  private List<PartitionedFile> partitionedFiles = new ArrayList<>();\n  private long totalBytes = 0L;\n  // Estimated size in bytes accounting for column projection, used for query optimizer cost\n  // estimation\n  private long estimatedSizeInBytes = 0L;\n  private volatile boolean planned = false;\n\n  // Runtime predicates applied after planning (using Set for order-independent comparison)\n  private final Set<org.apache.spark.sql.connector.expressions.filter.Predicate>\n      appliedRuntimePredicates = new HashSet<>();\n\n  public SparkScan(\n      DeltaSnapshotManager snapshotManager,\n      Snapshot initialSnapshot,\n      StructType dataSchema,\n      StructType partitionSchema,\n      StructType readDataSchema,\n      Predicate[] pushedToKernelFilters,\n      Filter[] dataFilters,\n      io.delta.kernel.Scan kernelScan,\n      Optional<Statistics> catalogStats,\n      CaseInsensitiveStringMap options) {\n\n    this.snapshotManager = Objects.requireNonNull(snapshotManager, \"snapshotManager is null\");\n    this.initialSnapshot = Objects.requireNonNull(initialSnapshot, \"initialSnapshot is null\");\n    this.dataSchema = Objects.requireNonNull(dataSchema, \"dataSchema is null\");\n    this.partitionSchema = Objects.requireNonNull(partitionSchema, \"partitionSchema is null\");\n    this.readDataSchema = Objects.requireNonNull(readDataSchema, \"readDataSchema is null\");\n    this.pushedToKernelFilters =\n        pushedToKernelFilters == null ? new Predicate[0] : pushedToKernelFilters.clone();\n    this.dataFilters = dataFilters == null ? new Filter[0] : dataFilters.clone();\n    this.kernelScan = Objects.requireNonNull(kernelScan, \"kernelScan is null\");\n    this.catalogStats = Objects.requireNonNull(catalogStats, \"catalogStats is null\");\n    this.options = Objects.requireNonNull(options, \"options is null\");\n    this.scalaOptions = ScalaUtils.toScalaMap(options);\n    this.hadoopConf = SparkSession.active().sessionState().newHadoopConfWithOptions(scalaOptions);\n    this.sqlConf = SQLConf.get();\n    this.zoneId = ZoneId.of(sqlConf.sessionLocalTimeZone());\n  }\n\n  /**\n   * Read schema for the scan, which is the projection of data columns followed by partition\n   * columns.\n   */\n  @Override\n  public StructType readSchema() {\n    final List<StructField> fields =\n        new ArrayList<>(readDataSchema.fields().length + partitionSchema.fields().length);\n    Collections.addAll(fields, readDataSchema.fields());\n    Collections.addAll(fields, partitionSchema.fields());\n    return new StructType(fields.toArray(new StructField[0]));\n  }\n\n  /**\n   * Override columnarSupportMode to explicitly declare whether this scan supports columnar\n   * (vectorized) reading. Without this override, the default {@code PARTITION_DEFINED} mode causes\n   * Spark to eagerly call {@code planInputPartitions()} during query planning to check\n   * per-partition columnar support, triggering unnecessary early file enumeration.\n   *\n   * <p>Since columnar support is uniform across all partitions (determined by schema compatibility\n   * and table features, not by individual files), we can declare it at the scan level to avoid this\n   * overhead.\n   *\n   * <p>This must stay consistent with the vectorized reader decision in {@link\n   * PartitionUtils#createDeltaParquetReaderFactory}. In particular, deletion-vector-enabled tables\n   * augment the read schema with internal columns (e.g., {@code __delta_internal_is_row_deleted}),\n   * which changes the schema passed to the vectorized reader check. We replicate that logic here to\n   * ensure the scan-level declaration matches the per-partition reader behavior.\n   */\n  @Override\n  public Scan.ColumnarSupportMode columnarSupportMode() {\n    // When the table supports deletion vectors, the reader factory augments the read schema\n    // with internal columns via DeletionVectorSchemaContext. Reuse the same class here so the\n    // batch-read check stays consistent — if DeletionVectorSchemaContext adds new fields in\n    // the future, this code path picks them up automatically.\n    StructType schemaForBatchCheck =\n        PartitionUtils.tableSupportsDeletionVectors(initialSnapshot)\n            ? new DeletionVectorSchemaContext(readDataSchema, partitionSchema)\n                .getSchemaWithDvColumn()\n            : readDataSchema;\n\n    return ParquetUtils.isBatchReadSupportedForSchema(sqlConf, schemaForBatchCheck)\n        ? Scan.ColumnarSupportMode.SUPPORTED\n        : Scan.ColumnarSupportMode.UNSUPPORTED;\n  }\n\n  @Override\n  public Batch toBatch() {\n    ensurePlanned();\n    return new SparkBatch(\n        initialSnapshot,\n        dataSchema,\n        partitionSchema,\n        readDataSchema,\n        partitionedFiles,\n        pushedToKernelFilters,\n        dataFilters,\n        totalBytes,\n        scalaOptions,\n        hadoopConf);\n  }\n\n  @Override\n  public MicroBatchStream toMicroBatchStream(String checkpointLocation) {\n    DeltaOptions deltaOptions = new DeltaOptions(scalaOptions, sqlConf);\n    // Validate streaming options immediately after constructing DeltaOptions\n    validateStreamingOptions(deltaOptions);\n    return new SparkMicroBatchStream(\n        snapshotManager,\n        // Loads a fresh snapshot as the baseline for schema change detection and table identity\n        // checks. SparkScan's initialSnapshot is from analysis time and may be stale by stream\n        // start/restart.\n        // Matches V1's DeltaDataSource.createSource() behavior.\n        snapshotManager.loadLatestSnapshot(),\n        hadoopConf,\n        SparkSession.active(),\n        deltaOptions,\n        getTablePath(),\n        dataSchema,\n        partitionSchema,\n        readDataSchema,\n        dataFilters != null ? dataFilters : new Filter[0],\n        scalaOptions != null ? scalaOptions : scala.collection.immutable.Map$.MODULE$.empty());\n  }\n\n  @Override\n  public String description() {\n    final String pushed =\n        Arrays.stream(pushedToKernelFilters)\n            .map(Object::toString)\n            .collect(Collectors.joining(\", \"));\n    final String data =\n        Arrays.stream(dataFilters).map(Object::toString).collect(Collectors.joining(\", \"));\n    return String.format(Locale.ROOT, \"PushedFilters: [%s], DataFilters: [%s]\", pushed, data);\n  }\n\n  @Override\n  public Statistics estimateStatistics() {\n    ensurePlanned();\n    final long plannedBytes = estimatedSizeInBytes;\n\n    // When catalog stats are available and CBO is enabled, combine table-level stats\n    // (for numRows/columnStats) with planned file stats (for sizeInBytes).\n    // This mirrors V1's LogicalRelation.computeStats() which gates column stats on\n    // conf.cboEnabled || conf.planStatsEnabled.\n    boolean useCatalogStats = sqlConf.cboEnabled() || sqlConf.planStatsEnabled();\n    if (useCatalogStats && catalogStats.isPresent()) {\n      final Statistics stats = catalogStats.get();\n      return new Statistics() {\n        @Override\n        public OptionalLong sizeInBytes() {\n          // Planned file size is authoritative (even if 0 for an empty table)\n          return OptionalLong.of(plannedBytes);\n        }\n\n        @Override\n        public OptionalLong numRows() {\n          // TODO: Use accurate row count from planned files (sum of AddFile.numRecords)\n          //  instead of catalog stats, which are stale (point-in-time from ANALYZE) and\n          //  not adjusted for partition pruning.\n          return stats.numRows();\n        }\n\n        @Override\n        public Map<NamedReference, ColumnStatistics> columnStats() {\n          // TODO: After partition pruning, column stats (e.g. min, max, nullCount,\n          //  distinctCount) could be tightened based on the pruned file-level stats.\n          return stats.columnStats();\n        }\n      };\n    }\n\n    // No catalog stats available or CBO disabled — return stats from planned files only\n    return new Statistics() {\n      @Override\n      public OptionalLong sizeInBytes() {\n        return OptionalLong.of(plannedBytes);\n      }\n\n      @Override\n      public OptionalLong numRows() {\n        // Row count is unknown without catalog stats\n        return OptionalLong.empty();\n      }\n    };\n  }\n\n  /**\n   * Computes the estimated size in bytes accounting for column projection.\n   *\n   * <p>This mirrors what {@code SizeInBytesOnlyStatsPlanVisitor.visitUnaryNode} (from Spark code)\n   * would compute for a {@code Project} over a {@code LogicalRelation}: {@code sizeInBytes =\n   * childSizeInBytes * outputRowSize / childRowSize}\n   *\n   * <p>Where:\n   *\n   * <ul>\n   *   <li><b>childRowSize</b> = {@code ROW_OVERHEAD + dataSchema + partitionSchema} (equivalent to\n   *       LogicalRelation output)\n   *   <li><b>outputRowSize</b> = {@code ROW_OVERHEAD + readDataSchema + partitionSchema}\n   *       (equivalent to Project output)\n   * </ul>\n   *\n   * <p>When catalog column stats are available, uses per-column {@code avgLen} instead of {@code\n   * defaultSize()} for more accurate size estimation, mirroring {@code\n   * EstimationUtils.getSizePerRow()} behavior.\n   *\n   * <p>This provides consistent statistics with the v1 code path (LogicalRelation + visitUnaryNode\n   * from Spark code directory).\n   *\n   * @param totalBytes the total size in bytes of the planned files (raw physical size)\n   * @return the estimated size in bytes after accounting for column projection\n   */\n  private long computeEstimatedSizeWithColumnProjection(long totalBytes) {\n    if (totalBytes <= 0) {\n      return totalBytes;\n    }\n\n    // Row overhead constant, matching EstimationUtils.getSizePerRow (from Spark)\n    final int ROW_OVERHEAD = 8;\n\n    // Use avgLen from catalog column stats when available for more accurate estimation\n    Map<String, OptionalLong> avgLenByColumn = getAvgLenByColumn();\n\n    final long fullSchemaRowSize =\n        ROW_OVERHEAD\n            + getSchemaSize(dataSchema, avgLenByColumn)\n            + getSchemaSize(partitionSchema, avgLenByColumn);\n    final long outputRowSize = ROW_OVERHEAD + getSchemaSize(readSchema(), avgLenByColumn);\n\n    long estimatedBytes = (totalBytes * outputRowSize) / fullSchemaRowSize;\n\n    return Math.max(1L, estimatedBytes);\n  }\n\n  /**\n   * Returns a map of column name to avgLen from catalog column stats, used to improve size\n   * estimation accuracy when catalog stats are available.\n   */\n  private Map<String, OptionalLong> getAvgLenByColumn() {\n    if (!catalogStats.isPresent()) {\n      return Collections.emptyMap();\n    }\n    Map<NamedReference, ColumnStatistics> colStats = catalogStats.get().columnStats();\n    if (colStats.isEmpty()) {\n      return Collections.emptyMap();\n    }\n    Map<String, OptionalLong> result = new HashMap<>();\n    for (Map.Entry<NamedReference, ColumnStatistics> entry : colStats.entrySet()) {\n      result.put(entry.getKey().fieldNames()[0], entry.getValue().avgLen());\n    }\n    return result;\n  }\n\n  /**\n   * Computes the estimated in-memory size for a schema, using avgLen from catalog stats when\n   * available, falling back to defaultSize(). Mirrors EstimationUtils.getSizePerRow(). For\n   * StringType columns with avgLen, adds UTF8String overhead (base + offset + numBytes = 12 bytes).\n   */\n  private static long getSchemaSize(StructType schema, Map<String, OptionalLong> avgLenByColumn) {\n    long size = 0;\n    for (StructField field : schema.fields()) {\n      OptionalLong avgLen = avgLenByColumn.getOrDefault(field.name(), OptionalLong.empty());\n      if (avgLen.isPresent()) {\n        if (field.dataType() instanceof StringType) {\n          // UTF8String: base + offset + numBytes (matching EstimationUtils.getSizePerRow)\n          size += avgLen.getAsLong() + 8 + 4;\n        } else {\n          size += avgLen.getAsLong();\n        }\n      } else {\n        size += field.dataType().defaultSize();\n      }\n    }\n    return size;\n  }\n\n  /**\n   * Get the table path from the scan state.\n   *\n   * @return the table path with trailing slash\n   */\n  public String getTablePath() {\n    final Engine tableEngine = DefaultEngine.create(hadoopConf);\n    final Row scanState = kernelScan.getScanState(tableEngine);\n    final String tableRoot = ScanStateRow.getTableRoot(scanState).toUri().toString();\n    return tableRoot.endsWith(\"/\") ? tableRoot : tableRoot + \"/\";\n  }\n\n  /**\n   * Plan the files to scan by materializing {@link PartitionedFile}s and aggregating size stats.\n   * Ensures all iterators are closed to avoid resource leaks.\n   */\n  private void planScanFiles() {\n    final Engine tableEngine = DefaultEngine.create(hadoopConf);\n    final String tablePath = getTablePath();\n    final Iterator<FilteredColumnarBatch> scanFileBatches = kernelScan.getScanFiles(tableEngine);\n\n    final String[] locations = new String[0];\n    final scala.collection.immutable.Map<String, Object> otherConstantMetadataColumnValues =\n        scala.collection.immutable.Map$.MODULE$.empty();\n\n    while (scanFileBatches.hasNext()) {\n      final FilteredColumnarBatch batch = scanFileBatches.next();\n\n      try (CloseableIterator<Row> addFileRowIter = batch.getRows()) {\n        while (addFileRowIter.hasNext()) {\n          final Row row = addFileRowIter.next();\n          final AddFile addFile = new AddFile(row.getStruct(0));\n\n          final PartitionedFile partitionedFile =\n              PartitionUtils.buildPartitionedFile(addFile, partitionSchema, tablePath, zoneId);\n\n          totalBytes += addFile.getSize();\n          partitionedFiles.add(partitionedFile);\n        }\n      } catch (IOException e) {\n        throw new RuntimeException(e);\n      }\n    }\n\n    // Pre-compute estimated size accounting for column projection\n    estimatedSizeInBytes = computeEstimatedSizeWithColumnProjection(totalBytes);\n  }\n\n  /**\n   * Ensure the scan is planned exactly once in a thread-safe manner, optionally applying runtime\n   * filters.\n   */\n  private synchronized void ensurePlanned(List<RuntimePredicate> runtimePredicates) {\n    // First, ensure planning is done\n    if (!planned) {\n      planScanFiles();\n      planned = true;\n    }\n\n    // Then apply runtime predicates if provided\n    if (runtimePredicates != null && !runtimePredicates.isEmpty()) {\n      // Record the applied predicates for equals/hashCode comparison\n      for (RuntimePredicate filter : runtimePredicates) {\n        appliedRuntimePredicates.add(filter.predicate);\n      }\n\n      List<PartitionedFile> runtimeFilteredPartitionedFiles = new ArrayList<>();\n      for (PartitionedFile pf : this.partitionedFiles) {\n        InternalRow partitionValues = pf.partitionValues();\n        boolean allMatch =\n            runtimePredicates.stream()\n                .allMatch(predicate -> predicate.evaluator.eval(partitionValues));\n        if (allMatch) {\n          runtimeFilteredPartitionedFiles.add(pf);\n        }\n      }\n\n      // Update partitionedFiles, totalBytes, and estimatedSizeInBytes if any partition is\n      // filtered out\n      if (runtimeFilteredPartitionedFiles.size() < this.partitionedFiles.size()) {\n        this.partitionedFiles = runtimeFilteredPartitionedFiles;\n        this.totalBytes =\n            runtimeFilteredPartitionedFiles.stream().mapToLong(PartitionedFile::fileSize).sum();\n        this.estimatedSizeInBytes = computeEstimatedSizeWithColumnProjection(this.totalBytes);\n      }\n    }\n  }\n\n  /** Ensure the scan is planned exactly once in a thread-safe manner. */\n  private void ensurePlanned() {\n    // Pass null to indicate no runtime predicate should be applied - just perform the scan planning\n    ensurePlanned(null);\n  }\n\n  public StructType getDataSchema() {\n    return dataSchema;\n  }\n\n  public StructType getPartitionSchema() {\n    return partitionSchema;\n  }\n\n  public StructType getReadDataSchema() {\n    return readDataSchema;\n  }\n\n  public CaseInsensitiveStringMap getOptions() {\n    return options;\n  }\n\n  public Configuration getConfiguration() {\n    return hadoopConf;\n  }\n\n  @Override\n  public NamedReference[] filterAttributes() {\n    return Arrays.stream(partitionSchema.fields())\n        .map(field -> FieldReference.column(field.name()))\n        .toArray(NamedReference[]::new);\n  }\n\n  @Override\n  public void filter(org.apache.spark.sql.connector.expressions.filter.Predicate[] predicates) {\n\n    // Try to convert runtime predicates to catalyst expressions, then create predicate evaluators\n    // Only track predicates that successfully convert to evaluators\n    List<RuntimePredicate> runtimePredicates = new ArrayList<>();\n    for (org.apache.spark.sql.connector.expressions.filter.Predicate predicate : predicates) {\n      // only the predicates on partition columns will be converted\n      Optional<Expression> catalystExpr =\n          dsv2PredicateToCatalystExpression(predicate, partitionSchema);\n      if (catalystExpr.isPresent()) {\n        InterpretedPredicate predicateEvaluator =\n            org.apache.spark.sql.catalyst.expressions.Predicate.createInterpreted(\n                catalystExpr.get());\n        runtimePredicates.add(new RuntimePredicate(predicate, predicateEvaluator));\n      }\n    }\n\n    if (!runtimePredicates.isEmpty()) {\n      // Apply runtime predicates within the synchronized ensurePlanned method\n      ensurePlanned(runtimePredicates);\n    }\n  }\n\n  /**\n   * Validates that unsupported streaming options are not used. Uses a block list approach - only\n   * blocks known DeltaOptions that are unsupported, allowing user-defined custom options to pass\n   * through.\n   *\n   * <p>Note: DeltaOptions internally uses CaseInsensitiveMap, which preserves the original key\n   * casing but performs case-insensitive lookups.\n   *\n   * @param deltaOptions the DeltaOptions to validate\n   * @throws UnsupportedOperationException if unsupported options are found\n   */\n  static void validateStreamingOptions(DeltaOptions deltaOptions) {\n    List<String> unsupportedOptions = new ArrayList<>();\n    scala.collection.Iterator<String> keysIterator = deltaOptions.options().keysIterator();\n\n    while (keysIterator.hasNext()) {\n      String key = keysIterator.next();\n      // DeltaOptions uses CaseInsensitiveMap with keys already lowercased.\n      if (UNSUPPORTED_STREAMING_OPTIONS.contains(key)) {\n        unsupportedOptions.add(key);\n      }\n    }\n\n    if (!unsupportedOptions.isEmpty()) {\n      throw new UnsupportedOperationException(\n          String.format(\n              \"The following streaming options are not supported: [%s]. \"\n                  + \"Supported options are: [%s].\",\n              String.join(\", \", unsupportedOptions),\n              String.join(\", \", SUPPORTED_STREAMING_OPTIONS)));\n    }\n  }\n\n  /**\n   * Reports partition key expressions to Spark so it can recognize partition-aligned data layout.\n   * Called by V2ScanPartitioningAndOrdering during logical optimization to extract partition keys.\n   * Together with HasPartitionKey on DeltaInputPartition, this enables Spark to eliminate shuffles\n   * for joins and aggregations on partition columns.\n   *\n   * <p>Note: This method triggers scan file materialization via {@link #ensurePlanned()} because\n   * {@code numPartitions} is derived from the planned file count. Since Spark calls this during\n   * logical optimization (before {@link #toBatch()}), this changes when planning occurs compared to\n   * the non-partitioned path. This is functionally correct as {@code ensurePlanned} is idempotent.\n   */\n  @Override\n  public Partitioning outputPartitioning() {\n    // If no partition columns, return unknown partitioning\n    if (partitionSchema.fields().length == 0) {\n      return new UnknownPartitioning(0);\n    }\n\n    ensurePlanned();\n\n    // Create partition key expressions from partition schema\n    org.apache.spark.sql.connector.expressions.Expression[] keys =\n        Arrays.stream(partitionSchema.fields())\n            .map(\n                field ->\n                    (org.apache.spark.sql.connector.expressions.Expression)\n                        FieldReference.column(field.name()))\n            .toArray(org.apache.spark.sql.connector.expressions.Expression[]::new);\n\n    // numPartitions is not used by Spark's KeyGroupedPartitioning handling (Spark derives\n    // partition count from the actual InputPartition[] with HasPartitionKey), so we use the\n    // file count as a reasonable upper-bound estimate.\n    return new KeyGroupedPartitioning(keys, partitionedFiles.size());\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    SparkScan that = (SparkScan) o;\n    return Objects.equals(initialSnapshot.getPath(), that.initialSnapshot.getPath())\n        && initialSnapshot.getVersion() == that.initialSnapshot.getVersion()\n        && Objects.equals(dataSchema, that.dataSchema)\n        && Objects.equals(partitionSchema, that.partitionSchema)\n        && Objects.equals(readDataSchema, that.readDataSchema)\n        && Arrays.equals(pushedToKernelFilters, that.pushedToKernelFilters)\n        && Arrays.equals(dataFilters, that.dataFilters)\n        // ignoring kernelScan because it is derived from Snapshot which is created from tablePath,\n        // with pushed down filters that are also recorded in `pushedToKernelFilters`\n        && Objects.equals(options, that.options)\n        && Objects.equals(appliedRuntimePredicates, that.appliedRuntimePredicates)\n        && Objects.equals(catalogStats, that.catalogStats);\n  }\n\n  @Override\n  public int hashCode() {\n    int result =\n        Objects.hash(\n            catalogStats,\n            initialSnapshot.getPath(),\n            initialSnapshot.getVersion(),\n            dataSchema,\n            partitionSchema,\n            readDataSchema,\n            options,\n            appliedRuntimePredicates);\n    result = 31 * result + Arrays.hashCode(pushedToKernelFilters);\n    result = 31 * result + Arrays.hashCode(dataFilters);\n    return result;\n  }\n\n  /**\n   * Holds a runtime predicate from {@link #filter(Predicate[])} along with its compiled evaluator.\n   *\n   * <p>Only created for predicates that can be successfully converted to Catalyst expressions\n   * (typically predicates on partition columns) and compiled into InterpretedPredicate evaluators.\n   * Predicates that cannot be converted are not instantiated as RuntimePredicate objects.\n   */\n  private static class RuntimePredicate {\n    final org.apache.spark.sql.connector.expressions.filter.Predicate predicate;\n    final InterpretedPredicate evaluator;\n\n    RuntimePredicate(\n        org.apache.spark.sql.connector.expressions.filter.Predicate predicate,\n        InterpretedPredicate evaluator) {\n      this.predicate = predicate;\n      this.evaluator = evaluator;\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/SparkScanBuilder.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.expressions.And;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.spark.internal.v2.snapshot.DeltaSnapshotManager;\nimport io.delta.spark.internal.v2.utils.ExpressionUtils;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.apache.spark.sql.connector.read.ScanBuilder;\nimport org.apache.spark.sql.connector.read.Statistics;\nimport org.apache.spark.sql.connector.read.SupportsPushDownFilters;\nimport org.apache.spark.sql.connector.read.SupportsPushDownRequiredColumns;\nimport org.apache.spark.sql.sources.Filter;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\n\n/**\n * A Spark ScanBuilder implementation that wraps Delta Kernel's ScanBuilder. This allows Spark to\n * use Delta Kernel for reading Delta tables.\n */\npublic class SparkScanBuilder\n    implements ScanBuilder, SupportsPushDownRequiredColumns, SupportsPushDownFilters {\n\n  private io.delta.kernel.ScanBuilder kernelScanBuilder;\n  private final Snapshot initialSnapshot;\n  private final DeltaSnapshotManager snapshotManager;\n  private final StructType dataSchema;\n  private final StructType partitionSchema;\n  private final StructType tableSchema;\n  private final Optional<Statistics> catalogStats;\n  private final CaseInsensitiveStringMap options;\n  private final Set<String> partitionColumnSet;\n  private StructType requiredDataSchema;\n  // pushedKernelPredicates: Predicates that have been pushed down to the Delta Kernel for\n  // evaluation.\n  // pushedSparkFilters: The same pushed predicates, but represented using Spark’s {@link Filter}\n  // API (needed because Spark operates on Filter objects while the Kernel uses Predicate)\n  private Predicate[] pushedKernelPredicates;\n  private Filter[] pushedSparkFilters;\n  private Filter[] dataFilters;\n\n  /**\n   * Creates a SparkScanBuilder with the given snapshot and configuration.\n   *\n   * @param tableName the name of the table\n   * @param initialSnapshot Snapshot created during connector setup\n   * @param snapshotManager the snapshot manager for this table\n   * @param dataSchema the data schema (non-partition columns)\n   * @param partitionSchema the partition schema\n   * @param tableSchema the full table schema (all columns) for filter type alignment\n   * @param catalogStats optional V2 Statistics converted from catalog stats\n   * @param options scan options\n   */\n  public SparkScanBuilder(\n      String tableName,\n      io.delta.kernel.Snapshot initialSnapshot,\n      DeltaSnapshotManager snapshotManager,\n      StructType dataSchema,\n      StructType partitionSchema,\n      StructType tableSchema,\n      Optional<Statistics> catalogStats,\n      CaseInsensitiveStringMap options) {\n    this.initialSnapshot = requireNonNull(initialSnapshot, \"initialSnapshot is null\");\n    this.kernelScanBuilder = initialSnapshot.getScanBuilder();\n    this.snapshotManager = requireNonNull(snapshotManager, \"snapshotManager is null\");\n    this.dataSchema = requireNonNull(dataSchema, \"dataSchema is null\");\n    this.partitionSchema = requireNonNull(partitionSchema, \"partitionSchema is null\");\n    this.tableSchema = requireNonNull(tableSchema, \"tableSchema is null\");\n    this.catalogStats = requireNonNull(catalogStats, \"catalogStats is null\");\n    this.options = requireNonNull(options, \"options is null\");\n    this.requiredDataSchema = this.dataSchema;\n    this.partitionColumnSet =\n        Arrays.stream(this.partitionSchema.fields())\n            .map(f -> f.name().toLowerCase(Locale.ROOT))\n            .collect(Collectors.toSet());\n    this.pushedKernelPredicates = new Predicate[0];\n    this.dataFilters = new Filter[0];\n  }\n\n  @Override\n  public void pruneColumns(StructType requiredSchema) {\n    requireNonNull(requiredSchema, \"requiredSchema is null\");\n    this.requiredDataSchema =\n        new StructType(\n            Arrays.stream(requiredSchema.fields())\n                .filter(f -> !partitionColumnSet.contains(f.name().toLowerCase(Locale.ROOT)))\n                .toArray(StructField[]::new));\n  }\n\n  @Override\n  public Filter[] pushFilters(Filter[] filters) {\n    List<Filter> kernelSupportedFilters = new ArrayList<>();\n    List<Predicate> convertedKernelPredicates = new ArrayList<>();\n    List<Filter> dataFilterList = new ArrayList<>();\n    List<Filter> postScanFilters = new ArrayList<>();\n\n    for (Filter filter : filters) {\n      ExpressionUtils.FilterClassificationResult classification =\n          ExpressionUtils.classifyFilter(filter, partitionColumnSet, tableSchema);\n      // Collect kernel predicates if supported\n      if (classification.isKernelSupported) {\n        convertedKernelPredicates.add(classification.kernelPredicate.get());\n        if (!classification.isPartialConversion) {\n          // Add filter to kernelSupportedFilters if it is fully converted\n          // TODO: add partially converted Spark filter as well\n          // right now we only have the partially converted kernel predicate\n          kernelSupportedFilters.add(filter);\n        }\n      }\n\n      // Collect data filters\n      if (classification.isDataFilter) {\n        dataFilterList.add(filter);\n      }\n\n      // Collect post-scan filters\n      // Filters with the following characteristics need to be evaluated after delta kernel scan:\n      // 1. filters that are not supported by delta kernel, thus kernel cannot apply them during\n      // scan\n      // 2. filters that are not fully converted to kernel predicates, thus the unconverted part\n      // needs to be evaluated after scan\n      // 3. filters that are data filters, as kernel only evaluate data filter based on min/max\n      // stats, thus need to be evaluated with actual data after scan\n      //\n      // Fully converted partition filters are used to prune partitions during scan. Only the\n      // partitions that satisfy the filters will be scanned, so no need for post-scan evaluation.\n      if (!classification.isKernelSupported\n          || classification.isPartialConversion\n          || classification.isDataFilter) {\n        postScanFilters.add(filter);\n      }\n    }\n\n    this.pushedSparkFilters = kernelSupportedFilters.toArray(new Filter[0]);\n    this.pushedKernelPredicates = convertedKernelPredicates.toArray(new Predicate[0]);\n    if (this.pushedKernelPredicates.length > 0) {\n      Optional<Predicate> kernelAnd = Arrays.stream(this.pushedKernelPredicates).reduce(And::new);\n      this.kernelScanBuilder = this.kernelScanBuilder.withFilter(kernelAnd.get());\n    }\n    this.dataFilters = dataFilterList.toArray(new Filter[0]);\n    return postScanFilters.toArray(new Filter[0]);\n  }\n\n  @Override\n  public Filter[] pushedFilters() {\n    return this.pushedSparkFilters;\n  }\n\n  @Override\n  public org.apache.spark.sql.connector.read.Scan build() {\n    return new SparkScan(\n        snapshotManager,\n        initialSnapshot,\n        dataSchema,\n        partitionSchema,\n        requiredDataSchema,\n        pushedKernelPredicates,\n        dataFilters,\n        kernelScanBuilder.build(),\n        catalogStats,\n        options);\n  }\n\n  CaseInsensitiveStringMap getOptions() {\n    return options;\n  }\n\n  StructType getDataSchema() {\n    return dataSchema;\n  }\n\n  StructType getPartitionSchema() {\n    return partitionSchema;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/deletionvector/ColumnVectorWithFilter.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read.deletionvector;\n\nimport org.apache.spark.sql.types.Decimal;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.vectorized.ColumnVector;\nimport org.apache.spark.sql.vectorized.ColumnarArray;\nimport org.apache.spark.sql.vectorized.ColumnarMap;\nimport org.apache.spark.unsafe.types.UTF8String;\n\n/**\n * A column vector that applies row-level filtering using a row ID mapping.\n *\n * <p>Wraps an existing column vector and remaps row indices during data access, effectively\n * filtering the original data to only expose the live subset of rows without copying data.\n *\n * <p>Follows Apache Iceberg's ColumnVectorWithFilter pattern.\n */\npublic class ColumnVectorWithFilter extends ColumnVector {\n  private final ColumnVector delegate;\n  private final int[] rowIdMapping;\n  private volatile ColumnVectorWithFilter[] children = null;\n\n  public ColumnVectorWithFilter(ColumnVector delegate, int[] rowIdMapping) {\n    super(delegate.dataType());\n    this.delegate = delegate;\n    this.rowIdMapping = rowIdMapping;\n  }\n\n  @Override\n  public void close() {\n    delegate.close();\n  }\n\n  @Override\n  public boolean hasNull() {\n    return delegate.hasNull();\n  }\n\n  @Override\n  public int numNulls() {\n    // Computing the actual number of nulls with rowIdMapping is expensive.\n    // It is OK to overestimate and return the number of nulls in the original vector.\n    return delegate.numNulls();\n  }\n\n  @Override\n  public boolean isNullAt(int rowId) {\n    return delegate.isNullAt(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public boolean getBoolean(int rowId) {\n    return delegate.getBoolean(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public byte getByte(int rowId) {\n    return delegate.getByte(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public short getShort(int rowId) {\n    return delegate.getShort(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public int getInt(int rowId) {\n    return delegate.getInt(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public long getLong(int rowId) {\n    return delegate.getLong(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public float getFloat(int rowId) {\n    return delegate.getFloat(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public double getDouble(int rowId) {\n    return delegate.getDouble(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public ColumnarArray getArray(int rowId) {\n    return delegate.getArray(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public ColumnarMap getMap(int rowId) {\n    return delegate.getMap(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public Decimal getDecimal(int rowId, int precision, int scale) {\n    return delegate.getDecimal(rowIdMapping[rowId], precision, scale);\n  }\n\n  @Override\n  public UTF8String getUTF8String(int rowId) {\n    return delegate.getUTF8String(rowIdMapping[rowId]);\n  }\n\n  @Override\n  public byte[] getBinary(int rowId) {\n    return delegate.getBinary(rowIdMapping[rowId]);\n  }\n\n  /**\n   * Returns the child vector at the given ordinal, wrapped with the same row ID mapping.\n   *\n   * <p>Uses double-checked locking for thread-safe lazy initialization: the volatile {@code\n   * children} field allows lock-free reads after initialization, while the synchronized block\n   * ensures at-most-once creation. All children are created eagerly within the lock to avoid\n   * per-element races.\n   */\n  @Override\n  public ColumnVector getChild(int ordinal) {\n    if (children == null) {\n      synchronized (this) {\n        if (children == null) {\n          // Eagerly create all children to avoid race condition on children[ordinal] access\n          StructType structType = (StructType) dataType();\n          ColumnVectorWithFilter[] newChildren =\n              new ColumnVectorWithFilter[structType.fields().length];\n          for (int i = 0; i < newChildren.length; i++) {\n            newChildren[i] = new ColumnVectorWithFilter(delegate.getChild(i), rowIdMapping);\n          }\n          children = newChildren;\n        }\n      }\n    }\n    return children[ordinal];\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/deletionvector/DeletionVectorReadFunction.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read.deletionvector;\n\nimport io.delta.spark.internal.v2.utils.CloseableIterator;\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.catalyst.ProjectingInternalRow;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.vectorized.ColumnVector;\nimport org.apache.spark.sql.vectorized.ColumnarBatch;\nimport scala.Function1;\nimport scala.collection.Iterator;\nimport scala.runtime.AbstractFunction1;\n\n/**\n * Wraps a Parquet reader function to apply deletion vector filtering.\n *\n * <p>This function:\n *\n * <ol>\n *   <li>Reads rows from the base Parquet reader (which includes the is_row_deleted column)\n *   <li>Filters out deleted rows (where is_row_deleted != 0)\n *   <li>Projects out the is_row_deleted column from the output\n * </ol>\n *\n * <p>The returned iterator implements {@link java.io.Closeable} to ensure proper resource cleanup\n * of the underlying Parquet reader, even when the iterator is not fully consumed.\n *\n * <p>The reader mode (row-based vs vectorized) is determined once at scan planning time and does\n * not change mid-stream. See {@link #wrap}.\n */\npublic class DeletionVectorReadFunction\n    extends AbstractFunction1<PartitionedFile, Iterator<InternalRow>> implements Serializable {\n\n  private static final long serialVersionUID = 1L;\n\n  /** Byte value in the DV column indicating the row is NOT deleted (row should be kept). */\n  private static final byte ROW_NOT_DELETED = 0;\n\n  private final Function1<PartitionedFile, Iterator<InternalRow>> baseReadFunc;\n  private final DeletionVectorSchemaContext dvSchemaContext;\n  private final boolean isVectorizedReader;\n\n  private DeletionVectorReadFunction(\n      Function1<PartitionedFile, Iterator<InternalRow>> baseReadFunc,\n      DeletionVectorSchemaContext dvSchemaContext,\n      boolean isVectorizedReader) {\n    this.baseReadFunc = baseReadFunc;\n    this.dvSchemaContext = dvSchemaContext;\n    this.isVectorizedReader = isVectorizedReader;\n  }\n\n  @Override\n  public Iterator<InternalRow> apply(PartitionedFile file) {\n    if (isVectorizedReader) {\n      return applyBatch(file);\n    } else {\n      return applyRow(file);\n    }\n  }\n\n  /** Row-based: filter deleted rows and project out the DV column. */\n  private Iterator<InternalRow> applyRow(PartitionedFile file) {\n    int dvColumnIndex = dvSchemaContext.getDvColumnIndex();\n    ProjectingInternalRow projection =\n        ProjectingInternalRow.apply(\n            dvSchemaContext.getOutputSchema(), dvSchemaContext.getOutputColumnOrdinals());\n\n    return CloseableIterator.wrap(baseReadFunc.apply(file))\n        .filterCloseable(row -> row.getByte(dvColumnIndex) == ROW_NOT_DELETED)\n        .mapCloseable(\n            row -> {\n              projection.project(row);\n              return (InternalRow) projection;\n            });\n  }\n\n  /** Vectorized: filter active rows and project out the DV column from each batch. */\n  @SuppressWarnings(\"unchecked\")\n  private Iterator<InternalRow> applyBatch(PartitionedFile file) {\n    int dvColumnIndex = dvSchemaContext.getDvColumnIndex();\n\n    // In vectorized mode Spark passes ColumnarBatch via type erasure as InternalRow.\n    Iterator<Object> baseIterator = (Iterator<Object>) (Iterator<?>) baseReadFunc.apply(file);\n    return (Iterator<InternalRow>)\n        (Iterator<?>)\n            CloseableIterator.wrap(baseIterator)\n                .mapCloseable(\n                    item -> {\n                      if (item instanceof ColumnarBatch) {\n                        return filterAndProjectBatch((ColumnarBatch) item, dvColumnIndex);\n                      }\n                      throw new IllegalStateException(\n                          \"Expected ColumnarBatch when vectorized reader is enabled, but got: \"\n                              + item.getClass());\n                    });\n  }\n\n  /** Filter active rows and project out the DV column from a ColumnarBatch. */\n  private static ColumnarBatch filterAndProjectBatch(ColumnarBatch batch, int dvColumnIndex) {\n    int[] activeRows = findActiveRows(batch, dvColumnIndex);\n    ColumnVector[] filteredColumns = buildFilteredColumns(batch, dvColumnIndex, activeRows);\n    return new ColumnarBatch(filteredColumns, activeRows.length);\n  }\n\n  /**\n   * Build projected output columns by dropping the DV column and applying active row mapping. The\n   * excluded DV column's lifecycle is managed by the base Spark vectorized reader, not by us.\n   */\n  private static ColumnVector[] buildFilteredColumns(\n      ColumnarBatch batch, int dvColumnIndex, int[] activeRows) {\n    ColumnVector[] filteredColumns = new ColumnVector[batch.numCols() - 1];\n    int outputIndex = 0;\n    for (int inputIndex = 0; inputIndex < batch.numCols(); inputIndex++) {\n      if (inputIndex == dvColumnIndex) {\n        continue;\n      }\n      filteredColumns[outputIndex++] =\n          new ColumnVectorWithFilter(batch.column(inputIndex), activeRows);\n    }\n    return filteredColumns;\n  }\n\n  /** Find indices of rows where DV column is 0 (not deleted). */\n  private static int[] findActiveRows(ColumnarBatch batch, int dvColumnIndex) {\n    ColumnVector dvColumn = batch.column(dvColumnIndex);\n    int[] temp = new int[batch.numRows()];\n    int count = 0;\n    for (int i = 0; i < batch.numRows(); i++) {\n      if (dvColumn.getByte(i) == ROW_NOT_DELETED) {\n        temp[count++] = i;\n      }\n    }\n    return Arrays.copyOf(temp, count);\n  }\n\n  /** Factory method to wrap a reader function with DV filtering. */\n  public static DeletionVectorReadFunction wrap(\n      Function1<PartitionedFile, Iterator<InternalRow>> baseReadFunc,\n      DeletionVectorSchemaContext dvSchemaContext,\n      boolean isVectorizedReader) {\n    return new DeletionVectorReadFunction(baseReadFunc, dvSchemaContext, isVectorizedReader);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/read/deletionvector/DeletionVectorSchemaContext.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read.deletionvector;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport org.apache.spark.sql.delta.DeltaParquetFileFormat;\nimport org.apache.spark.sql.types.StructType;\nimport scala.collection.immutable.Seq;\n\n/**\n * Schema context for deletion vector processing in the V2 connector.\n *\n * <p>Encapsulates schema with DV column and pre-computed indices needed for DV filtering.\n */\npublic class DeletionVectorSchemaContext implements Serializable {\n\n  private static final long serialVersionUID = 1L;\n\n  private final StructType schemaWithDvColumn;\n  private final int dvColumnIndex;\n  private final int inputColumnCount;\n  private final StructType outputSchema;\n  private final Seq<Object> outputColumnOrdinals;\n\n  /**\n   * Create a DV schema context for encapsulating schema info and indices needed for DV filtering.\n   *\n   * @param readDataSchema original data schema without DV column\n   * @param partitionSchema partition columns schema\n   * @throws IllegalArgumentException if readDataSchema already contains the DV column\n   */\n  public DeletionVectorSchemaContext(StructType readDataSchema, StructType partitionSchema) {\n    // Validate that readDataSchema doesn't already contain the DV column to ensure the DV column\n    // is added only once. While Delta uses the \"__delta_internal_\" prefix as a naming convention\n    // for internal columns (listed in DeltaColumnMapping.DELTA_INTERNAL_COLUMNS), there's no\n    // enforced schema validation that prevents users from creating such columns. This check\n    // provides a safety guard in the V2 connector.\n    String dvColumnName = DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME();\n    if (Arrays.asList(readDataSchema.fieldNames()).contains(dvColumnName)) {\n      throw new IllegalArgumentException(\n          \"readDataSchema already contains the deletion vector column: \" + dvColumnName);\n    }\n    this.schemaWithDvColumn =\n        readDataSchema.add(DeltaParquetFileFormat.IS_ROW_DELETED_STRUCT_FIELD());\n    this.dvColumnIndex =\n        schemaWithDvColumn.fieldIndex(DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME());\n    this.inputColumnCount = schemaWithDvColumn.fields().length + partitionSchema.fields().length;\n    this.outputSchema = readDataSchema.merge(partitionSchema, /* handleDuplicateColumns= */ false);\n    // Pre-compute output column ordinals: all indices except dvColumnIndex.\n    int[] ordinals = new int[inputColumnCount - 1];\n    int idx = 0;\n    for (int i = 0; i < inputColumnCount; i++) {\n      if (i != dvColumnIndex) {\n        ordinals[idx++] = i;\n      }\n    }\n    this.outputColumnOrdinals = scala.Predef.wrapIntArray(ordinals).toSeq();\n  }\n\n  /** Returns schema with the __delta_internal_is_row_deleted column added. */\n  public StructType getSchemaWithDvColumn() {\n    return schemaWithDvColumn;\n  }\n\n  public int getDvColumnIndex() {\n    return dvColumnIndex;\n  }\n\n  public int getInputColumnCount() {\n    return inputColumnCount;\n  }\n\n  public StructType getOutputSchema() {\n    return outputSchema;\n  }\n\n  /** Returns pre-computed output column ordinals for ProjectingInternalRow. */\n  public Seq<Object> getOutputColumnOrdinals() {\n    return outputColumnOrdinals;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/snapshot/DeltaSnapshotManager.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot;\n\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.spark.internal.v2.exception.VersionNotFoundException;\nimport java.util.Optional;\nimport org.apache.spark.annotation.Experimental;\n\n/**\n * Interface for managing Delta table snapshots.\n *\n * <p>This interface provides methods for loading, caching, and querying Delta table snapshots. It\n * supports both current snapshot access and historical snapshot queries based on version or\n * timestamp.\n *\n * <p>Implementations of this interface are responsible for managing snapshot lifecycle, including\n * loading snapshots from storage and maintaining any necessary caching.\n */\n@Experimental\npublic interface DeltaSnapshotManager {\n\n  /**\n   * Loads and returns the latest snapshot of the Delta table.\n   *\n   * @return the latest snapshot of the Delta table\n   */\n  Snapshot loadLatestSnapshot();\n\n  /**\n   * Loads and returns a snapshot at a specific version of the Delta table.\n   *\n   * @param version the version number to load (must be >= 0)\n   * @return the snapshot at the specified version\n   * @throws io.delta.kernel.exceptions.KernelException if the version cannot be loaded\n   */\n  Snapshot loadSnapshotAt(long version);\n\n  /**\n   * Finds and returns the commit that was active at a specific timestamp.\n   *\n   * @param timestampMillis the timestamp in milliseconds since epoch (UTC)\n   * @param canReturnLastCommit if true, returns the last commit if the timestamp is after all\n   *     commits; if false, throws an exception\n   * @param mustBeRecreatable if true, only considers commits that can be fully recreated from\n   *     available log files; if false, considers all commits\n   * @param canReturnEarliestCommit if true, returns the earliest commit if the timestamp is before\n   *     all commits; if false, throws an exception\n   * @return the commit that was active at the specified timestamp\n   * @throws io.delta.kernel.exceptions.KernelException if no suitable commit is found based on the\n   *     provided flags\n   */\n  DeltaHistoryManager.Commit getActiveCommitAtTime(\n      long timestampMillis,\n      boolean canReturnLastCommit,\n      boolean mustBeRecreatable,\n      boolean canReturnEarliestCommit);\n\n  /**\n   * Checks if a specific version of the Delta table exists and is accessible.\n   *\n   * @param version the version to check\n   * @param mustBeRecreatable if true, requires that the version can be fully recreated from\n   *     available log files; if false, only requires that the version's log file exists\n   * @param allowOutOfRange if true, allows versions greater than the latest version without\n   *     throwing an exception; if false, throws exception for out-of-range versions\n   * @throws VersionNotFoundException if the version is not available or does not meet the specified\n   *     criteria\n   */\n  void checkVersionExists(long version, boolean mustBeRecreatable, boolean allowOutOfRange)\n      throws VersionNotFoundException;\n\n  /**\n   * Gets a range of table changes (commits) between start and end versions.\n   *\n   * <p><b>Expected Behavior:</b>\n   *\n   * <ul>\n   *   <li>Returns a {@link io.delta.kernel.CommitRange} representing all commits from the start\n   *       version (inclusive) to the end version (inclusive if provided)\n   *   <li>If endVersion is not provided, the range extends to the latest available version\n   *   <li>The returned CommitRange can be used to iterate through actions in the version range\n   *   <li>This is typically used for streaming and incremental processing scenarios\n   * </ul>\n   *\n   * <p><b>Use Case:</b> Use this method for streaming queries, incremental processing, or CDC\n   * scenarios where you need to process changes between versions.\n   *\n   * @param engine the engine implementation for executing operations\n   * @param startVersion the starting version (inclusive)\n   * @param endVersion optional ending version (inclusive); if not provided, extends to latest\n   * @return a CommitRange representing the specified range of commits\n   */\n  CommitRange getTableChanges(Engine engine, long startVersion, Optional<Long> endVersion);\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/snapshot/PathBasedSnapshotManager.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.CommitRangeBuilder;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.TableManager;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.spark.internal.v2.exception.VersionNotFoundException;\nimport java.util.ArrayList;\nimport java.util.Optional;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.annotation.Experimental;\n\n/** Implementation of DeltaSnapshotManager for managing Delta snapshots for Path-based Table. */\n@Experimental\npublic class PathBasedSnapshotManager implements DeltaSnapshotManager {\n\n  private final String tablePath;\n  private final Engine kernelEngine;\n\n  public PathBasedSnapshotManager(String tablePath, Configuration hadoopConf) {\n    this(tablePath, DefaultEngine.create(requireNonNull(hadoopConf, \"hadoopConf is null\")));\n  }\n\n  public PathBasedSnapshotManager(String tablePath, Engine kernelEngine) {\n    this.tablePath = requireNonNull(tablePath, \"tablePath is null\");\n    this.kernelEngine = requireNonNull(kernelEngine, \"kernelEngine is null\");\n  }\n\n  /**\n   * Loads the latest snapshot of the Delta table.\n   *\n   * @return the newly loaded snapshot\n   */\n  @Override\n  public Snapshot loadLatestSnapshot() {\n    return TableManager.loadSnapshot(tablePath).build(kernelEngine);\n  }\n\n  /**\n   * Loads a specific version of the Delta table.\n   *\n   * @param version the version to load\n   * @return the snapshot at the specified version\n   */\n  @Override\n  public Snapshot loadSnapshotAt(long version) {\n    return TableManager.loadSnapshot(tablePath).atVersion(version).build(kernelEngine);\n  }\n\n  /**\n   * Finds the active commit at a specific timestamp.\n   *\n   * <p>This method searches the Delta table's commit history to find the commit that was active at\n   * the specified timestamp.\n   *\n   * @param timestampMillis the timestamp in milliseconds since epoch (UTC)\n   * @param canReturnLastCommit if true, returns the last commit if the timestamp is after all\n   *     commits\n   * @param mustBeRecreatable if true, only considers commits that can be recreated (i.e., all\n   *     necessary log files are available)\n   * @param canReturnEarliestCommit if true, returns the earliest commit if the timestamp is before\n   *     all commits\n   * @return the commit that was active at the specified timestamp\n   */\n  @Override\n  public DeltaHistoryManager.Commit getActiveCommitAtTime(\n      long timestampMillis,\n      boolean canReturnLastCommit,\n      boolean mustBeRecreatable,\n      boolean canReturnEarliestCommit) {\n    SnapshotImpl snapshot = (SnapshotImpl) loadLatestSnapshot();\n    return DeltaHistoryManager.getActiveCommitAtTimestamp(\n        kernelEngine,\n        snapshot,\n        snapshot.getLogPath(),\n        timestampMillis,\n        mustBeRecreatable,\n        canReturnLastCommit,\n        canReturnEarliestCommit,\n        new ArrayList<>());\n  }\n\n  /**\n   * Checks if a specific version of the Delta table exists and is accessible.\n   *\n   * @param version the version to check\n   * @param mustBeRecreatable if true, requires that the version can be fully recreated from\n   *     available log files\n   * @param allowOutOfRange if true, allows versions greater than the latest version without\n   *     throwing an exception\n   * @throws VersionNotFoundException if the version is not available\n   */\n  @Override\n  public void checkVersionExists(long version, boolean mustBeRecreatable, boolean allowOutOfRange)\n      throws VersionNotFoundException {\n    SnapshotImpl snapshot = (SnapshotImpl) loadLatestSnapshot();\n    long earliest =\n        mustBeRecreatable\n            ? DeltaHistoryManager.getEarliestRecreatableCommit(\n                kernelEngine,\n                snapshot.getLogPath(),\n                Optional.empty() /*earliestRatifiedCommitVersion*/)\n            : DeltaHistoryManager.getEarliestDeltaFile(\n                kernelEngine,\n                snapshot.getLogPath(),\n                Optional.empty() /*earliestRatifiedCommitVersion*/);\n\n    long latest = snapshot.getVersion();\n    if (version < earliest || ((version > latest) && !allowOutOfRange)) {\n      throw new VersionNotFoundException(version, earliest, latest);\n    }\n  }\n\n  @Override\n  public CommitRange getTableChanges(Engine engine, long startVersion, Optional<Long> endVersion) {\n    CommitRangeBuilder builder =\n        TableManager.loadCommitRange(\n            tablePath, CommitRangeBuilder.CommitBoundary.atVersion(startVersion));\n\n    if (endVersion.isPresent()) {\n      builder =\n          builder.withEndBoundary(CommitRangeBuilder.CommitBoundary.atVersion(endVersion.get()));\n    }\n\n    return builder.build(engine);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/snapshot/SnapshotManagerFactory.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot;\n\nimport io.delta.kernel.Meta;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.unitycatalog.UCCatalogManagedClient;\nimport io.delta.spark.internal.v2.snapshot.unitycatalog.UCManagedTableSnapshotManager;\nimport io.delta.spark.internal.v2.snapshot.unitycatalog.UCTableInfo;\nimport io.delta.spark.internal.v2.snapshot.unitycatalog.UCUtils;\nimport io.delta.storage.commit.uccommitcoordinator.UCClient;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.spark.annotation.Experimental;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.delta.coordinatedcommits.UCTokenBasedRestClientFactory$;\n\n/**\n * Factory for creating {@link DeltaSnapshotManager} instances.\n *\n * <p>This factory determines the appropriate snapshot manager based on the table configuration:\n *\n * <ul>\n *   <li>For Unity Catalog managed tables: creates {@link UCManagedTableSnapshotManager}\n *   <li>For path-based tables: creates {@link PathBasedSnapshotManager}\n * </ul>\n */\n@Experimental\npublic final class SnapshotManagerFactory {\n\n  // Utility class - no instances\n  private SnapshotManagerFactory() {}\n\n  /**\n   * Creates a snapshot manager for the given table.\n   *\n   * @param tablePath the filesystem path to the Delta table\n   * @param kernelEngine the pre-configured Kernel {@link Engine} to use for table operations\n   * @param catalogTable optional Spark catalog table metadata\n   * @return a {@link DeltaSnapshotManager} appropriate for the table type\n   */\n  public static DeltaSnapshotManager create(\n      String tablePath, Engine kernelEngine, Optional<CatalogTable> catalogTable) {\n\n    if (catalogTable.isPresent()) {\n      Optional<UCTableInfo> ucTableInfo =\n          UCUtils.extractTableInfo(catalogTable.get(), SparkSession.active());\n      if (ucTableInfo.isPresent()) {\n        return createUCManagedSnapshotManager(ucTableInfo.get(), kernelEngine);\n      }\n      // Catalog table without UC metadata falls back to path-based handling.\n    }\n\n    // Default: path-based snapshot manager for non-UC tables\n    return new PathBasedSnapshotManager(tablePath, kernelEngine);\n  }\n\n  private static UCManagedTableSnapshotManager createUCManagedSnapshotManager(\n      UCTableInfo tableInfo, Engine kernelEngine) {\n    // Start from defaults (Delta, Spark, Scala, Java) and add connector-specific entries\n    Map<String, String> appVersions =\n        UCTokenBasedRestClientFactory$.MODULE$.defaultAppVersionsAsJava();\n    appVersions.put(\"Kernel\", Meta.KERNEL_VERSION);\n    appVersions.put(\"Delta V2 connector\", \"true\");\n    UCClient ucClient =\n        UCTokenBasedRestClientFactory$.MODULE$.createUCClientWithVersions(\n            tableInfo.getUcUri(), tableInfo.getAuthConfig(), appVersions);\n    UCCatalogManagedClient ucCatalogClient = new UCCatalogManagedClient(ucClient);\n    return new UCManagedTableSnapshotManager(ucCatalogClient, tableInfo, kernelEngine);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/snapshot/unitycatalog/UCManagedTableSnapshotManager.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot.unitycatalog;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.CommitRange;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.files.ParsedCatalogCommitData;\nimport io.delta.kernel.unitycatalog.UCCatalogManagedClient;\nimport io.delta.spark.internal.v2.exception.VersionNotFoundException;\nimport io.delta.spark.internal.v2.snapshot.DeltaSnapshotManager;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Snapshot manager for Unity Catalog managed tables.\n *\n * <p>Used for tables with the catalog-managed commit feature enabled. Unity Catalog serves as the\n * source of truth for the table's commit history.\n */\npublic class UCManagedTableSnapshotManager implements DeltaSnapshotManager {\n\n  private final UCCatalogManagedClient ucCatalogManagedClient;\n  private final String tableId;\n  private final String tablePath;\n  private final Engine engine;\n\n  /**\n   * Creates a new UCManagedTableSnapshotManager.\n   *\n   * @param ucCatalogManagedClient the UC client for catalog-managed operations\n   * @param tableInfo the UC table information (tableId, tablePath, etc.)\n   * @param engine the Kernel engine for table operations\n   */\n  public UCManagedTableSnapshotManager(\n      UCCatalogManagedClient ucCatalogManagedClient, UCTableInfo tableInfo, Engine engine) {\n    this.ucCatalogManagedClient =\n        requireNonNull(ucCatalogManagedClient, \"ucCatalogManagedClient is null\");\n    requireNonNull(tableInfo, \"tableInfo is null\");\n    this.tableId = tableInfo.getTableId();\n    this.tablePath = tableInfo.getTablePath();\n    this.engine = requireNonNull(engine, \"engine is null\");\n  }\n\n  /**\n   * Loads and returns the latest snapshot of the UC-managed Delta table.\n   *\n   * @return the latest snapshot of the table\n   */\n  @Override\n  public Snapshot loadLatestSnapshot() {\n    return ucCatalogManagedClient.loadSnapshot(\n        engine,\n        tableId,\n        tablePath,\n        Optional.empty() /* versionOpt */,\n        Optional.empty() /* timestampOpt */);\n  }\n\n  @Override\n  public Snapshot loadSnapshotAt(long version) {\n    return ucCatalogManagedClient.loadSnapshot(\n        engine, tableId, tablePath, Optional.of(version), Optional.empty() /* timestampOpt */);\n  }\n\n  /**\n   * Finds the active commit at a specific timestamp.\n   *\n   * <p>For UC-managed tables, this loads the latest snapshot and uses {@link\n   * DeltaHistoryManager#getActiveCommitAtTimestamp} to resolve the timestamp to a commit.\n   *\n   * @param timestampMillis the timestamp to find the version for in milliseconds since the unix\n   *     epoch\n   * @param canReturnLastCommit whether we can return the latest version of the table if the\n   *     provided timestamp is after the latest commit\n   * @param mustBeRecreatable whether the state at the returned commit should be recreatable\n   * @param canReturnEarliestCommit whether we can return the earliest version of the table if the\n   *     provided timestamp is before the earliest commit\n   * @return the commit that was active at the specified timestamp\n   */\n  @Override\n  public DeltaHistoryManager.Commit getActiveCommitAtTime(\n      long timestampMillis,\n      boolean canReturnLastCommit,\n      boolean mustBeRecreatable,\n      boolean canReturnEarliestCommit) {\n    SnapshotImpl snapshot = (SnapshotImpl) loadLatestSnapshot();\n    List<ParsedCatalogCommitData> catalogCommits = snapshot.getLogSegment().getAllCatalogCommits();\n    return DeltaHistoryManager.getActiveCommitAtTimestamp(\n        engine,\n        snapshot,\n        snapshot.getLogPath(),\n        timestampMillis,\n        mustBeRecreatable,\n        canReturnLastCommit,\n        canReturnEarliestCommit,\n        catalogCommits);\n  }\n\n  /**\n   * Checks if a specific version exists and is accessible.\n   *\n   * <p>For UC-managed tables with catalogManaged, log files may be cleaned up, so we need to use\n   * DeltaHistoryManager to find the earliest available version based on filesystem state.\n   *\n   * @param version the version to check\n   * @param mustBeRecreatable whether the state at this version should be recreatable\n   * @param allowOutOfRange whether versions greater than the latest version are allowed without\n   *     throwing an exception\n   * @throws VersionNotFoundException if the version is not available or does not meet the specified\n   *     criteria\n   */\n  @Override\n  public void checkVersionExists(long version, boolean mustBeRecreatable, boolean allowOutOfRange)\n      throws VersionNotFoundException {\n    // Load latest to get the current version bounds\n    SnapshotImpl snapshot = (SnapshotImpl) loadLatestSnapshot();\n    // Latest version visible in this UC-managed snapshot.\n    long latestSnapshotVersion = snapshot.getVersion();\n\n    // Fast path: check upper bound before expensive filesystem operations\n    if ((version > latestSnapshotVersion) && !allowOutOfRange) {\n      throw new VersionNotFoundException(version, 0 /* earliest */, latestSnapshotVersion);\n    }\n\n    // Get the earliest version among catalog commits. This bounds the Kernel's filesystem search\n    // for the earliest available version (e.g., if catalog has v0, no filesystem search is needed).\n    List<ParsedCatalogCommitData> catalogCommits = snapshot.getLogSegment().getAllCatalogCommits();\n    Optional<Long> earliestCatalogCommitVersion =\n        catalogCommits.stream().map(ParsedCatalogCommitData::getVersion).min(Long::compare);\n\n    long earliestVersion =\n        mustBeRecreatable\n            ? DeltaHistoryManager.getEarliestRecreatableCommit(\n                engine, snapshot.getLogPath(), earliestCatalogCommitVersion)\n            : DeltaHistoryManager.getEarliestDeltaFile(\n                engine, snapshot.getLogPath(), earliestCatalogCommitVersion);\n\n    if (version < earliestVersion) {\n      throw new VersionNotFoundException(version, earliestVersion, latestSnapshotVersion);\n    }\n  }\n\n  /**\n   * Gets a range of table changes (commits) between start and end versions.\n   *\n   * @param engine the engine implementation for executing operations\n   * @param startVersion the starting version (inclusive)\n   * @param endVersion optional ending version (inclusive); if not provided, extends to latest\n   * @return a CommitRange representing the specified range of commits\n   */\n  @Override\n  public CommitRange getTableChanges(Engine engine, long startVersion, Optional<Long> endVersion) {\n    return ucCatalogManagedClient.loadCommitRange(\n        engine,\n        tableId,\n        tablePath,\n        Optional.of(startVersion) /* startVersionOpt */,\n        Optional.empty() /* startTimestampOpt */,\n        endVersion /* endVersionOpt */,\n        Optional.empty() /* endTimestampOpt */);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/snapshot/unitycatalog/UCTableInfo.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot.unitycatalog;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.util.Collections;\nimport java.util.Map;\n\n/**\n * Table information for Unity Catalog managed tables.\n *\n * <p>This POJO encapsulates all the information needed to interact with a Unity Catalog table\n * without requiring Spark dependencies.\n */\npublic final class UCTableInfo {\n\n  private final String tableId;\n  private final String tablePath;\n  private final String ucUri;\n  private final Map<String, String> authConfig;\n\n  public UCTableInfo(\n      String tableId, String tablePath, String ucUri, Map<String, String> authConfig) {\n    this.tableId = requireNonNull(tableId, \"tableId is null\");\n    this.tablePath = requireNonNull(tablePath, \"tablePath is null\");\n    this.ucUri = requireNonNull(ucUri, \"ucUri is null\");\n    this.authConfig = Collections.unmodifiableMap(requireNonNull(authConfig, \"authConfig is null\"));\n  }\n\n  public String getTableId() {\n    return tableId;\n  }\n\n  public String getTablePath() {\n    return tablePath;\n  }\n\n  public String getUcUri() {\n    return ucUri;\n  }\n\n  public Map<String, String> getAuthConfig() {\n    return authConfig;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/snapshot/unitycatalog/UCUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot.unitycatalog;\n\nimport static java.util.Objects.requireNonNull;\nimport static scala.jdk.javaapi.CollectionConverters.asJava;\n\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.delta.coordinatedcommits.UCCatalogConfig;\nimport org.apache.spark.sql.delta.coordinatedcommits.UCCommitCoordinatorBuilder$;\nimport org.apache.spark.sql.delta.util.CatalogTableUtils;\n\n/**\n * Utility class for extracting Unity Catalog table information from Spark catalog metadata.\n *\n * <p>This class isolates Spark dependencies, allowing {@link UCManagedTableSnapshotManager} to be\n * created without Spark if table info is provided directly via {@link UCTableInfo}.\n */\npublic final class UCUtils {\n\n  // Utility class - no instances\n  private UCUtils() {}\n\n  /**\n   * Extracts Unity Catalog table information from Spark catalog table metadata.\n   *\n   * @param catalogTable Spark catalog table metadata\n   * @param spark SparkSession for resolving Unity Catalog configurations\n   * @return table info if table is UC-managed, empty otherwise\n   * @throws IllegalArgumentException if table is UC-managed but configuration is invalid\n   */\n  public static Optional<UCTableInfo> extractTableInfo(\n      CatalogTable catalogTable, SparkSession spark) {\n    requireNonNull(catalogTable, \"catalogTable is null\");\n    requireNonNull(spark, \"spark is null\");\n\n    if (!CatalogTableUtils.isUnityCatalogManagedTable(catalogTable)) {\n      return Optional.empty();\n    }\n\n    String tableId = extractUCTableId(catalogTable);\n    String tablePath = extractTablePath(catalogTable);\n\n    // Get catalog name - require explicit catalog in identifier\n    scala.Option<String> catalogOption = catalogTable.identifier().catalog();\n    if (catalogOption.isEmpty()) {\n      throw new IllegalArgumentException(\n          \"Unable to determine Unity Catalog for table \"\n              + catalogTable.identifier()\n              + \": catalog name is missing. Use a fully-qualified table name with an explicit \"\n              + \"catalog (e.g., catalog.schema.table).\");\n    }\n    String catalogName = catalogOption.get();\n\n    // Get UC endpoint and token from Spark configs\n    scala.collection.immutable.Map<String, UCCatalogConfig> ucConfigs =\n        UCCommitCoordinatorBuilder$.MODULE$.getCatalogConfigMap(spark);\n\n    scala.Option<UCCatalogConfig> configOpt = ucConfigs.get(catalogName);\n\n    if (configOpt.isEmpty()) {\n      throw new IllegalArgumentException(\n          \"Cannot create UC client for table \"\n              + catalogTable.identifier()\n              + \": Unity Catalog configuration not found for catalog '\"\n              + catalogName\n              + \"'.\");\n    }\n\n    UCCatalogConfig config = configOpt.get();\n    String ucUri = config.uri();\n\n    return Optional.of(new UCTableInfo(tableId, tablePath, ucUri, asJava(config.authConfig())));\n  }\n\n  private static String extractUCTableId(CatalogTable catalogTable) {\n    Map<String, String> storageProperties =\n        scala.jdk.javaapi.CollectionConverters.asJava(catalogTable.storage().properties());\n\n    // TODO: UC constants should be consolidated in a shared location (future PR)\n    String ucTableId = storageProperties.get(UCCommitCoordinatorClient.UC_TABLE_ID_KEY);\n    if (ucTableId == null || ucTableId.isEmpty()) {\n      throw new IllegalArgumentException(\n          \"Cannot extract ucTableId from table \" + catalogTable.identifier());\n    }\n    return ucTableId;\n  }\n\n  private static String extractTablePath(CatalogTable catalogTable) {\n    if (catalogTable.location() == null) {\n      throw new IllegalArgumentException(\n          \"Cannot extract table path: location is null for table \" + catalogTable.identifier());\n    }\n    return catalogTable.location().toString();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/CloseableIterator.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2.utils;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.NoSuchElementException;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport scala.collection.AbstractIterator;\nimport scala.collection.Iterator;\n\n/**\n * A Scala iterator that implements {@link Closeable}. Unlike standard Scala iterators, the {@link\n * #filterCloseable} and {@link #mapCloseable} methods return a {@link CloseableIterator} that\n * properly delegates {@link #close()} to the underlying iterator.\n *\n * <p>This is important for iterators that hold resources (e.g., file handles from Parquet readers)\n * that must be released when iteration completes or is interrupted.\n *\n * <p>Inspired by Spark's {@code org.apache.spark.sql.util.CloseableIterator}, which is\n * package-private ({@code private[sql]}) and cannot be used directly.\n *\n * @param <T> the type of elements returned by this iterator\n */\npublic abstract class CloseableIterator<T> extends AbstractIterator<T> implements Closeable {\n\n  /**\n   * Wraps a Scala iterator as a {@link CloseableIterator}. If the iterator already implements\n   * {@link Closeable}, {@link #close()} will delegate to it; otherwise close is a no-op.\n   */\n  public static <T> CloseableIterator<T> wrap(Iterator<T> iterator) {\n    if (iterator instanceof CloseableIterator) {\n      return (CloseableIterator<T>) iterator;\n    }\n    return new WrappedIterator<>(iterator);\n  }\n\n  /**\n   * Returns a new {@link CloseableIterator} that applies the given function to each element.\n   * Closing the returned iterator will close this iterator.\n   */\n  public <U> CloseableIterator<U> mapCloseable(Function<T, U> mapper) {\n    CloseableIterator<T> self = this;\n    return new CloseableIterator<U>() {\n      @Override\n      public boolean hasNext() {\n        return self.hasNext();\n      }\n\n      @Override\n      public U next() {\n        return mapper.apply(self.next());\n      }\n\n      @Override\n      public void close() throws IOException {\n        self.close();\n      }\n    };\n  }\n\n  /**\n   * Returns a new {@link CloseableIterator} that includes only elements matching the predicate.\n   * Closing the returned iterator will close this iterator.\n   */\n  public CloseableIterator<T> filterCloseable(Predicate<T> predicate) {\n    CloseableIterator<T> self = this;\n    return new CloseableIterator<T>() {\n      private T nextElement;\n      private boolean hasNextElement;\n\n      @Override\n      public boolean hasNext() {\n        if (hasNextElement) {\n          return true;\n        }\n        while (self.hasNext()) {\n          T element = self.next();\n          if (predicate.test(element)) {\n            nextElement = element;\n            hasNextElement = true;\n            return true;\n          }\n        }\n        return false;\n      }\n\n      @Override\n      public T next() {\n        if (!hasNext()) {\n          throw new NoSuchElementException();\n        }\n        hasNextElement = false;\n        return nextElement;\n      }\n\n      @Override\n      public void close() throws IOException {\n        self.close();\n      }\n    };\n  }\n\n  /** A wrapper that makes any Scala iterator closeable. */\n  private static class WrappedIterator<T> extends CloseableIterator<T> {\n    private final Iterator<T> delegate;\n\n    WrappedIterator(Iterator<T> delegate) {\n      this.delegate = delegate;\n    }\n\n    @Override\n    public boolean hasNext() {\n      return delegate.hasNext();\n    }\n\n    @Override\n    public T next() {\n      return delegate.next();\n    }\n\n    @Override\n    public void close() throws IOException {\n      if (delegate instanceof Closeable) {\n        ((Closeable) delegate).close();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/ExpressionUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.apache.spark.sql.connector.catalog.CatalogV2Implicits.parseColumnPath;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport io.delta.kernel.expressions.AlwaysFalse;\nimport io.delta.kernel.expressions.And;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.In;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Or;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.internal.util.InternalUtils;\nimport java.math.BigDecimal;\nimport java.sql.Date;\nimport java.sql.Timestamp;\nimport java.util.*;\nimport org.apache.spark.sql.catalyst.expressions.BoundReference;\nimport org.apache.spark.sql.catalyst.expressions.Expression;\nimport org.apache.spark.sql.connector.expressions.LiteralValue;\nimport org.apache.spark.sql.connector.expressions.NamedReference;\nimport org.apache.spark.sql.sources.*;\nimport org.apache.spark.sql.types.DataType;\nimport org.apache.spark.sql.types.DecimalType;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.unsafe.types.UTF8String;\nimport scala.collection.JavaConverters;\n\n/**\n * Utility class for converting Spark SQL filter expressions to Delta Kernel predicates.\n *\n * <p>This class provides methods to convert Spark's {@link Filter} objects into Delta Kernel's\n * {@link Predicate} objects for push-down query optimization.\n *\n * <p>Note: Only expressions that can be safely converted are processed\n */\npublic final class ExpressionUtils {\n\n  /**\n   * Converts a Spark SQL filter to a Delta Kernel predicate.\n   *\n   * <p>Supported filter types:\n   *\n   * <ul>\n   *   <li>Comparison: EqualTo, GreaterThan, LessThan, etc.\n   *   <li>Null tests: IsNull, IsNotNull\n   *   <li>Null-safe comparison: EqualNullSafe\n   *   <li>String prefix: StringStartsWith\n   *   <li>Set membership: In\n   *   <li>Logical operators: And, Or, Not\n   * </ul>\n   *\n   * @param filter the Spark SQL filter to convert\n   * @return ConvertedPredicate containing the converted Kernel predicate, or empty if conversion is\n   *     not supported, along with a boolean indicating whether the conversion was partial\n   */\n  public static ConvertedPredicate convertSparkFilterToKernelPredicate(Filter filter) {\n    return convertSparkFilterToKernelPredicate(filter, true /*canPartialPushDown*/, null);\n  }\n\n  /**\n   * Converts a Spark SQL filter to a Delta Kernel predicate with table schema for type alignment.\n   *\n   * <p>When a table schema is provided, decimal literal types in comparison filters are widened to\n   * match the column's declared decimal type. This prevents type mismatch errors in the Kernel's\n   * expression evaluator during data skipping, where stats column types (from the table schema)\n   * must match the filter literal types exactly.\n   *\n   * @param filter the Spark SQL filter to convert\n   * @param tableSchema the table schema for looking up column types, may be null\n   * @return ConvertedPredicate containing the converted Kernel predicate, or empty if conversion is\n   *     not supported, along with a boolean indicating whether the conversion was partial\n   */\n  public static ConvertedPredicate convertSparkFilterToKernelPredicate(\n      Filter filter, StructType tableSchema) {\n    return convertSparkFilterToKernelPredicate(filter, true /*canPartialPushDown*/, tableSchema);\n  }\n\n  /**\n   * Converts a Spark SQL filter to a Delta Kernel predicate with partial pushdown control. When\n   * canPartialPushDown is true, AND filters can be partially converted if at least one operand can\n   * be converted. OR filters always require both operands to be convertible. NOT filters disable\n   * partial pushdown for their child to preserve semantic correctness.\n   *\n   * <p>Return a ConvertedPredicate object, which contains: - Optional<Predicate>: the converted\n   * Kernel predicate, or empty if conversion is not supported - boolean isPartial: indicates\n   * whether the conversion was partial\n   */\n  @VisibleForTesting\n  static ConvertedPredicate convertSparkFilterToKernelPredicate(\n      Filter filter, boolean canPartialPushDown) {\n    return convertSparkFilterToKernelPredicate(filter, canPartialPushDown, null);\n  }\n\n  /**\n   * Core implementation of filter-to-predicate conversion with partial pushdown control and\n   * optional table schema for decimal type alignment.\n   *\n   * @param filter the Spark SQL filter to convert\n   * @param canPartialPushDown whether partial pushdown is allowed for AND filters\n   * @param tableSchema the table schema for looking up column types, may be null\n   */\n  private static ConvertedPredicate convertSparkFilterToKernelPredicate(\n      Filter filter, boolean canPartialPushDown, StructType tableSchema) {\n    if (filter instanceof EqualTo) {\n      EqualTo f = (EqualTo) filter;\n      return new ConvertedPredicate(\n          convertComparisonLiteral(f.value(), f.attribute(), tableSchema)\n              .map(l -> new Predicate(\"=\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof EqualNullSafe) {\n      EqualNullSafe f = (EqualNullSafe) filter;\n      // EqualNullSafe with null value should be translated to IS_NULL\n      // For non-null values, we use \"=\" operator.\n      return new ConvertedPredicate(\n          f.value() == null\n              ? Optional.of(new Predicate(\"IS_NULL\", kernelColumn(f.attribute())))\n              : convertComparisonLiteral(f.value(), f.attribute(), tableSchema)\n                  .map(l -> new Predicate(\"=\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof GreaterThan) {\n      GreaterThan f = (GreaterThan) filter;\n      return new ConvertedPredicate(\n          convertComparisonLiteral(f.value(), f.attribute(), tableSchema)\n              .map(l -> new Predicate(\">\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof GreaterThanOrEqual) {\n      GreaterThanOrEqual f = (GreaterThanOrEqual) filter;\n      return new ConvertedPredicate(\n          convertComparisonLiteral(f.value(), f.attribute(), tableSchema)\n              .map(l -> new Predicate(\">=\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof LessThan) {\n      LessThan f = (LessThan) filter;\n      return new ConvertedPredicate(\n          convertComparisonLiteral(f.value(), f.attribute(), tableSchema)\n              .map(l -> new Predicate(\"<\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof LessThanOrEqual) {\n      LessThanOrEqual f = (LessThanOrEqual) filter;\n      return new ConvertedPredicate(\n          convertComparisonLiteral(f.value(), f.attribute(), tableSchema)\n              .map(l -> new Predicate(\"<=\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof IsNull) {\n      IsNull f = (IsNull) filter;\n      return new ConvertedPredicate(\n          Optional.of(new Predicate(\"IS_NULL\", kernelColumn(f.attribute()))));\n    }\n    if (filter instanceof IsNotNull) {\n      IsNotNull f = (IsNotNull) filter;\n      return new ConvertedPredicate(\n          Optional.of(new Predicate(\"IS_NOT_NULL\", kernelColumn(f.attribute()))));\n    }\n    if (filter instanceof StringStartsWith) {\n      StringStartsWith f = (StringStartsWith) filter;\n      return new ConvertedPredicate(\n          convertValueToKernelLiteral(f.value())\n              .map(l -> new Predicate(\"STARTS_WITH\", kernelColumn(f.attribute()), l)));\n    }\n    if (filter instanceof org.apache.spark.sql.sources.In) {\n      org.apache.spark.sql.sources.In f = (org.apache.spark.sql.sources.In) filter;\n      // An empty IN list can never match any row. Push ALWAYS_FALSE so the kernel skips\n      // all files entirely, rather than scanning every file only to discard every row.\n      if (f.values().length == 0) {\n        return new ConvertedPredicate(Optional.of(AlwaysFalse.ALWAYS_FALSE));\n      }\n      List<io.delta.kernel.expressions.Expression> literals = new ArrayList<>();\n      for (Object value : f.values()) {\n        Optional<Literal> lit = convertValueToKernelLiteral(value);\n        if (!lit.isPresent()) {\n          // A value that can't be converted (e.g. null, unsupported type) makes the whole\n          // IN expression unsafe to push down; return empty to keep it for post-scan evaluation.\n          return new ConvertedPredicate(Optional.empty());\n        }\n        literals.add(lit.get());\n      }\n      return new ConvertedPredicate(Optional.of(new In(kernelColumn(f.attribute()), literals)));\n    }\n    if (filter instanceof org.apache.spark.sql.sources.And) {\n      org.apache.spark.sql.sources.And f = (org.apache.spark.sql.sources.And) filter;\n      ConvertedPredicate left =\n          convertSparkFilterToKernelPredicate(f.left(), canPartialPushDown, tableSchema);\n      ConvertedPredicate right =\n          convertSparkFilterToKernelPredicate(f.right(), canPartialPushDown, tableSchema);\n      boolean isPartial = left.isPartial() || right.isPartial();\n      if (left.isPresent() && right.isPresent()) {\n        return new ConvertedPredicate(Optional.of(new And(left.get(), right.get())), isPartial);\n      }\n      if (canPartialPushDown && left.isPresent()) {\n        return new ConvertedPredicate(left.getConvertedPredicate(), true);\n      }\n      if (canPartialPushDown && right.isPresent()) {\n        return new ConvertedPredicate(right.getConvertedPredicate(), true);\n      }\n      return new ConvertedPredicate(Optional.empty(), isPartial);\n    }\n    if (filter instanceof org.apache.spark.sql.sources.Or) {\n      org.apache.spark.sql.sources.Or f = (org.apache.spark.sql.sources.Or) filter;\n      ConvertedPredicate left =\n          convertSparkFilterToKernelPredicate(f.left(), canPartialPushDown, tableSchema);\n      ConvertedPredicate right =\n          convertSparkFilterToKernelPredicate(f.right(), canPartialPushDown, tableSchema);\n      // OR requires both operands to be convertible for correctness\n      boolean isPartial = left.isPartial() || right.isPartial();\n      if (!left.isPresent() || !right.isPresent()) {\n        return new ConvertedPredicate(Optional.empty(), isPartial);\n      }\n      return new ConvertedPredicate(Optional.of(new Or(left.get(), right.get())), isPartial);\n    }\n    if (filter instanceof Not) {\n      Not f = (Not) filter;\n      // NOT disables partial pushdown for semantic correctness.\n      // Example: Pushing down NOT(A AND B) requires both A and B to be convertible.\n      // We cannot convert it to just return NOT A if only A is convertible when B is not,\n      // because:\n      //\n      // Original: NOT(age < 30 AND name = \"John\")\n      //\n      // Row 1: age=25, name=\"John\"\n      // Row 2: age=25, name=\"Mike\"\n      // (age < 30 AND name = \"John\") = (true AND true) = true\n      // (age < 30 AND name = \"Mike\") = (true AND false) = false\n      // NOT(true) = false → row 1 should be EXCLUDED\n      // NOT(false) = true → row 2 should be INCLUDED\n\n      // But if we naively push down just NOT(age < 30):\n      //\n      // NOT(age < 30) = NOT(true) = false → system excludes both row\n      // We will return incorrect result, then.\n      ConvertedPredicate child =\n          convertSparkFilterToKernelPredicate(f.child(), false /*canPartialPushDown*/, tableSchema);\n      return new ConvertedPredicate(\n          child.getConvertedPredicate().map(c -> new Predicate(\"NOT\", c)), child.isPartial());\n    }\n\n    return new ConvertedPredicate(Optional.empty());\n  }\n\n  /**\n   * Converts a filter literal value to a Kernel Literal, aligning decimal types with the column's\n   * declared type from the table schema when available.\n   *\n   * <p>When the value is a {@link java.math.BigDecimal} and the table schema is provided, this\n   * method looks up the column's declared {@link org.apache.spark.sql.types.DecimalType} and widens\n   * the literal to match. This prevents type mismatch errors in the Kernel's expression evaluator\n   * during data skipping, where stats column types must match the filter literal types exactly.\n   *\n   * <p>If the literal's scale exceeds the column's scale, or the widened value exceeds the column's\n   * precision, the filter cannot be safely pushed down and an empty Optional is returned.\n   *\n   * @param value the filter literal value\n   * @param attribute the column name referenced by the filter\n   * @param tableSchema the table schema for type lookup, may be null\n   * @return Optional containing the Kernel Literal with aligned type, or empty if conversion fails\n   */\n  private static Optional<Literal> convertComparisonLiteral(\n      Object value, String attribute, StructType tableSchema) {\n    if (value instanceof BigDecimal && tableSchema != null) {\n      BigDecimal bd = (BigDecimal) value;\n      Optional<DataType> columnType = lookupColumnType(attribute, tableSchema);\n      if (columnType.isPresent() && columnType.get() instanceof DecimalType) {\n        DecimalType colDecimalType = (DecimalType) columnType.get();\n        return widenDecimalLiteral(bd, colDecimalType.precision(), colDecimalType.scale());\n      }\n    }\n    return convertValueToKernelLiteral(value);\n  }\n\n  /**\n   * Widens a BigDecimal literal to match the target decimal precision and scale. Returns empty if\n   * the literal cannot be safely represented in the target type (e.g., the literal has more decimal\n   * digits than the target scale, or the widened value exceeds the target precision).\n   */\n  private static Optional<Literal> widenDecimalLiteral(\n      BigDecimal bd, int targetPrecision, int targetScale) {\n    if (bd.scale() <= targetScale) {\n      BigDecimal widened = bd.setScale(targetScale);\n      if (widened.precision() <= targetPrecision) {\n        return Optional.of(Literal.ofDecimal(widened, targetPrecision, targetScale));\n      }\n    }\n    // Literal doesn't fit in column type or has higher scale - skip pushdown\n    return Optional.empty();\n  }\n\n  /**\n   * Looks up the data type of a top-level column in the table schema using case-insensitive name\n   * matching. Returns empty for nested columns or if the column is not found.\n   */\n  private static Optional<DataType> lookupColumnType(String attribute, StructType tableSchema) {\n    for (StructField field : tableSchema.fields()) {\n      if (field.name().equalsIgnoreCase(attribute)) {\n        return Optional.of(field.dataType());\n      }\n    }\n    return Optional.empty();\n  }\n\n  /**\n   * Creates a Delta Kernel Column from a Spark SQL column attribute name.\n   *\n   * <p>This method handles nested column references (e.g., \"user.profile.name\") by parsing the\n   * dot-separated path into an array of field names using Spark's column path parser.\n   *\n   * <p>If a column name contains literal dots that should not be treated as field separators, it\n   * must be properly quoted/escaped in the original Spark SQL. For example:\n   *\n   * <ul>\n   *   <li>{@code `my.column.with.dots`} - treats the entire string as a single column name\n   *   <li>{@code my.nested.field} - treats this as nested field access: my -> nested -> field\n   * </ul>\n   *\n   * @param attribute the column attribute name, potentially dot-separated for nested fields\n   * @return Delta Kernel Column object representing the parsed column path\n   */\n  private static Column kernelColumn(String attribute) {\n    scala.collection.Seq<String> seq = parseColumnPath(attribute);\n    String[] parts = JavaConverters.seqAsJavaList(seq).toArray(new String[0]);\n    return new Column(parts);\n  }\n\n  /**\n   * Converts a Java object to a Delta Kernel Literal with appropriate type inference.\n   *\n   * <p>This method handles the most common Java types and converts them to their corresponding\n   * Delta Kernel Literal representations. The type mapping follows standard SQL data type\n   * conventions.\n   *\n   * <p>Supported types:\n   *\n   * <ul>\n   *   <li>Primitives: Boolean, Byte, Short, Integer, Long, Float, Double\n   *   <li>BigDecimal (with precision and scale preservation)\n   *   <li>String (for string literals from Spark V1 filters)\n   *   <li>byte[] (binary data)\n   *   <li>java.sql.Date (converted to days since epoch)\n   *   <li>java.sql.Timestamp (converted to microseconds since epoch)\n   * </ul>\n   *\n   * <p>Note: null values return empty Optional, which is correct SQL behavior for most operations.\n   * Only EqualNullSafe should handle null values explicitly.\n   *\n   * @param value the Java object to convert\n   * @return Optional containing the Delta Kernel Literal, or empty if the value is null or of an\n   *     unsupported type\n   */\n  @VisibleForTesting\n  static Optional<Literal> convertValueToKernelLiteral(Object value) {\n    // TODO: convert null to NULL literal.\n    if (value == null) return Optional.empty();\n\n    if (value instanceof Boolean) {\n      Boolean b = (Boolean) value;\n      return Optional.of(Literal.ofBoolean(b));\n    }\n    if (value instanceof Byte) {\n      Byte b = (Byte) value;\n      return Optional.of(Literal.ofByte(b));\n    }\n    if (value instanceof Short) {\n      Short s = (Short) value;\n      return Optional.of(Literal.ofShort(s));\n    }\n    if (value instanceof Integer) {\n      Integer i = (Integer) value;\n      return Optional.of(Literal.ofInt(i));\n    }\n    if (value instanceof Long) {\n      Long l = (Long) value;\n      return Optional.of(Literal.ofLong(l));\n    }\n    if (value instanceof Float) {\n      Float f = (Float) value;\n      return Optional.of(Literal.ofFloat(f));\n    }\n    if (value instanceof Double) {\n      Double d = (Double) value;\n      return Optional.of(Literal.ofDouble(d));\n    }\n    if (value instanceof BigDecimal) {\n      // Preserve precision and scale from the original BigDecimal\n      BigDecimal bd = (BigDecimal) value;\n      return Optional.of(Literal.ofDecimal(bd, bd.precision(), bd.scale()));\n    }\n    if (value instanceof UTF8String) {\n      UTF8String s = (UTF8String) value;\n      return Optional.of(Literal.ofString(s.toString()));\n    }\n    if (value instanceof String) {\n      String s = (String) value;\n      return Optional.of(Literal.ofString(s));\n    }\n    if (value instanceof byte[]) {\n      byte[] arr = (byte[]) value;\n      return Optional.of(Literal.ofBinary(arr));\n    }\n    if (value instanceof Date) {\n      // Convert java.sql.Date to days since epoch\n      Date date = (Date) value;\n      return Optional.of(Literal.ofDate(InternalUtils.daysSinceEpoch(date)));\n    }\n    if (value instanceof Timestamp) {\n      // Convert java.sql.Timestamp to microseconds since epoch\n      Timestamp timestamp = (Timestamp) value;\n      return Optional.of(Literal.ofTimestamp(InternalUtils.microsSinceEpoch(timestamp)));\n    }\n\n    // Unsupported type - return empty Optional to skip the conversion.\n    return Optional.empty();\n  }\n\n  /*\n   * Wrapper class to hold the result of converting a Spark Filter to a Kernel Predicate,\n   * including a boolean indicator for whether the conversion was partial.\n   */\n  public static final class ConvertedPredicate {\n    private final Optional<Predicate> convertedPredicate;\n    private final boolean isPartial;\n\n    public ConvertedPredicate(Optional<Predicate> convertedPredicate) {\n      this.convertedPredicate = convertedPredicate;\n      this.isPartial = false;\n    }\n\n    public ConvertedPredicate(Optional<Predicate> convertedPredicate, boolean isPartial) {\n      this.convertedPredicate = convertedPredicate;\n      this.isPartial = isPartial;\n    }\n\n    public Optional<Predicate> getConvertedPredicate() {\n      return convertedPredicate;\n    }\n\n    public boolean isPartial() {\n      return isPartial;\n    }\n\n    public boolean isPresent() {\n      return convertedPredicate.isPresent();\n    }\n\n    public Predicate get() {\n      assert convertedPredicate.isPresent();\n      return convertedPredicate.get();\n    }\n  }\n\n  /*\n   * Helper class to hold the classification result of a Filter\n   */\n  public static class FilterClassificationResult {\n    public final Boolean isKernelSupported;\n    public final Boolean isPartialConversion;\n    public final Boolean isDataFilter;\n    public final Optional<Predicate> kernelPredicate;\n\n    public FilterClassificationResult(\n        Boolean isKernelSupported,\n        Boolean isPartialConversion,\n        Boolean isDataFilter,\n        Optional<Predicate> kernelPredicate) {\n      this.isKernelSupported = isKernelSupported;\n      this.isPartialConversion = isPartialConversion;\n      this.isDataFilter = isDataFilter;\n      this.kernelPredicate = kernelPredicate;\n    }\n  }\n\n  /**\n   * Classifies a Spark Filter based on its convertibility to a Kernel Predicate and whether it is a\n   * data filter (i.e., references non-partition columns).\n   *\n   * @param filter the Spark Filter to classify\n   * @param partitionColumnSet a set of partition column names (in lower case) for identifying data\n   *     filters\n   * @return FilterClassificationResult containing:\n   *     <ul>\n   *       <li>isKernelSupported: true if the filter can be converted to a Kernel Predicate\n   *       <li>isPartialConversion: true if the conversion was partial (for AND filters)\n   *       <li>isDataFilter: true if the filter references at least one non-partition column\n   *       <li>kernelPredicate: Optional containing the converted Kernel Predicate, if any\n   *     </ul>\n   */\n  public static FilterClassificationResult classifyFilter(\n      Filter filter, Set<String> partitionColumnSet) {\n    return classifyFilter(filter, partitionColumnSet, null);\n  }\n\n  /**\n   * Classifies a Spark Filter with table schema for decimal type alignment.\n   *\n   * @param filter the Spark Filter to classify\n   * @param partitionColumnSet a set of partition column names (in lower case) for identifying data\n   *     filters\n   * @param tableSchema the table schema for aligning decimal literal types, may be null\n   * @return FilterClassificationResult containing classification details\n   */\n  public static FilterClassificationResult classifyFilter(\n      Filter filter, Set<String> partitionColumnSet, StructType tableSchema) {\n    // try to convert Spark filter to Kernel Predicate\n    ConvertedPredicate convertedPredicate =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, tableSchema);\n\n    boolean isKernelSupported = convertedPredicate.isPresent();\n    boolean isPartialConversion = convertedPredicate.isPartial();\n    Optional<Predicate> kernelPredicate = convertedPredicate.getConvertedPredicate();\n\n    // check if the filter is a data filter\n    // A data filter is a filter that references at least one non-partition column.\n    String[] refs = filter.references();\n    boolean isDataFilter =\n        refs != null\n            && refs.length > 0\n            && Arrays.stream(refs)\n                .anyMatch((col -> !partitionColumnSet.contains(col.toLowerCase(Locale.ROOT))));\n\n    return new FilterClassificationResult(\n        isKernelSupported, isPartialConversion, isDataFilter, kernelPredicate);\n  }\n\n  /**\n   * Converts a Spark DataSourceV2 Predicate to a Catalyst Expression.\n   *\n   * <p>This method translates supported DSV2 predicates into their equivalent Catalyst expressions,\n   * using the provided schema for column resolution. Unsupported predicates, or those referencing\n   * unknown columns, will result in an empty Optional.\n   *\n   * <p>Supported predicates include:\n   *\n   * <ul>\n   *   <li>Null tests: IS_NULL, IS_NOT_NULL\n   *   <li>String functions: STARTS_WITH, ENDS_WITH, CONTAINS\n   *   <li>IN operator\n   *   <li>Comparison: =, >, >=, <, <=\n   *   <li>Null-safe comparison: <=>\n   *   <li>Logical operators: AND, OR, NOT\n   *   <li>Constant predicates: ALWAYS_TRUE, ALWAYS_FALSE\n   * </ul>\n   *\n   * @param predicate the DSV2 Predicate to convert\n   * @param schema the schema used for resolving column references\n   * @return Catalyst Expression representing the converted predicate, or empty if the predicate is\n   *     unsupported or references unknown columns\n   */\n  public static Optional<Expression> dsv2PredicateToCatalystExpression(\n      org.apache.spark.sql.connector.expressions.filter.Predicate predicate, StructType schema) {\n    String predicateName = predicate.name();\n    org.apache.spark.sql.connector.expressions.Expression[] children = predicate.children();\n\n    switch (predicateName) {\n      case \"IS_NULL\":\n        if (children.length == 1) {\n          Optional<Expression> expressionOpt =\n              dsv2ExpressionToCatalystExpression(children[0], schema);\n          if (expressionOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.IsNull(expressionOpt.get()));\n          }\n        }\n        break;\n\n      case \"IS_NOT_NULL\":\n        if (children.length == 1) {\n          Optional<Expression> expressionOpt =\n              dsv2ExpressionToCatalystExpression(children[0], schema);\n          if (expressionOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.IsNotNull(expressionOpt.get()));\n          }\n        }\n        break;\n\n      case \"STARTS_WITH\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.StartsWith(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"ENDS_WITH\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.EndsWith(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"CONTAINS\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.Contains(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"IN\":\n        if (children.length >= 2) {\n          Optional<Expression> firstOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          if (firstOpt.isPresent()) {\n            List<Expression> values = new ArrayList<>();\n            for (int i = 1; i < children.length; i++) {\n              Optional<Expression> valueOpt =\n                  dsv2ExpressionToCatalystExpression(children[i], schema);\n              if (valueOpt.isPresent()) {\n                values.add(valueOpt.get());\n              } else {\n                // if any value in the IN list cannot be converted, return empty\n                return Optional.empty();\n              }\n            }\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.In(\n                    firstOpt.get(), JavaConverters.asScalaBuffer(values).toSeq()));\n          }\n        }\n        break;\n\n      case \"=\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.EqualTo(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"<>\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.Not(\n                    new org.apache.spark.sql.catalyst.expressions.EqualTo(\n                        leftOpt.get(), rightOpt.get())));\n          }\n        }\n        break;\n\n      case \"<=>\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.EqualNullSafe(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"<\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.LessThan(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"<=\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.LessThanOrEqual(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \">\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.GreaterThan(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \">=\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt = dsv2ExpressionToCatalystExpression(children[0], schema);\n          Optional<Expression> rightOpt = dsv2ExpressionToCatalystExpression(children[1], schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.GreaterThanOrEqual(\n                    leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"AND\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt =\n              dsv2PredicateToCatalystExpression(\n                  (org.apache.spark.sql.connector.expressions.filter.Predicate)\n                      predicate.children()[0],\n                  schema);\n          Optional<Expression> rightOpt =\n              dsv2PredicateToCatalystExpression(\n                  (org.apache.spark.sql.connector.expressions.filter.Predicate)\n                      predicate.children()[1],\n                  schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.And(leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"OR\":\n        if (children.length == 2) {\n          Optional<Expression> leftOpt =\n              dsv2PredicateToCatalystExpression(\n                  (org.apache.spark.sql.connector.expressions.filter.Predicate)\n                      predicate.children()[0],\n                  schema);\n          Optional<Expression> rightOpt =\n              dsv2PredicateToCatalystExpression(\n                  (org.apache.spark.sql.connector.expressions.filter.Predicate)\n                      predicate.children()[1],\n                  schema);\n          if (leftOpt.isPresent() && rightOpt.isPresent()) {\n            return Optional.of(\n                new org.apache.spark.sql.catalyst.expressions.Or(leftOpt.get(), rightOpt.get()));\n          }\n        }\n        break;\n\n      case \"NOT\":\n        if (children.length == 1) {\n          Optional<Expression> childOpt =\n              dsv2PredicateToCatalystExpression(\n                  (org.apache.spark.sql.connector.expressions.filter.Predicate)\n                      predicate.children()[0],\n                  schema);\n          if (childOpt.isPresent()) {\n            return Optional.of(new org.apache.spark.sql.catalyst.expressions.Not(childOpt.get()));\n          }\n        }\n        break;\n\n      case \"ALWAYS_TRUE\":\n        if (children.length == 0) {\n          return Optional.of(\n              org.apache.spark.sql.catalyst.expressions.Literal.create(\n                  true, org.apache.spark.sql.types.DataTypes.BooleanType));\n        }\n        break;\n\n      case \"ALWAYS_FALSE\":\n        if (children.length == 0) {\n          return Optional.of(\n              org.apache.spark.sql.catalyst.expressions.Literal.create(\n                  false, org.apache.spark.sql.types.DataTypes.BooleanType));\n        }\n        break;\n    }\n\n    return Optional.empty();\n  }\n\n  /**\n   * Resolves a DSV2 Expression to a Catalyst Expression using the provided schema.\n   *\n   * <p>This method handles NamedReference and LiteralValue expressions. NamedReferences are\n   * resolved to BoundReferences based on the schema, while LiteralValues are converted to Catalyst\n   * Literals. Unsupported expression types or references to unknown columns will result in an empty\n   * Optional.\n   *\n   * @param expr the DSV2 Expression to resolve\n   * @param schema the schema used for resolving column references\n   * @return Catalyst Expression representing the resolved expression, or empty if the expression is\n   *     unsupported or references unknown columns\n   */\n  private static Optional<Expression> dsv2ExpressionToCatalystExpression(\n      org.apache.spark.sql.connector.expressions.Expression expr, StructType schema) {\n    if (expr instanceof NamedReference) {\n      NamedReference ref = (NamedReference) expr;\n      String columnName = ref.fieldNames()[0];\n      try {\n        int index = schema.fieldIndex(columnName);\n        StructField field = schema.fields()[index];\n        return Optional.of(new BoundReference(index, field.dataType(), field.nullable()));\n      } catch (IllegalArgumentException e) {\n        // schema.fieldIndex(columnName) throws IllegalArgumentException if a field with the given\n        // name does not exist\n        return Optional.empty();\n      }\n    } else if (expr instanceof LiteralValue) {\n      LiteralValue<?> literal = (LiteralValue<?>) expr;\n      return Optional.of(\n          org.apache.spark.sql.catalyst.expressions.Literal.create(\n              literal.value(), literal.dataType()));\n    } else {\n      return Optional.empty();\n    }\n  }\n\n  private ExpressionUtils() {}\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/PartitionUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.internal.SnapshotImpl;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.DeletionVectorDescriptor;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.tablefeatures.TableFeatures;\nimport io.delta.spark.internal.v2.read.DeltaParquetFileFormatV2;\nimport io.delta.spark.internal.v2.read.SparkReaderFactory;\nimport io.delta.spark.internal.v2.read.deletionvector.DeletionVectorReadFunction;\nimport io.delta.spark.internal.v2.read.deletionvector.DeletionVectorSchemaContext;\nimport java.time.ZoneId;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.paths.SparkPath;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.connector.read.PartitionReaderFactory;\nimport org.apache.spark.sql.delta.DeltaColumnMapping;\nimport org.apache.spark.sql.delta.DeltaParquetFileFormat;\nimport org.apache.spark.sql.delta.RowIndexFilterType;\nimport org.apache.spark.sql.execution.datasources.FileFormat$;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.execution.datasources.PartitioningUtils;\nimport org.apache.spark.sql.execution.datasources.parquet.ParquetUtils;\nimport org.apache.spark.sql.internal.SQLConf;\nimport org.apache.spark.sql.sources.Filter;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport scala.Function1;\nimport scala.Option;\nimport scala.Tuple2;\nimport scala.collection.Iterator;\nimport scala.jdk.javaapi.CollectionConverters;\n\n/** Utility class for partition-related operations shared across Delta Kernel Spark components. */\npublic class PartitionUtils {\n\n  private PartitionUtils() {}\n\n  /**\n   * Returns whether the given snapshot's table supports deletion vectors. A table supports DVs when\n   * its protocol includes the {@link TableFeatures#DELETION_VECTORS_RW_FEATURE} and the table\n   * format is Parquet.\n   */\n  public static boolean tableSupportsDeletionVectors(Snapshot snapshot) {\n    SnapshotImpl snapshotImpl = (SnapshotImpl) snapshot;\n    Protocol protocol = snapshotImpl.getProtocol();\n    Metadata metadata = snapshotImpl.getMetadata();\n    return protocol.supportsFeature(TableFeatures.DELETION_VECTORS_RW_FEATURE)\n        && \"parquet\".equalsIgnoreCase(metadata.getFormat().getProvider());\n  }\n\n  /**\n   * Calculate the maximum split bytes for file partitioning, considering total bytes and file\n   * count. This is used for optimal file splitting in both batch and streaming read.\n   */\n  public static long calculateMaxSplitBytes(\n      SparkSession sparkSession, long totalBytes, int fileCount, SQLConf sqlConf) {\n    long defaultMaxSplitBytes = sqlConf.filesMaxPartitionBytes();\n    long openCostInBytes = sqlConf.filesOpenCostInBytes();\n    Option<Object> minPartitionNumOption = sqlConf.filesMinPartitionNum();\n\n    int minPartitionNum =\n        minPartitionNumOption.isDefined()\n            ? ((Number) minPartitionNumOption.get()).intValue()\n            : sqlConf\n                .getConf(SQLConf.LEAF_NODE_DEFAULT_PARALLELISM())\n                .getOrElse(() -> sparkSession.sparkContext().defaultParallelism());\n    if (minPartitionNum <= 0) {\n      minPartitionNum = 1;\n    }\n\n    long calculatedTotalBytes = totalBytes + (long) fileCount * openCostInBytes;\n    long bytesPerCore = calculatedTotalBytes / minPartitionNum;\n\n    return Math.min(defaultMaxSplitBytes, Math.max(openCostInBytes, bytesPerCore));\n  }\n\n  /**\n   * Build the partition {@link InternalRow} from kernel partition values by casting them to the\n   * desired Spark types using the session time zone for temporal types.\n   *\n   * <p>Note: Partition values in AddFile use physical column names as keys when column mapping is\n   * enabled. This method uses DeltaColumnMapping.getPhysicalName to map from logical schema fields\n   * to physical partition value keys.\n   *\n   * @implNote The returned {@link InternalRow} is a {@code GenericInternalRow} (via {@code\n   *     InternalRow.fromSeq}), which has value-based {@code equals()}/{@code hashCode()}. Callers\n   *     such as {@code SparkBatch.planPartitionedInputPartitions} rely on this for grouping files\n   *     by partition key. Changing the return type to a different InternalRow subtype (e.g. {@code\n   *     UnsafeRow}) may break that contract.\n   */\n  public static InternalRow getPartitionRow(\n      MapValue partitionValues, StructType partitionSchema, ZoneId zoneId) {\n    final int numPartCols = partitionSchema.fields().length;\n    assert partitionValues.getSize() == numPartCols\n        : String.format(\n            java.util.Locale.ROOT,\n            \"Partition values size from add file %d != partition columns size %d\",\n            partitionValues.getSize(),\n            numPartCols);\n\n    final Object[] values = new Object[numPartCols];\n\n    // Build physical name -> index map once\n    // Partition values use physical names as keys when column mapping is enabled\n    final Map<String, Integer> physicalNameToIndex = new HashMap<>(numPartCols);\n    for (int i = 0; i < numPartCols; i++) {\n      StructField field = partitionSchema.fields()[i];\n      String physicalName = DeltaColumnMapping.getPhysicalName(field);\n      physicalNameToIndex.put(physicalName, i);\n      values[i] = null;\n    }\n\n    // Fill values in a single pass over partitionValues\n    for (int idx = 0; idx < partitionValues.getSize(); idx++) {\n      final String key = partitionValues.getKeys().getString(idx);\n      final String strVal = partitionValues.getValues().getString(idx);\n      final Integer pos = physicalNameToIndex.get(key);\n      if (pos != null) {\n        final StructField field = partitionSchema.fields()[pos];\n        values[pos] =\n            (strVal == null)\n                ? null\n                : PartitioningUtils.castPartValueToDesiredType(field.dataType(), strVal, zoneId);\n      }\n    }\n    return InternalRow.fromSeq(\n        CollectionConverters.asScala(Arrays.asList(values).iterator()).toSeq());\n  }\n\n  /**\n   * Build a PartitionedFile from an AddFile with the given partition schema and table path.\n   *\n   * @param addFile The AddFile to convert\n   * @param partitionSchema The partition schema for parsing partition values\n   * @param tablePath The table path\n   * @param zoneId The timezone for temporal partition values\n   * @return A PartitionedFile ready for Spark execution\n   */\n  public static PartitionedFile buildPartitionedFile(\n      AddFile addFile, StructType partitionSchema, String tablePath, ZoneId zoneId) {\n    InternalRow partitionRow =\n        getPartitionRow(addFile.getPartitionValues(), partitionSchema, zoneId);\n\n    // Preferred node locations are not used.\n    String[] preferredLocations = new String[0];\n\n    // Build metadata map with DV info if present\n    scala.collection.immutable.Map<String, Object> otherConstantMetadataColumnValues =\n        buildDvMetadata(addFile.getDeletionVector());\n\n    return new PartitionedFile(\n        partitionRow,\n        SparkPath.fromUrlString(new Path(tablePath, addFile.getPath()).toString()),\n        /* start= */ 0L,\n        /* length= */ addFile.getSize(),\n        preferredLocations,\n        addFile.getModificationTime(),\n        /* fileSize= */ addFile.getSize(),\n        otherConstantMetadataColumnValues);\n  }\n\n  /**\n   * Create a PartitionReaderFactory for reading Parquet files with Delta-specific features.\n   *\n   * <p>Uses DeltaParquetFileFormatV2 which supports column mapping, deletion vectors, and other\n   * Delta features through the ProtocolMetadataAdapterV2.\n   *\n   * <p>For tables with deletion vectors enabled, this method:\n   *\n   * <ol>\n   *   <li>Adds __delta_internal_is_row_deleted column to read schema\n   *   <li>Creates a reader that generates the is_row_deleted column using DV bitmap\n   *   <li>Wraps the reader to filter out deleted rows and remove internal columns\n   * </ol>\n   *\n   * @param snapshot The Delta table snapshot containing protocol, metadata, and table path\n   */\n  public static PartitionReaderFactory createDeltaParquetReaderFactory(\n      Snapshot snapshot,\n      StructType dataSchema,\n      StructType partitionSchema,\n      StructType readDataSchema,\n      Filter[] dataFilters,\n      scala.collection.immutable.Map<String, String> scalaOptions,\n      Configuration hadoopConf,\n      SQLConf sqlConf) {\n    SnapshotImpl snapshotImpl = (SnapshotImpl) snapshot;\n    Protocol protocol = snapshotImpl.getProtocol();\n    Metadata metadata = snapshotImpl.getMetadata();\n    // Use Path.toString() instead of toUri().toString() to avoid URL encoding issues.\n    // toUri().toString() encodes special characters (e.g., space -> %20), which causes\n    // DV file path resolution failures.\n    String tablePath = snapshotImpl.getDataPath().toString();\n\n    // Create DV schema context if table supports deletion vectors\n    Optional<DeletionVectorSchemaContext> dvSchemaContext =\n        tableSupportsDeletionVectors(snapshot)\n            ? Optional.of(new DeletionVectorSchemaContext(readDataSchema, partitionSchema))\n            : Optional.empty();\n    StructType finalReadDataSchema =\n        dvSchemaContext\n            .map(DeletionVectorSchemaContext::getSchemaWithDvColumn)\n            .orElse(readDataSchema);\n\n    boolean enableVectorizedReader =\n        ParquetUtils.isBatchReadSupportedForSchema(sqlConf, finalReadDataSchema);\n    scala.collection.immutable.Map<String, String> optionsWithVectorizedReading =\n        scalaOptions.$plus(\n            new Tuple2<>(\n                FileFormat$.MODULE$.OPTION_RETURNING_BATCH(),\n                String.valueOf(enableVectorizedReader)));\n\n    // TODO(https://github.com/delta-io/delta/issues/5859): Enable file splitting for DV tables\n    boolean optimizationsEnabled = !dvSchemaContext.isPresent();\n\n    // TODO(https://github.com/delta-io/delta/issues/5859): Support _metadata.row_index for DV\n    Option<Boolean> useMetadataRowIndex =\n        dvSchemaContext.isPresent() ? Option.apply(Boolean.FALSE) : Option.empty();\n    DeltaParquetFileFormatV2 deltaFormat =\n        new DeltaParquetFileFormatV2(\n            protocol,\n            metadata,\n            /* nullableRowTrackingConstantFields */ false,\n            /* nullableRowTrackingGeneratedFields */ false,\n            optimizationsEnabled,\n            Option.apply(tablePath),\n            /* isCDCRead */ false,\n            /* useMetadataRowIndexOpt */ useMetadataRowIndex);\n\n    Function1<PartitionedFile, Iterator<InternalRow>> readFunc =\n        deltaFormat.buildReaderWithPartitionValues(\n            SparkSession.active(),\n            dataSchema,\n            partitionSchema,\n            finalReadDataSchema,\n            CollectionConverters.asScala(Arrays.asList(dataFilters)).toSeq(),\n            optionsWithVectorizedReading,\n            hadoopConf);\n\n    // Wrap reader to filter deleted rows and remove internal columns if DV is enabled.\n    if (dvSchemaContext.isPresent()) {\n      readFunc =\n          DeletionVectorReadFunction.wrap(readFunc, dvSchemaContext.get(), enableVectorizedReader);\n    }\n\n    return new SparkReaderFactory(readFunc, enableVectorizedReader);\n  }\n\n  /**\n   * Build metadata map for PartitionedFile containing DV descriptor if present.\n   *\n   * <p>The metadata is used by DeltaParquetFileFormat to generate the is_row_deleted column.\n   */\n  private static scala.collection.immutable.Map<String, Object> buildDvMetadata(\n      Optional<DeletionVectorDescriptor> dvOpt) {\n    Map<String, Object> metadata = new HashMap<>();\n    if (dvOpt.isPresent()) {\n      metadata.put(\n          DeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_ID_ENCODED(),\n          dvOpt.get().serializeToBase64());\n      metadata.put(\n          DeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_TYPE(), RowIndexFilterType.IF_CONTAINED);\n    }\n    return scala.collection.immutable.Map$.MODULE$.from(CollectionConverters.asScala(metadata));\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/RowTrackingUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.rowtracking.MaterializedRowTrackingColumn;\nimport io.delta.kernel.internal.rowtracking.RowTracking;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.apache.spark.sql.catalyst.expressions.FileSourceConstantMetadataStructField;\nimport org.apache.spark.sql.catalyst.expressions.FileSourceGeneratedMetadataStructField;\nimport org.apache.spark.sql.delta.DeltaIllegalStateException;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.MetadataBuilder;\nimport org.apache.spark.sql.types.StructField;\n\n/**\n * Utility methods for row tracking in Kernel based connector. This class provides row tracking\n * functionality with Spark-specific metadata attributes for marking metadata columns as constant or\n * generated fields.\n */\npublic class RowTrackingUtils {\n\n  // Field names for row tracking metadata columns (matching Spark V1 definitions)\n  private static final String BASE_ROW_ID = \"base_row_id\";\n  private static final String ROW_ID = \"row_id\";\n  private static final String DEFAULT_ROW_COMMIT_VERSION = \"default_row_commit_version\";\n  private static final String ROW_COMMIT_VERSION = \"row_commit_version\";\n\n  // Metadata keys for row tracking metadata fields\n  private static final String BASE_ROW_ID_METADATA_COL_ATTR_KEY = \"__base_row_id_metadata_col\";\n  private static final String DEFAULT_ROW_COMMIT_VERSION_METADATA_COL_ATTR_KEY =\n      \"__default_row_version_metadata_col\";\n  private static final String ROW_ID_METADATA_COL_ATTR_KEY = \"__row_id_metadata_col\";\n  private static final String ROW_COMMIT_VERSION_METADATA_COL_ATTR_KEY =\n      \"__row_commit_version_metadata_col\";\n\n  private RowTrackingUtils() {}\n\n  /**\n   * Create the row tracking metadata struct fields for reading.\n   *\n   * <p>The order and presence of fields matches Spark V1 implementation:\n   *\n   * <ul>\n   *   <li>row_id (generated field, always present when row tracking is enabled)\n   *   <li>base_row_id (constant field, always present when row tracking is enabled)\n   *   <li>default_row_commit_version (constant field, always present when row tracking is enabled)\n   *   <li>row_commit_version (generated field, always present when row tracking is enabled)\n   * </ul>\n   *\n   * @param protocol the protocol\n   * @param metadata the metadata\n   * @param nullableConstantFields whether constant fields should be nullable\n   * @param nullableGeneratedFields whether generated fields should be nullable\n   * @return list of struct fields for row tracking metadata, or empty list if row tracking is not\n   *     enabled\n   * @throws DeltaIllegalStateException if row tracking is enabled but materialized column names are\n   *     missing\n   */\n  public static List<StructField> createMetadataStructFields(\n      Protocol protocol,\n      Metadata metadata,\n      boolean nullableConstantFields,\n      boolean nullableGeneratedFields) {\n    if (!RowTracking.isEnabled(protocol, metadata)) {\n      return new ArrayList<>();\n    }\n\n    List<StructField> fields = new ArrayList<>();\n\n    // Add row_id (generated field) - will throw if materialized column name is not configured\n    String rowIdPhysicalName =\n        getPhysicalColumnNameOrThrow(MaterializedRowTrackingColumn.MATERIALIZED_ROW_ID, metadata);\n    fields.add(\n        new StructField(\n            ROW_ID,\n            DataTypes.LongType,\n            nullableGeneratedFields,\n            createGeneratedFieldMetadata(ROW_ID, rowIdPhysicalName, ROW_ID_METADATA_COL_ATTR_KEY)));\n\n    // Add base_row_id (constant field)\n    fields.add(\n        new StructField(\n            BASE_ROW_ID,\n            DataTypes.LongType,\n            nullableConstantFields,\n            createConstantFieldMetadata(BASE_ROW_ID, BASE_ROW_ID_METADATA_COL_ATTR_KEY)));\n\n    // Add default_row_commit_version (constant field)\n    fields.add(\n        new StructField(\n            DEFAULT_ROW_COMMIT_VERSION,\n            DataTypes.LongType,\n            nullableConstantFields,\n            createConstantFieldMetadata(\n                DEFAULT_ROW_COMMIT_VERSION, DEFAULT_ROW_COMMIT_VERSION_METADATA_COL_ATTR_KEY)));\n\n    // Add row_commit_version (generated field) - will throw if materialized column name is not\n    // configured\n    String rowCommitVersionPhysicalName =\n        getPhysicalColumnNameOrThrow(\n            MaterializedRowTrackingColumn.MATERIALIZED_ROW_COMMIT_VERSION, metadata);\n    fields.add(\n        new StructField(\n            ROW_COMMIT_VERSION,\n            DataTypes.LongType,\n            nullableGeneratedFields,\n            createGeneratedFieldMetadata(\n                ROW_COMMIT_VERSION,\n                rowCommitVersionPhysicalName,\n                ROW_COMMIT_VERSION_METADATA_COL_ATTR_KEY)));\n\n    return fields;\n  }\n\n  /**\n   * Helper method to get physical column name from MaterializedRowTrackingColumn, converting kernel\n   * IllegalArgumentException to Spark DeltaIllegalStateException. This matches the exception thrown\n   * by Spark V1's MaterializedRowTrackingColumn.\n   *\n   * @param column the MaterializedRowTrackingColumn instance\n   * @param metadata the table metadata\n   * @return the physical column name\n   * @throws DeltaIllegalStateException if the materialized column name is missing\n   */\n  private static String getPhysicalColumnNameOrThrow(\n      MaterializedRowTrackingColumn column, Metadata metadata) {\n    try {\n      return column.getPhysicalColumnName(metadata.getConfiguration());\n    } catch (IllegalArgumentException e) {\n      // Convert kernel exception to Spark V1-compatible DeltaIllegalStateException\n      // Use the same error class as Spark V1: DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\n      String rowTrackingColumnType =\n          column == MaterializedRowTrackingColumn.MATERIALIZED_ROW_ID\n              ? \"Row ID\"\n              : \"Row Commit Version\";\n      throw new DeltaIllegalStateException(\n          \"DELTA_MATERIALIZED_ROW_TRACKING_COLUMN_NAME_MISSING\",\n          new String[] {rowTrackingColumnType, metadata.getId()},\n          e);\n    }\n  }\n\n  private static org.apache.spark.sql.types.Metadata createConstantFieldMetadata(\n      String columnName, String attrKey) {\n    return new MetadataBuilder()\n        .withMetadata(FileSourceConstantMetadataStructField.metadata(columnName))\n        .putBoolean(attrKey, true)\n        .build();\n  }\n\n  private static org.apache.spark.sql.types.Metadata createGeneratedFieldMetadata(\n      String readColumnName, String writeColumnName, String attrKey) {\n    return new MetadataBuilder()\n        .withMetadata(\n            FileSourceGeneratedMetadataStructField.metadata(readColumnName, writeColumnName))\n        .putBoolean(attrKey, true)\n        .build();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/ScalaUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Optional;\nimport scala.Option;\nimport scala.Tuple2;\nimport scala.collection.immutable.Map$;\nimport scala.collection.mutable.Builder;\nimport scala.jdk.javaapi.CollectionConverters;\n\npublic final class ScalaUtils {\n  public static scala.collection.immutable.Map<String, String> toScalaMap(\n      Map<String, String> javaMap) {\n    if (javaMap == null) throw new NullPointerException(\"options\");\n\n    // Works on Scala 2.12 and 2.13\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    Builder<Tuple2<String, String>, scala.collection.immutable.Map<String, String>> b =\n        (Builder) Map$.MODULE$.newBuilder();\n\n    for (Map.Entry<String, String> e : javaMap.entrySet()) {\n      b.$plus$eq(new Tuple2<>(e.getKey(), e.getValue()));\n    }\n    return b.result();\n  }\n\n  public static Map<String, String> toJavaMap(\n      scala.collection.immutable.Map<String, String> scalaMap) {\n    if (scalaMap == null) {\n      return null;\n    }\n    if (scalaMap.isEmpty()) {\n      return Collections.emptyMap();\n    }\n    return CollectionConverters.asJava(scalaMap);\n  }\n\n  /**\n   * Converts a Java {@link Optional} to a Scala {@link Option}.\n   *\n   * @param optional the Java Optional to convert\n   * @param <T> the type of the value\n   * @return the corresponding Scala Option\n   */\n  public static <T> Option<T> toScalaOption(Optional<T> optional) {\n    return optional.map(Option::apply).orElse(Option.empty());\n  }\n\n  /**\n   * Converts a Scala {@link Option} to a Java {@link Optional}.\n   *\n   * @param option the Scala Option to convert\n   * @param <T> the type of the value\n   * @return the corresponding Java Optional\n   */\n  public static <T> Optional<T> toJavaOptional(Option<T> option) {\n    return option.isDefined() ? Optional.of(option.get()) : Optional.empty();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/SchemaUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static java.util.Objects.requireNonNull;\n\nimport io.delta.kernel.types.ArrayType;\nimport io.delta.kernel.types.BinaryType;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.ByteType;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.DateType;\nimport io.delta.kernel.types.DecimalType;\nimport io.delta.kernel.types.DoubleType;\nimport io.delta.kernel.types.FloatType;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.MapType;\nimport io.delta.kernel.types.ShortType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructField;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.types.TimestampNTZType;\nimport io.delta.kernel.types.TimestampType;\nimport io.delta.kernel.types.VariantType;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.Metadata;\nimport org.apache.spark.sql.types.MetadataBuilder;\nimport scala.jdk.CollectionConverters;\n\n/** A utility class for converting between Delta Kernel and Spark schemas and data types. */\npublic class SchemaUtils {\n\n  //////////////////////\n  // Kernel --> Spark //\n  //////////////////////\n\n  /** Converts a Delta Kernel schema to a Spark schema. */\n  public static org.apache.spark.sql.types.StructType convertKernelSchemaToSparkSchema(\n      StructType kernelSchema) {\n    requireNonNull(kernelSchema);\n    List<org.apache.spark.sql.types.StructField> fields = new ArrayList<>();\n\n    for (StructField field : kernelSchema.fields()) {\n      fields.add(\n          new org.apache.spark.sql.types.StructField(\n              field.getName(),\n              convertKernelDataTypeToSparkDataType(field.getDataType()),\n              field.isNullable(),\n              convertKernelFieldMetadataToSparkMetadata(field.getMetadata())));\n    }\n\n    return new org.apache.spark.sql.types.StructType(\n        fields.toArray(new org.apache.spark.sql.types.StructField[0]));\n  }\n\n  /** Converts a Delta Kernel data type to a Spark data type. */\n  public static org.apache.spark.sql.types.DataType convertKernelDataTypeToSparkDataType(\n      DataType kernelDataType) {\n    requireNonNull(kernelDataType);\n    if (kernelDataType instanceof StringType) {\n      return DataTypes.StringType;\n    } else if (kernelDataType instanceof BooleanType) {\n      return DataTypes.BooleanType;\n    } else if (kernelDataType instanceof IntegerType) {\n      return DataTypes.IntegerType;\n    } else if (kernelDataType instanceof LongType) {\n      return DataTypes.LongType;\n    } else if (kernelDataType instanceof BinaryType) {\n      return DataTypes.BinaryType;\n    } else if (kernelDataType instanceof ByteType) {\n      return DataTypes.ByteType;\n    } else if (kernelDataType instanceof DateType) {\n      return DataTypes.DateType;\n    } else if (kernelDataType instanceof DecimalType) {\n      DecimalType kernelDecimal = (DecimalType) kernelDataType;\n      return DataTypes.createDecimalType(kernelDecimal.getPrecision(), kernelDecimal.getScale());\n    } else if (kernelDataType instanceof DoubleType) {\n      return DataTypes.DoubleType;\n    } else if (kernelDataType instanceof FloatType) {\n      return DataTypes.FloatType;\n    } else if (kernelDataType instanceof ShortType) {\n      return DataTypes.ShortType;\n    } else if (kernelDataType instanceof TimestampType) {\n      return DataTypes.TimestampType;\n    } else if (kernelDataType instanceof TimestampNTZType) {\n      return DataTypes.TimestampNTZType;\n    } else if (kernelDataType instanceof ArrayType) {\n      ArrayType kernelArray = (ArrayType) kernelDataType;\n      return DataTypes.createArrayType(\n          convertKernelDataTypeToSparkDataType(kernelArray.getElementType()),\n          kernelArray.containsNull());\n    } else if (kernelDataType instanceof MapType) {\n      MapType kernelMap = (MapType) kernelDataType;\n      return DataTypes.createMapType(\n          convertKernelDataTypeToSparkDataType(kernelMap.getKeyType()),\n          convertKernelDataTypeToSparkDataType(kernelMap.getValueType()),\n          kernelMap.isValueContainsNull());\n    } else if (kernelDataType instanceof StructType) {\n      return convertKernelSchemaToSparkSchema((StructType) kernelDataType);\n    } else if (kernelDataType instanceof VariantType) {\n      return DataTypes.VariantType;\n    } else {\n      throw new IllegalArgumentException(\"unsupported data type \" + kernelDataType);\n    }\n  }\n\n  //////////////////////\n  // Spark --> Kernel //\n  //////////////////////\n\n  /** Converts a Spark schema to a Delta Kernel schema. */\n  public static StructType convertSparkSchemaToKernelSchema(\n      org.apache.spark.sql.types.StructType sparkSchema) {\n    requireNonNull(sparkSchema);\n    List<StructField> kernelFields = new ArrayList<>();\n\n    for (org.apache.spark.sql.types.StructField field : sparkSchema.fields()) {\n      kernelFields.add(\n          new StructField(\n              field.name(),\n              convertSparkDataTypeToKernelDataType(field.dataType()),\n              field.nullable(),\n              convertSparkMetadataToKernelFieldMetadata(field.metadata())));\n    }\n\n    return new StructType(kernelFields);\n  }\n\n  /** Converts a Spark data type to a Delta Kernel data type. */\n  public static DataType convertSparkDataTypeToKernelDataType(\n      org.apache.spark.sql.types.DataType sparkDataType) {\n    requireNonNull(sparkDataType);\n    if (sparkDataType instanceof org.apache.spark.sql.types.StringType) {\n      return StringType.STRING;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.BooleanType) {\n      return BooleanType.BOOLEAN;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.IntegerType) {\n      return IntegerType.INTEGER;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.LongType) {\n      return LongType.LONG;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.BinaryType) {\n      return BinaryType.BINARY;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.ByteType) {\n      return ByteType.BYTE;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.DateType) {\n      return DateType.DATE;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.DecimalType) {\n      org.apache.spark.sql.types.DecimalType sparkDecimal =\n          (org.apache.spark.sql.types.DecimalType) sparkDataType;\n      return new DecimalType(sparkDecimal.precision(), sparkDecimal.scale());\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.DoubleType) {\n      return DoubleType.DOUBLE;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.FloatType) {\n      return FloatType.FLOAT;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.ShortType) {\n      return ShortType.SHORT;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.TimestampType) {\n      return TimestampType.TIMESTAMP;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.TimestampNTZType) {\n      return TimestampNTZType.TIMESTAMP_NTZ;\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.ArrayType) {\n      org.apache.spark.sql.types.ArrayType sparkArray =\n          (org.apache.spark.sql.types.ArrayType) sparkDataType;\n      return new ArrayType(\n          convertSparkDataTypeToKernelDataType(sparkArray.elementType()),\n          sparkArray.containsNull());\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.MapType) {\n      org.apache.spark.sql.types.MapType sparkMap =\n          (org.apache.spark.sql.types.MapType) sparkDataType;\n      return new MapType(\n          convertSparkDataTypeToKernelDataType(sparkMap.keyType()),\n          convertSparkDataTypeToKernelDataType(sparkMap.valueType()),\n          sparkMap.valueContainsNull());\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.StructType) {\n      return convertSparkSchemaToKernelSchema(\n          (org.apache.spark.sql.types.StructType) sparkDataType);\n    } else if (sparkDataType instanceof org.apache.spark.sql.types.VariantType) {\n      return VariantType.VARIANT;\n    } else {\n      throw new IllegalArgumentException(\"unsupported data type \" + sparkDataType);\n    }\n  }\n\n  ///////////////////////////////\n  // Field Metadata Conversion //\n  ///////////////////////////////\n\n  /**\n   * Converts Kernel FieldMetadata to Spark Metadata.\n   *\n   * @param kernelMetadata the Kernel FieldMetadata to convert\n   * @return the equivalent Spark Metadata\n   */\n  public static Metadata convertKernelFieldMetadataToSparkMetadata(\n      io.delta.kernel.types.FieldMetadata kernelMetadata) {\n    requireNonNull(kernelMetadata);\n    if (kernelMetadata.getEntries().isEmpty()) {\n      return Metadata.empty();\n    }\n    MetadataBuilder builder = new MetadataBuilder();\n    kernelMetadata\n        .getEntries()\n        .forEach(\n            (key, value) -> {\n              if (value instanceof Long) {\n                builder.putLong(key, (Long) value);\n              } else if (value instanceof Double) {\n                builder.putDouble(key, (Double) value);\n              } else if (value instanceof Boolean) {\n                builder.putBoolean(key, (Boolean) value);\n              } else if (value instanceof String) {\n                builder.putString(key, (String) value);\n              } else if (value instanceof io.delta.kernel.types.FieldMetadata) {\n                builder.putMetadata(\n                    key,\n                    convertKernelFieldMetadataToSparkMetadata(\n                        (io.delta.kernel.types.FieldMetadata) value));\n              } else if (value instanceof Long[]) {\n                builder.putLongArray(key, unboxLongArray((Long[]) value, key));\n              } else if (value instanceof Double[]) {\n                builder.putDoubleArray(key, unboxDoubleArray((Double[]) value, key));\n              } else if (value instanceof Boolean[]) {\n                builder.putBooleanArray(key, unboxBooleanArray((Boolean[]) value, key));\n              } else if (value instanceof String[]) {\n                builder.putStringArray(key, (String[]) value);\n              } else if (value instanceof io.delta.kernel.types.FieldMetadata[]) {\n                io.delta.kernel.types.FieldMetadata[] kernelMetadatas =\n                    (io.delta.kernel.types.FieldMetadata[]) value;\n                Metadata[] sparkMetadatas =\n                    Arrays.stream(kernelMetadatas)\n                        .map(SchemaUtils::convertKernelFieldMetadataToSparkMetadata)\n                        .toArray(Metadata[]::new);\n                builder.putMetadataArray(key, sparkMetadatas);\n              } else if (value == null) {\n                builder.putNull(key);\n              } else {\n                throw new UnsupportedOperationException(\n                    \"Unsupported metadata value type: \" + value.getClass().getName());\n              }\n            });\n    return builder.build();\n  }\n\n  /**\n   * Converts Spark Metadata to Kernel FieldMetadata.\n   *\n   * @param sparkMetadata the Spark Metadata to convert\n   * @return the equivalent Kernel FieldMetadata\n   */\n  public static io.delta.kernel.types.FieldMetadata convertSparkMetadataToKernelFieldMetadata(\n      Metadata sparkMetadata) {\n    requireNonNull(sparkMetadata);\n    if (sparkMetadata.map().isEmpty()) {\n      return io.delta.kernel.types.FieldMetadata.empty();\n    }\n    io.delta.kernel.types.FieldMetadata.Builder builder =\n        io.delta.kernel.types.FieldMetadata.builder();\n\n    CollectionConverters.MapHasAsJava(sparkMetadata.map())\n        .asJava()\n        .forEach(\n            (key, value) -> {\n              if (value instanceof Long) {\n                builder.putLong(key, (Long) value);\n              } else if (value instanceof Double) {\n                builder.putDouble(key, (Double) value);\n              } else if (value instanceof Boolean) {\n                builder.putBoolean(key, (Boolean) value);\n              } else if (value instanceof String) {\n                builder.putString(key, (String) value);\n              } else if (value instanceof Metadata) {\n                builder.putFieldMetadata(\n                    key, convertSparkMetadataToKernelFieldMetadata((Metadata) value));\n              } else if (value instanceof long[]) {\n                builder.putLongArray(\n                    key, Arrays.stream((long[]) value).boxed().toArray(Long[]::new));\n              } else if (value instanceof double[]) {\n                builder.putDoubleArray(\n                    key, Arrays.stream((double[]) value).boxed().toArray(Double[]::new));\n              } else if (value instanceof boolean[]) {\n                boolean[] valArray = (boolean[]) value;\n                Boolean[] booleanArray = new Boolean[valArray.length];\n                for (int i = 0; i < valArray.length; i++) {\n                  booleanArray[i] = valArray[i];\n                }\n                builder.putBooleanArray(key, booleanArray);\n              } else if (value instanceof String[]) {\n                builder.putStringArray(key, (String[]) value);\n              } else if (value instanceof Metadata[]) {\n                Metadata[] sparkMetadatas = (Metadata[]) value;\n                io.delta.kernel.types.FieldMetadata[] kernelMetadatas =\n                    Arrays.stream(sparkMetadatas)\n                        .map(SchemaUtils::convertSparkMetadataToKernelFieldMetadata)\n                        .toArray(io.delta.kernel.types.FieldMetadata[]::new);\n                builder.putFieldMetadataArray(key, kernelMetadatas);\n              } else if (value == null) {\n                builder.putNull(key);\n              } else {\n                throw new UnsupportedOperationException(\n                    \"Unsupported metadata value type: \" + value.getClass().getName());\n              }\n            });\n    return builder.build();\n  }\n\n  /**\n   * Unboxes a Long[] to long[], checking for nulls.\n   *\n   * @throws NullPointerException if any element is null\n   */\n  private static long[] unboxLongArray(Long[] boxedArray, String key) {\n    long[] primitiveArray = new long[boxedArray.length];\n    for (int i = 0; i < boxedArray.length; i++) {\n      requireNonNull(\n          boxedArray[i],\n          String.format(\"Null element at index %s in Long array for key '%s'\", i, key));\n      primitiveArray[i] = boxedArray[i];\n    }\n    return primitiveArray;\n  }\n\n  /**\n   * Unboxes a Double[] to double[], checking for nulls.\n   *\n   * @throws NullPointerException if any element is null\n   */\n  private static double[] unboxDoubleArray(Double[] boxedArray, String key) {\n    double[] primitiveArray = new double[boxedArray.length];\n    for (int i = 0; i < boxedArray.length; i++) {\n      requireNonNull(\n          boxedArray[i],\n          String.format(\"Null element at index %s in Double array for key '%s'\", i, key));\n      primitiveArray[i] = boxedArray[i];\n    }\n    return primitiveArray;\n  }\n\n  /**\n   * Unboxes a Boolean[] to boolean[], checking for nulls.\n   *\n   * @throws NullPointerException if any element is null\n   */\n  private static boolean[] unboxBooleanArray(Boolean[] boxedArray, String key) {\n    boolean[] primitiveArray = new boolean[boxedArray.length];\n    for (int i = 0; i < boxedArray.length; i++) {\n      requireNonNull(\n          boxedArray[i],\n          String.format(\"Null element at index %s in Boolean array for key '%s'\", i, key));\n      primitiveArray[i] = boxedArray[i];\n    }\n    return primitiveArray;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/SerializableKernelRowWrapper.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.defaults.internal.json.JsonUtils;\nimport io.delta.kernel.internal.types.DataTypeJsonSerDe;\nimport io.delta.kernel.types.StructType;\nimport java.io.Serializable;\n\npublic class SerializableKernelRowWrapper implements Serializable {\n\n  private final String rowJson;\n  private final String schemaJson;\n  private transient Row row;\n\n  public SerializableKernelRowWrapper(Row row) {\n    this.rowJson = JsonUtils.rowToJson(row);\n    this.schemaJson = DataTypeJsonSerDe.serializeDataType(row.getSchema());\n    this.row = row;\n  }\n\n  public Row getRow() {\n    if (row == null) {\n      StructType schema = DataTypeJsonSerDe.deserializeStructType(schemaJson);\n      row = JsonUtils.rowFromJson(rowJson, schema);\n    }\n    return row;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (o == null || getClass() != o.getClass()) return false;\n    SerializableKernelRowWrapper that = (SerializableKernelRowWrapper) o;\n    return rowJson.equals(that.rowJson) && schemaJson.equals(that.schemaJson);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = rowJson.hashCode();\n    result = 31 * result + schemaJson.hashCode();\n    return result;\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/StatsUtils.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static io.delta.spark.internal.v2.utils.ScalaUtils.toJavaOptional;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.OptionalLong;\nimport org.apache.spark.sql.catalyst.catalog.CatalogColumnStat;\nimport org.apache.spark.sql.catalyst.catalog.CatalogColumnStat$;\nimport org.apache.spark.sql.catalyst.catalog.CatalogStatistics;\nimport org.apache.spark.sql.connector.expressions.FieldReference;\nimport org.apache.spark.sql.connector.expressions.NamedReference;\nimport org.apache.spark.sql.connector.read.Statistics;\nimport org.apache.spark.sql.connector.read.colstats.ColumnStatistics;\nimport org.apache.spark.sql.types.DataType;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\n\n/** Utilities for converting catalog statistics to V2 connector statistics. */\npublic final class StatsUtils {\n\n  private StatsUtils() {}\n\n  /**\n   * Convert {@link CatalogStatistics} to V2 connector {@link Statistics}.\n   *\n   * @param catalogStats the catalog statistics to convert\n   * @param dataSchema the data schema (non-partition columns)\n   * @param partitionSchema the partition schema\n   * @return V2 Statistics representation\n   */\n  public static Statistics toV2Statistics(\n      CatalogStatistics catalogStats, StructType dataSchema, StructType partitionSchema) {\n    // Build a map of column name -> DataType from both schemas\n    Map<String, DataType> columnTypes = new HashMap<>();\n    for (StructField field : dataSchema.fields()) {\n      columnTypes.put(field.name(), field.dataType());\n    }\n    for (StructField field : partitionSchema.fields()) {\n      columnTypes.put(field.name(), field.dataType());\n    }\n\n    Map<NamedReference, ColumnStatistics> colStatsMap = buildColumnStats(catalogStats, columnTypes);\n\n    return new Statistics() {\n      @Override\n      public OptionalLong sizeInBytes() {\n        return OptionalLong.of(catalogStats.sizeInBytes().longValue());\n      }\n\n      @Override\n      public OptionalLong numRows() {\n        return toJavaOptional(catalogStats.rowCount())\n            .map(r -> OptionalLong.of(r.longValue()))\n            .orElse(OptionalLong.empty());\n      }\n\n      @Override\n      public Map<NamedReference, ColumnStatistics> columnStats() {\n        return colStatsMap;\n      }\n    };\n  }\n\n  private static Map<NamedReference, ColumnStatistics> buildColumnStats(\n      CatalogStatistics catalogStats, Map<String, DataType> columnTypes) {\n    Map<String, CatalogColumnStat> colStats =\n        scala.collection.JavaConverters.mapAsJavaMapConverter(catalogStats.colStats()).asJava();\n\n    if (colStats.isEmpty()) {\n      return Collections.emptyMap();\n    }\n\n    Map<NamedReference, ColumnStatistics> result = new HashMap<>();\n    for (Map.Entry<String, CatalogColumnStat> entry : colStats.entrySet()) {\n      String colName = entry.getKey();\n      CatalogColumnStat stat = entry.getValue();\n      DataType dataType = columnTypes.get(colName);\n\n      if (dataType == null) {\n        continue;\n      }\n\n      NamedReference ref = FieldReference.apply(colName);\n      int version = stat.version();\n\n      // Eagerly parse min/max to avoid repeated fromExternalString calls\n      Optional<Object> minValue =\n          toJavaOptional(stat.min())\n              .map(\n                  minStr ->\n                      CatalogColumnStat$.MODULE$.fromExternalString(\n                          minStr, colName, dataType, version));\n      Optional<Object> maxValue =\n          toJavaOptional(stat.max())\n              .map(\n                  maxStr ->\n                      CatalogColumnStat$.MODULE$.fromExternalString(\n                          maxStr, colName, dataType, version));\n      OptionalLong distinctCount =\n          toJavaOptional(stat.distinctCount())\n              .map(d -> OptionalLong.of(d.longValue()))\n              .orElse(OptionalLong.empty());\n      OptionalLong nullCount =\n          toJavaOptional(stat.nullCount())\n              .map(n -> OptionalLong.of(n.longValue()))\n              .orElse(OptionalLong.empty());\n      OptionalLong avgLen =\n          toJavaOptional(stat.avgLen())\n              .map(v -> OptionalLong.of(((Number) v).longValue()))\n              .orElse(OptionalLong.empty());\n      OptionalLong maxLen =\n          toJavaOptional(stat.maxLen())\n              .map(v -> OptionalLong.of(((Number) v).longValue()))\n              .orElse(OptionalLong.empty());\n\n      ColumnStatistics v2ColStats =\n          new ColumnStatistics() {\n            @Override\n            public OptionalLong distinctCount() {\n              return distinctCount;\n            }\n\n            @Override\n            public Optional<Object> min() {\n              return minValue;\n            }\n\n            @Override\n            public Optional<Object> max() {\n              return maxValue;\n            }\n\n            @Override\n            public OptionalLong nullCount() {\n              return nullCount;\n            }\n\n            @Override\n            public OptionalLong avgLen() {\n              return avgLen;\n            }\n\n            @Override\n            public OptionalLong maxLen() {\n              return maxLen;\n            }\n          };\n      result.put(ref, v2ColStats);\n    }\n    return Collections.unmodifiableMap(result);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/main/java/io/delta/spark/internal/v2/utils/StreamingHelper.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static io.delta.kernel.internal.util.Preconditions.checkArgument;\nimport static io.delta.kernel.internal.util.Preconditions.checkState;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.internal.DeltaLogActionUtils;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.RemoveFile;\nimport io.delta.kernel.internal.commitrange.CommitRangeImpl;\nimport io.delta.kernel.internal.data.StructRow;\nimport io.delta.kernel.internal.util.Preconditions;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.spark.internal.v2.snapshot.DeltaSnapshotManager;\nimport java.io.IOException;\nimport java.util.*;\nimport org.apache.spark.annotation.Experimental;\n\n/**\n * Helper class providing utilities for working with Delta table data in streaming scenarios.\n *\n * <p>This class provides static utility methods for extracting information from Delta table\n * batches, such as version numbers and data change actions.\n */\n@Experimental\npublic class StreamingHelper {\n\n  /**\n   * Returns the index of the field with the given name in the schema of the batch. Throws an {@link\n   * IllegalArgumentException} if the field is not found.\n   */\n  private static int getFieldIndex(ColumnarBatch batch, String fieldName) {\n    int index = batch.getSchema().indexOf(fieldName);\n    checkArgument(index >= 0, \"Field '%s' not found in schema: %s\", fieldName, batch.getSchema());\n    return index;\n  }\n\n  /**\n   * Get the version from a {@link ColumnarBatch} of Delta log actions. Assumes all rows in the\n   * batch belong to the same commit version, so it reads the version from the first row (rowId=0).\n   */\n  public static long getVersion(ColumnarBatch batch) {\n    int versionColIdx = getFieldIndex(batch, \"version\");\n    return batch.getColumnVector(versionColIdx).getLong(0);\n  }\n\n  /**\n   * Get AddFile action from a FilteredColumnarBatch at the specified row, if present.\n   *\n   * <p>This method respects the selection vector to filter out duplicate files that may appear when\n   * stats re-collection (e.g., ANALYZE TABLE COMPUTE STATISTICS) re-adds files with updated stats.\n   * The Kernel uses selection vectors to mark which rows (AddFiles) are logically valid.\n   *\n   * @param batch the FilteredColumnarBatch containing AddFile actions\n   * @param rowId the row index to check\n   * @return Optional containing the AddFile if present and selected, empty otherwise\n   */\n  public static Optional<AddFile> getAddFile(FilteredColumnarBatch batch, int rowId) {\n    // Check selection vector first - rows may be filtered out when stats re-collection\n    // re-adds files with updated stats\n    Optional<ColumnVector> selectionVector = batch.getSelectionVector();\n    boolean isFiltered =\n        selectionVector.map(sv -> sv.isNullAt(rowId) || !sv.getBoolean(rowId)).orElse(false);\n    if (isFiltered) {\n      return Optional.empty();\n    }\n\n    return getAddFile(batch.getData(), rowId);\n  }\n\n  /**\n   * Get AddFile action from a ColumnarBatch at the specified row, if present.\n   *\n   * <p>Caller should ensure all rows are valid (e.g., not filtered by selection vector). For\n   * FilteredColumnarBatch with selection vectors, use {@link #getAddFile(FilteredColumnarBatch,\n   * int)} instead.\n   */\n  private static Optional<AddFile> getAddFile(ColumnarBatch batch, int rowId) {\n    int addIdx = getFieldIndex(batch, DeltaLogActionUtils.DeltaAction.ADD.colName);\n    ColumnVector addVector = batch.getColumnVector(addIdx);\n    if (addVector.isNullAt(rowId)) {\n      return Optional.empty();\n    }\n\n    Row addFileRow = StructRow.fromStructVector(addVector, rowId);\n    checkState(\n        addFileRow != null,\n        String.format(\"Failed to extract AddFile struct from batch at rowId=%d.\", rowId));\n\n    return Optional.of(new AddFile(addFileRow));\n  }\n\n  /** Get AddFile action from a batch at the specified row, if present and has dataChange=true. */\n  public static Optional<AddFile> getAddFileWithDataChange(ColumnarBatch batch, int rowId) {\n    return getAddFile(batch, rowId).filter(AddFile::getDataChange);\n  }\n\n  /**\n   * Get RemoveFile action from a batch at the specified row, if present and has dataChange=true.\n   */\n  public static Optional<RemoveFile> getDataChangeRemove(ColumnarBatch batch, int rowId) {\n    int removeIdx = getFieldIndex(batch, DeltaLogActionUtils.DeltaAction.REMOVE.colName);\n    ColumnVector removeVector = batch.getColumnVector(removeIdx);\n    if (removeVector.isNullAt(rowId)) {\n      return Optional.empty();\n    }\n\n    Row removeFileRow = StructRow.fromStructVector(removeVector, rowId);\n    checkState(\n        removeFileRow != null,\n        String.format(\"Failed to extract RemoveFile struct from batch at rowId=%d.\", rowId));\n\n    RemoveFile removeFile = new RemoveFile(removeFileRow);\n    return removeFile.getDataChange() ? Optional.of(removeFile) : Optional.empty();\n  }\n\n  /** Get Metadata action from a batch at the specified row, if present. */\n  public static Optional<Metadata> getMetadata(ColumnarBatch batch, int rowId) {\n    int metadataIdx = getFieldIndex(batch, DeltaLogActionUtils.DeltaAction.METADATA.colName);\n    ColumnVector metadataVector = batch.getColumnVector(metadataIdx);\n    Metadata metadata = Metadata.fromColumnVector(metadataVector, rowId);\n\n    return Optional.ofNullable(metadata);\n  }\n\n  /**\n   * Gets commit-level actions from a commit range without requiring a snapshot at the exact start\n   * version.\n   *\n   * <p>Returns an iterator over {@link CommitActions}, where each CommitActions represents a single\n   * commit.\n   *\n   * <p>This method is \"unsafe\" because it bypasses the standard {@code\n   * CommitRange.getCommitActions()} API which requires a snapshot at the exact start version for\n   * protocol validation.\n   *\n   * @param engine the Delta engine\n   * @param commitRange the commit range to read actions from\n   * @param tablePath the path to the Delta table\n   * @param actionSet the set of actions to read (e.g., ADD, REMOVE)\n   * @return an iterator over {@link CommitActions}, one per commit version\n   */\n  public static CloseableIterator<CommitActions> getCommitActionsFromRangeUnsafe(\n      Engine engine,\n      CommitRangeImpl commitRange,\n      String tablePath,\n      Set<DeltaLogActionUtils.DeltaAction> actionSet) {\n    return DeltaLogActionUtils.getActionsFromCommitFilesWithProtocolValidation(\n        engine, tablePath, commitRange.getDeltaFiles(), actionSet);\n  }\n\n  /**\n   * Collects metadata actions from a commit range, mapping each version to its metadata.\n   *\n   * <p>This method is \"unsafe\" because it uses {@code getActionsFromRangeUnsafe()} which bypasses\n   * the standard snapshot requirement for protocol validation.\n   *\n   * <p>Returns a map preserving version order (via LinkedHashMap) where each version maps to its\n   * metadata action. Throws an exception if multiple metadata actions are found in the same commit.\n   *\n   * @param startVersion the starting version (inclusive) of the commit range\n   * @param endVersionOpt optional ending version (exclusive) of the commit range\n   * @param snapshotManager the Delta snapshot manager\n   * @param engine the Delta engine\n   * @param tablePath the path to the Delta table\n   * @return a map from version number to metadata action, in version order\n   */\n  public static Map<Long, Metadata> collectMetadataActionsFromRangeUnsafe(\n      long startVersion,\n      Optional<Long> endVersionOpt,\n      DeltaSnapshotManager snapshotManager,\n      Engine engine,\n      String tablePath) {\n    CommitRangeImpl commitRange =\n        (CommitRangeImpl) snapshotManager.getTableChanges(engine, startVersion, endVersionOpt);\n    // LinkedHashMap to preserve insertion order\n    Map<Long, Metadata> versionToMetadata = new LinkedHashMap<>();\n\n    try (CloseableIterator<CommitActions> commitsIter =\n        getCommitActionsFromRangeUnsafe(\n            engine, commitRange, tablePath, Set.of(DeltaLogActionUtils.DeltaAction.METADATA))) {\n      while (commitsIter.hasNext()) {\n        try (CommitActions commit = commitsIter.next()) {\n          long version = commit.getVersion();\n          try (CloseableIterator<ColumnarBatch> actionsIter = commit.getActions()) {\n            while (actionsIter.hasNext()) {\n              ColumnarBatch batch = actionsIter.next();\n              int numRows = batch.getSize();\n              for (int rowId = 0; rowId < numRows; rowId++) {\n                Optional<Metadata> metadataOpt = StreamingHelper.getMetadata(batch, rowId);\n                if (metadataOpt.isPresent()) {\n                  Metadata existing = versionToMetadata.putIfAbsent(version, metadataOpt.get());\n                  Preconditions.checkArgument(\n                      existing == null,\n                      \"Should not encounter two metadata actions in the same commit of version %d\",\n                      version);\n                }\n              }\n            }\n          } catch (IOException e) {\n            throw new RuntimeException(\"Failed to process commit at version \" + version, e);\n          }\n        }\n      }\n    } catch (RuntimeException e) {\n      throw e; // Rethrow runtime exceptions directly\n    } catch (Exception e) {\n      // CommitActions.close() throws Exception\n      throw new RuntimeException(\"Failed to process commits\", e);\n    }\n\n    return versionToMetadata;\n  }\n\n  /** Private constructor to prevent instantiation of this utility class. */\n  private StreamingHelper() {}\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/DeltaV2TestBase.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2;\n\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport org.apache.spark.sql.SparkSession;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\n\npublic abstract class DeltaV2TestBase {\n\n  protected static SparkSession spark;\n  protected static Engine defaultEngine;\n\n  @BeforeAll\n  public static void setUpSparkAndEngine() {\n    spark =\n        SparkSession.builder()\n            .master(\"local[*]\")\n            .appName(\"SparkKernelDsv2Tests\")\n            .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtensionV1\")\n            .config(\n                \"spark.sql.catalog.spark_catalog\",\n                \"org.apache.spark.sql.delta.catalog.DeltaCatalogV1\")\n            .getOrCreate();\n    defaultEngine = DefaultEngine.create(spark.sessionState().newHadoopConf());\n  }\n\n  @AfterAll\n  public static void tearDownSpark() {\n    if (spark != null) {\n      spark.stop();\n      spark = null;\n    }\n  }\n\n  protected void createTestTableWithData(String path, String tableName) {\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING, value DOUBLE) USING delta LOCATION '%s'\",\n            tableName, path));\n    spark.sql(\n        String.format(\n            \"INSERT INTO %s VALUES (1, 'Alice', 10.5), (2, 'Bob', 20.5), (3, 'Charlie', 30.5)\",\n            tableName));\n  }\n\n  protected void createEmptyTestTable(String path, String tableName) {\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\", tableName, path));\n  }\n\n  protected void createEmptyPartitionedTestTable(String path, String tableName) {\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta PARTITIONED BY (name) LOCATION '%s'\",\n            tableName, path));\n  }\n\n  protected void createSchemaEvolutionTestTable(String path, String tableName) {\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT NOT NULL, \"\n                + \"name String, value FLOAT, \"\n                + \"info STRUCT<col1: INT, col2: STRING>) USING delta LOCATION '%s'\"\n                + \"TBLPROPERTIES (\"\n                + \"'delta.columnMapping.mode' = 'name', \"\n                + \"'delta.enableTypeWidening' = 'true')\",\n            tableName, path));\n    spark.sql(\n        String.format(\n            \"INSERT INTO %s VALUES \"\n                + \"(1, 'Alice', 10.5, named_struct('col1', 27, 'col2', 'LA')), \"\n                + \"(2,'Bob', NULL, named_struct('col1', 30, 'col2', 'NYC'))\",\n            tableName));\n  }\n\n  /** A runnable that can throw checked exceptions, for use with {@link #withSQLConf}. */\n  @FunctionalInterface\n  protected interface ThrowingRunnable {\n    void run() throws Exception;\n  }\n\n  /**\n   * Runs the given action with a Spark SQL configuration temporarily set, then restores the\n   * original value afterwards (similar to Scala's {@code withSQLConf}).\n   */\n  protected void withSQLConf(String key, String value, ThrowingRunnable action) throws Exception {\n    scala.Option<String> original = spark.conf().getOption(key);\n    spark.conf().set(key, value);\n    try {\n      action.run();\n    } finally {\n      if (original.isDefined()) {\n        spark.conf().set(key, original.get());\n      } else {\n        spark.conf().unset(key);\n      }\n    }\n  }\n\n  /**\n   * Runs the given action and drops the specified tables afterwards, similar to Scala's {@code\n   * withTable}.\n   */\n  protected void withTable(String[] tableNames, ThrowingRunnable action) throws Exception {\n    try {\n      action.run();\n    } finally {\n      for (String tableName : tableNames) {\n        spark.sql(String.format(\"DROP TABLE IF EXISTS %s\", tableName));\n      }\n    }\n  }\n\n  protected static void createPartitionedTable(String tableName, String path) {\n    spark.sql(\n        String.format(\n            \"CREATE TABLE `%s` (part INT, date STRING, city STRING, name STRING, cnt INT) USING delta LOCATION '%s' PARTITIONED BY (date, city, part)\",\n            tableName, path));\n    spark.sql(\n        String.format(\n            \"INSERT INTO %s VALUES \"\n                + \"('1', '20180520', 'hz', 'Alice', '10'),\"\n                + \"('1', '20180718', 'hz', 'Bob', '20'),\"\n                + \"('1', '20180512', 'sh', 'Charlie', '30'),\"\n                + \"('2', '20180520', 'bj', 'David', '40'),\"\n                + \"('2', '20181212', 'sz', 'Eve', '50')\",\n            tableName));\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/InternalRowTestUtils.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.RowFactory;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.catalyst.expressions.GenericInternalRow;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.vectorized.ColumnarBatch;\nimport org.apache.spark.unsafe.types.UTF8String;\nimport scala.Function1;\nimport scala.collection.Iterator;\nimport scala.jdk.javaapi.CollectionConverters;\nimport scala.runtime.AbstractFunction1;\n\n/** Test helper utilities for InternalRow operations. */\npublic class InternalRowTestUtils {\n\n  private InternalRowTestUtils() {}\n\n  /** Create a mock base reader that returns the given rows. */\n  public static Function1<PartitionedFile, Iterator<InternalRow>> mockReader(\n      List<InternalRow> rows) {\n    return new AbstractFunction1<PartitionedFile, Iterator<InternalRow>>() {\n      @Override\n      public Iterator<InternalRow> apply(PartitionedFile file) {\n        return CollectionConverters.asScala(rows.iterator());\n      }\n    };\n  }\n\n  /**\n   * Create a mock reader that returns ColumnarBatch objects through Iterator&lt;InternalRow&gt;.\n   *\n   * <p>Mimics Spark vectorized mode where ColumnarBatch is passed via type erasure.\n   */\n  @SuppressWarnings(\"unchecked\")\n  public static Function1<PartitionedFile, Iterator<InternalRow>> mockBatchReader(\n      List<ColumnarBatch> batches) {\n    return new AbstractFunction1<PartitionedFile, Iterator<InternalRow>>() {\n      @Override\n      public Iterator<InternalRow> apply(PartitionedFile file) {\n        return (Iterator<InternalRow>)\n            (Iterator<?>) CollectionConverters.asScala(batches.iterator());\n      }\n    };\n  }\n\n  /** Collect all rows from iterator into a list, copying each row. */\n  public static List<InternalRow> collectRows(Iterator<InternalRow> iter) {\n    List<InternalRow> result = new ArrayList<>();\n    while (iter.hasNext()) {\n      result.add(iter.next().copy());\n    }\n    return result;\n  }\n\n  /** Collect all ColumnarBatch objects from an iterator (for vectorized mode testing). */\n  @SuppressWarnings(\"unchecked\")\n  public static List<ColumnarBatch> collectBatches(Iterator<InternalRow> iter) {\n    Iterator<Object> objectIter = (Iterator<Object>) (Iterator<?>) iter;\n    List<ColumnarBatch> result = new ArrayList<>();\n    while (objectIter.hasNext()) {\n      result.add((ColumnarBatch) objectIter.next());\n    }\n    return result;\n  }\n\n  /** Create an InternalRow from values. Strings are converted to UTF8String. */\n  public static InternalRow row(Object... values) {\n    Object[] converted = new Object[values.length];\n    for (int i = 0; i < values.length; i++) {\n      converted[i] =\n          values[i] instanceof String ? UTF8String.fromString((String) values[i]) : values[i];\n    }\n    return new GenericInternalRow(converted);\n  }\n\n  /** Assert that actual InternalRows match expected rows. Converts to Row for comparison. */\n  public static void assertRowsEquals(List<InternalRow> actual, List<InternalRow> expected) {\n    assertEquals(toRows(expected), toRows(actual));\n  }\n\n  /** Convert InternalRows to Rows, converting UTF8String back to String. */\n  private static List<Row> toRows(List<InternalRow> rows) {\n    return rows.stream()\n        .map(\n            r ->\n                RowFactory.create(\n                    IntStream.range(0, r.numFields())\n                        .mapToObj(\n                            i ->\n                                r.get(i, null) instanceof UTF8String\n                                    ? r.get(i, null).toString()\n                                    : r.get(i, null))\n                        .toArray()))\n        .collect(Collectors.toList());\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/V2DDLTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.apache.spark.sql.*;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.io.TempDir;\n\n/** Tests for V2 DDL operations. */\npublic class V2DDLTest extends V2TestBase {\n\n  @Test\n  public void testCreateTable() {\n    spark.sql(\n        str(\n            \"CREATE TABLE dsv2.%s.create_table_test (id INT, name STRING, value DOUBLE)\",\n            nameSpace));\n\n    Dataset<Row> actual = spark.sql(str(\"DESCRIBE TABLE dsv2.%s.create_table_test\", nameSpace));\n\n    List<Row> expectedRows =\n        Arrays.asList(\n            RowFactory.create(\"id\", \"int\", null),\n            RowFactory.create(\"name\", \"string\", null),\n            RowFactory.create(\"value\", \"double\", null));\n    assertDatasetEquals(actual, expectedRows);\n  }\n\n  @Test\n  public void testQueryTableNotExist() {\n    AnalysisException e =\n        org.junit.jupiter.api.Assertions.assertThrows(\n            AnalysisException.class,\n            () -> spark.sql(str(\"SELECT * FROM dsv2.%s.not_found_test\", nameSpace)));\n    assertEquals(\n        \"TABLE_OR_VIEW_NOT_FOUND\",\n        e.getErrorClass(),\n        \"Missing table should raise TABLE_OR_VIEW_NOT_FOUND\");\n  }\n\n  @Test\n  public void testPathBasedTable(@TempDir File deltaTablePath) {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Create test data and write as Delta table\n    Dataset<Row> testData =\n        spark.createDataFrame(\n            Arrays.asList(\n                RowFactory.create(1, \"Alice\", 100.0),\n                RowFactory.create(2, \"Bob\", 200.0),\n                RowFactory.create(3, \"Charlie\", 300.0)),\n            DataTypes.createStructType(\n                Arrays.asList(\n                    DataTypes.createStructField(\"id\", DataTypes.IntegerType, false),\n                    DataTypes.createStructField(\"name\", DataTypes.StringType, false),\n                    DataTypes.createStructField(\"value\", DataTypes.DoubleType, false))));\n\n    testData.write().format(\"delta\").save(tablePath);\n\n    // TODO: [delta-io/delta#5001] change to select query after batch read is supported for dsv2\n    // path.\n    Dataset<Row> actual = spark.sql(str(\"DESCRIBE TABLE dsv2.delta.`%s`\", tablePath));\n\n    List<Row> expectedRows =\n        Arrays.asList(\n            RowFactory.create(\"id\", \"int\", null),\n            RowFactory.create(\"name\", \"string\", null),\n            RowFactory.create(\"value\", \"double\", null));\n\n    assertDatasetEquals(actual, expectedRows);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/V2ReadTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.util.List;\nimport org.apache.spark.sql.delta.DeltaLog;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.io.TempDir;\nimport scala.Option;\n\n/** Tests for V2 batch read operations. */\npublic class V2ReadTest extends V2TestBase {\n\n  @Test\n  public void testBatchRead() {\n    spark.sql(\n        str(\"CREATE TABLE dsv2.%s.batch_read_test (id INT, name STRING, value DOUBLE)\", nameSpace));\n\n    check(str(\"SELECT * FROM dsv2.%s.batch_read_test\", nameSpace), List.of());\n  }\n\n  @Test\n  public void testColumnMappingRead(@TempDir File deltaTablePath) {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Create a Delta table with column mapping enabled using name mode\n    spark.sql(\n        str(\n            \"CREATE TABLE delta.`%s` (id INT, user_name STRING, amount DOUBLE) \"\n                + \"USING delta \"\n                + \"TBLPROPERTIES ('delta.columnMapping.mode' = 'name')\",\n            tablePath));\n\n    // Insert test data\n    spark.sql(\n        str(\"INSERT INTO delta.`%s` VALUES (1, 'Alice', 100.0), (2, 'Bob', 200.0)\", tablePath));\n\n    // Read through V2 and verify\n    check(\n        str(\"SELECT * FROM dsv2.delta.`%s` ORDER BY id\", tablePath),\n        List.of(row(1, \"Alice\", 100.0), row(2, \"Bob\", 200.0)));\n  }\n\n  @Test\n  public void testDeletionVectorRead(@TempDir File tempDir) throws Exception {\n    // Create a directory with space in the name to test URL encoding handling\n    File dirWithSpace = new File(tempDir, \"my table\");\n    Files.createDirectories(dirWithSpace.toPath());\n    String tablePath = dirWithSpace.getAbsolutePath();\n\n    // Create a Delta table with deletion vectors enabled.\n    spark.sql(\n        str(\n            \"CREATE TABLE delta.`%s` (id LONG, value STRING) \"\n                + \"USING delta \"\n                + \"TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\",\n            tablePath));\n\n    // Insert enough data so that DELETE creates DVs instead of rewriting the file.\n    // Use spark.range() to generate more rows.\n    spark\n        .range(1000)\n        .selectExpr(\"id\", \"cast(id as string) as value\")\n        .write()\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tablePath);\n\n    // Delete some rows to create deletion vectors (not whole file deletions).\n    spark.sql(str(\"DELETE FROM delta.`%s` WHERE id %% 2 = 0\", tablePath));\n\n    // Verify that deletion vectors were actually created.\n    DeltaLog deltaLog = DeltaLog.forTable(spark, tablePath);\n    long numDVs =\n        (long)\n            deltaLog\n                .update(false, Option.empty(), Option.empty())\n                .numDeletionVectorsOpt()\n                .getOrElse(() -> 0L);\n    assertTrue(numDVs > 0, \"Expected deletion vectors to be created, but none were found\");\n\n    // Read through V2 and verify deleted rows are filtered out (only odd ids remain).\n    long count = spark.sql(str(\"SELECT * FROM dsv2.delta.`%s`\", tablePath)).count();\n    // 500 odd numbers from 0-999: 1, 3, 5, ..., 999\n    assertTrue(count == 500, \"Expected 500 rows after DV filtering, got \" + count);\n  }\n\n  @Test\n  public void testPartitionedJoinEliminatesShuffle(@TempDir File tempDir) {\n    String tablePath = tempDir.getAbsolutePath();\n\n    // Create a partitioned Delta table via V1\n    spark.sql(\n        str(\n            \"CREATE TABLE delta.`%s` (id INT, data STRING, part INT) \"\n                + \"USING delta PARTITIONED BY (part)\",\n            tablePath));\n    spark.sql(\n        str(\n            \"INSERT INTO delta.`%s` VALUES (1, 'a', 1), (2, 'b', 1), (3, 'c', 2), (4, 'd', 3)\",\n            tablePath));\n\n    // Disable broadcast join so Spark must use shuffle or partition-aware join.\n    // Enable V2 bucketing so Spark recognizes KeyGroupedPartitioning from\n    // SupportsReportPartitioning (default is false in Spark 4.0, true in 4.1).\n    withSQLConf(\n        \"spark.sql.autoBroadcastJoinThreshold\",\n        \"-1\",\n        () ->\n            withSQLConf(\n                \"spark.sql.sources.v2.bucketing.enabled\",\n                \"true\",\n                () -> {\n                  // Self-join on partition column via DSv2 catalog\n                  String joinQuery =\n                      str(\n                          \"SELECT a.id, b.data FROM dsv2.delta.`%s` a \"\n                              + \"JOIN dsv2.delta.`%s` b ON a.part = b.part\",\n                          tablePath, tablePath);\n\n                  String explainOutput =\n                      spark.sql(joinQuery).queryExecution().executedPlan().toString();\n\n                  // With SupportsReportPartitioning + HasPartitionKey, Spark should recognize\n                  // both sides are KeyGroupedPartitioned on 'part' and skip the shuffle\n                  // (no Exchange node)\n                  assertFalse(\n                      explainOutput.contains(\"Exchange\"),\n                      \"Expected no Exchange (shuffle) in plan for partition-aligned join, \"\n                          + \"but found one.\\nPlan:\\n\"\n                          + explainOutput);\n                }));\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/V2StreamingReadTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\nimport java.util.stream.LongStream;\nimport org.apache.spark.sql.*;\nimport org.apache.spark.sql.catalyst.expressions.Expression;\nimport org.apache.spark.sql.catalyst.expressions.Literal$;\nimport org.apache.spark.sql.delta.DeltaIllegalStateException;\nimport org.apache.spark.sql.delta.DeltaLog;\nimport org.apache.spark.sql.delta.stats.StatisticsCollection;\nimport org.apache.spark.sql.streaming.StreamingQuery;\nimport org.apache.spark.sql.streaming.StreamingQueryException;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.io.TempDir;\nimport scala.Option;\nimport scala.collection.JavaConverters;\n\n/** Tests for V2 streaming read operations. */\npublic class V2StreamingReadTest extends V2TestBase {\n\n  @Test\n  public void testStreamingReadWithStartingVersion(@TempDir File deltaTablePath) throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Write version 0\n    spark\n        .createDataFrame(Arrays.asList(RowFactory.create(1, \"Alice\", 100.0)), TEST_SCHEMA)\n        .write()\n        .format(\"delta\")\n        .save(tablePath);\n\n    // Write version 1\n    spark\n        .createDataFrame(Arrays.asList(RowFactory.create(2, \"Bob\", 200.0)), TEST_SCHEMA)\n        .write()\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tablePath);\n\n    // Write version 2\n    spark\n        .createDataFrame(Arrays.asList(RowFactory.create(3, \"Charlie\", 300.0)), TEST_SCHEMA)\n        .write()\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tablePath);\n\n    // Start streaming from version 0 - should read all three versions\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n    Dataset<Row> streamingDF =\n        spark.readStream().option(\"startingVersion\", \"1\").table(dsv2TableRef);\n    assertTrue(streamingDF.isStreaming(), \"Dataset should be streaming\");\n\n    // Process all batches - should have all data from versions 0, 1, and 2\n    List<Row> actualRows = processStreamingQuery(streamingDF, \"test_with_starting_version\");\n    List<Row> expectedRows =\n        Arrays.asList(RowFactory.create(2, \"Bob\", 200.0), RowFactory.create(3, \"Charlie\", 300.0));\n\n    assertDataEquals(actualRows, expectedRows);\n  }\n\n  @Test\n  public void testStreamingRead(@TempDir File deltaTablePath) throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Write version 0\n    spark\n        .createDataFrame(Arrays.asList(RowFactory.create(1, \"Alice\", 100.0)), TEST_SCHEMA)\n        .write()\n        .format(\"delta\")\n        .save(tablePath);\n\n    // Write version 1\n    spark\n        .createDataFrame(Arrays.asList(RowFactory.create(2, \"Bob\", 200.0)), TEST_SCHEMA)\n        .write()\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tablePath);\n\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n    assertTrue(streamingDF.isStreaming(), \"Dataset should be streaming\");\n\n    List<Row> actualRows = processStreamingQuery(streamingDF, \"test_streaming_read\");\n    List<Row> expectedRows =\n        Arrays.asList(RowFactory.create(1, \"Alice\", 100.0), RowFactory.create(2, \"Bob\", 200.0));\n\n    assertDataEquals(actualRows, expectedRows);\n  }\n\n  /**\n   * Tests that streaming read after stats recompute does not produce duplicate rows.\n   *\n   * <p>StatisticsCollection.recompute re-adds files with updated stats (dataChange=false), creating\n   * duplicate AddFile entries in the log. The initial snapshot scan must use the selection vector\n   * to filter out stale entries.\n   */\n  @Test\n  public void testStreamingReadAfterStatsRecompute(@TempDir File deltaTablePath) throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Write data with stats collection disabled - files will have no stats\n    withSQLConf(\n        \"spark.databricks.delta.stats.collect\",\n        \"false\",\n        () ->\n            spark\n                .range(10)\n                .selectExpr(\"id\", \"cast(id as string) as value\")\n                .write()\n                .format(\"delta\")\n                .save(tablePath));\n\n    // Recompute statistics - this re-adds files with updated stats (dataChange=false),\n    // creating duplicate AddFile entries in the log that must be filtered by selection vector\n    DeltaLog deltaLog = DeltaLog.forTable(spark, tablePath);\n    StatisticsCollection.recompute(\n        spark,\n        deltaLog,\n        Option.empty(),\n        JavaConverters.<Expression>asScalaBuffer(\n                new ArrayList<>(List.of((Expression) Literal$.MODULE$.apply(true))))\n            .toList(),\n        af -> (Object) Boolean.TRUE);\n\n    // Stream via V2 - should see each row exactly once, not duplicated\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n\n    List<Row> actualRows = processStreamingQuery(streamingDF, \"test_stats_recompute\");\n\n    List<Row> expectedRows =\n        LongStream.range(0, 10)\n            .mapToObj(i -> RowFactory.create(i, String.valueOf(i)))\n            .collect(Collectors.toList());\n\n    assertDataEquals(actualRows, expectedRows);\n  }\n\n  /**\n   * Tests that streaming read correctly handles multiple deletion vectors on the same file.\n   *\n   * <p>When multiple DELETE operations are applied to the same file, each creates/updates a\n   * deletion vector. The streaming initial snapshot should correctly apply the cumulative DV and\n   * only return non-deleted rows.\n   */\n  @Test\n  public void testStreamingReadWithMultipleDeletionVectors(@TempDir File deltaTablePath)\n      throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Create table with deletion vectors enabled\n    spark.sql(\n        str(\n            \"CREATE TABLE delta.`%s` (value INT) USING delta \"\n                + \"TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\",\n            tablePath));\n\n    // Write 10 rows (0-9) in a single file\n    spark\n        .range(10)\n        .selectExpr(\"cast(id as int) as value\")\n        .coalesce(1)\n        .write()\n        .format(\"delta\")\n        .mode(\"append\")\n        .save(tablePath);\n\n    // Delete rows 0, 1, 2 - each creates/updates a DV on the same file\n    spark.sql(str(\"DELETE FROM delta.`%s` WHERE value = 0\", tablePath));\n    spark.sql(str(\"DELETE FROM delta.`%s` WHERE value = 1\", tablePath));\n    spark.sql(str(\"DELETE FROM delta.`%s` WHERE value = 2\", tablePath));\n\n    // Verify DVs were created\n    DeltaLog deltaLog = DeltaLog.forTable(spark, tablePath);\n    long numDVs =\n        (long)\n            deltaLog\n                .update(false, Option.empty(), Option.empty())\n                .numDeletionVectorsOpt()\n                .getOrElse(() -> 0L);\n    assertTrue(numDVs > 0, \"Expected deletion vectors to be created\");\n\n    // Stream via V2 - should see rows 3-9 only (rows 0, 1, 2 deleted)\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n\n    List<Row> actualRows = processStreamingQuery(streamingDF, \"test_multiple_dvs\");\n\n    List<Row> expectedRows =\n        Arrays.asList(\n            RowFactory.create(3),\n            RowFactory.create(4),\n            RowFactory.create(5),\n            RowFactory.create(6),\n            RowFactory.create(7),\n            RowFactory.create(8),\n            RowFactory.create(9));\n\n    assertDataEquals(actualRows, expectedRows);\n  }\n\n  /**\n   * Verifies that stopping a V2 streaming query does not surface an exception.\n   *\n   * <p>When Spark stops a streaming query it calls {@link Thread#interrupt()} on the micro-batch\n   * thread. If that thread is blocked inside Kernel's {@code DefaultJsonHandler} reading delta log\n   * JSON files via NIO channels, the interrupt causes a {@link\n   * java.nio.channels.ClosedByInterruptException} wrapped in a {@code KernelEngineException}. The\n   * fix in {@code SparkMicroBatchStream.latestOffset()} and {@code\n   * SparkMicroBatchStream.planInputPartitions()} re-wraps this as an {@link\n   * java.io.UncheckedIOException} so Spark's {@code isInterruptedByStop} recognizes it as a clean\n   * shutdown.\n   */\n  @Test\n  public void testStreamingQueryStopDoesNotSurfaceException(@TempDir File deltaTablePath)\n      throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n\n    // Write data\n    spark\n        .createDataFrame(Arrays.asList(RowFactory.create(1, \"Alice\", 100.0)), TEST_SCHEMA)\n        .write()\n        .format(\"delta\")\n        .save(tablePath);\n\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n\n    StreamingQuery query =\n        streamingDF\n            .writeStream()\n            .format(\"memory\")\n            .queryName(\"test_stop_no_exception\")\n            .outputMode(\"append\")\n            .start();\n\n    // Continuously write new commits so latestOffset() keeps reading fresh delta log JSON files\n    // via NIO. This ensures Thread.interrupt() from stop() is likely to arrive while a channel\n    // is open inside DefaultJsonHandler.hasNext(), directly exercising the fix.\n    ExecutorService writer = Executors.newSingleThreadExecutor();\n    try {\n      writer.submit(\n          () -> {\n            for (int i = 0; i < 100; i++) {\n              try {\n                spark\n                    .createDataFrame(\n                        Arrays.asList(RowFactory.create(i + 2, \"User\" + i, (double) i * 10)),\n                        TEST_SCHEMA)\n                    .write()\n                    .format(\"delta\")\n                    .mode(\"append\")\n                    .save(tablePath);\n                Thread.sleep(20);\n              } catch (Exception ignored) {\n                return;\n              }\n            }\n          });\n\n      // Let the query process a few batches with active NIO reads before stopping.\n      Thread.sleep(300);\n      query.stop();\n    } finally {\n      writer.shutdownNow();\n      writer.awaitTermination(5, TimeUnit.SECONDS);\n    }\n\n    // Release cached DeltaLog references so @TempDir cleanup can delete the directory.\n    DeltaLog.clearCache();\n\n    // The stop should be clean — no exception should have been captured\n    assertTrue(\n        query.exception().isEmpty(),\n        () ->\n            \"Expected no exception after query.stop(), but got: \"\n                + query.exception().get().toString());\n  }\n\n  // TODO(#5319): The V1 source does not detect nested field additions on restart. Port this\n  //  validation to V1 for parity.\n  @Test\n  public void testNestedColumnAdditionDetectedOnRestart(@TempDir File deltaTablePath)\n      throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n\n    // Create table with struct column: data STRUCT<x: INT>\n    spark.sql(\n        str(\"CREATE TABLE delta.`%s` (id STRING, data STRUCT<x: INT>) USING delta\", tablePath));\n    spark.sql(str(\"INSERT INTO delta.`%s` VALUES ('0', named_struct('x', 1))\", tablePath));\n\n    // Start streaming and process initial data\n    File checkpointDir = new File(deltaTablePath, \"_checkpoint\");\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n    StreamingQuery query =\n        streamingDF\n            .writeStream()\n            .format(\"noop\")\n            .option(\"checkpointLocation\", checkpointDir.getAbsolutePath())\n            .start();\n    query.processAllAvailable();\n    query.stop();\n\n    // Evolve struct via ALTER TABLE (metadata-only, no file deletion):\n    // add nested field y -> data STRUCT<x: INT, y: INT>\n    spark.sql(str(\"ALTER TABLE delta.`%s` ADD COLUMNS (data.y INT)\", tablePath));\n\n    // Restart with stale DataFrame — should fail with schema mismatch\n    StreamingQueryException ex =\n        assertThrows(\n            StreamingQueryException.class,\n            () -> {\n              StreamingQuery q =\n                  streamingDF\n                      .writeStream()\n                      .format(\"noop\")\n                      .option(\"checkpointLocation\", checkpointDir.getAbsolutePath())\n                      .start();\n              try {\n                q.processAllAvailable();\n              } finally {\n                q.stop();\n              }\n            });\n    assertInstanceOf(DeltaIllegalStateException.class, ex.cause());\n    assertTrue(\n        ex.getMessage().contains(\"DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART\"),\n        \"Expected DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART but got: \" + ex.getMessage());\n  }\n\n  // TODO(#6232): v2 source cannot adopt type widening schema change without refreshing the\n  //  dataframe due to the lack of support in spark stream engine. Throw an error at stream\n  //  start time to instruct user.\n  @Test\n  public void testNestedTypeWideningDetectedOnRestart(@TempDir File deltaTablePath)\n      throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n\n    // Create table with struct column: data STRUCT<x: INT>\n    spark.sql(\n        str(\"CREATE TABLE delta.`%s` (id STRING, data STRUCT<x: INT>) USING delta\", tablePath));\n    spark.sql(str(\"INSERT INTO delta.`%s` VALUES ('0', named_struct('x', 1))\", tablePath));\n\n    // Start streaming and process initial data\n    File checkpointDir = new File(deltaTablePath, \"_checkpoint\");\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n    StreamingQuery query =\n        streamingDF\n            .writeStream()\n            .format(\"noop\")\n            .option(\"checkpointLocation\", checkpointDir.getAbsolutePath())\n            .start();\n    query.processAllAvailable();\n    query.stop();\n\n    // Widen nested field type via ALTER TABLE (metadata-only, no file deletion):\n    // data.x INT -> data.x BIGINT\n    spark.sql(\n        str(\n            \"ALTER TABLE delta.`%s` SET TBLPROPERTIES ('delta.enableTypeWidening' = 'true')\",\n            tablePath));\n    spark.sql(str(\"ALTER TABLE delta.`%s` ALTER COLUMN data.x TYPE BIGINT\", tablePath));\n\n    // Restart with stale DataFrame — should fail with schema mismatch\n    StreamingQueryException ex =\n        assertThrows(\n            StreamingQueryException.class,\n            () -> {\n              StreamingQuery q =\n                  streamingDF\n                      .writeStream()\n                      .format(\"noop\")\n                      .option(\"checkpointLocation\", checkpointDir.getAbsolutePath())\n                      .start();\n              try {\n                q.processAllAvailable();\n              } finally {\n                q.stop();\n              }\n            });\n    assertInstanceOf(DeltaIllegalStateException.class, ex.cause());\n    assertTrue(\n        ex.getMessage().contains(\"DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART\"),\n        \"Expected DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART but got: \" + ex.getMessage());\n  }\n\n  // TODO(#6232): v2 source cannot adopt type widening schema change without refreshing the\n  //  dataframe due to the lack of support in spark stream engine. Throw an error at stream\n  //  start time to instruct user.\n  @Test\n  public void testNestedNullabilityRelaxDetectedOnRestart(@TempDir File deltaTablePath)\n      throws Exception {\n    String tablePath = deltaTablePath.getAbsolutePath();\n    String dsv2TableRef = str(\"dsv2.delta.`%s`\", tablePath);\n\n    // Create table via SQL DDL to preserve the NOT NULL constraint on the nested field.\n    // DataFrame writes go through ImplicitMetadataOperation which calls schema.asNullable,\n    // forcing all fields (including nested ones) to nullable — losing the NOT NULL.\n    spark.sql(\n        str(\n            \"CREATE TABLE delta.`%s` (id STRING, data STRUCT<x: INT NOT NULL>) USING delta\",\n            tablePath));\n    spark.sql(str(\"INSERT INTO delta.`%s` VALUES ('0', named_struct('x', 1))\", tablePath));\n\n    // Start streaming and process initial data\n    File checkpointDir = new File(deltaTablePath, \"_checkpoint\");\n    Dataset<Row> streamingDF = spark.readStream().table(dsv2TableRef);\n    StreamingQuery query =\n        streamingDF\n            .writeStream()\n            .format(\"noop\")\n            .option(\"checkpointLocation\", checkpointDir.getAbsolutePath())\n            .start();\n    query.processAllAvailable();\n    query.stop();\n\n    // Relax nullability via ALTER TABLE (metadata-only, no file deletion):\n    // data.x NOT NULL -> data.x nullable\n    spark.sql(str(\"ALTER TABLE delta.`%s` ALTER COLUMN data.x DROP NOT NULL\", tablePath));\n\n    // Restart with stale DataFrame — should fail with schema mismatch\n    StreamingQueryException ex =\n        assertThrows(\n            StreamingQueryException.class,\n            () -> {\n              StreamingQuery q =\n                  streamingDF\n                      .writeStream()\n                      .format(\"noop\")\n                      .option(\"checkpointLocation\", checkpointDir.getAbsolutePath())\n                      .start();\n              try {\n                q.processAllAvailable();\n              } finally {\n                q.stop();\n              }\n            });\n    assertInstanceOf(DeltaIllegalStateException.class, ex.cause());\n    assertTrue(\n        ex.getMessage().contains(\"DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART\"),\n        \"Expected DELTA_STREAMING_SCHEMA_MISMATCH_ON_RESTART but got: \" + ex.getMessage());\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/V2TestBase.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\nimport org.apache.spark.SparkConf;\nimport org.apache.spark.sql.*;\nimport org.apache.spark.sql.streaming.StreamingQuery;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.io.TempDir;\n\n/**\n * Base class for V2 tests with common SparkSession setup and helper methods.\n *\n * <p>This base class configures the Spark session with the V2 catalog and provides utility methods\n * for assertions. The V1 catalog is still used for write operations because this is currently\n * necessary until V2 supports write operations.\n */\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic abstract class V2TestBase {\n\n  protected SparkSession spark;\n  protected String nameSpace;\n\n  protected static final StructType TEST_SCHEMA =\n      DataTypes.createStructType(\n          Arrays.asList(\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, false),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, false),\n              DataTypes.createStructField(\"value\", DataTypes.DoubleType, false)));\n\n  @BeforeAll\n  public void setUp(@TempDir File tempDir) {\n    // Spark doesn't allow '-'\n    nameSpace = \"ns_\" + UUID.randomUUID().toString().replace('-', '_');\n    SparkConf conf =\n        new SparkConf()\n            .set(\"spark.sql.catalog.dsv2\", \"io.delta.spark.internal.v2.catalog.TestCatalog\")\n            .set(\"spark.sql.catalog.dsv2.base_path\", tempDir.getAbsolutePath())\n            .set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtensionV1\")\n            .set(\n                \"spark.sql.catalog.spark_catalog\",\n                \"org.apache.spark.sql.delta.catalog.DeltaCatalogV1\")\n            .setMaster(\"local[*]\")\n            .setAppName(getClass().getSimpleName());\n    spark = SparkSession.builder().config(conf).getOrCreate();\n  }\n\n  @AfterAll\n  public void tearDown() {\n    if (spark != null) {\n      spark.stop();\n    }\n  }\n\n  /**\n   * Builds a formatted string by substituting placeholders with the provided arguments. Useful for\n   * constructing SQL queries and table identifiers.\n   */\n  protected static String str(String template, Object... args) {\n    return String.format(template, args);\n  }\n\n  /**\n   * Executes a SQL query and verifies the result matches the expected rows.\n   *\n   * @param sql the SQL query to execute\n   * @param expectedRows the expected rows as a list of lists (each inner list is a row)\n   */\n  protected void check(String sql, List<List<Object>> expectedRows) {\n    Dataset<Row> result = spark.sql(sql);\n    List<Row> actualRows = result.collectAsList();\n    List<Row> expected =\n        expectedRows.stream()\n            .map(row -> RowFactory.create(row.toArray()))\n            .collect(java.util.stream.Collectors.toList());\n    assertEquals(\n        expected,\n        actualRows,\n        () -> \"Query: \" + sql + \"\\nExpected: \" + expected + \"\\nActual: \" + actualRows);\n  }\n\n  /** Creates a row representation as a list of values. */\n  protected static List<Object> row(Object... values) {\n    return Arrays.asList(values);\n  }\n\n  /** Asserts that a dataset equals the expected rows. */\n  protected void assertDatasetEquals(Dataset<Row> actual, List<Row> expectedRows) {\n    List<Row> actualRows = actual.collectAsList();\n    assertEquals(\n        expectedRows,\n        actualRows,\n        () -> \"Datasets differ: expected=\" + expectedRows + \"\\nactual=\" + actualRows);\n  }\n\n  /**\n   * Processes a streaming query and returns the collected rows.\n   *\n   * @param streamingDF the streaming DataFrame to process\n   * @param queryName the name for the memory sink query\n   * @return the list of rows collected from the stream\n   * @throws Exception if the streaming query fails\n   */\n  protected List<Row> processStreamingQuery(Dataset<Row> streamingDF, String queryName)\n      throws Exception {\n    StreamingQuery query = null;\n    try {\n      query =\n          streamingDF\n              .writeStream()\n              .format(\"memory\")\n              .queryName(queryName)\n              .outputMode(\"append\")\n              .start();\n\n      query.processAllAvailable();\n\n      // Query the memory sink to get results\n      Dataset<Row> results = spark.sql(\"SELECT * FROM \" + queryName);\n      return results.collectAsList();\n    } finally {\n      if (query != null) {\n        query.stop();\n      }\n    }\n  }\n\n  /**\n   * Runs the given action with a Spark SQL configuration temporarily set, then restores the\n   * original value afterwards (similar to Scala's {@code withSQLConf}).\n   */\n  protected void withSQLConf(String key, String value, Runnable action) {\n    scala.Option<String> original = spark.conf().getOption(key);\n    spark.conf().set(key, value);\n    try {\n      action.run();\n    } finally {\n      if (original.isDefined()) {\n        spark.conf().set(key, original.get());\n      } else {\n        spark.conf().unset(key);\n      }\n    }\n  }\n\n  /**\n   * Asserts that rows equal the expected rows (order-independent).\n   *\n   * @param actualRows the actual rows\n   * @param expectedRows the expected rows\n   */\n  protected void assertDataEquals(List<Row> actualRows, List<Row> expectedRows) {\n    assertEquals(\n        expectedRows.size(),\n        actualRows.size(),\n        () ->\n            \"Row count differs: expected=\"\n                + expectedRows.size()\n                + \" actual=\"\n                + actualRows.size()\n                + \"\\nExpected rows: \"\n                + expectedRows\n                + \"\\nActual rows: \"\n                + actualRows);\n\n    // Compare rows (order-independent for robustness)\n    assertTrue(\n        actualRows.containsAll(expectedRows) && expectedRows.containsAll(actualRows),\n        () -> \"Data differs:\\nExpected: \" + expectedRows + \"\\nActual: \" + actualRows);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/catalog/SparkTableTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.catalog;\n\nimport static org.apache.spark.sql.connector.catalog.TableCapability.BATCH_READ;\nimport static org.apache.spark.sql.connector.catalog.TableCapability.BATCH_WRITE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport java.io.File;\nimport java.lang.reflect.Method;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.BiFunction;\nimport java.util.stream.Stream;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.catalyst.TableIdentifier;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.connector.catalog.Column;\nimport org.apache.spark.sql.connector.catalog.Identifier;\nimport org.apache.spark.sql.connector.catalog.SupportsWrite;\nimport org.apache.spark.sql.connector.expressions.Transform;\nimport org.apache.spark.sql.connector.write.LogicalWriteInfo;\nimport org.apache.spark.sql.delta.catalog.DeltaTableV2;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport scala.Option;\n\npublic class SparkTableTest extends DeltaV2TestBase {\n\n  @ParameterizedTest(name = \"{0} - {1}\")\n  @MethodSource(\"tableTestCases\")\n  public void testDeltaKernelTable(\n      TableTestCase testCase, ConstructionMethod method, @TempDir File tempDir) throws Exception {\n    String path = tempDir.getAbsolutePath();\n    String tableName =\n        \"test_\" + testCase.name.toLowerCase().replace(\" \", \"_\") + \"_\" + method.name().toLowerCase();\n    testCase.createTableSql.apply(tableName, path);\n    Identifier identifier = Identifier.of(new String[] {\"default\"}, tableName);\n\n    // Create SparkTable based on construction method\n    SparkTable kernelTable;\n    CatalogTable catalogTable = null;\n\n    switch (method) {\n      case FROM_PATH:\n        kernelTable = new SparkTable(identifier, path);\n        break;\n      case FROM_CATALOG_TABLE:\n        catalogTable =\n            spark.sessionState().catalog().getTableMetadata(new TableIdentifier(tableName));\n        kernelTable = new SparkTable(identifier, catalogTable, Collections.emptyMap());\n        break;\n      default:\n        throw new IllegalArgumentException(\"Unknown construction method: \" + method);\n    }\n\n    // ===== Test table name =====\n    String expectedName;\n    switch (method) {\n      case FROM_PATH:\n        expectedName = \"delta.`\" + path + \"`\";\n        break;\n      case FROM_CATALOG_TABLE:\n        // Catalog table should return fully qualified name: spark_catalog.default.tableName\n        expectedName = \"spark_catalog.default.\" + tableName;\n        break;\n      default:\n        throw new IllegalArgumentException(\"Unknown method: \" + method);\n    }\n    assertEquals(expectedName, kernelTable.name());\n\n    // ===== Test schema =====\n    StructType sparkSchema = kernelTable.schema();\n    Column[] actualColumns = kernelTable.columns();\n    assertEquals(testCase.expectedColumns.size(), sparkSchema.fields().length);\n    for (int i = 0; i < testCase.expectedColumns.size(); i++) {\n      Column expectedCol = testCase.expectedColumns.get(i);\n      assertEquals(\n          expectedCol.name(),\n          sparkSchema.fields()[i].name(),\n          \"Column name mismatch at position \" + i);\n      assertEquals(\n          expectedCol.dataType(),\n          sparkSchema.fields()[i].dataType(),\n          \"Data type mismatch for column: \" + expectedCol.name());\n      // Check column object from table.columns()\n      assertEquals(expectedCol, actualColumns[i], \"Column mismatch at position \" + i);\n    }\n\n    // ===== Verify schema consistency with DeltaTableV2 =====\n    // This ensures SparkTable (Kernel-based) returns the same schema as DeltaTableV2 (V1-based)\n    // Both should properly remove internal Delta metadata (e.g., column mapping physical names)\n    DeltaTableV2 deltaTableV2;\n    switch (method) {\n      case FROM_PATH:\n        deltaTableV2 =\n            DeltaTableV2.apply(\n                spark,\n                new Path(path),\n                Option.empty(),\n                Option.empty(),\n                scala.collection.immutable.Map$.MODULE$.empty(),\n                Option.empty());\n        break;\n      case FROM_CATALOG_TABLE:\n        deltaTableV2 =\n            DeltaTableV2.apply(\n                spark,\n                new Path(path),\n                Option.apply(catalogTable),\n                Option.apply(tableName),\n                scala.collection.immutable.Map$.MODULE$.empty(),\n                Option.empty());\n        break;\n      default:\n        throw new IllegalArgumentException(\"Unknown method: \" + method);\n    }\n\n    // Verify schemas are equal (including field names, types, and metadata)\n    assertEquals(\n        deltaTableV2.schema(),\n        sparkSchema,\n        \"SparkTable schema should match DeltaTableV2 schema for test case: \" + testCase.name);\n\n    // ===== Test partitioning =====\n    Transform[] partitioning = kernelTable.partitioning();\n    assertEquals(testCase.expectedPartitionColumns.length, partitioning.length);\n    for (int i = 0; i < testCase.expectedPartitionColumns.length; i++) {\n      assertEquals(\n          testCase.expectedPartitionColumns[i],\n          partitioning[i].references()[0].describe(),\n          \"Partition column mismatch at position \" + i);\n    }\n\n    // ===== Test properties =====\n    Map<String, String> properties = kernelTable.properties();\n    testCase.expectedProperties.forEach(\n        (key, value) -> {\n          assertTrue(properties.containsKey(key), \"Property not found: \" + key);\n          assertEquals(value, properties.get(key), \"Property value mismatch for: \" + key);\n        });\n\n    // ===== Test capabilities =====\n    assertTrue(kernelTable.capabilities().contains(BATCH_READ));\n    assertTrue(kernelTable.capabilities().contains(BATCH_WRITE));\n    assertTrue(kernelTable instanceof SupportsWrite);\n\n    // ===== Test getCatalogTable based on construction method =====\n    Optional<CatalogTable> retrievedCatalogTable = kernelTable.getCatalogTable();\n    switch (method) {\n      case FROM_PATH:\n        assertFalse(\n            retrievedCatalogTable.isPresent(),\n            \"Path-based SparkTable should not have catalog table\");\n        break;\n      case FROM_CATALOG_TABLE:\n        assertTrue(\n            retrievedCatalogTable.isPresent(),\n            \"CatalogTable-based SparkTable should have catalog table\");\n        assertEquals(\n            catalogTable,\n            retrievedCatalogTable.get(),\n            \"Retrieved catalog table should match the original\");\n        break;\n    }\n\n    // ===== Test getTablePath returns Path from tablePath =====\n    Path retrievedPath = kernelTable.getTablePath();\n    assertEquals(new Path(path), retrievedPath, \"getTablePath should return Path from tablePath\");\n  }\n\n  /** Enum to represent different construction methods for SparkTable */\n  enum ConstructionMethod {\n    FROM_PATH(\"Path\"),\n    FROM_CATALOG_TABLE(\"CatalogTable\");\n\n    private final String displayName;\n\n    ConstructionMethod(String displayName) {\n      this.displayName = displayName;\n    }\n\n    @Override\n    public String toString() {\n      return displayName;\n    }\n  }\n\n  /** Represents a test case configuration for Delta tables */\n  private static class TableTestCase {\n    final String name;\n    final BiFunction<String, String, Void> createTableSql;\n    final List<Column> expectedColumns;\n    final String[] expectedPartitionColumns;\n    final Map<String, String> expectedProperties;\n\n    public TableTestCase(\n        String name,\n        BiFunction<String, String, Void> createTableSql,\n        List<Column> expectedColumns,\n        String[] expectedPartitionColumns,\n        Map<String, String> expectedProperties) {\n\n      this.name = name;\n      this.createTableSql = createTableSql;\n      this.expectedColumns = expectedColumns;\n      this.expectedPartitionColumns = expectedPartitionColumns;\n      this.expectedProperties = expectedProperties;\n    }\n\n    @Override\n    public String toString() {\n      return name;\n    }\n  }\n\n  /** Provides different test cases for Delta tables combined with construction methods */\n  static Stream<Arguments> tableTestCases() {\n\n    // ===== Partitioned Table =====\n    List<Column> partitionedTableColumns = new ArrayList<>();\n    partitionedTableColumns.add(Column.create(\"id\", DataTypes.IntegerType));\n    partitionedTableColumns.add(Column.create(\"data\", DataTypes.StringType));\n    partitionedTableColumns.add(Column.create(\"part\", DataTypes.IntegerType));\n\n    // ===== Unpartitioned Table =====\n    List<Column> unPartitionedTableColumns = new ArrayList<>();\n    unPartitionedTableColumns.add(Column.create(\"id\", DataTypes.IntegerType));\n    unPartitionedTableColumns.add(Column.create(\"data\", DataTypes.StringType));\n\n    // ===== Setup Single Properties =====\n    Map<String, String> basicProps = new HashMap<>();\n    basicProps.put(\"foo\", \"bar\");\n\n    // ===== Setup Multiple Properties =====\n    Map<String, String> multiProps = new HashMap<>();\n    multiProps.put(\"prop1\", \"value1\");\n    multiProps.put(\"prop2\", \"value2\");\n    multiProps.put(\"delta.enableChangeDataFeed\", \"true\");\n\n    List<Column> singleColumn = new ArrayList<>();\n    singleColumn.add(Column.create(\"id\", DataTypes.IntegerType));\n\n    // ===== Name Mapping Table =====\n    List<Column> nameMappingTableColumns = new ArrayList<>();\n    nameMappingTableColumns.add(Column.create(\"id\", DataTypes.IntegerType));\n    nameMappingTableColumns.add(Column.create(\"name\", DataTypes.StringType));\n    nameMappingTableColumns.add(Column.create(\"value\", DataTypes.DoubleType));\n\n    Map<String, String> nameMappingProps = new HashMap<>();\n    nameMappingProps.put(\"delta.columnMapping.mode\", \"name\");\n\n    List<TableTestCase> testCases =\n        Arrays.asList(\n            new TableTestCase(\n                \"Partitioned Table\",\n                (tableName, path) -> {\n                  spark.sql(\n                      String.format(\n                          \"CREATE TABLE %s (id INT, data STRING, part INT) USING delta \"\n                              + \"PARTITIONED BY (part) TBLPROPERTIES ('foo'='bar') LOCATION '%s'\",\n                          tableName, path));\n                  return null;\n                },\n                partitionedTableColumns,\n                new String[] {\"part\"},\n                basicProps),\n            new TableTestCase(\n                \"UnPartitioned Table\",\n                (tableName, path) -> {\n                  spark.sql(\n                      String.format(\n                          \"CREATE TABLE %s (id INT, data STRING) USING delta LOCATION '%s'\",\n                          tableName, path));\n                  return null;\n                },\n                unPartitionedTableColumns,\n                new String[] {},\n                new HashMap<>()),\n            new TableTestCase(\n                \"Multiple Properties\",\n                (tableName, path) -> {\n                  spark.sql(\n                      String.format(\n                          \"CREATE TABLE %s (id INT) USING delta \"\n                              + \"TBLPROPERTIES ('prop1'='value1', 'prop2'='value2', 'delta.enableChangeDataFeed'='true') \"\n                              + \"LOCATION '%s'\",\n                          tableName, path));\n                  return null;\n                },\n                singleColumn,\n                new String[] {},\n                multiProps),\n            new TableTestCase(\n                \"Name Mapping Table\",\n                (tableName, path) -> {\n                  spark.sql(\n                      String.format(\n                          \"CREATE TABLE %s (id INT, name STRING, value DOUBLE) USING delta \"\n                              + \"TBLPROPERTIES ('delta.columnMapping.mode'='name') \"\n                              + \"LOCATION '%s'\",\n                          tableName, path));\n                  spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'test', 100.0)\", tableName));\n                  return null;\n                },\n                nameMappingTableColumns,\n                new String[] {},\n                nameMappingProps));\n\n    // Create cartesian product of test cases and construction methods\n    return testCases.stream()\n        .flatMap(\n            testCase ->\n                Stream.of(ConstructionMethod.FROM_PATH, ConstructionMethod.FROM_CATALOG_TABLE)\n                    .map(method -> Arguments.of(testCase, method)));\n  }\n\n  /**\n   * Test that getDecodedPath handles various URI schemes correctly, not just file:// URIs. This\n   * verifies the fix for supporting cloud storage paths (s3, abfss, gs) and HDFS.\n   */\n  @ParameterizedTest(name = \"URI scheme: {0}\")\n  @MethodSource(\"uriSchemeTestCases\")\n  public void testGetDecodedPathSupportsVariousUriSchemes(String scheme, String uriString)\n      throws Exception {\n    // Access the private static method via reflection\n    Method getDecodedPath =\n        SparkTable.class.getDeclaredMethod(\"getDecodedPath\", java.net.URI.class);\n    getDecodedPath.setAccessible(true);\n\n    URI uri = new URI(uriString);\n    String result = (String) getDecodedPath.invoke(null, uri);\n\n    // Verify the path is decoded correctly\n    // The result should contain the path portion without URL encoding issues\n    assertTrue(\n        result.contains(\"/path/to/table\"),\n        \"Decoded path should contain the expected path. Got: \" + result);\n  }\n\n  /** Test that URL-encoded characters are properly decoded */\n  @Test\n  public void testGetDecodedPathDecodesUrlEncodedCharacters() throws Exception {\n    // Access the private static method via reflection\n    Method getDecodedPath =\n        SparkTable.class.getDeclaredMethod(\"getDecodedPath\", java.net.URI.class);\n    getDecodedPath.setAccessible(true);\n\n    // Test URL-encoded path: \"spark%25dir%25prefix\" should decode to \"spark%dir%prefix\"\n    // %25 is the URL encoding for %\n    URI uri = new URI(\"file:///data/spark%25dir%25prefix/table\");\n    String result = (String) getDecodedPath.invoke(null, uri);\n\n    // For file URIs, getDecodedPath returns just the path without the scheme\n    assertEquals(\n        \"/data/spark%dir%prefix/table\",\n        result, \"URL-encoded characters should be properly decoded\");\n  }\n\n  /** Provides test cases for different URI schemes */\n  static Stream<Arguments> uriSchemeTestCases() {\n    return Stream.of(\n        Arguments.of(\"file\", \"file:///path/to/table\"),\n        Arguments.of(\"s3\", \"s3://bucket/path/to/table\"),\n        Arguments.of(\"s3a\", \"s3a://bucket/path/to/table\"),\n        Arguments.of(\"abfss\", \"abfss://container@account.dfs.core.windows.net/path/to/table\"),\n        Arguments.of(\"gs\", \"gs://bucket/path/to/table\"),\n        Arguments.of(\"hdfs\", \"hdfs://namenode:8020/path/to/table\"));\n  }\n\n  @Test\n  public void testEqualsAndHashCode(@TempDir File tempDir) {\n    String path = tempDir.getAbsolutePath();\n    spark.sql(String.format(\"CREATE TABLE test_equals (id INT) USING delta LOCATION '%s'\", path));\n\n    Identifier identifier = Identifier.of(new String[] {\"default\"}, \"test_equals\");\n    Map<String, String> options = Collections.singletonMap(\"key\", \"value\");\n\n    SparkTable table1 = new SparkTable(identifier, path, options);\n    SparkTable table2 = new SparkTable(identifier, path, options);\n    SparkTable table3 = new SparkTable(identifier, path, Collections.emptyMap());\n\n    // Same identifier, path, and options should be equal\n    assertEquals(table1, table2);\n    assertEquals(table1.hashCode(), table2.hashCode());\n\n    // Different options should not be equal and hashCodes should differ\n    assertNotEquals(table1, table3);\n    assertNotEquals(table1.hashCode(), table3.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithCatalogTable(@TempDir File tempDir) throws Exception {\n    String path1 = new File(tempDir, \"table1\").getAbsolutePath();\n    String path2 = new File(tempDir, \"table2\").getAbsolutePath();\n    spark.sql(\n        String.format(\"CREATE TABLE test_catalog1 (id INT) USING delta LOCATION '%s'\", path1));\n    spark.sql(\n        String.format(\"CREATE TABLE test_catalog2 (id INT) USING delta LOCATION '%s'\", path2));\n\n    Identifier identifier = Identifier.of(new String[] {\"default\"}, \"test_catalog\");\n\n    // Create table1 and table2 with separately fetched CatalogTable objects (not same instance)\n    SparkTable table1 =\n        new SparkTable(\n            identifier,\n            spark.sessionState().catalog().getTableMetadata(new TableIdentifier(\"test_catalog1\")),\n            Collections.emptyMap());\n    SparkTable table2 =\n        new SparkTable(\n            identifier,\n            spark.sessionState().catalog().getTableMetadata(new TableIdentifier(\"test_catalog1\")),\n            Collections.emptyMap());\n\n    // Same identifier, catalogTable, and options should be equal\n    assertEquals(table1, table2);\n    assertEquals(table1.hashCode(), table2.hashCode());\n\n    // Different catalogTable should not be equal\n    SparkTable table3 =\n        new SparkTable(\n            identifier,\n            spark.sessionState().catalog().getTableMetadata(new TableIdentifier(\"test_catalog2\")),\n            Collections.emptyMap());\n    assertNotEquals(table1, table3);\n    assertNotEquals(table1.hashCode(), table3.hashCode());\n\n    // Path-based table (no catalogTable) should not equal catalog-based table\n    SparkTable table4 = new SparkTable(identifier, path1, Collections.emptyMap());\n    assertNotEquals(table1, table4);\n    assertNotEquals(table1.hashCode(), table4.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithDifferentSnapshotVersions(@TempDir File tempDir) {\n    String path = tempDir.getAbsolutePath();\n    spark.sql(String.format(\"CREATE TABLE test_snapshot (id INT) USING delta LOCATION '%s'\", path));\n\n    Identifier identifier = Identifier.of(new String[] {\"default\"}, \"test_snapshot\");\n\n    // Create first SparkTable instance at version 0\n    SparkTable table1 = new SparkTable(identifier, path);\n\n    // Modify the table to create a new version\n    spark.sql(\"INSERT INTO test_snapshot VALUES (1)\");\n\n    // Create second SparkTable instance at version 1\n    SparkTable table2 = new SparkTable(identifier, path);\n\n    // Same identifier and path but different snapshot versions should not be equal\n    assertNotEquals(\n        table1,\n        table2,\n        \"SparkTable instances with different snapshot versions should not be equal\");\n    assertNotEquals(\n        table1.hashCode(),\n        table2.hashCode(),\n        \"Hash codes should differ for different snapshot versions\");\n  }\n\n  @Test\n  public void testNewWriteBuilderThrowsUnsupported(@TempDir File tempDir) throws Exception {\n    String path = tempDir.getAbsolutePath();\n    spark.sql(\n        String.format(\n            \"CREATE TABLE test_write_builder_unsupported (id INT) USING delta LOCATION '%s'\",\n            path));\n\n    SparkTable table =\n        new SparkTable(\n            Identifier.of(new String[] {\"default\"}, \"test_write_builder_unsupported\"), path);\n    LogicalWriteInfo writeInfo =\n        new LogicalWriteInfo() {\n          @Override\n          public String queryId() {\n            return \"test-query-id\";\n          }\n\n          @Override\n          public StructType schema() {\n            return new StructType().add(\"id\", DataTypes.IntegerType);\n          }\n\n          @Override\n          public CaseInsensitiveStringMap options() {\n            return new CaseInsensitiveStringMap(Collections.emptyMap());\n          }\n        };\n\n    UnsupportedOperationException ex =\n        assertThrows(UnsupportedOperationException.class, () -> table.newWriteBuilder(writeInfo));\n    assertEquals(\n        \"Batch write for Delta tables via the DSv2 connector is not yet supported.\",\n        ex.getMessage());\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/catalog/TestCatalog.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.catalog;\n\nimport io.delta.kernel.Operation;\nimport io.delta.kernel.defaults.engine.DefaultEngine;\nimport io.delta.kernel.engine.Engine;\nimport io.delta.kernel.utils.CloseableIterable;\nimport io.delta.spark.internal.v2.utils.SchemaUtils;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.catalyst.analysis.NoSuchTableException;\nimport org.apache.spark.sql.connector.catalog.Identifier;\nimport org.apache.spark.sql.connector.catalog.Table;\nimport org.apache.spark.sql.connector.catalog.TableCatalog;\nimport org.apache.spark.sql.connector.catalog.TableChange;\nimport org.apache.spark.sql.connector.expressions.Transform;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\n\n/**\n * A {@link TableCatalog} implementation that uses Delta Kernel for table operations. This catalog\n * is used for facilitating testing for spark-dsv2 code path.\n *\n * <p>This catalog is initialized with a base path where all tables will be created. The catalog\n * maintains a mapping of table identifiers to their physical paths on the filesystem. When a table\n * is created, it gets a unique subdirectory under the base path to store its data.\n */\npublic class TestCatalog implements TableCatalog {\n\n  /** The name of this catalog instance, set during initialization. */\n  private String catalogName;\n\n  /**\n   * The base directory path where all tables created by this catalog will be stored. Each table\n   * gets a unique subdirectory under this path.\n   */\n  private String basePath;\n\n  // TODO: Support catalog owned commit.\n  private final Map<String, String> tablePaths = new ConcurrentHashMap<>();\n  private final Configuration hadoopConf = new Configuration();\n  private final Engine engine = DefaultEngine.create(hadoopConf);\n\n  @Override\n  public Identifier[] listTables(String[] namespace) {\n    throw new UnsupportedOperationException(\"listTables method is not implemented\");\n  }\n\n  @Override\n  public Table loadTable(Identifier ident) throws NoSuchTableException {\n    // Check if this is a path-based table identifier\n    String tablePath;\n    if (isPathIdentifier(ident)) {\n      tablePath = ident.name();\n    } else {\n      // Handle catalog-managed tables\n      String tableKey = getTableKey(ident);\n      tablePath = tablePaths.get(tableKey);\n    }\n    if (tablePath == null) {\n      throw new NoSuchTableException(ident);\n    }\n    try {\n      return new SparkTable(ident, tablePath);\n    } catch (Exception e) {\n      throw new RuntimeException(\"Failed to load table: \" + ident, e);\n    }\n  }\n\n  @Override\n  public Table createTable(\n      Identifier ident, StructType schema, Transform[] partitions, Map<String, String> properties) {\n    String tableKey = getTableKey(ident);\n    String tablePath = basePath + UUID.randomUUID() + \"/\";\n    tablePaths.put(tableKey, tablePath);\n    try {\n      // TODO: migrate to use CCv2 table\n      io.delta.kernel.Table kernelTable = io.delta.kernel.Table.forPath(engine, tablePath);\n      List<String> partitionColumns = new ArrayList<>();\n      for (Transform partition : partitions) {\n        // Extract column name from partition transform\n        String columnName = partition.references()[0].describe();\n        partitionColumns.add(columnName);\n      }\n\n      // TODO: migrate to use CCv2's committer API\n      io.delta.kernel.Table.forPath(engine, tablePath)\n          .createTransactionBuilder(\n              engine, \"kernel-spark-dsv2-test-catalog\", Operation.CREATE_TABLE)\n          .withSchema(engine, SchemaUtils.convertSparkSchemaToKernelSchema(schema))\n          .withPartitionColumns(engine, partitionColumns)\n          .withTableProperties(engine, properties)\n          .build(engine)\n          .commit(engine, CloseableIterable.emptyIterable());\n\n      // Load the created table and return SparkTable\n      return new SparkTable(ident, tablePath);\n\n    } catch (Exception e) {\n      // Remove the table entry if creation fails\n      tablePaths.remove(tableKey);\n      throw new RuntimeException(\"Failed to create table: \" + ident, e);\n    }\n  }\n\n  @Override\n  public Table alterTable(Identifier ident, TableChange... changes) {\n    throw new UnsupportedOperationException(\"alterTable method is not implemented\");\n  }\n\n  @Override\n  public boolean dropTable(Identifier ident) {\n    String tableKey = getTableKey(ident);\n    return tablePaths.remove(tableKey) != null;\n  }\n\n  @Override\n  public void renameTable(Identifier oldIdent, Identifier newIdent) {\n    throw new UnsupportedOperationException(\"renameTable method is not implemented\");\n  }\n\n  @Override\n  public void initialize(String name, CaseInsensitiveStringMap options) {\n    this.catalogName = name;\n    // Use a default path if base_path is not provided\n    this.basePath = options.getOrDefault(\"base_path\", \"/tmp/dsv2_test/\");\n  }\n\n  @Override\n  public String name() {\n    return catalogName;\n  }\n\n  /**\n   * Check if the given identifier represents a path-based table. Path-based tables are identified\n   * by having a delta namespace. This follows the same logic as Delta Spark's\n   * SupportsPathIdentifier.\n   */\n  private boolean isPathIdentifier(Identifier ident) {\n    // For testing, simply check if it has a delta namespace\n    return ident.namespace().length == 1 && ident.namespace()[0].toLowerCase().equals(\"delta\");\n  }\n\n  /** Helper method to get the table key from identifier. */\n  private String getTableKey(Identifier ident) {\n    if (ident.namespace().length == 0) {\n      return ident.name();\n    } else {\n      return String.join(\".\", ident.namespace()) + \".\" + ident.name();\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/SparkGoldenTableTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport io.delta.golden.GoldenTableUtils$;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.spark.internal.v2.catalog.SparkTable;\nimport java.io.File;\nimport java.lang.reflect.Field;\nimport java.math.BigDecimal;\nimport java.util.*;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.SparkConf;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.QueryTest$;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.expressions.GenericRow;\nimport org.apache.spark.sql.connector.catalog.Identifier;\nimport org.apache.spark.sql.connector.expressions.Expression;\nimport org.apache.spark.sql.connector.read.Scan;\nimport org.apache.spark.sql.connector.read.ScanBuilder;\nimport org.apache.spark.sql.sources.*;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.io.TempDir;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic class SparkGoldenTableTest {\n\n  private SparkSession spark;\n\n  @BeforeAll\n  public void setUp(@TempDir File tempDir) {\n    SparkConf conf =\n        new SparkConf()\n            .set(\"spark.sql.catalog.dsv2\", \"io.delta.spark.internal.v2.catalog.TestCatalog\")\n            .set(\"spark.sql.catalog.dsv2.base_path\", tempDir.getAbsolutePath())\n            .set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtensionV1\")\n            .set(\n                \"spark.sql.catalog.spark_catalog\",\n                \"org.apache.spark.sql.delta.catalog.DeltaCatalogV1\")\n            .setMaster(\"local[*]\")\n            .setAppName(\"SparkGoldenTableTest\");\n    spark = SparkSession.builder().config(conf).getOrCreate();\n  }\n\n  @AfterAll\n  public void tearDown() {\n    if (spark != null) {\n      spark.stop();\n      spark = null;\n    }\n  }\n\n  /** Helper method to check DataFrame results against expected rows. */\n  private void checkAnswer(Dataset<Row> df, List<Row> expected) {\n    QueryTest$.MODULE$.checkAnswer(df, expected);\n  }\n\n  @Test\n  public void testDsv2Internal() throws Exception {\n    String tableName = \"deltatbl-partition-prune\";\n    String tablePath = goldenTablePath(\"hive/\" + tableName);\n    CaseInsensitiveStringMap options =\n        new CaseInsensitiveStringMap(\n            new java.util.HashMap<String, String>() {\n              {\n                put(\"key1\", \"value1\");\n                put(\"key2\", \"value2\");\n              }\n            });\n    SparkTable table =\n        new SparkTable(\n            Identifier.of(new String[] {\"spark_catalog\", \"default\"}, tableName),\n            tablePath,\n            options);\n    StructType expectedDataSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true),\n              DataTypes.createStructField(\"cnt\", DataTypes.IntegerType, true),\n            });\n    StructType expectedPartitionSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"date\", DataTypes.StringType, true),\n              DataTypes.createStructField(\"city\", DataTypes.StringType, true),\n            });\n    StructType expectedSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              expectedPartitionSchema.fields()[1],\n              expectedPartitionSchema.fields()[0],\n              expectedDataSchema.fields()[0],\n              expectedDataSchema.fields()[1]\n            });\n    assertEquals(expectedSchema, table.schema());\n    assertEquals(String.format(\"delta.`%s`\", tablePath), table.name());\n    // Check table columns\n    assertEquals(4, table.columns().length);\n    assertEquals(\"city\", table.columns()[0].name());\n    assertEquals(\"date\", table.columns()[1].name());\n    assertEquals(\"name\", table.columns()[2].name());\n    assertEquals(\"cnt\", table.columns()[3].name());\n\n    // Check table partitioning\n    assertEquals(2, table.partitioning().length);\n    assertEquals(\"identity(date)\", table.partitioning()[0].toString());\n    assertEquals(\"identity(city)\", table.partitioning()[1].toString());\n\n    // Check table properties\n    assertEquals(Map.of(), table.properties());\n\n    CaseInsensitiveStringMap scanOptions =\n        new CaseInsensitiveStringMap(\n            new java.util.HashMap<String, String>() {\n              {\n                put(\"key3\", \"value3\");\n                put(\"key2\", \"new_value2\");\n              }\n            });\n    ScanBuilder builder = table.newScanBuilder(scanOptions);\n    assertTrue((builder instanceof SparkScanBuilder));\n    SparkScanBuilder scanBuilder = (SparkScanBuilder) builder;\n    assertEquals(expectedDataSchema, scanBuilder.getDataSchema());\n    assertEquals(expectedPartitionSchema, scanBuilder.getPartitionSchema());\n    CaseInsensitiveStringMap combinedOptions =\n        new CaseInsensitiveStringMap(\n            new java.util.HashMap<String, String>() {\n              {\n                put(\"key1\", \"value1\");\n                put(\"key2\", \"new_value2\");\n                put(\"key3\", \"value3\");\n              }\n            });\n    assertEquals(combinedOptions, scanBuilder.getOptions());\n\n    Scan scan1 = scanBuilder.build();\n    assertTrue(scan1 instanceof SparkScan);\n    SparkScan sparkScan1 = (SparkScan) scan1;\n    assertEquals(expectedDataSchema, sparkScan1.getDataSchema());\n    assertEquals(expectedDataSchema, sparkScan1.getReadDataSchema());\n    assertEquals(expectedPartitionSchema, sparkScan1.getPartitionSchema());\n    assertEquals(combinedOptions, sparkScan1.getOptions());\n    verifyHadoopConf(sparkScan1.getConfiguration());\n\n    // check SupportsPushDownRequiredColumns\n    StructType prunedSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              expectedDataSchema.fields()[0], expectedPartitionSchema.fields()[0],\n            });\n    scanBuilder.pruneColumns(prunedSchema);\n    Scan scan2 = scanBuilder.build();\n    assertTrue(scan2 instanceof SparkScan);\n    SparkScan sparkScan2 = (SparkScan) scan2;\n    assertEquals(expectedDataSchema, sparkScan2.getDataSchema());\n    StructType expectedReadDataSchemaAfterPrune =\n        DataTypes.createStructType(new StructField[] {expectedDataSchema.fields()[0]});\n    assertEquals(expectedReadDataSchemaAfterPrune, sparkScan2.getReadDataSchema());\n    assertEquals(combinedOptions, sparkScan2.getOptions());\n    verifyHadoopConf(sparkScan2.getConfiguration());\n\n    // check SupportsPushDownFilters\n    // case 1: mix of supported and unsupported, data and partition filters\n    checkSupportsPushDownFilters(\n        table,\n        scanOptions,\n        // input filters\n        new Filter[] {\n          new GreaterThan(\"cnt\", 10), // supported data filter\n          new StringStartsWith(\"name\", \"foo\"), // supported data filter\n          new EqualTo(\"date\", \"2025-09-01\"), // supported partition filter\n          new StringEndsWith(\"city\", \"York\"), // unsupported partition filter\n        },\n        // expected post-scan filters\n        new Filter[] {\n          new GreaterThan(\"cnt\", 10),\n          new StringStartsWith(\"name\", \"foo\"),\n          new StringEndsWith(\"city\", \"York\"),\n        },\n        // expected pushed filters\n        new Filter[] {\n          new GreaterThan(\"cnt\", 10),\n          new StringStartsWith(\"name\", \"foo\"),\n          new EqualTo(\"date\", \"2025-09-01\")\n        },\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\">\", new Column(\"cnt\"), Literal.ofInt(10)),\n          new Predicate(\"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"foo\")),\n          new Predicate(\"=\", new Column(\"date\"), Literal.ofString(\"2025-09-01\"))\n        },\n        // expected data filters\n        new Filter[] {new GreaterThan(\"cnt\", 10), new StringStartsWith(\"name\", \"foo\")},\n        // expected kernel scan builder predicate\n        Optional.of(\n            new Predicate(\n                \"AND\",\n                new Predicate(\n                    \"AND\",\n                    new Predicate(\">\", new Column(\"cnt\"), Literal.ofInt(10)),\n                    new Predicate(\"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"foo\"))),\n                new Predicate(\"=\", new Column(\"date\"), Literal.ofString(\"2025-09-01\")))));\n\n    // case 2: OR and NOT filters\n    checkSupportsPushDownFilters(\n        table,\n        scanOptions,\n        // input filters\n        new Filter[] {\n          new Or(new GreaterThan(\"cnt\", 10), new StringStartsWith(\"name\", \"foo\")),\n          new Or(new EqualTo(\"cnt\", 50), new EqualTo(\"date\", \"2025-10-01\")),\n          new Not(new And(new GreaterThan(\"cnt\", 100), new EqualTo(\"date\", \"2025-09-01\"))),\n          new Not(new Or(new EqualTo(\"name\", \"foo\"), new StringStartsWith(\"city\", \"New\")))\n        },\n        // expected post-scan filters\n        new Filter[] {\n          new Or(new GreaterThan(\"cnt\", 10), new StringStartsWith(\"name\", \"foo\")),\n          new Or(new EqualTo(\"cnt\", 50), new EqualTo(\"date\", \"2025-10-01\")),\n          new Not(new And(new GreaterThan(\"cnt\", 100), new EqualTo(\"date\", \"2025-09-01\"))),\n          new Not(new Or(new EqualTo(\"name\", \"foo\"), new StringStartsWith(\"city\", \"New\")))\n        },\n        // expected pushed filters\n        new Filter[] {\n          new Or(new GreaterThan(\"cnt\", 10), new StringStartsWith(\"name\", \"foo\")),\n          new Or(new EqualTo(\"cnt\", 50), new EqualTo(\"date\", \"2025-10-01\")),\n          new Not(new And(new GreaterThan(\"cnt\", 100), new EqualTo(\"date\", \"2025-09-01\"))),\n          new Not(new Or(new EqualTo(\"name\", \"foo\"), new StringStartsWith(\"city\", \"New\")))\n        },\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\n              \"OR\",\n              new Predicate(\">\", new Column(\"cnt\"), Literal.ofInt(10)),\n              new Predicate(\"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"foo\"))),\n          new Predicate(\n              \"OR\",\n              new Predicate(\"=\", new Column(\"cnt\"), Literal.ofInt(50)),\n              new Predicate(\"=\", new Column(\"date\"), Literal.ofString(\"2025-10-01\"))),\n          new Predicate(\n              \"NOT\",\n              new Predicate(\n                  \"AND\",\n                  new Predicate(\">\", new Column(\"cnt\"), Literal.ofInt(100)),\n                  new Predicate(\"=\", new Column(\"date\"), Literal.ofString(\"2025-09-01\")))),\n          new Predicate(\n              \"NOT\",\n              new Predicate(\n                  \"OR\",\n                  new Predicate(\"=\", new Column(\"name\"), Literal.ofString(\"foo\")),\n                  new Predicate(\"STARTS_WITH\", new Column(\"city\"), Literal.ofString(\"New\"))))\n        },\n        // expected data filters\n        new Filter[] {\n          new Or(new GreaterThan(\"cnt\", 10), new StringStartsWith(\"name\", \"foo\")),\n          new Or(new EqualTo(\"cnt\", 50), new EqualTo(\"date\", \"2025-10-01\")),\n          new Not(new And(new GreaterThan(\"cnt\", 100), new EqualTo(\"date\", \"2025-09-01\"))),\n          new Not(new Or(new EqualTo(\"name\", \"foo\"), new StringStartsWith(\"city\", \"New\")))\n        },\n        // expected kernel scan builder predicate\n        // reduce(And::new) over 4 predicates gives left-associative nesting:\n        // AND(AND(AND(pred1, pred2), pred3), pred4)\n        Optional.of(\n            new Predicate(\n                \"AND\",\n                new Predicate(\n                    \"AND\",\n                    new Predicate(\n                        \"AND\",\n                        new Predicate(\n                            \"OR\",\n                            new Predicate(\">\", new Column(\"cnt\"), Literal.ofInt(10)),\n                            new Predicate(\n                                \"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"foo\"))),\n                        new Predicate(\n                            \"OR\",\n                            new Predicate(\"=\", new Column(\"cnt\"), Literal.ofInt(50)),\n                            new Predicate(\n                                \"=\", new Column(\"date\"), Literal.ofString(\"2025-10-01\")))),\n                    new Predicate(\n                        \"NOT\",\n                        new Predicate(\n                            \"AND\",\n                            new Predicate(\">\", new Column(\"cnt\"), Literal.ofInt(100)),\n                            new Predicate(\n                                \"=\", new Column(\"date\"), Literal.ofString(\"2025-09-01\"))))),\n                new Predicate(\n                    \"NOT\",\n                    new Predicate(\n                        \"OR\",\n                        new Predicate(\"=\", new Column(\"name\"), Literal.ofString(\"foo\")),\n                        new Predicate(\n                            \"STARTS_WITH\", new Column(\"city\"), Literal.ofString(\"New\")))))));\n\n    // check SupportsRuntimeV2Filtering\n    // city = 'hz' AND date = '20180520'\n    org.apache.spark.sql.connector.expressions.filter.Predicate andPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"AND\", new Expression[] {SparkScanTest.cityPredicate, SparkScanTest.datePredicate});\n    SparkScanTest.checkSupportsRuntimeFilters(\n        table,\n        options,\n        new org.apache.spark.sql.connector.expressions.filter.Predicate[] {andPredicate},\n        Arrays.asList(\"date=20180520/city=hz\"));\n\n    // city = 'hz' OR date = '20180520'\n    org.apache.spark.sql.connector.expressions.filter.Predicate orPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"OR\", new Expression[] {SparkScanTest.cityPredicate, SparkScanTest.datePredicate});\n    SparkScanTest.checkSupportsRuntimeFilters(\n        table,\n        scanOptions,\n        new org.apache.spark.sql.connector.expressions.filter.Predicate[] {orPredicate},\n        Arrays.asList(\"city=hz\", \"date=20180520\"));\n\n    //  city = 'hz', cnt > 10\n    SparkScanTest.checkSupportsRuntimeFilters(\n        table,\n        options,\n        new org.apache.spark.sql.connector.expressions.filter.Predicate[] {\n          SparkScanTest.cityPredicate, SparkScanTest.dataPredicate\n        },\n        Arrays.asList(\"city=hz\"));\n\n    //  city = 'hz' OR cnt > 10\n    org.apache.spark.sql.connector.expressions.filter.Predicate orDataPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"OR\", new Expression[] {SparkScanTest.cityPredicate, SparkScanTest.dataPredicate});\n    SparkScanTest.checkSupportsRuntimeFilters(\n        table,\n        options,\n        new org.apache.spark.sql.connector.expressions.filter.Predicate[] {orDataPredicate},\n        SparkScanTest.allCities);\n\n    // city = date\n    SparkScanTest.checkSupportsRuntimeFilters(\n        table,\n        options,\n        new org.apache.spark.sql.connector.expressions.filter.Predicate[] {\n          SparkScanTest.negativeInterColPredicate\n        },\n        Arrays.asList());\n\n    // city <> date\n    SparkScanTest.checkSupportsRuntimeFilters(\n        table,\n        options,\n        new org.apache.spark.sql.connector.expressions.filter.Predicate[] {\n          SparkScanTest.interColPredicate\n        },\n        SparkScanTest.allCities);\n  }\n\n  private void checkSupportsPushDownFilters(\n      SparkTable table,\n      CaseInsensitiveStringMap scanOptions,\n      Filter[] inputFilters,\n      Filter[] expectedPostScanFilters,\n      Filter[] expectedPushedFilters,\n      Predicate[] expectedPushedKernelPredicates,\n      Filter[] expectedDataFilters,\n      Optional<Predicate> expectedKernelScanBuilderPredicate)\n      throws Exception {\n    ScanBuilder newBuilder = table.newScanBuilder(scanOptions);\n    SparkScanBuilder builder = (SparkScanBuilder) newBuilder;\n\n    Filter[] postScanFilters = builder.pushFilters(inputFilters);\n\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedPostScanFilters)),\n        new HashSet<>(Arrays.asList(postScanFilters)));\n\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedPushedFilters)),\n        new HashSet<>(Arrays.asList(builder.pushedFilters())));\n\n    Predicate[] pushedPredicates = getPushedKernelPredicates(builder);\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedPushedKernelPredicates)),\n        new HashSet<>(Arrays.asList(pushedPredicates)));\n\n    Filter[] dataFilters = getDataFilters(builder);\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedDataFilters)),\n        new HashSet<>(Arrays.asList(dataFilters)));\n\n    Optional<Predicate> predicateOpt = getKernelScanBuilderPredicate(builder);\n    assertEquals(expectedKernelScanBuilderPredicate, predicateOpt);\n  }\n\n  private Predicate[] getPushedKernelPredicates(SparkScanBuilder builder) throws Exception {\n    Field field = SparkScanBuilder.class.getDeclaredField(\"pushedKernelPredicates\");\n    field.setAccessible(true);\n    return (Predicate[]) field.get(builder);\n  }\n\n  private Filter[] getDataFilters(SparkScanBuilder builder) throws Exception {\n    Field field = SparkScanBuilder.class.getDeclaredField(\"dataFilters\");\n    field.setAccessible(true);\n    return (Filter[]) field.get(builder);\n  }\n\n  private Optional<Predicate> getKernelScanBuilderPredicate(SparkScanBuilder builder)\n      throws Exception {\n    Field field = SparkScanBuilder.class.getDeclaredField(\"kernelScanBuilder\");\n    field.setAccessible(true);\n    Object kernelScanBuilder = field.get(builder);\n    Field predicateField = kernelScanBuilder.getClass().getDeclaredField(\"predicate\");\n    predicateField.setAccessible(true);\n    Object raw = predicateField.get(kernelScanBuilder);\n    if (raw == null) {\n      return Optional.empty();\n    }\n    Optional<?> opt = (Optional<?>) raw;\n    return opt.map(Predicate.class::cast);\n  }\n\n  @Test\n  public void testDsv2InteralWithNestedStruct() {\n    String tableName = \"data-reader-nested-struct\";\n    String tablePath = goldenTablePath(tableName);\n    SparkTable table =\n        new SparkTable(\n            Identifier.of(new String[] {\"spark_catalog\", \"default\"}, tableName), tablePath);\n\n    StructType expectedSchema =\n        StructType.fromDDL(\n            \"a STRUCT<aa: STRING, ab: STRING, ac: STRUCT<aca: INT, acb: BIGINT>>,b INT\");\n\n    assertEquals(expectedSchema, table.schema());\n    assertEquals(String.format(\"delta.`%s`\", tablePath), table.name());\n    assertEquals(0, table.partitioning().length);\n\n    CaseInsensitiveStringMap options =\n        new CaseInsensitiveStringMap(\n            java.util.Collections.singletonMap(\"another_option_key\", \"another_option_value\"));\n    ScanBuilder builder = table.newScanBuilder(options);\n    assertTrue((builder instanceof SparkScanBuilder));\n    SparkScanBuilder scanBuilder = (SparkScanBuilder) builder;\n\n    assertEquals(expectedSchema, scanBuilder.getDataSchema());\n    assertTrue(scanBuilder.getPartitionSchema().isEmpty());\n    assertEquals(options, scanBuilder.getOptions());\n\n    // Initial scan (no pruning)\n    Scan scan1 = scanBuilder.build();\n    assertTrue(scan1 instanceof SparkScan);\n    SparkScan sparkScan1 = (SparkScan) scan1;\n    assertEquals(expectedSchema, sparkScan1.getDataSchema());\n    assertEquals(expectedSchema, sparkScan1.getReadDataSchema());\n    assertTrue(sparkScan1.getPartitionSchema().isEmpty());\n    assertEquals(options, sparkScan1.getOptions());\n\n    StructType prunedSchema = StructType.fromDDL(\"a STRUCT<aa: STRING, ab: STRING>\");\n    scanBuilder.pruneColumns(prunedSchema);\n\n    Scan scan2 = scanBuilder.build();\n    assertTrue(scan2 instanceof SparkScan);\n    SparkScan sparkScan2 = (SparkScan) scan2;\n    assertEquals(expectedSchema, sparkScan2.getDataSchema());\n    assertEquals(prunedSchema, sparkScan2.getReadDataSchema());\n    assertTrue(sparkScan2.getPartitionSchema().isEmpty());\n    assertEquals(options, sparkScan2.getOptions());\n  }\n\n  @Test\n  public void testTablePrimitives() throws Exception {\n    List<Row> expected = new ArrayList<>();\n    for (int i = 0; i <= 10; i++) {\n      if (i == 10) {\n        expected.add(\n            new GenericRow(\n                new Object[] {null, null, null, null, null, null, null, null, null, null}));\n      } else {\n        expected.add(\n            new GenericRow(\n                new Object[] {\n                  i,\n                  (long) i,\n                  (byte) i,\n                  (short) i,\n                  i % 2 == 0,\n                  (float) i,\n                  (double) i,\n                  Integer.toString(i),\n                  new byte[] {(byte) i, (byte) i},\n                  new BigDecimal(i)\n                }));\n      }\n    }\n\n    checkTable(\"data-reader-primitives\", expected);\n  }\n\n  @Test\n  public void testTableWithNestedStruct() {\n    List<Row> expected = new ArrayList<>();\n    for (int i = 0; i < 10; i++) {\n      Row innerMost = new GenericRow(new Object[] {i, (long) i});\n      Row middle =\n          new GenericRow(new Object[] {Integer.toString(i), Integer.toString(i), innerMost});\n      expected.add(new GenericRow(new Object[] {middle, i}));\n    }\n    // Assuming `checkTable` is made accessible (e.g., protected in base class)\n    checkTable(\"data-reader-nested-struct\", expected);\n  }\n\n  @Test\n  public void testPartitionedTable() {\n    // Build expected rows (excluding unsupported partition column `as_timestamp`)\n    List<Row> expected = new ArrayList<>();\n    java.sql.Date fixedDate = java.sql.Date.valueOf(\"2021-09-08\");\n\n    for (int i = 0; i < 2; i++) {\n      // Array field: Seq(TestRow(i), TestRow(i), TestRow(i)) where TestRow(i) => struct(i)\n      List<Row> arrElems =\n          Arrays.asList(\n              new GenericRow(new Object[] {i}),\n              new GenericRow(new Object[] {i}),\n              new GenericRow(new Object[] {i}));\n      Object arrSeq = scala.collection.JavaConverters.asScalaBuffer(arrElems).toList();\n\n      // Nested struct: TestRow(i.toString, i.toString, TestRow(i, i.toLong))\n      Row innerMost = new GenericRow(new Object[] {i, (long) i});\n      Row middle =\n          new GenericRow(new Object[] {Integer.toString(i), Integer.toString(i), innerMost});\n\n      expected.add(\n          new GenericRow(\n              new Object[] {\n                i, // int\n                (long) i, // long\n                (byte) i, // byte\n                (short) i, // short\n                i % 2 == 0, // boolean\n                (float) i, // float\n                (double) i, // double\n                Integer.toString(i), // string\n                \"null\", // literal string\n                fixedDate, // date (was daysSinceEpoch int)\n                new BigDecimal(i), // decimal\n                arrSeq, // array<struct>\n                middle, // nested struct\n                Integer.toString(i) // final string\n              }));\n    }\n\n    // Null row variant with specific non-null complex fields (matches Scala test)\n    List<Row> nullArrElems =\n        Arrays.asList(\n            new GenericRow(new Object[] {2}),\n            new GenericRow(new Object[] {2}),\n            new GenericRow(new Object[] {2}));\n    Object nullArrSeq = scala.collection.JavaConverters.asScalaBuffer(nullArrElems).toList();\n    Row nullInnerMost = new GenericRow(new Object[] {2, 2L});\n    Row nullMiddle = new GenericRow(new Object[] {\"2\", \"2\", nullInnerMost});\n    expected.add(\n        new GenericRow(\n            new Object[] {\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              nullArrSeq,\n              nullMiddle,\n              \"2\"\n            }));\n\n    // Read table, drop unsupported column `as_timestamp`\n    String tablePath = goldenTablePath(\"data-reader-partition-values\");\n    Dataset<Row> full = spark.sql(\"SELECT * FROM `dsv2`.`delta`.`\" + tablePath + \"`\");\n\n    List<String> projectedCols = new ArrayList<>();\n    for (String f : full.schema().fieldNames()) {\n      if (!f.equals(\"as_timestamp\")) {\n        projectedCols.add(f);\n      }\n    }\n    Dataset<Row> df = full.selectExpr(projectedCols.toArray(new String[0]));\n\n    checkAnswer(df, expected);\n  }\n\n  @Test\n  public void testVariantTypeTable() {\n    String tablePath = goldenTablePath(\"spark-variant-checkpoint\");\n    Dataset<Row> df = spark.sql(\"SELECT * FROM `dsv2`.`delta`.`\" + tablePath + \"`\");\n\n    // Verify schema: id (long) + 6 variant/nested-variant columns\n    StructType schema = df.schema();\n    assertEquals(7, schema.fields().length);\n    assertEquals(DataTypes.LongType, schema.apply(\"id\").dataType());\n    assertEquals(DataTypes.VariantType, schema.apply(\"v\").dataType());\n    assertEquals(\n        DataTypes.createArrayType(DataTypes.VariantType, true),\n        schema.apply(\"array_of_variants\").dataType());\n    assertEquals(\n        DataTypes.createStructType(\n            new StructField[] {\n              new StructField(\n                  \"v\", DataTypes.VariantType, true, org.apache.spark.sql.types.Metadata.empty())\n            }),\n        schema.apply(\"struct_of_variants\").dataType());\n    assertEquals(\n        DataTypes.createMapType(DataTypes.StringType, DataTypes.VariantType, true),\n        schema.apply(\"map_of_variants\").dataType());\n\n    // Verify row count: 100 base rows + 2 appended rows\n    assertEquals(102, df.count());\n\n    // Verify id values are readable (non-variant column)\n    List<Row> ids = df.select(\"id\").orderBy(\"id\").limit(3).collectAsList();\n    assertEquals(0L, ids.get(0).getLong(0));\n    assertEquals(0L, ids.get(1).getLong(0));\n    assertEquals(1L, ids.get(2).getLong(0));\n\n    // Verify all variant column values. Each variant value is parse_json('{\"key\": id}'),\n    // so variant_get(..., '$.key', 'long') must equal id for all rows.\n    // - v:                                  direct variant\n    // - array_of_variants[0]:               first element (indices 1, 3 are null)\n    // - struct_of_variants.v:               struct field\n    // - map_of_variants[CAST(id AS STRING)]: map value by string key\n    // - array_of_struct_of_variants[0].v:   first struct element's variant field\n    // - struct_of_array_of_variants.v[1]:   struct's array field at index 1 (index 0 is null)\n    long matchingRows =\n        df.where(\n                \"variant_get(v, '$.key', 'long') = id\"\n                    + \" AND variant_get(array_of_variants[0], '$.key', 'long') = id\"\n                    + \" AND variant_get(struct_of_variants.v, '$.key', 'long') = id\"\n                    + \" AND variant_get(map_of_variants[CAST(id AS STRING)], '$.key', 'long') = id\"\n                    + \" AND variant_get(array_of_struct_of_variants[0].v, '$.key', 'long') = id\"\n                    + \" AND variant_get(struct_of_array_of_variants.v[1], '$.key', 'long') = id\")\n            .count();\n    assertEquals(102, matchingRows);\n\n    // Verify known null values within variant columns:\n    // - array_of_variants[1] and [3]:          null array elements\n    // - map_of_variants['nullKey']:             null map value\n    // - array_of_struct_of_variants[1].v:       non-null struct but null variant field\n    // - array_of_struct_of_variants[2]:         null struct element\n    // - struct_of_array_of_variants.v[0]:       null first element of struct's array field\n    long nullMatchingRows =\n        df.where(\n                \"array_of_variants[1] IS NULL\"\n                    + \" AND array_of_variants[3] IS NULL\"\n                    + \" AND map_of_variants['nullKey'] IS NULL\"\n                    + \" AND array_of_struct_of_variants[1].v IS NULL\"\n                    + \" AND array_of_struct_of_variants[2] IS NULL\"\n                    + \" AND struct_of_array_of_variants.v[0] IS NULL\")\n            .count();\n    assertEquals(102, nullMatchingRows);\n  }\n\n  @Test\n  public void testAllGoldenTables() {\n    List<String> tableNames = getAllGoldenTableNames();\n    List<String> unsupportedTables =\n        Arrays.asList(\n            \"canonicalized-paths-normal-a\",\n            \"canonicalized-paths-normal-b\",\n            \"canonicalized-paths-special-a\",\n            \"canonicalized-paths-special-b\",\n            \"checkpoint\",\n            \"corrupted-last-checkpoint\",\n            \"data-reader-absolute-paths-escaped-chars\",\n            \"data-reader-escaped-chars\",\n            // File delete-re-add-same-file-different-transactions/bar does not exist\n            \"delete-re-add-same-file-different-transactions\",\n            // Root node at key schemaString is null but field isn't nullable\n            \"deltalog-commit-info\",\n            // [DELTA_INVALID_PROTOCOL_VERSION] Unsupported Delta protocol version\n            \"deltalog-invalid-protocol-version\",\n            // [DELTA_STATE_RECOVER_ERROR] The metadata of your Delta table could not be recovered\n            // while Reconstructing\n            \"deltalog-state-reconstruction-from-checkpoint-missing-metadata\",\n            // [DELTA_STATE_RECOVER_ERROR] The protocol of your Delta table could not be recovered\n            // while Reconstructing\n            \"deltalog-state-reconstruction-from-checkpoint-missing-protocol\");\n\n    for (String tableName : tableNames) {\n      if (unsupportedTables.contains(tableName)) {\n        continue;\n      }\n\n      // For simplicity, just check that we can read the table and it has at least one row\n      String tablePath = goldenTablePath(tableName);\n      // Many golden tables only have corrupted _delta_log subdir. The new kernel table reader will\n      // fail on some of those.\n      // TODO: fix the read result of those tables.\n      if (hasOnlyDeltaLogSubdir(tablePath)) {\n        continue;\n      }\n      Dataset<Row> df = spark.sql(\"SELECT * FROM `spark_catalog`.`delta`.`\" + tablePath + \"`\");\n      Dataset<Row> df2 = spark.sql(\"SELECT * FROM `dsv2`.`delta`.`\" + tablePath + \"`\");\n      assertEquals(df.schema(), df2.schema(), \"Schema mismatch for table: \" + tableName);\n      checkAnswer(df2, df.collectAsList());\n    }\n  }\n\n  private void verifyHadoopConf(Configuration conf) {\n    assertEquals(\"value1\", conf.get(\"key1\"));\n    assertEquals(\"new_value2\", conf.get(\"key2\"));\n    assertEquals(\"value3\", conf.get(\"key3\"));\n  }\n\n  private boolean hasOnlyDeltaLogSubdir(String path) {\n    File dir = new File(path);\n    if (!dir.exists() || !dir.isDirectory()) {\n      return false;\n    }\n\n    File[] subFiles = dir.listFiles(File::isDirectory);\n    if (subFiles == null) {\n      return false;\n    }\n\n    // Check: only one subdirectory, and it is \"_delta_log\"\n    return subFiles.length == 1 && \"_delta_log\".equals(subFiles[0].getName());\n  }\n\n  private void checkTable(String path, List<Row> expected) {\n    String tablePath = goldenTablePath(path);\n    Dataset<Row> df = spark.sql(\"SELECT * FROM `dsv2`.`delta`.`\" + tablePath + \"`\");\n    checkAnswer(df, expected);\n  }\n\n  private String goldenTablePath(String name) {\n    return GoldenTableUtils$.MODULE$.goldenTablePath(name);\n  }\n\n  private List<String> getAllGoldenTableNames() {\n    return scala.collection.JavaConverters.seqAsJavaList(GoldenTableUtils$.MODULE$.allTableNames());\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/SparkMicroBatchStreamTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.delta.kernel.CommitActions;\nimport io.delta.kernel.data.ColumnarBatch;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport io.delta.spark.internal.v2.snapshot.PathBasedSnapshotManager;\nimport io.delta.spark.internal.v2.utils.ScalaUtils;\nimport java.io.File;\nimport java.nio.channels.ClosedByInterruptException;\nimport java.sql.Timestamp;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.Dataset;\nimport org.apache.spark.sql.Row;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.catalyst.expressions.Expression;\nimport org.apache.spark.sql.connector.read.InputPartition;\nimport org.apache.spark.sql.connector.read.PartitionReader;\nimport org.apache.spark.sql.connector.read.PartitionReaderFactory;\nimport org.apache.spark.sql.connector.read.streaming.Offset;\nimport org.apache.spark.sql.connector.read.streaming.ReadLimit;\nimport org.apache.spark.sql.delta.*;\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf;\nimport org.apache.spark.sql.delta.sources.DeltaSource;\nimport org.apache.spark.sql.delta.sources.DeltaSourceOffset;\nimport org.apache.spark.sql.delta.sources.ReadMaxBytes;\nimport org.apache.spark.sql.delta.storage.ClosableIterator;\nimport org.apache.spark.sql.delta.util.JsonUtils;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport scala.Option;\nimport scala.collection.JavaConverters;\nimport scala.collection.immutable.Map$;\nimport scala.collection.immutable.Seq;\n\npublic class SparkMicroBatchStreamTest extends DeltaV2TestBase {\n\n  /**\n   * Helper method to create a minimal SparkMicroBatchStream instance for tests that only check for\n   * UnsupportedOperationException.\n   */\n  private SparkMicroBatchStream createTestStream(File tempDir) {\n    String tablePath = tempDir.getAbsolutePath();\n    String tableName = \"test_unsupported_\" + System.nanoTime();\n    createEmptyTestTable(tablePath, tableName);\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager = new PathBasedSnapshotManager(tablePath, hadoopConf);\n    return createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n  }\n\n  private DeltaOptions emptyDeltaOptions() {\n    return new DeltaOptions(Map$.MODULE$.empty(), spark.sessionState().conf());\n  }\n\n  @Test\n  public void testLatestOffset_throwsUnsupportedOperationException(@TempDir File tempDir) {\n    SparkMicroBatchStream microBatchStream = createTestStream(tempDir);\n    IllegalStateException exception =\n        assertThrows(IllegalStateException.class, () -> microBatchStream.latestOffset());\n  }\n\n  @Test\n  public void testInitialOffset_withInitialSnapshot(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_initial_snapshot_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Insert some data to create versions\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 3,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ false);\n\n    // Create stream without startingVersion (emptyDeltaOptions)\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n    // Get the latest version\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    long latestVersion = deltaLog.update(false, Option.empty(), Option.empty()).version();\n\n    // Call initialOffset - should create initial snapshot at latest version\n    Offset initialOffset = stream.initialOffset();\n    assertNotNull(initialOffset, \"Initial offset should not be null\");\n\n    DeltaSourceOffset deltaOffset = (DeltaSourceOffset) initialOffset;\n    assertEquals(\n        latestVersion,\n        deltaOffset.reservoirVersion(),\n        \"Initial offset should be at latest version\");\n    assertEquals(\n        DeltaSourceOffset.BASE_INDEX(),\n        deltaOffset.index(),\n        \"Initial offset should start at BASE_INDEX\");\n    assertTrue(\n        deltaOffset.isInitialSnapshot(), \"Initial offset should be marked as initial snapshot\");\n  }\n\n  @Test\n  public void testDeserializeOffset_ValidJson(@TempDir File tempDir) throws Exception {\n    String tablePath = tempDir.getAbsolutePath();\n    SparkMicroBatchStream stream = createTestStream(tempDir);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(tablePath));\n    String tableId = deltaLog.tableId();\n    DeltaSourceOffset expected = new DeltaSourceOffset(tableId, 5L, 10L, false);\n    String json = org.apache.spark.sql.delta.util.JsonUtils.mapper().writeValueAsString(expected);\n\n    Offset result = stream.deserializeOffset(json);\n    DeltaSourceOffset actual = (DeltaSourceOffset) result;\n\n    assertEquals(expected.reservoirId(), actual.reservoirId());\n    assertEquals(expected.reservoirVersion(), actual.reservoirVersion());\n    assertEquals(expected.index(), actual.index());\n    assertEquals(expected.isInitialSnapshot(), actual.isInitialSnapshot());\n  }\n\n  @Test\n  public void testDeserializeOffset_MismatchedTableId(@TempDir File tempDir) throws Exception {\n    SparkMicroBatchStream stream = createTestStream(tempDir);\n\n    // Create offset with wrong tableId\n    String wrongTableId = \"wrong-table-id\";\n    DeltaSourceOffset offset =\n        new DeltaSourceOffset(\n            wrongTableId,\n            /* reservoirVersion= */ 1L,\n            /* index= */ 0L,\n            /* isInitialSnapshot= */ false);\n    String json = JsonUtils.mapper().writeValueAsString(offset);\n    RuntimeException exception =\n        assertThrows(RuntimeException.class, () -> stream.deserializeOffset(json));\n\n    assertTrue(\n        exception\n            .getMessage()\n            .contains(\"streaming query was reading from an unexpected Delta table\"));\n  }\n\n  @Test\n  public void testDeserializeOffset_InvalidJson(@TempDir File tempDir) {\n    SparkMicroBatchStream stream = createTestStream(tempDir);\n    String invalidJson = \"{this is not valid json}\";\n    assertThrows(RuntimeException.class, () -> stream.deserializeOffset(invalidJson));\n  }\n\n  @Test\n  public void testDeserializeOffset_WithInitialSnapshot(@TempDir File tempDir) throws Exception {\n    String tablePath = tempDir.getAbsolutePath();\n    SparkMicroBatchStream stream = createTestStream(tempDir);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(tablePath));\n    String tableId = deltaLog.tableId();\n    long baseIndex = DeltaSourceOffset.BASE_INDEX();\n    DeltaSourceOffset expected =\n        new DeltaSourceOffset(\n            tableId, /* reservoirVersion= */ 0L, baseIndex, /* isInitialSnapshot= */ true);\n    String json = org.apache.spark.sql.delta.util.JsonUtils.mapper().writeValueAsString(expected);\n\n    Offset result = stream.deserializeOffset(json);\n    DeltaSourceOffset actual = (DeltaSourceOffset) result;\n\n    assertTrue(actual.isInitialSnapshot());\n    assertEquals(0L, actual.reservoirVersion());\n    assertEquals(baseIndex, actual.index());\n  }\n\n  @Test\n  public void testCommit_NoOp(@TempDir File tempDir) throws Exception {\n    String tablePath = tempDir.getAbsolutePath();\n    SparkMicroBatchStream stream = createTestStream(tempDir);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(tablePath));\n    String tableId = deltaLog.tableId();\n    DeltaSourceOffset offset = new DeltaSourceOffset(tableId, 1L, 0L, false);\n\n    assertDoesNotThrow(() -> stream.commit(offset));\n  }\n\n  @Test\n  public void testStop_NoOp(@TempDir File tempDir) {\n    SparkMicroBatchStream stream = createTestStream(tempDir);\n    assertDoesNotThrow(() -> stream.stop());\n  }\n\n  // ================================================================================================\n  // Tests for initialOffset parity between DSv1 and DSv2\n  // ================================================================================================\n\n  @ParameterizedTest\n  @MethodSource(\"initialOffsetParameters\")\n  public void testInitialOffset_firstBatchParity(\n      String startingVersion,\n      ReadLimitConfig limitConfig,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_initial_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ false);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    ReadLimit readLimit = limitConfig.toReadLimit();\n    DeltaOptions options;\n    if (startingVersion == null) {\n      options = emptyDeltaOptions();\n    } else {\n      scala.collection.immutable.Map<String, String> scalaMap =\n          Map$.MODULE$.<String, String>empty().updated(\"startingVersion\", startingVersion);\n      options = new DeltaOptions(scalaMap, spark.sessionState().conf());\n    }\n\n    // DSv1\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath, options);\n    // DSv1 sources don't have an initialOffset() method.\n    // Batch 0 is called with startOffset=null.\n    Offset dsv1Offset = deltaSource.latestOffset(/* startOffset= */ null, readLimit);\n\n    // DSv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, options);\n    Offset initialOffset = stream.initialOffset();\n    Offset dsv2Offset = stream.latestOffset(initialOffset, readLimit);\n\n    compareOffsets(dsv1Offset, dsv2Offset, testDescription);\n  }\n\n  /** Provides test parameters for the initialOffset parity test. */\n  private static Stream<Arguments> initialOffsetParameters() {\n    return Stream.of(\n        // Initial snapshot cases (no startingVersion)\n        Arguments.of(null, ReadLimitConfig.noLimit(), \"InitialSnapshot_NoLimit\"),\n        Arguments.of(null, ReadLimitConfig.maxFiles(5), \"InitialSnapshot_MaxFiles\"),\n        Arguments.of(null, ReadLimitConfig.maxBytes(1000), \"InitialSnapshot_MaxBytes\"),\n        // Specific version cases\n        Arguments.of(\"0\", ReadLimitConfig.noLimit(), \"NoLimit1\"),\n        Arguments.of(\"1\", ReadLimitConfig.noLimit(), \"NoLimit2\"),\n        Arguments.of(\"3\", ReadLimitConfig.noLimit(), \"NoLimit3\"),\n        Arguments.of(\"latest\", ReadLimitConfig.noLimit(), \"LatestNoLimit\"),\n        Arguments.of(\"latest\", ReadLimitConfig.maxFiles(1000), \"LatestMaxFiles\"),\n        Arguments.of(\"latest\", ReadLimitConfig.maxBytes(1000), \"LatestMaxBytes\"),\n        Arguments.of(\"0\", ReadLimitConfig.maxFiles(5), \"MaxFiles1\"),\n        Arguments.of(\"1\", ReadLimitConfig.maxFiles(10), \"MaxFiles2\"),\n        Arguments.of(\"0\", ReadLimitConfig.maxBytes(1000), \"MaxBytes1\"),\n        Arguments.of(\"1\", ReadLimitConfig.maxBytes(2000), \"MaxBytes2\"));\n  }\n\n  // ================================================================================================\n  // Tests for getFileChanges parity between DSv1 and DSv2\n  // ================================================================================================\n\n  /**\n   * Parameterized test that verifies parity between DSv1 DeltaSource.getFileChanges and DSv2\n   * SparkMicroBatchStream.getFileChanges using Delta Kernel APIs.\n   *\n   * <p>Tests both regular delta log streaming and initial snapshot scenarios.\n   *\n   * <p>TODO(#5319): consider adding a test similar to SparkGoldenTableTest.java.\n   *\n   * <p>TODO(#5318): add tests for ccv2 tables once we fully support them.\n   */\n  @ParameterizedTest\n  @MethodSource(\"getFileChangesParameters\")\n  public void testGetFileChanges(\n      long fromVersion,\n      long fromIndex,\n      boolean isInitialSnapshot,\n      Optional<Long> endVersion,\n      Optional<Long> endIndex,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    // Use unique table name per test instance to avoid conflicts\n    String testTableName =\n        \"test_file_changes_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 5 versions of data (versions 1-5, version 0 is the CREATE TABLE)\n    // Insert 100 rows per commit to potentially trigger multiple batches\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 100,\n        /* includeEmptyVersion= */ false);\n\n    // dsv1 DeltaSource\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n\n    Option<DeltaSourceOffset> scalaEndOffset = Option.empty();\n    if (endVersion.isPresent()) {\n      long offsetIndex = endIndex.orElse(DeltaSourceOffset.END_INDEX());\n      scalaEndOffset =\n          Option.apply(\n              new DeltaSourceOffset(\n                  deltaLog.tableId(), endVersion.get(), offsetIndex, isInitialSnapshot));\n    }\n    ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n        deltaSource.getFileChanges(\n            fromVersion,\n            fromIndex,\n            isInitialSnapshot,\n            scalaEndOffset,\n            /* verifyMetadataAction= */ true);\n    List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n    while (deltaChanges.hasNext()) {\n      deltaFilesList.add(deltaChanges.next());\n    }\n    deltaChanges.close();\n\n    // dsv2 SparkMicroBatchStream\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    Optional<DeltaSourceOffset> endOffsetOption = ScalaUtils.toJavaOptional(scalaEndOffset);\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(fromVersion, fromIndex, isInitialSnapshot, endOffsetOption)) {\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n      compareFileChanges(deltaFilesList, kernelFilesList);\n    }\n  }\n\n  /** Provides test parameters for the parameterized getFileChanges test. */\n  private static Stream<Arguments> getFileChangesParameters() {\n    boolean notInitialSnapshot = false;\n    boolean isInitialSnapshot = true;\n    long BASE_INDEX = DeltaSourceOffset.BASE_INDEX();\n    long END_INDEX = DeltaSourceOffset.END_INDEX();\n    Optional<Long> noEndVersion = Optional.empty();\n    Optional<Long> noEndIndex = Optional.empty();\n\n    // Arguments: (fromVersion, fromIndex, isInitialSnapshot, endVersion, endIndex, testDescription)\n    return Stream.of(\n        // With FromVersion: start with BASE_INDEX, no endVersion\n        Arguments.of(\n            0L, BASE_INDEX, notInitialSnapshot, noEndVersion, noEndIndex, \"With FromVersion 1\"),\n        Arguments.of(\n            3L, BASE_INDEX, notInitialSnapshot, noEndVersion, noEndIndex, \"With FromVersion 2\"),\n\n        // With FromIndex: start with specific fromIndex, no endVersion\n        Arguments.of(0L, 0L, notInitialSnapshot, noEndVersion, noEndIndex, \"With FromIndex 1\"),\n        Arguments.of(1L, 5L, notInitialSnapshot, noEndVersion, noEndIndex, \"With FromIndex 2\"),\n\n        // With EndVersion\n        Arguments.of(\n            1L, BASE_INDEX, notInitialSnapshot, Optional.of(3L), noEndIndex, \"With EndVersion 1\"),\n        Arguments.of(\n            1L,\n            BASE_INDEX,\n            notInitialSnapshot,\n            Optional.of(2L),\n            Optional.of(5L),\n            \"With EndVersion 2\"),\n        Arguments.of(\n            1L,\n            5L,\n            notInitialSnapshot,\n            Optional.of(3L),\n            Optional.of(END_INDEX),\n            \"With EndVersion 3\"),\n        Arguments.of(\n            1L,\n            END_INDEX,\n            notInitialSnapshot,\n            Optional.of(2L),\n            Optional.of(END_INDEX),\n            \"With EndVersion 4\"),\n\n        // Empty Range\n        Arguments.of(2L, 50L, notInitialSnapshot, Optional.of(2L), Optional.of(40L), \"Empty Range\"),\n\n        // Initial Snapshot: snapshot only (no subsequent delta changes to combine)\n        Arguments.of(\n            0L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            noEndVersion,\n            noEndIndex,\n            \"InitialSnapshot_Version0_NoDelta\"),\n        Arguments.of(\n            2L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            noEndVersion,\n            noEndIndex,\n            \"InitialSnapshot_Version2_NoDelta\"),\n\n        // Initial Snapshot: snapshot + delta changes (tests combine logic)\n        // Note: These assume the table has 5 versions (latest=5), so snapshot at version 0-2 will\n        // have delta changes to combine\n        Arguments.of(\n            0L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            Optional.of(5L),\n            Optional.of(END_INDEX),\n            \"InitialSnapshot_Version0_WithDelta_ToEnd\"),\n        Arguments.of(\n            1L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            Optional.of(3L),\n            Optional.of(END_INDEX),\n            \"InitialSnapshot_Version1_WithDelta_ToVersion3\"),\n        Arguments.of(\n            2L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            Optional.of(4L),\n            noEndIndex,\n            \"InitialSnapshot_Version2_WithDelta_ToVersion4\"),\n\n        // Initial Snapshot: with specific endIndex\n        Arguments.of(\n            0L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            Optional.of(2L),\n            Optional.of(5L),\n            \"InitialSnapshot_Version0_WithEndIndex\"),\n\n        // Initial Snapshot: at latest version (latestVersion == fromVersion, no delta to combine)\n        Arguments.of(\n            5L,\n            BASE_INDEX,\n            isInitialSnapshot,\n            noEndVersion,\n            noEndIndex,\n            \"InitialSnapshot_LatestVersion_NoDelta\"));\n  }\n\n  // ================================================================================================\n  // Tests for getFileChangesWithRateLimit parity between DSv1 and DSv2\n  // ================================================================================================\n\n  /**\n   * Test that verifies parity between DSv1 DeltaSource.getFileChangesWithRateLimit and DSv2\n   * SparkMicroBatchStream.getFileChangesWithRateLimit.\n   *\n   * <p>Tests both regular delta log streaming and initial snapshot scenarios with rate limiting.\n   */\n  @ParameterizedTest\n  @MethodSource(\"getFileChangesWithRateLimitParameters\")\n  public void testGetFileChangesWithRateLimit(\n      long fromVersion,\n      boolean isInitialSnapshot,\n      Optional<Integer> maxFiles,\n      Optional<Long> maxBytes,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_rate_limit_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 5 versions with 10 rows each (versions 1-5)\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ false);\n\n    // dsv1 DeltaSource\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n    DeltaOptions options = emptyDeltaOptions();\n\n    Optional<DeltaSource.AdmissionLimits> dsv1Limits =\n        createAdmissionLimits(deltaSource, maxFiles, maxBytes);\n\n    ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n        deltaSource.getFileChangesWithRateLimit(\n            fromVersion,\n            DeltaSourceOffset.BASE_INDEX(),\n            isInitialSnapshot,\n            ScalaUtils.toScalaOption(dsv1Limits));\n    List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n    while (deltaChanges.hasNext()) {\n      deltaFilesList.add(deltaChanges.next());\n    }\n    deltaChanges.close();\n\n    // dsv2 SparkMicroBatchStream\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    // We need a separate AdmissionLimits object for DSv2 because the method is stateful.\n    Optional<DeltaSource.AdmissionLimits> dsv2Limits =\n        createAdmissionLimits(deltaSource, maxFiles, maxBytes);\n\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChangesWithRateLimit(\n            fromVersion, DeltaSourceOffset.BASE_INDEX(), isInitialSnapshot, dsv2Limits)) {\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n      compareFileChanges(deltaFilesList, kernelFilesList);\n    }\n  }\n\n  /** Provides test parameters for the parameterized getFileChangesWithRateLimit test. */\n  private static Stream<Arguments> getFileChangesWithRateLimitParameters() {\n    boolean notInitialSnapshot = false;\n    boolean isInitialSnapshot = true;\n    Optional<Integer> noMaxFiles = Optional.empty();\n    Optional<Long> noMaxBytes = Optional.empty();\n\n    // Arguments: (fromVersion, isInitialSnapshot, maxFiles, maxBytes, testDescription)\n    return Stream.of(\n        // Regular delta log streaming (not initial snapshot)\n        Arguments.of(0L, notInitialSnapshot, noMaxFiles, noMaxBytes, \"DeltaLog_NoLimits\"),\n        Arguments.of(0L, notInitialSnapshot, Optional.of(5), noMaxBytes, \"DeltaLog_MaxFiles\"),\n        Arguments.of(0L, notInitialSnapshot, noMaxFiles, Optional.of(5000L), \"DeltaLog_MaxBytes\"),\n        Arguments.of(\n            0L,\n            notInitialSnapshot,\n            Optional.of(10),\n            Optional.of(10000L),\n            \"DeltaLog_MaxFilesAndMaxBytes\"),\n\n        // Initial snapshot with rate limiting\n        Arguments.of(\n            0L, isInitialSnapshot, noMaxFiles, noMaxBytes, \"InitialSnapshot_Version0_NoLimits\"),\n        Arguments.of(\n            0L, isInitialSnapshot, Optional.of(5), noMaxBytes, \"InitialSnapshot_Version0_MaxFiles\"),\n        Arguments.of(\n            0L,\n            isInitialSnapshot,\n            noMaxFiles,\n            Optional.of(5000L),\n            \"InitialSnapshot_Version0_MaxBytes\"),\n        Arguments.of(\n            0L,\n            isInitialSnapshot,\n            Optional.of(10),\n            Optional.of(10000L),\n            \"InitialSnapshot_Version0_MaxFilesAndMaxBytes\"),\n        Arguments.of(\n            2L, isInitialSnapshot, Optional.of(5), noMaxFiles, \"InitialSnapshot_Version2_MaxFiles\"),\n        Arguments.of(\n            2L,\n            isInitialSnapshot,\n            noMaxFiles,\n            Optional.of(3000L),\n            \"InitialSnapshot_Version2_MaxBytes\"));\n  }\n\n  private void compareFileChanges(\n      List<org.apache.spark.sql.delta.sources.IndexedFile> deltaSourceFiles,\n      List<IndexedFile> kernelFiles) {\n    assertEquals(\n        deltaSourceFiles.size(),\n        kernelFiles.size(),\n        String.format(\n            \"Number of file changes should match between dsv1 (%d) and dsv2 (%d)\",\n            deltaSourceFiles.size(), kernelFiles.size()));\n\n    for (int i = 0; i < deltaSourceFiles.size(); i++) {\n      org.apache.spark.sql.delta.sources.IndexedFile deltaFile = deltaSourceFiles.get(i);\n      IndexedFile kernelFile = kernelFiles.get(i);\n\n      assertEquals(\n          deltaFile.version(),\n          kernelFile.getVersion(),\n          String.format(\n              \"Version mismatch at index %d: dsv1=%d, dsv2=%d\",\n              i, deltaFile.version(), kernelFile.getVersion()));\n\n      assertEquals(\n          deltaFile.index(),\n          kernelFile.getIndex(),\n          String.format(\n              \"Index mismatch at index %d: dsv1=%d, dsv2=%d\",\n              i, deltaFile.index(), kernelFile.getIndex()));\n\n      // Sentinel files have null AddFile and null RemoveFile.\n      String deltaPath = deltaFile.add() != null ? deltaFile.add().path() : null;\n      String kernelPath =\n          kernelFile.getAddFile() != null ? kernelFile.getAddFile().getPath() : null;\n\n      if (deltaPath != null || kernelPath != null) {\n        assertEquals(\n            deltaPath,\n            kernelPath,\n            String.format(\n                \"AddFile path mismatch at index %d: dsv1=%s, dsv2=%s\", i, deltaPath, kernelPath));\n      }\n    }\n  }\n\n  // ================================================================================================\n  // Tests for commits with no data file changes\n  // ================================================================================================\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 handle commits with no ADD or REMOVE\n   * actions correctly. Such commits only contain METADATA, PROTOCOL, or other non-data changes.\n   */\n  @ParameterizedTest\n  @MethodSource(\"emptyVersionScenarios\")\n  public void testGetFileChanges_emptyVersions(\n      ScenarioSetup scenarioSetup,\n      List<Long> expectedEmptyVersions,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_empty_versions_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Execute the scenario-specific setup\n    scenarioSetup.setup(testTableName, tempDir);\n\n    // Read from version 0 (start of the table) to capture all changes\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    boolean isInitialSnapshot = false;\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n\n    // Test DSv1 DeltaSource\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n\n    ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n        deltaSource.getFileChanges(\n            fromVersion, fromIndex, isInitialSnapshot, endOffset, /* verifyMetadataAction= */ true);\n    List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n    while (deltaChanges.hasNext()) {\n      deltaFilesList.add(deltaChanges.next());\n    }\n    deltaChanges.close();\n\n    // Test DSv2 SparkMicroBatchStream\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(\n            fromVersion, fromIndex, isInitialSnapshot, ScalaUtils.toJavaOptional(endOffset))) {\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n\n      // Compare results\n      compareFileChanges(deltaFilesList, kernelFilesList);\n    }\n  }\n\n  /** Provides test scenarios with various types of empty versions (no ADD/REMOVE actions). */\n  private static Stream<Arguments> emptyVersionScenarios() {\n    return Stream.of(\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", tableName);\n                  sql(\"ALTER TABLE %s SET TBLPROPERTIES ('test.property' = 'value1')\", tableName);\n                  sql(\"INSERT INTO %s VALUES (3, 'User3')\", tableName);\n                },\n            Arrays.asList(2L),\n            \"Single metadata-only version\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1')\", tableName);\n                  sql(\"ALTER TABLE %s SET TBLPROPERTIES ('p1' = 'v1')\", tableName);\n                  sql(\"ALTER TABLE %s SET TBLPROPERTIES ('p2' = 'v2')\", tableName);\n                  sql(\"ALTER TABLE %s SET TBLPROPERTIES ('p3' = 'v3')\", tableName);\n                },\n            Arrays.asList(2L),\n            \"Multiple consecutive metadata-only versions\"));\n  }\n\n  @Test\n  public void testGetFileChanges_lazyLoading(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_lazy_loading_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 3 INSERT versions (versions 1-3 with ADD files only)\n    sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", testTableName);\n    sql(\"INSERT INTO %s VALUES (3, 'User3'), (4, 'User4')\", testTableName);\n    sql(\"INSERT INTO %s VALUES (5, 'User5'), (6, 'User6')\", testTableName);\n\n    // Version 4: DELETE operation that will create a REMOVE file\n    sql(\"DELETE FROM %s WHERE id = 1\", testTableName);\n\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n    // Get file changes from version 0 (which will include versions 1-4)\n    CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(\n            /* fromVersion= */ 0L,\n            /* fromIndex= */ DeltaSourceOffset.BASE_INDEX(),\n            /* isInitialSnapshot= */ false,\n            /* endOffset= */ Optional.empty());\n\n    try {\n      // Partially consume the iterator: only read files from versions 1-2\n      // With lazy loading, this should succeed without hitting the REMOVE file error in version 4\n      List<IndexedFile> partialFiles = new ArrayList<>();\n      int filesRead = 0;\n      int targetVersion = 2; // Only read up to version 2\n\n      while (kernelChanges.hasNext()) {\n        IndexedFile file = kernelChanges.next();\n        partialFiles.add(file);\n        filesRead++;\n\n        // Stop after we've passed version 2's END sentinel\n        // Each version has: BEGIN sentinel + actual files + END sentinel\n        if (file.getVersion() == targetVersion\n            && file.getIndex() == DeltaSourceOffset.END_INDEX()) {\n          break;\n        }\n      }\n\n      // If we got here, lazy loading worked - we successfully read versions 1-2 without\n      // encountering the REMOVE file error in version 4\n      // Version 0 (CREATE TABLE): BEGIN + 1 metadata/protocol action + END = 3 files\n      // Version 1 (INSERT): BEGIN + 1 data file + END = 3 files\n      // Version 2 (INSERT): BEGIN + 1 data file + END = 3 files\n      // Total = 9 files\n      assertEquals(9, filesRead, \"Should have read exactly 9 IndexedFiles from versions 0-2\");\n\n      // Now consume the rest of the iterator - this should hit the REMOVE file in version 4\n      assertThrows(\n          UnsupportedOperationException.class,\n          () -> {\n            while (kernelChanges.hasNext()) {\n              kernelChanges.next(); // This should throw when it reaches version 4's REMOVE\n            }\n          },\n          \"Should throw UnsupportedOperationException when reaching version 4 with REMOVE file\");\n    } finally {\n      try {\n        kernelChanges.close();\n      } catch (Exception ignored) {\n      }\n    }\n  }\n\n  // ================================================================================================\n  // Tests for REMOVE file handling\n  // ================================================================================================\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 throw UnsupportedOperationException when\n   * encountering REMOVE actions (from DELETE, UPDATE, MERGE operations).\n   */\n  @ParameterizedTest\n  @MethodSource(\"removeFileScenarios\")\n  public void testGetFileChanges_onRemoveFile_throwError(\n      ScenarioSetup scenarioSetup, String testDescription, @TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_remove_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Execute the scenario-specific setup (which will generate REMOVE actions)\n    scenarioSetup.setup(testTableName, tempDir);\n\n    // Try to read from version 0, which should include commits with REMOVE actions\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    boolean isInitialSnapshot = false;\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n\n    // Test DSv1 DeltaSource\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n\n    AtomicInteger dsv1SuccessfulCalls = new AtomicInteger(0);\n    assertThrows(\n        UnsupportedOperationException.class,\n        () -> {\n          ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n              deltaSource.getFileChanges(\n                  fromVersion,\n                  fromIndex,\n                  isInitialSnapshot,\n                  endOffset,\n                  /* verifyMetadataAction= */ true);\n          try {\n            while (deltaChanges.hasNext()) {\n              deltaChanges.next(); // Should throw when hitting REMOVE file\n              dsv1SuccessfulCalls.incrementAndGet();\n            }\n          } finally {\n            deltaChanges.close();\n          }\n        },\n        String.format(\"DSv1 should throw on REMOVE for scenario: %s\", testDescription));\n\n    // Test DSv2 SparkMicroBatchStream\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n    AtomicInteger dsv2SuccessfulCalls = new AtomicInteger(0);\n    assertThrows(\n        UnsupportedOperationException.class,\n        () -> {\n          CloseableIterator<IndexedFile> kernelChanges =\n              stream.getFileChanges(\n                  fromVersion, fromIndex, isInitialSnapshot, ScalaUtils.toJavaOptional(endOffset));\n          try {\n            while (kernelChanges.hasNext()) {\n              kernelChanges.next(); // Should throw when hitting REMOVE file\n              dsv2SuccessfulCalls.incrementAndGet();\n            }\n          } finally {\n            kernelChanges.close();\n          }\n        },\n        String.format(\"DSv2 should throw on REMOVE for scenario: %s\", testDescription));\n\n    // Verify both threw at the exact same point\n    assertEquals(\n        dsv1SuccessfulCalls.get(),\n        dsv2SuccessfulCalls.get(),\n        String.format(\n            \"DSv1 and DSv2 should throw after the same number of next() calls for scenario: %s. \"\n                + \"DSv1=%d, DSv2=%d\",\n            testDescription, dsv1SuccessfulCalls.get(), dsv2SuccessfulCalls.get()));\n  }\n\n  /** Provides test scenarios that generate REMOVE actions through various DML operations. */\n  private static Stream<Arguments> removeFileScenarios() {\n    return Stream.of(\n        // Simple DELETE scenario\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", tableName);\n                  sql(\"INSERT INTO %s VALUES (3, 'User3'), (4, 'User4')\", tableName);\n                  sql(\"DELETE FROM %s WHERE id = 1\", tableName);\n                },\n            \"DELETE: Simple delete\"),\n\n        // Many ADDs followed by REMOVE\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  // Create 10 versions with ADDs (50 rows each)\n                  for (int i = 0; i < 10; i++) {\n                    StringBuilder values = new StringBuilder();\n                    for (int j = 0; j < 50; j++) {\n                      if (j > 0) values.append(\", \");\n                      int id = i * 50 + j;\n                      values.append(String.format(\"(%d, 'User%d')\", id, id));\n                    }\n                    sql(\"INSERT INTO %s VALUES %s\", tableName, values);\n                  }\n                  sql(\"DELETE FROM %s WHERE id < 100\", tableName);\n                },\n            \"DELETE: Many ADDs (10 versions) followed by REMOVE\"),\n\n        // UPDATE scenario (generates REMOVE + ADD pairs)\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\n                      \"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2'), (3, 'User3'), (4,\"\n                          + \" 'User4'), (5, 'User5')\",\n                      tableName);\n                  sql(\"INSERT INTO %s VALUES (6, 'User6'), (7, 'User7'), (8, 'User8')\", tableName);\n                  sql(\"UPDATE %s SET name = 'UpdatedUser' WHERE id <= 3\", tableName);\n                },\n            \"UPDATE: Update multiple rows (generates REMOVE + ADD)\"),\n\n        // MERGE scenario (generates REMOVE + ADD for matched, ADD for not matched)\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2'), (3, 'User3')\", tableName);\n\n                  // Create a source table for MERGE\n                  String sourceTableName = \"merge_source_\" + System.nanoTime();\n                  sql(\n                      \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\",\n                      sourceTableName, new File(tempDir, \"source\").getAbsolutePath());\n                  sql(\"INSERT INTO %s VALUES (2, 'UpdatedUser2'), (4, 'User4')\", sourceTableName);\n\n                  // Perform MERGE operation\n                  sql(\n                      \"MERGE INTO %s AS target USING %s AS source ON target.id = source.id WHEN\"\n                          + \" MATCHED THEN UPDATE SET target.name = source.name WHEN NOT MATCHED\"\n                          + \" THEN INSERT (id, name) VALUES (source.id, source.name)\",\n                      tableName, sourceTableName);\n\n                  sql(\"DROP TABLE IF EXISTS %s\", sourceTableName);\n                },\n            \"MERGE: Matched (REMOVE+ADD) and not matched (ADD)\"),\n\n        // Full table delete (only RemoveFiles, no AddFiles)\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", tableName);\n                  sql(\"DELETE FROM %s\", tableName);\n                },\n            \"DELETE: Full table delete (removes only, no adds)\"));\n  }\n\n  // ================================================================================================\n  // Tests for ignoreDeletes parity between DSv1 and DSv2\n  // ================================================================================================\n\n  /**\n   * Verifies that with ignoreDeletes=true, both DSv1 and DSv2 produce the same file changes for\n   * delete-only commits (commits with only RemoveFile actions, no AddFile actions). The delete-only\n   * commit should be silently skipped (only sentinels emitted, no data files).\n   */\n  @ParameterizedTest\n  @MethodSource(\"deleteOnlyScenarios\")\n  public void testGetFileChanges_withIgnoreDeletes_deleteOnlyParity(\n      ScenarioSetup scenarioSetup,\n      boolean isInitialSnapshot,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_ignore_deletes_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyPartitionedTestTable(testTablePath, testTableName);\n\n    scenarioSetup.setup(testTableName, tempDir);\n\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    DeltaOptions options = createDeltaOptions(\"ignoreDeletes\", \"true\");\n\n    // DSv1\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath, options);\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n    ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n        deltaSource.getFileChanges(\n            fromVersion, fromIndex, isInitialSnapshot, endOffset, /* verifyMetadataAction= */ true);\n    List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n    while (deltaChanges.hasNext()) {\n      deltaFilesList.add(deltaChanges.next());\n    }\n    deltaChanges.close();\n\n    // DSv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, options);\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(fromVersion, fromIndex, isInitialSnapshot, Optional.empty())) {\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n      compareFileChanges(deltaFilesList, kernelFilesList);\n    }\n  }\n\n  /**\n   * Verifies that with ignoreDeletes=true, both DSv1 and DSv2 still throw on commits containing\n   * both adds and removes (e.g., UPDATE, MERGE), since ignoreDeletes only suppresses delete-only\n   * commits.\n   */\n  @ParameterizedTest\n  @MethodSource(\"changeCommitScenarios\")\n  public void testGetFileChanges_withIgnoreDeletes_changeCommitStillThrows(\n      ScenarioSetup scenarioSetup,\n      boolean isInitialSnapshot,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_ignore_deletes_change_\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    scenarioSetup.setup(testTableName, tempDir);\n\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n    DeltaOptions options = createDeltaOptions(\"ignoreDeletes\", \"true\");\n\n    // DSv1\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath, options);\n\n    AtomicInteger dsv1SuccessfulCalls = new AtomicInteger(0);\n    assertThrows(\n        UnsupportedOperationException.class,\n        () -> {\n          ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n              deltaSource.getFileChanges(\n                  fromVersion,\n                  fromIndex,\n                  isInitialSnapshot,\n                  endOffset,\n                  /* verifyMetadataAction= */ true);\n          try {\n            while (deltaChanges.hasNext()) {\n              deltaChanges.next();\n              dsv1SuccessfulCalls.incrementAndGet();\n            }\n          } finally {\n            deltaChanges.close();\n          }\n        },\n        String.format(\n            \"DSv1 should throw on change commit with ignoreDeletes for: %s\", testDescription));\n\n    // DSv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, options);\n\n    AtomicInteger dsv2SuccessfulCalls = new AtomicInteger(0);\n    assertThrows(\n        UnsupportedOperationException.class,\n        () -> {\n          CloseableIterator<IndexedFile> kernelChanges =\n              stream.getFileChanges(fromVersion, fromIndex, isInitialSnapshot, Optional.empty());\n          try {\n            while (kernelChanges.hasNext()) {\n              kernelChanges.next();\n              dsv2SuccessfulCalls.incrementAndGet();\n            }\n          } finally {\n            kernelChanges.close();\n          }\n        },\n        String.format(\n            \"DSv2 should throw on change commit with ignoreDeletes for: %s\", testDescription));\n\n    assertEquals(\n        dsv1SuccessfulCalls.get(),\n        dsv2SuccessfulCalls.get(),\n        String.format(\n            \"DSv1 and DSv2 should throw after the same number of next() calls for: %s. \"\n                + \"DSv1=%d, DSv2=%d\",\n            testDescription, dsv1SuccessfulCalls.get(), dsv2SuccessfulCalls.get()));\n  }\n\n  // ================================================================================================\n  // Tests for skipChangeCommits parity between DSv1 and DSv2\n  // ================================================================================================\n\n  /**\n   * Verifies that with skipChangeCommits=true, both DSv1 and DSv2 produce the same file changes for\n   * delete-only commits. Since skipChangeCommits suppresses all commits containing RemoveFile\n   * actions, these commits should be silently skipped (only sentinels emitted, no data files).\n   */\n  @ParameterizedTest\n  @MethodSource(\"deleteOnlyScenarios\")\n  public void testGetFileChanges_withSkipChangeCommits_deleteOnlyParity(\n      ScenarioSetup scenarioSetup,\n      boolean isInitialSnapshot,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_skip_change_del_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyPartitionedTestTable(testTablePath, testTableName);\n\n    scenarioSetup.setup(testTableName, tempDir);\n\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    DeltaOptions options = createDeltaOptions(\"skipChangeCommits\", \"true\");\n\n    // DSv1\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath, options);\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n    ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n        deltaSource.getFileChanges(\n            fromVersion, fromIndex, isInitialSnapshot, endOffset, /* verifyMetadataAction= */ true);\n    List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n    while (deltaChanges.hasNext()) {\n      deltaFilesList.add(deltaChanges.next());\n    }\n    deltaChanges.close();\n\n    // DSv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, options);\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(fromVersion, fromIndex, isInitialSnapshot, Optional.empty())) {\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n      compareFileChanges(deltaFilesList, kernelFilesList);\n    }\n  }\n\n  /**\n   * Verifies that with skipChangeCommits=true, both DSv1 and DSv2 silently skip commits containing\n   * both adds and removes (e.g., UPDATE, MERGE), instead of throwing. This is the key behavioral\n   * difference from ignoreDeletes, which throws on such commits.\n   */\n  @ParameterizedTest\n  @MethodSource(\"changeCommitScenarios\")\n  public void testGetFileChanges_withSkipChangeCommits_changeCommitParity(\n      ScenarioSetup scenarioSetup,\n      boolean isInitialSnapshot,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_skip_change_chg_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    scenarioSetup.setup(testTableName, tempDir);\n\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    DeltaOptions options = createDeltaOptions(\"skipChangeCommits\", \"true\");\n\n    // DSv1\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath, options);\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n    ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n        deltaSource.getFileChanges(\n            fromVersion, fromIndex, isInitialSnapshot, endOffset, /* verifyMetadataAction= */ true);\n    List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n    while (deltaChanges.hasNext()) {\n      deltaFilesList.add(deltaChanges.next());\n    }\n    deltaChanges.close();\n\n    // DSv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, options);\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(fromVersion, fromIndex, isInitialSnapshot, Optional.empty())) {\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n      compareFileChanges(deltaFilesList, kernelFilesList);\n    }\n  }\n\n  // ================================================================================================\n  // Shared scenario providers for ignoreDeletes and skipChangeCommits tests\n  // ================================================================================================\n\n  /**\n   * Provides delete-only scenarios: commits with only RemoveFile actions and no AddFile actions.\n   * Used by both ignoreDeletes and skipChangeCommits tests.\n   *\n   * <p>Arguments: (ScenarioSetup, isInitialSnapshot, testDescription)\n   */\n  private static Stream<Arguments> deleteOnlyScenarios() {\n    return Stream.of(\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", tableName);\n                  sql(\"INSERT INTO %s VALUES (3, 'User3'), (4, 'User4')\", tableName);\n                  sql(\"DELETE FROM %s\", tableName);\n                },\n            false,\n            \"Full table delete\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", tableName);\n                  sql(\"DELETE FROM %s\", tableName);\n                  sql(\"INSERT INTO %s VALUES (3, 'User3'), (4, 'User4')\", tableName);\n                },\n            false,\n            \"Insert-Delete-Insert: data resumes after delete\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'PartA'), (2, 'PartA'), (3, 'PartB')\", tableName);\n                  sql(\"DELETE FROM %s WHERE name = 'PartA'\", tableName);\n                },\n            false,\n            \"Partitioned table: delete entire partition\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\n                      \"ALTER TABLE %s SET TBLPROPERTIES ('delta.enableDeletionVectors' = true)\",\n                      tableName);\n                  sql(\n                      \"INSERT INTO %s SELECT /*+ COALESCE(1) */ * FROM VALUES \"\n                          + \"(1, 'User1'), (2, 'User2'), (3, 'User3') AS t(id, name)\",\n                      tableName);\n                  sql(\"DELETE FROM %s WHERE id >= 1\", tableName);\n                },\n            false,\n            \"Full DELETE with DV: full file delete via WHERE clause\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2')\", tableName);\n                  sql(\"INSERT INTO %s VALUES (3, 'User3'), (4, 'User4')\", tableName);\n                  sql(\"DELETE FROM %s\", tableName);\n                },\n            true,\n            \"Full table delete with initial snapshot\"));\n  }\n\n  /**\n   * Provides change-commit scenarios: commits containing both AddFile and RemoveFile actions (e.g.,\n   * UPDATE, MERGE). Used by both ignoreDeletes and skipChangeCommits tests — ignoreDeletes expects\n   * these to throw, while skipChangeCommits expects them to be silently skipped.\n   *\n   * <p>Arguments: (ScenarioSetup, isInitialSnapshot, testDescription)\n   */\n  private static Stream<Arguments> changeCommitScenarios() {\n    return Stream.of(\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2'), (3, 'User3')\", tableName);\n                  sql(\"UPDATE %s SET name = 'Updated' WHERE id = 1\", tableName);\n                },\n            false,\n            \"UPDATE: AddFile + RemoveFile\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2'), (3, 'User3')\", tableName);\n                  String sourceTableName = \"merge_src_\" + System.nanoTime();\n                  sql(\n                      \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\",\n                      sourceTableName, new File(tempDir, \"source\").getAbsolutePath());\n                  sql(\"INSERT INTO %s VALUES (2, 'UpdatedUser2'), (4, 'User4')\", sourceTableName);\n                  sql(\n                      \"MERGE INTO %s AS target USING %s AS source ON target.id = source.id \"\n                          + \"WHEN MATCHED THEN UPDATE SET target.name = source.name \"\n                          + \"WHEN NOT MATCHED THEN INSERT (id, name) \"\n                          + \"VALUES (source.id, source.name)\",\n                      tableName, sourceTableName);\n                  sql(\"DROP TABLE IF EXISTS %s\", sourceTableName);\n                },\n            false,\n            \"MERGE: AddFile + RemoveFile\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\n                      \"ALTER TABLE %s SET TBLPROPERTIES ('delta.enableDeletionVectors' = true)\",\n                      tableName);\n                  // Coalesce to to ensure DV is partial delete\n                  sql(\n                      \"INSERT INTO %s SELECT /*+ COALESCE(1) */ * FROM VALUES \"\n                          + \"(1, 'User1'), (2, 'User2'), (3, 'User3') AS t(id, name)\",\n                      tableName);\n                  sql(\"DELETE FROM %s WHERE id = 1\", tableName);\n                },\n            false,\n            \"Partial DELETE with DV: AddFile(with DV) + RemoveFile\"),\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"INSERT INTO %s VALUES (1, 'User1'), (2, 'User2'), (3, 'User3')\", tableName);\n                  sql(\"UPDATE %s SET name = 'Updated' WHERE id = 1\", tableName);\n                },\n            true,\n            \"UPDATE with initial snapshot: AddFile + RemoveFile\"));\n  }\n\n  @Test\n  public void testGetFileChanges_startingVersionAfterCheckpointAndLogCleanup(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_checkpoint_cleanup_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Insert 5 versions\n    for (int i = 1; i <= 5; i++) {\n      sql(\"INSERT INTO %s VALUES (%d, 'User%d')\", testTableName, i, i);\n    }\n\n    // Create checkpoint at version 5\n    DeltaLog.forTable(spark, new Path(testTablePath)).checkpoint();\n\n    // Delete 0.json to simulate log cleanup\n    Path logPath = new Path(testTablePath, \"_delta_log\");\n    Path logFile0 = new Path(logPath, \"00000000000000000000.json\");\n    File file0 = new File(logFile0.toUri().getPath());\n    if (file0.exists()) {\n      file0.delete();\n    }\n\n    // Now test with startingVersion=1\n    Configuration hadoopConf = spark.sessionState().newHadoopConf();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n    // Get file changes from version 1 onwards\n    try (CloseableIterator<IndexedFile> kernelChanges =\n        stream.getFileChanges(\n            /* fromVersion= */ 1L,\n            /* fromIndex= */ DeltaSourceOffset.BASE_INDEX(),\n            /* isInitialSnapshot= */ false,\n            /* endOffset= */ Optional.empty())) {\n\n      List<IndexedFile> kernelFilesList = new ArrayList<>();\n      while (kernelChanges.hasNext()) {\n        kernelFilesList.add(kernelChanges.next());\n      }\n\n      // Filter to get only actual data files (addFile != null)\n      long actualFileCount = kernelFilesList.stream().filter(f -> f.getAddFile() != null).count();\n\n      // Should be able to read 5 data files from versions 1-5\n      assertEquals(\n          5,\n          actualFileCount,\n          \"Should read 5 data files from versions 1-5 even though version 0 log is deleted\");\n    }\n  }\n\n  // ================================================================================================\n  // Tests for latestOffset parity between DSv1 and DSv2\n  // ================================================================================================\n\n  /**\n   * Parameterized test that verifies parity between DSv1 DeltaSource.latestOffset and DSv2\n   * SparkMicroBatchStream.latestOffset.\n   */\n  @ParameterizedTest\n  @MethodSource(\"latestOffsetParameters\")\n  public void testLatestOffset_notInitialSnapshot(\n      Long startVersion,\n      Long startIndex,\n      ReadLimitConfig limitConfig,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_latest_offset_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ true);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    String tableId = deltaLog.tableId();\n    Offset startOffset =\n        new DeltaSourceOffset(tableId, startVersion, startIndex, /* isInitialSnapshot= */ false);\n    ReadLimit readLimit = limitConfig.toReadLimit();\n\n    // dsv1\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n    Offset v1EndOffset = deltaSource.latestOffset(startOffset, readLimit);\n\n    // dsv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    Offset v2EndOffset = stream.latestOffset(startOffset, readLimit);\n\n    compareOffsets(v1EndOffset, v2EndOffset, testDescription);\n  }\n\n  /** Provides test parameters for the parameterized latestOffset test. */\n  private static Stream<Arguments> latestOffsetParameters() {\n    long BASE_INDEX = DeltaSourceOffset.BASE_INDEX();\n    long END_INDEX = DeltaSourceOffset.END_INDEX();\n\n    // TODO(#5318): Add tests for initial offset & latestOffset(null, ReadLimit)\n    return Stream.of(\n        // No limits\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.noLimit(),\n            \"NoLimits1\"),\n        Arguments.of(\n            /* startVersion= */ 2L, /* startIndex= */ 5L, ReadLimitConfig.noLimit(), \"NoLimits2\"),\n        Arguments.of(\n            /* startVersion= */ 3L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.noLimit(),\n            \"NoLimits3\"),\n        Arguments.of(\n            /* startVersion= */ 5L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.noLimit(),\n            \"NoLimits4\"),\n\n        // Max files\n        Arguments.of(\n            /* startVersion= */ 3L,\n            /* startIndex= */ 5L,\n            ReadLimitConfig.maxFiles(10),\n            \"MaxFiles1\"),\n        Arguments.of(\n            /* startVersion= */ 4L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(5),\n            \"MaxFiles2\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.maxFiles(1),\n            \"MaxFiles3\"),\n        Arguments.of(\n            /* startVersion= */ 5L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.maxFiles(1),\n            \"MaxFiles4\"),\n\n        // Max bytes\n        Arguments.of(\n            /* startVersion= */ 3L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxBytes(1000),\n            \"MaxBytes1\"),\n        Arguments.of(\n            /* startVersion= */ 3L,\n            /* startIndex= */ 5L,\n            ReadLimitConfig.maxBytes(1000),\n            \"MaxBytes2\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.maxBytes(1000),\n            \"MaxBytes3\"),\n        Arguments.of(\n            /* startVersion= */ 5L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.maxBytes(1000),\n            \"MaxBytes4\"));\n  }\n\n  /**\n   * Parameterized test that verifies sequential batch advancement produces identical offset\n   * sequences for DSv1 and DSv2. This simulates real streaming where latestOffset is called\n   * multiple times, each using the previous offset as the starting point.\n   */\n  @ParameterizedTest\n  @MethodSource(\"sequentialBatchAdvancementParameters\")\n  public void testLatestOffset_sequentialBatchAdvancement(\n      long startVersion,\n      long startIndex,\n      ReadLimitConfig limitConfig,\n      int numIterations,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_sequential_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ true);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    String tableId = deltaLog.tableId();\n\n    DeltaSourceOffset startOffset =\n        new DeltaSourceOffset(tableId, startVersion, startIndex, /* isInitialSnapshot= */ false);\n\n    // dsv1\n    ReadLimit readLimit = limitConfig.toReadLimit();\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n    List<Offset> dsv1Offsets =\n        advanceOffsetSequenceDsv1(deltaSource, startOffset, numIterations, readLimit);\n\n    // dsv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    List<Offset> dsv2Offsets =\n        advanceOffsetSequenceDsv2(stream, startOffset, numIterations, readLimit);\n\n    compareOffsetSequence(dsv1Offsets, dsv2Offsets, testDescription);\n  }\n\n  /** Provides test parameters for sequential batch advancement test. */\n  private static Stream<Arguments> sequentialBatchAdvancementParameters() {\n    long BASE_INDEX = DeltaSourceOffset.BASE_INDEX();\n    long END_INDEX = DeltaSourceOffset.END_INDEX();\n\n    return Stream.of(\n        // No limits\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits1\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits2\"),\n        Arguments.of(\n            /* startVersion= */ 4L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits3\"),\n        Arguments.of(\n            /* startVersion= */ 4L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits4\"),\n\n        // Max files\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(5),\n            /* numIterations= */ 5,\n            \"MaxFiles1\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(5),\n            /* numIterations= */ 3,\n            \"MaxFiles2\"),\n        Arguments.of(\n            /* startVersion= */ 4L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.maxFiles(1),\n            /* numIterations= */ 10,\n            \"MaxFiles3\"),\n        // Max bytes\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxBytes(1000),\n            /* numIterations= */ 3,\n            \"MaxBytes1\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ 5L,\n            ReadLimitConfig.maxBytes(1000),\n            /* numIterations= */ 3,\n            \"MaxBytes2\"),\n        Arguments.of(\n            /* startVersion= */ 4L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.maxBytes(1000),\n            /* numIterations= */ 3,\n            \"MaxBytes3\"));\n  }\n\n  /**\n   * Parameterized test that verifies behavior when calling latestOffset but no new data is\n   * available (we're already at the latest version).\n   */\n  @ParameterizedTest\n  @MethodSource(\"noNewDataAtLatestVersionParameters\")\n  public void testLatestOffset_noNewDataAtLatestVersion(\n      long startIndex,\n      Long expectedVersionOffset,\n      Long expectedIndex,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_no_new_data_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 1,\n        /* includeEmptyVersion= */ false);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    String tableId = deltaLog.tableId();\n    long latestVersion =\n        deltaLog\n            .update(\n                /* isForce= */ false,\n                /* timestamp= */ scala.Option.empty(),\n                /* version= */ scala.Option.empty())\n            .version();\n\n    DeltaSourceOffset startOffset =\n        new DeltaSourceOffset(tableId, latestVersion, startIndex, /* isInitialSnapshot= */ false);\n    ReadLimit readLimit = ReadLimit.allAvailable();\n\n    // dsv1\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n    org.apache.spark.sql.connector.read.streaming.Offset dsv1Offset =\n        deltaSource.latestOffset(startOffset, readLimit);\n\n    // dsv2\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    Offset dsv2Offset = stream.latestOffset(startOffset, readLimit);\n\n    compareOffsets(dsv1Offset, dsv2Offset, testDescription);\n\n    // Verify expected offset\n    if (expectedVersionOffset == null) {\n      assertNull(\n          dsv1Offset,\n          String.format(\n              \"Test: %s | Expected null offset but got: %s\", testDescription, dsv1Offset));\n    } else {\n      assertNotNull(\n          dsv1Offset,\n          String.format(\"Test: %s | Expected non-null offset but got null\", testDescription));\n      DeltaSourceOffset dsv1DeltaOffset = (DeltaSourceOffset) dsv1Offset;\n      long expectedVersion = latestVersion + expectedVersionOffset;\n      assertEquals(\n          expectedVersion,\n          dsv1DeltaOffset.reservoirVersion(),\n          String.format(\n              \"Test: %s | Expected version: %d, Actual version: %d\",\n              testDescription, expectedVersion, dsv1DeltaOffset.reservoirVersion()));\n      assertEquals(\n          expectedIndex,\n          dsv1DeltaOffset.index(),\n          String.format(\n              \"Test: %s | Expected index: %d, Actual index: %d\",\n              testDescription, expectedIndex, dsv1DeltaOffset.index()));\n    }\n  }\n\n  /** Provides test parameters for no new data at latest version test. */\n  private static Stream<Arguments> noNewDataAtLatestVersionParameters() {\n    long BASE_INDEX = DeltaSourceOffset.BASE_INDEX();\n    long END_INDEX = DeltaSourceOffset.END_INDEX();\n\n    // Arguments: (startIndex, expectedVersionOffset, expectedIndex, testDescription)\n    // expectedVersionOffset is relative to latestVersion (null means expect null offset)\n    return Stream.of(\n        Arguments.of(BASE_INDEX, 1L, BASE_INDEX, \"Latest version BASE_INDEX, no new data\"),\n        Arguments.of(END_INDEX, 0L, END_INDEX, \"Latest version END_INDEX, no new data\"),\n        Arguments.of(0L, 1L, BASE_INDEX, \"Latest version index=0, no new data\"));\n  }\n\n  // ================================================================================================\n  // Tests for availableNow parity between DSv1 and DSv2\n  // ================================================================================================\n\n  @ParameterizedTest\n  @MethodSource(\"availableNowParameters\")\n  public void testAvailableNow_SequentialBatchAdvancement(\n      Long startVersion,\n      Long startIndex,\n      ReadLimitConfig limitConfig,\n      int numIterations,\n      String testDescription,\n      @TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_availableNow_sequential\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ true);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    String tableId = deltaLog.tableId();\n\n    DeltaSourceOffset startOffset =\n        new DeltaSourceOffset(tableId, startVersion, startIndex, /* isInitialSnapshot= */ false);\n    ReadLimit readLimit = limitConfig.toReadLimit();\n\n    // dsv1 source\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n    // Enable availableNow\n    deltaSource.prepareForTriggerAvailableNow();\n    // Advance through multiple batches using dsv1, collecting offset after each batch\n    List<Offset> dsv1Offsets =\n        advanceOffsetSequenceDsv1(deltaSource, startOffset, numIterations, readLimit);\n\n    // dsv2 source\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    // Enable availableNow\n    stream.prepareForTriggerAvailableNow();\n    // Advance through multiple batches using dsv2, collecting offset after each batch\n    List<Offset> dsv2Offsets =\n        advanceOffsetSequenceDsv2(stream, startOffset, numIterations, readLimit);\n\n    // Ensure dsv1 and dsv2 produce identical offset sequences\n    compareOffsetSequence(dsv1Offsets, dsv2Offsets, testDescription);\n  }\n\n  private static Stream<Arguments> availableNowParameters() {\n    long BASE_INDEX = DeltaSourceOffset.BASE_INDEX();\n    long END_INDEX = DeltaSourceOffset.END_INDEX();\n\n    return Stream.of(\n        // No limits respects availableNow\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits1\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits2\"),\n        Arguments.of(\n            /* startVersion= */ 4L,\n            /* startIndex= */ END_INDEX,\n            ReadLimitConfig.noLimit(),\n            /* numIterations= */ 3,\n            \"NoLimits3\"),\n\n        // Max files respects availableNow\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(1),\n            /* numIterations= */ 10,\n            \"MaxFiles1\"),\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(1000),\n            /* numIterations= */ 3,\n            \"MaxFiles2\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(2),\n            /* numIterations= */ 10,\n            \"MaxFiles3\"),\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxFiles(0),\n            /* numIterations= */ 3,\n            \"MaxFiles4\"),\n\n        // Max bytes respects availableNow\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxBytes(1),\n            /* numIterations= */ 100,\n            \"MaxBytes1\"),\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxBytes(1000000), // ensure larger than total file size\n            /* numIterations= */ 3,\n            \"MaxBytes2\"),\n        Arguments.of(\n            /* startVersion= */ 1L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxBytes(1000),\n            /* numIterations= */ 100,\n            \"MaxBytes3\"),\n        Arguments.of(\n            /* startVersion= */ 0L,\n            /* startIndex= */ BASE_INDEX,\n            ReadLimitConfig.maxBytes(0),\n            /* numIterations= */ 3,\n            \"MaxBytes4\"));\n  }\n\n  // ================================================================================================\n  // Tests for planInputPartitions\n  // ================================================================================================\n\n  @ParameterizedTest\n  @MethodSource(\"planInputPartitionsParameters\")\n  public void testPlanInputPartitions_dataParity(\n      long fromVersion,\n      long toVersion,\n      Optional<Integer> maxFiles,\n      Optional<Long> maxBytes,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_plan_partitions_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 10,\n        /* includeEmptyVersion= */ true);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSourceOffset startOffset =\n        new DeltaSourceOffset(\n            deltaLog.tableId(),\n            fromVersion,\n            DeltaSourceOffset.BASE_INDEX(),\n            /* isInitialSnapshot= */ false);\n    DeltaSourceOffset planPartitionsEndOffset =\n        new DeltaSourceOffset(\n            deltaLog.tableId(),\n            toVersion,\n            DeltaSourceOffset.END_INDEX(),\n            /* isInitialSnapshot= */ false);\n\n    // Ground truth: Read directly from Delta table\n    List<Row> expectedRows = new ArrayList<>();\n    Dataset<Row> toVersionData =\n        spark.read().format(\"delta\").option(\"versionAsOf\", toVersion).load(testTablePath);\n    if (fromVersion > 0) {\n      Dataset<Row> beforeFromVersionData =\n          spark.read().format(\"delta\").option(\"versionAsOf\", fromVersion - 1).load(testTablePath);\n      toVersionData = toVersionData.except(beforeFromVersionData);\n    }\n    expectedRows.addAll(toVersionData.collectAsList());\n\n    // DSv2: planInputPartitions + createReaderFactory\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    org.apache.spark.sql.delta.Snapshot deltaSnapshot = deltaLog.unsafeVolatileSnapshot();\n    StructType dataSchema = deltaSnapshot.metadata().schema();\n    StructType partitionSchema = deltaSnapshot.metadata().partitionSchema();\n\n    SparkMicroBatchStream stream =\n        new SparkMicroBatchStream(\n            snapshotManager,\n            snapshotManager.loadLatestSnapshot(),\n            spark.sessionState().newHadoopConf(),\n            spark,\n            emptyDeltaOptions(),\n            testTablePath,\n            dataSchema,\n            partitionSchema,\n            dataSchema,\n            new org.apache.spark.sql.sources.Filter[0],\n            Map$.MODULE$.empty());\n\n    InputPartition[] partitions = stream.planInputPartitions(startOffset, planPartitionsEndOffset);\n    PartitionReaderFactory readerFactory = stream.createReaderFactory();\n\n    // Simulates how Spark calls the reader factory and reads the data\n    List<Row> dsv2Rows = new ArrayList<>();\n    for (InputPartition partition : partitions) {\n      if (readerFactory.supportColumnarReads(partition)) {\n        PartitionReader<org.apache.spark.sql.vectorized.ColumnarBatch> reader =\n            readerFactory.createColumnarReader(partition);\n        while (reader.next()) {\n          org.apache.spark.sql.vectorized.ColumnarBatch batch = reader.get();\n          // Convert ColumnarBatch to Rows\n          org.apache.spark.sql.catalyst.expressions.UnsafeProjection projection =\n              org.apache.spark.sql.catalyst.expressions.UnsafeProjection.create(dataSchema);\n          for (int rowId = 0; rowId < batch.numRows(); rowId++) {\n            InternalRow internalRow = batch.getRow(rowId);\n            Row row = convertInternalRowToRow(internalRow, dataSchema);\n            dsv2Rows.add(row);\n          }\n        }\n        reader.close();\n      } else {\n        PartitionReader<InternalRow> reader = readerFactory.createReader(partition);\n        while (reader.next()) {\n          InternalRow internalRow = reader.get();\n          // Convert InternalRow to Row for comparison using the dataSchema we already have\n          Row row = convertInternalRowToRow(internalRow, dataSchema);\n          dsv2Rows.add(row);\n        }\n        reader.close();\n      }\n    }\n\n    // Compare results\n    compareDataResults(expectedRows, dsv2Rows, testDescription);\n  }\n\n  /** Provides test parameters for the planInputPartitions data parity test. */\n  private static Stream<Arguments> planInputPartitionsParameters() {\n    Optional<Integer> noMaxFiles = Optional.empty();\n    Optional<Long> noMaxBytes = Optional.empty();\n\n    return Stream.of(\n        // Basic version range tests\n        Arguments.of(\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 2L,\n            noMaxFiles,\n            noMaxBytes,\n            \"Single version (1 to 2)\"),\n        Arguments.of(\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 3L,\n            noMaxFiles,\n            noMaxBytes,\n            \"Multiple versions (1 to 3)\"),\n        Arguments.of(\n            /* fromVersion= */ 0L,\n            /* toVersion= */ 5L,\n            noMaxFiles,\n            noMaxBytes,\n            \"From version 0 to 5\"),\n        Arguments.of(\n            /* fromVersion= */ 2L,\n            /* toVersion= */ 4L,\n            noMaxFiles,\n            noMaxBytes,\n            \"Mid-range versions (2 to 4)\"),\n\n        // Rate limiting tests\n        Arguments.of(\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 5L,\n            Optional.of(5),\n            noMaxFiles,\n            \"With maxFiles limit\"),\n        Arguments.of(\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 5L,\n            noMaxFiles,\n            Optional.of(5000L),\n            \"With maxBytes limit\"),\n        Arguments.of(\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 5L,\n            Optional.of(10),\n            Optional.of(10000L),\n            \"With both limits\"),\n\n        // Edge cases\n        Arguments.of(\n            /* fromVersion= */ 3L,\n            /* toVersion= */ 3L,\n            noMaxFiles,\n            noMaxBytes,\n            \"Same version (3 to 3)\"));\n  }\n\n  // ================================================================================================\n  // Tests for planInputPartitions with excludeRegex\n  // ================================================================================================\n\n  /**\n   * Parameterized test that verifies planInputPartitions correctly applies the excludeRegex read\n   * option.\n   *\n   * <p>Uses a partitioned table so file paths contain partition directory names (e.g.\n   * \"category=alpha/...\"), making it straightforward to craft regex patterns that target specific\n   * subsets of files.\n   *\n   * <p>Verifies correctness by extracting the {@code id} column (always at ordinal 0) from the\n   * reader output and comparing the resulting set of IDs against the expected set.\n   */\n  @ParameterizedTest\n  @MethodSource(\"excludeRegexPlanInputPartitionsParameters\")\n  public void testPlanInputPartitions_excludeRegex(\n      String excludeRegexPattern,\n      long fromVersion,\n      long toVersion,\n      int[] expectedIds,\n      boolean isInitialSnapshot,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_exclude_regex_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n\n    // Create table partitioned by category with 4 versions of data.\n    // Version 0: CREATE TABLE (empty)\n    // Version 1: ids {1,2} in category=alpha\n    // Version 2: ids {3,4} in category=beta\n    // Version 3: ids {5,6} in category=alpha\n    // Version 4: ids {7,8} in category=gamma\n    sql(\n        \"CREATE TABLE %s (id INT, name STRING, category STRING) \"\n            + \"USING delta LOCATION '%s' PARTITIONED BY (category)\",\n        testTableName, testTablePath);\n    sql(\"INSERT INTO %s VALUES (1, 'Alice', 'alpha'), (2, 'Bob', 'alpha')\", testTableName);\n    sql(\"INSERT INTO %s VALUES (3, 'Charlie', 'beta'), (4, 'David', 'beta')\", testTableName);\n    sql(\"INSERT INTO %s VALUES (5, 'Eve', 'alpha'), (6, 'Frank', 'alpha')\", testTableName);\n    sql(\"INSERT INTO %s VALUES (7, 'Grace', 'gamma'), (8, 'Harry', 'gamma')\", testTableName);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSourceOffset startOffset =\n        new DeltaSourceOffset(\n            deltaLog.tableId(), fromVersion, DeltaSourceOffset.BASE_INDEX(), isInitialSnapshot);\n    DeltaSourceOffset endOffset =\n        new DeltaSourceOffset(\n            deltaLog.tableId(), toVersion, DeltaSourceOffset.END_INDEX(), isInitialSnapshot);\n\n    DeltaOptions options = createDeltaOptions(\"excludeRegex\", excludeRegexPattern);\n\n    // DSv2: planInputPartitions + createReaderFactory\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    org.apache.spark.sql.delta.Snapshot deltaSnapshot = deltaLog.unsafeVolatileSnapshot();\n    StructType fullSchema = deltaSnapshot.metadata().schema();\n    StructType partitionSchema = deltaSnapshot.metadata().partitionSchema();\n    Set<String> partitionColNames =\n        Arrays.stream(partitionSchema.fieldNames()).collect(Collectors.toSet());\n    StructType dataSchema =\n        new StructType(\n            Arrays.stream(fullSchema.fields())\n                .filter(f -> !partitionColNames.contains(f.name()))\n                .toArray(StructField[]::new));\n\n    SparkMicroBatchStream stream =\n        new SparkMicroBatchStream(\n            snapshotManager,\n            snapshotManager.loadLatestSnapshot(),\n            spark.sessionState().newHadoopConf(),\n            spark,\n            options,\n            testTablePath,\n            dataSchema,\n            partitionSchema,\n            fullSchema,\n            new org.apache.spark.sql.sources.Filter[0],\n            Map$.MODULE$.empty());\n\n    InputPartition[] partitions = stream.planInputPartitions(startOffset, endOffset);\n    PartitionReaderFactory readerFactory = stream.createReaderFactory();\n\n    // Extract IDs (column ordinal 0) from reader output.\n    // We read only the id to avoid schema complications with partitioned tables where\n    // the vectorized reader may not include partition columns in the InternalRow.\n    List<Integer> dsv2Ids = new ArrayList<>();\n    for (InputPartition partition : partitions) {\n      if (readerFactory.supportColumnarReads(partition)) {\n        PartitionReader<org.apache.spark.sql.vectorized.ColumnarBatch> reader =\n            readerFactory.createColumnarReader(partition);\n        while (reader.next()) {\n          org.apache.spark.sql.vectorized.ColumnarBatch batch = reader.get();\n          for (int rowId = 0; rowId < batch.numRows(); rowId++) {\n            dsv2Ids.add(batch.getRow(rowId).getInt(0));\n          }\n        }\n        reader.close();\n      } else {\n        PartitionReader<InternalRow> reader = readerFactory.createReader(partition);\n        while (reader.next()) {\n          dsv2Ids.add(reader.get().getInt(0));\n        }\n        reader.close();\n      }\n    }\n\n    List<Integer> expected =\n        Arrays.stream(expectedIds).sorted().boxed().collect(Collectors.toList());\n    Collections.sort(dsv2Ids);\n\n    assertEquals(\n        expected,\n        dsv2Ids,\n        String.format(\"[%s] ID mismatch: expected=%s, got=%s\", testDescription, expected, dsv2Ids));\n  }\n\n  /** Provides test parameters for the excludeRegex planInputPartitions test. */\n  private static Stream<Arguments> excludeRegexPlanInputPartitionsParameters() {\n    return Stream.of(\n        Arguments.of(\n            /* excludeRegexPattern= */ (String) null,\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {1, 2, 3, 4, 5, 6, 7, 8},\n            /* isInitialSnapshot= */ false,\n            \"No excludeRegex - all versions\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=alpha\",\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {3, 4, 7, 8},\n            /* isInitialSnapshot= */ false,\n            \"Exclude alpha category\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=beta\",\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {1, 2, 5, 6, 7, 8},\n            /* isInitialSnapshot= */ false,\n            \"Exclude beta category\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=gamma\",\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {1, 2, 3, 4, 5, 6},\n            /* isInitialSnapshot= */ false,\n            \"Exclude gamma category\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=(alpha|beta)\",\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {7, 8},\n            /* isInitialSnapshot= */ false,\n            \"Exclude alpha and beta categories\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"nonexistent_xyz_pattern\",\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {1, 2, 3, 4, 5, 6, 7, 8},\n            /* isInitialSnapshot= */ false,\n            \"Regex matching no files\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=\",\n            /* fromVersion= */ 1L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {},\n            /* isInitialSnapshot= */ false,\n            \"Regex matching all files\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=alpha\",\n            /* fromVersion= */ 2L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {3, 4, 7, 8},\n            /* isInitialSnapshot= */ false,\n            \"Exclude alpha versions 2 to 4\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=beta\",\n            /* fromVersion= */ 2L,\n            /* toVersion= */ 2L,\n            /* expectedIds= */ new int[] {},\n            /* isInitialSnapshot= */ false,\n            \"Exclude beta version 2 only\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ (String) null,\n            /* fromVersion= */ 0L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {1, 2, 3, 4, 5, 6, 7, 8},\n            /* isInitialSnapshot= */ true,\n            \"Initial snapshot - no excludeRegex\"),\n        Arguments.of(\n            /* excludeRegexPattern= */ \"category=alpha\",\n            /* fromVersion= */ 0L,\n            /* toVersion= */ 4L,\n            /* expectedIds= */ new int[] {3, 4, 7, 8},\n            /* isInitialSnapshot= */ true,\n            \"Initial snapshot - exclude alpha\"));\n  }\n\n  /**\n   * Helper method to convert InternalRow to Row for comparison.\n   *\n   * @param internalRow The InternalRow to convert\n   * @param schema The schema of the row\n   * @return A Row object\n   */\n  private Row convertInternalRowToRow(InternalRow internalRow, StructType schema) {\n    // Use Spark's built-in conversion from InternalRow to Row\n    scala.collection.Seq<Object> seq = internalRow.toSeq(schema);\n    Object[] values = scala.collection.JavaConverters.seqAsJavaList(seq).toArray();\n    return new org.apache.spark.sql.catalyst.expressions.GenericRowWithSchema(values, schema);\n  }\n\n  /**\n   * Helper method to compare data results between expected (ground truth) and DSv2.\n   *\n   * @param expectedRows Rows from ground truth (batch read from Delta table)\n   * @param dsv2Rows Rows from DSv2's planInputPartitions()\n   * @param testDescription Description of the test case for error messages\n   */\n  private void compareDataResults(\n      List<Row> expectedRows, List<Row> dsv2Rows, String testDescription) {\n    assertEquals(\n        expectedRows.size(),\n        dsv2Rows.size(),\n        String.format(\n            \"[%s] Number of rows should match: Expected=%d, DSv2=%d\",\n            testDescription, expectedRows.size(), dsv2Rows.size()));\n\n    // Sort both lists for consistent comparison (order may differ due to partitioning)\n    Comparator<Row> rowComparator =\n        (r1, r2) -> {\n          // Compare by id field (first column)\n          int id1 = r1.getInt(0);\n          int id2 = r2.getInt(0);\n          return Integer.compare(id1, id2);\n        };\n\n    List<Row> sortedExpected =\n        expectedRows.stream().sorted(rowComparator).collect(Collectors.toList());\n    List<Row> sortedDsv2 = dsv2Rows.stream().sorted(rowComparator).collect(Collectors.toList());\n\n    // Compare each row\n    for (int i = 0; i < sortedExpected.size(); i++) {\n      Row expectedRow = sortedExpected.get(i);\n      Row dsv2Row = sortedDsv2.get(i);\n\n      assertEquals(\n          expectedRow.length(),\n          dsv2Row.length(),\n          String.format(\n              \"[%s] Row %d length mismatch: Expected=%d, DSv2=%d\",\n              testDescription, i, expectedRow.length(), dsv2Row.length()));\n\n      // Compare each field\n      for (int fieldIdx = 0; fieldIdx < expectedRow.length(); fieldIdx++) {\n        Object expectedValue = expectedRow.get(fieldIdx);\n        Object dsv2Value = dsv2Row.get(fieldIdx);\n\n        // Convert both values to strings for comparison to handle UTF8String vs String\n        String expectedStr = expectedValue == null ? null : expectedValue.toString();\n        String dsv2Str = dsv2Value == null ? null : dsv2Value.toString();\n\n        assertEquals(\n            expectedStr,\n            dsv2Str,\n            String.format(\n                \"[%s] Row %d, field %d mismatch: Expected=%s, DSv2=%s\",\n                testDescription, i, fieldIdx, expectedStr, dsv2Str));\n      }\n    }\n  }\n\n  // ================================================================================================\n  // Tests for getStartingVersion parity between DSv1 and DSv2\n  // ================================================================================================\n\n  /**\n   * Parameterized test that verifies parity between DSv1 DeltaSource.getStartingVersion and DSv2\n   * SparkMicroBatchStream.getStartingVersion.\n   */\n  @ParameterizedTest\n  @MethodSource(\"getStartingVersionParameters\")\n  public void testGetStartingVersion(\n      String startingVersion, Optional<Long> expectedVersion, @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_starting_version_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 5 versions (version 0 = CREATE TABLE, versions 1-5 = INSERTs)\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 1,\n        /* includeEmptyVersion= */ false);\n\n    testAndCompareStartingVersion(\n        testTablePath, startingVersion, expectedVersion, \"startingVersion=\" + startingVersion);\n  }\n\n  /** Provides test parameters for the parameterized getStartingVersion test. */\n  private static Stream<Arguments> getStartingVersionParameters() {\n    return Stream.of(\n        Arguments.of(/* startingVersion= */ \"0\", /* expectedVersion= */ Optional.of(0L)),\n        Arguments.of(/* startingVersion= */ \"1\", /* expectedVersion= */ Optional.of(1L)),\n        Arguments.of(/* startingVersion= */ \"3\", /* expectedVersion= */ Optional.of(3L)),\n        Arguments.of(/* startingVersion= */ \"5\", /* expectedVersion= */ Optional.of(5L)),\n        Arguments.of(/* startingVersion= */ \"latest\", /* expectedVersion= */ Optional.of(6L)),\n        Arguments.of(/* startingVersion= */ null, /* expectedVersion= */ Optional.empty()));\n  }\n\n  /**\n   * Test that verifies both DSv1 and DSv2 handle the case where no DeltaOptions are provided. DSv1\n   * receives an empty DeltaOptions (no parameters), while DSv2 receives Optional.empty(). This\n   * tests the equivalence between these two approaches.\n   */\n  @Test\n  public void testGetStartingVersion_noOptions(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_no_options_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 5 versions (version 0 = CREATE TABLE, versions 1-5 = INSERTs)\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 1,\n        /* includeEmptyVersion= */ false);\n\n    // dsv1\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaOptions emptyOptions = emptyDeltaOptions();\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath, emptyOptions);\n    scala.Option<Object> dsv1Result = deltaSource.getStartingVersion();\n\n    // dsv2\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, new Configuration());\n    SparkMicroBatchStream dsv2Stream =\n        createTestStreamWithDefaults(snapshotManager, new Configuration(), emptyDeltaOptions());\n    Optional<Long> dsv2Result = dsv2Stream.getStartingVersion();\n\n    compareStartingVersionResults(dsv1Result, dsv2Result, Optional.empty(), \"No options provided\");\n  }\n\n  /** Test that verifies both DSv1 and DSv2 handle negative startingVersion values identically. */\n  @Test\n  public void testGetStartingVersion_negativeVersion_throwsError(@TempDir File tempDir)\n      throws Exception {\n    // Negative values are rejected during DeltaOptions parsing, before getStartingVersion is\n    // called.\n    assertThrows(IllegalArgumentException.class, () -> createDeltaOptions(\"startingVersion\", \"-1\"));\n  }\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 handle the protocol validation behavior\n   * identically with the validation flag on/off.\n   *\n   * <p>When protocol validation is enabled, validateProtocolAt is called and must succeed. When\n   * disabled, the code immediately falls back to checkVersionExists without protocol validation.\n   */\n  @ParameterizedTest\n  @MethodSource(\"protocolValidationParameters\")\n  public void testGetStartingVersion_protocolValidationFlag(\n      boolean enableProtocolValidation,\n      String startingVersion,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_protocol_fallback_\" + Math.abs(testDescription.hashCode()) + \"_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 5 versions (version 0 = CREATE TABLE, versions 1-5 = INSERTs)\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 5,\n        /* rowsPerVersion= */ 1,\n        /* includeEmptyVersion= */ false);\n\n    // Test with protocol validation enabled/disabled\n    String configKey = DeltaSQLConf.FAST_DROP_FEATURE_STREAMING_ALWAYS_VALIDATE_PROTOCOL().key();\n    try {\n      spark.conf().set(configKey, String.valueOf(enableProtocolValidation));\n      testAndCompareStartingVersion(\n          testTablePath,\n          startingVersion,\n          Optional.of(Long.parseLong(startingVersion)),\n          testDescription);\n    } finally {\n      spark.conf().unset(configKey);\n    }\n  }\n\n  /** Provides test parameters for protocol validation scenarios. */\n  private static Stream<Arguments> protocolValidationParameters() {\n    return Stream.of(\n        Arguments.of(\n            /* enableProtocolValidation= */ true,\n            /* startingVersion= */ \"2\",\n            \"Protocol validation enabled\"),\n        Arguments.of(\n            /* enableProtocolValidation= */ false,\n            /* startingVersion= */ \"3\",\n            \"Protocol validation disabled\"));\n  }\n\n  // TODO(#5320): Add test for unsupported table feature\n  // Test case where protocol validation encounters an unsupported table feature and throws\n  // (does NOT fall back to checkVersionExists). This is difficult to test reliably as it\n  // requires creating a table with features that Kernel doesn't support, which Spark SQL\n  // validates upfront. This scenario is tested through integration tests.\n\n  /**\n   * Test case where protocol validation fails with a non-feature exception (snapshot cannot be\n   * recreated), but checkVersionExists succeeds (commit logically exists).\n   *\n   * <p>Scenario: After creating a checkpoint at version 10, old log files 0-5 are deleted\n   * (simulating log cleanup by timestamp). This makes version 7 non-recreatable (it exists between\n   * the deleted logs and the checkpoint). Protocol validation fails when trying to build snapshot\n   * at version 7, but checkVersionExists succeeds because the commit still logically exists.\n   */\n  @Test\n  public void testGetStartingVersion_protocolValidationNonFeatureExceptionFallback(\n      @TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_non_recreatable_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // Create 10 versions (version 0 = CREATE TABLE, versions 1-10 = INSERTs)\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 10,\n        /* rowsPerVersion= */ 1,\n        /* includeEmptyVersion= */ false);\n\n    // Create checkpoint at version 10\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    Snapshot snapshotV10 =\n        deltaLog.getSnapshotAt(\n            10, Option.<CheckpointInstance>empty(), Option.<CatalogTable>empty(), false);\n    deltaLog.checkpoint(snapshotV10, Option.<CatalogTable>empty());\n\n    // Simulate log cleanup by timestamp: delete logs 0-5\n    // This makes version 7 non-recreatable while allowing DeltaLog to load the latest snapshot\n    Path logPath = new Path(testTablePath, \"_delta_log\");\n    for (long version = 0; version <= 5; version++) {\n      Path logFile = new Path(logPath, String.format(\"%020d.json\", version));\n      File file = new File(logFile.toUri().getPath());\n      if (file.exists()) {\n        file.delete();\n      }\n    }\n\n    // Test with startingVersion=7 (a version that's no longer recreatable but logically exists)\n    String startingVersion = \"7\";\n\n    // dsv1\n    DeltaLog freshDeltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource =\n        createDeltaSource(\n            freshDeltaLog, testTablePath, createDeltaOptions(\"startingVersion\", startingVersion));\n    scala.Option<Object> dsv1Result = deltaSource.getStartingVersion();\n\n    // dsv2\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, new Configuration());\n    SparkMicroBatchStream dsv2Stream =\n        createTestStreamWithDefaults(\n            snapshotManager,\n            new Configuration(),\n            createDeltaOptions(\"startingVersion\", startingVersion));\n    Optional<Long> dsv2Result = dsv2Stream.getStartingVersion();\n\n    compareStartingVersionResults(\n        dsv1Result,\n        dsv2Result,\n        Optional.of(Long.parseLong(startingVersion)),\n        \"Protocol validation fallback with non-recreatable version\");\n  }\n\n  /**\n   * Test that verifies parity between DSv1 DeltaSource.getStartingVersion and DSv2\n   * SparkMicroBatchStream.getStartingVersion when using startingTimestamp option.\n   *\n   * <p>Uses ICT (In-Commit Timestamps) so we can read exact commit timestamps from the delta log\n   * and test the boundary case where startingTimestamp exactly equals a commit time.\n   */\n  @Test\n  public void testGetStartingVersionFromTimestamp(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_starting_timestamp_\" + System.nanoTime();\n\n    // Enable ICT so commit timestamps are deterministic and stored in the delta log\n    sql(\n        \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\"\n            + \" TBLPROPERTIES ('delta.enableInCommitTimestamps' = 'true')\",\n        testTableName, testTablePath); // Version 0\n\n    String beforeV1TS = new Timestamp(System.currentTimeMillis()).toString();\n    // Version 1\n    sql(\"INSERT INTO %s VALUES (1, 'User1')\", testTableName);\n    Thread.sleep(10);\n    String betweenV1V2TS = new Timestamp(System.currentTimeMillis()).toString();\n    // Version 2\n    sql(\"INSERT INTO %s VALUES (2, 'User2')\", testTableName);\n    Thread.sleep(10);\n    String afterV2TS = new Timestamp(System.currentTimeMillis()).toString();\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    // Read exact commit timestamps from the delta log for boundary testing\n    scala.collection.Seq<DeltaHistory> history =\n        deltaLog.history().getHistory(0L, scala.Option.apply(2L), scala.Option.empty());\n    java.util.Map<Long, Timestamp> versionTimestamps = new java.util.HashMap<>();\n    for (int i = 0; i < history.size(); i++) {\n      DeltaHistory entry = history.apply(i);\n      long version = (long) entry.version().get();\n      versionTimestamps.put(version, entry.timestamp());\n    }\n    String v1ExactTS = versionTimestamps.get(1L).toString();\n    String v2ExactTS = versionTimestamps.get(2L).toString();\n\n    class TimestampTestCase {\n      final String timestamp;\n      final long expectedVersion;\n      final String message;\n\n      TimestampTestCase(String timestamp, long expectedVersion, String message) {\n        this.timestamp = timestamp;\n        this.expectedVersion = expectedVersion;\n        this.message = message;\n      }\n    }\n\n    TimestampTestCase[] testCases = {\n      new TimestampTestCase(beforeV1TS, 1L, \"timestamp between v0 and v1 should return version 1\"),\n      new TimestampTestCase(\n          v1ExactTS, 1L, \"timestamp exactly at v1 commit time should return version 1\"),\n      new TimestampTestCase(\n          betweenV1V2TS, 2L, \"timestamp between v1 and v2 should return version 2\"),\n      new TimestampTestCase(\n          v2ExactTS, 2L, \"timestamp exactly at v2 commit time should return version 2\")\n    };\n    for (TimestampTestCase testCase : testCases) {\n      String timestamp = testCase.timestamp;\n      long expectedVersion = testCase.expectedVersion;\n      String message = testCase.message;\n\n      // dsv1\n      DeltaSource deltaSource =\n          createDeltaSource(\n              deltaLog, testTablePath, createDeltaOptions(\"startingTimestamp\", timestamp));\n      scala.Option<Object> dsv1Result = deltaSource.getStartingVersion();\n\n      // dsv2\n      PathBasedSnapshotManager snapshotManager =\n          new PathBasedSnapshotManager(testTablePath, new Configuration());\n      SparkMicroBatchStream dsv2Stream =\n          createTestStreamWithDefaults(\n              snapshotManager,\n              new Configuration(),\n              createDeltaOptions(\"startingTimestamp\", timestamp));\n      Optional<Long> dsv2Result = dsv2Stream.getStartingVersion();\n\n      compareStartingVersionResults(dsv1Result, dsv2Result, Optional.of(expectedVersion), message);\n    }\n\n    // dsv1\n    DeltaSource deltaSource =\n        createDeltaSource(\n            deltaLog, testTablePath, createDeltaOptions(\"startingTimestamp\", afterV2TS));\n    DeltaAnalysisException dsv1Exception =\n        assertThrows(\n            DeltaAnalysisException.class,\n            deltaSource::getStartingVersion,\n            String.format(\n                \"DSv1 should throw when no commit after timestamp and not allow out of range\"));\n\n    // dsv2\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, new Configuration());\n    SparkMicroBatchStream dsv2Stream =\n        createTestStreamWithDefaults(\n            snapshotManager,\n            new Configuration(),\n            createDeltaOptions(\"startingTimestamp\", afterV2TS));\n    DeltaAnalysisException dsv2Exception =\n        assertThrows(\n            DeltaAnalysisException.class,\n            dsv2Stream::getStartingVersion,\n            String.format(\n                \"DSv2 should throw when no commit after timestamp and not allow out of range\"));\n\n    assertEquals(\n        dsv1Exception.getErrorClass(),\n        dsv2Exception.getErrorClass(),\n        \"v1 connector and v2 connector should throw the same error class when no commit after timestamp and not allow out of range\");\n    assertEquals(\n        dsv1Exception.getMessageParameters(),\n        dsv2Exception.getMessageParameters(),\n        \"v1 connector and v2 connector should throw the same error messages when no commit after timestamp and not allow out of range\");\n  }\n\n  // ================================================================================================\n  // Tests for checkReadIncompatibleSchemaChanges parity between v1 connector vs v2 connector\n  // ================================================================================================\n\n  // TODO(#5319): Tests on RESTORE on delta table after applying an additive schema change\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 throw DeltaIllegalStateException when\n   * encountering forward-fill additive schema change actions.\n   */\n  @ParameterizedTest\n  @MethodSource(\"additiveSchemaEvolutionScenarios\")\n  public void testSchemaEvolution_onForwardAdditiveChanges_throwsError(\n      ScenarioSetup scenarioSetup,\n      Map<String, String> sparkConf,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_forward_additive_changes\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n    createSchemaEvolutionTestTable(testTablePath, testTableName);\n\n    // Try to read from version 0, which should include commits with METADATA actions\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    boolean isInitialSnapshot = false;\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n\n    try {\n      // setup specific spark config\n      sparkConf.forEach((key, value) -> spark.conf().set(key, value));\n\n      // Create DSv1 DeltaSource\n      DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n      DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n\n      // Create DSv2 SparkMicroBatchStream\n      Configuration hadoopConf = new Configuration();\n      PathBasedSnapshotManager snapshotManager =\n          new PathBasedSnapshotManager(testTablePath, hadoopConf);\n      SparkMicroBatchStream stream =\n          createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n      // Execute schema change after source initialization to ensure forward change\n      scenarioSetup.setup(testTableName, tempDir);\n\n      DeltaIllegalStateException dsv1Exception =\n          assertThrows(\n              DeltaIllegalStateException.class,\n              () -> {\n                ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n                    deltaSource.getFileChanges(\n                        fromVersion,\n                        fromIndex,\n                        isInitialSnapshot,\n                        endOffset,\n                        /* verifyMetadataAction= */ true);\n                // Consume the iterator to trigger validation\n                while (deltaChanges.hasNext()) {\n                  // Exception is thrown by .next() when it encounters a REMOVE\n                  deltaChanges.next();\n                }\n                deltaChanges.close();\n              },\n              String.format(\"DSv1 should throw on METADATA for scenario: %s\", testDescription));\n\n      DeltaIllegalStateException dsv2Exception =\n          assertThrows(\n              DeltaIllegalStateException.class,\n              () -> {\n                CloseableIterator<IndexedFile> kernelChanges =\n                    stream.getFileChanges(\n                        fromVersion,\n                        fromIndex,\n                        isInitialSnapshot,\n                        ScalaUtils.toJavaOptional(endOffset));\n                try {\n                  // Consume the iterator to trigger validation (if not already triggered)\n                  while (kernelChanges.hasNext()) {\n                    kernelChanges.next();\n                  }\n                  kernelChanges.close();\n                } finally {\n                  // Make sure to close the iterator even if exception occurs\n                  if (kernelChanges != null) {\n                    try {\n                      kernelChanges.close();\n                    } catch (Exception ignored) {\n                    }\n                  }\n                }\n              },\n              String.format(\"DSv2 should throw on METADATA for scenario: %s\", testDescription));\n\n      assertEquals(\n          dsv1Exception.getErrorClass(),\n          dsv2Exception.getErrorClass(),\n          \"v1 connector and v2 connector should throw the same error class on forward-fill additive schema changes\");\n      assertEquals(\n          dsv1Exception.getMessageParameters(),\n          dsv2Exception.getMessageParameters(),\n          \"v1 connector and v2 connector should throw the same error messages on forward-fill additive schema changes\");\n    } finally {\n      // recover spark config to original state\n      sparkConf.forEach((key, value) -> spark.conf().unset(key));\n    }\n  }\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 return the same file changes when\n   * encountering backfill additive schema change actions.\n   */\n  @ParameterizedTest\n  @MethodSource(\"additiveSchemaEvolutionScenarios\")\n  public void testSchemaEvolution_onBackfillAdditiveChanges(\n      ScenarioSetup scenarioSetup,\n      Map<String, String> sparkConf,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_backfill_additive_changes\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n    createSchemaEvolutionTestTable(testTablePath, testTableName);\n\n    // Execute schema change before source initialization to ensure backfill change\n    scenarioSetup.setup(testTableName, tempDir);\n\n    // Try to read from version 0, which should include commits with METADATA actions\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    boolean isInitialSnapshot = false;\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n\n    try {\n      // setup specific spark config\n      sparkConf.forEach((key, value) -> spark.conf().set(key, value));\n\n      // Test DSv1 DeltaSource\n      DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n      DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n      ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n          deltaSource.getFileChanges(\n              fromVersion,\n              fromIndex,\n              isInitialSnapshot,\n              endOffset,\n              /* verifyMetadataAction= */ true);\n      List<org.apache.spark.sql.delta.sources.IndexedFile> deltaFilesList = new ArrayList<>();\n      while (deltaChanges.hasNext()) {\n        deltaFilesList.add(deltaChanges.next());\n      }\n      deltaChanges.close();\n\n      // Test DSv2 SparkMicroBatchStream\n      Configuration hadoopConf = new Configuration();\n      PathBasedSnapshotManager snapshotManager =\n          new PathBasedSnapshotManager(testTablePath, hadoopConf);\n      SparkMicroBatchStream stream =\n          createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n      try (CloseableIterator<IndexedFile> kernelChanges =\n          stream.getFileChanges(\n              fromVersion, fromIndex, isInitialSnapshot, ScalaUtils.toJavaOptional(endOffset))) {\n        List<IndexedFile> kernelFilesList = new ArrayList<>();\n        while (kernelChanges.hasNext()) {\n          kernelFilesList.add(kernelChanges.next());\n        }\n        compareFileChanges(deltaFilesList, kernelFilesList);\n      }\n    } finally {\n      // recover spark config to original state\n      sparkConf.forEach((key, value) -> spark.conf().unset(key));\n    }\n  }\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 throw Exception when encountering\n   * forward-fill non-additive schema change actions.\n   */\n  @ParameterizedTest\n  @MethodSource(\"nonAdditiveSchemaEvolutionScenarios\")\n  public void testSchemaEvolution_onForwardNonAdditiveChanges_throwsError(\n      ScenarioSetup scenarioSetup,\n      Map<String, String> sparkConf,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_forward_non_additive_changes\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n\n    createSchemaEvolutionTestTable(testTablePath, testTableName);\n\n    // Try to read from version 0, which should include commits with METADATA actions\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    boolean isInitialSnapshot = false;\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n\n    try {\n      // setup specific spark config\n      sparkConf.forEach((key, value) -> spark.conf().set(key, value));\n\n      // Create DSv1 DeltaSource\n      DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n      DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n\n      // Create DSv2 SparkMicroBatchStream\n      Configuration hadoopConf = new Configuration();\n      PathBasedSnapshotManager snapshotManager =\n          new PathBasedSnapshotManager(testTablePath, hadoopConf);\n      SparkMicroBatchStream stream =\n          createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n      // Execute schema change after source initialization to ensure forward change\n      scenarioSetup.setup(testTableName, tempDir);\n\n      DeltaUnsupportedOperationException dsv1Exception =\n          assertThrows(\n              DeltaUnsupportedOperationException.class,\n              () -> {\n                ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n                    deltaSource.getFileChanges(\n                        fromVersion,\n                        fromIndex,\n                        isInitialSnapshot,\n                        endOffset,\n                        /* verifyMetadataAction= */ true);\n                // Consume the iterator to trigger validation\n                while (deltaChanges.hasNext()) {\n                  // Exception is thrown by .next() when it encounters a REMOVE\n                  deltaChanges.next();\n                }\n                deltaChanges.close();\n              },\n              String.format(\"DSv1 should throw on METADATA for scenario: %s\", testDescription));\n\n      DeltaUnsupportedOperationException dsv2Exception =\n          assertThrows(\n              DeltaUnsupportedOperationException.class,\n              () -> {\n                CloseableIterator<IndexedFile> kernelChanges =\n                    stream.getFileChanges(\n                        fromVersion,\n                        fromIndex,\n                        isInitialSnapshot,\n                        ScalaUtils.toJavaOptional(endOffset));\n                try {\n                  // Consume the iterator to trigger validation (if not already triggered)\n                  while (kernelChanges.hasNext()) {\n                    kernelChanges.next();\n                  }\n                  kernelChanges.close();\n                } finally {\n                  // Make sure to close the iterator even if exception occurs\n                  if (kernelChanges != null) {\n                    try {\n                      kernelChanges.close();\n                    } catch (Exception ignored) {\n                    }\n                  }\n                }\n              },\n              String.format(\"DSv2 should throw on METADATA for scenario: %s\", testDescription));\n\n      // TODO(#5319): assertEqual after schema tracking log is supported\n      String expectedPrefix = \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE\";\n      assertTrue(\n          dsv1Exception.getErrorClass().startsWith(expectedPrefix),\n          String.format(\n              \"v1 connector error class should start with %s, but got: %s\",\n              expectedPrefix, dsv1Exception.getErrorClass()));\n      assertTrue(\n          dsv2Exception.getErrorClass().startsWith(expectedPrefix),\n          String.format(\n              \"v2 connector error class should start with %s, but got: %s\",\n              expectedPrefix, dsv2Exception.getErrorClass()));\n      assertEquals(\n          dsv1Exception.getMessageParameters(),\n          dsv2Exception.getMessageParameters(),\n          \"v1 connector and v2 connector should throw the same error messages on forward-fill non-additive schema changes\");\n    } finally {\n      // recover spark config to original state\n      sparkConf.forEach((key, value) -> spark.conf().unset(key));\n    }\n  }\n\n  /**\n   * Parameterized test that verifies both DSv1 and DSv2 throw Exception when encountering backfill\n   * non-additive schema change actions.\n   */\n  @ParameterizedTest\n  @MethodSource(\"nonAdditiveSchemaEvolutionScenarios\")\n  public void testSchemaEvolution_onBackfillNonAdditiveChanges_throwsError(\n      ScenarioSetup scenarioSetup,\n      Map<String, String> sparkConf,\n      String testDescription,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName =\n        \"test_backfill_non_additive_changes\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n    createSchemaEvolutionTestTable(testTablePath, testTableName);\n\n    // Try to read from version 0, which should include commits with METADATA actions\n    long fromVersion = 0L;\n    long fromIndex = DeltaSourceOffset.BASE_INDEX();\n    boolean isInitialSnapshot = false;\n    Option<DeltaSourceOffset> endOffset = Option.empty();\n\n    try {\n      // setup specific spark config\n      sparkConf.forEach((key, value) -> spark.conf().set(key, value));\n\n      // Execute schema change before source initialization to ensure backfill change\n      scenarioSetup.setup(testTableName, tempDir);\n\n      // Create DSv1 DeltaSource\n      DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n      DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n\n      // Create DSv2 SparkMicroBatchStream\n      Configuration hadoopConf = new Configuration();\n      PathBasedSnapshotManager snapshotManager =\n          new PathBasedSnapshotManager(testTablePath, hadoopConf);\n      SparkMicroBatchStream stream =\n          createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n      DeltaUnsupportedOperationException dsv1Exception =\n          assertThrows(\n              DeltaUnsupportedOperationException.class,\n              () -> {\n                ClosableIterator<org.apache.spark.sql.delta.sources.IndexedFile> deltaChanges =\n                    deltaSource.getFileChanges(\n                        fromVersion,\n                        fromIndex,\n                        isInitialSnapshot,\n                        endOffset,\n                        /* verifyMetadataAction= */ true);\n                // Consume the iterator to trigger validation\n                while (deltaChanges.hasNext()) {\n                  // Exception is thrown by .next() when it encounters a REMOVE\n                  deltaChanges.next();\n                }\n                deltaChanges.close();\n              },\n              String.format(\"DSv1 should throw on METADATA for scenario: %s\", testDescription));\n\n      DeltaUnsupportedOperationException dsv2Exception =\n          assertThrows(\n              DeltaUnsupportedOperationException.class,\n              () -> {\n                CloseableIterator<IndexedFile> kernelChanges =\n                    stream.getFileChanges(\n                        fromVersion,\n                        fromIndex,\n                        isInitialSnapshot,\n                        ScalaUtils.toJavaOptional(endOffset));\n                try {\n                  // Consume the iterator to trigger validation (if not already triggered)\n                  while (kernelChanges.hasNext()) {\n                    kernelChanges.next();\n                  }\n                  kernelChanges.close();\n                } finally {\n                  // Make sure to close the iterator even if exception occurs\n                  if (kernelChanges != null) {\n                    try {\n                      kernelChanges.close();\n                    } catch (Exception ignored) {\n                    }\n                  }\n                }\n              },\n              String.format(\"DSv2 should throw on METADATA for scenario: %s\", testDescription));\n\n      // TODO(#5319): assertEqual after schema tracking log is supported\n      String expectedPrefix = \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE\";\n      assertTrue(\n          dsv1Exception.getErrorClass().startsWith(expectedPrefix),\n          String.format(\n              \"v1 connector error class should start with %s, but got: %s\",\n              expectedPrefix, dsv1Exception.getErrorClass()));\n      assertTrue(\n          dsv2Exception.getErrorClass().startsWith(expectedPrefix),\n          String.format(\n              \"v2 connector error class should start with %s, but got: %s\",\n              expectedPrefix, dsv2Exception.getErrorClass()));\n      assertEquals(\n          dsv1Exception.getMessageParameters(),\n          dsv2Exception.getMessageParameters(),\n          \"v1 connector and v2 connector should throw the same error messages on backfill non-additive schema changes\");\n    } finally {\n      // recover spark config to original state\n      sparkConf.forEach((key, value) -> spark.conf().unset(key));\n    }\n  }\n\n  /**\n   * Test that verifies DSv1 and DSv2 throw errors when the starting snapshot has an incompatible\n   * schema change that gets reverted before the latest version.\n   *\n   * <p>Edge case: checkReadIncompatibleSchemaChange only checks metadata actions, so it misses the\n   * incompatible intermediate state (id → userId → id). The\n   * checkReadIncompatibleSchemaChangeOnStreamStartOnce method catches this by validating each\n   * snapshot in the range.\n   */\n  @Test\n  public void testSchemaEvolution_onStreamStartOnce(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testDescription = \"testSchemaEvolution_onStreamStartOnce\";\n    String testTableName =\n        \"test_schema_changes_on_stream_start_once\"\n            + Math.abs(testDescription.hashCode())\n            + \"_\"\n            + System.nanoTime();\n    createSchemaEvolutionTestTable(testTablePath, testTableName);\n\n    // Execute schema change before source initialization to ensure backfill change\n    spark.sql(String.format(\"ALTER table %s RENAME COLUMN id TO userId\", testTableName));\n    spark.sql(\n        String.format(\n            \"INSERT INTO %s VALUES (3, 'Cathy', 5, named_struct('col1', 18, 'col2', 'SF'))\",\n            testTableName));\n    // Record the version prior to reverting schema change\n    long incompatibleSchemaVersion =\n        DeltaLog.forTable(spark, new Path(testTablePath))\n            .update(false, Option.empty(), Option.empty())\n            .version();\n    // Revert the schema change\n    spark.sql(String.format(\"ALTER table %s RENAME COLUMN userId TO id\", testTableName));\n    spark.sql(\n        String.format(\n            \"INSERT INTO %s VALUES (4, 'David', 8, named_struct('col1', 47, 'col2', 'DC'))\",\n            testTableName));\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    String tableId = deltaLog.tableId();\n    // Try to read from version 0 without readLimit to check all commits\n    DeltaSourceOffset startOffset =\n        new DeltaSourceOffset(\n            tableId,\n            incompatibleSchemaVersion,\n            DeltaSourceOffset.BASE_INDEX(),\n            /* isInitialSnapshot= */ false);\n    ReadLimit readLimit = ReadLimitConfig.noLimit().toReadLimit();\n\n    // Test DSv1 DeltaSource\n    DeltaSource deltaSource = createDeltaSource(deltaLog, testTablePath);\n    DeltaUnsupportedOperationException dsv1Exception =\n        assertThrows(\n            DeltaUnsupportedOperationException.class,\n            () -> deltaSource.latestOffset(startOffset, readLimit),\n            String.format(\n                \"DSv1 should throw error on stream start for scenario: %s\", testDescription));\n    assertThat(dsv1Exception.getStackTrace())\n        .as(\"Error should be thrown by 'checkReadIncompatibleSchemaChangeOnStreamStartOnce'\")\n        .anyMatch(\n            element ->\n                element.toString().contains(\"checkReadIncompatibleSchemaChangeOnStreamStartOnce\"));\n\n    // Test DSv2 SparkMicroBatchStream\n    Configuration hadoopConf = new Configuration();\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, hadoopConf);\n    SparkMicroBatchStream stream =\n        createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n    DeltaUnsupportedOperationException dsv2Exception =\n        assertThrows(\n            DeltaUnsupportedOperationException.class,\n            () -> stream.latestOffset(startOffset, readLimit),\n            String.format(\n                \"DSv2 should throw error on stream start for scenario: %s\", testDescription));\n    assertThat(dsv2Exception.getStackTrace())\n        .as(\"Error should be thrown by 'checkReadIncompatibleSchemaChangeOnStreamStartOnce'\")\n        .anyMatch(\n            element ->\n                element.toString().contains(\"checkReadIncompatibleSchemaChangeOnStreamStartOnce\"));\n\n    // TODO(#5319): assertEqual after schema tracking log is supported\n    String expectedPrefix = \"DELTA_STREAMING_INCOMPATIBLE_SCHEMA_CHANGE\";\n    assertTrue(\n        dsv1Exception.getErrorClass().startsWith(expectedPrefix),\n        String.format(\n            \"v1 connector error class should start with %s, but got: %s\",\n            expectedPrefix, dsv1Exception.getErrorClass()));\n    assertTrue(\n        dsv2Exception.getErrorClass().startsWith(expectedPrefix),\n        String.format(\n            \"v2 connector error class should start with %s, but got: %s\",\n            expectedPrefix, dsv2Exception.getErrorClass()));\n    assertEquals(\n        dsv1Exception.getMessageParameters(),\n        dsv2Exception.getMessageParameters(),\n        \"v1 connector and v2 connector should throw the same error messages on stream start schema changes\");\n  }\n\n  /** Provides test scenarios that generate additive schema changes actions. */\n  private static Stream<Arguments> additiveSchemaEvolutionScenarios() {\n    return Stream.of(\n        // Add nullable INT column\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ADD COLUMN age INT\", tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Add nullable INT column\"),\n\n        // Add nullable STRING column\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ADD COLUMN address STRING\", tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Add nullable STRING column\"),\n\n        // Add nullable STRUCT column\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\n                      \"ALTER TABLE %s ADD COLUMN (address STRUCT<country: STRING, zip: INT>)\",\n                      tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Add nullable STRUCT column\"),\n\n        // Add multiple nullable columns\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\n                      \"ALTER TABLE %s ADD COLUMN (address STRING, zip INT, time TIMESTAMP)\",\n                      tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Add multiple nullable columns\"),\n\n        // Make non-nullable column nullable\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ALTER COLUMN id DROP NOT NULL\", tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Make non-nullable column nullable\"),\n\n        // Add nullable column and then make non-nullable column nullable\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ALTER COLUMN id DROP NOT NULL\", tableName);\n                  sql(\"ALTER TABLE %s ADD COLUMN age INT\", tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Add nullable column and then make non-nullable column nullable\"),\n\n        // Make non-nullable column nullable and then add nullable column\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ADD COLUMN age INT\", tableName);\n                  sql(\"ALTER TABLE %s ALTER COLUMN id DROP NOT NULL\", tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Make non-nullable column nullable and then add nullable column\"),\n\n        // Widen INT column to BIGINT\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ALTER COLUMN id TYPE BIGINT\", tableName);\n                },\n            // Set enableSchemaTrackingForTypeWidening to be false to treat widening type changes as\n            // additive\n            /* sparkConf */ Map.of(\n                DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING().key(), \"false\"),\n            \"Widen INT column to BIGINT\"));\n  }\n\n  /** Provides test scenarios that generate non-additive schema changes actions. */\n  private static Stream<Arguments> nonAdditiveSchemaEvolutionScenarios() {\n    return Stream.of(\n        // Rename column\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s RENAME COLUMN id TO userId\", tableName);\n                },\n            /* sparkConf */ Map.of(),\n            \"Rename column\"),\n\n        // Drop nullable, non-nullable and struct columns\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s DROP COLUMNS (id, value, info)\", tableName);\n                },\n            /* sparkConf */ Map.of(\n                DeltaSQLConf\n                    .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES()\n                    .key(),\n                \"false\"),\n            \"Drop nullable, non-nullable and struct columns\"),\n\n        // Drop column in nested struct\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s DROP COLUMNS info.col1\", tableName);\n                },\n            /* sparkConf */ Map.of(\n                DeltaSQLConf\n                    .DELTA_STREAMING_UNSAFE_READ_ON_INCOMPATIBLE_COLUMN_MAPPING_SCHEMA_CHANGES()\n                    .key(),\n                \"false\"),\n            \"Drop column in nested struct\"),\n\n        // Widen INT column to BIGINT\n        Arguments.of(\n            (ScenarioSetup)\n                (tableName, tempDir) -> {\n                  sql(\"ALTER TABLE %s ALTER COLUMN id TYPE BIGINT\", tableName);\n                },\n            // Set enableSchemaTrackingForTypeWidening to be true to treat widening type changes as\n            // non-additive\n            /* sparkConf */ Map.of(\n                DeltaSQLConf.DELTA_TYPE_WIDENING_ENABLE_STREAMING_SCHEMA_TRACKING().key(), \"true\"),\n            \"Widen INT column to BIGINT\"));\n  }\n\n  // ================================================================================================\n\n  // Helper methods\n  // ================================================================================================\n\n  /** Functional interface for setting up test scenarios. */\n  @FunctionalInterface\n  interface ScenarioSetup {\n    /**\n     * Set up the test scenario by executing SQL statements.\n     *\n     * @param tableName The name of the test table\n     * @param tempDir The temporary directory for this test\n     */\n    void setup(String tableName, File tempDir) throws Exception;\n  }\n\n  static class ReadLimitConfig {\n    private final Optional<Integer> maxFiles;\n    private final Optional<Long> maxBytes;\n\n    private ReadLimitConfig(Optional<Integer> maxFiles, Optional<Long> maxBytes) {\n      this.maxFiles = maxFiles;\n      this.maxBytes = maxBytes;\n    }\n\n    static ReadLimitConfig noLimit() {\n      return new ReadLimitConfig(Optional.empty(), Optional.empty());\n    }\n\n    static ReadLimitConfig maxFiles(int files) {\n      return new ReadLimitConfig(Optional.of(files), Optional.empty());\n    }\n\n    static ReadLimitConfig maxBytes(long bytes) {\n      return new ReadLimitConfig(Optional.empty(), Optional.of(bytes));\n    }\n\n    ReadLimit toReadLimit() {\n      if (maxFiles.isPresent()) {\n        return ReadLimit.maxFiles(maxFiles.get());\n      } else if (maxBytes.isPresent()) {\n        return new ReadMaxBytes(maxBytes.get());\n      } else {\n        return ReadLimit.allAvailable();\n      }\n    }\n  }\n\n  private void compareOffsets(Offset dsv1Offset, Offset dsv2Offset, String testDescription) {\n    if (dsv1Offset == null && dsv2Offset == null) {\n      return; // Both null is valid (no data case)\n    }\n\n    // Both should be non-null or both should be null\n    if (dsv1Offset == null || dsv2Offset == null) {\n      throw new AssertionError(\n          String.format(\n              \"Offset mismatch for test '%s': DSv1=%s, DSv2=%s\",\n              testDescription, dsv1Offset, dsv2Offset));\n    }\n\n    DeltaSourceOffset dsv1DeltaOffset = (DeltaSourceOffset) dsv1Offset;\n    DeltaSourceOffset dsv2DeltaOffset = (DeltaSourceOffset) dsv2Offset;\n\n    assertEquals(\n        dsv1DeltaOffset.reservoirVersion(),\n        dsv2DeltaOffset.reservoirVersion(),\n        String.format(\n            \"Version mismatch for test '%s': DSv1=%d, DSv2=%d\",\n            testDescription,\n            dsv1DeltaOffset.reservoirVersion(),\n            dsv2DeltaOffset.reservoirVersion()));\n\n    assertEquals(\n        dsv1DeltaOffset.index(),\n        dsv2DeltaOffset.index(),\n        String.format(\n            \"Index mismatch for test '%s': DSv1=%d, DSv2=%d\",\n            testDescription, dsv1DeltaOffset.index(), dsv2DeltaOffset.index()));\n\n    assertEquals(\n        dsv1DeltaOffset.isInitialSnapshot(),\n        dsv2DeltaOffset.isInitialSnapshot(),\n        String.format(\n            \"isInitialSnapshot mismatch for test '%s': DSv1=%b, DSv2=%b\",\n            testDescription,\n            dsv1DeltaOffset.isInitialSnapshot(),\n            dsv2DeltaOffset.isInitialSnapshot()));\n  }\n\n  /** Helper method to execute SQL with String.format. */\n  private static void sql(String query, Object... args) {\n    DeltaV2TestBase.spark.sql(String.format(query, args));\n  }\n\n  /**\n   * Helper method to insert multiple versions of data into a test table.\n   *\n   * @param tableName The name of the table to insert into\n   * @param numVersions The number of versions (commits) to create\n   * @param rowsPerVersion The number of rows to insert per version\n   * @param includeEmptyVersion Whether to include an empty version (metadata-only change) at\n   *     version 1\n   */\n  private void insertVersions(\n      String tableName, int numVersions, int rowsPerVersion, boolean includeEmptyVersion) {\n    for (int i = 0; i < numVersions; i++) {\n      if (i == 1 && includeEmptyVersion) {\n        sql(\"ALTER TABLE %s SET TBLPROPERTIES ('test.property' = 'value')\", tableName);\n      } else {\n        StringBuilder values = new StringBuilder();\n        for (int j = 0; j < rowsPerVersion; j++) {\n          if (j > 0) values.append(\", \");\n          int id = i * rowsPerVersion + j;\n          values.append(String.format(\"(%d, 'User%d')\", id, id));\n        }\n        sql(\"INSERT INTO %s VALUES %s\", tableName, values.toString());\n      }\n    }\n  }\n\n  private Optional<DeltaSource.AdmissionLimits> createAdmissionLimits(\n      DeltaSource deltaSource, Optional<Integer> maxFiles, Optional<Long> maxBytes) {\n    Option<Object> scalaMaxFiles = ScalaUtils.toScalaOption(maxFiles.map(i -> (Object) i));\n    Option<Object> scalaMaxBytes = ScalaUtils.toScalaOption(maxBytes.map(l -> (Object) l));\n\n    if (scalaMaxFiles.isEmpty() && scalaMaxBytes.isEmpty()) {\n      return Optional.empty();\n    }\n    DeltaOptions options = emptyDeltaOptions();\n    return Optional.of(new DeltaSource.AdmissionLimits(options, scalaMaxFiles, scalaMaxBytes));\n  }\n\n  /** Helper method to format a DSv1 IndexedFile for debugging. */\n  private String formatIndexedFile(org.apache.spark.sql.delta.sources.IndexedFile file) {\n    return String.format(\n        \"IndexedFile(version=%d, index=%d, hasAdd=%b)\",\n        file.version(), file.index(), file.add() != null);\n  }\n\n  /** Helper method to format a DSv2 IndexedFile for debugging. */\n  private String formatKernelIndexedFile(IndexedFile file) {\n    return String.format(\n        \"IndexedFile(version=%d, index=%d, hasAdd=%b)\",\n        file.getVersion(), file.getIndex(), file.getAddFile() != null);\n  }\n\n  private List<Offset> advanceOffsetSequenceDsv1(\n      DeltaSource deltaSource, Offset startOffset, int numIterations, ReadLimit limit) {\n    List<Offset> offsets = new ArrayList<>();\n    offsets.add(startOffset);\n\n    Offset currentOffset = startOffset;\n    for (int i = 0; i < numIterations; i++) {\n      Offset nextOffset = deltaSource.latestOffset(currentOffset, limit);\n      offsets.add(nextOffset);\n      currentOffset = nextOffset;\n    }\n    return offsets;\n  }\n\n  private List<Offset> advanceOffsetSequenceDsv2(\n      SparkMicroBatchStream stream, Offset startOffset, int numIterations, ReadLimit limit) {\n    List<Offset> offsets = new ArrayList<>();\n    offsets.add(startOffset);\n\n    Offset currentOffset = startOffset;\n    for (int i = 0; i < numIterations; i++) {\n      Offset nextOffset = stream.latestOffset(currentOffset, limit);\n      offsets.add(nextOffset);\n      currentOffset = nextOffset;\n    }\n    return offsets;\n  }\n\n  private void compareOffsetSequence(\n      List<Offset> dsv1Offsets, List<Offset> dsv2Offsets, String testDescription) {\n    assertEquals(\n        dsv1Offsets.size(),\n        dsv2Offsets.size(),\n        String.format(\n            \"Offset sequence length mismatch for test '%s': DSv1=%d, DSv2=%d\",\n            testDescription, dsv1Offsets.size(), dsv2Offsets.size()));\n\n    for (int i = 0; i < dsv1Offsets.size(); i++) {\n      compareOffsets(\n          dsv1Offsets.get(i),\n          dsv2Offsets.get(i),\n          String.format(\"%s (iteration %d)\", testDescription, i));\n    }\n  }\n\n  private DeltaSource createDeltaSource(DeltaLog deltaLog, String tablePath) {\n    DeltaOptions options = emptyDeltaOptions();\n    return createDeltaSource(deltaLog, tablePath, options);\n  }\n\n  private DeltaSource createDeltaSource(DeltaLog deltaLog, String tablePath, DeltaOptions options) {\n    Seq<Expression> emptySeq = JavaConverters.asScalaBuffer(new ArrayList<Expression>()).toList();\n    Snapshot snapshot = deltaLog.update(false, Option.empty(), Option.empty());\n    return new DeltaSource(\n        spark,\n        deltaLog,\n        /* catalogTableOpt= */ Option.empty(),\n        options,\n        /* snapshotAtSourceInit= */ snapshot,\n        /* metadataPath= */ tablePath + \"/_checkpoint\",\n        /* metadataTrackingLog= */ Option.empty(),\n        /* filters= */ emptySeq);\n  }\n\n  /** Helper method to create a SparkMicroBatchStream with default values for testing. */\n  private SparkMicroBatchStream createTestStreamWithDefaults(\n      PathBasedSnapshotManager snapshotManager, Configuration hadoopConf, DeltaOptions options) {\n    io.delta.kernel.Snapshot snapshot = snapshotManager.loadLatestSnapshot();\n    StructType tableSchema =\n        io.delta.spark.internal.v2.utils.SchemaUtils.convertKernelSchemaToSparkSchema(\n            snapshot.getSchema());\n    return new SparkMicroBatchStream(\n        snapshotManager,\n        snapshot,\n        hadoopConf,\n        spark,\n        options,\n        /* tablePath= */ \"\",\n        /* dataSchema= */ tableSchema,\n        /* partitionSchema= */ new StructType(),\n        /* readDataSchema= */ new StructType(),\n        /* dataFilters= */ new org.apache.spark.sql.sources.Filter[0],\n        /* scalaOptions= */ scala.collection.immutable.Map$.MODULE$.empty());\n  }\n\n  /** Helper method to create DeltaOptions with read option for testing. */\n  private DeltaOptions createDeltaOptions(String optionName, String optionValue) {\n    if (optionName == null || optionValue == null) {\n      // Empty options\n      return emptyDeltaOptions();\n    } else {\n      // Create Scala Map with read option\n      scala.collection.immutable.Map<String, String> scalaMap =\n          Map$.MODULE$.<String, String>empty().updated(optionName, optionValue);\n      return new DeltaOptions(scalaMap, spark.sessionState().conf());\n    }\n  }\n\n  /** Helper method to test and compare getStartingVersion results from DSv1 and DSv2. */\n  private void testAndCompareStartingVersion(\n      String testTablePath,\n      String startingVersion,\n      Optional<Long> expectedVersion,\n      String testDescription)\n      throws Exception {\n    // DSv1: Create DeltaSource and get starting version\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    DeltaSource deltaSource =\n        createDeltaSource(\n            deltaLog, testTablePath, createDeltaOptions(\"startingVersion\", startingVersion));\n    scala.Option<Object> dsv1Result = deltaSource.getStartingVersion();\n\n    // DSv2: Create SparkMicroBatchStream and get starting version\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, new Configuration());\n    SparkMicroBatchStream dsv2Stream =\n        createTestStreamWithDefaults(\n            snapshotManager,\n            new Configuration(),\n            createDeltaOptions(\"startingVersion\", startingVersion));\n    Optional<Long> dsv2Result = dsv2Stream.getStartingVersion();\n\n    compareStartingVersionResults(dsv1Result, dsv2Result, expectedVersion, testDescription);\n  }\n\n  /** Helper method to compare getStartingVersion results from DSv1 and DSv2. */\n  private void compareStartingVersionResults(\n      scala.Option<Object> dsv1Result,\n      Optional<Long> dsv2Result,\n      Optional<Long> expectedVersion,\n      String testDescription) {\n\n    Optional<Long> dsv1Optional;\n    if (dsv1Result.isEmpty()) {\n      dsv1Optional = Optional.empty();\n    } else {\n      dsv1Optional = Optional.of((Long) dsv1Result.get());\n    }\n\n    assertEquals(\n        dsv1Optional,\n        dsv2Result,\n        String.format(\"DSv1 and DSv2 getStartingVersion should match for %s\", testDescription));\n\n    assertEquals(\n        expectedVersion,\n        dsv2Result,\n        String.format(\"DSv2 getStartingVersion should match for %s\", testDescription));\n  }\n\n  @Test\n  public void testMemoryProtection_initialSnapshotTooLarge(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_memory_protection_\" + System.nanoTime();\n    createEmptyTestTable(testTablePath, testTableName);\n\n    // At version 5, there will be at least 25 files.\n    insertVersions(\n        testTableName,\n        /* numVersions= */ 10,\n        /* rowsPerVersion= */ 5,\n        /* includeEmptyVersion= */ false);\n\n    String configKey = DeltaSQLConf.DELTA_STREAMING_INITIAL_SNAPSHOT_MAX_FILES().key();\n    spark.conf().set(configKey, \"5\");\n\n    try {\n      Configuration hadoopConf = new Configuration();\n      PathBasedSnapshotManager snapshotManager =\n          new PathBasedSnapshotManager(testTablePath, hadoopConf);\n      SparkMicroBatchStream stream =\n          createTestStreamWithDefaults(snapshotManager, hadoopConf, emptyDeltaOptions());\n\n      long version = 5L;\n      long fromIndex = DeltaSourceOffset.BASE_INDEX();\n      boolean isInitialSnapshot = true;\n\n      RuntimeException exception =\n          assertThrows(\n              RuntimeException.class,\n              () -> {\n                try (CloseableIterator<IndexedFile> iter =\n                    stream.getFileChanges(\n                        version, fromIndex, isInitialSnapshot, Optional.empty())) {\n                  while (iter.hasNext()) {\n                    iter.next();\n                  }\n                }\n              });\n\n      String errorMessage = exception.getMessage();\n      assertTrue(errorMessage.contains(\"DELTA_STREAMING_INITIAL_SNAPSHOT_TOO_LARGE\"));\n      assertTrue(\n          errorMessage.contains(\"initial snapshot\") || errorMessage.contains(\"Initial snapshot\"));\n    } finally {\n      spark.conf().unset(configKey);\n    }\n  }\n\n  /**\n   * Simulates the Kernel integration path where {@code DefaultJsonHandler.hasNext()} wraps a {@link\n   * ClosedByInterruptException} inside a {@link io.delta.kernel.exceptions.KernelEngineException}.\n   * Verifies that {@code findClosedByInterruptCause} extracts the interrupt cause so {@code\n   * latestOffset()} can re-throw it as {@link java.io.UncheckedIOException} for Spark's {@code\n   * isInterruptedByStop}.\n   */\n  @Test\n  public void testFindClosedByInterruptCause() {\n    // KernelEngineException wrapping ClosedByInterruptException -> present\n    ClosedByInterruptException cbie = new ClosedByInterruptException();\n    assertThat(\n            SparkMicroBatchStream.findClosedByInterruptCause(\n                new io.delta.kernel.exceptions.KernelEngineException(\"readJsonFile\", cbie)))\n        .isPresent()\n        .contains(cbie);\n\n    // Plain RuntimeException -> empty\n    assertThat(SparkMicroBatchStream.findClosedByInterruptCause(new RuntimeException(\"unrelated\")))\n        .isEmpty();\n\n    // KernelEngineException wrapping a different IOException -> empty\n    assertThat(\n            SparkMicroBatchStream.findClosedByInterruptCause(\n                new io.delta.kernel.exceptions.KernelEngineException(\n                    \"readJsonFile\", new java.io.FileNotFoundException(\"missing\"))))\n        .isEmpty();\n  }\n\n  /** Regression test: closing the wrapped iterator must also close CommitActions. */\n  @Test\n  public void testWrapIteratorWithCommitClose_closesCommitOnIteratorClose() throws Exception {\n    AtomicBoolean commitClosed = new AtomicBoolean(false);\n    AtomicBoolean innerClosed = new AtomicBoolean(false);\n\n    CloseableIterator<IndexedFile> inner =\n        new CloseableIterator<IndexedFile>() {\n          private boolean consumed = false;\n\n          @Override\n          public boolean hasNext() {\n            return !consumed;\n          }\n\n          @Override\n          public IndexedFile next() {\n            consumed = true;\n            return new IndexedFile(/* version= */ 1L, /* index= */ 0L, /* addFile= */ null);\n          }\n\n          @Override\n          public void close() {\n            innerClosed.set(true);\n          }\n        };\n\n    try (CloseableIterator<IndexedFile> wrapped =\n        SparkMicroBatchStream.wrapIteratorWithCommitClose(\n            inner, newTrackingCommitActions(commitClosed))) {\n      while (wrapped.hasNext()) {\n        wrapped.next();\n      }\n    }\n\n    assertTrue(innerClosed.get(), \"Inner iterator should be closed\");\n    assertTrue(commitClosed.get(), \"CommitActions should be closed\");\n  }\n\n  /** Closing the wrapped iterator before full consumption must still close CommitActions. */\n  @Test\n  public void testWrapIteratorWithCommitClose_closesCommitOnEarlyClose() throws Exception {\n    AtomicBoolean commitClosed = new AtomicBoolean(false);\n\n    CloseableIterator<IndexedFile> inner =\n        new CloseableIterator<IndexedFile>() {\n          private int remaining = 10;\n\n          @Override\n          public boolean hasNext() {\n            return remaining > 0;\n          }\n\n          @Override\n          public IndexedFile next() {\n            remaining--;\n            return new IndexedFile(/* version= */ 1L, /* index= */ remaining, /* addFile= */ null);\n          }\n\n          @Override\n          public void close() {}\n        };\n\n    try (CloseableIterator<IndexedFile> wrapped =\n        SparkMicroBatchStream.wrapIteratorWithCommitClose(\n            inner, newTrackingCommitActions(commitClosed))) {\n      assertTrue(wrapped.hasNext());\n      wrapped.next();\n      // Intentionally don't consume the rest\n    }\n\n    assertTrue(commitClosed.get(), \"CommitActions should be closed on early termination\");\n  }\n\n  /** If inner.close() throws, CommitActions must still be closed. */\n  @Test\n  public void testWrapIteratorWithCommitClose_closesCommitEvenWhenInnerCloseThrows()\n      throws Exception {\n    AtomicBoolean commitClosed = new AtomicBoolean(false);\n\n    CloseableIterator<IndexedFile> inner =\n        new CloseableIterator<IndexedFile>() {\n          @Override\n          public boolean hasNext() {\n            return false;\n          }\n\n          @Override\n          public IndexedFile next() {\n            throw new java.util.NoSuchElementException();\n          }\n\n          @Override\n          public void close() {\n            throw new RuntimeException(\"inner close failed\");\n          }\n        };\n\n    CloseableIterator<IndexedFile> wrapped =\n        SparkMicroBatchStream.wrapIteratorWithCommitClose(\n            inner, newTrackingCommitActions(commitClosed));\n    try {\n      wrapped.close();\n    } catch (RuntimeException e) {\n      // Expected — inner.close() threw\n    }\n\n    assertTrue(commitClosed.get(), \"CommitActions should be closed even when inner.close() throws\");\n  }\n\n  /** Creates a CommitActions stub that sets {@code closedFlag} to true on close. */\n  private static CommitActions newTrackingCommitActions(AtomicBoolean closedFlag) {\n    return new CommitActions() {\n      @Override\n      public long getVersion() {\n        return 1L;\n      }\n\n      @Override\n      public long getTimestamp() {\n        return 0L;\n      }\n\n      @Override\n      public CloseableIterator<ColumnarBatch> getActions() {\n        throw new UnsupportedOperationException(\"not needed for this test\");\n      }\n\n      @Override\n      public void close() {\n        closedFlag.set(true);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/SparkScanBuilderTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.expressions.Column;\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport io.delta.spark.internal.v2.snapshot.PathBasedSnapshotManager;\nimport java.io.File;\nimport java.lang.reflect.Field;\nimport java.math.BigDecimal;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.apache.spark.sql.connector.read.Scan;\nimport org.apache.spark.sql.connector.read.streaming.MicroBatchStream;\nimport org.apache.spark.sql.sources.*;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\npublic class SparkScanBuilderTest extends DeltaV2TestBase {\n\n  @Test\n  public void testBuild_returnsScanWithExpectedSchema(@TempDir File tempDir) {\n    String path = tempDir.getAbsolutePath();\n    String tableName = \"scan_builder_test\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING, dep_id INT) USING delta PARTITIONED BY (dep_id) LOCATION '%s'\",\n            tableName, path));\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(path, spark.sessionState().newHadoopConf());\n    Snapshot snapshot = snapshotManager.loadLatestSnapshot();\n    StructType dataSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true)\n            });\n    StructType partitionSchema =\n        DataTypes.createStructType(\n            new StructField[] {DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)});\n    StructType tableSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true),\n              DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)\n            });\n    SparkScanBuilder builder =\n        new SparkScanBuilder(\n            tableName,\n            snapshot,\n            snapshotManager,\n            dataSchema,\n            partitionSchema,\n            tableSchema,\n            Optional.empty(),\n            CaseInsensitiveStringMap.empty());\n\n    StructType expectedSparkSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true /*nullable*/),\n              DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)\n            });\n\n    builder.pruneColumns(expectedSparkSchema);\n    Scan scan = builder.build();\n\n    assertTrue(scan instanceof SparkScan);\n    assertEquals(expectedSparkSchema, scan.readSchema());\n  }\n\n  @Test\n  public void testToMicroBatchStream_returnsSparkMicroBatchStream(@TempDir File tempDir) {\n    String path = tempDir.getAbsolutePath();\n    String tableName = \"microbatch_test\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING, dep_id INT) USING delta PARTITIONED BY (dep_id) LOCATION '%s'\",\n            tableName, path));\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(path, spark.sessionState().newHadoopConf());\n    Snapshot snapshot = snapshotManager.loadLatestSnapshot();\n    StructType dataSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true)\n            });\n    StructType partitionSchema =\n        DataTypes.createStructType(\n            new StructField[] {DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)});\n    StructType tableSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true),\n              DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)\n            });\n    SparkScanBuilder builder =\n        new SparkScanBuilder(\n            tableName,\n            snapshot,\n            snapshotManager,\n            dataSchema,\n            partitionSchema,\n            tableSchema,\n            Optional.empty(),\n            CaseInsensitiveStringMap.empty());\n    Scan scan = builder.build();\n\n    String checkpointLocation = \"/tmp/checkpoint\";\n    MicroBatchStream microBatchStream = scan.toMicroBatchStream(checkpointLocation);\n\n    assertNotNull(microBatchStream, \"MicroBatchStream should not be null\");\n    assertTrue(\n        microBatchStream instanceof SparkMicroBatchStream,\n        \"MicroBatchStream should be an instance of SparkMicroBatchStream\");\n  }\n\n  @Test\n  public void testPushFilters_singleSupportedDataFilter(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new EqualTo(\"id\", 100)},\n        // expected post-scan filters\n        new Filter[] {new EqualTo(\"id\", 100)},\n        // expected pushed filters\n        new Filter[] {new EqualTo(\"id\", 100)},\n        // expected pushed kernel predicates\n        new Predicate[] {new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100))},\n        // expected data filters\n        new Filter[] {new EqualTo(\"id\", 100)},\n        // expected kernelScanBuilder.predicate\n        Optional.of(new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100))));\n  }\n\n  @Test\n  public void testPushFilters_singleUnsupportedDataFilter(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        new Filter[] {new StringEndsWith(\"name\", \"test\")}, // input filters\n        new Filter[] {\n          new StringEndsWith(\"name\", \"test\")\n        }, // expected post-scan filters (unsupported, stays for row-level eval)\n        new Filter[] {}, // expected pushed filters (nothing pushed)\n        new Predicate[] {}, // expected pushed kernel predicates\n        new Filter[] {new StringEndsWith(\"name\", \"test\")}, // expected data filters\n        Optional.empty() // expected kernelScanBuilder.predicate\n        );\n  }\n\n  @Test\n  public void testPushFilters_singleSupportedDataFilter_StringStartsWith(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        new Filter[] {new StringStartsWith(\"name\", \"test\")}, // input filters\n        new Filter[] {\n          new StringStartsWith(\"name\", \"test\")\n        }, // expected post-scan filters (data filter still needs row-level eval)\n        new Filter[] {new StringStartsWith(\"name\", \"test\")}, // expected pushed filters\n        new Predicate[] {\n          new Predicate(\"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"test\"))\n        }, // expected pushed kernel predicates\n        new Filter[] {new StringStartsWith(\"name\", \"test\")}, // expected data filters\n        Optional.of(\n            new Predicate(\n                \"STARTS_WITH\",\n                new Column(\"name\"),\n                Literal.ofString(\"test\"))) // expected kernelScanBuilder.predicate\n        );\n  }\n\n  @Test\n  public void testPushFilters_multiSupportedDataFilters(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50)},\n        // expected post-scan filters\n        new Filter[] {new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50)},\n        // expected pushed filters\n        new Filter[] {new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50)},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n          new Predicate(\">\", new Column(\"id\"), Literal.ofInt(50))\n        },\n        // expected data filters\n        new Filter[] {new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50)},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"AND\",\n                Arrays.asList(\n                    new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                    new Predicate(\">\", new Column(\"id\"), Literal.ofInt(50))))));\n  }\n\n  @Test\n  public void testPushFilters_mixedSupportedAndUnsupportedDataFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new EqualTo(\"id\", 100), // supported\n          new StringEndsWith(\"name\", \"test\") // unsupported\n        },\n        // expected post-scan filters\n        new Filter[] {new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"test\")},\n        // expected pushed filters (only the supported EqualTo is pushed)\n        new Filter[] {new EqualTo(\"id\", 100)},\n        // expected pushed kernel predicates\n        new Predicate[] {new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100))},\n        // expected data filters\n        new Filter[] {new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"test\")},\n        // expected kernelScanBuilder.predicate (only EqualTo was pushed to kernel)\n        Optional.of(new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100))));\n  }\n\n  @Test\n  public void testPushFilters_singleSupportedPartitionFilter(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new EqualTo(\"dep_id\", 1)},\n        // expected post-scan filters\n        new Filter[] {},\n        // expected pushed filters\n        new Filter[] {new EqualTo(\"dep_id\", 1)},\n        // expected pushed kernel predicates\n        new Predicate[] {new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(1))},\n        // expected data filters\n        new Filter[] {},\n        // expected kernelScanBuilder.predicate\n        Optional.of(new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(1))));\n  }\n\n  @Test\n  public void testPushFilters_singleUnsupportedPartitionFilter(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        new Filter[] {new StringStartsWith(\"dep_id\", \"1\")}, // input filters\n        new Filter[] {}, // expected post-scan filters (partition filter, fully pushed)\n        new Filter[] {new StringStartsWith(\"dep_id\", \"1\")}, // expected pushed filters\n        new Predicate[] {\n          new Predicate(\"STARTS_WITH\", new Column(\"dep_id\"), Literal.ofString(\"1\"))\n        }, // expected pushed kernel predicates\n        new Filter[] {}, // expected data filters\n        Optional.of(\n            new Predicate(\n                \"STARTS_WITH\",\n                new Column(\"dep_id\"),\n                Literal.ofString(\"1\"))) // expected kernelScanBuilder.predicate\n        );\n  }\n\n  @Test\n  public void testPushFilters_multiSupportedPartitionFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new EqualTo(\"dep_id\", 2), new GreaterThan(\"dep_id\", 1)},\n        // expected post-scan filters\n        new Filter[] {},\n        // expected pushed filters\n        new Filter[] {new EqualTo(\"dep_id\", 2), new GreaterThan(\"dep_id\", 1)},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(2)),\n          new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1))\n        },\n        // expected data filters\n        new Filter[] {},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"AND\",\n                Arrays.asList(\n                    new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(2)),\n                    new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1))))));\n  }\n\n  @Test\n  public void testPushFilters_mixedSupportedAndUnsupportedPartitionFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new EqualTo(\"dep_id\", 1), // supported\n          new StringStartsWith(\"dep_id\", \"1\") // unsupported\n        },\n        // expected post-scan filters\n        new Filter[] {},\n        // expected pushed filters\n        new Filter[] {new EqualTo(\"dep_id\", 1), new StringStartsWith(\"dep_id\", \"1\")},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(1)),\n          new Predicate(\"STARTS_WITH\", new Column(\"dep_id\"), Literal.ofString(\"1\"))\n        },\n        // expected data filters\n        new Filter[] {},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"AND\",\n                new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(1)),\n                new Predicate(\"STARTS_WITH\", new Column(\"dep_id\"), Literal.ofString(\"1\")))));\n  }\n\n  @Test\n  public void testPushFilters_mixedFilters(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new EqualTo(\"id\", 100), // data filter, supported\n          new StringStartsWith(\"name\", \"foo\"), // data filter, unsupported\n          new GreaterThan(\"dep_id\", 1), // partition filter, supported\n          new StringEndsWith(\"dep_id\", \"1\") // partition filter, unsupported\n        },\n        // expected post-scan filters\n        new Filter[] {\n          new EqualTo(\"id\", 100), // data filter, supported\n          new StringStartsWith(\"name\", \"foo\"), // data filter, unsupported\n          new StringEndsWith(\"dep_id\", \"1\") // partition filter, unsupported\n        },\n        // expected pushed filters\n        new Filter[] {\n          new EqualTo(\"id\", 100), // data filter, supported\n          new StringStartsWith(\"name\", \"foo\"), // data filter, supported\n          new GreaterThan(\"dep_id\", 1) // partition filter, supported\n        },\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n          new Predicate(\"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"foo\")),\n          new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1))\n        },\n        // expected data filters\n        new Filter[] {\n          new EqualTo(\"id\", 100), // data filter, supported\n          new StringStartsWith(\"name\", \"foo\") // data filter, supported\n        },\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"AND\",\n                new Predicate(\n                    \"AND\",\n                    new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                    new Predicate(\"STARTS_WITH\", new Column(\"name\"), Literal.ofString(\"foo\"))),\n                new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1)))));\n  }\n\n  @Test\n  public void testPushFilters_ORFilters(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50))},\n        // expected post-scan filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50))},\n        // expected pushed filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50))},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\n              \"OR\",\n              Arrays.asList(\n                  new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                  new Predicate(\">\", new Column(\"id\"), Literal.ofInt(50))))\n        },\n        // expected data filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"id\", 50))},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"OR\",\n                Arrays.asList(\n                    new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                    new Predicate(\">\", new Column(\"id\"), Literal.ofInt(50))))));\n  }\n\n  @Test\n  public void testPushFilters_ORSupportedAndUnsupportedDataFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    // OR(supported, unsupported) cannot be partially pushed: if one branch is unsupported,\n    // the whole OR must remain for post-scan evaluation and nothing is pushed to the kernel.\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"foo\"))},\n        // expected post-scan filters (whole OR stays, since one branch is unsupported)\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"foo\"))},\n        // expected pushed filters (nothing pushed)\n        new Filter[] {},\n        // expected pushed kernel predicates\n        new Predicate[] {},\n        // expected data filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"foo\"))},\n        // expected kernelScanBuilder.predicate\n        Optional.empty());\n  }\n\n  @Test\n  public void testPushFilters_ORSupportedDataAndPartitionFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1)),\n        },\n        // expected post-scan filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))},\n        // expected pushed filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\n              \"OR\",\n              Arrays.asList(\n                  new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                  new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1))))\n        },\n        // expected data filters\n        new Filter[] {new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"OR\",\n                Arrays.asList(\n                    new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                    new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1))))));\n  }\n\n  /*\n   * (partitionFilterA AND partitionFilterB) OR partitionFilterC\n   * where A = EqualTo(\"dep_id\", 1), B = StringStartsWith(\"dep_id\", \"1\"), C = GreaterThan(\"dep_id\", 2)\n   * All three are fully supported partition filters, so the whole expression is pushed down.\n   *\n   * Expected post-scan filters: none (all partition filters, fully pushed)\n   * Expected pushed filters: (A AND B) OR C\n   * Expected pushed kernel predicates: (predicateA AND predicateB) OR predicateC\n   * Expected data filters: none\n   * Expected kernelScanBuilder.predicate: (predicateA AND predicateB) OR predicateC\n   */\n  @Test\n  public void testPushFilters_mixedORandAND(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new Or(\n              new And(new EqualTo(\"dep_id\", 1), new StringStartsWith(\"dep_id\", \"1\")),\n              new GreaterThan(\"dep_id\", 2))\n        },\n        // expected post-scan filters\n        new Filter[] {},\n        // expected pushed filters\n        new Filter[] {\n          new Or(\n              new And(new EqualTo(\"dep_id\", 1), new StringStartsWith(\"dep_id\", \"1\")),\n              new GreaterThan(\"dep_id\", 2))\n        },\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\n              \"OR\",\n              Arrays.asList(\n                  new Predicate(\n                      \"AND\",\n                      Arrays.asList(\n                          new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(1)),\n                          new Predicate(\n                              \"STARTS_WITH\", new Column(\"dep_id\"), Literal.ofString(\"1\")))),\n                  new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(2))))\n        },\n        // expected data filters\n        new Filter[] {},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"OR\",\n                Arrays.asList(\n                    new Predicate(\n                        \"AND\",\n                        Arrays.asList(\n                            new Predicate(\"=\", new Column(\"dep_id\"), Literal.ofInt(1)),\n                            new Predicate(\n                                \"STARTS_WITH\", new Column(\"dep_id\"), Literal.ofString(\"1\")))),\n                    new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(2))))));\n  }\n\n  @Test\n  public void testPushFilters_NOTFilters(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new Not(new EqualTo(\"id\", 100))},\n        // expected post-scan filters\n        new Filter[] {new Not(new EqualTo(\"id\", 100))},\n        // expected pushed filters\n        new Filter[] {new Not(new EqualTo(\"id\", 100))},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\"NOT\", new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)))\n        },\n        // expected data filters\n        new Filter[] {new Not(new EqualTo(\"id\", 100))},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\"NOT\", new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)))));\n  }\n\n  @Test\n  public void testPushFilters_NOTSupportedDataANDSupportedPartitionFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new Not(new And(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))),\n        },\n        // expected post-scan filters\n        new Filter[] {\n          new Not(new And(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))),\n        },\n        // expected pushed filters\n        new Filter[] {\n          new Not(new And(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))),\n        },\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\n              \"NOT\",\n              new Predicate(\n                  \"AND\",\n                  Arrays.asList(\n                      new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                      new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1)))))\n        },\n        // expected data filters\n        new Filter[] {\n          new Not(new And(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))),\n        },\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"NOT\",\n                new Predicate(\n                    \"AND\",\n                    Arrays.asList(\n                        new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                        new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1)))))));\n  }\n\n  @Test\n  public void testPushFilters_NOTSupportedDataANDUnsupportedDataFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {new Not(new And(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"bar\")))},\n        // expected post-scan filters\n        new Filter[] {new Not(new And(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"bar\")))},\n        // expected pushed filters\n        new Filter[] {},\n        // expected pushed kernel predicates\n        new Predicate[] {},\n        // expected data filters\n        new Filter[] {new Not(new And(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"bar\")))},\n        // expected kernelScanBuilder.predicate\n        Optional.empty());\n  }\n\n  @Test\n  public void testPushFilters_NOTSupportedDataORSupportedPartitionFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new Not(new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1))),\n        },\n        // expected post-scan filters\n        new Filter[] {new Not(new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1)))},\n        // expected pushed filters\n        new Filter[] {new Not(new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1)))},\n        // expected pushed kernel predicates\n        new Predicate[] {\n          new Predicate(\n              \"NOT\",\n              new Predicate(\n                  \"OR\",\n                  Arrays.asList(\n                      new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                      new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1)))))\n        },\n        // expected data filters\n        new Filter[] {new Not(new Or(new EqualTo(\"id\", 100), new GreaterThan(\"dep_id\", 1)))},\n        // expected kernelScanBuilder.predicate\n        Optional.of(\n            new Predicate(\n                \"NOT\",\n                new Predicate(\n                    \"OR\",\n                    Arrays.asList(\n                        new Predicate(\"=\", new Column(\"id\"), Literal.ofInt(100)),\n                        new Predicate(\">\", new Column(\"dep_id\"), Literal.ofInt(1)))))));\n  }\n\n  @Test\n  public void testPushFilters_NOTSupportedDataORUnsupportedDataFilters(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createTestScanBuilder(tempDir);\n\n    // NOT(OR(supported, unsupported)): the OR branch contains an unsupported filter\n    // (StringEndsWith),\n    // so the whole NOT(OR(...)) cannot be pushed to the kernel.\n    checkSupportsPushDownFilters(\n        builder,\n        // input filters\n        new Filter[] {\n          new Not(new Or(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"foo\"))),\n        },\n        // expected post-scan filters (whole NOT(OR) stays, unsupported branch blocks pushdown)\n        new Filter[] {new Not(new Or(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"foo\")))},\n        // expected pushed filters (nothing pushed)\n        new Filter[] {},\n        // expected pushed kernel predicates\n        new Predicate[] {},\n        // expected data filters\n        new Filter[] {new Not(new Or(new EqualTo(\"id\", 100), new StringEndsWith(\"name\", \"foo\")))},\n        // expected kernelScanBuilder.predicate\n        Optional.empty());\n  }\n\n  private void checkSupportsPushDownFilters(\n      SparkScanBuilder builder,\n      Filter[] inputFilters,\n      Filter[] expectedPostScanFilters,\n      Filter[] expectedPushedFilters,\n      Predicate[] expectedPushedKernelPredicates,\n      Filter[] expectedDataFilters,\n      Optional<Predicate> expectedKernelScanBuilderPredicate)\n      throws Exception {\n    Filter[] postScanFilters = builder.pushFilters(inputFilters);\n\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedPostScanFilters)),\n        new HashSet<>(Arrays.asList(postScanFilters)));\n\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedPushedFilters)),\n        new HashSet<>(Arrays.asList(builder.pushedFilters())));\n\n    Predicate[] pushedPredicates = getPushedKernelPredicates(builder);\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedPushedKernelPredicates)),\n        new HashSet<>(Arrays.asList(pushedPredicates)));\n\n    Filter[] dataFilters = getDataFilters(builder);\n    assertEquals(\n        new HashSet<>(Arrays.asList(expectedDataFilters)),\n        new HashSet<>(Arrays.asList(dataFilters)));\n\n    Optional<Predicate> predicateOpt = getKernelScanBuilderPredicate(builder);\n    assertEquals(expectedKernelScanBuilderPredicate, predicateOpt);\n  }\n\n  private SparkScanBuilder createTestScanBuilder(File tempDir) {\n    StructType dataSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true)\n            });\n    StructType partitionSchema =\n        DataTypes.createStructType(\n            new StructField[] {DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)});\n    StructType tableSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"name\", DataTypes.StringType, true),\n              DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)\n            });\n    return createScanBuilder(tempDir, dataSchema, partitionSchema, tableSchema);\n  }\n\n  /**\n   * Integration test: decimal widening end-to-end through pushFilters() → classifyFilter() →\n   * convertComparisonLiteral(). Verifies that a decimal literal Decimal(5,2) is widened to match\n   * column type Decimal(7,2) when pushed through the full filter pushdown path.\n   */\n  @Test\n  public void testPushFilters_decimalWideningEndToEnd(@TempDir File tempDir) throws Exception {\n    SparkScanBuilder builder = createDecimalTestScanBuilder(tempDir);\n\n    // price = 100.00 where literal is Decimal(5,2) and column is Decimal(7,2)\n    Filter[] sparkFilter = new Filter[] {new EqualTo(\"price\", new BigDecimal(\"100.00\"))};\n    Predicate kernelPredicate =\n        new Predicate(\"=\", new Column(\"price\"), Literal.ofDecimal(new BigDecimal(\"100.00\"), 7, 2));\n\n    checkSupportsPushDownFilters(\n        builder,\n        sparkFilter, // input filters\n        sparkFilter, // expected post-scan filters (data filter, stays for row-level eval)\n        sparkFilter, // expected pushed filters\n        new Predicate[] {kernelPredicate}, // expected pushed kernel predicates (widened)\n        sparkFilter, // expected data filters\n        Optional.of(kernelPredicate)); // expected kernelScanBuilder.predicate\n  }\n\n  /**\n   * Integration test: decimal literal with scale exceeding column scale is rejected during\n   * pushFilters(). The filter AND(price > 99.999, price < 200.00) should partially push only the\n   * right side because 99.999 has scale=3 exceeding column's Decimal(7,2) scale=2.\n   */\n  @Test\n  public void testPushFilters_decimalRejectionPartialPushDown(@TempDir File tempDir)\n      throws Exception {\n    SparkScanBuilder builder = createDecimalTestScanBuilder(tempDir);\n\n    // AND(price > 99.999, price < 200.00): left side has scale=3 exceeding column's scale=2\n    Filter[] sparkFilter =\n        new Filter[] {\n          new And(\n              new GreaterThan(\"price\", new BigDecimal(\"99.999\")),\n              new LessThan(\"price\", new BigDecimal(\"200.00\")))\n        };\n    // Only the right side (price < 200.00) is pushed as a kernel predicate\n    Predicate kernelPredicate =\n        new Predicate(\"<\", new Column(\"price\"), Literal.ofDecimal(new BigDecimal(\"200.00\"), 7, 2));\n\n    checkSupportsPushDownFilters(\n        builder,\n        sparkFilter, // input filters\n        sparkFilter, // expected post-scan filters (partial conversion, stays for row-level eval)\n        new Filter[] {}, // expected pushed filters (partial: Spark filter not added)\n        new Predicate[] {kernelPredicate}, // expected pushed kernel predicates\n        sparkFilter, // expected data filters\n        Optional.of(kernelPredicate)); // expected kernelScanBuilder.predicate\n  }\n\n  private SparkScanBuilder createDecimalTestScanBuilder(File tempDir) {\n    StructType dataSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"price\", DataTypes.createDecimalType(7, 2), true),\n              DataTypes.createStructField(\"quantity\", DataTypes.IntegerType, true)\n            });\n    StructType partitionSchema =\n        DataTypes.createStructType(\n            new StructField[] {DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)});\n    StructType tableSchema =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"price\", DataTypes.createDecimalType(7, 2), true),\n              DataTypes.createStructField(\"quantity\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"dep_id\", DataTypes.IntegerType, true)\n            });\n    return createScanBuilder(tempDir, dataSchema, partitionSchema, tableSchema);\n  }\n\n  /**\n   * Shared helper for creating a SparkScanBuilder with the given schemas. Both\n   * createTestScanBuilder and createDecimalTestScanBuilder delegate to this method to avoid\n   * duplicating snapshot loading and builder instantiation logic.\n   */\n  private SparkScanBuilder createScanBuilder(\n      File tempDir, StructType dataSchema, StructType partitionSchema, StructType tableSchema) {\n    String path = tempDir.getAbsolutePath();\n    String tableName = String.format(\"test_%d\", System.currentTimeMillis());\n    // Build CREATE TABLE SQL from the tableSchema and partitionSchema\n    StringBuilder columns = new StringBuilder();\n    Set<String> partitionCols = new HashSet<>();\n    for (StructField f : partitionSchema.fields()) {\n      partitionCols.add(f.name());\n    }\n    for (StructField f : tableSchema.fields()) {\n      if (columns.length() > 0) columns.append(\", \");\n      columns.append(f.name()).append(\" \").append(f.dataType().sql());\n    }\n    String partitionColNames = String.join(\", \", partitionCols);\n    spark.sql(\n        String.format(\n            \"CREATE OR REPLACE TABLE %s (%s) USING delta PARTITIONED BY (%s) LOCATION '%s'\",\n            tableName, columns, partitionColNames, path));\n    PathBasedSnapshotManager snapshotManager =\n        new PathBasedSnapshotManager(path, spark.sessionState().newHadoopConf());\n    Snapshot snapshot = snapshotManager.loadLatestSnapshot();\n    return new SparkScanBuilder(\n        tableName,\n        snapshot,\n        snapshotManager,\n        dataSchema,\n        partitionSchema,\n        tableSchema,\n        Optional.empty(),\n        CaseInsensitiveStringMap.empty());\n  }\n\n  private Predicate[] getPushedKernelPredicates(SparkScanBuilder builder) throws Exception {\n    // TODO: replace reflection with other testing manners, possibly Mockito ArgumentCaptor\n    Field field = SparkScanBuilder.class.getDeclaredField(\"pushedKernelPredicates\");\n    field.setAccessible(true);\n    return (Predicate[]) field.get(builder);\n  }\n\n  private Filter[] getDataFilters(SparkScanBuilder builder) throws Exception {\n    // TODO: replace reflection with other testing manners, possibly Mockito ArgumentCaptor\n    Field field = SparkScanBuilder.class.getDeclaredField(\"dataFilters\");\n    field.setAccessible(true);\n    return (Filter[]) field.get(builder);\n  }\n\n  private Optional<Predicate> getKernelScanBuilderPredicate(SparkScanBuilder builder)\n      throws Exception {\n    // TODO: replace reflection with other testing manners, possibly Mockito ArgumentCaptor\n    Field field = SparkScanBuilder.class.getDeclaredField(\"kernelScanBuilder\");\n    field.setAccessible(true);\n    Object kernelScanBuilder = field.get(builder);\n\n    Field predicateField = kernelScanBuilder.getClass().getDeclaredField(\"predicate\");\n    predicateField.setAccessible(true);\n    Object raw = predicateField.get(kernelScanBuilder);\n    if (raw == null) {\n      return Optional.empty();\n    }\n    Optional<?> opt = (Optional<?>) raw;\n    return opt.map(Predicate.class::cast);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/SparkScanTest.java",
    "content": "package io.delta.spark.internal.v2.read;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport io.delta.spark.internal.v2.catalog.SparkTable;\nimport io.delta.spark.internal.v2.utils.ScalaUtils;\nimport java.io.File;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.catalyst.TableIdentifier;\nimport org.apache.spark.sql.catalyst.catalog.CatalogColumnStat;\nimport org.apache.spark.sql.catalyst.catalog.CatalogStatistics;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.connector.catalog.Identifier;\nimport org.apache.spark.sql.connector.expressions.Expression;\nimport org.apache.spark.sql.connector.expressions.FieldReference;\nimport org.apache.spark.sql.connector.expressions.LiteralValue;\nimport org.apache.spark.sql.connector.expressions.NamedReference;\nimport org.apache.spark.sql.connector.expressions.filter.Predicate;\nimport org.apache.spark.sql.connector.read.Batch;\nimport org.apache.spark.sql.connector.read.HasPartitionKey;\nimport org.apache.spark.sql.connector.read.InputPartition;\nimport org.apache.spark.sql.connector.read.Scan;\nimport org.apache.spark.sql.connector.read.ScanBuilder;\nimport org.apache.spark.sql.connector.read.Statistics;\nimport org.apache.spark.sql.connector.read.colstats.ColumnStatistics;\nimport org.apache.spark.sql.connector.read.partitioning.KeyGroupedPartitioning;\nimport org.apache.spark.sql.connector.read.partitioning.Partitioning;\nimport org.apache.spark.sql.connector.read.partitioning.UnknownPartitioning;\nimport org.apache.spark.sql.delta.DeltaOptions;\nimport org.apache.spark.sql.execution.datasources.FilePartition;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\npublic class SparkScanTest extends DeltaV2TestBase {\n\n  private static String tablePath;\n  private static final String tableName = \"deltatbl_partitioned\";\n\n  @BeforeAll\n  public static void setupPartitionedTable(@TempDir File tempDir) {\n    createPartitionedTable(tableName, tempDir.getAbsolutePath());\n    tablePath = tempDir.getAbsolutePath();\n  }\n\n  private final CaseInsensitiveStringMap options =\n      new CaseInsensitiveStringMap(new java.util.HashMap<>());\n\n  private final SparkTable table =\n      new SparkTable(\n          Identifier.of(new String[] {\"spark_catalog\", \"default\"}, tableName), tablePath, options);\n\n  protected static final Predicate cityPredicate =\n      new Predicate(\n          \"=\",\n          new Expression[] {\n            FieldReference.apply(\"city\"), LiteralValue.apply(\"hz\", DataTypes.StringType)\n          });\n\n  protected static final Predicate datePredicate =\n      new Predicate(\n          \"=\",\n          new Expression[] {\n            FieldReference.apply(\"date\"), LiteralValue.apply(\"20180520\", DataTypes.StringType)\n          });\n\n  protected static final Predicate partPredicate =\n      new Predicate(\n          \">\",\n          new Expression[] {\n            FieldReference.apply(\"part\"), LiteralValue.apply(1, DataTypes.IntegerType)\n          });\n\n  protected static final Predicate dataPredicate =\n      new Predicate(\n          \">\",\n          new Expression[] {\n            FieldReference.apply(\"cnt\"), LiteralValue.apply(10, DataTypes.IntegerType)\n          });\n\n  protected static final Predicate negativeCityPredicate =\n      new Predicate(\n          \"=\",\n          new Expression[] {\n            FieldReference.apply(\"city\"), LiteralValue.apply(\"zz\", DataTypes.StringType)\n          });\n\n  protected static final Predicate interColPredicate =\n      new Predicate(\n          \"!=\", new Expression[] {FieldReference.apply(\"city\"), FieldReference.apply(\"date\")});\n\n  protected static final Predicate negativeInterColPredicate =\n      new Predicate(\n          \"=\", new Expression[] {FieldReference.apply(\"city\"), FieldReference.apply(\"date\")});\n\n  // a full set of cities in the golden table, repsents all partitions\n  protected static final List<String> allCities =\n      Arrays.asList(\"city=hz\", \"city=sh\", \"city=bj\", \"city=sz\");\n\n  // ===============================================================================================\n  // Tests for columnarSupportMode\n  // ===============================================================================================\n\n  @Test\n  public void testColumnarSupportModeReturnsSupported() {\n    // Table schema uses simple types (INT, STRING) which are batch-read-compatible\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    assertEquals(\n        Scan.ColumnarSupportMode.SUPPORTED,\n        scan.columnarSupportMode(),\n        \"columnarSupportMode should return SUPPORTED for batch-compatible schema\");\n  }\n\n  @Test\n  public void testColumnarSupportModeDoesNotTriggerPlanning() throws Exception {\n    // Calling columnarSupportMode() must NOT trigger file planning (the whole point of the\n    // override is to avoid the early planInputPartitions() call that PARTITION_DEFINED causes).\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    // Call columnarSupportMode before any planning\n    scan.columnarSupportMode();\n\n    // Verify the scan has not been planned yet\n    Field plannedField = SparkScan.class.getDeclaredField(\"planned\");\n    plannedField.setAccessible(true);\n    assertFalse(\n        (boolean) plannedField.get(scan), \"columnarSupportMode() should not trigger file planning\");\n  }\n\n  @Test\n  public void testColumnarSupportModeWithUnsupportedSchema(@TempDir File tempDir) throws Exception {\n    // Create a table with a MAP column and disable nested column vectorized reading\n    // to ensure the schema is not batch-read-compatible\n    String path = tempDir.getAbsolutePath();\n    String mapTableName = \"columnar_map_table\";\n\n    withTable(\n        new String[] {mapTableName},\n        () -> {\n          spark.sql(\n              String.format(\n                  \"CREATE TABLE %s (id INT, tags MAP<STRING, STRING>) USING delta LOCATION '%s'\",\n                  mapTableName, path));\n          spark.sql(String.format(\"INSERT INTO %s VALUES (1, map('k', 'v'))\", mapTableName));\n\n          withSQLConf(\n              \"spark.sql.parquet.enableNestedColumnVectorizedReader\",\n              \"false\",\n              () -> {\n                SparkTable mapTable =\n                    new SparkTable(\n                        Identifier.of(new String[] {\"spark_catalog\", \"default\"}, mapTableName),\n                        path,\n                        options);\n                SparkScanBuilder builder = (SparkScanBuilder) mapTable.newScanBuilder(options);\n                SparkScan scan = (SparkScan) builder.build();\n\n                assertEquals(\n                    Scan.ColumnarSupportMode.UNSUPPORTED,\n                    scan.columnarSupportMode(),\n                    \"columnarSupportMode should return UNSUPPORTED for schema with MAP type\"\n                        + \" when nested column vectorized reader is disabled\");\n              });\n        });\n  }\n\n  @Test\n  public void testColumnarSupportModeWithVectorizedReaderDisabled() throws Exception {\n    // When spark.sql.parquet.enableVectorizedReader is false, columnarSupportMode should\n    // return UNSUPPORTED even for batch-compatible schemas.\n    withSQLConf(\n        \"spark.sql.parquet.enableVectorizedReader\",\n        \"false\",\n        () -> {\n          SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n          SparkScan scan = (SparkScan) builder.build();\n\n          assertEquals(\n              Scan.ColumnarSupportMode.UNSUPPORTED,\n              scan.columnarSupportMode(),\n              \"columnarSupportMode should return UNSUPPORTED when vectorized reader is disabled\");\n        });\n  }\n\n  @Test\n  public void testColumnarSupportModeWithDeletionVectors(@TempDir File tempDir) throws Exception {\n    // For a DV-enabled table with a batch-compatible schema, columnarSupportMode should still\n    // return SUPPORTED because the DV internal column (__delta_internal_is_row_deleted, ByteType)\n    // is also batch-compatible. This verifies consistency with PartitionUtils reader factory.\n    String dvPath = tempDir.getAbsolutePath();\n    String dvTableName = \"columnar_dv_table\";\n\n    withTable(\n        new String[] {dvTableName},\n        () -> {\n          spark.sql(\n              String.format(\n                  \"CREATE TABLE %s (id INT, value STRING) USING delta \"\n                      + \"TBLPROPERTIES ('delta.enableDeletionVectors' = 'true') \"\n                      + \"LOCATION '%s'\",\n                  dvTableName, dvPath));\n          spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a'), (2, 'b')\", dvTableName));\n\n          SparkTable dvTable =\n              new SparkTable(\n                  Identifier.of(new String[] {\"spark_catalog\", \"default\"}, dvTableName),\n                  dvPath,\n                  options);\n          SparkScanBuilder builder = (SparkScanBuilder) dvTable.newScanBuilder(options);\n          SparkScan scan = (SparkScan) builder.build();\n\n          // DV-enabled table with simple types should still return SUPPORTED because the\n          // DV column (ByteType) is batch-compatible\n          assertEquals(\n              Scan.ColumnarSupportMode.SUPPORTED,\n              scan.columnarSupportMode(),\n              \"columnarSupportMode should return SUPPORTED for DV-enabled table with\"\n                  + \" batch-compatible schema\");\n        });\n  }\n\n  // ===============================================================================================\n  // Tests for getDataSchema, getPartitionSchema, getReadDataSchema, getOptions, getConfiguration\n  // ===============================================================================================\n\n  @Test\n  public void testGetDataSchemaPartitionSchemaReadDataSchemaOptionsConfiguration() {\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    // Table schema: (part INT, date STRING, city STRING, name STRING, cnt INT)\n    // Partition columns: (date STRING, city STRING, part INT)\n    // Data columns: (name STRING, cnt INT)\n    StructType dataSchema = scan.getDataSchema();\n    StructType partitionSchema = scan.getPartitionSchema();\n    StructType readDataSchema = scan.getReadDataSchema();\n    CaseInsensitiveStringMap scanOptions = scan.getOptions();\n    Configuration configuration = scan.getConfiguration();\n\n    assertEquals(2, dataSchema.fields().length, \"dataSchema should have 2 fields (name, cnt)\");\n    assertNotNull(dataSchema.fieldNames());\n    assertTrue(\n        Arrays.asList(dataSchema.fieldNames()).containsAll(Arrays.asList(\"name\", \"cnt\")),\n        \"dataSchema should contain name and cnt\");\n\n    assertEquals(\n        3,\n        partitionSchema.fields().length,\n        \"partitionSchema should have 3 fields (date, city, part)\");\n    assertTrue(\n        Arrays.asList(partitionSchema.fieldNames())\n            .containsAll(Arrays.asList(\"date\", \"city\", \"part\")),\n        \"partitionSchema should contain date, city, part\");\n\n    assertEquals(\n        dataSchema,\n        readDataSchema,\n        \"readDataSchema should equal dataSchema without column pruning\");\n\n    assertNotNull(scanOptions, \"options should not be null\");\n    assertEquals(options, scanOptions, \"options should match the scan options\");\n\n    assertNotNull(configuration, \"configuration should not be null\");\n    // Verify configuration matches expected: built from same options via Spark session\n    Configuration expectedConf =\n        spark.sessionState().newHadoopConfWithOptions(ScalaUtils.toScalaMap(options));\n    assertEquals(\n        expectedConf.get(\"fs.defaultFS\"),\n        configuration.get(\"fs.defaultFS\"),\n        \"fs.defaultFS should match expected\");\n    assertEquals(\n        expectedConf.get(\"fs.default.name\"),\n        configuration.get(\"fs.default.name\"),\n        \"fs.default.name should match expected\");\n  }\n\n  @Test\n  public void testGetTablePathReturnsTablePath() {\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    String retrievedPath = scan.getTablePath();\n    assertNotNull(retrievedPath, \"getTablePath should not return null\");\n    // getTablePath returns file URI with trailing slash; tablePath is from tempDir\n    String expectedUri = new File(tablePath).toURI().toString();\n    String expectedPath = expectedUri.endsWith(\"/\") ? expectedUri : expectedUri + \"/\";\n    assertEquals(\n        expectedPath,\n        retrievedPath,\n        \"getTablePath should return path matching table location (with trailing slash)\");\n  }\n\n  @Test\n  public void testGetConfigurationWithHadoopOptions() {\n    // Pass Hadoop options and verify they appear in the returned Configuration\n    Map<String, String> optionsWithHadoop = new HashMap<>();\n    optionsWithHadoop.put(\"fs.file.impl.disable.cache\", \"true\");\n    optionsWithHadoop.put(\"dfs.replication\", \"2\");\n    CaseInsensitiveStringMap optionsWithHadoopMap = new CaseInsensitiveStringMap(optionsWithHadoop);\n\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(optionsWithHadoopMap);\n    SparkScan scan = (SparkScan) builder.build();\n    Configuration configuration = scan.getConfiguration();\n\n    assertEquals(\n        \"true\",\n        configuration.get(\"fs.file.impl.disable.cache\"),\n        \"Hadoop option fs.file.impl.disable.cache should flow through to Configuration\");\n    assertEquals(\n        \"2\",\n        configuration.get(\"dfs.replication\"),\n        \"Hadoop option dfs.replication should flow through to Configuration\");\n  }\n\n  @Test\n  public void testGetReadDataSchemaWithColumnPruning() {\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n\n    StructType prunedSchema =\n        new StructType()\n            .add(\"name\", DataTypes.StringType)\n            .add(\"date\", DataTypes.StringType)\n            .add(\"city\", DataTypes.StringType)\n            .add(\"part\", DataTypes.IntegerType);\n    builder.pruneColumns(prunedSchema);\n\n    SparkScan scan = (SparkScan) builder.build();\n\n    StructType dataSchema = scan.getDataSchema();\n    StructType readDataSchema = scan.getReadDataSchema();\n\n    assertEquals(2, dataSchema.fields().length, \"dataSchema should still have 2 fields\");\n    assertEquals(\n        1,\n        readDataSchema.fields().length,\n        \"readDataSchema should have 1 field (name) after pruning cnt\");\n    assertEquals(\"name\", readDataSchema.fields()[0].name());\n  }\n\n  @Test\n  public void testDPP_singleFilter() throws Exception {\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {cityPredicate}, Arrays.asList(\"city=hz\"));\n\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {datePredicate}, Arrays.asList(\"date=20180520\"));\n\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {partPredicate}, Arrays.asList(\"part=2\"));\n  }\n\n  @Test\n  public void testDPP_multiFilters() throws Exception {\n    checkSupportsRuntimeFilters(\n        table,\n        options,\n        new Predicate[] {cityPredicate, datePredicate},\n        Arrays.asList(\"date=20180520/city=hz\"));\n  }\n\n  @Test\n  public void testDPP_ANDFilters() throws Exception {\n    Predicate andPredicate = new Predicate(\"AND\", new Expression[] {cityPredicate, datePredicate});\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {andPredicate}, Arrays.asList(\"date=20180520/city=hz\"));\n  }\n\n  @Test\n  public void testDPP_ORFilters() throws Exception {\n    Predicate orPredicate = new Predicate(\"OR\", new Expression[] {cityPredicate, datePredicate});\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {orPredicate}, Arrays.asList(\"city=hz\", \"date=20180520\"));\n  }\n\n  @Test\n  public void testDPP_NOTFilter() throws Exception {\n    Predicate notPredicate = new Predicate(\"NOT\", new Expression[] {cityPredicate});\n    checkSupportsRuntimeFilters(\n        table,\n        options,\n        new Predicate[] {notPredicate},\n        Arrays.asList(\"city=sh\", \"city=bj\", \"city=sz\"));\n  }\n\n  @Test\n  public void testDPP_INFilter() throws Exception {\n    Predicate inPredicate =\n        new Predicate(\n            \"IN\",\n            new Expression[] {\n              FieldReference.apply(\"city\"),\n              LiteralValue.apply(\"hz\", DataTypes.StringType),\n              LiteralValue.apply(\"sh\", DataTypes.StringType)\n            });\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {inPredicate}, Arrays.asList(\"city=hz\", \"city=sh\"));\n  }\n\n  @Test\n  public void testDPP_negativeFilter() throws Exception {\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {negativeCityPredicate}, Arrays.asList());\n  }\n\n  @Test\n  public void testDPP_ANDNegativeFilter() throws Exception {\n    Predicate andPredicate =\n        new Predicate(\"AND\", new Expression[] {cityPredicate, negativeCityPredicate});\n    checkSupportsRuntimeFilters(table, options, new Predicate[] {andPredicate}, Arrays.asList());\n  }\n\n  @Test\n  public void testDPP_ORNegativeFilter() throws Exception {\n    Predicate orPredicate =\n        new Predicate(\"OR\", new Expression[] {cityPredicate, negativeCityPredicate});\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {orPredicate}, Arrays.asList(\"city=hz\"));\n  }\n\n  @Test\n  public void testDPP_nonPartitionColumnFilter() throws Exception {\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {cityPredicate, dataPredicate}, Arrays.asList(\"city=hz\"));\n  }\n\n  @Test\n  public void testDPP_nonPartitionColumnFilterOnly() throws Exception {\n    checkSupportsRuntimeFilters(table, options, new Predicate[] {dataPredicate}, allCities);\n  }\n\n  @Test\n  public void testDPP_ANDDataPredicate() throws Exception {\n    Predicate andPredicate = new Predicate(\"AND\", new Expression[] {cityPredicate, dataPredicate});\n    checkSupportsRuntimeFilters(table, options, new Predicate[] {andPredicate}, allCities);\n  }\n\n  @Test\n  public void testDPP_ORDataPredicate() throws Exception {\n    Predicate orPredicate = new Predicate(\"OR\", new Expression[] {cityPredicate, dataPredicate});\n    checkSupportsRuntimeFilters(table, options, new Predicate[] {orPredicate}, allCities);\n  }\n\n  @Test\n  public void testDPP_interColumnFilter() throws Exception {\n    checkSupportsRuntimeFilters(table, options, new Predicate[] {interColPredicate}, allCities);\n  }\n\n  @Test\n  public void testDPP_negativeInterColumnFilter() throws Exception {\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {negativeInterColPredicate}, Arrays.asList());\n  }\n\n  @Test\n  public void testDPP_integerFilter() throws Exception {\n    checkSupportsRuntimeFilters(\n        table, options, new Predicate[] {partPredicate}, Arrays.asList(\"part=2\"));\n  }\n\n  protected static void checkSupportsRuntimeFilters(\n      SparkTable table,\n      CaseInsensitiveStringMap scanOptions,\n      org.apache.spark.sql.connector.expressions.filter.Predicate[] runtimeFilters,\n      List<String> remainingPartitionValueAfterDpp)\n      throws Exception {\n    ScanBuilder newBuilder = table.newScanBuilder(scanOptions);\n    SparkScanBuilder builder = (SparkScanBuilder) newBuilder;\n    Scan scan = builder.build();\n    SparkScan sparkScan = (SparkScan) scan;\n\n    List<PartitionedFile> beforeDppFiles = getPartitionedFiles(sparkScan);\n    // make a copy for comparison after DPP\n    beforeDppFiles = new ArrayList<>(beforeDppFiles);\n    long beforeDppTotalBytes = getTotalBytes(sparkScan);\n    long beforeDppEstimatedSize = getEstimatedSizeInBytes(sparkScan);\n    assert (beforeDppFiles.size() == 5);\n    // Without column pruning, estimatedSizeInBytes should equal totalBytes\n    assertEquals(beforeDppTotalBytes, beforeDppEstimatedSize);\n\n    sparkScan.filter(runtimeFilters);\n    List<PartitionedFile> afterDppFiles = getPartitionedFiles(sparkScan);\n    long afterDppTotalBytes = getTotalBytes(sparkScan);\n    long afterDppEstimatedSize = getEstimatedSizeInBytes(sparkScan);\n    assert (beforeDppFiles.containsAll(afterDppFiles));\n    assert (beforeDppTotalBytes >= afterDppTotalBytes);\n\n    List<PartitionedFile> expectedPartitionFilesAfterDpp = new ArrayList<>();\n    long expectedTotalBytesAfterDpp = 0;\n    for (PartitionedFile pf : beforeDppFiles) {\n      for (String partitionValue : remainingPartitionValueAfterDpp) {\n        if (pf.filePath().toString().contains(partitionValue)) {\n          expectedPartitionFilesAfterDpp.add(pf);\n          expectedTotalBytesAfterDpp += pf.fileSize();\n          break;\n        }\n      }\n    }\n\n    assertEquals(expectedPartitionFilesAfterDpp.size(), afterDppFiles.size());\n    assertEquals(new HashSet<>(expectedPartitionFilesAfterDpp), new HashSet<>(afterDppFiles));\n    assertEquals(expectedTotalBytesAfterDpp, afterDppTotalBytes);\n    // Without column pruning, estimatedSizeInBytes should equal totalBytes after filtering too\n    assertEquals(afterDppTotalBytes, afterDppEstimatedSize);\n  }\n\n  private static List<PartitionedFile> getPartitionedFiles(SparkScan scan) throws Exception {\n    scan.estimateStatistics(); // ensurePlanned\n    Field field = SparkScan.class.getDeclaredField(\"partitionedFiles\");\n    field.setAccessible(true);\n    return (List<PartitionedFile>) field.get(scan);\n  }\n\n  private static long getTotalBytes(SparkScan scan) throws Exception {\n    scan.estimateStatistics(); // ensurePlanned\n    Field field = SparkScan.class.getDeclaredField(\"totalBytes\");\n    field.setAccessible(true);\n    return (long) field.get(scan);\n  }\n\n  private static long getEstimatedSizeInBytes(SparkScan scan) throws Exception {\n    scan.estimateStatistics(); // ensurePlanned\n    Field field = SparkScan.class.getDeclaredField(\"estimatedSizeInBytes\");\n    field.setAccessible(true);\n    return (long) field.get(scan);\n  }\n\n  // ================================================================================================\n  // Tests for streaming options validation\n  // ================================================================================================\n\n  @Test\n  public void testValidateStreamingOptions_SupportedOptions() {\n    // Test with supported options (case insensitive) and custom user options\n    Map<String, String> javaOptions = new HashMap<>();\n    javaOptions.put(\"startingVersion\", \"0\");\n    javaOptions.put(\"MaxFilesPerTrigger\", \"100\");\n    javaOptions.put(\"MAXBYTESPERTRIGGER\", \"1g\");\n    javaOptions.put(\"myCustomOption\", \"value\");\n    scala.collection.immutable.Map<String, String> supportedOptions =\n        ScalaUtils.toScalaMap(javaOptions);\n    DeltaOptions deltaOptions = new DeltaOptions(supportedOptions, spark.sessionState().conf());\n\n    // Verify DeltaOptions can recognize the options (case insensitive)\n    assertEquals(true, deltaOptions.maxFilesPerTrigger().isDefined());\n    assertEquals(100, deltaOptions.maxFilesPerTrigger().get());\n    assertEquals(true, deltaOptions.maxBytesPerTrigger().isDefined());\n\n    // Should not throw - supported and custom options are allowed\n    SparkScan.validateStreamingOptions(deltaOptions);\n  }\n\n  @Test\n  public void testValidateStreamingOptions_UnsupportedOptions() {\n    // Test with blocked DeltaOptions, supported options, and custom user options\n    Map<String, String> javaOptions = new HashMap<>();\n    javaOptions.put(\"startingVersion\", \"0\");\n    javaOptions.put(\"readChangeFeed\", \"true\");\n    javaOptions.put(\"myCustomOption\", \"value\");\n    scala.collection.immutable.Map<String, String> mixedOptions =\n        ScalaUtils.toScalaMap(javaOptions);\n    DeltaOptions deltaOptions = new DeltaOptions(mixedOptions, spark.sessionState().conf());\n\n    UnsupportedOperationException exception =\n        assertThrows(\n            UnsupportedOperationException.class,\n            () -> SparkScan.validateStreamingOptions(deltaOptions));\n\n    // Verify exact error message - only the blocked option should appear\n    // Note: DeltaOptions uses CaseInsensitiveMap which lowercases keys during iteration\n    assertEquals(\n        \"The following streaming options are not supported: [readchangefeed]. \"\n            + \"Supported options are: [startingVersion, startingTimestamp, maxFilesPerTrigger, \"\n            + \"maxBytesPerTrigger, ignoreDeletes, skipChangeCommits, excludeRegex].\",\n        exception.getMessage());\n  }\n\n  // ================================================================================================\n  // Tests for equals and hashCode\n  // ================================================================================================\n\n  @Test\n  public void testEqualsAndHashCode() {\n    // Create two scans from the same table with same options\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    // Same table, same options should be equal\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsWithDifferentOptions() {\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n\n    Map<String, String> differentOptions = new HashMap<>();\n    differentOptions.put(\"customOption\", \"value\");\n    CaseInsensitiveStringMap optionsMap = new CaseInsensitiveStringMap(differentOptions);\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(optionsMap);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    // Different options should not be equal and hashCodes should differ\n    assertNotEquals(scan1, scan2);\n    assertNotEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsWithSameFilters() {\n    // Both scans with equivalent filters created separately (not same instance)\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    builder1.pushFilters(\n        new org.apache.spark.sql.sources.Filter[] {\n          new org.apache.spark.sql.sources.EqualTo(\"city\", \"hz\")\n        });\n    SparkScan scan1 = (SparkScan) builder1.build();\n\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    builder2.pushFilters(\n        new org.apache.spark.sql.sources.Filter[] {\n          new org.apache.spark.sql.sources.EqualTo(\"city\", \"hz\")\n        });\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    // Same options and equivalent filters should be equal\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsWithDifferentFilters() {\n    // Scan without filters\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n\n    // Scan with filters pushed\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    builder2.pushFilters(\n        new org.apache.spark.sql.sources.Filter[] {\n          new org.apache.spark.sql.sources.EqualTo(\"city\", \"hz\")\n        });\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    // Same options but different filters should not be equal and hashCodes should differ\n    assertNotEquals(scan1, scan2);\n    assertNotEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  // ================================================================================================\n  // Tests for estimated size with column projection\n  // ================================================================================================\n\n  @Test\n  public void testEstimatedSizeMatchesStatistics() throws Exception {\n    // Test that estimateStatistics().sizeInBytes() returns the estimatedSizeInBytes field\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    long estimatedSizeFromStats = scan.estimateStatistics().sizeInBytes().getAsLong();\n    long estimatedSizeFromField = getEstimatedSizeInBytes(scan);\n\n    assertEquals(estimatedSizeFromField, estimatedSizeFromStats);\n  }\n\n  @Test\n  public void testEstimatedSizeWithColumnPruning() throws Exception {\n    // Test that with column pruning, estimatedSizeInBytes is computed correctly\n    // Table schema: (part INT, date STRING, city STRING, name STRING, cnt INT)\n    // Partition columns: (date STRING, city STRING, part INT)\n    // Data columns: (name STRING, cnt INT)\n    //\n    // Formula: estimatedBytes = (totalBytes * outputRowSize) / fullSchemaRowSize\n    // Where:\n    //   ROW_OVERHEAD = 8\n    //   dataSchema.defaultSize() = 20 (STRING) + 4 (INT) = 24\n    //   partitionSchema.defaultSize() = 20 + 20 + 4 = 44\n    //   fullSchemaRowSize = 8 + 24 + 44 = 76\n    //\n    // With pruning to only 'name' column:\n    //   readDataSchema.defaultSize() = 20 (STRING only)\n    //   readSchema().defaultSize() = 20 + 44 = 64\n    //   outputRowSize = 8 + 64 = 72\n    //   estimatedBytes = (totalBytes * 72) / 76\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n\n    // Prune columns to only include 'name' (a data column) and partition columns\n    // This simulates: SELECT name, date, city, part FROM table\n    StructType prunedSchema =\n        new StructType()\n            .add(\"name\", DataTypes.StringType) // only one data column\n            .add(\"date\", DataTypes.StringType) // partition columns are always included\n            .add(\"city\", DataTypes.StringType)\n            .add(\"part\", DataTypes.IntegerType);\n    builder.pruneColumns(prunedSchema);\n\n    SparkScan scan = (SparkScan) builder.build();\n\n    long totalBytes = getTotalBytes(scan);\n    long estimatedSize = getEstimatedSizeInBytes(scan);\n\n    // Calculate expected estimated size using the formula\n    // outputRowSize = 8 + 64 = 72, fullSchemaRowSize = 8 + 24 + 44 = 76\n    // Note: We don't use Math.max(1, ...) here because totalBytes is guaranteed to be large enough\n    // (parquet files with actual data) that the division result won't be zero.\n    long expectedEstimatedSize = (totalBytes * 72) / 76;\n\n    assertTrue(totalBytes > 0, \"totalBytes should be positive\");\n    assertEquals(\n        expectedEstimatedSize,\n        estimatedSize,\n        String.format(\n            \"estimatedSize should be (totalBytes * 72) / 76 = (%d * 72) / 76 = %d\",\n            totalBytes, expectedEstimatedSize));\n  }\n\n  @Test\n  public void testEstimatedSizeWithColumnPruningAndFiltering() throws Exception {\n    // Test that column pruning and runtime filtering work together correctly\n    // Using same formula as testEstimatedSizeWithColumnPruning:\n    //   estimatedBytes = (totalBytes * 72) / 76\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n\n    // Prune columns to only include 'name' column\n    StructType prunedSchema =\n        new StructType()\n            .add(\"name\", DataTypes.StringType)\n            .add(\"date\", DataTypes.StringType)\n            .add(\"city\", DataTypes.StringType)\n            .add(\"part\", DataTypes.IntegerType);\n    builder.pruneColumns(prunedSchema);\n\n    SparkScan scan = (SparkScan) builder.build();\n\n    // Get initial stats with column pruning\n    long initialTotalBytes = getTotalBytes(scan);\n    long initialEstimatedSize = getEstimatedSizeInBytes(scan);\n\n    // Verify initial estimated size matches formula\n    // Note: No Math.max(1, ...) needed - totalBytes from parquet files is large enough\n    long expectedInitialEstimated = (initialTotalBytes * 72) / 76;\n    assertEquals(\n        expectedInitialEstimated,\n        initialEstimatedSize,\n        \"Initial estimatedSize should match formula\");\n\n    // Apply a runtime filter\n    scan.filter(new Predicate[] {cityPredicate}); // city=hz\n\n    // After filtering, verify both values are updated correctly\n    long afterFilterTotalBytes = getTotalBytes(scan);\n    long afterFilterEstimatedSize = getEstimatedSizeInBytes(scan);\n\n    // Verify estimated size matches formula with new totalBytes\n    long expectedAfterFilterEstimated = (afterFilterTotalBytes * 72) / 76;\n    assertEquals(\n        expectedAfterFilterEstimated,\n        afterFilterEstimatedSize,\n        \"After filter, estimatedSize should match formula with new totalBytes\");\n\n    // Verify both values were reduced\n    assertTrue(afterFilterTotalBytes < initialTotalBytes, \"totalBytes should be reduced\");\n    assertTrue(afterFilterEstimatedSize < initialEstimatedSize, \"estimatedSize should be reduced\");\n  }\n\n  @Test\n  public void testEstimatedSizeZeroAfterFilteringOutAllFiles() throws Exception {\n    // Test that filtering out all files results in zero for both sizes\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    // Apply filter that matches nothing\n    scan.filter(new Predicate[] {negativeCityPredicate}); // city=zz doesn't exist\n\n    long afterFilterTotalBytes = getTotalBytes(scan);\n    long afterFilterEstimatedSize = getEstimatedSizeInBytes(scan);\n\n    assertEquals(0, afterFilterTotalBytes, \"totalBytes should be 0 after filtering out all files\");\n    assertEquals(\n        0, afterFilterEstimatedSize, \"estimatedSize should be 0 after filtering out all files\");\n    assertEquals(\n        0,\n        scan.estimateStatistics().sizeInBytes().getAsLong(),\n        \"Statistics sizeInBytes should be 0 after filtering out all files\");\n  }\n\n  // ================================================================================================\n  // Tests for equals and hashCode with runtime filters\n  // ================================================================================================\n\n  @Test\n  public void testEqualsAndHashCodeWithSameRuntimeFilter() {\n    // Same filter applied to both scans (same instance)\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate});\n    scan2.filter(new Predicate[] {cityPredicate});\n\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithEquivalentRuntimeFilters() {\n    // Equivalent filters (different instances)\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate});\n\n    Predicate cityPredicateCopy =\n        new Predicate(\n            \"=\",\n            new Expression[] {\n              FieldReference.apply(\"city\"), LiteralValue.apply(\"hz\", DataTypes.StringType)\n            });\n    scan2.filter(new Predicate[] {cityPredicateCopy});\n\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithMultipleRuntimeFiltersInSameOrder() {\n    // Multiple filters in same order\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate, datePredicate});\n    scan2.filter(new Predicate[] {cityPredicate, datePredicate});\n\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithIdempotentRuntimeFilters() {\n    // Filter idempotency - applying same filter once vs twice\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate});\n    scan2.filter(new Predicate[] {cityPredicate});\n    scan2.filter(new Predicate[] {cityPredicate}); // Apply same filter twice\n\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithSeparateRuntimeFilterCalls() {\n    // Multiple separate filter() calls vs single call with multiple filters\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate});\n    scan1.filter(new Predicate[] {datePredicate});\n    scan2.filter(new Predicate[] {cityPredicate, datePredicate});\n\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithRuntimeFiltersInDifferentOrder() {\n    // Same filters in different order (order-independent)\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate, datePredicate});\n    scan2.filter(new Predicate[] {datePredicate, cityPredicate});\n\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testEqualsAndHashCodeWithNonPartitionColumnRuntimeFilters() {\n    // Non-partition column predicates should not affect equality\n    // Only partition column predicates should be tracked\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    // cityPredicate is on partition column, dataPredicate is on non-partition column (cnt)\n    scan1.filter(new Predicate[] {cityPredicate});\n    scan2.filter(new Predicate[] {cityPredicate, dataPredicate});\n\n    // They should be equal because dataPredicate doesn't produce an evaluator\n    assertEquals(scan1, scan2);\n    assertEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testNotEqualsWithDifferentRuntimeFilters() {\n    // Different filters\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan1.filter(new Predicate[] {cityPredicate});\n    scan2.filter(new Predicate[] {datePredicate});\n\n    assertNotEquals(scan1, scan2);\n    assertNotEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  @Test\n  public void testNotEqualsWithAndWithoutRuntimeFilter() {\n    // One with filter, one without\n    SparkScanBuilder builder1 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan1 = (SparkScan) builder1.build();\n    SparkScanBuilder builder2 = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan2 = (SparkScan) builder2.build();\n\n    scan2.filter(new Predicate[] {cityPredicate});\n\n    assertNotEquals(scan1, scan2);\n    assertNotEquals(scan1.hashCode(), scan2.hashCode());\n  }\n\n  // ================================================================================================\n  // Tests for output partitioning (SupportsReportPartitioning)\n  // ================================================================================================\n\n  @Test\n  public void testOutputPartitioningForPartitionedTable() {\n    // Partitioned table should return KeyGroupedPartitioning\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    Partitioning partitioning = scan.outputPartitioning();\n\n    assertTrue(\n        partitioning instanceof KeyGroupedPartitioning,\n        \"Partitioned table should return KeyGroupedPartitioning\");\n\n    KeyGroupedPartitioning kgp = (KeyGroupedPartitioning) partitioning;\n\n    // The partitioned table has 3 partition columns: date, city, part\n    Expression[] keys = kgp.keys();\n    assertEquals(3, keys.length, \"Should have 3 partition key expressions\");\n\n    // Verify partition key names match partition schema\n    Set<String> keyNames = new HashSet<>();\n    for (Expression key : keys) {\n      assertTrue(key instanceof FieldReference, \"Key should be a FieldReference\");\n      keyNames.add(((FieldReference) key).fieldNames()[0]);\n    }\n    assertTrue(keyNames.containsAll(Arrays.asList(\"date\", \"city\", \"part\")));\n\n    // numPartitions returns partitionedFiles.size() (file count, not unique partition count).\n    // In this test data, each partition has one file, so file count equals partition count.\n    assertEquals(5, kgp.numPartitions(), \"Should have 5 files (one per partition)\");\n  }\n\n  /** Creates a non-partitioned table with sample data and returns a SparkScan for it. */\n  private SparkScan createNonPartitionedScan(File tempDir, String tableName) {\n    spark.sql(\n        String.format(\n            \"CREATE TABLE `%s` (id INT, name STRING) USING delta LOCATION '%s'\",\n            tableName, tempDir.getAbsolutePath()));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'Alice'), (2, 'Bob')\", tableName));\n\n    SparkTable nonPartTable =\n        new SparkTable(\n            Identifier.of(new String[] {\"spark_catalog\", \"default\"}, tableName),\n            tempDir.getAbsolutePath(),\n            options);\n\n    SparkScanBuilder builder = (SparkScanBuilder) nonPartTable.newScanBuilder(options);\n    return (SparkScan) builder.build();\n  }\n\n  @Test\n  public void testOutputPartitioningForNonPartitionedTable(@TempDir File tempDir) {\n    // Non-partitioned table should return UnknownPartitioning\n    SparkScan scan = createNonPartitionedScan(tempDir, \"deltatbl_nonpartitioned\");\n\n    Partitioning partitioning = scan.outputPartitioning();\n\n    assertTrue(\n        partitioning instanceof UnknownPartitioning,\n        \"Non-partitioned table should return UnknownPartitioning\");\n  }\n\n  @Test\n  public void testOutputPartitioningAfterRuntimeFilter() {\n    // Output partitioning should reflect filtered partition count\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    // Apply filter to only keep city=hz (2 rows: part=1/date=20180520 and part=1/date=20180718)\n    scan.filter(new Predicate[] {cityPredicate});\n\n    Partitioning partitioning = scan.outputPartitioning();\n    assertTrue(partitioning instanceof KeyGroupedPartitioning);\n\n    KeyGroupedPartitioning kgp = (KeyGroupedPartitioning) partitioning;\n    // numPartitions returns partitionedFiles.size() (file count); here each partition has one file.\n    assertEquals(\n        2,\n        kgp.numPartitions(),\n        \"After filtering to city=hz, should have 2 files (one per partition)\");\n  }\n\n  // ================================================================================================\n  // Tests for DeltaInputPartition in planInputPartitions\n  // ================================================================================================\n\n  @Test\n  public void testPlanInputPartitionsReturnsHasPartitionKeyForPartitionedTable() {\n    SparkScanBuilder builder = (SparkScanBuilder) table.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n    Batch batch = scan.toBatch();\n\n    InputPartition[] partitions = batch.planInputPartitions();\n\n    assertTrue(partitions.length > 0, \"Should have at least one partition\");\n    for (InputPartition partition : partitions) {\n      assertTrue(\n          partition instanceof DeltaInputPartition,\n          \"Partitioned table should return DeltaInputPartition instances\");\n      assertTrue(\n          partition instanceof HasPartitionKey,\n          \"DeltaInputPartition should implement HasPartitionKey\");\n\n      DeltaInputPartition deltaPartition = (DeltaInputPartition) partition;\n      assertNotNull(deltaPartition.partitionKey(), \"Partition key should not be null\");\n      assertNotNull(deltaPartition.getFilePartition(), \"FilePartition should not be null\");\n    }\n  }\n\n  @Test\n  public void testPlanInputPartitionsGroupsFilesByPartition(@TempDir File tempDir)\n      throws Exception {\n    // Create a table with multiple files per partition to actually exercise the grouping logic\n    // in planPartitionedInputPartitions (the default test table has 1 file per partition,\n    // which would pass even without grouping).\n    String multiFileTableName = \"deltatbl_multifile_partitioned\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE `%s` (id INT, data STRING, part INT) USING delta \"\n                + \"LOCATION '%s' PARTITIONED BY (part)\",\n            multiFileTableName, tempDir.getAbsolutePath()));\n    // Insert in separate statements to create multiple files per partition\n    spark.sql(\n        String.format(\"INSERT INTO `%s` VALUES (1, 'a', 1), (2, 'b', 2)\", multiFileTableName));\n    spark.sql(\n        String.format(\"INSERT INTO `%s` VALUES (3, 'c', 1), (4, 'd', 2)\", multiFileTableName));\n    spark.sql(String.format(\"INSERT INTO `%s` VALUES (5, 'e', 1)\", multiFileTableName));\n    // Now part=1 has 3 files, part=2 has 2 files\n\n    SparkTable multiFileTable =\n        new SparkTable(\n            Identifier.of(new String[] {\"spark_catalog\", \"default\"}, multiFileTableName),\n            tempDir.getAbsolutePath(),\n            options);\n\n    // Force maxPartitionBytes=1 so each file gets its own FilePartition, making the\n    // totalPartitions > 2 assertion deterministic regardless of default parallelism.\n    withSQLConf(\n        \"spark.sql.files.maxPartitionBytes\",\n        \"1\",\n        () -> {\n          SparkScanBuilder builder = (SparkScanBuilder) multiFileTable.newScanBuilder(options);\n          SparkScan scan = (SparkScan) builder.build();\n          Batch batch = scan.toBatch();\n\n          InputPartition[] partitions = batch.planInputPartitions();\n\n          // Verify all partitions are DeltaInputPartition with partition keys\n          Map<InternalRow, List<DeltaInputPartition>> partitionsByKey = new HashMap<>();\n          for (InputPartition p : partitions) {\n            assertTrue(p instanceof DeltaInputPartition);\n            DeltaInputPartition dp = (DeltaInputPartition) p;\n            partitionsByKey.computeIfAbsent(dp.partitionKey(), k -> new ArrayList<>()).add(dp);\n          }\n\n          // Should have exactly 2 unique partition keys (part=1 and part=2)\n          assertEquals(2, partitionsByKey.size(), \"Should have 2 unique partition keys\");\n\n          // Verify that the grouping actually produced multiple DeltaInputPartitions for a\n          // single partition key (since multiple files exist per partition and each gets its\n          // own FilePartition when maxPartitionBytes=1)\n          int totalPartitions = partitions.length;\n          assertTrue(\n              totalPartitions > 2,\n              \"With 5 files across 2 partitions, should have more than 2 input partitions, \"\n                  + \"got \"\n                  + totalPartitions);\n\n          // Verify all DeltaInputPartitions with the same key share the same partition key\n          for (Map.Entry<InternalRow, List<DeltaInputPartition>> entry :\n              partitionsByKey.entrySet()) {\n            List<DeltaInputPartition> group = entry.getValue();\n            InternalRow expectedKey = group.get(0).partitionKey();\n            for (DeltaInputPartition dp : group) {\n              assertEquals(\n                  expectedKey,\n                  dp.partitionKey(),\n                  \"All partitions in the same group should have equal partition keys\");\n            }\n          }\n        });\n  }\n\n  @Test\n  public void testPlanInputPartitionsReturnsFilePartitionForNonPartitionedTable(\n      @TempDir File tempDir) {\n    SparkScan scan = createNonPartitionedScan(tempDir, \"deltatbl_nonpartitioned_batch\");\n    Batch batch = scan.toBatch();\n\n    InputPartition[] partitions = batch.planInputPartitions();\n\n    assertTrue(partitions.length > 0, \"Should have at least one partition\");\n    for (InputPartition partition : partitions) {\n      assertTrue(\n          partition instanceof FilePartition,\n          \"Non-partitioned table should return FilePartition instances, not DeltaInputPartition\");\n      assertFalse(\n          partition instanceof DeltaInputPartition,\n          \"Non-partitioned table should NOT return DeltaInputPartition\");\n    }\n  }\n\n  @Test\n  public void testOutputPartitioningForEmptyPartitionedTable(@TempDir File tempDir) {\n    // Empty partitioned table should return KeyGroupedPartitioning with 0 partitions\n    String emptyTableName = \"deltatbl_empty_partitioned\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE `%s` (id INT, name STRING, part INT) USING delta \"\n                + \"LOCATION '%s' PARTITIONED BY (part)\",\n            emptyTableName, tempDir.getAbsolutePath()));\n\n    SparkTable emptyTable =\n        new SparkTable(\n            Identifier.of(new String[] {\"spark_catalog\", \"default\"}, emptyTableName),\n            tempDir.getAbsolutePath(),\n            options);\n\n    SparkScanBuilder builder = (SparkScanBuilder) emptyTable.newScanBuilder(options);\n    SparkScan scan = (SparkScan) builder.build();\n\n    Partitioning partitioning = scan.outputPartitioning();\n    assertTrue(\n        partitioning instanceof KeyGroupedPartitioning,\n        \"Empty partitioned table should still return KeyGroupedPartitioning\");\n\n    KeyGroupedPartitioning kgp = (KeyGroupedPartitioning) partitioning;\n    assertEquals(0, kgp.numPartitions(), \"Empty table should have 0 partitions\");\n\n    Batch batch = scan.toBatch();\n    InputPartition[] partitions = batch.planInputPartitions();\n    assertEquals(0, partitions.length, \"Empty table should return 0 input partitions\");\n  }\n\n  // ================================================================================================\n  // Tests for catalog statistics propagation\n  // ================================================================================================\n\n  /**\n   * Helper to inject CatalogStatistics into a catalog table via alterTableStats. This is needed\n   * because ANALYZE TABLE is not supported for V2 tables in the test environment.\n   */\n  private CatalogTable injectCatalogStats(String tblName, CatalogStatistics stats)\n      throws Exception {\n    TableIdentifier tableId = new TableIdentifier(tblName);\n    spark.sessionState().catalog().alterTableStats(tableId, scala.Option.apply(stats));\n    return spark.sessionState().catalog().getTableMetadata(tableId);\n  }\n\n  @Test\n  public void testEstimateStatisticsWithCatalogStats_cboEnabled(@TempDir File tempDir)\n      throws Exception {\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_cbo_enabled\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING, value DOUBLE) USING delta LOCATION '%s'\",\n            tblName, path));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a', 1.0), (2, 'b', 2.0)\", tblName));\n\n    // Inject catalog stats with column stats for \"id\"\n    CatalogColumnStat idColStat =\n        new CatalogColumnStat(\n            scala.Option.apply(scala.math.BigInt.apply(2L)), // distinctCount\n            scala.Option.apply(\"1\"), // min\n            scala.Option.apply(\"2\"), // max\n            scala.Option.apply(scala.math.BigInt.apply(0L)), // nullCount\n            scala.Option.apply((Object) 4L), // avgLen\n            scala.Option.apply((Object) 4L), // maxLen\n            scala.Option.empty(), // histogram\n            CatalogColumnStat.VERSION());\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            scala.math.BigInt.apply(1024L),\n            scala.Option.apply(scala.math.BigInt.apply(2L)),\n            buildColStatsMap(new String[] {\"id\"}, new CatalogColumnStat[] {idColStat}));\n\n    CatalogTable catalogTable = injectCatalogStats(tblName, catalogStats);\n\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"true\",\n        () -> {\n          Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n          SparkTable sparkTable = new SparkTable(id, catalogTable, Collections.emptyMap());\n\n          SparkScanBuilder builder =\n              (SparkScanBuilder)\n                  sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n          SparkScan scan = (SparkScan) builder.build();\n          Statistics stats = scan.estimateStatistics();\n\n          // Should have numRows from catalog stats\n          assertTrue(stats.numRows().isPresent(), \"numRows should be present with CBO enabled\");\n          assertEquals(2L, stats.numRows().getAsLong(), \"numRows should be 2\");\n\n          // sizeInBytes should still come from planned files (more accurate)\n          assertTrue(stats.sizeInBytes().isPresent(), \"sizeInBytes should be present\");\n          assertTrue(stats.sizeInBytes().getAsLong() > 0, \"sizeInBytes should be positive\");\n\n          // Should have column stats\n          Map<NamedReference, ColumnStatistics> colStats = stats.columnStats();\n          assertNotNull(colStats, \"columnStats should not be null\");\n          assertFalse(colStats.isEmpty(), \"columnStats should not be empty\");\n\n          // Check that column stats contain expected columns\n          ColumnStatistics idStats = colStats.get(FieldReference.apply(\"id\"));\n          assertNotNull(idStats, \"id column stats should be present\");\n          assertTrue(idStats.nullCount().isPresent(), \"id nullCount should be present\");\n          assertTrue(idStats.distinctCount().isPresent(), \"id distinctCount should be present\");\n          assertTrue(idStats.min().isPresent(), \"id min should be present\");\n          assertTrue(idStats.max().isPresent(), \"id max should be present\");\n          assertEquals(1, idStats.min().get(), \"id min should be 1\");\n          assertEquals(2, idStats.max().get(), \"id max should be 2\");\n        });\n  }\n\n  @Test\n  public void testEstimateStatisticsWithCatalogStats_cboDisabled(@TempDir File tempDir)\n      throws Exception {\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_cbo_disabled\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\", tblName, path));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a'), (2, 'b')\", tblName));\n\n    // Inject catalog stats\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            scala.math.BigInt.apply(512L),\n            scala.Option.apply(scala.math.BigInt.apply(2L)),\n            scala.collection.immutable.Map$.MODULE$.empty());\n\n    CatalogTable catalogTable = injectCatalogStats(tblName, catalogStats);\n\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"false\",\n        () -> {\n          Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n          SparkTable sparkTable = new SparkTable(id, catalogTable, Collections.emptyMap());\n\n          SparkScanBuilder builder =\n              (SparkScanBuilder)\n                  sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n          SparkScan scan = (SparkScan) builder.build();\n          Statistics stats = scan.estimateStatistics();\n\n          // With CBO disabled, numRows should be empty (matching V1 behavior)\n          assertFalse(stats.numRows().isPresent(), \"numRows should be empty with CBO disabled\");\n\n          // sizeInBytes should still come from planned files\n          assertTrue(stats.sizeInBytes().isPresent(), \"sizeInBytes should be present\");\n          assertTrue(stats.sizeInBytes().getAsLong() > 0, \"sizeInBytes should be positive\");\n        });\n  }\n\n  @Test\n  public void testEstimateStatisticsWithCatalogStats_planStatsEnabled(@TempDir File tempDir)\n      throws Exception {\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_plan_stats_enabled\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\", tblName, path));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a'), (2, 'b')\", tblName));\n\n    // Inject catalog stats with numRows\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            scala.math.BigInt.apply(512L),\n            scala.Option.apply(scala.math.BigInt.apply(2L)),\n            scala.collection.immutable.Map$.MODULE$.empty());\n\n    CatalogTable catalogTable = injectCatalogStats(tblName, catalogStats);\n\n    // CBO disabled but planStatsEnabled=true should still surface catalog stats\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"false\",\n        () -> {\n          withSQLConf(\n              \"spark.sql.cbo.planStats.enabled\",\n              \"true\",\n              () -> {\n                Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n                SparkTable sparkTable = new SparkTable(id, catalogTable, Collections.emptyMap());\n\n                SparkScanBuilder builder =\n                    (SparkScanBuilder)\n                        sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n                SparkScan scan = (SparkScan) builder.build();\n                Statistics stats = scan.estimateStatistics();\n\n                // With planStatsEnabled, numRows should come from catalog stats\n                assertTrue(\n                    stats.numRows().isPresent(), \"numRows should be present with planStatsEnabled\");\n                assertEquals(2L, stats.numRows().getAsLong(), \"numRows should be 2\");\n\n                // sizeInBytes should still come from planned files\n                assertTrue(stats.sizeInBytes().isPresent(), \"sizeInBytes should be present\");\n                assertTrue(stats.sizeInBytes().getAsLong() > 0, \"sizeInBytes should be positive\");\n              });\n        });\n  }\n\n  @Test\n  public void testEstimateStatisticsWithoutCatalogStats(@TempDir File tempDir) throws Exception {\n    // Path-based table has no catalog stats\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_no_catalog\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\", tblName, path));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a')\", tblName));\n\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"true\",\n        () -> {\n          // Path-based table — no catalog table, no stats\n          Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n          SparkTable sparkTable = new SparkTable(id, path);\n\n          SparkScanBuilder builder =\n              (SparkScanBuilder)\n                  sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n          SparkScan scan = (SparkScan) builder.build();\n          Statistics stats = scan.estimateStatistics();\n\n          // Without catalog stats, numRows should be empty\n          assertFalse(stats.numRows().isPresent(), \"numRows should be empty for path-based table\");\n          assertTrue(stats.sizeInBytes().isPresent(), \"sizeInBytes should be present\");\n          assertTrue(stats.sizeInBytes().getAsLong() > 0, \"sizeInBytes should be positive\");\n        });\n  }\n\n  @Test\n  public void testEstimateStatisticsWithPartitionedTableAndCatalogStats(@TempDir File tempDir)\n      throws Exception {\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_partitioned\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING, part INT) USING delta \"\n                + \"PARTITIONED BY (part) LOCATION '%s'\",\n            tblName, path));\n    spark.sql(\n        String.format(\"INSERT INTO %s VALUES (1, 'a', 1), (2, 'b', 1), (3, 'c', 2)\", tblName));\n\n    // Inject catalog stats with column stats for both data and partition columns\n    CatalogColumnStat idColStat =\n        new CatalogColumnStat(\n            scala.Option.apply(scala.math.BigInt.apply(3L)),\n            scala.Option.apply(\"1\"),\n            scala.Option.apply(\"3\"),\n            scala.Option.apply(scala.math.BigInt.apply(0L)),\n            scala.Option.empty(),\n            scala.Option.empty(),\n            scala.Option.empty(),\n            CatalogColumnStat.VERSION());\n\n    CatalogColumnStat partColStat =\n        new CatalogColumnStat(\n            scala.Option.apply(scala.math.BigInt.apply(2L)),\n            scala.Option.apply(\"1\"),\n            scala.Option.apply(\"2\"),\n            scala.Option.apply(scala.math.BigInt.apply(0L)),\n            scala.Option.empty(),\n            scala.Option.empty(),\n            scala.Option.empty(),\n            CatalogColumnStat.VERSION());\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            scala.math.BigInt.apply(2048L),\n            scala.Option.apply(scala.math.BigInt.apply(3L)),\n            buildColStatsMap(\n                new String[] {\"id\", \"part\"}, new CatalogColumnStat[] {idColStat, partColStat}));\n\n    CatalogTable catalogTable = injectCatalogStats(tblName, catalogStats);\n\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"true\",\n        () -> {\n          Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n          SparkTable sparkTable = new SparkTable(id, catalogTable, Collections.emptyMap());\n\n          SparkScanBuilder builder =\n              (SparkScanBuilder)\n                  sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n          SparkScan scan = (SparkScan) builder.build();\n          Statistics stats = scan.estimateStatistics();\n\n          assertTrue(stats.numRows().isPresent(), \"numRows should be present\");\n          assertEquals(3L, stats.numRows().getAsLong(), \"numRows should be 3\");\n\n          // Verify column stats include both data and partition columns\n          Map<NamedReference, ColumnStatistics> colStats = stats.columnStats();\n          assertNotNull(colStats.get(FieldReference.apply(\"id\")), \"id stats should be present\");\n          assertNotNull(colStats.get(FieldReference.apply(\"part\")), \"part stats should be present\");\n\n          // Check partition column stats\n          ColumnStatistics partStats = colStats.get(FieldReference.apply(\"part\"));\n          assertTrue(partStats.min().isPresent(), \"part min should be present\");\n          assertTrue(partStats.max().isPresent(), \"part max should be present\");\n          assertEquals(1, partStats.min().get(), \"part min should be 1\");\n          assertEquals(2, partStats.max().get(), \"part max should be 2\");\n        });\n  }\n\n  @Test\n  public void testEstimatedSizeUsesAvgLenFromCatalogStats(@TempDir File tempDir) throws Exception {\n    // Verify that computeEstimatedSizeWithColumnProjection uses avgLen from catalog stats\n    // instead of defaultSize(), mirroring EstimationUtils.getSizePerRow() (#5952).\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_avglen\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\", tblName, path));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a'), (2, 'bb'), (3, 'ccc')\", tblName));\n\n    // Create column stats with avgLen=5 for 'name' (STRING defaultSize is 20)\n    CatalogColumnStat nameColStat =\n        new CatalogColumnStat(\n            scala.Option.empty(),\n            scala.Option.empty(),\n            scala.Option.empty(),\n            scala.Option.empty(),\n            scala.Option.apply((Object) 5L), // avgLen = 5 (vs STRING defaultSize 20)\n            scala.Option.empty(),\n            scala.Option.empty(),\n            CatalogColumnStat.VERSION());\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            scala.math.BigInt.apply(1024L),\n            scala.Option.apply(scala.math.BigInt.apply(3L)),\n            buildColStatsMap(new String[] {\"name\"}, new CatalogColumnStat[] {nameColStat}));\n\n    CatalogTable catalogTable = injectCatalogStats(tblName, catalogStats);\n\n    // avgLen is used for sizeInBytes estimation regardless of CBO/planStats settings\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"false\",\n        () -> {\n          withSQLConf(\n              \"spark.sql.cbo.planStats.enabled\",\n              \"false\",\n              () -> {\n                Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n                SparkTable sparkTable = new SparkTable(id, catalogTable, Collections.emptyMap());\n\n                SparkScanBuilder builder =\n                    (SparkScanBuilder)\n                        sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n\n                // Prune to only 'name' column to trigger column projection estimation\n                StructType prunedSchema = new StructType().add(\"name\", DataTypes.StringType);\n                builder.pruneColumns(prunedSchema);\n\n                SparkScan scan = (SparkScan) builder.build();\n\n                long totalBytes = getTotalBytes(scan);\n                long estimatedSize = getEstimatedSizeInBytes(scan);\n                assertTrue(totalBytes > 0, \"totalBytes should be positive\");\n\n                // Schema: dataSchema = (id INT, name STRING), partitionSchema = empty\n                // readSchema = (name STRING) after pruning\n                //\n                // With avgLen=5 for name (STRING: avgLen + 12 = 17, vs defaultSize 20):\n                //   fullSchemaRowSize = 8 + (4 [INT default] + 17 [STRING avgLen]) = 29\n                //   outputRowSize     = 8 + 17 = 25\n                //   estimated = (totalBytes * 25) / 29\n                //\n                // Without avgLen (defaultSize only):\n                //   fullSchemaRowSize = 8 + (4 + 20) = 32\n                //   outputRowSize     = 8 + 20 = 28\n                //   estimated = (totalBytes * 28) / 32\n                long expectedWithAvgLen = (totalBytes * 25) / 29;\n                long expectedWithoutAvgLen = (totalBytes * 28) / 32;\n\n                assertEquals(\n                    expectedWithAvgLen,\n                    estimatedSize,\n                    \"estimatedSize should use avgLen from catalog stats\");\n                assertNotEquals(\n                    expectedWithoutAvgLen,\n                    estimatedSize,\n                    \"estimatedSize should differ from defaultSize-only calculation\");\n              });\n        });\n  }\n\n  @Test\n  public void testEstimateStatisticsWithoutAnalyze(@TempDir File tempDir) throws Exception {\n    // Table exists in catalog but no stats were injected\n    String path = tempDir.getAbsolutePath();\n    String tblName = \"stats_no_analyze\";\n    spark.sql(\n        String.format(\n            \"CREATE TABLE %s (id INT, name STRING) USING delta LOCATION '%s'\", tblName, path));\n    spark.sql(String.format(\"INSERT INTO %s VALUES (1, 'a')\", tblName));\n\n    withSQLConf(\n        \"spark.sql.cbo.enabled\",\n        \"true\",\n        () -> {\n          CatalogTable catalogTable =\n              spark.sessionState().catalog().getTableMetadata(new TableIdentifier(tblName));\n          Identifier id = Identifier.of(new String[] {\"default\"}, tblName);\n          SparkTable sparkTable = new SparkTable(id, catalogTable, Collections.emptyMap());\n\n          SparkScanBuilder builder =\n              (SparkScanBuilder)\n                  sparkTable.newScanBuilder(new CaseInsensitiveStringMap(new HashMap<>()));\n          SparkScan scan = (SparkScan) builder.build();\n          Statistics stats = scan.estimateStatistics();\n\n          // Without catalog stats, we fall back to file-only stats\n          assertFalse(stats.numRows().isPresent(), \"numRows should be empty without catalog stats\");\n          assertTrue(stats.sizeInBytes().isPresent(), \"sizeInBytes should be present\");\n          assertTrue(stats.sizeInBytes().getAsLong() > 0, \"sizeInBytes should be positive\");\n        });\n  }\n\n  @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n  private static scala.collection.immutable.Map<String, CatalogColumnStat> buildColStatsMap(\n      String[] keys, CatalogColumnStat[] values) {\n    scala.collection.mutable.Builder b = scala.collection.immutable.Map$.MODULE$.newBuilder();\n    for (int i = 0; i < keys.length; i++) {\n      b.$plus$eq(new scala.Tuple2<>(keys[i], values[i]));\n    }\n    return (scala.collection.immutable.Map<String, CatalogColumnStat>) b.result();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/deletionvector/ColumnVectorWithFilterTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read.deletionvector;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.function.IntFunction;\nimport org.apache.spark.sql.execution.vectorized.OnHeapColumnVector;\nimport org.apache.spark.sql.execution.vectorized.WritableColumnVector;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.vectorized.ColumnVector;\nimport org.junit.jupiter.api.Test;\n\npublic class ColumnVectorWithFilterTest {\n\n  @Test\n  void testIntegerColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(5, DataTypes.IntegerType)) {\n      for (int i = 0; i < 5; i++) {\n        delegate.putInt(i, (i + 1) * 10); // [10, 20, 30, 40, 50]\n      }\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {1, 3});\n\n      assertMappedValues(mappedVector, mappedVector::getInt, new Integer[] {20, 40});\n    }\n  }\n\n  @Test\n  void testLongColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(4, DataTypes.LongType)) {\n      for (int i = 0; i < 4; i++) {\n        delegate.putLong(i, (i + 1) * 100L);\n      }\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {0, 2});\n\n      assertMappedValues(mappedVector, mappedVector::getLong, new Long[] {100L, 300L});\n    }\n  }\n\n  @Test\n  void testDoubleColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(3, DataTypes.DoubleType)) {\n      delegate.putDouble(0, 1.1);\n      delegate.putDouble(1, 2.2);\n      delegate.putDouble(2, 3.3);\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {0, 2});\n\n      assertMappedValues(mappedVector, mappedVector::getDouble, new Double[] {1.1, 3.3});\n    }\n  }\n\n  @Test\n  void testBooleanColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(4, DataTypes.BooleanType)) {\n      delegate.putBoolean(0, true);\n      delegate.putBoolean(1, false);\n      delegate.putBoolean(2, true);\n      delegate.putBoolean(3, false);\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {1, 2});\n\n      assertMappedValues(mappedVector, mappedVector::getBoolean, new Boolean[] {false, true});\n    }\n  }\n\n  @Test\n  void testStringColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(3, DataTypes.StringType)) {\n      delegate.putByteArray(0, \"alice\".getBytes());\n      delegate.putByteArray(1, \"bob\".getBytes());\n      delegate.putByteArray(2, \"charlie\".getBytes());\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {2});\n\n      assertMappedValues(\n          mappedVector, i -> mappedVector.getUTF8String(i).toString(), new String[] {\"charlie\"});\n    }\n  }\n\n  @Test\n  void testStructColumnVector() {\n    StructType structType =\n        new StructType().add(\"id\", DataTypes.IntegerType).add(\"name\", DataTypes.StringType);\n    try (WritableColumnVector delegate = new OnHeapColumnVector(3, structType)) {\n      WritableColumnVector idChild = (WritableColumnVector) delegate.getChild(0);\n      for (int i = 0; i < 3; i++) {\n        idChild.putInt(i, i + 1); // [1, 2, 3]\n      }\n      WritableColumnVector nameChild = (WritableColumnVector) delegate.getChild(1);\n      nameChild.putByteArray(0, \"a\".getBytes());\n      nameChild.putByteArray(1, \"b\".getBytes());\n      nameChild.putByteArray(2, \"c\".getBytes());\n\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {0, 2});\n\n      ColumnVector mappedIdColumn = mappedVector.getChild(0);\n      ColumnVector mappedNameColumn = mappedVector.getChild(1);\n\n      assertMappedValues(mappedIdColumn, mappedIdColumn::getInt, new Integer[] {1, 3});\n      assertMappedValues(\n          mappedNameColumn,\n          i -> mappedNameColumn.getUTF8String(i).toString(),\n          new String[] {\"a\", \"c\"});\n    }\n  }\n\n  @Test\n  void testNullColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(4, DataTypes.IntegerType)) {\n      // original rows: [10, null, 30, null]\n      delegate.putInt(0, 10);\n      delegate.putNull(1);\n      delegate.putInt(2, 30);\n      delegate.putNull(3);\n      // selected rows (in order): [3, 0, 1] => [null, 10, null]\n      ColumnVectorWithFilter mappedVector =\n          new ColumnVectorWithFilter(delegate, new int[] {3, 0, 1});\n\n      assertMappedValues(mappedVector, mappedVector::getInt, new Integer[] {null, 10, null});\n    }\n  }\n\n  @Test\n  void testAllNullColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(3, DataTypes.IntegerType)) {\n      delegate.putNull(0);\n      delegate.putNull(1);\n      delegate.putNull(2);\n      ColumnVectorWithFilter mappedVector = new ColumnVectorWithFilter(delegate, new int[] {2, 0});\n\n      assertMappedValues(mappedVector, mappedVector::getInt, new Integer[] {null, null});\n    }\n  }\n\n  @Test\n  void testNoneSelectColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(3, DataTypes.IntegerType)) {\n      delegate.putInt(0, 10);\n      delegate.putInt(1, 20);\n      delegate.putInt(2, 30);\n\n      ColumnVectorWithFilter noRowsSelected = new ColumnVectorWithFilter(delegate, new int[] {});\n      assertEquals(DataTypes.IntegerType, noRowsSelected.dataType());\n    }\n  }\n\n  @Test\n  void testAllSelectColumnVector() {\n    try (WritableColumnVector delegate = new OnHeapColumnVector(3, DataTypes.IntegerType)) {\n      delegate.putInt(0, 10);\n      delegate.putInt(1, 20);\n      delegate.putInt(2, 30);\n\n      // all select\n      ColumnVectorWithFilter allRowsSelected =\n          new ColumnVectorWithFilter(delegate, new int[] {0, 1, 2});\n      assertMappedValues(allRowsSelected, allRowsSelected::getInt, new Integer[] {10, 20, 30});\n    }\n  }\n\n  private static <T> void assertMappedValues(\n      ColumnVector vector, IntFunction<T> getter, T[] expected) {\n    for (int i = 0; i < expected.length; i++) {\n      if (expected[i] == null) {\n        assertTrue(vector.isNullAt(i), \"Expected null at mapped row \" + i);\n      } else {\n        assertFalse(vector.isNullAt(i), \"Expected non-null at mapped row \" + i);\n        assertEquals(expected[i], getter.apply(i), \"Mismatch at mapped row \" + i);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/deletionvector/DeletionVectorReadFunctionTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read.deletionvector;\n\nimport static io.delta.spark.internal.v2.InternalRowTestUtils.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.execution.vectorized.OnHeapColumnVector;\nimport org.apache.spark.sql.execution.vectorized.WritableColumnVector;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.apache.spark.sql.vectorized.ColumnVector;\nimport org.apache.spark.sql.vectorized.ColumnarBatch;\nimport org.junit.jupiter.api.Test;\n\npublic class DeletionVectorReadFunctionTest {\n\n  private static final StructType DATA_SCHEMA =\n      new StructType().add(\"id\", DataTypes.IntegerType).add(\"name\", DataTypes.StringType);\n  private static final StructType PARTITION_SCHEMA = new StructType();\n\n  // ===== Row-based tests =====\n\n  @Test\n  public void testFilterDeletedRowsAndProjectRemovesDvColumn() {\n    // Input: 3 rows, middle one is deleted.\n    List<InternalRow> inputRows =\n        List.of(\n            row(1, \"alice\", (byte) 0), // Not deleted.\n            row(2, \"bob\", (byte) 1), // Deleted.\n            row(3, \"charlie\", (byte) 0)); // Not deleted.\n\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(DATA_SCHEMA, PARTITION_SCHEMA);\n    DeletionVectorReadFunction readFunc =\n        DeletionVectorReadFunction.wrap(mockReader(inputRows), context, false);\n\n    List<InternalRow> result = collectRows(readFunc.apply(/* file= */ null));\n\n    // Verify filtered and projected output (DV column removed, deleted row filtered).\n    assertRowsEquals(result, List.of(row(1, \"alice\"), row(3, \"charlie\")));\n  }\n\n  @Test\n  public void testAllRowsDeleted() {\n    List<InternalRow> inputRows =\n        List.of(row(1, \"alice\", (byte) 1), row(2, \"bob\", (byte) 1)); // All deleted.\n\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(DATA_SCHEMA, PARTITION_SCHEMA);\n    DeletionVectorReadFunction readFunc =\n        DeletionVectorReadFunction.wrap(mockReader(inputRows), context, false);\n\n    List<InternalRow> result = collectRows(readFunc.apply(/* file= */ null));\n\n    assertRowsEquals(result, List.of());\n  }\n\n  @Test\n  public void testNoRowsDeleted() {\n    List<InternalRow> inputRows =\n        List.of(row(1, \"alice\", (byte) 0), row(2, \"bob\", (byte) 0), row(3, \"charlie\", (byte) 0));\n\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(DATA_SCHEMA, PARTITION_SCHEMA);\n    DeletionVectorReadFunction readFunc =\n        DeletionVectorReadFunction.wrap(mockReader(inputRows), context, false);\n\n    List<InternalRow> result = collectRows(readFunc.apply(/* file= */ null));\n\n    assertRowsEquals(result, List.of(row(1, \"alice\"), row(2, \"bob\"), row(3, \"charlie\")));\n  }\n\n  // ===== ColumnarBatch (vectorized) tests =====\n\n  @Test\n  public void testBatchFilterDeletedRowsAndProjectRemovesDvColumn() {\n    // 3 rows: row 1 deleted, rows 0 and 2 kept.\n    ColumnarBatch inputBatch = createBatch(new int[] {1, 2, 3}, new byte[] {0, 1, 0});\n    List<ColumnarBatch> result = runBatchRead(inputBatch);\n\n    assertEquals(1, result.size());\n    // Filtered: row 0 -> original 0 (id=1), row 1 -> original 2 (id=3)\n    assertBatchRows(result.get(0), new int[] {1, 3}, new String[] {\"name_0\", \"name_2\"});\n  }\n\n  @Test\n  public void testBatchAllRowsDeleted() {\n    ColumnarBatch inputBatch = createBatch(new int[] {1, 2}, new byte[] {1, 1});\n    List<ColumnarBatch> result = runBatchRead(inputBatch);\n\n    assertEquals(1, result.size());\n    assertBatchRows(result.get(0), new int[] {}, new String[] {});\n  }\n\n  @Test\n  public void testBatchNoRowsDeleted() {\n    ColumnarBatch inputBatch = createBatch(new int[] {1, 2, 3}, new byte[] {0, 0, 0});\n    List<ColumnarBatch> result = runBatchRead(inputBatch);\n\n    assertEquals(1, result.size());\n    assertBatchRows(\n        result.get(0), new int[] {1, 2, 3}, new String[] {\"name_0\", \"name_1\", \"name_2\"});\n  }\n\n  @Test\n  public void testBatchMultipleBatchesWithDifferentBatch() {\n    ColumnarBatch allDeleted = createBatch(new int[] {1, 2, 3}, new byte[] {1, 1, 1});\n    ColumnarBatch mixed = createBatch(new int[] {4, 5, 6}, new byte[] {0, 1, 0});\n    ColumnarBatch allLive = createBatch(new int[] {7}, new byte[] {0});\n\n    // Ordering 1: allDeleted, mixed, allLive\n    List<ColumnarBatch> result1 = runBatchRead(allDeleted, mixed, allLive);\n    assertEquals(3, result1.size());\n    assertBatchRows(result1.get(0), new int[] {}, new String[] {});\n    assertBatchRows(result1.get(1), new int[] {4, 6}, new String[] {\"name_0\", \"name_2\"});\n    assertBatchRows(result1.get(2), new int[] {7}, new String[] {\"name_0\"});\n\n    // Ordering 2: allLive, allDeleted, mixed\n    List<ColumnarBatch> result2 = runBatchRead(allLive, allDeleted, mixed);\n    assertEquals(3, result2.size());\n    assertBatchRows(result2.get(0), new int[] {7}, new String[] {\"name_0\"});\n    assertBatchRows(result2.get(1), new int[] {}, new String[] {});\n    assertBatchRows(result2.get(2), new int[] {4, 6}, new String[] {\"name_0\", \"name_2\"});\n  }\n\n  private List<ColumnarBatch> runBatchRead(ColumnarBatch... inputBatches) {\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(DATA_SCHEMA, PARTITION_SCHEMA);\n    DeletionVectorReadFunction readFunc =\n        DeletionVectorReadFunction.wrap(mockBatchReader(List.of(inputBatches)), context, true);\n    return collectBatches(readFunc.apply(/* file= */ null));\n  }\n\n  private static void assertBatchRows(\n      ColumnarBatch batch, int[] expectedIds, String[] expectedNames) {\n    assertEquals(expectedIds.length, expectedNames.length, \"Expected id/name lengths must match\");\n    assertEquals(expectedIds.length, batch.numRows(), \"Unexpected number of filtered rows\");\n    for (int i = 0; i < expectedIds.length; i++) {\n      assertEquals(expectedIds[i], batch.column(0).getInt(i));\n      assertEquals(expectedNames[i], batch.column(1).getUTF8String(i).toString());\n    }\n  }\n\n  /**\n   * Creates a ColumnarBatch with columns [id (int), name (string), is_row_deleted (byte)].\n   *\n   * <p>Name values are auto-generated as \"name_0\", \"name_1\", etc.\n   */\n  private static ColumnarBatch createBatch(int[] ids, byte[] deletionVector) {\n    int numRows = ids.length;\n    WritableColumnVector idCol = new OnHeapColumnVector(numRows, DataTypes.IntegerType);\n    WritableColumnVector nameCol = new OnHeapColumnVector(numRows, DataTypes.StringType);\n    WritableColumnVector dvCol = new OnHeapColumnVector(numRows, DataTypes.ByteType);\n\n    for (int i = 0; i < numRows; i++) {\n      idCol.putInt(i, ids[i]);\n      nameCol.putByteArray(i, (\"name_\" + i).getBytes(StandardCharsets.UTF_8));\n      dvCol.putByte(i, deletionVector[i]);\n    }\n\n    return new ColumnarBatch(new ColumnVector[] {idCol, nameCol, dvCol}, numRows);\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/read/deletionvector/DeletionVectorSchemaContextTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.read.deletionvector;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport org.apache.spark.sql.delta.DeltaParquetFileFormat;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.junit.jupiter.api.Test;\n\npublic class DeletionVectorSchemaContextTest {\n\n  // Common test schemas.\n  private static final StructType DATA_SCHEMA =\n      new StructType().add(\"id\", DataTypes.IntegerType).add(\"name\", DataTypes.StringType);\n  private static final StructType PARTITION_SCHEMA =\n      new StructType().add(\"date\", DataTypes.StringType);\n\n  @Test\n  void testWithFullSchemas() {\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(DATA_SCHEMA, PARTITION_SCHEMA);\n\n    StructType expectedSchemaWithDv =\n        DATA_SCHEMA.add(DeltaParquetFileFormat.IS_ROW_DELETED_STRUCT_FIELD());\n    assertEquals(expectedSchemaWithDv, context.getSchemaWithDvColumn());\n    assertEquals(2, context.getDvColumnIndex());\n    // Input: 2 data + 1 DV + 1 partition = 4.\n    assertEquals(4, context.getInputColumnCount());\n    StructType expectedOutputSchema =\n        DATA_SCHEMA.merge(PARTITION_SCHEMA, /* handleDuplicateColumns= */ false);\n    assertEquals(expectedOutputSchema, context.getOutputSchema());\n  }\n\n  @Test\n  void testEmptyPartitionSchema() {\n    StructType emptyPartition = new StructType();\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(DATA_SCHEMA, emptyPartition);\n\n    StructType expectedSchemaWithDv =\n        DATA_SCHEMA.add(DeltaParquetFileFormat.IS_ROW_DELETED_STRUCT_FIELD());\n    assertEquals(expectedSchemaWithDv, context.getSchemaWithDvColumn());\n    assertEquals(2, context.getDvColumnIndex());\n    // Input: 2 data + 1 DV = 3.\n    assertEquals(3, context.getInputColumnCount());\n    assertEquals(DATA_SCHEMA, context.getOutputSchema());\n  }\n\n  @Test\n  void testEmptyDataSchema() {\n    StructType emptyData = new StructType();\n    DeletionVectorSchemaContext context =\n        new DeletionVectorSchemaContext(emptyData, PARTITION_SCHEMA);\n\n    StructType expectedSchemaWithDv =\n        emptyData.add(DeltaParquetFileFormat.IS_ROW_DELETED_STRUCT_FIELD());\n    assertEquals(expectedSchemaWithDv, context.getSchemaWithDvColumn());\n    assertEquals(0, context.getDvColumnIndex());\n    // Input: 1 DV + 1 partition = 2.\n    assertEquals(2, context.getInputColumnCount());\n    assertEquals(PARTITION_SCHEMA, context.getOutputSchema());\n  }\n\n  @Test\n  void testDuplicateDvColumnThrowsException() {\n    // Schema that already contains the DV column.\n    StructType schemaWithDv =\n        new StructType()\n            .add(\"id\", DataTypes.IntegerType)\n            .add(DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME(), DataTypes.ByteType);\n\n    IllegalArgumentException exception =\n        assertThrows(\n            IllegalArgumentException.class,\n            () -> new DeletionVectorSchemaContext(schemaWithDv, new StructType()));\n\n    assertTrue(\n        exception.getMessage().contains(DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME()));\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/snapshot/PathBasedSnapshotManagerTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport io.delta.spark.internal.v2.exception.VersionNotFoundException;\nimport java.io.File;\nimport java.sql.Timestamp;\nimport java.util.stream.Stream;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.delta.DeltaLog;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport scala.Option;\n\npublic class PathBasedSnapshotManagerTest extends DeltaV2TestBase {\n\n  private PathBasedSnapshotManager snapshotManager;\n\n  @Test\n  public void testUnsafeVolatileSnapshot(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_volatile_snapshot\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.Snapshot deltaSnapshot = deltaLog.unsafeVolatileSnapshot();\n    Snapshot kernelSnapshot = snapshotManager.loadLatestSnapshot();\n\n    spark.sql(String.format(\"INSERT INTO %s VALUES (4, 'David')\", testTableName));\n\n    assertEquals(0L, deltaSnapshot.version());\n    assertEquals(deltaSnapshot.version(), kernelSnapshot.getVersion());\n  }\n\n  @Test\n  public void testLoadLatestSnapshot(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_update\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    Snapshot initialSnapshot = snapshotManager.loadLatestSnapshot();\n    assertEquals(0L, initialSnapshot.getVersion());\n\n    spark.sql(String.format(\"INSERT INTO %s VALUES (4, 'David')\", testTableName));\n\n    org.apache.spark.sql.delta.Snapshot deltaSnapshot =\n        deltaLog.update(false, Option.empty(), Option.empty());\n    Snapshot updatedSnapshot = snapshotManager.loadLatestSnapshot();\n    org.apache.spark.sql.delta.Snapshot cachedSnapshot = deltaLog.unsafeVolatileSnapshot();\n    Snapshot kernelcachedSnapshot = snapshotManager.loadLatestSnapshot();\n\n    assertEquals(1L, updatedSnapshot.getVersion());\n    assertEquals(deltaSnapshot.version(), updatedSnapshot.getVersion());\n    assertEquals(1L, kernelcachedSnapshot.getVersion());\n    assertEquals(cachedSnapshot.version(), kernelcachedSnapshot.getVersion());\n  }\n\n  @Test\n  public void testMultipleLoadLatestSnapshot(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_multiple_updates\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    assertEquals(0L, snapshotManager.loadLatestSnapshot().getVersion());\n\n    for (int i = 0; i < 3; i++) {\n      spark.sql(\n          String.format(\"INSERT INTO %s VALUES (%d, 'User%d')\", testTableName, 20 + i, 20 + i));\n\n      org.apache.spark.sql.delta.Snapshot deltaSnapshot =\n          deltaLog.update(false, Option.empty(), Option.empty());\n      Snapshot kernelSnapshot = snapshotManager.loadLatestSnapshot();\n\n      long expectedVersion = i + 1;\n      assertEquals(expectedVersion, deltaSnapshot.version());\n      assertEquals(expectedVersion, kernelSnapshot.getVersion());\n    }\n  }\n\n  @Test\n  public void testLoadSnapshotAt(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_load_at_version\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    // Create multiple versions\n    for (int i = 0; i < 3; i++) {\n      spark.sql(\n          String.format(\"INSERT INTO %s VALUES (%d, 'User%d')\", testTableName, 10 + i, 10 + i));\n    }\n\n    // Load specific versions\n    Snapshot snapshot0 = snapshotManager.loadSnapshotAt(0L);\n    assertEquals(0L, snapshot0.getVersion());\n\n    Snapshot snapshot1 = snapshotManager.loadSnapshotAt(1L);\n    assertEquals(1L, snapshot1.getVersion());\n\n    Snapshot snapshot2 = snapshotManager.loadSnapshotAt(2L);\n    assertEquals(2L, snapshot2.getVersion());\n\n    Snapshot snapshot3 = snapshotManager.loadSnapshotAt(3L);\n    assertEquals(3L, snapshot3.getVersion());\n\n    // Note: loadSnapshotAt does not update the cached snapshot\n  }\n\n  private void setupTableWithDeletedVersions(String testTablePath, String testTableName) {\n    createEmptyTestTable(testTablePath, testTableName);\n    for (int i = 0; i < 10; i++) {\n      spark.sql(\n          String.format(\"INSERT INTO %s VALUES (%d, 'User%d')\", testTableName, 100 + i, 100 + i));\n    }\n    File deltaLogDir = new File(testTablePath, \"_delta_log\");\n    File version0File = new File(deltaLogDir, \"00000000000000000000.json\");\n    File version1File = new File(deltaLogDir, \"00000000000000000001.json\");\n    assertTrue(version0File.exists());\n    assertTrue(version1File.exists());\n    version0File.delete();\n    version1File.delete();\n    assertFalse(version0File.exists());\n    assertFalse(version1File.exists());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_pastTimestamp(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_past\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Thread.sleep(100);\n    Timestamp timestamp = new Timestamp(System.currentTimeMillis());\n    spark.sql(String.format(\"INSERT INTO %s VALUES (200, 'NewUser')\", testTableName));\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                timestamp,\n                Option.empty() /* catalogTable */,\n                false /* canReturnLastCommit */,\n                true /* mustBeRecreatable */,\n                false /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            timestamp.getTime(),\n            false /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_futureTimestamp_canReturnLast(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_future_last\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp futureTimestamp = new Timestamp(System.currentTimeMillis() + 10000);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                futureTimestamp,\n                Option.empty() /* catalogTable */,\n                true /* canReturnLastCommit */,\n                true /* mustBeRecreatable */,\n                false /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            futureTimestamp.getTime(),\n            true /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_futureTimestamp_notMustBeRecreatable(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_future_not_recreatable\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp futureTimestamp = new Timestamp(System.currentTimeMillis() + 10000);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                futureTimestamp,\n                Option.empty() /* catalogTable */,\n                true /* canReturnLastCommit */,\n                false /* mustBeRecreatable */,\n                false /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            futureTimestamp.getTime(),\n            true /* canReturnLastCommit */,\n            false /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_earlyTimestamp_canReturnEarliest(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_early\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp earlyTimestamp = new Timestamp(0);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                earlyTimestamp,\n                Option.empty() /* catalogTable */,\n                false /* canReturnLastCommit */,\n                true /* mustBeRecreatable */,\n                true /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            earlyTimestamp.getTime(),\n            false /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            true /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_earlyTimestamp_notMustBeRecreatable_canReturnEarliest(\n      @TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_early_not_recreatable\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp earlyTimestamp = new Timestamp(0);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                earlyTimestamp,\n                Option.empty() /* catalogTable */,\n                false /* canReturnLastCommit */,\n                false /* mustBeRecreatable */,\n                true /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            earlyTimestamp.getTime(),\n            false /* canReturnLastCommit */,\n            false /* mustBeRecreatable */,\n            true /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  private static Stream<Arguments> checkVersionExistsTestCases() {\n    return Stream.of(\n        Arguments.of(\n            \"current\",\n            10L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            false /* shouldThrow */),\n        Arguments.of(\n            \"notAllowOutOfRange\",\n            21L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            true /* shouldThrow */),\n        Arguments.of(\n            \"allowOutOfRange\",\n            21L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            true /* allowOutOfRange */,\n            false /* shouldThrow */),\n        Arguments.of(\n            \"belowEarliest\",\n            1L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            true /* shouldThrow */),\n        Arguments.of(\n            \"mustBeRecreatable_false\",\n            2L /* versionToCheck */,\n            false /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            false /* shouldThrow */),\n        Arguments.of(\n            \"mustBeRecreatable_true\",\n            2L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            true /* shouldThrow */));\n  }\n\n  @ParameterizedTest(name = \"{0}\")\n  @MethodSource(\"checkVersionExistsTestCases\")\n  public void testCheckVersionExists(\n      String testName,\n      long versionToCheck,\n      boolean mustBeRecreatable,\n      boolean allowOutOfRange,\n      boolean shouldThrow,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_version_\" + testName;\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    if (shouldThrow) {\n      assertThrows(\n          VersionNotFoundException.class,\n          () ->\n              snapshotManager.checkVersionExists(\n                  versionToCheck, mustBeRecreatable, allowOutOfRange));\n\n      assertThrows(\n          org.apache.spark.sql.delta.VersionNotFoundException.class,\n          () ->\n              deltaLog\n                  .history()\n                  .checkVersionExists(\n                      versionToCheck, Option.empty(), mustBeRecreatable, allowOutOfRange));\n    } else {\n      snapshotManager.checkVersionExists(versionToCheck, mustBeRecreatable, allowOutOfRange);\n      deltaLog\n          .history()\n          .checkVersionExists(versionToCheck, Option.empty(), mustBeRecreatable, allowOutOfRange);\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/snapshot/unitycatalog/UCTableInfoTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot.unitycatalog;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\n\n/** Tests for {@link UCTableInfo}. */\nclass UCTableInfoTest {\n\n  @Test\n  void testConstructor() {\n    // Use distinctive values that would fail if implementation had hardcoded defaults\n    String tableId = \"uc_tbl_7f3a9b2c-e8d1-4f6a\";\n    String tablePath = \"abfss://container@acct.dfs.core.windows.net/delta/v2\";\n    String ucUri = \"https://uc-server.example.net/api/2.1/uc\";\n    String ucToken = \"dapi_Kx9mN$2pQr#7vWz\";\n\n    Map<String, String> authConfig = new HashMap<>();\n    authConfig.put(\"type\", \"static\");\n    authConfig.put(\"token\", ucToken);\n\n    UCTableInfo info = new UCTableInfo(tableId, tablePath, ucUri, authConfig);\n\n    assertEquals(tableId, info.getTableId(), \"Table ID should be stored correctly\");\n    assertEquals(tablePath, info.getTablePath(), \"Table path should be stored correctly\");\n    assertEquals(ucUri, info.getUcUri(), \"UC URI should be stored correctly\");\n\n    Map<String, String> ret = info.getAuthConfig();\n    assertEquals(\"static\", ret.get(\"type\"), \"Type should be static\");\n    assertEquals(ucToken, ret.get(\"token\"), \"UC token should be stored correctly in configMap\");\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/CloseableIteratorTest.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport org.junit.jupiter.api.Test;\nimport scala.collection.Iterator;\nimport scala.collection.JavaConverters;\n\npublic class CloseableIteratorTest {\n\n  @Test\n  public void testFilterMapAndClose() throws IOException {\n    AtomicBoolean closed = new AtomicBoolean(false);\n    Iterator<Integer> base = new CloseableTestIterator<>(Arrays.asList(1, 2, 3, 4, 5, 6), closed);\n\n    CloseableIterator<String> iter =\n        CloseableIterator.wrap(base).filterCloseable(x -> x % 2 == 0).mapCloseable(x -> \"v\" + x);\n\n    List<String> result = new ArrayList<>();\n    while (iter.hasNext()) {\n      result.add(iter.next());\n    }\n\n    assertEquals(List.of(\"v2\", \"v4\", \"v6\"), result);\n    assertFalse(closed.get());\n    iter.close();\n    assertTrue(closed.get());\n  }\n\n  private static class CloseableTestIterator<T> implements Iterator<T>, Closeable {\n    private final Iterator<T> delegate;\n    private final AtomicBoolean closed;\n\n    CloseableTestIterator(List<T> elements, AtomicBoolean closed) {\n      this.delegate = JavaConverters.asScalaIterator(elements.iterator());\n      this.closed = closed;\n    }\n\n    @Override\n    public boolean hasNext() {\n      return delegate.hasNext();\n    }\n\n    @Override\n    public T next() {\n      return delegate.next();\n    }\n\n    @Override\n    public void close() {\n      closed.set(true);\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/ExpressionUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.delta.kernel.expressions.Literal;\nimport io.delta.kernel.expressions.Predicate;\nimport io.delta.kernel.types.*;\nimport java.math.BigDecimal;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\nimport org.apache.spark.sql.connector.expressions.FieldReference;\nimport org.apache.spark.sql.connector.expressions.LiteralValue;\nimport org.apache.spark.sql.connector.expressions.NamedReference;\nimport org.apache.spark.sql.sources.*;\nimport org.apache.spark.unsafe.types.UTF8String;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/** Tests for {@link ExpressionUtils}. */\npublic class ExpressionUtilsTest {\n\n  // Test data provider for comparison filters\n  static Stream<Arguments> comparisonFiltersProvider() {\n    return Stream.of(\n        Arguments.of(\"EqualTo\", (Supplier<Filter>) () -> new EqualTo(\"id\", 42), \"=\"),\n        Arguments.of(\"GreaterThan\", (Supplier<Filter>) () -> new GreaterThan(\"age\", 18), \">\"),\n        Arguments.of(\n            \"GreaterThanOrEqual\", (Supplier<Filter>) () -> new GreaterThanOrEqual(\"age\", 18), \">=\"),\n        Arguments.of(\"LessThan\", (Supplier<Filter>) () -> new LessThan(\"age\", 65), \"<\"),\n        Arguments.of(\n            \"LessThanOrEqual\", (Supplier<Filter>) () -> new LessThanOrEqual(\"age\", 65), \"<=\"));\n  }\n\n  @ParameterizedTest(name = \"{0} filter should be converted to {2}\")\n  @MethodSource(\"comparisonFiltersProvider\")\n  public void testComparisonFilters(\n      String filterName, Supplier<Filter> filterSupplier, String expectedOperator) {\n    Filter filter = filterSupplier.get();\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertTrue(result.isPresent(), filterName + \" filter should be converted\");\n    assertFalse(result.isPartial(), filterName + \" filter should be fully converted\");\n    assertEquals(expectedOperator, result.get().getName());\n    assertEquals(2, result.get().getChildren().size());\n  }\n\n  // Test data provider for null filters\n  static Stream<Arguments> nullFiltersProvider() {\n    return Stream.of(\n        Arguments.of(\"IsNull\", (Supplier<Filter>) () -> new IsNull(\"name\"), \"IS_NULL\"),\n        Arguments.of(\"IsNotNull\", (Supplier<Filter>) () -> new IsNotNull(\"name\"), \"IS_NOT_NULL\"));\n  }\n\n  @ParameterizedTest(name = \"{0} filter should be converted to {2}\")\n  @MethodSource(\"nullFiltersProvider\")\n  public void testNullFilters(\n      String filterName, Supplier<Filter> filterSupplier, String expectedOperator) {\n    Filter filter = filterSupplier.get();\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertTrue(result.isPresent(), filterName + \" filter should be converted\");\n    assertFalse(result.isPartial(), filterName + \" filter should be fully converted\");\n    assertEquals(expectedOperator, result.get().getName());\n    assertEquals(1, result.get().getChildren().size());\n  }\n\n  @Test\n  public void testEqualNullSafeFilter() {\n    // Test EqualNullSafe with null value - converted to IS_NULL\n    // Cannot use IS NOT DISTINCT FROM because kernel requires typed null literals\n    EqualNullSafe nullFilter = new EqualNullSafe(\"name\", null);\n    ExpressionUtils.ConvertedPredicate nullResult =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(nullFilter);\n\n    assertTrue(nullResult.isPresent(), \"EqualNullSafe with null should be converted\");\n    assertFalse(nullResult.isPartial(), \"EqualNullSafe with null should be fully converted\");\n    assertEquals(\"IS_NULL\", nullResult.get().getName());\n    assertEquals(1, nullResult.get().getChildren().size());\n\n    // Test EqualNullSafe with non-null value - uses \"=\" operator\n    EqualNullSafe nonNullFilter = new EqualNullSafe(\"id\", 42);\n    ExpressionUtils.ConvertedPredicate nonNullResult =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(nonNullFilter);\n\n    assertTrue(nonNullResult.isPresent(), \"EqualNullSafe with value should be converted\");\n    assertFalse(nonNullResult.isPartial(), \"EqualNullSafe with value should be fully converted\");\n    assertEquals(\"=\", nonNullResult.get().getName());\n    assertEquals(2, nonNullResult.get().getChildren().size());\n  }\n\n  static Stream<Arguments> stringStartsWithProvider() {\n    return Stream.of(Arguments.of(\"non-empty prefix\", \"Al\"), Arguments.of(\"empty prefix\", \"\"));\n  }\n\n  @ParameterizedTest(name = \"StringStartsWith with {0} should be converted\")\n  @MethodSource(\"stringStartsWithProvider\")\n  public void testStringStartsWithFilter(String desc, String prefix) {\n    StringStartsWith filter = new StringStartsWith(\"name\", prefix);\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertTrue(result.isPresent(), \"StringStartsWith filter should be converted\");\n    assertFalse(result.isPartial(), \"StringStartsWith filter should be fully converted\");\n    assertEquals(\"STARTS_WITH\", result.get().getName());\n    // Children: column + string literal\n    assertEquals(2, result.get().getChildren().size());\n  }\n\n  @Test\n  public void testStringStartsWithFilter_NullValue() {\n    // A null prefix cannot be converted — treated as unsupported, falls back to post-scan\n    StringStartsWith filter = new StringStartsWith(\"name\", null);\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertFalse(result.isPresent(), \"StringStartsWith with null value should not be converted\");\n  }\n\n  @Test\n  public void testInFilter_BasicConversion() {\n    In filter = new In(\"city\", new Object[] {\"hz\", \"sh\", \"bj\"});\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertTrue(result.isPresent(), \"In filter should be converted\");\n    assertFalse(result.isPartial(), \"In filter should be fully converted\");\n    assertEquals(\"IN\", result.get().getName());\n    // Children: column + 3 literals\n    assertEquals(4, result.get().getChildren().size());\n  }\n\n  @Test\n  public void testInFilter_SingleValue() {\n    In filter = new In(\"id\", new Object[] {42});\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertTrue(result.isPresent(), \"In filter with single value should be converted\");\n    assertFalse(result.isPartial(), \"In filter should be fully converted\");\n    assertEquals(\"IN\", result.get().getName());\n    // Children: column + 1 literal\n    assertEquals(2, result.get().getChildren().size());\n  }\n\n  @Test\n  public void testInFilter_EmptyValues() {\n    In filter = new In(\"city\", new Object[] {});\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    // Empty IN list always evaluates to FALSE; push ALWAYS_FALSE so the kernel skips all files.\n    assertTrue(result.isPresent(), \"In filter with empty values should push ALWAYS_FALSE\");\n    assertEquals(\"ALWAYS_FALSE\", result.get().getName());\n  }\n\n  @Test\n  public void testInFilter_WithNullValue() {\n    // null in the values array makes the IN expression unsafe to push down (SQL null semantics)\n    In filter = new In(\"city\", new Object[] {\"hz\", null, \"bj\"});\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertFalse(result.isPresent(), \"In filter with null value should not be pushed down\");\n  }\n\n  @Test\n  public void testInFilter_WithUnsupportedType() {\n    In filter = new In(\"col\", new Object[] {42, new Object()});\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertFalse(result.isPresent(), \"In filter with unconvertible value should not be pushed down\");\n  }\n\n  @Test\n  public void testInFilter_InAndFilter() {\n    // AND(In(...), EqualTo(...)) — both convertible, should be fully pushed down\n    In inFilter = new In(\"city\", new Object[] {\"hz\", \"sh\"});\n    EqualTo eqFilter = new EqualTo(\"part\", 1);\n    org.apache.spark.sql.sources.And andFilter =\n        new org.apache.spark.sql.sources.And(inFilter, eqFilter);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter);\n\n    assertTrue(result.isPresent(), \"AND(In, EqualTo) should be converted\");\n    assertFalse(result.isPartial(), \"AND(In, EqualTo) should be fully converted\");\n    assertTrue(\n        result.get() instanceof io.delta.kernel.expressions.And,\n        \"Result should be an AND predicate\");\n  }\n\n  @Test\n  public void testInFilter_NotInWithNullValue() {\n    // NOT(IN(col, 1, null)): null in the IN list causes IN to bail → NOT also bails\n    In inFilter = new In(\"city\", new Object[] {\"hz\", null});\n    Not notFilter = new Not(inFilter);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(notFilter);\n\n    assertFalse(\n        result.isPresent(), \"NOT(IN(..., null)) should not be pushed down due to null in IN list\");\n  }\n\n  @Test\n  public void testInFilter_ORWithUnsupportedFilter() {\n    // OR(In(...), StringEndsWith(...)) — one branch unsupported, whole OR cannot be pushed\n    In inFilter = new In(\"city\", new Object[] {\"hz\", \"sh\"});\n    StringEndsWith endsWithFilter = new StringEndsWith(\"name\", \"foo\");\n    Or orFilter = new Or(inFilter, endsWithFilter);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(orFilter);\n\n    assertFalse(\n        result.isPresent(),\n        \"OR(In, unsupported) should not be pushed down when one branch is unsupported\");\n  }\n\n  // Test data provider for parameterized literal conversion tests\n  static Stream<Arguments> valueTypesProvider() {\n    return Stream.of(\n        // Primitive types\n        Arguments.of(\"Boolean\", true, BooleanType.BOOLEAN),\n        Arguments.of(\"Byte\", (byte) 42, ByteType.BYTE),\n        Arguments.of(\"Short\", (short) 1000, ShortType.SHORT),\n        Arguments.of(\"Integer\", 12345, IntegerType.INTEGER),\n        Arguments.of(\"Long\", 123456789L, LongType.LONG),\n        Arguments.of(\"Float\", 3.14f, FloatType.FLOAT),\n        Arguments.of(\"Double\", 2.718281828, DoubleType.DOUBLE),\n\n        // BigDecimal - precision=6, scale=3 for \"123.456\"\n        Arguments.of(\"BigDecimal\", new BigDecimal(\"123.456\"), new DecimalType(6, 3)),\n\n        // String type\n        Arguments.of(\"String\", \"hello world\", StringType.STRING),\n        Arguments.of(\"UTF8String\", UTF8String.fromString(\"hello world\"), StringType.STRING),\n\n        // Binary data\n        Arguments.of(\"byte[]\", new byte[] {1, 2, 3, 4, 5}, BinaryType.BINARY),\n\n        // Date/time types (java.sql types for V1 Filters)\n        Arguments.of(\"java.sql.Date\", java.sql.Date.valueOf(\"2023-01-15\"), DateType.DATE),\n        Arguments.of(\n            \"java.sql.Timestamp\",\n            java.sql.Timestamp.valueOf(\"2023-01-15 10:30:00\"),\n            TimestampType.TIMESTAMP));\n  }\n\n  @Test\n  public void testUnsupportedFilter() {\n    // Create an unsupported filter (StringContains is not implemented in our conversion method)\n    Filter unsupportedFilter = new StringContains(\"col1\", \"test\");\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(unsupportedFilter);\n    assertFalse(result.isPresent(), \"Unsupported filters should return empty Optional\");\n    assertFalse(result.isPartial(), \"Unsupported filters should not be marked as partial\");\n  }\n\n  @Test\n  public void testAndFilter() {\n    EqualTo leftFilter = new EqualTo(\"id\", 1);\n    GreaterThan rightFilter = new GreaterThan(\"age\", 18);\n    org.apache.spark.sql.sources.And andFilter =\n        new org.apache.spark.sql.sources.And(leftFilter, rightFilter);\n\n    ExpressionUtils.ConvertedPredicate andResult =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter);\n\n    assertTrue(andResult.isPresent(), \"And filter should be converted\");\n    assertFalse(andResult.isPartial(), \"And filter should be fully converted\");\n    assertTrue(\n        andResult.get() instanceof io.delta.kernel.expressions.And,\n        \"Result should be And predicate\");\n    assertEquals(2, andResult.get().getChildren().size());\n  }\n\n  @Test\n  public void testAndFilter_PartialPushDownWithLeftConvertible() {\n    // Create an AND filter where left can be converted but right cannot\n    EqualTo leftFilter = new EqualTo(\"id\", 1);\n    Filter unsupportedRightFilter = new StringContains(\"unsupported_col\", \"test\");\n\n    org.apache.spark.sql.sources.And andFilter =\n        new org.apache.spark.sql.sources.And(leftFilter, unsupportedRightFilter);\n\n    // Without partial pushdown - should return empty\n    ExpressionUtils.ConvertedPredicate resultWithoutPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter, false);\n    assertFalse(\n        resultWithoutPartial.isPresent(),\n        \"AND filter with unconvertible operand should return empty without partial pushdown\");\n    assertFalse(resultWithoutPartial.isPartial(), \"Empty result should not be marked as partial\");\n\n    // With partial pushdown - should return the convertible part\n    ExpressionUtils.ConvertedPredicate resultWithPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter, true);\n    assertTrue(\n        resultWithPartial.isPresent(),\n        \"AND filter with partial pushdown should return the convertible operand\");\n    assertTrue(\n        resultWithPartial.isPartial(),\n        \"AND filter with partial pushdown should be marked as partial\");\n    assertEquals(\"=\", resultWithPartial.get().getName());\n    assertEquals(2, resultWithPartial.get().getChildren().size());\n  }\n\n  @Test\n  public void testAndFilter_PartialPushDownWithRightConvertible() {\n    // Create an AND filter where right can be converted but left cannot\n    Filter unsupportedLeftFilter = new StringContains(\"unsupported_col\", \"test\");\n    GreaterThan rightFilter = new GreaterThan(\"age\", 18);\n    org.apache.spark.sql.sources.And andFilter =\n        new org.apache.spark.sql.sources.And(unsupportedLeftFilter, rightFilter);\n\n    // With partial pushdown - should return the convertible part (right side)\n    ExpressionUtils.ConvertedPredicate resultWithPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter, true);\n    assertTrue(resultWithPartial.isPresent(), \"AND filter should return the convertible operand\");\n    assertTrue(resultWithPartial.isPartial(), \"AND filter should be marked as partial\");\n    assertEquals(\">\", resultWithPartial.get().getName());\n    assertEquals(2, resultWithPartial.get().getChildren().size());\n\n    // Without partial pushdown - should return empty\n    ExpressionUtils.ConvertedPredicate resultWithoutPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter, false);\n    assertFalse(\n        resultWithoutPartial.isPresent(),\n        \"AND filter should return empty without partial pushdown\");\n    assertFalse(resultWithoutPartial.isPartial(), \"Empty result should not be marked as partial\");\n  }\n\n  @Test\n  public void testAndFilter_PartialPushDown_BothUnconvertible() {\n    // Create an AND filter where neither side can be converted\n    Filter unsupportedLeftFilter = new StringContains(\"unsupported_col1\", \"test\");\n    Filter unsupportedRightFilter = new StringContains(\"unsupported_col2\", \"test\");\n    org.apache.spark.sql.sources.And andFilter =\n        new org.apache.spark.sql.sources.And(unsupportedLeftFilter, unsupportedRightFilter);\n\n    ExpressionUtils.ConvertedPredicate resultWithPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter);\n    assertFalse(\n        resultWithPartial.isPresent(),\n        \"AND filter should return empty if both operands are unconvertible\");\n    assertFalse(resultWithPartial.isPartial(), \"Empty result should not be marked as partial\");\n  }\n\n  @Test\n  public void testOrFilter_RequiresBothConvertible() {\n    // Create an OR filter where left can be converted but right cannot\n    EqualTo leftFilter = new EqualTo(\"id\", 1);\n    Filter unsupportedRightFilter = new StringContains(\"unsupported_col\", \"test\");\n\n    org.apache.spark.sql.sources.Or orFilter =\n        new org.apache.spark.sql.sources.Or(leftFilter, unsupportedRightFilter);\n\n    ExpressionUtils.ConvertedPredicate resultWithPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(orFilter);\n    assertFalse(\n        resultWithPartial.isPresent(),\n        \"OR filter with unconvertible operand should return empty even with partial pushdown\");\n    assertFalse(resultWithPartial.isPartial(), \"Empty result should not be marked as partial\");\n  }\n\n  @Test\n  public void testNotFilter() {\n    EqualTo leftFilter = new EqualTo(\"id\", 1);\n    Not notFilter = new Not(leftFilter);\n\n    ExpressionUtils.ConvertedPredicate notResult =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(notFilter);\n\n    assertTrue(notResult.isPresent(), \"Not filter should be converted\");\n    assertFalse(notResult.isPartial(), \"Not filter should be fully converted\");\n    assertEquals(\"NOT\", notResult.get().getName());\n    assertEquals(1, notResult.get().getChildren().size());\n  }\n\n  @Test\n  public void testNotFilter_RequiresChildConvertible() {\n    // StringContains is not yet supported\n    Filter unsupportedFilter = new StringContains(\"unsupported_col\", \"test\");\n\n    Not notFilter = new Not(unsupportedFilter);\n\n    // NOT requires child to be convertible.\n    ExpressionUtils.ConvertedPredicate resultWithoutPartial =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(notFilter);\n    assertFalse(\n        resultWithoutPartial.isPresent(),\n        \"NOT filter with unconvertible child should return empty\");\n    assertFalse(resultWithoutPartial.isPartial(), \"Empty result should not be marked as partial\");\n\n    // Create NOT(A AND B) where A is convertible but B is not\n    // This tests that NOT disables partial pushdown for semantic correctness\n    EqualTo convertibleFilter = new EqualTo(\"id\", 1);\n    org.apache.spark.sql.sources.And andFilter =\n        new org.apache.spark.sql.sources.And(convertibleFilter, unsupportedFilter);\n\n    // Now verify that NOT(AND) returns empty because NOT disables partial pushdown\n    Not notAndFilter = new Not(andFilter);\n    ExpressionUtils.ConvertedPredicate notResult =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(notAndFilter);\n    assertFalse(\n        notResult.isPresent(),\n        \"NOT(A AND B) should return empty when B is unconvertible, even with partial pushdown enabled\"\n            + \" - this preserves semantic correctness as NOT(A AND B) != NOT(A)\");\n    assertFalse(notResult.isPartial(), \"Empty result should not be marked as partial\");\n  }\n\n  @ParameterizedTest(name = \"convertValueToKernelLiteral should support {0}\")\n  @MethodSource(\"valueTypesProvider\")\n  public void testConvertValueToKernelLiteral(\n      String typeName, Object value, DataType expectedDataType) {\n    Optional<Literal> result = ExpressionUtils.convertValueToKernelLiteral(value);\n\n    assertTrue(result.isPresent(), \"Value of type \" + typeName + \" should be convertible\");\n\n    Literal literal = result.get();\n    assertNotNull(literal, \"Literal should not be null\");\n    assertNotNull(literal.getDataType(), \"DataType should not be null\");\n    assertEquals(\n        expectedDataType,\n        literal.getDataType(),\n        \"DataType should match expected type for \" + typeName);\n  }\n\n  @Test\n  public void testConvertValueToKernelLiteral_NullValue() {\n    Optional<Literal> result = ExpressionUtils.convertValueToKernelLiteral(null);\n    assertFalse(result.isPresent(), \"null values should return empty Optional\");\n  }\n\n  @Test\n  public void testConvertValueToKernelLiteral_UnsupportedType() {\n    // Test with an unsupported type like a custom object\n    Object unsupportedValue = new Object();\n    Optional<Literal> result = ExpressionUtils.convertValueToKernelLiteral(unsupportedValue);\n    assertFalse(result.isPresent(), \"Unsupported types should return empty Optional\");\n  }\n\n  @Test\n  public void testNestedFieldParsing() {\n    EqualTo nestedFieldFilter = new EqualTo(\"user.profile.name\", \"John\");\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(nestedFieldFilter);\n\n    assertTrue(result.isPresent(), \"Nested field filter should be convertible\");\n    assertFalse(result.isPartial(), \"Nested field filter should be fully convertible\");\n    Predicate predicate = result.get();\n    io.delta.kernel.expressions.Column column =\n        (io.delta.kernel.expressions.Column) predicate.getChildren().get(0);\n    assertArrayEquals(\n        new String[] {\"user\", \"profile\", \"name\"},\n        column.getNames(),\n        \"Nested field names should be parsed correctly\");\n  }\n\n  @Test\n  public void testSingleColumnNameWithDots() {\n    EqualTo singleColumnFilter = new EqualTo(\"`user.profile.name`\", \"value\");\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(singleColumnFilter);\n\n    assertTrue(result.isPresent(), \"Single column filter should be convertible\");\n    assertFalse(result.isPartial(), \"Single column filter should be fully convertible\");\n    Predicate predicate = result.get();\n    io.delta.kernel.expressions.Column column =\n        (io.delta.kernel.expressions.Column) predicate.getChildren().get(0);\n    assertArrayEquals(\n        new String[] {\"user.profile.name\"},\n        column.getNames(),\n        \"Single column name with dots should be preserved as-is\");\n  }\n\n  // Tests for dsv2PredicateToCatalystExpression\n\n  private final org.apache.spark.sql.types.StructType testSchema =\n      new org.apache.spark.sql.types.StructType()\n          .add(\"id\", org.apache.spark.sql.types.DataTypes.IntegerType, false)\n          .add(\"name\", org.apache.spark.sql.types.DataTypes.StringType, true)\n          .add(\"age\", org.apache.spark.sql.types.DataTypes.IntegerType, true);\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_IsNull() {\n    NamedReference nameRef = FieldReference.apply(\"name\");\n    org.apache.spark.sql.connector.expressions.filter.Predicate isNullPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"IS_NULL\", new org.apache.spark.sql.connector.expressions.Expression[] {nameRef});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(isNullPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.IsNull,\n        \"Result should be IsNull expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_IsNotNull() {\n    NamedReference nameRef = FieldReference.apply(\"name\");\n    org.apache.spark.sql.connector.expressions.filter.Predicate isNotNullPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"IS_NOT_NULL\", new org.apache.spark.sql.connector.expressions.Expression[] {nameRef});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(isNotNullPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.IsNotNull,\n        \"Result should be IsNotNull expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_EqualTo() {\n    NamedReference idRef = FieldReference.apply(\"id\");\n    LiteralValue<Integer> value =\n        LiteralValue.apply(42, org.apache.spark.sql.types.DataTypes.IntegerType);\n    org.apache.spark.sql.connector.expressions.filter.Predicate equalToPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"=\", new org.apache.spark.sql.connector.expressions.Expression[] {idRef, value});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(equalToPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.EqualTo,\n        \"Result should be EqualTo expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_LessThan() {\n    NamedReference ageRef = FieldReference.apply(\"age\");\n    LiteralValue<Integer> value =\n        LiteralValue.apply(30, org.apache.spark.sql.types.DataTypes.IntegerType);\n    org.apache.spark.sql.connector.expressions.filter.Predicate lessThanPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"<\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(lessThanPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.LessThan,\n        \"Result should be LessThan expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_GreaterThanOrEqual() {\n    NamedReference ageRef = FieldReference.apply(\"age\");\n    LiteralValue<Integer> value =\n        LiteralValue.apply(18, org.apache.spark.sql.types.DataTypes.IntegerType);\n    org.apache.spark.sql.connector.expressions.filter.Predicate greaterThanOrEqualPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \">=\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(greaterThanOrEqualPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.GreaterThanOrEqual,\n        \"Result should be GreaterThanOrEqual expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_In() {\n    NamedReference idRef = FieldReference.apply(\"id\");\n    LiteralValue<Integer> val1 =\n        LiteralValue.apply(1, org.apache.spark.sql.types.DataTypes.IntegerType);\n    LiteralValue<Integer> val2 =\n        LiteralValue.apply(2, org.apache.spark.sql.types.DataTypes.IntegerType);\n    LiteralValue<Integer> val3 =\n        LiteralValue.apply(3, org.apache.spark.sql.types.DataTypes.IntegerType);\n    org.apache.spark.sql.connector.expressions.filter.Predicate inPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"IN\",\n            new org.apache.spark.sql.connector.expressions.Expression[] {idRef, val1, val2, val3});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(inPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.In,\n        \"Result should be In expression\");\n    org.apache.spark.sql.catalyst.expressions.In inExpr =\n        (org.apache.spark.sql.catalyst.expressions.In) result.get();\n    assertEquals(3, inExpr.list().size(), \"IN expression should have 3 values\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_And() {\n    NamedReference ageRef = FieldReference.apply(\"age\");\n    LiteralValue<Integer> value1 =\n        LiteralValue.apply(18, org.apache.spark.sql.types.DataTypes.IntegerType);\n    LiteralValue<Integer> value2 =\n        LiteralValue.apply(65, org.apache.spark.sql.types.DataTypes.IntegerType);\n\n    org.apache.spark.sql.connector.expressions.filter.Predicate leftPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \">\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value1});\n    org.apache.spark.sql.connector.expressions.filter.Predicate rightPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"<\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value2});\n    org.apache.spark.sql.connector.expressions.filter.Predicate andPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"AND\",\n            new org.apache.spark.sql.connector.expressions.Expression[] {\n              leftPredicate, rightPredicate\n            });\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(andPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.And,\n        \"Result should be And expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_Or() {\n    NamedReference ageRef = FieldReference.apply(\"age\");\n    LiteralValue<Integer> value1 =\n        LiteralValue.apply(18, org.apache.spark.sql.types.DataTypes.IntegerType);\n    LiteralValue<Integer> value2 =\n        LiteralValue.apply(65, org.apache.spark.sql.types.DataTypes.IntegerType);\n\n    org.apache.spark.sql.connector.expressions.filter.Predicate leftPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"<\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value1});\n    org.apache.spark.sql.connector.expressions.filter.Predicate rightPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \">\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value2});\n    org.apache.spark.sql.connector.expressions.filter.Predicate orPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"OR\",\n            new org.apache.spark.sql.connector.expressions.Expression[] {\n              leftPredicate, rightPredicate\n            });\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(orPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.Or,\n        \"Result should be Or expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_Not() {\n    NamedReference ageRef = FieldReference.apply(\"age\");\n    LiteralValue<Integer> value =\n        LiteralValue.apply(18, org.apache.spark.sql.types.DataTypes.IntegerType);\n\n    org.apache.spark.sql.connector.expressions.filter.Predicate childPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"<\", new org.apache.spark.sql.connector.expressions.Expression[] {ageRef, value});\n    org.apache.spark.sql.connector.expressions.filter.Predicate notPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"NOT\", new org.apache.spark.sql.connector.expressions.Expression[] {childPredicate});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(notPredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.Not,\n        \"Result should be Not expression\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_AlwaysTrue() {\n    org.apache.spark.sql.connector.expressions.filter.Predicate alwaysTruePredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"ALWAYS_TRUE\", new org.apache.spark.sql.connector.expressions.Expression[] {});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(alwaysTruePredicate, testSchema);\n\n    assertTrue(result.isPresent(), \"Result should be present\");\n    assertTrue(\n        result.get() instanceof org.apache.spark.sql.catalyst.expressions.Literal,\n        \"Result should be Literal expression\");\n    org.apache.spark.sql.catalyst.expressions.Literal literal =\n        (org.apache.spark.sql.catalyst.expressions.Literal) result.get();\n    assertEquals(true, literal.value(), \"ALWAYS_TRUE should return literal true\");\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_UnsupportedPredicate() {\n    org.apache.spark.sql.connector.expressions.filter.Predicate unsupportedPredicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"UNSUPPORTED_OPERATOR\", new org.apache.spark.sql.connector.expressions.Expression[] {});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(unsupportedPredicate, testSchema);\n\n    assertFalse(result.isPresent(), \"Unsupported predicates should return empty Optional\");\n  }\n\n  // ===== Tests for decimal type alignment with table schema =====\n\n  private final org.apache.spark.sql.types.StructType decimalSchema =\n      new org.apache.spark.sql.types.StructType()\n          .add(\"price\", new org.apache.spark.sql.types.DecimalType(7, 2), true)\n          .add(\"quantity\", org.apache.spark.sql.types.DataTypes.IntegerType, true);\n\n  @Test\n  public void testDecimalLiteralWidenedToColumnType() {\n    // Literal 100.00 has Decimal(5,2), column is Decimal(7,2) → should widen to Decimal(7,2)\n    EqualTo filter = new EqualTo(\"price\", new BigDecimal(\"100.00\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"Decimal filter should be converted with widened type\");\n    assertFalse(result.isPartial());\n    Predicate pred = result.get();\n    assertEquals(\"=\", pred.getName());\n    Literal literal = (Literal) pred.getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralScaleWidened() {\n    // Literal 100 has Decimal(3,0), column is Decimal(7,2) → should widen to Decimal(7,2)\n    EqualTo filter = new EqualTo(\"price\", new BigDecimal(\"100\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"Decimal filter with lower scale should be widened\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n    assertEquals(new BigDecimal(\"100.00\"), literal.getValue());\n  }\n\n  @Test\n  public void testDecimalLiteralHigherScaleThanColumn() {\n    // Literal 99.999 has scale=3, column is Decimal(7,2) → scale exceeds column, skip pushdown\n    GreaterThan filter = new GreaterThan(\"price\", new BigDecimal(\"99.999\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertFalse(\n        result.isPresent(),\n        \"Decimal literal with higher scale than column should not be pushed down\");\n  }\n\n  @Test\n  public void testDecimalLiteralExceedsColumnPrecision() {\n    // Literal 123456.00 has 6 integral digits + 2 scale = precision 8,\n    // column is Decimal(7,2) which holds max 99999.99 → skip pushdown\n    LessThan filter = new LessThan(\"price\", new BigDecimal(\"123456.00\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertFalse(\n        result.isPresent(), \"Decimal literal exceeding column precision should not be pushed down\");\n  }\n\n  @Test\n  public void testDecimalLiteralMatchingType() {\n    // Literal already matches column type Decimal(7,2) → no widening needed\n    EqualTo filter = new EqualTo(\"price\", new BigDecimal(\"12345.67\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"Decimal filter with matching type should be converted\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralWithoutSchema() {\n    // Without schema, decimal literal retains its intrinsic type\n    EqualTo filter = new EqualTo(\"price\", new BigDecimal(\"100.00\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter);\n\n    assertTrue(result.isPresent(), \"Decimal filter without schema should still be converted\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    // 100.00 has precision=5, scale=2\n    assertEquals(new DecimalType(5, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralNonDecimalColumn() {\n    // Column \"quantity\" is IntegerType, not DecimalType → use default conversion\n    EqualTo filter = new EqualTo(\"quantity\", new BigDecimal(\"42\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"BigDecimal for non-decimal column should use default type\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    // Default BigDecimal conversion: precision=2, scale=0\n    assertEquals(new DecimalType(2, 0), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralCaseInsensitiveColumnLookup() {\n    // Filter uses \"PRICE\" but schema has \"price\" → should still widen\n    EqualTo filter = new EqualTo(\"PRICE\", new BigDecimal(\"100.00\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"Case-insensitive column lookup should match\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralColumnNotInSchema() {\n    // Column \"unknown\" not in schema → use default conversion\n    EqualTo filter = new EqualTo(\"unknown\", new BigDecimal(\"100.00\"));\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"Unknown column should fall back to default conversion\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    assertEquals(new DecimalType(5, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalPartialPushDownInAndFilter() {\n    // AND(price > 99.999, price < 200.00) where left has scale=3 exceeding column's scale=2.\n    // Only the right side should be pushed down (partial pushdown).\n    GreaterThan left = new GreaterThan(\"price\", new BigDecimal(\"99.999\"));\n    LessThan right = new LessThan(\"price\", new BigDecimal(\"200.00\"));\n    org.apache.spark.sql.sources.And andFilter = new org.apache.spark.sql.sources.And(left, right);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"AND filter should partially push down the valid side\");\n    assertTrue(result.isPartial(), \"Result should be marked as partial conversion\");\n    // Only the right side (price < 200.00) should be pushed, not a compound AND\n    Predicate pred = result.get();\n    assertNotEquals(\n        \"AND\",\n        pred.getName(),\n        \"Left side (price > 99.999) should be dropped, not pushed as compound AND\");\n    assertEquals(\"<\", pred.getName());\n    Literal literal = (Literal) pred.getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n    assertEquals(new BigDecimal(\"200.00\"), literal.getValue());\n  }\n\n  @Test\n  public void testDecimalLiteralInCompoundFilter() {\n    // AND(price >= 100.00, price <= 200.00) with Decimal(7,2) column\n    GreaterThanOrEqual left = new GreaterThanOrEqual(\"price\", new BigDecimal(\"100.00\"));\n    LessThanOrEqual right = new LessThanOrEqual(\"price\", new BigDecimal(\"200.00\"));\n    org.apache.spark.sql.sources.And andFilter = new org.apache.spark.sql.sources.And(left, right);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(andFilter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"AND filter with decimal operands should be converted\");\n    assertFalse(result.isPartial());\n    // Verify both literals are widened to Decimal(7,2)\n    io.delta.kernel.expressions.And andPred = (io.delta.kernel.expressions.And) result.get();\n    Predicate leftPred = (Predicate) andPred.getChildren().get(0);\n    Predicate rightPred = (Predicate) andPred.getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), ((Literal) leftPred.getChildren().get(1)).getDataType());\n    assertEquals(new DecimalType(7, 2), ((Literal) rightPred.getChildren().get(1)).getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralInOrFilter() {\n    // OR(price >= 100.00, price >= 200.00) with Decimal(7,2) column\n    GreaterThanOrEqual left = new GreaterThanOrEqual(\"price\", new BigDecimal(\"100.00\"));\n    GreaterThanOrEqual right = new GreaterThanOrEqual(\"price\", new BigDecimal(\"200.00\"));\n    org.apache.spark.sql.sources.Or orFilter = new org.apache.spark.sql.sources.Or(left, right);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(orFilter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"OR filter with decimal operands should be converted\");\n  }\n\n  @Test\n  public void testDecimalLiteralInNotFilter() {\n    // NOT(price = 100.00) with Decimal(7,2) column\n    EqualTo eq = new EqualTo(\"price\", new BigDecimal(\"100.00\"));\n    Not notFilter = new Not(eq);\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(notFilter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"NOT filter with decimal operand should be converted\");\n    Predicate notPred = result.get();\n    assertEquals(\"NOT\", notPred.getName());\n    Predicate innerPred = (Predicate) notPred.getChildren().get(0);\n    Literal literal = (Literal) innerPred.getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralWithEqualNullSafe() {\n    // EqualNullSafe(price, 100.00) with Decimal(7,2) column\n    EqualNullSafe filter = new EqualNullSafe(\"price\", new BigDecimal(\"100.00\"));\n\n    ExpressionUtils.ConvertedPredicate result =\n        ExpressionUtils.convertSparkFilterToKernelPredicate(filter, decimalSchema);\n\n    assertTrue(result.isPresent(), \"EqualNullSafe with decimal should be converted\");\n    Literal literal = (Literal) result.get().getChildren().get(1);\n    assertEquals(new DecimalType(7, 2), literal.getDataType());\n  }\n\n  @Test\n  public void testDecimalLiteralAllComparisonOperators() {\n    // Test all comparison operators widen decimals correctly\n    BigDecimal value = new BigDecimal(\"50.00\"); // Decimal(4,2) → should widen to Decimal(7,2)\n    Filter[] filters =\n        new Filter[] {\n          new EqualTo(\"price\", value),\n          new GreaterThan(\"price\", value),\n          new GreaterThanOrEqual(\"price\", value),\n          new LessThan(\"price\", value),\n          new LessThanOrEqual(\"price\", value),\n        };\n    String[] expectedOps = new String[] {\"=\", \">\", \">=\", \"<\", \"<=\"};\n\n    for (int i = 0; i < filters.length; i++) {\n      ExpressionUtils.ConvertedPredicate result =\n          ExpressionUtils.convertSparkFilterToKernelPredicate(filters[i], decimalSchema);\n      assertTrue(result.isPresent(), expectedOps[i] + \" filter should be converted\");\n      Literal literal = (Literal) result.get().getChildren().get(1);\n      assertEquals(\n          new DecimalType(7, 2),\n          literal.getDataType(),\n          expectedOps[i] + \" should widen decimal to column type\");\n    }\n  }\n\n  @Test\n  public void testClassifyFilterWithNullSchemaMatchesTwoArgOverload() {\n    // Directly validates that classifyFilter(filter, partitionColumns, null) produces\n    // the same result as the 2-arg classifyFilter(filter, partitionColumns).\n    Set<String> partitionColumns = new HashSet<>();\n    partitionColumns.add(\"dep_id\");\n\n    EqualTo filter = new EqualTo(\"price\", new BigDecimal(\"100.00\"));\n\n    ExpressionUtils.FilterClassificationResult resultTwoArg =\n        ExpressionUtils.classifyFilter(filter, partitionColumns);\n    ExpressionUtils.FilterClassificationResult resultThreeArg =\n        ExpressionUtils.classifyFilter(filter, partitionColumns, null);\n\n    assertEquals(resultTwoArg.isKernelSupported, resultThreeArg.isKernelSupported);\n    assertEquals(resultTwoArg.isPartialConversion, resultThreeArg.isPartialConversion);\n    assertEquals(resultTwoArg.isDataFilter, resultThreeArg.isDataFilter);\n    assertEquals(resultTwoArg.kernelPredicate, resultThreeArg.kernelPredicate);\n  }\n\n  @Test\n  public void testDsv2PredicateToCatalystExpression_ColumnNotFound() {\n    NamedReference invalidRef = FieldReference.apply(\"nonexistent_column\");\n    LiteralValue<Integer> value =\n        LiteralValue.apply(42, org.apache.spark.sql.types.DataTypes.IntegerType);\n    org.apache.spark.sql.connector.expressions.filter.Predicate predicate =\n        new org.apache.spark.sql.connector.expressions.filter.Predicate(\n            \"=\", new org.apache.spark.sql.connector.expressions.Expression[] {invalidRef, value});\n\n    Optional<org.apache.spark.sql.catalyst.expressions.Expression> result =\n        ExpressionUtils.dsv2PredicateToCatalystExpression(predicate, testSchema);\n\n    assertFalse(\n        result.isPresent(), \"Should return empty Optional when column is not found in schema\");\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/PartitionUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static io.delta.kernel.internal.util.VectorUtils.stringStringMapValue;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.delta.kernel.Scan;\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.Table;\nimport io.delta.kernel.data.FilteredColumnarBatch;\nimport io.delta.kernel.data.MapValue;\nimport io.delta.kernel.data.Row;\nimport io.delta.kernel.internal.actions.AddFile;\nimport io.delta.kernel.utils.CloseableIterator;\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport java.time.ZoneId;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.spark.sql.catalyst.InternalRow;\nimport org.apache.spark.sql.connector.read.PartitionReaderFactory;\nimport org.apache.spark.sql.execution.datasources.PartitionedFile;\nimport org.apache.spark.sql.internal.SQLConf;\nimport org.apache.spark.sql.sources.Filter;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructField;\nimport org.apache.spark.sql.types.StructType;\nimport org.junit.jupiter.api.Test;\nimport scala.collection.immutable.Map$;\n\npublic class PartitionUtilsTest extends DeltaV2TestBase {\n\n  private static final long MB = 1024 * 1024;\n\n  @Test\n  public void testGetPartitionRow_FieldOrdering() {\n    // Schema defines order: year, month, day\n    StructType partitionSchema =\n        new StructType(\n            new StructField[] {\n              DataTypes.createStructField(\"year\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"month\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"day\", DataTypes.IntegerType, true)\n            });\n\n    // map value has different order: day, year, month\n    Map<String, String> partitionValues = new HashMap<>();\n    partitionValues.put(\"day\", \"25\");\n    partitionValues.put(\"year\", \"2024\");\n    partitionValues.put(\"month\", \"11\");\n\n    MapValue mapValue = stringStringMapValue(partitionValues);\n    InternalRow row = PartitionUtils.getPartitionRow(mapValue, partitionSchema, ZoneId.of(\"UTC\"));\n\n    // verify order is schema order: year, month, day\n    assertEquals(2024, row.getInt(0));\n    assertEquals(11, row.getInt(1));\n    assertEquals(25, row.getInt(2));\n  }\n\n  @Test\n  public void testGetPartitionRow_SizeMismatchExtraKeys() {\n    StructType partitionSchema =\n        new StructType(\n            new StructField[] {\n              DataTypes.createStructField(\"year\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"month\", DataTypes.IntegerType, true)\n            });\n\n    Map<String, String> partitionValues = new HashMap<>();\n    partitionValues.put(\"year\", \"2024\");\n    partitionValues.put(\"month\", \"11\");\n    partitionValues.put(\"day\", \"25\");\n    partitionValues.put(\"hour\", \"10\");\n\n    MapValue mapValue = stringStringMapValue(partitionValues);\n\n    assertThrows(\n        AssertionError.class,\n        () -> PartitionUtils.getPartitionRow(mapValue, partitionSchema, ZoneId.of(\"UTC\")));\n  }\n\n  @Test\n  public void testGetPartitionRow_SizeMismatchMissingKeys() {\n    StructType partitionSchema =\n        new StructType(\n            new StructField[] {\n              DataTypes.createStructField(\"year\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"month\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"day\", DataTypes.IntegerType, true)\n            });\n\n    Map<String, String> partitionValues = new HashMap<>();\n    partitionValues.put(\"year\", \"2024\");\n    partitionValues.put(\"month\", \"11\");\n\n    MapValue mapValue = stringStringMapValue(partitionValues);\n\n    assertThrows(\n        AssertionError.class,\n        () -> PartitionUtils.getPartitionRow(mapValue, partitionSchema, ZoneId.of(\"UTC\")));\n  }\n\n  @Test\n  public void testCreateDeltaParquetReaderFactory_Basic() {\n    String tablePath = createTestTable(\"test_delta_reader_factory_\" + System.nanoTime(), true);\n\n    Table table = Table.forPath(defaultEngine, tablePath);\n    Snapshot snapshot = table.getLatestSnapshot(defaultEngine);\n\n    StructType dataSchema =\n        new StructType(\n            new StructField[] {\n              DataTypes.createStructField(\"id\", DataTypes.LongType, true),\n            });\n    StructType partitionSchema =\n        new StructType(\n            new StructField[] {DataTypes.createStructField(\"part\", DataTypes.StringType, true)});\n    StructType readDataSchema = dataSchema;\n    Filter[] filters = new Filter[0];\n    scala.collection.immutable.Map<String, String> options = Map$.MODULE$.empty();\n    Configuration hadoopConf = new Configuration();\n    SQLConf sqlConf = SQLConf.get();\n\n    PartitionReaderFactory factory =\n        PartitionUtils.createDeltaParquetReaderFactory(\n            snapshot,\n            dataSchema,\n            partitionSchema,\n            readDataSchema,\n            filters,\n            options,\n            hadoopConf,\n            sqlConf);\n\n    assertNotNull(factory, \"PartitionReaderFactory should not be null\");\n  }\n\n  @Test\n  public void testCalculateMaxSplitBytes_Basic() {\n    SQLConf sqlConf = SQLConf.get();\n    long minPartitionNum = 4;\n    sqlConf.setConfString(\"spark.sql.files.minPartitionNum\", String.valueOf(minPartitionNum));\n\n    long totalBytes = 100 * MB;\n    int fileCount = 10;\n\n    long result = PartitionUtils.calculateMaxSplitBytes(spark, totalBytes, fileCount, sqlConf);\n    long openCostInBytes = sqlConf.filesOpenCostInBytes();\n    long maxPartitionBytes = sqlConf.filesMaxPartitionBytes();\n\n    long calculatedTotalBytes = totalBytes + (long) fileCount * openCostInBytes;\n    assertEquals(calculatedTotalBytes / minPartitionNum, result);\n  }\n\n  @Test\n  public void testCalculateMaxSplitBytes_BoundaryConditions() {\n    SQLConf sqlConf = SQLConf.get();\n    // Set minPartitionNum=1 for predictable calculations\n    sqlConf.setConfString(\"spark.sql.files.minPartitionNum\", \"1\");\n    long openCostInBytes = sqlConf.filesOpenCostInBytes();\n    long maxPartitionBytes = sqlConf.filesMaxPartitionBytes();\n\n    // Zero files and bytes\n    long result1 = PartitionUtils.calculateMaxSplitBytes(spark, 0L, 0, sqlConf);\n    assertEquals(openCostInBytes, result1);\n\n    // Single large file (exceeds maxPartitionBytes)\n    long result2 = PartitionUtils.calculateMaxSplitBytes(spark, 1000 * MB, 1, sqlConf);\n    assertEquals(maxPartitionBytes, result2);\n\n    // Very small totalBytes\n    long result3 = PartitionUtils.calculateMaxSplitBytes(spark, 1024L, 1, sqlConf);\n    long expected3 = 1024L + openCostInBytes;\n    assertEquals(expected3, result3);\n\n    // Many small files\n    long result4 = PartitionUtils.calculateMaxSplitBytes(spark, 1 * MB, 1000, sqlConf);\n    assertEquals(maxPartitionBytes, result4);\n  }\n\n  @Test\n  public void testCalculateMaxSplitBytes_UndefinedMinPartitionNum() {\n    SQLConf sqlConf = SQLConf.get();\n    // Ensure filesMinPartitionNum is undefined\n    if (sqlConf.filesMinPartitionNum().isDefined()) {\n      sqlConf.unsetConf(\"spark.sql.files.minPartitionNum\");\n    }\n\n    long totalBytes = 200 * MB;\n    int fileCount = 10;\n\n    long result = PartitionUtils.calculateMaxSplitBytes(spark, totalBytes, fileCount, sqlConf);\n\n    // Verify the result is still valid\n    assertTrue(result > 0);\n    assertTrue(result >= sqlConf.filesOpenCostInBytes());\n    assertTrue(result <= sqlConf.filesMaxPartitionBytes());\n    long calculatedTotalBytes = totalBytes + (long) fileCount * sqlConf.filesOpenCostInBytes();\n    assertTrue(result <= calculatedTotalBytes);\n  }\n\n  @Test\n  public void testBuildPartitionedFile() throws Exception {\n    String tablePath = createTestTable(\"test_build_partitioned_file_\" + System.nanoTime(), true);\n\n    // Get an AddFile from the table\n    Table table = Table.forPath(defaultEngine, tablePath);\n    Scan scan = table.getLatestSnapshot(defaultEngine).getScanBuilder().build();\n    FilteredColumnarBatch batch = scan.getScanFiles(defaultEngine).next();\n    CloseableIterator<Row> rows = batch.getRows();\n    AddFile addFile = new AddFile(rows.next().getStruct(0));\n    rows.close();\n\n    // Build PartitionedFile\n    StructType partitionSchema =\n        new StructType(\n            new StructField[] {DataTypes.createStructField(\"part\", DataTypes.StringType, true)});\n    String normalizedTablePath = tablePath.endsWith(\"/\") ? tablePath : tablePath + \"/\";\n    PartitionedFile partitionedFile =\n        PartitionUtils.buildPartitionedFile(\n            addFile, partitionSchema, normalizedTablePath, ZoneId.of(\"UTC\"));\n\n    assertNotNull(partitionedFile);\n    assertEquals(addFile.getSize(), partitionedFile.fileSize());\n    assertEquals(1, partitionedFile.partitionValues().numFields());\n  }\n\n  /** Helper to create a test Delta table. */\n  private String createTestTable(String tableName, boolean partitioned) {\n    String tablePath = getTempTablePath(tableName);\n    if (partitioned) {\n      spark\n          .range(10)\n          .selectExpr(\"id\", \"cast(id % 3 as string) as part\")\n          .write()\n          .format(\"delta\")\n          .partitionBy(\"part\")\n          .save(tablePath);\n    } else {\n      spark.range(10).write().format(\"delta\").save(tablePath);\n    }\n    return tablePath;\n  }\n\n  private String getTempTablePath(String tableName) {\n    return java.nio.file.Paths.get(System.getProperty(\"java.io.tmpdir\"), \"delta-test-\" + tableName)\n        .toString();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/RowTrackingUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport io.delta.kernel.data.ArrayValue;\nimport io.delta.kernel.data.ColumnVector;\nimport io.delta.kernel.defaults.internal.json.JsonUtils;\nimport io.delta.kernel.internal.TableConfig;\nimport io.delta.kernel.internal.actions.Format;\nimport io.delta.kernel.internal.actions.Metadata;\nimport io.delta.kernel.internal.actions.Protocol;\nimport io.delta.kernel.internal.actions.SingleAction;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.StructType;\nimport java.util.*;\nimport java.util.stream.Stream;\nimport org.apache.spark.sql.catalyst.expressions.FileSourceConstantMetadataStructField;\nimport org.apache.spark.sql.delta.DeltaIllegalStateException;\nimport org.apache.spark.sql.delta.RowTracking;\nimport org.apache.spark.sql.delta.actions.Action;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.MetadataBuilder;\nimport org.apache.spark.sql.types.StructField;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport scala.collection.JavaConverters;\n\npublic class RowTrackingUtilsTest {\n\n  @Test\n  public void testIsEnabled_NotEnabledInMetadata_ReturnsFalse() {\n    Protocol protocol = createProtocol(3, 7, Set.of(\"rowTracking\"), Set.of(\"rowTracking\"));\n    Metadata metadata = createMetadata(Collections.emptyMap());\n    assertFalse(io.delta.kernel.internal.rowtracking.RowTracking.isEnabled(protocol, metadata));\n  }\n\n  @Test\n  public void testIsEnabled_SupportedAndEnabled_ReturnsTrue() {\n    Protocol protocol = createProtocol(3, 7, Set.of(\"rowTracking\"), Set.of(\"rowTracking\"));\n    Map<String, String> config = new HashMap<>();\n    config.put(\"delta.enableRowTracking\", \"true\");\n    Metadata metadata = createMetadata(config);\n    assertTrue(io.delta.kernel.internal.rowtracking.RowTracking.isEnabled(protocol, metadata));\n  }\n\n  @Test\n  public void testIsEnabled_EnabledButNotSupported_ThrowsError() {\n    Protocol protocol = createProtocol(1, 1, Collections.emptySet(), Collections.emptySet());\n    Map<String, String> config = new HashMap<>();\n    config.put(\"delta.enableRowTracking\", \"true\");\n    Metadata metadata = createMetadata(config);\n\n    IllegalStateException exception =\n        assertThrows(\n            IllegalStateException.class,\n            () -> io.delta.kernel.internal.rowtracking.RowTracking.isEnabled(protocol, metadata));\n    assertTrue(\n        exception\n            .getMessage()\n            .contains(\"doesn't support table feature 'delta.feature.rowTracking'\"));\n  }\n\n  @Test\n  public void testCreateMetadataStructFields_NotEnabled_ReturnsEmptyList() {\n    Protocol protocol = createProtocol(3, 7, Set.of(\"rowTracking\"), Set.of(\"rowTracking\"));\n    Metadata metadata = createMetadata(Collections.emptyMap());\n    List<StructField> fields =\n        RowTrackingUtils.createMetadataStructFields(protocol, metadata, false, false);\n    assertTrue(fields.isEmpty());\n  }\n\n  @Test\n  public void testCreateMetadataStructFields_MissingMaterializedRowId_ThrowsException() {\n    Protocol protocol = createProtocol(3, 7, Set.of(\"rowTracking\"), Set.of(\"rowTracking\"));\n    Map<String, String> config = new HashMap<>();\n    config.put(\"delta.enableRowTracking\", \"true\");\n    // Missing MATERIALIZED_ROW_ID_COLUMN_NAME\n    config.put(TableConfig.MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME.getKey(), \"__row_version\");\n    Metadata metadata = createMetadata(config);\n\n    DeltaIllegalStateException exception =\n        assertThrows(\n            DeltaIllegalStateException.class,\n            () -> RowTrackingUtils.createMetadataStructFields(protocol, metadata, false, false));\n    assertTrue(exception.getMessage().contains(\"Row ID\"));\n  }\n\n  @Test\n  public void testCreateMetadataStructFields_MissingMaterializedRowCommitVersion_ThrowsException() {\n    Protocol protocol = createProtocol(3, 7, Set.of(\"rowTracking\"), Set.of(\"rowTracking\"));\n    Map<String, String> config = new HashMap<>();\n    config.put(\"delta.enableRowTracking\", \"true\");\n    config.put(TableConfig.MATERIALIZED_ROW_ID_COLUMN_NAME.getKey(), \"__row_id\");\n    // Missing MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME\n    Metadata metadata = createMetadata(config);\n\n    DeltaIllegalStateException exception =\n        assertThrows(\n            DeltaIllegalStateException.class,\n            () -> RowTrackingUtils.createMetadataStructFields(protocol, metadata, false, false));\n    assertTrue(exception.getMessage().contains(\"Row Commit Version\"));\n  }\n\n  private static Stream<Arguments> createMetadataStructFieldsTestProvider() {\n    // Note: withMaterializedColumns must be true when row tracking is enabled\n    // because materialized column names are required\n    return Stream.of(\n        // nullableConstant, nullableGenerated\n        Arguments.of(false, false),\n        Arguments.of(false, true),\n        Arguments.of(true, false),\n        Arguments.of(true, true));\n  }\n\n  @ParameterizedTest\n  @MethodSource(\"createMetadataStructFieldsTestProvider\")\n  public void testCreateMetadataStructFields(boolean nullableConstant, boolean nullableGenerated) {\n    // Create Kernel Protocol and Metadata\n    // Note: materialized columns are always configured when row tracking is enabled\n    Protocol kernelProtocol = createProtocol(3, 7, Set.of(\"rowTracking\"), Set.of(\"rowTracking\"));\n    Map<String, String> config = new HashMap<>();\n    config.put(\"delta.enableRowTracking\", \"true\");\n    config.put(TableConfig.MATERIALIZED_ROW_ID_COLUMN_NAME.getKey(), \"__row_id\");\n    config.put(TableConfig.MATERIALIZED_ROW_COMMIT_VERSION_COLUMN_NAME.getKey(), \"__row_version\");\n    Metadata kernelMetadata = createMetadata(config);\n\n    // Get actual result from Kernel-Spark API\n    List<StructField> actualFields =\n        RowTrackingUtils.createMetadataStructFields(\n            kernelProtocol, kernelMetadata, nullableConstant, nullableGenerated);\n\n    // Build expected fields\n    List<StructField> expectedFields = new ArrayList<>();\n    // row_id (generated field)\n    expectedFields.add(\n        new StructField(\n            \"row_id\",\n            DataTypes.LongType,\n            nullableGenerated,\n            new MetadataBuilder()\n                .withMetadata(\n                    org.apache.spark.sql.catalyst.expressions.FileSourceGeneratedMetadataStructField\n                        .metadata(\"row_id\", \"__row_id\"))\n                .putBoolean(\"__row_id_metadata_col\", true)\n                .build()));\n    // base_row_id (constant field)\n    expectedFields.add(\n        new StructField(\n            \"base_row_id\",\n            DataTypes.LongType,\n            nullableConstant,\n            new MetadataBuilder()\n                .withMetadata(FileSourceConstantMetadataStructField.metadata(\"base_row_id\"))\n                .putBoolean(\"__base_row_id_metadata_col\", true)\n                .build()));\n    // default_row_commit_version (constant field)\n    expectedFields.add(\n        new StructField(\n            \"default_row_commit_version\",\n            DataTypes.LongType,\n            nullableConstant,\n            new MetadataBuilder()\n                .withMetadata(\n                    FileSourceConstantMetadataStructField.metadata(\"default_row_commit_version\"))\n                .putBoolean(\"__default_row_version_metadata_col\", true)\n                .build()));\n    // row_commit_version (generated field)\n    expectedFields.add(\n        new StructField(\n            \"row_commit_version\",\n            DataTypes.LongType,\n            nullableGenerated,\n            new MetadataBuilder()\n                .withMetadata(\n                    org.apache.spark.sql.catalyst.expressions.FileSourceGeneratedMetadataStructField\n                        .metadata(\"row_commit_version\", \"__row_version\"))\n                .putBoolean(\"__row_commit_version_metadata_col\", true)\n                .build()));\n\n    String protocolJson =\n        JsonUtils.rowToJson(SingleAction.createProtocolSingleAction(kernelProtocol.toRow()));\n    org.apache.spark.sql.delta.actions.Protocol sparkV1Protocol =\n        Action.fromJson(protocolJson).wrap().protocol();\n    String metadataJson =\n        JsonUtils.rowToJson(SingleAction.createMetadataSingleAction(kernelMetadata.toRow()));\n    org.apache.spark.sql.delta.actions.Metadata sparkV1Metadata =\n        Action.fromJson(metadataJson).wrap().metaData();\n    scala.collection.Iterable<StructField> sparkV1FieldsIterable =\n        RowTracking.createMetadataStructFields(\n            sparkV1Protocol, sparkV1Metadata, nullableConstant, nullableGenerated);\n    List<StructField> v1Fields =\n        new ArrayList<>(JavaConverters.asJavaCollection(sparkV1FieldsIterable));\n\n    assertEquals(expectedFields, actualFields);\n    // Ensure both delta implementation return same result.\n    assertEquals(v1Fields, actualFields);\n  }\n\n  private Protocol createProtocol(\n      int minReaderVersion,\n      int minWriterVersion,\n      Set<String> readerFeatures,\n      Set<String> writerFeatures) {\n    return new Protocol(minReaderVersion, minWriterVersion, readerFeatures, writerFeatures);\n  }\n\n  private Metadata createMetadata(Map<String, String> configuration) {\n    StructType schema = new StructType().add(\"id\", IntegerType.INTEGER);\n    ArrayValue emptyPartitionColumns =\n        new ArrayValue() {\n          @Override\n          public int getSize() {\n            return 0;\n          }\n\n          @Override\n          public ColumnVector getElements() {\n            return null;\n          }\n        };\n    return new Metadata(\n        \"id\",\n        Optional.empty() /* name */,\n        Optional.empty() /* description */,\n        new Format(),\n        schema.toJson(),\n        schema,\n        emptyPartitionColumns,\n        Optional.empty() /* createdTime */,\n        io.delta.kernel.internal.util.VectorUtils.stringStringMapValue(configuration));\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/ScalaUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\n\nclass ScalaUtilsTest {\n\n  @Test\n  void testToJavaMap_NullInput_ReturnsNull() {\n    assertNull(ScalaUtils.toJavaMap(null), \"Null scala maps should return null\");\n  }\n\n  @Test\n  void testToJavaMap_EmptyInput_ReturnsEmptyMap() {\n    scala.collection.immutable.Map<String, String> emptyScalaMap =\n        ScalaUtils.toScalaMap(Collections.emptyMap());\n\n    Map<String, String> javaMap = ScalaUtils.toJavaMap(emptyScalaMap);\n\n    assertTrue(javaMap.isEmpty(), \"Empty scala maps should convert to empty java maps\");\n  }\n\n  @Test\n  void testToJavaMap_PopulatedInput_PreservesEntries() {\n    scala.collection.immutable.Map<String, String> scalaMap =\n        ScalaUtils.toScalaMap(Map.of(\"foo\", \"bar\"));\n\n    Map<String, String> javaMap = ScalaUtils.toJavaMap(scalaMap);\n\n    assertEquals(Map.of(\"foo\", \"bar\"), javaMap, \"Scala map entries should be preserved\");\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/SchemaUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport io.delta.kernel.types.ArrayType;\nimport io.delta.kernel.types.BinaryType;\nimport io.delta.kernel.types.BooleanType;\nimport io.delta.kernel.types.ByteType;\nimport io.delta.kernel.types.DataType;\nimport io.delta.kernel.types.DateType;\nimport io.delta.kernel.types.DecimalType;\nimport io.delta.kernel.types.DoubleType;\nimport io.delta.kernel.types.FieldMetadata;\nimport io.delta.kernel.types.FloatType;\nimport io.delta.kernel.types.IntegerType;\nimport io.delta.kernel.types.LongType;\nimport io.delta.kernel.types.MapType;\nimport io.delta.kernel.types.ShortType;\nimport io.delta.kernel.types.StringType;\nimport io.delta.kernel.types.StructType;\nimport io.delta.kernel.types.TimestampNTZType;\nimport io.delta.kernel.types.TimestampType;\nimport io.delta.kernel.types.VariantType;\nimport java.util.stream.Stream;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.Metadata;\nimport org.apache.spark.sql.types.MetadataBuilder;\nimport org.apache.spark.sql.types.StructField;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/** Tests for {@link SchemaUtils}. */\npublic class SchemaUtilsTest {\n\n  @Test\n  public void testPrimitiveTypes() {\n    checkConversion(DataTypes.StringType, StringType.STRING);\n    checkConversion(DataTypes.BooleanType, BooleanType.BOOLEAN);\n    checkConversion(DataTypes.IntegerType, IntegerType.INTEGER);\n    checkConversion(DataTypes.LongType, LongType.LONG);\n    checkConversion(DataTypes.BinaryType, BinaryType.BINARY);\n    checkConversion(DataTypes.ByteType, ByteType.BYTE);\n    checkConversion(DataTypes.DateType, DateType.DATE);\n    checkConversion(DataTypes.createDecimalType(10, 2), new DecimalType(10, 2));\n    checkConversion(DataTypes.DoubleType, DoubleType.DOUBLE);\n    checkConversion(DataTypes.FloatType, FloatType.FLOAT);\n    checkConversion(DataTypes.ShortType, ShortType.SHORT);\n    checkConversion(DataTypes.TimestampType, TimestampType.TIMESTAMP);\n    checkConversion(DataTypes.TimestampNTZType, TimestampNTZType.TIMESTAMP_NTZ);\n    checkConversion(DataTypes.VariantType, VariantType.VARIANT);\n  }\n\n  @Test\n  public void testArrayType() {\n    checkConversion(\n        DataTypes.createArrayType(DataTypes.IntegerType, true /* containsNull */),\n        new ArrayType(IntegerType.INTEGER, true /* containsNull */));\n    checkConversion(\n        DataTypes.createArrayType(DataTypes.StringType, false /* containsNull */),\n        new ArrayType(StringType.STRING, false /* containsNull */));\n  }\n\n  @Test\n  public void testMapType() {\n    checkConversion(\n        DataTypes.createMapType(\n            DataTypes.StringType, DataTypes.IntegerType, true /* valueContainsNull */),\n        new MapType(StringType.STRING, IntegerType.INTEGER, true /* valueContainsNull */));\n    checkConversion(\n        DataTypes.createMapType(\n            DataTypes.LongType, DataTypes.BooleanType, false /* valueContainsNull */),\n        new MapType(LongType.LONG, BooleanType.BOOLEAN, false /* valueContainsNull */));\n  }\n\n  @Test\n  public void testStructType() {\n    org.apache.spark.sql.types.StructType sparkStruct =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\"a\", DataTypes.IntegerType, true /* nullable */),\n              DataTypes.createStructField(\"b\", DataTypes.StringType, false /* nullable */)\n            });\n    StructType kernelStruct =\n        new StructType()\n            .add(\"a\", IntegerType.INTEGER, true /* nullable */)\n            .add(\"b\", StringType.STRING, false /* nullable */);\n\n    checkConversion(sparkStruct, kernelStruct);\n  }\n\n  @Test\n  public void testNestedTypes() {\n    org.apache.spark.sql.types.StructType sparkStruct =\n        DataTypes.createStructType(\n            new StructField[] {\n              DataTypes.createStructField(\n                  \"a\",\n                  DataTypes.createArrayType(DataTypes.IntegerType, true /* containsNull */),\n                  true /* nullable */),\n              DataTypes.createStructField(\n                  \"b\",\n                  DataTypes.createMapType(\n                      DataTypes.StringType, DataTypes.BooleanType, false /* valueContainsNull */),\n                  false /* nullable */)\n            });\n    StructType kernelStruct =\n        new StructType()\n            .add(\n                \"a\",\n                new ArrayType(IntegerType.INTEGER, true /* containsNull */),\n                true /* nullable */)\n            .add(\n                \"b\",\n                new MapType(StringType.STRING, BooleanType.BOOLEAN, false /* valueContainsNull */),\n                false /* nullable */);\n\n    checkConversion(sparkStruct, kernelStruct);\n  }\n\n  @Test\n  public void testConvertSparkSchemaToKernelSchema() {\n    org.apache.spark.sql.types.StructType sparkSchema =\n        DataTypes.createStructType(\n            new org.apache.spark.sql.types.StructField[] {\n              DataTypes.createStructField(\"a\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"b\", DataTypes.StringType, false)\n            });\n\n    StructType expectedKernelSchema =\n        new StructType().add(\"a\", IntegerType.INTEGER, true).add(\"b\", StringType.STRING, false);\n\n    StructType actualKernelSchema = SchemaUtils.convertSparkSchemaToKernelSchema(sparkSchema);\n    assertEquals(expectedKernelSchema, actualKernelSchema);\n  }\n\n  @Test\n  public void testConvertKernelSchemaToSparkSchema() {\n    StructType kernelSchema =\n        new StructType().add(\"a\", IntegerType.INTEGER, true).add(\"b\", StringType.STRING, false);\n\n    org.apache.spark.sql.types.StructType expectedSparkSchema =\n        DataTypes.createStructType(\n            new org.apache.spark.sql.types.StructField[] {\n              DataTypes.createStructField(\"a\", DataTypes.IntegerType, true),\n              DataTypes.createStructField(\"b\", DataTypes.StringType, false)\n            });\n\n    org.apache.spark.sql.types.StructType actualSparkSchema =\n        SchemaUtils.convertKernelSchemaToSparkSchema(kernelSchema);\n    assertEquals(expectedSparkSchema, actualSparkSchema);\n  }\n\n  static Stream<Arguments> nullInPrimitiveArraysProvider() {\n    return Stream.of(\n        Arguments.of(\n            \"Long\",\n            FieldMetadata.builder().putLongArray(\"ids\", new Long[] {1L, null, 3L}).build(),\n            1),\n        Arguments.of(\n            \"Double\",\n            FieldMetadata.builder().putDoubleArray(\"scores\", new Double[] {1.1, 2.2, null}).build(),\n            2),\n        Arguments.of(\n            \"Boolean\",\n            FieldMetadata.builder()\n                .putBooleanArray(\"flags\", new Boolean[] {true, null, false})\n                .build(),\n            1));\n  }\n\n  @ParameterizedTest(name = \"{0} array with null at index {2}\")\n  @MethodSource(\"nullInPrimitiveArraysProvider\")\n  public void testNullInPrimitiveArrays(\n      String typeName, FieldMetadata kernelMetadata, int expectedNullIndex) {\n    Exception ex =\n        assertThrows(\n            Exception.class,\n            () -> SchemaUtils.convertKernelFieldMetadataToSparkMetadata(kernelMetadata));\n    assertTrue(ex.getMessage().contains(\"Null element at index \" + expectedNullIndex));\n  }\n\n  @Test\n  public void testSchemaRoundTripWithFieldMetadata() {\n    // Schema with field metadata (e.g., column mapping)\n    org.apache.spark.sql.types.StructType sparkSchema =\n        new org.apache.spark.sql.types.StructType()\n            .add(\n                \"user_id\",\n                DataTypes.IntegerType,\n                true,\n                new MetadataBuilder()\n                    .putLong(\"delta.columnMapping.id\", 123L)\n                    .putString(\"delta.columnMapping.physicalName\", \"col-abc-123\")\n                    .build());\n\n    StructType expectedKernelSchema =\n        new StructType()\n            .add(\n                \"user_id\",\n                IntegerType.INTEGER,\n                true,\n                FieldMetadata.builder()\n                    .putLong(\"delta.columnMapping.id\", 123L)\n                    .putString(\"delta.columnMapping.physicalName\", \"col-abc-123\")\n                    .build());\n\n    // Verify Spark → Kernel conversion\n    StructType actualKernelSchema = SchemaUtils.convertSparkSchemaToKernelSchema(sparkSchema);\n    assertEquals(expectedKernelSchema, actualKernelSchema);\n\n    // Verify Kernel → Spark conversion\n    org.apache.spark.sql.types.StructType sparkSchema2 =\n        SchemaUtils.convertKernelSchemaToSparkSchema(actualKernelSchema);\n    assertEquals(sparkSchema, sparkSchema2);\n  }\n\n  private void checkConversion(\n      org.apache.spark.sql.types.DataType sparkDataType, DataType kernelDataType) {\n    DataType toKernel = SchemaUtils.convertSparkDataTypeToKernelDataType(sparkDataType);\n    assertEquals(kernelDataType, toKernel);\n    org.apache.spark.sql.types.DataType toSpark =\n        SchemaUtils.convertKernelDataTypeToSparkDataType(kernelDataType);\n    assertEquals(sparkDataType, toSpark);\n  }\n\n  ////////////////////////////////\n  // Field Metadata Tests       //\n  ////////////////////////////////\n\n  static Stream<Arguments> metadataTypesProvider() {\n    return Stream.of(\n        // Empty\n        Arguments.of(\"Empty\", Metadata.empty(), FieldMetadata.empty()),\n        // Primitives\n        Arguments.of(\n            \"Long\",\n            new MetadataBuilder().putLong(\"id\", 123L).build(),\n            FieldMetadata.builder().putLong(\"id\", 123L).build()),\n        Arguments.of(\n            \"Double\",\n            new MetadataBuilder().putDouble(\"score\", 3.14).build(),\n            FieldMetadata.builder().putDouble(\"score\", 3.14).build()),\n        Arguments.of(\n            \"Boolean\",\n            new MetadataBuilder().putBoolean(\"flag\", true).build(),\n            FieldMetadata.builder().putBoolean(\"flag\", true).build()),\n        Arguments.of(\n            \"String\",\n            new MetadataBuilder().putString(\"name\", \"test\").build(),\n            FieldMetadata.builder().putString(\"name\", \"test\").build()),\n        Arguments.of(\n            \"Null\",\n            new MetadataBuilder().putNull(\"empty\").build(),\n            FieldMetadata.builder().putNull(\"empty\").build()),\n        // Arrays\n        Arguments.of(\n            \"LongArray\",\n            new MetadataBuilder().putLongArray(\"ids\", new long[] {1L, 2L, 3L}).build(),\n            FieldMetadata.builder().putLongArray(\"ids\", new Long[] {1L, 2L, 3L}).build()),\n        Arguments.of(\n            \"DoubleArray\",\n            new MetadataBuilder().putDoubleArray(\"scores\", new double[] {1.1, 2.2, 3.3}).build(),\n            FieldMetadata.builder().putDoubleArray(\"scores\", new Double[] {1.1, 2.2, 3.3}).build()),\n        Arguments.of(\n            \"BooleanArray\",\n            new MetadataBuilder()\n                .putBooleanArray(\"flags\", new boolean[] {true, false, true})\n                .build(),\n            FieldMetadata.builder()\n                .putBooleanArray(\"flags\", new Boolean[] {true, false, true})\n                .build()),\n        Arguments.of(\n            \"StringArray\",\n            new MetadataBuilder().putStringArray(\"names\", new String[] {\"a\", \"b\", \"c\"}).build(),\n            FieldMetadata.builder().putStringArray(\"names\", new String[] {\"a\", \"b\", \"c\"}).build()),\n        // Nested\n        Arguments.of(\n            \"NestedMetadata\",\n            new MetadataBuilder()\n                .putMetadata(\"inner\", new MetadataBuilder().putString(\"nested\", \"value\").build())\n                .build(),\n            FieldMetadata.builder()\n                .putFieldMetadata(\n                    \"inner\", FieldMetadata.builder().putString(\"nested\", \"value\").build())\n                .build()),\n        // Metadata Array\n        Arguments.of(\n            \"MetadataArray\",\n            new MetadataBuilder()\n                .putMetadataArray(\n                    \"items\",\n                    new Metadata[] {\n                      new MetadataBuilder().putString(\"name\", \"first\").build(),\n                      new MetadataBuilder().putString(\"name\", \"second\").build()\n                    })\n                .build(),\n            FieldMetadata.builder()\n                .putFieldMetadataArray(\n                    \"items\",\n                    new FieldMetadata[] {\n                      FieldMetadata.builder().putString(\"name\", \"first\").build(),\n                      FieldMetadata.builder().putString(\"name\", \"second\").build()\n                    })\n                .build()),\n        // Complex (multiple types mixed)\n        Arguments.of(\n            \"ComplexMetadata\",\n            new MetadataBuilder()\n                .putLong(\"id\", 123L)\n                .putDouble(\"score\", 3.14)\n                .putBoolean(\"active\", true)\n                .putString(\"name\", \"test\")\n                .putLongArray(\"versions\", new long[] {1L, 2L})\n                .putDoubleArray(\"scores\", new double[] {1.1, 2.2})\n                .putBooleanArray(\"flags\", new boolean[] {true, false})\n                .putStringArray(\"tags\", new String[] {\"a\", \"b\"})\n                .putMetadata(\n                    \"nested\",\n                    new MetadataBuilder()\n                        .putString(\"type\", \"nested\")\n                        .putLongArray(\"ids\", new long[] {1L, 2L, 3L})\n                        .build())\n                .putNull(\"empty\")\n                .build(),\n            FieldMetadata.builder()\n                .putLong(\"id\", 123L)\n                .putDouble(\"score\", 3.14)\n                .putBoolean(\"active\", true)\n                .putString(\"name\", \"test\")\n                .putLongArray(\"versions\", new Long[] {1L, 2L})\n                .putDoubleArray(\"scores\", new Double[] {1.1, 2.2})\n                .putBooleanArray(\"flags\", new Boolean[] {true, false})\n                .putStringArray(\"tags\", new String[] {\"a\", \"b\"})\n                .putFieldMetadata(\n                    \"nested\",\n                    FieldMetadata.builder()\n                        .putString(\"type\", \"nested\")\n                        .putLongArray(\"ids\", new Long[] {1L, 2L, 3L})\n                        .build())\n                .putNull(\"empty\")\n                .build()));\n  }\n\n  @ParameterizedTest(name = \"Metadata type: {0}\")\n  @MethodSource(\"metadataTypesProvider\")\n  public void testMetadataConversion(\n      String typeName, Metadata sparkMetadata, FieldMetadata kernelMetadata) {\n    assertEquals(\n        kernelMetadata, SchemaUtils.convertSparkMetadataToKernelFieldMetadata(sparkMetadata));\n    assertEquals(\n        sparkMetadata, SchemaUtils.convertKernelFieldMetadataToSparkMetadata(kernelMetadata));\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/StatsUtilsTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Map;\nimport org.apache.spark.sql.catalyst.catalog.CatalogColumnStat;\nimport org.apache.spark.sql.catalyst.catalog.CatalogStatistics;\nimport org.apache.spark.sql.connector.expressions.FieldReference;\nimport org.apache.spark.sql.connector.expressions.NamedReference;\nimport org.apache.spark.sql.connector.read.Statistics;\nimport org.apache.spark.sql.connector.read.colstats.ColumnStatistics;\nimport org.apache.spark.sql.types.DataTypes;\nimport org.apache.spark.sql.types.StructType;\nimport org.junit.jupiter.api.Test;\nimport scala.Option;\n\nclass StatsUtilsTest {\n\n  @Test\n  void testToV2Statistics_sizeAndRowCount() {\n    StructType dataSchema =\n        new StructType().add(\"id\", DataTypes.IntegerType).add(\"name\", DataTypes.StringType);\n    StructType partitionSchema = new StructType();\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            BigInt(1024L),\n            Option.apply(BigInt(100L)),\n            scala.collection.immutable.Map$.MODULE$.empty());\n\n    Statistics v2Stats = StatsUtils.toV2Statistics(catalogStats, dataSchema, partitionSchema);\n\n    assertEquals(1024L, v2Stats.sizeInBytes().getAsLong(), \"sizeInBytes should match\");\n    assertTrue(v2Stats.numRows().isPresent(), \"numRows should be present\");\n    assertEquals(100L, v2Stats.numRows().getAsLong(), \"numRows should match\");\n    assertTrue(v2Stats.columnStats().isEmpty(), \"columnStats should be empty\");\n  }\n\n  @Test\n  void testToV2Statistics_sizeOnlyNoRowCount() {\n    StructType dataSchema = new StructType().add(\"id\", DataTypes.IntegerType);\n    StructType partitionSchema = new StructType();\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(\n            BigInt(512L), Option.empty(), scala.collection.immutable.Map$.MODULE$.empty());\n\n    Statistics v2Stats = StatsUtils.toV2Statistics(catalogStats, dataSchema, partitionSchema);\n\n    assertEquals(512L, v2Stats.sizeInBytes().getAsLong(), \"sizeInBytes should match\");\n    assertFalse(v2Stats.numRows().isPresent(), \"numRows should be empty\");\n  }\n\n  @Test\n  void testToV2Statistics_withColumnStats() {\n    StructType dataSchema =\n        new StructType().add(\"id\", DataTypes.IntegerType).add(\"name\", DataTypes.StringType);\n    StructType partitionSchema = new StructType().add(\"part\", DataTypes.IntegerType);\n\n    // Create column stats for \"id\" column\n    CatalogColumnStat idColStat =\n        new CatalogColumnStat(\n            Option.apply(BigInt(10L)), // distinctCount\n            Option.apply(\"1\"), // min\n            Option.apply(\"100\"), // max\n            Option.apply(BigInt(0L)), // nullCount\n            Option.apply((Object) 4L), // avgLen\n            Option.apply((Object) 4L), // maxLen\n            Option.empty(), // histogram\n            CatalogColumnStat.VERSION());\n\n    scala.collection.immutable.Map<String, CatalogColumnStat> colStatsMap =\n        buildScalaMap(new String[] {\"id\"}, new CatalogColumnStat[] {idColStat});\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(BigInt(2048L), Option.apply(BigInt(50L)), colStatsMap);\n\n    Statistics v2Stats = StatsUtils.toV2Statistics(catalogStats, dataSchema, partitionSchema);\n\n    assertEquals(2048L, v2Stats.sizeInBytes().getAsLong());\n    assertEquals(50L, v2Stats.numRows().getAsLong());\n\n    Map<NamedReference, ColumnStatistics> colStats = v2Stats.columnStats();\n    assertEquals(1, colStats.size(), \"Should have 1 column stat\");\n\n    ColumnStatistics idStats = colStats.get(FieldReference.apply(\"id\"));\n    assertNotNull(idStats, \"id column stats should be present\");\n    assertEquals(10L, idStats.distinctCount().getAsLong(), \"distinctCount should be 10\");\n    assertEquals(0L, idStats.nullCount().getAsLong(), \"nullCount should be 0\");\n    assertEquals(4L, idStats.avgLen().getAsLong(), \"avgLen should be 4\");\n    assertEquals(4L, idStats.maxLen().getAsLong(), \"maxLen should be 4\");\n    assertTrue(idStats.min().isPresent(), \"min should be present\");\n    assertTrue(idStats.max().isPresent(), \"max should be present\");\n    assertEquals(1, idStats.min().get(), \"min should be 1\");\n    assertEquals(100, idStats.max().get(), \"max should be 100\");\n  }\n\n  @Test\n  void testToV2Statistics_skipsColumnsNotInSchema() {\n    // Only \"id\" is in schema, \"unknown\" should be skipped\n    StructType dataSchema = new StructType().add(\"id\", DataTypes.IntegerType);\n    StructType partitionSchema = new StructType();\n\n    CatalogColumnStat colStat =\n        new CatalogColumnStat(\n            Option.apply(BigInt(5L)),\n            Option.empty(),\n            Option.empty(),\n            Option.apply(BigInt(1L)),\n            Option.empty(),\n            Option.empty(),\n            Option.empty(),\n            CatalogColumnStat.VERSION());\n\n    scala.collection.immutable.Map<String, CatalogColumnStat> colStatsMap =\n        buildScalaMap(new String[] {\"id\", \"unknown\"}, new CatalogColumnStat[] {colStat, colStat});\n\n    CatalogStatistics catalogStats =\n        new CatalogStatistics(BigInt(100L), Option.empty(), colStatsMap);\n\n    Statistics v2Stats = StatsUtils.toV2Statistics(catalogStats, dataSchema, partitionSchema);\n\n    Map<NamedReference, ColumnStatistics> result = v2Stats.columnStats();\n    assertEquals(1, result.size(), \"Should only have 1 column stat (unknown skipped)\");\n    assertNotNull(result.get(FieldReference.apply(\"id\")), \"id stats should be present\");\n  }\n\n  private static scala.math.BigInt BigInt(long value) {\n    return scala.math.BigInt.apply(value);\n  }\n\n  @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n  private static scala.collection.immutable.Map<String, CatalogColumnStat> buildScalaMap(\n      String[] keys, CatalogColumnStat[] values) {\n    scala.collection.mutable.Builder<\n            scala.Tuple2<String, CatalogColumnStat>,\n            scala.collection.immutable.Map<String, CatalogColumnStat>>\n        b = (scala.collection.mutable.Builder) scala.collection.immutable.Map$.MODULE$.newBuilder();\n    for (int i = 0; i < keys.length; i++) {\n      b.$plus$eq(new scala.Tuple2<>(keys[i], values[i]));\n    }\n    return b.result();\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/java/io/delta/spark/internal/v2/utils/StreamingHelperTest.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport io.delta.kernel.Snapshot;\nimport io.delta.kernel.internal.DeltaHistoryManager;\nimport io.delta.spark.internal.v2.DeltaV2TestBase;\nimport io.delta.spark.internal.v2.exception.VersionNotFoundException;\nimport io.delta.spark.internal.v2.snapshot.PathBasedSnapshotManager;\nimport java.io.File;\nimport java.sql.Timestamp;\nimport java.util.stream.Stream;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.delta.DeltaLog;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport scala.Option;\n\npublic class StreamingHelperTest extends DeltaV2TestBase {\n\n  private PathBasedSnapshotManager snapshotManager;\n\n  @Test\n  public void testUnsafeVolatileSnapshot(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_volatile_snapshot\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.Snapshot deltaSnapshot = deltaLog.unsafeVolatileSnapshot();\n    Snapshot kernelSnapshot = snapshotManager.loadLatestSnapshot();\n\n    spark.sql(String.format(\"INSERT INTO %s VALUES (4, 'David')\", testTableName));\n\n    assertEquals(0L, deltaSnapshot.version());\n    assertEquals(deltaSnapshot.version(), kernelSnapshot.getVersion());\n  }\n\n  @Test\n  public void testLoadLatestSnapshot(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_update\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    Snapshot initialSnapshot = snapshotManager.loadLatestSnapshot();\n    assertEquals(0L, initialSnapshot.getVersion());\n\n    spark.sql(String.format(\"INSERT INTO %s VALUES (4, 'David')\", testTableName));\n\n    org.apache.spark.sql.delta.Snapshot deltaSnapshot =\n        deltaLog.update(false, Option.empty(), Option.empty());\n    Snapshot updatedSnapshot = snapshotManager.loadLatestSnapshot();\n    org.apache.spark.sql.delta.Snapshot cachedSnapshot = deltaLog.unsafeVolatileSnapshot();\n    Snapshot kernelcachedSnapshot = snapshotManager.loadLatestSnapshot();\n\n    assertEquals(1L, updatedSnapshot.getVersion());\n    assertEquals(deltaSnapshot.version(), updatedSnapshot.getVersion());\n    assertEquals(1L, kernelcachedSnapshot.getVersion());\n    assertEquals(cachedSnapshot.version(), kernelcachedSnapshot.getVersion());\n  }\n\n  @Test\n  public void testMultipleLoadLatestSnapshot(@TempDir File tempDir) {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_multiple_updates\";\n    createEmptyTestTable(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    assertEquals(0L, snapshotManager.loadLatestSnapshot().getVersion());\n\n    for (int i = 0; i < 3; i++) {\n      spark.sql(\n          String.format(\"INSERT INTO %s VALUES (%d, 'User%d')\", testTableName, 20 + i, 20 + i));\n\n      org.apache.spark.sql.delta.Snapshot deltaSnapshot =\n          deltaLog.update(false, Option.empty(), Option.empty());\n      Snapshot kernelSnapshot = snapshotManager.loadLatestSnapshot();\n\n      long expectedVersion = i + 1;\n      assertEquals(expectedVersion, deltaSnapshot.version());\n      assertEquals(expectedVersion, kernelSnapshot.getVersion());\n    }\n  }\n\n  private void setupTableWithDeletedVersions(String testTablePath, String testTableName) {\n    createEmptyTestTable(testTablePath, testTableName);\n    for (int i = 0; i < 10; i++) {\n      spark.sql(\n          String.format(\"INSERT INTO %s VALUES (%d, 'User%d')\", testTableName, 100 + i, 100 + i));\n    }\n    File deltaLogDir = new File(testTablePath, \"_delta_log\");\n    File version0File = new File(deltaLogDir, \"00000000000000000000.json\");\n    File version1File = new File(deltaLogDir, \"00000000000000000001.json\");\n    assertTrue(version0File.exists());\n    assertTrue(version1File.exists());\n    version0File.delete();\n    version1File.delete();\n    assertFalse(version0File.exists());\n    assertFalse(version1File.exists());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_pastTimestamp(@TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_past\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Thread.sleep(100);\n    Timestamp timestamp = new Timestamp(System.currentTimeMillis());\n    spark.sql(String.format(\"INSERT INTO %s VALUES (200, 'NewUser')\", testTableName));\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                timestamp,\n                Option.empty() /* catalogTable */,\n                false /* canReturnLastCommit */,\n                true /* mustBeRecreatable */,\n                false /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            timestamp.getTime(),\n            false /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_futureTimestamp_canReturnLast(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_future_last\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp futureTimestamp = new Timestamp(System.currentTimeMillis() + 10000);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                futureTimestamp,\n                Option.empty() /* catalogTable */,\n                true /* canReturnLastCommit */,\n                true /* mustBeRecreatable */,\n                false /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            futureTimestamp.getTime(),\n            true /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_futureTimestamp_notMustBeRecreatable(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_future_not_recreatable\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp futureTimestamp = new Timestamp(System.currentTimeMillis() + 10000);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                futureTimestamp,\n                Option.empty() /* catalogTable */,\n                true /* canReturnLastCommit */,\n                false /* mustBeRecreatable */,\n                false /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            futureTimestamp.getTime(),\n            true /* canReturnLastCommit */,\n            false /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_earlyTimestamp_canReturnEarliest(@TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_early\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp earlyTimestamp = new Timestamp(0);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                earlyTimestamp,\n                Option.empty() /* catalogTable */,\n                false /* canReturnLastCommit */,\n                true /* mustBeRecreatable */,\n                true /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            earlyTimestamp.getTime(),\n            false /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            true /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  @Test\n  public void testGetActiveCommitAtTime_earlyTimestamp_notMustBeRecreatable_canReturnEarliest(\n      @TempDir File tempDir) throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_commit_early_not_recreatable\";\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n\n    Timestamp earlyTimestamp = new Timestamp(0);\n\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n    org.apache.spark.sql.delta.DeltaHistoryManager.Commit deltaCommit =\n        deltaLog\n            .history()\n            .getActiveCommitAtTime(\n                earlyTimestamp,\n                Option.empty() /* catalogTable */,\n                false /* canReturnLastCommit */,\n                false /* mustBeRecreatable */,\n                true /* canReturnEarliestCommit */);\n\n    DeltaHistoryManager.Commit kernelCommit =\n        snapshotManager.getActiveCommitAtTime(\n            earlyTimestamp.getTime(),\n            false /* canReturnLastCommit */,\n            false /* mustBeRecreatable */,\n            true /* canReturnEarliestCommit */);\n\n    assertEquals(deltaCommit.version(), kernelCommit.getVersion());\n    assertEquals(deltaCommit.timestamp(), kernelCommit.getTimestamp());\n  }\n\n  private static Stream<Arguments> checkVersionExistsTestCases() {\n    return Stream.of(\n        Arguments.of(\n            \"current\",\n            10L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            false /* shouldThrow */),\n        Arguments.of(\n            \"notAllowOutOfRange\",\n            21L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            true /* shouldThrow */),\n        Arguments.of(\n            \"allowOutOfRange\",\n            21L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            true /* allowOutOfRange */,\n            false /* shouldThrow */),\n        Arguments.of(\n            \"belowEarliest\",\n            1L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            true /* shouldThrow */),\n        Arguments.of(\n            \"mustBeRecreatable_false\",\n            2L /* versionToCheck */,\n            false /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            false /* shouldThrow */),\n        Arguments.of(\n            \"mustBeRecreatable_true\",\n            2L /* versionToCheck */,\n            true /* mustBeRecreatable */,\n            false /* allowOutOfRange */,\n            true /* shouldThrow */));\n  }\n\n  @ParameterizedTest(name = \"{0}\")\n  @MethodSource(\"checkVersionExistsTestCases\")\n  public void testCheckVersionExists(\n      String testName,\n      long versionToCheck,\n      boolean mustBeRecreatable,\n      boolean allowOutOfRange,\n      boolean shouldThrow,\n      @TempDir File tempDir)\n      throws Exception {\n    String testTablePath = tempDir.getAbsolutePath();\n    String testTableName = \"test_version_\" + testName;\n    setupTableWithDeletedVersions(testTablePath, testTableName);\n    snapshotManager =\n        new PathBasedSnapshotManager(testTablePath, spark.sessionState().newHadoopConf());\n    DeltaLog deltaLog = DeltaLog.forTable(spark, new Path(testTablePath));\n\n    if (shouldThrow) {\n      assertThrows(\n          VersionNotFoundException.class,\n          () ->\n              snapshotManager.checkVersionExists(\n                  versionToCheck, mustBeRecreatable, allowOutOfRange));\n\n      assertThrows(\n          org.apache.spark.sql.delta.VersionNotFoundException.class,\n          () ->\n              deltaLog\n                  .history()\n                  .checkVersionExists(\n                      versionToCheck, Option.empty(), mustBeRecreatable, allowOutOfRange));\n    } else {\n      snapshotManager.checkVersionExists(versionToCheck, mustBeRecreatable, allowOutOfRange);\n      deltaLog\n          .history()\n          .checkVersionExists(versionToCheck, Option.empty(), mustBeRecreatable, allowOutOfRange);\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/scala/io/delta/spark/internal/v2/snapshot/unitycatalog/UCManagedTableSnapshotManagerSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot.unitycatalog\n\nimport java.util.Optional\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.kernel.exceptions.KernelException\nimport io.delta.kernel.unitycatalog.{InMemoryUCClient, UCCatalogManagedClient, UCCatalogManagedTestUtils}\nimport io.delta.spark.internal.v2.exception.VersionNotFoundException\nimport io.delta.storage.commit.uccommitcoordinator.InvalidTargetTableException\n\nimport org.scalatest.funsuite.AnyFunSuite\n\n/** Integration tests for [[UCManagedTableSnapshotManager]]. */\nclass UCManagedTableSnapshotManagerSuite\n    extends AnyFunSuite\n    with UCCatalogManagedTestUtils {\n\n  private val testUcTableId = \"testUcTableId\"\n  private val testUcUri = \"https://test-uc.example.com\"\n  private val testUcToken = \"test-token\"\n  private val testUcAuthConfig = Map(\"token\" -> testUcToken).asJava\n\n  private def createManager(\n      ucClient: InMemoryUCClient,\n      tablePath: String) = {\n    val client = new UCCatalogManagedClient(ucClient)\n    val tableInfo = new UCTableInfo(testUcTableId, tablePath, testUcUri, testUcAuthConfig)\n    new UCManagedTableSnapshotManager(client, tableInfo, defaultEngine)\n  }\n\n  // ==================== Constructor ====================\n\n  test(\"constructor rejects null arguments\") {\n    val ucClient = new InMemoryUCClient(\"testMetastore\")\n    val client = new UCCatalogManagedClient(ucClient)\n    val tableInfo = new UCTableInfo(testUcTableId, \"/test/path\", testUcUri, testUcAuthConfig)\n\n    val ex1 = intercept[NullPointerException] {\n      new UCManagedTableSnapshotManager(null, tableInfo, defaultEngine)\n    }\n    assert(ex1.getMessage == \"ucCatalogManagedClient is null\")\n\n    val ex2 = intercept[NullPointerException] {\n      new UCManagedTableSnapshotManager(client, null, defaultEngine)\n    }\n    assert(ex2.getMessage == \"tableInfo is null\")\n\n    val ex3 = intercept[NullPointerException] {\n      new UCManagedTableSnapshotManager(client, tableInfo, null)\n    }\n    assert(ex3.getMessage == \"engine is null\")\n  }\n\n  // ==================== loadLatestSnapshot ====================\n\n  test(\"loadLatestSnapshot: returns snapshot at max ratified version\") {\n    withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>\n      val manager = createManager(ucClient, tablePath)\n\n      val snapshot = manager.loadLatestSnapshot()\n\n      assert(snapshot.getVersion == maxRatifiedVersion)\n    }\n  }\n\n  test(\"loadLatestSnapshot: throws when table does not exist in catalog\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val tableInfo = new UCTableInfo(\"nonExistentTableId\", \"/fake/path\", testUcUri, testUcAuthConfig)\n    val client = new UCCatalogManagedClient(ucClient)\n    val manager = new UCManagedTableSnapshotManager(client, tableInfo, defaultEngine)\n\n    val ex = intercept[RuntimeException] {\n      manager.loadLatestSnapshot()\n    }\n    assert(ex.getCause.isInstanceOf[InvalidTargetTableException])\n  }\n\n  // ==================== loadSnapshotAt ====================\n\n  test(\"loadSnapshotAt: valid versions including v0 succeed, invalid versions throw\") {\n    withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>\n      val manager = createManager(ucClient, tablePath)\n\n      assert(manager.loadSnapshotAt(0L).getVersion == 0L)\n      assert(manager.loadSnapshotAt(1L).getVersion == 1L)\n\n      intercept[IllegalArgumentException] { manager.loadSnapshotAt(-1L) }\n      intercept[IllegalArgumentException] { manager.loadSnapshotAt(maxRatifiedVersion + 10) }\n    }\n  }\n\n  // ==================== checkVersionExists ====================\n\n  test(\"checkVersionExists: validates version bounds and allowOutOfRange flag\") {\n    withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>\n      val manager = createManager(ucClient, tablePath)\n\n      // Valid versions including v0 do not throw\n      manager.checkVersionExists(0L, true /* mustBeRecreatable */, false /* allowOutOfRange */ )\n      manager.checkVersionExists(\n        maxRatifiedVersion,\n        true /* mustBeRecreatable */,\n        false /* allowOutOfRange */ )\n      manager.checkVersionExists(\n        maxRatifiedVersion - 1,\n        true /* mustBeRecreatable */,\n        false /* allowOutOfRange */ )\n      manager.checkVersionExists(1L, true /* mustBeRecreatable */, false /* allowOutOfRange */ )\n      manager.checkVersionExists(1L, false /* mustBeRecreatable */, false /* allowOutOfRange */ )\n\n      // Out-of-bounds versions throw\n      val belowLowerBound = intercept[VersionNotFoundException] {\n        manager.checkVersionExists(-1L, true /* mustBeRecreatable */, false /* allowOutOfRange */ )\n      }\n      assert(belowLowerBound.getUserVersion == -1L)\n      assert(belowLowerBound.getEarliest == 0L)\n      assert(belowLowerBound.getLatest == maxRatifiedVersion)\n\n      val aboveUpperBound = intercept[VersionNotFoundException] {\n        manager.checkVersionExists(\n          maxRatifiedVersion + 10,\n          true /* mustBeRecreatable */,\n          false /* allowOutOfRange */ )\n      }\n      assert(aboveUpperBound.getUserVersion == maxRatifiedVersion + 10)\n      assert(aboveUpperBound.getEarliest == 0L)\n      assert(aboveUpperBound.getLatest == maxRatifiedVersion)\n\n      // allowOutOfRange=true bypasses upper bound check\n      manager.checkVersionExists(\n        maxRatifiedVersion + 10,\n        true /* mustBeRecreatable */,\n        true /* allowOutOfRange */ )\n    }\n  }\n\n  // ==================== getActiveCommitAtTime ====================\n\n  test(\"getActiveCommitAtTime: resolves timestamps across all boundaries\") {\n    withUCClientAndTestTable { (ucClient, tablePath, _) =>\n      val manager = createManager(ucClient, tablePath)\n\n      // Before first commit (v0) - throws without canReturnEarliestCommit\n      intercept[KernelException] {\n        manager.getActiveCommitAtTime(\n          v0Ts - 1,\n          false /* canReturnLastCommit */,\n          true /* mustBeRecreatable */,\n          false /* canReturnEarliestCommit */ )\n      }\n      intercept[KernelException] {\n        manager.getActiveCommitAtTime(\n          -100L,\n          false /* canReturnLastCommit */,\n          true /* mustBeRecreatable */,\n          false /* canReturnEarliestCommit */ )\n      }\n      // With canReturnEarliestCommit, returns v0\n      val earliestCommit = manager.getActiveCommitAtTime(\n        v0Ts - 1,\n        false /* canReturnLastCommit */,\n        true /* mustBeRecreatable */,\n        true /* canReturnEarliestCommit */ )\n      assert(earliestCommit.getVersion == 0L)\n\n      // Exact and between-commit timestamps\n      def activeVersion(ts: Long): Long =\n        manager\n          .getActiveCommitAtTime(\n            ts,\n            false /* canReturnLastCommit */,\n            true /* mustBeRecreatable */,\n            false /* canReturnEarliestCommit */ )\n          .getVersion\n\n      assert(activeVersion(v0Ts) == 0L)\n      assert(activeVersion(v0Ts + 1) == 0L)\n      assert(activeVersion(v1Ts) == 1L)\n      assert(activeVersion(v1Ts + 1) == 1L)\n      assert(activeVersion(v2Ts) == 2L)\n\n      // After last commit (v2) - throws without canReturnLastCommit\n      intercept[KernelException] {\n        manager.getActiveCommitAtTime(\n          v2Ts + 1,\n          false /* canReturnLastCommit */,\n          true /* mustBeRecreatable */,\n          false /* canReturnEarliestCommit */ )\n      }\n      intercept[KernelException] {\n        manager.getActiveCommitAtTime(\n          Long.MaxValue,\n          false /* canReturnLastCommit */,\n          true /* mustBeRecreatable */,\n          false /* canReturnEarliestCommit */ )\n      }\n      // With canReturnLastCommit, returns v2\n      val lastCommit = manager.getActiveCommitAtTime(\n        v2Ts + 1,\n        true /* canReturnLastCommit */,\n        true /* mustBeRecreatable */,\n        false /* canReturnEarliestCommit */ )\n      assert(lastCommit.getVersion == 2L)\n    }\n  }\n\n  test(\"getActiveCommitAtTime: non-recreatable path returns earliest delta file\") {\n    withUCClientAndTestTable { (ucClient, tablePath, _) =>\n      val manager = createManager(ucClient, tablePath)\n\n      val active = manager.getActiveCommitAtTime(\n        v0Ts - 1,\n        false /* canReturnLastCommit */,\n        false /* mustBeRecreatable */,\n        true /* canReturnEarliestCommit */ )\n\n      assert(active.getVersion == 0L)\n    }\n  }\n\n  // ==================== getTableChanges ====================\n\n  test(\"getTableChanges: returns valid ranges and rejects invalid arguments\") {\n    withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>\n      val manager = createManager(ucClient, tablePath)\n\n      // Valid ranges including v0 and latest boundaries\n      val fullRange = manager.getTableChanges(defaultEngine, 0L, Optional.of(maxRatifiedVersion))\n      assert(fullRange.getStartVersion == 0L)\n      assert(fullRange.getEndVersion == maxRatifiedVersion)\n\n      val toLatest = manager.getTableChanges(defaultEngine, 1L, Optional.empty())\n      assert(toLatest.getStartVersion == 1L)\n      assert(toLatest.getEndVersion == maxRatifiedVersion)\n\n      val single = manager.getTableChanges(defaultEngine, 1L, Optional.of(1L))\n      assert(single.getStartVersion == 1L)\n      assert(single.getEndVersion == 1L)\n\n      val first = manager.getTableChanges(defaultEngine, 0L, Optional.of(0L))\n      assert(first.getStartVersion == 0L)\n      assert(first.getEndVersion == 0L)\n\n      val last = manager.getTableChanges(\n        defaultEngine,\n        maxRatifiedVersion,\n        Optional.of(maxRatifiedVersion))\n      assert(last.getStartVersion == maxRatifiedVersion)\n      assert(last.getEndVersion == maxRatifiedVersion)\n\n      // Invalid ranges throw\n      intercept[IllegalArgumentException] {\n        manager.getTableChanges(\n          defaultEngine,\n          maxRatifiedVersion,\n          Optional.of(maxRatifiedVersion - 1))\n      }\n\n      intercept[IllegalArgumentException] {\n        manager.getTableChanges(defaultEngine, maxRatifiedVersion + 5, Optional.empty())\n      }\n    }\n  }\n\n  // ==================== Exception Propagation ====================\n\n  test(\"operations propagate InvalidTargetTableException from client\") {\n    val ucClient = new InMemoryUCClient(\"ucMetastoreId\")\n    val tableInfo = new UCTableInfo(\"nonExistentTableId\", \"/fake/path\", testUcUri, testUcAuthConfig)\n    val client = new UCCatalogManagedClient(ucClient)\n    val manager = new UCManagedTableSnapshotManager(client, tableInfo, defaultEngine)\n\n    val ex1 = intercept[RuntimeException] { manager.loadLatestSnapshot() }\n    assert(ex1.getCause.isInstanceOf[InvalidTargetTableException])\n\n    val ex2 = intercept[RuntimeException] { manager.loadSnapshotAt(0L) }\n    assert(ex2.getCause.isInstanceOf[InvalidTargetTableException])\n\n    val ex3 = intercept[RuntimeException] { manager.checkVersionExists(0L, true, false) }\n    assert(ex3.getCause.isInstanceOf[InvalidTargetTableException])\n\n    val ex4 = intercept[RuntimeException] {\n      manager.getTableChanges(defaultEngine, 0L, Optional.empty())\n    }\n    assert(ex4.getCause.isInstanceOf[InvalidTargetTableException])\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/scala/io/delta/spark/internal/v2/snapshot/unitycatalog/UCUtilsSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.snapshot.unitycatalog\n\nimport java.net.URI\nimport java.util.{HashMap => JHashMap}\n\nimport io.delta.kernel.internal.tablefeatures.TableFeatures\nimport io.delta.spark.internal.v2.utils.CatalogTableTestUtils\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\n\nimport org.apache.spark.SparkFunSuite\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.test.SharedSparkSession\n\n/**\n * Unit tests for [[UCUtils]].\n *\n * Tests use distinctive, high-entropy values that would fail if the implementation\n * had hardcoded defaults instead of actually extracting values from the inputs.\n */\nclass UCUtilsSuite extends SparkFunSuite with SharedSparkSession {\n\n  // Use the same constants as CatalogTableUtils to ensure consistency\n  private val FEATURE_CATALOG_MANAGED =\n    TableFeatures.SET_TABLE_FEATURE_SUPPORTED_PREFIX +\n      TableFeatures.CATALOG_MANAGED_RW_FEATURE.featureName()\n  private val FEATURE_SUPPORTED = TableFeatures.SET_TABLE_FEATURE_SUPPORTED_VALUE\n  private val UC_TABLE_ID_KEY = UCCommitCoordinatorClient.UC_TABLE_ID_KEY\n  private val UC_CATALOG_CONNECTOR = \"io.unitycatalog.spark.UCSingleCatalog\"\n\n  // Distinctive values that would fail if hardcoded\n  private val TABLE_ID_ALPHA = \"uc_8f2b3c9a-d1e7-4a6f-b8c2\"\n  private val TABLE_PATH_ALPHA = \"abfss://delta-store@prod.dfs.core.windows.net/warehouse/tbl_v3\"\n  private val UC_URI_ALPHA = \"https://uc-server-westus2.example.net/api/2.1/unity-catalog\"\n  private val UC_TOKEN_ALPHA = \"dapi_Xk7mP$9qRs#2vWz_prod\"\n  private val CATALOG_ALPHA = \"uc_catalog_westus2_prod\"\n\n  // ==================== Helper Methods ====================\n\n  private def makeNonUCTable(): CatalogTable = {\n    CatalogTableTestUtils.createCatalogTable(locationUri = Some(new URI(TABLE_PATH_ALPHA)))\n  }\n\n  private def makeUCTable(\n      tableId: String = TABLE_ID_ALPHA,\n      tablePath: String = TABLE_PATH_ALPHA,\n      catalogName: Option[String] = None): CatalogTable = {\n    val storageProps = new JHashMap[String, String]()\n    storageProps.put(FEATURE_CATALOG_MANAGED, FEATURE_SUPPORTED)\n    storageProps.put(UC_TABLE_ID_KEY, tableId)\n\n    CatalogTableTestUtils.createCatalogTable(\n      catalogName = catalogName,\n      storageProperties = storageProps,\n      locationUri = Some(new URI(tablePath)))\n  }\n\n  private def withUCCatalogConfig(\n      catalogName: String,\n      uri: String,\n      token: String)(testCode: => Unit): Unit = {\n    val configs = Seq(\n      s\"spark.sql.catalog.$catalogName\" -> UC_CATALOG_CONNECTOR,\n      s\"spark.sql.catalog.$catalogName.uri\" -> uri,\n      s\"spark.sql.catalog.$catalogName.token\" -> token)\n    val originalValues = configs.map { case (key, _) => key -> spark.conf.getOption(key) }.toMap\n\n    try {\n      configs.foreach { case (key, value) => spark.conf.set(key, value) }\n      testCode\n    } finally {\n      configs.foreach { case (key, _) =>\n        originalValues.get(key).flatten match {\n          case Some(v) => spark.conf.set(key, v)\n          case None => spark.conf.unset(key)\n        }\n      }\n    }\n  }\n\n  // ==================== Tests ====================\n\n  test(\"returns empty for non-UC table\") {\n    val table = makeNonUCTable()\n    val result = UCUtils.extractTableInfo(table, spark)\n    assert(result.isEmpty, \"Non-UC table should return empty Optional\")\n  }\n\n  test(\"returns empty when UC table ID present but feature flag missing\") {\n    val storageProps = new JHashMap[String, String]()\n    storageProps.put(UC_TABLE_ID_KEY, \"orphan_id_9x7y5z\")\n    // No FEATURE_CATALOG_MANAGED - simulates corrupted/partial metadata\n\n    val table = CatalogTableTestUtils.createCatalogTable(\n      storageProperties = storageProps,\n      locationUri = Some(new URI(\"gs://other-bucket/path\")))\n    val result = UCUtils.extractTableInfo(table, spark)\n    assert(result.isEmpty, \"Missing feature flag should return empty\")\n  }\n\n  test(\"throws IllegalArgumentException for UC table with empty table ID\") {\n    val storageProps = new JHashMap[String, String]()\n    storageProps.put(FEATURE_CATALOG_MANAGED, FEATURE_SUPPORTED)\n    storageProps.put(UC_TABLE_ID_KEY, \"\")\n\n    val table = CatalogTableTestUtils.createCatalogTable(\n      storageProperties = storageProps,\n      locationUri = Some(new URI(\"s3://empty-id-bucket/path\")))\n    val exception = intercept[IllegalArgumentException] {\n      UCUtils.extractTableInfo(table, spark)\n    }\n    assert(exception.getMessage.contains(\"Cannot extract ucTableId\"))\n  }\n\n  test(\"throws exception for UC table without location\") {\n    val storageProps = new JHashMap[String, String]()\n    storageProps.put(FEATURE_CATALOG_MANAGED, FEATURE_SUPPORTED)\n    storageProps.put(UC_TABLE_ID_KEY, \"no_location_tbl_id_3k9m\")\n\n    val table = CatalogTableTestUtils.createCatalogTable(storageProperties = storageProps)\n    // Spark throws AnalysisException when location is missing\n    val exception = intercept[Exception] {\n      UCUtils.extractTableInfo(table, spark)\n    }\n    assert(exception.getMessage.contains(\"locationUri\") ||\n      exception.getMessage.contains(\"location\"))\n  }\n\n  test(\"throws IllegalArgumentException when no matching catalog configuration\") {\n    val table = makeUCTable(catalogName = Some(\"nonexistent_catalog_xyz\"))\n\n    val exception = intercept[IllegalArgumentException] {\n      UCUtils.extractTableInfo(table, spark)\n    }\n    assert(exception.getMessage.contains(\"Unity Catalog configuration not found\") ||\n      exception.getMessage.contains(\"Cannot create UC client\"))\n  }\n\n  test(\"extracts table info when UC catalog is properly configured\") {\n    val table = makeUCTable(catalogName = Some(CATALOG_ALPHA))\n\n    withUCCatalogConfig(CATALOG_ALPHA, UC_URI_ALPHA, UC_TOKEN_ALPHA) {\n      val result = UCUtils.extractTableInfo(table, spark)\n\n      assert(result.isPresent, \"Should return table info\")\n      val info = result.get()\n      // Each assertion uses the specific expected value - would fail if hardcoded\n      assert(info.getTableId == TABLE_ID_ALPHA, s\"Table ID mismatch: got ${info.getTableId}\")\n      assert(\n        info.getTablePath == TABLE_PATH_ALPHA,\n        s\"Table path mismatch: got ${info.getTablePath}\")\n      assert(info.getUcUri == UC_URI_ALPHA, s\"UC URI mismatch: got ${info.getUcUri}\")\n      val configMap = info.getAuthConfig\n      assert(\n        configMap.get(\"type\") == \"static\",\n        s\"Type should be static: got ${configMap.get(\"type\")}\")\n      assert(\n        configMap.get(\"token\") == UC_TOKEN_ALPHA,\n        s\"UC token mismatch: got ${configMap.get(\"token\")}\")\n    }\n  }\n\n  test(\"selects correct catalog when multiple catalogs configured\") {\n    // Use completely different values for each catalog to prove selection works\n    val catalogBeta = \"uc_catalog_eastus_staging\"\n    val ucUriBeta = \"https://uc-server-eastus.example.net/api/2.1/uc\"\n    val ucTokenBeta = \"dapi_Yz3nQ$8wRt#1vXa_staging\"\n    val tableIdBeta = \"uc_tbl_staging_4d7e2f1a\"\n    val tablePathBeta = \"s3://staging-bucket-us-east/delta/tables/v2\"\n\n    val catalogGamma = \"uc_catalog_euwest_dev\"\n    val ucUriGamma = \"https://uc-server-euwest.example.net/api/2.1/uc\"\n    val ucTokenGamma = \"dapi_Jk5pL$3mNq#9vBc_dev\"\n\n    // Table is in catalogBeta\n    val table = makeUCTable(\n      tableId = tableIdBeta,\n      tablePath = tablePathBeta,\n      catalogName = Some(catalogBeta))\n\n    val configs = Seq(\n      // catalogGamma config (should NOT be used)\n      s\"spark.sql.catalog.$catalogGamma\" -> UC_CATALOG_CONNECTOR,\n      s\"spark.sql.catalog.$catalogGamma.uri\" -> ucUriGamma,\n      s\"spark.sql.catalog.$catalogGamma.token\" -> ucTokenBeta,\n      // catalogBeta config (should be used)\n      s\"spark.sql.catalog.$catalogBeta\" -> UC_CATALOG_CONNECTOR,\n      s\"spark.sql.catalog.$catalogBeta.uri\" -> ucUriBeta,\n      s\"spark.sql.catalog.$catalogBeta.token\" -> ucTokenBeta)\n    val originalValues = configs.map { case (key, _) => key -> spark.conf.getOption(key) }.toMap\n\n    try {\n      configs.foreach { case (key, value) => spark.conf.set(key, value) }\n\n      val result = UCUtils.extractTableInfo(table, spark)\n      assert(result.isPresent, \"Should return table info\")\n\n      val info = result.get()\n      // Verify it selected catalogBeta's config, not catalogGamma's\n      assert(\n        info.getUcUri == ucUriBeta,\n        s\"Should use catalogBeta's URI, got: ${info.getUcUri}\")\n      val configMap = info.getAuthConfig\n      assert(configMap.get(\"type\") == \"static\", s\"Type should be static\")\n      assert(\n        configMap.get(\"token\") == ucTokenBeta,\n        s\"Should use catalogBeta's token, got: ${configMap.get(\"token\")}\")\n      assert(info.getTableId == tableIdBeta, s\"Should extract tableIdBeta, got: ${info.getTableId}\")\n      assert(\n        info.getTablePath == tablePathBeta,\n        s\"Should extract tablePathBeta, got: ${info.getTablePath}\")\n    } finally {\n      configs.foreach { case (key, _) =>\n        originalValues.get(key).flatten match {\n          case Some(v) => spark.conf.set(key, v)\n          case None => spark.conf.unset(key)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark/v2/src/test/scala/io/delta/spark/internal/v2/utils/CatalogTableTestUtils.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.spark.internal.v2.utils\n\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable, CatalogTableType}\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Helpers for constructing [[CatalogTable]] instances inside Java tests.\n *\n * Spark's [[CatalogTable]] is defined in Scala and its constructor signature shifts between Spark\n * releases. Centralising the construction in Scala keeps the kernel tests insulated from those\n * binary changes and saves Java tests from manually wiring the many optional parameters.\n */\nobject CatalogTableTestUtils {\n\n  /**\n   * Creates a [[CatalogTable]] with configurable options.\n   *\n   * @param tableName table name (default: \"tbl\")\n   * @param catalogName optional catalog name for the identifier\n   * @param properties table properties (default: empty)\n   * @param storageProperties storage properties (default: empty)\n   * @param locationUri optional storage location URI\n   * @param nullStorage if true, sets storage to null (for edge case testing)\n   * @param nullStorageProperties if true, sets storage properties to null\n   */\n  def createCatalogTable(\n      tableName: String = \"tbl\",\n      catalogName: Option[String] = None,\n      properties: java.util.Map[String, String] = new java.util.HashMap[String, String](),\n      storageProperties: java.util.Map[String, String] = new java.util.HashMap[String, String](),\n      locationUri: Option[java.net.URI] = None,\n      nullStorage: Boolean = false,\n      nullStorageProperties: Boolean = false): CatalogTable = {\n\n    val scalaProps = ScalaUtils.toScalaMap(properties)\n    val scalaStorageProps =\n      if (nullStorageProperties) null else ScalaUtils.toScalaMap(storageProperties)\n\n    val identifier = catalogName match {\n      case Some(catalog) =>\n        TableIdentifier(tableName, Some(\"default\"), Some(catalog))\n      case None => TableIdentifier(tableName)\n    }\n\n    val storage = if (nullStorage) {\n      null\n    } else {\n      CatalogStorageFormat(\n        locationUri = locationUri,\n        inputFormat = None,\n        outputFormat = None,\n        serde = None,\n        compressed = false,\n        properties = scalaStorageProps)\n    }\n\n    CatalogTable(\n      identifier = identifier,\n      tableType = CatalogTableType.MANAGED,\n      storage = storage,\n      schema = new StructType(),\n      provider = None,\n      partitionColumnNames = Seq.empty,\n      bucketSpec = None,\n      properties = scalaProps)\n  }\n}\n"
  },
  {
    "path": "spark-connect/client/src/main/scala/io/delta/connect/tables/DeltaColumnBuilder.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport org.apache.spark.annotation.Evolving\nimport org.apache.spark.sql.catalyst.parser.DataTypeParser\nimport org.apache.spark.sql.types.{DataType, LongType, MetadataBuilder, StructField}\n\n/**\n * :: Evolving ::\n *\n * Builder to specify a table column.\n *\n * See [[DeltaTableBuilder]] for examples.\n *\n * @since 4.0.0\n */\n@Evolving\nclass DeltaColumnBuilder private[tables](private val colName: String) {\n  private var dataType: DataType = _\n  private var nullable: Boolean = true\n  private var generationExpr: Option[String] = None\n  private var comment: Option[String] = None\n  private var identityStart: Option[Long] = None\n  private var identityStep: Option[Long] = None\n  private var identityAllowExplicitInsert: Option[Boolean] = None\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the column data type.\n   *\n   * @param dataType string column data type\n   * @since 4.0.0\n   */\n  @Evolving\n  def dataType(dataType: String): DeltaColumnBuilder = {\n    this.dataType = DataTypeParser.parseDataType(dataType)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the column data type.\n   *\n   * @param dataType DataType column data type\n   * @since 4.0.0\n   */\n  @Evolving\n  def dataType(dataType: DataType): DeltaColumnBuilder = {\n    this.dataType = dataType\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify whether the column can be null.\n   *\n   * @param nullable boolean whether the column can be null or not.\n   * @since 4.0.0\n   */\n  @Evolving\n  def nullable(nullable: Boolean): DeltaColumnBuilder = {\n    this.nullable = nullable\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a expression if the column is always generated as a function of other columns.\n   *\n   * @param expr string the the generation expression\n   * @since 4.0.0\n   */\n  @Evolving\n  def generatedAlwaysAs(expr: String): DeltaColumnBuilder = {\n    this.generationExpr = Option(expr)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column with default values that is always generated\n   * by the system (i.e. does not allow user-specified values).\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def generatedAlwaysAsIdentity(): DeltaColumnBuilder = {\n    generatedAlwaysAsIdentity(start = 1, step = 1)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column that is always generated by the system (i.e. does not\n   * allow user-specified values).\n   *\n   * @param start the start value of the identity column\n   * @param step  the increment step of the identity column\n   * @since 4.0.0\n   */\n  @Evolving\n  def generatedAlwaysAsIdentity(start: Long, step: Long): DeltaColumnBuilder = {\n    this.identityStart = Some(start)\n    this.identityStep = Some(step)\n    this.identityAllowExplicitInsert = Some(false)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column that allows user-specified values such that the\n   * generated values use default start and step values.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def generatedByDefaultAsIdentity(): DeltaColumnBuilder = {\n    generatedByDefaultAsIdentity(start = 1, step = 1)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column as an identity column that allows user-specified values.\n   *\n   * @param start the start value of the identity column\n   * @param step  the increment step of the identity column\n   * @since 4.0.0\n   */\n  @Evolving\n  def generatedByDefaultAsIdentity(start: Long, step: Long): DeltaColumnBuilder = {\n    this.identityStart = Some(start)\n    this.identityStep = Some(step)\n    this.identityAllowExplicitInsert = Some(true)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column comment.\n   *\n   * @param comment string column description\n   * @since 4.0.0\n   */\n  @Evolving\n  def comment(comment: String): DeltaColumnBuilder = {\n    this.comment = Option(comment)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Build the column as a structField.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def build(): StructField = {\n    val metadataBuilder = new MetadataBuilder()\n    if (generationExpr.nonEmpty) {\n      metadataBuilder.putString(\"delta.generationExpression\", generationExpr.get)\n    }\n\n    identityAllowExplicitInsert.foreach { allowExplicitInsert =>\n      if (generationExpr.nonEmpty) {\n        throw DeltaTable.createAnalysisException(\n          \"IDENTITY column cannot be specified with a generated column expression.\")\n      }\n\n      if (dataType != null && dataType != LongType) {\n        throw DeltaTable.createAnalysisException(\n          s\"DataType ${dataType.typeName} is not supported for IDENTITY columns.\")\n      }\n\n      metadataBuilder.putBoolean(\"delta.identity.allowExplicitInsert\", allowExplicitInsert)\n      metadataBuilder.putLong(\"delta.identity.start\", identityStart.get)\n      if (identityStep.get == 0L) {\n        throw DeltaTable.createAnalysisException(\"IDENTITY column step cannot be 0.\")\n      }\n      metadataBuilder.putLong(\"delta.identity.step\", identityStep.get)\n    }\n\n    if (comment.nonEmpty) {\n      metadataBuilder.putString(\"comment\", comment.get)\n    }\n    val fieldMetadata = metadataBuilder.build()\n    if (dataType == null) {\n      throw DeltaTable.createAnalysisException(\n        s\"The data type of the column $colName is not provided\")\n    }\n    StructField(\n      colName,\n      dataType,\n      nullable = nullable,\n      metadata = fieldMetadata)\n  }\n}\n"
  },
  {
    "path": "spark-connect/client/src/main/scala/io/delta/connect/tables/DeltaMergeBuilder.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport java.util.Arrays\n\nimport scala.collection.JavaConverters._\nimport scala.collection.Map\n\nimport io.delta.connect.proto\nimport io.delta.connect.spark.{proto => spark_proto}\n\nimport org.apache.spark.annotation.Unstable\nimport org.apache.spark.sql.{functions, Column, DataFrame}\nimport org.apache.spark.sql.connect.ColumnNodeToProtoConverter.toExpr\nimport org.apache.spark.sql.connect.ConnectConversions._\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\nimport org.apache.spark.sql.functions.expr\n\n/**\n * Builder to specify how to merge data from source DataFrame into the target Delta table.\n * You can specify any number of `whenMatched` and `whenNotMatched` clauses.\n * Here are the constraints on these clauses.\n *\n *   - `whenMatched` clauses:\n *\n *     - The condition in a `whenMatched` clause is optional. However, if there are multiple\n *       `whenMatched` clauses, then only the last one may omit the condition.\n *\n *     - When there are more than one `whenMatched` clauses and there are conditions (or the lack\n *       of) such that a row satisfies multiple clauses, then the action for the first clause\n *       satisfied is executed. In other words, the order of the `whenMatched` clauses matters.\n *\n *     - If none of the `whenMatched` clauses match a source-target row pair that satisfy\n *       the merge condition, then the target rows will not be updated or deleted.\n *\n *     - If you want to update all the columns of the target Delta table with the\n *       corresponding column of the source DataFrame, then you can use the\n *       `whenMatched(...).updateAll()`. This is equivalent to\n *       <pre>\n *         whenMatched(...).updateExpr(Map(\n *           (\"col1\", \"source.col1\"),\n *           (\"col2\", \"source.col2\"),\n *           ...))\n *       </pre>\n *\n *   - `whenNotMatched` clauses:\n *\n *     - The condition in a `whenNotMatched` clause is optional. However, if there are\n *       multiple `whenNotMatched` clauses, then only the last one may omit the condition.\n *\n *     - When there are more than one `whenNotMatched` clauses and there are conditions (or the\n *       lack of) such that a row satisfies multiple clauses, then the action for the first clause\n *       satisfied is executed. In other words, the order of the `whenNotMatched` clauses matters.\n *\n *     - If no `whenNotMatched` clause is present or if it is present but the non-matching source\n *       row does not satisfy the condition, then the source row is not inserted.\n *\n *     - If you want to insert all the columns of the target Delta table with the\n *       corresponding column of the source DataFrame, then you can use\n *       `whenNotMatched(...).insertAll()`. This is equivalent to\n *       <pre>\n *         whenNotMatched(...).insertExpr(Map(\n *           (\"col1\", \"source.col1\"),\n *           (\"col2\", \"source.col2\"),\n *           ...))\n *       </pre>\n *\n *   - `whenNotMatchedBySource` clauses:\n *\n *     - The condition in a `whenNotMatchedBySource` clause is optional. However, if there are\n *       multiple `whenNotMatchedBySource` clauses, then only the last one may omit the condition.\n *\n *     - When there are more than one `whenNotMatchedBySource` clauses and there are conditions (or\n *       the lack of) such that a row satisfies multiple clauses, then the action for the first\n *       clause satisfied is executed. In other words, the order of the `whenNotMatchedBySource`\n *       clauses matters.\n *\n *     - If no `whenNotMatchedBySource` clause is present or if it is present but the\n *       non-matching target row does not satisfy any of the `whenNotMatchedBySource` clause\n *       condition, then the target row will not be updated or deleted.\n *\n *\n * Scala example to update a key-value Delta table with new key-values from a source DataFrame:\n * {{{\n *    deltaTable\n *     .as(\"target\")\n *     .merge(\n *       source.as(\"source\"),\n *       \"target.key = source.key\")\n *     .withSchemaEvolution()\n *     .whenMatched()\n *     .updateExpr(Map(\n *       \"value\" -> \"source.value\"))\n *     .whenNotMatched()\n *     .insertExpr(Map(\n *       \"key\" -> \"source.key\",\n *       \"value\" -> \"source.value\"))\n *     .whenNotMatchedBySource()\n *     .updateExpr(Map(\n *       \"value\" -> \"target.value + 1\"))\n *     .execute()\n * }}}\n *\n * Java example to update a key-value Delta table with new key-values from a source DataFrame:\n * {{{\n *    deltaTable\n *     .as(\"target\")\n *     .merge(\n *       source.as(\"source\"),\n *       \"target.key = source.key\")\n *     .withSchemaEvolution()\n *     .whenMatched()\n *     .updateExpr(\n *        new HashMap<String, String>() {{\n *          put(\"value\", \"source.value\");\n *        }})\n *     .whenNotMatched()\n *     .insertExpr(\n *        new HashMap<String, String>() {{\n *         put(\"key\", \"source.key\");\n *         put(\"value\", \"source.value\");\n *       }})\n *     .whenNotMatchedBySource()\n *     .updateExpr(\n *        new HashMap<String, String>() {{\n *         put(\"value\", \"target.value + 1\");\n *       }})\n *     .execute();\n * }}}\n *\n * @since 4.0.0\n */\nclass DeltaMergeBuilder private(\n    private val targetTable: DeltaTable,\n    private val source: DataFrame,\n    private val onCondition: Column,\n    private val whenMatchedClauses: Seq[proto.MergeIntoTable.Action],\n    private val whenNotMatchedClauses: Seq[proto.MergeIntoTable.Action],\n    private val whenNotMatchedBySourceClauses: Seq[proto.MergeIntoTable.Action],\n    private val schemaEvolutionEnabled: Boolean) {\n\n  // Schema Evolution is off by default in Merge.\n  def this(\n      targetTable: DeltaTable,\n      source: DataFrame,\n      onCondition: Column,\n      whenMatchedClauses: Seq[proto.MergeIntoTable.Action],\n      whenNotMatchedClauses: Seq[proto.MergeIntoTable.Action],\n      whenNotMatchedBySourceClauses: Seq[proto.MergeIntoTable.Action]) =\n    this(targetTable, source, onCondition, whenMatchedClauses,\n      whenNotMatchedClauses, whenNotMatchedBySourceClauses, schemaEvolutionEnabled = false)\n\n  /**\n   * Build the actions to perform when the merge condition was matched.  This returns\n   * [[DeltaMergeMatchedActionBuilder]] object which can be used to specify how\n   * to update or delete the matched target table row with the source row.\n   *\n   * @since 4.0.0\n   */\n  def whenMatched(): DeltaMergeMatchedActionBuilder = {\n    DeltaMergeMatchedActionBuilder(this, None)\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was matched and\n   * the given `condition` is true. This returns [[DeltaMergeMatchedActionBuilder]] object\n   * which can be used to specify how to update or delete the matched target table row with the\n   * source row.\n   *\n   * @param condition boolean expression as a SQL formatted string\n   * @since 4.0.0\n   */\n  def whenMatched(condition: String): DeltaMergeMatchedActionBuilder = {\n    whenMatched(expr(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was matched and\n   * the given `condition` is true. This returns a [[DeltaMergeMatchedActionBuilder]] object\n   * which can be used to specify how to update or delete the matched target table row with the\n   * source row.\n   *\n   * @param condition boolean expression as a Column object\n   * @since 4.0.0\n   */\n  def whenMatched(condition: Column): DeltaMergeMatchedActionBuilder = {\n    DeltaMergeMatchedActionBuilder(this, Some(condition))\n  }\n\n  /**\n   * Build the action to perform when the merge condition was not matched. This returns\n   * [[DeltaMergeNotMatchedActionBuilder]] object which can be used to specify how\n   * to insert the new sourced row into the target table.\n   * @since 4.0.0\n   */\n  def whenNotMatched(): DeltaMergeNotMatchedActionBuilder = {\n    DeltaMergeNotMatchedActionBuilder(this, None)\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched and\n   * the given `condition` is true. This returns [[DeltaMergeNotMatchedActionBuilder]] object\n   * which can be used to specify how to insert the new sourced row into the target table.\n   *\n   * @param condition boolean expression as a SQL formatted string\n   * @since 4.0.0\n   */\n  def whenNotMatched(condition: String): DeltaMergeNotMatchedActionBuilder = {\n    whenNotMatched(expr(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched and\n   * the given `condition` is true. This returns [[DeltaMergeNotMatchedActionBuilder]] object\n   * which can be used to specify how to insert the new sourced row into the target table.\n   *\n   * @param condition boolean expression as a Column object\n   * @since 4.0.0\n   */\n  def whenNotMatched(condition: Column): DeltaMergeNotMatchedActionBuilder = {\n    DeltaMergeNotMatchedActionBuilder(this, Some(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched by the source. This\n   * returns [[DeltaMergeNotMatchedBySourceActionBuilder]] object which can be used to specify how\n   * to update or delete the target table row.\n   * @since 4.0.0\n   */\n  def whenNotMatchedBySource(): DeltaMergeNotMatchedBySourceActionBuilder = {\n    DeltaMergeNotMatchedBySourceActionBuilder(this, None)\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched by the source and the\n   * given `condition` is true. This returns [[DeltaMergeNotMatchedBySourceActionBuilder]] object\n   * which can be used to specify how to update or delete the target table row.\n   *\n   * @param condition boolean expression as a SQL formatted string\n   * @since 4.0.0\n   */\n  def whenNotMatchedBySource(condition: String): DeltaMergeNotMatchedBySourceActionBuilder = {\n    whenNotMatchedBySource(expr(condition))\n  }\n\n  /**\n   * Build the actions to perform when the merge condition was not matched by the source and the\n   * given `condition` is true. This returns [[DeltaMergeNotMatchedBySourceActionBuilder]] object\n   * which can be used to specify how to update or delete the target table row .\n   *\n   * @param condition boolean expression as a Column object\n   * @since 4.0.0\n   */\n  def whenNotMatchedBySource(condition: Column): DeltaMergeNotMatchedBySourceActionBuilder = {\n    DeltaMergeNotMatchedBySourceActionBuilder(this, Some(condition))\n  }\n\n  /**\n   * Enable schema evolution for the merge operation. This allows the schema of the target\n   * table/columns to be automatically updated based on the schema of the source table/columns.\n   *\n   * @since 4.0.0\n   */\n  def withSchemaEvolution(): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(\n      this.targetTable,\n      this.source,\n      this.onCondition,\n      this.whenMatchedClauses,\n      this.whenNotMatchedClauses,\n      this.whenNotMatchedBySourceClauses,\n      schemaEvolutionEnabled = true)\n  }\n\n  /**\n   * Execute the merge operation based on the built matched and not matched actions.\n   *\n   * @since 4.0.0\n   */\n  def execute(): DataFrame = {\n    val sparkSession = targetTable.toDF.sparkSession\n    val merge = proto.MergeIntoTable\n      .newBuilder()\n      .setTarget(targetTable.toDF.plan.getRoot)\n      .setSource(source.plan.getRoot)\n      .setCondition(toExpr(onCondition))\n      .addAllMatchedActions(whenMatchedClauses.asJava)\n      .addAllNotMatchedActions(whenNotMatchedClauses.asJava)\n      .addAllNotMatchedBySourceActions(whenNotMatchedBySourceClauses.asJava)\n      .setWithSchemaEvolution(schemaEvolutionEnabled)\n    val relation = proto.DeltaRelation.newBuilder().setMergeIntoTable(merge).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    val resultDf = sparkSession.newDataFrame(_.mergeFrom(sparkRelation))\n    val resultSchema = resultDf.schema\n    // Ensure this is actually executed instead of just passing the DataFrame directly back to\n    // the caller, in case they just drop it. The return type used to be Unit so dropping is\n    // likely common.\n    val result = resultDf.collect()\n    sparkSession.createDataFrame(Arrays.asList(result: _*), resultSchema)\n  }\n\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def withWhenMatchedClause(\n      clause: proto.MergeIntoTable.Action): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(\n      this.targetTable,\n      this.source,\n      this.onCondition,\n      this.whenMatchedClauses :+ clause,\n      this.whenNotMatchedClauses,\n      this.whenNotMatchedBySourceClauses,\n      this.schemaEvolutionEnabled)\n  }\n\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def withWhenNotMatchedClause(\n      clause: proto.MergeIntoTable.Action): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(\n      this.targetTable,\n      this.source,\n      this.onCondition,\n      this.whenMatchedClauses,\n      this.whenNotMatchedClauses :+ clause,\n      this.whenNotMatchedBySourceClauses,\n      this.schemaEvolutionEnabled)\n  }\n\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def withWhenNotMatchedBySourceClause(\n      clause: proto.MergeIntoTable.Action): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(\n      this.targetTable,\n      this.source,\n      this.onCondition,\n      this.whenMatchedClauses,\n      this.whenNotMatchedClauses,\n      this.whenNotMatchedBySourceClauses :+ clause,\n      this.schemaEvolutionEnabled)\n  }\n}\n\nobject DeltaMergeBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      targetTable: DeltaTable, source: DataFrame, onCondition: Column): DeltaMergeBuilder = {\n    new DeltaMergeBuilder(targetTable, source, onCondition, Nil, Nil, Nil)\n  }\n}\n\n/**\n * Builder class to specify the actions to perform when a target table row has matched a\n * source row based on the given merge condition and optional match condition.\n *\n * See [[DeltaMergeBuilder]] for more information.\n *\n * @since 4.0.0\n */\nclass DeltaMergeMatchedActionBuilder private(\n    private val mergeBuilder: DeltaMergeBuilder,\n    private val matchCondition: Option[Column]) {\n\n  /**\n   * Update the matched table rows based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 4.0.0\n   */\n  def update(set: Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set)\n  }\n\n  /**\n   * Update the matched table rows based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 4.0.0\n   */\n  def updateExpr(set: Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set))\n  }\n\n  /**\n   * Update a matched table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as Column objects.\n   * @since 4.0.0\n   */\n  def update(set: java.util.Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set.asScala.toMap)\n  }\n\n  /**\n   * Update a matched table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as SQL formatted strings.\n   * @since 4.0.0\n   */\n  def updateExpr(set: java.util.Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set.asScala.toMap))\n  }\n\n  /**\n   * Update all the columns of the matched table row with the values of the\n   * corresponding columns in the source row.\n   *\n   * @since 4.0.0\n   */\n  def updateAll(): DeltaMergeBuilder = {\n    val clause = proto.MergeIntoTable.Action\n      .newBuilder()\n      .setUpdateStarAction(proto.MergeIntoTable.Action.UpdateStarAction.newBuilder())\n    matchCondition.foreach(c => clause.setCondition(toExpr(c)))\n    mergeBuilder.withWhenMatchedClause(clause.build())\n  }\n\n  /**\n   * Delete a matched row from the table.\n   *\n   * @since 4.0.0\n   */\n  def delete(): DeltaMergeBuilder = {\n    val clause = proto.MergeIntoTable.Action\n      .newBuilder()\n      .setDeleteAction(proto.MergeIntoTable.Action.DeleteAction.newBuilder())\n    matchCondition.foreach(c => clause.setCondition(toExpr(c)))\n    mergeBuilder.withWhenMatchedClause(clause.build())\n  }\n\n  private def addUpdateClause(set: Map[String, Column]): DeltaMergeBuilder = {\n    if (set.isEmpty && matchCondition.isEmpty) {\n      // This is a catch all clause that doesn't update anything: we can ignore it.\n      mergeBuilder\n    } else {\n      val assignments = set.map { case (field, value) =>\n        proto.Assignment.newBuilder().setField(toExpr(expr(field))).setValue(toExpr(value)).build()\n      }\n      val action = proto.MergeIntoTable.Action.UpdateAction\n        .newBuilder()\n        .addAllAssignments(assignments.asJava)\n      val clause = proto.MergeIntoTable.Action\n        .newBuilder()\n        .setUpdateAction(action)\n      matchCondition.foreach(c => clause.setCondition(toExpr(c)))\n      mergeBuilder.withWhenMatchedClause(clause.build())\n    }\n  }\n\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] =\n    map.mapValues(functions.expr).toMap\n}\n\nobject DeltaMergeMatchedActionBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      mergeBuilder: DeltaMergeBuilder,\n      matchCondition: Option[Column]): DeltaMergeMatchedActionBuilder = {\n    new DeltaMergeMatchedActionBuilder(mergeBuilder, matchCondition)\n  }\n}\n\n\n/**\n * Builder class to specify the actions to perform when a source row has not matched any target\n * Delta table row based on the merge condition, but has matched the additional condition\n * if specified.\n *\n * See [[DeltaMergeBuilder]] for more information.\n *\n * @since 4.0.0\n */\nclass DeltaMergeNotMatchedActionBuilder private(\n    private val mergeBuilder: DeltaMergeBuilder,\n    private val notMatchCondition: Option[Column]) {\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Scala map between target column names and\n   *               corresponding expressions as Column objects.\n   * @since 4.0.0\n   */\n  def insert(values: Map[String, Column]): DeltaMergeBuilder = {\n    addInsertClause(values)\n  }\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Scala map between target column names and\n   *               corresponding expressions as SQL formatted strings.\n   * @since 4.0.0\n   */\n  def insertExpr(values: Map[String, String]): DeltaMergeBuilder = {\n    addInsertClause(toStrColumnMap(values))\n  }\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Java map between target column names and\n   *               corresponding expressions as Column objects.\n   * @since 4.0.0\n   */\n  def insert(values: java.util.Map[String, Column]): DeltaMergeBuilder = {\n    addInsertClause(values.asScala)\n  }\n\n  /**\n   * Insert a new row to the target table based on the rules defined by `values`.\n   *\n   * @param values rules to insert a row as a Java map between target column names and\n   *               corresponding expressions as SQL formatted strings.\n   *\n   * @since 4.0.0\n   */\n  def insertExpr(values: java.util.Map[String, String]): DeltaMergeBuilder = {\n    addInsertClause(toStrColumnMap(values.asScala))\n  }\n\n  /**\n   * Insert a new target Delta table row by assigning the target columns to the values of the\n   * corresponding columns in the source row.\n   * @since 4.0.0\n   */\n  def insertAll(): DeltaMergeBuilder = {\n    val clause = proto.MergeIntoTable.Action\n      .newBuilder()\n      .setInsertStarAction(proto.MergeIntoTable.Action.InsertStarAction.newBuilder())\n    notMatchCondition.foreach(c => clause.setCondition(toExpr(c)))\n    mergeBuilder.withWhenNotMatchedClause(clause.build())\n  }\n\n  private def addInsertClause(setValues: Map[String, Column]): DeltaMergeBuilder = {\n    val assignments = setValues.map { case (field, value) =>\n      proto.Assignment.newBuilder().setField(toExpr(expr(field))).setValue(toExpr(value)).build()\n    }\n    val action = proto.MergeIntoTable.Action.InsertAction\n      .newBuilder()\n      .addAllAssignments(assignments.asJava)\n    val clause = proto.MergeIntoTable.Action\n      .newBuilder()\n      .setInsertAction(action)\n    notMatchCondition.foreach(c => clause.setCondition(toExpr(c)))\n    mergeBuilder.withWhenNotMatchedClause(clause.build())\n  }\n\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] =\n    map.mapValues(functions.expr).toMap\n}\n\nobject DeltaMergeNotMatchedActionBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      mergeBuilder: DeltaMergeBuilder,\n      notMatchCondition: Option[Column]): DeltaMergeNotMatchedActionBuilder = {\n    new DeltaMergeNotMatchedActionBuilder(mergeBuilder, notMatchCondition)\n  }\n}\n\n/**\n * Builder class to specify the actions to perform when a target table row has no match in the\n * source table based on the given merge condition and optional match condition.\n *\n * See [[DeltaMergeBuilder]] for more information.\n *\n * @since 4.0.0\n */\nclass DeltaMergeNotMatchedBySourceActionBuilder private(\n    private val mergeBuilder: DeltaMergeBuilder,\n    private val notMatchBySourceCondition: Option[Column]) {\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   * @since 4.0.0\n   */\n  def update(set: Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set)\n  }\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   * @since 4.0.0\n   */\n  def updateExpr(set: Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set))\n  }\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as Column objects.\n   * @since 4.0.0\n   */\n  def update(set: java.util.Map[String, Column]): DeltaMergeBuilder = {\n    addUpdateClause(set.asScala)\n  }\n\n  /**\n   * Update an unmatched target table row based on the rules defined by `set`.\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding expressions as SQL formatted strings.\n   * @since 4.0.0\n   */\n  def updateExpr(set: java.util.Map[String, String]): DeltaMergeBuilder = {\n    addUpdateClause(toStrColumnMap(set.asScala))\n  }\n\n  /**\n   * Delete an unmatched row from the target table.\n   * @since 4.0.0\n   */\n  def delete(): DeltaMergeBuilder = {\n    val clause = proto.MergeIntoTable.Action\n      .newBuilder()\n      .setDeleteAction(proto.MergeIntoTable.Action.DeleteAction.newBuilder())\n    notMatchBySourceCondition.foreach(c => clause.setCondition(toExpr(c)))\n    mergeBuilder.withWhenNotMatchedBySourceClause(clause.build())\n  }\n\n  private def addUpdateClause(set: Map[String, Column]): DeltaMergeBuilder = {\n    if (set.isEmpty && notMatchBySourceCondition.isEmpty) {\n      // This is a catch all clause that doesn't update anything: we can ignore it.\n      mergeBuilder\n    } else {\n      val assignments = set.map { case (field, value) =>\n        proto.Assignment.newBuilder().setField(toExpr(expr(field))).setValue(toExpr(value)).build()\n      }\n      val action = proto.MergeIntoTable.Action.UpdateAction\n        .newBuilder()\n        .addAllAssignments(assignments.asJava)\n      val clause = proto.MergeIntoTable.Action\n        .newBuilder()\n        .setUpdateAction(action)\n      notMatchBySourceCondition.foreach(c => clause.setCondition(toExpr(c)))\n      mergeBuilder.withWhenNotMatchedBySourceClause(clause.build())\n    }\n  }\n\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] =\n    map.mapValues(functions.expr).toMap\n}\n\nobject DeltaMergeNotMatchedBySourceActionBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      mergeBuilder: DeltaMergeBuilder,\n      notMatchBySourceCondition: Option[Column]): DeltaMergeNotMatchedBySourceActionBuilder = {\n    new DeltaMergeNotMatchedBySourceActionBuilder(mergeBuilder, notMatchBySourceCondition)\n  }\n}\n"
  },
  {
    "path": "spark-connect/client/src/main/scala/io/delta/connect/tables/DeltaOptimizeBuilder.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.connect.proto\nimport io.delta.connect.spark.{proto => spark_proto}\n\nimport org.apache.spark.annotation.Unstable\nimport org.apache.spark.sql.{DataFrame, SparkSession}\nimport org.apache.spark.sql.connect.ConnectConversions._\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\n\n/**\n * Builder class for constructing OPTIMIZE command and executing.\n *\n * @param sparkSession SparkSession to use for execution\n * @param tableIdentifier Id of the table on which to\n *        execute the optimize\n * @param options Hadoop file system options for read and write.\n * @since 4.0.0\n */\nclass DeltaOptimizeBuilder private(\n    private val sparkSession: SparkSession,\n    private val table: proto.DeltaTable) {\n  private var partitionFilters: Seq[String] = Seq.empty\n\n  /**\n   * Apply partition filter on this optimize command builder to limit\n   * the operation on selected partitions.\n   *\n   * @param partitionFilter The partition filter to apply\n   * @return [[DeltaOptimizeBuilder]] with partition filter applied\n   * @since 4.0.0\n   */\n  def where(partitionFilter: String): DeltaOptimizeBuilder = {\n    this.partitionFilters = this.partitionFilters :+ partitionFilter\n    this\n  }\n\n  /**\n   * Compact the small files in selected partitions.\n   *\n   * @return DataFrame containing the OPTIMIZE execution metrics\n   * @since 4.0.0\n   */\n  def executeCompaction(): DataFrame = {\n    execute(Seq.empty)\n  }\n\n  /**\n   * Z-Order the data in selected partitions using the given columns.\n   *\n   * @param columns Zero or more columns to order the data\n   *                using Z-Order curves\n   * @return DataFrame containing the OPTIMIZE execution metrics\n   * @since 4.0.0\n   */\n  @scala.annotation.varargs\n  def executeZOrderBy(columns: String*): DataFrame = {\n    execute(columns)\n  }\n\n  private def execute(zOrderBy: Seq[String]): DataFrame = {\n    val optimize = proto.OptimizeTable\n      .newBuilder()\n      .setTable(table)\n      .addAllPartitionFilters(partitionFilters.asJava)\n      .addAllZorderColumns(zOrderBy.asJava)\n    val relation = proto.DeltaRelation.newBuilder().setOptimizeTable(optimize).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    val result = sparkSession.newDataFrame(_.mergeFrom(sparkRelation)).collectResult()\n    val data = try {\n      result.toArray.toSeq.asJava\n    } finally {\n      result.close()\n    }\n    sparkSession.createDataFrame(data, result.schema)\n  }\n}\n\nprivate[delta] object DeltaOptimizeBuilder {\n  /**\n   * :: Unstable ::\n   *\n   * Private method for internal usage only. Do not call this directly.\n   */\n  @Unstable\n  private[delta] def apply(\n      sparkSession: SparkSession,\n      table: proto.DeltaTable): DeltaOptimizeBuilder = {\n    new DeltaOptimizeBuilder(sparkSession, table)\n  }\n}\n"
  },
  {
    "path": "spark-connect/client/src/main/scala/io/delta/connect/tables/DeltaTable.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.connect.proto\nimport io.delta.connect.spark.{proto => spark_proto}\nimport io.delta.tables.execution.{CreateTableOptions, ReplaceTableOptions}\n\nimport org.apache.spark.annotation.Evolving\nimport org.apache.spark.sql.{functions, AnalysisException, Column, DataFrame, Dataset, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.encoders.AgnosticEncoders.PrimitiveBooleanEncoder\nimport org.apache.spark.sql.connect.ColumnNodeToProtoConverter.toExpr\nimport org.apache.spark.sql.connect.ConnectConversions._\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\n\n/**\n * Main class for programmatically interacting with Delta tables.\n * You can create DeltaTable instances using the static methods.\n * {{{\n *   DeltaTable.forPath(sparkSession, pathToTheDeltaTable)\n * }}}\n *\n * @since 4.0.0\n */\nclass DeltaTable private[tables](\n    private val df: Dataset[Row],\n    private val table: proto.DeltaTable)\n  extends Serializable {\n\n  private def sparkSession: SparkSession = df.sparkSession\n\n  /**\n   * Apply an alias to the DeltaTable. This is similar to `Dataset.as(alias)` or\n   * SQL `tableName AS alias`.\n   *\n   * @since 4.0.0\n   */\n  def as(alias: String): DeltaTable = new DeltaTable(df.as(alias), table)\n\n  /**\n   * Apply an alias to the DeltaTable. This is similar to `Dataset.as(alias)` or\n   * SQL `tableName AS alias`.\n   *\n   * @since 4.0.0\n   */\n  def alias(alias: String): DeltaTable = as(alias)\n\n  /**\n   * Get a DataFrame (that is, Dataset[Row]) representation of this Delta table.\n   *\n   * @since 4.0.0\n   */\n  def toDF: Dataset[Row] = df\n\n  /**\n   * Helper method for the vacuum APIs.\n   *\n   * @param retentionHours The retention threshold in hours. Files required by the table for\n   *                       reading versions earlier than this will be preserved and the\n   *                       rest of them will be deleted.\n   *\n   * @since 4.0.0\n   */\n  private def executeVacuum(retentionHours: Option[Double]): DataFrame = {\n    val vacuum = proto.VacuumTable\n      .newBuilder()\n      .setTable(table)\n    retentionHours.foreach(vacuum.setRetentionHours)\n    val command = proto.DeltaCommand\n      .newBuilder()\n      .setVacuumTable(vacuum)\n      .build()\n    execute(command)\n    sparkSession.emptyDataFrame\n  }\n\n  /**\n   * Recursively delete files and directories in the table that are not needed by the table for\n   * maintaining older versions up to the given retention threshold. This method will return an\n   * empty DataFrame on successful completion.\n   *\n   * @param retentionHours The retention threshold in hours. Files required by the table for\n   *                       reading versions earlier than this will be preserved and the\n   *                       rest of them will be deleted.\n   * @since 4.0.0\n   */\n  def vacuum(retentionHours: Double): DataFrame = {\n    executeVacuum(Some(retentionHours))\n  }\n\n  /**\n   * Recursively delete files and directories in the table that are not needed by the table for\n   * maintaining older versions up to the given retention threshold. This method will return an\n   * empty DataFrame on successful completion.\n   *\n   * note: This will use the default retention period of 7 days.\n   *\n   * @since 4.0.0\n   */\n  def vacuum(): DataFrame = {\n    executeVacuum(None)\n  }\n\n  /**\n   * Helper method for the history APIs.\n   *\n   * @param limit The number of previous commands to get history for.\n   *\n   * @since 4.0.0\n   */\n  private def executeHistory(limit: Option[Int]): DataFrame = {\n    val describeHistory = proto.DescribeHistory\n      .newBuilder()\n      .setTable(table)\n    val relation = proto.DeltaRelation.newBuilder().setDescribeHistory(describeHistory).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    val df = sparkSession.newDataFrame(_.mergeFrom(sparkRelation))\n    limit match {\n      case Some(limit) => df.limit(limit)\n      case None => df\n    }\n  }\n\n  /**\n   * Get the information of the latest `limit` commits on this table as a Spark DataFrame.\n   * The information is in reverse chronological order.\n   *\n   * @param limit The number of previous commands to get history for.\n   *\n   * @since 4.0.0\n   */\n  def history(limit: Int): DataFrame = {\n    executeHistory(Some(limit))\n  }\n\n  /**\n   * Get the information available commits on this table as a Spark DataFrame.\n   * The information is in reverse chronological order.\n   *\n   * @since 4.0.0\n   */\n  def history(): DataFrame = {\n    executeHistory(limit = None)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Get the details of a Delta table such as the format, name, and size.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def detail(): DataFrame = {\n    val describeDetail = proto.DescribeDetail\n      .newBuilder()\n      .setTable(table)\n    val relation = proto.DeltaRelation.newBuilder().setDescribeDetail(describeDetail).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    sparkSession.newDataFrame(_.mergeFrom(sparkRelation))\n  }\n\n  /**\n   * Helper method for the delete APIs.\n   *\n   * @param condition Boolean SQL expression.\n   *\n   * @since 4.0.0\n   */\n  private def executeDelete(condition: Option[Column]): Unit = {\n    val delete = proto.DeleteFromTable\n      .newBuilder()\n      .setTarget(df.plan.getRoot)\n    condition.foreach(c => delete.setCondition(toExpr(c)))\n    val relation = proto.DeltaRelation.newBuilder().setDeleteFromTable(delete).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    sparkSession.newDataFrame(_.mergeFrom(sparkRelation)).collect()\n  }\n\n  /**\n   * Delete data from the table that match the given `condition`.\n   *\n   * @param condition Boolean SQL expression.\n   *\n   * @since 4.0.0\n   */\n  def delete(condition: String): Unit = {\n    delete(functions.expr(condition))\n  }\n\n  /**\n   * Delete data from the table that match the given `condition`.\n   *\n   * @param condition Boolean SQL expression.\n   *\n   * @since 4.0.0\n   */\n  def delete(condition: Column): Unit = {\n    executeDelete(condition = Some(condition))\n  }\n\n  /**\n   * Delete data from the table.\n   *\n   * @since 4.0.0\n   */\n  def delete(): Unit = {\n    executeDelete(condition = None)\n  }\n\n\n  /**\n   * Optimize the data layout of the table. This returns\n   * a [[DeltaOptimizeBuilder]] object that can be used to specify\n   * the partition filter to limit the scope of optimize and\n   * also execute different optimization techniques such as file\n   * compaction or order data using Z-Order curves.\n   *\n   * See the [[DeltaOptimizeBuilder]] for a full description\n   * of this operation.\n   *\n   * Scala example to run file compaction on a subset of\n   * partitions in the table:\n   * {{{\n   *    deltaTable\n   *     .optimize()\n   *     .where(\"date='2021-11-18'\")\n   *     .executeCompaction();\n   * }}}\n   *\n   * @since 4.0.0\n   */\n  def optimize(): DeltaOptimizeBuilder = {\n    DeltaOptimizeBuilder(sparkSession, table)\n  }\n\n  /**\n   * Helper method for the update APIs.\n   *\n   * @param condition boolean expression as Column object specifying which rows to update.\n   * @param set       rules to update a row as a Scala map between target column names and\n   *                  corresponding update expressions as Column objects.\n   *\n   * @since 4.0.0\n   */\n  private def executeUpdate(condition: Option[Column], set: Map[String, Column]): Unit = {\n    val assignments = set.toSeq.map { case (field, value) =>\n      proto.Assignment\n        .newBuilder()\n        .setField(toExpr(functions.expr(field)))\n        .setValue(toExpr(value))\n        .build()\n    }\n    val update = proto.UpdateTable\n      .newBuilder()\n      .setTarget(df.plan.getRoot)\n      .addAllAssignments(assignments.asJava)\n    condition.foreach(c => update.setCondition(toExpr(c)))\n    val relation = proto.DeltaRelation.newBuilder().setUpdateTable(update).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    sparkSession.newDataFrame(_.mergeFrom(sparkRelation)).collect()\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.functions._\n   *\n   *    deltaTable.update(Map(\"data\" -> col(\"data\") + 1))\n   * }}}\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as Column objects.\n   *\n   * @since 4.0.0\n   */\n  def update(set: Map[String, Column]): Unit = {\n    executeUpdate(condition = None, set)\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.Column;\n   *    import org.apache.spark.sql.functions;\n   *\n   *    deltaTable.update(\n   *      new HashMap<String, Column>() {{\n   *        put(\"data\", functions.col(\"data\").plus(1));\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding update expressions as Column objects.\n   *\n   * @since 4.0.0\n   */\n  def update(set: java.util.Map[String, Column]): Unit = {\n    update(set.asScala.asInstanceOf[Map[String, Column]])\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`\n   * based on the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.functions._\n   *\n   *    deltaTable.update(\n   *      col(\"date\") > \"2018-01-01\",\n   *      Map(\"data\" -> col(\"data\") + 1))\n   * }}}\n   *\n   * @param condition boolean expression as Column object specifying which rows to update.\n   * @param set       rules to update a row as a Scala map between target column names and\n   *                  corresponding update expressions as Column objects.\n   *\n   * @since 4.0.0\n   */\n  def update(condition: Column, set: Map[String, Column]): Unit = {\n    executeUpdate(Some(condition), set)\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`\n   * based on the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    import org.apache.spark.sql.Column;\n   *    import org.apache.spark.sql.functions;\n   *\n   *    deltaTable.update(\n   *      functions.col(\"date\").gt(\"2018-01-01\"),\n   *      new HashMap<String, Column>() {{\n   *        put(\"data\", functions.col(\"data\").plus(1));\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param condition boolean expression as Column object specifying which rows to update.\n   * @param set       rules to update a row as a Java map between target column names and\n   *                  corresponding update expressions as Column objects.\n   *\n   * @since 4.0.0\n   */\n  def update(condition: Column, set: java.util.Map[String, Column]): Unit = {\n    executeUpdate(Some(condition), set.asScala.toMap)\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    deltaTable.updateExpr(Map(\"data\" -> \"data + 1\")))\n   * }}}\n   *\n   * @param set rules to update a row as a Scala map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   *\n   * @since 4.0.0\n   */\n  def updateExpr(set: Map[String, String]): Unit = {\n    update(toStrColumnMap(set))\n  }\n\n  /**\n   * Update rows in the table based on the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    deltaTable.updateExpr(\n   *      new HashMap<String, String>() {{\n   *        put(\"data\", \"data + 1\");\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param set rules to update a row as a Java map between target column names and\n   *            corresponding update expressions as SQL formatted strings.\n   *\n   * @since 4.0.0\n   */\n  def updateExpr(set: java.util.Map[String, String]): Unit = {\n    update(toStrColumnMap(set.asScala.toMap))\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`,\n   * which performs the rules defined by `set`.\n   *\n   * Scala example to increment the column `data`.\n   * {{{\n   *    deltaTable.update(\n   *      \"date > '2018-01-01'\",\n   *      Map(\"data\" -> \"data + 1\"))\n   * }}}\n   *\n   * @param condition boolean expression as SQL formatted string object specifying\n   *                  which rows to update.\n   * @param set       rules to update a row as a Scala map between target column names and\n   *                  corresponding update expressions as SQL formatted strings.\n   *\n   * @since 4.0.0\n   */\n  def updateExpr(condition: String, set: Map[String, String]): Unit = {\n    executeUpdate(Some(functions.expr(condition)), toStrColumnMap(set))\n  }\n\n  /**\n   * Update data from the table on the rows that match the given `condition`,\n   * which performs the rules defined by `set`.\n   *\n   * Java example to increment the column `data`.\n   * {{{\n   *    deltaTable.update(\n   *      \"date > '2018-01-01'\",\n   *      new HashMap<String, String>() {{\n   *        put(\"data\", \"data + 1\");\n   *      }}\n   *    );\n   * }}}\n   *\n   * @param condition boolean expression as SQL formatted string object specifying\n   *                  which rows to update.\n   * @param set       rules to update a row as a Java map between target column names and\n   *                  corresponding update expressions as SQL formatted strings.\n   *\n   * @since 4.0.0\n   */\n  def updateExpr(condition: String, set: java.util.Map[String, String]): Unit = {\n    executeUpdate(Some(functions.expr(condition)), toStrColumnMap(set.asScala.toMap))\n  }\n\n  /**\n   * Merge data from the `source` DataFrame based on the given merge `condition`. This returns\n   * a [[DeltaMergeBuilder]] object that can be used to specify the update, delete, or insert\n   * actions to be performed on rows based on whether the rows matched the condition or not.\n   *\n   * See the [[DeltaMergeBuilder]] for a full description of this operation and what combinations of\n   * update, delete and insert operations are allowed.\n   *\n   * Scala example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(Map(\n   *       \"value\" -> \"source.value\"))\n   *     .whenNotMatched\n   *     .insertExpr(Map(\n   *       \"key\" -> \"source.key\",\n   *       \"value\" -> \"source.value\"))\n   *     .execute()\n   * }}}\n   *\n   * Java example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(\n   *        new HashMap<String, String>() {{\n   *          put(\"value\" -> \"source.value\");\n   *        }})\n   *     .whenNotMatched\n   *     .insertExpr(\n   *        new HashMap<String, String>() {{\n   *         put(\"key\", \"source.key\");\n   *         put(\"value\", \"source.value\");\n   *       }})\n   *     .execute();\n   * }}}\n   *\n   * @param source    source Dataframe to be merged.\n   * @param condition boolean expression as SQL formatted string\n   * @since 4.0.0\n   */\n  def merge(source: DataFrame, condition: String): DeltaMergeBuilder = {\n    merge(source, functions.expr(condition))\n  }\n\n  /**\n   * Merge data from the `source` DataFrame based on the given merge `condition`. This returns\n   * a [[DeltaMergeBuilder]] object that can be used to specify the update, delete, or insert\n   * actions to be performed on rows based on whether the rows matched the condition or not.\n   *\n   * See the [[DeltaMergeBuilder]] for a full description of this operation and what combinations of\n   * update, delete and insert operations are allowed.\n   *\n   * Scala example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(Map(\n   *       \"value\" -> \"source.value\"))\n   *     .whenNotMatched\n   *     .insertExpr(Map(\n   *       \"key\" -> \"source.key\",\n   *       \"value\" -> \"source.value\"))\n   *     .execute()\n   * }}}\n   *\n   * Java example to update a key-value Delta table with new key-values from a source DataFrame:\n   * {{{\n   *    deltaTable\n   *     .as(\"target\")\n   *     .merge(\n   *       source.as(\"source\"),\n   *       \"target.key = source.key\")\n   *     .whenMatched\n   *     .updateExpr(\n   *        new HashMap<String, String>() {{\n   *          put(\"value\" -> \"source.value\")\n   *        }})\n   *     .whenNotMatched\n   *     .insertExpr(\n   *        new HashMap<String, String>() {{\n   *         put(\"key\", \"source.key\");\n   *         put(\"value\", \"source.value\");\n   *       }})\n   *     .execute()\n   * }}}\n   *\n   * @param source    source Dataframe to be merged.\n   * @param condition boolean expression as a Column object\n   * @since 4.0.0\n   */\n  def merge(source: DataFrame, condition: Column): DeltaMergeBuilder = {\n    DeltaMergeBuilder(this, source, condition)\n  }\n\n  private def executeClone(\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String],\n      versionAsOf: Option[Int] = None,\n      timestampAsOf: Option[String] = None): DeltaTable = {\n    val clone = proto.CloneTable\n      .newBuilder()\n      .setTable(table)\n      .setTarget(target)\n      .setIsShallow(isShallow)\n      .setReplace(replace)\n      .putAllProperties(properties.asJava)\n    versionAsOf.foreach(clone.setVersion)\n    timestampAsOf.foreach(clone.setTimestamp)\n    val command = proto.DeltaCommand.newBuilder().setCloneTable(clone).build()\n    execute(command)\n    DeltaTable.forPath(sparkSession, target)\n  }\n\n  /**\n   * Clone a DeltaTable to a given destination to mirror the existing table's data and metadata.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *  io.delta.tables.DeltaTable.clone(\n   *   \"/some/path/to/table\",\n   *   true,\n   *   true,\n   *   Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   *\n   * @since 4.0.0\n   */\n  def clone(\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String]): DeltaTable = {\n    executeClone(target, isShallow, replace, properties, versionAsOf = None, timestampAsOf = None)\n  }\n\n  /**\n   * Clone a DeltaTable to a given destination to mirror the existing table's data and metadata.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.clone(\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true)\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   *\n   * @since 4.0.0\n   */\n  def clone(target: String, isShallow: Boolean, replace: Boolean): DeltaTable = {\n    clone(target, isShallow, replace, properties = Map.empty[String, String])\n  }\n\n  /**\n   * Clone a DeltaTable to a given destination to mirror the existing table's data and metadata.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.clone(\n   *     \"/some/path/to/table\",\n   *     true)\n   * }}}\n   *\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   *\n   * @since 4.0.0\n   */\n  def clone(target: String, isShallow: Boolean): DeltaTable = {\n    clone(target, isShallow, replace = false)\n  }\n\n  /**\n   * Clone a DeltaTable at a specific version to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   *\n   * @since 4.0.0\n   */\n  def cloneAtVersion(\n      version: Int,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String]): DeltaTable = {\n    executeClone(\n      target, isShallow, replace, properties,\n      versionAsOf = Some(version), timestampAsOf = None)\n  }\n\n  /**\n   * Clone a DeltaTable at a specific version to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true)\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   *\n   * @since 4.0.0\n   */\n  def cloneAtVersion(\n      version: Int, target: String, isShallow: Boolean, replace: Boolean): DeltaTable = {\n    cloneAtVersion(version, target, isShallow, replace, properties = Map.empty[String, String])\n  }\n\n  /**\n   * Clone a DeltaTable at a specific version to a given destination to mirror the existing\n   * table's data and metadata at that version.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtVersion(\n   *     5,\n   *     \"/some/path/to/table\",\n   *     true)\n   * }}}\n   *\n   * @param version The version of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   *\n   * @since 4.0.0\n   */\n  def cloneAtVersion(version: Int, target: String, isShallow: Boolean): DeltaTable = {\n    cloneAtVersion(version, target, isShallow, replace = false)\n  }\n\n  /**\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss.\n   *\n   * Specifying properties here means that the target will override any properties with the same key\n   * in the source table with the user-defined properties.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtTimestamp(\n   *     \"2019-01-01\",\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true,\n   *     Map(\"foo\" -> \"bar\"))\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   * @param properties The table properties to override in the clone.\n   *\n   * @since 4.0.0\n   */\n  def cloneAtTimestamp(\n      timestamp: String,\n      target: String,\n      isShallow: Boolean,\n      replace: Boolean,\n      properties: Map[String, String]): DeltaTable = {\n    executeClone(\n      target, isShallow, replace, properties, versionAsOf = None, timestampAsOf = Some(timestamp))\n  }\n\n  /**\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtTimestamp(\n   *     \"2019-01-01\",\n   *     \"/some/path/to/table\",\n   *     true,\n   *     true)\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   * @param replace Whether to replace the destination with the clone command.\n   *\n   * @since 4.0.0\n   */\n  def cloneAtTimestamp(\n      timestamp: String, target: String, isShallow: Boolean, replace: Boolean): DeltaTable = {\n    cloneAtTimestamp(timestamp, target, isShallow, replace, properties = Map.empty[String, String])\n  }\n\n  /**\n   * Clone a DeltaTable at a specific timestamp to a given destination to mirror the existing\n   * table's data and metadata at that timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss.\n   *\n   * An example would be\n   * {{{\n   *   io.delta.tables.DeltaTable.cloneAtTimestamp(\n   *     \"2019-01-01\",\n   *     \"/some/path/to/table\",\n   *     true)\n   * }}}\n   *\n   * @param timestamp The timestamp of this table to clone from.\n   * @param target The path or table name to create the clone.\n   * @param isShallow Whether to create a shallow clone or a deep clone.\n   *\n   * @since 4.0.0\n   */\n  def cloneAtTimestamp(timestamp: String, target: String, isShallow: Boolean): DeltaTable = {\n    cloneAtTimestamp(timestamp, target, isShallow, replace = false)\n  }\n\n  /**\n   * Helper method for the restoreToVersion and restoreToTimestamp APIs.\n   *\n   * @param version The version number of the older version of the table to restore to.\n   * @param timestamp The timestamp of the older version of the table to restore to.\n   *\n   * @since 4.0.0\n   */\n  private def executeRestore(version: Option[Long], timestamp: Option[String]): DataFrame = {\n    val restore = proto.RestoreTable\n      .newBuilder()\n      .setTable(table)\n    version.foreach(restore.setVersion)\n    timestamp.foreach(restore.setTimestamp)\n    val relation = proto.DeltaRelation.newBuilder().setRestoreTable(restore).build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    val result = sparkSession.newDataFrame(_.mergeFrom(sparkRelation)).collectResult()\n    val data = try {\n      result.toArray.toSeq.asJava\n    } finally {\n      result.close()\n    }\n    sparkSession.createDataFrame(data, result.schema)\n  }\n\n  /**\n   * Restore the DeltaTable to an older version of the table specified by version number.\n   *\n   * An example would be\n   * {{{ io.delta.tables.DeltaTable.restoreToVersion(7) }}}\n   *\n   * @since 4.0.0\n   */\n  def restoreToVersion(version: Long): DataFrame = {\n    executeRestore(version = Some(version), timestamp = None)\n  }\n\n  /**\n   * Restore the DeltaTable to an older version of the table specified by a timestamp.\n   *\n   * Timestamp can be of the format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss\n   *\n   * An example would be\n   * {{{ io.delta.tables.DeltaTable.restoreToTimestamp(\"2019-01-01\") }}}\n   *\n   * @since 4.0.0\n   */\n  def restoreToTimestamp(timestamp: String): DataFrame = {\n    executeRestore(version = None, timestamp = Some(timestamp))\n  }\n\n  /**\n   * Converts a map of strings to expressions as SQL formatted string\n   * into a map of strings to Column objects.\n   *\n   * @param map A map where the value is an expression as SQL formatted string.\n   * @return A map where the value is a Column object created from the expression.\n   */\n  private def toStrColumnMap(map: Map[String, String]): Map[String, Column] = {\n    map.toSeq.map { case (k, v) => k -> functions.expr(v) }.toMap\n  }\n\n  /**\n   * Generate a manifest for the given Delta Table\n   *\n   * @param mode Specifies the mode for the generation of the manifest.\n   *             The valid modes are as follows (not case sensitive):\n   *              - \"symlink_format_manifest\" : This will generate manifests in symlink format\n   *                for Presto and Athena read support.\n   *                See the online documentation for more information.\n   * @since 4.0.0\n   */\n  def generate(mode: String): Unit = {\n    val generate = proto.Generate\n      .newBuilder()\n      .setTable(table)\n      .setMode(mode)\n    val command = proto.DeltaCommand.newBuilder().setGenerate(generate).build()\n    execute(command)\n  }\n\n  /**\n   * Updates the protocol version of the table to leverage new features. Upgrading the reader\n   * version will prevent all clients that have an older version of Delta Lake from accessing this\n   * table. Upgrading the writer version will prevent older versions of Delta Lake to write to this\n   * table. The reader or writer version cannot be downgraded.\n   *\n   * See online documentation and Delta's protocol specification at PROTOCOL.md for more details.\n   *\n   * @since 4.0.0\n   */\n  def upgradeTableProtocol(readerVersion: Int, writerVersion: Int): Unit = {\n    val upgrade = proto.UpgradeTableProtocol\n      .newBuilder()\n      .setTable(table)\n      .setReaderVersion(readerVersion)\n      .setWriterVersion(writerVersion)\n    val command = proto.DeltaCommand.newBuilder().setUpgradeTableProtocol(upgrade).build()\n    execute(command)\n  }\n\n  /**\n   * Modify the protocol to add a supported feature, and if the table does not support table\n   * features, upgrade the protocol automatically. In such a case when the provided feature is\n   * writer-only, the table's writer version will be upgraded to `7`, and when the provided\n   * feature is reader-writer, both reader and writer versions will be upgraded, to `(3, 7)`.\n   *\n   * See online documentation and Delta's protocol specification at PROTOCOL.md for more details.\n   *\n   * @since 4.0.0\n   */\n  def addFeatureSupport(featureName: String): Unit = {\n    val addFeatureSupport = proto.AddFeatureSupport\n      .newBuilder()\n      .setTable(table)\n      .setFeatureName(featureName)\n    val command = proto.DeltaCommand.newBuilder().setAddFeatureSupport(addFeatureSupport).build()\n    execute(command)\n  }\n\n  private def executeDropFeature(featureName: String, truncateHistory: Option[Boolean]): Unit = {\n    val dropFeatureSupport = proto.DropFeatureSupport\n      .newBuilder()\n      .setTable(table)\n      .setFeatureName(featureName)\n    truncateHistory.foreach(dropFeatureSupport.setTruncateHistory)\n    val command = proto.DeltaCommand.newBuilder().setDropFeatureSupport(dropFeatureSupport).build()\n    execute(command)\n  }\n\n  /**\n   * Modify the protocol to drop a supported feature. The operation always normalizes the\n   * resulting protocol. Protocol normalization is the process of converting a table features\n   * protocol to the weakest possible form. This primarily refers to converting a table features\n   * protocol to a legacy protocol. A table features protocol can be represented with the legacy\n   * representation only when the feature set of the former exactly matches a legacy protocol.\n   * Normalization can also decrease the reader version of a table features protocol when it is\n   * higher than necessary. For example:\n   *\n   * (1, 7, None, {AppendOnly, Invariants, CheckConstraints}) -> (1, 3)\n   * (3, 7, None, {RowTracking}) -> (1, 7, RowTracking)\n   *\n   * The dropFeatureSupport method can be used as follows:\n   * {{{\n   *   io.delta.tables.DeltaTable.dropFeatureSupport(\"rowTracking\")\n   * }}}\n   *\n   * See online documentation for more details.\n   *\n   * @param featureName The name of the feature to drop.\n   * @param truncateHistory Whether to truncate history before downgrading the protocol.\n   * @return None.\n   * @since 4.0.0\n   */\n  def dropFeatureSupport(featureName: String, truncateHistory: Boolean): Unit = {\n    executeDropFeature(featureName, Some(truncateHistory))\n  }\n\n  /**\n   * Modify the protocol to drop a supported feature. The operation always normalizes the\n   * resulting protocol. Protocol normalization is the process of converting a table features\n   * protocol to the weakest possible form. This primarily refers to converting a table features\n   * protocol to a legacy protocol. A table features protocol can be represented with the legacy\n   * representation only when the feature set of the former exactly matches a legacy protocol.\n   * Normalization can also decrease the reader version of a table features protocol when it is\n   * higher than necessary. For example:\n   *\n   * (1, 7, None, {AppendOnly, Invariants, CheckConstraints}) -> (1, 3)\n   * (3, 7, None, {RowTracking}) -> (1, 7, RowTracking)\n   *\n   * The dropFeatureSupport method can be used as follows:\n   * {{{\n   *   io.delta.tables.DeltaTable.dropFeatureSupport(\"rowTracking\")\n   * }}}\n   *\n   * Note, this command will not truncate history.\n   *\n   * See online documentation for more details.\n   *\n   *\n   * @param featureName The name of the feature to drop.\n   * @return None.\n   * @since 4.0.0\n   */\n  def dropFeatureSupport(featureName: String): Unit = {\n    executeDropFeature(featureName, None)\n  }\n\n  private def execute(command: proto.DeltaCommand): Unit = {\n    val extension = com.google.protobuf.Any.pack(command)\n    val sparkCommand = spark_proto.Command\n      .newBuilder()\n      .setExtension(extension)\n      .build()\n    sparkSession.execute(sparkCommand)\n  }\n}\n\n/**\n * Companion object to create DeltaTable instances.\n *\n * {{{\n *   DeltaTable.forPath(sparkSession, pathToTheDeltaTable)\n * }}}\n *\n * @since 4.0.0\n */\nobject DeltaTable {\n  /**\n   * Helper method to get the active SparkSession.\n   *\n   * @return The active SparkSession if one exists.\n   * @throws IllegalArgumentException if no active SparkSession is found.\n   */\n  private def getActiveSparkSession(): SparkSession = {\n    SparkSession.getActiveSession.getOrElse {\n      throw new IllegalArgumentException(\"Could not find active SparkSession\")\n    }\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object representing the data at the given path, If the given\n   * path is invalid (i.e. either no table exists or an existing table is not a Delta table),\n   * it throws a `not a Delta table` error.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 4.0.0\n   */\n  def forPath(path: String): DeltaTable = {\n    forPath(getActiveSparkSession(), path)\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object representing the data at the given path, If the given\n   * path is invalid (i.e. either no table exists or an existing table is not a Delta table),\n   * it throws a `not a Delta table` error.\n   *\n   * @since 4.0.0\n   */\n  def forPath(sparkSession: SparkSession, path: String): DeltaTable = {\n    forPath(sparkSession, path, Map.empty[String, String])\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object representing the data at the given path, If the given\n   * path is invalid (i.e. either no table exists or an existing table is not a Delta table),\n   * it throws a `not a Delta table` error.\n   *\n   * @param hadoopConf Hadoop configuration starting with \"fs.\" or \"dfs.\" will be picked up\n   *                   by `DeltaTable` to access the file system when executing queries.\n   *                   Other configurations will not be allowed.\n   *\n   * {{{\n   *   val hadoopConf = Map(\n   *     \"fs.s3a.access.key\" -> \"<access-key>\",\n   *     \"fs.s3a.secret.key\" -> \"<secret-key>\"\n   *   )\n   *   DeltaTable.forPath(spark, \"/path/to/table\", hadoopConf)\n   * }}}\n   *\n   * @since 4.0.0\n   */\n  def forPath(\n      sparkSession: SparkSession,\n      path: String,\n      hadoopConf: scala.collection.Map[String, String]): DeltaTable = {\n    val table = proto.DeltaTable\n      .newBuilder()\n      .setPath(\n        proto.DeltaTable.Path\n          .newBuilder().setPath(path)\n          .putAllHadoopConf(hadoopConf.asJava))\n      .build()\n    forTable(sparkSession, table)\n  }\n\n  /**\n   * Java friendly API to instantiate a [[DeltaTable]] object representing the data at the given\n   * path, If the given path is invalid (i.e. either no table exists or an existing table is not a\n   * Delta table), it throws a `not a Delta table` error.\n   *\n   * @param hadoopConf Hadoop configuration starting with \"fs.\" or \"dfs.\" will be picked up\n   *                   by `DeltaTable` to access the file system when executing queries.\n   *                   Other configurations will be ignored.\n   *\n   * {{{\n   *   val hadoopConf = Map(\n   *     \"fs.s3a.access.key\" -> \"<access-key>\",\n   *     \"fs.s3a.secret.key\", \"<secret-key>\"\n   *   )\n   *   DeltaTable.forPath(spark, \"/path/to/table\", hadoopConf)\n   * }}}\n   *\n   * @since 4.0.0\n   */\n  def forPath(\n      sparkSession: SparkSession,\n      path: String,\n      hadoopConf: java.util.Map[String, String]): DeltaTable = {\n    val fsOptions = hadoopConf.asScala.toMap\n    forPath(sparkSession, path, fsOptions)\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object using the given table name. If the given\n   * tableOrViewName is invalid (i.e. either no table exists or an existing table is not a\n   * Delta table), it throws a `not a Delta table` error. Note: Passing a view name will also\n   * result in this error as views are not supported.\n   *\n   * The given tableOrViewName can also be the absolute path of a delta datasource (i.e.\n   * delta.`path`), If so, instantiate a [[DeltaTable]] object representing the data at\n   * the given path (consistent with the [[forPath]]).\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 4.0.0\n   */\n  def forName(tableOrViewName: String): DeltaTable = {\n    forName(getActiveSparkSession(), tableOrViewName)\n  }\n\n  /**\n   * Instantiate a [[DeltaTable]] object using the given table name using the given\n   * SparkSession. If the given tableName is invalid (i.e. either no table exists or an\n   * existing table is not a Delta table), it throws a `not a Delta table` error. Note:\n   * Passing a view name will also result in this error as views are not supported.\n   *\n   * The given tableName can also be the absolute path of a delta datasource (i.e.\n   * delta.`path`), If so, instantiate a [[DeltaTable]] object representing the data at\n   * the given path (consistent with the [[forPath]]).\n   *\n   * @since 4.0.0\n   */\n  def forName(sparkSession: SparkSession, tableName: String): DeltaTable = {\n    val table = proto.DeltaTable\n      .newBuilder()\n      .setTableOrViewName(tableName)\n      .build()\n    forTable(sparkSession, table)\n  }\n\n  private def forTable(sparkSession: SparkSession, table: proto.DeltaTable): DeltaTable = {\n    val relation = proto.DeltaRelation\n      .newBuilder()\n      .setScan(proto.Scan.newBuilder().setTable(table))\n      .build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    val df = sparkSession.newDataFrame(_.mergeFrom(sparkRelation))\n    new DeltaTable(df, table)\n  }\n\n  /**\n   * Check if the provided `identifier` string, in this case a file path,\n   * is the root of a Delta table using the given SparkSession.\n   *\n   * An example would be\n   * {{{\n   *   DeltaTable.isDeltaTable(spark, \"path/to/table\")\n   * }}}\n   *\n   * @since 4.0.0\n   */\n  def isDeltaTable(sparkSession: SparkSession, identifier: String): Boolean = {\n    val relation = proto.DeltaRelation\n      .newBuilder()\n      .setIsDeltaTable(proto.IsDeltaTable.newBuilder().setPath(identifier))\n      .build()\n    val extension = com.google.protobuf.Any.pack(relation)\n    val sparkRelation = spark_proto.Relation.newBuilder().setExtension(extension).build()\n    sparkSession.newDataset(PrimitiveBooleanEncoder)(_.mergeFrom(sparkRelation)).head()\n  }\n\n  /**\n   * Check if the provided `identifier` string, in this case a file path,\n   * is the root of a Delta table.\n   *\n   * Note: This uses the active SparkSession in the current thread to search for the table. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * An example would be\n   * {{{\n   *   DeltaTable.isDeltaTable(spark, \"/path/to/table\")\n   * }}}\n   *\n   * @since 4.0.0\n   */\n  def isDeltaTable(identifier: String): Boolean = {\n    isDeltaTable(getActiveSparkSession(), identifier)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * error if the table exists (the same as SQL `CREATE TABLE`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def create(): DeltaTableBuilder = {\n    create(getActiveSparkSession())\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * error if the table exists (the same as SQL `CREATE TABLE`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @since 4.0.0\n   */\n  @Evolving\n  def create(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, CreateTableOptions(ifNotExists = false))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * if it does not exists (the same as SQL `CREATE TABLE IF NOT EXISTS`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def createIfNotExists(): DeltaTableBuilder = {\n    createIfNotExists(getActiveSparkSession())\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to create a Delta table,\n   * if it does not exists (the same as SQL `CREATE TABLE IF NOT EXISTS`).\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @since 4.0.0\n   */\n  @Evolving\n  def createIfNotExists(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, CreateTableOptions(ifNotExists = true))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table,\n   * error if the table doesn't exist (the same as SQL `REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def replace(): DeltaTableBuilder = {\n    replace(getActiveSparkSession())\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table,\n   * error if the table doesn't exist (the same as SQL `REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user\n   * @since 4.0.0\n   */\n  @Evolving\n  def replace(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, ReplaceTableOptions(orCreate = false))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table\n   * or create table if not exists (the same as SQL `CREATE OR REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def createOrReplace(): DeltaTableBuilder = {\n    createOrReplace(getActiveSparkSession())\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaTableBuilder]] to replace a Delta table,\n   * or create table if not exists (the same as SQL `CREATE OR REPLACE TABLE`)\n   * Refer to [[DeltaTableBuilder]] for more details.\n   *\n   * @param spark sparkSession sparkSession passed by the user.\n   * @since 4.0.0\n   */\n  @Evolving\n  def createOrReplace(spark: SparkSession): DeltaTableBuilder = {\n    new DeltaTableBuilder(spark, ReplaceTableOptions(orCreate = true))\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaColumnBuilder]] to specify a column.\n   * Refer to [[DeltaTableBuilder]] for examples and [[DeltaColumnBuilder]] detailed APIs.\n   *\n   * Note: This uses the active SparkSession in the current thread to read the table data. Hence,\n   * this throws error if active SparkSession has not been set, that is,\n   * `SparkSession.getActiveSession()` is empty.\n   *\n   * @param colName string the column name\n   * @since 4.0.0\n   */\n  @Evolving\n  def columnBuilder(colName: String): DeltaColumnBuilder = {\n    new DeltaColumnBuilder(colName)\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Return an instance of [[DeltaColumnBuilder]] to specify a column.\n   * Refer to [[DeltaTableBuilder]] for examples and [[DeltaColumnBuilder]] detailed APIs.\n   *\n   * @param spark   sparkSession sparkSession passed by the user\n   * @param colName string the column name\n   * @since 4.0.0\n   */\n  @Evolving\n  def columnBuilder(spark: SparkSession, colName: String): DeltaColumnBuilder = {\n    new DeltaColumnBuilder(colName)\n  }\n\n  private[tables] def createAnalysisException(message: String): AnalysisException = {\n    // TODO: We should refactor this to use DeltaErrors. Until then, we need to use a dummy Spark\n    // error class to initialize the AnalysisException, which we then remove in the copy method.\n    new AnalysisException(\n      errorClass = \"ALL_PARTITION_COLUMNS_NOT_ALLOWED\",\n      messageParameters = Map(\"message\" -> message)).copy(\n        message = message,\n        errorClass = None,\n        messageParameters = Map.empty)\n  }\n}\n"
  },
  {
    "path": "spark-connect/client/src/main/scala/io/delta/connect/tables/DeltaTableBuilder.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\n\nimport io.delta.connect.proto\nimport io.delta.connect.spark.{proto => spark_proto}\nimport io.delta.tables.execution.{CreateTableOptions, DeltaTableBuilderOptions, ReplaceTableOptions}\n\nimport org.apache.spark.annotation.Evolving\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.connect.ConnectConversions._\nimport org.apache.spark.sql.connect.common.DataTypeProtoConverter\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\nimport org.apache.spark.sql.types.{DataType, StructField, StructType}\n\n/**\n * :: Evolving ::\n *\n * Builder to specify how to create / replace a Delta table.\n * You must specify the table name or the path before executing the builder.\n * You can specify the table columns, the partitioning columns, the location of the data,\n * the table comment and the property, and how you want to create / replace the Delta table.\n *\n * After executing the builder, an instance of [[DeltaTable]] is returned.\n *\n * Scala example to create a Delta table with generated columns, using the table name:\n * {{{\n *   val table: DeltaTable = DeltaTable.create()\n *     .tableName(\"testTable\")\n *     .addColumn(\"c1\",  dataType = \"INT\", nullable = false)\n *     .addColumn(\n *       DeltaTable.columnBuilder(\"c2\")\n *         .dataType(\"INT\")\n *         .generatedAlwaysAs(\"c1 + 10\")\n *         .build()\n *     )\n *     .addColumn(\n *       DeltaTable.columnBuilder(\"c3\")\n *         .dataType(\"INT\")\n *         .comment(\"comment\")\n *         .nullable(true)\n *         .build()\n *     )\n *     .partitionedBy(\"c1\", \"c2\")\n *     .execute()\n * }}}\n *\n * Scala example to create a delta table using the location:\n * {{{\n *   val table: DeltaTable = DeltaTable.createIfNotExists(spark)\n *     .location(\"/foo/`bar`\")\n *     .addColumn(\"c1\", dataType = \"INT\", nullable = false)\n *     .addColumn(\n *       DeltaTable.columnBuilder(spark, \"c2\")\n *         .dataType(\"INT\")\n *         .generatedAlwaysAs(\"c1 + 10\")\n *         .build()\n *     )\n *     .addColumn(\n *       DeltaTable.columnBuilder(spark, \"c3\")\n *         .dataType(\"INT\")\n *         .comment(\"comment\")\n *         .nullable(true)\n *         .build()\n *     )\n *     .partitionedBy(\"c1\", \"c2\")\n *     .execute()\n * }}}\n *\n * Java Example to replace a table:\n * {{{\n *   DeltaTable table = DeltaTable.replace()\n *     .tableName(\"db.table\")\n *     .addColumn(\"c1\",  \"INT\", false)\n *     .addColumn(\n *       DeltaTable.columnBuilder(\"c2\")\n *         .dataType(\"INT\")\n *         .generatedAlwaysBy(\"c1 + 10\")\n *         .build()\n *     )\n *     .execute();\n * }}}\n *\n * @since 4.0.0\n */\n@Evolving\nclass DeltaTableBuilder private[tables](\n    spark: SparkSession,\n    builderOption: DeltaTableBuilderOptions) {\n  private var identifier: Option[String] = None\n  private var partitioningColumns: Seq[String] = Nil\n  private var clusteringColumns: Seq[String] = Nil\n  private var columns: mutable.Seq[StructField] = mutable.Seq.empty\n  private var location: Option[String] = None\n  private var tblComment: Option[String] = None\n  private var properties = Map.empty[String, String]\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the table name, optionally qualified with a database name [database_name.] table_name\n   *\n   * @param identifier string the table name\n   * @since 4.0.0\n   */\n  @Evolving\n  def tableName(identifier: String): DeltaTableBuilder = {\n    this.identifier = Some(identifier)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the table comment to describe the table.\n   *\n   * @param comment string table comment\n   * @since 4.0.0\n   */\n  @Evolving\n  def comment(comment: String): DeltaTableBuilder = {\n    tblComment = Option(comment)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the path to the directory where table data is stored,\n   * which could be a path on distributed storage.\n   *\n   * @param location string the data location\n   * @since 4.0.0\n   */\n  @Evolving\n  def location(location: String): DeltaTableBuilder = {\n    this.location = Option(location)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType string the DDL data type\n   * @since 4.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: String): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType dataType the DDL data type\n   * @since 4.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: DataType): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType string the DDL data type\n   * @param nullable boolean whether the column is nullable\n   * @since 4.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: String, nullable: Boolean): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).nullable(nullable).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param colName string the column name\n   * @param dataType dataType the DDL data type\n   * @param nullable boolean whether the column is nullable\n   * @since 4.0.0\n   */\n  @Evolving\n  def addColumn(colName: String, dataType: DataType, nullable: Boolean): DeltaTableBuilder = {\n    addColumn(\n      DeltaTable.columnBuilder(spark, colName).dataType(dataType).nullable(nullable).build()\n    )\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a column.\n   *\n   * @param col structField the column struct\n   * @since 4.0.0\n   */\n  @Evolving\n  def addColumn(col: StructField): DeltaTableBuilder = {\n    columns = columns :+ col\n    this\n  }\n\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify columns with an existing schema.\n   *\n   * @param cols structType the existing schema for columns\n   * @since 4.0.0\n   */\n  @Evolving\n  def addColumns(cols: StructType): DeltaTableBuilder = {\n    columns = columns ++ cols\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the columns to partition the output on the file system.\n   *\n   * Note: This should only include table columns already defined in schema.\n   *\n   * @param colNames string* column names for partitioning\n   * @since 4.0.0\n   */\n  @Evolving\n  @scala.annotation.varargs\n  def partitionedBy(colNames: String*): DeltaTableBuilder = {\n    partitioningColumns = colNames\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify the columns to use as clustering keys for liquid clustering.\n   *\n   * Note: This should only include table columns already defined in schema.\n   *\n   * @param colNames string* column names for liquid clustering\n   * @since 4.0.0\n   */\n  @Evolving\n  @scala.annotation.varargs\n  def clusterBy(colNames: String*): DeltaTableBuilder = {\n    clusteringColumns = colNames\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Specify a key-value pair to tag the table definition.\n   *\n   * @param key string the table property key\n   * @param value string the table property value\n   * @since 4.0.0\n   */\n  @Evolving\n  def property(key: String, value: String): DeltaTableBuilder = {\n    this.properties = this.properties + (key -> value)\n    this\n  }\n\n  /**\n   * :: Evolving ::\n   *\n   * Execute the command to create / replace a Delta table and returns a instance of [[DeltaTable]].\n   *\n   * @since 4.0.0\n   */\n  @Evolving\n  def execute(): DeltaTable = {\n    if (identifier.isEmpty && location.isEmpty) {\n      val exMessage = \"Table name or location has to be specified\"\n      throw DeltaTable.createAnalysisException(exMessage)\n    }\n\n    val mode = builderOption match {\n      case CreateTableOptions(ifNotExists) =>\n        if (ifNotExists) proto.CreateDeltaTable.Mode.MODE_CREATE_IF_NOT_EXISTS\n        else proto.CreateDeltaTable.Mode.MODE_CREATE\n      case ReplaceTableOptions(orCreate) =>\n        if (orCreate) proto.CreateDeltaTable.Mode.MODE_CREATE_OR_REPLACE\n        else proto.CreateDeltaTable.Mode.MODE_REPLACE\n    }\n\n    val createDeltaTable = proto.CreateDeltaTable\n      .newBuilder()\n      .setMode(mode)\n      .addAllPartitioningColumns(partitioningColumns.asJava)\n      .addAllClusteringColumns(clusteringColumns.asJava)\n      .putAllProperties(properties.asJava)\n    identifier.foreach(createDeltaTable.setTableName)\n    location.foreach(createDeltaTable.setLocation)\n    tblComment.foreach(createDeltaTable.setComment)\n\n    val protoColumns = columns.map { f =>\n      val builder = proto.CreateDeltaTable.Column\n        .newBuilder()\n        .setName(f.name)\n        .setDataType(DataTypeProtoConverter.toConnectProtoType(f.dataType))\n        .setNullable(f.nullable)\n      if (f.metadata.contains(\"delta.generationExpression\")) {\n        builder.setGeneratedAlwaysAs(f.metadata.getString(\"delta.generationExpression\"))\n      }\n      if (f.metadata.contains(\"delta.identity.allowExplicitInsert\")) {\n        builder.setIdentityInfo(\n          proto.CreateDeltaTable.Column.IdentityInfo\n            .newBuilder()\n            .setStart(f.metadata.getLong(\"delta.identity.start\"))\n            .setStep(f.metadata.getLong(\"delta.identity.step\"))\n            .setAllowExplicitInsert(f.metadata.getBoolean(\"delta.identity.allowExplicitInsert\"))\n            .build()\n        )\n      }\n      if (f.metadata.contains(\"comment\")) {\n        builder.setComment(f.metadata.getString(\"comment\"))\n      }\n      builder.build()\n    }\n    createDeltaTable.addAllColumns(protoColumns.asJava)\n\n    val command = proto.DeltaCommand.newBuilder().setCreateDeltaTable(createDeltaTable).build()\n    val extension = com.google.protobuf.Any.pack(command)\n    val sparkCommand = spark_proto.Command.newBuilder().setExtension(extension).build()\n    spark.execute(sparkCommand)\n\n    if (location.isDefined) {\n      DeltaTable.forPath(spark, location.get)\n    } else {\n      DeltaTable.forName(spark, identifier.get)\n    }\n  }\n}\n"
  },
  {
    "path": "spark-connect/client/src/main/scala/io/delta/connect/tables/execution/DeltaTableBuilderOptions.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables.execution\n\n/**\n * DeltaTableBuilder option to indicate whether it's to create / replace the table.\n */\nsealed trait DeltaTableBuilderOptions\n\n/**\n * Specify that the builder is to create a Delta table.\n *\n * @param ifNotExists boolean whether to ignore if the table already exists.\n */\ncase class CreateTableOptions(ifNotExists: Boolean) extends DeltaTableBuilderOptions\n\n/**\n * Specify that the builder is to replace a Delta table.\n *\n * @param orCreate boolean whether to create the table if the table doesn't exist.\n */\ncase class ReplaceTableOptions(orCreate: Boolean) extends DeltaTableBuilderOptions\n"
  },
  {
    "path": "spark-connect/client/src/main/scala-shims/spark-4.0/SparkStringUtilsShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.util\n\nimport org.apache.spark.sql.catalyst.util.SparkStringUtils.sideBySide\n\n/**\n * Shim for SparkStringUtils to handle package relocation between Spark versions.\n * In Spark 4.0, SparkStringUtils was moved from org.apache.spark.sql.catalyst.util\n * to org.apache.spark.util.\n */\nobject SparkStringUtilsShims {\n  def sideBySide(left: Seq[String], right: Seq[String]): Seq[String] = {\n    SparkStringUtils.sideBySide(left, right)\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/main/scala-shims/spark-4.1/SparkStringUtilsShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.util\n\nimport org.apache.spark.util.SparkStringUtils\n\n/**\n * Shim for SparkStringUtils to handle package relocation between Spark versions.\n * In Spark 4.1+, SparkStringUtils was moved from org.apache.spark.sql.catalyst.util\n * to org.apache.spark.util.\n */\nobject SparkStringUtilsShims {\n  def sideBySide(left: Seq[String], right: Seq[String]): Seq[String] = {\n    SparkStringUtils.sideBySide(left, right)\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/main/scala-shims/spark-4.2/SparkStringUtilsShims.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.catalyst.util\n\nimport org.apache.spark.util.SparkStringUtils\n\n/**\n * Shim for SparkStringUtils to handle package relocation between Spark versions.\n * In Spark 4.2, SparkStringUtils is in org.apache.spark.util (same as Spark 4.1+).\n */\nobject SparkStringUtilsShims {\n  def sideBySide(left: Seq[String], right: Seq[String]): Seq[String] = {\n    SparkStringUtils.sideBySide(left, right)\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/test/scala/io/delta/connect/tables/DeltaMergeBuilderSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.connect.ConnectConversions._\nimport org.apache.spark.sql.functions.{col, expr}\nimport org.apache.spark.sql.test.DeltaQueryTest\n\nclass DeltaMergeBuilderSuite extends DeltaQueryTest with RemoteSparkSession {\n  private def writeTargetTable(path: String): Unit = {\n    val session = spark\n    import session.implicits._\n    Seq((\"a\", 1), (\"b\", 2), (\"c\", 3), (\"d\", 4)).toDF(\"key\", \"value\")\n      .write.mode(\"overwrite\").format(\"delta\").save(path)\n  }\n\n  private def testSource = {\n    val session = spark\n    import session.implicits._\n    Seq((\"a\", -1), (\"b\", 0), (\"e\", -5), (\"f\", -6)).toDF(\"k\", \"v\")\n  }\n\n  test(\"string expressions in merge conditions and assignments\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      val mergeOutput = deltaTable\n        .merge(testSource, \"key = k\")\n        .whenMatched().updateExpr(Map(\"value\" -> \"value + v\"))\n        .whenNotMatched().insertExpr(Map(\"key\" -> \"k\", \"value\" -> \"v\"))\n        .whenNotMatchedBySource().updateExpr(Map(\"value\" -> \"value - 1\"))\n        .execute()\n\n      checkAnswer(\n        mergeOutput,\n        Seq(Row(\n          6, // affected rows\n          4, // updated rows (a and b in WHEN MATCHED and c and d in WHEN NOT MATCHED BY SOURCE)\n          0, // deleted rows\n          2 // inserted rows (e and f)\n        )))\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 0), Row(\"b\", 2), Row(\"c\", 2), Row(\"d\", 3), Row(\"e\", -5), Row(\"f\", -6)))\n    }\n  }\n\n  test(\"column expressions in merge conditions and assignments\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, col(\"key\") === col(\"k\"))\n        .whenMatched().update(Map(\"value\" -> (col(\"value\") + col(\"v\"))))\n        .whenNotMatched().insert(Map(\"key\" -> col(\"k\"), \"value\" -> col(\"v\")))\n        .whenNotMatchedBySource().update(Map(\"value\" -> (col(\"value\") - 1)))\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 0), Row(\"b\", 2), Row(\"c\", 2), Row(\"d\", 3), Row(\"e\", -5), Row(\"f\", -6)))\n    }\n  }\n\n  test(\"multiple when matched then update clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, expr(\"key = k\"))\n        .whenMatched(\"key = 'a'\").updateExpr(Map(\"value\" -> \"5\"))\n        .whenMatched().updateExpr(Map(\"value\" -> \"0\"))\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 5), Row(\"b\", 0), Row(\"c\", 3), Row(\"d\", 4)))\n    }\n  }\n\n  test(\"multiple when matched then delete clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, \"key = k\")\n        .whenMatched(\"key = 'a'\").delete()\n        .whenMatched().delete()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"c\", 3), Row(\"d\", 4)))\n    }\n  }\n\n  test(\"redundant when matched then update and delete clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, col(\"key\") === col(\"k\"))\n        .whenMatched(\"key = 'a'\").updateExpr(Map(\"value\" -> \"5\"))\n        .whenMatched(\"key = 'a'\").updateExpr(Map(\"value\" -> \"0\"))\n        .whenMatched(\"key = 'b'\").updateExpr(Map(\"value\" -> \"6\"))\n        .whenMatched(\"key = 'b'\").delete()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 5), Row(\"b\", 6), Row(\"c\", 3), Row(\"d\", 4)))\n    }\n  }\n\n  test(\"interleaved when matched then update and delete clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource, col(\"t.key\") === col(\"k\"))\n        .whenMatched(\"t.key = 'a'\").delete()\n        .whenMatched(\"t.key = 'a'\").updateExpr(Map(\"value\" -> \"5\"))\n        .whenMatched(\"t.key = 'b'\").delete()\n        .whenMatched().updateExpr(Map(\"value\" -> \"6\"))\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"c\", 3), Row(\"d\", 4)))\n    }\n  }\n\n  test(\"multiple when not matched then insert clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), col(\"t.key\") === col(\"s.key\"))\n        .whenNotMatched(\"s.key = 'e'\").insertExpr(Map(\"t.key\" -> \"s.key\", \"t.value\" -> \"5\"))\n        .whenNotMatched().insertAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 1), Row(\"b\", 2), Row(\"c\", 3), Row(\"d\", 4), Row(\"e\", 5), Row(\"f\", -6)))\n    }\n  }\n\n  test(\"redundant when not matched then insert clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, expr(\"key = k\"))\n        .whenNotMatched(\"k = 'e'\").insertExpr(Map(\"key\" -> \"k\", \"value\" -> \"5\"))\n        .whenNotMatched(\"k = 'e'\").insertExpr(Map(\"key\" -> \"k\", \"value\" -> \"6\"))\n        .whenNotMatched(\"k = 'f'\").insertExpr(Map(\"key\" -> \"k\", \"value\" -> \"7\"))\n        .whenNotMatched(\"k = 'f'\").insertExpr(Map(\"key\" -> \"k\", \"value\" -> \"8\"))\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 1), Row(\"b\", 2), Row(\"c\", 3), Row(\"d\", 4), Row(\"e\", 5), Row(\"f\", 7)))\n    }\n  }\n\n  test(\"multiple when not matched by source then update clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.merge(testSource, expr(\"key = k\"))\n        .whenNotMatchedBySource(\"key = 'c'\").updateExpr(Map(\"value\" -> \"5\"))\n        .whenNotMatchedBySource().updateExpr(Map(\"value\" -> \"0\"))\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 1), Row(\"b\", 2), Row(\"c\", 5), Row(\"d\", 0)))\n    }\n  }\n\n  test(\"multiple when not matched by source then delete clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.merge(testSource, expr(\"key = k\"))\n        .whenNotMatchedBySource(\"key = 'c'\").delete()\n        .whenNotMatchedBySource().delete()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 1), Row(\"b\", 2)))\n    }\n  }\n\n  test(\"redundant when not matched by source then update and delete clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.merge(testSource, expr(\"key = k\"))\n        .whenNotMatchedBySource(\"key = 'c'\").updateExpr(Map(\"value\" -> \"5\"))\n        .whenNotMatchedBySource(\"key = 'c'\").updateExpr(Map(\"value\" -> \"0\"))\n        .whenNotMatchedBySource(\"key = 'd'\").updateExpr(Map(\"value\" -> \"6\"))\n        .whenNotMatchedBySource(\"key = 'd'\").delete()\n        .whenNotMatchedBySource().delete()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 1), Row(\"b\", 2), Row(\"c\", 5), Row(\"d\", 6)))\n    }\n  }\n\n\n  test(\"interleaved when not matched by source then update and delete clauses\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.merge(testSource, expr(\"key = k\"))\n        .whenNotMatchedBySource(\"key = 'c'\").delete()\n        .whenNotMatchedBySource(\"key = 'c'\").updateExpr(Map(\"value\" -> \"5\"))\n        .whenNotMatchedBySource(\"key = 'd'\").delete()\n        .whenNotMatchedBySource().updateExpr(Map(\"value\" -> \"6\"))\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", 1), Row(\"b\", 2)))\n    }\n  }\n\n  test(\"string expressions in all conditions and assignments\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, \"key = k\")\n        .whenMatched(\"k = 'a'\").updateExpr(Map(\"value\" -> \"v + 0\"))\n        .whenMatched(\"k = 'b'\").delete()\n        .whenNotMatched(\"k = 'e'\").insertExpr(Map(\"key\" -> \"k\", \"value\" -> \"v + 0\"))\n        .whenNotMatchedBySource(\"key = 'c'\").updateExpr(Map(\"value\" -> \"value + 0\"))\n        .whenNotMatchedBySource(\"key = 'd'\").delete()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", -1), Row(\"c\", 3), Row(\"e\", -5)))\n    }\n  }\n\n  test(\"column expressions in all conditions and assignments\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable\n        .merge(testSource, expr(\"key = k\"))\n        .whenMatched(expr(\"k = 'a'\")).update(Map(\"value\" -> (col(\"v\") + 0)))\n        .whenMatched(expr(\"k = 'b'\")).delete()\n        .whenNotMatched(expr(\"k = 'e'\")).insert(Map(\"key\" -> col(\"k\"), \"value\" -> (col(\"v\") + 0)))\n        .whenNotMatchedBySource(expr(\"key = 'c'\")).update(Map(\"value\" -> (col(\"value\") + 0)))\n        .whenNotMatchedBySource(expr(\"key = 'd'\")).delete()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", -1), Row(\"c\", 3), Row(\"e\", -5)))\n    }\n  }\n\n  test(\"no clause conditions and insertAll/updateAll + aliases\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), expr(\"t.key = s.key\"))\n        .whenMatched().updateAll()\n        .whenNotMatched().insertAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", -1), Row(\"b\", 0), Row(\"c\", 3), Row(\"d\", 4), Row(\"e\", -5), Row(\"f\", -6)))\n    }\n  }\n\n  test(\"string expressions in all clause conditions and insertAll/updateAll + aliases\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), \"t.key = s.key\")\n        .whenMatched(\"s.key = 'a'\").updateAll()\n        .whenNotMatched(\"s.key = 'e'\").insertAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", -1), Row(\"b\", 2), Row(\"c\", 3), Row(\"d\", 4), Row(\"e\", -5)))\n    }\n  }\n\n  test(\"column expressions in all clause conditions and insertAll/updateAll + aliases\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      writeTargetTable(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), expr(\"t.key = s.key\"))\n        .whenMatched(expr(\"s.key = 'a'\")).updateAll()\n        .whenNotMatched(expr(\"s.key = 'e'\")).insertAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\", -1), Row(\"b\", 2), Row(\"c\", 3), Row(\"d\", 4), Row(\"e\", -5)))\n    }\n  }\n\n  test(\"automatic schema evolution\") {\n    val session = spark\n    import session.implicits._\n\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      Seq(\"a\", \"b\", \"c\", \"d\").toDF(\"key\")\n        .write.mode(\"overwrite\").format(\"delta\").save(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      withSQLConf(\"spark.databricks.delta.schema.autoMerge.enabled\" -> \"true\") {\n        deltaTable.as(\"t\")\n          .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), expr(\"t.key = s.key\"))\n          .whenMatched().updateAll()\n          .whenNotMatched().insertAll()\n          .execute()\n      }\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(\n          Row(\"a\", -1), Row(\"b\", 0), Row(\"c\", null), Row(\"d\", null), Row(\"e\", -5), Row(\"f\", -6)))\n    }\n  }\n\n  test(\"merge with the withSchemaEvolution API\") {\n    val session = spark\n    import session.implicits._\n\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      Seq(\"a\", \"b\", \"c\", \"d\").toDF(\"key\")\n        .write.mode(\"overwrite\").format(\"delta\").save(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), expr(\"t.key = s.key\"))\n        .withSchemaEvolution()\n        .whenMatched().updateAll()\n        .whenNotMatched().insertAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(\n          Row(\"a\", -1), Row(\"b\", 0), Row(\"c\", null), Row(\"d\", null), Row(\"e\", -5), Row(\"f\", -6)))\n    }\n  }\n\n  test(\"merge with no withSchemaEvolution while the source's schema \" +\n      \"is different than the target's schema\") {\n    val session = spark\n    import session.implicits._\n\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      Seq(\"a\", \"b\", \"c\", \"d\").toDF(\"key\")\n        .write.mode(\"overwrite\").format(\"delta\").save(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.as(\"t\")\n        .merge(testSource.toDF(\"key\", \"value\").as(\"s\"), expr(\"t.key = s.key\"))\n        .whenMatched().updateAll()\n        .whenNotMatched().insertAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(Row(\"a\"), Row(\"b\"), Row(\"c\"), Row(\"d\"), Row(\"e\"), Row(\"f\")))\n    }\n  }\n\n  test(\"merge dataframe with many columns\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      val id = col(\"id\")\n      val numColumns = 100\n      val cols1 = id +: Seq.tabulate(numColumns)(i => id.as(s\"col$i\"))\n      val df1 = spark.range(1).select(cols1: _*)\n      df1.write.mode(\"overwrite\").format(\"delta\").save(path)\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, path)\n\n      val cols2 = id +: Seq.tabulate(numColumns)(i => (id + 1).as(s\"col$i\"))\n      val df2 = spark.range(1).select(cols2: _*)\n\n      deltaTable\n        .as(\"t\")\n        .merge(df2.as(\"s\"), \"s.id = t.id\")\n        .whenMatched().updateAll()\n        .execute()\n\n      checkAnswer(\n        deltaTable.toDF,\n        Seq(df2.collectAsList().get(0)))\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/test/scala/io/delta/connect/tables/DeltaQueryTest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.test\n\nimport java.util.TimeZone\n\nimport org.scalatest.Assertions\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport org.apache.spark.sql.{DataFrame, Dataset, Row}\nimport org.apache.spark.sql.catalyst.util.SparkStringUtilsShims.sideBySide\nimport org.apache.spark.sql.connect.ConnectConversions._\nimport org.apache.spark.sql.connect.SparkSession\nimport org.apache.spark.sql.connect.test.SQLHelper\nimport org.apache.spark.util.ArrayImplicits._\n\n// TODO: Copied from Spark until SPARK-48341 is resolved.\n\nabstract class DeltaQueryTest extends AnyFunSuite with SQLHelper {\n\n  def spark: SparkSession\n\n  /**\n   * Runs the plan and makes sure the answer matches the expected result.\n   *\n   * @param df\n   *   the [[DataFrame]] to be executed\n   * @param expectedAnswer\n   *   the expected result in a [[Seq]] of [[Row]]s.\n   */\n  protected def checkAnswer(df: => DataFrame, expectedAnswer: Seq[Row]): Unit = {\n    DeltaQueryTest.checkAnswer(df, expectedAnswer)\n  }\n\n  protected def checkAnswer(df: => DataFrame, expectedAnswer: Row): Unit = {\n    checkAnswer(df, Seq(expectedAnswer))\n  }\n\n  protected def checkAnswer(df: => DataFrame, expectedAnswer: DataFrame): Unit = {\n    checkAnswer(df, expectedAnswer.collect().toImmutableArraySeq)\n  }\n\n  protected def checkAnswer(df: => DataFrame, expectedAnswer: Array[Row]): Unit = {\n    checkAnswer(df, expectedAnswer.toImmutableArraySeq)\n  }\n\n  /**\n   * Evaluates a dataset to make sure that the result of calling collect matches the given\n   * expected answer.\n   */\n  protected def checkDataset[T](ds: => Dataset[T], expectedAnswer: T*): Unit = {\n    val result = ds.collect()\n\n    if (!DeltaQueryTest.compare(result.toSeq, expectedAnswer)) {\n      fail(s\"\"\"\n              |Decoded objects do not match expected objects:\n              |expected: $expectedAnswer\n              |actual:   ${result.toSeq}\n           \"\"\".stripMargin)\n    }\n  }\n\n  /**\n   * Evaluates a dataset to make sure that the result of calling collect matches the given\n   * expected answer, after sort.\n   */\n  protected def checkDatasetUnorderly[T: Ordering](\n      ds: => Dataset[T],\n      expectedAnswer: T*): Unit = {\n    val result = ds.collect()\n\n    if (!DeltaQueryTest.compare(result.toSeq.sorted, expectedAnswer.sorted)) {\n      fail(s\"\"\"\n              |Decoded objects do not match expected objects:\n              |expected: $expectedAnswer\n              |actual:   ${result.toSeq}\n           \"\"\".stripMargin)\n    }\n  }\n}\n\nobject DeltaQueryTest extends Assertions {\n\n  /**\n   * Runs the plan and makes sure the answer matches the expected result.\n   *\n   * @param df\n   *   the DataFrame to be executed\n   * @param expectedAnswer\n   *   the expected result in a Seq of Rows.\n   */\n  def checkAnswer(df: DataFrame, expectedAnswer: Seq[Row], isSorted: Boolean = false): Unit = {\n    getErrorMessageInCheckAnswer(df, expectedAnswer, isSorted) match {\n      case Some(errorMessage) => fail(errorMessage)\n      case None =>\n    }\n  }\n\n  /**\n   * Runs the plan and makes sure the answer matches the expected result. If there was exception\n   * during the execution or the contents of the DataFrame does not match the expected result, an\n   * error message will be returned. Otherwise, a None will be returned.\n   *\n   * @param df\n   *   the DataFrame to be executed\n   * @param expectedAnswer\n   *   the expected result in a Seq of Rows.\n   */\n  def getErrorMessageInCheckAnswer(\n      df: DataFrame,\n      expectedAnswer: Seq[Row],\n      isSorted: Boolean = false): Option[String] = {\n    val sparkAnswer =\n      try df.collect().toSeq\n      catch {\n        case e: Exception =>\n          val errorMessage =\n            s\"\"\"\n               |Exception thrown while executing query:\n               |${df.analyze}\n               |== Exception ==\n               |$e\n               |${org.apache.spark.util.SparkErrorUtils.stackTraceToString(e)}\n            \"\"\".stripMargin\n          return Some(errorMessage)\n      }\n\n    sameRows(expectedAnswer, sparkAnswer, isSorted).map { results =>\n      s\"\"\"\n         |Results do not match for query:\n         |Timezone: ${TimeZone.getDefault}\n         |Timezone Env: ${sys.env.getOrElse(\"TZ\", \"\")}\n         |\n         |${df.analyze}\n         |== Results ==\n         |$results\n      \"\"\".stripMargin\n    }\n  }\n\n  def prepareAnswer(answer: Seq[Row], isSorted: Boolean): Seq[Row] = {\n    // Converts data to types that we can do equality comparison using Scala collections.\n    // For BigDecimal type, the Scala type has a better definition of equality test (similar to\n    // Java's java.math.BigDecimal.compareTo).\n    // For binary arrays, we convert it to Seq to avoid of calling java.util.Arrays.equals for\n    // equality test.\n    val converted: Seq[Row] = answer.map(prepareRow)\n    if (!isSorted) converted.sortBy(_.toString()) else converted\n  }\n\n  // We need to call prepareRow recursively to handle schemas with struct types.\n  def prepareRow(row: Row): Row = {\n    Row.fromSeq(row.toSeq.map {\n      case null => null\n      case bd: java.math.BigDecimal => BigDecimal(bd)\n      // Equality of WrappedArray differs for AnyVal and AnyRef in Scala 2.12.2+\n      case seq: Seq[_] =>\n        seq.map {\n          case b: java.lang.Byte => b.byteValue\n          case s: java.lang.Short => s.shortValue\n          case i: java.lang.Integer => i.intValue\n          case l: java.lang.Long => l.longValue\n          case f: java.lang.Float => f.floatValue\n          case d: java.lang.Double => d.doubleValue\n          case x => x\n        }\n      // Convert array to Seq for easy equality check.\n      case b: Array[_] => b.toSeq\n      case r: Row => prepareRow(r)\n      case o => o\n    })\n  }\n\n  private def genError(\n      expectedAnswer: Seq[Row],\n      sparkAnswer: Seq[Row],\n      isSorted: Boolean = false): String = {\n    val getRowType: Option[Row] => String = row =>\n      row\n        .map(row =>\n          if (row.schema == null) {\n            \"struct<>\"\n          } else {\n            s\"${row.schema.catalogString}\"\n          })\n        .getOrElse(\"struct<>\")\n\n    s\"\"\"\n       |== Results ==\n       |${sideBySide(\n      s\"== Correct Answer - ${expectedAnswer.size} ==\" +:\n        getRowType(expectedAnswer.headOption) +:\n        prepareAnswer(expectedAnswer, isSorted).map(_.toString()),\n      s\"== Spark Answer - ${sparkAnswer.size} ==\" +:\n        getRowType(sparkAnswer.headOption) +:\n        prepareAnswer(sparkAnswer, isSorted).map(_.toString())).mkString(\"\\n\")}\n    \"\"\".stripMargin\n  }\n\n  def includesRows(expectedRows: Seq[Row], sparkAnswer: Seq[Row]): Option[String] = {\n    if (!prepareAnswer(expectedRows, true).toSet.subsetOf(\n      prepareAnswer(sparkAnswer, true).toSet)) {\n      return Some(genError(expectedRows, sparkAnswer, true))\n    }\n    None\n  }\n\n  def compare(obj1: Any, obj2: Any): Boolean = (obj1, obj2) match {\n    case (null, null) => true\n    case (null, _) => false\n    case (_, null) => false\n    case (a: Array[_], b: Array[_]) =>\n      a.length == b.length && a.zip(b).forall { case (l, r) => compare(l, r) }\n    case (a: Map[_, _], b: Map[_, _]) =>\n      a.size == b.size && a.keys.forall { aKey =>\n        b.keys.find(bKey => compare(aKey, bKey)).exists(bKey => compare(a(aKey), b(bKey)))\n      }\n    case (a: Iterable[_], b: Iterable[_]) =>\n      a.size == b.size && a.zip(b).forall { case (l, r) => compare(l, r) }\n    case (a: Product, b: Product) =>\n      compare(a.productIterator.toSeq, b.productIterator.toSeq)\n    case (a: Row, b: Row) =>\n      compare(a.toSeq, b.toSeq)\n    // 0.0 == -0.0, turn float/double to bits before comparison, to distinguish 0.0 and -0.0.\n    case (a: Double, b: Double) =>\n      java.lang.Double.doubleToRawLongBits(a) == java.lang.Double.doubleToRawLongBits(b)\n    case (a: Float, b: Float) =>\n      java.lang.Float.floatToRawIntBits(a) == java.lang.Float.floatToRawIntBits(b)\n    case (a, b) => a == b\n  }\n\n  def sameRows(\n      expectedAnswer: Seq[Row],\n      sparkAnswer: Seq[Row],\n      isSorted: Boolean = false): Option[String] = {\n    if (!compare(prepareAnswer(expectedAnswer, isSorted), prepareAnswer(sparkAnswer, isSorted))) {\n      return Some(genError(expectedAnswer, sparkAnswer, isSorted))\n    }\n    None\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/test/scala/io/delta/connect/tables/DeltaTableBuilderSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport org.apache.spark.sql.AnalysisException\nimport org.apache.spark.sql.test.DeltaQueryTest\nimport org.apache.spark.sql.types.{IntegerType, LongType, MetadataBuilder, StringType, StructType}\n\nclass DeltaTableBuilderSuite extends DeltaQueryTest with RemoteSparkSession {\n\n  // Define the information for a default test table used by many tests.\n  private val defaultTestTableSchema = \"c1 int, c2 int, c3 string\"\n  private val defaultTestTableGeneratedColumns = Map(\"c2\" -> \"c1 + 10\")\n  private val defaultTestTableIdentityColumns = Map.empty[String, String]\n  private val defaultTestTablePartitionColumns = Seq(\"c1\")\n  private val defaultTestTableColumnComments = Map(\"c1\" -> \"foo\", \"c3\" -> \"bar\")\n  private val defaultTestTableComment = \"tbl comment\"\n  private val defaultTestTableNullableCols = Set(\"c1\", \"c3\")\n  private val defaultTestTableProperty = Map(\"foo\" -> \"bar\")\n  private val defaultTestTableClusteringColumns = Seq(\"c1\", \"c3\")\n\n\n  /**\n   * Verify if the table metadata matches the test table. We use this to verify DDLs\n   * write correct table metadata into the transaction logs.\n   * If clusteringCols are not empty, it verifies the clustering columns for liquid.\n   */\n  protected def verifyTestTableMetadata(\n      table: String,\n      schemaString: String,\n      generatedColumns: Map[String, String] = Map.empty,\n      identityColumns: Map[String, String] = Map.empty,\n      colComments: Map[String, String] = Map.empty,\n      colNullables: Set[String] = Set.empty,\n      tableComment: Option[String] = None,\n      expectedPartitionCols: Seq[String] = Seq.empty,\n      expectedTableProperties: Map[String, String] = Map.empty,\n      expectedClusteringCols: Seq[String] = Seq.empty): Unit = {\n    val session = spark\n    import session.implicits._\n\n    val expectedSchema = StructType(StructType.fromDDL(schemaString).map { field =>\n      val newMetadata = new MetadataBuilder().withMetadata(field.metadata)\n      if (colComments.contains(field.name)) {\n        newMetadata.putString(\"comment\", colComments(field.name))\n      }\n      field.copy(\n        nullable = colNullables.contains(field.name),\n        metadata = newMetadata.build())\n    })\n\n    val deltaTable = DeltaTable.forName(spark, table)\n    assert(deltaTable.toDF.schema == expectedSchema)\n\n    val (description, partitionColumns, properties, clusteringColumns) =\n      deltaTable.detail()\n        .select(\"description\", \"partitionColumns\", \"properties\", \"clusteringColumns\")\n        .as[(String, Seq[String], Map[String, String], Seq[String])]\n        .head()\n\n    assert(description == tableComment.orNull)\n    assert(partitionColumns == expectedPartitionCols)\n    // It may contain other properties other than the ones we added.\n    expectedTableProperties.foreach {\n      prop => properties.contains(prop._1) && properties.get(prop._1).get == prop._2\n    }\n    assert(clusteringColumns == expectedClusteringCols)\n\n    val schemaFields = deltaTable.toDF.schema.fields\n\n    // Verify generated columns\n    generatedColumns.foreach { case (col, expr) =>\n      val fieldOpt = schemaFields.find(_.name == col)\n      assert(fieldOpt.isDefined, s\"Generated column $col not found in table schema\")\n      val field = fieldOpt.get\n      \n      // Check if the metadata contains the generation expression key\n      if (field.metadata.contains(\"delta.generationExpression\")) {\n        val generationExpr = field.metadata.getString(\"delta.generationExpression\")\n        assert(generationExpr == expr, \n          s\"Generated column $col has expression '$generationExpr' but expected '$expr'\")\n      }\n    }\n    \n    // Verify identity columns without detailed metadata validation\n    identityColumns.foreach { case (col, _) =>\n      val fieldOpt = schemaFields.find(_.name == col)\n      assert(fieldOpt.isDefined, s\"Identity column $col not found in table schema\")\n    }\n  }\n\n  protected def testCreateTable(testName: String)(createFunc: String => Unit): Unit = {\n    test(testName) {\n      withTable(testName) {\n        createFunc(testName)\n        verifyTestTableMetadata(\n          testName, defaultTestTableSchema, defaultTestTableGeneratedColumns,\n          defaultTestTableIdentityColumns, defaultTestTableColumnComments,\n          defaultTestTableNullableCols,\n          Some(defaultTestTableComment), defaultTestTablePartitionColumns,\n          defaultTestTableProperty\n        )\n      }\n    }\n  }\n\n  protected def testCreateTableWithNameAndLocation(\n      testName: String)(createFunc: (String, String) => Unit): Unit = {\n    test(testName + \": external - with location and name\") {\n      withTempPath { path =>\n        withTable(testName) {\n          createFunc(testName, path.getCanonicalPath)\n          verifyTestTableMetadata(\n            testName,\n            defaultTestTableSchema, defaultTestTableGeneratedColumns,\n            defaultTestTableIdentityColumns, defaultTestTableColumnComments,\n            defaultTestTableNullableCols, Some(defaultTestTableComment),\n            defaultTestTablePartitionColumns, defaultTestTableProperty\n          )\n        }\n      }\n    }\n  }\n\n  protected def testCreateTableWithLocationOnly(\n      testName: String)(createFunc: String => Unit): Unit = {\n    test(testName + \": external - location only\") {\n      withTempPath { path =>\n        withTable(testName) {\n          createFunc(path.getCanonicalPath)\n          verifyTestTableMetadata(\n            s\"delta.`${path.getCanonicalPath}`\",\n            defaultTestTableSchema, defaultTestTableGeneratedColumns,\n            defaultTestTableIdentityColumns, defaultTestTableColumnComments,\n            defaultTestTableNullableCols, Some(defaultTestTableComment),\n            defaultTestTablePartitionColumns, defaultTestTableProperty\n          )\n        }\n      }\n    }\n  }\n\n  def defaultCreateTableBuilder(\n      ifNotExists: Boolean,\n      tableName: Option[String] = None,\n      location: Option[String] = None): DeltaTableBuilder = {\n    val tableBuilder = if (ifNotExists) {\n      io.delta.tables.DeltaTable.createIfNotExists(spark)\n    } else {\n      io.delta.tables.DeltaTable.create(spark)\n    }\n    defaultTableBuilder(tableBuilder, tableName, location)\n  }\n\n  def defaultReplaceTableBuilder(\n      orCreate: Boolean,\n      tableName: Option[String] = None,\n      location: Option[String] = None): DeltaTableBuilder = {\n    val tableBuilder = if (orCreate) {\n      io.delta.tables.DeltaTable.createOrReplace(spark)\n    } else {\n      io.delta.tables.DeltaTable.replace(spark)\n    }\n    defaultTableBuilder(tableBuilder, tableName, location)\n  }\n\n  private def defaultTableBuilder(\n      builder: DeltaTableBuilder,\n      tableName: Option[String],\n      location: Option[String],\n      clusterBy: Boolean = false): DeltaTableBuilder = {\n    var tableBuilder = builder\n    if (tableName.nonEmpty) {\n      tableBuilder = tableBuilder.tableName(tableName.get)\n    }\n    if (location.nonEmpty) {\n      tableBuilder = tableBuilder.location(location.get)\n    }\n    tableBuilder.addColumn(\n      io.delta.tables.DeltaTable.columnBuilder(spark, \"c1\").dataType(\"int\")\n        .nullable(true).comment(\"foo\").build()\n    )\n    tableBuilder.addColumn(\n      io.delta.tables.DeltaTable.columnBuilder(spark, \"c2\").dataType(\"int\")\n        .nullable(false).generatedAlwaysAs(\"c1 + 10\").build()\n    )\n    tableBuilder.addColumn(\n      io.delta.tables.DeltaTable.columnBuilder(spark, \"c3\").dataType(\"string\")\n        .comment(\"bar\").build()\n    )\n    if (clusterBy) {\n      tableBuilder.clusterBy(\"c1\", \"c3\")\n    } else {\n      tableBuilder.partitionedBy(\"c1\")\n    }\n    tableBuilder.property(\"foo\", \"bar\")\n    tableBuilder.comment(\"tbl comment\")\n    tableBuilder\n  }\n\n  test(\"create table with existing schema and extra column\") {\n    withTable(\"table\") {\n      withTempPath { dir =>\n        spark.range(10).toDF(\"key\").write.format(\"parquet\").saveAsTable(\"table\")\n        val existingSchema = spark.read.format(\"parquet\").table(\"table\").schema\n        io.delta.tables.DeltaTable.create(spark)\n          .location(dir.getAbsolutePath)\n          .addColumns(existingSchema)\n          .addColumn(\"value\", \"string\", false)\n          .execute()\n        verifyTestTableMetadata(s\"delta.`${dir.getAbsolutePath}`\",\n          \"key bigint, value string\", colNullables = Set(\"key\"))\n      }\n    }\n  }\n\n  test(\"create table with variation of addColumns - with spark session\") {\n    withTable(\"test\") {\n      io.delta.tables.DeltaTable.create(spark)\n        .tableName(\"test\")\n        .addColumn(\"c1\", \"int\")\n        .addColumn(\"c2\", IntegerType)\n        .addColumn(\"c3\", \"string\", false)\n        .addColumn(\"c4\", StringType, true)\n        .addColumn(\n          io.delta.tables.DeltaTable.columnBuilder(spark, \"c5\")\n            .dataType(\"bigint\")\n            .comment(\"foo\")\n            .nullable(false)\n            .build\n        )\n        .addColumn(\n          io.delta.tables.DeltaTable.columnBuilder(spark, \"c6\")\n            .dataType(LongType)\n            .generatedAlwaysAs(\"c5 + 10\")\n            .build\n        ).execute()\n      verifyTestTableMetadata(\n        \"test\", \"c1 int, c2 int, c3 string, c4 string, c5 bigint, c6 bigint\",\n        generatedColumns = Map(\"c6\" -> \"c5 + 10\"),\n        colComments = Map(\"c5\" -> \"foo\"),\n        colNullables = Set(\"c1\", \"c2\", \"c4\", \"c6\")\n      )\n    }\n  }\n\n  test(\"test addColumn using columnBuilder, without dataType\") {\n    val e = intercept[AnalysisException] {\n      DeltaTable.columnBuilder(spark, \"value\")\n        .generatedAlwaysAs(\"true\")\n        .nullable(true)\n        .build()\n    }\n    assert(e.getMessage == \"The data type of the column value is not provided\")\n  }\n\n  testCreateTable(\"create_table\") { table =>\n    defaultCreateTableBuilder(ifNotExists = false, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"create_table\") { (name, path) =>\n    defaultCreateTableBuilder(ifNotExists = false, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"create_table\") { path =>\n    defaultCreateTableBuilder(ifNotExists = false, location = Some(path)).execute()\n  }\n\n  test(\"create table - errors if already exists\") {\n    withTable(\"testTable\") {\n      spark.sql(s\"CREATE TABLE testTable (c1 int) USING DELTA\")\n      intercept[AnalysisException] {\n        defaultCreateTableBuilder(ifNotExists = false, Some(\"testTable\")).execute()\n      }\n    }\n  }\n\n  test(\"create table - ignore if already exists\") {\n    withTable(\"testTable\") {\n      spark.sql(s\"CREATE TABLE testTable (c1 int) USING DELTA\")\n      defaultCreateTableBuilder(ifNotExists = true, Some(\"testTable\")).execute()\n      verifyTestTableMetadata(\"testTable\", \"c1 int\", colNullables = Set(\"c1\"))\n    }\n  }\n\n  testCreateTable(\"create_table_if_not_exists\") { table =>\n    defaultCreateTableBuilder(ifNotExists = true, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"create_table_if_not_exists\") { (name, path) =>\n    defaultCreateTableBuilder(ifNotExists = true, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"create_table_if_not_exists\") { path =>\n    defaultCreateTableBuilder(ifNotExists = true, location = Some(path)).execute()\n  }\n\n  test(\"replace table - errors if not exists\") {\n    intercept[AnalysisException] {\n      defaultReplaceTableBuilder(orCreate = false, Some(\"testTable\")).execute()\n    }\n  }\n\n  testCreateTable(\"replace_table\") { table =>\n    spark.sql(s\"CREATE TABLE replace_table(c1 int) USING DELTA\")\n    defaultReplaceTableBuilder(orCreate = false, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"replace_table\") { (name, path) =>\n    spark.sql(s\"CREATE TABLE $name (c1 int) USING DELTA LOCATION '$path'\")\n    defaultReplaceTableBuilder(orCreate = false, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"replace_table\") { path =>\n    spark.sql(s\"CREATE TABLE delta.`$path` (c1 int) USING DELTA\")\n    defaultReplaceTableBuilder(orCreate = false, location = Some(path)).execute()\n  }\n\n  testCreateTable(\"replace_or_create_table\") { table =>\n    defaultReplaceTableBuilder(orCreate = true, Some(table)).execute()\n  }\n\n  testCreateTableWithNameAndLocation(\"replace_or_create_table\") { (name, path) =>\n    defaultReplaceTableBuilder(orCreate = true, Some(name), Some(path)).execute()\n  }\n\n  testCreateTableWithLocationOnly(\"replace_or_create_table\") { path =>\n    defaultReplaceTableBuilder(orCreate = true, location = Some(path)).execute()\n  }\n\n  test(\"test no identifier and no location\") {\n    val e = intercept[AnalysisException] {\n      io.delta.tables.DeltaTable.create(spark).addColumn(\"c1\", \"int\").execute()\n    }\n    assert(e.getMessage.equals(\"Table name or location has to be specified\"))\n  }\n\n  test(\"partitionedBy only should contain columns in the schema\") {\n    val e = intercept[AnalysisException] {\n      io.delta.tables.DeltaTable.create(spark).tableName(\"testTable\")\n        .addColumn(\"c1\", \"int\")\n        .partitionedBy(\"c2\")\n        .execute()\n    }\n    assert(e.getMessage.contains(\"Couldn't find column c2\"))\n  }\n\n  test(\"errors if table name and location are different paths\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      // TODO: This should be an AnalysisException, but it ends up as a Spark Exception\n      // that arises from the Connect Client failing to enrich the exception with the Delta\n      // error class that we expect.\n      val e = intercept[Exception] {\n        io.delta.tables.DeltaTable.create(spark).tableName(s\"delta.`$path`\")\n          .addColumn(\"c1\", \"int\")\n          .location(\"src/test/resources/delta/non_generated_columns\")\n          .execute()\n      }\n      val deltaErrorClass = \"DELTA_CREATE_TABLE_IDENTIFIER_LOCATION_MISMATCH\"\n      assert(e.getMessage.contains(deltaErrorClass))\n    }\n  }\n\n  test(\"table name and location are the same\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      io.delta.tables.DeltaTable.create(spark).tableName(s\"delta.`$path`\")\n        .addColumn(\"c1\", \"int\")\n        .location(path)\n        .execute()\n    }\n  }\n\n  test(\"errors if use parquet path as identifier\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      val e = intercept[AnalysisException] {\n        io.delta.tables.DeltaTable.create(spark).tableName(s\"parquet.`$path`\")\n          .addColumn(\"c1\", \"int\")\n          .location(path)\n          .execute()\n      }\n      assert(e.getMessage == \"Database 'main.parquet' not found\" ||\n        e.getMessage == \"Database 'parquet' not found\" ||\n        e.getMessage.contains(\"is not a valid name\") ||\n        e.getMessage.contains(\"schema `parquet` cannot be found\")\n      )\n    }\n  }\n\n  private def testCreateTableWithClusterBy(testName: String)(createFunc: String => Unit): Unit = {\n    test(testName) {\n      withTable(testName) {\n        createFunc(testName)\n        verifyTestTableMetadata(\n          testName, defaultTestTableSchema, defaultTestTableGeneratedColumns,\n          defaultTestTableIdentityColumns, defaultTestTableColumnComments,\n          defaultTestTableNullableCols, Some(defaultTestTableComment),\n          expectedTableProperties = defaultTestTableProperty,\n          expectedClusteringCols = defaultTestTableClusteringColumns\n        )\n      }\n    }\n  }\n\n  testCreateTableWithClusterBy(\"create_table_with_clusterBy\") { table =>\n    val builder = DeltaTable.create(spark)\n    defaultTableBuilder(builder, Some(table), None, true).execute()\n  }\n\n  testCreateTableWithClusterBy(\"replace_table_with_clusterBy\") { table =>\n    spark.sql(s\"CREATE TABLE $table(c1 int) USING DELTA\")\n    val builder = DeltaTable.replace(spark)\n    defaultTableBuilder(builder, Some(table), None, true).execute()\n  }\n\n  testCreateTableWithClusterBy(\"create_or_replace_table_with_clusterBy\") { table =>\n    spark.sql(s\"CREATE TABLE $table(c1 int) USING DELTA\")\n    val builder = DeltaTable.createOrReplace(spark)\n    defaultTableBuilder(builder, Some(table), None, true).execute()\n  }\n\n  test(\"clusterBy only should contain columns in the schema\") {\n    val e = intercept[AnalysisException] {\n      io.delta.tables.DeltaTable.create(spark).tableName(\"testTable\")\n        .addColumn(\"c1\", \"int\")\n        .clusterBy(\"c2\")\n        .execute()\n    }\n    assert(e.getMessage.contains(\"`c2` is missing\"))\n  }\n\n  test(\"partitionedBy and clusterBy cannot be used together\") {\n    // TODO: This should be an AnalysisException, but it ends up as a Spark Exception\n    // that arises from the Connect Client failing to enrich the exception with the Delta\n    // error class that we expect.\n    val e = intercept[Exception] {\n      io.delta.tables.DeltaTable.create(spark).tableName(\"testTable\")\n        .addColumn(\"c1\", \"int\")\n        .addColumn(\"c2\", \"int\")\n        .partitionedBy(\"c2\")\n        .clusterBy(\"c1\")\n        .execute()\n    }\n    val deltaErrorClass = \"DELTA_CLUSTER_BY_WITH_PARTITIONED_BY\"\n    assert(e.getMessage.contains(deltaErrorClass))\n  }\n\n  test(\"create table with identity columns\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      DeltaTable.create(spark)\n        .tableName(\"testTable\")\n        .addColumn(\n          DeltaTable.columnBuilder(spark, \"id1\")\n            .dataType(\"long\")\n            .nullable(false)\n            .generatedAlwaysAsIdentity()\n            .build()\n        )\n        .addColumn(\n          DeltaTable.columnBuilder(spark, \"id2\")\n            .dataType(\"long\")\n            .nullable(false)\n            .generatedByDefaultAsIdentity()\n            .build()\n        )\n        .addColumn(\n          DeltaTable.columnBuilder(spark, \"id3\")\n            .dataType(\"long\")\n            .nullable(false)\n            .generatedAlwaysAsIdentity(start = 100, step = 10)\n            .build()\n        )\n        .addColumn(\n          DeltaTable.columnBuilder(spark, \"id4\")\n            .dataType(\"long\")\n            .nullable(false)\n            .generatedByDefaultAsIdentity(start = 100, step = 10)\n            .build()\n        )\n        .location(path)\n        .execute()\n\n      // Verify the columns exist in the schema\n      val table = DeltaTable.forPath(spark, path)\n      val schema = table.toDF.schema\n      \n      // Verify basic structure\n      assert(schema.fieldNames.toSeq === Seq(\"id1\", \"id2\", \"id3\", \"id4\"))\n      schema.fields.foreach { field =>\n        assert(field.dataType.typeName === \"long\")\n        assert(!field.nullable)\n      }\n      \n      // Test identity column functionality: \n      // Insert data without providing values for GENERATED ALWAYS identity columns\n      // and verify correct identity values are generated\n      spark.sql(s\"INSERT INTO delta.`$path` (id2, id4) VALUES (20, 200)\")\n      \n      // First row should have id1=1, id3=100 (start values) generated automatically\n      // id2=20, id4=200 should be the explicitly provided values\n      val result = spark.sql(s\"SELECT * FROM delta.`$path`\").collect()\n      assert(result.length === 1)\n      assert(result(0).getLong(0) === 1) // id1 = 1 (default start)\n      assert(result(0).getLong(1) === 20) // id2 = 20 (explicitly provided)\n      assert(result(0).getLong(2) === 100) // id3 = 100 (custom start)\n      assert(result(0).getLong(3) === 200) // id4 = 200 (explicitly provided)\n      \n      // Verify identity column insertion restrictions\n      // Attempting to explicitly insert a value for a GENERATED ALWAYS identity column should fail\n      val e = intercept[Exception] {\n        spark.sql(s\"INSERT INTO delta.`$path` (id1, id2, id3, id4) VALUES (10, 20, 30, 40)\")\n      }\n      // TODO: This should be an AnalysisException, but it ends up as a Spark Exception\n      // that arises from the Connect Client failing to enrich the exception with the Delta\n      // error class that we expect.\n      val deltaErrorClass = \"DELTA_IDENTITY_COLUMNS_EXPLICIT_INSERT_NOT_SUPPORTED\"\n      assert(\n      e.getMessage.contains(deltaErrorClass),\n        \"Explicit inserts are never possible for a GENERATED ALWAYS identity column.\")\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/test/scala/io/delta/connect/tables/DeltaTableSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.Files\nimport java.text.SimpleDateFormat\n\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.functions.{col, lit}\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.test.DeltaQueryTest\n\nclass DeltaTableSuite extends DeltaQueryTest with RemoteSparkSession {\n  private lazy val testData = spark.range(100).toDF(\"value\")\n\n  test(\"forPath\") {\n    withTempPath { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      checkAnswer(\n        DeltaTable.forPath(spark, dir.getAbsolutePath).toDF,\n        testData.collect().toSeq\n      )\n    }\n  }\n\n  test(\"forName\") {\n    withTable(\"deltaTable\") {\n      testData.write.format(\"delta\").saveAsTable(\"deltaTable\")\n      checkAnswer(\n        DeltaTable.forName(spark, \"deltaTable\").toDF,\n        testData.collect().toSeq\n      )\n    }\n  }\n\n  test(\"as\") {\n    withTempPath { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      checkAnswer(\n        DeltaTable.forPath(spark, dir.getAbsolutePath).as(\"tbl\").toDF.select(\"tbl.value\"),\n        testData.select(\"value\").collect().toSeq\n      )\n    }\n  }\n\n  test(\"vacuum\") {\n    withTempPath { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      val table = io.delta.tables.DeltaTable.forPath(spark, dir.getAbsolutePath)\n\n      // create a uncommitted file.\n      val notCommittedFile = \"notCommittedFile.json\"\n      val file = new File(dir, notCommittedFile)\n      Files.write(file.toPath, \"gibberish\".getBytes(StandardCharsets.UTF_8))\n      // set to ancient time so that the file is eligible to be vacuumed.\n      file.setLastModified(0)\n      assert(file.exists())\n\n      table.vacuum()\n\n      val file2 = new File(dir, notCommittedFile)\n      assert(!file2.exists())\n    }\n  }\n\n  test(\"history\") {\n    val session = spark\n    import session.implicits._\n\n    withTempPath { dir =>\n      Seq(1, 2, 3).toDF().write.format(\"delta\").save(dir.getAbsolutePath)\n      Seq(4, 5).toDF().write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n\n      val table = DeltaTable.forPath(spark, dir.getAbsolutePath)\n      checkAnswer(\n        table.history().select(\"version\"),\n        Seq(Row(0L), Row(1L))\n      )\n    }\n  }\n\n  test(\"detail\") {\n    val session = spark\n    import session.implicits._\n\n    withTempPath { dir =>\n      Seq(1, 2, 3).toDF().write.format(\"delta\").save(dir.getAbsolutePath)\n\n      val deltaTable = DeltaTable.forPath(spark, dir.getAbsolutePath)\n      checkAnswer(\n        deltaTable.detail().select(\"format\"),\n        Seq(Row(\"delta\"))\n      )\n    }\n  }\n\n  test(\"isDeltaTable - path - with _delta_log dir\") {\n    withTempPath { dir =>\n      testData.write.format(\"delta\").save(dir.getAbsolutePath)\n      assert(DeltaTable.isDeltaTable(spark, dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - path - with empty _delta_log dir\") {\n    withTempPath { dir =>\n      new File(dir, \"_delta_log\").mkdirs()\n      assert(!DeltaTable.isDeltaTable(spark, dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - path - with no _delta_log dir\") {\n    withTempPath { dir =>\n      assert(!DeltaTable.isDeltaTable(spark, dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - path - with non-existent dir\") {\n    withTempPath { dir =>\n      assert(!DeltaTable.isDeltaTable(spark, dir.getAbsolutePath))\n    }\n  }\n\n  test(\"isDeltaTable - with non-Delta table path\") {\n    withTempPath { dir =>\n      testData.write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n      assert(!DeltaTable.isDeltaTable(spark, dir.getAbsolutePath))\n    }\n  }\n\n  test(\"generate\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      testData.toDF().write.format(\"delta\").save(path)\n      val table = DeltaTable.forPath(spark, path)\n      val manifestDir = new File(dir, \"_symlink_format_manifest\")\n      assert(!manifestDir.exists())\n      table.generate(\"symlink_format_manifest\")\n      assert(manifestDir.exists())\n    }\n  }\n\n  test(\"delete\") {\n    val session = spark\n    import session.implicits._\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      Seq((\"a\", 1), (\"b\", 2), (\"c\", 3), (\"d\", 4)).toDF(\"key\", \"value\")\n        .write.format(\"delta\").save(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.delete(\"key = 'a'\")\n      checkAnswer(deltaTable.toDF, Seq(Row(\"b\", 2), Row(\"c\", 3), Row(\"d\", 4)))\n\n      deltaTable.delete(col(\"key\") === lit(\"b\"))\n      checkAnswer(deltaTable.toDF, Seq(Row(\"c\", 3), Row(\"d\", 4)))\n\n      deltaTable.delete()\n      checkAnswer(deltaTable.toDF, Nil)\n    }\n  }\n\n  test(\"update\") {\n    val session = spark\n    import session.implicits._\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      Seq((\"a\", 1), (\"b\", 2), (\"c\", 3), (\"d\", 4)).toDF(\"key\", \"value\")\n        .write.format(\"delta\").save(path)\n      val deltaTable = DeltaTable.forPath(spark, path)\n\n      deltaTable.updateExpr(\"key = 'a' or key = 'b'\", Map(\"value\" -> \"1\"))\n      checkAnswer(deltaTable.toDF, Seq(Row(\"a\", 1), Row(\"b\", 1), Row(\"c\", 3), Row(\"d\", 4)))\n\n      deltaTable.update(col(\"key\") === lit(\"a\") || col(\"key\") === lit(\"b\"), Map(\"value\" -> lit(0)))\n      checkAnswer(deltaTable.toDF, Seq(Row(\"a\", 0), Row(\"b\", 0), Row(\"c\", 3), Row(\"d\", 4)))\n\n      deltaTable.updateExpr(Map(\"value\" -> \"-1\"))\n      checkAnswer(deltaTable.toDF, Seq(Row(\"a\", -1), Row(\"b\", -1), Row(\"c\", -1), Row(\"d\", -1)))\n\n      deltaTable.update(Map(\"value\" -> lit(37)))\n      checkAnswer(deltaTable.toDF, Seq(Row(\"a\", 37), Row(\"b\", 37), Row(\"c\", 37), Row(\"d\", 37)))\n    }\n  }\n\n  private def getTimestampForVersion(path: String, version: Long): String = {\n    val logPath = new File(path, \"_delta_log\")\n    val file = new File(logPath, f\"$version%020d.json\")\n    val sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\")\n    sdf.format(file.lastModified())\n  }\n\n  test(\"clone\") {\n    withTempPath { dir =>\n      val baseDir = dir.getAbsolutePath\n\n      val srcDir = new File(baseDir, \"source\").getCanonicalPath\n      val dstDir = new File(baseDir, \"destination\").getCanonicalPath\n\n      spark.range(10).write.format(\"delta\").save(srcDir)\n\n      val srcTable = io.delta.tables.DeltaTable.forPath(spark, srcDir)\n      srcTable.clone(dstDir, true)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(dstDir),\n        spark.read.format(\"delta\").load(srcDir))\n    }\n  }\n\n  private def writeOptimizeTestData(path: String): Unit = {\n    testData\n      .withColumn(\"col1\", col(\"value\") % 7)\n      .withColumn(\"col2\", col(\"value\") % 27)\n      .withColumn(\"p\", col(\"value\") % 10)\n      .repartition(numPartitions = 4)\n      .write.partitionBy(\"p\").format(\"delta\").save(path)\n  }\n\n  private def checkOptimizeMetrics(\n      result: DataFrame, numFilesAdded: Long, numFilesRemoved: Long): Unit = {\n    val metrics = result.select(\"metrics.*\").head()\n    assert(metrics.getLong(0) == numFilesAdded)\n    assert(metrics.getLong(1) == numFilesRemoved)\n  }\n\n  private def checkOptimizeHistory(\n      table: DeltaTable, expectedPredicates: Seq[String], expectedZorderCols: Seq[String]): Unit = {\n    val session = table.toDF.sparkSession\n    import session.implicits._\n\n    val (operation, operationParameters) = table.history()\n      .select(\"operation\", \"operationParameters\")\n      .as[(String, Map[String, String])]\n      .head()\n    assert(operation == \"OPTIMIZE\")\n    assert(operationParameters(\"predicate\") ==\n      expectedPredicates.map(p => s\"\"\"\\\"($p)\\\"\"\"\").mkString(start = \"[\", sep = \",\", end = \"]\"))\n    assert(operationParameters(\"zOrderBy\") ==\n      expectedZorderCols.map(c => s\"\"\"\\\"$c\\\"\"\"\").mkString(start = \"[\", sep = \",\", end = \"]\"))\n  }\n\n  test(\"optimize - compaction\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n\n      writeOptimizeTestData(path)\n\n      val numDataFilesPreCompaction =\n        spark.read.format(\"delta\").load(path)\n          .select(\"_metadata.file_path\")\n          .distinct().count()\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path)\n      val result = table.optimize().executeCompaction()\n\n      checkOptimizeMetrics(result, numFilesAdded = 10, numFilesRemoved = numDataFilesPreCompaction)\n      checkOptimizeHistory(table, expectedPredicates = Nil, expectedZorderCols = Nil)\n    }\n  }\n\n  test(\"optimize - compaction - with partition filter\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n\n      writeOptimizeTestData(path)\n\n      val numDataFilesPreCompaction =\n        spark.read.format(\"delta\").load(path)\n          .where(\"p = 2\")\n          .select(\"_metadata.file_path\")\n          .distinct().count()\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path)\n      val result = table.optimize().where(\"p = 2\").executeCompaction()\n\n      checkOptimizeMetrics(result, numFilesAdded = 1, numFilesRemoved = numDataFilesPreCompaction)\n      checkOptimizeHistory(table, expectedPredicates = Seq(\"'p = 2\"), expectedZorderCols = Nil)\n    }\n  }\n\n  test(\"optimize - zorder\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n\n      writeOptimizeTestData(path)\n\n      val numDataFilesPreZOrder =\n        spark.read.format(\"delta\").load(path)\n          .select(\"_metadata.file_path\")\n          .distinct().count()\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path)\n      val result = table.optimize().executeZOrderBy(\"col1\", \"col2\")\n\n      checkOptimizeMetrics(result, numFilesAdded = 10, numFilesRemoved = numDataFilesPreZOrder)\n      checkOptimizeHistory(\n        table, expectedPredicates = Nil, expectedZorderCols = Seq(\"col1\", \"col2\"))\n    }\n  }\n\n  test(\"optimize - zorder - with partition filter\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n\n      writeOptimizeTestData(path)\n\n      val numDataFilesPreZOrder =\n        spark.read.format(\"delta\").load(path)\n          .where(\"p = 2\")\n          .select(\"_metadata.file_path\")\n          .distinct().count()\n\n      val table = io.delta.tables.DeltaTable.forPath(spark, path)\n      val result = table.optimize().where(\"p = 2\").executeZOrderBy(\"col1\", \"col2\")\n\n      checkOptimizeMetrics(result, numFilesAdded = 1, numFilesRemoved = numDataFilesPreZOrder)\n      checkOptimizeHistory(\n        table, expectedPredicates = Seq(\"'p = 2\"), expectedZorderCols = Seq(\"col1\", \"col2\"))\n    }\n  }\n\n  test(\"cloneAtVersion/timestamp - with filesystem options\") {\n    val session = spark\n    import session.implicits._\n    withTempPath { dir =>\n      val baseDir = dir.getAbsolutePath\n\n      val srcDir = new File(baseDir, \"source\").getCanonicalPath\n      val dstDir1 = new File(baseDir, \"destination1\").getCanonicalPath\n      val dstDir2 = new File(baseDir, \"destination2\").getCanonicalPath\n\n      val df1 = Seq(1, 2, 3).toDF(\"id\")\n      val df2 = Seq(4, 5).toDF(\"id\")\n      val df3 = Seq(6, 7).toDF(\"id\")\n\n      // version 0.\n      df1.write.format(\"delta\").save(srcDir)\n      // version 1.\n      df2.write.format(\"delta\").mode(\"append\").save(srcDir)\n      // version 2.\n      df3.write.format(\"delta\").mode(\"append\").save(srcDir)\n\n      val srcTable = io.delta.tables.DeltaTable.forPath(spark, srcDir)\n\n      srcTable.cloneAtVersion(1, dstDir1, true)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(dstDir1),\n        df1.union(df2))\n\n      val timestamp = getTimestampForVersion(srcDir, version = 0)\n      srcTable.cloneAtTimestamp(timestamp, dstDir2, isShallow = true, replace = true)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(dstDir2),\n        df1)\n    }\n  }\n\n  test(\"restore\") {\n    val session = spark\n    import session.implicits._\n    withTempPath { dir =>\n      val path = dir.getPath\n\n      val df1 = Seq(1, 2, 3).toDF(\"id\")\n      val df2 = Seq(4, 5).toDF(\"id\")\n      val df3 = Seq(6, 7).toDF(\"id\")\n\n      // version 0.\n      df1.write.format(\"delta\").save(path)\n      // version 1.\n      df2.write.format(\"delta\").mode(\"append\").save(path)\n      // version 2.\n      df3.write.format(\"delta\").mode(\"append\").save(path)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        df1.union(df2).union(df3))\n\n      val deltaTable = io.delta.tables.DeltaTable.forPath(spark, path)\n      deltaTable.restoreToVersion(1)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        df1.union(df2))\n\n      val deltaTable2 = io.delta.tables.DeltaTable.forPath(spark, path)\n      val timestamp = getTimestampForVersion(path, version = 0)\n      deltaTable2.restoreToTimestamp(timestamp)\n\n      checkAnswer(\n        spark.read.format(\"delta\").load(path),\n        df1)\n    }\n  }\n\n  test(\"upgradeTableProtocol\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      testData.write.format(\"delta\").save(path)\n      val table = DeltaTable.forPath(spark, path)\n      table.upgradeTableProtocol(1, 2)\n      checkAnswer(\n        table.history().select(\"version\", \"operation\"),\n        Seq(Row(0L, \"WRITE\"), Row(1L, \"SET TBLPROPERTIES\"))\n      )\n    }\n  }\n\n  test(\"addFeatureSupport\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      testData.write.format(\"delta\").save(path)\n      val table = DeltaTable.forPath(spark, path)\n      checkAnswer(\n        table.history().select(\"version\", \"operation\"),\n        Seq(Row(0L, \"WRITE\"))\n      )\n      table.addFeatureSupport(\"testReaderWriter\")\n      checkAnswer(\n        table.history().select(\"version\", \"operation\"),\n        Seq(Row(0L, \"WRITE\"), Row(1L, \"SET TBLPROPERTIES\"))\n      )\n    }\n  }\n\n  test(\"dropFeatureSupport\") {\n    withTempPath { dir =>\n      val path = dir.getAbsolutePath\n      testData.write.format(\"delta\").save(path)\n      val table = DeltaTable.forPath(spark, path)\n      checkAnswer(\n        table.history().select(\"version\", \"operation\"),\n        Seq(Row(0L, \"WRITE\"))\n      )\n\n      table.addFeatureSupport(\"testRemovableWriter\")\n      checkAnswer(\n        table.history().select(\"version\", \"operation\"),\n        Seq(Row(0L, \"WRITE\"), Row(1L, \"SET TBLPROPERTIES\"))\n      )\n\n      // Attempt truncating the history when dropping a feature that is not required.\n      // This verifies the truncateHistory option was correctly passed.\n      assert(intercept[Exception] {\n        table.dropFeatureSupport(\"testRemovableWriter\", truncateHistory = true)\n      }.getMessage.contains(\"The particular feature does not require history truncation.\"))\n\n      table.dropFeatureSupport(\"testRemovableWriter\")\n      checkAnswer(\n        table.history().select(\"version\", \"operation\"),\n        Seq(Row(0L, \"WRITE\"), Row(1L, \"SET TBLPROPERTIES\"), Row(2L, \"DROP FEATURE\"))\n      )\n    }\n  }\n\n  test(\"DataFrameWriter V1 overwrite preserves partitioning information\") {\n    withTable(\"foo\") {\n      val data = Seq(\n        (1, \"Alice\", 29),\n        (2, \"Bob\", 35),\n        (3, \"Charlie\", 23)\n      )\n      val df = spark.createDataFrame(data).toDF(\"id\", \"name\", \"age\")\n      df.write.partitionBy(\"age\").format(\"delta\").saveAsTable(\"foo\")\n      val overwriteData = Seq(\n        (4, \"Flip\", 11),\n        (5, \"Flap\", 11),\n        (6, \"Flop\", 13),\n        (6, \"Flep\", 13)\n      )\n      val df1 = spark.createDataFrame(overwriteData).toDF(\"id\", \"name\", \"age\")\n      df1.write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .saveAsTable(\"foo\")\n      // Verify partitioning is preserved\n      assert(\n        DeltaTable\n          .forName(spark, \"foo\")\n          .detail()\n          .select(\"partitionColumns\")\n          .head()\n          .getSeq[String](0) == Seq(\"age\"))\n      // Verify row count after overwrite\n      assert(DeltaTable.forName(spark, \"foo\").toDF.count() == 4)\n    }\n  }\n\n  test(\"DataFrameWriter V1 replaceWhere preserves non-overwritten partitions\") {\n    withTable(\"foo\") {\n      val data = Seq(\n        (1, \"Alice\", 29),\n        (2, \"Bob\", 35),\n        (3, \"Charlie\", 23)\n      )\n      val df = spark.createDataFrame(data).toDF(\"id\", \"name\", \"age\")\n      df.write.partitionBy(\"age\").format(\"delta\").saveAsTable(\"foo\")\n      val overwriteData = Seq((4, \"Daniel\", 29), (5, \"Eve\", 29))\n      val df1 = spark.createDataFrame(overwriteData).toDF(\"id\", \"name\", \"age\")\n      df1.write\n        .format(\"delta\")\n        .option(\"replaceWhere\", \"age = 29\")\n        .mode(\"overwrite\")\n        .saveAsTable(\"foo\")\n      // Verify partitioning is preserved\n      assert(\n        DeltaTable\n          .forName(spark, \"foo\")\n          .detail()\n          .select(\"partitionColumns\")\n          .head()\n          .getSeq[String](0) == Seq(\"age\"))\n      // Verify row count - should have 4 rows (2 replaced + 2 preserved)\n      assert(DeltaTable.forName(spark, \"foo\").toDF.count() == 4)\n    }\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/client/src/test/scala/io/delta/connect/tables/RemoteSparkSession.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport java.io.{BufferedReader, File, InputStreamReader}\nimport java.util.concurrent.TimeUnit\n\nimport org.scalatest.{BeforeAndAfterAll, Suite}\n\nimport org.apache.spark.sql.connect.SparkSession\n\n/**\n * An util class to start a local Delta Connect server in a different process for local E2E tests.\n * Pre-running the tests, the Delta Connect artifact needs to be built using e.g. `build/sbt\n * assembly`. It is designed to start the server once but shared by all tests. It is equivalent to\n * use the following command to start the connect server via command line:\n *\n * {{{\n * bin/spark-shell \\\n * --jars `ls spark-connect/server/target/**/delta-connect-server-assembly*SNAPSHOT.jar | paste -sd ',' -` \\\n * --conf spark.plugins=org.apache.spark.sql.connect.SparkConnectPlugin\n * --conf spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension\n * --conf spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog\n * }}}\n *\n * Set system property `delta.test.home` or env variable `DELTA_HOME` if the test is not executed\n * from the Delta project top folder.\n */\ntrait RemoteSparkSession extends BeforeAndAfterAll { self: Suite =>\n\n  // TODO: Instead of hard-coding the server port, assign port number the same way as in Spark Connect.\n  private val serverPort = 15003\n  var spark: SparkSession = _\n\n  private val javaHome = System.getProperty(\"java.home\")\n\n  private val sparkHome = System.getProperty(\"delta.spark.home\")\n\n  private lazy val server = {\n    // We start SparkSubmit directly. This saves us from downloading an entire Spark distribution\n    // for a single test. The parameters used here are the ones that would have been used to start\n    // spark-submit.\n    val command = Seq.newBuilder[String]\n    command += s\"$javaHome/bin/java\"\n    // Uncomment for debugging the server process.\n    // command += \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005\"\n    command += \"-cp\" += sparkHome + \"/jars/*\"\n    command += \"-Xmx1g\"\n    command += \"-XX:+IgnoreUnrecognizedVMOptions\"\n    command += \"--add-modules=jdk.incubator.vector\"\n    command += \"--add-opens=java.base/java.lang=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.lang.invoke=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.lang.reflect=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.io=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.nio=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.net=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.util=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.util.concurrent=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/sun.nio.cs=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/sun.security.action=ALL-UNNAMED\"\n    command += \"--add-opens=java.base/sun.util.calendar=ALL-UNNAMED\"\n    command += \"--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED\"\n    command += \"-Djdk.reflect.useDirectMethodHandle=false\"\n    command += \"-Dio.netty.tryReflectionSetAccessible=true\"\n    command += \"-Dderby.connection.requireAuthentication=false\"\n    command += \"-Dlog4j.configurationFile=conf/log4j2.properties\"\n    command += \"org.apache.spark.deploy.SparkSubmit\"\n    command += \"--class\" += \"io.delta.tables.SimpleDeltaConnectService\"\n    command += \"--conf\" += s\"spark.connect.grpc.binding.port=$serverPort\"\n    command += \"--conf\" += \"spark.connect.extensions.relation.classes=\" +\n      \"org.apache.spark.sql.connect.delta.DeltaRelationPlugin\"\n    command += \"--conf\" += \"spark.connect.extensions.command.classes=\" +\n      \"org.apache.spark.sql.connect.delta.DeltaCommandPlugin\"\n    // Spark submit requires a jar. We pick one we know exists.\n    command += s\"$sparkHome/jars/unused-1.0.0.jar\"\n\n    val builder = new ProcessBuilder(command.result(): _*)\n    builder.environment().put(\"SPARK_HOME\", sparkHome)\n    builder.environment().put(\"SPARK_LOCAL_IP\", \"127.0.0.1\")\n    builder.directory(new File(sparkHome))\n    builder.redirectError(ProcessBuilder.Redirect.INHERIT)\n    builder.start()\n  }\n\n  private val SERVER_READY_TIMEOUT_SECONDS = 120\n\n  /**\n   * Wait for the server process to print \"Ready for client connections.\" to stdout,\n   * indicating the gRPC port is bound and accepting connections.\n   */\n  private def waitForServerReady(process: Process): Unit = {\n    val reader = new BufferedReader(new InputStreamReader(process.getInputStream))\n    val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(SERVER_READY_TIMEOUT_SECONDS)\n    var ready = false\n    var line: String = null\n    while (!ready && System.nanoTime() < deadline) {\n      line = reader.readLine()\n      if (line == null) {\n        throw new RuntimeException(\n          \"Spark Connect server process exited before becoming ready. \" +\n            s\"Exit code: ${process.exitValue()}\")\n      }\n      // Mirror to stdout so server logs are still visible in test output.\n      // scalastyle:off println\n      println(line)\n      // scalastyle:on println\n      if (line.contains(\"Ready for client connections.\")) {\n        ready = true\n      }\n    }\n    if (!ready) {\n      process.destroy()\n      throw new RuntimeException(\n        s\"Spark Connect server did not become ready within $SERVER_READY_TIMEOUT_SECONDS seconds\")\n    }\n    // Continue forwarding server stdout in background so the process doesn't block on a full\n    // output buffer.\n    val forwarder = new Thread(() => {\n      var l: String = null\n      while ({ l = reader.readLine(); l != null }) {\n        // scalastyle:off println\n        println(l)\n        // scalastyle:on println\n      }\n    })\n    forwarder.setDaemon(true)\n    forwarder.start()\n  }\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    val process = server\n    waitForServerReady(process)\n    spark = SparkSession.builder().remote(s\"sc://localhost:$serverPort\").create()\n  }\n\n  override def afterAll(): Unit = {\n    server.destroy()\n    super.afterAll()\n  }\n}\n\n"
  },
  {
    "path": "spark-connect/common/src/main/buf.gen.yaml",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nversion: v1\nplugins:\n  - plugin: buf.build/protocolbuffers/python:v21.7\n    out: gen/proto/python\n  - name: mypy\n    out: gen/proto/python\n"
  },
  {
    "path": "spark-connect/common/src/main/buf.work.yaml",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nversion: v1\ndirectories:\n  - protobuf\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/buf.yaml",
    "content": "#\n# Copyright (2024) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nversion: v1\nbreaking:\n  use:\n    - FILE\nlint:\n  use:\n    - DEFAULT\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/delta/connect/base.proto",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage delta.connect;\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.proto\";\n\n// Information required to access a Delta table either by name or by path.\nmessage DeltaTable {\n  // (Required)\n  oneof access_type {\n    Path path = 1;\n    string table_or_view_name = 2;\n  }\n\n  message Path {\n    // (Required) Path to the Delta table.\n    string path = 1;\n\n    // (Optional) Hadoop configuration used to access the file system.\n    map<string, string> hadoop_conf = 2;\n  }\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/delta/connect/commands.proto",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage delta.connect;\n\nimport \"delta/connect/base.proto\";\nimport \"spark/connect/types.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.proto\";\n\n// Message to hold all command extensions in Delta Connect.\nmessage DeltaCommand {\n  oneof command_type {\n    CloneTable clone_table = 1;\n    VacuumTable vacuum_table = 2;\n    UpgradeTableProtocol upgrade_table_protocol = 3;\n    Generate generate = 4;\n    CreateDeltaTable create_delta_table = 5;\n    AddFeatureSupport add_feature_support = 6;\n    DropFeatureSupport drop_feature_support = 7;    \n  }\n}\n\n// Command that creates a copy of a DeltaTable in the specified target location.\nmessage CloneTable {\n  // (Required) The source Delta table to clone.\n  DeltaTable table = 1;\n\n  // (Required) Path to the location where the cloned table should be stored.\n  string target = 2;\n\n  // (Optional) Optional parameter to clone a previous state of the source table. The current\n  // state of the table is cloned when it is not specified.\n  oneof version_or_timestamp {\n    // Clones the source table as of the provided version.\n    int32 version = 3;\n    // Clones the source table as of the provided timestamp.\n    string timestamp = 4;\n  }\n\n  // (Required) Performs a clone when true, this field should always be set to true.\n  bool is_shallow = 5;\n\n  // (Required) Overwrites the target location when true.\n  bool replace = 6;\n\n  // (Required) User-defined table properties that override properties with the same key in the\n  // source table.\n  map<string, string> properties = 7;\n}\n\n// Command that deletes files and directories in the table that are not needed by the table for\n// maintaining older versions up to the given retention threshold.\nmessage VacuumTable {\n  // (Required) The Delta table to vacuum.\n  DeltaTable table = 1;\n\n  // (Optional) Number of hours retain history for. If not specified, then the default retention\n  // period will be used.\n  optional double retention_hours = 2;\n}\n\n// Command to updates the protocol version of the table so that new features can be used.\nmessage UpgradeTableProtocol {\n  // (Required) The Delta table to upgrade the protocol of.\n  DeltaTable table = 1;\n\n  // (Required) The minimum required reader protocol version.\n  int32 reader_version = 2;\n\n  // (Required) The minimum required writer protocol version.\n  int32 writer_version = 3;\n}\n\n// Command that generates manifest files for a given Delta table.\nmessage Generate {\n  // (Required) The Delta table to generate the manifest files for.\n  DeltaTable table = 1;\n\n  // (Required) The type of manifest file to be generated.\n  string mode = 2;\n}\n\n// Command that creates or replace a Delta table (depending on the mode).\nmessage CreateDeltaTable {\n  enum Mode {\n    MODE_UNSPECIFIED = 0;\n    // Create the table if it does not exist, and throw an error otherwise.\n    MODE_CREATE = 1;\n    // Create the table if it does not exist, and do nothing otherwise.\n    MODE_CREATE_IF_NOT_EXISTS = 2;\n    // Replace the table if it already exists, and throw an error otherwise.\n    MODE_REPLACE = 3;\n    // Create the table if it does not exist, and replace it otherwise.\n    MODE_CREATE_OR_REPLACE = 4;\n  }\n\n  // Column in the schema of the table.\n  message Column {\n    message IdentityInfo {\n      // (Required) The start value of the identity column.\n      int64 start = 1;\n\n      // (Required) The increment value of the identity column.\n      int64 step = 2;\n\n      // (Required) Whether the identity column is BY DEFAULT (true) or ALWAYS (false).\n      bool allow_explicit_insert = 3;\n    }\n\n    // (Required) Name of the column.\n    string name = 1;\n\n    // (Required) Data type of the column.\n    spark.connect.DataType data_type = 2;\n\n    // (Required) Whether the column is nullable.\n    bool nullable = 3;\n\n    // (Optional) SQL Expression that is used to generate the values in the column.\n    optional string generated_always_as = 4;\n\n    // (Optional) Comment to describe the column.\n    optional string comment = 5;\n\n    // (Optional) Identity information for the column.\n    optional IdentityInfo identity_info = 6;\n  }\n\n  // (Required) Mode that determines what to do when a table with the given name or location\n  // already exists.\n  Mode mode = 1;\n\n  // (Optional) Qualified name of the table.\n  optional string table_name = 2;\n\n  // (Optional) Path to the directory where the table date is stored.\n  optional string location = 3;\n\n  // (Optional) Comment describing the table.\n  optional string comment = 4;\n\n  // (Optional) Columns in the schema of the table.\n  repeated Column columns = 5;\n\n  // (Optional) Columns used for partitioning the table.\n  repeated string partitioning_columns = 6;\n\n  // (Optional) Properties of the table.\n  map<string, string> properties = 7;\n\n  // (Optional) Columns used for clustering the table.\n  repeated string clustering_columns = 8;\n}\n\n// Command to add a supported feature to the table by modifying the protocol.\nmessage AddFeatureSupport {\n  // (Required) The Delta table to add the supported feature to.\n  DeltaTable table = 1;\n\n  // (Required) The name of the supported feature to add.\n  string feature_name = 2;\n}\n\n// Command to drop a supported feature from the table by modifying the protocol.\nmessage DropFeatureSupport {\n  // (Required) The Delta table to drop the supported feature from.\n  DeltaTable table = 1;\n\n  // (Required) The name of the supported feature to drop.\n  string feature_name = 2;\n\n  // (optional) Whether to truncate history. When not specified, history is not truncated.\n  optional bool truncate_history = 3;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/delta/connect/relations.proto",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage delta.connect;\n\nimport \"delta/connect/base.proto\";\nimport \"spark/connect/expressions.proto\";\nimport \"spark/connect/relations.proto\";\nimport \"spark/connect/types.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.proto\";\n\n// Message to hold all relation extensions in Delta Connect.\nmessage DeltaRelation {\n  oneof relation_type {\n    Scan scan = 1;\n    DescribeHistory describe_history = 2;\n    DescribeDetail describe_detail = 3;\n    ConvertToDelta convert_to_delta = 4;\n    RestoreTable restore_table = 5;\n    IsDeltaTable is_delta_table = 6;\n    DeleteFromTable delete_from_table = 7;\n    UpdateTable update_table = 8;\n    MergeIntoTable merge_into_table = 9;\n    OptimizeTable optimize_table = 10;    \n  }\n}\n\n// Relation that reads from a Delta table.\nmessage Scan {\n  // (Required) The Delta table to scan.\n  DeltaTable table = 1;\n}\n\n// Relation containing information of the latest commits on a Delta table.\n// The information is in reverse chronological order.\nmessage DescribeHistory {\n  // (Required) The Delta table to read the history of.\n  DeltaTable table = 1;\n}\n\n// Relation containing the details of a Delta table such as the format, name, and size.\nmessage DescribeDetail {\n  // (Required) The Delta table to describe the details of.\n  DeltaTable table = 1;\n}\n\n// Command that turns a Parquet table into a Delta table.\n//\n// This needs to be a Relation as it returns the identifier of the resulting table.\n// We cannot simply reuse the input identifier, as it could be a path-based identifier,\n// and in that case we need to replace \"parquet.`...`\" with \"delta.`...`\".\nmessage ConvertToDelta {\n  // (Required) Parquet table identifier formatted as \"parquet.`path`\"\n  string identifier = 1;\n\n  // (Optional) Partitioning schema of the input table\n  oneof partition_schema {\n    // Hive DDL formatted string\n    string partition_schema_string = 2;\n    // Struct with names and types of partitioning columns\n    spark.connect.DataType partition_schema_struct = 3;\n  }\n}\n\n// Command that restores the DeltaTable to an older version of the table specified by either a\n// version number or a timestamp.\n//\n// Needs to be a Relation, as it returns a row containing the execution metrics.\nmessage RestoreTable {\n  // (Required) The Delta table to restore to an earlier version.\n  DeltaTable table = 1;\n\n  // (Required) Version to restore to.\n  oneof version_or_timestamp {\n    // The version number to restore to.\n    int64 version = 2;\n    // The timestamp to restore to.\n    string timestamp = 3;\n  }\n}\n\n// Relation containing a single row containing a single boolean that indicates whether the provided\n// path contains a Delta table.\nmessage IsDeltaTable {\n  // (Required) The path to check.\n  string path = 1;\n}\n\n// Command that deletes data from the target table that matches the given condition.\n//\n// Needs to be a Relation, as it returns a row containing the execution metrics.\nmessage DeleteFromTable {\n  // (Required) Target table to delete data from. Must either be a DeltaRelation containing a Scan\n  // or a SubqueryAlias with a DeltaRelation containing a Scan as its input.\n  spark.connect.Relation target = 1;\n\n  // (Optional) Expression returning a boolean.\n  spark.connect.Expression condition = 2;\n}\n\n// Command that updates data in the target table using the given assignments for rows that matches\n// the given condition.\n//\n// Needs to be a Relation, as it returns a row containing the execution metrics.\nmessage UpdateTable {\n  // (Required) Target table to delete data from. Must either be a DeltaRelation containing a Scan\n  // or a SubqueryAlias with a DeltaRelation containing a Scan as its input.\n  spark.connect.Relation target = 1;\n\n  // (Optional) Condition that determines which rows must be updated.\n  // Must be an expression returning a boolean.\n  spark.connect.Expression condition = 2;\n\n  // (Optional) Set of assignments to apply to the rows matching the condition.\n  repeated Assignment assignments = 3;\n}\n\n// Command that merges a source query/table into a Delta table,\n//\n// Needs to be a Relation, as it returns a row containing the execution metrics.\nmessage MergeIntoTable {\n  // (Required) Target table to merge into.\n  spark.connect.Relation target = 1;\n\n  // (Required) Source data to merge from.\n  spark.connect.Relation source = 2;\n\n  // (Required) Condition for a source row to match with a target row.\n  spark.connect.Expression condition = 3;\n\n  // (Optional) Actions to apply when a source row matches a target row.\n  repeated Action matched_actions = 4;\n\n  // (Optional) Actions to apply when a source row does not match a target row.\n  repeated Action not_matched_actions = 5;\n\n  // (Optional) Actions to apply when a target row does not match a source row.\n  repeated Action not_matched_by_source_actions = 6;\n\n  // (Optional) Whether Schema Evolution is enabled for this command.\n  optional bool with_schema_evolution = 7;\n\n  // Rule that specifies how the target table should be modified.\n  message Action {\n    // (Optional) Condition for the action to be applied.\n    spark.connect.Expression condition = 1;\n\n    // (Required)\n    oneof action_type {\n      DeleteAction delete_action = 2;\n      UpdateAction update_action = 3;\n      UpdateStarAction update_star_action = 4;\n      InsertAction insert_action = 5;\n      InsertStarAction insert_star_action = 6;\n    }\n\n    // Action that deletes the target row.\n    message DeleteAction {}\n\n    // Action that updates the target row using a set of assignments.\n    message UpdateAction {\n      // (Optional) Set of assignments to apply.\n      repeated Assignment assignments = 1;\n    }\n\n    // Action that updates the target row by overwriting all columns.\n    message UpdateStarAction {}\n\n    // Action that inserts the source row into the target using a set of assignments.\n    message InsertAction {\n      // (Optional) Set of assignments to apply.\n      repeated Assignment assignments = 1;\n    }\n\n    // Action that inserts the source row into the target by setting all columns.\n    message InsertStarAction {}\n  }\n}\n\n// Represents an assignment of a value to a field.\nmessage Assignment {\n  // (Required) Expression identifying the (struct) field that is assigned a new value.\n  spark.connect.Expression field = 1;\n\n  // (Required) Expression that produces the value to assign to the field.\n  spark.connect.Expression value = 2;\n}\n\n\n// Command that optimizes the layout of a Delta table by either compacting small files or\n// by ordering the data. Allows specifying partition filters to limit the scope of the data\n// reorganization.\n//\n// Needs to be a Relation, as it returns a row containing the execution metrics.\nmessage OptimizeTable {\n  // (Required) The Delta table to optimize.\n  DeltaTable table = 1;\n\n  // (Optional) Partition filters that limit the operation to the files in the matched partitions.\n  repeated string partition_filters = 2;\n\n  // (Optional) Columns to z-order by. Compaction is performed when no z-order columns are provided.\n  repeated string zorder_columns = 3;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/base.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\nimport \"google/protobuf/any.proto\";\nimport \"spark/connect/commands.proto\";\nimport \"spark/connect/common.proto\";\nimport \"spark/connect/expressions.proto\";\nimport \"spark/connect/relations.proto\";\nimport \"spark/connect/types.proto\";\nimport \"spark/connect/ml.proto\";\nimport \"spark/connect/pipelines.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// A [[Plan]] is the structure that carries the runtime information for the execution from the\n// client to the server. A [[Plan]] can either be of the type [[Relation]] which is a reference\n// to the underlying logical plan or it can be of the [[Command]] type that is used to execute\n// commands on the server.\nmessage Plan {\n  oneof op_type {\n    Relation root = 1;\n    Command command = 2;\n  }\n}\n\n\n\n// User Context is used to refer to one particular user session that is executing\n// queries in the backend.\nmessage UserContext {\n  string user_id = 1;\n  string user_name = 2;\n\n  // To extend the existing user context message that is used to identify incoming requests,\n  // Spark Connect leverages the Any protobuf type that can be used to inject arbitrary other\n  // messages into this message. Extensions are stored as a `repeated` type to be able to\n  // handle multiple active extensions.\n  repeated google.protobuf.Any extensions = 999;\n}\n\n// Request to perform plan analyze, optionally to explain the plan.\nmessage AnalyzePlanRequest {\n  // (Required)\n  //\n  // The session_id specifies a spark session for a user id (which is specified\n  // by user_context.user_id). The session_id is set by the client to be able to\n  // collate streaming responses from different queries within the dedicated session.\n  // The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 17;\n\n  // (Required) User context\n  UserContext user_context = 2;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 3;\n\n  oneof analyze {\n    Schema schema = 4;\n    Explain explain = 5;\n    TreeString tree_string = 6;\n    IsLocal is_local = 7;\n    IsStreaming is_streaming = 8;\n    InputFiles input_files = 9;\n    SparkVersion spark_version = 10;\n    DDLParse ddl_parse = 11;\n    SameSemantics same_semantics = 12;\n    SemanticHash semantic_hash = 13;\n    Persist persist = 14;\n    Unpersist unpersist = 15;\n    GetStorageLevel get_storage_level = 16;\n    JsonToDDL json_to_ddl = 18;\n  }\n\n  message Schema {\n    // (Required) The logical plan to be analyzed.\n    Plan plan = 1;\n  }\n\n  // Explains the input plan based on a configurable mode.\n  message Explain {\n    // (Required) The logical plan to be analyzed.\n    Plan plan = 1;\n\n    // (Required) For analyzePlan rpc calls, configure the mode to explain plan in strings.\n    ExplainMode explain_mode = 2;\n\n    // Plan explanation mode.\n    enum ExplainMode {\n      EXPLAIN_MODE_UNSPECIFIED = 0;\n\n      // Generates only physical plan.\n      EXPLAIN_MODE_SIMPLE = 1;\n\n      // Generates parsed logical plan, analyzed logical plan, optimized logical plan and physical plan.\n      // Parsed Logical plan is a unresolved plan that extracted from the query. Analyzed logical plans\n      // transforms which translates unresolvedAttribute and unresolvedRelation into fully typed objects.\n      // The optimized logical plan transforms through a set of optimization rules, resulting in the\n      // physical plan.\n      EXPLAIN_MODE_EXTENDED = 2;\n\n      // Generates code for the statement, if any and a physical plan.\n      EXPLAIN_MODE_CODEGEN = 3;\n\n      // If plan node statistics are available, generates a logical plan and also the statistics.\n      EXPLAIN_MODE_COST = 4;\n\n      // Generates a physical plan outline and also node details.\n      EXPLAIN_MODE_FORMATTED = 5;\n    }\n  }\n\n  message TreeString {\n    // (Required) The logical plan to be analyzed.\n    Plan plan = 1;\n\n    // (Optional) Max level of the schema.\n    optional int32 level = 2;\n  }\n\n  message IsLocal {\n    // (Required) The logical plan to be analyzed.\n    Plan plan = 1;\n  }\n\n  message IsStreaming {\n    // (Required) The logical plan to be analyzed.\n    Plan plan = 1;\n  }\n\n  message InputFiles {\n    // (Required) The logical plan to be analyzed.\n    Plan plan = 1;\n  }\n\n  message SparkVersion { }\n\n  message DDLParse {\n    // (Required) The DDL formatted string to be parsed.\n    string ddl_string = 1;\n  }\n\n\n  // Returns `true` when the logical query plans  are equal and therefore return same results.\n  message SameSemantics {\n    // (Required) The plan to be compared.\n    Plan target_plan = 1;\n\n    // (Required) The other plan to be compared.\n    Plan other_plan = 2;\n  }\n\n  message SemanticHash {\n    // (Required) The logical plan to get a hashCode.\n    Plan plan = 1;\n  }\n\n  message Persist {\n    // (Required) The logical plan to persist.\n    Relation relation = 1;\n\n    // (Optional) The storage level.\n    optional StorageLevel storage_level = 2;\n  }\n\n  message Unpersist {\n    // (Required) The logical plan to unpersist.\n    Relation relation = 1;\n\n    // (Optional) Whether to block until all blocks are deleted.\n    optional bool blocking = 2;\n  }\n\n  message GetStorageLevel {\n    // (Required) The logical plan to get the storage level.\n    Relation relation = 1;\n  }\n\n  message JsonToDDL {\n    // (Required) The JSON formatted string to be converted to DDL.\n    string json_string = 1;\n  }\n}\n\n// Response to performing analysis of the query. Contains relevant metadata to be able to\n// reason about the performance.\n// Next ID: 16\nmessage AnalyzePlanResponse {\n  string session_id = 1;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 15;\n\n  oneof result {\n    Schema schema = 2;\n    Explain explain = 3;\n    TreeString tree_string = 4;\n    IsLocal is_local = 5;\n    IsStreaming is_streaming = 6;\n    InputFiles input_files = 7;\n    SparkVersion spark_version = 8;\n    DDLParse ddl_parse = 9;\n    SameSemantics same_semantics = 10;\n    SemanticHash semantic_hash = 11;\n    Persist persist = 12;\n    Unpersist unpersist = 13;\n    GetStorageLevel get_storage_level = 14;\n    JsonToDDL json_to_ddl = 16;\n  }\n\n  message Schema {\n    DataType schema = 1;\n  }\n\n  message Explain {\n    string explain_string = 1;\n  }\n\n  message TreeString {\n    string tree_string = 1;\n  }\n\n  message IsLocal {\n    bool is_local = 1;\n  }\n\n  message IsStreaming {\n    bool is_streaming = 1;\n  }\n\n  message InputFiles {\n    // A best-effort snapshot of the files that compose this Dataset\n    repeated string files = 1;\n  }\n\n  message SparkVersion {\n    string version = 1;\n  }\n\n  message DDLParse {\n    DataType parsed = 1;\n  }\n\n  message SameSemantics {\n    bool result = 1;\n  }\n\n  message SemanticHash {\n    int32 result = 1;\n  }\n\n  message Persist { }\n\n  message Unpersist { }\n\n  message GetStorageLevel {\n    // (Required) The StorageLevel as a result of get_storage_level request.\n    StorageLevel storage_level = 1;\n  }\n\n  message JsonToDDL {\n    string ddl_string = 1;\n  }\n}\n\n// A request to be executed by the service.\nmessage ExecutePlanRequest {\n  // (Required)\n  //\n  // The session_id specifies a spark session for a user id (which is specified\n  // by user_context.user_id). The session_id is set by the client to be able to\n  // collate streaming responses from different queries within the dedicated session.\n  // The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 8;\n\n  // (Required) User context\n  //\n  // user_context.user_id and session+id both identify a unique remote spark session on the\n  // server side.\n  UserContext user_context = 2;\n\n  // (Optional)\n  // Provide an id for this request. If not provided, it will be generated by the server.\n  // It is returned in every ExecutePlanResponse.operation_id of the ExecutePlan response stream.\n  // The id must be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  optional string operation_id = 6;\n\n  // (Required) The logical plan to be executed / analyzed.\n  Plan plan = 3;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 4;\n\n  // Repeated element for options that can be passed to the request. This element is currently\n  // unused but allows to pass in an extension value used for arbitrary options.\n  repeated RequestOption request_options = 5;\n\n  message RequestOption {\n    oneof request_option {\n      ReattachOptions reattach_options = 1;\n      // Extension type for request options\n      google.protobuf.Any extension = 999;\n    }\n  }\n\n  // Tags to tag the given execution with.\n  // Tags cannot contain ',' character and cannot be empty strings.\n  // Used by Interrupt with interrupt.tag.\n  repeated string tags = 7;\n}\n\n// The response of a query, can be one or more for each request. Responses belonging to the\n// same input query, carry the same `session_id`.\n// Next ID: 17\nmessage ExecutePlanResponse {\n  string session_id = 1;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 15;\n\n  // Identifies the ExecutePlan execution.\n  // If set by the client in ExecutePlanRequest.operationId, that value is returned.\n  // Otherwise generated by the server.\n  // It is an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string operation_id = 12;\n\n  // Identified the response in the stream.\n  // The id is an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string response_id = 13;\n\n  // Union type for the different response messages.\n  oneof response_type {\n    ArrowBatch arrow_batch = 2;\n\n    // Special case for executing SQL commands.\n    SqlCommandResult sql_command_result = 5;\n\n    // Response for a streaming query.\n    WriteStreamOperationStartResult write_stream_operation_start_result = 8;\n\n    // Response for commands on a streaming query.\n    StreamingQueryCommandResult streaming_query_command_result = 9;\n\n    // Response for 'SparkContext.resources'.\n    GetResourcesCommandResult get_resources_command_result = 10;\n\n    // Response for commands on the streaming query manager.\n    StreamingQueryManagerCommandResult streaming_query_manager_command_result = 11;\n\n    // Response for commands on the client side streaming query listener.\n    StreamingQueryListenerEventsResult streaming_query_listener_events_result = 16;\n\n    // Response type informing if the stream is complete in reattachable execution.\n    ResultComplete result_complete = 14;\n\n    // Response for command that creates ResourceProfile.\n    CreateResourceProfileCommandResult create_resource_profile_command_result = 17;\n\n    // (Optional) Intermediate query progress reports.\n    ExecutionProgress execution_progress = 18;\n\n    // Response for command that checkpoints a DataFrame.\n    CheckpointCommandResult checkpoint_command_result = 19;\n\n    // ML command response\n    MlCommandResult ml_command_result = 20;\n\n    // Response containing pipeline event that is streamed back to the client during a pipeline run\n    PipelineEventResult pipeline_event_result = 21;\n\n    // Pipeline command response\n    PipelineCommandResult pipeline_command_result = 22;\n\n    // Support arbitrary result objects.\n    google.protobuf.Any extension = 999;\n  }\n\n  // Metrics for the query execution. Typically, this field is only present in the last\n  // batch of results and then represent the overall state of the query execution.\n  Metrics metrics = 4;\n\n  // The metrics observed during the execution of the query plan.\n  repeated ObservedMetrics observed_metrics = 6;\n\n  // (Optional) The Spark schema. This field is available when `collect` is called.\n  DataType schema = 7;\n\n  // A SQL command returns an opaque Relation that can be directly used as input for the next\n  // call.\n  message SqlCommandResult {\n    Relation relation = 1;\n  }\n\n  // Batch results of metrics.\n  message ArrowBatch {\n    // Count rows in `data`. Must match the number of rows inside `data`.\n    int64 row_count = 1;\n    // Serialized Arrow data.\n    bytes data = 2;\n\n    // If set, row offset of the start of this ArrowBatch in execution results.\n    optional int64 start_offset = 3;\n  }\n\n  message Metrics {\n\n    repeated MetricObject metrics = 1;\n\n    message MetricObject {\n      string name = 1;\n      int64 plan_id = 2;\n      int64 parent = 3;\n      map<string, MetricValue> execution_metrics = 4;\n    }\n\n    message MetricValue {\n      string name = 1;\n      int64 value = 2;\n      string metric_type = 3;\n    }\n  }\n\n  message ObservedMetrics {\n    string name = 1;\n    repeated Expression.Literal values = 2;\n    repeated string keys = 3;\n    int64 plan_id = 4;\n  }\n\n  message ResultComplete {\n    // If present, in a reattachable execution this means that after server sends onComplete,\n    // the execution is complete. If the server sends onComplete without sending a ResultComplete,\n    // it means that there is more, and the client should use ReattachExecute RPC to continue.\n  }\n\n  // This message is used to communicate progress about the query progress during the execution.\n  message ExecutionProgress {\n    // Captures the progress of each individual stage.\n    repeated StageInfo stages = 1;\n\n    // Captures the currently in progress tasks.\n    int64 num_inflight_tasks = 2;\n\n    message StageInfo {\n      int64 stage_id = 1;\n      int64 num_tasks = 2;\n      int64 num_completed_tasks = 3;\n      int64 input_bytes_read = 4;\n      bool done = 5;\n    }\n  }\n}\n\n// The key-value pair for the config request and response.\nmessage KeyValue {\n  // (Required) The key.\n  string key = 1;\n  // (Optional) The value.\n  optional string value = 2;\n}\n\n// Request to update or fetch the configurations.\nmessage ConfigRequest {\n  // (Required)\n  //\n  // The session_id specifies a spark session for a user id (which is specified\n  // by user_context.user_id). The session_id is set by the client to be able to\n  // collate streaming responses from different queries within the dedicated session.\n  // The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 8;\n\n  // (Required) User context\n  UserContext user_context = 2;\n\n  // (Required) The operation for the config.\n  Operation operation = 3;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 4;\n\n  message Operation {\n    oneof op_type {\n      Set set = 1;\n      Get get = 2;\n      GetWithDefault get_with_default = 3;\n      GetOption get_option = 4;\n      GetAll get_all = 5;\n      Unset unset = 6;\n      IsModifiable is_modifiable = 7;\n    }\n  }\n\n  message Set {\n    // (Required) The config key-value pairs to set.\n    repeated KeyValue pairs = 1;\n\n    // (Optional) Whether to ignore failures.\n    optional bool silent = 2;\n  }\n\n  message Get {\n    // (Required) The config keys to get.\n    repeated string keys = 1;\n  }\n\n  message GetWithDefault {\n    // (Required) The config key-value pairs to get. The value will be used as the default value.\n    repeated KeyValue pairs = 1;\n  }\n\n  message GetOption {\n    // (Required) The config keys to get optionally.\n    repeated string keys = 1;\n  }\n\n  message GetAll {\n    // (Optional) The prefix of the config key to get.\n    optional string prefix = 1;\n  }\n\n  message Unset {\n    // (Required) The config keys to unset.\n    repeated string keys = 1;\n  }\n\n  message IsModifiable {\n    // (Required) The config keys to check the config is modifiable.\n    repeated string keys = 1;\n  }\n}\n\n// Response to the config request.\n// Next ID: 5\nmessage ConfigResponse {\n  string session_id = 1;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 4;\n\n  // (Optional) The result key-value pairs.\n  //\n  // Available when the operation is 'Get', 'GetWithDefault', 'GetOption', 'GetAll'.\n  // Also available for the operation 'IsModifiable' with boolean string \"true\" and \"false\".\n  repeated KeyValue pairs = 2;\n\n  // (Optional)\n  //\n  // Warning messages for deprecated or unsupported configurations.\n  repeated string warnings = 3;\n}\n\n// Request to transfer client-local artifacts.\nmessage AddArtifactsRequest {\n\n  // (Required)\n  //\n  // The session_id specifies a spark session for a user id (which is specified\n  // by user_context.user_id). The session_id is set by the client to be able to\n  // collate streaming responses from different queries within the dedicated session.\n  // The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string session_id = 1;\n\n  // User context\n  UserContext user_context = 2;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 7;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 6;\n\n  // A chunk of an Artifact.\n  message ArtifactChunk {\n    // Data chunk.\n    bytes data = 1;\n    // CRC to allow server to verify integrity of the chunk.\n    int64 crc = 2;\n  }\n\n  // An artifact that is contained in a single `ArtifactChunk`.\n  // Generally, this message represents tiny artifacts such as REPL-generated class files.\n  message SingleChunkArtifact {\n    // The name of the artifact is expected in the form of a \"Relative Path\" that is made up of a\n    // sequence of directories and the final file element.\n    // Examples of \"Relative Path\"s: \"jars/test.jar\", \"classes/xyz.class\", \"abc.xyz\", \"a/b/X.jar\".\n    // The server is expected to maintain the hierarchy of files as defined by their name. (i.e\n    // The relative path of the file on the server's filesystem will be the same as the name of\n    // the provided artifact)\n    string name = 1;\n    // A single data chunk.\n    ArtifactChunk data = 2;\n  }\n\n  // A number of `SingleChunkArtifact` batched into a single RPC.\n  message Batch {\n    repeated SingleChunkArtifact artifacts = 1;\n  }\n\n  // Signals the beginning/start of a chunked artifact.\n  // A large artifact is transferred through a payload of `BeginChunkedArtifact` followed by a\n  // sequence of `ArtifactChunk`s.\n  message BeginChunkedArtifact {\n    // Name of the artifact undergoing chunking. Follows the same conventions as the `name` in\n    // the `Artifact` message.\n    string name = 1;\n    // Total size of the artifact in bytes.\n    int64 total_bytes = 2;\n    // Number of chunks the artifact is split into.\n    // This includes the `initial_chunk`.\n    int64 num_chunks = 3;\n    // The first/initial chunk.\n    ArtifactChunk initial_chunk = 4;\n  }\n\n  // The payload is either a batch of artifacts or a partial chunk of a large artifact.\n  oneof payload {\n    Batch batch = 3;\n    // The metadata and the initial chunk of a large artifact chunked into multiple requests.\n    // The server side is notified about the total size of the large artifact as well as the\n    // number of chunks to expect.\n    BeginChunkedArtifact begin_chunk = 4;\n    // A chunk of an artifact excluding metadata. This can be any chunk of a large artifact\n    // excluding the first chunk (which is included in `BeginChunkedArtifact`).\n    ArtifactChunk chunk = 5;\n  }\n}\n\n// Response to adding an artifact. Contains relevant metadata to verify successful transfer of\n// artifact(s).\n// Next ID: 4\nmessage AddArtifactsResponse {\n  // Session id in which the AddArtifact was running.\n  string session_id = 2;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 3;\n\n  // The list of artifact(s) seen by the server.\n  repeated ArtifactSummary artifacts = 1;\n\n  // Metadata of an artifact.\n  message ArtifactSummary {\n    string name = 1;\n    // Whether the CRC (Cyclic Redundancy Check) is successful on server verification.\n    // The server discards any artifact that fails the CRC.\n    // If false, the client may choose to resend the artifact specified by `name`.\n    bool is_crc_successful = 2;\n  }\n}\n\n// Request to get current statuses of artifacts at the server side.\nmessage ArtifactStatusesRequest {\n  // (Required)\n  //\n  // The session_id specifies a spark session for a user id (which is specified\n  // by user_context.user_id). The session_id is set by the client to be able to\n  // collate streaming responses from different queries within the dedicated session.\n  // The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 5;\n\n  // User context\n  UserContext user_context = 2;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 3;\n\n  // The name of the artifact is expected in the form of a \"Relative Path\" that is made up of a\n  // sequence of directories and the final file element.\n  // Examples of \"Relative Path\"s: \"jars/test.jar\", \"classes/xyz.class\", \"abc.xyz\", \"a/b/X.jar\".\n  // The server is expected to maintain the hierarchy of files as defined by their name. (i.e\n  // The relative path of the file on the server's filesystem will be the same as the name of\n  // the provided artifact)\n  repeated string names = 4;\n}\n\n// Response to checking artifact statuses.\n// Next ID: 4\nmessage ArtifactStatusesResponse {\n  // Session id in which the ArtifactStatus was running.\n  string session_id = 2;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 3;\n  // A map of artifact names to their statuses.\n  map<string, ArtifactStatus> statuses = 1;\n\n  message ArtifactStatus {\n    // Exists or not particular artifact at the server.\n    bool exists = 1;\n  }\n}\n\nmessage InterruptRequest {\n  // (Required)\n  //\n  // The session_id specifies a spark session for a user id (which is specified\n  // by user_context.user_id). The session_id is set by the client to be able to\n  // collate streaming responses from different queries within the dedicated session.\n  // The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 7;\n\n  // (Required) User context\n  UserContext user_context = 2;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 3;\n\n  // (Required) The type of interrupt to execute.\n  InterruptType interrupt_type = 4;\n\n  enum InterruptType {\n    INTERRUPT_TYPE_UNSPECIFIED = 0;\n\n    // Interrupt all running executions within the session with the provided session_id.\n    INTERRUPT_TYPE_ALL = 1;\n\n    // Interrupt all running executions within the session with the provided operation_tag.\n    INTERRUPT_TYPE_TAG = 2;\n\n    // Interrupt the running execution within the session with the provided operation_id.\n    INTERRUPT_TYPE_OPERATION_ID = 3;\n  }\n\n  oneof interrupt {\n    // if interrupt_tag == INTERRUPT_TYPE_TAG, interrupt operation with this tag.\n    string operation_tag = 5;\n\n    // if interrupt_tag == INTERRUPT_TYPE_OPERATION_ID, interrupt operation with this operation_id.\n    string operation_id = 6;\n  }\n}\n\n// Next ID: 4\nmessage InterruptResponse {\n  // Session id in which the interrupt was running.\n  string session_id = 1;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 3;\n\n  // Operation ids of the executions which were interrupted.\n  repeated string interrupted_ids = 2;\n\n}\n\nmessage ReattachOptions {\n  // If true, the request can be reattached to using ReattachExecute.\n  // ReattachExecute can be used either if the stream broke with a GRPC network error,\n  // or if the server closed the stream without sending a response with StreamStatus.complete=true.\n  // The server will keep a buffer of responses in case a response is lost, and\n  // ReattachExecute needs to back-track.\n  //\n  // If false, the execution response stream will will not be reattachable, and all responses are\n  // immediately released by the server after being sent.\n  bool reattachable = 1;\n}\n\nmessage ReattachExecuteRequest {\n  // (Required)\n  //\n  // The session_id of the request to reattach to.\n  // This must be an id of existing session.\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 6;\n\n  // (Required) User context\n  //\n  // user_context.user_id and session+id both identify a unique remote spark session on the\n  // server side.\n  UserContext user_context = 2;\n\n  // (Required)\n  // Provide an id of the request to reattach to.\n  // This must be an id of existing operation.\n  string operation_id = 3;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 4;\n\n  // (Optional)\n  // Last already processed response id from the response stream.\n  // After reattach, server will resume the response stream after that response.\n  // If not specified, server will restart the stream from the start.\n  //\n  // Note: server controls the amount of responses that it buffers and it may drop responses,\n  // that are far behind the latest returned response, so this can't be used to arbitrarily\n  // scroll back the cursor. If the response is no longer available, this will result in an error.\n  optional string last_response_id = 5;\n}\n\nmessage ReleaseExecuteRequest {\n  // (Required)\n  //\n  // The session_id of the request to reattach to.\n  // This must be an id of existing session.\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 7;\n\n  // (Required) User context\n  //\n  // user_context.user_id and session+id both identify a unique remote spark session on the\n  // server side.\n  UserContext user_context = 2;\n\n  // (Required)\n  // Provide an id of the request to reattach to.\n  // This must be an id of existing operation.\n  string operation_id = 3;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 4;\n\n  // Release and close operation completely.\n  // This will also interrupt the query if it is running execution, and wait for it to be torn down.\n  message ReleaseAll {}\n\n  // Release all responses from the operation response stream up to and including\n  // the response with the given by response_id.\n  // While server determines by itself how much of a buffer of responses to keep, client providing\n  // explicit release calls will help reduce resource consumption.\n  // Noop if response_id not found in cached responses.\n  message ReleaseUntil {\n    string response_id = 1;\n  }\n\n  oneof release {\n    ReleaseAll release_all = 5;\n    ReleaseUntil release_until = 6;\n  }\n}\n\n// Next ID: 4\nmessage ReleaseExecuteResponse {\n  // Session id in which the release was running.\n  string session_id = 1;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 3;\n\n  // Operation id of the operation on which the release executed.\n  // If the operation couldn't be found (because e.g. it was concurrently released), will be unset.\n  // Otherwise, it will be equal to the operation_id from request.\n  optional string operation_id = 2;\n}\n\nmessage ReleaseSessionRequest {\n  // (Required)\n  //\n  // The session_id of the request to reattach to.\n  // This must be an id of existing session.\n  string session_id = 1;\n\n  // (Required) User context\n  //\n  // user_context.user_id and session+id both identify a unique remote spark session on the\n  // server side.\n  UserContext user_context = 2;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 3;\n\n  // Signals the server to allow the client to reconnect to the session after it is released.\n  //\n  // By default, the server tombstones the session upon release, preventing reconnections and\n  // fully cleaning the session state.\n  //\n  // If this flag is set to true, the server may permit the client to reconnect to the session\n  // post-release, even if the session state has been cleaned. This can result in missing state,\n  // such as Temporary Views, Temporary UDFs, or the Current Catalog, in the reconnected session.\n  //\n  // Use this option sparingly and only when the client fully understands the implications of\n  // reconnecting to a released session. The client must ensure that any queries executed do not\n  // rely on the session state prior to its release.\n  bool allow_reconnect = 4;\n}\n\n// Next ID: 3\nmessage ReleaseSessionResponse {\n  // Session id of the session on which the release executed.\n  string session_id = 1;\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 2;\n}\n\nmessage FetchErrorDetailsRequest {\n\n  // (Required)\n  // The session_id specifies a Spark session for a user identified by user_context.user_id.\n  // The id should be a UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`.\n  string session_id = 1;\n\n  // (Optional)\n  //\n  // Server-side generated idempotency key from the previous responses (if any). Server\n  // can use this to validate that the server side session has not changed.\n  optional string client_observed_server_side_session_id = 5;\n\n  // User context\n  UserContext user_context = 2;\n\n  // (Required)\n  // The id of the error.\n  string error_id = 3;\n\n  // Provides optional information about the client sending the request. This field\n  // can be used for language or version specific information and is only intended for\n  // logging purposes and will not be interpreted by the server.\n  optional string client_type = 4;\n}\n\n// Next ID: 5\nmessage FetchErrorDetailsResponse {\n\n  // Server-side generated idempotency key that the client can use to assert that the server side\n  // session has not changed.\n  string server_side_session_id = 3;\n\n  string session_id = 4;\n\n  // The index of the root error in errors. The field will not be set if the error is not found.\n  optional int32 root_error_idx = 1;\n\n  // A list of errors.\n  repeated Error errors = 2;\n\n  message StackTraceElement {\n    // The fully qualified name of the class containing the execution point.\n    string declaring_class = 1;\n\n    // The name of the method containing the execution point.\n    string method_name = 2;\n\n    // The name of the file containing the execution point.\n    optional string file_name = 3;\n\n    // The line number of the source line containing the execution point.\n    int32 line_number = 4;\n  }\n\n  // QueryContext defines the schema for the query context of a SparkThrowable.\n  // It helps users understand where the error occurs while executing queries.\n  message QueryContext {\n    // The type of this query context.\n    enum ContextType {\n      SQL = 0;\n      DATAFRAME = 1;\n    }\n    ContextType context_type = 10;\n\n    // The object type of the query which throws the exception.\n    // If the exception is directly from the main query, it should be an empty string.\n    // Otherwise, it should be the exact object type in upper case. For example, a \"VIEW\".\n    string object_type = 1;\n\n    // The object name of the query which throws the exception.\n    // If the exception is directly from the main query, it should be an empty string.\n    // Otherwise, it should be the object name. For example, a view name \"V1\".\n    string object_name = 2;\n\n    // The starting index in the query text which throws the exception. The index starts from 0.\n    int32 start_index = 3;\n\n    // The stopping index in the query which throws the exception. The index starts from 0.\n    int32 stop_index = 4;\n\n    // The corresponding fragment of the query which throws the exception.\n    string fragment = 5;\n\n    // The user code (call site of the API) that caused throwing the exception.\n    string call_site = 6;\n\n    // Summary of the exception cause.\n    string summary = 7;\n  }\n\n  // SparkThrowable defines the schema for SparkThrowable exceptions.\n  message SparkThrowable {\n    // Succinct, human-readable, unique, and consistent representation of the error category.\n    optional string error_class = 1;\n\n    // The message parameters for the error framework.\n    map<string, string> message_parameters = 2;\n\n    // The query context of a SparkThrowable.\n    repeated QueryContext query_contexts = 3;\n\n    // Portable error identifier across SQL engines\n    // If null, error class or SQLSTATE is not set.\n    optional string sql_state = 4;\n  }\n\n  // Error defines the schema for the representing exception.\n  message Error {\n    // The fully qualified names of the exception class and its parent classes.\n    repeated string error_type_hierarchy = 1;\n\n    // The detailed message of the exception.\n    string message = 2;\n\n    // The stackTrace of the exception. It will be set\n    // if the SQLConf spark.sql.connect.serverStacktrace.enabled is true.\n    repeated StackTraceElement stack_trace = 3;\n\n    // The index of the cause error in errors.\n    optional int32 cause_idx = 4;\n\n    // The structured data of a SparkThrowable exception.\n    optional SparkThrowable spark_throwable = 5;\n  }\n}\n\nmessage CheckpointCommandResult {\n  // (Required) The logical plan checkpointed.\n  CachedRemoteRelation relation = 1;\n}\n\n// Main interface for the SparkConnect service.\nservice SparkConnectService {\n\n  // Executes a request that contains the query and returns a stream of [[Response]].\n  //\n  // It is guaranteed that there is at least one ARROW batch returned even if the result set is empty.\n  rpc ExecutePlan(ExecutePlanRequest) returns (stream ExecutePlanResponse) {}\n\n  // Analyzes a query and returns a [[AnalyzeResponse]] containing metadata about the query.\n  rpc AnalyzePlan(AnalyzePlanRequest) returns (AnalyzePlanResponse) {}\n\n  // Update or fetch the configurations and returns a [[ConfigResponse]] containing the result.\n  rpc Config(ConfigRequest) returns (ConfigResponse) {}\n\n  // Add artifacts to the session and returns a [[AddArtifactsResponse]] containing metadata about\n  // the added artifacts.\n  rpc AddArtifacts(stream AddArtifactsRequest) returns (AddArtifactsResponse) {}\n\n  // Check statuses of artifacts in the session and returns them in a [[ArtifactStatusesResponse]]\n  rpc ArtifactStatus(ArtifactStatusesRequest) returns (ArtifactStatusesResponse) {}\n\n  // Interrupts running executions\n  rpc Interrupt(InterruptRequest) returns (InterruptResponse) {}\n\n  // Reattach to an existing reattachable execution.\n  // The ExecutePlan must have been started with ReattachOptions.reattachable=true.\n  // If the ExecutePlanResponse stream ends without a ResultComplete message, there is more to\n  // continue. If there is a ResultComplete, the client should use ReleaseExecute with\n  rpc ReattachExecute(ReattachExecuteRequest) returns (stream ExecutePlanResponse) {}\n\n  // Release an reattachable execution, or parts thereof.\n  // The ExecutePlan must have been started with ReattachOptions.reattachable=true.\n  // Non reattachable executions are released automatically and immediately after the ExecutePlan\n  // RPC and ReleaseExecute may not be used.\n  rpc ReleaseExecute(ReleaseExecuteRequest) returns (ReleaseExecuteResponse) {}\n\n  // Release a session.\n  // All the executions in the session will be released. Any further requests for the session with\n  // that session_id for the given user_id will fail. If the session didn't exist or was already\n  // released, this is a noop.\n  rpc ReleaseSession(ReleaseSessionRequest) returns (ReleaseSessionResponse) {}\n\n  // FetchErrorDetails retrieves the matched exception with details based on a provided error id.\n  rpc FetchErrorDetails(FetchErrorDetailsRequest) returns (FetchErrorDetailsResponse) {}\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/catalog.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\nimport \"spark/connect/common.proto\";\nimport \"spark/connect/types.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// Catalog messages are marked as unstable.\nmessage Catalog {\n  oneof cat_type {\n    CurrentDatabase current_database = 1;\n    SetCurrentDatabase set_current_database = 2;\n    ListDatabases list_databases = 3;\n    ListTables list_tables = 4;\n    ListFunctions list_functions = 5;\n    ListColumns list_columns = 6;\n    GetDatabase get_database = 7;\n    GetTable get_table = 8;\n    GetFunction get_function = 9;\n    DatabaseExists database_exists = 10;\n    TableExists table_exists = 11;\n    FunctionExists function_exists = 12;\n    CreateExternalTable create_external_table = 13;\n    CreateTable create_table = 14;\n    DropTempView drop_temp_view = 15;\n    DropGlobalTempView drop_global_temp_view = 16;\n    RecoverPartitions recover_partitions = 17;\n    IsCached is_cached = 18;\n    CacheTable cache_table = 19;\n    UncacheTable uncache_table = 20;\n    ClearCache clear_cache = 21;\n    RefreshTable refresh_table = 22;\n    RefreshByPath refresh_by_path = 23;\n    CurrentCatalog current_catalog = 24;\n    SetCurrentCatalog set_current_catalog = 25;\n    ListCatalogs list_catalogs = 26;\n  }\n}\n\n// See `spark.catalog.currentDatabase`\nmessage CurrentDatabase { }\n\n// See `spark.catalog.setCurrentDatabase`\nmessage SetCurrentDatabase {\n  // (Required)\n  string db_name = 1;\n}\n\n// See `spark.catalog.listDatabases`\nmessage ListDatabases {\n  // (Optional) The pattern that the database name needs to match\n  optional string pattern = 1;\n}\n\n// See `spark.catalog.listTables`\nmessage ListTables {\n  // (Optional)\n  optional string db_name = 1;\n  // (Optional) The pattern that the table name needs to match\n  optional string pattern = 2;\n}\n\n// See `spark.catalog.listFunctions`\nmessage ListFunctions {\n  // (Optional)\n  optional string db_name = 1;\n  // (Optional) The pattern that the function name needs to match\n  optional string pattern = 2;\n}\n\n// See `spark.catalog.listColumns`\nmessage ListColumns {\n  // (Required)\n  string table_name = 1;\n  // (Optional)\n  optional string db_name = 2;\n}\n\n// See `spark.catalog.getDatabase`\nmessage GetDatabase {\n  // (Required)\n  string db_name = 1;\n}\n\n// See `spark.catalog.getTable`\nmessage GetTable {\n  // (Required)\n  string table_name = 1;\n  // (Optional)\n  optional string db_name = 2;\n}\n\n// See `spark.catalog.getFunction`\nmessage GetFunction {\n  // (Required)\n  string function_name = 1;\n  // (Optional)\n  optional string db_name = 2;\n}\n\n// See `spark.catalog.databaseExists`\nmessage DatabaseExists {\n  // (Required)\n  string db_name = 1;\n}\n\n// See `spark.catalog.tableExists`\nmessage TableExists {\n  // (Required)\n  string table_name = 1;\n  // (Optional)\n  optional string db_name = 2;\n}\n\n// See `spark.catalog.functionExists`\nmessage FunctionExists {\n  // (Required)\n  string function_name = 1;\n  // (Optional)\n  optional string db_name = 2;\n}\n\n// See `spark.catalog.createExternalTable`\nmessage CreateExternalTable {\n  // (Required)\n  string table_name = 1;\n  // (Optional)\n  optional string path = 2;\n  // (Optional)\n  optional string source = 3;\n  // (Optional)\n  optional DataType schema = 4;\n  // Options could be empty for valid data source format.\n  // The map key is case insensitive.\n  map<string, string> options = 5;\n}\n\n// See `spark.catalog.createTable`\nmessage CreateTable {\n  // (Required)\n  string table_name = 1;\n  // (Optional)\n  optional string path = 2;\n  // (Optional)\n  optional string source = 3;\n  // (Optional)\n  optional string description = 4;\n  // (Optional)\n  optional DataType schema = 5;\n  // Options could be empty for valid data source format.\n  // The map key is case insensitive.\n  map<string, string> options = 6;\n}\n\n// See `spark.catalog.dropTempView`\nmessage DropTempView {\n  // (Required)\n  string view_name = 1;\n}\n\n// See `spark.catalog.dropGlobalTempView`\nmessage DropGlobalTempView {\n  // (Required)\n  string view_name = 1;\n}\n\n// See `spark.catalog.recoverPartitions`\nmessage RecoverPartitions {\n  // (Required)\n  string table_name = 1;\n}\n\n// See `spark.catalog.isCached`\nmessage IsCached {\n  // (Required)\n  string table_name = 1;\n}\n\n// See `spark.catalog.cacheTable`\nmessage CacheTable {\n  // (Required)\n  string table_name = 1;\n\n  // (Optional)\n  optional StorageLevel storage_level = 2;\n}\n\n// See `spark.catalog.uncacheTable`\nmessage UncacheTable {\n  // (Required)\n  string table_name = 1;\n}\n\n// See `spark.catalog.clearCache`\nmessage ClearCache { }\n\n// See `spark.catalog.refreshTable`\nmessage RefreshTable {\n  // (Required)\n  string table_name = 1;\n}\n\n// See `spark.catalog.refreshByPath`\nmessage RefreshByPath {\n  // (Required)\n  string path = 1;\n}\n\n// See `spark.catalog.currentCatalog`\nmessage CurrentCatalog { }\n\n// See `spark.catalog.setCurrentCatalog`\nmessage SetCurrentCatalog {\n  // (Required)\n  string catalog_name = 1;\n}\n\n// See `spark.catalog.listCatalogs`\nmessage ListCatalogs {\n  // (Optional) The pattern that the catalog name needs to match\n  optional string pattern = 1;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/commands.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\nimport \"google/protobuf/any.proto\";\nimport \"spark/connect/common.proto\";\nimport \"spark/connect/expressions.proto\";\nimport \"spark/connect/relations.proto\";\nimport \"spark/connect/ml.proto\";\nimport \"spark/connect/pipelines.proto\";\n\npackage spark.connect;\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// A [[Command]] is an operation that is executed by the server that does not directly consume or\n// produce a relational result.\nmessage Command {\n  oneof command_type {\n    CommonInlineUserDefinedFunction register_function = 1;\n    WriteOperation write_operation = 2;\n    CreateDataFrameViewCommand create_dataframe_view = 3;\n    WriteOperationV2 write_operation_v2 = 4;\n    SqlCommand sql_command = 5;\n    WriteStreamOperationStart write_stream_operation_start = 6;\n    StreamingQueryCommand streaming_query_command = 7;\n    GetResourcesCommand get_resources_command = 8;\n    StreamingQueryManagerCommand streaming_query_manager_command = 9;\n    CommonInlineUserDefinedTableFunction register_table_function = 10;\n    StreamingQueryListenerBusCommand streaming_query_listener_bus_command = 11;\n    CommonInlineUserDefinedDataSource register_data_source = 12;\n    CreateResourceProfileCommand create_resource_profile_command = 13;\n    CheckpointCommand checkpoint_command = 14;\n    RemoveCachedRemoteRelationCommand remove_cached_remote_relation_command = 15;\n    MergeIntoTableCommand merge_into_table_command = 16;\n    MlCommand ml_command = 17;\n    ExecuteExternalCommand execute_external_command = 18;\n    PipelineCommand pipeline_command = 19;\n\n    // This field is used to mark extensions to the protocol. When plugins generate arbitrary\n    // Commands they can add them here. During the planning the correct resolution is done.\n    google.protobuf.Any extension = 999;\n\n  }\n}\n\n// A SQL Command is used to trigger the eager evaluation of SQL commands in Spark.\n//\n// When the SQL provide as part of the message is a command it will be immediately evaluated\n// and the result will be collected and returned as part of a LocalRelation. If the result is\n// not a command, the operation will simply return a SQL Relation. This allows the client to be\n// almost oblivious to the server-side behavior.\nmessage SqlCommand {\n  // (Required) SQL Query.\n  string sql = 1 [deprecated=true];\n\n  // (Optional) A map of parameter names to literal expressions.\n  map<string, Expression.Literal> args = 2 [deprecated=true];\n\n  // (Optional) A sequence of literal expressions for positional parameters in the SQL query text.\n  repeated Expression.Literal pos_args = 3 [deprecated=true];\n\n  // (Optional) A map of parameter names to expressions.\n  // It cannot coexist with `pos_arguments`.\n  map<string, Expression> named_arguments = 4 [deprecated=true];\n\n  // (Optional) A sequence of expressions for positional parameters in the SQL query text.\n  // It cannot coexist with `named_arguments`.\n  repeated Expression pos_arguments = 5 [deprecated=true];\n\n  // (Optional) The relation that this SQL command will be built on.\n  Relation input = 6;\n}\n\n// A command that can create DataFrame global temp view or local temp view.\nmessage CreateDataFrameViewCommand {\n  // (Required) The relation that this view will be built on.\n  Relation input = 1;\n\n  // (Required) View name.\n  string name = 2;\n\n  // (Required) Whether this is global temp view or local temp view.\n  bool is_global = 3;\n\n  // (Required)\n  //\n  // If true, and if the view already exists, updates it; if false, and if the view\n  // already exists, throws exception.\n  bool replace = 4;\n}\n\n// As writes are not directly handled during analysis and planning, they are modeled as commands.\nmessage WriteOperation {\n  // (Required) The output of the `input` relation will be persisted according to the options.\n  Relation input = 1;\n\n  // (Optional) Format value according to the Spark documentation. Examples are: text, parquet, delta.\n  optional string source = 2;\n\n  // (Optional)\n  //\n  // The destination of the write operation can be either a path or a table.\n  // If the destination is neither a path nor a table, such as jdbc and noop,\n  // the `save_type` should not be set.\n  oneof save_type {\n    string path = 3;\n    SaveTable table = 4;\n  }\n\n  // (Required) the save mode.\n  SaveMode mode = 5;\n\n  // (Optional) List of columns to sort the output by.\n  repeated string sort_column_names = 6;\n\n  // (Optional) List of columns for partitioning.\n  repeated string partitioning_columns = 7;\n\n  // (Optional) Bucketing specification. Bucketing must set the number of buckets and the columns\n  // to bucket by.\n  BucketBy bucket_by = 8;\n\n  // (Optional) A list of configuration options.\n  map<string, string> options = 9;\n\n  // (Optional) Columns used for clustering the table.\n  repeated string clustering_columns = 10;\n\n  message SaveTable {\n    // (Required) The table name.\n    string table_name = 1;\n    // (Required) The method to be called to write to the table.\n    TableSaveMethod save_method = 2;\n\n    enum TableSaveMethod {\n      TABLE_SAVE_METHOD_UNSPECIFIED = 0;\n      TABLE_SAVE_METHOD_SAVE_AS_TABLE = 1;\n      TABLE_SAVE_METHOD_INSERT_INTO = 2;\n    }\n  }\n\n  message BucketBy {\n    repeated string bucket_column_names = 1;\n    int32 num_buckets = 2;\n  }\n\n  enum SaveMode {\n    SAVE_MODE_UNSPECIFIED = 0;\n    SAVE_MODE_APPEND = 1;\n    SAVE_MODE_OVERWRITE = 2;\n    SAVE_MODE_ERROR_IF_EXISTS = 3;\n    SAVE_MODE_IGNORE = 4;\n  }\n}\n\n// As writes are not directly handled during analysis and planning, they are modeled as commands.\nmessage WriteOperationV2 {\n  // (Required) The output of the `input` relation will be persisted according to the options.\n  Relation input = 1;\n\n  // (Required) The destination of the write operation must be either a path or a table.\n  string table_name = 2;\n\n  // (Optional) A provider for the underlying output data source. Spark's default catalog supports\n  // \"parquet\", \"json\", etc.\n  optional string provider = 3;\n\n  // (Optional) List of columns for partitioning for output table created by `create`,\n  // `createOrReplace`, or `replace`\n  repeated Expression partitioning_columns = 4;\n\n  // (Optional) A list of configuration options.\n  map<string, string> options = 5;\n\n  // (Optional) A list of table properties.\n  map<string, string> table_properties = 6;\n\n  // (Required) Write mode.\n  Mode mode = 7;\n\n  enum Mode {\n    MODE_UNSPECIFIED = 0;\n    MODE_CREATE = 1;\n    MODE_OVERWRITE = 2;\n    MODE_OVERWRITE_PARTITIONS = 3;\n    MODE_APPEND = 4;\n    MODE_REPLACE = 5;\n    MODE_CREATE_OR_REPLACE = 6;\n  }\n\n  // (Optional) A condition for overwrite saving mode\n  Expression overwrite_condition = 8;\n\n  // (Optional) Columns used for clustering the table.\n  repeated string clustering_columns = 9;\n}\n\n// Starts write stream operation as streaming query. Query ID and Run ID of the streaming\n// query are returned.\nmessage WriteStreamOperationStart {\n\n  // (Required) The output of the `input` streaming relation will be written.\n  Relation input = 1;\n\n  // The following fields directly map to API for DataStreamWriter().\n  // Consult API documentation unless explicitly documented here.\n\n  string format = 2;\n  map<string, string> options = 3;\n  repeated string partitioning_column_names = 4;\n\n  oneof trigger {\n    string processing_time_interval = 5;\n    bool available_now = 6;\n    bool once = 7;\n    string continuous_checkpoint_interval = 8;\n  }\n\n  string output_mode = 9;\n  string query_name = 10;\n\n  // The destination is optional. When set, it can be a path or a table name.\n  oneof sink_destination {\n    string path = 11;\n    string table_name = 12;\n  }\n\n  StreamingForeachFunction foreach_writer = 13;\n  StreamingForeachFunction foreach_batch = 14;\n\n  // (Optional) Columns used for clustering the table.\n  repeated string clustering_column_names = 15;\n}\n\nmessage StreamingForeachFunction {\n  oneof function {\n    PythonUDF python_function = 1;\n    ScalarScalaUDF scala_function = 2;\n  }\n}\n\nmessage WriteStreamOperationStartResult {\n\n  // (Required) Query instance. See `StreamingQueryInstanceId`.\n  StreamingQueryInstanceId query_id = 1;\n\n  // An optional query name.\n  string name = 2;\n\n  // Optional query started event if there is any listener registered on the client side.\n  optional string query_started_event_json = 3;\n\n  // TODO: How do we indicate errors?\n  // TODO: Consider adding status, last progress etc here.\n}\n\n// A tuple that uniquely identifies an instance of streaming query run. It consists of `id` that\n// persists across the streaming runs and `run_id` that changes between each run of the\n// streaming query that resumes from the checkpoint.\nmessage StreamingQueryInstanceId {\n\n  // (Required) The unique id of this query that persists across restarts from checkpoint data.\n  // That is, this id is generated when a query is started for the first time, and\n  // will be the same every time it is restarted from checkpoint data.\n  string id = 1;\n\n  // (Required) The unique id of this run of the query. That is, every start/restart of a query\n  // will generate a unique run_id. Therefore, every time a query is restarted from\n  // checkpoint, it will have the same `id` but different `run_id`s.\n  string run_id = 2;\n}\n\n// Commands for a streaming query.\nmessage StreamingQueryCommand {\n\n  // (Required) Query instance. See `StreamingQueryInstanceId`.\n  StreamingQueryInstanceId query_id = 1;\n\n  // See documentation for the corresponding API method in StreamingQuery.\n  oneof command {\n    // status() API.\n    bool status = 2;\n    // lastProgress() API.\n    bool last_progress = 3;\n    // recentProgress() API.\n    bool recent_progress = 4;\n    // stop() API. Stops the query.\n    bool stop = 5;\n    // processAllAvailable() API. Waits till all the available data is processed\n    bool process_all_available = 6;\n    // explain() API. Returns logical and physical plans.\n    ExplainCommand explain = 7;\n    // exception() API. Returns the exception in the query if any.\n    bool exception = 8;\n    // awaitTermination() API. Waits for the termination of the query.\n    AwaitTerminationCommand await_termination = 9;\n  }\n\n  message ExplainCommand {\n    // TODO: Consider reusing Explain from AnalyzePlanRequest message.\n    //       We can not do this right now since it base.proto imports this file.\n    bool extended = 1;\n  }\n\n  message AwaitTerminationCommand {\n    optional int64 timeout_ms = 2;\n  }\n}\n\n// Response for commands on a streaming query.\nmessage StreamingQueryCommandResult {\n  // (Required) Query instance id. See `StreamingQueryInstanceId`.\n  StreamingQueryInstanceId query_id = 1;\n\n  oneof result_type {\n    StatusResult status = 2;\n    RecentProgressResult recent_progress = 3;\n    ExplainResult explain = 4;\n    ExceptionResult exception = 5;\n    AwaitTerminationResult await_termination = 6;\n  }\n\n  message StatusResult {\n    // See documentation for these Scala 'StreamingQueryStatus' struct\n    string status_message = 1;\n    bool is_data_available = 2;\n    bool is_trigger_active = 3;\n    bool is_active = 4;\n  }\n\n  message RecentProgressResult {\n    // Progress reports as an array of json strings.\n    repeated string recent_progress_json = 5;\n  }\n\n  message ExplainResult {\n    // Logical and physical plans as string\n    string result = 1;\n  }\n\n  message ExceptionResult {\n    // (Optional) Exception message as string, maps to the return value of original\n    // StreamingQueryException's toString method\n    optional string exception_message = 1;\n    // (Optional) Exception error class as string\n    optional string error_class = 2;\n    // (Optional) Exception stack trace as string\n    optional string stack_trace = 3;\n  }\n\n  message AwaitTerminationResult {\n    bool terminated = 1;\n  }\n}\n\n// Commands for the streaming query manager.\nmessage StreamingQueryManagerCommand {\n\n  // See documentation for the corresponding API method in StreamingQueryManager.\n  oneof command {\n    // active() API, returns a list of active queries.\n    bool active = 1;\n    // get() API, returns the StreamingQuery identified by id.\n    string get_query = 2;\n    // awaitAnyTermination() API, wait until any query terminates or timeout.\n    AwaitAnyTerminationCommand await_any_termination = 3;\n    // resetTerminated() API.\n    bool reset_terminated = 4;\n    // addListener API.\n    StreamingQueryListenerCommand add_listener = 5;\n    // removeListener API.\n    StreamingQueryListenerCommand remove_listener = 6;\n    // listListeners() API, returns a list of streaming query listeners.\n    bool list_listeners = 7;\n  }\n\n  message AwaitAnyTerminationCommand {\n    // (Optional) The waiting time in milliseconds to wait for any query to terminate.\n    optional int64 timeout_ms = 1;\n  }\n\n  message StreamingQueryListenerCommand {\n    bytes listener_payload = 1;\n    optional PythonUDF python_listener_payload = 2;\n    string id = 3;\n  }\n}\n\n// Response for commands on the streaming query manager.\nmessage StreamingQueryManagerCommandResult {\n  oneof result_type {\n    ActiveResult active = 1;\n    StreamingQueryInstance query = 2;\n    AwaitAnyTerminationResult await_any_termination = 3;\n    bool reset_terminated = 4;\n    bool add_listener = 5;\n    bool remove_listener = 6;\n    ListStreamingQueryListenerResult list_listeners = 7;\n  }\n\n  message ActiveResult {\n    repeated StreamingQueryInstance active_queries = 1;\n  }\n\n  message StreamingQueryInstance {\n    // (Required) The id and runId of this query.\n    StreamingQueryInstanceId id = 1;\n    // (Optional) The name of this query.\n    optional string name = 2;\n  }\n\n  message AwaitAnyTerminationResult {\n    bool terminated = 1;\n  }\n\n  message StreamingQueryListenerInstance {\n    bytes listener_payload = 1;\n  }\n\n  message ListStreamingQueryListenerResult {\n    // (Required) Reference IDs of listener instances.\n    repeated string listener_ids = 1;\n  }\n}\n\n// The protocol for client-side StreamingQueryListener.\n// This command will only be set when either the first listener is added to the client, or the last\n// listener is removed from the client.\n// The add_listener_bus_listener command will only be set true in the first case.\n// The remove_listener_bus_listener command will only be set true in the second case.\nmessage StreamingQueryListenerBusCommand {\n  oneof command {\n    bool add_listener_bus_listener = 1;\n    bool remove_listener_bus_listener = 2;\n  }\n}\n\n// The enum used for client side streaming query listener event\n// There is no QueryStartedEvent defined here,\n// it is added as a field in WriteStreamOperationStartResult\nenum StreamingQueryEventType {\n  QUERY_PROGRESS_UNSPECIFIED = 0;\n  QUERY_PROGRESS_EVENT = 1;\n  QUERY_TERMINATED_EVENT = 2;\n  QUERY_IDLE_EVENT = 3;\n}\n\n// The protocol for the returned events in the long-running response channel.\nmessage StreamingQueryListenerEvent {\n  // (Required) The json serialized event, all StreamingQueryListener events have a json method\n  string event_json = 1;\n  // (Required) Query event type used by client to decide how to deserialize the event_json\n  StreamingQueryEventType event_type = 2;\n}\n\nmessage StreamingQueryListenerEventsResult {\n  repeated StreamingQueryListenerEvent events = 1;\n  optional bool listener_bus_listener_added = 2;\n}\n\n// Command to get the output of 'SparkContext.resources'\nmessage GetResourcesCommand { }\n\n// Response for command 'GetResourcesCommand'.\nmessage GetResourcesCommandResult {\n  map<string, ResourceInformation> resources = 1;\n}\n\n// Command to create ResourceProfile\nmessage CreateResourceProfileCommand {\n  // (Required) The ResourceProfile to be built on the server-side.\n  ResourceProfile profile = 1;\n}\n\n// Response for command 'CreateResourceProfileCommand'.\nmessage CreateResourceProfileCommandResult {\n  // (Required) Server-side generated resource profile id.\n  int32 profile_id = 1;\n}\n\n// Command to remove `CashedRemoteRelation`\nmessage RemoveCachedRemoteRelationCommand {\n  // (Required) The remote to be related\n  CachedRemoteRelation relation = 1;\n}\n\nmessage CheckpointCommand {\n  // (Required) The logical plan to checkpoint.\n  Relation relation = 1;\n\n  // (Required) Locally checkpoint using a local temporary\n  // directory in Spark Connect server (Spark Driver)\n  bool local = 2;\n\n  // (Required) Whether to checkpoint this dataframe immediately.\n  bool eager = 3;\n\n  // (Optional) For local checkpoint, the storage level to use.\n  optional StorageLevel storage_level = 4;\n}\n\nmessage MergeIntoTableCommand {\n  // (Required) The name of the target table.\n  string target_table_name = 1;\n\n  // (Required) The relation of the source table.\n  Relation source_table_plan = 2;\n\n  // (Required) The condition to match the source and target.\n  Expression merge_condition = 3;\n\n  // (Optional) The actions to be taken when the condition is matched.\n  repeated Expression match_actions = 4;\n\n  // (Optional) The actions to be taken when the condition is not matched.\n  repeated Expression not_matched_actions = 5;\n\n  // (Optional) The actions to be taken when the condition is not matched by source.\n  repeated Expression not_matched_by_source_actions = 6;\n\n  // (Required) Whether to enable schema evolution.\n  bool with_schema_evolution = 7;\n}\n\n// Execute an arbitrary string command inside an external execution engine\nmessage ExecuteExternalCommand {\n  // (Required) The class name of the runner that implements `ExternalCommandRunner`\n  string runner = 1;\n\n  // (Required) The target command to be executed.\n  string command = 2;\n\n  // (Optional) The options for the runner.\n  map<string, string> options = 3;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/common.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// StorageLevel for persisting Datasets/Tables.\nmessage StorageLevel {\n  // (Required) Whether the cache should use disk or not.\n  bool use_disk = 1;\n  // (Required) Whether the cache should use memory or not.\n  bool use_memory = 2;\n  // (Required) Whether the cache should use off-heap or not.\n  bool use_off_heap = 3;\n  // (Required) Whether the cached data is deserialized or not.\n  bool deserialized = 4;\n  // (Required) The number of replicas.\n  int32 replication = 5;\n}\n\n\n// ResourceInformation to hold information about a type of Resource.\n// The corresponding class is 'org.apache.spark.resource.ResourceInformation'\nmessage ResourceInformation {\n  // (Required) The name of the resource\n  string name = 1;\n  // (Required) An array of strings describing the addresses of the resource.\n  repeated string addresses = 2;\n}\n\n// An executor resource request.\nmessage ExecutorResourceRequest {\n  // (Required) resource name.\n  string resource_name = 1;\n\n  // (Required) resource amount requesting.\n  int64 amount = 2;\n\n  // Optional script used to discover the resources.\n  optional string discovery_script = 3;\n\n  // Optional vendor, required for some cluster managers.\n  optional string vendor = 4;\n}\n\n// A task resource request.\nmessage TaskResourceRequest {\n  // (Required) resource name.\n  string resource_name = 1;\n\n  // (Required) resource amount requesting as a double to support fractional\n  // resource requests.\n  double amount = 2;\n}\n\nmessage ResourceProfile {\n  // (Optional) Resource requests for executors. Mapped from the resource name\n  // (e.g., cores, memory, CPU) to its specific request.\n  map<string, ExecutorResourceRequest> executor_resources = 1;\n\n  // (Optional) Resource requests for tasks. Mapped from the resource name\n  // (e.g., cores, memory, CPU) to its specific request.\n  map<string, TaskResourceRequest> task_resources = 2;\n}\n\nmessage Origin {\n  // (Required) Indicate the origin type.\n  oneof function {\n    PythonOrigin python_origin = 1;\n    JvmOrigin jvm_origin = 2;\n  }\n}\n\nmessage PythonOrigin {\n  // (Required) Name of the origin, for example, the name of the function\n  string fragment = 1;\n\n  // (Required) Callsite to show to end users, for example, stacktrace.\n  string call_site = 2;\n}\n\nmessage JvmOrigin {\n  // (Optional) Line number in the source file.\n  optional int32 line = 1;\n\n  // (Optional) Start position in the source file.\n  optional int32 start_position = 2;\n\n  // (Optional) Start index in the source file.\n  optional int32 start_index = 3;\n\n  // (Optional) Stop index in the source file.\n  optional int32 stop_index = 4;\n\n  // (Optional) SQL text.\n  optional string sql_text = 5;\n\n  // (Optional) Object type.\n  optional string object_type = 6;\n\n  // (Optional) Object name.\n  optional string object_name = 7;\n\n  // (Optional) Stack trace.\n  repeated StackTraceElement stack_trace = 8;\n}\n\n// A message to hold a [[java.lang.StackTraceElement]].\nmessage StackTraceElement {\n  // (Optional) Class loader name\n  optional string class_loader_name = 1;\n\n  // (Optional) Module name\n  optional string module_name = 2;\n\n  // (Optional) Module version\n  optional string module_version = 3;\n\n  // (Required) Declaring class\n  string declaring_class = 4;\n\n  // (Required) Method name\n  string method_name = 5;\n\n  // (Optional) File name\n  optional string file_name = 6;\n\n  // (Required) Line number\n  int32 line_number = 7;\n}\n\nmessage Bools {\n  repeated bool values = 1;\n}\n\nmessage Ints {\n  repeated int32 values = 1;\n}\n\nmessage Longs {\n  repeated int64 values = 1;\n}\n\nmessage Floats {\n  repeated float values = 1;\n}\n\nmessage Doubles {\n  repeated double values = 1;\n}\n\nmessage Strings {\n  repeated string values = 1;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/example_plugins.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\nimport \"spark/connect/relations.proto\";\nimport \"spark/connect/expressions.proto\";\noption go_package = \"internal/generated\";\n\npackage spark.connect;\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\n\nmessage ExamplePluginRelation {\n  Relation input = 1;\n  string custom_field = 2;\n\n}\n\nmessage ExamplePluginExpression {\n  Expression child = 1;\n  string custom_field = 2;\n}\n\nmessage ExamplePluginCommand {\n  string custom_field = 1;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/expressions.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\nimport \"google/protobuf/any.proto\";\nimport \"spark/connect/types.proto\";\nimport \"spark/connect/common.proto\";\n\npackage spark.connect;\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// Expression used to refer to fields, functions and similar. This can be used everywhere\n// expressions in SQL appear.\nmessage Expression {\n\n  ExpressionCommon common = 18;\n  oneof expr_type {\n    Literal literal = 1;\n    UnresolvedAttribute unresolved_attribute = 2;\n    UnresolvedFunction unresolved_function = 3;\n    ExpressionString expression_string = 4;\n    UnresolvedStar unresolved_star = 5;\n    Alias alias = 6;\n    Cast cast = 7;\n    UnresolvedRegex unresolved_regex = 8;\n    SortOrder sort_order = 9;\n    LambdaFunction lambda_function = 10;\n    Window window = 11;\n    UnresolvedExtractValue unresolved_extract_value = 12;\n    UpdateFields update_fields = 13;\n    UnresolvedNamedLambdaVariable unresolved_named_lambda_variable = 14;\n    CommonInlineUserDefinedFunction common_inline_user_defined_function = 15;\n    CallFunction call_function = 16;\n    NamedArgumentExpression named_argument_expression = 17;\n    MergeAction merge_action = 19;\n    TypedAggregateExpression typed_aggregate_expression = 20;\n    SubqueryExpression subquery_expression = 21;\n\n    // This field is used to mark extensions to the protocol. When plugins generate arbitrary\n    // relations they can add them here. During the planning the correct resolution is done.\n    google.protobuf.Any extension = 999;\n  }\n\n\n  // Expression for the OVER clause or WINDOW clause.\n  message Window {\n\n    // (Required) The window function.\n    Expression window_function = 1;\n\n    // (Optional) The way that input rows are partitioned.\n    repeated Expression partition_spec = 2;\n\n    // (Optional) Ordering of rows in a partition.\n    repeated SortOrder order_spec = 3;\n\n    // (Optional) Window frame in a partition.\n    //\n    // If not set, it will be treated as 'UnspecifiedFrame'.\n    WindowFrame frame_spec = 4;\n\n    // The window frame\n    message WindowFrame {\n\n      // (Required) The type of the frame.\n      FrameType frame_type = 1;\n\n      // (Required) The lower bound of the frame.\n      FrameBoundary lower = 2;\n\n      // (Required) The upper bound of the frame.\n      FrameBoundary upper = 3;\n\n      enum FrameType {\n        FRAME_TYPE_UNDEFINED = 0;\n\n        // RowFrame treats rows in a partition individually.\n        FRAME_TYPE_ROW = 1;\n\n        // RangeFrame treats rows in a partition as groups of peers.\n        // All rows having the same 'ORDER BY' ordering are considered as peers.\n        FRAME_TYPE_RANGE = 2;\n      }\n\n      message FrameBoundary {\n        oneof boundary {\n          // CURRENT ROW boundary\n          bool current_row = 1;\n\n          // UNBOUNDED boundary.\n          // For lower bound, it will be converted to 'UnboundedPreceding'.\n          // for upper bound, it will be converted to 'UnboundedFollowing'.\n          bool unbounded = 2;\n\n          // This is an expression for future proofing. We are expecting literals on the server side.\n          Expression value = 3;\n        }\n      }\n    }\n  }\n\n  // SortOrder is used to specify the  data ordering, it is normally used in Sort and Window.\n  // It is an unevaluable expression and cannot be evaluated, so can not be used in Projection.\n  message SortOrder {\n    // (Required) The expression to be sorted.\n    Expression child = 1;\n\n    // (Required) The sort direction, should be ASCENDING or DESCENDING.\n    SortDirection direction = 2;\n\n    // (Required) How to deal with NULLs, should be NULLS_FIRST or NULLS_LAST.\n    NullOrdering null_ordering = 3;\n\n    enum SortDirection {\n      SORT_DIRECTION_UNSPECIFIED = 0;\n      SORT_DIRECTION_ASCENDING = 1;\n      SORT_DIRECTION_DESCENDING = 2;\n    }\n\n    enum NullOrdering {\n      SORT_NULLS_UNSPECIFIED = 0;\n      SORT_NULLS_FIRST = 1;\n      SORT_NULLS_LAST = 2;\n    }\n  }\n\n  message Cast {\n    // (Required) the expression to be casted.\n    Expression expr = 1;\n\n    // (Required) the data type that the expr to be casted to.\n    oneof cast_to_type {\n      DataType type = 2;\n      // If this is set, Server will use Catalyst parser to parse this string to DataType.\n      string type_str = 3;\n    }\n\n    // (Optional) The expression evaluation mode.\n    EvalMode eval_mode = 4;\n\n    enum EvalMode {\n      EVAL_MODE_UNSPECIFIED = 0;\n      EVAL_MODE_LEGACY = 1;\n      EVAL_MODE_ANSI = 2;\n      EVAL_MODE_TRY = 3;\n    }\n  }\n\n  message Literal {\n    oneof literal_type {\n      DataType null = 1;\n      bytes binary = 2;\n      bool boolean = 3;\n\n      int32 byte = 4;\n      int32 short = 5;\n      int32 integer = 6;\n      int64 long = 7;\n      float float = 10;\n      double double = 11;\n      Decimal decimal = 12;\n\n      string string = 13;\n\n      // Date in units of days since the UNIX epoch.\n      int32 date = 16;\n      // Timestamp in units of microseconds since the UNIX epoch.\n      int64 timestamp = 17;\n      // Timestamp in units of microseconds since the UNIX epoch (without timezone information).\n      int64 timestamp_ntz = 18;\n\n      CalendarInterval calendar_interval = 19;\n      int32 year_month_interval = 20;\n      int64 day_time_interval = 21;\n      Array array = 22;\n      Map map = 23;\n      Struct struct = 24;\n\n      SpecializedArray specialized_array = 25;\n    }\n\n    message Decimal {\n      // the string representation.\n      string value = 1;\n      // The maximum number of digits allowed in the value.\n      // the maximum precision is 38.\n      optional int32 precision = 2;\n      // declared scale of decimal literal\n      optional int32 scale = 3;\n    }\n\n    message CalendarInterval {\n      int32 months = 1;\n      int32 days = 2;\n      int64 microseconds = 3;\n    }\n\n    message Array {\n      DataType element_type = 1;\n      repeated Literal elements = 2;\n    }\n\n    message Map {\n      DataType key_type = 1;\n      DataType value_type = 2;\n      repeated Literal keys = 3;\n      repeated Literal values = 4;\n    }\n\n    message Struct {\n      DataType struct_type = 1;\n      repeated Literal elements = 2;\n    }\n\n    message SpecializedArray {\n      oneof value_type {\n        Bools bools = 1;\n        Ints ints = 2;\n        Longs longs = 3;\n        Floats floats = 4;\n        Doubles doubles = 5;\n        Strings strings = 6;\n      }\n    }\n  }\n\n  // An unresolved attribute that is not explicitly bound to a specific column, but the column\n  // is resolved during analysis by name.\n  message UnresolvedAttribute {\n    // (Required) An identifier that will be parsed by Catalyst parser. This should follow the\n    // Spark SQL identifier syntax.\n    string unparsed_identifier = 1;\n\n    // (Optional) The id of corresponding connect plan.\n    optional int64 plan_id = 2;\n\n    // (Optional) The requested column is a metadata column.\n    optional bool is_metadata_column = 3;\n  }\n\n  // An unresolved function is not explicitly bound to one explicit function, but the function\n  // is resolved during analysis following Sparks name resolution rules.\n  message UnresolvedFunction {\n    // (Required) name (or unparsed name for user defined function) for the unresolved function.\n    string function_name = 1;\n\n    // (Optional) Function arguments. Empty arguments are allowed.\n    repeated Expression arguments = 2;\n\n    // (Required) Indicate if this function should be applied on distinct values.\n    bool is_distinct = 3;\n\n    // (Required) Indicate if this is a user defined function.\n    //\n    // When it is not a user defined function, Connect will use the function name directly.\n    // When it is a user defined function, Connect will parse the function name first.\n    bool is_user_defined_function = 4;\n\n    // (Optional) Indicate if this function is defined in the internal function registry.\n    // If not set, the server will try to look up the function in the internal function registry\n    // and decide appropriately.\n    optional bool is_internal = 5;\n  }\n\n  // Expression as string.\n  message ExpressionString {\n    // (Required) A SQL expression that will be parsed by Catalyst parser.\n    string expression = 1;\n  }\n\n  // UnresolvedStar is used to expand all the fields of a relation or struct.\n  message UnresolvedStar {\n\n    // (Optional) The target of the expansion.\n    //\n    // If set, it should end with '.*' and will be parsed by 'parseAttributeName'\n    // in the server side.\n    optional string unparsed_target = 1;\n\n    // (Optional) The id of corresponding connect plan.\n    optional int64 plan_id = 2;\n  }\n\n  // Represents all of the input attributes to a given relational operator, for example in\n  // \"SELECT `(id)?+.+` FROM ...\".\n  message UnresolvedRegex {\n    // (Required) The column name used to extract column with regex.\n    string col_name = 1;\n\n    // (Optional) The id of corresponding connect plan.\n    optional int64 plan_id = 2;\n  }\n\n  // Extracts a value or values from an Expression\n  message UnresolvedExtractValue {\n    // (Required) The expression to extract value from, can be\n    // Map, Array, Struct or array of Structs.\n    Expression child = 1;\n\n    // (Required) The expression to describe the extraction, can be\n    // key of Map, index of Array, field name of Struct.\n    Expression extraction = 2;\n  }\n\n  // Add, replace or drop a field of `StructType` expression by name.\n  message UpdateFields {\n    // (Required) The struct expression.\n    Expression struct_expression = 1;\n\n    // (Required) The field name.\n    string field_name = 2;\n\n    // (Optional) The expression to add or replace.\n    //\n    // When not set, it means this field will be dropped.\n    Expression value_expression = 3;\n  }\n\n  message Alias {\n    // (Required) The expression that alias will be added on.\n    Expression expr = 1;\n\n    // (Required) a list of name parts for the alias.\n    //\n    // Scalar columns only has one name that presents.\n    repeated string name = 2;\n\n    // (Optional) Alias metadata expressed as a JSON map.\n    optional string metadata = 3;\n  }\n\n  message LambdaFunction {\n    // (Required) The lambda function.\n    //\n    // The function body should use 'UnresolvedAttribute' as arguments, the sever side will\n    // replace 'UnresolvedAttribute' with 'UnresolvedNamedLambdaVariable'.\n    Expression function = 1;\n\n    // (Required) Function variables. Must contains 1 ~ 3 variables.\n    repeated Expression.UnresolvedNamedLambdaVariable arguments = 2;\n  }\n\n  message UnresolvedNamedLambdaVariable {\n\n    // (Required) a list of name parts for the variable. Must not be empty.\n    repeated string name_parts = 1;\n  }\n}\n\nmessage ExpressionCommon {\n  // (Required) Keep the information of the origin for this expression such as stacktrace.\n  Origin origin = 1;\n}\n\nmessage CommonInlineUserDefinedFunction {\n  // (Required) Name of the user-defined function.\n  string function_name = 1;\n  // (Optional) Indicate if the user-defined function is deterministic.\n  bool deterministic = 2;\n  // (Optional) Function arguments. Empty arguments are allowed.\n  repeated Expression arguments = 3;\n  // (Required) Indicate the function type of the user-defined function.\n  oneof function {\n    PythonUDF python_udf = 4;\n    ScalarScalaUDF scalar_scala_udf = 5;\n    JavaUDF java_udf = 6;\n  }\n  // (Required) Indicate if this function should be applied on distinct values.\n  bool is_distinct = 7;\n}\n\nmessage PythonUDF {\n  // (Required) Output type of the Python UDF\n  DataType output_type = 1;\n  // (Required) EvalType of the Python UDF\n  int32 eval_type = 2;\n  // (Required) The encoded commands of the Python UDF\n  bytes command = 3;\n  // (Required) Python version being used in the client.\n  string python_ver = 4;\n  // (Optional) Additional includes for the Python UDF.\n  repeated string additional_includes = 5;\n}\n\nmessage ScalarScalaUDF {\n  // (Required) Serialized JVM object containing UDF definition, input encoders and output encoder\n  bytes payload = 1;\n  // (Optional) Input type(s) of the UDF\n  repeated DataType inputTypes = 2;\n  // (Required) Output type of the UDF\n  DataType outputType = 3;\n  // (Required) True if the UDF can return null value\n  bool nullable = 4;\n  // (Required) Indicate if the UDF is an aggregate function\n  bool aggregate = 5;\n}\n\nmessage JavaUDF {\n  // (Required) Fully qualified name of Java class\n  string class_name = 1;\n\n  // (Optional) Output type of the Java UDF\n  optional DataType output_type = 2;\n\n  // (Required) Indicate if the Java user-defined function is an aggregate function\n  bool aggregate = 3;\n}\n\nmessage TypedAggregateExpression {\n  // (Required) The aggregate function object packed into bytes.\n  ScalarScalaUDF scalar_scala_udf = 1;\n}\n\nmessage CallFunction {\n  // (Required) Unparsed name of the SQL function.\n  string function_name = 1;\n\n  // (Optional) Function arguments. Empty arguments are allowed.\n  repeated Expression arguments = 2;\n}\n\nmessage NamedArgumentExpression {\n  // (Required) The key of the named argument.\n  string key = 1;\n\n  // (Required) The value expression of the named argument.\n  Expression value = 2;\n}\n\nmessage MergeAction {\n  // (Required) The action type of the merge action.\n  ActionType action_type = 1;\n\n  // (Optional) The condition expression of the merge action.\n  optional Expression condition = 2;\n\n  // (Optional) The assignments of the merge action. Required for ActionTypes INSERT and UPDATE.\n  repeated Assignment assignments = 3;\n\n  enum ActionType {\n    ACTION_TYPE_INVALID = 0;\n    ACTION_TYPE_DELETE = 1;\n    ACTION_TYPE_INSERT = 2;\n    ACTION_TYPE_INSERT_STAR = 3;\n    ACTION_TYPE_UPDATE = 4;\n    ACTION_TYPE_UPDATE_STAR = 5;\n  }\n\n  message Assignment {\n    // (Required) The key of the assignment.\n    Expression key = 1;\n\n    // (Required) The value of the assignment.\n    Expression value = 2;\n  }\n}\n\nmessage SubqueryExpression {\n  // (Required) The ID of the corresponding connect plan.\n  int64 plan_id = 1;\n\n  // (Required) The type of the subquery.\n  SubqueryType subquery_type = 2;\n\n  // (Optional) Options specific to table arguments.\n  optional TableArgOptions table_arg_options = 3;\n\n  // (Optional) IN subquery values.\n  repeated Expression in_subquery_values = 4;\n\n  enum SubqueryType {\n    SUBQUERY_TYPE_UNKNOWN = 0;\n    SUBQUERY_TYPE_SCALAR = 1;\n    SUBQUERY_TYPE_EXISTS = 2;\n    SUBQUERY_TYPE_TABLE_ARG = 3;\n    SUBQUERY_TYPE_IN = 4;\n  }\n\n  // Nested message for table argument options.\n  message TableArgOptions {\n    // (Optional) The way that input rows are partitioned.\n    repeated Expression partition_spec = 1;\n\n    // (Optional) Ordering of rows in a partition.\n    repeated Expression.SortOrder order_spec = 2;\n\n    // (Optional) Whether this is a single partition.\n    optional bool with_single_partition = 3;\n  }\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/ml.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\nimport \"spark/connect/relations.proto\";\nimport \"spark/connect/expressions.proto\";\nimport \"spark/connect/ml_common.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// Command for ML\nmessage MlCommand {\n  oneof command {\n    Fit fit = 1;\n    Fetch fetch = 2;\n    Delete delete = 3;\n    Write write = 4;\n    Read read = 5;\n    Evaluate evaluate = 6;\n    CleanCache clean_cache = 7;\n    GetCacheInfo get_cache_info = 8;\n  }\n\n  // Command for estimator.fit(dataset)\n  message Fit {\n    // (Required) Estimator information (its type should be OPERATOR_TYPE_ESTIMATOR)\n    MlOperator estimator = 1;\n    // (Optional) parameters of the Estimator\n    optional MlParams params = 2;\n    // (Required) the training dataset\n    Relation dataset = 3;\n  }\n\n  // Command to delete the cached objects which could be a model\n  // or summary evaluated by a model\n  message Delete {\n    repeated ObjectRef obj_refs = 1;\n  }\n\n  // Force to clean up all the ML cached objects\n  message CleanCache { }\n\n  // Get the information of all the ML cached objects\n  message GetCacheInfo { }\n\n  // Command to write ML operator\n  message Write {\n    // It could be an estimator/evaluator or the cached model\n    oneof type {\n      // Estimator or evaluator\n      MlOperator operator = 1;\n      // The cached model\n      ObjectRef obj_ref = 2;\n    }\n    // (Optional) The parameters of operator which could be estimator/evaluator or a cached model\n    optional MlParams params = 3;\n    // (Required) Save the ML instance to the path\n    string path = 4;\n    // (Optional) Overwrites if the output path already exists.\n    optional bool should_overwrite = 5;\n    // (Optional) The options of the writer\n    map<string, string> options = 6;\n  }\n\n  // Command to load ML operator.\n  message Read {\n    // (Required) ML operator information\n    MlOperator operator = 1;\n    // (Required) Load the ML instance from the input path\n    string path = 2;\n  }\n\n  // Command for evaluator.evaluate(dataset)\n  message Evaluate {\n    // (Required) Evaluator information (its type should be OPERATOR_TYPE_EVALUATOR)\n    MlOperator evaluator = 1;\n    // (Optional) parameters of the Evaluator\n    optional MlParams params = 2;\n    // (Required) the evaluating dataset\n    Relation dataset = 3;\n  }\n}\n\n// The result of MlCommand\nmessage MlCommandResult {\n  oneof result_type {\n    // The result of the attribute\n    Expression.Literal param = 1;\n    // Evaluate a Dataset in a model and return the cached ID of summary\n    string summary = 2;\n    // Operator information\n    MlOperatorInfo operator_info = 3;\n  }\n\n  // Represents an operator info\n  message MlOperatorInfo {\n    oneof type {\n      // The cached object which could be a model or summary evaluated by a model\n      ObjectRef obj_ref = 1;\n      // Operator name\n      string name = 2;\n    }\n    // (Optional) the 'uid' of a ML object\n    // Note it is different from the 'id' of a cached object.\n    optional string uid = 3;\n    // (Optional) parameters\n    optional MlParams params = 4;\n    // (Optional) warning message generated during the ML command execution\n    optional string warning_message = 5;\n  }\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/ml_common.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\nimport \"spark/connect/expressions.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// MlParams stores param settings for ML Estimator / Transformer / Evaluator\nmessage MlParams {\n  // User-supplied params\n  map<string, Expression.Literal> params = 1;\n}\n\n// MLOperator represents the ML operators like (Estimator, Transformer or Evaluator)\nmessage MlOperator {\n  // (Required) The qualified name of the ML operator.\n  string name = 1;\n\n  // (Required) Unique id of the ML operator\n  string uid = 2;\n\n  // (Required) Represents what the ML operator is\n  OperatorType type = 3;\n\n  enum OperatorType {\n    OPERATOR_TYPE_UNSPECIFIED = 0;\n    // ML estimator\n    OPERATOR_TYPE_ESTIMATOR = 1;\n    // ML transformer (non-model)\n    OPERATOR_TYPE_TRANSFORMER = 2;\n    // ML evaluator\n    OPERATOR_TYPE_EVALUATOR = 3;\n    // ML model\n    OPERATOR_TYPE_MODEL = 4;\n  }\n}\n\n// Represents a reference to the cached object which could be a model\n// or summary evaluated by a model\nmessage ObjectRef {\n  // (Required) The ID is used to lookup the object on the server side.\n  // Note it is different from the 'uid' of a ML object.\n  string id = 1;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/pipelines.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\nimport \"spark/connect/relations.proto\";\nimport \"spark/connect/types.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\n\n// Dispatch object for pipelines commands. See each individual command for documentation.\nmessage PipelineCommand {\n  oneof command_type {\n    CreateDataflowGraph create_dataflow_graph = 1;\n    DefineDataset define_dataset = 2;\n    DefineFlow define_flow = 3;\n    DropDataflowGraph drop_dataflow_graph = 4;\n    StartRun start_run = 5;\n    DefineSqlGraphElements define_sql_graph_elements = 6;\n  }\n\n  // Request to create a new dataflow graph.\n  message CreateDataflowGraph {\n    // The default catalog.\n    optional string default_catalog = 1;\n\n    // The default database.\n    optional string default_database = 2;\n\n    // SQL configurations for all flows in this graph.\n    map<string, string> sql_conf = 5;\n\n    message Response {\n      // The ID of the created graph.\n      optional string dataflow_graph_id = 1;\n    }\n  }\n\n  // Drops the graph and stops any running attached flows.\n  message DropDataflowGraph {\n    // The graph to drop.\n    optional string dataflow_graph_id = 1;\n  }\n\n  // Request to define a dataset: a table, a materialized view, or a temporary view.\n  message DefineDataset {\n    // The graph to attach this dataset to.\n    optional string dataflow_graph_id = 1;\n\n    // Name of the dataset. Can be partially or fully qualified.\n    optional string dataset_name = 2;\n\n    // The type of the dataset.\n    optional DatasetType dataset_type = 3;\n\n    // Optional comment for the dataset.\n    optional string comment = 4;\n\n    // Optional table properties. Only applies to dataset_type == TABLE and dataset_type == MATERIALIZED_VIEW.\n    map<string, string> table_properties = 5;\n\n    // Optional partition columns for the dataset. Only applies to dataset_type == TABLE and\n    // dataset_type == MATERIALIZED_VIEW.\n    repeated string partition_cols = 6;\n\n    // Schema for the dataset. If unset, this will be inferred from incoming flows.\n    optional spark.connect.DataType schema = 7;\n\n    // The output table format of the dataset. Only applies to dataset_type == TABLE and\n    // dataset_type == MATERIALIZED_VIEW.\n    optional string format = 8;\n  }\n\n  // Request to define a flow targeting a dataset.\n  message DefineFlow {\n    // The graph to attach this flow to.\n    optional string dataflow_graph_id = 1;\n\n    // Name of the flow. For standalone flows, this must be a single-part name.\n    optional string flow_name = 2;\n\n    // Name of the dataset this flow writes to. Can be partially or fully qualified.\n    optional string target_dataset_name = 3;\n\n    // An unresolved relation that defines the dataset's flow.\n    optional spark.connect.Relation plan = 4;\n\n    // SQL configurations set when running this flow.\n    map<string, string> sql_conf = 5;\n\n    // If true, this flow will only be run once per full refresh.\n    optional bool once = 6;\n  }\n\n  // Resolves all datasets and flows and start a pipeline update. Should be called after all\n  // graph elements are registered.\n  message StartRun {\n    // The graph to start.\n    optional string dataflow_graph_id = 1;\n  }\n}\n\n// Parses the SQL file and registers all datasets and flows.\nmessage DefineSqlGraphElements {\n  // The graph to attach this dataset to.\n  optional string dataflow_graph_id = 1;\n\n  // The full path to the SQL file. Can be relative or absolute.\n  optional string sql_file_path = 2;\n\n  // The contents of the SQL file.\n  optional string sql_text = 3;\n}\n\n// Dispatch object for pipelines command results.\nmessage PipelineCommandResult {\n  oneof result_type {\n    CreateDataflowGraphResult create_dataflow_graph_result = 1;\n  }\n  message CreateDataflowGraphResult {\n    // The ID of the created graph.\n    optional string dataflow_graph_id = 1;\n  }\n}\n\n// The type of dataset.\nenum DatasetType {\n  // Safe default value. Should not be used.\n  DATASET_TYPE_UNSPECIFIED = 0;\n  // A materialized view dataset which is published to the catalog\n  MATERIALIZED_VIEW = 1;\n  // A table which is published to the catalog\n  TABLE = 2;\n  // A view which is not published to the catalog\n  TEMPORARY_VIEW = 3;\n}\n\n// A response containing an event emitted during the run of a pipeline.\nmessage PipelineEventResult {\n  PipelineEvent event = 1;\n}\n\n// An event emitted during the run of a graph.\nmessage PipelineEvent {\n  // The time of the event.\n  optional string timestamp = 1;\n  // The message that should be displayed to users.\n  optional string message = 2;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/relations.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\nimport \"google/protobuf/any.proto\";\nimport \"spark/connect/expressions.proto\";\nimport \"spark/connect/types.proto\";\nimport \"spark/connect/catalog.proto\";\nimport \"spark/connect/common.proto\";\nimport \"spark/connect/ml_common.proto\";\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// The main [[Relation]] type. Fundamentally, a relation is a typed container\n// that has exactly one explicit relation type set.\n//\n// When adding new relation types, they have to be registered here.\nmessage Relation {\n  RelationCommon common = 1;\n  oneof rel_type {\n    Read read = 2;\n    Project project = 3;\n    Filter filter = 4;\n    Join join = 5;\n    SetOperation set_op = 6;\n    Sort sort = 7;\n    Limit limit = 8;\n    Aggregate aggregate = 9;\n    SQL sql = 10;\n    LocalRelation local_relation = 11;\n    Sample sample = 12;\n    Offset offset = 13;\n    Deduplicate deduplicate = 14;\n    Range range = 15;\n    SubqueryAlias subquery_alias = 16;\n    Repartition repartition = 17;\n    ToDF to_df = 18;\n    WithColumnsRenamed with_columns_renamed = 19;\n    ShowString show_string = 20;\n    Drop drop = 21;\n    Tail tail = 22;\n    WithColumns with_columns = 23;\n    Hint hint = 24;\n    Unpivot unpivot = 25;\n    ToSchema to_schema = 26;\n    RepartitionByExpression repartition_by_expression = 27;\n    MapPartitions map_partitions = 28;\n    CollectMetrics collect_metrics = 29;\n    Parse parse = 30;\n    GroupMap group_map = 31;\n    CoGroupMap co_group_map = 32;\n    WithWatermark with_watermark = 33;\n    ApplyInPandasWithState apply_in_pandas_with_state = 34;\n    HtmlString html_string = 35;\n    CachedLocalRelation cached_local_relation = 36;\n    CachedRemoteRelation cached_remote_relation = 37;\n    CommonInlineUserDefinedTableFunction common_inline_user_defined_table_function = 38;\n    AsOfJoin as_of_join = 39;\n    CommonInlineUserDefinedDataSource common_inline_user_defined_data_source = 40;\n    WithRelations with_relations = 41;\n    Transpose transpose = 42;\n    UnresolvedTableValuedFunction unresolved_table_valued_function = 43;\n    LateralJoin lateral_join = 44;\n\n    // NA functions\n    NAFill fill_na = 90;\n    NADrop drop_na = 91;\n    NAReplace replace = 92;\n\n    // stat functions\n    StatSummary summary = 100;\n    StatCrosstab crosstab = 101;\n    StatDescribe describe = 102;\n    StatCov cov = 103;\n    StatCorr corr = 104;\n    StatApproxQuantile approx_quantile = 105;\n    StatFreqItems freq_items = 106;\n    StatSampleBy sample_by = 107;\n\n    // Catalog API (experimental / unstable)\n    Catalog catalog = 200;\n\n    // ML relation\n    MlRelation ml_relation = 300;\n\n    // This field is used to mark extensions to the protocol. When plugins generate arbitrary\n    // relations they can add them here. During the planning the correct resolution is done.\n    google.protobuf.Any extension = 998;\n    Unknown unknown = 999;\n  }\n}\n\n// Relation to represent ML world\nmessage MlRelation {\n  oneof ml_type {\n    Transform transform = 1;\n    Fetch fetch = 2;\n  }\n  // Relation to represent transform(input) of the operator\n  // which could be a cached model or a new transformer\n  message Transform {\n    oneof operator {\n      // Object reference\n      ObjectRef obj_ref = 1;\n      // Could be an ML transformer like VectorAssembler\n      MlOperator transformer = 2;\n    }\n    // the input dataframe\n    Relation input = 3;\n    // the operator specific parameters\n    MlParams params = 4;\n  }\n}\n\n// Message for fetching attribute from object on the server side.\n// Fetch can be represented as a Relation or a ML command\n// Command: model.coefficients, model.summary.weightedPrecision which\n// returns the final literal result\n// Relation: model.summary.roc which returns a DataFrame (Relation)\nmessage Fetch {\n  // (Required) reference to the object on the server side\n  ObjectRef obj_ref = 1;\n  // (Required) the calling method chains\n  repeated Method methods = 2;\n\n  // Represents a method with inclusion of method name and its arguments\n  message Method {\n    // (Required) the method name\n    string method = 1;\n    // (Optional) the arguments of the method\n    repeated Args args = 2;\n\n    message Args {\n      oneof args_type {\n        Expression.Literal param = 1;\n        Relation input = 2;\n      }\n    }\n  }\n}\n\n// Used for testing purposes only.\nmessage Unknown {}\n\n// Common metadata of all relations.\nmessage RelationCommon {\n  // (Required) Shared relation metadata.\n  string source_info = 1 [deprecated=true];\n\n  // (Optional) A per-client globally unique id for a given connect plan.\n  optional int64 plan_id = 2;\n\n  // (Optional) Keep the information of the origin for this expression such as stacktrace.\n  Origin origin = 3;\n}\n\n// Relation that uses a SQL query to generate the output.\nmessage SQL {\n  // (Required) The SQL query.\n  string query = 1;\n\n  // (Optional) A map of parameter names to literal expressions.\n  map<string, Expression.Literal> args = 2 [deprecated=true];\n\n  // (Optional) A sequence of literal expressions for positional parameters in the SQL query text.\n  repeated Expression.Literal pos_args = 3 [deprecated=true];\n\n  // (Optional) A map of parameter names to expressions.\n  // It cannot coexist with `pos_arguments`.\n  map<string, Expression> named_arguments = 4;\n\n  // (Optional) A sequence of expressions for positional parameters in the SQL query text.\n  // It cannot coexist with `named_arguments`.\n  repeated Expression pos_arguments = 5;\n}\n\n// Relation of type [[WithRelations]].\n//\n// This relation contains a root plan, and one or more references that are used by the root plan.\n// There are two ways of referencing a relation, by name (through a subquery alias), or by plan_id\n// (using RelationCommon.plan_id).\n//\n// This relation can be used to implement CTEs, describe DAGs, or to reduce tree depth.\nmessage WithRelations {\n  // (Required) Plan at the root of the query tree. This plan is expected to contain one or more\n  // references. Those references get expanded later on by the engine.\n  Relation root = 1;\n\n  // (Required) Plans referenced by the root plan. Relations in this list are also allowed to\n  // contain references to other relations in this list, as long they do not form cycles.\n  repeated Relation references = 2;\n}\n\n// Relation that reads from a file / table or other data source. Does not have additional\n// inputs.\nmessage Read {\n  oneof read_type {\n    NamedTable named_table = 1;\n    DataSource data_source = 2;\n  }\n\n  // (Optional) Indicates if this is a streaming read.\n  bool is_streaming = 3;\n\n  message NamedTable {\n    // (Required) Unparsed identifier for the table.\n    string unparsed_identifier = 1;\n\n    // Options for the named table. The map key is case insensitive.\n    map<string, string> options = 2;\n  }\n\n  message DataSource {\n    // (Optional) Supported formats include: parquet, orc, text, json, parquet, csv, avro.\n    //\n    // If not set, the value from SQL conf 'spark.sql.sources.default' will be used.\n    optional string format = 1;\n\n    // (Optional) If not set, Spark will infer the schema.\n    //\n    // This schema string should be either DDL-formatted or JSON-formatted.\n    optional string schema = 2;\n\n    // Options for the data source. The context of this map varies based on the\n    // data source format. This options could be empty for valid data source format.\n    // The map key is case insensitive.\n    map<string, string> options = 3;\n\n    // (Optional) A list of path for file-system backed data sources.\n    repeated string paths = 4;\n\n    // (Optional) Condition in the where clause for each partition.\n    //\n    // This is only supported by the JDBC data source.\n    repeated string predicates = 5;\n  }\n}\n\n// Projection of a bag of expressions for a given input relation.\n//\n// The input relation must be specified.\n// The projected expression can be an arbitrary expression.\nmessage Project {\n  // (Optional) Input relation is optional for Project.\n  //\n  // For example, `SELECT ABS(-1)` is valid plan without an input plan.\n  Relation input = 1;\n\n  // (Required) A Project requires at least one expression.\n  repeated Expression expressions = 3;\n}\n\n// Relation that applies a boolean expression `condition` on each row of `input` to produce\n// the output result.\nmessage Filter {\n  // (Required) Input relation for a Filter.\n  Relation input = 1;\n\n  // (Required) A Filter must have a condition expression.\n  Expression condition = 2;\n}\n\n// Relation of type [[Join]].\n//\n// `left` and `right` must be present.\nmessage Join {\n  // (Required) Left input relation for a Join.\n  Relation left = 1;\n\n  // (Required) Right input relation for a Join.\n  Relation right = 2;\n\n  // (Optional) The join condition. Could be unset when `using_columns` is utilized.\n  //\n  // This field does not co-exist with using_columns.\n  Expression join_condition = 3;\n\n  // (Required) The join type.\n  JoinType join_type = 4;\n\n  // Optional. using_columns provides a list of columns that should present on both sides of\n  // the join inputs that this Join will join on. For example A JOIN B USING col_name is\n  // equivalent to A JOIN B on A.col_name = B.col_name.\n  //\n  // This field does not co-exist with join_condition.\n  repeated string using_columns = 5;\n\n  enum JoinType {\n    JOIN_TYPE_UNSPECIFIED = 0;\n    JOIN_TYPE_INNER = 1;\n    JOIN_TYPE_FULL_OUTER = 2;\n    JOIN_TYPE_LEFT_OUTER = 3;\n    JOIN_TYPE_RIGHT_OUTER = 4;\n    JOIN_TYPE_LEFT_ANTI = 5;\n    JOIN_TYPE_LEFT_SEMI = 6;\n    JOIN_TYPE_CROSS = 7;\n  }\n\n  // (Optional) Only used by joinWith. Set the left and right join data types.\n  optional JoinDataType join_data_type = 6;\n\n  message JoinDataType {\n    // If the left data type is a struct.\n    bool is_left_struct = 1;\n    // If the right data type is a struct.\n    bool is_right_struct = 2;\n  }\n}\n\n// Relation of type [[SetOperation]]\nmessage SetOperation {\n  // (Required) Left input relation for a Set operation.\n  Relation left_input = 1;\n\n  // (Required) Right input relation for a Set operation.\n  Relation right_input = 2;\n\n  // (Required) The Set operation type.\n  SetOpType set_op_type = 3;\n\n  // (Optional) If to remove duplicate rows.\n  //\n  // True to preserve all results.\n  // False to remove duplicate rows.\n  optional bool is_all = 4;\n\n  // (Optional) If to perform the Set operation based on name resolution.\n  //\n  // Only UNION supports this option.\n  optional bool by_name = 5;\n\n  // (Optional) If to perform the Set operation and allow missing columns.\n  //\n  // Only UNION supports this option.\n  optional bool allow_missing_columns = 6;\n\n  enum SetOpType {\n    SET_OP_TYPE_UNSPECIFIED = 0;\n    SET_OP_TYPE_INTERSECT = 1;\n    SET_OP_TYPE_UNION = 2;\n    SET_OP_TYPE_EXCEPT = 3;\n  }\n}\n\n// Relation of type [[Limit]] that is used to `limit` rows from the input relation.\nmessage Limit {\n  // (Required) Input relation for a Limit.\n  Relation input = 1;\n\n  // (Required) the limit.\n  int32 limit = 2;\n}\n\n// Relation of type [[Offset]] that is used to read rows staring from the `offset` on\n// the input relation.\nmessage Offset {\n  // (Required) Input relation for an Offset.\n  Relation input = 1;\n\n  // (Required) the limit.\n  int32 offset = 2;\n}\n\n// Relation of type [[Tail]] that is used to fetch `limit` rows from the last of the input relation.\nmessage Tail {\n  // (Required) Input relation for an Tail.\n  Relation input = 1;\n\n  // (Required) the limit.\n  int32 limit = 2;\n}\n\n// Relation of type [[Aggregate]].\nmessage Aggregate {\n  // (Required) Input relation for a RelationalGroupedDataset.\n  Relation input = 1;\n\n  // (Required) How the RelationalGroupedDataset was built.\n  GroupType group_type = 2;\n\n  // (Required) Expressions for grouping keys\n  repeated Expression grouping_expressions = 3;\n\n  // (Required) List of values that will be translated to columns in the output DataFrame.\n  repeated Expression aggregate_expressions = 4;\n\n  // (Optional) Pivots a column of the current `DataFrame` and performs the specified aggregation.\n  Pivot pivot = 5;\n\n  // (Optional) List of values that will be translated to columns in the output DataFrame.\n  repeated GroupingSets grouping_sets = 6;\n\n  enum GroupType {\n    GROUP_TYPE_UNSPECIFIED = 0;\n    GROUP_TYPE_GROUPBY = 1;\n    GROUP_TYPE_ROLLUP = 2;\n    GROUP_TYPE_CUBE = 3;\n    GROUP_TYPE_PIVOT = 4;\n    GROUP_TYPE_GROUPING_SETS = 5;\n  }\n\n  message Pivot {\n    // (Required) The column to pivot\n    Expression col = 1;\n\n    // (Optional) List of values that will be translated to columns in the output DataFrame.\n    //\n    // Note that if it is empty, the server side will immediately trigger a job to collect\n    // the distinct values of the column.\n    repeated Expression.Literal values = 2;\n  }\n\n  message GroupingSets {\n    // (Required) Individual grouping set\n    repeated Expression grouping_set = 1;\n  }\n}\n\n// Relation of type [[Sort]].\nmessage Sort {\n  // (Required) Input relation for a Sort.\n  Relation input = 1;\n\n  // (Required) The ordering expressions\n  repeated Expression.SortOrder order = 2;\n\n  // (Optional) if this is a global sort.\n  optional bool is_global = 3;\n}\n\n\n// Drop specified columns.\nmessage Drop {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) columns to drop.\n  repeated Expression columns = 2;\n\n  // (Optional) names of columns to drop.\n  repeated string column_names = 3;\n}\n\n\n// Relation of type [[Deduplicate]] which have duplicate rows removed, could consider either only\n// the subset of columns or all the columns.\nmessage Deduplicate {\n  // (Required) Input relation for a Deduplicate.\n  Relation input = 1;\n\n  // (Optional) Deduplicate based on a list of column names.\n  //\n  // This field does not co-use with `all_columns_as_keys`.\n  repeated string column_names = 2;\n\n  // (Optional) Deduplicate based on all the columns of the input relation.\n  //\n  // This field does not co-use with `column_names`.\n  optional bool all_columns_as_keys = 3;\n\n  // (Optional) Deduplicate within the time range of watermark.\n  optional bool within_watermark = 4;\n}\n\n// A relation that does not need to be qualified by name.\nmessage LocalRelation {\n  // (Optional) Local collection data serialized into Arrow IPC streaming format which contains\n  // the schema of the data.\n  optional bytes data = 1;\n\n  // (Optional) The schema of local data.\n  // It should be either a DDL-formatted type string or a JSON string.\n  //\n  // The server side will update the column names and data types according to this schema.\n  // If the 'data' is not provided, then this schema will be required.\n  optional string schema = 2;\n}\n\n// A local relation that has been cached already.\nmessage CachedLocalRelation {\n  // `userId` and `sessionId` fields are deleted since the server must always use the active\n  // session/user rather than arbitrary values provided by the client. It is never valid to access\n  // a local relation from a different session/user.\n  reserved 1, 2;\n  reserved \"userId\", \"sessionId\";\n\n  // (Required) A sha-256 hash of the serialized local relation in proto, see LocalRelation.\n  string hash = 3;\n}\n\n// Represents a remote relation that has been cached on server.\nmessage CachedRemoteRelation {\n  // (Required) ID of the remote related (assigned by the service).\n  string relation_id = 1;\n}\n\n// Relation of type [[Sample]] that samples a fraction of the dataset.\nmessage Sample {\n  // (Required) Input relation for a Sample.\n  Relation input = 1;\n\n  // (Required) lower bound.\n  double lower_bound = 2;\n\n  // (Required) upper bound.\n  double upper_bound = 3;\n\n  // (Optional) Whether to sample with replacement.\n  optional bool with_replacement = 4;\n\n  // (Required) The random seed.\n  // This field is required to avoid generating mutable dataframes (see SPARK-48184 for details),\n  // however, still keep it 'optional' here for backward compatibility.\n  optional int64 seed = 5;\n\n  // (Required) Explicitly sort the underlying plan to make the ordering deterministic or cache it.\n  // This flag is true when invoking `dataframe.randomSplit` to randomly splits DataFrame with the\n  // provided weights. Otherwise, it is false.\n  bool deterministic_order = 6;\n}\n\n// Relation of type [[Range]] that generates a sequence of integers.\nmessage Range {\n  // (Optional) Default value = 0\n  optional int64 start = 1;\n\n  // (Required)\n  int64 end = 2;\n\n  // (Required)\n  int64 step = 3;\n\n  // Optional. Default value is assigned by 1) SQL conf \"spark.sql.leafNodeDefaultParallelism\" if\n  // it is set, or 2) spark default parallelism.\n  optional int32 num_partitions = 4;\n}\n\n// Relation alias.\nmessage SubqueryAlias {\n  // (Required) The input relation of SubqueryAlias.\n  Relation input = 1;\n\n  // (Required) The alias.\n  string alias = 2;\n\n  // (Optional) Qualifier of the alias.\n  repeated string qualifier = 3;\n}\n\n// Relation repartition.\nmessage Repartition {\n  // (Required) The input relation of Repartition.\n  Relation input = 1;\n\n  // (Required) Must be positive.\n  int32 num_partitions = 2;\n\n  // (Optional) Default value is false.\n  optional bool shuffle = 3;\n}\n\n// Compose the string representing rows for output.\n// It will invoke 'Dataset.showString' to compute the results.\nmessage ShowString {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) Number of rows to show.\n  int32 num_rows = 2;\n\n  // (Required) If set to more than 0, truncates strings to\n  // `truncate` characters and all cells will be aligned right.\n  int32 truncate = 3;\n\n  // (Required) If set to true, prints output rows vertically (one line per column value).\n  bool vertical = 4;\n}\n\n// Compose the string representing rows for output.\n// It will invoke 'Dataset.htmlString' to compute the results.\nmessage HtmlString {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) Number of rows to show.\n  int32 num_rows = 2;\n\n  // (Required) If set to more than 0, truncates strings to\n  // `truncate` characters and all cells will be aligned right.\n  int32 truncate = 3;\n}\n\n// Computes specified statistics for numeric and string columns.\n// It will invoke 'Dataset.summary' (same as 'StatFunctions.summary')\n// to compute the results.\nmessage StatSummary {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) Statistics from to be computed.\n  //\n  // Available statistics are:\n  //  count\n  //  mean\n  //  stddev\n  //  min\n  //  max\n  //  arbitrary approximate percentiles specified as a percentage (e.g. 75%)\n  //  count_distinct\n  //  approx_count_distinct\n  //\n  // If no statistics are given, this function computes 'count', 'mean', 'stddev', 'min',\n  // 'approximate quartiles' (percentiles at 25%, 50%, and 75%), and 'max'.\n  repeated string statistics = 2;\n}\n\n// Computes basic statistics for numeric and string columns, including count, mean, stddev, min,\n// and max. If no columns are given, this function computes statistics for all numerical or\n// string columns.\nmessage StatDescribe {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) Columns to compute statistics on.\n  repeated string cols = 2;\n}\n\n// Computes a pair-wise frequency table of the given columns. Also known as a contingency table.\n// It will invoke 'Dataset.stat.crosstab' (same as 'StatFunctions.crossTabulate')\n// to compute the results.\nmessage StatCrosstab {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The name of the first column.\n  //\n  // Distinct items will make the first item of each row.\n  string col1 = 2;\n\n  // (Required) The name of the second column.\n  //\n  // Distinct items will make the column names of the DataFrame.\n  string col2 = 3;\n}\n\n// Calculate the sample covariance of two numerical columns of a DataFrame.\n// It will invoke 'Dataset.stat.cov' (same as 'StatFunctions.calculateCov') to compute the results.\nmessage StatCov {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The name of the first column.\n  string col1 = 2;\n\n  // (Required) The name of the second column.\n  string col2 = 3;\n}\n\n// Calculates the correlation of two columns of a DataFrame. Currently only supports the Pearson\n// Correlation Coefficient. It will invoke 'Dataset.stat.corr' (same as\n// 'StatFunctions.pearsonCorrelation') to compute the results.\nmessage StatCorr {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The name of the first column.\n  string col1 = 2;\n\n  // (Required) The name of the second column.\n  string col2 = 3;\n\n  // (Optional) Default value is 'pearson'.\n  //\n  // Currently only supports the Pearson Correlation Coefficient.\n  optional string method = 4;\n}\n\n// Calculates the approximate quantiles of numerical columns of a DataFrame.\n// It will invoke 'Dataset.stat.approxQuantile' (same as 'StatFunctions.approxQuantile')\n// to compute the results.\nmessage StatApproxQuantile {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The names of the numerical columns.\n  repeated string cols = 2;\n\n  // (Required) A list of quantile probabilities.\n  //\n  // Each number must belong to [0, 1].\n  // For example 0 is the minimum, 0.5 is the median, 1 is the maximum.\n  repeated double probabilities = 3;\n\n  // (Required) The relative target precision to achieve (greater than or equal to 0).\n  //\n  // If set to zero, the exact quantiles are computed, which could be very expensive.\n  // Note that values greater than 1 are accepted but give the same result as 1.\n  double relative_error = 4;\n}\n\n// Finding frequent items for columns, possibly with false positives.\n// It will invoke 'Dataset.stat.freqItems' (same as 'StatFunctions.freqItems')\n// to compute the results.\nmessage StatFreqItems {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The names of the columns to search frequent items in.\n  repeated string cols = 2;\n\n  // (Optional) The minimum frequency for an item to be considered `frequent`.\n  // Should be greater than 1e-4.\n  optional double support = 3;\n}\n\n\n// Returns a stratified sample without replacement based on the fraction\n// given on each stratum.\n// It will invoke 'Dataset.stat.freqItems' (same as 'StatFunctions.freqItems')\n// to compute the results.\nmessage StatSampleBy {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The column that defines strata.\n  Expression col = 2;\n\n  // (Required) Sampling fraction for each stratum.\n  //\n  // If a stratum is not specified, we treat its fraction as zero.\n  repeated Fraction fractions = 3;\n\n  // (Required) The random seed.\n  // This field is required to avoid generating mutable dataframes (see SPARK-48184 for details),\n  // however, still keep it 'optional' here for backward compatibility.\n  optional int64 seed = 5;\n\n  message Fraction {\n    // (Required) The stratum.\n    Expression.Literal stratum = 1;\n\n    // (Required) The fraction value. Must be in [0, 1].\n    double fraction = 2;\n  }\n}\n\n\n// Replaces null values.\n// It will invoke 'Dataset.na.fill' (same as 'DataFrameNaFunctions.fill') to compute the results.\n// Following 3 parameter combinations are supported:\n//  1, 'values' only contains 1 item, 'cols' is empty:\n//    replaces null values in all type-compatible columns.\n//  2, 'values' only contains 1 item, 'cols' is not empty:\n//    replaces null values in specified columns.\n//  3, 'values' contains more than 1 items, then 'cols' is required to have the same length:\n//    replaces each specified column with corresponding value.\nmessage NAFill {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) Optional list of column names to consider.\n  repeated string cols = 2;\n\n  // (Required) Values to replace null values with.\n  //\n  // Should contain at least 1 item.\n  // Only 4 data types are supported now: bool, long, double, string\n  repeated Expression.Literal values = 3;\n}\n\n\n// Drop rows containing null values.\n// It will invoke 'Dataset.na.drop' (same as 'DataFrameNaFunctions.drop') to compute the results.\nmessage NADrop {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) Optional list of column names to consider.\n  //\n  // When it is empty, all the columns in the input relation will be considered.\n  repeated string cols = 2;\n\n  // (Optional) The minimum number of non-null and non-NaN values required to keep.\n  //\n  // When not set, it is equivalent to the number of considered columns, which means\n  // a row will be kept only if all columns are non-null.\n  //\n  // 'how' options ('all', 'any') can be easily converted to this field:\n  //   - 'all' -> set 'min_non_nulls' 1;\n  //   - 'any' -> keep 'min_non_nulls' unset;\n  optional int32 min_non_nulls = 3;\n}\n\n\n// Replaces old values with the corresponding values.\n// It will invoke 'Dataset.na.replace' (same as 'DataFrameNaFunctions.replace')\n// to compute the results.\nmessage NAReplace {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) List of column names to consider.\n  //\n  // When it is empty, all the type-compatible columns in the input relation will be considered.\n  repeated string cols = 2;\n\n  // (Optional) The value replacement mapping.\n  repeated Replacement replacements = 3;\n\n  message Replacement {\n    // (Required) The old value.\n    //\n    // Only 4 data types are supported now: null, bool, double, string.\n    Expression.Literal old_value = 1;\n\n    // (Required) The new value.\n    //\n    // Should be of the same data type with the old value.\n    Expression.Literal new_value = 2;\n  }\n}\n\n\n// Rename columns on the input relation by the same length of names.\nmessage ToDF {\n  // (Required) The input relation of RenameColumnsBySameLengthNames.\n  Relation input = 1;\n\n  // (Required)\n  //\n  // The number of columns of the input relation must be equal to the length\n  // of this field. If this is not true, an exception will be returned.\n  repeated string column_names = 2;\n}\n\n\n// Rename columns on the input relation by a map with name to name mapping.\nmessage WithColumnsRenamed {\n  // (Required) The input relation.\n  Relation input = 1;\n\n\n  // (Optional)\n  //\n  // Renaming column names of input relation from A to B where A is the map key\n  // and B is the map value. This is a no-op if schema doesn't contain any A. It\n  // does not require that all input relation column names to present as keys.\n  // duplicated B are not allowed.\n  map<string, string> rename_columns_map = 2 [deprecated=true];\n\n  repeated Rename renames = 3;\n\n  message Rename {\n    // (Required) The existing column name.\n    string col_name = 1;\n\n    // (Required) The new column name.\n    string new_col_name = 2;\n  }\n}\n\n// Adding columns or replacing the existing columns that have the same names.\nmessage WithColumns {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required)\n  //\n  // Given a column name, apply the corresponding expression on the column. If column\n  // name exists in the input relation, then replace the column. If the column name\n  // does not exist in the input relation, then adds it as a new column.\n  //\n  // Only one name part is expected from each Expression.Alias.\n  //\n  // An exception is thrown when duplicated names are present in the mapping.\n  repeated Expression.Alias aliases = 2;\n}\n\nmessage WithWatermark {\n\n  // (Required) The input relation\n  Relation input = 1;\n\n  // (Required) Name of the column containing event time.\n  string event_time = 2;\n\n  // (Required)\n  string delay_threshold = 3;\n}\n\n// Specify a hint over a relation. Hint should have a name and optional parameters.\nmessage Hint {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) Hint name.\n  //\n  // Supported Join hints include BROADCAST, MERGE, SHUFFLE_HASH, SHUFFLE_REPLICATE_NL.\n  //\n  // Supported partitioning hints include COALESCE, REPARTITION, REPARTITION_BY_RANGE.\n  string name = 2;\n\n  // (Optional) Hint parameters.\n  repeated Expression parameters = 3;\n}\n\n// Unpivot a DataFrame from wide format to long format, optionally leaving identifier columns set.\nmessage Unpivot {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) Id columns.\n  repeated Expression ids = 2;\n\n  // (Optional) Value columns to unpivot.\n  optional Values values = 3;\n\n  // (Required) Name of the variable column.\n  string variable_column_name = 4;\n\n  // (Required) Name of the value column.\n  string value_column_name = 5;\n\n  message Values {\n    repeated Expression values = 1;\n  }\n}\n\n// Transpose a DataFrame, switching rows to columns.\n// Transforms the DataFrame such that the values in the specified index column\n// become the new columns of the DataFrame.\nmessage Transpose {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Optional) A list of columns that will be treated as the indices.\n  // Only single column is supported now.\n  repeated Expression index_columns = 2;\n}\n\nmessage UnresolvedTableValuedFunction {\n  // (Required) name (or unparsed name for user defined function) for the unresolved function.\n  string function_name = 1;\n\n  // (Optional) Function arguments. Empty arguments are allowed.\n  repeated Expression arguments = 2;\n}\n\nmessage ToSchema {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The user provided schema.\n  //\n  // The Sever side will update the dataframe with this schema.\n  DataType schema = 2;\n}\n\nmessage RepartitionByExpression {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) The partitioning expressions.\n  repeated Expression partition_exprs = 2;\n\n  // (Optional) number of partitions, must be positive.\n  optional int32 num_partitions = 3;\n}\n\nmessage MapPartitions {\n  // (Required) Input relation for a mapPartitions-equivalent API: mapInPandas, mapInArrow.\n  Relation input = 1;\n\n  // (Required) Input user-defined function.\n  CommonInlineUserDefinedFunction func = 2;\n\n  // (Optional) Whether to use barrier mode execution or not.\n  optional bool is_barrier = 3;\n\n  // (Optional) ResourceProfile id used for the stage level scheduling.\n  optional int32 profile_id = 4;\n}\n\nmessage GroupMap {\n  // (Required) Input relation for Group Map API: apply, applyInPandas.\n  Relation input = 1;\n\n  // (Required) Expressions for grouping keys.\n  repeated Expression grouping_expressions = 2;\n\n  // (Required) Input user-defined function.\n  CommonInlineUserDefinedFunction func = 3;\n\n  // (Optional) Expressions for sorting. Only used by Scala Sorted Group Map API.\n  repeated Expression sorting_expressions = 4;\n\n  // Below fields are only used by (Flat)MapGroupsWithState\n  // (Optional) Input relation for initial State.\n  Relation initial_input = 5;\n\n  // (Optional) Expressions for grouping keys of the initial state input relation.\n  repeated Expression initial_grouping_expressions = 6;\n\n  // (Optional) True if MapGroupsWithState, false if FlatMapGroupsWithState.\n  optional bool is_map_groups_with_state = 7;\n\n  // (Optional) The output mode of the function.\n  optional string output_mode = 8;\n\n  // (Optional) Timeout configuration for groups that do not receive data for a while.\n  optional string timeout_conf = 9;\n\n  // (Optional) The schema for the grouped state.\n  optional DataType state_schema = 10;\n\n  // Below fields are used by TransformWithState and TransformWithStateInPandas\n  // (Optional) TransformWithState related parameters.\n  optional TransformWithStateInfo transform_with_state_info = 11;\n}\n\n// Additional input parameters used for TransformWithState operator.\nmessage TransformWithStateInfo {\n  // (Required) Time mode string for transformWithState.\n  string time_mode = 1;\n\n  // (Optional) Event time column name.\n  optional string event_time_column_name = 2;\n\n  // (Optional) Schema for the output DataFrame.\n  // Only required used for TransformWithStateInPandas.\n  optional DataType output_schema = 3;\n}\n\nmessage CoGroupMap {\n  // (Required) One input relation for CoGroup Map API - applyInPandas.\n  Relation input = 1;\n\n  // Expressions for grouping keys of the first input relation.\n  repeated Expression input_grouping_expressions = 2;\n\n  // (Required) The other input relation.\n  Relation other = 3;\n\n  // Expressions for grouping keys of the other input relation.\n  repeated Expression other_grouping_expressions = 4;\n\n  // (Required) Input user-defined function.\n  CommonInlineUserDefinedFunction func = 5;\n\n  // (Optional) Expressions for sorting. Only used by Scala Sorted CoGroup Map API.\n  repeated Expression input_sorting_expressions = 6;\n\n  // (Optional) Expressions for sorting. Only used by Scala Sorted CoGroup Map API.\n  repeated Expression other_sorting_expressions = 7;\n}\n\nmessage ApplyInPandasWithState {\n  // (Required) Input relation for applyInPandasWithState.\n  Relation input = 1;\n\n  // (Required) Expressions for grouping keys.\n  repeated Expression grouping_expressions = 2;\n\n  // (Required) Input user-defined function.\n  CommonInlineUserDefinedFunction func = 3;\n\n  // (Required) Schema for the output DataFrame.\n  string output_schema = 4;\n\n  // (Required) Schema for the state.\n  string state_schema = 5;\n\n  // (Required) The output mode of the function.\n  string output_mode = 6;\n\n  // (Required) Timeout configuration for groups that do not receive data for a while.\n  string timeout_conf = 7;\n}\n\nmessage CommonInlineUserDefinedTableFunction {\n  // (Required) Name of the user-defined table function.\n  string function_name = 1;\n\n  // (Optional) Whether the user-defined table function is deterministic.\n  bool deterministic = 2;\n\n  // (Optional) Function input arguments. Empty arguments are allowed.\n  repeated Expression arguments = 3;\n\n  // (Required) Type of the user-defined table function.\n  oneof function {\n    PythonUDTF python_udtf = 4;\n  }\n}\n\nmessage PythonUDTF {\n  // (Optional) Return type of the Python UDTF.\n  optional DataType return_type = 1;\n\n  // (Required) EvalType of the Python UDTF.\n  int32 eval_type = 2;\n\n  // (Required) The encoded commands of the Python UDTF.\n  bytes command = 3;\n\n  // (Required) Python version being used in the client.\n  string python_ver = 4;\n}\n\nmessage CommonInlineUserDefinedDataSource {\n  // (Required) Name of the data source.\n  string name = 1;\n\n  // (Required) The data source type.\n  oneof data_source {\n    PythonDataSource python_data_source = 2;\n  }\n}\n\nmessage PythonDataSource {\n  // (Required) The encoded commands of the Python data source.\n  bytes command = 1;\n\n  // (Required) Python version being used in the client.\n  string python_ver = 2;\n}\n\n// Collect arbitrary (named) metrics from a dataset.\nmessage CollectMetrics {\n  // (Required) The input relation.\n  Relation input = 1;\n\n  // (Required) Name of the metrics.\n  string name = 2;\n\n  // (Required) The metric sequence.\n  repeated Expression metrics = 3;\n}\n\nmessage Parse {\n  // (Required) Input relation to Parse. The input is expected to have single text column.\n  Relation input = 1;\n  // (Required) The expected format of the text.\n  ParseFormat format = 2;\n\n  // (Optional) DataType representing the schema. If not set, Spark will infer the schema.\n  optional DataType schema = 3;\n\n  // Options for the csv/json parser. The map key is case insensitive.\n  map<string, string> options = 4;\n  enum ParseFormat {\n    PARSE_FORMAT_UNSPECIFIED = 0;\n    PARSE_FORMAT_CSV = 1;\n    PARSE_FORMAT_JSON = 2;\n  }\n}\n\n// Relation of type [[AsOfJoin]].\n//\n// `left` and `right` must be present.\nmessage AsOfJoin {\n  // (Required) Left input relation for a Join.\n  Relation left = 1;\n\n  // (Required) Right input relation for a Join.\n  Relation right = 2;\n\n  // (Required) Field to join on in left DataFrame\n  Expression left_as_of = 3;\n\n  // (Required) Field to join on in right DataFrame\n  Expression right_as_of = 4;\n\n  // (Optional) The join condition. Could be unset when `using_columns` is utilized.\n  //\n  // This field does not co-exist with using_columns.\n  Expression join_expr = 5;\n\n  // Optional. using_columns provides a list of columns that should present on both sides of\n  // the join inputs that this Join will join on. For example A JOIN B USING col_name is\n  // equivalent to A JOIN B on A.col_name = B.col_name.\n  //\n  // This field does not co-exist with join_condition.\n  repeated string using_columns = 6;\n\n  // (Required) The join type.\n  string join_type = 7;\n\n  // (Optional) The asof tolerance within this range.\n  Expression tolerance = 8;\n\n  // (Required) Whether allow matching with the same value or not.\n  bool allow_exact_matches = 9;\n\n  // (Required) Whether to search for prior, subsequent, or closest matches.\n  string direction = 10;\n}\n\n// Relation of type [[LateralJoin]].\n//\n// `left` and `right` must be present.\nmessage LateralJoin {\n  // (Required) Left input relation for a Join.\n  Relation left = 1;\n\n  // (Required) Right input relation for a Join.\n  Relation right = 2;\n\n  // (Optional) The join condition.\n  Expression join_condition = 3;\n\n  // (Required) The join type.\n  Join.JoinType join_type = 4;\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/protobuf/spark/connect/types.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = 'proto3';\n\npackage spark.connect;\n\noption java_multiple_files = true;\noption java_package = \"io.delta.connect.spark.proto\";\noption go_package = \"internal/generated\";\n\n// This message describes the logical [[DataType]] of something. It does not carry the value\n// itself but only describes it.\nmessage DataType {\n  oneof kind {\n    NULL null = 1;\n\n    Binary binary = 2;\n\n    Boolean boolean = 3;\n\n    // Numeric types\n    Byte byte = 4;\n    Short short = 5;\n    Integer integer = 6;\n    Long long = 7;\n\n    Float float = 8;\n    Double double = 9;\n    Decimal decimal = 10;\n\n    // String types\n    String string = 11;\n    Char char = 12;\n    VarChar var_char = 13;\n\n    // Datatime types\n    Date date = 14;\n    Timestamp timestamp = 15;\n    TimestampNTZ timestamp_ntz = 16;\n\n    // Interval types\n    CalendarInterval calendar_interval = 17;\n    YearMonthInterval year_month_interval = 18;\n    DayTimeInterval day_time_interval = 19;\n\n    // Complex types\n    Array array = 20;\n    Struct struct = 21;\n    Map map = 22;\n    Variant variant = 25;\n\n    // UserDefinedType\n    UDT udt = 23;\n\n    // UnparsedDataType\n    Unparsed unparsed = 24;\n  }\n\n  message Boolean {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Byte {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Short {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Integer {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Long {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Float {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Double {\n    uint32 type_variation_reference = 1;\n  }\n\n  message String {\n    uint32 type_variation_reference = 1;\n    string collation = 2;\n  }\n\n  message Binary {\n    uint32 type_variation_reference = 1;\n  }\n\n  message NULL {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Timestamp {\n    uint32 type_variation_reference = 1;\n  }\n\n  message Date {\n    uint32 type_variation_reference = 1;\n  }\n\n  message TimestampNTZ {\n    uint32 type_variation_reference = 1;\n  }\n\n  message CalendarInterval {\n    uint32 type_variation_reference = 1;\n  }\n\n  message YearMonthInterval {\n    optional int32 start_field = 1;\n    optional int32 end_field = 2;\n    uint32 type_variation_reference = 3;\n  }\n\n  message DayTimeInterval {\n    optional int32 start_field = 1;\n    optional int32 end_field = 2;\n    uint32 type_variation_reference = 3;\n  }\n\n  // Start compound types.\n  message Char {\n    int32 length = 1;\n    uint32 type_variation_reference = 2;\n  }\n\n  message VarChar {\n    int32 length = 1;\n    uint32 type_variation_reference = 2;\n  }\n\n  message Decimal {\n    optional int32 scale = 1;\n    optional int32 precision = 2;\n    uint32 type_variation_reference = 3;\n  }\n\n  message StructField {\n    string name = 1;\n    DataType data_type = 2;\n    bool nullable = 3;\n    optional string metadata = 4;\n  }\n\n  message Struct {\n    repeated StructField fields = 1;\n    uint32 type_variation_reference = 2;\n  }\n\n  message Array {\n    DataType element_type = 1;\n    bool contains_null = 2;\n    uint32 type_variation_reference = 3;\n  }\n\n  message Map {\n    DataType key_type = 1;\n    DataType value_type = 2;\n    bool value_contains_null = 3;\n    uint32 type_variation_reference = 4;\n  }\n\n  message Variant {\n    uint32 type_variation_reference = 1;\n  }\n\n  message UDT {\n    string type = 1;\n    // Required for Scala/Java UDT\n    optional string jvm_class = 2;\n    // Required for Python UDT\n    optional string python_class = 3;\n    // Required for Python UDT\n    optional string serialized_python_class = 4;\n    // Required for Python UDT\n    optional DataType sql_type = 5;\n  }\n\n  message Unparsed {\n    // (Required) The unparsed data type string\n    string data_type_string = 1;\n  }\n}\n"
  },
  {
    "path": "spark-connect/common/src/main/scala/org/apache/spark/sql/connect/delta/ImplicitProtoConversions.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.connect.delta\n\nimport io.delta.connect.spark.{proto => delta_spark_proto}\n\nimport org.apache.spark.connect.{proto => spark_proto}\nimport org.apache.spark.sql.connect.ConnectProtoUtils\n\nobject ImplicitProtoConversions {\n  implicit def convertRelationToSpark(\n      relation: delta_spark_proto.Relation): spark_proto.Relation = {\n    ConnectProtoUtils.parseRelationWithRecursionLimit(relation.toByteArray, recursionLimit = 1024)\n  }\n\n  implicit def convertRelationToDelta(\n      relation: spark_proto.Relation): delta_spark_proto.Relation = {\n    // TODO: Recursion limits\n    delta_spark_proto.Relation.parseFrom(relation.toByteArray)\n  }\n\n  implicit def convertCommandToSpark(\n      command: delta_spark_proto.Command): spark_proto.Command = {\n    ConnectProtoUtils.parseCommandWithRecursionLimit(command.toByteArray, recursionLimit = 1024)\n  }\n\n  implicit def convertCommandToDelta(\n      command: spark_proto.Command): delta_spark_proto.Command = {\n    // TODO: Recursion limits\n    delta_spark_proto.Command.parseFrom(command.toByteArray)\n  }\n\n  implicit def convertExpressionToSpark(\n      expr: delta_spark_proto.Expression): spark_proto.Expression = {\n    ConnectProtoUtils.parseExpressionWithRecursionLimit(expr.toByteArray, recursionLimit = 1024)\n  }\n\n  implicit def convertExpressionToDelta(\n      expr: spark_proto.Expression): delta_spark_proto.Expression = {\n    // TODO: Recursion limits\n    delta_spark_proto.Expression.parseFrom(expr.toByteArray)\n  }\n\n  implicit def convertDataTypeToSpark(\n      dataType: delta_spark_proto.DataType): spark_proto.DataType = {\n    ConnectProtoUtils.parseDataTypeWithRecursionLimit(dataType.toByteArray, recursionLimit = 1024)\n  }\n\n  implicit def convertDataTypeToDelta(\n      dataType: spark_proto.DataType): delta_spark_proto.DataType = {\n    // TODO: Recursion limits\n    delta_spark_proto.DataType.parseFrom(dataType.toByteArray)\n  }\n}\n"
  },
  {
    "path": "spark-connect/server/src/main/scala/io/delta/connect/DeltaCommandPlugin.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.connect.delta\n\nimport scala.collection.JavaConverters._\n\nimport com.google.protobuf\nimport io.delta.connect.proto\nimport io.delta.connect.spark.{proto => spark_proto}\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.connect.common.{DataTypeProtoConverter, InvalidPlanInput}\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\nimport org.apache.spark.sql.connect.planner.SparkConnectPlanner\nimport org.apache.spark.sql.connect.plugin.CommandPlugin\n\n/**\n * Planner plugin for command extensions using [[proto.DeltaCommand]].\n */\nclass DeltaCommandPlugin extends CommandPlugin with DeltaPlannerBase {\n  override def process(raw: Array[Byte], planner: SparkConnectPlanner): Boolean = {\n    val command = protobuf.Any.parseFrom(raw)\n    if (command.is(classOf[proto.DeltaCommand])) {\n      process(command.unpack(classOf[proto.DeltaCommand]), planner)\n      true\n    } else {\n      false\n    }\n  }\n\n  private def process(command: proto.DeltaCommand, planner: SparkConnectPlanner): Unit = {\n    command.getCommandTypeCase match {\n      case proto.DeltaCommand.CommandTypeCase.CLONE_TABLE =>\n        processCloneTable(planner, command.getCloneTable)\n      case proto.DeltaCommand.CommandTypeCase.VACUUM_TABLE =>\n        processVacuumTable(planner, command.getVacuumTable)\n      case proto.DeltaCommand.CommandTypeCase.UPGRADE_TABLE_PROTOCOL =>\n        processUpgradeTableProtocol(planner, command.getUpgradeTableProtocol)\n      case proto.DeltaCommand.CommandTypeCase.GENERATE =>\n        processGenerate(planner, command.getGenerate)\n      case proto.DeltaCommand.CommandTypeCase.CREATE_DELTA_TABLE =>\n        processCreateDeltaTable(planner, command.getCreateDeltaTable)\n      case proto.DeltaCommand.CommandTypeCase.ADD_FEATURE_SUPPORT =>\n        processAddFeatureSupport(planner, command.getAddFeatureSupport)\n      case proto.DeltaCommand.CommandTypeCase.DROP_FEATURE_SUPPORT =>\n        processDropFeatureSupport(planner, command.getDropFeatureSupport)\n      case _ =>\n        throw InvalidPlanInput(s\"${command.getCommandTypeCase}\")\n    }\n  }\n\n\n  private def processCloneTable(\n      planner: SparkConnectPlanner, cloneTable: proto.CloneTable): Unit = {\n    val deltaTable = transformDeltaTable(planner, cloneTable.getTable)\n    if (cloneTable.hasVersion) {\n      deltaTable.cloneAtVersion(\n        cloneTable.getVersion,\n        cloneTable.getTarget,\n        cloneTable.getIsShallow,\n        cloneTable.getReplace,\n        cloneTable.getPropertiesMap.asScala.toMap\n      )\n    } else if (cloneTable.hasTimestamp) {\n      deltaTable.cloneAtTimestamp(\n        cloneTable.getTimestamp,\n        cloneTable.getTarget,\n        cloneTable.getIsShallow,\n        cloneTable.getReplace,\n        cloneTable.getPropertiesMap.asScala.toMap\n      )\n    } else {\n      deltaTable.clone(\n        cloneTable.getTarget,\n        cloneTable.getIsShallow,\n        cloneTable.getReplace,\n        cloneTable.getPropertiesMap.asScala.toMap\n      )\n    }\n  }\n\n  private def processVacuumTable(planner: SparkConnectPlanner, vacuum: proto.VacuumTable): Unit = {\n    val deltaTable = transformDeltaTable(planner, vacuum.getTable)\n    if (vacuum.hasRetentionHours) {\n      deltaTable.vacuum(vacuum.getRetentionHours)\n    } else {\n      deltaTable.vacuum()\n    }\n  }\n\n  private def processUpgradeTableProtocol(\n      planner: SparkConnectPlanner, upgradeTableProtocol: proto.UpgradeTableProtocol): Unit = {\n    val deltaTable = transformDeltaTable(planner, upgradeTableProtocol.getTable)\n    deltaTable.upgradeTableProtocol(\n      upgradeTableProtocol.getReaderVersion, upgradeTableProtocol.getWriterVersion)\n  }\n\n  private def processGenerate(planner: SparkConnectPlanner, generate: proto.Generate): Unit = {\n    val deltaTable = transformDeltaTable(planner, generate.getTable)\n    deltaTable.generate(generate.getMode)\n  }\n\n  private def processCreateDeltaTable(\n      planner: SparkConnectPlanner, createDeltaTable: proto.CreateDeltaTable): Unit = {\n    val spark = planner.session\n    val tableBuilder = createDeltaTable.getMode match {\n      case proto.CreateDeltaTable.Mode.MODE_CREATE =>\n        DeltaTable.create(spark)\n      case proto.CreateDeltaTable.Mode.MODE_CREATE_IF_NOT_EXISTS =>\n        DeltaTable.createIfNotExists(spark)\n      case proto.CreateDeltaTable.Mode.MODE_REPLACE =>\n        DeltaTable.replace(spark)\n      case proto.CreateDeltaTable.Mode.MODE_CREATE_OR_REPLACE =>\n        DeltaTable.createOrReplace(spark)\n      case _ =>\n        throw new Exception(\"Unsupported table creation mode\")\n    }\n    if (createDeltaTable.hasTableName) {\n      tableBuilder.tableName(createDeltaTable.getTableName)\n    }\n    if (createDeltaTable.hasLocation) {\n      tableBuilder.location(createDeltaTable.getLocation)\n    }\n    if (createDeltaTable.hasComment) {\n      tableBuilder.comment(createDeltaTable.getComment)\n    }\n    for (column <- createDeltaTable.getColumnsList.asScala) {\n      val colBuilder = DeltaTable\n        .columnBuilder(spark, column.getName)\n        .nullable(column.getNullable)\n      if (column.getDataType.getKindCase == spark_proto.DataType.KindCase.UNPARSED) {\n        colBuilder.dataType(column.getDataType.getUnparsed.getDataTypeString)\n      } else {\n        colBuilder.dataType(DataTypeProtoConverter.toCatalystType(column.getDataType))\n      }\n      if (column.hasGeneratedAlwaysAs) {\n        colBuilder.generatedAlwaysAs(column.getGeneratedAlwaysAs)\n      }\n      if (column.hasIdentityInfo) {\n        val identityInfo = column.getIdentityInfo\n        if (identityInfo.getAllowExplicitInsert) {\n          colBuilder.generatedByDefaultAsIdentity(identityInfo.getStart, identityInfo.getStep)\n        } else {\n          colBuilder.generatedAlwaysAsIdentity(identityInfo.getStart, identityInfo.getStep)\n        }\n      }\n      if (column.hasComment) {\n        colBuilder.comment(column.getComment)\n      }\n      tableBuilder.addColumn(colBuilder.build())\n    }\n    val partitioningColumns = createDeltaTable.getPartitioningColumnsList.asScala.toSeq\n    if (!partitioningColumns.isEmpty) {\n      tableBuilder.partitionedBy(partitioningColumns: _*)\n    }\n    val clusteringColumns = createDeltaTable.getClusteringColumnsList.asScala.toSeq\n    if (!clusteringColumns.isEmpty) {\n      tableBuilder.clusterBy(clusteringColumns: _*)\n    }\n    for ((key, value) <- createDeltaTable.getPropertiesMap.asScala) {\n      tableBuilder.property(key, value)\n    }\n    tableBuilder.execute()\n  }\n\n  private def processAddFeatureSupport(\n      planner: SparkConnectPlanner, addFeatureSupport: proto.AddFeatureSupport): Unit = {\n    val deltaTable = transformDeltaTable(planner, addFeatureSupport.getTable)\n    deltaTable.addFeatureSupport(addFeatureSupport.getFeatureName)\n  }\n\n  private def processDropFeatureSupport(\n      planner: SparkConnectPlanner, dropFeatureSupport: proto.DropFeatureSupport): Unit = {\n    val deltaTable = transformDeltaTable(planner, dropFeatureSupport.getTable)\n    if (dropFeatureSupport.hasTruncateHistory) {\n      deltaTable.dropFeatureSupport(\n        dropFeatureSupport.getFeatureName,\n        dropFeatureSupport.getTruncateHistory)\n    } else {\n      deltaTable.dropFeatureSupport(dropFeatureSupport.getFeatureName)\n    }\n  }\n}\n"
  },
  {
    "path": "spark-connect/server/src/main/scala/io/delta/connect/DeltaPlannerBase.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.connect.delta\n\nimport io.delta.connect.proto\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.sql.connect.planner.SparkConnectPlanner\n\n/**\n * Base trait for the planner plugins of Delta Connect.\n */\ntrait DeltaPlannerBase {\n  protected def transformDeltaTable(\n      planner: SparkConnectPlanner, deltaTable: proto.DeltaTable): DeltaTable = {\n    deltaTable.getAccessTypeCase match {\n      case proto.DeltaTable.AccessTypeCase.PATH =>\n        DeltaTable.forPath(\n          planner.session, deltaTable.getPath.getPath, deltaTable.getPath.getHadoopConfMap)\n      case proto.DeltaTable.AccessTypeCase.TABLE_OR_VIEW_NAME =>\n        DeltaTable.forName(planner.session, deltaTable.getTableOrViewName)\n    }\n  }\n}\n"
  },
  {
    "path": "spark-connect/server/src/main/scala/io/delta/connect/DeltaRelationPlugin.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.connect.delta\n\nimport java.util.Optional\n\nimport scala.collection.JavaConverters._\n\nimport com.google.protobuf\nimport com.google.protobuf.{ByteString, InvalidProtocolBufferException}\nimport io.delta.connect.proto\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.SparkEnv\nimport org.apache.spark.sql.{Dataset, Encoders, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.UnresolvedStar\nimport org.apache.spark.sql.catalyst.expressions.Literal\nimport org.apache.spark.sql.catalyst.expressions.Expression\nimport org.apache.spark.sql.catalyst.plans.logical._\nimport org.apache.spark.sql.classic.Dataset\nimport org.apache.spark.sql.connect.common.{DataTypeProtoConverter, InvalidPlanInput}\nimport org.apache.spark.sql.connect.config.Connect\nimport org.apache.spark.sql.connect.delta.DeltaRelationPlugin.{parseAnyFrom, parseRelationFrom}\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\nimport org.apache.spark.sql.connect.planner.SparkConnectPlanner\nimport org.apache.spark.sql.connect.plugin.RelationPlugin\nimport org.apache.spark.sql.delta.DataFrameUtils\nimport org.apache.spark.sql.delta.commands.ConvertToDeltaCommand\nimport org.apache.spark.sql.types.StructType\n\n/**\n * Planner plugin for relation extensions using [[proto.DeltaRelation]].\n */\nclass DeltaRelationPlugin extends RelationPlugin with DeltaPlannerBase {\n  override def transform(raw: Array[Byte], planner: SparkConnectPlanner): Optional[LogicalPlan] = {\n    val relation = parseAnyFrom(raw,\n      SparkEnv.get.conf.get(Connect.CONNECT_GRPC_MARSHALLER_RECURSION_LIMIT))\n    if (relation.is(classOf[proto.DeltaRelation])) {\n      Optional.of(\n        transform(\n          parseRelationFrom(relation.getValue,\n            SparkEnv.get.conf.get(Connect.CONNECT_GRPC_MARSHALLER_RECURSION_LIMIT)),\n          planner\n        ))\n    } else {\n      Optional.empty()\n    }\n  }\n\n\n  private def transform(\n      relation: proto.DeltaRelation, planner: SparkConnectPlanner): LogicalPlan = {\n    relation.getRelationTypeCase match {\n      case proto.DeltaRelation.RelationTypeCase.SCAN =>\n        transformScan(planner, relation.getScan)\n      case proto.DeltaRelation.RelationTypeCase.DESCRIBE_HISTORY =>\n        transformDescribeHistory(planner, relation.getDescribeHistory)\n      case proto.DeltaRelation.RelationTypeCase.DESCRIBE_DETAIL =>\n        transformDescribeDetail(planner, relation.getDescribeDetail)\n      case proto.DeltaRelation.RelationTypeCase.CONVERT_TO_DELTA =>\n        transformConvertToDelta(planner, relation.getConvertToDelta)\n      case proto.DeltaRelation.RelationTypeCase.RESTORE_TABLE =>\n        transformRestoreTable(planner, relation.getRestoreTable)\n      case proto.DeltaRelation.RelationTypeCase.IS_DELTA_TABLE =>\n        transformIsDeltaTable(planner.session, relation.getIsDeltaTable)\n      case proto.DeltaRelation.RelationTypeCase.DELETE_FROM_TABLE =>\n        transformDeleteFromTable(planner, relation.getDeleteFromTable)\n      case proto.DeltaRelation.RelationTypeCase.UPDATE_TABLE =>\n        transformUpdateTable(planner, relation.getUpdateTable)\n      case proto.DeltaRelation.RelationTypeCase.MERGE_INTO_TABLE =>\n        transformMergeIntoTable(planner, relation.getMergeIntoTable)\n      case proto.DeltaRelation.RelationTypeCase.OPTIMIZE_TABLE =>\n        transformOptimizeTable(planner, relation.getOptimizeTable)\n      case _ =>\n        throw InvalidPlanInput(s\"Unknown DeltaRelation ${relation.getRelationTypeCase}\")\n    }\n  }\n\n  private def transformScan(planner: SparkConnectPlanner, scan: proto.Scan): LogicalPlan = {\n    val deltaTable = transformDeltaTable(planner, scan.getTable)\n    deltaTable.toDF.queryExecution.analyzed\n  }\n\n  private def transformDescribeHistory(\n      planner: SparkConnectPlanner, describeHistory: proto.DescribeHistory): LogicalPlan = {\n    val deltaTable = transformDeltaTable(planner, describeHistory.getTable)\n    deltaTable.history().queryExecution.analyzed\n  }\n\n  private def transformDescribeDetail(\n      planner: SparkConnectPlanner, describeDetail: proto.DescribeDetail): LogicalPlan = {\n    val deltaTable = transformDeltaTable(planner, describeDetail.getTable)\n    deltaTable.detail().queryExecution.analyzed\n  }\n\n  private def transformConvertToDelta(\n      planner: SparkConnectPlanner, convertToDelta: proto.ConvertToDelta): LogicalPlan = {\n    val spark = planner.session\n    val tableIdentifier =\n      spark.sessionState.sqlParser.parseTableIdentifier(convertToDelta.getIdentifier)\n    val partitionSchema = if (convertToDelta.hasPartitionSchemaStruct) {\n      Some(DataTypeProtoConverter.toCatalystType(convertToDelta.getPartitionSchemaStruct)\n        .asInstanceOf[StructType])\n    } else if (convertToDelta.hasPartitionSchemaString) {\n      Some(StructType.fromDDL(convertToDelta.getPartitionSchemaString))\n    } else {\n      None\n    }\n\n    val cvt = ConvertToDeltaCommand(\n      tableIdentifier,\n      partitionSchema,\n      collectStats = true,\n      deltaPath = None)\n    cvt.run(spark)\n\n    val result = if (cvt.isCatalogTable(spark.sessionState.analyzer, tableIdentifier)) {\n      convertToDelta.getIdentifier\n    } else {\n      s\"delta.`${tableIdentifier.table}`\"\n    }\n    spark.createDataset(result :: Nil)(Encoders.STRING).queryExecution.analyzed\n  }\n\n  private def transformRestoreTable(\n      planner: SparkConnectPlanner, restoreTable: proto.RestoreTable): LogicalPlan = {\n    val deltaTable = transformDeltaTable(planner, restoreTable.getTable)\n    val df = if (restoreTable.hasVersion) {\n      deltaTable.restoreToVersion(restoreTable.getVersion)\n    } else if (restoreTable.hasTimestamp) {\n      deltaTable.restoreToTimestamp(restoreTable.getTimestamp)\n    } else {\n      throw new RuntimeException()\n    }\n    df.queryExecution.commandExecuted\n  }\n\n  private def transformIsDeltaTable(\n      spark: SparkSession, isDeltaTable: proto.IsDeltaTable): LogicalPlan = {\n    val result = DeltaTable.isDeltaTable(spark, isDeltaTable.getPath)\n    spark.createDataset(result :: Nil)(Encoders.scalaBoolean).queryExecution.analyzed\n  }\n\n  private def transformDeleteFromTable(\n      planner: SparkConnectPlanner, deleteFromTable: proto.DeleteFromTable): LogicalPlan = {\n    val target = planner.transformRelation(deleteFromTable.getTarget)\n    val condition = if (deleteFromTable.hasCondition) {\n      Some(planner.transformExpression(deleteFromTable.getCondition))\n    } else {\n      None\n    }\n    DataFrameUtils.ofRows(\n        planner.session, DeleteFromTable(target, condition.getOrElse(Literal.TrueLiteral)))\n      .queryExecution.commandExecuted\n  }\n\n  private def transformUpdateTable(\n      planner: SparkConnectPlanner, updateTable: proto.UpdateTable): LogicalPlan = {\n    val target = planner.transformRelation(updateTable.getTarget)\n    val condition = if (updateTable.hasCondition) {\n      Some(planner.transformExpression(updateTable.getCondition))\n    } else {\n      None\n    }\n    val assignments = updateTable.getAssignmentsList.asScala.map(transformAssignment(planner, _))\n    DataFrameUtils.ofRows(planner.session, UpdateTable(target, assignments.toSeq, condition))\n      .queryExecution.commandExecuted\n  }\n\n  private def transformMergeIntoTable(\n      planner: SparkConnectPlanner, protoMerge: proto.MergeIntoTable): LogicalPlan = {\n    val target = planner.transformRelation(protoMerge.getTarget)\n    val source = planner.transformRelation(protoMerge.getSource)\n    val condition = planner.transformExpression(protoMerge.getCondition)\n    val matchedActions = protoMerge.getMatchedActionsList.asScala\n      .map(transformMergeWhenMatchedAction(planner, _))\n    val notMatchedActions = protoMerge.getNotMatchedActionsList.asScala\n      .map(transformMergeWhenNotMatchedAction(planner, _))\n    val notMatchedBySourceActions = protoMerge.getNotMatchedBySourceActionsList.asScala\n      .map(transformMergeWhenNotMatchedBySourceAction(planner, _))\n    val withSchemaEvolution = protoMerge.getWithSchemaEvolution\n\n    val merge = DeltaMergeInto(\n      target,\n      source,\n      condition,\n      matchedActions.toSeq ++ notMatchedActions.toSeq ++ notMatchedBySourceActions.toSeq,\n      withSchemaEvolution\n    )\n    Dataset.ofRows(planner.session, merge).queryExecution.commandExecuted\n  }\n\n  private def transformMergeActionCondition(\n      planner: SparkConnectPlanner,\n      protoAction: proto.MergeIntoTable.Action): Option[Expression] = {\n    if (protoAction.hasCondition) {\n      Some(planner.transformExpression(protoAction.getCondition))\n    } else {\n      None\n    }\n  }\n\n  private def transformMergeWhenMatchedAction(\n      planner: SparkConnectPlanner,\n      protoAction: proto.MergeIntoTable.Action): DeltaMergeIntoMatchedClause = {\n    val condition = transformMergeActionCondition(planner, protoAction)\n\n    protoAction.getActionTypeCase match {\n      case proto.MergeIntoTable.Action.ActionTypeCase.DELETE_ACTION =>\n        DeltaMergeIntoMatchedDeleteClause(condition)\n      case proto.MergeIntoTable.Action.ActionTypeCase.UPDATE_ACTION =>\n        val actions = transformMergeAssignments(\n          planner, protoAction.getUpdateAction.getAssignmentsList.asScala.toSeq)\n        DeltaMergeIntoMatchedUpdateClause(condition, actions)\n      case proto.MergeIntoTable.Action.ActionTypeCase.UPDATE_STAR_ACTION =>\n        DeltaMergeIntoMatchedUpdateClause(condition, Seq(UnresolvedStar(None)))\n    }\n  }\n\n  private def transformMergeWhenNotMatchedAction(\n      planner: SparkConnectPlanner,\n      protoAction: proto.MergeIntoTable.Action): DeltaMergeIntoNotMatchedClause = {\n    val condition = transformMergeActionCondition(planner, protoAction)\n\n    protoAction.getActionTypeCase match {\n      case proto.MergeIntoTable.Action.ActionTypeCase.INSERT_ACTION =>\n        val actions = transformMergeAssignments(\n          planner, protoAction.getInsertAction.getAssignmentsList.asScala.toSeq)\n        DeltaMergeIntoNotMatchedInsertClause(condition, actions)\n      case proto.MergeIntoTable.Action.ActionTypeCase.INSERT_STAR_ACTION =>\n        DeltaMergeIntoNotMatchedInsertClause(condition, Seq(UnresolvedStar(None)))\n    }\n  }\n\n  private def transformMergeWhenNotMatchedBySourceAction(\n      planner: SparkConnectPlanner,\n      protoAction: proto.MergeIntoTable.Action): DeltaMergeIntoNotMatchedBySourceClause = {\n    val condition = transformMergeActionCondition(planner, protoAction)\n\n    protoAction.getActionTypeCase match {\n      case proto.MergeIntoTable.Action.ActionTypeCase.DELETE_ACTION =>\n        DeltaMergeIntoNotMatchedBySourceDeleteClause(condition)\n      case proto.MergeIntoTable.Action.ActionTypeCase.UPDATE_ACTION =>\n        val actions = transformMergeAssignments(\n          planner, protoAction.getUpdateAction.getAssignmentsList.asScala.toSeq)\n        DeltaMergeIntoNotMatchedBySourceUpdateClause(condition, actions)\n    }\n  }\n\n  private def transformMergeAssignments(\n      planner: SparkConnectPlanner,\n      protoAssignments: Seq[proto.Assignment]): Seq[Expression] = {\n    if (protoAssignments.isEmpty) {\n      Seq.empty\n    } else {\n      DeltaMergeIntoClause.toActions(protoAssignments.map(transformAssignment(planner, _)))\n    }\n  }\n\n  private def transformAssignment(\n      planner: SparkConnectPlanner, assignment: proto.Assignment): Assignment = {\n    Assignment(\n      key = planner.transformExpression(assignment.getField),\n      value = planner.transformExpression(assignment.getValue))\n  }\n\n  private def transformOptimizeTable(\n      planner: SparkConnectPlanner, optimizeTable: proto.OptimizeTable): LogicalPlan = {\n    val deltaTable = transformDeltaTable(planner, optimizeTable.getTable)\n    var optimizeBuilder = deltaTable.optimize()\n    for (partitionFilter <- optimizeTable.getPartitionFiltersList.asScala) {\n      optimizeBuilder = optimizeBuilder.where(partitionFilter)\n    }\n    val df = if (optimizeTable.getZorderColumnsList.isEmpty) {\n      optimizeBuilder.executeCompaction()\n    } else {\n      optimizeBuilder.executeZOrderBy(optimizeTable.getZorderColumnsList.asScala.toSeq: _*)\n    }\n    df.queryExecution.commandExecuted\n  }\n}\n\nobject DeltaRelationPlugin {\n  private def parseAnyFrom(ba: Array[Byte], recursionLimit: Int): protobuf.Any = {\n    val bs = ByteString.copyFrom(ba)\n    val cis = bs.newCodedInput()\n    cis.setSizeLimit(Integer.MAX_VALUE)\n    cis.setRecursionLimit(recursionLimit)\n    val plan = protobuf.Any.parseFrom(cis)\n    try {\n      // If the last tag is 0, it means the message is correctly parsed.\n      // If the last tag is not 0, it means the message is not correctly\n      // parsed, and we should throw an exception.\n      cis.checkLastTagWas(0)\n      plan\n    } catch {\n      case e: InvalidProtocolBufferException =>\n        e.setUnfinishedMessage(plan)\n        throw e\n    }\n  }\n\n  private def parseRelationFrom(bs: ByteString, recursionLimit: Int): proto.DeltaRelation = {\n    val cis = bs.newCodedInput()\n    cis.setSizeLimit(Integer.MAX_VALUE)\n    cis.setRecursionLimit(recursionLimit)\n    val plan = proto.DeltaRelation.parseFrom(cis)\n    try {\n      // If the last tag is 0, it means the message is correctly parsed.\n      // If the last tag is not 0, it means the message is not correctly\n      // parsed, and we should throw an exception.\n      cis.checkLastTagWas(0)\n      plan\n    } catch {\n      case e: InvalidProtocolBufferException =>\n        e.setUnfinishedMessage(plan)\n        throw e\n    }\n  }\n}\n"
  },
  {
    "path": "spark-connect/server/src/main/scala/io/delta/connect/SimpleDeltaConnectService.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * This file contains code from the Apache Spark project (original license above).\n * It contains modifications, which are licensed as follows:\n */\n\n/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.tables\n\nimport java.util.concurrent.TimeUnit\n\nimport scala.io.StdIn\nimport scala.sys.exit\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.connect.service.SparkConnectService\n\n/**\n * A simple main class method to start the delta connect server as a service for client tests\n * using spark-submit:\n * {{{\n *     bin/spark-submit --class io.delta.tables.SimpleDeltaConnectService\n * }}}\n * The service can be stopped by receiving a stop command or until the service get killed.\n */\nobject SimpleDeltaConnectService {\n  private val stopCommand = \"q\"\n\n  def main(args: Array[String]): Unit = {\n    val conf = new SparkConf()\n      .set(\"spark.plugins\", \"org.apache.spark.sql.connect.SparkConnectPlugin\")\n      .set(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n      .set(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n    val sparkSession = SparkSession.builder().config(conf).getOrCreate()\n    // scalastyle:off println\n    println(\"Ready for client connections.\")\n    // scalastyle:on println\n    while (true) {\n      val code = StdIn.readLine()\n      if (code == stopCommand) {\n        // scalastyle:off println\n        println(\"No more client connections.\")\n        // scalastyle:on println\n        // Wait for 1 min for the server to stop\n        SparkConnectService.stop(Some(1), Some(TimeUnit.MINUTES))\n        sparkSession.close()\n        exit(0)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark-connect/server/src/test/scala/io/delta/connect/DeltaConnectPlannerSuite.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.connect.delta\n\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.UUID\n\nimport scala.collection.JavaConverters._\n\nimport com.google.protobuf\nimport io.delta.connect.proto\nimport io.delta.connect.spark.{proto => spark_proto}\nimport io.delta.tables.DeltaTable\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.{AnalysisException, Dataset, QueryTest, Row, SparkSession}\nimport org.apache.spark.sql.catalyst.analysis.ResolvedTable\nimport org.apache.spark.sql.catalyst.encoders.ExpressionEncoder\nimport org.apache.spark.sql.catalyst.plans.logical.LocalRelation\nimport org.apache.spark.sql.classic.{Dataset, DataFrame}\nimport org.apache.spark.sql.connect.config.Connect\nimport org.apache.spark.sql.connect.delta.ImplicitProtoConversions._\nimport org.apache.spark.sql.connect.planner.{SparkConnectPlanTest, SparkConnectPlanner}\nimport org.apache.spark.sql.connect.service.{SessionHolder, SparkConnectService}\nimport org.apache.spark.sql.delta.{DataFrameUtils, DeltaConfigs, DeltaHistory, DeltaLog, DeltaTableFeatureException, TestReaderWriterFeature, TestRemovableWriterFeature}\nimport org.apache.spark.sql.delta.ClassicColumnConversions._\nimport org.apache.spark.sql.delta.actions.Protocol\nimport org.apache.spark.sql.delta.commands.{DescribeDeltaDetailCommand, DescribeDeltaHistory}\nimport org.apache.spark.sql.delta.commands.optimize.OptimizeMetrics\nimport org.apache.spark.sql.delta.hooks.GenerateSymlinkManifest\nimport org.apache.spark.sql.delta.sources.{DeltaSourceUtils, DeltaSQLConf}\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils.GENERATION_EXPRESSION_METADATA_KEY\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.delta.test.DeltaTestImplicits._\nimport org.apache.spark.sql.delta.util.{DeltaFileOperations, FileNames}\nimport org.apache.spark.sql.types.{IntegerType, LongType, MetadataBuilder, StructField, StructType}\nimport org.apache.spark.sql.functions._\n\nclass DeltaConnectPlannerSuite\n  extends QueryTest\n  with DeltaSQLCommandTest\n  with SparkConnectPlanTest {\n    \n  import DeltaConnectPlannerSuite._\n\n  protected override def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(Connect.CONNECT_EXTENSIONS_RELATION_CLASSES.key, classOf[DeltaRelationPlugin].getName)\n      .set(Connect.CONNECT_EXTENSIONS_COMMAND_CLASSES.key, classOf[DeltaCommandPlugin].getName)\n  }\n\n  def createDummySessionHolder(session: SparkSession): SessionHolder = {\n    val ret = SessionHolder(\n      userId = \"testUser\",\n      sessionId = UUID.randomUUID().toString,\n      session = session\n    )\n    SparkConnectService.sessionManager.putSessionForTesting(ret)\n    ret\n  }\n\n  test(\"scan table by name\") {\n    withTable(\"table\") {\n      DeltaTable.create(spark).tableName(identifier = \"table\").execute()\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setScan(\n            proto.Scan\n              .newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(\"table\"))\n          )\n      )\n\n      val result = transform(input)\n      val expected = DeltaTable.forName(spark, \"table\").toDF.queryExecution.analyzed\n      comparePlans(result, expected)\n    }\n  }\n\n  test(\"scan table by path\") {\n    withTempDir { dir =>\n      DeltaTable.create(spark).location(dir.getAbsolutePath).execute()\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setScan(\n            proto.Scan\n              .newBuilder()\n              .setTable(\n                proto.DeltaTable\n                  .newBuilder()\n                  .setPath(\n                    proto.DeltaTable.Path.newBuilder().setPath(dir.getAbsolutePath)\n                  )\n              )\n          )\n      )\n\n      val result = transform(input)\n      val expected = DeltaTable.forPath(spark, dir.getAbsolutePath).toDF.queryExecution.analyzed\n      comparePlans(result, expected)\n    }\n  }\n\n  test(\"convert to delta\") {\n    withTempDir { dir =>\n      spark.range(100).write.format(\"parquet\").mode(\"overwrite\").save(dir.getAbsolutePath)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setConvertToDelta(\n            proto.ConvertToDelta\n              .newBuilder()\n              .setIdentifier(s\"parquet.`${dir.getAbsolutePath}`\")\n          )\n      )\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n\n      assert(result.length === 1)\n      val deltaTable = DeltaTable.forName(spark, result.head.getString(0))\n      assert(!deltaTable.toDF.isEmpty)\n    }\n  }\n\n  test(\"convert to delta with partitioning schema string\") {\n    withTempDir { dir =>\n      spark.range(100)\n        .select(col(\"id\") % 10 as \"part\", col(\"id\") as \"value\")\n        .write\n        .partitionBy(\"part\")\n        .format(\"parquet\")\n        .mode(\"overwrite\")\n        .save(dir.getAbsolutePath)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setConvertToDelta(\n            proto.ConvertToDelta\n              .newBuilder()\n              .setIdentifier(s\"parquet.`${dir.getAbsolutePath}`\")\n              .setPartitionSchemaString(\"part LONG\")\n          )\n      )\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n\n      assert(result.length === 1)\n      val deltaTable = DeltaTable.forName(spark, result.head.getString(0))\n      assert(!deltaTable.toDF.isEmpty)\n    }\n  }\n\n  test(\"convert to delta with partitioning schema struct\") {\n    withTempDir { dir =>\n      spark.range(100)\n        .select(col(\"id\") % 10 as \"part\", col(\"id\") as \"value\")\n        .write\n        .partitionBy(\"part\")\n        .format(\"parquet\")\n        .mode(\"overwrite\")\n        .save(dir.getAbsolutePath)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setConvertToDelta(\n            proto.ConvertToDelta\n              .newBuilder()\n              .setIdentifier(s\"parquet.`${dir.getAbsolutePath}`\")\n              .setPartitionSchemaStruct(\n                spark_proto.DataType\n                  .newBuilder()\n                  .setStruct(\n                    spark_proto.DataType.Struct\n                      .newBuilder()\n                      .addFields(\n                        spark_proto.DataType.StructField\n                          .newBuilder()\n                          .setName(\"part\")\n                          .setNullable(false)\n                          .setDataType(\n                            spark_proto.DataType\n                              .newBuilder()\n                              .setLong(spark_proto.DataType.Long.newBuilder())\n                          )\n                      )\n                  )\n              )\n          )\n      )\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n\n      assert(result.length === 1)\n      val deltaTable = DeltaTable.forName(spark, result.head.getString(0))\n      assert(!deltaTable.toDF.isEmpty)\n    }\n  }\n\n  test(\"history\") {\n    withTable(\"table\") {\n      DeltaTable.create(spark).tableName(identifier = \"table\").execute()\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setDescribeHistory(\n            proto.DescribeHistory.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(\"table\"))\n          )\n      )\n\n      val result = transform(input)\n      result match {\n        case lr: LocalRelation if lr.schema == ExpressionEncoder[DeltaHistory]().schema =>\n        case other => fail(s\"Unexpected plan: $other\")\n      }\n    }\n  }\n\n  test(\"detail\") {\n    withTable(\"table\") {\n      DeltaTable.create(spark).tableName(identifier = \"table\").execute()\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setDescribeDetail(\n            proto.DescribeDetail.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(\"table\"))\n          )\n      )\n\n      val result = transform(input)\n      val expected = DeltaTable.forName(spark, \"table\").detail().queryExecution.analyzed\n\n      assert(result.isInstanceOf[DescribeDeltaDetailCommand])\n      val childResult = result.asInstanceOf[DescribeDeltaDetailCommand].child\n      val childExpected = expected.asInstanceOf[DescribeDeltaDetailCommand].child\n\n      assert(childResult.asInstanceOf[ResolvedTable].identifier.name ===\n        childExpected.asInstanceOf[ResolvedTable].identifier.name)\n    }\n  }\n\n  private val expectedRestoreOutputColumns = Seq(\n    \"table_size_after_restore\",\n    \"num_of_files_after_restore\",\n    \"num_removed_files\",\n    \"num_restored_files\",\n    \"removed_files_size\",\n    \"restored_files_size\"\n  )\n\n  test(\"restore to version number\") {\n    withTable(\"table\") {\n      spark.range(start = 0, end = 1000, step = 1, numPartitions = 1)\n        .write.format(\"delta\").saveAsTable(\"table\")\n      spark.range(start = 0, end = 2000, step = 1, numPartitions = 2)\n        .write.format(\"delta\").mode(\"append\").saveAsTable(\"table\")\n\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setRestoreTable(\n            proto.RestoreTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(\"table\"))\n              .setVersion(0L)\n          )\n      )\n\n      val plan = transform(input)\n      assert(plan.columns.toSeq == expectedRestoreOutputColumns)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(2) === 2) // Two files should have been removed.\n      assert(spark.read.table(\"table\").count() === 1000)\n    }\n  }\n\n  test(\"restore to timestamp\") {\n    withTempDir { dir =>\n      spark.range(start = 0, end = 1000, step = 1, numPartitions = 1)\n        .write.format(\"delta\").save(dir.getAbsolutePath)\n      spark.range(start = 0, end = 2000, step = 1, numPartitions = 2)\n        .write.format(\"delta\").mode(\"append\").save(dir.getAbsolutePath)\n\n      val deltaLog = DeltaLog.forTable(spark, dir)\n      val input = createSparkRelation(\n        proto.DeltaRelation\n          .newBuilder()\n          .setRestoreTable(\n            proto.RestoreTable.newBuilder()\n              .setTable(\n                proto.DeltaTable.newBuilder().setPath(\n                  proto.DeltaTable.Path.newBuilder().setPath(dir.getAbsolutePath)\n                )\n              )\n              .setTimestamp(getTimestampForVersion(deltaLog, version = 0))\n          )\n      )\n\n      val plan = transform(input)\n      assert(plan.columns.toSeq === expectedRestoreOutputColumns)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(2) === 2) // Two files should have been removed.\n      assert(spark.read.format(\"delta\").load(dir.getAbsolutePath).count() === 1000)\n    }\n  }\n\n  test(\"isDeltaTable - delta\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      spark.range(end = 1000).write.format(\"delta\").mode(\"overwrite\").save(path)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setIsDeltaTable(proto.IsDeltaTable.newBuilder().setPath(path))\n      )\n\n      val plan = transform(input)\n      assert(plan.schema.length === 1)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getBoolean(0))\n    }\n  }\n\n  test(\"isDeltaTable - parquet\") {\n    withTempDir { dir =>\n      val path = dir.getAbsolutePath\n      spark.range(end = 1000).write.format(\"parquet\").mode(\"overwrite\").save(path)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setIsDeltaTable(proto.IsDeltaTable.newBuilder().setPath(path))\n      )\n\n      val plan = transform(input)\n      assert(plan.schema.length === 1)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(!result.head.getBoolean(0))\n    }\n  }\n\n  test(\"delete without condition\") {\n    val tableName = \"table\"\n    withTable(tableName) {\n      spark.range(end = 1000).write.format(\"delta\").saveAsTable(tableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setDeleteFromTable(\n            proto.DeleteFromTable.newBuilder()\n              .setTarget(createScan(tableName))\n          )\n      )\n\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(0) === 1000)\n      assert(spark.read.table(tableName).isEmpty)\n    }\n  }\n\n  test(\"delete with condition\") {\n    val tableName = \"table\"\n    withTable(tableName) {\n      spark.range(end = 1000).write.format(\"delta\").saveAsTable(tableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setDeleteFromTable(\n            proto.DeleteFromTable.newBuilder()\n              .setTarget(createScan(tableName))\n              .setCondition(createExpression(\"id % 2 = 0\"))\n          )\n      )\n\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(0) === 500)\n      assert(spark.read.table(tableName).count() === 500)\n    }\n  }\n\n  test(\"update without condition\") {\n    val tableName = \"target\"\n    withTable(tableName) {\n      spark.range(end = 1000).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setUpdateTable(\n            proto.UpdateTable.newBuilder()\n              .setTarget(createScan(tableName))\n              .addAssignments(createAssignment(field = \"value\", value = \"value + 1\"))\n          )\n      )\n\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(0) === 1000)\n      checkAnswer(\n        spark.read.table(tableName),\n        Seq.tabulate(1000)(i => Row(i, i + 1))\n      )\n    }\n  }\n\n  test(\"update with condition\") {\n    val tableName = \"target\"\n    withTable(tableName) {\n      spark.range(end = 1000).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setUpdateTable(\n            proto.UpdateTable.newBuilder()\n              .setTarget(createScan(tableName))\n              .setCondition(createExpression(\"key % 2 = 0\"))\n              .addAssignments(createAssignment(field = \"value\", value = \"value + 1\"))\n          )\n      )\n\n      val plan = transform(input)\n      val result = DataFrameUtils.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(0) === 500)\n      checkAnswer(\n        spark.read.table(tableName),\n        Seq.tabulate(1000)(i => Row(i, if (i % 2 == 0) i + 1 else i))\n      )\n    }\n  }\n\n    test(\"merge - insert only\") {\n    val targetTableName = \"target\"\n    val sourceTableName = \"source\"\n    withTable(targetTableName, sourceTableName) {\n      spark.range(end = 100).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(targetTableName)\n\n      spark.range(end = 100)\n        .select(col(\"id\") + 50 as \"id\")\n        .select(col(\"id\") as \"key\", col(\"id\") + 1000 as \"value\")\n        .write.format(\"delta\").saveAsTable(sourceTableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setMergeIntoTable(\n            proto.MergeIntoTable.newBuilder()\n              .setTarget(createSubqueryAlias(createScan(targetTableName), alias = \"t\"))\n              .setSource(createSubqueryAlias(createScan(sourceTableName), alias = \"s\"))\n              .setCondition(createExpression(\"t.key = s.key\"))\n              .addNotMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setInsertAction(\n                    proto.MergeIntoTable.Action.InsertAction.newBuilder()\n                      .addAssignments(createAssignment(field = \"t.key\", value = \"s.key\"))\n                      .addAssignments(createAssignment(field = \"t.value\", value = \"s.value\"))\n                  )\n              )\n          )\n      )\n\n      val plan = transform(input)\n      val result = Dataset.ofRows(spark, plan).collect()\n      assert(result.length == 1)\n      assert(result.head.getLong(0) == 50) // num_affected_rows\n      assert(result.head.getLong(1) == 0) // num_updated_rows\n      assert(result.head.getLong(2) == 0) // num_deleted_rows\n      assert(result.head.getLong(3) == 50) // num_inserted_rows\n\n      checkAnswer(\n        spark.read.table(targetTableName),\n        Seq.tabulate(100)(i => Row(i, i)) ++ Seq.tabulate(50)(i => Row(i + 100, i + 1100))\n      )\n    }\n  }\n\n  test(\"merge - update only\") {\n    val targetTableName = \"target\"\n    val sourceTableName = \"source\"\n    withTable(targetTableName, sourceTableName) {\n      spark.range(end = 100).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(targetTableName)\n\n      spark.range(end = 100)\n        .select(col(\"id\") + 50 as \"id\")\n        .select(col(\"id\") as \"key\", col(\"id\") + 1000 as \"value\")\n        .write.format(\"delta\").saveAsTable(sourceTableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setMergeIntoTable(\n            proto.MergeIntoTable.newBuilder()\n              .setTarget(createSubqueryAlias(createScan(targetTableName), alias = \"t\"))\n              .setSource(createSubqueryAlias(createScan(sourceTableName), alias = \"s\"))\n              .setCondition(createExpression(\"t.key = s.key\"))\n              .addMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setUpdateAction(\n                    proto.MergeIntoTable.Action.UpdateAction.newBuilder()\n                      .addAssignments(createAssignment(field = \"t.key\", value = \"s.key\"))\n                      .addAssignments(createAssignment(field = \"t.value\", value = \"s.value\"))\n                  )\n              )\n          )\n      )\n\n      val plan = transform(input)\n      val result = Dataset.ofRows(spark, plan).collect()\n      assert(result.length == 1)\n      assert(result.head.getLong(0) == 50) // num_affected_rows\n      assert(result.head.getLong(1) == 50) // num_updated_rows\n      assert(result.head.getLong(2) == 0) // num_deleted_rows\n      assert(result.head.getLong(3) == 0) // num_inserted_rows\n\n      checkAnswer(\n        spark.read.table(targetTableName),\n        Seq.tabulate(50)(i => Row(i, i)) ++ Seq.tabulate(50)(i => Row(i + 50, i + 1050))\n      )\n    }\n  }\n\n  test(\"merge - mixed\") {\n    val targetTableName = \"target\"\n    val sourceTableName = \"source\"\n    withTable(targetTableName, sourceTableName) {\n      spark.range(end = 100).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(targetTableName)\n\n      spark.range(end = 100)\n        .select(col(\"id\") + 50 as \"id\")\n        .select(col(\"id\") as \"key\", col(\"id\") + 1000 as \"value\")\n        .write.format(\"delta\").saveAsTable(sourceTableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setMergeIntoTable(\n            proto.MergeIntoTable.newBuilder()\n              .setTarget(createSubqueryAlias(createScan(targetTableName), alias = \"t\"))\n              .setSource(createSubqueryAlias(createScan(sourceTableName), alias = \"s\"))\n              .setCondition(createExpression(\"t.key = s.key\"))\n              .addMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setUpdateStarAction(proto.MergeIntoTable.Action.UpdateStarAction.newBuilder())\n              )\n              .addNotMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setInsertStarAction(proto.MergeIntoTable.Action.InsertStarAction.newBuilder())\n              )\n              .addNotMatchedBySourceActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setCondition(createExpression(\"t.value < 25\"))\n                  .setDeleteAction(proto.MergeIntoTable.Action.DeleteAction.newBuilder())\n              )\n          )\n      )\n\n      val plan = transform(input)\n      val result = Dataset.ofRows(spark, plan).collect()\n      assert(result.length == 1)\n      assert(result.head.getLong(0) == 125) // num_affected_rows\n      assert(result.head.getLong(1) == 50) // num_updated_rows\n      assert(result.head.getLong(2) == 25) // num_deleted_rows\n      assert(result.head.getLong(3) == 50) // num_inserted_rows\n\n      checkAnswer(\n        spark.read.table(targetTableName),\n        Seq.tabulate(25)(i => Row(25 + i, 25 + i)) ++\n          Seq.tabulate(50)(i => Row(i + 50, i + 1050)) ++\n          Seq.tabulate(50)(i => Row(i + 100, i + 1100))\n      )\n    }\n  }\n\n  test(\"merge - withSchemaEvolution\") {\n    val targetTableName = \"target\"\n    val sourceTableName = \"source\"\n    withTable(targetTableName, sourceTableName) {\n      spark.range(end = 100).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(targetTableName)\n\n      spark.range(end = 100)\n        .select(col(\"id\") + 50 as \"id\")\n        .select(col(\"id\") as \"key\", col(\"id\") + 1000 as \"value\", col(\"id\") + 1 as \"extracol\")\n        .write.format(\"delta\").saveAsTable(sourceTableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setMergeIntoTable(\n            proto.MergeIntoTable.newBuilder()\n              .setTarget(createSubqueryAlias(createScan(targetTableName), alias = \"t\"))\n              .setSource(createSubqueryAlias(createScan(sourceTableName), alias = \"s\"))\n              .setCondition(createExpression(\"t.key = s.key\"))\n              .addMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setUpdateStarAction(proto.MergeIntoTable.Action.UpdateStarAction.newBuilder())\n              )\n              .addNotMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setInsertStarAction(proto.MergeIntoTable.Action.InsertStarAction.newBuilder())\n              )\n              .addNotMatchedBySourceActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setCondition(createExpression(\"t.value < 25\"))\n                  .setDeleteAction(proto.MergeIntoTable.Action.DeleteAction.newBuilder())\n              )\n              .setWithSchemaEvolution(true)\n          )\n      )\n\n\n      val plan = transform(input)\n      val result = Dataset.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(0) === 125) // num_affected_rows\n      assert(result.head.getLong(1) === 50) // num_updated_rows\n      assert(result.head.getLong(2) === 25) // num_deleted_rows\n      assert(result.head.getLong(3) === 50) // num_inserted_rows\n\n      checkAnswer(\n        spark.read.table(targetTableName),\n        Seq.tabulate(25)(i => Row(25 + i, 25 + i, null)) ++\n          Seq.tabulate(50)(i => Row(i + 50, i + 1050, i + 51)) ++\n          Seq.tabulate(50)(i => Row(i + 100, i + 1100, i + 101))\n      )\n    }\n  }\n\n  test(\"merge with no withSchemaEvolution while the source's schema \" +\n      \"is different than the target's schema\") {\n    val targetTableName = \"target\"\n    val sourceTableName = \"source\"\n    withTable(targetTableName, sourceTableName) {\n      spark.range(end = 100).select(col(\"id\") as \"key\", col(\"id\") as \"value\")\n        .write.format(\"delta\").saveAsTable(targetTableName)\n\n      spark.range(end = 100)\n        .select(col(\"id\") + 50 as \"id\")\n        .select(col(\"id\") as \"key\", col(\"id\") + 1000 as \"value\", col(\"id\") + 1 as \"extracol\")\n        .write.format(\"delta\").saveAsTable(sourceTableName)\n\n      val input = createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setMergeIntoTable(\n            proto.MergeIntoTable.newBuilder()\n              .setTarget(createSubqueryAlias(createScan(targetTableName), alias = \"t\"))\n              .setSource(createSubqueryAlias(createScan(sourceTableName), alias = \"s\"))\n              .setCondition(createExpression(\"t.key = s.key\"))\n              .addMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setUpdateStarAction(proto.MergeIntoTable.Action.UpdateStarAction.newBuilder())\n              )\n              .addNotMatchedActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setInsertStarAction(proto.MergeIntoTable.Action.InsertStarAction.newBuilder())\n              )\n              .addNotMatchedBySourceActions(\n                proto.MergeIntoTable.Action.newBuilder()\n                  .setCondition(createExpression(\"t.value < 25\"))\n                  .setDeleteAction(proto.MergeIntoTable.Action.DeleteAction.newBuilder())\n              )\n          )\n      )\n\n      val plan = transform(input)\n      val result = Dataset.ofRows(spark, plan).collect()\n      assert(result.length === 1)\n      assert(result.head.getLong(0) === 125) // num_affected_rows\n      assert(result.head.getLong(1) === 50) // num_updated_rows\n      assert(result.head.getLong(2) === 25) // num_deleted_rows\n      assert(result.head.getLong(3) === 50) // num_inserted_rows\n\n      checkAnswer(\n        spark.read.table(targetTableName),\n        Seq.tabulate(25)(i => Row(25 + i, 25 + i)) ++\n          Seq.tabulate(50)(i => Row(i + 50, i + 1050)) ++\n          Seq.tabulate(50)(i => Row(i + 100, i + 1100))\n      )\n    }\n  }\n\n  test(\"clone - shallow\") {\n    withTempDir { targetDir =>\n      val sourceTableName = \"source_table\"\n      withTable(sourceTableName) {\n        spark.range(end = 1000).write.format(\"delta\").saveAsTable(sourceTableName)\n\n        transform(createSparkCommand(\n          proto.DeltaCommand.newBuilder()\n            .setCloneTable(\n              proto.CloneTable.newBuilder()\n                .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(sourceTableName))\n                .setTarget(targetDir.getAbsolutePath)\n                .setIsShallow(true)\n                .setReplace(true)\n            )\n        ))\n\n        // Check that we have successfully cloned the table.\n        checkAnswer(\n          spark.read.format(\"delta\").load(targetDir.getAbsolutePath),\n          spark.read.table(sourceTableName))\n\n        // Check that a shallow clone was performed.\n        val deltaLog = DeltaLog.forTable(spark, targetDir)\n        deltaLog.update().allFiles.collect().foreach { f =>\n          assert(f.pathAsUri.isAbsolute)\n        }\n      }\n    }\n  }\n\n   test(\"optimize - compaction\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      spark.range(1000).select(col(\"id\") % 3 as \"key\", col(\"id\") as \"val\")\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      val plan = transform(createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setOptimizeTable(\n            proto.OptimizeTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n          )\n      ))\n      assert(plan.columns.toSeq == Seq(\"path\", \"metrics\"))\n      val df = Dataset.ofRows(spark, plan)\n\n      checkOptimizeMetrics(df)\n      checkOptimizeUsingHistory(tableName, expectedPredicates = Nil, expectedZorderCols = Nil)\n    }\n  }\n\n  test(\"optimize - compaction with partition filters\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      spark.range(1000).select(col(\"id\") % 3 as \"key\", col(\"id\") as \"val\")\n        .write.partitionBy(\"key\").format(\"delta\").saveAsTable(tableName)\n\n      val plan = transform(createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setOptimizeTable(\n            proto.OptimizeTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .addPartitionFilters(\"key = 1\")\n          )\n      ))\n      assert(plan.columns.toSeq == Seq(\"path\", \"metrics\"))\n      val df = Dataset.ofRows(spark, plan)\n\n      checkOptimizeMetrics(df)\n      checkOptimizeUsingHistory(\n        tableName, expectedPredicates = Seq(\"'key = 1\"), expectedZorderCols = Nil)\n    }\n  }\n\n  test(\"optimize - z-order\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      spark.range(1000).select(col(\"id\") % 3 as \"key\", col(\"id\") as \"val\")\n        .write.format(\"delta\").saveAsTable(tableName)\n\n      val plan = transform(createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setOptimizeTable(\n            proto.OptimizeTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .addZorderColumns(\"val\")\n          )\n      ))\n      assert(plan.columns.toSeq == Seq(\"path\", \"metrics\"))\n      val df = Dataset.ofRows(spark, plan)\n\n      checkOptimizeMetrics(df)\n      checkOptimizeUsingHistory(\n        tableName, expectedPredicates = Nil, expectedZorderCols = Seq(\"val\"))\n    }\n  }\n\n  test(\"optimize - z-order with partition filters\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      spark.range(1000).select(col(\"id\") % 3 as \"key\", col(\"id\") as \"val\")\n        .write.partitionBy(\"key\").format(\"delta\").saveAsTable(tableName)\n\n      val plan = transform(createSparkRelation(\n        proto.DeltaRelation.newBuilder()\n          .setOptimizeTable(\n            proto.OptimizeTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .addPartitionFilters(\"key = 1\")\n              .addZorderColumns(\"val\")\n          )\n      ))\n      assert(plan.columns.toSeq == Seq(\"path\", \"metrics\"))\n      val df = Dataset.ofRows(spark, plan)\n\n      checkOptimizeMetrics(df)\n      checkOptimizeUsingHistory(\n        tableName, expectedPredicates = Seq(\"'key = 1\"), expectedZorderCols = Seq(\"val\"))\n    }\n  }\n\n  private def checkOptimizeMetrics(df: DataFrame): Unit = {\n    import testImplicits._\n    val result = df.as[(String, OptimizeMetrics)].collect()\n    assert(result.length == 1)\n    val (_, metrics) = result.head\n    assert(metrics.numFilesRemoved > metrics.numFilesAdded)\n  }\n\n  private def checkOptimizeUsingHistory(\n      tableName: String, expectedPredicates: Seq[String], expectedZorderCols: Seq[String]): Unit = {\n    import testImplicits._\n    val (operation, operationParameters) = DeltaTable.forName(spark, tableName).history()\n      .select(\"operation\", \"operationParameters\").as[(String, Map[String, String])].head()\n    assert(operation == \"OPTIMIZE\")\n    assert(operationParameters(\"predicate\") ==\n      expectedPredicates.map(p => s\"\"\"\\\"($p)\\\"\"\"\").mkString(start = \"[\", sep = \",\", end = \"]\"))\n    assert(operationParameters(\"zOrderBy\") ==\n      expectedZorderCols.map(c => s\"\"\"\\\"$c\\\"\"\"\").mkString(start = \"[\", sep = \",\", end = \"]\"))\n  }\n\n  test(\"vacuum - without retention hours argument\") {\n    val tableName = \"test_table\"\n\n    def runVacuum(): Unit = {\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setVacuumTable(\n            proto.VacuumTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n          )\n      ))\n    }\n\n    def setRetentionInterval(retentionInterval: String): Unit = {\n      spark.sql(\n        s\"\"\"ALTER TABLE $tableName\n           |SET TBLPROPERTIES (\n           |  '${DeltaConfigs.TOMBSTONE_RETENTION.key}' = '$retentionInterval',\n           |  '${DeltaConfigs.LOG_RETENTION.key}' = '$retentionInterval'\n           |)\"\"\".stripMargin\n      )\n    }\n\n    withTable(tableName) {\n      // Set up a Delta table with an untracked file.\n      spark.range(1000).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val tempFile =\n        new File(DeltaFileOperations.absolutePath(deltaLog.dataPath.toString, \"abc.parquet\").toUri)\n      tempFile.createNewFile()\n\n      // Run a vacuum with the untracked file still within the retention period.\n      setRetentionInterval(retentionInterval = \"1000 hours\")\n      runVacuum()\n      assert(tempFile.exists())\n\n      // Run a vacuum with the untracked file outside of the retention period.\n      setRetentionInterval(retentionInterval = \"0 hours\")\n      runVacuum()\n      assert(!tempFile.exists())\n    }\n  }\n\n  test(\"vacuum - with retention hours argument\") {\n    val tableName = \"test_table\"\n\n    def runVacuum(retentionHours: Float): Unit = {\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setVacuumTable(\n            proto.VacuumTable.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .setRetentionHours(retentionHours)\n          )\n      ))\n    }\n\n    withTable(tableName) {\n      // Set up a Delta table with an untracked file.\n      spark.range(1000).write.format(\"delta\").saveAsTable(tableName)\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val tempFile =\n        new File(DeltaFileOperations.absolutePath(deltaLog.dataPath.toString, \"abc.parquet\").toUri)\n      tempFile.createNewFile()\n\n      // Run a vacuum with the untracked file still within the retention period.\n      runVacuum(retentionHours = 1000.0f)\n      assert(tempFile.exists())\n\n      // Run a vacuum with the untracked file outside of the retention period.\n      withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n        runVacuum(retentionHours = 0.0f)\n      }\n      assert(!tempFile.exists())\n    }\n  }\n\n  test(\"upgrade table protocol\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      // Create a Delta table with protocol version (1, 1).\n      val oldReaderVersion = 1\n      val oldWriterVersion = 1\n      spark.range(1000)\n        .write\n        .format(\"delta\")\n        .option(DeltaConfigs.MIN_READER_VERSION.key, oldReaderVersion)\n        .option(DeltaConfigs.MIN_WRITER_VERSION.key, oldWriterVersion)\n        .saveAsTable(tableName)\n\n      // Check that protocol version is as expected, before we upgrade it.\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val oldProtocol = deltaLog.update().protocol\n      assert(oldProtocol.minReaderVersion === oldReaderVersion)\n      assert(oldProtocol.minWriterVersion === oldWriterVersion)\n\n      // Use Delta Connect to upgrade the protocol of the table.\n      val newReaderVersion = 2\n      val newWriterVersion = 5\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setUpgradeTableProtocol(\n            proto.UpgradeTableProtocol.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .setReaderVersion(newReaderVersion)\n              .setWriterVersion(newWriterVersion)\n          )\n      ))\n\n      // Check that protocol version is as expected, after we have upgraded it.\n      val newProtocol = deltaLog.update().protocol\n      assert(newProtocol.minReaderVersion === newReaderVersion)\n      assert(newProtocol.minWriterVersion === newWriterVersion)\n    }\n  }\n\n  test(\"add table feature support\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      // Create a Delta table with protocol version (1, 1).\n      val oldReaderVersion = 1\n      val oldWriterVersion = 1\n      spark.range(1000)\n        .write\n        .format(\"delta\")\n        .option(DeltaConfigs.MIN_READER_VERSION.key, oldReaderVersion)\n        .option(DeltaConfigs.MIN_WRITER_VERSION.key, oldWriterVersion)\n        .saveAsTable(tableName)\n\n      // Check that protocol version is as expected, before we add the TestReaderWriterFeature.\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      val oldProtocol = deltaLog.update().protocol\n      assert(oldProtocol.minReaderVersion === oldReaderVersion)\n      assert(oldProtocol.minWriterVersion === oldWriterVersion)\n\n      // Use Delta Connect to add table feature.\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setAddFeatureSupport(\n            proto.AddFeatureSupport.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .setFeatureName(TestReaderWriterFeature.name)\n          )\n      ))\n\n      // Check that protocol version is as expected, after we added the\n      // TestReaderWriterFeature.\n      val newProtocol = deltaLog.update().protocol\n      assert(newProtocol.minReaderVersion === TestReaderWriterFeature.minReaderVersion)\n      assert(newProtocol.minWriterVersion === TestReaderWriterFeature.minWriterVersion)\n    }\n  }\n\n  test(\"drop table feature support\") {\n    val tableName = \"test_table\"\n    withTable(tableName) {\n      spark.range(1000)\n        .write\n        .format(\"delta\")\n        .option(DeltaConfigs.MIN_READER_VERSION.key, 1)\n        .option(DeltaConfigs.MIN_WRITER_VERSION.key, 1)\n        .saveAsTable(tableName)\n\n    sql(\n      s\"\"\"ALTER TABLE $tableName SET TBLPROPERTIES (\n         |delta.feature.${TestRemovableWriterFeature.name} = 'supported'\n         |)\"\"\".stripMargin)\n\n      // Check that protocol version is as expected.\n      val deltaLog = DeltaLog.forTable(spark, TableIdentifier(tableName))\n      assert(deltaLog.update().protocol === Protocol(\n        minReaderVersion = 1,\n        minWriterVersion = 7,\n        readerFeatures = None,\n        writerFeatures = Some(Set(TestRemovableWriterFeature.name))))\n\n      // Attempt truncating the history when dropping a feature that is not required.\n      // This verifies the truncateHistory option was correctly passed.\n      assert(intercept[DeltaTableFeatureException] {\n        transform(createSparkCommand(\n          proto.DeltaCommand.newBuilder()\n            .setDropFeatureSupport(\n              proto.DropFeatureSupport.newBuilder()\n                .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n                .setFeatureName(TestRemovableWriterFeature.name)\n                .setTruncateHistory(true)\n            )\n        ))\n      }.getErrorClass === \"DELTA_FEATURE_DROP_HISTORY_TRUNCATION_NOT_ALLOWED\")\n\n      // Use Delta Connect to drop table feature.\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setDropFeatureSupport(\n            proto.DropFeatureSupport.newBuilder()\n              .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n              .setFeatureName(TestRemovableWriterFeature.name)\n          )\n      ))\n\n      assert(deltaLog.update().protocol === Protocol(1, 1))\n    }\n  }\n  \n  test(\"generate manifest\") {\n    withTempDir { dir =>\n      spark.range(1000).write.format(\"delta\").mode(\"overwrite\").save(dir.getAbsolutePath)\n\n      val manifestFile = new File(dir, GenerateSymlinkManifest.MANIFEST_LOCATION)\n      assert(!manifestFile.exists())\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setGenerate(\n            proto.Generate.newBuilder()\n              .setTable(\n                proto.DeltaTable.newBuilder()\n                  .setPath(proto.DeltaTable.Path.newBuilder().setPath(dir.getAbsolutePath)))\n              .setMode(\"symlink_format_manifest\"))))\n\n      assert(manifestFile.exists())\n\n    }\n  }\n\n  test(\"create delta table - basic\") {\n    withTempDir { dir =>\n      val tableName = \"test_table\"\n      withTable(tableName) {\n        val tableComment = \"table comment\"\n        val nameColA = \"colA\"\n        val generatedAlwaysAsColA = \"1\"\n        val nameColB = \"colB\"\n        val commentColB = \"colB comment\"\n        val nameColC = \"colC\"\n        val tableProperties = Map(\"k1\" -> \"v1\", \"k2\" -> \"v2\")\n        transform(createSparkCommand(\n          proto.DeltaCommand.newBuilder()\n            .setCreateDeltaTable(\n              proto.CreateDeltaTable.newBuilder()\n                .setMode(proto.CreateDeltaTable.Mode.MODE_CREATE)\n                .setTableName(tableName)\n                .setLocation(dir.getAbsolutePath)\n                .setComment(tableComment)\n                .addColumns(\n                  proto.CreateDeltaTable.Column.newBuilder()\n                    .setName(nameColA)\n                    .setDataType(UNPARSED_INT_DATA_TYPE)\n                    .setNullable(true)\n                    .setGeneratedAlwaysAs(generatedAlwaysAsColA))\n                .addColumns(\n                  proto.CreateDeltaTable.Column.newBuilder()\n                    .setName(nameColB)\n                    .setDataType(LONG_DATA_TYPE)\n                    .setNullable(false)\n                    .setComment(commentColB))\n                .addColumns(\n                  proto.CreateDeltaTable.Column.newBuilder()\n                    .setName(nameColC)\n                    .setDataType(LONG_DATA_TYPE)\n                    .setNullable(false)\n                    .setIdentityInfo(\n                        proto.CreateDeltaTable.Column.IdentityInfo.newBuilder()\n                          .setStart(100)\n                          .setStep(10)\n                          .setAllowExplicitInsert(true)\n                    )\n                )\n                .putAllProperties(tableProperties.asJava))))\n\n        val table = spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))\n        val metadata = DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot.metadata\n        \n        assert(table.comment.contains(tableComment))\n        assert(table.location.getPath == dir.getAbsolutePath)\n        assert(metadata.configuration == tableProperties)\n\n        val colAMetadata = new MetadataBuilder()\n          .putString(GENERATION_EXPRESSION_METADATA_KEY, generatedAlwaysAsColA)\n          .build()\n        val colBMetadata = new MetadataBuilder().putString(\"comment\", commentColB).build()\n        val colCMetadata = new MetadataBuilder()\n          .putLong(DeltaSourceUtils.IDENTITY_INFO_START, 100)\n          .putLong(DeltaSourceUtils.IDENTITY_INFO_STEP, 10)\n          .putBoolean(DeltaSourceUtils.IDENTITY_INFO_ALLOW_EXPLICIT_INSERT, true)\n          .build()\n        val expectedSchema = StructType(Seq(\n          StructField(nameColA, IntegerType, nullable = true, colAMetadata),\n          StructField(nameColB, LongType, nullable = false, colBMetadata),\n          StructField(nameColC, LongType, nullable = false, colCMetadata)))\n        \n        assert(metadata.schema == expectedSchema)\n      }\n    }\n  }\n\n  test(\"create delta table - modes\") {\n    val tableName = \"test_table\"\n    def createTable(mode: proto.CreateDeltaTable.Mode, colName: String): Unit = {\n      transform(createSparkCommand(\n        proto.DeltaCommand.newBuilder()\n          .setCreateDeltaTable(\n            proto.CreateDeltaTable.newBuilder()\n              .setMode(mode)\n              .setTableName(tableName)\n              .addColumns(\n                proto.CreateDeltaTable.Column.newBuilder()\n                  .setName(colName)\n                  .setDataType(LONG_DATA_TYPE)\n                  .setNullable(true)))))\n    }\n\n    def getColumnName(): String = {\n      DeltaLog.forTable(spark, TableIdentifier(tableName)).snapshot.metadata.schema.head.name\n    }\n\n    val expectedQualifiedTableName = s\"`default`.`$tableName`\"\n\n    withTable(tableName) {\n      val replaceError = intercept[AnalysisException] {\n        createTable(proto.CreateDeltaTable.Mode.MODE_REPLACE, colName = \"column1\")\n      }\n      checkError(\n        replaceError,\n        condition = \"TABLE_OR_VIEW_NOT_FOUND\",\n        parameters = Map(\"relationName\" -> expectedQualifiedTableName))\n\n      createTable(proto.CreateDeltaTable.Mode.MODE_CREATE, colName = \"column2\")\n      assert(getColumnName() == \"column2\")\n\n      val createError = intercept[AnalysisException] {\n        createTable(proto.CreateDeltaTable.Mode.MODE_CREATE, colName = \"column3\")\n      }\n      checkError(\n        createError,\n        condition = \"TABLE_OR_VIEW_ALREADY_EXISTS\",\n        parameters = Map(\"relationName\" -> s\"`default`.`$tableName`\"))\n\n      createTable(proto.CreateDeltaTable.Mode.MODE_REPLACE, colName = \"column4\")\n      assert(getColumnName() == \"column4\")\n\n      createTable(proto.CreateDeltaTable.Mode.MODE_CREATE_IF_NOT_EXISTS, colName = \"column5\")\n      assert(getColumnName() == \"column4\")\n\n      spark.sql(s\"DROP TABLE $tableName\")\n      createTable(proto.CreateDeltaTable.Mode.MODE_CREATE_IF_NOT_EXISTS, colName = \"column6\")\n      assert(getColumnName() == \"column6\")\n\n      createTable(proto.CreateDeltaTable.Mode.MODE_CREATE_OR_REPLACE, colName = \"column7\")\n      assert(getColumnName() == \"column7\")\n\n      spark.sql(s\"DROP TABLE $tableName\")\n      createTable(proto.CreateDeltaTable.Mode.MODE_CREATE_OR_REPLACE, colName = \"column8\")\n      assert(getColumnName() == \"column8\")\n    }\n  }  \n\n  private def getTimestampForVersion(log: DeltaLog, version: Long): String = {\n    val file = new File(FileNames.unsafeDeltaFile(log.logPath, version).toUri)\n    val sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\")\n    sdf.format(file.lastModified())\n  }\n}\n\nobject DeltaConnectPlannerSuite {\n  val UNPARSED_INT_DATA_TYPE: spark_proto.DataType = spark_proto.DataType\n    .newBuilder()\n    .setUnparsed(spark_proto.DataType.Unparsed.newBuilder().setDataTypeString(\"int\"))\n    .build()\n\n  val LONG_DATA_TYPE: spark_proto.DataType = spark_proto.DataType\n    .newBuilder()\n    .setLong(spark_proto.DataType.Long.newBuilder())\n    .build()\n\n  private def createSparkCommand(command: proto.DeltaCommand.Builder): spark_proto.Command = {\n    spark_proto.Command.newBuilder().setExtension(protobuf.Any.pack(command.build())).build()\n  }\n\n  private def createSparkRelation(relation: proto.DeltaRelation.Builder): spark_proto.Relation = {\n    spark_proto.Relation.newBuilder().setExtension(protobuf.Any.pack(relation.build())).build()\n  }\n\n  private def createScan(tableName: String): spark_proto.Relation = {\n    createSparkRelation(\n      proto.DeltaRelation.newBuilder()\n        .setScan(\n          proto.Scan.newBuilder()\n            .setTable(proto.DeltaTable.newBuilder().setTableOrViewName(tableName))\n        )\n    )\n  }\n\n  def createSubqueryAlias(\n      input: spark_proto.Relation, alias: String): spark_proto.Relation = {\n    spark_proto.Relation.newBuilder()\n      .setSubqueryAlias(\n        spark_proto.SubqueryAlias.newBuilder()\n          .setAlias(alias)\n          .setInput(input)\n      )\n      .build()\n  }\n\n  private def createExpression(expr: String): spark_proto.Expression = {\n    spark_proto.Expression.newBuilder()\n      .setExpressionString(\n        spark_proto.Expression.ExpressionString.newBuilder()\n          .setExpression(expr)\n      )\n      .build()\n  }\n\n  private def createAssignment(field: String, value: String): proto.Assignment = {\n    proto.Assignment.newBuilder()\n      .setField(createExpression(field))\n      .setValue(createExpression(value))\n      .build()\n  }\n\n}\n"
  },
  {
    "path": "spark-unified/README.md",
    "content": "# Delta Spark Unified Module\n\nThis module contains the final, published `delta-spark` JAR that unifies both:\n- **V1 (hybrid DSv1 and DSv2)**: The traditional Delta Lake connector using `DeltaLog` for Delta support\n- **V2 (Pure DSv2)**: The new Kernel-backed connector.\n\n## Architecture\n\nThe unified module provides single entry points for both V1 and V2:\n- `DeltaCatalog`: Extends `AbstractDeltaCatalog` from the `spark` module\n- `DeltaSparkSessionExtension`: Extends `AbstractDeltaSparkSessionExtension` from the `spark` module\n\n## Module Structure\n\n```\nspark-unified/           (This module - final published artifact)\n  ├── src/main/java/\n  │   └── org.apache.spark.sql.delta.catalog.DeltaCatalog.java\n  └── src/main/scala/\n      └── io.delta.sql.DeltaSparkSessionExtension.scala\n\nspark/                   (sparkV1 - V1 implementation)\n  ├── Core Delta Lake classes with DeltaLog\n  ├── AbstractDeltaCatalog, AbstractDeltaSparkSessionExtension\n  └── v2/                (sparkV2 - V2 implementation)\n      └── Kernel-backed DSv2 connector\n```\n\n## How It Works\n\n1. **sparkV1** (`spark/`): Contains production code for the V1 connector using DeltaLog\n2. **sparkV1Filtered** (`spark-v1-shaded/`): Filtered version of V1 excluding DeltaLog, Snapshot, OptimisticTransaction, and actions.scala\n3. **sparkV2** (`spark/v2/`): Kernel-backed V2 connector that depends on sparkV1Filtered\n4. **spark** (this module): Final JAR that merges V1 + V2 classes\n\nThe final JAR includes:\n- All classes from sparkV1, sparkV2, and storage\n- Python files\n- No internal module dependencies in the published POM\n\n## Internal vs Published Modules\n\n**Internal modules** (not published to Maven):\n- `delta-spark-v1`\n- `delta-spark-v1-filtered`\n- `delta-spark-v2`\n\n**Published module**:\n- `delta-spark` (this module) - contains merged classes from all internal modules\n\n## Build\n\nThe module automatically:\n1. Merges classes from V1, V2, and storage modules\n2. Detects duplicate classes (fails build if found)\n3. Filters internal modules from POM dependencies\n4. Exports as JAR\n\n## Testing\n\nTests are located in `spark/src/test/` and run against the combined JAR to ensure V1+V2 integration works correctly.\n\n"
  },
  {
    "path": "spark-unified/src/main/java/org/apache/spark/sql/delta/catalog/DeltaCatalog.java",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.catalog;\n\nimport io.delta.spark.internal.v2.catalog.SparkTable;\nimport org.apache.spark.sql.delta.DeltaV2Mode;\nimport java.util.HashMap;\nimport java.util.function.Supplier;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.spark.sql.SparkSession;\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable;\nimport org.apache.spark.sql.connector.catalog.Identifier;\nimport org.apache.spark.sql.connector.catalog.Table;\n\n/**\n * A Spark catalog plugin for Delta Lake tables that implements the Spark DataSource V2 Catalog API.\n *\n * To use this catalog, configure it in your Spark session:\n * <pre>{@code\n * // Scala example\n * val spark = SparkSession\n *   .builder()\n *   .appName(\"...\")\n *   .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n *   .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n *   .getOrCreate()\n *\n *\n * // Python example\n * spark = SparkSession \\\n *   .builder \\\n *   .appName(\"...\") \\\n *   .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n *   .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n *   .getOrCreate()\n * }</pre>\n *\n * <h2>Architecture and Delegation Logic</h2>\n *\n * This class sits in the delta-spark (unified) module and provides a single entry point for\n * Delta Lake catalog operations.\n *\n * <p>The unified module can access both implementations:</p>\n * <ul>\n *   <li>V1 connector: {@link DeltaTableV2} - Legacy connector using DeltaLog, full read/write support</li>\n *   <li>V2 connector: {@link SparkTable} - sparkV2 connector, read-only support</li>\n * </ul>\n *\n * <p>See {@link DeltaV2Mode} for V1 vs V2 connector definitions and enable mode configuration.</p>\n */\npublic class DeltaCatalog extends AbstractDeltaCatalog {\n\n  /**\n   * Loads a Delta table that is registered in the catalog.\n   *\n   * <p>Routing logic based on {@link DeltaV2Mode}:\n   * <ul>\n   *   <li>STRICT: Returns sparkV2 {@link SparkTable} (V2 connector)</li>\n   *   <li>NONE (default): Returns {@link DeltaTableV2} (V1 connector)</li>\n   * </ul>\n   *\n   * @param ident The identifier of the table in the catalog.\n   * @param catalogTable The catalog table metadata containing table properties and location.\n   * @return Table instance (SparkTable for V2, DeltaTableV2 for V1).\n   */\n  @Override\n  public Table loadCatalogTable(Identifier ident, CatalogTable catalogTable) {\n    return loadTableInternal(\n        () -> new SparkTable(ident, catalogTable, new HashMap<>()),\n        () -> super.loadCatalogTable(ident, catalogTable));\n  }\n\n  /**\n   * Loads a Delta table directly from a path.\n   * This is used for path-based table access where the identifier name is the table path.\n   *\n   * <p>Routing logic based on {@link DeltaV2Mode}:\n   * <ul>\n   *   <li>STRICT: Returns sparkV2 {@link SparkTable} (V2 connector)</li>\n   *   <li>NONE (default): Returns {@link DeltaTableV2} (V1 connector)</li>\n   * </ul>\n   *\n   * @param ident The identifier whose name contains the path to the Delta table.\n   * @return Table instance (SparkTable for V2, DeltaTableV2 for V1).\n   */\n  @Override\n  public Table loadPathTable(Identifier ident) {\n    return loadTableInternal(\n        // delta.`/path/to/table`, where ident.name() is `/path/to/table`\n        () -> new SparkTable(ident, ident.name()),\n        () -> super.loadPathTable(ident));\n  }\n\n  /**\n   * Loads a table based on the {@link DeltaV2Mode} SQL configuration.\n   *\n   * <p>This method checks the configuration and delegates to the appropriate supplier:\n   * <ul>\n   *   <li>STRICT mode: Uses V2 connector (sparkV2 SparkTable) - for testing V2 capabilities</li>\n   *   <li>NONE mode (default): Uses V1 connector (DeltaTableV2) - production default with full features</li>\n   * </ul>\n   *\n   * <p>See {@link DeltaV2Mode} for detailed V1 vs V2 connector definitions.\n   *\n   * @param v2ConnectorSupplier Supplier for V2 connector (sparkV2 SparkTable) - used in STRICT mode\n   * @param v1ConnectorSupplier Supplier for V1 connector (DeltaTableV2) - used in NONE mode (default)\n   * @return Table instance from the selected supplier\n   */\n  private Table loadTableInternal(\n      Supplier<Table> v2ConnectorSupplier,\n      Supplier<Table> v1ConnectorSupplier) {\n    DeltaV2Mode connectorMode = new DeltaV2Mode(spark().sessionState().conf());\n    if (connectorMode.shouldCatalogReturnV2Tables()) {\n      return v2ConnectorSupplier.get();\n    } else {\n      return v1ConnectorSupplier.get();\n    }\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/main/scala/io/delta/internal/ApplyV2Streaming.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.internal\n\nimport scala.jdk.CollectionConverters._\nimport scala.jdk.OptionConverters._\n\nimport io.delta.spark.internal.v2.catalog.SparkTable\nimport io.delta.spark.internal.v2.utils.ScalaUtils\nimport org.apache.spark.sql.delta.DeltaV2Mode\nimport org.apache.spark.sql.delta.sources.DeltaSourceUtils\n\nimport org.apache.spark.sql.SparkSession\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.Rule\nimport org.apache.spark.sql.catalyst.streaming.StreamingRelationV2\nimport org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.delta.Relocated.StreamingRelation\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\n/**\n * Rule for applying the V2 streaming path by rewriting V1 StreamingRelation\n * with Delta DataSource to StreamingRelationV2 with SparkTable.\n *\n * This rule handles the case where Spark's FindDataSourceTable rule has converted\n * a StreamingRelationV2 (with DeltaTableV2) back to a StreamingRelation because\n * DeltaTableV2 doesn't advertise STREAMING_READ capability. We convert it back to\n * StreamingRelationV2 with SparkTable (from sparkV2) which does support streaming.\n *\n * See [[DeltaV2Mode]] for configuration behavior.\n *\n * @param session The Spark session for configuration access\n */\nclass ApplyV2Streaming(\n    @transient private val session: SparkSession)\n  extends Rule[LogicalPlan] {\n\n  private def isDeltaStreamingRelation(s: StreamingRelation): Boolean = {\n    // Check if this is a Delta streaming relation by examining:\n    // 1. The source name (e.g., \"delta\" from .format(\"delta\"))\n    // 2. The catalog table's provider (e.g., \"DELTA\" from Unity Catalog)\n    s.dataSource.catalogTable match {\n      case Some(catalogTable) =>\n        DeltaSourceUtils.isDeltaDataSourceName(s.sourceName) ||\n        catalogTable.provider.exists(DeltaSourceUtils.isDeltaDataSourceName)\n      case None => false\n    }\n  }\n\n  private def shouldApplyV2Streaming(s: StreamingRelation): Boolean = {\n    if (!isDeltaStreamingRelation(s)) {\n      return false\n    }\n\n    val deltaV2Mode = new DeltaV2Mode(session.sessionState.conf)\n    deltaV2Mode.isStreamingReadsEnabled(s.dataSource.catalogTable.toJava)\n  }\n\n  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {\n    case s: StreamingRelation if shouldApplyV2Streaming(s) =>\n      // catalogTable is guaranteed to be defined because shouldApplyV2Streaming checks it\n      // via isDeltaStreamingRelation.\n      val catalogTable = s.dataSource.catalogTable.get\n      val ident =\n        Identifier.of(catalogTable.identifier.database.toArray, catalogTable.identifier.table)\n      val table =\n        new SparkTable(\n          ident,\n          catalogTable,\n          // Use user-specified streaming options to override catalog storage properties.\n          // SparkTable handles merging catalogTable storage props internally.\n          ScalaUtils.toJavaMap(s.dataSource.options))\n      val catalog = catalogTable.identifier.catalog.map(\n        session.sessionState.catalogManager.catalog)\n\n\n      StreamingRelationV2(\n        // We rebuild this from the resolved V2 table, so there is no V1 source to carry through.\n        // This is only non-None when StreamingRelationV2 is created by wrapping a V1 streaming\n        // data source; in that case Spark keeps the underlying V1 DataSource instance here.\n        source = None,\n        sourceName = DeltaSourceUtils.NAME,\n        table = table,\n        extraOptions = new CaseInsensitiveStringMap(s.dataSource.options.asJava),\n        output = toAttributes(table.schema),\n        catalog = catalog,\n        identifier = Some(ident),\n        // Keep this None to force the V2 path; we don't want to fall back to V1 here.\n        v1Relation = None)\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/main/scala/io/delta/sql/DeltaSparkSessionExtension.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.sql\n\nimport io.delta.internal.ApplyV2Streaming\nimport org.apache.spark.sql.SparkSessionExtensions\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.rules.Rule\n\n/**\n * An extension for Spark SQL to activate Delta SQL parser to support Delta SQL grammar.\n *\n * Scala example to create a `SparkSession` with the Delta SQL parser:\n * {{{\n *    import org.apache.spark.sql.SparkSession\n *\n *    val spark = SparkSession\n *       .builder()\n *       .appName(\"...\")\n *       .master(\"...\")\n *       .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n *       .getOrCreate()\n * }}}\n *\n * Java example to create a `SparkSession` with the Delta SQL parser:\n * {{{\n *    import org.apache.spark.sql.SparkSession;\n *\n *    SparkSession spark = SparkSession\n *                 .builder()\n *                 .appName(\"...\")\n *                 .master(\"...\")\n *                 .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n *                 .getOrCreate();\n * }}}\n *\n * Python example to create a `SparkSession` with the Delta SQL parser (PySpark doesn't pick up the\n * SQL conf \"spark.sql.extensions\" in Apache Spark 2.4.x, hence we need to activate it manually in\n * 2.4.x. However, because `SparkSession` has been created and everything has been materialized, we\n * need to clone a new session to trigger the initialization. See SPARK-25003):\n * {{{\n *    from pyspark.sql import SparkSession\n *\n *    spark = SparkSession \\\n *        .builder \\\n *        .appName(\"...\") \\\n *        .master(\"...\") \\\n *        .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n *        .getOrCreate()\n *    if spark.sparkContext().version < \"3.\":\n *        spark.sparkContext()._jvm.io.delta.sql.DeltaSparkSessionExtension() \\\n *            .apply(spark._jsparkSession.extensions())\n *        spark = SparkSession(spark.sparkContext(), spark._jsparkSession.cloneSession())\n * }}}\n *\n * @since 0.4.0\n */\nclass DeltaSparkSessionExtension extends AbstractDeltaSparkSessionExtension {\n\n  override def apply(extensions: SparkSessionExtensions): Unit = {\n    // First register all the base Delta rules from the V1 implementation.\n    super.apply(extensions)\n\n    // Register a post-hoc resolution rule that rewrites V1 StreamingRelation plans that\n    // read catalog owned Delta tables into V2 StreamingRelationV2 plans backed by SparkTable.\n    //\n    // NOTE: This rule is functional (not a placeholder). Binary compatibility concerns are\n    // handled separately via the nested NoOpRule class below (kept for MiMa).\n    extensions.injectResolutionRule { session =>\n      new ApplyV2Streaming(session)\n    }\n  }\n\n  /**\n   * NoOpRule for binary compatibility with Delta 3.3.0\n   * This class must remain here to satisfy MiMa checks\n   */\n  class NoOpRule extends Rule[LogicalPlan] {\n    override def apply(plan: LogicalPlan): LogicalPlan = plan\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/io/delta/internal/ApplyV2StreamingSuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.internal\n\nimport java.net.URI\nimport java.util.{HashMap => JHashMap}\n\nimport scala.jdk.CollectionConverters._\n\nimport io.delta.spark.internal.v2.catalog.SparkTable\nimport io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient\nimport org.apache.spark.sql.catalyst.TableIdentifier\nimport org.apache.spark.sql.catalyst.plans.logical.LogicalPlan\nimport org.apache.spark.sql.catalyst.catalog.CatalogTable\nimport org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTableType}\nimport org.apache.spark.sql.catalyst.streaming.StreamingRelationV2\nimport org.apache.spark.sql.delta.Relocated.StreamingRelation\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\nimport org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation\nimport org.apache.spark.sql.execution.datasources.DataSource\nimport org.apache.spark.sql.connector.catalog.Identifier\nimport org.apache.spark.sql.types.StructType\nimport org.apache.spark.sql.util.CaseInsensitiveStringMap\n\nclass ApplyV2StreamingSuite extends DeltaSQLCommandTest {\n\n  private def applyRule(plan: LogicalPlan): LogicalPlan = {\n    new ApplyV2Streaming(spark).apply(plan)\n  }\n\n  private def assertV2(result: LogicalPlan): Unit = {\n    result match {\n      case StreamingRelationV2(_, _, _: SparkTable, _, _, _, _, v1Relation) =>\n        assert(v1Relation.isEmpty)\n      case other =>\n        fail(s\"Expected StreamingRelationV2, got $other\")\n    }\n  }\n\n  private def assertV1(result: LogicalPlan): Unit = {\n    assert(result.isInstanceOf[StreamingRelation])\n    assert(!result.isInstanceOf[StreamingRelationV2])\n  }\n\n  private def createDeltaTable(path: String): Unit = {\n    spark.range(1).selectExpr(\"id\").write.format(\"delta\").save(path)\n  }\n\n  private def createCatalogTable(locationUri: URI, ucManaged: Boolean): CatalogTable = {\n    val storageProps = new JHashMap[String, String]()\n    if (ucManaged) {\n      storageProps.put(UCCommitCoordinatorClient.UC_TABLE_ID_KEY, \"uc-table-id\")\n      storageProps.put(\"delta.feature.catalogManaged\", \"supported\")\n    }\n    val identifier = TableIdentifier(\"tbl\", Some(\"default\"), Some(\"spark_catalog\"))\n    val storage = CatalogStorageFormat(\n      locationUri = Some(locationUri),\n      inputFormat = None,\n      outputFormat = None,\n      serde = None,\n      compressed = false,\n      properties = storageProps.asScala.toMap)\n    CatalogTable(\n      identifier = identifier,\n      tableType = CatalogTableType.MANAGED,\n      storage = storage,\n      schema = new StructType(),\n      provider = None,\n      partitionColumnNames = Seq.empty,\n      bucketSpec = None,\n      properties = Map.empty)\n  }\n\n  private val relationBuilders: Seq[(String, (CatalogTable, String) => LogicalPlan)] = Seq(\n    (\"streaming\", (catalogTable, path) => {\n      val dataSource = DataSource(\n        sparkSession = spark,\n        userSpecifiedSchema = None,\n        className = \"delta\",\n        options = Map(\"path\" -> path),\n        catalogTable = Some(catalogTable))\n      StreamingRelation(dataSource)\n    }),\n    (\"non-streaming\", (catalogTable, _) => {\n      val ident = Identifier.of(\n        catalogTable.identifier.database.toArray,\n        catalogTable.identifier.table)\n      val table = new SparkTable(ident, catalogTable, new JHashMap[String, String]())\n      DataSourceV2Relation.create(\n        table,\n        None,\n        None,\n        CaseInsensitiveStringMap.empty)\n    })\n  )\n\n  private val modes: Seq[(String, Boolean)] = Seq(\n    (\"STRICT\", true),\n    (\"AUTO\", false),\n    (\"NONE\", false)\n  )\n\n  modes.foreach { case (mode, expectV2) =>\n    relationBuilders.foreach { case (relationType, buildPlan) =>\n      test(s\"ApplyV2Streaming respects $mode mode for $relationType relation\") {\n        withTempDir { dir =>\n          val path = dir.getCanonicalPath\n          createDeltaTable(path)\n          val catalogTable = createCatalogTable(dir.toURI, ucManaged = false)\n          val plan = buildPlan(catalogTable, path)\n\n          withSQLConf(DeltaSQLConf.V2_ENABLE_MODE.key -> mode) {\n            val result = applyRule(plan)\n            if (relationType == \"streaming\") {\n              if (expectV2) {\n                assertV2(result)\n              } else {\n                assertV1(result)\n              }\n            } else {\n              assert(result == plan)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/org/apache/spark/sql/delta/DataFrameWriterV2WithV2ConnectorSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta\n\nimport org.apache.spark.sql.delta.test.V2ForceTest\n\n/**\n * Test suite that runs OpenSourceDataFrameWriterV2Tests with Delta V2 connector\n * mode forced to STRICT.\n */\nclass DataFrameWriterV2WithV2ConnectorSuite\n  extends OpenSourceDataFrameWriterV2Tests\n  with V2ForceTest {\n\n  /**\n   * Tests that we expect to fail because they require write operations after initial\n   * table creation.\n   *\n   * Kernel's SparkTable (V2 connector) only implements SupportsRead, not SupportsWrite.\n   * Tests that perform append/replace operations after table creation are expected to fail.\n   */\n  override protected def shouldFail(testName: String): Boolean = {\n    val shouldFailTests = Set(\n      // Append operations - require SupportsWrite\n      \"Append: basic append\",\n      \"Append: by name not position\",\n\n      // Overwrite operations - require SupportsWrite\n      \"Overwrite: overwrite by expression: true\",\n      \"Overwrite: overwrite by expression: id = 3\",\n      \"Overwrite: by name not position\",\n\n      // OverwritePartitions operations - require SupportsWrite\n      \"OverwritePartitions: overwrite conflicting partitions\",\n      \"OverwritePartitions: overwrite all rows if not partitioned\",\n      \"OverwritePartitions: by name not position\",\n\n      // Create operations - TODO: fix SparkTable's name() to match DeltaTableV2\n      // SparkTable.name() returns simple table name, but tests expect catalog.schema.table format\n      \"Create: basic behavior\",\n      \"Create: with using\",\n      \"Create: with property\",\n      \"Create: identity partitioned table\",\n      \"Create: fail if table already exists\",\n\n      // Replace operations - require SupportsWrite\n      \"Replace: basic behavior\",\n      \"Replace: partitioned table\",\n\n      // CreateOrReplace operations - require SupportsWrite\n      \"CreateOrReplace: table does not exist\",\n      \"CreateOrReplace: table exists\"\n    )\n\n    shouldFailTests.contains(testName)\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/org/apache/spark/sql/delta/ProtocolMetadataAdapterV2Suite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.spark.sql.delta\n\nimport io.delta.kernel.internal.actions.{Format, Metadata, Protocol}\nimport io.delta.kernel.internal.util.VectorUtils\nimport io.delta.spark.internal.v2.read.ProtocolMetadataAdapterV2\nimport io.delta.spark.internal.v2.utils.SchemaUtils\nimport io.delta.kernel.types.{ArrayType, StringType => KernelStringType}\n\nimport org.apache.spark.sql.types.{IntegerType, StructType}\nimport org.scalactic.source.Position\nimport org.scalatest.Tag\n\nimport java.util.Optional\nimport scala.jdk.CollectionConverters._\n\n/**\n * Unit tests for ProtocolMetadataAdapterV2.\n *\n * This suite tests the V2 wrapper implementation that adapts kernel's Protocol and Metadata\n * to the ProtocolMetadataAdapter interface.\n */\nclass ProtocolMetadataAdapterV2Suite extends ProtocolMetadataAdapterSuiteBase {\n\n  /**\n   * Tests that are not applicable to V2 (kernel-based) implementation.\n   * These tests can be ignored because V2 has different behavior or limitations.\n   */\n  protected def ignoredTests: Set[String] = Set(\n    // TODO(delta-io/delta#5649): add type widening validation\n    \"assertTableReadable with table with unsupported type widening\",\n    // V1 IcebergCompat is not supported in Kernel (only V2/V3)\n    \"isIcebergCompatAnyEnabled when v1 enabled\",\n    \"isIcebergCompatGeqEnabled when v1 enabled\"\n  )\n\n  override protected def test(\n      testName: String,\n      testTags: Tag*)(testFun: => Any)(implicit pos: Position): Unit = {\n    if (ignoredTests.contains(testName)) {\n      super.ignore(s\"$testName - not applicable to V2 implementation\")(testFun)\n    } else {\n      super.test(testName, testTags: _*)(testFun)\n    }\n  }\n\n  override protected def createWrapper(\n      minReaderVersion: Int = 1,\n      minWriterVersion: Int = 2,\n      readerFeatures: Option[Set[String]] = None,\n      writerFeatures: Option[Set[String]] = None,\n      schema: StructType = new StructType().add(\"id\", IntegerType),\n      configuration: Map[String, String] = Map.empty): ProtocolMetadataAdapter = {\n\n    // Create kernel Protocol\n    val protocol = new Protocol(\n      minReaderVersion,\n      minWriterVersion,\n      readerFeatures.map(_.asJava).getOrElse(java.util.Collections.emptySet()),\n      writerFeatures.map(_.asJava).getOrElse(java.util.Collections.emptySet())\n    )\n\n    // Convert Spark schema to Kernel schema\n    val kernelSchema = SchemaUtils.convertSparkSchemaToKernelSchema(schema)\n    val schemaString = kernelSchema.toJson\n\n    // Create kernel Metadata\n    val metadata = new Metadata(\n      \"test-id\",\n      Optional.of(\"test-table\"),\n      Optional.of(\"test description\"),\n      new Format(\"parquet\", java.util.Collections.emptyMap()),\n      schemaString,\n      kernelSchema,\n      VectorUtils.buildArrayValue(\n        java.util.Collections.emptyList(),\n        new ArrayType(KernelStringType.STRING, true)),\n      Optional.of(System.currentTimeMillis()),\n      VectorUtils.stringStringMapValue(configuration.asJava)\n    )\n\n    // Create and return the V2 adapter\n    new ProtocolMetadataAdapterV2(protocol, metadata)\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/org/apache/spark/sql/delta/catalog/DeltaCatalogSuite.scala",
    "content": "/*\n * Copyright (2025) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.catalog\n\nimport io.delta.spark.internal.v2.catalog.SparkTable\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.test.DeltaSQLCommandTest\n\nimport java.io.File\nimport java.util.Locale\n\n/**\n * Unit tests for DeltaCatalog's V2 connector routing logic.\n *\n * Verifies that DeltaCatalog correctly routes table loading based on\n * DeltaSQLConf.V2_ENABLE_MODE:\n * - STRICT mode: Kernel's SparkTable (V2 connector)\n * - NONE mode (default): DeltaTableV2 (V1 connector)\n */\nclass DeltaCatalogSuite extends DeltaSQLCommandTest {\n\n  private val modeTestCases = Seq(\n    (\"STRICT\", classOf[SparkTable], \"Kernel SparkTable\"),\n    (\"NONE\", classOf[DeltaTableV2], \"DeltaTableV2\")\n  )\n\n  modeTestCases.foreach { case (mode, expectedClass, description) =>\n    test(s\"catalog-based table with mode=$mode returns $description\") {\n      withTempDir { tempDir =>\n        val tableName = s\"test_catalog_${mode.toLowerCase(Locale.ROOT)}\"\n        val location = new File(tempDir, tableName).getAbsolutePath\n\n        withSQLConf(DeltaSQLConf.V2_ENABLE_MODE.key -> mode) {\n          sql(s\"CREATE TABLE $tableName (id INT, name STRING) USING delta LOCATION '$location'\")\n\n          val catalog = spark.sessionState.catalogManager.v2SessionCatalog\n            .asInstanceOf[DeltaCatalog]\n          val ident = org.apache.spark.sql.connector.catalog.Identifier\n            .of(Array(\"default\"), tableName)\n          val table = catalog.loadTable(ident)\n\n          assert(table.getClass == expectedClass,\n            s\"Mode $mode should return ${expectedClass.getSimpleName}\")\n        }\n      }\n    }\n  }\n\n  modeTestCases.foreach { case (mode, expectedClass, description) =>\n    test(s\"path-based table with mode=$mode returns $description\") {\n      withTempDir { tempDir =>\n        val path = tempDir.getAbsolutePath\n\n        withSQLConf(DeltaSQLConf.V2_ENABLE_MODE.key -> mode) {\n          sql(s\"CREATE TABLE delta.`$path` (id INT, name STRING) USING delta\")\n\n          val catalog = spark.sessionState.catalogManager.v2SessionCatalog\n            .asInstanceOf[DeltaCatalog]\n          val ident = org.apache.spark.sql.connector.catalog.Identifier\n            .of(Array(\"delta\"), path)\n          val table = catalog.loadTable(ident)\n\n          assert(table.getClass == expectedClass,\n            s\"Mode $mode should return ${expectedClass.getSimpleName} for path-based table\")\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/org/apache/spark/sql/delta/test/DeltaV2SourceDeletionVectorsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.sql.delta.DeltaSourceDeletionVectorsSuite\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\n\n/**\n * Test suite that runs DeltaSourceDeletionVectorsSuite using the V2 connector\n * (V2_ENABLE_MODE=STRICT).\n */\nclass DeltaV2SourceDeletionVectorsSuite\n  extends DeltaSourceDeletionVectorsSuite with V2ForceTest {\n\n  override protected def useDsv2: Boolean = true\n\n  /**\n   * Override executeDml to temporarily use V1 connector for DML operations.\n   * SparkTable (V2) is read-only and does not support writes, so DML must\n   * go through the V1 path. Only streaming reads use the V2 connector.\n   */\n  override protected def executeDml(sqlText: String): Unit = {\n    withSQLConf(DeltaSQLConf.V2_ENABLE_MODE.key -> \"NONE\") {\n      sql(sqlText)\n    }\n  }\n\n  private lazy val shouldPassTests = Set(\n    \"allow to delete files before starting a streaming query\",\n    \"allow to delete files before staring a streaming query without checkpoint\",\n    \"multiple deletion vectors per file with initial snapshot\",\n    \"deleting files fails query if ignoreDeletes = false\",\n    \"allow to delete files after staring a streaming query when ignoreDeletes is true\",\n    \"updating the source table causes failure when ignoreChanges = false - using DELETE\",\n    \"updating source table when ignoreDeletes = true fails the query - using DELETE\",\n    \"subsequent DML commands are processed correctly in a batch - DELETE->DELETE - List()\",\n    \"subsequent DML commands are processed correctly in a batch - DELETE->DELETE\" +\n      \" - List((ignoreDeletes,true))\",\n    \"subsequent DML commands are processed correctly in a batch - DELETE->DELETE\" +\n      \" - List((skipChangeCommits,true))\",\n    \"subsequent DML commands are processed correctly in a batch - INSERT->DELETE - List()\",\n    \"subsequent DML commands are processed correctly in a batch - INSERT->DELETE\" +\n      \" - List((ignoreDeletes,true))\",\n    \"subsequent DML commands are processed correctly in a batch - INSERT->DELETE\" +\n      \" - List((skipChangeCommits,true))\"\n  )\n\n  private lazy val shouldFailTests = Set(\n    // TODO(#5319): enable these tests after ignoreChanges/ignoreFileDeletion read options\n    //  are supported by the V2 connector.\n    \"allow to delete files after staring a streaming query when ignoreFileDeletion is true\",\n    \"allow to update the source table when ignoreChanges = true - using DELETE\",\n    \"deleting files when ignoreChanges = true doesn't fail the query\",\n    \"subsequent DML commands are processed correctly in a batch - DELETE->DELETE\" +\n      \" - List((ignoreChanges,true))\",\n    \"subsequent DML commands are processed correctly in a batch - INSERT->DELETE\" +\n      \" - List((ignoreChanges,true))\",\n    \"multiple deletion vectors per file - List((ignoreFileDeletion,true))\",\n    \"multiple deletion vectors per file - List((ignoreChanges,true))\"\n  )\n\n  override protected def shouldFail(testName: String): Boolean = {\n    val inPassList = shouldPassTests.contains(testName)\n    val inFailList = shouldFailTests.contains(testName)\n\n    assert(inPassList || inFailList, s\"Test '$testName' not in shouldPassTests or shouldFailTests\")\n    assert(!(inPassList && inFailList),\n      s\"Test '$testName' in both shouldPassTests and shouldFailTests\")\n\n    inFailList\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/org/apache/spark/sql/delta/test/DeltaV2SourceSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.sql.delta.DeltaLog\nimport org.apache.spark.sql.delta.DeltaConfigs\nimport org.apache.spark.sql.delta.DeltaOperations\nimport org.apache.spark.sql.delta.DeltaSourceSuite\n\n/**\n * Test suite that runs DeltaSourceSuite using the V2 connector (V2_ENABLE_MODE=STRICT).\n */\nclass DeltaV2SourceSuite extends DeltaSourceSuite with V2ForceTest {\n\n  override protected def useDsv2: Boolean = true\n\n  /**\n   * Override disableLogCleanup to use DeltaLog API instead of SQL ALTER TABLE.\n   * Path-based ALTER TABLE doesn't work properly with V2_ENABLE_MODE=STRICT.\n   * TODO(#5731): pending kernel v2 connector support.\n   */\n  override protected def disableLogCleanup(tablePath: String): Unit = {\n    val deltaLog = DeltaLog.forTable(spark, tablePath)\n    val metadata = deltaLog.snapshot.metadata\n    val newConfiguration = metadata.configuration ++ Map(\n      DeltaConfigs.ENABLE_EXPIRED_LOG_CLEANUP.key -> \"false\"\n    )\n    deltaLog.startTransaction().commit(\n      metadata.copy(configuration = newConfiguration) :: Nil,\n      DeltaOperations.SetTableProperties(\n        Map(DeltaConfigs.ENABLE_EXPIRED_LOG_CLEANUP.key -> \"false\"))\n    )\n  }\n\n  private lazy val shouldPassTests = Set(\n    // ========== Core streaming tests ==========\n    \"basic\",\n    \"initial snapshot ends at base index of next version\",\n    \"new commits arrive after stream initialization - with explicit startingVersion\",\n    \"SC-11561: can consume new data without update\",\n    \"Delta sources don't write offsets with null json\",\n\n    // === Schema Evolution ===\n    \"add column: restarting with new DataFrame should recover\",\n    \"add column: restarting with stale DataFrame should fail\",\n    \"relax nullability: restarting with new DataFrame should recover\",\n    \"type widening: restarting with new DataFrame should recover\",\n    \"disallow to change schema after starting a streaming query\",\n    \"allow to change schema before starting a streaming query\",\n    \"drop column: should fail with non-additive schema change error\",\n    \"drop column: should succeed with unsafe column mapping schema change flag enabled\",\n    \"rename column: should fail with non-additive schema change error\",\n    \"rename column: should throw schema change error with unsafe flag enabled\",\n    \"type widening: should fail with non-additive schema change error when enable schema tracking\",\n\n    // === Read options ===\n    \"excludeRegex works and doesn't mess up offsets across restarts - parquet version\",\n    \"streaming with ignoreDeletes = true skips delete-only commits\",\n    \"streaming with ignoreDeletes = true still fails on change commits\",\n    \"streaming with skipChangeCommits = true skips both delete and change commits\",\n\n    // ========== startingVersion option tests ==========\n    \"startingVersion\",\n    \"startingVersion latest\",\n    \"startingVersion latest defined before started\",\n    \"startingVersion latest works on defined but empty table\",\n    \"startingVersion specific version: new commits arrive after stream initialization\",\n    \"startingVersion: user defined start works with mergeSchema\",\n    \"startingVersion latest calls update when starting\",\n    \"startingVersion should be ignored when restarting from a checkpoint, withRowTracking = true\",\n    \"startingVersion should be ignored when restarting from a checkpoint, withRowTracking = false\",\n    \"startingVersion and startingTimestamp are both set\",\n    \"startingTimestamp\",\n\n    // ========== Rate limiting tests ==========\n    \"maxFilesPerTrigger\",\n    \"maxBytesPerTrigger: process at least one file\",\n    \"maxFilesPerTrigger: change and restart\",\n    \"maxFilesPerTrigger: invalid parameter\",\n    \"maxFilesPerTrigger: ignored when using Trigger.Once\",\n    \"maxFilesPerTrigger: Trigger.AvailableNow respects read limits\",\n    \"maxBytesPerTrigger: change and restart\",\n    \"maxBytesPerTrigger: invalid parameter\",\n    \"maxBytesPerTrigger: Trigger.AvailableNow respects read limits\",\n    \"maxBytesPerTrigger: max bytes and max files together\",\n    \"Trigger.AvailableNow with an empty table\",\n    \"Rate limited Delta source advances with non-data inserts\",\n    \"ES-445863: delta source should not hang or reprocess data when using AvailableNow\",\n    \"startingVersion should work with rate time\",\n    \"maxFilesPerTrigger: metadata checkpoint\",\n    \"maxBytesPerTrigger: metadata checkpoint\",\n\n    // ========== Error handling tests ==========\n    \"streaming query should fail when table is deleted and recreated with new id\",\n    \"deltaSourceIgnoreDeleteError contains removeFile, version, tablePath\",\n    \"deltaSourceIgnoreChangesError contains removeFile, version, tablePath\",\n    \"excludeRegex throws good error on bad regex pattern\",\n\n    // ========== Misc tests ==========\n    \"a fast writer should not starve a Delta source\",\n    \"should not attempt to read a non exist version\",\n    \"can delete old files of a snapshot without update\",\n    \"Delta source advances with non-data inserts and generates empty dataframe for \" +\n      \"non-data operations\"\n  )\n\n  private lazy val shouldFailTests = Set(\n    // === Null Type Column Handling ===\n    \"streaming delta source should not drop null columns\",\n    \"streaming delta source should drop null columns without feature flag\",\n\n    // === Schema Evolution ===\n    // TODO(#6232): enable the two tests after spark streaming engine supports leaf node projection\n    //  for datasource v2 such that we can adopt the two schema changes without refreshing the\n    //  dataframe\n    \"relax nullability: restarting with stale DataFrame should recover\",\n    \"type widening: restarting with stale DataFrame should recover\",\n\n    // === Data Loss Detection ===\n    \"fail on data loss - starting from missing files\",\n    \"fail on data loss - gaps of files\",\n    \"fail on data loss - starting from missing files with option off\",\n    \"fail on data loss - gaps of files with option off\",\n\n    // === Misc ===\n    // TODO(#5900): fix exception mismatch\n    \"no schema should throw an exception\",\n    // TODO(#5900): fix exception mismatch\n    \"Delta sources should verify the protocol reader version\",\n    // TODO(#5895): gracefully handle corrupt checkpoint\n    \"start from corrupt checkpoint\",\n\n    // === Tests that bypass V2 by not using loadStreamWithOptions ===\n    \"disallow user specified schema\", // Uses .schema() directly\n    \"make sure that the delta sources works fine\", // Uses .delta() directly\n    \"self union a Delta table should pass the catalog table assert\", // Uses .table() directly\n    \"handling nullability schema changes\", // Uses .table() directly\n    \"allow user specified schema if consistent: v1 source\", // Uses DataSource directly\n    // Calls deltaSource.createSource() directly\n    \"createSource should create source with empty or matching table schema provided\"\n  )\n\n  override protected def shouldFail(testName: String): Boolean = {\n    val inPassList = shouldPassTests.contains(testName)\n    val inFailList = shouldFailTests.contains(testName)\n\n    assert(inPassList || inFailList, s\"Test '$testName' not in shouldPassTests or shouldFailTests\")\n    assert(!(inPassList && inFailList),\n      s\"Test '$testName' in both shouldPassTests and shouldFailTests\")\n\n    inFailList\n  }\n}\n"
  },
  {
    "path": "spark-unified/src/test/scala/org/apache/spark/sql/delta/test/V2ForceTest.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.spark.sql.delta.test\n\nimport org.apache.spark.SparkConf\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.scalatest.Tag\nimport org.scalactic.source.Position\n\nimport scala.collection.mutable\n\n/**\n * Trait that forces Delta V2 connector mode to STRICT, ensuring all operations\n * use the Kernel-based SparkTable implementation (V2 connector) instead of\n * DeltaTableV2 (V1 connector).\n *\n * See [[DeltaSQLConf.V2_ENABLE_MODE]] for V1 vs V2 connector definitions.\n *\n * Usage:\n * {{{\n * class MyKernelTest extends MyOriginalSuite with V2ForceTest {\n *   override protected def shouldSkipTest(testName: String): Boolean = {\n *     testName.contains(\"unsupported feature\")\n *   }\n * }\n * }}}\n */\ntrait V2ForceTest extends DeltaSQLCommandTest {\n\n  private val testsRun: mutable.Set[String] = mutable.Set.empty\n\n  /**\n   * Override `test` to apply the `shouldFail` logic.\n   * Tests that are expected to fail are converted to ignored tests.\n   */\n  abstract override protected def test(\n      testName: String,\n      testTags: Tag*)(testFun: => Any)(implicit pos: Position): Unit = {\n    if (shouldFail(testName)) {\n      // TODO(#5754): Assert on test failure instead of ignoring\n      super.ignore(\n        s\"$testName - expected to fail with Kernel-based V2 connector (not yet supported)\")(testFun)\n    } else {\n      super.test(testName, testTags: _*) {\n        testsRun.add(testName)\n        testFun\n      }\n    }\n  }\n\n  /**\n   * Determine if a test is expected to fail based on the test name.\n   * Subclasses should override this method to define which tests are expected to fail.\n   * By default, no tests are expected to fail.\n   *\n   * @param testName The name of the test\n   * @return true if the test is expected to fail, false otherwise\n   */\n  protected def shouldFail(testName: String): Boolean = false\n\n  /**\n   * Override `sparkConf` to set V2_ENABLE_MODE to \"STRICT\".\n   * This ensures all catalog operations use Kernel SparkTable (V2 connector).\n   */\n  abstract override protected def sparkConf: SparkConf = {\n    super.sparkConf\n      .set(DeltaSQLConf.V2_ENABLE_MODE.key, \"STRICT\")\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n  }\n}\n\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/AzureLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.IOException;\nimport java.util.Iterator;\n\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\n\n/**\n * LogStore implementation for Azure.\n * <p>\n * We assume the following from Azure's [[FileSystem]] implementations:\n * <ul>\n *     <li>Rename without overwrite is atomic.</li>\n *     <li>List-after-write is consistent.</li>\n * </ul>\n * <p>\n * Regarding file creation, this implementation:\n *  <ul>\n *     <li>Uses atomic rename when overwrite is false; if the destination file exists or the rename\n *         fails, throws an exception.</li>\n *     <li>Uses create-with-overwrite when overwrite is true. This does not make the file atomically\n *         visible and therefore the caller must handle partial files.</li>\n * </ul>\n */\npublic class AzureLogStore extends HadoopFileSystemLogStore {\n\n    public AzureLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    @Override\n    public void write(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        writeWithRename(path, actions, overwrite, hadoopConf);\n    }\n\n    @Override\n    public Boolean isPartialWriteVisible(Path path, Configuration hadoopConf) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/CloseableIterator.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.Closeable;\nimport java.util.Iterator;\n\n/**\n * :: DeveloperApi ::\n *\n * An iterator that may contain resources which should be released after use. Users of\n * CloseableIterator are responsible for closing the iterator if they are done with it.\n *\n * @since 1.0.0\n */\npublic interface CloseableIterator<T> extends Iterator<T>, Closeable {}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/GCSLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport com.google.common.base.Throwables;\nimport io.delta.storage.internal.ThreadUtils;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FSDataOutputStream;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.LocalFileSystem;\nimport org.apache.hadoop.fs.Path;\n\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.Iterator;\nimport java.util.concurrent.Callable;\n\n/**\n * The {@link LogStore} implementation for GCS, which uses gcs-connector to\n * provide the necessary atomic and durability guarantees:\n *\n * <ol>\n *   <li>Atomic Visibility: Read/read-after-metadata-update/delete are strongly\n * consistent for GCS.</li>\n *\n *   <li>Consistent Listing: GCS guarantees strong consistency for both object and\n * bucket listing operations.\n * https://cloud.google.com/storage/docs/consistency</li>\n *\n *   <li>Mutual Exclusion: Preconditions are used to handle race conditions.</li>\n * </ol>\n *\n * Regarding file creation, this implementation:\n * <ul>\n *    <li>Opens a stream to write to GCS otherwise.</li>\n *    <li>Throws [[FileAlreadyExistsException]] if file exists and overwrite is false.</li>\n *    <li>Assumes file writing to be all-or-nothing, irrespective of overwrite option.</li>\n * </ul>\n * <p>\n * This class is not meant for direct access but for configuration based on storage system.\n * See https://docs.delta.io/latest/delta-storage.html for details.\n */\npublic class GCSLogStore extends HadoopFileSystemLogStore {\n\n    final String preconditionFailedExceptionMessage = \"412 Precondition Failed\";\n\n    public GCSLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    @Override\n    public void write(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        final FileSystem fs = path.getFileSystem(hadoopConf);\n\n        // This is needed for the tests to throw error with local file system.\n        if (fs instanceof LocalFileSystem && !overwrite && fs.exists(path)) {\n            throw new FileAlreadyExistsException(path.toString());\n        }\n\n        // GCS may upload an incomplete file when the current thread is interrupted, hence we move\n        // the write to a new thread so that the write cannot be interrupted.\n        // TODO Remove this hack when the GCS Hadoop connector fixes the issue.\n        // If overwrite=false and path already exists, gcs-connector will throw\n        // org.apache.hadoop.fs.FileAlreadyExistsException after fs.create is invoked.\n        // This should be mapped to java.nio.file.FileAlreadyExistsException.\n        Callable body = () -> {\n            FSDataOutputStream stream = fs.create(path, overwrite);\n            while (actions.hasNext()) {\n                stream.write((actions.next() + \"\\n\").getBytes(StandardCharsets.UTF_8));\n            }\n            stream.close();\n            return \"\";\n        };\n\n        try {\n            ThreadUtils.runInNewThread(\"delta-gcs-logstore-write\", true, body);\n        } catch (org.apache.hadoop.fs.FileAlreadyExistsException e) {\n            throw new FileAlreadyExistsException(path.toString());\n        } catch (IOException e) {\n            // GCS uses preconditions to handle race conditions for multiple writers.\n            // If path gets created between fs.create and stream.close by an external\n            // agent or race conditions. Then this block will execute.\n            // Reference: https://cloud.google.com/storage/docs/generations-preconditions\n            if (isPreconditionFailure(e)) {\n                if (!overwrite) {\n                    throw new FileAlreadyExistsException(path.toString());\n                }\n            } else {\n                throw e;\n            }\n        } catch (InterruptedException e) {\n            InterruptedIOException iio = new InterruptedIOException(e.getMessage());\n            iio.initCause(e);\n            throw iio;\n        } catch (Error | RuntimeException t) {\n            throw t;\n        } catch (Throwable t) {\n            // Throw RuntimeException to avoid the calling interfaces from throwing Throwable\n            throw new RuntimeException(t.getMessage(), t);\n        }\n    }\n\n    private boolean isPreconditionFailure(Throwable x) {\n        return Throwables.getCausalChain(x)\n            .stream()\n            .filter(p -> p != null)\n            .filter(p -> p.getMessage() != null)\n            .anyMatch(p -> p.getMessage().contains(preconditionFailedExceptionMessage));\n    }\n\n    @Override\n    public Boolean isPartialWriteVisible(Path path, Configuration hadoopConf) throws IOException {\n        return false;\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/HDFSLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.lang.reflect.Method;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.EnumSet;\nimport java.util.Iterator;\n\nimport io.delta.storage.internal.LogStoreErrors;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.*;\nimport org.apache.hadoop.fs.CreateFlag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The {@link LogStore} implementation for HDFS, which uses Hadoop {@link FileContext} API's to\n * provide the necessary atomic and durability guarantees:\n * <ol>\n *     <li>Atomic visibility of files: `FileContext.rename` is used write files which is atomic for\n *         HDFS.</li>\n *     <li>Consistent file listing: HDFS file listing is consistent.</li>\n * </ol>\n */\npublic class HDFSLogStore extends HadoopFileSystemLogStore {\n    private static final Logger LOG = LoggerFactory.getLogger(HDFSLogStore.class);\n    public static final String NO_ABSTRACT_FILE_SYSTEM_EXCEPTION_MESSAGE = \"No AbstractFileSystem\";\n\n    public HDFSLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    @Override\n    public void write(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        final boolean isLocalFs = path.getFileSystem(hadoopConf) instanceof RawLocalFileSystem;\n        if (isLocalFs) {\n            // We need to add `synchronized` for RawLocalFileSystem as its rename will not throw an\n            // exception when the target file exists. Hence we must make sure `exists + rename` in\n            // `writeInternal` for RawLocalFileSystem is atomic in our tests.\n            synchronized(this) {\n                writeInternal(path, actions, overwrite, hadoopConf);\n            }\n        } else {\n            // rename is atomic and also will fail when the target file exists. Not need to add the\n            // extra `synchronized`.\n            writeInternal(path, actions, overwrite, hadoopConf);\n        }\n    }\n\n    @Override\n    public Boolean isPartialWriteVisible(Path path, Configuration hadoopConf) {\n        return true;\n    }\n\n    /**\n     * @throws IOException if this HDFSLogStore is used to write into a Delta table on a non-HDFS\n     *                     storage system.\n     * @throws FileAlreadyExistsException if {@code overwrite} is false and the file at {@code path}\n     *                                    already exists.\n     */\n    private void writeInternal(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        final FileContext fc;\n        try {\n            fc = FileContext.getFileContext(path.toUri(), hadoopConf);\n        } catch (IOException e) {\n            if (e.getMessage().contains(NO_ABSTRACT_FILE_SYSTEM_EXCEPTION_MESSAGE)) {\n                final IOException newException =\n                    LogStoreErrors.incorrectLogStoreImplementationException(e);\n                LOG.error(newException.getMessage(), newException.getCause());\n                throw newException;\n            } else {\n                throw e;\n            }\n        }\n\n        if (!overwrite && fc.util().exists(path)) {\n            // This is needed for the tests to throw error with local file system\n            throw new FileAlreadyExistsException(path.toString());\n        }\n\n        final Path tempPath = createTempPath(path);\n        boolean streamClosed = false; // This flag is to avoid double close\n        boolean renameDone = false; // This flag is to save the delete operation in most cases.\n        final FSDataOutputStream stream = fc.create(\n            tempPath,\n            EnumSet.of(CreateFlag.CREATE),\n            Options.CreateOpts.checksumParam(Options.ChecksumOpt.createDisabled())\n        );\n\n        try {\n            while (actions.hasNext()) {\n                stream.write((actions.next() + \"\\n\").getBytes(StandardCharsets.UTF_8));\n            }\n            stream.close();\n            streamClosed = true;\n            try {\n                final Options.Rename renameOpt =\n                    overwrite ? Options.Rename.OVERWRITE : Options.Rename.NONE;\n                fc.rename(tempPath, path, renameOpt);\n                renameDone = true;\n                // TODO: this is a workaround of HADOOP-16255 - remove this when HADOOP-16255 is\n                // resolved\n                tryRemoveCrcFile(fc, tempPath);\n            } catch (org.apache.hadoop.fs.FileAlreadyExistsException e) {\n                throw new FileAlreadyExistsException(path.toString());\n            }\n        } finally {\n            if (!streamClosed) {\n                stream.close();\n            }\n            if (!renameDone) {\n                fc.delete(tempPath, false); // recursive=false\n            }\n        }\n\n        msyncIfSupported(path, hadoopConf);\n    }\n\n    /**\n     * Normally when using HDFS with an Observer NameNode setup, there would be read after write\n     * consistency within a single process, so the write would be guaranteed to be visible on the\n     * next read. However, since we are using the FileContext API for writing (for atomic rename),\n     * and the FileSystem API for reading (for more compatibility with various file systems), we\n     * are essentially using two separate clients that are not guaranteed to be kept in sync.\n     * Therefore we \"msync\" the FileSystem instance, which is cached across all uses of the same\n     * protocol/host combination, to make sure the next read through the HDFSLogStore can see this\n     * write.\n     * Any underlying FileSystem that is not the DistributedFileSystem will simply throw an\n     * UnsupportedOperationException, which can be ignored. Additionally, if an older version of\n     * Hadoop is being used that does not include msync, a NoSuchMethodError will be thrown while\n     * looking up the method, which can also be safely ignored.\n     */\n    private void msyncIfSupported(Path path, Configuration hadoopConf) throws IOException {\n        try {\n            FileSystem fs = path.getFileSystem(hadoopConf);\n            Method msync = fs.getClass().getMethod(\"msync\");\n            msync.invoke(fs);\n        } catch (InterruptedIOException e) {\n            throw e;\n        } catch (Throwable e) {\n            if (e instanceof InterruptedException) {\n                // Propagate the interrupt status\n                Thread.currentThread().interrupt();\n            }\n            // We ignore non fatal errors as calling msync is best effort.\n        }\n    }\n\n    /**\n     * @throws IOException if a fatal exception occurs. Will try to ignore most exceptions.\n     */\n    private void tryRemoveCrcFile(FileContext fc, Path path) throws IOException {\n        try {\n            final Path checksumFile =\n                new Path(path.getParent(), String.format(\".%s.crc\", path.getName()));\n\n            if (fc.util().exists(checksumFile)) {\n                // checksum file exists, deleting it\n                fc.delete(checksumFile, true); // recursive=true\n            }\n        } catch (Throwable e) {\n            if (!LogStoreErrors.isNonFatal(e)) {\n                throw e;\n            }\n            // else, ignore - we are removing crc file as \"best-effort\"\n        }\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/HadoopFileSystemLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.UUID;\n\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FSDataInputStream;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.FSDataOutputStream;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Default implementation of {@link LogStore} for Hadoop {@link FileSystem} implementations.\n */\npublic abstract class HadoopFileSystemLogStore extends LogStore {\n\n    public HadoopFileSystemLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    @Override\n    public CloseableIterator<String> read(Path path, Configuration hadoopConf) throws IOException {\n        FileSystem fs = path.getFileSystem(hadoopConf);\n        FSDataInputStream stream = fs.open(path);\n        Reader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));\n        return new LineCloseableIterator(reader);\n    }\n\n    @Override\n    public Iterator<FileStatus> listFrom(Path path, Configuration hadoopConf) throws IOException {\n        FileSystem fs = path.getFileSystem(hadoopConf);\n        if (!fs.exists(path.getParent())) {\n            throw new FileNotFoundException(\n                String.format(\"No such file or directory: %s\", path.getParent())\n            );\n        }\n        FileStatus[] files = fs.listStatus(path.getParent());\n        return Arrays.stream(files)\n            .filter(f -> f.getPath().getName().compareTo(path.getName()) >= 0)\n            .sorted(Comparator.comparing(o -> o.getPath().getName()))\n            .iterator();\n    }\n\n    @Override\n    public Path resolvePathOnPhysicalStorage(\n            Path path,\n            Configuration hadoopConf) throws IOException {\n        return path.getFileSystem(hadoopConf).makeQualified(path);\n    }\n\n    /**\n     * An internal write implementation that uses FileSystem.rename().\n     * <p>\n     * This implementation should only be used for the underlying file systems that support atomic\n     * renames, e.g., Azure is OK but HDFS is not.\n     */\n    protected void writeWithRename(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        FileSystem fs = path.getFileSystem(hadoopConf);\n\n        if (!fs.exists(path.getParent())) {\n            throw new FileNotFoundException(\n                    String.format(\"No such file or directory: %s\", path.getParent())\n            );\n        }\n        if (overwrite) {\n            final FSDataOutputStream stream = fs.create(path, true);\n            try {\n                while (actions.hasNext()) {\n                    stream.write((actions.next() + \"\\n\").getBytes(StandardCharsets.UTF_8));\n                }\n            } finally {\n                stream.close();\n            }\n        } else {\n            if (fs.exists(path)) {\n                throw new FileAlreadyExistsException(path.toString());\n            }\n            Path tempPath = createTempPath(path);\n            boolean streamClosed = false; // This flag is to avoid double close\n            boolean renameDone = false; // This flag is to save the delete operation in most cases\n            final FSDataOutputStream stream = fs.create(tempPath);\n            try {\n                while (actions.hasNext()) {\n                    stream.write((actions.next() + \"\\n\").getBytes(StandardCharsets.UTF_8));\n                }\n                stream.close();\n                streamClosed = true;\n                try {\n                    if (fs.rename(tempPath, path)) {\n                        renameDone = true;\n                    } else {\n                        if (fs.exists(path)) {\n                            throw new FileAlreadyExistsException(path.toString());\n                        } else {\n                            throw new IllegalStateException(\n                                    String.format(\"Cannot rename %s to %s\", tempPath, path)\n                            );\n                        }\n                    }\n                } catch (org.apache.hadoop.fs.FileAlreadyExistsException e) {\n                    throw new FileAlreadyExistsException(path.toString());\n                }\n            } finally {\n                if (!streamClosed) {\n                    stream.close();\n                }\n                if (!renameDone) {\n                    fs.delete(tempPath, false);\n                }\n            }\n        }\n    }\n\n    /**\n     * Create a temporary path (to be used as a copy) for the input {@code path}\n     */\n    protected Path createTempPath(Path path) {\n        return new Path(\n            path.getParent(),\n            String.format(\".%s.%s.tmp\", path.getName(), UUID.randomUUID())\n        );\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/LineCloseableIterator.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.UncheckedIOException;\nimport java.util.NoSuchElementException;\n\n/**\n * Turn a {@link Reader} to {@link CloseableIterator} which can be read on demand. Each element is\n * a trimmed line.\n */\npublic class LineCloseableIterator implements CloseableIterator<String> {\n    private final BufferedReader reader;\n\n    // Whether `nextValue` is valid. If it's invalid, we should try to read the next line.\n    private boolean gotNext = false;\n\n    // The next value to return when `next` is called. This is valid only if `getNext` is true.\n    private String nextValue = null;\n\n    // Whether the reader is closed.\n    private boolean closed = false;\n\n    // Whether we have consumed all data in the reader.\n    private boolean finished = false;\n\n    public LineCloseableIterator(Reader reader) {\n        this.reader =\n            reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader);\n    }\n\n    @Override\n    public boolean hasNext() {\n        try {\n            if (!finished) {\n                // Check whether we have closed the reader before reading. Even if `nextValue` is\n                // valid, we still don't return `nextValue` after a reader is closed. Otherwise, it\n                // would be confusing.\n                if (closed) {\n                    throw new IllegalStateException(\"Iterator is closed\");\n                }\n                if (!gotNext) {\n                    String nextLine = reader.readLine();\n                    if (nextLine == null) {\n                        finished = true;\n                        close();\n                    } else {\n                        nextValue = nextLine.trim();\n                    }\n                    gotNext = true;\n                }\n            }\n            return !finished;\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n    }\n\n    @Override\n    public String next() {\n        if (!hasNext()) {\n            throw new NoSuchElementException(\"End of stream\");\n        }\n        gotNext = false;\n        return nextValue;\n    }\n\n    @Override\n    public void close() throws IOException {\n        if (!closed) {\n            closed = true;\n            reader.close();\n        }\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/LocalLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.hadoop.fs.RawLocalFileSystem;\n\nimport java.io.IOException;\nimport java.util.Iterator;\n\n/**\n * Default {@link LogStore} implementation (should be used for testing only!).\n *\n * Production users should specify the appropriate {@link LogStore} implementation in Spark properties.<p>\n *\n * We assume the following from {@link FileSystem} implementations:\n * <ul>\n *  <li>Rename without overwrite is atomic.</li>\n *  <li>List-after-write is consistent.</li>\n * </ul>\n * Regarding file creation, this implementation:\n * <ul>\n *  <li>Uses atomic rename when overwrite is false; if the destination file exists or the rename\n *   fails, throws an exception. </li>\n *  <li>Uses create-with-overwrite when overwrite is true. This does not make the file atomically\n *   visible and therefore the caller must handle partial files.</li>\n * </ul>\n */\npublic class LocalLogStore extends HadoopFileSystemLogStore{\n    public LocalLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    /**\n     * This write implementation needs to wrap `writeWithRename` with `synchronized` as rename()\n     * for {@link RawLocalFileSystem} doesn't throw an exception when the target file\n     * exists. Hence, we must make sure `exists + rename` in `writeWithRename` is atomic in our tests.\n     */\n    @Override\n    public void write(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        synchronized(this) {\n            writeWithRename(path, actions, overwrite, hadoopConf);\n        }\n    }\n\n    @Override\n    public Boolean isPartialWriteVisible(Path path, Configuration hadoopConf) throws IOException {\n        return true;\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/LogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.Path;\n\nimport java.io.IOException;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.Iterator;\n\n/**\n * :: DeveloperApi ::\n *\n * <p>\n * General interface for all critical file system operations required to read and write the\n * Delta logs. The correctness is predicated on the atomicity and durability guarantees of\n * the implementation of this interface. Specifically,\n * </p>\n * <ol>\n *     <li>Atomic visibility of files: If isPartialWriteVisible is false, any file written through\n *         this store must be made visible atomically. In other words, this should not generate\n *         partial files.</li>\n *\n *    <li>Mutual exclusion: Only one writer must be able to create (or rename) a file at the final\n *         destination.</li>\n *\n *    <li>Consistent listing: Once a file has been written in a directory, all future listings for\n *    that directory must return that file.</li>\n * </ol>\n * <p>\n * All subclasses of this interface is required to have a constructor that takes Configuration\n * as a single parameter. This constructor is used to dynamically create the LogStore.\n * </p>\n * <p>\n * LogStore and its implementations are not meant for direct access but for configuration based\n * on storage system. See [[https://docs.delta.io/latest/delta-storage.html]] for details.\n * </p>\n *\n * @since 1.0.0\n */\npublic abstract class LogStore {\n\n  private Configuration initHadoopConf;\n\n  public LogStore(Configuration initHadoopConf) {\n    this.initHadoopConf = initHadoopConf;\n  }\n\n  /**\n   * :: DeveloperApi ::\n   *\n   * Hadoop configuration that should only be used during initialization of LogStore. Each method\n   * should use their `hadoopConf` parameter rather than this (potentially outdated) hadoop\n   * configuration.\n   */\n  public Configuration initHadoopConf() { return initHadoopConf; }\n\n  /**\n   * :: DeveloperApi ::\n   *\n   * Load the given file and return an `Iterator` of lines, with line breaks removed from each line.\n   * Callers of this function are responsible to close the iterator if they are done with it.\n   *\n   * @throws IOException if there's an issue resolving the FileSystem\n   * @since 1.0.0\n   */\n  public abstract CloseableIterator<String> read(\n        Path path,\n        Configuration hadoopConf) throws IOException;\n\n  /**\n   * :: DeveloperApi ::\n   *\n   * Write the given `actions` to the given `path` with or without overwrite as indicated.\n   * Implementation must throw {@link java.nio.file.FileAlreadyExistsException} exception if the\n   * file already exists and overwrite = false. Furthermore, if isPartialWriteVisible returns false,\n   * implementation must ensure that the entire file is made visible atomically, that is,\n   * it should not generate partial files.\n   *\n   * @throws IOException if there's an issue resolving the FileSystem\n   * @throws FileAlreadyExistsException if the file already exists and overwrite is false\n   * @since 1.0.0\n   */\n  public abstract void write(\n        Path path,\n        Iterator<String> actions,\n        Boolean overwrite,\n        Configuration hadoopConf) throws IOException;\n\n  /**\n   * :: DeveloperApi ::\n   *\n   * List the paths in the same directory that are lexicographically greater or equal to\n   * (UTF-8 sorting) the given `path`. The result should also be sorted by the file name.\n   *\n   * @throws IOException if there's an issue resolving the FileSystem\n   * @throws FileAlreadyExistsException if {@code path} directory can't be found\n   * @since 1.0.0\n   */\n  public abstract Iterator<FileStatus> listFrom(\n        Path path,\n        Configuration hadoopConf) throws IOException;\n\n  /**\n   * :: DeveloperApi ::\n   *\n   * Resolve the fully qualified path for the given `path`.\n   *\n   * @throws IOException if there's an issue resolving the FileSystem\n   * @since 1.0.0\n   */\n  public abstract Path resolvePathOnPhysicalStorage(\n        Path path,\n        Configuration hadoopConf) throws IOException;\n\n  /**\n   * :: DeveloperApi ::\n   *\n   * Whether a partial write is visible for the underlying file system of `path`.\n   *\n   * @throws IOException if there's an issue resolving the FileSystem\n   * @since 1.0.0\n   */\n  public abstract Boolean isPartialWriteVisible(\n        Path path,\n        Configuration hadoopConf) throws IOException;\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/S3SingleDriverLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\n\nimport com.google.common.io.CountingOutputStream;\nimport io.delta.storage.internal.PathLock;\nimport io.delta.storage.internal.S3LogStoreUtil;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.LocalFileSystem;\nimport org.apache.hadoop.fs.Path;\nimport org.apache.hadoop.fs.RawLocalFileSystem;\n\n/**\n * Single JVM LogStore implementation for S3.\n * <p>\n * We assume the following from S3's {@link FileSystem} implementations:\n * <ul>\n *   <li>File writing on S3 is all-or-nothing, whether overwrite or not.</li>\n *   <li>List-after-write is strongly consistent.</li>\n * </ul>\n * <p>\n * Regarding file creation, this implementation:\n * <ul>\n *   <li>Opens a stream to write to S3 (regardless of the overwrite option).</li>\n *   <li>Failures during stream write may leak resources, but may never result in partial\n *       writes.</li>\n * </ul>\n */\npublic class S3SingleDriverLogStore extends HadoopFileSystemLogStore {\n\n    /**\n     * Enables a faster implementation of listFrom by setting the startAfter parameter in S3 list\n     * requests. The feature is enabled by setting the property delta.enableFastS3AListFrom in the\n     * Hadoop configuration.\n     *\n     * This feature requires the Hadoop file system used for S3 paths to be castable to\n     * org.apache.hadoop.fs.s3a.S3AFileSystem.\n     */\n    private final boolean enableFastListFrom\n            = initHadoopConf().getBoolean(\"delta.enableFastS3AListFrom\", false);\n\n    ///////////////////////////\n    // Static Helper Methods //\n    ///////////////////////////\n\n    /**\n     * A global path lock to ensure that no concurrent writers writing to the same path in the same\n     * JVM.\n     */\n    private static final PathLock pathLock = new PathLock();\n\n    /////////////////////////////////////////////\n    // Constructor and Instance Helper Methods //\n    /////////////////////////////////////////////\n\n    public S3SingleDriverLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    private Path resolvePath(FileSystem fs, Path path) {\n        return stripUserInfo(fs.makeQualified(path));\n    }\n\n    private Path stripUserInfo(Path path) {\n        final URI uri = path.toUri();\n\n        try {\n            final URI newUri = new URI(\n                uri.getScheme(),\n                null, // userInfo\n                uri.getHost(),\n                uri.getPort(),\n                uri.getPath(),\n                uri.getQuery(),\n                uri.getFragment()\n            );\n\n            return new Path(newUri);\n        } catch (URISyntaxException e) {\n            // Propagating this URISyntaxException to callers would mean we would have to either\n            // include it in the public LogStore.java interface or wrap it in an\n            // IllegalArgumentException somewhere else. Instead, catch and wrap it here.\n            throw new IllegalArgumentException(e);\n        }\n    }\n\n    /**\n     * List files starting from `resolvedPath` (inclusive) in the same directory, which merges\n     * the file system list and the cache list when `useCache` is on, otherwise\n     * use file system list only.\n     */\n    private Iterator<FileStatus> listFromInternal(\n            FileSystem fs,\n            Path resolvedPath) throws IOException {\n        final Path parentPath = resolvedPath.getParent();\n        if (!fs.exists(parentPath)) {\n            throw new FileNotFoundException(\n                String.format(\"No such file or directory: %s\", parentPath)\n            );\n        }\n\n        FileStatus[] statuses;\n        if (\n            // LocalFileSystem and RawLocalFileSystem checks are needed for tests to pass\n            fs instanceof LocalFileSystem || fs instanceof RawLocalFileSystem || !enableFastListFrom\n        ) {\n            statuses = fs.listStatus(parentPath);\n        } else {\n            statuses = S3LogStoreUtil.s3ListFromArray(fs, resolvedPath, parentPath);\n        }\n\n        return Arrays\n            .stream(statuses)\n            .filter(s -> s.getPath().getName().compareTo(resolvedPath.getName()) >= 0)\n            .sorted(Comparator.comparing(a -> a.getPath().getName()))\n            .iterator();\n    }\n\n    ////////////////////////\n    // Public API Methods //\n    ////////////////////////\n\n    @Override\n    public void write(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        final FileSystem fs = path.getFileSystem(hadoopConf);\n        final Path resolvedPath = resolvePath(fs, path);\n        try {\n            pathLock.acquire(resolvedPath);\n            try {\n                if (fs.exists(resolvedPath) && !overwrite) {\n                    throw new java.nio.file.FileAlreadyExistsException(\n                        resolvedPath.toUri().toString()\n                    );\n                }\n\n                final CountingOutputStream stream =\n                    new CountingOutputStream(fs.create(resolvedPath, overwrite));\n\n                while (actions.hasNext()) {\n                    stream.write((actions.next() + \"\\n\").getBytes(StandardCharsets.UTF_8));\n                }\n                stream.close();\n            } catch (org.apache.hadoop.fs.FileAlreadyExistsException e) {\n                // Convert Hadoop's FileAlreadyExistsException to Java's FileAlreadyExistsException\n                throw new java.nio.file.FileAlreadyExistsException(e.getMessage());\n            }\n        } catch (java.lang.InterruptedException e) {\n            throw new InterruptedIOException(e.getMessage());\n        } finally {\n            pathLock.release(resolvedPath);\n        }\n    }\n\n    @Override\n    public Iterator<FileStatus> listFrom(Path path, Configuration hadoopConf) throws IOException {\n        final FileSystem fs = path.getFileSystem(hadoopConf);\n        final Path resolvedPath = resolvePath(fs, path);\n        return listFromInternal(fs, resolvedPath);\n    }\n\n    @Override\n    public Boolean isPartialWriteVisible(Path path, Configuration hadoopConf) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/Commit.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport org.apache.hadoop.fs.FileStatus;\n\nimport java.util.Objects;\n\n/**\n * Representation of a commit file\n */\npublic class Commit {\n\n  private long version;\n\n  private FileStatus fileStatus;\n\n  private long commitTimestamp;\n\n  public Commit(long version, FileStatus fileStatus, long commitTimestamp) {\n    this.version = version;\n    this.fileStatus = fileStatus;\n    this.commitTimestamp = commitTimestamp;\n  }\n\n  public long getVersion() {\n    return version;\n  }\n\n  public FileStatus getFileStatus() {\n    return fileStatus;\n  }\n\n  public long getCommitTimestamp() {\n    return commitTimestamp;\n  }\n\n  public Commit withFileStatus(FileStatus fileStatus) {\n    return new Commit(version, fileStatus, commitTimestamp);\n  }\n\n  public Commit withCommitTimestamp(long commitTimestamp) {\n      return new Commit(version, fileStatus, commitTimestamp);\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (this == obj) return true;\n    if (obj == null || getClass() != obj.getClass()) return false;\n\n    Commit commit = (Commit) obj;\n\n    if (version != commit.version) return false;\n    if (commitTimestamp != commit.commitTimestamp) return false;\n    return Objects.equals(fileStatus, commit.fileStatus);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(version, fileStatus, commitTimestamp);\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/CommitCoordinatorClient.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport java.io.IOException;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport io.delta.storage.LogStore;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\n\n/**\n * The CommitCoordinatorClient is responsible for communicating with the commit coordinator\n * and backfilling commits. It has four main APIs that need to be implemented\n *\n * <ul>\n * <li>Commit a new version of the table. See {@link #commit}.</li>\n * <li>Ensure that commits are backfilled if/when needed. See {@link #backfillToVersion}</li>\n * <li>Tracks and returns unbackfilled commits. See {@link #getCommits}.</li>\n * <li>Determine the table config during commit coordinator registration.\n *     See {@link #registerTable}</li>\n * </ul>\n */\npublic interface CommitCoordinatorClient {\n\n  /**\n   * API to register the table represented by the given `logPath` at the provided\n   * currentTableVersion with the commit coordinator this commit coordinator client represents.\n   *\n   * This API is called when the table is being converted from a file system table to a\n   * coordinated-commit table.\n   *\n   * When a new coordinated-commit table is being created, the currentTableVersion will be -1 and\n   * the upgrade commit needs to be a file system commit which will write the backfilled file\n   * directly.\n   *\n   * @param logPath         The path to the delta log of the table that should be converted\n   * @param tableIdentifier The optional tableIdentifier for the table. Some commit coordinators may\n   *                         choose to make this compulsory and error out if this is not provided.\n   * @param currentVersion  The currentTableVersion is the version of the table just before\n   *                        conversion. currentTableVersion + 1 represents the commit that\n   *                        will do the conversion. This must be backfilled atomically.\n   *                        currentTableVersion + 2 represents the first commit after conversion.\n   *                        This will go through the CommitCoordinatorClient and the client is\n   *                        free to choose when it wants to backfill this commit.\n   * @param currentMetadata The metadata of the table at currentTableVersion\n   * @param currentProtocol The protocol of the table at currentTableVersion\n   * @return A map of key-value pairs which is issued by the commit coordinator to identify the\n   *         table. This should be stored in the table's metadata. This information needs to be\n   *         passed to the {@link #commit}, {@link #getCommits}, and {@link #backfillToVersion}\n   *         APIs to identify the table.\n   */\n  Map<String, String> registerTable(\n    Path logPath,\n    Optional<TableIdentifier> tableIdentifier,\n    long currentVersion,\n    AbstractMetadata currentMetadata,\n    AbstractProtocol currentProtocol);\n\n  /**\n   * API to commit the given set of actions to the table represented by logPath at the\n   * given commitVersion.\n   *\n   * @param logStore        The log store to use for writing the commit file.\n   * @param hadoopConf      The Hadoop configuration required to access the file system.\n   * @param tableDescriptor The descriptor for the table.\n   * @param commitVersion   The version of the commit that is being committed.\n   * @param actions         The actions that need to be committed.\n   * @param updatedActions  The commit info and any metadata or protocol changes that are made\n   *                        as part of this commit.\n   * @return CommitResponse which contains the file status of the committed commit file. If the\n   *         commit is already backfilled, then the file status could be omitted from the response\n   *         and the client could retrieve the information by itself.\n   * @throws CommitFailedException if the commit operation fails.\n   */\n  CommitResponse commit(\n    LogStore logStore,\n    Configuration hadoopConf,\n    TableDescriptor tableDescriptor,\n    long commitVersion,\n    Iterator<String> actions,\n    UpdatedActions updatedActions) throws CommitFailedException;\n\n  /**\n   * API to get the unbackfilled commits for the table represented by the given logPath.\n   * Commits older than startVersion or newer than endVersion (if given) are ignored. The\n   * returned commits are contiguous and in ascending version order.\n   *\n   * Note that the first version returned by this API may not be equal to startVersion. This\n   * happens when some versions starting from startVersion have already been backfilled and so\n   * the commit coordinator may have stopped tracking them.\n   *\n   * The returned latestTableVersion is the maximum commit version ratified by the commit\n   * coordinator. Note that returning latestTableVersion as -1 is acceptable only if the commit\n   * coordinator never ratified any version, i.e. it never accepted any unbackfilled commit.\n   *\n   * @param tableDescriptor The descriptor for the table.\n   * @param startVersion    The minimum version of the commit that should be returned. Can be null.\n   * @param endVersion      The maximum version of the commit that should be returned. Can be null.\n   * @return GetCommitsResponse which has a list of {@link Commit}s and the latestTableVersion which\n   *         is tracked by {@link CommitCoordinatorClient}.\n   */\n  GetCommitsResponse getCommits(\n    TableDescriptor tableDescriptor,\n    Long startVersion,\n    Long endVersion);\n\n  /**\n   * API to ask the commit coordinator client to backfill all commits up to {@code version}\n   * and notify the commit coordinator.\n   *\n   * If this API returns successfully, that means the backfill must have been completed, although\n   * the commit coordinator may not be aware of it yet.\n   *\n   * @param logStore                   The log store to use for writing the backfilled commits.\n   * @param hadoopConf                 The Hadoop configuration required to access the file system.\n   * @param tableDescriptor            The descriptor for the table.\n   * @param version                    The version till which the commit coordinator client should\n   *                                   backfill.\n   * @param lastKnownBackfilledVersion The last known version that was backfilled before this API\n   *                                   was called. If it is None or invalid, then the commit\n   *                                   coordinator client should backfill from the beginning of\n   *                                   the table. Can be null.\n   * @throws IOException if there is an IO error while backfilling the commits.\n   */\n  void backfillToVersion(\n    LogStore logStore,\n    Configuration hadoopConf,\n    TableDescriptor tableDescriptor,\n    long version,\n    Long lastKnownBackfilledVersion) throws IOException;\n\n  /**\n   * Determines whether this CommitCoordinatorClient is semantically equal to another\n   * CommitCoordinatorClient.\n   *\n   * Semantic equality is determined by each CommitCoordinatorClient implementation based on\n   * whether the two instances can be used interchangeably when invoking any of the\n   * CommitCoordinatorClient APIs, such as {@link #commit}, {@link #getCommits}, etc. For example,\n   * both instances might be pointing to the same underlying endpoint.\n   */\n  boolean semanticEquals(CommitCoordinatorClient other);\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/CommitFailedException.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport java.util.Iterator;\nimport java.util.Map;\n\nimport io.delta.storage.LogStore;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Exception raised by\n * {@link CommitCoordinatorClient#commit(LogStore, Configuration, TableDescriptor, long, Iterator, UpdatedActions)}\n *\n * <pre>\n *  | retryable | conflict  | meaning                                                         |\n *  |   no      |   no      | something bad happened (e.g. auth failure)                      |\n *  |   no      |   yes     | permanent transaction conflict (e.g. multi-table commit failed) |\n *  |   yes     |   no      | transient error (e.g. network hiccup)                           |\n *  |   yes     |   yes     | physical conflict (allowed to rebase and retry)                 |\n *  </pre>\n */\npublic class CommitFailedException extends Exception {\n\n  private boolean retryable;\n\n  private boolean conflict;\n\n  public CommitFailedException(boolean retryable, boolean conflict, String message) {\n    super(message);\n    this.retryable = retryable;\n    this.conflict = conflict;\n  }\n\n  public CommitFailedException(boolean retryable, boolean conflict, String message, Throwable cause) {\n    super(message, cause);\n    this.retryable = retryable;\n    this.conflict = conflict;\n  }\n\n  public boolean getRetryable() {\n    return retryable;\n  }\n\n  public boolean getConflict() {\n    return conflict;\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/CommitResponse.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport java.util.Iterator;\nimport java.util.Map;\n\nimport io.delta.storage.LogStore;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Response container for\n * {@link CommitCoordinatorClient#commit(LogStore, Configuration, TableDescriptor, long, Iterator, UpdatedActions)}.\n */\npublic class CommitResponse {\n\n  private Commit commit;\n\n  public CommitResponse(Commit commit) {\n    this.commit = commit;\n  }\n\n  public Commit getCommit() {\n    return commit;\n  }\n}\n\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/CoordinatedCommitsUtils.java",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.delta.storage.LogStore;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.Path;\n\nimport java.io.IOException;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\n\npublic class CoordinatedCommitsUtils {\n\n    private CoordinatedCommitsUtils() {}\n\n    /** The subdirectory in which to store the delta log. */\n    private static final String LOG_DIR_NAME = \"_delta_log\";\n\n    /** The subdirectory in which to store the unbackfilled commit files. */\n    private static final String COMMIT_SUBDIR = \"_staged_commits\";\n\n    /** The configuration key for the coordinated commits owner name. */\n    private static final String COORDINATED_COMMITS_COORDINATOR_NAME_KEY =\n            \"delta.coordinatedCommits.commitCoordinator-preview\";\n\n    /** The configuration key for the coordinated commits owner conf. */\n    private static final String COORDINATED_COMMITS_COORDINATOR_CONF_KEY =\n        \"delta.coordinatedCommits.commitCoordinatorConf-preview\";\n\n\n    /** The configuration key for the coordinated commits table conf. */\n    private static final String COORDINATED_COMMITS_TABLE_CONF_KEY =\n        \"delta.coordinatedCommits.tableConf-preview\";\n\n    /**\n     * Creates a new unbackfilled delta file path for the given commit version.\n     * The path is of the form:\n     * `tablePath/_delta_log/_staged_commits/00000000000000000001.uuid.json`.\n     */\n    public static Path generateUnbackfilledDeltaFilePath(\n            Path logPath,\n            long version) {\n        String uuid = UUID.randomUUID().toString();\n        Path basePath = new Path(logPath, COMMIT_SUBDIR);\n        return new Path(basePath, String.format(\"%020d.%s.json\", version, uuid));\n    }\n\n    /**\n     * Returns the path to the backfilled delta file for the given commit version.\n     * The path is of the form `tablePath/_delta_log/00000000000000000001.json`.\n     */\n    public static Path getBackfilledDeltaFilePath(\n            Path logPath,\n            Long version) {\n        return new Path(logPath, String.format(\"%020d.json\", version));\n    }\n\n    /**\n     * Returns true if the commit is a coordinated commits to filesystem conversion.\n     */\n    public static boolean isCoordinatedCommitsToFSConversion(\n            Long commitVersion,\n            UpdatedActions updatedActions) {\n        boolean oldMetadataHasCoordinatedCommits =\n                getCoordinatorName(updatedActions.getOldMetadata()).isPresent();\n        boolean newMetadataHasCoordinatedCommits =\n                getCoordinatorName(updatedActions.getNewMetadata()).isPresent();\n        return oldMetadataHasCoordinatedCommits && !newMetadataHasCoordinatedCommits &&\n                commitVersion > 0;\n    }\n\n    /**\n     * Get the table path from the provided log path.\n     */\n    public static Path getTablePath(Path logPath) {\n        return logPath.getParent();\n    }\n\n    /**\n     * Returns the un-backfilled uuid formatted delta (json format) path for a given version.\n     *\n     * @param logPath The root path of the delta log.\n     * @param version The version of the delta file.\n     * @return The path to the un-backfilled delta file: logPath/_staged_commits/version.uuid.json\n     */\n    public static Path getUnbackfilledDeltaFile(\n            Path logPath, long version, Optional<String> uuidString) {\n        Path basePath = commitDirPath(logPath);\n        String uuid = uuidString.orElse(UUID.randomUUID().toString());\n        return new Path(basePath, String.format(\"%020d.%s.json\", version, uuid));\n    }\n\n    /**\n     * Write a UUID-based commit file for the specified version to the table at logPath.\n     */\n    public static FileStatus writeUnbackfilledCommitFile(\n            LogStore logStore,\n            Configuration hadoopConf,\n            String logPath,\n            long commitVersion,\n            Iterator<String> actions,\n            String uuid) throws IOException {\n        Path commitPath = new Path(getUnbackfilledDeltaFile(\n                new Path(logPath), commitVersion, Optional.of(uuid)).toString());\n        // Do not use Put-If-Absent for Unbackfilled Commits files since we assume that UUID-based\n        // commit files are globally unique, and so we will never have concurrent writers attempting\n        // to write the same commit file.\n        logStore.write(commitPath, actions, true /* overwrite */, hadoopConf);\n        return commitPath.getFileSystem(hadoopConf).getFileStatus(commitPath);\n    }\n\n    /** Returns path to the directory which holds the delta log */\n    public static Path logDirPath(Path tablePath) {\n        return new Path(tablePath, LOG_DIR_NAME);\n    }\n\n    /** Returns path to the directory which holds the unbackfilled commits */\n    public static Path commitDirPath(Path logPath) {\n        return new Path(logPath, COMMIT_SUBDIR);\n    }\n\n    /**\n     * Retrieves the coordinator name from the provided abstract metadata.\n     * If no coordinator is set, an empty optional is returned.\n     *\n     * @param metadata The abstract metadata from which to retrieve the coordinator name.\n     * @return The coordinator name if set, otherwise an empty optional.\n     */\n    public static Optional<String> getCoordinatorName(AbstractMetadata metadata) {\n        String coordinator = metadata\n                .getConfiguration()\n                .get(COORDINATED_COMMITS_COORDINATOR_NAME_KEY);\n        return Optional.ofNullable(coordinator);\n    }\n\n    private static Map<String, String> parseConfFromMetadata(\n            AbstractMetadata abstractMetadata,\n            String confKey) {\n        String conf = abstractMetadata\n            .getConfiguration()\n            .getOrDefault(confKey, \"{}\");\n        try {\n            return new ObjectMapper().readValue(\n                conf,\n                new TypeReference<Map<String, String>>() {});\n        } catch (JsonProcessingException e) {\n            throw new RuntimeException(\"Failed to parse conf: \", e);\n        }\n    }\n\n    /**\n     * Get the coordinated commits owner configuration from the provided abstract metadata.\n     */\n    public static Map<String, String> getCoordinatorConf(AbstractMetadata abstractMetadata) {\n        return parseConfFromMetadata(abstractMetadata, COORDINATED_COMMITS_COORDINATOR_CONF_KEY);\n    }\n\n    /**\n     * Get the coordinated commits table configuration from the provided abstract metadata.\n     */\n    public static Map<String, String> getTableConf(AbstractMetadata abstractMetadata) {\n        return parseConfFromMetadata(abstractMetadata, COORDINATED_COMMITS_TABLE_CONF_KEY);\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/GetCommitsResponse.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Response container for\n * {@link CommitCoordinatorClient#getCommits(TableDescriptor, Long, Long)}.\n */\npublic class GetCommitsResponse {\n\n  private List<Commit> commits;\n\n  private long latestTableVersion;\n\n  public GetCommitsResponse(List<Commit> commits, long latestTableVersion) {\n    this.commits = commits;\n    this.latestTableVersion = latestTableVersion;\n  }\n\n  public List<Commit> getCommits() {\n    return commits;\n  }\n\n  public long getLatestTableVersion() {\n    return latestTableVersion;\n  }\n}\n\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/TableDescriptor.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Container for all the info to uniquely identify the table\n */\npublic class TableDescriptor {\n\n    private Path logPath;\n    private Optional<TableIdentifier> tableIdentifier;\n\n    private Map<String, String> tableConf;\n\n    public TableDescriptor(Path logPath, Optional<TableIdentifier> tableIdentifier, Map<String, String> tableConf) {\n        this.logPath = logPath;\n        this.tableIdentifier = tableIdentifier;\n        this.tableConf = tableConf;\n    }\n\n    public Optional<TableIdentifier> getTableIdentifier() {\n        return tableIdentifier;\n    }\n\n    public Path getLogPath() {\n        return logPath;\n    }\n\n    public Map<String, String> getTableConf() {\n        return tableConf;\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/TableIdentifier.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\n/**\n * Identifier for a table.\n */\npublic class TableIdentifier {\n\n    // The name of the table.\n    private String name;\n\n    // The namespace of the table. e.g. <catalog> / <schema>\n    private String[] namespace;\n\n    public TableIdentifier(String[] namespace, String name) {\n        this.namespace = namespace;\n        this.name = name;\n    }\n\n    public TableIdentifier(String firstName, String... rest) {\n        String[] namespace = new String[rest.length];\n        String name;\n        if (rest.length > 0) {\n            name = rest[rest.length-1];\n            namespace[0] = firstName;\n            System.arraycopy(rest, 0, namespace, 1, rest.length-1);\n        } else {\n            name = firstName;\n        }\n        this.namespace = namespace;\n        this.name = name;\n    }\n\n    /**\n     * Returns the namespace of the table.\n     */\n    public String[] getNamespace() {\n        return namespace;\n    }\n\n    /**\n     * Returns the name of the table.\n     */\n    public String getName() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/UpdatedActions.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit;\n\nimport io.delta.storage.commit.actions.AbstractCommitInfo;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\n\n/**\n * A container class to inform the CommitCoordinatorClient about any changes in Protocol/Metadata\n */\npublic class UpdatedActions {\n\n  private AbstractCommitInfo commitInfo;\n\n  private AbstractMetadata newMetadata;\n\n  private AbstractProtocol newProtocol;\n\n  private AbstractMetadata oldMetadata;\n\n  private AbstractProtocol oldProtocol;\n\n  public UpdatedActions(\n      AbstractCommitInfo commitInfo,\n      AbstractMetadata newMetadata,\n      AbstractProtocol newProtocol,\n      AbstractMetadata oldMetadata,\n      AbstractProtocol oldProtocol) {\n    this.commitInfo = commitInfo;\n    this.newMetadata = newMetadata;\n    this.newProtocol = newProtocol;\n    this.oldMetadata = oldMetadata;\n    this.oldProtocol = oldProtocol;\n  }\n\n  public AbstractCommitInfo getCommitInfo() {\n    return commitInfo;\n  }\n\n  public AbstractMetadata getNewMetadata() {\n    return newMetadata;\n  }\n\n  public AbstractProtocol getNewProtocol() {\n    return newProtocol;\n  }\n\n  public AbstractMetadata getOldMetadata() {\n    return oldMetadata;\n  }\n\n  public AbstractProtocol getOldProtocol() {\n    return oldProtocol;\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/actions/AbstractCommitInfo.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.actions;\n\n/**\n * Interface for objects that represents the base information for a commit.\n * Commits need to provide an in-commit timestamp. This timestamp is used\n * to specify the exact time the commit happened and determines the target\n * version for time-based time travel queries.\n */\npublic interface AbstractCommitInfo {\n\n  /**\n   * Get the timestamp of the commit as millis after the epoch.\n   */\n  long getCommitTimestamp();\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/actions/AbstractMetadata.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.actions;\n\nimport java.util.Map;\nimport java.util.List;\n\n/**\n * Interface for metadata actions in Delta. The metadata defines the metadata\n * of the table.\n */\npublic interface AbstractMetadata {\n\n  /**\n   * A unique table identifier.\n   */\n  String getId();\n\n  /**\n   * User-specified table identifier.\n   */\n  String getName();\n\n  /**\n   * User-specified table description.\n   */\n  String getDescription();\n\n  /** The table provider format. */\n  String getProvider();\n\n  /** The format options */\n  Map<String, String> getFormatOptions();\n\n  /**\n   * The table schema in string representation.\n   */\n  String getSchemaString();\n\n  /**\n   * List of partition columns.\n   */\n  List<String> getPartitionColumns();\n\n  /**\n   * The table properties defined on the table.\n   */\n  Map<String, String> getConfiguration();\n\n  /**\n   * Timestamp for the creation of this metadata.\n   */\n  Long getCreatedTime();\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/actions/AbstractProtocol.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.actions;\n\nimport java.util.Set;\n\n/**\n * Interface for protocol actions in Delta. The protocol defines the requirements\n * that readers and writers of the table need to meet.\n */\npublic interface AbstractProtocol {\n\n  /**\n   * The minimum reader version required to read the table.\n   */\n  int getMinReaderVersion();\n\n  /**\n   * The minimum writer version required to read the table.\n   */\n  int getMinWriterVersion();\n\n  /**\n   * The reader features that need to be supported to read the table.\n   */\n  Set<String> getReaderFeatures();\n\n  /**\n   * The writer features that need to be supported to write the table.\n   */\n  Set<String> getWriterFeatures();\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/CommitLimitReachedException.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\n/**\n * This exception is thrown by the UC client in case UC has reached the maximum\n * number of commits that it is allowed to track (50 by default). Upon receiving\n * this exception, the client should run a backfill.\n */\npublic class CommitLimitReachedException extends UCCommitCoordinatorException {\n  public CommitLimitReachedException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/InvalidTargetTableException.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\n/**\n * This exception is thrown by the UC client in case a commit attempt tried to add\n * a UUID-based commit to the wrong table. The table is wrong if the path prefixes\n * of the table and the UUID commit do not match.\n * For example, adding /path/to/table1/_staged_commits/01-uuid.json to the table at\n * /path/to/table2 is not allowed.\n */\npublic class InvalidTargetTableException extends UCCommitCoordinatorException {\n  public InvalidTargetTableException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UCClient.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\nimport io.delta.storage.commit.Commit;\nimport io.delta.storage.commit.CommitFailedException;\nimport io.delta.storage.commit.GetCommitsResponse;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport io.delta.storage.commit.uniform.UniformMetadata;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.Optional;\n\n/**\n * Interface for interacting with the Unity Catalog.\n *\n * This interface defines the contract for operations related to the Unity Catalog,\n * including retrieving the metastore ID, and adding new commits to Delta tables where UC\n * acts as the Commit Coordinator and similarly retrieving unbackfilled commits.\n *\n * Implementations of this interface should handle the specifics of connecting to and\n * communicating with the Unity Catalog, including any necessary authentication and\n * request handling.\n */\npublic interface UCClient extends AutoCloseable {\n\n  /**\n   * Retrieves the metastore ID associated with this Unity Catalog instance.\n   *\n   * @return A String representing the unique identifier of the metastore\n   * @throws IOException if there's an error in retrieving the metastore ID\n   */\n  String getMetastoreId() throws IOException;\n\n  /**\n   * Commits new changes to a Delta table using the Unity Catalog as the Commit Coordinator.\n   *\n   * This method is responsible for committing changes to a Delta table, including new data,\n   * metadata updates, and protocol changes. It interacts with the Unity Catalog to ensure\n   * proper coordination and consistency of the commit process.\n   * Note: At least one of commit or lastKnownBackfilledVersion must be present.\n   *\n   * @param tableId The unique identifier of the Delta table.\n   * @param tableUri The URI of the storage location of the table. e.g. s3://bucket/path/to/table\n   *                 (and not s3://bucket/path/to/table/_delta_log).\n   *                 If the tableId exists but the tableUri is different from the one previously\n   *                 registered (e.g., if the table as moved), the request will fail.\n   * @param commit An Optional containing the Commit object with the changes to be committed.\n   *               If empty, it indicates that no new data is being added in this commit.\n   * @param lastKnownBackfilledVersion An Optional containing the last known backfilled version\n   *                                   of the table. This value serves as a hint to the UC about the\n   *                                   most recent version that has been successfully backfilled.\n   *                                   UC can use this information to optimize its internal state\n   *                                   management by cleaning up tracking information for backfilled\n   *                                   commits up to this version.\n   *                                   If not provided (Optional.empty()), UC will rely on its\n   *                                   current state without any additional cleanup hints.\n   * @param disown A boolean flag indicating whether to disown the table after commit.\n   *               If true, the coordinator will release ownership of the table after the commit.\n   * @param newMetadata An Optional containing new metadata to be applied to the table.\n   *                    If present, the table's metadata will be updated atomically with the commit.\n   * @param newProtocol An Optional containing a new protocol version to be applied to the table.\n   *                    If present, the table's protocol will be updated atomically with the commit.\n   * @param uniform An Optional containing UniForm metadata for Delta Universal Format support.\n   *                If present, this metadata will be used by UC to manage format conversions\n   *                (e.g., Iceberg, Hudi).\n   * @throws IOException if there's an error during the commit process, such as network issues.\n   * @throws CommitFailedException if the commit fails due to conflicts or other logical errors.\n   * @throws UCCommitCoordinatorException if there's an error specific to the Unity Catalog\n   *         commit coordination process.\n   */\n  void commit(\n      String tableId,\n      URI tableUri,\n      Optional<Commit> commit,\n      Optional<Long> lastKnownBackfilledVersion,\n      boolean disown,\n      Optional<AbstractMetadata> newMetadata,\n      Optional<AbstractProtocol> newProtocol,\n      Optional<UniformMetadata> uniform\n  ) throws IOException, CommitFailedException, UCCommitCoordinatorException;\n\n  /**\n   * Retrieves the unbackfilled commits for a Delta table within a specified version range.\n   *\n   * @param tableId The unique identifier of the Delta table.\n   * @param tableUri The URI of the storage location of the table. e.g. s3://bucket/path/to/table\n   *                 (and not s3://bucket/path/to/table/_delta_log).\n   *                 If the tableId exists but the tableUri is different from the one previously\n   *                 registered (e.g., if the table as moved), the request will fail.\n   * @param startVersion An Optional containing the start version of the range of commits to\n   *                     retrieve.\n   * @param endVersion An Optional containing the end version of the range of commits to retrieve.\n   * @return A GetCommitsResponse object containing the unbackfilled commits within the specified\n   *         version range. If all commits are backfilled, the response will contain an empty list.\n   *         The response also contains the last known backfilled version of the table. If no\n   *         commits are ratified via UC, the lastKnownBackfilledVersion will be -1.\n   * @throws IOException if there's an error during the commit process, such as network issues.\n   * @throws UCCommitCoordinatorException if there's an error specific to the Unity Catalog such as\n   *                                      the table not being found.\n   */\n  GetCommitsResponse getCommits(\n      String tableId,\n      URI tableUri,\n      Optional<Long> startVersion,\n      Optional<Long> endVersion) throws IOException, UCCommitCoordinatorException;\n\n  /**\n   * Closes any resources used by this client.\n   * This method should be called to properly release resources such as network\n   * connections (e.g., HTTPClient) when the client is no longer needed.\n   * Once this method is called, the client should not be used to perform any operations.\n   *\n   * @throws IOException if there's an error while closing resources\n   */\n  @Override\n  void close() throws IOException;\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UCCommitCoordinatorClient.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.BiConsumer;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nonnull;\n\nimport io.delta.storage.CloseableIterator;\nimport io.delta.storage.LogStore;\nimport io.delta.storage.commit.*;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport io.delta.storage.internal.FileNameUtils;\nimport io.delta.storage.internal.LogStoreErrors;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A commit coordinator client that uses unity-catalog as the commit coordinator.\n */\npublic class UCCommitCoordinatorClient implements CommitCoordinatorClient {\n  public UCCommitCoordinatorClient(Map<String, String> conf, UCClient ucClient) {\n    this.conf = conf;\n    this.ucClient = ucClient;\n  }\n\n  /**\n   * Logger for UCCommitCoordinatorClient class operations and diagnostics.\n   */\n  private static final Logger LOG = LoggerFactory.getLogger(UCCommitCoordinatorClient.class);\n\n  // UC Protocol Version Control Constants\n  /** Supported version for read operations in the Unity Catalog protocol. */\n  private static final int SUPPORTED_READ_VERSION = 0;\n\n  /** Supported version for write operations in the Unity Catalog protocol. */\n  private static final int SUPPORTED_WRITE_VERSION = 0;\n\n  /** Key used to identify the read version in protocol communications with the UC server. */\n  private static final String READ_VERSION_KEY = \"readVersion\";\n\n  /** Key used to identify the write version in protocol communications with the UC server. */\n  private static final String WRITE_VERSION_KEY = \"writeVersion\";\n\n  /**\n   * Temporary kill switch for sending metadata updates through UC from the Spark path.\n   * TODO(issue #6296): remove once metadata updates are supported end-to-end.\n   */\n  private static final boolean SHOULD_PASS_METADATA_TO_UC = false;\n\n  // Unity Catalog Identifiers\n  /**\n   * Key for identifying Unity Catalog table ID in `delta.coordinatedCommits.tableConf{-preview}`.\n   */\n  final static public String UC_TABLE_ID_KEY = \"io.unitycatalog.tableId\";\n  // Previously this key was ucTableId. It was later renamed.\n  final static public String UC_TABLE_ID_KEY_OLD = \"ucTableId\";\n\n  /**\n   * Key for identifying Unity Catalog metastore ID in\n   * `delta.coordinatedCommits.commitCoordinatorConf{-preview}`.\n   */\n  final static public String UC_METASTORE_ID_KEY = \"ucMetastoreId\";\n\n  // Backfill and Retry Configuration\n  /**\n   * Offset from current commit version for backfill listing optimization.\n   * Used to prevent expensive listings from version 0.\n   */\n  public static int BACKFILL_LISTING_OFFSET = 100;\n\n  /** Maximum number of retry attempts for transient errors. */\n  protected static final int MAX_RETRIES_ON_TRANSIENT_ERROR = 15;\n\n  /** Initial wait time in milliseconds before retrying after a transient error. */\n  protected static final long TRANSIENT_ERROR_RETRY_INITIAL_WAIT_MS = 100;\n\n  /** Maximum wait time in milliseconds between retries for transient errors. */\n  protected static final long TRANSIENT_ERROR_RETRY_MAX_WAIT_MS = 1000 * 60; // 1 minute\n\n  // Thread Pool Configuration\n  /** Size of the thread pool for handling asynchronous operations. */\n  static protected int THREAD_POOL_SIZE = 20;\n\n  /**\n   * Thread pool executor for handling asynchronous tasks like backfilling.\n   * Configured with daemon threads and custom naming pattern.\n   */\n  private static final ThreadPoolExecutor asyncExecutor;\n\n  // Static Initializer Block\n  static {\n    asyncExecutor = new ThreadPoolExecutor(\n      THREAD_POOL_SIZE,\n      THREAD_POOL_SIZE,\n      60L,\n      TimeUnit.SECONDS,\n      new LinkedBlockingQueue<>(Integer.MAX_VALUE),\n      new ThreadFactory() {\n        private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();\n\n        @Override\n        public Thread newThread(@Nonnull Runnable r) {\n          Thread t = defaultFactory.newThread(r);\n          // Set the thread name to uc-commit-coordinator-pool-1-thread-1\n          t.setName(\"uc-commit-coordinator-\" + t.getName());\n          t.setDaemon(true);\n          return t;\n        }\n      });\n    asyncExecutor.allowCoreThreadTimeOut(true);\n  }\n\n  // Instance Variables\n  /** Unity Catalog client instance for interacting with UC services. */\n  public final UCClient ucClient;\n\n  /** Configuration map containing settings for the coordinator client. */\n  public final Map<String, String> conf;\n\n  /**\n   * Runs a task asynchronously using the backfillThreadPool.\n   *\n   * @param task The task to be executed asynchronously\n   * @return A Future representing pending completion of the task\n   */\n  protected<T> Future<T> executeAsync(Callable<T> task) {\n    return asyncExecutor.submit(task);\n  }\n\n  protected String extractUCTableId(TableDescriptor tableDesc) {\n    Map<String, String> tableConf = tableDesc.getTableConf();\n    if (!tableConf.containsKey(UC_TABLE_ID_KEY)) {\n      throw new IllegalStateException(\"UC Table ID not found in \" + tableConf);\n    }\n    return tableConf.get(UC_TABLE_ID_KEY);\n  }\n\n  /**\n   * For UC, table registration is a no-op because we already contacted UC during table\n   * creation and that already obtained the necessary table config and added\n   * it to the metadata (this is for performance reasons and ease of use). As a result,\n   * this method only verifies that the metadata has been added correct and is present.\n   * Otherwise, it throws an exception.\n   */\n  @Override\n  public Map<String, String> registerTable(\n      Path logPath,\n      Optional<TableIdentifier> tableIdentifier,\n      long currentVersion,\n      AbstractMetadata currentMetadata,\n      AbstractProtocol currentProtocol) {\n    Map<String, String> tableConf = CoordinatedCommitsUtils.getTableConf(currentMetadata);\n    checkVersionSupported(tableConf, false /* compareRead */);\n\n    // The coordinatedCommitsTableConf must have been instantiated prior to this call\n    // with the UC table ID.\n    if (!tableConf.containsKey(UC_TABLE_ID_KEY)) {\n      throw new IllegalStateException(\"Could not verify if the table is registered with the \" +\n        \"UC commit coordinator because the table ID is missing from the table metadata.\");\n    }\n    // The coordinatedCommitsCoordinatorConf must have been instantiated prior to this call\n    // with the metastore ID of the metastore, which stores the table.\n    if (!CoordinatedCommitsUtils.getCoordinatorConf(currentMetadata).containsKey(\n        UC_METASTORE_ID_KEY)) {\n      throw new IllegalStateException(\"Could not verify if the table is registered with the UC \" +\n        \"commit coordinator because the metastore ID is missing from the table metadata.\");\n    }\n    return tableConf;\n  }\n\n  /**\n   * Find the last known backfilled version by doing a listing of the last\n   * {@link #BACKFILL_LISTING_OFFSET} commits. If no backfilled commits are found\n   * among those, a UC call is made to get the oldest tracked commit in UC.\n   */\n  public long getLastKnownBackfilledVersion(\n      long commitVersion,\n      Configuration hadoopConf,\n      LogStore logStore,\n      TableDescriptor tableDesc\n  ) {\n    Path logPath = tableDesc.getLogPath();\n    long listFromVersion = Math.max(0, commitVersion - BACKFILL_LISTING_OFFSET);\n    Optional<Long> lastKnownBackfilledVersion =\n      listAndGetLastKnownBackfilledVersion(listFromVersion, logStore, hadoopConf, logPath);\n    if (!lastKnownBackfilledVersion.isPresent()) {\n      // In case we don't find anything in the last 100 commits (should not happen)\n      // we go to UC to find the earliest commit it is tracking as the commit prior\n      // to that must have been backfilled.\n      recordDeltaEvent(\n        UCCoordinatedCommitsUsageLogs.UC_LAST_KNOWN_BACKFILLED_VERSION_NOT_FOUND,\n        new HashMap<String, Object>() {{\n          put(\"commitVersion\", commitVersion);\n          put(\"conf\", conf);\n          put(\"listFromVersion\", listFromVersion);\n          put(\"tableConf\", tableDesc.getTableConf());\n        }},\n        logPath.getParent()\n      );\n      long minVersion =\n        getCommits(tableDesc, null, null)\n          .getCommits()\n          .stream()\n          .min(Comparator.comparingLong(Commit::getVersion))\n          .map(Commit::getVersion)\n          .orElseThrow(() -> new IllegalStateException(\"Couldn't find any unbackfilled commit \" +\n            \"for table at \" + logPath + \" at version \" + commitVersion));\n      lastKnownBackfilledVersion = listAndGetLastKnownBackfilledVersion(\n        minVersion - 1, logStore, hadoopConf, logPath);\n      if (!lastKnownBackfilledVersion.isPresent()) {\n        throw new IllegalStateException(\"Couldn't find any backfilled commit for table at \" +\n          logPath + \" at version \" + commitVersion);\n      }\n    }\n    return lastKnownBackfilledVersion.get();\n  }\n\n  protected Iterator<FileStatus> listFrom(\n      LogStore logStore,\n      long listFromVersion,\n      Configuration hadoopConf,\n      Path logPath) {\n    Path listingPath = CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, listFromVersion);\n    try {\n      return logStore.listFrom(listingPath, hadoopConf);\n    } catch (IOException e) {\n      LOG.error(\"Failed to list files from {} due to: {}\", listingPath, exceptionString(e));\n      throw new IllegalStateException(e);\n    }\n  }\n\n  protected Optional<Long> listAndGetLastKnownBackfilledVersion(\n      long listFromVersion,\n      LogStore logStore,\n      Configuration hadoopConf,\n      Path logPath) {\n    Optional<Long> lastKnownBackfilledVersion = Optional.empty();\n    Iterator<FileStatus> deltaLogFileIt =\n      listFrom(logStore, listFromVersion, hadoopConf, logPath);\n    while (deltaLogFileIt.hasNext()) {\n      FileStatus fileStatus = deltaLogFileIt.next();\n      if (FileNameUtils.isDeltaFile(fileStatus.getPath())) {\n        lastKnownBackfilledVersion =\n          Optional.of(FileNameUtils.deltaVersion(fileStatus.getPath()));\n      }\n    }\n    return lastKnownBackfilledVersion;\n  }\n\n  @Override\n  public CommitResponse commit(\n      LogStore logStore,\n      Configuration hadoopConf,\n      TableDescriptor tableDesc,\n      long commitVersion,\n      Iterator<String> actions,\n      UpdatedActions updatedActions) throws CommitFailedException {\n    return commitImpl(\n      logStore,\n      hadoopConf,\n      tableDesc,\n      commitVersion,\n      actions,\n      updatedActions);\n  }\n\n  /**\n   * Commits the provided actions as the specified version. The steps are as follows.\n   *\n   * 1. Write the actions to a UUID-based commit file\n   * 2. In parallel to 1. determine the last known backfilled version.\n   *    If a backfill hint is provided, we verify that it exists via a single HEAD call. Otherwise,\n   *    the last known backfilled version is determined via a listing.\n   * 3. Send commit request to UC to commit the version and register backfills up to the\n   *    found last known backfilled version.\n   * 4. Backfill all unbackfilled commits (including the latest one made in this call)\n   *    asynchronously.\n   *    A getCommits call is made to UC to retrieve all currently unbackfilled commits.\n   */\n  protected CommitResponse commitImpl(\n      LogStore logStore,\n      Configuration hadoopConf,\n      TableDescriptor tableDesc,\n      long commitVersion,\n      Iterator<String> actions,\n      UpdatedActions updatedActions) throws CommitFailedException {\n    Path logPath = tableDesc.getLogPath();\n    Map<String, String> coordinatedCommitsTableConf = tableDesc.getTableConf();\n    checkVersionSupported(coordinatedCommitsTableConf, false /* compareRead */);\n    // Writes may also have to perform reads to determine the last known backfilled\n    // version/the commits to backfill in case we don't have a backfill hint. To\n    // prevent to write to succeed but then fail the read, we do the read protocol\n    // version check here.\n    checkVersionSupported(coordinatedCommitsTableConf, true /* compareRead */);\n\n    if (commitVersion == 0) {\n      throw new CommitFailedException(\n        false /* retryable */,\n        false /* conflict */,\n        \"Commit version 0 must go via filesystem.\");\n    }\n\n    long startTimeMs = System.currentTimeMillis();\n    Map<String, Object> eventData = new HashMap<>();\n    eventData.put(\"commitVersion\", commitVersion);\n    eventData.put(\"coordinatedCommitsTableConf\", coordinatedCommitsTableConf);\n    eventData.put(\"updatedActions\", updatedActions);\n\n    BiConsumer<Optional<Throwable>, String> recordUsageLog = (exception, opType) -> {\n      exception.ifPresent(throwable -> {\n        eventData.put(\"exceptionClass\", throwable.getClass().getName());\n        eventData.put(\"exceptionString\", exceptionString(throwable));\n      });\n      eventData.put(\"totalTimeTakenMs\", System.currentTimeMillis() - startTimeMs);\n      recordDeltaEvent(opType, eventData, logPath.getParent());\n    };\n\n    // After commit 0, the table ID must exist in UC\n    String tableId = extractUCTableId(tableDesc);\n    LOG.info(\"Attempting to commit version \" + commitVersion + \" to table \" + tableId);\n\n    // Asynchronously verify/retrieve the last known backfilled version\n    // Using AtomicLong instead of Long because we need to update the value in the lambda\n    // and \"Variable used in lambda expression should be final or effectively final\".\n    AtomicLong timeSpentInGettingLastKnownBackfilledVersion =\n      new AtomicLong(System.currentTimeMillis());\n    Future<Long> lastKnownBackfilledVersionFuture;\n    try {\n      lastKnownBackfilledVersionFuture = executeAsync(() -> {\n        long foundVersion = getLastKnownBackfilledVersion(\n          commitVersion,\n          hadoopConf,\n          logStore,\n          tableDesc);\n        timeSpentInGettingLastKnownBackfilledVersion.getAndUpdate(start ->\n          System.currentTimeMillis() - start);\n        return foundVersion;\n      });\n    } catch (Exception e) {\n      // Synchronously verify/retrieve last known backfilled version.\n      LOG.warn(\"Error while submitting task to verify/retrieve last known backfilled version \" +\n        \"due to: \" + exceptionString(e) + \". Verifying/retrieving synchronously\");\n      recordUsageLog.accept(\n        Optional.of(e),\n        UCCoordinatedCommitsUsageLogs.UC_BACKFILL_VALIDATION_FALLBACK_TO_SYNC);\n      long foundVersion = getLastKnownBackfilledVersion(\n        commitVersion,\n        hadoopConf,\n        logStore,\n        tableDesc);\n      timeSpentInGettingLastKnownBackfilledVersion.getAndUpdate(start ->\n        System.currentTimeMillis() - start);;\n      lastKnownBackfilledVersionFuture = CompletableFuture.completedFuture(foundVersion);\n    }\n\n    // In parallel to verifying/getting the last known backfilled version, write the commit file.\n    long writeStartTimeMs = System.currentTimeMillis();\n    FileStatus commitFile;\n    try {\n      commitFile = CoordinatedCommitsUtils.writeUnbackfilledCommitFile(\n        logStore,\n        hadoopConf,\n        logPath.toString(),\n        commitVersion,\n        actions,\n        UUID.randomUUID().toString()\n      );\n    } catch (IOException e) {\n      throw new CommitFailedException(\n        true /* retryable */,\n        false /* conflict */,\n        \"Failed to write commit file due to: \" + e.getMessage(),\n        e);\n    }\n    eventData.put(\"writeCommitFileTimeTakenMs\", System.currentTimeMillis() - writeStartTimeMs);\n\n    // Using AtomicLong instead of Long because we need to access the value in the lambda\n    // and \"Variable used in lambda expression should be final or effectively final\".\n    AtomicLong lastKnownBackfilledVersion = new AtomicLong();\n    try {\n      lastKnownBackfilledVersion.set(lastKnownBackfilledVersionFuture.get());\n    } catch (InterruptedException | ExecutionException e) {\n      throw new RuntimeException(e);\n    }\n    long commitTimestamp = updatedActions.getCommitInfo().getCommitTimestamp();\n    boolean disown = isDisownCommit(\n      updatedActions.getOldMetadata(),\n      updatedActions.getNewMetadata());\n    eventData.put(\"tableId\", tableId);\n    eventData.put(\"lastKnownBackfilledVersion\", lastKnownBackfilledVersion.get());\n    eventData.put(\"commitTimestamp\", commitTimestamp);\n    eventData.put(\"disown\", disown);\n    eventData.put(\n      \"timeSpentInGettingLastKnownBackfilledVersion\",\n      timeSpentInGettingLastKnownBackfilledVersion);\n\n    int transientErrorRetryCount = 0;\n    while (transientErrorRetryCount <= MAX_RETRIES_ON_TRANSIENT_ERROR) {\n      try {\n        commitToUC(\n          tableDesc,\n          logPath,\n          Optional.of(commitFile),\n          Optional.of(commitVersion),\n          Optional.of(commitTimestamp),\n          Optional.of(lastKnownBackfilledVersion.get()),\n          disown,\n          updatedActions.getNewMetadata() == updatedActions.getOldMetadata() || !SHOULD_PASS_METADATA_TO_UC ?\n            Optional.empty() :\n            Optional.of(updatedActions.getNewMetadata()),\n          updatedActions.getNewProtocol() == updatedActions.getOldProtocol() ?\n            Optional.empty() :\n            Optional.of(updatedActions.getNewProtocol())\n        );\n        break;\n      } catch (CommitFailedException cfe) {\n        if (transientErrorRetryCount > 0 && cfe.getConflict() && cfe.getRetryable() &&\n          hasSameContent(\n            logStore,\n            hadoopConf,\n            logPath,\n            CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, commitVersion),\n            commitFile.getPath())) {\n          // The commit was persisted in UC, but we did not get a response. Continue\n          // because the commit was successful\n          eventData.put(\"alreadyBackfilledCommitCausedConflict\", true);\n          break;\n        } else {\n          // Rethrow the exception here as is because the caller needs to handle it.\n          recordUsageLog.accept(Optional.of(cfe), UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS);\n          throw cfe;\n        }\n      } catch (IOException ioe) {\n        if (transientErrorRetryCount == MAX_RETRIES_ON_TRANSIENT_ERROR) {\n          // Rethrow exception in case we've reached the retry limit.\n          recordUsageLog.accept(Optional.of(ioe), UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS);\n          throw new CommitFailedException(\n            true /* retryable */,\n            false /* conflict */,\n            ioe.getMessage(),\n            ioe);\n        }\n        // Exponentially back off. The initial wait time is set to 100ms and the max retry count\n        // is 15. The max wait time is 1 min so overall, we'll be waiting for a max of ~8 min.\n        long sleepTime = Math.min(\n          TRANSIENT_ERROR_RETRY_INITIAL_WAIT_MS << transientErrorRetryCount,\n          TRANSIENT_ERROR_RETRY_MAX_WAIT_MS\n        );\n        LOG.info(\"Sleeping for \" + sleepTime + \"ms before retrying commit after transient error \" +\n          ioe.getMessage());\n        try {\n          Thread.sleep(sleepTime);\n        } catch (InterruptedException e) {\n          throw new RuntimeException(e);\n        }\n        transientErrorRetryCount++;\n        eventData.put(\"transientErrorRetryCount\", transientErrorRetryCount);\n      } catch (UpgradeNotAllowedException\n          unae) {\n        // This is translated to a non-retryable, non-conflicting commit failure.\n        recordUsageLog.accept(Optional.of(unae), UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS);\n        throw new CommitFailedException(\n          false /* retryable */,\n          false /* conflict */,\n          unae.getMessage(),\n          unae);\n      } catch (InvalidTargetTableException\n          itte) {\n        // Just rethrow, this will propagate to the user.\n        recordUsageLog.accept(Optional.of(itte), UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS);\n        throw new CommitFailedException(\n          false /* retryable */,\n          false /* conflict */,\n          itte.getMessage(),\n          itte);\n      } catch (CommitLimitReachedException\n          clre) {\n        // We attempt a full backfill and then retry the commit.\n        try {\n          AtomicReference<Exception> caughtException = new AtomicReference<>(null);\n          lastKnownBackfilledVersion.getAndUpdate(lastKnownBackfilledVersionVal -> {\n            try {\n              return attemptFullBackfill(\n                logStore,\n                hadoopConf,\n                tableDesc,\n                commitVersion,\n                tableId,\n                lastKnownBackfilledVersionVal,\n                eventData\n              );\n            } catch (Exception e) {\n              caughtException.set(e);\n              return lastKnownBackfilledVersionVal; // Return unchanged value on exception\n            }\n          });\n          if (caughtException.get() != null) {\n            throw caughtException.get();\n          }\n        } catch (Throwable e) {\n          recordUsageLog.accept(\n            Optional.of(e), UCCoordinatedCommitsUsageLogs.UC_FULL_BACKFILL_ATTEMPT_FAILED);\n          String message = String.format(\n            \"Commit limit reached (%s) for table %s. A full backfill attempt failed due to: %s\",\n            exceptionString(clre),\n            tableId,\n            exceptionString(e));\n          throw new CommitFailedException(\n            true /* retryable */,\n            false /* conflict */,\n            message,\n            clre);\n        }\n        eventData.put(\"lastKnownBackfilledVersion\", lastKnownBackfilledVersion.get());\n        eventData.put(\"encounteredCommitLimitReachedException\", true);\n        // Retry the commit as there should be space in UC now. We set isCommitLimitReachedRetry\n        // to true so that in case the full backfill attempt was unsuccessful in freeing up space\n        // in UC, we don't indefinitely retry but rather throw the CommitLimitReachedException.\n        // Don't increase transientErrorRetryCount as this is not a transient error.\n      } catch (UCCommitCoordinatorException\n          ucce) {\n        // Just rethrow, this will propagate to the user.\n        recordUsageLog.accept(Optional.of(ucce), UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS);\n        throw new CommitFailedException(\n          false /* retryable */,\n          false /* conflict */,\n          ucce.getMessage(),\n          ucce);\n      }\n    }\n\n    LOG.info(\"Successfully wrote \" + commitFile.getPath() + \" as commit \" + commitVersion +\n      \" to table \" + tableId);\n\n    // Asynchronously backfill everything up to the latest commit.\n    Callable<Void> doBackfill = () -> {\n      backfillToVersion(\n        logStore,\n        hadoopConf,\n        tableDesc,\n        commitVersion,\n        lastKnownBackfilledVersion.get()\n      );\n      return null;\n    };\n\n    try {\n      executeAsync(doBackfill);\n    } catch (Throwable e) {\n      if (LogStoreErrors.isFatal(e)) {\n        throw e;\n      }\n      // attempt a synchronous backfill\n      LOG.warn(\"Error while submitting backfill task: \" + exceptionString(e) +\n        \". Performing synchronous backfill now.\");\n      recordUsageLog.accept(\n        Optional.of(e),\n        UCCoordinatedCommitsUsageLogs.UC_BACKFILL_FALLBACK_TO_SYNC);\n      try {\n        doBackfill.call();\n      } catch (Throwable t) {\n        if (LogStoreErrors.isFatal(t)) {\n          throw new RuntimeException(t);\n        }\n      }\n    }\n\n    recordUsageLog.accept(Optional.empty(), UCCoordinatedCommitsUsageLogs.UC_COMMIT_STATS);\n    return new CommitResponse(new Commit(commitVersion, commitFile, commitTimestamp));\n  }\n\n  /**\n   * Attempts a full backfill of all currently unbackfilled versions in order to free\n   * up space in UC. After the attempt, will do a listing to find the new last known\n   * backfilled version and returns it.\n   */\n  protected long attemptFullBackfill(\n      LogStore logStore,\n      Configuration hadoopConf,\n      TableDescriptor tableDesc,\n      long commitVersion,\n      String tableId,\n      long lastKnownBackfilledVersion,\n      Map<String, Object> eventData) throws IOException,\n        UCCommitCoordinatorException,\n        CommitFailedException {\n    Path logPath = tableDesc.getLogPath();\n    LOG.info(\"Too many unbackfilled commits in UC at version {} for table at {} \" +\n      \"and ID {}. Last known backfill version is {}. Attempting a full backfill.\",\n      commitVersion, logPath, tableId, lastKnownBackfilledVersion);\n\n    long backfillStartTime = System.currentTimeMillis();\n      backfillToVersion(\n        logStore,\n        hadoopConf,\n        tableDesc,\n        commitVersion,\n        lastKnownBackfilledVersion\n      );\n    long backfillDuration = System.currentTimeMillis() - backfillStartTime;\n\n    long updatedLastKnownBackfilledVersion = getLastKnownBackfilledVersion(\n      commitVersion,\n      hadoopConf,\n      logStore,\n      tableDesc);\n\n    long commitStartTime = System.currentTimeMillis();\n    commitToUC(\n      tableDesc,\n      logPath,\n      Optional.empty() /* commitFile */,\n      Optional.empty() /* commitVersion */,\n      Optional.empty() /* commitTimestamp */,\n      Optional.of(updatedLastKnownBackfilledVersion),\n      true /* disown */,\n      Optional.empty() /* newMetadata */,\n      Optional.empty() /* newProtocol */\n    );\n    long commitDuration = System.currentTimeMillis() - commitStartTime;\n\n    recordDeltaEvent(\n      UCCoordinatedCommitsUsageLogs.UC_ATTEMPT_FULL_BACKFILL,\n      new HashMap<String, Object>(eventData) {{\n        put(\"commitVersion\", commitVersion);\n        put(\"coordinatedCommitsTableConf\", tableDesc.getTableConf());\n        put(\"lastKnownBackfilledVersion\", lastKnownBackfilledVersion);\n        put(\"updatedLastKnownBackfilledVersion\", updatedLastKnownBackfilledVersion);\n        put(\"tableId\", tableId);\n        put(\"backfillTime\", backfillDuration);\n        put(\"ucCommitTime\", commitDuration);\n      }},\n      logPath.getParent()\n    );\n    return updatedLastKnownBackfilledVersion;\n  }\n\n  protected void commitToUC(\n      TableDescriptor tableDesc,\n      Path logPath,\n      Optional<FileStatus> commitFile,\n      Optional<Long> commitVersion,\n      Optional<Long> commitTimestamp,\n      Optional<Long> lastKnownBackfilledVersion,\n      boolean disown,\n      Optional<AbstractMetadata> newMetadata,\n      Optional<AbstractProtocol> newProtocol\n  ) throws IOException, CommitFailedException, UCCommitCoordinatorException\n  {\n    Optional<Commit> commit = commitFile.map(f -> new Commit(\n      commitVersion.orElseThrow(() -> new IllegalArgumentException(\n        \"Commit version should be specified when commitFile is present\")),\n      f,\n      commitTimestamp.orElseThrow(() -> new IllegalArgumentException(\n        \"Commit timestamp should be specified when commitFile is present\"))\n    ));\n    ucClient.commit(\n      extractUCTableId(tableDesc),\n      CoordinatedCommitsUtils.getTablePath(logPath).toUri(),\n      commit,\n      lastKnownBackfilledVersion,\n      disown,\n      newMetadata,\n      newProtocol,\n      Optional.empty() /* uniform */\n    );\n  }\n\n  /**\n   * Detects whether the current commit is a downgrade (disown) commit by checking\n   * that the UC commit coordinator name is present in the old metadata but removed from\n   * the new metadata.\n   */\n  protected boolean isDisownCommit(AbstractMetadata oldMetadata, AbstractMetadata newMetadata) {\n    return CoordinatedCommitsUtils\n      .getCoordinatorName(oldMetadata)\n      .filter(\"unity-catalog\"::equals).isPresent() &&\n      !CoordinatedCommitsUtils.getCoordinatorName(newMetadata).isPresent();\n  }\n\n  /**\n   * This method provides idempotency under network failures by verifying whether the currently\n   * attempted commit already exists as a backfilled commit. This prevents duplicate data from\n   * being written when UC returns a retryable conflict for a commit that was actually successful\n   * but the client didn't receive the success response.\n   *\n   * Failure sequence requiring this check:\n   * 1. Client attempts to make commit v.\n   * 2. UC persists the commit in its database but the connection to the client breaks.\n   * 3. The client receives a transient error (retryable=true, conflict=false).\n   * 4. Before retrying, a concurrent client commits v + 1 and backfills v.\n   * 5. Another subsequent commit registers the backfill of v with UC, leading UC to\n   *    delete the commit for v from its database.\n   * 6. Now this client retries commit v (without conflict resolution since conflict=false\n   *    in step 3).\n   * 7. UC rejects the commit because v {@literal <=} latest_table_version and returns a retryable\n   *    conflict (retryable=true, conflict=true).\n   *\n   * Without this check, Delta's default response to retryable=true, conflict=true would be to\n   * rebase the commit on top of the latest table version and retry, effectively trying to\n   * commit the contents of v as v+2. This would result in duplicate data being written.\n   *\n   * This method prevents that by checking if the backfilled commit (v.json) has the same\n   * content as our retry attempt (v.{@literal <uuid>}.json). If yes, we know our original commit\n   * succeeded and can safely ignore the conflict and exit early without rebasing.\n   *\n   * Below is a concrete example of the failure and retry sequence:\n   * - Attempt 1: Try to commit v. UC responds with retryable=true, conflict=false under\n   *              network failure.\n   * - Attempt 2: Try to commit v without conflict resolution since conflict=false in attempt-1.\n   *              UC responds with retryable=true, conflict=true in the above scenario.\n   *              (i.e. v is backfilled and latest version is v+1).\n   * - Fix: Compare v.{@literal <uuid>}.json and v.json and *early exit* here.\n   * - Attempt 3: [Without fix] Rebase, conflict-resolution + Try to commit v+2\n   *              {@literal =>} double-commit for contents of v {@literal =>} bug.\n   */\n  protected boolean hasSameContent(\n      LogStore logStore,\n      Configuration hadoopConf,\n      Path logPath,\n      Path backfilledCommit,\n      Path unbackfilledCommit) {\n    try {\n      FileSystem fs = logPath.getFileSystem(hadoopConf);\n      if (fs.getFileStatus(backfilledCommit).getLen() !=\n          fs.getFileStatus(unbackfilledCommit).getLen()) {\n        return false;\n      }\n    } catch (FileNotFoundException e) {\n      // If we get a FileNotFoundException, it should be for the backfilled\n      // commit because we are only calling this method from commit() at the moment,\n      // which means we just wrote the unbackfilled commit.\n      return false;\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n\n    // Compare content.\n    try (CloseableIterator<String> contentBackfilled = logStore.read(backfilledCommit, hadoopConf);\n         CloseableIterator<String> contentUnbackfilled =\n           logStore.read(unbackfilledCommit, hadoopConf)) {\n      while (contentUnbackfilled.hasNext() && contentBackfilled.hasNext()) {\n        if (!contentUnbackfilled.next().equals(contentBackfilled.next())) {\n          return false;\n        }\n      }\n      return !contentBackfilled.hasNext() && !contentUnbackfilled.hasNext();\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public GetCommitsResponse getCommits(\n      TableDescriptor tableDesc,\n      Long startVersion,\n      Long endVersion) {\n    checkVersionSupported(tableDesc.getTableConf(), true /* compareRead */);\n    GetCommitsResponse resp = getCommitsFromUCImpl(\n      tableDesc,\n      Optional.ofNullable(startVersion),\n      Optional.ofNullable(endVersion));\n    // Sort by version just in case commits in the response from UC aren't sorted.\n    List<Commit> sortedCommits =\n      resp\n        .getCommits()\n        .stream()\n        .sorted(Comparator.comparingLong(Commit::getVersion))\n        .collect(Collectors.toList());\n    return new GetCommitsResponse(sortedCommits, resp.getLatestTableVersion());\n  }\n\n  protected GetCommitsResponse getCommitsFromUCImpl(\n      TableDescriptor tableDesc,\n      Optional<Long> startVersion,\n      Optional<Long> endVersion) {\n    try {\n      return ucClient.getCommits(\n        extractUCTableId(tableDesc),\n        CoordinatedCommitsUtils.getTablePath(tableDesc.getLogPath()).toUri(),\n        startVersion,\n        endVersion);\n    } catch (IOException | UCCommitCoordinatorException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public void backfillToVersion(\n      LogStore logStore,\n      Configuration hadoopConf,\n      TableDescriptor tableDesc,\n      long version,\n      Long lastKnownBackfilledVersion) throws IOException {\n    // backfillToVersion currently does not depend on write. However, it is\n    // technically a write operation, so we also add a write version check here\n    // in case we ever introduce a write dependency.\n    checkVersionSupported(tableDesc.getTableConf(), true /* compareRead */);\n    checkVersionSupported(tableDesc.getTableConf(), false /* compareRead */);\n\n    Path logPath = tableDesc.getLogPath();\n    String tableId = extractUCTableId(tableDesc);\n    long startVersion = (lastKnownBackfilledVersion == null) ? 0L : lastKnownBackfilledVersion;\n    long startTimeMs = System.currentTimeMillis();\n    LOG.info(\"Backfilling {}: startVersion {} to endVersion {}\", tableId, startVersion, version);\n\n    // Check that the last known backfilled version actually exists if it\n    // has been specified. If it doesn't exist, we fail the backfill. If it\n    // hasn't been specified backfill everything that hasn't been backfilled yet.\n    if (lastKnownBackfilledVersion != null) {\n      FileSystem fs = logPath.getFileSystem(hadoopConf);\n      // Check that the last known backfilled version actually exists.\n      if (!fs.exists(CoordinatedCommitsUtils\n          .getBackfilledDeltaFilePath(logPath, lastKnownBackfilledVersion))) {\n        LOG.error(\"Specified last known backfilled version {} does not exist for table {}\",\n            lastKnownBackfilledVersion, tableId);\n        recordDeltaEvent(\n          UCCoordinatedCommitsUsageLogs.UC_BACKFILL_DOES_NOT_EXIST,\n          new HashMap<String, Object>() {{\n            put(\"lastKnownBackfilledVersion\", lastKnownBackfilledVersion);\n            put(\"version\", version);\n            put(\"tableConf\", tableDesc.getTableConf());\n          }},\n          logPath.getParent()\n        );\n        throw new IllegalStateException(\"Last known backfilled version \" +\n          lastKnownBackfilledVersion + \" doesn't exist for table at \" + logPath);\n      }\n    }\n    GetCommitsResponse commitsResponse = getCommits(tableDesc, lastKnownBackfilledVersion, version);\n    for (Commit commit : commitsResponse.getCommits()) {\n      boolean backfillResult = backfillSingleCommit(\n        logStore,\n        hadoopConf,\n        logPath,\n        commit.getVersion(),\n        commit.getFileStatus(),\n        false /* failOnException */);\n      if (!backfillResult) {\n        break;\n      }\n    }\n\n    recordDeltaEvent(\n      UCCoordinatedCommitsUsageLogs.UC_BACKFILL_TO_VERSION,\n      new HashMap<String, Object>() {{\n        put(\"coordinatedCommitsTableConf\", tableDesc.getTableConf());\n        put(\"totalTimeTakenMs\", System.currentTimeMillis() - startTimeMs);\n        put(\"lastKnownBackfilledVersion\", lastKnownBackfilledVersion);\n        put(\"tableId\", tableId);\n        put(\"version\", version);\n      }},\n      logPath.getParent()\n    );\n  }\n\n  /**\n   * Backfill the specified commit as the target version. Returns true if the\n   * backfill was successful (or the backfilled file already existed) and false\n   * in case the backfill failed.\n   */\n  protected boolean backfillSingleCommit(\n      LogStore logStore,\n      Configuration hadoopConf,\n      Path logPath,\n      long version,\n      FileStatus fileStatus,\n      Boolean failOnException) {\n    Path targetFile = CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, version);\n    try (CloseableIterator<String> commitContentIterator =\n           logStore.read(fileStatus.getPath(), hadoopConf)) {\n      // Use put-if-absent for backfills so that files are not overwritten and the\n      // modification time does not change for already backfilled files.\n      logStore.write(targetFile, commitContentIterator, false /* overwrite */, hadoopConf);\n    } catch (FileAlreadyExistsException e) {\n      LOG.info(\"The backfilled file {} already exists.\", targetFile);\n    } catch (Exception e) {\n      if (LogStoreErrors.isFatal(e) || failOnException) {\n        throw new RuntimeException(e);\n      }\n      LOG.warn(\"Backfill for table at {} failed for version {} due to: {}\",\n        logPath, version, exceptionString(e));\n      recordDeltaEvent(\n        UCCoordinatedCommitsUsageLogs.UC_BACKFILL_FAILED,\n        new HashMap<String, Object>() {{\n          put(\"version\", version);\n          put(\"exceptionClass\", e.getClass().getName());\n          put(\"exceptionString\", exceptionString(e));\n        }},\n        logPath.getParent()\n      );\n      return false;\n    }\n    return true;\n  }\n\n  @Override\n  public boolean semanticEquals(CommitCoordinatorClient other) {\n    if (!(other instanceof UCCommitCoordinatorClient)) {\n      return false;\n    }\n    UCCommitCoordinatorClient otherStore = (UCCommitCoordinatorClient) other;\n    return this.conf == otherStore.conf;\n  }\n\n  protected void recordDeltaEvent(String opType, Object data, Path path) {\n    LOG.info(\"Delta event recorded with opType={}, data={}, and path={}\", opType, data, path);\n  }\n\n  protected String exceptionString(Throwable e) {\n    if (e == null) {\n      return \"\";\n    } else {\n      StringWriter stringWriter = new StringWriter();\n      e.printStackTrace(new PrintWriter(stringWriter));\n      return stringWriter.toString();\n    }\n  }\n\n  protected void checkVersionSupported(Map<String, String> tableConf, boolean compareRead) {\n    int readVersion = Integer.parseInt(tableConf.getOrDefault(READ_VERSION_KEY, \"0\"));\n    int writeVersion = Integer.parseInt(tableConf.getOrDefault(WRITE_VERSION_KEY, \"0\"));\n    int targetVersion = compareRead ? readVersion : writeVersion;\n    int supportedVersion = compareRead ? SUPPORTED_READ_VERSION : SUPPORTED_WRITE_VERSION;\n    String op = compareRead ? \"read\" : \"write\";\n    if (supportedVersion != targetVersion) {\n      throw new UnsupportedOperationException(\"The version of the UC commit coordinator protocol\" +\n      \" is not supported by this version of the UC commit coordinator client. Please upgrade\" +\n      \" the commit coordinator client to \" + op + \" this table.\");\n    }\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UCCommitCoordinatorException.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\n/**\n * Base class for all exceptions thrown by the UC client from coordinated commits-related APIs.\n */\npublic abstract class UCCommitCoordinatorException extends Exception {\n  public UCCommitCoordinatorException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UCCoordinatedCommitsUsageLogs.java",
    "content": "package io.delta.storage.commit.uccommitcoordinator;\n\n/** Class containing usage logs emitted by Coordinated Commits. */\npublic class UCCoordinatedCommitsUsageLogs {\n\n  // Common prefix for all coordinated-commits usage logs.\n  private static final String PREFIX = \"delta.coordinatedCommits\";\n\n  // Usage log emitted after backfilling to a version.\n  public static final String UC_BACKFILL_TO_VERSION = PREFIX + \".uc.backfillToVersion\";\n\n  // Usage log emitted if the specified last known backfilled version does not exist.\n  public static final String UC_BACKFILL_DOES_NOT_EXIST = PREFIX + \".uc.backfillDoesNotExist\";\n\n  // Usage log emitted if a backfill attempt for a single file failed.\n  public static final String UC_BACKFILL_FAILED = PREFIX + \".uc.backfillFailed\";\n\n  // Usage log emitted when the last known backfilled version cannot be determined from the last\n  // `BACKFILL_LISTING_OFFSET` commits.\n  public static final String UC_LAST_KNOWN_BACKFILLED_VERSION_NOT_FOUND =\n    PREFIX + \".uc.lastKnownBackfilledVersionNotFound\";\n\n  // Usage log emitted if UC commit coordinator client falls back to synchronous backfill.\n  public static final String UC_BACKFILL_VALIDATION_FALLBACK_TO_SYNC =\n    PREFIX + \".uc.backfillValidation.fallbackToSync\";\n\n  // Usage log emitted when commit limit is reached, and we attempt a full backfill.\n  public static final String UC_ATTEMPT_FULL_BACKFILL = PREFIX + \".uc.attemptFullBackfill\";\n\n  // Usage log emitted if UC commit coordinator client falls back to synchronous backfill.\n  public static final String UC_BACKFILL_FALLBACK_TO_SYNC = PREFIX + \".uc.backfill.fallbackToSync\";\n\n  // Usage log emitted as part of [[UCCommitCoordinatorClient.commit]] call.\n  public static final String UC_COMMIT_STATS = PREFIX + \".uc.commitStats\";\n\n  // Usage log emitted when a full backfill attempt has failed\n  public static final String UC_FULL_BACKFILL_ATTEMPT_FAILED =\n    PREFIX + \".uc.fullBackfillAttemptFailed\";\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UCRestClientPayload.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport io.delta.storage.commit.Commit;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport io.delta.storage.commit.uniform.UniformMetadata;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.Path;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Container for internal REST classes used by UCTokenBasedRestClient.\n * Encapsulates all necessary classes for JSON serialization/deserialization.\n */\nclass UCRestClientPayload {\n\n  // ==============================\n  // CommitInfo Class\n  // ==============================\n  static class CommitInfo {\n    Long version;\n    Long timestamp;\n    String fileName;\n    Long fileSize;\n    Long fileModificationTimestamp;\n    Boolean isDisownCommit;\n\n    static CommitInfo fromCommit(Commit externalCommit, boolean isDisownCommit) {\n      if (externalCommit == null) {\n        throw new IllegalArgumentException(\"externalCommit cannot be null\");\n      }\n      if (externalCommit.getFileStatus() == null) {\n        throw new IllegalArgumentException(\"externalCommit.getFileStatus() cannot be null\");\n      }\n\n      CommitInfo commitInfo = new CommitInfo();\n      commitInfo.version = externalCommit.getVersion();\n      commitInfo.timestamp = externalCommit.getCommitTimestamp();\n      commitInfo.fileName = externalCommit.getFileStatus().getPath().getName();\n      commitInfo.fileSize = externalCommit.getFileStatus().getLen();\n      commitInfo.fileModificationTimestamp = externalCommit.getFileStatus().getModificationTime();\n      commitInfo.isDisownCommit = isDisownCommit;\n      return commitInfo;\n    }\n\n    static Commit toCommit(CommitInfo commitInfo, Path basePath) {\n      FileStatus fileStatus = new FileStatus(\n        commitInfo.fileSize,\n        false /* isdir */,\n        0 /* block_replication */,\n        0 /* blocksize */,\n        commitInfo.fileModificationTimestamp,\n        new Path(basePath, commitInfo.fileName));\n      return new Commit(commitInfo.version, fileStatus, commitInfo.timestamp);\n    }\n  }\n\n  // ==============================\n  // Protocol Class\n  // ==============================\n  static class Protocol {\n    Integer minReaderVersion;\n    Integer minWriterVersion;\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    List<String> readerFeatures;\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    List<String> writerFeatures;\n\n    static Protocol fromAbstractProtocol(AbstractProtocol externalProtocol) {\n      if (externalProtocol == null) {\n        throw new IllegalArgumentException(\"externalProtocol cannot be null\");\n      }\n\n      Protocol protocol = new Protocol();\n      protocol.minReaderVersion = externalProtocol.getMinReaderVersion();\n      protocol.minWriterVersion = externalProtocol.getMinWriterVersion();\n      protocol.readerFeatures = new ArrayList<>(externalProtocol.getReaderFeatures());\n      protocol.writerFeatures = new ArrayList<>(externalProtocol.getWriterFeatures());\n\n      return protocol;\n    }\n  }\n\n  // ==============================\n  // Metadata Class\n  // ==============================\n  static class Metadata {\n    String deltaTableId;\n    String name;\n    String description;\n    String provider;\n    OptionsKVPairs formatOptions;\n    ColumnInfos schema;\n    List<String> partitionColumns;\n    PropertiesKVPairs properties;\n    String createdTime;\n\n    static Metadata fromAbstractMetadata(AbstractMetadata externalMetadata) {\n      if (externalMetadata == null) {\n        throw new IllegalArgumentException(\"externalMetadata cannot be null\");\n      }\n\n      Metadata metadata = new Metadata();\n      metadata.deltaTableId = externalMetadata.getId();\n      metadata.name = externalMetadata.getName();\n      metadata.description = externalMetadata.getDescription();\n      metadata.provider = externalMetadata.getProvider();\n      metadata.formatOptions = OptionsKVPairs.fromFormatOptions(\n        externalMetadata.getFormatOptions());\n      metadata.schema = ColumnInfos.fromSchemaString(externalMetadata.getSchemaString());\n      metadata.partitionColumns = externalMetadata.getPartitionColumns();\n      metadata.properties = PropertiesKVPairs.fromProperties(externalMetadata.getConfiguration());\n      metadata.createdTime = externalMetadata.getCreatedTime().toString(); // Assuming ISO format\n\n      return metadata;\n    }\n  }\n\n  // ==============================\n  // OptionsKVPairs Class\n  // ==============================\n  static class OptionsKVPairs {\n    Map<String, String> options;\n\n    static OptionsKVPairs fromFormatOptions(Map<String, String> externalOptions) {\n      if (externalOptions == null) {\n        throw new IllegalArgumentException(\"externalOptions cannot be null\");\n      }\n\n      OptionsKVPairs kvPairs = new OptionsKVPairs();\n      kvPairs.options = externalOptions;\n      return kvPairs;\n    }\n  }\n\n  // ==============================\n  // PropertiesKVPairs Class\n  // ==============================\n  static class PropertiesKVPairs {\n    Map<String, String> properties;\n\n    static PropertiesKVPairs fromProperties(Map<String, String> externalProperties) {\n      if (externalProperties == null) {\n        throw new IllegalArgumentException(\"externalProperties cannot be null\");\n      }\n\n      PropertiesKVPairs kvPairs = new PropertiesKVPairs();\n      kvPairs.properties = externalProperties;\n      return kvPairs;\n    }\n  }\n\n  // ==============================\n  // ColumnInfos Class\n  // ==============================\n  static class ColumnInfos {\n    List<ColumnInfo> columns;\n\n    static ColumnInfos fromSchemaString(String schemaString) {\n      // TODO: Implement actual schema parsing logic based on schema format\n      return null;\n    }\n\n    static class ColumnInfo {\n      String name;\n      String type;\n      Boolean nullable;\n\n      static ColumnInfo fromColumnDetails(String name, String type, Boolean nullable) {\n        if (name == null || type == null || nullable == null) {\n          throw new IllegalArgumentException(\"Column details cannot be null\");\n        }\n\n        ColumnInfo columnInfo = new ColumnInfo();\n        columnInfo.name = name;\n        columnInfo.type = type;\n        columnInfo.nullable = nullable;\n        return columnInfo;\n      }\n    }\n  }\n\n  // ==============================\n  // IcebergMetadata Class\n  // ==============================\n  static class IcebergMetadata {\n    String metadataLocation;\n    Long convertedDeltaVersion;\n    String convertedDeltaTimestamp;\n\n    static IcebergMetadata fromIcebergMetadata(io.delta.storage.commit.uniform.IcebergMetadata icebergMetadata) {\n      if (icebergMetadata == null) {\n        throw new IllegalArgumentException(\"icebergMetadata cannot be null\");\n      }\n\n      IcebergMetadata iceberg = new IcebergMetadata();\n      iceberg.metadataLocation = icebergMetadata.getMetadataLocation();\n      iceberg.convertedDeltaVersion = icebergMetadata.getConvertedDeltaVersion();\n      iceberg.convertedDeltaTimestamp = icebergMetadata.getConvertedDeltaTimestamp();\n      return iceberg;\n    }\n  }\n\n  // ==============================\n  // Uniform Class\n  // ==============================\n  static class Uniform {\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    IcebergMetadata iceberg;\n\n    static Uniform fromUniformMetadata(UniformMetadata uniformMetadata) {\n      Uniform uniform = new Uniform();\n      uniformMetadata.getIcebergMetadata().ifPresent(\n          icebergMeta -> uniform.iceberg = IcebergMetadata.fromIcebergMetadata(icebergMeta));\n      return uniform;\n    }\n  }\n\n  // ==============================\n  // CommitRequest Class\n  // ==============================\n  static class CommitRequest {\n    String tableId;\n    String tableUri;\n    CommitInfo commitInfo;\n    Long latestBackfilledVersion;\n    Metadata metadata;\n    Protocol protocol;\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    Uniform uniform;\n  }\n\n  // ==============================\n  // GetCommitsRequest Class\n  // ==============================\n  static class GetCommitsRequest {\n    String tableId;\n    String tableUri;\n    Long startVersion;\n    Long endVersion;\n  }\n\n  // ==============================\n  // RestGetCommitsResponse Class\n  // ==============================\n  static class RestGetCommitsResponse {\n    public List<CommitInfo> commits;\n    public Long latestTableVersion;\n  }\n\n  // ==============================\n  // GetMetastoreSummaryResponse Class\n  // ==============================\n  static class GetMetastoreSummaryResponse {\n    String metastoreId;\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClient.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\nimport io.delta.storage.commit.Commit;\nimport io.delta.storage.commit.CommitFailedException;\nimport io.delta.storage.commit.CoordinatedCommitsUtils;\nimport io.delta.storage.commit.GetCommitsResponse;\nimport io.delta.storage.commit.actions.AbstractMetadata;\nimport io.delta.storage.commit.actions.AbstractProtocol;\nimport io.delta.storage.commit.uniform.IcebergMetadata;\nimport io.delta.storage.commit.uniform.UniformMetadata;\nimport io.unitycatalog.client.ApiClient;\nimport io.unitycatalog.client.ApiClientBuilder;\nimport io.unitycatalog.client.ApiException;\nimport io.unitycatalog.client.api.DeltaCommitsApi;\nimport io.unitycatalog.client.api.MetastoresApi;\nimport io.unitycatalog.client.auth.TokenProvider;\nimport io.unitycatalog.client.model.DeltaCommit;\nimport io.unitycatalog.client.model.DeltaCommitInfo;\nimport io.unitycatalog.client.model.DeltaCommitMetadataProperties;\nimport io.unitycatalog.client.model.DeltaGetCommits;\nimport io.unitycatalog.client.model.DeltaGetCommitsResponse;\nimport io.unitycatalog.client.model.DeltaMetadata;\nimport io.unitycatalog.client.model.DeltaUniform;\nimport io.unitycatalog.client.model.DeltaUniformIceberg;\nimport io.unitycatalog.client.model.GetMetastoreSummaryResponse;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.Path;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.*;\n\n/**\n * A REST client implementation of {@link UCClient} for interacting with Unity Catalog's commit\n * coordination service. This client uses the Unity Catalog SDK with TokenProvider-based\n * authentication for managing Delta table commits and metadata.\n *\n * <p>The client handles the following primary operations:\n * <ul>\n *   <li>Retrieving metastore information</li>\n *   <li>Committing changes to Delta tables</li>\n *   <li>Fetching unbackfilled commit histories</li>\n * </ul>\n *\n * <p>All requests are authenticated using a TokenProvider that generates Bearer tokens dynamically.\n * The client uses the Unity Catalog SDK's {@link DeltaCommitsApi} and {@link MetastoresApi} for\n * API interactions.\n *\n * <p>Usage example:\n * <pre>{@code\n * TokenProvider tokenProvider = ... // Create or configure TokenProvider\n * try (UCTokenBasedRestClient client = new UCTokenBasedRestClient(baseUri, tokenProvider, Map.of())) {\n *     String metastoreId = client.getMetastoreId();\n *     // Perform operations with the client...\n * }\n * }</pre>\n *\n * @see UCClient\n * @see Commit\n * @see GetCommitsResponse\n * @see TokenProvider\n */\npublic class UCTokenBasedRestClient implements UCClient {\n\n  private DeltaCommitsApi deltaCommitsApi;\n  private MetastoresApi metastoresApi;\n\n  // HTTP status codes for error handling\n  private static final int HTTP_BAD_REQUEST = 400;\n  private static final int HTTP_NOT_FOUND = 404;\n  private static final int HTTP_CONFLICT = 409;\n  private static final int HTTP_TOO_MANY_REQUESTS = 429;\n\n  /**\n   * Constructs a new UCTokenBasedRestClient with the specified base URI, TokenProvider,\n   * and application version information for telemetry.\n   *\n   * @param baseUri The base URI of the Unity Catalog server\n   * @param tokenProvider The TokenProvider to use for authentication\n   * @param appVersions A map of application name to version string\n   *                    (e.g. {@code \"Delta\" -> \"4.0.0\"}). Each entry is\n   *                    registered for User-Agent telemetry. May be empty.\n   */\n  public UCTokenBasedRestClient(\n      String baseUri,\n      TokenProvider tokenProvider,\n      Map<String, String> appVersions) {\n    Objects.requireNonNull(baseUri, \"baseUri must not be null\");\n    Objects.requireNonNull(tokenProvider, \"tokenProvider must not be null\");\n    Objects.requireNonNull(appVersions, \"appVersions must not be null\");\n\n    ApiClientBuilder builder = ApiClientBuilder.create()\n        .uri(baseUri)\n        .tokenProvider(tokenProvider);\n\n    appVersions.forEach((name, version) -> {\n      if (version != null) {\n        builder.addAppVersion(name, version);\n      }\n    });\n\n    ApiClient apiClient = builder.build();\n    this.deltaCommitsApi = new DeltaCommitsApi(apiClient);\n    this.metastoresApi = new MetastoresApi(apiClient);\n  }\n\n  /**\n   * Ensures the client has not been closed. Must be called before any API operation.\n   */\n  private void ensureOpen() {\n    if (deltaCommitsApi == null || metastoresApi == null) {\n      throw new IllegalStateException(\"UCTokenBasedRestClient has been closed.\");\n    }\n  }\n\n  @Override\n  public String getMetastoreId() throws IOException {\n    ensureOpen();\n    try {\n      GetMetastoreSummaryResponse response = metastoresApi.summary();\n      return response.getMetastoreId();\n    } catch (ApiException e) {\n      throw new IOException(\n          String.format(\"Failed to get metastore ID (HTTP %s): \", e.getCode()), e);\n    }\n  }\n\n  @Override\n  public void commit(\n      String tableId,\n      URI tableUri,\n      Optional<Commit> commit,\n      Optional<Long> lastKnownBackfilledVersion,\n      boolean disown,\n      Optional<AbstractMetadata> newMetadata,\n      Optional<AbstractProtocol> newProtocol,\n      Optional<UniformMetadata> uniform\n  ) throws IOException, CommitFailedException, UCCommitCoordinatorException {\n    ensureOpen();\n    Objects.requireNonNull(tableId, \"tableId must not be null.\");\n    Objects.requireNonNull(tableUri, \"tableUri must not be null.\");\n\n    // Build the DeltaCommit request using SDK models\n    DeltaCommit deltaCommit = new DeltaCommit()\n        .tableId(tableId)\n        .tableUri(tableUri.toString());\n\n    // Add commit info if present\n    commit.ifPresent(c -> deltaCommit.commitInfo(toDeltaCommitInfo(c)));\n\n    // Add latest backfilled version if present\n    lastKnownBackfilledVersion.ifPresent(deltaCommit::latestBackfilledVersion);\n\n    // Add metadata if present\n    newMetadata.ifPresent(m -> deltaCommit.metadata(toDeltaMetadata(m)));\n\n    // Add uniform metadata if present\n    uniform.flatMap(u -> u.getIcebergMetadata().map(this::toDeltaUniformIceberg))\n        .ifPresent(iceberg -> deltaCommit.uniform(new DeltaUniform().iceberg(iceberg)));\n\n    // Note: protocol and disown are not part of the DeltaCommit schema in the Unity Catalog\n    // OpenAPI spec. They are intentionally not sent.\n\n    try {\n      deltaCommitsApi.commit(deltaCommit);\n    } catch (ApiException e) {\n      handleCommitException(e);\n    }\n  }\n\n  @Override\n  public GetCommitsResponse getCommits(\n      String tableId,\n      URI tableUri,\n      Optional<Long> startVersion,\n      Optional<Long> endVersion) throws IOException, UCCommitCoordinatorException {\n    ensureOpen();\n    Objects.requireNonNull(tableId, \"tableId must not be null.\");\n    Objects.requireNonNull(tableUri, \"tableUri must not be null.\");\n\n    // Build the DeltaGetCommits request using SDK models\n    DeltaGetCommits request = new DeltaGetCommits()\n        .tableId(tableId)\n        .tableUri(tableUri.toString())\n        .startVersion(startVersion.orElse(0L));\n\n    endVersion.ifPresent(request::endVersion);\n\n    try {\n      DeltaGetCommitsResponse response = deltaCommitsApi.getCommits(request);\n      return toGetCommitsResponse(response, tableUri);\n    } catch (ApiException e) {\n      int statusCode = e.getCode();\n      String responseBody = e.getResponseBody();\n\n      if (statusCode == HTTP_NOT_FOUND) {\n        throw new InvalidTargetTableException(\n            String.format(\"Invalid Target Table (HTTP %s) due to: %s\", statusCode, responseBody));\n      } else {\n        throw new IOException(\n            String.format(\"Unexpected getCommits failure (HTTP %s): due to: %s\", statusCode,\n                responseBody), e);\n      }\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    // Nulling out the API instances makes them eligible for GC. Once garbage collected,\n    // the underlying connection pool is freed and destroyed.\n    this.deltaCommitsApi = null;\n    this.metastoresApi = null;\n  }\n\n  /**\n   * Converts a Delta {@link Commit} to a Unity Catalog SDK {@link DeltaCommitInfo}.\n   *\n   * @param commit The Delta commit to convert\n   * @return The converted DeltaCommitInfo\n   */\n  private DeltaCommitInfo toDeltaCommitInfo(Commit commit) {\n    if (commit == null) {\n      throw new IllegalArgumentException(\"commit cannot be null\");\n    }\n    if (commit.getFileStatus() == null) {\n      throw new IllegalArgumentException(\"commit.getFileStatus() cannot be null\");\n    }\n\n    return new DeltaCommitInfo()\n        .version(commit.getVersion())\n        .timestamp(commit.getCommitTimestamp())\n        .fileName(commit.getFileStatus().getPath().getName())\n        .fileSize(commit.getFileStatus().getLen())\n        .fileModificationTimestamp(commit.getFileStatus().getModificationTime());\n  }\n\n  /**\n   * Converts a Delta {@link IcebergMetadata} to a Unity Catalog SDK\n   * {@link DeltaUniformIceberg}.\n   *\n   * <p>Field mapping (Delta internal -> OpenAPI snake_case):\n   * <ul>\n   *   <li>metadataLocation -> metadata_location</li>\n   *   <li>convertedDeltaVersion -> converted_delta_version</li>\n   *   <li>convertedDeltaTimestamp -> converted_delta_timestamp</li>\n   * </ul>\n   */\n  private DeltaUniformIceberg toDeltaUniformIceberg(IcebergMetadata iceberg) {\n    return new DeltaUniformIceberg()\n        .metadataLocation(URI.create(iceberg.getMetadataLocation()))\n        .convertedDeltaVersion(iceberg.getConvertedDeltaVersion())\n        .convertedDeltaTimestamp(iceberg.getConvertedDeltaTimestamp());\n  }\n\n  /**\n   * Converts an {@link AbstractMetadata} to a Unity Catalog SDK {@link DeltaMetadata}.\n   *\n   * @param metadata The abstract metadata to convert\n   * @return The converted DeltaMetadata\n   */\n  private DeltaMetadata toDeltaMetadata(AbstractMetadata metadata) {\n    if (metadata == null) {\n      throw new IllegalArgumentException(\"metadata cannot be null\");\n    }\n\n    DeltaMetadata deltaMetadata = new DeltaMetadata()\n        .description(metadata.getDescription());\n\n    // Set properties if available\n    if (metadata.getConfiguration() != null && !metadata.getConfiguration().isEmpty()) {\n      DeltaCommitMetadataProperties properties = new DeltaCommitMetadataProperties()\n          .properties(metadata.getConfiguration());\n      deltaMetadata.properties(properties);\n    }\n\n    // Schema conversion is not directly supported as the SDK expects ColumnInfos\n    // which requires parsing the schema string. For now, we skip schema conversion.\n    // If needed, implement schema string parsing to ColumnInfos.\n\n    return deltaMetadata;\n  }\n\n  /**\n   * Converts a Unity Catalog SDK {@link DeltaGetCommitsResponse} to a Delta\n   * {@link GetCommitsResponse}.\n   *\n   * @param response The SDK response to convert\n   * @param tableUri The table URI for constructing file paths\n   * @return The converted GetCommitsResponse\n   */\n  private GetCommitsResponse toGetCommitsResponse(DeltaGetCommitsResponse response, URI tableUri) {\n    Path basePath = CoordinatedCommitsUtils.commitDirPath(\n        CoordinatedCommitsUtils.logDirPath(new Path(tableUri)));\n\n    List<Commit> commits = new ArrayList<>();\n    for (DeltaCommitInfo commitInfo : response.getCommits()) {\n      commits.add(fromDeltaCommitInfo(commitInfo, basePath));\n    }\n\n    return new GetCommitsResponse(commits, response.getLatestTableVersion());\n  }\n\n  /**\n   * Converts a Unity Catalog SDK {@link DeltaCommitInfo} to a Delta {@link Commit}.\n   *\n   * @param commitInfo The SDK commit info to convert\n   * @param basePath   The base path for constructing file paths\n   * @return The converted Commit\n   */\n  private Commit fromDeltaCommitInfo(DeltaCommitInfo commitInfo, Path basePath) {\n    FileStatus fileStatus = new FileStatus(\n        commitInfo.getFileSize(),\n        false /* isdir */,\n        0 /* block_replication */,\n        0 /* blocksize */,\n        commitInfo.getFileModificationTimestamp(),\n        new Path(basePath, commitInfo.getFileName()));\n\n    return new Commit(commitInfo.getVersion(), fileStatus, commitInfo.getTimestamp());\n  }\n\n  // ===========================\n  // Exception Handling Methods\n  // ===========================\n\n  /**\n   * Handles {@link ApiException} from commit operations by converting to appropriate Delta\n   * exceptions.\n   *\n   * @param e The API exception to handle\n   * @throws CommitFailedException        If the commit failed due to various reasons\n   * @throws UCCommitCoordinatorException If there's a UC-specific error\n   * @throws IOException                  If there's an unexpected error\n   */\n  private void handleCommitException(ApiException e)\n      throws CommitFailedException, UCCommitCoordinatorException {\n    int statusCode = e.getCode();\n    String responseBody = e.getResponseBody();\n\n    switch (statusCode) {\n      case HTTP_BAD_REQUEST:\n        throw new CommitFailedException(\n            false /* retryable */,\n            false /* conflict */,\n            \"Invalid commit parameters: \" + responseBody,\n            e);\n      case HTTP_NOT_FOUND:\n        throw new InvalidTargetTableException(\"Invalid Target Table: \" + responseBody);\n      case HTTP_CONFLICT:\n        throw new CommitFailedException(\n            true /* retryable */,\n            true /* conflict */,\n            \"Commit conflict: \" + responseBody,\n            e);\n      case HTTP_TOO_MANY_REQUESTS:\n        throw new CommitLimitReachedException(\"Backfilled commits limit reached: \" + responseBody);\n      default:\n        throw new CommitFailedException(\n            true /* retryable */,\n            false /* conflict */,\n            \"Unexpected commit failure (HTTP \" + statusCode + \"): \" + responseBody,\n            e);\n    }\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uccommitcoordinator/UpgradeNotAllowedException.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator;\n\n/**\n * This exception is thrown by the UC client in case the client attempted an upgrade\n * of a table to a UC managed table but the upgrade is not allowed because the previous\n * commit was a downgrade.\n */\npublic class UpgradeNotAllowedException extends UCCommitCoordinatorException {\n  public UpgradeNotAllowedException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uniform/IcebergMetadata.java",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uniform;\n\nimport java.util.Objects;\n\n/**\n * Metadata for Delta Uniform Iceberg conversion.\n *\n * <p>This class contains information about the latest Iceberg conversion for a Delta table,\n * which is sent to Unity Catalog to track the Iceberg metadata state.\n */\npublic class IcebergMetadata {\n  private final String metadataLocation;\n  private final long convertedDeltaVersion;\n  private final String convertedDeltaTimestamp;\n\n  /**\n   * Constructs IcebergMetadata with the specified conversion details.\n   *\n   * @param metadataLocation The Iceberg metadata file location (e.g., \"s3://bucket/metadata/v1.json\")\n   * @param convertedDeltaVersion The Delta version that was converted (e.g., 1044)\n   * @param convertedDeltaTimestamp The timestamp of the conversion (e.g., \"2025-01-04T03:13:11.423\")\n   */\n  public IcebergMetadata(\n      String metadataLocation, long convertedDeltaVersion, String convertedDeltaTimestamp) {\n    this.metadataLocation = Objects.requireNonNull(metadataLocation, \"metadataLocation is null\");\n    this.convertedDeltaVersion = convertedDeltaVersion;\n    this.convertedDeltaTimestamp =\n        Objects.requireNonNull(convertedDeltaTimestamp, \"convertedDeltaTimestamp is null\");\n  }\n\n  /** Returns the Iceberg metadata file location. */\n  public String getMetadataLocation() {\n    return metadataLocation;\n  }\n\n  /** Returns the Delta version that was converted to Iceberg. */\n  public long getConvertedDeltaVersion() {\n    return convertedDeltaVersion;\n  }\n\n  /** Returns the timestamp when the conversion occurred. */\n  public String getConvertedDeltaTimestamp() {\n    return convertedDeltaTimestamp;\n  }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/commit/uniform/UniformMetadata.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uniform;\n\nimport java.util.Optional;\n\n/**\n * Metadata for Delta Universal Format (UniForm) conversions.\n *\n * <p>UniForm allows Delta tables to be read by other table formats. This class contains\n * conversion metadata for supported formats that is sent to Unity Catalog.\n */\npublic class UniformMetadata {\n  private final IcebergMetadata icebergMetadata;\n  // Future: private final HudiMetadata hudiMetadata;\n\n  /**\n   * Constructs UniformMetadata with Iceberg conversion metadata.\n   *\n   * @param icebergMetadata The Iceberg conversion metadata (can be null if not enabled)\n   */\n  public UniformMetadata(IcebergMetadata icebergMetadata) {\n    this.icebergMetadata = icebergMetadata;\n  }\n\n  /**\n   * Returns the Iceberg metadata if Iceberg conversion is enabled.\n   *\n   * @return Optional containing Iceberg metadata, or empty if not enabled\n   */\n  public Optional<IcebergMetadata> getIcebergMetadata() {\n    return Optional.ofNullable(icebergMetadata);\n  }\n\n  // Future: public Optional<HudiMetadata> getHudiMetadata() { ... }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/internal/FileNameUtils.java",
    "content": "package io.delta.storage.internal;\n\nimport java.util.regex.Pattern;\n\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Helper for misc functions relating to file names for delta commits.\n */\npublic final class FileNameUtils {\n    static Pattern DELTA_FILE_PATTERN = Pattern.compile(\"\\\\d+\\\\.json\");\n\n    /**\n     * Returns the delta (json format) path for a given delta file.\n     */\n    public static Path deltaFile(Path path, long version) {\n        return new Path(path, String.format(\"%020d.json\", version));\n    }\n\n    /**\n     * Returns the version for the given delta path.\n     */\n    public static long deltaVersion(Path path) {\n        return Long.parseLong(path.getName().split(\"\\\\.\")[0]);\n    }\n\n    /**\n     * Returns true if the given path is a delta file, else false.\n     */\n    public static boolean isDeltaFile(Path path) {\n        return DELTA_FILE_PATTERN.matcher(path.getName()).matches();\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/internal/LogStoreErrors.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.internal;\n\nimport java.io.IOException;\n\npublic class LogStoreErrors {\n\n    /**\n     * Returns true if the provided Throwable is to be considered non-fatal, or false if it is to be\n     * considered fatal\n     */\n    public static boolean isNonFatal(Throwable t) {\n        // VirtualMachineError includes OutOfMemoryError and other fatal errors\n        if (t instanceof VirtualMachineError ||\n            t instanceof ThreadDeath ||\n            t instanceof InterruptedException ||\n            t instanceof LinkageError) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Returns true if the provided Throwable is to be considered fatal, or false if it is to be\n     * considered non-fatal\n     */\n    public static boolean isFatal(Throwable t) {\n        return !isNonFatal(t);\n    }\n\n    public static IOException incorrectLogStoreImplementationException(Throwable cause) {\n        return new IOException(\n            String.join(\"\\n\",\n                \"The error typically occurs when the default LogStore implementation, that\",\n                \"is, HDFSLogStore, is used to write into a Delta table on a non-HDFS storage system.\",\n                \"In order to get the transactional ACID guarantees on table updates, you have to use the\",\n                \"correct implementation of LogStore that is appropriate for your storage system.\",\n                \"See https://docs.delta.io/latest/delta-storage.html for details.\"\n            ),\n            cause\n        );\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/internal/PathLock.java",
    "content": "package io.delta.storage.internal;\n\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport org.apache.hadoop.fs.Path;\n\n/**\n * A lock that provides per-file-path `acquire` and `release` semantics. Can be used to ensure that\n * no two writers are creating the same external (e.g. S3) file at the same time.\n * <p>\n * Note: For all APIs, the caller should resolve the path to make sure we are locking the correct\n * absolute path.\n */\npublic class PathLock {\n\n    private final ConcurrentHashMap<Path, Object> pathLock;\n\n    public PathLock() {\n        this.pathLock = new ConcurrentHashMap<>();\n    }\n\n    /** Release the lock for the path after writing. */\n    public void release(Path resolvedPath) {\n        final Object lock = pathLock.remove(resolvedPath);\n        synchronized(lock) {\n            lock.notifyAll();\n        }\n    }\n\n    /** Acquire a lock for the path before writing. */\n    public void acquire(Path resolvedPath) throws InterruptedException {\n        while (true) {\n            final Object lock = pathLock.putIfAbsent(resolvedPath, new Object());\n            if (lock == null) {\n                return;\n            }\n            synchronized (lock) {\n                while (pathLock.get(resolvedPath) == lock) {\n                    lock.wait();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/internal/S3LogStoreUtil.java",
    "content": "/*\n * Copyright (2022) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.internal;\n\nimport software.amazon.awssdk.services.s3.model.ListObjectsV2Request;\nimport org.apache.hadoop.fs.*;\nimport org.apache.hadoop.fs.s3a.*;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashSet;\n\nimport static org.apache.hadoop.fs.s3a.Constants.DEFAULT_MAX_PAGING_KEYS;\nimport static org.apache.hadoop.fs.s3a.Constants.MAX_PAGING_KEYS;\nimport static org.apache.hadoop.fs.s3a.S3AUtils.iteratorToStatuses;\n\n\n/**\n * Static utility methods for the S3SingleDriverLogStore.\n *\n * Used to trick the class loader so we can use methods of org.apache.hadoop:hadoop-aws without needing to load this as\n * a dependency for tests in core.\n */\npublic final class S3LogStoreUtil {\n    private S3LogStoreUtil() {}\n\n    private static PathFilter ACCEPT_ALL = new PathFilter() {\n        @Override\n        public boolean accept(Path file) {\n            return true;\n        }\n\n        @Override\n        public String toString() {\n            return \"ACCEPT_ALL\";\n        }\n    };\n\n    /**\n     * Uses the S3ListRequest.v2 interface with the startAfter parameter to only list files\n     * which are lexicographically greater than resolvedPath.\n     */\n    private static RemoteIterator<S3AFileStatus> s3ListFrom(\n            S3AFileSystem s3afs,\n            Path resolvedPath,\n            Path parentPath) throws IOException {\n        int maxKeys = S3AUtils.intOption(s3afs.getConf(), MAX_PAGING_KEYS, DEFAULT_MAX_PAGING_KEYS, 1);\n        Listing listing = s3afs.getListing();\n        // List files lexicographically after resolvedPath inclusive within the same directory\n        return listing.createFileStatusListingIterator(resolvedPath,\n                S3ListRequest.v2(\n                    ListObjectsV2Request.builder()\n                        .bucket(s3afs.getBucket())\n                        .maxKeys(maxKeys)\n                        .prefix(s3afs.pathToKey(parentPath))\n                        .startAfter(keyBefore(s3afs.pathToKey(resolvedPath)))\n                        .build()\n                ), ACCEPT_ALL,\n                new Listing.AcceptAllButSelfAndS3nDirs(parentPath),\n                s3afs.getActiveAuditSpan());\n    }\n\n    /**\n     * Uses the S3ListRequest.v2 interface with the startAfter parameter to only list files\n     * which are lexicographically greater than resolvedPath.\n     *\n     * Wraps s3ListFrom in an array. Contained in this class to avoid contaminating other\n     * classes with dependencies on recent Hadoop versions.\n     *\n     * TODO: Remove this method when iterators are used everywhere.\n     */\n    public static FileStatus[] s3ListFromArray(\n            FileSystem fs,\n            Path resolvedPath,\n            Path parentPath) throws IOException {\n        S3AFileSystem s3afs;\n        try {\n             s3afs = (S3AFileSystem) fs;\n        } catch (ClassCastException e) {\n            throw new UnsupportedOperationException(\n                    \"The Hadoop file system used for the S3LogStore must be castable to \" +\n                            \"org.apache.hadoop.fs.s3a.S3AFileSystem.\", e);\n        }\n        return iteratorToStatuses(S3LogStoreUtil.s3ListFrom(s3afs, resolvedPath, parentPath));\n    }\n\n    /**\n     * Get the key which is lexicographically right before key.\n     * If the key is empty return null.\n     * If the key ends in a null byte, remove the last byte.\n     * Otherwise, subtract one from the last byte.\n     */\n    static String keyBefore(String key) {\n        byte[] bytes = key.getBytes(StandardCharsets.UTF_8);\n        if(bytes.length == 0) return null;\n        if(bytes[bytes.length - 1] > 0) {\n            bytes[bytes.length - 1] -= 1;\n            return new String(bytes, StandardCharsets.UTF_8);\n        } else {\n            return new String(bytes, 0, bytes.length - 1, StandardCharsets.UTF_8);\n        }\n    }\n}\n"
  },
  {
    "path": "storage/src/main/java/io/delta/storage/internal/ThreadUtils.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.internal;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.Callable;\n\npublic final class ThreadUtils {\n\n    /**\n     * Based on Apache Spark's ThreadUtils.runInNewThread\n     * Run a piece of code in a new thread and return the result.\n     */\n    public static <T> T runInNewThread(\n            String threadName,\n            boolean isDaemon,\n            Callable<T> body) throws Throwable {\n        // Using a single-element list to hold the throwable and result,\n        // since values used in static method must be final\n        List<Throwable> exceptionHolder = new ArrayList<>(1);\n        List<T> resultHolder = new ArrayList<>(1);\n        Thread thread = new Thread(threadName) {\n            @Override\n            public void run() {\n                try {\n                    resultHolder.add(body.call());\n                } catch (Throwable t) {\n                    exceptionHolder.add(t);\n                }\n            }\n        };\n        thread.setDaemon(isDaemon);\n        thread.start();\n        thread.join();\n\n        if (!exceptionHolder.isEmpty()) {\n            Throwable realException = exceptionHolder.get(0);\n\n            // Remove the part of the stack that shows method calls into this helper method\n            // This means drop everything from the top until the stack element\n            // ThreadUtils.runInNewThread(), and then drop that as well (hence the `drop(1)`).\n            List<StackTraceElement> baseStackTrace = new ArrayList<>();\n            boolean shouldDrop = true;\n            for (StackTraceElement st : Thread.currentThread().getStackTrace()) {\n                if (!shouldDrop) {\n                    baseStackTrace.add(st);\n                } else if (st.getClassName().contains(ThreadUtils.class.getSimpleName())){\n                    shouldDrop = false;\n                }\n            }\n\n            // Remove the part of the new thread stack that shows methods call from this helper\n            // method. This means take everything from the top until the stack element\n            List<StackTraceElement> extraStackTrace = new ArrayList<>();\n            for (StackTraceElement st : realException.getStackTrace()) {\n                if (!st.getClassName().contains(ThreadUtils.class.getSimpleName())) {\n                    extraStackTrace.add(st);\n                } else {\n                    break;\n                }\n            }\n\n            // Combine the two stack traces, with a placeholder just specifying that there\n            // was a helper method used, without any further details of the helper\n            StackTraceElement placeHolderStackElem = new StackTraceElement(\n                String.format( // Providing the helper class info.\n                    \"... run in separate thread using %s static method runInNewThread\",\n                    ThreadUtils.class.getSimpleName()\n                ),\n                \" \", // method name containing the execution point, not required here.\n                \"\", // filename containing the execution point, not required here.\n                -1); // source line number also not required. -1 indicates unavailable.\n            List<StackTraceElement> finalStackTrace = new ArrayList<>();\n            finalStackTrace.addAll(extraStackTrace);\n            finalStackTrace.add(placeHolderStackElem);\n            finalStackTrace.addAll(baseStackTrace);\n\n            // Update the stack trace and rethrow the exception in the caller thread\n            realException.setStackTrace(\n                finalStackTrace.toArray(new StackTraceElement[0])\n            );\n            throw realException;\n        } else {\n            return resultHolder.get(0);\n        }\n    }\n}\n"
  },
  {
    "path": "storage/src/test/scala/io/delta/storage/ThreadUtilsSuite.scala",
    "content": "package io.delta.storage\n\nimport java.io.IOException\n\nimport scala.util.Random\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ThreadUtilsSuite extends AnyFunSuite {\n  test(\"runInNewThread\") {\n    import io.delta.storage.internal.ThreadUtils.runInNewThread\n\n    assert(runInNewThread(\"thread-name\",\n      true,\n      () => {\n        Thread.currentThread().getName\n      }) === \"thread-name\"\n    )\n    assert(runInNewThread(\"thread-name\",\n      true,\n      () => {\n        Thread.currentThread().isDaemon\n      })\n    )\n    assert(runInNewThread(\"thread-name\",\n      false,\n      () => {\n        Thread.currentThread().isDaemon\n      } === false)\n    )\n\n    val ioExceptionMessage = \"test\" + Random.nextInt()\n    val ioException = intercept[IOException] {\n      runInNewThread(\"thread-name\",\n        true,\n        () => {\n          throw new IOException(ioExceptionMessage)\n        })\n    }\n    assert(ioException.getMessage === ioExceptionMessage)\n    assert(ioException.getStackTrace.mkString(\"\\n\")\n      .contains(\"... run in separate thread using ThreadUtils\"))\n    assert(!ioException.getStackTrace.mkString(\"\\n\").contains(\"ThreadUtils.java\"))\n  }\n}\n"
  },
  {
    "path": "storage/src/test/scala/io/delta/storage/commit/InMemoryCommitCoordinator.scala",
    "content": "/*\n * Copyright (2024) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage io.delta.storage.commit\n\nimport java.lang.{Long => JLong}\nimport java.nio.file.FileAlreadyExistsException\nimport java.util.{ArrayList, Collections, Iterator => JIterator, Map => JMap, Optional, TreeMap, UUID}\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.locks.ReentrantReadWriteLock\n\nimport io.delta.storage.LogStore\nimport io.delta.storage.commit.CoordinatedCommitsUtils\nimport io.delta.storage.commit.actions.AbstractMetadata\nimport io.delta.storage.commit.actions.AbstractProtocol\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.FileStatus\nimport org.apache.hadoop.fs.Path\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\nclass InMemoryCommitCoordinator(val batchSize: Long) extends CommitCoordinatorClient {\n  protected val logger: Logger = LoggerFactory.getLogger(classOf[InMemoryCommitCoordinator])\n\n  /**\n   * @param maxCommitVersion represents the max commit version known for the table. This is\n   *                         initialized at the time of pre-registration and updated whenever a\n   *                         commit is successfully added to the commit-coordinator.\n   * @param active represents whether this commit-coordinator has ratified any commit or not.\n   * |----------------------------|------------------|---------------------------|\n   * |        State               | maxCommitVersion |          active           |\n   * |----------------------------|------------------|---------------------------|\n   * | Table is pre-registered    | currentVersion+1 |          false            |\n   * |----------------------------|------------------|---------------------------|\n   * | Table is pre-registered    |       X          |          true             |\n   * | and more commits are done  |                  |                           |\n   * |----------------------------|------------------|---------------------------|\n   */\n  private[commit] class PerTableData(\n    var maxCommitVersion: Long = -1,\n    var active: Boolean = false\n  ) {\n    def updateLastRatifiedCommit(commitVersion: Long): Unit = {\n      this.active = true\n      this.maxCommitVersion = commitVersion\n    }\n\n    /**\n     * Returns the last ratified commit version for the table. If no commits have been done from\n     * commit-coordinator yet, returns -1.\n     */\n    def lastRatifiedCommitVersion: Long = if (!active) -1 else maxCommitVersion\n\n    // Map from version to Commit data\n    val commitsMap: TreeMap[Long, Commit] = new TreeMap[Long, Commit]\n    // We maintain maxCommitVersion explicitly since commitsMap might be empty\n    // if all commits for a table have been backfilled.\n    val lock: ReentrantReadWriteLock = new ReentrantReadWriteLock()\n  }\n\n  private[commit] val perTableMap = new ConcurrentHashMap[String, PerTableData]()\n\n  override def registerTable(\n      logPath: Path,\n      tableIdentifier: Optional[TableIdentifier],\n      currentVersion: Long,\n      currentMetadata: AbstractMetadata,\n      currentProtocol: AbstractProtocol): JMap[String, String] = {\n    val newPerTableData = new PerTableData(currentVersion + 1)\n    perTableMap.compute(logPath.toString, (_, existingData) => {\n      if (existingData != null) {\n        if (existingData.lastRatifiedCommitVersion != -1) {\n          throw new IllegalStateException(\n            s\"Table $logPath already exists in the commit-coordinator.\")\n        }\n        // If lastRatifiedCommitVersion is -1 i.e. the commit-coordinator has never\n        // attempted any commit for this table => this table was just pre-registered. If\n        // there is another pre-registration request for an older version, we reject it and\n        // table can't go backward.\n        if (currentVersion < existingData.maxCommitVersion) {\n          throw new IllegalStateException(\n            s\"Table $logPath already registered with commit-coordinator\")\n        }\n      }\n      newPerTableData\n    })\n    Collections.emptyMap[String, String]()\n  }\n\n  override def commit(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      tableDesc: TableDescriptor,\n      commitVersion: Long,\n      actions: JIterator[String],\n      updatedActions: UpdatedActions): CommitResponse = {\n    val logPath = tableDesc.getLogPath\n    val tablePath = CoordinatedCommitsUtils.getTablePath(logPath)\n    if (commitVersion == 0) {\n      throw new CommitFailedException(false, false, \"Commit version 0 must go via filesystem.\")\n    }\n    logger.info(\"Attempting to commit version {} on table {}\", commitVersion, tablePath)\n    val fs = logPath.getFileSystem(hadoopConf)\n    if (batchSize <= 1) {\n      // Backfill until `commitVersion - 1`\n      logger.info(\n        \"Making sure commits are backfilled until {}\" + \" version for table {}\",\n        commitVersion - 1,\n        tablePath)\n      backfillToVersion(logStore, hadoopConf, tableDesc, commitVersion - 1, null)\n    }\n    // Write new commit file in `_staged_commits` directory\n    val fileStatus = CoordinatedCommitsUtils.writeUnbackfilledCommitFile(\n      logStore, hadoopConf, logPath.toString, commitVersion, actions, generateUUID())\n    // Do the actual commit\n    val commitTimestamp = updatedActions.getCommitInfo.getCommitTimestamp\n    val commitResponse = addToMap(logPath, commitVersion, fileStatus, commitTimestamp)\n\n    val mcToFsConversion = CoordinatedCommitsUtils.isCoordinatedCommitsToFSConversion(\n      commitVersion, updatedActions)\n    // Backfill if needed\n    if (batchSize <= 1) {\n      // Always backfill when batch size is configured as 1\n      backfill(logStore, hadoopConf, logPath, commitVersion, fileStatus)\n      val targetFile = CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, commitVersion)\n      val targetFileStatus = fs.getFileStatus(targetFile)\n      val newCommit = commitResponse.getCommit.withFileStatus(targetFileStatus)\n      return new CommitResponse(newCommit)\n    }\n    else if (commitVersion % batchSize == 0 || mcToFsConversion) {\n      logger.info(\n        \"Making sure commits are backfilled till {} version for table {}\",\n        commitVersion,\n        tablePath)\n      backfillToVersion(logStore, hadoopConf, tableDesc, commitVersion, null)\n    }\n    logger.info(\"Commit {} done successfully on table {}\", commitVersion, tablePath)\n    commitResponse\n  }\n\n  override def getCommits(\n      tableDesc: TableDescriptor,\n      startVersion: JLong,\n      endVersion: JLong)\n      : GetCommitsResponse = withReadLock[GetCommitsResponse](tableDesc.getLogPath) {\n    val tableData = perTableMap.get(tableDesc.getLogPath.toString)\n    val startVersionOpt = Optional.ofNullable(startVersion)\n    val endVersionOpt = Optional.ofNullable(endVersion)\n    val effectiveStartVersion = startVersionOpt.orElse(0L)\n    // Calculate the end version for the range, or use the last key if endVersion is not\n    // provided\n    val effectiveEndVersion = endVersionOpt.orElseGet(\n      () => if (tableData.commitsMap.isEmpty) effectiveStartVersion\n      else tableData.commitsMap.lastKey)\n    val commitsInRange = tableData.commitsMap.subMap(effectiveStartVersion, effectiveEndVersion + 1)\n    new GetCommitsResponse(\n      new ArrayList[Commit](commitsInRange.values), tableData.lastRatifiedCommitVersion)\n  }\n\n  override def backfillToVersion(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      tableDesc: TableDescriptor,\n      version: Long,\n      lastKnownBackfilledVersion: JLong): Unit = {\n    val logPath = tableDesc.getLogPath\n    // Confirm the last backfilled version by checking the backfilled delta file's existence.\n    var validLastKnownBackfilledVersion = lastKnownBackfilledVersion\n    if (lastKnownBackfilledVersion != null) {\n      val fs = logPath.getFileSystem(hadoopConf)\n      if (!fs.exists(CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, version))) {\n        validLastKnownBackfilledVersion = null\n      }\n    }\n    var startVersion: JLong = null\n    if (validLastKnownBackfilledVersion != null) startVersion = validLastKnownBackfilledVersion + 1\n    val commitsResponse = getCommits(tableDesc, startVersion, version)\n    commitsResponse.getCommits.forEach((commit: Commit) => {\n      backfill(logStore, hadoopConf, logPath, commit.getVersion, commit.getFileStatus)\n    })\n  }\n\n  override def semanticEquals(other: CommitCoordinatorClient): Boolean = this == other\n\n  /** Backfills a given `fileStatus` to `version`.json */\n  protected def backfill(\n      logStore: LogStore,\n      hadoopConf: Configuration,\n      logPath: Path,\n      version: Long,\n      fileStatus: FileStatus): Unit = {\n    val targetFile = CoordinatedCommitsUtils.getBackfilledDeltaFilePath(logPath, version)\n    logger.info(\"Backfilling commit \" + fileStatus.getPath + \" to \" + targetFile)\n    val commitContentIterator = logStore.read(fileStatus.getPath, hadoopConf)\n    try {\n      logStore.write(targetFile, commitContentIterator, false, hadoopConf)\n      registerBackfill(logPath, version)\n    } catch {\n      case _: FileAlreadyExistsException =>\n        logger.info(\"The backfilled file \" + targetFile + \" already exists.\")\n    } finally commitContentIterator.close()\n  }\n\n  protected def generateUUID(): String = UUID.randomUUID().toString\n\n  private def addToMap(\n      logPath: Path,\n      commitVersion: Long,\n      commitFile: FileStatus,\n      commitTimestamp: Long): CommitResponse = withWriteLock[CommitResponse](logPath) {\n    val tableData = perTableMap.get(logPath.toString)\n    val expectedVersion = tableData.maxCommitVersion + 1\n    if (commitVersion != expectedVersion) {\n      throw new CommitFailedException(\n        commitVersion < expectedVersion,\n        commitVersion < expectedVersion,\n        s\"Commit version $commitVersion is not valid. Expected version: $expectedVersion.\")\n    }\n    val commit = new Commit(commitVersion, commitFile, commitTimestamp)\n    tableData.commitsMap.put(commitVersion, commit)\n    tableData.updateLastRatifiedCommit(commitVersion)\n\n    logger.info(\"Added commit file \" + commitFile.getPath + \" to commit-coordinator.\")\n    new CommitResponse(commit)\n  }\n\n  /**\n   * Callback to tell the CommitCoordinator that all commits <= `backfilledVersion` are\n   * backfilled.\n   */\n  protected[delta] def registerBackfill(logPath: Path, backfilledVersion: Long): Unit = {\n    withWriteLock(logPath) {\n      val tableData = perTableMap.get(logPath.toString)\n      if (backfilledVersion > tableData.lastRatifiedCommitVersion) {\n        throw new IllegalArgumentException(\n          \"Unexpected backfill version: \" + backfilledVersion + \". \" +\n            \"Max backfill version: \" + tableData.maxCommitVersion)\n      }\n      // Remove keys with versions less than or equal to 'untilVersion'\n      val iterator = tableData.commitsMap.keySet.iterator\n      while (iterator.hasNext) {\n        val version = iterator.next\n        if (version <= backfilledVersion) {\n          iterator.remove()\n        } else {\n          return\n        }\n      }\n    }\n  }\n\n  private[commit] def withReadLock[T](logPath: Path)(operation: => T): T = {\n    val tableData = perTableMap.get(logPath.toString)\n    if (tableData == null) {\n      throw new IllegalArgumentException(s\"Unknown table $logPath.\")\n    }\n    val lock = tableData.lock.readLock()\n    lock.lock()\n    try {\n      operation\n    } finally {\n      lock.unlock()\n    }\n  }\n\n  private[commit] def withWriteLock[T](logPath: Path)(operation: => T): T = {\n    val tableData = Option(perTableMap.get(logPath.toString)).getOrElse {\n      throw new IllegalArgumentException(s\"Unknown table $logPath.\")\n    }\n    val lock = tableData.lock.writeLock()\n    lock.lock()\n    try {\n      operation\n    } finally {\n      lock.unlock()\n    }\n  }\n}\n"
  },
  {
    "path": "storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala",
    "content": "/*\n * Copyright (2026) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.commit.uccommitcoordinator\n\nimport java.net.{InetSocketAddress, URI}\nimport java.nio.charset.StandardCharsets\nimport java.util.{Collections, Optional}\n\nimport com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}\nimport com.sun.net.httpserver.{HttpExchange, HttpServer}\nimport io.delta.storage.commit.{Commit, CommitFailedException}\nimport io.delta.storage.commit.actions.AbstractMetadata\nimport io.delta.storage.commit.uniform.{IcebergMetadata, UniformMetadata}\nimport io.unitycatalog.client.auth.TokenProvider\n\nimport org.apache.hadoop.fs.{FileStatus, Path}\nimport org.apache.http.HttpStatus\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass UCTokenBasedRestClientSuite\n    extends AnyFunSuite\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach {\n\n  private val testTableId = \"test-table-id\"\n  private val testTableUri = new URI(\"s3://bucket/path/to/table\")\n  private val testMetastoreId = \"test-metastore-123\"\n\n  private var server: HttpServer = _\n  private var serverUri: String = _\n  private var metastoreHandler: HttpExchange => Unit = _\n  private var commitsHandler: HttpExchange => Unit = _\n  private val objectMapper = new ObjectMapper()\n\n  override def beforeAll(): Unit = {\n    server = HttpServer.create(new InetSocketAddress(\"localhost\", 0), 0)\n    server.createContext(\"/api/2.1/unity-catalog/metastore_summary\", exchange => {\n      if (metastoreHandler != null) metastoreHandler(exchange)\n      else sendJson(exchange, HttpStatus.SC_OK, s\"\"\"{\"metastore_id\":\"$testMetastoreId\"}\"\"\")\n      exchange.close()\n    })\n    server.createContext(\"/api/2.1/unity-catalog/delta/preview/commits\", exchange => {\n      if (commitsHandler != null) commitsHandler(exchange)\n      else {\n        val body = if (exchange.getRequestMethod == \"POST\") \"{}\"\n          else \"\"\"{\"commits\":[],\"latest_table_version\":-1}\"\"\"\n        sendJson(exchange, HttpStatus.SC_OK, body)\n      }\n      exchange.close()\n    })\n    server.start()\n    serverUri = s\"http://localhost:${server.getAddress.getPort}\"\n  }\n\n  override def afterAll(): Unit = if (server != null) server.stop(0)\n\n  override def beforeEach(): Unit = {\n    metastoreHandler = null\n    commitsHandler = null\n  }\n\n  private def readRequestBody(exchange: HttpExchange): String = {\n    val is = exchange.getRequestBody\n    try new String(is.readAllBytes(), StandardCharsets.UTF_8) finally is.close()\n  }\n\n  private def sendJson(exchange: HttpExchange, status: Int, body: String): Unit = {\n    val bytes = body.getBytes(StandardCharsets.UTF_8)\n    exchange.getResponseHeaders.add(\"Content-Type\", \"application/json\")\n    exchange.sendResponseHeaders(status, bytes.length)\n    exchange.getResponseBody.write(bytes)\n    exchange.getResponseBody.close()\n  }\n\n  private def createTokenProvider(): TokenProvider = new TokenProvider {\n    override def accessToken(): String = \"mock-token\"\n    override def initialize(configs: java.util.Map[String, String]): Unit = {}\n    override def configs(): java.util.Map[String, String] = Collections.emptyMap()\n  }\n\n  private def createClient(): UCTokenBasedRestClient =\n    new UCTokenBasedRestClient(serverUri, createTokenProvider(), Collections.emptyMap())\n\n  private def withClient(fn: UCTokenBasedRestClient => Unit): Unit = {\n    val client = createClient()\n    try fn(client) finally client.close()\n  }\n\n  private def createCommit(version: Long): Commit = {\n    val fs = new FileStatus(1024L, false, 1, 4096L, System.currentTimeMillis(),\n      new Path(s\"/path/_delta_log/_staged_commits/$version.uuid.json\"))\n    new Commit(version, fs, System.currentTimeMillis())\n  }\n\n  private def createMetadata(): AbstractMetadata = new AbstractMetadata {\n    override def getId: String = \"id\"\n    override def getName: String = \"name\"\n    override def getDescription: String = \"desc\"\n    override def getProvider: String = \"delta\"\n    override def getFormatOptions: java.util.Map[String, String] = Collections.emptyMap()\n    override def getSchemaString: String = \"\"\"{\"type\":\"struct\",\"fields\":[]}\"\"\"\n    override def getPartitionColumns: java.util.List[String] = Collections.emptyList()\n    override def getConfiguration: java.util.Map[String, String] = Collections.emptyMap()\n    override def getCreatedTime: java.lang.Long = 0L\n  }\n\n  private def createUniformMetadata(): UniformMetadata =\n    new UniformMetadata(\n      new IcebergMetadata(\"s3://bucket/metadata/v1.json\", 42L, \"2025-01-04T03:13:11.423Z\"))\n\n  // Constructor tests\n  test(\"constructor validates required parameters\") {\n    intercept[NullPointerException] {\n      new UCTokenBasedRestClient(null, createTokenProvider(), Collections.emptyMap())\n    }\n    intercept[NullPointerException] {\n      new UCTokenBasedRestClient(serverUri, null, Collections.emptyMap())\n    }\n    intercept[NullPointerException] {\n      new UCTokenBasedRestClient(serverUri, createTokenProvider(), null)\n    }\n  }\n\n  // getMetastoreId tests\n  test(\"getMetastoreId returns ID on success\") {\n    withClient { client =>\n      assert(client.getMetastoreId() === testMetastoreId)\n    }\n  }\n\n  test(\"getMetastoreId throws IOException on error\") {\n    metastoreHandler = exchange => sendJson(exchange, HttpStatus.SC_INTERNAL_SERVER_ERROR, \"{}\")\n    withClient { client =>\n      intercept[java.io.IOException] { client.getMetastoreId() }\n    }\n  }\n\n  // commit tests\n  test(\"commit succeeds with valid parameters\") {\n    withClient { client =>\n      client.commit(testTableId, testTableUri, Optional.of(createCommit(1L)),\n        Optional.empty(), false, Optional.empty(), Optional.empty(), Optional.empty())\n    }\n  }\n\n  test(\"commit succeeds with metadata\") {\n    withClient { client =>\n      client.commit(\n        testTableId,\n        testTableUri,\n        Optional.of(createCommit(1L)),\n        Optional.of(java.lang.Long.valueOf(0L)),\n        true,\n        Optional.of(createMetadata()),\n        Optional.empty(),\n        Optional.empty())\n    }\n  }\n\n  test(\"commit validates required parameters\") {\n    withClient { client =>\n      intercept[NullPointerException] {\n        client.commit(null, testTableUri, Optional.empty(), Optional.empty(),\n          false, Optional.empty(), Optional.empty(), Optional.empty())\n      }\n      intercept[NullPointerException] {\n        client.commit(testTableId, null, Optional.empty(), Optional.empty(),\n          false, Optional.empty(), Optional.empty(), Optional.empty())\n      }\n    }\n  }\n\n  test(\"commit throws appropriate exceptions for HTTP errors\") {\n    def commitWith(status: Int): Unit = {\n      commitsHandler = exchange => sendJson(exchange, status, s\"\"\"{\"error\":\"$status\"}\"\"\")\n      withClient { client =>\n        client.commit(testTableId, testTableUri, Optional.of(createCommit(1L)),\n          Optional.empty(), false, Optional.empty(), Optional.empty(), Optional.empty())\n      }\n    }\n\n    // 400 -> CommitFailedException (non-retryable, non-conflict)\n    val e400 = intercept[CommitFailedException] { commitWith(400) }\n    assert(!e400.getRetryable && !e400.getConflict)\n\n    // 404 -> InvalidTargetTableException\n    intercept[InvalidTargetTableException] { commitWith(404) }\n\n    // 409 -> CommitFailedException (retryable, conflict)\n    val e409 = intercept[CommitFailedException] { commitWith(409) }\n    assert(e409.getRetryable && e409.getConflict)\n\n    // Note: 429 (CommitLimitReachedException) cannot be tested here because the SDK's\n    // RetryingHttpClient intercepts 429 before it reaches handleCommitException.\n    // The 429 path is exercised via UCCommitCoordinatorClientSuite integration tests.\n\n    // 500 (default branch) -> CommitFailedException (retryable, non-conflict)\n    val e500 = intercept[CommitFailedException] { commitWith(500) }\n    assert(e500.getRetryable && !e500.getConflict)\n  }\n\n  // getCommits tests\n  test(\"getCommits returns commits correctly\") {\n    val responseJson =\n      \"\"\"{\"commits\":[{\"version\":1,\"file_name\":\"1.json\",\"file_size\":100,\"\"\" +\n      \"\"\"\"timestamp\":1000,\"file_modification_timestamp\":1001}],\"latest_table_version\":1}\"\"\"\n    commitsHandler = exchange => sendJson(exchange, HttpStatus.SC_OK, responseJson)\n    withClient { client =>\n      val response = client.getCommits(\n        testTableId, testTableUri, Optional.empty(), Optional.empty())\n      assert(response.getCommits.size() === 1)\n      assert(response.getCommits.get(0).getVersion === 1L)\n      assert(response.getLatestTableVersion === 1L)\n    }\n  }\n\n  test(\"getCommits validates required parameters\") {\n    withClient { client =>\n      intercept[NullPointerException] {\n        client.getCommits(null, testTableUri, Optional.empty(), Optional.empty())\n      }\n      intercept[NullPointerException] {\n        client.getCommits(testTableId, null, Optional.empty(), Optional.empty())\n      }\n    }\n  }\n\n  test(\"getCommits throws InvalidTargetTableException on 404\") {\n    commitsHandler = exchange => sendJson(exchange, HttpStatus.SC_NOT_FOUND, \"{}\")\n    withClient { client =>\n      intercept[InvalidTargetTableException] {\n        client.getCommits(testTableId, testTableUri, Optional.empty(), Optional.empty())\n      }\n    }\n  }\n\n  // uniform tests\n  test(\"commit with uniform.iceberg sends correct snake_case JSON per all.yaml\") {\n    var capturedBody: String = null\n    commitsHandler = exchange => {\n      capturedBody = readRequestBody(exchange)\n      sendJson(exchange, HttpStatus.SC_OK, \"{}\")\n    }\n\n    withClient { client =>\n      client.commit(testTableId, testTableUri, Optional.of(createCommit(1L)),\n        Optional.empty(), false, Optional.empty(), Optional.empty(),\n        Optional.of(createUniformMetadata()))\n    }\n\n    val json: JsonNode = objectMapper.readTree(capturedBody)\n    assert(json.get(\"table_id\").asText() === testTableId)\n\n    val iceberg = json.get(\"uniform\").get(\"iceberg\")\n    assert(iceberg.get(\"metadata_location\").asText() === \"s3://bucket/metadata/v1.json\")\n    assert(iceberg.get(\"converted_delta_version\").asLong() === 42L)\n    assert(iceberg.get(\"converted_delta_timestamp\").asText() === \"2025-01-04T03:13:11.423Z\")\n\n    assert(!json.has(\"protocol\"), \"protocol is not in the OpenAPI spec and must not be sent\")\n  }\n\n  test(\"commit without uniform does not include uniform field in JSON\") {\n    var capturedBody: String = null\n    commitsHandler = exchange => {\n      capturedBody = readRequestBody(exchange)\n      sendJson(exchange, HttpStatus.SC_OK, \"{}\")\n    }\n\n    withClient { client =>\n      client.commit(testTableId, testTableUri, Optional.of(createCommit(1L)),\n        Optional.empty(), false, Optional.empty(), Optional.empty(), Optional.empty())\n    }\n\n    val json = objectMapper.readTree(capturedBody)\n    assert(!json.has(\"uniform\") || json.get(\"uniform\").isNull)\n  }\n\n  test(\"commit with uniform but no iceberg metadata does not include uniform field\") {\n    var capturedBody: String = null\n    commitsHandler = exchange => {\n      capturedBody = readRequestBody(exchange)\n      sendJson(exchange, HttpStatus.SC_OK, \"{}\")\n    }\n\n    withClient { client =>\n      client.commit(testTableId, testTableUri, Optional.of(createCommit(1L)),\n        Optional.empty(), false, Optional.empty(), Optional.empty(),\n        Optional.of(new UniformMetadata(null)))\n    }\n\n    val json = objectMapper.readTree(capturedBody)\n    assert(!json.has(\"uniform\") || json.get(\"uniform\").isNull)\n  }\n}\n"
  },
  {
    "path": "storage/src/test/scala/io/delta/storage/integration/S3LogStoreUtilIntegrationTest.scala",
    "content": "package io.delta.storage.integration\n\nimport io.delta.storage.internal.{FileNameUtils, S3LogStoreUtil}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.hadoop.fs.s3a.S3AFileSystem\nimport org.scalatest.Tag\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.net.URI\nimport scala.math.max\nimport scala.math.ceil\n\n/**\n * These integration tests are executed by setting the\n * environment variables\n * S3_LOG_STORE_UTIL_TEST_BUCKET=some-s3-bucket-name\n * S3_LOG_STORE_UTIL_TEST_RUN_UID=some-uuid-for-test-run\n * and running\n * python run-integration-tests.py --s3-log-store-util-only\n *\n * Alternatively you can set the environment variables\n * S3_LOG_STORE_UTIL_TEST_ENABLED=true\n * S3_LOG_STORE_UTIL_TEST_BUCKET=some-s3-bucket-name\n * S3_LOG_STORE_UTIL_TEST_RUN_UID=some-uuid-for-test-run\n * and run the tests in this suite using your preferred\n * test execution mechanism (e.g., the IDE or sbt)\n *\n * S3_LOG_STORE_UTIL_TEST_BUCKET is the name of the S3 bucket used for the test.\n * S3_LOG_STORE_UTIL_TEST_RUN_UID is a prefix for all keys used in the test.\n * This is useful for isolating multiple test runs.\n */\nclass S3LogStoreUtilIntegrationTest extends AnyFunSuite {\n  private val runIntegrationTests: Boolean =\n    Option(System.getenv(\"S3_LOG_STORE_UTIL_TEST_ENABLED\")).exists(_.toBoolean)\n  private val bucket = System.getenv(\"S3_LOG_STORE_UTIL_TEST_BUCKET\")\n  private val testRunUID =\n    System.getenv(\"S3_LOG_STORE_UTIL_TEST_RUN_UID\") // Prefix for all S3 keys in the current run\n  private lazy val fs: S3AFileSystem = {\n    val fs = new S3AFileSystem()\n    fs.initialize(new URI(s\"s3a://$bucket\"), configuration)\n    fs\n  }\n  private val maxKeys = 2\n  private val configuration = new Configuration()\n  configuration.set(\"fs.s3a.paging.maximum\", maxKeys.toString)\n\n  private def touch(key: String) {\n    fs.create(new Path(s\"s3a://$bucket/$key\")).close()\n  }\n\n  private def key(table: String, version: Int): String =\n    s\"$testRunUID/$table/_delta_log/%020d.json\".format(version)\n\n  private def path(table: String, version: Int): Path =\n    new Path(s\"s3a://$bucket/${key(table, version)}\")\n\n  private def version(path: Path): Long = FileNameUtils.deltaVersion(path)\n\n  private val integrationTestTag = Tag(\"IntegrationTest\")\n\n  def integrationTest(name: String)(testFun: => Any): Unit =\n    if (runIntegrationTests) test(name, integrationTestTag)(testFun)\n\n  def testCase(testName: String, numKeys: Int): Unit = integrationTest(testName) {\n    // Setup delta log\n    (1 to numKeys).foreach(v => touch(s\"$testRunUID/$testName/_delta_log/%020d.json\".format(v)))\n\n    // Check number of S3 requests and correct listing\n    (1 to numKeys + 2).foreach(v => {\n      val startCount = fs.getIOStatistics.counters().get(\"object_list_request\") +\n        fs.getIOStatistics.counters().get(\"object_continue_list_request\")\n      val resolvedPath = path(testName, v)\n      val response = S3LogStoreUtil.s3ListFromArray(fs, resolvedPath, resolvedPath.getParent)\n      val endCount = fs.getIOStatistics.counters().get(\"object_list_request\") +\n        fs.getIOStatistics.counters().get(\"object_continue_list_request\")\n      // Check that we don't do more S3 list requests than necessary\n      val numberOfKeysToList = numKeys - (v - 1)\n      val optimalNumberOfListRequests =\n        max(ceil(numberOfKeysToList / maxKeys.toDouble).toInt, 1)\n      val actualNumberOfListRequests = endCount - startCount\n      assert(optimalNumberOfListRequests == actualNumberOfListRequests)\n      // Check that we get consecutive versions from v to the max version. The smallest version is 1\n      assert((max(1, v) to numKeys) == response.map(r => version(r.getPath)).toSeq)\n    })\n  }\n\n  integrationTest(\"setup empty delta log\") {\n    touch(s\"$testRunUID/empty/some.json\")\n  }\n\n  testCase(\"empty\", 0)\n\n  testCase(\"small\", 1)\n\n  testCase(\"medium\", maxKeys)\n\n  testCase(\"large\", 10 * maxKeys)\n\n}\n"
  },
  {
    "path": "storage/src/test/scala/io/delta/storage/internal/S3LogStoreUtilTest.scala",
    "content": "package io.delta.storage.internal\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass S3LogStoreUtilTest extends AnyFunSuite {\n  test(\"keyBefore\") {\n    assert(\"a\" == S3LogStoreUtil.keyBefore(\"b\"))\n    assert(\"aa/aa\" == S3LogStoreUtil.keyBefore(\"aa/ab\"))\n    assert(Seq(1.toByte, 1.toByte)\n       == S3LogStoreUtil.keyBefore(new String(Seq(1.toByte, 2.toByte).toArray)).getBytes.toList)\n  }\n\n  test(\"keyBefore with emojis\") {\n    assert(\"♥a\" == S3LogStoreUtil.keyBefore(\"♥b\"))\n  }\n\n  test(\"keyBefore with zero bytes\") {\n    assert(\"abc\" == S3LogStoreUtil.keyBefore(\"abc\\u0000\"))\n  }\n\n  test(\"keyBefore with empty key\") {\n    assert(null == S3LogStoreUtil.keyBefore(\"\"))\n  }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/integration_tests/dynamodb_logstore.py",
    "content": "#\n# Copyright (2021) The Delta Lake Project Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport sys\nimport threading\n\nfrom pyspark.sql import SparkSession\nfrom multiprocessing.pool import ThreadPool\nimport time\n\n\"\"\"\nCreate required dynamodb table with:\n\n$ aws dynamodb create-table \\\n    --region <region> \\\n    --table-name <table_name> \\\n    --attribute-definitions AttributeName=tablePath,AttributeType=S \\\n                            AttributeName=fileName,AttributeType=S \\\n    --key-schema AttributeName=tablePath,KeyType=HASH \\\n                AttributeName=fileName,KeyType=RANGE \\\n    --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5\n    \nEnable TTL with:\n\n$ aws dynamodb update-time-to-live \\\n  --region <region> \\\n  --table-name <table_name> \\\n  --time-to-live-specification \"Enabled=true, AttributeName=expireTime\"\n\nRun this script in root dir of repository:\n\n# ===== Mandatory input from user =====\nexport RUN_ID=run001\nexport S3_BUCKET=delta-lake-dynamodb-test-00\n\n# ===== Optional input from user =====\nexport DELTA_CONCURRENT_WRITERS=20\nexport DELTA_CONCURRENT_READERS=2\nexport DELTA_STORAGE=io.delta.storage.S3DynamoDBLogStore\nexport DELTA_NUM_ROWS=200\nexport DELTA_DYNAMO_REGION=us-west-2\nexport DELTA_DYNAMO_ERROR_RATES=0.00\n\n# ===== Optional input from user (we calculate defaults using S3_BUCKET and RUN_ID) =====\nexport RELATIVE_DELTA_TABLE_PATH=___\nexport DELTA_DYNAMO_TABLE_NAME=___\n\n./run-integration-tests.py --use-local \\\n    --run-storage-s3-dynamodb-integration-tests \\\n    --packages org.apache.hadoop:hadoop-aws:3.3.1 \\\n    --dbb-conf io.delta.storage.credentials.provider=com.amazonaws.auth.profile.ProfileCredentialsProvider \\\n               spark.hadoop.fs.s3a.aws.credentials.provider=com.amazonaws.auth.profile.ProfileCredentialsProvider\n\"\"\"\n\n# ===== Mandatory input from user =====\nrun_id = os.environ.get(\"RUN_ID\")\ns3_bucket = os.environ.get(\"S3_BUCKET\")\n\n# ===== Optional input from user =====\nconcurrent_writers = int(os.environ.get(\"DELTA_CONCURRENT_WRITERS\", 2))\nconcurrent_readers = int(os.environ.get(\"DELTA_CONCURRENT_READERS\", 2))\n# className to instantiate. io.delta.storage.S3DynamoDBLogStore or .FailingS3DynamoDBLogStore\ndelta_storage = os.environ.get(\"DELTA_STORAGE\", \"io.delta.storage.S3DynamoDBLogStore\")\nnum_rows = int(os.environ.get(\"DELTA_NUM_ROWS\", 16))\ndynamo_region = os.environ.get(\"DELTA_DYNAMO_REGION\", \"us-west-2\")\n# used only by FailingS3DynamoDBLogStore\ndynamo_error_rates = os.environ.get(\"DELTA_DYNAMO_ERROR_RATES\", \"\")\n\n# ===== Optional input from user (we calculate defaults using RUN_ID) =====\nrelative_delta_table_path = os.environ.get(\"RELATIVE_DELTA_TABLE_PATH\", \"tables/table_\" + run_id)\\\n    .rstrip(\"/\")\ndynamo_table_name = os.environ.get(\"DELTA_DYNAMO_TABLE_NAME\", \"ddb_table_\" + run_id)\n\ndelta_table_path = \"s3a://\" + s3_bucket + \"/\" + relative_delta_table_path\nrelative_delta_log_path = relative_delta_table_path + \"/_delta_log/\"\n\nif delta_table_path is None:\n    print(f\"\\nSkipping Python test {os.path.basename(__file__)} due to the missing env variable \"\n          f\"`DELTA_TABLE_PATH`\\n=====================\")\n    sys.exit(0)\n\ntest_log = f\"\"\"\n========================================== \nrun id: {run_id}\ndelta table path: {delta_table_path}\ndynamo table name: {dynamo_table_name}\n\nconcurrent writers: {concurrent_writers}\nconcurrent readers: {concurrent_readers}\nnumber of rows: {num_rows}\ndelta storage: {delta_storage}\ndynamo_error_rates: {dynamo_error_rates}\n\nrelative_delta_table_path: {relative_delta_table_path}\nrelative_delta_log_path: {relative_delta_log_path}\n========================================== \n\"\"\"\nprint(test_log)\n\nspark = SparkSession \\\n    .builder \\\n    .appName(\"utilities\") \\\n    .master(\"local[*]\") \\\n    .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\") \\\n    .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\") \\\n    .config(\"spark.delta.logStore.s3.impl\", delta_storage) \\\n    .config(\"spark.delta.logStore.s3a.impl\", delta_storage) \\\n    .config(\"spark.delta.logStore.s3n.impl\", delta_storage) \\\n    .config(\"spark.io.delta.storage.S3DynamoDBLogStore.ddb.tableName\", dynamo_table_name) \\\n    .config(\"spark.io.delta.storage.S3DynamoDBLogStore.ddb.region\", dynamo_region) \\\n    .config(\"spark.io.delta.storage.S3DynamoDBLogStore.errorRates\", dynamo_error_rates) \\\n    .config(\"spark.io.delta.storage.S3DynamoDBLogStore.provisionedThroughput.rcu\", 12) \\\n    .config(\"spark.io.delta.storage.S3DynamoDBLogStore.provisionedThroughput.wcu\", 13) \\\n    .getOrCreate()\n\n# spark.sparkContext.setLogLevel(\"INFO\")\n\ndata = spark.createDataFrame([], \"id: int, a: int\")\nprint(\"writing:\", data.collect())\ndata.write.format(\"delta\").mode(\"overwrite\").partitionBy(\"id\").save(delta_table_path)\n\n\ndef write_tx(n):\n    data = spark.createDataFrame([[n, n]], \"id: int, a: int\")\n    print(\"writing:\", data.collect())\n    data.write.format(\"delta\").mode(\"append\").partitionBy(\"id\").save(delta_table_path)\n\n\nstop_reading = threading.Event()\n\n\ndef read_data():\n    while not stop_reading.is_set():\n        print(\"Reading {:d} rows ...\".format(\n            spark.read.format(\"delta\").load(delta_table_path).distinct().count())\n        )\n        time.sleep(1)\n\n\ndef start_read_thread():\n    thread = threading.Thread(target=read_data)\n    thread.start()\n    return thread\n\n\nprint(\"===================== Starting reads and writes =====================\")\nread_threads = [start_read_thread() for i in range(concurrent_readers)]\npool = ThreadPool(concurrent_writers)\nstart_t = time.time()\npool.map(write_tx, range(num_rows))\nstop_reading.set()\n\nfor thread in read_threads:\n    thread.join()\n\nprint(\"===================== Evaluating number of written rows =====================\")\nactual = spark.read.format(\"delta\").load(delta_table_path).distinct().count()\nprint(\"Actual number of written rows:\", actual)\nprint(\"Expected number of written rows:\", num_rows)\nassert actual == num_rows\n\nt = time.time() - start_t\nprint(f\"{num_rows / t:.02f} tx / sec\")\n\nprint(\"===================== Evaluating DDB writes =====================\")\nimport boto3\nfrom botocore.config import Config\nmy_config = Config(\n    region_name=dynamo_region,\n)\ndynamodb = boto3.resource('dynamodb',  config=my_config)\ntable = dynamodb.Table(dynamo_table_name)  # this ensures we actually used/created the input table\nresponse = table.scan()\nitems = response['Items']\nitems = sorted(items, key=lambda x: x['fileName'])\n\nprint(\"========== All DDB items ==========\")\nfor item in items:\n    print(item)\n\nprint(\"===================== Evaluating _delta_log commits =====================\")\ns3_client = boto3.client(\"s3\")\nprint(f\"querying {s3_bucket}/{relative_delta_log_path}\")\nresponse = s3_client.list_objects_v2(Bucket=s3_bucket, Prefix=relative_delta_log_path)\nitems = response['Contents']\nprint(\"========== Raw _delta_log contents ========== \")\nfor item in items:\n    print(item)\n\ndelta_log_commits = filter(lambda x: \".json\" in x['Key'] and \".tmp\" not in x['Key'],\n                           items)\ndelta_log_commits = sorted(delta_log_commits, key=lambda x: x['Key'])\n\nprint(\"========== _delta_log commits in version order ==========\")\nfor commit in delta_log_commits:\n    print(commit)\n\nprint(\"========== _delta_log commits in timestamp order ==========\")\ndelta_log_commits_sorted_timestamp = sorted(delta_log_commits, key=lambda x: x['LastModified'])\nfor commit in delta_log_commits_sorted_timestamp:\n    print(commit)\n\nprint(\"========== ASSERT that these orders (version vs timestamp) are the same ==========\")\nassert(delta_log_commits == delta_log_commits_sorted_timestamp)\n"
  },
  {
    "path": "storage-s3-dynamodb/src/main/java/io/delta/storage/BaseExternalLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.Optional;\nimport java.util.concurrent.TimeUnit;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport io.delta.storage.internal.FileNameUtils;\nimport io.delta.storage.internal.PathLock;\nimport org.apache.commons.io.IOUtils;\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileStatus;\nimport org.apache.hadoop.fs.FSDataInputStream;\nimport org.apache.hadoop.fs.FSDataOutputStream;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A base {@link LogStore} implementation for cloud stores (e.g. Amazon S3) that do not provide\n * mutual exclusion.\n * <p>\n * This implementation depends on child methods, particularly `putExternalEntry`, to provide\n * the mutual exclusion that the cloud store is lacking.\n *\n * Notation:\n * - N: the target commit version we are writing. e.g. 10 for 0000010.json\n * - N.json: the actual target commit we want to write.\n * - T(N): the temp file path for commit N used during the prepare-commit-acknowledge `write`\n *         algorithm below. We will eventually copy T(N) into N.json\n * - E(N, T(N), complete=true/false): the entry we will atomically commit into the external\n *                                      cache.\n */\npublic abstract class BaseExternalLogStore extends HadoopFileSystemLogStore {\n    private static final Logger LOG = LoggerFactory.getLogger(BaseExternalLogStore.class);\n\n    /**\n     * A global path lock to ensure that no two writers/readers are copying a given T(N) into N.json\n     * at the same time within the same JVM. This can occur\n     * - while a writer is performing a normal write operation AND a reader happens to see an\n     *   external entry E(complete=false) and so starts a recovery operation\n     * - while two readers see E(complete=false) and so both start a recovery operation\n     */\n    private static final PathLock pathLock = new PathLock();\n\n    /**\n     * The delay, in seconds, after an external entry has been committed to the delta log at which\n     * point it is safe to be deleted from the external store.\n     *\n     * We want a delay long enough such that, after the external entry has been deleted, another\n     * write attempt for the SAME delta log commit can FAIL using ONLY the FileSystem's existence\n     * check (e.g. `fs.exists(path)`). Recall we assume that the FileSystem does not provide mutual\n     * exclusion.\n     *\n     * We use a value of 1 day.\n     *\n     * If we choose too small of a value, like 0 seconds, then the following scenario is possible:\n     * - t0:  Writers W1 and W2 start writing data files\n     * - t1:  W1 begins to try and write into the _delta_log.\n     * - t2:  W1 checks if N.json exists in FileSystem. It doesn't.\n     * - t3:  W1 writes actions into temp file T1(N)\n     * - t4:  W1 writes to external store entry E1(N, complete=false)\n     * - t5:  W1 copies (with overwrite=false) T1(N) into N.json.\n     * - t6:  W1 overwrites entry in external store E1(N, complete=true, expireTime=now+0)\n     * - t7:  E1 is safe to be deleted, and some external store TTL mechanism deletes E1\n     * - t8:  W2 begins to try and write into the _delta_log.\n     * - t9:  W1 checks if N.json exists in FileSystem, but too little time has transpired between\n     *        t5 and t9 that the FileSystem check (fs.exists(path)) returns FALSE.\n     *        Note: This isn't possible on S3 (which provides strong consistency) but could be\n     *        possible on eventually-consistent systems.\n     * - t10: W2 writes actions into temp file T2(N)\n     * - t11: W2 writes to external store entry E2(N, complete=false)\n     * - t12: W2 successfully copies (with overwrite=false) T2(N) into N.json. FileSystem didn't\n     *        provide the necessary mutual exclusion, so the copy succeeded. Thus, DATA LOSS HAS\n     *        OCCURRED.\n     *\n     * By using an expiration delay of 1 day, we ensure one of the steps at t9 or t12 will fail.\n     */\n    protected static final long DEFAULT_EXTERNAL_ENTRY_EXPIRATION_DELAY_SECONDS =\n        TimeUnit.DAYS.toSeconds(1);\n\n    /**\n     * Completed external commit entries will be created with a value of\n     * NOW_EPOCH_SECONDS + getExpirationDelaySeconds().\n     */\n    protected long getExpirationDelaySeconds() {\n        return DEFAULT_EXTERNAL_ENTRY_EXPIRATION_DELAY_SECONDS;\n    }\n\n    ////////////////////////\n    // Public API Methods //\n    ////////////////////////\n\n    public BaseExternalLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    /**\n     * First checks if there is any incomplete entry in the external store. If so, tries to perform\n     * a recovery/fix.\n     *\n     * Then, performs a normal listFrom user the `super` implementation.\n     */\n    @Override\n    public Iterator<FileStatus> listFrom(Path path, Configuration hadoopConf) throws IOException {\n        final FileSystem fs = path.getFileSystem(hadoopConf);\n        final Path resolvedPath = stripUserInfo(fs.makeQualified(path));\n\n        // VACUUM operations may use this LogStore::listFrom API. We don't need to attempt to\n        // perform a fix/recovery during such operations that are not listing the _delta_log.\n        if (isDeltaLogPath(resolvedPath)) {\n            final Path tablePath = getTablePath(resolvedPath);\n            final Optional<ExternalCommitEntry> entry = getLatestExternalEntry(tablePath);\n\n            if (entry.isPresent() && !entry.get().complete) {\n                // Note: `fixDeltaLog` will apply per-JVM mutual exclusion via a lock to help reduce\n                // the chance of many reader threads in a single JVM doing duplicate copies of\n                // T(N) -> N.json.\n                fixDeltaLog(fs, entry.get());\n            }\n        }\n\n        // This is predicated on the storage system providing consistent listing\n        // If there was a recovery performed in the `fixDeltaLog` call, then some temp file\n        // was just copied into some N.json in the delta log. Because of consistent listing,\n        // the `super.listFrom` is guaranteed to see N.json.\n        return super.listFrom(path, hadoopConf);\n    }\n\n    /**\n     * If overwrite=true, then write normally without any interaction with external store.\n     * Else, to commit for delta version N:\n     * - Step 0: Fail if N.json already exists in FileSystem.\n     * - Step 1: Ensure that N-1.json exists. If not, perform a recovery.\n     * - Step 2: PREPARE the commit.\n     *      - Write `actions` into temp file T(N)\n     *      - Write with mutual exclusion to external store and entry E(N, T(N), complete=false)\n     * - Step 3: COMMIT the commit to the delta log.\n     *      - Copy T(N) into N.json\n     * - Step 4: ACKNOWLEDGE the commit.\n     *      - Overwrite entry E in external store and set complete=true\n     */\n    @Override\n    public void write(\n            Path path,\n            Iterator<String> actions,\n            Boolean overwrite,\n            Configuration hadoopConf) throws IOException {\n        final FileSystem fs = path.getFileSystem(hadoopConf);\n        final Path resolvedPath = stripUserInfo(fs.makeQualified(path));\n        try {\n            // Prevent concurrent writers in this JVM from either\n            // a) concurrently overwriting N.json if overwrite=true\n            // b) both checking if N-1.json exists and performing a \"recovery\" where they both\n            //    copy T(N-1) into N-1.json\n            //\n            // Note that the mutual exclusion on writing into N.json with overwrite=false from\n            // different JVMs (which is the entire point of BaseExternalLogStore) is provided by the\n            // external cache, not by this lock, of course.\n            //\n            // Also note that this lock path (resolvedPath) is for N.json, while the lock path used\n            // below in the recovery `fixDeltaLog` path is for N-1.json. Thus, no deadlock.\n            pathLock.acquire(resolvedPath);\n\n            if (overwrite) {\n                writeActions(fs, path, actions);\n                return;\n            } else if (fs.exists(path)) {\n                // Step 0: Fail if N.json already exists in FileSystem and overwrite=false.\n                throw new java.nio.file.FileAlreadyExistsException(path.toString());\n            }\n\n            // Step 1: Ensure that N-1.json exists\n            final Path tablePath = getTablePath(resolvedPath);\n            if (FileNameUtils.isDeltaFile(path)) {\n                final long version = FileNameUtils.deltaVersion(path);\n                if (version > 0) {\n                    final long prevVersion = version - 1;\n                    final Path deltaLogPath = new Path(tablePath, \"_delta_log\");\n                    final Path prevPath = FileNameUtils.deltaFile(deltaLogPath, prevVersion);\n                    final String prevFileName = prevPath.getName();\n                    final Optional<ExternalCommitEntry> prevEntry = getExternalEntry(\n                        tablePath.toString(),\n                        prevFileName\n                    );\n                    if (prevEntry.isPresent() && !prevEntry.get().complete) {\n                        fixDeltaLog(fs, prevEntry.get());\n                    } else {\n                        if (!fs.exists(prevPath)) {\n                            throw new java.nio.file.FileSystemException(\n                                String.format(\"previous commit %s doesn't exist on the file system but does in the external log store\", prevPath)\n                            );\n                        }\n                    }\n                } else {\n                    final String fileName = path.getName();\n                    final Optional<ExternalCommitEntry> entry = getExternalEntry(\n                        tablePath.toString(),\n                        fileName\n                    );\n                    if (entry.isPresent()) {\n                        if (entry.get().complete && !fs.exists(path)) {\n                            throw new java.nio.file.FileSystemException(\n                                String.format(\n                                    \"Old entries for table %s still exist in the external log store\",\n                                    tablePath\n                                )\n                            );\n                        }\n                    }\n                }\n            }\n\n            // Step 2: PREPARE the commit\n            final String tempPath = createTemporaryPath(resolvedPath);\n            final ExternalCommitEntry entry = new ExternalCommitEntry(\n                tablePath,\n                resolvedPath.getName(),\n                tempPath,\n                false, // not complete\n                null // no expireTime\n            );\n\n            // Step 2.1: Create temp file T(N)\n            writeActions(fs, entry.absoluteTempPath(), actions);\n\n            // Step 2.2: Create externals store entry E(N, T(N), complete=false)\n            putExternalEntry(entry, false); // overwrite=false\n\n            try {\n                // Step 3: COMMIT the commit to the delta log.\n                //         Copy T(N) -> N.json with overwrite=false\n                writeCopyTempFile(fs, entry.absoluteTempPath(), resolvedPath);\n\n                // Step 4: ACKNOWLEDGE the commit\n                writePutCompleteDbEntry(entry);\n            } catch (Throwable e) {\n                LOG.info(\n                    \"{}: ignoring recoverable error\", e.getClass().getSimpleName(), e\n                );\n            }\n        } catch (java.lang.InterruptedException e) {\n            throw new InterruptedIOException(e.getMessage());\n        } finally {\n            pathLock.release(resolvedPath);\n        }\n    }\n\n    @Override\n    public Boolean isPartialWriteVisible(Path path, Configuration hadoopConf) {\n        return false;\n    }\n\n    /////////////////////////////////////////////////////////////\n    // Protected Members (for interaction with external store) //\n    /////////////////////////////////////////////////////////////\n\n    /**\n     * Write file with actions under a specific path.\n     */\n    protected void writeActions(\n        FileSystem fs,\n        Path path,\n        Iterator<String> actions\n    ) throws IOException {\n        LOG.debug(\"writeActions to: {}\", path);\n        FSDataOutputStream stream = fs.create(path, true);\n        while (actions.hasNext()) {\n            byte[] line = String.format(\"%s\\n\", actions.next()).getBytes(StandardCharsets.UTF_8);\n            stream.write(line);\n        }\n        stream.close();\n    }\n\n    /**\n     * Generate temporary path for TransactionLog.\n     */\n    protected String createTemporaryPath(Path path) {\n        String uuid = java.util.UUID.randomUUID().toString();\n        return String.format(\".tmp/%s.%s\", path.getName(), uuid);\n    }\n\n    /**\n     * Returns the base table path for a given Delta log entry located in\n     * e.g. input path of $tablePath/_delta_log/00000N.json would return $tablePath\n     */\n    protected Path getTablePath(Path path) {\n        return path.getParent().getParent();\n    }\n\n    /**\n     * Write to external store in exclusive way.\n     *\n     * @throws java.nio.file.FileAlreadyExistsException if path exists in cache and `overwrite` is\n     *                                                  false\n     */\n    abstract protected void putExternalEntry(\n        ExternalCommitEntry entry,\n        boolean overwrite) throws IOException;\n\n    /**\n     * Return external store entry corresponding to delta log file with given `tablePath` and\n     * `fileName`, or `Optional.empty()` if it doesn't exist.\n     */\n    abstract protected Optional<ExternalCommitEntry> getExternalEntry(\n        String tablePath,\n        String fileName) throws IOException;\n\n    /**\n     * Return the latest external store entry corresponding to the delta log for given `tablePath`,\n     * or `Optional.empty()` if it doesn't exist.\n     */\n    abstract protected Optional<ExternalCommitEntry> getLatestExternalEntry(\n        Path tablePath) throws IOException;\n\n    //////////////////////////////////////////////////////////\n    // Protected Members (for error injection during tests) //\n    //////////////////////////////////////////////////////////\n\n    /**\n     * Wrapper for `copyFile`, called by the `write` method.\n     */\n    @VisibleForTesting\n    protected void writeCopyTempFile(FileSystem fs, Path src, Path dst) throws IOException {\n        copyFile(fs, src, dst);\n    }\n\n    /**\n     * Wrapper for `putExternalEntry`, called by the `write` method.\n     */\n    @VisibleForTesting\n    protected void writePutCompleteDbEntry(ExternalCommitEntry entry) throws IOException {\n        putExternalEntry(entry.asComplete(getExpirationDelaySeconds()), true); // overwrite=true\n    }\n\n    /**\n     * Wrapper for `copyFile`, called by the `fixDeltaLog` method.\n     */\n    @VisibleForTesting\n    protected void fixDeltaLogCopyTempFile(FileSystem fs, Path src, Path dst) throws IOException {\n        copyFile(fs, src, dst);\n    }\n\n    /**\n     * Wrapper for `putExternalEntry`, called by the `fixDeltaLog` method.\n     */\n    @VisibleForTesting\n    protected void fixDeltaLogPutCompleteDbEntry(ExternalCommitEntry entry) throws IOException {\n        putExternalEntry(entry.asComplete(getExpirationDelaySeconds()), true); // overwrite=true\n    }\n\n    ////////////////////\n    // Helper Methods //\n    ////////////////////\n\n    /**\n     * Method for assuring consistency on filesystem according to the external cache.\n     * Method tries to rewrite TransactionLog entry from temporary path if it does not exist.\n     *\n     * Should never throw a FileAlreadyExistsException.\n     * - If we see one when copying the temp file, we can assume the target file N.json already\n     *   exists and a concurrent writer has already copied the contents of T(N).\n     * - We will never see one when writing to the external cache since overwrite=true.\n     */\n    private void fixDeltaLog(FileSystem fs, ExternalCommitEntry entry) throws IOException {\n        if (entry.complete) {\n            return;\n        }\n\n        final Path targetPath = entry.absoluteFilePath();\n        try {\n            pathLock.acquire(targetPath);\n\n            int retry = 0;\n            boolean copied = false;\n            while (true) {\n                LOG.info(\"trying to fix: {}\", entry.fileName);\n                try {\n                    if (!copied && !fs.exists(targetPath)) {\n                        fixDeltaLogCopyTempFile(fs, entry.absoluteTempPath(), targetPath);\n                        copied = true;\n                    }\n                    fixDeltaLogPutCompleteDbEntry(entry);\n                    LOG.info(\"fixed file {}\", entry.fileName);\n                    return;\n                } catch (java.nio.file.FileAlreadyExistsException e) {\n                    LOG.info(\"file {} already copied: {}:\",\n                        entry.fileName, e.getClass().getSimpleName(), e);\n                    copied = true;\n                    // Don't return since we still need to mark the DB entry as complete. This will\n                    // happen when we execute the main try block on the next while loop iteration\n                } catch (Throwable e) {\n                    LOG.info(\"{}:\", e.getClass().getSimpleName(), e);\n                    if (retry >= 3) {\n                        throw e;\n                    }\n                }\n                retry += 1;\n            }\n        } catch (java.lang.InterruptedException e) {\n            throw new InterruptedIOException(e.getMessage());\n        } finally {\n            pathLock.release(targetPath);\n        }\n    }\n\n   /**\n    * Copies file within filesystem.\n    *\n    * @param fs reference to [[FileSystem]]\n    * @param src path to source file\n    * @param dst path to destination file\n    */\n    private void copyFile(FileSystem fs, Path src, Path dst) throws IOException {\n        LOG.info(\"copy file: {} -> {}\", src, dst);\n        final FSDataInputStream inputStream = fs.open(src);\n        try {\n            final FSDataOutputStream outputStream = fs.create(dst, false); // overwrite=false\n            IOUtils.copy(inputStream, outputStream);\n\n            // We don't close `outputStream` if an exception happens because it may create a partial\n            // file.\n            outputStream.close();\n        } catch (org.apache.hadoop.fs.FileAlreadyExistsException e) {\n            throw new java.nio.file.FileAlreadyExistsException(dst.toString());\n        } finally {\n            inputStream.close();\n        }\n    }\n\n    /**\n     * Returns path stripped user info.\n     */\n    private Path stripUserInfo(Path path) {\n        final URI uri = path.toUri();\n\n        try {\n            final URI newUri = new URI(\n                uri.getScheme(),\n                null, // userInfo\n                uri.getHost(),\n                uri.getPort(),\n                uri.getPath(),\n                uri.getQuery(),\n                uri.getFragment()\n            );\n\n            return new Path(newUri);\n        } catch (URISyntaxException e) {\n            // Propagating this URISyntaxException to callers would mean we would have to either\n            // include it in the public LogStore.java interface or wrap it in an\n            // IllegalArgumentException somewhere else. Instead, catch and wrap it here.\n            throw new IllegalArgumentException(e);\n        }\n    }\n\n    /** Returns true if this path is contained within a _delta_log folder. */\n    @VisibleForTesting\n    protected boolean isDeltaLogPath(Path normalizedPath) {\n        return Arrays.stream(normalizedPath\n            .toUri()\n            .toString()\n            .split(Path.SEPARATOR)\n        ).anyMatch(\"_delta_log\"::equals);\n    }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/main/java/io/delta/storage/ExternalCommitEntry.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport org.apache.hadoop.fs.Path;\n\n/**\n * Wrapper class representing an entry in an external store for a given commit into the Delta log.\n *\n * Contains relevant fields and helper methods.\n */\npublic final class ExternalCommitEntry {\n\n    /**\n     * Absolute path to this delta table\n     */\n    public final Path tablePath;\n\n    /**\n     * File name of this commit, e.g. \"000000N.json\"\n     */\n    public final String fileName;\n\n    /**\n     * Path to temp file for this commit, relative to the `_delta_log\n     */\n    public final String tempPath;\n\n    /**\n     * true if delta json file is successfully copied to its destination location, else false\n     */\n    public final boolean complete;\n\n    /**\n     * If complete = true, epoch seconds at which this external commit entry is safe to be deleted.\n     * Else, null.\n     */\n    public final Long expireTime;\n\n    public ExternalCommitEntry(\n            Path tablePath,\n            String fileName,\n            String tempPath,\n            boolean complete,\n            Long expireTime) {\n        this.tablePath = tablePath;\n        this.fileName = fileName;\n        this.tempPath = tempPath;\n        this.complete = complete;\n        this.expireTime = expireTime;\n    }\n\n    /**\n     * @return this entry with `complete=true` and a valid `expireTime`\n     */\n    public ExternalCommitEntry asComplete(long expirationDelaySeconds) {\n        return new ExternalCommitEntry(\n            this.tablePath,\n            this.fileName,\n            this.tempPath,\n            true,\n            System.currentTimeMillis() / 1000L + expirationDelaySeconds\n        );\n    }\n\n    /**\n     * @return the absolute path to the file for this entry.\n     * e.g. $tablePath/_delta_log/0000000N.json\n     */\n    public Path absoluteFilePath() {\n        return new Path(new Path(tablePath, \"_delta_log\"), fileName);\n    }\n\n    /**\n     * @return the absolute path to the temp file for this entry\n     */\n    public Path absoluteTempPath() {\n        return new Path(new Path(tablePath, \"_delta_log\"), tempPath);\n    }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/main/java/io/delta/storage/RetryableCloseableIterator.java",
    "content": "package io.delta.storage;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.NoSuchElementException;\nimport java.util.Objects;\nimport java.util.function.Supplier;\n\nimport io.delta.storage.utils.ThrowingSupplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * This class presents an iterator view over the iterator supplier in the constructor.\n *\n * This class assumes that the iterator supplied by the supplier can throw, and that subsequent\n * supplier.get() calls will return an iterator over the same data.\n *\n * If there are any RemoteFileChangedException during `next` and `hasNext` calls, will retry\n * at most `MAX_RETRIES` times. If there are similar exceptions during the retry, those are handled\n * and count towards the MAX_RETRIES.\n *\n * Internally, keeps track of the last-successfully-returned index. Upon retry, will iterate back\n * to that same position.\n */\npublic class RetryableCloseableIterator implements CloseableIterator<String> {\n    private static final Logger LOG = LoggerFactory.getLogger(RetryableCloseableIterator.class);\n\n    public static final int DEFAULT_MAX_RETRIES = 3;\n\n    private final ThrowingSupplier<CloseableIterator<String>, IOException> iterSupplier;\n\n    private final int maxRetries;\n\n    /**\n     * Index of the last element successfully returned without an exception. A value of -1 means\n     * that no element has ever been returned yet.\n     */\n    private int lastSuccessfullIndex;\n\n    private int numRetries = 0;\n\n    private CloseableIterator<String> currentIter;\n\n    public RetryableCloseableIterator(\n            ThrowingSupplier<CloseableIterator<String>, IOException> iterSupplier,\n            int maxRetries) throws IOException {\n        if (maxRetries < 0) throw new IllegalArgumentException(\"maxRetries can't be negative\");\n\n        this.iterSupplier = Objects.requireNonNull(iterSupplier);\n        this.maxRetries = maxRetries;\n        this.lastSuccessfullIndex = -1;\n        this.currentIter = this.iterSupplier.get();\n    }\n\n    public RetryableCloseableIterator(\n            ThrowingSupplier<CloseableIterator<String>, IOException> iterSupplier)\n        throws IOException {\n\n        this(iterSupplier, DEFAULT_MAX_RETRIES);\n    }\n\n    /////////////////\n    // Public APIs //\n    /////////////////\n\n    @Override\n    public void close() throws IOException {\n        currentIter.close();\n    }\n\n    /**\n     * `hasNext` must be idempotent. It does not change the `lastSuccessfulIndex` variable.\n     */\n    @Override\n    public boolean hasNext() {\n        try {\n            return hasNextInternal();\n        } catch (IOException ex) {\n            if (isRemoteFileChangedException(ex)) {\n                try {\n                    replayIterToLastSuccessfulIndex(ex);\n                } catch (IOException ex2) {\n                    throw new UncheckedIOException(ex2);\n                }\n                return hasNext();\n            } else {\n                throw new UncheckedIOException(ex);\n            }\n\n        }\n    }\n\n    @Override\n    public String next() {\n        if (!hasNext()) throw new NoSuchElementException();\n\n        try {\n            final String ret = nextInternal();\n            lastSuccessfullIndex++;\n            return ret;\n        } catch (IOException ex) {\n            if (isRemoteFileChangedException(ex)) {\n                try {\n                    replayIterToLastSuccessfulIndex(ex);\n                } catch (IOException ex2) {\n                    throw new UncheckedIOException(ex2);\n                }\n\n                if (!hasNext()) {\n                    throw new IllegalStateException(\n                        String.format(\n                            \"A retried iterator doesn't have enough data \" +\n                                \"(hasNext=false, lastSuccessfullIndex=%s)\",\n                            lastSuccessfullIndex\n                        )\n                    );\n                }\n\n                return next();\n            } else {\n                throw new UncheckedIOException(ex);\n            }\n        }\n    }\n\n    //////////////////////////////////////\n    // Package-private APIs for testing //\n    //////////////////////////////////////\n\n    /** Visible for testing. */\n    int getLastSuccessfullIndex() {\n        return lastSuccessfullIndex;\n    }\n\n    /** Visible for testing. */\n    int getNumRetries() {\n        return numRetries;\n    }\n\n    ////////////////////\n    // Helper Methods //\n    ////////////////////\n\n    /** Throw a checked exception so we can catch this in the caller. */\n    private boolean hasNextInternal() throws IOException {\n        return currentIter.hasNext();\n    }\n\n    /** Throw a checked exception so we can catch this in the caller. */\n    private String nextInternal() throws IOException {\n        return currentIter.next();\n    }\n\n    /**\n     * Called after a RemoteFileChangedException was thrown. Tries to replay the underlying\n     * iter implementation (supplied by the `implSupplier`) to the last successful index, so that\n     * the previous error open (hasNext, or next) can be retried. If a RemoteFileChangedException\n     * is thrown while replaying the iter, we just increment the `numRetries` counter and try again.\n     */\n    private void replayIterToLastSuccessfulIndex(IOException topLevelEx) throws IOException {\n        LOG.warn(\n            \"Caught a RemoteFileChangedException. NumRetries is {} / {}.\\n{}\",\n            numRetries + 1, maxRetries, topLevelEx\n        );\n        currentIter.close();\n\n        while (numRetries < maxRetries) {\n            numRetries++;\n            LOG.info(\n                \"Replaying until (inclusive) index {}. NumRetries is {} / {}.\",\n                lastSuccessfullIndex, numRetries + 1, maxRetries\n            );\n            currentIter = iterSupplier.get();\n\n            // Last successful index replayed. Starts at -1, and not 0, because 0 means we've\n            // already replayed the 1st element!\n            int replayIndex = -1;\n            try {\n                while (replayIndex < lastSuccessfullIndex) {\n                    if (currentIter.hasNext()) {\n                        currentIter.next(); // Disregard data that has been read\n                        replayIndex++;\n                    } else {\n                        throw new IllegalStateException(\n                            String.format(\n                                \"A retried iterator doesn't have enough data \" +\n                                    \"(replayIndex=%s, lastSuccessfullIndex=%s)\",\n                                replayIndex,\n                                lastSuccessfullIndex\n                            )\n                        );\n                    }\n                }\n\n                // Just like how in RetryableCloseableIterator::next we have to handle\n                // RemoteFileChangedException, we must also hadnle that here during the replay.\n                // `currentIter.next()` isn't declared to throw a RemoteFileChangedException, so we\n                // trick the compiler into thinking this block can throw RemoteFileChangedException\n                // via `fakeIOException`. That way, we can catch it, and retry replaying the iter.\n                fakeIOException();\n\n                LOG.info(\"Successfully replayed until (inclusive) index {}\", lastSuccessfullIndex);\n\n                return;\n            } catch (IOException ex) {\n                if (isRemoteFileChangedException(ex)) {\n                    // Ignore and try replaying the iter again at the top of the while loop\n                    LOG.warn(\"Caught a RemoteFileChangedException while replaying the iterator\");\n                } else {\n                    throw ex;\n                }\n            }\n        }\n\n        throw topLevelEx;\n    }\n\n    private boolean isRemoteFileChangedException(IOException ex) {\n        // `endsWith` should still work if the class is shaded.\n        final String exClassName = ex.getClass().getName();\n        return exClassName.endsWith(\"org.apache.hadoop.fs.s3a.RemoteFileChangedException\");\n    }\n\n    private void fakeIOException() throws IOException {\n        if (false) {\n            throw new IOException();\n        }\n    }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/main/java/io/delta/storage/S3DynamoDBLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport io.delta.storage.utils.ReflectionUtils;\nimport org.apache.hadoop.fs.Path;\n\nimport java.io.InterruptedIOException;\nimport java.io.UncheckedIOException;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.io.IOException;\n\nimport org.apache.hadoop.conf.Configuration;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;\nimport com.amazonaws.services.dynamodbv2.model.AttributeDefinition;\nimport com.amazonaws.services.dynamodbv2.model.AttributeValue;\nimport com.amazonaws.services.dynamodbv2.model.ComparisonOperator;\nimport com.amazonaws.services.dynamodbv2.model.Condition;\nimport com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;\nimport com.amazonaws.services.dynamodbv2.model.DescribeTableResult;\nimport com.amazonaws.services.dynamodbv2.model.TableDescription;\nimport com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;\nimport com.amazonaws.services.dynamodbv2.model.GetItemRequest;\nimport com.amazonaws.services.dynamodbv2.model.KeySchemaElement;\nimport com.amazonaws.services.dynamodbv2.model.KeyType;\nimport com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;\nimport com.amazonaws.services.dynamodbv2.model.PutItemRequest;\nimport com.amazonaws.services.dynamodbv2.model.QueryRequest;\nimport com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;\nimport com.amazonaws.services.dynamodbv2.model.ResourceInUseException;\nimport com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;\nimport com.amazonaws.regions.Region;\nimport com.amazonaws.regions.Regions;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A concrete implementation of {@link BaseExternalLogStore} that uses an external DynamoDB table\n * to provide the mutual exclusion during calls to `putExternalEntry`.\n *\n * DynamoDB entries are of form\n * - key\n * -- tablePath (HASH, STRING)\n * -- filename (RANGE, STRING)\n *\n * - attributes\n * -- tempPath (STRING, relative to _delta_log)\n * -- complete (STRING, representing boolean, \"true\" or \"false\")\n * -- commitTime (NUMBER, epoch seconds)\n */\npublic class S3DynamoDBLogStore extends BaseExternalLogStore {\n    private static final Logger LOG = LoggerFactory.getLogger(S3DynamoDBLogStore.class);\n\n    /**\n     * Configuration keys for the DynamoDB client.\n     *\n     * Keys are either of the form $SPARK_CONF_PREFIX.$CONF or $BASE_CONF_PREFIX.$CONF,\n     * e.g. spark.io.delta.storage.S3DynamoDBLogStore.ddb.tableName\n     * or io.delta.storage.S3DynamoDBLogStore.ddb.tableName\n     */\n    public static final String SPARK_CONF_PREFIX = \"spark.io.delta.storage.S3DynamoDBLogStore\";\n    public static final String BASE_CONF_PREFIX = \"io.delta.storage.S3DynamoDBLogStore\";\n    public static final String READ_RETRIES = \"read.retries\";\n    public static final String DDB_CLIENT_TABLE = \"ddb.tableName\";\n    public static final String DDB_CLIENT_REGION = \"ddb.region\";\n    public static final String DDB_CLIENT_CREDENTIALS_PROVIDER = \"credentials.provider\";\n    public static final String DDB_CREATE_TABLE_RCU = \"provisionedThroughput.rcu\";\n    public static final String DDB_CREATE_TABLE_WCU = \"provisionedThroughput.wcu\";\n\n    // WARNING: setting this value too low can cause data loss. Defaults to a duration of 1 day.\n    public static final String TTL_SECONDS = \"ddb.ttl\";\n\n    /**\n     * DynamoDB table attribute keys\n     */\n    private static final String ATTR_TABLE_PATH = \"tablePath\";\n    private static final String ATTR_FILE_NAME = \"fileName\";\n    private static final String ATTR_TEMP_PATH = \"tempPath\";\n    private static final String ATTR_COMPLETE = \"complete\";\n    private static final String ATTR_EXPIRE_TIME = \"expireTime\";\n\n    /**\n     * Member fields\n     */\n    private final AmazonDynamoDBClient client;\n    private final String tableName;\n    private final String credentialsProviderName;\n    private final String regionName;\n    private final long expirationDelaySeconds;\n\n    public S3DynamoDBLogStore(Configuration hadoopConf) throws IOException {\n        super(hadoopConf);\n\n        tableName = getParam(hadoopConf, DDB_CLIENT_TABLE, \"delta_log\");\n        credentialsProviderName = getParam(\n            hadoopConf,\n            DDB_CLIENT_CREDENTIALS_PROVIDER,\n            \"com.amazonaws.auth.DefaultAWSCredentialsProviderChain\"\n        );\n        regionName = getParam(hadoopConf, DDB_CLIENT_REGION, \"us-east-1\");\n\n        final String ttl = getParam(hadoopConf, TTL_SECONDS, null);\n        expirationDelaySeconds = ttl == null ?\n            BaseExternalLogStore.DEFAULT_EXTERNAL_ENTRY_EXPIRATION_DELAY_SECONDS :\n            Long.parseLong(ttl);\n        if (expirationDelaySeconds < 0) {\n            throw new IllegalArgumentException(\n                String.format(\n                    \"Can't use negative `%s` value of %s\", TTL_SECONDS, expirationDelaySeconds));\n        }\n\n        LOG.info(\"using tableName {}\", tableName);\n        LOG.info(\"using credentialsProviderName {}\", credentialsProviderName);\n        LOG.info(\"using regionName {}\", regionName);\n        LOG.info(\"using ttl (seconds) {}\", expirationDelaySeconds);\n\n        client = getClient();\n        tryEnsureTableExists(hadoopConf);\n    }\n\n    @Override\n    public CloseableIterator<String> read(Path path, Configuration hadoopConf) throws IOException {\n        // With many concurrent readers/writers, there's a chance that concurrent 'recovery'\n        // operations occur on the same file, i.e. the same temp file T(N) is copied into the target\n        // N.json file more than once. Though data loss will *NOT* occur, readers of N.json may\n        // receive a RemoteFileChangedException from S3 as the ETag of N.json was changed. This is\n        // safe to retry, so we do so here.\n        final int maxRetries = Integer.parseInt(\n            getParam(\n                hadoopConf,\n                READ_RETRIES,\n                Integer.toString(RetryableCloseableIterator.DEFAULT_MAX_RETRIES)\n            )\n        );\n\n        return new RetryableCloseableIterator(() -> super.read(path, hadoopConf), maxRetries);\n    }\n\n    @Override\n    protected long getExpirationDelaySeconds() {\n        return expirationDelaySeconds;\n    }\n\n    @Override\n    protected void putExternalEntry(\n            ExternalCommitEntry entry,\n            boolean overwrite) throws IOException {\n        try {\n            LOG.debug(String.format(\"putItem %s, overwrite: %s\", entry, overwrite));\n            client.putItem(createPutItemRequest(entry, overwrite));\n        } catch (ConditionalCheckFailedException e) {\n            LOG.debug(e.toString());\n            throw new java.nio.file.FileAlreadyExistsException(\n                entry.absoluteFilePath().toString()\n            );\n        }\n    }\n\n    @Override\n    protected Optional<ExternalCommitEntry> getExternalEntry(\n            String tablePath,\n            String fileName) {\n        final Map<String, AttributeValue> attributes = new ConcurrentHashMap<>();\n        attributes.put(ATTR_TABLE_PATH, new AttributeValue(tablePath));\n        attributes.put(ATTR_FILE_NAME, new AttributeValue(fileName));\n\n        Map<String, AttributeValue> item = client.getItem(\n            new GetItemRequest(tableName, attributes).withConsistentRead(true)\n        ).getItem();\n\n        return item != null ? Optional.of(dbResultToCommitEntry(item)) : Optional.empty();\n    }\n\n    @Override\n    protected Optional<ExternalCommitEntry> getLatestExternalEntry(Path tablePath) {\n        final Map<String, Condition> conditions = new ConcurrentHashMap<>();\n        conditions.put(\n            ATTR_TABLE_PATH,\n            new Condition()\n                .withComparisonOperator(ComparisonOperator.EQ)\n                .withAttributeValueList(new AttributeValue(tablePath.toString()))\n        );\n\n        final List<Map<String,AttributeValue>> items = client.query(\n            new QueryRequest(tableName)\n                .withConsistentRead(true)\n                .withScanIndexForward(false)\n                .withLimit(1)\n                .withKeyConditions(conditions)\n        ).getItems();\n\n        if (items.isEmpty()) {\n            return Optional.empty();\n        } else {\n            return Optional.of(dbResultToCommitEntry(items.get(0)));\n        }\n    }\n\n    /**\n     * Map a DBB query result item to an {@link ExternalCommitEntry}.\n     */\n    private ExternalCommitEntry dbResultToCommitEntry(Map<String, AttributeValue> item) {\n        final AttributeValue expireTimeAttr = item.get(ATTR_EXPIRE_TIME);\n        return new ExternalCommitEntry(\n            new Path(item.get(ATTR_TABLE_PATH).getS()),\n            item.get(ATTR_FILE_NAME).getS(),\n            item.get(ATTR_TEMP_PATH).getS(),\n            item.get(ATTR_COMPLETE).getS().equals(\"true\"),\n            expireTimeAttr != null ? Long.parseLong(expireTimeAttr.getN()) : null\n        );\n    }\n\n    private PutItemRequest createPutItemRequest(ExternalCommitEntry entry, boolean overwrite) {\n        final Map<String, AttributeValue> attributes = new ConcurrentHashMap<>();\n        attributes.put(ATTR_TABLE_PATH, new AttributeValue(entry.tablePath.toString()));\n        attributes.put(ATTR_FILE_NAME, new AttributeValue(entry.fileName));\n        attributes.put(ATTR_TEMP_PATH, new AttributeValue(entry.tempPath));\n        attributes.put(\n            ATTR_COMPLETE,\n            new AttributeValue().withS(Boolean.toString(entry.complete))\n        );\n\n        if (entry.expireTime != null) {\n            attributes.put(\n                ATTR_EXPIRE_TIME,\n                new AttributeValue().withN(entry.expireTime.toString())\n            );\n        }\n\n        final PutItemRequest pr = new PutItemRequest(tableName, attributes);\n\n        if (!overwrite) {\n            Map<String, ExpectedAttributeValue> expected = new ConcurrentHashMap<>();\n            expected.put(ATTR_FILE_NAME, new ExpectedAttributeValue(false));\n            pr.withExpected(expected);\n        }\n\n        return pr;\n    }\n\n    private void tryEnsureTableExists(Configuration hadoopConf) throws IOException {\n        int retries = 0;\n        boolean created = false;\n        while(retries < 20) {\n            String status = \"CREATING\";\n            try {\n                // https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/model/TableDescription.html#getTableStatus--\n                DescribeTableResult result = client.describeTable(tableName);\n                TableDescription descr = result.getTable();\n                status = descr.getTableStatus();\n            } catch (ResourceNotFoundException e) {\n                final long rcu = Long.parseLong(getParam(hadoopConf, DDB_CREATE_TABLE_RCU, \"5\"));\n                final long wcu = Long.parseLong(getParam(hadoopConf, DDB_CREATE_TABLE_WCU, \"5\"));\n\n                LOG.info(\n                    \"DynamoDB table `{}` in region `{}` does not exist. \" +\n                    \"Creating it now with provisioned throughput of {} RCUs and {} WCUs.\",\n                    tableName, regionName, rcu, wcu);\n                try {\n                    client.createTable(\n                        // attributeDefinitions\n                        java.util.Arrays.asList(\n                            new AttributeDefinition(ATTR_TABLE_PATH, ScalarAttributeType.S),\n                            new AttributeDefinition(ATTR_FILE_NAME, ScalarAttributeType.S)\n                        ),\n                        tableName,\n                        // keySchema\n                        Arrays.asList(\n                            new KeySchemaElement(ATTR_TABLE_PATH, KeyType.HASH),\n                            new KeySchemaElement(ATTR_FILE_NAME, KeyType.RANGE)\n                        ),\n                        new ProvisionedThroughput(rcu, wcu)\n                    );\n                    created = true;\n                } catch (ResourceInUseException e3) {\n                    // race condition - table just created by concurrent process\n                }\n            }\n            if (status.equals(\"ACTIVE\")) {\n                if (created) {\n                    LOG.info(\"Successfully created DynamoDB table `{}`\", tableName);\n                } else {\n                    LOG.info(\"Table `{}` already exists\", tableName);\n                }\n                break;\n            } else if (status.equals(\"CREATING\")) {\n                retries += 1;\n                LOG.info(\"Waiting for `{}` table creation\", tableName);\n                try {\n                    Thread.sleep(1000);\n                } catch(InterruptedException e) {\n                    throw new InterruptedIOException(e.getMessage());\n                }\n            } else {\n                LOG.error(\"table `{}` status: {}\", tableName, status);\n                break;  // TODO - raise exception?\n            }\n        };\n    }\n\n    private AmazonDynamoDBClient getClient() throws java.io.IOException {\n        try {\n            final AWSCredentialsProvider awsCredentialsProvider =\n                    ReflectionUtils.createAwsCredentialsProvider(credentialsProviderName, initHadoopConf());\n            final AmazonDynamoDBClient client = new AmazonDynamoDBClient(awsCredentialsProvider);\n            client.setRegion(Region.getRegion(Regions.fromName(regionName)));\n            return client;\n        } catch (ReflectiveOperationException e) {\n            throw new java.io.IOException(e);\n        }\n    }\n\n    /**\n     * Get the hadoopConf param $name that is prefixed either with $SPARK_CONF_PREFIX or\n     * $BASE_CONF_PREFIX.\n     *\n     * If two parameters exist, one for each prefix, then an IllegalArgumentException is thrown.\n     *\n     * If no parameters exist, then the $defaultValue is returned.\n     */\n    protected static String getParam(Configuration hadoopConf, String name, String defaultValue) {\n        final String sparkPrefixKey = String.format(\"%s.%s\", SPARK_CONF_PREFIX, name);\n        final String basePrefixKey = String.format(\"%s.%s\", BASE_CONF_PREFIX, name);\n\n        final String sparkPrefixVal = hadoopConf.get(sparkPrefixKey);\n        final String basePrefixVal = hadoopConf.get(basePrefixKey);\n\n        if (sparkPrefixVal != null &&\n            basePrefixVal != null &&\n            !sparkPrefixVal.equals(basePrefixVal)) {\n            throw new IllegalArgumentException(\n                String.format(\n                    \"Configuration properties `%s=%s` and `%s=%s` have different values. \" +\n                    \"Please set only one.\",\n                    sparkPrefixKey, sparkPrefixVal, basePrefixKey, basePrefixVal\n                )\n            );\n        }\n\n        if (sparkPrefixVal != null) return sparkPrefixVal;\n        if (basePrefixVal != null) return basePrefixVal;\n        return defaultValue;\n    }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/main/java/io/delta/storage/utils/ReflectionUtils.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.utils;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport org.apache.hadoop.conf.Configuration;\n\nimport java.util.Arrays;\n\npublic class ReflectionUtils {\n\n    private static boolean readsCredsFromHadoopConf(Class<?> awsCredentialsProviderClass) {\n        return Arrays.stream(awsCredentialsProviderClass.getConstructors())\n                .anyMatch(constructor -> constructor.getParameterCount() == 1 &&\n                        Arrays.equals(constructor.getParameterTypes(), new Class[]{Configuration.class}));\n    }\n\n    /**\n     * Create AWS credentials provider from given provider classname and {@link Configuration}.\n     *\n     * It first check if AWS Credentials Provider class has constructor Hadoop configuration as parameter.\n     *   If yes - create instance of class using this constructor.\n     *   If no - create instance with empty parameters constructor.\n     *\n     * @param credentialsProviderClassName Fully qualified name of the desired credentials provider class.\n     * @param hadoopConf Hadoop configuration, used to create instance of AWS credentials\n     *                                      provider, if supported.\n     * @return {@link AWSCredentialsProvider} object, instantiated from the class @see {credentialsProviderClassName}\n     * @throws ReflectiveOperationException When AWS credentials provider constrictor do not matched.\n     *                                      Means class has neither an constructor with no args as input\n     *                                      nor constructor with only Hadoop configuration as argument.\n     */\n    public static AWSCredentialsProvider createAwsCredentialsProvider(\n            String credentialsProviderClassName,\n            Configuration hadoopConf) throws ReflectiveOperationException {\n        Class<?> awsCredentialsProviderClass = Class.forName(credentialsProviderClassName);\n        if (readsCredsFromHadoopConf(awsCredentialsProviderClass))\n            return (AWSCredentialsProvider) awsCredentialsProviderClass\n                    .getConstructor(Configuration.class)\n                    .newInstance(hadoopConf);\n        else\n            return (AWSCredentialsProvider) awsCredentialsProviderClass.getConstructor().newInstance();\n    }\n\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/main/java/io/delta/storage/utils/ThrowingSupplier.java",
    "content": "package io.delta.storage.utils;\n\n@FunctionalInterface\npublic interface ThrowingSupplier<T, E extends Exception> {\n    T get() throws E;\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/test/java/io/delta/storage/FailingS3DynamoDBLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport java.io.IOException;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.FileSystem;\nimport org.apache.hadoop.fs.Path;\n\n/**\n * An ExternalLogStore implementation that allows for easy, probability-based error injection during\n * runtime.\n *\n * This is used to test the error-handling capabilities of S3DynamoDBLogStore during integration\n * tests.\n */\npublic class FailingS3DynamoDBLogStore extends S3DynamoDBLogStore {\n\n    private static java.util.Random rng = new java.util.Random();\n    private final ConcurrentHashMap<String, Float> errorRates;\n\n    public FailingS3DynamoDBLogStore(Configuration hadoopConf) throws IOException {\n        super(hadoopConf);\n        errorRates = new ConcurrentHashMap<>();\n\n        // for each optional key in set { write_copy_temp_file, write_put_db_entry,\n        // fix_delta_log_copy_temp_file, fix_delta_log_put_db_entry }, `errorRates` string is\n        // expected to be of form key1=value1,key2=value2 etc where each value is a fraction\n        // indicating how often that method should fail (e.g. 0.10 ==> 10% failure rate).\n        String errorRatesDef = getParam(hadoopConf, \"errorRates\", \"\");\n        for (String s: errorRatesDef.split(\",\")) {\n            if (!s.contains(\"=\")) continue;\n            String[] parts = s.split(\"=\", 2);\n            if (parts.length == 2) {\n                errorRates.put(parts[0], Float.parseFloat(parts[1]));\n            }\n        }\n    }\n\n    @Override\n    protected void writeCopyTempFile(FileSystem fs, Path src, Path dst) throws IOException {\n        injectError(\"write_copy_temp_file\");\n        super.writeCopyTempFile(fs, src, dst);\n    }\n\n    @Override\n    protected void writePutCompleteDbEntry(ExternalCommitEntry entry) throws IOException {\n        injectError(\"write_put_db_entry\");\n        super.writePutCompleteDbEntry(entry);\n    }\n\n    @Override\n    protected void fixDeltaLogCopyTempFile(FileSystem fs, Path src, Path dst) throws IOException {\n        injectError(\"fix_delta_log_copy_temp_file\");\n        super.fixDeltaLogCopyTempFile(fs, src, dst);\n    }\n\n    @Override\n    protected void fixDeltaLogPutCompleteDbEntry(ExternalCommitEntry entry) throws IOException {\n        injectError(\"fix_delta_log_put_db_entry\");\n        super.fixDeltaLogPutCompleteDbEntry(entry);\n    }\n\n    private void injectError(String name) throws IOException {\n      float rate = errorRates.getOrDefault(name, 0.1f);\n      if (rng.nextFloat() < rate) {\n          throw new IOException(String.format(\"injected failure: %s\", name));\n      }\n    }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/test/java/io/delta/storage/MemoryLogStore.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage;\n\nimport org.apache.hadoop.conf.Configuration;\nimport org.apache.hadoop.fs.Path;\nimport java.io.IOException;\nimport java.util.Comparator;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport java.util.Optional;\n\n/**\n * Simple ExternalLogStore implementation using an in-memory hashmap (as opposed to an actual\n * database)\n */\npublic class MemoryLogStore extends BaseExternalLogStore {\n    public static String IS_DELTA_LOG_PATH_OVERRIDE_KEY =\n        \"spark.hadoop.io.delta.storage.MemoryLogStore.isDeltaLogPath.alwaysTrue\";\n\n    public static int numGetLatestExternalEntryCalls = 0;\n\n    public MemoryLogStore(Configuration hadoopConf) {\n        super(hadoopConf);\n    }\n\n    ///////////////////\n    // API Overrides //\n    ///////////////////\n\n    @Override\n    protected void putExternalEntry(\n            ExternalCommitEntry entry,\n            boolean overwrite) throws IOException {\n        final String key = createKey(entry.tablePath.toString(), entry.fileName);\n        final ExternalCommitEntry correctedEntry = new ExternalCommitEntry(\n            // some tests use \"failing:\" scheme to inject errors, but we want to store normal paths\n            new Path(fixPathSchema(entry.tablePath.toString())),\n            entry.fileName,\n            entry.tempPath,\n            entry.complete,\n            entry.expireTime\n        );\n\n        if (overwrite) {\n            hashMap.put(key, correctedEntry);\n        } else if (hashMap.containsKey(key)) { // and overwrite=false\n            throw new java.nio.file.FileAlreadyExistsException(\"already exists\");\n        } else {\n            hashMap.put(key, correctedEntry);\n        }\n    }\n\n    @Override\n    protected Optional<ExternalCommitEntry> getExternalEntry(\n            String tablePath,\n            String fileName) {\n        final String key = createKey(tablePath, fileName);\n        if (hashMap.containsKey(key)) {\n            return Optional.of(hashMap.get(key));\n        }\n        return Optional.empty();\n    }\n\n    @Override\n    protected Optional<ExternalCommitEntry> getLatestExternalEntry(Path tablePath) {\n        numGetLatestExternalEntryCalls++;\n\n        final Path fixedTablePath = new Path(fixPathSchema(tablePath.toString()));\n        return hashMap\n            .values()\n            .stream()\n            .filter(item -> item.tablePath.equals(fixedTablePath))\n            .max(Comparator.comparing(ExternalCommitEntry::absoluteFilePath));\n    }\n\n    @Override\n    protected boolean isDeltaLogPath(Path normalizedPath) {\n        if (initHadoopConf().getBoolean(IS_DELTA_LOG_PATH_OVERRIDE_KEY, false)) {\n            return true; // hardcoded to return true\n        } else {\n            return super.isDeltaLogPath(normalizedPath); // only return true if in _delta_log folder\n        }\n    }\n\n    ////////////////////\n    // Static Helpers //\n    ////////////////////\n\n    /**\n     * ExternalLogStoreSuite sometimes uses \"failing:\" scheme prefix to inject errors during tests\n     * However, we want lookups for the same $tablePath to return the same result, regardless of\n     * scheme.\n     */\n    static String fixPathSchema(String tablePath) {\n        return tablePath.replace(\"failing:\", \"file:\");\n    }\n\n    static String createKey(String tablePath, String fileName) {\n        return String.format(\"%s-%s\", fixPathSchema(tablePath), fileName);\n    }\n\n    static ExternalCommitEntry get(Path path) {\n        final String tablePath = path.getParent().getParent().toString();\n        final String fileName = path.getName();\n        final String key = createKey(tablePath, fileName);\n        return hashMap.get(key);\n    }\n\n    static boolean containsKey(Path path) {\n        final String tablePath = path.getParent().getParent().toString();\n        final String fileName = path.getName();\n        final String key = createKey(tablePath, fileName);\n        return hashMap.containsKey(key);\n    }\n\n    static ConcurrentHashMap<String, ExternalCommitEntry> hashMap = new ConcurrentHashMap<>();\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/test/java/io/delta/storage/utils/ReflectionsUtilsSuiteHelper.java",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.utils;\n\nimport com.amazonaws.auth.AWSCredentials;\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport org.apache.hadoop.conf.Configuration;\n\npublic class ReflectionsUtilsSuiteHelper {\n    // this class only purpose to test DynamoDBLogStore logic to create AWS credentials provider with reflection.\n    public static class TestOnlyAWSCredentialsProviderWithHadoopConf implements AWSCredentialsProvider {\n\n        public TestOnlyAWSCredentialsProviderWithHadoopConf(Configuration hadoopConf) {}\n\n        @Override\n        public AWSCredentials getCredentials() {\n            return null;\n        }\n\n        @Override\n        public void refresh() {\n\n        }\n    }\n\n    // this class only purpose to test DynamoDBLogStore logic to create AWS credentials provider with reflection.\n    public static class TestOnlyAWSCredentialsProviderWithUnexpectedConstructor implements AWSCredentialsProvider {\n\n        public TestOnlyAWSCredentialsProviderWithUnexpectedConstructor(String hadoopConf) {}\n\n        @Override\n        public AWSCredentials getCredentials() {\n            return null;\n        }\n\n        @Override\n        public void refresh() {\n\n        }\n    }\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/test/scala/io/delta/storage/ExternalLogStoreSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage\n\nimport java.io.File\nimport java.net.URI\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs._\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport org.apache.spark.sql.delta.FakeFileSystem\nimport org.apache.spark.sql.delta.sources.DeltaSQLConf\nimport org.apache.spark.sql.delta.storage.LogStoreAdaptor\nimport org.apache.spark.sql.delta.util.FileNames\nimport org.apache.spark.sql.functions.col\n\n/////////////////////\n// Base Test Suite //\n/////////////////////\n\nclass ExternalLogStoreSuite extends org.apache.spark.sql.delta.PublicLogStoreSuite {\n  protected def shouldUseRenameToWriteCheckpoint: Boolean = false\n\n  override protected val publicLogStoreClassName: String =\n    classOf[MemoryLogStore].getName\n\n  protected override def beforeEach(): Unit = {\n    super.beforeEach()\n\n    MemoryLogStore.numGetLatestExternalEntryCalls = 0\n  }\n\n  testHadoopConf(\n    expectedErrMsg = \"No FileSystem for scheme \\\"fake\\\"\",\n    \"fs.fake.impl\" -> classOf[FakeFileSystem].getName,\n    \"fs.fake.impl.disable.cache\" -> \"true\"\n  )\n\n  def getDeltaVersionPath(logDir: File, version: Int): Path = {\n    FileNames.unsafeDeltaFile(new Path(logDir.toURI), version)\n  }\n\n  def getFailingDeltaVersionPath(logDir: File, version: Int): Path = {\n    FileNames.unsafeDeltaFile(new Path(s\"failing:${logDir.getCanonicalPath}\"), version)\n  }\n\n  test(\"#3423: listFrom only checks latest external store entry if listing a delta log file\") {\n    withTempDir { tempDir =>\n      val store = createLogStore(spark)\n          .asInstanceOf[LogStoreAdaptor].logStoreImpl\n          .asInstanceOf[MemoryLogStore]\n      val logDir = new File(tempDir.getCanonicalPath, \"_delta_log\")\n      logDir.mkdir()\n\n      val deltaFilePath = getDeltaVersionPath(logDir, 0)\n      val dataFilePath = new Path(tempDir.getCanonicalPath, \".part-00000-da82aeb5-snappy.parquet\")\n\n      val fs = deltaFilePath.getFileSystem(sessionHadoopConf)\n      fs.create(deltaFilePath).close()\n      fs.create(dataFilePath).close()\n\n      assert(MemoryLogStore.numGetLatestExternalEntryCalls == 0)\n\n      store.listFrom(deltaFilePath, sessionHadoopConf)\n      assert(MemoryLogStore.numGetLatestExternalEntryCalls == 1) // contacted external store\n\n      store.listFrom(dataFilePath, sessionHadoopConf)\n      assert(MemoryLogStore.numGetLatestExternalEntryCalls == 1) // did not contact external store\n    }\n  }\n\n  test(\"#3423: VACUUM does not check external store for latest entry\") {\n\n    // previous behaviour: always check external store for latest entry when listing\n    // current behaviour: only check external store for latest entry when listing a delta log file\n    def doVacuumTestGetNumVacuumExternalStoreCalls(usePreviousListBehavior: Boolean): Int = {\n      var ret = -1\n\n      withTempDir { tempDir =>\n        withSQLConf(DeltaSQLConf.DELTA_VACUUM_RETENTION_CHECK_ENABLED.key -> \"false\") {\n          spark.conf.set(\n            MemoryLogStore.IS_DELTA_LOG_PATH_OVERRIDE_KEY,\n            usePreviousListBehavior)\n\n          val path = tempDir.getCanonicalPath\n\n          spark.range(100)\n              .withColumn(\"part\", col(\"id\") % 10)\n              .write\n              .format(\"delta\")\n              .partitionBy(\"part\")\n              .save(path)\n\n          spark.sql(s\"DELETE FROM delta.`$path`\")\n\n          val numExternalCallsBeforeVacuum = MemoryLogStore.numGetLatestExternalEntryCalls\n\n          spark.sql(s\"VACUUM delta.`$path` RETAIN 0 HOURS\")\n\n          val numExternalCallsAfterVacuum = MemoryLogStore.numGetLatestExternalEntryCalls\n\n          ret = numExternalCallsAfterVacuum - numExternalCallsBeforeVacuum\n        }\n      }\n\n      ret\n    }\n\n    assert(\n      doVacuumTestGetNumVacuumExternalStoreCalls(true) >\n      doVacuumTestGetNumVacuumExternalStoreCalls(false)\n    )\n  }\n\n  test(\"#3423: BaseExternalLogStore::isDeltaLogPath\") {\n    val store = createLogStore(spark)\n        .asInstanceOf[LogStoreAdaptor].logStoreImpl\n        .asInstanceOf[MemoryLogStore]\n\n    // json file\n    assert(store.isDeltaLogPath(new Path(\"s3://bucket/_delta_log/0000.json\")))\n\n    // checkpoint file\n    assert(store.isDeltaLogPath(new Path(\"s3://bucket/_delta_log/0010.checkpoint.parquet\")))\n\n    // file listing prefix\n    assert(store.isDeltaLogPath(new Path(\"s3://bucket/_delta_log/0000.\")))\n\n    // delta_log folder (with / prefix)\n    assert(store.isDeltaLogPath(new Path(\"s3://bucket/_delta_log/\")))\n\n    // delta_log folder (without / prefix)\n    assert(store.isDeltaLogPath(new Path(\"s3://bucket/_delta_log\")))\n\n    // obvious negative cases\n    assert(!store.isDeltaLogPath(new Path(\"s3://bucket/part-000-UUID.parquet\")))\n\n    // edge cases of `_delta_log` in a folder\n    assert(!store.isDeltaLogPath(new Path(\"s3://bucket/__delta_log/\")))\n    assert(!store.isDeltaLogPath(new Path(\"s3://bucket/_delta_log_\")))\n  }\n\n  test(\"single write\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n      val path = getDeltaVersionPath(tempLogDir, 0)\n      store.write(path, Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n      val entry = MemoryLogStore.get(path);\n      assert(entry != null)\n      assert(entry.complete);\n    }\n  }\n\n  test(\"double write\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n      val path = getDeltaVersionPath(tempLogDir, 0)\n      store.write(path, Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n      assert(MemoryLogStore.containsKey(path))\n      assertThrows[java.nio.file.FileSystemException] {\n        store.write(path, Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n      }\n    }\n  }\n\n  test(\"overwrite\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n      val path = getDeltaVersionPath(tempLogDir, 0)\n      store.write(path, Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n      assert(MemoryLogStore.containsKey(path))\n      store.write(path, Iterator(\"foo\", \"bar\"), overwrite = true, sessionHadoopConf)\n      assert(MemoryLogStore.containsKey(path))\n    }\n  }\n\n  test(\"write N fails if overwrite=false and N already exists in FileSystem \" +\n    \"and N does not exist in external store\") {\n    withTempLogDir { tempLogDir =>\n      val delta0 = getDeltaVersionPath(tempLogDir, 0)\n      val delta1_a = getDeltaVersionPath(tempLogDir, 1)\n      val delta1_b = getDeltaVersionPath(tempLogDir, 1)\n\n      val store = createLogStore(spark)\n      store.write(delta0, Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n      store.write(delta1_a, Iterator(\"one_a\"), overwrite = false, sessionHadoopConf)\n\n      // Pretend that BaseExternalLogStore.getExpirationDelaySeconds() seconds have\n      // transpired and that the external store has run TTL cleanup.\n      MemoryLogStore.hashMap.clear();\n\n      val e = intercept[java.nio.file.FileAlreadyExistsException] {\n        store.write(delta1_b, Iterator(\"one_b\"), overwrite = false, sessionHadoopConf)\n      }\n\n      assert(e.getMessage.contains(delta1_b.toString))\n    }\n  }\n\n  test(\"write N fails and does not write to external store if overwrite=false and N \" +\n    \"already exists in FileSystem and N already exists in external store\") {\n    withTempLogDir { tempLogDir =>\n      val delta0 = getDeltaVersionPath(tempLogDir, 0)\n      val delta1_a = getDeltaVersionPath(tempLogDir, 1)\n      val delta1_b = getDeltaVersionPath(tempLogDir, 1)\n\n      val store = createLogStore(spark)\n      store.write(delta0, Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n      store.write(delta1_a, Iterator(\"one_a\"), overwrite = false, sessionHadoopConf)\n\n      assert(MemoryLogStore.hashMap.size() == 2)\n\n      val e = intercept[java.nio.file.FileAlreadyExistsException] {\n        store.write(delta1_b, Iterator(\"one_b\"), overwrite = false, sessionHadoopConf)\n      }\n\n      assert(e.getMessage.contains(delta1_b.toString))\n      assert(MemoryLogStore.hashMap.size() == 2)\n    }\n  }\n\n  test(\"write N+1 fails if N doesn't exist in external store or FileSystem\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n\n      val delta0 = getDeltaVersionPath(tempLogDir, 0)\n      val delta1 = getDeltaVersionPath(tempLogDir, 1)\n      val e = intercept[java.nio.file.FileSystemException] {\n        store.write(delta1, Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n      }\n      assert(e.getMessage == s\"previous commit $delta0 doesn't exist on the file system but does in the external log store\")\n    }\n  }\n\n  // scalastyle:off line.size.limit\n  test(\"write N+1 fails if N is marked as complete in external store but doesn't exist in FileSystem\") {\n    // scalastyle:on line.size.limit\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n\n      val delta0 = getDeltaVersionPath(tempLogDir, 0)\n      val delta1 = getDeltaVersionPath(tempLogDir, 1)\n\n      store.write(delta0, Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n      delta0.getFileSystem(sessionHadoopConf).delete(delta0, true)\n      val e = intercept[java.nio.file.FileSystemException] {\n        store.write(delta1, Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n      }\n      assert(e.getMessage == s\"previous commit $delta0 doesn't exist on the file system but does in the external log store\")\n    }\n  }\n\n  test(\"write N+1 succeeds and recovers version N if N is incomplete in external store\") {\n    withSQLConf(\n      \"fs.failing.impl\" -> classOf[FailingFileSystem].getName,\n      \"fs.failing.impl.disable.cache\" -> \"true\"\n    ) {\n      withTempLogDir { tempLogDir =>\n        val store = createLogStore(spark)\n\n        val delta0_normal = getDeltaVersionPath(tempLogDir, 0)\n        val delta0_fail = getFailingDeltaVersionPath(tempLogDir, 0)\n        val delta1 = getDeltaVersionPath(tempLogDir, 1)\n\n        // Create N (incomplete) in external store, with no N in FileSystem\n        FailingFileSystem.failOnSuffix = Some(delta0_fail.getName)\n        store.write(delta0_fail, Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n        assert(!delta0_fail.getFileSystem(sessionHadoopConf).exists(delta0_fail))\n        assert(!MemoryLogStore.get(delta0_fail).complete)\n\n        // Write N + 1 and check that recovery was performed\n        store.write(delta1, Iterator(\"one\"), overwrite = false, sessionHadoopConf)\n        assert(delta0_fail.getFileSystem(sessionHadoopConf).exists(delta0_fail))\n        assert(MemoryLogStore.get(delta0_fail).complete)\n        assert(MemoryLogStore.get(delta1).complete)\n      }\n    }\n  }\n\n  test(\"listFrom performs recovery\") {\n    withSQLConf(\n      \"fs.failing.impl\" -> classOf[FailingFileSystem].getName,\n      \"fs.failing.impl.disable.cache\" -> \"true\"\n    ) {\n      withTempLogDir { tempLogDir =>\n        val store = createLogStore(spark)\n        val delta0_normal = getDeltaVersionPath(tempLogDir, 0)\n        val delta0_fail = getFailingDeltaVersionPath(tempLogDir, 0)\n\n        // fail to write to FileSystem when we try to commit 0000.json\n        FailingFileSystem.failOnSuffix = Some(delta0_fail.getName)\n\n        // try and commit 0000.json\n        store.write(delta0_fail, Iterator(\"foo\", \"bar\"), overwrite = false, sessionHadoopConf)\n\n        // check that entry was written to external store and that it doesn't exist in FileSystem\n        val entry = MemoryLogStore.get(delta0_fail)\n        assert(!entry.complete)\n        assert(!delta0_fail.getFileSystem(sessionHadoopConf).exists(delta0_fail))\n\n        // Now perform a `listFrom` read, which should fix the transaction log\n        val contents = store.read(entry.absoluteTempPath(), sessionHadoopConf).toList\n        FailingFileSystem.failOnSuffix = None\n        store.listFrom(delta0_normal, sessionHadoopConf)\n\n        val entry2 = MemoryLogStore.get(delta0_normal)\n        assert(entry2.complete)\n        assert(store.read(entry2.absoluteFilePath(), sessionHadoopConf).toList == contents)\n      }\n    }\n  }\n\n  test(\"write to new Delta table but a DynamoDB entry for it already exists\") {\n    withTempLogDir { tempLogDir =>\n      val store = createLogStore(spark)\n\n      // write 0000.json\n      val path = getDeltaVersionPath(tempLogDir, 0)\n      store.write(path, Iterator(\"foo\"), overwrite = false, sessionHadoopConf)\n\n      // delete 0000.json from FileSystem\n      val fs = path.getFileSystem(sessionHadoopConf)\n      fs.delete(path, false)\n\n      // try and write a new 0000.json, while the external store entry still exists\n      val e = intercept[java.nio.file.FileSystemException] {\n        store.write(path, Iterator(\"bar\"), overwrite = false, sessionHadoopConf)\n      }.getMessage\n\n      val tablePath = path.getParent.getParent\n      assert(e == s\"Old entries for table $tablePath still exist in the external log store\")\n    }\n  }\n\n  test(\"listFrom exceptions\") {\n    val store = createLogStore(spark)\n    assertThrows[java.io.FileNotFoundException] {\n      store.listFrom(\"/non-existing-path/with-parent\")\n    }\n  }\n\n  test(\"MemoryLogStore ignores failing scheme\") {\n    withSQLConf(\n      \"fs.failing.impl\" -> classOf[FailingFileSystem].getName,\n      \"fs.failing.impl.disable.cache\" -> \"true\"\n    ) {\n      withTempLogDir { tempLogDir =>\n        val store = createLogStore(spark)\n        val delta0_normal = getDeltaVersionPath(tempLogDir, 0)\n        val delta0_fail = getFailingDeltaVersionPath(tempLogDir, 0)\n\n        store.write(delta0_fail, Iterator(\"zero\"), overwrite = false, sessionHadoopConf)\n        assert(MemoryLogStore.get(delta0_fail) eq MemoryLogStore.get(delta0_normal))\n      }\n    }\n  }\n}\n\n///////////////////////////////////\n// S3DynamoDBLogStore Test Suite //\n///////////////////////////////////\n\nclass S3DynamoDBLogStoreSuite extends AnyFunSuite {\n  test(\"getParam\") {\n    import S3DynamoDBLogStore._\n\n    val sparkPrefixKey = \"spark.io.delta.storage.S3DynamoDBLogStore.ddb.tableName\"\n    val basePrefixKey = \"io.delta.storage.S3DynamoDBLogStore.ddb.tableName\"\n\n    // Sanity check\n    require(sparkPrefixKey == SPARK_CONF_PREFIX + \".\" + DDB_CLIENT_TABLE)\n    require(basePrefixKey == BASE_CONF_PREFIX + \".\" + DDB_CLIENT_TABLE)\n\n    // Case 1: no parameters exist, should use default\n    assert(getParam(new Configuration(), DDB_CLIENT_TABLE, \"default_table\") == \"default_table\")\n\n    // Case 2: spark-prefix param only\n    {\n      val hadoopConf = new Configuration()\n      hadoopConf.set(sparkPrefixKey, \"some_other_table_2\")\n      assert(getParam(hadoopConf, DDB_CLIENT_TABLE, \"default_table\") == \"some_other_table_2\")\n    }\n\n    // Case 3: base-prefix param only\n    {\n      val hadoopConf = new Configuration()\n      hadoopConf.set(basePrefixKey, \"some_other_table_3\")\n      assert(getParam(hadoopConf, DDB_CLIENT_TABLE, \"default_table\") == \"some_other_table_3\")\n    }\n\n    // Case 4: both params set, same value\n    {\n      val hadoopConf = new Configuration()\n      hadoopConf.set(sparkPrefixKey, \"some_other_table_4\")\n      hadoopConf.set(basePrefixKey, \"some_other_table_4\")\n      assert(getParam(hadoopConf, DDB_CLIENT_TABLE, \"default_table\") == \"some_other_table_4\")\n    }\n\n    // Case 5: both param set, different value\n    {\n      val hadoopConf = new Configuration()\n      hadoopConf.set(sparkPrefixKey, \"some_other_table_5a\")\n      hadoopConf.set(basePrefixKey, \"some_other_table_5b\")\n      val e = intercept[IllegalArgumentException] {\n        getParam(hadoopConf, DDB_CLIENT_TABLE, \"default_table\")\n      }.getMessage\n      assert(e == (s\"Configuration properties `$sparkPrefixKey=some_other_table_5a` and \" +\n        s\"`$basePrefixKey=some_other_table_5b` have different values. Please set only one.\"))\n    }\n  }\n}\n\n////////////////////////////////\n// File System Helper Classes //\n////////////////////////////////\n\n/**\n * This utility enables failure simulation on file system.\n * Providing a matching suffix results in an exception being\n * thrown that allows to test file system failure scenarios.\n */\nclass FailingFileSystem extends RawLocalFileSystem {\n  override def getScheme: String = FailingFileSystem.scheme\n\n  override def getUri: URI = FailingFileSystem.uri\n\n  override def create(path: Path, overwrite: Boolean): FSDataOutputStream = {\n\n    FailingFileSystem.failOnSuffix match {\n      case Some(suffix) =>\n        if (path.toString.endsWith(suffix)) {\n          throw new java.nio.file.FileSystemException(\"fail\")\n        }\n      case None => ;\n    }\n    super.create(path, overwrite)\n  }\n}\n\nobject FailingFileSystem {\n  private val scheme = \"failing\"\n  private val uri: URI = URI.create(s\"$scheme:///\")\n\n  var failOnSuffix: Option[String] = None\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/test/scala/io/delta/storage/RetryableCloseableIteratorSuite.scala",
    "content": "package io.delta.storage\n\nimport java.io.{FileNotFoundException, IOException}\n\nimport scala.collection.JavaConverters._\n\nimport io.delta.storage.utils.ThrowingSupplier\nimport org.apache.hadoop.fs.s3a.RemoteFileChangedException\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass RetryableCloseableIteratorSuite extends AnyFunSuite {\n\n  private def getIter(\n      range: Range,\n      throwAtIndex: Option[Int] = None): CloseableIterator[String] =\n    new CloseableIterator[String] {\n      var index = 0\n      val impl = range.iterator.asJava\n\n      override def close(): Unit = { }\n\n      override def hasNext: Boolean = {\n        impl.hasNext\n      }\n\n      override def next(): String = {\n        if (throwAtIndex.contains(index)) {\n          throw new RemoteFileChangedException(s\"path -> index $index\", \"operation\", \"msg\");\n        }\n\n        index = index + 1\n\n        impl.next().toString\n      }\n    }\n\n  /**\n   * Fails at indices 25, 50, 75, 110.\n   *\n   * Provide a suitable input range to get the # of failures you want. e.g. range 0 to 100 will fail\n   * 3 times.\n   */\n  def getFailingIterSupplier(\n      range: Range,\n      failIndices: Seq[Int] = Seq.empty): ThrowingSupplier[CloseableIterator[String], IOException] =\n    new ThrowingSupplier[CloseableIterator[String], IOException] {\n      var numGetCalls = 0\n\n      override def get(): CloseableIterator[String] = {\n        if (numGetCalls < failIndices.length) {\n          val result = getIter(range, Some(failIndices(numGetCalls)))\n          numGetCalls = numGetCalls + 1\n          result\n        } else {\n          getIter(range)\n        }\n      }\n    }\n\n  test(\"simple case - internally keeps track of the correct index\") {\n    val testIter = new RetryableCloseableIterator(() => getIter(0 to 100))\n    assert(testIter.getLastSuccessfullIndex == -1)\n\n    for (i <- 0 to 100) {\n      val elem = testIter.next()\n      assert(elem.toInt == i)\n      assert(testIter.getLastSuccessfullIndex == i)\n    }\n\n    assert(!testIter.hasNext) // this would be index 101\n  }\n\n  test(\"complex case - replays underlying iter back to correct index after error\") {\n    // Here, we just do the simplest verification\n    val testIter1 = new RetryableCloseableIterator(\n      getFailingIterSupplier(0 to 100, Seq(25, 50, 75)))\n\n    // this asserts the size, order, and elements of the testIter1\n    assert(testIter1.asScala.toList.map(_.toInt) == (0 to 100).toList)\n\n    // Here, we do more complex verification\n    val testIter2 = new RetryableCloseableIterator(\n      getFailingIterSupplier(0 to 100, Seq(25, 50, 75)))\n\n    for (_ <- 0 to 24) { testIter2.next() }\n    assert(testIter2.getLastSuccessfullIndex == 24)\n    assert(testIter2.getNumRetries == 0)\n\n    assert(testIter2.next().toInt == 25) // this will fail once, and then re-scan\n    assert(testIter2.getLastSuccessfullIndex == 25)\n    assert(testIter2.getNumRetries == 1)\n\n    for (_ <- 26 to 49) { testIter2.next() }\n    assert(testIter2.getLastSuccessfullIndex == 49)\n    assert(testIter2.getNumRetries == 1)\n\n    assert(testIter2.next().toInt == 50) // this will fail once, and then re-scan\n    assert(testIter2.getLastSuccessfullIndex == 50)\n    assert(testIter2.getNumRetries == 2)\n\n    for (_ <- 51 to 74) { testIter2.next() }\n    assert(testIter2.getLastSuccessfullIndex == 74)\n    assert(testIter2.getNumRetries == 2)\n\n    assert(testIter2.next().toInt == 75) // this will fail once, and then re-scan\n    assert(testIter2.getLastSuccessfullIndex == 75)\n    assert(testIter2.getNumRetries == 3)\n\n    for (_ <- 76 to 100) { testIter2.next() }\n    assert(testIter2.getLastSuccessfullIndex == 100)\n    assert(!testIter2.hasNext)\n  }\n\n  test(\"handles exceptions while retrying\") {\n    // Iterates normally until index 50 (return [0, 49] successfully). Then fails.\n    // Tries to replay, but fails at 30\n    // Tries to replay again, but fails at 20\n    // Successfully replays to 49, starts returning results from index 50 (inclusive) again\n    val testIter1 =\n      new RetryableCloseableIterator(getFailingIterSupplier(0 to 100, Seq(50, 30, 20)))\n\n    assert(testIter1.asScala.toList.map(_.toInt) == (0 to 100).toList)\n\n    // Iterates normally until index 50 (return [0, 49] successfully). Then fails.\n    // Successfully replayed to 49, starts returning results from index 50 (inclusive)\n    // Fails at index 50 (returned [50, 69]). Tries to replay, but fails at 5\n    // Successfully replays until 69, then normally returns results from 70\n    val testIter2 =\n      new RetryableCloseableIterator(getFailingIterSupplier(0 to 100, Seq(50, 70, 5)))\n    assert(testIter2.asScala.toList.map(_.toInt) == (0 to 100).toList)\n  }\n\n  test(\"throws after maxRetries exceptions\") {\n    val testIter =\n      new RetryableCloseableIterator(getFailingIterSupplier(0 to 100, Seq(20, 49, 60, 80)))\n\n    for (i <- 0 to 79) {\n      assert(testIter.next().toInt == i)\n    }\n    assert(testIter.getNumRetries == 3)\n    val ex = intercept[RuntimeException] {\n      testIter.next()\n    }\n    assert(ex.getCause.isInstanceOf[RemoteFileChangedException])\n  }\n\n  test(\"can specify maxRetries\") {\n    val testIter1 =\n      new RetryableCloseableIterator(\n        getFailingIterSupplier(0 to 100, Seq(5, 10, 15, 20, 25, 30, 35, 40, 45, 50)),\n        10 // maxRetries\n      )\n\n    assert(testIter1.asScala.toList.map(_.toInt) == (0 to 100).toList)\n\n    val testIter2 =\n      new RetryableCloseableIterator(\n        getFailingIterSupplier(0 to 100, Seq(5, 10, 15, 20, 25, 30)),\n        5 // maxRetries\n      )\n\n    for (i <- 0 to 29) {\n      assert(testIter2.next().toInt == i)\n    }\n    assert(testIter2.getNumRetries == 5)\n    val ex = intercept[RuntimeException] {\n      testIter2.next()\n    }\n    assert(ex.getCause.isInstanceOf[RemoteFileChangedException])\n  }\n\n  test(\"retried iterator doesn't have enough data (underlying data changed!)\") {\n    val testIter = new RetryableCloseableIterator(\n      new ThrowingSupplier[CloseableIterator[String], IOException] {\n        var getCount = 0\n\n        override def get(): CloseableIterator[String] = getCount match {\n          case 0 =>\n            getCount = getCount + 1\n            getIter(0 to 100, Some(50)) // try to iterate 0->100, fail at 50\n\n          case 1 =>\n            getCount = getCount + 1\n            getIter(0 to 30) // try to replay 0 to 50, but no elements after 30!\n        }\n      }\n    )\n\n    for (_ <- 0 to 49) { testIter.next() }\n    val e = intercept[IllegalStateException] {\n      testIter.next()\n    }\n    assert(e.getMessage.contains(\"A retried iterator doesn't have enough data\"))\n  }\n\n  test(\"after replaying the iter, hasNext is false\") {\n    val testIter = new RetryableCloseableIterator(\n      new ThrowingSupplier[CloseableIterator[String], IOException] {\n        var getCount = 0\n\n        override def get(): CloseableIterator[String] = getCount match {\n          case 0 =>\n            getCount = getCount + 1\n            getIter(0 to 100, Some(50)) // try to iterate 0->100, fail at 50\n\n          case 1 =>\n            getCount = getCount + 1\n            // when we failed at index 50 above, the lastSuccessfulIndex was 49. here, we can\n            // replay back to index 49, but the `hasNext` call will be false!\n            getIter(0 to 49)\n        }\n      }\n    )\n\n    for (_ <- 0 to 49) { testIter.next() }\n    assert(testIter.getLastSuccessfullIndex == 49)\n\n    val e = intercept[IllegalStateException] {\n      testIter.next()\n    }\n    assert(e.getMessage.contains(\"A retried iterator doesn't have enough data (hasNext=false, \" +\n      \"lastSuccessfullIndex=49)\"))\n  }\n\n  test(\"throws FileNotFoundException (i.e. not UncheckedIOException) if file not found\") {\n    intercept[FileNotFoundException] {\n      new RetryableCloseableIterator(() => { throw new FileNotFoundException() })\n    }\n  }\n\n}\n"
  },
  {
    "path": "storage-s3-dynamodb/src/test/scala/io/delta/storage/utils/ReflectionsUtilsSuite.scala",
    "content": "/*\n * Copyright (2021) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage io.delta.storage.utils\n\nimport com.amazonaws.auth.EnvironmentVariableCredentialsProvider\nimport io.delta.storage.utils.ReflectionsUtilsSuiteHelper.TestOnlyAWSCredentialsProviderWithHadoopConf\nimport org.apache.hadoop.conf.Configuration\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass ReflectionsUtilsSuite extends AnyFunSuite {\n  private val emptyHadoopConf = new Configuration()\n\n  test(\"support AWS credentials provider with hadoop Configuration as constructor parameter\") {\n    val awsProvider = ReflectionUtils.createAwsCredentialsProvider(\n      \"io.delta.storage.utils.ReflectionsUtilsSuiteHelper\" +\n        \"$TestOnlyAWSCredentialsProviderWithHadoopConf\",\n      emptyHadoopConf\n    )\n    assert(\n      awsProvider.isInstanceOf[TestOnlyAWSCredentialsProviderWithHadoopConf]\n    )\n  }\n\n  test(\"support AWS credentials provider with empty constructor(default from aws lib)\") {\n    val awsProvider = ReflectionUtils.createAwsCredentialsProvider(\n      classOf[EnvironmentVariableCredentialsProvider].getCanonicalName,\n      emptyHadoopConf\n    )\n    assert(awsProvider.isInstanceOf[EnvironmentVariableCredentialsProvider])\n  }\n\n  test(\"do not support AWS credentials provider with unexpected constructors parameters\") {\n    assertThrows[NoSuchMethodException] {\n      ReflectionUtils.createAwsCredentialsProvider(\n        \"io.delta.storage.utils.ReflectionsUtilsSuiteHelper\" +\n          \"$TestOnlyAWSCredentialsProviderWithUnexpectedConstructor\",\n        emptyHadoopConf\n      )\n    }\n  }\n\n}\n"
  },
  {
    "path": "testDeltaIcebergJar/src/test/scala/JarSuite.scala",
    "content": "/*\n * Copyright (2023-present) The Delta Lake Project Authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.io.File\nimport java.net.JarURLConnection\nimport java.util.jar.JarFile\n\nimport scala.collection.JavaConverters._\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass JarSuite extends AnyFunSuite {\n\n  val allowedClassPrefixes = Seq(\n    // e.g. shadedForDelta/org/apache/iceberg/BaseTable.class\n    \"shadedForDelta/\",\n    // e.g. scala/collection/compat/immutable/ArraySeq.class\n    // e.g. scala/jdk/CollectionConverters.class\n    \"scala/\",\n    // e.g. org/apache/spark/sql/delta/icebergShaded/IcebergTransactionUtils.class\n    \"org/apache/spark/sql/delta/icebergShaded/\",\n    // Server-side planning support\n    \"org/apache/spark/sql/delta/serverSidePlanning/\",\n    // We explicitly include all the /delta/commands/convert classes we want, to ensure we don't\n    // accidentally pull in some from delta-spark package.\n    \"org/apache/spark/sql/delta/commands/convert/IcebergFileManifest\",\n    \"org/apache/spark/sql/delta/commands/convert/IcebergSchemaUtils\",\n    \"org/apache/spark/sql/delta/commands/convert/IcebergTable\",\n    // e.g. org/apache/iceberg/transforms/IcebergPartitionUtil.class\n    \"com/github/benmanes/caffeine/\",\n    \"org/apache/avro/\"\n  )\n\n  test(\"audit files in assembly jar\") {\n    // Step 1: load the jar (and make sure it exists)\n    // scalastyle:off classforname\n    val classUrl = Class.forName(\"org.apache.spark.sql.delta.icebergShaded.IcebergConverter\").getResource(\"IcebergConverter.class\")\n    // scalastyle:on classforname\n    assert(classUrl != null, \"Could not find delta-iceberg jar\")\n    val connection = classUrl.openConnection().asInstanceOf[JarURLConnection]\n    val url = connection.getJarFileURL\n    val jarFile = new JarFile(new File(url.toURI))\n\n    // Step 2: Verify the JAR has the classes we want it to have\n    try {\n      val jarClasses = jarFile\n        .entries()\n        .asScala\n        .filter(!_.isDirectory)\n        .map(_.toString)\n        .filter(_.endsWith(\".class\")) // let's ignore any .properties or META-INF files for now\n        .toSet\n\n      // 2.1: Verify there are no prohibited classes (e.g. io/delta/storage/...)\n      //\n      //      You can test this code path by commenting out the \"io/delta\" match case of the\n      //      assemblyMergeStrategy config in build.sbt.\n      val prohibitedJarClasses = jarClasses\n        .filter { clazz => !allowedClassPrefixes.exists(prefix => clazz.startsWith(prefix)) }\n\n      if (prohibitedJarClasses.nonEmpty) {\n        throw new Exception(\n            s\"Prohibited jar class(es) found:\\n- ${prohibitedJarClasses.mkString(\"\\n- \")}\"\n          )\n      }\n\n      // 2.2: Verify that, for each allowed class prefix, we actually loaded a class for it (instead\n      //      of, say, loading an empty jar).\n      //\n      //      You can test this code path by adding the following code snippet to the delta-iceberg\n      //      assemblyMergeStrategy config in build.sbt:\n      //      case PathList(\"shadedForDelta\", xs @ _*) => MergeStrategy.discard\n\n      // Map of prefix -> # classes with that prefix\n      val allowedClassesCounts = scala.collection.mutable.Map(\n        allowedClassPrefixes.map(prefix => (prefix, 0)) : _*\n      )\n      jarClasses.foreach { clazz =>\n        allowedClassPrefixes.foreach { prefix =>\n          if (clazz.startsWith(prefix)) {\n            allowedClassesCounts(prefix) += 1\n          }\n        }\n      }\n      val missingClasses = allowedClassesCounts.filter(_._2 == 0).keys\n      if (missingClasses.nonEmpty) {\n        throw new Exception(\n          s\"No classes found for the following prefix(es):\\n- ${missingClasses.mkString(\"\\n- \")}\"\n        )\n      }\n    } finally {\n      jarFile.close()\n    }\n  }\n}\n"
  },
  {
    "path": "version.sbt",
    "content": "ThisBuild / version := \"4.1.0-SNAPSHOT\"\n"
  }
]